目录
从C语言说起
本篇文章主要是剖析函数传参的三种方式,所涉及到的知识有:指针、变量存储、生命周期、左值引用等。指针是实现传址传参的基础,变量存储的位置是理解几个传参方式不同之处与如何实现的基础,关于引用,实际上引用分为左值引用和右值引用(C++11),而我们通常使用的引用是左值引用。对于上述几个概念不是太清楚的博友也不用着急,接着看下去,能理解多少就理解多少,等到后面学到更多的知识再回来看会有不一样的看法。
本篇文章大部分为笔者自己的观点,欢迎各位博友一起探讨!
一、变量的存储
变量的存储位置是一个非常重要的概念,个人觉得了解了这部分内容才算是对于语言学习的入门。
变量的存储位置直接影响其生命周期、作用域和访问效率。程序运行时内存通常分为5个关键区域:栈区、堆区、全局/静态存储区、常量区以及代码区。

1. 栈区(Stack)
- 存放内容:函数内的局部变量、函数参数
- 生命周期:函数调用时创建,函数返回时自动销毁
- 特点:
- 内存分配连续,通过栈指针快速操作
- 空间有限(默认1-8MB,可通过编译器调整)
- 访问速度极快(仅次于寄存器)
void func() {
int a; // 栈区
char buf[1024]; // 栈区(注意栈溢出风险!)
}
2. 堆区(Heap)
- 存放内容:
malloc/new动态分配的内存 - 生命周期:手动控制(
free/delete前一直存在) - 特点:
- 空间巨大(受限于系统可用内存)
- 内存碎片化风险
- 访问速度较慢(需通过指针间接访问)
int *p = (int*)malloc(100); // C风格堆分配
int *arr = new int[100]; // C++风格堆分配
3. 全局/静态存储区
- 存放内容:
- 全局变量(函数外部定义的变量)
- 静态变量(
static修饰的局部/全局变量)
- 生命周期:程序启动时分配,程序结束时释放
- 特点:
- 未初始化的变量默认置零(与栈/堆不同!)
- 数据可读可写
int global_var; // 全局变量区
static int static_var; // 静态变量区
void func() {
static int cnt = 0; // 静态局部变量(持久化计数)
}
4. 常量区(只读区)
- 存放内容:
- 字符串常量(如
"Hello") const修饰的全局常量
- 字符串常量(如
- 特点:
- 内容只读,修改会导致段错误
- 相同常量可能共享内存(编译器优化)
const int MAX = 100; // 可能存入常量区(C++常量优化)
char* s = "immutable"; // 字符串常量在只读区
5. 代码区(Text)
- 存放内容:编译后的二进制机器指令
- 特点:只读,程序运行的“蓝图”
二、生命周期与作用域
| 存储区域 | 典型变量类型 | 生命周期 | 作用域 |
|---|---|---|---|
| 栈区 (Stack) | 局部变量、函数参数 | 函数调用时创建 → 函数返回时销毁 | 仅在定义它的函数/代码块内有效 |
| 堆区 (Heap) | new/malloc分配的内存 | 手动分配 → 手动释放 | 通过指针全局可达,但需主动管理 |
| 全局/静态区 | 全局变量、static变量 | 程序启动时创建 → 程序终止时销毁 | 全局变量:整个程序;静态变量:定义域内 |
| 常量区 (RO) | 字符串常量、const全局常量 | 程序启动时创建 → 程序终止时销毁 | 全局可见(字符串常量)或文件内可见 |
| 代码区 (Text) | 函数代码 | 程序启动时加载 → 程序终止时卸载 | 全局可调用 |
三、函数传参
函数传参的方式主要有三种,分别是传值、传址和传引用。
函数列表中的参数被称为形式参数(形参),函数调用时传入的参数被称为实际参数(实参),实参与形参的变量名可以一致也可以不一致。形参的生命周期在对应函数作用域内。
3.1 传值传参
对于传值传参来说,形参是实参的一个拷贝,也就是说实际上它们是两个变量。在函数执行结束后,形式参数以及函数栈区的变量都会被销毁。
举例如下:
void modify(int val) {
val = 100; // 修改的是副本
}
int main() {
int a = 10;
modify(a); // 原变量a的值不变
cout << a; // 输出10
}
对于上面的程序,对应的内存示意图如下:

从图中可以看出,变量val与变量a是两块空间,所以说改变变量val与变量a无关,变量a的值依然为10.
3.2 传址传参
对应传址传参来说,形参为内存地址的一个拷贝,但是通过解引用可以拿到该内存地址的内容,从而可以直接改变实参所指地址的值。
举例如下:
void swap(int* x, int* y) {
int tmp = *x;
*x = *y;
*y = tmp;
}
int main() {
int a = 1, b = 2;
swap(&a, &b); // 传递地址
cout << a << b; // 输出2 1
}
对于上面的程序,对应的内存示意图如下:

如图可以知道,x与y中存储的分别为a与b变量的地址,经过解引用后可以直接访问main函数中的a、b变量,因此在swap函数中对它们进行操作后原来的变量也会发生变化。
3.3 传引用传参
随着指针的使用,某些场景下嵌套的指针越来越多,有二级指针、三级指针甚至多级指针,指针进行加减时需要根据自己的类型,还要注意解引用的顺序,导致编程变得越来越复杂。于是C++引入了引用,引用事实上就是一个别名,使用别名可以像操作原本的变量一样去操作它,但是它具有和使用指针一样的效果,无论是在主函数还是传参时改变它,原来的值都会被改变,因为它的底层也是用指针来实现的,作为使用者我们不必过多关注它的实现。
但是有一点需要注意,C++中的引用初始时必须绑定且一旦绑定就不可以再将换绑。
int a = 10;
int b = 20;
int& ref = a; // 正确:ref绑定到a
ref = b; // 错误理解:试图让ref指向b?
// 实际行为:将b的值赋给a → a变为20
举例如下:
void swap(int& x, int& y) {
int tmp = x; // 直接操作原变量
x = y;
y = tmp;
}
int main() {
int a = 1, b = 2;
swap(a, b); // 语法更简洁
cout << a << b; // 输出2 1
}
对于上面的程序,对应底层内存示意图如下:

引用在底层是用指针实现的,在函数栈帧中它只传递一个指针,但是引用的使用隐藏了内部细节,对于程序员来说使用起来好似是在直接操作原来的变量,可以使用下面的模型理解:
逻辑内存示意图:

在使用时,笔者觉得可以这样理解:x,y相当与是a与b的别名,它们都管理同一块内存。
最后,在使用引用传参作为返回值时要注意悬空引用的问题:
int& create_dangling_ref() {
int x = 10;
return x; // x销毁后返回的引用无效!
}
写在最后
本篇文章只是简单的从应用的角度去理解三种传参方式,后面会陆续更新深入的一些理解与验证。同时,C++的左值引用与右值引用笔者觉得十分重要,后续会对这部分内容进行深入剖析。

1163

被折叠的 条评论
为什么被折叠?



