指针(pointer)
一、指针基础核心语法
1. 指针的本质
指针:我们知道内存基本存储单位是byte,而内存中每个byte都有个唯一编号,称为他的地址,具体讲就是个十六进制数,也称为指针。
指针变量:存放变量内存地址的特殊变量,核心作用是通过地址间接访问、修改目标变量的值。
2. 指针的定义与初始化
定义语法
指向某类型变量的指针,定义格式为:类型名* 指针名; 或 类型名 *指针名;
- 示例:
int* ptr;(定义一个指向int型变量的指针ptr) - 真题高频错误写法:
*int ptr;、int ptr*;、ptr int;
初始化规则
指针必须用同类型变量的内存地址初始化,未初始化的指针为「野指针」,禁止直接解引用。
正确示例:
int a = 10; int* p = &a; // 用变量a的地址&a初始化指针p,p指向a高频错误写法:
- 用变量值直接初始化指针:
int a=5; int *p=a;(错误,把变量值当内存地址) - 用普通变量存指针:
int a=5; int p=&a;(错误,内存地址无法存入普通变量) - 未初始化就解引用赋值:
int* ptr; *ptr = 10;(错误,野指针,访问非法内存)
- 用变量值直接初始化指针:
3. 核心运算符:取地址符& 与 解引用符*
&变量名:取该变量在内存中的起始地址,示例:&a得到变量a的地址。*指针名:解引用,访问指针指向的内存空间,可读取/修改目标变量的值。真题高频核心考点:通过解引用修改原变量的值
int a = 42; int* p = &a; *p = *p + 1; // 等价于 a = a + 1,最终a的值为43解引用操作修改的是指针指向的原变量,而非指针本身。
这里特别注意对p和 p 的理解,修改p是修改它指向谁,而修改 p则是修改它指向地址里面的内容
4. 同类型指针的赋值
同类型的指针可以直接互相赋值,赋值后多个指针指向同一块内存空间,修改任意一个指针的解引用值,都会影响原变量。
真题示例(第4题):
int a = 5; int* p1 = &a; int* p2 = p1; // p2和p1指向同一个变量a *p2 = 10; // 等价于 a=10,最终a、*p1、*p2的值均为10
二、指针与函数传参
1. 三种参数传递方式对比
| 传递方式 | 函数形参定义 | 实参传递 | 能否修改实参 | 能否避免大对象拷贝 |
|---|---|---|---|---|
| 值传递 | void func(int x) | 直接传变量func(a) | ❌ 仅修改副本,不影响实参 | ❌ 会生成完整副本 |
| 指针传递 | void func(int* p) | 传变量地址func(&a) | ✅ 通过解引用*p修改实参 | ✅ 仅传递地址,无拷贝 |
| 引用传递 | void func(int& x) | 直接传变量func(a) | ✅ 直接修改实参 | ✅ 仅传递别名,无拷贝 |
2. 核心考点
值传递的本质:函数接收的是实参的副本,函数内对形参的任何修改,都不会影响外部实参。
示例:
void increaseA(int x) { x++; } int main() { int a = 5; increaseA(a); // 值传递,仅修改副本x,a的值仍为5 return 0; }
指针传递的用法:形参为指针类型,实参必须传递变量的地址;函数内通过
*解引用,直接修改实参的值。示例:
void increaseB(int* p) { (*p)++; } int main() { int a = 5; increaseB(&a); // 传a的地址,解引用修改a,最终a=6 return 0; }- 易错点:必须加括号
(*p)++,若写成*p++,会因运算符优先级问题,先移动指针再解引用,不会修改原变量的值。
- 引用传递的用法:
引用: 也叫别名,本身不占内存,声明后直接绑定到某个现有同类型数据,俩名字代表一个东西。
形参为变量的引用(别名),函数内对形参的修改直接作用于实参,用法比指针更简洁。 - 其他考点:想要避免大型对象的拷贝,或在函数内修改外部实参,指针传递和引用传递均可实现,值传递无法实现。
三、指针与一维数组(核心考点)
1. 数组名的本质
数组名是数组首元素的内存地址,是一个常量指针,不能修改其指向。
- 核心等价关系:
arr == &arr[0],数组名arr直接代表数组第一个元素的地址。
2. 数组与指针的等价访问规则
数组的下标访问,完全可以用指针的解引用实现,核心等价公式:arr[i] == *(arr + i)
- 示例:
arr[2]等价于*(arr + 2),代表数组第3个元素的值。
3. 指针的算术运算
指针加减整数,不是简单的地址数值加减,而是按指针指向的类型大小进行偏移。
- 示例:
int* p;,p+1代表地址向后偏移sizeof(int)(4字节),指向数组的下一个元素,而非地址十六进制数值+1(实际相当于+4)。 示例:
int arr[4] = {0,1,2,3}; int* p = arr; p += 1; // 最终p的值是arr[1]的地址,而非数值1
4. 指针的下标访问
指向数组的指针,可以直接使用数组的[]下标语法,等价于解引用操作:p[i] == *(p + i)
示例:
double* p_arr = new double[3]; p_arr[0] = 0.2; p_arr[1] = 0.5; p_arr[2] = 0.8; p_arr += 1; // 指针向后偏移1个元素,指向原p_arr[1] cout << p_arr[0] << endl; // 等价于*(p_arr+0),输出0.5
四、指针与二维数组
1. 二维数组的内存布局
C++中二维数组采用行优先存储,所有元素在内存中是连续排列的。
- 示例:
int arr[2][3] = {{1,2,3},{4,5,6}};
实际内存存储顺序为顺序的:arr[0][0] → arr[0][1] → arr[0][2] → arr[1][0] → arr[1][1] → arr[1][2],也就是说如果arr0后面的地址里就是arr1。
2. 二维数组的地址等价转换
arr[i][j] == *(*(arr + i) + j)
公式解释:
arr + i:偏移i行,指向第i行的一维数组;*(arr + i):取第i行的首地址,等价于arr[i];*(arr + i) + j:在第i行中偏移j列,指向arr[i][j]的地址;*(*(arr + i) + j):解引用,得到arr[i][j]的值。
- 示例:
int arr[2][3] = {{1,2,3},{4,5,6}};,*(*(arr + 1) + 2)等价于arr[1][2],值为6。
3. 二维数组名的本质
二维数组名是指向一维数组的指针(数组指针)。
- 示例:
int arr[3][4];,arr的类型为int (*)[4],指向一个长度为4的int型一维数组;arr + 1会偏移一整行的大小(4*4=16字节),直接指向第二行的起始位置。
4. 用一级指针遍历二维数组
利用二维数组内存连续的特性,可通过一级指针遍历所有元素,核心公式:arr[i][j] == *(p + i * 列数 + j),其中p = &arr[0][0](数组首元素地址)
真题示例(第10题):
int arr[3][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12}}; int* p = &arr[0][0]; // *(p + 5) 等价于 arr[1][1],值为6
5. 二维数组的内存地址计算
已知数组首地址、元素类型大小,计算arr[i][j]的内存地址
示例:
32位int占4字节,二维数组int array[2][3]首地址为0x7ffee4065820,则&array[1][1]的地址为:0x7ffee4065820 + 4 * 4 = 0x7ffee4065820 + 0x10 = 0x7ffee4065830- 4 * 4 : 顺序考虑,ar1距离ar0隔着四个元素,每个占四字节。
- 16进制下的16就是0x10
6. 静态二维数组 vs 动态二维数组
| 类型 | 定义方式 | 内存位置 | 释放方式 | 内存连续性 |
|---|---|---|---|---|
| 静态二维数组 | int arr[3][4]; | 栈内存 | 函数结束自动释放 | 连续 |
| 动态一维数组模拟二维 | int* arr = new int[12]; | 堆内存 | 手动delete[] arr;释放 | 连续 |
- 静态数组在栈上分配,自动释放;动态数组在堆上分配,必须手动释放,无法自动释放。
五、指针与结构体(常考考点)
1. 结构体指针的定义与初始化
// 定义结构体
struct Point {
int x, y;
};
struct Student {
string name;
int age;
};
// 结构体指针初始化
Point p = {10, 20};
Point* ptr = &p; // 用结构体变量的地址初始化指针2. 结构体指针的成员访问
核心访问运算符为->,等价于解引用后用.访问成员:ptr->成员名 == (*ptr).成员名
真题示例(第16题):
struct Rectangle { Point topLeft; Point bottomRight; }; Rectangle rect = {{10,10}, {20,20}}; Point* p = &rect.bottomRight; p -> y = 5; // 等价于 (*p).y =5,修改rect.bottomRight.y的值为5
3. 结构体数组与指针
- 静态结构体数组:
Student students[20]; - 动态结构体数组:
Student* students = new Student[20];(合法写法,真题第19题判断题考点) - 错误写法:
Student students = new Student[20];(错误,new返回指针,不能赋值给结构体变量)
六、堆内存动态分配(new/delete)
除了我们熟悉的声明变量、数组来申请内存,还可以用new关键字直接申请内存,并把返回地址存到指针里。
1. new运算符:申请堆内存
- 申请单个变量:
int* p = new int;// 分配1个int占用的内存,返回首地址 - 申请数组:
int* p = new int[10];// 分配10个int占用的内存,返回首地址 - 申请结构体数组:
Student* p = new Student[20];// 分配20个Student占用的内存,返回首地址
2. delete运算符:释放堆内存
- 释放单个变量:
delete p; - 释放数组:
delete[] p;(数组释放必须加[])
3. 核心考点与易错点
- 堆内存申请后必须手动释放,否则会造成内存泄漏;栈内存会自动释放,无需手动操作。
释放内存时,指针必须指向申请时的首地址;若指针已发生偏移,必须先回到首地址再释放,否则会导致程序崩溃。
double* p_arr = new double[3]; p_arr += 1; // 指针偏移 p_arr -= 1; // 回到首地址 delete p_arr; // 正确释放- 不能释放栈内存的地址,只能释放new申请的堆内存。
七、易错判断题汇总
核心:分清楚类型,是指针还是普通变量
int a, b;
int *p = &a; // 正确 指针赋值给指针变量
b = p; // 错误,指针赋值给普通变量
b = *p; // 正确,解引用得到普通int,赋值给整数变量
a = &b; // 错误,指针赋值给普通变量
int * q = p; // 正确,指针赋值给指针| 题目说法 | 正确/错误 | 核心原因 |
|---|---|---|
int x=5; int *p=&x; *p=*p+3; 运行后x的值变成8 | 正确 | 解引用修改指针指向的原变量x的值 |
int a=5; int *p=a; 能正确初始化指针 | 错误 | 不能用变量值初始化指针,必须用变量地址&a |
struct Student {...}; Student* students = new Student[20]; 代码合法 | 正确 | 正确定义动态结构体数组,new返回指针赋值给结构体指针 |
数组arr首地址为0x7ffee4065820,int* p=arr; p+=1;后p的值是1 | 错误 | 指针算术运算按类型大小偏移,p的值是arr[1]的地址,而非数值1 |
void add(int &x){ x += 10; } int a=5; add(a); 运行后a的值变成15 | 正确 | 引用传递直接修改实参的值 |
void func(int* p) { *p = 10; } int a=5; func(&a); cout<<a; 输出10 | 正确 | 指针传递通过解引用修改实参a的值 |
int* ptr; *ptr = 10; 可以正确定义并初始化指针 | 错误 | 指针未初始化(野指针),直接解引用访问非法内存 |
评论已关闭