c++各大面试总结

====================================================

技术一面:主要是一些关于数据结构和C/C++相关的内容

1、链表和数组的相同点和不同点?
答案:

相同点:两种结构均可实现数据的顺序存储,构造出来的模型呈线性结构。
不同点:
从逻辑结构上来说:
1)、链表是链式存储结构,数组是顺序存储结构
2)、 链表没有相应的下标,至于指向下一个元素的指针,而数组中每一个元素都有一个相对应的下标。
3)、链表是按指针可以指向空间不连续的元素与元素,而数组是把所有元素按照次序依次存储起来
4)、链表的插入与删除元素相对来说比较简单,不需要移动大量的元素,且较为容易实现长度扩充,但是查询某个元素不方便。
数组查询某个元素简单,但是插入与删除元素需要移动大量的元素,最大长度需要在一开始就要指定,所以扩充长度不如链表方便。
数组利用下标定位,时间复杂度为O(1),链表定位元素时间复杂度O(n);
  数组插入或删除元素的时间复杂度O(n),链表的时间复杂度O(1)。
从内存存储角度来说:
1)、(静态)数组是从栈上来分配空间,操作自由度小;
链表是从队上分配内存空间,操作自由度大,但申请管理比较麻烦,容易造成内存泄露问题。

2、指针数组和数组指针的区别:
答案:
1)、 指针数组:int *p[10]; p先是和[]结合说明p是数组,数组里面有10个元素,每个元素为int *,
数组指针:int (p)[10]; p先是和结合说明是p是一个指针变量,指向一维数组int[10];

     	数组指针(也称行指针)
定义 int (*p)[n];
()优先级高,首先说明p是一个指针,指向一个整型的一维数组,这个一维数组的长度是n,也可以说是p的步长。也就是说执行p+1时,p要跨过n个整型数据的长度。
如要将二维数组赋给一指针,应这样赋值:
int a[3][4];
int (*p)[4]; //该语句是定义一个数组指针,指向含4个元素的一维数组。
 p=a;        //将该二维数组的首地址赋给p,也就是a[0]或&a[0][0]
 p++;       //该语句执行过后,也就是p=p+1;p跨过行a[0][]指向了行a[1][]
所以数组指针也称指向一维数组的指针,亦称行指针。

指针数组
定义 int *p[n];
[]优先级高,先与p结合成为一个数组,再由int*说明这是一个整型指针数组,它有n个指针类型的数组元素。
这里执行p+1时,则p指向下一个数组元素,这样赋值是错误的:
p=a;因为p是个不可知的表示,只存在p[0]、p[1]、p[2]...p[n-1],
而且它们分别是指针变量可以用来存放变量地址。但可以这样 *p=a; 
这里*p表示指针数组第一个元素的值,a的首地址的值。
如要将二维数组赋给一指针数组:
int *p[3];
int a[3][4];
p++; //该语句表示p数组指向下一个数组元素。注:此数组每一个元素都是一个指针
for(i=0;i<3;i++)
p[i]=a[i]
这里int *p[3] 表示一个一维数组内存放着三个指针变量,分别是p[0]、p[1]、p[2]
所以要分别赋值。

这样两者的区别就豁然开朗了,数组指针只是一个指针变量,似乎是C语言里专门用来指向二维数组的,
它占有内存中一个指针的存储空间。指针数组是多个指针变量,以数组形式存在内存当中,
占有多个指针的存储空间。
还需要说明的一点就是,同时用来指向二维数组时,其引用和用数组名引用都是一样的。
比如要表示数组中i行j列一个元素:
*(p[i]+j)、*(*(p+i)+j)、(*(p+i))[j]、p[i][j]
优先级:()>[]>*

3、堆、栈、堆栈的区别
答案:
堆栈:什么是堆栈?又该怎么理解呢?
注意:其实堆栈本身就是栈,只是换了个抽象的名字
1.堆栈空间分配
①栈(操作系统):由操作系统自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
②堆(操作系统): 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收,分配方式倒是类似于链表。
2.堆栈缓存方式
①栈使用的是一级缓存, 他们通常都是被调用时处于存储空间中,调用完毕立即释放。
②堆则是存放在二级缓存中,生命周期由虚拟机的垃圾回收算法来决定(并不是一旦成为孤儿对象就能被回收)。
所以调用这些对象的速度要相对来得低一些。
3.堆栈数据结构区别
①堆(数据结构):堆可以被看成是一棵树,如:堆排序。
②栈(数据结构):一种先进后出的数据结构。

4、 了解静态链表吗?怎么用数组实现静态链表?
逻辑结构上相邻的数据元素,存储在指定的一块内存空间中,数据元素只允许在这块内存空间中随机存放,
这样的存储结构生成的链表称为静态链表。
静态链表和动态链表的区别:静态链表限制了数据元素存放的位置范围;动态链表是整个内存空间。

静态链表使用数组这一数据类型预先申请足够大的内存空间。

由于各数据元素在数组申请的内存空间内随机存放,为了体现逻辑上的相邻,
为每一个数据元素配备一个具有指针作用的整形变量,用于记录下一元素在数组中的位置。
在数组申请的存储空间中,各数据元素虽随机存储,每一个元素都记录着下一元素在数组中的位置,
通过前一个元素,可以找到下一个元素,构成了一条链表,这条被局限在特定内存空间的链表就是静态链表。
静态链表中每个结点既有自己的数据部分,还需要存储下一个结点的位置,
所以静态链表的存储实现使用的是结构体数组,
包含两部分: 数据域 和 游标(存放的是下一个结点在数组中的位置下标)
typedef struct 
{
  int data;//数据域
  int cur;//游标
}component;

详细参考网址:https://www.cnblogs.com/ciyeer/p/9027973.html

5、使用库函数strcpy应该注意的问题有哪些?
char* strcpy(char dest,const charsrc);
注意:
1)字符串dest的长度要大于src的长度,以防溢出,发生越界;
2) dest和src不能为NULL
3)src和dest所指内存区域不可以重叠且dest必须有足够的空间来容纳src的字符串
6、strcpy和memcpy的区别:
strcpy和memcpy主要有以下3方面的区别。
1)、复制的内容不同。strcpy只能复制字符串,而memcpy可以复制任意内容,例如字符数组、整型、结构体、类等。
2)、复制的方法不同。strcpy不需要指定长度,它遇到被复制字符的串结束符"\0"才结束,所以容易溢出。
memcpy则是根据其第3个参数决定复制的长度。
3)、用途不同。通常在复制字符串时用strcpy,而需要复制其他类型数据时则一般用memcpy
strcpy 是依据 “\0” 作为结束判断的,如果 s2 的空间不够,则会引起 buffer overflow。

memcpy用来在内存中复制数据,由于字符串是以“\0”结尾的,所以对于在数据中包含“\0”的数据只能用memcpy。

Strncpy和memcpy很相似,只不过它在一个终止的空字符处停止。当n>strlen(s1)时,给s2不够数的空间里填充“\0”;当n<=strlen(s1)时,s2是没有结束符“\0”的。

这里隐藏了一个事实,就是s2指向的内存一定会被写n个字符。

所以总的来说注意:

1、s2指向的空间要足够拷贝;使用strcpy时,s2指向的空间要大于等于s1指向的空间;使用strncpy或memcpy时,s2指向的空间要大于或等于n。

2、使用strncpy或memcpy时,n应该大于strlen(s1),或者说最好n >= strlen(s1)+1;这个1 就是最后的“\0”。

3、使用strncpy时,确保s2的最后一个字符是“\0”。
--------------------- 
版权声明:本文为CSDN博主「三个臭皮匠抵得一个诸葛亮」的原创文章,遵循CC 4.0 by-sa版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/lianghui0811/article/details/76476130

7、实现strcpy和memcpy:

实现strcpy:
#include<assert.h>
char *strcpy(char *des, const char *src)
{
 	          assert((des != NULL) && (src != NULL));//如果值为假,打印输出错误
  	          char *add = des;
  	          while ((*des++ = *src++ ) != ‘\0’) ;
         return add;
}

实现memcpy
void  *memcpy(void *pvTo, const void *pvFrom, size_t size) 
{
	          assert((pvTo != NULL) && (pvFrom != NULL));      // 使用断言
	          byte *pbTo = (byte *) pvTo;         // 防止改变pvTo的地址
 	          byte *pbFrom = (byte *) pvFrom; // 防止改变pvFrom的地址
 	          while(size -- > 0 )
  	          *pbTo = *pbFrom ;
  	         return pvTo;
}

8、链表反转手撕代码?
非递归的方法:
#include
using namespace std;

typedef struct Node{
    int data;
    Node *next;
} Node, *List;

Node * reverseList(List head)
{
     //定义三个指针,保存原来的连接的状态
     //当前结点指针
    Node *pnow = head;
     //当前结点的前驱指针,初始化是 NULL
     Node *pre = NULL;
     //当前结点的后继指针,初始化也是 null
     Node *pnext = NULL;
     //定义尾指针
     Node *tail = NULL;
     //开始遍历链表
     while(pnow != NULL)
      {
             //如果当前结点不是 null,那么初始化 pnext 指针指向当前结点的下一个结点
              pnext = pnow->next;
             //如果找到了尾结点,初始化 tail 指针
             if(NULL == pnext)
              {
				tail = pnow;
              }
  	 	  //进行链表的反转,当前结点的 next 指针指向前一个结点,实现链表方向的反转,此时发生了断链
 		    pnow->next = pre;
  	 	  //勿忘断链的情形,需要使用 pre 指针保存状态,pre 等价于是后移一个结点
 	  	  pre = pnow;
 	  	  //pnow 后移一个结点
 	  	  pnow = pnext;
	 } 

                 return tail;
}

         注意:定义的这个三个指针,目的就是防止断链之后无法继续遍历链表以后的结点,实现全部的反转。当 pnow 的 next 指向 pnow 的前驱pre(初始化是 null)的时候,
      已经实现了 pnow 和前驱pre的方向反转,但是 pnow 此时就和后继pnext断链了,那么使用 pre 后移的方式,指向 pnow,同时 pnow 也后移,指向 pnext,
      而 pnext 继续指向更新之后的 pnow 的 next 结点即可。从而实现了状态的保存,继续遍历全部结点,实现链表反转。
         注意关于链表问题的常见注意点的思考:
        1、如果输入的头结点是 NULL,或者整个链表只有一个结点的时候
        2、链表断裂的考虑

递归方法:
//递归方式
Node * reverseList(List head)
{
//如果链表为空或者链表中只有一个元素
if(head == NULL || head->next == NULL)
{
return head;
}
else
{
//先反转后面的链表,走到链表的末端结点
Node *newhead = reverseList(head->next);
//再将当前节点设置为后面节点的后续节点
head->next->next = head;
head->next = NULL;
return newhead;
}
}

9、判断含括号的表达式是否合法,手撕代码?
答案:#include “stdio.h”
#include “stdlib.h”
#include “linkstack.h”

int isLeft(char c)
{
int ret = 0;

switch(c)
{
case '<':
case '(':
case '[':
case '{':
case '\'':
case '\"':
	ret = 1;
	break;
default:
	ret = 0;
	break;
}

return ret;

}

int isRight(char c)
{
int ret = 0;

switch(c)
{
case '>':
case ')':
case ']':
case '}':
case '\'':
case '\"':
	ret = 1;
	break;
default:
	ret = 0;
	break;
}

return ret;

}

int match(char left, char right)
{
int ret = 0;

switch(left)
{
case '<':
	ret = (right == '>');
	break;
case '(':
	ret = (right == ')');
	break;
case '[':
	ret = (right == ']');
	break;
case '{':
	ret = (right == '}');
	break;
case '\'':
	ret = (right == '\'');
	break;
case '\"':
	ret = (right == '\"');
	break;
default:
	ret = 0;
	break;
}

return ret;

}

int scanner(const char* code)
{
LinkStack* stack = LinkStack_Create();
int ret = 0;
int i = 0;

while( code[i] != '\0' )
{
	if( isLeft(code[i]) )
	{
		LinkStack_Push(stack, (void*)(code + i)); //&code[i]
	}

	if( isRight(code[i]) )
	{
		char* c = (char*)LinkStack_Pop(stack);

		if( (c == NULL) || !match(*c, code[i]) )
		{
			printf("%c does not match!\n", code[i]);
			ret = 0;
			break;
		}
	}

	i++;
}

if( (LinkStack_Size(stack) == 0) && (code[i] == '\0') )
{
	printf("Succeed!\n");
	ret = 1;
}
else
{
	printf("Invalid code!\n");
	ret = 0;
}

LinkStack_Destroy(stack);

return ret;

}

void main()
{
const char* code = "#include <stdio.h> int main() { int a[4][4]; int (*p)[4]; p = a[0]; return 0; ";

scanner(code);
system("pause");
return ;

}

10、map的底层实现是什么?为什么要用红黑树,从插入删除考虑?

11、重载重写覆盖的区别是什么?
重载和重写的区别:

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

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

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

隐藏和重写,重载的区别:
重写就是覆盖

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

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

12、virtual关键字是为了实现什么,具体怎么实现?

为了实现多态性和虚继承(问题:多继承的二义性)

多态性:
当类中声明虚函数时,编译器会在类中生成一个虚函数表
虚函数表是一个存储类成员函数指针的数据结构
虚函数表是由编译器自动生成与维护的
virtual成员函数会被编译器放入虚函数表中
当存在虚函数时,每个对象中都有一个指向虚函数表的指针(C++编译器给父类对象、
  子类对象提前布局vptr指针;当进行howToPrint(Parent *base)函数是,
  C++编译器不需要区分子类对象或者父类对象,只需要再base指针中,找vptr指针即可。)
VPTR一般作为类对象的第一个成员 
虚继承:
虚继承底层实现原理与编译器相关,一般通过虚基类指针和虚基类表实现,
每个虚继承的子类都有一个虚基类指针(占用一个指针的存储空间,4字节)
和虚基类表(不占用类对象的存储空间)(需要强调的是,虚基类依旧会在子类里面存在拷贝,
只是仅仅最多存在一份而已,并不是不在子类里面了);当虚继承的子类被当做父类继承时,
虚基类指针也会被继承。
实际上,vbptr指的是虚基类表指针(virtual base table pointer),
该指针指向了一个虚基类表(virtual table),虚表中记录了虚基类与本类的偏移地址;
通过偏移地址,这样就找到了虚基类成员,而虚继承也不用像普通多继承那样维持着公共基类(虚基类)的
两份同样的拷贝,节省了存储空间。:

13、二叉平衡式插入节点的时间复杂度,具体怎么实现插入的。插入数据是否会导致树的不平衡?

技术二面:主要是简历和算法

1)介绍一下自己?

2)介绍一下你参加过的比赛,你认为最好的一次,你做对了几道,具体问题是什么?

3)介绍一下你编程比赛中华为编程比赛的细节?LSTM算法的具体实现是怎样的?

4)手撕代码,两个链表,找出链表中的相同元素,并把相同元素放到另外一个链表中,
返回重复元素的链表,相同元素算一次;

答案:

5)问了笔试做过题目的思路,为什么会没有AC?

=============================================================================================
C++经典面试题(最全,面中率最高)

【1】new、delete、malloc、free关系
1)delete会调用对象的析构函数,和new对应free只会释放内存,new调用构造函数。
malloc与free是C++/C语言的标准库函数,
new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。
对于非内部数据类型的对象而言,光用maloc/free无法满足动态对象的要求。
对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。
由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,
不能够把执行构造函数和析构函数的任务强加于malloc/free。
因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,
以及一个能完成清理与释放内存工作的运算符delete。
2)注意new/delete不是库函数。
3)new在申请内存时可以给内存赋初值
int *p = new int(10);//分配一段指向int型的空间,并将空间赋初值为10
【2】delete与 delete []区别
delete只会调用一次析构函数,而delete[]会调用每一个成员的析构函数。
在More Effective C++中有更为详细的解释:“当delete操作符用于数组时,
它为每个数组元素调用析构函数,然后调用operator delete来释放内存。
”delete与new配套,delete []与new []配套

MemTest *mTest1=new MemTest[10];

MemTest *mTest2=new MemTest;

Int *pInt1=new int [10];

Int *pInt2=new int;

delete[]pInt1; //-1-

delete[]pInt2; //-2-

delete[]mTest1;//-3-

delete[]mTest2;//-4-

在-4-处报错。

这就说明:对于内建简单数据类型,delete和delete[]功能是相同的。对于自定义的复杂数据类型,
delete和delete[]不能互用。delete[]删除一个数组,delete删除一个指针。
简单来说,用new分配的内存用delete删除;用new[]分配的内存用delete[]删除。
delete[]会调用数组元素的析构函数。内部数据类型没有析构函数,所以问题不大。
如果你在用delete时没用括号,delete就会认为指向的是单个对象,否则,它就会认为指向的是一个数组。

3.C++有哪些性质(面向对象特点)
封装,继承和多态。
C++ 三大特性 封装,继承,多态

封装

定义:封装就是将抽象得到的数据和行为相结合,形成一个有机的整体,也就是将数据与操作数据的源代码进行有机的结合,形成类,其中数据和函数都是类的成员,目的在于将对象的使用者和设计者分开,

以提高软件的可维护性和可修改性

特性:1. 结合性,即是将属性和方法结合 2. 信息隐蔽性,利用接口机制隐蔽内部实现细节,只留下接口给外界调用 3. 实现代码重用

继承

定义:继承就是新类从已有类那里得到已有的特性。 类的派生指的是从已有类产生新类的过程。原有的类成为基类或父类,产生的新类称为派生类或子类,

子类继承基类后,可以创建子类对象来调用基类函数,变量等

单一继承:继承一个父类,这种继承称为单一继承,一般情况尽量使用单一继承,使用多重继承容易造成混乱易出问题

多重继承:继承多个父类,类与类之间要用逗号隔开,类名之前要有继承权限,假使两个或两个基类都有某变量或函数,在子类中调用时需要加类名限定符如c.a::i = 1;

菱形继承:多重继承掺杂隔代继承1-n-1模式,此时需要用到虚继承,例如 B,C虚拟继承于A,D再多重继承B,C,否则会出错

继承权限:继承方式规定了如何访问继承的基类的成员。继承方式指定了派生类成员以及类外对象对于从基类继承来的成员的访问权限

继承权限:子类继承基类除构造和析构函数以外的所有成员

继承可以扩展已存在的代码,目的也是为了代码重用

继承也分为接口继承和实现继承:

普通成员函数的接口总是会被继承:  子类继承一份接口和一份强制实现

普通虚函数被子类重写     :  子类继承一份接口和一份缺省实现

纯虚函数只能被子类继承接口  :  子类继承一份接口,没有继承实现

访问权限图如下:

为了便于理解,伪代码如下,注意这个例子编译是不过的,仅是为了可以更简洁的说明继承权限的作用:

class Animal //父类

{

public:

      void eat(){

            cout<<"animal eat"<<endl;

}

protected:

      void sleep(){    

            cout<<"animal sleep"<<endl;

}

private:

void breathe(){

            cout<<"animal breathe"<<endl;

}

};
    class Fish:public Animal //子类

{

public:

      void test() {

            eat();       //此时eat()的访问权限为public,在类内部能够访问

            sleep();     //此时sleep()的访问权限为protected,在类内部能够访问

            breathe();   //此时breathe()的访问权限为no access,在类内部不能够访问

}

};

int main(void) {

Fish f;
     f.eat(); //此时eat()的访问权限为public,在类外部能够访问

f.sleep(); //此时sleep()的访问权限为protected,在类外部不能够访问

f.breathe() //此时breathe()的访问权限为no access,在类外部不能够访问

}

多态

定义:可以简单概括为“一个接口,多种方法”,即用的是同一个接口,但是效果各不相同,多态有两种形式的多态,一种是静态多态,一种是动态多态

动态多态: 是指在程序运行时才能确定函数和实现的链接,此时才能确定调用哪个函数,父类指针或者引用能够指向子类对象,调用子类的函数,所以在编译时是无法确定调用哪个函数

使用时在父类中写一个虚函数,在子类中分别重写,用这个父类指针调用这个虚函数,它实际上会调用各自子类重写的虚函数。

运行期多态的设计思想要归结到类继承体系的设计上去。对于有相关功能的对象集合,我们总希望能够抽象出它们共有的功能集合,在基类中将这些功能声明为虚接口(虚函数),

然后由子类继承基类去重写这些虚接口,以实现子类特有的具体功能。

运行期多态的实现依赖于虚函数机制。当某个类声明了虚函数时,编译器将为该类对象安插一个虚函数表指针,并为该类设置一张唯一的虚函数表,虚函数表中存放的是该类虚函数地址。

运行期间通过虚函数表指针与虚函数表去确定该类虚函数的真正实现。

优点: OO设计重要的特性,对客观世界直觉认识; 能够处理同一个继承体系下的异质类集合

vector<Animal*>anims;

Animal * anim1 = new Dog;

Animal * anim2 = new Cat;

//处理异质类集合

anims.push_back(anim1);

anims.push_back(anim2);

缺点:运行期间进行虚函数绑定,提高了程序运行开销;庞大的类继承层次,对接口的修改易影响类继承层次;由于虚函数在运行期才绑定,所以编译器无法对虚函数进行优化

虚函数

定义:用virtual关键字修饰的函数,本质:由虚指针和虚表控制,虚指针指向虚表中的某个函数入口地址,就实现了多态,作用:实现了多态,虚函数可以被子类重写,虚函数地址存储在虚表中

虚表:虚表中主要是一个类的虚函数的地址表,这张表解决了继承,覆盖的问题,
保证其真实反应实际的函数,当我们用父类指针来指向一个子类对象的时候,虚表指明了实际所应调用的函数

基类有一个虚表,可以被子类继承,(当类中有虚函数时该类才会有虚表,该类的对象才有虚指针,子类继承时也会继承基类的虚表),子类如果重写了基类的某虚函数,那么子类继承于基类的虚表中该虚函数的地址也会相应改变,指向子类

自身的该虚函数实现,如果子类有自己的虚函数,那么子类的虚表中就会增加该项,编译器为每个类对象定义了一个虚指针,来定位虚表,所以虽然是父类指针指向子类对象,但因为此时子类

重写了该虚函数,该虚函数地址在子类虚表中的地址已经被改变了,所以它实际调用的是子类的重写后的函数,正是由于每个对象调用的虚函数都是通过虚表指针来索引的,也就决定了虚表指针的

正确初始化是非常重要的,即是说,在虚表指针没有正确初始化之前,我们是不能调用虚函数的,因为生成一个对象是构造函数的工作,所以设置虚指针也是构造函数的工作,编译器在构造函数

的开头部分秘密插入能初始化虚指针的代码, 在构造函数中进行虚表的创建和虚指针的初始化

一但虚指针被初始化为指向相应的虚表,对象就“知道”它自己是什么类型,但只有当虚函数被调用时这种自我认知才有用

类中若没有虚函数,类对象的大小正好是数据成员的大小,包含有一个或者多个虚函数的类对象。编译器会向里面插入一个虚指针,指向虚表,这些都是编译器为我们做的,我们完全不必关心

这些,所有有虚函数的类对象的大小是数据成员的大小加一个虚指针的大小;对于虚继承,若子类也有自己的虚函数,则它本身需要有一个虚指针,指向自己的虚表,另外子类继承基类时,

首先要通过加入一个虚指针来指向基类,因此可能会有两个或多个虚指针(多重继承会多个),其他情况一般是一个虚指针,一张虚表

每一个带有virtual函数的类都有一个相应的虚表,当对象调用某一virtual函数时,实际被调用的函数取决于该对象的虚指针所指向的那个虚表-编译器在其中寻找适当的函数指针。

效率漏洞:我们必须明白,编译器正在插入隐藏代码到我们的构造函数中,这些隐藏代码不仅必须初始化虚指针,而且还必须检查this的值(以免operator new返回零)和调用基类构造函数。放在一起,

这些代码可以影响我们认为是一个小内联函数的调用,特别是,构造函数的规模会抵消函数调用代价的减少,如果做大量的内联函数调用,代码长度就会增长,而在速度上没有任何好处,

当然,也许不会立即把所有这些小构造函数都变成非内联,因为它们更容易写为内联构造函数,但是,当我们正在调整我们的代码时,请务必去掉这些内联构造函数

虚函数使用:将函数声明为虚函数会降低效率,一般函数在编译期其相对地址是确定的,编译器可以直接生成imp/invoke指令,如果是虚函数,那么函数的地址是动态的,譬如取到的地址在eax寄存

器里,则在call eax之后的那些已经被预取到流水线的所有指令都将失效, 流水线越长,那么一次分支预测失败的代价越大,建议若不打算让某类成为基类,那么类中最好不要出现虚函数,

纯虚函数:含有至少一个纯虚函数的类叫抽象类,因为抽象类含有纯虚函数,所以其虚表是不健全的,在虚表不健全的情况下是不能实例化对象的,子类继承抽象基类后必须重写基类的所有纯虚函数

否则子类仍为纯虚函数子类将抽象基类的纯虚函数全部重写后会将虚表完善,此时子类才能实例化对象,纯虚函数只声明不定义,形如 virtual void print() = 0

静态多态:是在编译期就把函数链接起来,此时即可确定调用哪个函数或模板,静态多态是由模板和重载实现的,在宏多态中,是通过定义变量,编译时直接把变量替换,实现宏多态

优点: 带来了泛型编程的概念,使得C++拥有泛型编程与STL这样的武器; 在编译期完成多态,提高运行期效率; 具有很强的适配性和松耦合性,(耦合性指的是两个功能模块之间的依赖关系)

缺点: 程序可读性降低,代码调试带来困难;无法实现模板的分离编译,当工程很大时,编译时间不可小觑 ;无法处理异质对象集合

调用基类指针创建子类对象,那么基类应该有虚析构函数,因为如果基类没有虚析构函数,那么在删除这个子类对象的时候会调用错误的析构函数而导致删除失败产生不明确行为,

int main() {

Base *p = new Derive();    //调用基类指针创建子类对象,那么基类应有虚析构函数,不然当删除的时候会调用错误的析构函数而导致删除失败产生不明确行为,

delete p;            //删除子类对象时,如果基类有虚析构函数,那么delete时会先调用子类的析构函数,然后再调用基类的析构函数,成功删除

return 0;            //如果基类没有虚析构函数,那么就只会调用父类的析构函数,只删除了对象内的父类部分,造成一个局部销毁,可能导致资源泄露

}                  //注:只有当此类希望成为 基类时才会打算声明一个虚析构函数,否则不必要给此类声明一个虚函数
4.子类析构时要调用父类的析构函数吗?
析构函数调用的次序是先派生类的析构后基类的析构,
也就是说在基类的的析构调用的时候,派生类的信息已经全部销毁了。
定义一个对象时先调用基类的构造函数、
然后调用派生类的构造函数;
析构的时候恰好相反:先调用派生类的析构函数、然后调用基类的析构函数。

5.多态,虚函数,纯虚函数
多态:是对于不同对象接收相同消息时产生不同的动作。
C++的多态性具体体现在运行和编译两个方面:
在程序运行时的多态性通过继承和虚函数来体现;

在程序编译时多态性体现在函数和运算符的重载上;

虚函数:在基类中冠以关键字 virtual 的成员函数。
它提供了一种接口界面。允许在派生类中对基类的虚函数重新定义。

纯虚函数的作用:在基类中为其派生类保留一个函数的名字,以便派生类根据需要对它进行定义。
作为接口而存在 纯虚函数不具备函数的功能,一般不能直接被调用。

从基类继承来的纯虚函数,在派生类中仍是虚函数。如果一个类中至少有一个纯虚函数,那么这个类被称为抽象类(abstract class)。

抽象类中不仅包括纯虚函数,也可包括虚函数。抽象类必须用作派生其他类的基类,
而不能用于直接创建对象实例。但仍可使用指向抽象类的指针支持运行时多态性。

6.求下面函数的返回值(微软)
int func(x)

{

int countx = 0; 

while(x) 

{ 

	countx ++; 

	x = x&(x-1); 

} 

return countx; 

}

假定x = 9999。
答案:8

思路:将x转化为2进制,看含有的1的个数。

7.什么是“引用”?申明和使用“引用”要注意哪些问题?
答:引用就是某个目标变量的“别名”(alias),对应用的操作与对变量直接操作效果完全相同。
申明一个引用的时候,切记要对其进行初始化。引用声明完毕后,相当于目标变量名有两个名称,
即该目标原名称和引用名,不能再把该引用名作为其他变量名的别名。声明一个引用,不是新定义了一个变量,
它只表示该引用名是目标变量名的一个别名,它本身不是一种数据类型,因此引用本身不占存储单元,
系统也不给引用分配存储单元。不能建立数组的引用。

8.将“引用”作为函数参数有哪些特点?
(1)传递引用给函数与传递指针的效果是一样的。这时,
被调函数的形参就成为原来主调函数中的实参变量或对象的一个别名来使用,
所以在被调函数中对形参变量的操作就是对其相应的目标对象(在主调函数中)的操作。

(2)使用引用传递函数的参数,在内存中并没有产生实参的副本,它是直接对实参操作;
而使用一般变量传递函数的参数,当发生函数调用时,需要给形参分配存储单元,
形参变量是实参变量的副本;如果传递的是对象,还将调用拷贝构造函数。
因此,当参数传递的数据较大时,用引用比用一般变量传递参数的效率和所占空间都好。

(3)使用指针作为函数的参数虽然也能达到与使用引用的效果,
但是,在被调函数中同样要给形参分配存储单元,且需要重复使用"*指针变量名"的形式进行运算,
这很容易产生错误且程序的阅读性较差;另一方面,在主调函数的调用点处,必须用变量的地址作为实参。
而引用更容易使用,更清晰。

9.在什么时候需要使用“常引用”? 
如果既要利用引用提高程序的效率,又要保护传递给函数的数据不在函数中被改变,就应使用常引用。
常引用声明方式:const 类型标识符 &引用名=目标变量名;

例1

int a ;

const int &ra=a;

ra=1; //错误

a=1; //正确

例2

string foo( );

void bar(string & s);

那么下面的表达式将是非法的:

bar(foo( ));

bar(“hello world”);

原因在于foo( )和"hello world"串都会产生一个临时对象,而在C++中,这些临时对象都是const类型的。
因此上面的表达式就是试图将一个const类型的对象转换为非const类型,这是非法的。
引用型参数应该在能被定义为const的情况下,尽量定义为const 。

10.将“引用”作为函数返回值类型的格式、好处和需要遵守的规则?

格式:类型标识符 &函数名(形参列表及类型说明){ //函数体 }

好处:在内存中不产生被返回值的副本;
注意:正是因为这点原因,所以返回一个局部变量的引用是不可取的。
因为随着该局部变量生存期的结束,相应的引用也会失效,产生runtime error!

注意事项:

(1)不能返回局部变量的引用。这条可以参照Effective C++[1]的Item 31。
主要原因是局部变量会在函数返回后被销毁,因此被返回的引用就成为了"无所指"的引用,
程序会进入未知状态。

(2)不能返回函数内部new分配的内存的引用。这条可以参照Effective C++[1]的Item 31。
虽然不存在局部变量的被动销毁问题,可对于这种情况(返回函数内部new分配内存的引用),
又面临其它尴尬局面。例如,被函数返回的引用只是作为一个临时变量出现,
而没有被赋予一个实际的变量,那么这个引用所指向的空间(由new分配)就无法释放,造成memory leak。

(3)可以返回类成员的引用,但最好是const。这条原则可以参照Effective C++[1]的Item 30。
主要原因是当对象的属性是与某种业务规则(business rule)相关联的时候,
其赋值常常与某些其它属性或者对象的状态有关,因此有必要将赋值操作封装在一个业务规则当中。
如果其它对象可以获得该属性的非常量引用(或指针),
那么对该属性的单纯赋值就会破坏业务规则的完整性。

(4)流操作符重载返回值申明为“引用”的作用:

流操作符<<和>>,这两个操作符常常希望被连续使用,例如:cout << "hello" << endl; 
因此这两个操作符的返回值应该是一个仍然支持这两个操作符的流引用。
可选的其它方案包括:返回一个流对象和返回一个流对象指针。但是对于返回一个流对象,
程序必须重新(拷贝)构造一个新的流对象,也就是说,连续的两个<<操作符实际上是针对不同对象的!
这无法让人接受。对于返回一个流指针则不能连续使用<<操作符(原因:参数是operator<<(ostream &out,A a))。因此,返回一个流对象引用是惟一选择。
这个唯一选择很关键,它说明了引用的重要性以及无可替代性,
也许这就是C++语言中引入引用这个概念的原因吧。 
赋值操作符=。这个操作符象流操作符一样,是可以连续使用的,
例如:x = j = 10;或者(x=10)=100;赋值操作符的返回值必须是一个左值,以便可以被继续赋值。
因此引用成了这个操作符的惟一返回值选择。

#include<iostream.h>

int &put(int n);

int vals[10];

int error=-1;

void main()

{

put(0)=10; //以put(0)函数值作为左值,等价于vals[0]=10; 

put(9)=20; //以put(9)函数值作为左值,等价于vals[9]=20; 

cout<<vals[0]; 

cout<<vals[9];

} 

int &put(int n)

{

if (n>=0 && n<=9 ) return vals[n]; 

else { cout<<"subscript error"; return error; }

}

(5)在另外的一些操作符中,却千万不能返回引用:+-*/ 四则运算符。
	 它们不能返回引用,Effective C++[1]的Item23详细的讨论了这个问题。
	 主要原因是这四个操作符没有side effect,因此,它们必须构造一个对象作为返回值,
	 可选的方案包括:返回一个对象、返回一个局部变量的引用,
	 返回一个new分配的对象的引用、返回一个静态对象引用。根据前面提到的引用作为返回值的三个规则,
	 2、3两个方案都被否决了。静态对象的引用又因为((a+b) == (c+d))会永远为true而导致错误。
	 所以可选的只剩下返回一个对象了。

11、结构与联合有和区别?

(1). 结构和联合都是由多个不同的数据类型成员组成,
但在任何同一时刻, 联合中只存放了一个被选中的成员(所有成员共用一块地址空间),
而结构的所有成员都存在(不同成员的存放地址不同)。

(2). 对于联合的不同成员赋值, 将会对其它成员重写, 原来成员的值就不存在了,
而对于结构的不同成员赋值是互不影响的。

12、试写出程序结果:
int a=4;

int &f(int x)

{ a=a+x;

  return  a;

}

int main(void)

{ int t=5;

 cout<<f(t)<<endl;  a = 9

f(t)=20;             a = 20

cout<<f(t)<<endl;     t = 5,a = 20  a = 25

 t=f(t);                a = 30 t = 30

cout<<f(t)<<endl;  }    t = 60

}

13.重载(overload)和重写(overried,有的书也叫做“覆盖”)的区别?
常考的题目。从定义上来说:

重载:是指允许存在多个同名函数,而这些函数的参数表不同(或许参数个数不同,或许参数类型不同,或许两者都不同)。

重写:是指子类重新定义父类虚函数的方法。

从实现原理上来说:

重载:编译器根据函数不同的参数表,对同名函数的名称做修饰,
然后这些同名函数就成了不同的函数(至少对于编译器来说是这样的)。
如,有两个同名函数:function func(p:integer):integer;和function func(p:string):integer;。
那么编译器做过修饰后的函数名称可能是这样的:int_func、str_func。对于这两个函数的调用,
在编译器间就已经确定了,是静态的。也就是说,它们的地址在编译期就绑定了(早绑定),
因此,重载和多态无关!

重写:和多态真正相关。当子类重新定义了父类的虚函数后,父类指针根据赋给它的不同的子类指针,
动态的调用属于子类的该函数,这样的函数调用在编译期间是无法确定的
(调用的子类的虚函数的地址无法给出)。
因此,这样的函数地址是在运行期绑定的(晚绑定)。

14.有哪几种情况只能用intialization list 而不能用assignment?
答案:当类中含有const、reference 成员变量;基类的构造函数都需要初始化表。

  1. C++是不是类型安全的?
    答案:不是。两个不同类型的指针之间可以强制转换(用reinterpret cast)。C#是类型安全的。

  2. main 函数执行以前,还会执行什么代码?
    答案:
    全局对象的构造函数会在main 函数之前执行。
    main结束 不代表整个进程结束
    (1)全局对象的构造函数会在main 函数之前执行,
    全局对象的析构函数会在main函数之后执行;
    用atexit注册的函数也会在main之后执行。
    (2)一些全局变量、对象和静态变量、对象的空间分配和赋初值就是在执行main函数之前,
    而main函数执行完后,还要去执行一些诸如释放空间、释放资源使用权等操作
    (3)进程启动后,要执行一些初始化代码(如设置环境变量等),然后跳转到main执行。
    全局对象的构造也在main之前。
    atexit 函数是标准 C 新增的。它“注册”一个函数,使这个函数将在 exit 函数被调用时或者当 mian 函数返回时被调用。当程序异常终止时(例如调用 abort 或 raise),通过它注册的函数并不会被调用。编译器必须至少允许程序员注册32个函数。如果注册成功,atexit 返回0,否则返回非零值。没有办法取消一个函数的注册。在 exit 所执行的任何标准清理操作之前,被注册的函数按照与注册顺序相反的顺序被依次调用。每个被调用的函数不接受任何参数,并且返回类型是 void。被注册的函数不应该试图引用任何存储类别为 auto 或 register 的对象(例如通过指针),除非是它自己所定义的。多次注册同一个函数将导致这个函数被多次调用。有些传统 C 编译器用 onexit 这个名称实现了像是的功能。
    atexit是注册后进先出的函数,和函数入栈出栈是一样的。
    在这里注册了四个函数,理解为入栈的顺序为fn1() -> fn2() -> fn3() -> fn4();出栈的顺序正好相反,而什么时候出栈呢?就是在调用函数结束时,准确的说应该是函数调用的最后的操作就是出栈过程。main()同样也是一个函数,在结束时,按出栈的顺序调用四个函数,即为fn4() -> fn3() -> fn2() -> fn1();
    注册这个函数的目的就是为了在函数退出时调用的,即使是main()函数也是这样的。可以在这些函数中加入一些清理工作,比如内存释放等等。

  3. 描述内存分配方式以及它们的区别?
    1) 从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。
    例如全局变量,static 变量。

2) 在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,
函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集。

3) 从堆上分配,亦称动态内存分配。程序在运行的时候用malloc 或new 申请任意多少的内存,
程序员自己负责在何时用free 或delete 释放内存。动态内存的生存期由程序员决定,
使用非常灵活,但问题也最多。

18.分别写出BOOL,int,float,指针类型的变量a 与“零”的比较语句。
答案:

BOOL : if ( !a ) or if(a)

int : if ( a == 0)

float : const EXPRESSION EXP = 0.000001

if ( a < EXP && a >-EXP)

pointer : if ( a != NULL) or if(a == NULL)

19.请说出const与#define 相比,有何优点?
答案:

const作用:定义常量、修饰函数参数、修饰函数返回值三个作用。被Const修饰的东西都受到强制保护,
可以预防意外的变动,能提高程序的健壮性。

1) const 常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。
而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误。

2) 有些集成化的调试工具可以对const 常量进行调试,但是不能对宏常量进行调试。

20.简述数组与指针的区别?
数组要么在静态存储区被创建(如全局数组),要么在栈上被创建。
指针可以随时指向任意类型的内存块。

(1)修改内容上的差别

char a[] = “hello”;

a[0] = ‘X’;

char *p = “world”; // 注意p 指向常量字符串

p[0] = ‘X’; // 编译器不能发现该错误,运行时错误

(2) 用运算符sizeof 可以计算出数组的容量(字节数)。
sizeof§,p 为指针得到的是一个指针变量的字节数,而不是p 所指的内存容量。
C++/C 语言没有办法知道指针所指的内存容量,除非在申请内存时记住它。
注意当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针。

char a[] = “hello world”;

char *p = a;

cout<< sizeof(a) << endl; // 12 字节

cout<< sizeof§ << endl; // 4 字节

计算数组和指针的内存容量

void Func(char a[100])

{

cout<< sizeof(a) << endl; // 4 字节而不是100 字节

}

21题: int (*s[10])(int) 表示的是什么?
int (*s[10])(int) 函数指针数组,每个指针指向一个int func(int param)的函数。

22题:栈内存与文字常量区

char str1[] = "abc";

char str2[] = “abc”;

const char str3[] = “abc”;
  const char str4[] = “abc”;

const char *str5 = “abc”;
  const char *str6 = “abc”;

char *str7 = “abc”;
  char *str8 = “abc”;

cout << ( str1 == str2 ) << endl;//0 分别指向各自的栈内存
  cout << ( str3 == str4 ) << endl;//0 分别指向各自的栈内存
  cout << ( str5 == str6 ) << endl;//1指向文字常量区地址相同

cout << ( str7 == str8 ) << endl;//1指向文字常量区地址相同

结果是:0 0 1 1

解答:str1,str2,str3,str4是数组变量,它们有各自的内存空间;
而str5,str6,str7,str8是指针,它们指向相同的常量区域。

23题:将程序跳转到指定内存地址
要对绝对地址0x100000赋值,我们可以用(unsigned int*)0x100000 = 1234;
那么要是想让程序跳转到绝对地址是0x100000去执行,应该怎么做?

((void ()( ))0x100000 ) ( );
  首先要将0x100000强制转换成函数指针,即:
  (void ()())0x100000
  然后再调用它:
  ((void ()())0x100000)();
  用typedef可以看得更直观些:
  typedef void(
)() voidFuncPtr;
  *((voidFuncPtr)0x100000)();

24题:int id[sizeof(unsigned long)];这个对吗?为什么?

答案:正确 这个 sizeof是编译时运算符,编译时就确定了 ,可以看成和机器有关的常量。

25题:引用与指针有什么区别?

【参考答案】

  1. 引用必须被初始化,指针不必。

  2. 引用初始化以后不能被改变,指针可以改变所指的对象。

  3. 不存在指向空值的引用,但是存在指向空值的指针。

26题:const 与 #define 的比较 ,const有什么优点?

【参考答案】

(1) const 常量有数据类型,而宏常量没有数据类型。
编译器可以对前者进行类型安全检查。而对后者只进行字符替换,没有类型安全检查,
并且在字符替换可能会产生意料不到的错误(边际效应) 。

(2) 有些集成化的调试工具可以对 const 常量进行调试,但是不能对宏常量进行调试。

27题:复杂声明

void * ( * (*fp1)(int))[10];

float (( fp2)(int,int,int))(int);

int (* ( * fp3)())10;

分别表示什么意思?
【标准答案】

1.void * ( * (fp1)(int))[10];
fp1是一个指针,指向一个函数,这个函数的参数为int型,
函数的返回值是一个指针,这个指针指向一个数组,这个数组有10个元素,
每个元素是一个void
型指针。

2.float (( fp2)(int,int,int))(int);
fp2是一个指针,指向一个函数,这个函数的参数为3个int型,函数的返回值是一个指针,
这个指针指向一个函数,这个函数的参数为int型,函数的返回值是float型。

3.int (* ( * fp3)())10;
fp3是一个指针,指向一个函数,这个函数的参数为空,函数的返回值是一个指针,
这个指针指向一个数组,这个数组有10个元素,每个元素是一个指针,指向一个函数,
这个函数的参数为空,函数的返回值是int型。

28题:内存的分配方式有几种?
【参考答案】

一、从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量。

二、在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。

三、从堆上分配,亦称动态内存分配。程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多。

29题:基类的析构函数不是虚函数,会带来什么问题?
【参考答案】派生类的析构函数用不上,会造成资源的泄漏。

30题:全局变量和局部变量有什么区别?是怎么实现的?操作系统和编译器是怎么知道的?
【参考答案】

生命周期不同:
全局变量随主程序创建和创建,随主程序销毁而销毁;
局部变量在局部函数内部,甚至局部循环体等内部存在,退出就不存在;

使用方式不同:
通过声明后全局变量程序的各个部分都可以用到;局部变量只能在局部使用;分配在栈区。

操作系统和编译器通过内存分配的位置来知道的,
全局变量分配在全局数据段并且在程序开始运行的时候被加载。
局部变量则分配在堆栈里面 。
【3】简单说一下new 跟 malloc的差别
(1)new 返回指定类型的指针,并且可以自动计算所需要大小。
(2)而 malloc 则必须要由我们计算字节数,并且在返回后强行转换为实际类型的指针
(3)malloc 只管分配内存,并不能对所得的内存进行初始化,所以得到的一片新内存中,其值将是随机的。
除了分配及最后释放的方法不一样以外,通过malloc或new得到指针,在其它操作上保持一致。
【4】有了malloc/free为什么还要new/delete?
malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。
它们都可用于申请动态内存和释放内存。
对于非内部数据类型的对象而言,光用maloc/free无法满足动态对象的要求。
对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。
由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,
不能够把执行构造函数和析构函数的任务强加于malloc/free。
因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,
以及一个能完成清理与释放内存工作的运算符delete。注意new/delete不是库函数。
我们不要企图用malloc/free来完成动态对象的内存管理,应该用new/delete。
由于内部数据类型的“对象”没有构造与析构的过程,对它们而言malloc/free和new/delete是等价的。
既然new/delete的功能完全覆盖了malloc/free,为什么C++不把malloc/free淘汰出局呢?
这是因为C++程序经常要调用C函数,而C程序只能用malloc/free管理动态内存。
如果用free释放“new创建的动态对象”,那么该对象因无法执行析构函数而可能导致程序出错。
如果用delete释放“malloc申请的动态内存”,结果也会导致程序出错,但是该程序的可读性很差。
所以new/delete必须配对使用,malloc/free也一样。

【4】简单说一下TCP的三次握手,并说说三次握手做了什么事情,每次通讯包发了什么?

序列号seq:占4个字节,用来标记数据段的顺序,TCP把连接中发送的所有数据字节都编上一个序号,
第一个字节的编号由本地随机产生;给字节编上序号后,就给每一个报文段指派一个序号;
序列号seq就是这个报文段中的第一个字节的数据编号。
------------------------------------------

确认号ack:占4个字节,期待收到对方下一个报文段的第一个数据字节的序号;
序列号表示报文段携带数据的第一个字节的编号;而确认号指的是期望接收到下一个字节的编号;
因此当前报文段最后一个字节的编号+1即为确认号。
--------------------------------------------
    确认ACK:占1位,仅当ACK=1时,确认号字段才有效。ACK=0时,确认号无效
--------------------------------------------
    同步SYN:连接建立时用于同步序号。当SYN=1,ACK=0时表示:这是一个连接请求报文段。
若同意连接,则在响应报文段中使得SYN=1,ACK=1。因此,SYN=1表示这是一个连接请求,或连接接受报文。
SYN这个标志位只有在TCP建产连接时才会被置1,握手完成后SYN标志位被置0。
--------------------------------------------
    终止FIN:用来释放一个连接。FIN=1表示:此报文段的发送方的数据已经发送完毕,并要求释放运输连接

PS:ACK、SYN和FIN这些大写的单词表示标志位,其值要么是1,要么是0;ack、seq小写的单词表示序号。

版权声明:本文为CSDN博主「青柚_」的原创文章,遵循CC 4.0 by-sa版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_38950316/article/details/81087809
【5】为什么连接的时候是三次握手,关闭的时候却是四次握手?

答:因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。
其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,
很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,"你发的FIN报文我收到了"。
只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。
客户端TCP状态迁移:        
CLOSED->SYN_SENT->ESTABLISHED->FIN_WAIT_1->FIN_WAIT_2->TIME_WAIT->CLOSED

服务器TCP状态迁移:      
CLOSED->LISTEN->SYN收到->ESTABLISHED->CLOSE_WAIT->LAST_ACK->CLOSED

【6】为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态?

答:
(1)虽然按道理,四个报文都发送完毕,我们可以直接进入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连接。
(2)防止“已失效的连接请求报文段”出现在本连接中:A在发送完最后一个ACK报文段后,再经过2MSL,
	 就可以使本连接持续的时间内所产生的所有报文段都从网络中消失,
	 使下一个新的连接中不会出现这种旧的连接请求报文段

【7】为什么不能用两次握手进行连接?

答:
(1)3次握手完成两个重要的功能,既要双方做好发送数据的准备工作(双方都知道彼此已准备好),
	 也要允许双方就初始序列号进行协商,这个序列号在握手过程中被发送和确认。
	 现在把三次握手改成仅需要两次握手,死锁是可能发生的。
	 作为例子,考虑计算机S和C之间的通信,假定C给S发送一个连接请求分组,S收到了这个分组,
	 并发送了确认应答分组。按照两次握手的协定,S认为连接已经成功地建立了,可以开始发送数据分组。
	 可是,C在S的应答分组在传输中被丢失的情况下,将不知道S 是否已准备好,不知道S建立什么样的序列号,
	 C甚至怀疑S是否收到自己的连接请求分组。在这种情况下,C认为连接还未建立成功,
	 将忽略S发来的任何数据分组,只等待连接确认应答分组。而S在发出的分组超时后,重复发送同样的分组。
	 这样就形成了死锁。
(2)主要为了防止已失效的连接请求报文段突然又传送到了B,因而产生错误。如A发出连接请求,
	 但因连接请求报文丢失而未收到确认,于是A再重传一次连接请求。后来收到了确认,建立了连接。
	 数据传输完毕后,就释放了连接,A工发出了两个连接请求报文段,其中第一个丢失,第二个到达了B,
	 但是第一个丢失的报文段只是在某些网络结点长时间滞留了,延误到连接释放以后的某个时间才到达B,
	 此时B误认为A又发出一次新的连接请求,于是就向A发出确认报文段,同意建立连接,不采用三次握手,
	 只要B发出确认,就建立新的连接了,此时A不理睬B的确认且不发送数据,则B一致等待A发送数据,
	 浪费资源

【8】如果已经建立了连接,但是客户端突然出现故障了怎么办?

TCP还设有一个保活计时器,显然,客户端如果出现故障,服务器不能一直等下去,白白浪费资源。
服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为2小时,
若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔75秒钟发送一次。
若一连发送10个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接。

【9】Server端易受到SYN攻击?

服务器端的资源分配是在二次握手时分配的,而客户端的资源是在完成三次握手时分配的,
所以服务器容易受到SYN洪泛攻击,SYN攻击就是Client在短时间内伪造大量不存在的IP地址,
并向Server不断地发送SYN包,Server则回复确认包,并等待Client确认,由于源地址不存在,
因此Server需要不断重发直至超时,这些伪造的SYN包将长时间占用未连接队列,
导致正常的SYN请求因为队列满而被丢弃,从而引起网络拥塞甚至系统瘫痪。

防范SYN攻击措施:降低主机的等待时间使主机尽快的释放半连接的占用,
短时间受到某IP的重复SYN则丢弃后续请求
【10】TCP连接时系统会分配什么资源?
(1)一个tcp连接需要:1,socket文件描述符;2,IP地址;3,端口;4,内存

(2)TCP连接的四元组:源IP 源端口 目标IP 目标端口,这四元组构成了一个唯一的tcp连接。

(3)对于一台服务器,我们假设只有一个网卡,那么就对应一个唯一的IP地址,而监听端口,
	 我们可以在1024-65535之间任选一个。通过这个监听端口,我们接收来自客户端的连接请求。
	 那么,它的IP、端口已经确定了,下面就是讨论socket文件描述符合内存了。

(4)对于文件描述符fd,每个tcp连接占用一个,那么一个文件描述符下的文件大约占1K字节,
	 而内核对这块也有说明,文件描述符建议最多占用10%的内存,如果是8G内存,
	 那么就相当于800M即80000,80万个文件描述符,当然,这个数据也可以通过linux参数调优进行调节,
	 我在之前的一篇章节中也有讨论到,请大家参考:http://blog.csdn.net/fox_hacker/article/details/41148115
(5)而对于内存,tcp连接归根结底需要双方接收和发送数据,那么就需要一个读缓冲区和写缓冲区,
	 这两个buffer在linux下最小为4096字节,可通过cat /proc/sys/net/ipv4/tcp_rmem
	 和cat /proc/sys/net/ipv4/tcp_wmem来查看。所以,一个tcp连接最小占用内存为4096+4096 = 8k,
	 那么对于一个8G内存的机器,在不考虑其他限制下,最多支持的并发量为:810241024/8 约等于100万。
	 此数字为纯理论上限数值,在实际中,由于linux kernel对一些资源的限制,加上程序的业务处理,
	 所以,8G内存是很难达到100万连接的,当然,我们也可以通过增加内存的方式增加并发量。
	 网上也有人做过相关试验,程序接收1024000个连接,共消耗7,5G内存,即每个连接消耗在8K左右。
--------------------- 

原文链接:https://blog.csdn.net/qq_38950316/article/details/81087809
【11】linux对文件描述符的种种限制
最近在研究linux服务器下TCP的最大连接数问题,因为系统为每个TCP连接都要创建一个socket句柄,而每个socket句柄同时也是一个文件句柄,所以就专门对linux下文件描述符的种种限制作了一些深入的研究:

(1)ulimit  -n 65556

	ulimit -n是用来查看用户单一进程可同时打开的最大文件数,默认情况下是1024,我们通过ulimit -n 65536,将最大文件数修改为65536,此修改只对当前会话有效。

	当然,ulimit还有其他的用法,比如ulimit -a就能够显示系统资源的各种限制,这里就不再继续讲解,有兴趣的童鞋可以继续深入挖掘下~

(2)/etc/security/limits.conf

	看其所在文件夹和文件名字我们也可以猜到,这个是为了linux的性能、安全而设计的一些限制,那么如果我们要修改最大可打开的文件句柄数,我们可以在limits.conf在增加如下代码:

	* soft nofile 65536

	* hard nofile 65536

这里面会有几个概念,比如nofile参数、软限制与硬限制等等,大家也可以自己继续挖掘下。

(3)cat proc/sys/fs/file-max:
	 内核参数:linux内核将分配的最大的文件句柄数,所有进程一共可以打开的文件数量。
	 还有一个是:cat /proc/sys/fs/file-nr: 5728  0  61763

第一个参数:代表已分配的文件句柄
         第二个参数:代表已使用的文件句柄
         第三个参数:代表文件句柄的最大值

为了继续寻找文件限制的相关证据,我又进行了深入的挖掘,
在深入理解linux内核的第三版中有这样一些描述:

The files_init() function, executed during kernel initialization,
sets the max_files to one-tenth of the available RAM in kilobytes, 
bute the system administrator can tune this parameter by writing into the /proc/sys/fs/file-max file.

上面这段即是:
内存初始化期间,files_init()函数把max_files字段设置为可用RAM大小的1/10(kilobytes),
不过,系统管理员可以通过写/proc/sys/fs/file-max文件来修改这个值。
而能改到多大,就没有这个说法了。
而在linux内核源代码:files_init()内则有:
 /*
  * One file with associated inode and dcache is very roughly 1K.
  * Per default don't use more than 10% of our memory for files. 
  */

 n = (mempages * (PAGE_SIZE / 1024)) / 10;
 files_stat.max_files = n; 
通过上面的一段描述:我们知道,带inode和dcache的一个文件大概就是1K(bit),
而我们接收到的建议是文件的使用不超过10%的内存使用量。
总结:linux的最大可打开文件数是可修改的,但具体能修改到多大,
这要看机器的内存情况了。按照这个说法,tcp的连接数限制条件中的文件打开数
我们就可以通过修改文件限制参数和扩展内存来增加。至于tcp连接数的其他限制条件,
则要在之后继续研究了。
--------------------- 

版权声明:本文为CSDN博主「无法除尽的零」的原创文章,遵循CC 4.0 by-sa版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/fox_hacker/article/details/41148115
【12】讲一下TCP与UDP的差异,优缺点。
TCP与UDP区别总结:
1、TCP面向连接(如打电话要先拨号建立连接);UDP是无连接的,即发送数据之前不需要建立连接
2、TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;
UDP尽最大努力交付,即不保证可靠交付,
比如普通的会议视频图像,当然首选UDP,毕竟丢几包无所谓。
如果传输文件等,不能丢包,用TCP
3、TCP面向字节流,实际上是TCP把数据看成一连串无结构的字节流;UDP是面向报文的
UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如IP电话,实时视频会议等)
4、每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信
5、TCP首部开销20字节;UDP的首部开销小,只有8个字节
6、TCP的逻辑通信信道是全双工的可靠信道,UDP则是不可靠信道
【13】为什么UDP有时比TCP更有优势?
UDP以其简单、传输快的优势,在越来越多场景下取代了TCP,如实时游戏。
(1)网速的提升给UDP的稳定性提供可靠网络保障,丢包率很低,如果使用应用层重传,能够确保传输的可靠性。
(2)TCP为了实现网络通信的可靠性,使用了复杂的拥塞控制算法,建立了繁琐的握手过程,
由于TCP内置的系统协议栈中,极难对其进行改进。
采用TCP,一旦发生丢包,TCP会将后续的包缓存起来,等前面的包重传并接收到后再继续发送,
延时会越来越大,基于UDP对实时性要求较为严格的情况下,采用自定义重传机制,
能够把丢包产生的延迟降到最低,尽量减少网络问题对游戏性造成影响。
【14】为什么TCP不适用于实时传输?
TCP影响实时性不是因为握手消耗时间。握手一开始建立完就没事了
一般来说,单位时间内传输的数据流量比较平滑TCP,赖滑动窗口进行流量控制,
滑动窗口大小是自适应的,影响滑动窗口主要有两个因素,一是网络延时,二是传输速率,
滑动窗口的大小与延时成正比,与传输速率也成正比。在给定的网络环境下,延时可以认为是固定的,
因此滑动窗口仅与传输速率有关,当传输实时数据时,因为数据流通量比较固定,
所以这时TCP上的滑动窗口会处于一个不大不小的固定值,这个值大小恰好保证当前生产的数据实时传输到对方,
当出现网络丢包时,按TCP协议(快速恢复),滑动窗口将减少到原来的一半,因此速率立刻减半,
此时发送速率将小于数据生产速率,一些数据将滞留在发送端,然后滑动窗口将不断增大,
直到积累的数据全部发送完毕。上述过程即为典型的TCP流量抖动过程,对于实时传输影响很大,
可能形成较大的突发时延,从用户感观角度来说,就是有时比较流畅,但有时卡(“抖一下”,并且比较严重),
因此实时传输通常不使用TCP。
【15】udp如何实现可靠性传输(新浪)
UDP它不属于连接型协议,因而具有资源消耗小,处理速度快的优点,所以通常音频、视频和普通数据在传送时使用UDP较多,
因为它们即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响。
传输层无法保证数据的可靠传输,只能通过应用层来实现了。实现的方式可以参照tcp可靠性传输的方式,
只是实现不在传输层,实现转移到了应用层。实现确认机制、重传机制、窗口确认机制。
目前有如下开源程序利用udp实现了可靠的数据传输。分别为RUDP、RTP、UDT。
基于UDP的数据传输协议(UDP-based Data Transfer Protocol,简称UDT)是一种互联网数据传输协议。
UDT的主要目的是支持高速广域网上的海量数据传输,而互联网上的标准数据传输协议TCP在高带宽长距离网络上性能很差。
顾名思义,UDT建于UDP之上,并引入新的拥塞控制和数据可靠性控制机制。UDT是面向连接的双向的应用层协议。
【16】什么情况下,连接处于CLOSE_WAIT状态呢?
(1)在被动关闭连接情况下,在已经接收到FIN,但是还没有发送自己的FIN的时刻,
连接处于CLOSE_WAIT状态。通常来讲,CLOSE_WAIT状态的持续时间应该很短,正如SYN_RCVD状态。
但是在一些特殊情况下,就会出现连接长时间处于CLOSE_WAIT状态的情况。

(2)出现大量close_wait的现象,主要原因是某种情况下对方关闭了socket链接,
	 但是我方忙与读或者写,没有关闭连接。代码需要判断socket,一旦读到0,断开连接,
	 read返回负,检查一下errno,如果不是AGAIN,就断开连接

【16】TCP如何保证可靠性传输?
TCP充分实现了数据传输时各种控制功能,可以进行丢包的重发控制,
还可以对次序乱掉的分包进行顺序控制。而这些在UDP中都没有。
此外,TCP作为一种面向有连接的协议,只有在确认通信对端存在时才会发送数据,
从而可以控制通信流量的浪费。TCP通过检验和、序列号、确认应答、重发控制、
连接管理以及窗口控制等机制实现可靠性传输。

	(1)校验和:
	   	 发送的数据包的二进制相加然后取反,目的是检测数据在传输过程中的任何变化。
		 如果收到段的检验和有差错,TCP将丢弃这个报文段和不确认收到此报文段。 

	(2)确认应答+序列号(累计确认+seq):
	     接收方收到报文就会确认(累积确认:对所有按序接收的数据的确认)
		 TCP给发送的每一个包进行编号,接收方对数据包进行排序,把有序数据传送给应用层。 

	(3)超时重传:
		 当TCP发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段。
		 如果不能及时收到一个确认,将重发这个报文段。 

	(4)流量控制:
		 TCP连接的每一方都有固定大小的缓冲空间,TCP的接收端只允许发送端发送接收端缓冲区能接纳
		 的数据。当接收方来不及处理发送方的数据,能提示发送方降低发送的速率,防止包丢失。
		 TCP使用的流量控制协议是可变大小的滑动窗口协议。
		 接收方有即时窗口(滑动窗口),随ACK报文发送

	(5)拥塞控制:

	当网络拥塞时,减少数据的发送。
	发送方有拥塞窗口,发送数据前比对接收方发过来的即使窗口(滑动窗口),取小
	慢启动、拥塞避免、拥塞发送、快速恢复

	(6)应用数据被分割成TCP认为最适合发送的数据块。 
		 TCP的接收端会丢弃重复的数据。

【16】TCP的丢包重传机制、滑动窗口、流量控制、拥塞控制?

【17】TCP的四个定时器?

(1)超时重传定时器:在发送端发送数据后,启动超时重传定时器,在给定时间内没有收到对端
发送的确认消息,此定时器超时,发送端重新发送数据,并将此定时器复位。

(2)坚持定时器:发送端给接收端发送一部分数据后,接收端的缓存已满无法在接收数据,
就向接收端发送一个滑窗大小为0的ACK通知发送方停止发送数据,接收方收到此消息后,
启动坚持定时器,并向接收方发送大小为1个字节的探测报文,
在定时器超时后还未收到接收端窗口更新的消息,将坚持定时器加倍并复位,
直到坚持定时器已达max=60s,接下来就每隔60s发送探测报文,直到收到接收端的窗口更新消息。
   设计此定时器的原因:假设发送端收到滑窗大小为0的ACK消息后,接收端发现自己已经可以接受数据了,
于是向发送方发送滑窗大小不为0的ACK,不巧的是这个ACK丢了,发送方若没有坚持定时器,
则一直在等待发送端通知自己什么时候可以发数据,而接收端认为自己已经通知发送方了,
但是没有收到发送方的数据,这就会造成一种死锁状态。

(3)保活定时器:在一个TCP成功连接后,接收端在收到一次发送方的数据后,会启动一个保活定时器,
时间为2h,如果此定时器超时,发送方会每隔75s发送一个探测报文,在尝试10次以后,
发送方就可以断开这个连接。

(4)2MS定时器:此定时器是为了处理TCP连接中主动断开连接的一方处于TIME_WAIT状态而设置的,
大小为2MSL(数据包在网络上存活的最大时长的两倍,即一个往返时间),
处于此状态的一方无法立即建立新的连接,设置此状态下的定时器有连个作用,
其一,防止断开连接时的最后一个ACK丢失,保证TCP连接的可靠性;
其二,防止旧的连接上传送的脏数据被新的连接收到。

【17】域套接字比流式套接字快的原因?
UNIX域套接字用于同一台pc上运行的进程之间通信,它仅仅复制数据,不执行协议处理,
不需要增加删除网络报头,无需计算校验和,不产生顺序号,无需发送确认报文。
#include <sys/un.h>

struct sockaddr_un 
{
	sa_family_t sun_family;/* AF_LOCAL */
	char sun_path[104];/* 以空字符结尾的字符串 */
}

【13】写一下strstr的实现函数
char * strstr(const char *src,constr char *find)
{
assert((src != NULL) || (find != NULL));
while()
}

【14】说一下epoll,select实现的功能,聊一聊多路复用的理解;

【15】说一下,一个函数在堆栈中是如何放置的,static变量,全局变量是放置在哪里?

算法题:

【16】提供总数为100万个数,可能会分10次提供,即每次提供10万个,然后你如何在每次的能刷选出前10个最大的数据
堆排序:
对于这种题目,最普通的想法是先对这10万个数进行排序,然后再选取数组中前10个数,即为最后的答案,排序算法的时间复杂度不下于O(N lgN)。最好的方法是建立一个最小堆。
算法描述:
我们首先取10万个元素中的前10个元素来建立由10个元素组成的最小堆。这样堆顶元素便是当前已知元素的第10大的数;然后依次读取剩下的99990个元素,若读取的元素比堆顶元素大,则将堆顶元素和当前元素替换,并自堆顶至下调整堆;这样读取完所有元素后,堆中的10个元素即为这10万个数最大的10个数,同时堆顶元素为这10万个元素第10大元素。
时间复杂度:
设从N个数中找M个最大数
每次重新恢复堆的时间复杂都为O(logM),最多供进行了(N-M)次恢复堆操作,顾时间复杂度为O(NlogM)。

doc格式,60多页吧,几百道吧,都有答案吧,看好在下!部分:1.求下面函数的返回值(微软)int func(x) { int countx = 0; while(x) { countx ++; x = x&(x-1); } return countx; } 假定x = 9999。 答案:8思路:将x转化为2进制,看含有的1的个数。2. 什么是“引用”?申明和使用“引用”要注意哪些问?答:引用就是某个目标变量的“别名”(alias),对应用的操作与对变量直接操作效果完全相同。申明一个引用的时候,切记要对其进行初始化。引用声明完毕后,相当于目标变量名有两个名称,即该目标原名称和引用名,不能再把该引用名作为其他变量名的别名。声明一个引用,不是新定义了一个变量,它只表示该引用名是目标变量名的一个别名,它本身不是一种数据类型,因此引用本身不占存储单元,系统也不给引用分配存储单元。不能建立数组的引用。3. 将“引用”作为函数参数有哪些特点?(1)传递引用给函数与传递指针的效果是一样的。这时,被调函数的形参就成为原来主调函数中的实参变量或对象的一个别名来使用,所以在被调函数中对形参变量的操作就是对其相应的目标对象(在主调函数中)的操作。(2)使用引用传递函数的参数,在内存中并没有产生实参的副本,它是直接对实参操作;而使用一般变量传递函数的参数,当发生函数调用时,需要给形参分配存储单元,形参变量是实参变量的副本;如果传递的是对象,还将调用拷贝构造函数。因此,当参数传递的数据较大时,用引用比用一般变量传递参数的效率和所占空间都好。(3)使用指针作为函数的参数虽然也能达到与使用引用的效果,但是,在被调函数中同样要给形参分配存储单元,且需要重复使用"*指针变量名"的形式进行运算,这很容易产生错误且程序的阅读性较差;另一方面,在主调函数的调用点处,必须用变量的地址作为实参。而引用更容易使用,更清晰。4. 在什么时候需要使用“常引用”? 如果既要利用引用提高程序的效率,又要保护传递给函数的数据不在函数中被改变,就应使用常引用。常引用声明方式:const 类型标识符 &引用名=目标变量名;例1int a ;const int &ra=a;ra=1; //错误a=1; //正确 例2string foo( );void bar(string & s); 那么下面的表达式将是非法的:bar(foo( ));bar("hello world"); 原因在于foo( )和"hello world"串都会产生一个临时对象,而在C++中,这些临时对象都是const类型的。因此上面的表达式就是试图将一个const类型的对象转换为非const类型,这是非法的。引用型参数应该在能被定义为const的情况下,尽量定义为const 。5. 将“引用”作为函数返回值类型的格式、好处和需要遵守的规则?格式:类型标识符 &函数名(形参列表及类型说明){ //函数体 }好处:在内存中不产生被返回值的副本;(注意:正是因为这点原因,所以返回一个局部变量的引用是不可取的。因为随着该局部变量生存期的结束,相应的引用也会失效,产生runtime error!注意事项:(1)不能返回局部变量的引用。这条可以参照Effective C++[1]的Item 31。主要原因是局部变量会在函数返回后被销毁,因此被返回的引用就成为了"无所指"的引用,程序会进入未知状态。 (2)不能返回函数内部new分配的内存的引用。这条可以参照Effective C++[1]的Item 31。虽然不存在局部变量的被动销毁问,可对于这种情况(返回函数内部new分配内存的引用),又面临其它尴尬局面。例如,被函数返回的引用只是作为一个临时变量出现,而没有被赋予一个实际的变量,那么这个引用所指向的空间(由new分配)就无法释放,造成memory leak。(3)可以返回类成员的引用,但最好是const。这条原则可以参照Effective C++[1]的Item 30。主要原因是当对象的属性是与某种业务规则(business rule)相关联的时候,其赋值常常与某些其它属性或者对象的状态有关,因此有必要将赋值操作封装在一个业务规则当中。如果其它对象可以获得该属性的非常量引用(或指针),那么对该属性的单纯赋值就会破坏业务规则的完整性。(4)流操作符重载返回值申明为“引用”的作用:流操作符<>,这两个操作符常常希望被连续使用,例如:cout << "hello" << endl; 因此这两个操作符的返回值应该是一个仍然支持这两个操作符的流引用。可选的其它方案包括:返回一个流对象和返回一个流对象指针。但是对于返回一个流对象,程序必须重新(拷贝)构造一个新的流对象,也就是说,连续的两个<<操作符实际上是针对不同对象的!这无法让人接受。对于返回一个流指针则不能连续使用<<操作符。因此,返回一个流对象引用是惟一选择。这个唯一选择很关键,它说明了引用的重要性以及无可替代性,也许这就是C++语言中引入引用这个概念的原因吧。 赋值操作符=。这个操作符象流操作符一样,是可以连续使用的,例如:x = j = 10;或者(x=10)=100;赋值操作符的返回值必须是一个左值,以便可以被继续赋值。因此引用成了这个操作符的惟一返回值选择。例3#i nclude int &put(int n);int vals[10];int error=-1;void main(){put(0)=10; //以put(0)函数值作为左值,等价于vals[0]=10; put(9)=20; //以put(9)函数值作为左值,等价于vals[9]=20; cout<<vals[0]; cout<<vals[9];} int &put(int n){if (n>=0 && n<=9 ) return vals[n]; else { cout<<"subscript error"; return error; }} (5)在另外的一些操作符中,却千万不能返回引用:+-*/ 四则运算符。它们不能返回引用,Effective C++[1]的Item23详细的讨论了这个问。主要原因是这四个操作符没有side effect,因此,它们必须构造一个对象作为返回值,可选的方案包括:返回一个对象、返回一个局部变量的引用,返回一个new分配的对象的引用、返回一个静态对象引用。根据前面提到的引用作为返回值的三个规则,第2、3两个方案都被否决了。静态对象的引用又因为((a+b) == (c+d))会永远为true而导致错误。所以可选的只剩下返回一个对象了。6. “引用”与多态的关系?引用是除指针外另一个可以产生多态效果的段。这意味着,一个基类的引用可以指向它的派生类实例。例4Class A; Class B : Class A{...}; B b; A& ref = b;7. “引用”与指针的区别是什么?指针通过某个指针变量指向一个对象后,对它所指向的变量间接操作。程序中使用指针,程序的可读性差;而引用本身就是目标变量的别名,对引用的操作就是对目标变量的操作。此外,就是上面提到的对函数传ref和pointer的区别。8. 什么时候需要“引用”?流操作符<>、赋值操作符=的返回值、拷贝构造函数的参数、赋值操作符=的参数、其它情况都推荐使用引用。以上 2-8 参考:http://blog.csdn.net/wfwd/archive/2006/05/30/763551.aspx9. 结构与联合有和区别?1. 结构和联合都是由多个不同的数据类型成员组成, 但在任何同一时刻, 联合中只存放了一个被选中的成员(所有成员共用一块地址空间), 而结构的所有成员都存在(不同成员的存放地址不同)。 2. 对于联合的不同成员赋值, 将会对其它成员重写, 原来成员的值就不存在了, 而对于结构的不同成员赋值是互不影响的。10. 下面关于“联合”的目的输出?a)#i nclude union{int i;char x[2];}a;void main(){a.x[0] = 10; a.x[1] = 1;printf("%d",a.i);}答案:266 (低位低地址,高位高地址,内存占用情况是Ox010A)………………
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值