C++八股文

第一章 编译内存相关

1.1 main函数之前和之后执行的代码

main函数之前

  • 设置栈指针

  • 初始化静态变量和全局变量(即.data内容);将未初始化的全局变量赋值:short、int、long初始化为0,bool初始化为false,指针指向NULL(即.bss内容)

  • 执行全局对象的构造函数

  • 将main函数的参数argc和argv传递给main函数

  • __attribute__((constructor))

main函数之后

  • 执行全局对象的析构函数

  • 用atexit注册一个函数,做善后工作

  • __attribute__((destructor))

补充知识:

__attribute__ 可以设置函数属性(Function Attribute )、变量属性(Variable Attribute )和类型属性(Type Attribute ),用于初始化一些在程序中使用的数据

1.2 C++ 程序编译过程

编译过程分为三个过程:编译(编译预处理、编译、优化),汇编,链接。

  • 编译预处理:处理以 # 开头指令;

  • 编译、优化:将.cpp源码翻译成.s汇编代码;包括词法分析:编译器会读取源代码,将源代码分解成若干个单词,并将每个单词转换成词法单元,例如关键字、标识符、操作符等。语法分析:编译器会对词法分析得到的词法单元进行解析和分析,生成语法树,并将其转换为中间代码。语义分析:编译器会对中间代码进行分析,检查代码是否符合语言的语法规范,包括数据类型是否匹配、符号是否定义、函数是否调用正确等。代码优化:编译器会对生成的中间代码进行分析和优化,以提高代码执行效率,例如去除无用代码、利用CPU指令优化代码等。代码生成:编译器会根据中间代码生成目标代码,如汇编语言。

  • 汇编:将.s汇编代码翻译成.o机器指令;

  • 链接:由于.cpp文件中的函数可能引用了另一个.cpp文件中定义的符号或者函数,链接的目的就是将文件对应的其他目标文件连接成一个整体,从而生成可执行的程序.exe。(链接阶段可以发现被调用的函数未定义)

在这里插入图片描述
链接分为两种:

  • 静态链接:代码从其所在的静态链接库中拷贝到最终的可执行程序中,程序被执行时,这些代码会被装入到该进程的虚拟地址空间中。
  • 动态链接:代码被放到动态链接库或共享对象的某个目标文件中,链接只是在最终的可执行程序中记录了共享对象的名字等一些信息。程序执行时,动态链接库的全部内容会被映射到该进程的虚拟地址空间。

二者的优缺点:

  • 静态链接:优点就是执行的时候运行速度快,因为可执行程序具备了程序运行的所有内容。缺点是浪费空间,且更新困难,因为每个可执行程序都会有目标文件的一个副本,且如果目标文件进行了更新操作,就需要重新生成可执行程序。
  • 动态链接:优点是节省内存、更新方便,缺点是运行速度慢,程序每次执行都需要链接,相比静态链接会有一定的性能损失。

1.3 C++ 内存分为哪几部分

C++ 内存分区:栈、堆、全局/静态存储区、常量存储区、代码区

  • :存放函数的局部变量、函数参数、返回地址等,由编译器自动分配和释放。栈从高地址向低地址增长,一般分配几M大小的内存。
  • :动态申请的内存空间,是malloc分配的内存块,由程序员分配和释放,如果程序执行结束还没有释放,操作系统会自动释放。堆从低地址向高地址增长。一般可以分配几G大小的内存。
  • 全局区/静态存储区(.bss 段和 .data 段):存放全局变量和静态变量,程序运行结束操作系统自动释放,在C语言中,未初始化的放在.bss 段中,初始化的放在 .data 段中,C++ 中不再区分了。
  • 常量存储区(.data 段):存放的是常量,不允许修改,程序运行结束自动释放。
  • 代码区(.text 段):存放代码,不允许修改,但可以执行。编译后的二进制文件存放在这里。

1.4 栈和堆的区别

  • 管理方式:

    • 堆中资源由程序员控制
    • 栈资源由编译器自动管理,无需手动控制
  • 内存管理机制:

    • 堆:有一个记录空闲内存地址的链表,当系统收到程序申请时,遍历该链表,寻找第一个空间大于申请空间的堆结点,将该结点分配给程序,并删除空闲结点链表中的该结点。
    • 栈:只要栈的剩余空间大于所申请空间,系统为程序提供内存,否则报异常,提示栈溢出

    哪些操作可能会导致栈溢出?(1)递归调用:当一个函数多次递归调用自身时,每次调用都会在栈上分配一段新的内存空间,导致栈空间被耗尽。(2)大量函数调用:如果频繁地调用函数,每次函数调用都需要在栈上分配一部分内存用于保存函数的信息,过多的函数调用导致栈溢出。(3)局部变量过多或过大:如果在函数内部定义了过多的局部变量,或者某个局部变量占用的内存过大,超过栈空间的大小可能导致溢出。

  • 空间大小:

    • 堆不是连续的内存区域,一般有1-4G
    • 栈是一块连续的内存,大小是操作系统定好的,window下为2M

    为什么栈空间比堆空间小?因为栈空间主要存储的是函数的局部变量、函数参数及返回值地址,一个程序中这些信息所占内存较小,且栈空间用户是无法进行操作,是由编译器和操作系统决定。

  • 碎片问题:

    • 对于堆,频繁的new/delete会造成大量碎片
    • 对于栈类似于数据结构中的栈,先进后出,不会产生碎片
  • 生长方向:

    • 堆向上,向高地址方向增长
    • 栈向下,向低地址方向增长
  • 分配效率(堆快还是栈快):

    • 堆由C/C++提供的库函数完成操作,在分配堆内存时需要一定的算法寻找合适大小的内存,并且获取堆的内容需要两次访问,第一次访问指针,第二次根据指针保存的地址访问内存,因此堆比较慢,效率低。
    • 栈是系统提供的数据结构,计算机底层对栈提供支持,分配专门寄存器存放栈地址,栈操作有专门指令,因此栈比较快、效率高。

1.5 变量的区别

全局变量、局部变量、静态全局变量、静态局部变量的区别:

(补充知识:C++作用域可分为 6 种:全局作用域,局部作用域,语句作用域,类作用域,命名空间作用域和文件作用域。)

作用域:

  • 全局变量:具有全局作用域。全局变量只需在一个源文件中定义,就可以作用于所有的源文件。当然,其他不包含全局变量定义的源文件需要用extern关键字再次声明这个全局变量。
  • 静态全局变量:具有文件作用域。它与全局变量的区别在于如果程序包含多个文件的话,它作用于定义它的文件里,不能作用到其它文件里,即被static 关键字修饰过的变量具有文件作用域。这样即使两个不同的源文件都定义了相同名字的静态全局变量,它们也是不同的变量。
  • 局部变量:具有局部作用域。在程序运行期间不是一直存在,而是只在函数执行期间存在,函数的一次调用执行结束后,变量被撤销,其所占用的内存也被收回。
  • 静态局部变量:具有局部作用域。它只被初始化一次,自从第一次被初始化直到程序运行结束都一直存在,它和全局变量的区别在于全局变量对所有的函数都是可见的,而静态局部变量只对定义自己的函数体始终可见。

分配内存空间:

  • 静态存储区:全局变量,静态局部变量,静态全局变量。
  • :局部变量。

1.6 全局变量定义在头文件中有什么问题?

如果在头文件中定义全局变量,当该头文件被多个文件 include 时,该头文件中的全局变量就会被定义多次,导致重复定义,因此不能在头文件中定义全局变量

1.7 内存对齐

什么是内存对齐?内存对齐的原则?为什么要进行内存对齐,有什么优点?

内存对齐:数据在计算机存储时按照特定规则对齐的过程,通常以特定字节大小的块进行对齐。

结构体内存对齐的原则(注:对齐基数可以通过#pragma pack ()设置,现在机器默认是 8):

  1. 结构体的首地址能够被其最宽基本类型成员大小与对齐基数中的较小者所整除;
  2. 结构体每个成员相对于结构体首地址的偏移量(offset)都是该成员大小与对齐基数中的较小者的整数倍,如有需要编译器会在成员之间加上填充字节(internal padding);
  3. 结构体的总大小为结构体最宽基本类型成员大小与对齐基数中的较小者的整数倍,如有需要编译器会在最末一个成员之后加上填充字节(trailing padding)。

进行内存对齐的原因:(主要是硬件设备方面的问题)

  1. 某些硬件设备只能存取对齐数据,存取非对齐的数据可能会引发异常;
  2. 某些硬件设备只支持简单数据指令非对齐存取,不支持复杂数据指令的非对齐存取。
  3. 某些硬件设备不能保证在存取非对齐数据的时候的操作是原子操作;
  4. 相比于存取对齐的数据,存取非对齐的数据需要花费更多的时间。

内存对齐的优点

  1. 便于在不同的平台之间进行移植;
  2. 提高内存的访问效率,因为 CPU 在读取内存时,是一块一块的读取。

1.8 什么是内存泄露

并非指内存从物理上消失,而是由于疏忽或错误导致的程序不能释放已经不再使用的内存。常指堆内存泄漏,使用 malloc、new 申请内存空间时,使用完后要free或 delete释放内存,否则会产生内存泄漏。指针重新赋值也容易导致内存泄漏,如下:

char *p = (char *)malloc(10);
char *p1 = (char *)malloc(10);
p = np;

开始时,指针 p 和 p1 分别指向一块内存空间,但指针 p 被重新赋值,导致 p 初始时指向的那块内存空间无法找到,从而发生了内存泄漏。

1.9 怎么防止内存泄漏?内存泄漏检测工具的原理?

  1. 内部封装:将内存的分配和释放封装到类中,在构造的时候申请内存,析构的时候释放内存。但这样不是最佳的做法,在类的对象复制时,程序会出现同一块内存空间释放两次的情况。
  2. 智能指针:智能指针是为了解决动态内存分配时带来的内存泄漏以及多次释放同一块内存空间而提出的。

1.10 智能有哪几种?智能指针的实现原理?

智能指针是为了解决动态内存分配时带来的内存泄漏以及多次释放同一块内存空间而提出的,封装在了memory头文件中。

C++11 中智能指针包括以下三种:

  • 共享指针(shared_ptr):资源可以被多个指针共享,使用引用计数机制表明资源被几个指针共享。可以通过make_shared<>来构造,通过 use_count() 查看资源的所有者的个数。拷贝使得对象的引用计数增加1,赋值使得原对象引用计数减1,调用reset()释放资源的所有权,计数减一,当计数减为 0 时,会自动释放内存空间,从而避免了内存泄漏。
  • 独占指针(unique_ptr):独享所有权的智能指针,资源只能被一个指针占有,该指针不能拷贝构造和赋值。但可以进行移动赋值(调用move() 函数),即一个 unique_ptr 对象可以通过move赋值给另一个 unique_ptr 对象。
  • 弱指针(weak_ptr):指向 shared_ptr 指向的对象,能够解决由shared_ptr带来的循环引用问题。

1.11 一个 unique_ptr 怎么赋值给另一个 unique_ptr 对象?

借助 std::move() 可以实现将一个 unique_ptr 对象赋值给另一个 unique_ptr 对象,其目的是实现所有权的转移。

// A 作为一个类 
std::unique_ptr<A> ptr1(new A());
std::unique_ptr<A> ptr2 = std::move(ptr1);

1.12 使用智能指针会出现什么问题?怎么解决?

智能指针可能出现的问题:循环引用

如果定义了两个类CA、CB,在两个类中分别定义另一个类的对象的共享指针。程序结束后,两个指针相互指向对方的内存空间,出现了循环引用,导致内存无法释放,造成内存泄漏。

循环引用的解决方法:

将其中一个共享指针定义为弱指针,weak_ptr 对被 shared_ptr 管理的对象存在弱引用,weak_ptr 用来表达临时所有权的概念,不增加引用计数。当某个对象只有存在时才需要被访问,需要获得所有权时将其转化为 shared_ptr。

1.13 深拷贝与浅拷贝

  • **c++默认的拷贝构造函数是浅拷贝,**浅拷贝就是类对象的数据成员之间的简单赋值,但当数据成员中有指针时,如果采用简单的浅拷贝,则两类中的两个指针将指向同一个地址,当对象快结束时,会调用两次析构函数,而导致指针悬挂现象,所以,此时,必须采用深拷贝。
  • 深拷贝与浅拷贝的区别就在于深拷贝会在堆内存中另外申请空间来储存数据,而不是一个简单的赋值过程,从而解决了指针悬挂的问题。

1.14 什么是字节序?如何用代码判断大小端字节序

字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序

  • 大端存储:数据的高字节存储在低地址中,windows系统存储方式
  • 小端存储:数据的高字节存储在高地址中,unix系统存储方式

网络字节顺序采用大端排序方式,往往将小端IP转为大端IP,才能进行网络传输

法1:使用强制类型转换(由于int和char的长度不同,借助int型转换成char型,只会留下内存低地址的部分)

#include <iostream>
using namespace std;
int main()
{
    int a = 0x1234;
    //由于int和char的长度不同,借助int型转换成char型,只会留下低地址的部分
    char c = (char)(a);
    if (c == 0x12)
        cout << "big endian" << endl;
    else if(c == 0x34)
        cout << "little endian" << endl;
}

法2:使用union联合体

#include <iostream>
using namespace std;
//union联合体的重叠式存储,endian联合体占用内存的空间为每个成员字节长度的最大值
union endian
{
    int a;
    char ch;
};
int main()
{
    endian value;
    value.a = 0x1234;
    //a和ch共用4字节的内存空间
    if (value.ch == 0x12)
        cout << "big endian"<<endl;
    else if (value.ch == 0x34)
        cout << "little endian"<<endl;
}

第二章 语言对比

2.1 C++ 11 新特性(9个)

1. auto自动类型推导

编译器会在编译期间通过初始值推导出变量的类型,即通过auto定义的变量必须有初始值

2. decltype 声明类型

auto 的功能一样,都是在编译时期进行自动类型推导。如果希望从表达式中推断出要定义的变量的类型,但是不想用该表达式的值初始化变量就用decltype,decltype 作用是返回操作数的数据类型,定义变量的时候可初始化也可不初始化

auto var = val1 + val2; 
decltype(val1 + val2) var1 = 0; 

3. lambda 表达式(匿名函数)

[capture list] (parameter list) -> reurn type
{
   function body
}
  • capture list:捕获列表,指 lambda 表达式所在函数中定义的局部变量的列表,通常为空。lambda 表达式内部函数体在默认情况下是不能够使用函数体外部的变量的,但如果lambda 函数体中用到了 lambda 表达式所在函数的局部变量,必须捕获该变量,即将此变量写在捕获列表中。捕获方式分为:引用捕获方式 [&]、值捕获方式 [=]。
  • return type、parameter list、function body:分别表示返回值类型、参数列表、函数体,和普通函数一样。

4. 范围 for 语句

for (declaration : expression){
    statement
}

参数的含义:

  • expression:必须是一个序列,例如用花括号括起来的初始值列表、数组、vector ,string等。
  • declaration:此处定义一个变量,序列中的每一个元素都能转化成该变量的类型,常用 auto 类型说明符

5. 智能指针

相关知识已在第一章中进行了详细的说明,这里不再重复。

6. nullptr

  • NULL:预处理变量,是一个宏,它的值是 0。
  • nullptr:C++ 11 中的关键字,是一种特殊类型的字面值类型

nullptr 的优势:

  1. 有类型,类型是 typdef decltype(nullptr) nullptr_t,使用 nullptr 提高代码的健壮性。
  2. 函数重载:因为 NULL 本质上是 0,在函数调用过程中,若出现函数重载并且传递的实参是 NULL,可能会出现不知和哪一个函数匹配的情况,但是传递实参 nullptr 就不会出现这种情况。

7. delete 函数和 default 函数

  • delete 函数:= delete 表示该函数不能被调用。
  • default 函数:= default 表示编译器生成默认的函数,例如:生成默认的构造函数。
#include 
using namespace std;

class A
{
public:
	A() = default; // 表示使用默认的构造函数
	~A() = default;	// 表示使用默认的析构函数
	A(const A &) = delete; // 表示类的对象禁止拷贝构造
	A &operator=(const A &) = delete; // 表示类的对象禁止拷贝赋值
};
int main()
{
	A ex1;
	A ex2 = ex1; // error: use of deleted function 'A::A(const A&)'
	A ex3;
	ex3 = ex1; // error: use of deleted function 'A& A::operator=(const A&)'
	return 0;
}

8. 右值引用

右值引用必须绑定到右值的引用,通过 && 获得。右值引用只能绑定到一个将要销毁的对象上,因此可以自由地移动其资源。

#include 
#include 
using namespace std;
int main()
{
    int var = 42;
    int &l_var = var;
    int &&r_var = var; // error: cannot bind rvalue reference of type 'int&&' to lvalue of type 'int' 错误:不能将右值引用绑定到左值上

    int &&r_var2 = var + 40; // 正确:将 r_var2 绑定到求和结果上
    return 0;
}

9. std::move() 函数

std::move 可以将一个左值强制转化为右值引用,包含在 utility 头文件中。

2.2 面向过程和面向对象的区别及优缺点

面向过程是分析出解决问题所需要的步骤,然后用函数把这些步骤实现,使用的时候依次调用即可;面向对象是把问题分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在解决问题中的行为。

面向过程语言的优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;单片机、嵌入式开发、 Linux/Unix等一般采用面向过程开发,性能是最重要的因素。缺点:没有面向对象易维护、易复用、易扩展

面向对象语言的优点:易维护、易复用、易扩展,面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护。缺点:性能比面向过程低

2.3 C 和 C++ 的区别

  • **动态内存管理:**C语言通过malloc和free来进行堆内存的分配和释放,而C++是通过new和delete来管理堆内存的;
  • **强制类型转换:**C的强制类型转换使用()小括号里面加类型进行类型强转的,而C++有四种自己的类型强转方式,分别是static_cast、const_cast、reinterpret_cast和dynamic_cast;
  • **输入输出方式:**C使用scanf/printf输入输出,C++使用cin/cout输入输出,前面一组是C的库函数,后面是istream和ostream类型的对象。
  • **应用领域:**C 语言主要用于单片机、嵌入式等与硬件直接打交道的领域,C++ 可以用于应用层开发,用户界面开发等与操作系统打交道的领域。
  • C++对C语言进行了扩展:增加了异常处理、运算符重载、命名空间作用域、面向对象的机制、泛型编程的机制(Template)、标准模板库(STL)。

2.4 Python 和 C++ 的区别

  • **语言特性:**Python 为脚本语言,解释执行,不需要经过编译;C++ 是一种需要编译后才能运行的语言,在特定的机器上编译后运行。
  • **运行效率:**C++ 运行效率高,安全稳定。原因:Python 代码和 C++ 最终都会变成 CPU指令来跑,但一般情况下,Python 最终转换出来的 CPU 指令会比 C++ 多很多。首先,Python中涉及的内容比 C++ 多,经过了更多层,Python 中甚至连数字都是 object ;其次,Python 是解释执行的,和物理机CPU 之间多了解释器这层,而 C++ 是编译执行的,直接就是机器码,编译的时候编译器又可以进行一些优化。
  • **开发效率:**Python 开发效率高。Python 一两句代码就能实现的功能,C++ 往往需要更多的代码才能实现。
  • **语法格式:**Python 的语法格式不同于 C++ 定义声明才能使用,而且极其灵活,完全面向更上层的开发者。

2.5 Java和C++的区别

  • **语言特性:**Java语言给开发人员提供了更为简洁的语法,**完全面向对象,**Java程序一般生成字节码,在JVM里运行得到结果,由于JVM可以安装到任何操作系统上,因此可移植性强。C++也可以在其他系统运行,但是需要不同的编码,例如一个数字在windows中是大端字节序(数据的高位字节存储在内存的低地址处),在unix中则为小端字节序(数据的高位字节存储在内存的高地址处)。
  • **指针:**Java语言没有指针,不同于C++中利用指针实现的伪数组,Java引入了真正的数组,同时将容易造成麻烦的指针从语言中去掉,防止常见的因为数组操作越界等指针操作而对系统数据进行非法读写带来的不安全问题
  • **垃圾回收:**Java内存的分配和回收都是自动进行的,程序员无需考虑内存碎片问题,C++用析构函数回收垃圾,写C/C++程序时要注意内存的申请和释放
  • **应用场景:**Java适合web开发,有丰富的框架。C++适合桌面应用开发,可以直接编译成exe文件。

第三章 类相关(高频)

3.1 什么是面向对象?面向对象的三大特性

面向对象:对象是指具体的某一个事物,这些事物的抽象就是类,类中包含成员变量和成员函数。面向对象编程将变量和函数封装到一个类中,并声明它们的访问级别(public、private、protected),对数据成员起到一定的保护作用。而且在类的对象调用成员函数时,只需知道成员函数的名字、参数列表以及返回值类型即可,无需了解其函数的实现原理。

面向对象的三大特性:

  • 封装:将具体的实现过程和数据封装成类或函数,只能通过接口进行访问,降低耦合性
  • 继承:子类继承父类的特征和行为,子类有父类的非private变量和函数,子类可以对父类的方法进行重写,增强了类之间的耦合性
  • 多态不同继承类的对象对同一消息做出不同的响应,基类的指针指向绑定到派生类的对象,使得基类指针呈现不同的表现方式

3.2 重载、重写、隐藏的区别

  1. 重载同一个访问区内声明几个具有不同参数列表(类型、个数、顺序)的同名函数,根据参数列表确定调用哪个函数,重载不关心函数返回类型。

在C++中,::*.?:这4个运算符不能重载

class A
{
public:
    void fun(int tmp);
    void fun(float tmp);        // 重载 参数类型不同(相对于上一个函数)
    void fun(int tmp, float tmp1); // 重载 参数个数不同(相对于上一个函数)
    void fun(float tmp, int tmp1); // 重载 参数顺序不同(相对于上一个函数)
    int fun(int tmp);            // error: 'int A::fun(int)' cannot be overloaded 错误:注意重载不关心函数返回类型
};
  1. 隐藏:派生类的函数屏蔽了与其同名的基类函数,不管参数列表是否相同,基类函数都会被隐藏。
#include <iostream>
using namespace std;

class Base
{
public:
    void fun(int tmp, float tmp1) { cout << "Base::fun(int tmp, float tmp1)" << endl; }
};

class Derive : public Base
{
public:
    void fun(int tmp) { cout << "Derive::fun(int tmp)" << endl; } // 隐藏基类中的同名函数
};

int main()
{
    Derive ex;
    ex.fun(1);       // Derive::fun(int tmp)
    ex.fun(1, 0.01); // error: candidate expects 1 argument, 2 provided,改为 ex.Base::fun(1, 0.01)就可以调用基类中的同名函数。
    return 0;
}
  1. 重写(覆盖)基类中被重写的函数必须有virtual关键词修饰。派生类中的重写函数的函数名、参数列表、返回值类型都必须同基类中被重写函数一致,只有函数体不同。通常派生类中重写函数的参数列表后面有override关键词修饰,是编译器提供的安全机制,用于帮助开发者在继承和重写函数时避免潜在书写错误。
#include <iostream>
using namespace std;

class Base
{
public:
    virtual void fun(int tmp) { cout << "Base::fun(int tmp) : " << tmp << endl; }
};

class Derived : public Base
{
public:
    void fun(int tmp) override { cout << "Derived::fun(int tmp) : " << tmp << endl; } // 重写基类中的 fun 函数
};
int main()
{
    Base *p = new Derived();
    p->fun(3); // Derived::fun(int) : 3
    return 0;
}

重载、隐藏、重写的区别:

  • 范围不同:重载发生在同一个类的内部,隐藏、重写发生在不同类之间。
  • 参数区别:重载与被重载函数的参数列表不同,隐藏不关注参数列表,重写与被重写函数的参数列表相同

3.3 什么是多态?多态如何实现?多态的分类

多态:不同继承类的对象对同一消息做出不同的响应,基类的指针指向或绑定到派生类的对象,使得基类指针呈现不同的表现方式。在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据实际的对象类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数,如果对象类型是基类,就调用基类的函数。

多态实现过程(虚函数的作用机制):

  1. 多态是通过虚函数实现的,虚函数的地址保存在虚函数表中,存在虚函数的类都有一个虚函数表(编译时创建),当创建该类的对象时,该对象有一个指向虚函数表的虚表指针(虚函数表和类对应的,虚表指针是和对象对应);
  2. 当基类指针指向派生类对象,基类指针调用虚函数时,基类指针指向派生类对象的虚表指针,该虚表指针指向派生类虚函数表,通过遍历虚表,寻找相应的虚函数。

静态多态与动态多态:

  1. 静态多态:也称为编译期间的多态,编译器根据函数实参的类型,可推断出要调用哪个函数,如果有对应的函数就调用该函数,否则出现编译错误。重载属于静态多态。
  2. 动态多态(动态绑定):即运行时的多态,在程序执行期间判断所引用对象的实际类型,从而调用相应的方法。多态属于动态时多态。
#include <iostream>
using namespace std;

class Base
{
public:
	virtual void fun() { cout << "Base::fun()" << endl; }

	virtual void fun1() { cout << "Base::fun1()" << endl; }

	virtual void fun2() { cout << "Base::fun2()" << endl; }
};
class Derive : public Base
{
public:
	void fun() { cout << "Derive::fun()" << endl; }

	virtual void D_fun1() { cout << "Derive::D_fun1()" << endl; }

	virtual void D_fun2() { cout << "Derive::D_fun2()" << endl; }
};
int main()
{
	Base *p = new Derive();
	p->fun(); // Derive::fun() 调用派生类中的虚函数
	return 0;
}

3.4 什么是虚函数?什么是纯虚函数?

虚函数:被virtual关键字修饰的成员函数就是虚函数。

纯虚函数:虚函数在类中声明时加上等于0,就是纯虚函数。含有纯虚函数的类称为抽象类,类中只有接口,没有具体的实现方法,不能实例化对象;继承抽象类的派生类,如果没有完全实现基类纯虚函数,依然是抽象类,不能实例化对象。此外,对于实现纯虚函数的派生类,该纯虚函数在派生类中被称为虚函数。

3.5 虚函数和纯虚函数的区别?

  • 定义形式不同:虚函数在定义时只需要在普通函数前加上virtual关键字即可,纯虚函数定义时除了加上virtual 关键字还需要加上等于0;
  • 使用方式不同:虚函数必须实现,否则编译器会报错,而且虚函数可以直接使用,纯虚函数必须在派生类中实现后才能使用。

3.6 为什么析构函数要为虚函数?为什么C++默认的析构函数不是虚函数?

为了防止内存泄露,因为对于基类指针指向派生类对象,如果基类析构函数不是虚函数,那么当delete该对象时,只会调用父类的析构函数,不会调用子类的析构函数,会造成内存泄漏。

C++默认的析构函数不是虚函数是因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。

3.7 不能声明为虚函数的有哪些?

  1. **构造函数:**虚函数的调用需要虚函数表指针,而该指针存放在对象的内存空间中;若构造函数声明为虚函数,那么由于对象还未创建,还没有内存空间,也就不能调用该构造函数。
  2. **静态成员函数:**该函数类共用,与该类的对象无关。
  3. **内联函数:**inline是在编译的时候替换到函数调用处,虚函数的调用只有在程序运行的时候才能知道到底调用的是哪个函数,是动态的。
  4. 类外普通函数

3.8 override和final

override重写父类的虚函数,final表示不希望某个虚函数被重写

3.9 C++类有哪几种构造函数

  • 默认构造函数(无参构造函数)

  • 初始化构造函数(有参构造函数)

  • 拷贝构造函数

  • 移动构造函数

默认情况下,c++编译器至少给一个类添加3个函数

  • 默认构造函数(无参,函数体为空)

  • 默认析构函数(无参,函数体为空)

  • 默认拷贝构造函数,对属性进行值拷贝(浅拷贝)

构造函数调用规则如下:

  • 如果用户定义有参构造函数,c++不再提供默认无参构造,但是会提供默认拷贝构造
  • 如果用户定义拷贝构造函数,c++不会再提供其他构造函数
class Person {
public:
	Person() {
		cout << "无参构造函数!" << endl;
		mAge = 0;
	}
	Person(int age) {
		cout << "有参构造函数!" << endl;
		mAge = age;
	}
	Person(const Person& p) {
		cout << "拷贝构造函数!" << endl;
		mAge = p.mAge;
	}
	//析构函数在释放内存之前调用
	~Person() {
		cout << "析构函数!" << endl;
	}
public:
	int mAge;
};

//1. 使用一个已经创建完毕的对象来初始化一个新对象
void test01() {

	Person man(100); //p对象已经创建完毕
	Person newman(man); //调用拷贝构造函数
	Person newman2 = man; //拷贝构造

	//Person newman3;
	//newman3 = man; //不是调用拷贝构造函数,赋值操作
}

//2. 值传递的方式给函数参数传值
//相当于Person p1 = p;
void doWork(Person p1) {}
void test02() {
	Person p; //无参构造函数
	doWork(p);
}

//3. 以值方式返回局部对象
Person doWork2()
{
	Person p1;
	cout << (int *)&p1 << endl;
	return p1;
}

void test03()
{
	Person p = doWork2();
	cout << (int *)&p << endl;
}


int main() {

	//test01();
	//test02();
	test03();

	system("pause");

	return 0;
}

3.10 多个类的构造函数和析构函数的顺序问题

  • 子类继承父类后,当创建子类对象,先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反
  • 若B类中有对象A作为成员,A为对象成员,则先构造A,再构造B,而析构函数则相反。

3.11 析构函数可以重载吗?

不能重载,析构函数在对象的生存周期即将结束的时候由系统自动调用,调用结束后对象就消失了,之后的重载的析构函数也就不能被调用了。

3.12 public、protected、private的区别

访问范围:

  • private: 只能由该类中的函数、其友元函数访问,不能被任何其他类访问,该类的对象也不能访问
  • protected: 可以被该类中的函数、子类的函数、以及其友元函数访问,但不能被该类的对象访问
  • public: 可以被该类中的函数、子类的函数、其友元函数访问,也可以由该类的对象访问

继承后的变化:

  • 使用private继承,父类的所有方法在子类中变为private;
  • 使用protected继承,父类的protected和public方法在子类中变为protected,而private方法不变;
  • 使用public继承,父类中的方法属性不发生改变;

第四章 关键字库函数

4.1 sizeof 和 strlen 的区别

  • sizeof是操作符,在编译时获得;strlen是C语言库函数,在运行时获得
  • sizeof可以是任何数据类型或者数据,strlen的参数只能是字符指针且结尾是‘\0’的字符串

数组名的意义:

  1. sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小。
  2. & 数组名,这里的数组名表示整个数组,取出的是整个数组的地址。
  3. 除此之外所有的数组名都表示首元素的地址
char* a = "12345";
char b[8] = "12345";
char c[] = "12345";
// 64位编译器
sizeof(a);  // 8,一个指针占8个字节
strlen(a);  // 5,不包括\0
sizeof(b);  // 8,表示在内存中预分配的大小
strlen(b);  // 5,不包括\0
sizeof(c);  // 6,c是数组且包括\0
strlen(b);  // 5,不包括\0

注意:在64位编译环境下,一个指针占8个字节,在32位下占4个字节

sizeof(类)相关

  1. 类的大小为类的非静态成员变量大小之和,不包括普通成员函数(因为普通成员函数代码被类对象所共享)。存在虚函数的类需要维护虚函数指针,所以占用一个4字节/8个字节(32位操作系统/64位操作系统)大小,且满足内存对齐要求。
  2. 一个空类和空类对象的大小为都为1个字节
#include<iostream>
using namespace std;
class A
{
};
class B
{
public:
    B() {}
    ~B() {}
};
class C
{
public:
    C() {}
    virtual ~C() {}
};
int main()
{
    cout <<"sizeof一个空类的大小为  "<< sizeof(A) << endl;//1
    cout << "sizeof一个带有构造函数和析构函数的类的大小为  " << sizeof(B) << endl;//1
    cout << "sizeof一个带有虚函数的类的大小为  " << sizeof(C) << endl;//4
    return 0;
}

sizeof(1==1) 在 C 和 C++ 中分别是什么结果?

C语言为4,C++为1,因为c语言返回的是int,而c++返回的是bool

4.2 lambda 表达式(匿名函数)的具体应用和使用场景

[capture list] (parameter list) -> reurn type
{
   function body
}
  • capture list:捕获列表,指 lambda 表达式所在函数中定义的局部变量的列表,通常为空。lambda 表达式内部函数体在默认情况下是不能够使用函数体外部的变量的,但如果lambda 函数体中用到了 lambda 表达式所在函数的局部变量,必须捕获该变量,即将此变量写在捕获列表中。捕获方式分为:引用捕获方式 [&]、值捕获方式 [=]。
  • return type、parameter list、function body:分别表示返回值类型、参数列表、函数体,和普通函数一样。

使用场景:排序算法

bool compare(int& a, int& b)
{
    return a > b;
}

int main(void)
{
    int data[6] = { 3, 4, 12, 2, 1, 6 };
    vector<int> testdata;
    testdata.insert(testdata.begin(), data, data + 6);
    
    // 排序算法
    sort(testdata.begin(), testdata.end(), compare);    // 降序

    // 使用lambda表达式
    sort(testdata.begin(), testdata.end(), [](int a, int b){ return a > b; });  // 降序

    return 0;
}   

4.3 explicit 的作用(如何避免编译器进行隐式类型转换)

用来声明类构造函数是显示调用的,而非隐式调用,可以阻止调用构造函数时进行隐式转换。只可用于修饰单参构造函数,因为无参构造函数和多参构造函数本身就是显示调用的,再加上 explicit 关键字也没有什么意义。

隐式转换:

#include <iostream>
#include <cstring>
using namespace std;

class A
{
public:
    int var;
    A(int tmp)
    {
        var = tmp;
    }
};
int main()
{
    A ex = 10; // 发生了隐式转换
    return 0;
}

上述代码中,A ex = 10; 在编译时,进行了隐式转换,将 10 转换成 A 类型的对象,然后将该对象赋值给 ex,等同于如下操作:

为了避免隐式转换,可用 explicit 关键字进行声明:

#include <iostream>
#include <cstring>
using namespace std;

class A
{
public:
    int var;
    explicit A(int tmp)
    {
        var = tmp;
        cout << var << endl;
    }
};
int main()
{
    A ex(100);
    A ex1 = 10; // error: conversion from 'int' to non-scalar type 'A' requested
    return 0;
}

4.4 C 和 C++ static 的区别

  • 在 C 语言中,使用 static 可以定义静态局部变量、静态全局变量、静态函数
  • 在 C++ 中,使用 static 可以定义静态局部变量、静态全局变量、静态函数、静态成员变量和静态成员函数

4.5 static 的作用

  • 保持变量内容持久:static 作用于局部变量,改变了局部变量的生存周期,使得该变量存在于定义后直到程序运行结束的这段时间。
  • 隐藏:static作用于全局变量和函数,改变了全局变量和函数的作用域,使得全局变量和函数只能在定义它的文件中使用,在源文件中不具有全局可见性。(注:普通全局变量和函数具有全局可见性,即其他的源文件也可以使用。)
  • static 作用于类的成员变量和类的成员函数,使得类变量或者类成员函数和类有关,也就是说可以不定义类的对象就可以通过类访问这些静态成员。注意:类的静态成员函数中只能访问静态成员变量或者静态成员函数,不能将静态成员函数定义成虚函数

4.6 static在类中用法

static 静态成员变量

  1. 声明和初始化:静态成员变量是在类内进行声明,在类外进行初始化,在类外进行定义和初始化的时候不要出现static关键字和private、public、protected 访问权限。
  2. 作用域:静态成员变量相当于类作用域中的全局变量,被类的所有对象所共享,包括派生类的对象。
  3. 参数:静态成员变量可以作为成员函数的参数,而普通成员变量不可以。
  4. **类型:**静态成员变量的类型可以是所属类的类型,而普通成员变量的类型只能是该类类型的指针或引用。
#include <iostream>
using namespace std;

class A
{
public:
    static int s_var;
    int var;
    void fun1(int i = s_var); // 正确,静态成员变量可以作为成员函数的参数
    void fun2(int i = var);   //  error: invalid use of non-static data member 'A::var'
};

#include <iostream>
using namespace std;

class A
{
public:
    static A s_var; // 正确,静态数据成员
    A var;          // error: field 'var' has incomplete type 'A'
    A *p;           // 正确,指针
    A &var1;        // 正确,引用
};

static静态成员函数

  1. **声明:**静态成员函数不能声明成虚函数和const 函数。
  2. 作用域:静态成员函数做为类作用域的全局函数
  3. **调用:**只能访问静态成员变量或者静态成员函数,因为静态成员函数没有this指针。

4.7 const在类中的用法

const成员变量:

  • **声明和初始化:**const成员变量只能在类内声明,在构造函数初始化列表中初始化
  • **常量特性:**const成员变量只在某个对象的生存周期内是常量,对于整个类而言却是可变的,因为类可以创建多个对象,不同对象的const成员变量的值可能不同。

const成员函数:

  • 成员变量:不能修改成员变量的值,除非有 mutable 修饰;只能访问成员变量。
  • **成员函数:**不能调用非常量成员函数,以防修改成员变量的值。

4.8 用define实现比较大小,以及两个数中的最小值

#include <iostream>
#define MAX(X, Y) ((X)>(Y)?(X):(Y))
#define MIN(X, Y) ((X)<(Y)?(X):(Y))
using namespace std;

int main ()
{
    int var1 = 10, var2 = 100;
    cout << MAX(var1, var2) << endl;
    cout << MIN(var1, var2) << endl;
    return 0;
}
/*
程序运行结果:
100
10
*/

4.9 define和const的区别

区别:

  • **编译阶段:**define是在编译预处理阶段进行替换,const是在编译阶段确定其值。
  • **安全性:**define 定义的宏常量没有数据类型,不会进行数据类型检查;const 定义的常量是有数据类型的,会进行数据类型检查,可以避免一些低级的错误。
  • 内存占用:define 定义的宏常量在程序中使用多少次就会进行多少次替换,内存中有多个备份,存储在代码区;const 定义的常量在程序运行过程中只有一份,存储在静态存储区
  • 调试:define 定义的宏常量不能调试,因为在预处理阶段就已经进行替换了;const定义的常量可以进行调试。

const 的优点:

  • 有数据类型,会进行数据类型检查。
  • 占用较少的空间。
  • 可调式。

4.10 define和typedef的区别

  • 编译阶段:define是在编译预处理阶段进行替换,typedef在编译时进行处理。
  • **安全性:**define 定义的宏常量没有数据类型,不会进行数据类型检查;typedef有类型检查功能,只能用来给已经存在的类型起别名。
  • **作用域:**define没有作用域,只要是之前预定义过的宏,在以后的程序中都可以使用,而typedef有作用域。
  • **指针的操作:**typedef 和 define 在处理指针时不完全一样
#include <iostream>
#define INTPTR1 int *
typedef int * INTPTR2;

using namespace std;

int main()
{
    INTPTR1 p1, p2; // p1: int *; p2: int
    INTPTR2 p3, p4; // p3: int *; p4: int *

    int var = 1;
    const INTPTR1 p5 = &var; // 相当于 const int * p5; 指针常量,底层const,指针指向地址中存储的值不能改变,指针指向的地址可以改变。
    const INTPTR2 p6 = &var; // 相当于 int * const p6; 常量指针,不能改变指针指向的地址,可以改变指针指向地址中存储的值。
    return 0;
}

4.11 define和inline的区别

  1. 编译阶段:define是在编译预处理阶段进行替换,inline在编译阶段将函数体嵌入到每一个调用该函数的语句块中
  2. 安全性:define 定义的宏常量没有数据类型,不会进行数据类型检查。内联函数是真正的函数,会对参数的类型、函数体内的语句编写是否正确等进行检查。
#include <iostream>

#define MAX(a, b) ((a) > (b) ? (a) : (b))

using namespace std;

inline int fun_max(int a, int b)
{
    return a > b ? a : b;
}

int main()
{
    int var = 1;
    cout << MAX(var, 5) << endl;     
    cout << fun_max(var, 0) << endl; 
    return 0;
}
/*
程序运行结果:
5
1
*/

4.12 inline原理、与普通函数的区别、使用方法及限制

原理:用于定义内联函数,在编译阶段将函数体嵌入到每一个调用该函数的语句块中。

与普通函数的区别:

  • **内联函数:**像普通函数一样被调用,但是在调用时直接在调用点处展开,避免了函数参数的压栈操作,减少了调用的开销,提高了程序的运行效率。
  • **普通函数:**调用时转移到被调用函数所存放的内存地址,当函数执行完后,返回到先前的地方。转移操作需要保护现场,被调用函数执行完后,再恢复现场,该过程需要较大的资源开销。

使用方法:

  • **类内定义成员函数默认是内联函数:**在类内定义成员函数,可以不用在函数头部加 inline 关键字,因为编译器会自动将类内定义的函数(构造函数、析构函数、普通成员函数等)声明为内联函数,代码如下:
#include <iostream>
using namespace std;

class A{
public:
    int var;
    A(int tmp){ 
      var = tmp;
    }    
    void fun(){ 
        cout << var << endl;
    }
};
  • **类外定义成员函数,若想定义为内联函数,需用关键字声明,**可以在类内声明时不加 inline 关键字,而在类外定义函数时加上 inline 关键字。
#include <iostream>
using namespace std;

class A{
public:
    int var;
    A(int tmp){ 
      var = tmp;
    }    
    void fun();
};

inline void A::fun(){
    cout << var << endl;
}

限制:inline对编译器的一种请求,编译器可能拒绝这种请求:

  • 不能存在任何形式的循环语句
  • 不能存在过多的条件判断语句
  • 函数体结构不能过于庞大
  • 内联函数声明必须在调用语句之前

4.13 new和malloc的区别,delete和free的区别

  • 类型:new、delete是关键字,而malloc、free是库函数。
  • 申请空间的大小:new申请空间时无需指定所申请空间的大小,编译器会根据类型自行计算;malloc 在申请空间时需要指定所申请空间的大小。
  • **返回类型:**new申请空间时,返回类型是对象的指针类型,无需强制类型转换;malloc申请空间时,返回的是void*类型,需要进行强制类型的转换。
  • 分配失败:new分配失败时,抛出 bad_alloc异常,malloc分配失败时返回空指针。
  • **调用步骤:**对于自定义的类,new首先调用operator new()函数申请空间(底层通过malloc实现),然后调用构造函数进行初始化;delete首先调用析构函数,然后调用operator delete()释放空间(底层通过free实现)。malloc、free 无法进行自定义类型的对象的构造和析构。

4.14 malloc的原理?

malloc在申请内存时,一般会通过系统函数brk或者mmap系统调用来申请内存。当申请内存小于128k时,会使用系统函数brk在堆区分配;而当申请内存大于128k时,会使用mmap系统调用在映射区分配。

4.15 new有哪几种类型

  • plain new,我们常用的new,在内存分配失败时抛出bad_alloc
  • nothrow new,在内存分配失败时抛出NULL
  • placement new,允许在一块已经分配的内存上重新构造对象,不存在内存分配失败的情况,注意析构时要显示的调用析构函数,不能用delete,因为placement new构造的对象大小不一定等于原来分配的内存大小,用delete可能会导致内存泄漏

4.16 被free回收的内存会立即返回给操作系统吗

不会,被free回收的内存会首先被ptmalloc使用双向链表保存起来,当用户下一次申请内存的时候,会尝试从这些内存中寻找合适的返回。这样就避免了频繁的系统调用,占用过多的系统资源。同时ptmalloc也会尝试对小块内存进行合并,避免过多的内存碎片。

4.17 delete 和 delete[] 的区别?

  • delete用来释放单个对象所占的空间,只会调用一次析构函数;
  • delete [] 用来释放数组空间,会对数组中的每个成员都调用一次析构函数。

4.18 union和struct的区别

说明:union 是联合体,struct 是结构体。

区别:

  • **有效:**联合体只有一个有效的成员;而结构体所有的成员都有效。
  • **赋值:**对联合体的一个成员赋值,将会覆盖其他成员的值,而对结构体的对不同成员赋值,相互不影响。
  • **大小:**联合体的大小为其内部所有变量的最大值;结构体分配内存的大小遵循内存对齐原则。

4.19 C和C++ struct的区别?

  • **访问权限:**C语言中struct没有访问权限,C++中struct和class一样有访问权限。
  • **成员函数:**C语言中不能定义成员函数,C++中struct和class一样可以定义成员函数。
  • **定义:**C语言中定义struct类型的变量时,需要加上struct关键字,而C++中不用加该关键字。

4.20 C++为什么有了class还保留struct?

C++是在C语言的基础上发展起来的,为了与C语言兼容,C++中保留了 struct。

4.21 C++中class和struct的相同点和不同点

相同点:

  • struct和class都可以自定义数据类型,也支持继承操作。

不同点:

  • **权限:**struct中默认的访问权限是public,默认的继承权限也是public;class 中默认的访问权限是private,默认的继承权限也是private。
  • **模板参数:**class可以用于定义模板参数,struct 不能用于定义模板参数。

当class继承struct或者struct继承class时,默认的继承权限取决于派生类的默认继承

struct A{};
class B : A{}; // private 继承 
struct C : B{}; // public 继承

4.22 extern C 的作用?

加上extern "C"后,会指示编译器这部分代码按C语言而不是C++的方式进行编译。由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包括函数名。

// 可能出现在 C++ 头文件<cstring>中的链接指示
extern "C"{
    int strcmp(const char*, const char*);
}

第五章 语言特性相关

5.1 左值和右值的区别?如何判断左值和右值?左值引用和右值引用的区别?

左值和右值的区别?

  • 左值:变量的形式存在,指向内存,生命周期比较长,我们可以对左值进行各种操作;
  • 右值:通常以常量的形式存在,是一个临时值,生命周期很短。

如何判断某个值是左值还是右值:

  • 位置:可以在等号左侧的表达式是左值;只能位于等号右侧的表达式就是右值;
  • 地址:可以获取到存储地址的是左值,反之是右值;

左值引用和右值引用的区别:

  • 左值引用不能绑定到字面常量或返回右值的表达式。
  • 右值引用必须绑定到右值的引用,通过 && 获得。右值引用只能绑定到一个将要销毁的对象上,因此可以自由地移动其资源。
#include <iostream>
using namespace std;

void fun1(int& tmp) 
{ 
  cout << "fun1(int& tmp):" << tmp << endl; 
} 

void fun2(int&& tmp) 
{ 
  cout << "fun2(int&& tmp)" << tmp << endl; 
} 

int main() 
{ 
  int var = 11; 
  fun1(12); // error: cannot bind non-const lvalue reference of type 'int&' to an rvalue of type 'int'
  fun1(var);
  fun2(1); 
}

5.2 如何将左值转换成右值引用?

std::move 可以将一个左值强制转化为右值引用,包含在 utility 头文件中。函数原型如下:

template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
   return static_cast<typename remove_reference<T>::type &&>(t);
}

// remove_reference相关
//原始的,最通用的版本
template <typename T> struct remove_reference{
    typedef T type;  //定义 T 的类型别名为 type
};
 
//部分版本特例化,将用于左值引用和右值引用
template <class T> struct remove_reference<T&> //左值引用
{ typedef T type; }
 
template <class T> struct remove_reference<T&&> //右值引用
{ typedef T type; }   
  
//举例如下,下列定义的a、b、c三个变量都是int类型
int i;
remove_refrence<decltype(42)>::type a;             //使用原版本,
remove_refrence<decltype(i)>::type  b;             //左值引用特例版本
remove_refrence<decltype(std::move(i))>::type  b;  //右值引用特例版本

引用折叠原理

  • 右值传递给上述函数的形参 T&& 依然是右值,即 T&& && 相当于 T&&。
  • 左值传递给上述函数的形参 T&& 依然是左值,即 T&& & 相当于 T&。

move原理

  • 利用引用折叠原理:右值经过 T&& 变为右值引用,而左值经过 T&&变为左值引用,以保证模板可以传递任意实参,且保持类型不变;
  • 然后通过 remove_refrence 移除引用,得到具体的类型 T;
  • 最后通过 static_cast<> 进行强制类型转换,返回右值引用。

5.3 什么是this指针?

this指针:指向类对象的指针常量。空指针是没有this指针的

#include <iostream>
#include <cstring>
using namespace std;

class A
{
public:
    void set_name(string tmp)
    {
        this->name = tmp;
    }
    void set_age(int tmp)
    {
        this->age = age;
    }
    void set_sex(int tmp)
    {
        this->sex = tmp;
    }
    void show()
    {
        cout << "Name: " << this->name << endl;
        cout << "Age: " << this->age << endl;
        cout << "Sex: " << this->sex << endl;
    }

private:
    string name;
    int age;
    int sex;
};

int main()
{
    A *p = new A();
    p->set_name("Alice");
    p->set_age(16);
    p->set_sex(1);
    p->show();

    return 0;
}

#include <iostream>
using namespace std;
class A {
public:
    void func() {
        cout << "test" << endl;
        m_ = 10;  // 有这句话会报错,没有的话不会报错。因为没有这句话不涉及this指针,不管类对象是什么类型都会正确执行;有的话涉及this指针,空指针是没有this指针的,所以会报错。
    }
public:
    int m_;
};
int main() {

    A* p = nullptr;
    p->func();
}

5.4 什么是野指针和悬空指针?

野指针指的是指针变量未初始化,只需要在定义指针变量时及时初始化。悬空指针指的是指针free或delete后没有及时置空,指针仍然指向之前分配的内存,如果这块内存暂时可以被程序访问并且不会造成冲突,那么之后使用该指针并不会引发错误。为了避免出现“悬空指针”引发不可预知的错误,可以在释放后立即置空

void *p; 
// 此时 p 是野指针

void *p = malloc(size);
free(p); 
// 此时,p 指向的内存空间已释放, p 就是悬空指针

5.5 指针和引用的区别?

  • 指针存放某个对象地址,本身就是变量(命了名的变量),本身就有地址,所以可以有指向指针的指针。可变,包括其指向地址的改变和其指向地址中存放数据的改变。
  • 引用是变量的别名,从一而终,不可变,必须要初始化
  • 不存在指向空值的引用,但存在指向空值的指针。

5.6 指针常量和常量指针的区别

  • 指针常量:指针指向地址中存储的值不能改变,指针指向的地址可以改变,写成int const* p或const int* p(即*靠近变量名),属于底层const
  • 常量指针:不能改变指针指向的地址,可以改变指针指向地址中存储的值,写成int* const p(即*靠近数据类型),属于顶层const

5.7 指针函数和函数指针的区别

**指针函数:**本质是一个函数,只不过该函数的返回值是一个指针。

#include <iostream>
using namespace std;

struct Type
{
  int var1;
  int var2;
};

Type * fun(int tmp1, int tmp2){
    Type * t = new Type();
    t->var1 = tmp1;
    t->var2 = tmp2;
    return t;
}

int main()
{
    Type *p = fun(5, 6);
    return 0;
}

函数指针:函本质是一个指针,只不过这个指针指向一个函数。函数指针即指向函数的指针。

#include <iostream>
using namespace std;
int fun1(int tmp1, int tmp2)
{
  return tmp1 * tmp2;
}
int fun2(int tmp1, int tmp2)
{
  return tmp1 / tmp2;
}

int main()
{
  int (*fun)(int x, int y); 
  fun = fun1;
  cout << fun(15, 5) << endl; 
  fun = fun2;
  cout << fun(15, 5) << endl; 
  return 0;
}
/*
运行结果:
75
3
*/

5.8 强制类型转换有哪几种?

  • static_cast:

    • 用于基本数据类型的转换;
    • 用于基类和派生类之间指针或者引用的转换(不要求必须包含虚函数),其中上行转换(派生类的指针或引用转换成基类)是安全的,下行转换(基类的指针或引用转换成派生类)由于没有动态类型检查,所以是不安全的,最好用 dynamic_cast 进行下行转换。
    • 可以将任何类型的表达式转化成 void 类型。
  • const_cast:只能用于去除指针或引用的常量性,不能用于去掉变量的常量性。

  • reinterpret_cast:改变指针或引用的类型,可以将指针或引用转换为一个足够长度的整型或将整型转化为指针或引用类型。

#include <iostream>
using namespace std;

int main() {
    int a = 0x1234;
    cout << reinterpret_cast<char*>(&a) << endl;
}

输出可能是一些乱码字符或直接崩溃,因为reinterpret_cast<char*>(&a)int类型的指针转换为char*类型的指针,然后cout将其视为一个以null终止的C风格字符串,而0x1234并不是一个有效的null终止C风格字符串。

  • dynamic_cast

    • 其他三种都是编译时完成的,动态类型转换是在程序运行时处理的,运行时会进行类型检查。
    • 不能用于基本数据类型的转换。只能用于带有虚函数的基类或派生类的指针或者引用对象的转换,转换成功返回对应类型的指针或引用,转换失败返回 NULL;
    • 在向上进行转换时,和 static_cast 效果一样的;但在下行转换时,只有当要转换的指针指向的对象类型和转化以后的对象类型相同时,才会成功转换。
#include <iostream>
#include <cstring>

using namespace std;

class Base
{
public:
    virtual void fun()
    {
        cout << "Base::fun()" << endl;
    }
};

class Derive : public Base
{
public:
    virtual void fun()
    {
        cout << "Derive::fun()" << endl;
    }
};

int main()
{
    Base *p1 = new Derive();
    Base *p2 = new Base();
    Derive *p3 = new Derive();

    //转换成功
    p3 = dynamic_cast<Derive *>(p1);
    if (p3 == NULL)
    {
        cout << "NULL" << endl;
    }
    else
    {
        cout << "NOT NULL" << endl; // 输出
    }

    //转换失败
    p3 = dynamic_cast<Derive *>(p2);
    if (p3 == NULL)
    {
        cout << "NULL" << endl; // 输出
    }
    else
    {
        cout << "NOT NULL" << endl;
    }

    return 0;
}

5.9 参数传递时,值传递、引用传递、指针传递的区别?

  • 值传递:形参是实参的拷贝,函数对形参的操作不会影响实参。
  • 指针传递:本质上是值传递,只不过拷贝的是指针,实参和形参是不同的指针,通过指针可以间接的访问指针所指向的对象,从而可以修改它所指对象的值。
  • 引用传递:形参是引用类型,函数对形参的操作会影响实参。

在传递参数时,什么时候使用指针,什么时候使用引用

  • 数据对象是数组时只能使用指针
  • 对栈空间大小比较敏感(比如递归)时使用引用,使用引用传参不需要创建临时变量,开销更小;类对象作为参数传递时使用引用,这是C++类对象传递的标准方式

5.10 什么是模板?如何实现?

模板:创建类或者函数的蓝图或者公式,分为函数模板和类模板。实现方式:模板定义以关键字 template 开始,后跟一个模板参数列表。

  • 模板参数列表不能为空;
  • 模板类型参数前必须使用关键字 class 或者 typename,在模板参数列表中这两个关键字含义相同,可互换使用。且有几个参数就添加关键字,关键字不能省略
template <typename T, typename U, ...>

函数模板:通过定义一个函数模板,可以避免为每一种类型定义一个新函数。

  • 函数模板定义:模板类型参数可以用来指定返回类型或函数的参数类型,以及在函数体内用于变量声明或类型转换。
  • 函数模板实例化:当调用一个模板时,编译器用函数实参来推断模板实参,从而使用实参的类型来确定绑定到模板参数的类型。
#include<iostream>

using namespace std;

template <typename T>
T add_fun(const T & tmp1, const T & tmp2){
    return tmp1 + tmp2;
}

int main(){
    int var1, var2;
    cin >> var1 >> var2;
    cout << add_fun(var1, var2);

    double var3, var4;
    cin >> var3 >> var4;
    cout << add_fun(var3, var4);
    return 0;
}

类模板:类似函数模板,类模板以关键字 template 开始,后跟模板参数列表。但是,编译器不能为类模板推断模板参数类型,需要在使用该类模板时,在模板名后面的尖括号中指明类型

#include <iostream>

using namespace std;

template <typename T>
class Complex
{
public:
    //构造函数
    Complex(T a, T b)
    {
        this->a = a;
        this->b = b;
    }

    //运算符重载
    Complex<T> operator+(Complex &c)
    {
        Complex<T> tmp(this->a + c.a, this->b + c.b);
        cout << tmp.a << " " << tmp.b << endl;
        return tmp;
    }

private:
    T a;
    T b;
};

int main()
{
    Complex<int> a(10, 20);
    Complex<int> b(20, 30);
    Complex<int> c = a + b;

    return 0;
}

5.12 函数模板和类模板的区别?

  • 实例化方式不同:函数模板实例化由编译程序在处理函数调用时自动完成,类模板实例化需要在程序中显式指定。
  • 实例化的结果不同:函数模板实例化后是一个函数,类模板实例化后是一个类。
  • 默认参数:类模板在模板参数列表中可以有默认参数。
  • 特化:函数模板只能全特化;而类模板可以全特化,也可以偏特化。
  • 调用方式不同:函数模板可以隐式调用,也可以显式调用;类模板只能显式调用。

5.13 什么是可变参数模板?

可变参数模板:接受可变数目参数的模板函数或模板类。将可变数目的参数被称为参数包,包括模板参数包和函数参数包。

  • 模板参数包:表示零个或多个模板参数;
  • 函数参数包:表示零个或多个函数参数。

用省略号来指出一个模板参数或函数参数表示一个包,在模板参数列表中,class… 或 typename… 指出接下来的参数表示零个或多个类型的列表;一个类型名后面跟一个省略号表示零个或多个给定类型的非类型参数的列表。当需要知道包中有多少元素时,可以使用 sizeof… 运算符

template <typename T, typename... Args> // Args 是模板参数包
void foo(const T &t, const Args&... rest); // 可变参数模板,rest 是函数参数包

#include <iostream>

using namespace std;

template <typename T>
void print_fun(const T &t)
{
    cout << t << endl; // 最后一个元素
}

template <typename T, typename... Args>
void print_fun(const T &t, const Args &...args)
{
    cout << t << " ";
    print_fun(args...);
}

int main()
{
    print_fun("Hello", "wolrd", "!");
    return 0;
}
/*运行结果:
Hello wolrd !
*/

说明:可变参数函数通常是递归的,第一个版本的 print_fun 负责终止递归并打印初始调用中的最后一个实参。第二个版本的 print_fun 是可变参数版本,打印绑定到 t 的实参,并用来调用自身来打印函数参数包中的剩余值。

5.14 什么是模板特化?为什么特化?

模板特化的原因:模板并非对任何模板实参都合适、都能实例化,某些情况下,通用模板的定义对特定类型不合适,可能会编译失败,或者得不到正确的结果。因此,当不希望使用模板版本时,可以定义类或者函数模板的一个特例化版本。

模板特化:模板参数在某种特定类型下的具体实现。分为函数模板特化和类模板特化

  • 函数模板特化:将函数模板中的全部类型进行特例化,称为函数模板特化。
  • 类模板特化:将类模板中的部分或全部类型进行特例化,称为类模板特化。

特化分为全特化和偏特化:

  • 全特化:模板中的模板参数全部特例化。
  • 偏特化:模板中的模板参数只确定了一部分,剩余部分需要在编译器编译时确定。

说明:要区分下函数重载与函数模板特化定义函数模板的特化版本,本质上是接管了编译器的工作,为原函数模板定义了一个特殊实例,而不是函数重载,函数模板特化并不影响函数匹配。

#include <iostream>
#include <cstring>

using namespace std;
//函数模板
template <class T>
bool compare(T t1, T t2)
{
    cout << "通用版本:";
    return t1 == t2;
}

template <> //函数模板特化
bool compare(char *t1, char *t2)
{
    cout << "特化版本:";
    return strcmp(t1, t2) == 0;
}

int main(int argc, char *argv[])
{
    char arr1[] = "hello";
    char arr2[] = "abc";
    cout << compare(123, 123) << endl;
    cout << compare(arr1, arr2) << endl;

    return 0;
}
/*
运行结果:
通用版本:1
特化版本:0
*/

5.15 泛型编程如何实现?

泛型编程实现的基础:模板。模板是创建类或者函数的蓝图或者说公式,当时用一个 vector 这样的泛型,或者 find 这样的泛型函数时,编译时会转化为特定的类或者函数。

泛型编程涉及到的知识点较广,例如:容器、迭代器、算法等都是泛型编程的实现实例。面试者可选择自己掌握比较扎实的一方面进行展开。

  • 容器:涉及到 STL 中的容器,例如:vector、list、map 等,可选其中熟悉底层原理的容器进行展开讲解。
  • 迭代器:在无需知道容器底层原理的情况下,遍历容器中的元素。
  • 模板:可参考本章节中的模板相关问题。

5.16 include " " 和 <> 的区别

  • 查找文件的位置:include<文件名>在标准库头文件所在的目录中查找,如果没有,再到当前源文件所在目录下查找;#include"文件名" 在当前源文件所在目录中进行查找,如果没有,再到系统目录中查找。
  • 使用习惯:对于标准库中的头文件常用 include<文件名>,对于自己定义的头文件,常用 #include"文件名"

5.17 C++异常处理方法

  • try,throw和catch关键字:程序先执行try语句块,如果没有throw抛出异常,则不会进入任何catch语句块,如果有的话用catch精准捕获throw异常,如果匹配不到catch参数就报错,可以使用catch(…)捕获任何异常

  • 函数异常声明列表:程序员在定义函数的时候知道函数可能发生的异常,可以在函数声明和定义时,指出所抛出的异常列表

  • C++标准异常类exception

    • bad_typeid:使用typeid运算符,如果操作数是一个多态类的指针,且值为NULL,则会抛出bad_typeid异常
    • bad_cast:使用dynamic_cast从多态基类对象到派生类引用强制类型转换时,如果不安全则会抛出bad_cast
    • bad_alloc:new动态分配内存时,没有足够的内存空间会抛出bad_alloc
    • out_of_range:用vector或string的at成员根据下标访问元素时越界会抛出out_of_range
int fun() throw(int, double, A, B, C) {...};  // A,B,C是类型

5.18 runtime-error使用

只有在运行时才能检测到的错误,包括range_error(生成的结果超出有意义的值域范围)、overflow_error(上溢)、underflow_error(下溢)、system_error(系统错误),没有默认构造函数。

5.19 C++函数调用过程

在C++中,函数调用是指通过函数名称参数来执行函数内部代码的过程。当程序调用一个函数时,CPU首先需要将函数的参数和返回地址等信息保存到栈空间中,并跳转到函数的入口处开始执行函数代码。当函数执行完毕后,程序又会从函数返回的地方继续执行。

易错点:在设定函数的参数默认值后,该参数后面定义的所有参数都必须设定默认值。

5.20 C++同步和异步调用是什么?

同步:当函数发起调用时,在没得到结果之前,该调用永不返回

异步:当函数发起调用时,不用等待结果,就去执行下面的内容

第六章 STL相关

6.1 STL六大组件是什么

  1. 容器:各种数据结构,如vector、list、deque、set、map等,用来存放数据,从实现角度来看是类模板

    1. 顺序容器:vector、list、deque
    2. 关联容器:pair、map、set、multimap、multiset
  2. 算法:各种常用算法,如sort、find、copy

  3. 迭代器

  4. 仿函数

  5. 适配器

  6. 分配器

6.2 什么是哈希表?哈希表的原理是什么?什么是哈希冲突?如何解决哈希冲突?C++用什么数据结构实现哈希表?(高频)

定义:哈希表是根据关键码的值而直接进行访问的数据结构。

原理:哈希表把Key通过哈希函数转换成一个整型数字,该数字对数组长度取余,取余结果就当作数组的下标,将value存储在以该数字为下标的数组空间里。当使用哈希表进行查询的时候,利用哈希函数将key转换为对应的数组下标,从而获得value。

哈希冲突:不同的key映射到了同一位置的下标,这一现象叫做哈希碰撞

如何解决:拉链法(同一位置下标进行链表连接)和线性探索法(如果原下标位置有value,就向下找一个空位放置key)。如何访问呢?

C++底层是哈希表的数据结构:unordered_set和unordered_map
在这里插入图片描述
在这里插入图片描述

6.3 set、map相关

unordered_map和map的区别?底层是什么数据结构?map的key有序体现在哪里?

unordered_map的key是无序的,map的key是有序的;unordered_map的底层是哈希表,map的底层是红黑树;unordered_map的查询和增删效率都是O(1),map都是O(logn)。std::map 中的 key 默认是按照升序排序的。当插入新的键值对时,std::map 会根据 key 进行排序,确保 key 始终按照升序排列。

第七章 设计模式

7.1 单例模式

定义:只包含一个被称为单例的特殊类,它的目的是保证一个类仅有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。

  • 饿汉模式
class Singleton {
public:
    static Singleton* getInstance() {  // 静态方法,返回唯一实例
        return instance;
    }
private:
    Singleton() {}  // 私有的构造函数,防止外部创建实例
    static Singleton* instance;  // 私有的静态成员变量,保存唯一实例
};

// 下面这个静态成员变量在类加载的时候就已经初始化好了
Singleton* Singleton::instance = new Singleton();

int main() {
    // 通过getInstance获取实例
    Singleton* singleton = Singleton::getInstance();
    return 0;
}
  • 懒汉模式
class Singleton {
public:
    static Singleton* getInstance() {  // 静态方法,返回唯一实例
        if (instance == nullptr) instance = new Singleton();
        return instance;
    }
private:
    Singleton() {}  // 私有的构造函数,防止外部创建实例
    static Singleton* instance;  // 私有的静态成员变量,保存唯一实例
};

Singleton* Singleton::instance = nullptr;

int main() {
    // 通过getInstance获取实例
    Singleton* singleton = Singleton::getInstance();
    return 0;
}

饿汉模式和懒汉模式的区别:

在饿汉模式中,单例对象在程序启动时就会被立即创建和初始化,不管是否会被用到。优点是线程安全,因为实例已经在使用之前就被创建,不会存在多线程同时创建的问题。缺点是可能会造成不必要的资源浪费,尤其是在单例对象的初始化过程较为耗时或占用较多资源的情况下。

在懒汉模式中,单例对象的创建被延迟到了真正被需要的时候才进行。优点是可以避免不必要的资源浪费,只有当需要使用单例对象时才会进行实例化。缺点是懒汉模式需要考虑多线程并发访问问题。

第八章 其他

8.1 原码、反码和补码

  • 原码:十进制数据的二进制表现形式就是原码,原码最左边的一个数字就是符号位,0为正,1为负
  • 反码:正数的反码是其本身(等于原码),负数的反码是符号位保持不变,其余位取反。
  • 补码:正数的补码是其本身,负数的补码等于其反码+1
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值