Cpp 拾遗

复习目标:

  • 函数/运算符重载
  • 虚函数与抽象类
  • 内存模型
  • OOP 相关(封装继承多态)
  • 指针(原始指针)
  • 动态内存分配与释放
  • 引用 (右值引用、完美转发)
  • 字符串处理
  • 模版
  • 友元

输出:

  • 博客 1-3 篇

1. 函数/运算符重载

C++ 允许在同一作用域中的某个函数运算符指定多个定义,分别称为函数重载运算符重载

重载声明是指一个与之前已经在该作用域内声明过的函数或方法具有相同名称的声明,但是它们的参数列表和定义(实现)不相同

函数重载比较简单,但是需要注意的是,区分重载函数的是参数列表,返回值不作为判断的标准。两个函数如果只有返回值类型不同,而参数列表相同,是不能构成重载的,会发生:会出现编译错误,提示函数重载冲突,因为编译器无法仅通过返回值类型来区分这两个函数。

运算符重载稍微有点复杂。可以重定义或重载大部分 C++ 内置的运算符,重载的运算符是带有特殊名称的函数,函数名是由关键字 operator 和其后要重载的运算符符号构成的。与其他函数一样,重载运算符有一个返回类型和一个参数列表。

1
2
3
4
// 定义为成员函数
Box operator+(const Box&);
// 定义上面的函数为类的非成员函数
Box operator+(const Box&, const Box&);
CPP

具体的用法:

1
2
3
4
5
6
7
8
9
10
Box operator+(const Box& b)
{
Box box;
box.length = this->length + b.length;
box.breadth = this->breadth + b.breadth;
box.height = this->height + b.height;
return box;
}
// 把两个对象相加,得到 Box3
Box3 = Box1 + Box2;
CPP

哪些能够重载,哪些不能重载?
可重载的:
![[Pasted image 20240911181705.png]]

下面是不可重载的运算符列表:

  • .:成员访问运算符
  • .*, ->*:成员指针访问运算符
  • :::域运算符
  • sizeof:长度运算符
  • ?::条件运算符
  • # 预处理符号

一个🌰—重载前缀/后缀 ++,注意体会他们的区别

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
// 重载前缀递增运算符( ++ )
Time operator++ ()
{
++minutes; // 对象加 1
if(minutes >= 60)
{
++hours;
minutes -= 60;
}
return Time(hours, minutes);
}
// 重载后缀递增运算符( ++ )
Time operator++( int )
{
// 保存原始值
Time T(hours, minutes);
// 对象加 1
++minutes;
if(minutes >= 60)
{
++hours;
minutes -= 60;
}
// 返回旧的原始值
return T;
}
CPP

2. 虚函数与抽象类

使用关键字 virtual 在基类中声明虚函数,主要用于实现多态性。

编译器为包含虚函数的类创建一个虚函数表(V-Table)。每个对象包含一个指向虚函数表的指针(V-Ptr)。

在派生类中重写该虚函数,不需要再次使用 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
#include <iostream>

// 基类
class Base {
public:
virtual void show() const { // 虚函数
std::cout << "Base class" << std::endl;
}
virtual ~Base() = default; // 虚析构函数
};

// 派生类1
class Derived1 : public Base {
public:
void show() const override { // 重写虚函数
std::cout << "Derived1 class" << std::endl;
}
};

// 派生类2
class Derived2 : public Base {
public:
void show() const override { // 重写虚函数
std::cout << "Derived2 class" << std::endl;
}
};

// 派生类3
class Derived3 : public Base {
public:
void show() const override { // 重写虚函数
std::cout << "Derived3 class" << std::endl;
}
};
// 这里传入的参数是基类的引用
void display(const Base& obj) {
obj.show(); // 调用实际对象的show函数
}

int main() {
Base base;
Derived1 derived1;
Derived2 derived2;
Derived3 derived3;

display(base); // 调用Base::show
display(derived1); // 调用Derived1::show
display(derived2); // 调用Derived2::show
display(derived3); // 调用Derived3::show

return 0;
}
CPP

纯虚函数
主要用于定义抽象类。抽象类不能实例化,只能作为基类使用。纯虚函数的主要作用是强制派生类实现该函数,从而确保接口的一致性。
使用方式:
1. 定义抽象类
- 在基类中声明纯虚函数。
- 使用 = 0 语法来定义纯虚函数。
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
28
29
#include <iostream>
using namespace std;

class Base {
public:
virtual void display() = 0; // 纯虚函数
void defaultDisplay() {
cout << "Default display of Base" << endl;
}
};

void Base::display() {
cout << "Pure virtual function with body in Base" << endl;
}

class Derived : public Base {
public:
void display() override {
Base::display(); // 调用基类中的纯虚函数实现
cout << "Display of Derived" << endl;
}
};

int main() {
Derived derivedObj;
derivedObj.display();

return 0;
}
CPP

3. 内存模型

示意图

1
2
3
4
5
6
7
8
9
10
11
12
13
+------------------+
| 代码区 |
+------------------+
| 常量区 |
+------------------+
| 全局/静态区 |
+------------------+
| 堆 |
| (向上增长) |
+------------------+
| 栈 |
| (向下增长) |
+------------------+
MD
  1. Stack:存储局部变量函数调用的相关信息,通常是自动管理,速度较快
  2. Heap:用于动态分配内存,通过 new 和 delete 进行管理,速度较慢
  3. Global/Static Area:生命周期长,存放全局变量和静态变量,在程序开始时进行初始化
  4. Constant Area:存储常量数据,包括字符串字面量和 const 常量,通常是只读的,并且可以共享
    1
    2
    const int constVar = 30; // 常量变量
    const char* str = "Hello, World!"; // 字符串字面量
    CPP
  5. Code Area:代码区用于存储程序的可执行代码。代码区通常是只读的,防止程序意外修改代码。共享:代码区可以在多个进程之间共享,以节省内存。
    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
    #include <iostream>

    // 全局变量
    int globalVar = 10;

    void function() {
    // 局部变量(栈)
    int localVar = 20;

    // 静态变量(全局/静态区)
    static int staticVar = 30;

    // 动态分配内存(堆)
    int* heapVar = new int(40);

    // 常量变量(常量区)
    const int constVar = 50;

    std::cout << "Global Variable: " << globalVar << std::endl;
    std::cout << "Local Variable: " << localVar << std::endl;
    std::cout << "Static Variable: " << staticVar << std::endl;
    std::cout << "Heap Variable: " << *heapVar << std::endl;
    std::cout << "Constant Variable: " << constVar << std::endl;

    // 释放堆内存
    delete heapVar;
    }

    int main() {
    function();
    return 0;
    }
    CPP

4. OOP 相关

封装

封装的主要目的是将对象的状态(属性)和行为(方法)绑定在一起,并隐藏对象的内部实现细节,只暴露必要的接口给外部使用。

  1. 数据隐藏
    • 通过将对象的属性设为私有(private),防止外部直接访问和修改。
    • 通过提供公有(public)或保护(protected)的访问方法(getter 和 setter),控制对属性的访问和修改。
  2. 接口公开
    • 通过公有方法(public methods)提供对对象功能的访问。
    • 这些方法定义了对象与外部交互的接口。

继承

首先继承分为 public 、protect 、 private 三种。

  1. 公有继承(Public Inheritance)
    • 基类的公有成员和保护成员在派生类中保持其原有的访问权限。
    • 基类的私有成员在派生类中不可访问。
  2. 保护继承(Protected Inheritance)
    • 基类的公有成员和保护成员在派生类中变为保护成员。
    • 基类的私有成员在派生类中不可访问。
  3. 私有继承(Private Inheritance)
    • 基类的公有成员和保护成员在派生类中变为私有成员。
    • 基类的私有成员在派生类中不可访问。

还有一种额外的继承方式,叫做虚继承(virtual 关键字)
虚继承用于解决多重继承中的菱形继承问题。通过虚继承,基类的成员只会有一个实例,从而避免重复继承和二义性。

1
2
3
4
5
  A
/ \
B C
\ /
D
MD

其中:

  • A 是基类。
  • B 和 C 是从 A 继承的派生类。
  • D 是从 B 和 C 继承的最终派生类。

在这种结构中,如果 B 和 C 都继承了 A 的成员,而 D 又继承了 B 和 C,就会出现菱形继承的问题。通过虚继承可以解决这个问题,确保 A 的成员在 D 中只存在一个实例。
通过继承机制,可以实现代码重用扩展

多态

多态(Polymorphism)是面向对象编程(OOP)的一个重要特性,它允许对象以多种形式出现。多态性使得同一个接口可以用于不同的数据类型,从而实现接口的重用和灵活性。多态一般分为下面两种:

  1. 静态多态(编译时多态)
    • 函数重载
    • 运算符重载
    • 模板
  2. 动态多态(运行时多态)
    • 虚函数
    • 纯虚函数
    • 抽象类
    • 接口

静态多态:

  • 在编译的时候就决定调用哪个函数,
  • 编译时确定,性能较高。
  • 代码可读性和可维护性较好。
    缺点:
  • 灵活性较低,不能在运行时改变行为。

关于底层实现逻辑:
在编译时,编译器会将重载的函数名进行“修饰”(mangling),生成唯一的标识符。例如:

  • show(int) 可能被编译器修饰为 _Z4showi
  • show(double) 可能被编译器修饰为 _Z4showd
  • c1 + c2 会被转换为 c1.operator+(c2)
    静态多态在底层的实现主要依赖于编译器的功能。编译器在编译阶段根据函数签名和操作数类型来选择合适的函数或运算符进行调用。

动态多态:

  • 动态多态是在运行时决定的多态性,通过虚函数、纯虚函数、抽象类和接口实现。
  • 灵活性高,可以在运行时改变行为。
  • 通过接口实现代码的解耦和模块化。
    缺点:
  • 运行时确定,性能较低。
  • 实现复杂度较高。

关于虚函数表和虚函数指针

  1. 虚函数表(V-Table)
    • 存储类的虚函数指针的表,每个包含虚函数的类都有一个虚函数表。
  2. 虚函数指针(VPTR)
    • 每个对象都有一个指向虚函数表的指针,用于在运行时动态绑定函数。

引用

引用折叠和万能引用(与模板直接相关的右值引用 T)
![[Pasted image 20240911194924.png]]


Cpp 拾遗
https://yintel12138.github.io/2024/09/11/Cpp-Scavenging/
作者
Yintel
发布于
2024年9月11日
许可协议