C++指针那些事

本文最后更新于:2023年12月17日 下午

前言

指针真的让人又爱又恨,每当自己认为已经参悟一二,现实总是狠狠地打了我脸,不是忘了这个,就是忘了那个。因此趁这个机会,再温习回顾下C/C++里指针的相关语法知识,并记录下来以便日后复习。

基本知识

指针基本概念

在C语言中,每定义一个变量,系统就会为变量分配一块内存,而内存是有地址的。C语言中,采用运算符 & 来获取变量的地址。

指针是一种特殊类型的变量,用于存储变量的地址。当指针变量赋值之后,就可以使用运算符 * (解引用运算符),取得指针所指向地址的值,简单用法如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main() {
int* p; // 定义一个指针p,类型为 int;
int num = 10; // 定义一个 int 变量

p = &b; //将 p 指向 b

cout << "变量 num 的地址为 " << p << endl;
cout << "变量 num 的地址为 " << (&num) << endl;
cout << "变量 num 的值为" << num << endl;
cout << "变量 num 的值为" << *p << endl; // *p 表示指针p中存储的地址所对应的值;

return 0;
}

注意:在 C++ 创建指针时,计算机将会分配用来存储地址的内存,但不会分配用来存储指针所指向的数据的内存 ,为数据分配的空间是一个独立的空间,不可省略,如下所示。

1
2
int *pt;			// 定义一个指向 int 类型的指针 pt;
*pt = 23; // 错误,指针 pt 未指向任何地址,

所以需要谨记一点,在对指针应用解引用运算符(*) 之前,将指针初始化为一个确定的,适当的地址。

空指针与野指针

空指针:没有赋值的指针变量(没有指向内存变量的地址),对空指针进行操作会造成程序的Core dump(段错误)。如下代码所示。

1
2
int* p = 0;
*p = 10; // 运行报错

野指针:指针指向内存已释放,但指针的值不会被清零,对野指针操作的结果不可预知。如下代码所示。

1
2
3
4
5
6
int* p = new int(3);
delete p ;
if (p != nullptr) { // 已经释放了对应内存空间,但指针的值还没有清零,此时成为野指针
cout << "not nullptr" << endl;
}
p = nullptr; // 释放指针后,应当手动置指针为nullptr

sizeof的坑

sizeof(x) ,当 x 为指针变量时,求得的是指针类型的大小;当 x 为数组名时,求得的是数组的大小(数组元素个数 * 数组元素类型大小)。

注意:当数组作为函数的参数进行传递时,数组自动退化为同类型的指针。参考下面示例。

1
2
3
4
5
6
7
8
9
10
11
12
int get_size(int data[]) {
return sizeof(data);
}
int main() {
int data1[] = {1,2,3,4,5};
int size1 = sizeof(data1); // 20 (输出的是数组大小)

int* data2 = data1;
int size2 = sizeof(data2); // 8 (64位机器) 输出的是指针大小

int size3 = get_size(data1); // 8 (64位机器)
}

指针与数组

  • 数组名用法上 类似于一个指针常量,指向数组首元素的地址
  • 对于指针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
2
3
4
5
6
7
8
9
10
11
12
13
int a = 10, b = 20;
int * const p1 = &a; // p1是指针常量
*p1 = 30; // p1指向的地址是一定的,但其内容可以修改
// p1 = &b; // p1指针常量不能指向其他变量的地址

const int *p2 = &a; // p2是常量指针
// int const *p2 = &a; // 和上一行等价
p2 = &b; // p2可以指向其他地址,但是内容不可以改变
// *p2 = 10; // p2常量指针,所指向地址的值不可以修改

const int * const p3 = &a; // p3既是指针常量又是常量指针
// p3 = &b; // p3不能指向其他地址
// *p3 = 30; // p3所指向地址的值不能修改

有时候指针常量和常量指针傻傻分不清,教大家一个小技巧,就是看 const 在 * 的哪一侧,如果const 在 * 左侧,表明const 靠近所指向的变量类型,即所指向的变量是个常量,也就是常量指针;如果 const 在 * 的右侧,表明 const 靠近指针变量名,所以这个指针变量是个常量,也就是指针常量。

函数名与函数指针

就像定义 int 变量时,会在内存里分配一个4字节的空间存储该变量,对应有一个地址;当定义了一个函数后,同样需要在内存中分配空间进行存储,调用函数就像使用变量一样需要一个地址来唯一的指向它,所以每个函数都需要一个地址来唯一标识自己,也就是所说的入口地址。

函数名标识映射该函数的入口地址,而函数指针是指向函数入口地址的指针变量(记住了函数名本身并不是一个指针类型)。

有了指向函数的指针变量后,可以用函数指针变量调用函数,就像用指针变量操作其他类型变量一样。函数指针主要有两个用途:调用函数和做函数的参数。


我们都知道在调用函数的时候有函数名就够了,比如fun(2),但编译器在编译的时候会进行所谓的”Function-to-pointer conversion“,也就是把函数名隐式转换成函数指针类型,也就是要通过函数指针来调用函数,所以如果你在调用函数的时候写成(&fun)(2)也是一样能工作的,因为&fun实际上就是返回一个函数指针。参照下面例子,只是这种写法很不常见,即使你不显式的写出&的话编译器也会隐式的进行转换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void fun(int a)
{
cout<<"fun"<<endl;
}

int main()
{
printf("fun=%p\n",fun);
printf("*fun=%p\n",*fun);
printf("&fun=%p\n",&fun);
printf("*******fun=%p\n",*******fun);

void(*p1)(int)=fun; // 函数指针p1,fun进行一次隐式转换
void(*p2)(int)=*fun; // fun进行了两次隐式转换
void(*p3)(int)=&fun; // 显示转换成函数指针
// void(*p4)(int)=&&fun; // 不可以,连续取两次地址,就变成了函数指针的指针类型
printf("p1=%p\n", p1);
printf("p2=%p\n", p2);
printf("p3=%p\n", p3);
}

正如上面代码示例所示,其实即使你写成(* 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为对象指针,pClassFunc 的一个实例(即函数指针),加一个 * 可以理解为把指针转换成了函数名,这样就和正常调用成员函数的情况一致了(这是我个人的理解哈。。。)

更详细的测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>
using namespace std;
class MyClass{
public:
void func() {
cout << "func" << endl;
}
static void func2() {
cout << "func2" << endl;
}
};
void func3() {
cout << "func3" << endl;
}

typedef void (MyClass:: *ClassFunc)(); // 定义MyClass类的 非静态成员函数指针
typedef void (* NormalFunc)(); // 定义普通函数指针

int main(){
MyClass m;
MyClass* p_m = &m;
ClassFunc p = &MyClass::func; // 这里 & 不能少,不清楚为什么这里没有隐式转换,可能是类成员函数特别一点?

(m.*p)(); // 正常调用
(p_m->*p)(); // 正常调用

// NormalFunc a = MyClass::func; // 编译不通过
NormalFunc b = MyClass::func2; // 编译通过,类的静态成员函数可以被普通函数指针接收
NormalFunc c = func3; // 编译通过
b();
c();

}

参考资料

[1] C/C++的函数名和函数指针的关系剖析 - Esfog - 博客园

[2] 数组名与数组指针、指针数组

[3] C++ 指针详讲、及指针与数组 - 掘金

[4] 解决全部C语言指针的问题_哔哩哔哩


C++指针那些事
https://2017zhangyuxuan.github.io/2022/01/17/2022-01/2022-01-17 C++指针那些事/
作者
Zhang Yuxuan
发布于
2022年1月17日
许可协议