1. 结构体(struct)——C++中的复合数据类型
在C++编程中,结构体(struct)是一种将不同类型的数据组合成一个整体的复合数据类型。它起源于C语言,但在C++中得到了扩展和增强。结构体特别适合用来表示那些逻辑上相关但类型不同的数据集合。
1.1 结构体的定义与使用
结构体的基本语法如下:
cpp复制struct 结构体名 {
数据类型 成员1;
数据类型 成员2;
// ...
};
在实际开发中,结构体常用于以下场景:
- 表示一个实体的多个属性(如学生信息)
- 构建链表、树等数据结构节点
- 函数需要返回多个值时(替代多个输出参数)
一个典型的学生结构体示例:
cpp复制struct Student {
string name; // 姓名
int age; // 年龄
double score; // 分数
// 构造函数
Student() : name(""), age(0), score(0.0) {}
Student(string n, int a, double s) : name(n), age(a), score(s) {}
// 成员函数
void printInfo() {
cout << "姓名:" << name << " 年龄:" << age << " 分数:" << score << endl;
}
};
1.2 结构体的内存布局与对齐
结构体在内存中的存储遵循成员声明顺序,但需要考虑内存对齐问题。内存对齐是编译器为了提高访问效率而采取的一种优化策略。
考虑以下结构体:
cpp复制struct Example {
char c; // 1字节
int i; // 4字节
double d; // 8字节
};
在64位系统上,这个结构体实际占用的内存可能是16字节而非13字节(1+4+8),因为:
- char c占用1字节
- int i需要4字节对齐,所以会在c后填充3字节空白
- double d需要8字节对齐,正好接在i后面
提示:使用
#pragma pack(n)可以改变默认对齐方式,但可能影响性能。
1.3 结构体的高级用法
现代C++中,结构体可以拥有几乎所有类的特性:
- 成员函数:结构体可以定义方法
- 构造函数/析构函数:支持各种构造方式
- 运算符重载:可以重载+、-等运算符
- 继承:结构体可以继承其他结构体或类
cpp复制struct Vector {
float x, y;
Vector operator+(const Vector& other) {
return {x + other.x, y + other.y};
}
};
2. 类(class)——面向对象的核心
类是C++面向对象编程的基石,它将数据和对数据的操作封装在一起,实现了信息隐藏和抽象。
2.1 类的三大特性
- 封装:隐藏实现细节,暴露接口
- 继承:实现代码复用和层次化设计
- 多态:通过虚函数实现运行时绑定
2.2 类的定义与实现
一个完整的类通常包含以下部分:
cpp复制class Person {
private: // 私有成员,仅类内可访问
string name;
int age;
public: // 公有接口,外部可访问
// 构造函数
Person() : name(""), age(0) {}
Person(string n, int a) : name(n), age(a) {}
// 成员函数
void setName(string n) { name = n; }
string getName() const { return name; }
// 析构函数
~Person() { cout << "Person destroyed" << endl; }
};
2.3 构造函数详解
构造函数是类对象创建时自动调用的特殊成员函数,有以下特点:
- 与类同名
- 无返回类型
- 可以重载
- 可以使用初始化列表
初始化列表 vs 赋值初始化:
cpp复制// 初始化列表方式(推荐)
Person(string n, int a) : name(n), age(a) {}
// 赋值方式
Person(string n, int a) {
name = n;
age = a;
}
初始化列表方式效率更高,特别是对于:
- const成员
- 引用成员
- 没有默认构造函数的类成员
2.4 拷贝控制:拷贝构造与赋值
拷贝构造函数在以下情况被调用:
- 用一个对象初始化另一个对象
- 函数参数值传递
- 函数返回对象
浅拷贝问题:
cpp复制class String {
char* data;
public:
String(const char* str) {
data = new char[strlen(str)+1];
strcpy(data, str);
}
~String() { delete[] data; }
// 默认拷贝构造是浅拷贝
};
int main() {
String s1("hello");
String s2 = s1; // 浅拷贝,两个对象指向同一内存
return 0; // 析构时重复释放,崩溃!
}
解决方案:实现深拷贝
cpp复制String(const String& other) {
data = new char[strlen(other.data)+1];
strcpy(data, other.data);
}
3. 结构体与类的核心区别
虽然结构体和类在功能上几乎相同,但存在以下关键差异:
| 特性 | 类(class) | 结构体(struct) |
|---|---|---|
| 默认访问权限 | private | public |
| 继承默认权限 | private | public |
| 设计理念 | 强调封装和行为 | 强调数据聚合 |
| 典型用途 | 复杂对象模型 | 简单数据容器 |
实际应用建议:
- 当需要数据聚合且不需要复杂操作时,使用struct
- 当需要封装复杂行为时,使用class
- 在模板元编程中,struct更常用(因为默认public)
4. 实战经验与避坑指南
4.1 构造函数最佳实践
-
提供完整的构造系列:
- 默认构造函数
- 参数化构造函数
- 拷贝构造函数
- 移动构造函数(C++11)
-
使用委托构造函数(C++11):
cpp复制class Circle {
double radius;
public:
Circle() : Circle(1.0) {} // 委托给下面的构造函数
Circle(double r) : radius(r) {}
};
4.2 资源管理要点
-
RAII原则:资源获取即初始化
- 在构造函数中获取资源
- 在析构函数中释放资源
-
三/五法则:
- 如果需要自定义析构函数,通常也需要自定义拷贝构造和拷贝赋值
- C++11后扩展为五法则(加上移动构造和移动赋值)
4.3 常见错误排查
-
未初始化成员变量:
- 解决方案:总是初始化所有成员
- 可以使用
{}统一初始化:int x{};(初始化为0)
-
虚析构函数遗漏:
- 基类必须有虚析构函数,否则通过基类指针删除派生类对象会导致未定义行为
-
const正确性:
- 不会修改成员变量的函数应该声明为const
- const对象只能调用const成员函数
5. 现代C++中的改进
5.1 默认和删除函数
cpp复制class MyClass {
public:
MyClass() = default; // 显式要求编译器生成默认构造
MyClass(const MyClass&) = delete; // 禁止拷贝
};
5.2 移动语义
C++11引入了移动构造和移动赋值,避免不必要的拷贝:
cpp复制class Buffer {
int* data;
size_t size;
public:
// 移动构造函数
Buffer(Buffer&& other) noexcept
: data(other.data), size(other.size) {
other.data = nullptr;
other.size = 0;
}
};
5.3 结构化绑定(C++17)
方便地从结构体/类中提取成员:
cpp复制struct Point { double x, y; };
Point p{1.0, 2.0};
auto [x, y] = p; // x=1.0, y=2.0
在实际工程中,结构体和类的选择往往取决于团队的编码规范。Google C++风格指南建议:
- 仅当只有数据成员时使用struct
- 有私有成员或成员函数时使用class
理解这些基础概念对于掌握C++面向对象编程至关重要,也是面试中经常考察的重点内容。