C++(知识点更新中)

第一节 预备知识


一 关键字

        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 函数介绍

malloccallocrealloc

①不会进行初始化

②不够简洁,需要强转

③无法适应自定义类型,不能调用构造/析构函数

④malloc无法初始化

⑤手动计算申请空间大小

⑥ malloc申请空间失败后返回空指针

自动初始化为0

①原地扩容

②异地扩容(自动释放之前的空间)

newdeleteoperator 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/freenew/deletenew[]/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类中的成员函数
构造

①默认构造函数

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 成员函数(目前还不完善)

list类中的成员函数
构造函数

①默认构造函数

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

用于访问 vector 容器中指定位置的元素,使用起来等价于[]

但是[]在访问越界时不会进行检查,可能导致程序错误

at() 将会抛出out_of_range 异常

(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 成员函数(不完善,模拟实现较复杂)

vector类中的成员函数
构造函数

①默认构造函数

list<int> myList;

②大小和初始值构造函数

list<int> myList(n, x);//n个x

③迭代器区间构造函数
list<int> myList(myVector.begin(), myVector.end());

析构函数
拷贝构造

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;
}

  • 19
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值