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

本站文章一览:

相关文章
|
8天前
|
编译器 C++
【C++核心】指针和引用案例详解
这篇文章详细讲解了C++中指针和引用的概念、使用场景和操作技巧,包括指针的定义、指针与数组、指针与函数的关系,以及引用的基本使用、注意事项和作为函数参数和返回值的用法。
14 3
|
29天前
|
C++
C++(十八)Smart Pointer 智能指针简介
智能指针是C++中用于管理动态分配内存的一种机制,通过自动释放不再使用的内存来防止内存泄漏。`auto_ptr`是早期的一种实现,但已被`shared_ptr`和`weak_ptr`取代。这些智能指针基于RAII(Resource Acquisition Is Initialization)原则,即资源获取即初始化。RAII确保对象在其生命周期结束时自动释放资源。通过重载`*`和`-&gt;`运算符,可以方便地访问和操作智能指针所指向的对象。
|
29天前
|
C++
C++(九)this指针
`this`指针是系统在创建对象时默认生成的,用于指向当前对象,便于使用。其特性包括:指向当前对象,适用于所有成员函数但不适用于初始化列表;作为隐含参数传递,不影响对象大小;类型为`ClassName* const`,指向不可变。`this`的作用在于避免参数与成员变量重名,并支持多重串联调用。例如,在`Stu`类中,通过`this-&gt;name`和`this-&gt;age`明确区分局部变量与成员变量,同时支持链式调用如`s.growUp().growUp()`。
|
2月前
|
存储 安全 C++
C++:指针引用普通变量适用场景
指针和引用都是C++提供的强大工具,它们在不同的场景下发挥着不可或缺的作用。了解两者的特点及适用场景,可以帮助开发者编写出更加高效、可读性更强的代码。在实际开发中,合理选择使用指针或引用是提高编程技巧的关键。
24 1
|
2月前
|
存储 C++
c++学习笔记06 指针
C++指针的详细学习笔记06,涵盖了指针的定义、使用、内存占用、空指针和野指针的概念,以及指针与数组、函数的关系和使用技巧。
30 0
|
4月前
|
C语言
指针进阶(C语言终)
指针进阶(C语言终)
|
24天前
|
存储 人工智能 C语言
C语言程序设计核心详解 第八章 指针超详细讲解_指针变量_二维数组指针_指向字符串指针
本文详细讲解了C语言中的指针,包括指针变量的定义与引用、指向数组及字符串的指针变量等。首先介绍了指针变量的基本概念和定义格式,随后通过多个示例展示了如何使用指针变量来操作普通变量、数组和字符串。文章还深入探讨了指向函数的指针变量以及指针数组的概念,并解释了空指针的意义和使用场景。通过丰富的代码示例和图形化展示,帮助读者更好地理解和掌握C语言中的指针知识。
|
2月前
|
C语言
【C初阶——指针5】鹏哥C语言系列文章,基本语法知识全面讲解——指针(5)
【C初阶——指针5】鹏哥C语言系列文章,基本语法知识全面讲解——指针(5)
|
2月前
|
C语言
【C初阶——指针4】鹏哥C语言系列文章,基本语法知识全面讲解——指针(4)
【C初阶——指针4】鹏哥C语言系列文章,基本语法知识全面讲解——指针(4)
|
2月前
|
存储 编译器 C语言
【C初阶——指针3】鹏哥C语言系列文章,基本语法知识全面讲解——指针(3)
【C初阶——指针3】鹏哥C语言系列文章,基本语法知识全面讲解——指针(3)