c语言
c++语言
1.c++vsc
1.1命名空间(可嵌套使用)
为什么提出命名空间?
C语言中没有,在公司A与公司B之间进行合作时,很大概率上要发生命名的冲突(变量名相同,函数名相同,或者struct相同)
C语言解决问题的方式是:对于所有变量或者函数在命名时会加上公司信。因此C++中提出了命名空间namespace
命名空间的使用方式
1.使用作用域限定符:: 最安全的写法 namespace::func(); namespace::variable;
2.using编译指令 using namespace std; using编译指令它会把该空间中的所有实体一次性全部引入
3.using声明机制 using声明机制的作用域是从using语句开始,到using所在的作用域结束。要注意,在同一作用域内用using声明的不同的命名空间的成员不能有同名的成员,否则会发生重定义。
匿名命名空间
C语言没有命名空间 =》 C是C++的子集,兼容C语言
什么叫模块? 一个*.c/.cc/.cpp的文件就可以称为一个模块
只能在本模块内部使用的是什么?不能跨模块使用的是什么?
C语言中,static变量 static函数都只能在本模块内部使用,外部无法引用
C++中,匿名命名空间的实体也是只能在本模块内部使用
全局变量可以跨模块调用
匿名命名空间的实体无法跨模块调用,只能在本模块内部使用,功能与static变量是相同的
命名空间是否只可以定义一次?
同一个模块中,可以定义多次命名空间;在不同的模块中,也可以定义多次命名空间.命名空间就像一个容器(黑洞),可以无限定义实体
访问同名的实体,遵循就近原则
1.2const关键字
const关键字修饰变量:const关键字修饰的变量称为常量,常量必须要进行初始化
const关键字修饰指针
int number1 = 10;
int number2 = 20;
const int * p1 = &number1;//常量指针,Pointer to const
*p1 = 100;//error 通过p1指针无法修改其所指内容的值
p1 = &numbers;//ok 可以改变p1指针的指向
int const * p2 = &number1; //常量指针的第二种写法
int * const p3 = &number1;//指针常量,const pointer
*p3 = 100;//ok 通过p3指针可以修改其所指内容的值
p3 = &number2;//error 不可以改变p1指针的指向
const int * const p4 = &number1;//两者皆不能进行修改
const关键字修饰成员函数
const关键字修饰对象
常考题:const常量与宏定义的区别是什么?
1)编译器处理方式不同。宏定义是在预处理阶段展开,做字符串的替换;而const常量是在编译时。
2)类型和安全检查不同。宏定义没有类型,不做任何类型检查;const常量有具体的类型,在编译期会执行类型检查。
1.3 new/delete表达式
在C中用来开辟和回收堆空间的方式是采用malloc/free库函数,在C++中提供了新的开辟和回收堆空间的方式,即采用new/delete表达式。
1. 开辟一个元素的空间
int *p = new int(1);//new表达式申请空间的同时,也进行了初始化
cout << *p << endl;
delete p;
2. 开辟一个数组的空间
int *p = new int[10]();//开辟数组时,要记得采用[]
for(int idx = 0; idx != 10; ++idx)
{
p[idx] = idx;
}
delete [] p;//回收时,也要采用[]
常考题:new/delete表达式与malloc/free的区别是?
1)malloc/free是C/C++语言的标准库函数,new/delete是C++的运算符或表达式 ;
2)new能够自动分配空间大小,malloc需要传入参数;
3)new开辟空间的同时还对空间做了初始化的操作,而malloc不行;
4)new/delete能对对象进行构造和析构函数的调用,进而对内存进行更加详细的工作,而malloc/free不能。
既然new/delete的功能完全覆盖了malloc/free,为什么C++还保留malloc/free呢?
因为C++程序经常要调用C函数,而C程序只能用malloc/free管理动态内存。
内存泄漏检测工具valgrind
由于他的命令比较长,因此起一个别名,放在
$ vim ~/.bashrc 文件的末尾即可
1.4 引用
- **什么是引用?**在C++中,引用是一个已定义变量的别名。
类型 &引用名 = 目标变量名;
void test0()
{
int a = 1;
int &ref1 = a;
int &ref2;//不能独立存在
}
在使用引用的过程中,要注意以下几点:
3. &在这里不再是取地址符号,而是引用符号,相当于&有了第二种用法
4. 引用的类型必须和其绑定的变量的类型相同
5. 声明引用的同时,必须对引用进行初始化;否则编译时报错
6. 一旦绑定到某个变量之后,就不会再改变其指向
2. 引用的本质
C++中的引用本质上是一种被限制的指针。所以引用是占据内存的,占据的大小就是一个指针的大小.
引用变量会占据存储空间,存放的是一个地址,但是编译器阻止对它本身的任何访问,从一而终总是指向初始的目标单元。在汇编里, 引用的本质就是“间接寻址”。
3. 引用作为函数参数
在很多场合下就可以用引用代替指针,因而也具有更好的可读性和实用性。这就是引用存在的意义。
//用指针作为参数
void swap(int *pa, int *pb)
{
int temp = *pa;
*pa = *pb;
*pb = temp;
}
//引用作为参数
void swap(int &x, int &y)
{
int temp = x;
x = y;
y = temp;
}
在C++中推荐使用引用而非指针作为函数的参数
4. 引用作为函数的返回值
//语法:
类型 &函数名(形参列表)
{
//函数体
}
当以引用作为函数的返回值时,返回的变量其生命周期一定是要大于函数的生命周期的,即当函数执行完毕时,返回的变量还存在。
当引用作为函数的返回值时,必须遵守以下规则:
7. 不能返回局部变量的引用。主要原因是局部变量会在函数返回后被销毁,因此被返回的引用就成为了"无所指"的引用,程序会进入未知状态。
8. 不能在函数内部返回new分配的堆空间变量的引用。如果返回的引用只是作为一个临时变量出现,而没有被赋予一个实际的变量,那么该引用所在的空间就无法释放,会造成内存泄漏。
引用总结:
9. 在引用的使用中,单纯给某个变量取个别名是毫无意义的,引用的目的主要用于在函数参数传递中,解决大块数据或对象的传递效率和空间不如意的问题。
10. 用引用传递函数的参数,能保证参数传递中不产生副本,提高传递的效率,且通过const的使用,保证了引用传递的安全性。
11. 引用与指针的区别是,指针通过某个指针变量指向一个变量后,对它所指向的变量间接操作。程序中使用指针,程序的可读性差;而引用本身就是目标变量的别名,对引用的操作就是对目标变量的操作
1.5 c++强制类型转换
c风格的类型转换的缺点,因为它可以在任意类型之间转换,比如你可以把一个指向const对象的指针转换成指向非const对象的指针,把一个指向基类对象的指针转换成指向一个派生类对象的指针,这两种转换之间的差别是巨大的,但是传统的c语言风格的类型转换没有区分这些。另一个缺点就是,c风格的转换不容易查找,它由一个括号加上一个标识符组成,而这样的东西在c++程序里一大堆。
c++为了克服这些缺点,引进了4个新的类型转换操作符,他们是static_cast,const_cast,dynamic_cast,reinterpret_cast
1. static_cast最常用的类型转换符
常用于void*转为其它类型的指针
int iNumber = 100;
float fNumber = 0;
fNumber = (float) iNumber;//C风格
fNumber = static_cast<float>(iNumber);//将一种数据类型转换成另一种数据类型
void *pVoid = malloc(sizeof(int));
int *pInt = static_cast<int*>(pVoid);//指针之间的转换
*pInt = 1;
int iNumber = 1;
int *pInt = &iNumber;
float *pFloat = static_cast<float *>(pInt);//error,但不能完成任意两个指针类型间的转换
总结,static_cast的用法主要有以下几种:
1)用于基本数据类型之间的转换,如把int转成char,把int转成enum。这种转换的安全性也要开发人员来保证。
2)把void指针转换成目标类型的指针,但不安全。
3)把任何类型的表达式转换成void类型。
4)用于类层次结构中基类和派生类之间指针或引用的转换。进行上行转换(把派生类的指针或引用转换成基类指针或引用)是安全的;进行下行转换(把基类指针或引用转换成派生类指针或引用)时,由于没有动态类型检查,所以是不安全的。
2. const_cast去除常量属性
该运算符用来修改类型的const属性。常量指针被转化成非常量指针,并且仍然指向原来的对象;常量引用被转换成非常量引用,并且仍然指向原来的对象;常量对象被转换成非常量对象。
3. dynamic_cast
主要用于多态时基类和派生类间的转换,尤其是向下转型的用法中。
4. reinterpret_cast不轻易使用
用来处理无关类型之间的转换,即用在任意指针(或引用)类型之间的转换,以及指针与足够大的整数类型之间的转换。由此可以看出,reinterpret_cast的效果很强大,但错误的使用reinterpret_cast很容易导致程序的不安全,只有将转换后的类型值转换回到其原始类型,这样才是正确使用reinterpret_cast方式
1.6 函数重载(overload)
C++ 允许多个函数拥有相同的名字,只要它们的参数列表不同就可以,这就是函数重载(Function Overloading)。借助重载,一个
实现原理:名字改编(name mangling)
具体步骤:当函数名相同时,会根据函数参数的类型、个数、顺序不同进行改编。
函数名可以有多种用途。
C++进行函数重载的实现原理叫名字改编(name mangling),具体的规则是:
- 函数名称必须相同 。
- 参数列表必须不同(参数的类型不同、个数不同、顺序不同)。
- 函数的返回类型可以相同也可以不相同。
- 仅仅返回类型不同不足以成为函数的重载
1.7 默认参数
1. 默认参数的目的
void func1(int x = 0, int y = 0);
这样调用时,若不给参数传递实参,则func1函数会按指定的默认值进行工作。允许函数设置默认参数值,是为了让编程简单,让编译器做更多的检查错误工作。
2. 默认参数的声明
一般默认参数在函数声明中提供。当一个函数既有声明又有定义时,只需要在其中一个中设置默认值即可。若在定义时而不是在声明时置默认值,那么函数定义一定要在函数的调用之前。因为声明时已经给编译器一个该函数的向导,所以只在定义时设默认值时,编译器只有检查到定义时才知道函数使用了默认值。若先调用后定义,在调用时编译器并不知道哪个参数设了默认值。所以我们通常是将默认值的设置放在声明中而不是定义中。
3. 默认参数的顺序规定
如果一个函数中有多个默认参数,则形参分布中,默认参数应从右至左逐渐定义。当调用函数时,只能向左匹配参数
若给某一参数设置了默认值,那么在参数表中其后所有的参数都必须也设置默认值,否则,由于函数调用时可不列出已设置默认值的参数,编译器无法判断在调用时是否有参数遗漏
4. 默认参数与函数重载
如果一组重载函数(可能带有默认参数)都允许相同实参个数的调用,将会引起调用的二义性。所以在函数重载时,要谨慎使用默认参数。
void func4(int);
void func4(int x, int y = 0);
void func4(int x = 0, int y = 0);
1.8 bool类型
在C++中,还添加了一种基本类型,就是bool类型,用来表示true和false。true和false是字面值,可以通过转换变为int类型,true为1,false为0.
任何数字或指针值都可以隐式转换为bool值。
任何非零值都将转换为true,而零值转换为false.
一个bool类型的数据占据的内存空间大小为1.
1.9 inline函数
在C语言中,我们使用带参数的宏定义这种借助编译器的优化技术来减少程序的执行时间,那么在C++中用内联(inline)函数。内联函数作为编译器优化手段的一种技术,在降低运行时间上非常有用。
1. 什么是内联函数?
内联函数是C++的增强特性之一,用来降低程序的运行时间。当内联函数收到编译器的指示时,即可发生内联:编译器将使用函数的定义体来替代函数调用语句,这种替代行为发生在编译阶段而非程序运行阶段。
定义函数时,在函数的最前面以关键字“inline”声明函数,即可使函数称为内联声明函数。
2. 内联函数和带参数的宏定义
使用宏代码最大的缺点是容易出错,预处理器在拷贝宏代码时常常产生意想不到的边际效应。
宏的另一个缺点就是不可调试,但是内联函数是可以调试的。
对于C++而言,使用宏代码还有另一种缺点:无法操作类的私有数据成员。
内联函数的另一个优点就是:函数被内联后,编译器就可以通过上下文相关的优化技术对结果代码执行更深入的优化,而这种优化在普通函数体内是无法单独进
3. 将内联函数放入头文件
关键字 inline 必须与函数定义体放在一起才能使函数成为内联,仅将 inline 放在函数声明前面不起任何作用。
inline void bar(int x, in y)//该语句在头文件中
{
//...
}
C++ inline函数是一种“用于实现的关键字”,而不是一种“用于声明的关键字”。
内联函数应该在头文件中定义,这一点不同于其他函数。编译器在调用点内联展开函数的代码时,必须能够找到 inline函数的定义才能将调用函数替换为函数代码,而对于在头文件中仅有函数声明是不够的。
当然内联函数定义也可以放在源文件中,但此时只有定义的那个源文件可以用它,而且必须为每个源文件拷贝一份定义(即每个源文件里的定义必须是完全相同的),当然即使是放在头文件中,也是对每个定义做一份拷贝,只不过是编译器替你完成这种拷贝罢了。但相比于放在源文件中,放在头文件中既能够确保调用函数是定义是相同的,又能够保证在调用点能够找到函数定义从而完成内联(替换)。
2.程序内存分配方式
2.1程序内存布局
应用程序都运行在一个虚拟内存空间里,以32位系统为例,其寻址空间为4G,Linux默认将高地址的1G空间分配给内核,用户使用剩下的3G空间成为用户态空间,用户态空间一般有如下默认区域:
- 栈区(stack):由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
- 堆区(heap):一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。
- 全局/静态区(static):全局变量和静态变量的存储是放在一块的,在程序编译时分配。
- 文字常量区:存放常量字符串。
- 程序代码区:存放函数体(类的成员函数、全局函数)的二进制代码
虚拟内存空间示意图如下:
2.2栈与堆的比较
申请后系统的响应
栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
堆:操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。另外,对于大多数系统,首地址处会记录这块内存空间中本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。
申请效率的比较:
栈由系统自动分配,速度较快。但程序员无法控制。
堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便。
申请大小的限制
栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。
堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。由此可见,堆获得的空间比堆的大小受限于计算机系统中有效的虚拟内存较灵活,也比较大。
堆和栈中的存储内容
栈: 在函数调用时,第一个进栈的是主函数的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。
堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容由程序员安排。
栈与堆的区别
栈与堆的区别在一下六个方面有所不同:
- 管理方式不同。对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生memory leak.
- 空间大小不同。一般来讲在32位系统下,内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的,例如,在VS下,默认的栈空间大小是1M
- 分配方式。内存有2种分配方式:静态分配和动态分配。堆都是动态分配的,没有静态分配的堆。静态分配是编译器完成的,比如局部变量的分配。动态分配由malloc, calloc函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。
- 生长方向。对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。
- 碎片问题。对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在他弹出之前,在它上面的后进的栈内容已经被弹出。
3.类与对象
3.1、C++中类的定义
class 类名
{
public:
//公有数据成员和成员函数
protected:
//保护数据成员和成员函数
private:
//私有数据成员和成员函数
}; // 千万不要忘了这个分号
class内部可以拥有的是数据成员(属性)和成员函数(行为),他们可以分别用三个不同的关键字进行修饰,public、protected、private. 其中public进行修饰的成员表示的是该类可以提供的接口、功能、或者服务;protected进行修饰的成员,其访问权限是开放给其子类;private进行修饰的成员是不可以在类之外进行访问的,只能在类内部访问,可以说封装性就是由该关键字来体现。
在类中定义的成员函数,都是inline函数。除了可以在类内部实现外,成员函数还可以在类之外实现。在类定义的外部定义成员函数时,应使用作用域限定符(::)来标识函数所属的类,即有如下形式:
返回类型 类名::成员函数名(参数列表)
{
//....
}
3.2、class与struct的区别
在C++中,与C相比,struct的功能已经进行了扩展。class能做的事儿,struct一样能做,他们之间唯一的区别,就是默认访问权限不同。class的默认访问权限是private,struct的默认访问权限是public
3.3、对象的创建
1.C++中,对象的创建会调用一个特殊的成员函数->构造函数
2.构造函数的作用:就是用来初始化数据成员的
3.形式:
没有返回值,即使是void也不能有,与类名相同,再加上函数参数列表
注:
1.当类中没有显式定义构造函数时,系统会自动提供一个默认(无参)构造函数;
2.一旦当类中显式提供了有参构造函数时,系统就不会再自动提供一个默认(无参)构造函数
3.如果还希望通过默认构造函数创建对象,则必须要手动提供一个默认构造函数
4.构造函数可以重载
3.4 初始化表达式
1.在C++中,对于类中数据成员的初始化,需要使用初始化列表完成
2.初始化列表的形式:
在构造函数的头与构造函数体之间的位置,
开始用冒号进行分隔,
如果有多个数据成员需要初始化,则用逗号分隔
class Point
{
public:
//...
Point(int ix = 0, int iy = 0)
: _ix(ix)
, _iy(iy)
{
cout << "Point(int = 0,int = 0)" << endl;
}
//...
};
如果没有在构造函数的初始化列表中显式地初始化成员,则该成员将在构造函数体之前执行默认初始化。如在“对象的创建”部分的两个构造函数中的_ix和_iy都是先执行默认初始化后,再在函数体中执行赋值操作。可能有同学会觉得在初始化列表中进行成员初始化不习惯,但有些时候成员必须在初始化列表中进行,否则会出现编译报错。
注意:每个成员在初始化列表之中只能出现一次,其初始化的顺序不是由成员变量在初始化列表中的顺序决定的,而是由成员变量在类中被声明时的顺序决定的。(举例说明)
class Foo
{
public:
Foo(int a)
: _iy(a) //在初始化列表中, _iy好像先被初始化
, _ix(_iy)
{
cout << "Foo(int)" << endl;
}
private:
int _ix; //在声明时,_ix在前
int _iy;
};
3.5 对象的销毁
1.析构函数:对象在销毁时,一定会调用析构函数
2.析构函数的作用:清理类对象成员申请的资源(堆空间)
3.形式:
没有返回值,即使是void也没有
没有参数
函数名与类名相同,在类名之前需要加上一个波浪号~
~类名() {} 析构函数只有一个
4. 析构函数默认情况下,系统也会自动提供一个
5. 当对象被销毁时,会自动调用析构函数【非常重要】
析构函数调用时机
- 栈对象生命周期结束时,会自动调用析构函数
- 全局对象在main函数退出时,会自动调用析构函数
- 静态对象在main函数退出时,会自动调用析构函数
- 堆对象当执行delete表达式时,会自动调用析构函数
析构函数的调用与本对象的销毁是一回事儿么?
对象销毁时,一定会自动调用析构函数,但调用析构函数,不一定就是销毁对象
析构函数除了在对象被销毁时自动调用外,还可以显式手动调用,但一般不建议这样使用。
3.6 拷贝构造函数
涉及到对象的创建,就必然需要调用构造函数,而这里会调用的就是复制构造函数,又称为拷贝构造函数。
如果类中没有显式定义拷贝构造函数时,编译器会自动提供一个缺省的拷贝构造函数。
拷贝构造函数的调用时机
- 当用一个已经存在的对象初始化另一个新对象时。
- 当实参和形参都是对象,进行实参与形参的结合时。
- 当函数的返回值是对象,函数调用完成return时。(优化选项-fno-elide-constructors)
拷贝构造函数的参数形式可以改变吗?(也就是引用符号可以去掉吗?const关键字可以去掉吗?)
右值:无法取地址。左值:能取地址。
3.7 隐含的this指针
类中定义的非静态成员函数中都有一个隐含的this指针,它代表的就是当前对象本身,它作为成员函数的第一个参数,由编译器自动补全.
- 对象调用函数时,是如何找到自己本对象的数据成员的?this指针
- 类中定义的成员函数存放在什么位置? 程序代码区
- this指针代表的是当前对象
- this指针在什么位置? 作为成员函数第一个隐含的参数
- this指针的形式是什么? 类名 * const this 保护this不会被修改指向
对于类成员函数而言,并不是一个对象对应一个单独的成员函数体,而是此类的所有对象共用这个成员函数体。 当程序被编译之后,此成员函数地址即已确定。而成员函数之所以能把属于此类的各个对象的数据区别开, 就是靠这个this指针。函数体内所有对类数据成员的访问,都会被转化为this->数据成员的方式。
3.8 赋值运算符函数
对象已经创建 --> 调用赋值运算符函数
- 形式: 类名 & operator=(const 类名 &); //双目运算符
- 如果类中没有显式定义赋值运算符函数时,编译器会自动提供一个缺省的赋值运算符函数。
- 深拷贝4部曲四步曲:0. 考虑自复制1. c2对象要先回收原来的申请空间2. 进行深拷贝3. 返回*this
注意: 赋值运算符函数的返回值类型可以改变吗?(返回类型必须是类类型吗?返回值一定要是引用吗?)
3.9 特殊数据成员的初始化
常量数据成员
当数据成员用const关键字进行修饰以后,就成为常量成员。一经初始化,该数据成员便具有“只读属性”,在程序中无法对其值修改。事实上,在构造函数体内初始化const数据成员是非法的,它们只能在构造函数初始化列表中进行初始化。
引用数据成员
和常量成员相同,引用成员也必须在构造函数初始化列表中进行初始化,否则编译报错。
类对象成员
当数据成员本身也是自定义类类型对象时,比如一个直线类Line对象中包含两个Point类对象,对Point对象的创建就必须要放在Line的构造函数的初始化列表中进行。
静态数据成员
C++允许使用static(静态存储)修饰数据成员,这样的成员在编译时就被创建并初始化的(与之相比,对象是在运行时被创建的),且其实例只有一个,被所有该类的对象共享,当程序执行时,该成员已经存在,一直到程序结束,任何该类对象都可对其进行访问,静态数据成员存储在全局/静态区,并不占据对象的存储空间。
因为静态数据成员不属于类的任何一个对象,所以它们并不是在创建类对象时被定义的。这意味着它们不是由类的的构造函数初始化的,一般来说,**我们不能在类的内部初始化静态数据成员,必须在类的外部定义和初始化静态数据成员,且不再包含static关键字.**格式如下:
类型 类名::变量名 = 初始化表达式; //普通变量
类型 类名::对象名(构造参数); //对象变量
3.10 特殊的成员函数
静态成员函数
在某一个成员函数的前面加上static关键字,不能是构造函数/析构函数/赋值运算符函数
特点:
A. 没有this指针的存在
B. 不能访问非静态的数据成员和成员函数
C.只能访问静态的数据成员和成员函数
D. 静态成员函数可以直接通过类名调用,不需要对象存在
const成员函数
把const关键字放在函数的参数表和函数体之间,void print() const;
特点:
- 只能读取类数据成员,而不能修改之。
- 只能调用const成员函数,不能调用非const成员函数。
- this指针被双重限制,原本Point *const this --> const Point *this
3.11 单例模式
由类到对象,称为类的实例化
- 需求:一个类只能生成一个对象,且是唯一的对象
解决方案:- 将构造函数私有化
- 在类中定义一个静态的指向本类型的指针变量
- 定义一个返回值为类指针的静态成员函数
- 应用场景:
A. 用单例模式替换全局变量
B. 读取配置文件(程序的输入)
C. 词典、网页库
代码
4字符串
4.1 c风格字符串
4.2 C++风格字符串
- std::string 标准库提供的一个自定义类类型
basic_string --> 模板
API ==> 接口(开发)文档
字符串的长度和容量相关
bool empty() const;
size_type size() const;
size_type length() const;
size_type capacity() const;
size_type max_size() const;
元素追加和相加
string &operator+=(const string & tr);
string &operator+=(CharT ch);
string &operator+=(const CharT* s);
string &append(size_type count, CharT ch);
string &append(const basic_string & str);
string &append(const CharT* s);
string &append(InputIt first, InputIt last);
//以下为非成员函数
string operator+(const string & lhs, const string & rhs);
string operator+(const string & lhs, const char* rhs);
string operator+(const char* lhs, const string & rhs);
string operator+(const string & lhs, char rhs);
string operator+(char lhs, const string & rhs);
元素删除
iterator erase(iterator position);
iterator erase(const_iterator position);
iterator erase(iterator first, iterator last);
元素清空
void clear();
字符串比较
//非成员函数
bool operator==(const string & lhs, const string & rhs);
bool operator!=(const string & lhs, const string & rhs);
bool operator>(const string & lhs, const string & rhs);
bool operator<(const string & lhs, const string & rhs);
bool operator>=(const string & lhs, const string & rhs);
bool operator<=(const string & lhs, const string & rhs);
搜索与查找
//find系列:
size_type find(const basic_string & str, size_type pos = 0) const;
size_type find(const CharT* s, size_type pos = 0) const;
size_type find(const CharT* s, size_type pos, size_type count) const;
size_type find(char ch, size_type pos = npos ) const;
//rfind系列:
size_type rfind(const basic_string & str, size_type pos = 0) const;
size_type rfind(const CharT* s, size_type pos = 0) const;
size_type rfind(const CharT* s, size_type pos, size_type count) const;
size_type rfind(char ch, size_type pos = npos) const;
自定义string实现,深拷贝
4.3 动态数组vector
5 new和delete表达式
5.1 new表达式工作步骤
5.2 delete表达式工作步骤
5.3 operator new和operator delete函数的重载版本
//operator new库函数
void *operator new(size_t);
void *operator new[](size_t);
//operator delete库函数
void operator delete(void *);
void operator delete[](void *);
5.4 要求一个类只能创建栈对象
创建栈对象需要的条件
1. 必须提供合法的构造函数
2. 必须提供一个合法的析构函数
只需要将operator new放入Student类的private区域
5.5 要求一个类只能创建堆对象
创建堆对象需要的条件
1. 需要合法的operator new库函数
2. 需要合法的构造函数
只需要将Student类的析构函数放入private区域。
6 C++输入输出流
6.1 输入输出的含义
从程序(进程)的角度来看,数据从其他地方传送到程序(内存)叫输入,数据从程序(内存)传送到其他地方叫输出。
6.2 C++输入输出机制
6.2.1 c++流的机制
6.2.2 标准输入输出流
6.2.3流的状态
6.2.4 流类型之间的关系
6.2.5 流的通用操作
//----以下输入流操作----
int_type get();//读取一个字符
istream & get(char_type & ch);
//读取一行数据
istream & getline(char_type * s, std::streamsize count, char_type delim='\n');
//读取count个字节的数据
istream & read(char_type * s, std::streamsize count);
//最多获取count个字节,返回值为实际获取的字节数
std::streamsize readsome(char_type * s, std::streamsize count);
//读取到前count个字符或在读这count个字符进程中遇到delim字符就停止,并把读取的这些东西丢掉
istream & ignore(std::streamsize count = 1, int_type delim = Traits::eof());
//查看输入流中的下一个字符, 但是并不将该字符从输入流中取走
//不会跳过输入流中的空格、回车符; 在输入流已经结束的情况下,返回 EOF。
int_type peek();
//获取当前流中游标所在的位置
pos_type tellg();
//偏移游标的位置
basic_istream & seekg(pos_type pos);
basic_istream & seekg(off_type off, std::ios::seekdir dir);
//----以下为输出流操作----
//往输出流中写入一个字符
ostream & put(char_type ch);
//往输出流中写入count个字符
ostream & write(const char_type * s, std::streamsize count);
//获取当前流中游标所在的位置
pos_type tellp();
//刷新缓冲区
ostream & flush();
//偏移游标的位置
ostream & seekp(pos_type pos);
ostream & seekp(off_type off, std::ios_base::seekdir dir);
6.3缓冲区
缓冲区就是一块内存区,它用在输入输出设备和CPU之间,用来缓存数据。它使得低速的输入输出设备和高速的CPU能够协调工作,避免低速的输入输出设备占用CPU,解放出CPU,使其能够高效率工作.
6.3.1 缓冲区的类型
- 全缓冲区: 只有当缓冲区满的时候,才会执行刷新操作
- 行缓冲区:碰到换行符的时候,进行刷新
- 非缓冲区:不带缓冲区,有多少数据就刷新多少
A. cout一般情况下,输入其中的数据,如果没有换行符,不会立刻刷新缓冲区;
B. cout碰到换行符时,立刻刷新缓冲区
C. cout当缓冲区满的时候,也会立刻刷新缓冲区
cout缓冲区默认1024个字节
6.4 文件IO
- 文件输入流 --> basic_ifstream --> ifstream
- 文件输出流 --> basic_ofstream --> ofstream
- 文件输入输出流 --> basic_fstream --> fstream
文件输入流ifstream
每次获取一行数据
文件输出流ofstream
动态查看某一个文件的内容
文件游标定位信息
写入操作
6.4 字符串IO
- istringstream 字符串输入流, 将字符串转换为其他类型
- ostringstream 字符串输出流, 将其他类型转换为字符串
- stringstream
istringstream
ostringstream
7.日志系统
日志系统的设计
8.运算符重载
8.1友元
类的私有成员只能在类的内部访问,类之外是不能访问它们的。但如果将其他类或函数设置为类的友元(friend),就可以访问了。
友元的形式可以分为友元函数和友元类。
class 类名
{
//...
friend 函数原型;
friend class 类名;
//...
}
友元函数之普通函数
友元函数之成员函数
友元之友元类
友元是单向的、不具备传递性、不能被继承
注意:友元的声明是不受public/protected/private关键字限制的。
8.2运算符重载
8.2.1为什么需要对运算符进行重载?
C++预定义中的运算符的操作对象只局限于基本的内置数据类型,但是对于我们自定义的类型是没有办法操作的。
运算符重载的实质就是函数重载或函数多态。运算符重载是一种形式的 C++ 多态。目的在于让人能够用同名的函数来完成不同的基本操作。
返回类型 operator 运算符(参数表)
{
//...
}
8.2.2运算符重载的规则
并不是所有的运算符都可以重载。可以重载的运算符有:
不可以重载的运算符5个: 成员访问运算符 . 成员指针运算符 . 三目运算符 ?: 作用域限定符 :: sizeof*
运算符重载还具有以下规则:
为了防止用户对标准类型进行运算符重载,C++规定重载的运算符的操作对象必须至少有一个是自定义类型或枚举类型
重载运算符之后,其优先级和结合性还是固定不变的。
重载不会改变运算符的用法,原来有几个操作数、操作数在左边还是在右边,这些都不会改变。
重载运算符函数不能有默认参数,否则就改变了运算符操作数的个数。
重载逻辑运算符(&&,||)后,不再具备短路求值特性。
不能臆造一个并不存在的运算符,如@、$等
8.2.3运算符重载的形式
以普通函数的形式
以成员函数的形式
友元函数的形式
8.3特殊运算符的重载
8.3.1复合赋值运算符
复合赋值运算符推荐以成员函数的形式进行重载,包括这些(+=,-=,*=,/=,%=,<<=,>>=,&=,^=,|=),因为对象本身会发生变化。
8.3.2自增自减运算符
自增运算符++和自减运算符–推荐以成员函数形式重载。
C++根据参数的个数来区分前置和后置形式。如果按照通常的方法(成员函数不带参数)来重载++/–运算符,那么重载的就是前置版本。要对后置形式进行重载,就必须为重载函数再增加一个int类型的参数,该参数仅仅用来告诉编译器这是一个运算符后置形式,在实际调用时不需要传递实参。
8.3.3赋值运算符
赋值运算符=,只能以成员函数形式进行重载
8.3.4函数调用运算符
一个普通函数执行完毕,它所在的函数栈空间就会被销毁,所以普通函数执行时的状态信息,是无法保存下来的,这就让它无法应用在那些需要对每次的执行状态信息进行维护的场景。有了对象的存在,对象执行某些操作之后,只要对象没有销毁,其状态就是可以保留下来的,但在函数作为参数传递时,会有障碍。为了解决这个问题,C++引入了函数调用运算符。函数调用运算符的重载形式只能是成员函数形式
一个类如果重载了函数调用operator(),就可以将该类对象作为一个函数使用。对于这种重载了函数调用运算符的类创建的对象,我们称为函数对象(Function Object)。
8.3.5下标访问运算符
下标访问运算符[]通常用于访问数组元素,它是一个二元运算符,如arr[idx]可以理解成arr是左操作数,idx是右操作数。对下标访问运算符进行重载时,只能以成员函数形式进行,如果从函数的观点来看,语句arr[idx];可以解释为arr.operator;
8.3.6成员访问运算符
成员访问运算符包括箭头访问运算符->和解引用运算符*,我们先来看箭头运算符->.箭头运算符只能以成员函数的形式重载,其返回值必须是一个指针或者重载了箭头运算符的对象。
8.3.7输入输出流运算符
由于非静态成员函数的第一个参数是隐含的this指针,代表当前对象本身,这与其要求是冲突的,因此>>和<<不能重载为成员函数,只能是非成员函数,如果涉及到要对类中私有成员进行访问,还得将非成员函数设置为类的友元函数。
对于流对象而言,是不能进行拷贝操作的,或者说拷贝构造函数已经被删除了
总结:对于运算符重载时采用的形式的建议:
所有的一元运算符,建议以成员函数重载
运算符 = () [] -> ->* ,必须以成员函数重载
运算符 += -= /= *= %= ^= &= != >>= <<= 建议以成员函数形式重载
其它二元运算符,建议以非成员函数重载
8.4类型转换
由其他类型向自定义类型转换
由其他类型向定义类型转换是由构造函数来实现的,只有当类中定义了合适的构造函数时,转换才能通过。这种转换,一般称为隐式转换
禁止隐式转换只需要在相应构造函数前面加上explicit关键字。
由自定义类型向其他类型转换
由自定义类型向其他类型的转换是由类型转换函数完成的,这是一个特殊的成员函数
9类作用域
作用域可以分为类作用域、类名的作用域以及对象的作用域
9.1全局作用域
在函数和其他类定义的外部定义的类称为全局类,绝大多数的 C++ 类是定义在该作用域中,我们在前面定义的所有类都是在全局作用域中,全局类具有全局作用域。
9.2类作用域
一个类可以定义在另一类的定义中,这是所谓嵌套类或者内部类.
如果变量的名字不相同,此时变量的可见域与变量的作用域是等同的;但是如果变量名字相同的时候,变量的可见域是小于变量的作用域的。
内部类:将一个类A的定义写到另外一个类B中,此时类A就是类B的内部类。
9.2.1类作用域设计模式之Pimpl
PIMPL(Private Implementation 或Pointer to Implementation)是通过一个私有的成员指针,将指针所指向的类的内部实现数据进行隐藏。PIMPL又称作“编译防火墙”,它的实现中就用到了嵌套类。PIMPL设计模式有如下优点:
- 提高编译速度;
- 实现信息隐藏;
- 减小编译依赖,可以用最小的代价平滑的升级库文件;
- 接口与实现进行解耦;
- 移动语义友好。
9.2.2单例模式的自动释放的四种方法
1、友元类
2、内部类 + 静态数据成员
3、饿汉模式 + atexit
4、pthread_once + atexit
9.3块作用域
类的定义在代码块中,这是所谓局部类,该类完全被块包含,其作用域仅仅限于定义所在块,不能在块外使用类名声明该类的对象。
10 标准库中string的底层实现方式
有三种方式:
10.1 Eager Copy(深拷贝)
这种实现方式,在需要对字符串进行频繁复制而又并不改变字符串内容时,效率比较低下。所以需要对其实现进行优化,之后便出现了下面的COW的实现方式。
10.2 COW(Copy-On-Write 写时复制
当两个std::string发生复制构造或者赋值时,不会复制字符串内容,而是增加一个引用计数,然后字符串指针进行浅拷贝,其执行效率为O(1)。只有当需要修改其中一个字符串内容时,才执行真正的复制。其实现的示意图,有下面两种形式:
//第一种
class string
{
private:
Allocator _allocator;
size_t size;
size_t capacity;
char * pointer;
};
//第二种
class string
{
private:
char * _pointer;
};
当执行复制构造或赋值时,引用计数加1,std::string对象共享字符串内容;当std::string对象销毁时,并不直接释放字符串所在的空间,而是先将引用计数减1,直到引用计数为0时,则真正释放字符串内容所在的空间。
写时复制的关键在于引用计数的存放
写时复制区分读写
10.3 SSO(Short String Optimization-短字符串优化)
一个程序里用到的字符串大部分都很短小,而在64位机器上,一个char*指针就占用了8个字节,所以SSO就出现了,其核心思想是:发生拷贝时要复制一个指针,对小字符串来说,为啥不直接复制整个字符串呢,说不定还没有复制一个指针的代价大。
class string
{
union Buffer
{
char * _pointer;
char _local[16];
};
Buffer _buffer;
size_t _size;
size_t _capacity;
};
sso:短字符串优化,当字符串的长度小于等于15字节的时候,会存在栈上;当字符串的长度大于等于16字节的时候,存储在堆上。
10.4 最佳策略
以上三种方式,都不能解决所有可能遇到的字符串的情况,各有所长,又各有缺陷。综合考虑所有情况之后,facebook开源的folly库中,实现了一个fbstring, 它根据字符串的不同长度使用不同的拷贝策略,最终每个fbstring对象占据的空间大小都是24字节。
- 很短的(0~22)字符串用SSO,23字节表示字符串(包括’\0’),1字节表示长度
- 中等长度的(23~255)字符串用eager copy,8字节字符串指针,8字节size,8字节capacity.
- 很长的(大于255)字符串用COW, 8字节指针(字符串和引用计数),8字节size,8字节capacity.
10.5 线程安全性
11 面向对象之继承
通过继承,我们可以用原有类型来定义一个新类型,定义的新类型既包含了原有类型的成员,也能自己添加新的成员,而不用将原有类的内容重新书写一遍
11.1继承的定义
当一个派生类继承一个基类时,需要在派生类的类派生列表中明确的指出它是从哪个基类继承而来的。类派生列表的形式是在类名之后,大括号之前用冒号分隔,后面紧跟以逗号分隔的基类列表,其中每个基类前面可以有访问修饰限定符,其形式如下:
class 派生类
: public/protected/private 基类
{
};
派生类的生成过程包含3个步骤:
- 吸收基类的成员
- 改造基类的成员
- 添加自己新的成员
11.2继承的局限
不论何种继承方式,下面这些基类的特征是不能从基类继承下来的:
构造函数
析构函数
用户重载的operator new/delete运算符
用户重载的operator=运算符
友元关系
11.3派生方式对基类成员的访问权限
派生(继承)方式有3种,分别是
public(公有)继承
protected(保护型)继承
private(私有)继承
保护继承与私有继承的区别
保护继承基类中的非私有成员,在以后可以无限继承下去;但是私有继承基类中的非私有成员,不能再继续继承下去。
默认的继承方式是私有的。
总结:派生类的访问权限规则如下:
1.不管以什么继承方式,派生类内部都不能访问基类的私有成员。
2.不管以什么继承方式,派生类内部除了基类的私有成员不可以访问外,其他的都可以访问。
3.不管以什么继承方式,派生类对象除了公有继承基类中的公有成员可以访问外,其他的一律不能访问。
11.4派生类对象的构造
构造函数和析构函数是不能继承的,为了对数据成员进行初始化,派生类必须重新定义构造函数和析构函数。由于派生类对象通过继承而包含了基类数据成员,因此,创建派生类对象时,系统首先通过派生类的构造函数来调用基类的构造函数,完成基类成员的初始化,而后对派生类中新增的成员进行初始化。
派生类构造函数的一般格式为:
派生类名(总参数表)
: 基类构造函数(参数表)
{
//函数体
};
对于派生类对象的构造,我们分下面4种情况进行讨论:
- 如果派生类有显式定义构造函数,而基类没有显示定义构造函数,则创建派生类的对象时,派生类相应的构造函数会被自动调用,此时都自动调用了基类缺省的无参构造函数。
- 如果派生类没有显式定义构造函数而基类有显示定义构造函数,则基类必须拥有默认构造函数。
- 如果派生类有构造函数,基类有默认构造函数,则创建派生类的对象时,基类的默认构造函数会自动调用,如果你想调用基类的有参构造函数,必须要在派生类构造函数的初始化列表中显示调用基类的有参构造函数。
- 如果派生类和基类都有构造函数,但基类没有默认的无参构造函数,即基类的构造函数均带有参数,则派生类的每一个构造函数必须在其初始化列表中显示的去调用基类的某个带参的构造函数。如果派生类的初始化列表中没有显示调用则会出错,因为基类中没有默认的构造函数。
虽然上面细分了四种情况进行讨论,但不管如何,谨记一条: 必须将基类构造函数放在派生类构造函数的初试化列表中,以调用基类构造函数完成基类数据成员的初始化。派生类构造函数实现的功能,或者说调用顺序为: - 完成对象所占整块内存的开辟,由系统在调用构造函数时自动完成。
- 调用基类的构造函数完成基类成员的初始化。
- 若派生类中含对象成员、const成员或引用成员,则必须在初始化表中完成其初始化。
- 派生类构造函数体执行。
11.5派生类对象的销毁
当派生类对象被删除时,派生类的析构函数被执行。析构函数同样不能继承,因此,在执行派生类析构函数时,基类析构函数会被自动调用。执行顺序是先执行派生类的析构函数,再执行基类的析构函数,这和执行构造函数时的顺序正好相反。当考虑对象成员时,继承机制下析构函数的调用顺序:
- 先调用派生类的析构函数
- 再调用派生类中成员对象的析构函数
- 最后调用普通基类的析构函数
当派生类对象进行销毁的时候,派生类的析构函数会被自动调用(完成派生类自己新增数据成员清理操作),然后基类的析构函数会被自动调用(作用是,为了完成从基类这里吸收过来的数据成员的清理操作)
11.6多基继承(多基派生)
多重继承的定义形式如下:
class 派生类
: public/protected/private 基类1
, ...
, public/protected/private 基类N
{
};
11.6.1多基继承的派生类对象的构造和销毁
多基派生时,派生类的构造函数格式如(假设有N个基类):
派生类名(总参数表)
: 基类名1(参数表1)
, 基类名2(参数表2)
, ...
, 基类名N(参数表N)
{
//函数体
}
和前面所讲的单基派生类似,总参数表中包含了后面各个基类构造函数需要的参数。
多基继承和单基继承的派生类构造函数完成的任务和执行顺序并没有本质不同,唯一一点区别在于:首先要执行所有基类的构造函数,再执行派生类构造函数中初始化表达式的其他内容和构造函数体。各基类构造函数的执行顺序与其在初始化表中的顺序无关,而是由定义派生类时指定的基类顺序决定的。
析构函数的执行顺序同样是与构造函数的执行顺序相反。但在使用多基继承过程中,会产生两种二义性。
11.6.2 成员名冲突的二义性
一般来说,在派生类中对基类成员的访问应当具有唯一性,但在多基继承时,如果多个基类中存在同名成员的情况,造成编译器无从判断具体要访问的哪个基类中的成员,则称为对基类成员访问的二义性问题。
只需要在调用时,指明要调用的是某个基类的成员函数即可,即使用作用域限定符就可以解决该问题。
11.6.3 菱形继承的二义性问题
而另外一个就是菱形继承的问题了。多基派生中,如果在多条继承路径上有一个共同的基类,如下图所示,不难看出,在D类对象中,会有来自两条不同路径的共同基类(类A)的双重拷贝。出现这种问题时,我们的解决方案是采用虚拟继承。中间的类B、C虚拟继承自A,就可以解决了。
11.7 基类与派生类间的相互转换
类型适应”是指两种类型之间的关系,说A类适应B类是指A类的对象能直接用于B类对象所能应用的场合,从这种意义上讲,派生类适应于基类,派生类的对象适应于基类对象,派生类对象的指针和引用也适应于基类对象的指针和引用。
可以把派生类的对象赋值给基类的对象
可以把基类的引用绑定到派生类的对象
可以声明基类的指针指向派生类的对象 (向上转型)
也就是说如果函数的形参是基类对象或者基类对象的引用或者基类对象的指针类型,在进行函数调用时,相应的实参可以是派生类对象。
向上转型:派生类向基类进行转换。语法是支持的,并且都是安全的。
向下转型:基类向派生类进行转换。原本的语法是不支持,但是可以使用强制转换。
11.8 派生类对象间的复制控制
从前面的知识,我们知道,基类的拷贝构造函数和operator=运算符函数不能被派生类继承,那么在执行派生类对象间的复制操作时,就需要注意以下几种情况:
-
如果用户定义了基类的拷贝构造函数,而没有定义派生类的拷贝构造函数,那么在用一个派生类对象初始化新的派生类对象时,两对象间的派生类部分执行缺省的行为,而两对象间的基类部分执行用户定义的基类拷贝构造函数。
-
如果用户重载了基类的赋值运算符函数,而没有重载派生类的赋值运算符函数,那么在用一个派生类对象给另一个已经存在的派生类对象赋值时,两对象间的派生类部分执行缺省的赋值行为,而两对象间的基类部分执行用户定义的重载赋值函数。
-
如果用户定义了派生类的拷贝构造函数或者重载了派生类的对象赋值运算符=,则在用已有派生类对象初始化新的派生类对象时,或者在派生类对象间赋值时,将会执行用户定义的派生类的拷贝构造函数或者重载赋值函数,而不会再自动调用基类的拷贝构造函数和基类的重载对象赋值运算符,这时,通常需要用户在派生类的拷贝构造函数或者派生类的赋值函数中显式调用基类的拷贝构造或赋值运算符函数
12多态
12.1为什么要用多态?
基本概念
多态:对于同一种指令(警车鸣笛),针对不同的对象(警察、普通人、嫌疑犯),产生不一样的行为(追捕行动、该干什么干什么、逃跑)。
多态的类型
多态除了代码的复用性外,还可以解决项目中紧偶合的问题,提高程序的可扩展性。
C++支持两种多态性:静态多态和动态多态。
静态多态:包括函数重载、运算符重载、模板。发生的时机在编译的时候。
动态多态:发生的时机在运行时。虚函数
12.2虚函数的概念
在成员函数的前面加上virtual的函数称为虚函数。
// 类内部
class 类名
{
virtual 返回类型 函数名(参数表)
{
//...
}
};
//类之外
virtual 返回类型 类名::函数名(参数表)
{
//...
}
当派生类继承基类的时候,基类的虚函数会被派生类继承,该虚函数在派生类里面仍然是虚函数,即使在派生类中不加virtual关键字,该函数仍然是虚函数。
派生类重定义虚函数的时候,格式非常严格
1、函数名字要相同
2、函数的参数列表要完全相同(参数的个数、参数的类型、参数顺序)
3、函数的返回类型也必须相同
总结:除了函数的函数体可以不一样之外,其他的都必须一样
虚函数的函数体才有可能在运行时确定,其他的全部在编译时候就确定,包括函数的参数。
12.3虚函数的原理(动态多态的原理)
虚函数的实现是怎样的呢?简单来说,就是通过一张虚函数表(Virtual Fucntion Table)实现的。具体地讲,当类中定义了一个虚函数后,会在该类创建的对象的存储布局的开始位置多一个虚函数指针(vfptr),该虚函数指针指向了一张虚函数表,而该虚函数表就像一个数组,表中存放的就是各虚函数的入口地址。如下图
当一个基类中设有虚函数,而一个派生类继承了该基类,并对虚函数进行了重定义,我们称之为覆盖(override). 这里的覆盖指的是派生类的虚函数表中相应虚函数的入口地址被覆盖。
当基类定义了虚函数的时候,就会在基类的对象的存储布局前面多一个虚函数指针,该虚函数指针指向基类自己的虚函数表(也称为虚表),该虚表存放的是虚函数的入口地址;当派生类继承基类的时候,会吸收基类中的虚函数,此时派生类自己也会有自己的虚函数,有虚函数就会在派生类对象的存储布局前面产生一个虚函数指针,该虚函数指针会指向派生类自己的虚函数表,派生类自己的虚表存放是是自己的虚函数的入口地址,如果此时派生类重定义了该虚函数,就会用派生类自己的虚函数的地址去覆盖从基类吸收过来的虚函数的入口地址。
虚函数被激活的五个条件
1、基类要定义虚函数
2、派生类要重定义(重写、覆盖)该虚函数
3、创建派生类对象
4、用基类的指针(引用)指向(绑定)派生类的对象
5、用基类的指针(引用)调用虚函数
12.4哪些函数不能被设计为虚函数
1、普通函数(自由函数、全局函数)(非成员函数):虚函数必须是一个成员函数。
2、静态成员函数:从发生时机上看,静态函数调用时机在编译的时候,而虚函数要体现动态多态,发生时机在运行时候;静态成员函数没有this;
3、内联成员函数:发生时机在编译时候,而虚函数要体现动态多态,在运行的时候;如果将内联函数设置为虚函数,此时该内联函数已经失去内联的含义。
4、友元函数。如果友元函数本身是一个普通函数,就不能被设置为虚函数;但是如果友元函数本身是一个成员函数,它是可以被设置为虚函数的。
5、构造函数:发生的时机在编译的时候,而虚函数要体现动态多态,发生的时机在运行时候;如果将构造函数设置为函数,需要在虚表中存放虚函数的入口地址,需要通过虚函数指针找到虚表,而虚函数指针位于对象的内存布局的前面,而构造函数不调用,对象本身是不完整的(初始化还没有成功,在内存中的布局还不完整),就不知道虚函数指针是否存在,就不能找到虚表,就不能调用虚函数;从继承角度看,构造函数不能被继承,而虚函数是可以被继承的。
12.5虚函数的访问
1、使用指针调用虚函数,可以体现出多态
2、使用引用调用虚函数,可以体现出多态
3、对象调用虚函数,没有体现多态
4、可以使用其他成员函数调用虚函数,可以体现动态多态。
5、使用构造函数与析构函数调用虚函数,没有体现多态。
对象的销毁与析构函数的执行是不是等价的?
栈对象的,等价;堆对象,析构函数的执行只是对象销毁一部分。
虚函数与动态多态是不是等价的?
动态多态的体现必须要有虚函数;但是有虚函数并不一定体现动态多态。
12.6纯虚函数
纯虚函数是一种特殊的虚函数,在许多情况下,在基类中不能对虚函数给出有意义的实现,而把它声明为纯虚函数,它的实现留给该基类的派生类去做。这就是纯虚函数的作用。纯虚函数的格式如下:
class 类名
{
public:
virtual 返回类型 函数名(参数包) = 0;
};
设置纯虚函数的意义,就是让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但类无法为纯虚函数提供一个合理的缺省实现。所以类纯虚函数的声明就是在告诉子类的设计者,“你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它”。
声明纯虚函数的目的在于,提供一个与派生类一致的接口。
12.7抽象类
抽象类是作为接口使用的
一个类可以包含多个纯虚函数。只要类中含有一个纯虚函数,该类便为抽象类。一个抽象类只能作为基类来派生新类,不能创建抽象类的对象。
和普通的虚函数不同,在派生类中一般要对基类中纯虚函数进行重定义。如果该派生类没有对所有的纯虚函数进行重定义,则该派生类也会成为抽象类。这说明只有在派生类中给出了基类中所有纯虚函数的实现时,该派生类才不再是抽象类。
除此以外,还有另外一种形式的抽象类。对一个类来说,如果只定义了protected型的构造函数而没有提供public构造函数,无论是在外部还是在派生类中作为其对象成员都不能创建该类的对象,但可以由其派生出新的类,这种能派生新类,却不能创建自己对象的类是另一种形式的抽象类。
12.8虚析构函数
为了解决内存泄漏。
虽然构造函数不能被定义成虚函数,但析构函数可以定义为虚函数,一般来说,如果类中定义了虚函数,析构函数也应被定义为虚析构函数,尤其是类内有申请的动态内存,需要清理和释放的时候。
如果有一个基类的指针指向派生类的对象,并且想通过该指针delete派生类对象,系统将只会执行基类的析构函数,而不会执行派生类的析构函数。为避免这种情况的发生,往往把基类的析构函数声明为虚的,此时,系统将先执行派生类对象的析构函数,然后再执行基类的析构函数。
如果基类的析构函数声明为虚的,派生类的析构函数也将自动成为虚析构函数,无论派生类析构函数声明中是否加virtual关键字。
12.9重载、隐藏、覆盖
重载:在同一个作用域中,函数名字相同,参数列表不一样(参数类型、参数个数、参数顺序)(单靠返回类型不能区分重载)
覆盖(重定义、重写):在基类与派生类中,虚函数的名字与参数列表都要一样。
隐藏:在基类与派生类中,派生类中的函数屏蔽了基类中的同名函数。(同名的数据成员也可以发生隐藏)
12.10测试虚表的存在
Base(long = 0)
Derived(long = 0, long = 0)
Derived的地址 : 0x7fff28d10e60
Derived的地址 : 0x7fff28d10e60
derived2的地址 : 0x7fff28d10e80
derived2的地址 : 0x7fff28d10e80
虚表的地址 : 0x55f3486b2d00
虚表的地址 : 0x55f3486b2d00
第一个虚函数的地址 : 0x55f3484b20ea
第一个虚函数的地址 : 0x55f3484b20ea
Derived::f()
第一个虚函数的入口地址: 0x55f3484b20ea
Derived::g()
第二个虚函数的入口地址: 0x55f3484b2122
Derived::h()
第三个虚函数的入口地址: 0x55f3484b215a
Base(long = 0)
Derived(long = 0, long = 0)
derived2的地址 : 0x7fff28d10e80
derived2的地址 : 0x7fff28d10e80
虚表的地址 : 0x55f3486b2d00
第一个虚函数的地址 : 0x55f3484b20ea
~Derived()
~Base()
~Derived()
~Base()
12.11带虚函数的多基派生
结论:多重继承(带虚函数)
- 每个基类都有自己的虚函数表
- 派生类如果有自己的虚函数,会被加入到第一个虚函数表之中
- 内存布局中,其基类的布局按照基类被声明时的顺序进行排列
- 派生类会覆盖基类的虚函数,只有第一个虚函数表中存放的是真实的被覆盖的函数的地址;其它的虚函数表中存放的并不是真实的对应的虚函数的地址,而只是一条跳转指令
12.12多基派生的二义性
C c;
printf("&c: %p\n", &c);
c.b();
cout << endl;
A *pA = &c;
printf("pA: %p\n", pA);
pA->a();
pA->b();
pA->c();
cout << endl;
B *pB = &c;
printf("pB: %p\n", pB);
pB->a();
pB->b();
pB->c();
pB->d();
cout << endl;
C *pC = &c;
printf("pC: %p\n", pC);
pC->a();
pC->b();//此处就是二义性
pC->c();//此处的c()走的是虚函数机制还是非虚函数机制,如何判别?
pC->d();//此处是隐藏,不是重写
如何验证一个函数是不是虚函数?
可以再继续的写一个派生类,让其继承,并将该函数重新在派生类中实现一次。
12.13虚拟继承
虚拟继承测试完整版在文章末尾
虚拟继承是指在继承定义中包含了virtual关键字的继承关系。虚基类是指在虚继承体系中的通过virtual继承而来的基类。
虚拟继承与虚函数中的virtual的关系,二者都表示存在、间接、共享.
虚函数中的虚
1、虚函数是存在的
2、虚函数必须要通过一种间接的运行时(而不是编译时)机制才能够激活(调用)的函数(虚表)
3、共享性表现在基类会共享被派生类重定义后的虚函数
虚拟继承中的虚
1、存在即表示虚继承体系和虚基类确实存在
2、间接性表现在当访问虚基类的成员时同样也必须通过某种间接机制来完成(通过虚基表来完成)
3、共享性表现在虚基类会在虚继承体系中被共享,而不会出现多份拷贝(最终的派生类负责虚基类的数据成员的初始化)
共享性
在 C++ 中,如果继承链上存在虚继承的基类,则最底层的子类要负责完成该虚基类部分成员的构造。即我们需要显式调用虚基类的构造函数来完成初始化,如果不显式调用,则编译器会调用虚基类的缺省构造函数,不管初始化列表中次序如何,对虚基类构造函数的调用总是先于普通基类的构造函数。如果虚基类中没有定义的缺省构造函数,则会编译错误。因为如果不这样做,虚基类部分会在存在的多个继承链上被多次初始化。很多时候,对于继承链上的中间类,我们也会在其构造函数中显式调用虚基类的构造函数,因为一旦有人要创建这些中间类的对象,我们要保证它们能够得到正确的初始化。这种情况在菱形继承中非常明显。
普通继承与虚拟继承的区别
虚拟继承会多一个虚基指针,虚基指针指向虚基表,虚基表中存放的是虚基指针距离派生类对象首地址的偏移信息,以及虚基指针距离虚基类的偏移信息;此时派生类对象的数据成员存放在基类数据成员的前面
普通继承的时候,派生类有新增虚函数?
虚拟继承,基类带虚函数,派生有新增虚函数
总结:有虚函数就会有虚函数指针,有虚拟继承就会有虚基指针,虚函数指针指向虚函数表,虚基指针指向虚基表。
如果是普通继承的话,基类的布局形式会存在派生类对象的前面,派生类的布局形式就会排在后面。如果派生类自己新增虚函数,对派生对象的大小是没有影响的,新增虚函数只会存在之前的虚表中。
如果是虚拟继承的话,派生类的布局形式会在派生类对象的前面,基类的布局形式在后面。如果派生类有新增虚函数,派生类对象的大小会发生变化,会多产生一个虚函数指针,用来存放新的虚函数的入口地址。
普通多继承
棱形继承:存在的存储二义性的问题
虚拟继承解决棱形继承产生的存储二义性问题
对于虚继承的派生类对象的析构,析构函数的调用顺序为:
先调用派生类的析构函数;
然后调用派生类中成员对象的析构函数;
再调用普通基类的析构函数;
最后调用虚基类 的析构函数。
12.14效率分析
多重继承和虚拟继承对象模型较单一继承复杂的对象模型,造成了成员访问低效率,表现在两个方面:对象构造时vptr的多次设定,以及this指针的调整。
13移动语义
13.1、几个基本概念的理解
左值、右值、 const 左值引用、右值引用?
左值:可以取地址。
右值:不能进行取地址。右值可能存在寄存器,也可能存在于栈上(短暂存在栈上)。右值包括:包括临时变量、临时对象(string(“world”))、字面值常量.
const左值引用:const int &ref = a; const int &ref = 10;const左值引用既可以绑定到左值,也可以绑定到右值,称为万能引用。正因如此,也就无法区分传进来的参数是左值还是右值。
右值引用只能绑定到右值,不能绑定到左值。所以可以区分出传进来的参数到底是左值还是右值,进而可以区分。
右值引用到底是左值还是右值?
这个与右值引用本身有没有名字有关,如果是 int &&rref = 10 ,右值引用本身就是左值,因为有名字。如果右值引用本身没有名字,那右值引用就是右值,如右值引用作为函数返回值。
int &&func()
{
return 10;
}
&func();//error,右值引用本身也可以是右值
13.2、移动构造函数
13.3、移动赋值函数
13.4、std::move函数
将左值转换为右值,在内部其实上是做了一个强制转换, static_cast<T &&>(lvaule) 。
将左值转换为右值后,左值就不能直接使用了,如果还想继续使用,必须重新赋值。
std::move()作用于内置类型没有任何作用,内置类型本身是左值还是右值,经过std::move()后不会改变。
13.5、移动语义总结
拷贝构造函数与赋值运算符函数,编译器会自动提供;但是移动构造函数与移动赋值运算符函数,编译器不会自动提供,必须要手写。
将拷贝构造函数与赋值运算符函数称为具有拷贝控制语义的函数;将移动构造函数与移动赋值运算符函数称为具有移动语义的函数。
移动语义的函数优先于拷贝语义的函数。
13.6、左值右值再探
文字常量区的变量,并不一定是字符串,有可能是const修饰的全局变量;用const修饰的变量并不一定位于内存中的同一块区域。
14资源管理与智能指针
14.1C语言中的问题
C语言在对资源管理的时候,比如文件指针,由于分支较多,或者由于写代码的人与维护的人不一致,导致分支没有写的那么完善,从而导致文件指针没有释放,所以可以使用C++的方式管理文件指针。
14.2C++的解决办法( RAII 技术):
资源管理 RAII 技术,利用栈对象的生命周期管理程序资源(包括内存、文件句柄、锁等)的技术,因为对象在离开作用域的时候,会自动调用析构函数。
关键:要保证资源的释放顺序与获取顺序严格相反。正好是析构函数与构造函数的作用。
RAII常见特征
1、在构造时初始化资源,或者托管资源。
2、析构时释放资源。
3、一般不允许复制或者赋值(值语义-对象语义)
4、提供若干访问资源的方法。
区分:值语义:可以进行复制与赋值。
对象语义:不能进行复制与赋值,
一般使用两种方法达到要求:
(1)、将拷贝构造函数和赋值运算符函数设置为私有的就 ok 。
(2)、将拷贝构造函数和赋值运算符函数使用=delete.
14.3智能指针
14.3.1auto_ptr
最简单的智能指针,使用上存在缺陷,所以被弃用。(C++17已经将其删除了)
//auto_ptr<int> ap2(ap);
auto_ptr(auto_ptr& __a)
: _M_ptr(__a.release())
{
}
//ap
_Tp* release() {
_Tp* __tmp = _M_ptr;
_M_ptr = nullptr;
return __tmp;
}
14.3.2unique_ptr
比auto_ptr安全多了,明确表明是独享所有权的智能指针,所以不能进行复制与赋值
1、在语法层面不允许复制或者赋值
2、unique_ptr具有移动语义(具有移动构造函数与移动赋值函数),可以作为容器的元素
14.3.3shared_ptr
虽然unique_ptr解决了auto_ptr的缺陷,但是使用上还是有限制,因为其只能独享所有权,于是就有共享所有权的需求,即是shared_ptr采用的思想与之前的写时复制技术类型,浅拷贝 + 引用计数,use_count函数记录指向一块内存的指针的数目。
1、可以进行复制或者赋值
2、shared_ptr也具有移动语义,也可以作为容器的元素
3、循环引用
该智能指针在使用的时候,会使得引用计数增加,从而会出现循环引用的问题?
两个shared_ptr智能指针互指,导致引用计数增加,不能靠对象的销毁使得引用计数变为0,从而导致内存泄漏。
解决循环引用的办法是使得其中一个改为weak_ptr,不会增加引用计数,这样可以使用对象的销毁而打破引用计数减为0的问题。
14.3.4weak_ptr
与shared_ptr相比,称为弱引用的智能指针,shared_ptr是强引用的智能指针。
weak_ptr不会导致引用计数增加,不能直接托管裸指针,只能从shared_ptr去进行复制或者赋值,或者从其他的weak_ptr复制或者赋值。
不能直接获取资源,必须通过lock函数从wp提升为sp,从而判断共享的资源是否已经销毁。判断weak_ptr托管的资源还存在与否,可以使用lock函数或者expired函数.
14.4为智能指针定制删除器
unique_ptr(存在于模板的第二个参数)与shared_ptr(存在于构造函数之中)相关的删除器。
库中实现的各种智能指针,默认也都是用delete来释放空间,但是若我们采用malloc申请的空间或是用fopen打开的文件,这时我们的智能指针就无法来处理,因此我们需要为智能指针定制删除器,提供一个可以自由选择析构的接口,这样,我们的智能指针就可以处理不同形式开辟的空间以及可以管理文件指针。
自定义智能指针的方式有两种,函数指针与仿函数(函数对象)
14.5智能指针的误用
14.5.1同一个裸指针被不同的智能指针托管,导致被析构两次。
不同的share_ptr也不能直接托管同一个裸指针
14.5.2还是裸指针被智能指针托管形式,但是比较隐蔽。
15模板
15.1为什么要定义模板
1、简化程序,少写代码,维持结构的清晰,大大提高程序的效率。
2、解决强类型语言的严格性和灵活性之间的冲突。。
2.1、带参数的宏定义(原样替换)
2.2、函数重载(函数名字相同,参数不同)
2.3、模板(将数据类型作为参数)
3、强类型语言程序设计:C/C++/Java等,有严格的类型检查,如int a = 10,在编译时候明确变量的类型,如果有 问题就可以在编译时发现错误,安全,但是不够灵活,C++引进auto其实就是借鉴弱类型语言的特征。
弱类型程序语言设计:js/python等,虽然也有类型,但是在使用的时候直接使用let/var number,不知道变量具体类型,由编译器解释变量类型,属于解释型语言。如果有错,到运行时才发现,虽然灵活,但是不安全.
15.2形式
//例子:函数模板
template <模板参数列表>
函数的返回类型 函数名字(函数的参数列表)
{
}
template <typename T1, typename T2,...>
template <class T1, class T2,...>
//模板参数列表中typename与class的含义是完全一样
15.3模板的类型
函数模板与类模板。通过参数实例化构造出具体的函数或者类,称为模板函数或者模板类
15.3.1函数模板
template <typename T>//模板参数列表
T add(T x, T y)
{
cout << "T add(T, T)" << endl;
return x + y;
}
实例化:隐式实例化与显示实例化
cout << "add(ia, ib) = " << add(ia, ib) << endl;//隐式实例化,没有明确说明类型,靠编译器推导
cout << "add(da, db) = " << add<double>(da, db) << endl;//显示实例化,编译器无序推导
函数模板、普通函数间的关系
普通函数可以与函数模板进行重载
普通函数优先于函数模板的执行
函数模板与函数模板之间也是可以进行重载的
模板头文件与实现文件
模板不能写成头文件与实现文件形式(类型inline函数),或者说不能将声明与实现分开,这样会导致编译报错。
分开可以编译,但是在链接的时候是有问题的。
模板的特化:偏特化与全特化
全特化:将模板的参数列表中的参数全部以特殊版本的形式写出来;
偏特化(部分特化):将模板参数列表中的参数类型,至少有一个没有特化出来。
函数模板的参数类型
1、类型参数,class T 这种就是类型参数
2、非类型参数 常量表达式,整型:bool/char/short/int/long/size_t,注意:float/double这些就不是整型
成员函数模板
15.3.2类模板
使用与函数模板也差不多,只是要注意模板的嵌套(函数模板与类模板都可以嵌套,比如函数参数是模板,类模板里面还有类模板)
template <typename T>
class Stack
{
private:
T *_data;
};
模板是可以进行嵌套的。
template <typename T>
class A
{
template <typename K>
class B
{
template <typename ...Args>
T func(Args ...args);
};
};
template <typename T>
template <typename K>
template <typename ...Args>
class A
class B
T func(Args ...args)
{
}
15.4可变模板参数
是C++11新增的最强大的特性之一,它对参数进行了高度的泛化,它能表示0到任意个数、任意类型的参数。
template <typename ...Args>//Args模板参数包
void func(Args ...args)//args函数参数包
{
}
template <typename T1, typename T2, typename T3,...>
void func(T1 t1, T2 t2, T3 t3,...)
{
}
Args标识符的左侧使用了省略号,在C++11中Args被称为“模板参数包”,表示可以接受任意多个参数作为模板参数,编译器将多个模板参数打包成“单个”的模板参数包.
args 被称为函数参数包,表示函数可以接受多个任意类型的参数.
在C++11标准中,要求函数参数包必须唯一,且是函数的最后一个参数; 模板参数包则没有。
当使用参数包时,省略号位于参数名称的右侧,表示立即展开该参数,这个过程也被称为解包。
可变模板参数的优势(有两条)
1、参数个数,那么对于模板来说,在模板推导的时候,就已经知道参数的个数了,也就是说在编译的时候就确定 了,这样编译器就存在可能去优化代码
2、参数类型,推导的时候也已经确定了,模板函数就可以知道参数类型了。
16标准模板库STL
STL包含六大基本组件
1、容器:用来存数据的,也称为数据结构。
序列式容器:vector、list
关联式容器:set、map
无序关联式容器:unordered_set、unordered_map
2、迭代器 行为类似于指针,具有指针的功能。用迭代器来连接容器与算法的。
3、算法。用来操作数据。
4、适配器。因为STL中的算法的实现不是针对于具体容器的,所以可能某些算法并不适用具体的容器,需要使用适配器进行适配(进行转接)。
包含:容器的适配器、迭代器的适配器、算法的适配器
5、函数对象(仿函数)。做定制化操作的。
6、空间配置器。进行空间的申请与释放的。
数据结构 + 算法 = 程序
16.1容器(container)
16.1.1序列式容器(sequential container)
模板参数类型
template<class T,class Allocator = std::allocator<T>> class vector;
template<class T,class Allocator = std::allocator<T>> class deque;
template<class T,class Allocator = std::allocator<T>> class list;
vector、deque、list三者的初始化、遍历、尾部插入与删除、头部插入与删除(deque、list)(这里要讲vector与deque的底层实现)、中间插入insert(vector、deque会有失效)、清空元素、list的特殊操作(sort、merge、splice、unique、reverse)。
template<
class T,
class Allocator = std::allocator<T>
> class vector;
template<
class T,
class Allocator = std::allocator<T>
> class deque;
template<
class T,
class Allocator = std::allocator<T>
> class list;
1.0、头文件
#include <vector>
#include <deque>
#include <list>
1.1、初始化
1.1.1、直接初始化为空
vector<int> numbers ;
deque<int> numbers;
list<int> numbers;
1.1.2、初始化为多个数据(默认初始化为0)
vector<int> numbers(10, 1);
deque<int> numbers(10, 1);
list<int> numbers(10, 1);
1.1.3、使用迭代器范围
int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
vector<int> numbers(arr, arr + 10);//左闭右开区间
1.1.4、使用大括号
vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};//deque与list直接将vector替换即可
1.2、遍历
//2.1、使用下标进行遍历(要求容器必须是支持下标访问的,list不支持下标,所以就不适用)
for(size_t idx = 0; idx != numbers.size(); ++idx)
{
cout << numbers[idx] << " ";
}
cout << endl;
//2.2、使用迭代器进行遍历
vector<int>::iterator it;//或者使用常量迭代器vector<int>::const_iterator it
for(it = numbers.begin(); it != numbers.end(); ++it)
{
cout << *it << " ";
}
cout << endl;
//2.3、使用auto加迭代器
auto it = numbers.begin();
for(; it != numbers.end(); ++it)
{
cout << *it << " ";
}
cout << endl;
//2.4、使用for加上auto进行遍历
for(auto &elem : numbers)
{
cout << elem << " ";
}
cout << endl;
//重定义的方式
using line_no = vector<string>::size_type;//C++11的写法
typedef vector<string>::size_type line_no;//C语言的实现方式
1.3查找
1.4、尾部插入、头部插入与删除
push_back与pop_back,这两个函数比较简单,三个都适用。push_back有个两倍扩容(gcc)
push_front与pop_front,这两个函数与尾部插入一样,比较简单,但是vector不适用,只有deque与list适用。
1.5、中间插入
insert在中间进行插入,list使用起来很好,但是deque与vector使用起来就有问题,因为vector是物理上连续的,所以在中间插入元素会导致插入元素后面的所有元素向后移动,deque也有类似情况,可能因为插入而引起扩容导致迭代器失效(指向了新的空间),即使没有扩容,插入之后的迭代器也失效了(不再指向之前的元素)
1.5.1、insert的几种插入方式
//5.1、直接在某个位置插入一个元素
iterator insert( iterator pos, const T& value );
iterator insert( const_iterator pos, const T& value );
numbers.insert(it, 22);
//5.2、直接在某个位置插入count个元素
void insert(iterator pos, size_type count, const T& value)
iterator insert(const_iterator pos, size_type count, const T& value)
numbers.insert(it1, 4, 44);
//5.3、直接在某个位置插入迭代器范围的元素
template<class InputIt> void insert(iterator pos, InputIt first, InputIt
last)
template<class InputIt> iterator insert(const_iterator pos, InputIt first,
InputIt last)
vector<int> vec{51, 52, 53, 54, 55, 56, 57, 58, 59};
numbers.insert(it, vec.begin(), vec.end());
//5.4、插入一个大括号范围的元素
iterator insert(const_iterator pos, std::initializer_list<T> ilist)
numbers.insert(it, std::initialiser_list<int>{1, 2, 3});
1.6、元素的删除
erase函数,删除容器中的元素,删除一个元素,删除迭代器范围。注意删除之后,对于vector而言,会导致删除之后的元素前移,从而导致删除之后的所有迭代器失效(迭代器的位置没有改变,但是因为元素的移动,导致迭代器指向的不是删除之前的元素,所以需要注意)(当然list的删除没有影响)。deque比vector复杂,要看pos前后的元素个数来决定(deque的erase函数可以看STL源码,需要看删除位置与size()的一半的大小,然后看是挪动前一半还是后一半,尽量减少挪动的次数)。
1.7、清空元素
clear(清空元素,元素个数为0)与shrink_to_fit(缩减到刚刚好)(vector与deque有这个函数,但是list没有,list清空元素时候,节点都销毁了)
1.8、获取容器中元素个数与空间大小
size()获取容器中元素个数,三者都有该函数
capacity()获取容器申请的空间大小,只有vector有这个函数。
1.9、list的特殊操作
1.9.1、排序函数sort
void sort();//默认以升序进行排序,其实也就是,使用operator<进行排序
template< class Compare > void sort(Compare comp);//其实也就是传入一个具有比较的类型,即函数对象
template <typename T1, typename T2>
struct Compare
{
bool operator()(const T1 &a, const T2 &b) const
{
return a < b;
}
};
1.9.2、移除重复元素unique
注意使用unique的时候,要保证元素list是已经排好顺序的,否则使用unique是没有用的。
1.9.3、逆置链表中的元素reverse
将链表逆置输出
1.9.4、合并链表的函数merge
合并的链表必须是有序的,如果没有顺序,合并没有效果。两个链表合并之后,另一个链表就为空了。
1.9.5、从一个链表转移元素到另一个链表splice
void splice(const_iterator pos, list& other)//移动一个链表到另一个链表的某个指定位置
void splice(const_iterator pos, list&& other)
//移动一个链表中的某个元素到另一个链表的某个指定位置
void splice(const_iterator pos, list& other, const_iterator it)
void splice(const_iterator pos, list&& other, const_iterator it);
//移动一对迭代器范围元素到另一个链表的某个指定位置
void splice(const_iterator pos, list& other, const_iterator first,
const_iterator last)
void splice(const_iterator pos, list&& other,const_iterator first,
const_iterator last)
void test()
{
vector<Point> points;
points.reserve(10);
points.emplace_back(1, 2);
points.emplace_back(3, 4);
/* points.push_back(Point(1, 2));//减少临时变量的产生对内存的消耗 */
points[0].print();
points[1].print();
}
1.9.6、另外的一种插入方式emplace与emplace_back
emplace有放置的意思
vector的实现
为什么vector不支持在头部进行插入与删除呢?
插入与删除第一个元素的时候,都会将后面元素进行挪动,时间复杂度O(N)
探讨vector的底层实现,三个指针:
_M_start:指向第一个元素的位置
_M_finish:指向最后一个元素的下一个位置
_M_end_of_storage:指向当前分配空间的最后一个位置的下一个位置
类型萃取帮助我们提取出自定义类型进行深拷贝,而内置类型统一进行浅拷贝,也就是所谓的值拷贝。
对于vector下标访问与at的区别?
at可以进行范围的检查,比operator[]更加安全
vector的迭代器失效
迭代器已经失效了 (在进行insert操作的时候,底层已经发生了扩容)。解决方案,每次进行insert之 后将迭代器重新置位。
vector在删除元素的时候,也可能导致迭代器失效。
vector的insert扩容原理
deque实现:
物理上是不连续的,逻辑上是连续的。
中控器数组、多个连续的小片段、迭代器是一个类。
中控器数组是一个二级指针,包括中控器的大小。
小片段内部是连续的,但是片段与片段之间是不连续的。
迭代器是一个类,deque有两个迭代器指针,一个指向第一个小片段,一个指向最后一个小片段。
16.1.2关联式容器(associative container)
模板参数
template<class Key,class Compare = std::less<Key>,class Allocator = std::allocator<Key>> class set;
template<class Key,class Compare = std::less<Key>,class Allocator = std::allocator<Key>> class multiset;
template<class Key,class T,class Compare = std::less<Key>,class Allocator = std::allocator<std::pair<const Key, T> >
> class map;
template<class Key,class T,class Compare = std::less<Key>,class Allocator = std::allocator<std::pair<const Key, T> >
> class multimap;
(set、multiset、map、multimap):底层实现使用红黑树。初始化、遍历、查找(count,find)、插入(insert)(set与map需要判断插入是不是成功),自定义类型需要去对Compare进行改写(std::less、std::greater、函数对象)。
2.1、初始化
使用形式与序列式容器完全一致(可以参考序列式容器)。
set特征:
1、不能存放关键字key相同的元素,关键字必须唯一
2、默认以升序进行排列
3、底层实现是红黑树
红黑树的五大特征:
1、节点不是红色就是黑色
2、根节点是黑色的
3、叶子节点也是黑色的
4、如果一个节点是红色的,那么它的左右孩子节点必须是黑色的
5、从根节点到叶子节点上所有路径要保证黑色节点的个数相同
multiset特征:
1、可以存放关键字key相同的元素,关键字不唯一
2、默认以升序进行排列
3、底层实现是红黑树
map的特征:
1、存放的是键值对,也就是也个pair,即 pair<const Key, value> ,key值必须唯一,不能重复
2、默认按照关键字key进行升序排列
3、底层实现是红黑树
multimap的特征:
1、存放的是键值对,也就是也个pair,即 pair<const Key, value> ,key值不唯一,可以重复
2、默认按照关键字key进行升序排列
3、底层实现是红黑树
set与multiset的初始化、遍历、查找、删除的使用都一样,都不支持下标访问、都不支持修改。对于insert插入而言,multiset肯定可以插入成功,所以二者返回类型不一样,至于插入迭代器返回与大括号类型都是一样的。
map<int, string> cities =
{
{1, "北京"},//大括号方式进行初始化
{2, "上海"}
std::pair<int,string>(3, "广州"),//map中存的是pair类型,就直接使用pair
std::pair<int,string>(4, "深圳"),
std::make_pair(5, "武汉"),//std::make_pair()函数返回类型就是pair类型
std::make_pair(6, "南京"),
std::make_pair(2, "上海"),
std::make_pair(3, "南京"),
};
display(cities);
2.2、遍历
使用形式与序列式容器完全一致(可以参考序列式容器)
2.3、查找
两个函数count(返回元素的数目)与find函数(返回查找后的迭代器的位置)
还有三个用于查找的函数,这个函数对于multiset与multimap效果要好一点。
lower_bound(key);//不大于key的第一个位置
upper_bound(key);//大于key的第一个位置
equal_range(key);
2.4、插入
2.5、删除
2.6、修改
由于底层实现是红黑树,所以不支持修改。
2.7、下标访问
只有map支持下标访问,且兼具插入的功能,所以使用起来比较方便,但是时间复杂度是O(logN)
2.8、针对自定义类型的操作
set针对于自定义类型,必须要实现Compare,也就是std::less或者std::greater
//std::less针对Point进行特化
namespace std
{
template <>
struct less<Point>
{
bool operator()(const Point &lhs, const Point &rhs)
{
//......
}
};
}//end of namespace std
16.1.3无序关联式容器(unordered associative container)
无序关联式容器的底层实现使用的是哈希表,关于哈希表有几个概念需要了解:哈希函数、哈希冲突、解决哈希冲突的方法、装载因子(装填因子、负载因子)
3.1、哈希函数
是一种根据关键码key去寻找值的数据映射的结构,即:根据key值找到key对应的存储位置。size_t index = H(key)//由关键字获取所在位置
3.2、哈希函数的构造
1、直接定址法: H(key) = a * key + b
2、平方取中法: key^2 = 1234^2 = 1522756 ------>227
3、数字分析法:H(key) = key % 10000;
4、除留取余法:H(key) = key mod p (p <= m, m为表长)
3.3、哈希冲突
就是对于不一样的key值,可能得到相同的地址,即:H(key1) = H(key2)
3.4、解决哈希冲突的解决办法
1、开放定址法
2、链地址法 (推荐使用这种,这也是STL中使用的方法)
3、再散列法
4、建立公共溢出区
3.5、装载因子
装载因子 a = (实际装载数据的长度n)/(表长m)
a越大,哈希表填满时所容纳的元素越多,空闲位置越少,好处是提高了空间利用率,但是增加了哈希碰撞的风险,降低了哈希表的性能,所以平均查找长度也就越长;但是a越小,虽然冲突发生的概率急剧下降,但是因为很多都没有存数据,空间的浪费比较大,经过测试,装载因子的大小在[0.5~0.75]之间比较合理,特别是0.75
3.6、哈希表的设计思想
用空间换时间,注意数组本身就是一个完美的哈希,所有元素都有存储位置,没有冲突,空间利用率也达到极致。
3.7、四种无序关联式容器
(unordered_set、unordered_multiset、unordered_map、unordered_multimap):底层实现使用哈希表。针对于自定义类型需要自己定义std::hash函数与std::equal_to函数,四种容器的类模板如下:
//unordered_set与unordered_multiset位于#include <unordered_set>中
template < class Key,class Hash = std::hash<Key>,class KeyEqual = std::equal_to<Key>,class Allocator = std::allocator<Key>> class unordered_set;
template < class Key,class Hash = std::hash<Key>,class KeyEqual = std::equal_to<Key>,class Allocator = std::allocator<Key>> class unordered_multiset;
//unordered_map与unordered_multimap位于#include <unordered_map>中
template< class Key,class T,class Hash = std::hash<Key>,class KeyEqual = std::equal_to<Key>,class Allocator = std::allocator< std::pair<const Key, T> >> class unordered_map;
template< class Key,class T,class Hash = std::hash<Key>,class KeyEqual = std::equal_to<Key>,class Allocator = std::allocator< std::pair<const Key, T> >> class unordered_multimap;
//委托构造函数
unordered_set()
: unordered_set( size_type(/*implementation-defined*/) ) //初始化列表
{
}
3.8针对内置类型的特化版本
3.9针对于自定义类型的特化
5.1、std::hash的实现,使用函数对象的方式
5.2、等于符号的实现
5.3hash针对string的特化
总结:所有关联式容器的查找、插入、删除函数,以set与map的使用为主。所有无序关联式容器的使用以unordered_set以及unordered_map为主。
16.1.4容器适配器
priority_queue的使用,这里讲堆排序,大小堆的建立(八大排序算法,这个大家要熟练,这是基本功)优先级队列默认使用std::less,但是体现出来是一个大根堆。(可以直接看源码,也就是堆排序的建堆、堆调整)
template < class T,class Container = std::vector<T>,class Compare = std::less<typename Container::value_type>> class priority_queue;
2、初始化与遍历
3、原理
4、针对自定义类型的用法
16.1.5如何选择合适的容器
1、如果元素是连续存储的?
肯定不是关联式容器,也不是无序关联式容器,肯定就只能在序列式容器中进行选取。list元素之间也不是连续的,是链表;deque是逻辑上是连续的,但是物理上是不一定连续的;只能是vector。
2、查找数据的时候,时间复杂度?
查找的时间复杂度是O(1),可以首选底层使用哈希表的容器,就是四个无序关联式容器。需要注意vector是可以支持下标访问,时间复杂度也能是O(1)。
时间复杂度是O(logN),树的结构查找数据的时候,满足这个条件。关联式容器底层使用红黑树,所以可以首选四种关联式容器。
3、可以使用下标?
vector、deque、map、unordered_map
4、下标具有插入操作的
map或者unordered_map
5、容器可以使用迭代器
不能使用容器适配器,不能使用stack、queue、priority_queue
16.2迭代器(iterator)
迭代器(iterator)模式又称为游标(Cursor)模式,用于提供一种方法顺序访问一个聚合对象中各个元素, 而又不需暴露该对象的内部表示。或者这样说可能更容易理解:Iterator模式是运用于聚合对象的一种模式,通过运用该模式,使得我们可以在不知道对象内部表示的情况下,按照一定顺序(由iterator提供的方法)访问聚合对象中的各个元素。
迭代器类似于指针,可以操作容器中的数据,使得算法与容器之间就关联起来。
输入输出流数据都会进缓冲区,缓冲区是可以存数据的,而容器就是用来存数据的,所以输入输出流可以看成是容器。
16.2.1迭代器产生原因(或者本质)
Iterator类的访问方式就是把不同集合类的访问逻辑抽象出来,使得不用暴露集合内部的结构而达到循环遍历集合的效果。
16.2.2迭代器的类型
随机访问迭代器(RandomAccessIterator)
双向迭代器(BidirectionalIterator)
前向迭代器(ForwardIterator)
输出迭代器(OutputIterator)
输入迭代器(InputIterator)
以及其头文件#include
16.2.3为什么定义这么多迭代器?
物尽其用,使得具体的操作使用具体类型的迭代器,避免迭代器的功能太大或者太小,导致使用起来不方便。
每个容器及其对应的迭代器的类型图表如下:
每个迭代器的类型与其对应的操作
16.2.4流迭代器
流迭代器是特殊的迭代器,可以将输入/输出流作为容器看待(因为输入输出都有缓冲区的概念)
ostream_iterator(ostream_type& stream, const CharT* delim)
ostream_iterator(ostream_type& stream)
template< class InputIt, class OutputIt >
OutputIt copy( InputIt first, InputIt last, OutputIt d_first );
_Trivial:平凡的。如果对应的是类的话,类没有显示的写构造函数、析构函数、拷贝构造函数、赋值运算符函数等。int/bool/char 内置类型
noTrival:非平凡的。如果对应的是类的话,类有显示的写构造函数、析构函数、拷贝构造函数、赋值运算符函数等。
16.2.5迭代器适配器
back_inserter函数模板,返回类型是back_insert_iterator
back_insert_iterator是类模板,底层调用了push_back函数
front_inserter函数模板,返回类型是front_insert_iterator
front_insert_iterator是类模板,底层调用了push_front函数
inserter函数模板,返回类型是insert_iterator
insert_iterator是类模板,底层调用了insert函数
三个迭代器适配器的类模板与三个迭代器适配器的函数模板
16.2.6逆向迭代器(反向迭代器)
16.3适配器(adapter)
定义:适配器就是Interface(接口),对容器、迭代器和算法进行包装,但其实质还是容器、迭代器和算法,只是不依赖于具体的标准容器、迭代器和算法类型,容器适配器可以理解为容器的模板,迭代器适配器可理解为迭代器的模板,算法适配器可理解为算法的模板。
本质:适配器是使一事物的行为类似于另一事物的行为的一种机制。
1、容器适配器
stack、queue、priority_queue,上面已经讲过,直接使用即可
2、迭代器适配器
back_insert_iterator、front_insert_iterator、insert_iterator,迭代器中已经讲过
3、函数适配器
bind1st、bind2nd、bind(后面算法阶段会讲解使用)
not1、not2 否定器
mem_fn 成员函数绑定器,后面会讲解
16.4、算法(algorithm)
算法中包含很多对容器进行处理的算法,使用迭代器来标识要处理的数据或数据段、以及结果的存放位置,有的函数还作为对象参数传递给另一个函数,实现数据的处理。这些算法可以操作在多种容器类型上,所以称为“泛型”,泛型算法不是针对容器编写,而只是单独依赖迭
代器和迭代器操作实现。
16.4.1、头文件
#include <algorithm> //泛型算法
#include <numeric> //泛化的算术算法
16.4.2、分类
1、非修改式序列操作:不改变容器的内容,如 count、find、find_xxx、for_each等。
2、修改式序列操作:可以修改容器中的内容,如copy、remove、remove_if、unique、fill等。
3、排序和相关操作:包括各种排序函数等,sort以及sort相关、lower_bound、upper_bound、binary_search
4、集合操作 set_intersection
5、heap的操作 make_heap、push_heap、pop_heap
6、最大值与最小值 min、max
7、比较函数 equal
8、当空间的申请与对象的构建分开的时候,可以uninitialized_copy未初始化拷贝操作,uninitialized_xxx
16.4.3、一元函数以及一元断言/一元谓词:
一元函数:函数的参数只有一个;一元断言/一元谓词:函数的参数只有一个,并且返回类型是bool类型。
二元函数:函数的参数有两个;二元断言/二元谓词:函数的参数两个,并且返回类型是bool类型。
16.4.4、非修改式算法(for_each)
模板形式
template< class InputIt, class UnaryFunction >
UnaryFunction for_each( InputIt first, InputIt last, UnaryFunction f );//UnaryFunction一元函数
void print(int value)
{
cout << value << " ";
}
vector<int> number = {1, 6, 8, 4, 2, 7, 9};
for_each(number.begin(), number.end(), print);
template <class _InputIter, class _Function>
_Function for_each(_InputIter __first, _InputIter __last, _Function __f)
{
for ( ; __first != __last; ++__first)
__f(*__first);//print(*__first)
return __f;
}
16.4.5、修改式算法(copy/remove_if)
template<class InputIt, class UnaryPredicate>
constexpr InputIt find_if(InputIt first, InputIt last, UnaryPredicate p)
{
for (; first != last; ++first) {
if (p(*first)) {
return first;
}
}
return last;
}
template<class ForwardIt, class UnaryPredicate>
ForwardIt remove_if(ForwardIt first, ForwardIt last, UnaryPredicate p)
{
first = std::find_if(first, last, p);
if (first != last)
{
for(ForwardIt i = first; ++i != last; )
{
if (!p(*i))
{
//*first++ = std::move(*i);
*first = *i;
first++;
}
}
}
return first;
}
STL算法库中的算法属于通用算法,不是针对于某一种具体容器去进行设计,所有的容器都可以直接使用。这就是通用编程,或者称为泛型编程的思想。
remove_if(number.begin(), number.end(), bind1st(std::less<int>(), 4))
struct less
{
bool operator()(const int &num1 = 4, const int &num2)
{
return 4 < num2;
}
}
//可以绑定二元函数对象f的第一个参数,使得二元函数变成一元函数,并固定第一个参数
template< class F, class T >
std::binder1st<F> bind1st( const F& f, const T& x );
//可以绑定二元函数对象f的第二个参数,使得二元函数变成一元函数,并固定第二个参数
template< class F, class T >
std::binder2nd<F> bind2nd( const F& f, const T& x );
16.4.6、函数绑定器的详解(bind,function,mem_fn):
Defined in header <functional>
template< class F, class... Args >
/*unspecified*/ bind( F&& f, Args&&... args );
template< class R, class F, class... Args >
/*unspecified*/ bind( F&& f, Args&&... args );
模板形式中,两个函数绑定器的第一个参数就是一个函数,第二个参数就是一个数字,如果F是一个二元函数(普通二元函数或者二元谓词),我们可以绑定F的第一个参数(bind1st)或者第二个参数(bind2nd),达到我们想要的效果(使用二元谓词的效果)
C++11中bind函数的使用
bind可以绑定到n元函数,该函数可以是自由函数、全局函数(非成员函数),也可以绑定到成员函数,甚至可以绑定的数据成员上。绑定到数据成员上时,将数据成员看为无参函数。
bind函数的另外一个问题,就是占位符的概念,占位符本身是什么,占位符的数字代表什么?
占位符本身所在的位置是形参的位置,占位符的数字代表实参传递时候的位置。
bind函数的返回类型到底是什么呢?
其实就是一种函数类型,就是一种可以装函数类型的容器,即:function。有了function之后,bind函数还可以绑定到数据成员上面来,其实就是int()这中类型上来。
function的使用
Defined in header <functional>
template< class >
class function; /* undefined */
template< class R, class... Args >
class function<R(Args...)>;
function<类型> f;
function<int(int, int, int)> f;//function也是容器,可以传某种类型的数据
template<class T, class Allocator = std::allocator<T>>
class vector;
vector<int> number;//vector属于容器,可以存任何类型的数据
std::function与std::bind结合使用体现出多态性的例子,也就是注册回调函数与执行回调函数。
使用的是基于对象的思想(没有使用继承)
mem_fn的使用
成员函数绑定器
mem_fun() //容器参数为类指针
mem_fun_ref() //容器参数为类对象
mem_fn() 两者都能用(C++11),其使用更具有广泛性,直接使用代码就ok
//非静态的成员函数,都会在第一个参数的位置隐藏一个this
//成员函数指针
int (Test::*pFunc)(int, int);//解决this指针的问题
pFunc = &Test::add;
template< class M, class T >
/*unspecified*/ mem_fn(M T::* pm);
//在C语言中,函数名字是函数的入口地址,C语言是不支持函数重载的
add;
//在C++中因为存在函数重载,要找到函数,可以使用函数进行取地址&add;C++是兼容C语言,所以普通
函数的函数名就是函数的入口地址,但是C++中成员函数的函数名字就不是函数的入口地址。
16.5、函数对象(functor)
函数对象是可以以函数方式与()结合使用的任意对象,包括:(functor-仿函数)
1、函数名;
2、指向函数的指针;
3、重载了()操作符的类对象(即定义了函数operator()()的类)。
以上就是对函数对象做了一个扩充,具体的使用形式已经用过,此处不再赘述。
16.6、空间配置器(allocator)
在C++中所有STL容器的空间分配其实都是使用的std::allocator,它是可以感知类型的空间分配器,并将空间的申请与对象的构建分离开来。
单个对象使用new的时候,空间的申请与对象的构造好像是没有分开的,连在一起了,但是对于容器vector而言,其有个函数叫做reserve函数,先申请空间,然后在在该空间上构建对象。
为什么要将空间的申请与对象的构建分开呢
STL中存放的是大量元素,如果每次创建一个对象就申请一次空间,这样的话效率是非常低的。所以就可以直接一次申请一大片空间,然后在申请的空间上面进行构建对象。
16.6.1头文件
template< class T > struct allocator;
template<> struct allocator<void>;
16.6.2空间申请与释放以及对象的构建与销毁的四个函数如下
//空间的申请,申请的是原始的,未初始化的空间
pointer allocate( size_type n, const void * hint = 0 );
T* allocate( std::size_t n, const void * hint);
T* allocate( std::size_t n );
//空间的释放
void deallocate( T* p, std::size_t n );
//对象的构建,在指定的未初始化的空间上构建对象,使用的是定位new表达式
void construct( pointer p, const_reference val );
//对象的销毁
void destroy( pointer p );
自定义vector
copy(_start, _finish, ptmp);
_OutputIter __copy(_InputIter __first, _InputIter __last,
_OutputIter __result,
input_iterator_tag, _Distance*)
{
for ( ; __first != __last; ++__result, ++__first)//_M_read() cin >>
_M_value
*__result = *__first;
return __result;
}
16.6.3定位new表达式
定位new表达式接受指向未构造内存的指针,并在该空间中初始化一个对象或者数组。不分配内存,它只在特定的,预分配的内存地址构造一个对象。
template <class _T1, class _T2>
inline void construct(_T1* __p, const _T2& __value) {
_Construct(__p, __value);
}
template <class _T1, class _T2>
inline void _Construct(_T1* __p, const _T2& __value) {
new ((void*) __p) _T1(__value);//定位new表达式,在执行空间__p上构建对象
}
template <class _Tp>
inline void destroy(_Tp* __pointer) {
_Destroy(__pointer);
}
template <class _Tp>
inline void _Destroy(_Tp* __pointer) {
__pointer->~_Tp();
}
16.6.4真正具备空间分配功能的std::alloc
两级空间配置器,第一级空间配置器使用类模板malloc_alloc_template ,其底层使用的是malloc/free进行空间的申请与释放,二级空间配置器使用类模板,default_alloc_template,其底层根据申请空间大小有分为两个分支进行,第一分支是当申请的空间大于128字节的时候,还是走__malloc_alloc_template ,当申请的空间小于128字节的使用,使用内存池+16个自由链表的结构进行。
class allocator
{
public:
_Tp* allocate(size_type __n, const void* = 0)
{
return __n != 0 ? static_cast<_Tp*>(_Alloc::allocate(__n *sizeof(_Tp))): 0;
}
void construct(pointer __p, const _Tp& __val)
{
//定位new表达式
new(__p) _Tp(__val);//new int(1);
}
void destroy(pointer __p)
{
__p->~_Tp(); //显示调用析构函数
}
};
为何不全部使用malloc申请空间
1、对于小块空间而言,频繁的申请与释放,会有内存碎片的问题
2、频繁的malloc会调用系统调用,会在用户态与内核态之间频繁的切换,效率相对而言较低。
为什么要分成两部分呢?
1、向系统堆要求空间。2、考虑多续状态(multi-threads)。 3、考虑内存不足时的应变措施。4、考虑过多小块内存造成的内存碎片
内存碎片
内部碎片:页式管理、段式管理、段页式管理(局部性原理),无法避免,但是通过算法可以优化。
外部碎片:申请堆内存之间的片段空隙,这个是可以合理使用的。
分成两部分的解决办法?
一级空间配置器:使用malloc/free系统调用,缺点:频繁的用户态到内核态的切换,开销大(brk,mmap)
二级空间配置器:内存池+16个自由链表,优点:以空间换时间,缺点:内存占用比较大,如果内存有限,内存不可控,这也是早期STL提出时候不被重用的原因,那是内存较小。
源码解析
以《STL源码剖析》这本书的例子进行研究,先申请32字节空间,然后申请64字节空间,接着申请96字节空间,最后申请72字节(假设此时内存池耗尽、堆空间没有大于72字节的连续空间)
释放内存的deallocate
对应一级空间配置器,直接使用free将内存回收到堆空间。
对应二级空间配置器,直接将用完后的空间链回到相应的链表下面,使用头插法进行连接。。
阅读第三方库的方式
1、找到相应的头文件目录与实现文件目录(找源码,可以完全掌控第三方库的原理)
2、可以看看是不是有测试文件(加快该第三方库的使用)
3、需要找main函数,main函数是入口函数(找到突破口)
4、别人是怎么样使用该第三方库的。参考其中的例子
虚拟继承测试
以下测试都是基于VS,X86环境(32bit)。。
注意:虚基指针指向虚基类,虚函数指针指向虚表。。
Linux与vs的唯一区别是,在Linux下虚函数指针与虚基指针合并了
项目->(右键)属性->配置属性->C/C++->命令行
/d1 reportSingleClassLayoutXXX 或者/d1 reportAllClassLayout
测试一、虚继承与继承的区别
// 1. 多了一个虚基指针
// 2. 虚基类子对象位于派生类存储空间的最末尾(先存不变的后存共享的)
1.1、单个继承,不带虚函数
4/8
1>class B size(8):
1> +---
1> 0 | +--- (base class A)
1> 0 | | _ia
1> | +---
1> 4 | _ib
1.2、单个虚继承,不带虚函数
4/12
1>class B size(12):
1> +---
1> 0 | {vbptr}
1> 4 | _ib
1> +---
1> +--- (virtual base A)
1> 8 | _ia
1> +---
1>B::$vbtable@:
1> 0 | 0
1> 1 | 8 (Bd(B+0)A)
测试二:单个虚继承,带虚函数
// 1.如果派生类没有自己新增的虚函数,此时派生类对象不会产生虚函数指针
// 2.如果派生类拥有自己新增的虚函数,此时派生类对象就会产生自己本身的
// 虚函数指针(指向新增的虚函数),并且该虚函数指针位于派生类对象存储空间的开始位置
2.1、单个继承,带虚函数
8/12
1>class B size(12):
1> +---
1> 0 | +--- (base class A)
1> 0 | | {vfptr}
1> 4 | | _ia
1> | +---
1> 8 | _ib
1> +---
1>B::$vftable@:
1> | &B_meta
1> | 0
1> 0 | &B::f
2.2、单个继承,带虚函数(自己新增虚函数)
8/12
>class B size(12):
1> +---
1> 0 | +--- (base class A)
1> 0 | | {vfptr}
1> 4 | | _ia
1> | +---
1> 8 | _ib
1> +---
1>B::$vftable@:
1> | &B_meta
1> | 0
1> 0 | &B::f
1> 1 | &B::fb2
总结:针对2.1、2.2,普通继承,派生类新增虚函数直接放在基类虚表中;且基类布局在前面
2.3、单个虚继承,带虚函数
8/16
1>class B size(16):
1> +---
1> 0 | {vbptr} //有虚继承的时候就多一个虚基指针,虚基指针指向虚基表
1> 4 | _ib //有虚函数的时候就产生一个虚函数指针,虚函数指针指向虚函数表
1> +---
1> +--- (virtual base A)
1> 8 | {vfptr}
1>12 | _ia
1> +---
1>B::$vbtable@:
1> 0 | 0
1> 1 | 8 (Bd(B+0)A)
1>B::$vftable@:
1> | -8
1> 0 | &B::f
2.4、单个虚继承,带虚函数(自己新增虚函数)
8/20
1>class B size(20):
1> +---
1> 0 | {vfptr}
1> 4 | {vbptr}
1> 8 | _ib
1> +---
1> +--- (virtual base A)
1>12 | {vfptr}
1>16 | _ia
1> +---
1>B::$vftable@B@:
1> | &B_meta
1> | 0
1> 0 | &B::fb2
1>B::$vbtable@:
1> 0 | -4
1> 1 | 8 (Bd(B+4)A)
1>B::$vftable@A@:
1> | -12
1> 0 | &B::f
总结:2.3、2.4、虚继承多一个虚基指针,如果派生类新增虚函数,则放在最前面;且基类布局放在最后面
// 测试三:多重继承(带虚函数)
// 1、每个基类都有自己的虚函数表
// 2、派生类如果有自己新增的虚函数,会被加入到第一个虚函数表之中
// 3、内存布局中,其基类的布局按照基类被继承时的顺序进行排列
// 4、派生类会覆盖基类的虚函数,只有第一个虚函数表中存放的是真实的被覆盖的虚函数的地址;
// 其它的虚函数表中存放的并不是真实的对应的虚函数的地址,而只是一条跳转指令
3.1、普通多重继承,带虚函数,自己有新增虚函数
28
1>class Derived size(28):
1> +---
1> 0 | +--- (base class Base1)
1> 0 | | {vfptr}
1> 4 | | _iBase1
1> | +---
1> 8 | +--- (base class Base2)
1> 8 | | {vfptr}
1>12 | | _iBase2
1> | +---
1>16 | +--- (base class Base3)
1>16 | | {vfptr}
1>20 | | _iBase3
1> | +---
1>24 | _iDerived
1> +---
1>Derived::$vftable@Base1@:
1> | &Derived_meta
1> | 0
1> 0 | &Derived::f(虚函数的覆盖)
1> 1 | &Base1::g
1> 2 | &Base1::h
1> 3 | &Derived::g1(新的虚函数,直接放在基类之后,加快查找速度)
1>Derived::$vftable@Base2@:
1> | -8
1> 0 | &thunk: this-=8; goto Derived::f //虚函数表还可以存放跳转指令
1> 1 | &Base2::g
1> 2 | &Base2::h
1>Derived::$vftable@Base3@:
1> | -16
1> 0 | &thunk: this-=16; goto Derived::f
1> 1 | &Base3::g
1> 2 | &Base3::h
3.2、虚拟多重继承,带虚函数,自己有新增虚函数(只有第一个是虚继承)
32
1>class Derived size(32):
1> +---
1> 0 | +--- (base class Base2)
1> 0 | | {vfptr}
1> 4 | | _iBase2
1> | +---
1> 8 | +--- (base class Base3)
1> 8 | | {vfptr}
1>12 | | _iBase3
1> | +---
1>16 | {vbptr}
1>20 | _iDerived
1> +---
1> +--- (virtual base Base1)
1>24 | {vfptr}
1>28 | _iBase1
1> +---
1>Derived::$vftable@Base2@:
1> | &Derived_meta
1> | 0
1> 0 | &Derived::f
1> 1 | &Base2::g
1> 2 | &Base2::h
1> 3 | &Derived::g1
1>Derived::$vftable@Base3@:
1> | -8
1> 0 | &thunk: this-=8; goto Derived::f
1> 1 | &Base3::g
1> 2 | &Base3::h
1>Derived::$vbtable@:
1> 0 | -16
1> 1 | 8 (Derivedd(Derived+16)Base1)
1>Derived::$vftable@Base1@:
1> | -24
1> 0 | &thunk: this-=24; goto Derived::f
1> 1 | &Base1::g
1> 2 | &Base1::h
3.3、虚拟多重继承,带虚函数,自己有新增虚函数(三个都是虚继承)
36
1>class Derived size(36):
1> +---
1> 0 | {vfptr} //以空间换时间
1> 4 | {vbptr}
1> 8 | _iDerived
1> +---
1> +--- (virtual base Base1)
1>12 | {vfptr}
1>16 | _iBase1
1> +---
1> +--- (virtual base Base2)
1>20 | {vfptr}
1>24 | _iBase2
1> +---
1> +--- (virtual base Base3)
1>28 | {vfptr}
1>32 | _iBase3
1> +---
1>Derived::$vftable@Derived@:
1> | &Derived_meta
1> | 0
1> 0 | &Derived::g1
1>Derived::$vbtable@:
1> 0 | -4
1> 1 | 8 (Derivedd(Derived+4)Base1)
1> 2 | 16 (Derivedd(Derived+4)Base2)
1> 3 | 24 (Derivedd(Derived+4)Base3)
1>Derived::$vftable@Base1@:
1> | -12
1> 0 | &Derived::f
1> 1 | &Base1::g
1> 2 | &Base1::h
1>Derived::$vftable@Base2@:
1> | -20
1> 0 | &thunk: this-=8; goto Derived::f
1> 1 | &Base2::g
1> 2 | &Base2::h
1>Derived::$vftable@Base3@:
1> | -28
1> 0 | &thunk: this-=16; goto Derived::f
1> 1 | &Base3::g
1> 2 | &Base3::h
// 测试四:菱形虚继承
//虚基指针所指向的虚基表的内容:
// 1. 虚基指针的第一条内容表示的是该虚基指针距离所在的子对象的首地址的偏移
// 2. 虚基指针的第二条内容表示的是该虚基指针距离虚基类子对象的首地址的偏移
4.1、菱形普通继承(存储二义性)
48
class D size(48):
1> +---
1> 0 | +--- (base class B1)
1> 0 | | +--- (base class B)
1> 0 | | | {vfptr}
1> 4 | | | _ib
1> 8 | | | _cb //1
1> | | | <alignment member> (size=3) //内存对齐
1> | | +---
1>12 | | _ib1
1>16 | | _cb1
1> | | <alignment member> (size=3)
1> | +---
1>20 | +--- (base class B2)
1>20 | | +--- (base class B)
1>20 | | | {vfptr}
1>24 | | | _ib
1>28 | | | _cb
1> | | | <alignment member> (size=3)
1> | | +---
1>32 | | _ib2
1>36 | | _cb2
1> | | <alignment member> (size=3)
1> | +---
1>40 | _id
1>44 | _cd
1> | <alignment member> (size=3)
1> +---
1>D::$vftable@B1@:
1> | &D_meta
1> | 0
1> 0 | &D::f
1> 1 | &B::Bf
1> 2 | &D::f1
1> 3 | &B1::Bf1
1> 4 | &D::Df
1>D::$vftable@B2@:
1> | -20
1> 0 | &thunk: this-=20; goto D::f
1> 1 | &B::Bf
1> 2 | &D::f2
1> 3 | &B2::Bf2
4.2、菱形虚拟继承
52
1>class D size(52):
1> +---
1> 0 | +--- (base class B1)
1> 0 | | {vfptr}
1> 4 | | {vbptr}
1> 8 | | _ib1
1>12 | | _cb1
1> | | <alignment member> (size=3)
1> | +---
1>16 | +--- (base class B2)
1>16 | | {vfptr}
1>20 | | {vbptr}
1>24 | | _ib2
1>28 | | _cb2
1> | | <alignment member> (size=3)
1> | +---
1>32 | _id
1>36 | _cd
1> | <alignment member> (size=3)
1> +---
1> +--- (virtual base B)
1>40 | {vfptr}
1>44 | _ib
1>48 | _cb
1> | <alignment member> (size=3)
1> +---
1>D::$vftable@B1@:
1> | &D_meta
1> | 0
1> 0 | &D::f1
1> 1 | &B1::Bf1
1> 2 | &D::Df
1>D::$vftable@B2@:
1> | -16
1> 0 | &D::f2
1> 1 | &B2::Bf2
1>D::$vbtable@B1@:
1> 0 | -4
1> 1 | 36 (Dd(B1+4)B)
1>D::$vbtable@B2@:
1> 0 | -4
1> 1 | 20 (Dd(B2+4)B)
1>D::$vftable@B@:
1> | -40
1> 0 | &D::f
1> 1 | &B::Bf
高级篇
对于一个工程、项目
面向对象的分析(OOA):需求、做什么
面向对象的设计(OOD):怎么样做
面向对象的编程(OOP):具体实现
软件需求是一直都在变化的,想以最小的代价去满足需求的变化。
UML:统一建模语言,一个是类图、一个是序列图
1类与类之间的关系
1、继承(泛化)
可以使用空心三角箭头从派生类指向基类。
2、关联关系
代码上:数据成员使用的是指针或引用。彼此时间不会负责对方生命周期的销毁
双向的关联关系(客户与订单之间的关系)(直接使用直线连接两个类)
单向的关联关系(条件变量知道互斥锁的存在,互斥锁不知道条件变量的存在,可以使用从条件变量到互斥所以的箭头)
3、聚合
从部分指向整体的一个空心菱形箭头,类与类之前表现问整体与部分的关系,整体部分并不负责局部部分的销毁,在代码上面可以使用指针或者引用。
4、组合
从部分指向整体的实心菱形箭头,整体部分会负责局部对象的销毁,可以将局部类创建的对象作为整体的数据成员。
5、依赖
从A指向B的虚线箭头。在代码上面表现为:B作为A的成员函数参数;B作为A的成员函数的局部变量(B作为A的成员函数返回值);A的成员函数调用B的静态方法
总结
耦合:两个模块或者两个部分之间的连接关系。低耦合(让两个模块或者两个类之间的关系变得微弱一些)
1、继承表现的是类与类之间的纵向关系(垂直关系),其它四种表现的是类与类之间的横向关系
2、从耦合程度看的话:依赖 < 关联关系 < 聚合 < 组合 < 继承(泛化)
3、语义上:继承(A is B)、关联、聚合、组合(A has B)、依赖(A use B)
4、 组合+依赖(基于对象) vs 继承 + 虚函数(面向对象)
2面向对象的设计原则
总纲:低耦合(模块与模块之间,类与类之间的关系),高内聚(模块内部或者类内部的关系)
sloid
单一职责原则 (Single Responsibility Principle)、开闭原则(Open Closed Principle)、里氏替换原则 (Liscov Substitution Principle)、接口分离原则 (Interface Segregation Principle)、依赖倒置原则 (Dependency Inversion Principle)、迪米特法则(Law of Demeter -> Least Knowledge Principle)、组合复用原则 (Composite/Aggregate Reuse Principle)
1、单一职责原则
核心:一个类或者一个模块尽量只做一件事情,只有一个引起类或者模块变化的外因。
2、开闭原则
核心:对扩展开放,对修改关闭
3、里氏替换原则
核心:派生类必须能够替代基类。
4、接口分离原则
核心:使用多个小的专门的接口,而不要使用一个大的总接口
5、依赖导致原则
核心:面向接口编程,依赖于抽象(抽象是稳定的,具体的是在变化的)
在此处面向接口编程,说的就是纯虚函数,包含纯虚函数的类,称为抽象类,抽象类是稳定不变的。
在大多数情况下,开闭原则、里氏代换原则和依赖倒置原则会同时出现,开闭原则是目标,里氏代换原则是基础,依赖倒置原则是手段。
6、最少知识原则
核心:尽量减少类与类之间的耦合程度,或者模块与模块的的关系
7、组合复用原则
核心:尽量采用组合、聚合的方式而不是继承的方式来达到软件复用的目标
设计模式
简单工厂(静态工厂)
工厂方法
抽象工厂
观察者模式
定义对象的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
3线程的封装
面向对象的线程封装
1、threadFunc要设置为静态成员函数,消除this指针的影响
2、run方法如何在threadFunc中进行调用?可以在pthread_create方法中,将第四个参数使用this传进来,将this使用arg传进来,就可以在threadFunc中调用run方法,并且该run方法是可以体现多态,调用派生类run方法
基于对象的线程封装
将run方法看做一个任务,使用bind与function的方法进行实现
生产者消费者
1、原理图
2、类图