C++重点知识梳理(上)
目录
C++基础知识
命名空间:
缺省函数(不支持C):
函数重载:
引用:
内敛函数(inline):
C++类与对象
C++类
封装的特性:
类的实例化:
this指针:
C++动态内存管理
C/C++对内存的分区:
malloc/calloc/realloc的区别:
new和delete的实现原理:
new、delete和malloc、free的区别:
内存泄漏:
C++基础知识
命名空间:
-
概念:一个新的命名空间就是一个新的作用域,可以定义函数、变量、类、命名空间等等,可以防止命名冲突。
-
使用:
namespace + name{......}-
成员需要使用
name(命名空间的名字)::成员,类似于using namespace std;,也可以用using name::成员来单独指定,之后这个成员就不需要再用命名空间修饰,但可能会引起命名冲突。
-
-
Namespac name可以在多个地方定义,在编译时,编译器会将这些同名的命名空间合并为一个。
缺省函数(不支持C):
-
概念:函数定义时,给函数的参数指定默认值,如果调用函数时没传这个参数就会使用默认值,这里的默认值即缺省值。
-
全缺省函数:每个参数都赋予了缺省值。
-
半缺省函数:只有部分参数被赋予了缺省值,并且缺省值只能从右往左给。
-
函数的声明和定义不可同时赋予缺省值,会有二义性,编译器搞不清楚用哪个。
-
缺省值必须是编译时期就可确认的常量或全局变量。
函数重载:
-
概念:相同作用域,函数名相同,参数列表(参数的数量、类型、顺序)不同,与返回值无关。
-
原理:在编译时期,编译器会根据传入的实参去推导使用哪个函数,若推导的结果无二义性就选择函数,否则编译报错。
-
为什么C语言不支持函数重载?
-
函数名修饰规则:编译期,编译器会对函数名进行修饰,在C语言中,编译器仅在函数名前添加
_;在C++中,编译器会将参数类型放在函数名中,来保证函数重载时函数名在底层是不同的。
-
-
两个函数仅返回值不同,是不能重载的:
-
编译器在编译期是通过传入实参的类型来选择要重载的函数的,而参数类型相同会导致二义性,返回值不同不影响结果。
-
-
extern "C" 的用处:让编译器以C语言风格在编译器修饰函数。
引用:
-
概念:引用不开辟空间,是变量的别名(另一个名字),它与被引用对象用的是相同的空间。
-
特性:
-
引用必须在定义时初始化。
-
一旦初始化后,不能再引用其它对象。
-
一个变量可以有多个引用。
-
-
const修饰的引用,引用的对象不可被修改。
-
使用:可作为函数的参数使用,引用作为参数时,相当于传入的这个实参,修改会同步发生变化,与指针相比更安全,指针可能会产生非法操作而导致异常。当对象是自定义类型时,可减少拷贝构造的性能开销。
-
作函数返回值使用:不可返回栈空间上对象的引用,函数一旦返回,这个函数的栈帧就会销毁,放回的引用就会变成野引用,再去操作就是非法操作。
-
引用与指针的区别:
-
在底层实现上:引用在底层就是指针,
T& = T* const,const T& = const T* const。 -
引用在定义时必须初始化,而指针可以悬挂。同样,引用初始化后不能再引用其它对象,而指针可以。并且没有多级引用,只有多级指针。
-
没有空引用,有空指针(nullptr、NULL)。
-
sizeof的区别:
sizeof 引用时是被引用对象类型的大小,而sizeof 指针则是地址所占字节的大小。 -
给引用++、--是对被引用的对象操作,而对指针则是指针本身指向的操作。
-
指针在使用时需要解引用,而编译器会自动处理引用。
-
引用比指针安全。
-
内敛函数(inline):
-
概念:inline修饰的函数,就是内敛函数。在编译期,编译器会在调用内敛函数的地方展开函数,没有压栈的性能开销,但会损耗空间,是空间换时间提升效率的做法。
-
特性:
-
会展开函数,不需要压栈,如果代码很长或代码体很大,不建议内敛,这可能会导致代码暴涨,涉及递归的函数同样不建议内敛,尽管编译器不一定会将它们内敛。
-
inline对于编译器只是建议,编译器不一定会对函数进行内敛,在遇到递归或循环等等的情况,编译器在优化时自动忽略inline的建议。
-
inline函数的声明和定义不要分离,因为一般的函数都是有函数地址的,而内敛的函数是直接展开,在链接时找不到函数地址,也就找不到函数体。
-
-
内敛函数与宏函数的区别:
-
宏函数:在预处理阶段会自动展开,没有函数压栈、调用等等的性能开销。
-
缺点:1. 增加了预处理的时间。2. 编译报错时,不好调试定位。不能调试。3. 参数没有类型,缺乏安全检查。4. 可读性差。5. 可能会引起代码膨胀。
-
-
内敛函数:
-
优点:相比于宏函数:1. 有参数类型,会对参数类型检查,安全性高。2. 可读性好,编译器展开,同样没有压栈、调用等等的消耗。3. DEBUG阶段,不会内敛,并且可调试。4. 几乎没有副作用。
-
缺点:1. 同样可能引起代码膨胀。2. 因为inline只是给编译器的建议,在编译期间,编译器怎么选择用户无法准确得知,可能有歧义。
-
-
C++类与对象
C++类
面向对象与面向过程的区别:
-
面向过程:更注重每个实现的过程,更关注实现中的每个过程步骤。
-
面向对象:更注重每个对象之间的关系,关注每个对象的数据和动作封装。
封装的特性:
-
概念:将对象的数据以及执行的方法有机结合起来,将对象的属性和方法的实现细节隐藏,只对外暴露接口。
-
C++中的封装实现:
-
通过Class或struct将对象的成员数据和成员函数封装起来。
-
再通过类的访问限定符,来访问暴露处理的接口。
-
-
优点:
-
减少耦合度:外部只需关注接口的功能,而无需关注实现细节,可以在外界感知不到的情况下,实现对接口功能实现代码的重写或重构。
-
增强代码复用性以及抽象简化逻辑:将相同的代码进行封装,让多个函数复用;将复杂的函数实现逻辑加一层封装,将其简化成简单的接口,供外部使用。
-
支持继承和多态:子类可继承父类的方法进行复用,或进行重写。
-
-
缺点:
-
间接访问:通过封装的函数访问资源,相比未封装时更慢。
-
过度封装和过度设计:将简单的接口或逻辑进行多次封装,将简单的逻辑多层抽象,过度封装和设计。
-
访问限定符:
-
public:public修饰的类成员可直接在类外访问。
-
protected:被修饰的成员在类外无法访问,不过可以被子类访问。
-
private:在类外无法访问。
struct与class的区别:struct成员的默认访问权限默认是public,而class中的成员默认访问权限则是private,在继承中亦是如此,struct默认为public继承,class默认为private继承。
类的作用域:在类外需要使用相应类名加::来访问成员函数或成员变量,在类则可以将成员变量当作成员函数的全局变量。
类的实例化:
-
用类目去创建对象就是类的实例化。
-
类是对象实例化的前提,对象是类实例化的结果。
-
怎么计算类的大小:
-
根据成员变量作内存对齐,不足最小对齐数的补足,超出部分也作补足,在计算总体大小。
-
还需考虑多继承、虚函数表指针等等的元素大小。
-
-
空类的大小为1字节,1字节的意义是证明自己存在过,且C++规定每个对象的大小至少为1字节,保证它占有唯一的地址。
-
内存对齐:
-
内存对齐概念:要求数据存放的地址必须是某个特定数的整数值,以提高CPU访问内存的效率。
-
结构体中内存对齐的规则:
-
每个变量的存储的起始地址必须是自身对齐值(如int为4,double为8,char为0等等)的整数倍。
-
结构体的总体大小应为最大内存对齐值的整数倍(max(int, double, char......))。
-
编译器自动填充空白字节。
-
-
如何让结构体按指定大小的值进行内存对齐:
-
#pragma pack(push, n):push--保存当前的内存对齐大小,n--新的对齐大小值(字节),可使用#pragma pack(pop)来恢复之前的内存对齐大小。 -
C++11的alignas(n),n--对齐字节大小,非全局,仅指定一个类,
struct alignas(n) structName{};。 -
MSCV编译器支持的
__declspec(align(n)),__declspec(align(n)) struct MyStruct{};。
-
-
结构体成员中某个成员相对结构体的偏移量:通过内存对齐规则计算出这个成员对齐后的起始内存大小即可。
-
大小端是什么,怎么判断,那些场景需要:
-
大小端是两种不同的数据存储方式,大端机的存储方式为--高位数据存在低地址,低位数据存高地址;小端机的存储方式--高位数据存在高地址,低位数据存在低地址。
-
只需使用联合union来判断即可:
-
union example{int num;char ptr[sizeof(int)]; }int main() {example ex;ex.num = 1;if(ex.ptr[0] == 1){std::cout << "低位在前是小端机" << std::endl;}else{std::cout << "高位在前是大端机" << std::endl;}return 0; } -
在网络传输场景中、嵌入式系统开发、硬件寄存器访问等等场景需要分大小端,有必要的话还需统一数据。
-
this指针:
概念:C++给每个类都提供了一个隐含的this指针,在该类的非静态成员函数中访问成员变量或者调用其他成员函数时,就会默认调用this指针。
特点:
-
它的类型是
className* const并指向当前对象。 -
它只能在成员函数中使用。
-
this指针本质是成员函数的一个形参,在对象调用函数时将对象的地址作为实参传给
this的形参。所以this指针并不存于对象中。className::function(className* const this);函数中隐藏的一个参数。 -
this指针作为非静态成员函数的一个隐藏形参,通常是由编译器通过exc寄存器传递的,不需要用户手动传递。
-
概念:函数名与类名相同的特殊成员函数,它在对象的生命周期内只会被调用一次,只在对象首次创建的时候会调用这个构造函数,在这个函数执行时,还会初始化类内的每一个成员。没有返回值。
-
在对象实例化时编译器会自动调用构造函数。
-
构造函数可以重载,编译器会根据实例化时传入的参数自动选择最合适的构造函数。
-
若没有显式定义构造函数,编译器会自动生成一个无参的默认构造函数,只要显式实现了构造函数就不会自动生成。
-
默认构造函数只能有一个。无参构造函数和全缺省构造函数都是默认构造函数,编译器自动生成的也是默认构造函数,即无需传参。
-
构造函数不能用const修饰。并且构造函数不能是virtual函数。
-
即使构造函数为空,构造函数也有具体的事做。
-
初始化列表:所有的成员变量都会通过初始化列表来初始化,初始化的顺序为类内声明的顺序。即使没有显式调用初始化列表,也会走。
-
初始化列表以“:”开头,之后是用逗号隔开的每个成员变量组成的列表,每个成员变量后跟
(初始化内容--值或表达式)来初始化。 -
每个成员变量的初始化都在初始化列表,且只能初始化一次,在构造函数体内的是赋值。
-
只能初始化非静态成员变量。
-
没有构造函数的类类型对象、const成员函数、引用变量必须在初始化列表初始化。
-
-
概念:函数名与类名一样且只有一个形参(
const 本类名&),在创建新对象时传入一个已存在的本类对象时编译器自动调用。 -
特性:
-
拷贝构造函数是构造函数的一种重载。
-
若未显式实现拷贝构造,编译器会自动生成一个拷贝构造函数,不过这个拷贝构造函数是按字节拷贝对象给新构造的对象,也就是浅拷贝(值拷贝)。
-
拷贝构造函数的形参必须是传对象的引用,如果传值的话,会导致无限递归调用拷贝构造,即传值过程中,又会根据传入的对象构造一个副本再给拷贝构造函数使用。
-
-
当涉及到资源管理时(即动态申请内存时),必须要手动实现一个拷贝构造或禁用拷贝构造函数,来避免因为浅拷贝导致的资源泄漏以及程序崩溃。
-
拷贝构造函数也有初始化列表,与构造函数中的一样。
-
概念:用于释放对象申请的内存空间,清理申请的资源,局部对象离开作用域销毁对象时自动调用,若是动态申请的对象,需要手动delete。析构的作用与构造相反。
-
特性:
-
函数名与类名相同,无参,需要函数名加上~。
-
无返回值,且只能有一个析构函数。
-
若不显式实现,编译器会自动生成一个。
-
在继承场景中,析构函数最好加上virtual设置为虚函数。
-
-
若动态申请了资源,在对象销毁时需要释放时,最好手动实现析构函数。
-
在C++中局部若定义了多个对象,遵循后定义先析构。
-
概念:对=这个运算符重载,即
operator=。 -
函数格式:
T& operator=(const T& object); -
概念:函数名与类名相同,与拷贝构造类似,其参数是类对象的引用,不过是右值引用(&&),它通过传入的右值对象来直接初始化对象。如
className(className&& object)。 -
特性:
-
可以直接使用右值对象的成员在初始化列表初始化本对象的成员,之后将后者置空即可。
-
可以有多个参数,但多出的参数必须是缺省参数。
-
可以给移动构造函数添加noexcept标识,这样编译器会优先使用移动构造,而非拷贝构造,在失败时才使用拷贝构造。最好确保移动构造函数不会抛出异常。
-
-
移动构造在有动态申请资源的对象的拷贝效率上比拷贝构造要高不少。
-
概念:移动赋值重载是一种赋值运算符重载,与拷贝赋值类似,不过传入的是类型对象的右值引用。
-
函数格式:
T& operator=(T&& object); -
它的语义是窃取传入的右值对象的资源,所以可以实现一个类内swap来交换类内的资源,毕竟右值对象在使用完之后就要“丢弃”了。若还有资源需要手动释放,并置空指针。
-
就是对&的重载函数,有两个这样的重载,
T* operator&()以及const T* operator&() const。 -
编译器会自动生成这两个函数,返回的是this指针。
-
概念:对于const变量,被视作常量,在编译时期会被编译器识别并被替换,可用于替换宏常量,比宏常量安全。
-
const成员变量:被const修饰的成员变量,不可在成员函数中被修改,除非使用const_cast(不建议)。且const成员变量必须在构造函数的初始化列表初始化,除非使用static或constexpr修饰。
-
const成员函数:const修饰成员函数,相当于用const修饰隐藏的this指针。被const修饰的成员函数不可修改成员变量,除非该成员变量被mutable修饰。
-
mutable:用mutable修饰的成员变量可以在const成员函数中被修改,但不能用于修饰static成员变量。
-
const *:修饰的是指向的数据,指向的数据不可修改,但指针可指向别处。
-
* const:修饰指针本身,指针不可指向别处,但指向的内容可修改。
-
概念:在类中被static修饰的成员,就是静态成员。
-
在类内声明的静态成员变量,不可在类的初始化列表初始化,必须要在类外初始化,在类外必须使用className::来使用。
-
静态成员变量是类的属性,并不属于某个对象,是所有该类对象共享的。不存在某个对象中,因此不会影响sizeof。
-
程序启动时就完成初始化了。
-
用static修饰的成员函数,不存在this指针,并且不可使用virtual修饰。
-
因为不存在this指针,所以不可访问成员变量和其它成员函数。
-
不可被this修饰。
-
可以通过对象和
className::来访问static成员函数 -
static是用于限制定义域的,而extern是用于拓展定义域的,static修饰的变量仅在当前的作用域可见,而一般的全局变量通过extern拓展,可以在其它.c/.cpp......文件内可见。
-
static变量存于静态存储区,全局static变量在程序启动时就完成初始化,局部的static变量在程序第一次进入其作用域时完成初始化。
-
extern仅作为声明,不分配空间,其存储的位置,由定义的变量决定。
-
概念:在类中使用friend声明函数或变量就是这个类的友元,其提供了一种突破封装的方法。它在破坏了封装性的同时提供了一定的便利,需权衡利弊再用。
-
友元函数:它不属于了,但是可以访问类的公有和私有成员,需要在类内使用friend来声明这个函数。
-
特性:
-
可在类内的任何地方声明,不受类内作用域限定。
-
一个函数,可被多个类声明为友元。
-
友元函数不可被const修饰。
-
调用友元函数的方法与调用普通函数一样,且友元函数可访问类内的成员。
-
-
友元类:其类内函数只要有声明友元类的类对象即可调用该类的成员。
-
友元关系是单向的,不可传递,不可继承,不可交换。
-
在C++中为了支持连续赋值操作以及避免不必要的对象拷贝开销,因此其返回值是引用类型。
-
其参数使用
const &,一是为了减少不必要的拷贝开销,二是兼容移动语义,符合将只读拷贝,将传入的 对象转化为左值,三是可以给const对象进行赋值操作。 -
需要显式检测是否为给自己赋值。
-
返回*this是为了支持连续拷贝赋值。
C++动态内存管理
C/C++对内存的分区:
-
内核空间和用户空间。
-
用户空间:栈、内存映射段、堆、数据段(静态区)、代码段(常量区)。
-
各个分区存储的数据:
-
栈区:存储非静态局部变量、函数返回值和函数传递的参数。
-
内存映射段:用于映射各种动态库、文件或共享内存等等,可实现进程间通信,是高效的IO映射方式。
-
堆区:存放程序运行时动态分配的内存。
-
数据段:存放全局数据和静态数据。
-
代码段:存放编译成二进制指令的代码、只读常量。
-
malloc/calloc/realloc的区别:
void* malloc(size_t size);
void* calloc(size_t num, size_t size);
void* realloc(void* ptr, size_t new_size);
-
malloc:底层维护了一个空闲链表,链表上有空闲对象,malloc会优先到空闲链表上查找内存,如果找不到会调用sbrk/mmap分配,若分配大小大于128KB会调用mmap直接分配。
-
calloc:底层调用malloc,在此基础上加上了将申请内存初始化。
-
realloc:当传入的指针为NULL时,行为与malloc一样,可按照传入的指针和新的大小扩容,若扩容空间不足,会调用sbrk/mmap。
new和delete的实现原理:
-
new:调用
void* operator new(size_t size);申请空间。底层循环调用malloc申请空间,若成功则返回,失败则检查用户是否设置空间不足的应对措施,如果没有提供继续调用malloc的申请,就会抛异常bad_alloc。-
可以对已分配空间,进行显式调用构造函数初始化。
-
-
delete:调用
void operator delete(void* p);函数释放资源,底层调用的free函数。若是自定义类型,会调用对应析构函数来释放资源。 -
new T[N]:调用
void* operator new[](size_t size);申请空间,实际调用的是void* operator new来完成空间申请。会调用N次构造来从左往右初始化。 -
delete[] p:与new T[N]类似,实际是调delete,会调用N次析构来释放空间。
new、delete和malloc、free的区别:
-
相同点:都用于申请和释放内存空间,但申请完的空间需要手动释放,否则会造成内存资源泄漏。
-
不同点:
-
malloc和free是函数,new和delete是操作符。
-
malloc不会初始化空间而new可以初始化。
-
malloc需要手动计算申请空间的大小,而new不需要,因为new传的是空间类型。并且malloc返回的void*需要强转,而new不需要。
-
malloc失败时会返回NULL,需要判断,new失败时会检查是否设置备用方案,没有时会抛异常,需要捕获异常。
-
对自定义类型的空间申请和释放,malloc和free不会调构造以及析构函数,只负责开辟和释放内存空间;new和delete会调用构造和析构函数,在申请内存空间后初始化,在释放内存空间前释放资源。
-
内存泄漏:
-
概念:是因为疏忽或失误造成程序无法释放已分配的内存资源的情况。因为设计的失误,造成失去对已分配内存资源的控制,导致资源无法释放,占用内存资源,而导致浪费。
-
堆资源泄漏:即在程序运行期间通过new、malloc从堆上动态申请的内存空间,没有delete、free时,会产生堆资源泄漏(Heap Leak)。
-
系统资源泄漏:如套接字、文件描述符、管道等等系统资源申请后,没有用相应的函数释放掉,导致系统资源泄漏,严重时会导致系统效能减小、变得不稳定。
-
-
危害:若是短期运行且内存需求不大的程序,可能难以发现、影响不大。但对于长期运行的程序危害很大,像操作系统、服务器后台服务等等会因为内存泄漏,导致内存可用资源变小而运行变慢,最后卡死。
-
如何检查内存泄漏:内存资源占用异常、程序异常变慢等等不正常因素可能是内存泄漏问题,在vs中,可以使用windows提供的的
_CrtDumpMemoryLeaks()函数进行简单检测,该函数只报出了大概泄漏了多少个字节,没有其他更准确的位置信息。-
其它的内存泄漏检测工具:
-
Linux环境:https://blog.csdn.net/gatieme/article/details/51959654。
-
windows环境的第三方检测工具:https://blog.csdn.net/GZrhaunt/article/details/56839765。
-
其它检测工具:https://www.cnblogs.com/liangxiaofeng/p/4318499.html。
-
-
-
怎么避免内存泄漏:
-
良好的编程习惯,在申请资源时,要记住是释放,可以注释来表明去处。若期间有异常抛出,需要第一时间,对内存资源进行管理。
-
使用RAII的方式来管理资源,通过智能指针以及配套的释放函数,来管理内存等资源。
-
最后可使用相应检测工具来检查内存泄漏。
-
一个C/C++程序员应该格外关注内存泄漏问题,不应该造成内存泄漏,若使真发生了内存泄漏,应及时排查问题。提前预防即可避免大部分的内存泄漏风险。
-
-
在64位操作系统下,一次申请4G或更大的内存空间:
-
通过malloc来申请,其底层会调用mmap来映射内存空间。
-
直接调用mmap来映射内存空间。
-
通过映射大内存文件来实现,如映射4G的文件到内存中。
-
