C++指针的这些点你真的懂吗?

在C++中,指针的概念很重要,也是C++中的精髓之一。这一篇笔记适用于指针的入门学习。让我们一起来了解C++指针。


1.什么是指针

指针就是内存地址,指针变量就是用来存放内存地址的变量。

  • 我们知道,在计算机中,数据是存放在内存中的,而这些内存是有编号的,我们把这些编号称为地址,每个具体的地址都能有对应的内存单元。也就是说,地址用来标识每一个存储单元,方便用户对存储单元中的数据进行正确的访问。而指针就是来存放这些地址的变量。
  • 举个例子,在学校图书馆里,你找管理员想借某一本书,这时管理员就给你一张纸,纸上写着你要的书的编码:01060311,你拿到纸上的编码,也就知道了书的位置,并取出了想看的书。在这个例子中那张纸就相当于指针变量,其上对应的书的编码就是地址。 你根据这个地址,也就得到了存放在地址中的书。

总而言之,指针变量就是存放地址的变量,其指向的对象必须是变量,数组,函数等占有一定内存空间的实体。


2.为什么学指针

在C++中,指针的使用十分广泛。我们可以使用指针来写出更加高效,简洁的代码。

  • 在函数的参数传递中,利用指针传递能完成值传递无法完成的任务。
  • 一些复杂的数据结构需要指针的使用来完成相应的代码实现

3.怎么使用指针

指针的定义:

int* p;

  • 在这里,p就是一个指针变量,其实这和其他变量并没有什么不同。p就是一个变量。之所以叫指针,只是这个变量里存储的是其他变量的地址。

指针的赋值:

p = &a;

  • 在这里,我们使用了取地址符&来得到变量a的地址,并赋给指针p。
  • 指针p里存储着变量a的地址编号。

指针的解引用:

*p

  • 在这里,* 是解引用运算符,我们通过*p来得到指针p指向的地址中的具体内容

我们来看一段代码:

#include <iostream>
using namespace std;
int main() {
int var = 6;
int* p_var = &var;
*p_var = 8;
cout<<" p_var ="<<p_var<<'\n'
    <<"  &var ="<<&var<<'\n'
    <<"*p_var ="<<*p_var<<'\n'
    <<"  var  ="<<var;
return 0;
}

运行效果如下:

 p_var =005CF714
  &var =005CF714
*p_var =8
  var  =8

由代码输出的内容可知:

  • 指针p_var里存放的是变量var的地址;
  • 指针p_var指向的内存空间里存放的是变量var的值。

我们来逐行分析一下:

int var = 6;

  • 我们首先定义了一个变量var,并赋值为6。

int* p_var = &var;

  • 接着,我们就定义了一个int类型的指针变量p_var(指针指向的是int类型的空间)
  • 并把==变量var的地址赋给它(&是取地址运算符)。

*p_var = 8;

  • 这条语句相当于var = 8;,因为p_var是一个指针,* 是一个解引用运算符
  • 操作*p_var就相当于操作变量var

4.指针的注意事项

4.1.指针的大小

我们知道,在C++中,存放数据的变量因数据类型不同,导致所占用的存储空间长度也不同。但需要注意的是,不同类型的指针变量所占用的存储单元长度是相同的。

  • 对于指针,其大小是固定的,不受数据类型的限制
  • 在32位平台上,指针是4个字节的大小
  • 在64位平台上,指针是8个字节的大小
  • 我们可以用sizeof操作符来验证指针的大小

如图所示:

指针大小.png

运行结果如下:

char类型的指针大小为:8
int类型的指针大小为:8
double类型的指针大小为:8

4.2.野指针

当出现以下三种情况时,我们称指针为野指针:

  • 指针越界(访问了位置不可知的内存空间)
  • 指针定义后没有进行初始化
  • 指针指向的内存空间被施放掉了

野指针的危害很大,如果野指针指向的空间正在被使用,或者指向的地址根本无法访问,这就会导致数据被破坏,程序出错甚至崩溃。因此我们应该避免野指针的出现。

5.二级指针

二级指针就是指针的指针

嘶,有点绕,这么说吧,二级指针里的存储的是一级指针的地址。也就是说二级指针是一种指向指针的指针

  • 我们说一个变量的地址可以由指针来存储。这里的指针一般指一级指针。
  • 那我们能不能存储一级指针的地址?
  • 答案是肯定的,我们使用二级指针来存储一级指针的地址。

来看一段代码吧:

#include <iostream>
using namespace std;
int main()
{
int var = 6;
int* p_var = &var;
int** pp_var = &p_var;
cout << "&p_var = " << &p_var << '\n'
     << "pp_var = " << pp_var << '\n'
     << "变量var的值为:" << **pp_var;
return 0;
}

代码运行结果如下:

&p_var = 008FF8AC
pp_var = 008FF8AC
变量var的值为:6

输出语句验证了:

  • 二级指针里的存储的是一级指针的地址

代码分析:

int** pp_var = &p_var;

  • 这行代码定义了一个二级指针pp_var,并把一级指针p_var的地址赋给它。

cout << "变量var的值为:" << **pp_var;

  • 当我们利用二级指针想得到一级指针指向的地址里的内容,对二级指针必须解引用两次。

还有一点需要注意:

  • 二级指针的步长取决于一级指针。
  • 也就是说,二级指针的步长等于一级指针的大小

6.指针与数组的联系

  • 在C++中,指针与数组是密切相关的。二者甚至能互相转换。

  • 我们使用指针来操作数组,可以让代码更加高效且紧凑,但这也使其难以理解

6.1.用指针访问数组

  • 一个指向数组开头的指针,可以通过使用指针的算术运算来访问数组。

事实上,利用指针操作数组会比使用索引来操作数组更快一些。

来看一段代码:

#include <iostream>
using namespace std; 
int main ()
{
int  arr[3] = {19,45,37};
int  *p;
p = arr;
for (int i = 0; i < 3; i++)
{
cout<< "arr[" << i << "]的内存地址为 "
    << p << endl
    << "arr[" << i << "] 的值为 "
    << *p << endl;
p++;   
}
return 0;
}

运行结果如下;

arr[0]的内存地址为 0x6ffdf0
arr[0] 的值为 19
arr[1]的内存地址为 0x6ffdf4
arr[1] 的值为 45
arr[2]的内存地址为 0x6ffdf8
arr[2] 的值为 37

在上述代码中,有三条语句引起我们注意:

  • p = arr;
  • cout<<*p
  • p++;

我们来分析一下:

  • 首先,我们知道,数组的数组名其实是数组首元素的地址
  • 因此p = arr;等同于p = &arr[0];
  • 我们执行完语句p = arr;后,指针p里的内容就是数组首元素的地址
  • 因此在这里,指针p所指向的内容就是arr[0]
  • cout<<*p等同于cout<<arr[0]
  • 接着是p++,这是指针运算
  • 在这里,语句P++执行后,指针p所指向的内容变成了arr[1]
  • 看输出结果就知道:
  • arr[0]的内存地址为 0x6ffdf0
  • arr[1]的内存地址为 0x6ffdf4
  • 语句p++;表示指针向后走了4个字节数
  • 向后走的字节数取决于指针的类型
  • 也就是说指针的类型决定了指针跨内存单元的步长
  • 就拿语句p++;来说,如果指针pchar类型,则向后走1个字节数,short int类型就向后走2个字节数,int类型就走4个字节数...
  • 类似的,指针减一就是向前走
  • 还有一点,*指针运算符也可以应用在数组
  • 比如语句*(p+1);等同于*(arr+1);

再来看一段代码:

#include <iostream>
using namespace std; 
int main()
{
int arr[10]={1,2,3,4,5,6,7,8,9};
int* p1 = arr;
int* p2 = &arr[4];
cout<<"p2-p1 = "<<p2-p1;
return 0;
}

输出结果为:

p2-p1 = 4

我们的结论是:

两个指向同一个数组的指针相减,其结果是两个指针之间的元素个数。


6.2.注意事项

一:避免出现野指针

  • 当我们进行指针运算时,不要越界!即不要访问了位置不可知的内存空间

二:不能修改数组地址

  • 比如arr++;语句时错误的
  • 这是因为arr++;等同于arr=arr+1;
  • arr是数组名(也是数组首元素地址),不是一个变量,不可修改其值
  • 也就是数组的地址不能改变
  • 但是语句*(arr+i)=66;却可以成功执行
  • 这是因为数组里的元素可以改变,这并不会修改到数组的地址
  • 我们并没有对数组arr进行赋值,只是通过与i相加去访问元素的地址

7.NULL指针

NULL指针是一个定义在标准库中的值为零的常量

  • 当我们声明指针时,如果没有确切的地址可以赋值,就可以将指针赋值为NULL。

  • 像这样,被赋为 NULL 值的指针就被称为空指针

  • 空指针不指向任何地址

  • 也就是说空指针不指向任何一个可访问的内存位置

看代码:

#include <iostream>
using namespace std; 
int main ()
{
int* p = NULL;
cout<<"空指针的值是 "<<p;
return 0;
}

运行结果为:

空指针的值是 0

小结:

当我们把未使用的指针都赋值为NULL时,我们就可以使用if语句:if(p!=NULL)判断指针是否不为空,如果不为空,再进行指针操作,这能有效地避免使用未初始化的指针。


8.数组指针与指针数组

8.1数组指针

  • 数组指针就是数组名的指针,即数组首元素的指针

  • 数组指针本质上就是一个指针,指向数组的指针。

  • 我们在本章6.指针与数组的联系里讲的就是指向一维数组的数组指针。

来看一段代码:

#include <iostream>
using namespace std; 
int main()
{
int arr[5]={16,75,31,65,82};
int *p = arr;
for(int i=0;i<5;i++)
{
    cout<<*(p+i)<<" ";    
}
return 0;
}

运行结果如下:

16 75 31 65 82

分析:

  • 在这里,指针p指向了一维数组的首地址,即p=arr;

  • 或者我们也可以这样:p=&arr[0];

  • parr&arr[0]均指向同一单元,它们是数组arr的首地址,也是元素arr[0]的首地址。

  • p+1arr+1&arr[1]均指向元素arr[1]

  • 类似的,p+iarr+i&arr[i]均指向元素arr[i]


现在,我们来看看指向二维数组的数组指针

代码如下:

#include <iostream>
using namespace std; 
int main()
{
int arr[3][4]={{11,16,24,28},{36,41,48,53},{66,88,90,98}};
int (*p)[4] = arr;//定义了一个数组指针,且指向了二维数组 
for(int i=0;i<3;i++)
{
    for(int j=0;j<4;j++){
        cout<<*(*(p+i)+j)<<"  ";    
    }
    cout<<'\n';
}
return 0;
}

运行结果如下:

11  16  24  28
36  41  48  53
66  88  90  98

分析:

  • 二维数组arr相当于一个二级指针常量。需要注意的是,二维数组名并不等同于二级指针

  • 一个二维数组可以分解为若干个一维数组

  • 比如上述代码中,二维数组arr可以分解为三个一维数组,分别为arr[0],arr[1],arr[2]

  • arr[0]就是第一个一维数组的数组名

  • 每个一维数组含有四个元素。

  • 例如arr[0]数组含有arr[0][0],arr[0][1],arr[0][2],arr[0][3]四个元素。

  • p是一个数组指针,它指向一维数组arr[0]

  • 数组指针p的增量以它所指向的一维数组长度为单位

  • *(p+i)等同于arr[i]等同于&arr[i][0]

  • 也就是说*(p+i)里存放的是arr[i][0]的地址

  • 那么*(p+i)+j就表示arr[i][j]的地址

  • 再解一次引用就能取到二维数组的值

  • *(*(p+i)+j)表示arr[i][j]的值。

还有一点很有意思:

语句*(*(p+i)+j)等同于*(p[i]+j)等同于p[i][j]等同于arr[i][j]


8.2.指针数组

我们将元素全是指针的数组称为指针数组

并且数组里的指针是指向相同数据类型的指针

  • 比如如char* p[3]
  • 根据运算符的结合性可知
  • p先与[]结合,说明p是一个数组,数组里面有3个元素
  • 每个元素是char*类型

来看一段代码:

#include <iostream>
using namespace std; 
int main()
{
int a1[3] = {1,2,3};
int a2[3] = {1,2,3};
int a3[3] = {1,2,3};
int *p[3] = {a1,a2,a3};
for(int i=0;i<3;i++)
{
    for(int j=0;j<3;j++){
        cout<<p[i][j]<<" ";
    }
    cout<<'\n';

}
return 0;
}

运行结果如下:

1 2 3
1 2 3
1 2 3

指针数组本质上是一个数组

指针数组常用于指向若干字符串,这会使字符串处理更加灵活方便,也更加节省内存空间

实例:

#include <iostream>
using namespace std; 
int main()
{
char *p[] = {"Hello ","World!"};
for(int i=0;i<2;i++)
{
    for(int j=0;j<6;j++)
    {
        cout<<p[i][j];
        //也可以用 *(*(p+i)+j)
    }
}
return 0;
}

运行结果如下:

Hello World!

9.函数指针与指针函数

先看定义:

  • 函数指针就是存放函数地址的指针,本质是一个指针,指向函数的指针。

  • 指针函数指的是带指针的函数,本质是一个函数。其返回类型是某一类型的指针

函数指针与指针函数区别在于

  • 前者是一个指针;
  • 后者是一个函数。

9.1.函数指针

声明语法:

返回类型 (*f)(形参列表); /* 声明一个函数指针 */

赋值语法(两种):

f=func; /* 将func函数的首地址赋给指针f */

f=&func; /* 或者直接将函数地址赋给函数指针也一样*/

需要注意的是:

  • * ( f) 的括号不能省略!** 括号保证了运算符的优先级。
  • “形参列表”表示 指针变量指向的函数所带的参数列表。
  • 赋值时函数func不带括号,也不带参数
  • 由于func代表函数的首地址,因此经过赋值以后,指针f就指向函数func(x)的代码的首地址。
  • 函数括号中的形参列表可有可无,视情况而定
  • 赋给函数指针的函数应该和函数指针所指的函数原型是一致的。

来看段代码

#include <iostream>
using namespace std;
int sum(int x, int y) { return x + y; };
int main()
{
    int (*ptr)(int, int);
    ptr = sum;
    int a = 3, b = 3;
    int c = (*ptr)(a, b);
    cout << "c的值为:"<<c;
return 0;
}

运行结果如下:

c的值为:6

分析:

int sum(int x, int y) { return x + y; };

  • 函数sum返回两个数之和

int (*ptr)(int, int);

  • 定义了一个函数指针

ptr = sum;

  • 把sum函数的入口地址赋值给函数指针

int c = (*ptr)(a, b);

  • 通过函数指针来调用sum函数

总结:

  • 实际上,无论是函数指针还是函数名,都指向了函数的入口地址

  • 不同的是,函数指针是一个指针,它可以指向任何函数

  • 在程序中把哪个函数的地址赋给它,它就指向哪个函数,而后用指针变量调用它

  • 因此,函数指针可以先后指向不同的函数。


9.2.指针函数

声明语法:

返回类型*函数名(形参列表)

来看段代码

#include <iostream>
using namespace std;
float *find(float(*p_func)[3],int n) /*定义指针函数*/
{
    float *p_new;
    p_new = *(p_func+n);
    return p_new;
}
int main(void)
{
    static float score[][3]={{96.3,89.7,66.9},{85.1,89.2,87.5},{66.3,59.8,76.4}};
    float *p;
    int m;
    cout<<"您想查找第几位同学的成绩(共三位):\n";
    cin>>m;
    cout<<"第"<<m<<"位同学的语数英成绩分别为:\n|" ;
    p=find(score,m-1);
    for(int i=0;i<3;i++)
        cout<<*(p+i)<<" |"; 
    return 0;
}

运行结果为:

您想查找第几位同学的成绩(共三位):
1
第1位同学的语数英成绩分别为:
|96.3 |89.7 |66.9 |

分析:

  • 一共有三名学生的成绩,我们用二维数组来存储每位学生的成绩。
  • 我们定义一个指针函数find(),用来查询想查询的同学是排在第几位。
  • 指针函数的形参p_func是一个指针变量,该指针变量指向含有3个元素的一维数组。我们把数组score传入。
  • 在函数体中,我们定义了一个指针p_new,我们将*(pointer+1)赋给p_new来让p_new指向第n行的第0个元素。

总结:

指针函数指的是带指针的函数,本质是一个函数。其返回类型是某一类型的指针


10.函数指针数组

顾名思义,函数指针数组就是存放函数指针的数组

函数指针数组的用法就有点像点菜单一样,我们在数组里面存放若干个函数指针,然后让用户来选择使用哪些函数,数组中的指针可以用来调用函数。

来看段代码:

#include <iostream>
using namespace std; 
int get_sum(int a,int b)
{
    return a+b;
}
int get_sub(int a,int b)
{
    return a-b;
}
int main()
{
int (*p[2])(int,int)={get_sum,get_sub};
int opt;
cout<<"请选择运算(加法为0,减法为1):\n";
cin>>opt;
int x,y;
cout<<"请输入参与运算的数值:\n";
cin>>x>>y; 
cout<<"运算结果为:"<<(*p[opt])(x,y);
return 0;
}

运行结果如下:

请选择运算(加法为0,减法为1):
0
请输入参与运算的数值:
2
4
运算结果为:6

分析:

一:int (*p[2])(int,int)={get_sum,get_sub};

  • 我们直接将这3个函数名保存在数组p
  • 类似的,我们也可以定义两个函数指针,再放在函数指针数组里
  • 如:
  • int (*p1)(int, int) = get_sum;
  • int (*p2)(int, int) = get_sub;
  • int (*p[2])(int, int) = {p1,p2};
  • 这三条语句和int (*p[2])(int,int)={get_sum,get_sub};的作用是一样的

二:cout<<"运算结果为:"<<(*p[opt])(x,y);

  • 选择指针
  • p[opt]:选择在数组中位置为opt的指针。
  • 调用指针
  • (*p[opt]):指针被解引用,也就是调用函数
  • 传入参数
  • xy作为实参传递给这个函数

11.this指针

在C++中:

  • this指针保存当前对象的地址,即指向当前对象。通过this指针可以访问类中的所有成员

  • 每一个类的对象都能通过this指针来访问自己的地址。this指针是所有成员函数的隐含参数,在成员函数内部,它可以用来指向调用对象。也就是说,当我们调用成员函数时,实际上是替某个对象调用它

  • this作用域是在类内部,当我们调用一个成员函数时,系统会用请求该函数的对象的地址去初始化this指针。举个例子,当执行到语句object.get_area(),编译器会把object的地址传递给get_area()的隐式形参this指针。

  • 在成员函数内部,我们能调用函数的对象的成员,而无须通过成员访问运算符来做到这一点,这是因为有隐式参数this指针的存在,其指的正是这个对象。

注意!:

  • 友元函数没有this指针,因为友元不是类的成员。只有成员函数才有this指针

  • this是一个常量指针,因此不能改变this中保存的地址

this指针的某些使用情景:

  • 返回类对象本身
  • 成员函数的参数与成员变量名相同

好了,指针部分的复习就到这了,可能有些晕(我之前也好晕),看不懂就多看几遍,并学会运用,相信指针部分是能够完美搞定。按照惯例,最后再来张思维导图梳理一下吧。


文末备注:

本章笔记参考书:《C++程序设计教程》----钱能 著


#C语言##C/C++##笔记#
全部评论
8.1节 最后一句话是不是漏了一个*号呀? 原文:  语句*((p+i)+j) 等同于*(p[i]+j)等同于p[i][j]等同于arr[i][j]
点赞 回复 分享
发布于 2022-06-16 23:38

相关推荐

11-04 14:10
东南大学 Java
_可乐多加冰_:去市公司包卖卡的
点赞 评论 收藏
分享
10-27 17:26
东北大学 Java
点赞 评论 收藏
分享
评论
14
62
分享
牛客网
牛客企业服务