C++重点知识整理(导图在最下面)
入门小知识
命名空间
- 概念:命名空间是新定义一个作用域,里面可以放函数,变量,定义类等,主要用来防止命名冲突
- 实现
- namespace关键字+命名空间名字{命名空间成员}
- namespace N1{XXX}
- 嵌套:namespace N1{ XXX;namespace N2{XXX}}
- 同一工程中允许存在相同名称的命名空间,编译器最后会将所有相同名称命名空间合并成一个
- 成员使用
- N::成员名
- using N::成员名,该成员可以直接在当前文件中使用,但是其他成员需要加N::
- using namespace std: 该种方式使用最广泛,优点:命名空间中成员可以直接在当前文件中使用,缺陷:当前文件中存在同名成员时可能会冲突
缺省参数
- 概念:是声明或定义函数时为函数的参数指定一个默认值。在调用该函数时,如果没有指定实参则采用该
默认值,否则使用指定的实参 - 分类
- 全缺省参数:所有参数带有缺省值
- 半缺省参数:部分参数带有缺省值(注意:缺省值必须从右往左依次给出)
- 注意事项
- 半缺省参数只能从右往左一次给出,不能隔着给
- 不能再声明和定义时同时给出 原因:如果两个位置给的不同意编译器就不知道应该用哪一个
- 缺省参数必须是常用和着全局变量,即在编译时要能够明确知道缺省值的内容
- C语言不支持
函数重载(重点+考点)
- 概念:相同作用域,函数名相同,参数列表不同,参数列表不同具体体现在参数类型不同、个数不同以及类型次序不同,与返回值类型是否相同无关
- 调用原理:函数名相同,编译器如果只掉调用那个函数?编译期间,编译器通过对实参类型进行推演,根据推演的结果找对应的重载函数,如果存在且不会造成二义性则调用,否则产生编译错误
- 名字修饰规则:即编译器在编译时对函数名字的改写方式
- C语言:简单,只是在函数名字前增加下划线_
- C++: 复杂,编译器将函数参数的类型放置在函数名字中,来保证函数重载时名字在底层的不同
- 常考问题
- 什么是函数重载?
- 函数重载如何确定到底应该调用那个函数?编译期间还是运行期间?
- C 语言支持函数重载吗?为什么?
- 答案:C语言不支持,因为C语言编译器对函数名修饰规则:仅仅只是在函数名前加_,这样编译器在调用时就无法区分函数名相同参数列表不同的函数了
- C++底层是怎么支持函数重载的?
- 答案:C++编译器对函数名字修饰规则:编译器将参数类型信息增加到名字中了,这样即使函数名相同,只要参数类型不同,其在底层的名字就不同,编译器根据所传递参数在编译期间就可以确定到底应该调用那个函数
- 如果两个函数仅仅是因为返回值不同,为什么不能形成重载?
- 答案:不能,比如:两个Add函数,参数都是int类型,一个返回int,一个返回double,如果按照Add(1,2),应该调用那个重载函数呢,编译器就无法通过参数来确定了,因此报错
- extern “C”的作用?
- 答案:在C++函数前如果使用extern "C"修饰,表明告诉编译器,将该函数按照C的方式进行编译
引用(概念+考点+应用)
- 概念:引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它
引用的变量共用同一块内存空间 - 特性
- 引用在定义时必须初始化
- 一个变量可以有多个引用
- 引用一旦引用一个实体,再不能引用其他实体
- const引用:用const修饰的引用,该引用变量不能修改
- 应用场景
- 为实现简单,普通的取别名,比如:结构体套结构体,想要使用里层结构体中成员,A.B.成员,不方便可以直接用引用简化
- 做函数参数
- 想要通过形参修改外部实参
- 指针:虽然可以做到,但是比较麻烦而且可能会出现指针非法操作异常
- 引用类型的参数,如果不想通过形参改变实参时可以加const进行限制
- 自定义类型对象传参(最好用引用,可以提高传参的效率)
- 做函数返回值
- 注意不能返回栈上空间的引用,因为函数结束后,栈空间已经被回收,如果用户在函数外以引用方式接收函数返回值,那引用的将会是一段非法的空间
- 引用和指针的区别
- 概念上:引用就是一个别名,与其实体共用同一块内存空间
- 底层实现上:引用就是按照指针的方式实现的,即引用在底层就是一个指针,T&—>T* const const T&—>const T* const
- 区别主要体现在特性和使用形式上:
- 引用在定义时必须要初始化,而指针没有要求
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型
实体 - 没有空引用,但是有NULL指针
- 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占
4个字节) - 引用变量的++和–是直接给引用实体的值+1或者-1,而指针变量的++和–是让该指针向前或向后偏移一个类型的大小
- 有多级指针,但是没有多级引用
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
- 引用比指针使用起来相对更安全
内联函数(考点+引用)
- 概念:以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数压栈的开销,
内联函数提升程序运行的效率 - 特性
- inline是一种以空间换时间的做法,省去调用函数额开销。所以代码很长或者有循环/递归的函数不适宜
使用作为内联函数 - inline对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline的函数体内有循环/递归等
等,编译器优化时会忽略掉内联。 - inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会
找不到。
- 内联函数和宏函数区别
- 宏函数
- 优点
- 在预处理阶段会展开,少了函数调用参数压栈等的开销
- 缺陷
- 在预处理阶段展开,增加了预处理时间
- 编译之前已经展开了,如果编译时报错,错误不好定位
- 参数没有类型,而缺编译时已经展开,不能进行参数类型检测,安全性低
- 可读性比较差,比如:为了保证参数的正确性,需要多出加括号,但仍旧可能会引起副作用。
- 不能调试
- 因为要已经展开,如果调用的位置比较多,可能会引体代码膨胀
- 内联函数
- 优点
- 是一个函数,参数具有类型,可以进行类型检测,安全性高
- 在编译阶段展开,少了函数调用开销,能够提高代码运行效率
- 是函数,在调试时可以不让编译器展开,方便调试
- 没有副作用
- 缺陷:
- 可能会引起代码膨胀
- inline是建议性关键字,即建议编译器将inline修饰的函数按照内联函数处理,但是编译器是否真正会将其当成内联函数处理,则不一定,容易给用户造成困然
类和对象
面相对象和面向过程区别
类定义方式
- 类声明和定义全部放在类中
- 类声明在.h的头文件中, 类定义在.cpp的源文件中,注意:在源文件中定义类时,类名前必须要加类名::限定符
封装特性
- 访问限定符
- private:修饰的成员不能再类外被访问
- protected:主要应用在继承体系中,修饰的成员不能在类外被访问,但是可以被子类访问
- public:修饰的成员可以直接在类外被访问
- class定义类默认访问限定符是private,而struct定义类默认访问限定符是public,struct之所以是public,原因是为了兼容C语言
- 考点:C++中struct和class的区别?
- 封装的概念:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行
交互 - C++如何实现封装
- 先通过class的方式,将对象的属性和方法包装成一个整体,更符合人们对于事物的认知
- 通过访问限定符,选择性的将接口暴露给使用者,让对象之间可以完成交互
- 考点:
- 面相对象的三大特性:封装、继承、多态
- 什么是封装?C++是如何实现封装的?
类的作用域
- 类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员,需要使用 :: 作用域解析符
指明成员属于哪个类域 - 类中成员变量可以在任意"成员函数中使用",因此:成员变量可以看成是成员函数的全局变量
- C++中作用域:全局作用域、函数体中局部作用域、命名空间、类域
类的实例化
- 概念:用类类型创建对象的过程称之为类的实例化
- 类和对象的区别
- 考点:
- 如何计算一个类的大小?
- 空类的大小是多少?为什么
- 什么是内存对其?结构体是如何进行内存对其的?
- 如何让结构体按照指定的默认对其数进行对其?
- 如何知道结构体中某个成员相对于结构体起始位置的偏移量?
- 什么是大小端?如何测试某台机器是大端还是小端,有没有遇到过要考虑大小端的场景?
this指针
- 概念:为了让成员函数知道操作那个对象,C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参
数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有成员变量的操作,都是通过该
指针去访问 - 特性
- this指针的类型:类类型* const
- 只能在“成员函数”的内部使用
- this指针本质上其实是一个成员函数的形参,是对象调用成员函数时,将对象地址作为实参传递给this
形参。所以对象中不存储this指针 - this指针是成员函数第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户
传递
- 考点:
- 解释下什么是this指针
- this指针存放在哪里?
- this指针有没有可能是NULL?
- 常见的调用约定有那些?
类中六个默认的成员函数:如果类中什么也没有定义,编译器会生成六个默认的成员函数
- 构造函数
- 概念:构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员
都有 一个合适的初始值,并且在对象的生命周期内只调用一次 - 特性
- 函数名与类名相同
- 无返回值
- 对象实例化时编译器自动调用对应的构造函数
- 构造函数具有初始化列表
- 概念:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括
号中的初始值或表达式 - 注意:
- 初始化列表是真正的对成员变量进行初始化,而构造函数体中是赋值
- 初始化列表的位置只能初始化非静态成员变量
- 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
- 类中包含以下成员,必须放在初始化列表位置进行初始化
- const类型成员变量
- 引用类型成员变量
- 类类型对象(该类没有默认构造函数)
- 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
- 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
- 构造函数可以重载
- 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成
- 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认成员函数
- 构造函数不能用const修饰
- 构造函数不能是虚函数
- 应用场景
- 创建对象时,由编译器自动进行调用,并且在对象生命周期内只调用一次
- 关于编译器生成的默认构造函数
- 用户没有显式定义任何构造函数时,编译器才会生成
- 编译器生成的构造函数一定是无参的
- 编译器生成的构造函数是有具体的事情要做的
- 拷贝构造函数
- 概念:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用
- 特性
- 构造函数的一种重载形式,因此构造函数的特性拷贝构造函数都满足
- 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用
- 若未显示定义,系统生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝
- 应用场景
- 当使用已经存在的类类型对象构造新对象时,或者以对象做为参数或者返回值类型时,编译器会自动调用拷贝构造函数
- 关于编译器生成的默认拷贝构造函数
- 用户没有显式定义时,编译器会生成一个默认的拷贝构造函数
- 编译器生成的拷贝构造函数是浅拷贝,即:将对象中内容原封不动的拷贝到新对象中。问题:如果原对象中涉及到资源管理,那么新对象和原对象共用的就是同一份资源,在进行赋值或者析构时会造成内存泄漏或者程序崩溃
- 如果类中涉及到资源管理时,用户必须要显式提供拷贝构造函数,一般是按照深拷贝方式提供的,即:在拷贝对象中内容时,发现对象中管理资源了,那么个新对象也要重新申请一份资源,即没有对象都有自己唯一的资源
- 析构函数
- 概念::与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作
- 特性
- 析构函数名是在类名前加上字符 ~
- 无参数无返回值
- 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数,即析构函数不能重载
- 对象生命周期结束时,C++编译系统系统自动调用析构函数
- 用户没有显式定义时,编译器会自动生成一个
- 基类的析构函数最好设置成虚函数
- 应用场景
- 在对象销毁时,由编译器自动进行调用,并且只调用一次,完成对象中资源的清理工作
- 关于编译器生成的默认析构函数
- 如果用户没有显式提供,编译器会自动提供一份,做一些力所能及的事情
- 如果对象中涉及到资源管理,用户必须要显式给出析构函数,否则会造成内存泄漏
- 赋值运算符重载
- 运算符重载
- 为了提高代码的可能性,C++通过operator关键字来支持运算符重载,运算符重载是针对于自定义类型的
- 写法:返回值类型 operator重载运算符(参数列表)
- 运算符重载和函数重载的区别?
- 注意事项:
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型或者枚举类型的操作数
- 用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不 能改变其含义
- 作为类成员的重载函数时,其形参看起来比操作数数目少1,成员函数的操作符有一个默认的形参this,限定为第一个形参
- 常见运算符重载
- 赋值运算符重载
- ++和–重载
- 输入和输出运算符即(>>和<<)重载
- []运算符重载
- *和->重载
- ()重载
- 赋值运算符重载
- T& operator=(const T& elem)
- 注意:
- 参数类型:为什么要加&? 为什么要加const?
- 返回值类型:为什么要按照&方式返回?
- 是否检测自己给自己赋值?
- 为什么要返回*this,而不是返回参数?
- 调用场景:用已经存在对象给另一个对象赋值时会调用赋值运算符重载
- 默认赋值运算符重载
- 如果一个类没有显式定义赋值运算符重载,编译器会生成一个默认的赋值运算符重载
- 编译器生成的默认赋值运算符重载是按照浅拷贝方式生成的
- 如果类中涉及到资源管理时,用户必须要显式提供赋值运算符重载,否则可能会造成内存泄漏或者运行时崩溃,用户一般是按照深拷贝方式提供的
- T* operator&()和const T* operator&()const
const成员
- 普通变量
- 表示该变量是一个常量,并且再编译阶段会进行参数类型检测以及替换,比宏常量更安全,因此可以用其取代宏常量
- const修饰类成员
- 修饰成员变量
- 该成员变量不能在成员函数中被修改
- 必须在类初始化列表的位置完成初始化工作
- 修饰成员函数
- 实际修饰的是函数隐藏的this指针
- 在该类中不能修改对象中任何成员变量,除非该变量使用nutable修饰
- 该成员函数中:只能调用const成员函数,不能调用普通成员函数
- 考点:
- const关键字的作用
- const修饰的常量和宏常量的区别
- const成员和普通成员函数的区别?
静态成员
- 在类中,被static修饰的成员称为静态成员
- static修饰成员变量,称之为静态成员变量
- 静态成员变量不能在初始化列表位置初始化,必须在类外进行初始阿化,在类外初始阿化时必须要加类名::,类中只是声明
- 静态成员变量是类的属性,不属于某个具体的对象,是类所有对象共享的
- 不存在在具体的对象中,因此不会影响sizeof的结果
- 4,可以通过对象.静态成员名,也可以通过类名::静态成员变量名方式访问
- 在程序启动时,就完成了对静态成员变量的初始化工作
- static修饰的函数,称之为静态成员函数
- 静态成员函数没有this指针
- 静态成员函数中不能直接访问非静态成员变量,因为所有非静态成员变量都是通过this指针访问的
- 静态成员函数中不能调用普通成员函数
- 静态成员函数不能被this修饰
- 静态成员函数不能是虚函数
- 既可以通过对象,也可以通过类名::方式访问
- 考点:
- 解释下static关键字的作用
- static和extern关键字的区别
- 什么是静态成员函数?和普通成员函数的区别?
- 静态成员函数中能调用非静态成员函数吗?为什么?
- 静态成员函数能用const修饰吗?为什么?
- 静态成员函数能设置成虚函数吗?为什么?
友元
- 概念:友元提供了一种突破封装的方式,有时提供了便利。但是友元破坏了封装,所以友元不宜多用。
- 分类
- 友元函数
- 友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声
明,声明时需要加friend关键字 - 特性:
- 友元函数可访问类的私有和保护成员,但不是类的成员函数
- 友元函数不能用const修饰
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
- 一个函数可以是多个类的友元函数
- 友元函数的调用与普通函数的调用和原理相同
- 友元类
- 注意:
- 友元关系是单向的,不具有交换性
- 友元关系不能传递
- 友元关系不能继承
C++动态内存管理
考点
- C/C++程序将内存分为了那些区?为什么要进行分区?
- 不同类型的变量在那些区?每个区大概作用
- malloc/calloc/realloc的区别?
- malloc函数的实现原理
- new和delete的实现原理
- new
- 调用void* operator new(size_t size)申请空间
- 循环调用malloc申请空间
- 如果malloc申请空间成功则返回,如果malloc申请空间失败,先检测用户是否设置空间不足应对措施,如果提供继续调用malloc申请,如果没有提供,抛出bad_alloc异常
- 对申请好的空间调用构造函数进行初始化
- delete
- 调用对应类型的析构函数,完成对象中资源的清理工作
- 调用void operator delete(void* p)函数完成空间的释放工作,而该函数中真正的释放是使用free来进行释放的
- new T[N]
- 调用void* operator new[](size_t size)函数来申请空间
- 该函数中实际是调用void* operator new(size_t size)来完成空间申请的
- 调用N次构造函数,对N个对象的空间从前往后来进行初始化
- delete[] p
- 调用N次析构函数,完成空间中N个对象中资源的清理工作
- 调用void operator delete[](void* p)完成空间的释放工作,但实际是调用void operator delete(void* p)来释放资源的
- new/delete和malloc/free的区别
- 相同点
- 都是用来动态申请内存空间
- 申请的空间在用完之后必须要手动释放,否则会造成内存泄漏
- 不同点
- malloc和free是函数,new和delete是操作符
- malloc申请的空间不会初始化,new可以初始化
- malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可
- malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型
- malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需要捕获异常
- 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理
- 什么是内存泄漏?内存泄漏会有什么危害?
- 如何知道程序是否发生了内存泄漏?如何检测?
- 如何避免内存泄漏?
- 如何一次性申请4G内存空间?
- 如何设计一款内存池?
模板
泛型编程:编写与类型无关的通用代码,是代码复用的一种手段
分类
- 函数模板
- 函数模板概念:函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本
- 语法:
template<typename T1, typename T2,…,typename Tn>
返回值类型 函数名(参数列表){} - 函数模板实例化
- 概念:用不同类型的参数使用函数模板时,称为函数模板的实例化
- 分类
- 隐式实例化:让编译器根据实参推演模板参数的实际类型,来生成代码进行调用,注意:生成代码期间不会进行隐式类型转化
- 显式实例化:在函数名后的<>中指定模板参数的实际类型,来生成代码,在生成代码时如果参数类型不匹配会尝试进行隐式类型转化,转换成功则调用,否则报错
- 函数模板运行原理
- 实例化之前
- 编译器在编译阶段只是简单检测下函数模板是否存在语法问题,并不会生成代码
- 实例化之后
- 编译阶段,编译器检测到对模板函数的实例化之后,进行以下操作:
- 先检测工程中是否否早合适可调用的普通函数,如果处在则调用
- 检测是否存在合适的函数模板,存在时,编译器会推演实参的类型,推演成功后,然后根据函数模板生成对应类型的具体函数,然后调用
- 没有对应的普通函数和对应的模板时,编译器会报错
- 类模板:参考函数模板
模板参数分类
- 类型参数:出现在模板参数列表中,跟在class或者typename之类的参数类型名称
- 非类型参数:用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用
- 注意:
- 浮点数、类对象以及字符串是不允许作为非类型模板参数的
- 非类型的模板参数必须在编译期就能确认结果
模板特化
- 特化概念:使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果,因此模板需要对部分类型进行特殊化处理
- 分类
- 函数模板特化
- 类模板特化
- 全特化:全特化即是将模板参数列表中所有的参数都确定化
- 偏特化
- 部分参数特化
- 对参数类型进行条件更严格的约束
分离编译
- 什么是分离编译
- 模板支持分离编译吗?为什么
模板的优缺点
异常
考点:程序终止方式有那些?
传统错误处理机制
- 暴力的终止程序
- 返回错误码
- setjmp和longjmp组合
- 其他
异常
- 异常概念
- 实现方式
- throw:抛出异常
- catch:按照类型捕获异常
- try: 将可能抛出异常的代码放在try块中,注意:try之后必须跟catch对抛出的异常进行捕获
- 异常的抛出和捕获规则
- 异常不是直接将异常对象抛出去,抛出的是一个副本
- 异常是按照类型进行捕获的,一般情况下不会进行类型转化
- 距离异常抛出位置进行的匹配捕获先捕获到异常
- 工程实现时一般都是通过自定义异常类来进行异常的抛出和捕获的,具体错误抛出具体的类型异常对象,然后捕获位置使用基类的引用来进行捕获
- 异常的重新抛出
- 本函数对其中调用函数抛出异常不进行处理,但是要进行捕获做一些其他事情,然后将异常继续往出抛,让外部关心的函数去处理
- 栈展开
- 异常安全
- 由异常处理不当引起的程序安全性问题
- 自定义异常类
继承和多态
继承
- 概念:继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程
- 继承作用
- 实现代码复用
- 实现多态
- 继承方式
- public继承方式
- protected继承方式
- private继承方式
- 考点
- 不同继承方式间的区别
- class和struct的默认继承权限
- 同名隐藏
- 概念:如果子类和基类存在相同名称的成员,当使用派生类对象调用该相同名称的成员时,优先调用的是子类的,基类的无法直接被调用到,就相当于是子类将基类中同名称的成员屏蔽了
- 注意
- 基类和子类隶属于不同的作用域
- 如果想要通过子类对象调用相同名称的基类成员,必须在该成员前加基类名称以及作用域限定符
- 成员变量隐藏:只与成员变量名是否相同有关,与成员变量类型是否相同无法
- 成员函数隐藏:只与函数名是否相同有关,与函数原型是否相同无关
- 赋值兼容规则
- 前提必须是public类型继承方式,此时子类和基类时is-a的关系
- 此时有:
- 子类对象可以直接赋值给基类对象,但反过来则不行
- 基类对象的指针和引用可以直接指向子类对象,但返回来不行(但可以强转让代码通过编译,但是不安全)
- 派生类对象的构造和析构
- 派生类对象构造
- 注意构造那个类的对象,编译器就会调用该类的成员函数
- 函数调用:调用派生类构造函数()–>然后在派生类构造函数初始化列表的位置调用基类构造函数,完成基类部分成员的初始化工作—>执行派生类构造函数函数体
- 函数体执行顺序:先执行基类构造函数函数体,然后再执行派生类构造函数函数体
- 派生类对象析构过程
- 析构那个类的对象,编译器就会调用那个类的析构函数
- 析构函数调用过程:先调用派生类析构函数,以完成派生类中资源的清理工作,然后编译器在派生类析构函数最后一条语句之后插入了call 基类构造函数的汇编指令,即派生类析构函数中最后一条有效语句执行完成后,才会执行基类的析构函数,已完成基类中资源的清理工作
- 函数体执行过程:先执行派生类析构函数函数体—>再执行基类析构函数函数体
- 关于构造与析构的其他注意事项
- 不同继承方式下对象模型
- 单继承
- 多继承
- 菱形继承
- 虚拟继承
- 菱形虚拟继承
- 考点
- 继承的优缺点
- 什么是同名隐藏
- 基类中那些成员被继承到子类中了?
- 什么是菱形继承?菱形继承有什么缺陷?如何解决?
- 菱形虚拟继承是如何解决菱形继承存在二义性问题的?
- 继承和组合的区别?
- 如何实现一个不能被继承的类?
多态
- 面试常问问题:什么是多态?说说你理解的多态等
- 多态概念:通俗说就是同一事物,在不同场景下所表现出的不同状态
- 多态分类
- 静态多态(早绑定,静态联编)
- 在编译阶段,编译器根据传递实参类型确定具体调用那个函数
- 体现
- 函数重载
- 模板
- 动态多态(晚绑定、动态联编)
- 在运行时,根据基类指针或引用指向的不同类的对象,调用具体的虚函数
- 动态多态的实现条件
- 基类必须具有虚函数(被virtual关键字修饰的成员函数),并且派生类必须要对基类的虚函数进行重写
- 通过基类的指针或引用调用虚函数
- 表现方式:在代码运行时,根据不同基类指针或引用指向不同类的对象,选择调用具体类的虚函数
- 注意:以上两个条件缺一不可,否则就不能实现动态多态
- 重写
- 在继承体系中
- 基类函数一定是虚函数
- 派生类虚函数必须要与基类虚函数原型完全一致,即:返回值类型相同 函数名相同(参数列表相同)
- 基类和派生类虚函数的访问权限可以不同
- 两个例外
- 协变
- 基类虚函数返回基类的指针或引用,派生类虚函数返回派生类的指针或引用,基类和派生类析构函数的返回值类型不同
- 析构函数:基类和派生类虚函数的函数名不同
- 考点:
- 函数重载、同名隐藏和重写之间不同?
- override和final两个关键字的作用
- 抽象类
- 纯虚函数:在虚函数的后面写上 =0 ,则这个函数为纯虚函数
- 概念:。包含纯虚函数的类叫做抽象类(也叫接口类)
- 特性
- 抽象类不能实例化对象,但是可以定义抽象类的指针或引用
- 子类必须要重写抽象类中的纯虚函数,否则子类也是抽象类
- 多态实现原理
- 对象模型
- 如果类中包含有虚函数,对象中将会多4个字节,在对象前4个字节中保存虚表的地址
- 虚表构建规则
- 基类虚表构建规则
- 按照虚函数在类中声明的先后次序依次加载到虚表中
- 派生类虚表的构建规则
- 先将基类虚表中内容拷贝一份放置到子类虚表中
- 如果派生类重写了基类哪个虚函数,则用派生类虚函数的地址替换虚表中相同偏移量位置的基类虚函数(即覆盖)
- 如果派生类增加了新的虚函数,将派生类新增加的虚函数按照其在派生类中声明的先后次序依次增加到派生类虚表的最后
- 多态的调用原理
- 构建动态多态的两个条件已经完全满足
- 虚函数调用
- 从对象全4个字节中获取虚表的地址
- 从虚表中找到具体的虚函数
- 传参
- 调用该虚函数
- 因为基类指针可以指向不同子类的对象,将来从对象前4个字节中拿到的就是其实际指向子类的虚表,而子类虚表存放的虚函数是自己类的虚函数,此时拿到的虚函数就是派生类自己的虚函数,借此来实现多态
- 考点
- 什么是多态?
- 什么是重载、重写(覆盖)、重定义(隐藏)?
- 多态的实现原理?
- inline函数可以是虚函数吗?
- 静态成员可以是虚函数吗?
- 构造函数可以是虚函数吗?
- 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?
- 多态的缺陷
- 虚函数表是在什么阶段生成的,存在哪的?
- 同一个类的不同对象,使用的是同一张虚表吗?
- 一个类的对象可以包含多张虚表吗?
- C++菱形继承的问题?虚继承的原理?
- 什么是抽象类?抽象类的作用?
四种类型转换
static_cast
const_cast
reinterpret_cast
dynamic_cast
STL
概念
- C++提供的标准模板类
- 以模板的方式对常见数据结构进行封装,然后提供了一些通用类型的泛型算法
STL六大组件
- 容器
- 序列式容器
- C++98
- string
- 字符类型的顺序表
- vector
- 动态类型顺序表
- list
- 带头结点双向循环链表
- deque
- 类似动态二维数组
- C++11
- array
- 静态顺序表
- forward
- 带头结点循环单链表
- 关联式容器
- C++98-树形结构容器
- map
- set
- multimap
- multiset
- C++11提供哈希结构容器
- unordered_map
- unordered_set
- unordered_multimap
- unordered_multiset
- 考点
- 熟悉每个容器的常用接口
- 熟悉每个容器背后的数据结构
- 熟悉每个容器的应用场景
- 熟悉各个容器之间的区别
- vector和list之间的区别
- map和set之间的区别
- map和unordered_map之间的区别
- 容器中常用接口的时间复杂度
- 迭代器
- 迭代器模式概念
- 迭代器作用
- 让算法能够透明化(即不用关心容器底层的数据结构)的操作容器中的数据
- 迭代器分类
- 功能分类
- 正向迭代器
- 反向迭代器
- 双向迭代器
- 随机迭代器
- 能否修改迭代器指向元素
- 普通迭代器
- const类型迭代器
- 迭代器的本质
- 迭代器的本质就是一个指针或者对指针进行封装
- 如何给一个容器自定义迭代器
- 先熟悉容器底层数据结构
- 定义迭代器类
- 构造
- 具有指针类似操作:重载operator*()/operator->()
- 迭代器要能够移动:operator++()/operator++(int),根据容器背后的数据结构选择是否支持前置–和后置–
- 迭代器要能够比较:重载operator!=()和operator==()
- 算法
- 与数据结构相关算法
- 各个容器中的方法
- 通用算法
- 不带仿函数的算法
- 带有仿函数算法:用户可以定制算法功能
- 注意
- 使用算法时候包含:头文件
- 常见算法
- 算法的时间复杂度
- 适配器
- 适配器模式概念
- 分类
- 容器适配器
- stack
- queue
- priority_queue
- 迭代器适配器
- 反向迭代器等
- 函数适配器
- 仿函数
- 概念:让一个类的对象可以像函数的方式使用
- 实现方式:在类中重载()即可
- 作用:用来定制算法的功能,让算法的功能更加灵活
- 空间配置器
- 考点
- 什么是空间配置器?空间配置器的作用
- 为什么需要空间配置器
- SGI STL空间配置器实现原理
- 一级空间配置器
- 应用场景:用来处理大于128字节的小块内存
- 实现原理:对malloc和free进行了封装,并增加了空间不足的应对措施
- 二级空间配置器
- 应用场景:处理小于等于128字节的小块内存
- 实现原理
- 封装了一个内存池
- 使用哈希桶对申请和释放空间进行高效管理
- 空间配置器的优缺点
C++11语法糖
1. 列表初始化
2. 类型推导
- auto
- decltype
3. 范围for循环
4. 空值指针
5. override和final关键字
6. 默认成员函数控制:delete和default扩展功能
7. 新增容器
- 序列式容器
- array:静态顺序表
- forward_list:带头结点单向循环链表
- 哈希桶结构关联式容器
- unordered_map
- unordered_set
- unordered_multimap
- unordered_multiset
8. 新增智能指针
- 什么是智能指针
- RAII
- 智能指针的实现原理
- C++98中auto_ptr的实现原理以及缺陷
- unique_ptr
- 实现原理:一份资源只能被一个unique_ptr对象管理,即防拷贝
- 缺陷:多个对象之间不能共享资源
- shared_ptr
- 实现原理:使用引用计数方式实现多个shared_ptr对象之间的资源共享
- 缺陷:可能会出现循环引用
- 解决方式:使用weak_ptr
9. 右值引用
- 什么是右值引用
- 右值引用和引用的区别
- 右值引用能否引用左值,如何做?
- move函数
- 左值和右值
- 移动语义
- 移动构造和移动赋值
- 完美转发
10. lambda表达式
- lambda表达式概念
- lambda表达式语法
- lambda表达式捕获列表的捕获规则
- lambda表达式和仿函数区别
- lambda表达式底层实现原理
11. 线程库
配套视频B站虽好的C语言视频教程,视频里有导图领取方法