从内存/汇编角度看C与C++:指针、引用、对象的底层差异

博主介绍:程序喵大人

前两天模拟面试,问了学员一个问题:

"你觉得 C++ 的引用和指针,在汇编层面有什么区别?"

他想了一下,回答:"引用不能为空,指针可以为空。"

面试官又问:"那从汇编看呢?"

他答不出了。

这个问题的答案其实很简单:汇编层面,引用和指针完全一样。之所以要区分它们,差别在编译期的类型检查和语法保护。

一、指针与引用:汇编层完全一致

写两段代码,编译成汇编看看:

cpp 复制代码
// 指针
int a = 10;
int* p = &a;
*p = 20;

// 引用
int b = 10;
int& r = b;
r = 20;

用 g++ -S 编译出汇编代码,你会发现两者生成的汇编指令几乎一模一样:都是把变量的地址存入寄存器,然后通过地址去读写。

asm 复制代码
; 指针版本
movl    $10, -4(%rbp)      ; a = 10
leaq    -4(%rbp), %rax     ; 取 a 的地址
movq    %rax, -16(%rbp)    ; p = &a
movq    -16(%rbp), %rax    ; 取地址
movl    $20, (%rax)        ; *p = 20

; 引用版本
movl    $10, -4(%rbp)      ; b = 10
leaq    -4(%rbp), %rax     ; 取 b 的地址
movq    %rax, -16(%rbp)    ; r = &b(引用实际存的是地址)
movq    -16(%rbp), %rax
movl    $20, (%rax)        ; r = 20

可以看到,引用本质就是指针的语法糖。编译器会确保引用在初始化时必须绑定到有效对象,且不能重新绑定,这些约束都是编译期完成的,运行时并不会多出任何指令。

二、对象内存布局:C 结构体 vs C++ 类

C 的 struct 只包含数据,C++ 的 class 可以包含数据、成员函数、虚函数表指针,内存布局会发生本质变化。

简单类(无虚函数)

cpp 复制代码
class Point {
    int x, y;
public:
    void move(int dx, int dy) { x += dx; y += dy; }
};

sizeof(Point) 等于 sizeof(int) * 2。

成员函数不占对象空间,所有对象共享同一份函数代码。

带虚函数的类

cpp 复制代码
class Shape {
    int id;
public:
    virtual void draw() {}
};

这时 sizeof(Shape) 不再是 4,而是 16 左右(64 位系统)。

多出来的是 vtable 指针。

内存布局大致如下:

text 复制代码
+--------+
| vptr   |  --> 指向 vtable
+--------+
| id     |  数据成员
+--------+

vtable 在哪里

vtable 存在只读数据段中,所有同类对象共享同一张 vtable:

text 复制代码
vtable for Shape:
+--------+
| RTTI   |  运行时类型信息
+--------+
| ~Shape |  虚构构函数
+--------+
| draw   |  虚函数实现
+--------+

面试中的高频考点是:如果类有虚函数,构造函数中会隐式初始化 vptr。这也是为什么在构造函数里调用虚函数时,调用的是当前类的版本,而不是派生类的版本------因为此时 vptr 还没被子类覆盖。

三、new / delete vs malloc / free:底层差了两步

cpp 复制代码
// C++
Obj* p = new Obj();
delete p;

// C
Obj* p = (Obj*)malloc(sizeof(Obj));
free(p);

new 的底层可以拆成两步:

  1. 调用 operator new 分配内存(类似 malloc)
  2. 调用构造函数

delete 也同样是两步:

  1. 调用析构函数
  2. 调用 operator delete 释放内存
cpp 复制代码
// new 的底层展开
void* mem = ::operator new(sizeof(Obj));
Obj* p = new (mem) Obj();  // placement new

// delete 的底层展开
p->~Obj();
::operator delete(p);

面试官经常用 malloc 构造对象来考察这一点:

cpp 复制代码
Obj* p = (Obj*)malloc(sizeof(Obj));
// p 现在指向的是未初始化的原始内存,不能直接用
new (p) Obj();  // 用 placement new 在已分配内存上构造对象

四、快速对比清单

特性 C C++ 底层差异
对象大小 sizeof(struct) sizeof(class) C++ 可能多 vptr
函数 全局函数 成员函数 成员函数隐式传 this
内存分配 malloc / free new / delete new 多一步构造
类型安全 强制转换随意 转换受限 编译期检查
构造 手动初始化 自动调用构造 RAII 机制

面试中能说清楚"引用在汇编层就是指针"和"虚函数会引入 vptr",基本就能在这一类问题上脱颖而出。

很多人学了三年 C++,却从没用过 objdump -d 或 gdb 看对象的内存布局。真正理解底层,不是背概念,而是亲眼看到它。

码字不易,欢迎大家点赞,关注,评论,谢谢!

相关推荐
纪念 22923 分钟前
C++ 算法基础 (差分)
开发语言·c++·算法
ShineWinsu5 小时前
对于Qt:QTreeWidget常见属性以及QTreeWidgetItem类的解析
开发语言·c++·qt·gui·客户端·多叉树·qtreewidget
落羽的落羽6 小时前
【C++】C++17的主要特性
linux·服务器·开发语言·c++·算法·机器学习
汉克老师11 小时前
《从C++小白到算法高手:GESP 1—8级成长地图》第一讲:认识GESP——从编程新手到算法探索者
c++·gesp·信息学竞赛·小学生学c++
晴天的雨.99212 小时前
【C++】继承篇
java·开发语言·c++
SomeOtherTime12 小时前
第 1 讲:内存布局与所有权解剖——栈帧、胖指针与“移动/拷贝”在汇编层面的真实面目
java·开发语言·汇编
青少儿编程课堂13 小时前
蓝桥杯青少组图形化 + 全国青少年信息素养大赛 + 电子学会图形化考级:校园大富翁经营闯关解析
c++·python·算法·bfs·信息学竞赛
another heaven13 小时前
【C/C++ 第一次跨越(上):用 C++11/14 停止制造新的旧代码】
c语言·c++·制造
qq_4069810313 小时前
c++ lamda(三)Lambda表达式
java·前端·c++
I_belong_to_jesus1 天前
C++关于分支预测的优化1
开发语言·c++·算法