在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 关键改进点
- 封装性增强:从完全分离 → 初步结合 → 完整封装
- 资源管理自动化:从手动管理 → 半自动 → 全自动(RAII)
- 接口简化:从显式传递指针 → 隐式this指针 → 更自然的对象语法
- 安全性提升:从无访问控制 → 默认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++中,对象是一个既能描述属性(数据成员)又能描述行为(成员函数)的独立捆绑实体。它拥有自己的内存空间(存储数据)和操作这些数据的方法。对象的核心特征包括:
- 封装性:将数据和操作数据的方法捆绑在一起,隐藏内部实现细节
- 独立性:每个对象都有自己的内存空间和状态
- 行为性:对象能够执行操作(通过成员函数)
- 标识性:每个对象都有唯一的身份(内存地址)
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 成员函数的存储位置
首先明确一个关键点:成员函数的代码不存储在对象内部。每个对象只包含:
- 非静态数据成员
- 虚函数表指针(如果类有虚函数)
- 基类子对象(如果存在继承)
成员函数的代码存储在程序的**代码段(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 函数调用解析过程
当对象调用成员函数时,编译器执行以下步骤:
- 确定函数地址:根据函数名和类名,在编译时确定函数的代码地址
- 传递this指针:将对象的地址作为第一个隐式参数传递给函数
- 调整成员访问:在函数内部,所有对数据成员的访问都通过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指针,因此:
- 不能访问非静态成员变量
- 不能调用非静态成员函数
- 可以通过类名直接调用,不需要对象
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 性能考虑
- 非虚函数调用:与普通函数调用开销相同,只是多了一个隐式的this参数
- 虚函数调用:需要额外的间接寻址(通过虚函数表),有轻微性能开销
- 内联函数:编译器可以将小函数内联,消除函数调用开销
- 静态绑定 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字节
内存对齐解释:
int a(4字节) 从偏移0开始char b(1字节) 从偏移4开始- 由于
double c需要8字节对齐,所以在b后面填充3字节(0xcc) double c(8字节) 从偏移8开始(8的倍数)short d(2字节) 从偏移16开始- 整个对象需要按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;
}
关键理解要点:
-
内存对齐原则:
- 每个成员的偏移量必须是其类型大小的整数倍
- 整个对象的大小必须是最大成员对齐要求的整数倍
- 编译器会插入填充字节以满足对齐要求
-
虚函数表指针:
- 有虚函数的类会在对象开头包含一个vptr(虚函数表指针)
- vptr指向该类的虚函数表,表中存储虚函数的地址
- 每个有虚函数的类都有自己的虚函数表
-
继承的内存布局:
- 单继承:派生类包含基类子对象 + 自己的成员
- 多重继承:派生类包含多个基类子对象(可能有多个vptr)
- 虚继承:更复杂,引入虚基类指针
-
空类的大小:
cppclass Empty {}; // sizeof(Empty) == 1(不能为0,确保每个对象有唯一地址) class EmptyWithVirtual { virtual void func() {} }; // sizeof(EmptyWithVirtual) == 8(64位系统,只有vptr) -
标准布局类型(Standard Layout Type):
- 没有虚函数
- 所有非静态成员具有相同的访问控制
- 没有引用类型的非静态成员
- 没有虚基类
- 所有非静态数据成员和基类都是标准布局类型
- 满足这些条件的类型可以与C结构体互操作
实际调试技巧:
-
使用调试器查看内存:
bash# GDB示例 (gdb) p obj (gdb) x/8gx &obj # 以16进制查看前8个8字节 (gdb) info vtbl obj # 查看虚函数表 -
使用reinterpret_cast查看原始内存:
cppvoid* 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]); } -
使用offsetof宏:
cpp#include <cstddef> std::cout << "成员偏移: " << offsetof(ClassName, memberName) << std::endl;
性能影响:
- 缓存友好性:连续存储的小对象更可能在同一缓存行
- 虚假共享:不同线程访问同一缓存行的不同部分会导致性能下降
- 内存访问模式:顺序访问比随机访问快,考虑数据布局
- 对象大小:小于64字节的对象通常更适合栈分配
通过深入理解内存布局,你可以:
- 优化数据访问模式,提高缓存命中率
- 减少内存碎片和填充字节
- 理解多态的实现机制
- 调试内存相关的问题
- 设计更高效的类结构
记住:C++对象的内存布局是由编译器根据语言规则和平台ABI决定的,理解这些规则有助于编写更高效、更可预测的代码。
7.3.9 总结:对象如何定位函数
回答用户的核心问题:"对象是怎么定位函数的呢?"
-
非虚函数:
- 函数代码存储在代码段,所有对象共享
- 编译时确定函数地址
- 调用时,编译器自动插入对象的地址作为this指针
- 函数内部通过this指针访问对象的数据成员
-
虚函数:
- 每个对象包含一个虚函数表指针(vptr)
- 虚函数表存储虚函数的地址
- 调用时,通过vptr找到虚函数表,再找到函数地址
- 支持运行时多态
-
静态函数:
- 没有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++对象的本质理解
-
对象是变量:在C++中,对象本质上就是一块内存空间,它既存储数据,也隐含着对这些数据进行处理的操作(通过成员函数)。
-
this指针的魔法:
cpp// 编译器视角:成员函数调用被转换为普通函数调用 stash.add(&element); // 实际调用:StashClass::add(&stash, &element) // this指针在add()函数中隐式可用 -
构造函数的作用:
- 不是"创建"对象(内存已分配),而是"初始化"对象
- 确保对象在创建后处于有效状态
- 实现RAII(资源获取即初始化)
-
析构函数的作用:
- 不是"销毁"对象(内存将释放),而是"清理"对象
- 确保资源正确释放
- 实现异常安全
7.5 现代C++中的对象演进
-
移动语义(C++11):
cppStashClass a(10); StashClass b = std::move(a); // 移动构造,避免深拷贝 -
智能指针:
cppstd::unique_ptr<StashClass> stash = std::make_unique<StashClass>(10); // 自动管理生命周期,无需手动delete -
值语义 vs 引用语义:
- C++默认使用值语义(对象拷贝)
- 可通过引用、指针实现引用语义
- 现代C++鼓励值语义 + 移动语义
7.6 对象设计的最佳实践
- 遵循RAII原则:资源在构造函数中获取,在析构函数中释放
- 优先使用栈对象:自动管理生命周期,避免内存泄漏
- 明确所有权语义 :使用
unique_ptr、shared_ptr明确资源所有权 - 支持移动语义:对于可移动的资源,实现移动构造函数和移动赋值运算符
- 保持对象不变式:确保对象在任何公开方法调用前后都处于有效状态
7.8 对象的大小与设计原则
7.8.1 对象应当多大?
在C++中,对象的大小是一个重要的设计考虑因素。对象大小直接影响内存使用效率、缓存局部性和性能。
对象大小的决定因素:
- 数据成员大小:所有非静态数据成员的总和
- 内存对齐:编译器为优化内存访问而添加的填充字节
- 虚函数表指针:如果类有虚函数,会增加一个指针大小(通常8字节)
- 继承开销:基类子对象的大小
示例:计算对象大小
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;
}
对象大小的指导原则:
-
小对象优先(通常小于64字节):
- 适合栈分配,避免堆分配开销
- 更好的缓存局部性
- 适合值语义和频繁拷贝
-
中等对象(64字节到1KB):
- 根据使用场景选择栈或堆分配
- 考虑使用智能指针管理生命周期
-
大对象(大于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 对象设计的经验法则
- 像值一样思考 :设计类时,思考它是否应该像
int、double一样工作 - 像资源管理器一样思考:管理资源的类应该遵循RAII原则
- 像抽象数据类型一样思考:提供清晰的接口,隐藏实现细节
- 像服务提供者一样思考:提供特定功能,不暴露不必要的信息
对象大小的经验法则:
- 如果对象小于缓存行(通常64字节),优先考虑栈分配
- 如果对象包含大块数据,使用指针或智能指针间接存储
- 避免"胖接口"类,保持对象职责单一
- 考虑使用Pimpl惯用法隐藏大对象实现细节
cpp
// Pimpl惯用法示例:隐藏大对象实现
class BigObjectImpl; // 前向声明
class BigObject {
std::unique_ptr<BigObjectImpl> pImpl;
public:
BigObject();
~BigObject();
// 接口方法...
void doSomething();
};
7.8.4 实际案例分析:Stash类的设计演进
回顾我们的Stash示例,可以看到对象设计的演进:
- C风格:数据与操作分离,对象只是被动数据结构
- C++ struct风格:数据与操作初步结合,但缺乏封装
- 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管理 - 值语义:支持移动,禁止不必要的拷贝
- RAII :
std::vector自动管理内存 - 最小接口:提供必要的操作,隐藏实现细节
- 异常安全:使用异常报告错误
- STL兼容:提供迭代器支持
7.8.5 总结:优秀C++对象的特征
- 大小适中:通常小于缓存行,或使用间接存储管理大内存
- 行为明确:像值、像资源管理器、或像服务提供者
- 接口简洁:提供最小必要接口,隐藏实现细节
- 资源安全:遵循RAII原则,自动管理资源
- 异常安全:提供基本或强异常安全保证
- 可移动:支持移动语义,避免不必要的深拷贝
- 可测试:依赖注入,便于单元测试
- 符合直觉:使用起来像内置类型或标准库组件
记住:好的C++对象应该让使用者感到自然、安全、高效,就像使用语言内置类型一样。
7.7 C++中struct与class的异同与选择
在C++中,struct和class是两种非常相似但又有关键区别的语法结构。理解它们的异同对于编写清晰、可维护的代码至关重要。
7.7.1 基本语法对比
相同点:
- 都可以包含数据成员和成员函数
- 都支持构造函数、析构函数、拷贝控制
- 都支持继承和多态
- 都支持访问控制(public/protected/private)
- 都支持模板和运算符重载
语法示例:
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. 默认访问权限
这是struct和class最核心的区别:
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?
-
简单的数据聚合
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; }; -
C兼容性要求
cpp// 需要与C代码交互时 #ifdef __cplusplus extern "C" { #endif struct CCompatible { int id; char name[32]; }; #ifdef __cplusplus } #endif -
值类型(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; } }; -
模板元编程中的类型特征
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?
-
需要严格封装的复杂类型
cppclass 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; } }; -
需要实现多态的类层次结构
cppclass 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; } }; -
资源管理类(RAII)
cppclass 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); } }; -
需要复杂不变式维护的类
cppclass 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 实际代码中的混合使用
在实际项目中,经常根据语义混合使用struct和class:
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), "对齐应该相同");
访问权限不影响性能:
public、private、protected只是编译期检查- 运行时没有任何性能差异
- 内联函数同样有效
7.7.6 现代C++中的趋势
-
统一初始化语法(C++11起):
cppstruct 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 - 使用构造函数 -
结构化绑定(C++17):
cppstruct Point { int x, y; }; Point get_point() { return {10, 20}; } auto [x, y] = get_point(); // x=10, y=20 -
三路比较运算符(C++20):
cppstruct 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兼容性 | 更好 | 较差 |
| 模板元编程 | 常用 | 较少用 |
选择指南:
-
使用struct当:
- 只有公有数据成员
- 没有或很少成员函数
- 需要与C代码交互
- 作为简单的值类型(如Point、Color、Rect)
- 在模板元编程中定义类型特征
-
使用class当:
- 有私有或保护成员
- 需要封装复杂的不变式
- 管理资源(文件、内存、网络连接)
- 需要多态(虚函数)
- 有复杂的构造函数/析构函数逻辑
-
一致性原则:
- 在同一个项目中保持一致的约定
- 如果团队已有规范,遵循团队规范
- 公开API考虑使用者的期望
最终建议:
- 语义优先 :用
struct表示"这是一个数据结构",用class表示"这是一个有行为的对象" - 明确性优先 :即使使用
struct,如果某些成员应该是私有的,显式使用private: - 一致性优先:在整个代码库中保持一致的风格
记住:在C++中,struct和class的技术差异很小(主要是默认访问权限),选择哪一个更多是基于代码的语义和可读性考虑。
7.7 从C到C++对象思维的转变
从C的"数据+函数"分离到C++的"对象"封装,不仅仅是语法变化,更是思维方式的转变:
- 从操作数据到操作对象:C程序员思考"如何操作这块内存",C++程序员思考"这个对象能做什么"
- 从手动管理到自动管理:C程序员手动管理生命周期,C++程序员依赖构造函数/析构函数
- 从全局状态到对象状态:C使用全局变量和静态变量,C++将状态封装在对象内部
- 从函数接口到对象接口:C提供函数库,C++提供类库,接口更自然、更安全
这种思维转变使得代码更加模块化、可维护,是现代软件工程的重要基础。
总结
从C结构体到C++对象的转变,不仅仅是语法上的变化,更是编程范式的转变:
- 从过程式到面向对象:将数据和操作封装在一起
- 从手动管理到自动管理:利用构造函数/析构函数实现RAII
- 从松散耦合到紧密封装:通过访问控制保护数据完整性
- 从函数调用到方法调用:更自然的对象操作语法
这种演变使得代码更加安全、易维护和可扩展,是现代C++编程的重要基础。
注:本文示例代码基于Bruce Eckel的《Thinking in C++》中的Stash示例,进行了适当简化和修正。