首页 简历|笔试面试

C++面试总结

  • 25年9月11日 发布
  • 12.61MB 共147页
C++面试总结C++面试总结C++面试总结C++面试总结C++面试总结

C++ 面试总结

一、语言基础

1.1 指针

智能指针

指针和引用的区别

数组、指针区别

数组指针、指针数组区别

迭代器、指针区别

strcpy 和 memcpy 区别

1.2 内存管理与分配

内存分配与存储区

malloc / free

、 区别

volatile extern

拷贝构造函数

预处理、编译、汇编、链接

define/const/typedef/inline 区别

、 区别

const static

声明和定义的区别

1.3 编程特性

C++11新特性

C 和 C++ 区别

C++ 和 Java 区别

C++ 和 Pyhton 区别

1.4 类与面向对象

初始化

重载、重写以及重定义的区别

四种强制类型转换

结构体和类的区别

类的大小 与 内存对齐

1

面向对象三大特征

虚函数

1.5 基础概念

简述 fork/wait/exec 函数

1.6 STL

模板

vector

list

deque

priority_queue

map 和 set

hashtable

1.7 算法

memcpy 实现

读写锁

死锁复现代码

二叉树序列号与反序列化

生产者消费者模式

二叉树-前中后迭代算法模板

手写智能指针

十大排序算法(升序实现)

二、计算机网络

2.1 OSI 七层模型

2.2 TCP/IP 四层模型

2.3 TCP 和 UDP

TCP 协议

TCP 流量控制

TCP 拥塞控制

TCP 三次握手

TCP 四次挥手

TCP 粘包问题

TCP Socket交互流程

2

UDP 协议

2.4 HTTP

HTTP 请求方法

HTTP 状态码

HTTP 缓存

HTTP 报文格式

Cookie 和 Session 区别

Session 的实现原理

HTTP/1.1

HTTP/2.0

HTTP/3 / QUIC

2.5 HTTPS

SSL/TLS

RSA密钥交换算法

ECDHE 密钥交换算法

2.6 DNS

2.7 IP

2.8 WebSocket

2.9 DHCP

2.10 NET

2.11 IGMP

三、操作系统

3.1 用户态 和 内核态

执行系统调用时 OS 状态

CPU 中断

零拷贝

3.2 进程、线程、协程

进程与线程区别

线程的实现方式

线程安全

线程的同步方式

进程状态

3

进程/CPU调度算法

进程的分类

进程间的通信方式

协程

3.3 锁

类型

死锁

3.4 虚拟内存

虚拟地址空间

虚拟内存实现思路

内存管理方法

页面放置算法

页面置换算法

磁盘调度算法

内存不足会发生什么

3.5 缓存区

缓存区溢出

3.6 I/O 模型

I/O 模型类型

I/O 复用模型

Reactor 与 Proactor 模式

3.7 Copy on Write

3.8 Linux

启动过程

常用命令

3.9 硬件结构

CPU 缓存一致性

伪共享

一致性哈希

四、数据库 / MySQL

4.1 三大范式

4.2 数据库引擎

4

Innodb

MyISAM

4.3 数据库索引

索引分类

索引优缺点

索引使用场景

优化索引方法

数据库为什么用 B+ 树做索引

联合索引与最左匹配原则

count(...) 查询效率

4.4 关系型数据库、非关系型数据库

关系型数据库(SQL)

非关系型数据库(NOSQL)

4.5 数据库连接池

4.6 事务

ACID 特性 与 实现

两阶段提交

隔离性问题

隔离级别

4.7 数据库中的锁

意向锁(Intention Locks)

多版本并发控制(MVCC)

4.8 查询优化方法

mysql 深度分页

4.9 主从复制

五、设计模式

5.1 设计原则

5.2 常用设计模式

单例模式

简单工厂模式

模板方法

责任链模式

5

装饰器模式

六、Redis

6.1 基本数据结构(底层实现)

SDS

链表

字典

跳跃表

整数集合

压缩列表

6.2 对象

字符串对象

列表对象

哈希对象

集合对象 set

有序集合对象 zset

6.3 数据库

RDB 持久化

AOF 持久化

数据淘汰策略

6.4 客户端与服务器

复制

Sentinel(哨兵)

集群

6.5 事务

6.6 缓存管理

缓存穿透

缓存击穿

缓存雪崩

6.7 高并发系统设计

一、语言基础

6

1.1 指针

野指针:指针指向的位置是不可知的

悬空指针:指针最初指向的内存已经被释放了的一种指针

两种指针都指向无效内存空间,即不安全不可控。需要在定义指针后且在使用之前完成初始化或者使用

智能指针来避免

智能指针

智能指针的 作用 是管理指针,避免使用普通指针申请的空间在函数结束时忘记释放,造成内存泄漏。

因为智能指针是一个类,当超出类的作用域时,类会自动调用析构函数,析构函数有释放资源的操作。

类型:

● auto_ptr 采用所有权模式(C11已弃用),使得一个该类型指针可以剥夺另一个该类型指针的所有

权,使得被剥夺所有权的指针失效,缺点是使用被剥夺的指针存在潜在的内存崩溃问题。

● unique_ptr 实现独占式拥有,保证同一时间内只有一个智能指针可以指向该对象,避免上述内存崩

溃的出现。只能通过 new 来创建。

● shared_ptr 实现共享式拥有,可以用 new 来构造,还可以引入其他智能指针来构造。多个智能指

针可以指向相同的对象,该对象和其相关资源会在最后一个引用(use_count() 查看引用数)被销

毁时释放。当调用 release() 时,当前指针会释放资源所有权,计数减一。当计数等于0 时,资源

会被释放。资源消耗大,因为创建时会调用两次new(其中一次是引用计数对象)

● weak_ptr 是一种不控制对象生命周期的智能指针,它指向一个 shared_ptr 管理的对象。进行该对

象内存管理的是 shared_ptr,weak_ptr 只是提供了对管理对象的一个访问方法,目的是为了协助

shared_ptr 工作,它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造,且不会引起引用计

数值的变化。主要用来解决 空悬指针 和 循环引用 问题。空悬指针是两个共享指针同时引用同一个

对象,但是其中一个指针将该对象销毁,另一个指针会指向为空,可通过使用 weak_ptr 来判断指

向对象是否有效;循环引用是指两个对象之间相互引用,则引用计数将无法减为0,而其中一方改

为 weak_ptr 则可检测是否有效,且能将有效的指向对象转换为共享指针进行操作。[1] [2]

指针和引用的区别

● 指针和引用都是一种内存地址的概念,但是指针是一个实体,可以声明为 void;引用只是一个别

名,不能为 void。

● 引用内部其实是一个指针,引用比指针更安全;相对的,引用没有指针灵活。

● 引用和指针都可以作为参数传递给函数,用于更改函数作用域外的变量,在传递大对象时避免复

制,提升效率。作为参数时也有不同,传递指针的实质是传值,传递的值是指针的地址;传引用的

实质是传地址,传递的是变量的地址。

● 指针可以有多级指向,但是引用只能有一级引用。

7

● 引用是一块内存的别名,在添加到符号表时,是将“引用变量名-引用对象的地址”添加到符号表

中,符号表一经完成不能改变,所以引用只能在定义时被绑定到一块内存上,后续不能更改引用对

象。指针指向一块内存,其值是所指向的内存的地址,在编译的时候,则是将“指针变量名-指针变

量的地址”添加到符号表中,所以指针包含的内容是可以改变的,允许拷贝和赋值。

数组、指针区别

1. 数组存放的是数据,是直接访问数据的;指针存放的是变量的地址,是间接访问数据的;

2. 数组通常存储在静态存储区或栈上;指针可以随时地指向任意类型的内存块;

3. 用运算符 sizeof 可以计算出数组的容量(字节数);sizeof(p) 得到的是一个指针变量 p 的字节

数,而不是 p 指针所指向的内存容量;

4. char a[] = "hello" 数组指向每个数组元素;char *p = "hello" 而 p 指向字符串首地址;

数组指针、指针数组区别

数组指针(指向数组的指针)

1. 数组在内存中的表示:创建一个数组就是在内存里面开辟一块连续的空间;

1 int a[2][2] = {1,2,3,4}; // 这是一个2*2的二维数组

2 数组指针

int (*p)[2]; //

3 令 指向数组

p = a; // p a

指针数组(存放指针的数组)

指针数组的好处:

迭代器、指针区别

迭代器:用于提供一种方法顺序访问一个聚合对象中各个元素,而又无需暴露该对象的内部表示。

迭代器和指针区别:迭代器不是指针,是类模板,表现的像指针,其本质是封装了原生指针,提供了比

指针更高级的行为,相当于一种智能指针,可以根据不同类型的数据结构来实现递增、递减等操作。迭

代器返回的是对象引用而不是对象的值

strcpy 和 memcpy 区别

● 复制的内容不同。strcpy 只能复制字符串,而 memcpy 可以复制任意内容,例如字符数组、整

型、结构体、类等;

● 复制的方法不同。strcpy 不需要指定长度,它遇到被复制字符串的结束符 "" 才结束,所以容易

溢出。memcpy 则是根据其第 3 个参数决定复制的长度;

8

● 用途不同。通常在复制字符串时用 strcpy,而需要复制其他类型数据时则一般用 memcpy。

1.2 内存管理与分配

内存分配与存储区

C/C++内存分配有三种方式[1]:

● 从静态存储区分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存

在。例如全局变量,static变量。

● 从栈上分配。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些

存储单元自动被释放。分配的内存容量有限。

● 从堆上分配,亦称动态内存分配。程序在运行的时候用 malloc 或 new 申请任意大小的内存,程序

员自己负责在何时用 free 或 delete 释放内存。

C/C++程序编译时内存分为5大存储区:

● 栈区(stack)。编译器自动分配与释放,主要存放函数的参数值,局部变量值等,连续的内存空

间,由高地址向低地址扩展。

● 堆区(heap) 。由程序员分配与释放;不连续的空间,通过空闲链表进行连接。堆是低地址向高

地址扩展,空间较大。频繁地分配和释放不同大小的堆空间将会产生堆内碎块。

● 静态存储区。存放全局变量和静态变量;分为全局初始化区和全局未初始化区。

● 常量存储区。存放常量字符串;对于全局常量,编译器一般不分配内存,放在符号表中以提高访问

效率。

● 程序代码区。存放函数体的二进制代码。

malloc / free

malloc 申请内存的方式

● 方式一:通过 brk() 系统调用从堆分配内存:如果用户分配的内存小于 128 KB,就是通过 brk() 函

数将「堆顶」指针向高地址移动,获得新的内存空间。free 释放内存的时候,并不会把内存归还给

操作系统,而是缓存在 malloc 的内存池中,待下次使用;

● 方式二:通过 mmap() 系统调用在文件映射区域分配内存。free 释放内存的时候,会把内存归还给

操作系统,内存得到真正的释放。

malloc() 分配的是物理内存吗?

不是的,malloc() 分配的是虚拟内存。如果分配后的虚拟内存没有被访问的话,是不会将虚拟内存映射

到物理内存,这样就不会占用物理内存了。只有在访问已分配的虚拟地址空间的时候,操作系统通过查

找页表,发现虚拟内存对应的页没有在物理内存中,就会触发缺页中断,然后操作系统会建立虚拟内存

和物理内存之间的映射关系。

9

malloc(1) 会分配多大的虚拟内存?

malloc() 在分配内存的时候,并不是按用户预期申请的字节数来分配内存空间大小,而是会预分配更大

的空间。具体会预分配多大的空间,跟 malloc 使用的内存管理器有关。

new/delete、malloc/free区别

都可以分配和回收空间,new/delete 是运算符,malloc/free 是库函数。new得到的是经过初始化的空

间,而malloc得到的是未初始化的空间。

执行new有两个过程:

1. 分配未初始化的内存空间(malloc)。若出现问题则抛出异常

2. 使用对象的构造函数进行初始化。若出现异常则自动调用delete释放内存

执行delete有两个过程:

1. 使用析构函数对对象进行析构

2. 回收内存空间(free)

volatile、extern区别

volatile

1. 数据重新从内存中读取

2. 告诉编译器,不要对这个变量做优化,保证其顺序性

extern

1. 用在变量或函数的声明前,说明此变量/函数是在别处定义的,要在此处引用

2. 在 C++ 中调用 C 库函数,需要在 C++ 程序中用 extern "C" 声明要引用的函数,告诉链接器在链

接的时候用 C 语言规范来链接。主要原因是 C++ 和 C 程序编译完成后在目标代码中命名规则不

同,以此来解决名字匹配的问题

拷贝构造函数

类的对象需要拷贝时,会调用拷贝构造函数。

在未定义显式拷贝构造函数的情况下,系统会调用默认的拷贝函数(浅拷贝),它能够完成成员的一一

复制。但当数据成员中有指针时,如果采用简单的浅拷贝,则两类中的两个指针指向同一个地址,当对

象快要结束时,会调用两次析构函数。深拷贝和浅拷贝的区别 就在于深拷贝会在堆内存中另外申请空间

来存储数据,从而解决悬空指针的问题。简而言之,当数据成员中有指针时,必须要深拷贝才安全。

有三种情况会需要拷贝构造函数:

1. 一个对象以值传递的方式传入函数体,需要拷贝构造函数创建一个临时对象压入到栈空间

2. 一个对象以值传递的方式从函数返回,需要拷贝构造函数创建一个临时对象作为返回值

3. 一个对象需要通过另一个对象进行初始化

10

为什么拷贝构造函数必须是引用传递?

防止递归调用。当一个对象需要以值方式传递时,编译器会生成代码调用它的拷贝构造函生成一个副

本,如果该类的拷贝构造函数的参数不是引用传递,而是值传递,那么就需要创建传递给拷贝构造函数

参数的临时对象,而又一次调用该类的拷贝构造函数,这就是一个无限递归。

预处理、编译、汇编、链接

预处理阶段:预处理器根据 # 开头的命令,修改原始的程序,如把头文件插入到程序文本中,删除所有

的注释等。

编译阶段:编译过程就是把预处理完的文件进行一系列的词法分析、语法分析、语义分析等,最终产生

相应的汇编语言文件,不同的高级语言翻译的汇编语言相同。编译是对源文件分别进行的,每个源文件

都产生一个目标文件。

汇编阶段:把汇编语言代码翻译成目标机器指令。

链接阶段:将有关的目标文件和库文件相连接,使得所有的这些文件成为一个能够被操作系统装入执行

的统一整体。链接处理可分为两种:

● 静态链接:函数的代码将从其所在的静态链接库中被拷贝到最终的可执行文件中。这样程序在被执

行时会将其装入到该进程的虚拟地址空间中。静态链接库实际上是一个目标文件的集合,其中的每

个文件含有库中的一个或者一组相关函数的代码。

● 动态链接:函数的代码被 放到 称作是动态链接库或共享对象的某个目标文件中。链接程序要做的

只是在最终的可执行文件中记录下相对应的信息。在可执行文件被执行时,根据可执行程序中记录

的信息,将动态链接库的全部内容映射到相应运行进程的虚拟地址空间上。

对于可执行文件中的函数调用,可分别采用动态链接或静态链接的方法。使用动态链接能够使最终的可

执行文件比较短小,并且当共享对象被多个进程使用时能节约一些内存,因为在内存中只需要保存一份

此共享对象的代码。但并不是使用动态链接就一定比使用静态链接要优越。 [1]

define/const/typedef/inline 区别

const #define typedef inline

执行/作 编译阶段、链 预处理阶段(文本替换) 编译阶段 编译阶段(复制)

用时间 接阶段

类型检查 有 没有 有 有

功能 定义常量,无 定义类型别名、定义常量/变 定义类型 解决一些频繁调用的函数

法重定义 量、定义编译开关、可重定义 别名 大量消耗栈空间(栈内

(#undef) 存)的问题

作用域 没有 有

const、static 区别

11

static:控制变量的存储方式和可见性

1. 修饰局部变量:将存放在栈区且生命周期在包含语句块执行结束时便销毁的局部变量改变为存放在

静态存储区,且生命周期会一直延续到整个程序执行结束,但是作用域还是限制在其语句块。

2. 修饰全局变量/函数:对于全局变量,既可以在本文件中被访问,也可以被在同一工程中的其他源文

件访问(添加 extern 进行声明)。用 static 进行修饰改变了其作用域范围,由原来整个工程可见

变为了本文件可见。

3. 修饰类函数:表示该函数属于一个类而非实例;若对类中某变量修饰,则表示该变量被所有该类实

例所共有,static修饰的类变量先于对象存在,所以其要在类外初始化

const:定义常量

1. 修饰基本数据类型:修饰符 const 可在类型说明符前或后,其结果是一样的。在使用时不可以改变

这些变量的值。

2. 修饰指针:

指针常量:常量,指向的地址不能被改变,但是可以改变地址内的内容。 C++ 复制代码

1 int a,b;

2 int * const p=&a; //指针常量

3

4 *p = 9; //操作成功

5 p = &b; //操作错误

常量指针:指向常量的指针。可以改变其指向的(常量/非常量)地址,但是… C++ 复制代码

1 int a,b;

2 const int *p = &a; //常量指针

3

4 *p = 9; //操作错误

5 p = &b; //操作成功

3. 类中的用法:const成员变量,只在某个对象生命周期内是常量,而对于整个类而言是可以改变

的。因为类可以创建多个对象,不同的对象其const数据成员值可以不同。const数据成员的初始化

只能在类的构造函数的初始化列表中进行。const成员函数(在参数列表后加const,此时对this隐

式加const)的主要目的是防止成员函数修改对象的内容。常量对象只能调用常量函数。

声明和定义的区别

【C/C++面试必备】声明和定义的区别_Linux猿的博客-CSDN博客_定义和声明的区别

● 变量/函数可以声明多次,变量/函数的定义只能一次。

● 声明不会分配内存,定义会分配内存。

● 声明是告诉编译器变量或函数的类型和名称等,定义是告诉编译器变量的值,函数具体干什么。

12

1.3 编程特性

C++11新特性

1. 空指针nullptr:目的是为了替代NULL,用来区分空指针和0,能够隐式转换为任何指针的类型,也

能和他们进行相等判断。由于传统C++会把NULL和0视为同一种东西,这取决于编译器如何定义

NULL,有些编译器会将NULL定义为 ((void)0),有些会直接将其定义为0。C++不允许直接将 void

隐式转换到其他类型,但如果NULL被定义为前者,那么当编译 char *ch = NULL 时,NULL只好

被定义为0。而这依然会产生问题,这导致了C++中重载特性会发生混乱

2. 智能指针

3. Lambda表达式:利用lambda表达式可以编写内嵌的匿名函数,用以替换独立函数或者函数对象,

并且使代码更可读,有值捕获和引用捕获两种方式获取外部对象。

4. 右值引用:右值引用特性允许我们对右值进行修改,借此可以实现move,即从右值中直接拿数据过

来初始化或修改左值,而不需要重新构造左值后再析构右值。

5. constexpr :constexpr 告诉编译器这是一个编译期常量,使得定义的变量(无需加const)也可

以作为数组大小的表示。甚至可以把一个函数声明为编译期常量表达式

6. 统一的初始化方法:均可使用 {} 进行初始化变量

7. 类型推导:提供 auto 和 decltype 来静态推导类型。decltype 用于获取一个表达式的类型,而不

对表达式求值

C++ 复制代码

1 const std::vector<int> v(1);

2 const int&& foo(); // 返回临终值:生命周期已结束但内存还未拿走

3

4 auto a = v[0]; // a 为 int

5 decltype(v[0]) b = 0; // b 为 const int&

6

7 auto c = 0; // c, d 均为 int

8 auto d = c;

9 decltype(c) e; // e 为 int,即 c 的类型

10 decltype((c)) f = e; // f 为 int&,因为 c 是左值

11 decltype(0) g; // g 为 int,因为 0 是右值

8. 基于范围的 for 循环

9. final 和 override:提供 final 来禁止虚函数被重写/禁止类被继承,override 来显式地重写虚函数

10. default 和 delete:可以显式地指定和禁止编译器为类自动生成构造或析构函数等

11. 静态断言:static_assert 关键字可在编译期进行使用,而之前的assert仅在运行期起作用(模板检

查在编译期)

12. 初始化列表:提供 initializer_list 来接受变长的对象初始化列表

13. 正则表达式

13

C 和 C++ 区别

● C 是面向过程的,C++ 是面向对象的。因此C++语言中有类和对象、继承和多态这样的OOP语言必

备的内容,此外C++还支持模板,运算符重载以及STL;

● 在输入输出方式上,C 是 printf/scanf 库函数,C++ 是 cout/cin,即 ostream和 istream 类型的

对象;

● 在动态内存管理上,C 语言通过 malloc/free 来进行堆内存的分配和释放,而 C++ 是通过

new/delete 来管理堆内存;

● 在强制类型转换上,C 的强制类型转换使用小括号里面加类型进行强转,而 C++ 可以使用

const_cast,static_cast,reinterpret_cast 和 dynamic_cast 继续强转;

● 在C++中,struct 关键字不仅可以用来定义结构体,也可以用来定义类;

● C++不仅支持指针,还支持更安全的引用。不过在汇编代码上,指针和引用的操作是一样的;

● C++支持自定义命名空间,而 C 不支持。

C++ 和 Java 区别

● 指针:Java虚拟机内部用到了指针,程序员无法直接访问内存,无指针概念

● 多重继承:C++支持多重继承但Java不支持,Java支持一个类实现多个接口

● 自动内存管理:Java自动进行无用内存回收,而C++必须程序员释放内存资源

● 重载运算符:Java不支持

● 类型转换:C++可隐含转换,Java必须强制转换

● 字符串:C++中字符串是以NULL终止符代表字符串的结束;Java是用类对象实现的

● 预处理:Java不支持预处理功能,C++在编译过程中会有一个预编译阶段,Java没有预处理器,但

提供了import与C++预处理器有类似的功能

C++ 和 Pyhton 区别

● C++为编译型语言;python为解释型的脚本语言。

● C++运行效率高。Python是解释执行的,和物理机CPU之间多了解释器这层,而C++是编译执行

的,直接就是机器码,编译的时候编译器又可以进行一些优化。

● 开发效率上,Python要比C++快很多

● 代码形式

1.4 类与面向对象

14

初始化

在C++语言中,初始化和赋值是两个完全不同的操作。初始化和赋值的区别事关底层效率问题。

● 初始化:创建变量时赋予其一个初始值。

● 赋值:把对象的当前值删除,并赋予一个新的值。

如果在变量初始化时没有指定初始值,则变量进行默认初始化,默认值是由变量类型和位置决定的。

内置类型的初始值由定义的位置决定:

● 定义在函数体之外的变量被初始化为0

● 定义在函数体内部的局部变量则未定义

● 对于函数体内部的局部静态变量,如果没有显式初始化,它将执行默认值初始化

类内成员的默认初始值由类自己决定:

● 在默认构造函数中进行了赋值,则初始化值为默认构造函数的值

● 在默认构造函数中没有赋值,但是该数据成员提供了类内初始值,则创建对象时,其初始值就是类

内初始值

● 若上述都无,对于内置类型,则其值未定义;对于类类型则调用其默认构造函数,如果没有默认构

造函数,则不能进行值初始化。

若某个类有一个类成员是类类型,那么

● 若类通过在构造函数体内初始化,会先调用成员类的默认无参构造函数,再调用类的赋值运算符;

● 若类通过在初始化列表去初始化,则只调用成员类的拷贝构造函数。

另外,虽然对于成员类型是内置类型的情况,通过上述两种情况去初始化是相同的,但是为了标准化,

推荐使用初始化列表。[1]

必须使用初始化列表去初始化类成员的情况:

● 成员类型是引用 / 常量;

● 成员类型是对象,并且这个对象没有无参数的构造函数;

● 子类初始化父类的私有成员。

15

类成员的初始化顺序不是按照初始化列表的顺序来的,而是按照类成员的声明顺序

重载、重写以及重定义的区别

重载 重写(函数体) 重定义

同名函数 是 是 是

参数列表 参数个数、参数类型或参数顺序三者中 同 可以不同

必须至少有一种不同

返回类型有关 可以相同,也可以不相同 同 可以不同

用于 同一作用域 父类虚函数 父类非虚函数

重载:函数名相同,函数的参数个数、参数类型或参数顺序三者中必须至少有一种不同。函数返回值的

类型可以相同,也可以不相同。发生在一个作用域内。

重写:也叫覆盖(override),一般发生在子类和父类继承关系之间。子类重写父类中有相同名称和参数的

虚函数。

重定义:子类重新定义父类中有相同名称的非虚函数 ( 参数列表可以不同 ) ,派生类的函数屏蔽了与其

同名的基类函数。如果一个派生类,存在重定义的函数,那么,这个类将会隐藏其父类的方法,除非在

调用的时候,强制转换为父类类型,才能调用到父类方法。否则试图对子类和父类做类似重载的调用是

不能成功的。

重写需要注意:

1、 被重写的函数必须是 virtual 的;

2 、重写函数必须有相同的类型,名称和参数列表;

3 、重写函数的访问修饰符可以不同。

重定义需要注意:

● 如果派生类的函数与基类的函数同名,但是参数不同,此时,不管有无 virtual,基类的函数被隐

藏。

● 如果派生类的函数与基类的函数同名,参数也相同,但是基类函数没有 vitual 关键字,此时,基类

的函数被隐藏(如果有 Virtual 就是重写覆盖了)。

四种强制类型转换

static_cast:明确指出类型转换,一般建议将隐式转换都替换成显示转换,因为没有动态类型检查,派

生类转基类安全,反之不安全,所以主要执行非多态的转换

16

dynamic_cast:专门用于派生类之间的转换。当类型不一致时转换过来的是空指针,而 static_cast 当

类型不一致时转换过来的是错误的指针,可能造成非法访问等问题

const_cast:专门用于const属性的转换,主要用来去除 const 和 volatile 限定符。具体用法是用于指

针或引用,间接操作被const修饰的变量 [1]

reinterpret_cast:从底层对数据进行重新解释,依赖具体的平台,可移植性差

结构体和类的区别

● 默认继承、成员访问权限不一样

● 是否支持类模板

类的大小 与 内存对齐

1 class A{}; // sizeof(A) = 1 (标识这是一个类)

2 class A{virtual Fun()}; // sizeof(A) = 8 (64位,有一个指向虚函数表的指针)

3 class A{static int a;}; // sizeof(A) = 1 (静态变量存储在静态存储区,不占类空

间)

4 class A{int a;} // sizeof(A) = 4

5 class A{int a; char c;} // sizeof(A) = 8

6 class A{Fun()}; // sizeof(A) = 1 ( 普通函数不占空间)

为什么进行内存对齐?

● 内存访问次数影响:为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需

要一次访问。

● 硬件平台支持问题:不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在

某些地址处取某些特定类型的数据,否则抛出硬件异常。

● 空间消耗问题:没有进行内存对齐的结构体或类会浪费一定的空间,当创建对象越多时,消耗的空

间越多。

面向对象三大特征

封装:把客观事物封装成抽象的类。一个类就是一个封装了数据以及操作这些数据的逻辑实现,其中某

些代码或者数据是有不同的访问权限,以防止程序被意外改变或使用对象的私有部分。

继承:指可以让某个类型的对象获得另一个类型对象属性的方法。可以使用现有类的所有功能,并在无

需重新编写原来类的情况下对这些功能进行扩展。继承分为 实现继承 和 接口继承 两种,实现继承是指

17

直接使用基类的属性和方法而无需额外编码的能力;接口继承是指仅使用属性和方法的名称,但是派生

类必须提供其实现。

多态:就是向不同的对象发送同一个消息,不同对象在接收时会产生不同的行为(即方法),即一个接

口可以实现多种方法。多态和非多态的实质区别是函数地址是早绑定还是晚绑定(早绑定 是在编译器编

译期间就可以确定函数的调用地址,并产生代码,是静态的)。

虚函数

哪些函数不能是虚函数

1. 构造函数

2. 内联函数:内联函数在编译阶段进行函数体的替换操作,而虚函数在运行期间进行类型确定,所以

不能是虚函数

3. 静态函数:不属于对象属于类,静态成员函数没有 this 指针,设置为虚函数没有意义

4. 友元函数:不属于类的成员函数,不能被继承

5. 普通函数:不属于类的成员函数,不能被继承

虚函数表 与 虚函数内部实现原理

每个拥有虚函数的类都至少有一个虚函数指针,所有的虚函数都是通过 虚函数指针 在虚函数表中调用

的,虚函数表会记录这个类中所有的虚函数的地址。对于派生类来说,编译器(编译期创建)建立虚函

数表的过程分为三步:

1. 拷贝基类的虚函数表,如果是多继承,则拷贝每个有虚函数基类的虚函数表

2. 选取继承的第一个基类函数表,将该类虚函数表与其合并来共用一个虚函数表(一个虚函数表对应

一个虚函数指针)

3. 检测该类中是否有重写基类中的虚函数,若有则替换成已重写的虚函数地址

18

基类析构函数为什么要使用虚函数?

当不使用多态时,可正常运行并析构对象;

当使用多态时,如不将基类析构设置为虚函数,则当对象销毁时派生类无法正常析构,仅仅只有基类被

析构。

19

1 class F {

2 public:

3 分配

F() { cout << " f" << endl; }

4 // 基类析构不是虚函数,则无法正常析构派生类

5 析构

~F() { cout << " f" << endl; }

6 };

7 class A: public F {

8 public:

9 A() { cout << "分配 a" << endl; }

10 ~A() { cout << "析构 a" << endl; }

11 };

12 int main() {

13 cout << "不使用多态 : " << endl;

14 { A a; }

15 cout << "使用多态 : " << endl;

16 {

17 F *f = new A();

18 delete f;

19 }

20 return 0;

21 }

1.5 基础概念

回调函数:当发生某种事件时,系统或其他函数将会自动调用定义的一段函数。回调函数就是一个通过

函数指针调用的函数,如果把函数的指针(地址)作为参数传递给另一个函数,当这个指针被调用时,

我们就说这是回调函数。

简述 fork/wait/exec 函数

fork 将父进程复制一份给子进程,子进程从 fork 调用处继续执行,之后的代码在父子进程中各自执行

一遍。最终父进程的 fork 返回子进程的 pid,子进程的 fork 返回 0 表示创建成功。所以看起来仿佛

fork 有两个返回值,其实是两个进程的 fork 各自的返回值。

exec 函数族可以根据指定的文件名或目录名找到可执行文件,并用它取代原调用进程的数据段、代码

段和堆栈段。在执行完后,原调用进程除了进程号外,其他全部被新程序的内容替换。

wait 会暂时停止当前进程的执行,直到有信号或子进程结束。如果子进程已经结束,则 wait 会立即返

回子进程结束状态值。

1.6 STL

模板

20

「直播回放」腾讯工程师分享:现代C++模版分享 - 腾讯技术工程的视频 - 知乎

https://www.zhihu.com/zvideo/1546245357092491265

模板底层实现:编译器会对函数模板进行两次编译,在声明的地方对模板代码本身进行编译,在调用的

地方对参数替换后的代码进行编译。

模板传参分析

模板重载

21

vector

是动态空间,随着元素的加入,它的内部机制会自行扩充空间以容纳新元素。vector 的数据结构其实就

是三个迭代器构成的,一个指向目前使用的空间头,一个指向目前使用空间尾,一个指向目前可用的空

间尾。当有新元素插入时,如果目前容量够用则直接插入;若不够则容量扩充至两倍,依次类推。扩充

的过程是重新申请一块连续空间,将原有数据拷贝到新空间,再释放原有空间,扩充后原有的迭代器会

失效。

remove() 的实现原理:在遍历容器中的元素时,一旦遇到目标元素,就做上标记,然后继续遍历,直

到找到一个非目标元素,即用此元素将最先做标记的位置覆盖掉,同时将此非目标元素所在的位置也做

上标记,等待找到新的非目标元素将其覆盖。remove() 不会改变其容量大小,而 erase() 可以改变其容

量大小,通常将 remove() 返回的迭代器传入 erase() 中清除后续无用元素。

22

注意事项:

● 插入和删除 元素后,如果由于内存重分配则会导致迭代器全部失效;没有重分配则插入和删除之后

的迭代器失效。[1]

● 清空 vector 数据时,如果保存的数据项是指针类型,需要逐项 delete,否则会造成 内存泄漏

● 频繁调用 push_back 影响:向vector 的尾部添加元素,很有可能引起整个对象存储空间的重新分

配,这个过程是耗时耗力的。C++11之后,新增 emplace_back 方法,都是添加元素,但是该方法

效率更高。emplace_back 在内存优化方面和运行效率方面有改进,内存优化方面主要体现在就地

构造(直接在容器内构造对象,不用拷贝一个再使用)+强制类型转换,在运行效率方面,由于省

去了拷贝构造,因此有提高。

list

STL中的 list 是一个双向循环链表,相比双向链表结构的好处是在构建 list 容器时,只需借助一个指针

即可轻松表示 list 容器的首尾元素。

deque

支持从头尾两端进行元素的插入和删除操作,没有容量的概念,因为它是动态地以 分段连续空间 组合

而成,随时可以增加一段新的空间并连接起来,但是拥有复杂的迭代器结构。deque 采用一块所谓的

map 作为主控,这里的 map 实际就是一块大小连续的空间,其中每一个元素称为节点 node,都指向

了另一段连续线性空间,该空间是 deque 真正的存储空间

deque 实现原理

1. 迭代器是一个类,其中迭代器中的 node 变量指向 map 的一个单元,而 map 中的每个单元指向当

前迭代器对应的数据(缓冲区),如下图所示。map 的实现为 vector。

2. 当某个数据缓冲区头部或尾部已满时,将回到 map 中定位到相邻的数据缓冲区。内部 分段连续 实

现。

3. 当插入元素时,当前位置位于首部或尾部时,直接插入;否则比较当前元素距离首部近还是尾部

近,距离哪边近则将当前位置那段的元素整体移动,再插入当前元素。

4. 堆 和 栈 的实现原理

23

priority_queue

优先队列(STL为大顶堆),每个节点大于其子节点,采用 vector 实现

map 和 set

STL 原理解析:https://www.bilibili.com/video/BV1NB4y1W7Uf?

spm_id_from=333.999.list.card_archive.click&vd_source=de7529d9789e1cd154061dd03f6490

20

map 和 set 都是C++的关联容器,其底层实现都是红黑树。

set、multiset 使用红黑树的迭代器是 const 类型的。

map、multimap 使用红黑树的迭代器不是 const 类型的。

(key、val在内部是存储在一个元素中,因此set、map底层实现一样)

红黑树(平衡二叉搜索树)

24

红黑树定义:

● 每个节点不是红色就是黑色

● 根节点必须是黑色,相邻节点颜色不一样

● 从根节点到叶子节点的黑色节点个数是一样的

实现:有个虚拟头结点,左右指针分别指向红黑树最左侧和最右侧节点,便于最大最小值查找。每个节

点都有左右指针、父节点指针和K-V值(还有颜色值)

为什么采用红黑树实现:

● 二叉树在某些情况下可能会退化为 O(N) 的时间复杂度;

● AVL树左右子树高度差最大为1,红黑树不遵循高度差条件,为的是减少旋转的次数

hashtable

STL 原理解析:https://www.bilibili.com/video/BV1NB4y1W7Uf?

spm_id_from=333.999.list.card_archive.click&vd_source=de7529d9789e1cd154061dd03f6490

20

hash 转换:

1. 整数值直接作为 hash 值

2. 字符串各字符处理(只要够乱就可)作为 hash 值

哈希表扩充:当元素个数大于 buckets 大小时,将会进行哈希表扩充(约2倍数扩充),并重新分配所

有元素。

碰撞处理:同一位置用链表存储

实现:unordered_set、unordered_map

25

STL 底层实现

● 只有一个链表的头结点

● 每个桶指向上一个有效桶的最后一个节点(即上一个节点)

1.7 算法

memcpy 实现

1 void mymemcpy(void* dst, const void* src, size_t num) {

2 assert((dst != nullptr) && (src != nullptr));

3

4 const char* psrc = (const char*)src; // 因为void*是无法完成 '++' 或 '--'

5 char* pdst = (char*)dst;

6

7 if (pdst > psrc && pdst < psrc + num) {

8 for (size_t i = num - 1; i >= 0 && i < num; --i) {

9 pdst[i] = psrc[i];

10 }

11 } else {

12 for (size_t i = 0; i < num; ++i) {

13 pdst[i] = psrc[i];

14 }

15 }

16 }

读写锁

26

1

2 class ReadWriteLock {

3 private:

4 std::mutex write;

5 std::shared_mutex read; // C++14

6 int readCnt;

7 public:

8 ReadWriteLock()

开通会员 本次下载免费

所有资料全部免费下载! 推荐用户付费下载获取返佣积分! 积分可以兑换商品!
一键复制 下载文档 联系客服