C++学习日记
1.has-a 可以通过包含和私有继承,不获得接口,但可以获得实现
2.包含与私有继承区别:私有继承使用类名而不是使用成员名来构造函数,包含使用对象名来调用方法,而私有继承使用类名和域解析符来调用方法;访问基类对象采用强制转换为基类对象的引用
3.对于继承虚基类,需将虚基类单独放入构造函数,多重继承也将基类作为构造函数参数,虚基类不允许自动传递基类参数,需单独添加一个,必须显示调用该虚基类的某个构造函数
注意当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针。
char a[] = "hello world";
char *p = a;
cout<< sizeof(a) << endl; // 12 字节
cout<< sizeof(p) << endl; // 4 字节
4.类成员函数的重载、覆盖和隐藏区别?
答案:
a.成员函数被重载的特征:
(1)相同的范围(在同一个类中);
(2)函数名字相同;
(3)参数不同;
(4)virtual 关键字可有可无。
b.覆盖是指派生类函数覆盖基类函数,特征是:
(1)不同的范围(分别位于派生类与基类);
(2)函数名字相同;
(3)参数相同;
(4)基类函数必须有virtual 关键字。
c.“隐藏”是指派生类的函数屏蔽了与其同名的基类函数,规则如下:
(1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。
(2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual 关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)
5 C++代码重用
包含,组合,层次化,包含其他类;私有或保护继承,私有代表只继承了类的实现,没有继承类的接口,只能通过包含类的公有方法访问,类外无法访问;
is-a 是获得接口和实现;
has-a 不继承接口,获得实现;
6 valarray使用:
valarray
valarray
valarray
方法:
operator[]();
size();
max();
min();
sum();
7 explicit Student(const std::string &s):name(s),socres(){}
explicit Student(int n):num(n),name("Nully"){}
使用explicit 将防止单参数构造函数的隐式转换,将认为doh =5 是错误的初始化
比如Student doh("homer",10); //store "Homer" ,create array of 10 element;
doh = 5; //reset name to "Nully",reset to empty array of 5 element;
8 对于继承的对象,构造函数在成员初始化列表中使用类名来调用特定的基类构造函数;对于成员对象,构造函数使用成员名;被包含的对象接口不是公有的,但可以在类方法中使用它。
9.对于使用私有继承来实现has-a方法的,不像包含可以使用对象名来调用成员类方法,使用私有继承是将使用类名和作用域解析运算符来调用方法;
类定义以前
class Student
{ ...
std::string name;
std::valarray
}
替换为
class Student:private std::string, private std::valarray
{ ... }
在类方法使用中要域运算符来调用方法,有些还要作隐式向上转换;
初始化调用域运算符:Student(const std::string & s,const ArrayDb & a)
:std::string(s),ArrayDb(a){}
强制向上转换:cout << name; ->(std::string &)stu;
os <
10. 捕获异常,
try{代码块}
catch{代码块} 可接多个catch,若是派生类,则顺序需要协定
throw(...)引发异常:异常可根据异常类型而定,可以类,字符串,基类,派生类;
若catch 的是基类,throw派生类也可以捕捉,反之不行
11.嵌套类,在类声明中声明新类嵌套,作用域和访问控制遵从常规类继承特性;
12.友元:类可以将其他函数、其他类和其他类的成员函数作为友元
友元类:将类作为另一个类的友元,也可以指定某个类的某个函数为其友元,但需要前向声明;
可互为友元,需前向声明,且定义为两类之后;需要特别注意类和方法声明的顺序,以正确地组合友元;
13 inline 内联属性,一般在头文件内,内联具有局域性;如果在定义处则不要使用内联,直接定义;
14 RTTI是运行阶段类型识别,RTTI只适用于包含虚函数的类;
dynamic_cast :dynamic_cast
Super * ps;
ps = dynamic_cast
Typeid 返回一个type_info对象,type_info各厂不相同,具有name()方法,可以用来对两个Type_id的返回值进行比较,以确定对象是否是为特定的类型;
type_info对象用于获取对象的信息
ex:typeid(Magnificant) = typeid(*pg); //result true or false;
const_cast:改变值为const或volatile, const_cast
ex: High bar; const High *pbar = &bar; ...;High *pb = const_cast
14 string类
构造函数:
1)string(const char *s)
2)string(size_type n,char c)
3)string(const string &str)
4)string()
5)string(cosnt char *s,size_type n)
6)template
string(Iter begin,Iter end)
ex:char allis[]="allis well but ends well";
string six(allis+6,allis+10);
string seven(&allis[5],&allis[9]);
7)string(cosnt string &str,string size_type pos=0,size_type n = npos)
8)string(string && str)noexcept
9)string(initializer_list
string方法:
string.size();string.length();
查找:string.find();rfind();find_last_of();find_first_not_of();find_first_of();
输入: getline(cin,string)或者cin>>string 而C风格字符串是cin.getline(info,100)
转换为C-风格字符串:string.cstr();
15 智能指针模板类:行为类似于指针的类对象
auto_ptr c++98
unique_ptr C++11
赋值操作转让所有权,不会在运行时报错,有错误会在编译器代码行报错;试图将一个unique_ptr赋值给另一个时,如果源unique_ptr是个临时右值,编译器允许这样做;如果源unique_ptr存在一段时间,编译器将禁止这样做;
ex:unique_ptr
share_ptr C++11 存在一个计数器,计数减到一才删除内存;可将unique_ptr作为右值赋给share_ptr,左值则不行;
auto_ptr和 unique_ptr在使用new和delete时使用,在new[]和delete[]不可使用,要用share_ptr,反之亦然;
所有智能指针都一个explicit显式构造函数:
double *p_reg = new double;
share_ptr
pd =share_ptr
share_ptr
share_ptr
16 C++ 11新加特性
initializer_list;初始化列表
17 Vetor和Valarray array的区别
Vetor 模板类是容器了算法支持面向容器的操作,如排序,插入,重新排列,搜索,将数据转移到其他容器中;valarray类模板是面向数值计算的,不是STL的一部分,没有push_back(),insert()方法,但为数字运算提供了简单直观的接口,;array是代替内置数组而设计的,效率更高,更紧凑,表示固定长度的数组,不支持push_back(),insert(),但提供了STL方法,包括begin(),end(),rbegin(),rend(),size()使得STL算法用于array对象;
Vetor 序列容器,模板使用动态内存分配,ex:Vetor
提供随机访问,动态改变对象的长度,在尾部添加和删除元素的时间是固定的,但在头部或中间插入和删除元素的复杂度为线性时间,所以不提供push_front();
deque 双端队列,序列容器,支持首尾固定时间插入,随机访问,多数操作插入删除在首尾考虑使用
list 双向链表,list和Vetor之间关键的区别在于,list在链表中任一位置进行插入和删除的时间都是固定的(Vetor模板除结尾以外的线性时间的插入和删除,在结尾处,它提供了固定时间的插入和删除)。因此,vetor强调的是随机访问进行快速访问,而list强调的是快速插入和删除;list不支持数组表示法和随机访问;list还包含的成员函数:merge,remove,sort,splice,unique;
queue限制比deque多,不允许随机访问元素,不允许遍历队列,它把使用限制在定义队列的基本操作上,可以将元素添加到队尾,从队首删除元素,查看队首和队尾的值,检查元素数目和测试队列是否为空;empty,size,front(),back(),push(),back()
array 不是STL容器,长度固定,没有push_back和insert,有成员函数 operator[](),at(),可将很多标准STL算法用于array对象,如copy()和for_each();
18 STL是一种泛型编程,面向对象编程关注的是数据方面,而泛型编程关注的是算法。它们之间的共同点是抽象和创建可重用代码,但理念不同
19 静态数据成员
静态数所成员是同一个类中所有对象共享的成,而不是某一对象的成员。
静态数据成员始化须在类的外部进行,与一般数据成员初始化不同,它的格式如下:
<数据类型><类名>::<静态数据成员名>=<值>
静态成员函数和静态数据成员一样,它们都属于类的静态成员,但它们都不是对象的成员。
20 static_cast是一个c++运算符,功能是把一个表达式转换为某种类型,但没有运行时类型检查来保证转换的安全性。该运算符把expression转换为type-id类型,但没有运行时类型检查来保证转换的安全性
static_cast
①用于类层次结构中基类(父类)和派生类(子类)之间指针或引用的转换。
进行上行转换(把派生类的指针或引用转换成基类表示)是安全的;
进行下行转换(把基类指针或引用转换成派生类表示)时,由于没有动态类型检查,所以是不安全的。
②用于基本数据类型之间的转换,如把int转换成char,把int转换成enum。这种转换的安全性也要开发人员来保证。
③把空指针转换成目标类型的空指针。
④把任何类型的表达式转换成void类型。
注意:static_cast不能转换掉expression的const、volatile、或者__unaligned属性。
基本类型数据转换举例如下:
char a = 'a';
int b = static_cast
double *c = new double;
void *d = static_cast
int e = 10;
const int f = static_cast
const int g = 20;
int *h = static_cast
类上行和下行转换:
class Base
{};
class Derived : public Base
{}
Base* pB = new Base();
if(Derived* pD = static_cast
{}//下行转换是不安全的(坚决抵制这种方法)
Derived* pD = new Derived();
if(Base* pB = static_cast
{}//上行转换是安全的
dynamic_cast详解:
转换方式:
dynamic_cast< type* >(e)
type必须是一个类类型且必须是一个有效的指针
dynamic_cast< type& >(e)
type必须是一个类类型且必须是一个左值
dynamic_cast< type&& >(e)
type必须是一个类类型且必须是一个右值
e的类型必须符合以下三个条件中的任何一个:
1、e的类型是目标类型type的公有派生类
2、e的类型是目标type的共有基类
3、e的类型就是目标type的类型。
如果一条dynamic_cast语句的转换目标是指针类型并且失败了,则结果为0。如果转换目标是引用类型并且失败了,则dynamic_cast运算符将抛出一个std::bad_cast异常(该异常定义在typeinfo标准库头文件中)。e也可以是一个空指针,结果是所需类型的空指针。
dynamic_cast主要用于类层次间的上行转换和下行转换,还可以用于类之间的交叉转换(cross cast)。
在类层次间进行上行转换时,dynamic_cast和static_cast的效果是一样的;在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全。dynamic_cast是唯一无法由旧式语法执行的动作,也是唯一可能耗费重大运行成本的转型动作。
(1)指针类型
举例,Base为包含至少一个虚函数的基类,Derived是Base的共有派生类,如果有一个指向Base的指针bp,我们可以在运行时将它转换成指向Derived的指针,代码如下:
if(Derived *dp = dynamic_cast
//使用dp指向的Derived对象
}
else{
//使用bp指向的Base对象
值得注意的是,在上述代码中,if语句中定义了dp,这样做的好处是可以在一个操作中同时完成类型转换和条件检查两项任务。
(2)引用类型
因为不存在所谓空引用,所以引用类型的dynamic_cast转换与指针类型不同,在引用转换失败时,会抛出std::bad_cast异常,该异常定义在头文件typeinfo中。
void f(const Base &b){
try{
const Derived &d = dynamic_cast
//使用b引用的Derived对象
}
catch(std::bad_cast){
//处理类型转换失败的情况
}
转换注意事项:
尽量少使用转型操作,尤其是dynamic_cast,耗时较高,会导致性能的下降,尽量使用其他方法替代。
21 比较大小
inline int max(int a, int b) { return a > b ? a : b; }
不过这和上面的宏不大一样,因为这个版本的max只能处理int类型。但模板可以很轻巧地解决这个问题:
template
inline const T& max(const T& a, const T& b)
{ return a > b ? a : b; }
22. string c = a; 拷贝构造;
string s="hello";
void func (string c);func(s);也是拷贝构造;
解决缺乏赋值跟赋值构造函数造成的指针浅拷贝内存泄漏问题,解决这类指针混乱问题的方案在于,只要类里有指针时,就要写自己版本的拷贝构造函数和赋值操作符函数。在这些函数里,你可以拷贝那些被指向的数据结构,从而使每个对象都有自己的拷贝;或者你可以采用某种引用计数机制(见条款 m29)去跟踪当前有多少个对象指向某个数据结构。引用计数的方法更复杂,而且它要求构造函数和析构函数内部做更多的工作,但在某些(虽然不是所有)程序里,它会大量节省内存并切实提高速度。
23
1. 同步,就是我调用一个功能,该功能没有结束前,我死等结果。
2. 异步,就是我调用一个功能,不需要知道该功能结果,该功能有结果后通知我(回调通知)。
3.阻塞,就是调用我(函数),我(函数)没有接收完数据或者没有得到结果之前,我不会返回。
4. 非阻塞,就是调用我(函数),我(函数)立即返回,通过select通知调用者
同步IO和异步IO的区别就在于:数据拷贝的时候进程是否阻塞
阻塞IO和非阻塞IO的区别就在于:应用程序的调用是否立即返回
24
在vc中怎样解决内存泄漏的问题(release版本)
(1)放置宏assert( )。assert是宏,而不是函数。在C的assert.h头文件中。assert宏的原型定义在
(2)CRT调试堆函数
如何调试C、C++ 内存泄露的。
答核心为使用valgrind工具。 跑一段程序,发生内存泄露,valgrind 会报出来。
25.将程序移植到不同的32位cpu中,经常出现结构字节对齐和大小端的问题,有哪些方法避免?
结构体字节对齐:
许多实际的计算机系统对基本类型数据在内存中存放的位置有限制,它们会要求这些数据的首地址的值是某个数k(通常它为4或8)的倍数,这就是所谓的内存对齐。
每个特定平台上的编译器都有自己的默认“对齐系数”(32位机一般为4,64位机一般为8)。我们可以通过预编译命令#pragma pack(k),k=1,2,4,8,16来改变这个系数,其中k就是需要指定的“对齐系数”;也可以使用#pragma pack()取消自定义字节对齐方式。
规则:
1、数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的对齐按照#pragma pack指定的数值和这个数据成员自身长度中,比较小的那个进行。
2、结构(或联合)的整体对齐规则:在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐,对齐将按照#pragma pack指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行。
26.全局对象的构造函数在main函数之前调用,析构函数在main函数之后调用。
27.epoll是一种I/O事件通知机制
I/O事件
基于file descriptor,支持file, socket, pipe等各种I/O方式
当文件描述符关联的内核读缓冲区可读,则触发可读事件,什么是可读呢?就是内核缓冲区非空,有数据可以读取
当文件描述符关联的内核写缓冲区可写,则触发可写事件,什么是可写呢?就是内核缓冲区不满,有空闲空间可以写入
通知机制
通知机制,就是当事件发生的时候,去通知他
通知机制的反面,就是轮询机制
水平触发与边缘触发
水平触发(level-trggered)
只要文件描述符关联的读内核缓冲区非空,有数据可以读取,就一直发出可读信号进行通知,
当文件描述符关联的内核写缓冲区不满,有空间可以写入,就一直发出可写信号进行通知
边缘触发(edge-triggered)
当文件描述符关联的读内核缓冲区由空转化为非空的时候,则发出可读信号进行通知,
当文件描述符关联的内核写缓冲区由满转化为不满的时候,则发出可写信号进行通知
两者的区别在哪里呢?水平触发是只要读缓冲区有数据,就会一直触发可读信号,而边缘触发仅仅在空变为非空的时候通知一次,举个例子:
读缓冲区刚开始是空的
读缓冲区写入2KB数据
水平触发和边缘触发模式此时都会发出可读信号
收到信号通知后,读取了1kb的数据,读缓冲区还剩余1KB数据
水平触发会再次进行通知,而边缘触发不会再进行通知
所以边缘触发需要一次性的把缓冲区的数据读完为止,也就是一直读,直到读到EGAIN为止,EGAIN说明缓冲区已经空了,因为这一点,边缘触发需要设置文件句柄为非阻塞
28. TCP三次握手
第一次握手:建立连接时,客户端发送syn包(syn=x)到服务器,并进入SYN_SENT状态,等待服务器确认;SYN:同步序列编号(Synchronize Sequence Numbers)。
第二次握手:服务器收到syn包,必须确认客户的SYN(ack=x+1),同时自己也发送一个SYN包(syn=y),即SYN+ACK包,此时服务器进入SYN_RECV状态;
第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=y+1),此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握
四次挥手过程理解
1)客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1(终止等待1)状态。 TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。
2)服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。
3)客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。
4)服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。
5)客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2∗∗MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。
6)服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些。
常见面试题
【问题1】为什么连接的时候是三次握手,关闭的时候却是四次握手?
答:因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,"你发的FIN报文我收到了"。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。
【问题2】为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态?
答:虽然按道理,四个报文都发送完毕,我们可以直接进入CLOSE状态了,但是我们必须假象网络是不可靠的,有可以最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文。在Client发送出最后的ACK回复,但该ACK可能丢失。Server如果没有收到ACK,将不断重复发送FIN片段。所以Client不能立即关闭,它必须确认Server接收到了该ACK。Client会在发送出ACK之后进入到TIME_WAIT状态。Client会设置一个计时器,等待2MSL的时间。如果在该时间内再次收到FIN,那么Client会重发ACK并再次等待2MSL。所谓的2MSL是两倍的MSL(Maximum Segment Lifetime)。MSL指一个片段在网络中最大的存活时间,2MSL就是一个发送和一个回复所需的最大时间。如果直到2MSL,Client都没有再次收到FIN,那么Client推断ACK已经被成功接收,则结束TCP连接。
【问题3】为什么不能用两次握手进行连接?
答:3次握手完成两个重要的功能,既要双方做好发送数据的准备工作(双方都知道彼此已准备好),也要允许双方就初始序列号进行协商,这个序列号在握手过程中被发送和确认。
现在把三次握手改成仅需要两次握手,死锁是可能发生的。作为例子,考虑计算机S和C之间的通信,假定C给S发送一个连接请求分组,S收到了这个分组,并发 送了确认应答分组。按照两次握手的协定,S认为连接已经成功地建立了,可以开始发送数据分组。可是,C在S的应答分组在传输中被丢失的情况下,将不知道S 是否已准备好,不知道S建立什么样的序列号,C甚至怀疑S是否收到自己的连接请求分组。在这种情况下,C认为连接还未建立成功,将忽略S发来的任何数据分 组,只等待连接确认应答分组。而S在发出的分组超时后,重复发送同样的分组。这样就形成了死锁。
【问题4】如果已经建立了连接,但是客户端突然出现故障了怎么办?
TCP还设有一个保活计时器,显然,客户端如果出现故障,服务器不能一直等下去,白白浪费资源。服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为2小时,若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔75秒钟发送一次。若一连发送10个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接。
29. C++中为什么构造函数不能定义为虚函数
简单讲就是没有意义。虚函数的作用在于通过父类的指针或引用来调用子类的那个成员函数。而构造函数是在创建对象时自己主动调用的,不可能通过子类的指针或者引用去调用。
网络上还有一个很普遍的解释是这样的:虚函数相应一个指向vtable虚函数表的指针,但是这个指向vtable的指针事实上是存储在对象的内存空间的。假设构造函数是虚的,就须要通过 vtable来调用,但是对象还没有实例化,也就是内存空间还没有,怎么找vtable呢?所以构造函数不能是虚函数。
不是没有意义,而是不能。 原因是:构造函数必须清晰的构造类型。
30. 为什么复制构造函数的参数必须是引用类型?
看第四个输出: copy constructor // CExample ccc = aaa;
构造ccc,实质上是ccc.CExample(aaa); 我们假如拷贝构造函数参数不是引用类型的话, 那么将使得 ccc.CExample(aaa)变成aaa传值给ccc.CExample(CExample ex),即CExample ex = aaa,因为 ex 没有被初始化, 所以 CExample ex = aaa 继续调用拷贝构造函数,接下来的是构造ex,也就是 ex.CExample(aaa),必然又会有aaa传给CExample(CExample ex), 即 CExample ex = aaa;那么又会触发拷贝构造函数,就这下永远的递归下去。
如果一个函数是通过值传递(pass by value)的话,那么它真正传递的其实是实参的副本,该副本产生必定会调用复制构造函数。那么,试想一下,如果我们的复制构造函数是通过值传递的话,它就会调用它本身来产生一个副本,就这样会无限递归下去,而如果传递引用,就不会产生副本,也不会调用复制构造函数,问题得以解决。
说明拷贝构造函数的参数使用引用类型不是为了减少一次内存拷贝,而是避免拷贝构造函数无限制的递归下去。所以, 拷贝构造函数是必须要带引用类型的参数的, 而且这也是编译器强制性要求的
31. select、poll、epoll之间的区别(搜狗面试)
(1)select==>时间复杂度O(n)
它仅仅知道了,有I/O事件发生了,却并不知道是哪那几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以select具有O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。
(2)poll==>时间复杂度O(n)
poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。 但是它没有最大连接数的限制,原因是它是基于链表来存储的.
(3)epoll==>时间复杂度O(1)
epoll是在2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。
epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。所以我们说epoll实际上是事件驱动(每个事件关联上fd)的,此时我们对这些流的操作都是有意义的。(复杂度降低到了O(1))
epoll的优点:
1、没有最大并发连接的限制,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口)。
2、效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。
只有活跃可用的FD才会调用callback函数;即Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll。
3、内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。
select,poll,epoll都是IO多路复用的机制。I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。
epoll跟select都能提供多路I/O复用的解决方案。在现在的Linux内核里有都能够支持,其中epoll是Linux所特有,而select则应该是POSIX所规定,一般操作系统均有实现
select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点。
1、select最大的缺陷就是单个进程所打开的FD是有一定限制的,它由FD_SETSIZE设置,默认值是1024。
2、对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低。
3、需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大
poll
注意:如果没有大量的idle-connection或者dead-connection,epoll的效率并不会比select/poll高很多,但是当遇到大量的idle-connection,就会发现epoll的效率大大高于select/poll。因为epoll内核中实现是根据每个fd上的callback函数来实现的,只有活跃的socket才会主动调用callback,所以在活跃socket较少的情况下,使用epoll没有前面两者的线性下降的性能问题,但是所有socket都很活跃的情况下,可能会有性能呢个问题;
总之区别:
1.句柄;
2.FD剧增后带来的IO效率问题;
3.消息传递方式;select和poll都是内核需要将消息传递到用户空间,都需要内核拷贝动作;epoll通过内核和用户空间共享一块内存来实现的;
综上,在选择select,poll,epoll时要根据具体的使用场合以及这三种方式的自身特点:
1、表面上看epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调。
2、select低效是因为每次它都需要轮询。但低效也是相对的,视情况而定,也可通过良好的设计改善。
32 快速排序的基本实现
快速排序算法是一种基于交换的高效的排序算法,它采用了分治法的思想:
1、从数列中取出一个数作为基准数(枢轴,pivot)。
2、将数组进行划分(partition),将比基准数大的元素都移至枢轴右边,将小于等于基准数的元素都移至枢轴左边。
3、再对左右的子区间重复第二步的划分操作,直至每个子区间只有一个元素。
快排最重要的一步就是划分了。划分的过程用通俗的语言讲就是“挖坑”和“填坑”。
快速排序时间复杂度
快速排序的时间复杂度在最坏情况下是O(N2),平均的时间复杂度是O(N*lgN)。
这句话很好理解:假设被排序的数列中有N个数。遍历一次的时间复杂度是O(N),需要遍历多少次呢?至少lg(N+1)次,最多N次。
(01) 为什么最少是lg(N+1)次?快速排序是采用的分治法进行遍历的,我们将它看作一棵二叉树,它需要遍历的次数就是二叉树的深度,而根据完全二叉树的定义,它的深度至少是lg(N+1)。
因此,快速排序的遍历次数最少是lg(N+1)次。
(02) 为什么最多是N次?这个应该非常简单,还是将快速排序看作一棵二叉树,它的深度最大是N。因此,快读排序的遍历次数最多是N次。
33.正则表达式
特别字符 描述
$ 匹配输入字符串的结尾位置。如果设置了 RegExp 对象的 Multiline 属性,则 $ 也匹配 '\n' 或 '\r'。要匹配 $ 字符本身,请使用 \$。
( ) 标记一个子表达式的开始和结束位置。子表达式可以获取供以后使用。要匹配这些字符,请使用 \( 和 \)。
* 匹配前面的子表达式零次或多次。要匹配 * 字符,请使用 \*。
+ 匹配前面的子表达式一次或多次。要匹配 + 字符,请使用 \+。
. 匹配除换行符 \n 之外的任何单字符。要匹配 . ,请使用 \. 。
[ 标记一个中括号表达式的开始。要匹配 [,请使用 \[。
? 匹配前面的子表达式零次或一次,或指明一个非贪婪限定符。要匹配 ? 字符,请使用 \?。
\ 将下一个字符标记为或特殊字符、或原义字符、或向后引用、或八进制转义符。例如, 'n' 匹配字符 'n'。'\n' 匹配换行符。序列 '\\' 匹配 "\",而 '\(' 则匹配 "("。
^ 匹配输入字符串的开始位置,除非在方括号表达式中使用,当该符号在方括号表达式中使用时,表示不接受该方括号表达式中的字符集合。要匹配 ^ 字符本身,请使用 \^。
{ 标记限定符表达式的开始。要匹配 {,请使用 \{。
| 指明两项之间的一个选择。要匹配 |,请使用 \|。
通过在 *、+ 或 ? 限定符之后放置 ?,该表达式从"贪婪"表达式转换为"非贪婪"表达式或者最小匹配
34.特别是const和引用数据成员只能用初始化,不能被赋值。
35.初始化列表中成员列出的顺序和它们在类中声明的顺序相同
36.虚函数的目的是让派生类去定制自己的行为,实现多态
37.在operator=中对所有数据成员赋值,赋值函数和拷贝构造函数在继承类中都要有对基类的赋值和拷贝,否则会造成基类的某些数据成员没有初始化;,“通过值来传递一个对象”的具体含义是由这个对象的类的拷贝构造函数定义的。
38.在operator=中检查给自己赋值的情况
39.然而,我们刚才研究的这个类是要设计成可以允许固定类型到rational的隐式转换的——这就是为什么rational的构造函数没有声明为explicit的原因。
40.尽量用“传引用”而不用“传值”,这会非常高效:没有构造函数或析构函数被调用,因为没有新的对象被创建。
通过引用来传递参数还有另外一个优点:它避免了所谓的“切割问题(slicing problem)”。当一个派生类的对象作为基类对象被传递时,它(派生类对象)的作为派生类所具有的行为特性会被“切割”掉,从而变成了一个简单的基类对象。这往往不是你所想要的。
41.必须返回一个对象时不要试图返回一个引用
42.operator const char*() const是类型转换函数的定义,即该类型可以自动转换为const char*类型。operator char *() const; // 转换string -> char*;
则是不写返回类型,但是必须返回对应类型的值,即必须出现return语句.
const string b("hello world"); // b是一个const对象
char *str = b; // 调用b.operator char*()
strcpy(str, "hi mom"); // 修改str指向的值
b的值现在还是"hello world"吗?或者,它是否已经变成了对母亲的问候语?答案完全取决于string::operator char*的实现。
string::operator char*本身写的没有一点错,麻烦的是它可以用于const对象。如果这个函数不声明为const,就不会有问题,因为这样它就不能用于象b这样的const对象了。
或者使函数为非const,或者重写函数,使之不返回句柄。
43.一个给定的内联函数是否真的被内联取决于所用的编译器的具体实现。大多数编译器拒绝内联"复杂"的函数(例如,包含循环和递归的函数);还有,即使是最简单的虚函数调用,编译器的内联处理程序对它也爱莫能助。(这一点也不奇怪。virtual的意思是"等到运行时再决定调用哪个函数",inline的意思是"在编译期间将调用之处用被调函数来代替",如果编译器甚至还不知道哪个函数将被调用,当然就不能责怪它拒绝生成内联调用了)。
44.接口和实现的分离。 effective c++ 条款34
分离的关键在于,"对类定义的依赖" 被 "对类声明的依赖" 取代了。所以,为了降低编译依赖性,我们只要知道这么一条就足够了:只要有可能,尽量让头文件不要依赖于别的文件;如果不可能,就借助于类的声明,不要依靠类的定义。句柄类和协议类分离了接口和实现,从而降低了文件间编译的依赖性。支持协议类接口的某个具体类(concrete class)必然要被定义,真的构造函数也必然要被调用。
45。
使得事情更趋复杂的另一个原因是,C++中很多不同的部件或多或少地好象都在做相同的事。例如:
· 假如需要设计一组具有共同特征的类,是该使用继承使得所有的类都派生于一个共同的基类呢,还是使用模板使得它们都从一个共同的代码框架中产生?
· 类A的实现要用到类B,是让A拥有一个类型为B的数据成员呢,还是让A私有继承于B?
· 假设想设计一个标准库中没有提供的、类型安全的同族容器类(条款49列出了标准库实际提供的容器类),是使用模板呢,还是最好为某个 "自身用普通(void*)指针来实现" 的类建立类型安全的接口呢?
46.有时,声明一个除纯虚函数外什么也不包含的类很有用。这样的类叫协议类(Protocol class),它为派生类仅提供函数接口,完全没有实现,简单虚函数的情况和纯虚函数有点不一样。照例,派生类继承了函数的接口,但简单虚函数一般还提供了实现,派生类可以选择改写它们或不改写它们。思考片刻就可以认识到:
· 定义纯虚函数的目的在于,使派生类仅仅只是继承函数的接口。
(基类的纯虚函数也可以定义,这样在派生类中可以调用该方法,但是必须重写该函数;)
· 声明简单虚函数的目的在于,使派生类继承函数的接口和缺省实现。
· 声明非虚函数的目的在于,使派生类继承函数的接口和强制性实现。
47.任何条件下都要禁止重新定义继承而来的非虚函数。非虚函数是静态绑定的,相反,虚函数是动态绑定的,结论是,如果写类D时重新定义了从类B继承而来的非虚函数mf,D的对象就可能表现出精神分裂症般的异常行为。也就是说,D的对象在mf被调用时,行为有可能象B,也有可能象D,决定因素和对象本身没有一点关系,而是取决于指向它的指针所声明的类型。引用也会和指针一样表现出这样的异常行为。
48.虚函数是动态绑定而缺省参数值是静态绑定的。
49.,"向下转换" 可以通过几种方法来消除。最好的方法是将这种转换用虚函数调用来代替,同时,它可能对有些类不适用,所以要使这些类的每个虚函数成为一个空操作。第二个方法是加强类型约束,使得指针的声明类型和你所知道的真的指针类型之间没有出入。是有比上面那种原始转换更好的办法。这种方法称为 "安全的向下转换",它通过C++的dynamic_cast运算符(参见条款M2)来实现。当对一个指针使用dynamic_cast时,先尝试转换,如果成功(即,指针的动态类型(见条款38)和正被转换的类型一致),就返回新类型的合法指针;如果dynamic_cast失败,返回空指针。
普通的向下转换:static_cast
另一种直接将虚函数用于所有的基类和派生类,基类虚函数定义为空;
第三种是安全的向下转换:SavingsAccount *psa = dynamic_cast
50.使某个类的对象成为另一个类的数据成员,从而实现将一个类构筑在另一个类之上,这一过程称为 "分层"(Layering)。
51.区分继承和模板
· 当对象的类型不影响类中函数的行为时,就要使用模板来生成这样一组类。
· 当对象的类型影响类中函数的行为时,就要使用继承来得到这样一组类。
52.明智地使用私有继承
这为我们引出了私有继承的含义:私有继承意味着 "用...来实现"。如果使类D私有继承于类B,这样做是因为你想利用类B中已经存在的某些代码,而不是因为类型B的对象和类型D的对象之间有什么概念上的关系。因而,私有继承纯粹是一种实现技术。用条款36引入的术语来说,私有继承意味着只是继承实现,接口会被忽略。如果D私有继承于B,就是说D对象在实现中用到了B对象,仅此而已。私有继承在软件 "设计" 过程中毫无意义,只是在软件 "实现" 时才有用。
template
class Stack: private GenericStack {
public:
void push(T *objectPtr) { GenericStack::push(objectPtr); }
T * pop() { return static_cast
bool empty() const { return GenericStack::empty(); }
};
这是一段令人惊叹的代码,虽然你可能一时还没意识到。因为这是一个模板,编译器将根据你的需要自动生成所有的接口类。因为这些类是类型安全的,用户类型错误在编译期间就能发现。因为GenericStack的成员函数是保护类型,并且接口类把GenericStack作为私有基类来使用,用户将不可能绕过接口类。因为每个接口类成员函数被(隐式)声明为inline,使用这些类型安全的类时不会带来运行开销;生成的代码就象用户直接使用GenericStack来编写的一样(假设编译器满足了inline请求 ---- 参见条款33)。因为GenericStack使用了void*指针,操作堆栈的代码就只需要一份,而不管程序中使用了多少不同类型的堆栈。简而言之,这个设计使代码达到了最高的效率和最高的类型安全。很难做得比这更好。
本书的基本认识之一是,C++的各种特性是以非凡的方式相互作用的。这个例子,我希望你能同意,确实是非凡的。
从这个例子中可以发现,如果使用分层,就达不到这样的效果。只有继承才能访问保护成员,只有继承才使得虚函数可以重新被定义。(虚函数的存在会引发私有继承的使用,例子参见条款43)因为存在虚函数和保护成员,有时私有继承是表达类之间 "用...来实现" 关系的唯一有效途径。所以,当私有继承是你可以使用的最合适的实现方法时,就要大胆地使用它。同时,广泛意义上来说,分层是应该优先采用的技术,所以只要有可能,就要尽量使用它。
53.
· 共同的基类意味着共同的特性。如果类D1和类D2都把类B声明为基类,D1和D2将从B继承共同的数据成员和/或共同的成员函数。见条款43。
· 公有继承意味着 "是一个"。如果类D公有继承于类B,类型D的每一个对象也是一个类型B的对象,但反过来不成立。见条款35。
· 私有继承意味着 "用...来实现"。如果类D私有继承于类B,类型D的对象只不过是用类型B的对象来实现而已;类型B和类型D的对象之间不存在概念上的关系。见条款42。
· 分层意味着 "有一个" 或 "用...来实现"。如果类A包含一个类型B的数据成员,类型A的对象要么具有一个类型为B的部件,要么在实现中使用了类型B的对象。见条款40。
下面的对应关系只适用于公有继承的情况:
· 纯虚函数意味着仅仅继承函数的接口。如果类C声明了一个纯虚函数mf,C的子类必须继承mf的接口,C的具体子类必须为之提供它们自己的实现。见条款36。
· 简单虚函数意味着继承函数的接口加上一个缺省实现。如果类C声明了一个简单(非纯)虚函数mf,C的子类必须继承mf的接口;如果需要的话,还可以继承一个缺省实现。见条款36。
· 非虚函数意味着继承函数的接口加上一个强制实现。如果类C声明了一个非虚函数mf,C的子类必须同时继承mf的接口和实现。实际上,mf定义了C的 "特殊性上的不变性"。见条款36。
54.构造函数中为什么不能调用虚函数?
对于在构造函数中调用一个虚函数的情况,被调用的只是这个函数的本地版本。也就是说,虚机制在构造函数中不工作。
1. 从语法上讲,调用完全没有问题。
2. 但是从效果上看,往往不能达到需要的目的。
Effective 的解释是:
派生类对象构造期间进入基类的构造函数时,对象类型变成了基类类型,而不是派生类类型。
同样,进入基类析构函数时,对象也是基类类型。
所以,虚函数始终仅仅调用基类的虚函数(如果是基类调用虚函数),不能达到多态的效果,所以放在构造函数中是没有意义的,而且往往不能达到本来想要的效果。
55.如何用两个栈模拟实现一个队列? 如果这两个堆栈的容量分别是m和n(m>n),你的方法能保证队列的最大容量是多少?
栈的特点是“后进先出(LIFO)”,而队列的特点是“先进先出(FIFO)”。用两个栈模拟实现一个队列的基本思路是:用一个栈作为存储空间,另一个栈作为输出缓冲区,入队时把元素按顺序压入两栈模拟的队列,出队时按入队的顺序出栈即可。
56.假如你永远不想让类的对象进行赋值。
方法是声明这个函数(operator=),并使之为private。显式地声明一个成员函数,就防止了编译器去自动生成它的版本;使函数为private,就防止了别人去调用它。