当前位置: 首页 > news >正文

C++:栈帧、命名空间、引用

一、前置知识

1.1、栈区(Stack)

1.1.1、内存分配与回收机制

  • 分配方式​​:由编译器自动管理,通过调整栈指针(ESP/RSP)实现。
  1. 函数调用时,栈指针下移(栈从高地址向低地址增长),分配栈帧空间。
  2. 函数返回时,栈指针上移,回收栈帧(​​不擦除数据,仅移动指针​​)。
  • ​存储内容​​:
  1. 局部变量(非 static
  2. 函数参数(x86 下部分通过栈传递)
  3. 返回地址(Caller 的 EIP/RIP
  4. 上一栈帧的基址(EBP/RBP

1.1.2、栈帧销毁时的行为

  • ​栈指针回退​​:RET 指令执行后,栈指针恢复到调用前的状态。
  • ​内存未清零​​:栈内存只是“逻辑释放”,数据仍残留(可能被后续函数覆盖)。
  • ​局部变量失效​​:
  1. 普通变量(如 int x):直接失效,访问会导致未定义行为(UB)。
  2. 指针变量(如 int* p):若指向栈内存(如 p = &x),则成为 ​​悬空指针。

1.1.3、底层汇编视角

; 函数调用时
push ebp          ; 保存调用者的栈基址
mov ebp, esp      ; 设置当前栈帧基址
sub esp, 16       ; 分配16字节栈空间(局部变量); 函数返回时
mov esp, ebp      ; 恢复栈指针(销毁栈帧)
pop ebp           ; 恢复调用者的栈基址
ret               ; 返回

1.1.4、典型问题

  • ​栈溢出(Stack Overflow)​​:递归过深或局部变量过大(如 int a[1000000])导致栈耗尽。
  • ​返回栈地址的陷阱​:
int* foo() {int x = 42;return &x;  // 返回局部变量地址(危险!)
}
int* p = foo(); // p现在是悬垂指针

2.1、堆区(Heap)

2.1.1、内存分配与回收机制

  • ​分配方式​​:手动管理(malloc/freenew/delete)。
  1. malloc 调用 brk 或 mmap 向操作系统申请内存。
  2. 堆内存由 ​​内存管理器(如 glibc 的 ptmalloc)​​ 维护,可能存在碎片。
  • ​存储内容​​:
  1. 动态分配的数据(如 int* p = malloc(sizeof(int)))。
  2. 需要显式释放,否则泄漏。

2.1.2、栈帧销毁时的行为

  • ​堆内存不受影响​​:
  1. 栈帧销毁仅回收栈上的指针变量(如 int* p),但堆内存仍存在。
  2. 若未调用 free,则内存泄漏。
  3. 若已 free 但继续访问,则导致 ​​野指针​​。
  • ​指针变量的生命周期​​:
void func() {int* p = malloc(sizeof(int)); // p在栈上,指向堆内存*p = 100;
} // p被销毁,但堆内存未释放(泄漏!)

2.1.3、底层实现(Linux glibc)

  • malloc 内部可能调用 brk(扩展堆)或 mmap(大内存映射)。
  • free 不会立即归还内存给 OS,而是由内存池管理(提高复用效率)。

2.1.4、典型问题

  • 内存泄漏
void leak() {while (1) malloc(1024); // 持续泄漏,最终OOM
}
  • 双重释放
int* p = malloc(sizeof(int));
free(p);
free(p); // 崩溃或安全漏洞(如Use-After-Free)
  • 野指针
int* p = malloc(sizeof(int));
free(p);
*p = 42; // 未定义行为(可能崩溃或数据损坏)

3.1、静态区(Static/全局存储区)

3.1.1、内存分配与回收机制

  • ​分配方式​​:
  1. 全局变量​​:程序启动时分配,程序结束时释放。
  2. ​静态变量​​(static 修饰):首次执行到定义处时初始化(C++11 后线程安全)。
  • ​存储位置​​:
  1. .data 段(已初始化的全局/静态变量)
  2. .bss 段(未初始化的全局/静态变量,默认零值)
  3. 常量区(如字符串字面量 "hello"

3.1.2、栈帧销毁时的行为

  • ​完全不受影响​​:
  1. 静态变量的生命周期与程序相同,栈帧销毁后仍可访问。
  2. 局部静态变量(static int x)仅作用域受限,但内存持久。
void counter() {static int count = 0; // 只初始化一次count++;printf("%d\n", count);
}
// 多次调用counter()会输出递增的count

3.1.3、底层实现

  • 编译时确定地址,运行时直接通过固定地址访问。
  • 示例(反汇编):
mov eax, DWORD PTR [0x404000] ; 访问静态变量

3.1.4、典型问题

  • 线程安全问题
static int shared = 0;
void thread_func() {shared++; // 多线程竞争(需加锁)
}
  • 初始化顺序问题
extern int a; // 定义在其他文件
static int b = a + 1; // a的初始化顺序不确定

4.1、销毁与返回值传递的顺序

就以这段代码为例讲解一下两者之间的关系:

int* foo() {int x = 42;return &x;  // 返回局部变量地址(危险!)
}
int* p = foo(); // p现在是悬空指针

结论:先传递,后销毁。【先将返回值存储在临时变量里面(有的话),再销毁函数栈帧】

4.1.1、执行顺序

  • ​计算返回值​​:

  1. return &x; 会先计算 x 的地址(即栈上的某个位置)。

  2. 这个地址会被 ​​临时存储​​(通常在寄存器 EAX/RAX 中,或者某个临时内存位置)。
  • ​销毁栈帧​​:

  1. 函数 foo 的栈帧被销毁(mov esp, ebp + pop ebp)。

  2. 局部变量 x 的内存被“逻辑释放”(栈指针回退,但数据可能残留)。

  • ​返回值传递​​:

  1. 计算好的地址(&x)从临时存储(如 EAX)传递给调用者 p

  2. 此时 p 指向的 x 的地址 ​​已经失效​​(因为栈帧已销毁)。

4.1.2、为什么返回值还能“正确”访问?(未定义行为的陷阱)

即使 p 是悬空指针,以下代码可能偶尔“工作”:

int* p = foo();
printf("%d\n", *p);  // 可能输出42(未定义行为!)

原因:

  • 栈内存未被其他数据覆盖(残留值仍在、在下文介绍引用的时候,也会涉及到这一点)。
  • ​但这不合法​​,任何修改(如调用其他函数)可能导致崩溃或数据错误。

5.5、总结

特性栈区堆区静态区
分配方式自动(编译器)手动(malloc、free)自动(程序启动,首次使用)
释放时机函数返回时显示调用free程序结束时
访问速度极快(寄存器,栈指针访问)较慢(间接寻址)快(固定寻址)
生命周期函数作用域内手动控制程序整个生命周期
线程安全是(每个线程独立)需同步需同步(全局变量)
典型问题栈溢出,悬空指针内存泄漏,野指针初始化顺序,线程竞争

二、C++关键字(C++98)

C++总计63个关键字,C语言32个关键字
ps:下面我们只是看一下C++有多少关键字,不对关键字进行具体的讲解。
asmdoifreturntrycontinue
autodoubleinlineshorttypedeffor
booldynamic_castintsignedtypeidpublic
breakelselongsizeoftypenamethrow
caseenummutablestaticunionwchar_t
catchexplicitnamespacestatic_castunsigneddefault
charexportnewstructusingfriend
classexternoperatorswitchvirtualregister
constfalseprivatetemplatevoidtrue
const_castfloatprotectedthisvolatilewhile
deletegotoreinterpret_cat

三、命名空间

在C/C++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化,避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。
#include <stdio.h>
#include <stdlib.h>
int rand = 10;
// C语言没办法解决类似这样的命名冲突问题,所以C++提出了namespace来解决
int main()
{printf("%d\n", rand);return 0;
}
// 编译后后报错:error C2365: “rand”: 重定义;以前的定义是“函数”

3.1、命名空间定义

定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,然后接一对{}即可,{}中即为命名空间的成员。
// ljt是命名空间的名字,一般开发中是用项目名字做命名空间名。
// 1. 正常的命名空间定义
namespace ljt
{// 命名空间中可以定义变量/函数/类型int rand = 10;int Add(int left, int right){return left + right;}struct Node{struct Node* next;int val;};
}
//2. 命名空间可以嵌套
// test.cpp
namespace N1
{int a;int b;int Add(int left, int right){return left + right;}namespace N2{int c;int d;int Sub(int left, int right){return left - right;}}
}
//3. 同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中。
// ps:一个工程中的test.h和上面test.cpp中两个N1会被合并成一个
// test.h
namespace N1
{int Mul(int left, int right){return left * right;}
}
注:一个命名空间就定义了一个新的作用域,命名空间中的所有内容都局限于该命名空间中。

3.2、命名空间的使用

命名空间中成员该如何使用呢?比如:
namespace ljt
{// 命名空间中可以定义变量/函数/类型int a = 0;int b = 1;int Add(int left, int right){return left + right;}struct Node{struct Node* next;int val;};
}
int main()
{// 编译报错:error C2065: “a”: 未声明的标识符printf("%d\n", a);return 0;
}
命名空间的使用有三种方式:
  • 加命名空间名称及作用域限定符
int main()
{printf("%d\n", N::a);return 0;    
}
  • 使用using将命名空间中某个成员引入(工程中常用)
using N::b;
int main()
{printf("%d\n", N::a);printf("%d\n", b);return 0;    
}
  • 使用using namespace 命名空间名称 引入
using namespce N;
int main()
{printf("%d\n", N::a);printf("%d\n", b);Add(10, 20);return 0;    
}

四、引用

4.1、引用的概念

引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空
间,它和它引用的变量共用同一块内存空间。

类型& 引用变量名(对象名) = 引用实体;

void TestRef()
{int a = 10;int& ra = a;//<====定义引用类型printf("%p\n", &a);printf("%p\n", &ra);
}
注意:引用类型必须和引用实体同种类型

4.2、引用特性

  • 引用在定义时必须初始化
  • 一个变量可以有多个引用
  • 引用一旦引用一个实体,再不能引用其他实体
#include <iostream>
//using std::cout;using namespace std;
int main()
{int a = 5;int& b = a;cout << &a << " " << & b << endl;//引用即取别名,内存地址是一样的//引用一旦引用一个实体,再不能引用其他实体int x = 10;a = x;//并没有改变引用,只是赋值。cout << a << " " << b << endl;return 0;
}
打印:

4.3、常引用

void TestConstRef()
{const int a = 10;//int& ra = a;   // 该语句编译时会出错,a为常量const int& ra = a;// int& b = 10; // 该语句编译时会出错,b为常量const int& b = 10;double d = 12.34;//int& rd = d; // 该语句编译时会出错,类型不同const int& rd = d; 
}

注:对于(常)引用,你只要明白引用拥有两个权限,一个是写(改写),另一个是读。如果我写的是注释中的那种形式,那么对于一个常数有了写与读的权限,但是常数它只能读,不能改写,则权限被你放大了,需要加上const修饰,限制写的权限。

int main()
{// 不可以// 引用过程中,权限不能放大const int a = 0;//int& b = a;// 可以,c拷贝给d,没有放大权限,因为d的改变不影响cconst int c = 0;int d = c;// 不可以// 引用过程中,权限可以平移或者缩小int x = 0;int& y = x;const int& z = x;++x;++y;cout << z << endl;//++z;//常值引用const int& m = 10;double dd = 1.11;int ii = dd;const int& rii = dd;return 0;
}int func1()
{static int x = 0;return x;
}int& func2()
{static int x = 0;return x;
}int main()
{//int& ret1 = func1();  // 权限放大//const int& ret1 = func1(); // 权限平移// int ret1 = func1();  // 拷贝int& ret2 = func2();		// 权限平移const int& rret2 = func2();  // 权限缩小return 0;
}

像这些代码,本质上都是一样的,无论是引用作为返回值,还是什么情况,都存在权限的放大,平移,缩小的问题,看你的函数想实现的是什么功能,写 or 读 or 写 + 读,再来判断是否要加const修饰。

4.4、使用场景

  • 做参数
引用做参数(减少拷贝提高效率)(大对象/深拷贝类对象)
#include <iostream>
using namespace std;void swap1(int x, int y)
{int temp = x;x = y;y = temp;
}void swap2(int* x, int* y)
{int temp = *x;*x = *y;*y = temp;
}void swap3(int& x, int& y)
{int temp = x;x = y;y = temp;
}
int main()
{int a = 1, b = 2;//传值cout << "a = " << a << " " << "b = " << b << endl;swap1(a, b);cout << "a = " << a << " " << "b = " << b << endl;//指针传参(C语言)cout << "a = " << a << " " << "b = " << b << endl;swap2(&a, &b);cout << "a = " << a << " " << "b = " << b << endl;//引用做参数cout << "a = " << a << " " << "b = " << b << endl;swap3(a, b);cout << "a = " << a << " " << "b = " << b << endl;return 0;
}

三种方式可以比较一下,传值,C语言传地址,C++引用。

  • 做返回值
//引用做返回值  (减少拷贝提高效率)(大对象/深拷贝类对象--什么是深拷贝以后会讲)
//引用做返回值   修改返回值+获取返回值(权限)
//做返回值
#include <iostream>
using namespace std;
//传值返回
int count()
{static int n = 0;n++;return n;
}int main()
{//并不是直接把值返回给ret,栈帧销毁的时候,先把数据放在临时变量里面,再赋值给retint ret = count();return 0;
}

解释在注释中,下面讲一个与栈帧联系比较紧密的代码。

#include <iostream>
#include <ctime>
#include <cassert>
using namespace std;int& Count(int x)
{int n = x;n++;return n;
}int main()
{int& ret = Count(10);cout << ret << endl;rand();Count(20);cout << ret << endl;return 0;
}

这段代码乍一看好像没有问题,确实,如果栈帧没有销毁干净,有数据残留,是可以正常打印11,21的。但是,这段代码大错特错,问题一堆,BUG也非常难以察觉。

首先你要明白返回值是int&类型,返回的时候,没有临时变量的产生(int作为返回值时候有),这段Count函数代码返回的是n的引用,但是n是int类型,内存开辟在栈区,函数结束,生命周期结束,后续会造成非法访问(UB:未定义行为),而你的接收值也是int&类型,即是引用的引用,本质上还是指那个销毁的变量,你现在要去打印ret,一个已经被销毁的变量,如果栈帧销毁干净,没有数据残留,就是随机值,但也有可能成功打印,栈帧未销毁干净。

其次,如果你中间没有调用其他函数(如printf,rand等等),你再去调用Count函数,它大概率还会开辟在原来的那块栈区,还是有可能打印出21,但也可能是随机值,道理和上面那段话一样。但如果你中间加了其他函数,就会存在栈帧的覆盖,将原来Count的栈帧占据,使再次调用Count的时候,是一块新的栈空间,会导致随机值的产生,但也有可能打印出21,就是并没有占据核心的栈区部分。

总之,这种写法非常危险,漏洞百出,在大型项目里还非常难察觉,尽量减少这种写法。

也可以看一下下面这张图来理解:

注意:如果函数返回时,出了函数作用域,如果返回对象还在(还没还给系统),则可以使用 引用返回,如果已经还给系统了,则必须使用传值返回。

4.5、传值、传引用的效率

以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。
typedef struct A
{int a[2000];
}A;//效率问题
//拷贝//​​test1(a)(传值调用)​​:
//每次调用都会在栈上复制整个 A 结构体(800KB)。
//10000 次循环 = 8GB 数据拷贝(非常低效)。//​​test2(a)(传引用)​​:
//只传递指针(8字节),没有数据拷贝。
//10000 次循环 = 80KB 数据传递(高效)
void test1(A a)
{}//不需要拷贝
void test2(A& a)
{}void TestRefAndValue()
{A a;// 以值作为函数参数size_t begin1 = clock();for (size_t i = 0; i < 10000; ++i)test1(a);size_t end1 = clock();// 以引用作为函数参数size_t begin2 = clock();for (size_t i = 0; i < 10000; ++i)test2(a);size_t end2 = clock();// 分别计算两个函数运行结束后的时间cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}int main()
{TestRefAndValue();return 0;
}

 通过上述代码的比较,发现传值和指针在作为传参以及返回值类型上效率相差很大

4.6、引用和指针的区别

语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。
int main()
{int a = 10;int& ra = a;cout << "&a = " << &a << endl;cout << "&ra = " << &ra << endl;return 0;
}
底层实现上实际是有空间的,因为引用是按照指针方式来实现的。
int main()
{int a = 10;int& ra = a;ra = 20;int* pa = &a; *pa = 20;return 0;
}
我们来看下引用和指针的汇编代码对比:

 引用和指针的不同点:

  • 引用概念上定义一个变量的别名,指针存储一个变量地址。
  • 引用在定义时必须初始化,指针没有要求
  • 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
  • 没有NULL引用,但有NULL指针
  • 在sizeof中含义不同引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
  • 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
  • 有多级指针,但是没有多级引用
  • 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
  • 引用比指针使用起来相对更安全

相关文章:

  • 人工智能浪潮下,制造企业如何借力DeepSeek实现数字化转型?
  • 学习黑客小故事理解 Metasploit 的 Meterpreter
  • 酷派Cool20/20S/30/40手机安装Play商店-谷歌三件套-GMS方法
  • NUMA 架构科普:双路 CPU 系统是如何构建的?
  • 如何给老旧 iOS App 添加安全保护?用 Ipa Guard 对 IPA 文件混淆加固实录
  • ComfyUI+阿里Wan2.1+内网穿透技术:本地AI视频生成系统搭建实战
  • WebVm:无需安装,一款可以在浏览器运行的 Linux 来了
  • 本地部署大模型llm+RAG向量检索问答系统 deepseek chatgpt
  • SpringBoot(五)--- 异常处理、JWT令牌、拦截技术
  • json转成yolo用的txt(json中没有宽高,需要自设宽高的)
  • VMware ESXi网络配置
  • Learning Discriminative Data Fitting Functions for Blind Image Deblurring论文阅读
  • dto vo类为什么要序列化?
  • 跨架构镜像打包问题及解决方案
  • Odoo 打印功能架构与工作流程深度剖析
  • ADB推送文件到指定路径解析
  • 一键提取Office内图片的工具
  • 华为OD机试真题——字母组合过滤组合字符串(2025A卷:100分)Java/python/JavaScript/C/C++/GO最佳实现
  • 大型工业控制系统中私有云计算模式的弊端剖析与反思
  • react-color-palette源码解析
  • 做美食类网站分析/alexa全球网站排名分析
  • 家教网站如何做/杭州网站优化企业
  • 网站 mip/怎么自己做一个小程序
  • b2c电商网站账户/能打开各种网站的浏览器
  • 广州做网站哪间公司好/网站制作的流程
  • 做招聘的网站/中国十大电商培训机构