面试

1. C语言的宏是怎么回事?有哪些问题?
首先c的编译过程是: 预处理->编译->链接 ,预处理阶段预处理器会对预处理指令进行处理,比如会引用#include 包含的头文件,替换#define 定义的宏,因此编译器是看不见宏。
比如:

#include <stdio.h>

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

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

int main(){
   int a=A;
   max(a++,3);
   MAX(a++,3);
}

gcc -E filename.c 经预处理后 输出:

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

int main(){
   int a=3;
   max(a++,3);
   ((a++)>(3) ? (a++) : (3));
}

从这里发现:

  1. 经过预处理器后符号A被替换,于是编译器符号表没有A,gdb调试看不见A,而相比这种定义方式const int A=3;编译器保存了符号,还可以进行类型检查,其缺点由此可见:一个是没有类型的检查,另一个是不好debug
  2. MAX(a++,3)被替换为 ((a++)>(3) ? (a++) : (3)); 明显a++执行 两次不是我们想要的结果,对于函数max的调用传递一个副本,不存在这种问题,由此可见:宏使代码存在安全隐患
  3. 编写宏函数还得注意加括号,不然运算符优先级会乱;
  4. 使代码膨胀;
  5. 任何改变代码都需要重新编译。

ps:这个问题我只说出了第三点,描述了第1点,其他都是皓哥补上,顿时亲身感觉皓哥技术很体系很全面。

2. C++用哪些技术来解决宏的问题?

  • const
  • inline
    inline 类型的函数比宏函数多了类型检测,也没有函数调用开销。
  • template
    宏是简单替换,不带数据类型,而template 实例化有数据类型,可以根据不同数据类型实例化不同模板。最典型的是比较函数,
template<typename T>
T max(T a,T b)
{
	return a > b ? a : b;
}

template <> int max<const char *>(const char * a, const char * b)
{
 return strcmp(a, b);
}

可以比较int char*类型,而宏无能为力。

3. C++为什么要拷贝构造函数?在什么时候会被调用?
c++默认构造函数(复制和赋值)都是浅拷贝,因此当一个类有指针存在时,就必须实现自己的拷贝构造函数、赋值函数,否则浅拷贝指针简单赋值,导致多个指针指向同一块内存,析构阶段也会出现double free。所以一个类需要实现拷贝构造函数,那么必然要实现opertor=函数,析构函数,没有析构会内存泄露。
拷贝构造函数也是很耗性能的,因此函数类类型形式参数都是用引用或者指针,返回时也避免拷贝构造函数调用开销。
ps:这个问题我都没有答上来,当听到指针的时候,我一下子相关联的知识就浮现出来了,说明还没有形成条件反射。

4.C++为什么要有初始化列表?
这个其实是需要了解成员变量在构造函数中初始化过程。如果没有提供初始化列表,那么在进入构造函数体之前会执行默认初始化,于是再进入构造函数体初始化如同赋值操作,如果是class 类型,影响性能。由此得出结论如果类中有引用和const类型、基类没有默认构造函数、成员变量没有默认构造函数,那么一定需要初始化列表。

5. C++的虚拟继承是怎么一回事?
对于这个问题涉及到c++内存布局,不同编译器下布局还有些不同,很复杂。在继承过程中

  1. 基类成员变量都会被子类继承;
  2. 有虚函数情况下还会涉及虚函数指针和虚函数表;
  3. 虚继承;
    变量在c++继承会将基类成员变量全部继承过来,于是在多重继承下,比如钻石继承,基类成员变量会被重复继承,于是就使用虚继承解决这个问题。
    典型的钻石继承结构:
class Base {};

class Derive1 :  virtual public Base {...};

class Derive2 :  virtual   public Base {...};

class Derive3 :  public Derive1,public Derive2 {...};

具体可以看皓哥的文章,很好的动手实践文章,理论加持就是《深度探索c++对象模型》。

6. TCP的CLOSE_WAIT和TIME_WAIT是怎么一件事?在Wait什么?
在这里插入图片描述
这是连接释放过程,这个图可以结合写网络程序经验理解性的记忆。client 调用close关闭连接时会发送FIN给server,server 回复ACK 同时通知应用对端关闭,此时server应用可以选择不立马close socket ,而是继续发送数据,这就是CLOSE_WAIT,也可以立即close socket,于是server 发送FIN给client。
专业的说法是tcp是全双工,A端close socket后 tcp处于半关闭状态,A->B端链路已关闭,但是B->A端仍未关闭,可以继续发送数据。

TIME_WAIT 主动关闭一方才会出现,http服务器才会经常出现。这个设计有两个原因:

  1. B端超时收到ACK,可以继续发送FIN ;
  2. 避免刚结束的连接数据包出现在新连接中,可以想象假如跳过这个状态,立马又建立一个新链接,新连接B端可能会收到上一个连接的数据包。

更详细专业的论述看这里

7. TCP怎么区分识别一个链接的?
对于这个问题国内教科书一般都是讲四元组,可能是为了便于教学吧。然而完整标识一个tcp链接应该是在原有四元组基础上再加一个传输协议,因此确切的说是五元组。另一个标识链接的是nat。
nat:地址转换,内部网络的所有主机共享一个合法外部IP地址,解决公网ipv4地址不够用问题。通常使用的技术是端口多路复用,内网的主机(ip1:port1)访问外网通过路由器时内网地址会映射路由器的地址(ip2:port2),于是给外界的印象是请求起始端都是路由器,路由器在收到外网数据包时再做(ip2:port2)->(ip1:port1)转换。于是三者之间的通信就构成了一个链接吧。
ps:对于这个问题我是没有答对,受中文文档影响严重,可见学习it技术要多关注国外文档。另外nat也没有考虑在内,视野太窄。

8. TCP的TCP_NODELAY是怎么一回事?优缺点是什么?
当一个ip包比较小 ,小于1460=1500-20-20,只有几十个字节,那么有效负荷(data len/ (data len + tcp+ip header )就很低,没有提高宽带利用率,其次需要更多ack,停等待发送,无法提高吞吐量。于是tcp默认采用nagle算法,将小包合并减少要发送包数量以提高网络效率,但是同时也带来消息实时性差缺点。
因此,在数据量小,实时性要求比较高的场合下会设置TCP_NODELAY不开启nagle,比如我们常使用的ssh,这么做的缺点就是会造成网络中有大量的小包,宽带利用率低。

9. ARP是个什么协议,原理是什么?
ARP(Address Resolution Protocol) 地址解析协议,将 IP 地址映射到数据链路层的地址。网络层使用的是ip,链路层使用的mac,在给对端发送数据时,我们只知道ip,不知道mac,那么数据流从ip层到mac层,对端dest mac地址是如何寻找的?这个就是arp解决的问题。这个问题涉及考点:1 ip查找mac的原理 2 ip路由和转发过程(ip和mac地址在路由过程查找转换)
以这个图为例,假设A要给C发消息。
在这里插入图片描述
在链路层填充frame的过程中需要查找C的mac。首先从ARP 缓存中找,如果缓存没有,于是A发一个广播,询问ip=192.168.0.16的mac是多少,C收到这个请求后回复,于是A将C 的ip和对应的mac缓存。当数据经过交换机,根据dest 的mac将数据转发给C。
在这里插入图片描述
再看这张图源于Andrew S·Tanenbaum的《计算机网络》,主机1发数据给主机4,重点阐述了路由转发过程中srcip srcmac 和destip destmac的转换过程。当然这里可能忽略了nat,加上nat srcip srcmac变化更复杂。
从这里可以看到一个小知识点就可以引出一堆知识点。

10. C10K是个什么问题?
c10k对于现在的我们来说已经不是什么问题了,很轻松能编写出远超过这个的并发程序。比如redis 都达到 100k,普通一个c/c++写的应用程序也能有几万。但是背后的思想值得学习的,也是常见网络编程模型的发展历程,多进程->多线程->epoll 异步 ,除了古老的apache 是多进程,现代的redis/nginx/memcache都是epoll 异步。当然这种使得网络应用程序编写变得繁琐,要花很多精力处理异步和大量的socket连接管理。于是新型的go就采取了轻量级的线程,占用很少的堆栈,可以一个连接对应一个线程,回到了古老简单的编程模式。这里的知识点也很多,进程和线程相关的,甚至是并发通信模型,比如go的csp模型,erlang的actor模型,还有锁相关的,以及无锁队列等等,一切切都有利于提高并发。

11. 为什么epoll的性能会这么好?
内核实现:
红黑树:存放监听socket
链表:存放准备好的事件、当有事件产生时,将事件放入list中
相比select区别:

  1. 监听socket数量没有限制,select通常是1024,由内核sys/select.h FD_SETSIZE所制 ;
  2. select 每次设置事件都需要将所有监听fd数据结构传给内核,大量用户空间到内核空间拷贝,而epoll只需初始化一次;内核用红黑树记录了所有socket。
  3. select每次有事件都将整个事件列表返回,而epoll只返回准备好的事件,不用遍历检测;

这篇文章关于select/poll/epoll 论述的很详细,包括多线程下select epoll区别。

12. Libevent用过吗?什么原理?**
libevent是一个网络常用技术的封装,方便编写跨平台(android linux ios win)网络应用程序。

  1. 对各个平台的多路复用的封装 ;
  2. 对事件(定时事件,读写事件,信号)的管理,还有buffer缓存和对读写的处理;
  3. 提供一些http https 功能。

第二点才是网络程序编写最有挑战的,涉及异步事件处理,socket读写的处理,应用层缓冲区的处理。这里有高效节约内存的处理方式,比如用数组代替list/map,以fd为索引。有缓存的设计避免拷贝,还有减少系统调用的处理方式,总之是一个繁琐和令人exceting 的地方。
对于libevent的精简实现就是redis,一两个文件,实现了网络应用的基本功能。

这里我基本都是按我的思路写的,面试过程虽然用嘴说起来轻松,但是要真正写好很费力。
虽然这里有12个问题,如果仔细深挖,再将来龙去脉都串起来,能引出很多知识技术点。比如虚继承要讲清楚写清楚,必须要把《深度探索c++对象模型》这本书熟读一遍吧,ARP讲清楚必须得把tcp/ip 路由转发过程搞明白,而TCP的TCP_NODELAY的设置虽然是一个小知识点,但是背后涉及tcp 流式传输、拥塞 、性能一堆知识点,对于10k问题都能从操作系统进程/线程引到网络。

在写的过程还有一个强烈的感受,看到皓哥写的文章都是相当有深度,广征博引,因此我觉得这几个问题我难以写好。一方面是知识和视野有限,另一方面是写作水平,因此让我发到coolshell 让各路大牛看实在压力不小,更重要的是怕误导后者。因此接下来一定要让知识体系化,和标准化技术流。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值