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操作符来验证指针的大小
如图所示:
运行结果如下:
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++;
来说,如果指针p
是char
类型,则向后走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];
p
,arr
,&arr[0]
均指向同一单元,它们是数组arr
的首地址,也是元素arr[0]
的首地址。
p+1
,arr+1
,&arr[1]
均指向元素arr[1]
。类似的,
p+i
,arr+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])
:指针被解引用,也就是调用函数- 传入参数
- 以
x
,y
作为实参传递给这个函数
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++##笔记#