美文网首页C++
C/C++:指针初学

C/C++:指针初学

作者: 埠默笙声 | 来源:发表于2017-02-20 20:29 被阅读62次

    整理自计蒜客-CS 112: C++ 程序设计

    找不到的 是心头的悸动

    指针是什么

    指针是一个变量,其储存的是值的地址,而不是值本身。指针提供了另一种访问内存空间的方法:虽然我们不知道变量的名称,但我们可以通过变量存放的地址访问它。


    指针的初始化

    • 可以直接初始化:
    //<数据类型> *指针变量名 = 赋值;
    //指针的数据类型,表示指针所指向的数据的数据类型
    int a = 1;
    int array[] = { 1,2,3,4,5 }; 
    int *p1 = &a;
    int *p2 = array; 或者 int *p2 = &array[0]
    

    如果int *p2 = &array;将不能通过编译,原因是数据类型不匹配。

    • 也可以先定义,再赋值:
    //对象指针:
    Node node1;
    Node *p3 = &node1;
    //函数指针:
    int add(int x,int y);
    int (*p3)(int,int);
    p3 = add;
    

    指针的运算

    • 算数运算:
      指针可以跟 整数 进行加法和减法的运算,但是运算规则比较特殊——对指针进行加减运算的结果,与指针本身的类型密切相关。可以看出,指针+1移动了一种数据类型在内存中存放的字节数。不过,空指针和函数指针是不能进行算数运算的(无法通过编译),因为无法确定移动多少个字节。
    //整数指针:
    int *p1 = &a;
    cout << p1 << " " << p1+1 <<endl;
    //字符指针:
    char c[] = "people";
    char *p2 = &c;
    cout << p2 << " " << p2+1 <<endl; 
    printf("%p %p\n",p2,p2+1); 
    //对象指针:
    Node *p3 = &node;
    cout << p3 << " " << p3+1 <<endl;
    //函数指针:
    int (*p4)(int,int) = add;
    printf("%p\n",p4);
    cout << p4 <<endl;
    

    输出结果:
    0x7ffe597f872c 0x7ffe597f8730 people eople 0x7ffe597f8760 0x7ffe597f8761 0x7ffe597f8750 0x7ffe597f8760 0x4009ed 1
    1)以上使用cout输出字符指针的时候结果跟想象中不太一样?
    答:这是因为cout对象认为char的地址是字符串的地址,因此它打印该地址处的字符,然后继续打印后面的字符,直到遇到空字符(\0)为止。
    2)为什么用cout输出函数指针会得到1呢?
    答:这里先挖个坑...

    • 关系运算&逻辑运算
      指针变量的关系运算,指的是 指向相同类型数据的指针之间,进行的关系运算。 如果两个相同类型的指针相等,就表示两个指针指向的是同一个地址——不同类型的指针之间,或者指针与非 0 整数之间的比较是没有意义的。
      但是有一种情况是特殊的——指针可以跟 整数 0 之间进行比较,0专门用于表示空指针,即指针变量中保存的地址是空的,不指向任何有效的地址。
      关系运算的结果经常用于逻辑运算。
        int a = 9,b = 7;
        int *p[4] = {&a,&a,&b,NULL};
        //判断前后指针是否相等
        cout << "equal?" <<endl;
        for(int i=0;i<4;i++){
            if(p[i]==p[i+1]) {
                cout << "YES ";
            }
            else cout << "NO ";
        }
        cout <<endl;
        //判断是否为空指针
        cout << "NULL?" <<endl;
        for(int i=0;i<4;i++){
            if(p[i]==NULL) {
                cout << "YES " <<endl;
            }
            else cout << "NO ";
        }
        cout << endl;
    

    输出结果:
    equal? YES NO NO YES NULL? NO NO NO YES
    值得注意的是,越界的指针数组元素p[4]是一个空指针。


    空指针

    为什么我们需要空指针呢?因为有的时候,我们在声明一个指针的时候,并没有一个确定的地址值可以赋给它,当程序运行到某个时刻的时候,才会将某个地址赋值给这个指针。这样,在指针定义但没有使用的这段时间里,它的值是不确定的——要是误用了这个不确定的指针的话,就很有可能会造成不可预见的错误(比如意外地把某个不该变更的值给改掉了),因此在这种情况下,我们首先应该将地址设置为空。

    除了给指针赋值0NULL使其为空,在C++11标准中,我们还可以使用nullptr关键字来表示空指针,用法跟NULL基本相同(需要引用命名空间std中的对应标识符)。


    指针与数组

    数组的本质,实际上是一串连续的相同大小的内存空间——比如说,对于整形数组int a[10],它在内存中就是连续排列的十个可以容纳一个整形变量的内存空间。而数组的名称,其实就是一个常量指针,即不能被赋值的指针。作为一个指针,数组名指向的是数组的第一个元素。

    指针加减运算的特点,使得它可以特别被用于处理存储在一段连续内存空间中的同类数据。而数组正好是具有一定顺序关系的,若干同类型变量的集合体——数组元素的存储,在物理上与逻辑上都是连续的,数组名就是变量的首地址。如果有数组array[5],那么array&array[0]是相同的。

    • 要访问数组元素,下面两种方法是等效的:
    int *p = array;
    cout << array[10] <<endl;
    cout << *(p+10) <<endl;
    
    • 此外,如果我们要把数组作为函数的形参的话,那么它实际上是等价于把指向数组元素类型的指针作为形参的——例如,下面三个写法,出现在形参列表中就是等价的:
    void f(int p[]);
    void f(int p[3]);
    void f(int *p);
    

    指针数组

    如果一个数组的所有元素都是指针变量,那么这就是一个指针数组。指针数组的每一个元素都必须是同一类型的指针。指针数组有一个神奇的应用:

    //创建一个指针数组,其元素分别指向三个数组
    int line1[]={1,0,0};
    int line2[]={0,1,0};
    int line3[]={0,0,1};
    int *pLine[3]={line1,line2,line3};
    
    //用类似二维数组的形式访问三个数组
    for(int i=0;i<3;i++){
        for(int j=0;j<3;j++){
          cout << pLine[i][j] << “ ”;
        }
    }
    

    输出结果:
    1 0 0 0 1 0 0 0 1

    上个例子中的pLine在使用上跟一个二维数组没有区别,但是在存储方式上,它跟真正的二维数组并不相同:

    二维数组在内存中,是以行优先的方式按照一维顺序关系存放的。因此,对于二维数组,可以将其理解成一个一维数组的一维数组,其首地址为数组名,元素个数就是行数——而它的每一个元素,就是一个一维数组。

    然而,对于指针数组pLine,它的三个“元素数组”在内存中,并不是连续存放的——访问line2或者line3的时候,首先要在pLine中找出对应的元素指针,即为指向line2或者line3头元素的地址,然后再通过指针跳转到要访问的数组。

    使用指针数组的情形

    对象指针

    跟基本类型的变量一样,每一个对象在初始化之后,都会在内存中占据一定的空间——所以我们同样也可以通过地址来访问一个对象。尽管对象同时包含了数据和函数两种成员,但是对象所占据的内存空间只用于存放数据成员——函数成员并不在每一个对象的存储副本之中。对象指针就是用于存放对象地址的变量——对象指针遵循一般变量指针的各种规则。

    • 通过对象名,我们可以访问对象成员——同样,通过对象指针,我们可以访问对象的成员,以下三种方法完全等价:
    //假设已有Line类
    cout << line1.getLength() <<endl;
    cout << line_ptr->getLength() <<endl;
    cout << (*line_ptr).getLength() <<endl;
    
    • this指针
      对于类的成员函数来说,我们可以直接在函数体内访问成员变量——例如,如果对象Line有一个成员变量length的话,那么我们就可以直接在成员函数内访问这个成员:
    int getLength(){return length;}
    

    而实际上,C++ 为每一个类的非静态成员函数(就是没有static关键字的成员函数),都提供了一个隐含的指针this,当我们写下return length;的时候,编译器执行的实际上是return this->length;
    this指针明确地指出了函数当前所操作的数据所属的对象——它是成员函数隐藏的一个形参,当我们在成员函数中操作对象的数据成员的时候,我们其实就是在使用this指针。
    然在一般情况下,我们不需要特别把this指针写出来——但是如果函数的形参列表中的参数跟成员变量重名的话,那么由于标识符作用域覆盖,我们将没法直接通过成员变量名来访问它。当然我们也可以选择更改形参名——但是更好的办法是通过this指针来访问成员变量,这样我们可以让代码拥有更好的可读性:

    void setLength(int length){
        this->length=length;
    }
    

    另一种解决方法是使用初始化列表:

    void setLength(int length):length(length){
    }
    

    函数指针

    以上我们使用的指针都是指向数据的——而在程序运行的时候,不仅数据要占据内存空间,执行程序的代码也会被存入到内存,并占据一定的空间。每一个函数都有函数名,而实际上这个函数名就表示函数的代码在内存中的起始地址。在程序中可以像使用函数名一样,使用指向函数的指针来调用函数——也就是说,一旦函数指针指向了某个函数,那么它与函数名就具有同样的作用。

    函数名在表示函数代码起始地址的同事,也包括函数的返回值类型,以及参数的个数、类型、排列次序等信息。因此,在通过函数名调用函数的时候,编译器就可以自动检查实参与形参是否相符,用函数的返回值参与其他运算时,能够自动进行类型一致性检查。而函数指针也具有同样的效果。

    • 声明:
      声明一个函数指针时,需要提供构造一个函数需要的所有信息——包括函数的返回值和形式参数列表,如下所示:
    返回值类型 (* 函数指针名)(形参表)
    

    由于对函数指针的定义在形式上比较复杂,如果在程序中出现多个这样的定义,那么多次重复这样的定义会相当繁琐。这里我们有一种很方便的解决方案——使用typedef。例如:

    typedef int (* DoubleIntFunction)(double);
    

    这里我们声明了DoubleIntFunction为“有一个double形参,返回类型为int的函数的指针”的类型的别名——接下来,如果我们需要声明这个类型的变量的时候,我们就可以直接进行使用:

    DoubleIntFunction funcPtr;
    

    这样我们就可以直接使用这个类型的指针funcPtr了。

    • 赋值:
    函数指针名=函数名;
    

    注意这里的“函数名”必须是一个已经声明过的函数,并且必须具有跟函数指针相同返回类型跟相同参数表的函数。赋值之后你就可以像使用函数一样,使用函数指针了。

    • 使用:
    int add(int x,int y) {
        return x+y;
    }
    int (*func_ptr)(int,int);
    func_ptr = add;
    //以下二者完全等价
    cout << func_ptr(2,3) <<endl;
    cout << add(2,3) <<endl;
    
    • C++ 11 提供的lambda 表达式,它可以替代函数指针的作用。

    动态内存分配

    动态内存解决了诸如“用户输入XX个数据,那么我应该开多大的数组?”之类的只能在程序运行时才能确定的问题。那跟指针有什么关系呢?这是因为我们申请的动态内存时,返回的就是指向这个这个动态内存首地址的指针。

    在 C++ 中,动态内存分配可以保证程序在运行的过程中,可以按照实际需要申请适量的内存,等到使用结束之后我们还可以将其释放——这种在程序运行的过程中申请和释放的存储单元也称为堆对象,而动态内存分配所调用的内存空间则称为堆内存。建立和删除堆对象使用以下两个运算符:newdelete

    • new的功能是动态分配内存,其语法形式如下所示:
    new 数据类型(初始化参数列表);
    

    以上语句的作用是在程序运行的过程中,申请分配用于存放指定类型数据的内存空间,然后根据参数列表中给出的值来进行初始化。如果内存申请成功,那么new运算符就会返回一个指向新分配内存区域首地址的指针——我们可以通过这个指针来访问堆对象。

    • 例如我们申请一个int类型的内存空间:
    int *point;
    point=new int(2);
    

    以上,系统动态分配了用于存放int类型数据的内存空间,然后用初始值2赋值,得到的地址返回给point指针变量。我们也可以这么写,但注意区别:

    int *point = new int;//没有初值
    int *point = new int();//初始值为0
    
    • 还可以解决“开多大数组?”的问题:
    cin >> n;
    int *a=new int[n];
    

    其中,方括号内的表达式表示数组长度,它可以是任何能够得到正整数值的式子。

    • 除了数组类型跟基本类型之外,new运算符还可以建立一个类的实例对象:
    //假设已有类Node
    Node *node_ptr;
    node_ptr = new Node();
    

    如果要建立一个对象的话,那么这里的“参数列表”就要跟对象所属类的构造函数一一对应:如果不写括号或者括号里为空的话,那么就会调用类的默认构造函数;而如果写了对应的参数的话就会调用类所具有的对应的构造函数

    • delete的作用是删除一个用new建立的对象,回收其申请的内存。
      所有用new分配的内存,都必须使用delete进行回收,否则会导致动态分配的内存无法回收,造成内存泄露!另外,deletenew是一一对应的,不能delete一个不是用new建立的对象,否则会出现“段错误”之类的问题。
      使用方法比较简单——如果你觉得一个堆对象已经不再被需要,那么你直接将其删除即可,如下所示:
    delete node_ptr;//对于基本类型或者对象的指针
    delete[] array_ptr;//对于指向数组的指针
    

    注意如果要删除的是一个数组的话,那么后面的那对方括号不可省略

    相关文章

      网友评论

        本文标题:C/C++:指针初学

        本文链接:https://www.haomeiwen.com/subject/qjpkwttx.html