c++面试常见问题2019-10-03

多线程和多进程的应用场景

多线程模型适用于I/O密集型场景,因为I/O密集型场景因为I/O阻塞导致频繁切换,线程只占用栈,程序计数器,一组寄存器等少量资源,切换效率高,单机多核分布式。

多进程模型适用于需要频繁的计算场景,多机分布式。



c++面试常见问题2019-10-03_第1张图片

网络的七层协议和五层协议;各层都分别有哪些协议?

TCP/IP: 

数据链路层:ARP,RARP 

网络层: IP,ICMP,IGMP 

传输层:TCP ,UDP,UGP 

应用层:Telnet,FTP,SMTP,SNMP. 

OSI: 

物理层:EIA/TIA-232, EIA/TIA-499, V.35, V.24, RJ45, Ethernet, 802.3, 802.5, FDDI, NRZI, NRZ, B8ZS

数据链路层:Frame Relay, HDLC, PPP, IEEE 802.3/802.2, FDDI, ATM, IEEE 802.5/802.2 

网络层:IP,IPX,AppleTalk DDP 

传输层:TCP,UDP,SPX 

会话层:RPC,SQL,NFS,NetBIOS,names,AppleTalk,ASP,DECnet,SCP 

表示层:TIFF,GIF,JPEG,PICT,ASCII,EBCDIC,encryption,MPEG,MIDI,HTML 

应用层:FTP,WWW,Telnet,NFS,SMTP,Gateway,SNMP

c++面试常见问题2019-10-03_第2张图片
c++面试常见问题2019-10-03_第3张图片
c++面试常见问题2019-10-03_第4张图片

http长链接和短连接的区别?

短连接 

连接->传输数据->关闭连接

HTTP是无状态的,浏览器和服务器每进行一次HTTP操作,就建立一次连接,但任务结束就中断连接。

也可以这样说:短连接是指SOCKET连接后发送后接收完数据后马上断开连接。

长连接 

连接->传输数据->保持连接 -> 传输数据-> 。。。 ->关闭连接。

长连接指建立SOCKET连接后不管是否使用都保持连接,但安全性较差。

http的长连接 

HTTP也可以建立长连接的,使用Connection:keep-alive,HTTP 1.1默认进行持久连接。HTTP1.1和HTTP1.0相比较而言,最大的区别就是增加了持久连接支持(貌似最新的 http1.0 可以显示的指定 keep-alive),但还是无状态的,或者说是不可以信任的。

什么时候用长连接,短连接? 

长连接多用于操作频繁,点对点的通讯,而且连接数不能太多情况。每个TCP连接都需要三步握手,这需要时间,如果每个操作都是先连接,再操作的话那么处理速度会降低很多,所以每个操作完后都不断开,次处理时直接发送数据包就OK了,不用建立TCP连接。例如:数据库的连接用长连接, 如果用短连接频繁的通信会造成socket错误,而且频繁的socket 创建也是对资源的浪费。

而像WEB网站的http服务一般都用短链接,因为长连接对于服务端来说会耗费一定的资源,而像WEB网站这么频繁的成千上万甚至上亿客户端的连接用短连接会更省一些资源,如果用长连接,而且同时有成千上万的用户,如果每个用户都占用一个连接的话,那可想而知吧。所以并发量大,但每个用户无需频繁操作情况下需用短连好

总之,长连接和短连接的选择要视情况而定。 




c++面试常见问题2019-10-03_第5张图片

TCP协议在每次建立连接时,都要在收发双方之间交换 (   三个  ) 报文

TCP建立连接的过程会交换哪些信息(起始的序列号、窗口、各种选项)???



string简单实现

#include

#include

using namespace std;

class String

{

public:

    String(const char* str = NULL);//通用构造函数,String("abc")

    String(const String &str);//拷贝构造

    ~String();

    String& operator=(const String &str);//赋值运算符。返回引用

    String operator+(const String &str) const;

    String& operator+=(const String &str);//+=操作符。返回引用

    char& operator[](int n) const;//下标操作符。返回引用

    bool operator==(const String &str) const;

    int size() const;//字符串实际大小,不包括结束符

    const char *c_str() const;//将string转为char *

private:

    char *data;

    int length;

};

String::String(const char* str)//通用构造

{

    if (!str)

    {//为空。String a()

        length = 0;

        data = new char[1];

        *data = '\0';

    }

    else

    {

        length = strlen(str);

        data = new char[length + 1];

        strcpy(data, str);//会拷贝源的结束符

    }

}

String::String(const String &str)//拷贝构造,深拷贝

{

    length = str.size();

    data = new char[length + 1];

    strcpy(data, str.c_str());

}

String::~String()

{

    delete[] data;

    length = 0;

}

String& String::operator=(const String &str)//赋值操作符4步

{

    if (this == &str) return *this;//1 自我赋值,返回自身引用

    delete[] data;//2 删除原有数据


    length = str.size();//3 深拷贝

    data = new char[length + 1];

    strcpy(data, str.c_str());

    return *this;//4 返回自身引用

}

String String::operator+(const String &str) const//+操作符3步

{//新建对象包括新空间,拷贝两个数据,返回新空间

    String newString;

    newString.length = length + str.size();

    newString.data = new char[newString.length + 1];

    strcpy(newString.data, data);

    strcat(newString.data, str.data);

    return newString;

}

String& String::operator+=(const String &str)//+=操作符5步

{//重分配新空间,拷贝两个数据,删除自己原空间,赋值为新空间,返回引用

    length += str.size();//成员length是实际长度

    char *newdata = new char[length + 1];

    strcpy(newdata, data);

    strcat(newdata, str.c_str());

    delete[] data;

    data = newdata;

    return *this;

}

char& String::operator[](int n) const

{//下标操作符,返回引用

    if (n >= length) return data[length - 1];//如果越界,返回最后一个字符

    else return data[n];

}

bool String::operator==(const String &str) const

{

    if (length != str.size()) return false;

    return strcmp(data, str.c_str()) ? false : true;

}

int String::size() const

{

    return length;

}

const char *String::c_str() const

{

    return data;

}

int main()

{

    char a[] = "Hello", b[] = "World!";

    String s1(a), s2(b);

    cout << s1.c_str() << endl;

    cout << s2.c_str() << endl;

    s1 += s2;

    cout << s1.c_str() << endl;

    s1 = s2;

    cout << s1.c_str() << endl;

    cout << (s1 + s2).c_str() << endl;

    cout << s1.size() << endl;

    cout << s1[1] << endl;

    if (s1 == s2)

        cout << "相等" << endl;

}


拷贝构造函数为什么是常引用

一、为何要用引用:

例如,在执行bbb.myTestFunc(aaa);时,其实会调用拷贝构造函数。如果我们的拷贝构造函数的参数不是引用,那么在bbb.myTestFunc(aaa);时,调用CExample ex = aaa;,又因为ex之前没有被创建,所以又需要调用拷贝构造函数,故而又执行CExample ex = aaa;,就这样永远的递归调用下去了。

所以, 拷贝构造函数是必须要带引用类型的参数的, 而且这也是编译器强制性要求的。

二、为何要要用const

如果在函数中不会改变引用类型参数的值,加不加const的效果是一样的。而且不加const,编译器也不会报错。但是为了整个程序的安全,还是加上const,防止对引用类型参数值的意外修改。


孤儿进程可以理解为一个子进程的父进程英年早逝(父进程先于子进程退出),就将这样的一个进程称为孤儿进程。

僵尸进程:

(1)父进程成功创建子进程,且子进程先于父进程退出。 

(2)子进程需要父进程回收其所占资源,释放pcb(进程控制块)。但是父进程不作为,不去释放已经退出子进程的pcb。 

(3)这样的子进程变为僵尸进程。 

(4)僵尸进程是一个已经死掉了的进程。



重载和重写的区别:

(1)范围区别:重写和被重写的函数在不同的类中,重载和被重载的函数在同一类中。

(2)参数区别:重写与被重写的函数参数列表一定相同,重载和被重载的函数参数列表一定不同。

(3)virtual的区别:重写的基类必须要有virtual修饰,重载函数和被重载函数可以被virtual修饰,也可以没有。

隐藏和重写,重载的区别:

(1)与重载范围不同:隐藏函数和被隐藏函数在不同类中。

(2)参数的区别:隐藏函数和被隐藏函数参数列表可以相同,也可以不同,但函数名一定同;当参数不同时,无论基类中的函数是否被virtual修饰,基类函数都是被隐藏,而不是被重写。 



循环引用

https://blog.csdn.net/sai_j/article/details/82908241



拷贝构造函数起作用的三种情况:

1.当用类的对象去初始化同类的另一个对象时。

    Date d2(d1);

    Date d2 = d1;  //初始化语句,并非赋值语句。

2.当函数的形参是类的对象,调用函数进行形参和实参结合时。

    void Func(A a1)  //形参是类Date的对象a1

    {  }

    int main( )

    {

      A a

      Func(a2); //调用Func时,实参a2是类Date的对象,将调用拷贝构造函数,初始化形参a1.

      return 0;

    }

3.当函数的返回值是对象,函数执行完成返回调用者时。

    A Func1()

    {

        A a1(4);

        return a1;  //函数的返回值是对象

    }

    int main( )

    {

        A a2;

        a2 = Func1();  //函数执行完成,返回调用者时,调用拷贝构造函数

        return 0;

    }

    在函数Func1( )内,执行语句“return a1;”时,将会调用拷贝构造函数将a1的值复制到一个匿名对象中,

    这个匿名对象是编译系统在主程序中临时创建的。函数执行结束时对象a1消失,但临时对象会存在于语句

    “a2 = Func( )”中。执行完这个语句后,临时对象的使命也就完成了,该临时对象便自动消失了。


异步进程通信方式---信号

同步,是所有的操作都做完,才返回给用户结果。即写完数据库之后,在相应用户,用户体验不好。

异步,不用等所有操作等做完,就相应用户请求。即先相应用户请求,然后慢慢去写数据库,用户体验较好。


TCP 滑动窗口(发送窗口和接收窗口)

TCP的滑动窗口主要有两个作用,一是提供TCP的可靠性,二是提供TCP的流控特性。同时滑动窗口机制还体现了TCP面向字节流的设计思路。

发送窗口与接收窗口关系

TCP是双工的协议,会话的双方都可以同时接收、发送数据。TCP会话的双方都各自维护一个“发送窗口”和一个“接收窗口”。其中各自的“接收窗口”大小取决于应用、系统、硬件的限制(TCP传输速率不能大于应用的数据处理速率)。各自的“发送窗口”则要求取决于对端通告的“接收窗口”,要求相同。


数据库索引为什么要用B+树?

B/B+树是为了磁盘或其它存储设备而设计的一种平衡多路查找树(相对于二叉,B树每个内节点有多个分支),与红黑树相比,在相同的的节点的情况下,一颗B/B+树的高度远远小于红黑树的高度(在下面B/B+树的性能分析中会提到)。B/B+树上操作的时间通常由存取磁盘的时间和CPU计算时间这两部分构成,而CPU的速度非常快,所以B树的操作效率取决于访问磁盘的次数,关键字总数相同的情况下B树的高度越小,磁盘I/O所花的时间越少。



名称 关键字     用法

增加 insert     insert into user(name,age,sex) values(值1,值2,值3);

删除 delete     delete from user where 条件;

修改 update     update user set 字段1=值1,字段2=值2 where 条件;

查询 select     select * from user;

去重 distinct     select distinct 去重字段 from user;

在···之间 between     select * from user where age between 20 and 30; (查询年龄在20-30之间的用户)

模糊匹配 like     select * from user where name like ‘张_%’; (其中_匹配 一个字符,%匹配 一个或多个)

分页查询 LIMIT     SELECT * FROM user LIMIT 5; (查询前 5 个记录行)

记录条数 count     select COUNT(*) from user; (查询user表所有记录条数)

求和 sum     select sum(age) from user;(查询所有的年龄和)

最大最小值 max、min     select max(age) from user;(最大的年龄最小同理)

平均值 avg     select avg(age) from user;(所有人年龄的平均值)

排序 order by     select * from user order by age;(默认从小到大的正序, asc 正序,desc倒序)

分组 group by     select sex,count(*) from user group by sex;(分组查询男女总人数)

分组后筛选 having 其实与where用法相似,having后能用聚合函数where不行,分组筛选后建议用having关键字



在设计数据库时需要注意哪些?

1.在针对表结构设计时如果是n对n的关系,尽可能的设计成1对N的关系。避免表关联太复杂,以便于提高查询效率。

2.首先在定义字段名称是尽可能以简单字符串来完成,建议是能读懂字段所存储内容的大概意思,同时字段名称的长度在14个字符以内。

3.明确表字段是否允许为空,不建议表字段都为可为空,因为当为null时就会影响到查询效率。

4.在设置字段类型是时需要考虑字段应该存放那些值,有效的节省空间大小:

        只存在两个是否两个值 使用 bit类型;

        数字值 建议使用 int,大数据使用bigint,极小数据使用tinyint;

        如果是跟钱打交道字段,或者精度维度的地理位置,或者是有小数的,使用decimal;

        时间值得使用datetime类型

        如果是在有中文的字段 建议使用nvarchar,nchar

        如果只是字符 使用 varchar,char

        如果是大文本可使用text,ntext(Unicode码)

5.每张表建聚集索引

6.针对查询功能适当对表建立非聚集索引,但不建议建太多索引,索引太多会影响插入效率。


Linux对字符串常用操作命令

以空格分割字符串 

awk ‘{print $1}’

以特定字符分割字符串 

str=${str//,/ } ——————–//后面是分割字符串的标志符号,最后一个/后面还有一个空格

剪切字符串 

cut -b|-c|-f 3 ———————–b代表字节,-c代表字符,-f代表域 后面的数组是第几个字符

去掉字符串中的特定字符 

sed ‘s/\”//g’ s代表替换,默认字符被替换为空,\后面的字符是要被替换的字符,g表示全部替换


查找近十天更改过的文件

find ./ -name * -mtime -10

1.Linux查看当前操作系统版本信息  cat /proc/version

2.Linux查看版本当前操作系统内核信息 uname -a

3.linux查看版本当前操作系统发行信息 cat /etc/issue 或 cat /etc/centos-release

4.Linux查看cpu相关信息,包括型号、主频、内核信息等 cat /etc/cpuinfo

1.查看系统版本信息的命令 lsb_release -a

2.查看centos版本号 cat /etc/issue

3.使用 file /bin/ls



后台运行进程的命令?

1、nohup

2、setsid

3、&


vector的实现技术,关键在于其对大小的控制以及重新配置时的数据移动效率,一旦vector的旧有空间满载,如果客户端每新增一个元素,vector扩充空间,都是配置新空间,vector动态增加大小时,并不是在原空间之后持续新空间,而是以原大小的两倍另外配置一块较大的空间,然后将内容拷贝过来,然后才开始在原内容之后构造新元素,并释放原空间,因此,一旦引起空间重新配置,指向原vector的所有迭代器都失效了,这是程序员易犯的一个错误,务必小心。   并且vector维护的是一个连续线性空间,所以支持vector随机存取。

 Map是关联容器,以键值对的形式进行存储,方便进行查找,关键词起到索引的作用,值则表示与索引相关联的数据,以红黑树的结构实现,插入删除等操作都可以在O(log n)时间内完成。

 Set是关联容器,set中每个元素都只包含一个关键字,set支持高效的关键字查询操作---检查每一个给定的关键字是否在set中,set是以红黑树的平衡二叉检索树结构实现的,支持高效插入删除,插入元素的时候会自动调整二叉树的结构,使得每个子树根节点键值大于左子树所有节点的键值,小于右子树所有节点的键值,另外还得保证左子树和右子树的高度相等。



程序优化方法:

1.消除循环的低效率

2.减少过程调用

3.消除不必要的内存引用

4.循环展开

5.提高并行性


野指针是指存在一个指针变量,但是这个指针变量指向的内存空间已经被释放,这时候指针的值还是不为空。或未申请访问受限内存区域的指针。

"野指针"的成因主要有两种:

        (1)指针变量没有被初始化。任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。例如

                char *p = NULL;

                char *str = (char *) malloc(100);

        (2)指针p被free或者delete之后,没有置为NULL,让人误以为p是个合法的指针。

空指针的简单描述:

它 “与任何对象或函数的指针值都不相等”。也就是说, 取地址操作符 & 永远也不能得到空指针, 同样对 malloc() 的成功调用也不会返回空指针, 如果失败, malloc() 的确返回空指针, 这是空指针的典型用法:表示 “未分配”或者 “尚未指向任何地方”的指针。

空指针和未初始化的指针:

空指针在概念上不同于未初始化的指针。空指针可以确保不指向任何对象或函数; 而未初始化指针则可能指向任何地方。

野指针是如何产生的

野指针的产生有以下3种情况

1、定义一个指针变量时没有初始化

int *p;

2、动态开辟的内存空间在使用完后调用free函数释放掉这段内存空间,

//却没有将对应的指针职位NULL。虽然开辟的空间被释放掉但指针依旧存在。

int func()

{

    int *p = malloc(sizeof(int));

    free(p);//没有将p值为NULL的操作

}

3、对指针的操作已经超出了指针变量的作用域

比如通常我们实现了一个函数,该函数里创建了一个指针变量,而函数结束时最终返回这个指针变量,但是函数调用结束后,该函数的函数栈帧就会被销毁,所以返回的这个指针变量所指向的空间已经被释放了,因此这个指针变量指向的空间就变成了随机的。

使用野指针会产生的后果

我们知道野指针是指向一个不可知地址的指针,这里分为3种情况:

1、指向不可访问的地址

危害:触发段错误。

2、指向一个可用的,但是没有明确意义的空间

危害:程序可以正确运行,但通常这种情况下,我们就会认为我们的程序是正确的没有问题的,然而事实上就是有问题存在,所以这样就掩盖了我们程序上的错误。

3、指向一个可用的,而且正在被使用的空间

危害:如果我们对这样一个指针进行解引用,对其所指向的空间内容进行了修改,但是实际上这块空间正在被使用,那么这个时候变量的内容突然被改变,当然就会对程序的运行产生影响,因为我们所使用的变量已经不是我们所想要使用的那个值了。通常这样的程序都会崩溃,或者数据被损坏。

如何避免 

1、定义一个指针变量时一定记得初始化

2、动态开辟的内存空间使用完free之后一定将对应的指针置为NULL

3、不要在函数中返回栈空间的指针和引用

4、注意在使用时对指针的合法性的判断



内存泄露就是系统回收不了那些分配出去但是又不使用的内存, 随着程序的运行,可以使用的内存就会越来越少,机子就会越来越卡,直到内存数据溢出,然后程序就会挂掉,再跟着操作系统也可能无响应. 接着你就按重启了。



线程通信方式:

互斥、事件、临界区、消息队列

线程安全 

一般说来,确保线程安全的方法有这几个:竞争与原子操作、同步与锁、可重入、防过度优化

竞争与原子操作 

多个线程同时访问和修改一个数据,可能造成很严重的后果。出现严重后果的原因是很多操作被操作系统编译为汇编代码之后不止一条指令,因此在执行的时候可能执行了一半就被调度系统打断了而去执行别的代码了。一般将单指令的操作称为原子的(Atomic),因为不管怎样,单条指令的执行是不会被打断的。

因此,为了避免出现多线程操作数据的出现异常,Linux系统提供了一些常用操作的原子指令,确保了线程的安全。但是,它们只适用于比较简单的场合,在复杂的情况下就要选用其他的方法了。

同步与锁 

为了避免多个线程同时读写一个数据而产生不可预料的后果,开发人员要将各个线程对同一个数据的访问同步,也就是说,在一个线程访问数据未结束的时候,其他线程不得对同一个数据进行访问。

同步的最常用的方法是使用锁(Lock),它是一种非强制机制,每个线程在访问数据或资源之前首先试图获取锁,并在访问结束之后释放锁;在锁已经被占用的时候试图获取锁时,线程会等待,直到锁重新可用。

二元信号量是最简单的一种锁,它只有两种状态:占用与非占用,它适合只能被唯一一个线程独占访问的资源。对于允许多个线程并发访问的资源,要使用多元信号量(简称信号量)。

可重入 

一个函数被重入,表示这个函数没有执行完成,但由于外部因素或内部因素,又一次进入该函数执行。一个函数称为可重入的,表明该函数被重入之后不会产生任何不良后果。可重入是并发安全的强力保障,一个可重入的函数可以在多线程环境下放心使用。

防过度优化 

在很多情况下,即使我们合理地使用了锁,也不一定能够保证线程安全,因此,我们可能对代码进行过度的优化以确保线程安全。

我们可以使用volatile关键字试图阻止过度优化,它可以做两件事:第一,阻止编译器为了提高速度将一个变量缓存到寄存器而不写回;第二,阻止编译器调整操作volatile变量的指令顺序。


手写strcpy

char * strcpy(char *dst,const char *src) //[1]

{

    assert(dst != NULL && src != NULL);    //[2]

    char *ret = dst;  //[3]

    while ((*dst++=*src++)!='\0'); //[4]

    return ret;

}

注意问题:

[1]const修饰

源字符串参数用const修饰,防止修改源字符串。

[2]空指针检查

(A)不检查指针的有效性,说明答题者不注重代码的健壮性。

(B)检查指针的有效性时使用assert(!dst && !src);

char *转换为bool即是类型隐式转换,这种功能虽然灵活,但更多的是导致出错概率增大和维护成本升高。

(C)检查指针的有效性时使用assert(dst != 0 && src != 0);

直接使用常量(如本例中的0)会减少程序的可维护性。而使用NULL代替0,如果出现拼写错误,编译器就会检查出来。

[3]返回目标地址

(A)忘记保存原始的strdst值。

[4]'\0'

(A)循环写成while (*dst++=*src++);明显是错误的。

(B)循环写成while (*src!='\0') *dst++=*src++;

循环体结束后,dst字符串的末尾没有正确地加上'\0'。


C语言文件操作API

FILE这个结构包含了文件操作的基本属性,对文件的操作都要通过这个结构的指针来进行,此种文件操作常用的函数见下表 函数 功能

fopen() 打开流

fclose() 关闭流

fputc() 写一个字符到流中

fgetc() 从流中读一个字符

fseek() 在流中定位到指定的字符

fputs() 写字符串到流

fgets() 从流中读一行或指定个字符

fprintf() 按格式输出到流

fscanf() 从流中按格式读取

feof() 到达文件尾时返回真值

ferror() 发生错误时返回其值

rewind() 复位文件定位器到文件开始处

remove() 删除文件

fread() 从流中读指定个数的字符

fwrite() 向流中写指定个数的字符

tmpfile() 生成一个临时文件流

tmpnam() 生成一个唯一的文件名

二、直接I/O文件操作

函数 说明

open() 打开一个文件并返回它的句柄

close() 关闭一个句柄

lseek() 定位到文件的指定位置

read() 块读文件

write() 块写文件

eof() 测试文件是否结束

filelength() 取得文件长度

rename() 重命名文件

chsize() 改变文件长度

https://blog.csdn.net/tantion/article/details/85927721



两个结点的最近公共祖先

/**

* Definition for a binary tree node.

* struct TreeNode {

*    int val;

*    TreeNode *left;

*    TreeNode *right;

*    TreeNode(int x) : val(x), left(NULL), right(NULL) {}

* };

*/

class Solution {

public:

    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {

        if(p == NULL || q == NULL || root == NULL)

            return NULL;

        if(p->val < root->val && q->val < root->val)

            return lowestCommonAncestor(root->left, p, q);

        if(p->val > root->val && q->val > root->val)

            return lowestCommonAncestor(root->right, p, q);

        return root;

    }

};


二叉树距离最远的两个节点?

//后序 求 每个节点最远距离, 倒着遍历, 只遍历了一遍 O( N )

//如果从上往下遍历 则 O( N*N )

int GetMaxPathLen( Node* root, int& maxLen ) //maxLen初始值传0

{

//别用 全局变量(存在线程安全问题)

if ( NULL == root )

return 0;

int left = GetMaxPathLen( root->_left, maxLen );

int right = GetMaxPathLen( root->_right, maxLen );

if ( left + right > maxLen )

maxLen = left + right;

//返回高度

return (left>right? left:right) + 1;

}

//运行结束后的 maxLen值 就是树中最远的两个节点的距离


红黑树和B树应用场景有何不同?

2者都是有序数据结构,可用作数据容器。红黑树多用在内部排序,即全放在内存中的,微软STL的map和set的内部实现就是红黑树。B树多用在内存里放不下,大部分数据存储在外存上时。因为B树层数少,因此可以确保每次操作,读取磁盘的次数尽可能的少。

在数据较小,可以完全放到内存中时,红黑树的时间复杂度比B树低。反之,数据量较大,外存中占主要部分时,B树因其读磁盘次数少,而具有更快的速度。

B树相对于红黑树的区别

在大规模数据存储的时候,红黑树往往出现由于树的深度过大而造成磁盘IO读写过于频繁,进而导致效率低下的情况。为什么会出现这样的情况,我们知道要获取磁盘上数据,必须先通过磁盘移动臂移动到数据所在的柱面,然后找到指定盘面,接着旋转盘面找到数据所在的磁道,最后对数据进行读写。磁盘IO代价主要花费在查找所需的柱面上,树的深度过大会造成磁盘IO频繁读写。根据磁盘查找存取的次数往往由树的高度所决定,所以,只要我们通过某种较好的树结构减少树的结构尽量减少树的高度,B树可以有多个子女,从几十到上千,可以降低树的高度。

B树和B+树的区别

B树:所有叶子结点都出现在同一层,叶子结点不包含任何关键字信息。

B+树:所有的叶子结点中包含了全部关键字的信息,及指向含有这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大的顺序链接,所有的非终端结点可以看成是索引部分,结点中仅含有其子树根结点中最大(或最小)关键字。 (而B 树的非终节点也包含需要查找的有效信息)

为什么说B+比B树更适合实际应用中操作系统的文件索引和数据库索引?

1) B+的磁盘读写代价更低

B+的内部结点并没有指向关键字具体信息的指针。因此其内部结点相对B树更小。如果把所有同一内部结点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多。一次性读入内存中的需要查找的关键字也就越多。相对来说IO读写次数也就降低了。

2) B±tree的查询效率更加稳定

由于非终结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。

数据库索引采用B+树的主要原因是 B树在提高了磁盘IO性能的同时并没有解决元素遍历的效率低下的问题。正是为了解决这个问题,B+树应运而生。B+树只要遍历叶子节点就可以实现整棵树的遍历。而且在数据库中基于范围的查询是非常频繁的,而B树不支持这样的操作(或者说效率太低)



.内核态与用户态的区别

内核态与用户态是操作系统的两种运行级别,当程序运行在3级特权级上时,就可以称之为运行在用户态。因为这是最低特权级,是普通的用户进程运行的特权级,大部分用户直接面对的程序都是运行在用户态;

当程序运行在0级特权级上时,就可以称之为运行在内核态。

运行在用户态下的程序不能直接访问操作系统内核数据结构和程序。当我们在系统中执行一个程序时,大部分时间是运行在用户态下的,在其需要操作系统帮助完成某些它没有权力和能力完成的工作时就会切换到内核态(比如操作硬件)。

这两种状态的主要差别是

处于用户态执行时,进程所能访问的内存空间和对象受到限制,其所处于占有的处理器是可被抢占的

处于内核态执行时,则能访问所有的内存空间和对象,且所占有的处理器是不允许被抢占的。


Linux系统是依靠软中断来实现用户态到内核态的切换的。具体过程可分为以下几步:

1、用户程序执行到调用系统调用处引发一个异常,这个异常就是触发int&0x80指令

2、int&0x80会导致系统切换到内核态,并执行第128号异常(中断)处理程序

3、而第128号异常处理程序就是用来执行系统调用的。



字符串反转单词
string reverseWords(string s) {

        int front = 0;

        for(int i = 0; i <= s.length(); ++i){

            if(i == s.length() || s[i] == ' '){

                reverse(&s[front], &s[i]);

                front = i + 1;

            }

        }

        return s;

    }


股票最佳收益

class Solution {

public:

    int maxProfit(vector& prices) {

        int buyprice = INT_MAX;

        int benifit = 0;

        for (int i = 0; i < prices.size();i++){

            if (prices[i] < buyprice ){

                buyprice = prices[i];

            }

            else if (prices[i]-buyprice > benifit){

                benifit = prices[i]-buyprice;

            }

        }

        return benifit;

    }

};



海量字符流判重问题(bitmap,布隆过滤器)



功能:把格式化的数据写入某个字符串缓冲区。

头文件:stdio.h

原型:int sprintf( char *buffer, const char *format, [ argument] … );

参数列表:buffer:char型指针,指向将要写入的字符串的缓冲区;format:格式化字符串;[argument]...:可选参数,可以是任何类型的数据。

返回值: 返回成功写入buffer 的字符数,出错则返回-1. 如果 buffer 或 format 是空指针,且不出错而继续,函数将返回-1,并且 errno 会被设置为 EINVAL。sprintf 返回被写入buffer 的字节数,结束字符‘\0’不计入内。即,如果“Hello”被写入空间足够大的buffer后,函数sprintf 返回5。

功能:用于将格式化的数据写入字符串

*1 如果格式化后的字符串长度 < n,则将此字符串全部复制到str中,并给其后添加一个字符串结束符('\0');

*2 如果格式化后的字符串长度 >= n,则只将其中的(n-1)个字符复制到str中,并给其后添加一个字符串结束符('\0'),返回值为欲写入的字符串长度。(跟编译器有关)

头文件:stdio.h

原型:int snprintf(char *str, int n, char * format [, argument, ...]);

参数列表:str为要写入的字符串;n为要写入的字符的最大数目,超过n会被截断;format为格式化字符串,与printf()函数相同;[argument]...:可选参数,可以是任何类型的数据。

返回值:成功则返回参数str 字符串长度,失败则返回-1,错误原因存于errno 中。

**注意**:snprintf()并不是标C中规定的函数,在GCC中,该函数名称就snprintf(),而在VC中称为_snprintf()。由于不是标准函数,没有一个统一的标准来规定该函数的行为。所以不同编译器结果可能不同。[参考](http://c.biancheng.net/cpp/html/2417.html)

在VS2008中需在预编译处加入

区别

snprintf比sprintf更加安全,可以控制写入字符串长度;

snprintf会先把目标字符串清空,然后写入;而sprintf不会。这会导致问题参考

返回值不同。snprintf成功则返回欲写入字符串长度即格式化后字符串长度,失败则返回负值;sprintf返回成功写入字符串长度,出错返回-1。所以在使用返回值时候一定要注意特别是snprintf;


一、为什么会出现内存溢出问题?导致内存溢出问题的原因有很多,比如:

(1) 使用非类型安全(non-type-safe)的语言如 C/C++ 等。

(2) 以不可靠的方式存取或者复制内存缓冲区。

(3) 编译器设置的内存缓冲区太靠近关键数据结构。

下面来分析这些因素:

1. 内存溢出问题是 C 语言或者 C++ 语言所固有的缺陷,它们既不检查数组边界,又不检查类型可靠性(type-safety)。众所周知,用 C/C++ 语言开发的程序由于目标代码非常接近机器内核,因而能够直接访问内存和寄存器,这种特性大大提升了 C/C++ 语言代码的性能。只要合理编码,C/C++ 应用程序在执行效率上必然优于其它高级语言。然而,C/C++ 语言导致内存溢出问题的可能性也要大许多。其他语言也存在内容溢出问题,但它往往不是程序员的失误,而是应用程序的运行时环境出错所致。

2. 当应用程序读取用户(也可能是恶意攻击者)数据,试图复制到应用程序开辟的内存缓冲区中,却无法保证缓冲区的空间足够时(换言之,假设代码申请了 N 字节大小的内存缓冲区,随后又向其中复制超过 N 字节的数据)。内存缓冲区就可能会溢出。想一想,如果你向 12 盎司的玻璃杯中倒入 16 盎司水,那么多出来的 4 盎司水怎么办?当然会满到玻璃杯外面了!

3. 最重要的是,C/C++ 编译器开辟的内存缓冲区常常邻近重要的数据结构。现在假设某个函数的堆栈紧接在在内存缓冲区后面时,其中保存的函数返回地址就会与内存缓冲区相邻。此时,恶意攻击者就可以向内存缓冲区复制大量数据,从而使得内存缓冲区溢出并覆盖原先保存于堆栈中的函数返回地址。这样,函数的返回地址就被攻击者换成了他指定的数值;一旦函数调用完毕,就会继续执行“函数返回地址”处的代码。非但如此,C++ 的某些其它数据结构,比如 v-table 、例外事件处理程序、函数指针等,也可能受到类似的攻击。

二、解决内存溢出问题不要太悲观,下面讨论内存溢出问题的解决和预防措施。

1、改用受控代码

2、遵守黄金规则

当你用 C/C++ 书写代码时,应该处处留意如何处理来自用户的数据。如果一个函数的数据来源不可靠,又用到内存缓冲区,那么它就必须严格遵守下列规则:

必须知道内存缓冲区的总长度。

检验内存缓冲区。

提高警惕。

多态性,在c++中指具有不同功能的函数可以用同一个函数名,即可以用同一个函数名调用不同内容的函数。向不同的对象发送用一个消息,不同的对象在接收同样的消息,会产生不同的行为(方法)。

  从系统实现角度来看。多态性分为两类:静态多态性和动态多态性。

  静态多态性:在程序编译时系统就能决定调用哪个函数,因此静态函数有称编译时的多态性(实质上是通过函数的重载实现)。例如:函数的重载和运算符重载实现.

  动态多态性:运行过程中才动态地确定操作指针所指的对象。主要通过虚函数和重写来实现。



虚函数vptr的初始化时间
vptr都是在自身构造函数体之前初始化,vptr初始化是在初始化列表之前还是之后是跟编译器实现有关的


TCP慢启动

TCP在连接过程的三次握手完成后,开始传数据,并不是一开始向网络通道中发送大量的数据包,这样很容易导致网络中路由器缓存空间耗尽,从而发生拥塞;而是根据初始的cwnd大小逐步增加发送的数据量,cwnd初始化为1个最大报文段(MSS)大小(这个值可配置不一定是1个MSS);每当有一个报文段被确认,cwnd大小指数增长。


TCP_NODELAY选项是用来控制是否开启Nagle算法,该算法是为了提高较慢的广域网传输效率,减小小分组的报文个数,完整描述:

该算法要求一个TCP连接上最多只能有一个未被确认的小分组,在该小分组的确认到来之前,不能发送其他小分组。

套接字上設置了TCP_NODELAY。有的包头的包将被立即传输,在某些情況下(取决于内部的包计数器),因为这个包成功的被对方收到后需要请求对方确认。这样,大量数据的传输就会被推迟而且产生了不必要的网络流量交换。


map:

优点:

有序性,这是map结构最大的优点,其元素的有序性在很多应用中都会简化很多的操作

红黑树,内部实现一个红黑书使得map的很多操作在lgn的时间复杂度下就可以实现,因此效率非常的高

缺点: 空间占用率高,因为map内部实现了红黑树,虽然提高了运行效率,但是因为每一个节点都需要额外保存父节点、孩子节点和红/黑性质,使得每一个节点都占用大量的空间

适用处:对于那些有顺序要求的问题,用map会更高效一些

unordered_map:

优点: 因为内部实现了哈希表,因此其查找速度非常的快

缺点: 哈希表的建立比较耗费时间

适用处:对于查找问题,unordered_map会更加高效一些,因此遇到查找问题,常会考虑一下用unordered_map。


1)滑动窗口的作用:

 滑动窗口机制是TCP用来控制发送数据包速率的。

 发送方每次只能发送滑动窗口内部的数据包。

2)滑动窗口的运行方式:

 每收到一个新的确认(ack),滑动窗口的位置就向右移动一格。

 滑动窗口大小,受拥塞窗口(cwnd)和通告窗口(awnd)的制约。swnd = min [ cwnd , awnd ]。

 拥塞窗口是为了不造成阻塞,网络对发送方发包数量的限制。

 通告窗口是接收方TCP缓存当前的大小。它阻止由于发包数量过多,超出接收方缓存的容量。

3)滑动窗口的意义:

 因特网中有数以万计的TCP连接,它们需要共享带宽,缓存等网络资源。 TCP希望能最大效率的利用网络资源,并将资源公平的分配到每条TCP连接上,还要尽量保证不让网络超负荷。滑动窗口机制有效的解决了这些问题。



DNS域名解析使用的是TCP协议还是UDP协议?
DNS在进行区域传输的时候使用TCP协议,其它时候则使用UDP协议;

DNS的规范规定了2种类型的DNS服务器,一个叫主DNS服务器,一个叫辅助DNS服务器。在一个区中主DNS服务器从自己本机的数据文件中读取该区的DNS数据信息,而辅助DNS服务器则从区的主DNS服务器中读取该区的DNS数据信息。当一个辅助DNS服务器启动时,它需要与主DNS服务器通信,并加载数据信息,这就叫做区传送(zone transfer)。

为什么既使用TCP又使用UDP?

首先了解一下TCP与UDP传送字节的长度限制:

UDP报文的最大长度为512字节,而TCP则允许报文长度超过512字节。当DNS查询超过512字节时,协议的TC标志出现删除标志,这时则使用TCP发送。通常传统的UDP报文一般不会大于512字节。

区域传送时使用TCP,主要有一下两点考虑:

1.辅域名服务器会定时(一般时3小时)向主域名服务器进行查询以便了解数据是否有变动。如有变动,则会执行一次区域传送,进行数据同步。区域传送将使用TCP而不是UDP,因为数据同步传送的数据量比一个请求和应答的数据量要多得多。

2.TCP是一种可靠的连接,保证了数据的准确性。

域名解析时使用UDP协议:

客户端向DNS服务器查询域名,一般返回的内容都不超过512字节,用UDP传输即可。不用经过TCP三次握手,这样DNS服务器负载更低,响应更快。虽然从理论上说,客户端也可以指定向DNS服务器查询的时候使用TCP,但事实上,很多DNS服务器进行配置的时候,仅支持UDP查询包。


LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。



数据库之连接查询与组合查询

你可能感兴趣的:(c++面试常见问题2019-10-03)