第一节 预备知识
一 关键字
C++共计63个关键字,C语言共计32个关键字
nullptr为空指针的关键字,原本c语言中的NULL被定义为0
二 命名空间
C++标准库的命名空间,std
1 作用
防止定义的函数名与库中的/项目组多人定义的名字之间的冲突
(1)在C语言中若list.h/queue.h两个头文件中都包含struct Node{},则会直接报错
(2)在C++中可以命名两个空间,在不同空间中使用struct Node{}
2 展开方式
域作用限定符::
(1)挨个指定:在使用命名空间内容的时候,需要指定(保证安全但是繁琐)
(2)全局展开:using namespace A;将A空间全部展开了,不用再按个指定(但是不安全),且容易出现重名情况
一般不建议全局展开(一般在项目中禁止使用),大多数是部分展开+指定命名空间访问,在日常训练中可以全局展开
(3)部分展开:using std::cout
此时在项目的使用中,cout的使用不再需要std::cout,可以直接使用
3 注意事项
(1)命名空间域只会影响使用,不影响生命周期
(2)当命名空间重名的时候,编译器会自动进行合并操作,合并为一个空间
(3)命名空间之间可以嵌套
(4)命名空间也重名的话,别无他法,只能去修改一个
三 常用运算符
1 输入输出运算符
cout和cin均可以自动识别类型
(1)流插入运算符:<<
cout<<endl;等价于cout<<'\n';
(2)流提取运算符:>>
(3)控制精度问题
在需要控制精度时,使用printf,虽然cout也可以,但不推荐(使用起来很麻烦)
在需要输出的时候cout/printf哪个方便,就是用哪个
四 缺省参数
缺省参数之间不构成函数重载
函数的缺省值只能在声明中给出(不可以在定义中设置)
应用实例:在用malloc申请空间时,可以在函数中设置缺省(默认空间大小)
五 函数重载
在同一作用域内,因为在c语言中直接用函数名去查找函数,所以不允许出现重名的函数,在c++中依靠函数重载解决了这个问题
1 函数重载的条件
参数的个数不同(int)(int,int)
参数的类型不同(int)(double)
参数的顺序不同(int,double)和(double,int)
2 注意事项
函数在编译阶段对函数名进行了单独的修饰,从而区分同名的函数(注:不同平台环境的修饰方式不同)
返回值类型不同不可以实现函数重载,因为在调用时无法区分想要的返回值类型,调用时只能知道参数的信息
六 引用
1 概念
为已有的变量(任意类型)取一个别名(外号)
一个变量可以有多个别名,同时也可以为别名起别名
2 区别于typedef
引用用于为已存在的对象提供别名,而typedef用于创建类型别名
3 简单应用
(1)做输出型参数
可以有效减少1、2级指针的使用
(2)做返回值
适用范围:出了作用域不销毁的(静态static、malloc、上一层栈帧的、全局的等),否则,空间被销毁后,交还给系统,无法确定存储内容,返回值结果为随机值
优点:避免拷贝开销、允许修改原始对象(int& fun(int& num))
4 权限问题
指针/引用在进行赋值/初始化时的权限,可以缩小不能放大
4.1 返回值
返回值具有常性,不能修改,因为返回值是先将返回值存入临时变量中,再返回,而临时变量具有常性,所以不能修改
4.2 隐式类型转换
会在中间创建一个double类型的临时变量存放a的数值,因此具有常性,不能修改
5 指针与引用
指针 | 引用 | |
相同点 | 底层汇编:引用是用指针来实现的(基本一致) | |
不同点 | ①语法方面:开辟了新的空间(存储一个变量地址) ②指针在定义时没有要求 ③指针在任何时候都可以指向任何一个同类型的实体 ④有NULL指针 ⑤sizeof(指针) == 4/8 ⑥++,指针向后偏移一个该类型的字节长度 ⑦存在多级指针 ⑧指针:需要显示解引用(*) | ①语法方面:未开辟空间(定义了一个变量的别名) ②引用在定义时必须初始化 ③在初始化一个实体后,就不能再引用其他的实体 ④没有NULL引用 ⑤sizeof(引用) == 引用类型的大小 ⑥++,实体+1 ⑦不存在多级引用 ⑧引用:编译器来处理 |
引用的优点 | 比指针使用起来更安全,因为不存在空引用,并且只能引用一个对象 |
6 延长匿名对象的生命周期
使用const T& xx = T(),可以延长匿名对象的生命周期,使得逆命对象的生命周期等同于xx的生命周期,不会只是当前行了,以后xx就是匿名对象了
必须要有const,因为匿名对象/临时对象都有常性
七 内联函数(inline)
对于编译器来说,内联函数只是一个建议,不同的编译器实现的机制不同(具体是不是当作内联函数使用,完全取决于编译器本身),从而防止编程的人乱来
1 适用场景
函数规模小(十几行)、非递归、被频繁使用
2 与宏的比较
在C++中,通常用const/enum来代替宏常量,同时使用inline来代替宏函数
优点 | 缺点 | |
宏 | ①类型不固定(int/double等),各种类型都可以带入使用 ②不用建立栈帧(所以不能调试) | ①不能调试(在于处理阶段直接进行替换) ②有些场景下很复杂,易出错(例如:括号的使用) ③没有类型安全的检查(纯粹简单的替换,不会区分传入是int还是double) |
优点 | 缺点 | |
inline | ①不会建立栈帧,且使用方式和函数相同(解决了宏的缺点) ②效率等同于宏 | 用空间(非内存)换时间(编译后的程序空间变大) |
3 注意事项
声明和定义不要分离,会出现链接错误,要写在一个文件中
(内联函数不进入符号表,无法依据声明来找函数本身)
(内联是在预编译时被展开,展开就没有函数地址了,链接就会找不到)
八 auto
1 意义
用于简化代码,类型名很长时,自动推导
类型名很长时也可以用typedef,但是不如auto
auto缺点 | typedef缺点 |
在不熟悉的时候,区分不开定义的变量是什么变量 | 很容易出错,例如: typedef char* pchar; const pchar p1;//错误 //实际上:(char* const p1) const pchar* p2; |
2 经典应用
范围for(自动依次取出数据赋值给e对象,自动判断结束)
3 注意事项
同一行声明多个变量时,类型要相同。错误示例:auto a=4, b=1.3;
不能用来声明数组:auto arr[10];
不能做形参:int add (auto, int);
第二节 类和对象(上)
C面向过程:关注的是过程,分析出解决问题的步骤,通过函数调用逐步解决问题
C++面向对象:关注的是对象,将一件事情拆解为不同的对象,靠对象之间的交互完成
一 实例化
开辟空间的时候struct A a;
二 访问限定符
public(公共的)
private(私有的)
protected(受保护的)
struct在不使用访问限定符时:默认为public
class在不使用访问限定符时:默认为private
三 成员变量和成员函数
1 存储位置
成员变量在对象中,成员函数不在对象中。
每个成员变量是不一样的,独立存储,占用不同的空间,存在内存对齐;不同对象调用的成员函数是一样的,存放于公共区域(代码段中)
若类内无成员变量,则sizeof(A) == 1,用来占位,标识对象的存在,被定义出来了(不存储有效的数据)
四 this指针
1 this指针可以显示使用
不可以自己加上this指针,由编译器自己处理,但是可以在类中显示的使用this指针
2 this指针存在位置
在栈上,因为是形参(隐含的-编译器自己加的),但是在vs中是通过ecx寄存器(因为使用频繁,所以做了优化)
(计算对象大小的时候,并没有算this,所以不在对象中)
3 this指针可以为空吗
this指针可以为空,但不能进行解引用操作
因为第一步fun()并未对this指针进行解引用操作,本质上只是调用fun()函数;但第二步进行了解引用操作,从而引发异常
第三节 类和对象(中)
一 默认成员函数
初始化和清理 | ||
---|---|---|
函数名 | 构造函数 | 析构函数 |
函数形式 | 类名() | ~类名() |
作用 | 初始化工作 | 清理工作 对象在销毁时(出作用域时)会自动调用析构函数,完成对象资源的清理工作(代替的是destroy函数的作用) |
注意事项 | ①函数名与类名相同 ②无返回值 ③对象实例化时编译器自动调用对应的构造函数 ④构造函数可以重载(有多个构造函数) ⑤无参构造/全缺省构造/默认生成的构造函数,均为默认构造函数(不用传参) ⑥无参构造和全缺省构造,两者语法上可以同时存在,但是调用时存在歧义(两者一般不会同时存在) ⑦默认生成的构造函数 内置类型成员(int、double、char、int*、double*等)不做处理 自定义类型成员会自动调用默认构 ⑧在声明时可以加上缺省值,此时未进行初始化,只是在声明位置加了缺省值。 未写构造函数时,才会用缺省值 (C++11中打的补丁) | ①函数名在类名前加上~ ②无参数+无返回值 ③一个类只能由一个析构函数(不可重载) ④对象生命周期结束时,自动调用析构函数 ⑤默认生成的析构函数 内置类型成员(int、double、char、int*、double*等)不做处理 自定义类型成员会自动调用默认构造 ⑥后定义的先析构 |
示例图 | ![]() |
拷贝复制 | 取地址重载 | ||
---|---|---|---|
函数名 | 拷贝构造 | 赋值重载 | |
函数形式 | 类名() | operator= | operator& |
注意事项 | ①浅拷贝 导致两个指针指向同一块地址,修改一处,另一处也会受到影响 重复析构,导致程序崩溃 ②深拷贝 各自有各自的空间,防止浅拷贝问题的出现 ③默认生成的拷贝构造 内置类型成员(int、double、char、int*、double*等)直接拷贝(浅拷贝,按bite一个个的拷贝) 自定义类型成员:调用拷贝构造 ④需要写拷贝构造的情况:自己实现了析构函数来释放空间 ⑤为了提高程序效率 一般对象传参时,尽量使用引用类型(传参时调用拷贝构造,创建形参) 返回时根据场景,能用引用就用引用(以数值的形式返回时,会调用拷贝构造创建临时对象) ⑥拷贝构造的使用 ⑦区别于普通构造函数 | ①默认生成的复制重载: 内置类型成员(int、double、char、int*、double*等)默认生成 自定义类型成员:要调用拷贝构造(自己实现) ②区分拷贝构造和赋值重载: 赋值重载是两个已经实例化定义好的对象 | 不重要 |
二 运算符重载
为了实现内置类型的运算
1 注意事项
运算符重载不可自创运算符,例如:@
重载运算符的参数中必须有一个 类 类型参数(int,Date)
不可对内置类型进行运算符重载(int,int)
在类内定义时,形参比实际少一个(类内成员函数)
5个不可以重载的运算符:域作用限定符(::)、sizeof、成员访问(.)、三目运算符(?:)、(.*)(不清楚该运算符的使用,c语言中就有的运算符)
三 获取private的成员变量
提供一个成员函数专门实现各个数据的访问
友元函数(friend+函数声明)
成员函数可以访问数据
四 初始化列表
const类/引用类/自定义类的变量(自定义类中无默认构造(无参)),三类必须在定义位置初始化,即在初始化列表初始化
成员变量定义的位置:初始化列表
对象整体定义的位置:Date d;
不论是否显示在初始化列表中写出,编译器会为每个变量,都进行初始化列表进行初始化
先声明的先进行初始化,继承顺序为声明顺序
在初始化列表中,每个成员只能出现一次
五 类型转换
1 单参数类型构造
在上述位置加上explicit后将不再能进行类型转换
2 多参数构造
仅仅在c++11中才可以进行类型转换,形式为Date d3 = {2023,12};
同样加上explicit后将不再能进行类型转换
六 静态成员
1 静态成员变量
2 静态成员函数
七 友元函数
会提供便利,但不宜多用,会增加耦合度,破坏了封装
1 友元函数
可访问类的私有/保护成员,但不是类的成员函数
不能用const修饰
可以在类定义的任何地方声明,不受类访问限定符限制
一个函数可以是多个类的友元函数
友元函数的调用与普通函数的调用原理相同
2 友元类
友元关系是单向的,不具有交换性
友元关系不能传递
友元关系不能继承
八 匿名对象
九 内部类
内部类的书写位置:公有/私有,会受到了A的类域限制
1 演示示例
十 内存管理
1 函数介绍
malloc | calloc | realloc | |
①不会进行初始化 ②不够简洁,需要强转 ③无法适应自定义类型,不能调用构造/析构函数 ④malloc无法初始化 ⑤手动计算申请空间大小 ⑥ malloc申请空间失败后返回空指针 | 自动初始化为0 | ①原地扩容 ②异地扩容(自动释放之前的空间) | |
new | delete | operator new(使用少) | operator delete(使用少) |
①首先申请需要开辟的空间 ②其次调用构造函数进行初始化操作 ③为自定义类型而准备,会自动调用构造/析构函数 ④new可以初始化 int* p2 = new int(5); int* p1 = new int[5]{1,2,3}; ⑤无需计算申请空间大小 ⑥申请空间失败后抛异常 | ①首先调用析构函数 ②释放空间 | ①本质上是对malloc的封装 int* p = (int*)opeartor(sizeof(int)) ②申请空间失败后抛异常 | 本质是对free的封装 |
使用顺序: new使用时会调用operator new,operator new在使用时会调用malloc delete使用时会调用operator delete,operator delete在使用时会调用free |
2 搭配使用
最好是搭配使用,不然有时结果不可确定,非常容易出错
malloc/free | new/delete | new[]/delete[] |
delete[]可以自动识别使用new[]开辟的空间大小,从而实现释放的功能(方式依据编译器不同而有所不同) | ||
都从堆上申请空间,需要手动释放 需要调用析构函数释放空间的时候,使用free会造成内存泄漏的问题 | ||
![]() |
3 指针和变量
4 异常的捕捉
try+catch
5 定位new
(1)适用场景
(2)使用方式
A* p = new A;
new(p1) A(1); //初始化为1(会使用构造函数来初始化)
delete p1
十一 模板
1 函数模板
template <class T> 或 template<typename T>
(1)不同类型参数调用时并不是一个函数,会由编译器根据模板实例化(刻画出)一个函数,实际的代码量不会减少,只是由编译器来代替完成
模板可以与自己书写的函数同时存在(大致理解为函数重载),优先调用自己的函数,因为在使用模板的话会多生成一份,过于麻烦
2 类模板
声明和定义分离的(最好在同一个文件中,否则会出现链接错误)
只能显式实例化
2 类型参数/非类型参数
template<class T = int, size_t N = 20>
类型参数:T,默认为int
非类型参数:N,默认为20
template<class T = int, size_t N = 20>
class A
{
public:
private:
T _arr[N];
//T默认为int,N默认为20
}
int main()
{
A a;
A<double,50> b;
}
3 模板特化
模板特化有两种形式:全特化和偏特化
(1)全特化只能针对某种类型,必须要初始化
(2)偏特化,能针对指针/引用一大类型
(3)必须要有原模版,只有特化模板会出错
template<class T>
bool Less(T left, T right)
{
return left < right;
}
//全特化,只针对int*类型
template<>
bool Less<int*>(int* left, int* right)
{
return *left < *right;
}
//偏特化,针对所有指针类型
template<class T>
bool Less(T* left, int* right)
{
return *left < *right;
}
4 注意事项
(1)模板参数可以设置缺省值
//模板参数也可以设置缺省值
template <class T, class Y = int>
void add(T a, Y b)
{
cout << a + b << endl;
}
void add(size_t a, size_t b)
{
cout << a + b << endl;
}
int main()
{
add(2,3);
//此时调用的是模板,因为2,3为int型,若调用size_t的实例,会涉及到类型转换
}
(2)模板的分离编译
类模板的声明和定义分离的时候,在链接前不可以实例化,因为不确定模板中究竟是什么类型,无法实例化,从而在链接阶段(看下面)找不到连接的地址,从而出错
解决分离编译的办法:
①显式实例化
不好用,只能解决一个类型,实际中一般不使用
②声明和定义在同一个文件中,此时直接就可以实例化,编译时就有地址,不需要链接
5 优缺点
(1)优点:模板服用了代码,节省了资源,更快的迭代开发,C++的标准模板库因此而产生
增强了代码的灵活性
(2)缺点:模板会导致代码膨胀的问题,也会导致编译时间变长
出现模板编译错误时,错误信息非常凌乱,不易定位错误
第四节 STL
一 发展史
STL是C++标准库的重要组成部分,主要包含数据结构与算法的软件框架,发展版本由原始版本 --> P.T.版本(微软使用版本) -- > RW版本 --> SGI版本(Linux使用版本)
二 六大组件
三 string类
1 成员函数总结
构造 | ①默认构造函数 string str1; ②字符串字面值构造函数 string str2 = "hello"; ③子字符串构造函数,用前三个来构造 string str3 (str1, 0, 3); string str3 ("hello",3); ④重复字符构造函数 string str4 (5,'x'); ⑤迭代器区间构造函数 string str5(str.begin(), str.begin() + 3); |
拷贝构造 | string str1(str) |
析构 | |
赋值 | string str1 = str; |
size() | 实际存储字符的个数 最大长度:max_size() |
length() | 实际存储字符的个数 先有的length,但size更适合其他容器,所以size用的多 |
capacity() | 容量的大小,实际capacity可能略大于申请的容量 |
c_str() | 读取内容直到\0处 为了兼容c语言 |
== | 为了判断string类型的是否相同 s1 == s2 |
swap() | 不会产生临时变量 仅仅交换指针的指向 |
find() | 从开始位置开始查找,找到后返回下标位置 若查找不到,会返回npos(整型最大值) find()既可以查找字符也可以查找字符串 |
rfind() | 从最后开始查找,找到后返回下标位置, 若找不到则返回npos(整形最大值) |
find_first_of() | 找出所有的字符串中内容的位置,找不到时返回npos(整型最大值) 默认每次均从下标0处开始查找 |
find_last_of() | 找到查找字符中,最后包含其中字符的位置 |
find_first_not_of() | 找出所有非字符串中内容的位置 默认从0下标处开始查找 为了防止死循环,设置从pos+1位置处开始查找 |
+= | 更简洁 既可以尾插字符串,也可以尾插字符 (但底层实现由append和push_back完成) |
append | 尾插字符串 |
push_back() | 尾插字符 |
insert() | ①insert(pos,"") pos位置插入字符串 ②insert(pos,1,'!') pos位置插入一个字符! ③迭代器插入,只能插入单个字符 insert(s.begin(),'!') |
erase() | ①erase(pos) 缺省值为npos,删除到最后 ②erase(pos,n) 删除n个字符 ③erase(pos,n) n>size,删除到最后 ④迭代器删除 erase(s.begin(),n) |
resize() | 扩容:resize(n) resize(n,'x') 若进行扩容操作,后面扩容部分,默认初始化为0 缩小:resize(n) 若为缩小,则只保留前n个数据 注:只改变size不改变capacity |
reserve() | 提前知道需要开辟多少空间,减少扩容次数,提高效率,只扩容不初始化 只修改capacity()的大小 |
assign() | assign("haha") 替换string中的内容 |
replace() | replace(pos,x,"") 将pos位置处的x个字符替换为某字符串 可能涉及扩容/挪动数据(能少用就少用) |
substr() | substr(pos,n) 从pos位置取出n个字符 |
at() | at()的使用方式等同于下标法[],不同之处在于,当越界访问时,[]会直接报错(断言的方式),而at会抛异常,其中异常可以用try--catch被捕获 |
shrink_to_fit() | 会去额外开辟一块新空间来存放原数据,然后释放掉原空间(非原地缩容) 仅改变capacity |
iterator | begin()第一个字符的位置 end()最后一个字符的后一个位置 定义一个string中的iterator类it,存放起始位置,然后依次向后查找 |
reverse_iterator | begin()最后一个字符的位置 end()第一个字符的前一个位置 ++会向左边移动 |
const_iterator | 用于接收const类型的变量 |
const_reverse_iterator | 用于接收const类型的变量 |
范围for | 本质也是迭代器,只是用起来简单一些 |
共计4种迭代器,iterator/reverse_iterator/const_iterator/const_reverse_iterator,均可以写为auto,但是要在熟悉的时候使用,不然用以出错 其中的const迭代器中的const,只是名字的一部分,为了保证*it不被修改 对比迭代器和[]的使用,在string/数组/顺序表中,更多的使用下标法[],但是在链表/树/哈希中无法使用[],只能使用迭代器来访问 |
2 遍历方式总结
下标法
迭代器
范围for
3 内存存储情况
在vs中,string中包含一个_buf[16]的数组,当内容小于16时,存入数组;当内容大于16时,首先开辟32个空间,然后空间不足的时候,1.5倍增长空间大小(具体也和编译器相关)
在linux中仅仅存放一个指针,指向存储内容的指针(内容:size+capacity+引用计数(有几个对象使用)+内容),在进行修改的时候,再进行拷贝工作
四 vector
1 成员函数(目前还不完善)
构造函数 | ①默认构造函数 vector<int> v1; ②大小和初始值构造函数 vector<int> v2(5, 10);//5个10 ③迭代器区间构造函数 string str5(v.begin(), v.begin()+3); |
析构函数 | |
拷贝构造 | vector<int> v1(v); |
赋值 | v1 = v; |
size | |
capacity | 要多大的空间就会给多大,不会像string中的capacity一样 |
front | 访问首个元素 |
back | 访问最后一个元素 |
insert | 不推荐使用,效率低 ①插入一个元素 v.insert(v.begin()+2, 100); ②插入一个范围 v.insert(v.begin()+2, v1.) |
erase | 不推荐使用,效率低 v.erase(v.begin()+2); |
resize | 使用频繁 |
at | 用于访问 但是[]在访问越界时不会进行检查,可能导致程序错误
|
(1)vector中无find成员函数,想要使用查找的时候,均使用std中的find
(原因:std中的find既可以给sting、vector、list等使用,而string为了实现某些特定的功能,单独实现了find)
(2)vector<char>不能替换string,只是结构类似
string中的最后有\0
string比较大小按照ascii比较
string中的+=在字符串中使用比较方便
sting中的find可以实现自己单独的功能
(3)
2 迭代器失效问题
(1)pos位置的意义改变
在进行erase时,pos位置不变,但是该位置处的内容已经不是之前的内容了
(2)pos变为野指针
在进行insert时,进行扩容,pos进行了更新,但是外面的pos并不会更新,导致,外面pos指向的位置仍然是指向一个已经被释放的空间的位置
在进行erase时,pos指向的时最后一个位置,但是进行删除后,访问该位置为越界访问
(3)解决迭代器失效的问题
不可以通过pos的引用传参来解决,因为begin()是传值返回,临时变量具有常性,不可修改
可以通过insert函数使用返回值,返回插入数据的位置,以达到更新pos的目的
(4)总结
insert和erase操作后,最好不要进行访问操作了,因为insert后访问的话,可能会有野指针的问题;erase后也不要来访问,因为可能会有结果未定义的情况
string中的insert和erase也会涉及迭代器失效的问题,但是string大多用下标
3 遍历方式总结
下标法
迭代器
范围for
4 深浅拷贝的问题
(1)浅拷贝的问题:指向同一块空间+多次析构
(2)较为传统的两种方式:reserve开辟空间+push_back和memcpy的拷贝方式
深拷贝时,若用memcpy进行拷贝,但memcpy进行的也是浅拷贝,若是拷贝的内容中包含指针,则仍会出现上面的浅拷贝的问题(多出现于自定义类型中)
(在reserve的时候也不能使用memcpy,不然也会出现这个错误,也要使用一个个赋值的方式进行拷贝)
解决办法:使用赋值即可(就是深拷贝了),让自定义类型用自己的方式来进行这个拷贝的过程
5 sort使用方法
//常规使用(升序)
sort(v, v+sizeof(v)/sizeof(int));
//降序
sort(v, v+sizeof(v)/sizeof(int), greater<int>());
6 实际应用
(1)通过类模板实现类似于二维数组的效果
不同于二维数组的地方:二维数组的每行不能不等,都是同样长度,更优于二维数组
五 list
是一个带有头节点的双向链表
1 成员函数(不完善,模拟实现较复杂)
构造函数 | ①默认构造函数 list<int> myList; ②大小和初始值构造函数 list<int> myList(n, x);//n个x ③迭代器区间构造函数 |
析构函数 | |
拷贝构造 | list<int> myList(otherList); |
赋值 | v1 = v; |
size | |
capacity | 要多大的空间就会给多大,不会像string中的capacity一样 |
front | 访问首个元素 |
back | 访问最后一个元素 |
insert | 不推荐使用,效率低 ①插入一个元素 v.insert(v.begin()+2, 100); ②插入一个范围 v.insert(v.begin()+2, v1.) |
erase | 不推荐使用,效率低 v.erase(v.begin()+2); |
2 sort
list的sor的性能不行,vector中的sort效率是list中的4倍
3 迭代器的分类
一般从迭代器名字就可以看出来
①单向(单链表) ++ (只能++不能--)
②双向(链表) ++ --
③随机 ++ -- + -(随机是特殊的双向)
要求单向可以传入双向,要求双向可以传入随机
4 list和vector的区别
vector | list | |
底层结构 | 动态顺序表,一段连续的空间 | 带头结点的双向循环链表 |
随机访问 | 支持随机访问 访问某个元素效率O(1) | 不支持随机访问 访问某个元素效率O(n) |
插入和删除 | 任意位置插入和删除效率低O(n) 插入可能需要扩容,开辟新空间,拷贝数据,释放就空间,导致效率更低 | 任意位置插入和删除效率高O(1) 无需搬移数据 |
空间利用率 | 连续的空间,不易造成内存碎片,空间利用率高,缓存利用率高 | 底层节点动态开辟,小姐点容易造成内存碎片,空间利用率低,缓存利用率低 |
迭代器 | 原生态指针 | 对原生态指针(结点指针)进行封装 |
迭代器失效 | 在插入元素时,要给所有迭代器重新赋值,因为插入数据可能导致重新扩容,从而致使迭代器失效;删除数据,可能导致迭代器要重新赋值,否则会失效 | 插入数据不会导致迭代器失效;删除数据,只会导致当前迭代器失效,其他迭代器不受影响 |
适用场景 | 需要高效存储,支持随机访问,不关心插入删除效率 | 大量插入和删除操作,不关心随机访问 |
5 注意事项
(1)不会涉及到扩容等
(2)erase会导致迭代器失效问题,毕竟已经清除了
(3)哨兵位不饿能erase
(4)注意区分:clear和析构
clear:清除所有的节点,不清除头节点
析构:彻底不用了,释放头节点
六 栈
栈满足后进先出,且不是容器,是容器适配器,因此无迭代器
七 双端队列(deque)
对标的是:vector + list,因为它既支持vector的功能又支持list的功能
1 适用场景
需要大量头插头删,尾插尾删时,做适配的默认容器是很好的
2 注意事项
(1)排序的时候,用list和deque都不如vector,甚至不如将list和deque内容拷贝到vector,排完后再拷贝回来的速度
(2)虽然既支持vector的功能又支持list的功能,但是效率都比较一般
八 优先级队列
#include <iostream>
#include <queue>
using namespace std;
int main()
{
priority_queue<int> pq;
pq.push(30); // 插入元素30
pq.push(10); // 插入元素10
pq.push(50); // 插入元素50
pq.push(20); // 插入元素20
while (!pq.empty()) {
cout << pq.top() << " "; // 输出最高优先级的元素
pq.pop(); // 删除最高优先级的元素
}
//输出结果:50、30、20、10
return 0;
}
第五节 继承
面向对象的语言(C++等)的三大特性:封装(类和对象)/继承/多态
一 父类(基类)与子类(派生类)
1 简单示例
class Person
{
public:
protected:
string _name = "LiMing";//姓名
size_t _age = 18;//年龄
};
class Student:public Person
{
public:
void fun()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
cout << "stuid:" << _stuid << endl;
}
protected:
size_t _stuid = 58;//学分
};
2 继承的方式
继承方式分为:公有继承、保护继承、私有继承,实际中一般都是使用公有继承(public)。
(1)class中默认继承方式为:private
struct中默认继承方式为:public
(2)权限的比较
公有继承 > 保护继承 > 私有继承
(3)私有(private)和保护(protected)
保护(protected) | 私有(private) | |
类内 | 可以访问 | 可以访问 |
派生类 | 可以访问 | 不能访问 |
类外 | 不能访问 | 不能访问 |
(4)继承基类成员访问方式的变化
基类常用public和protected成员,而派生类常用public继承
在派生类中不可见:子类不能使用父类中的内容
3 基类和派生类的赋值转换
子可以给与父,但是目前父不能给予子,因为父内容比子的内容少
Student s1("Zhangsan", 20, 100);
Person p1;
p1 = s1;
//此时不存在隐式类型转换,不会产生临时变量
//会自动分离出来父类的内容,并将其赋值过去(切割/切片)
//会调用拷贝构造
4 继承中的作用域
(1)基类和派生类具有独立的作用域,只有在同一作用域中才能构成函数重载
(2)父类和子类中同名的函数名,构成隐藏的关系,将父类中同名函数隐藏屏蔽(只要函数名相同就构成隐藏)
(3)父类和子类中可以定义同名的变量 (且遵循就近原则)
(4)不建议使用同名函数
Student s1("Zhangsan", 20, 100);
//调用Student中的函数
s1.fun();
//调用父类Person中的同名函数
s1.Person::fun();
5 派生类的默认成员函数
(1)构造函数
不显式的写出来的话,会自动调用父类的默认构造函数
子类不能初始化父类的,只能调用父类的构造函数进行初始化操作
class Student : public Person {
public:
Student(const string& name, size_t age, size_t stuid)
//调用父类的构造函数
: Person(name, age)
{
_stuid = stuid;
}
private:
size_t _stuid;
};
(2)拷贝构造
基本同上
class Student : public Person {
public:
Student(const Student& s)
//调用父类的拷贝构造
:Person(s)
,_stuid(s._stuid)
{}
private:
size_t _stuid;
};
(3)赋值重载
要注意:隐藏的问题,因为函数名相同
Student& operator=(const Student& s)
{
if(s != this)
{
//构成隐藏,需要显式调用Person中的赋值重载
Person::operator(s);
_stuid = s._stuid;
}
return *this;
}
(4)析构函数
不用显示的调用父类析构函数,正常使用析构函数即可
因为:①要保证后构造的先析构。
先构造的基类,后构造的派生类,若在析构的时候调用基类的析构函数,则会先析构基类,不正确
②由于多态的原因,析构函数在之后均会被处理为Destructor(析构),函数名之间构成隐藏
③若是先析构基类,派生类中的一些指针可能变为野指针
6 继承和友元
(1) 友元关系不能被继承。
父类的友元函数不能访问子类的成员,想要访问子类的成员只能在子类中再定义一个友元
(2)静态成员的继承
父类中的静态的变量,子类中会继承到该静态的变量,但是不论继承多少次,此静态成员变量始终都是同一个(因为地址相同)
7 实现一个不能被继承的类
(1)构造函数私有化/析构函数私有化
(2)final关键字
8 继承
(1)继承:单继承和多继承
(2)多继承的问题可能会导致菱形继承
菱形继承会导致数据冗余和二义性,数据冗余会导致空间浪费的问题
(3)使用指定访问的方式解决二义性
//基类
class Base {
public:
void someFunction() {
cout << "Base class function" << endl;
}
};
//基类的派生类1
class Derived1 : public Base {
public:
void someFunction() {
cout << "Derived1 class function" << endl;
}
};
//基类的派生类2
class Derived2 : public Base {
public:
void someFunction() {
cout << "Derived2 class function" << endl;
}
};
//两个派生类的派生类
class Diamond : public Derived1, public Derived2 {
public:
void callBaseFunction() {
// 使用作用域限定符指定调用 Derived1 的成员函数
Derived1::someFunction();
// 使用作用域限定符指定调用 Derived2 的成员函数
Derived2::someFunction();
}
};
(4)使用虚继承来解决菱形继承的两个问题
虚继承会在每个结构体的存储空间上多加上一个指针,这个指针指向记录偏移量的空间,用来记录存放基类的内容相对于指针所在地址位置的偏移量
相同的结构体,拥有的偏移量空间是一样的,所以相同的结构体,只需要一片空间来存放记录偏移量数值的即可
#include <string>
#include <iostream>
using namespace std;
class Base {
public:
void someFunction() {
cout << "Base class function" << endl;
}
};
class Derived1 : virtual public Base { // 虚继承
public:
void someFunction() {
cout << "Derived1 class function" << endl;
}
};
class Derived2 : virtual public Base { // 虚继承
public:
void someFunction() {
cout << "Derived2 class function" << endl;
}
};
class Diamond : public Derived1, public Derived2 {
public:
void callBaseFunction() {
// 显式指定调用 Derived1 的 someFunction()
Derived1::someFunction();
}
};
int main()
{
Diamond().callBaseFunction();
return 0;
}
(5)继承的总结和反思
多继承要少用,且一定不要设计出菱形继承【例题:3月27,1:30】
9 组合
(1)组合和继承都是复用,但是继承的耦合度更高,组合的耦合度较低
(2)继承可以直接用除private内容,改动保护(protected)可能会影响派生类
组合只能使用public中的内容,其中改动私有(private)和保护(protected)基本不影响其他
(3)相对来说,组合更好,因为耦合度较低,改动对其他的类的影响较小,如果既能使用继承也能使用组合,更推荐使用组合
//继承
class A
{}
class B : class A
{}
//组合
class C
{}
class D
{
private:
C _cc;
}
第六节 多态
继承是复用,多态是多种形态
一 多态的两个条件
虚函数virtual,虚函数和虚继承没有任何关系,只是共用一个关键字,且虚函数是为了重写(覆盖),重写是为了多态
(1)虚函数的重写(三者相同①函数②参数③返回值)
重写/覆盖,才能修改基类虚表的功能,从而实现谁调用就使用谁的虚表
(2)父类的指针或引用去调用(父类;指针和引用)
只有这样才能,既指向父类又能指向子类
class A
{
public:
virtual void fun1()
{
cout << "A" << endl;
}
};
class B :public A
{
virtual void fun1()//虚函数的重写
{
cout << "B" << endl;
}
};
void fun(A& a)//父类的指针/引用
{
a.fun1();
}
int main()
{
A a;
fun(a);//输出结果为A
B b;
fun(b);//输出结果为B
return 0;
}
(3)当不满足多态的任一条件时,就不会构成多态
谁调用的,就会输出谁的
class A
{
public:
virtual void fun1()
{
cout << "A" << endl;
}
};
class B :public A
{
virtual void fun1()//虚函数的重写
{
cout << "B" << endl;
}
};
void fun(A a)//假设不满足:父类的指针/引用
{
a.fun1();
}
int main()
{
A a;
fun(a);//输出结果为A
B b;
fun(b);//输出结果为A
return 0;
}
二 虚函数的两个意外
但是最好不要按照两个意外这样去写
1 子类中可以不写virtual
更有益于析构函数,因为析构函数被统一设置为destruction后,此时满足三个相同。防止父类指针指向子类函数,但是调用析构的时候,不会调用子类析构的现象的产生。
class A
{
public:
virtual void fun1()
{
cout << "A" << endl;
}
};
class B :public A
{
void fun1()//虚函数的重写(不写virtual)
{
cout << "B" << endl;
}
};
2 返回值可以不同(协变)
两种例外可以叠加,但是协变用的很少
class A
{
public:
virtual void fun1()
{
cout << "A" << endl;
}
};
class B :public A
{
int fun1()//虚函数的重写(不写virtual),且返回值不同
{
cout << "B" << endl;
}
};
三 override和final
override:检查派生类虚函数是否重写了基类某个虚函数,若果没有重写就会编译报错(检查是否完成重写,不是就会报错)
final:修饰虚函数,表示该虚函数不能再被重写(很少使用)
四 重载、重写、隐藏
熟悉三者的不同
五 抽象类
在虚函数后面加上=0,这个函数就是纯虚函数,包含纯虚函数的叫做抽象类(接口类),抽象类不能实例化出来对象
(1)若有一个类继承了抽象类,那么这个派生类也不可以实例化出来对象(继承了抽象类的类,也是抽象类,同样也包含纯虚函数)
(2)子类可以通过重写来脱离抽象类,不重写的话,不可以实例化出来对象
(3)纯虚函数和override的区别:
override是检查重写,在子类
纯虚函数是间接强制重写,在父类
六 虚函数表
每个类都会有一个指向虚函数表的指针,子类中首先会拷贝父类中的虚函数表,若子类中有重写,那么便会修改从父类中拷贝过来的虚函数表的内容,且子类中独有的虚函数,会写入自己的虚表之中
(1)类的大小会多出一个指向虚表的指针的大小
(2)虚函数表,本质上是虚函数指针数组。(数组,指针数组,虚函数指针数组)
(3)虚表中只存放虚函数的地址,其中的顺序是虚函数声明的顺序
(4)虚函数表是在编译的时候确定好的
(5)在多态中,谁调用就使用谁的虚表
(6)同类型的共用同一张虚表
七 为什么必须要指针/引用,直接使用对象不行呢?
(只拷成员,不拷虚表)
对象也可以实现切片的功能,切片功能不可以将子类中的虚表拷贝过去,若拷贝虚表,那么父类对象通过赋值/拷贝构造后,基类的虚表变为了子类的虚表,此时不满足同类型的共用同一张虚表了(同类型的虚表要相同)
八 打印虚表
虚表的地址存储在对象的起始位置的4/8字节内
虚表中的地址拿出来后,就能通过地址来调用函数(也可以用来判断地址中存放的函数是不是我们想要的虚函数)
1 int*强制类型转换
具有局限性,只能在32位下使用,因为int*只能访问4个字节的指针大小
typedef void(*VF_PTR)();
void fun(VF_PTR* table)
{
for (int i = 0; table[i] != nullptr; i++)
{
cout << "[" << i << "]" << ":" << table[i] << endl;
}
}
int main()
{
A a;
fun((VF_PTR*)(*(int*)&a));
B b;
fun((VF_PTR*)(*(int*)&b));
return 0;
}
2 *(VF_PTR**)&b(没有局限性)
A a;
fun(*(VF_PTR**)&a);
B b;
fun(*(VF_PTR**)&b);
指针的大小是4字节,强转为指针后,再解引用即可访问前4个字节的内容,但是因为需要的类型是VF-PTR*,所以要写成VF-PTR**
九 虚表的三个问题
1 虚表是在什么阶段生成的
在编译的时候生成
2 对象中的虚表指针什么时候初始化的
在构造函数的初始化列表阶段初始化的
3 虚表是存在哪里的
常量区
十 多继承中的虚表
1
#include<iostream>
using namespace std;
class A
{
public:
virtual void fun1()
{
cout << "A:fun1()" << endl;
}
virtual void fun2()
{
cout << "A:fun2()" << endl;
}
};
class B
{
public:
virtual void fun1()
{
cout << "B:fun1()" << endl;
}
virtual void fun3()
{
cout << "B:fun3()" << endl;
}
};
class C : public A , public B
{
public:
virtual void fun1()
{
cout << "C:fun1()" << endl;
}
virtual void fun4()
{
cout << "C:fun4()" << endl;
}
};
typedef void(*VF_PTR)();
void Print(VF_PTR* table)
{
for (int i = 0; table[i] != nullptr; i++)
{
cout << "[" << i << "]" << ":" << table[i];
VF_PTR fun = table[i];
cout << ":";
fun();
}
}
int main()
{
C c;
//表一
Print(*(VF_PTR**)&c);
cout << endl;
//表二
Print((VF_PTR*)(*(int*)((char*)&c + sizeof(A))));
//或者利用指针的偏移
//B* ptr = &c;
//Print((VF_PTR*)(*(int*)(ptr)));
return 0;
}
因为继承了几个基类,就会生成几个虚表(先继承/声明的会在前面)
所以此处有两个虚表,第一个虚表之中包含A类的fun1和fun2,以及C类中的fun4(其余虚函数)
第一个虚表之中包含B类的fun1和fun3
但是很容易发现两个fun1地址是不同的
调用重写的函数时,需要使用this指针来调用
一个继承的基类的地址正好就是该对象的起始地址,可以直接通过this指针来调用函数
二个类,处于第一个基类的末尾,它想要使用this指针来调用该函数的话,就需要从当前位置向前跨越上一个类的长度,来找到this指针,再通过this指针调用该函数
2 动态绑定和静态绑定
编译时就是静态 cin、cout
运行时就是动态
注意事项
一 空格输入
#include<iostream>
#include<string>
using namespace std;
int main()
{
char ch;
string str1;
string str2;
//证明cin遇到在遇到空格或其他空白字符时停止读取
cout << "str1开始输入:";
cin >> str1;
cout << str1 << endl;
//清除缓冲区
while (cin.get(ch) && ch != '\n')
{
continue;
}
//使用getline可以读取包含空格的整行字符串
cout << "str2开始输入:";
getline(cin, str2);
cout << str2 << endl;
//读取一个字符,并将其存储为 char 类型的数据
cout << "ch开始输入:";
cin.get(ch);
cout << ch;
return 0;
}