您现在的位置是:首页 >其他 >C++ 知识点总结 面经网站首页其他

C++ 知识点总结 面经

ky0uma 2024-06-17 11:28:18
简介C++ 知识点总结 面经

总结C++面试常问的知识点总结

注意事项

1.不要着急,先想一下,组织语言
2.对简单问题回答要有自己的理解,把细节做好
3.对于相对复杂的问题,整理好逻辑思路,以及问题大致描述的顺序。需要跟面试官沟通,不要自顾自滔滔不绝
4.对于不知道的内容,不要只说不知道,可以回答其他相关的,引导提问,体现自己擅长的地方
5.你还有什么问题?如果能得到工作将来在公司会用到那些技术,对面试做出点评

回答问题,饱满有层次

C++11 新特性

容器、智能指针、lambda表达式

程序内存布局

高地址向低地址
栈区、堆区 数据区、代码区

栈区

栈区(stack):由编译器自动分配释放,存放函数的参数值,局部变量等。栈区的内存分配和释放是由系统自动完成的,速度快。

堆区

堆区(heap):一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。在C++中,可以使用new在堆区分配内存,使用delete释放内存。堆区的内存分配和释放需要手动完成,速度相对较慢。

数据区

数据区通常包括静态存储区和常量区。
静态存储区包括.DATA段和.BSS段
数据区是程序内存中的一部分,用于存储全局变量和静态变量。它分为两个部分:DATA段和BSS段。DATA段(全局初始化区)存放初始化的全局变量和静态变量;BSS段(全局未初始化区)存放未初始化的全局变量和静态变量。程序运行结束时自动释放。

静态变量和局部变量

静态局部变量和局部变量的主要区别在于它们的生命周期和作用域。

局部变量是在函数内部定义的变量,它的生命周期仅限于函数的执行期间。当函数执行完毕后,局部变量就会被销毁。局部变量只能在定义它的函数内部访问。

静态局部变量也是在函数内部定义的,但是它在程序执行期间一直存在,即使定义它的函数已经执行完毕。静态局部变量只能在定义它的函数内部访问,但是它的值会在函数调用之间保持不变。

C++程序编译过程

C++程序的编译过程通常包括以下四个步骤:

  1. 预处理:预处理器将源代码文件中的预处理指令(如#include和#define)进行替换和展开,生成一个预处理后的文件。

  2. 编译:编译器将预处理后的文件翻译成汇编语言代码,并对代码进行优化。然后,汇编器将汇编语言代码翻译成机器语言代码,生成目标文件。

  3. 链接:链接器将多个目标文件和库文件链接在一起,生成可执行文件。链接器解决了目标文件之间的相互引用问题,并将库函数与程序代码链接在一起。

  4. 加载:当程序运行时,操作系统负责将可执行文件加载到内存中,并为程序分配运行所需的资源。然后,操作系统将控制权转交给程序,程序开始执行。

C++ 三大特性

继承、多态、封装
继承:允许派生类使用基类变量,减少代码量
多态:动态多态(基类指针指向派生类),静态多态(函数、操作符重载)
封装:模块化,通过访问控制方式(public、protected、private)控制对成员的访问

继承

1.代码的复用
2.通过继承,再基类里面给所有派生类可以保留统一的纯虚函数接口,等待派生类进行重写,通过使用多态,可以通过基类的指针访问不同派生类对象同名覆盖方法

C++继承多态

多态:
静态多态(编译时期):函数重载和模板。模板是一种静态多态,是一种编译时的多态(类似函数重载)
动态多态(运行时期):虚函数,指针指向派生类对象,

在C++中,隐藏、重载和重写是三个不同的概念:

隐藏(Hiding):当派生类中定义了一个与基类中同名的成员时,派生类中的成员将隐藏基类中的成员。这意味着,如果我们使用派生类对象访问该成员,将访问派生类中定义的成员,而不是基类中的成员。

重载(Overloading):当在同一作用域内定义了多个同名函数,但它们的参数列表不同时,这些函数被称为重载函数。编译器根据调用函数时提供的实参来确定应调用哪个函数。

重写(Overriding):当派生类中定义了一个与基类中虚函数同名、同参数列表、同返回类型的函数时,派生类中的函数将重写基类中的虚函数。这意味着,如果我们使用基类指针或引用指向派生类对象并调用该虚函数,将调用派生类中定义的函数,而不是基类中的函数。

这些概念之间的主要区别在于它们所涉及的上下文和目的。隐藏涉及到派生类和基类之间的关系;重载涉及到同一作用域内多个同名函数之间的关系;重写涉及到派生类和基类之间虚函数的关系。

this指针

对于一个类型定义的多个成员变量,它们共享成员函数。当调用成员函数的时候,通过传入this指针来区别到底是哪个成员调用的成员函数

对齐

使用预处理命令#pragma pack(n)
编译器会按照n来进行对齐,小于n的将会升到n,大于n的为n的倍数

sizeof问题

new 和delete什么时候使用new[]申请可以使用delete释放

delet有两步,第一步先调用析构函数,然后再释放内存
如果是自定义类型,而且提供了析构函数,那么new[] 一定需要匹配delete[]

空间配置器

给容器使用的,主要的作用把对象的内存开辟和对象构造分开,把对象析构和内存释放分开

vector和list区别

数组和链表的区别
vector底层是可以扩容的数组,随机访问多
list底层是双向链表deque,增加删除多

map和多重map multimap

map映射表[key - value],底层实现是红黑树
multimap 允许key重复

红黑树

5个性质
插入的3种情况(最多旋转2次)
删除4种情况(最多旋转3次)

防止内存泄漏?智能指针

分配的堆内存没有释放,也再也没有释放机会了
智能指针利用栈上指针出作用域自动析构

unique_ptr/scoped_ptr/auot_ptr
shared_ptr/weak_ptr

C++调用C语言语句

C和C++生成符号方式不同
C语言的函数声明必须阔在 extern"C"{ /* code */} 中

C++类的初始化列表

可以指定对象成员变量的初始化方式,尤其是指定成员对象的构造方式

C和C++区别,内存分布有什么区别

1.引用
2.函数重载
3.new/delete malloc/free
4.const,inline,带默认参数值的函数
5.模板
6.OOP
7.STL
8.智能指针

int * const p和const int* p 区别

int * const p,修饰的是p,p不能修改,*p可以修改
const int* p,修饰的是int,p可以修改,*p不能修改

malloc和new

1.malloc按字节开辟内存,new底层也是通过malloc开辟内存,new还可以提供初始化
2.malloc开辟失败 nullptr,new开辟失败抛出异常
3.malloc是库函数,new是操作符

map和set容器实现原理

set集合,只存储key
map映射表,存储key-value键值对
底层都是红黑树

shared_ptr引用计数在哪

堆上分配的

迭代器失效

迭代器不允许一边读一边修改
当通过一个迭代器插入一个元素,所有迭代器都失效
当通过一个迭代器插入一个元素,当前删除位置之后的所有元素的迭代器就都失效了

当通过迭代器更新容器元素亦幻,要及时对迭代器进行更新,

struct和class区别

1.定义类的时候的区别,struct默认public,class默认private
2.继承时
3.c++中 struct空类sizeof是1
4.class还可以定义模板类型参数

宏和内联函数

#define 字符串替换,预编译阶段
inline 编译阶段,在函数调用点,把函数代码直接展开调用,节省了函数的调用开销
宏没有办法调试,inline可调试,在debug模式下跟普通函数没有区别

局部变量存放在栈上

stack ebp指针偏移来一定访问,不产生符号.text,属于指令的一部分

静态全局或静态局部在数据段上.data .bss

拷贝构造传引用而不传值

因为如果传值则需要再调用拷贝构造函数产生一个临时变量,这样会递归下去,产生错误,会一直调用拷贝构造函数
编译器会检查这个错误,直接产生编译错误

不可被继承的类

派生类构造时会先调用基类的构造函数,因此把基类的构造函数私有化可以实现以不可被继承的类

什么是纯虚函数?为什么要有纯虚函数?虚函数表放在哪里?

virtual void func() = 0; 纯虚函数 =》 抽象类(不能被实例化对象的,可以定义指针和引用)

一般定义在基类里面,基类不代表任务实体,它的主要作用之一就是给所有的派生类保留统一的纯虚函数接口,让派生类进行重写,方便使用多态。
虚函数表在编译阶段产生,运行是加载到.rodata段(常量区,只读)

单例模式

饿汉式单例模式:还没有获取实例对象,实例对象就已经产生了
懒汉式单例模式:唯一的实例对象,直到第一次获取它的时候才产生

const static volatile

const

const 定义的叫常量,编译方式为:编译过程中,把出现常量名字的地方,用常量的值进行替换

const int a = 10;
int* p = (int*)& a;
*p = 20;
cout << *p << " " << a << endl;

最终输出为20 10
即使把a内存中存放的值改为20,a也是10

const还可以定义常成员方法

static关键字作用

static修饰全局变量,在符号表中符号的作用域从g(global)变成l(local),其它文件不可见
static修饰局部变量,(数据区)放在.data和.bss段,如果初始化了放在.data段,如果未初始化或初始化未0放在.bss段

static修饰成员变量:这个成员变量从对象私有变为对象共享
static修饰成员方法:不再产生this指针,成员方法不再使用成员对象进行调用,可直接使用作用域调用。 静态成员变量属于类,而不属于类的任何一个实例

const和static区别

面向过程:
const:修饰全局变量、局部变量、形参变量
static:修饰全局变量、局部变量

面向对象
const:常方法/成员变量 Test *this -> const Test *this 依赖对象
static:静态方法/成员变量 Test *this -> 没有,没有this指针 不依赖对象,通过类作用域访问

四种强制类型转换

const_cast
static_cast
reinterpret_cast:C风格的类型转换,没有安全可言,可以随便转换为任意类型
dynamic_cast:支持RTTI信息识别的类型转换

deque的底层原理

deque双端队列,两端都有队头和队尾,两端都可以插入删除O(1)
动态开辟的二维数组
#define MAP_SIZE 2
#define QUE_SIZE(T) 4096/sizeof(T)

开始时
第一维数组大小为MAP_SIZE(T*)
第二维数组默认开辟的大小为QUE_SIZE(T)

扩容:把第一维数组按照2倍的方式进行扩容,扩容以后,会把原来的第二维的数组,从新一维数组的第oldsize/2 开始存放

虚函数,多态

一个类存在虚函数是,在编译阶段就会产生一张虚函数表,运行时虚函数表加载到.rodata段。
在使用指针或者引用时,调用虚函数,指针访问对象的头四个字节vfptr获取指针,再到vftable中取到虚函数的地址,进行动态绑定调用

多态:基类指针指向不同派生类,然后调用不同派生类的同名覆盖方法。
设计函数接口的时候,可以使用基类的指针或者引用来接收不同派生类对象

虚析构函数

当基类指针指向派生类对象的时候,在调用析构函数的时候会只调用基类的析构,没有调用派生类的析构,会产生内存的泄露

智能指针

管理资源的生命周期

构造函数和析构函数

构造函数不能为虚函数
虚函数的调用必须首先对象得存在,只有对象存在才能获取对象前四个字节的vfptr然后在从vftable中找到对应的虚函数进行调用。构造函数没调用完,理论上是没有对象产生的

析构函数可以为虚函数

异常机制

try{
    /* 可能会抛出异常的代码 */
}catch(const string &err) {
    /* 捕获想应的异常类型对象,进行处理,完成后,代码继续向下执行 */
}

早绑定 晚绑定

早绑定(静态绑定):编译时期的绑定,普通函数的调用,用对象调用虚函数
晚绑定(动态绑定):用指针/引用调用虚函数的时候都是动态绑定

指针和引用的区别

指针可以不初始化,引用必须初始化

野指针

指针未初始化但已被使用
指针指向的内存已被释放或删除,但指针未被重置
指针越界访问

智能指针交叉引用问题

定义对象的时候用强智能指针shared_ptr,而引用对象的时候用弱智能指针weak_ptr,当通过weak_ptr访问对象成员时,需要先调用weak_ptr的lock提升方法,把weak_ptr提升成shared_ptr强智能指针,再进行对象成员的调用

重载的底层实现,虚函数的底层实现

重载,因为在C++生成函数符号是依赖 名字+参数列表
编译到函数调用点时,根据函数明在和传入的实参(个数和类型),和某一个函数重载匹配的话,那么就直接调用想应的函数重载版本(静态的多态,都是在编译阶段处理的)

虚函数
虚函数的底层实现是通过虚函数表和虚表指针来实现的。

每个含有虚函数的类都有一个与之对应的虚函数表,其中存放着该类所有的虚函数对应的函数指针。

每个含有虚函数的类对象都有一个隐藏成员,即虚表指针vfptr,它指向该类的虚函数表vftable。

当基类指针指向一个子类对象时,通过这个指针调用同名的虚函数时,会根据虚表指针找到对应的子类或基类的虚函数地址,并执行该函数。

sizeof

sizeof是一个编译时运算符,它用于计算给定类型或表达式的大小(以字节为单位)
如计算sizeof(string) 的时候,输出的是string类的大小,而不是str的长度

strlen

输出字符串长度,到’’之前

string str = "12345";
std::cout << sizeof(str) << std::endl; // 输出 6,此时加上了''
std::cout << strlen(str) << std::endl; // 输出 5

内存池

C++容器

c++容器有两大类:顺序容器(sequence container)和关联容器(associative container)。顺序容器按照线性顺序存储元素,如vector,list,deque等。关联容器按照某种规则组织元素,如set,map,multiset等。

序列容器 容器适配器

容器适配器是一种封装了序列容器的类模板它在一般序列容器的基础上提供了一些不同的功能。
c++标准库提供了三种容器适配器,分别是stack,queue和priority_queue。它们的底层实现可以是任何序列容器,但默认是deque

stack和queue底层

容器适配器底层没有实现任何数据结构,底层直接依赖一个现有的顺序容器,stack和queue依赖deque

vector和list的区别

  1. vector底层实现是数组;list是双向链表
  2. vector是顺序内存,⽀持随机访问,list不⾏
  3. vector在中间节点进⾏插⼊删除会导致内存拷⻉,list不会
  4. vector⼀次性分配好内存,不够时才进⾏翻倍扩容;list每次插⼊新节点都会进⾏内存申请
  5. vector随机访问性能好,插⼊删除性能差;list随机访问性能差,插⼊删除性能好

网络五层模型

物理层
数据链路层 ARP
网络层 ICMP、IP
传输层 TCP、UDP
应用层 HTTP

ARP协议

根据IP地址获取物理地址。
ARP(地址解析协议)是根据IP地址获取物理地址的一个TCP/IP协议。主机发送信息时将包含目标IP地址的ARP请求广播到局域网络上的所有主机,并接收返回消息,以此确定目标的物理地址;收到返回消息后将该IP地址和物理地址存入本机ARP缓存中并保留一定时间,下次请求时直接查询ARP缓存以节约资源。

TCP和UDP区别

TCP(传输控制协议)和UDP(用户数据报协议)是两种常用的传输层协议。它们的主要区别在于:

TCP是面向连接的协议,传输数据前需要先建立连接。UDP是无连接的协议,发送数据前不需要建立连接。
TCP提供可靠的服务,通过TCP连接传送的数据无差错、不丢失、不重复、按序到达。而UDP尽最大努力交付,不保证可靠交付。
TCP有拥塞控制和流量控制机制,保证数据传输的安全性。而UDP则没有。
TCP首部长度较长,会有一定的开销。而UDP首部只有8个字节,开销较小。
TCP是流式传输,没有边界,但保证顺序和可靠。而UDP是一个包一个包的发送,是有边界的,但可能会丢包和乱序。

TCP如何保证可靠传输

TCP(传输控制协议)通过多种机制来保证可靠传输,包括:

1.序列号和确认应答:TCP为每个数据包分配一个序列号,并要求接收方发送确认应答。这样可以保证数据按序到达,且不丢失、不重复。
2.数据校验:TCP使用校验和来检测数据是否在传输过程中发生错误。
3.超时重传:如果发送方在一定时间内没有收到接收方的确认应答,它会认为数据包丢失并进行重传。
4.流量控制:TCP使用滑动窗口机制来控制发送方的发送速率,以防止接收方处理不过来而导致数据丢失。
5.拥塞控制:当网络拥塞时,TCP会通过慢启动、拥塞避免、快速重传等机制来调整发送速率,以防止数据丢失。

TCP/IP协议栈

TCP三次握手 四次挥手

三次握手

  1. 客户端发出生成seq,发送SYN置为1的包给服务器
  2. 服务器发送有SYN+ACK标志的包给客户端,ACK表示验证字段
  3. 客户端发送有ACK标志的包给服务器,链接建立

四次挥手:由于TCP连接是全双工的,因此每个方向都必须单独进行关闭

  1. 客户端发送FIN标志的包,关闭客户端到服务端的数据传送,客户端进入FIN_WAIT_1状态
  2. 服务器收到FIN后,发送ACK给客户端,服务端进入CLOSE_WAIT状态
  3. 服务端发送FIN,关闭服务端到客户端的数据传输,服务端进入LAST_ACK状态
  4. 客户端收到FIN后,客户端进入TIME_WAIT状态,发送ACK给服务端。服务端收到后服务端进入CLOSE状态,链接关闭。

常用设计模式

string的特性

智能指针shared怎样实现

函数中堆和栈的区别

堆是运行时确定内存大小,而栈在编译时即可确定内存大小
malloc分配的内存在堆区
函数传参,形参在栈上

大端小端区别

优先级队列

priority_queue<int, vector<int>, greater<>> //堆顶最小,小根堆
priority_queue<int, vector<int>, less<>> //堆顶最大(默认)

小根堆:
  父节点的值都小于子节点的值。小根堆用于降序排序。
大根堆:
  父节点的值都大于子节点的值。大根堆用于升序排序。排序中每次都可以确定数组中最大的值,并将其输出,放在后面。然后接着对剩下的堆中的数进行调整,最终全部确定位置。由于最先确定的大的数字放在后面,小的数放在前面,因此是一个升序排序

deque

支持快速随机访问,由于deque需要处理内部跳转,因此速度上没有vector快
所以除非必要尽量使用vector

哈希表

unordered_map的使用方法

map[“a”]++; 直接加,如果没有,插入新值

unordered_map<string, int> map;
for(auto iter = map.begin(); iter != map.end(); iter++)
{
    string s = "";
    s += to_string(iter->second) + " " + iter->first;
}

map排序

bool static cmp (const pair<int, int>& a, const pair<int, int>& b) {
    return a.second > b.second; // 按照频率从大到小排序
}
vector<pair<int, int>> vec(map.begin(), map.end());
sort(vec.begin(), vec.end(), cmp); 

multiset

multiset是库中一个非常有用的类型,它可以看成一个序列,插入一个数,删除一个数都能够在O(logn)的时间内完成,而且他能时刻保证序列中的数是有序的,而且序列中可以存在重复的数。自动排序

二叉树

二叉搜索树

若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
它的左、右子树也分别为二叉搜索树

高度

指从该节点到叶子节点的最长简单路径边的条数

深度

指从根节点到该节点的最长简单路径边的条数

sort

默认升序
降序

static bool cmp(int a, int b) {
    return a > b;
}
sort(num.begin(), num.end(), cmp);

进程

linux进程间相互通信

Linux进程间通信有几种方式,包括管道(包括无名管道和命名管道)、消息队列、信号量、信号、共享内存、Socket等

僵尸进程

子进程已经完成退出但没有被父进程获取进程状态信息,子进程的进程描述符仍然保存在系统中
一个进程使用 fork 创建子进程,如果子进程退出,而父进程并没有调用 wait 或 waitpid 获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中,这种进程称之为僵死进程。

杀死僵尸进程

僵尸进程已经死了,所以不能直接杀死它。但是,可以杀死僵尸进程的父进程。父进程死后,僵尸进程成为“孤儿进程”,过继给1号进程init,init始终会负责清理僵尸进程。它产生的所有僵尸进程也跟着消失

避免产生僵尸进程

僵尸进程的产生是因为父进程没有调用wait()或waitpid()来获取子进程的状态信息。所以,为了防止产生僵尸进程,在fork子进程之后,我们都要wait它们

孤儿进程

父进程退出,但子进程还在运行
一个父进程退出,而它的一个或多个子进程还在运行,那么这些子进程将成为孤儿进程。孤儿进程将被 init 进程(进程号为1)所收养,并由 init 进程对它们完成状态收集工作。

排序算法

快速排序

是对冒泡排序的改进
左右两个指针l,r,有一个基准值(一般为数组最右边的值)
进行完一轮排序后,基准值左边的都比基准值小,右边的都比基准值大。
递归,排完左右两部分,最终完成排序

堆排序

二叉树,priority_queue
大顶堆:每一次调整,将父节点调整为大于两个子节点
构造一个大顶堆, 进行完一轮后,堆顶的元素与二叉树最后一层的最后一个数进行交换,最大的数完成排序

宏操作

用宏来实现比较大小
#define MAX(a,b) ((a) > (b) ? (a) : (b))

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。