【重学C++】【指针】一文看透:指针中容易混淆的四个概念、算数运算以及使用场景中容易忽视的细节

简介: 【重学C++】【指针】一文看透:指针中容易混淆的四个概念、算数运算以及使用场景中容易忽视的细节

大家好,我是 同学小张,持续学习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);  

而数组名aa数组之间的关系,如同ptra数组之间的关系:

// 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 指向局部变量的指针不要传递

参考:https://mp.weixin.qq.com/s/CPbfKg70fA2W3NJejqtfuw

以下示例代码中,在 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;
}

如果要将局部变量的值传递出去,需要开辟堆空间上的地址(newmalloc),如下:

#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 一起交流💬,一起进步💪。
  • 微信公众号也可搜【同学小张】 🙏

本站文章一览:

相关文章
|
28天前
|
自然语言处理 前端开发 JavaScript
深入理解前端中的 “this” 指针:从基础概念到复杂应用
本文全面解析前端开发中“this”指针的运用,从基本概念入手,逐步探讨其在不同场景下的表现与应用技巧,帮助开发者深入理解并灵活掌握“this”的使用。
|
2月前
|
人工智能
魔法指针 之 指针变量的意义 指针运算
魔法指针 之 指针变量的意义 指针运算
25 0
|
2月前
|
算法 C++
【算法】双指针+二分(C/C++
【算法】双指针+二分(C/C++
|
2月前
|
程序员 C++ 开发者
C++入门教程:掌握函数重载、引用与内联函数的概念
通过上述介绍和实例,我们可以看到,函数重载提供了多态性;引用提高了函数调用的效率和便捷性;内联函数则在保证代码清晰的同时,提高了程序的运行效率。掌握这些概念,对于初学者来说是非常重要的,它们是提升C++编程技能的基石。
26 0
|
5月前
|
存储 安全 C++
浅析C++的指针与引用
虽然指针和引用在C++中都用于间接数据访问,但它们各自拥有独特的特性和应用场景。选择使用指针还是引用,主要取决于程序的具体需求,如是否需要动态内存管理,是否希望变量可以重新指向其他对象等。理解这二者的区别,将有助于开发高效、安全的C++程序。
38 3
|
5月前
|
C++ 索引 运维
开发与运维数组问题之在C++中数组名和指针是等价如何解决
开发与运维数组问题之在C++中数组名和指针是等价如何解决
39 6
|
5月前
|
JSON Go C++
开发与运维C++问题之在iLogtail新架构中在C++主程序中新增插件的概念如何解决
开发与运维C++问题之在iLogtail新架构中在C++主程序中新增插件的概念如何解决
50 1
|
5月前
|
存储 安全 编译器
【C++入门 四】学习C++内联函数 | auto关键字 | 基于范围的for循环(C++11) | 指针空值nullptr(C++11)
【C++入门 四】学习C++内联函数 | auto关键字 | 基于范围的for循环(C++11) | 指针空值nullptr(C++11)
|
5月前
|
C++ 开发者
C++一分钟之-概念(concepts):C++20的类型约束
【7月更文挑战第4天】C++20引入了Concepts,提升模板编程的类型约束和可读性。概念定义了模板参数需遵循的规则。常见问题包括过度约束、约束不完整和重载决议复杂性。避免问题的关键在于适度约束、全面覆盖约束条件和理解重载决议。示例展示了如何用Concepts限制模板函数接受的类型。概念将增强模板的安全性和灵活性,但需谨慎使用以防止错误。随着C++的发展,Concepts将成为必备工具。
111 2
|
6月前
|
存储 安全 编译器
【C++航海王:追寻罗杰的编程之路】引用、内联、auto关键字、基于范围的for、指针空值nullptr
【C++航海王:追寻罗杰的编程之路】引用、内联、auto关键字、基于范围的for、指针空值nullptr
70 5