封装继承多态
C++ 面向对象的核心是通过封装实现数据安全,继承实现代码复用,多态实现接口灵活。
- 类与对象:
- 构造函数/析构函数、拷贝构造函数、赋值运算符。
this指针、常量成员函数、友元函数。- 静态成员(
static)、单例模式。
- 继承与多态:
- 公有/私有继承、虚函数、虚析构函数。
- 抽象类、纯虚函数。
1. 类与对象(Class & Object)
-
C++ 核心特性:封装数据和行为。 -
public:外部可直接访问(暴露的接口)。 -private:仅类内部可访问(隐藏的实现细节)。 -protected:类内部和子类可访问(用于继承场景)。 -
🧱 C++ 的六大特殊成员函数(Special Member Functions)
| 函数 | 作用 |
|---|---|
| 1. 默认构造函数 | T() —— 创建对象 |
| 2. 析构函数 | ~T() —— 销毁对象 |
| 3. 拷贝构造函数 | T(const T&) —— 从左值拷贝 |
| 4. 拷贝赋值运算符 | T& operator=(const T&) —— 左值赋值 |
| 5. 移动构造函数 | T(T&&) —— 从右值移动 |
| 6. 移动赋值运算符 | T& operator=(T&&) —— 右值赋值 |
✅ 这六个函数是 C++ 对象生命周期管理的核心。
2. 构造函数与析构函数
好的,我们来深入详解 C++ 中的构造函数(Constructor) 和析构函数(Destructor)。它们是类(class)和结构体(struct)的特殊成员函数,负责对象的生命周期管理:构造函数在对象创建时初始化资源,析构函数在对象销毁时清理资源。正确理解和使用它们是编写安全、高效 C++ 程序的基础。
初始化方法
C++ 的初始化方法多种多样,不同语法在不同上下文中会触发不同的构造函数或赋值操作。理解这些机制是掌握现代 C++ 的关键。
我们来系统性地详解 C++ 中的各种初始化方式,包括它们的语法、语义、调用的构造函数类型,以及适用场景。
📚 C++ 初始化的 5 种主要方式
| 初始化方式 | 语法示例 | 名称 |
|---|---|---|
| 1. 直接初始化 | T obj(arg); | Direct Initialization |
| 2. 拷贝初始化 | T obj = arg; | Copy Initialization |
| 3. 列表初始化 | T obj{arg}; 或 T obj = {arg}; | List Initialization (C++11) |
| 4. 值初始化 | T obj{}; 或 T(); | Value Initialization |
| 5. 聚合初始化 | struct S {int x; int y;} s{1,2}; | Aggregate Initialization |
1️⃣ 直接初始化(Direct Initialization)
T obj(arg1, arg2, ...);
T obj{T1{}, T2{}}; // 也可用花括号- 调用能匹配参数的构造函数
- 不进行隐式转换(除非构造函数允许)
- 最“直接”的初始化方式
class MyClass {
public:
MyClass(int x) { /* 构造 */ }
MyClass(int x, int y) { }
};
MyClass a(5); // 调用 MyClass(int)
MyClass b(1, 2); // 调用 MyClass(int, int)
MyClass c{1, 2}; // 也是直接初始化!✅
{}在构造函数调用中也属于直接初始化,不是列表初始化(见下文)。
2️⃣ 拷贝初始化(Copy Initialization)
T obj = arg;- 看起来像赋值,其实是初始化
- 编译器会尝试将
arg转换为T类型 - 可能涉及:
- 一个转换构造函数
- 或一个拷贝/移动构造函数
class Widget {
public:
Widget(int x); // 转换构造函数
Widget(const Widget&); // 拷贝构造
Widget(Widget&&); // 移动构造
};
Widget w = 5; // ✅ 合法:等价于 Widget w(5);
// 先用 5 构造临时对象,再移动构造 w(C++11 起通常被优化掉)
Widget w = {5}; // 仍然是拷贝初始化,但使用列表3️⃣ 列表初始化(List Initialization)—— C++11 引入
这是最复杂但也最强大的初始化方式。
T obj{arg1, arg2, ...}; // 直接列表初始化
T obj = {arg1, arg2, ...}; // 拷贝列表初始化🔹 优先级规则:
编译器按以下顺序尝试匹配:
- 是否有
std::initializer_list<T>构造函数? - 是否有能接受这些参数的普通构造函数?
- 是否是聚合类型?→ 聚合初始化
✅ 情况 1:有 initializer_list 构造函数
class Vec {
public:
Vec(std::initializer_list<int> il) {
// 初始化列表构造
}
};
Vec v{1, 2, 3}; // 调用 initializer_list 构造函数✅ 情况 2:无 initializer_list,但有匹配构造函数
class Point {
public:
Point(int x, int y) {}
};
Point p{1, 2}; // 调用 Point(int, int)
// 不是 initializer_list,因为没有定义✅ 情况 3:聚合类型(POD 结构体)
struct Color {
int r, g, b;
};
Color c{255, 0, 0}; // 聚合初始化:r=255, g=0, b=0C++20 起支持 = default 成员、静态成员等更复杂的聚合。
⚠️ 拷贝列表初始化 Vs 直接列表初始化
std::vector<int> v1{1, 2, 3}; // 直接:调用 initializer_list 构造
std::vector<int> v2 = {1, 2, 3}; // 拷贝列表初始化两者效果相同,但 v2 语法上是“拷贝初始化”,不过通常被优化。
4️⃣ 值初始化(Value Initialization)
T obj{}; // 推荐
T obj = T(); // 老式写法- 如果
T有默认构造函数 → 调用它 - 如果没有 → 对所有成员进行零初始化(zero-initialization)
int x{}; // x = 0
std::string s{}; // 调用 string 的默认构造函数
MyClass m{}; // 调用 MyClass()✅ 推荐使用
{}进行值初始化,避免“未初始化”错误。
5️⃣ 聚合初始化(Aggregate Initialization)
适用于聚合类型(aggregate types):
- 普通旧数据(POD)结构体/类
- 无用户定义构造函数
- 无私有/保护非静态成员
- 无基类
- 无虚函数
struct S { int x; double y; };
S s{1, 2.5}; // x=1, y=2.5C++20 起支持更宽松的聚合定义。
🧠 特殊情况:何时调用移动/拷贝构造函数?
| 代码 | 调用的构造函数 |
|---|---|
T a = b; | 拷贝构造(如果 b 是左值) |
T a = std::move(b); | 移动构造 |
T a{std::move(b)}; | 移动构造(直接初始化) |
T a = {1,2}; | 列表构造或聚合初始化 |
T a = T(5); | 移动构造(C++11 起通常被省略) |
💡 复制省略(Copy Elision) 和 NRVO 在现代 C++ 中非常普遍,即使写了拷贝/移动,也可能被优化掉。
📌 总结表:不同初始化方式对比
| 初始化方式 | 语法 | 调用的构造函数 | 是否允许窄化 | 是否允许隐式转换 |
|---|---|---|---|---|
| 直接初始化 | T obj(arg) 或 T obj{arg} | 匹配的构造函数 | ❌ 不允许({}) | ✅ 允许(如果构造函数接受) |
| 拷贝初始化 | T obj = arg | 转换构造 + 拷贝/移动 | ❌ 不允许(= + {}) | ✅ 允许 |
| 列表初始化 | T obj{a,b} | initializer_list 或普通构造 | ❌ 不允许 | ✅ 但受限 |
| 值初始化 | T obj{} | 默认构造或零初始化 | N/A | N/A |
| 聚合初始化 | struct {int x;} s{1}; | 无(直接赋值) | ❌ 不允许 | ✅ 基本类型可转换 |
✅ 最佳实践建议
-
优先使用
{}初始化(统一初始化)- 防止 Most Vexing Parse
- 禁止窄化转换
- 更安全
-
避免
=拷贝初始化,除非你明确需要隐式转换 -
移动语义设计:
class Holder { Resource r; public: explicit Holder(Resource r) : r(std::move(r)) {} // 按值传递 + 移动 }; -
聚合类型用
{}初始化 -
POD 类型用
{}进行值初始化,避免未定义行为
💡 额外提示:C++17 起的类模板参数推导(CTAD)
std::pair p{1, "hello"}; // C++17 自动推导为 std::pair<int, const char*>
std::thread t{my_func}; // 无需写 std::thread<>这也依赖于列表初始化机制。
✅ 结论
C++ 的初始化机制虽然复杂,但核心原则是:
用
{}初始化一切,除非有特殊原因。
它能:
- 避免 Most Vexing Parse
- 防止窄化转换
- 支持聚合、列表、直接初始化
- 完美配合移动语义
构造函数 (Constructor)
构造函数在对象被创建时自动调用,用于初始化对象的成员变量和执行必要的设置工作。
核心特点
- 名称: 与类名完全相同。
- 无返回类型: 连
void都不能写。 - 自动调用: 创建对象时由编译器自动调用,不能显式调用。
- 可重载: 一个类可以有多个构造函数,通过参数列表区分(重载)。
- 可以是
explicit: 防止隐式类型转换。
构造函数的类型
-
默认构造函数 (Default Constructor)
- 定义: 不接受任何参数,或者所有参数都有默认值的构造函数。
- 作用: 当用
T t;或new T;这种不带参数的方式创建对象时调用。 - 自动生成: 如果类中没有定义任何构造函数,编译器会自动生成一个默认的默认构造函数。这个默认构造函数会调用成员变量的默认构造函数(对于类类型)或进行值初始化(对于内置类型,在某些情况下可能不初始化!)。
- 重要: 一旦你定义了任何构造函数(包括带参数的),编译器就不再生成默认构造函数。如果你还需要默认构造函数,必须显式定义。
class MyClass { public: int x; std::string s; // 编译器生成的默认构造函数 (隐式) // 效果: x 的值未定义 (垃圾值), s 被默认构造 (空字符串) }; class MyClass2 { public: int x; std::string s; MyClass2(int val) : x(val) {} // 定义了一个构造函数 // MyClass2() = default; // 必须显式定义,否则 MyClass2 obj; 会编译错误 }; -
带参数的构造函数 (Parameterized Constructor)
- 定义: 接受一个或多个参数的构造函数。
- 作用: 使用提供的参数来初始化对象。
- 成员初始化列表 (Member Initializer List): 强烈推荐使用成员初始化列表来初始化成员变量,而不是在构造函数体内赋值。初始化列表在进入构造函数体之前执行,对于类类型的成员(如
std::string,std::vector),这避免了一次不必要的默认构造和一次赋值操作,效率更高。
class Point { private: double x, y; public: // 推荐使用初始化列表 Point(double x_val, double y_val) : x(x_val), y(y_val) { // 构造函数体 (可选) } // 不推荐 (效率较低,尤其对类类型成员) // Point(double x_val, double y_val) { // x = x_val; // 先默认构造 x (可能是0),再赋值 // y = y_val; // } }; -
拷贝构造函数 (Copy Constructor)
- 定义: 以同类型对象的 const 引用作为唯一参数的构造函数。
- 作用: 当用一个已存在的对象来初始化一个新对象时调用。
- 调用场景:
T t1 = t2;或T t1(t2);- 函数参数按值传递 (
void func(T obj); func(t);) - 函数返回对象按值返回 (
T func() { T t; return t; })
- 自动生成: 如果没有定义,编译器会生成一个逐成员拷贝(Memberwise Copy)的拷贝构造函数。对于包含指针的类,这会导致浅拷贝(Shallow Copy),通常是错误的(两个对象的指针指向同一块内存)。此时必须自定义拷贝构造函数实现深拷贝(Deep Copy)。
class String { private: char* data; size_t length; public: // 自定义拷贝构造函数 (深拷贝) String(const String& other) : length(other.length) { data = new char[length + 1]; std::strcpy(data, other.data); } // ... 其他成员 ... }; -
移动构造函数 (Move Constructor) - C++11
- 定义: 以同类型对象的右值引用作为参数的构造函数。
- 作用: ” 窃取 ” 临时对象(右值)的资源,避免昂贵的拷贝。是实现移动语义(Move Semantics) 的关键。
- 调用场景: 用临时对象(如函数返回值、
std::move()的结果)初始化新对象时。 - 自动生成: 如果没有定义拷贝构造、拷贝赋值、移动赋值、析构函数中的任何一个,且拷贝构造函数有意义,编译器可能生成。但通常建议显式定义。
class String { public: // 移动构造函数 String(String&& other) noexcept // noexcept 表示不抛异常,很重要 : data(other.data), length(other.length) { other.data = nullptr; // "窃取"后,源对象置空 other.length = 0; } // ... 其他成员 ... }; String createString() { String s("Hello"); return s; // 返回时,可能调用移动构造函数 (RVO/NRVO 优化可能避免) } String s1 = createString(); // 很可能调用移动构造,而非拷贝构造 -
转换构造函数 (Conversion Constructor)
- 定义: 只接受一个参数(或多个参数但其余都有默认值)的构造函数。
- 作用: 允许从参数类型隐式转换为类类型。
explicit关键字: 用explicit修饰可以禁止这种隐式转换,防止意外的类型转换,提高类型安全。
class MyInt { public: // explicit MyInt(int val); // 禁止隐式转换 MyInt(int val) : value(val) {} // 允许隐式转换 private: int value; }; MyInt a = 10; // 隐式转换: int -> MyInt (如果构造函数不是 explicit) MyInt b(20); // 直接初始化 // MyInt c = "hello"; // 错误,没有从 const char* 到 MyInt 的转换
析构函数 (Destructor)
析构函数在对象生命周期结束时自动调用,用于释放对象占用的资源(如动态内存、文件句柄、网络连接等)。
核心特点
- 名称: 在类名前加波浪号
~。 - 无返回类型,无参数: 不能重载。
- 自动调用: 对象销毁时由编译器自动调用,不能显式调用(除了极少数特殊情况,如 placement new)。
- 只有一个: 每个类最多只能有一个析构函数。
- 调用顺序: 对于类对象,析构函数的执行顺序与构造函数相反。先构造的后析构。
析构函数的作用
-
资源清理: 这是析构函数最主要的任务。
-
RAII (Resource Acquisition Is Initialization): C++ 中管理资源的核心技术。资源的获取在构造函数中完成,资源的释放则在析构函数中完成。只要对象在作用域内,资源就有效;对象离开作用域,析构函数自动调用,资源自动释放。这极大地简化了资源管理,避免了内存泄漏。
class FileHandler { private: FILE* file; public: FileHandler(const char* filename) { file = std::fopen(filename, "r"); if (!file) throw std::runtime_error("Cannot open file"); } ~FileHandler() { // RAII: 析构时自动关闭文件 if (file) { std::fclose(file); file = nullptr; } } // ... 其他成员 ... }; // file 在这里自动关闭,即使函数因异常退出 void processFile() { FileHandler fh("data.txt"); // 构造: 打开文件 // ... 处理文件 ... // fh 离开作用域,析构函数自动调用,文件关闭 } // 即使这里发生异常,C++ 的栈展开机制也会调用 fh 的析构函数
自动生成的析构函数
-
如果类中没有定义析构函数,编译器会自动生成一个默认的析构函数。
-
默认析构函数的行为:
- 按声明顺序的逆序调用所有非静态成员变量的析构函数。
- 调用所有直接基类的析构函数。
-
关键点: 默认析构函数不会释放动态分配的内存! 如果你的类拥有通过
new分配的指针,你必须显式定义析构函数来delete它。class BadClass { int* ptr; public: BadClass() { ptr = new int(42); } // 没有析构函数!会导致内存泄漏 }; // ptr 指向的内存永远不会被释放 class GoodClass { int* ptr; public: GoodClass() { ptr = new int(42); } ~GoodClass() { delete ptr; } // 显式释放 // ... 还需要定义拷贝构造、拷贝赋值等 (Rule of Three/Five) ... };
特殊规则
Rule Of Three (C++98/03)/ Rule of Five(C++11 起)
-
Rule of Three (三法则): 如果一个类需要显式定义以下三个函数中的任何一个,那么它通常也需要显式定义另外两个:
- 析构函数 (Destructor)
- 拷贝构造函数 (Copy Constructor)
- 拷贝赋值运算符 (Copy Assignment Operator)
- 原因: 这通常意味着类管理了需要特殊处理的资源(如裸指针)。默认的拷贝是浅拷贝,会导致多个对象管理同一块资源,析构时发生双重释放(Double Free)错误。
-
Rule of Five (五法则) - C++11: 在 Rule of Three 的基础上,增加了两个与移动语义相关的函数:
- 移动构造函数 (Move Constructor)
- 移动赋值运算符 (Move Assignment Operator)
- 原因: 为了充分利用 C++11 的移动语义,提高性能。如果定义了析构函数或拷贝操作,通常也需要定义移动操作,或者显式
= delete来禁用移动(如果移动没有意义或不安全)。
编译器会在首次需要时自动生成六大函数(如果未被删除或私有化)。
但有一个重要前提:只有当你没有显式定义某些函数时,编译器才会生成对应的默认函数。
🔧 = default 和 = delete 的作用
✅ = default
- 显式要求编译器生成默认版本
- 通常用于你定义了其他构造函数,但仍希望默认构造函数存在
- 可以在类内或类外写
class MyClass {
public:
MyClass() = default; // 让编译器生成默认构造
};
✅ = delete
- 显式禁止某个函数被调用
- 用于禁用拷贝、移动等操作
- 任何尝试调用都会导致编译错误
class NonCopyable {
NonCopyable(const NonCopyable&) = delete;
NonCopyable& operator=(const NonCopyable&) = delete;
};
所有构造/拷贝/移动/赋值方法汇总
| 方法 | 语法 | 触发条件 | 说明 |
|---|---|---|---|
| 默认构造 | T() 或 T{} | 对象定义无参数 | 若未定义构造函数,编译器生成 |
| 值初始化 | T{} | 希望零初始化 | 调用默认构造或零初始化 |
| 直接初始化 | T(arg) | 显式传参 | 调用匹配构造函数 |
| 拷贝构造 | T(const T&) | T b = a; 或传值 | 复制左值对象 |
| 移动构造 | T(T&&) | T b = std::move(a); | “窃取”右值资源 |
| 拷贝赋值 | T& operator=(const T&) | a = b;(b 是左值) | 赋值已有对象 |
| 移动赋值 | T& operator=(T&&) | a = std::move(b); | 转移资源到已有对象 |
掌握要点:
- 理解生命周期: 构造函数和析构函数是对象生命周期的起点和终点。
- 善用初始化列表: 提高效率,尤其对类类型成员。
- 理解拷贝与移动: 区分浅拷贝和深拷贝,理解移动语义的优势。
- 遵循 RAII: 用构造函数获取资源,用析构函数释放资源。
- 牢记 Rule of Three/Five: 当管理资源时,必须同时考虑拷贝、移动和析构的语义一致性。
- 优先使用标准库: 尽量使用
std::string,std::vector,std::unique_ptr等 RAII 类,它们已经正确实现了这些规则,可以避免手动管理资源的复杂性。
| 场景 | 建议 |
|---|---|
| 普通数据类 | 让编译器自动生成所有函数 |
RAII 资源管理类(如 thread_guard) | 禁用拷贝,启用移动 |
| 不希望被拷贝的类 | = delete 拷贝构造和赋值 |
| 希望有默认构造 | 显式写 T() = default; |
| 实现深拷贝 | 显式写拷贝构造和赋值 |
| 移动语义优化 | = default 移动构造和赋值 |
3. 继承
基本概念
- 基类(Base Class):被继承的类,也称为父类或超类。
- 派生类(Derived Class):继承基类的类,也称为子类或派生类。
- 继承关系:派生类自动获得基类的成员(变量和函数),并可以添加新成员或重写基类成员。
C++ 通过 class 子类 : 继承方式 父类 语法实现继承,继承方式(public/private/protected)控制父类成员在子类中的访问权限(默认 private)。
// 父类:通用动物
class Animal {
protected: // 受保护成员:子类可访问
string name;
public:
Animal(string n) : name(n) {}
void eat() { // 通用行为
cout << name << " is eating." << endl;
}
virtual void makeSound() { // 虚函数:允许子类重写
cout << name << " makes a sound." << endl;
}
};
// 子类:狗(继承自动物)
class Dog : public Animal {
public:
// 继承父类构造函数
Dog(string n) : Animal(n) {}
// 重写父类方法(实现狗的特有行为)
void makeSound() override {
cout << name << " barks: Woof!" << endl;
}
// 新增子类特有方法
void fetch() {
cout << name << " is fetching the ball." << endl;
}
};二、继承方式(访问控制)
C++ 通过继承方式控制基类成员在派生类中的访问权限,有三种继承方式:
| 继承方式 | 基类 public 成员 | 基类 protected 成员 | 基类 private 成员 |
|---|---|---|---|
| public | 派生类中为 public | 派生类中为 protected | 不可访问 |
| protected | 派生类中为 protected | 派生类中为 protected | 不可访问 |
| private | 派生类中为 private | 派生类中为 private | 不可访问 |
说明:
-
public继承:最常用,基类的public成员在派生类中仍为public,protected仍为protected(体现 “接口继承”)。 -
protected/private继承:基类成员在派生类中权限被降低(通常用于 “实现继承”,隐藏基类接口)。 -
基类的
private成员任何时候都不能被派生类直接访问(需通过基类的public/protected成员间接访问)。
示例:
cpp
运行
class Base {
public:
int pub;
protected:
int pro;
private:
int pri;
};
// public 继承
class Derived : public Base {
public:
void func() {
pub = 1; // 可访问(public)
pro = 2; // 可访问(protected)
// pri = 3; // 错误:private 成员不可访问
}
};
int main() {
Derived d;
d.pub = 10; // 可访问(public)
// d.pro = 20; // 错误:protected 成员类外不可访问
return 0;
}三、派生类的构造与析构
派生类对象的创建和销毁需要遵循特定规则:
- 构造函数:
- 派生类构造函数必须先调用基类构造函数(初始化基类部分)。
- 若基类无默认构造函数,派生类必须在初始化列表中显式调用基类的带参构造函数。
- 析构函数:
- 析构函数执行顺序与构造相反:先调用派生类析构,再调用基类析构。
- 基类析构函数必须声明为虚函数(
virtual),否则删除派生类对象时可能导致基类部分内存泄漏。
四、函数重写(覆盖)与虚函数
派生类可以重写基类的函数,实现多态:
-
虚函数(
virtual):- 基类中用
virtual声明的函数,允许派生类重写。 - 通过基类指针 / 引用调用时,会根据实际对象类型调用对应的派生类函数(动态多态)。
- 基类中用
-
重写规则:
- 函数名、参数列表、返回值类型必须与基类完全一致(协变返回类型除外)。
- 派生类函数前可加
override关键字(C++11),显式声明重写,编译器会检查正确性。
class Base { public: virtual void show() { // 虚函数 cout << "Base::show()" << endl; } }; class Derived : public Base { public: void show() override { // 重写基类函数 cout << "Derived::show()" << endl; } }; int main() { Base* ptr = new Derived(); ptr->show(); // 输出:Derived::show()(多态效果) delete ptr; return 0; } -
纯虚函数与抽象类:
- 纯虚函数:
virtual 返回类型 函数名() = 0;,没有实现,强制派生类重写。 - 包含纯虚函数的类是抽象类,不能实例化,只能作为基类使用。
class Shape { // 抽象类 public: virtual double area() = 0; // 纯虚函数 }; class Circle : public Shape { private: double radius; public: Circle(double r) : radius(r) {} double area() override { // 必须重写 return 3.14 * radius * radius; } }; - 纯虚函数:
五、继承的特殊场景
-
多继承:
- 一个派生类同时继承多个基类(
class D : public B1, public B2 { ... })。 - 可能导致菱形继承问题(多个基类继承自同一间接基类,导致成员冗余)。
- 解决方案:使用虚继承(
class B1 : virtual public A { ... }),确保间接基类只被继承一次。
class A { public: int x; }; class B1 : virtual public A {}; // 虚继承 class B2 : virtual public A {}; // 虚继承 class D : public B1, public B2 {}; // 仅包含一个 A::x int main() { D d; d.x = 10; // 正确:无歧义 return 0; } - 一个派生类同时继承多个基类(
-
继承与访问控制:
- 派生类可以通过
using声明提升基类成员的访问权限(仅对public/protected继承有效)。
class Base { protected: void func() {} }; class Derived : public Base { public: using Base::func; // 将 protected 成员提升为 public }; int main() { Derived d; d.func(); // 正确:已提升为 public return 0; } - 派生类可以通过
4. 多态(Polymorphism):接口复用与动态行为
多态是指同一接口(如父类指针 / 引用)可以表现出不同的行为,具体行为由实际对象类型决定,分为 “编译期多态”(函数重载)和 “运行期多态”(虚函数)。而虚函数(Virtual function) 和抽象类(Abstract Class) 是实现多态的关键技术手段,三者紧密关联:虚函数是多态的基础机制,抽象类则是多态的一种高级应用形式(定义接口规范)。
- 接口统一:用统一的父类接口操作不同子类对象,无需关心具体类型。
- 动态绑定:运行时根据对象实际类型执行对应方法,提高代码灵活性。
一、多态:同一接口,不同实现
多态的核心思想是:用统一的父类接口(如指针或引用)操作不同的子类对象时,程序会根据对象的实际类型执行对应的方法,而不是接口的静态类型。
例如,“动物” 是一个父类,“狗” 和 “猫” 是子类:
// 父类
class Animal {
public:
// 虚函数:关键!为多态提供支持
virtual void makeSound() {
cout << "动物发出声音" << endl;
}
};
// 子类1
class Dog : public Animal {
public:
// 重写父类的虚函数
void makeSound() override {
cout << "狗汪汪叫" << endl;
}
};
// 子类2
class Cat : public Animal {
public:
// 重写父类的虚函数
void makeSound() override {
cout << "猫喵喵叫" << endl;
}
};多态的体现:用父类指针指向不同引用指向子类对象,调用方法时会执行子类的实现:
int main() {
Animal* animal1 = new Dog(); // 父类指针指向Dog对象
Animal* animal2 = new Cat(); // 父类指针指向Cat对象
animal1->makeSound(); // 输出:狗汪汪叫(实际执行Dog的方法)
animal2->makeSound(); // 输出:猫喵喵叫(实际执行Cat的方法)
delete animal1;
delete animal2;
return 0;
}本质:多态通过 “动态绑定” 实现 —— 程序在运行时才确定要调用的具体方法(而非编译期根据指针类型确定)。
二、虚函数:多态的 “开关”
虚函数是被 virtual 关键字修饰的成员函数,它是 C++ 实现多态的基础。其核心作用是:允许子类重写(override)父类的方法,并在运行时根据对象实际类型调用对应的重写版本。
- 父类声明:在父类中用
virtual声明函数(如virtual void makeSound())。 - 子类重写:子类用
override关键字显式重写(C++11 起推荐,确保重写正确),函数签名(返回类型、参数列表)必须与父类完全一致。 - 动态绑定:只有通过父类指针或引用调用虚函数时,才会触发多态(动态绑定);直接用子类对象调用时,行为与普通函数相同。
如果没有 virtual,函数调用会在编译期根据指针类型(而非对象类型)确定,无法实现多态:
class Animal {
public:
void makeSound() { // 非虚函数
cout << "动物发出声音" << endl;
}
};
// …(Dog和Cat类定义同上)
int main() {
Animal* animal = new Dog();
animal->makeSound(); // 输出:动物发出声音(编译期绑定,调用父类方法)
return 0;
}三、抽象类:多态的 “接口规范”
抽象类是包含纯虚函数(Pure Virtual Function)的类,它不能实例化对象,只能作为父类被继承。其核心作用是:定义 “必须实现的接口”,强制子类提供具体实现,从而规范多态行为。
- 纯虚函数的定义:表示该函数没有默认实现,必须由子类重写:
class Animal { // 抽象类(因为包含纯虚函数)
public:
// 纯虚函数:只有声明,没有实现
virtual void makeSound() = 0;
};- 抽象类的特性
- 不能实例化:
Animal animal;或new Animal();都会编译错误(抽象类是 “接口”,不是 “具体对象”)。 - 强制重写:子类必须重写所有纯虚函数,否则子类也会成为抽象类(无法实例化)。
- 定义接口:抽象类本质是 “接口规范”,例如
Animal规定 “所有动物必须能发出声音”,但不关心具体怎么叫(由子类实现)。
- 不能实例化:
class Dog : public Animal {
public:
void makeSound() override { // 必须重写,否则Dog也是抽象类
cout << "狗汪汪叫" << endl;
}
};运算符重载(Operator Overloading)
- C++ 特性:自定义类的运算符行为。
- 示例:
class Vector {
public:
Vector operator+(const Vector& other) {
return Vector(x + other.x, y + other.y);
}
};POD
🧱 什么是 POD?
POD = Plain Old Data 中文:“普通的旧式数据”
你可以把它理解为:
✅ 一个“纯数据”的结构体,就像 C 语言里的 struct 一样简单。
它没有复杂的 C++ 特性(比如构造函数、虚函数、继承等),只是简单地把一些变量打包在一起。
🔍 POD 的两个核心特征
POD 实际上是两个概念的交集:
1. Trivial(平凡的)
- 对象的创建、销毁、拷贝都可以用最原始的方式完成
- 比如:直接用
memcpy复制内存块就能正确复制对象
✅ 允许的操作:
- 默认构造(不初始化)
- 析构(什么都不做)
- 拷贝(直接复制内存)
❌ 不允许:
- 用户定义的构造函数/析构函数
- 虚函数
- 虚继承
2. Standard Layout(标准布局)
- 成员变量在内存中的排列方式是“标准的”
- 和 C 语言结构体完全兼容
✅ 保证:
- 成员按声明顺序排列
- 没有访问控制变化(比如
public: int x; private: int y;就不行) - 没有多重继承或虚继承
✅ 举个例子:POD 类型
struct Point {
int x;
int y;
};这个 Point 是 POD,因为:
- 它只有两个
int成员 - 没有构造函数、析构函数
- 没有虚函数
- 没有
private或protected成员 - 没有基类
你甚至可以这样操作:
Point a{1, 2};
Point b;
memcpy(&b, &a, sizeof(Point)); // ✅ 安全!b 现在等于 {1,2}也可以在 C 和 C++ 之间传递这个结构体。
❌ 哪些不是 POD?
| 代码 | 为什么不是 POD |
|---|---|
struct S {<br> int x;<br> S() : x(0) {}<br>}; | ❌ 有用户定义构造函数 → 不是 trivial |
struct S {<br> virtual ~S();<br> int x;<br>}; | ❌ 有虚函数 → 不是 trivial |
struct S {<br> int x;<br>private:<br> int y;<br>}; | ❌ 访问控制变化 → 不是 standard layout |
struct Base { int x; };<br>struct Derived : Base { int y; }; | ❌ 有基类 → 可能不是 standard layout(单继承且都是 POD 时仍是) |
struct S {<br> std::string name;<br>}; | ❌ std::string 不是 trivial 类型 |
📌 POD 的重要性(为什么我们要关心?)
-
与 C 兼容
// C 语言函数 extern "C" void process_point(struct Point* p); // C++ 中调用 Point p{1, 2}; process_point(&p); // ✅ 安全,因为 POD 内存布局标准 -
可以用
memcpy安全复制Point a{1, 2}, b; memcpy(&b, &a, sizeof(Point)); // ✅ 安全 -
可以静态初始化
Point p = {1, 2}; // ✅ 即使没有构造函数也能初始化 -
保证内存布局可预测
- 可用于网络通信、文件存储、GPU 编程等底层场景
-
性能优化
- 编译器知道它是简单的数据,可以做更多优化
🆕 C++11 后的变化
C++11 把 POD 拆成了两个独立的概念:
std::is_trivial<T>::valuestd::is_standard_layout<T>::value
而 std::is_pod<T>::value 在 C++20 中被弃用,但概念仍然广泛使用。
现在更推荐说:“这个类型是 trivial 且 standard layout”。
你可以用标准库检测:
#include <type_traits>
static_assert(std::is_pod<Point>::value, "Point should be POD");
static_assert(std::is_trivial<Point>::value, "");
static_assert(std::is_standard_layout<Point>::value, "");💡 总结:一句话理解 POD
POD 就是一个“纯数据容器”,像 C 语言的 struct 一样简单、可预测、可移植。
它没有构造函数、没有虚函数、没有继承、没有访问控制变化,只是一个内存块的集合。
- 如果你需要和 C 交互,或者做底层内存操作,尽量设计成 POD。
- 如果你写的是高性能数据结构(如向量、矩阵),POD 非常有用。
- 否则,不必强求 POD,现代 C++ 的 RAII 和移动语义更重要。