280 复习
函数重载 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 | Base *Derived::clone(){ |
运算符重载 Operator Overloading
标准运算符 +-*/%
- 输入的数据类型必须是
const引用,说明这些输入值不会被修改 - 返回类型是一个 对象, 而不是引用,因为
- 区分是不是类的成员函数,如果是,那么第一个输入默认就是
*this
赋值运算符 =
- 返回的数据类型就是 引用
流操作符 >>, <<
应用对象:ios 对象,例如
1 | ostream& operator<<(ostream& os, const IntSet& s){ |
这样的写码方式确保了返回值可以连接起来串成一串
Subscript 操作符 []
首先我们定义的时候都会需要 const 和 非const两类返回值的函数,
友元关系 friendship
应用对象
非成员函数,但是想要使用类的私有变量
声明方式
1 | class A{ |
这里说明,函数 f 能够访问类 A 的私有变量,同时 类B (即成员函数) 也能访问 类 A 的私有变量
从属关系
- 友元函数、友元类并不是类的成员,也就是说,只是声明(只是认识认识)
- 友元函数不是互相的
A是B的友元表示B函数可以使用A的私有变量,但是A不可以使用B的私有变量 - 不能通过类的命名空间进行调用友元函数,即
A::f的写法是不合法的
常见容器
线性表 linear list
数据元素的有序集合,其中每个元素具有唯一的前驱和后继(除了第一个和最后一个元素)。线性表可以具体实现为数组或链表等形式,这些数据结构允许数据按顺序存储和访问。
方法
insert(): 插入,一般有两个参数:插入位置int pos, 和插入值int valremove(): 移除,一般只有一个参数:删除位置
应用
- 购物清单:购物时,你可能会有一个清单,列出你需要购买的商品。这可以视为一个线性表,其中商品的顺序表示你打算访问商店中各个区域的顺序。
- 待办事项列表:许多人使用待办事项列表来管理日常任务,这些任务可以添加、删除或重新排序,就像线性表中的元素一样。
- 文本编辑:在文本编辑器中,文本可以被视为字符的线性表,编辑器允许在任何位置插入和删除字符。
栈 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 的数组,所有数据初始化为 0vector<int> v(n,3)生成一个 长度为 n 的向量,所有位置数据都是 3vector<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 | template <typename T> |
