(1)strlen: 是函数,在运行时才能计算,参数必须是字符型指针,且必须是以\0结尾的,当数组名作为参数传入时,实际上数组已经退化为指针,他的功能时返回字符串的长度。
(2)sizeof: 是运算符,而不是函数,在编译时就已经计算好了,用于计算数据空间的字节数。因此sizeof不能用来返回动态分配的内存空间的大小。sizeof常用于返回的类型和静态分配的对象、结构或数组所占的空间,返回值跟对象、结构、数组所储存的内容没有关系。
strlen 的返回结果是 size_t 类型(即无符号整型),而 size_t 类型绝不可能是负的。
char sArr[] = "ILOVEC";
printf("sArr的长度=%d\n", sizeof(sArr)); //7
printf("sArr的长度=%d\n", strlen(sArr)); //6(最后一位为null)
/*****************************************/
strlen("\0") = 0;
sizeof("\0") = 2;
/*****************************************/
C语言会自动在在双引号"“括起来的内容的末尾补上”\0"代表结束,ASCII中的0号位也占用一个字符。
数组指针也称行指针,
int(*p)[n]
首先说明p是一个指针,指向一个整型的一维数组,这个一维数组的长度为n。指针数组不同于数组指针int *p[n]
这是一个整型的指针数组,他有n个指针类型的数组元素。
数组指针和指针数组的区别:数组指针只有一个指针变量,可以认为是c语言里专门用来指向二维数组的,它占用内存中一个指针的储存空间;指针数组是多个指针变量,以数组的形式储存在内存中,占用多个指针的储存空间,还需要说明的一点是,同时指向二维数组时,其直接引用和数组名引用是一样的。
int* func()
int (*func)()
函数指针:指向函数的指针变量,所以函数指针首先是一个指针变量,而且这个变量指向一个函数。C++在编译时,每一个函数都有一个入口地址,该入口地址就是函数指针所指向的地址,有了指向函数的指针变量之后,就可以用该指针变量调用函数
int(*f)(int a)
;
int foo()
{
return 5;
}
int goo()
{
return 6;
}
int main()
{
int (*funcPtr)() = foo; // funcPtr 现在指向了函数foo
funcPtr = goo; // funcPtr 现在又指向了函数goo
//但是千万不要写成funcPtr = goo();这是把goo的返回值赋值给了funcPtr
return 0;
}
引用的是一种变量类型,它用于为一个变量起一个别名。指针是一个存放地址的变量,当指针指向某个变量,这时这个里就存放了那个变量的地址。
引用和指针的区别:
(1)引用必须被初始化,指针不必;
(2)引用被初始化以后不能被改变,指针可以改变所指的对象;
(3)不存在指向空值的引用,但存在指向空值的指针;
(4)指针保存的是所指对象的地址,引用是所指对象的别名;
(5)指针通过解引用间接访问,引用是直接访问;
(6)指针更灵活,引用更安全。(比值传递高效)
首先我们要认识到,使用引用传递函数的参数时,在内存中并没有实参的副本,而是对实参直接操作。当使用传值调用时,需要给形参分配存储单元,形参变量是实参的副本,如果传递的是对象,还要调用拷贝构造函数。因此传引用调用要比传值调用效率更高,占空间更少。
使用指针作为函数的参数也可以达到引用同样的效果,但是在被调函数中同样要给形参分配存储单元,在这个意义上说,引用的效率更高。而且频繁使用“*指针变量名”的形式进行运算容易产生错误而且可阅读性较差。因此引用是个更安全高效的选择。
int a[3] = {1,2,3};
&a+1地址与&a相比,偏移了12个字节,即声明数组的空间大小;
a+1地址与a相比,偏移了4个字节,即数组中一个元素的空间大小;
&a[0]+1地址与&a[0]相比,偏移了4个字节,即数组中一个元素的空间大小;
如果既要提高程序的效率,又要使传递给函数的数据不在函数里被改变,可以使用常引用。
const typename & 引用名 = 变量名 const int & a = b ;
用这种方式声明的引用,不能通过引用对目标变量的值进行修改。保证了引用的安全性。
引用在可以被定义为const的情况下,应当尽量被定义成const。
野指针指向的位置是不可知的。
野指针不同于空指针,空指针是指一个指针的值为null,而野指针的值并不为null,野指针会指向一段实际的内存,只是它指向哪里我们并不知情,或者是它所指向的内存空间已经被释放,所以在实际使用的过程中,我们并不能通过指针判空去识别一个指针是否为野指针。
指针初始化
c中使用宏#define定义,c++使用const定义。const是有数据类型的常量,而宏没有。编译器对const进行静态类型安全检查,对#define仅仅是字符替换,不进行安全检查,而且在字符替换时会产生意想不到的错误。有些编译器可以对const常量进行调试,而不能对宏进行调试。
c++无法代替宏作为卫哨,防止文件重复包含
const int a = 3;
int* b = (int*) &a; // 通过强制类型转换得到a所在的内存地址
*b = 5;
#include
using namespace std;
int func()
{
int a = 9;
return a;
}
const int fun()
{
return 9;
}
int main()
{
int c = 9;
const int a = func();
const int d = 20;
//d = c ; error
//int *p1 = &c ; error
int b = fun();
const int* p = &c;
c = 10; //改变c的值可以改变*p的值
//int *p2 = p ;
cout << a << endl;
cout << b << endl;
cout << *p << endl; //10
return 0;
}
int main() {
int m = 10;
const int n = 20; // 必须在定义的同时初始化
const int *ptr1 = &m; // 指针指向的内容不可改变 ||底层const
int * const ptr2 = &m; // 指针不可以指向其他的地方 ||顶层const
ptr1 = &n; // 正确
ptr2 = &n; // 错误,ptr2不能指向其他地方
*ptr1 = 3; // 错误,ptr1不能改变指针内容
*ptr2 = 4; // 正确
int *ptr3 = &n; // 错误,常量地址不能初始化普通指针吗,常量地址只能赋值给常量指针
const int * ptr4 = &n; // 正确,常量地址初始化常量指针
int * const ptr5; // 错误,指针常量定义时必须初始化
ptr5 = &m; // 错误,指针常量不能在定义后赋值
const int * const ptr6 = &n; // 指向“常量”的指针常量,具有常量指针和指针常量的特点,指针内容不能改变,也不能指向其他地方,定义同时要进行初始化
*ptr6 = 5; // 错误,不能改变指针内容
ptr6 = &n; // 错误,不能指向其他地方
const int * const ptr9 = &m;
const int * ptr7; // 正确
ptr7 = &m; // 正确
int* const ptr8 = &m;//error: invalid conversion from 'const int*' to 'int*'
return 0;
}
const用于定义常量,define用于定义宏,也可以定义常量,当两者都用于定义常量时,区别为:
对于C语言的全局和静态变量,初始化发生在任何代码执行之前,属于编译期初始化。
而C++标准规定:全局或静态对象当且仅当对象首次用到时才进行构造。
1. 修饰全局变量。该变量只能在该文件中使用,其他文件不可访问,存放在静态存储区。
2. 修饰局部变量。该变量作用域只在该局部函数里,出了函数静态局部变量不会被释放,如果未初始化默认会初始化为0。存放在静态存储区。
3. 修饰静态函数。在函数返回类型前加static,函数就定义为静态函数。函数的定义和声明在默认情况下都是extern的,但静态函数只是在声明他的文件当中可见,不能被其他文件所用。
4. 修饰成员变量,该变量为所有类对象共享,不需要this指针,并且不能和const一起使用,因为const需要this指针。
5. 修饰成员函数,用命名空间表示。
静态非常量数据成员只能在类外初始化;
非静态的常量数据成员不能在类内初始化,也不能在构造函数中初始化,而只能且必须在构造函数的初始化列表中初始化;
非静态的非常量数据成员不能在类内初始化,可以在构造函数中初始化,也可以在构造函数的初始化列表中初始化
类中static不能和const一起修饰成员函数**
static和const分别怎么用
(1)从静态储存区域分配内存:内存在编译时就分配好了,这块内存在程序的整个运行期间都存在,如全局变量,static变量
(2)在栈上创建:在执行函数时,函数内的局部变量的存储单元都可以在栈上创建,函数执行结束时这些储存单元被自动释放。栈分配内存运算内置于处理器指令集中,效率很高,但是分配内存容量有限
(3)从堆上分配(动态分配):程序在运行时用malloc或者new申请任意多少内存,程序员负责在何时free或delete释放内存,动态内存生存期自己决定,使用灵活。
(1)alloca是向栈申请内存,因此无需释放
(2)malloc分配的内存是位于堆中,并且没有初始化内存的内容,因此基本上malloc之后,调用函数memset来初始化这部分的内存空间
(3)calloc则将初始这部分的内存,设置为0
(4)realloc则对malloc申请的内存进行大小的调整
(5)申请的内存最终需要通过函数free来释放
当程序运行过程中malloc了,但是没有free的话,会造成内存泄漏.一部分的内存没有被使用,但是由于没有free,因此系统认为这部分内存还在使用,造成不断的向系统申请内存,使得系统可用内存不断减少.但是内存泄漏仅仅指程序在运行时,程序退出时,OS将回收所有的资源.因此,适当的重起一下程序,有时候还是有点作用.
三个函数声明分别是:
void* malloc(unsigned size);
void* realloc(void* ptr , unsignde newsize);
void* calloc(size_t numElements,size_t sizeOfElements);
/**********************/
char *str;
str = (char *) malloc(15);
/**********************/
都在stdlib.h函数库内,它们的返回值都是请求系统分配的地址,如果请求失败就返回NULL.
在内存的动态存储区中分配一块长度为size字节的连续区域,参数size为需要内存空间的长度,返回该区域的首地址
与malloc相似,参数sizeOfElement为申请地址的单位元素长度,numElements为元素个数,即在内存中申请numElements*sizeOfElement字节大小的连续地址空间.
给一个已经分配了地址的指针重新分配空间,参数ptr为原有的空间地址,newsize是重新申请的地址长度.
(1)函数malloc()不能初始化所分配的内存空间,而函数calloc能。如果由malloc()函数分配的内存空间原来没有被使用过,则其中的每一位可能都是0;反之, 如果这部分内存曾经被分配过,则其中可能遗留有各种各样的数据。函数calloc() 会将所分配的内存空间中的每一位都初始化为零,也就是说,如果你是为字符类型或整数类型的元素分配内存,那么这些元素将保证会被初始化为0;如果你是为指针类型的元素分配内存,那么这些元素通常会被初始化为空指针;如果你为实型数据分配内存,则这些元素会被初始化为浮点型的零.
(2)malloc()向系统申请分配指定size个字节的内存空间。返回类型是void类型。void表示未确定类型指针。
(3)realloc可以给指针指定空间进行扩大或者缩小,内容不变或者相应缩小
(4)realloc是从堆上分配内存,当扩大一块内存空间时,realloc()试图直接从堆上现存的数据后面的那些字节中获得附加的字节,如果能够满足,自然天下太平;如果数据后面的字节不够,问题就出来了,那么就使用堆上第一个有足够大小的自由块,现存的数据然后就被拷贝至新的位置,而老块则放回到堆上.这句话传递的一个重要的信息就是数据可能被移动.
C++内存四大区域
从低地址到高地址,一个程序由代码段、数据段、BSS段组成
函数调用所用栈部分叫做栈帧指针,帧指针(ebp起始)、栈指针(esp栈顶),函数访问都是基于帧指针。
栈帧指针一般都有专门的寄存器,通常使用ebp寄存器作为帧指针,使用esp寄存器作为栈指针。帧指针指向栈帧结构的头,存放着上一个帧栈的头部结构,栈指针指向栈顶。
int a = 0; //全局初始化区
char *p1; //全局未初始化区
void main()
{
int b; //栈
char s[] = "abc"; //栈
char *p2; //栈
char *p3 = "123456"; //123456{post.content}在常量区,p3在栈上
static int c = 0; //全局(静态)初始化区
p1 = (char *)malloc(10); //分配得来得10字节的区域在堆区
p2 = (char *)malloc(20); //分配得来得20字节的区域在堆区
strcpy(p1, "123456");
//123456{post.content}放在常量区,编译器可能会将它与p3所指向的"123456"优化成一块
}
全局变量、文件域的静态变量和类的静态成员变量在main执行之前的静态初始化过程中分配内存并初始化;局部静态变量(一般为函数内的静态变量)在第一次使用时分配内存并初始化。这里的变量包含内置数据类型和自定义类型的对象。
内存碎片通常分为内部碎片和外部碎片:
解决方法:
malloc 底层实现及原理
malloc函数用于动态分配内存。为了减少内存碎片和系统调用的开销,malloc其采用内存池的方式,先申请大块内存作为堆区,然后将堆区分为多个内存块,以块作为内存管理的基本单位。当用户申请内存时,直接从堆区分配一块合适的空闲块。Malloc采用隐式链表结构将堆区分成连续的、大小不一的块,包含已分配块和未分配块;同时malloc采用显式链表结构来管理所有的空闲块,即使用一个双向链表将空闲块连接起来,每一个空闲块记录了一个连续的、未分配的地址。
Linux进程分配内存的两种方式–brk() 和mmap()
new/delete的实现原理:
new和delete是用户进行动态内存申请和释放的操作符,operator new 和operator delete是系统提供的全局函数,
new在底层调用operator new全局函数来申请空间;
delete在底层通过operator delete全局函数来释放空间;
内存池也是一种对象池,我们在使用内存对象之前,先申请分配一定数量的内存块留作备用。当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够用再继续申请新的内存。当不需要此内存时,重新将此内存放入预分配的内存块中,以待下次利用。这样合理的分配回收内存使得内存分配效率得到提升。
内存泄漏的分类:
堆内存泄漏 (Heap leak)。对内存指的是程序运行中根据需要分配通过malloc,realloc new等从堆中分配的一块内存,再是完成后必须通过调用对应的 free或者delete 删掉。如果程序的设计的错误导致这部分内存没有被释放,那么此后这块内存将不会被使用,就会产生Heap Leak.
系统资源泄露(Resource Leak)。主要指程序使用系统分配的资源比如 Bitmap,handle ,SOCKET等没有使用相应的函数释放掉,导致系统资源的浪费,严重可导致系统效能降低,系统运行不稳定。
没有将基类的析构函数定义为虚函数。当基类指针指向子类对象时,如果基类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有正确是释放,因此造成内存泄露。
有以下几种避免方法:
第一:良好的编码习惯,使用了内存分配的函数,一旦使用完毕,要记得使用其相应的函数释放掉。
第二:将分配的内存的指针以链表的形式自行管理,使用完毕之后从链表中删除,程序结束时可检查改链表。
第三:使用智能指针。
第四:一些常见的工具插件可以帮助检测内存泄露,如ccmalloc、Dmalloc、Leaky、Valgrind等等。
在arm-linux-gcc这个开发环境中,如果全局变量的初始值是0,编译器会将该全局变量放在BSS段。
(1)生命周期不一样:全局变量随主程序的创建而被创建,随主程序的销毁而销毁。局部变量在局部函数内部,退出就不存在了。
(2)使用方式不一样:通过声明后,全局变量可以再各个部分进行调用,局部变量只能在局部使用,分配在堆栈
操作系统和编译系统是通过内存分配位置知道的,全局变量分配在全局数据段并且在程序运行时候被加载,局部变量分配在堆栈里面
(1) 使用全局变量会占用大量的内存(生命周期长)
(2) 使用大量的全局变量容易造成名字冲突
(3) 当出现问题时,很难定位问题来源
不使用static的时候,两个不同的源文件都可以正常编译,但会出现链接错误,原因是有两个地方存在相同的变量,导致编译器无法识别应该使用哪一个。
关于全局变量的几点说明:
举例说明:项目文件夹project下有main.c、common.c和common.h三个文件,其中common.h文件分别#include在main.c和common.c文件中。现在希望声明一个字符型变量key,在main.c和common.c中公用。
有人想,既然是想两个文件都用,那就在common.h中声明一个unsigned char key,然后由于包含关系,在main.c和common.c中都是可见的,所以就能共用了。
想起来确实有道理,但是实际写出来,我们发现编译的时候编译器提示出错,一般提示大概都类似于:Error: L6200E: Symbol key multiply defined (by common.o and main.o).也就是说编译器认为我们重复定义了key这个变量。这是因为#include命令就是原封不同的把头文件中的内容搬到#include的位置,所以相当于main.c和common.c中都执行了一次unsigned char key,而C语言中全局变量是项目内(或者叫工程内)可见的,这样就造成了一个项目中两个变量key,编译器就认为是重复定义。
正确的解决办法:使用extern关键字来声明变量为外部变量。具体说就是在其中一个c文件中定义一个全局变量key,然后在另一个要使用key这个变量的c文件中使用extern关键字声明一次,说明这个变量为外部变量,是在其他的c文件中定义的全局变量。请注意我这里的用词:定义和声明。例如在main.c文件中定义变量key,在common.c文件中声明key变量为外部变量,这样这两个文件中就能共享这个变量key了。
虽然在代码中好像使用了相同的变量,但是实际上使用的是不同的变量,在每个源文件中都有单独的变量。所以,在头文件中定义static变量会造成变量多次定义,造成内存空间的浪费,而且也不是真正的全局变量。
虚函数
子类重写父类方法时,不能降低访问权限,只能提高访问权限。
(1)继承:子类继承父类的特征和行为,使得子类具有父类的各种属性和方法。或者子类从父类继承方法,使得子类具有父类相同的行为。
(2)多态:具有不同功能的函数可以用同一个函数名,这样就可以用同一个函数名调用不同内容的函数。在面向对象中,多态是指:向不同对象发同一个消息,不同的对象在接收时会产生不同的行为,即每个对象可以用自己的方式去响应共同的消息。
多态是指“一个接口,多种方法”,多态性分为两类:静态多态性和动态多态性。函数重载和运算符重载实现的多态属于静态多态性,动态多态性是通过虚函数实现的。静态多态性:在程序编译时,系统会决定调用那个函数,因此静态多态性又称为编译对象。动态多态性:在程序运行过程中才动态都确定操作所针对的对象,他又称运行时的多态性。类中有虚函数存在,所以编译器会为他添加一个vptr指针,并为他们分别创建一个vtbl,vptr指向那个表。每个类都有自己的虚函数表,虚函数表的作用就是保存自己类中虚函数的地址,我们可以把虚函数表形象的看成是一个数组,这个数组的每个元素存放的是虚函数的地址,不同的vptr指向不同的虚函数表,不同的虚函数表装着对应类的虚函数地址,这样虚函数就可以完成它的任务。子类重写虚函数的地址,直接替换服了虚函数在虚函数表中的位置,因此访问虚函数表时,表中是谁就访问谁。
注意:(1)存在虚函数的类都有一个一维的虚函数表叫虚表,类的对象有一个指向虚表开始的虚指针。虚表和类对应,虚表指针和对象对应
(2)对于虚函数调用来说,每一个对象内部都有一个虚表指针,该虚表指针初始化为本类的虚表,所以程序中,不管你的对象类型如何转换,但是该对象内部的虚表指针是固定的,所以才能实现动态的对象函数调用,这就是c++多态的实现原理。
(1)用于实现多态: 允许在派生类中重新定义与基类同名函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数,允许派生类中对基类虚函数的重新定义
(2)虚函数在设计上还有抽象和封装的作用
virtual是C++面向对象机制中很重要的一个关键字,类中加关键字virtual的函数被称为虚函数,基类的派生类可以通过重写虚函数,实现对基类虚函数的覆盖。
如果基类的析构函数不是虚函数,删除指针时,只有基类的内存被释放,派生类的没有释放,会造成内存泄漏
每个类的实例化对象都会拥有虚函数指针并且都排列在对象的地址首部。而它们也都是按照一定的顺序组织起来的,从而构成了一种表状结构,称为虚函数表 (virtual table) 。
首先基函数的表项仍然保留,而得到正确继承的虚函数其指针将会被覆盖,而子类自己的虚函数将跟在表后。而当多重继承的时候,表项将会增多,顺序会体现为继承的顺序,并且子函数自己的虚函数将跟在第一个表项后。
C++中一个类是公用一张虚函数表的,基类有基类的虚函数表,子类是子类的虚函数表,这极大的节省了内存
包含有虚函数的类通常有一个虚表指针(在32位机器上),大小为4
class Base1
{
public:
int base1_1;
int base1_2;
virtual void base1_fun1() {}
virtual void base1_fun2() {}
};
class Base2
{
public:
int base2_1;
int base2_2;
virtual void base2_fun1() {}
virtual void base2_fun2() {}
};
// 多继承
class Derive1 : public Base1, public Base2
{
public:
int derive1_1;
int derive1_2;
// 基类虚函数覆盖
virtual void base1_fun1() {}
virtual void base2_fun2() {}
// 自身定义的虚函数
virtual void derive1_fun1() {}
virtual void derive1_fun2() {}
};
初步了解一下对象大小及偏移信息:
sizeof(Base1) | 12 |
---|---|
sizeof(Base1) | 12 |
sizeof(Derive1) | 32 |
offsetof(Derive1.derive1_1) | 24 |
offsetof(Derive1.derive1_2) | 28 |
class Base1
{
public:
int base1_1;
int base1_2;
};
class Base2
{
public:
int base2_1;
int base2_2;
virtual void base2_fun1() {}
virtual void base2_fun2() {}
};
// 多继承
class Derive1 : public Base1, public Base2
{
public:
int derive1_1;
int derive1_2;
// 自身定义的虚函数
virtual void derive1_fun1() {}
virtual void derive1_fun2() {}
};
sizeof(Base1) | 8 |
---|---|
sizeof(Base1) | 12 |
sizeof(Derive1) | 28 |
offsetof(Derive1.derive1_1) | 20 |
offsetof(Derive1.derive1_2) | 24 |
(1)为每一个包含虚函数的类设置一个虚表(VTABLE)每当创建一个包含虚函数的类或者包含虚函数的类的派生类,编译器会为这个类创建一个VTABLE。在VTABLE中,编译器放置在这个类中,或者它的基类中所有已经声明为virtual的函数地址。如果在这个派生类中没有对基类中声明为virtual的函数进行重新定义,编译器就会使用基类这个虚函数的地址。而且所有VTABLE中虚函数地址的顺序完全相同。
初始化虚指针(VPTR)然后编译器在这个类的各个对象放置VPTR。VPTR在对象的相同位置(通常都在对象的开头)。VPTR必须被初始化为指向相应的VTABLE。
为虚函数调用插入代码:当通过基类指针调用派生类的虚函数时,编译器将在调用处插入相应代码,以实现通过VPTR找到VTABLE,并根据VTABLE中储存的正确的虚函数地址,访问正确的函数。
因为在类设计时,虚函数表直接从基类继承过来,如果覆盖了其中某些虚函数,那么虚函数的指针就会被替换,因此可以根据指针查找调用了哪个函数
不是虚函数不能实现多态
纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加
=0
:
virtual void funtion1()=0
1.为了方便使用多态特性
2、在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。为了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数(方法:
virtual ReturnType Function()= 0
;),则编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。这样就很好地解决了上述两个问题。声明了纯虚函数的类是一个抽象类。所以,用户不能创建类的实例,只能创建它的派生类的实例。
1.它们必须在继承类中重新声明函数(不要后面的=0,否则该派生类也不能实例化),而且它们在抽象类中往往没有定义。定义纯虚函数的目的在于,使派生类仅仅只是继承函数的接口。
2.纯虚函数的意义:让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但类无法为纯虚函数提供一个合理的默认实现。所以类纯虚函数的声明就是在告诉子类的设计者,“你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它”。
1.虚函数和纯虚函数可以定义在同一个类中,含有纯虚函数的类被称之为抽象类,只含有虚函数的类不能称之为抽象类
2.虚函数可以直接使用,也可以被子类重载以后以多态形式调用,而纯虚函数必须在子类中实现该函数才可以使用,因为纯虚函数在基类中只有声明没有定义。
3.虚函数和纯虚函数都可以在子类中被重载,以多态形式调用
4.虚函数和纯虚函数通常存在于抽象基类当中,被继承的子类重载,目的是提供一个统一的接口
5.虚函数的定义形式: vitual{method body}
纯虚函数的定义形式: vitual{} = 0;
6.虚函数必须实现,如果不实现,编译器会报错。
7.纯虚函数不能被实例化
1.定义一个函数为虚函数,不代表函数为不被实现的函数。
2.定义他为虚函数是为了允许用基类的指针来调用子类的这个函数。
3.定义一个函数为纯虚函数,才代表函数没有被实现。
4.定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。
class A
{
public:
virtual void foo()
{
cout<<"A::foo() is called"<foo(); // 在这里,a虽然是指向A的指针,但是被调用的函数(foo)却是B的!
return 0;
}
这个例子是虚函数的一个典型应用,通过这个例子,也许你就对虚函数有了一些概念。它虚就虚在所谓"推迟联编"或者"动态联编"上,一个类函数的调用并不是在编译时刻被确定的,而是在运行时刻被确定的。由于编写代码的时候并不能确定被调用的是基类的函数还是哪个派生类的函数,所以被成为"虚"函数。
虚函数只能借助于指针或者引用来达到多态的效果。
(1) C语言通过宏编译实现编译时多态;
(2) C语言可以通过函数指针实现动态多态。
纯虚函数不能被实例化:虚函数的原理采用虚函数表,类中含有纯虚函数时,其虚函数表不完全,有个空位,即纯虚函数在类的虚函数表中对应的表项被赋值为0,也就是指向一个不存在的函数,由于编译器绝对不允许调用一个不存在函数的可能,所以该类不能生成对象,在他的派生类中,除非重写函数,否则不能生成对象。所以纯虚函数不能实例化。
virtual ~myclass() = 0;
为了实现多态进行动态绑定,将派生类对象指针绑定到基类指针上,对象销毁时,如果析构函数没有定义为虚函数,则会调用基类的析构函数,显然只能销毁部分数据,如果调用对象的析构函数,就需要将该对象的析构函数定义为虚函数,销毁时通过虚函数表来找到对应的析构函数。
(1)如果析构函数抛出异常,则异常点之后不会执行,如果析构函数在异常点之后执行必要的动作,比如释放某些资源,则这些动作不会执行,会造成诸如资源泄露的问题
(2)通常异常发生时,C++会调用析构函数来释放资源,若此时析构函数本身也抛出异常,则前一个异常尚未处理,又有新异常,会造成程序崩溃。
虚函数对应一个虚函数表,虚函数表其实是存储在对象的内存空间的。如果构造函数是虚的,就需要通过 虚函数表来调用,可是对象还没有实例化,也就是内存空间还没有,就没有虚函数表,所以构造函数不能是虚函数。
虚函数的作用在于通过父类的指针或者引用来调用它的时候能够变成调用子类的那个成员函数。而构造函数是在创建对象时自动调用的,不可能通过父类的指针或者引用去调用,因此也就规定构造函数不能是虚函数。
析构函数可以使用虚函数,而且在复杂的类中,这往往是必须的。析构函数也可以是纯虚函数,但是纯虚函数必须有定义体,因为析构函数的调用是子类隐含的。
不可以,string是模板类,是类,不适合继承。
解释1:
编译器都期望在处理类的定义时就能确定这个类的虚函数表的大小,如果有类的虚函数模板函数,那么就必须要求编译器提前知道程序中所有对该类的该虚成员模板函数的调用,而这是不可行的。
解释2:
(1) 在实例化模板类时,需要创建virtual table。在模板类被实例化完成之前不能确定函数模板(包括虚函数模板,加入支持的话)会被实例化多少个。
(2) 普通成员函数模板无所谓,什么时候需要什么时候就给你实例化,编译器不用知道到底需要实例化多少个,虚函数的个数必须知道,否则这个类就无法被实例化(因为要创建virtual table)。因此,目前不支持虚函数模板。
C++的编译器应该是保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证取到虚函数表的有最高的性能——如果有多层继承或是多重继承的情况下)。 这意味着我们通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。 目前gcc 和微软的编译器都是将vptr放在对象内存布局的最前面。
虽然我们知道vptr指向虚函数表,那么虚函数表具体存放在内存哪个位置呢,虽然这里我们已经可以得到虚函数表的地址。实际上虚函数指针是在构造函数执行时初始化的,而虚函数表是存放在可执行文件中的。
使用指针访问虚函数表
C++ 基类指针和派生类指针之间的转换
static_cast和dynamic_cast一般用于基类指针和子类指针之间的类型转换
Base *P = new Derived();
Derived *pd1 = static_cast<Derived *>(P);
Derived *pd2 = dynamic_cast<Derived *>(P);
以上转换都能成功。
但是,如果 P 指向的不是子类对象,而是父类对象,如下所示:
Base *P = new Base;
Derived *pd3 = static_cast<Derived *>(P);
Derived *pd4 = dynamic_cast<Derived *>(P);
在以上转换中,static_cast转换在编译时不会报错,也可以返回一个子类对象指针(假想),但是这样是不安全的,在运行时可能会有问题,因为子类中包含父类中没有的数据和函数成员,这里需要理解转换的字面意思,转换是什么?转换就是把对象从一种类型转换到另一种类型,如果这时用 pd3 去访问子类中有但父类中没有的成员,就会出现访问越界的错误,导致程序崩溃。而dynamic_cast由于具有运行时类型检查功能,它能检查P的类型,由于上述转换是不合理的,所以它返回NULL。
包含虚函数的类的起始地址处保存的是虚函数表的地址,这个地址值是由类的构造函数填写进去的。
在生成派生类Derive的实例时,由Derive的构造函数来调用Base的构造函数先完成基类Base的构建。Base的构造函数中调用虚函数Foo,此时从虚函数表中获取的只能是Base类的虚函数表的地址,因此虚函数Foo绑定的是Base类的Foo,只能执行Base的Foo。
在生成派生类Derive的实例时,Derive的this指针指向的地址其实首先被Base的构造函数填充一次,然后又被Derive的构造函数填充一次。
结论: 基类部分在派生类部分之前被构造,当基类构造函数执行时派生类中的数据成员还没被初始化。如果基类构造函数中的虚函数调用被解析成调用派生类的虚函数,而派生类的虚函数中又访问到未初始化的派生类数据,将导致程序出现一些未定义行为和bug。
(1) new/delete是操作符,可以重载,只能在C++中使用,malloc/free是函数,可以覆盖,C和C++都可以使用
(2) new可以调用对象的构造函数,对应的delete可以调用相应的析构函数
(3) malloc仅仅分配内存,free仅仅回收内存,并不执行构造函数和析构函数
(4) new/delete返回的是某种数据类型的指针,malloc/free返回的是void指针
(5) new自动计算需要分配的内存空间,malloc需要手动计算
(6) malloc/free需要库文件支持,new/delete则不需要
(7) malloc分配内存不够时可以扩容,new没有这种功能
(8) new分配失败时会抛出异常,malloc失败会返回null
(9) new分配内存的位置可以是堆,也可以是静态储存区,malloc分配内存的位置是堆
(10) 在处理数组上,new有处理数组的版本new[ ] , malloc需要计算数组大小后进行内存分配
malloc/free具体操作方式:假设你用malloc需要申请100字节,实际是申请了104个字节。把前4字节存成该块内存的实际大小,并把前4字节后的地址返回给你。 free释放的时候会根据传入的地址向前偏移4个字节 从这4字节获取具体的内存块大小并释放。(实际上的实现很可能使用8字节做为头部:其中每四个字节分别标记大小和是否正在使用)
/ ************************************* /
深入理解C++ new/delete, new []/delete[]动态内存管理
/ ************************************* /
delete只会调用一次析造函数,因此delete基本类型的指针和数组都是可以的。
delete[ ]对每个成员调用一次析构函数,主要针对A *a = new A[10]
这种类对象的销毁,delete只会销毁a[0],后面就会产生内存泄漏。
(1) 储存形式:数组是一块连续的空间,声明时要确定长度,链表是一块可以不连续的动态空间,长度可变,每个节点要保留相邻节点的指针
(2) 数据查找:数组的线性查找速度快,链表需要按节点遍历,效率低
(3) 数据的插入删除:链表可以快速的插入删除,数组需要进行大量的数据搬移
(4) 越界问题:链表不存在越界问题,数组存在
(1) 默认的继承访问权限:struct默认为public,class默认为private
(2) 模板为C++语言新增特性,C语言没有,只有class可以用于定义参数,而struct不可以
四种类型转换
静态转换,(1)主要用于内置数据类型之间的相互转换;(2)用于自定义类时,静态转换会判断转换类型之间的关系,如果转换类型之间没有任何关系,则编译器会报错,不可转换;(3)把void类型指针转为目标类型指针(不安全)。
//static_cast.cpp
//内置类型的转换
double dValue = 12.12;
float fValue = 3.14; // VS2013 warning C4305: “初始化”从“double”到“float”截断
int nDValue = static_cast<int>(dValue); // 12
int nFValue = static_cast<int>(fValue); // 3
//自定义类的转换
class A{};
class B : public A{};
class C{};
void main(){
A *pA = new A;
B *pB = static_cast<B*>(pA); // 编译不会报错, B类继承于A类
pB = new B;
pA = static_cast<A*>(pB); // 编译不会报错, B类继承于A类
C *pC = static_cast<C*>(pA); // 编译报错, C类与A类没有任何关系。error C2440: “static_cast”: 无法从“A *”转换为“C *”
}
const_cast 比较好理解,它用来去掉表达式的 const 修饰或 volatile 修饰。换句话说,const_cast 就是用来将 const/volatile 类型转换为非 const/volatile 类型。只能改变运算对象的底层const。
用于动态类型转换,只能用于含有虚函数的类,用于类层次间向上和向下转换。只能转指针或引用。向下转化时,如果是非法的对于指针返回NULL,对于引用则抛出异常。其他三种都是编译时完成的,dynamic_cast是运行时处理的,运行时需要进行类型检查。不能用于内置的基本类型之间的强制转换。使用dynamic_cast进行转换的,基类一定有虚函数,否则编译不通过。
有条件转换,动态类型转换,运行时检查类型安全(转换失败返回NULL):
1)安全的基类和子类之间的转换。
2)必须有虚函数。
3)相同基类不同子类之间的交叉转换,但结果返回NULL。
class Base {
public:
int _i;
virtual void foo() {}; //基类必须有虚函数。保持多态特性才能使用dynamic_cast
};
class Sub : public Base {
public:
char *_name[100];
void Bar() {};
};
int main() {
Base* pb = new Sub();
Sub* ps1 = static_cast<Sub*>(pb); //子类->父类,静态类型转换,正确但不推荐
Sub* ps2 = dynamic_cast<Sub*>(pb); //子类->父类,动态类型转换,正确
Base* pb2 = new Base();
Sub* ps21 = static_cast<Sub*>(pb2); //父类->子类,静态类型转换,危险!访问子类_name成员越界
Sub* ps22 = dynamic_cast<Sub*>(pb2);//父类->子类,动态类型转换,安全,但结果为NULL
return 0;
}
几乎什么都可以转,比如讲int转换成指针,可能会出问题,尽量少用
C的强制转换表面上看起来很强大,什么都能转,但是转化不够明确,不能进行错误检测,容易出错。
(1) 从机制上:c是面向过程的,c++是面向对象的,提供了类,c++编写面向对象的程序比c容易
(2) 从适用的方向:c 适合要求代码体积小的,效率高的场合,如嵌入式;c++更适合上层,复杂的,Linux核心大部分是c写的,因为他是系统软件,效率要求极高。
(3) C语言是结构化编程语言,C++是面向对象编程语言
(4) C++侧重于对象而不是过程,侧重于类的设计而不是逻辑设计
- 作为一种面向过程的结构化语言,易于调试和维护;
- 表现能力和处理能力极强,可以直接访问内存的物理地址;
- C语言实现了对硬件的编程操作,也适合于应用软件的开发;
- C语言还具有效率高,可移植性强等特点。
- 在C语言的基础上进行扩充和完善,使C++兼容了C语言的面向过程特点,又成为了一种面向对象的程序设计语言;
- 可以使用抽象数据类型进行基于对象的编程;
- 可以使用多继承、多态进行面向对象的编程;
- 可以担负起以模版为特征的泛型化编程。
在于C++是面向对象的,而C语言是面向过程的。或者说C++是在C语言的基础上增加了面向对象程序设计的新内容,是对C语言的一次更重要的改革,使得C++成为软件开发的重要工具。
面向过程是直接将解决问题的步骤分析出来,然后用函数把步骤一步一步实现,然后再依次调用就可以了;而面向对象是将构成问题的事物,分解成若干个对象,建立对象的目的不是为了完成一个步骤,而是为了描述某个事物在解决问题过程中的行为。
面向过程思想偏向于我们做一件事的流程,首先做什么,其次做什么,最后做什么。
面向对象思想偏向于了解一个人,这个人的性格、特长是怎么样的,有没有遗传到什么能力,有没有家族病史。
Extern可以置于变量或者函数前,以标志变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数在其他模块中找到其他定义,此外也可用于连接指定。
extern 声明变量,说明变量将在文件以外或在文件后面部分定义
extern 是声明函数,暗示这个函数可能在别的源文件里面定义,没有其他作用
extern “C”是由C++提供的链接交换的指定符号,用于告诉C++这段代码是C函数,加上extern “C”后,C++可以直接调用C函数,使用extern ”c”可以实现C++与C及其他语言的混合编程。由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包括函数名
访问寄存器要比访问内存快,因为CPU会优先访问该数据在寄存器中存储的结果,但是内存中的数据可能已经发生了改变,而寄存器中还保留着原来的结果。为了避免这种情况的发生,将该变量声明为volatile,告诉CPU 每次都从内存去读取数据
可以,一个例子是只读状态寄存器,是volatile是因为他可能被意想不到的被改变,是const告诉程序不应该试图去修改它。
- 在本次线程内, 当读取一个变量时,为提高存取速度,编译器优化时有时会先把变量读取到一个寄存器中;以后,再取变量值时,就直接从寄存器中取值;当变量值在本线程里改变时,会同时把变量的新值copy到该寄存器中,以便保持一致。
- 当变量在因别的线程等而改变了值,该寄存器的值不会相应改变,从而造成应用程序读取的值和实际的变量值不一致。
- 当该寄存器在因别的线程等而改变了值,原变量的值不会改变,从而造成应用程序读取的值和实际的变量值不一致。
volatile应该解释为“直接存取原始内存地址”比较合适,“易变的”这种解释简直有点误导人。
//当调用max(1, 2);时无法确定调用的是哪个,单从这一点上来说,仅返回值类型不同的重载是不应该允许的。
float max(int a, int b);
int max(int a, int b);
是指派生类函数屏蔽了与其名字相同的基类函数,调用函数取决于指向他的指针所声明的类型,规则如下:
指针变量声明时没有初始化: 解决办法:指针声明时初始化,可以是具体的地址值,也可以是NULL
指针变量被free或delete后没有指向NULL: 解决办法:指针指向的内存空间被释放后,指针应该指向NULL
指针操作超过了变量的作用范围: 解决办法:在变量作用域结束前,释放掉变量的地址空间,并让指针指向NULL
调用的开销:函数的栈帧的开辟和回退
执行的开销:函数体内代码执行的开销
编译阶段不编译.h文件,只编译.c 或.cpp 文件
概念1:
左值:可以放到等号左边的东西叫左值。
右值:不可以放到等号左边的东西就叫右值。
概念2:
左值:可以取地址并且有名字的东西就是左值。
右值:不能取地址的没有名字的东西就是右值。
int a = b + c;
a 是左值,a 有变量名,也可以取地址,可以放到等号左边, 表达式b+c 的返回值是右值,没有名字且不能取地址,
&(b+c)
不能通过编译,而且也不能放到等号左边。
int a = 4; // a 是左值,4 作为普通字面量是右值
左值一般有:
- 函数名和变量名
- 返回左值引用的函数调用
- 前置自增自减表达式++i、–i
- 由赋值表达式或赋值运算符连接的表达式(a=b, a += b 等)
- 解引用表达式*p
- 字符串字面值"abcd"
纯右值和将亡值都属于右值。
运算表达式产生的临时变量、不和对象关联的原始字面量、非引用返回的临时变量、lambda 表达式等都是纯右值。
举例:
将亡值是指C++11 新增的和右值引用相关的表达式,通常指将要被移动的对象、T&&函数的返回值、std::move 函数的返回值、
转换为T&&类型转换函数的返回值,将亡值可以理解为即将要销毁的值,通过“盗取”其它变量内存空间方式获取的值,在确保
其它变量不再被使用或者即将被销毁时,可以避免内存空间的释放和分配,延长变量值的生命周期,常用来完成移动构造或者
移动赋值的特殊任务。
class A {
xxx;
};
A a;
auto c = std::move(a); // c 是将亡值
auto d = static_cast(a); // d 是将亡值
左值:能对表达式取地址、或具名对象/变量。一般指表达式结束后依然存在的持久对象。
右值:不能对表达式取地址,或匿名对象。一般指表达式结束就不再存在的临时对象。
type &name = exp; // 左值引用
type &&name = exp; // 右值引用
int a = 5;
int &b = a; // b 是左值引用
b = 4;
int &c = 10; // error,10 无法取地址,无法进行引用
const int &d = 10; // ok,因为是常引用,引用常量数字,这个常量数字会存储在内存中,可以取地址
可以得出结论:对于左值引用,等号右边的值必须可以取地址,如果不能取地址,则会编译失败,或者可以使用const 引用形式,但这样就只能通过引用来读取输出,不能修改数组,因为是常量引用。
如果使用右值引用,那表达式等号右边的值需要是右值,可以使用std::move 函数强制把左值转换为右值。
int a = 4;
int &&b = a; // error, a 是左值
int &&c = std::move(a); // ok
右值引用和左值引用的区别:
- 左值可以寻址,而右值不可以。
- 左值可以被赋值,右值不可以被赋值,可以用来给左值赋值。
- 左值可变,右值不可变(仅对基础类型适用,用户自定义类型右值引用可以通过成员函数改变)。
class A {
public:
A(int size) : size_(size) {
data_ = new int[size];
}
A(){}
A(const A& a) {
size_ = a.size_;
data_ = a.data_; //
cout << "copy " << endl;
}
~A() {
delete[] data_;
}
int *data_;
int size_;
};
int main() {
A a(10);
A b = a;
cout << "b " << b.data_ << endl;
cout << "a " << a.data_ << endl;
return 0;
}
上面代码中,两个输出的是相同的地址,a 和b 的data_指针指向了同一块内存,这就是浅拷贝,只是数据的简单赋值,那再析构时data_内存会被释放两次,导致程序出问题,这里正常会出现double free 导致程序崩溃的,但是不知道为什么我自己测试程序却没有崩溃,能力有限,没搞明白,无论怎样,这样的程序肯定是有隐患的,如何消除这种隐患呢,
可以使用如下深拷贝:
class A {
public:
A(int size) : size_(size) {
data_ = new int[size];
}
A(){}
A(const A& a) {
size_ = a.size_;
data_ = new int[size_];
cout << "copy " << endl;
}
~A() {
delete[] data_;
}
int *data_;
int size_;
};
int main() {
A a(10);
A b = a;
cout << "b " << b.data_ << endl;
cout << "a " << a.data_ << endl;
return 0;
}
深拷贝就是再拷贝对象时,如果被拷贝对象内部还有指针引用指向其它资源,自己需要重新开辟一块新内存存储资源,而不是简单的赋值。
可以理解为转移所有权,之前的拷贝是对于别人的资源,自己重新分配一块内存存储复制过来的资源,而对于移动语义,类似于转让或者资源窃取的意思,对于那块资源,转为自己所拥有,别人不再拥有也不会再使用,通过C++11新增的移动语义可以省去很多拷贝负担,怎么利用移动语义呢,是通过移动构造函数。
class A {
public:
A(int size) : size_(size) {
data_ = new int[size];
}
A(){}
A(const A& a) {
size_ = a.size_;
data_ = new int[size_];
cout << "copy " << endl;
}
A(A&& a) {
this->data_ = a.data_;
a.data_ = nullptr;
cout << "move " << endl;
}
~A() {
if (data_ != nullptr)
{
delete[] data_;
}
}
int *data_;
int size_;
};
int main() {
A a(10);
A b = a;
A c = std::move(a); // 调用移动构造函数
return 0;
}
如果不使用std::move(),会有很大的拷贝代价,使用移动语义可以避免很多无用的拷贝,提供程序性能,C++所有的STL都实现了移动语义,方便我们使用。例如:
std::vector vecs;...
std::vector vecm = std::move(vecs); // 免去很多拷贝
//std::move 源码 强制转换
template
typename remove_reference::type&& move(T&& t)
{
return static_cast::type &&>(t);
}
注意:移动语义仅针对于那些实现了移动构造函数的类的对象,对于那种基本类型int、float 等没有任何优化作用,还是会拷贝,因为它们实现没有对应的移动构造函数。
完美转发指可以写一个接受任意实参的函数模板,并转发到其它函数,目标函数会收到与转发函数完全相同的实参,转发函数实参是左值那目标函数实参也是左值,转发函数实参是右值那目标函数实参也是右值。
那如何实现完美转发呢,答案是使用
std::forward()。
void PrintV(int &t) {
cout << "lvalue" << endl;
}
void PrintV(int &&t) {
cout << "rvalue" << endl;
}
template
void Test(T &&t) {
PrintV(t);
PrintV(std::forward(t));
PrintV(std::move(t));
}
int main() {
Test(1); // lvalue rvalue rvalue
int a = 1;
Test(a); // lvalue lvalue rvalue
Test(std::forward(a)); // lvalue rvalue rvalue
Test(std::forward(a)); // lvalue lvalue rvalue
Test(std::forward(a)); // lvalue rvalue rvalue
return 0;
}
- Test(1):1 是右值,模板中T &&t 这种为万能引用,右值1 传到Test 函数中变成了右值引用,但是调用PrintV()时候,t 变成了左值,因为它变成了一个拥有名字的变量,所以打印lvalue,而PrintV(std::forward(t))时候,会进行完美转发,按照原来的类型转发,所以打印rvalue,PrintV(std::move(t))毫无疑问会打印rvalue。
- Test(a):a 是左值,模板中T &&这种为万能引用,左值a 传到Test 函数中变成了左值引用,所以有代码中打印。
- Test(std::forward(a)):转发为左值还是右值,依赖于T,T 是左值那就转发为左值,T 是右值那就转发为右值。
返回值优化(RVO)是一种C++编译优化技术,当函数需要返回一个对象实例时候,就会创建一个临时对象并通过复制构造函数将目标对象复制到临时对象,这里有复制构造函数和析构函数会被多余的调用到,有代价,而通过返回值优化,C++标准允许省略调用这些复制构造函数。
那什么时候编译器会进行返回值优化呢?
- return 的值类型与函数的返回值类型相同
- return 的是一个局部对象
//看几个例子:
//示例1:
std::vector return_vector(void) {
std::vector tmp {1,2,3,4,5};
return tmp;
}
std::vector &&rval_ref = return_vector();
//不会触发RVO,拷贝构造了一个临时的对象,临时对象的生命周期和rval_ref 绑定,等价于下面这段代码:
const std::vector& rval_ref = return_vector();
//示例2:
std::vector&& return_vector(void) {
std::vector tmp {1,2,3,4,5};
return std::move(tmp);
}
std::vector &&rval_ref = return_vector();
//这段代码会造成运行时错误,因为rval_ref 引用了被析构的tmp。讲道理来说这段代码是错的,但我自己运行过程中却成功了,
//我没有那么幸运,这里不纠结,继续向下看什么时候会触发RVO。
//示例3:
std::vector return_vector(void){
std::vector tmp {1,2,3,4,5};
return std::move(tmp);
}
std::vector &&rval_ref = return_vector();
//和示例1 类似,std::move 一个临时对象是没有必要的,也会忽略掉返回值优化。
//最好的代码:
std::vector return_vector(void){
std::vector tmp {1,2,3,4,5};
return tmp;
}
std::vector rval_ref = return_vector();
//这段代码会触发RVO,不拷贝也不移动,不生成临时对象。
C++11的新特性
decltype(exp) varname = value;
根据表达式exp推导varname的类型(exp不能是viod类型)auto使用的是模板实参推断(Template Argument Deduction)的机制。auto被一个虚构的模板类型参数T替代,然后进行推断,即相当于把变量设为一个函数参数,将其传递给模板并推断为实参,auto相当于利用了其中进行的实参推断,承担了模板参数T的作用。比如
template<typename Container>
void useContainer(const Container& container)
{
auto pos = container.begin();
while (pos != container.end())
{
auto& element = *pos++;
… // 对元素进行操作
}
}
其中第一个auto的初始化相当于下面这个模板传参时的情形,T就是为auto推断的类型
// auto pos = container.begin()的推断等价于如下调用模板的推断
template<typename T>
void deducePos(T pos);
deducePos(container.begin());
而auto类型变量不会是引用类型(模板实参推断的规则),所以要用auto&(C++14支持直接用decltype(auto)推断原始类型),第二个auto推断对应于下面这个模板传参时的情形,同样T就是为auto推断的类型
// auto& element = *pos++的推断等价于如下调用模板的推断
template<typename T>
void deduceElement(T& element);
deduceElement(*pos++);
唯一例外的是对初始化列表的推断,auto会将其视为std::initializer_list,而模板则不能对其推断
auto x = { 1, 2 }; // C++14禁止了对auto用initializer_list直接初始化,必须用=
auto x2 { 1 }; // 保留了单元素列表的直接初始化,但不会将其视为initializer_list
std::cout << typeid(x).name(); // class std::initializer_list
std::cout << typeid(x2).name(); // C++14中为int
template<typename T>
void deduceX(T x);
deduceX(x); // 错误:不能推断
C++14还允许auto作为返回类型,但此时auto仍然使用的是模板实参推断的机制,因此返回类型为auto的函数如果返回一个初始化列表,则会出错
auto newInitList() { return { 1 }; } // 错误
decltype比auto更确切地推断名称或表达式的类型(即原始的declared type),实现原理应该和auto类似,只是特殊情况不太一样,具体实现需要更多考虑
int i = 0;
decltype(i) x; // int x
decltype((i)) y = i; // int& y
decltype(i = 1) z = i; // int& z
std::cout << i << z; // 00
1. NULL是一个宏定义,C++中通常将其定义为0,编译器总是优先把它当作一个整型常量(C标准下定义为(void*)0)。
2. nullptr是一个编译期常量,其类型为nullptr_t。它既不是整型类型,也不是指针类型。
3. 在 模板推导中, nullptr被推导为nullptr_t类型,仍可隐式转为指针。 但0或NULL则会被推导为整型类型。
4. 要避免在整型和指针间进行函数重载。因为NULL会被匹配到整型形参版本的函数,而不是预期的指针版本。
1、智能指针是利用一种叫做RAII(资源获取初始化)的技术对普通的指针进行封装,这时智能指针实质上是一个对象,行为表现得像一个指针;
2、智能指针的作用:防止忘记调用delete释放内存和程序异常的进入catch块忘记释放内存,另外指针的释放时机也是非常考究的,多次释放同一个指针,会造成程序崩溃,这些都可以通过智能指针来解决。智能指针还有一个作用就是把值语义转化成引用语义;
3、智能指针的使用:智能指针是C++11中提供的,包含在头文件,shared_ptr,unique_ptr,weak_ptr
4、 shared_ptr : 多个指针指向同一个对象,share_ptr适用引用计数,每一个shared_ptr的拷贝都指向相同的内存,每使用一次,内部计数加1,每析构一次,内部引用次数减1,减为0时,自动删除所有指向堆的内存。shared_ptr内部的引用计数的线程是安全的,但是对象的读取需要加锁。
(1) 初始化:智能指针是一个模板类,
- 裸指针直接初始化,但不能通过隐式转换来构造,因为shared_ptr构造函数被声明为explicit;
- 允许移动构造,也允许拷贝构造;
- 通过make_shared构造,在C++11版本中就已经支持了**不能将指针直接赋给一个智能指针,一个是类,一个是指针。
shared_ptr<int> p1(new int(100));
shared_ptr<int> p2(std::move(p1)); // 移动语义,移动构造一个新的智能指针p2
// p1就不再指向该对象(变成空),引用计数依旧是1
shared_ptr<int> p3;
p3 = std::move(p2); // 移动赋值,p2指向空, p3指向该对象,整个对象的引用计数仍旧为1
移动肯定比复制快;复制你要增加引用技术,移动不需要,
移动构造函数快过复制构造函数,移动赋值运算符快过拷贝赋值运算符
#include
#include
class Frame {};
int main()
{
std::shared_ptr<Frame> f(new Frame()); // 裸指针直接初始化
std::shared_ptr<Frame> f1 = new Frame(); // Error,explicit禁止隐式初始化
std::shared_ptr<Frame> f2(f); // 拷贝构造函数
std::shared_ptr<Frame> f3 = f; // 拷贝构造函数
f2 = f; // copy赋值运算符重载
std::cout << f3.use_count() << " " << f3.unique() << std::endl;
std::shared_ptr<Frame> f4(std::move(new Frame())); // 移动构造函数
std::shared_ptr<Frame> f5 = std::move(new Frame()); // Error,explicit禁止隐式初始化
std::shared_ptr<Frame> f6(std::move(f4)); // 移动构造函数
std::shared_ptr<Frame> f7 = std::move(f6); // 移动构造函数
std::cout << f7.use_count() << " " << f7.unique() << std::endl;
std::shared_ptr<Frame[]> f8(new Frame[10]()); // Error,管理动态数组时,需要指定删除器
std::shared_ptr<Frame> f9(new Frame[10](), std::default_delete<Frame[]>());
auto f10 = std::make_shared<Frame>(); // std::make_shared来创建
return 0;
}
(2) 拷贝与赋值:拷贝时对象引用计数加一,赋值使得引用计数减一(原来指向对象的引用计数),当计数为0时,自动释放内存,后来指向对象引用计数加一,指向后来的对象。
p = q
p q必须都是shared_ptr,所保存的指针必须能相互转换,此操作会让p引用计数减一,q引用计数加一,p计数变为0时,则释放其管理的内存。
(3) get函数获取原始指针,若智能指针释放其对象,则返回指针所指对象也不复存在。
(4) 注意,不要用一个原始指针初始化多个share_ptr,否则会造成二次释放同一内存。
(5) 注意避免循环引用,share_ptr最大的陷阱就是循环引用,会导致内存无法释放,导致内存泄漏。
shared_ptr内存模型
由图可以看出,shared_ptr包含了一个指向对象的指针和一个指向控制块的指针。每一个由shared_ptr管理的对象都有一个控制块,它除了包含强引用计数、弱引用计数之外,还包含了自定义删除器的副本和分配器的副本以及其他附加数据。
控制块的创建规则:
- std::make_shared总是创建一个控制块;
- 从具备所有权的指针出发构造一个std::shared_ptr时,会创建一个控制块(如std::unique_ptr转为shared_ptr时会创建控制块,因为unique_ptr本身不使用控制块,同时unique_ptr置空);
- 当std::shared_ptr构造函数使用裸指针(int *p)作为实参时,会创建一个控制块。这意味从同一个裸指针出发来构造不止一个std::shared_ptr时会创建多重的控制块,也意味着对象会被析构多次。如果想从一个己经拥有控制块的对象出发创建一个std::shared_ptr,可以传递一个shared_ptr或weak_ptr而非裸指针作为构造函数的实参,这样则不会创建新的控制块。
智能指针详解
智能指针线程安全
shared_ptr共享型智能指针
5、 unique_ptr: 唯一拥有所指对象,同一时刻只能有一个unique_ptr指向给定的对象(通过禁止拷贝语义,只有移动语义来实现)相比于原始指针unique_ptr用语气RAII的特性,使得再出现异常的情况下,动态资源都能得到释放。
unique_ptr本身的生命周期:从unique_ptr指针创建时开始,直到离开作用域。离开作用域时,若其指向对象,则将其所指对象销毁。
unique_ptr指针与其所指对象的关系:在智能指针生命周期内,可以改变智能指针所指对象。
unique_ptr独占型智能指针
6、weak_ptr: 是为了配合share_ptr而引入的一种智能指针,因此他不具有普通指针的行为,没有重载,他的最大作用是协助share_ptr工作,像旁观者那样观测资源的使用情况。weak_ptr可以从share_ptr或者另一个weak_ptr对象构造,获得观测权,但weak_ptr没有共享资源,他的构造不会引起指针引用计数的增加,使用weak_ptr的成员函数use_count()可以观测资源的引用计数,另一个成员函数expirred()的功能等价于use_count()==0,表示被观测的资源已经不存在了。weak_ptr可以使用一个更重要的成员函数lock()从被观测的share_ptr获得一个可用的share_ptr对象,从而操作资源,当expirred()==true的时候,lock()函数将返回一个存储空指针的share_ptr。
注意的是我们不能通过weak_ptr直接访问对象的方法,比如B对象中有一个方法print(),我们不能这样问,pa->pb->print(); 英文pb是一个weak_ptr,应该先把它转化为shared_ptr,如:shared_ptr p =pa->pb_.lock(); p->print();
weak_ptr弱引用智能指针
7、 智能指针的设计与实现 :智能指针类讲一个计数器与类所指向的对象相关联,引用计数跟踪该类有多少个对象共享一个指针,每次传建类的新对象时,初始指针并将引用计数器置1,当对象作为另一个对象的副本而建立的时候,拷贝构造函数拷贝指针并增加与之相对用的引用计数,对一个对象进行赋值,赋值使得原对象的引用计数减1,并增加当前对象的引用计数加1.调用析构函数时,构造函数减少引用计数(如果引用计数减少至0,则删除基础对象)智能指针就是模拟指针动作的类。所有的智能指针都会重载 -> 和 * 操作符。智能指针还有许多其他功能,比较有用的是自动销毁。这主要是利用栈对象的有限作用域以及临时对象(有限作用域实现)析构函数释放内存。
char * p = (char *)malloc(10);
char * np = (char *)malloc(10);
p = np;
free(p); // 泄露
free(p->np); //不泄露
free(p);
char *f(){
return (char *)malloc(10);
}
void f1(){
f();
}
//函数 f1 中对 f 函数的调用并未处理该内存位置的返回地址,其结果将导致 f 函数所分配的 10 个字节的块丢失,并导致内存泄漏。
(1)src串长度 <= dest串长度
(1)如果n=(0,src串长度),src的前n个字符复制到dest中。但是由于没有NULL字符,所以直接访问dest串会发生栈溢出的异常情况。这时,一般建议采取memset将dest的全部元素用null填充
(2)如果n = src串长度,与strcpy一致
(3)如果n = dest串长度,[0,src串长度]处存放于desk字串,[src串长度, dest串长度]处存放NULL。
(2)src串长度>dest串长度
如果n = dest串长度,则dest串没有NULL字符,会导致输出会有乱码。如果不考虑src串复制完整性,可以将dest最后一字符置为NULL。所以,一般把n设为dest(含null)的长度(除非将多个src复制到dest中)。当2)中n = dest串长度时,定义dest为字符数组,因为这时没有null字符拷贝。
#include
#include
struct test
{
int a;
char b;
int c;
short d;
};
typedef union
{
double i;
int k[5];
char c;
}DATE;
int main(int argc,char *argv)
{
/*在32位和64位的机器上,size_t的大小不同*/
printf("the size of struct test is %zu\n",sizeof(struct test));
return 0;
}
结构体
未对齐时:
0~3 | 4 | 5~9 | 10~11 |
---|---|---|---|
a | b | c | d |
对齐时:
0~3 | 4 | 5~7 | 8~11 | 12 ~ 13 | 14 ~ 15 |
---|---|---|---|---|---|
a | b | 填充内容 | c | d | 填充 |
联合
union中最大的变量类型是int[5],所以占用20个字节,大小是20,由于double占8字节,要进行8字节对齐,所以内存空间是8的倍数,最后所占空间是24。
总的来说,字节对齐有以下准则:
注意:64位机器指针大小为8字节,32为机器为4字节
当结构体中有复合符合成员时,复合成员相对于结构体首地址偏移量是复合成员最宽基本类型大小的整数倍。
#pragma pack () 取消指定对齐,恢复缺省对齐
#pragma pack (2) 2字节对齐
给联合中的成员赋值时,只会对这个成员所属的数据类型所占内存空间的大小覆盖成后来的这个值,而不会影响其他位置的值。
一个字的位数,现代电脑的字长通常为16,32, 64位。(一般N位系统的字长是N/8字节。)
不同的CPU一次可以处理的数据位数是不同的,32位CPU可以一次处理32位数据,64位CPU可以一次处理64位数据,这里的位,指的就是字长。
(1)如果函数中定义了一个对象,当这个函数调用结束时,对象被释放,且在对象释放前会自动执行析构函数
(2)static局部对象在函数调用结束时对象不释放,所以也不执行析构函数,只有在main函数结束或调用exit函数结束程序时,才调用static局部对象的析构函数
(3)全局对象则在程序流程离开作用域时,才会执行该全局对象的析构函数
(4)用new建立对象,用delete释放对象时,会调用该对象的析构函数
不可以为虚函数,因为调用构造函数时,虚表指针并没有在对象的内存空间中,必须要构造函数调用完后才会形成虚表指针
拷贝构造函数理由同上
只能写在一个一个头文件中。模板类的实现,脱离具体的使用,是无法单独的编译的;把声明和实现分开的做法也是不可取的,必须把实现全部写在头文件里面
原因: 多文件处理变为一个文件其实是通过链接器来实现的,所以如果用源文件来处理模板实现,会导致链接失效,最主要的原因还是在编译,编译器会暂时不处理模板类只有在实例化对象时才去处理,但是这就需要实现的代码了,如果放在其他文件的话,就会无法形成相应的类。
函数的重载:
C++允许用同一函数名定义多个函数,这些函数的参数个数和参数类型不同。这就是函数重载。
重载函数的参数个数、参数类型或参数顺序3者中必须至少有一种不同,函数返回值类型可以相同也可以不同。
函数模板:
所谓函数模板。实际上是建立一个通用函数,其函数类型和形参类型不具体指定,用一个虚拟的类型来代表。这个通用函数就称为函数模板。凡是函数体相同的函数都可以用这个模板来代替,不必定以多个函数,只需在模板中定义一次即可。
template < typename T> //模板声明。template的含义是“模板”。关键字typename或class表示“类型名”。其中T为类型参数,类型参数可以不只一个,可以根据需要确定个数。
T max (T a,T b,T c) //定义一个通用函数,用T作虚拟的类型名
模板只适用于函数体相同、函数的参数个数相同而类型不同的情况,如果参数的个数不同,则不能用函数模板。
方法 | 返回值类型 | 参数个数 | 参数类型 | 参数顺序 | 函数体 |
---|---|---|---|---|---|
重载 | 可同也可不同 | 必须有一种不同 | 不同 | ||
模板 | 相同 | 相同 | 不同 | 相同 | 相同 |
首先编译不会出错的
函数模板与同名的非模板函数重载时候,调用顺序:
模板特化
理解函数模板的类型推导
template<typename T>
void f(ParamType param); //注意这里是ParamType而不是T。这两者可能不一样!
f(expr); //调用形式,以实参expr调用f
注意事项:
- ParamType是个引用类型。由于引用的特点,即形参代表实参本身,所以实参expr的CV属性会被T保留下来。(注意,如果传进来的实参是个指针,则param会将实参(指针)的顶层和底层const的都保留下来)。
- 当ParamType为T* 指针,传参时param指针是实参的副本,因此实参和形参是两个不同的指针。T在推导中,会保留实参中的底层const,而舍弃顶层const。因为底层const修饰的是指针所向对象,表示该对象不可更改,所以const应保留,而顶层const表示指针本身,从实参到形参传递时复制的是指针,因此形参的指针是个副本,无须保留const属性。(如,const char* const ptr中,顶层const指修饰ptr的const,即号右侧const,而底层const指号左侧的const)
注意事项:
- 当实参为左值时T被推导为T&(不是T类型),表示形参是实参的引用,即代表实参本身。因此指针的顶层和底层const属性均会被保留。
- 形如const T&&或vector&&)均属于右值引用,因为const的引用会剥夺引用成为万能引用的资格,因为由其定义的变量再也不能成为非const类型,所以不是“万能”的类型,而后者己经确定是个vector类型,不可能成为“万能”的类型,如不会再是int型,因此也不是万能引用。(详见《万能引用》一节)
- 在推导过程中会发生引用折叠(详见《引用折叠》一节)。
//由于数组到指针的退化规则,以下两个函数等价的,所以不能同时声明这两个同名函数。
void myFunc(int param[]);
void myFunc(int* param);
数组引用的妙用:用于推导数组元素个数
类 A 派生出类 B 和类 C,类 D 继承自类 B 和类 C,这个时候类 A 中的成员变量和成员函数继承到类 D 中变成了两份,一份来自 A–>B–>D 这条路径,另一份来自 A–>C–>D 这条路径。
在一个派生类中保留间接基类的多份同名成员,虽然可以在不同的成员变量中分别存放不同的数据,但大多数情况下这是多余的:因为保留多份成员变量不仅占用较多的存储空间,还容易产生命名冲突。假如类 A 有一个成员变量 a,那么在类 D 中直接访问 a 就会产生歧义,编译器不知道它究竟来自 A -->B–>D 这条路径,还是来自 A–>C–>D 这条路径。下面是菱形继承的具体实现:
//间接基类A
class A{
protected:
int m_a;
};
//直接基类B
class B: public A{
protected:
int m_b;
};
//直接基类C
class C: public A{
protected:
int m_c;
};
//派生类D
class D: public B, public C{
public:
void seta(int a){ m_a = a; } //命名冲突
void setb(int b){ m_b = b; } //正确
void setc(int c){ m_c = c; } //正确
void setd(int d){ m_d = d; } //正确
private:
int m_d;
};
int main(){
D d;
return 0;
}
这段代码实现了上图所示的菱形继承,第 25 行代码试图直接访问成员变量 m_a,结果发生了错误,因为类 B 和类 C 中都有成员变量 m_a(从 A 类继承而来),编译器不知道选用哪一个,所以产生了歧义。
//我们可以在 m_a 的前面指明它具体来自哪个类
void seta(int a){ B::m_a = a; }
void seta(int a){ C::m_a = a; }
为了解决从为了解决多继承时的命名冲突和冗余数据问题,将基类设置为虚基类。这时从不同的路径继承过来的同名数据成员在内存中只有一个拷贝,同一个函数名也只有一个映射。这样不仅解决了二义性问题,也节省内存,避免数据不一致的问题。
定义:在多重继承下,一个基类可以在派生类中出现多次。(派生类对象中可能出现多个基类对象)在C++中,通过使用虚继承解决这类问题。虚继承是一种机制,类通过虚继承指出它希望共享其虚基类的状态。在虚继承下,对给定虚基类,无论该类在派生层次中作为虚基类出现多少次,只继承一个共享的基类子对象。共享的基类子对象称为虚基类。
//间接基类A
class A{
protected:
int m_a;
};
//直接基类B
class B: virtual public A{ //虚继承
protected:
int m_b;
};
//直接基类C
class C: virtual public A{ //虚继承
protected:
int m_c;
};
//派生类D
class D: public B, public C{
public:
void seta(int a){ m_a = a; } //正确
void setb(int b){ m_b = b; } //正确
void setc(int c){ m_c = c; } //正确
void setd(int d){ m_d = d; } //正确
private:
int m_d;
};
int main(){
D d;
return 0;
}
虚继承中的子类多了一张基类的虚函数表。
在虚继承中,虚基类是由最终的派生类初始化的,换句话说,最终派生类的构造函数必须要调用虚基类的构造函数。对最终的派生类来说,虚基类是间接基类,而不是直接基类。这跟普通继承不同,在普通继承中,派生类构造函数中只能调用直接基类的构造函数,不能调用间接基类的。
对于普通继承,基类成员变量的偏移是固定的,不会随着继承层级的增加而改变,存取起来非常方便。而对于虚继承,恰恰和普通继承相反,大部分编译器会把基类成员变量放在派生类成员变量的后面,这样随着继承层级的增加,基类成员变量的偏移就会改变,就得通过其他方案来计算偏移量。下面我们来一步一步地分析虚继承时的对象内存模型。
class B: virtual public A;
简单点说,就是一个应用程序中,某个类的实例对象只有一个,你没有办法去new,因为构造器被private修饰的,一般通过getInstance()方法来获取它们的实例。getInstance()的返回值是一个对象的引用,并不是一个新的实例,所以不要错误的理解成多个对象
//饿汉式
//优点:简单
//缺点:可能会导致进程启动慢,且如果有多个单例类对象实例
class Singleton
{
public:
static Singleton* getInstance()
{
return &s_instance;
}
private:
Singleton(){};//构造函数私有
// C++11
Singleton(Singleton const&) = delete;
Singleton& operator=(Singleton const&) = delete;
static Singleton s_instance;
}
Singleton Singleton::instance;
int main()
{
Singleton *a1 = Singleton::getinstance();
cout << a1 << endl;
return 0;
}
//懒汉式
//有了上面饿汉式的经验,我们同样可以这么想:懒汉式,因为懒,所以唯一实例能拖就拖;一直等到有人要使用的时候,才不得不构造。
class Singleton
{
public:
static Singleton *getinstance()
{
if (instance == nullptr)
instance = new Singleton();
return instance;
}
private:
static Singleton *instance;
Singleton() {}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
Singleton *Singleton::instance = nullptr;
//线程安全版实现
class Singleton
{
public:
static Singleton* getinstance()
{
if(instance == nullptr)
{
mtx.lock();
if(instance == nullptr)
{
instance = new Singleton();
}
mtx.unlock();
}
return instance;
}
private:
static Singleton* instance;
Singleton(){}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static mutex mtx;
};
Singleton* Singleton::instance = nullptr;
mutex Singleton::mtx;
对象间一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新
对已有的业务逻辑进一步的封装, 使其增加额外的功能,如java中的IO流就使用了装饰者模式,用户在使用的时候,可以任意组装,达到自己想要的效果。
将两种完全不同的事物联系到一起,就像现实生活中的变压器。假设一个手机充电器需要的电压是20V,但是正常的电压是220V,这时候就需要一个变压器,将220V的电压转换成20V的电压,这样,变压器就将20V的电压和手机联系起来了。
简单工厂模式:一个抽象的接口,多个抽象接口的实现类,一个工厂类,用来实例化抽象的接口
工厂方法模式:有四个角色,抽象工厂模式,具体工厂模式,抽象产品模式,具体产品模式。不再是由一个工厂类去实例化具体的产品,而是由抽象工厂的子类去实例化产品
#include
#include
using namespace std;
//产品类(抽象类,不能实例化)
class Product{
public:
Product(){};
virtual void show()=0; //纯虚函数
};
class productA:public Product{
public:
productA(){};
void show(){ cout << "product A create!" << endl; };
~productA(){};
};
class productB:public Product{
public:
productB(){};
void show(){ cout << "product B create!" << endl; };
~productB(){};
};
class simpleFactory{ // 工厂类
public:
simpleFactory(){};
Product* product(const string str){
if (str == "productA")
return (new productA());
if (str == "productB")
return (new productB());
return NULL;
};
};
int main(){
simpleFactory obj; // 创建工厂
Product* pro; // 创建产品
pro = obj.product("productA");
pro->show(); // product A create!
delete pro;
pro = obj.product("productB");
pro->show(); // product B create!
delete pro;
return 0;
}
//工厂模式为的就是代码解耦,如果我们不采用工厂模式,如果要创建产品A、B,我们通常做法是不是用switch...case语句?那麻烦了,代码耦合程度高,后期添加更多的产品进来,我们不是要添加更多的case吗?这样就太麻烦了,而且不符合设计模式中的开放封闭原则。
//为了进一步解耦,在简单工厂的基础上发展出了抽象工厂模式,即连工厂都抽象出来,实现了进一步代码解耦。代码如下:
#include
#include
using namespace std;
//产品类(抽象类,不能实例化)
class Product{
public:
Product(){}
virtual void show()=0; //纯虚函数
};
//产品A
class ProductA:public Product{
public:
ProductA(){}
void show(){ cout<<"product A create!"<<endl; };
};
//产品B
class ProductB:public Product{
public:
ProductB(){}
void show(){ cout<<"product B create!"<<endl; };
};
class Factory{//抽象类
public:
virtual Product* CreateProduct()=0;
};
class FactorA:public Factory{//工厂类A,只生产A产品
public:
Product* CreateProduct(){
return (new ProductA());
}
};
class FactorB:public Factory{//工厂类B,只生产B产品
public:
Product* CreateProduct(){
return (new ProductB());
}
};
int main(){
Product* _Product = nullptr;
auto MyFactoryA = new FactorA();
_Product = MyFactoryA->CreateProduct();// 调用产品A的工厂来生产A产品
_Product->show();
delete _Product;
auto MyFactoryB=new FactorB();
_Product=MyFactoryB->CreateProduct();// 调用产品B的工厂来生产B产品
_Product->show();
delete _Product;
getchar();
return 0;
}
悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁。
两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果经常产生冲突,上层应用会不断的进行retry,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。
文件系统是一种存储和组织计算机数据的方法。它负责对文件存储设备的空间进行组织和分配,负责文件存储并对存入的文件进行保护,以及检索的系统。如ext3和NTFS这两种文件系统对存储空间的划分、碎片的整理及检索的实现都不同,前者常用于linux的文件系统,后者常用于windows的文件系统。
所以,任意一种存储设备,如果想像linux文件系统一样,以树形结构查看文件,必须被格式化成某一种文件系统。所以我们会看到无论是硬盘还是闪存都会被格式化成某种文件系统(以分区为单位,当然有的分区不用来挂载文件系统,不必格式化,如存放内核的闪存分区)
首先,应用程序要操作一个磁盘上的文件(如系统调用open)
接着,会通过系统调用接口(见前面的博文,glibc的接口函数,它会执行swi指令)
然后,通过上面的swi指令陷入到内核,到达VFS层(VFS层是一个中间层,它可以让open()、read()、write()等系统调用不用关心底层的存储介质和文件系统类型就可以工作。如系统调用open陷入到内核后调用的sys_open就是属于这一层的函数)
再接着,VFS层会帮我们找到具体要操作文件所在的分区和文件系统类型
最后,是根据硬件驱动来驱动硬件来进行实际操作(硬件驱动之上还有一层,隐藏硬件驱动差异的,给它的上一层提供统一的接口)
#include
using namespace std;
class Test1
{
public :
Test1(int num):n(num){}
private:
int n;
};
class Test2
{
public :
explicit Test2(int num):n(num){}
private:
int n;
};
int main()
{
Test1 t1 = 12;
Test2 t2(13);
Test2 t3 = 14;
return 0;
}
编译时,会指出 t3那一行error:无法从“int”转换为“Test2”。而t1却编译通过。注释掉t3那行,调试时,t1已被赋值成功。
在C++中,如果一个类有且只有一个参数的构造函数,C++允许一种特殊的声明类变量的方式。在这种情况下,可以直接将一个对应于构造函数参数类型的数据直接赋值给类变量,编译器在编译时会自动进行类型转换,将对应于构造函数参数类型的数据转换为类的对象。如果在构造函数前加上explicit修饰词,则会禁止这种自动转换,在这种情况下,即使将对应于构造函数参数类型的数据直接赋值给类变量,编译器也会报错。
假如Myclass为一类,执行Myclass a[3],*p[2];语句时会自动调用该类构造函数几次?
答:3次
Myclass a[3],*p[2];
a[3]中有3个Myclass对象,定义时会各调用Myclass构造函数一次。
Myclass *p[2]只定义了两个指针,只是两个指针变量。
构造函数是特殊的成员函数,只要创建类类型的新对象,都要执行构造函数。构造函数的工作就是保证每个对象的数据成员具有合适的初始值。
构造函数与其他函数不同:构造函数函数与类同名,没有返回类型。
构造函数与其他函数相同:构造函数也有形参表(可为void)和函数体
构造函数构造类对象的顺序是:
A():a(0){}
Salesitem():isbn(10,‘9’),units_sold(0),resenue(0,0){}
初始化列表的优点: 主要是对于自定义类型,初始化列表是作用在函数体之前,他调用构造函数对对象进行初始化。
然而在函数体内,需要先调用构造函数,然后进行赋值,这样效率就不如初始化表。
A(int i = 1):a(i),ca(i),ra(i){};
合成默认构造函数: 当类中没有定义构造函数(注意是构造函数)的时候,编译器自动生成的函数。
但是我们不能过分依赖编译器,如果我们的类中有复合类型或者自定义类型成员,我们需要自己定义构造函数。
自定义的默认构造函数:
A(A&&h):a(h.a)
{
h.a = nullptr;
}
可以看到,这个构造函数的参数不同,有两个&操作符, 移动构造函数接收的是“右值引用”的参数。
还要来说一下,这里h.a置为空,如果不这样做,h.a在移动构造函数结束时候执行析构函数会将我们偷来的内存析构掉。h.a会变成悬垂指针。
移动构造函数何时触发? 那就是临时对象(右值)。用到临时对象的时候就会执行移动语义。
这里要注意的是,异常发生的情况,要尽量保证移动构造函数 不发生异常,可以通过noexcept关键字,这里可以保证移动构造函数中抛出来的异常会直接调用terminate终止程序。
他的原理跟移动构造函数相同
A & operator = (A&& h)
{
assert(this != &h);
a = nullptr;
a = move(h.a);
h.a = nullptr;
return *this;
}
他是一种特殊的构造函数,具有单个形参,形参是对该类类型的引用。当定义一个新对象并用一个同类型的对象对它进行初始化时,将显式使用复制构造函数。当将该类型的对象传递给函数或从函数返回该类型的对象时,将隐式使用复制构造函数。
必须定义复制构造函数的情况:
什么情况下调用:
box2=box1//box2(box1)
void fun(Box b) //形参是类的对象
下面给出赋值构造函数的编写:
A(const A& h):a(h.a){}
如果不想让对象拷贝呢?
那就将复制构造函数声明为 private
他跟构造函数一样,赋值操作符可以通过制定不同类型的右操作数而重载。
赋值和复制经常是一起使用的,这个要注意。
下面给出赋值操作符的写法:
A& operator = (const A& h)
{
assert(this != &h);
this->a = h.a;
return *this;
}
是构造函数的互补,当对象超出作用域或动态分配的对象被删除时,将自动应用析构函数。析构函数可用于释放对象时构造或在对象的生命期中所获取的资源。不管类是否定义了自己的析构函数,编译器都会自动执行类中非static数据成员的析构函数。
析构函数的运行:
当对象引用或指针越界的时候不会执行析构函数,只有在删除指向动态分配对象的指针或实际对象超出作用域时才会调用析构函数。
合成析构函数:
编译器总是会合成一个析构函数,合成析构函数按对象创建时的逆序撤销每个非static成员。要注意的是,合成的析构函数不会删除指针成员所指向的对象。
什么时候调用析构函数:
最后要注意的是:类如果需要析构函数,那么他肯定也需要复制构造函数和赋值操作符
class A
{
protected:
A()
{};
~A()
{};
static A* Create()
{
return new A();
}
static void Destory(A* p)
{
delete p;
p = NULL;
}
};
class AA
{
private:
void* operator new(size_t)
{};
void operator delete(void*)
{};
public:
AA()
{
cout << "AA()" << endl;
}
~AA()
{
cout << "~AA()" << endl;
}
};
class singleclass
{
public:
static singleclass* getsingleclass()
{
if (count > 0)
{
count--;
return new singleclass();
}
else
{
return NULL;
}
}
private:
static int count;
singleclass(){};
};
一般来说,memcpy的实现非常简单,只需要顺序的循环,把字节一个一个从src拷贝到dest就行:
#include /* size_t */
void *memcpy(void *dest, const void *src, size_t n)
{
char *dp = dest;
const char *sp = src;
while (n--)
*dp++ = *sp++;
return dest;
}
memmove会对拷贝的数据作检查,确保内存没有覆盖,如果发现会覆盖数据,简单的实现是调转开始拷贝的位置,从尾部开始拷贝:
#include /* for size_t */
void *memmove(void *dest, const void *src, size_t n)
{
unsigned char *pd = dest;
const unsigned char *ps = src;
if (__np_anyptrlt(ps, pd))
for (pd += n, ps += n; n--;)
*--pd = *--ps;
else
while(n--)
*pd++ = *ps++;
return dest;
}
重载运算符本质上是函数。
如果一个运算符是一个成员函数,则其左侧运算对象就绑定到隐式的this参数。
赋值运算符通常返回一个指向其左侧运算对象的引用。Foo& operator = (const Foo&)
重载运算符应该继承而不是违背其内置版本的定义
核心思想:数据抽象、继承、动态绑定。
1、任何构造函数之外的非静态函数都可以是虚函数。关键字virtual
只能出现在类内部而不能用于类外部的函数定义;
2、如果派生类没有覆盖其基类中的某个虚函数,则该函数的行为类似于其他的普通成员,派生类会直接继承其在基类的版本;
3、首先初始化基类的部分,然后按声明的顺序依次初始化派生类的成员;
Bulk_quote(const std::string& book , double p ,
std::size_t qty , double disc) :Quote(book,p) ,
min_qty(qty) , discount(disc) {};
4、如果基类定义了一个静态成员,则整个继承体系中只存在该成员的唯一定义,加入该成员是可访问的,则可以通过派生类去使用它;
5、在类名后面加final
可以防止该类被继承;
6、一个派生类如果要去覆盖某个继承而来的虚函数,则他的形参类型必须与他覆盖的的基类函数完全一致。同样,派生类中虚函数的返回类型也必须与基类函数匹配。但是当虚函数返回类型是类本身的指针或引用时,上述规则无效。
7、如果我们使用override
标记某个函数,但该函数并没有覆盖已存在的虚函数,此时编译器将报错;
8、含有(或者未经覆盖直接继承)纯虚函数的类是抽象基类;
9、派生类的成员或者友元只能通过派生类对象来访问基类受保护成员;
10、基类、友元、派生类访问权限:
public成员 | protected成员 | private成员 | |
---|---|---|---|
基类及其友元 | 有 | 有 | 有 |
派生类及其友元 | 有 | 有 | 无 |
类用户 | 有 | 无 | 无 |
具体来说就是:
1)基类本身及其友元对基类中任何成员都有访问权限;
2)派生类及派生类的友元可以通过派生类的对象访问基类中受保护的成员,对private成员无访问权限;
3)类用户(由基类定义的对象)只能访问基类中的public成员。
11、我们通常把基类的析构函数定义成虚函数以保证执行正确的析构函数版本;
12、虚析构函数将阻止合成移动操作;
1、模板是实现代码重用机制的一种工具,它可以实现类型参数化,即把类型定义为参数,从而实现真正的代码可重用性;
2、模板分为两类,一个是函数模板一个是类模板。
建立一个通用函数模板
template
T max(T a , T b , T c)
{
if(b > a)
a = b;
if(c > a)
a = c;
return a;
}
c++的类模板为生成通用的类声明提供一种更好的方法,模板提供参数化类型,即能够将类型名作为参数传递给接收方来建立类或者函数
当我们在类模板外定义一个成员模板时,必须同时为类模板和成员模板提供模板参数列表。类模板参数在前,成员模板参数在后。
template //类的类型参数
template //构造函数的类型参数
Blob::Blob(It b , It e) : data(std::make_shared>(b,e)) { };
lambda表达式具体形式:
[capture list](parameter list) -> return type{function body}
其中capture list(捕获列表)是一个lambda所在函数中定义的局部变量的列表(通常为空),parameter list和function body与任何普通函数一样,分别表示返回类型、参数列表和函数体。与普通函数不同,lambda通常使用尾置返回来指定类型。
我们可以忽略参数列表和返回类型,但是必须永远包含捕获列表和函数体。与普通函数不同,lambda不能有默认参数,一个lambda调用的实参必须与形参数量相同
// 长度排序
sort(words.begin() , words.end() ,
[](const string& a , const string& b)
{ return a.size() < b.size();})
Lambda捕获列表 | |
---|---|
[] | 空捕获列表,Lambda不能使用所在函数中的变量。 |
[names] | names是一个逗号分隔的名字列表,这些名字都是Lambda所在函数的局部变量。默认情况下,这些变量会被拷贝,然后按值传递,名字前面如果使用了&,则按引用传递 |
[&] | 隐式捕获列表,Lambda体内使用的局部变量都按引用方式传递 |
[=] | 隐式捕获列表,Lanbda体内使用的局部变量都按值传递 |
[&,identifier_list] | identifier_list是一个逗号分隔的列表,包含0个或多个来自所在函数的变量,这些变量采用值捕获的方式,其他变量则被隐式捕获,采用引用方式传递,identifier_list中的名字前面不能使用&。 |
[=,identifier_list] | identifier_list中的变量采用引用方式捕获,而被隐式捕获的变量都采用按值传递的方式捕获。identifier_list中的名字不能包含this,且这些名字面前必须使用&。 |
auto func(int i)
{
return i;
}
注意
return {1,2,3}
auto f();
auto f() { return 42;}
int main()
{
cout<
auto sum(int i)
{
if(i == 1)
return i; //return int
else
return sum(i - 1) + i; // ok
}
auto f = [](auto a){ return a;}
cout<< f(1) <
template
constexpr T pi = T(3.1415926535897932385L);
int main() {
cout << pi << endl; // 3
cout << pi << endl; // 3.14159
return 0;
}
templatestruct A {
T t;
U u;
};
template
using B = A;
int main() {
B b;
b.t = 10;
b.u = 20;
cout << b.t << endl;
cout << b.u << endl;
return 0;
}
C++14 相较于C++11对constexpr减少了一些限制:
C++14中增加了deprecated标记,修饰类、变、函数等,当程序中使用到了被其修饰的代码时,编译时被产生警告,用户提示开发者该标记修饰的内容将来可能会被丢弃,尽量不要使用。
int a = 0b0001'0011'1010;
double b = 3.14'1234'1234'1234;
我们都知道C++11中有std::make_shared,却没有std::make_unique,在C++14已经改善。
struct A {};
std::unique_ptr ptr = std::make_unique();
C++14通过std::shared_timed_mutex和std::shared_lock来实现读写锁,保证多个线程可以同时读,但是写线程必须独立运行,写操作不可以同时和读操作一起进行。
C++17之前构造一个模板类需要指明类型,C++17就不需要
pair p(1, 2.2); // before c++17
pair p(1, 2.2); // c++17 自动推导
vector v = {1, 2, 3}; // c++17
结构化绑定详解
结构化绑定:通过对象的元素或成员初始化多个实体。
结构化绑定之前我们遍历给定的是无意义的elem。
for (const auto& elem : mymap) {
std::cout << elem.first << ": " << elem.second << std::endl;
}
有了结构体绑定之后,我们只需要[key, val]。
for (const auto& [key, val] : mymap) {
std::cout << key << ": " << val << std::endl;
}
//C++11
int a = GetValue();
if (a < 101) {
cout << a;
}
//C++17
if (int a = GetValue()); a < 101) {
cout << a;
}
string str = "Hi World";
if (auto [pos, size] = pair(str.find("Hi"), str.size()); pos != string::npos) {
std::cout << pos << " Hello, size is " << size;
}
C++17前只有内联函数,现在有了内联变量,我们印象中C++类的静态成员变量在头文件中是不能初始化的,但是有了内联变量,就可以达到此目的:
// header file
struct A {
static const int value;
};
inline int const A::value = 10;
// ==========或者========
struct A {
inline static const int value = 10;
}
template
auto sum(Ts ... ts) {
return (ts + ...);
}
int a {sum(1, 2, 3, 4, 5)}; // 15
std::string a{"hello "};
std::string b{"world"};
cout << sum(a, b) << endl; // hello world
C++17前lambda表达式只能在运行时使用,C++17引入了constexpr lambda表达式,可以用于在编译期进行计算。
int main() { // c++17可编译
constexpr auto lamb = [] (int n) { return n * n; };
static_assert(lamb(3) == 9, "a");
}
constexpr函数有如下限制:
函数体不能包含汇编语句、goto语句、label、try块、静态变量、线程局部存储、没有初始化的普通变量,不能动态分配内存,不能有new delete等,不能虚函数。
namespace A {
namespace B {
namespace C {
void func();
}
}
}
// c++17,更方便更舒适
namespace A::B::C {
void func();)
}
对于 (a <=> b),如果a > b ,则运算结果>0,如果a < b,则运算结果<0,如果a==b,则运算结果等于0,注意下,运算符的结果类型会根据a和b的类型来决定,所以我们平时使用时候最好直接用auto,方便快捷。
for (auto n = v.size(); auto i : v) // the init-statement (C++20)
std::cout << --n + i << ' ';
[[likely]]和[[unlikely]]:在分支预测时,用于告诉编译器哪个分支更容易被执行,哪个不容易执行,方便编译器做优化。
constexpr long long fact(long long n) noexcept {
if (n > 1) [[likely]]
return n * fact(n - 1);
else [[unlikely]]
return 1;
}
C++20之前[=]会隐式捕获this,而C++20需要显式捕获,这样[=, this]
struct S2 { void f(int i); };
void S2::f(int i)
{
[=]{}; // OK: by-copy capture default
[=, &i]{}; // OK: by-copy capture, except i is captured by reference
[=, *this]{}; // until C++17: Error: invalid syntax
// since c++17: OK: captures the enclosing S2 by copy
[=, this] {}; // until C++20: Error: this when = is the default
// since C++20: OK, same as [=]
}
// helloworld.cpp
export module helloworld; // module declaration
import ; // import declaration
export void hello() { // export declaration
std::cout << "Hello world!\n";
}
// main.cpp
import helloworld; // import declaration
int main() {
hello();
}
least = MIN(*p++, b)
ans:
#define MIN(A,B) ((A) <= (B) ? (A) : (B))
MIN(*p++, b)会产生宏的副作用
宏定义可以实现类似于函数的功能,但是它终归不是函数,而宏定义中括弧中的“参数”也不是真的参数,在宏展开的时候对“参数”进行的是一对一的替换。
程序员对宏定义的使用要非常小心,特别要注意两个问题:
1)谨慎地将宏定义中的“参数”和整个宏用用括弧括起来。所以,严格地讲,下述解答:
#define MIN(A,B) (A) <= (B) ? (A) : (B)
#define MIN(A,B) (A <= B ? A : B )
3*MIN(A,B) -> 3*(A)<=(B)?(A):(B)//发生歧义
都应判0分;
2)防止宏的副作用。
宏定义#define MIN(A,B) ((A) <= (B) ? (A) : (B))对MIN(*p++, b)的作用结果是:
((*p++) <= (b) ? (*p++) : (b))
这个表达式会产生副作用,指针p会作2次++自增操作。
除此之外,另一个应该判0分的解答是:
#define MIN(A,B) ((A) <= (B) ? (A) : (B));
这个解答在宏定义的后面加“;”,显示编写者对宏的概念模糊不清,只能被无情地判0分并被面试官淘汰。
源代码–>预编译–>编译–>优化–>汇编–>链接–>可执行文件
扩展:静态链接和动态链接?
区别:
静态链接和动态链接
面向对象设计五大原则
ans:
1、单一职责原则
单一职责有2个含义,一个是避免相同的职责分散到不同的类中,另一个是避免一个类承担太多职责。减少类的耦合,提高类的复用性。
2、接口隔离原则
表明客户端不应该被强迫实现一些他们不会使用的接口,应该把接口按方法分组,然后用多个接口代替它,每个接口服务于一个子模块。简单说,就是使用多个专门的接口比使用单个接口好很多。
该原则观点如下:
1)一个类对另外一个类的依赖性应当是建立在最小的接口上
2)客户端程序不应该依赖它不需要的接口方法。
3、开放-封闭原则
open模块的行为必须是开放的、支持扩展的,而不是僵化的。
closed在对模块的功能进行扩展时,不应该影响或大规模影响已有的程序模块。一句话概括:一个模块在扩展性方面应该是开放的而在更改性方面应该是封闭的。
核心思想就是对抽象编程,而不对具体编程。
4、替换原则
子类型必须能够替换掉他们的父类型、并出现在父类能够出现的任何地方。
主要针对继承的设计原则
1)父类的方法都要在子类中实现或者重写,并且派生类只实现其抽象类中生命的方法,而不应当给出多余的,方法定义或实现。
2)在客户端程序中只应该使用父类对象而不应当直接使用子类对象,这样可以实现运行期间绑定。
5、依赖倒置原则
上层模块不应该依赖于下层模块,他们共同依赖于一个抽象,即:父类不能依赖子类,他们都要依赖抽象类。
抽象不能依赖于具体,具体应该要依赖于抽象。
ans:
c是面向过程的,c++是面向对象的;
1、兼容c;
2、多了OOP面向对象。有继承、封装、多态三大特点,有虚函数、虚表指针。
3、泛型编程、模板、STL。
ans:
1、const_cast用于将const变量转为非const
2、static_cast用的最多,对于各种隐式转换,非const转const,void*转指针等, static_cast能用于多态想上转化,如果向下转能成功但是不安全,结果未知;
3、dynamic_cast用于动态类型转换。只能用于含有虚函数的类,用于类层次间的向上和向下转化。只能转指针或引用。向下转化时,如果是非法的对于指针返回NULL,对于引用抛异常。要深入了解内部转换的原理。
4、reinterpret_cast几乎什么都可以转,比如将int转指针,可能会出问题,尽量少用;
ans:
C的强制转换表面上看起来功能强大什么都能转,但是转化不够明确,不能进行错误检查,容易出错。
ans:
- 作用:
拷贝:用原对象创建并初始化新对象;
赋值:用原对象对已有对象进行赋值;
析构函数:释放对象等作用。
注意::拷贝构造函数中创建的对象是一个实实在在的新开辟内存区域的对象,而并不是一个指向原对象的指针。
- 声明方式:
拷贝: MyClass(const MyClass& mycla);
赋值: MyClass&MyClass::operator= (const MyClass & mycla);
析构函数 ~MyClass();
- 注意事项:
- 拷贝构造函数也是构造函数,所以没有返回值。拷贝构造函数的形参不限制为const,但是必须是一个引用,以传地址方式传递参数,否则导致拷贝构造函数无穷的递归下去,指针也不行,本质还是传值。
- 赋值构造函数是通过重载赋值操作符实现的,它接受的参数和返回值都是指向类对象的引用变量。
- 区别与共同点:
注意,拷贝构造函数和赋值构造函数的调用都是发生在有赋值运算符‘=’存在的时候,只是有一区别:
拷贝构造函数调用发生在对象还没有创建且需要创建时,如:MyClass obj1; MyClass obj2=obj1或MyClass obj2(obj1);
赋值构造函数仅发生在对象已经执行过构造函数,即已经创建的情况下,如:
MyClass obj1; MyClass obj2; obj2=obj1;
区别:拷贝构造函数就像变量初始化,赋值构造函数就如同变量赋值。前者是在用原对象创建新对象,而后者是在用原对象对已有对象进行赋值。
共同点:拷贝构造函数和赋值构造函数都是浅拷贝,所以遇到类成员含有指针变量时,类自动生成的默认拷贝构造函数和默认赋值构造函数就不灵了。因为其只可以将指针变量拷贝给新对象,而指针成员指向的还是同一内存区域,容易产生:冲突、野指针、多次释放等问题。解决方法就是自己定义具有深拷贝能力的拷贝构造函数或者赋值构造函数。
- 拷贝与赋值构造函数内在原理(m_data是String类成员):
// 拷贝构造函数
String::String(const String &other)
{
//允许操作other 的私有成员m_data
int length = strlen(other.m_data);
m_data = new char[length+1];(1)//开辟新对象内存
strcpy(m_data, other.m_data);(2)//复制内容到新对象
}
// 赋值函数
String & String::operator =(const String &other)
{
//(1) 检查自赋值
if(this == &other)
return *this;
//(2) 释放原有的内存资源
delete [] m_data;
//(3)分配新的内存资源,并复制内容
int length = strlen(other.m_data);
m_data = new char[length+1];
strcpy(m_data, other.m_data);
//(4)返回本对象的引用
return *this;
}
- 拷贝和赋值构造函数都是新开辟内存,然后复制内容进来;
- 赋值构造函数一定要最先检测本操作是否为自己给自己赋值,若是就会直接返回本身。若直接从第(2)步开始就会释放掉自身,从而造成第(3)步strcpy中的other找不到内存数据,从而使得赋值操作失败。
6. 将类中的析构函数设为私有,类外就不可以自动调用销毁对象,所以只可以通过new创建对象,手动delete销毁。
#define
命令是一个宏命令,它用来将一个标识符定义为一个字符串,该标识符被称为宏名,被定义的字符串称为替换文本。
该命令有两种格式:一种是不带参数的宏定义,另一种是带参数的宏定义。
C++ 内联函数是通常与类一起使用。如果一个函数是内联的,那么在编译时,编译器会把该函数的代码副本放置在每个调用该函数的地方。
如果想把一个函数定义为内联函数,则需要在函数名前面放置关键字 inline。
为什么使用内联函数?
函数调用是有调用开销的,执行速度要慢很多,调用函数要先保存寄存器,返回时再恢复,复制实参等等。
如果本身函数体很简单,那么函数调用的开销将远大于函数体执行的开销。为了减少这种开销,我们才使用内联函数。
内联函数使用的条件
以下情况不宜使用内联:
(1)如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。
(2)如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。
内联不是什么时候都能展开的,一个好的编译器将会根据函数的定义体,自动地取消不符合要求的内联。
#include
#include
struct test
{
int a;
char b;
int c;
short d;
};
typedef union
{
double i;
int k[5];
char c;
}DATE;
int main(int argc,char *argv)
{
/*在32位和64位的机器上,size_t的大小不同*/
printf("the size of struct test is %zu\n",sizeof(struct test));
return 0;
}
结构体
未对齐时:
0~3 | 4 | 5~9 | 10~11 |
---|---|---|---|
a | b | c | d |
对齐时:
0~3 | 4 | 5~7 | 8~11 | 12 ~ 13 | 14 ~ 15 |
---|---|---|---|---|---|
a | b | 填充内容 | c | d | 填充 |
联合
union中最大的变量类型是int[5],所以占用20个字节,大小是20,由于double占8字节,要进行8字节对齐,所以内存空间是8的倍数,最后所占空间是24。
总的来说,字节对齐有以下准则:
注意:64位机器指针大小为8字节,32为机器为4字节
#pragma pack () 取消指定对齐,恢复缺省对齐
#pragma pack (2) 2字节对齐
给联合中的成员赋值时,只会对这个成员所属的数据类型所占内存空间的大小覆盖成后来的这个值,而不会影响其他位置的值。
1. const int a; //指的是a是一个常量,不允许修改。
2. const int *a; //a指针所指向的内存里的值不变,即(*a)不变
3. int const *a; //同const int *a;
4. int *const a; //a指针所指向的内存地址不变,即a不变
5. const int *const a; //都不变,即(*a)不变,a也不变
对于C语言的全局和静态变量,初始化发生在任何代码执行之前,属于编译期初始化。
而C++标准规定:全局或静态对象当且仅当对象首次用到时才进行构造。
1. 修饰全局变量。该变量只能在该文件中使用,其他文件不可访问,存放在静态存储区。
2. 修饰局部变量。该变量作用域只在该局部函数里,出了函数静态局部变量不会被释放,如果未初始化默认会初始化为0。存放在静态存储区。
3. 修饰静态函数。在函数返回类型前加static,函数就定义为静态函数。函数的定义和声明在默认情况下都是extern的,但静态函数只是在声明他的文件当中可见,不能被其他文件所用。
4. 修饰成员变量,该变量为所有类对象共享,不需要this指针,并且不能和const一起使用,因为const需要this指针。
5. 修饰成员函数,用命名空间表示。
静态成员函数不属于任何一个对象,因此C++规定静态成员函数没有this指针。既然他没有指向某一个对象,也就无法对一个对象中的非静态成员进行访问。
静态成员函数没有this指针,只能访问静态成员;
普通成员函数有this指针,可以访问类中任意成员;而静态成员函数没有this指针。
mutable是为了突破const的限制而设置的。被mutable修饰的变量将永远处于可变的状态,即使在一个const函数中,甚至结构体变量或者类对象为const,其mutable成员也可以被修改。mutable在类中只能修饰非静态数据成员。
一个定义为volatile的变量是说这变量是说这变量可能会被意想不到的修改,寄存器中的值没有发生变化,但是内存中的值有可能发生了变化,因此需要每次都从内存中取值。
原子操作
原子操作(atomic operation)指的是由多步操作组成的一个操作。如果该操作不能原子地执行,则要么执行完所有步骤,要么一步也不执行,不可能只执行所有步骤的一个子集。
原子操作类似互斥锁,但是原子操作比锁效率更高,这是因为原子操作更加接近底层,它的实现原理是基于总线加锁和缓存加锁的方式。
在并发多线程的编程中,不同线程间对共享内存的竞争是存在一定危险的。所以C++11引入了自己的互斥量的概念来避免在多线程的运行中出现的问题,那么对于每次的加锁解锁以及其他的操作对于资源的消耗都是一定的,那么就又引入了std::atomic的类模板,实现了原子操作,从而避免了在数据的修改过程中被切换到另一个线程中,也就是说对于值的修改操作必须一次性执行完毕,中途不会被打断。atomic的运行效率上比互斥锁的效率要高好多。但是对于atomic和mutex的实际需要还需要根据设定情况来看,没有绝对的完美和高效。
std::atomic的用法简单,定义一个你所需要的变量就好,可以实现++,–,+=等操作,但是对于x = x + 1就不可用。
#include
#include
#include
std::atomic<int> myat;
void fun() {
for (int i = 0; i < 100000; i++) {
myat++;
}
}
int main()
{
std::thread t1(fun);
std::thread t2(fun);
t1.join();
t2.join();
std::cout << myat << std::endl;
return 0;
}
C++中有两种类型表达式:
变量是左值,因此可以出现在赋值号的左边。数值型的字面值是右值,因此不能被赋值,不能出现在赋值号的左边。
C++11引入右值引用主要是为了实现移动语义和完美转发。
移动语义为了避免临时对象的拷贝,为类增加移动构造函数。
完美转发,就是通过一个函数将参数继续转交给另一个函数进行处理,原参数可能是右值,可能是左值,如果还能继续保持参数的原有特征,那么它就是完美的。
移动语义为了避免临时对象的拷贝,为类增加移动构造函数。移动构造函数与拷贝构造不同,它并不是重新分配一块新的空间同时将要拷贝的对象复制过来,而是"拿"了过来,将自己的指针指向别人的资源,然后将别人的指针修改为nullptr
多线程编程中,变量的值在内存中可能已经被修改,而编译器优化优先从寄存器里读值,读取的并不是最新值。
解决办法:
使用值传递会调用拷贝构造函数,会陷入无穷递归之中。
在每一个成员函数中都包含一个特殊的指针,这个指针的名字是固定的,称为this指针。它是指向本类对象的指针,它的值是当前被调用的成员函数所在的对象的起始地址。
对于一个类的实例来说,你可以看到它的成员函数、成员变量,但是实例本身呢?
this是一个指针,它时时刻刻指向你这个实例本身。
基类指针可以按照基类的方式来做事,也可以按照派生类的方式来做事,它有多种形态,或者说有多种表现方式,我们将这种现象称为多态(Polymorphism)。
可以通过引用实现多态
能,因为在编译时对象就绑定了函数地址,和指针空不空没关系。
C++实现虚函数的原理是虚函数表+虚表指针。
当一个类里存在虚函数时,编译器会为类创建一个虚函数表,虚函数表是一个数组,数组的元素存放的是类中虚函数的地址。
同时为每个类的对象添加一个隐藏成员,该隐藏成员保存了指向该虚函数表的指针。该隐藏成员占据该对象的内存布局的最前端。
所以虚函数表只有一份,而有多少个对象,就对应多少个虚函数表指针。
当一个类里存在虚函数时,编译器会为类创建一个虚函数表,发生在编译期。
虚函数表只有一份,而有多少个对象,就对应多少个虚函数表指针。
在编译后,函数签名已经都不一样了,自然也就不冲突了。这就是为什么C++可以实现重名函数,但实际上编译后的函数签名是不一样的。
签名命名的方式是:_z+函数名字符个数+函数参数列表。
STL的分配器用于封装STL容器在内存管理上的底层细节。在C++中,其内存配置和释放如下:
为了精密分工,STL allocator将两个阶段操作区分开来:内存配置有alloc::allocate()负责,内存释放由alloc::deallocate()负责;对象构造由::construct()负责,对象析构由::destroy()负责。同时为了提升内存管理的效率,减少申请小内存造成的内存碎片问题,SGI STL采用了两级配置器,当分配的空间超过128字节时,会使用第一级空间配置器;当分配的空间小于128字节时,将使用第二级空间配置器。第一级空间配置器直接使用malloc()、realloc()、free()函数进行内存空间的分配和释放,而第二级空间配置器采用了内存池技术,通过空闲链表来管理内存。
map和set都是C++的关联容器,其底层实现都是红黑树(RB-Tree)。由于 map 和set所开放的各种操作接口,RB-tree 也都提供了,所以几乎所有的 map 和set的操作行为,都只是转调 RB-tree 的操作行为。
map和set区别在于:
二级配置器结构 STL内存管理使用二级内存配置器。
二级内存池采用了16个空闲链表,这里的16个空闲链表分别管理大小为8、16、24…120、128的数据块。这里空闲链表节点的设计十分巧妙,这里用了一个联合体既可以表示下一个空闲数据块(存在于空闲链表中)的地址,也可以表示已经被用户使用的数据块(不存在空闲链表中)的地址。
空间配置函数allocate 首先先要检查申请空间的大小,如果大于128字节就调用第一级配置器,小于128字节就检查对应的空闲链表,如果该空闲链表中有可用数据块,则直接拿来用(拿取空闲链表中的第一个可用数据块,然后把该空闲链表的地址设置为该数据块指向的下一个地址),如果没有可用数据块,则调用refill重新填充空间。
空间释放函数deallocate 首先先要检查释放数据块的大小,如果大于128字节就调用第一级配置器,小于128字节则根据数据块的大小来判断回收后的空间会被插入到哪个空闲链表。
重新填充空闲链表refill 在用allocate配置空间时,如果空闲链表中没有可用数据块,就会调用refill来重新填充空间,新的空间取自内存池。缺省取20个数据块,如果内存池空间不足,那么能取多少个节点就取多少个。 从内存池取空间给空闲链表用是chunk_alloc的工作,首先根据end_free-start_free来判断内存池中的剩余空间是否足以调出nobjs个大小为size的数据块出去,如果内存连一个数据块的空间都无法供应,需要用malloc取堆中申请内存。 假如山穷水尽,整个系统的堆空间都不够用了,malloc失败,那么chunk_alloc会从空闲链表中找是否有大的数据块,然后将该数据块的空间分给内存池(这个数据块会从链表中去除)。
总结:
Include头文件的顺序:对于include的头文件来说,如果在文件a.h中声明一个在文件b.h中定义的变量,而不引用b.h。那么要在a.c文件中引用b.h文件,并且要先引用b.h,后引用a.h,否则会报变量类型未声明错误。
双引号和尖括号的区别:编译器预处理阶段查找头文件的路径不一样。
对于使用双引号包含的头文件,查找头文件路径的顺序为:
对于使用尖括号包含的头文件,查找头文件的路径顺序为:
1. push_back()和emplace_back()的区别
emplace_back() 和 push_back() 的区别,就在于底层实现的机制不同。push_back() 向容器尾部添加元素时,首先会创建这个元素,然后再将这个元素拷贝或者移动到容器中(如果是拷贝的话,事后会自行销毁先前创建的这个元素);而 emplace_back() 在实现时,则是直接在容器尾部创建这个元素,省去了拷贝或移动元素的过程。
2. insert()和emplace()的区别
当调用push或insert成员函数时,我们将元素类型的对象传递给他们,这些对象被拷贝到容器中。而当我们调用一个emplace成员函数时,则是将参数传递给元素类型的构造函数。emplace成员使用这些参数在容器管理的内存空间中直接构造元素。
- deque采用一块map作为主控,这里的map并非STL中的map容器,而是类似于动态一维数组的一小块连续的空间,其中每一个元素(node)都是一个指针,这个指针指向另一段较大的连续线性空间
- 中控区中指针指向的内存段称为缓冲区,缓冲区才是deque的存储空间主体
- deque迭代器具有的结构:
cur:迭代器表示的当前元素
first:缓冲区Buffer的起始位置
last:缓冲区Buffer的结束为止
node:保存此时位于map中的哪一个指针
vector拥有一段连续的内存空间,因此支持随机访问,如果需要高效的随即访问,而不在乎插入和删除的效率,使用vector。
list拥有一段不连续的内存空间,如果需要高效的插入和删除,而不关心随机访问,则应使用list。
迭代器
Iterator(迭代器)模式又称Cursor(游标)模式,用于提供一种方法顺序访问一个聚合对象中各个元素, 而又不需暴露该对象的内部表示。或者这样说可能更容易理解:Iterator模式是运用于聚合对象的一种模式,通过运用该模式,使得我们可以在不知道对象内部表示的情况下,按照一定顺序(由iterator提供的方法)访问聚合对象中的各个元素。
由于Iterator模式的以上特性:与聚合对象耦合,在一定程度上限制了它的广泛运用,一般仅用于底层聚合支持类,如STL的list、vector、stack等容器类及ostream_iterator等扩展iterator。
迭代器和指针的区别
迭代器不是指针,是类模板,表现的像指针。他只是模拟了指针的一些功能,通过重载了指针的一些操作符, ->、* 、++ 、–等。迭代器封装了指针,是一个“可遍历STL( Standard Template Library)容器内全部或部分元素” 的对象, 本质是封装了原生指针,是指针概念的一种提升(lift),提供了比指针更高级的行为,相当于一种智能指针,他可以根据不同类型的数据结构来实现不同的 ++,-- 等操作。
迭代器返回的是对象引用而不是对象的值,所以cout只能输出迭代器使用 * 取值后的值而不能直接输出其自身。
迭代器产生原因
Iterator类的访问方式就是把不同集合类的访问逻辑抽象出来,使得不用暴露集合内部的结构而达到循环遍历集合的效果。
这个主要考察的是迭代器失效的问题。
ans:
红黑树。unordered map底层结构是哈希表
方式 | 函数 | key值已存在时是否会覆盖原value值 |
---|---|---|
方法一 | pair | 不会覆盖 |
方法二 | make_pair | 不会覆盖 |
方法三 | value_type | 不会覆盖 |
方法四 | [ ] | 会覆盖 |
STL主要由:以下几部分组成:
容器、迭代器、仿函数、算法、分配器、配接器
他们之间的关系:
ans:
1、Map映射:map 的所有元素都是 pair,同时拥有实值(value)和键值(key)。pair 的第一元素被视为键值,第二元素被视为实值。所有元素都会根据元素的键值自动被排序。不允许键值重复。
底层实现:红黑树
适用场景:有序键值对不重复映射
2、Multimap多重映射:multimap 的所有元素都是 pair,同时拥有实值(value)和键值(key)。pair 的第一元素被视为键值,第二元素被视为实值。所有元素都会根据元素的键值自动被排序。允许键值重复。
底层实现:红黑树
适用场景:有序键值对可重复映射
map的底层是红黑树,unordered_map底层是哈希表,明明哈希表的查询效率更高,为什么还需要红黑树?
hashmap有unordered_map,map其实就是很明确的红黑树。map比起unordered_map的优势主要有:
ans:
调用顺序:
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
首先创建一个epoll对象,然后使用epoll_ctl对这个对象进行操作,把需要监控的描述添加进去,这些描述如将会以epoll_event结构体的形式组成一颗红黑树,接着阻塞在epoll_wait,进入大循环,当某个fd上有事件发生时,内核将会把其对应的结构体放入到一个链表中,返回有事件发生的链表。
ans:
vector FindMax(vector &num)
{
int len=num.size();
if(len==0) return {}; //空数组,返回空
vector res(len,-1); //返回结果:初始化-1,表示未找到
stack notFind; //栈:num中还未找到符合条件的元素索引
int i=0;
while(i=num[i])
{
notFind.push(i++);//处理索引
}
//有待处理元素,且num当前元素大于栈顶 索引 元素,符合条件,更新结果数组中该索引的值,栈顶出栈。
else
{
res[notFind.top()]=num[i];
notFind.pop();
}
}
return res;
}
ans:
了解这两个函数的区别,首先要搞清楚容器的capacity(容量)与size(长度)的区别。
size:指容器当前拥有的元素个数;
capacity:则指容器在必须分配新存储空间之前可以存储的元素总数。
也可以说是预分配存储空间的大小。
resize()函数和容器的size息息相关。调用resize(n)后,容器的size即为n。
至于是否影响capacity,取决于调整后的容器的size是否大于capacity,大于size,capacity会成倍增长。
从两个函数的用途可以发现,容器调用resize()函数后,所有的空间都已经初始化了,所以可以直接访问。
而reserve()函数预分配出的空间没有被初始化,所以不可访问
ans:
集合,所有元素都会根据元素的值自动被排序,且不允许重复。
底层实现:红黑树
set: 底层是通过红黑树(RB-tree)来实现的,由于红黑树是一种平衡二叉搜索树,自动排序的效果很不错,所以标准的 STL 的 set 即以 RB-Tree 为底层机制。又由于 set 所开放的各种操作接口,RB-tree 也都提供了,所以几乎所有的 set 操作行为,都只有转调用 RB-tree 的操作行为而已。
适用场景:有序不重复集合
map: 的所有元素都是 pair,同时拥有实值(value)和键值(key)。pair 的第一元素被视为键值,第二元素被视为实值。所有元素都会根据元素的键值自动被排序。不允许键值重复。
底层:红黑树
适用场景:有序键值对不重复映射
段错误通常发生在访问非法内存地址的时候,具体来说分为以下几种情况:
class A
{
private:
A(){ }
~A(){ }
public:
void Instance()//类A的内部的一个函数
{
A a;
}
};
上面的代码是能通过编译的。上面代码里的Instance函数就是类A的内部的一个函数。Instance函数体里就构建了一个A的对象。
但是,这个Instance函数还是不能够被外面调用的。为什么呢?
如果要调用Instance函数,必须有一个对象被构造出来。但是构造函数被声明为private的了。外部不能直接构造一个对象出来。
A aObj; // 编译通不过
aObj.Instance();
但是,如果Instance是一个static静态函数的话,就可以不需要通过一个对象,而可以直接被调用。
#include
using namespace std;
class A
{
private:
A():data(10){ cout << "A" << endl; }
~A(){ cout << "~A" << endl; }
public:
static A& Instance()
{
static A a;
return a;
}
void Print()
{
cout << data << endl;
}
private:
int data;
};
int main(int argc, char** argv)
{
A& ra = A::Instance();
ra.Print();
}
上面的代码其实是设计模式singleton模式的一个简单的C++代码实现。
还有一个情况是:通常将拷贝构造函数和operator=(赋值操作符重载)声明成private,但是没有实现体。这个的目的是禁止一个类的外部用户对这个类的对象进行复制动作。
create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
>首先创建一个epoll对象,然后使用epoll_ctl对这个对象进行操作,把需要监控的描述添加进去,这些描述如将会以epoll_event结构体的形式组成一颗红黑树,接着阻塞在epoll_wait,进入大循环,当某个fd上有事件发生时,内核将会把其对应的结构体放入到一个链表中,返回有事件发生的链表。
#### 73、n个整数的无序数组,找到每个元素后面比它大的第一个数,要求时间复杂度为O(N) ?
**ans:**
```c++
#include
int main()
{
int i = 6;
int j = 1;
if(i > 0 || (j++) > 0)
{
printf("%d\r\n",j);
}
return 0;
}
// 结果为 1
对于条件语句,||
前如果为true,则不必执行后面的语句,同理,如果&&
前面为false,也不必执行后面的语句,这就是短路求值。
排序比较
排序算法 | 平均复杂度 | 最坏时间复杂度 | 空间复杂度 | 是否稳定 |
---|---|---|---|---|
冒泡排序 | O(n2) | O(n2) | O(1) | 是 |
选择排序 | O(n2) | O(n2) | O(1) | 不是 |
直接插入排序 | O(n2) | O(n2) | O(1) | 是 |
归并排序 | O(nlogn) | O(nlogn) | O(n) | 是 |
快速排序 | O(nlogn) | O(n2) | O(logn) | 是 |
堆排序 | O(nlogn) | O(nlogn) | O(1) | 不是 |
希尔排序 | O(nlogn) | O(n8) | O(1) | 不是 |
计数排序 | O(n+k) | O(n+k) | O(n+k) | 是 |
基数排序 | O(N*M) | O(N*M) | O(M) | 是 |
稳定性:快希选堆
STL的sort算法,数据量大时采用快速排序算法,分段归并排序。一旦分段后的数据量小于某个门槛(16),为避免QuickSort快排的递归调用带来过大的额外负荷,就改用插入排序。如果递归层次过深,还会改用堆排序。
适用:deque、vector、list
答:不可以!
这里说不可以,不是说真的不可以,而是说真的别这样!有些情况下是可以用的,因为类只是一个说明,对象也是这个类的一个具体化了的内存块,当你memset一个对象时,它把这块对象内存初始化了,在不影响内部结构的情况下是不会有问题的,这就是为什么有时候使用memset一个对象时不会出错的原因。
每个包含虚函数的类对象都有一个指针指向虚函数表(vtbl)。这个指针被用于解决运行时以及动态类型强制转换时虚函数的调用问题。该指针是被隐藏的,对程序员来说,这个指针也是不可存取的。当进行memset操作时,这个指针的值也要被overwrite,这样一来,只要一调用虚函 数,程序便崩溃。这在很多由C转向C++的程序员来说,很容易犯这个错误,而且这个错误很难查。
大端模式:低字节在高地址上,高字节在低地址上。
小端模式:高字节在高地址上,低字节在低地址上。
如何判断计算机是大端还是小端
#include
bool checkCPU()
{
{
union w
{
int a;
char b;
}c;
c.a = 1;
return (c.b == 1);
}
}
小端则1存在低地址上,取b时可以取出1;
大端则1存在高地址上,取b时只能取出0;
1、负数
正数负数都是补码存放,正数补码为原码,负数(-1 ----> 1000 0001 高位表示符号)先反码,然后反码加一为补码 。
当初始化一个字节单位的数组时,可以用memset把每个数组单元初始化成任何你想要的值,比如,
char data[10];
memset(data, 1, sizeof(data)); // right
memset(data, 0, sizeof(data)); // right
而在初始化其他基础类型时,则需要注意,比如,
int data[10];
memset(data, 0, sizeof(data)); // right
memset(data, -1, sizeof(data)); // right
memset(data, 1, sizeof(data)); // wrong, data[x] would be 0x0101 instead of 1
比如如下代码中,
struct Parameters {
int x;
int* p_x;
};
Parameters par;
par.p_x = new int[10];
memset(&par, 0, sizeof(par));
当memset初始化时,并不会初始化p_x指向的int数组单元的值,而会把已经分配过内存的p_x指针本身设置为0,造成内存泄漏。同理,对std::vector等数据类型,显而易见也是不应该使用memset来初始化的。
这个问题就是在开头项目中发现的问题,如下代码中,
class BaseParameters
{
public:
virtual void reset() {}
};
class MyParameters : public BaseParameters
{
public:
int data[3];
int buf[3];
};
MyParameters my_pars;
memset(&my_pars, 0, sizeof(my_pars));
BaseParameters* pars = &my_pars;
//......
MyParameters* my = dynamic_cast<MyParameters*>(pars);
程序运行到dynamic_cast时发生异常。原因其实也很容易发现,我们的目的是为了初始化数据结构MyParameters里的data和buf,正常来说需要初始化的内存空间是sizeof(int) * 3 * 2 = 24字节,但是使用memset直接初始化MyParameters类型的数据结构时,sizeof(my_pars)却是28字节,因为为了实现多态机制,C++对有虚函数的对象会包含一个指向虚函数表(V-Table)的指针,当使用memset时,会把该虚函数表的指针也初始化为0,而dynamic_cast也使用RTTI技术,运行时会使用到V-Table,可此时由于与V-Table的链接已经被破坏,导致程序发生异常。
// error
strArry* GrientArr;
memset(GrientArr,0,sizeof(strArry));
//right
strArry* GrientArr=new strArry;
memset(GrientArr,0,sizeof(strArry));