函数重载 function overloading

  • 静态多态 static polymorphism: 在某个类内部的函数进行重载,在编译的时候生成
  • 动态多态 dynamic polymorphism: 虚函数机制,在运行的时候进行重载,不同类

函数重载

定义:相同名称但是不同的参数数量或者参数的数据类型

规则:

  • 默认参数应该放在参数的末尾
  • 返回类型可以不同
  • 要能区分不同的函数,不要有两个名称和数据类型、参数数量完全相同的函数出现

constructor 重载

destructor 重载

对于通过 new 方法申请内存空间的变量我们也要通过 delete 的方法删除,这时候我们就需要通过一个专门做这个的特殊函数 ~ 析构函数进行拆解删除
每次在类的生命周期结束的时候,会自动调用析构函数将变量 delete

硬拷贝 Deep Copy

所谓深浅,区别在于对于指针的复制是按照地址复制还是内容复制,如果按照地址复制,在该代码块(函数)结束的时候会自动调用析构函数

三大法则 rule of big three

对于任何动态分配的内存,我们都必须提供:

  • 析构函数 destructor
  • 复制构造函数 copy constructor
  • 赋值操作符 assign operator

两个步骤

  • 申请空间
  • 赋值 *val = *input.val

注意

在赋值的前提是 传入变量是引用而不是具体变量 Box(const Box& box){...}
这样就不会在结束函数的时候自动删除引用变量

操作符重载

返回值是 Box& 一定要返回引用,不然首先传递内容太大,其次返回原值会形成一个临时变量,但是只能作为 rvalue 很多时候无法进行 lvalue 的操作, 例如下面的代码:
![[Pasted image 20240424192708.png]]

清除旧有问题

在赋值、复制的时候我们首先应该判断该空间是否为空,如果不为空,那么首先就要先delete变量,然后再进行申请空间和赋值

指针容器 多态容器

指针容器

三个规则和一个不变量

  • At-most-once Invariant: 每一个申请的动态内存都只能有一个指针指向它,也就是说,不能多个指针同时指向一个动态内存
  • Existence Rule: 对象必须首先被申请内存然后其指针才能被保存到 container 中
  • Ownership Rule: 一旦对象的指针被储存到容器中,这个对象就是容器的一种性质了,也就是我们只能通过修改容器自身来修改对象
  • Conservation Rule: 当指针被从容器中清除的时候,要么是被delete了或者被其他容器存储了

多态容器 polymorphic containers

背景原因:我们想要在一个链表中存储不同类型的数据。
做法:设置指向父类(基类)的指针,通过父类指针/引用指向基类实现该功能

自上向下:dynamic_cast<type*>

将基类的指针或引用安全地转换为派生类的指针或引用。如果转换失败,dynamic_cast 会返回适当的值(对于指针是 nullptr,对于引用则抛出异常)

自下向上:克隆思路 Clone Method

背景原因:我们对一个基类指针进行初始化的时候,可能需要拷贝一个派生类的对象的值,但是由于基类的构造函数并没有针对这一数据类型的输入

  • 一般在基类是纯虚函数,在子类实现
  • 返回值是基类的指针
  • implement是首先生成本类(派生类)指针,然后直接转换(由于多态性质,我们可以表面上返回 派生类指针,但是编译器由于声明的返回数据类型是 基类指针,所以真正返回基类指针)
例子
1
2
3
4
Base *Derived::clone(){ 
Derived *dp = new Derived(*this);
return dp;
}

运算符重载 Operator Overloading

标准运算符 +-*/%

  • 输入的数据类型必须是 const 引用,说明这些输入值不会被修改
  • 返回类型是一个 对象, 而不是引用,因为
  • 区分是不是类的成员函数,如果是,那么第一个输入默认就是 *this

赋值运算符 =

  • 返回的数据类型就是 引用

流操作符 >>, <<

应用对象:ios 对象,例如

1
2
3
4
5
6
7
8
9
ostream& operator<<(ostream& os, const IntSet& s){

return os;
}

istream& operator>>(istream& is, const IntSet& s){

return is;
}

这样的写码方式确保了返回值可以连接起来串成一串

Subscript 操作符 []

首先我们定义的时候都会需要 const 和 非const两类返回值的函数,

友元关系 friendship

应用对象

非成员函数,但是想要使用类的私有变量

声明方式

1
2
3
4
class A{
friend void f(A a, int newNum);
friend class B;
};

这里说明,函数 f 能够访问类 A 的私有变量,同时 类B (即成员函数) 也能访问 类 A 的私有变量

从属关系

  • 友元函数、友元类并不是类的成员,也就是说,只是声明(只是认识认识)
  • 友元函数不是互相的 AB 的友元表示 B 函数可以使用 A 的私有变量,但是 A 不可以使用 B 的私有变量
  • 不能通过类的命名空间进行调用友元函数,即A::f 的写法是不合法的

常见容器

线性表 linear list

数据元素的有序集合,其中每个元素具有唯一的前驱和后继(除了第一个和最后一个元素)。线性表可以具体实现为数组或链表等形式,这些数据结构允许数据按顺序存储和访问。

方法

  • insert(): 插入,一般有两个参数:插入位置 int pos, 和插入值 int val
  • remove(): 移除,一般只有一个参数:删除位置

应用

  1. 购物清单:购物时,你可能会有一个清单,列出你需要购买的商品。这可以视为一个线性表,其中商品的顺序表示你打算访问商店中各个区域的顺序。
  2. 待办事项列表:许多人使用待办事项列表来管理日常任务,这些任务可以添加、删除或重新排序,就像线性表中的元素一样。
  3. 文本编辑:在文本编辑器中,文本可以被视为字符的线性表,编辑器允许在任何位置插入和删除字符。

栈 Stack

  • 只能将数据存储在一端并且访问端节点数据,满足 LIFO 的原则
  • 可以通过链表或者数组进行实现

应用

  • 函数调用栈
  • 括号的匹配
  • 撤销操作 (web browser)
  • 页面访问历史 (其实和上面那个差不多)

队列 Queue

  • 满足 FIFO 的原则
    • 元素是从一端(通常称为“队尾”或“rear”)添加的,而从另一端(称为“队首”或“front”)移除的。这种结构设计意味着你只能在队列的尾部添加新元素,而从头部移除元素
  • enqueue 在链表的末尾插入一个新的节点
  • dequeue 在开头插入一个新的节点
  • 可以通过 链表,环形数组实现
    • 环形数组的首个元素在数组中的位置 front = (front + 1) % capacity
    • 末尾元素位置 rear = (rear + 1) % capacity

双向队列 Deque

和队列不同,双向队列同时能从首尾添加和删除队列元素

向量 Vector

初始化

  • vector<int> v 生成一个空的数组
  • vector<int> v(5) 生成一个长为 5 的数组,所有数据初始化为 0
  • vector<int> v(n,3) 生成一个 长度为 n 的向量,所有位置数据都是 3
  • vector<int> v = {1,2,3,4,5} 生成一个长为 5 的数组,
  • vector<int> v2(v) 复制一个向量

迭代器 iterator

常用方法
  • begin() 返回指向第一个元素的迭代器
  • end() 返回指向最后一个元素之后的迭代器
  • rbegin() 返回一个指向最后一个元素的反向迭代器(从容器的末尾开始并向开始方向移动)
  • rend() 返回一个指向第一个元素之前的迭代器
写法

for (std::vector<int>::iterator it = v.begin(); it != v.end(); ++it)

迭代器和指针

迭代器不是指针,但是迭代器和指针的功能很像

  • *it 返回迭代器指向的数据内容
    • 可以通过迭代器修改一个对象的内容 *it = 5
  • it++ 返回指向的下一个对象
  • it-- 返回指向的下一个对象
  • it + n 表示前进 n 个元素
  • it1 == it2 说明迭代器可以判断是否重合,但是 it1 < it2 的写法是非法的

动态内存分配

模板和容器

背景:在编译的时候会被预留 typename 的方法

语法

1
2
3
4
template <typename T>
class List{
...
}