C++指针那些事
本文最后更新于:2023年12月17日 下午
前言
指针真的让人又爱又恨,每当自己认为已经参悟一二,现实总是狠狠地打了我脸,不是忘了这个,就是忘了那个。因此趁这个机会,再温习回顾下C/C++里指针的相关语法知识,并记录下来以便日后复习。
基本知识
指针基本概念
在C语言中,每定义一个变量,系统就会为变量分配一块内存,而内存是有地址的。C语言中,采用运算符 & 来获取变量的地址。
指针是一种特殊类型的变量,用于存储变量的地址。当指针变量赋值之后,就可以使用运算符 * (解引用运算符),取得指针所指向地址的值,简单用法如下所示。
1 |
|
注意:在 C++ 创建指针时,计算机将会分配用来存储地址的内存,但不会分配用来存储指针所指向的数据的内存 ,为数据分配的空间是一个独立的空间,不可省略,如下所示。
1 |
|
所以需要谨记一点,在对指针应用解引用运算符(*) 之前,将指针初始化为一个确定的,适当的地址。
空指针与野指针
空指针:没有赋值的指针变量(没有指向内存变量的地址),对空指针进行操作会造成程序的Core dump(段错误)。如下代码所示。
1 |
|
野指针:指针指向内存已释放,但指针的值不会被清零,对野指针操作的结果不可预知。如下代码所示。
1 |
|
sizeof的坑
sizeof(x) ,当 x 为指针变量时,求得的是指针类型的大小;当 x 为数组名时,求得的是数组的大小(数组元素个数 * 数组元素类型大小)。
注意:当数组作为函数的参数进行传递时,数组自动退化为同类型的指针。参考下面示例。
1 |
|
指针与数组
- 数组名用法上 类似于一个指针常量,指向数组首元素的地址
- 对于指针p, p+1 的跨度,即移动p一次增加的字节数大小,等于指针p所指元素类型的大小
- 对于int *p,增加4字节
- 对于double *p ,增加8字节
- 对于int (* p)[3),增加 3 * 4 = 12 字节,因为p指向的是一个数组,整个数组的大小为3、*4 = 12字节
- 对于int* * p,增加8字节(64位系统),因为p指向的是一个int*指针,而指针大小为8字节
- 对于new的使用,通常是和指针结合在一起,对于 Type* p (Type代表某种类型),通常new是有两种用法
Type* p = new Type( )
,只分配单个Type的内存,new返回的也是这个Type的内存地址- 获取对应元素,通常使用 *p,但是 p[0] 也是同样的效果;使用 p[1] 的话,会访问超出了我们分配的内存,可能带来不可预知的后果
Type* p = new Type[n]
,分配n个 Type的内存,new返回的是Type数组首元素的地址- 获取对应元素,通常使用 p[i],获取下标为 i 的元素
这样的话有这样一个疑惑,如下方图片中标红区域,可以有申请一个int的大小,申请一个指针的大小,如何单独申请一个数组对象?
似乎并没有相对应的方式,因为申请一个数组的空间已经用 int* p = new int[3]
表达了,其实这个问题有点类似钻牛角尖,或者说自以为有这样的对称规律在。但在实际使用中并不推荐使用new 和 delete,对于数组的需求,使用vector< T> 就行了;这里只是为了探讨研究语法问题。
指针常量与常量指针
指针常量:指针类型的常量。表示这个指针变量用const修饰后成了常量,变量的值不能改变即不能指向其他地址,但是指针所指向地址里的值是可以修改的。同时注意这是个常量,所以在定义的时候要初始化。
常量指针:指针变量指向的类型为常量类型,即指针指向地址里的值不可以修改。
具体示例代码如下:
1 |
|
有时候指针常量和常量指针傻傻分不清,教大家一个小技巧,就是看 const 在 * 的哪一侧,如果const 在 * 左侧,表明const 靠近所指向的变量类型,即所指向的变量是个常量,也就是常量指针;如果 const 在 * 的右侧,表明 const 靠近指针变量名,所以这个指针变量是个常量,也就是指针常量。
函数名与函数指针
就像定义 int 变量时,会在内存里分配一个4字节的空间存储该变量,对应有一个地址;当定义了一个函数后,同样需要在内存中分配空间进行存储,调用函数就像使用变量一样需要一个地址来唯一的指向它,所以每个函数都需要一个地址来唯一标识自己,也就是所说的入口地址。
函数名标识映射该函数的入口地址,而函数指针是指向函数入口地址的指针变量(记住了函数名本身并不是一个指针类型)。
有了指向函数的指针变量后,可以用函数指针变量调用函数,就像用指针变量操作其他类型变量一样。函数指针主要有两个用途:调用函数和做函数的参数。
我们都知道在调用函数的时候有函数名就够了,比如fun(2),但编译器在编译的时候会进行所谓的”Function-to-pointer conversion“,也就是把函数名隐式转换成函数指针类型,也就是要通过函数指针来调用函数,所以如果你在调用函数的时候写成(&fun)(2)也是一样能工作的,因为&fun实际上就是返回一个函数指针。参照下面例子,只是这种写法很不常见,即使你不显式的写出&的话编译器也会隐式的进行转换。
1 |
|
正如上面代码示例所示,其实即使你写成(* fun)(2)也是可以正常运行的,这是因为当编译器看到fun的时候发现它前面没有&,也就是如果没有将函数名显示地转换成指针,那么他就会隐式地转换成指针,当转换完之后发现前面又有一个 * 这时候也就是要进行所谓的”解引用”操作,也就是取出 * 指针里的值,而那么值实际上也就函数名fun,这样一次隐式换然后再来一次解引用实际上相当于什么也没做,所以系统还会再进行一次隐式的”Function-to-pointer conversion”,所以即使你写成(************fun)(2)也会正常运行,和刚才的一个道理,只是多做了几次反复的转解操作而已,都是编译器自己完成的。
后来又学习到了定义某个类的非静态成员函数指针的方式,真是又“涨姿势”了~ 例如typedef void (MyClass:: *ClassFunc)();
这是定义了MyClass
类的非静态成员函数指针,即ClassFunc
定义的变量只能接收MyClass
的非静态成员函数。
而调用的方式也比较奇特,因为是绑定的是类的非静态成员函数,所以就需要该类的一个实例对象来进行调用,所以调用形式为(m. *p)() 或 (p_m-> *p )()
,其中分别是m
为对象实例,p_m
为对象指针,p
是 ClassFunc
的一个实例(即函数指针),加一个 * 可以理解为把指针转换成了函数名,这样就和正常调用成员函数的情况一致了(这是我个人的理解哈。。。)
更详细的测试代码如下:
1 |
|
参考资料
[1] C/C++的函数名和函数指针的关系剖析 - Esfog - 博客园
[2] 数组名与数组指针、指针数组