二十万字C/C++、嵌入式软开面试题全集宝典四

目录

1、 虚函数的代价?

2、 用C语言实现C++的继承

3、 继承机制中对象之间如何转换?指针和引用之间如何转换?

4、 C++四种类型转换

5、 为什么要用static_cast转换而不用c语言中的转换?

6、 组合与继承优缺点?

7、 左值右值

8、 总结左值和右值的概念

9、 移动构造函数

10、 C语言的编译链接过程?

11、 vector与list的区别与应用?怎么找某vector或者list的倒数第二个元素

12、 STL vector的实现,删除其中的元素,迭代器如何变化?为什么是两倍扩容?释放空间?

13、 容器内部删除一个元素

14、 STL迭代器如何实现

15、 set与hash_set的区别

16、 hash_map与map的区别

17、 map、set是怎么实现的,红黑树是怎么能够同时实现这两种容器?为什么使用红黑树?

18、 如何在共享内存上使用stl标准库?

19、 map插入方式有几种?

20、 STL中unordered_map(hash_map)和map的区别,hash_map如何解决冲突以及扩容


 二十万字C/C++、嵌入式软开面试题全集宝典四_第1张图片

 二十万字C/C++、嵌入式软开面试题全集宝典四_第2张图片

二十万字C/C++、嵌入式软开面试题全集宝典四_第3张图片

1、 虚函数的代价?

1.带有虚函数的类,每一个类会产生一个虚函数表,用来存储指向虚成员函数的指针,增大类;

2.带有虚函数的类的每一个对象,都会有一个指向虚表的指针,会增加对象的空间大小;

3.不能再是内联的函数,因为内联函数在编译阶段进行替代,而虚函数表示等待,在运行阶段才能确定到底是采用哪种函数,虚函数不能是内联函数。

2、 用C语言实现C++的继承

#include 

using namespace std;

//C++中的继承与多态

struct A

{

    virtual void fun()    //C++中的多态:通过虚函数实现

    {

        cout<<"A:fun()"<fun();    //调用父类的同名函数

    p1 = &b;      //让父类指针指向子类的对象

    p1->fun();    //调用子类的同名函数

    //C语言模拟继承与多态的测试

    _A _a;    //定义一个父类对象_a

    _B _b;    //定义一个子类对象_b

    _a._fun = _fA;        //父类的对象调用父类的同名函数

    _b._a_._fun = _fB;    //子类的对象调用子类的同名函数

    _A* p2 = &_a;   //定义一个父类指针指向父类的对象

    p2->_fun();     //调用父类的同名函数

    p2 = (_A*)&_b;  //让父类指针指向子类的对象,由于类型不匹配所以要进行强转

    p2->_fun();     //调用子类的同名函数

}

3、 继承机制中对象之间如何转换?指针和引用之间如何转换?

1.向上类型转换

将派生类指针或引用转换为基类的指针或引用被称为向上类型转换,向上类型转换会自动进行,而且向上类型转换是安全的。

2.向下类型转换

将基类指针或引用转换为派生类指针或引用被称为向下类型转换,向下类型转换不会自动进行,因为一个基类对应几个派生类,所以向下类型转换时不知道对应哪个派生类,所以在向下类型转换时必须加动态类型识别技术。RTTI技术,用dynamic_cast进行向下类型转换。

4、 C++四种类型转换

C++的四种强制转换包括:static_cast, dynamic_cast, const_cast, reinterpret_cast。

1.static_cast能进行基础类型之间的转换,也是最常看到的类型转换。明确指出类型转换,⼀般建议将隐式转换都替换成显示转换,因为没有动态类型检查,上⾏转换(派⽣类->基类)安全,下⾏转换(基类->派⽣类) 不安全,所以主要执⾏⾮多态的转换操作;

它主要有如下几种用法:

1用于类层次结构中父类和子类之间指针或引用的转换。进行上行转换(把子类的指针或引用转换成父类表示)是安全的;

2进行下行转换(把父类指针或引用转换成子类指针或引用)时,由于没有动态类型检查,所以是不安全的;

3用于基本数据类型之间的转换,如把int转换成char,把int转换成enum。这种转换的安全性也要开发人员来保证。

4把void指针转换成目标类型的指针(不安全!!)

5把任何类型的表达式转换成void类型。

2.const_cast专⻔⽤于const,volatile属性的转换,去除const性质,或增加const性质,是四个转换符中唯⼀⼀个可以操作常量的转换符。除了去掉const 或volatile修饰之外,type_id和expression得到的类型是一样的。但需要特别注意的是const_cast不是用于去除变量的常量性,而是去除指向常数对象的指针或引用的常量性,其去除常量性的对象必须为指针或引用。

3.reinterpret_cast不到万不得已,不要使⽤这个转换符,⾼危操作。使⽤特点:从底层对数据进⾏重新解释,依赖具体的平台,可移植性差。它可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针(先把一个指针转换成一个整数,在把该整数转换成原类型的指针,还可以得到原先的指针值)。可以在指针和引⽤之间进⾏肆⽆忌惮的转换。

4.dynamic_cast 专⻔⽤于派⽣类之间的转换,type-id必须是类指针,类引⽤或void*,主要用在继承体系中的安全向下转型。它能安全地将指向基类的指针转型为指向子类的指针或引用,并获知转型动作成功是否。当类型不⼀致时,转型失败会返回null(转型对象为指针时)或抛出异常bad_cast(转型对象为引用时),⽽static_cast,当类型不⼀致时,转换过来的事错误意义的指针,可能造成⾮法访问等问题。dynamic_cast 会动用运行时信息(RTTI)来进行类型安全检查,因此 dynamic_cast 存在一定的效率损失。当使用dynamic_cast时,该类型必须含有虚函数,这是因为dynamic_cast使用了存储在VTABLE中的信息来判断实际的类型,RTTI运行时类型识别用于判断类型。typeid表达式的形式是typeid(e),typeid操作的结果是一个常量对象的引用,该对象的类型是type_info或type_info的派生。

5、 为什么要用static_cast转换而不用c语言中的转换?

1.更加安全;

2.更直接明显,能够一眼看出是什么类型转换为什么类型,容易找出程序中的错误;可清楚地辨别代码中每个显式的强制转;可读性更好,能体现程序员的意图

6、 组合与继承优缺点?

一、继承

继承是is a 的关系,比如说Student继承Person,则说明Student is a Person。继承的优点是子类可以重写父类的方法来方便地实现对父类的扩展。

继承的缺点有以下几点:

①:父类的内部细节对子类是可见的。

②:子类从父类继承的方法在编译时就确定下来了,所以无法在运行期间改变从父类继承的方法的行为。

③:如果对父类的方法做了修改的话(比如增加了一个参数),则子类的方法必须做出相应的修改。所以说子类与父类是一种高耦合,违背了面向对象思想。

二、组合

组合也就是设计类的时候把要组合的类的对象加入到该类中作为自己的成员变量。

组合的优点:

①当前对象只能通过所包含的那个对象去调用其方法,所以所包含的对象的内部细节对当前对象时不可见的。

②当前对象与包含的对象是一个低耦合关系,如果修改包含对象的类中代码不需要修改当前对象类的代码。

③当前对象可以在运行时动态的绑定所包含的对象。可以通过set方法给所包含对象赋值。

组合的缺点:

①容易产生过多的对象。

②为了能组合多个对象,必须仔细对接口进行定义。

7、 左值右值

1.在C++11中所有的值必属于左值、右值两者之一,右值又可以细分为纯右值、将亡值。在C++11中可以取地址的、有名字的就是左值,反之,不能取地址的、没有名字的就是右值(将亡值或纯右值)。举个例子,int a = b+c, a 就是左值,其有变量名为a,通过&a可以获取该变量的地址;表达式b+c、函数int func()的返回值是右值,在其被赋值给某一变量前,我们不能通过变量名找到它,&(b+c)这样的操作则不会通过编译。

2.C++11对C++98中的右值进行了扩充。在C++11中右值又分为纯右值(prvalue,Pure Rvalue)和将亡值(xvalue,eXpiring Value)。其中纯右值的概念等同于我们在C++98标准中右值的概念,指的是临时变量和不跟对象关联的字面量值;将亡值则是C++11新增的跟右值引用相关的表达式,这样表达式通常是将要被移动的对象(移为他用),比如返回右值引用T&&的函数返回值、std::move的返回值,或者转换为T&&的类型转换函数的返回值。将亡值可以理解为通过“盗取”其他变量内存空间的方式获取到的值。在确保其他变量不再被使用、或即将被销毁时,通过“盗取”的方式可以避免内存空间的释放和分配,能够延长变量值的生命期。

3.左值引用就是对一个左值进行引用的类型。右值引用就是对一个右值进行引用的类型,事实上,由于右值通常不具有名字,我们也只能通过引用的方式找到它的存在。右值引用和左值引用都是属于引用类型。无论是声明一个左值引用还是右值引用,都必须立即进行初始化。而其原因可以理解为是引用类型本身自己并不拥有所绑定对象的内存,只是该对象的一个别名。左值引用是具名变量值的别名,而右值引用则是不具名(匿名)变量的别名。左值引用通常也不能绑定到右值,但常量左值引用是个“万能”的引用类型。它可以接受非常量左值、常量左值、右值对其进行初始化。不过常量左值所引用的右值在它的“余生”中只能是只读的。相对地,非常量左值只能接受非常量左值对其进行初始化。

4.右值引用通常不能绑定到任何的左值,要想绑定一个左值到右值引用,通常需要std::move()将左值强制转换为右值。

lsy注:推荐去看一下move源码,很巧妙的用法!

8、 总结左值和右值的概念

1.左值是可以放在赋值号左边可以被赋值的值;左值必须要在内存中有实体;

2.右值当在赋值号右边取出值赋给其他变量的值;右值可以在内存也可以在CPU寄存器。

3.一个对象被用作右值时,使用的是它的内容(值),被当作左值时,使用的是它的地址。

4.左值:指表达式结束后依然存在的持久对象,可以取地址,具名变量或对象 。

5.右值:表达式结束后就不再存在的临时对象,不可以取地址,没有名字。

9、 移动构造函数

1.我们用对象a初始化对象b后,对象a我们就不在使用了,但是对象a的空间还在呀(在析构之前),既然拷贝构造函数,实际上就是把a对象的内容复制一份到b中,那么为什么我们不能直接使用a的空间呢?这样就避免了新的空间的分配,大大降低了构造的成本。这就是移动构造函数设计的初衷;

2.拷贝构造函数中,对于指针,我们一定要采用深层复制,而移动构造函数中,对于指针,我们采用浅层复制。浅层复制之所以危险,是因为两个指针共同指向一片内存空间,若第一个指针将其释放,另一个指针的指向就不合法了。所以我们只要避免第一个指针释放空间就可以了。避免的方法就是将第一个指针(比如a->value)置为NULL,这样在调用析构函数的时候,由于有判断是否为NULL的语句,所以析构a的时候并不会回收a->value指向的空间;

3.C++引入了移动构造函数,专门处理这种,用a初始化b后,就将a析构的情况;

4.移动构造函数的参数和拷贝构造函数不同,拷贝构造函数的参数是一个左值引用,但是移动构造函数的初值是一个右值引用。意味着,移动构造函数的参数是一个右值或者将亡值的引用。也就是说,只用用一个右值,或者将亡值初始化另一个对象的时候,才会调用移动构造函数。而那个move语句,就是将一个左值变成一个将亡值。

5.与拷贝类似,移动也使用一个对象的值设置另一个对象的值。但是,又与拷贝不同的是,移动实现的是对象值真实的转移(源对象到目的对象):源对象将丢失其内容,其内容将被目的对象占有。移动操作的发生的时候,是当移动值的对象是未命名的对象的时候。这里未命名的对象就是那些临时变量,甚至都不会有名称。典型的未命名对象就是函数的返回值或者类型转换的对象。使用临时对象的值初始化另一个对象值,不会要求对对象的复制:因为临时对象不会有其它使用,因而,它的值可以被移动到目的对象。做到这些,就要使用移动构造函数和移动赋值:当使用一个临时变量对象进行构造初始化的时候,调用移动构造函数。类似的,使用未命名的变量的值赋给一个对象时,调用移动赋值操作;

Example6 (Example6&& x) : ptr(x.ptr)

    {

        x.ptr = nullptr;

    }

    // move assignment

    Example6& operator= (Example6&& x)

    {

        delete ptr;

        ptr = x.ptr;

        x.ptr=nullptr;

        return *this;

}

10、 C语言的编译链接过程?

源代码(.c)-->预处理(.i)-->编译(.s)-->优化-->汇编(.o)-->链接-->可执行文件

1、预处理

读取c源程序,对其中的伪指令(以#开头的指令)和特殊符号进行处理。包括宏定义替换、条件编译指令、头文件包含指令、特殊符号。预编译程序所完成的基本上是对源程序的“替代”工作。经过此种替代,生成一个没有宏定义、没有条件编译指令、没有特殊符号的输出文件。c文件预处理后生成.i,C++文件预处理后生成.ii。

2、编译阶段

编译程序所要作得工作就是通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码。生成.s文件

3、汇编过程

汇编过程实际上指把汇编语言代码翻译成目标机器指令的过程。对于被翻译系统处理的每一个C语言源程序,都将最终经过这一处理而得到相应的目标文件。目标文件中所存放的也就是与源程序等效的目标的机器语言代码。生成.o目标文件

4、链接阶段

链接程序的主要工作就是将有关的目标文件彼此相连接,也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够被操作系统装入执行的统一整体。

11、 vector与list的区别与应用?怎么找某vector或者list的倒数第二个元素

1.vector数据结构

vector和数组类似,拥有一段连续的内存空间,并且起始地址不变。因此能高效的进行随机存取,时间复杂度为o(1);但因为内存空间是连续的,所以在进行插入和删除操作时,会造成内存块的拷贝,时间复杂度为o(n)。另外,当数组中内存空间不够时,会重新申请一块内存空间并进行内存拷贝。连续存储结构:vector是可以实现动态增长的对象数组,支持对数组高效率的访问和在数组尾端的删除和插入操作,在中间和头部删除和插入相对不易,需要挪动大量的数据。它与数组最大的区别就是vector不需程序员自己去考虑容量问题,库里面本身已经实现了容量的动态增长,而数组需要程序员手动写入扩容函数进形扩容。

2.list数据结构

list是由双向链表实现的,因此内存空间是不连续的。只能通过指针访问数据,所以list的随机存取非常没有效率,时间复杂度为o(n);但由于链表的特点,能高效地进行插入和删除。非连续存储结构:list是一个双链表结构,支持对链表的双向遍历。每个节点包括三个信息:元素本身,指向前一个元素的节点(prev)和指向下一个元素的节点(next)。因此list可以高效率的对数据元素任意位置进行访问和插入删除等操作。由于涉及对额外指针的维护,所以开销比较大。

3、vector与list区别:

1vector的随机访问效率高,但在插入和删除时(不包括尾部)需要挪动数据,不易操作。

2list的访问要遍历整个链表,它的随机访问效率低。但对数据的插入和删除操作等都比较方便,改变指针的指向即可。list是单向的,vector是双向的。vector中的迭代器在使用后就失效了,而list的迭代器在使用之后还可以继续使用。

为什么链表插入复杂度不是O(n)而是O(1),不需要找到吗?

查找并不属于插入和删除操作范围内,你说的情况只能说查找元素的复杂度是O(n)。查找完元素之后才开始插入或删除,而由于链表本身的特性:插入时不用移动后续所有节点或者将整个链表拷贝到新的内存空间删除时可以用下一个节点覆盖当前节点,将问题转化成删除下一个节点所以插入和删除都是O(1)。

二十万字C/C++、嵌入式软开面试题全集宝典四_第4张图片

int mySize = vec.size();
vec.at(mySize -2);

list不提供随机访问,所以不能用下标直接访问到某个位置的元素,要访问list里的元素只能遍历,不过你要是只需要访问list的最后N个元素的话,可以用反向迭代器来遍历:

12、 STL vector的实现,删除其中的元素,迭代器如何变化?为什么是两倍扩容?释放空间?

1、size()函数返回的是已用空间大小,capacity()返回的是总空间大小,capacity()-size()则是剩余的可用空间大小。当size()和capacity()相等,说明vector目前的空间已被用完,如果再添加新元素,则会引起vector空间的动态增长。

2、由于动态增长会引起重新分配内存空间、拷贝原空间、释放原空间,这些过程会降低程序效率。因此,可以使用reserve(n)预先分配一块较大的指定大小的内存空间,这样当指定大小的内存空间未使用完时,是不会重新分配内存空间的,这样便提升了效率。只有当n>capacity()时,调用reserve(n)才会改变vector容量。resize()成员函数只改变元素的数目,不改变vector的容量。

3、resize()与reserve()

resize()设置大小(size)并初始化新元素;reserve(),设置容量(capacity),只是预留空间,而不会构造新的元素出来

1)空的vector对象,size()和capacity()都为0

2)当空间大小不足时,新分配的空间大小为原空间大小的2倍。

3)使用reserve()预先分配一块内存后,在空间未满的情况下,不会引起重新分配,从而提升了效率。

4)当reserve()分配的空间比原空间小时,是不会引起重新分配的。

5)resize()函数只改变容器的元素数目,未改变容器大小。

6)用reserve(size_type)只是扩大capacity值,这些内存空间可能还是“野”的,如果此时使用“[ ]”来访问,则可能会越界。而resize(size_type new_size)会真正使容器具有new_size个对象。

4、vector扩容方式

1)不同的编译器,vector有不同的扩容大小。在vs下是1.5倍,在GCC下是2倍;

2)空间和时间的权衡。简单来说,空间分配的多,平摊时间复杂度低,但浪费空间也多。

3)使用k=2增长因子的问题在于,每次扩展的新尺寸必然刚好大于之前分配的总和,也就是说,之前分配的内存空间不可能被使用。这样对内存不友好。最好把增长因子设为(1,2)

二十万字C/C++、嵌入式软开面试题全集宝典四_第5张图片

4)对比可以发现采用采用成倍方式扩容,可以保证常数的时间复杂度,而增加指定大小的容量只能达到O(n)的时间复杂度,因此,使用成倍的方式扩容。

5、如何释放空间:

由于vector的内存占用空间只增不减,比如你首先分配了10,000个字节,然后erase掉后面9,999个,留下一个有效元素,但是内存占用仍为10,000个。所有内存空间是在vector析构时候才能被系统回收。empty()用来检测容器是否为空的,clear()可以清空所有元素。但是即使clear(),vector所占用的内存空间依然如故,无法保证内存的回收。

如果需要空间动态缩小,可以考虑使用deque。如果vector,可以用swap()来帮助你释放内存。

vector(Vec).swap(Vec);//将Vec的内存空洞清除;
vector().swap(Vec);//清空Vec的内存;

13、 容器内部删除一个元素

1、顺序容器

erase迭代器不仅使所指向被删除的迭代器失效,而且使被删元素之后的所有迭代器失效(list除外),所以不能使用erase(it++)的方式,但是erase的返回值是下一个有效迭代器;

It = c.erase(it);

2、关联容器

erase迭代器只是被删除元素的迭代器失效,但是返回值是void,所以要采用erase(it++)的方式删除迭代器;

c.erase(it++)

14、 STL迭代器如何实现

1.迭代器是一种抽象的设计理念,通过迭代器可以在不了解容器内部原理的情况下遍历容器,除此之外,STL中迭代器一个最重要的作用就是作为容器与STL算法的粘合剂。

2.迭代器的作用就是提供一个遍历容器内部所有元素的接口,因此迭代器内部必须保存一个与容器相关联的指针,然后重载各种运算操作来遍历,其中最重要的是*运算符与->运算符,以及++、--等可能需要重载的运算符重载。这和C++中的智能指针很像,智能指针也是将一个指针封装,然后通过引用计数或是其他方法完成自动释放内存的功能。

3.最常用的迭代器的相应型别有五种:value type、difference type、pointer、reference、iterator catagoly;

15、 set与hash_set的区别

1.set底层是以RB-Tree实现,hash_set底层是以hash_table实现的;

2.RB-Tree有自动排序功能,而hash_table不具有自动排序功能;

3.set和hash_set元素的键值就是实值;

4.hash_table有一些无法处理的型别;

16、 hash_map与map的区别

1.底层实现不同, hash_map采用hash表存储,map一般采用红黑树(RB Tree)实现;

2.map具有自动排序的功能,hash_map不具有自动排序的功能;

3.hash_table有一些无法处理的型别;

17、 map、set是怎么实现的,红黑树是怎么能够同时实现这两种容器?为什么使用红黑树?

1.他们的底层都是以红黑树的结构实现,因此插入删除等操作都在O(logn)时间内完成,因此可以完成高效的插入删除;

2.在这里我们定义了一个模版参数,如果它是key那么它就是set,如果它是map,那么它就是map;底层是红黑树,实现map的红黑树的节点数据类型是key+value,而实现set的节点数据类型是value

3.因为map和set要求是自动排序的,红黑树能够实现这一功能,而且时间复杂度比较低。

18、 如何在共享内存上使用stl标准库?

1.想像一下把STL容器,例如map, vector, list等等,放入共享内存中,IPC一旦有了这些强大的通用数据结构做辅助,无疑进程间通信的能力一下子强大了很多。我们没必要再为共享内存设计其他额外的数据结构,另外,STL的高度可扩展性将为IPC所驱使。STL容器被良好的封装,默认情况下有它们自己的内存管理方案。当一个元素被插入到一个STL列表(list)中时,列表容器自动为其分配内存,保存数据。考虑到要将STL容器放到共享内存中,而容器却自己在堆上分配内存。一个最笨拙的办法是在堆上构造STL容器,然后把容器复制到共享内存,并且确保所有容器的内部分配的内存指向共享内存中的相应区域,这基本是个不可能完成的任务。

2.假设进程A在共享内存中放入了数个容器,进程B如何找到这些容器呢?一个方法就是进程A把容器放在共享内存中的确定地址上(fixed offsets),则进程B可以从该已知地址上获取容器。另外一个改进点的办法是,进程A先在共享内存某块确定地址上放置一个map容器,然后进程A再创建其他容器,然后给其取个名字和地址一并保存到这个map容器里。进程B知道如何获取该保存了地址映射的map容器,然后同样再根据名字取得其他容器的地址。

19、 map插入方式有几种?

1.用insert函数插入pair数据,

mapStudent.insert(pair<int, string>(1, "student_one"));

2.用insert函数插入value_type数据

mapStudent.insert(map<int,string>::value_type(1, "student_one"));

3.在insert函数中使用make_pair()函数

mapStudent.insert(make_pair(1, "student_one"));

4.用数组方式插入数据

mapStudent[1] = "student_one";

20、 STL中unordered_map(hash_map)和map的区别,hash_map如何解决冲突以及扩容

1.unordered_map和map类似,都是存储的key-value的值,可以通过key快速索引到value。不同的是unordered_map不会根据key的大小进行排序,

2.存储时是根据key的hash值判断元素是否相同,即unordered_map内部元素是无序的,而map中的元素是按照二叉搜索树存储,进行中序遍历会得到有序遍历。

3.所以使用时map的key需要定义operator<。而unordered_map需要定义hash_value函数并且重载operator==。但是很多系统内置的数据类型都自带这些,

4.那么如果是自定义类型,那么就需要自己重载operator<或者hash_value()了。

5.如果需要内部元素自动排序,使用map,不需要排序使用unordered_map

6.unordered_map的底层实现是hash_table;

7.hash_map底层使用的是hash_table,而hash_table使用的开链法进行冲突避免,所有hash_map采用开链法进行冲突解决。

8.什么时候扩容:当向容器添加元素的时候,会判断当前容器的元素个数,如果大于等于阈值---即当前数组的长度乘以加载因子的值的时候,就要自动扩容啦。

9.扩容(resize)就是重新计算容量,向HashMap对象里不停的添加元素,而HashMap对象内部的数组无法装载更多的元素时,对象就需要扩大数组的长度,以便能装入更多的元素。

你可能感兴趣的:(二十万字C/C++面试总结,C/C++知识点汇总,C/C++,嵌入式知识整理,c++,面试,算法)