秋招基本尘埃落定了,打算总结一下C++学习路线的相关知识,今天是第一篇:C++基础知识总结。
封装:把客观事物抽象为类,包含自己的属性和方法。
继承:使用现有类的所有功能,在无需重新编写原有类的情况下对类的功能进行拓展。被继承的类成为父类或基类,继承的类成为子类或派生类。
多态:一种形式,多种状态,分为静态多态和动态多态。静态多态指编译时多态,如函数重载、模板;动态多态指运行时多态,特指virtual虚函数机制形成的多态。
类中可以直接访问自己类的public、protected、private成员,但类对象只能访问自己类的public成员。
构造函数:一个类对象创建时自动调用的方法,用来完成初始化得工作。
析构函数:当类对象生命周期结束时,自动销毁对象占有的内存。
拷贝构造函数:参数为类的引用,即用一个类对象复制构造一个新的类对象。
赋值函数:将一个类对象成员的值赋给当前对象。
一个类会自定义默认构造函数、默认拷贝构造函数、默认赋值函数、析构函数。
拷贝构造函数的形参是一个左值引用,而移动构造函数的形参是一个右值引用
拷贝构造函数完成的是整个对象或变量的拷贝,而移动构造函数是生成一个指针指向源对象或变量的地址,接管源对象的内存,相对于大量数据的拷贝节省时间和内存空间。
浅拷贝:
利用类提供的默认拷贝构造函数,将一个对象的成员所在内存的数据复制给另一个对象的成员。
深拷贝:
显式定义类的拷贝构造函数,不仅会将原对象的成员变量复制给新对象,还会在堆中为新对象分配一块新的内存,并将原对象持有的动态内存资源也拷贝过来。
一个空类包括默认构造函数、拷贝构造函数、默认赋值运算符、默认取值运算符、析构函数。
空类也可以实例化,在内存会有独一无二的地址,编译器规定空类的大小为1字节;
仅含有一个虚函数的类的大小为4字节,因为虚表指针占有4个字节地址。
全局区:存储全局变量和静态变量,编译时分配,存在整个程序运行期间。
堆区:程序员手动管理的内存区间,运行时分配,手动开辟、释放内存。
栈区:存储局部变量,运行时分配,系统自动管理内存的开辟和释放。
常量区:存放常量,编译时分配,存在整个程序运行期间。
代码区:存放程序运行的cpu指令,编译时分配,系统自动管理内存的开辟和释放。
申请方式:栈区内存由系统自动管理,堆区内存由程序员手动管理。
空间大小:栈区的大小一般为1M或2M,不超过10M;堆区的大小是不固定的,受硬件内存的限制。
申请效率:栈是线性存储的,由系统自动分配,速度较快;堆区通过一个链表管理空闲内存,容易造成内存泄漏。
生长方向:栈区是由高到低的,堆区是由低到高的。
C++是面向对象的编程语言,C是面向过程的结构化编程语言。
C++采用new/delete运算符动态管理内存,而C用malloc/free库函数管理内存。
C++提供了class,C提供了struct。在C++中使用struct与class等同。
C++有引用,而C没有。
C++提供了很多输入输出流,如:iostream、ofstream。
指示编译器这部分代码按照C语言的方式进行编译。如:C语言不提供函数重载,为避免重载,可使用extern C。
C语言中,函数在编译后的代码名称为“_函数名”,当同名的函数参数不同时,编译器无法解析到它们的不同,
因为它们的编译名称都相同。
C++中,函数参数不同对象的编译后的名称也不同,所以支持重载。
struct在C中,不能定义函数、没有访问权限的设定,只能定义数据成员变量;在C++中,与class类似,可以定义函数、实现继承以及多态,以及有public、protected、private三种访问权限。
C++中,struct与class的唯一区别是 :
struct中成员默认的访问权限是public,class中成员默认的访问权限是private。
结构体中各成员变量的首地址必须是“对齐系数”和“变量大小”的较小值的整数倍;
结构体占用内存的总大小是“对齐系数”和“最大变量长度”的较小值的整数倍。
处理时机不同:宏定义是“编译时”概念,在预处理阶段展开,生命周期结束于编译时期;const是“运行时”概念,在程序运行时使用。
存储方式不同:宏定义直接替换,不会分配内存,存储在程序的代码区中;const变量需要进行内存分配。
类型和安全检查不同:宏定义是字符替换,没有数据类型的区别,没有类型安全检查;const是常量的声明,有类型区别,编译时会进行安全检查。
malloc是C语言定义的库函数,new是C++定义的运算符;
malloc由程序员指定分配内存的大小,而new由编译器自动计算指定类型的内存大小;
new会调用构造函数,并返回对象内存大小,malloc不会;
malloc返回类型是void*指针,需要强转换,new的返回类型是对象类型的指针;
malloc不能重载,new可以重载operator new。
不可以。new一个对象,delete时会先调用类的析构函数再释放内存,而free仅仅释放内存,会出现错误。
new一块内存,没有手动delete,会造成内存泄漏;
使用智能指针shared_ptr时,产生循环引用,导致内存无法释放,会造成内存泄漏;
类的继承中,用一个基类指针指向父类或子类对象时,析构函数没有采用虚函数,生命期结束时导致子类对象占有内存无法释放,造成内存泄漏。
sizeof计算的是分配的内存空间实际占用的字节数; strlen计算的是内存空间中字符的个数(不包括"\0")。
sizeof是运算符,参数可以是指针、数组、类型、对象和函数; strlen是函数,参数必须是字符型指针(char*)。
sizeof是在编译时计算,故参数为数组名时计算的是整个数组的占用内存大小; strlen是在运行时计算,参数为数组名时,数组会退化为指针。
安全性:引用的对象不能改变,安全性好;但指针指向的对象是可以改变的,不能保证安全性。
方便性:引用实际上是封装好的指针解引用(即b->*b),可以直接使用;但指针需要手动解引用,使用更麻烦;
级数:引用只有一级,不能多次引用;但指针的级数没有限制。
初始化:引用必须被初始化为一个已有对象的引用,而指针可以初始化为NULL。
指针函数:返回值是指针的函数,本质上是一个函数;
函数指针:指向一个函数的指针,本质上是一个指针;
野指针产生的原因:
创建指针时没有对指针进行初始化,导致指针指向一个随机的位置;
释放指针指向的内存后没有置空,从而指向垃圾内存;
在超越变量作用域下使用指针,如:在栈内存被释放之后,指向栈内存的指针会指向垃圾内存;
野指针的避免方法:
在创建指针时必须进行初始化;
使用delete释放指针指向的内存之后必须将指针置空;
动态多态:运行时多态,通过指向基类的引用或指针来操作子类对象,对虚函数的调用会自动绑定到实际提供的子类对象。
当派生类继承基类时,在基类中利用virtual关键字定义若干个虚函数,会生成基类的虚表A,表中存放着所有虚函数的地址,而在派生类中重写某个虚函数func1时,派生类的的虚表B中,会用重写的虚函数地址覆盖原来的函数func1的地址,其它虚函数地址不变。程序采用迟绑定的编译模式,当基类指针指向基类对象时,基类对象的vptr指向虚表A,就会运行基类的func1;而基类指针指向派生类对象时,派生类对象的vptr指向虚表B,就会运行派生类的func1。这样,根据基类指针指向不同的对象,进而调用不同的函数体,就会有不同的表现形式,即实现动态多态。(虚函数实现多态的原理)
静态多态:编译时多态,模板、函数重载均为静态多态。
纯虚函数是在基类中只声明不定义的虚函数,提供多态实现的接口,其声明方式为“virtual void function()=0”。
含有纯虚函数的基类是纯虚基类,不能生成对象。
派生类中必须对基类中声明的纯虚函数进行定义,否则无法编译。
析构函数应为虚函数:(基类的析构函数是否定义为虚函数)
若不用虚函数,对象生命期结束时,只会释放父类占有的内存,而不会释放子类内存,造成内存泄漏。
构造函数不能为虚函数:
虚函数的作用是通过基类指针来调用子类的成员函数,而构造函数是在创建对象时自己主动调用的,不需要通过基类指针去调用;
若构造函数是虚函数,会有相应的虚表指针vptr指向虚函数表,但是对象只有通过构造函数实例化,才能生成虚表指针,这是矛盾的。
函数重载:函数参数类型不同、个数不同、顺序不同,导致编译时识别的函数名称不同。
模板:分为函数模板和类模板,通常由template生命。
(从C++运算符重载到类模板)
static+局部变量:存储在静态区,存在于整个程序运行期间。函数外部无法访问该变量。
static+全局变量:存储在静态区,存在于整个程序运行期间。只存在于当前文件,其他cpp文件无法访问。
static+类中成员变量:属于类而不属于某个对象,不具有this指针。类内定义,类外初始化。
static+类中成员函数:属于类而不属于某个对象,不具有this指针。无法访问类的非静态成员函数和非静态成员变量。
const+变量:常量。
const+指针:常指针。
const+类对象:对象的任何成员都不能被修改,只能调用const成员函数。
const+成员变量:变量的值不能修改,只能在初始化时赋值。
const+成员函数:不能调用非const成员函数和非const成员变量。
extern+全局变量:表示该全局变量在其他文件中已经定义,可以直接使用。
避免编译器指令优化
static_cast:用于编译器无法自动执行的类型转换;
dynamic_cast:将一个基类指针(或引用)转换为派生类指针(或引用),一般用于有继承关系的类中;
const_cast:将const修饰的常量转换为非常量,一般用于重载函数中;
reinterpret_cast:将变量从类型1转换为类型2,编译器会当作类型2,真实类型为类型1。(不建议使用)
右值,就是在内存没有确定存储地址、没有变量名,表达式结束就会销毁的值。顾名思义,右值引用就是对右值的引用。
引入右值引用的原因:
替代需要销毁对象的拷贝,提高效率;
使用move移动含有不能共享资源的类对象
move告诉编译器将左值像对待同类型右值一样处理,但是被调用后的左值将不能再被使用。常用于内存资源所有权的转移,避免内存重复拷贝及释放。
std::move,将左值转换为右值,从而将转移内存资源的所有权,不用进行内存拷贝。
std::forward,完美转发,参数将左值时返回左值引用,参数为右值时返回右值引用。
万能引用:当一个参数或变量被声明为T&&,其中T是被推导的类型,那么这个参数或变量就是万能引用。即传入的可以是左值也可以是右值。
独占式智能指针:
auto_ptr:提供了拷贝构造、拷贝赋值操作,再置空原对象指针成员,这样显得拷贝毫无意义,而且在使用中极易误操作原对象,从而出现错误且很难发现
unique_ptr:利用移动构造函数和移动赋值函数来实现资源的转移,即通过右值引用方式来销毁原对象,并把内存资源的所有权转移给新对象。
智能指针原理剖析(一):auto_ptr、unique_ptr
共享式智能指针:shared_ptr、weak_ptr
所有shared_ptr对象中通过同一个计数器来计算资源的引用计数_Uses,可通过拷贝构造函数、移动构造函数、赋值函数来增加新的shared_ptr指向该对象,并自动调用计数器递增引用计数。当某个shared_ptr生命期结束时,会自动调用析构函数,析构函数中会递减它所指向对象的引用计数,若强引用计数为0,析构函数就会调用计数器的_Destroy() 函数释放内存资源。
智能指针原理剖析(二):shared_ptr、weak_ptr
在可能出现循环引用的地方使用weak_ptr来替代shared_ptr。
vector:
相当于一个动态数组,可实现容器长度的变化,在内存上占有一段连续的地址空间;
擅长随机访问以及在容器尾部添加或删除元素,效率较高;
在容器头部及中间位置插入或删除元素,需要移动该位置以后的所有元素,效率较低;
list:
容器中各个元素的前后顺序靠指针联系在一起,在内存中不一定是连续的地址空间;
擅长随机插入或删除元素,只改变前后两个元素的联系,效率高;
不擅于随机访问元素,遍历容器需要访问大量指针,效率极低;
deque:
将多个连续内存空间通过指针数组连接在一起,不能保证所有元素在同一段连续的地址空间中;
容器的地址空间是动态变化的,可以向两端进行伸缩,内部实现比较复杂;
擅长在容器首部、尾部插入或删除元素,效率高;
push_back导致迭代器失效:当容量不足时会触发扩容,导致整个vector重新申请内存,造成迭代器失效。
insert导致迭代器失效:与push_back类似。
erase导致迭代器失效:导致被删除位置之后的元素移动,导致被移动部分的迭代器失效。
(vector迭代器失效的情况)
有序性:map是有序的,unordered_map是无序的。
底层数据结构:map底层数据结构是红黑树,unordered_map底层数据结构是哈希表。
查询、插入效率:map查询时间复杂度为O(logn),unordered_map查询时间复杂度为O(1)。
占用内存:unordered_map占用内存比map小。
unordered_map是key-value式存储的,unordered_set是key式存储的。