搞懂指针,看这一文就够了

什么是指针?

指针是一个特殊类型的变量,它存储的值是内存地址。在编程语言中,通过声明指针,可以告诉编译器我们要存储一个地址,指针的类型和它所指向的数据类型一致。

通过声明指针,将它指向另一个变量,我们可以直接操作另一个变量的值和内存。

内存和内存地址 计算机的内存是存储数据的地方,可以作为一个大的字节数组,每个存储的字节(存储单元)都有一个唯一的内存地址,这些地址类似于房子的号码,用于表示内存中不同的位置,通过这个地址就能找到这个存储的字节(存储单元)。每一个存储单元可以存储一个字节(8bit)的数据。数据(整形、浮点型、字符型等)就保存在这些存储单元。中。

如何使用指针?

使用指针,需要了解两种运算符:取地址运算符(&)间接引用运算符(*)


取地址运算符(&) 首先说说取地址运算符(&),使用取地址运算符(&)可以获得变量的内存地址。下面用部分代码举例子

C++ 复制代码
int a = 10; //步骤1
cout << &a << endl; //步骤2,结果:000000AD69AFFAE4
cout << a << endl; //步骤3,结果:10

上面我们声明一个int类型的变量a,请求在内存空间分配一块连续的存储区域,并赋值为10。使用&a可以拿到变量a的内存地址,而是用a可得到存储的数据。


间接引用运算符(*) 指针是一个特殊类型的变量,它存储的值是内存地址。通过间接引用运算符(*)和指针变量,我们可以访问指针变量锁存储内存地址中的数据。

C++ 复制代码
int a = 10; //  步骤1
int* intPtr = &a;   //步骤2
cout << intPtr << endl; //步骤3,结果:0000000E894FF8A4
cout << &intPtr << endl;//步骤4,结果:0000000E894FF8C8
cout << *intPtr << endl; //步骤5,结果:10
cout << &*intPtr << endl;   //步骤6,结果:0000000E894FF8A4
cout << &a << endl;   //步骤7,结果:0000000E894FF8A4

例子中,步骤2使用到取地址运算符(&)获得变量a的内存地址,并将其赋值给指针intPtr,这样指针变量就指向了变量a的内存地址。

通过步骤3、步骤6、步骤7打印结果,不难看出它们指向同一块内存地址,这块地址就是变量a的内存地址。intPtr显示的是指针变量保存的内存地址,&*intPtr的方式则是可以拆解成两步:先使用*intPtr获取指针指向的内存空间的数据,再使用取地址运算符(&) + *intPtr,也就是&(*intPtr)获得指向内存空间数据的内存地址. 步骤4打印出的值,是指针变量intPtr自身的内存地址,和普通变量获取内存地址的方式相同。

我们知道存储一个int类型的变量需要使用四个连续的存储单元,每个存储单元都有唯一的内存地址。使用取地址运算符(&)获取内存地址的时候,获得的是四个连续存储单元中最前面的那一个,以上面的打印结果为例子,变量a占用的四个存储单元为:0000000E894FF8A4、0000000E894FF8A5、0000000E894FF8A6、0000000E894FF8A7。


使用指针指向类对象,内存地址是如何变化的?

下面创建一个类,里面定义3个int类型的变量,然后再main()总,创建一个类对象并声明一个指针指向它,打印出它的内存地址变化

C 复制代码
class MyClass {
public:
    int a;
    int b;
    int c;
};

int main(){
    
    MyClass* ptr = (MyClass*)malloc(sizeof(MyClass));  // 分配内存并将指针指向该内存

    if (ptr) {
        ptr->a = 1;  // 设置数据成员的值
        ptr->b = 2;
        ptr->c = 3;

        // 访问数据成员
        std::cout << "a: " << ptr->a << ", b: " << ptr->b << ", c: " << ptr->c << std::endl;
        std::cout << "a: " << &ptr->a << ", b: " << &ptr->b << ", c: " << &ptr->c << std::endl;
        std::cout << "&*ptr: " << &*ptr << std::endl;
        std::cout  << "&ptr: " << &ptr << std::endl;
        std::cout  << "ptr: " << ptr << std::endl;

        free(ptr);  // 释放分配的内存
    }

    return 0;
}

/*
打印结果:
    a: 1, b: 2, c: 3
    a: 000002CE00423050, b: 000002CE00423054, c: 000002CE00423058
    &*ptr: 000002CE00423050
    &ptr: 000000D9501DF908
    ptr: 000002CE00423050
*/

通过打印结果可以看出,使用malloc给类对象分配的是一块连续存储单元。指针指向这连续存储单元的最前main的内存地址。

指针的高级用法


动态的内存分配和释放 指针常用于动态分配内存,通过函数如 malloc(在C中)或 new(在C++中),您可以在运行时动态地分配内存。这对于处理不定大小的数据非常重要。然后,使用指针来对刚问并修改这些分配的内存块。最后通过freedelete(在C++中)释放内存。

数组和指针的关系 数组名本身就是一个指针,指向数组的第一个元素。我们可以通过指针进行数组操作,如遍历、更新、修改数组

数组指针 数组指针是指一个指向数组的指针,它指向整个数组,而不是数组中的单个元素。也就是说数组的内存地址保存在指针变量中,数组指针可以通过将数组名作为指针来实现,然后可以对该指针进行指针算术运算,来访问数组中的不同元素

C++ 复制代码
int arr[5] = { 1,6,8,4,5 };
int(*arrPtr)[5];
arrPtr = &arr;  //  将指针arrPtr指向数组的内存地址,数组本身也是指针
cout << "arrPtr:" << arrPtr << endl;
cout << "*arrPtr:" << *arrPtr << endl;
cout << "arr:" << arr << endl;
cout << "&arr:" << &arr << endl;
cout << "*arr:" << *arr << endl;
cout << "**arrPtr:" << **arrPtr << endl;
cout << "*(*arrPtr + 1):" << *(*arrPtr + 1) << endl;
cout << "*(arr + 1):" << *(arr + 1) << endl;
cout << "*arr + 1:" << *arr + 1 << endl;
cout << "arr[0]:" << arr[0]<< endl;
cout << "(*arrPtr)[0]:" << (*arrPtr)[0]<< endl;
cout << "&arr[0]:" << &arr[0]<< endl;

/*
打印结果:
    arrPtr:00000054BF0FFC48
    *arrPtr:00000054BF0FFC48
    arr:00000054BF0FFC48
    &arr:00000054BF0FFC48
    *arr:1
    **arrPtr:1
    *arrPtr + 1:6
    *(arr + 1):6
    *arr + 1:2
    arr[0]:1
    (*arrPtr)[0]:1
    &arr[0]:00000054BF0FFC48
*/

数组本身就是指针,可以通过指针的方式使用它,而数组指针是指向数组的指针,里面存储的是数组的内存空间,有一点抽象,从一定程度上说,可以将数组名 arr 类比为指针 *arrPtr。 根据上面的话再集合例子,可以看出一下几点:

  • *arr**arrPtr,获得的都是数组的第一个元素值,值为1。
  • arr&a取得的都是数组中第一个元素的内存地址
  • arrPtr*arrptr取得的都是保存的数组arr的内存地址
  • 数组可以通过下标来访问内存地址中的元素,如:arr[0],(*atrPtr)[1]。而指针访问不同元素的方式,采用指针加一的方式,如:(arr + 1)、(*arrPtr + 1)来访问下标为1的元素。

指针数组 指针数值是一个数组,每个元素都是一个指针。每个指针指向不同的数据对象,数据对象可以是不同类型。这在处理多个指针或多个数据对象时非常有用。

C++ 复制代码
int num = 10;
float pi = 3.14;
char ch = 'A';

int* intPtr = &num;
float* floatPtr = &pi;
char* charPtr = &ch;

// 声明一个指针数组,其中的每个元素都是指针,可以指向不同类型的数据
void* ptrArr[3];
ptrArr[0] = intPtr;     // 第一个元素指向 int 类型的数据
ptrArr[1] = floatPtr;   // 第二个元素指向 float 类型的数据
ptrArr[2] = charPtr;    // 第三个元素指向 char 类型的数据
 // 通过指针数组访问不同类型的数据
printf("Value of num: %d\n", *((int*)ptrArr[0]));
printf("Value of pi: %.2f\n", *((float*)ptrArr[1]));
printf("Value of ch: %c\n", *((char*)ptrArr[2]));

cout << "intPtr:" << (void *)intPtr << endl;
cout << "floatPtr:" << (void *)floatPtr << endl;
cout << "charPtr:" << (void *)charPtr << endl;

/*
打印结果:
    Value of num: 10
    Value of pi: 3.14
    Value of ch: A
    intPtr:0000001ACF8FFAE4
    floatPtr:0000001ACF8FFB04
    charPtr:0000001ACF8FFB24
*/

在这个例子中,指针数组 ptrArr 中的每个元素都是指针,可以指向不同类型的数据。通过对指针进行类型转换,我们可以访问每个指针所指向的不同类型的数据对象。这种用法在需要处理多个指针或多个不同类型的数据对象时非常有用。


数据结构 指针在构建数据结构(如链表、树、图)时非常重要。通过指针,我们可以将多个存储单元链接在一起,构建出复杂的数据结构。下面例举一个树节点中指针的用法。

arduino 复制代码
typedef struct BiTNode
{
    char data; /*结点数据*/
    struct BiTNode* lchild, * rchild; /*左右孩子指针*/
}BiTNode, * BiTree;

函数参数传递 指针可以用来传递大量的数据,避免复制开销。通过传递指针,可以在函数之间共享和修改数据。下面例举一个参数传递的用法:

scss 复制代码
    //树的前序遍历,传入的参数是树的根结点,BiTree就是BiTNode*
    void PreOrderTraverse(BiTree T) {
        if (T == NULL) {
            return;
        }
        printf("%c", T->data); 
        PreOrderTraverse(T->lchild);    //先遍历左子树
        PreOrderTraverse(T->rchild);    //最后遍历右子树
    }

修改参数 通过传递指针,函数可以修改原始变量的值,即使它们位于不同的作用域。下面例举一个参数传递的用法:

arduino 复制代码
    //树的前序遍历,传入的参数是树的根结点,BiTree就是BiTNode*
    void UpadteTreeData(BiTree T) {
        if (T == NULL) {
            return;
        }
        T->data = 'A';  //修改树结点的值
    }

野指针和空指针

arduino 复制代码
int* x; //代码一,野指针
int* y = NULL;  //代码二,空指针

野指针 代码一中的指针 x是一个野指针,在声明时,指针没有进行指向,指针 x 被分配了一个不确定的值,它可能是任意的内存地址,这被称为野指针。使用野指针进行间接引用(即访问指针指向的内存)可能会导致不可预测的结果,甚至引发程序崩溃。

空指针 代码二的指针 y是一个空指针,在声明时,指针 y 被明确地初始化为 NULL,表示它不指向任何有效的内存地址。使用空指针进行间接引用不会导致访问任何实际的内存,因此它通常被用来表示未初始化的指针或者指针的初始状态。

有疑惑或者不清晰的地方,请在下方留言,我会不定期的更新文章内容。

相关推荐
GeekAGI8 分钟前
Gunicorn 返回 502 问题解析
后端
Rinai9 分钟前
Redis,从数据结构到集群实践的知识总结
redis·后端
GeekAGI12 分钟前
MongoDB 启动错误分析与解决方案
后端
五指坑16 分钟前
告别硬编码:优雅管理状态常量与响应码
java·后端
MacroZheng20 分钟前
27k star!DeepSeek 官方出品,太香了!
java·后端·deepseek
Moment20 分钟前
从输入 URL 到浏览器展示,到底经历了什么 (超级详细!)
前端·javascript·后端
LTPP24 分钟前
Hyperlane:解锁并发编程的未来
前端·后端·github
LTPP26 分钟前
Hyperlane:轻量、高效、安全的 Rust Web 框架新选择
前端·后端·github
努力的搬砖人.1 小时前
RabbitMQ相关的面试题
java·后端·rabbitmq
Goboy1 小时前
从零到一,实现图像识别实践教学
后端·程序员·架构