- 声明和定义
定义(definition)除了声明该变量或者函数外,同时也给它分配了内存空间,并可为其赋予初始值。对于函数来说,函数的定义就是函数体,包含了具体的代码实现。
int x; // 定义了一个整型变量x double func(int a, int b) { return a+b; } // 定义了一个函数func
声明(declaration)声明是编译器需要的,它告诉编译器某个变量或者函数的存在,使其可以在编译期间进行正确的引用,它并不会为变量分配内存。它包含了变量或者函数名字和类型,对函数来说还包括了参数类型,但是不需要参数名。
extern int x; // 声明了一个全局变量x double func(int, int); // 声明了一个函数func
在C++中可以在多个地方声明一个变量或者函数一般在头文件中,但只能定义一次(全局变量或函数可以在多个.cpp文件中声明,但只能在一个.cpp文件中定义),哪怕在同一个作用域中也是如此。如果一个变量或函数被定义但未被使用,编译器不会报错。然而,如果一个变量或函数被声明但是没有定义,编译器会在链接阶段报错。
- vector扩容
- 为什么成倍增长
假设倍增因子为m,当我们向vector中push_back n次时,总共经过logm(n)次扩容,每次扩容拷贝m^i个元素,则共拷贝约nm/m-1次元素,则总体时间复杂度依旧为O(n)
如果每次增加k个元素,则需进行n/k次扩容,每次每次扩容拷贝ki个元素,则总体时间复杂度为O(n^2) - 扩容的倍数为1<x<2
首先扩容倍数不可能小于1,其次当扩容倍数大于等于2时,每次扩容后的空间总是大于之前被替换掉的空间没有办法利用之前的空间,导致局部性比较差。比如扩容倍数为2,那么每次扩容后的空间一次为1 2 4 8 16 32 ....... 1+2<4,1+2+4<8 ......如果扩容倍数小于2那么多次扩容后就能够利用之前的空间。
- 为什么成倍增长
- malloc实现
- 内存分配
- 栈区(stack)
由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。 - 堆区(heap)
一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表 - 全局区(静态区)(static)
全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 程序结束后由系统释放 - 文字常量区
常量字符串就是放在这里的。 程序结束后由系统释放 - 程序代码区
存放函数体的二进制代码
- 栈区(stack)
- 内存泄漏
程序执行时开辟了堆上的空间,但使用完毕后没有释放该空间
- 野指针
指向非法内存块的指针
- 函数指针与数组指针
- int *p[10]表示指针数组,强调数组概念,是一个数组变量,数组大小为10,数组内每个元素都是指向int类型的指针变量。
- int (*p)[10]表示数组指针,强调是指针,只有一个变量,是指针类型,不过指向的是一个int类型的数组,这个数组大小是10。
- int *p(int)是函数声明,函数名是p,参数是int类型的,返回值是int *类型的。
- int (*p)(int)是函数指针,强调是指针,该指针指向的函数具有int类型参数,并且返回值是int类型的。
- 迭代器失效
当迭代器指向的元素位置发生改变的时候迭代器失效。
- 数组型数据结构:该数据结构的元素是分配在连续的内存中,insert和erase操作,都会使得删除点和插入点之后的元素挪位置,插入点和删除点之后的迭代器全部失效。特别的对于vector,当insert操作导致容器扩容时,所有的迭代器均失效。
- 链表型数据结构:对于list型的数据结构,使用了不连续分配的内存,insert操作不会使得任何迭代器失效,erase操作使指向删除位置的迭代器失效,但是不会失效其他迭代器。
- 树形数据结构:使用红黑树来存储数据,insert操作不会使得任何迭代器失效,erase操作使指向删除位置的迭代器失效,但是不会失效其他迭代器。
- volatile
- volatile修饰变量
volatile的本意是“易变的”遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。当使用volatile声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。 - volatile修饰成员函数
若类的对象被声明为volatile,则只能调用volatile成员函数
- volatile修饰变量
- static
- static修饰函数内变量
static修饰的变量分配在静态存储区,在程序整个运行期间都不释放
static局部变量在初次运行时进行初始化工作,且只初始化一次 - static修饰函数外变量/函数
用来表示不能被其它文件访问的全局变量和函数。 - static修饰数据成员
静态数据成员实际上是类域中的全局变量,这种数据成员的生存期大于类的对象
静态数据成员每个类有一份,普通数据成员每个对象有一份 - static修饰成员函数
静态成员函数不可以调用类的非静态成员
静态成员是可以独立访问的,也就是说,无须创建任何对象实例就可以访问
- static修饰函数内变量
- const
- const修饰普通变量和指针
const修饰的类型为TYPE的变量value是不可变的
const TYPE value;
TYPE const value;
指针本身是常量
char* const p;
指针所指向的内容是常量不可变
const char *p;
char const p;
两者都不可变
const char const p; - const修饰类对象
任何成员都不能被修改,只能调用const成员函数 - const修饰数据成员
const数据成员只在某个对象生存期内是常量,而对于整个类而言却是可变的不同的对象其const数据成员的值可以不同 - const修饰成员函数
用const修饰的成员函数不能改变对象的成员变量 - const常量与define宏定义的区别
- 编译器处理方式不同
define宏是在预处理阶段展开。
const常量是编译运行阶段使用。 - 类型和安全检查不同
define宏没有类型,不做任何类型检查,仅仅是展开。
const常量有具体的类型,在编译阶段会执行类型检查。 - 存储方式不同
define宏仅仅是展开,有多少地方使用,就展开多少次,不会分配内存
const常量会在内存中分配(可以是堆中也可以是栈中)
- const修饰普通变量和指针
- mutable
mutalbe的中文意思是“可变的,易变的”,跟constant(既C++中的const)是反义词,在C++中,mutable也是为了突破const的限制而设置的。
被mutable修饰的变量(mutable只能用于修饰类的非静态数据成员),将永远处于可变的状态,即使在一个const函数中。 - extern
- extern修饰变量或函数
表明此变量/函数是在别的文件定义的,要在此处引用 - extern "C" {}
表明之后的代码段为C语言代码段,告诉链接器在链接的时候用C函数规范来链接。
主要原因是C++和C程序编译完成后再目标代码中命名规则不同
- extern修饰变量或函数
- strlen和sizeof区别
- sizeof是运算符,并不是函数,结果在编译时得到而非运行中获得;strlen是字符处理的库函数。
- sizeof参数可以是任何数据的类型或者数据(sizeof数组参数不退化);strlen的参数只能是字符指针且结尾是'\0'的字符串。
- 因为sizeof值在编译时确定,所以不能用来得到动态分配(运行时分配)存储空间的大小。
- sizeof()返回字节个数,strlen()返回字符串长度(不含'\0')
- malloc和new区别
- new/delete:在C++中,new和delete不仅负责分配和释放内存,而且还负责对象的构造和析构。当你使用new创建一个对象时,它会先分配所需的内存,然后调用对象的构造函数。类似地delete不仅会释放内存,还会调用对象的析构函数。new和delete还支持通过抛出异常处理分配内存失败的情况。
- malloc/free:在C和C++中,malloc和free只负责分配和释放内存,不涉及对象的构造和析构。因此一般而言,在C++中应该使用new/delete,在C中应该使用malloc/free。
- 如果使用free释放一个由new创建的动态对象,不会调用对象的析构函数可能会造成资源泄露(如动态分配的内存或打开的文件句柄等)。同样如果使用delete去释放malloc分配的动态内存,可能会导致内存破坏,导致程序崩溃。
- delete与delete[]
class A { private: char *m_cBuffer; int m_nLen; public: A(){ m_cBuffer = new char[m_nLen]; } ~A() { delete [] m_cBuffer; } }; A *a = new A[10]; delete a; //仅释放了a指针指向的全部内存空间 但是只调用了a[0]对象的析构函数 剩下的从a[1]到a[9]这9个用户自行分配的m_cBuffer对应内存空间将不能释放 从而造成内存泄漏 delete [] a; //调用使用类对象的析构函数释放用户自己分配内存空间并且 释放了a指针指向的全部内存空间
- new,operator new,placement new
- operator new
声明:void *operator new(size_t size)
使用:void *mem = new(sizeof(string));
operator new负责分配size长度的内存并返回一个指向这块内存的指针。可以重载。 - placement new
使用:string *sp = new(mem) string()
placement new负责在给定的内存上构造一个对象。 - new
使用:string *sp = new string()
new为一个关键字,它先分配内存,再在这个内存上构造对象。可以理解为先执行operator new,再执行placement new。
- operator new
- 内联函数
- 普通函数的执行过程:在栈空间上定义函数的参数,保存函数的返回地址,执行函数体的代码,执行完毕后在根据返回地址返回到原来的执行位置。
- 普通函数都会在text段对应着一段代码,每次调用函数时都要执行这个代码段。而內联函数每次被调用时都会将代码段直接就地展开。
- 当编译器处理调用内联函数的语句时,直接将整个函数体的代码插人调用语句处。
- inline对编译器而言只是一个建议,编译器优化时可能会自动忽略掉内联。
- 优点:没有函数压栈出栈的开销。与宏相比,宏为简单的文本替换,有时候执行结果与预料结果可能不同 SQUARE(1+2)。
- 缺点:代码量膨胀,导致内存中代码区体积变大,从而影响程序性能。
-
面向对象的三大特性
- 封装性:隐藏实现细节,使得代码模块化;封装是把函数和数据捆绑起来,对数据的访问只能通过已定义的接口。
- 继承性:让某种类获得另一个类的数据和函数。
- 多态性:同一事物表现出不同事物的能力,即向不同对象发送同一消息,不同的对象在接收时会产生不同的行为(重载实现编译时多态,虚函数实现运行时多态)。
-
拷贝构造函数和赋值运算符重载的区别
- 拷贝构造函数是函数,赋值运算符是运算符重载。
- 拷贝构造函数会生成新的类对象,赋值运算符不能。
- 拷贝构造函数是直接构造一个新的类对象,所以在初始化对象前不需要检查源对象和新建对象是否相同;赋值运算符需要上述操作并提供两套不同的复制策略,另外赋值运算符中如果原来的对象有内存分配则需要先把内存释放掉。
- 形参传递是调用拷贝构造函数(调用的被赋值对象的拷贝构造函数),但并不是所有出现"="的地方都是使用赋值运算符,如下:
Student s; Student s1 = s; // 调用拷贝构造函数 Student s2; s2 = s; // 赋值运算符操作
-
拷贝构造函数实现不可以用值传递
当一个对象需要以值方式传递时,编译器会生成代码调用它的拷贝构造函数以生成一个副本。如果类A的拷贝构造函数是以值方式传递一个类A对象作为参数的话,当需要调用类A的拷贝构造函数时,需要以值方式传进一个A的对象作为实参;而以值方式传递需要调用类A的拷贝构造函数;结果就是调用类A的拷贝构造函数导 致又一次调用类A的拷贝构造函数,这就是一个无限递归。
-
构造函数和析构函数能否为虚函数
- 构造函数不可以为虚函数
类的构造函数会为对象进行内存分配。虚函数的调用需用访问虚函数表。如果构造函数为虚函数,而此时构造函数还没有为虚函数表分配内存,如果没有虚函数表那么就无法调用虚函数。结果导致,要想调用虚构造函数就必须要由虚函数表,而要有虚函数表就必须先调用构造函数。 - 析构函数可以为虚函数
一般情况下基类析构函数要定义为虚函数。只有在基类析构函数定义为虚函数时,调用操作符delete销毁指向对象的基类指针时,才能通过运行时多态的机制调用派生类的析构函数准确销毁数据。如果析构函数不是虚函数,调用操作符delete销毁指向对象的基类指针时会调用基类的析构函数,从而导致派生类的部分内存空间没有释放,造成内存泄漏。
- 构造函数不可以为虚函数
-
构造函数与析构函数的调用顺序
1.根据继承表从左到右的执行虚基类的构造函数
2.根据继承表从左到右的执行基类的构造函数
3.如果为多重继承,则先执行最底层的基类的构造函数,逐步向上的执行构造函数,最后执行直接继承的基类的构造函数
4.根据成员类的声明顺序,从上到下的执行成员类的构造函数
5.最后执行自身的构造函数
//析构函数的执行顺序与构造函数的执行顺序完全对称相反. -
多继承
多继承可以看作是单继承的扩展。所谓多继承是指派生类具有多个基类,派生类与每个基类之间的关系仍可看作是一个单继承。派生类的成员包含所有基类中的成员以及该类本身的成员。
class <派生类名>:<继承方式1><基类名1>,<继承方式2><基类名2>,… { <派生类类体> };
- 菱形继承
class A //sizeof(A) = sizeof(m) + sizeof(vptr) = 12 { public: int m; void virtual fun(){} }; class AA : public A //sizeof(AA) = sizeof(m) + sizeof(vptr) = 12 { public: void virtual fun(){} }; class BB : public A //sizeof(BB) = sizeof(m) + sizeof(vptr) = 12 { public: void virtual fun(){} }; class AAA : public AA, public BB //sizeof(AAA) = sizeof(m) + sizeof(vptr) + sizeof(m) + sizeof(vptr)= 24 { public: void virtual fun(){} };
此时类D中有两份类A的数据,如果使用D.a访问变量a则会造成语义不清,此时要通过作用域运算符指定访问哪一个变量a,如: D.B::a; D.C::a;
-
虚继承
虚继承是解决C++多重继承问题的一种手段,从不同途径继承来的同一基类(菱形继承),会在子类中存在多份拷贝。这将存在两个问题:其一,浪费存储空间;第二,存在二义性问题。
class A //sizeof(A) = sizeof(m) + sizeof(vptr) = 12 { public: int m; void virtual fun(){} }; class AA : virtual public A //sizeof(AA) = sizeof(m) + sizeof(vptr) + sizeof(vbptr)= 20 { public: void virtual fun(){} }; class BB : virtual public A //sizeof(BB) = sizeof(m) + sizeof(vptr) + sizeof(vbptr)= 20 { public: void virtual fun(){} }; class AAA : public AA, public BB //sizeof(AAA) = sizeof(m) + sizeof(vptr) + sizeof(vbptr) + sizeof(vbptr)= 28 { public: void virtual fun(){} };
-
C++空类大小
在C++中空类会占一个字节,因为空类同样可以被实例化,并且每个实例都在内存中占有空间,因此,编译器会给空类隐含加上一个字节,这样空类实例化之后就会拥有内存地址。如果没有这一个字节的占位,那么空类就无法实例化,因为实例化的过程就是在内存中分配一块地址。此特殊字节仅仅是为了使空类对象在内存中占有空间,当空类被继承时,此特殊字节会被优化掉。
-
内存对齐
CPU一般按照某一固定字节数存取内存,内存对齐可以使访问某一数据时CPU进行内存存取的次数减少。
结构体内存对齐规则:- 每个编译器都有自己的“对齐系数”,gcc中通过预处理指令#pragma pack(n)来指定“对齐系数”。
- 结构体的对齐单位为对齐系数和结构体中最长数据类型长度中较小的那个。
- 结构体的第一个数据成员的偏移量为0,之后的数据成员偏移量为数据成员大小与结构体对齐单位中较小的那个的整数倍。必要时编译器会在数据成员之间添加填充字节。
- 结构体的总大小为结构体对齐单元的整数倍,必要时编译器会在最后一个成员之后添加填充字节。
-
虚函数
虚函数是为了实现运行时多态而出现的。通过基类类型的指针指向不同的由基类派生出的类的对象时,自动调用相应的同名虚函数。
在成员函数声明前加上virtual关键字表示虚函数。子类的虚函数需要与对应的基类的虚函数的函数类型完全相同,在子类虚函数声明后加override关键字可以在编译期检测子类虚函数覆盖是否成功。
在基类虚函数声明后加上关键字=0表示纯虚函数。基类不对纯虚函数进行实现,纯虚函数相当与提供一个接口。含有纯虚函数的类叫做抽象类,抽象类不能被实例化,一般用来被子类继承。
如果一个类含有虚函数或继承的基类中含有虚函数,那么这个类中包含一个指针vptr位于地址起点,vptr指向一个对应该类的虚函数表,虚函数表中存储着指向虚函数的指针。 -
对象模型
- 非静态数据成员:存贮在类内,占用类的内存空间。
- 静态数据成员:存贮在类外,进程的data段或bss段,不占用类的内存空间。
- 非静态成员函数:存贮在类外,进程的text段,不占用类的内存空间。与普通函数不同之处在于,类的非静态成员函数中暗含着一个this指针,该指针指向类的对象,并通过this指针访问对象的数据成员。
- 静态成员函数:存贮在类外,进程的text段,不占用类的内存空间。
- 虚函数:如果类中存在虚函数,那么类就暗含一个vptr指针,该指针占用类的内存空间并指向虚函数表,虚函数表中包含指向虚函数的指针,位于类的地址起点。
- 虚基类:如果类虚继承于基类,那么类就暗含一个vbptr指针,该指针占用类的内存空间并指向虚基类表,虚基类表中包含虚基类的数据在继承类中的偏移,位于vptr的上方(地址大于vptr)。
- 填充字节:编译器为了内存对齐而在类中加入填充字节,占用类的内存空间。
-
限制类对象的分配方法
- 只能动态分配 将类的构造函数与析构函数设为私有函数,这样就无法直接在栈上定义类对象,再创建一个公有的静态函数为类对象动态分配内存空间,和一个共有的函数为类对象释放空间。为了使类能够被顺利继承(如果基类的构造函数与析构函数为私有的,那么派生类就无法构造和析构)我们一般将构造函数与析构函数申明为受保护的。
class A { protected: A(){} ~A(){} public: static A* create(){return new A();} void destory(){delete this;} };
- 只能静态分配
重载new,delete运算符,使其为私有函数,这样就无法为类对象动态分配内存空间。
class A { private: void* operator new(size_t t){} void operator delete(void* ptr){} public: A(){} ~A(){} };
- RAII
RAII(Resource Acquisition Is Initialization),是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的技术。RAII 的一般做法是:在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。
- 右值引用
右值引用(R-value reference),标记为T &&,说到右值引用类型之前先要了解什么是左值和右值。左值有名称,对应指定内存域,可访问;右值不具名,不对应内存域,不可访问。临时对像是右值。左值可处于等号左边,右值只能放在等号右边。区分表达式的左右值属性有一个简便方法:若可对表达式用 & 符取址,则为左值,否则为右值。右值引用本身为左值。
- 引用折叠
- 所有右值引用折叠到右值引用上仍然是一个右值引用。(A&& && 变成 A&&)
- 所有的其他引用类型之间的折叠都将变成左值引用。 (A& & 变成 A&; A& && 变成 A&; A&& & 变成 A&)
- std::move
std::move()函数可以理解为一个右值引用转换,即获取实参的右值引用,实现方式如下:
template <typename T> typename remove_reference<T>::type&& move(T&& t) { return static_cast<typename remove_reference<T>::type&&>(t); }
- 移动构造函数与移动赋值运算符
class A { A(A&& obj); A&operator=(A&& obj); };
由于函数实参为一个右值引用,我们认为它具有临时性,所以我们进行浅拷贝(对于该对象堆上的资源,我们只拷贝它的地址,而不完全开辟新的内存空间拷贝它的资源),浅拷贝完成后将函数实参对象内的指针变量置为nullptr。所以,对被浅拷贝过的对象进行操作是未定义的行为。
- 函数对象
函数对象即一个可执行的对象,通常做法是在重载一个类的()运算符。函数对象可用来代替函数指针。
- lambda表达式
[ 捕获列表 ] ( 参数列表 ) -> 返回类型 { 函数体 }
lambda是函数对象,lambda产生的类为捕获的参数创建相应的数据成员 - std::function
std::function< 返回类型 ( 参数列表 ) >是一个通用函数封装模板,可以封装所有的可调用元素,例如普通函数和函数对象函数指针
- std::bind
std::bind用来为函数对象绑定参数,接受一个函数对象与数个待绑定的参数返回一个新的函数对象,用占位符(std::placeholders::)表示新生成的函数对象的参数。
int fun(int,int); std::function<int(int)> new_fun = std::bind(fun,1,std::placeholders::_1);
- 智能指针
以RAII的方式管理资源
- 引用计数:
要想实现引用计数这个功能,需要在类内定义一个指针成员,指针成员指向在堆上存储的引用计数变量。
1. 调用构造函数初始化对象时,将引用计数初始化为1。
2. 调用拷贝构造函数初始化对象时,拷贝给定对象的计数器,并将计数器+1。
3. 调用析构函数销毁对象时,将引用计数-1,当引用计数为0时,销毁共享资源。
4.调用拷贝运算符时,将右值引用计数+1,将左值引用计数-1,当左值引用计数为0时,销毁共享资源。 - unique_ptr:
unique_ptr独享资源,unique_ptr不可拷贝可移动。unique_ptr对象构造时获取资源,析构时释放资源。 - shared_ptr:
shared_ptr共享资源,shared_ptr可以拷贝。每个shared_ptr都有一个引用计数,引用计数表示shared_ptr指向的资源当前被多少个shared_ptr共享,当引用计数为0时(即:没有shared_ptr共享该资源)该资源被释放。 - 循环引用:
class A { public: shared_ptr<B> _sp_b_; }; class B { public: shared_ptr<A> _sp_a_; }; int main() { shared_ptr<A> sp_a(new A()); shared_ptr<B> sp_b(new B()); sp_a._sp_b_ = sp_b; sp_b._sp_a_ = sp_a; }
当sp_a、sp_b析构时,并不会释放它们指向的资源,从而造成内存泄漏。
- weak_ptr:
weak_ptr是一种弱引用,它只观察被管理的资源,不控制资源的生命周期。
weak_ptr通过shared_ptr或weak_ptr对象初始化,通过lock()获得被观察对象的shared_ptr,调用lock前需要调用expired()确定被观察对象是否已被销毁。
weak_ptr的出现可以用来解决循环引用的问题。将上述循环引用的shared_ptr成员改为weak_ptr成员即可避免循环引用。
- 引用计数:
- nullptr
NULL和nullptr的定义分别如下:
#define NULL 0 #define nullptr ((void *)0)
可以看出NULL为常量0,nullptr为空指针
引入nullptr是为了避免函数重载时的二义性,如:void fun_NULL(int); void fun_nullptr(int *);
- emplace_back()
emplace_back与push_back()的主要差异在,emplace_back使用可变参数模版。例如我们的容器内存储的对象类型为A,构造A的参数为int,int,int。当我们调用push_back({1,2,3})时,我们先执行依次构造函数生成一个临时对象,再调用移动构造函数生成一个对象。而对于emplace_back由于可以接收多个参数,那么就只需要调用一次构造函数。