CB1-3-面向对象

发布于:2025-08-31 ⋅ 阅读:(20) ⋅ 点赞:(0)

C/C++道经第1卷 - 第3阶 - 面向对象

心法:本章使用 C++ 项目进行练习

练习项目结构如下

|_ v1-3-basic-oop

S01. OOP基础思想

心法:参考 # JB1-3-面向对象(一) S01 笔记。

S02. OOP封装特性

心法:封装 Encapsulation 是在抽象的前提下,将一个类设计出来的过程。

封装的目的:将抽象设计好的类用代码实现出来:

  • 实例 instance 通过抽象封装得到类 class。
  • 类 class 通过实例化得到实例 instance。

封装的过程:先装后封:

  • :将抽象出来的一些属性和方法编写到类中。
  • :对类中的属性或方法设置不同级别的访问权限以保护对象隐私,隔离变化。
权限修饰符 描述
public 提供了类的公共接口,允许外部代码直接访问
protected 在父类和子类之间共享实现细节,同时对外部代码隐藏
private 隐藏了类的内部实现细节,只允许类的内部访问

E01. 实体类

心法:C++ 实体类 Class 是封装过程的产物,包含属性和方法。

实体类 vs 结构体

  • 实体类:成员默认使用 private 访问权限。
  • 结构体:成员默认使用 public 访问权限。

1. 实例化

心法:C++ 中的实例根据创建方式分为栈实例和堆实例两种。

栈实例 在作用域结束时自动销毁:

  • 创建方式:User user;
  • 调用方式:user.getName();

堆实例 需要手动使用 delete user; 销毁该实例,避免内存泄漏:

  • 创建方式:User user = new User;
  • 调用方式:user -> getName();

武技:测试实例化过程

// encapsulation/NewClass.h
// Created by 周航宇 on 2025/3/3.
//
#ifndef V1_2_BASIC_OOP_NEW_CLASS_H
#define V1_2_BASIC_OOP_NEW_CLASS_H
#include <iostream>

namespace NewClass {

    class User {
    private:
        long id;
    public:
        std::string name;
        int age;

        /** 打印对象信息 */
        void toString() const;
    };

    void test();
}

#endif //V1_2_BASIC_OOP_NEW_CLASS_H
// encapsulation/NewClass.cpp
// Created by 周航宇 on 2025/3/3.
//
#include "NewClass.h"

using std::cout, std::endl;

void NewClass::User::toString() const {
    cout << "User {"
         << "id=" << id << ", "
         << "name=" << name << ", "
         << "age=" << age
         << "}"
         << endl;
}

void NewClass::test() {
    // 栈实例
    User user01;
    user01.name = "小白";
    user01.age = 20;
    user01.toString();

    // 堆实例
    User *pUser = new User;
    pUser->name = "小花";
    pUser->age = 30;
    pUser->toString();
}

2. 构造器

心法:构造器,也叫构造方法,格式为 类名(参数){},每实例化一次,构造器就会被调用一次,一般用于类的初始化工作(比如为成员属性赋值等),支持重载。

默认构造器:每个类都默认存在一个隐式构造器,支持如下两种方式进行显示声明覆盖:

  • User(){}:此时编译器不再为类生成默认的复制构造器和移动构造器。
  • User() = default;:此时编译器仍会为类生成默认的复制构造器和移动构造器。

赋值方式初始化

  • 代码示例:User(long a, string b){ this->a = a; this->b = std::move(b) }
  • 方式特点:进入构造器 赋值,赋值前成员属性会被赋值为默认值。

列表方式初始化

  • 代码示例:User(long a, string b) : a(a), b(std::move(b)){}
  • 方式特点:进入构造器 赋值,避免成员属性的默认初始化,提高效率和可读性。

显示调用构造器:即创建对象时明确地调用构造器,代码较复杂但意图更清晰,可读性高:

  • 示例:User u("JoeZhou");
  • 示例:User u = User("Lucky", 18);

隐式调用构造器:编译器自动调用构造器进行类型转换或对象初始化,代码简单但意图模糊,可读性低:

  • 示例:User u = "JoeZhou"; 隐式转换为 User u("JoeZhou");
  • 示例:User u = {"Lucky", 18}; 隐式转换为 User u = User("Lucky", 18);

explicit(明确地)

  • explicit 修饰的构造器只能用于显示的创建,从而防止编译器进行意外的隐式类型转换。
  • explicit 可以避免代码中出现难以察觉的错误,避免代码逻辑混乱,提升代码的可维护性。

武技:测试类的构造器

// encapsulation/Constructor.h
// Created by 周航宇 on 2025/3/3.
//
#ifndef V1_2_BASIC_OOP_CONSTRUCTOR_H
#define V1_2_BASIC_OOP_CONSTRUCTOR_H
#include <iostream>

namespace Constructor {
    class User {
    private:
        long id = 0; // 初始值为 0
        std::string name;
    public:

        /** 构造器:每个类都默认存在一个隐式无参的构造器,显示声明构造器时会被覆盖 */
        User();

        /**
         * 构造器:explicit 禁止隐式调用
         * @param id 主键
         */
        explicit User(long id);

        /**
         * 构造器:进入构造器后赋值,赋值前成员属性会被赋值为默认值
         * @param id 主键
         * @param name 姓名
         */
        User(long id, std::string name);

        /**
         * 构造器:进入构造器时赋值,避免成员属性的默认初始化,提高效率和可读性
         * @param name 姓名
         * @param id 主键
         */
        User(std::string name, long id);

        /** 打印对象信息 */
        void toString() const;
    };

    void test();
}

#endif //V1_2_BASIC_OOP_CONSTRUCTOR_H
// encapsulation/Constructor.cpp
// Created by 周航宇 on 2025/3/3.
//
#include "Constructor.h"

using std::cout, std::endl;

Constructor::User::User() {
    cout << "构造器 User()" << endl;
}

Constructor::User::User(long id) {
    this->id = id;
    cout << "构造器 User(long id)" << endl;
}

Constructor::User::User(long id, std::string name) {
    this->id = id;
    this->name = std::move(name);
    cout << "构造器 User(long id, string name)" << endl;
}

Constructor::User::User(std::string name, long id) :
        id(id),
        name(std::move(name)) {
    cout << "构造器 User(string name, long id)" << endl;
}

void Constructor::User::toString() const {
    cout << "User {"
         << "id=" << id << ", "
         << "name=" << name
         << "}"
         << endl;
}

void Constructor::test() {
    // 通过无参构造器创建 User 对象
    User user01;

    // 通过有参构造器创建 User 对象
    User user02(2);
    // User user02 = 2; // error:因为标记了 explicit 关键字,不允许隐式转换

    // 通过有参构造器创建 User 对象(显示)
    User user03$1(3, "比目鱼"); // 显示
    User user03$2 = {3, "比目鱼"}; // 隐式

    // 通过有参构造器创建 User 对象(隐式)
    User user04$1("鱿鱼", 4);
    User user04$2 = {"鱿鱼", 4};

    user01.toString();
    user02.toString();
    user03$1.toString();
    user03$2.toString();
    user04$1.toString();
    user04$2.toString();
}

3. 复制构造器

心法:复制构造函数是一种特殊的构造函数,用于根据另一个同类型的对象创建自己(浅拷贝),格式为 类名(const 类名 &that){},其中的 const 保证不会修改另一个同类型的对象。

默认复制构造器:在 C++ 中,如果类没有显式定义复制构造函数,编译器会自动生成一个默认的复制构造函数,对类的每个成员变量进行浅拷贝。

复制方法

  • User(const User &that) {this->a = that.b; this->b = that.b}:通过代码手动复制。
  • User(const User &that) : a(that.a), b(that.b){}:通过列表手动复制。
  • User(const User &that) = default;:全参自动复制。

浅拷贝问题:复制构造器默认执行浅拷贝,若类中包含指针等动态分配的资源,可能会导致多个对象指向同一块内存,从而在析构时引发重复释放的问题,此时可以在复制构造器中手动完成深拷贝。

武技:测试复制构造器

// encapsulation/CopyConstructor.h
// Created by 周航宇 on 2025/3/3.
//
#ifndef V1_2_BASIC_OOP_COPY_CONSTRUCTOR_H
#define V1_2_BASIC_OOP_COPY_CONSTRUCTOR_H
#include <iostream>
#include <utility>

namespace CopyConstructor {

    class User {
    private:
        long id;
        std::string name;
        int *arr;
        int arrSize;
    public:
        /** 构造函数 */
        User(long id, std::string name, const int *array, int arrSize);

        /**
         * 复制构造器
         *
         * 浅拷贝可简化为 Lion(const Lion &that) = default;
         * @param other 同类型引用
         * @param deepCopy  是否深拷贝,默认false
         */
        User(const User &other, bool deepCopy = false);

        /** 打印对象信息 */
        void toString() const;

        /** 析构函数 */
        ~User();
    };

    void test();
}

#endif //V1_2_BASIC_OOP_COPY_CONSTRUCTOR_H
// encapsulation/CopyConstructor.cpp
// Created by 周航宇 on 2025/3/3.
//
#include "CopyConstructor.h"

using std::cout, std::endl, std::flush, std::string;

CopyConstructor::User::User(long id,
                            string name,
                            const int *array,
                            int arrSize)
        : id(id), name(std::move(name)), arrSize(arrSize) {
    cout << "构造函数" << endl;
    arr = new int[arrSize];
    for (int j = 0; j < arrSize; ++j) {
        arr[j] = array[j];
    }
}

CopyConstructor::User::User(const CopyConstructor::User &other, bool deepCopy) {
    this->id = other.id;
    this->name = other.name;
    this->arrSize = other.arrSize;
    if (deepCopy) {
        arr = new int[other.arrSize];
        for (int i = 0; i < other.arrSize; i++) {
            arr[i] = other.arr[i];
        }
        cout << "复制构造器(深拷贝)" << endl;
    } else {
        arr = other.arr;
        cout << "复制构造器(浅拷贝)" << endl;
    }
}

void CopyConstructor::User::toString() const {
    cout << "User { " << flush;
    cout << "name: " << name << ", " << flush;
    cout << "id: " << id << ", " << flush;
    cout << "array: " << flush;
    for (int i = 0; i < arrSize; i++) {
        cout << arr[i] << " " << flush;
    }
    cout << "}" << endl;
}

CopyConstructor::User::~User() {
    cout << "析构函数" << endl;
    delete[] arr;
}

void CopyConstructor::test() {
    // 调用构造器
    int arr[3] = {1, 2, 3};
    User user$01(1, "赵四", arr, 3);
    user$01.toString();

    // 调用复制构造器,根据 user$01 构造 user$02
    // 浅拷贝会导致重复释放资源,会导致程序崩溃
    User user$02(user$01, true);
    user$02.toString();
}

4. 移动构造器

心法:移动构造函数是 C++11 引入的新特性,用于将另一个同类型的对象直接转移给自己接管,避免了不必要的复制操作,提高了性能,格式为 类名(同类型 &&that){},转移成功后,通常还需要将原对象的资源指针置为 nullptr,避免析构时重复释放。

默认移动构造器:在 C++11 及以后的标准中,如果类没有显式定义移动构造函数,并且满足以下条件,编译器会自动生成一个默认的移动构造函数:

  • 没有显式定义复制构造器。
  • 没有显式定义复制赋值运算符。
  • 没有显式定义移动赋值运算符。
  • 没有显式定义析构函数。

如果不满足上述条件,编译器将不会生成默认的移动构造函数。需要注意的是,即使编译器生成了默认的移动构造函数,对于一些包含复杂资源的类,默认的移动构造函数可能无法正确处理资源的转移,此时需要显式定义移动构造函数。

移动方法

  • User(User &&that) noexcept {this->a = std::move(that.a);}:通过代码手动移动。
  • User(User &&that) noexcept : a(std:move(that.a)){}:通过列表手动移动。
  • User(User &&that) noexcept = default;:全参自动移动。

武技:测试移动构造器

// encapsulation/MoveConstructor.h
// Created by 周航宇 on 2025/3/3.
//
#ifndef V1_2_BASIC_OOP_MOVECONSTRUCTOR_H
#define V1_2_BASIC_OOP_MOVECONSTRUCTOR_H
#include <iostream>
#include <utility>

namespace MoveConstructor {
    class User {
    private:
        std::string name;
        int age;
    public:
        /** 构造函数 */
        User(std::string name, int age);

        /**
         * 移动构造函数
         *
         * 可简化为 User(User &&that) noexcept = default;
         * @param that 同类型引用
         */
        User(User &&that) noexcept;

        /** 打印对象信息 */
        void toString();
    };

    void test();
}

#endif //V1_2_BASIC_OOP_MOVECONSTRUCTOR_H
// encapsulation/MoveConstructor.cpp
// Created by 周航宇 on 2025/3/3.
//
#include "MoveConstructor.h"

using std::cout, std::endl;

MoveConstructor::User::User(std::string name, int age)
        : name(std::move(name)), age(age) {
    cout << "构造器" << endl;
}

MoveConstructor::User::User(MoveConstructor::User &&that) noexcept
        : name(std::move(that.name)), age(that.age) {
    cout << "移动构造器" << endl;
    // 重置原对象的状态,避免资源重复释放
    that.age = 0;
}

void MoveConstructor::User::toString() {
    cout << "User: " << name << " " << age << endl;
}

void MoveConstructor::test() {
    // 创建一个 User 对象
    User user$01("赵四", 3);

    // 移动构造:将 user$01 移动到 user$02,然后 user$01 被重置
    User user$02(std::move(user$01));
    user$01.toString();
    user$02.toString();
}

5. 析构器

心法:析构器,也叫析构方法,格式为 ~ 类名 (){},每个对象在死亡时自动调用一次析构器,一般用于类的清理工作(比如释放动态分配的内存,关闭打开的文件等)。

每个类都默认存在一个隐式无参的析构器,显示声明析构器时会被覆盖。

武技:测试类的析构器

// encapsulation/Destructor.h
// Created by 周航宇 on 2025/3/3.
//
#ifndef V1_2_BASIC_OOP_DESTRUCTOR_H
#define V1_2_BASIC_OOP_DESTRUCTOR_H
#include <iostream>

namespace Destructor {

    class User {
    private:
        int *array;

    public:
        /** 构造器 */
        User();
        /** 析构器 */
        ~User();
    };

    void test();
};

#endif //V1_2_BASIC_OOP_DESTRUCTOR_H
// encapsulation/Destructor.cpp
// Created by 周航宇 on 2025/3/3.
//
#include "Destructor.h"

using std::cout, std::endl;

Destructor::User::User() {
    cout << "User():创建数组" << endl;
    // 资源分配
    array = new int[10];
}

Destructor::User::~User() {
    cout << "~User():销毁数组" << endl;
    // 资源释放
    delete[] array;
}

void Destructor::test() {
    User user;
}

E02. 实例属性

心法:属性 Field 负责存储数据,直接在类的内部编写,支持权限修饰符。

局部变量在不赋值使用的情况下会指向一个随机值,而成员属性在不赋值的情况下直接使用,是否会拥有初始值(零值),要取决于调用方将该类的实例创建为何种类型:

  • 若创建为 全局实例:成员属性赋零值,比如 0, 0.0, '\0', false, nullptr 等。
  • 若创建为 静态实例:成员属性赋零值,比如 0, 0.0, '\0', false, nullptr 等。
  • 若创建为 局部实例:成员属性均指向一个随机值。

武技:测试实例属性

// encapsulation/Field.h
// Created by 周航宇 on 2025/3/3.
//
#ifndef V1_2_BASIC_OOP_FIELD_H
#define V1_2_BASIC_OOP_FIELD_H
#include <iostream>
#include <string>

namespace Field {
    class User {
    public:
        long id;
        std::string name;
        float weight;
        double price;
        bool isBoy;
        char gender;
        int roleIds[5];
    };

    void test();
};

#endif //V1_2_BASIC_OOP_FIELD_H
// encapsulation/Field.cpp
// Created by 周航宇 on 2025/3/3.
//
#include "Field.h"

using std::cout, std::endl, std::flush;

Field::User globalUser;

void printUser(const Field::User &user) {
    cout << "User:id=" << user.id
         << ", name=\"" << user.name << "\""
         << ", weight=" << user.weight
         << ", price=" << user.price
         << ", isBoy=" << user.isBoy
         << ", gender=" << user.gender
         << ", roleIds=[ "
         << flush;
    for (int roleId: user.roleIds) {
        cout << roleId << " " << flush;
    }
    cout << "]" << endl;
}

void Field::test() {

    User user;
    cout << "局部 User 实例:" << flush;
    printUser(user);

    cout << "全局 User 实例:" << flush;
    printUser(globalUser);

    static User staticUser;
    cout << "静态 User 实例:" << flush;
    printUser(staticUser);
}

1. SET/GET访问

心法:出于安全考虑,一般情况下,属性是不允许被外界直接修改或访问的,只能通过 SET/GET 方法进行访问。

在 CLION 中,使用 alt + insert 可以快速生成指定属性的 SET/GET 方法,但目前看来不是特别只能,需要二次修改。

[[nodiscard]] 修饰符

nodiscard 是 C++17 引入的方法修饰符,它属于 C++ 语言标准的一部分,其主要作用是告诉编译器,如果函数的返回值被忽略,编译器应该给出警告,这有助于开发者避免一些潜在的错误,比如调用了一个应该检查返回值的函数却没有检查。

武技:测试 SET/GET 方法

// encapsulation/SetGet.h
// Created by 周航宇 on 2025/3/3.
//
#ifndef V1_2_BASIC_OOP_SETGET_H
#define V1_2_BASIC_OOP_SETGET_H
#include <iostream>

namespace SetGet {
    class User {
    private:
        long id;
        std::string name;
        float weight;
        double price;
        bool isBoy;
        char gender;
        int roleIds[5];

    public:
        [[nodiscard]] long getId() const;
        [[nodiscard]] const std::string &getName() const;
        [[nodiscard]] float getWeight() const;
        [[nodiscard]] double getPrice() const;
        [[nodiscard]] bool getIsBoy() const;
        [[nodiscard]] char getGender() const;
        [[nodiscard]] const int *getRoleIds() const;
        void setId(long id);
        void setName(const std::string &name);
        void setWeight(float weight);
        void setPrice(double price);
        void setIsBoy(bool isBoy);
        void setGender(char gender);
        void setRoleIds(const int *roleIds);
    };

    void test();
};

#endif //V1_2_BASIC_OOP_SETGET_H
// encapsulation/SetGet.cpp
// Created by 周航宇 on 2025/3/3.
//
#include "SetGet.h"

using std::cout, std::endl, std::flush, std::string;

long SetGet::User::getId() const {
    return id;
}

void SetGet::User::setId(long newId) {
    this->id = newId;
}

const string &SetGet::User::getName() const {
    return name;
}

void SetGet::User::setName(const string &newName) {
    this->name = newName;
}

float SetGet::User::getWeight() const {
    return weight;
}

void SetGet::User::setWeight(float newWeight) {
    this->weight = newWeight;
}

double SetGet::User::getPrice() const {
    return price;
}

void SetGet::User::setPrice(double newPrice) {
    this->price = newPrice;
}

bool SetGet::User::getIsBoy() const {
    return isBoy;
}

void SetGet::User::setIsBoy(bool newIsBoy) {
    this->isBoy = newIsBoy;
}

char SetGet::User::getGender() const {
    return gender;
}

void SetGet::User::setGender(char newGender) {
    this->gender = newGender;
}

const int *SetGet::User::getRoleIds() const {
    return roleIds;
}

void SetGet::User::setRoleIds(const int *newRoleIds) {
    for (int i = 0; i < 3; i++) {
        this->roleIds[i] = newRoleIds[i];
    }
}

void SetGet::test() {
    User user;
    user.setId(1L);
    user.setName("圆圆");
    user.setWeight(12.5f);
    user.setPrice(100.33);
    user.setIsBoy(true);
    user.setGender('F');
    int roleIds[3] = {1, 2, 99};
    user.setRoleIds(roleIds);

    cout << "id=" << user.getId() << endl;
    cout << "name=" << user.getName() << endl;
    cout << "weight=" << user.getWeight() << endl;
    cout << "price=" << user.getPrice() << endl;
    cout << "isBoy=" << user.getIsBoy() << endl;
    cout << "gender=" << user.getGender() << endl;
    cout << "roleIds=" << flush;
    for (int i = 0; i < 3; i++) {
        cout << user.getRoleIds()[i] << " " << flush;
    }
    cout << endl;
}

E03. 实例方法

心法:方法 Method 负责操作数据,其意义是用于提高代码重用性,支持权限修饰符。

方法名称:小驼峰,尽量使用动词。

方法参数:表示当你调用这个方法时,需要传递给它的数据:

  • 形参:定义方法时设计的变量,如 void setId(long id){ .. } 中的 id,多个形参称为形参列表。
  • 实参:调用方法时传入的变量,如 user.setId(money) 中的 money,多个实参称为实参列表。
  • 默认值:C++ 允许在定义方法时对形参设置默认值,但一定要从右向左连续指定。
  • 常形参:当函数参数被 const 修饰时,在函数内部不能对该参数进行修改,增强代码的可读性和可维护性,提高了代码的安全性,也更符合接口设计的原则(你总不会希望你设计的接口参数被调用者随意更改吧?)。

方法返回值:表示当你调用这个方法后,它给你返回的数据:

  • 若方法没有返回值,也要声明为 void 关键字。
  • 若方法拥有返回值,则必须要在方法体内部使用 return 关键字返回一个对应类型的数据。

内联方法

在程序执行过程中,方法调用会涉及一系列的操作,比如保存当前执行位置、传递参数、跳转到方法体执行代码,执行完毕后再返回到调用点等,这些操作会带来一定的开销。

C++ 支持使用 inline 关键字修饰方法,此时编译器会尝试将方法体中的代码直接嵌入到调用处,以减少方法调用的开销。

inline 关键字只是向编译器发出一个将方法内联的建议,编译器会综合考虑方法复杂度、调用方式以及自身的优化策略等因素,决定是否真正将方法内联:

  • 如果方法体中的代码逻辑比较复杂,比如包含大量的代码、循环、递归调用等,编译器通常不会内联处理它。
  • 如果方法通过函数指针调用,编译器无法在编译时确定具体调用的目标,也就无法进行内联处理。
  • 不同的编译器有不同的优化策略和算法,它们会根据自身的判断来决定是否将一个函数内联,有些编译器可能对 inline 关键字的处理更为保守,只有在非常简单的函数情况下才会进行内联。

武技:测试实例方法

// encapsulation/Method.h
// Created by 周航宇 on 2025/3/3.
//
#ifndef V1_2_BASIC_OOP_METHOD_H
#define V1_2_BASIC_OOP_METHOD_H
#include <iostream>

namespace Method {
    class User {
    public:

        /**
         * 说话
         * @param content 说话的内容
         * @param times 重复的次数,默认为 5
         */
        void talk(const std::string &content = "哼哼", int times = 5);

        /** 运动(内联方法)*/
        inline void run();
    };

    void test();
};

#endif //V1_2_BASIC_OOP_METHOD_H
// encapsulation/Method.cpp
// Created by 周航宇 on 2025/3/3.
//
#include "Method.h"

using std::cout, std::endl, std::string;

void Method::User::talk(const string &content, int times) {
    for (int i = 0; i < times; i++) {
        cout << content << endl;
    }
}

void Method::User::run() {
    cout << "猪跑起来了!" << endl;

}

void Method::test() {
    User user;
    user.talk("heng heng");
    user.talk("哼哼", 1);
    // 在编译期间直接替换为 "std::cout << "猪跑起来了!" << std::endl;" 代码
    user.run();
}

1. 常函数

心法:常函数是在成员函数声明或定义的参数列表后面加上 const 关键字的成员函数,如 void info() const { ..} 等。

常函数承诺不会修改调用该函数的实例的状态,即不能修改对象的非静态成员(否则编译报错),除非这些成员被添加了 mutable 可变修饰,

// encapsulation/ConstFunction.h
// Created by 周航宇 on 2025/3/3.
//
#ifndef V1_2_BASIC_OOP_CONSTFUNCTION_H
#define V1_2_BASIC_OOP_CONSTFUNCTION_H

namespace ConstFunction {

    class User {
    private:
        int a = 1;
        static int b;
        mutable int c = 3;
    public:
        /** 常函数 */
        int calc() const;
    };

    void test();
};

#endif //V1_2_BASIC_OOP_CONSTFUNCTION_H
// encapsulation/ConstFunction.cpp
// Created by 周航宇 on 2025/3/3.
//
#include "ConstFunction.h"
#include <iostream>

using std::cout, std::endl;

int ConstFunction::User::b = 2;

int ConstFunction::User::calc() const {
    // 常函数中不能修改非 mutable 的成员变量
    // a = 10; // error
    // 常函数中可以修改静态成员变量
    b = 20;
    // 常函数中可以修改 mutable 的成员变量
    c = 30;
    return a + b + c;
}

void ConstFunction::test() {
    User user;
    cout << "a + b + c = " << user.calc() << endl;
}

2. 方法传参

心法:C++ 主要有三种参数传递方式,每种方式各有特点,使用时需谨慎。

值传递

  • 原理:方法调用时,会创建实参的副本传递给形参,方法内部对形参的修改不会影响实参。
  • 注意:如果实参是较大的对象,值传递会带来较大的开销,因为需要复制整个对象。

引用传递

  • 原理:传递实参的引用(别名)给形参,方法内部对形参的修改会直接影响实参。
  • 注意:引用传递可以避免对象复制的开销,但使用时要确保引用的对象在方法调用期间是有效的。

指针传递

  • 原理:传递实参的地址给形参,方法内部可以通过指针间接访问和修改实参。
  • 注意:要确保指针不为空指针,避免出现对空指针解地址的错误。

武技:测试方法传参内容

// encapsulation/MethodParameter.h
// Created by 周航宇 on 2025/2/28.
//
#ifndef V1_2_BASIC_OOP_METHODPARAMETER_H
#define V1_2_BASIC_OOP_METHODPARAMETER_H
#include <iostream>
#include <string>

namespace MethodParameter {
    /** 测试值传递 */
    void changeValueA(int num);
    /** 测试引用传递 */
    void changeValueB(int &num);
    /** 测试指针传递 */
    void changeValueC(int *num);

    void test();
}

#endif //V1_2_BASIC_OOP_METHODPARAMETER_H
// encapsulation/MethodParameter.cpp
// Created by 周航宇 on 2025/2/28.
//
#include "MethodParameter.h"

using std::cout, std::endl;

void MethodParameter::changeValueA(int num) {
    num = 100;
    cout << "num = " << num << endl;
}

void MethodParameter::changeValueB(int &num) {
    num = 100;
    cout << "num = " << num << endl;
}

void MethodParameter::changeValueC(int *num) {
    if (num != nullptr) {
        *num = 100;
        std::cout << "num = " << *num << std::endl;
    }
}

void MethodParameter::test() {
    int a = 10;
    changeValueA(a);
    cout << "a = " << a << endl;

    int b = 10;
    changeValueB(b);
    cout << "b = " << b << endl;

    int c = 10;
    changeValueC(&c);
    cout << "c = " << c << endl;
}

E04. 静态成员

心法:被 static 修饰的成员称为静态成员,包括静态局部变量,静态方法和静态代码块(简称静态块)等。

共享:静态成员属于类而非某个实例,即被所有实例共享,推荐直接通过类名调用。

先行:静态成员在类加载时优先执行,故静态区域中无法访问非静态成员,也无法使用this关键字。

1. 静态属性

心法:被 static 修饰的属性称为静态属性,静态属性不能在类的定义中直接赋值进行初始化(静态常量整数如 int、char、long 等除外),但可以在类外进行赋值初始化。

静态成员变量属于类本身,整个程序生命周期内只有一个实例,直接在类定义中赋值会导致每次创建对象时都尝试初始化,不符合其特性。类定义仅描述结构,不分配内存,因此不能直接赋值。

武技:测试静态属性

// encapsulation/staticField.h
// Created by 周航宇 on 2025/2/28.
//
#ifndef V1_2_BASIC_OOP_STATICFIELD_H
#define V1_2_BASIC_OOP_STATICFIELD_H
#include <iostream>
#include <string>

namespace StaticField {
    class User {
    public:
        long long id = 1L;
        static std::string name;
        static const int TYPE = 2;
    };

    void test();
}

#endif //V1_2_BASIC_OOP_STATICFIELD_H
// encapsulation/staticField.cpp
// Created by 周航宇 on 2025/2/28.
//
#include "StaticField.h"

using std::cout, std::endl, std::string;

// 为 User 的静态成员变量赋值
string StaticField::User::name = "JoeZhou";

void StaticField::test() {
    User user;
    cout << "id: " << user.id << endl;
    cout << "name: " << User::name << endl;
    cout << "TYPE: " << User::TYPE << endl;
}

2. 静态方法

心法:静态成员比实例成员更早被加载,所以静态域中无法访问非静态数据。

静态块中 静态方法中 实例方法中
是否可以访问静态属性 可以访问 可以访问 可以访问
是否可以访问实例属性 无法访问 无法访问 可以访问

武技:测试静态属性

// encapsulation/staticMethod.h
// Created by 周航宇 on 2025/2/28.
//
#ifndef V1_2_BASIC_OOP_STATICMETHOD_H
#define V1_2_BASIC_OOP_STATICMETHOD_H
#include <iostream>
#include <string>

namespace StaticMethod {

    class User {
    private:
        static std::string name;
        int age = 100;
    public:
        void talk01();
        static void talk02();
    };

    void test();
}

#endif //V1_2_BASIC_OOP_STATICMETHOD_H
// encapsulation/staticMethod.cpp
// Created by 周航宇 on 2025/2/28.
//
#include "StaticMethod.h"

using std::cout, std::endl, std::string;

string StaticMethod::User::name = "Lucky";

void StaticMethod::User::talk01() {
    User user;
    // 实例方法可以访问静态属性
    cout << "name: " << User::name << endl;

    // 实例方法可以访问实例属性
    cout << "age: " << user.age << endl;
}

void StaticMethod::User::talk02() {
    {
        // 静态方法可以访问静态属性
        cout << "name: " << User::name << endl;

        // 静态方法不可以访问实例属性
        // cout << "age: " << user.age << endl;
    };
}

void StaticMethod::test() {
    User user;
    user.talk01();
    User::talk02();
}

E05. 内部类

心法:在 C++ 中,内部类的定义很简单,只需要在一个类的内部声明并定义另一个类即可。

作用域:内部类的作用域被限制在外部类的内部,创建方式如下:

  • 栈上创建内部类:Outer::Inner inner;
  • 堆上创建内部类:auto *inner = new Outer::Inner;

访问权限:内部类可以通过一个外部类对象的引用来访问外部类成员(包括私有成员)。

武技:测试实例内部类

// encapsulation/InnerClass.h
// Created by 周航宇 on 2025/3/3.
//
#ifndef V1_2_BASIC_OOP_INNERCLASS_H
#define V1_2_BASIC_OOP_INNERCLASS_H
#include <iostream>

namespace InnerClass {
    class User {
    private:
        int age = 10;
    public:
        std::string name = "悟空";

        // 内部类
        class Child {
        public:
            void accessName();
            static void accessAge();
        };
    };

    void test();
}

#endif //V1_2_BASIC_OOP_INNERCLASS_H
// encapsulation/InnerClass.cpp
// Created by 周航宇 on 2025/3/3.
//
#include "InnerClass.h"

using std::cout, std::endl;

void InnerClass::User::Child::accessName() {
    User user;
    // 内部类可以通过一个外部类对象的引用来访问外部类成员(包括私有成员)
    cout << "name: " << user.name << endl;
}

void InnerClass::User::Child::accessAge() {
    User user;
    // 内部类可以通过一个外部类对象的引用来访问外部类成员(包括私有成员)
    cout << "age: " << user.age << endl;
}

void InnerClass::test() {

    // 栈上分配
    User::Child child$01;
    child$01.accessName();
    User::Child::accessAge();

    // 堆上分配
    auto *child$02 = new User::Child;
    child$02->accessName();
    User::Child::accessAge();
    delete child$02;
}

S03. OOP继承特性

E01. 继承具体实现

心法:继承的意义就是提高代码可重用性,C++ 支持多继承,父类也称为基类,子类也称为派生类。

A 类继承 B 类的格式为 class A : 继承方式 B {},但被 final 修饰的类是无法被继承的。

1. 继承方式

心法:C++ 中支持 public,protected 和 private 三种继承方式。

public 公有继承:父类的 public 和 protected 成员继承给子类后,权限修饰符不变。

protected 保护继承:父类的 public 和 protected 成员继承给子类后,权限修饰符都变为 protected,此时若存在孙子类,则孙子类可以继续继承其父类中从爷爷类继承过来的 protected 成员。

private 私有继承:父类的 public 和 protected 成员继承给子类后,权限修饰符都变为 private 成员,此时若存在孙子类,则孙子类无法继续继承其父类从爷爷类继承过来的 protected 成员。

无论哪种继承方式,父类的 private 成员在子类中均不可直接访问,即子类的成员方法中,只能访问父类的 public 和 protected 成员。

武技:测试三种继承方式

// extend/ExtendType.h
// Created by 周航宇 on 2025/2/28.
//
#ifndef V1_2_BASIC_OOP_EXTENDTYPE_H
#define V1_2_BASIC_OOP_EXTENDTYPE_H
#include <iostream>
#include <string>

namespace ExtendType {

    class Father {
    public:
        std::string pub = "father-public";
    private:
        std::string pri = "father-private";
    protected:
        std::string pro = "father-protected";
    };

    // 切换这里的 public 为 private 或 protected 进行测试
    class Son : public Father {
    public:
        void testSon();
    };

    class GrandSon : public Son {
    public:
        void testGrandSon();
    };

    void test();
}

#endif //V1_2_BASIC_OOP_EXTENDTYPE_H
// extend/ExtendType.cpp
// Created by 周航宇 on 2025/2/28.
//
#include "ExtendType.h"

using std::cout, std::endl;

void ExtendType::Son::testSon() {
    // 只能访问父类的 public 和 protected 成员
    cout << "我是儿子,我父亲的 pub 属性是:" << pub << endl;
    cout << "我是儿子,我父亲的 pro 属性是:" << pro << endl;
}

void ExtendType::GrandSon::testGrandSon() {
    // 只能访问父类的 public 和 protected 成员
    cout << "我是孙子,我爷爷的 pub 属性是:" << pub << endl;
    cout << "我是孙子,我爷爷的 pro 属性是:" << pro << endl;
}

void ExtendType::test() {
    Son son;
    // cout << "son.pub:" << son.pub << endl;
    son.testSon();
    GrandSon grandSon;
    grandSon.testGrandSon();
}

2. 父子调用

心法:子类可以直接继承使用父类的非 private 成员,但父类无法访问子类任何成员。

构造顺序:父类构造器 -> 子类构造器 -> 子类析构器 -> 父类析构器。

成员访问:访问子类的某个成员时,先在子类中寻找,不存在则根据继承关系依次向上寻找。

武技:测试父子调用

// extend/ExtendMember.h
// Created by 周航宇 on 2025/2/28.
//
#ifndef V1_2_BASIC_OOP_EXTENDMEMBER_H
#define V1_2_BASIC_OOP_EXTENDMEMBER_H
#include <iostream>
#include <string>

namespace ExtendMember {

    class Fu {
    protected:
        std::string fuName = "老父亲";
    public:
        Fu();
        ~Fu();
    };

    class Zi : public Fu {
    protected:
        std::string ziName = "大儿子";
    public:
        Zi();
        ~Zi();
    };

    class Sun : public Zi {
    protected:
        std::string sunName = "小孙子";
    public:
        Sun();
        ~Sun();
        void test();
    };

    void test();
}

#endif //V1_2_BASIC_OOP_EXTENDMEMBER_H
// extend/ExtendMember.cpp
// Created by 周航宇 on 2025/2/28.
//
#include "ExtendMember.h"

using std::cout, std::endl;

ExtendMember::Fu::Fu() {
    cout << "Fu()" << endl;
}

ExtendMember::Fu::~Fu() {
    cout << "~Fu()" << endl;
}

ExtendMember::Zi::Zi() {
    cout << "Zi()" << endl;
}

ExtendMember::Zi::~Zi() {
    cout << "~Zi()" << endl;
}

ExtendMember::Sun::Sun() {
    cout << "Sun()" << endl;
}

void ExtendMember::Sun::test() {
    // 先在子类中寻找,不存在则根据继承关系依次向上寻找
    cout << "sunName: " << sunName << endl;
    cout << "ziName: " << ziName << endl;
    cout << "fuName: " << fuName << endl;
}

ExtendMember::Sun::~Sun() {
    cout << "~Sun()" << endl;
}

void ExtendMember::test() {
    Sun sun;
    sun.test();
}

3. 隐藏父成员

心法:当子类设计了和父类同名的成员(包括实例属性,静态属性,实例方法和静态方法)时,会优先访问自己的成员,此时视为子类隐藏了父类的成员。

隐藏的意义:一般用于防止子类误操作父类的某些成员,此时在子类中,除非使用作用域解析符 :: 来强行引用父类的成员,否则是访问不到它们,也操作不了它们的。

隐藏 vs 重写

  • 隐藏方法的时候,父子类方法名相同,参数列表可以相同也可以不同,不具有多态性(运行时才能动态确定具体指向哪个子类)。
  • 重写方法的时候,父子类方法名相同,参数列表必须相同,且必须是虚函数,具有多态性(运行时才能动态确定具体指向哪个子类)。

武技:测试隐藏父成员效果

// extend/MemberHiding.h
// Created by 周航宇 on 2025/3/1.
//
#ifndef V1_2_BASIC_OOP_MEMBERHIDING_H
#define V1_2_BASIC_OOP_MEMBERHIDING_H
#include <iostream>
#include <string>

namespace MemberHiding {

    class Animal {
    protected:
        // 父类的成员属性,用于测试成员属性的隐藏
        std::string name = "animal-name";
        // 父类的静态成员属性,用于测试静态属性的隐藏
        static std::string type;
    public:
        /** 父类的成员方法,用于测试成员方法的隐藏 */
        std::string talk();
        /** 父类的静态成员方法,用于测试静态方法的隐藏 */
        static std::string run();
    };

    class Snake : public Animal {
    protected:
        // 子类的成员属性,和父类同名,视为隐藏了父类的同名属性
        std::string name = "snake-name";
        // 子类的静态成员属性,和父类同名,视为隐藏了父类的同名属性
        static std::string type;
    public:
        /** 子类的成员方法,和父类同名不同参,视为隐藏了父类的同名方法 */
        std::string talk(std::string msg);
        /** 子类的静态方法,和父类同名不同参,视为隐藏了父类的同名方法 */
        static std::string run(int times);
        /** 用于测试子类访问父类成员 */
        void test();
    };

    void test();
}

#endif //V1_2_BASIC_OOP_MEMBERHIDING_H
// extend/MemberHiding.cpp
// Created by 周航宇 on 2025/3/1.
//
#include "MemberHiding.h"

using std::cout, std::endl, std::string;

string MemberHiding::Animal::type = "动物类";

string MemberHiding::Animal::talk() {
    return "我是动物";
}

string MemberHiding::Animal::run() {
    return "动物在跑";
}

string MemberHiding::Snake::type = "蛇类";

string MemberHiding::Snake::talk(string msg) {
    return msg;
}

string MemberHiding::Snake::run(int times) {
    string result = "蛇跑了整整";
    result += std::to_string(times);
    result += "圈";
    return result;
}

void MemberHiding::Snake::test() {
    // 访问子类成员:此时父类的同名成员被隐藏,不会被误访问
    cout << "snake-name: " << name << endl;
    cout << "snake-type: " << type << endl;
    cout << "snake-talk(): " << talk("我是蛇") << endl;
    cout << "snake-run(): " << run(18) << endl;
    // 强行访问父类成员
    cout << "animal-name: " << Animal::name << endl;
    cout << "animal-type: " << Animal::type << endl;
    cout << "animal-talk(): " << Animal::talk() << endl;
    cout << "animal-run(): " << Animal::run() << endl;
}

void MemberHiding::test() {
    Snake snake;
    snake.test();
}

S04. OOP多态特性

E01. 多态具体实现

心法:C++ 的多态性 polymorphic 是 OOP 的一个核心概念,可以理解为一种事物多种形态,它允许我们在运行时根据不同的实现调用相同的方法,产生不同的结果。

你开了一家餐厅,无论什么样的餐厅,你都要出售至少一道招牌菜 sellFood()

  • 将餐厅实现为中餐厅,则 sellFood() 的执行结果应该是 北京烤鸭
  • 将餐厅实现为西餐厅,则 sellFood() 的执行结果应该是 牛排
  • 将餐厅实现为法餐厅,则 sellFood() 的执行结果应该是 勃艮第红酒炖牛肉
  • 将餐厅实现为泰餐厅,则 sellFood() 的执行结果应该是 冬阴功汤

多态性:四种实现,调用相同方法时,产生不同的结果。

你养了一只宠物,无论什么样的宠物,你都希望它能发出可爱的声音 makeSound()

  • 将宠物实现为猫,则 makeSound() 的执行结果应该是 喵喵
  • 将宠物实现为狗,则 makeSound() 的执行结果应该是 汪汪
  • 将宠物实现为猪,则 makeSound() 的执行结果应该是 哼哼
  • 将宠物实现为牛,则 makeSound() 的执行结果应该是 哞哞

多态性:四种实现,调用相同方法时,产生不同的结果。

1. 方法重载

心法:方法重载 Overload 是编译时多态的一种体现,在编译时期就能确定具体应该调用哪个方法。

方法重载指的是同一个类中出现了多个同名但不同参的方法的代码现象,对返回值,修饰符,以及是否静态均没有任何要求。

常量性不同也视为形参列表不同,如 void info(const int num)void info(int num) 可以视为发生了重载,编译器会根据传入参数是否为常量来区分调用哪个重载版本的函数。

武技:测试方法重载

// polymorphic/Overload.h
// Created by 周航宇 on 2025/3/1.
//
#ifndef V1_2_BASIC_OOP_OVERLOAD_H
#define V1_2_BASIC_OOP_OVERLOAD_H
#include <iostream>

namespace Overload {

    class MyMath {
    public:
        /** 两个 int 变量计算 */
        int calc(int a, int b);
        /** 一个 int 变量 和一个 double 变量计算 */
        double calc(int a, double b);
        /** 一个 double 变量 和一个 int 变量计算 */
        double calc(double a, int b);
        /** 两个 double 变量计算 */
        double calc(double a, double b);
    };

    void test();
}

#endif //V1_2_BASIC_OOP_OVERLOAD_H
// polymorphic/Overload.cpp
// Created by 周航宇 on 2025/3/1.
//
#include "Overload.h"

using std::cout, std::endl;

int Overload::MyMath::calc(int a, int b) {
    return a + b;
}

double Overload::MyMath::calc(int a, double b) {
    return a + b;
}

double Overload::MyMath::calc(double a, int b) {
    return a + b;
}

double Overload::MyMath::calc(double a, double b) {
    return a + b;
}

void Overload::test() {
    MyMath myMath;
    int a = 1, b = 2;
    double c = 1.0, d = 2.0;
    cout << "result: " << myMath.calc(a, b) << endl;
    cout << "result: " << myMath.calc(a, c) << endl;
    cout << "result: " << myMath.calc(c, a) << endl;
    cout << "result: " << myMath.calc(c, d) << endl;
}

2. 方法重写

心法:方法重写 Override 是运行时多态的一种体现,在运行时期才能确定具体应该调用哪个方法。

虚函数:只有父类中被声明为 virtual 的虚函数才能在子类中被重写,否则视为隐藏,是不具有多态性的。

  • 被 final 修饰的虚函数不能被子类重写。
  • 被 private 修饰的虚函数压根不会继承给子类,所以子类也无法被重写。
  • 被 static 修饰的虚函数在编译时就确定指向父类了,不能被子类重写,只能被隐藏。

重写规则

  • 函数名相同(包括 const 常量性)。
  • 形参列表相同(包括 const 常量性)。
  • 修饰符必须相同或变大,如 protected 只能重写为 protected 或 public。
  • 返回值必须相同或变小,如 Fu* 只能重写为 Fu* 或 Zi*,Fu& 只能重写为 Fu& 或 Zi& 等,且不能无中生有或有中生无。

override 关键字:在 C++11 及以后的标准中,建议在子类重写的函数小括号后面加上 override 关键字,这样做可以让编译器检查该函数是否真的重写了父类的虚函数,有助于发现潜在的错误,具体流程如下:

  • 编译器到父类中查找是否存在同名同参同常量性的函数(private 和 final 函数)会被忽略。
  • 若找不到,则将 override 标记的函数视为一个子类独有的函数,虽然编译不报错,但也没有发生重写,此时该关键字的使用是不符合标准的,应该去除。
  • 若找到了,则比对重写规则,如修饰符,返回值等,比对成功完成重写,比对失败则会编译报错。

武技:测试方法重写

// polymorphic/Override.h
// Created by 周航宇 on 2025/3/3.
//
#ifndef V1_2_BASIC_OOP_OVERRIDE_H
#define V1_2_BASIC_OOP_OVERRIDE_H

namespace Override {

    class Chicken {
    private:
        virtual void testA();
    protected:
        virtual void testB();
    public:
        virtual void testC() final;
        virtual Chicken *testD();
        virtual Chicken &testE();
    };

    class SubChicken : public Chicken {
    public:

        /**
         * 父类的 private 成员不会继承给子类。
         * 所以子类和父类的 testA() 方法相互独立,不发生重写。
         * 关键字 override 压根没有看到父类的 testA() 方法,也不会检查重写规则,所以编译不报错
         * 关键字 override 虽然不报错,但这样使用非常不合理,推荐去除
         * */
        void testA() override;

        /** 修饰符只能越写越大:protected -> public */
        void testB() override;

        /** 被 final 修饰的虚函数不能被子类重写 */
        // void testC(); // error

        /** 返回值只能越写越小:Chicken* -> SubChicken* */
        SubChicken *testD() override;

        /** 返回值只能越写越小:Chicken& -> SubChicken& */
        SubChicken &testE() override;
    };

    void test();
}

#endif //V1_2_BASIC_OOP_OVERRIDE_H
// polymorphic/Override.cpp
// Created by 周航宇 on 2025/3/3.
//
#include "Override.h"
#include <iostream>

using std::cout, std::endl;

void Override::Chicken::testA() {
    cout << "Chicken testA" << endl;
}

void Override::Chicken::testB() {
    cout << "Chicken testB" << endl;
}

void Override::Chicken::testC() {
    cout << "Chicken testC" << endl;
}

Override::Chicken *Override::Chicken::testD() {
    cout << "Chicken testD" << endl;
    return nullptr;
}

Override::Chicken &Override::Chicken::testE() {
    cout << "Chicken testE" << endl;
    return *this;
}

void Override::SubChicken::testA() {
    cout << "SubChicken testA" << endl;
}

void Override::SubChicken::testB() {
    cout << "SubChicken testB" << endl;
}

Override::SubChicken *Override::SubChicken::testD() {
    cout << "SubChicken testD" << endl;
    return nullptr;
}

Override::SubChicken &Override::SubChicken::testE() {
    cout << "SubChicken testE" << endl;
    return *this;
}

void Override::test() {
    SubChicken subChicken;
    subChicken.testA();
    subChicken.testB();
    subChicken.testC();
    subChicken.testD();
    subChicken.testE();
}

E02. 动态绑定机制

1. 动态绑定写法

心法:C++ 中的动态绑定也称运行时绑定或后期绑定,是 C++ 中多态性的一个重要特性。

不绑定

  • 实例属性没有多态性,父类指针或引用调用时就指向父类,子类指针或引用调用时就指向子类。
  • 静态成员属于类而不属于实例,哪个类调用它就指向哪个类。

静态绑定:在编译阶段就绑定到父类上,无论子类如何改变,都不影响调用目标:

  • private 方法不能被重写,当通过父类指针或引用调用时只会指向父类,属于静态绑定。
  • final 方法不能被重写,当通过父类指针或引用调用时只会指向父类,属于静态绑定。

动态绑定:在运行阶段才能明确绑定目标,子类为 A 则绑定到 A 上,子类为 B 则绑定到 B 上:

  • 只有父类中声明为 virtual 的实例方法才能实现动态绑定,否则均视为静态绑定。

多态写法:即父类指针或引用指向子类实例的写法,只有使用多态写法才会发生动态绑定现象:

  • Fu *fu = new Fu;:非多态写法,fu->fn() 只会根据多态规则进行调用。
  • Zi *zi = new Zi;:非多态写法,zi->fn() 只会根据多态规则进行调用。
  • Fu *fu = new Zi;:多态写法,fu->fn() 将调用 Zi 的方法。
  • Fu &fu = new Zi;:多态写法,fu->fn() 将调用 Zi 的方法。

武技:测试多态的动态绑定机制

// polymorphic/DynamicBind.h
// Created by 周航宇 on 2025/3/3.
//
#ifndef V1_2_BASIC_OOP_DYNAMICBIND_H
#define V1_2_BASIC_OOP_DYNAMICBIND_H
#include <iostream>

namespace DynamicBind {

    class Tiger {
    public:
        std::string a = "Tiger-a";
        static std::string b;
        void testA();
        virtual void testB();
    };

    class SubTigerA : public Tiger {
    public:
        std::string a = "SubTigerA-a";
        static std::string b;
        void testA();
        void testB() override;
    };

    class SubTigerB : public Tiger {
    public:
        std::string a = "SubTigerB-b";
        static std::string b;
        void testA();
        void testB() override;
    };

    void test();
}

#endif //V1_2_BASIC_OOP_DYNAMICBIND_H
// polymorphic/DynamicBind.cpp
// Created by 周航宇 on 2025/3/3.
//
#include "DynamicBind.h"

using std::cout, std::endl, std::string;

string DynamicBind::Tiger::b = "Tiger-b";
string DynamicBind::SubTigerA::b = "SubTigerA-b";
string DynamicBind::SubTigerB::b = "SubTigerB-b";

void DynamicBind::Tiger::testA() {
    cout << "Tiger::testA()" << endl;
}

void DynamicBind::Tiger::testB() {
    cout << "Tiger::testB()" << endl;
}

void DynamicBind::SubTigerA::testA() {
    cout << "SubTigerA::testA()" << endl;
}

void DynamicBind::SubTigerA::testB() {
    cout << "SubTigerA::testB()" << endl;
}

void DynamicBind::SubTigerB::testA() {
    cout << "SubTigerB::testA()" << endl;
}

void DynamicBind::SubTigerB::testB() {
    cout << "SubTigerB::testB()" << endl;
}

void DynamicBind::test() {
    Tiger *tigerA = new SubTigerA;
    Tiger *tigerB = new SubTigerB;
    // 成员属性没有多态性,通过父类指针访问就指向父类,无论子类是谁
    cout << "成员属性 a: " << tigerA->a << endl;
    cout << "成员属性 a: " << tigerB->a << endl;
    // 静态属性属于类,哪个类调用就指向哪个类
    cout << "静态属性 b: " << Tiger::b << endl;
    cout << "静态属性 b: " << SubTigerA::b << endl;
    cout << "静态属性 b: " << SubTigerB::b << endl;
    // 非虚函数属于静态绑定,通过父类指针访问就指向父类
    tigerA->testA();
    tigerB->testA();
    // 虚函数属于动态绑定,通过父类指针访问就指向子类
    tigerA->testB();
    tigerB->testB();
    // 释放
    delete tigerA;
    delete tigerB;
}

2. typeid运算符

心法:typeid 是 C++ 中的一个运算符,它可以在运行时获取对象的类型信息,通过比较 typeid 的结果,我们可以判断一个对象是否是某个特定类型(不包含子类型)。

使用前提:父类中必须有至少一个虚函数(即使子类并未重写),这样 typeid 才能进行运行时类型识别。

相关 API 方法 描述
typeid(*A) == typeid(B) 返回 A 对象是否为 B 类型

武技:测试 typeid 运算符

// polymorphic/TypeId.h
// Created by 周航宇 on 2025/3/3.
//
#ifndef V1_2_BASIC_OOP_TYPEID_H
#define V1_2_BASIC_OOP_TYPEID_H

namespace TypeId {

    class Rabbit {
    public:
        /** 父类至少要有一个虚函数才能测试 typeid 运算符*/
        [[maybe_unused]] virtual void testA();
    };
    
    class SubRabbit : public Rabbit {};
    
    void test();
}

#endif //V1_2_BASIC_OOP_TYPEID_H
// polymorphic/TypeId.cpp
// Created by 周航宇 on 2025/3/3.
//
#include "TypeId.h"
#include <iostream>

using std::cout, std::endl;

/**
 * [[maybe_unused]] 是 C++17 引入的一个属性
 * 它的主要作用是告诉编译器某个变量、函数、类、枚举等实体可能不会被使用
 * 从而避免编译器针对未使用实体发出警告信息
 */
[[maybe_unused]] void TypeId::Rabbit::testA() {}

void TypeId::test() {
    Rabbit *p = new SubRabbit;
    cout << (typeid(*p) == typeid(SubRabbit)) << endl;
    cout << (typeid(*p) == typeid(Rabbit)) << endl;
    delete p;
}

3. 多态类型转换

心法:dynamic_cast 是一种安全的向下转型方式,即从父类指针或引用转换为子类指针或引用。

使用前提:父类中必须有至少一个虚函数(即使子类并未重写),这样 dynamic_cast 才能工作。

C 风格转换

  • 转换格式:B *b = (B*)A;
  • 安全性:转换时不进行任何检查,转换失败时返回的指针可能会导致未定义行为,例如访问非法内存等。
  • 性能:高,只是简单的类型重新解释,不涉及运行时的额外开销。

C++ 风格转换

  • 转换格式:B *b = dynamic_cast<B*>(A)
  • 安全性:转换时检查有效性,若父类变量中存放的是子类 A 的实例,则该变量仅能向下转型回子类 A,强转回子类 B 就会失败,返回 nullptr。
  • 性能:低,需要在运行时进行类型检查,会带来一定的性能开销,尤其是在复杂的继承体系中。

武技:测试多态类型转换

// polymorphic/DynamicCast.h
// Created by 周航宇 on 2025/3/3.
//
#ifndef V1_2_BASIC_OOP_DYNAMICCAST_H
#define V1_2_BASIC_OOP_DYNAMICCAST_H

namespace DynamicCast {
    class Dragon {
    public:
        /** 必须有虚函数,dynamic_cast 才能正常工作 */
        virtual void testA();
    };

    class SubDragonA : public Dragon {};
    
    class SubDragonB : public Dragon {};

    void test();
}

#endif //V1_2_BASIC_OOP_DYNAMICCAST_H
// polymorphic/DynamicCast.cpp
// Created by 周航宇 on 2025/3/3.
//
#include "DynamicCast.h"
#include <iostream>

using std::cout, std::endl;

void DynamicCast::Dragon::testA() {}

void DynamicCast::test() {
    // 由小到大自动转
    Dragon *dragon = new SubDragonA;
    // 由大到小需强转:类型匹配,强转成功
    auto *subDragonA = dynamic_cast<SubDragonA *>(dragon);
    // 由大到小需强转:类型不匹配,返回 nullptr
    auto *subDragonB = dynamic_cast<SubDragonB *>(dragon);
    cout << "subDragonA: " << (subDragonA == nullptr) << endl;
    cout << "subDragonB: " << (subDragonB == nullptr) << endl;
    delete dragon;
}

4. 虚析构函数

心法:在任何存在继承关系的类层次结构中,只要可能存在通过父类指针或引用删除子类实例的操作,都有必要使用虚析构器。

虚析构器:即被 virtual 修饰的析构器:

  • 虚析构器因为被 virtual 修饰,所以具备了虚函数的特性,能够实现运行时的动态绑定。
  • 子类的析构器会自动重写父类的虚析构器,即使名称不同,所以建议添加 override 关键字。

使用示例:假设正在通过父类指针删除子类实例 Fu *fu = new Zi; delete fu;

  • 若父类析构器不虚,则只会调用父类析构器,子类中申请的内存可能会泄漏。
  • 若父类析构器虚,则依次调用子析构器和父析构器。

武技:测试虚析构函数

// polymorphic/VirtualDestructor.h
// Created by 周航宇 on 2025/3/3.
//
#ifndef V1_2_BASIC_OOP_VIRTUALDESTRUCTOR_H
#define V1_2_BASIC_OOP_VIRTUALDESTRUCTOR_H

namespace VirtualDestructor {

    class Sheep {
    public:
        Sheep();
        /** 虚析构器 */
        virtual ~Sheep();
    };

    class SubSheep : public Sheep {
    public:
        SubSheep();
        /** 自动重写父类的析构器 */
        ~SubSheep() override;
    };

    void test();
}

#endif //V1_2_BASIC_OOP_VIRTUALDESTRUCTOR_H
// polymorphic/VirtualDestructor.cpp
// Created by 周航宇 on 2025/3/3.
//
#include "VirtualDestructor.h"
#include <iostream>

using std::cout, std::endl;

VirtualDestructor::Sheep::Sheep() {
    cout << "Sheep::Sheep" << endl;
}

VirtualDestructor::Sheep::~Sheep() {
    cout << "Sheep::~Sheep" << endl;
}

VirtualDestructor::SubSheep::SubSheep() {
    cout << "SubSheep::SubSheep" << endl;
}

VirtualDestructor::SubSheep::~SubSheep() {
    cout << "SubSheep::~SubSheep" << endl;
}

void VirtualDestructor::test() {
    Sheep *sheep = new SubSheep();
    // 通过父类指针删除子类对象
    delete sheep;
}

E03. 抽象类

心法:抽象类是一种不能被实例化的类,它主要用于为子类提供一个通用的接口,强制子类实现特定的方法,这也意味着抽象类不能被 final 修饰。

抽象类:如果一个类包含了至少一个纯虚函数,那么这个类就被认为是一个抽象类:

  • 抽象类中可以包含非纯虚函数和成员变量。
  • 抽象类拥有构造器和析构器,但是不能直接调用其构造器进行实例化。

纯虚函数:声明时赋值为零的虚函数,如 virtual void testA() = 0; 等:

  • 纯虚函数没有函数体,其具体实现由子类来重写完成,若子类仅重写了父类部分的纯虚函数,则子类也必须被视为是抽象类,同样不能实例化。

武技:测试抽象类的创建和使用

// polymorphic/AbstractClass.h
// Created by 周航宇 on 2025/3/3.
//
#ifndef V1_2_BASIC_OOP_ABSTRACTCLASS_H
#define V1_2_BASIC_OOP_ABSTRACTCLASS_H

namespace AbstractClass {

    class Horse {
    public:
        Horse();

        /** 纯虚函数 */
        virtual void testA() = 0;

        /** 虚函数 */
        virtual void testB();

        /** 普通函数 */
        void testC();

        /** 虚析构函数 */
        virtual ~Horse();
    };

    class SubHorse : public Horse {
    public:
        SubHorse();
        void testA() override;
        ~SubHorse() override;
    };

    void test();
}

#endif //V1_2_BASIC_OOP_ABSTRACTCLASS_H
// polymorphic/AbstractClass.cpp
// Created by 周航宇 on 2025/3/3.
//
#include "AbstractClass.h"
#include <iostream>

using std::cout, std::endl;

AbstractClass::Horse::Horse() {
    cout << "Horse::Horse" << endl;
}

void AbstractClass::SubHorse::testA() {
    cout << "SubHorse::testA" << endl;
}

AbstractClass::SubHorse::SubHorse() {
    cout << "SubHorse::SubHorse" << endl;
}

AbstractClass::SubHorse::~SubHorse() {
    cout << "SubHorse::~SubHorse" << endl;
}

void AbstractClass::Horse::testB() {
    cout << "Horse::testB" << endl;
}

void AbstractClass::Horse::testC() {
    cout << "Horse::testC" << endl;
}

AbstractClass::Horse::~Horse() {
    cout << "Horse::~Horse" << endl;
}

void AbstractClass::test() {
    // 抽象类不能实例化
    // Horse horse;
    Horse *horse = new SubHorse();
    horse->testA();
    horse->testB();
    horse->testC();
    delete horse;
}

C/C++道经第1卷 - 第3阶 - 面向对象


网站公告

今日签到

点亮在社区的每一天
去签到