您的位置:首页 > 编程语言 > C语言/C++

C++学习笔记(三)-- 指针和引用

2020-01-12 11:33 148 查看

C++ 学习笔记(三)

  • 7.3 指针与数组
  • 7.4 指针的运算
  • 7.5 指向指针的指针
  • 7.6 const 指针
  • 7.7 堆内存分配
  • 7.8 引用
  • 参考资料:面向对象程序设计–赵宏

    本笔记石墨文档链接

    第七章 指针和引用

    7.1 指针的概念

    声明任何一个变量,系统都会为其分配一定大小的内存,访问变量实际上是访问其所占据的内存空间。比如:
    int a=10;
    系统在内存中为 a 分配了 sizeof(int)=4的空间,并且该片内存中所存储的数据为10;a 所占据的首地址(即起始地址)可通过”&a"获取,其中“&"称为取址运算符。
    cout<<a;
    系统将变量a 所占据的内存空间中存储的数据取出来并显示在屏幕上。
    指针是用于存放内存地址的一种数据类型。
    指针变量 p=&a;

    7.2 指针变量的声明,初始化和访问

    7.2.1 指针变量的声明

    指针变量也是一种变量,使用之前需要先声明,声明形式为:
    *数据类型 变量名;
    其中,数据类型表示指针变量所指向的数据的类型,”*“表示所要声明的变量为一个指针变量,而不是普通变量。
    注意:要同时定义两个指针变量p1和p2,需要写成如下形式:
    int *p1,*p2;
    如果写成 int *p1,p2; 则表示声明了一个指针变量和一个普通变量。

    7.2.2 指针变量的初始化

    与普通变量一样,在声明指针变量时,可以对其进行初始化。形式为:
    *数据类型 变量名=地址表达式;
    地址表达式一般有三种形式:

    1. 初始化为 NULL 或0:NULL 为系统定义的一个常量,其值为0,表示指针变量指向的是一片无效的不可访问的内存。

    int *p=NULL;

    1. 初始化为已声明变量的地址:将一个已声明变量的地址作为指针变量的初值,此时通过该指针变量可以直接操作已声明变量占据的内存。

    int a;
    int *p=&a;

    1. 初始化为某一动态分配内存空间的地址。

    注意:

    1. 在访问指针变量所指向的内存时,要求该片内存必须是有效的可以访问的,当一个指针变量指向无效内存时,应将其赋值为 NULL (注意大写)或0。在访问指针变量所指向内存之前,应判断所指向内存是否有效:

    if(p1!=NULL)
    {
    //访问p1指向的内存
    }

    1. 指针变量的声明类型与其所指向的变量的类型必须一致,否则就要给出显式的强制类型转换。

    int a;
    int *p1=&a; //正确
    char *p2=&a; //错误,指针类型与指向变量类型不一致
    char p3=(char)&a; //正确,通过强制转换,将 int型地址转换为char型,不建议使用

    7.2.3 指针变量的访问

    声明指针变量并保证其指向一片有效的内存地址后,可以通过指针变量访问内存中的数据。
    其访问形式为:
    *指针变量名

    #include<iostream>
    using namespace std;
    int main()
    {
    int a=9,b=5;
    int *p1=&a,*p2=&b;
    cout<<a<<" "<<b<<" "<<*p1<<" "<<*p2<<endl;
    *p1=8;
    *p2=2;
    cout<<a<<" "<<b<<" "<<*p1<<" "<<*p2<<endl;
    a=3;
    b=6;
    cout<<a<<" "<<b<<" "<<*p1<<" "<<*p2<<endl;
    *p1=*p2;
    cout<<a<<" "<<b<<" "<<*p1<<" "<<*p2<<endl;
    p1=p2;
    *p1=7;
    cout<<a<<" "<<b<<" "<<*p1<<" "<<*p2<<endl;
    return 0;
    }

    提示:

    1. 至今为止,”*“运算符有三种情况:

    (1)乘法运算符;
    (2)声明指针变量;
    (3)访问指针变量所指向的内存:当出现在非变量声明语句中,且前面没有操作数,后面有一个指针变量时,表示访问紧随其后的指针变量所指向的内存。此时”*“被称为间接访问运算符,或取内容运算符,其与取地址运算符”&“功能相反。

    1. 程序中既可以修改指针变量指向内存的数据,也可以修改指针变量所指向的内存地址。

    *p1=*p1; //修改内容
    p1=p2; //修改地址。

    7.3 指针与数组

    将数组所占据内存空间的首地址赋给指针后,可以使用指针访问数组中的元素。

    7.3.1 用指针操作一维数组

    步骤:

    1. 定义相同类型的数组和指针变量;
    2. 将指针变量指向数组首地址;
    3. 使用指针变量访问数组元素。
    #include<iostream>
    using namespace std;
    int main()
    {
    int a[]={1,2},b[]={5,6,7};
    int *p=NULL;
    p=a;
    cout<<p[0]<<" "<<p[1]<<endl;
    p=&b[0];
    cout<<p[0]<<" "<<p[0]<<endl;
    return 0;
    }

    提示:

    1. 将数组首地址赋给指针变量后,可以使用指针代替数组名去进行访问数组元素;
    2. 取一维数组的首地址:数组名或者&数组名[0].

    代码中 &数组名 也可以

    7.3.2 用指针操作二维数组

    可使用两种不同类型的指针变量操作二维数组。
    (1)使用指向行的指针变量操作二维数组。
    对于二维数组来说,数组名表示数组第0行的首地址,即对于数组a[3][2]来说,a 与 &a[0]等价。指针变量的类型必须与其所存储的地址类型一致。使用数组名或&数组名[0]为指针赋值时,要求该指针必须声明为一个指向行的指针变量。指向行的指针变量声明形式为:
    *数据类型 (指针变量名)[行长度];

    #include<iostream>
    using namespace std;
    int main()
    {
    int a[][2]={1,2},b[][2]={3,4};
    int (*p)[2]=NULL;
    p=a;
    cout<<p[0][0]<<" "<<p[0][1]<<endl;
    p=&b[0];
    cout<<p[0][1]<<" "<<p[0][1]<<endl;
    return 0;
    }

    除此之外,还可以:

    #include<iostream>
    using namespace std;
    int main()
    {
    int a[][2]={{1,2},{5,6}},b[][2]={{3,4}}; //注意只有一行的二维数组的声明
    int (*p)[2]=NULL;
    p=a;
    cout<<p[0][0]<<" "<<p[1][1]<<endl; //指向行的指针可以访问数组中任意一个元素
    p=&b[0];
    cout<<p[0][0]<<" "<<p[0][1]<<endl;
    return 0;
    }

    (2)使用指针变量操作二维数组
    二维数组的元素在内存中按照先行后列的顺序连续排放,其存放方式与含有同样元素数目的一维数组完全一样,可以将MN大小的二维数组看成 包含MN 个元素的一维数组。

    #include<iostream>
    using namespace std;
    int main()
    {
    int a[][2]={1,2,3,4},b[][2]={4,3,2,1};
    int *p=NULL;
    p=a[0];
    cout<<p[0]<<" "<<p[1]<<" "<<p[2]<<" "<<p[3]<<endl;
    p=&b[0][0];
    cout<<p[0]<<" "<<p[1]<<" "<<p[2]<<" "<<p[3]<<endl;
    return 0;
    }

    提示:

    1. 使用一级指针变量p 操作二维数组a[M]
      时,要访问元素 a[m]
      ,则应该写为 p[m*N+n].
    2. a[0]与 &a[0][0]等价,都表示二维数组第一个元素的首地址。

    7.3.3 数组名与指针变量的区别

    数组名表示数组的首地址,指针变量可以指向数组的首地址,数组名和指针变量有什么区别吗?
    数组名也可以看做是一个指针,只不过是一个指针常量。因此数组名虽然可以用于表示数组的首地址,但是其值在程序中不能发生变化。
    指针变量指向的地址可在程序中根据需要修改。
    因此,指针变量可以作为赋值语句的左值,数组名不可以。

    int array[]={1,2,3};
    int a[3];
    int *p=NULL;
    a=array;   //错误。数组名不能作为赋值语句的左值。
    p=array;   //正确。

    二维数组中数组名可以作为数组的首地址,数组名[m]可以表示行地址。

    int array={1,2,3};
    int a[2][3]={1,2,3,4,5,6};
    int b[2][3];
    b=a;          //错误。
    b[0]=array;   //错误。

    对于数组来说,只有其中的元素可以进行修改,数组首地址、行地址等在声明数组时就已经确定,不能修改。

    7.3.4 指针数组

    指针是一种数据类型,所以可以创建一个指针类型的数组。指针数组同样可以有不同的维数,一维指针数组的声明形式为:
    *数据类型 数组名[长度];
    指针数组中每个元素都是指向同一数据类型的指针变量,指针数组元素的访问方法与一般数组元素的访问方法完全一样。

    #include<iostream>
    using namespace std;
    int main()
    {
        int a[2]={1,2};
        int *p[2];
        p[0]=a;
        p[1]=&a[1];
        cout<<a<<" "<<&a[1]<<endl;
        cout<<p[0]<<" "<<p[1]<<endl;
        return 0;
    }

    7.4 指针的运算

    指针作为一种数据类型,也可以进行运算,包含:指针加减整数、两个指针相减、两个指针做关系运算。(指针存储的是内存地址,对其进行加减乘除没有任何意义)

    7.4.1 指针加减整数

    设 p 为指针变量,n 为一个整数,指针加减运算后得到的结果仍然是一个地址,指针加减运算共包含以下六种形式:

    1. p+n

    从 p 指向的地址开始后面第 n 项数据的地址.
    char 型:1 个字节,p和 p+n之间差 n个字节的内存;
    short 型:2 个字节,p和 p+n之间差 n2个字节的内存;
    int long float 型:4 个字节,p和 p+n之间差 n4个字节的内存;
    double 型:8 个字节,p和 p+n之间差 n*8个字节的内存;

    #include<iostream>
    using namespace std;
    int main()
    {
        int a[2]={1,2};
        
        int *p[2];
        p[0]=a;
        p[1]=p[0]+1;
        cout<<a<<" "<<&a[1]<<endl;
        cout<<p[0]<<" "<<p[1]<<endl;
        return 0;
    }

    提示:
    (1)令 p 指向数组 a 第 m 项的地址,那么 p+n 指向第(m+n)项数据的地址。
    (2)令 p 指向数组元素的首地址,那么(p+n)指向数组 a 第 n项数据的地址,通过*(p+n)可以访问 a
    。有如下两种等价关系成立:
    a
    , p
    , *(a+n), *(p+n)等价;
    &a
    , &p
    , (a+n), (p+n)等价。
    (3)不同内存的使用情况下输出数组中各元素的内存地址会有所不同,但相邻两个元素之间的地址间隔固定。

    #include<iostream>
    using namespace std;
    int main()
    {
    int a[]={1,2,3,4};
    int *p=NULL;
    p=a;
    for(int i=0;i<sizeof(a)/sizeof(int);i++)
    {
    cout<<"a["<<i<<"]的地址为"<<p+i<<",其中的内容是:"<<*(p+i)<<endl;
    }
    return 0;
    }
    1. p-n

    从 p 指向的地址开始前面第 n 项的地址。

    1. p++

    先获取当前 p 的值进行表达式运算,然后通过 p=p+1将 p+1的值赋给 p,使得 p 指向下一项数据的地址。

    1. p–

    先获取当前 p 的值进行表达式运算,然后通过 p=p-1将 p-1的值赋给 p,使得 p 指向前一项数据的地址。

    1. ++p

    通过 p=p+1将 p+1的值赋给 p,使得 p 指向下一项数据的地址,然后进行表达式运算。

    1. –p

    通过 p=p-1将 p-1的值赋给 p,使得 p 指向前一项数据的地址,然后进行表达式运算。

    #include<iostream>
    using namespace std;
    int main()
    {
    int a[]={1,2};
    int *p=NULL;
    p=a;
    *(p++)=3;
    cout<<a[0]<<" "<<a[1]<<endl;
    p=a;
    *(++P)=4;
    cout<<a[0]<<" "<<a[1]<<endl;
    return 0;
    }

    “*(p++)=3;” 相当于
    p=3;
    p++;
    "(++p)=4;" 相当于
    p++;
    *p=4;

    7.4.2指针相减运算

    假设p1、p2 为同一类型的变量指针,通过"p1-p2"能够得到p1 和 p2 之间的数据项的数目,当 p1 指向的地址在 p2 的前面时,结果为负值,否则为正值。

    #include<iostream>
    using namespace std;
    int main()
    {
    int a[]={1,2};
    int *p1=NULL, *p2=NULL;
    p1=&a[0];
    p2=&a[1];
    cout<<p1-p2<<" "<<p2-p1<<endl;
    return 0;
    }

    7.5 指向指针的指针

    前面学习的指针指向的内存存储的是非指针类型的数据,这样的指针称为一级指针。指针变量也是一种变量,因此声明一个指针变量后,系统也要为其分配内存空间来存储指针变量的值。那么可以再声明一个指针,用它指向存储一级指针变量的内存,这种指向指针的指针就被称为二级指针,同样的还有三级指针等等。
    二级指针变量的声明方式为:
    **数据类型 变量名;
    使用二级指针操作二维数组

    #include<iostream>
    using namespace std;
    int main()
    {
        int i,j,a[][2]={1,2,3,4,5,6};
        int *p[3];     //声明一个指针数组,大小为3
        int **pp;
        for(i=0;i<3;i++)
        {
            p[i]=a[i];
        }
        pp=p;
        for(i=0;i<3;i++)
        {
            for(j=0;j<2;j++)
                cout<<*(*(pp+i)+j)<<" ";
            cout<<endl;
        }
        return 0;
    }

    ((pp+i)+j)可以写成(pp[i]+j)或者 pp[i][j];*
    pp 是指向 指针数组 p 的首地址 的指针,pp 是指针数组p 的首地址中的内容,即是 a[0]的地址,因此(pp)是 a[0]的内容,a[1]的地址是 a[0]的地址+1,因此 a[1]的内容是(pp+1).a[2]的地址是指针数组 p[1],pp+1即是 p[1]的地址,p1 的内容是(pp+1),因此 a[2]的内容是*(*(pp+1))。

    7.6 const 指针

    由于符号常量的值不能被修改,通过加上 const 修饰词限制对指针的赋值操作。根据对不同复制操作的限制,const 指针分为指针常量和常量指针。

    7.6.1 常量指针

    常量指针的声明形式为:
    *const 数据类型 变量名;
    常量指针所指向的内存地址可以改变,但通过常量指针只能从内存读取数据,但不能修改内存中的数据。

    int a=10,b=20;
    const int c=30;
    const int *p=&a;
    cout<<p<<endl;
    *p=15;   //错误
    a=15;    //正确
    *p=&b;   //正确,可以更改变量指针指向的内存。
    *p=&c;   //正确,因为常量指针不能修改内存,因此可以将符号常量的地址赋给指针常量。

    7.6.2 指针常量

    指针常量的声明形式:
    *数据类型 const 常量名;
    指针常量是一个常量而不是变量,因此,指针常量指向的内存是固定的不能改变。但通过指针常量可以修改指向的内存的数据。

    int a=10,b=20;
    int *const p=&a;
    *p=30;      //正确。
    p=&b;       //错误,不能修改指针常量指向的内存地址
    cout<<a<<endl;

    可以将一个指针同时声明为指针常量和常量指针,此时既不能改变指针指向的数据地址,也不能通过指针修改内存中的数据。

    int a=10,b=20;
    const int *const p=&a;
    *p=20;  //错误
    p=&b;   //错误

    7.7 堆内存分配

    在第六章学习数组时,声明数组前必须先确定数组的长度,但是我们可能无法预先知道要处理的数据量有多大。只能使数组定义的足够大,这样会造成内存的浪费。因此我们引入堆内存分配的概念。
    堆是内存中的一片空间,它允许程序运行时从中申请某一长度的内存空间。堆内存分配就是动态内存分配,是指程序运行时从堆中为指针变量申请实际需要的内存空间,并且当这片内存空间不再使用时,可以将这片堆内存及时释放。
    使用 new 和 delete 两个关键字完成堆内存分配和释放的操作,堆内存分配 new 的用法为:
    new 数据类型[表达式];
    注意:

    1. 使用 new 分配内存时,必须使用 delete 释放,不然会造成内存泄漏(即内存空间一直处于被占用状态,导致其他程序无法使用)。系统出现大量内存泄漏时会导致系统可用资源减少,计算速度变慢。
    2. 在使用 new 分配堆内存时,要注意区分[],().[]中的表达式指定了元素数目,而()中的表达式指定了内存的初值。
    int a=3;
    int *p1=new int[a];    //分配了3个int型元素大小的内存空间
    int *p2=new int(a);    //分配了 1 个 int型元素大小的内存空间,且初值为3.
    1. 在分配堆内存时,为多个元素分配了空间,那么在使用 delete 释放堆内存时必须加[],否则会造成内存泄漏。
    int a=3;
    int *p=new int[a];
    ...
    delete p;       //只释放了第一个 int 型元素所占据的堆内存,后两个没有释放。
    1. 必须使用指针保存分配的堆内存首地址,这是由于 delete 根据首地址进行堆内存释放,如果不知道首地址则无法释放内存,从而造成内存泄漏。
    int *p=new int[2];
    *(p++)=10;     //将首地址的内存数据赋值为 10 后,p=p+1,p 指向下一个数据的地址。
    *(p++)=20;
    delete []p;   //错误,指针 p 没有指向分配的堆内存的首地址。

    其中,表达式可以是常量也可以是变量,其作用与声明数组时[]里的表达式的作用一样,用于指定数目,如果只申请一个元素的空间,那么[表达式]部分可以不写:
    new 数据类型;
    同时还可以进行内存的初始化工作:
    new 数据类型(表达式);
    堆内存释放 delete 的用法为:
    delete []p; ** //p 为指向待释堆内存首地址的指针。
    如果 p 指向的堆内存
    只有一个元素**,可以将[]省掉。
    delete p;
    例题:
    使用堆内存分配方式实现学生成绩录入功能,要求程序运行时由用户输入学生人数。

    #include<iostream>
    using namespace std;
    int main()
    {
    int *pScore=NULL;
    int n,i;
    cout<<"请输入学生人数:";
    cin>>n;
    pScore=new int
    ;
    if(pScore==NULL)
    {
    cout<<"堆内存分配失败!"<<endl;
    return 0;
    }
    for(i=0;i<n;i++)
    {
    cout<<"请输入第"<<i+1<<"名学生的成绩:";
    cin>>pScore[i];
    }
    for(i=0;i<n;i++)
    {
    cout<<"第"<<i+1<<"名学生的成绩为:"<<*pScore[i]<<endl;    //也可以写为 pScore[i];
    }
    delete []pScore;
    pScore=NULL;
    return 0;
    }

    提示:

    1. 使用 new运算符申请内存,会返回申请到的内存空间的首地址。在程序中应将这个首地址保存在指针变量中,然后就可以使用该指针变量操作分配的堆内存空间。
    2. 可以将堆内存分配方式看做是一种动态数组,堆内存分配方式与数组的主要区别在于:数组在程序运行前必须确定长度;堆内存方式分配方式则可以在程序运行时根据实际情况确定长度。堆内存访问方式与数组完全一样。
    3. 当申请分配的内存太大、系统资源不够时,堆内存分配会失败,此时会返回 NULL,表示堆内存分配失败。因此分配内存后,应判断返回值是否是 NULL,如果是则报错并退出程序。

    7.8 引用

    7.8.1 引用的概念

    引用就是别名,变量的引用就是变量的别名,对引用的操作就是对所引用变量的操作。

    7.8.2 引用的声明与特点

    引用的声明形式为:
    数据类型 &引用名 = 变量名;
    其中 & 是引用运算符。
    提示:

    1. 建立引用时,必须用一直变量为其初始化,表示该引用就是该变量的别名。其所引用的对象一旦确定就不能修改。
    2. “&” 作用于引用名,表示紧随其后的为一个引用。如果要同时定义两个 int 型引用 r1 和 r2,必须写成如下形式:
    int a,b;
    int &r1=a,&r2=b;
    1. 至今为止,“&”运算符有以下三种用法:

    位于运算符;
    取地址运算符;
    引用运算符;出现在声明语句中。

    #include<iostream>
    using namespace std;
    int main()
    {
    int a=3,b=4,*p=&a;
    int &r=a;
    int *&rp=p;
    cout<<"a="<<a<<","<<"r="<<r<<endl;
    cout<<"p="<<p<<","<<"rp="<<rp<<endl;
    a=5;
    rp=&b;
    cout<<"a="<<a<<","<<"r="<<r<<endl;
    cout<<"p="<<p<<","<<"rp="<<rp<<endl;         //指针指向的内存的地址
    cout<<"&a="<<&a<<","<<"&r="<<&r<<endl;
    cout<<"&p="<<&p<<","<<"&rp="<<&rp<<endl;     //指针的地址
    return 0;
    }

    提示:

    1. 引用就是一个别名,声明引用不会再为其分配内存空间,而是与所引用的对象占据同一片内存空间,因此对用用的操作与对所引用对象的操作完全一样。
    2. 可以为指针变量声明引用,声明形式为:

    *数据类型 &引用名=指针变量名;

    7.8.3 const引用

    通过加上const 关键字限制对引用的复制操作,与指针不用的是,引用本身一旦初始化后就不能更改其指向,因此,const 没有引用常量和常量引用之分。声明形式有两种:
    const 数据类型 &引用名=变量名或常量;

    数据类型 const &引用名=变量名或常量;
    const 引用只能访问引用对象的值,不能通过引用名改变引用对象的值。
    int a=3;
    const &r=a;
    r=10; //错误。
    由于 const 引用不需要修改所引用对象的值,所以 const 引用可以使用常量对其进行初始化,非 const 引用不可以。
    const int &r1=3; //正确
    int &r2=3; //错误

    7.8.4 指针与引用的区别

    指针与引用都可以直接操作它们所指向的内存地址,虽然指针和引用是两个概念,但是在编译程序时,编译器一般会将引用自动转换为指针。在具体使用上存在一些区别。

    1. 指针是一个变量,本身要占据内存空间,引用仅仅是一个别名。
    2. 引用在声明时确定其指向,初始化后就不能修改;而指针在声明时不必初始化,可在声明后的任何地方赋值且可以进行多次赋值。
    3. 访问指针所指向的内存中的内容时,需要在指针变量前加“*”,引用则可以直接写引用名。
    • 点赞
    • 收藏
    • 分享
    • 文章举报
    小饼干smile 发布了3 篇原创文章 · 获赞 0 · 访问量 28 私信 关注
    内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
    标签: