C++
C++
基本概念
变量:一块具有类型的内存(类型:数据的存储表示方式以及你可以对它进行的操作)
指针:一块内存的地址,指针的类型可以说明这个指针指向特定类型的变量(void*),指针可以发生变化,包括其所指向的地址的改变和其指向的地址中所存放的数据的改变。存在空指针。
引用:可以理解为指针的一种“语法糖”(左值引用/右值引用),引用其实就是变量的别名,从一而终,不可变,必须进行初始化。不存在空应用。
数组:内存中连续排列的多个同类型变量。数组名称可以用作指向第一个元素的指针
类:
对象:
指针与引用的区别
- 指针是一个实体变量,使用*符号;而引用只是个别名,使用&符号
- 指针可以为空,引用不可以为空
- 可以改变指针的指向,使其指向不同的内存地址。引用一旦被初始化,将一直引用同一个对象,不能改变绑定
- 指针通常用于动态内存分配、数组操作以及函数参数传递。引用通常用于函数参数传递、操作符重载以及创建别名
数据类型
整形
short至少16位,int至少与short一样长,long至少32位,且至少与int一样长,long long至少64位,且至少与long一样长。很多系统都是用最小长度,short为16位即2字节,int为32位即4字节,long为32位即4字节,long long为64位即8字节,可以使用sizeof()函数查看数据类型的长度
无符号类型
不存储负数值的整形,可以增大变量能够存储的最大值,数据长度不变。int被设置为自然长度,即为计算机处理起来效率最高的长度,所以选择类型时一般选用int类型
浮点型
float至少32位,double至少64位,long double至少80位
字符型
char至少8位,可以存储ASCII码,wchar_t至少16位,可以存储Unicode码
布尔型
bool类型只有两个值:true和false
关键字
const关键字
const修饰的值不能改变,是只读变量。必须在定义时就初始化,否则编译器会报错。const修饰的变量在编译时就会被替换成相应的值,不会占用内存
- 常量指针:指定义了一个指针,这个指针指向一个只读的对象,不能通过常量指针来改变这个对象的指。常量指针强调的是指针对其所指对象的不可改变性。$$const 数据类型* 指针变量 = 变量名\ 数据类型 const* 指针变量 = 变量名$$
- 指针常量:指定义了一个指针,这个指针的指只能在定义时初始化,其他地方不能改变。指针常量强调的是指针的不可改变性。$$数据类型* const 指针变量 = 变量名$$
左定值,右定向,看const是在*的左边还是右边
const的作用:用于指定变量、指针、引用、成员函数的只读属性 - 常量变量:const修饰的变量,不能被修改
- 指针和引用:const修饰的指针和引用,不能通过指针和引用修改所指对象的值
- 成员函数:const修饰的成员函数,该函数不能修改对象的成员变量(对于成员变量是非静态的情况)
- 常量对象:const修饰的对象,对象的成员变量不能被修改
- 常引用参数:const修饰的引用参数,该函数不能修改传入的实参
- 常指针参数:const修饰的指针参数,该函数不能修改指针所指对象的值
static关键字
static关键字主要用于控制变量和函数的生命周期、作用域以及访问权限。
- 静态变量
在函数内部使用static关键字修饰的变量称为静态变量
静态变量的生命周期:静态变量在程序运行期间一直存在,不会因为函数的退出而销毁
静态变量的作用域:静态变量只能在定义它的函数内部使用,不能被其他函数访问
静态变量的初始化:静态变量只会被初始化一次,即使函数被多次调用,静态变量也只会被初始化一次。默认初始化为0 - 静态成员函数
在类内部使用static关键字修饰的函数称为静态函数
静态函数属于类而不是类的实例,可以直接通过类名调用,不需要创建类的实例
静态函数只能访问静态成员变量和静态成员函数 - 静态成员变量
在类内部使用static关键字修饰的成员变量称为静态成员变量
所有类的对象共享同一个静态成员变量的副本
静态成员变量必须在类外部单独定义(不是声明),以便为其分配内存空间 - 静态局部变量
在函数内部使用static关键字修饰的变量称为静态局部变量
静态局部变量的生命周期为整个程序运行期间,不会因为函数的退出而销毁,但是作用域仍然是在定义它的函数内部
define与typedef
define
- 只是简单的字符串替换,不会进行类型检查
- 在编译预处理阶段起作用
- 可以用来防止头文件重复引用
- 不分配内存,给出的是立即数,有多少次使用就替换多少次
typedef - 有对应的数据类型,会进行类型检查
- 在编译、运行的时候起作用
- 在静态存储区中分配内存,在程序运行过程中内存中只有一个拷贝
define和inline的区别
define定义预编译时处理的宏,只是简单的字符串替换,无类型检查,不安全
inline是先将内联函数编译完成生成了函数体直接插入被调用的地方,减少了压栈,跳转和返回的操作。没有普通函数调用时的额外开销,内联函数是一种特殊的函数,会进行类型检查,对编译器的一种请求,编译器有可能拒绝这种请求。
C++中inline编译限制:
- 不能存在任何形式的循环语句
- 不能存在过多的条件判断语句
- 函数体不能过于庞大
- 内联函数声明必须在调用语句之前
const和define的区别
const用于定义常量,而define用于定义宏,而宏也可以定义常量。都用于常量定义时,以下是区别:
- const生效于编译的阶段,而define生效于预编译的阶段
- const定义的常量,在C语言中是存储在内存中、需要额外的内存空间的,而define定义的常量,运行时是直接替换称操作数的,不需要额外的内存空间
- const定义的常量是带类型的,define定义的常量是不带类型的,所以define定义的常量不利于类型检查
new和malloc的区别
- new内存分配失败时,会抛出bac_alloc异常,不会返回NULL;malloc内存分配失败时,会返回NULL
- 使用new操作符申请内存分配时无需指定内存块的大小,而malloc需要通过sizeof计算出需要分配的内存块大小
- operator new/operator delete可以被重载,而malloc/free不能被重载
- new/delete会调用对象的构造函数/析构函数以完成对对象的构造/析构,而malloc不会
- malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符
- new操作符从自由存储区上为对象动态分配内存,malloc函数从堆上为对象动态分配内存
delete和free的区别
- delete会调用对象的析构函数,确保资源被正确释放。free不了解对象的析构和构造,只是简单地释放内存块
- delete释放的内存块的指针值会被设置为nullptr,以避免野指针。free不会修改指针的值,可能导致野指针问题。
- delete可以正确释放通过new分配的数组。free不了解数组大小,不适用于释放通过malloc分配的数组
constexpr和const
const表示“只读”的语义,constexpr表示“常量”的语义
cosntexpr只能定义编译期常量,而const可以定义编译器常量,也可以定义运行期常量。
将一个成员函数标记为constexpr,则顺带也将它标记为了const,将一个变量标记为constexpr,则同样是const。但相反并不成立,一个const的变量或函数,并不是constexpr的
复杂系统中很难分辨一个初始值是不是常量表达式,可以将其声明为constexpr类型,由编译器来验证变量的值是否是一个常量表达式。constexpr必须使用常量初始化,如果constexpr声明中定义了一个指针,constexpr仅对指针有效,和所指对象无关
constexpr函数是指能用于常量表达式的函数,函数的返回类型和所有形参类型都是字面值类型,函数体有且只有一条return语句,为了在编译过程展开,constexpr函数被隐式转换成了内联函数,constexpr和内联函数可以在程序中多次定义,一般定义在头文件中
constexpr构造函数:构造函数不能说const,但字面值常量类的构造函数可以说constexpr,constexpr构造函数必须有一个空的函数体,即所有成员变量的初始化都放在初始化列表中。对象调用的成员函数必须使用constexpr修饰
如果想要修改const修饰的变量的值,需要加上关键字volatile,如果想要修改const成员函数中某些与类状态无关的数据成员,可以使用mutable关键字来修饰这个数据成员。
constexpr的好处:1. 为一些不能修改的数据提供保障,写成变量则就有被意外修改的风险;2. 为一些常量表达式提供了编译期计算的能力,在编译期对constexpr的代码进行优化,提高了程序的运行效率;3. 相比宏来说,没有额外的内存开销,而且更加安全可靠
volatile
定义:与const绝对对立的,是类型修饰符,影响编译器编译的结果,用该关键字声明的变量表示该变量随时可能发生变化,与该变量有关的运算,不要进行编译优化,会从内存重新装载内容,而不是直接从寄存器拷贝内容
作用:指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值,保证对特殊地址的稳定访问,防止被编译器优化掉
使用场合:在中断服务程序和cpu相关寄存器的定义
extern
定义:声明外部变量(在函数或者文件外部定义的全局变量)
static
作用:实现多个对象之间的数据共享+隐藏,并且使用静态成员还不会破坏隐藏原则,默认初始化为0
函数指针
函数指针是指向函数的指针变量。它可以用于存储函数的地址,允许在运行时动态选择调用的函数
函数指针的使用场景:
- 回调函数:函数指针常用于实现回调机制,允许将函数地址传递给其他函数,以便在适当的时候调用
- 函数指针数组:可以使用函数指针数组实现类似于状态机的逻辑,根据不同的输入调用不同的函数
- 动态加载库:函数指针可用于在运行时动态加载库中的函数,实现动态链接库的调用
- 多态实现:在C++中,虚函数和函数指针结合使用,可以实现类似于多态的效果
- 函数指针作为参数:可以将函数指针作为参数传递给其他函数,实现一种可插拔的函数行为
- 实现函数映射表:在一些需要根据某些条件调用不同函数的情况下,可以使用函数指针来实现函数映射表
函数指针和指针函数的区别
函数指针是指向函数的指针变量,可以存储特定函数的地址,并在运行时动态选择要调用的函数。通常用于回调函数、动态加载库时的函数调用等场景
指针函数是一个返回指针类型的函数,用于返回指向某种类型的数据的指针
struct和class的区别
- struct用于表示一组相关的数据,而class用于表示一个封装了数据和操作的对象,在实际使用中,可以根据具体的需求选择使用struct或class。如果只能用来组织一些数据,而不涉及复杂的封装和继承关系,struct可能更直观,如果需要进行封装、继承等面向对象编程的特性,可以选择使用class
- struct结构体中的成员默认是共有的(public)。类中的成员默认是私有的(private)
- struct继承时默认使用公有继承,class继承时默认使用私有继承
- 如果结构体没有定义任何构造函数,编译器会生成默认的无参数构造函数。如果类中没有定义任何构造函数,编译器也会生成默认的无参数构造函数
静态局部变量\全局变量\局部变量的区别和使用场景
- 静态局部变量
作用域:限定在定义它的函数内
生命周期:与程序的生命周期相同,但只能在定义它的函数内部访问
关键字:使用static关键字修饰
初始化:仅在第一次调用函数时初始化,之后保持其值 - 全局变量
作用域:整个程序
生命周期:与程序的生命周期相同
关键字:定义在全局作用域,不适用特定关键字 - 局部变量
作用域:限定在定义它的块(大括号内)
生命周期:在定义它的块结束时销毁
关键字:定义在函数、语句块或类的成员函数中
总结:静态局部变量用于在函数调用之间保留变量的值,全局变量适用于多个函数需要共享的数据,局部变量适用于仅在特定作用域内有效的情况
C++强制类型转换
- static_cast:没有运行时类型检查来保证转换的安全性,进行上行转换(把派生类的指针或引用转换成基类表示)是安全的,进行下行转换(把基类指针或引用转换成派生类表示)时,由于没有动态类型检查,所以是不安全的。使用:1.用于基本数据类型之间的转换,如把int转换成char。2. 把任何类型的表达式转换成void类型
- dynamic_cast:在进行下行转换时,dynamic_cast具有类型检查(信息在虚函数中)的功能,比static_cast更安全。转换后必须是类的指针、引用或者void*,基类要有基函数、可以交叉转换。dynamic本身只能用于存在虚函数的父子关系的强制类型转换,对于指针,转换失败则返回nullptr,对于引用,转换失败会抛出异常。
- reinterpret_cast:可以将整形转换为指针,也可以把指针转换为数组,可以在指针和引用里进行转换,但平台移植性比较差
- const_cast:常量指针转换为非常量指针,并且任然指向原来的对象。常量引用被转换为非常量引用,并且仍然指向原来的对象。去掉类型的const或volatile属性
C++内存管理
堆和栈的区别
栈和堆都是用于存储程序数据的内存区域。栈是一种有限的内存区域,用于存储局部变量、函数调用信息等。堆是一种动态分配的内存区域,用于存储程序运行时动态分配的数据。
栈上的变量生命周期与其所在函数的执行周期相同,而堆上的变量生命周期由程序员显示控制,可以分配(使用new或malloc函数)和释放(使用delete或free)
栈上的内存分配和释放是自动的,速度较快。而堆上的内存分配和释放需要手动操作,速度较慢
C++内存分区
C++程序运行时,内存被分为几个不同的区域,每个区域负责不同的任务
- 栈用于存储函数的局部变量、函数参数和函数调用信息的区域。函数的调用和返回通过栈来管理
- 堆用于存储动态分配的内存的区域,由程序员手动分配和释放。使用new和delete或malloc和free函数来进行堆内存的分配和释放
- 全局/静态区:全局区存储全局变量和静态变量。生命周期是整个程序运行期间。在程序启动时分配,程序结束时释放
- 常量区也被称为只读取,存储常量数据,如字符串常量
- 代码区存储程序的代码
内存泄漏
内存泄漏是指由于疏忽或错误造成了程序未能释放掉不再使用内存的情况。内存泄露并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因而造成了内存的浪费。可以使用Valgrind,mtrace进行内存泄漏检查
内存泄漏的分类
- 堆内存泄漏(Heap leak):堆内存指的是程序运行中根据需要分配通过malloc,realloc,new等从堆中分配的一块内存,再是完成后必须通过调用对应的free或者delete删除。如果程序的设计的错误导致这部分内存没有被释放,那么此后这块内存将不会被使用,就会产生Heap Leak
- 系统资源泄漏(Resource Leak):主要指程序使用系统分配的资源,比如Bitmap,handle,socket等没有使用相应的函数释放掉,导致系统资源的浪费,严重可导致系统效能降低,系统运行不稳定
- 没有将基类的析构函数定义为虚函数:当基类指针指向子类对象时,如果基类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有正确的释放,因此造成了内存泄漏
指针所指的方向改变,会导致未释放动态分配的内存
防止内存泄漏:将内存的分配封装在类中,构造函数分配内存,析构函数释放内存,或使用智能指针
智能指针
智能指针是为了解决动态分配内存导致内存泄漏和多次释放同一内存所提出的,C11标准中放在
- std::unique_ptr:独占指针,提供对动态分配的单一对象所有权的独占管理。通过独占所有权,确保只有一个unique_ptr可以拥有指定的内存资源。移动语义和右值引用允许unique_ptr在所有权转移时高效地进行转移
- std::shared_ptr:共享指针,允许多个智能指针共享同一块内存资源。内部使用引用计数来跟踪对象被共享的次数,当计数为零时,资源被释放。提供更灵活的内存共享,但可能存在循环引用的问题
- std::weak_ptr:弱指针,是为了解决shared_ptr可能导致的循环引用问题,weak_ptr可以从shared_ptr创建,但不会增加引用计数,不会影响资源的释放。weak_ptr可以通过lock()函数获得一个shared_ptr,如果资源已经被释放,lock()函数返回一个空的shared_ptr
野指针
野指针是指指向已被释放的或者无效的内存地址的指针。使用野指针可能导致程序崩溃、数据损坏或者其他不可预测的行为。野指针的产生原因:1. 释放后没有置空指针 2. 返回局部变量的指针 3. 释放内存后没有调整指针 4. 函数参数指针被释放。避免野指针:1. 在释放内存后将指针置为nullptr 2. 避免返回局部变量的指针 3. 使用智能指针 4. 注意函数参数的生命周期,避免在函数内释放调用方传递的指针,或者通过引用传递指针
野指针和悬浮指针的区别
悬浮指针是指向已经被销毁的对象的引用。当函数返回一个局部变量的引用,而调用者使用该引用时,就可能产生悬浮引用。访问悬浮引用会导致未定义行为,因为引用指向的对象已经被销毁,数据不再有效。避免悬浮指针:1. 避免在函数中返回局部变量的引用 2. 如果需要在函数之外使用函数内部创建的对象,使用返回指针或智能指针而不是引用
野指针和悬浮指针的区别:
- 野指针涉及指针类型,悬浮指针涉及引用类型
- 野指针可能导致访问已释放或无效内存,引发崩溃或数据损坏。悬浮指针可能导致访问已销毁的对象,引发未定义行为
- 野指针通常由于不正确管理指针生命周期引起,悬浮指针通常由于在函数中返回局部变量的引用引起
内存对齐
内存对齐是指数据在内存中的存储起始地址是某个值的倍数。在C语言中,结构体是一种复合数据类型,其构成元素既可以是基本数据类型(如int、long、float等)的变量,也可以是一些复合数据类型(如数组、结构体、联合体等)的数据单元。在结构体中,编译器为结构体的每个成员按其自然边界(alignment)分配空间。各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构体的地址相同。
为了使CPU能够对变量进行快速的访问,变量的起始地址应该具有某些特性,即所谓的“对齐”,比如4字节的int型,其起始地址应该位于4字节的边界上,即起始地址能够被4整除,如果一个变量的内存地址正好位于它长度的整数倍,就被称为自然对齐。
内存对齐的根本原因在于提高CPU访问数据的效率。一些系统对内存对齐要求非常严格,比如sparc系统,如果取未对齐的数据会发生错误,而在x86上就不会出现错误,但是效率下降。
许多计算机体系结构使用缓存行(cache line)来从内存中加载数据到缓存中。如果数据是对齐的,那么一个缓存行可以装载更多的数据,提高缓存的命中率
有些计算机架构要求原子性操作(比如原子性读写)必须在特定的内存地址上执行。如果数据不对齐,可能导致无法执行原子性操作,进而引发竞态条件
进程地址空间
- 命令行参数和环境变量:命令行参数是指从命令行执行程序的时候,给程序的参数
- 栈区:存储局部变量、函数参数值。栈从高地址向低地址增长,是一块连续的空间
- 文件映射区:位于堆区和栈区之间,用于存储动态链接库、共享内存等
- 堆区:动态申请内存用,堆从低地址向高地址增长
- BSS段:存放程序中未初始化的全局变量和静态变量的一块内存区域
- 数据段:存放程序中已初始化的全局变量和静态变量的一块内存区域
- 代码段:存放程序执行代码的一块内存区域。只读,代码段的头部还会包含一些只读的常数变量
C/C++内存分配
从静态存储区分配,内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。如全局变量,static变量。
在栈上创建,在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
从堆上分配(动态内存分配),程序在运行的时候用malloc或new申请任意多少的内存,程序员负责在何时用free或delete释放内存。动态内存的生存期由自己决定,使用非常灵活,但是分配释放要由程序员自己控制,容易产生内存泄漏。
C++中的库提供的六种内存模型
- memory_order_relaxed:松散的内存模型,不对内存访问做任何排序,也不保证任何内存访问的顺序
- memory_order_consume:消费内存模型,保证所有对共享数据的访问都是在读取一个数据之后进行的,但是不保证其他的内存访问的顺序
- memory_order_acquire:获取内存模型,保证所有对共享数据的访问都是在读取一个数据之后进行的,同时保证其他的内存访问的顺序
- memory_order_release:释放内存模型,保证所有对共享数据的访问都是在写入一个数据之后进行的,同时保证其他的内存访问的顺序
- memory_order_acq_rel:获取释放内存模型,同时包含了获取和释放的内存模型
- memory_order_seq_cst:顺序一致内存模型,保证所有对共享数据的访问都是在读取一个数据之后进行的,同时保证其他的内存访问的顺序,是最严格的内存模型
构造函数、析构函数是否要设为虚函数
析构函数需要设为虚函数,当派生类对象中由内存需要回收时,如果析构函数不是虚函数,不会触发动态绑定,只会调用基类析构函数,导致派生类资源无法释放,造成内存泄漏。
构造函数不需要,没有意义。虚函数调用是在部分信息下完成工作的机制,允许我们只知道接口而不知道对象的确切类型。要创建一个对象,需要知道对象的完整信息。特别是,需要知道想要创建的确切类型。因此,构造函数不能是虚函数
字符串操作函数
- strcpy:将一个字符串复制到另一个字符串
- strlen:返回字符串的长度
- strcat:将一个字符串连接到另一个字符串
- strcmp:比较两个字符串,若相等返回0,s1>s2返回正数,s1<s2返回负数
new和delete
new:分配内存,然后调用对应的构造函数(递归调用各个成员变量的构造函数)
delete:调用对应的析构函数,然后释放内存(递归调用各个成员变量的析构函数)
动态内存管理的两种风格(不代表只有这两种风格)
- RAII(Resource Acquisition Is Initialization):资源获取就是初始化(C++语言中可通过恰当实现构造/析构函数、恰当调用new/delete实现)
- 垃圾回收(C++语言中可通过智能指针实现)
深/浅拷贝与移动
- 浅拷贝:拷贝指针,指向同一块内存,进复制对象的值,而不涉及对象内部动态分配的资源。在浅拷贝中,新对象和原对象共享相同的资源,而不是复制一份新的资源。复制对象及其所有成员变量的值,对象内部动态分配的资源不会被复制,新对象和原对象共享同一份资源。浅拷贝通常使用默认的拷贝构造函数和赋值操作符,因此会逐成员地复制原对象的值。
- 深拷贝:拷贝内容,指向不同的内存,是对对象的完全独立复制,包括对象内部动态分配的资源。在深拷贝中,不仅复制对象的值,还会复制对象所指向的堆上的数据。复制对象及其所有成员变量的值,动态分配的资源也会被复制,新对象拥有自己的一份资源副本。深拷贝通常涉及到手动分配内存,并在拷贝构造函数或赋值操作符中进行资源的复制。
- 移动:将右值的内容转移到左值上,右值的内容不再可用
函数指针:指向函数的指针
函数对象:重载了函数调用运算符()的对象
lambda表达式:匿名函数对象
面向对象的三大特性
访问权限
C++通过public、protected、private三个关键字来控制成员变量和成员函数的访问权限,分别表示公有、保护、私有,被称为成员访问限定符。
public修饰符用于指定类中的成员可以被类的外部代码访问。公有成员可以被类外部的任何代码(包括类的实例)访问
private修饰符用于指定类中的成员只能被类的内部代码访问。私有成员对外部外码是不可见的,只有类内部的成员函数可以访问私有成员
protected修饰符用于指定类中的成员可以被类的派生类访问。受保护成员对外部代码是不可见的,但可以在派生类中被访问。
在类的内部(定义类的代码内部),无论成员被声明为public、protected还是private,都是可以互相访问的,没有访问权限的限制。
在类的外部(定义类的代码之外),只能通过对象访问成员,并且通过对象只能访问public属性的成员,不能访问private、protected属性的成员。
无论公有继承、私有和保护继承,私有成员不能被“派生类”访问,基类中的共有和保护成员能被“派生类”访问
对于公有继承,只有基类中的共有成员能被“派生类对象”访问,基类中的保护成员和私有成员不能被“派生类对象”访问。对于私有继承和保护继承,基类中的所有成员都不能被“派生类对象”访问
继承
定义:让某种类型对象获得另一个类型对象的属性和方法
功能:可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展
常见的继承有三种方式
- 实现继承:指使用基类的属性和方法而无需额外编码的能力
- 接口继承:指仅使用属性和方法的名称、但是子类必须提供实现的能力
- 可视继承:指子类(派生类)拥有基类(父类)中所有的属性和方法,同时还可以增加新的属性和方法
封装
定义:数据和代码捆绑在一起,避免外界干扰和不确定性访问
功能:把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏,例如:将公共的数据或方法使用public修饰,而不希望被访问的数据或方法使用private修饰
封装的优点:1. 减少耦合 2. 减少了代码的重复性 3. 提高了安全性 4. 提高了数据的安全性
多态
定义:同一事物表现出不同事物的能力,即向不同对象发送同一消息,不同的对象在接收时会产生不同的行为(重载实现编译时多态,虚函数实现运行时多态)
功能:多态性时允许将父对象设置成为和一个或更多的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。也就是说,允许将子类类型的指针赋值给父类类型的指针
实现多态有两种方式:1. 覆盖(override):指子类重新定义父类的虚函数的做法 2. 重载(overload):指允许存在多个同名函数,但是这些函数的参数表不同(参数个数不同或参数类型不同,或者两者都不同)
多重继承
一个类可以从多个基类继承属性和行为。在C++等支持多重继承的语言中,一个派生类可以同时拥有多个基类。
多重继承可能引入一些问题,如菱形继承问题,比如一个类C同时继承了两个类A和B,而A和B又同时继承了一个类X,这样C就同时继承了两份X的数据,这就是菱形继承问题。为了解决这个问题,C++提供了虚继承,通过在继承声明中使用virtual关键字,可以避免在派生类中生成多个基类的实例,从而解决菱形继承带来的二义性。
重载和重写
重载是指在同一作用域内,使用相同的函数名但具有不同的参数列表或类型,使得同一个函数名可以有多个版本。重写是指派生类重新定义基类的虚函数的做法,以提供特定于派生类的实现。重写是面向对象编程中多态性的一种体现,主要涉及基类和派生类之间的关系,用于实现运行时多态。
虚函数和虚函数表
- 虚函数
C++中的虚函数的作用主要是为了实现多态的机制。虚函数允许在派生类中重新定义基类中定义的函数,使得通过基类指针或引用调用的函数在运行时根据实际对象类型来确定。这样的机制称为动态绑定或运行时多态。在基类中,通过在函数声明前面加上virtual关键字,可以将其声明为虚函数。派生类可以重新定义虚函数,如果派生类不重新定义,则会使用基类中的实现 - 虚函数表
虚函数的实现通常依赖于一个被称为虚函数表(虚表)的数据结构。每个类(包括抽象类)都有一个虚表,其中包含了该类的虚函数的地址。每个对象都包含一个指向其类的虚表的指针,这个指针被称为虚指针(vptr)。当调用一个虚函数时,编译器会使用对象的虚指针查找虚表,并通过虚表中的函数地址来执行相应的虚函数。
虚函数和纯虚函数
虚函数
- 有实现:虚函数有函数声明和实现,即在基类中可以提供默认实现
- 可选实现:派生类可以选择是否覆盖虚函数。如果派生类没有提供实现,将使用基类的默认实现
- 允许实例化:虚函数的类可以被实例化,即你可以创建一个虚函数的类的对象
- 调用靠对象类型决定:在运行时,根据对象的实际类型来决定调用哪个版本的虚函数
- 用virtual关键字声明:虚函数使用virtual关键字声明,但不包含
纯虚函数
- 没有实现:纯虚函数没有函数体,只有函数声明,即没有提供默认的实现
- 强制覆盖:派生类必须提供纯虚函数的具体实现。否则它们也会成为抽象类
- 禁止实例化:包含纯虚函数的类是抽象类,不能被实例化,只能用于派生其他类
- 用声明:纯虚函数使用声明,即在函数声明的末尾加上
- 为接口提供规范:通过纯虚函数,抽象类提供一种接口规范,要求派生类提供相关实现
抽象类和纯虚函数
抽象类时不能被实例化的类,存在的目的是为了提供一个接口,供派生类继承和实现。抽象类中可以包含普通的成员函数、数据成员和构造函数,但它必须包含至少一个纯虚函数。即在声明中使用virtual关键字并赋予函数一个的纯虚函数。
纯虚函数是在抽象类中声明的虚函数,它没有具体的实现,只有函数声明。通过在函数声明的末尾使用,可以将虚函数声明为纯虚函数。派生类必须实现抽象类中的纯虚函数,否则它们也会成为抽象类
虚析构函数
虚析构函数是一个带有virtual关键字的析构函数。主要作用是确保在通过基类指针删除派生类对象时,能够正确调用派生类的析构函数,从而释放对象所占用的资源。通常,如果一个类可能被继承,且在其派生类中有可能使用delete运算符来删除通过基类指针指向的对象,那么该基类的析构函数应用声明为虚析构函数。
为什么要虚析构函数?虚析构函数允许在运行时根据对象的实际类型调用正确的析构函数,从而实现多态性。如果基类的析构函数不是虚的,当通过基类指针删除指向派生类对象的对象时,只会调用基类的析构函数,而不会调用派生类的析构函数,可能导致派生类的资源未被正确释放,造成内存泄漏
构造函数在对象的创建阶段被调用,对象的类型在构造函数中已经确定。因此,构造函数调用不涉及多态性,也就是说,在对象的构造期间无法实现动态绑定。虚构造函数没有意义,因为对象的类型在构造函数中就已经确定,不需要动态地选择构造函数。
不能声明为虚函数的函数
- 构造函数
- 普通函数,即非成员函数
- 静态成员函数
- 友元函数
- 内联成员函数
C++多态的实现
运行时多态:在运行时根据对象的实际类型来调用相应的成员函数
C++多态性是通过虚函数(virtual function)和虚函数表(vtable)来实现的。多态性允许在基类类型的指针或引用上调用派生类对象的函数,以便在运行是选择正确的函数实现
- 基类声明虚函数:在基类中声明虚函数,使用virtual关键字,以便派生类可以重写(override)这些函数
- 派生类重写虚函数:在派生类中重写基类中声明的虚函数,使用override关键字
- 使用基类类型的指针或引用指向派生类对象
- 调用虚函数:通过基类指针或引用调用虚函数。在运行时,系统会根据对象的实际类型来选择调用正确的函数实现
- 虚函数表:编译器在对象的内存布局中维护了一个虚函数表,其中存储了指向实际函数的指针,这个表在运行时用于动态查找调用的函数
成员函数/成员变量/静态成员函数/静态成员变量
- 成员函数:成员函数是属于类的函数,它们可以访问类的成员变量和其他成员函数。成员函数可以分为普通成员函数和静态成员函数。普通成员函数使用对象调用,可以访问对象的成员变量。普通成员函数的声明和定义通常在类的内部,但定义时需要使用类名作为限定符
- 成员变量:成员变量是属于类的变量,存储在类的每个对象中。每个对象拥有一份成员变量的副本,它们在对象创建时分配,并在对象销毁时释放。成员变量的访问权限可以是public、private或protected
- 静态成员函数:静态成员函数属于类而不是对象,因此可以直接通过类名调用,而不需要创建类的实例。静态成员函数不能直接访问普通成员变量,因为它们没有隐含的this指针。静态成员函数的声明和定义也通常在类的内部,但在定义时需要使用类名作为限定符
- 静态成员变量:静态成员变量是属于类而不是对象的变量,它们在所有对象之间共享。静态成员变量通常在类的声明中进行声明,但在类的定义外进行定义和初始化。静态成员变量可以通过类名或对象访问
构造函数和析构函数
- 构造函数:构造函数是在创建对象时自动调用的特殊成员函数。主要目的是初始化对象的成员变量,为对象分配资源,执行必要的初始化操作。构造函数的特点包括:1. 函数名与类名相同:构造函数的函数名必须与类名相同,且没有返回类型,包括void。2. 可以有多个构造函数:一个类可以有多个构造函数,可以根据参数的类型和数量不同而重载 3. 默认构造函数:如果没有为类定义任何构造函数,编译器会自动生成一个默认构造函数,默认构造函数没有参数。
- 析构函数:析构函数是在对象生命周期结束时自动调用的特殊成员函数。主要目的是释放对象占用的资源、执行必要的清理操作。析构函数的特点包括:1. 函数名与类名相同,前面加上波浪号~:析构函数的函数名为~ClassName,其中ClassName是类名。2. 没有参数和返回类型:析构函数没有参数,也没有返回类型,包括void,不能重载,每个类只有一个析构函数。3. 默认析构函数:如果没有为类定义任何析构函数,编译器会自动生成一个默认析构函数,执行简单的清理操作。
构造函数的类型
- 默认构造函数:没有参数的构造函数,如果没有为类定义任何构造函数,编译器会自动生成一个默认构造函数
- 带参数的构造函数:带有参数的构造函数,可以根据参数的类型和数量不同而重载
- 拷贝构造函数:用于创建一个新对象,该对象是通过拷贝另一个对象创建的。拷贝构造函数的参数通常是一个常量引用,以避免无限递归调用
- 委托构造函数:C++11标准引入了委托构造函数,允许一个构造函数调用另一个构造函数,减少代码重复。通过成员初始化列表或构造函数体内部调用其他构造函数
运算符重载
重载运算符函数,本质还是函数调用,所以重载后:
- 可以是和调用运算符的方式调用,data1+data2
- 可以是和函数调用的方式调用,operator+(data1,data2),这就要注意运算符函数的名字是“operator运算符”
在可以重载的运算符里有逗号、取地址、逻辑与、逻辑或
重载运算符,它本身是几元就有几个参数,对于二元的,第一个参数对应左侧运算对象,第二个参数对应右侧运算对象。而类成员函数的第一个参数隐式绑定了this指针,所以重载运算符如果是类的成员函数,左侧运算对象就相当于固定了是this
函数调用运算符
lambda是函数对象。编译器是将lambda表达式翻译为一个未命名类的未命名对象,捕获列表{函数体}对应类中重载调用运算符的参数列表、函数体,捕获列表的内容就对应类中的数据成员。所以捕获列表,值传递时,要拷贝并初始化那些数据成员,引用传递就是直接用。
通过模板实现编译时多态
模板:对于两个只有参数类型不同的函数,不用重复编写,编译器自动生成程序中用到的不同类型的函数。模板较多的代码往往编译起来非常慢,因此模板一般放在头文件中,以便于编译器在编译时能够直接展开模板,而不是在链接时再去实例化模板。
STL
广义上讲,STL分为3类:Algorithm算法、Container容器、Iterator迭代器,容器和算法通过迭代器可以进行无缝地连接。
详细的说,STL由6部分组成:容器Container、算法Algorithm、迭代器Iterator、适配器Adapter、仿函数Functor、空间配置器Allocator
STL六大组件的交互关系:
- 容器通过空间配置器取得数据存储空间
- 算法通过迭代器存储容器中的内容
- 仿函数可以协助算法完成不同的策略的变化
- 适配器可以修饰仿函数
容器:各种数据结构
算法:各种常用算法
迭代器:扮演容器与算法之间的胶合剂
仿函数:行为类似函数的对象
适配器:一种用来修饰容器或仿函数或迭代器接口的东西
空间配置器:负责空间的配置与管理
STL具有高可重用性、高性能、高移植性、跨平台的优点
算法应用
STL主要包含容器和算法两部分
容器(container):用来保存一系列对象的对象,例如vector、list、map等
迭代器
迭代器(iterator):对指针的抽象,用来遍历容器中的对象,例如vector
- 所有迭代器都支持解引用(*)、自增(++)、自减(–)等操作
- input iterator:只读迭代器,只能用于读取容器中的对象,单次读取,例如vector
::const_iterator - output iterator:只写迭代器,只能用于写入容器中的对象,单次写入,例如vector
::iterator - forward iterator:只读迭代器,只能用于读取容器中的对象,但是可以多次读取,例如list
::const_iterator - bidirectional iterator:双向迭代器,可以读取容器中的对象,也可以写入容器中的对象,可以多次读取,在forward iterator基础上支持自减运算符(–),例如list
::iterator
库函数
1 |
|
容器的begin()和end()方法可以获得首尾迭代器
1 | for(auto it=c.begin(); it!=c.end(); ++it) |
cbegin()和cend()方法可以获得首尾const迭代器
1 | for(auto it=c.cbegin(); it!=c.cend(); ++it) |
rbegin()和rend()方法可以获得反向迭代器
rcbegin()和rcend()方法可以获得反向const迭代器
接口
标准容器共享接口,从而发挥模板多态的特性
常见的构造函数
ContainerType c(num)
ContainerType c(num, val)
ContainerType c(begin, end)
容器相关的方法
1 | int size = c.size(); |
常见容器
栈(stack)
先进后出,只能在栈顶进行插入和删除操作
#include
C++: std::stack
push(x) 向栈顶插入元素x
pop() 删除栈顶元素
top() 返回栈顶元素
empty() 栈是否为空
size() 栈中元素个数
n个元素入栈,出栈顺序有多少种?
出栈顺序为卡特兰数:Cn = (n为非负整数)
队列(queue)
先进先出,只能在队尾进行插入操作,只能在队首进行删除操作
#include
C++: std::queue
push(x) 向队尾插入元素x
pop() 删除队首元素
front() 返回队首元素
back() 返回队尾元素
empty() 队列是否为空
size() 队列中元素个数
queue还提供了运算符,较为常用的是使用=为两个queue赋值,使用==判断两个queue是否相等
循环队列(circular queue)
循环队列是一种环形的队列,可以用数组实现。循环队列的队首和队尾是相邻的,当队尾到达数组的末尾时,如果队首不在数组的开头,可以将队尾指向数组的开头,从而实现循环队列
双端队列(deque)
双端队列是一种可以在队首和队尾进行插入和删除操作的队列
C++: std::deque
#include
push_back(x) 向队尾插入元素x
push_front(x) 向队首插入元素x
pop_back() 删除队尾元素
pop_front() 删除队首元素
front() 返回队首元素
back() 返回队尾元素
empty() 双端队列是否为空
size() 双端队列中元素个数
erase(pos) 删除pos位置的元素
insert(pos, x) 在pos位置插入元素x
优先队列(priority queue)
优先队列是一种可以在队尾插入元素,队首删除元素,但是删除的元素是队列中优先级最高的元素的队列
#include
C++: std::priority_queue
push(x) 向队尾插入元素x
pop() 删除队首元素
top() 返回队首元素
empty() 优先队列是否为空
size() 优先队列中元素个数
priority_queue<int, vector
自定义比较规则
struct cmp {
bool operator()(const Node& t1, const Node& t2) {
return t1.val > t2.val; // 小于号,小根堆;大于号,大根堆
}
};
priority_queue<Node, vector
优先队列也可以传入pair,pair的比较规则是先比较first,再比较second
pair
使用make_pair()函数创建pair对象,使用first和second成员访问pair对象的第一个和第二个元素
tuple
使用make_tuple()函数创建tuple对象,使用get()函数访问tuple对象的第i个元素
向量(vector)
向量是一种可以在任意位置插入和删除元素的容器,但是插入和删除元素的效率较低
#include
C++: std::vector
push_back(x) 向向量尾部插入元素x
pop_back() 删除向量尾部元素
front() 返回向量首部元素
back() 返回向量尾部元素
empty() 向量是否为空
size() 向量中元素个数
erase(pos) 删除pos位置的元素
insert(pos, x) 在pos位置插入元素x
emplace(pos, args) 在pos位置构造元素
auto智能输入输出
输入
for(auto &x:v)
cin >> x;
输出
for(auto x:v)
cout << x << " ";
列表(list)
列表是一种可以在任意位置插入和删除元素的容器,但是插入和删除元素的效率较高
#include
C++: std::list
push_back(x) 向列表尾部插入元素x
push_front(x) 向列表首部插入元素x
pop_back() 删除列表尾部元素
pop_front() 删除列表首部元素
front() 返回列表首部元素
back() 返回列表尾部元素
empty() 列表是否为空
size() 列表中元素个数
erase(pos) 删除pos位置的元素
insert(pos, x) 在pos位置插入元素x
集合(set)
集合是一种不包含重复元素的容器,集合中的元素是有序的
#include
C++: std::set
insert(x) 向集合中插入元素x
erase(x) 删除集合中元素x
find(x) 查找集合中元素x
empty() 集合是否为空
size() 集合中元素个数
count(x) 集合中元素x的个数
lower_bound(x) 返回集合中第一个大于等于x的元素的迭代器
upper_bound(x) 返回集合中第一个大于x的元素的迭代器
equal_range(x) 返回集合中第一个大于等于x的元素的迭代器和第一个大于x的元素的迭代器,即lower_bound和upper_bound的返回值,pair类型
set、multiset使用红黑树实现O(log n),unordered_set使用哈希表实现,无冲突时查找、插入、删除的时间复杂度为O(1)
映射(map)
映射是一种键值对的容器,键是唯一的,值可以重复
#include
for(auto p:m)
cout << p.first << " " << p.second << endl;
使用auto遍历无法直接修改原map中的元素,如果需要修改,需要使用auto &p:m
map、multimap使用红黑树实现O(log n),unordered_map使用哈希表实现,无冲突时查找、插入、删除的时间复杂度为O(1)
string
string是一种字符序列的容器
#include
C++: std::string
size() 字符串长度
empty() 字符串是否为空
clear() 清空字符串
substr(pos, len) 返回从pos位置开始长度为len的子串
find(s) 查找字符串s在字符串中的位置
replace(pos, len, s) 用字符串s替换从pos位置开始长度为len的子串
insert(pos, s) 在pos位置插入字符串s
erase(pos, len) 删除从pos位置开始长度为len的子串
getline(cin, s) 从cin中读取一行字符串到s中
bitset
bitset是一种位序列的容器
#include
C++: std::bitset
size() 位序列长度
count() 位序列中1的个数
bitset<10> b;
bitset<4> a(“0011”);
cout << (a^b) << endl;
方法
accumulate(begin, end, init):计算区间[begin, end)中的元素之和,init为初始值
stoi(s):将字符串s转换为整数 int
stol(s):将字符串s转换为长整数 long long
memset(arr, val, sizeof(arr)):将数组arr中的每个元素都赋值为val,只能用于赋值0或-1
to_string(n):将整数n转换为字符串
max_element(begin, end):返回区间[begin, end)中的最大元素的迭代器
min_element(begin, end):返回区间[begin, end)中的最小元素的迭代器
int mx = *max_element(arr, arr+n);
int mn = *min_element(arr, arr+n);
sort(begin, end):对区间[begin, end)中的元素进行排序
reverse(begin, end):对区间[begin, end)中的元素进行逆序
next_permutation(begin, end):返回区间[begin, end)中的下一个排列
prev_permutation(begin, end):返回区间[begin, end)中的上一个排列
lower_bound(begin, end, val):返回区间[begin, end)中第一个大于等于val的元素的迭代器
upper_bound(begin, end, val):返回区间[begin, end)中第一个大于val的元素的迭代器
_gcd(a, b):返回a和b的最大公约数
int gcd = _gcd(a, b);
_lcm(a, b):返回a和b的最小公倍数
int lcm = _lcm(a, b);
_lg(x): 求x的二进制位数