【c++面向对象编程】第26篇:对象的内存模型:成员变量与成员函数的存储分离

目录

一、一个简单的问题

二、成员变量的存储

非静态成员变量

静态成员变量

三、成员函数的存储

非虚成员函数

虚函数的影响

四、内存对齐与对象大小

对齐规则(简化版)

示例:同样的成员,不同顺序,大小不同

五、对象大小计算规则总结

计算对象大小的步骤

六、完整例子:观察内存布局

七、常见误区

[1. 认为成员函数存储在对象中](#1. 认为成员函数存储在对象中)

[2. 忽略对齐导致的大小计算错误](#2. 忽略对齐导致的大小计算错误)

[3. 认为空类不占内存](#3. 认为空类不占内存)

[4. 混淆静态成员变量和全局变量](#4. 混淆静态成员变量和全局变量)

八、这一篇的收获


一、一个简单的问题

先看这段代码,猜猜输出:

cpp

复制代码
#include <iostream>
using namespace std;

class Empty {
};

class Data {
    int x;
    double y;
    char c;
};

class WithFunc {
    int x;
public:
    void func1() {}
    void func2() {}
};

int main() {
    cout << "sizeof(Empty): " << sizeof(Empty) << endl;
    cout << "sizeof(Data): " << sizeof(Data) << endl;
    cout << "sizeof(WithFunc): " << sizeof(WithFunc) << endl;
    return 0;
}

典型输出(64位系统):

text

复制代码
sizeof(Empty): 1
sizeof(Data): 16
sizeof(WithFunc): 4

关键观察

  • 空类占用1字节(C++保证每个对象有唯一地址)

  • WithFunc的大小是4字节,就是int x的大小------成员函数不占用对象内存!


二、成员变量的存储

非静态成员变量

每个对象都有自己独立的一份非静态成员变量:

cpp

复制代码
class Point {
public:
    int x;
    int y;
    static int count;   // 静态成员,不属于对象
};

int Point::count = 0;

int main() {
    Point p1, p2;
    p1.x = 10;
    p2.x = 20;     // p2.x 独立于 p1.x
    
    cout << sizeof(Point) << endl;  // 8(两个int,各4字节)
    cout << &p1.x << endl;          // p1.x的地址
    cout << &p2.x << endl;          // p2.x的地址(不同)
}

内存布局

text

复制代码
p1 对象: [ x (4字节) ][ y (4字节) ]
p2 对象: [ x (4字节) ][ y (4字节) ]
静态成员 count: 存储在全局数据区(所有对象共享)

静态成员变量

静态成员变量不存储在对象中,而是存储在全局/静态数据区(类似全局变量)。所有对象共享同一份。


三、成员函数的存储

非虚成员函数

成员函数的代码存储在代码区 (text segment),所有对象共享。编译器在编译时将成员函数转换成普通函数,隐式传递this指针。

cpp

复制代码
class Demo {
    int value;
public:
    void setValue(int v) { value = v; }
    int getValue() const { return value; }
};

// 编译器大致转换成:
void setValue(Demo* this, int v) { this->value = v; }
int getValue(const Demo* this) { return this->value; }

结论:成员函数不占用对象内存。对象大小只由非静态成员变量决定(加上对齐填充和vptr)。

虚函数的影响

如果类有虚函数(或继承自包含虚函数的类),对象中会多一个vptr指针(8字节在64位系统)。

cpp

复制代码
class Base {
    int x;
public:
    virtual void func() {}   // 有虚函数
};

class Derived : public Base {
    int y;
};

cout << sizeof(Base) << endl;    // 16 (vptr 8 + x 4 + 对齐4)
cout << sizeof(Derived) << endl; // 24 (vptr 8 + x 4 + y 4 + 对齐8)

四、内存对齐与对象大小

内存对齐是编译器为了CPU高效访问而做的优化。对象大小不一定等于成员变量大小的简单相加

对齐规则(简化版)

  1. 每个类型有自己的对齐要求(通常是其大小的倍数)

  2. 结构体的总大小是其最大成员对齐要求的整数倍

  3. 成员按声明顺序排列,但可能插入填充字节

示例:同样的成员,不同顺序,大小不同

cpp

复制代码
#include <iostream>
using namespace std;

struct A {
    char c;   // 1字节
    int i;    // 4字节
    short s;  // 2字节
};

struct B {
    int i;    // 4字节
    short s;  // 2字节
    char c;   // 1字节
};

int main() {
    cout << "sizeof(A): " << sizeof(A) << endl;  // 12
    cout << "sizeof(B): " << sizeof(B) << endl;  // 8
    return 0;
}

A的布局

text

复制代码
偏移0: char c   (1字节)
偏移1-3: 填充 (3字节)  ← 为了让int对齐到4字节
偏移4-7: int i   (4字节)
偏移8-9: short s (2字节)
偏移10-11: 填充 (2字节)  ← 让整个结构体是4的倍数
总大小: 12

B的布局

text

复制代码
偏移0-3: int i   (4字节)
偏移4-5: short s (2字节)
偏移6: char c    (1字节)
偏移7: 填充      (1字节)  ← 让整个结构体是4的倍数
总大小: 8

教训:把大的成员放在前面,小的放在后面,通常能减小对象大小。


五、对象大小计算规则总结

组成部分 是否占用对象内存 说明
非静态成员变量 ✅ 是 每个对象独立存储
静态成员变量 ❌ 否 存储在静态区,对象共享
成员函数(非虚) ❌ 否 代码存储在代码区
虚函数表指针vptr ✅ 是 如果类有虚函数,每个对象多一个vptr
内存对齐填充 ✅ 是 为了对齐而插入的空白字节

计算对象大小的步骤

  1. 累加所有非静态成员变量的大小

  2. 如果类有虚函数,加上vptr大小(64位系统8字节)

  3. 考虑内存对齐,总大小调整为最大成员对齐的整数倍

cpp

复制代码
class Sample {
    char a;           // 1字节
    virtual void f() {} // 触发vptr
    int b;            // 4字节
    static int c;     // 不计入对象大小
};

int Sample::c = 0;

// 计算:vptr(8) + a(1) + 填充(3) + b(4) = 16
cout << sizeof(Sample); // 16(64位)

六、完整例子:观察内存布局

cpp

复制代码
#include <iostream>
using namespace std;

// 辅助函数:打印对象内存的十六进制表示
void printMemory(const void* obj, size_t size) {
    const unsigned char* bytes = static_cast<const unsigned char*>(obj);
    for (size_t i = 0; i < size; i++) {
        printf("%02x ", bytes[i]);
    }
    cout << endl;
}

class Simple {
public:
    char c;
    int i;
    short s;
};

class WithVirtual {
public:
    char c;
    int i;
    virtual void func() {}
    virtual void func2() {}
};

class Empty {
};

int main() {
    cout << "=== 类型大小 ===" << endl;
    cout << "sizeof(Simple): " << sizeof(Simple) << endl;
    cout << "sizeof(WithVirtual): " << sizeof(WithVirtual) << endl;
    cout << "sizeof(Empty): " << sizeof(Empty) << endl;
    
    cout << "\n=== Simple对象内存 ===" << endl;
    Simple s;
    s.c = 'A';   // ASCII 0x41
    s.i = 0x12345678;
    s.s = 0xABCD;
    printMemory(&s, sizeof(s));
    
    cout << "\n=== WithVirtual对象内存 ===" << endl;
    WithVirtual v;
    v.c = 'B';
    v.i = 0x87654321;
    printMemory(&v, sizeof(v));
    
    cout << "\n=== 多个对象的地址 ===" << endl;
    Simple s1, s2;
    cout << "s1地址: " << &s1 << endl;
    cout << "s2地址: " << &s2 << endl;
    cout << "s1.s地址: " << &(s1.i) << endl;
    cout << "s2.s地址: " << &(s2.i) << endl;
    
    cout << "\n=== 成员函数地址(所有对象共享) ===" << endl;
    cout << "Simple::printMemory地址: " << (void*)&Simple::printMemory << endl;
    // 注意:成员函数地址不是对象的一部分
    
    return 0;
}

七、常见误区

1. 认为成员函数存储在对象中

cpp

复制代码
// ❌ 错误理解
// 每个对象不会存储一份函数代码
// 函数代码只有一份,通过this指针区分操作哪个对象

2. 忽略对齐导致的大小计算错误

cpp

复制代码
struct Misaligned {
    char a;
    double b;   // 需要8字节对齐
};
// 实际大小是16(a+7填充+b),不是9

3. 认为空类不占内存

cpp

复制代码
Empty e1, e2;
cout << (&e1 == &e2);  // false,不同的对象必须有不同地址
// 所以空类占1字节(或更多,取决于编译器)

4. 混淆静态成员变量和全局变量

静态成员变量存储在静态区,但作用域在类内。


八、这一篇的收获

你现在应该理解:

  • 成员函数不占对象内存 :函数代码所有对象共享,通过this区分

  • 对象大小由非静态成员变量决定:加上vptr(如果有虚函数)和对齐填充

  • 内存对齐:CPU访问效率的优化,可能导致对象比成员总和大

  • 布局优化:把大成员放前面可以减小对象大小

  • 静态成员:不属于对象,存储在全局/静态区

💡 小作业:定义几个结构体,包含charshortintdouble类型成员,用不同顺序排列,sizeof观察大小变化。设计一个"最优"布局和最差布局,验证对齐规则。


下一篇预告:第27篇《空类的大小为什么是1?------C++对象标识的秘密》------深入探讨空类占1字节的原因,以及带虚函数的空类为什么是8(64位)?理解对象标识和唯一地址的设计哲学。

相关推荐
郝学胜-神的一滴2 小时前
Qt 高级开发 005: Qt Creator与Visual Studio 项目双向转换
开发语言·c++·ide·qt·程序人生·visual studio
贩卖黄昏的熊2 小时前
陕西省ICPC省赛总结
算法
解决问题no解决代码问题2 小时前
JAVA GC
java·开发语言·jvm
jieyucx2 小时前
Go 语言进阶:构造函数、父子结构体与组合复用详解
服务器·算法·golang·继承·结构体·构造函数
澈2072 小时前
滑动窗口算法:双指针高效解题秘籍
数据结构·c++·算法
之歆2 小时前
DAY_10 JavaScript 深度解析:原型链 · 引用类型 · 内置对象 · 数组方法全攻略(下)
开发语言·前端·javascript·ecmascript
risc1234562 小时前
python 的字符串前缀
开发语言·python
小程故事多_802 小时前
Agent Loop 核心突破,上下文压缩四大流派,重新定义窗口资源利用率
java·开发语言·人工智能
咩咦2 小时前
C++学习笔记12:类和对象入门
c++·学习笔记·类和对象·封装·struct·class