文章目录
- 一、C++基础部分
- 1、C++特点。
- 2、说说C语言和C++的区别。
- 3、说说 C++中 struct 和 class 的区别。
- 4、 include头文件的顺序以及双引号""和尖括号<>的区别。
- 5、说说C++结构体和C结构体的区别。
- 6、导入C函数的关键字是什么,C++编译时和C有什么不同?
- 7、C++从代码到可执行二进制文件的过程?
- 8、说说static关键字的作用
- 9、数组和指针的区别
- 10、说说什么是函数指针,如何定义函数指针,有什么使用场景?
- 11、静态变量什么时候初始化?
- 12、nullptr对象调用成员函数可以吗?为什么?
- 13、说说什么是野指针,怎么产生的,如何避免
- 14、说说静态局部变量,全局变量,局部变量的特点,以及使用场景
- 15、说说内联函数和宏函数的区别
- 16、内联函数使用的条件:
- 17、说说new和malloc的区别,各自底层实现原理。
- 18、 说说const和define的区别。
- 19、 说说C++中函数指针和指针函数的区别?
- 20、 说说const int *a, int const *a, const int a, int *const a, const int *const a分别是什么,有什么特点。
- 21、说说使用指针需要注意什么?
- 22、说说内联函数和函数的区别,内联函数的作用?
- 23、 简述C++有几种传值方式,之间的区别是什么?
- 24、简述const(星号)和(星号)const的区别?
- 二、C++内存部分
- 25、简述堆和栈的区别?
- 26、简述C++的内存管理?
- 27、常见的内存错误及其对策:
- 28、内存泄露及解决办法:
- 29、 malloc和局部变量分配在堆还是栈?
- 30、程序有哪些section,分别的作用?程序启动的过程?怎么判断数据分配在栈上还是堆上?
- 31、 初始化为0的全局变量在bss还是data?
- 32、简述C++中内存对齐的使用场景?
- 三、C++面向对象部分
- 33、简述一下什么是面向对象?
- 34、简述一下面向对象的三大特征?
- 35、简述一下 C++ 的重载和重写,以及它们的区别?
- 36、说说 C++ 的重载和重写是如何实现的?
- 37、说说构造函数有几种,分别什么作用?
- 38、只定义析构函数,会自动生成哪些构造函数?
- 39、说说一个类,默认会生成哪些函数?
- 40、说说 C++ 类对象的初始化顺序,有多重继承情况下的顺序?
- 41、简述下向上转型和向下转型?
- 42、简述下深拷贝和浅拷贝,如何实现深拷贝?
- 43、 简述一下 C++ 中的多态?
- 44、说说为什么要虚析构,为什么不能虚构造?
- 45、说说模板类是在什么时候实现的?
- 46、说说类继承时,派生类对不同关键字修饰的基类方法的访问权限?
- 48、简述一下移动构造函数,什么库用到了这个函数?
- 49、 简述一下什么是常函数,有什么作用?
- 50、 说说什么是虚继承,解决什么问题,如何实现?
- 51、简述一下虚函数和纯虚函数,以及实现原理?
- 52、说说纯虚函数能实例化吗,为什么?派生类要实现吗,为什么?
- 53、说说C++中虚函数与纯虚函数的区别?
- 54、说说 C++ 中什么是菱形继承问题,如何解决?
- 55、请问构造函数中的能不能调用虚方法?
- 56、请问拷贝构造函数的参数是什么传递方式,为什么?
- 57、如何理解抽象类?
- 58、什么是多态?除了虚函数,还有什么方式能实现多态?
- 59、简述一下虚析构函数,什么作用?
- 60、说说什么是虚基类,可否被实例化?
- 61、简述一下拷贝赋值和移动赋值?
- 62、仿函数了解吗?有什么作用?
- 62、C++ 中哪些函数不能被声明为虚函数?
- 63、解释下 C++ 中类模板和模板类的区别?
- 64、虚函数表里存放的内容是什么时候写进去的?
- 四、C++STL部分
- 65、请说说 STL 的基本组成部分?
- 66、请说说 STL 中常见的容器,并介绍一下实现原理?
- 67、请说说 STL 中常见的容器,并介绍一下实现原理?
- 68、请你来介绍一下 STL 的空间配置器(allocator)?
- 69、请你来介绍一下 STL 的空间配置器(allocator)?
- 70、迭代器用过吗?什么时候会失效?
- 71、说一下STL中迭代器的作用,有指针为何还要迭代器?
- 72、说说 STL 迭代器是怎么删除元素的?
- 73、说说 STL 中 resize 和 reserve 的区别?
- 74、说说 STL 容器动态链接可能产生的问题?
- 75、说说 map 和 unordered_map 的区别?底层实现?
- 76、说说 vector 和 list 的区别,分别适用于什么场景?
- 77、简述 vector 的实现原理?
- 78、简述STL 中的 map 的实现原理?
- 79、C++ 的 vector 和 list中,如果删除末尾的元素,其指针和迭代器如何变化?若删除的是中间的元素呢?
- 80、 请你来说一下 map 和 set 有什么区别,分别又是怎么实现的?
- 81、说说 push_back 和 emplace_back 的区别?
- 82、STL 中 vector 与 list 具体是怎么实现的?常见操作的时间复杂度是多少?
- 五、C++11部分
- 83、说说 C++11 的新特性有哪些?
- 84、说说 C++ 中智能指针和指针的区别是什么?
- 85、说说 C++中的智能指针有哪些?分别解决的问题以及区别?
- 86、简述 C++ 右值引用与转移语义?
- 87、简述 C++ 中智能指针的特点?
- 88、weak_ptr 能不能知道对象计数为 0,为什么?
- 89、 weak_ptr 如何解决 shared_ptr 的循环引用问题?
- 90、说说智能指针及其实现,shared_ptr 线程安全性,原理?
- 91、请你回答一下智能指针有没有内存泄露的情况?
- 92、简述一下 C++11 中四种类型转换?
- 93、简述一下 C++ 11 中 auto 的具体用法?
- 94、简述一下 C++11 中的可变参数模板新特性?
- 95、简述一下 C++11 中 Lambda 新特性?
一、C++基础部分
1、C++特点。
- C++在C语言的基础上引入了面向对象的机制,同时也兼容C语言。
- C++有三大特性,封装,继承,多态。
- C++生成的代码质量高,效率高。
- C++语言编写出的程序结构清晰、易于扩充,程序可读性好。
- C++更加安全,增加了const常量、引用、四类cast转化(static_cast、dynamic_cast、
const_cast、reinterpret_cast)、智能指针、try—catch等等; - C++可复用性高,C++引入了模板的概念,标准模板库STL(Standard Template Library)。
2、说说C语言和C++的区别。
- C语言是C++的子集,C++可以很好兼容C语言。但是C++又有很多新特性,如引用、智能指针、
auto变量等。 - . C++是面对对象的编程语言;C语言是面对过程的编程语言。
- C语言有一些不安全的语言特性,如指针使用的潜在危险、强制转换的不确定性、内存泄露等。而
C++对此增加了不少新特性来改善安全性,如const常量、引用、cast转换、智能指针、try—catch
等等。 - C++可复用性高,C++引入了模板的概念,后面在此基础上,实现了方便开发的标准模板库STL。
C++的STL库相对于C语言的函数库更灵活、更通用。
3、说说 C++中 struct 和 class 的区别。
- struct一般用于描述一个数据结构的集合,而class是对一个对象数据的封装。
- struct 中默认的访问控制权限是 public 的,而 class 中默认的访问控制权限是 private 的。
- 在继承关系中,struct 默认是公有继承,而 class 是私有继承。
- class 关键字可以用于定义模板参数,就像 typename,而 struct 不能用于定义模板参数。
4、 include头文件的顺序以及双引号""和尖括号<>的区别。
定义:尖括号<>的头文件是系统文件,双引号""的头文件是自定义文件。
区别:编译器预处理阶段查找头文件的路径不一样
- 使用尖括号<>的头文件的查找路径:编译器设置的头文件路径–>系统变量。
- 使用双引号""的头文件的查找路径:当前头文件目录–>编译器设置的头文件路径–>系统变
量。
5、说说C++结构体和C结构体的区别。
区别:
- C的结构体内不允许有函数存在,C++允许有内部成员函数,且允许该函数是虚函数。
- C的结构体对内部成员变量的访问权限只能是public,而C++允许public,protected,private三种。
- C语言的结构体是不可以继承的,C++的结构体可继承。
- C 中使用结构体需要加上 struct 关键字,而 C++ 中可以省略 struct 关键字直接使用。
- C++ 中的 struct 是对 C 中的 struct 进行了扩充,它们在声明时的区别如下:
6、导入C函数的关键字是什么,C++编译时和C有什么不同?
- 关键字:在C++中,导入C函数的关键字是extern,表达形式为extern “C”, extern "C"的主要作
用就是为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代
码按C语言的进行编译,而不是C++的。 - 编译区别:由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译
后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带
上函数的参数类型,一般只包括函数名。
7、C++从代码到可执行二进制文件的过程?
C++和C语言类似,一个C++程序从源码到执行文件,有四个过程,预编译、编译、汇编、链接。
- 预编译:这个过程主要的处理操作如下:
将所有的#define删除,并且展开所有的宏定义
处理所有的条件预编译指令,如#if、#ifdef
处理#include预编译指令,将被包含的文件插入到该预编译指令的位置。
过滤所有的注释
添加行号和文件名标识 - 编译:这个过程主要的处理操作如下:
词法分析:将源代码的字符序列分割成一系列的记号。
语法分析:对记号进行语法分析,产生语法树。
语义分析:判断表达式是否有意义。
代码优化。
目标代码生成:生成汇编代码。
目标代码优化。 - 汇编:这个过程主要是将汇编代码转变成机器可以执行的指令。
- 链接:将不同的源文件产生的目标文件进行链接,从而形成一个可以执行的程序。
链接分为静态链接和动态链接:
静态链接,是在链接的时候就已经把要调用的函数或者过程链接到了生成的可执行文件中,就算你在去
把静态库删除也不会影响可执行程序的执行;生成的静态链接库,Windows下以.lib为后缀,Linux下
以.a为后缀。
动**态链接,是在链接的时候没有把调用的函数代码链接进去,**而是在执行的过程中,再去找要链接的函
数,生成的可执行文件中没有函数代码,只包含函数的重定位信息,所以当你删除动态库时,可执行程
序就不能运行。生成的动态链接库,Windows下以.dll为后缀,Linux下以.so为后缀。
8、说说static关键字的作用
- 定义全局静态变量和局部静态变量:初始化的静态变量会在数据段分配内存,未初始化的静态变量
会在BSS段分配内存。直到程序结束,静态变量始终会维持前值。 - 定义静态函数:静态函数只能在本源文件中使用;如 static void func();
- 定义静态变量。静态变量只能在本源文件中使用;
- 定义类中的静态成员变量:使用静态数据成员,它既可以被当成全局变量那样去存储,但又被隐藏
在类的内部。类中的static静态数据成员拥有一块单独的存储区,而不管创建了多少个该类的对
象。所有这些对象的静态数据成员都共享这一块静态存储空间。 - 定义类中的静态成员函数:如静态成员函数也是类的一部分,而不是对象的一部分。所有这些对象
的静态数据成员都共享这一块静态存储空间。
此外:
当调用一个对象的非静态成员函数时,系统会把该对象的起始地址赋给成员函数的this指针。而静态成
员函数不属于任何一个对象,因此C++规定静态成员函数没有this指针。既然它没有指向某一对象,也
就无法对一个对象中的非静态成员进行访问!
9、数组和指针的区别
- 概念
(1)数组:数组是用于储存多个相同类型数据的集合。 数组名是首元素的地址。
(2)指针:指针相当于一个变量,它存放的是其它变量在内存中的地址。 指针名指向了内存的首地址。 - 区别:
(1)赋值:同类型指针变量可以相互赋值;数组不行,只能一个一个元素的赋值或拷贝
(2)存储方式:
数组:数组在内存中是连续存放的,数组的存储空间,不是在静态区就是在栈上。
指针:指针很灵活,它可以指向任意类型的数据。指针的类型说明了它所指向地址空间的内存。由
于指针本身就是一个变量,再加上它所存放的也是变量,所以指针的存储空间不能确定。
(3)求sizeof:
数组所占存储空间的内存大小**:sizeof(数组名)/sizeof(数据类型)**
在32位平台下,无论指针的类型是什么,sizeof(指针名)都是4,在64位平台下,无论指针的类
型是什么,sizeof(指针名)都是8。
10、说说什么是函数指针,如何定义函数指针,有什么使用场景?
- 概念:函数指针就是指向函数的指针变量。每一个函数都有一个入口地址,该入口地址就是函数指
针所指向的地址。 - 定义形式如下:
int func(int a);
int (*f)(int a);
f = &func;
- 函数指针的应用场景:回调(callback)。我们调用别人提供的 API函数(Application
Programming Interface,应用程序编程接口),称为Call;如果别人的库里面调用我们的函数,就叫
Callback。 - 例子:比如你要让windows系统知道在某种事件(如:鼠标按下)发生后该如何响应(或根本不响应),但系统怎么知道你的程序里有这么一个函数是用来响应鼠标按下的呢?所以在这个函数前面加一个CALLBACK并把此函数地址赋给系统,windows就知道调用哪个函数来响应哪个事件。具体可以看看关于回调函数的解释。
11、静态变量什么时候初始化?
- 对于C语言的全局和静态变量,初始化发生在任何代码执行之前,属于编译期初始化。
- 而C++标准规定:全局或静态对象当且仅当对象首次用到时才进行构造。
12、nullptr对象调用成员函数可以吗?为什么?
能。
- 原因:因为在编译时对象就绑定了函数地址,和指针空不空没关系。
13、说说什么是野指针,怎么产生的,如何避免
- 概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
- 产生原因:释放内存后指针不及时置空(野指针),依然指向了该内存,那么可能出现非法访问的
错误。这些我们都要注意避免。 - 避免办法:
(1)初始化置NULL
(2)申请内存后判空
(3)指针释放后置NULL
(4)使用智能指针
14、说说静态局部变量,全局变量,局部变量的特点,以及使用场景
- 首先从作用域考虑:C++里作用域可分为6种:全局,局部,类,语句,命名空间和文件作用域。
全局变量:全局作用域,可以通过extern作用于其他非定义的源文件。
静态全局变量 :全局作用域+文件作用域,所以无法在其他文件中使用。
局部变量:局部作用域,比如函数的参数,函数内的局部变量等等。
静态局部变量 :局部作用域,只被初始化一次,直到程序结束。 - 从所在空间考虑:除了局部变量在栈上外,其他都在静态存储区。因为静态变量都在静态存储区,
所以下次调用函数的时候还是能取到原来的值。 - 生命周期: 局部变量在栈上,出了作用域就回收内存;而全局变量、静态全局变量、静态局部变量
都在静态存储区,直到程序结束才会回收内存。
15、说说内联函数和宏函数的区别
区别:
- **宏定义不是函数,但是使用起来像函数。预处理器用复制宏代码的方式代替函数的调用,**省去了函
数压栈退栈过程,提高了效率;而内联函数本质上是一个函数,内联函数一般用于函数体的代码比
较简单的函数,不能包含复杂的控制语句,while、switch,并且内联函数本身不能直接调用自
身。 - 宏函数是在预编译的时候把所有的宏名用宏体来替换,简单的说就是字符串替换 ;而内联函数则是
在编译的时候进行代码插入,编译器会在每处调用内联函数的地方直接把内联函数的内容展开,这
样可以省去函数的调用的开销,提高效率! - 宏定义是没有类型检查的,无论对还是错都是直接替换;而内联函数在编译的时候会进行类型的检
查,内联函数满足函数的性质,比如有返回值、参数列表等。
16、内联函数使用的条件:
**内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。**如果
执行函数体内代码的时间,相比于函数调用的开销较大,那么效率 的收获会很少。另一方面,每一
处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。以下情况不宜
使用内联:
(1)如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。
(2)如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。
内联不是什么时候都能展开的,一个好的编译器将会根据函数的定义体,自动地取消不符合要求的
内联。
17、说说new和malloc的区别,各自底层实现原理。
- new是操作符,而malloc是函数。
- new在调用的时候先分配内存,在调用构造函数,释放的时候调用析构函数;而malloc没有构造函数和析构函数。
- malloc需要给定申请内存的大小,返回的指针需要强转;new会调用构造函数,不用指定内存的大
小,返回指针不用强转。 - new可以被重载;malloc不行
- new分配内存更直接和安全。
- new发生错误抛出异常,malloc返回null。
malloc底层实现:当开辟的空间小于 128K 时,调用 brk()函数;当开辟的空间大于 128K 时,调用mmap()。malloc采用的是内存池的管理方式,以减少内存碎片。先申请大块内存作为堆区,然后将堆区分为多个内存块。当用户申请内存时,直接从堆区分配一块合适的空闲快。采用隐式链表将所有空闲块,每一个空闲块记录了一个未分配的、连续的内存地址。
new底层实现:关键字new在调用构造函数的时候实际上进行了如下的几个步骤:
- 创建一个新的对象
- 将构造函数的作用域赋值给这个新的对象(因此this指向了这个新的对象)
- 执行构造函数中的代码(为这个新对象添加属性)
- 返回新对象
18、 说说const和define的区别。
const用于定义常量;而define用于定义宏,而宏也可以用于定义常量。都用于常量定义时,它们的区别
有:
- const生效于编译的阶段;define生效于预处理阶段。
- const定义的常量,在C语言中是存储在内存中、需要额外的内存空间的;define定义的常量,运行时是直接的操作数,并不会存放在内存中。
- const定义的常量是带类型的;define定义的常量不带类型。因此define定义的常量不利于类型检查。
19、 说说C++中函数指针和指针函数的区别?
- 定义不同
指针函数本质是一个函数,其返回值为指针。
函数指针本质是一个指针,其指向一个函数。 - 写法不同
指针函数:int *fun(int x,int y);
函数指针:int (*fun)(int x,int y);
20、 说说const int *a, int const *a, const int a, int *const a, const int *const a分别是什么,有什么特点。
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也不变
21、说说使用指针需要注意什么?
- 定义指针时,先初始化为NULL。
- 用malloc或new申请内存之后,应该立即检查指针值是否为NULL。防止使用指针值为NULL的内存。
- 不要忘记为数组和动态内存赋初值。防止将未被初始化的内存作为右值使用。
- 避免数字或指针的下标越界,特别要当心发生“多1”或者“少1”操作
- 动态内存的申请与释放必须配对,防止内存泄漏
- 用free或delete释放了内存之后,立即将指针设置为NULL,防止“野指针”。
22、说说内联函数和函数的区别,内联函数的作用?
- 内联函数比普通函数多了关键字inline。
- 内联函数避免了函数调用的开销;普通函数有调用的开销。
- 普通函数在被调用的时候,需要寻址(函数入口地址);内联函数不需要寻址。
- 内联函数有一定的限制,内联函数体要求代码简单,不能包含复杂的结构控制语句;普通函数没有这个要求。
内联函数的作用:内联函数在调用时,是将调用表达式用内联函数体来替换。避免函数调用的开销。
23、 简述C++有几种传值方式,之间的区别是什么?
传参方式有这三种:值传递、引用传递、指针传递
- 值传递:形参即使在函数体内值发生变化,也不会影响实参的值;
- 引用传递:形参在函数体内值发生变化,会影响实参的值;
- 指针传递:在指针指向没有发生改变的前提下,形参在函数体内值发生变化,会影响实参的值;
值传递用于对象时,整个对象会拷贝一个副本,这样效率低;而引用传递用于对象时,不发生拷贝行为,只是绑定对象,更高效;指针传递同理,但不如引用传递安全。
24、简述const(星号)和(星号)const的区别?
//const* 是指针常量,*const 是常量指针
int const *a; //a指针所指向的内存里的值不变,即(*a)不变,指针可变
int *const a; //a指针所指向的内存地址不变,即a不变,其所指内存值可变。
二、C++内存部分
25、简述堆和栈的区别?
区别:
- 堆栈空间分配不同。栈由操作系统自动分配释放 ,存放函数的参数值,局部变量的值等;堆一般由程序员分配释放。
- 堆栈缓存方式不同。栈使用的是一级缓存, 它们通常都是被调用时处于存储空间中,调用完毕立即释放;** **
- 堆栈数据结构不同。堆类似数组结构;栈类似栈结构,先进后出。
26、简述C++的内存管理?
- 内存分配方式:
在C++中,内存分成5个区,他们分别是堆、栈、自由存储区、全局/静态存储区和常量存储区。
- 栈,在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。
- 堆,就是那些由new分配的内存块,一般一个new就要对应一个delete。
- 自由存储区,就是那些由malloc等分配的内存块,和堆是十分相似的,不是用free来结束自己的生命。
- 全局/静态存储区,全局变量和静态变量被分配到同一块内存中
- 常量存储区,这是一块比较特殊的存储区,里面存放的是常量,不允许修改。
27、常见的内存错误及其对策:
错误:
(1)内存分配未成功,却使用了它。
(2)内存分配虽然成功,但是尚未初始化就引用它。
(3)内存分配成功并且已经初始化,但操作越过了内存的边界。
(4)忘记了释放内存,造成内存泄露。
(5)释放了内存却继续使用它。
对策:
(1)定义指针时,先初始化为NULL。
(2)用malloc或new申请内存之后,应该立即检查指针值是否为NULL。防止使用指针值为NULL
的内存。
(3)不要忘记为数组和动态内存赋初值。防止将未被初始化的内存作为右值使用。
(4)避免数字或指针的下标越界,特别要当心发生“多1”或者“少1”操作
(5)动态内存的申请与释放必须配对,防止内存泄漏
(6)用free或delete释放了内存之后,立即将指针设置为NULL,防止“野指针”。
(7)使用智能指针
28、内存泄露及解决办法:
- 什么是内存泄露?
简单地说就是申请了一块内存空间,使用完毕后没有释放掉。
(1)new和malloc申请资源使用后,没有用delete和free释放;
(2)子类继承父类时,父类析构函数不是虚函数。
(3)Windows句柄资源使用后没有释放。
怎么检测?
第一:良好的编码习惯,使用了内存分配的函数,一旦使用完毕,要记得使用其相应的函数释放掉。
第二:将分配的内存的指针以链表的形式自行管理,使用完毕之后从链表中删除,程序结束时可检查改链表。
第三:使用智能指针。
第四:一些常见的工具插件,如ccmalloc、Dmalloc、Leaky、Valgrind等等。
29、 malloc和局部变量分配在堆还是栈?
malloc是在堆上分配内存,需要程序员自己回收内存;
局部变量是在栈中分配内存,超过作用域就自动回收。
30、程序有哪些section,分别的作用?程序启动的过程?怎么判断数据分配在栈上还是堆上?
一个程序有哪些section:
如上图,从低地址到高地址,一个程序由代码段、数据段、 BSS 段组成。
- 数据段:存放程序中已初始化的全局变量和静态变量的一块内存区域。
- 代码段:存放程序执行代码的一块内存区域。只读,代码段的头部还会包含一些只读的常数变量。
- BSS 段:存放程序中未初始化的全局变量和静态变量的一块内存区域。
- 可执行程序在运行时又会多出两个区域:堆区和栈区。
堆区:动态申请内存用。堆从低地址向高地址增长。
栈区:存储局部变量、函数参数值。栈从高地址向低地址增长。是一块连续的空间。 - 最后还有一个文件映射区,位于堆和栈之间。
- 堆 heap :由new分配的内存块,其释放由程序员控制(一个new对应一个delete)
- 栈 stack :是那些编译器在需要时分配,在不需要时自动清除的存储区。存放局部变量、函数参数。
- 常量存储区 :存放常量,不允许修改。
程序启动的过程:
- 操作系统首先创建相应的进程并分配私有的进程空间,然后操作系统的加载器负责把可执行文件的数据段和代码段映射到进程的虚拟内存空间中。
- 加载器读入可执行程序的导入符号表,根据这些符号表可以查找出该可执行程序的所有依赖的动态链接库。
- 加载器针对该程序的每一个动态链接库调用LoadLibrary
(1)查找对应的动态库文件,加载器为该动态链接库确定一个合适的基地址。
(2)加载器读取该动态链接库的导入符号表和导出符号表,比较应用程序要求的导入符号是否匹配该库的导出符号。
(3)针对该库的导入符号表,查找对应的依赖的动态链接库,如有跳转,则跳到3
(4)调用该动态链接库的初始化函数。 - 初始化应用程序的全局变量,对于全局对象自动调用构造函数。
- 进入应用程序入口点函数开始执行。
怎么判断数据分配在栈上还是堆上:首先局部变量分配在栈上;而通过malloc和new申请的空间是在堆上。
31、 初始化为0的全局变量在bss还是data?
BSS段通常是指用来存放程序中未初始化的或者初始化为0的全局变量和静态变量的一块内存区域。特点是可读写的,在程序执行之前BSS段会自动清0。
32、简述C++中内存对齐的使用场景?
内存对齐应用于三种数据类型中:struct/class/union。
- struct/class/union内存对齐原则有四个:
- 数据成员对齐规则:结构(struct)或联合(union)的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小的整数倍开始。
- 结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部"最宽基本类型成员"的整数倍地址开始存储。(struct a里存有struct b,b里有char,int ,double等元素,那b应该从8的整数倍开始存储)。
- 收尾工作:结构体的总大小,也就是sizeof的结果,必须是其内部最大成员的"最宽基本类型成员"的整数倍。不足的要补齐。(基本类型不包括struct/class/uinon)。
- sizeof(union),以结构里面size最大元素为union的size,因为在某一时刻,union只有一个成员真正存储于该地址。
- 什么是内存对齐?
- 那么什么是字节对齐?在C语言中,结构体是一种复合数据类型,其构成元素既可以是基本数据类型(如int、long、float等)的变量,也可以是一些复合数据类型(如数组、结构体、联合体等)的数据单元。在结构体中,编译器为结构体的每个成员按其自然边界(alignment)分配空间。各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构体的地址相同。
- 为了使CPU能够对变量进行快速的访问,变量的起始地址应该具有某些特性,即所谓的“对齐”,比如4字节的int型,其起始地址应该位于4字节的边界上,即起始地址能够被4整除,也即“对齐”跟数据在内存中的位置有关。如果一个变量的内存地址正好位于它长度的整数倍,他就被称做自然对齐。
- 为什么要字节对齐?
为了快速准确的访问,若没有字节对齐则会出现多次访问浪费时间。
举例说明(定义一个char 和 int型数据不按照字节对齐存储的情况需要多次访问)
三、C++面向对象部分
33、简述一下什么是面向对象?
- 面向对象是一种编程思想,把一切东西看成是一个个对象,把这些对象拥有的属性变量和操作这些属性变量的函数打包成一个类来表示。
- 面向过程和面向对象的区别
面向过程:根据业务逻辑从上到下写代码
面向对象:将数据与函数绑定到一起,进行封装,加快开发程序,减少重复代码的重写过程。
34、简述一下面向对象的三大特征?
面向对象的三大特征是封装、继承、多态。
-
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。封装本质上是一种管理,不想给别人看到的,我们protected/private把成员封装起来。开放一些共有的成员函数对成员合理的访问。
-
继承:可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。
-
多态:用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。实现多态,有二种方式,重写,重载。
35、简述一下 C++ 的重载和重写,以及它们的区别?
- 重写
是指派生类中存在重新定义的函数。其函数名,参数列表,返回值类型,所有都必须同基类中被重写的函数一致。只有函数体不同(花括号内),派生类对象调用时会调用派生类的重写函数,不会调用被重写函数。重写的基类中被重写的函数必须有virtual修饰。
#include<bits/stdc++.h>
using namespace std;
class A {
public:
virtual void fun() {
cout << "A";
}
};
class B : public A
{
public:
virtual void fun() {
cout << "B";
}
};
int main(void)
{
A* a = new B();
a->fun();//输出B,A类中的fun在B类中重写
}
- 重载
我们在平时写代码中会用到几个函数但是他们的实现功能相同,但是有些细节却不同。函数重载是指同一可访问区内被声明的几个具有不同参数列(参数的类型,个数,顺序不同)的同名函数,根据参数列表确定调用哪个函数,重载不关心函数返回类型。
#include<bits/stdc++.h>
using namespace std;
class A
{
void fun() {};
void fun(int i) {};
void fun(int i, int j) {};
void fun1(int i,int j){};
};
36、说说 C++ 的重载和重写是如何实现的?
- 重载:C++利用命名倾轧(name mangling)技术,来改名函数名,区分参数不同的同名函数。命名倾轧是在编译阶段完成的。编译时将参数类型加入以区分不同。
- 重写:在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数。
虚函数需要注意的地方:
- 用virtual关键字申明的函数叫做虚函数,虚函数肯定是类的成员函数。
- 存在虚函数的类都有一个一维的虚函数表叫做虚表,类的对象有一个指向虚表开始的虚指针。 虚表是和类对应的,虚表指针是和对象对应的。
- 多态性是一个接口多种实现,是面向对象的核心,分为类的多态性和函数的多态性。
- 重写用虚函数来实现,结合动态绑定。
- 纯虚函数是虚函数再加上 = 0。
- 抽象类是指包括至少一个纯虚函数的类。
- 纯虚函数:virtual void fun()=0。即抽象类必须在子类实现这个函数,即先有名称,没有内容,在派生类实现内容。
37、说说构造函数有几种,分别什么作用?
C++中的构造函数可以分为4类:默认构造函数、初始化构造函数、拷贝构造函数、移动构造函数。
- 默认构造函数和初始化构造函数。 在定义类的对象的时候,完成对象的初始化工作。
注意:有了有参的构造了,编译器就不提供默认的构造函数。
class Student
{
public:
//默认构造函数
Student() {
num=1001;
age=18;
}
//初始化构造函数
Student(int n,int a):num(n),age(a){}
private:
int num;
int age;
};
int main() {
//用默认构造函数初始化对象S1
Student s1;
//用初始化构造函数初始化对象S2
Student s2(1002,18);
return 0;
}
- 拷贝构造函数
#include "iostream.h"
class Test {
int i;
int *p;
public:
Test(int ai,int value) {
i = ai;
p = new int(value);
}
~Test() {
delete p;
}
Test(const Test& t) {
this->i = t.i;
this->p = new int(*t.p);
}
};
//复制构造函数用于复制本类的对象
int main(int argc, char* argv[])
{
Test t1(1,2);
Test t2(t1);//将对象t1复制给t2。注意复制和赋值的概念不同
return 0;
}
赋值构造函数默认实现的是值拷贝(浅拷贝)。
3. 移动构造函数。用于将其他类型的变量,隐式转换为本类对象。下面的转换构造函数,将int类型的r转换为Student类型的对象,对象的age为r,num为1004。
Student(int r)
{
int num=1004;
int age= r;
}
38、只定义析构函数,会自动生成哪些构造函数?
只定义了析构函数,编译器将自动为我们生成拷贝构造函数和默认构造函数。
默认构造函数和初始化构造函数。 在定义类的对象的时候,完成对象的初始化工作。
class Student
{
public:
//默认构造函数
Student() {
num=1001;
age=18;}
//初始化构造函数
Student(int n,int a):num(n),age(a){}
private:
int num;
int age;
};
int main()
{
//用默认构造函数初始化对象S1
Student s1;
//用初始化构造函数初始化对象S2
Student s2(1002,18);
return 0;
}
有了有参的构造了,编译器就不提供默认的构造函数。
拷贝构造函数。
#include "iostream.h"
class Test {
int i;
int *p;
public:
Test(int ai,int value) {
i = ai;
p = new int(value);
}
~Test() {
delete p;
}
Test(const Test& t) {
this->i = t.i;
this->p = new int(*t.p);
}
};
//复制构造函数用于复制本类的对象
int main(int argc, char* argv[])
{
Test t1(1,2);
Test t2(t1);//将对象t1复制给t2。注意复制和赋值的概念不同
return 0;
}
class HasPtr
{
public:
HasPtr(const string& s = string()) :ps(new string(s)), i(0) {}
~HasPtr() { delete ps; }
private:
string * ps;
int i;
};
HasPtr f(HasPtr hp)
{
HasPtr ret = hp;
///... 其他操作
return ret;
}
如果类外面有这样一个
当函数执行完了之后,将会调用hp和ret的析构函数,将hp和ret的成员ps给delete掉,但是由于ret和hp指向了同一个对象,因此该对象的ps成员被delete了两次,这样产生一个未定义的错误,所以说,如果一个类定义了析构函数,那么它要定义自己的拷贝构造函数和默认构造函数。
39、说说一个类,默认会生成哪些函数?
定义一个空类:
- 无参的构造函数
class Empty {
};
- 拷贝构造函数
拷贝构造函数用于复制本类的对象
Empty(const Empty& copy) {
}
- 赋值运算符
Empty& operator = (const Empty& copy) {
}
- 析构函数(非虚)
~Empty() {
}
40、说说 C++ 类对象的初始化顺序,有多重继承情况下的顺序?
- 创建派生类的对象,基类的构造函数优先被调用(也优先于派生类里的成员类);
- 如果类里面有成员类,成员类的构造函数优先被调用;(也优先于该类本身的构造函数)
- 基类构造函数如果有多个基类,则构造函数的调用顺序是某类在类派生表中出现的顺序而不是它们在成员初始化表中的顺序;
- 成员类对象构造函数如果有多个成员类对象,则构造函数的调用顺序是对象在类中被声明的顺序而不是它们出现在成员初始化表中的顺序;
- 派生类构造函数,作为一般规则派生类构造函数应该不能直接向一个基类数据成员赋值而是把值传递给适当的基类构造函数。
- 综上可以得出,初始化顺序:
父类构造函数–>成员类对象构造函数–>自身构造函数
其中成员变量的初始化与声明顺序有关,构造函数的调用顺序是类派生列表中的顺序。
析构顺序和构造顺序相反。
41、简述下向上转型和向下转型?
- 子类转换为父类:向上转型,使用dynamic_cast(expression),这种转换相对来说比较安全不会有数据的丢失;
- 父类转换为子类:向下转型,可以使用强制转换,这种转换时不安全的,会导致数据的丢失,原因是父类的指针或者引用的内存中可能不包含子类的成员的内存。
42、简述下深拷贝和浅拷贝,如何实现深拷贝?
- 浅拷贝:又称值拷贝,将源对象的值拷贝到目标对象中去,本质上来说源对象和目标对象共用一份实体,只是所引用的变量名不同,地址其实还是相同的。举个简单的例子,你的小名叫西西,大名叫冬冬,当别人叫你西西或者冬冬的时候你都会答应,这两个名字虽然不相同,但是都指的是你。
- 深拷贝,拷贝的时候先开辟出和源对象大小一样的空间,然后将源对象里的内容拷贝到目标对象中去,这样两个指针就指向了不同的内存位置。并且里面的内容是一样的,深拷贝情况下,不会出现重复释放同一块内存的错误。
43、 简述一下 C++ 中的多态?
由于派生类重写基类方法,然后用基类引用指向派生类对象,调用方法时候会进行动态绑定,这就是多态。
- 不同的对象收到同一消息可以进行不同的响应,产生完全不同的结果。
多态分为静态多态和动态多态:
- 静态多态:编译器在编译期间完成的,编译器会根据实参类型来推断该调用哪个函数,如果有对应的函数,就调用,没有则在编译时报错。
include<iostream>
using namespace std;
int Add(int a,int b)//1 {
return a+b;
}
char Add(char a,char b)//2 {
return a+b;
}
int main() {
cout<<Add(666,888)<<endl;//1
cout<<Add('1','2');//2
return 0;
}
- 动态多态:其实要实现动态多态,需要几个条件——即动态绑定条件:
- 虚函数。基类中必须有虚函数,在派生类中必须重写虚函数。
- 通过基类类型的指针或引用来调用虚函数。
44、说说为什么要虚析构,为什么不能虚构造?
- 虚析构:将可能会被继承的父类的析构函数设置为虚函数,可以保证当我们new一个子类,然后使用基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄漏。如果基类的析构函数不是虚函数,在特定情况下会导致派生类无法被析构。
- 用派生类类型指针绑定派生类实例,析构的时候,不管基类析构函数是不是虚函数,都会正常析构
- 用基类类型指针绑定派生类实例,析构的时候,如果基类析构函数不是虚函数,则只会析构基类,不会析构派生类对象,从而造成内存泄漏。为什么会出现这种现象呢,个人认为析构的时候如果没有虚函数的动态绑定功能,就只根据指针的类型来进行的,而不是根据指针绑定的对象来进行,所以只是调用了基类的析构函数;如果基类的析构函数是虚函数,则析构的时候就要根据指针绑定的对象来调用对应的析构函数了。
C++默认的析构函数不是虚函数是因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。
- 不能虚构造:
- 从存储空间角度:虚函数对应一个vtale,这个表的地址是存储在对象的内存空间的。如果将构
造函数设置为虚函数,就需要到vtable 中调用,可是对象还没有实例化,没有内存空间分配,如何调用。(悖论) - 从实现上看,vbtl 在构造函数调用后才建立,因而构造函数不可能成为虚函数。
45、说说模板类是在什么时候实现的?
- 模板实例化:模板的实例化分为显示实例化和隐式实例化,前者是研发人员明确的告诉模板应该使用什么样的类型去生成具体的类或函数,后者是在编译的过程中由编译器来决定使用什么类型来实例化一个模板不管是显示实例化或隐式实例化,最终生成的类或函数完全是按照模板的定义来实现的。
- 模板具体化:当模板使用某种类型类型实例化后生成的类或函数不能满足需要时,可以考虑对模板进行具体化。具体化时可以修改原模板的定义,当使用该类型时,按照具体化后的定义实现,具体化相当于对某种类型进行特殊处理。
#include <iostream>
using namespace std;
// #1 模板定义
template<class T>
struct TemplateStruct {
TemplateStruct() {
cout << sizeof(T) << endl;
}
};
// #2 模板显示实例化
template struct TemplateStruct<int>;
// #3 模板具体化
template<> struct TemplateStruct<double> {
TemplateStruct() {
cout << "--8--" << endl;
}
};
int main() {
TemplateStruct<int> intStruct;
TemplateStruct<double> doubleStruct;
// #4 模板隐式实例化
TemplateStruct<char> llStruct;
}
46、说说类继承时,派生类对不同关键字修饰的基类方法的访问权限?
类中的成员可以分为三种类型,分别为public成员、protected成员、public成员。类中可以直接访问自己类的public、protected、private成员,但类对象只能访问自己类的public成员。
- public继承:派生类可以访问基类的public、protected成员,不可以访问基类的private成员;派生类对象可以访问基类的public成员,不可以访问基类的protected、private成员。
- protected继承:派生类可以访问基类的public、protected成员,不可以访问基类的private成员;派生类对象不可以访问基类的public、protected、private成员。
- private继承:派生类可以访问基类的public、protected成员,不可以访问基类的private成员;派生类对象不可以访问基类的public、protected、private成员。
48、简述一下移动构造函数,什么库用到了这个函数?
移动也使用一个对象的值设置另一个对象的值。移动实现的是对象值真实的转移(源对象到目的对象):源对象将丢失其内容,其内容将被目的对象占有。移动操作的发生的时候,是当移动值的对象是未命名的对象的时候。这里未命名的对象就是那些临时变量,甚至都不会有名称。典型的未命名对象就是函数的返回值或者类型转换的对象。
49、 简述一下什么是常函数,有什么作用?
类的成员函数后面加 const,表明这个函数不会对这个类对象的数据成员(准确地说是非静态数据成员)作任何改变。
在设计类的时候,一个原则就是对于不改变数据成员的成员函数都要在后面加const,而对于改变数据成员的成员函数不能加 const。所以 const 关键字对成员函数的行为作了更明确的限定:有 const 修饰的成员函数(指 const 放在函数参数表的后面,而不是在函数前面或者参数表内),只能读取数据成员,不能改变数据成员;
没有 const 修饰的成员函数,对数据成员则是可读可写的。除此之外,在类的成员函数后面加 const 还有什么好处呢?那就是常量(即 const)对象可以调用 const成员函数,而不能调用非const修饰的函数。正如非const类型的数据可以给const类型的变量赋值一样,反之则不成立。
#include<iostream>
using namespace std;
class CStu {
public:
int a;
CStu() {
a = 12;
}
void Show() const {
//a = 13; //常函数不能修改数据成员
cout <<a << "I am show()" << endl;
}
};
int main() {
CStu st;
st.Show();
system("pause");
return 0;
}
50、 说说什么是虚继承,解决什么问题,如何实现?
虚继承是解决C++多重继承问题的一种手段,从不同途径继承来的同一基类,会在子类中存在多份拷贝。这将存在两个问题:
- 其一,浪费存储空间;
- 第二,存在二义性问题,通常可以将派生类对象的地址赋值给基类对象,实现的具体方式是,将基类指针指向继承类(继承类有基类的拷贝)中的基类对象的地址,但是多重继承可能存在一个基类的多份拷贝,这就出现了二义性。虚继承可以解决多种继承前面提到的两个问题。
51、简述一下虚函数和纯虚函数,以及实现原理?
(一)虚函数
- C++中的虚函数的作用主要是实现了多态的机制。
- 关于多态,简而言之就是用父类型的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。
- 如果调用非虚函数,则无论实际对象是什么类型,都执行基类类型所定义的函数。非虚函数总是在编译时根据调用该函数的对象,引用或指针的类型而确定。如果调用虚函数,则直到运行时才能确定调用哪个函数,运行的虚函数是引用所绑定或指针所指向的对象所属类型定义的版本。虚函数必须是基类的非静态成员函数。
- 虚函数的作用是实现动态联编,也就是在程序的运行阶段动态地选择合适的成员函数,在定义了虚函数后,可以在基类的派生类中对虚函数重新定义,在派生类中重新定义的函数应与虚函数具有相同的形参个数和形参类型。以实现统一的接口,不同定义过程。如果在派生类中没有对虚函数重新定义,则它继承其基类的虚函数。
class Person{
public:
//虚函数
virtual void GetName(){
cout<<"PersonName:xiaosi"<<endl;
};
};
class Student:public Person{
public:
void GetName(){
cout<<"StudentName:xiaosi"<<endl;
};
};
int main(){
//指针
Person *person = new Student();
//基类调用子类的函数
person->GetName();//StudentName:xiaosi
}
虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。简称为V-Table。在
这个表中,主是要一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其容真实反应实际的函数。这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数。
(二)纯虚函数
纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现的实现方法。
在基类中实现纯虚函数的方法是在函数原型后加“=0” virtualvoid GetName() =0。在很多情
况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。为了解决上述问题,将函数定义为纯虚函数,则编译器要求在派生类中必须予以重写以实现多态性。
同时含有纯虚拟函数的类称为抽象类,它不能生成对象。这样就很好地解决了上述两个问题。将函数定义为纯虚函数能够说明,该函数为后代类型提供了可以覆盖的接口,但是这个类中的函数绝不会调用。
声明了纯虚函数的类是一个抽象类。
所以,用户不能创建类的实例,只能创建它的派生类的实例。必须在继承类中重新声明函数(不要后面的=0)否则该派生类也不能实例化,而且它们在抽象类中往往没有定义。定义纯虚函数的目的在于,使派生类仅仅只是继承函数的接口。纯虚函数的意义,让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但类无法为纯虚函数提供一个合理的缺省实现。所以类纯虚函数的声明就是在告诉子类的设计者,“你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它”。
//抽象类
class Person{
public:
//纯虚函数
virtual void GetName()=0;
};
class Student:public Person{
public:
Student(){
};
void GetName(){
cout<<"StudentName:xiaosi"<<endl;
};
};
int main(){
Student student;
}
52、说说纯虚函数能实例化吗,为什么?派生类要实现吗,为什么?
- 纯虚函数不可以实例化,但是可以用其派生类实例化,示例如下:
class Base
{
public:
virtual void func() = 0;
};
#include<iostream>
using namespace std;
class Base {
public:
virtual void func() = 0;
};
class Derived :public Base
{
public:
void func() override {
cout << "哈哈" << endl;
}
};
int main() {
Base *b = new Derived();
b->func();
return 0;
}
- 虚函数的原理采用 vtable。类中含有纯虚函数时,其vtable 不完全,有个空位。
即“纯虚函数在类的vftable表中对应的表项被赋值为0。也就是指向一个不存在的函数。由于编译器绝对不允许有调用一个不存在的函数的可能,所以该类不能生成对象。在它的派生类中,除非重写此函数,否则也不能生成对象。”所以纯虚函数不能实例化。 - 纯虚函数是在基类中声明的虚函数,它要求任何派生类都要定义自己的实现方法,以实现多态性。
- 定义纯虚函数是为了实现一个接口,用来规范派生类的行为,也即规范继承这个类的程序员必须实现这个函数。派生类仅仅只是继承函数的接口。纯虚函数的意义在于,让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但基类无法为纯虚函数提供一个合理的缺省实现。所以类纯虚函数的声明就是在告诉子类的设计者,“你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它”。
53、说说C++中虚函数与纯虚函数的区别?
- 虚函数和纯虚函数可以定义在同一个类中,含有纯虚函数的类被称为抽象类,而只含有虚函数的类不能被称为抽象类。
- 虚函数可以被直接使用,也可以被子类重载以后,以多态的形式调用,而纯虚函数必须在子类中实现该函数才可以使用,因为纯虚函数在基类有声明而没有定义。
- 虚函数和纯虚函数都可以在子类中被重载,以多态的形式被调用。
- 虚函数和纯虚函数通常存在于抽象基类之中,被继承的子类重载,目的是提供一个统一的接口。
- 虚函数的定义形式: virtual{} ;纯虚函数的定义形式: virtual { } = 0 ;在虚函数和纯虚函数
的定义中不能有static标识符,原因很简单,被static修饰的函数在编译时要求前期绑定,然而虚函数却是动态绑定,而且被两者修饰的函数生命周期也不一样。
54、说说 C++ 中什么是菱形继承问题,如何解决?
导致问题:菱形继承会导致数据重复和产生歧义;
解决办法:使用虚继承,可确保每份数据自继承一次;
55、请问构造函数中的能不能调用虚方法?
- 不要在构造函数中调用虚方法,从语法上讲,调用完全没有问题,但是从效果上看,往往不能达到需要的目的。
- 派生类对象构造期间进入基类的构造函数时,对象类型变成了基类类型,而不是派生类类型。
- 同样,进入基类析构函数时,对象也是基类类型。
所以,虚函数始终仅仅调用基类的虚函数(如果是基类调用虚函数),不能达到多态的效果,所以放在构造函数中是没有意义的,而且往往不能达到本来想要的效果。
56、请问拷贝构造函数的参数是什么传递方式,为什么?
- 拷贝构造函数的参数必须使用引用传递
- 如果拷贝构造函数中的参数不是一个引用,即形如CClass(const CClass c_class),那么就相当于采用了传值的方式(pass-by-value),而传值的方式会调用该类的拷贝构造函数,从而造成无穷递归地调用拷贝构造函数。因此拷贝构造函数的参数必须是一个引用。需要澄清的是,传指针其实也是传值,如果上面的拷贝构造函数写成CClass(const CClass*c_class),也是不行的。事实上,只有传引用不是传值外,其他所有的传递方式都是传值。
57、如何理解抽象类?
- 抽象类的定义如下:
纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0”,有虚函数的类就叫做抽象类。 - 抽象类有如下几个特点:
1)抽象类只能用作其他类的基类,不能建立抽象类对象。
2)抽象类不能用作参数类型、函数返回类型或显式转换的类型。
3)可以定义指向抽象类的指针和引用,此指针可以指向它的派生类,进而实现多态性。
58、什么是多态?除了虚函数,还有什么方式能实现多态?
- 多态是面向对象的重要特性之一,它是一种行为的封装,就是不同对象对同一行为会有不同的状态。(举例 : 学生和成人都去买票时,学生会打折,成人不会)
- 多态是以封装和继承为基础的。在C++中多态分为静态多态(早绑定)和动态多态(晚绑定)两种,其中动态多态是通过虚函数实现,静态多态通过函数重载实现,代码如下:
59、简述一下虚析构函数,什么作用?
- 虚析构函数,是将基类的析构函数声明为virtual,举例如下:
class TimeKeeper {
public:
TimeKeeper() {}
virtual ~TimeKeeper() {}
};
- 虚析构函数的主要作用是防止内存泄露。
定义一个基类的指针p,在delete p时,如果基类的析构函数是虚函数,这时只会看p所赋值的对
象,如果p赋值的对象是派生类的对象,就会调用派生类的析构函数(毫无疑问,在这之前也会先
调用基类的构造函数,在调用派生类的构造函数,然后调用派生类的析构函数,基类的析构函数,所谓先构造的后释放);如果p赋值的对象是基类的对象,就会调用基类的析构函数,这样就不会造成内存泄露。
如果基类的析构函数不是虚函数,在delete p时,调用析构函数时,只会看指针的数据类型,而不会去看赋值的对象,这样就会造成内存泄露。
60、说说什么是虚基类,可否被实例化?
- 在被继承的类前面加上virtual关键字,这时被继承的类称为虚基类,代码如下:
class A
class B1:public virtual A;
class B2:public virtual A;
class D:public B1,public B2;
- 虚继承的类可以被实例化,举例如下:
61、简述一下拷贝赋值和移动赋值?
- 拷贝赋值是通过拷贝构造函数来赋值,在创建对象时,使用同一类中之前创建的对象来初始化新创
建的对象。 - 移动赋值是通过移动构造函数来赋值,二者的主要区别在于
1)拷贝构造函数的形参是一个左值引用,而移动构造函数的形参是一个右值引用;
2)拷贝构造函数完成的是整个对象或变量的拷贝,而移动构造函数是生成一个指针指向源对象或
变量的地址,接管源对象的内存,相对于大量数据的拷贝节省时间和内存空间。
62、仿函数了解吗?有什么作用?
- 仿函数(functor)又称为函数对象(function object)是一个能行使函数功能的类。仿函数的语
法几乎和我们普通的函数调用一样,不过作为仿函数的类,都必须重载operator()运算符,举个例
子:
class Func{
public:
void operator() (const string& str) const {
cout<<str<<endl;
}
};
Func myFunc;
myFunc("helloworld!");
>>>helloworld!
62、C++ 中哪些函数不能被声明为虚函数?
常见的不不能声明为虚函数的有:普通函数(非成员函数),静态成员函数,内联成员函数,构造函数,友元函数。
63、解释下 C++ 中类模板和模板类的区别?
-
类模板是模板的定义,不是一个实实在在的类,定义中用到通用类型参数
-
模板类是实实在在的类定义,是类模板的实例化。类定义中参数被实际类型所代替。
-
类模板的类型参数可以有一个或多个,每个类型前面都必须加class,如template <class T1,class
T2>class someclass{…};在定义对象时分别代入实际的类型名,如 someclass<int,double> obj; -
和使用类一样,使用类模板时要注意其作用域,只能在其有效作用域内用它定义对象。
-
模板可以有层次,一个类模板可以作为基类,派生出派生模板类!
64、虚函数表里存放的内容是什么时候写进去的?
- 虚函数表是一个存储虚函数地址的数组,以NULL结尾。虚表(vftable)在编译阶段生成,对象内存
空间开辟以后,写入对象中的 vfptr,然后调用构造函数。即:虚表在构造函数之前写入。 - 除了在构造函数之前写入之外,我们还需要考虑到虚表的二次写入机制,通过此机制让每个对象的
虚表指针都能准确的指向到自己类的虚表,为实现动多态提供支持。
四、C++STL部分
65、请说说 STL 的基本组成部分?
标准模板库(Standard Template Library,简称STL)简单说,就是一些常用数据结构和算法的模板的集合。
广义上讲:STL分为3类:Algorithm(算法)、Container(容器)和Iterator(迭代器),容器和算法通过迭代器可以进行无缝地连接。
详细的说:STL由6部分组成:容器(Container)、算法(Algorithm)、 迭代器(Iterator)、仿函数(Function object)、适配器(Adaptor)、空间配制器(Allocator)。
标准模板库STL主要由6大组成部分:容器(Container)
- 是一种数据结构, 如list, vector, 和deques,以模板类的方法提供。为了访问容器中的数据,可以使用由容器类输出的迭代器。
- 算法(Algorithm)
是用来操作容器中的数据的模板函数。例如,STL用sort()来对一 个vector中的数据进行排序,用
find()来搜索一个list中的对象, 函数本身与他们操作的数据的结构和类型无关,因此他们可以用于
从简单数组到高度复杂容器的任何数据结构上。 - 迭代器(Iterator)
提供了访问容器中对象的方法。例如,可以使用一对迭代器指定list或vector中的一定范围的对象。迭代器就如同一个指针。事实上,C++ 的指针也是一种迭代器。 但是,迭代器也可以是那些定义了operator*()以及其他类似于指针的操作符方法的类对象; - 仿函数(Function object)
仿函数又称之为函数对象, 其实就是重载了操作符的struct,没有什么特别的地方 - 适配器(Adaptor)
简单的说就是一种接口类,专门用来修改现有类的接口,提供一中新的接口;或调用现有的函数来实现所需要的功能。主要包括3中适配器Container Adaptor、Iterator Adaptor、Function
Adaptor。 - 空间配制器(Allocator)
为STL提供空间配置的系统。其中主要工作包括两部分:
(1)对象的创建与销毁;(2)内存的获取与释放。
66、请说说 STL 中常见的容器,并介绍一下实现原理?
容器可以用于存放各种类型的数据(基本类型的变量,对象等)的数据结构,都是模板类,分为顺序容器、关联式容器、容器适配器三种类型,三种类型容器特性分别如下:
-
顺序容器
容器并非排序的,元素的插入位置同元素的值无关。包含vector、deque、list,具体实现原理如
下:
(1)vector 头文件
动态数组。元素在内存连续存放。随机存取任何元素都能在常数时间完成。在尾端增删元素具有较佳的性能。
(2)deque 头文件
双向队列。元素在内存连续存放。随机存取任何元素都能在常数时间完成(仅次于vector)。在两端增删元素具有较佳的性能(大部分情况下是常数时间)。
(3)list 头文件
双向链表。元素在内存不连续存放。在任何位置增删元素都能在常数时间完成。不支持随机存取。 -
关联式容器
元素是排序的;插入任何元素,都按相应的排序规则来确定其位置;在查找时具有非常好的性能;
通常以平衡二叉树的方式实现。包含set、multiset、map、multimap,具体实现原理如下:
(1)set/multiset 头文件
set 即集合。set中不允许相同元素,multiset中允许存在相同元素。
(2)map/multimap 头文件
map与set的不同在于map中存放的元素有且仅有两个成员变,一个名为first,另一个名为second,
map根据first值对元素从小到大排序,并可快速地根据first来检索元素。注意:map同multimap的不同在于是否允许相同first值的元素。 -
容器适配器
封装了一些基本的容器,使之具备了新的函数功能,比如把deque封装一下变为一个具有stack功能的数据结构。这新得到的数据结构就叫适配器。包含stack,queue,priority_queue,具体实现原理如下:
(1)stack 头文件
栈是项的有限序列,并满足序列中被删除、检索和修改的项只能是最进插入序列的项(栈顶的
项),后进先出。
(2)queue 头文件
队列,插入只可以在尾部进行,删除、检索和修改只允许从头部进行。先进先出。
(3)priority_queue 头文件
优先级队列。内部维持某种有序,然后确保优先级最高的元素总是位于头部。最高优先级元素总是第一个出列。
67、请说说 STL 中常见的容器,并介绍一下实现原理?
map、hashtable、deque、list实现机理分别为红黑树、函数映射、双向队列、双向链表,他们的特性分别如下:
- map实现原理
map内部实现了一个红黑树(红黑树是非严格平衡的二叉搜索树,而AVL是严格平衡二叉搜索
树),红黑树有自动排序的功能,因此map内部所有元素都是有序的,红黑树的每一个节点都代表着map的一个元素。因此,对于map进行的查找、删除、添加等一系列的操作都相当于是对红黑树进行的操作。map中的元素是按照二叉树(又名二叉查找树、二叉排序树)存储的,特点就是左子树上所有节点的键值都小于根节点的键值,右子树所有节点的键值都大于根节点的键值。使用中序历可将键值按照从小到大遍历出来。 - hashtable(也称散列表,直译作哈希表)实现原理
hashtable采用了函数映射的思想记录的存储位置与记录的关键字关联起来,从而能够很快速地进
行查找。这决定了哈希表特殊的数据结构,它同数组、链表以及二叉排序树等相比较有很明显的区别,它能够快速定位到想要查找的记录,而不是与表中存在的记录的关键字进行比较来进行查找。 - deque实现原理
deque内部实现的是一个双向队列。元素在内存连续存放。随机存取任何元素都在常数时间完成(仅次于vector)。所有适用于vector的操作都适用于deque。在两端增删元素具有较佳的性能(大部分情况下是常数时间)。 - list实现原理
list内部实现的是一个双向链表。元素在内存不连续存放。在任何位置增删元素都能在常数时间完成。不支持随机存取。无成员函数,给定一个下标i,访问第i个元素的内容,只能从头部挨个遍历到第i个元素。
68、请你来介绍一下 STL 的空间配置器(allocator)?
一般情况下,一个程序包括数据结构和相应的算法,而数据结构作为存储数据的组织形式,与内存空间有着密切的联系。
在C++ STL中,空间配置器便是用来实现内存空间(一般是内存,也可以是硬盘等空间)分配的工具,他与容器联系紧密,每一种容器的空间分配都是通过空间分配器alloctor实现的。
- 两种C++类对象实例化方式的异同
在c++中,创建类对象一般分为两种方式:一种是直接利用构造函数,直接构造类对象,如 Testtest();另一种是通过new来实例化一个类对象,如 Test *pTest = new Test;那么,这两种方式有
什么异同点呢?
我们知道,内存分配主要有三种方式:
(1) 静态存储区分配:内存在程序编译的时候已经分配好,这块内存在程序的整个运行空间内都存在。如全局变量,静态变量等。
(2) 栈空间分配:程序在运行期间,函数内的局部变量通过栈空间来分配存储(函数调用栈),当函数执行完毕返回时,相对应的栈空间被立即回收。主要是局部变量。
(3)堆空间分配:程序在运行期间,通过在堆空间上为数据分配存储空间,通过malloc和new创建的对象都是从堆空间分配内存,这类空间需要程序员自己来管理,必须通过free()或者是delete()函数对堆空间进行释放,否则会造成内存溢出。
那么,从内存空间分配的角度来对这两种方式的区别,就比较容易区分:
(1)对于第一种方式来说,是直接通过调用Test类的构造函数来实例化Test类对象的,如果该实例化对象是一个局部变量,则其是在栈空间分配相应的存储空间。
(2)对于第二种方式来说,就显得比较复杂。这里主要以new类对象来说明一下。new一个类对象,其实是执行了两步操作:首先,调用new在堆空间分配内存,然后调用类的构造函数构造对象的内容;同样,使用delete释放时,也是经历了两个步骤:首先调用类的析构函数释放类对象,然后调
用delete释放堆空间。 - C++ STL空间配置器实现
很容易想象,为了实现空间配置器,完全可以利用new和delete函数并对其进行封装实现STL的空间配置器,的确可以这样。但是,为了最大化提升效率,SGI STL版本并没有简单的这样做,而是采取了一定的措施,实现了更加高效复杂的空间分配策略。由于以上的构造都分为两部分,所以,在SGI STL中,将对象的构造切分开来,分成空间配置和对象构造两部分。
内存配置操作: 通过alloc::allocate()实现
内存释放操作: 通过alloc::deallocate()实现
对象构造操作: 通过::construct()实现
对象释放操作: 通过::destroy()实现
关于内存空间的配置与释放,SGI STL采用了两级配置器:一级配置器主要是考虑大块内存空间,利用malloc和free实现;二级配置器主要是考虑小块内存空间而设计的(为了最大化解决内存碎片问题,进而提升效率),采用链表free_list来维护内存池(memory pool),free_list通过union结构实现,空闲的内存块互相挂接在一块,内存块一旦被使用,则被从链表中剔除,易于维护。
69、请你来介绍一下 STL 的空间配置器(allocator)?
STL中常用的容器有vector、deque、list、map、set、multimap、multiset、unordered_map、
unordered_set等。容器底层实现方式及时间复杂度分别如下:
- vector
采用一维数组实现,元素在内存连续存放,不同操作的时间复杂度为:
插入: O(N)
查看: O(1)
删除: O(N) - deque
采用双向队列实现,元素在内存连续存放,不同操作的时间复杂度为:
插入: O(N)
查看: O(1)
删除: O(N) - list
采用双向链表实现,元素存放在堆中,不同操作的时间复杂度为:
插入: O(1)
查看: O(N)
删除: O(1) - map、set、multimap、multiset
上述四种容器采用红黑树实现,红黑树是平衡二叉树的一种。不同操作的时间复杂度近似为:
插入: O(logN)
查看: O(logN)
删除: O(logN) - unordered_map、unordered_set、unordered_multimap、 unordered_multiset
上述四种容器采用哈希表实现,不同操作的时间复杂度为:
插入: O(1),最坏情况O(N)
查看: O(1),最坏情况O(N)
删除: O(1),最坏情况O(N)
注意:容器的时间复杂度取决于其底层实现方式。
70、迭代器用过吗?什么时候会失效?
用过,常用容器迭代器失效情形如下。
- 对于序列容器vector,deque来说,使用erase后,后边的每个元素的迭代器都会失效,后边每个元素都往前移动一位,erase返回下一个有效的迭代器。
- 对于关联容器map,set来说,使用了erase后,当前元素的迭代器失效,但是其结构是红黑树,删除当前元素,不会影响下一个元素的迭代器,所以在调用erase之前,记录下一个元素的迭代器即可。
- 对于list来说,它使用了不连续分配的内存,并且它的erase方法也会返回下一个有效的迭代器,因此上面两种方法都可以使用。
71、说一下STL中迭代器的作用,有指针为何还要迭代器?
- 迭代器的定义:提供了访问容器中对象的方法。例如,可以使用一对迭代器指定list或vector中的一定范围的对象。迭代器就如同一个指针。事实上,C++ 的指针也是一种迭代器。 但是,迭代器也可以是那些定义了operator*()以及其他类似于指针的操作符方法的类对象;
- 迭代器的作用
(1)用于指向顺序容器和关联容器中的元素
(2)通过迭代器可以读取它指向的元素
(3)通过非const迭代器还可以修改其指向的元素 - 迭代器和指针的区别
迭代器不是指针,是类模板,表现的像指针。他只是模拟了指针的一些功能,重载了指针的一些操作符,–>、++、–等。迭代器封装了指针,是一个”可遍历STL( Standard Template Library)容器内全部或部分元素”的对象,本质是封装了原生指针,是指针概念的一种提升,提供了比指针更高级的行为,相当于一种智能指针,他可以根据不同类型的数据结构来实现不同的++,–等操作。迭代器返回的是对象引用而不是对象的值,所以cout只能输出迭代器使用取值后的值而不能直接输出其自身。 - 迭代器产生的原因
Iterator类的访问方式就是把不同集合类的访问逻辑抽象出来,使得不用暴露集合内部的结构而达到循环遍历集合的效果。
具体: - 迭代器
Iterator(迭代器)模式又称游标(Cursor)模式,用于提供一种方法顺序访问一个聚合对象中各个元素, 而又不需暴露该对象的内部表示。 或者这样说可能更容易理解:Iterator模式是运用于聚合对象的一种模式,通过运用该模式,使得我们可以在不知道对象内部表示的情况下,按照一定顺序(由iterator提供的方法)访问聚合对象中的各个元素。 由于Iterator模式的以上特性:与聚合对象耦合,在一定程度上限制了它的广泛运用,一般仅用于底层聚合支持类,如STL的list、vector、stack等容器类及ostream_iterator等扩展Iterator。
例子:
#include <vector>
#include <iostream>
using namespace std;
int main() {
vector<int> v; //一个存放int元素的数组,一开始里面没有元素
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
vector<int>::const_iterator i; //常量迭代器
for (i = v.begin(); i != v.end(); ++i) //v.begin()表示v第一个元素迭代器指针,++i
指向下一个元素
cout << *i << ","; //*i表示迭代器指向的元素
cout << endl;
vector<int>::reverse_iterator r; //反向迭代器
for (r = v.rbegin(); r != v.rend(); r++)
cout << *r << ",";
cout << endl;
vector<int>::iterator j; //非常量迭代器
for (j = v.begin();j != v.end();j++)
*j = 100;
for (i = v.begin();i != v.end();i++)
cout << *i << ",";
return 0;
}
/* 运行结果:
1,2,3,4,
4,3,2,1,
100,100,100,100,
*/
72、说说 STL 迭代器是怎么删除元素的?
这是主要考察迭代器失效的问题。
- 对于序列容器vector,deque来说,使用erase后,后边的每个元素的迭代器都会失效,后边每个元素都往前移动一位,erase返回下一个有效的迭代器;
- 对于关联容器map,set来说,使用了erase后,当前元素的迭代器失效,但是其结构是红黑树,删除当前元素,不会影响下一个元素的迭代器,所以在调用erase之前,记录下一个元素的迭代器即可;
- 对于list来说,它使用了不连续分配的内存,并且它的erase方法也会返回下一个有效的迭代器,因此上面两种方法都可以使用。
73、说说 STL 中 resize 和 reserve 的区别?
- 首先必须弄清楚两个概念:
(1)capacity:该值在容器初始化时赋值,指的是容器能够容纳的最大的元素的个数。还不能通过下标等访问,因为此时容器中还没有创建任何对象。
(2)size:指的是此时容器中实际的元素个数。可以通过下标访问0-(size-1)范围内的对象。 - resize和reserve区别主要有以下几点:
(1)resize既分配了空间,也创建了对象;reserve表示容器预留空间,但并不是真正的创建对象,需要通过insert()或push_back()等创建对象。
(2)resize既修改capacity大小,也修改size大小;reserve只修改capacity大小,不修改size大小。
(3)两者的形参个数不一样。 resize带两个参数,一个表示容器大小,一个表示初始值(默认为0);reserve只带一个参数,表示容器预留的大小。
问题延伸:
resize 和 reserve 既有差别,也有共同点。两个接口的共同点是它们都保证了vector的空间大小(capacity)最少达到它的参数所指定的大小。下面就他们的细节进行分析。
为实现resize的语义,resize接口做了两个保证:
(1)保证区间[0, new_size)范围内数据有效,如果下标index在此区间内,vector[indext]是合法的;
(2)保证区间[0, new_size)范围以外数据无效,如果下标index在区间外,vector[indext]是非法的。
而: reserve只是保证vector的空间大小(capacity)最少达到它的参数所指定的大小n。在区间[0, n)范围内,如果下标是index,vector[index]这种访问有可能是合法的,也有可能是非法的,视具体情况而定。
74、说说 STL 容器动态链接可能产生的问题?
- 可能产生的问题
容器是一种动态分配内存空间的一个变量集合类型变量。在一般的程序函数里,局部容器,参数传递容器,参数传递容器的引用,参数传递容器指针都是可以正常运行的,而在动态链接库函数内部使用容器也是没有问题的,但是给动态库函数传递容器的对象本身,则会出现内存堆栈破坏的问题。 - 产生问题的原因
容器和动态链接库相互支持不够好,动态链接库函数中使用容器时,参数中只能传递容器的引用,并且要保证容器的大小不能超出初始大小,否则导致容器自动重新分配,就会出现内存堆栈破坏问题。
75、说说 map 和 unordered_map 的区别?底层实现?
map和unordered_map的区别在于他们的实现基理不同。
- map实现机理
map内部实现了一个红黑树(红黑树是非严格平衡的二叉搜索树,而AVL是严格平衡二叉搜索树),红黑树有自动排序的功能,因此map内部所有元素都是有序的,红黑树的每一个节点都代表着map的一个元素。因此,对于map进行的查找、删除、添加等一系列的操作都相当于是对红黑树进行的操作,map中的元素是按照二叉树(又名二叉查找树、二叉排序树)存储的,特点就是左子树上所有节点的键值都小于根节点的键值,右子树所有节点的键值都大于根节点的键值。使用中序遍历可将键值按照从小到大遍历出来。 - unordered_map实现机理
unordered_map内部实现了一个哈希表(也叫散列表),通过把关键码值映射到Hash表中一个位置来访问记录,查找时间复杂度可达O(1),其中在海量数据处理中有着广泛应用。因此,元素的排列顺序是无序的。
76、说说 vector 和 list 的区别,分别适用于什么场景?
vector和list区别在于底层实现机理不同,因而特性和适用场景也有所不同。
-
vector:一维数组
特点:元素在内存连续存放,动态数组,在堆中分配内存,元素连续存放,有保留内存,如果减少大小
后内存也不会释放。
优点:和数组类似开辟一段连续的空间,并且支持随机访问,所以它的查找效率高其时间复杂度O(1)。
缺点:由于开辟一段连续的空间,所以插入删除会需要对数据进行移动比较麻烦,时间复杂度O(n),另外当空间不足时还需要进行扩容。 -
list:双向链表
特点:元素在堆中存放,每个元素都是存放在一块内存中,它的内存空间可以是不连续的,通过指针来
进行数据的访问。
优点:底层实现是循环双链表,当对大量数据进行插入删除时,其时间复杂度O(1)。
缺点:底层没有连续的空间,只能通过指针来访问,所以查找数据需要遍历其时间复杂度O(n),没
有提供[]操作符的重载。 -
应用场景:
vector拥有一段连续的内存空间,因此支持随机访问,如果需要高效的随即访问,而不在乎插入和删除
的效率,使用vector。
list拥有一段不连续的内存空间,如果需要高效的插入和删除,而不关心随机访问,则应使用list。
77、简述 vector 的实现原理?
vector底层实现原理为一维数组(元素在空间连续存放)。
- 新增元素
Vector通过一个连续的数组存放元素,如果集合已满,在新增数据的时候,就要分配一块更大的内存,将原来的数据复制过来,释放之前的内存,在插入新增的元素。插入新的数据分在最后插入push_back和通过迭代器在任何位置插入,这里说一下通过迭代器插入,通过迭代器与第一个元素的距离知道要插入的位置,即int index=iter-begin()。这个元素后面的所有元素都向后移动一个位置,在空出来的位置上存入新增的元素。
78、简述STL 中的 map 的实现原理?
map是关联式容器,它们的底层容器都是红黑树。map 的所有元素都是 pair,同时拥有实值(value)和键值(key)。pair 的第一元素被视为键值,第二元素被视为实值。所有元素都会根据元素的键值自动被排序。不允许键值重复。
map的特性如下
(1)map以RBTree作为底层容器;
(2)所有元素都是键+值存在;
(3)不允许键重复;
(4)所有元素是通过键进行自动排序的;
(5)map的键是不能修改的,但是其键对应的值是可以修改的。
79、C++ 的 vector 和 list中,如果删除末尾的元素,其指针和迭代器如何变化?若删除的是中间的元素呢?
- 迭代器和指针之间的区别
**迭代器不是指针,是类模板,表现的像指针。他只是模拟了指针的一些功能,重载了指针的一些操作符,–>、++、–等。迭代器封装了指针,是一个”可遍历STL( Standard Template Library)容器内全部或部分元素”的对象,本质是封装了原生指针,是指针概念的一种提升,提供了比指针更高级的行为,相当于一种智能指针,他可以根据不同类型的数据结构来实现不同的++,–等操作。**迭代器返回的是对象引用而不是对象的值,所以cout只能输出迭代器使用取值后的值而不能直接输出其自身。 - vector和list特性
vector特性 动态数组。元素在内存连续存放。随机存取任何元素都在常数时间完成。在尾端增删元素具有较大的性能(大部分情况下是常数时间)。list特性双向链表。元素在内存不连续存放。在任何位置增删元素都能在常数时间完成。不支持随机存取。 - vector增删元素
对于vector而言,删除某个元素以后,该元素后边的每个元素的迭代器都会失效,后边每个元素都往前移动一位,erase返回下一个有效的迭代器。 - list增删元素
对于list而言,删除某个元素,只有“指向被删除元素”的那个迭代器失效,其它迭代器不受任何影响。
80、 请你来说一下 map 和 set 有什么区别,分别又是怎么实现的?
- set是一种关联式容器,其特性如下:
(1)set以RBTree作为底层容器
(2)所得元素的只有key没有value,value就是key
(3)不允许出现键值重复
(4)所有的元素都会被自动排序
(5)不能通过迭代器来改变set的值,因为set的值就是键,set的迭代器是const的 - map和set一样是关联式容器,其特性如下:
(1)map以RBTree作为底层容器
(2)所有元素都是键+值存在
(3)不允许键重复
(4)所有元素是通过键进行自动排序的
(5)map的键是不能修改的,但是其键对应的值是可以修改的
综上所述,map和set底层实现都是红黑树;map和set的区别在于map的值不作为键,键和值是分
开的。
81、说说 push_back 和 emplace_back 的区别?
如果要将一个临时变量push到容器的末尾,push_back()需要先构造临时对象,再将这个对象拷贝到容
器的末尾,而emplace_back()则直接在容器的末尾构造对象,这样就省去了拷贝的过程。
#include <iostream>
#include <cstring>
#include <vector>
using namespace std;
class A {
public:
A(int i){
str = to_string(i);
cout << "构造函数" << endl;
}
~A(){}
A(const A& other): str(other.str){
cout << "拷贝构造" << endl;
}
public:
string str;
};
int main() {
vector<A> vec;
vec.reserve(10);
for(int i=0;i<10;i++){
vec.push_back(A(i)); //调用了10次构造函数和10次拷贝构造函数,
// vec.emplace_back(i); //调用了10次构造函数一次拷贝构造函数都没有调用过
return 0;
}
82、STL 中 vector 与 list 具体是怎么实现的?常见操作的时间复杂度是多少?
- vector 一维数组(元素在内存连续存放)
是动态数组,在堆中分配内存,元素连续存放,有保留内存,如果减少大小后,内存也不会释放;如果新增大小当前大小时才会重新分配内存。
扩容方式:
a. 倍放开辟三倍的内存
b. 旧的数据开辟到新的内存
c. 释放旧的内存
d. 指向新内存 - list 双向链表(元素存放在堆中)
元素存放在堆中,每个元素都是放在一块内存中,它的内存空间可以是不连续的,通过指针来进行数据的访问,这个特点,使得它的随机存取变得非常没有效率,因此它没有提供[ ]操作符的重载。但是由于链表的特点,它可以很有效的支持任意地方的删除和插入操作。
特点:
a. 随机访问不方便
b. 删除插入操作方便 - 常见时间复杂度
(1)vector插入、查找、删除时间复杂度分别为:O(n)、O(1)、O(n);
(2)list插入、查找、删除时间复杂度分别为:O(1)、O(n)、O(1)。
五、C++11部分
83、说说 C++11 的新特性有哪些?
C++新特性主要包括包含语法改进和标准库扩充两个方面,主要包括以下11点:
- 语法的改进
(1)统一的初始化方法
(2)成员变量默认初始化
(3)auto关键字 用于定义变量,编译器可以自动判断的类型(前提:定义一个变量时对其进行初
始化)
(4)decltype 求表达式的类型
(5)智能指针 shared_ptr
(6)空指针 nullptr(原来NULL)
(7)基于范围的for循环
(8)右值引用和move语义 让程序员有意识减少进行深拷贝操作,节约内存空间。 - 标准库扩充(往STL里新加进一些模板类,比较好用)
(9)无序容器(哈希表) 用法和功能同map一模一样,区别在于哈希表的效率更高
(10)正则表达式 可以认为正则表达式实质上是一个字符串,该字符串描述了一种特定模式的字
符串
(11)Lambda表达式
84、说说 C++ 中智能指针和指针的区别是什么?
- 智能指针
如果在程序中使用new从堆(自由存储区)分配内存,等到不需要时,应使用delete将其释放。C++引用了智能指针auto_ptr,以帮助自动完成这个过程。随后的编程体验(尤其是使用STL)表明,需要有更精致的机制。基于程序员的编程体验和BOOST库提供的解决方案,C++11摒弃了auto_ptr,并新增了三种智能指针:unique_ptr、shared_ptr和weak_ptr。所有新增的智能指针都能与STL容器和移动语义协同工作。 - 指针
C 语言规定所有变量在使用前必须先定义,指定其类型,并按此分配内存单元。指针变量不同于整型变量和其他类型的变量,它是专门用来存放地址的,所以必须将它定义为“指针类型”。 - 智能指针和普通指针的区别
智能指针和普通指针的区别在于智能指针实际上是对普通指针加了一层封装机制,区别是它负责自动释
放所指的对象,这样的一层封装机制的目的是为了使得智能指针可以方便的管理一个对象的生命期。
85、说说 C++中的智能指针有哪些?分别解决的问题以及区别?
- C++中的智能指针有4种,分别为:shared_ptr、unique_ptr、weak_ptr、auto_ptr,其中auto_ptr被C++11弃用!
- 使用智能指针的原因
申请的空间(即new出来的空间),在使用结束时,需要delete掉,否则会形成内存碎片。在程序运行期间,new出来的对象,在析构函数中delete掉,但是这种方法不能解决所有问题,因为有时候new发生在某个全局函数里面,该方法会给程序员造成精神负担。此时,智能指针就派上了用场。使用智能指针可以很大程度上避免这个问题,因为智能指针就是一个类,当超出了类的作用域时,类会自动调用析构函数,析构函数会自动释放资源。所以,智能指针的作用原理就是在函数结束时自动释放内存空间,避免了手动释放内存空间。 - 四种指针分别解决的问题以及各自特性如下:
(1)auto_ptr(C++98的方案,C++11已经弃用)
采用所有权模式:
auto_ptr<string> p1(new string("I reigned loney as a cloud."));
auto_ptr<string> p2;
p2=p1; //auto_ptr不会报错
此时不会报错,p2剥夺了p1的所有权,但是当程序运行时访问p1将会报错。所以auto_ptr的缺点
是:存在潜在的内存崩溃问题。
(2)unique_ptr(替换auto_ptr)
unique_ptr实现独占式拥有或严格拥有概念,保证同一时间内只有一个智能指针可以指向该对象。它对于避免资源泄露,例如,以new创建对象后因为发生异常而忘记调用delete时的情形特别有用。采用所有权模式,和上面例子一样。
auto_ptr<string> p3(new string("I reigned loney as a cloud."));
auto_ptr<string> p4;
p4=p3; //此时不会报错
编译器认为P4=P3非法,避免了p3不再指向有效数据的问题。因此,unique_ptr比auto_ptr更安全。 另外unique_ptr还有更聪明的地方:当程序试图将一个 unique_ptr 赋值给另一个时,如果源unique_ptr 是个临时右值,编译器允许这么做;如果源 unique_ptr 将存在一段时间,编译器将禁止这么做,比如:
unique_ptr<string> pu1(new string ("hello world"));
unique_ptr<string> pu2;
pu2 = pu1; // #1 not allowed
unique_ptr<string> pu3;
pu3 = unique_ptr<string>(new string ("You")); // #2 allowed
其中#1留下悬挂的unique_ptr(pu1),这可能导致危害。而#2不会留下悬挂的unique_ptr,因为它调用 unique_ptr 的构造函数,该构造函数创建的临时对象在其所有权让给 pu3 后就会被销毁。这种随情况而已的行为表明,unique_ptr 优于允许两种赋值的auto_ptr 。
注意:如果确实想执行类似与#1的操作,要安全的重用这种指针,可给它赋新值。C++有一个标准库函数std::move(),让你能够将一个unique_ptr赋给另一个。例如:
unique_ptr<string> ps1, ps2;
ps1 = demo("hello");
ps2 = move(ps1);
ps1 = demo("alexia");
cout << *ps2 << *ps1 << endl;
(3)shared_ptr(非常好使)
shared_ptr实现共享式拥有概念。多个智能指针可以指向相同对象,该对象和其相关资源会在“最后一个引用被销毁”时候释放。从名字share就可以看出了资源可以被多个指针共享,它使用计数机制来表明资源被几个指针共享。可以通过成员函数use_count()来查看资源的所有者个数。除了可以通过new来构造,还可以通过传入auto_ptr, unique_ptr,weak_ptr来构造。当我们调用release()时,当前指针会释放资源所有权,计数减一。当计数等于0时,资源会被释放。shared_ptr 是为了解决 auto_ptr 在对象所有权上的局限性(auto_ptr 是独占的), 在使用引用计数的机制上提供了可以共享所有权的智
use_count 返回引用计数的个数
unique 返回是否是独占所有权( use_count 为 1)
swap 交换两个 shared_ptr 对象(即交换所拥有的对象)
reset 放弃内部对象的所有权或拥有对象的变更, 会引起原有对象的引用计数的减少
get 返回内部对象(指针), 由于已经重载了()方法, 因此和直接使用对象是一样的.如 shared_ptr
sp(new int(1)); sp 与 sp.get()是等价的
(4)weak_ptr
weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象。进行该对象的内存管理的是那个强引用的 shared_ptr。weak_ptr只是提供了对管理对象的一个访问手段。weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作,它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。
class B;
class A
{
public:
shared_ptr<B> pb_;
~A()
{
cout<<"A delete\n";
}
};
class B
{
public:
shared_ptr<A> pa_;
~B()
{
cout<<"B delete\n";
}
};
void fun()
{
shared_ptr<B> pb(new B());
shared_ptr<A> pa(new A());
pb->pa_ = pa;
pa->pb_ = pb;
cout<<pb.use_count()<<endl;
cout<<pa.use_count()<<endl;
}
int main()
{
fun();
return 0;
}
可以看到fun函数中pa ,pb之间互相引用,两个资源的引用计数为2,当要跳出函数时,智能指针pa,pb析构时两个资源引用计数会减一,但是两者引用计数还是为1,导致跳出函数时资源没有被释放(A B的析构函数没有被调用),如果把其中一个改为weak_ptr就可以了,我们把类A里面的shared_ptr pb_; 改为weak_ptr pb; 运行结果如下,这样的话,资源B的引用开始就只有1,当pb析构时,B的计数变为0,B得到释放,B释放的同时也会使A的计数减一,同时pa析构时使A的计数减一,那么A的计数为0,A得到释放。
注意:我们不能通过weak_ptr直接访问对象的方法,比如B对象中有一个方法print(),我们不能这样访问,pa->pb->print(); 英文pb是一个weak_ptr,应该先把它转化为shared_ptr,如:shared_ptrp = pa->pb_.lock(); p->print();
86、简述 C++ 右值引用与转移语义?
- 右值引用
一般来说,不能取地址的表达式,就是右值引用,能取地址的,就是左值!
class A { };
A & r = A(); //error,A()是无名变量,是右值
A && r = A(); //ok,r是右值引用
C++98/03 标准中就有引用,使用 “&” 表示。但此种引用方式有一个缺陷,即正常情况下只能操作 C++
中的左值,无法对右值添加引用。举个例子
int num = 10;
int &b = num; //正确
int &c = 10; //错误
如上所示,编译器允许我们为 num 左值建立一个引用,但不可以为 10 这个右值建立引用。因此,
C++98/03 标准中的引用又称为左值引用。
注意:虽然 C++98/03 标准不支持为右值建立非常量左值引用,但允许使用常量左值引用操作右值。也
就是说,常量左值引用既可以操作左值,也可以操作右值,例如
我们知道,右值往往是没有名称的,因此要使用它只能借助引用的方式。这就产生一个问题,实际开发
中我们可能需要对右值进行修改(实现移动语义时就需要),显然左值引用的方式是行不通的。为此,C++11 标准新引入了另一种引用方式,称为右值引用,用 “&&” 表示。
注意:和声明左值引用一样,右值引用也必须立即进行初始化操作,且只能使用右值进行初始化,比
如:
int num = 10;
//int && a = num; //右值引用不能初始化为左值
int && a = 10;
和常量左值引用不同的是,右值引用还可以对右值进行修改。例如:
int && a = 10;
a = 100;
cout << a << endl;
/* 程序运行结果:
100
*/
另外值得一提的是,C++ 语法上是支持定义常量右值引用的,例如:
const int&& a = 10;//编译器不会报错
但这种定义出来的右值引用并无实际用处。一方面,右值引用主要用于移动语义和完美转发,其中前者需要有修改右值的权限;其次,常量右值引用的作用就是引用一个不可修改的右值,这项工作完全可以交给常量左值引用完成。
2. 转移语义
move 本意为 “移动”,但该函数并不能移动任何数据,它的功能很简单,就是将某个左值强制转化为右值。基于 move() 函数特殊的功能,其常用于实现移动语义。
//程序实例
#include <iostream>
using namespace std;
class first {
public:
first() :num(new int(0)) {
cout << "construct!" << endl;
}
//移动构造函数
first(first &&d) :num(d.num) {
d.num = NULL;
cout << "first move construct!" << endl;
}
public: //这里应该是 private,使用 public 是为了更方便说明问题
int *num;
};
class second {
public:
second() :fir() {}
//用 first 类的移动构造函数初始化 fir
second(second && sec) :fir(move(sec.fir)) {
cout << "second move construct" << endl;
}
public: //这里也应该是 private,使用 public 是为了更方便说明问题
first fir;
};
int main() {
second oth;
second oth2 = move(oth);
//cout << *oth.fir.num << endl; //程序报运行时错误
return 0;
}
/* 程序运行结果:
construct!
first move construct!
second move construct
*/
87、简述 C++ 中智能指针的特点?
- C++中的智能指针有4种,分别为:shared_ptr、unique_ptr、weak_ptr、auto_ptr,其中auto_ptr被C++11弃用。
- 为什么要使用智能指针:智能指针的作用是管理一个指针,因为存在申请的空间在函数结束时忘记释放,造成内存泄漏的情况。使用智能指针可以很大程度上避免这个问题,因为智能指针就是一个类,当超出了类的作用域时,类会自动调用析构函数,自动释放资源。
- 四种指针各自特性
(1)auto_ptr
auto指针存在的问题是,两个智能指针同时指向一块内存,就会两次释放同一块资源,自然报错。
(2)unique_ptr
unique指针规定一个智能指针独占一块内存资源。当两个智能指针同时指向一块内存,编译报错。实现原理:将拷贝构造函数和赋值拷贝构造函数申明为private或delete。不允许拷贝构造函数和赋值操作符,但是支持移动构造函数,通过std:move把一个对象指针变成右值之后可以移动给另一个unique_ptr
(3)shared_ptr
共享指针可以实现多个智能指针指向相同对象,该对象和其相关资源会在引用为0时被销毁释放。实现理:有一个引用计数的指针类型变量,专门用于引用计数,使用拷贝构造函数和赋值拷贝构造函数时,引用计数加1,当引用计数为0时,释放资源。
注意:weak_ptr、shared_ptr存在一个问题,当两个shared_ptr指针相互引用时,那么这两个指针的引用计数不会下降为0,资源得不到释放。因此引入weak_ptr,weak_ptr是弱引用,weak_ptr的构造和析构不会引起引用计数的增加或减少。
88、weak_ptr 能不能知道对象计数为 0,为什么?
不能。
weak_ptr是一种不控制对象生命周期的智能指针,它指向一个shared_ptr管理的对象。进行该对象管理的是那个引用的shared_ptr。weak_ptr只是提供了对管理 对象的一个访问手段。weak_ptr设计的目的只是为了配合shared_ptr而引入的一种智能指针,配合shared_ptr工作,它只可以从一个shared_ptr或者另一个weak_ptr对象构造,它的构造和析构不会引起计数的增加或减少。
89、 weak_ptr 如何解决 shared_ptr 的循环引用问题?
为了解决循环引用导致的内存泄漏,引入了弱指针weak_ptr,weak_ptr的构造函数不会修改引用计数的值,从而不会对对象的内存进行管理,其类似一个普通指针,但是不会指向引用计数的共享内存,但是可以检测到所管理的对象是否已经被释放,从而避免非法访问。
90、说说智能指针及其实现,shared_ptr 线程安全性,原理?
- C++里面的四个智能指针: auto_ptr, shared_ptr, weak_ptr, unique_ptr 其中后三个是c++11支持,并且第一个已经被11弃用。
- 为什么要使用智能指针
智能指针的作用是管理一个指针,因为存在以下这种情况:申请的空间在函数结束时忘记释放,造成内存泄漏。使用智能指针可以很大程度上的避免这个问题,因为智能指针就是一个类,当超出了类的作用域是,类会自动调用析构函数,析构函数会自动释放资源。所以智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间。 - 实现见85。
- 线程安全性
多线程环境下,调用不同shared_ptr实例的成员函数是不需要额外的同步手段的,即使这些shared_ptr拥有的是同样的对象。但是如果多线程访问(有写操作)同一个shared_ptr,则需要同步,否则就会有race condition 发生。也可以使用 shared_ptr overloads of atomic functions来防止race condition的发生。多个线程同时读同一个shared_ptr对象是线程安全的,但是如果是多个线程对同一个shared_ptr对象进行读和写,则需要加锁。多线程读写shared_ptr所指向的同一个对象,不管是相同的shared_ptr对象,还是不同的shared_ptr对象,也需要加锁保护。例子如下:
//程序实例
shared_ptr<long> global_instance = make_shared<long>(0);
std::mutex g_i_mutex;
void thread_fcn()
{
//std::lock_guard<std::mutex> lock(g_i_mutex);
//shared_ptr<long> local = global_instance;
for(int i = 0; i < 100000000; i++)
{
*global_instance = *global_instance + 1;
//*local = *local + 1;
}
}
int main(int argc, char** argv)
{
thread thread1(thread_fcn);
thread thread2(thread_fcn);
thread1.join();
thread2.join();
cout << "*global_instance is " << *global_instance << endl;
return 0;
}
在线程函数thread_fcn的for循环中,2个线程同时对global_instance进行加1的操作。这就是典型的非线程安全的场景,最后的结果是未定的,运行结果如下:
*global_instance is 197240539
如果使用的是每个线程的局部shared_ptr对象local,因为这些local指向相同的对象,因此结果也是未定的,运行结果如下: *global_instance is 160285803
因此,这种情况下必须加锁,将thread_fcn中的第一行代码的注释去掉之后,不管是使用global_instance,还是使用local,得到的结果都是:
*global_instance is 200000000
91、请你回答一下智能指针有没有内存泄露的情况?
智能指针有内存泄露的情况发生。
- 智能指针发生内存泄露的情况
当两个对象同时使用一个shared_ptr成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄露。 - 智能指针的内存泄漏如何解决
为了解决循环引用导致的内存泄漏,引入了弱指针weak_ptr,weak_ptr的构造函数不会修改引用计数的值,从而不会对对象的内存进行管理,其类似一个普通指针,但是不会指向引用计数的共享内存,但是可以检测到所管理的对象是否已经被释放,从而避免非法访问。
92、简述一下 C++11 中四种类型转换?
C++中四种类型转换分别为const_cast、static_cast、dynamic_cast、reinterpret_cast,四种转换
功能分别如下:
- const_cast
将const变量转为非const - static_cast
最常用,可以用于各种隐式转换,比如非const转const,static_cast可以用于类向上转换,但向下转换能成功但是不安全。 - dynamic_cast
只能用于含有虚函数的类转换,用于类向上和向下转换
向上转换:指子类向基类转换。
向下转换:指基类向子类转换。
这两种转换,子类包含父类,当父类转换成子类时可能出现非法内存访问的问题。
dynamic_cast通过判断变量运行时类型和要转换的类型是否相同来判断是否能够进行向下转换。
dynamic_cast可以做类之间上下转换,转换的时候会进行类型检查,类型相等成功转换,类型不等转换失败。运用RTTI技术,RTTI是”Runtime Type Information”的缩写,意思是运行时类型信息,它提供了运行时确定对象类型的方法。在c++层面主要体现在dynamic_cast和typeid,vs中虚函数表的-1位置存放了指向type_info的指针,对于存在虚函数的类型,dynamic_cast和typeid都会去查询type_info。 - reinterpret_cast
reinterpret_cast可以做任何类型的转换,不过不对转换结果保证,容易出问题。
注意:为什么不用C的强制转换:C的强制转换表面上看起来功能强大什么都能转,但是转换不够明确,
不能进行错误检查,容易出错。
93、简述一下 C++ 11 中 auto 的具体用法?
auto用于定义变量,编译器可以自动判断变量的类型。auto主要有以下几种用法:
- auto的基本使用方法
(1)基本使用语法如下
auto name = value; //name 是变量的名字,value 是变量的初始值
(2)程序实例如下
auto n = 10;
auto f = 12.8;
auto p = &n;
auto url = "www.123.com";
a. 第 1 行中,10 是一个整数,默认是 int 类型,所以推导出变量 n 的类型是 int。
b. 第 2 行中,12.8 是一个小数,默认是 double 类型,所以推导出变量 f 的类型是 double。
c. 第 3 行中,&n 的结果是一个 int* 类型的指针,所以推导出变量 f 的类型是 int*。
d. 第 4 行中,由双引号 “” 包围起来的字符串是 const char* 类型,所以推导出变量 url 的类型是 const
char*,也即一个常量指针。
2. auto和 const 的结合使用
(1) auto 与 const 结合的用法
a. 当类型不为引用时,auto 的推导结果将不保留表达式的 const 属性;
b. 当类型为引用时,auto 的推导结果将保留表达式的 const 属性
(2)程序实例如下
int x = 0;
const auto n = x; //n 为 const int ,auto 被推导为 int
auto f = n; //f 为 const int,auto 被推导为 int(const 属性被抛弃)
const auto &r1 = x; //r1 为 const int& 类型,auto 被推导为 int
auto &r2 = r1; //r1 为 const int& 类型,auto 被推导为 const int 类型
a. 第 2 行代码中,n 为 const int,auto 被推导为 int。
b. 第 3 行代码中,n 为 const int 类型,但是 auto 却被推导为 int 类型,这说明当 = 右边的表达式带有
const 属性时,auto 不会 使用 const 属性,而是直接推导出 non-const 类型。
c. 第 4 行代码中,auto 被推导为 int 类型,这个很容易理解,不再赘述。
d. 第 5 行代码中,r1 是 const int & 类型,auto 也被推导为 const int 类型,这说明当 const 和引用结
合时,auto 的推导将保留 表达式的 const 类型。
3. 用于泛型编程
auto 的另一个应用就是当我们不知道变量是什么类型,或者不希望指明具体类型的时候,比如泛
型编程中。请看下面例子:
#include <iostream>
using namespace std;
class A{
public:
static int get(void){
return 100;
}
};
class B{
public:
static const char* get(void){
return "www.123.com";
}
};
template <typename T>
void func(void){
auto val = T::get();
cout << val << endl;
}
int main(void){
func<A>();
func<B>();
return 0;
}
/* 运行结果:
100
www.123.com
*/
本例中的模板函数 func() 会调用所有类的静态函数 get(),并对它的返回值做统一处理,但是 get() 的返
回值类型并不一样,而且不能自动转换。这种要求在以前的 C++ 版本中实现起来非常的麻烦,需要额外
增加一个模板参数,并在调用时手动给该模板参数赋值,用以指明变量 val 的类型。但是有了 auto 类型
自动推导,编译器就根据 get() 的返回值自己推导出 val 变量的类型,就不用再增加一个模板参数了。
94、简述一下 C++11 中的可变参数模板新特性?
可变参数模板(variadic template)使得编程者能够创建这样的模板函数和模板类,即可接受可变数量的
参数。例如要编写一个函数,它可接受任意数量的参数,参数的类型只需是cout能显示的即可,并将参
数显示为用逗号分隔的列表。
int n = 14;
double x = 2.71828;
std::string mr = "Mr.String objects!";
show_list(n, x);
show_list(x*x, '!', 7, mr); //这里的目标是定义show_list()
/* 运行结果:
14, 2.71828
7.38905, !, 7, Mr.String objects!
要创建可变参数模板,需要理解几个要点:
(1)模板参数包(parameter pack);
(2)函数参数包;
(3)展开(unpack)参数包;
(4)递归。
95、简述一下 C++11 中 Lambda 新特性?
- 定义
lambda 匿名函数很简单,可以套用如下的语法格式:
[外部变量访问方式说明符] (参数) mutable noexcept/throw() -> 返回值类型
{
函数体;
};
其中各部分的含义分别为:
a. [外部变量方位方式说明符]
[ ] 方括号用于向编译器表明当前是一个 lambda 表达式,其不能被省略。在方括号内部,可以注明当前 lambda 函数的函数体中可以使用哪些外部变量
所谓外部变量,指的是和当前 lambda 表达式位于同一作用域内的所有局部变量。
b. (参数)
和普通函数的定义一样,lambda 匿名函数也可以接收外部传递的多个参数。和普通函数不同的是,如果不需要传递参数,可以连同 () 小括号一起省略;
c. mutable
此关键字可以省略,如果使用则之前的 () 小括号将不能省略(参数个数可以为 0)。默认情况下,对于以值传递方式引入的外部变量,不允许在 lambda 表达式内部修改它们的值(可以理解为这部分变量都是 const 常量)。而如果想修改它们,就必须使用 mutable 关键字!
注意:对于以值传递方式引入的外部变量,lambda 表达式修改的是拷贝的那一份,并不会修改真正的外部变量;
d. noexcept/throw()
可以省略,如果使用,在之前的 () 小括号将不能省略(参数个数可以为 0)。默认情况下,lambda函数的函数体中可以抛出任何类型的异常。而标注 noexcept 关键字,则表示函数体内不会抛出任何异常;使用 throw() 可以指定 lambda 函数内部可以抛出的异常类型。
e. -> 返回值类型
指明 lambda 匿名函数的返回值类型。值得一提的是,如果 lambda 函数体内只有一个 return 语句,或者该函数返回 void,则编译器可以自行推断出返回值类型,此情况下可以直接省略"-> 返回值类型"。
f. 函数体
和普通函数一样,lambda 匿名函数包含的内部代码都放置在函数体中。该函数体内除了可以使用指定传
递进来的参数之外,还可以使用指定的外部变量以及全局范围内的所有全局变量。
2. 程序实例
#include <iostream>
#include <algorithm>
using namespace std;
int main()
{
int num[4] = {4, 2, 3, 1};
//对 a 数组中的元素进行排序
sort(num, num+4, [=](int x, int y) -> bool{ return x < y; } );
for(int n : num){
cout << n << " ";
}
return 0;
}
/* 程序运行结果:
1 2 3 4
*/