c++ 引用底层实现

c++ 引用底层实现
原文地址:https://www.cnblogs.com/chenhuan001/p/6702934.html

主要是看了上面的这篇“从底层汇编理解 c++ 引用实现机制“的文章之后,觉得不错。就转了过来,同时,对文中的程序都在自己的机器上验证了一下。

使用的G++版本:g++ (GCC) 4.5.1 20100924

如果要查看汇编后代码与源码的关系,我用的方法是:

先用g++生成带有调试信息的目标文件:g++ -g -c ref.cc

然后再利用objdump命令查看目标文件ref.o:objdump -S ref.o

引用类型到底是什么?它和指针有什么关系?它本身占用内存空间吗? 带着这些疑问,我们来进行分析。 先看代码:

include

include

using namespace std;

void main()

{

int x = 1;

int &b = x;

}

int main()

{

int x=1;

int &b=x;

return 0;

}

通过汇编查看代码如下:

9: int x = 1;

00401048 mov dword ptr [ebp-4],1

10: int &b = x;

0040104F lea eax,[ebp-4]

00401052 mov dword ptr [ebp-8],eax

00000000 :

int main()

{

0: 55 push %ebp

1: 89 e5 mov %esp,%ebp

3: 83 ec 10 sub $0x10,%esp

int x=1;

6: c7 45 f8 01 00 00 00 movl $0x1,-0x8(%ebp)

int &b=x;

d: 8d 45 f8 lea -0x8(%ebp),%eax

10: 89 45 fc mov %eax,-0x4(%ebp)

return 0;

13: b8 00 00 00 00 mov $0x0,%eax

}

18: c9 leave

19: c3 ret

可以知道x的地址为ebp-4,b的地址为ebp-8,因为栈内的变量内存是从高往低进行分配的。所以b的地址比x的低。
lea eax,[ebp-4] 这条语句将x的地址ebp-4放入eax寄存器

mov dword ptr [ebp-8],eax 这条语句将eax的值放入b的地址ebp-8中

上面两条汇编的作用即:将x的地址存入变量b中,这不和将某个变量的地址存入指针变量是一样的吗?

所以从汇编层次来看,的确引用是通过指针来实现的。

下面我们通过程序来验证,我们知道,在程序一层我们只要直接涉及到引用变量的操作,我们操作的总是被引用变量,即编译器帮我们做了些手脚,总是在引用前面加上*。所以我们要读取真正的“引用变量的值”,必须采取一定的策略,好吧,我们就按照变量在栈中分布的特点来绕过编译器的这个特点。

[cpp] view plaincopyprint?

include

include

using namespace std;

void main()

{

int x = 1;

int y = 2;

int &b = x;

printf(“&x=%x,&y=%x,&b=%x,b=%x\n”,&x,&y,&y-1,*(&y-1));

}

输出结果为:&x=12ff7c,&y=12ff78,&b=12ff74,b=12ff7c

include

int main()

{

int x=1;

int y=2;

int &b=x;

printf(“&x=%x,&y=%x,&b=%x,b=%x\n”,&x,&y,&y-1,*(&y-1));

return 0;

}

输出结果是:

&x=bfe1b308,&y=bfe1b304,&b=bfe1b300,b=8048460

这个地方的结果和作者不一样,可以看后面的解释。

void main()

{

int x = 1;

int &b = x;

printf(“&x=%x,&b=%x\n”,&x,&b);

}

输出结果为:&x=12ff7c,&b=12ff7c.

include

int main()

{

int x=1;

int &b=x;

printf(“&x=%x,&b=%x\n”,&x,&b);

return 0;

}

输出结果:&x=bfe74aa8,&b=bfe74aa8

b的地址我们没法通过&b获得,因为编译器会将&b解释为:&(*b) =&x ,所以&b将得到&x。也验证了对所有的b的操作,和对x的操作等同。

但是我们可以间接通过&y-1来得到b的地址,从而得到b的值:*(&y-1) 从结果可以知道,b的值即x的地址,从而可以知道,从地层实现来看,引用变量的确存放的是被引用对象的地址,只不过,对于高级程序员来说是透明的,编译器 屏蔽了引用和指针的差别。

下面是程序的变量在内存栈中的分布,引用变量一样也占用内存空间,而且应该是4个字节的空间。

虽然从底层来说,引用的实质是指针,但是从高层语言级别来看,我们不能说引用就是指针,他们是两个完全不同的概念。有人说引用是受限的指针,这种说法我不赞同,因为从语言级别上,指针和引用没有关系,引用就是另一个变量的别名。对引用的任何操作等价于对被引用变量的操作。从语言级别上,我们就不要去考虑它的底层实现机制啦,因为这些对你是透明的。所以在面试的时候,如果面试的人问到这个问题,可以先从语言级别上谈谈引用,深入的话就从底层的实现机制进行分析。而不能什么条件没有就说:引用就是指针,没有差别,……之类的回答

对于下面的程序:

include

int main()

{

int x=1;

int y=2;

int &b=x;

printf(“&x=%x,&y=%x,&b=%x,b=%x\n”,&x,&y,&y-1,*(&y-1));

return 0;

}

我的结果是&x=bfe1b308,&y=bfe1b304,&b=bfe1b300,b=8048460

与原作者的不同,然后我就将上面的程序进行编译得到下面的结果:

00000000 :

include

int main()

{

0: 55 push %ebp

1: 89 e5 mov %esp,%ebp

3: 83 e4 f0 and $0xfffffff0,%esp

6: 83 ec 30 sub $0x30,%esp

int x=1;

9: c7 44 24 28 01 00 00 movl $0x1,0x28(%esp) 将1赋值给x(x在堆栈的0x28处)

10: 00

int y=2;

11: c7 44 24 24 02 00 00 movl $0x2,0x24(%esp) 将2赋值给y(y在堆栈的0x24处)

18: 00

int &b=x;

19: 8d 44 24 28 lea 0x28(%esp),%eax 将x的地址0x28 传给寄存器%eax

1d: 89 44 24 2c mov %eax,0x2c(%esp) 将%eax的值赋给堆栈0x2c处(这儿比较重要)

printf(“&x=%x,&y=%x,&b=%x,b=%x\n”,&x,&y,&y-1,*(&y-1));

21: 8d 44 24 24 lea 0x24(%esp),%eax 将堆栈0x24处的地址传给寄存器%eax

25: 83 e8 04 sub $0x4,%eax 将%eax的值减掉4

28: 8b 10 mov (%eax),%edx 将寄存器%eax中地址所指向的内容传给寄存器%edx

2a: 8d 44 24 24 lea 0x24(%esp),%eax 将堆栈0x24处的地址传给寄存器%eax

2e: 83 e8 04 sub $0x4,%eax 将%eax的值减掉4

31: 89 54 24 10 mov %edx,0x10(%esp) 将%edx的内容传给堆栈0x10处

35: 89 44 24 0c mov %eax,0xc(%esp) 将%eax的内容传给堆栈0xc处

39: 8d 44 24 24 lea 0x24(%esp),%eax 将堆栈0x24处的地址传给寄存器%eax

3d: 89 44 24 08 mov %eax,0x8(%esp) 将%eax的内容传给堆栈0x8处

41: 8d 44 24 28 lea 0x28(%esp),%eax 将堆栈0x28处的地址传给寄存器%eax

45: 89 44 24 04 mov %eax,0x4(%esp) 将%eax的内容传给堆栈0x4处

49: c7 04 24 00 00 00 00 movl $0x0,(%esp)

50: e8 fc ff ff ff call 51

<think>好的,用户之前问了关于用单向链表实现栈的问题,现在他们想知道C++中栈的底层实现。我需要先回顾一下之前的对话,确保这次回答符合他们的需求。 首先,用户可能对数据结构有一定了解,但想更深入理解C++标准库中栈的具体实现方式。他们可能在学习或开发中遇到了相关问题,需要了解底层机制来优化代码或解决问题。 C++中的stack通常是用deque或list实现的,默认是deque。我需要解释这两种容器作为底层结构时的区别,以及为什么选择它们。要提到deque的双端操作效率高,而list在某些情况下更灵活,比如元素较大时。 然后,需要详细说明底层实现的关键点,比如容器适配器的概念,栈的操作如何映射到底层容器的操作。例如,push调用push_back,pop调用pop_back,这样用户能明白栈是如何封装这些操作的。 还要比较不同底层容器的时间复杂度,deque和vector在尾部操作都是O(1),而list可能由于内存分配的原因稍慢,但不需要重新分配内存。这可能帮助用户根据使用场景选择合适的底层容器。 另外,需要给出示例代码,展示如何用不同的容器来实例化栈,比如使用vector或list,并说明它们的优缺点。用户可能在实际项目中需要调整底层容器,这部分信息对他们会有帮助。 最后,应用场景和注意事项也很重要,比如频繁插入删除时选择deque,元素较大时用list,避免vector的频繁扩容。还要提醒用户线程安全和异常处理的问题,这些是实际开发中常见的考虑点。 需要确保回答结构清晰,逐步引导用户理解,同时保持技术细节的准确性。避免使用过于专业的术语,但必要时解释清楚,确保用户能够跟上思路。检查是否有遗漏的关键点,比如内存管理或性能比较,确保回答全面。</think>以下是关于C++中栈(`std::stack`)底层实现的逐步解析: --- ### 一、底层实现基础 C++标准库中的栈 **`std::stack`** 是一个**容器适配器**(Container Adapter),它基于其他底层容器(如 `deque`、`list` 或 `vector`)封装实现。默认情况下使用 **`std::deque`** 作为底层容器。 #### 关键特性 1. **封装模式**:栈通过调用底层容器的方法实现核心操作 2. **默认选择**:`deque`(双端队列)因其头尾操作的高效性(均摊 $O(1)$ 时间复杂度)被选为默认容器 3. **可替换性**:可通过模板参数指定其他底层容器(如 `vector` 或 `list`) --- ### 二、底层容器对比 | 操作 | `deque`(默认) | `vector` | `list` | |----------------|----------------|----------------|----------------| | `push()` | $O(1)$ | 均摊 $O(1)$ | $O(1)$ | | `pop()` | $O(1)$ | $O(1)$ | $O(1)$ | | 内存连续性 | 分段连续 | 完全连续 | 非连续 | | 扩容成本 | 低 | 高(需复制数据)| 无 | --- ### 三、核心实现原理 #### 1. 容器适配器模式 栈通过**组合**底层容器对象,并限制其接口来实现LIFO特性: ```cpp template <class T, class Container = std::deque<T>> class stack { protected: Container c; // 底层容器 public: void push(const T& value) { c.push_back(value); } void pop() { c.pop_back(); } T& top() { return c.back(); } // ...其他方法 }; ``` #### 2. 操作映射关系 | 栈操作 | 底层容器对应操作 | 说明 | |----------|------------------|--------------------------| | `push()` | `push_back()` | 元素压入容器尾部 | | `pop()` | `pop_back()` | 删除容器尾部元素 | | `top()` | `back()` | 访问容器尾部元素 | | `empty()`| `empty()` | 判断容器是否为空 | | `size()` | `size()` | 获取容器元素数量 | --- ### 四、实现细节示例 #### 1. 使用 `deque` 的栈内存布局 ``` 栈顶 -> [元素3] -> [元素2] -> [元素1] (存储在deque的尾部) ``` - `deque` 通过多个固定大小的内存块管理元素 - 尾部插入/删除无需移动其他元素 #### 2. 使用 `vector` 的栈行为 ```cpp std::stack<int, std::vector<int>> vec_stack; ``` - `push()` 可能触发扩容(重新分配内存+元素复制) - `pop()` 不会释放内存(`vector` 的容量保持) #### 3. 使用 `list` 的栈行为 ```cpp std::stack<int, std::list<int>> list_stack; ``` - 每个操作均为严格 $O(1)$ 时间复杂度 - 每个元素单独分配内存,无扩容问题 --- ### 五、性能关键点 1. **`deque` 的优势**: - 尾部操作高效且无需大规模数据迁移 - 内存分段管理避免单一超大内存块申请 2. **`vector` 的陷阱**: - 频繁扩容时性能下降 - 即使 `pop()` 后内存也不会自动收缩(需手动调用 `shrink_to_fit()`) 3. **`list` 的适用场景**: - 元素体积特别大时避免复制成本 - 需要保证严格 $O(1)$ 时间复杂度的场景 --- ### 六、自定义底层容器 可通过模板参数指定其他容器,但需满足以下接口: ```cpp // 必须实现的方法: back() push_back() pop_back() empty() size() ``` **示例:基于 `vector` 的栈声明** ```cpp #include <stack> #include <vector> std::stack<int, std::vector<int>> custom_stack; ``` --- ### 七、应用场景建议 | 场景 | 推荐容器 | 原因 | |-----------------------|----------|------------------------------| | 高频压栈/弹栈 | `deque` | 平衡性能和内存连续性 | | 预先知道最大容量 | `vector` | 避免扩容,利用连续内存加速访问 | | 元素体积大(>1KB) | `list` | 减少复制开销 | | 需要严格异常安全保证 | `list` | 操作不影响其他元素指针/引用 | --- ### 八、底层实现源码示例 (简化版 `std::stack` 核心逻辑) ```cpp template<typename T, typename Container = std::deque<T>> class Stack { public: void push(const T& value) { container.push_back(value); } void pop() { if (!container.empty()) { container.pop_back(); } else { throw std::out_of_range("Stack underflow"); } } T& top() { return container.back(); } bool empty() const { return container.empty(); } private: Container container; }; ``` --- ### 九、注意事项 1. **线程安全**:`std::stack` 本身非线程安全,需自行加锁 2. **迭代器失效**:修改栈的操作会使所有迭代器失效 3. **异常处理**: - `push()` 可能因内存分配失败抛出 `std::bad_alloc` - `pop()` 空栈时行为未定义(应先检查 `empty()`) 通过理解底层实现,可以更高效地选择适合特定场景的栈实现方式。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值