c++和数据结构

1、new和malloc有什么区别?

  • new分配内存按照数据类型进行分配,malloc分配内存按照指定的大小分配;
  • new返回的是指定对象的指针,而malloc返回的是void*,因此malloc的返回值一般都需要进行类型转化。
  • new不仅分配一段内存,而且会调用构造函数,malloc不会。
  • new分配的内存要用delete销毁,malloc要用free来销毁;delete销毁的时候会调用对象的析构函数,而free则不会。
  • new是一个操作符可以重载,malloc是一个库函数。
  • malloc分配的内存不够的时候,可以用realloc扩容。扩容的原理?new没用这样操作。
  • new如果分配失败了会抛出bad_malloc的异常,而malloc失败了会返回NULL。
  • 申请数组时: new[]一次分配所有内存,多次调用构造函数,搭配使用delete[],delete[]多次调用析构函数,销毁数组中的每个对象。而malloc则只能sizeof(int) * n。

2、map的底层实现及其相关:

map由红黑树实现,有专门的键值比较函数;unordered_map是由哈希表实现的。
红黑树存储结构的存取是O(logn),而哈希表是O(1),当然这是在哈希表没有冲突的情况下的,但实际的hashmap和unordered_map是不允许冲突的。而红黑树的内存占用要比哈希表高。

3、继承和组合的优缺点:

继承优点

  • 支持扩展,通过继承父类实现,但会使系统结构较复杂
  • 易于修改被复用的代码

缺点

  • 代码白盒复用,父类的实现细节暴露给子类,破坏了封装性
  • 当父类的实现代码修改时,可能使得子类也不得不修改,增加维护难度。
  • 子类缺乏独立性,依赖于父类,耦合度较高
  • 不支持动态拓展,在编译期就决定了父类

组合优点

  • 代码黑盒复用,被包括的对象内部实现细节对外不可见,封装性好。
  • 整体类与局部类之间松耦合,相互独立。
  • 支持扩展。
  • 每个类只专注于一项任务。
  • 支持动态扩展,可在运行时根据具体对象选择不同类型的组合对象(扩展性比继承好)。

缺点:

  • 创建整体类对象时,需要创建所有局部类对象。导致系统对象很多。

4、拷贝构造函数和赋值运算符区别:

  • 拷贝构造函数是一个对象初始化一块内存区域,这块内存就是新对象的内存区,而赋值函数是对于一个已经被初始化的对象来进行赋值操作。
  • 一般来说在数据成员包含指针对象的时候,需要考虑两种不同的处理需求:一种是复制指针对象,另一种是引用指针对象。拷贝构造函数大多数情况下是复制,而赋值函数是引用对象。
  • 实现不一样。拷贝构造函数首先是一个构造函数,它调用时候是通过参数的对象初始化产生一个对象。赋值函数则是把一个新的对象赋值给一个原有的对象,所以如果原来的对象中有内存分配要先把内存释放掉,而且还要检察一下两个对象是不是同一个对象,如果是,不做任何操作,直接返回。

5、char和varchar区别:

  • char 表示定长,长度固定,varchar表示变长,即长度可变。char如果插入的长度小于定义长度时,则用空格填充;varchar小于定义长度时,还是按实际长度存储,插入多长就存多长。
  • 对 char 来说,最多能存放的字符个数 255,和编码无关。而 varchar 呢,最多能存放 65532 个字符。varchar的最大有效长度由最大行大小和使用的字符集确定。整体最大长度是 65,532字节。

6、排序稳定性及时间复杂度

在这里插入图片描述参考:https://www.cnblogs.com/lxy-xf/p/11321536.html

7、变量的存放位置

  • 局部变量存储在栈中
  • 全局变量、静态变量(全局和局部静态变量)存储在静态存储区
  • new申请的内存是在堆中
  • 字符串常量也是存储在静态存储区
    补充说明:
  • 栈中的变量内存会随着定义所在区间的结束自动释放;而对于堆,需要手动free,否则它就一直存在,直到程序结束;
  • 对于静态存储区,其中的变量常量在程序运行期间会一直存在,不会释放,且变量常量在其中只有一份拷贝,不会出现相同的变量和常量的不同拷贝。

8、布隆过滤器:

本质上布隆过滤器是一种数据结构,比较巧妙的概率型数据结构(probabilistic data structure),特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”。布隆过滤器本质上是一个 bit 向量或者说 bit 数组。
可参考:https://www.jianshu.com/p/2104d11ee0a2

9、实现不定长结构体:

可在结构体中定义一个长度为0的字符串数组,此时,在结构体中实际上只包含了c的地址,这样就可以直接将结构体后面的数据当成字符串数组c的内容。

10、什么函数不能声明为虚函数

(1)普通函数不能声明为虚函数。普通函数(非成员函数)只能被重载(overload),不能被重写(override),声明为虚函数也没有什么意思,因此编译器会在编译时绑定函数。
(2) 构造函数不能声明为虚函数。构造函数一般用来初始化对象,只有在一个对象生成之后,才能发挥多态作用。如果将构造函数声明为虚函数,则表现为在对象还没有生成的时候来定义它的多态,这两点是不统一的。另外,构造函数不能被继承,因而不能声明为虚函数。
(3) 静态成员函数不能声明为虚函数。静态成员函数对于每个类来说只有一份代码,所有的对象都共享这份代码,它不归某个对象所有,所以也没有动态绑定的必要性。
(4) 内联(inline)成员函数不能声明为虚函数。内联函数就是为了在代码中直接展开,减少函数调用开销的代价。虚函数是为了在继承后对象能够准确的执行自己的动作,这是不可能统一的。另外,内联函数在编译时被展开,虚函数在运行时才能动态的绑定函数。
(5) 友元函数不能声明为虚函数。友元函数不属于类的成员函数,不能被继承。

11、红黑树构造过程

可参考https://blog.csdn.net/Lyt15829797751/article/details/81054920

12、虚析构函数的作用:

当基类中的析构函数声明为虚析构函数时,派生类开始从基类继承,基类的指针指向派生类的对象时,delete基类的指针时,先调用派生类的析构函数,再调用基类中的析构函数。

13、隐藏和重写

对于隐藏,它的出现是在父子类拥有相同的成员(成员函数,成员属性)的时候,此时父类的成员就会被隐藏起来(还存在),子类的成员得到访问或调用。
对于重写(覆盖),它是指在父类中有一个虚函数,然后子类重写出一个和它形式(函数名,参数列表,返回值)完全相同的一个虚函数。

14、debug和release区别

debug为调试版本,其中包括了出错时能够定位源代码的在行,如果源文件已经改变,定位出来会有偏移,而且,在这个版本中编译器不会进行代码优化,并且在程序中能用宏定义_DEBUG来确定当前的版本。release为正试版本,程序出错只是进行简单的错误处理,编译器会优化代码,以提高性能。Release代码更小,执行更快,编译更严格,更慢。
ASSERT在Release版本中是不会被编译的。
debug中会自动给变量初始化,而在release版中则不会。

15、结构体比较相等,为什么字节对齐

通过运算符重载比较相等。不能用memcmp,因为结构体可能有字节对齐,作用是为了加快CPU的速度。

15、头文件能同时调用c和c++

  • C++调用C的函数比较简单,直接使用extern “C” {}告诉编译器用C的规则去调用C函数就可以了。不用extern C编译出来的函数代码不一样。
  • 用于C++代码中,修饰函数定义。这使得在其他C编译单元中可以调用该函数,在函数定义中可以使用C++语法、标准库等。如:extern “C” void handler(int) {}

17、stl内存分配,碎片处理

  • 内存管理工作是通过STL提供的一个默认的allocator实现的。
  • allocator是一个由两级分配器构成的内存管理器,当申请的内存大小大于128byte时,就启动第一级分配器通过malloc直接向系统的堆空间分配,如果申请的内存大小小于128byte时,就启动第二级分配器,从一个预先分配好的内存池中取一块内存交付给用户,这个内存池由16个不同大小(8的倍数,8~128byte)的空闲列表组成,allocator会根据申请内存的大小(将这个大小round up成8的倍数)从对应的空闲块列表取表头块给用户。
    可参考https://www.cnblogs.com/zsychanpin/p/6936810.html

18、linux删除同一文件夹下所有满足条件的文件

find . -name 'test*' | xargs rm -rf

19、cmake和makefile区别

  • gcc是GNU Compiler Collection(就是GNU编译器套件),也可以简单认为是编译器,它可以编译很多种编程语言(括C、C++、Objective-C、Fortran、Java等等)
  • make工具就根据makefile中的命令进行编译和链接的,包含了调用gcc(也可以是别的编译器)去编译某个源文件的命令。
  • cmake就可以更加简单的生成makefile文件给上面那个make用。当然cmake还有其他功能,就是可以跨平台生成对应平台能用的makefile。
  • cmake根据什么生成makefile呢?它又要根据一个叫CMakeLists.txt文件(学名:组态档)去生成makefile。

20、智能指针:

STL 一共给我们提供了四种智能指针:auto_ptr、unique_ptr、shared_ptr 和 weak_ptr,auto_ptr 是 C++98 提供的解决方案,C+11 已将其摒弃,并提出了 unique_ptr 作为 auto_ptr 替代方案。

21、git pull和git fetch的区别

https://blog.csdn.net/weixin_41975655/article/details/82887273

22、重载和重写的区别

  • 重载 Overload:表示同一个类中可以有多个名称相同的方法,但这些方法的参数列表各不相同(即参数个数或类型不同)。
  • 重写 Override:表示子类中的方法可以与父类中的某个方法的名称和参数完全相同,通过子类创建的实例对象调用这个方法时,将调用子类中的定义方法,这相当于把父类中定义的那个完全相同的方法给覆盖了,这也是面向对象编程的多态性的一种表现。

23、数据结构实现浏览器前进后退:采用两个栈实现。

可参考 https://www.jianshu.com/p/0d5a9f76e1aa

24、hashmap为什么线程不安全

扩容时多线程操作会造成链表循环
可参考https://blog.csdn.net/loveliness_peri/article/details/81092360

25、指针和引用的区别

  • 指针是一个实体,而引用仅是个别名;

  • 引用使用时无需解引用(*),指针需要解引用;

  • 引用只能在定义时被初始化一次,之后不可变;指针可变;引用“从一而终”;

  • 引用没有 const,指针有 const,const 的指针不可变;

  • 引用不能为空,指针可以为空;

  • “sizeof 引用”得到的是所指向的变量(对象)的大小,而“sizeof 指针”得到的是指针本身(所指向的变量或对象的地址)的大小;

  • typeid(T) == typeid(T&) 恒为真,sizeof(T) == sizeof(T&)恒为真,但是当引用作为成员时,其占用空间与指针相同(没找到标准的规定)。

  • 指针和引用的自增(++)运算意义不一样;

26、std::vector::shrink_to_fit

作用:请求容器降低其容量和size匹配,vector删除元素可能会造成size很小,cap很大的情况,这是就需要收缩。可以新建一个小的vector再swap。
stl中不会自动对vector大小做一个缩容。

27、抽象工厂模式及与工厂模式的区别

工厂模式是用来创建同一个产品的不同类型的(大肉包和牛肉包它们都是包子的不同类型展示),但是抽象工厂模式是用来创建不同类的产品,比如包子店还卖豆浆油条。一般来说,产品种类单一,适合用工厂模式;如果有多个种类,各种类型时,通过抽象工厂模式来进行创建是很合适的。
参考:https://blog.csdn.net/Olive_ZT/article/details/78861388?utm_source=distribute.pc_relevant.none-task

28、物理地址和逻辑地址怎么样转换

存储方式有三种段式存储、页式存储、段页式存储。
物理地址=块号+页内地址
逻辑地址=页号+页内地址

29、B树和B+树区别:

B树:每个节点都存储key和data,所有节点组成这棵树,并且叶子节点指针为null。
B+树:只有叶子节点存储data,叶子节点包含了这棵树的所有键值,叶子节点不存储指针。

30、const在成员函数前后的区别

  • const在函数后面表示函数不可以修改这个类的成员变量
  • const在函数前面用于描述返回值,表示返回一个常量

31、只能在堆或栈定义的类

  • 只能在堆定义:把析构函数设置为私有
  • 只能在栈定义:把new重载为私有

32、虚函数调用如下图所示

在这里插入图片描述

33、浅拷贝和深拷贝

浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。

34、C++内存管理

在C++中,虚拟内存分为代码段、数据段、BSS段、堆区、文件映射区以及栈区六部分。

  • 代码段:包括只读存储区和文本区,其中只读存储区存储字符串常量,文本区存储程序的机器代码。
  • 数据段:存储程序中已初始化的全局变量和静态变量
  • bss 段:存储未初始化的全局变量和静态变量(局部+全局),以及所有被初始化为0的全局变量和静态变量。
  • 堆区:调用new/malloc函数时在堆区动态分配内存,同时需要调用delete/free来手动释放申请的内存。
  • 映射区:存储动态链接库以及调用mmap函数进行的文件映射
  • 栈:使用栈空间存储函数的返回地址、参数、局部变量、返回值

35、虚表指针的初始化时间

虚表指针的初始化确实发生在构造函数的调用过程中, 但是在执行构造函数体之前,即进入到构造函数的"{“和”}"之前。

36、能不能修改代码段的内容

防止程序指令被修改,设置代码段权限为只读,设置数据段权限为可读写

37、unique_ptr的实现原理

我们可以在类中把拷贝构造函数和拷贝赋值声明为private,这样就不可以对指针指向进行拷贝了,也就不能产生指向同一个对象的指针。

38、brk、mmap、sbrk

brk:属于系统调用。malloc小于128k的内存,使用brk分配内存,将数据段(.data)的最高地址指针_edata往高地址推(只分配虚拟空间,不对应物理内存(因此没有初始化),第一次读/写数据时,引起内核缺页中断,内核才分配对应的物理内存,然后虚拟地址空间建立映射关系)
mmap:malloc大于128k的内存,使用mmap分配内存,在堆和栈之间找一块空闲内存分配(对应独立内存,而且初始化为0)
sbrk:不是系统调用,是C库函数。
参考

39、智能指针是否存在内存泄漏,如何解决

两个share_ptr相互引用导致循环而无法释放导致内存泄漏。可使用weak_ptr。

40、C语言函数参数压栈顺序

C语言函数参数压栈顺序是从右到左。
原因:printf(const char* format,…)是一个不定参函数,那么我们在实际使用中是怎么样知道它的参数个数呢?这就要靠format了,编译器通过format中的%占位符的个数来确定参数的个数。我们假设参数的压栈顺序是从左到右的,这时,函数调用的时候,format最先进栈,之后是各个参数进栈,最后pc进栈,此时,由于format先进栈了,上面压着未知个数的参数,想要知道参数的个数,必须找到format,而要找到format,必须要知道参数的个数,这样就陷入了一个无法求解的死循环了。
参考

41、虚继承

  • 多继承时很容易产生命名冲突,即使我们很小心地将所有类中的成员变量和成员函数都命名为不同的名字,命名冲突依然有可能发生,比如典型的是菱形继承。如类
    A 有一个成员变量 a,那么在类 D 中直接访问 a 就会产生歧义,编译器不知道它究竟来自 A -->B–>D 这条路径,还是来自
    A–>C–>D 这条路径。
  • 为了解决多继承时的命名冲突和冗余数据问题,C++ 提出了虚继承,使得在派生类中只保留一份间接基类的成员。在继承方式前面加上 virtual 关键字就是虚继承。

42、在类的成员函数中调用delete this

  • 调用delete只是告诉系统我们不需要这个对象的内存空间了,请求释放它,但并不会主动帮我们把这个指针置为null,它依然指向原来的内存地址。
  • 当我们是用A a = A()这样产生一个局部对象的时候,由于这个对象是被压入我们的函数栈当中,因此当你delete这个对象的指针,它也还是会存在这个函数栈当中,只有但函数栈回收的时候才会回收这个对象,而对于A *a = new A()这样的情况,a指针对应的对象是被分配到堆当中的,当我们delete的时候,系统就可以马上回收这个堆的内容并可能对堆的内容分布做一些优化和调整,这个时候对象对应的内容也就不存在了。

43、C++:如何判断类中是否存在特定的成员函数

个人的理解是用模板结构体SFINAE,参数包括T以及函数指针 。然后定义两个重载函数test,一个参数是带需要判断的成员函数加 * 的,另一个是不带参数。如果含有该方法,则(1)和(2)两个test()方法将都会在编译期生成,但test(T*)最匹配SFINAE<C, &C::foo> * 的结果,所以这时会调用(1)这个test(T*),返回true。如果不含有该方法,则只有(2)这个test()方法会在编译期生成, 于是调用(2)后返回false。
参考

44、构造函数的几种形式

  • 默认构造函数
  • 初始化构造函数
  • 拷贝构造函数
  • 转换构造函数

45、父子类内存关系

内存中先存储4个字节的虚表指针,再是父类的变量成员,子类的变量成员。
类B继承类A的内存分布如图:
在这里插入图片描述
遇到多个父类的情况下, 内存中先存储第一个父类的成员变量,其次是第二个,一直到第N个,最后才保存子类的成员变量。
虚继承的内存分布情况如下图所示:其中d继承bc、bc虚继承a。
在这里插入图片描述
参考

46、函数调用堆栈情况

在这里插入图片描述
参考

47、构造函数的几种形式

默认、初始、拷贝、转换

48、share_ptr线程安全问题

智能指针包括一个实际数据指针和一个引用计数指针,这两个操作不是一个指令可以完成的,因此多线程环境下,可能会出现问题。
但本身计数值是原子操作的。
参考

49、返回值不一样,形参一样能重载吗

参数不同返回值不可以重载,因为重载必须改变参数列表(否则,虚拟机怎么知道该调用哪一个方法)

50、获取文件大小

保存当前指针,fseek将文件指针指向文件尾,用ftell获得当前指针距离文件头的距离,恢复文件指针。参考

51、右值引用、move、完美转发

  • 左值、右值:C++中所有的值都必然属于左值、右值二者之一。左值是指表达式结束后依然存在的持久化对象,右值是指表达式结束时就不再存在的临时对象。所有的具名变量或者对象都是左值,而右值不具名。很难得到左值和右值的真正定义,但是有一个可以区分左值和右值的便捷方法:看能不能对表达式取地址,如果能,则为左值,否则为右值。
  • 右值引用:右值本来在表达式语句结束后,其生命也就该终结了(因为是临时变量),而通过右值引用,该右值又重获新生,其生命期将与右值引用类型变量a的生命期一样,只要a还活着,该右值临时变量将会一直存活下去。实际上就是给那个临时变量取了个名字。
  • 移动构造函数与拷贝构造函数的区别:拷贝构造的参数是const MyString& str,是常量左值引用,而移动构造的参数是MyString&& str,是右值引用,而MyString(“hello”)是个临时对象,是个右值,优先进入移动构造函数而不是拷贝构造函数。而移动构造函数与拷贝构造不同,它并不是重新分配一块新的空间,将要拷贝的对象复制过来,而是"偷"了过来,将自己的指针指向别人的资源,然后将别人的指针修改为nullptr,这一步很重要,如果不将别人的指针修改为空,那么临时对象析构的时候就会释放掉这个资源,"偷"也白偷了。
  • 对于一个左值,肯定是调用拷贝构造函数了,但是有些左值是局部变量,生命周期也很短,能不能也移动而不是拷贝呢?C++11为了解决这个问题,提供了std::move()方法来将左值转换为右值,从而方便应用移动语义。我觉得它其实就是告诉编译器,虽然我是一个左值,但是不要对我用拷贝构造函数,而是用移动构造函数吧。。
  • 当右值引用和模板结合的时候,就复杂了。T&&并不一定表示右值引用,它可能是个左值引用又可能是个右值引用。参考

52、空类sizeof的大小

空类对象的sizeof 大小为1,空结构体对象的sizeof 大小也为1。那是被编译器插进去的一个char ,使得这个class的不同实体(object)在内存中配置独一无二的地址。

53、可变参数的使用

使用va_list系api,求平均数的例子如下:

double averge(int num, ...) {
    va_list valist;
    double sum;
    va_start(valist, num);
    for (int i = 0; i < num; i++) {
        sum = sum + va_arg(valist, int);//参数2说明返回的类型为int
    }
    va_end(valist);
    return sum / num;
}

54、浮点数的存储

NUM = (-1) ^ S * M * 2 ^ E;//(S表示符号,E表示阶乘,M表示有效数字)
在这里插入图片描述

55、b+树的插入删除在这里插入图片描述在这里插入图片描述

参考

56、胜者树和败者树

  • 胜者树和败者树都是完全二叉树,是树形选择排序的一种变型。每个叶子结点相当于一个选手,每个中间结点相当于一场比赛,每一层相当于一轮比赛。
  • 不同的是,胜者树的中间结点记录的是胜者的标号;而败者树的中间结点记录的败者的标号。
  • 胜者树与败者树可以在log(n)的时间内找到最值。任何一个叶子结点的值改变后,利用中间结点的信息,还是能够快速地找到最值。在k路归并排序中经常用到。
  • 参考

57、智能指针实现

智能指针类将一个计数器与类指向的对象相关联,引用计数跟踪该类有多少个对象共享同一指针。每次创建类的新对象时,初始化指针并将引用计数置为1;
当对象作为另一对象的副本而创建时,拷贝构造函数拷贝指针并增加与之相应的引用计数;对一个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数(如果引用计数为减至0,则删除对象),并增加右操作数所指对象的引用计数;
调用析构函数时,构造函数减少引用计数(如果引用计数减至0,则删除基础对象)。智能指针就是模拟指针动作的类。所有的智能指针都会重载 -> 和 * 操作符。智能指针还有许多其他功能,比较有用的是自动销毁。这主要是利用栈对象的有限作用域以及临时对象(有限作用域实现)析构函数释放内存。
weak_ptr和share_ptr有不同的计数器,当两个都为0是才释放计数器结构体,指向同一个对象的智能指针共用一个计数器结构体参考

58、lambda实现

编译器器会把⼀一个lambda表达式⽣生成⼀一个匿匿名类的匿匿名对象,并在类中重载函数调⽤用运算符:

auto add = [](int a, int b){return a + b; };
//翻译成如下类:重载函数调⽤用运算符
class add_class
{
   public:
    auto operator()(int a, int b) const
    {
        return a + b;
    }
};
auto add = add_class(a,b);

优点:1. 简洁。2. 非常容易并行计算。3. 可能代表未来的编程趋势。
缺点:1. 若不用并行计算,很多时候计算速度没有比传统的 for 循环快。(并行计算有时需要预热才显示出效率优势)2. 不容易调试。3. 若其他程序员没有学过 lambda 表达式,代码不容易让其他语言的程序员看懂。

59、C++空类的成员函数

6个:默认构造函数、拷贝构造函数、析构函数、赋值函数、取值运算符、取值运算符const。

60、程序中typename的作用

class MyArray      
{      
publictypedef   int   LengthType;
   .....
}

template<class T>
void MyMethod( T myarr ) 
{          
    typedef typename T::LengthType LengthType;        
    LengthType length = myarr.GetLength; 
}

typename的作用就是告诉c++编译器,typename后面的字符串为一个类型名称,而不是成员函数或者成员变量,这个时候如果前面没有typename,编译器没有任何办法知道T::LengthType是一个类型还是一个成员名称(静态数据成员或者静态函数),所以编译不能够通过

61、public继承、protected继承和private继承的区别

  • 公有继承(public) 公有继承的特点是基类的公有成员和保护成员作为派生类的成员时,它们都保持原有的状态,而基类的私有成员仍然是私有的,不能被这个派生类的子类所访问。
  • 私有继承(private) 私有继承的特点是基类的公有成员和保护成员都作为派生类的私有成员,并且不能被这个派生类的子类所访问。
  • 保护继承(protected) 保护继承的特点是基类的所有公有成员和保护成员都成为派生类的保护成员,并且只能被它的派生类成员函数或友元访问,基类的私有成员仍然是私有。

62、string字符存放位置

string未定义时只有一个指针,定义时才赋值。
ubuntu64中:数据<=16字节,在当前栈区;数据>16字节,在堆区。

63、尾递归

  • 在传统的递归中,典型的模型是首先执行递归调用,然后获取递归调用的返回值并计算结果。以这种方式,在每次递归调用返回之前,您不会得到计算结果。
  • 在尾递归中,首先执行计算,然后执行递归调用,将当前步骤的结果传递给下一个递归步骤。这导致最后一个语句采用的形式(return (recursive-function params))。基本上,任何给定递归步骤的返回值与下一个递归调用的返回值相同。
  • 当编译器检测到一个函数调用是尾递归的时候,它就覆盖当前的活动记录而不是在栈中去创建一个新的。编译器可以做到这点,因为递归调用是当前活跃期内最后一条待执行的语句,于是当这个调用返回时栈帧中并没有其他事情可做,因此也就没有保存栈帧的必要了。通过覆盖当前的栈帧而不是在其之上重新添加一个,这样所使用的栈空间就大大缩减了,这使得实际的运行效率会变得更高。
    参考

64、sizeof一个vector对象

和数组大小无关,主要是class里面的内容,数组是在堆中分配的。class主要包括一个分配器指针以及三个迭代器,所以是16个字节,参考

65、delete[]怎么知道要调用多少次析构函数?

Array* p4 = new Array[10];
delete[] p4;

执行这两条语句的时候实际上调用operator new [] (10*sizeof(Array)+4)分配大小为10 * sizeof(Array)+4空间,其中多的四个字节空间用于存放N(10)这个数字以便于delete中调用析构函数析构对象(调用析构函数的次数),空间申请好了之后调用构造函数创建对象。delete[] p4执行的时候首先取N(10)对象个数,然后调用析构函数析构对象,最后用operator delete[]函数释放空间。

66、override final关键字

  • final关键字是用来修饰一个函数,防止这个函数被子类重写。
  • 我们确认自己目前在子类中正在重写一个来自父类的函数,那么我们最好是用override关键字来修饰该函数,override修饰的函数表示这个函数一定是父类(祖先)中传下来的。

67、decltype

  • 选择并返回操作数的数据类型。编译器分析表达式并得到它的类型,却不实际计算表达式的值。
  • 当调用的是函数时,编译器并不实际调用函数,而是使用当调用发生时函数的返回值类型作为定义参数的类型。
  • 与auto的区别:可以对一个表达式求类型。

68、vector push_back时间复杂度

当执行 push_back 操作,该 vector 需要分配更多空间时,它的容量(capacity)会增大到原来的 m 倍。​现在我们来均摊分析方法来计算 push_back 操作的时间复杂度。
假定有 n 个元素,倍增因子为 m。那么完成这 n 个元素往一个 vector 中的push_back​操作,需要重新分配内存的次数大约为 logm(n),第 i 次重新分配将会导致复制 m^i (也就是当前的vector.size() 大小)个旧空间中元素,因此 n 次 push_back 操作所花费的总时间约为 n*m/(m - 1):
在这里插入图片描述
很明显这是一个等比数列.那么 n 个元素,n 次操作,每一次操作需要花费时间为 m / (m - 1),这是一个常量。

69、构造函数里必须初始化什么数据

引用,const,父类构造函数需要的值,成员类函数需要的值?

70、禁止类的复制方法

可将复制构造函数和赋值运算符放在类的private部分,c++11可在函数后加delete起到同样的效果。关键字delete可用于禁止编译器使用特定方法。

71、STL迭代器失效的情况和原因

迭代器失效分三种情况考虑,也是分三种数据结构考虑,分别为数组型,链表型,树型数据结构。
1、数组型数据结构
该数据结构的元素是分配在连续的内存中,insert和erase操作,都会使得删除点和插入点之后的元素挪位置,所以,插入点和删除掉之后的迭代器全部失效,也就是说insert(*iter)(或erase(*iter)),然后在iter++,是没有意义的。
解决方法:erase(*iter)的返回值是下一个有效迭代器的值。

iter = cont.erase(iter);
//不要直接在循环条件中写++iter
for (iter = cont.begin(); iter != cont.end();)
{
   (*it)->doSomething();
   if (shouldDelete(*iter))
      iter = cont.erase(iter);  //erase删除元素,返回下一个迭代器
   else
      ++iter;
}

对于vector而言

  • 当vector在插入的时候,如果原来的空间不够,会将申请新的内存并将原来的元素移动到新的内存,此时指向原内存地址的迭代器就失效了,first和end迭代器都失效
  • 当vector在插入的时候,end迭代器肯定会失效
  • 当vector在删除的时候,被删除元素以及它后面的所有元素迭代器都失效。
    2、链表型数据结构
    对于list型的数据结构,使用了不连续分配的内存,插入不会使得任何迭代器失效,删除运算使指向删除位置的迭代器失效,但是不会失效其他迭代器.
    解决办法两种,erase(*iter)会返回下一个有效迭代器的值,或者erase(iter++).
    3、树形数据结构
    使用红黑树来存储数据,插入不会使得任何迭代器失效
    删除运算使指向删除位置的迭代器失效,但是不会失效其他迭代器.erase迭代器只是被删元素的迭代器失效,但是返回值为void,所以要采用erase(iter++)的方式删除迭代器。

注意 :经过erase(iter)之后的迭代器完全失效,该迭代器iter不能参与任何运算,包括iter++,*ite

72、NULL和nullptr

  • 在C语言中,NULL通常被定义为:#define NULL ((void * )0)
  • C++是强类型语言,void*是不能隐式转换成其他类型的指针的,所以c++中: #define NULL 0
  • 为解决NULL代指空指针存在的二义性问题,在C++11版本中特意引入了nullptr这一新的关键字来代指空指针

73、哈希表两种解决冲突方式的优缺点

(1)拉链法
1)优点:
①对于记录总数频繁可变的情况,处理的比较好(也就是避免了动态调整的开销)
②由于记录存储在结点中,而结点是动态分配,不会造成内存的浪费,所以尤其适合那种记录本身尺寸(size)很大的情况,因为此时指针的开销可以忽略不计了
③删除记录时,比较方便,直接通过指针操作即可
2)缺点:
①存储的记录是随机分布在内存中的,这样在查询记录时,相比结构紧凑的数据类型(比如数组),哈希表的跳转访问会带来额外的时间开销
②如果所有的 key-value 对是可以提前预知,并之后不会发生变化时(即不允许插入和删除),可以人为创建一个不会产生冲突的完美哈希函数(perfect hash function),此时封闭散列的性能将远高于开放散列
③由于使用指针,记录不容易进行序列化(serialize)操作(是将数据结构或对象状态转换为可存储格式的过程)
(2)开放地址法
1)优点:
①记录更容易进行序列化(serialize)操作
②如果记录总数可以预知,可以创建完美哈希函数,此时处理数据的效率是非常高的
2)缺点:
①存储记录的数目不能超过桶数组的长度,如果超过就需要扩容,而扩容会导致某次操作的时间成本飙升,这在实时或者交互式应用中可能会是一个严重的缺陷
②使用探测序列,有可能其计算的时间成本过高,导致哈希表的处理性能降低
③由于记录是存放在桶数组中的,而桶数组必然存在空槽,所以当记录本身尺寸(size)很大并且记录总数规模很大时,空槽占用的空间会导致明显的内存浪费
④删除记录时,比较麻烦。比如需要删除记录a,记录b是在a之后插入桶数组的,但是和记录a有冲突,是通过探测序列再次跳转找到的地址,所以如果直接删除a,a的位置变为空槽,而空槽是查询记录失败的终止条件,这样会导致记录b在a的位置重新插入数据前不可见,所以不能直接删除a,而是设置删除标记。这就需要额外的空间和操作。

74、堆和栈那个快

栈快。

  • 栈是由CPU提供指令支持的, 在指令的处理速度上, 对栈数据进行处理的速度自然要优于由操作系统支持的堆数据 。
  • 栈是和代码段一同被载入到CPU内存中的,同一个函数栈上的变量会被保存在寄存器中的,·而堆上的内存由于和函数栈不在同一个地址段,所以堆上的内存很有可能不在寄存器或者CUP缓存中,访问命中率就低,内存需要从硬盘到内存到CUP缓存再到寄存器。
  • 栈是在一级缓存中做缓存的, 而堆则是在二级缓存中, 两者在硬件性能上差异巨大。

75、构造函数调用虚函数

  • 一般情况下,不允许在构造函数或者析构函数中调用虚函数。其实语法上都没有问题,只是会失去多态性。
  • 如果在构造函数中调用虚函数,会先调用父类中的实现,也就失去了多态的性质。
  • 如果在析构函数中调用虚函数,也是同样的失去了多态性,参考

76、静态绑定和动态绑定

  1. 静态绑定发生在编译期,动态绑定发生在运行期;
  2. 对象的动态类型可以更改,但是静态类型无法更改;
  3. 要想实现动态,必须使用动态绑定;
  4. 在继承体系中只有虚函数使用的是动态绑定,其他的全部是静态绑定;

77、一些操作的运行时间

#define :预处理时展开
inline:编译时展开
typedef:编译时处理的,它是在自己的作用域内给已经存在的类型一个别名
模板:在声明的地方对模板代码本身进行编译,这次编译只会进行一个语法检查,并不会生成具体的代码。在运行时对代码进行参数替换后再进行编译,生成具体的函数代码。

78、定义和声明的区别

  • 声明是告诉编译器变量的类型和名字,不会为变量分配空间
  • 定义就是对这个变量和函数进行内存分配和初始化。需要分配空间,同一个变量可以被声明多次,但是只能被定义一次。

79、静态成员函数和非静态成员函数的区别

  • 静态成员函数没有this指针。
  • 静态成员函数只能访问静态成员变量。
  • 静态成员是可以独立访问的,也就是说,无须创建任何对象实例就可以访问。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值