C++面经总结

本文详细总结了C++面试中常见的知识点,包括宏定义与内联函数的区别、函数重载、内存分配、大小端判断、编译过程、二叉树问题、进程间通信、排序算法、内存泄漏检测、TCP与UDP的区别、哈希表、智能指针、fork函数等。还涵盖了面试中遇到的系统态用户态、TCP连接建立、HTTPS原理、链表操作、哈希冲突解决、数组与链表的应用、排序算法场景选择、布隆过滤器实现等技术要点。
摘要由CSDN通过智能技术生成

1 宏定义和内联函数的区别

① 宏定义是预处理,在编译阶段之前就替换,不进行类型检查,只是简单的字符串替换。
② 内联函数是在编译阶段展开,进行函数的类型检查,如果正确,直接用内敛函数的代码替换函数调用,省去了函数调用的开销。
③ const 与宏定义除了以上区别,#define 只是简单的展开,内存中有很多个备份;const是只读常量,在程序运行过程中只有一份备份。方便调试 , #define 不方便调试

2 函数重载

函数可以根据 参数列表个数和类型的不同进行重载,不能根据返回值进行重载,是静态多态的一种实现;
虚函数是动态多态的一种实现;
模板是“多对一”,多态是“一对多”。 模板是重载的一种特殊形式。也是静多态的一种形式,在编译的时候确定的
表面上看:模板与多态本身就是两个东西。模板实例化是发生在编译期(Compile-time)的,而多态中的函数调用决议是发生在运行时(Run-time)。
实际上:模板是静多态,模板是在编译时确定的,而平时我们说的多态是动多态,是在运行时确定。也就是多态分为两种:静多态和动多态。

3 说下内存分配

C++ 内存分配 分为 : 内核空间占1G,用户空间占3G;
栈 : 函数过程中的局部变量定义在栈区;
堆 : malloc 和 new 之类的对象定义在堆;
全局变量/静态存储区 : 存放全局变量、静态数据 全局区分为已初始化全局区(data)和未初始化全局区(bss);
常量区:存放常量字符串;
代码区: 存放函数体;

pass:栈和堆的区别:
① 栈是编译器自动申请和释放,堆需要程序员自己申请和释放;
② 栈的生长方向是向下的,即由高到低;堆的生长方向是向上的,即由低到高;
③ 空间大小不一样 栈VC6 下默认是1M,而堆最大可以到4G;

4 如何判断大小端

大端:高对低,低对高;小端:低对低,高对高;
可以用共同体去判断:

// 
Union TEST
{
   
	short a;
	char b[sizeof(a)];
}; 
int mian() 
{
   
	TEST test;
	test.a=0x1234;
	if(test.b[0]==0x12 && test.b[1]==0x34)
		big
	else
		small;
}
// 如果a在别的文件中定义为 static 变量,那么这样是拿不到a的值的。

5 hello.c编译之后是啥?经历哪些过程?

编译过后成.o 目标文件。
预处理->.i 文件->g++编译- >.s 文件-> 汇编生成.o 文件
预处理过程中:宏定义替换,条件编译,头文件,注释 ,#pragma

6 二叉树的公共祖先,写代码,如果没有指向父节点的指针呢?

剑指offer面试题

7 进程间通信方式

管道(有名管道,无名管道),消息队列,共享内存,套接字,信号量

8 输入有序数组A和B,合并后输出有序数组C,func(A[],B[],C[])

leetcode 原题

9 系统态用户态,说一个系统态的API


10 static 的作用 extren

static 隐藏,共享,全局,节省资源 ;
extern 全局跨文件 被 extern “C” 修饰的变量和函数是按照 C 语言方式编译和链接的
如果文件a.c需要引用b.c中变量int v,就可以在a.c中声明extern int v,然后就可以引用变量v。

11 一个空类占多少空间:1字节,标识类的地址。

一个字节,因为,这是为了让对象的实例能够相互区别,否则无法使用此对象啊;

12 EPOLL的水平触发和边缘触发

epoll有两种模式,Edge Triggered(简称ET) 和 Level Triggered(简称LT).在采用这两种模式时要注意的是,如果采用ET模式,那么仅当状态发生变化时才会通知,而采用LT模式类似于原来的select/poll操作,只要还有没有处理的事件就会一直通知。

LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表.

ET(edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once),不过在TCP协议中,ET模式的加速效用仍需要更多的benchmark确认(这句话不理解)。

13 top k

如果是最大的10个数,用小顶堆;如果是最小的十个数,用大顶堆;时间复杂度O(n*logn)

14 输入一个URL中间发生了什么

第一步、浏览器中输入域名www.baidu.com

第二步、域名解析 解析域名就要用到DNS协议,首先主机会查询DNS的缓存,如果没有就给本地DNS发送查询请求。DNS查询分为两种方式,一种是递归查询,一种是迭代查询。如果是迭代查询,本地的DNS服务器,向根域名服务器发送查询请求,根域名服务器告知该域名的一级域名服务器,然后本地服务器给该一级域名服务器发送查询请求,然后依次类推直到查询到该域名的IP地址。DNS服务器是基于UDP的,因此会用到UDP协议。

第三步、浏览器与目标服务器建立TCP连接

第四步、浏览器通过http协议向目标服务器发送请求

浏览器向主机发起一个HTTP-GET方法报文请求。请求中包含访问的URL,也就是http://www.baidu.com/ ,KeepAlive,长连接,还有User-Agent用户浏览器操作系统信息,编码等。

第五步、服务器给出响应,将指定文件发送给浏览器

状态行,响应头,响应实体内容,返回状态码200 OK,表示服务器可以响应请求,返回报文,由于在报头中Content-type为“text/html”,浏览器以HTML形式呈现,而不是下载文件。

15 正向代理和反向代理

两者的区别在于代理的对象不一样,「正向代理」代理的对象是客户端,「反向代理」代理的对象是服务端。

16 STL map

map是一类关联式容器。它的特点是增加和删除节点对迭代器的影响很小,除了那个操作节点,对其他的节点都没有什么影响。对于迭代器来说,可以修改实值,而不能修改key。
底层实现为红黑树。

性质1. 节点是红色或黑色。
性质2. 根节点是黑色。
性质3 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
性质4. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。

17 C 如何判断内存泄漏

valgrind
linux top 一下 随着时间增加 内存一直增大

解释一下: TCP time_wait
time_wait状态是四次挥手中server向client发送FIN终止连接后进入的状态。

18 TCP 和udp 的区别

1)TCP和UDP区别
1) 连接

TCP是面向连接的传输层协议,即传输数据之前必须先建立好连接。

UDP无连接。

2) 服务对象

TCP是点对点的两点间服务,即一条TCP连接只能有两个端点;

UDP支持一对一,一对多,多对一,多对多的交互通信。

3) 可靠性

TCP是可靠交付:无差错,不丢失,不重复,按序到达。

UDP是尽最大努力交付,不保证可靠交付。

4)拥塞控制,流量控制

TCP有拥塞控制和流量控制保证数据传输的安全性。

UDP没有拥塞控制,网络拥塞不会影响源主机的发送效率。

5) 报文长度

TCP是动态报文长度,即TCP报文长度是根据接收方的窗口大小和当前网络拥塞情况决定的。

UDP面向报文,不合并,不拆分,保留上面传下来报文的边界。

  1. 首部开销

TCP首部开销大,首部20个字节。

UDP首部开销小,8字节。(源端口,目的端口,数据长度,校验和)

2)TCP和UDP适用场景

从特点上我们已经知道,TCP 是可靠的但传输速度慢,UDP 是不可靠的但传输速度快。因此在选用具体协议通信时,应该根据通信数据的要求而决定。

若通信数据完整性需让位与通信实时性,则应该选用TCP 协议(如文件传输、重要状态的更新等);反之,则使用 UDP 协议(如视频传输、实时通信等)

19 一致哈希

在分布式集群中,对机器的添加删除,或者机器故障后自动脱离集群这些操作是分布式集群管理最基本的功能。如果采用常用的hash(object)%N算法,那么在有机器添加或者删除后,很多原有的数据就无法找到了,这样严重的违反了单调性原则。一致性哈希算法、、、

20 布隆过滤器

直观的说,bloom算法类似一个hash set,用来判断某个元素(key)是否在某个集合中。
和一般的hash set不同的是,这个算法无需存储key的值,对于每个key,只需要k个比特位,每个存储一个标志,用来判断key是否在集合中。

算法:

  1. 首先需要k个hash函数,每个函数可以把key散列成为1个整数
  2. 初始化时,需要一个长度为n比特的数组,每个比特位初始化为0
  3. 某个key加入集合时,用k个hash函数计算出k个散列值,并把数组中对应的比特位置为1
  4. 判断某个key是否在集合时,用k个hash函数计算出k个散列值,并查询数组中对应的比特位,如果所有的比特位都是1,认为在集合中。

优点:不需要存储key,节省空间

缺点:

  1. 算法判断key在集合中时,有一定的概率key其实不在集合中
  2. 无法删除

典型的应用场景:
某些存储系统的设计中,会存在空查询缺陷:当查询一个不存在的key时,需要访问慢设备,导致效率低下。
比如一个前端页面的缓存系统,可能这样设计:先查询某个页面在本地是否存在,如果存在就直接返回,如果不存在,就从后端获取。但是当频繁从缓存系统查询一个页面时,缓存系统将会频繁请求后端,把压力导入后端。

这是只要增加一个bloom算法的服务,后端插入一个key时,在这个服务中设置一次
需要查询后端时,先判断key在后端是否存在,这样就能避免后端的压力。

21 C/C++ 中指针和引用的区别

1.指针有自己的一块空间,而引用只是一个别名;
2.使用sizeof看一个指针的大小是4,而引用则是被引用对象的大小;

3.指针可以被初始化为NULL,而引用必须被初始化且必须是一个已有对象 的引用;

4.作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引 用的修改都会改变引用所指向的对象;

5.可以有const指针,但是没有const引用;

6.指针在使用中可以指向其它对象,但是引用只能是一个对象的引用,不能 被改变;

7.指针可以有多级指针(**p),而引用至于一级;

8.指针和引用使用++运算符的意义不一样;

9.如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄露。

21 C++智能指针

unique_ptr实现独占式拥有或严格拥有概念,保证同一时间内只有一个智能指针可以指向该对象。它对于避免资源泄露(例如“以new创建对象后因为发生异常而忘记调用delete”)特别有用。

shared_ptr实现共享式拥有概念。多个智能指针可以指向相同对象,该对象和其相关资源会在“最后一个引用被销毁”时候释放。从名字share就可以看出了资源可以被多个指针共享,它使用计数机制来表明资源被几个指针共享。可以通过成员函数use_count()来查看资源的所有者个数。除了可以通过new来构造,还可以通过传入auto_ptr, unique_ptr,weak_ptr来构造。当我们调用release()时,当前指针会释放资源所有权,计数减一。当计数等于0时,资源会被释放。

weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象. 进行该对象的内存管理的是那个强引用的 shared_ptr. weak_ptr只是提供了对管理对象的一个访问手段。weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。

21 fork函数

Fork:创建一个和当前进程映像一样的进程可以通过fork( )系统调用:
#include <sys/types.h>

#include <unistd.h>

pid_t fork(void);

成功调用fork( )会创建一个新的进程,它几乎与调用fork( )的进程一模一样,这两个进程都会继续运行。在子进程中,成功的fork( )调用会返回0。在父进程中fork( )返回子进程的pid。如果出现错误,fork( )返回一个负值。

最常见的fork( )用法是创建一个新的进程,然后使用exec( )载入二进制映像,替换当前进程的映像。这种情况下,派生(fork)了新的进程,而这个子进程会执行一个新的二进制可执行文件的映像。这种“派生加执行”的方式是很常见的。

在早期的Unix系统中,创建进程比较原始。当调用fork时,内核会把所有的内部数据结构复制一份,复制进程的页表项,然后把父进程的地址空间中的内容逐页的复制到子进程的地址空间中。但从内核角度来说,逐页的复制方式是十分耗时的。现代的Unix系统采取了更多的优化,例如Linux,采用了写时复制的方法,而不是对父进程空间进程整体复制。

22 C++里是怎么定义常量的?常量存放在内存的哪个位置?

对于局部常量,存放在栈区;对于全局常量,编译期一般不分配内存,放在符号表中以提高访问效率;字面值常量,比如字符串,放在常量区。

22 C语言参数压栈顺序?

从右往左

23 请你说说C++如何处理返回值

生成一个临时变量,把它的引用作为函数参数传入函数内。

24 线程死锁分析:

  1. 连续多次执行 $pstack 其中PID是进程号

    查看每个线程的函数调用关系的堆栈,观察每个线程当前的执行点是否在等待一个锁。

    多次执行该命令,发现某些线程的当前执行点不变,总是在等待同一个锁,就可以怀疑是否死锁了。

    如果怀疑哪些线程发生死锁了,可以采用gdb 进一步attach线程并进行分析。

25


linux 一切皆文件,当创建一个套接字的时候,返回的是文件标识符,即一个整数 fd
int fd1;
fd1 = socket(PF_INET, SOCK_STREAM, 0);



//下面为服务器的套接字或称作监听套接字
int socket(int domin, int type, int protocal)///成果时候返回 fd  失败返回-1
创建套接字要使用相应的协议,domain是套接字中使用的协议族,type 套接字数据传输类型信息,protocol 计算机通信中使用的协议信息;
如 PF_INET 为ipv4协议族,PF_INET6 为ipv6协议族等等
第二个参数 type为传输的方式,如IPv4中有很多传输方式
如SOCK_STREAM 是面向连接的  TCP
传输是无边界的, 发送送次数 和接受次数 可以是不同的

SOCK_DGRAM 是面向消息的传输方式 UDP
传输 有边界, 发送次数和接受次数必须相同

第三个 参数 是确定了 domin 和type  里面如果还有分类的话,在进行区分。  
但是 PF_INET+SOCK_STREAM 只有tcp  PF_INET+SOCK_DGRAM 只有udp  所以可以不用传参数


如果想接收多台计算机发来的数据,那么则相应个数的套接字,那么如何区分这些套接字你,就用到了端口号。





// bind函数原型
int bind(int sockfd, struct sockaddr *myaddr, socklen_t addrlen)
 绑定服务器的地址,然后listen 监听他;
结合之前的 地址介绍 端口介绍 给出以下地址结构体信息 
struct sockaddr_in  {
   
	sa_family_t  sin_family; // 地址族  如ip4(AF_INET)使用4字节地址组	 AF_INET6 ip6 使用16字节地址族
	unit16_t     sin_port;  //16位  tcp/udp 端口号
	struct in_adrr  sin_adrr ///32位ip地址;
		char            sin_zero[8]; ///不适用 ,为了和 struct sockaddr 保持一致;
};

从下面代码可以看出,直接向sockaddr结构体填充这些信息会带来麻烦;
struct sockaddr {
   
	sa_family_t  sin_family; // 地址族  如ip4(AF_INET)使用4字节地址组	 AF_INET6 ip6 使用16字节地址族
	char         sin_zero[14]; 
};

此结构体成员sa data保存的地址信息中需包含E地址和端口号, 剩余部分应填充0, 这也是bind函数要求的。 
而这对于包含地址信息来讲非常麻烦, 继而就有了新的结构体sockaddr m。
若按照之前的讲解填写 sockaddr m结构体, 则将生成符合bind函数要求的字节流。
 最后转换为 sockaddr型的结构体变量,再传递给bind函数即可。





listen 函数
int listen(int sockfd, int backlog) ///成功0  失败-1;
此时的sockfd位服务端fd///backlog为等待队列的长度,如backlog=5,最多可以使5个请求进入等待队列

 accpet 函数
int accept(int sockfd, struct sockaddr *myaddr, socklen_t addrlen)
成功 会发挥一个套接字的文件描述符,该套接字是用于I/O的套接字 。该套接字是自动创建的,并且自动和客户端建立连接。失败-1; 参数:sockfd服务器socket,myaddr客户端的地址,addrlen客户端地址长度;


// 客户端套接字
int socket(int domin, int type, int protocal)///成果时候返回 fd  失败返回-1

int connect(int sockfd, struct sockaddr *serv_addr, socklen_t addrlen)///成果时候返回 fd  失败返回-1
 参数:sockfd客户端的描述符,serv_addr服务器的地址,addrlen服务器地址的长度
 客户端的地址和端口是在connect调用的时候自动分配的,在系统的内核分配


echo 回声服务端/客户端  一般用于调试和检测中。   它可以基于TCP协议,服务器就在TCP端口7检测有无消息,如果使用UDP协议,基本过程和TCP一样,检测的端口也是7TCPI/O缓冲 
 I/O缓冲在每个套接字单独存在
 I/O缓冲在创建套接字的时候自动生成
 即使关闭套接字也会继续传输 输出缓冲 中遗留的数据
 关闭套接字将会丢失输入缓冲的数据

UDP 是不需要连接的,不必调用listen和accept函数。UDP只有创建套接字的过程和数据交换过程
UDP 的数据I/O函数如下:发送信息的函数
ssize_t sendto(int sock, void *buff, size_t nbytes, int flags, struct sockaddr *to, socklen_t addrlen);
成功返回传输的字节数,失败时返回-1
sock 用于传输数据的UDP套接字文件描述符  buff用于保存待传输数据的缓冲地址值  nbytes 是待传输的数据长度,flags 是可选项参数, to指向存有目标地址信息的sockaddr结构体变量
 addrlen是传递给参数to 的地址值结构体变量长度
接受信息的函数
ssize_t recvfrom(int sock, void *buff, size_t nbytes, int flags, struct sockaddr *from, socklen_t *addrlen);
在调用sendto 函数的时候会自动分配IP和端口号。即自动调用bind 函数

UDP 传输过程中一定要注意边界问题,sendto和recvfrom要相互对应
传输的过程中  TCP套接字中需注册待传输数据的目标E和端口号,而UDP中则无需注册。因此啻 通过sendto 函数传输数据的过程大致可分为以下3个阶段。
第1阶段:向UDP套接字注册目标E和端口号。
第2阶段:传输数据。
第3阶段:删除UDP套接字中注册的目标地址信息。
每次调用sendto雨数时重复上述过程。 每次都变更目标地址, 因此可以重复利用同一UDP套 接字向不同目标传输数据。
这种未注册目标地址信息的套接字称为未连接套接字. 反之, 注册了 目标地址的套接字称为连接connected套接字。
显然,UDP套接字默认属于未连接套接字。但UDP 套接字在下述情况下显得不太合理:
IP211.210.147.82的主机82号端口共准备了3个数据,调用3次sendto函数进行传输。”
此时需重复3次上述三阶段。 因此 , 要与同一主机进行长时间通信时,将UDP套接字变成已 连按套接字会提高效率。 
上述三个阶段中, 第一个和第三个阶段占整个通信过程近1 / 3的时间, 缩短这部分时间将大大提高整体性能。
 也就是说,创建的的确是UDP套接字。 当然 , 针对UDP套接字调用connect函数并不意味着要与对方UDP套接字连接 , 这只是向
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值