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

本站文章一览:

相关文章
|
14天前
|
存储 安全 C++
C++中的引用和指针:区别与应用
引用和指针在C++中都有其独特的优势和应用场景。引用更适合简洁、安全的代码,而指针提供了更大的灵活性和动态内存管理的能力。在实际编程中,根据需求选择适当的类型,能够编写出高效、可维护的代码。理解并正确使用这两种类型,是掌握C++编程的关键一步。
19 1
|
14天前
|
数据采集 存储 编译器
this指针如何使C++成员指针可调用
本文介绍了C++中的this指针,它是一个隐藏的指针,用于在成员函数中访问对象实例的成员。文章通过代码示例阐述了this指针的工作原理,以及如何使用指向成员变量和成员函数的指针。此外,还提供了一个多线程爬虫示例,展示this指针如何使成员指针在对象实例上调用,同时利用代理IP和多线程提升爬取效率。
this指针如何使C++成员指针可调用
|
18小时前
|
存储 安全 编译器
【C++航海王:追寻罗杰的编程之路】引用、内联、auto关键字、基于范围的for、指针空值nullptr
【C++航海王:追寻罗杰的编程之路】引用、内联、auto关键字、基于范围的for、指针空值nullptr
10 5
|
2天前
|
存储 Java C#
C++语言模板类对原生指针的封装与模拟
C++|智能指针的智能性和指针性:模板类对原生指针的封装与模拟
|
2天前
|
设计模式 C++ 开发者
C++一分钟之-智能指针:unique_ptr与shared_ptr
【6月更文挑战第24天】C++智能指针`unique_ptr`和`shared_ptr`管理内存,防止泄漏。`unique_ptr`独占资源,离开作用域自动释放;`shared_ptr`通过引用计数共享所有权,最后一个副本销毁时释放资源。常见问题包括`unique_ptr`复制、`shared_ptr`循环引用和裸指针转换。避免这些问题需使用移动语义、`weak_ptr`和明智转换裸指针。示例展示了如何使用它们管理资源。正确使用能提升代码安全性和效率。
13 2
|
5天前
|
C++
C++一分钟之-继承与多态概念
【6月更文挑战第21天】**C++的继承与多态概述:** - 继承允许类从基类复用代码,增强代码结构和重用性。 - 多态通过虚函数实现,使不同类对象能以同一类型处理。 - 关键点包括访问权限、构造/析构、菱形问题、虚函数与动态绑定。 - 示例代码展示如何创建派生类和调用虚函数。 - 注意构造函数初始化、空指针检查和避免切片问题。 - 应用这些概念能提升程序设计和维护效率。
16 2
|
7天前
|
存储 算法 安全
C++一分钟之-数组与指针基础
【6月更文挑战第19天】在C++中,数组和指针是核心概念,数组是连续内存存储相同类型的数据,而指针是存储内存地址的变量。数组名等同于指向其首元素的常量指针。常见问题包括数组越界、尝试改变固定大小数组、不正确的指针算术以及忘记释放动态内存。使用动态分配和智能指针可避免这些问题。示例代码展示了安全访问和管理内存的方法,强调了实践的重要性。
24 3
|
11天前
|
编译器 Linux C++
C++智能指针
**C++智能指针是RAII技术的体现,用于自动管理动态内存,防止内存泄漏。主要有三种类型:已废弃的std::auto_ptr、不可复制的std::unique_ptr和可共享的std::shared_ptr。std::unique_ptr通过禁止拷贝和赋值确保唯一所有权,而std::shared_ptr使用引用计数来协调多个指针对同一资源的共享。在C++17中,std::auto_ptr因设计缺陷被移除。**
|
11天前
|
编译器 C++
《Effective C++ 改善程序与设计的55个具体做法》 第二章 构造/析构/赋值运算 笔记
《Effective C++ 改善程序与设计的55个具体做法》 第二章 构造/析构/赋值运算 笔记
|
11天前
|
安全 程序员 编译器
【C语言基础】:深入理解指针(二)
【C语言基础】:深入理解指针(二)
【C语言基础】:深入理解指针(二)