一、 整体介绍
1 介绍面向对象的三大特性。
面向对象是一种基于对象的、基于类的软件开发思想。面向对象具有继承、封装、多态的特性。
封装:把客观的事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的类进行信息的隐藏。
继承:指的是建立一个新的派生类,从一个或多个先前定义的类中继承数据和函数,可以重新定义或加进新数据和函数,从而建立了类的层次或等级。
多态:同一操作作用于不同类的实例,将产生不同的执行结果,即不同类的对象收到相同的消息时,将得到不同的结果。
2 C++11新特性
C++11 最常用的新特性如下:
auto关键字:编译器可以根据初始值自动推导出类型。但是不能用于函数传参以及数组类型的推导
nullptr关键字:nullptr是一种特殊类型的字面值,它可以被转换成任意其它的指针类型;而NULL一般被宏定义为0,在遇到重载时可能会出现问题。
智能指针:C++11新增了std::shared_ptr、std::weak_ptr等类型的智能指针,用于解决内存管理的问题。
初始化列表:使用初始化列表来对类进行初始化
右值引用:基于右值引用可以实现移动语义和完美转发,消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率
atomic原子操作用于多线程资源互斥操作
新增STL容器array以及tuple
二、存储
1 C++ 内存的三种分配方式
1. 从静态存储区分配:此时的内存在程序编译的时候已经分配好,并且在程序的整个运行期间都存在。全局变量,static变量等在此存储。
2. 在栈区分配:相关代码执行时创建,执行结束时被自动释放。局部变量在此存储。栈内存分配运算内置于处理器的指令集中,效率高,但容量有限。函数的局部变量,返回值
3. 在堆区分配:动态分配内存。用new/malloc时开辟,delete/free时释放。生存期由用户指定,灵活。但有内存泄露等问题
2 堆和栈的区别
1、堆空间的内存是动态分配的,一般存放对象,并且需要手动释放内存。
2、栈空间的内存是由系统自动分配,一般存放局部变量,比如对象的地址等值,不需要程序员对这块内存进行管理,比如,函数中的局部变量的作用范围(生命周期)就是在调完这个函数之后就结束了。
3 new和delete是如何实现的,new 与 malloc的异同处
(2)Malloc和free在C程序中使用,而C++程序中使用new和delete,删除数组delete[]p,指针释放后,要将指针置空。
(3)New和delete可以调用构造函数和析构函数。
(4)Malloc是函数,new是关键字。
(5)Malloc不能赋初值,new可以,如int *p = new int(2).代表分配一个int型的内存空间,并赋初值2.如果new int ()代表赋初值0,new int[10]代表分配10个int.
(6)Malloc返回的指针是void *类型,而new返回的指针是它分配空间的类型。
4 使用free释放new的东西可以吗?
通过 free 调用释放 new 申请的内存并不总是能正确的释放所有申请的内存。因为使用 free 方法释放内存时并不会调用实例的析构函数,此时如果实例中有动态申请的内存将因为析构函数没有被调用而没有得到释放,从而导致内存泄漏。而通常你不一定总能知道该类中是否使用了动态内存,因此最佳的做法是 new 与 delete 搭配使用。
5 计算下面几个类的大小
class A {};: sizeof(A) = 1;
class A { virtual Fun(){} };: sizeof(A) = 4 (32位机器) / 8(64位机器);
class A { static int a; };: sizeof(A) = 1;
class A { int a; };: sizeof(A) = 4;
class A { static int a; int b; };: sizeof(A) = 4;
空类型的实例中不包含任何信息,本来求sizeof的结果应该是0,但是当我们声明该类型的实例时,必须在内存中占有一定得空间,否则无法使用这些实例。每个空类型的实例占用1字节的空间。
空类中添加一个构造函数和析构函数,sizeof() 还是1,。调用构造函数和析构函数只需知道函数的地址即可,而这些函数的地址只与类型相关,而与类型的实例无关,编译器也不会因为这两个函数而在实例内添加额外的信息。
如果发现有虚函数,就会为该类型生成虚函数表,并在该类型的每一个实例中添加指向虚函数表的指针(只有一个指针),在32位的机器上,一个指针占4字节空间,因此求得sizeof()为4;64位机器中,一个指针占8字节空间,因此得到sizeof为8。
static int内存分布在静态全局区,所以不占类的内存空间。
一个类中,虚函数、成员函数(包括静态与非静态)和静态数据成员都不占用类对象的存储空间。
6 内存泄露的定义,如何检测与避免
堆内存泄漏 (Heap leak)。对内存指的是程序运行中根据需要分配通过malloc,realloc new等从堆中分配的一块内存,再是完成后必须通过调用对应的 free或者delete 删掉。如果程序的设计的错误导致这部分内存没有被释放,那么此后这块内存将不会被使用,就会产生Heap Leak.
系统资源泄露(Resource Leak).主要指程序使用系统分配的资源比如 Bitmap,handle ,SOCKET等没有使用相应的函数释放掉,导致系统资源的浪费,严重可导致系统效能降低,系统运行不稳定。
在windows平台下通过CRT中的库函数进行检测;
在可能泄漏的调用前后生成块的快照,比较前后的状态,定位泄漏的位置
Linux下通过工具valgrind检测
三、指针、引用、define、const、static
1 指针和引用的区别
指针保存的是指向对象的地址,引用相当于变量的别名
引用在定义的时候必须初始化,指针没有这个要求
指针可以改变地址,引用必须从一而终
不存在空应引用,但是存在空指针NULL,相对而言引用更加安全
引用的创建不会调用类的拷贝构造函数
C++编译器在编译过程中用指针常量作为引用的内部实现
2 Struct和class的区别
在C++中,可以用struct和class定义类,都可以继承。
区别在于:structural的默认继承权限和默认访问权限是public,而class的默认继承权限和默认访问权限是private。
另外,class还可以定义模板类形参,比如template <class T, int i>。
3 define 和const的区别(编译阶段、安全性、内存占用等)
就起作用的阶段而言:#define是在预处理阶段进行替换, const修饰的只读变量是在编译的时候确定其值。
就起作用的方式而言:#define只是简单的字符串替换,没有类型检查。而const有对应的类型,是要进行判断的,可以避免一些低级的错误
就内存分配而言:编译器通常不为普通的const只读变量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的值,没有了存储与读内存的操作,使得它的效率也很高
4 const和static在类中使用的注意事项(定义、初始化和使用)
const类成员:
(1)const static int 可以使static数据成员在类定义体内初始化;
(2)可以通过将数据成员声明为mutable来实现对该类const对象的该数据成员的更改;
(3)初始化const数据成员的唯一机会是在构造函数的初始化列表中,直接在类的定义体中以及在构造函数的定义体中初始化都是不正确的。
(4)构造函数不能声明为const
static类成员:每个static对象是与类关联的对象,并不与该类的对象相关联。不同的类对象中共享static数据成员。
static函数:
(1)没有this指针,也不能被声明为虚函数和const函数。
(2)可以直接访问所属类的static成员,但不能直接使用非static成员,而非static函数可使用static成员。
5 Static关键字的作用
(1)全局静态变量
作用域:全局静态变量在声明他的文件之外是不可见的。
(2)局部静态变量
内存中的位置:静态存储区
作用域:作用域仍为局部作用域,当定义它的函数或者语句块结束的时候,作用域结束。但是当局部静态变量离开作用域后,并没有销毁,而是仍然驻留在内存当中,只不过我们不能再对它进行访问,直到该函数再次被调用,并且值不变;
(3)静态函数
在函数返回类型前加static,函数就定义为静态函数。函数的定义和声明在默认情况下都是extern的,但静态函数只是在声明他的文件当中可见,不能被其他文件所用。
函数的实现使用static修饰,那么这个函数只可在本cpp内使用,不会同其他cpp中的同名函数引起冲突;
warning:不要再头文件中声明static的全局函数,不要在cpp内声明非static的全局函数,如果你要在多个cpp中复用该函数,就把它的声明提到头文件里去,否则cpp内部声明需加上static修饰。
static修饰成员变量
对于非静态数据成员,每个类对象都有自己的拷贝。而静态数据成员被当做是类的成员,无论这个类被定义了多少个,静态数据成员都只有一份拷贝,为该类型的所有对象所共享(包括其派生类)。所以,静态数据成员的值对每个对象都是一样的,它的值可以更新。
因为静态数据成员在全局数据区分配内存,属于本类的所有对象共享,所以它不属于特定的类对象,在没有产生类对象前就可以使用。
static修饰成员函数
与普通的成员函数相比,静态成员函数由于不是与任何的对象相联系,因此它不具有this指针。从这个意义上来说,它无法访问属于类对象的非静态数据成员,也无法访问非静态成员函数,只能调用其他的静态成员函数。
static修饰的成员函数,在代码区分配内存。
(4) 类的静态成员
在类中,静态成员可以实现多个对象之间的数据共享。
(5) 类的静态函数
在静态成员函数的实现中不能直接引用类中说明的非静态成员,可以引用类中说明的静态成员(这点非常重要)。如果静态成员函数中要引用非静态成员时,可通过对象来引用。从中可看出,调用静态成员函数使用如下格式:<类名>::<静态成员函数名>(<参数表>);
6 C++中的const类成员函数(用法和意义),以及和非const成员函数的区别
const修饰成员函数,说明该函数不应该修改非静态成员,但是这并不是十分可靠的,指针所指的非成员对象值可能会被改变
在一个类的函数后面加上const后,就表明这个函数是不能改变类的成员变量的(加了mutable修饰的除外。
7 C++的顶层const和底层const
当指针本身被限定时,称指针为顶层const;
当指针所指的对象被限定为常量时,而指针本身未被限定,称指针为底层const
8 inline和宏定义的区别
内联函数和普通函数相比可以加快程序运行的速度,因为不需要中断调用,在编译的时候内联函数可以直接被镶嵌到目标代码中。而宏只是一个简单的替换。
内联函数要做参数类型检查,这是内联函数的优势;
对于短小的代码来说inline增加空间消耗换来的是效率提高,这方面和宏是一模一样的,但是inline在和宏相比没有付出任何额外代价的情况下更安全。
宏是在代码处不加任何验证的简单替代,而内联函数是将代码直接插入调用处,而减少了普通函数调用时的资源消耗。
宏不是函数,只是在编译前(编译预处理阶段)将程序中有关字符串替换成宏体。
四、虚函数
1 多态的实现
A 在编译期间实现多态
多态是指在不同的条件下表现出不同的状态,C++中通过重载函数的方法可以在编译期间实现多态。
B 使用虚函数实现多态
C++中运行时多态可以通过声明一个虚函数来实现。虚函数分为纯虚方法和半虚方法,纯函数父类没有实现版本,完全交给子类,且必须实现。半虚函数父类可以实现,子类需要重写,他们都由关键字virtual修饰。
运行时多态的条件:
必须是集成关系
基类中必须包含虚函数,并且派生类中一定要对基类中的虚函数进行重写。
通过基类对象的指针或者引用调用虚函数
以下函数不能作为虚函数
1)友元函数,它不是类的成员函数
2)全局函数
3)静态成员函数,它没有this指针
3)构造函数,拷贝构造函数,以及赋值运算符重载(可以但是一般不建议作为虚函数)
虚函数的实现原理
一个类中如果有虚函数声明,那么这些函数会由一个虚函数表来维护
1) 每个父类都有自己的虚表。
2) 子类的成员函数被放到了第一个父类的表中。
3) 内存布局中,其父类布局依次按声明顺序排列。
4) 每个父类的虚表中的函数都被overwrite成了子类函数。这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。
C++中虚函数表位于只读数据段(.rodata),也就是C++内存模型中的常量区;
而虚函数则位于代码段(.text),也就是C++内存模型中的代码区。
子类若重写父类虚函数,虚函数表中,该函数的地址会被替换,对于存在虚函数的类的对象,在VS中,对象的对象模型的头部存放指向虚函数表的指针,通过该机制实现多态。
如果一个类是局部变量则该类数据存储在栈区,如果一个类是通过new/malloc动态申请的,则该类数据存储在堆区。
如果该类是virutal继承而来的子类,则该类的虚函数表指针和该类其他成员一起存储。虚函数表指针指向只读数据段中的类虚函数表,虚函数表中存放着一个个函数指针,函数指针指向代码段中的具体函数。
如果类中成员是virtual属性,会隐藏父类对应的属性。
虚函数有关内存存储
1)通常,编译器给类的每个对象添加一个隐藏的成员,这个成员保存了一个指向虚函数表(virtual function table,vtbl)的指针,而虚函数表中保存了类对象进行声明的虚函数的地址。也就是说我们可以通过这个隐藏成员访问虚函数表,进而访问被声明的虚函数的地址,从而调用虚函数。
2)现在来看基类和派生类虚函数表的区别和联系,首先需要知道,基类和派生类的虚函数表是两个东西,保存在不同位置的两个独立的数组,也就是说基类的隐藏成员和派生类的隐藏成员指向不同的地址。
3)如果派生类没有重新定义基类的某个虚函数A,则派生类的虚函数表vtbl将保存基类的虚函数A的原始地址(此时派生类和基类的虚函数表中保存的虚函数A的地址是一样的)
4)如果派生类重写了基类的某个虚函数B,则派生类的虚函数表vtbl将保存新的虚函数B的地址(此时的虚函数B其实有两个版本,分别被基类和派生类的虚函数表分开保存)
5)如果派生类定义了新的虚函数C,则派生类的虚函数表vtbl将保存新虚函数C的地址
构造函数或者析构函数中调用虚函数会怎样
Effective C++ 条款9
这类调用不会下降至derived class,派生类对象在基类构造期间,对象类型是基类而不是派生类。同理,进入基类析构函数后对象就成为一个基类对象。
解决办法:在基类中将调用函数改成非虚函数,然后要求派生类构造函数传递必要的信息给基类构造函数,而后那个构造函数就可以安全地调用非虚函数了。
C++中的重载和重写的区别:
重载的函数都是在类内的。只有参数类型或者参数个数不同,重载不关心返回值的类型。
覆盖(重写)派生类中重新定义的函数,其函数名,返回值类型,参数列表都跟基类函数相同,并且基类函数前加了virtual关键字。
隐藏是指派生类的函数屏蔽了与其同名的基类函数,注意只要同名函数,不管参数列表是否相同,基类函数都会被隐藏。有两种情况:(1)参数列表不同,不管有无virtual关键字,都是隐藏;(2)参数列表相同,但是无virtual关键字,也是隐藏
五、构造析构函数
析构函数作用:当对象结束其生命周期,如对象所在的函数已调用完毕时,系统自动执行析构函数。析构函数往往用来做“清理善后” 的工作(例如在建立对象时用new开辟了一片内存空间,delete会自动调用析构函数后释放内存)按照 C++ 的要求,只要有 new 就要有相应的 delete 。这个 new 是在构造函数里 new 的,就是出生的时候。所以在死掉的时候,就是调用析构函数时,我们必须对指针进行 delete 操作。
(24)析构函数一般写成虚函数的原因。为什么C++默认的析构函数不是虚函数?考点:虚函数 析构函数
基类析构函数不是虚函数,但在main函数中,用基类指针操作子类,释放指针P的过程是:只是释放了基类申请的资源,而没有调用子类的析构函数,没有释放了子类申请的资源。调用fun1()函数,执行的也是基类定义的函数。
一般情况下,这样的删除只能够删除基类对象,而不能删除子类对象,形成了删除一半形象,造成内存泄漏。
这段代码中,基类析构函数是虚函数。在main函数中,用基类指针操作子类,释放指针P的过程是:先释放子类申请的资源,再调用基类析构函数,释放父类申请的资源。调用fun1()函数,执行的也是子类中定的函数,表现出了多态特征。
将可能会被继承的父类的析构函数设置为虚函数,可以保证当我们new一个子类,然后使用基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄漏。
C++默认的析构函数不是虚函数是因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。因此C++默认的析构函数不是虚函数,而是只有当需要当作父类时,设置为虚函数。
(33) 什么情况下会调用拷贝构造函数(三种情况)
1)用类的一个对象去初始化另一个对象的时候
2)当函数的参数是类的对象时,就是值传递的时候,如果是引用传递则不会调用
3)当函数的返回值是类的对象或者引用的时候
(24)构造函数、拷贝构造函数和赋值操作符的区别
构造函数:对象不存在,没用别的对象初始化
拷贝构造函数:对象不存在,用别的对象初始化 A (const A&other)
赋值运算符:对象存在,用别的对象给它赋值 A& operator = (const A& other)
如果不想写拷贝构造函数和赋值函数,又不允许别人使用编译器生成的缺省函数,最简单的办法是将拷贝构造函数和赋值函数声明为私有函数
拷贝构造函数必须是引用传递传递地址,不可以值传递,否则会陷入无限循环创建的过程。
因为默认的拷贝构造函数是按成员拷贝构造,这导致了两个不同的指针(如ptr1=ptr2)指向了相同的内存。当一个实例销毁时,调用析构函数free(ptr1)释放了这段内存,那么剩下的一个实例的指针ptr2就无效了,在被销毁的时候free(ptr2)就会出现错误了, 这相当于重复释放一块内存两次。这种情况必须显式声明并实现自己的拷贝构造函数,来为新的实例的指针分配新的内存
因为系统提供的默认拷贝构造函数工作方式是内存拷贝,也就是浅拷贝。如果对象中用到了需要手动释放的对象,则会出现问题,这时就要手动重载拷贝构造函数,实现深拷贝。
(30) 深拷贝和浅拷贝的区别(举例说明深拷贝的安全性)
深拷贝和浅拷贝可以简单的理解为:如果一个类拥有资源,当这个类的对象发生复制过程的时候,如果资源重新分配了就是深拷贝;反之没有重新分配资源,就是浅拷贝。
在对含有指针成员的对象进行拷贝时,必须要自己定义拷贝构造函数,使拷贝后的对象指针成员有自己的内存空间,即进行深拷贝,这样就避免了内存泄漏发生。
总结:浅拷贝只是对指针的拷贝,拷贝后两个指针指向同一个内存空间,深拷贝不但对指针进行拷贝,而且对指针指向的内容进行拷贝,经深拷贝后的指针是指向两个不同地址的指针。
(25)构造函数声明为explicit
普通构造函数能被隐式调用,而explicit构造函数只能被显示调用。
不带explicit的优点:可以从构造函数的参数类型向类类型隐式转换
带explicit的优点:任何构造函数(尤其是带一个参数的)都不能隐式地创建类对象
带explicit的缺点:该构造函数只能以直接初始化的形式使用
(25)构造函数为什么一般不定义为虚函数
虚函数的执行依赖于虚函数表。而虚函数表需要在构造函数中进行初始化工作,即初始化vptr,让他指向正确的虚函数表。而在构造对象期间,虚函数表还没有被初始化,将无法进行。
(26)构造函数的几种关键字(default delete 0)
= default:将拷贝控制成员定义为=default显式要求编译器生成合成的版本。显式缺省(告知编译器生成函数默认的缺省版本)
= delete:将拷贝构造函数和拷贝赋值运算符定义删除的函数,阻止拷贝(析构函数不能是删除的函数 C++Primer P450)
= 0:将虚函数定义为纯虚函数(纯虚函数无需定义,= 0只能出现在类内部虚函数的声明语句处;当然,也可以为纯虚函数提供定义,不过函数体必须定义在类的外部)
(32) 介绍C++所有的构造函数
无参数构造函数
有参数构造函数
拷贝构造函数
默认构造函数
(56)请你回答一下C++中拷贝赋值函数的形参能否进行值传递?
不能。如果是这种情况下,调用拷贝构造函数的时候,首先要将实参传递给形参,这个传递的时候又要调用拷贝构造函数。如此循环,无法完成拷贝,栈也会满。
(41) 成员初始化列表的概念,为什么用成员初始化列表会快一些(性能优势)?
①:成员类型是没有默认构造函数的类。若没有提供显示初始化,则类创建对象时会调用默认构造函数,如果没有默认构造函数,则必须显示初始化。
②:const成员或者引用类型的成员。因为const对象或者引用类型只能初始化,不能赋值。
为什么成员初始化列表效率更高?
因为对于非内置类型,少了一次调用默认构造函数的过程
(53)拷贝初始化和直接初始化,初始化和赋值的区别
(28) 静态类型和动态类型,静态绑定和动态绑定的介绍
C++中由于继承导致对象的指针和引用具有两种不同的类型,静态类型和动态类型。
对象的静态类型:对象在声明时采用的类型,在编译时确定;
对象的动态类型:目前所指对象的类型,在运行期时确定。
特别说明:静态类型是,指针或引用声明时的类型;动态类型是,指针或引用实际指向的类型。
静态绑定:绑定的是对象的静态类型,(函数)依赖于对象的静态类型,发生在编译时期
动态绑定:绑定的是对象的动态类型,(函数)依赖于对象的动态类型,发生在运行时期
特别说明:virtual构成动态绑定,no-virtual构成静态绑定。
(29) 引用是否能实现动态绑定,为什么引用可以实现
可以。因为引用(或指针)既可以指向基类对象也可以指向派生类对象,这一事实是动态绑定的关键。用引用(或指针)调用的虚函数在运行时确定,被调用的函数是引用(或指针)所指的对象的实际类型所定义的。
六、 智能指针、类型转换
(36)智能指针的循环引用
当两个对象相互使用一个shared_ptr成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄漏。
为了解决循环引用导致的内存泄漏,引入了weak_ptr弱指针,weak_ptr的构造函数不会修改引用计数的值,从而不会对对象的内存进行管理,其类似一个普通指针,但不指向引用计数的共享内存,但是其可以检测到所管理的对象是否已经被释放,从而避免非法访问。
(36)请你介绍一下C++中的智能指针
智能指针主要用于管理在堆上分配的内存,它将普通的指针封装为一个栈对象。当栈对象的生存周期结束后,会在析构函数中释放掉申请的内存,从而防止内存泄漏。
C++ 11中最常用的智能指针类型为shared_ptr,它采用引用计数的方法,记录当前内存资源被多少个智能指针引用。该引用计数的内存在堆上分配。当新增一个时引用计数加1,当过期时引用计数减一。只有引用计数为0时,智能指针才会自动释放引用的内存资源。
对shared_ptr进行初始化时不能将一个普通指针直接赋值给智能指针,因为一个是指针,一个是类。可以通过make_shared函数或者通过构造函数传入普通指针。并可以通过get函数获得普通指针。
(44) C++的四种强制转换
C++中四种类型转换是:static_cast, dynamic_cast, const_cast, reinterpret_cast
1、const_cast
用于将const变量转为非const
2、static_cast
用于各种隐式转换,比如非const转const,void*转指针等, static_cast能用于多态向上转化,如果向下转能成功但是不安全,结果未知;
3、dynamic_cast
用于动态类型转换。只能用于含有虚函数的类,用于类层次间的向上和向下转化。只能转指针或引用。向下转化时,如果是非法的对于指针返回NULL,对于引用抛异常。要深入了解内部转换的原理。
向上转换:指的是子类向基类的转换
向下转换:指的是基类向子类的转换
它通过判断在执行到该语句的时候变量的运行时类型和要转换的类型是否相同来判断是否能够进行向下转换。
4、reinterpret_cast
几乎什么都可以转,比如将int转指针,可能会出问题,尽量少用;
5、为什么不使用C的强制转换?
C的强制转换表面上看起来功能强大什么都能转,但是转化不够明确,不能进行错误检查,容易出错。
七、函数模板
(40) 模板的用法与适用场景
(55)模板函数和模板类的特例化
八、编译、运行
(46)静态连接与动态链接的区别
静态链接
所谓静态链接就是在编译链接时直接将需要的执行代码拷贝到调用处,优点就是在程序发布的时候就不需要依赖库,也就是不再需要带着库一块发布,程序可以独立执行,但是体积可能会相对大一些。
动态链接
所谓动态链接就是在编译的时候不直接拷贝可执行代码,而是通过记录一系列符号和参数,在程序运行或加载时将这些信息传递给操作系统,操作系统负责将需要的动态库加载到内存中,然后程序在运行到指定的代码时,去共享执行内存中已经加载的动态库可执行代码,最终达到运行时连接的目的。优点是多个程序可以共享同一段代码,而不需要在磁盘上存储多个拷贝,缺点是由于是运行时加载,可能会影响程序的前期执行性能。
(54)extern "C"的用法
C++调用C函数需要extern C,因为C语言没有函数重载。
(49)如何让main函数之前执行函数?
C++中在main函数之前定义一个全局对象,调用构造函数
(43) C++的调用惯例(简单一点C++函数调用的压栈过程)
每一个函数调用都会分配函数栈,在栈内进行函数执行过程。调用前,先把返回地址压栈,然后把当前函数的esp指针压栈。
九、 关键字
(45)volatile关键字
访问寄存器要比访问内存要块,因此CPU会优先访问该数据在寄存器中的存储结果,但是内存中的数据可能已经发生了改变,而寄存器中还保留着原来的结果。为了避免这种情况的发生将该变量声明为volatile,告诉CPU每次都从内存去读取数据。一个参数可以即是const又是volatile的吗?可以,一个例子是只读状态寄存器,是volatile是因为它可能被意想不到的被改变,是const告诉程序不应该试图去修改他
(47)decltype()和auto
decltype只是为了推断出表达式的类型而不用这个表达式的值来初始化对象。
auto: 一般来说, 在把一个表达式或者函数的返回值赋给一个对象的时候, 我们必须要知道这个表达式的返回类型, 但是有的时候我们很难或者无法知道这个表达式或者函数的返回类型. 这个时候, 我们就可以使用auto关键字来让编译器帮助我们分析表达式或者函数所属的类型
(52)final和override关键字
final限定某个类不能被继承或某个虚函数不能被重写。如果修饰函数只能修饰虚函数,且要话到类或函数后面。
被override修饰后如果父类无对应的虚函数则报错,无法override,这个有什么作用呢,假如你想虚继承基类的函数,但是继承的时候写错了,参数类型不对或个数不对,但是编译没问题,运行时候缺和你设计的不一样不被调用,override就是辅助你检查是否继承了想要虚继承的函数。
七、 其它
(37) 调试程序的方法
断点调试、逐语句执行、使用立即窗口、逐过程
(38) 遇到coredump要怎么调试
(45)C++的异常处理
异常提供了一种转移程序控制权的方式。C++ 异常处理涉及到三个关键字:try、catch、throw。
(46)优化程序的几种方法
选择一组合适的算法和数据结构
针对处理运算量特别大的计算, 将一个任务分成多个部分, 这些部分可以在多核和多处理器的某种组合上并行的计算。
消除循环的低效率, 尽量减少循环次数。 尽量不要在循环里 循环计算一些不会改变的值。
(48)如何让一个类不能实例化?
将类定义为抽象基类或者将构造函数声明为private。
(31) 对象复用的了解,零拷贝的了解
"零拷贝"中的"拷贝"是操作系统在I/O操作中,将数据从一个内存区域复制到另外一个内存区域. 而"零"并不是指0次复制, 更多的是指在用户态和内核态之前的复制是0次.
STL
基本组成:
容器、迭代器、仿函数、算法、分配器、配接器
他们之间的关系:分配器给容器分配存储空间,算法通过迭代器获取容器中的内容,仿函数可以协助算法完成各种操作,配接器用来套接适配仿函数
容器
vector
关于vector简单的说就是一个动态增长的数组,里面有一个指针指向一片连续的内存空间,当空间装不下要容纳的数据的时候会自动申请一片更大的空间(空间配置器)将原来的数据拷贝到新的空间,然后就会释放旧的空间。当删除的时候空间并不会释放只是清空了里面的数据。
vector的数据安排以及操作方式与数组非常相似,两者唯一区别在于空间运用的灵活性,数组是静态空间一旦配置了就不能再改变大小,如果要增容的话,就要把数据搬到新的数组里面,然后再把原来的空间释放掉还给操作系统。vector是动态的随着元素的增加,它的内部机制会自动的扩充空间来容纳新的元素。因此,vector的运用对于内存的合理利用与运用的灵活性有很大的帮助,我们不必害怕空间不足而一开始就开辟一块很大的内存。
vector的实现技术,关键在于其对大小的控制以及重新配置时的数据移动效率。一旦vector的旧空间满载了,如果客户端每新增加一个元素,vector的内部只是扩充了一个元素空间,其实这样是比较不明智的。因为所谓的扩充空间(无论多大),过程都是配置新空间——数据移动——释放旧空间,成本还是比较高的。vector维护的是一个连续的线性空间,所以vector支持随机访问。
在vector的动态增加大小的时候,并不是在原有的空间上持续增加成新的空间(无法保证原空间的后面还有可供配置的空间),而是以原大小的两倍另外配置一块较大的空间,然后将原来的内容拷贝过来,并释放原来的空间。因此,对vector的任何操作一旦引起了空间的重新配置,指向原vector的所有迭代器会都失效了,这是比较容易犯的一个错误。
resize和reserve的区别
resize():改变当前容器内含有元素的数量(size()),
eg: vectorv; v.resize(len);v的size变为len,如果原来v的size小于len,那么容器新增(len-size)个元素,元素的值为默认为0.当v.push_back(3);之后,则是3是放在了v的末尾,即下标为len,此时容器是size为len+1;
reserve():改变当前容器的最大容量(capacity),它不会生成元素,只是确定这个容器允许放入多少对象,如果reserve(len)的值大于当前的capacity(),那么会重新分配一块能存len个对象的空间,然后把之前v.size()个对象通过copy construtor复制过来,销毁之前的内存;
vector使用的注意点及其原因,频繁对vector调用push_back()对性能的影响和原因。
在一个vector的尾部之外的任何位置添加元素,都需要重新移动元素。而且,向一个vector添加元素可能引起整个对象存储空间的重新分配。重新分配一个对象的存储空间需要分配新的内存,并将元素从旧的空间移到新的空间。
set
请你来说一下map和set有什么区别,分别又是怎么实现的?
map和set都是C++的关联容器,其底层实现都是红黑树(RB-Tree)。由于 map 和set所开放的各种操作接口,RB-tree 也都提供了,所以几乎所有的 map 和set的操作行为,都只是转调 RB-tree 的操作行为。
map中的元素是key-value(关键字—值)对:关键字起到索引的作用,值则表示与索引相关联的数据;Set与之相对就是关键字的简单集合,set中每个元素只包含一个关键字。
set的迭代器是const的,不允许修改元素的值;map允许修改value,但不允许修改key。其原因是因为map和set是根据关键字排序来保证其有序性的,如果允许修改key的话,那么首先需要删除该键,然后调节平衡,再插入修改后的键值,调节平衡,如此一来,严重破坏了map和set的结构,导致iterator失效,不知道应该指向改变前的位置,还是指向改变后的位置。所以STL中将set的迭代器设置成const,不允许修改迭代器的值;而map的迭代器则不允许修改key值,允许修改value值。
unordered_map
底层结构是哈希表
unordered_map内部实现了一个哈希表(也叫散列表,通过把关键码值映射到Hash表中一个位置来访问记录,查找的时间复杂度可达到O(1),其在海量数据处理中有着广泛应用)。因此,其元素的排列顺序是无序的
map
Map底层结构是红黑树。
map: map内部实现了一个红黑树(红黑树是非严格平衡二叉搜索树,而AVL是严格平衡二叉搜索树),红黑树具有自动排序的功能,因此map内部的所有元素都是有序的,红黑树的每一个节点都代表着map的一个元素。因此,对于map进行的查找,删除,添加等一系列的操作都相当于是对红黑树进行的操作。map中的元素是按照二叉搜索树存储的,使用中序遍历可将键值按照从小到大遍历出来。
map 的所有元素都是 pair,同时拥有实值(value)和键值(key)。pair 的第一元素被视为键值,第二元素被视为实值。所有元素都会根据元素的键值自动被排序。不允许键值重复。
注意下标运算符[ ]
map支持下标操作,set不支持下标操作。map可以用key做下标,map的下标运算符[ ]将关键码作为下标去执行查找,如果关键码不存在,则插入一个具有该关键码和mapped_type类型默认值的元素至map中,因此下标运算符[ ]在map应用中需要慎用,const_map不能用,只希望确定某一个关键值是否存在而不希望插入元素时也不应该使用,mapped_type类型没有默认值也不应该使用。如果find能解决需要,尽可能用find。
Multimap
多重映射。multimap 的所有元素都是 pair,同时拥有实值(value)和键值(key)。pair 的第一元素被视为键值,第二元素被视为实值。所有元素都会根据元素的键值自动被排序。允许键值重复。
底层实现:红黑树
适用场景:有序键值对可重复映射
分配器
STL的分配器用于封装STL容器在内存管理上的底层细节。在C++中,其内存配置和释放如下:
new运算分两个阶段:
(1)调用::operator new配置内存;
(2)调用对象构造函数构造对象内容
delete运算分两个阶段:
(1)调用对象析构函数;
(2)调用::operator delete释放内存
为了精密分工,STL allocator将两个阶段操作区分开来:
内存配置有alloc::allocate()负责,内存释放由alloc::deallocate()负责;
对象构造由::construct()负责,对象析构由::destroy()负责。
同时为了提升内存管理的效率,减少申请小内存造成的内存碎片问题
SGI STL采用了两级配置器,当分配的空间大小超过128B时,会使用第一级空间配置器;
当分配的空间大小小于128B时,将使用第二级空间配置器。
第一级空间配置器直接使用malloc()、realloc()、free()函数进行内存空间的分配和释放,
而第二级空间配置器采用了内存池技术,通过空闲链表来管理内存。
内存池技术
- 使用allocate向内存池请求size大小的内存空间,如果需要请求的内存大小大于128bytes,直接使用malloc。
- 如果需要的内存大小小于128bytes,allocate根据size找到最适合的自由链表。
a. 如果链表不为空,返回第一个node,链表头改为第二个node。
b. 如果链表为空,使用blockAlloc请求分配node。
x. 如果内存池中有大于一个node的空间,分配竟可能多的node(但是最多20个),将一个node返回,其他的node添加到链表中。
y. 如果内存池只有一个node的空间,直接返回给用户。
z. 若果如果连一个node都没有,再次向操作系统请求分配内存。
①分配成功,再次进行b过程。
②分配失败,循环各个自由链表,寻找空间。
I. 找到空间,再次进行过程b。
II. 找不到空间,抛出异常。 - 用户调用deallocate释放内存空间,如果要求释放的内存空间大于128bytes,直接调用free。
- 否则按照其大小找到合适的自由链表,并将其插入。
迭代器
STL容器的几种迭代器以及对应的容器(输入迭代器,输出迭代器,前向迭代器,双向迭代器,随机访问迭代器)
顺序容器:vector,deque是随机访问迭代器;list是双向迭代器
容器适配器:stack,queue,priority_queue没有迭代器
关联容器:set,map,multiset,multimap是双向迭代器
unordered_set,unordered_map,unordered_multiset,unordered_multimap是前向迭代器
迭代器失效的问题。
1.对于序列容器vector,deque来说,使用erase(itertor)后,后边的每个元素的迭代器都会失效,但是后边每个元素都会往前移动一个位置,erase会返回下一个有效的迭代器;
2.对于关联容器map set来说,使用了erase(iterator)后,当前元素的迭代器失效,但是其结构是红黑树,删除当前元素的,不会影响到下一个元素的迭代器,所以在调用erase之前,记录下一个元素的迭代器即可。
3.对于list来说,它使用了不连续分配的内存,并且它的erase方法也会返回下一个有效的iterator,因此上面两种正确的方法都可以使用。
(22)请你来说一下STL中迭代器的作用,有指针为何还要迭代器
1、迭代器
Iterator(迭代器)模式又称Cursor(游标)模式,用于提供一种方法顺序访问一个聚合对象中各个元素, 而又不需暴露该对象的内部表示。或者这样说可能更容易理解:Iterator模式是运用于聚合对象的一种模式,通过运用该模式,、我们可以在不知道对象内部表示的情况下,按照一定顺序(由iterator提供的方法)访问聚合对象中的各个元素。
由于Iterator模式的以上特性:与聚合对象耦合,在一定程度上限制了它的广泛运用,一般仅用于底层聚合支持类,如STL的list、vector、stack等容器类及ostream_iterator等扩展iterator。
迭代器和指针的区别
迭代器不是指针,是类模板,表现的像指针。他只是模拟了指针的一些功能,通过重载了指针的一些操作符,->、、++、–等。迭代器封装了指针,是一个“可遍历STL( Standard Template Library)容器内全部或部分元素”的对象,本质是封装了原生指针,是指针概念的一种提升(lift),提供了比指针更高级的行为,相当于一种智能指针,他可以根据不同类型的数据结构来实现不同的++,–等操作。
迭代器返回的是对象引用而不是对象的值,所以cout只能输出迭代器使用取值后的值而不能直接输出其自身。
迭代器产生原因
Iterator类的访问方式就是把不同集合类的访问逻辑抽象出来,使得不用暴露集合内部的结构而达到循环遍历集合的效果。
traits技法
STL源码剖析
type_traits
iterator_traits
char traits
allocator_traits
pointer_traits
array_traits