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

本站文章一览:

相关文章
|
24天前
|
缓存 安全 编译器
C++面试周刊(3):面试不慌,这样回答指针与引用,青铜秒变王者
《C++面试冲刺周刊》第三期聚焦指针与引用的区别,从青铜到王者级别面试回答解析,助你21天系统备战,直击高频考点,提升实战能力,轻松应对大厂C++面试。
75 10
C++面试周刊(3):面试不慌,这样回答指针与引用,青铜秒变王者
|
25天前
|
存储 C++
C++语言中指针变量int和取值操作ptr详细说明。
总结起来,在 C++ 中正确理解和运用 int 类型地址及其相关取值、设定等操纵至关重要且基础性强:定义 int 类型 pointer 需加星号;初始化 pointer 需配合 & 取址;读写 pointer 执向之处需配合 * 解引用操纵进行。
137 12
|
6月前
|
存储 编译器 C++
【c++】多态(多态的概念及实现、虚函数重写、纯虚函数和抽象类、虚函数表、多态的实现过程)
本文介绍了面向对象编程中的多态特性,涵盖其概念、实现条件及原理。多态指“一个接口,多种实现”,通过基类指针或引用来调用不同派生类的重写虚函数,实现运行时多态。文中详细解释了虚函数、虚函数表(vtable)、纯虚函数与抽象类的概念,并通过代码示例展示了多态的具体应用。此外,还讨论了动态绑定和静态绑定的区别,帮助读者深入理解多态机制。最后总结了多态在编程中的重要性和应用场景。 文章结构清晰,从基础到深入,适合初学者和有一定基础的开发者学习。如果你觉得内容有帮助,请点赞支持。 ❤❤❤
792 0
|
9月前
|
存储 程序员 C++
深入解析C++中的函数指针与`typedef`的妙用
本文深入解析了C++中的函数指针及其与`typedef`的结合使用。通过图示和代码示例,详细介绍了函数指针的基本概念、声明和使用方法,并展示了如何利用`typedef`简化复杂的函数指针声明,提升代码的可读性和可维护性。
252 1
|
10月前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
531 4
|
11月前
|
存储 安全 编译器
在 C++中,引用和指针的区别
在C++中,引用和指针都是用于间接访问对象的工具,但它们有显著区别。引用是对象的别名,必须在定义时初始化且不可重新绑定;指针是一个变量,可以指向不同对象,也可为空。引用更安全,指针更灵活。
|
7月前
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
3月前
|
人工智能 机器人 编译器
c++模板初阶----函数模板与类模板
class 类模板名private://类内成员声明class Apublic:A(T val):a(val){}private:T a;return 0;运行结果:注意:类模板中的成员函数若是放在类外定义时,需要加模板参数列表。return 0;
84 0
|
3月前
|
存储 编译器 程序员
c++的类(附含explicit关键字,友元,内部类)
本文介绍了C++中类的核心概念与用法,涵盖封装、继承、多态三大特性。重点讲解了类的定义(`class`与`struct`)、访问限定符(`private`、`public`、`protected`)、类的作用域及成员函数的声明与定义分离。同时深入探讨了类的大小计算、`this`指针、默认成员函数(构造函数、析构函数、拷贝构造、赋值重载)以及运算符重载等内容。 文章还详细分析了`explicit`关键字的作用、静态成员(变量与函数)、友元(友元函数与友元类)的概念及其使用场景,并简要介绍了内部类的特性。
164 0
|
5月前
|
编译器 C++ 容器
【c++11】c++11新特性(上)(列表初始化、右值引用和移动语义、类的新默认成员函数、lambda表达式)
C++11为C++带来了革命性变化,引入了列表初始化、右值引用、移动语义、类的新默认成员函数和lambda表达式等特性。列表初始化统一了对象初始化方式,initializer_list简化了容器多元素初始化;右值引用和移动语义优化了资源管理,减少拷贝开销;类新增移动构造和移动赋值函数提升性能;lambda表达式提供匿名函数对象,增强代码简洁性和灵活性。这些特性共同推动了现代C++编程的发展,提升了开发效率与程序性能。
161 12