春招面试题

1. 构造函数无返回值,原因?

构造函数和析构函数无返回值。在程序中创建和消除一个对象的行为非常特殊,就像出生和死亡,而且总是由编译器来调用这些函数以确保它们被执行。

如果它们有返回值,要么编译器必须知道如何处理返回值,要么就只能由客户程序员自己来显式地调用构造函数与析构函数,这样一来,安全性就被破坏了。另外析构函数不带任何参数,因为析构不需任何选项。

如果允许构造函数有返回值,在某些情况下,会引起歧义。(隐式的类类型转换)

class C{

public:

  C():x(0){}

  C(int i):x(i){}

private:

  int x;

}

如果C的构造函数可以有返回值,比如int:int  C():x(0){return 1;}//1表示构造成功,0表示失败。

那么当:C c=C(); 时,C()生成临时对象,再用它初始化c。但如果C()有一个int型返回值,在C的构造函数没有用explicit声明时,也可以看作是用int值隐式地对类对象初始化(通过C(int i)这个构造函数)。所以这种情况下产生了歧义。

还有一种情况是用C的构造函数生成的临时对象作为参数的实参:

void f(int a){}

void f(const C&a){}

此时调用f(C())就会产生歧义,是用int值作为实参,还是C对象作为实参。

 

 

2. TCP和UDP的区别

-TCP面向连接的,可靠的数据传输服务;UDP面向无连接的,尽最大努力的数据传输服务,不保证数据传输的可靠性。

-TCP面向字节流,UDP面向报文段。应用进程交给UDP多长的报文,UDP就照样发送,一次发送一个报文;TCP在发送时采取的方式完全不同:TCP根据对方给出的窗口值和当前网络拥塞的程度来决定一个报文段应该包含多少个字节。如果报文太长,TCP会将其拆分再发送,如果报文太短,TCP会等待积累足够多的字节再构成报文段发送出去。

-TCP数据传输慢,UDP数据传输快。

-TCP有拥塞控制,UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有效,如直播,实时视频会议等)

-TCP只能是一对一的通信(TCP连接的端点是套接字socket),而UDP支持一对一、一对多、多对一和多对多的通信。

-TCP的首部开销大,有20字节,比UDP的8个字节的首部要长。

-TCP提供可靠全双边的通信服务。UDP是半双工的,只能单向传播。

 

 

3. 堆排序

将数组看作完全二叉树,子节点和父节点之间下标对应关系。第一次调整堆时,从后往前比较子节点和其父节点大小,将最大或最小的节点值交还给父节点,该父节点是以其为根节点的子树的最大或最小值节点,一次递归到下标为0的根节点。将其数组最后一位交换。交换过后对二叉树进行调整时,从根节点按路径迭代到叶子节点,该路径选择依据是与父节点交换的那个叶子节点的分支,当不存在交换时直接结束迭代。

复杂度:第一次从后往前调整时,时间复杂度是n;之后的每一次路径调整时时logn,所以总的时间复杂度是nlogn。

 

 

4. 设计模式

 

5. 可以使用指针地址访问类的私有成员吗?

可以访问。

类成员的地址访问:用类实例化一个对象,该对象取址得到其在内存中的地址,依次可以访问到其所有成员变量(除static外,次序是其在类体中的声明顺序。如 C c; &c; &c+1这样的。可以通过地址对该成员变量进行访问修改(protected和private也可以)。访问其地址时需要reinterpret_cast转换成相应的数据类型。

如果要用访问类中函数的地址,直接用类的域操作符访问。如&C::get_smt;(此处不加括号)同样可以访问三种public、private和protected状态下的成员函数(静态函数除外)。找到了函数地址就可以赋给函数指针并调用。类的函数是所有对象公用的,存放在固定地址中,其调用时需要用类对象进行 . 或->这样方式进行调用,该变量自动变为这次调用的this指针指向对象。

注意:需要特殊的函数指针才可以指向类的成员函数。如typedef int (C::*pfunc)();这个函数指针类型的变量可以指向C::smt即C域操作符得到的成员函数地址。若成员函数是const,则还需声明特殊的函数指针类型(例如int (C::*pfunc)()const=&C::getsmt;)。

注意:私有和保护成员变量和函数的访问有一些注意点,私有或保护的成员变量可以在main流程中类似公有变量那样用地址访问,但私有和保护的成员函数不能直接在main流程中直接用对应类型的函数指针形式访问。因为私有/保护的成员在运行时会分配内存空间,而函数代码在编译期间确定位置。即运行时和编译时的区别,编译期间编译器会对访问权限进行检查,所以直接在main流程中访问私有/保护函数的地址会出错。不过有方法修改使私有/保护函数也可通过函数指针形式访问,就是将函数指针指向私有/保护成员函数的操作装入一个公有成员函数中,再在main中调用这个函数可以避免变异期间的权限检查。

例1:

#include<iostream>
using namespace std;
class C{
public:
C(int a,int b,int c,int d,int e,int f):a(a),c(c),e(e){
b=b;
d=d;
f=f;
}
~C(){}
public:
const int a;
int b;
int geta()const{
return a;
}
int getb(){
return b;
}
private:
const int c;
int d;
int getc()const{
return c;
}
int getd(){
return d;
}
protected:
const int e;
int f;
int gete()const{
return e;
}
int getf(){
return f;
}
};
int main(){
C test(1,2,3,4,5,6);
cout<<"a:"<<test.geta()<<endl;
cout<<"b:"<<test.getb()<<endl;
cout<<"sizeof(int):"<<sizeof(int)<<endl;
//地址访问公有成员变量
*reinterpret_cast<int*>(&test)=10;
*(reinterpret_cast<int*>(&test)+1)=20;
cout<<"a:"<<test.geta()<<endl;
cout<<"b:"<<test.getb()<<endl;
typedef int (C::*pfunc)();
//地址访问公有成员函数
int (C::*fa)()const=&C::geta;
pfunc fb=&C::getb;
cout<<"a:"<<(test.*fa)()<<endl;
cout<<"b:"<<(test.*fb)()<<endl;
//访问私有和保护成员,可以通过地址访问变量但不能访问函数
cout<<"c:"<<*(reinterpret_cast<int*>(&test)+2)<<endl;
//int (C::*fc)()const=&C::getc;
//cout<<"c:"<<(test.*fc)()<<endl;
//pfunc fd=&C::getd;
//cout<<"d:"<<(test.*fd)()<<endl;
return 0;
}

例2:

#include<iostream>
using namespace std;
class PrivateMemberFuncPtr
{
public:
void bind();
private:
void print()
{
cout<<"success"<<endl;
}
};
void (PrivateMemberFuncPtr::*ptr)();
void PrivateMemberFuncPtr::bind()
{
ptr=&PrivateMemberFuncPtr::print;
}
int main()
{
PrivateMemberFuncPtr obj;
obj.bind();
((&obj)->*ptr)();
}

 

6. 死锁产生条件和预防

1)死锁

死锁是指多个进程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。例如,进程A执行过程中对资源a,b进行上锁,进出B对资源b,a进行上锁,A和B的上锁顺序不同,且锁为互斥状态,并且不会释放锁。那么A获得a而B获得b时,由于A无法获得b而B无法获得a,所以无法继续执行,陷入死锁状态。

2)死锁产生的原因

系统资源的竞争、进程的运行顺序不合适、

3)死锁产生的四个必要条件

互斥条件、请求与保持条件、不可剥夺条件、循环等待条件

只要系统发生死锁,上述条件必然成立。

4)死锁的避免和预防

死锁避免-系统对进程发出的每一个系统能满足的资源申请进行动态检查,并且根据检查结果决定是否分配资源,如果分配后系统可能发生死锁,则不予分配,这是一种保证系统不进入死锁状态的动态策略。

死锁预防-破坏不可剥夺条件(一个进程不能获得所需要的全部资源时便处于等待状态,等待期间它占有的资源将被隐式的释放重新加入到系统的资源列表中,可以被其它进程使用)、破环请求与保持条件(在每个进程开始执行时就申请所需要的全部资源;或者动态分配每个进程在申请所需要的资源时它本身不占用系统资源)、破坏循环等待条件(采取资源有序分配基本思想将系统中的所有资源顺序编号,将紧缺的稀少的采用较大的编号,在申请资源时必须按照编号的顺序进行,一个进程只有获得较小编号的资源才能申请较大编号的进程)

 

死锁中的互斥条件一般不会进行改变。。。

 

7. 最长公共子序列

二维矩阵动态规划

 

8. STL迭代器失效问题

STL中的节点容器(map,set,list)中元素被插入、删除时只会导致指向该节点的迭代器失效,因为节点存放在分散的空间,插入删除并不会改变其它节点的地址。

STL中的顺序容器(vector,deque,string)中元素被插入、删除时会导致指向该节点之后或整个容器所有节点的迭代器失效。(vector、string存在空间重分配)。

 

9.私有和保护的构造析构函数

 1)构造函数

构造函数定义为protected后,就意味着你不能在类的外部构造对象了。而只能在外部构造该类的子类对象。

构造函数定义为private后,意味着不仅仅不能在类的外部构造对象了,而且也不能在外部构造该类的子类对象了,只能通过类的static静态函数来访问类的内部定义的对象,单例模式就是私有构造函数的典型实例。

拷贝构造和赋值操作符定义为私有后,意味着禁止在外部对类的对象进行复制操作。这种情况的典型应用是类的成员中含有锁成员变量时,禁止拷贝构造和赋值操作可以防止对象被拷贝后,拷贝的对象进行加锁后影响到原有对象加锁,从而违背程序员意愿。

2)析构函数

对于堆中的对象,通常都是用new/delete来创建/销毁,当调用new时,它会自动调用相应类的构造函数,当调用delete时,它会自动调用相应类的析构函数。而在栈中产生对象时,对象的创建/销毁是自动完成的,也就是在创建时自动调用构造函数,在销毁时自动调用析构函数,即不需要显式调用new/delete,但有个前提是类的构造/析构函数都必须是public的。

析构函数无论是protected还是private,其共同作用都是禁止在栈中产生对象,因为无法自动完成析构函数的调用,自然就不能在栈中创建对象了;当然如果在堆上创建对象时,也不能直接delete对象了,因为这样也会在外部析构该对象,但是可以间接完成堆对象的析构。比如用一个destroy公有成员函数来销毁对象。

私有和保护析构函数的区别在于:私有的析构函数不仅禁止了在栈中产生对象,同时也禁止了继承。

即私有析构函数的子类也不能在栈中创建对象,子类也不能直接delete。

 

 

10.TCP流与UDP数据报

https://www.jianshu.com/p/9ad55e8fcce2

TCP数据沾包、发送端封包、接收端拆包。

TCP发送数据会等到数据缓冲区有一定量数据后发送,UDP立即发送。

沾包:TCP有验证机制,所以在发包时为了减少发包数量,采用流式传输。TCP沾包是指发送发发送的若干包数据到接收方时沾成一包,从接收缓冲区看来,后一个包的头紧接着前一包的尾。TCP发的数据可能会有不同的结构类型,每种结构如果直接混在一起会难以识别,所以有包结构进行区分。

封包:加上包头,包头是一个固定长度的数据结构。表示了包体的长度。

根据包头长度和包头中记录的包体长度就得到了数据包。

拆包:接收端,可以用动态缓冲区暂存方式和底层缓冲区进行拆包。

 

 

11.锁在父子进程间的继承

像文件锁是不会被继承的。

子进程只会继承父进程中fork了该子进程的线程,其它线程不会复制。

子进程中的锁可能被它不知道的线程所拥有,如果这样,子进程将没法获取或释放这些锁。

可以在调用fork之前,线程先获取进程中的所有锁,调用fork之后分别在父子进程中释放这些锁,从而可以重新利用这些资源。

pthread_atfork( void (*prepare)(void), void (*parent)(void), void (*child)(void) );

它安装了线程在fork时调用的回调函数。prepare函数将在fork创建子进程之前被调用,通常可以用来获取进程中的所有锁;parent在fork创建子进程后返回前在父进程中被调用,可以用来释放父进程中的锁;child在fork返回前在子进程中被调用,可以用来释放子进程中的锁。

可以调用pthread_atfork多次注册多组回调函数,这时回调函数调用顺序如下:

(1)prepare函数调用顺序与它们的注册顺序相反;

(2)parent和child函数的调用顺序与注册顺序相同。

 

posted @ 2020-03-04 16:24  吉吉boy  阅读(191)  评论(0编辑  收藏  举报