基础知识-指针
1、指针的基本概念
1.1 什么是指针
1.1.1 指针的定义
指针是一种特殊的变量,与普通变量存储具体数据不同,它存储的是内存地址。在计算机程序运行时,数据都被存放在内存中,而指针就像是指向这些数据存放位置的 “路标”。通过指针,程序可以间接访问和操作对应内存地址的数据。
1.1.2 指针与普通变量的区别
普通变量直接存储数据值,比如 int num = 10; ,变量 num 中直接存放的是数值 10 。而指针变量存储的是内存地址,例如 int num = 10; int *ptr = # ,指针 ptr 存储的是变量 num 在内存中的地址,要获取 num 的值,需要通过解引用操作 *ptr 。
此外,普通变量的运算基于其存储的数据类型,如 int 型变量可进行加减乘除;指针变量的运算则围绕内存地址偏移,比如 ptr++ 会根据 int 类型数据在内存中占用的字节数(通常 4 字节),将指针指向下一个 int 数据的地址。
1.2 内存与地址的关系
内存是计算机用于临时存储数据和程序的硬件设备,就像一个庞大的仓库,被划分成一个个连续的小格子,每个小格子都有唯一的编号,CPU 通过这些编号,找到相应的内存空间,这个编号就是内存地址。数据在存储时,会被分配到特定的内存地址空间中。地址就如同仓库格子的编号,程序通过地址来准确找到数据在内存中的存放位置,从而实现对数据的读写操作 。当定义一个变量时,系统会在内存中为其分配一定的空间,并赋予对应的内存地址,而指针变量存储的就是这些地址,以此建立起对数据的间接访问通道。
形象地来说,内存是一栋楼,一个内存单元是一户人家,指针是门牌号,CPU是我们,我们通过门牌号可以找到那户人家住的地方。
1.2.1 计算机内存结构简介
计算机内存通常采用线性编址结构,从低地址到高地址连续排列。在逻辑上,内存可分为多个区域,如栈区、堆区、全局数据区、代码区等。
- 栈区:主要用于存储函数调用时的局部变量、函数参数等,遵循后进先出原则
- 堆区:用于动态内存分配,可通过 malloc (C 语言)或 new (C++ 语言)等函数在堆上申请内存
- 全局数据区:存放全局变量和静态变量
- 代码区:存储程序的可执行代码
指针在不同内存区域的数据操作中都发挥着关键作用,比如在堆区通过指针管理动态分配的内存,在栈区利用指针传递函数参数等。
1.2.2 地址的作用与表示
地址的核心作用是标识内存中数据的存储位置,它使得程序能够准确找到并操作数据。
在计算机中,地址通常以二进制形式存储和处理,但在编程中,常以十六进制数表示,便于阅读和理解。在 C 语言的调试过程中,打印指针变量的值,显示的就是十六进制的内存地址。
2、指针的基本语法
2.1 指针的声明与初始化
2.1.1 指针的声明
语法格式:数据类型 *指针名;
注:“数据类型” 表明该指针所指向变量的类型,“指针变量名” 则是用户为指针取的名字
int *ptr; //声明一个指向int类型数据的指针ptr
float *fptr; //声明一个指向float类型数据的指针fptr
2.1.2 指针的初始化
指针初始化就是在声明指针的同时为其赋予一个合法的内存地址。
常见的初始化方式有:
- 初始化为 NULL
- 初始化为变量的地址
- 初始化为动态分配的内存地址
//初始化为 NULL
int *ptr = NULL;//初始化为变量的地址
int num = 10;
int *ptr = #//初始化为动态分配的内存地址
int *ptr = (int *)malloc(sizeof(int));
2.2 取地址操作符&
作用:用于获取变量的内存地址。
#include <stdio.h>int main()
{int num = 10;int *ptr = # //使用取地址操作符&获取变量num的地址并赋给指针ptrprintf("变量 num 的地址: %p\n", (void *)&num);printf("指针 ptr 存储的地址: %p\n", (void *)ptr);return 0;
}
在使用 printf 函数输出指针的值时,%p 格式说明符要求对应的参数是 void * 类型。这是由于 %p 用于以十六进制形式输出指针所存储的内存地址,而 void * 类型的指针可以存储任意类型的地址,能确保输出的是纯粹的地址信息。
2.3 解引用操作符*
作用1:通过指针访问所指向的值。
#include <stdio.h>int main()
{int num = 10;int *ptr = #int value = *ptr; //通过解引用操作符*获取指针ptr所指向的值printf("指针 ptr 所指向的值: %d\n", value); //输出为10return 0;
}
作用2:修改指针指向的值。
#include <stdio.h>int main()
{int num = 10;int *ptr = #*ptr = 20; //通过解引用操作符*修改指针ptr所指向的值printf("变量 num 的新值: %d\n", num); //输出为20return 0;
}
2.4 空指针
空指针表示不指向任何有效内存地址的指针。
- 在 C 语言中,通常用 NULL 来表示空指针
- 在 C++ 11 及以后的版本中,推荐使用 nullptr
int main()
{//C环境下int *ptr = NULL; //C语言中使用NULL初始化空指针printf("指针 ptr 的值: %p\n", (void *)ptr);//C++环境下int *ptr = nullptr; //C++中使用nullptr初始化空指针std::cout << "指针 ptr 的值: " << ptr << std::endl;return 0;
}
3、指针的核心应用
3.1 函数参数传递
3.1.1 值传递
在值传递中,函数调用时会将实参的值复制一份给形参。函数内部对形参的任何修改都只会影响形参本身,而不会影响到实参。
#include <stdio.h>void changeValue(int num)
{num = 20;printf("函数内部 num 的值: %d\n", num); //输出为20
}int main()
{int num = 10;changeValue(num);printf("函数外部 num 的值: %d\n", num); //输出依旧为10,值传递不影响实参return 0;
}
3.1.2 指针传递
指针传递是将实参的地址传递给形参,形参是一个指针,它指向实参所在的内存地址。因此,函数内部可以通过指针来修改实参的值。
#include <stdio.h>void changeValue(int *ptr)
{*ptr = 20;printf("函数内部指针指向的值: %d\n", *ptr); //输出为20
}int main()
{int num = 10;changeValue(&num);printf("函数外部 num 的值: %d\n", num); //输出为20,指针传递会改变实参return 0;
}
3.2 动态内存分配
- C 语言:malloc、calloc、realloc 和 free
- C++ 语言:new 和 delete
3.2.1 C 语言
malloc
用于在堆上分配指定大小的内存块,返回一个指向该内存块起始地址的指针。如果分配失败,返回 NULL。
#include <stdio.h>
#include <stdlib.h>int main()
{int *ptr = (int *)malloc(5 * sizeof(int));if (ptr == NULL){printf("内存分配失败\n");return 1;}//初始化数组元素for (int i = 0; i < 5; i++) {ptr[i] = i;}//输出数组元素for (int i = 0; i < 5; i++) {printf("%d ", ptr[i]);}printf("\n");//释放内存free(ptr);ptr = NULL;return 0;
}
calloc
用于在堆上分配指定数量和大小的内存块,并将其初始化为 0。返回一个指向该内存块起始地址的指针。如果分配失败,返回 NULL。
#include <stdio.h>
#include <stdlib.h>int main()
{int *ptr = (int *)calloc(5, sizeof(int));if (ptr == NULL) {printf("内存分配失败\n");return 1;}//输出数组元素,由于calloc会初始化为0,所以输出全为0for (int i = 0; i < 5; i++) {printf("%d ", ptr[i]);}printf("\n");//释放内存free(ptr);ptr = NULL;return 0;
}
realloc
用于调整已分配内存块的大小。可以扩大或缩小内存块。返回一个指向新内存块起始地址的指针。如果分配失败,返回 NULL,原内存块不会被释放。
#include <stdio.h>
#include <stdlib.h>int main()
{int *ptr = (int *)malloc(3 * sizeof(int));if (ptr == NULL) {printf("内存分配失败\n");return 1;}//初始化数组元素for (int i = 0; i < 3; i++) {ptr[i] = i;}//扩大内存块int *newPtr = (int *)realloc(ptr, 5 * sizeof(int));if (newPtr == NULL) {printf("内存重新分配失败\n");free(ptr);ptr = NULL;return 1;}ptr = newPtr;//初始化新增的数组元素for (int i = 3; i < 5; i++) {ptr[i] = i;}//输出数组元素for (int i = 0; i < 5; i++) {printf("%d ", ptr[i]);}printf("\n");//释放内存free(ptr);ptr = NULL;return 0;
}
分析代码里的内存分配情况:
- 借助 malloc 函数分配了 3 块 int 类型的内存,并且把指向这块内存的指针赋值给 ptr
- 利用 realloc 函数把之前分配的 3 块 int 类型内存重新分配为 5 块 int 类型的内存。要是重新分配成功,realloc 函数会返回一个指向新内存块的指针,然后把这个指针赋值给 newPtr
- 把 newPtr 的值赋给 ptr,这样 ptr 就指向了新分配的 5 块 int 类型的内存
注意:先分配的 3 块内存并没有单独释放,而是在重新分配内存后,将其包含在新的内存块中,最终一起释放。
free
用于释放之前通过 malloc、calloc 或 realloc 分配的内存块。释放后的内存可以被再次分配。
注意:释放后的指针成为野指针,建议将其置为 NULL,避免误操作。
3.2.2 C++ 语言
new
用于在堆上分配内存并构造对象。对于基本数据类型,直接分配内存;对于类类型,会调用构造函数。
#include <iostream>
using namespace std;int main()
{//分配一个int型内存,并初始化为10,也可以不初始化,直接new int就可以int *ptr = new int(10);cout << *ptr << endl;//分配一个int型数组,长度为5int *arr = new int[5];for (int i = 0; i < 5; i++) {arr[i] = i;}for (int i = 0; i < 5; i++) {cout << arr[i] << " ";}cout << endl;// 释放内存delete ptr;delete[] arr; //释放数组return 0;
}
delete
用于释放通过 new 分配的内存。对于基本数据类型,直接释放内存;对于类类型,会调用析构函数。
注意:释放数组时需要使用 delete[ ],否则会导致内存泄漏。
3.2.3 结构体与指针的结合使用
结构体可以将不同类型的数据组合在一起,而指针可以方便地访问和操作结构体。可以定义指向结构体的指针,通过指针来访问结构体的成员。
#include <stdio.h>
#include <stdlib.h>//定义结构体
typedef struct
{int age;char name[20];
} Person;int main()
{//创建结构体变量Person p = {20, "John"};//创建指向结构体的指针Person *ptr = &p;//通过指针访问结构体成员printf("Name: %s, Age: %d\n", ptr->name, ptr->age);return 0;
}
4、指针相关问题
4.1 野指针
4.1.1 产生原因
- 指针未初始化:定义指针变量后没有给它赋一个合法的地址值,此时指针指向的位置是随机的,变成野指针。
int* ptr; //ptr就是一个野指针
- 指针所指向的内存被释放后未置空:当使用 free 或 delete 释放了指针所指向的内存后,如果没有将指针设置为 NULL,指针仍然保存着原来已释放内存的地址,就变成了野指针。
int* ptr = (int*)malloc(sizeof(int));
free(ptr);
//此时ptr成为野指针,如果再次访问*ptr就会有问题
- 指针越界操作:当指针进行了不恰当的运算,使其指向了不属于原本所指向的内存区域,也会形成野指针。比如对数组指针进行越界的移动操作。
int main()
{//定义一个包含5个元素的整型数组int arr[5] = {1, 2, 3, 4, 5};//定义一个指针指向数组的首元素int *ptr = arr;//正常访问数组元素for (int i = 0; i < 5; i++) {printf("arr[%d] = %d\n", i, *(ptr + i));}//越界操作:将指针移动到数组范围之外ptr = ptr + 5;//尝试访问越界后的指针指向的内存printf("越界访问的值: %d\n", *ptr);return 0;
}
4.1.2 解决方法
- 初始化指针:在定义指针变量时,将其初始化为 NULL 或者指向一个合法的内存地址。
//法一:初始化为 NULL
int* ptr = NULL;//法二:指向一个合法的内存地址
int num;
int* ptr = #
- 内存释放后置空指针:在使用 free 或 delete 释放内存后,立即将指针赋值为 NULL,这样可以避免再次误操作该指针。
int* ptr = (int*)malloc(sizeof(int));
free(ptr);
ptr = NULL; //释放后置空
- 小心指针运算:进行指针运算时,要确保不超出所指向内存的范围,对于数组指针,要根据数组的大小进行合理的指针移动。
4.2 内存泄漏
4.2.1 动态内存未释放的后果
- 内存浪费:程序占用的内存会不断增加,导致系统可用内存减少,影响其他程序的运行,甚至可能导致系统性能下降。
- 程序崩溃:当系统内存耗尽时,程序可能会因为无法分配到所需的内存而崩溃。
- 资源耗尽:在一些资源有限的环境中,内存泄漏可能会导致系统无法正常工作,因为没有足够的内存来执行其他必要的操作。
4.2.2 解决方法
- 及时释放内存:在使用完动态分配的内存后,要及时使用 free 或 delete 释放内存。
- 使用智能指针(C++):C++ 提供了智能指针(如 std::unique_ptr、std::shared_ptr)来自动管理内存,当智能指针超出作用域时,会自动释放所指向的内存,从而避免内存泄漏。
4.3 指针运算
4.3.1 指针加减整数的运算
指针加减整数的运算结果是一个新的指针,其指向的位置会根据指针所指向的数据类型的大小进行移动。
#include <stdio.h>int main()
{int arr[5] = {1, 2, 3, 4, 5};int *ptr = arr; //ptr指向数组的第一个元素//指针加1int *next_ptr = ptr + 1;printf("ptr 指向的值: %d\n", *ptr); //输出为1printf("ptr + 1 指向的值: %d\n", *next_ptr); //输出为2//指针减1int *prev_ptr = next_ptr - 1;printf("next_ptr - 1 指向的值: %d\n", *prev_ptr); //输出为1return 0;
}
4.3.2 指针间的减法运算
指针的减法运算通常用于计算两个指针之间的距离,结果是一个整数,表示两个指针之间相差的元素个数,前提是这两个指针指向同一块连续的内存区域(如数组)。
#include <stdio.h>int main()
{int arr[5] = {1, 2, 3, 4, 5};int *ptr1 = &arr[0]; //指向数组第一个元素int *ptr2 = &arr[3]; //指向数组第四个元素//计算两个指针之间的距离int distance = ptr2 - ptr1;printf("ptr2 和 ptr1 之间相差的元素个数: %d\n", distance); //输出为3,表示它们之间相差3个int型元素return 0;
}
4.3.3 指针间的比较
只有当两个指针指向同一块连续的内存区域(如数组)且有关联时,进行比较才有意义。例如,比较数组中不同元素的指针,判断它们的先后顺序。
#include <stdio.h>int main()
{int arr[5] = {1, 2, 3, 4, 5};int *ptr1 = &arr[0]; //指向数组第一个元素int *ptr2 = &arr[3]; //指向数组第四个元素//比较指针if (ptr1 < ptr2) {printf("ptr1 指向的元素在 ptr2 指向的元素之前\n");} else {printf("ptr1 指向的元素在 ptr2 指向的元素之后或相同\n");}return 0;
}
4.4 二级指针、三级指针
4.4.1 二级指针
二级指针是指向指针的指针。也就是说,二级指针所存储的地址是一个指针变量的地址,而这个指针变量又指向实际的数据。
4.4.2 三级指针
三级指针是指向二级指针的指针,即它存储的地址是一个二级指针变量的地址。
int num = 10;
int* ptr1 = # //一级指针
int** ptr2 = &ptr1; //二级指针
int*** ptr3 = &ptr2; //三级指针
5、指针数组与数组指针
5.1 指针数组
- 定义:元素为指针的数组
- 语法格式:数据类型* 数组名[数组长度]
#include <stdio.h>int main()
{//定义一个数组a,并给数组赋值int a[10];for (int i = 0; i < 10; i++){a[i] = i + 1;}//指针数组:数组中的每一个元素都是指针int* arr[10] = { a, a + 1, a + 2, a + 3, a + 4, a + 5, a + 6, a + 7, a + 8, a + 9 };//a + 1 == &a[0] + 1 == &a[1];//a[5] ? --> a数组中下标为5的元素是多少?//printf("%d %d", *arr[5], **(arr + 5));//**(arr + 5)的推导//arr == &arr[0];//arr + 5 == &arr[0] + 5 == &arr[5];//*(arr + 5) == *(&arr[5]) == arr[5];//**(arr + 5) == *arr[5] == a[5];return 0;
}
5.2 数组指针
- 定义:指向某个数组的指针
- 语法格式:数据类型 (*指针名) [数组长度]
#include <stdio.h>int main()
{//定义一个数组a,并给数组赋值int a[10];for (int i = 0; i < 10; i++){a[i] = i + 1;}//数组指针:本身是一个指针,存的是元素类型是int、元素个数为10的数组的地址int (*arry)[10];arry = &a; //赋值printf("%d", *((int*)(arry + 1) - 1)); //输出为10return 0;
}
输出语句 printf("%d", *((int*)(arry + 1) - 1)); 的详细解释:
- arry + 1:由于 arry 是一个指向包含 10 个 int 元素数组的指针,arry + 1 会让指针向后移动一个包含 10 个 int 元素数组的长度,也就是跳过整个数组 a 所占用的内存空间,指向数组 a 之后的内存位置。
- (int*):将 arry + 1 的结果强制转换为 int* 类型的指针。这一步的作用是将原本指向整个数组的指针转换为指向单个 int 元素的指针,以便后续进行以 int 为单位的指针运算。
- (int*)(arry + 1) - 1:在强制转换为 int* 类型指针后,进行减 1 操作。因为现在是 int* 类型的指针,减 1 会让指针向前移动一个 int 类型的长度,也就是回到数组 a 的最后一个元素的地址。
- *((int*)(arry + 1) - 1):对前面得到的指针进行解引用操作,获取该指针所指向的内存位置存储的值,也就是数组 a 的最后一个元素的值,即 10。
6、指针函数与函数指针
6.1 指针函数
- 定义:本身是一个函数,返回值是一个指针
- 语法格式:数据类型* 函数名(参数列表)
#include <stdio.h>//指针函数:返回值为指针的函数
//注意:这里返回局部变量的地址会导致未定义行为,因为局部变量在函数结束后会被销毁
//可以使用静态局部变量来解决上述问题int* fun()
{static int a = 10; //使用静态局部变量return &a;
}int main()
{int* ptr = fun();printf("访问返回指针指向的值: %d\n", *ptr);return 0;
}
6.2 函数指针
- 定义:本身是一个指针,存的是函数的地址
- 语法格式:数据类型 (*指针名) (参数列表)
注:“数据类型”指的是函数的返回值类型,“参数列表”指的是函数的参数列表
#include <stdio.h>//函数的定义不允许嵌套
//函数的调用允许嵌套
//当函数体在主函数下方时,需要在函数上方进行函数声明//声明fff函数
int fff();int main()
{//函数指针:本身是一个指针,存的是函数的地址int (*f)() = fff; //fff等价于&fff//函数的调用方式fff();f(); //通过地址访问,fff()等价于f()等价于(*f)()return 0;
}//定义fff函数
int fff()
{printf("asd");return 0;
}
- fff():这是普通的函数调用方式。fff 是函数名,直接在函数名后面加上括号并传入相应的参数( fff 函数没有参数),就可以调用这个函数。当执行 fff() 时,程序会跳转到 fff 函数的代码块中执行其中的语句,即输出字符串 "asd"。
- f():f 是一个函数指针,它存储了函数 fff 的地址。当使用 f() 这种形式调用时,实际上是通过函数指针 f 来调用它所指向的函数(即 fff 函数)。因为 f 指向了 fff 函数的地址,所以 f() 的效果和直接调用 fff() 是一样的,程序同样会跳转到 fff 函数的代码块中执行。
- (*f)():这种写法也是通过函数指针 f 来调用函数。在 C 语言中,函数指针本质上是一个指向函数的地址的指针变量。*f 是对函数指针 f 进行解引用操作,从概念上来说,*f 就表示 fff 函数。所以 (*f)() 同样是调用 f 所指向的函数,它和 f() 以及 fff() 的作用是等价的,最终都会执行 fff 函数中的代码。