从C struct到C++中的class

在C语言中,我们使用结构体(struct)和独立的函数来组织数据和操作;而在C++中,我们可以将数据和操作封装在一起,形成"对象"。本文通过一个简单的动态数组(CStash/Stash)实现,展示从C风格到C++风格的完整演变过程:从C的struct+函数,到C++的struct+成员函数,再到C++的class封装。

1. C语言实现:结构体 + 独立函数

在C语言中,我们首先定义一个结构体 CStash 来存储数据,然后提供一系列独立的函数来操作这个结构体。

1.1 头文件声明 (CLib.h)

c 复制代码
// CLib.h
typedef struct CStashTag {
    int size;           // 每个元素的大小(字节)
    int quantity;       // 存储空间当前分配的元素个数
    int next;           // 下一个空闲位置的索引
    unsigned char* storage; // 动态分配的存储空间
} CStash;

// 函数声明
void initialize(CStash* s, int size);
void cleanup(CStash* s);
int add(CStash* s, const void* element);
void* fetch(CStash* s, int index);
int count(CStash* s);
void inflate(CStash* s, int increase);

1.2 函数实现 (CLib.cpp)

c 复制代码
#include "CLib.h"
#include <iostream>
#include <cassert>
using namespace std;

const int increment = 100;  // 每次扩容增加的元素数量

// 初始化CStash
void initialize(CStash* s, int size) {
    s->size = size;        // 注意:这里修正了原代码中的错误,应该是size而不是0
    s->quantity = 0;
    s->next = 0;
    s->storage = 0;
}

// 添加元素
int add(CStash* s, const void* element) {
    if(s->next >= s->quantity) {  // 空间不足时扩容
        inflate(s, increment);
    }
    int startBytes = s->next * s->size;
    unsigned char* e = (unsigned char*)element;
    for(int i = 0; i < s->size; i++) {
        s->storage[startBytes + i] = e[i];
    }
    s->next++;
    return (s->next - 1);  // 返回添加元素的索引
}

// 获取元素
void* fetch(CStash* s, int index) {
    assert(0 <= index);
    if(index >= s->next) {
        return 0;  // 索引越界返回空指针
    }
    return &(s->storage[index * s->size]);
}

// 获取元素数量
int count(CStash* s) {
    return s->next;
}

// 扩容函数
void inflate(CStash* s, int increase) {
    assert(increase > 0);
    int newQuantity = s->quantity + increase;
    int newBytes = newQuantity * s->size;
    int oldBytes = s->quantity * s->size;
    unsigned char* b = new unsigned char[newBytes];
    
    for(int i = 0; i < oldBytes; i++) {
        b[i] = s->storage[i];
    }
    
    delete [](s->storage);
    s->storage = b;
    s->quantity = newQuantity;
}

// 清理资源
void cleanup(CStash* s) {
    if(s->storage != 0) {
        cout << "freeing storage" << endl;
        delete []s->storage;
    }
}

1.3 C语言使用示例

c 复制代码
#include "CLib.h"
#include <iostream>

int main() {
    CStash intStash;
    initialize(&intStash, sizeof(int));
    
    // 添加一些整数
    for(int i = 0; i < 10; i++) {
        add(&intStash, &i);
    }
    
    // 获取并打印所有元素
    for(int i = 0; i < count(&intStash); i++) {
        int* value = (int*)fetch(&intStash, i);
        std::cout << *value << " ";
    }
    
    cleanup(&intStash);
    return 0;
}

2. C++中的struct:数据与函数的初步结合

在C++中,struct不仅可以包含数据成员,还可以包含成员函数。这是从C到C++对象的重要过渡阶段。

2.1 使用struct封装数据和函数

cpp 复制代码
// StashStruct.h
struct Stash {
    int size;           // 每个元素的大小(字节)
    int quantity;       // 存储空间当前分配的元素个数
    int next;           // 下一个空闲位置的索引
    unsigned char* storage; // 动态分配的存储空间
    
    // 成员函数声明
    void initialize(int size);
    void cleanup();
    int add(const void* element);
    void* fetch(int index);
    int count();
    void inflate(int increase);
};

2.2 成员函数实现

cpp 复制代码
#include "StashStruct.h"
#include <iostream>
#include <cassert>
using namespace std;

const int increment = 100;

// 初始化
void Stash::initialize(int sz) {
    size = sz;
    quantity = 0;
    next = 0;
    storage = 0;
}

// 添加元素
int Stash::add(const void* element) {
    if(next >= quantity) {
        inflate(increment);
    }
    int startBytes = next * size;
    unsigned char* e = (unsigned char*)element;
    for(int i = 0; i < size; i++) {
        storage[startBytes + i] = e[i];
    }
    next++;
    return (next - 1);
}

// 获取元素
void* Stash::fetch(int index) {
    assert(0 <= index);
    if(index >= next) {
        return 0;
    }
    return &(storage[index * size]);
}

// 获取元素数量
int Stash::count() {
    return next;
}

// 扩容函数
void Stash::inflate(int increase) {
    assert(increase > 0);
    int newQuantity = quantity + increase;
    int newBytes = newQuantity * size;
    int oldBytes = quantity * size;
    unsigned char* b = new unsigned char[newBytes];
    
    for(int i = 0; i < oldBytes; i++) {
        b[i] = storage[i];
    }
    
    delete []storage;
    storage = b;
    quantity = newQuantity;
}

// 清理资源
void Stash::cleanup() {
    if(storage != 0) {
        cout << "freeing storage" << endl;
        delete []storage;
    }
}

2.3 C++ struct使用示例

cpp 复制代码
#include "StashStruct.h"
#include <iostream>

int main() {
    Stash intStash;
    intStash.initialize(sizeof(int));
    
    // 添加一些整数
    for(int i = 0; i < 10; i++) {
        intStash.add(&i);
    }
    
    // 获取并打印所有元素
    for(int i = 0; i < intStash.count(); i++) {
        int* value = (int*)intStash.fetch(i);
        std::cout << *value << " ";
    }
    
    intStash.cleanup();
    return 0;
}

3. C++中的class:完整的面向对象封装

在C++中,class提供了更完整的封装机制,包括访问控制、构造函数、析构函数等特性。

3.1 类声明 (StashClass.h)

cpp 复制代码
// StashClass.h
class StashClass {
private:
    int size;           // 每个元素的大小(字节)
    int quantity;       // 存储空间当前分配的元素个数
    int next;           // 下一个空闲位置的索引
    unsigned char* storage; // 动态分配的存储空间
    
public:
    // 构造函数
    StashClass(int size);
    
    // 析构函数
    ~StashClass();
    
    // 成员函数
    int add(const void* element);
    void* fetch(int index);
    int count() const;
    
private:
    // 私有辅助函数
    void inflate(int increase);
};

3.2 类实现 (StashClass.cpp)

cpp 复制代码
#include "StashClass.h"
#include <iostream>
#include <cassert>
using namespace std;

const int increment = 100;

// 构造函数
StashClass::StashClass(int sz) {
    size = sz;
    quantity = 0;
    next = 0;
    storage = 0;
}

// 析构函数
StashClass::~StashClass() {
    if(storage != 0) {
        cout << "freeing storage" << endl;
        delete []storage;
    }
}

// 添加元素
int StashClass::add(const void* element) {
    if(next >= quantity) {
        inflate(increment);
    }
    int startBytes = next * size;
    unsigned char* e = (unsigned char*)element;
    for(int i = 0; i < size; i++) {
        storage[startBytes + i] = e[i];
    }
    next++;
    return (next - 1);
}

// 获取元素
void* StashClass::fetch(int index) {
    assert(0 <= index);
    if(index >= next) {
        return 0;
    }
    return &(storage[index * size]);
}

// 获取元素数量
int StashClass::count() const {
    return next;
}

// 扩容函数
void StashClass::inflate(int increase) {
    assert(increase > 0);
    int newQuantity = quantity + increase;
    int newBytes = newQuantity * size;
    int oldBytes = quantity * size;
    unsigned char* b = new unsigned char[newBytes];
    
    for(int i = 0; i < oldBytes; i++) {
        b[i] = storage[i];
    }
    
    delete []storage;
    storage = b;
    quantity = newQuantity;
}

3.3 C++ class使用示例

cpp 复制代码
#include "StashClass.h"
#include <iostream>

int main() {
    StashClass intStash(sizeof(int));
    
    // 添加一些整数
    for(int i = 0; i < 10; i++) {
        intStash.add(&i);
    }
    
    // 获取并打印所有元素
    for(int i = 0; i < intStash.count(); i++) {
        int* value = (int*)intStash.fetch(i);
        std::cout << *value << " ";
    }
    
    // 析构函数自动调用,无需手动清理
    return 0;
}

4. 从C到C++的完整演变过程

4.1 第一阶段:C语言风格(完全分离)

  • 数据结构 :独立的struct CStash
  • 操作函数:全局函数,需要显式传递结构体指针
  • 资源管理 :手动调用initialize()cleanup()
  • 访问控制:无,所有字段公开可访问

4.2 第二阶段:C++ struct风格(初步封装)

  • 数据结构struct Stash包含数据成员
  • 操作函数 :成员函数,通过this指针隐式访问对象
  • 资源管理 :仍需手动调用initialize()cleanup()
  • 访问控制:默认public,但函数与数据在同一作用域

4.3 第三阶段:C++ class风格(完整封装)

  • 数据结构class StashClass包含私有数据成员
  • 操作函数 :公有成员函数,通过this指针隐式访问对象
  • 资源管理:构造函数/析构函数自动管理(RAII)
  • 访问控制:明确的private/public分离,隐藏实现细节

4.4 关键改进点

  1. 封装性增强:从完全分离 → 初步结合 → 完整封装
  2. 资源管理自动化:从手动管理 → 半自动 → 全自动(RAII)
  3. 接口简化:从显式传递指针 → 隐式this指针 → 更自然的对象语法
  4. 安全性提升:从无访问控制 → 默认public → 明确的访问控制

5. 三种实现方式对比表

特性 C语言实现 C++ struct实现 C++ class实现
数据组织 结构体 + 全局函数 struct包含数据和成员函数 class(数据+方法)
初始化 显式调用 initialize() 显式调用 initialize() 构造函数自动调用
清理 显式调用 cleanup() 显式调用 cleanup() 析构函数自动调用
访问控制 无,所有字段公开 默认public,可添加private 支持 private/protected/public
this指针 无,需显式传递对象指针 隐式 this 指针 隐式 this 指针
内存管理 手动管理 手动管理 RAII自动管理
错误处理 返回错误码/特殊值 返回错误码/特殊值 异常机制(可扩展)
语法调用 add(&stash, &element) stash.add(&element) stash.add(&element)
设计理念 过程式编程 面向对象初步 完整的面向对象

5. 实际应用建议

何时使用C风格?

  • 嵌入式系统或资源受限环境
  • 需要与C代码交互
  • 简单的数据容器,不需要复杂操作

何时使用C++风格?

  • 大型项目,需要更好的封装
  • 需要资源自动管理
  • 需要继承和多态
  • 现代C++项目

6. 扩展思考

6.1 模板化改进

C++版本可以进一步改进为模板类,支持类型安全:

cpp 复制代码
template<typename T>
class TStash {
private:
    std::vector<T> storage;
    
public:
    void add(const T& element) {
        storage.push_back(element);
    }
    
    T& operator[](int index) {
        return storage[index];
    }
    
    int count() const {
        return storage.size();
    }
};

6.2 异常安全

为C++版本添加异常安全保证:

cpp 复制代码
void Stash::inflate(int increase) {
    if(increase <= 0) {
        throw std::invalid_argument("Increase must be positive");
    }
    
    // ... 原有实现
}

7. 深入理解C++对象概念

7.1 什么是对象?

在C++中,对象是一个既能描述属性(数据成员)又能描述行为(成员函数)的独立捆绑实体。它拥有自己的内存空间(存储数据)和操作这些数据的方法。对象的核心特征包括:

  1. 封装性:将数据和操作数据的方法捆绑在一起,隐藏内部实现细节
  2. 独立性:每个对象都有自己的内存空间和状态
  3. 行为性:对象能够执行操作(通过成员函数)
  4. 标识性:每个对象都有唯一的身份(内存地址)

7.2 C++对象的内存模型

cpp 复制代码
class StashClass {
private:
    int size;           // 4字节
    int quantity;       // 4字节  
    int next;          // 4字节
    unsigned char* storage; // 8字节(64位系统)
    // 总计:20字节(不考虑内存对齐)
    
public:
    StashClass(int sz);  // 构造函数代码不占用对象内存
    ~StashClass();       // 析构函数代码不占用对象内存
    int add(const void* element);  // 成员函数代码不占用对象内存
    // ...
};

关键理解

  • 对象的内存只包含数据成员,不包含成员函数的代码
  • 成员函数代码在内存中只有一份副本,所有对象共享
  • this指针是隐式参数,指向调用成员函数的对象

7.3 对象如何定位成员函数:this指针与函数调用机制

用户的问题触及了C++对象模型的核心:"对象就是变量,是一块空间,必须有唯一的地址,在这里能存放数据,而且还隐含这有对这些数据进行处理的操作"。这正是理解C++对象的关键------对象只存储数据成员,成员函数代码在内存中只有一份副本,那么对象是如何找到并调用这些函数的呢?

7.3.1 成员函数的存储位置

首先明确一个关键点:成员函数的代码不存储在对象内部。每个对象只包含:

  1. 非静态数据成员
  2. 虚函数表指针(如果类有虚函数)
  3. 基类子对象(如果存在继承)

成员函数的代码存储在程序的**代码段(text segment)**中,所有该类的对象共享同一份函数代码。

cpp 复制代码
class StashClass {
private:
    int size;           // 存储在对象中
    int quantity;       // 存储在对象中
    int next;          // 存储在对象中
    unsigned char* storage; // 存储在对象中
    
public:
    // 以下函数代码不存储在对象中,而是在代码段
    StashClass(int sz);      // 构造函数
    ~StashClass();           // 析构函数
    int add(const void* element);  // 成员函数
    void* fetch(int index);        // 成员函数
    int count() const;             // 成员函数
};
7.3.2 this指针:隐式的桥梁

当调用成员函数时,编译器会自动插入一个隐藏参数:this指针。这个指针指向调用该函数的对象。

编译器视角的转换:

cpp 复制代码
// 源代码
StashClass stash(10);
stash.add(&element);

// 编译器转换后的等价代码
StashClass stash(10);
StashClass::add(&stash, &element);  // 添加了this指针参数

成员函数的实际形式:

cpp 复制代码
// 你写的代码
int StashClass::add(const void* element) {
    if(next >= quantity) {
        inflate(increment);
    }
    // ...
    return (next - 1);
}

// 编译器的视角(简化)
int StashClass::add(StashClass* this, const void* element) {
    if(this->next >= this->quantity) {
        this->inflate(increment);
    }
    // ...
    return (this->next - 1);
}
7.3.3 函数调用解析过程

当对象调用成员函数时,编译器执行以下步骤:

  1. 确定函数地址:根据函数名和类名,在编译时确定函数的代码地址
  2. 传递this指针:将对象的地址作为第一个隐式参数传递给函数
  3. 调整成员访问:在函数内部,所有对数据成员的访问都通过this指针进行

示例分析:

cpp 复制代码
StashClass stash1(10);
StashClass stash2(20);

// 调用同一个函数,但操作不同的对象
stash1.add(&element1);  // this = &stash1
stash2.add(&element2);  // this = &stash2

// 编译器生成的代码类似于:
// StashClass::add(&stash1, &element1);
// StashClass::add(&stash2, &element2);
7.3.4 虚函数的特殊机制

对于非虚函数,函数地址在编译时确定。但对于虚函数,需要通过**虚函数表(vtable)**来动态解析:

cpp 复制代码
class Shape {
public:
    virtual double area() const = 0;  // 纯虚函数
    virtual void draw() const = 0;    // 纯虚函数
    virtual ~Shape() {}               // 虚析构函数
};

class Circle : public Shape {
    double radius;
public:
    Circle(double r) : radius(r) {}
    
    double area() const override {
        return 3.14159 * radius * radius;
    }
    
    void draw() const override {
        std::cout << "Drawing circle with radius " << radius << std::endl;
    }
};

// 内存布局
Circle circle(5.0);
// circle对象包含:
// 1. 虚函数表指针(vptr)-> 指向Circle的虚函数表
// 2. radius数据成员
// 3. 没有Shape的数据成员(因为Shape是抽象类)

// 虚函数表结构(简化):
// Circle的vtable:
// [0] Circle::area的地址
// [1] Circle::draw的地址
// [2] Circle::~Circle的地址

虚函数调用过程:

cpp 复制代码
Shape* shape = new Circle(5.0);
shape->area();  // 动态绑定

// 实际执行步骤:
// 1. 通过shape指针找到对象的vptr
// 2. 通过vptr找到虚函数表
// 3. 在虚函数表中查找area函数的地址
// 4. 调用该地址对应的函数,传递this指针
7.3.5 静态成员函数的区别

静态成员函数没有this指针,因此:

  1. 不能访问非静态成员变量
  2. 不能调用非静态成员函数
  3. 可以通过类名直接调用,不需要对象
cpp 复制代码
class MathUtils {
public:
    static int add(int a, int b) {
        return a + b;
        // 这里不能访问非静态成员
        // 错误:cout << this->value;
    }
    
    static double pi() {
        return 3.141592653589793;
    }
    
private:
    int value;  // 非静态成员
};

// 调用方式
int result = MathUtils::add(3, 4);  // 不需要对象
double piValue = MathUtils::pi();   // 不需要对象
7.3.6 从C到C++的函数调用演变

理解C++的对象如何定位函数,有助于理解从C到C++的演变:

C风格(显式传递结构体指针):

c 复制代码
// C语言:显式传递对象指针
void add(CStash* s, const void* element) {
    if(s->next >= s->quantity) {
        inflate(s, increment);
    }
    // ...
}

// 调用
add(&stash, &element);  // 显式传递&stash

C++风格(隐式this指针):

cpp 复制代码
// C++:隐式this指针
int StashClass::add(const void* element) {
    if(this->next >= this->quantity) {  // this是隐式的
        this->inflate(increment);
    }
    // ...
}

// 调用
stash.add(&element);  // 编译器自动传递&stash作为this
7.3.7 性能考虑
  1. 非虚函数调用:与普通函数调用开销相同,只是多了一个隐式的this参数
  2. 虚函数调用:需要额外的间接寻址(通过虚函数表),有轻微性能开销
  3. 内联函数:编译器可以将小函数内联,消除函数调用开销
  4. 静态绑定 vs 动态绑定
    • 非虚函数:静态绑定(编译时确定)
    • 虚函数:动态绑定(运行时确定)
cpp 复制代码
// 性能对比示例
class Calculator {
public:
    // 非虚函数 - 静态绑定
    int add(int a, int b) { return a + b; }
    
    // 虚函数 - 动态绑定
    virtual int multiply(int a, int b) { return a * b; }
    
    // 内联函数 - 可能被内联展开
    inline int subtract(int a, int b) { return a - b; }
};

// 调用开销(从低到高):
// 1. 内联函数:可能无函数调用开销
// 2. 非虚函数:普通函数调用 + this参数
// 3. 虚函数:虚表查找 + 间接调用 + this参数
7.3.8 实际内存布局示例:深入剖析

让我们通过一个更完整的示例来深入理解C++对象的内存布局,包括内存对齐、虚函数表、继承等复杂情况。

示例1:基本类的内存布局
cpp 复制代码
#include <iostream>
#include <cstddef>

class BasicObject {
private:
    int a;          // 4字节
    char b;         // 1字节
    double c;       // 8字节
    short d;        // 2字节
    
public:
    BasicObject(int a, char b, double c, short d) 
        : a(a), b(b), c(c), d(d) {}
    
    void print() const {
        std::cout << "a=" << a << ", b=" << b 
                  << ", c=" << c << ", d=" << d << std::endl;
    }
    
    int getA() const { return a; }
    void setA(int val) { a = val; }
};

int main() {
    BasicObject obj(10, 'X', 3.14, 5);
    
    std::cout << "BasicObject 大小: " << sizeof(obj) << " 字节" << std::endl;
    std::cout << "成员偏移量:" << std::endl;
    std::cout << "  a: " << offsetof(BasicObject, a) << " 字节" << std::endl;
    std::cout << "  b: " << offsetof(BasicObject, b) << " 字节" << std::endl;
    std::cout << "  c: " << offsetof(BasicObject, c) << " 字节" << std::endl;
    std::cout << "  d: " << offsetof(BasicObject, d) << " 字节" << std::endl;
    
    // 查看实际内存布局
    unsigned char* p = reinterpret_cast<unsigned char*>(&obj);
    std::cout << "\n内存布局(十六进制):" << std::endl;
    for(size_t i = 0; i < sizeof(obj); ++i) {
        printf("%02x ", p[i]);
        if((i + 1) % 8 == 0) std::cout << std::endl;
    }
    
    return 0;
}

输出分析:

复制代码
BasicObject 大小: 24 字节
成员偏移量:
  a: 0 字节
  b: 4 字节
  c: 8 字节
  d: 16 字节

内存布局(十六进制):
0a 00 00 00 58 cc cc cc   // a=10(0x0a), b='X'(0x58), 填充3字节(0xcc)
1f 85 eb 51 b8 1e 09 40   // c=3.14 (double的IEEE754表示)
05 00 cc cc cc cc cc cc   // d=5(0x0005), 填充6字节

内存对齐解释:

  1. int a (4字节) 从偏移0开始
  2. char b (1字节) 从偏移4开始
  3. 由于double c需要8字节对齐,所以在b后面填充3字节(0xcc)
  4. double c (8字节) 从偏移8开始(8的倍数)
  5. short d (2字节) 从偏移16开始
  6. 整个对象需要按8字节对齐(最大成员对齐要求),所以最后填充6字节
示例2:包含虚函数的类
cpp 复制代码
#include <iostream>
#include <cstddef>

class Base {
private:
    int data;  // 4字节
    
public:
    Base(int d) : data(d) {}
    
    virtual void vfunc1() { 
        std::cout << "Base::vfunc1()" << std::endl; 
    }
    
    virtual void vfunc2() { 
        std::cout << "Base::vfunc2()" << std::endl; 
    }
    
    void nonVirtual() { 
        std::cout << "Base::nonVirtual()" << std::endl; 
    }
};

class Derived : public Base {
private:
    double extra;  // 8字节
    
public:
    Derived(int d, double e) : Base(d), extra(e) {}
    
    void vfunc1() override { 
        std::cout << "Derived::vfunc1()" << std::endl; 
    }
    
    virtual void vfunc3() { 
        std::cout << "Derived::vfunc3()" << std::endl; 
    }
};

int main() {
    Base base(100);
    Derived derived(200, 3.14);
    
    std::cout << "Base 大小: " << sizeof(base) << " 字节" << std::endl;
    std::cout << "Derived 大小: " << sizeof(derived) << " 字节" << std::endl;
    
    // 查看虚函数表指针
    void** vptr_base = *(void***)&base;
    void** vptr_derived = *(void***)&derived;
    
    std::cout << "\nBase虚函数表地址: " << vptr_base << std::endl;
    std::cout << "Derived虚函数表地址: " << vptr_derived << std::endl;
    
    // 通过函数指针调用虚函数(仅用于演示,实际中不要这样用)
    using VFunc = void(*)();
    
    std::cout << "\nBase虚函数表内容:" << std::endl;
    VFunc func1_base = reinterpret_cast<VFunc>(vptr_base[0]);
    VFunc func2_base = reinterpret_cast<VFunc>(vptr_base[1]);
    
    std::cout << "vptr[0]: ";
    func1_base();
    std::cout << "vptr[1]: ";
    func2_base();
    
    std::cout << "\nDerived虚函数表内容:" << std::endl;
    VFunc func1_derived = reinterpret_cast<VFunc>(vptr_derived[0]);
    VFunc func2_derived = reinterpret_cast<VFunc>(vptr_derived[1]);
    VFunc func3_derived = reinterpret_cast<VFunc>(vptr_derived[2]);
    
    std::cout << "vptr[0]: ";
    func1_derived();
    std::cout << "vptr[1]: ";
    func2_derived();
    std::cout << "vptr[2]: ";
    func3_derived();
    
    return 0;
}

内存布局示意图:

复制代码
Base对象布局(64位系统):
+----------------+ 偏移0
| vptr (8字节)   | -> 指向Base的虚函数表
+----------------+ 偏移8
| data (4字节)   |
+----------------+ 偏移12
| 填充 (4字节)   | // 对齐到8字节边界
+----------------+ 总计16字节

Derived对象布局:
+----------------+ 偏移0
| vptr (8字节)   | -> 指向Derived的虚函数表
+----------------+ 偏移8  
| Base::data     |
+----------------+ 偏移12
| 填充 (4字节)   | // 对齐到8字节边界
+----------------+ 偏移16
| Derived::extra |
+----------------+ 偏移24
                 总计24字节

Base虚函数表:
+----------------+
| &Base::vfunc1  |
+----------------+
| &Base::vfunc2  |
+----------------+

Derived虚函数表:
+----------------+
| &Derived::vfunc1| // 重写了Base::vfunc1
+----------------+
| &Base::vfunc2   | // 继承Base::vfunc2
+----------------+
| &Derived::vfunc3| // 新增的虚函数
+----------------+
示例3:多重继承的内存布局
cpp 复制代码
#include <iostream>

class Base1 {
public:
    int data1;
    Base1(int d) : data1(d) {}
    virtual void func1() { std::cout << "Base1::func1" << std::endl; }
};

class Base2 {
public:
    int data2;
    Base2(int d) : data2(d) {}
    virtual void func2() { std::cout << "Base2::func2" << std::endl; }
};

class Derived : public Base1, public Base2 {
public:
    int data3;
    Derived(int d1, int d2, int d3) : Base1(d1), Base2(d2), data3(d3) {}
    void func1() override { std::cout << "Derived::func1" << std::endl; }
    void func2() override { std::cout << "Derived::func2" << std::endl; }
    virtual void func3() { std::cout << "Derived::func3" << std::endl; }
};

int main() {
    Derived d(10, 20, 30);
    
    std::cout << "Derived对象大小: " << sizeof(d) << " 字节" << std::endl;
    std::cout << "Base1子对象偏移: " << (void*)&d - (void*)(Base1*)&d << std::endl;
    std::cout << "Base2子对象偏移: " << (void*)&d - (void*)(Base2*)&d << std::endl;
    
    // 验证虚函数表
    Base1* b1 = &d;
    Base2* b2 = &d;
    
    std::cout << "\n通过Base1指针调用:" << std::endl;
    b1->func1();  // 调用Derived::func1
    
    std::cout << "\n通过Base2指针调用:" << std::endl;
    b2->func2();  // 调用Derived::func2
    
    return 0;
}

多重继承内存布局:

复制代码
Derived对象布局:
+----------------+ 偏移0
| Base1::vptr    | -> 指向Derived中Base1部分的虚函数表
+----------------+ 偏移8
| Base1::data1   |
+----------------+ 偏移12
| 填充 (4字节)   |
+----------------+ 偏移16
| Base2::vptr    | -> 指向Derived中Base2部分的虚函数表
+----------------+ 偏移24
| Base2::data2   |
+----------------+ 偏移28
| Derived::data3 |
+----------------+ 偏移32
| 填充 (4字节)   | // 对齐到8字节边界
+----------------+ 总计40字节
示例4:使用编译器特定扩展查看内存布局
cpp 复制代码
#include <iostream>

// GCC/Clang的编译器扩展,可以查看类的内存布局
#ifdef __GNUC__
    #define SHOW_LAYOUT(classname) \
        std::cout << #classname "的内存布局:" << std::endl; \
        __builtin_dump_struct(&obj, printf)
#else
    #define SHOW_LAYOUT(classname) \
        std::cout << #classname "的大小: " << sizeof(obj) << "字节" << std::endl
#endif

class SimpleClass {
public:
    int a;
    char b;
    double c;
    
    SimpleClass(int a, char b, double c) : a(a), b(b), c(c) {}
    
    virtual void virtFunc() {}
    void nonVirtFunc() {}
};

int main() {
    SimpleClass obj(1, 'A', 3.14);
    
    std::cout << "SimpleClass 大小: " << sizeof(obj) << " 字节" << std::endl;
    std::cout << "对齐要求: " << alignof(SimpleClass) << " 字节" << std::endl;
    
    // 使用编译器扩展查看布局(如果可用)
    SHOW_LAYOUT(SimpleClass);
    
    // 手动计算偏移量
    std::cout << "\n手动计算偏移量:" << std::endl;
    std::cout << "vptr 偏移: " << (void*)&obj - (void*)&obj << std::endl;
    std::cout << "a 偏移: " << (void*)&obj.a - (void*)&obj << std::endl;
    std::cout << "b 偏移: " << (void*)&obj.b - (void*)&obj << std::endl;
    std::cout << "c 偏移: " << (void*)&obj.c - (void*)&obj << std::endl;
    
    return 0;
}
关键理解要点:
  1. 内存对齐原则

    • 每个成员的偏移量必须是其类型大小的整数倍
    • 整个对象的大小必须是最大成员对齐要求的整数倍
    • 编译器会插入填充字节以满足对齐要求
  2. 虚函数表指针

    • 有虚函数的类会在对象开头包含一个vptr(虚函数表指针)
    • vptr指向该类的虚函数表,表中存储虚函数的地址
    • 每个有虚函数的类都有自己的虚函数表
  3. 继承的内存布局

    • 单继承:派生类包含基类子对象 + 自己的成员
    • 多重继承:派生类包含多个基类子对象(可能有多个vptr)
    • 虚继承:更复杂,引入虚基类指针
  4. 空类的大小

    cpp 复制代码
    class Empty {};
    // sizeof(Empty) == 1(不能为0,确保每个对象有唯一地址)
    
    class EmptyWithVirtual {
        virtual void func() {}
    };
    // sizeof(EmptyWithVirtual) == 8(64位系统,只有vptr)
  5. 标准布局类型(Standard Layout Type)

    • 没有虚函数
    • 所有非静态成员具有相同的访问控制
    • 没有引用类型的非静态成员
    • 没有虚基类
    • 所有非静态数据成员和基类都是标准布局类型
    • 满足这些条件的类型可以与C结构体互操作
实际调试技巧:
  1. 使用调试器查看内存

    bash 复制代码
    # GDB示例
    (gdb) p obj
    (gdb) x/8gx &obj  # 以16进制查看前8个8字节
    (gdb) info vtbl obj  # 查看虚函数表
  2. 使用reinterpret_cast查看原始内存

    cpp 复制代码
    void* raw = reinterpret_cast<void*>(&obj);
    unsigned char* bytes = static_cast<unsigned char*>(raw);
    for(size_t i = 0; i < sizeof(obj); ++i) {
        printf("%02x ", bytes[i]);
    }
  3. 使用offsetof宏

    cpp 复制代码
    #include <cstddef>
    std::cout << "成员偏移: " << offsetof(ClassName, memberName) << std::endl;
性能影响:
  1. 缓存友好性:连续存储的小对象更可能在同一缓存行
  2. 虚假共享:不同线程访问同一缓存行的不同部分会导致性能下降
  3. 内存访问模式:顺序访问比随机访问快,考虑数据布局
  4. 对象大小:小于64字节的对象通常更适合栈分配

通过深入理解内存布局,你可以:

  • 优化数据访问模式,提高缓存命中率
  • 减少内存碎片和填充字节
  • 理解多态的实现机制
  • 调试内存相关的问题
  • 设计更高效的类结构

记住:C++对象的内存布局是由编译器根据语言规则和平台ABI决定的,理解这些规则有助于编写更高效、更可预测的代码。

7.3.9 总结:对象如何定位函数

回答用户的核心问题:"对象是怎么定位函数的呢?"

  1. 非虚函数

    • 函数代码存储在代码段,所有对象共享
    • 编译时确定函数地址
    • 调用时,编译器自动插入对象的地址作为this指针
    • 函数内部通过this指针访问对象的数据成员
  2. 虚函数

    • 每个对象包含一个虚函数表指针(vptr)
    • 虚函数表存储虚函数的地址
    • 调用时,通过vptr找到虚函数表,再找到函数地址
    • 支持运行时多态
  3. 静态函数

    • 没有this指针
    • 通过类名直接调用
    • 不能访问非静态成员

关键理解

  • 对象是数据 的容器,不是代码的容器
  • 成员函数是共享的 ,通过this指针区分不同的对象
  • 这种设计既节省内存(代码不重复存储),又保持了面向对象的封装特性
  • 从C的"数据+函数分离"到C++的"对象封装",本质是通过编译器自动管理this指针实现的语法糖

这种机制使得C++既能保持C的高效性(函数代码共享),又能提供面向对象的封装和多态特性。

7.3 与其他语言中对象的对比

7.3.1 C++ vs Java
特性 C++对象 Java对象
内存管理 手动/RAII(栈对象自动销毁) 垃圾回收(GC)自动管理
对象创建 栈上或堆上(new 总是在堆上(new
默认行为 值语义(拷贝) 引用语义
访问控制 private/protected/public 相同,但包级访问不同
多继承 支持 不支持(只支持接口多继承)
对象大小 精确可控(不含虚函数表) 包含对象头、类指针等开销

示例对比

cpp 复制代码
// C++:栈对象,自动管理
StashClass stash(10);  // 栈上创建,析构时自动清理
java 复制代码
// Java:堆对象,GC管理
StashClass stash = new StashClass(10);  // 堆上创建,GC回收
7.3.2 C++ vs JavaScript
特性 C++对象 JavaScript对象
类型系统 静态类型,编译时检查 动态类型,运行时检查
内存布局 固定结构,高效内存访问 哈希表/字典,属性动态添加
继承机制 基于类的继承 基于原型的继承
方法绑定 编译时绑定(非虚函数) 运行时查找
性能特点 高效,接近硬件 解释执行,有JIT优化

示例对比

cpp 复制代码
// C++:编译时确定类型和方法
class Rect {
    int width, height;
public:
    int area() { return width * height; }
};
Rect r;  // 编译时确定内存布局
javascript 复制代码
// JavaScript:运行时动态
let rect = {
    width: 10,
    height: 20,
    area: function() { return this.width * this.height; }
};
// 可以动态添加属性
rect.color = "red";
7.3.3 C++ vs Python
特性 C++对象 Python对象
内存管理 手动/RAII/智能指针 引用计数 + GC
动态特性 有限(RTTI) 完全动态(可运行时修改)
性能 接近硬件,高效 解释执行,较慢
元编程 模板、constexpr 装饰器、元类
鸭子类型 不支持(静态类型) 支持

7.4 C++对象的本质理解

  1. 对象是变量:在C++中,对象本质上就是一块内存空间,它既存储数据,也隐含着对这些数据进行处理的操作(通过成员函数)。

  2. this指针的魔法

    cpp 复制代码
    // 编译器视角:成员函数调用被转换为普通函数调用
    stash.add(&element);  // 实际调用:StashClass::add(&stash, &element)
    // this指针在add()函数中隐式可用
  3. 构造函数的作用

    • 不是"创建"对象(内存已分配),而是"初始化"对象
    • 确保对象在创建后处于有效状态
    • 实现RAII(资源获取即初始化)
  4. 析构函数的作用

    • 不是"销毁"对象(内存将释放),而是"清理"对象
    • 确保资源正确释放
    • 实现异常安全

7.5 现代C++中的对象演进

  1. 移动语义(C++11):

    cpp 复制代码
    StashClass a(10);
    StashClass b = std::move(a);  // 移动构造,避免深拷贝
  2. 智能指针

    cpp 复制代码
    std::unique_ptr<StashClass> stash = std::make_unique<StashClass>(10);
    // 自动管理生命周期,无需手动delete
  3. 值语义 vs 引用语义

    • C++默认使用值语义(对象拷贝)
    • 可通过引用、指针实现引用语义
    • 现代C++鼓励值语义 + 移动语义

7.6 对象设计的最佳实践

  1. 遵循RAII原则:资源在构造函数中获取,在析构函数中释放
  2. 优先使用栈对象:自动管理生命周期,避免内存泄漏
  3. 明确所有权语义 :使用unique_ptrshared_ptr明确资源所有权
  4. 支持移动语义:对于可移动的资源,实现移动构造函数和移动赋值运算符
  5. 保持对象不变式:确保对象在任何公开方法调用前后都处于有效状态

7.8 对象的大小与设计原则

7.8.1 对象应当多大?

在C++中,对象的大小是一个重要的设计考虑因素。对象大小直接影响内存使用效率、缓存局部性和性能。

对象大小的决定因素:

  1. 数据成员大小:所有非静态数据成员的总和
  2. 内存对齐:编译器为优化内存访问而添加的填充字节
  3. 虚函数表指针:如果类有虚函数,会增加一个指针大小(通常8字节)
  4. 继承开销:基类子对象的大小

示例:计算对象大小

cpp 复制代码
#include <iostream>

class SmallObject {
    int x;          // 4字节
    char c;         // 1字节
    // 填充3字节(对齐到8字节边界)
};  // 总计:8字节

class MediumObject {
    int data[10];   // 40字节
    double d;       // 8字节
    // 总计:48字节
};

class LargeObject {
    int* array;     // 8字节(指针)
    int size;       // 4字节
    // 填充4字节(对齐到8字节边界)
    // 总计:16字节(不包含动态分配的内存)
};

int main() {
    std::cout << "SmallObject: " << sizeof(SmallObject) << " bytes\n";
    std::cout << "MediumObject: " << sizeof(MediumObject) << " bytes\n";
    std::cout << "LargeObject: " << sizeof(LargeObject) << " bytes\n";
    return 0;
}

对象大小的指导原则:

  1. 小对象优先(通常小于64字节):

    • 适合栈分配,避免堆分配开销
    • 更好的缓存局部性
    • 适合值语义和频繁拷贝
  2. 中等对象(64字节到1KB):

    • 根据使用场景选择栈或堆分配
    • 考虑使用智能指针管理生命周期
  3. 大对象(大于1KB):

    • 通常应该在堆上分配
    • 使用智能指针或自定义内存管理
    • 避免值语义,使用移动语义或引用
7.8.2 对象应当像什么?

C++对象的设计应该遵循一些核心原则,使其行为符合直觉且高效:

1. 值语义(Value Semantics)

cpp 复制代码
// 好的设计:像内置类型一样工作
class Point {
    int x, y;
public:
    Point(int x, int y) : x(x), y(y) {}
    
    // 支持拷贝和移动
    Point(const Point&) = default;
    Point& operator=(const Point&) = default;
    
    // 支持比较
    bool operator==(const Point& other) const {
        return x == other.x && y == other.y;
    }
    
    // 支持算术运算
    Point operator+(const Point& other) const {
        return Point(x + other.x, y + other.y);
    }
};

// 使用起来像内置类型
Point p1(1, 2);
Point p2 = p1;  // 拷贝
Point p3 = p1 + p2;  // 加法

2. RAII(资源获取即初始化)

cpp 复制代码
class File {
    FILE* handle;
public:
    explicit File(const char* filename) 
        : handle(fopen(filename, "r")) {
        if (!handle) throw std::runtime_error("无法打开文件");
    }
    
    ~File() {
        if (handle) fclose(handle);
    }
    
    // 禁止拷贝,支持移动
    File(const File&) = delete;
    File& operator=(const File&) = delete;
    
    File(File&& other) noexcept : handle(other.handle) {
        other.handle = nullptr;
    }
    
    File& operator=(File&& other) noexcept {
        if (this != &other) {
            if (handle) fclose(handle);
            handle = other.handle;
            other.handle = nullptr;
        }
        return *this;
    }
    
    void read(void* buffer, size_t size) {
        if (fread(buffer, 1, size, handle) != size) {
            throw std::runtime_error("读取失败");
        }
    }
};

3. 单一职责原则

cpp 复制代码
// 不好的设计:一个类做太多事情
class DatabaseManager {
    // 连接管理、查询执行、结果解析、事务处理...
};

// 好的设计:职责分离
class DatabaseConnection { /* 连接管理 */ };
class QueryExecutor { /* 查询执行 */ };
class ResultParser { /* 结果解析 */ };
class TransactionManager { /* 事务处理 */ };

4. 最小接口原则

cpp 复制代码
class Stack {
    std::vector<int> data;
public:
    // 只提供必要的操作
    void push(int value) { data.push_back(value); }
    int pop() {
        if (data.empty()) throw std::runtime_error("栈空");
        int value = data.back();
        data.pop_back();
        return value;
    }
    bool empty() const { return data.empty(); }
    size_t size() const { return data.size(); }
    
    // 不暴露内部实现细节
    // 没有提供:begin(), end(), operator[] 等
};

5. 不变式保持

cpp 复制代码
class Date {
    int year, month, day;
    
    bool isValid() const {
        // 验证日期是否有效
        return month >= 1 && month <= 12 &&
               day >= 1 && day <= daysInMonth(year, month);
    }
    
public:
    Date(int y, int m, int d) : year(y), month(m), day(d) {
        if (!isValid()) {
            throw std::invalid_argument("无效日期");
        }
    }
    
    void setMonth(int m) {
        if (m < 1 || m > 12) {
            throw std::invalid_argument("月份无效");
        }
        month = m;
        if (!isValid()) {
            throw std::invalid_argument("日期无效");
        }
    }
    
    // 所有公开方法都保持对象不变式
};
7.8.3 对象设计的经验法则
  1. 像值一样思考 :设计类时,思考它是否应该像intdouble一样工作
  2. 像资源管理器一样思考:管理资源的类应该遵循RAII原则
  3. 像抽象数据类型一样思考:提供清晰的接口,隐藏实现细节
  4. 像服务提供者一样思考:提供特定功能,不暴露不必要的信息

对象大小的经验法则:

  • 如果对象小于缓存行(通常64字节),优先考虑栈分配
  • 如果对象包含大块数据,使用指针或智能指针间接存储
  • 避免"胖接口"类,保持对象职责单一
  • 考虑使用Pimpl惯用法隐藏大对象实现细节
cpp 复制代码
// Pimpl惯用法示例:隐藏大对象实现
class BigObjectImpl;  // 前向声明

class BigObject {
    std::unique_ptr<BigObjectImpl> pImpl;
public:
    BigObject();
    ~BigObject();
    // 接口方法...
    void doSomething();
};
7.8.4 实际案例分析:Stash类的设计演进

回顾我们的Stash示例,可以看到对象设计的演进:

  1. C风格:数据与操作分离,对象只是被动数据结构
  2. C++ struct风格:数据与操作初步结合,但缺乏封装
  3. C++ class风格:完整封装,但仍有改进空间

现代C++改进版本:

cpp 复制代码
template<typename T>
class ModernStash {
    std::vector<T> storage;  // 使用标准库容器
    size_t next = 0;
    
public:
    ModernStash() = default;
    
    // 移动语义支持
    ModernStash(ModernStash&&) = default;
    ModernStash& operator=(ModernStash&&) = default;
    
    // 禁止拷贝(或实现深拷贝)
    ModernStash(const ModernStash&) = delete;
    ModernStash& operator=(const ModernStash&) = delete;
    
    void add(T value) {
        if (next >= storage.size()) {
            storage.resize(storage.empty() ? 1 : storage.size() * 2);
        }
        storage[next++] = std::move(value);
    }
    
    T& operator[](size_t index) {
        if (index >= next) {
            throw std::out_of_range("索引越界");
        }
        return storage[index];
    }
    
    const T& operator[](size_t index) const {
        if (index >= next) {
            throw std::out_of_range("索引越界");
        }
        return storage[index];
    }
    
    size_t size() const { return next; }
    bool empty() const { return next == 0; }
    
    // 迭代器支持
    auto begin() { return storage.begin(); }
    auto end() { return storage.begin() + next; }
    auto begin() const { return storage.begin(); }
    auto end() const { return storage.begin() + next; }
};

这个现代版本体现了良好的对象设计:

  • 适当的大小 :只存储必要数据,大内存由std::vector管理
  • 值语义:支持移动,禁止不必要的拷贝
  • RAIIstd::vector自动管理内存
  • 最小接口:提供必要的操作,隐藏实现细节
  • 异常安全:使用异常报告错误
  • STL兼容:提供迭代器支持
7.8.5 总结:优秀C++对象的特征
  1. 大小适中:通常小于缓存行,或使用间接存储管理大内存
  2. 行为明确:像值、像资源管理器、或像服务提供者
  3. 接口简洁:提供最小必要接口,隐藏实现细节
  4. 资源安全:遵循RAII原则,自动管理资源
  5. 异常安全:提供基本或强异常安全保证
  6. 可移动:支持移动语义,避免不必要的深拷贝
  7. 可测试:依赖注入,便于单元测试
  8. 符合直觉:使用起来像内置类型或标准库组件

记住:好的C++对象应该让使用者感到自然、安全、高效,就像使用语言内置类型一样。

7.7 C++中struct与class的异同与选择

在C++中,structclass是两种非常相似但又有关键区别的语法结构。理解它们的异同对于编写清晰、可维护的代码至关重要。

7.7.1 基本语法对比

相同点:

  1. 都可以包含数据成员和成员函数
  2. 都支持构造函数、析构函数、拷贝控制
  3. 都支持继承和多态
  4. 都支持访问控制(public/protected/private)
  5. 都支持模板和运算符重载

语法示例:

cpp 复制代码
// 使用struct定义
struct PointStruct {
    int x, y;
    
    PointStruct(int x, int y) : x(x), y(y) {}
    
    void move(int dx, int dy) {
        x += dx;
        y += dy;
    }
    
    int area() const {
        return x * y;
    }
};

// 使用class定义
class PointClass {
    int x, y;
    
public:
    PointClass(int x, int y) : x(x), y(y) {}
    
    void move(int dx, int dy) {
        x += dx;
        y += dy;
    }
    
    int area() const {
        return x * y;
    }
};
7.7.2 关键区别

1. 默认访问权限

这是structclass最核心的区别:

cpp 复制代码
struct S {
    int a;          // 默认public
    void f();       // 默认public
    
private:
    int b;          // 显式private
};

class C {
    int a;          // 默认private
    void f();       // 默认private
    
public:
    int b;          // 显式public
};

2. 默认继承权限

cpp 复制代码
struct D1 : Base {      // 默认public继承
    // ...
};

class D2 : Base {       // 默认private继承
    // ...
};

// 等价于:
struct D1 : public Base { /* ... */ };
class D2 : private Base { /* ... */ };

3. 模板参数中的行为

cpp 复制代码
template<typename T>
struct TemplateStruct {
    // T的成员默认public访问
};

template<typename T>
class TemplateClass {
    // T的成员默认private访问
};
7.7.3 使用场景与选择指南

何时使用struct?

  1. 简单的数据聚合

    cpp 复制代码
    // 适合使用struct的场景
    struct Point {
        double x, y, z;  // 都是public,直接访问
    };
    
    struct Color {
        uint8_t r, g, b, a;
    };
    
    struct Vertex {
        Point position;
        Color color;
        float texture_u, texture_v;
    };
  2. C兼容性要求

    cpp 复制代码
    // 需要与C代码交互时
    #ifdef __cplusplus
    extern "C" {
    #endif
    
    struct CCompatible {
        int id;
        char name[32];
    };
    
    #ifdef __cplusplus
    }
    #endif
  3. 值类型(Value Types)

    cpp 复制代码
    // 像内置类型一样工作的简单类型
    struct Money {
        long cents;  // 以分为单位存储
        
        Money(long c) : cents(c) {}
        
        Money operator+(const Money& other) const {
            return Money(cents + other.cents);
        }
        
        bool operator==(const Money& other) const {
            return cents == other.cents;
        }
    };
  4. 模板元编程中的类型特征

    cpp 复制代码
    // 类型特征通常用struct
    template<typename T>
    struct is_pointer {
        static constexpr bool value = false;
    };
    
    template<typename T>
    struct is_pointer<T*> {
        static constexpr bool value = true;
    };

何时使用class?

  1. 需要严格封装的复杂类型

    cpp 复制代码
    class BankAccount {
    private:
        std::string account_number;
        double balance;
        std::vector<Transaction> history;
        
        void log_transaction(const Transaction& t) {
            history.push_back(t);
            // 可能还有审计、加密等操作
        }
        
    public:
        explicit BankAccount(const std::string& acc_num)
            : account_number(acc_num), balance(0.0) {}
        
        void deposit(double amount) {
            if (amount <= 0) throw std::invalid_argument("金额必须为正");
            balance += amount;
            log_transaction(Transaction::deposit(amount));
        }
        
        void withdraw(double amount) {
            if (amount <= 0) throw std::invalid_argument("金额必须为正");
            if (amount > balance) throw std::runtime_error("余额不足");
            balance -= amount;
            log_transaction(Transaction::withdrawal(amount));
        }
        
        double get_balance() const { return balance; }
    };
  2. 需要实现多态的类层次结构

    cpp 复制代码
    class Shape {
    protected:
        std::string color;
        
    public:
        explicit Shape(const std::string& c) : color(c) {}
        virtual ~Shape() = default;
        
        virtual double area() const = 0;
        virtual void draw() const = 0;
    };
    
    class Circle : public Shape {
        double radius;
        
    public:
        Circle(const std::string& c, double r) 
            : Shape(c), radius(r) {}
        
        double area() const override {
            return 3.14159 * radius * radius;
        }
        
        void draw() const override {
            std::cout << "绘制" << color << "的圆,半径" << radius << std::endl;
        }
    };
  3. 资源管理类(RAII)

    cpp 复制代码
    class FileHandle {
        FILE* handle;
        
    public:
        explicit FileHandle(const char* filename, const char* mode)
            : handle(fopen(filename, mode)) {
            if (!handle) throw std::runtime_error("无法打开文件");
        }
        
        ~FileHandle() {
            if (handle) fclose(handle);
        }
        
        // 禁止拷贝
        FileHandle(const FileHandle&) = delete;
        FileHandle& operator=(const FileHandle&) = delete;
        
        // 允许移动
        FileHandle(FileHandle&& other) noexcept : handle(other.handle) {
            other.handle = nullptr;
        }
        
        FileHandle& operator=(FileHandle&& other) noexcept {
            if (this != &other) {
                if (handle) fclose(handle);
                handle = other.handle;
                other.handle = nullptr;
            }
            return *this;
        }
        
        size_t read(void* buffer, size_t size) {
            return fread(buffer, 1, size, handle);
        }
    };
  4. 需要复杂不变式维护的类

    cpp 复制代码
    class Date {
        int year, month, day;
        
        bool is_valid() const {
            // 复杂的日期验证逻辑
            return month >= 1 && month <= 12 &&
                   day >= 1 && day <= days_in_month(year, month);
        }
        
    public:
        Date(int y, int m, int d) : year(y), month(m), day(d) {
            if (!is_valid()) {
                throw std::invalid_argument("无效日期");
            }
        }
        
        void set_month(int m) {
            if (m < 1 || m > 12) {
                throw std::invalid_argument("月份无效");
            }
            month = m;
            if (!is_valid()) {
                throw std::invalid_argument("日期无效");
            }
        }
        
        // 其他方法都必须维护不变式
    };
7.7.4 实际代码中的混合使用

在实际项目中,经常根据语义混合使用structclass

cpp 复制代码
// 数据传递结构体
struct Point {
    int x, y;
};

struct Size {
    int width, height;
};

// 复杂的图形对象类
class Rectangle {
    Point position;
    Size size;
    std::string fill_color;
    std::string border_color;
    int border_width;
    
public:
    Rectangle(Point p, Size s, std::string fill = "white")
        : position(p), size(s), fill_color(std::move(fill)),
          border_color("black"), border_width(1) {}
    
    void draw() const {
        // 绘制逻辑
    }
    
    bool contains(Point p) const {
        return p.x >= position.x && p.x <= position.x + size.width &&
               p.y >= position.y && p.y <= position.y + size.height;
    }
    
    // 更多复杂方法...
};

// 函数参数使用struct传递配置
struct DrawOptions {
    bool antialias = true;
    bool transparent = false;
    int quality = 90;
};

void draw_shape(const Shape& shape, const DrawOptions& options);
7.7.5 性能考虑

内存布局完全相同:

cpp 复制代码
struct S { int a; int b; };
class C { int a; int b; public: C(int x, int y) : a(x), b(y) {} };

static_assert(sizeof(S) == sizeof(C), "大小应该相同");
static_assert(alignof(S) == alignof(C), "对齐应该相同");

访问权限不影响性能:

  • publicprivateprotected只是编译期检查
  • 运行时没有任何性能差异
  • 内联函数同样有效
7.7.6 现代C++中的趋势
  1. 统一初始化语法(C++11起):

    cpp 复制代码
    struct Point { int x, y; };
    class Circle { int x, y, r; public: Circle(int x, int y, int r) : x(x), y(y), r(r) {} };
    
    Point p{10, 20};        // OK
    Circle c{10, 20, 5};    // OK - 使用构造函数
  2. 结构化绑定(C++17):

    cpp 复制代码
    struct Point { int x, y; };
    Point get_point() { return {10, 20}; }
    
    auto [x, y] = get_point();  // x=10, y=20
  3. 三路比较运算符(C++20):

    cpp 复制代码
    struct Point {
        int x, y;
        
        auto operator<=>(const Point&) const = default;
    };
    
    Point a{1, 2}, b{1, 2};
    bool equal = (a == b);  // true
7.7.7 总结:struct vs class的选择原则
特性 struct class
默认访问权限 public private
默认继承权限 public private
典型用途 数据聚合、POD类型、值类型 复杂对象、资源管理、多态
语义含义 "这是一个数据结构" "这是一个有行为的对象"
C兼容性 更好 较差
模板元编程 常用 较少用

选择指南:

  1. 使用struct当:

    • 只有公有数据成员
    • 没有或很少成员函数
    • 需要与C代码交互
    • 作为简单的值类型(如Point、Color、Rect)
    • 在模板元编程中定义类型特征
  2. 使用class当:

    • 有私有或保护成员
    • 需要封装复杂的不变式
    • 管理资源(文件、内存、网络连接)
    • 需要多态(虚函数)
    • 有复杂的构造函数/析构函数逻辑
  3. 一致性原则:

    • 在同一个项目中保持一致的约定
    • 如果团队已有规范,遵循团队规范
    • 公开API考虑使用者的期望

最终建议:

  • 语义优先 :用struct表示"这是一个数据结构",用class表示"这是一个有行为的对象"
  • 明确性优先 :即使使用struct,如果某些成员应该是私有的,显式使用private:
  • 一致性优先:在整个代码库中保持一致的风格

记住:在C++中,structclass的技术差异很小(主要是默认访问权限),选择哪一个更多是基于代码的语义和可读性考虑。

7.7 从C到C++对象思维的转变

从C的"数据+函数"分离到C++的"对象"封装,不仅仅是语法变化,更是思维方式的转变:

  1. 从操作数据到操作对象:C程序员思考"如何操作这块内存",C++程序员思考"这个对象能做什么"
  2. 从手动管理到自动管理:C程序员手动管理生命周期,C++程序员依赖构造函数/析构函数
  3. 从全局状态到对象状态:C使用全局变量和静态变量,C++将状态封装在对象内部
  4. 从函数接口到对象接口:C提供函数库,C++提供类库,接口更自然、更安全

这种思维转变使得代码更加模块化、可维护,是现代软件工程的重要基础。

总结

从C结构体到C++对象的转变,不仅仅是语法上的变化,更是编程范式的转变:

  1. 从过程式到面向对象:将数据和操作封装在一起
  2. 从手动管理到自动管理:利用构造函数/析构函数实现RAII
  3. 从松散耦合到紧密封装:通过访问控制保护数据完整性
  4. 从函数调用到方法调用:更自然的对象操作语法

这种演变使得代码更加安全、易维护和可扩展,是现代C++编程的重要基础。


注:本文示例代码基于Bruce Eckel的《Thinking in C++》中的Stash示例,进行了适当简化和修正。

相关推荐
GIS阵地4 小时前
QgsRasterDataProvider 完整详解(QGIS 3.40.13 C++)
开发语言·c++·qt·开源软件·qgis
charlie1145141916 小时前
Cinux: 为大内核铺路
开发语言·c++·操作系统·现代c++
2501_914245936 小时前
C语言设计模式详解:从理论到实践的完整指南
c语言·开发语言·设计模式
米罗篮9 小时前
矩阵快速幂 (Exponentiation By Squaring Applied To Matrices)
c++·线性代数·算法·矩阵
肖爱Kun9 小时前
C++设计策略模式
开发语言·c++·策略模式
2501_914245939 小时前
C语言与硬件交互:从GPIO到中断的嵌入式编程实践
c语言·单片机·交互
炸膛坦客10 小时前
单片机/C/C++八股:(二十四)编译文件( .bin 和 .hex ,包括 .elf 和 .axf )
c语言·c++·单片机
cpp_250111 小时前
P10098 [ROIR 2023] 地铁建设 (Day 2)
数据结构·c++·算法·贪心·二分答案·洛谷题解
hehelm12 小时前
IO 多路复用 — Reactor
linux·服务器·网络·数据库·c++