第12章 面向对象编程简介

面向对象编程的核心概念与设计哲学

面向对象编程(Object-Oriented Programming,OOP)是一种基于对象概念的编程范式,它将数据和操作数据的方法封装在一起,组成对象,通过对象之间的交互来实现程序功能。OOP的核心思想是模拟现实世界的实体及其交互方式,使代码更加符合人类的思维模式。

面向对象编程的理论基础

OOP的理论基础源于以下几个重要概念:

  1. 抽象(Abstraction):从具体事物中提取共同的本质特征,忽略次要细节
  2. 封装(Encapsulation):将数据和操作数据的方法绑定在一起,隐藏内部实现细节
  3. 继承(Inheritance):通过层次结构复用代码和建立类型关系
  4. 多态(Polymorphism):同一接口可以有不同的实现,提高代码的灵活性和可扩展性

面向对象编程的设计原则

现代面向对象编程遵循一系列设计原则,这些原则有助于创建可维护、可扩展的代码:

  1. 单一职责原则(Single Responsibility Principle):一个类应该只有一个引起它变化的原因
  2. 开放-封闭原则(Open-Closed Principle):软件实体应该对扩展开放,对修改封闭
  3. 里氏替换原则(Liskov Substitution Principle):子类应该能够替换其父类,并且程序的行为不会改变
  4. 接口隔离原则(Interface Segregation Principle):客户端不应该依赖它不使用的接口
  5. 依赖倒置原则(Dependency Inversion Principle):高层模块不应该依赖低层模块,两者都应该依赖抽象
  6. 组合优于继承原则(Composition Over Inheritance):优先使用对象组合,而不是类继承
  7. 迪米特法则(Law of Demeter):一个对象应该对其他对象有尽可能少的了解

这些原则构成了设计模式的基础,是编写高质量面向对象代码的指南。

核心概念详解

  1. 对象(Object)

    • 现实世界中的实体在程序中的表示
    • 具有状态(State):由属性(Attributes)描述
    • 具有行为(Behavior):由方法(Methods)实现
    • 具有标识(Identity):每个对象都有唯一的标识
    • C++对象的底层实现
      • 对象在内存中是连续的内存块
      • 包含成员变量的值
      • 对于包含虚函数的类,对象还包含虚指针(vptr),指向虚函数表(vtable)
      • 对象的大小由成员变量的大小和对齐要求决定
  2. 类(Class)

    • 对象的蓝图或模板,定义了对象的属性和方法
    • 描述了一类对象的共同特征和行为
    • 在C++中,类是一种用户定义的类型
    • 类的实例化产生对象
    • C++类的底层实现
      • 类的定义在编译时被处理,生成类型信息
      • 对于包含虚函数的类,编译器生成虚函数表(vtable)
      • 成员函数在编译时被转换为普通函数,隐含传入this指针
      • 静态成员存储在全局数据区,不属于对象的一部分
  3. 封装(Encapsulation)

    • 将数据和操作数据的方法封装在一起
    • 隐藏内部实现细节,只暴露必要的接口
    • 通过访问控制修饰符(public、private、protected)实现
    • 提高代码的安全性和可维护性
    • C++封装的实现机制
      • 访问控制修饰符在编译时生效,是编译期检查
      • 私有成员在类外部不可访问,由编译器强制执行
      • 友元机制可以打破封装,但应谨慎使用
      • 封装通过信息隐藏减少了代码耦合
  4. 继承(Inheritance)

    • 从已有类(基类/父类)派生出新类(派生类/子类)
    • 子类继承父类的属性和方法
    • 可以添加新的属性和方法,或重写父类的方法
    • 建立类的层次结构,体现”is-a”关系
    • C++继承的底层实现
      • 派生类对象包含基类子对象
      • 构造函数调用顺序:基类→派生类
      • 析构函数调用顺序:派生类→基类
      • 虚函数通过虚函数表实现运行时多态
      • 多重继承可能导致菱形继承问题,需要使用虚拟继承解决
  5. 多态(Polymorphism)

    • 不同对象对同一消息做出不同的响应
    • 编译时多态:通过函数重载和运算符重载实现
    • 运行时多态:通过虚函数和继承实现
    • 提高代码的灵活性和可扩展性
    • C++多态的实现机制
      • 编译时多态:通过名称修饰(name mangling)和重载解析实现
      • 运行时多态:通过虚函数表(vtable)和虚指针(vptr)实现
      • 虚函数表存储虚函数的地址,每个类有一个虚函数表
      • 虚指针存储在对象的内存布局中,指向类的虚函数表
      • 虚函数调用通过虚指针和虚函数表间接调用,有一定的性能开销
  6. 组合(Composition)

    • 通过对象之间的包含关系实现代码重用
    • 体现”has-a”关系
    • 相比继承,组合具有更低的耦合度
    • C++组合的实现
      • 一个类包含另一个类的对象作为成员
      • 组合对象的构造函数负责初始化成员对象
      • 组合对象的析构函数负责销毁成员对象
      • 组合可以实现运行时的行为变化,而继承是编译时确定的
  7. 聚合(Aggregation)

    • 一种特殊的组合关系,表示整体与部分的关系
    • 部分可以独立于整体存在
    • C++聚合的实现
      • 通常通过指针或引用实现
      • 聚合对象不负责部分对象的生命周期
      • 部分对象可以被多个聚合对象共享
      • 聚合关系比组合关系更松散

面向对象编程的设计哲学

  1. 关注点分离(Separation of Concerns)

    • 将复杂问题分解为多个独立的关注点
    • 每个类负责解决一个特定的关注点
  2. 模块化(Modularity)

    • 将程序分解为独立的模块
    • 每个模块有明确的职责和接口
  3. 可扩展性(Extensibility)

    • 设计应易于扩展,无需修改现有代码
    • 通过继承和多态实现
  4. 可维护性(Maintainability)

    • 代码应易于理解和修改
    • 封装和模块化有助于提高可维护性
  5. 代码重用(Code Reuse)

    • 通过继承、组合和模板实现代码重用
    • 减少重复代码,提高开发效率

类和对象的深度剖析

类的定义与实现

在C++中,类是一种用户定义的类型,它封装了数据和操作数据的方法。类的定义包括成员变量和成员函数,以及访问控制修饰符。

类的基本结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 类的声明
class ClassName {
private:
// 私有成员(只能在类内部访问)
数据类型 成员变量; // 成员变量声明

public:
// 公共成员(可以在任何地方访问)
数据类型 成员变量;
返回类型 成员方法(参数列表); // 成员函数声明

protected:
// 保护成员(可以在类内部和子类中访问)
数据类型 成员变量;
返回类型 成员方法(参数列表);

public:
// 特殊成员函数
ClassName(); // 默认构造函数
ClassName(const ClassName&); // 拷贝构造函数
ClassName(ClassName&&) noexcept; // 移动构造函数
~ClassName(); // 析构函数
ClassName& operator=(const ClassName&); // 拷贝赋值运算符
ClassName& operator=(ClassName&&) noexcept; // 移动赋值运算符
};

// 类的实现(通常在.cpp文件中)
返回类型 ClassName::成员方法(参数列表) {
// 实现
}

// 内联成员函数(通常在头文件中)
inline 返回类型 ClassName::成员方法(参数列表) {
// 实现
}

类的底层实现原理

C++类的底层实现涉及以下几个关键概念:

  1. 内存布局

    • 类的成员变量在内存中是连续存储的
    • 成员函数不占用对象的内存空间,而是存储在代码段
    • 虚函数表指针(vptr)存储在对象的开头(对于包含虚函数的类)
    • 内存对齐会影响类的大小
  2. this指针

    • 每个非静态成员函数都有一个隐含的this指针参数
    • this指针指向调用该函数的对象
    • 在成员函数内部,通过this指针访问对象的成员
  3. 名称修饰(Name Mangling)

    • 编译器为了支持函数重载,会对函数名进行修饰
    • 修饰后的名称包含函数名、参数类型和类名等信息
    • 不同编译器的名称修饰规则可能不同
  4. 访问控制的实现

    • 访问控制是编译期检查,不是运行时检查
    • 编译器通过名称修饰和作用域规则实现访问控制
    • 友元机制通过特殊的名称修饰实现

C++11+类的新特性

C++11及以上版本为类添加了许多新特性:

  1. 默认成员初始化

    1
    2
    3
    4
    5
    class Person {
    private:
    std::string name = "Unknown"; // 默认初始化
    int age = 0; // 默认初始化
    };
  2. 委托构造函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class Person {
    private:
    std::string name;
    int age;
    std::string address;

    public:
    // 主构造函数
    Person(const std::string& n, int a, const std::string& addr)
    : name(n), age(a), address(addr) {}

    // 委托构造函数
    Person() : Person("Unknown", 0, "Unknown") {}
    Person(const std::string& n, int a) : Person(n, a, "Unknown") {}
    };
  3. 继承构造函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class Base {
    public:
    Base(int x) : value(x) {}
    private:
    int value;
    };

    class Derived : public Base {
    public:
    using Base::Base; // 继承基类的构造函数
    };
  4. 显式转换操作符

    1
    2
    3
    4
    5
    6
    7
    class MyInt {
    private:
    int value;
    public:
    MyInt(int v) : value(v) {}
    explicit operator int() const { return value; } // 显式转换
    };
  5. 类内枚举

    1
    2
    3
    4
    5
    6
    class Color {
    public:
    enum class Type { RED, GREEN, BLUE }; // 作用域枚举
    private:
    Type type;
    };
  6. 移动语义支持

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    class String {
    private:
    char* data;
    size_t size;
    public:
    // 移动构造函数
    String(String&& other) noexcept
    : data(other.data), size(other.size) {
    other.data = nullptr;
    other.size = 0;
    }

    // 移动赋值运算符
    String& operator=(String&& other) noexcept {
    if (this != &other) {
    delete[] data;
    data = other.data;
    size = other.size;
    other.data = nullptr;
    other.size = 0;
    }
    return *this;
    }
    };
  7. ** constexpr构造函数**:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class Point {
    private:
    int x, y;
    public:
    constexpr Point(int x, int y) : x(x), y(y) {}
    constexpr int getX() const { return x; }
    constexpr int getY() const { return y; }
    };

    // 编译期计算
    constexpr Point p(10, 20);
    constexpr int x = p.getX(); // 编译期计算
  8. 内联命名空间

    1
    2
    3
    4
    5
    6
    7
    8
    9
    namespace MyLib {
    inline namespace V2 {
    class Widget {
    // 实现
    };
    }
    }

    // 可以直接使用 MyLib::Widget

这些新特性使得C++类的设计更加灵活和强大,提高了代码的可读性和可维护性。

成员变量的初始化

成员变量的初始化是C++类设计中的重要环节,直接影响到对象的正确性和性能。

初始化方式

C++支持多种成员变量初始化方式:

  1. 类内默认初始化(C++11+)

    1
    2
    3
    4
    5
    class Person {
    private:
    std::string name = "Unknown"; // 默认初始化
    int age = 0; // 默认初始化
    };
  2. 构造函数初始化列表

    1
    2
    3
    4
    5
    6
    7
    8
    class Person {
    private:
    std::string name;
    int age;

    public:
    Person(const std::string& n, int a) : name(n), age(a) {} // 初始化列表
    };
  3. 构造函数体内赋值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class Person {
    private:
    std::string name;
    int age;

    public:
    Person(const std::string& n, int a) {
    name = n; // 赋值
    age = a; // 赋值
    }
    };

初始化顺序的重要性

成员变量的初始化顺序是由其在类中声明的顺序决定的,而不是初始化列表中的顺序:

1
2
3
4
5
6
7
8
9
class Test {
private:
int a;
int b;

public:
Test(int x) : b(x), a(b) {} // 危险:a在b之前声明,所以先初始化a
// a会被初始化为未定义值,然后b被初始化为x
};

正确的做法:确保初始化列表中的顺序与声明顺序一致,或者避免依赖其他成员变量进行初始化。

初始化方式的选择

初始化方式适用场景性能特点
类内默认初始化提供默认值代码简洁,适用于大多数成员
构造函数初始化列表依赖构造参数避免二次赋值,适用于所有成员
构造函数体内赋值复杂初始化逻辑允许复杂计算,有二次赋值开销

特殊成员的初始化

  1. const成员:必须在初始化列表中初始化

    1
    2
    3
    4
    5
    6
    7
    class Circle {
    private:
    const double radius;

    public:
    Circle(double r) : radius(r) {} // 必须使用初始化列表
    };
  2. 引用成员:必须在初始化列表中初始化

    1
    2
    3
    4
    5
    6
    7
    class ReferenceWrapper {
    private:
    int& ref;

    public:
    ReferenceWrapper(int& r) : ref(r) {} // 必须使用初始化列表
    };
  3. 成员对象:如果没有默认构造函数,必须在初始化列表中初始化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class Date {
    public:
    Date(int year, int month, int day) : year(year), month(month), day(day) {}
    private:
    int year, month, day;
    };

    class Person {
    private:
    Date birthDate; // 没有默认构造函数

    public:
    Person(int y, int m, int d) : birthDate(y, m, d) {} // 必须使用初始化列表
    };

C++11+的初始化列表增强

C++11引入了统一初始化语法,支持使用花括号进行列表初始化:

  1. 列表初始化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class Point {
    private:
    int x, y;

    public:
    Point(std::initializer_list<int> init) {
    auto it = init.begin();
    x = (it != init.end()) ? *it : 0;
    y = (++it != init.end()) ? *it : 0;
    }
    };

    // 使用
    Point p1 = {10, 20}; // 列表初始化
    Point p2{30, 40}; // 列表初始化
  2. 委托构造函数与初始化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class Person {
    private:
    std::string name;
    int age;
    std::string address;

    public:
    // 主构造函数
    Person(const std::string& n, int a, const std::string& addr)
    : name(n), age(a), address(addr) {}

    // 委托构造函数
    Person() : Person("Unknown", 0, "Unknown") {}
    Person(const std::string& n) : Person(n, 0, "Unknown") {}
    };

初始化的性能考虑

  1. 避免二次赋值:使用初始化列表而不是构造函数体内赋值,可以避免成员变量的默认构造和二次赋值

  2. 大对象的初始化:对于大对象,初始化列表可以直接构造,避免拷贝开销

  3. 常量表达式初始化:对于编译期常量,使用constexpr构造函数可以在编译期完成初始化

  4. 移动语义:在初始化列表中使用移动语义可以减少拷贝开销

    1
    2
    3
    4
    5
    6
    7
    class Person {
    private:
    std::string name;

    public:
    Person(std::string n) : name(std::move(n)) {} // 移动语义
    };

正确的成员变量初始化方式不仅能提高代码的可读性和可维护性,还能显著提升程序的性能。

构造函数的高级特性

构造函数是特殊的成员函数,用于初始化对象。C++提供了多种构造函数形式,每种形式都有其特定的用途和实现原理。

构造函数的类型与特性

  1. 默认构造函数:无参数的构造函数

    • 如果没有定义任何构造函数,编译器会生成默认构造函数
    • 使用= default可以显式要求编译器生成默认构造函数
    • 默认构造函数用于创建默认初始化的对象
  2. 带参数的构造函数:接受参数初始化对象

    • 可以有多个重载版本,根据参数类型和数量区分
    • 是最常用的构造函数形式
  3. 拷贝构造函数:通过另一个同类型对象初始化

    • 签名:ClassName(const ClassName& other)
    • 如果没有定义,编译器会生成默认拷贝构造函数
    • 执行成员逐个拷贝
  4. 移动构造函数:通过移动语义初始化对象(C++11+)

    • 签名:ClassName(ClassName&& other) noexcept
    • 接受右值引用参数
    • 移动资源而不是拷贝,提高性能
  5. 委托构造函数:一个构造函数调用同一类的其他构造函数(C++11+)

    • 减少代码重复
    • 只能在初始化列表中委托
    • 可以形成委托链,但不能循环委托
  6. 继承构造函数:继承基类的构造函数(C++11+)

    • 使用using BaseClass::BaseClass;声明
    • 继承基类的所有构造函数
    • 可以与派生类自己的构造函数共存

构造函数的底层实现

  1. 默认构造函数

    • 编译器生成的默认构造函数会调用成员变量的默认构造函数
    • 对于内置类型,不进行初始化
    • 对于类类型,递归调用其默认构造函数
  2. 拷贝构造函数

    • 编译器生成的拷贝构造函数会逐个拷贝成员变量
    • 对于类类型成员,调用其拷贝构造函数
    • 对于内置类型成员,直接拷贝值
  3. 移动构造函数

    • 编译器生成的移动构造函数会逐个移动成员变量
    • 对于类类型成员,调用其移动构造函数
    • 对于内置类型成员,直接拷贝值(因为内置类型没有移动语义)
  4. 委托构造函数

    • 底层实现类似于函数调用
    • 先调用目标构造函数初始化对象
    • 然后执行当前构造函数的函数体
  5. 继承构造函数

    • 编译器会为每个继承的构造函数生成一个派生类构造函数
    • 生成的构造函数会调用相应的基类构造函数
    • 不会初始化派生类的额外成员

构造函数的异常安全

构造函数可能会抛出异常,这会影响对象的初始化状态和内存管理:

  1. 异常传播

    • 构造函数抛出异常时,对象不会被完全构造
    • 已构造的成员会被析构
    • 不会执行析构函数
  2. 异常安全的构造函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    class SafeClass {
    private:
    std::string* data;
    int size;

    public:
    SafeClass(int s) : size(s), data(nullptr) {
    try {
    data = new std::string[s]; // 可能抛出异常
    } catch (...) {
    // 清理已分配的资源
    delete[] data;
    throw; // 重新抛出异常
    }
    }

    ~SafeClass() {
    delete[] data;
    }
    };
  3. 使用智能指针提高异常安全性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class SafeClass {
    private:
    std::unique_ptr<std::string[]> data;
    int size;

    public:
    SafeClass(int s) : size(s), data(new std::string[s]) {
    // 即使抛出异常,unique_ptr也会自动释放资源
    }
    // 不需要手动析构函数
    };

构造函数的性能优化

  1. 使用初始化列表

    • 避免成员变量的默认构造和二次赋值
    • 对于大对象,直接构造而不是拷贝
  2. 移动语义

    • 在构造函数中使用移动语义减少拷贝开销
    • 对于临时对象,优先使用移动构造
  3. ** constexpr构造函数**:

    • 对于编译期常量,使用constexpr构造函数
    • 允许在编译期完成初始化
  4. inline构造函数

    • 对于简单的构造函数,使用inline关键字
    • 减少函数调用开销
  5. 小对象优化

    • 对于小对象,考虑内联存储而不是堆分配
    • 减少内存分配开销

构造函数的实际应用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
class Person {
private:
std::string name;
int age;
std::string address;

public:
// 默认构造函数
Person() : Person("Unknown", 0, "Unknown") {} // 委托构造函数

// 带参数的构造函数
Person(const std::string& n, int a) : Person(n, a, "Unknown") {} // 委托构造函数

// 主构造函数
Person(const std::string& n, int a, const std::string& addr)
: name(n), age(a), address(addr) {
std::cout << "Person constructed: " << name << std::endl;
}

// 拷贝构造函数
Person(const Person& other)
: name(other.name), age(other.age), address(other.address) {
std::cout << "Person copied: " << name << std::endl;
}

// 移动构造函数
Person(Person&& other) noexcept
: name(std::move(other.name)),
age(std::move(other.age)),
address(std::move(other.address)) {
std::cout << "Person moved: " << name << std::endl;
}

// 析构函数
~Person() {
std::cout << "Person destructed: " << name << std::endl;
}

// 成员方法
void setName(const std::string& n) {
name = n;
}

std::string getName() const {
return name;
}

void setAge(int a) {
age = a;
}

int getAge() const {
return age;
}

void setAddress(const std::string& addr) {
address = addr;
}

std::string getAddress() const {
return address;
}

void introduce() const {
std::cout << "Hello, my name is " << name
<< ", I am " << age << " years old, "
<< "and I live at " << address << "." << std::endl;
}
};

// 继承构造函数示例
class Employee : public Person {
private:
std::string employeeId;
double salary;

public:
// 继承Person的构造函数
using Person::Person;

// 额外的构造函数
Employee(const std::string& n, int a, const std::string& addr,
const std::string& id, double s)
: Person(n, a, addr), employeeId(id), salary(s) {
std::cout << "Employee constructed: " << n << std::endl;
}

void getEmployeeInfo() const {
introduce();
std::cout << "Employee ID: " << employeeId << std::endl;
std::cout << "Salary: $" << salary << std::endl;
}
};

构造函数的最佳实践

  1. 使用初始化列表:优先使用初始化列表初始化成员变量,而不是构造函数体内赋值

  2. 避免在构造函数中执行复杂操作:构造函数应该只负责初始化,复杂操作应该移到其他方法中

  3. 使用委托构造函数减少代码重复:当多个构造函数有共同的初始化逻辑时,使用委托构造函数

  4. 正确处理异常:构造函数抛出异常时,确保已分配的资源被正确释放

  5. 使用移动语义提高性能:对于大对象,提供移动构造函数以提高性能

  6. 显式声明特殊构造函数:根据需要显式声明或删除拷贝构造函数和移动构造函数

  7. 使用智能指针管理资源:减少内存泄漏的风险,提高异常安全性

  8. 遵循零开销原则:构造函数的开销应该与所完成的工作相匹配,避免不必要的开销

对象的创建和使用

对象的创建和使用是C++编程中的核心操作,涉及到内存管理、生命周期控制和性能优化等多个方面。

对象的存储期

C++中的对象有四种存储期:

  1. 自动存储期(Automatic Storage Duration)

    • 在函数内部定义的对象(除了static修饰的)
    • 存储在栈上
    • 作用域结束时自动销毁
    • 例如:Person person1("Alice", 25);
  2. 静态存储期(Static Storage Duration)

    • 使用static修饰的对象
    • 存储在全局数据区
    • 程序启动时创建,程序结束时销毁
    • 例如:static Person person2("Bob", 30);
  3. 动态存储期(Dynamic Storage Duration)

    • 使用new运算符创建的对象
    • 存储在堆上
    • 需要手动使用delete运算符销毁
    • 例如:Person* person3 = new Person("Charlie", 35);
  4. 线程存储期(Thread Storage Duration)

    • 使用thread_local修饰的对象
    • 每个线程有一个独立的实例
    • 线程启动时创建,线程结束时销毁
    • 例如:thread_local Person person4("David", 40);

对象的内存布局

对象在内存中的布局取决于其成员变量和继承关系:

  1. 简单对象的内存布局

    • 成员变量按声明顺序存储
    • 考虑内存对齐
    • 不包含成员函数的空间
  2. 包含虚函数的对象

    • 开头存储虚函数表指针(vptr)
    • 然后存储成员变量
    • 虚函数表指针指向类的虚函数表(vtable)
  3. 继承关系中的内存布局

    • 基类子对象在前
    • 派生类成员在后
    • 多重继承会有多个虚函数表指针

对象的生命周期管理

  1. 手动管理

    • 使用new/delete手动管理动态对象
    • 容易出现内存泄漏和悬挂指针问题
  2. 智能指针管理

    • std::unique_ptr:独占所有权
    • std::shared_ptr:共享所有权
    • std::weak_ptr:非拥有式引用
    • 自动管理内存,避免内存泄漏
  3. RAII原则

    • 资源获取即初始化
    • 使用对象管理资源
    • 确保资源在作用域结束时被正确释放

对象的创建和使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 对象的创建和使用
int main() {
// 1. 栈上创建对象(自动存储期)
Person person1("Alice", 25, "123 Main St");
person1.introduce();

// 2. 堆上创建对象(动态存储期)
Person* person2 = new Person("Bob", 30, "456 Oak Ave");
person2->introduce();
delete person2; // 必须手动释放

// 3. 临时对象(临时存储期)
Person("Charlie", 35, "789 Pine Rd").introduce();

// 4. 使用拷贝构造函数
Person person3 = person1; // 拷贝构造
person3.setName("David");
person3.introduce();

// 5. 使用移动构造函数
Person person4 = std::move(person3); // 移动构造
person4.introduce();
person3.introduce(); // 注意:person3现在处于有效但未指定的状态

// 6. 使用默认构造函数
Person person5;
person5.introduce();

// 7. 使用智能指针管理对象
std::unique_ptr<Person> person6 = std::make_unique<Person>("Eve", 40, "101 Cedar Ln");
person6->introduce();

std::shared_ptr<Person> person7 = std::make_shared<Person>("Frank", 45, "202 Birch St");
std::shared_ptr<Person> person8 = person7; // 共享所有权
person7->introduce();
person8->introduce();

return 0;
}

对象操作的性能考虑

  1. 栈 vs 堆

    • 栈上创建和销毁对象的速度更快
    • 堆上创建的对象可以有更长的生命周期
    • 栈空间有限,不适合创建大对象
  2. 拷贝 vs 移动

    • 拷贝操作会创建新对象,开销较大
    • 移动操作只是转移资源所有权,开销较小
    • 对于大对象,优先使用移动语义
  3. 临时对象优化

    • 编译器会进行返回值优化(RVO)和命名返回值优化(NRVO)
    • 减少临时对象的创建和销毁开销
    • C++17引入了保证拷贝省略(Guaranteed Copy Elision)
  4. 对象池

    • 对于频繁创建和销毁的对象,使用对象池
    • 预先分配对象,减少内存分配开销
    • 适用于游戏、服务器等高性能场景

对象的高级使用技巧

  1. 对象的序列化和反序列化

    • 将对象转换为字节流存储或传输
    • 从字节流恢复对象
    • 用于文件存储、网络传输等
  2. 对象的克隆

    • 深拷贝:创建完全独立的对象副本
    • 浅拷贝:只拷贝对象的引用
    • 原型模式:通过克隆创建对象
  3. 对象的比较

    • 重载==和!=运算符
    • 实现比较操作符<, >, <=, >=
    • 支持标准库算法的使用
  4. 对象的哈希

    • 为对象提供哈希函数
    • 支持在unordered_map、unordered_set等容器中使用
    • 重载std::hash特化

智能指针的高级应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// 智能指针的高级应用
#include <memory>
#include <vector>

class Resource {
public:
Resource() { std::cout << "Resource acquired" << std::endl; }
~Resource() { std::cout << "Resource released" << std::endl; }
void use() { std::cout << "Resource used" << std::endl; }
};

// 自定义删除器
auto customDeleter = [](Resource* r) {
std::cout << "Custom deleter called" << std::endl;
delete r;
};

void smartPointerExample() {
// unique_ptr with custom deleter
std::unique_ptr<Resource, decltype(customDeleter)> res1(new Resource(), customDeleter);
res1->use();

// shared_ptr with custom deleter
std::shared_ptr<Resource> res2(new Resource(), customDeleter);
std::shared_ptr<Resource> res3 = res2;
res2->use();
res3->use();

// weak_ptr to avoid circular references
struct Node {
int value;
std::shared_ptr<Node> next;
std::weak_ptr<Node> prev; // 使用weak_ptr避免循环引用

Node(int v) : value(v) {}
~Node() { std::cout << "Node destroyed: " << value << std::endl; }
};

auto node1 = std::make_shared<Node>(1);
auto node2 = std::make_shared<Node>(2);
node1->next = node2;
node2->prev = node1;

// 没有循环引用,节点会被正确销毁
}

// 对象池示例
class ObjectPool {
private:
std::vector<std::unique_ptr<Resource>> objects;
std::vector<Resource* > freeObjects;

public:
ObjectPool(size_t size) {
objects.reserve(size);
for (size_t i = 0; i < size; ++i) {
objects.push_back(std::make_unique<Resource>());
freeObjects.push_back(objects.back().get());
}
}

Resource* acquire() {
if (freeObjects.empty()) {
return nullptr;
}
Resource* res = freeObjects.back();
freeObjects.pop_back();
return res;
}

void release(Resource* res) {
freeObjects.push_back(res);
}
};

对象创建和使用的最佳实践

  1. 优先使用栈对象:对于作用域内的对象,优先使用栈分配

  2. 使用智能指针管理堆对象:避免内存泄漏和悬挂指针

  3. 合理使用移动语义:对于大对象,使用移动构造和移动赋值

  4. 考虑对象的生命周期:明确对象的创建和销毁时机

  5. 优化对象的内存布局:合理安排成员变量顺序,减少内存对齐开销

  6. 使用对象池:对于频繁创建和销毁的对象,使用对象池提高性能

  7. 遵循RAII原则:使用对象管理资源,确保资源的正确释放

  8. 注意异常安全:确保对象在异常情况下也能正确管理资源

成员函数的const限定符

成员函数的const限定符是C++中实现常量正确性(Const Correctness)的重要机制,它确保了对象在不同上下文中的正确使用。

const成员函数的基本概念

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Person {
// ...
public:
// const成员函数:不会修改对象状态
std::string getName() const {
return name;
}

// 非const成员函数:可以修改对象状态
void setName(const std::string& n) {
name = n;
}

// const成员函数只能调用其他const成员函数
void printInfo() const {
std::cout << "Name: " << getName() << std::endl; // 允许
// setName("New Name"); // 错误:不能在const成员函数中调用非const成员函数
}
// ...
};

const成员函数的底层实现

  1. this指针的类型

    • 在非const成员函数中,this指针的类型是ClassName* const
    • 在const成员函数中,this指针的类型是const ClassName* const
    • 这意味着在const成员函数中,不能修改this指针指向的对象
  2. const成员函数的重载

    • const和非const成员函数可以构成重载
    • 非const对象调用非const版本
    • const对象调用const版本
  3. const成员函数的内存布局

    • const成员函数和非const成员函数在内存中的存储方式相同
    • const限定符是编译期检查,不影响运行时内存布局

mutable关键字

mutable关键字允许在const成员函数中修改特定的成员变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Counter {
private:
mutable int accessCount; // 可以在const成员函数中修改
int value;

public:
Counter(int v) : value(v), accessCount(0) {}

int getValue() const {
accessCount++; // 允许修改mutable成员
return value;
}

void setValue(int v) {
value = v;
accessCount = 0;
}

int getAccessCount() const {
return accessCount;
}
};

const的传递性

const限定符具有传递性,影响函数参数、返回值和成员函数调用:

  1. const参数

    • 表示函数不会修改参数
    • 可以接受const和非const实参
  2. const返回值

    • 表示函数返回的对象是常量
    • 防止返回值被意外修改
  3. const成员函数链

    • const成员函数只能调用其他const成员函数
    • 确保整个调用链都不会修改对象状态

const成员函数的高级应用

  1. const与引用返回

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    class StringView {
    private:
    const char* data;
    size_t length;

    public:
    StringView(const char* str) : data(str), length(strlen(str)) {}

    // 返回const引用,防止修改原始数据
    const char& operator[](size_t index) const {
    if (index >= length) {
    throw std::out_of_range("Index out of range");
    }
    return data[index];
    }

    size_t size() const {
    return length;
    }
    };
  2. const与运算符重载

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    class Complex {
    private:
    double real;
    double imag;

    public:
    Complex(double r = 0, double i = 0) : real(r), imag(i) {}

    // const成员函数重载运算符+
    Complex operator+(const Complex& other) const {
    return Complex(real + other.real, imag + other.imag);
    }

    // const成员函数重载运算符==
    bool operator==(const Complex& other) const {
    return real == other.real && imag == other.imag;
    }

    // 非const成员函数重载运算符=
    Complex& operator=(const Complex& other) {
    if (this != &other) {
    real = other.real;
    imag = other.imag;
    }
    return *this;
    }
    };
  3. const与智能指针

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    class Data {
    private:
    int value;

    public:
    Data(int v) : value(v) {}

    int getValue() const {
    return value;
    }

    void setValue(int v) {
    value = v;
    }
    };

    // const智能指针
    const std::unique_ptr<Data> constPtr = std::make_unique<Data>(42);
    // constPtr->setValue(100); // 错误:constPtr是const的
    std::cout << constPtr->getValue() << std::endl; // 允许

    // 指向const对象的智能指针
    std::unique_ptr<const Data> ptrToConst = std::make_unique<Data>(42);
    // ptrToConst->setValue(100); // 错误:指向的对象是const的
    std::cout << ptrToConst->getValue() << std::endl; // 允许

常量正确性的最佳实践

  1. 尽可能使用const

    • 对于不修改对象状态的成员函数,添加const限定符
    • 对于不修改参数的函数参数,添加const限定符
    • 对于不修改返回值的函数,考虑返回const值
  2. const与重载

    • 为const和非const对象提供适当的重载版本
    • 确保const版本和非const版本的行为一致
  3. 合理使用mutable

    • 只对真正需要在const成员函数中修改的成员变量使用mutable
    • 通常用于缓存、计数等不影响对象逻辑状态的成员
  4. const与引用传递

    • 对于大对象,优先使用const引用传递
    • 避免不必要的拷贝,同时确保对象不被修改
  5. const与移动语义

    • 注意const对象不能被移动,只能被拷贝
    • 移动语义需要修改源对象,与const语义冲突

const成员函数的性能考虑

  1. 编译器优化

    • const成员函数提供了更强的不变性保证
    • 编译器可以进行更多的优化,如常量折叠、函数内联等
  2. 线程安全

    • const成员函数通常是线程安全的(如果没有修改mutable成员)
    • 多个线程可以同时调用const成员函数
  3. 代码可读性

    • const限定符明确了函数的行为
    • 提高了代码的可读性和可维护性

const的编译期检查

const限定符是编译期检查,不是运行时检查:

  1. 编译错误

    • 在const成员函数中修改非mutable成员会导致编译错误
    • 调用非const成员函数会导致编译错误
  2. const_cast

    • 可以使用const_cast移除const限定符
    • 但这是不安全的,应该避免使用
    • 只有在处理旧代码或第三方库时才考虑使用
1
2
3
4
5
6
7
8
9
10
11
12
class Test {
private:
int value;
public:
Test(int v) : value(v) {}

int getValue() const {
// 不安全的做法,应避免
const_cast<Test*>(this)->value = 100;
return value;
}
};

常量正确性是C++编程中的重要概念,它通过编译期检查确保了对象的正确使用,提高了代码的安全性、可读性和可维护性。

this指针

this指针是C++中一个特殊的隐含指针,它在非静态成员函数中指向调用该函数的对象。理解this指针的工作原理对于掌握C++的面向对象编程至关重要。

this指针的基本概念

每个非静态成员函数都有一个隐含的this指针,指向调用该函数的对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Person {
// ...
public:
void setName(const std::string& name) {
this->name = name; // this指针指向当前对象
}

Person& setAge(int age) {
this->age = age;
return *this; // 返回当前对象的引用,支持链式调用
}
// ...
};

// 使用链式调用
person1.setName("Alice").setAge(25);

this指针的底层实现

  1. this指针的传递

    • this指针是作为隐藏参数传递给非静态成员函数的
    • 在大多数编译器中,this指针通过寄存器传递(如x86中的ECX寄存器)
    • 对于虚函数,this指针的传递机制更加复杂
  2. this指针的类型

    • 在非const成员函数中,this的类型是ClassName* const
    • 在const成员函数中,this的类型是const ClassName* const
    • 在volatile成员函数中,this的类型是volatile ClassName* const
    • 在const volatile成员函数中,this的类型是const volatile ClassName* const
  3. this指针的内存位置

    • this指针存储在栈上或寄存器中,不是对象的一部分
    • 对象本身不包含this指针,因此不会增加对象的大小
    • this指针的值是对象的内存地址

this指针的优化

  1. 编译器优化

    • 编译器会对this指针的访问进行优化,如内联展开
    • 对于简单的成员函数,this指针的开销可以忽略不计
    • 在某些情况下,编译器可以完全消除this指针的使用
  2. this指针的空值检查

    • 在成员函数中,this指针通常不应该为nullptr
    • 但是,通过显式调用或指针操作,可能会出现this指针为nullptr的情况
    • 成员函数在this指针为nullptr时调用会导致未定义行为
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Test {
public:
void foo() {
if (this == nullptr) {
std::cout << "this is nullptr!" << std::endl;
return;
}
std::cout << "this is not nullptr" << std::endl;
}
};

// 危险:未定义行为
Test* ptr = nullptr;
ptr->foo(); // 可能输出"this is nullptr!",但行为未定义

this指针在继承和多态中的应用

  1. 继承中的this指针

    • 在派生类的成员函数中,this指针的类型是派生类类型
    • 但是,通过类型转换,可以将this指针转换为基类类型
    • 在基类的成员函数中,this指针的类型是基类类型
  2. 多态中的this指针

    • 在虚函数调用中,this指针指向实际的对象类型
    • 虚函数表的查找基于this指针指向的对象类型
    • 这使得多态调用能够正确地调用派生类的实现
  3. this指针的类型转换

    • 在继承层次结构中,可以使用static_castdynamic_cast转换this指针的类型
    • static_cast用于已知的类型转换
    • dynamic_cast用于运行时类型检查
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Base {
public:
virtual void print() {
std::cout << "Base::print()" << std::endl;
}
};

class Derived : public Base {
public:
void print() override {
std::cout << "Derived::print()" << std::endl;
}

void derivedOnly() {
std::cout << "Derived::derivedOnly()" << std::endl;
}

void callBasePrint() {
// 通过this指针调用基类的print方法
Base::print();
}
};

void testPolymorphism() {
Derived d;
Base* b = &d;

b->print(); // 调用Derived::print(),多态

// 通过类型转换访问派生类特有的方法
if (Derived* dp = dynamic_cast<Derived*>(b)) {
dp->derivedOnly(); // 安全调用派生类方法
}
}

this指针的高级应用

  1. 返回*this实现链式调用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    class Calculator {
    private:
    int value;

    public:
    Calculator(int v = 0) : value(v) {}

    Calculator& add(int x) {
    value += x;
    return *this;
    }

    Calculator& subtract(int x) {
    value -= x;
    return *this;
    }

    Calculator& multiply(int x) {
    value *= x;
    return *this;
    }

    int getValue() const {
    return value;
    }
    };

    // 使用链式调用
    Calculator calc;
    int result = calc.add(5).subtract(2).multiply(3).getValue(); // 结果:(5-2)*3=9
  2. this指针与运算符重载

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    class Vector2D {
    private:
    double x, y;

    public:
    Vector2D(double x = 0, double y = 0) : x(x), y(y) {}

    // 重载+=运算符
    Vector2D& operator+=(const Vector2D& other) {
    x += other.x;
    y += other.y;
    return *this;
    }

    // 重载+运算符(基于+=)
    Vector2D operator+(const Vector2D& other) const {
    Vector2D result = *this;
    result += other;
    return result;
    }

    // 重载=运算符
    Vector2D& operator=(const Vector2D& other) {
    if (this != &other) { // 自我赋值检查
    x = other.x;
    y = other.y;
    }
    return *this;
    }
    };
  3. this指针与模板

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    template <typename T>
    class SmartPtr {
    private:
    T* ptr;

    public:
    SmartPtr(T* p) : ptr(p) {}
    ~SmartPtr() { delete ptr; }

    T& operator*() const {
    return *ptr;
    }

    T* operator->() const {
    return ptr;
    }

    // 检查两个SmartPtr是否指向同一个对象
    bool operator==(const SmartPtr& other) const {
    return ptr == other.ptr;
    }
    };

this指针的最佳实践

  1. 明确使用this指针

    • 当成员变量与函数参数同名时,使用this->明确访问成员变量
    • 提高代码的可读性,避免命名冲突
  2. 链式调用

    • 对于修改对象状态的成员函数,返回*this以支持链式调用
    • 提高代码的简洁性和表达力
  3. 自我赋值检查

    • 在赋值运算符和拷贝构造函数中,检查自我赋值
    • 避免不必要的操作和潜在的错误
  4. 谨慎使用this指针

    • 不要在构造函数中使用this指针注册对象(可能导致未完全构造的对象被使用)
    • 不要在析构函数中使用this指针调用虚函数(虚函数表可能已被销毁)
    • 避免将this指针存储在全局或静态变量中(可能导致悬空指针)
  5. this指针与线程安全

    • this指针本身不是线程安全的
    • 在多线程环境中,需要确保对this指向对象的访问是线程安全的
    • 考虑使用互斥锁或其他同步机制保护对象状态

this指针的性能影响

  1. 内存开销

    • this指针不增加对象的大小,它是一个隐含的参数
    • 在32位系统中,this指针占用4字节;在64位系统中,占用8字节
    • 但是,this指针的传递和使用会产生一定的运行时开销
  2. 缓存局部性

    • this指针指向的对象通常在内存中是连续存储的
    • 这有助于提高缓存局部性,减少缓存未命中
    • 成员变量的访问通常比全局变量或堆变量的访问更快
  3. 编译器优化

    • 现代编译器会对this指针的使用进行大量优化
    • 包括内联展开、常量传播、死代码消除等
    • 在大多数情况下,this指针的开销可以忽略不计

this指针是C++面向对象编程的核心概念之一,它提供了一种在成员函数中访问调用对象的机制。正确理解和使用this指针,对于编写高效、安全、可维护的C++代码至关重要。

静态成员

静态成员是C++中一种特殊的成员,它属于类而不是对象,所有对象共享同一个静态成员。静态成员在面向对象编程中有着广泛的应用,如计数器、配置信息、单例模式等。

静态成员的基本概念

静态成员属于类而不是对象,所有对象共享同一个静态成员:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Person {
private:
std::string name;
int age;
static int count; // 静态成员变量声明

public:
Person(const std::string& n, int a) : name(n), age(a) {
++count; // 构造时增加计数
}

~Person() {
--count; // 析构时减少计数
}

// 静态成员函数:只能访问静态成员
static int getCount() {
return count;
}

// ...
};

// 静态成员变量定义(必须在类外部)
int Person::count = 0;

// 使用静态成员
std::cout << "Person count: " << Person::getCount() << std::endl;

静态成员的底层实现

  1. 静态成员变量的存储

    • 静态成员变量存储在全局数据区,而不是对象的内存空间中
    • 它们在程序启动时分配内存,程序结束时释放内存
    • 所有对象共享同一个静态成员变量的实例
  2. 静态成员函数的实现

    • 静态成员函数不包含this指针
    • 它们在内存中只有一份拷贝,与普通函数类似
    • 静态成员函数不能访问非静态成员,因为没有this指针
  3. 静态成员的初始化

    • 静态成员变量必须在类外部定义和初始化
    • 初始化顺序在同一个编译单元内是确定的,但在不同编译单元间是不确定的
    • C++17引入了内联静态成员变量,可以在类内部初始化

静态成员的高级特性

  1. 内联静态成员变量(C++17+)

    1
    2
    3
    4
    5
    6
    class Config {
    public:
    // C++17+:内联静态成员变量,可以在类内部初始化
    inline static const std::string version = "1.0.0";
    inline static const int maxConnections = 100;
    };
  2. 静态成员与模板

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    template <typename T>
    class Array {
    private:
    static size_t objectCount;
    T* data;
    size_t size;

    public:
    Array(size_t s) : size(s), data(new T[s]) {
    ++objectCount;
    }

    ~Array() {
    delete[] data;
    --objectCount;
    }

    static size_t getObjectCount() {
    return objectCount;
    }
    };

    // 为每个模板实例化定义静态成员变量
    template <typename T>
    size_t Array<T>::objectCount = 0;
  3. 静态成员与单例模式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    class Singleton {
    private:
    static Singleton* instance;
    int value;

    // 私有构造函数,防止外部创建实例
    Singleton() : value(0) {}

    // 防止拷贝和赋值
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    public:
    static Singleton* getInstance() {
    if (instance == nullptr) {
    instance = new Singleton();
    }
    return instance;
    }

    static void destroyInstance() {
    delete instance;
    instance = nullptr;
    }

    void setValue(int v) {
    value = v;
    }

    int getValue() const {
    return value;
    }
    };

    // 初始化静态成员变量
    Singleton* Singleton::instance = nullptr;
  4. Meyer’s 单例模式(线程安全)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    class Singleton {
    private:
    int value;

    // 私有构造函数
    Singleton() : value(0) {}

    public:
    // 静态成员函数返回静态局部变量
    static Singleton& getInstance() {
    static Singleton instance; // C++11+保证线程安全的初始化
    return instance;
    }

    void setValue(int v) {
    value = v;
    }

    int getValue() const {
    return value;
    }

    // 防止拷贝和赋值
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
    };

静态成员的线程安全性

  1. 静态成员变量的线程安全问题

    • 多个线程同时访问和修改静态成员变量可能导致竞态条件
    • 需要使用互斥锁或其他同步机制来保护
  2. 线程安全的静态成员

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    #include <mutex>

    class ThreadSafeCounter {
    private:
    static int count;
    static std::mutex mtx;

    public:
    ThreadSafeCounter() {
    std::lock_guard<std::mutex> lock(mtx);
    ++count;
    }

    ~ThreadSafeCounter() {
    std::lock_guard<std::mutex> lock(mtx);
    --count;
    }

    static int getCount() {
    std::lock_guard<std::mutex> lock(mtx);
    return count;
    }
    };

    // 初始化静态成员
    int ThreadSafeCounter::count = 0;
    std::mutex ThreadSafeCounter::mtx;
  3. C++11+的线程安全初始化

    • C++11标准保证静态局部变量的初始化是线程安全的
    • 这使得Meyer’s单例模式成为实现线程安全单例的最佳选择

静态成员的性能考虑

  1. 内存使用

    • 静态成员变量在全局数据区只存储一份,节省内存
    • 对于频繁使用的常量,使用静态成员可以避免重复创建
  2. 访问速度

    • 静态成员变量的访问速度与全局变量相当,比成员变量稍快
    • 静态成员函数的调用速度与普通函数相当,比成员函数稍快(因为没有this指针)
  3. 初始化开销

    • 静态成员变量在程序启动时初始化,增加了程序的启动时间
    • 对于大型静态对象,初始化开销可能较大

静态成员的最佳实践

  1. 合理使用静态成员

    • 对于所有对象共享的数据,使用静态成员变量
    • 对于与对象状态无关的操作,使用静态成员函数
    • 避免过度使用静态成员,以免破坏面向对象的封装性
  2. 静态成员的命名约定

    • 使用大写字母或特定前缀标识静态成员
    • 提高代码的可读性和可维护性
  3. 静态成员的初始化

    • 确保静态成员变量在使用前正确初始化
    • 对于依赖于其他静态成员的情况,注意初始化顺序
    • 优先使用C++17的内联静态成员变量,简化代码
  4. 静态成员与继承

    • 静态成员在继承层次结构中是共享的
    • 派生类可以访问基类的静态成员
    • 派生类可以隐藏基类的静态成员,但不能覆盖它们
  5. 静态成员的测试

    • 静态成员的状态会在测试之间保持
    • 在单元测试中,需要确保静态成员的状态不会影响其他测试
    • 考虑在测试前后重置静态成员的状态

静态成员的常见应用场景

  1. 计数器:跟踪类的实例数量
  2. 配置信息:存储应用程序的配置参数
  3. 常量定义:定义类级别的常量
  4. 单例模式:确保类只有一个实例
  5. 工厂模式:创建对象的工厂方法
  6. 工具函数:与对象状态无关的工具方法
  7. 缓存:存储共享的缓存数据
  8. 全局状态:管理应用程序的全局状态

静态成员是C++中一种强大的特性,它提供了一种在类级别共享数据和行为的机制。正确使用静态成员可以提高代码的效率和可维护性,但过度使用可能会导致代码难以理解和测试。在实际编程中,应该根据具体情况权衡使用静态成员的利弊。

内联函数

内联函数可以减少函数调用的开销,适用于短小的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Person {
// ...
public:
// 内联成员函数
inline std::string getName() const {
return name;
}

// 隐式内联(类定义中的函数默认内联)
void setName(const std::string& n) {
name = n;
}
// ...
};

类的大小

类的大小由其成员变量决定,成员函数不占用对象空间:

1
2
3
4
5
6
7
8
9
10
11
12
class EmptyClass {}; // 空类的大小为1

class Person {
private:
char c; // 1字节
int i; // 4字节
double d; // 8字节
// 由于内存对齐,总大小为16字节
};

std::cout << "Size of EmptyClass: " << sizeof(EmptyClass) << std::endl;
std::cout << "Size of Person: " << sizeof(Person) << std::endl;

类的前向声明

当两个类相互引用时,可以使用前向声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 前向声明
class B;

class A {
private:
B* b; // 只能使用指针或引用
public:
A();
~A();
void setB(B* b);
};

class B {
private:
A* a;
public:
B();
~B();
void setA(A* a);
};

// 实现
A::A() : b(nullptr) {}
A::~A() {}
void A::setB(B* b) { this->b = b; }

B::B() : a(nullptr) {}
B::~B() {}
void B::setA(A* a) { this->a = a; }

封装:数据隐藏与接口设计

封装的概念与原则

封装是面向对象编程的核心原则之一,它将数据和操作数据的方法捆绑在一起,形成一个独立的单元(类),并通过访问控制机制隐藏内部实现细节,只暴露必要的接口给外部使用。

封装的核心原则

  • 信息隐藏:隐藏对象的内部状态和实现细节
  • 接口分离:只暴露必要的公共接口
  • 数据保护:防止外部代码直接修改对象的内部状态
  • 实现隔离:内部实现的变化不影响外部代码

访问控制的深度分析

C++提供了三种访问控制修饰符,用于控制类成员的可访问性:

访问修饰符类内部子类内部类外部说明
private只能在类内部访问
protected可以在类内部和子类中访问
public可以在任何地方访问

访问控制的高级应用

  1. 友元机制:打破封装的限制

    友元可以访问类的私有和保护成员,但应谨慎使用,因为它会破坏封装性:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    class BankAccount {
    private:
    std::string accountNumber;
    double balance;

    // 友元函数
    friend void auditAccount(const BankAccount& account);

    // 友元类
    friend class BankManager;

    public:
    BankAccount(const std::string& accNum, double initialBalance)
    : accountNumber(accNum), balance(initialBalance) {}

    // 公共接口
    void deposit(double amount);
    void withdraw(double amount);
    double getBalance() const;
    std::string getAccountNumber() const;
    };

    // 友元函数实现
    void auditAccount(const BankAccount& account) {
    std::cout << "Auditing account: " << account.accountNumber << std::endl;
    std::cout << "Balance: $" << account.balance << std::endl;
    }

    // 友元类
    class BankManager {
    public:
    void freezeAccount(BankAccount& account) {
    // 可以访问私有成员
    std::cout << "Freezing account: " << account.accountNumber << std::endl;
    }
    };
  2. 类成员的访问控制

    • 成员变量:通常设为private,通过公共方法访问
    • 成员函数:根据需要设置为publicprotectedprivate
    • 构造函数:通常设为public,但单例模式中设为private
    • 析构函数:通常设为public,但在某些情况下设为protected
  3. 继承中的访问控制

    继承时的访问控制修饰符会影响基类成员在派生类中的可访问性:

    基类成员公有继承保护继承私有继承
    publicpublicprotectedprivate
    protectedprotectedprotectedprivate
    private不可访问不可访问不可访问

封装的实现技巧

  1. 属性访问器模式

    使用getter和setter方法访问和修改私有成员,提供额外的控制和验证:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    class Person {
    private:
    std::string name;
    int age;

    public:
    // Getter方法
    const std::string& getName() const {
    return name;
    }

    int getAge() const {
    return age;
    }

    // Setter方法(带验证)
    void setName(const std::string& newName) {
    if (newName.empty()) {
    throw std::invalid_argument("Name cannot be empty");
    }
    name = newName;
    }

    void setAge(int newAge) {
    if (newAge < 0 || newAge > 150) {
    throw std::invalid_argument("Age must be between 0 and 150");
    }
    age = newAge;
    }
    };
  2. 不可变对象

    通过只提供getter方法,不提供setter方法,创建不可变对象:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    class ImmutablePerson {
    private:
    std::string name;
    int age;

    public:
    // 构造时初始化,之后不可修改
    ImmutablePerson(const std::string& n, int a) : name(n), age(a) {}

    // 只提供getter方法
    const std::string& getName() const {
    return name;
    }

    int getAge() const {
    return age;
    }

    // 不提供setter方法
    };
  3. 委托与组合

    通过组合和委托实现更灵活的封装:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    class AccountValidator {
    public:
    static bool isValidAmount(double amount) {
    return amount > 0;
    }

    static bool hasSufficientFunds(double balance, double amount) {
    return balance >= amount;
    }
    };

    class BankAccount {
    private:
    std::string accountNumber;
    double balance;
    AccountValidator validator; // 组合

    public:
    BankAccount(const std::string& accNum, double initialBalance)
    : accountNumber(accNum), balance(initialBalance) {
    if (!AccountValidator::isValidAmount(initialBalance)) {
    throw std::invalid_argument("Initial balance must be positive");
    }
    }

    void deposit(double amount) {
    if (AccountValidator::isValidAmount(amount)) {
    balance += amount;
    std::cout << "Deposited: $" << amount << std::endl;
    std::cout << "New balance: $" << balance << std::endl;
    } else {
    std::cout << "Invalid deposit amount" << std::endl;
    }
    }

    void withdraw(double amount) {
    if (AccountValidator::isValidAmount(amount) &&
    AccountValidator::hasSufficientFunds(balance, amount)) {
    balance -= amount;
    std::cout << "Withdrew: $" << amount << std::endl;
    std::cout << "New balance: $" << balance << std::endl;
    } else {
    std::cout << "Invalid withdrawal amount" << std::endl;
    }
    }

    double getBalance() const {
    return balance;
    }

    std::string getAccountNumber() const {
    return accountNumber;
    }
    };

封装的优点

  1. 数据安全

    • 防止外部代码意外修改对象的内部状态
    • 可以在setter方法中添加验证逻辑,确保数据的有效性
  2. 实现隐藏

    • 隐藏内部实现细节,只暴露必要的接口
    • 内部实现可以随时修改,而不影响外部代码
  3. 代码维护性

    • 集中管理数据的访问和修改,便于维护
    • 减少代码耦合,提高代码的可维护性
  4. 接口清晰

    • 只暴露必要的公共接口,使类的使用更加清晰
    • 降低了使用类的复杂度,提高了代码的可读性
  5. 可测试性

    • 封装使得类的行为更加可预测,便于单元测试
    • 可以通过mock对象模拟依赖,提高测试覆盖率

封装的最佳实践

  1. 成员变量私有化

    • 所有成员变量默认设为private
    • 通过公共方法访问和修改成员变量
  2. 接口设计原则

    • 最小接口原则:只暴露必要的接口
    • 接口稳定性:公共接口一旦确定,尽量保持稳定
    • 接口清晰度:接口名称应清晰表达其功能
  3. 访问器方法设计

    • Getter方法:返回成员变量的副本或常量引用
    • Setter方法:添加参数验证,确保数据有效性
    • 命名规范:使用getXxxsetXxx命名模式
  4. 避免过度封装

    • 不要为每个私有成员都提供getter和setter
    • 只提供必要的访问方法,保持类的简洁性
  5. 使用const

    • 对于不修改对象状态的方法,使用const修饰
    • 对于返回成员变量的方法,考虑返回const引用
  6. 异常安全

    • 在setter方法中使用异常处理无效参数
    • 确保在构造和析构过程中的异常安全性

封装的实际应用案例

银行账户管理系统

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
class BankAccount {
private:
std::string accountNumber;
std::string accountHolder;
double balance;
bool isFrozen;

// 私有辅助方法
void validateAmount(double amount) const {
if (amount <= 0) {
throw std::invalid_argument("Amount must be positive");
}
}

void checkIfFrozen() const {
if (isFrozen) {
throw std::runtime_error("Account is frozen");
}
}

public:
// 构造函数
BankAccount(const std::string& accNum, const std::string& holder, double initialBalance) {
if (accNum.empty() || holder.empty()) {
throw std::invalid_argument("Account number and holder cannot be empty");
}
validateAmount(initialBalance);

accountNumber = accNum;
accountHolder = holder;
balance = initialBalance;
isFrozen = false;
}

// 存款
void deposit(double amount) {
checkIfFrozen();
validateAmount(amount);

balance += amount;
std::cout << "Deposited: $" << amount << std::endl;
std::cout << "New balance: $" << balance << std::endl;
}

// 取款
void withdraw(double amount) {
checkIfFrozen();
validateAmount(amount);

if (amount > balance) {
throw std::runtime_error("Insufficient funds");
}

balance -= amount;
std::cout << "Withdrew: $" << amount << std::endl;
std::cout << "New balance: $" << balance << std::endl;
}

// 转账
void transfer(BankAccount& recipient, double amount) {
withdraw(amount); // 如果失败,会抛出异常
recipient.deposit(amount);
std::cout << "Transferred: $" << amount << " to account " << recipient.getAccountNumber() << std::endl;
}

// 账户操作
void freeze() {
isFrozen = true;
std::cout << "Account frozen" << std::endl;
}

void unfreeze() {
isFrozen = false;
std::cout << "Account unfrozen" << std::endl;
}

// Getter方法
std::string getAccountNumber() const {
return accountNumber;
}

std::string getAccountHolder() const {
return accountHolder;
}

double getBalance() const {
checkIfFrozen();
return balance;
}

bool isAccountFrozen() const {
return isFrozen;
}
};

// 使用示例
int main() {
try {
BankAccount aliceAccount("123456", "Alice Smith", 1000.0);
BankAccount bobAccount("789012", "Bob Johnson", 500.0);

aliceAccount.deposit(500.0);
aliceAccount.transfer(bobAccount, 300.0);

std::cout << "Alice's balance: $" << aliceAccount.getBalance() << std::endl;
std::cout << "Bob's balance: $" << bobAccount.getBalance() << std::endl;

// 尝试冻结账户
aliceAccount.freeze();
// aliceAccount.deposit(100.0); // 会抛出异常:账户已冻结

} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << std::endl;
}

return 0;
}

封装与其他OOP原则的关系

  1. 封装与继承

    • 封装确保派生类只能通过基类的公共接口访问基类的成员
    • 保护成员允许派生类访问基类的实现细节,同时保持对外部的封装
  2. 封装与多态

    • 封装隐藏了对象的内部实现,只通过公共接口与对象交互
    • 多态允许通过基类接口使用派生类对象,而不需要了解具体实现
  3. 封装与组合

    • 组合是实现封装的重要手段,通过包含其他对象来实现复杂功能
    • 封装确保组合对象的内部细节不被外部访问

封装是面向对象编程的基础,它通过信息隐藏和接口设计,提高了代码的安全性、可维护性和可扩展性。在C++中,合理使用访问控制修饰符和设计良好的接口,是实现有效封装的关键。

继承:代码重用与层次结构

继承的概念与原理

继承是面向对象编程的核心特性之一,它允许从已有类(基类/父类)派生出新类(派生类/子类),子类继承父类的属性和方法,同时可以添加自己的属性和方法,或者重写父类的方法。

继承的核心原理

  • 代码重用:子类继承父类的代码,减少重复代码
  • 层次结构:建立类的层次结构,更符合现实世界的模型
  • 类型关系:通过继承建立”is-a”关系
  • 多态基础:为运行时多态提供基础

继承的类型

C++支持三种继承方式,不同的继承方式会影响基类成员在派生类中的可访问性:

  1. 公有继承(public inheritance)

    • 基类的public成员在派生类中仍然是public
    • 基类的protected成员在派生类中仍然是protected
    • 基类的private成员在派生类中不可访问
    • 体现”is-a”关系,是最常用的继承方式
  2. 保护继承(protected inheritance)

    • 基类的publicprotected成员在派生类中都是protected
    • 基类的private成员在派生类中不可访问
    • 体现”is-implemented-in-terms-of”关系
  3. 私有继承(private inheritance)

    • 基类的publicprotected成员在派生类中都是private
    • 基类的private成员在派生类中不可访问
    • 体现”is-implemented-in-terms-of”关系,通常用于实现细节的重用

继承的语法与实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// 基类定义
class BaseClass {
public:
// 公共成员
void publicMethod();

protected:
// 保护成员
void protectedMethod();

private:
// 私有成员
void privateMethod();
};

// 公有继承
class DerivedClass1 : public BaseClass {
public:
// 新增公共成员
void derivedPublicMethod();

void accessBaseMembers() {
publicMethod(); // 可以访问
protectedMethod(); // 可以访问
// privateMethod(); // 不可访问
}
};

// 保护继承
class DerivedClass2 : protected BaseClass {
public:
// 新增公共成员
void derivedPublicMethod();

void accessBaseMembers() {
publicMethod(); // 可以访问(现在是protected)
protectedMethod(); // 可以访问
// privateMethod(); // 不可访问
}
};

// 私有继承
class DerivedClass3 : private BaseClass {
public:
// 新增公共成员
void derivedPublicMethod();

void accessBaseMembers() {
publicMethod(); // 可以访问(现在是private)
protectedMethod(); // 可以访问(现在是private)
// privateMethod(); // 不可访问
}
};

构造函数与析构函数的继承

  1. 构造函数的调用顺序

    • 派生类对象创建时,先调用基类的构造函数,再调用派生类的构造函数
    • 基类构造函数的调用顺序:从最顶层的基类开始,依次向下
  2. 析构函数的调用顺序

    • 派生类对象销毁时,先调用派生类的析构函数,再调用基类的析构函数
    • 基类析构函数的调用顺序:从最底层的派生类开始,依次向上
  3. 构造函数的继承

    • C++11及以上版本支持使用using BaseClass::BaseClass;继承基类的构造函数
    • 继承的构造函数会被视为派生类的构造函数,但不包括默认构造函数、拷贝构造函数和移动构造函数
  4. 虚析构函数

    • 当使用基类指针指向派生类对象时,基类的析构函数应该声明为virtual
    • 这样可以确保派生类的析构函数被正确调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
class Animal {
protected:
std::string name;

public:
// 构造函数
Animal(const std::string& n) : name(n) {
std::cout << "Animal constructor called for " << name << std::endl;
}

// 虚析构函数
virtual ~Animal() {
std::cout << "Animal destructor called for " << name << std::endl;
}

// 虚函数
virtual void eat() {
std::cout << name << " is eating." << std::endl;
}

void sleep() {
std::cout << name << " is sleeping." << std::endl;
}
};

class Dog : public Animal {
private:
std::string breed;

public:
// 构造函数
Dog(const std::string& n, const std::string& b) : Animal(n), breed(b) {
std::cout << "Dog constructor called for " << name << " (" << breed << ")" << std::endl;
}

// 析构函数
~Dog() {
std::cout << "Dog destructor called for " << name << " (" << breed << ")" << std::endl;
}

// 重写虚函数
void eat() override {
std::cout << name << " (" << breed << ") is eating bones." << std::endl;
}

// 新增方法
void bark() {
std::cout << name << " is barking." << std::endl;
}
};

// 使用示例
int main() {
// 栈上创建对象
{
Dog dog("Rex", "German Shepherd");
dog.eat();
dog.sleep();
dog.bark();
// 离开作用域时,先调用Dog的析构函数,再调用Animal的析构函数
}

std::cout << "\n";

// 堆上创建对象
Animal* animal = new Dog("Fido", "Labrador");
animal->eat(); // 多态:调用Dog::eat()
animal->sleep(); // 非虚函数:调用Animal::sleep()
// animal->bark(); // 错误:Animal类没有bark()方法
delete animal; // 虚析构函数:先调用Dog的析构函数,再调用Animal的析构函数

return 0;
}

继承与多态的深度关系

继承是实现多态的基础,多态通过虚函数和继承实现:

  1. 虚函数

    • 在基类中使用virtual关键字声明
    • 在派生类中使用override关键字重写
    • 虚函数表(vtable):每个包含虚函数的类都有一个虚函数表,存储虚函数的地址
    • 虚指针(vptr):每个对象都有一个虚指针,指向类的虚函数表
  2. 运行时多态

    • 通过基类指针或引用调用虚函数
    • 运行时根据对象的实际类型确定调用哪个版本的函数
    • 实现了”一个接口,多种实现”
  3. 静态多态

    • 通过模板和函数重载实现
    • 编译时确定调用哪个版本的函数
    • 不依赖继承和虚函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// 多态的经典示例
class Shape {
public:
virtual ~Shape() = default;

// 纯虚函数
virtual void draw() const = 0;

// 虚函数
virtual double area() const = 0;
};

class Circle : public Shape {
private:
double radius;

public:
explicit Circle(double r) : radius(r) {}

void draw() const override {
std::cout << "Drawing a circle with radius " << radius << std::endl;
}

double area() const override {
return M_PI * radius * radius;
}
};

class Rectangle : public Shape {
private:
double width;
double height;

public:
Rectangle(double w, double h) : width(w), height(h) {}

void draw() const override {
std::cout << "Drawing a rectangle with width " << width
<< " and height " << height << std::endl;
}

double area() const override {
return width * height;
}
};

// 使用多态
void processShape(const Shape& shape) {
shape.draw();
std::cout << "Area: " << shape.area() << std::endl;
}

int main() {
Circle circle(5.0);
Rectangle rectangle(4.0, 6.0);

// 多态调用
processShape(circle); // 传递Circle对象
processShape(rectangle); // 传递Rectangle对象

// 使用基类指针
Shape* shapes[] = {
new Circle(3.0),
new Rectangle(2.0, 4.0)
};

for (int i = 0; i < 2; ++i) {
shapes[i]->draw();
std::cout << "Area: " << shapes[i]->area() << std::endl;
delete shapes[i];
}

return 0;
}

继承的高级特性

  1. 多重继承

    • 一个派生类可以从多个基类继承
    • 语法:class Derived : public Base1, public Base2 { ... }
    • 可能导致菱形继承问题(钻石问题)
  2. 虚拟继承

    • 用于解决多重继承中的菱形继承问题
    • 语法:class Base : virtual public GrandBase { ... }
    • 确保派生类只包含一个基类的实例
  3. 函数隐藏

    • 当派生类定义了与基类同名的函数时,会隐藏基类的函数
    • 即使参数列表不同,也会隐藏
    • 可以使用using BaseClass::function;显式引入基类的函数
  4. 覆盖与隐藏的区别

    • 覆盖(override):派生类重写基类的虚函数,函数签名相同
    • 隐藏(hide):派生类定义了与基类同名的函数,函数签名可以不同
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
// 多重继承示例
class Animal {
protected:
std::string name;

public:
Animal(const std::string& n) : name(n) {}
virtual void eat() { std::cout << name << " is eating." << std::endl; }
};

class Pet {
protected:
std::string owner;

public:
Pet(const std::string& o) : owner(o) {}
virtual void play() { std::cout << "Pet is playing with " << owner << std::endl; }
};

// 多重继承
class Dog : public Animal, public Pet {
private:
std::string breed;

public:
Dog(const std::string& n, const std::string& o, const std::string& b)
: Animal(n), Pet(o), breed(b) {}

void eat() override {
std::cout << name << " (" << breed << ") is eating bones." << std::endl;
}

void play() override {
std::cout << name << " (" << breed << ") is playing with " << owner << std::endl;
}

void bark() {
std::cout << name << " is barking." << std::endl;
}
};

// 虚拟继承示例(解决菱形继承问题)
class Base {
public:
Base() { std::cout << "Base constructor" << std::endl; }
virtual ~Base() { std::cout << "Base destructor" << std::endl; }
int value = 42;
};

class Derived1 : virtual public Base {
public:
Derived1() { std::cout << "Derived1 constructor" << std::endl; }
~Derived1() { std::cout << "Derived1 destructor" << std::endl; }
};

class Derived2 : virtual public Base {
public:
Derived2() { std::cout << "Derived2 constructor" << std::endl; }
~Derived2() { std::cout << "Derived2 destructor" << std::endl; }
};

class MultipleDerived : public Derived1, public Derived2 {
public:
MultipleDerived() { std::cout << "MultipleDerived constructor" << std::endl; }
~MultipleDerived() { std::cout << "MultipleDerived destructor" << std::endl; }
};

int main() {
// 多重继承
Dog dog("Rex", "Alice", "German Shepherd");
dog.eat();
dog.play();
dog.bark();

// 虚拟继承
MultipleDerived md;
std::cout << md.value << std::endl; // 只有一个Base实例,不会产生歧义

return 0;
}

继承的优缺点

优点

  1. 代码重用

    • 继承父类的代码,减少重复代码
    • 提高开发效率,减少维护成本
  2. 层次结构

    • 建立类的层次结构,更符合现实世界的模型
    • 提高代码的可理解性和可维护性
  3. 多态支持

    • 为运行时多态提供基础
    • 实现了”一个接口,多种实现”
  4. 扩展性

    • 易于扩展现有代码,添加新功能
    • 符合开放-封闭原则

缺点

  1. 紧耦合

    • 派生类与基类紧密耦合,基类的变化可能影响派生类
    • 降低了代码的灵活性和可维护性
  2. 复杂性

    • 多重继承增加了代码的复杂性
    • 可能导致菱形继承问题
  3. 性能开销

    • 虚函数调用有一定的性能开销(通过虚函数表)
    • 虚拟继承增加了内存开销(额外的虚指针)
  4. 设计限制

    • 继承是静态的,编译时确定
    • 可能导致过度设计,违反组合优于继承原则

继承的最佳实践

  1. 优先使用公有继承

    • 只有当确实需要体现”is-a”关系时才使用公有继承
    • 避免使用保护继承和私有继承,除非有特殊理由
  2. 遵循里氏替换原则

    • 子类应该能够替换其父类,并且程序的行为不会改变
    • 子类不应违反父类的前置条件和后置条件
  3. 合理使用虚函数

    • 对于需要在派生类中重写的函数,使用虚函数
    • 对于不需要重写的函数,不使用虚函数(避免性能开销)
  4. 使用纯虚函数和抽象类

    • 对于只作为接口的基类,使用抽象类
    • 纯虚函数强制派生类提供实现
  5. 虚析构函数

    • 当类可能作为基类时,将析构函数声明为虚函数
    • 避免内存泄漏和未定义行为
  6. 避免多重继承

    • 尽量避免使用多重继承,除非有充分的理由
    • 如果必须使用多重继承,考虑使用虚拟继承解决菱形继承问题
  7. 组合优于继承

    • 当不需要体现”is-a”关系时,优先使用组合
    • 组合具有更低的耦合度,更灵活
  8. 接口分离

    • 将大的接口拆分为小的、专用的接口
    • 遵循接口隔离原则
  9. 继承层次不宜过深

    • 保持继承层次简洁,一般不超过3-4层
    • 过深的继承层次会增加代码的复杂性
  10. 文档化继承关系

    • 清晰记录类之间的继承关系
    • 说明派生类对基类的扩展和重写

继承的实际应用案例

图形绘制系统

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
// 抽象基类
class Shape {
public:
virtual ~Shape() = default;

// 纯虚函数
virtual void draw() const = 0;
virtual double area() const = 0;
virtual double perimeter() const = 0;

// 普通虚函数
virtual std::string name() const {
return "Shape";
}
};

// 派生类:圆形
class Circle : public Shape {
private:
double radius;

public:
explicit Circle(double r) : radius(r) {
if (r <= 0) {
throw std::invalid_argument("Radius must be positive");
}
}

void draw() const override {
std::cout << "Drawing a circle with radius " << radius << std::endl;
}

double area() const override {
return M_PI * radius * radius;
}

double perimeter() const override {
return 2 * M_PI * radius;
}

std::string name() const override {
return "Circle";
}

double getRadius() const {
return radius;
}
};

// 派生类:矩形
class Rectangle : public Shape {
private:
double width;
double height;

public:
Rectangle(double w, double h) : width(w), height(h) {
if (w <= 0 || h <= 0) {
throw std::invalid_argument("Width and height must be positive");
}
}

void draw() const override {
std::cout << "Drawing a rectangle with width " << width
<< " and height " << height << std::endl;
}

double area() const override {
return width * height;
}

double perimeter() const override {
return 2 * (width + height);
}

std::string name() const override {
return "Rectangle";
}

double getWidth() const {
return width;
}

double getHeight() const {
return height;
}
};

// 派生类:三角形
class Triangle : public Shape {
private:
double a;
double b;
double c;

public:
Triangle(double side1, double side2, double side3) : a(side1), b(side2), c(side3) {
if (a <= 0 || b <= 0 || c <= 0) {
throw std::invalid_argument("Sides must be positive");
}
if (a + b <= c || a + c <= b || b + c <= a) {
throw std::invalid_argument("Invalid triangle sides");
}
}

void draw() const override {
std::cout << "Drawing a triangle with sides " << a << ", " << b << ", " << c << std::endl;
}

double area() const override {
// 使用海伦公式
double s = (a + b + c) / 2;
return std::sqrt(s * (s - a) * (s - b) * (s - c));
}

double perimeter() const override {
return a + b + c;
}

std::string name() const override {
return "Triangle";
}

double getSideA() const {
return a;
}

double getSideB() const {
return b;
}

double getSideC() const {
return c;
}
};

// 使用示例
void processShapes(const std::vector<std::unique_ptr<Shape>>& shapes) {
for (const auto& shape : shapes) {
std::cout << "\nProcessing " << shape->name() << ":" << std::endl;
shape->draw();
std::cout << "Area: " << shape->area() << std::endl;
std::cout << "Perimeter: " << shape->perimeter() << std::endl;
}
}

int main() {
try {
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Circle>(5.0));
shapes.push_back(std::make_unique<Rectangle>(4.0, 6.0));
shapes.push_back(std::make_unique<Triangle>(3.0, 4.0, 5.0));

processShapes(shapes);

} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << std::endl;
}

return 0;
}

继承与其他OOP原则的关系

  1. 继承与封装

    • 继承不应破坏封装,派生类应通过基类的公共接口访问基类的成员
    • 保护成员允许派生类访问基类的实现细节,同时保持对外部的封装
  2. 继承与多态

    • 继承是实现多态的基础,多态通过虚函数和继承实现
    • 多态允许通过基类接口使用派生类对象,提高代码的灵活性
  3. 继承与抽象

    • 抽象类通过纯虚函数定义接口,派生类提供具体实现
    • 抽象类为继承层次提供了清晰的契约
  4. 继承与设计模式

    • 许多设计模式都基于继承,如模板方法模式、策略模式、观察者模式等
    • 合理使用继承可以实现更灵活、可扩展的设计

继承是面向对象编程的重要特性,它通过代码重用和层次结构,提高了代码的可维护性和可扩展性。然而,继承也有其局限性和缺点,需要谨慎使用。在实际开发中,应该根据具体情况选择合适的继承方式,或者考虑使用组合等其他技术来实现代码重用。

多态:接口与实现的分离

多态的概念与原理

多态(Polymorphism)是面向对象编程的核心特性之一,它允许不同类型的对象对同一消息做出不同的响应,即同一操作作用于不同的对象会产生不同的结果。

多态的核心原理

  • 接口与实现分离:通过统一的接口操作不同的实现
  • 运行时绑定:运行时根据对象的实际类型确定调用哪个方法
  • 代码复用:通过基类接口操作派生类对象
  • 扩展性:添加新的派生类不需要修改现有代码

多态的类型

C++支持两种类型的多态:

  1. 编译时多态(静态多态)

    • 通过函数重载和运算符重载实现
    • 编译时确定调用哪个函数
    • 不依赖继承和虚函数
    • 示例:函数重载、模板、const重载
  2. 运行时多态(动态多态)

    • 通过虚函数和继承实现
    • 运行时根据对象的实际类型确定调用哪个方法
    • 依赖虚函数表和虚指针
    • 示例:虚函数、抽象类

编译时多态的深度分析

函数重载

函数重载是指在同一个作用域内,函数名相同但参数列表不同的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 函数重载示例
class Calculator {
public:
// 重载:不同参数类型
int add(int a, int b) {
return a + b;
}

double add(double a, double b) {
return a + b;
}

// 重载:不同参数个数
int add(int a, int b, int c) {
return a + b + c;
}
};

// 使用
Calculator calc;
std::cout << calc.add(1, 2) << std::endl; // 调用 int add(int, int)
std::cout << calc.add(1.5, 2.5) << std::endl; // 调用 double add(double, double)
std::cout << calc.add(1, 2, 3) << std::endl; // 调用 int add(int, int, int)

运算符重载

运算符重载允许自定义运算符的行为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 运算符重载示例
class Vector {
private:
double x, y;

public:
Vector(double x = 0, double y = 0) : x(x), y(y) {}

// 重载 + 运算符
Vector operator+(const Vector& other) const {
return Vector(x + other.x, y + other.y);
}

// 重载 << 运算符(作为友元)
friend std::ostream& operator<<(std::ostream& os, const Vector& v) {
os << "(" << v.x << ", " << v.y << ")";
return os;
}
};

// 使用
Vector v1(1, 2), v2(3, 4);
Vector v3 = v1 + v2; // 调用 operator+
std::cout << v3 << std::endl; // 调用 operator<<

模板

模板是C++实现编译时多态的强大工具:

1
2
3
4
5
6
7
8
9
10
// 模板示例
template <typename T>
T maximum(T a, T b) {
return a > b ? a : b;
}

// 使用
std::cout << maximum(1, 2) << std::endl; // int 版本
std::cout << maximum(1.5, 2.5) << std::endl; // double 版本
std::cout << maximum("apple", "banana") << std::endl; // const char* 版本

运行时多态的深度分析

虚函数

虚函数是在基类中使用virtual关键字声明的函数,允许派生类重写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Base {
public:
// 虚函数
virtual void doSomething() {
std::cout << "Base::doSomething()" << std::endl;
}

// 虚析构函数
virtual ~Base() {
std::cout << "Base::~Base()" << std::endl;
}
};

class Derived : public Base {
public:
// 重写虚函数
void doSomething() override {
std::cout << "Derived::doSomething()" << std::endl;
}

~Derived() {
std::cout << "Derived::~Derived()" << std::endl;
}
};

虚函数的实现原理

虚函数通过虚函数表(vtable)和虚指针(vptr)实现:

  1. 虚函数表(vtable)

    • 每个包含虚函数的类都有一个虚函数表
    • 存储该类所有虚函数的地址
    • 类的所有对象共享同一个虚函数表
  2. 虚指针(vptr)

    • 每个对象都有一个虚指针,指向类的虚函数表
    • 在对象构造时初始化
    • 占用对象的存储空间(通常为4或8字节)
  3. 调用过程

    • 通过对象的虚指针找到虚函数表
    • 根据函数在表中的位置找到函数地址
    • 调用相应的函数

纯虚函数和抽象类

纯虚函数是在基类中声明但不提供实现的虚函数,派生类必须提供实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 抽象类
class AbstractShape {
public:
virtual ~AbstractShape() = default;

// 纯虚函数
virtual void draw() const = 0;
virtual double area() const = 0;

// 普通虚函数
virtual std::string name() const {
return "AbstractShape";
}
};

// 具体类
class Circle : public AbstractShape {
private:
double radius;

public:
explicit Circle(double r) : radius(r) {}

void draw() const override {
std::cout << "Drawing a circle" << std::endl;
}

double area() const override {
return M_PI * radius * radius;
}

std::string name() const override {
return "Circle";
}
};

接口

在C++中,接口是一种特殊的抽象类,只包含纯虚函数,没有数据成员:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 接口
class Drawable {
public:
virtual ~Drawable() = default;
virtual void draw() const = 0;
};

class Resizable {
public:
virtual ~Resizable() = default;
virtual void resize(double factor) = 0;
};

// 实现多个接口
class Rectangle : public Drawable, public Resizable {
private:
double width, height;

public:
Rectangle(double w, double h) : width(w), height(h) {}

void draw() const override {
std::cout << "Drawing a rectangle" << std::endl;
}

void resize(double factor) override {
width *= factor;
height *= factor;
}
};

多态的高级应用

虚函数与默认参数

虚函数可以有默认参数,但需要注意:默认参数值是在编译时根据指针类型确定的,而不是运行时根据对象类型确定的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Base {
public:
virtual void print(int value = 10) {
std::cout << "Base::print(" << value << ")" << std::endl;
}
};

class Derived : public Base {
public:
void print(int value = 20) override {
std::cout << "Derived::print(" << value << ")" << std::endl;
}
};

// 使用
Base* base = new Derived();
base->print(); // 输出 "Derived::print(10)",默认参数值来自Base类

虚函数与const

虚函数可以是const成员函数,派生类重写时也必须是const:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Base {
public:
virtual void print() const {
std::cout << "Base::print() const" << std::endl;
}
};

class Derived : public Base {
public:
void print() const override {
std::cout << "Derived::print() const" << std::endl;
}
};

虚函数与引用

多态也可以通过引用实现:

1
2
3
4
5
6
7
8
9
void printShape(const AbstractShape& shape) {
std::cout << "Shape: " << shape.name() << std::endl;
shape.draw();
std::cout << "Area: " << shape.area() << std::endl;
}

// 使用
Circle circle(5.0);
printShape(circle); // 多态调用

虚函数与继承层次

在多级继承中,虚函数会被正确传递:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Grandparent {
public:
virtual void doSomething() {
std::cout << "Grandparent::doSomething()" << std::endl;
}
};

class Parent : public Grandparent {
// 不重写doSomething()
};

class Child : public Parent {
public:
void doSomething() override {
std::cout << "Child::doSomething()" << std::endl;
}
};

// 使用
Grandparent* gp = new Child();
gp->doSomething(); // 输出 "Child::doSomething()"

多态的实现案例

图形绘制系统的多态实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
// 抽象基类
class Shape {
public:
virtual ~Shape() = default;

// 纯虚函数
virtual void draw() const = 0;
virtual double area() const = 0;
virtual double perimeter() const = 0;

// 虚函数
virtual std::string name() const {
return "Shape";
}

// 非虚函数
void description() const {
std::cout << "This is a " << name() << " with area "
<< area() << " and perimeter " << perimeter() << std::endl;
}
};

// 派生类:圆形
class Circle : public Shape {
private:
double radius;

public:
explicit Circle(double r) : radius(r) {
if (r <= 0) {
throw std::invalid_argument("Radius must be positive");
}
}

void draw() const override {
std::cout << "Drawing a circle with radius " << radius << std::endl;
}

double area() const override {
return M_PI * radius * radius;
}

double perimeter() const override {
return 2 * M_PI * radius;
}

std::string name() const override {
return "Circle";
}
};

// 派生类:矩形
class Rectangle : public Shape {
private:
double width;
double height;

public:
Rectangle(double w, double h) : width(w), height(h) {
if (w <= 0 || h <= 0) {
throw std::invalid_argument("Width and height must be positive");
}
}

void draw() const override {
std::cout << "Drawing a rectangle with width " << width
<< " and height " << height << std::endl;
}

double area() const override {
return width * height;
}

double perimeter() const override {
return 2 * (width + height);
}

std::string name() const override {
return "Rectangle";
}
};

// 派生类:三角形
class Triangle : public Shape {
private:
double a;
double b;
double c;

public:
Triangle(double side1, double side2, double side3) : a(side1), b(side2), c(side3) {
if (a <= 0 || b <= 0 || c <= 0) {
throw std::invalid_argument("Sides must be positive");
}
if (a + b <= c || a + c <= b || b + c <= a) {
throw std::invalid_argument("Invalid triangle sides");
}
}

void draw() const override {
std::cout << "Drawing a triangle with sides " << a << ", " << b << ", " << c << std::endl;
}

double area() const override {
// 使用海伦公式
double s = (a + b + c) / 2;
return std::sqrt(s * (s - a) * (s - b) * (s - c));
}

double perimeter() const override {
return a + b + c;
}

std::string name() const override {
return "Triangle";
}
};

// 多态使用
void processShapes(const std::vector<std::unique_ptr<Shape>>& shapes) {
for (const auto& shape : shapes) {
std::cout << "\nProcessing " << shape->name() << ":" << std::endl;
shape->draw();
shape->description();
}
}

int main() {
try {
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Circle>(5.0));
shapes.push_back(std::make_unique<Rectangle>(4.0, 6.0));
shapes.push_back(std::make_unique<Triangle>(3.0, 4.0, 5.0));

processShapes(shapes);

} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << std::endl;
}

return 0;
}

多态的优缺点

优点

  1. 灵活性

    • 代码可以处理不同类型的对象
    • 通过统一的接口操作不同的实现
  2. 可扩展性

    • 添加新的派生类不需要修改现有代码
    • 符合开放-封闭原则
  3. 可维护性

    • 代码更清晰,更易于维护
    • 减少了条件判断语句
  4. 接口一致性

    • 不同类型的对象通过相同的接口进行操作
    • 提高了代码的可读性
  5. 代码复用

    • 通过基类接口操作派生类对象
    • 减少了代码重复

缺点

  1. 性能开销

    • 虚函数调用比普通函数调用慢(需要通过虚函数表查找)
    • 虚指针增加了对象的大小
  2. 复杂性

    • 理解虚函数表和运行时绑定需要一定的技术水平
    • 调试多态代码可能更复杂
  3. 设计限制

    • 多态依赖于继承,可能导致紧耦合
    • 过度使用多态可能导致设计过于复杂
  4. 内存开销

    • 每个包含虚函数的类都需要一个虚函数表
    • 每个对象都需要一个虚指针

多态的最佳实践

  1. 合理使用虚函数

    • 只对需要在派生类中重写的函数使用虚函数
    • 避免在性能关键路径上使用虚函数
  2. 使用虚析构函数

    • 当类可能作为基类时,将析构函数声明为虚函数
    • 避免内存泄漏和未定义行为
  3. 使用纯虚函数和抽象类

    • 对于只作为接口的基类,使用抽象类
    • 纯虚函数强制派生类提供实现
  4. 接口设计原则

    • 设计清晰、简洁的接口
    • 接口应该专注于功能,而不是实现细节
  5. 避免过度设计

    • 不要为了使用多态而使用多态
    • 根据实际需求选择合适的设计
  6. 使用智能指针

    • 使用 std::unique_ptrstd::shared_ptr 管理多态对象
    • 避免内存泄漏
  7. 考虑静态多态

    • 对于性能关键的代码,考虑使用模板实现静态多态
    • 避免虚函数的运行时开销
  8. 文档化接口

    • 清晰记录虚函数的行为和预期
    • 说明派生类应该如何重写虚函数
  9. 测试多态行为

    • 测试不同派生类的行为
    • 确保多态调用按预期工作
  10. 组合优于继承

    • 当不需要体现”is-a”关系时,考虑使用组合
    • 组合具有更低的耦合度,更灵活

多态与其他OOP原则的关系

  1. 多态与封装

    • 多态通过接口与实现分离,增强了封装
    • 外部代码只需要了解接口,不需要了解具体实现
  2. 多态与继承

    • 继承是实现多态的基础
    • 多态通过虚函数和继承实现运行时绑定
  3. 多态与抽象

    • 抽象类通过纯虚函数定义接口,是多态的重要组成部分
    • 多态通过抽象接口操作具体实现
  4. 多态与设计模式

    • 许多设计模式都基于多态,如策略模式、观察者模式、工厂模式等
    • 多态是实现这些设计模式的关键

多态是面向对象编程的核心特性之一,它通过接口与实现的分离,提高了代码的灵活性、可扩展性和可维护性。在C++中,多态通过虚函数、纯虚函数和抽象类实现,同时也支持通过模板和重载实现的编译时多态。合理使用多态,可以编写更加优雅、灵活和可维护的代码。

抽象类和接口:契约与实现

抽象类的深度分析

抽象类是一种特殊的类,它至少包含一个纯虚函数,不能直接实例化,只能作为基类被继承。抽象类为派生类定义了一个共同的接口和部分实现。

抽象类的核心特性

  • 不能实例化:抽象类不能直接创建对象
  • 包含纯虚函数:至少有一个纯虚函数
  • 可以包含非纯虚函数:可以提供默认实现
  • 可以包含数据成员:可以有成员变量
  • 作为接口:定义派生类必须实现的方法

纯虚函数的高级特性

纯虚函数是在基类中声明但不提供实现的虚函数,派生类必须提供实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class AbstractClass {
public:
// 纯虚函数
virtual void pureVirtualFunction() = 0;

// 纯虚函数也可以有默认实现(C++11+)
virtual void pureVirtualWithDefault() = 0;

// 普通虚函数
virtual void virtualFunction() {
std::cout << "AbstractClass::virtualFunction()" << std::endl;
}

// 普通成员函数
void memberFunction() {
std::cout << "AbstractClass::memberFunction()" << std::endl;
}

// 虚析构函数
virtual ~AbstractClass() = default;
};

// 纯虚函数的默认实现
void AbstractClass::pureVirtualWithDefault() {
std::cout << "AbstractClass::pureVirtualWithDefault()" << std::endl;
}

// 派生类
class ConcreteClass : public AbstractClass {
public:
// 必须实现纯虚函数
void pureVirtualFunction() override {
std::cout << "ConcreteClass::pureVirtualFunction()" << std::endl;
}

// 可以选择使用默认实现或提供自己的实现
void pureVirtualWithDefault() override {
AbstractClass::pureVirtualWithDefault(); // 调用默认实现
std::cout << "ConcreteClass::pureVirtualWithDefault()" << std::endl;
}

// 可以重写虚函数
void virtualFunction() override {
std::cout << "ConcreteClass::virtualFunction()" << std::endl;
}
};

接口的概念与实现

在C++中,接口是一种特殊的抽象类,它只包含纯虚函数,没有数据成员和非纯虚函数实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 接口
class Drawable {
public:
virtual ~Drawable() = default;
virtual void draw() const = 0;
};

class Resizable {
public:
virtual ~Resizable() = default;
virtual void resize(double factor) = 0;
virtual void resize(double width, double height) = 0;
};

// 实现多个接口
class Rectangle : public Drawable, public Resizable {
private:
double width, height;

public:
Rectangle(double w, double h) : width(w), height(h) {}

void draw() const override {
std::cout << "Drawing a rectangle with width " << width
<< " and height " << height << std::endl;
}

void resize(double factor) override {
width *= factor;
height *= factor;
}

void resize(double newWidth, double newHeight) override {
width = newWidth;
height = newHeight;
}
};

抽象类与接口的区别

特性抽象类接口
实例化不能直接实例化不能直接实例化
纯虚函数至少有一个全部是纯虚函数
非纯虚函数可以有不能有
数据成员可以有不能有
访问修饰符可以使用public、protected、private通常只使用public
继承方式通常使用public继承通常使用public继承
多重继承支持,但应谨慎使用支持,是C++实现多重接口的方式
目的提供部分实现,定义共同行为定义纯粹的接口,不提供实现

抽象类的应用场景

  1. 定义接口

    • 为一组相关的类定义共同的接口
    • 强制派生类实现特定的方法
  2. 提供部分实现

    • 实现共同的功能,减少代码重复
    • 允许派生类重写需要自定义的方法
  3. 模板方法模式

    • 定义算法的骨架,将一些步骤延迟到派生类
    • 例如:排序算法、生命周期管理
  4. 工厂方法模式

    • 提供创建对象的接口,让子类决定实例化哪个类
  5. 策略模式

    • 定义一系列算法,将它们封装起来,并使它们可互换

抽象类的设计原则

  1. 单一职责原则

    • 抽象类应该只有一个引起它变化的原因
    • 专注于定义一个特定领域的接口
  2. 接口隔离原则

    • 将大的接口拆分为小的、专用的接口
    • 派生类不应该被迫实现它不使用的方法
  3. 依赖倒置原则

    • 高层模块不应该依赖低层模块,两者都应该依赖于抽象
    • 抽象不应该依赖于具体实现,具体实现应该依赖于抽象
  4. 里氏替换原则

    • 子类应该能够替换其父类,并且程序的行为不会改变
  5. 开放-封闭原则

    • 抽象类应该对扩展开放,对修改封闭
    • 新增功能应该通过添加新的派生类实现,而不是修改抽象类

抽象类的实际应用案例

图形绘制系统

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
// 抽象基类:形状
class Shape {
public:
virtual ~Shape() = default;

// 纯虚函数:绘制
virtual void draw() const = 0;

// 纯虚函数:计算面积
virtual double area() const = 0;

// 纯虚函数:计算周长
virtual double perimeter() const = 0;

// 虚函数:获取名称
virtual std::string name() const {
return "Shape";
}

// 非虚函数:获取描述
std::string description() const {
return "This is a " + name() + " with area " +
std::to_string(area()) + " and perimeter " +
std::to_string(perimeter());
}
};

// 抽象基类:二维形状
class TwoDShape : public Shape {
public:
// 纯虚函数:获取边界框
virtual void getBounds(double& minX, double& minY, double& maxX, double& maxY) const = 0;
};

// 具体类:圆形
class Circle : public TwoDShape {
private:
double x, y; // 圆心
double radius; // 半径

public:
Circle(double x, double y, double r) : x(x), y(y), radius(r) {
if (r <= 0) {
throw std::invalid_argument("Radius must be positive");
}
}

void draw() const override {
std::cout << "Drawing a circle at (" << x << ", " << y
<< ") with radius " << radius << std::endl;
}

double area() const override {
return M_PI * radius * radius;
}

double perimeter() const override {
return 2 * M_PI * radius;
}

std::string name() const override {
return "Circle";
}

void getBounds(double& minX, double& minY, double& maxX, double& maxY) const override {
minX = x - radius;
minY = y - radius;
maxX = x + radius;
maxY = y + radius;
}

double getRadius() const {
return radius;
}
};

// 具体类:矩形
class Rectangle : public TwoDShape {
private:
double x, y; // 左上角
double width, height; // 宽和高

public:
Rectangle(double x, double y, double w, double h)
: x(x), y(y), width(w), height(h) {
if (w <= 0 || h <= 0) {
throw std::invalid_argument("Width and height must be positive");
}
}

void draw() const override {
std::cout << "Drawing a rectangle at (" << x << ", " << y
<< ") with width " << width << " and height " << height << std::endl;
}

double area() const override {
return width * height;
}

double perimeter() const override {
return 2 * (width + height);
}

std::string name() const override {
return "Rectangle";
}

void getBounds(double& minX, double& minY, double& maxX, double& maxY) const override {
minX = x;
minY = y;
maxX = x + width;
maxY = y + height;
}

double getWidth() const {
return width;
}

double getHeight() const {
return height;
}
};

// 使用抽象类
void processShapes(const std::vector<std::unique_ptr<Shape>>& shapes) {
for (const auto& shape : shapes) {
std::cout << "\nProcessing " << shape->name() << ":" << std::endl;
shape->draw();
std::cout << shape->description() << std::endl;

// 尝试转换为TwoDShape
if (const auto* twoDShape = dynamic_cast<const TwoDShape*>(shape.get())) {
double minX, minY, maxX, maxY;
twoDShape->getBounds(minX, minY, maxX, maxY);
std::cout << "Bounds: (" << minX << ", " << minY
<< ") to (" << maxX << ", " << maxY << ")" << std::endl;
}
}
}

int main() {
try {
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Circle>(0, 0, 5));
shapes.push_back(std::make_unique<Rectangle>(0, 0, 4, 6));

processShapes(shapes);

} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << std::endl;
}

return 0;
}

接口的最佳实践

  1. 只定义纯虚函数

    • 接口应该只包含纯虚函数,不包含数据成员和非纯虚函数
    • 保持接口的纯粹性和简洁性
  2. 提供虚析构函数

    • 接口应该提供虚析构函数,通常使用默认实现
    • 避免内存泄漏和未定义行为
  3. 使用public继承

    • 实现接口时应该使用public继承
    • 确保接口的方法在派生类中保持public访问级别
  4. 接口命名规范

    • 接口名称通常使用形容词或动词形式
    • 例如:Drawable、Resizable、Serializable
  5. 接口隔离

    • 将大的接口拆分为小的、专用的接口
    • 避免创建臃肿的接口
  6. 多重接口

    • C++支持多重继承,可以实现多个接口
    • 这是C++实现类似Java接口的方式

抽象类的最佳实践

  1. 合理使用纯虚函数

    • 对于必须由派生类实现的方法,使用纯虚函数
    • 对于可以提供默认实现的方法,使用虚函数
  2. 提供虚析构函数

    • 抽象类应该提供虚析构函数
    • 确保派生类的析构函数被正确调用
  3. 设计清晰的层次结构

    • 抽象类应该位于继承层次的上层
    • 具体实现应该位于继承层次的下层
  4. 避免深度继承

    • 保持继承层次简洁,一般不超过3-4层
    • 过深的继承层次会增加代码的复杂性
  5. 文档化抽象类

    • 清晰记录抽象类的目的和使用方法
    • 说明纯虚函数的预期行为
  6. 测试抽象类

    • 通过测试具体派生类来间接测试抽象类
    • 确保抽象类的设计是合理的

抽象类与现代C++特性

  1. 抽象类与概念(C++20)

    • 概念可以用于约束模板参数,类似于接口
    • 概念是编译时的,而抽象类是运行时的
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    #include <concepts>

    // 概念:可绘制的
    template <typename T>
    concept Drawable = requires(const T& t) {
    { t.draw() } -> std::same_as<void>;
    };

    // 概念:可调整大小的
    template <typename T>
    concept Resizable = requires(T& t) {
    { t.resize(double{}) } -> std::same_as<void>;
    };

    // 使用概念约束模板参数
    template <Drawable D>
    void drawObject(const D& obj) {
    obj.draw();
    }

    template <Resizable R>
    void resizeObject(R& obj, double factor) {
    obj.resize(factor);
    }
  2. 抽象类与lambda表达式

    • lambda表达式可以用于实现简单的接口
    • 结合std::function使用,更加灵活
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 函数对象接口
    using DrawFunction = std::function<void()>;

    // 使用lambda实现接口
    DrawFunction createDrawFunction(const std::string& shape) {
    return [shape]() {
    std::cout << "Drawing a " << shape << std::endl;
    };
    }
  3. 抽象类与智能指针

    • 使用std::unique_ptr和std::shared_ptr管理抽象类的实例
    • 避免内存泄漏和悬垂指针
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 创建抽象类的实例
    std::unique_ptr<Shape> createShape(const std::string& type) {
    if (type == "circle") {
    return std::make_unique<Circle>(0, 0, 5);
    } else if (type == "rectangle") {
    return std::make_unique<Rectangle>(0, 0, 4, 6);
    } else {
    throw std::invalid_argument("Unknown shape type");
    }
    }

抽象类与设计模式

  1. 模板方法模式

    • 抽象类定义算法的骨架,将一些步骤延迟到派生类
    • 例如:排序算法、生命周期管理
  2. 工厂方法模式

    • 抽象类提供创建对象的接口,让子类决定实例化哪个类
  3. 策略模式

    • 定义一系列算法,将它们封装起来,并使它们可互换
  4. 观察者模式

    • 定义对象间的一种一对多依赖关系,当一个对象状态改变时,所有依赖它的对象都得到通知并被自动更新
  5. 命令模式

    • 将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化

小结

抽象类和接口是面向对象编程的重要概念,它们通过定义共同的接口和部分实现,提高了代码的可维护性和可扩展性。

抽象类

  • 包含至少一个纯虚函数
  • 可以提供部分实现
  • 不能直接实例化
  • 用于定义一组相关类的共同行为

接口

  • 只包含纯虚函数
  • 不提供任何实现
  • 不能直接实例化
  • 用于定义纯粹的接口,不涉及实现细节

在C++中,接口是通过特殊的抽象类实现的,即只包含纯虚函数的抽象类。合理使用抽象类和接口,可以编写更加灵活、可扩展和可维护的代码。

面向对象编程的设计原则

1. 单一职责原则(Single Responsibility Principle)

一个类应该只有一个引起它变化的原因,即一个类只负责一项功能。

2. 开放-封闭原则(Open-Closed Principle)

软件实体(类、模块、函数等)应该对扩展开放,对修改封闭。

3. 里氏替换原则(Liskov Substitution Principle)

子类应该能够替换其父类,并且程序的行为不会改变。

4. 接口隔离原则(Interface Segregation Principle)

客户端不应该依赖它不使用的接口,应该将庞大的接口拆分为更小的、更具体的接口。

5. 依赖倒置原则(Dependency Inversion Principle)

高层模块不应该依赖低层模块,两者都应该依赖于抽象;抽象不应该依赖于具体实现,具体实现应该依赖于抽象。

6. 组合优于继承原则(Composition Over Inheritance)

优先使用对象组合而不是类继承来实现代码重用,这样可以降低类之间的耦合度,提高代码的灵活性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 继承方式
class Engine {
public:
virtual void start() = 0;
};

class GasolineEngine : public Engine {
public:
void start() override {
std::cout << "Gasoline engine starting..." << std::endl;
}
};

class Car {
private:
GasolineEngine engine; // 固定使用汽油发动机
public:
void start() {
engine.start();
}
};

// 组合方式
class Car {
private:
std::unique_ptr<Engine> engine; // 可以使用任何发动机
public:
Car(std::unique_ptr<Engine> e) : engine(std::move(e)) {}

void start() {
engine->start();
}

// 可以动态更换发动机
void setEngine(std::unique_ptr<Engine> e) {
engine = std::move(e);
}
};

面向对象编程的优点

  1. 代码重用:通过继承和组合实现代码重用
  2. 模块化:将代码组织为独立的对象,便于管理和维护
  3. 可维护性:封装使得修改内部实现不影响外部代码
  4. 可扩展性:通过继承和多态易于扩展现有代码
  5. 可理解性:代码结构更符合现实世界的模型,易于理解
  6. 安全性:封装保护数据,减少错误

面向对象编程的缺点

  1. 复杂性:相比过程式编程,OOP的概念和实现更复杂
  2. 性能开销:继承和多态可能带来一定的性能开销
  3. 过度设计:可能导致过度设计,增加不必要的复杂性
  4. 学习曲线:学习OOP的概念和设计模式需要一定的时间

C++20新特性:概念(Concepts)

C++20引入了概念(Concepts),用于约束模板参数,提高模板代码的可读性和错误信息的清晰度:

概念的基本概念

  • 概念:对类型的约束,指定类型必须满足的条件
  • 约束:使用概念来限制模板参数的类型
  • requires表达式:定义概念的具体约束条件

概念的使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <concepts>
#include <iostream>

// 定义概念:算术类型
template <typename T>
concept Arithmetic = std::is_arithmetic_v<T>;

// 使用概念约束模板参数
template <Arithmetic T>
T add(T a, T b) {
return a + b;
}

// 更复杂的概念:可比较类型
template <typename T>
concept Comparable = requires(T a, T b) {
{ a < b } -> std::convertible_to<bool>;
{ a > b } -> std::convertible_to<bool>;
{ a == b } -> std::convertible_to<bool>;
};

// 使用概念约束模板参数
template <Comparable T>
T max(T a, T b) {
return a > b ? a : b;
}

// 标准库概念的使用
template <std::integral T>
T square(T x) {
return x * x;
}

template <std::floating_point T>
T square(T x) {
return x * x;
}

int main() {
// 使用add函数
std::cout << "Add integers: " << add(1, 2) << std::endl;
std::cout << "Add doubles: " << add(1.5, 2.5) << std::endl;

// 使用max函数
std::cout << "Max integer: " << max(10, 20) << std::endl;
std::cout << "Max string: " << max(std::string("apple"), std::string("banana")) << std::endl;

// 使用square函数
std::cout << "Square integer: " << square(5) << std::endl;
std::cout << "Square double: " << square(2.5) << std::endl;

return 0;
}

概念的优点

  1. 类型安全:在编译时检查类型约束,避免运行时错误
  2. 错误信息清晰:当类型不满足约束时,提供更清晰的错误信息
  3. 代码可读性:通过概念名称明确表达模板参数的要求
  4. 代码重用:概念可以被多个模板使用,提高代码重用性
  5. 灵活性:概念可以组合使用,创建更复杂的类型约束

面向对象编程与其他编程范式的比较

与过程式编程的比较

  • 过程式编程:关注过程和函数,将程序分解为一系列函数调用
  • 面向对象编程:关注对象和交互,将程序分解为一系列对象的交互

与函数式编程的比较

  • 函数式编程:关注纯函数,避免状态和副作用
  • 面向对象编程:关注对象的状态和行为,允许状态变化

与泛型编程的比较

  • 泛型编程:关注类型参数化,实现代码重用
  • 面向对象编程:关注对象和继承,实现代码重用

小结

本章介绍了面向对象编程的基本概念,包括:

  1. 面向对象编程的核心概念:对象、类、封装、继承、多态
  2. 类和对象:类的定义、对象的创建和使用
  3. 封装:数据隐藏、访问控制
  4. 继承:基类和派生类、继承的语法和优点
  5. 多态:编译时多态和运行时多态、虚函数
  6. 抽象类和接口:纯虚函数、抽象类的使用
  7. 面向对象编程的设计原则:单一职责、开放-封闭、里氏替换、接口隔离、依赖倒置
  8. 面向对象编程的优缺点:代码重用、模块化、可维护性等优点,以及复杂性、性能开销等缺点
  9. 与其他编程范式的比较:与过程式编程、函数式编程、泛型编程的比较

面向对象编程是现代编程语言的重要特性,它提供了一种更符合人类思维方式的编程方法,使得代码更易于理解、维护和扩展。掌握面向对象编程的概念和技巧,对于编写高质量的C++程序至关重要。

在后续章节中,我们将更深入地学习C++的面向对象特性,包括类的设计、构造函数和析构函数、运算符重载、模板等内容。