大家好,我是 同学小张,持续学习C++进阶知识和AI大模型应用实战案例,持续分享,欢迎大家点赞+关注,共同学习和进步。
重学C++系列文章,在会用的基础上深入探讨底层原理和实现,适合有一定C++基础,想在C++方向上持续学习和进阶的同学。争取让你每天用5-10分钟,了解一些以前没有注意到的细节。
本文为指针系列的内容。相信大家对指针的使用都有一定的了解,所以本文就不再赘述,仅对指针的使用中一些容易出问题的地方进行补充和学习。
0. 指针中应该区分的概念
指针的理解应该有四个概念:
- 指针的类型
- 指针所指向的对象的类型
- 指针本身的内存占用
- 指针所指向的对象的内存占用
上图中,a_ptr
是个指针,指向变量a,对应上面的四个概念:
- 指针的类型是
int*
- 指针所指向的对象的类型为变量
a
的类型,是int
- 指针本身的内存占用,即为
a_ptr
的内存占用,一般指针本身的值是一个地址数值,在32位程序里,所有类型的指针的值都是一个32位整数(4字节),因为32位程序里内存地址全都是32位长。 - 指针所指向的对象的内存占用,为变量
a
的内存占用
不管什么类型的指针,它在内存中的占用都是 32 位(32位程序中)。
1. 指针的算数运算
指针的算数运算与变量的算数运算是完全不同的。以几个例子做演示:
1.1 char* 类型指针
char a[11] = {'0','1','2','3','4','5','6','7','8','9', '\n'}; char* ptr = a; std::printf("当前地址 = %p\n", ptr); std::printf("当前值 = %c\n", *ptr); ptr++; std::printf("++之后的地址 = %p\n", ptr); std::printf("++之后指向的值 = %c\n", *ptr);
ptr++
,为指针的算数运算,在编译器中它会指针ptr
的值加上sizeof(char)
,也就是1,从运行结果来看,地址变化1。原来,ptr
指向的是a
的首地址,也就是指向的值是0。++
之后,ptr
就变成了指向1的指针。
1.2 int* 类型指针
int a[11] = {0,1,2,3,4,5,6,7, 8,9, 10}; int* ptr = a; std::printf("当前地址 = %p\n", ptr); std::printf("当前值 = %d\n", *ptr); ptr++; std::printf("++之后的地址 = %p\n", ptr); std::printf("++之后指向的值 = %d\n", *ptr);
类比char*
指针的例子,ptr++
在编译器中是在原来值的基础上加sizeof(int)
,也就是4。而a
数组中的每个值都占用4字节,因此原来ptr
指向的是0,++
之后地址变化了4字节,指向的是1。
1.3 int* 指针指向 char 数组
那么,如果一个int*
类型的指针指向了 char
数组,ptr++
是地址变化几呢?
char a[11] = {'0','1','2','3','4','5','6','7','8','9', '\n'}; int* ptr = (int*)a; // int* 指针强制指向 char 数组 std::printf("当前地址 = %p\n", ptr); std::printf("当前值 = %c\n", *ptr); ptr++; std::printf("++之后的地址 = %p\n", ptr); std::printf("++之后指向的值 = %c\n", *ptr);
从运行结果可以看出来,ptr++
实际是加了一个sizeof(int)
,也就是4字节。而char
数组中的每个元素都占1个字节,所以,之前ptr指向0,++
之后ptr
指向4。
任何类型指针之间都可以互相转换,因为本质上指针就是一个虚拟内存的地址,但是不能互相解引用。
当然,这种强制类型转换的方式不建议使用,如果非要使用,在作指针算数运算的时候一定要确保自己明确指针算数运算的步长和原数组中每个元素占用的字节数。
1.4 总结
从上面的例子来看,指针的算数运算是在本身指向的地址(本身指向的变量的内存地址)的基础上加上一个步长。这个步长由指针所指向的变量的类型决定(在int*
指向char
数组的例子中,虽然数组是char
,但是在指向时强制将char
转换成了int
,步长也应改为 sizeof(int)
)。
2. 指针的一些使用场景
2.1 指针与数组名
2.1.1 相同点
从前面的例子可以看出来,指针指向的是数组的首元素地址。
int* ptr = a
中 ptr指向了a数组的首元素地址,而ptr++
将指针移动到了a的第二个元素。所以指针与数组有以下简单的对应关系:
a[0];//也可写成:*ptr; a[3];//也可写成:*(ptr+3); a[4];//也可写成:*(ptr+4);
而数组名a
与a
数组之间的关系,如同ptr
与a
数组之间的关系:
// a[0];//也可写成:*a; // a[3];//也可写成:*(a+3); // a[4];//也可写成:*(a+4); std::printf("第0个元素:%c\n", *a); std::printf("第3个元素:%c\n", *(a+3)); std::printf("第4个元素:%c\n", *(a+4));
2.1.2 不同点
(1)ptr
可变,数组名a
不可变:数组名a
不能作算数元素,不能改变。
a++; //不被允许,编译报错
(2)sizeof
计算的值不一样
std::printf("用数组名计算的大小:%llu\n", sizeof(a)); std::printf("用指针计算的大小:%llu\n", sizeof(ptr));
从以上运行结果来看,数组名a
在计算sizeof
时是计算的整个数组的大小(所指向的变量所占用内存的大小)。而指针ptr
在计算sizeof
时,计算的是自身占用内存的大小(64位机器,地址都是64位,8字节)。
其实,
sizeof
计算的大小,永远都是变量自身所占用的内存大小。数组名计算出来的,是数组本身。为什么?大家可以思考下。
2.2 指针与结构体
假设我们有以下结构体:
struct MyStruct { int a; int b; int c; };
最常用的指针方法为:
MyStruct ss = {1,2,3}; //初始化 MyStruct *ptr_ss = &ss; // 声明了一个指向结构对象ss的指针。它的类型是MyStruct*, 它指向的类型是MyStruct。 // 结构体内变量的访问 ptr_ss->a; ptr_ss->b; ptr_ss->c;
那如果是以下指针呢?将结构体指针强制转换为int*
类型指针:
int *pstr_ss = (int*) &ss; // 声明了一个指向结构对象ss的指针。但是它的类型和它指向的类型和ptr是不同的。
如何访问a,b,c
属性值呢?如下:
std::printf("a的值:%d\n", *pstr_ss); std::printf("b的值:%d\n", *(pstr_ss + 1)); std::printf("c的值:%d\n", *(pstr_ss + 2));
运行结果:
这就考验你对结构体内存对齐以及各个类型占用内存大小的掌握程度了。如上结构体中都是int
类型,还好说。如果最后一个变量是char
类型呢?
struct MyStruct { int a; int b; char c; };
这又该如何访问?
答案:按对应的字节数取值,例如最后一个将 %d
换成了 %c
,如果还打印 %d
,会出现异常值。
std::printf("c的值:%c\n", *(pstr_ss + 2)); // %d 换成了 %c
当然,这种通过
pstr_ss
访问结构体内元素的值得方法是不建议用的,非常容易导致问题。因为结构体中的变量存储时存在字节对齐等操作,所以很可能将里面类型占用的字节数改变,例如char
类型实际应该只占1个字节,由于字节对齐,它需要占用4个字节,虽然3个字节是空的。这就导致了变量之间存在内存间隙,pstr_ss + 1
之后指向的不一定是下一个元素的起始位置了。数组是可以通过这种方式访问的,因为数组在内存中是连续存储的,中间没有字节对齐导致的内存间隙。
2.3 指针与函数
可以让指针指向一个函数。
2.3.1 函数指针 - 声明与使用方法
假如我们有下面这个Function:
int MyFunction(int a, char* b) { std::printf("MyFunction is called: %d, %s\n", a, b); return 0; }
该函数指针的声明方法:简单说就是将函数名替换成指针名,例如将 “MyFunction
” 替换成 “*ptr_fun
”,就算声明完成了。
int (*ptr_fun)(int, char*);
使用方法:让该指针指向 MyFunction
,调用时使用 *ptr_fun
代替函数名使用。
ptr_fun = MyFunction; char a[11] = {'0','1','2','3','4','5','6','7','8','9', '\0'}; int result = (*ptr_fun)(10, a); std::printf("result: %d\n", result);
运行结果:
2.3.2 函数参数 - 数组名作为函数参数,数组名将退化为指针
上例中,MyFunction
第二个参数为接收 char*
指针,传入数组名,这时候数组名在 MyFunction
内部就变成了指针,可以做算数运算,可以变更。用sizeof
计算出来的大小为指针本身的大小(8),不再是数组大小。
2.3.3 指向局部变量的指针不要传递
以下示例代码中,在 funcForSpace
函数中定义了一个局部变量a
,而随后将a
的地址传了出去。外部访问这个地址的值时,如果这个地址还没被释放或者没被复用还好,一旦被释放或者复用(如 stackFrame_resuse
函数),则无法得到正确的值,甚至引起Crash等严重问题。
#include <stdio.h> void funcForSpace(int **iptr) { int a = 10; *iptr = &a; } void stackFrame_reuse() { int a[1024] = {0}; } int main() { int *pNew; funcForSpace(&pNew); printf("%d\n",*pNew); // 10,此时栈帧还未被重复使用 stackFrame_reuse(); printf("%d\n",*pNew); // -858993460,垃圾值 while(1); return 0; }
如果要将局部变量的值传递出去,需要开辟堆空间上的地址(new
或 malloc
),如下:
#include <stdio.h> #include <malloc.h> int g(int **iptr) { // 当试图修改主调函数的一级指针变量时,被调函数的参数是一个二级指针 if ((*iptr = (int *)malloc(sizeof(int))) == NULL) return -1; } int main() { int *jptr; g(&jptr); *jptr = 10; printf("%d\n",*jptr); // 10 free(jptr); while(1); return 0; }
上述代码指针和地址传递过程如下:
3. 参考:
如果觉得本文对你有帮助,麻烦点个赞和关注呗 ~~~
- 大家好,我是 同学小张,持续学习C++进阶知识和AI大模型应用实战案例
- 欢迎 点赞 + 关注 👏,持续学习,持续干货输出。
- +v: jasper_8017 一起交流💬,一起进步💪。
- 微信公众号也可搜【同学小张】 🙏
本站文章一览: