C/C++函数传参:值、指针、引用的终极抉择

目录

从C语言说起

一、变量的存储

1. 栈区(Stack)

2. 堆区(Heap)

3. 全局/静态存储区

4. 常量区(只读区)

5. 代码区(Text)

二、生命周期与作用域

三、函数传参

3.1 传值传参

3.2 传址传参

3.3 传引用传参

 写在最后


从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++的左值引用与右值引用笔者觉得十分重要,后续会对这部分内容进行深入剖析。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值