封装继承多态

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, ...};  // 拷贝列表初始化

🔹 优先级规则:

编译器按以下顺序尝试匹配:

  1. 是否有 std::initializer_list<T> 构造函数?
  2. 是否有能接受这些参数的普通构造函数?
  3. 是否是聚合类型?→ 聚合初始化

✅ 情况 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=0

C++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.5

C++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/AN/A
聚合初始化struct {int x;} s{1};无(直接赋值)❌ 不允许✅ 基本类型可转换

✅ 最佳实践建议

  1. 优先使用 {} 初始化(统一初始化)

    • 防止 Most Vexing Parse
    • 禁止窄化转换
    • 更安全
  2. 避免 = 拷贝初始化,除非你明确需要隐式转换

  3. 移动语义设计

    class Holder {
       Resource r;
    public:
       explicit Holder(Resource r) : r(std::move(r)) {} // 按值传递 + 移动
    };
  4. 聚合类型用 {} 初始化

  5. 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: 防止隐式类型转换。

构造函数的类型

  1. 默认构造函数 (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; 会编译错误
    };
  2. 带参数的构造函数 (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;
        // }
    };
  3. 拷贝构造函数 (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);
        }
        // ... 其他成员 ...
    };
  4. 移动构造函数 (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(); // 很可能调用移动构造,而非拷贝构造
  5. 转换构造函数 (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)。
  • 只有一个: 每个类最多只能有一个析构函数。
  • 调用顺序: 对于类对象,析构函数的执行顺序与构造函数相反。先构造的后析构。

析构函数的作用

  1. 资源清理: 这是析构函数最主要的任务。

  2. 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 (三法则): 如果一个类需要显式定义以下三个函数中的任何一个,那么它通常也需要显式定义另外两个:

    1. 析构函数 (Destructor)
    2. 拷贝构造函数 (Copy Constructor)
    3. 拷贝赋值运算符 (Copy Assignment Operator)
    • 原因: 这通常意味着类管理了需要特殊处理的资源(如裸指针)。默认的拷贝是浅拷贝,会导致多个对象管理同一块资源,析构时发生双重释放(Double Free)错误。
  • Rule of Five (五法则) - C++11: 在 Rule of Three 的基础上,增加了两个与移动语义相关的函数:

    1. 移动构造函数 (Move Constructor)
    2. 移动赋值运算符 (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);转移资源到已有对象

掌握要点:

  1. 理解生命周期: 构造函数和析构函数是对象生命周期的起点和终点。
  2. 善用初始化列表: 提高效率,尤其对类类型成员。
  3. 理解拷贝与移动: 区分浅拷贝和深拷贝,理解移动语义的优势。
  4. 遵循 RAII: 用构造函数获取资源,用析构函数释放资源。
  5. 牢记 Rule of Three/Five: 当管理资源时,必须同时考虑拷贝、移动和析构的语义一致性。
  6. 优先使用标准库: 尽量使用 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 成员在派生类中仍为 publicprotected 仍为 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;
}

三、派生类的构造与析构

派生类对象的创建和销毁需要遵循特定规则:

  1. 构造函数
    • 派生类构造函数必须先调用基类构造函数(初始化基类部分)。
    • 若基类无默认构造函数,派生类必须在初始化列表中显式调用基类的带参构造函数。
  2. 析构函数
    • 析构函数执行顺序与构造相反:先调用派生类析构,再调用基类析构。
    • 基类析构函数必须声明为虚函数virtual),否则删除派生类对象时可能导致基类部分内存泄漏。

四、函数重写(覆盖)与虚函数

派生类可以重写基类的函数,实现多态:

  1. 虚函数(virtual

    • 基类中用 virtual 声明的函数,允许派生类重写。
    • 通过基类指针 / 引用调用时,会根据实际对象类型调用对应的派生类函数(动态多态)。
  2. 重写规则

    • 函数名、参数列表、返回值类型必须与基类完全一致(协变返回类型除外)。
    • 派生类函数前可加 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;
    }
  3. 纯虚函数与抽象类

    • 纯虚函数: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;
        }
    };

五、继承的特殊场景

  1. 多继承

    • 一个派生类同时继承多个基类(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;
    }
  2. 继承与访问控制

    • 派生类可以通过 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)父类的方法,并在运行时根据对象实际类型调用对应的重写版本

  1. 父类声明:在父类中用 virtual 声明函数(如 virtual void makeSound())。
  2. 子类重写:子类用 override 关键字显式重写(C++11 起推荐,确保重写正确),函数签名(返回类型、参数列表)必须与父类完全一致。
  3. 动态绑定:只有通过父类指针或引用调用虚函数时,才会触发多态(动态绑定);直接用子类对象调用时,行为与普通函数相同。

如果没有 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; 
};
  • 抽象类的特性
    1. 不能实例化Animal animal; 或 new Animal(); 都会编译错误(抽象类是 “接口”,不是 “具体对象”)。
    2. 强制重写:子类必须重写所有纯虚函数,否则子类也会成为抽象类(无法实例化)。
    3. 定义接口:抽象类本质是 “接口规范”,例如 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;
};

这个 PointPOD,因为:

  • 它只有两个 int 成员
  • 没有构造函数、析构函数
  • 没有虚函数
  • 没有 privateprotected 成员
  • 没有基类

你甚至可以这样操作:

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 的重要性(为什么我们要关心?)

  1. 与 C 兼容

    // C 语言函数
    extern "C" void process_point(struct Point* p);
     
    // C++ 中调用
    Point p{1, 2};
    process_point(&p); // ✅ 安全,因为 POD 内存布局标准
  2. 可以用 memcpy 安全复制

    Point a{1, 2}, b;
    memcpy(&b, &a, sizeof(Point)); // ✅ 安全
  3. 可以静态初始化

    Point p = {1, 2}; // ✅ 即使没有构造函数也能初始化
  4. 保证内存布局可预测

    • 可用于网络通信、文件存储、GPU 编程等底层场景
  5. 性能优化

    • 编译器知道它是简单的数据,可以做更多优化

🆕 C++11 后的变化

C++11 把 POD 拆成了两个独立的概念:

  • std::is_trivial<T>::value
  • std::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 和移动语义更重要。