1.c++有哪些容器,介绍一下。说说你对STL的了解,等等。
1.序列式:vector(动态数组,当前大小到达数组容量时进行2倍扩容,以此实现动态数组的效果)、list(双向链表)、
deque(双端队列)(将固定大小的多个数组用指针连接,以此实现首位插入和删除的功能)
2.关联式:(无序)unordered_map/set (底层是hash) ,特点是查找的时间复杂度是O(1)
(有序)map/set (底层是红黑树)
引申:为什么是红黑树不是平衡二叉树
红黑树和AVL树都是高效的平衡二叉树,增删改查的时间复杂度都是O(log N),红黑树不追求绝对平衡,其只需保证最长路径不超过最短路径的2倍,相对而言,降低了插入和旋转的次数,所以在经常进行增删的结构中性能比AVL树更优,而且红黑树实现比较简单,所以实际运用中红黑树更多。
set与map底层实现有什么不同?
底层实现都是红黑树,但是
map和set区别在于:
- map中的元素是key-value(关键字—值)对:关键字起到索引的作用,值则表示与索引相关联的数据;Set与之相对就是关键字的简单集合,set中每个元素只包含一个关键字。(或者可以理解为关键字和值是相同的值)
- set的迭代器是const的,不允许修改元素的值(关键字的值当然不能修改,只能通过删除再插入来实现,因为可能会涉及到重新排序)map允许修改value,但不允许修改key。其原因是因为map和set是根据关键字排序来保证其有序性的,如果允许修改key的话,那么首先需要删除该键,然后调节平衡,再插入修改后的键值,调节平衡,如此一来,严重破坏了map和set的结构,导致iterator失效,不知道应该指向改变前的位置,还是指向改变后的位置。所以STL中将set的迭代器设置成const,不允许修改迭代器的值;而map的迭代器则不允许修改key值,允许修改value值。
- map支持下标操作,set不支持下标操作。map可以用key做下标,map的下标运算符
[]
将关键码作为下标去执行查找,如果关键码不存在,则插入一个具有该关键码和mapped_type类型默认值的元素至map中,因此下标运算符[ ]
在map应用中需要慎用,const_map不能用,只希望确定某一个关键值是否存在而不希望插入元素时也不应该使用,mapped_type类型没有默认值也不应该使用。如果find能解决需要,尽可能用find。
2.虚函数的作用:
主要是为了实现运行时的多态,通过虚函数表、虚函数指针的方式实现不同的对象调用同名的函数实现不同的功能。
每个包含虚函数的类都会在内存中有虚函数表,对于子类来说,如果父类中的虚函数没有被自己重写,那么子类的虚函数表中存放的就是父类的虚函数指针,否则就是子类重写过得。这样就能知道不同类的对象应该调用什么函数,实现对应的功能了。
3.如何判断链表有环:
最优解是快慢指针,时间O(n),空间O(1)
次解是用一个map或者set记录访问过得节点,当遍历的过程中发现又遍历到访问过的节点则有环 时间O(n),空间O(n)
4.实现字符串的拼接,或者说字符串的拼接的底层原理是什么:
第一次申请的时候就申请一块大一些的内存,比如初始化的字符串长度为10,那我就申请一块大小为20的内存,如果这块内存用完了而字符串还需要扩展,那我就去找一块更大的内存,能够同时容纳需要的内存空间,而且还有一些余量,直接将字符串整体迁移到新内存空间中,放弃原来那部分内存空间,这样就实现了字符串的内存连续。
这个原理倒是很像动态数组。
5.linux中的gdb是什么:
1.什么是GDB,能干啥?
gdb是GNU开源组织发布的一个强大的Linux下的程序调试工具。
一般来说,GDB主要帮助你完成下面四个方面的功能:
1、启动你的程序,可以按照你的自定义的要求随心所欲的运行程序。
2、可让被调试的程序在你所指定的调置的断点处停住。(断点可以是条件表达式) 可以在指定的行数,指定的函数和一些指定的条件下设置断点,并用命令print输出变量的状态
3、当程序被停住时,可以检查此时你的程序中所发生的事。
4、你可以改变你的程序,将一个BUG产生的影响修正从而测试其他BUG。
6.项目中遇到难点和问题是怎么解决的?
考察一个人克服困难的能力吧,包括发现问题、分析问题、解决问题,是否会通过相关书籍、搜索引擎来查找相关资料,是否会去相关论坛、身边人求助,以及解决问题后是否会总结记录等等。。
在面试过程中碰到过很多次,这个问题的回答可以看出很多与技术本身无关的素养,比如:你的分析以及钻研问题的能力,你的处事方式等等……
答法示例:从难度或者不同角度着手着手: 1编程或者技术实现维度的:简答的代码bug这种问题,简单的自己调试就能解决,或者可能通过csdn,github等网站就能轻易解决,这就不用说了;
当然也有可能碰到难以解决的代码bug,譬如之前做项目的过程中使用了一个开源的点云库遇到一个内存泄露的问题,调试的时候看结果都很正常,因为这个变量是个智能指针,网上查看他人博客并没有发现类似的问题,最后还是去看了相关函数的源码才发现还需要主动释放变量使用的内存。
一般来说遇到的比较困难的问题主要有涉及技术方案底层的数学原理,因为我之前参与的项目很多是负责核心技术方案的开发,会遇到当前技术方案达不到预期效果的情况,这些都涉及到一些数学原理,这个时候就需要通过阅读论文或者相关书籍去解决或者优化。
2.项目进行过程中与他人的相处问题,因为项目往往是多人分模块做的,大家进度不一样,难度可能也不一样,大家的做事的积极性也有高低,这时候就会遇到问题。
7.C++main函数执行前后会有什么?
c++在main函数运行前,需要进行一些操作,主要是初始化系统的相关资源
1. 设置栈指针
2. 初始化static静态和global全局变量,即data段的内容
3. 将未初始化部分的全局变量赋初值:数值型short,int,long等为0,bool为FALSE,指针为NULL,等等,即.bss段的内容
4. 全局对象初始化,在main之前调用构造函数
5. 将main函数的参数,argc,argv等传递给main函数,然后才真正运行main函数
设置栈指针:
为栈分配相关的位置,用来放一些局部变量和其他数据
初始化静态和全局变量:
把全局和静态变量初始化,放在相应的位置
将未初始化的全局变量赋初值:
将未设置初值的全局变量赋初值
全局对象初始化:
在main之前调用构造函数
传值给main函数:
argc为整数
argv为指针的指针
在main函数执行之后会执行onexit()函数,主要功能是对全局对象和全局变量,静态变量的析构。
8.内存分区管理部分,五大区域
在C++中,内存分成5个区,由高地址到低地址依次是:
-
下面分别解释各段:
BSS段:用来存放程序中未初始化的全局变量和静态变量(初始化分为显式和隐式初始化,未初始化指程序员不初始化的话,自动初始化为0。)不占磁盘空间,只在运行在再用内存空间间。
数据段:数据段(data segment)通常是指用来存放程序中已初始化的全局变量和静态变量的一块内存区域。数据段属于静态内存分配,可以分为只读数据段和读写数据段。 字符串常量等,但一般都是放在只读数据段中。
代码段:代码段(code segment/text segment)通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读, 某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等,但一般都是放在只读数据段中 。
堆(heap):堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减) 。
栈 (stack):栈又称堆栈, 是用户存放程序临时创建的局部变量,也就是说我们函数括弧“{}”中定义的变量(但不包括static声明的变量,static意味着在数据段中存放变 量)。除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。由于栈的先进后出特点,所以 栈特别方便用来保存/恢复调用现场。从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。注意:栈空间是向下增长的,每个线程有一个自己的栈,在linux上默认的大小是8M,可以用ulimit查看和修改。内存分配方式
- 静态存储区域分配:内存在程序编译的时候已经分配好,这块内存在程序的整个运行空间都存在(全局变量,static变量)
- 在栈上创建:在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
- 从堆上分配,亦称动态内存分配。程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多。
引申问题: 堆和栈的区别:
栈,存放函数局部变量、参数等。函数调用时创建,函数结束时释放。
堆,一般由new分配的内存块,不会自动释放,一般一个new就要对应一个delete。在整个程序结束后,操作系统会自动回收。
对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆 来说,释放工作由程序员控制,容易产生memory leak。(不及时释放内存会产生内存溢出)
一般来讲在32位系统下,堆内存可以达到4G的空间,从这个角 度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间 大小的,栈空间大小基本M为单位。
对于堆来讲,频繁的new/delete势必会造成内存空间的不连续, 从而造成大量的碎片,使程序效率降低。(处理器是有专门的寄存器等硬件支持栈的,而堆只是代码上的支持 ,栈理论上更快。)对于栈来讲,则不会存在这个问题,因 为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内 存块从栈中间弹出,在他弹出之前,在他上面的后进的栈内容已经被弹出,详细 的可以参考数据结构,这里我们就不再一一讨论了。
对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方 向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。
堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静 态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配 由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由 编译器进行释放,无需我们手工实现。
栈是机器系统提供的数据结构,计算机会在底层对栈提供支持: 分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈 的效率比较高。堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分 配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系 统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是
由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这 样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。
从这里我们可以看到,堆和栈相比,由于大量new/delete的使用,容易造 成大量的内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态 和核心态的切换,内存的申请,代价变得更加昂贵。所以栈在程序中是应用最广 泛的,就算是函数的调用也利用栈去完成,函数调用过程中的参数,返回地址, EBP和局部变量都采用栈的方式存放。所以,我们推荐大家尽量用栈,而不是用 堆。
虽然栈有如此众多的好处,但是由于和堆相比不是那么灵活,有时候分配大 量的内存空间,还是用堆好一些。
9.手写C++的string的赋值和拷贝构造函数
已知类String的原型为:
class String
{
public:
String(const char *str = NULL); // 普通构造函数
String(const String &other); // 拷贝构造函数
~ String(void); // 析构函数
String & operator =(const String &other); // 赋值函数
private:
char *m_data; // 用于保存字符串
};
请编写 String 的上述 4 个函数。 解题示例:
class testString
{
public:
testString(const char* s = nullptr)//普通构造函数,根据字符指针创建字符串
{
if (s == nullptr)//为空
{
data = new char[1];
data[0] = '\0';
}
else
{
data = new char[strlen(s)+1];
strcpy(data, s);
}
}
testString(const testString& ts)//拷贝构造函数,深拷贝自己
{
data = new char[strlen(ts.data) + 1];
strcpy(data, ts.data);
}
~testString(void)//析构,
{
delete[] data;
}
testString & operator =(const testString & ts)//赋值函数
{
if (this == &ts)//避免自己赋值给自己
{
return *this;
}
else
{
delete[] data;//先清空,避免内存泄露,再赋值
data = new char[strlen(ts.data) + 1];
strcpy(data, ts.data);
return *this;
}
}
private:
char* data;
};
下面摘抄一段对这道题的评语:
能够准确无误地编写出String类的构造函数、拷贝构造函数、赋值函数和析构函数的面试者至少已经具备了C++基本功的60%以上!在这个类中包括了指针类成员变量m_data,当类中包括指针类成员变量时,一定要重载其拷贝构造函数、赋值函数和析构函数,这既是对C++程序员的基本要求,也是《Effective C++》中特别强调的条款。(这是因为不重载的话系统默认是浅拷贝,很可能出现对同一个地址的内容删除多次的操作,导致bug,所以必须重载拷贝构造函数、赋值函数和析构函数,避免问题的出现)。仔细学习这个类,特别注意加注释的得分点和加分点的意义,这样就具备了60%以上的C++基本功!
还有一个需要注意的是strcpy()函数本身是不够安全的
看看MSDN怎么说:
strcpy
原型:char *strcpy(char *dest,char *src);
用法:#include <string.h>
功能:把src所指由NULL结束的字符串复制到dest所指的数组中。
说明:src和dest所指内存区域不可以重叠且dest必须有足够的空间来容纳src的字符串。
返回指向dest的指针。
strcpy只是复制字符串,但不限制复制的数量。很容易造成缓冲溢出,也就是说,不过dest有没有足够的空间来容纳src的字符串,它都会把src指向的字符串全部复制到从dest开始的内存,这就造成了拷贝溢出的问题,所以后面出现了更安全的strcpy_s( char *strDestination, size_t numberOfElements, const char *strSource ); 的第二个参数. 原来第二个参数是指元素的个数.而非字节数..