秋招——语言篇(C++)

1、C++语言基础

1.1 简述下 C++ 语言的特点

  1. C++C 语言基础上引入了面向对象的概念,并且兼容C语言
  2. C++ 有三大特性:(1) 封装 (2) 继承 (3) 多态
  3. C++程序可读性好,通过引入模板的概念,可复用性
  4. C++ 更加安全,增加了 const 常量、引用、四类 cast 转换(static_cast、dynamic_cast、const_cast、reinterpret_cast)、智能指针、try–catch等等
  5. C++ 是不断在发展的语言,如 C++11 中引入了 nullptr、auto变量、Lambda 匿名函数、右值引用、智能指针

1.2 说说 C 语言和 C++ 的区别

  1. CC++ 的一个子集,C++ 兼容 C 语言,C++ 引入了许多新的特性
  2. C++ 是面向对象的,C 语言是面向过程的
  3. C 有一些不安全的语言特性,如指针使用的潜在危险、强制转换的不确定性、内存泄漏等。而 C++ 对此增加了不少新特性来改善安全性,如 const 常量、引用、cast 转换、智能指针、try–catch 等等
  4. C++ 可复用性高,引入了模板的概念

1.3 说说 C++ 中 struct 和 class 的区别

  1. 封装对象:struct 是一个数据结构集合,class 是一个对象数据的封装。
  2. 默认的访问控制权限:struct 默认是 public,class 默认是 private
  3. 默认继承关系:struct 默认是公有继承,class 默认是私有继承
  4. 是否能用于定义模板参数:class 可以,struct 不可以

1.4 说说 include 头文件的顺序以及双引号""和尖括号<>的区别

  1. 区别:
    1. 尖括号<>的头文件是系统文件,双引号""的头文件是自定义文件
    2. 编译器预处理阶段查找头文件的路径不一样
  2. 查找路径
    1. <>的查找路径:编译器设置的头文件路径 --> 系统变量
    2. ""的查找路径:当前头文件目录 --> 编译器设置的头文件路径 --> 系统变量

1.5 C++ 结构体和 C 结构体的区别(C++ 和 C 中 struct 的区别)

  1. C++ 中的 struct 是对 C 中的 struct 进行了扩充,它们在声明时的区别如下:

    C C++
    成员函数 不能有 可以
    静态成员 不能有 可以
    访问控制 默认public,不能修改 public/private/protected
    继承关系 不可以继承 可从类或者其他结构体继承
    初始化 不能直接初始化数据成员 可以
  2. C++ 中使用 struct 关键字时可以省略 struct 关键字直接使用。

1.6 导入 C 函数的关键字是什么?C++ 编译时和 C 有什么不同?

  1. 关键字:在 C++ 中,导入 C 函数的关键字是 extern,表现形式为 extern "C"
  2. 编译区别:由于 C++ 支持函数重载,因此编译器编译的过程中不仅会把函数名加到编译后的代码,还会加入函数的参数类型。C 语言不支持函数重载,编译后的代码只包含函数名。

1.6.1 扩展:宏 _cplusplus 有什么作用?

这个就是用来标识是 C++ 程序还是 C 语言程序,因为 C++ 和 C 语言的程序去调用同一个 C 文件的时候, C++ 需要加入 extern 关键字,而 C 语言不需要,通过这个宏就可以实现当 C++ 时加入 extern "C",C 语言时不会。

#ifdef __cplusplus
extern "C"{
    
    
#endif    
   ....(函数区域);
    例如:
       	int add(int,int);
    	int sub(int,int);
#ifdef __cplusplus
}
#endif

1.7 简述 C++ 从代码到可执行二进制文件的过程

预处理 --> 编译 --> 汇编 --> 链接

  • 预处理:主要是处理一下宏定义,过滤所有的注释,处理条件预编译指令,添加行号和文件名标识
  • 编译:主要是进行语法和语义分析,目标代码优化和目标代码生成
  • 汇编:将汇编代码转变成机器可以执行的指令
  • 链接:将不同的目标文件链接起来,形成一个可执行的程序

1.7.1 静态链接和动态链接

  1. 静态链接:
    • 描述: 静态链接,是在链接的时候就已经把要调用的函数或者过程链接到了生成的可执行文件中
    • 生成文件类型: Windows 下以 .lib为后缀,Linux 下以 .a 为后缀。
    • 优点:(1):加载速度快。(2):发布程序无需提供静态库,移植方便。
    • 缺点:(1):消耗系统资源,浪费内存。(2):更新、部署麻烦。
  2. 动态链接:
    • 描述: 动态链接,在连接的时候没有把调用的函数代码链接进去(只包含函数的重定位信息),而是在执行的过程中,通过函数的重定位信息找到要链接的函数。
    • 生成文件类型: Windows 下以 .dll为后缀,Linux 下以 .so 为后缀。
    • 优点:(1):可以实现进程间资源共享(共享库)。(2):更新、部署简单。(3):可以控制何时加载动态库。
    • 缺点:(1):加载速度比静态库慢。(2):发布程序时需要提供依赖的动态库。

1.8 说说 static 关键字的作用

  1. 定义全局静态变量和局部静态变量
  2. 定义静态函数
  3. 在 C++ 中,static 关键字可以用于定义类中的静态成员变量。
  4. 在 C++ 中,static 关键字可以用于定义类中的静态成员函数。

1.8.1 关于静态成员函数的问题(面试题常考)

当调用一个对象的非静态成员函数时,系统会把该对象的起始地址赋给成员函数的 this 指针。而静态成员函数不属于任何一个对象,因此 C++ 规定静态成员函数没有 this 指针(划重点,面试题常考)。既然它没有指向某一个对象,也就无法对一个对象中的非静态成员进行访问

1.9 全局和静态变量什么时候初始化?

  • C: 全局和静态变量的初始化发生在任何代码执行之前,属于编译器初始化。
  • C++: 全局和静态变量当且仅当对象首次用到时才进行构造。

1.9.1 静态变量的相关属性

  1. 作用域: C++ 作用域有6种:全局、局部、类、语句、命名空间和文件作用域。
    • 静态全局变量:全局作用域+文件作用域,无法在其他文件中使用。
    • 静态局部变量:局部作用域,只被初始化一次,直到程序结束
    • 类静态成员变量:类作用域。
  2. 所在空间: 静态存储区(.bss未初始化的全局变量)
  3. 生命周期: 静态全局变量、静态局部变量都在静态存储区,直到程序结束后才会回收内存。类静态成员变量在静态存储区,当超出类作用域时回收。

1.20 一个空类占多少个字节?

占 1 个字节,系统默认

1.21 explict 的作用

explict 的作用是防止构造函数发生隐式转换

1.22 constexpr和 const 的区别

constexpr 表示这玩意儿在编译期就可以算出来(前提是为了算出它所依赖的东西也是在编译器可以算出来的)。而 const 只保证了运行时不直接被修改(但这个东西仍然可能是个动态变量)

----------关于指针的相关问题------------

1.23 数组和指针的区别

  1. 概念:
    • 数组: 数组是用于储存多个相同类型数据的集合,数组名是首元素的地址。
    • 指针: 指针相当于一个变量,存放的是其他变量在内存中的地址
  2. 区别:
    • 赋值: 同类型指针变量可以相互赋值;数组不行,只能一个一个元素的赋值或拷贝。
    • 存储方式: 数组在内存中连续存放,存放在静态区或者上。指针很灵活,存储空间不能确定。
    • 求sizeof: 数组:sizeof(数组名) / sizeof(数据类型)。指针:32位平台是4字节,64位平台是8字节。

1.24 说说什么是函数指针,如何定义函数指针,有什么使用场景?

  • 概念:函数指针就是指向函数的指针变量。
  • 定义:
    int func(int a); 
    int (*f)(int a); 
    f = &func;
    
  • 应用场景: 回调函数(callback)

1.25 什么是野指针,怎么产生的,如何避免?

  1. 概念: 野指针就是指针指向的位置是不可知的。
  2. 产生原因:
    • 声明指针的时候没有初始化。
    • 释放内存后指针不及时置空。
  3. 避免方法
    • 声明指针时要初始化
    • 申请内存后判空
    • 指针释放后置 NULL
    • 使用智能指针

1.26 C++ 中函数指针和指针函数的区别

指针函数 函数指针
定义不同 指针函数本质是一个函数,其返回值为指针 函数指针本质是一个指针,指向的对象是一个函数
写法不同 int * fun(int x,int y) int (*fun)(int x,int y)

1.27 说说const int *a, int const *a, const int a, int *const a, const int *const a分别是什么,有什么特点。

主要根据 const 修饰的是谁(const 在 * 还是变量的左边)

const int a;	// 指的是a是一个常量,不允许修改。
const int *a;	// a指针所指向的内存里的值不变,即(*a)不变
int const *a;	// 同 const int *a;	指向常量的指针
int *const a;	//a指针所指向的内存地址不变,即a不变,	常量指针
const int *const a;	//都不变,即(*a)不变,a也不变

1.28 指针需要注意什么?

  1. 初始化置空
  2. 申请内存后判空,防止使用指针值为 NULL 的内存
  3. 内存释放后,指针置 NULL
  4. 动态内存的申请与释放必须配对,防止内存泄漏

1.29 const * 和 * const的区别

// const * 是常量指针,* const 是指针常量,指向常量的指针

int const *a;    // a指针所指向的内存里的值不变,即(*a)不变
int *const a;    // a指针所指向的内存地址不变,即a不变

1.30 区别以下指针类型(来源于拓扑阿秀)

int *p[10]
int (*p)[10]
int *p(int)
int (*p)(int)
  • int *p[10] 表示指针数组,强调数组概念,是一个数组变量,数组大小为 10,数组内每个元素都是指向 int 类型的指针
  • int (*p)[10] 表示数组指针,强调的是指针,只有一个指针变量,这个指针指向的是一个大小为 10 的 int 类型的数组
  • int *p(int) 是指针函数,函数名是 p,参数是 int 类型,返回值是 int * 类型的
  • int (*p)(int) 函数指针,强调是指针,指向的函数是具有 int 类型参数,并且返回值是 int 类型的。

1.31 . 和 -> 的区别

  1. 相同点: 两者都是二元操作符,而且右边的操作数都是成员的名称。
  2. 不同点:
    • 点运算符(.)的左边操作数是一个结果为结构体表达式(对象,变量);
    • 箭头运算符(->)的左边操作数是一个指向结构体指针

----------------------------------------------------------------

1.32 nullptr 调用成员函数可以吗?为什么?


原因: 因为在编译时对象就绑定了函数地址。和指针空不空没关系。但是调用的函数如果用到this,因为 this = nullptr,就会运行出错。

1.33 静态局部变量、全局变量、局部变量的特点,以及使用场景

  1. 从作用域考虑: C++ 作用域分为 6 种:全局、局部、类、语句、命名空间和文件作用域
    • 全局变量: 全局作用域,可以通过 extern 作用于其他非定义的源文件
    • 静态全局变量: 全局作用域 + 文件作用域,所以无法在其他文件中使用。
    • 局部变量: 局部作用域,比如函数的参数,函数内的局部变量等等。
    • 静态局部变量: 局部作用域,只被初始化一次,直到程序结束。
  2. 从所在空间考虑:
    • 局部变量:在栈空间上。
    • 其余变量:在静态存储区上
  3. 生命周期:
    • 局部变量:局部变量在栈上,出了作用域就回收内存。
    • 其余变量:在静态存储区,所以直到程序结束才会回收内存。
  4. 使用场景: 根据描述就可知

1.34 说说内联函数和宏函数的区别

区别:

  1. 概述以及本质:
    • 宏函数不是函数,但是使用起来像函数。预处理器用复制宏代码的方式代替函数的调用,省去了函数压栈退栈过程,提高了效率。
    • 内联函数本质上是一个函数,内联函数一般用于函数体的代码比较简单的函数,不能包含复杂的控制语句,while、switch,并且内联函数本身不能直接调用自身。
  2. 替换时机:
    • 宏函数是在预处理的时候把所有的宏名用宏体来替换,简单的就是字符串替换。
    • 内联函数是在编译的时候进行代码插入,编译器会在每处调用内联函数的地方直接把内联函数的内容展开,这样可以省去函数的调用的开销,提高效率。
  3. 类型检查:宏定义没有类型检查,内联函数会在编译的时候进行类型的检查。
  4. 使用:
    // 宏定义示例
    #define MAX(a, b) ((a)>(b)?(a):(b))
    MAX(a,"Hello")//错误地比较int和字符串,没有参数类型检查
    
    // 内联函数示例
    #include <stdio.h>
    inline int add(int a, int b){
          
          
        return (a + b);
    }
    int main(void){
          
          
        int a;
        a = add(1, 2);
        printf("a+b=%d\n", a);
        return 0;
    }
    // 以上 a = add(1, 2);处在编译时将被展开为:a = (a + b);
    

1.34.1 使用时的注意事项:

  • (1)使用宏定义要注意错误情况的出现。(2)宏定义要注意括号的使用,要小心处理宏参数,一般用括号括起来,否则容易出现二义性。
  • (1)内联函数一般用于比较小的,频繁调用的函数,可以减少函数调用带来的开销。
  • 要将内联(inline)函数定义在头文件中。因为编译器在处理(inline)函数时,需要在调用点内联展开该函数,所以仅需要函数声明是不够的。

1.34.2 内联函数使用的条件:

  • 内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。以下情况不宜使用内联:
    • 如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。
    • 如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。
  • 内联不是什么时候都能展开的,一个好的编译器将会根据函数的定义体,自动地取消不符合要求的内联。

1.35 说说运算法 i++ 和 ++i 的区别

  1. 赋值顺序不同: ++i 是先加加后运算,i++ 是先运算后加加。
  2. 效率不同: 后置++执行速度比前置的慢。
  3. i++ 不能作为左值,而 ++i 可以。(左值右值变量的区别在于能不能通过变量去找到它)
  4. 两者都不是原子操作。
  5. 在 stl 中迭代器的后置 ++ 的底层是前置 ++

1.36 new/delete 和 malloc/free 的区别,各自底层实现原理。

区别:

  1. new/delete 是操作符,而 malloc/free 是函数。
  2. new 在调用的时候先分配内存,再调用构造函数,delete 释放的时候,先调用析构函数,再释放内存。 而 malloc 申请的时候只分配内存,free 释放的时候只释放内存。
  3. malloc 需要给定申请内存的大小,返回的指针需要强转;new 会调用构造函数,不用指定内存的大小,返回指针不用强转。
  4. new/delete 可以被重载,malloc/free 不行。
  5. new/delete 分配/释放内存更直接和安全。
  6. new/delete 发生错误抛出异常,malloc 返回 null。

malloc底层实现: 当开辟的空间大小小于 128K 时,调用brk()函数;当开辟的空间大于 128K 时,调用mmap()。malloc 采用的是内存池的管理方式,以减少内存碎片。先申请大块内存作为堆区,然后将堆区分为多个内存块。当用户申请内存时,直接从堆区分配一块合适的空闲块。采用隐式链表链接所有空闲块,每一个空闲块记录了一个未分配的、连续的内存地址。

new底层实现:

  1. 分配内存。
  2. 调用构造函数。
    1. 创建一个新的对象
    2. 将构造函数的作用域赋值给这个新的对象。
    3. 执行构造函数中的代码
    4. 返回新对象

1.36.1 知道 reorder 现象吗?

new 申请内存通常是(1) 申请空间。(2)对象构造函数。(3)将申请的空间赋值给对象。但在编译器层面,可能不按照这个顺序,这就是 reorder 现象。

1.37 const 和 define 的区别

const 用于定义常量。
define 既可以用于定义宏,也可以定义常量。
定义常量时的区别:

  1. 生效时间: const 生效于编译的阶段,define 生效于预处理阶段。
  2. 存储位置: const 定义的常量,在 C++ 中存储在内存中、需要额外的内存空间。define 定义的常量,运行时是直接的操作数,并不会存放在内存中。
  3. 带不带类型: const 定义的常量是带类型的,define 定义的常量不带类型。

1.38 内联函数和函数的区别,内联函数的作用?

内联函数的作用: 内联函数在调用时,是将调用表达式用内联函数体来替换。避免函数调用的开销。
区别:

  1. 内联函数比普通函数多了关键字 inline
  2. 内联函数避免了函数调用的开销
  3. 普通函数在被调用的时候,需要寻址(函数入口地址)
  4. 内联函数有一定的限制,内联函数体要求代码简单,不能包含复杂的结构控制语句;普通函数没有这个要求。

1.39 简述 C++ 有几种传值方式,之间的区别是什么?

总共有三种:值传递、引用传递、指针传递
对于函数传值来说:

  1. 值传递: 形参即使在函数体内值发生变化,也不会影响实参的值。
  2. 引用传递: 形参在函数体内值发生变化,会影响实参的值;
  3. 指针传递: 在指针指向没有发生改变的前提下,如果要改变实参的值,需要加解引用,否则不会影响实参。

对于对象传值来说:

  1. 值传递用于对象时,整个对象会拷贝一个副本,这样效率低。
  2. 引用传递对于对象时,不发生拷贝行为,只是绑定对象,更高效。
  3. 指针传递同理,但不如引用传递安全。

2、 内存管理

2.1 简述一下堆和栈的区别

申请方式不同

  • 栈由系统分配
  • 堆由程序员自己申请和释放

申请大小限制不同

  • 栈的大小是固定的,栈顶和栈底是之前预设好的,栈是向栈底(低地址)扩展
  • 堆的大小可以灵活调整,是不连续的内存空间,堆是向高地址扩展

申请效率不同

  • 栈由系统分配,速度快,不会有碎片
  • 堆由程序员分配,速度慢,会有碎片

栈空间默认是4M,堆一般是1G-4G

管理方式 程序员控制 编译器自动管理,无需手动控制
内存管理机制 系统有一个记录空闲内存地址的链表,当系统收到程序申请时,遍历该链表,找到第一个空间大于申请空间大小的堆节点,删除空闲链表中的该节点,并将该节点空间分配给程序,之后系统会将多余的部分重新放入空闲链表中 只要栈的剩余空间大于申请空间,系统就为程序分配空间,否则报异常提示栈溢出
空间大小 堆是不连续的内存区域,堆大小受限于计算机系统中有效的虚拟内存,所以堆的空间比较灵活,比较大 栈是一块连续的内存区域,大小是操作系统预定好的,Windows下栈大小是2M(VC中可设置)
碎片问题 对于堆,频繁的new/delete会造成大量碎片,使程序效率降低 对于栈,它是类似于数据结构上的一个先进后出的栈,进出一一对应,不会产生碎片
生长方向 堆向上,向高地址增长 栈向下,向低地址方向增长
分配方式 堆都是动态分配 栈有静态分配和动态分配,静态分配由编译器完成,动态分配由malloc函数分配,但栈的动态分配的资源由编译器进行释放
分配效率 堆由C/C++函数库提供,机制很复杂,所以堆的效率比栈低很多。 栈是其系统提供的数据结构,计算机在底层对栈提供支持,分配专门的寄存器存放栈地址,栈操作有专门指令

形象的比喻

  • 栈就像我们去饭馆吃饭,只管点菜(发出申请),付钱、和吃(使用),而不用管其他操作,它的好吃是快捷、但自由度小。
  • 堆就像是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,自由度大。

2.2 C++ 的内存管理

C++ 虚拟内存总共为 4G,从高地址到低地址分为 1G 的内核区和 3G 的用户区
内核区: 进程控制块、文件描述符表、命令行参数和环境变量等。
C++ 用户区内存分区: 栈、堆、全局/静态存储区、常量存储区、代码区。(堆和栈是可执行程序运行的时候才开始分配的)

  • 栈: 存放函数的局部变量、函数参数、返回地址等。
  • 堆: 动态申请的内存空间。
  • 全局/静态存储区(.bss和.data): 存放全局变量和静态变量,程序运行结束时操作系统自动释放。
  • 常量存储区(.data段): 存放的是常量,不允许修改,程序运行结束自动释放。
  • 代码区(.text段): 存放代码,不允许修改,但可以执行。
    他们从低地址到高地址:.text段 -> .data段 -> .bss段 -> 堆 -> unuse ->栈 ->env

2.3 内存对齐问题?

  • 结构体内成员按照声明顺序存储,第一个成员地址和整个结构体地址相同
  • 未特殊声明时,按结构体中 size 最大的成员对齐

C++11引入两个关键字alignasalignof。其中alignof可以计算出类型的对齐方式,alignas可以指定结构体的对齐方式,注意:若alignas小于自然对齐的最小单位,则被忽略。若想使用单字节对齐的方式,使用alignas也是无效的。
内存对齐的规则:

  1. 第一个成员必须是从 0 位置开始偏移。
  2. 下面的成员从成员的大小和对齐模数(默认是8)相比,取小的整数倍的地方
  3. 最后要对结构体整体进行对齐:成员中最大的和那一个对齐模数相比,取小的整数倍的地方。
    在这里插入图片描述

3、C++ 面向对象

3.1 简述一下什么是面向对象?

概念: 面向对象是一种编程思想,把一切东西看成是一个个对象,他们各自都有属性,把这些对象拥有的属性变量和操作这些变量的函数打包成一个类来表示。
面向过程和面向对象的过程:

  • 面向过程: 根据业务逻辑从上到下写代码。
  • 面向对象: 将数据(类变量)与函数(类函数)绑定到一起,进行封装,这样能够更快速的开发程序,减少了重复代码的重写过程。

3.2 简述一下面向对象的三大特征

面向对象的三大特征是封装、继承和多态

  1. 封装: 将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外提供公开接口来和对象进行交互。封装本质是一种管理,我们对于类,不想给别人看到的,我们使用 protected/private 把成员封装起来。开放一些共有的成员函数对成员合理的访问。
  2. 继承: 可以使用现有类的所有功能,并在无需重新编写原来类的情况下对这些功能进行扩展。
  3. 多态: 是一个接口多种实现。实现多态有两种方式:重载和重写

3.2.1 继承的一些概念

private,protected和public的访问范围:

  • private: (1) 该类中的函数。(2)友元函数访问。
  • protected: (1)该类中的函数。(2)友元函数访问。(3)子类的函数。
  • public: (1)该类中的函数。(2)友元函数访问。(3)子类的函数。(4)该类的对象。

注: 友元函数包括 3 种:(1)设为友元的普通的非成员函数。(2)设为友元的其他类的成员函数。(3)设为友元类中的所有成员函数。
三种继承方式:

继承方式 private继承 protected继承 public继承
基类的 private 成员 不可见 不可见 不可见
基类的 protected 成员 变为 private 成员 变为 protected 成员 仍为protected成员
基类的 public 成员 变为 private 成员 变为 protected 成员 仍为public成员

3.3 简述下深拷贝和浅拷贝,如何实现深拷贝?

  • 浅拷贝: 又称值拷贝,将源对象的值(即如果是指针,就只是指向同一块区域)拷贝到目标对象中去,本质上来说源对象和目标对象共用一份实体,只是所引用的变量名不同。
  • 深拷贝: 将源对象里的内容拷贝到目标对象中去,拷贝的时候开辟出和源对象大小一样的空间,这样两个指针就指向了不同的内存位置。
  • 深拷贝的实现: 先开辟出和源对象一样大的内存区域,然后将需要拷贝的数据复制到目标拷贝对象。

----------关于多态的相关问题------------

3.4 简述一下 C++ 中的多态

概念: 一个接口的多种实现。主要分为静态多态和动态多态。
分类:

  • 实现条件:(动态绑定条件)
    1. 虚函数:基类中必须有虚函数,在派生类中必须重写虚函数。
    2. 通过基类类型 的指针或引用来调用虚函数。
  • 特例:协变是重写的特例,基类中返回值是基类类型的引用或指针,在派生类中,返回值为派生类类型的引用或指针。

3.5 什么是多态?除了虚函数,还有什么方式能实现多态?

  1. 多态是面向对象的重要特征之一,它是一种行为的封装。
  2. 多态是以封装和继承为基础的。在 C++ 中多态分为静态多态(早绑定,编译时)和动态多态(晚绑定,运行时)两种,其中动态多态是通过虚函数实现,静态多态通过函数重载实现。

3.6 简述一下重载和重写

重写与重载是实现多态的两种方式
重载:

  • 概念: 通常是指一个类中的多个函数,这些函数名称相同,参数不同,实现的功能相同或者相近。
  • 实现方式: C ++ 支持重载的方式主要是通过命名倾轧技术,即识别一个函数并不是像 C 语言一样,只通过函数名,而是通过函数名和参数的类型与个数去识别的。

重写:

  • 概念: 主要是指派生类中重新定义的函数,除函数体外,都与基类一样,可以说是多态的实现方式。
  • 实现方式: 在基类的函数前加上 virtual 关键字(定义为虚函数),在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数。总结:重写用虚函数来实现,结合动态绑定
  • 相关问题:
    • 虚函数: 用 virtual 关键字声明的函数叫做虚函数,虚函数肯定是类的成员函数。可以通过这个问题,引导面试官将问题集中到虚函数问题上。
    • 多态性: 多态性是一个接口多种实现,分为类的多态性和函数的多态性。
    • 纯虚函数: 纯虚函数是虚函数再加上 = 0。
    • 抽象类: 包括至少一个纯虚函数的类。

3.7 C 语言如何实现 C++ 语言的重载

  1. 使用函数指针来实现
  2. gcc 有内置函数,程序使用编译函数可以实现函数重载

3.8 重载、重写与隐藏的区别?

重载与重写的区别:

  • 重写是父类和子类,重载是同一个类里边。
  • 重写要求参数列表相同,重载不需要。
  • 重写调用方法根据对象类型来决定,重载根据调用时的实参和形参表来决定。

隐藏: 派生类中的函数屏蔽了基类中的同名函数。有以下情形:

  • 两个函数参数相同,但是基类函数不是虚函数。
  • 两个函数参数不同,无论基类函数是不是虚函数,都会被隐藏。

----------关于虚函数的相关问题------------

3.9 简述一下虚函数和纯虚函数,以及实现原理

虚函数:

  • 概念(作用): C++ 中的虚函数主要是实现多态的机制。虚函数必须是基类的非静态成员函数。
  • 底层原理: 主要是通过一张虚函数表(Virtual Table)来实现。每一个类如果有虚函数,都会有与其对应的虚函数表,即派生类和基类的虚函数表是不同的,从而能够实现多态。

纯虚函数:

  • 概念: 纯虚函数是一种特殊的虚函数,在基类中只有声明没有定义,要求任何派生类都要定义自己的实现方法。含有纯虚函数的类称为抽象类,它不能生成对象。
  • 实现方式: 在虚函数原型后加“=0”。 virtual void GetName() = 0
  • 显著特征: 它们必须在派生类中重新声明函数,而且它们在抽象类中往往没有定义。
  • 目的: 为了实现一个接口,用来规范派生类的行为。派生类仅仅只是继承函数的接口
  • 意义: 类纯虚函数的声明就是在告诉子类的设计者,“你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它”

3.10 说说 C++ 虚函数与纯虚函数的区别

不同点:

  1. 虚函数和纯虚函数可以定义在同一个类中,含有纯虚函数的类被称为抽象类
  2. 虚函数可以被直接使用,而纯虚函数不可以直接使用。因为纯虚函数在基类中有声明而没有定义。

相同点:

  1. 虚函数和纯虚函数都能在子类中被重写,以多态的形式调用。
  2. 虚函数和纯虚函数通常存在于抽象基类之中,被继承的子类重载,目的是提供一个统一的接口。
  3. 虚函数和纯虚函数都不可以是静态函数。

3.11 说说纯虚函数能实例化(生成对象)吗?为什么?派生类要实现吗?为什么?

  1. 纯虚函数不可以实例化,但是可以用其派生类实例化。
  2. 虚函数的原理采用 vtable。类中含有纯虚函数时,其 vtable 不完全,有个空位。直接实例化就会指向一个不存在的函数,所以不可以。
  3. 任何派生类都要定义自己的实现方法。
  4. 就上 3.9 问题中,纯虚函数的目的和意义。

3.12 说说为什么要虚析构

虚析构:

  • 概念: 是将基类的析构函数声明为 virtual。
  • 主要作用: 防止内存泄漏。
  • 详细描述: 我们在派生类的时候,可能会动态申请新的内存空间,如果不使用虚析构让派生类定义自己的析构函数,派生类就会默认执行基类的析构函数,不会析构派生类对象,那就不会释放掉派生类中申请的内存空间,这样就会造成内存泄漏。

3.13 构造函数为什么不能被声明为虚函数?

  1. 从存储空间角度: 虚函数对应一个虚函数表,表的地址存储在对象的内存空间里,如果将构造函数设置为虚函数,就要到虚函数表中调用,可是对象还没有实例化,没有内存空间分配,如何调用。
  2. 从使用角度: 虚函数主要用于在信息不全的情况下,能使重载的函数得到对应的调用。构造函数本身就是要初始化实例,那使用虚函数也没有实际意义。
  3. 从实现上看: 构造函数用来创建一个新的对象,而虚函数的运行是建立在对象的基础上,在构造函数执行时,对象尚未形成,所以不能将构造函数定义为虚函数。

3.14 什么是虚继承,解决什么问题?如何实现?

解决问题: 解决 C++ 多重继承问题的一种手段,从不同途径继承来的同一基类,会在子类中存在多份拷贝。这会产生两个问题。(通过虚继承就会避免这个问题,它会存储虚基表指针,指向虚基表。虚基表中会存放相对偏移量,用来找虚基类)。

  1. 浪费存储空间
  2. 存在二义性问题

3.15 构造函数中能不能调用虚方法

要在构造函数中调用虚方法,从语法上讲,调用完全没有问题,但是从效果上看,往往不能达到需要的目的。
派生类对象构造期间进入基类的构造函数时,对象类型变成了基类类型,而不是派生类类型。

3.16 如何理解抽象类

  • 定义: 有纯虚函数的类叫做抽象类。
  • 特征:
    1. 抽象类只能用作其他类的基类,不能建立抽象类对象(实例化)。
    2. 抽象类不能用作参数类型、函数返回类型或显式转换的类型。
    3. 可以定义指向抽象类的指针和引用,此指针可以指向它的派生类,进而实现多态性。

3.17 说说什么是虚基类,可否被实例化?

  1. 在被继承的类前面加上 virtual 关键字,这时被继承的类称为虚基类。
  2. 虚继承的类可以被实例化。

3.18 C++ 中那些函数不能被声明为虚函数?

不能被声明为虚函数的有:普通函数(非成员函数),静态成员函数,内联成员函数,构造函数,友元函数。

  1. 为什么 C++ 不支持普通函数为虚函数?
    • 普通函数(非成员函数)只能被 overload,不能被 override。
  2. 为什么 C++ 不支持构造函数为虚函数?
    • 见问题 3.13
  3. 为什么 C++ 不支持内联成员函数为虚函数?
    • 内联函数是在编译时期展开,而虚函数的特性是运行时才动态展开,所以两者矛盾,不能定义内联函数为虚函数
  4. 为什么 C++ 不支持静态成员函数为虚函数?
    • 静态成员函数对于每个类来说只有一份代码,所有的对象都共享这一份代码,他也没有要动态绑定的必要性。
  5. 为什么 C++ 不支持友元函数为虚函数
    • 因为C++不支持友元函数的继承

3.19 虚函数表里存放的内容是什么时候写进去的?

  1. 虚函数表是一个存储虚函数地址的数组,以 NULL 结尾。虚表生成在编译阶段,当对象内存空间开辟以后,写入对象中的 vfptr,然后调用构造函数。即:虚表在构造函数之前写入。
  2. 除了在构造函数之前写入之外,我们还需要考虑到虚表的二次写入机制,通过此机制让每个对象的虚表指针都能准确地指向到自己类的虚表。

3.20 基类的虚函数表存放在内存的什么区,虚表指针 vptr 的初始化时间

什么时候将函数定义为虚函数:

基类希望其派生类进行重写的函数。

虚函数表的特征:

  • 虚函数表是全局共享的元素,即全局仅有一个,在编译时就构造完成
  • 虚函数表类似一个数组,类对象中存储 vptr 指针,指向虚函数表,即虚函数表不是函数,不是程序代码,不可能存储在代码段
  • 虚函数表存储虚函数的地址,即虚函数表的元素是指向类成员函数的指针,而类中虚函数的个数在编译时期可以确定,不必动态分配内存空间存储虚函数表,所以不在堆中。

根据以上特征,测试结果显示:

虚函数表 vtable 在 Linux/Unix 中存放在只读数据段中(rodata),而微软的编译器将其存放在常量存储区(.data段)。

由于虚表指针vptr跟虚函数密不可分,对于有虚函数或者继承于拥有虚函数的基类,对该类进行实例化时,在构造函数执行时会对虚表指针进行初始化,并且存于对象内存布局的最前面。

总结:

C++中虚函数表位于只读数据段(.rodata),也就是 C++ 内存模型中的常量区;而虚函数则位于代码段(.text),也就是C++内存模型中的代码区。 虚表指针 vptr 在构造函数执行时进行初始化。

----------关于模板的相关问题------------

3.21 C++ 模板是什么?底层是怎么实现的?

作用: 主要是提高 C++ 的泛化性,能够减少重复代码的编写。

分类: 函数模板和类模板

  • 函数模板:

    #include<iostream> 
    using namespace std; 
    template<typename type1,typename type2>//函数模板
    type1 Max(type1 a,type2 b) 
    {
          
           
     return a > b ? a : b; 
    } 
    void main() 
     {
          
           
     cout<<"Max = "<<Max(5.5,'a')<<endl; 
    }
    
  • 类模板:

    template<class NAMETYPE, class AGETYPE = int>
    class Person {
          
          
    public:
    
    	Person(NAMETYPE name, AGETYPE age) {
          
          
    		this->m_Name = name;
    		this->m_Age = age;
    	}
    
    	void showPerson() {
          
          
    		cout << "姓名:" << this->m_Name << " 年龄:" << this->m_Age << endl;
    	}
    
    	NAMETYPE m_Name;
    	AGETYPE m_Age;
    };
    

底层实现: 编译器会对函数模板进行两次编译。

  1. 在声明的地方对模板代码本身进行编译。
  2. 在调用的地方对参数替换后的代码进行编译。

注意: 函数模版只有在实例化后才能成为真正的函数。

3.22 模板类和模板函数的区别是什么?

函数模板允许隐式调用显示调用,而类模板只能显示调用
函数模板的实例化是由编译程序在处理函数调用时自动完成的,而类模板的实例化必须由程序员在程序中显示地指定。

3.23 模板函数和普通函数的区别

模板函数不允许自动类型转换。

3.24 模板和实现可不可以不写在一个文件里面?为什么?

模板函数声明和定义要放在一个文件里边,不要放在不同文件里边,否则会发生链接错误。
原因: 模板类的成员函数分配是在实例化的时候。编译的时候不会生成对应的构造函数。

4、STL

4.1 为什么 malloc 需要指定内存大小,而 free 不需要

因为 malloc 申请内存的时候会增加一些开销。在所申请空间的开始和接收有一对 cookie(总共 8 字节,一个 4 字节),用来表示申请大小,那么 free 的时候就不需要指定大小了,通过 cookie 就知道从那里开始到那里结束。

4.2 说一说 STL 中迭代器的作用,有指针为何还要迭代器。

概念:

  • 用来提供一种方法顺序访问一个聚合对象中各个元素,而又不暴露该对象的内部表示。
  • 可以在不知道对象内部表示的情况下,按照一定顺序访问聚合对象中的各个元素。

产生原因:
迭代器类的访问方式就是把不同集合类的访问逻辑抽象出来,使得不用暴露集合内部的结构而达到循环遍历集合的效果。

和指针的区别:

  • 迭代器不是指针,是类模板,表现的像指针。本质是对指针的封装,它重载了指针的所有操作。能够提供比指针更高级的行为。
  • 迭代器返回的是对象引用而不是对象的值。

迭代器的作用:

  • 用于指向容器中的元素
  • 通过迭代器可以读取它指向的元素
  • 通过非 const 迭代器还可以修改其指向的元素

总结:
可以在不暴露对象内部的情况下去访问对象中的元素。
它是一个类模板,相当于是对指针的封装,重载了指针的所有操作,能够提供比指针更高级的行为,返回的是引用而不是值。

感觉容器更加重要,之后写一篇文章专门讨论 STL 中容器的底层实现

5、新特性

5.1 说说 C++ 11 的新特性有哪些

语法的改进:

  • 统一的初始化方法。
  • 成员变量默认初始化
  • auto 关键字用于定义变量,编译器可以自动判断类型
  • decltype 获取表达式的类型
  • 智能指针 shared_ptr
  • 空指针 nullptr
  • 基于范围的 for 循环
  • 右值引用和 move 定义(将某个左值强制转化为右值),让程序员有意识减少拷贝操作

标准库扩充:

  • 无序容器(哈希表)
  • 正则表达式
  • Lambda 表达式

5.1.1 正则表达式

符号 意义
^ 匹配行的开头
$ 匹配行的结尾
. 匹配任意单个字符
[…] 匹配[]中的任意一个字符
(…) 设定分组
\ 转义字符
\d 匹配数字[0-9]
\D \d 取反
\w 匹配字母[a-z],数字,下划线
\W \w 取反
\s 匹配空格
\S \s 取反
+ 前面的元素重复1次或多次
* 前面的元素重复任意次
? 前面的字符重复 0 次或 1 次
{n} 前面的元素重复 n 次
{n,} 前面的元素重复至少 n 次
{n,m} 前面的元素重复至少 n 次,至多 m 次
| 逻辑或

5.2 C++ 中智能指针和指针的区别是什么?

智能指针: 它是行为类似于指针的类对象,可以防止内存泄漏。普通指针要动态的申请和释放内存,如果忘记释放就会造成内存泄漏,通过智能指针,可以对普通指针进行封装,可以实现内存的自动申请和释放,防止内存泄漏。
主要思想: 将普通指针进行封装,当智能指针对象过期时,执行它的析构函数释放内存。

5.3 说说 C++ 中的智能指针有哪些?分别解决的问题以及区别?

智能指针总共有 4 类:auto_ptr、unique_ptr、shared_ptr、weak_ptr。
智能指针存在的问题: 如果两个智能指针之间执行指针复制,那么这两个智能指针将会指向同一块区域,那么当这两个智能指针过期时,就会释放一块内存两次,这样就会出现问题。
auto_ptr:

auto_ptr<string> p1(new string("I reigned loney as a cloud."));
auto_ptr<string> p2;
p2 = p1; // auto_ptr不会报错
  • 概念: 采用所有权模式解决问题,这个时候 p1 已经不能被访问了,但是编译的时候却不会报错,那么运行的时候就可能出现内存崩溃问题。
  • 缺点: 编译不会报错,运行时可能发生内存崩溃问题。

unique_ptr:

unique_ptr<string> pu1(new string ("hello world"));
unique_ptr<string> pu2;
pu2 = pu1;  // 编译会报错
  • 概念: 也是采用所有权模式解决问题,同一时间只能有一个智能指针指向该对象。
  • 注意: 当程序试图将一个 unique_ptr 赋值给另一个时,如果源 unique_ptr 是个临时右值,编译器允许这么做;如果源 unique_ptr 将存在一段时间,编译器将禁止这么做。
  • 如果想进行赋值: 可以通过 std::move()
  • 总结: 相当于是对 auto_ptr 进行了改进,如果不做特殊处理,进行赋值在编译的时候就会报错,这样就能避免在运行时造成内存崩溃问题。

shared_ptr:(非常好用)

  • 概念: 实现共享式拥有概念,多个智能指针能够同时指向一个对象。只有最后一个指针过期时,才进行内存释放。
  • 成员函数
    • use_count: 返回引用计数的个数
    • unique: 返回是否是独占所有权(即 use_count 是否为 1)
    • swap: 交换两个 shared_ptr 对象(即交换所拥有的对象)
    • reset: 放弃内部对象的所有权或拥有对象的变更,会引起原有对象的引用计数的减少。
    • get: 返回内部对象(指针), 由于已经重载了()方法, 因此和直接使用对象是一样的.如 shared_ptr sp(new int(1)); sp 与 sp.get()是等价的
  • 总结: 它解决了 auto_ptr 在对象所有权上的局限性(auto_ptr 是独占的),在使用引用计数的机制上提供了可以共享所有权的智能指针。

weak_ptr:

  • 概念: 它通常是和 shared_ptr 类型的智能指针搭配使用,可以视为是 shared_ptr 指针的一种辅助工具。
  • 特点: weak_ptr 只是提供了对管理对象的一个访问手段,它的构造和析构不会引起引用计数的增加或减少。
  • 作用: 可以用来解决 shared_ptr 的循环引用问题(因循环引用而造成的死锁问题)。在这里插入图片描述
  • 注意: 我们不能通过 weak_ptr 直接访问对象的方法,应该先把它转化为 shared_ptr;

5.4 C++ 中智能指针的特点(这个总结的更好)

分类: 总共有 4 类,分别为:shared_ptr、unique_ptr、weak_ptr、auto_ptr, 其中 atuo_ptr 已经被 C++11 弃用。
为什么使用智能指针: 智能指针的作用是管理一个指针,因为普通指针存在申请的空间在结束时忘记释放而造成内存泄漏的情况,使用智能指针就可以很大程度上解决这个问题,因为智能指针就是一个类,当超出了类的作用域时,类会自动调用析构函数,自动释放资源。
四种指针的各自特性:

  • auto_ptr: 当进行拷贝赋值的时候,编译不会报错,运行时访问原来的对象,就会发生内存崩溃问题(因为所有权已经交给了新对象)。
  • unique_ptr:
    • 概念: 规定一个智能指针独占一块内存资源。当两个智能指针同时指向同一块内存,编译时就会报错。
    • 底层原理: 将拷贝构造函数和赋值构造函数申明为 private 或 delete。不允许拷贝构造函数和赋值操作符,但是支持移动构造函数,通过 std::move 把一个对象指针变成右值之后可以移动给另一个 unique_ptr。
    • 补充: private 和 delete 的区别:在使用模板函数时,只能采用 delete 特化,不能采用 private 特化。
  • shared_ptr:
    • 概念: 共享指针可以实现多个智能指针指向相同对象,该对象和其相关资源会在引用为 0 时被销毁释放。
    • 底层原理: 有一个引用计数的指针类型变量,专门用于引用计数,使用拷贝构造函数和赋值构造函数时,引用计数加 1,当引用计数为 0 时,释放资源。
  • weak_ptr: weak_ptr 的构造和析构不会引起引用计数的增加或减少。它可以用来解决 shared_ptr 的循环引用问题。

5.5 weak_ptr 能不能知道对象计数为 0,为什么?

不能,

weak_ptr 是一种不控制对象生命周期的智能指针,它指向一个 shared_ptr 管理的对象。当 对象计数为 0 时,shared_ptr 对象就会被释放掉了。

猜你喜欢

转载自blog.csdn.net/q473203506/article/details/120887094
今日推荐