C++面经漏洞汇总
C++中几种智能指针的区别
为什么要使用智能指针:
智能指针的作用是管理一个指针,因为存在以下这种情况:申请的空间在函数结束时忘记释放,造成内存泄漏。使用智能指针可以很大程度上的避免这个问题,因为智能指针就是一个类,当超出了类的作用域是,类会自动调用析构函数,析构函数会自动释放资源。所以智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间。
1. auto_ptr(c++98的方案,cpp11已经抛弃)
弃用原因:由于使用了转移语义(就是下面的p1将控制权转给p2),在使用p1会出现内存崩溃。
采用所有权模式。
auto_ptr< string> p1 (new string ("I reigned lonely as a cloud.”)); auto_ptr<string> p2; p2 = p1; //auto_ptr不会报错.
此时不会报错,p2剥夺了p1的所有权,但是当程序运行时访问p1将会报错。所以auto_ptr的缺点是:存在潜在的内存崩溃问题!
1、进行深度复制,有几个指针就复制几个对象;
2、制定指针专有权的概念。即,只有一个智能指针能真正指向一个特定的对象,也只有该指针能析构这个对象所占用的空间,直到把这个指针赋给另一个指针,后一个指针才能真正指向这个对象,而前一个指针就不再起作用了,从而避免了两次delete而导致的未定义行为。这个概念比较适合auto_ptr和unique_ptr,但后者要求更严格(unique_ptr);
3、记录性智能指针。即,有一个智能指针指向某对象,就把这个对象上的智能指针数加1,有一个指针不再指向该对象,就把这个对象上的智能指针数减1。只有当最后一个智能指针生命期结束了,才真正释放对象空间。(shared_ptr)
2. unique_ptr(替换auto_ptr)
unique_ptr实现独占式拥有或严格拥有概念,保证同一时间内只有一个智能指针可以指向该对象。它对于避免资源泄露(例如“以new创建对象后因为发生异常而忘记调用delete”)特别有用。
采用所有权模式,还是上面那个例子
1
2
3
|
unique_ptr<string> p3 ( new string ( "auto" )); //#4 unique_ptr<string> p4; //#5 p4 = p3; //此时会报错!! |
编译器认为p4=p3非法,避免了p3不再指向有效数据的问题。因此,unique_ptr比auto_ptr更安全。
另外unique_ptr还有更聪明的地方:当程序试图将一个 unique_ptr 赋值给另一个时,如果源 unique_ptr 是个临时右值,编译器允许这么做;如果源 unique_ptr 将存在一段时间,编译器将禁止这么做,比如:
unique_ptr<string> pu1(new string ("hello world")); unique_ptr<string> pu2; pu2 = pu1; // #1 not allowed unique_ptr<string> pu3; pu3 = unique_ptr<string>(new string ("You")); // #2 allowed
其中#1留下悬挂的unique_ptr(pu1),这可能导致危害(语义转移)。而#2不会留下悬挂的unique_ptr,因为它调用 unique_ptr 的构造函数,该构造函数创建的临时对象在其所有权让给 pu3 后就会被销毁。这种随情况而已的行为表明,unique_ptr 优于允许两种赋值的auto_ptr 。
注:如果确实想执行类似与#1的操作,要安全的重用这种指针,可给它赋新值。C++有一个标准库函数std::move(),让你能够将一个unique_ptr赋给另一个。例如:
unique_ptr<string> ps1, ps2; ps1 = demo("hello"); ps2 = move(ps1); ps1 = demo("alexia"); cout << *ps2 << *ps1 << endl;
3. shared_ptr
shared_ptr实现共享式拥有概念。多个智能指针可以指向相同对象,该对象和其相关资源会在“最后一个引用被销毁”时候释放。从名字share就可以看出了资源可以被多个指针共享,它使用计数机制来表明资源被几个指针共享。可以通过成员函数use_count()来查看资源的所有者个数。除了可以通过new来构造,还可以通过传入auto_ptr, unique_ptr,weak_ptr来构造。当我们调用release()时,当前指针会释放资源所有权,计数减一。当计数等于0时,资源会被释放。
shared_ptr 是为了解决 auto_ptr 在对象所有权上的局限性(auto_ptr 是独占的), 在使用引用计数的机制上提供了可以共享所有权的智能指针。
成员函数:
use_count 返回引用计数的个数
unique 返回是否是独占所有权( use_count 为 1)
swap 交换两个 shared_ptr 对象(即交换所拥有的对象)
reset 放弃内部对象的所有权或拥有对象的变更, 会引起原有对象的引用计数的减少
get 返回内部对象(指针)(将原始指针暴露出来), 由于已经重载了()方法, 因此和直接使用对象是一样的.如 shared_ptr<int> sp(new int(1)); sp 与 sp.get()是等价的
shared_ptr不要使用这样的方式初始化:
int
*p=
new
int
(1);
shared_ptr<
int
> p1(p);
cout<<p1.use_count()<<endl;
shared_ptr<
int
> p2(p);
cout<<p1.use_count()<<endl;
cout<<p2.use_count()<<endl;
int
*p=
new
int
(1);
shared_ptr<
int
> p1(p);
cout<<p1.use_count()<<endl;
shared_ptr<
int
> p2(p1);
cout<<p1.use_count()<<endl;
cout<<p2.use_count()<<endl;
4. weak_ptr
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。
weak_ptr无法管理对象,必须通过expired判断shared_ptr指向的对象是否可用,如果返回false则可用,可以通过lock()->func()来升为shared_ptr并调用func().
注意的是我们不能通过weak_ptr直接访问对象的方法,比如B对象中有一个方法print(),我们不能这样访问,pa->pb_->print(); 英文pb_是一个weak_ptr,应该先把它转化为shared_ptr,如:shared_ptr p = pa->pb_.lock(); p->print();
C/C++程序编译流程:
预处理->编译->汇编->链接
具体的就是:
源代码(source coprede)→预处理器(processor)→编译器(compiler)→汇编程序(assembler)→目标程序(object code)→链接器(Linker)→可执行程序(executables)
1. 预处理
预处理相当于根据预处理指令组装新的C/C++程序。经过预处理,会产生一个没有宏定义,没有条件编译指令,没有特殊符号的输出文件,这个文件的含义同原本的文件无异,只是内容上有所不同。
-
读取C/C++源程序,对其中的伪指令(以#开头的指令)进行处理
①将所有的“#define”删除,并且展开所有的宏定义
②处理所有的条件编译指令,如:“#if”、“#ifdef”、“#elif”、“#else”、“endif”等。这些伪指令的引入使得程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理。预编译程序将根据有关的文件,将那些不必要的代码过滤掉。
③处理“#include”预编译指令,将被包含的文件插入到该预编译指令的位置。
(注意:这个过程可能是递归进行的,也就是说被包含的文件可能还包含其他文件)
-
删除所有的注释
-
添加行号和文件名标识。
以便于编译时编译器产生调试用的行号信息及用于编译时产生的编译错误或警告时能够显示行号
-
保留所有的#pragma编译器指令
2. 编译
将预处理完的文件进行一系列词法分析、语法分析、语义分析及优化后,产生相应的汇编代码文件。
3. 汇编
将编译完的汇编代码文件翻译成机器指令,并生成可重定位目标程序的.o文件,该文件为二进制文件,字节编码是机器指令。
汇编器是将汇编代码转变成机器可以执行的指令,每一个汇编语句几乎都对应一条机器指令。所以汇编器的汇编过程相对于编译器来讲比较简单,它没有复杂的语法,也没有语义,也不需要做指令优化,只是根据汇编指令和机器指令的对照表一一翻译即可。
4. 链接
通过链接器将一个个目标文件(或许还会有库文件)链接在一起生成一个完整的可执行程序。
由汇编程序生成的目标文件并不能立即就被执行,其中可能还有许多没有解决的问题。
将生成的.obj文件与库文件.lib等文件链接,生成可执行文件(.exe文件)
例如,某个源文件中的函数可能引用了另一个源文件中定义的某个符号(如变量或者函数调用等);在程序中可能调用了某个库文件中的函数,等等。所有的这些问题,都需要经链接程序的处理方能得以解决。
链接程序的主要工作就是将有关的目标文件彼此相连接,也就是将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够被操作系统装入执行的统一整体。
memcmp,strcmp与strncmp:
memcmp:
int memcmp (const void *a1, const void *a2, size_t size)
功能是把存储区 str1 和存储区 str2 的前 n 个字节进行比较。该函数是按字节比较的。
-
如果返回值 < 0,则表示 str1 小于 str2。
-
如果返回值 > 0,则表示 str2 小于 str1。
-
如果返回值 = 0,则表示 str1 等于 str2。
对于memcmp(),如果两个字符串相同而且count大于字符串长度的话,memcmp不会在\0处停下来,会继续比较\0后面的内存单元,直到_res不为零或者达到count次数。
strcmp:
int strcmp (const char *s1, const char *s2)
用于比较两个字符串并根据比较结果返回整数。基本形式为strcmp(str1,str2),若str1=str2,则返回零;若str1<str2,则返回负数;若str1>str2,则返回正数
两个字符串自左向右逐个字符相比(按ASCII值大小相比较),直到出现不同的字符或遇'\0'为止
strncmp:
int strncmp (const char *s1, const char *s2, size_t size)
此函数与strcmp极为类似。不同之处是,strncmp函数是指定比较size个字符。也就是说,如果字符串s1与s2的前size个字符相同,函数返回值为0。
如果size为最长的长度,则strncmp等价于strcmp
区别:
memcpy与strcmp的比较:
功能比较:
二者都可以用于字符串的比较,但是二者是有比较大的差异的,因为strcmp是按照字节(byte-wise)比较的,并且比较的过程中会检查是否出现了"/0"结束符,一旦任意一个字符串指针前进过程中遇到结束符,将终止比较。而memcmp函数是用于比较两个内存块的内容是否相等,在用于字符串比较时通常用于测试字符串是否相等,不常进行byte-wise的字符串比较。
效率差异:
strcmp比较的字符串,而memcmp比较的是内存块,strcmp需要时刻检查是否遇到了字符串结束的 /0 字符,而memcmp则完全不用担心这个问题,所以memcmp的效率要高于strcmp
memcpy与strncpy比较:
对于memcmp(),如果两个字符串相同而且count大于字符串长度的话,memcmp不会在\0处停下来,会继续比较\0后面的内存单元,直到_res不为零或者达到count次数。
对于strncmp(),比较必定会在最短的字符串的末尾停下来,即使count还未为零。
UDP connect后和TCP有什么区别
1、UDP中可以使用connect系统调用。
2、UDP中connect操作与TCP中connect操作有着本质区别。TCP中调用connect会引起三次握手,client与server建立连结。UDP中调用connect内核仅仅把对端ip&port记录下来。
3、UDP中可以多次调用connect,TCP只能调用一次connect。
标准的udp客户端开了套接口后,一般使用sendto和recvfrom函数来发数据,最近看到ntpclient的代码里面是使用send函数直接法的,就分析了一下,原来udp发送数据有两种方法供大家选用的,顺便把udp的connect用法也就解释清楚了。
方法一:
socket----->sendto()或recvfrom()
方法二:
socket----->connect()----->send()或recv()
软连接,硬连接
在Linux的文件系统中,保存在磁盘分区中的文件不管是什么类型都给它分配一个编号,称为索引节点号inode 。
-
软连接,其实就是新建立一个文件,这个文件就是专门用来指向别的文件的(那就和windows 下的快捷方式的那个文件有很接近的意味)。软链接产生的是一个新的文件,但这个文件的作用就是专门指向某个文件的,删了这个软连接文件,那就等于不需要这个连接,和原来的存在的实体原文件没有任何关系,但删除原来的文件,则相应的软连接不可用(cat那个软链接文件,则提示“没有该文件或目录“)
- 硬连接是不会建立inode的,他只是在文件原来的inode link count域再增加1而已,也因此硬链接是不可以跨越文件系统的。相反是软连接会重新建立一个inode,当然inode的结构跟其他的不一样,他只是一个指明源文件的字符串信息。一旦删除源文件,那么软连接将变得毫无意义。而硬链接删除的时候,系统调用会检查inode link count的数值,如果他大于等于1,那么inode不会被回收。因此文件的内容不会被删除。
- 硬链接实际上是为文件建一个别名,链接文件和原文件实际上是同一个文件。可以通过ls -i来查看一下,这两个文件的inode号是同一个,说明它们是同一个文件;而软链接建立的是一个指向,即链接文件内的内容是指向原文件的指针,它们是两个文件。
- 软链接可以跨文件系统,硬链接不可以;
- 软链接可以对一个不存在的文件名(filename)进行链接(当然此时如果你vi这个软链接文件,linux会自动新建一个文件名为filename的文件),硬链接不可以(其文件必须存在,inode必须存在);
- 软链接可以对目录进行连接,硬链接不可以。
- 两种链接都可以通过命令 ln 来创建。ln 默认创建的是硬链接。
-
使用 -s 开关可以创建软链接。
VLAN
https://blog.51cto.com/6930123/2115373
1.1、什么是VLAN?
VLAN(Virtual LAN),翻译成中文是“虚拟局域网”。LAN可以是由少数几台家用计算机构成的网络,也可以是数以百计的计算机构成的企业网络。VLAN所指的LAN特指使用路由器分割的网络——也就是广播域
在此让我们先复习一下广播域的概念。广播域,指的是广播帧(目标MAC地址全部为1)所能传递到的范围,亦即能够直接通信的范围。严格地说,并不仅仅是广播帧,多播帧(Multicast Frame)和目标不明的单播帧(Unknown Unicast Frame)也能在同一个广播域中畅行无阻。
本来,二层交换机只能构建单一的广播域,不过使用VLAN功能后,它能够将网络分割成多个广播域。
1.2、未分割广播域时将会发生什么?
那么,为什么需要分割广播域呢?那是因为,如果仅有一个广播域,有可能会影响到网络整体的传输性能。具体原因,请参看附图加深理解。
图中,是一个由5台二层交换机(交换机1~5)连接了大量客户机构成的网络。假设这时,计算机A需要与计算机B通信。在基于以太网的通信中,必须在数据帧中指定目标MAC地址才能正常通信,因此计算机A必须先广播“ARP请求(ARP Request)信息”,来尝试获取计算机B的MAC地址。
交换机1收到广播帧(ARP请求)后,会将它转发给除接收端口外的其他所有端口,也就是Flooding了。接着,交换机2收到广播帧后也会Flooding。交换机3、4、5也还会Flooding。最终ARP请求会被转发到同一网络中的所有客户机上。
请大家注意一下,这个ARP请求原本是为了获得计算机B的MAC地址而发出的。也就是说:只要计算机B能收到就万事大吉了。可是事实上,数据帧却传遍整个网络,导致所有的计算机都收到了它。如此一来,一方面广播信息消耗了网络整体的带宽,另一方面,收到广播信息的计算机还要消耗一部分CPU时间来对它进行处理。造成了网络带宽和CPU运算能力的大量无谓消耗。
1.4、广播域的分割与VLAN的必要性
分割广播域时,一般都必须使用到路由器。使用路由器后,可以以路由器上的网络接口(LAN Interface)为单位分割广播域。
但是,通常情况下路由器上不会有太多的网络接口,其数目多在1~4个左右。随着宽带连接的普及,宽带路由器(或者叫IP共享器)变得较为常见,但是需要注意的是,它们上面虽然带着多个(一般为4个左右)连接LAN一侧的网络接口,但那实际上是路由器内置的交换机,并不能分割广播域。
况且使用路由器分割广播域的话,所能分割的个数完全取决于路由器的网络接口个数,使得用户无法自由地根据实际需要分割广播域。
与路由器相比,二层交换机一般带有多个网络接口。因此如果能使用它分割广播域,那么无疑运用上的灵活性会大大提高。
用于在二层交换机上分割广播域的技术,就是VLAN。通过利用VLAN,我们可以自由设计广播域的构成,提高网络设计的自由度。
2.1、实现VLAN的机制
在理解了“为什么需要VLAN”之后,接下来让我们来了解一下交换机是如何使用VLAN分割广播域的。
首先,在一台未设置任何VLAN的二层交换机上,任何广播帧都会被转发给除接收端口外的所有其他端口(Flooding)。例如,计算机A发送广播信息后,会被转发给端口2、3、4
这时,如果在交换机上生成红、蓝两个VLAN;同时设置端口1、2属于红色VLAN、端口3、4属于蓝色VLAN。再从A发出广播帧的话,交换机就只会把它转发给同属于一个VLAN的其他端口——也就是同属于红色VLAN的端口2,不会再转发给属于蓝色VLAN的端口。
同样,C发送广播信息时,只会被转发给其他属于蓝色VLAN的端口,不会被转发给属于红色VLAN的端口。
就这样,VLAN通过限制广播帧转发的范围分割了广播域。上图中为了便于说明,以红、蓝两色识别不同的VLAN,在实际使用中则是用“VLAN ID”来区分的。
2.2、直观地描述VLAN
如果要更为直观地描述VLAN的话,我们可以把它理解为将一台交换机在逻辑上分割成了数台交换机。在一台交换机上生成红、蓝两个VLAN,也可以看作是将一台交换机换做一红一蓝两台虚拟的交换机。
在红、蓝两个VLAN之外生成新的VLAN时,可以想象成又添加了新的交换机。
但是,VLAN生成的逻辑上的交换机是互不相通的。因此,在交换机上设置VLAN后,如果未做其他处理,VLAN间是无法通信的。
明明接在同一台交换机上,但却偏偏无法通信——这个事实也许让人难以接受。但它既是VLAN方便易用的特征,又是使VLAN令人难以理解的原因。
2.3、需要VLAN间通信时怎么办?
那么,当我们需要在不同的VLAN间通信时又该如何是好呢?
请大家再次回忆一下:VLAN是广播域。而通常两个广播域之间由路由器连接,广播域之间来往的数据包都是由路由器中继的。因此,VLAN间的通信也需要路由器提供中继服务,这被称作“VLAN间路由”。
VLAN间路由,可以使用普通的路由器,也可以使用三层交换机。其中的具体内容,等有机会再细说吧。在这里希望大家先记住不同VLAN间互相通信时需要用到路由功能。
无锁操作(CAS)
关于CAS等原子操作
在开始说无锁队列之前,我们需要知道一个很重要的技术就是CAS操作——Compare & Set,或是 Compare & Swap,现在几乎所有的CPU指令都支持CAS的原子操作,X86下对应的是 CMPXCHG 汇编指令。有了这个原子操作,我们就可以用其来实现各种无锁(lock free)的数据结构。
这个操作用C语言来描述就是下面这个样子:意思就是说,看一看内存*reg里的值是不是oldval,如果是的话,则对其赋值newval。
int compare_and_swap (int* reg, intoldval, intnewval) { int old_reg_val = *reg; if(old_reg_val == oldval) *reg = newval; returnold_reg_val; }
这个操作可以变种为返回bool值的形式(返回 bool值的好处在于,可以调用者知道有没有更新成功):
bool compare_and_swap (int*accum, int*dest, intnewval) { if( *accum == *dest ) { *dest = newval; returntrue; } returnfalse; }
在reg处写入之前,先要保存reg处的旧值,因为在写入一个值前,要防止别的线程抢占写入,因此在写入前先记住reg处的旧值,然后进去开始写,如果进入后reg的值与之前保存的值相同,说明在这之间没有线程抢占写入,因此可以直接写入;而如果与之前保存的值不同,说明在这之间有其他线程进入抢占了,并写入,这时候就要return false.return false后知道有抢占,因此尝试重新记住reg的新值,在在这个值得基础上重新尝试写入
无锁队列的链表实现
CAS实现的方式:
EnQueue(x)//进队列 { //准备新加入的结点数据 q = newrecord(); q->value = x; q->next = NULL; do{ p = tail; //取链表尾指针的快照 }while( CAS(p->next, NULL, q) != TRUE); //如果没有把结点链上,再试 CAS(tail, p, q); //置尾结点 }
首先q是新值,准备将q加入到队列尾,加入前首先记住当前时刻链表尾部的元素,写入新值就要在这个时刻的状态内写入,将这个元素记为p,然后在while中进行CAS写入,如果返回false,说明在p=tail后有其他线程抢占了CPU并向链表尾写入了元素, 因此不应该写入,重新回到{},记住当前的新的值,以此状态进行下一次写入尝试。
为什么我们的“置尾结点”的操作不判断是否成功,因为:
1.如果有一个线程T1,它的while中的CAS如果成功的话,那么其它所有的 随后线程的CAS都会失败,然后就会再循环,
2.此时,如果T1 线程还没有更新tail指针,其它的线程继续失败,因为tail->next不是NULL了。
3.直到T1线程更新完tail指针,于是其它的线程中的某个线程就可以得到新的tail指针,继续往下走了。
这里有一个潜在的问题——如果T1线程在用CAS更新tail指针的之前,线程停掉了,那么其它线程就进入死循环了。
下面是改良版的EnQueue()
EnQueue(x)//进队列改良版 { q = newrecord(); q->value = x; q->next = NULL; p = tail; oldp = p do{ while(p->next != NULL) p = p->next; }while( CAS(p.next, NULL, q) != TRUE); //如果没有把结点链上,再试 CAS(tail, oldp, q); //置尾结点 }
这里,p是通过自己不断->next来到达尾部的,可能你会有问题:这样的话不是会出现某个线程还未CAS(tail,oldp,q);置尾结点的情况下就有另一个线程抢占并插到尾部吗?
改良版本里,记录当前插入时刻队列的状态是通过p=p->next这样一直到尾部而不是通过tail的方式。
这样首先第一点:不用担心某个线程崩溃而造成死循环的问题了,因为记录当前状态不是依靠tail,即使tail不更新也没问题。
其次,即使某个线程T1,在置尾结点前,另一个线程T2又将新节点插入到尾部了,这也不影响,因为tail只是一个记录当前尾部的指针,对其他线程来说,他们记录当前的尾部还是通过不断next来遍历的,即使tail不在尾部他也能找到正确的尾部状态。
time_wait状态如何避免
(1)首先服务器可以设置SO_REUSEADDR套接字选项来通知内核,如果端口忙,但TCP连接位于TIME_WAIT状态时可以重用端口。在一个非常有用的场景就是,如果你的服务器程序停止后想立即重启,而新的套接字依旧希望使用同一端口,此时SO_REUSEADDR选项就可以避免TIME_WAIT状态。
(2)由于time_wait状态是在主动关闭的一方出现的,所以在设计协议逻辑的时候,尽量由客户端主动关闭,避免服务端出现time_wait
(3)so_linger设置 l_onoff为非0,l_linger为0,则套接口关闭时TCP夭折连接,TCP将丢弃保留在套接口发送缓冲区中的任何数据并发送一个RST给对方,而不是通常的四分组终止序列,这避免了TIME_WAIT状态;
l_onoff表示开关,而l_linger表示优雅关闭的最长时限
服务端大量time_wait
我来解释下这个场景。主动正常关闭TCP连接,都会出现TIMEWAIT。为什么我们要关注这个高并发短连接呢?有两个方面需要注意:
1. 高并发可以让服务器在短时间范围内同时占用大量端口,而端口有个0~65535的范围,并不是很多,刨除系统和其他服务要用的,剩下的就更少了。
2. 在这个场景中,短连接表示“业务处理+传输数据的时间 远远小于 TIMEWAIT超时的时间”的连接。这里有个相对长短的概念,比如,取一个web页面,1秒钟的http短连接处理完业务,在关闭连接之后,这个业务用过的端口会停留在TIMEWAIT状态几分钟,而这几分钟,其他HTTP请求来临的时候是无法占用此端口的。单用这个业务计算服务器的利用率会发现,服务器干正经事的时间和端口(资源)被挂着无法被使用的时间的比例是 1:几百,服务器资源严重浪费。(说个题外话,从这个意义出发来考虑服务器性能调优的话,长连接业务的服务就不需要考虑TIMEWAIT状态。同时,假如你对服务器业务场景非常熟悉,你会发现,在实际业务场景中,一般长连接对应的业务的并发量并不会很高)
综合这两个方面,持续的到达一定量的高并发短连接,会使服务器因端口资源不足而拒绝为一部分客户服务。同时,这些端口都是服务器临时分配,无法用SO_REUSEADDR选项解决这个问题:
SO_LINGER作用
设置函数close()关闭TCP连接时的行为。缺省close()的行为是,如果有数据残留在socket发送缓冲区中则系统将继续发送这些数据给对方,等待被确认,然后返回。
利用此选项,可以将此缺省行为设置为以下两种
a.立即关闭该连接,通过发送RST分组(而不是用正常的FIN|ACK|FIN|ACK四个分组)来关闭该连接。至于发送缓冲区中如果有未发送完的数据,则丢弃。主动关闭一方的TCP状态则跳过TIMEWAIT,直接进入CLOSED。网上很多人想利用这一点来解决服务器上出现大量的TIMEWAIT状态的socket的问题,但是,这并不是一个好主意,这种关闭方式的用途并不在这儿,实际用途在于服务器在应用层的需求。
b.将连接的关闭设置一个超时。如果socket发送缓冲区中仍残留数据,进程进入睡眠,内核进入定时状态去尽量去发送这些数据。
在超时之前,如果所有数据都发送完且被对方确认,内核用正常的FIN|ACK|FIN|ACK四个分组来关闭该连接,close()成功返回。
如果超时之时,数据仍然未能成功发送及被确认,用上述a方式来关闭此连接。close()返回EWOULDBLOCK。
为什么vector的存储对象会析构多次
MyClass::~MyClass()
{
cout << "destroy" << data << endl;
}
vector<MyClass> vc({ MyClass(0), MyClass(1), MyClass(2) });
cout<<"***************************"<<endl;
return 0;
程序显示
前三个析构的是你初始化容器时定义的三个临时对象
如果需要在析构函数里delete,那么一般就是在构造函数里面new,然后你应该还需要补完复制/移动语义。
一个空类的内部组成
并不见得,任何一个类中都有六个默认的成员函数,空类也不例外
六大默认成员函数分别是:
构造函数
拷贝构造函数
析构函数
赋值运算符重载
取地址操作符重载
返回类的地址
类名* operator&() { return this; }
被const修饰的取地址操作符重载
返回const A*型指针,这个指针为底层const,表示不可通过这个指针改变它指向的值,也就是不可通过这个返回的指针改变该类
const 类名* operator&()const { return this; }
进程组
ps ax|grep nfsd
父进程退出了,子进程会咋样
若父进程退出,子进程尚未结束,则子进程会被init进程领养,也就是说init进程将成为该子进程的父进程。
子进程继承了父进程的什么
共同点:
用户号UIDs和用户组号GIDs
环境Environment
堆栈
共享内存
打开文件的描述符
执行时关闭(Close-on-exec)
标志
信号(Signal)
控制设定
进程组号
当前工作目录
根目录
文件方式创建屏蔽字
资源限制
控制终端
子进程独有
进程号PID
不同的父进程号
自己的文件描述符和目录流的拷贝
子进程不继承父进程的进程正文(text),数据和其他锁定内存(memory locks) 不继承异步输入和输出
父进程和子进程拥有独立的地址空间和PID参数。
nohup
& : 指在后台运行
nohup : nohup运行命令可以使命令永久的执行下去,和用户终端没有关系,例如我们断开SSH连接都不会影响他的运行,注意了nohup没有后台运行的意思;&才是后台运行
&是指在后台运行,但当用户推出(挂起)的时候,命令自动也跟着退出
进程间的通信方式有哪些,线程间的通信方式有哪些,它们之间有哪些区别。
进程和线程的区别:
对于进程来说,子进程是父进程的复制品,从父进程那里获得父进程的数据空间,堆和栈的复制品。
而线程,相对于进程而言,是一个更加接近于执行体的概念,可以和同进程的其他线程之间直接共享数据,而且拥有自己的栈空间,拥有独立序列。
共同点: 它们都能提高程序的并发度,提高程序运行效率和响应时间。线程和进程在使用上各有优缺点。 线程执行开销比较小,但不利于资源的管理和保护,而进程相反。同时,线程适合在SMP机器上运行,而进程可以跨机器迁移。
他们之间根本区别在于 多进程中每个进程有自己的地址空间,线程则共享地址空间。所有其他区别都是因为这个区别产生的。比如说:
1. 速度。线程产生的速度快,通讯快,切换快,因为他们处于同一地址空间。
2. 线程的资源利用率好。
3. 线程使用公共变量或者内存的时候需要同步机制,但进程不用。
而他们通信方式的差异也仍然是由于这个根本原因造成的。
通信方式之间的差异
因为那个根本原因,实际上只有进程间需要通信,同一进程的线程共享地址空间,没有通信的必要,但要做好同步/互斥,保护共享的全局变量。
而进程间通信无论是信号,管道pipe还是共享内存都是由操作系统保证的,是系统调用.
一、进程间的通信方式
管道( pipe ):
管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
有名管道 (namedpipe) :
有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
信号量(semophore ) :
信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
消息队列( messagequeue ) :
消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
信号 (sinal ) :
信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
共享内存(shared memory ) :
共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。
套接字(socket ) :
套接口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同设备及其间的进程通信。
二、线程间的通信方式
锁机制:包括互斥锁、条件变量、读写锁
互斥锁提供了以排他方式防止数据结构被并发修改的方法。
读写锁允许多个线程同时读共享数据,而对写操作是互斥的。
条件变量可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件的测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。
信号量机制(Semaphore):包括无名线程信号量和命名线程信号量
信号机制(Signal):类似进程间的信号处理
线程间的通信目的主要是用于线程同步,所以线程没有像进程通信中的用于数据交换的通信机制
new 和 malloc,new出来的对象用free释放会发生什么情况
new()函数实际过程中做了两步操作,第一步是分配内存空间,第二步是调用类的构造函数;delete()也同样是两步,第一步是调用类的析构函数,第二步才是释放内存;而malloc()和free()仅仅是分配内存与释放内存操作;
那么如果通过new分配的内存,再用free去释放,就会少一步调用析构函数的过程。
const用法
const的用法和注意事项:
①:被const修饰的变量可以阻止这个变量被改变,被const修饰的变量要进行初始化。
②:可以指定const指针也可以将指针指向的变量指定为const;
③:被const关键字修饰的形参的值在函数内部不能被改变;
④:const修饰的成员函数不能修改任何的成员变量(mutable修饰的变量除外);const成员函数不能调用非const成员函数,因为非const成员函数可能会修改成员变量。
⑤:const可以修饰函数返回值,使之不能被改变。
define和const的区别
①:const定义的常数是变量,也带类型,#define定义的只是个常数,不带类型。
②:define在预处理阶段进行替换;const在编译时确定其值;
③:define只进行简单的字符串替换,没有类型检查;而const由对应的数据类型,编译时会进行类型检查。
④:define定义的变量在预处理后存放在代码段的空间里,const修饰的变量占用数据段空间。
⑤:宏定义的作用范围仅限于当前文件,const对象使用extern声明后可以在多个文件之间共享。
const成员函数和非const成员函数的区别
①:const和非const函数是可以构成重载的,但是仅凭返回值是否为const是无法构成重载的。
class a
{
public:
int x()
{......}
int x() const
{......}
};
这样的 const 重载是允许的 但是什么时候会调用 x()
什么时候会调用 x() const ? 他们又有什么不同呢?
const a a1;
a a2;
a1.x();
a2.x();
a1调用const版本,a2调用非const版本
②:若将成员函数声明为const,则该函数不允许修改类的数据成员。值得注意的是,把一个成员函数声明为const可以保证这个成员函数不修改数据成员,但是,如果据成员是指针,则const成员函数并不能保证不修改指针指向的对象,编译器不会把这种修改检测为错误。
③:const成员函数可以被具有相同参数列表的非const成员函数重载。
④:const成员函数可以访问非const对象的非const数据成员、const数据成员,也可以访问const对象内的所有数据成员,但不能修改任何变量;非const成员函数可以访问非const对象的数据成员、const数据成员,但不可以访问const对象的任意数据成员。
⑤:const成员函数只能用于非静态成员函数,不能用于静态成员函数。
C++ const和static类中使用要注意的
①:static数据成员的初始化必须在类外初始化;const数据成员必须在构造函数初始化列表中进行初始化,因为类成员不能声明初始化,同时const成员不能在成员函数中赋值,因为const不能被改变。
②:const成员函数不能改变任何一个数据成员的值,但是可以改变static成员的值
5个进程如何互斥按序访问数据
用一个数表示5个进程的访问顺序。通过条件变量来控制,当4个进程被唤醒时分别判断当前数的值,满足条件的运行,其他的阻塞。
vector增长模式机制
如果是成倍增长:
设每次增加m倍空间
则第一次m个,第二次扩容后有m^2个,第三次有m^3个......则m^(log(m)n)次后,有n个元素。
也就是说要增长到n个的情况,需要log(m)n次。
每次移动m^i个元素,则共移动:
用等比数列求和可以得到。
如果每次增长固定大小:
设每次增长k个
第一次k个元素,第二次2k个元素,第三次3k个元素....则n/k次后才会增长到n个元素。
每次移动k*i个元素,则共移动:
等差数列求和可以求到。
倍增的方式复杂度在O(n),而固定增长模式复杂度在O(n^2).
App消息的推送机制,用什么实现
目前的Push技术实现基本都是Client主动连接Server,钻牛角尖来讲,现在的Push其实都是伪Push。下面简单讲两种方式:
一、轮询法:
这种方法最简单,Client每过一段时间向Server请求一次数据。优缺点很明显,优点是实现简单;缺点是间隔时间不好控制,并且消耗大(电量、流量)。
二、长连接法:
还是从socket入手(又是这货?),Client使用socket连接Server,并且保持socket连接,Server随时可以通过这个socket发送数据给Client。
优点:最有效,客户端设备消耗比第一种小(设备应该从系统层对socket的长连接做优化,socket链接维护成本从客户端来讲应该是小于频繁的http请求的);
缺点:服务端压力大,每一个设备都需要一个socket连接。
还有一些其他协议比如xmpp,其实也逃不过上面两种方式,只是做了一些封装。或者还有一种非互联网方式的做法,比如监听短信法,要push的时候,先发一条手机到目的手机,Client监听到了标的短信,然后向Server请求数据,不过像这类剑走偏锋的方法,限制条件也很多,不是很实用。
总结一下,目前各个推送平台的实现都是基于长连接法的,如果App要自己实现推送,也是建议使用这种方式。但是如果每个App都用一个长连接,那么手机也吃不消了,所以又有一些其他技术来实现,我们下篇再讲。
数组循环右移
如[1,2,3,4,5,6]右移两位:
[5,6,1,2,3,4]
暴力法需要每移动一个数都要整个数组右移,这样复杂度在O(n^2)
可以利用
[A B]逆置=[B逆置 A逆置]来实现
首先对A与B分别逆置:
[4 3 2 1 6 5],然后整体逆置:[5 6 1 2 3 4]
逆置只要用两个指针指向首尾然后交换就好了,类似于字符串的翻转
C++怎么实现随机数
rand()与srand()
这两个都能生成随机数,生成后在要取的范围内取余,比如生成0~100的随机数,可以用rand%100
当需要负数时,比如[-2,2],可以rand()%5,这时生成的是0~4,然后对rand()%5-2,即可得到-2~2
要取得[0,n) 就是rand()%n 表示 从0到n-1的数
要取得[a,b)的随机整数,使用(rand() % (b-a))+ a;
要取得[a,b]的随机整数,使用(rand() % (b-a+1))+ a;
要取得(a,b]的随机整数,使用(rand() % (b-a))+ a + 1;
通用公式:a + rand() % n;其中的a是起始值,n是整数的范围。
要取得a到b之间的随机整数,另一种表示:a + (int)b * rand() / (RAND_MAX + 1)。
要取得0~1之间的浮点数,可以使用rand() / double(RAND_MAX)。
rand与srand
rand:
计算机实际上并没有真正做到产生一个随机数,只是在一串预先定义好的数据中选择一个返回给函数。
当要产生多个随机数时,rand()会重复调用产生相同的数字序列。如果想要每次执行产生的随机数不同,就需要进行随机初始化。因此引入srand()函数.
srand:
void srand(unsigned seed);
功能:根据随机数生成器的种子seed的值初始化随机数。
我们当然可以用数组和循环来设置种子的值,那么有没有什么我们可以直接利用的一直变化的值呢?
当然有,时间就是。我们可以借助time.h头文件中的time(NULL)返回机器当前的时间。
srand(time(NULL));
多重继承下的虚函数表
虚函数表,以及虚函数指针:
1)每个有虚函数的类都有自己的虚函数表,每个包含虚函数的类对象都有虚函数表指针。
2)对于多重继承,如果多个基类都有虚函数,则继承类中包含多个基类虚函数表,子类的虚函数地址放在声明的第一个基类虚函数表后面(即新增的虚函数:多态情况下基类指针只能调用派生类所重写的虚函数而不能调用新增的虚函数,因此增加在第一个虚表中不影响。如果想要调用新增的虚函数,要使用指针强制转换)。
3)计算类对象的内存大小的时候,需要计算有多少个虚函数指针。
一般继承(无虚函数覆盖)
假设有如下所示的一个继承关系:
在这个继承关系中,子类没有重载任何父类的函数。那么,在派生类的实例中,其虚函数表如下所示:
对于实例:Derive d; 的虚函数表如下:
我们可以看到下面几点:
1)虚函数按照其声明顺序放于表中。
2)父类的虚函数在子类的虚函数前面。
一般继承(有虚函数覆盖)
我们从表中可以看到下面几点,
1)覆盖的f()函数被放到了虚表中原来父类虚函数的位置。
2)没有被覆盖的函数依旧。
多重继承(无虚函数覆盖)
下面,再让我们来看看多重继承中的情况,假设有下面这样一个类的继承关系。注意:子类并没有覆盖父类的函数
对于子类实例中的虚函数表,是下面这个样子:
1) 每个父类都有自己的虚表。
2) 子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)
这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。(强制转换的时候使用dynamic_cast)
多重继承(有虚函数覆盖)
下面我们再来看看,如果发生虚函数覆盖的情况。
下图中,我们在子类中覆盖了父类的f()函数。
我们可以看见,三个父类虚函数表中的f()的位置被替换成了子类的函数指针。
为什么构造函数不能定义为虚函数
虚函数相应一个指向vtable虚函数表的指针,但是这个指向vtable的指针事实上是存储在对象的内存空间的。假设构造函数是虚的,就须要通过 vtable来调用,但是对象还没有实例化,也就是内存空间还没有,怎么找vtable呢?所以构造函数不能是虚函数。
C++类型转换
static_cast
static_cast的转换格式:static_cast <type-id> (expression)
将expression转换为type-id类型,主要用于非多态类型之间的转换,不提供运行时的检查来确保转换的安全性。主要在以下几种场合中使用:
- 用于类层次结构中,基类和子类之间指针和引用的转换;
当进行上行转换,也就是把子类的指针或引用转换成父类表示,这种转换是安全的;
当进行下行转换,也就是把父类的指针或引用转换成子类表示,这种转换是不安全的,也需要程序员来保证; - 用于基本数据类型之间的转换,如把int转换成char,把int转换成enum等等,这种转换的安全性需要程序员来保证;
- 把void指针转换成目标类型的指针,是及其不安全的;
注:static_cast不能转换掉expression的const、volatile和__unaligned属性。
dynamic_cast
dynamic_cast的转换格式:dynamic_cast <type-id> (expression)
将expression转换为type-id类型,type-id必须是类的指针、类的引用或者是void *;如果type-id是指针类型,那么expression也必须是一个指针;如果type-id是一个引用,那么expression也必须是一个引用。
dynamic_cast主要用于类层次间的上行转换和下行转换,还可以用于类之间的交叉转换。在类层次间进行上行转换时,dynamic_cast和static_cast的效果是一样的;在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全。在多态类型之间的转换主要使用dynamic_cast,因为类型提供了运行时信息
如果expression是type-id的基类,使用dynamic_cast进行转换时,在运行时就会检查expression是否真正的指向一个type-id类型的对象,如果是,则能进行正确的转换,获得对应的值;否则返回NULL,如果是引用,则在运行时就会抛出异常
安全使用dynamic_cast:
const_cast
const_cast的转换格式:const_cast <type-id> (expression)
const_cast用来将类型的const、volatile和__unaligned属性移除。常量指针被转换成非常量指针,并且仍然指向原来的对象(常量指针指指向常量的指针;指针常量指指针自己是一个常量);常量引用被转换成非常量引用,并且仍然引用原来的对象。
reinterpret_cast
reinterpret_cast的转换格式:reinterpret_cast <type-id> (expression)
允许将任何指针类型转换为其它的指针类型;听起来很强大,但是也很不靠谱。它主要用于将一种数据类型从一种类型转换为另一种类型。它可以将一个指针转换成一个整数,也可以将一个整数转换成一个指针,在实际开发中,先把一个指针转换成一个整数,在把该整数转换成原类型的指针,还可以得到原来的指针值;特别是开辟了系统全局的内存空间,需要在多个应用程序之间使用时,需要彼此共享,传递这个内存空间的指针时,就可以将指针转换成整数值,得到以后,再将整数值转换成指针,进行对应的操作。
为什么要进行内存对齐?
cpu把内存当成是一块一块的,块的大小可以是2,4,8,16 个字节,因此CPU在读取内存的时候是一块一块进行读取的而不是一个字节一个字节读取,块的大小称为(memory granularity)内存读取粒度。
我们再来看看为什么内存不对齐会影响读取速度?
假设CPU要读取一个4字节大小的数据到寄存器中(假设内存读取粒度是4),分两种情况讨论:
1.数据从0字节开始
2.数据从1字节开始
解析:当数据从0字节开始的时候,直接将0-3四个字节完全读取到寄存器,结算完成了。
当数据从1字节开始的时候,问题很复杂,首先先将前4个字节读到寄存器,并再次读取4-7字节的数据进寄存器,接着把0字节,4,6,7字节的数据剔除,最后合并1,2,3,4字节的数据进寄存器,对一个内存未对齐的寄存器进行了这么多额外操作,大大降低了CPU的性能。
为什么空类的大小为1?
答:
1)空类也要给空间,标识在空间中的确存在过。当我们声明一个函数时,它必须在内存中占有一定的空间,否则无法得到这些事例。
2)操作系统对内存的读取是按照字节,因此即使是空类,也至少有一个字节,否则无法使用这些类。
3)举个例子:如果空类的大小为0,如果有两个空类A B,那么类创建的对象 a,b在内存上是重叠的(取地址 A B得到的就是相同的)——不符合语法或者操作系统规定
内存对齐规则:
struct或union成员对齐规则如下:
对齐规则如下:
原则一:存放的首地址偏移量 % min(当前类型大小,对齐系数) == 0
原则二:结构体整体对齐,也称作二次对齐,结构体整体大小 % min(当前类型最大的大小,对齐系数) == 0
为什么默认的析构函数不是虚函数
因为只有在需要派生和多态的情况下才需要将析构函数声明为虚函数,如果不需要这些功能则没必要,因为虚函数会引入虚指针与虚函数表,从而引入不必要的浪费。
Shared_ptr的实现:
Shared_ptr实现值得注意的地方:
1.数据成员只有两个:
一个T*相当于真正的原始指针
一个count(这里为rm)计数保存引用计数,应该声明为static变量,这个变量属于全体类
2.什么时候调用指向对象的析构:
无论是reset还是调用赋值,还是shared_ptr自己析构,都要先判断引用计数的值,如果等于0,此时应该发生指向对象的析构
3.*与->的实现:
重载*与->:
对于*,应该返回*T,对于->应该返回指针T
template<typename T> class Shared_Ptr { public: //用来接收一个指针 Shared_Ptr(T* ptr) :mptr(ptr) { //由引用计数器负责增加指针的引用数 AddRef(); } //拷贝构造函数也会增加该指针的引用数 Shared_Ptr(const Shared_Ptr<T>& rhs):mprt(rhs.mptr) { //同样增加引用交给引用计数器 AddRef(); } //赋值运算符的重载 Shared_Ptr<T>& operator=(const Shared_Ptr<T>& rhs) { //判断自赋值 if(this != &rhs) { //调用析构函数释放当前指针的指向 this->~Shared_Ptr(); mptr = rhs.mptr; AddRef(); } return *this; } ~Shared_Ptr() { //由引用计数器负责减少指针的引用数 DelRef(); //获取引用计数,若引用计数为0,则释放该指针 if(GetRef() == 0) { delete mptr; } mptr = NULL; } //重载->运算符 T* operator->() { return mptr; } //重载解引用运算符 T& operator*() { return *mptr; } int GetRef() { //实质上返回的是引用计数器类中的 mptr 指针的引用计数 return rm.getref(mptr); } private: void AddRef() { rm.addref(mptr); } void DelRef() { rm.delref(mptr); } //Shared_Ptr的成员有一个 指针用来接收原生指针和 引用计数器 T* mptr; //将引用计数器设置成静态成员,摆脱对类的依赖 ---记得在类外初始化 static Ref_Manggement rm; };
C++地址空间布局
一、首先进程地址空间的 1G 内核空间是给操作系统使用的,我们用户是没有操作权限的。
二、剩下的 3G 内存空间中,分为了栈区、内存映射段、堆区、数据段、bss段、代码段
1)栈区:这里的栈和数据结构的栈并不相同,数据结构的栈是一种后进先出的数据结构,而内存划分的栈是操作系统按照栈的特性,给用户划分出的内存区间。
栈区一般存放:函数体的局部变量、函数调用期间的所有参数压栈、函数的返回值
注意栈区这段内存是由操作系统自己维护的,所以函数结束,在栈上的空间会由操作系统自己回收。
2)堆区:用户所操作的内存就是堆上的空间,用户可以使用 malloc / calloc / realloc / new 申请堆上的空间,但是用户申请堆上的空间必须自己手动释放,不然会造成内存泄漏。
3)内存映射段:里面存放 动态库 / 静态库,以及文件映射,匿名映射等等一切有依赖性的东西都在这段区域
4)一个程序本质上都是由 bss段、数据段、代码段组成的
数据段:存放全局变量、静态类型的变量。当代码编译完后,在可执行程序这个文件中已经把这些数据的空间划分好了,这种类型的数据,在程序运行以前,操作系统就将数据段中的数据加载到内存了。也就是说在进入 main 函数之前这些数据已经划分好空间了。
bss段:其实在 C 语言中,数据段中还有一个 bss 段,这里面存放的是未初始化的全局变量和静态数据,而数据段中存放的是已经初始化过的全局变量和静态数据。数据段中的所有数据已经划分好空间了,但是 bss 段并没有给其中的数据划分空间。
代码段:存放可执行代码,以及只读常量(字符串常量等等)。这段内存是只读的(实际上对一个进程来说,它已经是一个可执行文件了,因此代码区存放的应该是机器指令而不是语言)。
链接
链接器是一个将编译器产生的目标文件打包成可执行文件或者库文件或者目标文件的程序。这个翻译比较拗口,不太好理解,这句话的意思具体如下:
首先是链接器的本质,链接器本质上也是一个程序,本质上和我们经常使用的普通程序没什么不同。
其次是链接器的输入,我们经常使用的程序比如播放器,其输入是一个MP4文件,而链接器的输入是编译器编译好的目标文件
最后是链接器的输出,链接器在将目标文件打包处理后,生成或者可执行文件,或者库,或者目标文件。
静态链接与动态链接
库与可执行文件
在链接器可操作的元素这一节中我们提到,链接器可以操作的最小单元为目标文件,也就是说我们见到的无论是静态库、动态库、可执行文件,都是基于目标文件构建出来的。目标文件就好比乐高积木中最小的零部件。
给定目标文件以及链接选项,链接器可以生成两种库,分别是静态库以及动态库,如图所示,给定同样的目标文件,链接器可以生成两种不同类型的库,接下来我们分别介绍。
静态库
假设这样一个应用场景,基础设计团队设计了好多实用并且功能强大的工具函数,业务团队需要用到里面的各种函数。每次新添加其中一个函数,业务团队都要去找相应的实现文件并修改链接选项。使用静态库就可以解决这个问题。静态库在Windows下是以.lib为后缀的文件,Linux下是以.a为后缀的文件。
为解决上述问题,基础设计团队可以提前将工具函数集合打包编译链接成为静态库提供给业务团队使用,业务团队在使用时只要链接该静态库就可以了,每次新使用一个工具函数的时候,只要该函数在此静态库中就无需进行任何修改。
你可以简单的将静态库理解为由一堆目标文件打包而成, 使用者只需要使用其中的函数而无需关注该函数来自哪个目标文件(找到函数实现所在的目标文件是链接器来完成的,从这里也可以看出,不是所有静态库中的目标文件都会用到,而是用到哪个链接器就链接哪个)。静态库极大方便了对其它团队所写代码的使用。
静态连接
静态库是链接器通过静态链接将其和其它目标文件合并生成可执行文件的,如下图一所示,而静态库只不过是将多个目标文件进行了打包,在链接时只取静态库中所用到的目标文件,因此,你可以将静态链接想象成如下图2所示的过程。
静态库是使用库的最简单的方法,如果你想使用别人的代码,找到这些代码的静态库并简单的和你的程序链接就可以了。静态链接生成的可执行文件在运行时不依赖任何其它代码,要理解这句话,我们需要知道静态链接下,可执行文件是如何生成的。
静态链接下可执行文件的生成
在上一节中我们知道,可以将静态链接简单的理解为链接器将使用到的目标文件集合进行拼装,拼装之后就生成了可执行文件,同时我们在目标文件里有什么这一节中知道,目标文件分成了三段,代码段,数据段,符号表,那么在静态链接下可执行文件的生成过程如图所示:
从下图中我们可以看到可执行文件的特点:
- 可执行文件和目标文件一样,也是由代码段和数据段组成。
- 每个目标文件中的数据段都合并到了可执行文件的数据段,每个目标文件当中的代码段都合并到了可执行文件的代码段。
- 目标文件当中的符号表并没有合并到可执行文件当中,因为可执行文件不需要这些字段。
可执行文件和目标文件没有什么本质的不同,可执行文件区别于目标文件的地方在于,可执行文件有一个入口函数,这个函数也就是我们在C语言当中定义的main函数,main函数在执行过程中会用到所有可执行文件当中的代码和数据。而这个main函数是被谁调用执行的呢,答案就是操作系统
静态链接是使用库的最简单最直观的形式, 从静态链接生成可执行文件的过程中可以看到,静态链接会将用到的目标文件直接合并到可执行文件当中,想象一下,如果有这样的一种静态库,几乎所有的程序都要使用到,也就是说,生成的所有可执行文件当中都有一份一模一样的代码和数据,这将是对硬盘和内存的极大浪费,假设一个静态库为2M,那么500个可执行文件就有1G的数据是重复的。如何解决这个问题呢,答案就是使用动态库。
动态库
动态库(Dynamic Library),又叫共享库(Shared Library),动态链接库等,在Windows下就是我们常见的大名鼎鼎的DLL文件了,Windows系统下大量使用了动态库。在Linux下动态库是以.so为后缀的文件,同时以lib为前缀,比如进行数字计算的动态库Math,编译链接后产生的动态库就叫做libMath.so。从名字中我们知道动态库也是库,本质上动态库同样包含我们已经熟悉的代码段、数据段、符号表。只不过动态库的使用方式以及使用时间和静态库不太一样。
在前面几个小节中我们知道,使用静态库时,静态库的代码段和数据段都会直接打包copy到可执行文件当中,使用静态库无疑会增大可执行文件的大小,同时如果程序都需要某种类型的静态库,比如libc,使用静态链接的话,每个可执行文件当中都会有一份同样的libc代码和数据的拷贝,如图所示,动态库的出现解决了此类问题。
动态库允许使用该库的可执行文件仅仅包含对动态库的引用而无需将该库拷贝到可执行文件当中。也就是说,同静态库进行整体拷贝的方式不同,对于动态库的使用仅仅需要可执行文件当中包含必要的信息即可,为了方便理解,你可以将可执行文件当中保存的必要信息仅仅理解为需要记录动态库的名字就可以了,如图所示,同静态库相比,动态库的使用减少了可执行文件的大小。
动态链接
我们知道静态库在编译链接期间就被打包copy到了可执行文件,也就是说静态库其实是在编译期间(Compile time)链接使用的,那么动态库又是在什么时候才链接使用的呢,动态链接可以在两种情况下被链接使用,分别是load-time dynamic linking(加载时动态链接) 以及 run-time dynamic linking(运行时动态链接),接下来我们分别讲解一下。
1.load-time dynamic linking(加载时动态链接)
首先可能有的同学会问,什么是load-time呢,load_time翻译过来也就是加载时,那么什么又是加载呢?
我们大家都玩过游戏,当我们打开游戏的时候经常会跳出来一句话:“加载中,请稍后。。。”和这里的加载意思差不多。这里的加载指的是程序的加载,而所谓程序的加载就是把可执行文件从磁盘搬到内存的过程,因为程序最终都是在内存中被执行的。
当把可执行文件复制到内存后,且在程序开始运行之前,操作系统会查找可执行文件依赖的动态库信息(主要是动态库的名字以及存放路径),找到该动态库后就将该动态库从磁盘搬到内存,并进行符号决议(关于符号决议,参考符号决议一节),如果这个过程没有问题,那么一切准备工作就绪,程序就可以开始执行了,如果找不到相应的动态库或者符号决议失败,那么会有相应的错误信息报告为用户,程序运行失败
从总体上看,加载时动态链接可以分为两个阶段:阶段一,将动态库信息写入可执行文件;阶段二,加载可执行文件时依据动态库信息进行动态链接。
2.接下来我们讲解第二种动态链接,run-time dynamic linking(运行时动态链接
上一小节中我们看到如果我们想使用加载时动态链接,那么在编译链接生成可执行文件阶段时需要告诉编译器所依赖的动态库信息,而run-time dynamic linking 运行时动态链接则不需要在编译链接时提供动态库信息,也就是说,在可执行文件被启动运行之前,可执行文件对所依赖的动态库信息一无所知,只有当程序运行到需要调用动态库所提供的代码时才会启动动态链接过程。
我们在上一节中介绍了load-time,也就是程序加载时,那么程序加载完成后就开始程序执行了,那么所谓run-time(运行时)指的就是从程序开始被CPU执行到程序执行完成退出的这段时间。
所以运行时动态链接这种方式对于“动态链接”阐释的更加淋漓尽致,因为可执行文件在启动运行之前都不知道需要依赖哪些动态库,只在运行时根据代码的需要再进行动态链接。同加载时动态链接相比,运行时动态链接将链接这个过程再次推迟往后推迟,推迟到了程序运行时。
由于在编译链接生成可执行文件的过程中没有提供所依赖的动态库信息,因此这项任务就留给了程序员,在代码当中如果需要使用某个动态库所提供的函数,我们可以使用特定的API来运行时加载动态库,在Windows下通过LoadLibrary或者LoadLibraryEx,在Linux下通过使用dlopen、dlsym、dlclose这样一组函数在运行时链接动态库。当这些API被调用后,同样是首先去找这些动态库,将其从磁盘copy到内存,然后查找程序依赖的函数是否在动态库中定义。这些过程完成后动态库中的代码就可以被正常使用了。
相对于加载时动态链接,运行时动态链接更加灵活,同时将动态链接过程推迟到运行时可以加快程序的启动速度。
动态链接下可执行文件的生成
在静态链接下,链接器通过将各个目标文件的代码段和数据段合并拷贝到可执行文件,因此静态链接下可执行文件当中包含了所依赖的所有代码和数据,而与之对比的动态链接下可执行文件又是什么样的呢?
其实我们在动态库这一节中已经了解了动态链接下可执行文件的生成,即,在动态链接下,链接器并不是将动态库中的代码和数据拷贝到可执行文件中,而是将动态库的必要信息写入了可执行文件,这样当可执行文件在加载时就可以根据此信息进行动态链接了。为方便理解,我们将该信息仅仅认为是动态库都名字,真实情况当然要更复杂一点,这里我们以Linux下可执行文件即ELF文件为例
在前几节中我们将可执行文件简单的划分为了两段,数据段和代码段,在这里我们继续丰富可执行文件中的内容,如图所示,在动态链接下,可执行文件当中会新增两段,即dynamic段以及GOT(Global offset table)段,这两段内容就是是我们之前所说的必要信息。
dynamic段中保存了可执行文件依赖哪些动态库,动态链接符号表的位置以及重定位表的位置等信息
当加载可执行文件时,操作系统根据dynamic段中的信息即可找到使用的动态库,从而完成动态链接(运行时动态链接是不需要这些信息的)
动态库与静态库的对比
动态链接的优点:
动态链接下可执行文件当中仅仅保留动态库的必要信息,因此解决了静态链接下磁盘浪费问题。
静态链接在内存中就会有成百上千份同样的libc代码,这对于宝贵的内存资源同样是极大的浪费,而使用动态链接,内存中只需要有一份libc代码,所有的程序(进程)共享这一份代码,因此极大的节省了内存资源
如果我们修改了动态库的代码,我们只需要重新编译动态库就可以了而无需重新新编译我们自己的程序,因为可执行文件当中仅仅保留了动态库的必要信息,重新编译动态库后这些必要都信息是不会改变的
动态链接的缺点:
首先由于动态库是程序加载时或运行是才进行链接的,因此同静态链接相比,使用动态链接的程序在性能上要稍弱于静态链接
动态链接下的可执行文件不可以被独立运行(这里讨论的是加载时动态链接,load-time dynamic link),换句话说就是,如果没有提供所依赖的动态库或者所提供的动态库版本和可执行文件所依赖的不兼容,程序是无法启动的。动态库的依赖问题会给程序的安装部署带来麻烦,在Linux环境下尤其严重,以笔者曾参与开发维护的一个虚拟桌面系统为例,我们在开发过程中依赖的一些比较有名的第三方库默认不会随着安装包发布,这就会导致用户在较低版本Linux中安装时经常会出现程序无法启动的问题,原因就在于我们编译链接使用都动态库和用户Linux系统中的动态库不兼容。解决这个问题的方法通常有两种,一个是用户升级系统中都动态库,另一个是我们讲需要都第三方库随安装包一起发布
静态链接的优点:
静态链接都最大优点就是使用简单,编译好的可执行文件是完备的,即静态链接下的可执行文件不需要依赖任何其它的库,因为静态链接下,链接器将所有依赖的代码和数据都写入到了最终的可执行文件当中,这就消除了动态链接下的库依赖问题
静态链接的缺点:
静态链接会导致可执行文件过大,且多个程序静态链接同一个静态库的话会导致磁盘浪费的问题。
左值,纯右值和将亡值
左值
能够用&取地址的表达式是左值表达式
纯右值
1)本身就是赤裸裸的、纯粹的字面值,如3、false;
2)求值结果相当于字面值或是一个不具名的临时对象。
++i是左值,i++是右值
前者,对i加1后再赋给i,最终的返回值就是i,所以,++i的结果是具名的,名字就是i;而对于i++而言,是先对i进行一次拷贝,将得到的副本作为返回结果,然后再对i加1,由于i++的结果是对i加1前i的一份拷贝,所以它是不具名的。假设自增前i的值是6,那么,++i得到的结果是7,这个7有个名字,就是i;而i++得到的结果是6,这个6是i加1前的一个副本,它没有名字,i不是它的名字,i的值此时也是7。可见,++i和i++都达到了使i加1的目的,但两个表达式的结果不同。
将亡值
C++11中的将亡值是随着右值引用的引入而新引入的。换言之,“将亡值”概念的产生,是由右值引用的产生而引起的,将亡值与右值引用息息相关
在C++之中,使用左值去初始化对象或为对象赋值时,会调用拷贝构造函数或赋值构造函数。而使用一个右值来初始化或赋值时,会调用移动构造函数或移动赋值运算符来移动资源,从而避免拷贝,提高效率。 而将亡值可以理解为通过移动构造其他变量内存空间的方式获取到的值。在确保其他变量不再被使用、或即将被销毁时,来延长变量值的生命期。而实际上该右值会马上被销毁,所以称之为:将亡值。
1)返回右值引用的函数的调用表达式(X&& 即为右值引用。指返回值为右值引用的函数)
2)转换为右值引用的转换函数的调用表达式(如move,将一个左值转换为一个右值:test(move(test2));这里test2通过move被转换为一个右值从而避免对test调用拷贝构造)
另外:
1)字符串字面值是左值。
不是所有的字面值都是纯右值,字符串字面值是唯一例外。"abc"是左值
2) 具名的右值引用是左值,不具名的右值引用是右值
如下面,x是左值,而get_a_X()是右值(get_a_X()返回类型为右值引用)
实际上过程是get_ax_X()生成一个右值(不具名),然后调用移动构造函数构造x.x是一个左值,像X anotherX=x将调用拷贝构造函数,执行后x不发生变化,而get_a_X()生成的那个值成为将亡值
X& foo(X&& x) { //对x进行一些操作 return x; } //调用 foo(get_a_X());
左值和右值的概念:
左值:能取地址,或者具名对象,表达式结束后依然存在的持久对象;
右值:不能取地址,匿名对象,表达式结束后就不再存在的临时对象;
区别:
左值能寻址,右值不能;
左值能赋值,右值不能;
左值可变,右值不能(仅对基础类型适用,用户自定义类型右值引用可以通过成员函数改变);
默认构造函数与拷贝构造函数的调用:
以下:
A b;调用默认构造函数;
A a=b;调用拷贝构造函数
class A { public: A() { cout<<"Agouzao"<<endl; } A( A&) { cout<<"Arrrr"<<endl; } }; int main() { A b; A a=b; cout<<endl; }
以下:
只在A b;时调用默认构造函数,而对A* a=&b没有任何操作,因为只是调整指针指向而已
class A { public: A() { cout<<"Agouzao"<<endl; } A( A&) { cout<<"Arrrr"<<endl; } }; int main() { A b; A* a=&b; cout<<endl; }
以下:
A* a=new A;将调用默认构造函数(注意区分A a=b;与A* a=new A的区别)
class A { public: A() { cout<<"Agouzao"<<endl; } A( A&) { cout<<"Arrrr"<<endl; } }; int main() { A* a=new A; cout<<endl; }
以下:
首先调用A的默认构造函数,然后调用B的默认构造函数
这是因为先调用基类,后调用派生类
class A { public: A() { cout<<"Agouzao"<<endl; } A( A&) { cout<<"Arrrr"<<endl; } }; class B:public A { public: B() { cout<<"Bgouzao"<<endl; } B( B&) { cout<<"brrrr"<<endl; } }; int main() { A* a=new B; cout<<endl; }
线程同步的方式:
1.互斥量
2.读写锁
3.条件变量
4.自旋锁
单链表的排序
leetcode 148
在 O(n log n) 时间复杂度和常数级空间复杂度下,对链表进行排序。
快排:
思路:
对于单链表,使用传统的Parition方式是行不通的,因为无法向前遍历而只能向后遍历,因此要换个方式来进行Parition
对于[head,tail]上的节点,考虑head作为flag位。
用两个指针mid与pCur。mid用于分割链表,mid左侧的都小于mid,mid右侧的都大于等于mid。
pCur用于遍历两边,当pCur->val<flag时,说明这个pCur比flag小,应该放在mid左侧,因此首先将mid=mid->next,这是为了给这个pCur腾出位置,现在的mid由于进行了->next,因此它并不一定满足小于flag,无论他到底小不小于,我们进行swap(mid,pCur),注意这里的swap只是交换节点的值,交换后的mid的值一定满足小于flag了。然后将pCur=pCur->next;进行下一个点的遍历。
如果pCur大于flag,说明这个点本就应该在mid右侧,因此mid不因腾出位置来,因此将pCur=pCur->next。
这个过程实际上就是,在[head,mid]间的元素一定是小于flag的,而在(mid,pCur)间的元素一定是大于flag的,当我们的pCur遍历完,实际上就变成了[head,mid]一定小于flag,[mid+1,end)一定大于flag。即现在mid就是分割点,但此时mid的值并不是分割点flag,因此我们swap(mid,head),这样mid的值既为flag,也是分割点。
另外注意:
pCur->val<flag不能改为pCur->val<=flag
因为如果改成小于等于,则对[5,5]这种情况,将无限递归,因为QuickSort(5,5)中,Partition的结果指向第二个5,则下一个QuickSort(5,5)又将重复这个过程。
class Solution { public: ListNode* sortList(ListNode* head) { if(!head) return NULL; ListNode* tail=head; while(tail->next) tail=tail->next; QuickSort(head,tail);//4 2 5 3 7 9 0 1 return head; } void QuickSort(ListNode* head,ListNode* tail) { if(head==tail) return; auto mid=Parition(head,tail); QuickSort(head,mid); if(mid==tail) return; QuickSort(mid->next,tail); return ; } ListNode* Parition(ListNode* head,ListNode* tail) { ListNode* pCur=head->next; ListNode* mid=head; int flag=head->val; while(pCur!=tail->next) { if(pCur->val<flag) { mid=mid->next; swap(pCur,mid); } pCur=pCur->next; } swap(mid,head); return mid; } void swap(ListNode* a,ListNode* b) { int c=a->val; a->val=b->val; b->val=c; } };
归并排序:
归并排序的话相对好写,只要找到中点然后对中点左部分的链表断链,然后递归左右两边的链表,最后merge即可
问题在于中点的找寻,这里用slow-fast指针就好了
class Solution { public: ListNode* sortList(ListNode* head) { if(!head) return NULL; ListNode* pCur=head; while(pCur->next) pCur=pCur->next; return MergeListCore(head,pCur); } ListNode* MergeListCore(ListNode* head,ListNode* tail) { if(head==tail||!head->next) return head; ListNode* p1=head; ListNode* p2=p1->next->next; while(p2) { p1=p1->next; p2=p2->next; if(!p2) break; p2=p2->next; } auto tmp=p1->next; p1->next=NULL; auto l1=MergeListCore(head,p1); auto l2=MergeListCore(tmp,p2); p1=l1;p2=l2; ListNode* phead=new ListNode(0); ListNode* pCur=phead; while(p1&&p2) { if(p1->val<=p2->val) { pCur->next=p1; p1=p1->next; } else { pCur->next=p2; p2=p2->next; } pCur=pCur->next; } if(p1) pCur->next=p1; if(p2) pCur->next=p2; return phead->next; } };
链表中点的寻找是很常见的一种工具,每次自己临时写在链表里特别容易出问题,所以下面的为寻找链表中点的模板,熟悉之后果断用就行,避免因为这个地方出问题而崩盘
ListNode* FindMid(ListNode*head) { ListNode* slow=head; ListNode* fast=slow; ListNode* brk=head; while(fast&&fast->next) { fast=fast->next->next; brk=slow; slow=slow->next; } //brk->next=NULL//断链为两部分 return brk; }
网络字节流与主机字节流
字节流分为两类
---------------
buf[3] (0x78) -- 低位
buf[2] (0x56)
buf[1] (0x34)
buf[0] (0x12) -- 高位
---------------
低地址
---------------
buf[3] (0x12) -- 高位
buf[2] (0x34)
buf[1] (0x56)
buf[0] (0x78) -- 低位
--------------
静态链接库和动态链接库里的全局变量:
一个静态库中的全局变量被同一个进程的不同的dll调用时,每一个dll对这些全局变量都各自有一份独立的存储空间,即使这些dll处于同一个线程。
一个动态库中的全局变量被不同进程调用时,在没有对这些全局变量修改的情况下,它们是共享的。如果一个进程修改了被调用dll中某个全局变量的值,那么根据copy on write机制,这个进程会获得这些全局变量的一个copy,即修改的全局变量只对这个进程有效。这样使不同的进程对同一个dll中全局变量做的修改不会相互影响。
在头文件里面声明一个static变量,在两个不同的cpp里面#include这个变量有没有问题
在对一个变量声明了static之后这个变量只能在当前文件中起作用了,你在头文件里面声明了,那么第一个包含这个头文件的源文件就是这个static变量的作用域了,第二个包含头文件的源文件就是这个变量的第二个作用域,当然也就不一样了。
cosnt也是如此,const的默认属性为internal,这样只有本编译单元里可以访问,因此虽然不同文件都#include了这个头文件,但不同文件里的const或static变量实际上是彼此不同的
两个不同的进程同时用一个端口号,有没有问题
1.父进程先监听,然后fork,这样不同进程均在监听listenfd这个描述符
2.此外不同进程不能共用一个端口
64匹马问题 求最快4匹问题
new和malloc:
https://blog.csdn.net/qq_40840459/article/details/81268252
malloc 和 new的区别?
1.malloc/free是标准库函数,new/delete是C++运算符
2.malloc失败返回空,new失败抛异常
3.new/delete会调用构造、析构函数,malloc/free不会,所以他们无法满足动态对象的要求。
4.new返回有类型的指针,malloc返回无类型的指针
还有没有其他的理解?
1.分配内存的位置
malloc是从堆上动态分配内存,new是从自由存储区为对象动态分配内存。
自由存储区的位置取决于operator new的实现。自由存储区不仅可以为堆,还可以是静态存储区,这都看operator new在哪里为对象分配内存。
下面为在静态存储区分配内存
char str[1024] = { 0 };//全局静态区 void main1() { int *p1 = new int[10]{1, 2, 3, 4, 5, 6, 7, 8, 9, 0};//堆上创建 开辟的内存 int *p2 = new (str)int[10]{1, 2, 3, 4, 5, 6, 7, 8, 9, 0};//ok 在str字段处开始 分配内存 int *p3 = new (str+40)int[10]{1, 2, 3, 4, 5, 6, 7, 8, 9, 0};//ok str+40 在str字段处的后40字节数处开始分配 for (int i = 0; i < 10; i++) { cout << p1[i] << " " << p1 + i << " " << p2[i] << " " << p2 + i << endl; } cout << endl; p1 = new int[10]{1, 2, 3, 4, 5, 6, 7, 8, 9, 0};//堆上创建 开辟的内存 p2 = new (str)int[10]{1, 2, 3, 4, 5, 6, 7, 8, 9, 0};//在str字段处开始 分配内存 for (int i = 0; i < 10; i++) { cout << p1[i] << " " << p1 + i << " " << p2[i] << " " << p2 + i << endl; } cin.get();
2.返回类型安全性
malloc内存分配成功后返回void*,然后再强制类型转换为需要的类型;new操作符分配内存成功后返回与对象类型相匹配的指针类型;因此new是符合类型安全的操作符。
3.内存分配失败返回值
malloc内存分配失败后返回NULL;new分配内存失败则会抛异常(bac_alloc)。
try
{
int *a = new int();
}
catch (bad_alloc)
{
...
}
4.分配内存的大小的计算
使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算,而malloc则需要显式地指出所需内存的尺寸。
5.是否调用构造函数/析构函数
使用new操作符来分配对象内存时会经历三个步骤:
- 第一步:调用operator new 函数(对于数组是operator new[])分配一块足够大的,原始的,未命名的内存空间以便存储特定类型的对象。
- 第二步:编译器运行相应的构造函数以构造对象,并为其传入初值。
- 第三步:对象构造完成后,返回一个指向该对象的指针。
使用delete操作符来释放对象内存时会经历两个步骤:
- 第一步:调用对象的析构函数。
- 第二步:编译器调用operator delete(或operator delete[])函数释放内存空间。
总之来说,new/delete会调用对象的构造函数/析构函数以完成对象的构造/析构;而malloc则不会。
6.对数组的处理
C++提供了new []和delete []用来专门处理数组类型。它会调用构造函数初始化每一个数组元素,然后释放对象时它会为每个对象调用析构函数,但是二者一定要配套使用;至于malloc,它并不知道你要在这块空间放置数组还是其他的东西,就只给一块原始的空间,再给一个内存地址就完事,如果要动态开辟一个数组的内存,还需要我们手动自定数组的大小。
A * ptr = new A[10];//分配10个A对象
delete [] ptr;
int * ptr = (int *) malloc( sizeof(int) * 10);//分配一个10个int元素的数组
7.new与malloc是否可以相互调用
operator new /operator delete的实现可以基于malloc,而malloc的实现不可以去调用new
8.是否可以被重载
opeartor new /operator delete可以被重载。而malloc/free则不能重载。
9.分配内存时内存不足
malloc动态分配内存后,如果不够用可以使用realloc函数重新分配实现内存的扩充;而new则没有这样的操作;
海量日志数据,提取出某日访问百度次数最多的那个IP
1、IP地址最多有2^32=4G种取值情况,所以不能完全加载到内存中处理;
2、可以考虑采用分而治之的思想,按照IP地址的Hash(IP) % 1024值,把海量IP日志分别存储到1024个小文件中,这样,每个小文件最多包含4MB个IP地址;
这里解释一下为什么用Hash(IP) % 1024值,如果不用,而直接分类的话,可能会出现这样一种情况,就是有个IP在每个小文件中都存在,而且这个IP并不一定在那个小文件中是数量最多的,那么最终可能选择的结果会有问题,所以这里用了Hash(IP)%1024值,这样的话,通过计算IP的Hash值,相同IP肯定会放到一个文件中,当然了不同的IP的Hash值也可能相同,就存在一个小文件中。
3、对于每一个小文件,可以构建一个IP为key,出现的次数为value的Hash Map,同时记录当前出现次数最多的那个IP地址;
4、可以得到1024个小文件中的出现次数最多的那个IP,再依据常规的排序算法得出总体上出现次数最多的IP。
Hash(IP)%1024就只有0~1023共1024种值,根据不同的值写入不同的小文件。
搜索引擎会通过日志文件把用户每次检索使用的所有检索串都记录下来,每个查询串的长度为1-255字节。
假设目前有一千万个记录(这些查询串的重复度比较高,虽然总数是1千万,但如果除去重复后,不超过3百万个。一个查询串的重复度越高,说明查询它的用户越多,也就是越热门。),请你统计最热门的10个查询串,要求使用的内存不能超过1G。
第一步:Query统计
一千万条记录,每条记录是225Byte,很显然要占据2.55G内存
题目中说明了,虽然有一千万个Query,但是由于重复度比较高,因此事实上只有300万的Query,每个Query255Byte,因此我们可以考虑把他们都放进内存中去,而现在只是需要一个合适的数据结构,在这里,Hash Table绝对是我们优先的选择,因为Hash Table的查询速度非常的快,几乎是O(1)的时间复杂度。
维护一个Key为Query字串,Value为该Query出现次数的HashTable,每次读取一个Query,如果该字串不在Table中,那么加入该字串,并且将Value值设为1;如果该字串在Table中,那么将该字串的计数加一即可。最终我们在O(N)的时间复杂度内完成了对该海量数据的处理。
第二步:找出Top 10
第二步借用堆排序找出最热门的10个查询串:时间复杂度为N'*logK。维护一个K(该题目中是10)大小的小根堆,然后遍历3百万个Query,分别和根元素进行对比(对比value的值),找出10个value值最大的query
有一个1G大小的一个文件,里面每一行是一个词,词的大小不超过16字节,内存限制大小是1M。返回频数最高的100个词.
首先,我们看到这个题目应该做一下计算,大概的计算,因为大家都清楚的知道1G的文件不可能用1M的内存空间处理。所以我们要按照1M的上线来计算,假设每个单词都为16个字节,那么1M的内存可以处理多少个单词呢? 1M = 1024 KB = 1024 * 1024 B 。然后1M / 16B = 2^16个单词,那么1G大概有多少个单词呢? 有2^26个单词,但是实际中远远不止这些,因为我们是按照最大单词长度算的(实际上单词共16个字节,则共16*8=128位,则共有2^128种单词)。我们需要把这1G的单词分批处理,根据上面的计算,可以分成大于2^10个文件。索性就分成2000个文件吧,怎么分呢,不能随便分,不能简单的按照单词的顺序然后模2000划分(这样相同的单词,由于位置不同可能被映射到不同的文件),因为这样有可能相同的单词被划分到不同的文件中去了。这样在统计个数的时候被当成的不同的单词,因为我们没有能力把在不同文件中相同单词出现的次数跨越文件的相加,这就迫使我们要把不同序号的同一个单词划分到同一个文件中:应用hash统计吧。稍后代码会给出方法。然后呢,我们队每个文件进行分别处理。按照key-value的方法处理每个单词,最终得出每个文件中包含每个单词和单词出现的次数。然后再建立大小为100的小根堆。一次遍历文件进行处理。我没有弄1G的文件,弄1M的,简单的实现了一下,不过原理就是这样的。
这里与第一题的区别在于,这里指明了文件大小为1G,因此可以根据内存大小来决定分配多少个文件。
而第一题只说了海量数据,并不知道文件的大小,因此要考虑所有IP的情况,也就是2^32种IP地址
给定a、b两个文件,各存放50亿个url,每个url各占64字节,内存限制是4G,让你找出a、b文件共同的url?可以估计每个文件安的大小为5G×64=320G,远远大于内存限制的4G。所以不可能将其完全加载到内存中处理。考虑采取分而治之的方法。
遍历文件a,对每个url求取hash(url)%1000,然后根据所取得的值将url分别存储到1000个小文件(记为a0,a1,...,a999)中。这样每个小文件的大约为300M。
遍历文件b,采取和a相同的方式将url分别存储到1000小文件(记为b0,b1,...,b999)。这样处理后,所有可能相同的url都在对应的小文件(a0vsb0,a1vsb1,...,a999vsb999)中,不对应的小文件不可能有相同的url。
然后我们只要求出1000对小文件中相同的url即可。求每对小文件中相同的url时,可以把其中一个小文件的url存储到hash_set中。然后遍历另一个小文件的每个url,看其是否在刚才构建的hash_set中,如果是,那么就是共同的url,存到文件里面就可以了。
已知某个文件内包含一些电话号码,每个号码为8位数字,统计不同号码的个数(共有都少个不同的号码)
8位最多99 999 999(0-99 999 999共1亿个数),每个数字对应一个Bit位,所以只需要99MBit==1.2MBytes,这样,就用了小小的1.2M左右的内存表示了所有的8位数的电话)
2.5亿个整数(int)中找出不重复的整数的个数,内存足够大。
int共32位,一共有2^32个数,位图的大小有2^32bit=2^32/8Byte=2^29Byte=2^10*2^10*2^10^2^-1=0.5*1024*1024*1024Byte=0.5*1G=0.5G.
将bit-map扩展一下,用2bit表示一个数即可,0表示未出现,1表示出现一次,2表示出现2次及以上。或者我们不用2bit来进行表示,我们用两个bit-map即可模拟实现这个2bit-map。 (每个整数用两位,存储所有的整数需要2^32*2=1GB的内存)
2.5亿个整数中找出不重复的整数的个数,内存空间不足以容纳这2.5亿个整数。
2^32个数要0.5个G,也就是512MB,如果划分为2^8个区域(比如用单个文件代表一个区域),然后将数据分离到不同的区域,然后不同的区域在利用bitmap(占用4MB,内存可以存下)就可以直接解决了。也就是说只要有足够的磁盘空间,就可以很方便的解决。
5亿个int找它们的中位数 (指将统计总体当中的各个变量值按大小顺序排列起来,形成一个数列,处于变量数列中间位置的变量值就称为中位数)
首先我们将int划分为2^16个区域(肯定是按大小的),然后读取数据统计落到各个区域里的数的个数,之后我们根据统计结果就可以判断中位数落到那个区域,同时知道这个区域中的第几大数刚好是中位数。 然后第二次扫描我们只统计落在这个区域中的那些数就可以了。
给40亿个不重复的unsigned int的整数,没排过序的,然后再给一个数,如何快速判断这个数是否在那40亿个数当中?
方案1:申请512M的内存(2^32/8=512MB),一个bit位代表一个unsigned int值。读入40亿个数,设置相应的bit位,读入要查询的数,查看相应bit位是否为1,为1表示存在,为0表示不存在。
方案2:因为2^32为40亿多,所以给定一个数可能在,也可能不在其中;这里我们把40亿个数中的每一个用32位的二进制来表示假设这40亿个数开始放在一个文件中。
然后将这40亿个数分成两类: 1. 最高位为0 2. 最高位为1
并将这两类分别写入到两个文件中,其中一个文件中数的个数<=20亿,而另一个>=20亿(这相当于折半了);与要查找的数的最高位比较并接着进入相应的文件再查找
再然后把这个文件为又分成两类: 1.次最高位为0 2.次最高位为1
并将这两类分别写入到两个文件中,其中一个文件中数的个数<=10亿,而另一个>=10亿(这相当于折半了); 与要查找的数的次最高位比较并接着进入相应的文件再查找。 ....... 以此类推,就可以找到了,而且时间复杂度为O(logn)
TCP与UDP
TCP和UDP
两者都是通信协议,TCP和UDP都是传输层协议,但是他们的通信机制和应用场景不同。
TCP
TCP(Transmission Control Protocol)又叫传输控制协议,TCP是面向连接的,并且是一种可靠的协议,在基于TCP进行通信时,通信双方需要建立TCP连接,建立连接需要经过三次握手,握手成功才可以通信。
UDP
UDP是一种面向无连接,切不可靠的协议,在通信过程中,它并不像TCP那样需要先建立一个连接,只要目的地址,端口号,源地址,端口号确定了,就可以直接发送信息报文,并且不需要一定能收到或者完整的数据。它仅仅提供了校验和机制来保障报文是否完整,若校验失败,则直接将报文丢弃,不做任何处理。
TCP,UDP的优缺点
TCP优点
可靠,稳定
TCP的可靠性体现在传输数据之前,三次握手建立连接(四次挥手断开连接),并且在数据传递时,有确认,窗口,重传,拥塞控制机制,数据传完之后断开连接来节省系统资源。
TCP缺点
慢,效率比较低,占用系统资源,容易被攻击
传输数据之前建立连接,这样会消耗时间,而且在消息传递时,确认机制,重传机制和拥塞机制都会消耗大量的时间,而且要在每台设备上维护所有的传输连接。而且每一个连接都会占用系统的CPU,内存等硬件软件资源。并且TCP的取而机制,三次握手机制导致TCP容易被人利用,实现DOS,DDOS攻击。
UDP优点
快,比TCP安全
UDP没有TCP的握手,确认窗口,重传,拥塞机制。UDP是一个无状态的传输机制,所以在传输数据时非常快。UDP没有TCP这些机制,相应被利用的漏洞就少一点。但是UDP的攻击也是存在的,比如:UDP 的flood攻击。
UDP缺点
不可靠,不稳定
因为UDP没有TCP的那些可靠机制,在网络质量不好的时候容易发生丢包。
应用场景
#TCP应用场景
当对网络通信质量有要求时,比如:整个数据要准确无误的传递给对方,这往往对于一些要求可靠的应用,比如HTTP,HTTPS,FTP等传输文件的协议,POP,SMTP等邮件的传输协议。常见使用TCP协议的应用:
1.浏览器使用的:HHTP
2.FlashFXP:FTP
3.Outlook:POP,SMTP
4.QQ文件传输
UDP 文件传输协议
对当前网络通讯质量要求不高的时候,要求网络通讯速度尽量的快,这时就使用UDP
日常生活中常见使用UDP协议:
1.QQ语音
2.QQ视频
3.TFTP
inline函数的总结
引入inline关键字的原因
在c/c++中,为了解决一些频繁调用的小函数大量消耗栈空间(栈内存)的问题,特别的引入了inline修饰符,表示为内联函数。
在内部的工作就是在每个for循环的内部任何调用dbtest(i)的地方都换成了(i%2>0)?”奇”:”偶”,这样就避免了频繁调用函数对栈内存重复开辟所带来的消耗。
inline使用限制
inline的使用时有所限制的,inline只适合函数体内部代码简单的函数使用,不能包含复杂的结构控制语句例如while、switch,并且不能内联函数本身不能是直接递归函数(即,自己内部还调用自己的函数)。
这是因为内联函数在程序编译期间就已经展开了,而递归的次数是无法在编译时期判断出来的,只有执行时才知道什么时候停止。
inline不同于define的在于他是在编译时期展开的,而define是在预处理阶段处理的。
inline仅是一个对编译器的建议
inline函数仅仅是一个对编译器的建议,所以最后能否真正内联,看编译器的意思:
类中的成员函数与inline
定义在类中的成员函数缺省都是内联的
- 如果在类定义时就在类内给出函数定义,那当然最好。
- 如果在类中未给出成员函数定义,而又想内联该函数的话,那在类外要加上inline,否则就认为不是内联的。
inline 是一种“用于实现的关键字”
关键字inline 必须与函数定义体放在一起才能使函数成为内联,仅将inline 放在函数声明前面不起任何作用
因此,inline 是一种“用于实现的关键字”,而不是一种“用于声明的关键字”
慎用inline
虽然说内联函数可以提高执行效率,但是不可以将所有的函数都定义为内联函数。
内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。
如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。
volatile
https://www.cnblogs.com/yc_sunniwell/archive/2010/07/14/1777432.html
volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。
当要求使用 volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。而且读取的数据立刻被保存。
volatile 指出 i 是随时可能发生变化的,每次使用它的时候必须从 i的地址中读取,因而编译器生成的汇编代码会重新从i的地址读取数据放在 b 中。而优化做法是,由于编译器发现两次从 i读数据的代码之间的代码没有对 i 进行过操作,它会自动把上次读的数据放在 b 中。而不是重新从 i 里面读。这样以来,如果 i是一个寄存器变量或者表示一个端口数据就容易出错,所以说 volatile 可以保证对特殊地址的稳定访问
更多的可能是多线程并发访问共享变量时,一个线程改变了变量的值,怎样让改变后的值对其它线程 visible。
中断
发成缺页中断后,执行了那些操作?
当一个进程发生缺页中断的时候,进程会陷入内核态,执行以下操作:
1、检查要访问的虚拟地址是否合法
2、查找/分配一个物理页
3、填充物理页内容(读取磁盘,或者直接置0,或者啥也不干)
4、建立映射关系(虚拟地址到物理地址)
重新执行发生缺页中断的那条指令
define与const的区别:
(1) 编译器处理方式不同
#define 宏是在预处理阶段展开。
const 常量是编译运行阶段使用。
(2) 类型和安全检查不同
#define 宏没有类型,不做任何类型检查,仅仅是展开。
const 常量有具体的类型,在编译阶段会执行类型检查。
(3) 存储方式不同
#define宏仅仅是展开,有多少地方使用,就展开多少次,不会分配内存。(宏定义不分配内存,变量定义分配内存。)
const常量会在内存中分配(可以是堆中也可以是栈中)。
malloc与brk调用(C++内存分配的原理)
https://www.cnblogs.com/vinozly/p/5489138.html
http://blog.sina.com.cn/s/blog_b4ef897e0102vg0o.html
从操作系统角度来看,进程分配内存有两种方式,分别由两个系统调用完成:brk和mmap(不考虑共享内存)。
1、brk是将数据段(.data)的最高地址指针_edata往高地址推;
2、mmap是在进程的虚拟地址空间中(堆和栈中间,称为文件映射区域的地方)找一块空闲的虚拟内存。
这两种方式分配的都是虚拟内存,没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系。
情况一、malloc小于128k的内存,使用brk分配内存,将_edata往高地址推(只分配虚拟空间,不对应物理内存(因此没有初始化),第一次读/写数据时,引起内核缺页中断,内核才分配对应的物理内存,然后虚拟地址空间建立映射关系),如下图:
情况二、malloc大于128k的内存,使用mmap分配内存,在堆和栈之间找一块空闲内存分配(对应独立内存,而且初始化为0),如下图:
进程调用C=malloc(200K)以后,内存空间如图4:
进程调用free(D)以后,如图8所示:
总结一下就是:
1、内核和用户进程分配内存的特点和原因
答:1)内核中只要请求内存得以满足,都会返回页描述符的地址或线性地址
2)当用户态进程申请动态内存时,并没有获取请求的页框,而是获得一个线性区,当第一次访问分配的线性区的时候,发生缺页中断,检查线性区是否是合法的,如果是合法的,??(谁给)查找或分配物理内存,并且填充该物理内存,最后建立虚拟地址和物理地址的映射
原因:进程对动态内存的请求认为是不紧迫的,内核总是推迟给用户态进程分配动态内存???
由于用户态进程是不可信任的,内核必须随时准能被捕获用户态进程引起的寻址错误
2.malloc的工作原理
http://blog.sina.com.cn/s/blog_b4ef897e0102vg0i.html
答:malloc函数分配内存主要是使用brk和mmap系统调用
brk是_edata指针堆中的地址往高地址推,mmap是在堆和栈之间找分配一块空闲的虚拟内存
当使用malloc分配的字节数小于128k的时候,调用brk分配虚拟内存
当malloc分配的字节数大于128的时候,使用过mmap分配虚拟内存
brk和mmap分配的都是虚拟内存,并没有分配物理内存,当第一次访问已经分配的虚拟内存的时候,发生缺页中断,操作系统负责分配物理内存,并且建立虚拟内存和物理内存之间的映射关系
3.free的工作原理
答:举例,假射系统调用brk先分配了内存A,然后在分配内存B,系统调用mmap分配了内存C,
如果free由mmap分配的内存C,直接将C中的虚拟内存和物理内存释放回给操作系统
如果free有brk分配的A,A的虚拟内存和物理内存都没有释放,但是A的内存可以重用(因为B没有释放)
如果再释放brk分配的B,如果A和B的内存数目大于128k则将A和B的虚拟内存和物理内存都释放回给操作系统,否则也没有释放
4.为什么malloc在小于128k时调用brk,而大于128k时调用mmap呢,为什么不全用brk或全用mmap呢
答:主要是为了较少内存碎片。
brk分配的内存只有等高地址的内存释放了低地址的内存才会释放,如果全是用brk,那么会因为内存的释放而导致产生很多内存碎片
mmap分配的内存是随机分配的,可能不连续,如果全用mmap的分配内存的话,会因为内存的不连续而产生很多内存碎片
所以使用一个折中的方法,小于128k使用brk,大于128k使用mmap
5.为什么等到第一次访问虚拟内存的时候才分配物理内存呢
答:因为申请的内存不一定马上使用,推迟分配可以系统拥有更多的空闲物理内存去出来其他事,从而提高系统的吞吐量
6.为什么A不能释放
答:因为只有一个_edata指针,如果A释放了,_edata指针要向后退,那B内存怎么办(除非紧缩,否则_edata没指向栈顶而指向了B内)
7.为什么达到128k才释放呢
答:因为小于128k的内存并不够mmap分配,不释放(即使释放了也不会由mmap分配,而是由brk分配,这样还是相当于复用),下次申请小内存时,直接重用该内存即可。
大于128k,可以释放该内存,以便mmap函数可以分配该内存
公有继承、保护继承和私有继承
父类的private对派生类是不可见的,只可以通过父类的方法来访问
这与private继承时,public和protected成员变为private成员是不一样的
另外,对于protected:
1. 基类的保护成员对于派生类的成员是可访问的。
2. 派生类的成员只能通过派生类对象访问基类的保护成员,派生类对一个基类对象中的受保护成员没有访问权限。
这两句话看的太头晕了,其实作者应该是想表达:只有在派生类中才可以通过派生类对象访问基类的protected成员。看这样的代码:
class Derive : public Base { public: Derive(); }; Derive::Derive() { this->m = 1; // 这里访问m没有问题 }
Derive *d = new Derive(); std::cout<< d->m <<endl; // 这里就报错了