承接上文:上一节我们理清了指针与数组底层内存布局,弄懂数组传入函数会退化为指针。本节聚焦指针最常用的工程场景:指针函数参数,对比值传递与地址传递,搞懂为什么指针可以在函数内部修改外部变量,同时讲解指针传数组的标准写法、空指针安全校验、常见踩坑点,建立工程化编码习惯。
学习说明
很多新手疑惑:函数内部修改形参,为什么外部原始变量不会变化?本质是值传递会拷贝一份独立副本。 想要在函数内直接修改外部原始变量,有两种主流方案:指针传参(本节)、引用传参(后续章节)。 指针传参是C语言兼容、C++工程里高频使用的写法,核心原理:传递变量的内存地址,函数拿到地址,直接操作原始内存,无需拷贝原始数据。
本节学习目标
- 理解值传递的副本机制,明白值传递无法修改外部变量的根本原因
- 掌握指针做函数参数:地址传递原理,能在函数内修改外部变量
- 掌握数组通过指针传入函数的标准工程写法,理解数组退化
- 学会指针参数的安全校验:判断空指针,规避崩溃
- 区分指针传参的两大使用场景:修改外部变量、批量传递数组
- 识别指针传参常见坑:野指针、空指针解引用、越界访问
一、回顾:值传递,只能修改副本
值传递调用函数时,会拷贝一份变量的值给形参,形参和实参是两块独立内存,修改副本不会影响外部原始变量。
#include <iostream>
using namespace std;
void change(int num)
{
num = 100; // 修改的只是副本,外部变量不受影响
cout << "函数内 num = " << num << endl;
}
int main()
{
int a = 10;
change(a);
cout << "函数调用后 a = " << a << endl; // a仍然是10
return 0;
}
运行输出:
函数内 num = 100
函数调用后 a = 10
核心结论:值传递传递的是完整数据副本,形参和实参相互独立,无法修改外部实参。
值传递内存示意图
值传递(独立副本,互不干扰)
main: a = 10 [0x2000] 原始变量
change: num = 100 [0x3000] 拷贝副本
修改num,仅修改0x3000内存,原始a完全不受影响
二、地址传递:指针作为函数参数,修改外部变量
不再传递变量的值,而是传递变量的内存地址 。函数形参接收这个地址(指针变量),通过解引用*操作,直接读写外部原始变量的内存。
#include <iostream>
using namespace std;
void change(int* p)
{
// 安全校验:先判断指针是否为空,防止空指针解引用崩溃
if(p == nullptr)
{
cout << "空指针,不执行操作" << endl;
return;
}
*p = 100; // 解引用,修改p指向的原始变量
cout << "函数内 *p = " << *p << endl;
}
int main()
{
int a = 10;
change(&a); // 传入变量a的地址
cout << "函数调用后 a = " << a << endl; // a被修改为100
return 0;
}
运行输出:
函数内 *p = 100
函数调用后 a = 100
原理总结(修正严谨表述)
- 实参:传入
&a(变量a的内存地址) - 形参:
int* p,指针变量接收地址 *p:根据地址找到外部变量a,直接修改原始内存- 重要补充 :地址传递同样存在拷贝,仅拷贝8字节地址值,不会拷贝原始大数据,节省内存、支持修改原值
地址传递内存示意图
地址传递(指向同一块内存)
main: a = 10 [0x2000] 原始变量
change: p = 0x2000 指针指向原始内存
*p = 100 直接修改 0x2000 内存,外部a同步变化
三、工程场景1:指针传参实现交换两个变量
值传递无法完成变量交换,仅地址传递可以实现。先展示值传递失败案例,再展示指针成功案例,对比更直观。
1. 值传递:交换失败(错误示范)
#include <iostream>
using namespace std;
// 值传递:仅交换副本,外部无变化
void swapFail(int a, int b)
{
int temp = a;
a = b;
b = temp;
}
int main()
{
int x = 10, y = 20;
swapFail(x, y);
cout << "值传递交换后:x=" << x << ", y=" << y << endl; // 数值不变
return 0;
}
2. 指针地址传递:交换成功(工程标准写法)
#include <iostream>
using namespace std;
void swapNum(int* p1, int* p2)
{
if(p1 == nullptr || p2 == nullptr)
{
cout << "存在空指针,终止交换" << endl;
return;
}
int temp = *p1;
*p1 = *p2;
*p2 = temp;
}
int main()
{
int x = 10, y = 20;
cout << "交换前:x=" << x << ", y=" << y << endl;
swapNum(&x, &y);
cout << "交换后:x=" << x << ", y=" << y << endl;
return 0;
}
四、工程场景2:指针接收数组(最常用)
数组传入函数自动退化为首元素指针,丢失长度信息。工程标准写法:指针形参 + 长度参数,强制增加空指针判断。
#include <iostream>
using namespace std;
// 形参 int* arr 等价于 int arr[]
void printArray(int* arr, int len)
{
// 工程强制:空指针校验 + 合法性校验
if(arr == nullptr || len <= 0)
{
cout << "数组指针为空或者长度非法" << endl;
return;
}
for(int i = 0; i < len; i++)
{
cout << arr[i] << " ";
}
cout << endl;
}
// 修改数组所有元素,统一翻倍
void doubleArray(int* arr, int len)
{
if(arr == nullptr || len <= 0)
{
return;
}
for(int i = 0; i < len; i++)
{
arr[i] *= 2;
}
}
int main()
{
int arr[] = {1,2,3,4,5};
int len = sizeof(arr)/sizeof(arr[0]);
printArray(arr, len);
doubleArray(arr, len);
printArray(arr, len);
return 0;
}
工程规范:只要函数接收指针类型参数,必须优先做空指针判断,避免空指针解引用导致程序崩溃,是工业级代码基础规范。
五、值传递 vs 地址传递对比表(精准修正)
| 传递方式 | 传递内容 | 内存拷贝 | 能否修改外部变量 | 适用场景 |
|---|---|---|---|---|
| 值传递 | 变量本身数据 | 拷贝整个数据 | 不能 | 只读场景、小数据类型、无需修改原值 |
| 地址传递(指针传参) | 变量/数组的内存地址 | 仅拷贝8字节地址 | 可以 | 修改外部变量、传递大数组、避免大数据拷贝 |
六、指针函数参数高频坑点(修复逻辑矛盾)
-
忘记空指针校验 传入
nullptr未做判断,直接解引用,直接触发程序崩溃。所有指针参数必须前置判空。 -
数组越界访问 传入非法长度,指针访问数组以外内存,产生未定义行为,大概率闪退、数据错乱。
-
返回局部变量地址(野指针)【修正标题与内容矛盾】 函数返回局部变量地址,局部变量随函数结束销毁,内存被回收,地址失效,形成野指针。
// 错误示例,绝对禁止
int* badFunc()
{
int num = 100;
return # // num是局部变量,函数结束内存回收,返回无效野指针
} -
混淆修改指针本身 VS 修改指针指向内容
*p = xxx:修改指针指向的数据,会改变外部原始变量(核心常用)p = xxx:仅修改指针变量自身存储的地址,不会影响外部实参
七、本节核心规则总结
- 值传递:拷贝完整数据副本,形参实参相互独立,无法修改外部原值。
- 地址传递:拷贝地址而非数据,通过
*p解引用直接操作原始内存,可修改外部变量。 - 数组传参自动退化为首元素指针,丢失长度属性,必须手动传入数组长度。
- 工程规范:所有指针参数,第一步必须做空指针校验,防御程序崩溃。
- 修改
*p是改数据,修改p是改地址,二者效果完全不同。 - 严禁返回局部变量地址,杜绝野指针隐患。
思考题(新增铺垫预习题)
-
值传递为什么无法在函数内部修改外部实参?
-
使用指针作为函数参数时,为什么推荐增加空指针判断?
-
写一个函数,利用指针参数,计算传入数组的最大值。
-
辨析:
*p = 50和p = nullptr,分别做了什么操作?会不会影响外部变量? -
预习题(铺垫指针的指针):运行以下代码是否会崩溃?为什么?
#include
using namespace std;void tryAlloc(int* p) {
p = new int(100);
}int main() {
int* p = nullptr;
tryAlloc(p);
cout << *p << endl;
return 0;
}
下期预告
下一节学习引用&详解:引用与指针的工程选型区别