从内存/汇编角度看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 -dgdb 看对象的内存布局。真正理解底层,不是背概念,而是亲眼看到它。

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

相关推荐
旖-旎12 小时前
《LeetCode647 回文子串 || LeetCode 5 最长回文子串》
c++·算法·leetcode·动态规划·哈希算法
Darkwanderor13 小时前
对Linux的进程控制的研究
linux·运维·c++
云泽80813 小时前
从零吃透 C++ 异常:抛出捕获、栈展开、异常重抛与编码规范详解
开发语言·c++·代码规范
REDcker13 小时前
libdatachannel 快速入门
c++·webrtc·datachannel
second6015 小时前
第一部分:快速上手 —— 建立 C++ 基本语法与编程范式
开发语言·c++
郝学胜-神的一滴15 小时前
算法实战:最小k个数——大顶堆的优雅解法
开发语言·数据结构·c++·python·程序人生·算法·排序算法
bksczm15 小时前
linux之线程概念和控制
linux·开发语言·c++
hehelm16 小时前
Linux网络编程—TCP字典翻译系统
linux·开发语言·网络·c++·tcp/ip
hansang_IR16 小时前
【记录】「SCOI2016」三道模拟赛/26.7.12
c++·算法
无相求码16 小时前
【C语言】字符串的本质是什么,为什么 strlen 会读到野内存
c语言·开发语言