2019年总结C++概念面试题目

一、有了malloc/free,为什么还要new/delete?
malloc与free是C/C++的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。
对于非内部数据类型的对象而言,光用malloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象的消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。
因此,C++需要一个能完成动态内存分配和初始化工作的运算符new,以及一个能完成清理与释放内存工作的运算符delete。注意:new/delete不是库函数。请看下面例子:

					#include <iostream>
					using namespace std;
					class Obj
					{
					public:
						Obj(void)
						{
							cout << "Initialization" << endl;
						}
						~Obj(void)
						{
							cout << "Destroy" << endl;
						}
					};
					
					void UseMallocFree(void)
					{
						cout << "in UseMallocFree()" << endl;
						Obj *a = (Obj*)malloc(sizeof(Obj));
						free(a);
					}
					
					void UseNewDelete(void)
					{
						cout << "in UseNewDelete()" << endl;
						Obj *a = new Obj;
						delete a;
					}
					
					int main()
					{
						UseMallocFree();
						UseNewDelete();
						return 0;
					}
		调试结果:in UseMallocFree()
                 in UseNewDelete()
                 Initialization
                 Destroy

在这个示例中,类Obj只有构造函数和析构函数的定义,这两个成员函数分别打印一句话。函数UseMallocFree()中调用malloc/free申请和释放堆内存;函数UseNewDelete()中调用new/delete申请和释放堆内存。可以看出函数UseMallocFree()执行时,类Obj的构造函数和析构函数都不会被调用;而函数UseNewDelete()执行时,类Obj的构造函数和析构函数都会被调用。
**总结:**对于非内部数据类型的对象而言,对象的消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能把执行构造函数和析构函数的任务强加于malloc/free,因此只有使用new/delete运算符。

二、map 如何使用结构体作为自定义键值
在使用map时,有时候我们需要自定义键值,才能符合程序的需要。
比如我们需要使用自定义的结构体来作为map的键值:

						struct  Test
						{
						    int x;
						    int y;
						};

这样直接使用的话,在编译时会出问题:

		1>------ Build started: Project: MapRemove, Configuration: Debug Win32 ------
		1>  Source.cpp
		1>d:\program files (x86)\microsoft visual studio 12.0\vc\include\xstddef(193): error C2784: 'bool std::operator <(const std::tuple<_Types...> &,const std::tuple<_Types1...> &)' : could not deduce template argument for 'const std::tuple<_Types...> &' from 'const Test'
		1>          d:\program files (x86)\microsoft visual studio 12.0\vc\include\tuple(480) : see declaration of 'std::operator <'
		1>          d:\program files (x86)\microsoft visual studio 12.0\vc\include\xstddef(192) : while compiling class template member function 'bool std::less<_Kty>::operator ()(const _Ty &,const _Ty &) const'
		1>          with
		1>          [
		1>              _Kty=Test
		1>  ,            _Ty=Test
		1>          ]
		1>          d:\program files (x86)\microsoft visual studio 12.0\vc\include\map(228) : see reference to function template instantiation 'bool std::less<_Kty>::operator ()(const _Ty &,const _Ty &) const' being compiled
		1>          with
		1>          [
		1>              _Kty=Test
		1>  ,            _Ty=Test
		1>          ]
		1>          d:\program files (x86)\microsoft visual studio 12.0\vc\include\type_traits(572) : see reference to class template instantiation 'std::less<_Kty>' being compiled
		1>          with
		1>          [
		1>              _Kty=Test
		1>          ]
		1>          d:\program files (x86)\microsoft visual studio 12.0\vc\include\xtree(1023) : see reference to class template instantiation 'std::is_empty<std::less<_Kty>>' being compiled
		1>          with
		1>          [
		1>              _Kty=Test
		1>          ]
		1>          d:\program files (x86)\microsoft visual studio 12.0\vc\include\map(70) : see reference to class template instantiation 'std::_Tree<std::_Tmap_traits<_Kty,_Ty,_Pr,_Alloc,false>>' being compiled
		1>          with
		1>          [
		1>              _Kty=Test
		1>  ,            _Ty=std::string
		1>  ,            _Pr=std::less<Test>
		1>  ,            _Alloc=std::allocator<std::pair<const Test,std::string>>
		1>          ]
		1>          f:\xdd\xaudio2\mapremove\source.cpp(12) : see reference to class template instantiation 'std::map<Test,std::string,std::less<_Kty>,std::allocator<std::pair<const _Kty,_Ty>>>' being compiled
		1>          with
		1>          [
		1>              _Kty=Test
		1>  ,            _Ty=std::string
		1>          ]
		1>d:\program files (x86)\microsoft visual studio 12.0\vc\include\xstddef(193): error C2784: 'bool std::operator <(const std::_Tree<_Traits> &,const std::_Tree<_Traits> &)' : could not deduce template argument for 'const std::_Tree<_Traits> &' from 'const Test'
		1>          d:\program files (x86)\microsoft visual studio 12.0\vc\include\xtree(2259) : see declaration of 'std::operator <'
		1>d:\program files (x86)\microsoft visual studio 12.0\vc\include\xstddef(193): error C2784: 'bool std::operator <(const std::basic_string<_Elem,_Traits,_Alloc> &,const _Elem *)' : could not deduce template argument for 'const std::basic_string<_Elem,_Traits,_Alloc> &' from 'const Test'
		1>          d:\program files (x86)\microsoft visual studio 12.0\vc\include\xstring(2545) : see declaration of 'std::operator <'
		1>d:\program files (x86)\microsoft visual studio 12.0\vc\include\xstddef(193): error C2784: 'bool std::operator <(const _Elem *,const std::basic_string<_Elem,_Traits,_Alloc> &)' : could not deduce template argument for 'const _Elem *' from 'const Test'
		1>          d:\program files (x86)\microsoft visual studio 12.0\vc\include\xstring(2535) : see declaration of 'std::operator <'
		1>d:\program files (x86)\microsoft visual studio 12.0\vc\include\xstddef(193): error C2784: 'bool std::operator <(const std::basic_string<_Elem,_Traits,_Alloc> &,const std::basic_string<_Elem,_Traits,_Alloc> &)' : could not deduce template argument for 'const std::basic_string<_Elem,_Traits,_Alloc> &' from 'const Test'
		1>          d:\program files (x86)\microsoft visual studio 12.0\vc\include\xstring(2525) : see declaration of 'std::operator <'
		1>d:\program files (x86)\microsoft visual studio 12.0\vc\include\xstddef(193): error C2784: 'bool std::operator <(const std::move_iterator<_RanIt> &,const std::move_iterator<_RanIt2> &)' : could not deduce template argument for 'const std::move_iterator<_RanIt> &' from 'const Test'
		1>          d:\program files (x86)\microsoft visual studio 12.0\vc\include\xutility(1997) : see declaration of 'std::operator <'
		1>d:\program files (x86)\microsoft visual studio 12.0\vc\include\xstddef(193): error C2784: 'bool std::operator <(const std::reverse_iterator<_RanIt> &,const std::reverse_iterator<_RanIt2> &)' : could not deduce template argument for 'const std::reverse_iterator<_RanIt> &' from 'const Test'
		1>          d:\program files (x86)\microsoft visual studio 12.0\vc\include\xutility(1155) : see declaration of 'std::operator <'
		1>d:\program files (x86)\microsoft visual studio 12.0\vc\include\xstddef(193): error C2784: 'bool std::operator <(const std::_Revranit<_RanIt,_Base> &,const std::_Revranit<_RanIt2,_Base2> &)' : could not deduce template argument for 'const std::_Revranit<_RanIt,_Base> &' from 'const Test'
		1>          d:\program files (x86)\microsoft visual studio 12.0\vc\include\xutility(971) : see declaration of 'std::operator <'
		1>d:\program files (x86)\microsoft visual studio 12.0\vc\include\xstddef(193): error C2784: 'bool std::operator <(const std::pair<_Ty1,_Ty2> &,const std::pair<_Ty1,_Ty2> &)' : could not deduce template argument for 'const std::pair<_Ty1,_Ty2> &' from 'const Test'
		1>          d:\program files (x86)\microsoft visual studio 12.0\vc\include\utility(230) : see declaration of 'std::operator <'
		1>d:\program files (x86)\microsoft visual studio 12.0\vc\include\xstddef(193): error C2676: binary '<' : 'const Test' does not define this operator or a conversion to a type acceptable to the predefined operator
		========== Build: 0 succeeded, 1 failed, 0 up-to-date, 0 skipped ==========

看错误是说,键值无法比较。因为map的键值是自动比较后进插入的,键值是递增的。
现在我们自定义的键值,编译器无法进行比较,找不到类似的模板,所以报错。
既然是没有‘<’,那我们自己重载小于操作符应该就可以了:

		struct  Test
		{
		    int x;
		    int y;
		
		    bool operator < (const Test &o) const
		    {
		        return x < o.x || y < o.y;
		    }
		};

重载后,重新编译,顺利通过。测试代码如下:

							#include <map>
							#include <iostream>
							struct  Test
							{
							    int x;
							    int y;
							
							    bool operator < (const Test &o) const
							    {
							        return x < o.x || y < o.y;
							    }
							};
							
							int main()
							{
							    std::map<Test, std::string> mapTest;
							    Test test = { 1, 2 };
							    mapTest[test] = "Test1";
							
							    for (auto it = mapTest.begin(); it != mapTest.end();it++)
							    {
							        std::cout << it->first.x << " " << it->first.y << " " << it->second.c_str() << std::endl;
							    }
							
							    return 0;
							}
		测试结果:
        1 2 Test1

三、构造函数中为什么不能调用虚函数?

构造函数调用层次会导致一个有趣的两难选择。试想:如果我们在构造函数中并且调用了虚函数,那么会发生什么现象呢?在普通的成员函数中,我们可以想象所发生的情况——虚函数的调用是在运行时决定的。这是因为编译时这个对象并不能知道它是属于这个成员函数所在的那个类,还是属于由它派生出来的某个类。于是,我们也许会认为在构造函数中也会发生同样的事情。
然而,情况并非如此。对于在构造函数中调用一个虚函数的情况,被调用的只是这个函数的本地版本。也就是说,虚机制在构造函数中不工作。
这种行为有两个理由
第一个理由是概念上的。
在概念上,构造函数的工作是生成一个对象。在任何构造函数中,可能只是部分形成对象——我们只能知道基类已被初始化,但并不能知道哪个类是从这个基类继承来的。然而,虚函数在继承层次上是“向前”和“向外”进行调用。它可以调用在派生类中的函数。如果我们在构造函数中也这样做,那么我们所调用的函数可能操作还没有被初始化的成员,这将导致灾难发生。
第二个理由是机械上的。
当一个构造函数被调用时,它做的首要的事情之一就是初始化它的VPTR。然而,它只能知道它属于“当前”类——即构造函数所在的类。于是它完全不知道这个对象是否是基于其它类。当编译器为这个构造函数产生代码时,它是为这个类的构造函数产生代码——既不是为基类,也不是为它的派生类(因为类不知道谁继承它)。所以它使用的VPTR必须是对于这个类的VTABLE。而且,只要它是最后的构造函数调用,那么在这个对象的生命期内,VPTR将保持被初始化为指向这个VTABLE。但如果接着还有一个更晚派生类的构造函数被调用,那么这个构造函数又将设置VPTR指向它的VTABLE,以此类推,直到最后的构造函数结束。VRTP的状态是由被最后调用的构造函数确定的。这就是为什么构造函数调用是按照从基类到最晚派生类的顺序的另一个理由。
但是,当这一系列构造函数调用正发生时,每个构造函数都已经设置VPTR指向它自己的VTABLE。如果函数调用使用虚机制,它将只产生通过它自己的VTABLE的调用,而不是最后派生的VTABLE(所有构造函数被调用后才会有最后派生的VTABLE)。另外,许多编译器认识到,如果在构造函数中进行虚函数调用,应该使用早绑定,因为它们知道晚绑定将只对本地函数产生调用。无论哪种情况,在构造函数中调用虚函数都不能得到预期的结果。

四、C++中map、hash_map、unordered_map、unordered_set的区别
标题中提到的四种容器,对于概念不清的人来说,经常容易弄混淆。这里我不去把库里面复杂的原码拿出剖析,这个如果有兴趣其实完全可以查C++Reference,网上的原码是最权威和细致的了,而且我觉得有耐心直接认真看原码的人,也不需要我这篇速记博文了,所以我这里还是讲的通俗一些,把它们区分的七七八八。
1、hash_map与unordered_map

这两个的内部结构都是采用哈希表来实现。区别在哪里?unordered_map在C++11的时候被引入标准库了,而hash_map没有,所以建议还是使用unordered_map比较好。
哈希表的好处是什么?查询平均时间是O(1)。顾名思义,unordered,就是无序了,数据是按散列函数插入到槽里面去的,数据之间无顺序可言,但是有些时候我只要访问而不需要顺序,因此可以选择哈希表。举个最好的例子,就是我的LeetCode的博文里------Longest Consecutive Sequence这一题,我的方法二就是用的unordered_map来实现“空间弥补时间”这样的做法。
2、unordered_map与map

虽然都是map,但是内部结构大大的不同哎,map的内部结构是R-B-tree来实现的,所以保证了一个稳定的动态操作时间,查询、插入、删除都是O(logn),最坏和平均都是。而unordered_map如前所述,是哈希表。顺便提一下,哈希表的查询时间虽然是O(1),但是并不是unordered_map查询时间一定比map短,因为实际情况中还要考虑到数据量,而且unordered_map的hash函数的构造速度也没那么快,所以不能一概而论,应该具体情况具体分析。
3、unordered_map与unordered_set

后者就是在哈希表插入value,而这个value就是它自己的key,而不是像之前的unordered_map那样有键-值对,这里单纯就是为了方便查询这些值。我在Longest Consecutive Sequence这一题,我的方法一就是用了unordered_set,同样是一个将空间弥补时间的方法。再举个大家好懂的例子,给你A,B两组数,由整数组成,然后把B中在A中出现的数字取出来,要求用线性时间完成。很明显,这道题应该将A的数放到一个表格,然后线性扫描B,发现存在的就取出。可是A的范围太大,你不可能做一个包含所有整数的表,因为这个域太大了,所以我们就用unordered_set来存放A的数,具体实现库函数已经帮你搞定了,如果对于具体怎么去散列的同学又兴趣可以查看《算法导论》的第11章或者《数据结构与算法分析》的第五章,如果要看《算法导论》的同学我给个建议,完全散列你第一遍的时候可以暂时跳过不看,确实有点复杂。

五、C/C++中extern的用法
在C语言中,修饰符extern用在变量或者函数的声明前,用来说明“此变量/函数是在别处定义的,要在此处引用”。
(1):extern修饰变量的声明。举例来说,如果文件a.c需要引用b.c中变量int v,就可以在a.c中声明extern int v,然后就可以引用变量v。这里需要注意的是,被引用的变量v的链接属性必须是外链接(external)的,也就是说a.c要引用到v,不只是取决于在a.c中声明extern int v,还取决于变量v本身是能够被引用到的。这涉及到c语言的另外一个话题--变量的作用域。能够被其他模块以extern修饰符引用到的变量通常是全局变量。还有很重要的一点是,extern int v可以放在a.c中的任何地方,比如你可以在a.c中的函数fun定义的开头处声明extern int v,然后就可以引用到变量v了,只不过这样只能在函数fun作用域中引用v罢了,这还是变量作用域的问题。对于这一点来说,很多人使用的时候都心存顾虑。好像extern声明只能用于文件作用域似的。
(2):extern修饰函数声明。从本质上来讲,变量和函数没有区别。函数名是指向函数二进制块开头处的指针。如果文件a.c需要引用b.c中的函数,比如在b.c中原型是int fun(int mu),那么就可以在a.c中声明extern int fun(int mu),然后就能使用fun来做任何事情。就像变量的声明一样,extern int fun(int mu)可以放在a.c中任何地方,而不一定非要放在a.c的文件作用域的范围中。对其他模块中函数的引用,最常用的方法是包含这些函数声明的头文件。
使用extern和包含头文件来引用函数有什么区别呢?
extern的引用方式比包含头文件要简洁得多!extern的使用方法是直接了当的,想引用哪个函数就用extern声明哪个函数。这大概是KISS原则的一种体现吧!这样做的一个明显的好处是,会加速程序的编译(确切的说是预处理)的过程,节省时间。在大型C程序编译过程中,这种差异是非常明显的。
(3):此外,extern修饰符可用于指示C或者C++函数的调用规范。比如在C++中调用C库函数,就需要在C++程序中用extern “C”声明要引用的函数。这是给链接器用的,告诉链接器在链接的时候用C函数规范来链接。主要原因是C++和C程序编译完成后在目标代码中命名规则不同。

猜你喜欢

转载自blog.csdn.net/N1314N/article/details/89412941