1、STL中的迭代器失效的问题? 迭代器是不允许一边读一遍修改的(通过迭代器来遍历容器) 当通过迭代器插入一个元素,所有迭代器就都失效了 当通过迭代器删除一个元素,当前删除位置到后面所有的元素的迭代器就都失效了 当通过迭代器更新容器元素以后,要及时对迭代器进行更新, insert / erase方法都会返回新位置的迭代器
2、STL中哪些底层由红黑树实现? set multiset map multimap
3、struct和class的区别? 1.定义类的时候的区别:struct定义的类默认是公有的,class定义的类不写访问限定符是默认私有的。 2.继承时,派生类 class B : A 默认私有继承 struct B : A 默认是公有继承 3.struct 空结构体是0 struct 空类是1 4.对于C++11中 struct Data { int ma, int mb} Data data = {10, 20};//可以这样进行初始化 5.class在template< class T >还可以定义模板类型参数
4、vector和数组的区别,STL的容器分类,各容器底层实现? 直接使用数组, 数组的内存够不够用,是不是需要扩容?需要扩容代码怎么写?会和业务代码混杂一块。 vector是数组的一个面向对象的表示,把数组封装起来了。自动扩容,添加,删除,不用担心数组的内存和空间越界。 vector: 可扩容的数组 deque:动态开辟第二维的数组 list :双向循环链表 stack(依赖deque适配):容器适配器,没有自己的数据结构和迭代器 push pop queue(依赖deque的方法) priority_queue(优先级队列,大根堆,基于vector的) set/map(红黑树) unordered_set/unordered_map :(链式哈希表)
5、编译链接全过程? 预编译、编译、汇编 =》 二进制可重定向obj文件 *.o 链接:1.合并段 ,符号解析 2.符号的重定向 =》 可执行文件
6、初始化全局变量和未初始化全局变量有什么区别? .data(初始化,且初始值不为0) .bss(未初始化,初始化为0)
7、堆和栈的区别? 堆内存的大小远远大于栈内存 但是堆申请空间要malloc/new free/delete 手动的free,delete,否则内存泄漏 栈内存:函数的运行需要在栈上分配栈帧 函数的局部变量在栈上定义 函数出大括号运行完了之后系统自动回收函数的栈帧。 堆的分配是从低地址 =》 高地址 栈的分配是从高地址 =》 低地址
8、构造函数和析构函数可不可以是虚函数,为什么? 构造函数不能是虚函数 因为虚函数必须把函数地址放在虚表中,虚表是通过虚函数指针访问的,虚函数指针在对象里面,虚函数就是基类指针指向派生类对象,调用的是派生类的同名覆盖方法,当派生类对象构造的时候,要先构造基类,在基类调用基类的构造函数,基类的构造函数是虚函数,就调用派生类的构造函数了吗???不能!!!如果能,派生类从基类继承而来的成员就不能初始化了!! 而且构造函数在调用的时候对象还不存在!!!对象不存在,哪来的虚函数指针???没有虚函数指针怎么访问虚函数表???
析构函数可以是虚函数 基类指针指向堆上的派生类对象的时候,如果基类的析构函数是普通函数,delete p,p只是基类指针,最终只调用基类的析构函数,派生类的析构函数调用不了。所以,我们把基类的析构函数实现成虚析构函数。 Base *p = new Derive(); 把基类的析构函数是现成虚析构函数 delete p; // 对析构函数的调用进行动态绑定 ~Base() ~Derive() 动态绑定就是访问p指向的对象是Derive类型,就访问Derive的虚函数表,在Derive的虚函数表上放的是派生类的析构函数,所以派生类和基类的虚函数就可以调用到。
9、构造函数和析构函数中能不能抛出异常,为什么? 构造函数不能抛异常, 如果构造函数抛异常,说明对象创建失败,编译器就不会调用对象的析构函数了
析构函数不能抛异常,如果抛异常,后面的代码就无法得到执行了(资源泄漏),在最后一行抛异常就可以。 解决方法:把分配的堆内存用智能指针来代替 在构造函数写简单的,然后把可能抛异常的代码封装在init函数中。 init() 保证对象创建是成功的!
10、宏和内联函数的区别? 宏是#define,内联是inline 宏是预编译阶段(字符串替换) 处理的(不仅仅可以表示一个常量,一个表达式,一组代码,一个函数)
内联是编译阶段(在函数调用点,通过函数的实参把函数代码直接展开调用,节省了函数的调用开销)处理的(只是修饰函数的)
对于调试,宏是没有办法调试的, inline函数可以调试(debug版本下inline就和普通函数一样,有标准的函数调用过程)
#define 可以定义常量,代码块,函数块 缺点是不好维护 而且宏定义代码多了,每一行后面要加个斜杆,而且斜杆后面是不能空格的。 inline只是修饰函数
11、局部变量存在哪里? 在栈stack上。 局部变量通过 ebp指针偏移来访问的,不产生符号,所以局部变量不能叫做数据,属于指令的一部分 int a = 10; => mov dword ptr[ebp-4], 0Ah
12、拷贝构造函数,为什么传引用而不传值?
class Test
{
public:
Test(const Test &t);
}
Test t1;
Test t2(t1);
如果拷贝构造函数,传值的话,形参的产生本身就要调用自己,调用自己就得先产生形参,这是不行的,有矛盾的,直接产生编译错误。
13、内联函数和普通函数的区别(从反汇编角度来回答)? 核心:函数的调用开销! push ebp (压栈) mov ebp, esp (esp赋个ebp) sub esp, 4Ch (给当前函数开辟栈帧) rep stos 0xCCCCCCCC(windows) (Windows给初始化) 在GCC(gcc/g++)中:分配完栈帧, 不做任何栈初始化动作
释放栈 mov esp, ebp (让esp从栈顶指向栈底) pop ebp (让ebp指向调用方函数的栈底) ret(把当前栈顶的下一行指令地址放到CPU的pc寄存器,相当于从当前函数运行完了之后转去调用方的函数–调用点的下一行指令开始执行。
14、如何实现一个不可以被继承的类? 派生类的初始化过程: 先是基类构造 =》 然后是派生类构造 我们把 基类的构造函数私有化 ,就不可以被派生类继承了,私有的派生类访问不了。
15、什么是纯虚函数?为什么要有纯虚函数?虚函数表放在哪里的? virtual void func() = 0; 是纯虚函数 =》 抽象类(不能实例化对象的,但是可以定义指针和引用)
纯虚函数一般定义在基类里面,基类不代表任务实体,它的主要作用之一就是给所有的派生类保留统一的纯虚函数接口,让派生类进行重写,方便的使用多态机制。因为基类不需要实例化,它的方法也就不指导该怎么去实现!当然只有派生类才知道怎么去实现。
虚函数表 在 编译阶段产生的! 虚函数表运行时,虚函数表加载到.rodata段(只读数据段)
16、说一下C++中的const,const与static的区别? const定义的叫常量,它的编译方式是:编译过程中,把出现常量名字的地方,用常量的值进行替换 如果是a是常量,const a=10; 然后指针p取a的地址解引用赋值,cout<<a都是输出10,和a没关系,就是10替换了a了 常变量,不能作为左值,其他都是和普通变量一样。
const还可以定义常成员方法, Test *this => const Test *this 这样,普通对象和常对象就都可以调用了!
const和static的区别: 面向过程: const只能修饰:全局变量,局部变量,形参变量 static只能修饰:全局变量,局部变量
面向对象: const:常方法/成员变量 Test *this => const Test *this 依赖对象 static:静态方法/成员变量 Test *this => this指针没有了! 不依赖于对象 通过类作用域访问
17、四种强制类型转换? const_cast (去掉常量属性) static_cast (类型安全的转换,编译器认为类型安全,就转换,认为不安全,就不转换,不同类型的指针转换,C++编译器不允许,认为不完全,报错。但是在C中可以任意类型指针转换)
reinterpret_cast: C风格的类型转换,没有任何安全可言 dynamic_cast:支持RTTI信息识别的类型转换 ,基类指针转成相应的派生类的指针时,它会识别你这个基类指针是不是指向相应派生类对象。
18、详细解释deque的底层原理 deque底层是动态开辟的二维数组 定义的2个宏:(不同的VS,g++,都有更改值) #define MAP_SIZE 2 #define QUE_SIZE(T) 4096/sizeof(T)
一维数组的初始大小 MAP_SIZE (T*) 第二维数组默认开辟的大小就是QUE_SIZE(int) 1024 deque是双端队列 两端都有队头和队尾 两端都可以插入删除 时间复杂度是O(1) 当第一维所有的位置都开辟了第二维,再增加元素的话就要扩容 扩容:把第一维数组按照2倍的方式进行扩容 2-4-8-16。。。 扩容以后,会把原来的第二维的数组,从新一维数组的第oldsize / 2 开始存放 也就是说,当它从2 扩容到 4的时候,在新一维数组的第 4/2 = 2处开始存放原来的第二维数组。 扩容之后,就是把中间2行放满,然后上下都留1行,为了以后头尾都要插入 stack queue =》 底层都是依赖deque(deque内存利用率好,第二维是分段连续的, 刚开始就有一段内存可以供使用) priority_queue(优先级队列,构建大根堆,需要内存都是连续的,才能通过下标计算左孩子右孩子和父节点的关系):底层是基于vector
19、虚函数,多态 一个类 如果有虚函数就要在编译阶段给该类产生一张虚函数表 =》 虚函数表运行时,加载到.rodata段
当用指针或者引用 调用 虚函数时,首先通过指针或者引用访问对象的头四个字节vfptr =》再去相应的vftable中取虚函数的地址,进行动态绑定调用
多态:设计函数接口的时候,可以都是用基类的指针或者引用来接收不同的派生类对象, 设计函数接口的时候,可以都使用基类的指针或者引用来接收不同派生类对象的同名覆盖方法,当我们进行功能增加或者删除的时候,函数接口不需要任何改变。参考OOP设计模式(高内聚,低耦合,软件设计的开闭原则,原有的接口不做任何改变,就可以支持任意功能的增加和减少)。
20、虚析构函数、智能指针 Base *p = new Derive(); delete p; // 析构函数的调用,动态绑定
智能指针:自动释放资源,自动管理资源的生命周期
21、一个类,写了一个构造函数,还写了一个虚构造函数,可不可以,会发生什么? 在构造函数中,是不会进行动态绑定的!构造函数本身也不能实现成虚函数!
22、异常机制怎么回事儿?
try
{
可能会抛出异常的代码 throw
}
catch(const string &err)
{
捕获相应异常类型对象,进行处理,完成后,代码继续向下运行
}
好处是:当前代码抛出异常,如果在当前函数栈帧没有找到相应的catch块,就会这个异常抛给调用方函数,调用方函数依然是这样处理的,如果有处理异常的catch代码块,就向下运行,如果没有,就继续抛给调用方,直到到main函数还没有处理再抛给系统,系统发现有异常没有处理,就中止了。 如果在函数上有找到catch块,就继续运行。
异常的栈展开! 异常的好处是可以把代码中所有的异常抛到统一的地方进行处理(比如说抛到main函数中统一处理)! exit(0);
23、早绑定和晚绑定? 早绑定(静态绑定,编译时期的绑定):普通函数的调用,用对象调用虚函数 ,call 编译阶段已经知道调用哪个函数
函数调用的地方打断点,转到反汇编,如果是call一个函数,就是静态绑定,如果是call一个寄存器,就是动态绑定。
晚绑定(动态绑定):用指针/引用调用虚函数的时候,都是动态绑定 p->vfptr->vftable->取virtual addr 把addr放到寄存器 =>然后 call eax 在编译阶段,是不知道最终调用哪个虚函数的。 最终从虚函数表取出的是哪个虚函数的地址,调用的就是哪个虚函数
24、指针和引用的区别(反汇编分析)
int a = 10;
int *p = &a; lea eax, [a] mov dword ptr[ebp-8], eax
int &b = a; lea eax, [a] mov dword ptr[ebp-0Ch], eax
*p = 20; mov eax, dword ptr[ebp-8] mov dword ptr[eax], 14H
b = 20; mov eax, dword ptr[ebp-0Ch] mov dword ptr[eax], 14H
引用是更安全的指针,因为引用是需要初始化的。 使用指针,是不是野指针?从编码的正确性保障。
25、智能指针交叉引用问题怎么解决? 定义对象的时候用强智能指针shared_ptr,而引用对象的时候用弱智能指针weak_ptr, 当通过weak_ptr访问对象成员时,需要先调用weak_ptr的lock提升方法,把weak_ptr提升成shared_ptr强智能指针,再进行对象成员调用。
26、重载的底层实现,虚函数的底层实现 重载,因为C++生成函数符号,是依赖函数名字+参数列表 编译到函数调用点时,根据函数名字和传入的实参(个数和类型),和某一个函数重载匹配的话,那么就直接调用相应的函数重载版本(静态的多态 都是在编译阶段处理的!)
虚函数 =》 对象 指针/引用调用虚函数的时候(动态绑定) =》 通过vfptr => 访问vftable => 取虚函数的地址 进行call eax的调用
27、讲一下map的底层实现,avl和rbtree有什么区别? map底层实现:红黑树 avl(是平衡树 每个节点的左右子树高度差不会超过1 增删查的效率好,为了维护平衡 引入了旋转,元素量大,旋转的次数比较多,在百万数据中,效率比红黑树逊色 rbtree(不是一颗平衡树,左右子树,长的不能超过短的2倍,红黑树的性质决定的)
28、假如map的键是类类型,那么map底层是如何调整的? map底层是红黑树 存储[key,value]键值对,对key进行比较的, 如果key是类类型,map默认用的是 less 比较小于,less< key 类类型 要提供operator<运算符的重载函数
29、问题:内存泄漏你会怎么处理?讲讲智能指针 怎么定位内存泄漏的问题,通过工具进行检测,比如说在VS上,通过添加vld,在运行的过程检查可能出现内存泄漏的问题,会指到相应的代码上。
为了防止内存泄漏,我们做好使用智能指针来智能管理资源
30、如果让你实现一个内存池,要求获取资源和插入资源时间花费O(1),你会怎么设计? SGI STL二级空间配置器的内存池的实现就可以了!!!(类似于哈希表)
31、编写一个C/C++程序你个人感觉需要注意一些什么? 分析问题的需求 分析完,进行概要设计和详细设计 分成哪些模块,模块和模块的共性地方 设计基础的类,函数接口,考虑全面,让程序的扩展性更好。 考虑线程安全的问题。 多线程,考虑代码的可移植性,调用系统的线程接口还是语言级别的线程类 设计模式
32、C++中vector和list的区别,stack和queue的底层实现,智能指针,C++11特性,迭代器失效的原因以及如何解决? stack和queue的底层实现:容器适配器,底层没有实现任何数据结构,底层直接依赖一个现有的顺序容器,依赖的是deque,第二维数组独立new出来,不需要连续,内存碎片比较多的情况下内存分配是容易成功的。 vector的初始内存使用效率太低了,因为默认定义一个vector,它是从0开始,0到1,1到2,2到4,4到8,才慢慢分配起来。deque的第二维一下子就可以分配一块可用的空间,初始的内存使用效率不错的。
33、如果构造函数里面抛出异常会发生什么?内存泄漏?怎么解决? 对象没有构造成功,析构函数就不会调用,析构函数写的那些释放的代码就都不会得到执行,就是资源泄漏,内存泄漏是资源泄漏的一种。
怎么解决? 采用智能指针(保证:只要开辟成功的资源,智能指针对象析构的时候一定会去释放资源)。
|