面试整理

一 c++

1.多态的实现

多态简单来说,是指在继承层次中,父类的指针可以具有多种形态——当它指向某个子类对象是,通过它能够调用到子类的函数,而非父类的函数。

一个接口,多种实现。使用继承+虚函数,使用virtual关键字修饰成员函数。New一个子类对象,赋给父类的指针和引用。(实现机制:虚函数表)

2.虚函数

虚函数表属于类,类的所有对象共享这个类的虚函数表。虚函数表存储在进程的只读数据段。每个对象都有一个默认的指针vptr指向虚函数表。确定vptr指向的具体对象类型,加上偏移量,就可以找到具体调用的函数。虚函数指针一般都放在对象内存布局的第一个位置上,这是为了保证在多层继承或多重继承的情况下能以最高效率取到虚函数表。当vptr位于对象内存最前面是,对象的地址即为虚函数指针地址。我们可以取得虚函数指针的地址。

3内存对齐

内存对齐只是指数据在内存时的起始地址是否是某个值得整数倍。如果只是放在内存中,是否对齐本身并没有什么问题。未提示读取、写入的时候。访问一个不对齐的数据可能会导致程序运行效率慢,结果出错,甚至是程序宕掉。

原则:

(1)数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如说是数组,结构体等)的整数倍开始(比如int在32位机为4字节, 则要从4的整数倍地址开始存储),基本类型不包括struct/class/uinon。

(2)结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部"最宽基本类型成员"的整数倍地址开始存储.(struct a里存有struct b,b里有char,int ,double等元素,那b应该从8的整数倍开始存储.)。

(3)收尾工作:结构体的总大小,也就是sizeof的结果,.必须是其内部最大成员的"最宽基本类型成员"的整数倍.不足的要补齐.(基本类型不包括struct/class/uinon)。

(4)sizeof(union),以结构里面size最大元素为union的size,因为在某一时刻,union只有一个成员真正存储于该地址

4.c++对象模型

在c++中有两种数据成员,静态和非静态的,有三种成员函数:静态、非静态和虚函数。

5.malloc/free和new/delete的区别

 1)new/delete是c++里才有的。

 2)new/delete通常来说是操作符,就是"+","-"一样。

 3)new/delete是可以重载的,而重载之后,就成为了函数。

 4)malloc在申请内存的时候,必须要提供申请的长度,而且返回的指针是void*型,必须要强转成需要的类型。

5)当new/delete在类中被重载的时候,可以自定义申请过程,比如记录所申请内存的总长度,以及跟踪每个对象的指针。

6) new/delete,其实内部也调用了malloc/free。

7)new 在申请内存的同时,还会调用对象的构造函数,而 malloc 只会申请内存;同样,delete 在释放内存之前,会调用对象的析构函数,而 free 只会释放内存。

共同点:

1) 都必须配对使用。

2) 都是申请内存,释放内存。

3) free和delete可以释放NULL指针。

注意点:

new/delete与malloc/free不能混合使用。

6.const关键字

Const对象一旦创建其值就无法改变,所以const对象必须初始化。

7.指针和引用的区别:

指针存储的是所指变量的地址,而引用是变量的别名,引用不能为空,必须初始化。

8.c++堆和栈的区别

栈:由程序自动向操作系统申请分配以及回收,速度快,使用方便。但程序员无法控制。若分配失败,则提示栈溢出错误。注意,const局部变量也存储在栈区内,栈区向地址减小的方向增长。

堆:程序员向操作系统申请一块内存,当系统收到程序的申请时,会遍历一个记录空闲内存地址的链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。分配的速度较慢地址不连续,容易碎片化。此外,由程序员申请,同时也必须由程序员负责销毁,否则则导致内存泄露。

9.关键字static

Static变量:存储在静态存储区。作用:第一个作用是限定作用域;第二个作用是保持变量内容持久化。

1. C 语言中static的用法:

  1. 全局静态变量:

   用法:在全局变量前加上关键字static,全局变量就定义成一个全局静态变量。

   内存中的位置:静态存储区,在整个程序运行期间一直存在。

   初始化:未经初始化的全局静态变量会被自动初始化为0(自动对象的值是任意的,除非他被显式初始化)。

   作用域:全局静态变量在声明他的文件之外是不可见得,准确的说是从定义之处开始,到文件结尾。

  1. 局部静态变量:

   用法:在局部变量前加上关键字static,局部变量就成为一个局部静态变量。

   内存的位置:静态存储区。

   初始化:未经初始化的全局静态变量会被自动初始化为0.

   作用域:作用域仍未局部作用域,当定义它的函数或者语句块结束的时候,作用域结束。但是当局部静态变量离开作用域后,并没有销毁,而是仍然驻留在内存当中,只不过我们不能再对它进行访问,直到该函数再次被调用,并且值不变。

  1. 静态函数:

在函数返回类型前加关键字static,函数就定义成静态成员函数。函数的定义和生命在默认情况下都是extern的,但静态函数只是在声明他的文件当中可见,不能被其他文件所用。

  1. C++中static的用法:
  1. 类的静态成员:

class A{

private:

     static int val;

};
在cpp中必须对它进行初始化,初始化时使用作用域运算符来标明他所属类,其属于该类的所有成员共有,只有一个拷贝。

  1. 类的静态成员函数:

class A{

private:

    static int func(int x);

}

实现的时候也不需要static的修饰,因为static是声明性关键字;类的静态函数是该类的范畴内的全局函数,不能访问类的私有成员,只能访问类的静态成员,不需要类的实例即可调用;实际上,他就是增加了类的访问权限的全局函数。

void A::func(int);

静态成员函数可以继承和覆盖,但无法是虚函数。

3)只在cpp内有效的全局变量:在cpp文件的全局范围内声明:static int val =0;

这个变量的含义是该cpp内有效,但是其他的cpp文件不能访问这个变量;如果两个cpp文件声明了同名的全局静态变量,那么他们实际上是独立的两个变量。

  1. 只在cpp内有效的全局函数:

函数的实现使用static修饰,那么这个函数只可在本cpp内使用,不会同其他cpp中的同名函数引起冲突。   

warning:不要在头文件中声明static的全局函数,不要在cpp内声明非static的全局函数,如果你要在多个cpp中复用该函数,就把它的声明提到头文件里去,否则cpp内部声明需加上static修饰。(说的逻辑不对?)

10.用过的设计模式,举几个例子

singleton:static成员函数中构造local static变量的方法来实现,因为static成员只能初始化一次。

template<typename T>

class Singleton

{

public:

    static T& getInstance()

    {

        if (!value_)

        {

            value_ = new T();

        }

        return *value_;

    }

private:

    Singleton();

    ~Singleton();

    static T* value_;

};

template<typename T>

T* Singleton<T>::value_ = NULL;

11.c++深拷贝和浅拷贝

浅拷贝,即在定义一个类A,使用类似A obj;  A obj1(obj);或者A obj1 = obj; 时候,由于没有自定义拷贝构造函数,C++编译器自动会产生一个默认的拷贝构造函数。这个默认的拷贝构造函数采用的是“位拷贝”(浅拷贝),而非“值拷贝”(深拷贝)的

方式,如果类中含有指针变量,默认的拷贝构造函数必定出错。

用一句简单的话来说就是浅拷贝,只是对指针的拷贝,拷贝后两个指针指向同一个内存空间,深拷贝不但对指针进行拷贝,而且对指针指向的内容进行拷贝,经深拷贝后的指针是指向两个不同地址的指针。

假如有一个成员变量的指针,char *m_data;

其一,浅拷贝只是拷贝了指针,使得两个指针指向同一个地址,这样在对象块结束,调用函数析构的时,会造成同一份资源析构2次,即delete同一块内存2次,造成程序崩溃。

其二,浅拷贝使得obj.m_data和obj1.m_data指向同一块内存,任何一方的变动都会影响到另一方。

其三,在释放内存的时候,会造成obj1.m_data原有的内存没有被释放(这句话,刚开始我不太理解,如果没有走自定义的拷贝构造函数,申请内存空间,A obj1(obj);也不走默认构造函数,走的是默认的拷贝构造函数,何来分配空间直说,更不会造成obj1.m_data原有的内存没有被释放,这里刚开始我一直有疑问),造成内存泄露。

事实是这样的,当delete obj.m_data, obj.m_data内存被释放后,由于之前obj.m_data和obj1.m_data指向的是同一个内存空间,obj1.m_data所指的空间不能在被利用了,delete obj1.m_data也不会成功,一直已经无法操作该空间,所以导致内存泄露。

深拷贝采用了在堆内存中申请新的空间来存储数据,这样每个可以避免指针悬挂12.volatile的作用

volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。声明时语法:int volatile vInt; 当要求使用 volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。而且读取的数据立刻被保存。例如:

 

volatile int i=10;

int a = i;

...

// 其他代码,并未明确告诉编译器,对 i 进行过操作

5

int b = i;

 volatile 指出 i 是随时可能发生变化的,每次使用它的时候必须从 i的地址中读取,因而编译器生成的汇编代码会重新从i的地址读取数据放在 b 中。而优化做法是,由于编译器发现两次从 i读数据的代码之间的代码没有对 i 进行过操作,它会自动把上次读的数据放在 b 中。而不是重新从 i 里面读。这样以来,如果 i是一个寄存器变量或者表示一个端口数据就容易出错,所以说 volatile 可以保证对特殊地址的稳定访问。

13.构造函数和析构函数可以调用虚函数吗

构造函数跟析构函数里面都可以调用虚函数,编译器不会报错。C++ primer中说到最好别用。

由于类的构造次序是由基类到派生类,所以在构造函数中调用虚函数,虚函数是不会呈现出多态的。

类的析构是从派生类到基类,当调用继承层次中某一层次的类的析构函数时意味着其派生类部分已经析构掉,所以也不会呈现多态。

因此如果在基类中声明的纯虚函数并且在基类的析构函数中调用之,编译器会发生错误。

 

14.友元函数

友元函数不含this指针,所以友元函数访问对象中的成员要通过对象名。友元函数可以在类内实现也可以在类外实现。

15.构造函数和拷贝构造函数能否为虚函数

不能,只有析构函数可以定义为虚函数,构造函数不能定义为虚函数。构造函数是在对象完全构造之前运行的,在构造函数运行的时候,对象的动态类型还不完整。

16.类对象所占内存空间

空的类是会占用内存空间的,而且大小是1,原因是C++要求每个实例在内存中都有独一无二的地址。

1)类内部的成员变量:

* 普通的变量:是要占用内存的,但是要注意对齐原则(这点和struct类型很相似)。

* static修饰的静态变量:不占用内存,原因是编译器将其放在全局变量区。

2)类内部的成员函数:

* 普通函数:不占用内存。

* 虚函数:要占用4个字节,用来指定虚函数的虚拟函数表的入口地址。所以一个类的虚函数所占用的地址是不变的,和虚函数的个数是没有关系。

17.关于析构函数和纯虚函数

纯虚析构函数和普通纯虚函数的区别在于,纯虚析构函数需要提供函数的实现,而一般纯虚函数不能有实现,原因在于,纯虚析构函数最终需要被调用,以析构基类对象,虽然是抽象类没有实体。而如果不提供该析构函数的实现,将使得在析构过程中,析构无法完成而导致析构异常的问题。

通过父类指针去析构子类对象,分三种情况:

1)父类的析构函数不是虚函数,这种情况下,将只会调用A的析构函数而不会调用子类的析构函数,前面的文章中有提到过,非虚函数是通过类型来寻址的,这样的析构将会导致析构畸形,这是笔试面试中经常遇到的考点。

2)父类的析构函数是普通的虚函数,这种情况下,会很正常,从子类一直析构到基类,最后完成析构,这就是针对A的修改方法之一。

3)父类的析构函数是纯析构函数,重点,在这种情况之下,由于析构函数首先是虚函数,所以会按2的方法从子类一直析构到父类,但是,又由于父类的析构函数是纯虚函数,如果没有实现体,导致父类无法析构,最终也导致了析构畸形,因此,特殊的地方就在于这里,纯虚析构函数需要提供一个实现体,以完成对象的析构。

18.数组名是数组首元素的地址

a[10];

 a=&a[0];

 a+2=&a[2];

 *(a+2)=a[2];

19.大端模式和小端模式

举一个例子,比如数字0x12 34 56 78在内存中的表示形式。

1)大端模式:Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。

(其实大端模式才是我们直观上认为的模式,和字符串存储的模式差类似)

低地址 --------------------> 高地址

0x12  |  0x34  |  0x56  |  0x78

2)小端模式:Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。

低地址 --------------------> 高地址

0x78  |  0x56  |  0x34  |  0x12

20.void *

void 指针可以指向任意类型的数据,就是说可以用任意类型的指针对void指针赋值

 

  1. 左值、右值、右值引用

1.左值:

对于早期的c语言,提到左值意味着:

  1. 它指定一个对象,所以引用内存中的地址。
  2. 它可用在赋值运算符的左侧,左值(lvalue)中的l源自left。

但是后来,标准中新增了const限定符。用const创建的变量不可修改。因此,const标识符满足上面的第一项,但是不满足第二项。一方面c继续把标识对象的表达式定义为左值,一方面某些左值却不能放在赋值运算符的左侧。此时,标准对左值的定义已经不能满足当前的状况。为此,c标准新增了一个术语:可修改的左值(modifiable lvalue),用于标识可修改的对象。所以赋值运算符的左侧应该是可修改的左值。

  1. 右值:

指的是能赋值给可修改左值的量,且本身不是左值。

考虑下面的语句:

bmw=2002;

这里bmw是可修改的左值,2002是右值。右值可以是常量、变量或其他可求值的表达式(如函数调用)。实际上,当前标准在描述这一概念是使用的是表达式的值(value of an expression),而不是右值。

示例:

int ex;

int why;

int zee;

const int two=2;

why=42;

zee=why;

ex=tow*(why+zee);

这里,ex、why和zee都是可修改的左值(或对象定位值),它们可用于赋值运算符的左侧和右侧。two是不可修改的左值,它只能用于赋值运算符的右侧(在该例中,two被初始化为2,这里的=运算符表示初始化而不是赋值,因此并未违反规则)。同时42是右值,它不能引用某指定内存的位置。另外,why和zee是可修改的左值,表达式(why+zee)是右值,该表达式不能表示特定内存位置,而且也不能给它赋值。它只是程序计算的一个临时值,在计算完毕后便会被丢弃。

  1. 右值引用

为了支持移动操作,c++新标准引入了一种新的引用类型——右值引用。所谓右值引用就是必须绑定到右值的引用。我们通过&&而不是&来获得右值引用。如我们将要看到的,右值引用有一个重要的性质:只能绑定到一个将要销毁的对象。因此,我们可以自由地将一个右值引用的资源“移动”到另一个对象中。

回忆一下,左值和右值是表达式的属性。一些表达式生成或要求左值,而另外一些则生成或者要求右值。一般而言,一个左值表达式表示的是一个对象的身份,而一个右值表达式表示的是对象的值。

类似任何引用,一个右值引用也不过是某个对象的另一个名字而已。午我们所知,对于常规引用(为了与右值引用区分开),我们可以称之为左值引用,我们不能将其绑定到要求转换的表达式、字面常量或是返回右值的表达式。右值引用有着完全相反的绑定特性:我们可以将一个右值绑定到这类表达式上,但不能将一个右值引用直接绑定到一个左值上。

int i=42;

int &r=i;       //正确:r引用i。

int &&rr=i     //错误:不能将一个右值绑定到一个左值上。

int &r2=i*42;   //错误:i*42是一个右值。

const int &r3=i*42; //正确:我们可以将一个const的引用绑定到一个右值上。

Int &&rr2=i*42;   //正确:将rr2绑定到乘法结果上。

返回左值引用的函数,连同赋值、下标、解引用和前置递增/递减运算符,都是返回左值表达式的例子。我们可以将一个左值引用绑定到这类表达式的结果上。

返回非引用类型的函数,连同算术、关系、位以及后置递增/递减运算符,都生成右值。我们不能将一个左值引用绑定到这类表达式上,但我们可以将一个const的左值引用或者一个右值引用绑定到这类表达式上。

左值持久,右值短暂

考察左值和右值表达式的列表,两者相互区别之处就很明显了,左值有持久的状态,而右值要么是字面常量,要么是表达式求值过程中创建的临时对象。

由于右值引用只能绑定到临时对象,我们得知:所有引用的对象将要销毁,该对象没有其他用户。这两个特性意味着:使用右值引用的代码可以自由地接管所引用的对象的资源。

标准库move函数

虽然不能将一个右值引用直接绑定到一个左值上,但我们可以显示地将一个左值转换为对应的右值引用类型。我们还可以通过调用一个名为move的新标准库函数来获得绑定到左值上的右值引用,次函数在头文件utility中。Move函数来返回给定对象的右值引用。

         Int &&r3=std::move(rr1); 

move调用告诉编译器:我们有一个左值,但我们希望像一个右值一样处理它。我们必须认识到,调用move就意味着承诺:除了对rr1赋值或销毁它外,我们将不再使用它。在调用move之后,我们不能对移后源对象的值做任何假设。

(我们可以销毁一个移后源对象,也可以赋新值,但不能使用一个移后源对象的值。)

  1. extern 关键字

1.用法:

(1)extern c:以c的规则进行编译。

(2)C++语言支持分离式编译机制,该机制允许将程序分割为若干个文件,每个文件可被独立编译。为了将程序分为许多文件,则需要在文件中共享代码,例如一个文件的代码可能需要另一个文件中中定义的变量。在C语言中,修饰符extern用在变量或者函数的声明前,用来说明“此变量/函数是在别处定义的,要在此处引用”。

2.

extern 修饰变量的声明

如果文件a.c需要引用b.c中变量int v,就可以在a.c中声明extern int v,然后就可以引用变量v。

这里需要注意的是,被引用的变量v的链接属性必须是外链接(external)的,也就是说a.c要引用到v,不只是取决于在a.c中声明extern int v,还取决于变量v本身是能够被引用到的。

这涉及到c语言的另外一个话题--变量的作用域。能够被其他模块以extern修饰符引用到的变量通常是全局变量。

还有很重要的一点是,extern int v可以放在a.c中的任何地方,比如你可以在a.c中的函数fun定义的开头处声明extern int v,然后就可以引用到变量v了,只不过这样只能在函数fun作用域中引用v罢了,这还是变量作用域的问题。对于这一点来说,很多人使用的时候都心存顾虑。好像extern声明只能用于文件作用域似的。

extern 修饰函数声明

从本质上来讲,变量和函数没有区别。函数名是指向函数二进制块开头处的指针。

如果文件a.c需要引用b.c中的函数,比如在b.c中原型是int fun(int mu),那么就可以在a.c中声明extern int fun(int mu),然后就能使用fun来做任何事情。

就像变量的声明一样,extern int fun(int mu)可以放在a.c中任何地方,而不一定非要放在a.c的文件作用域的范围中。

对其他模块中函数的引用,最常用的方法是包含这些函数声明的头文件。使用extern和包含头文件来引用函数有什么区别呢?extern的引用方式比包含头文件要简洁得多!extern的使用方法是直接了当的,想引用哪个函数就用extern声明哪个函数。

这样做的一个明显的好处是,会加速程序的编译(确切的说是预处理)的过程,节省时间。在大型C程序编译过程中,这种差异是非常明显的。

extern修饰符可用于指示c或者c++函数的调用规范

比如在C++中调用C库函数,就需要在C++程序中用extern “C”声明要引用的函数。

这是给链接器用的,告诉链接器在链接的时候用C函数规范来链接。主要原因是C++和C程序编译完成后在目标代码中命名规则不同。

为了支持分离式编译,C++允许将声明和定义分离开来。变量的声明规定了变量的类型和名字,即使一个名字为程序所知,一个文件如果想使用别处定义的名字则必须包含对那个名字的声明。定义则负责创建与名字关联的实体,定义还申请存储空间。

  1. 如何防止头文件被重复包含
  1. 使用条件编译,所有头文件格式如下

#ifndef _HEADERNAME_H

#define _HEADERNAME_H

...//(头文件内容)

#endif

  1. windows平台下,使用宏命令#progma once

24.什么是内存泄露?野指针?内存越界?如何避免。

1.内存泄露:程序员动态申请的内存没有释放。

2.野指针:指向内存被释放的内存或者没有访问权限的内存的指针。

成因:

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

char *p = NULL;

char *str = new char(100);

  1. 指针p被free或者delete之后,没有置为null。
  2. 指针操作超越了变量的作用范围。这种情况让人防不胜防

class A

{

public:

void Func(void){ cout << “Func of class A” << endl; }

};

void Test(void)

{

  A  *p;

  if(...){

 A  a;

p = &a; // 注意 a 的生命期

  }

     p->Func(); // p是“野指针”

}

内存越界:何谓内存访问越界,简单的说,你向系统申请了一块内存,在使用这块内存的时候,超出了你申请的范围。可能发生内存的改写,影响附近内存块的数据安全。

  1. 出core文件
  1. 程序崩溃时程序运行时的保存的内存信息的coredump文件,可以通过sysctl或者/proc中来设置core文件的文件名以及生成的路径等。一般的coredump文件为ELF格式,coredump包含了程序运行时的内存,寄存器状态,堆栈指针,内存管理信息等。许多程序和操作系统出错时会自动生成一个core文件。coredump可以用在很多场合,使用Linux系统在跑一些压力测试或者系统负载一大的话,系统就hang住了或者干脆system panic。这时唯一能帮助你分析和解决问题的就是coredump了。通常进程或者内核收到。
  2. 相关命令

ulimit命令改变shell的资源限制,显示shell的资源限制,coredump项值为coredump文件大小单位blocks(4kbytes),程序崩溃时的行为不可按平常时的行为来估计,比如缓冲区溢出等错误可能导致堆栈被破坏,因此经常会出现某个变量的值被修改成乱七八糟的,然后程序用这个大小去申请内存就可能导致程序比平常时多占用很多内存。因此无论程序正常运行时占用的内存多么少,要保证生成Core文件还是将大小限制设为unlimited为好。一般使用ulimit -c unlimited不限制coredump文件大小,生成文件太小gdb时候会报错。

开启系统的coredump步骤为:

第一步,开启内核宏支持coredump函数(对应的进程重新编译并编译中-g要加上才能正常显示gdb信息)

 

第二步,命令设置coredump文件大小,ulimit -c unlimited表示coredump没有限制或者ulimit -c 1024支持文件大小1024k,如果进程脱离终端利用getrlimit,setrlimit如下所示

 

第三步,设置文件生成路径以及文件名。可以/proc/sys/kernel/core_pattern和/proc/sys/kernel/core_uses_pi来设置或者sysctl -w kernel.core_uses_pid =0 sysctl -w kernel.core_pattern = /var/core.%e.%p设置

3)调式

如上述步骤成功则会生成对应的core文件,如果是大型服务器中core文件可以直接gdb进行调试,这里只说明在嵌入式中如何利用gdb达到调试的目的。对应目录生成的core文件从系统中拷贝出来类似tftp命令 ftp或者利用u盘拷贝。嵌入式中不能拷贝出来coredump文件那之前设置都是白费的。

成功获取core文件,并将拷贝的core文件放入对应的process的程序工程目录下同一目录下且工程目录有process生成bin文件,cd到process的目录

XXX-XXX-XX-gdb bin core

进入gdb模式,调试中可能有一些库要用到,所以还要设置gdb中调用库的库文件的绝对路径,一般linux嵌入式一般是生成的文件系统作为调用路径

initially, you will see a lot of error messages. They can be ignored.  Now on the gdb prompt, type:

(gdb) set solib-absolute-prefix SRCPATH/targets/PROFILE/fs.install

(gdb) bt

bt之后可以看见打印的堆栈信息。

  1. 设计模式及实现(c++)

singleton:单例模式。使用static成员函数中构造local static变量的方法来实现,因为static成员只能初始化一次。

template<typename T>

class Singleton

{

public:

    static T& getInstance()

    {

        if (!value_)

        {

            value_ = new T();

        }

        return *value_;

    }

private:

    Singleton();

    ~Singleton();

    static T* value_;

};

template<typename T>

T* Singleton<T>::value_ = NULL;

  1. c++四种强制类型转换

1)const_cast

常量指针被转化成非常量的指针,并且仍然指向原来的对象;常量引用被转换成非常量的引用,并且仍然指向原来的对象。

2)static_cast

static_cast作用和c语言风格强制转换的 效果基本一样,由于没有运行时类型检查来保证转换的安全性,所以这类型的强制转换和c语言风格的强制转换都有安全隐患;

用于类层次结构中基类和派生类之间指针或引用的转换。(进行上行转换:把派生类的指针或引用转换成基类表示是安全的,进行下行转换:把基类指针或引用转换成派生类表示时,由于没有动态类型检查,所以是不安全的。);

用于基本数据类型之间的转换;

static_cast不能转换调原有类型的const、volatile、或者_unaligned属性;

c++的任何隐式转换都是使用static_cast来实现。

3)dynamic_cast

会在程序运行时对类型转换对“运行期类型信息”进行检查。

4)reinterpret_cast

用来处理无关类型转换的, 通常为操作数的位模式提供较低层次的重新解释。(仅仅是重新解释了给出的对象的比特模型,并没有进行二进制的转换),用在任意指针之间的转换,引用之间的转换,指针和足够大的int型之间的转换,整数到指针的转换。

  1. c++抽象类
  1. 定义: 纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0”。
  2. 引入原因

第一:为了方便使用多态特性,我们常常需要在基类中定义虚拟函数。

第二:在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。

为了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数(方法:virtual ReturnType Function()= 0;),则编译器要求在派生类中必须予以重载以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。这样就很好地解决了上述两个问题。

二 网络

总体概述

一 OSI与TCP/IP各层的结构与功能,都有哪些协议?

学习计算机网络时我们一般采用折中的办法,也就是中和 OSI 和 TCP/IP 的优点,采用一种只有五层协议的体系结构,这样既简洁又能将概念阐述清楚。

 

结合互联网的情况,自上而下地,非常简要的介绍一下各层的作用。

1.1 应用层

应用层(application-layer)的任务是通过应用进程间的交互来完成特定网络应用。应用层协议定义的是应用进程(进程:主机中正在运行的程序)间的通信和交互的规则。对于不同的网络应用需要不同的应用层协议。在互联网中应用层协议很多,如域名系统DNS,支持万维网应用的 HTTP协议,支持电子邮件的 SMTP协议等等。我们把应用层交互的数据单元称为报文。

 

域名系统

 

域名系统(Domain Name System缩写 DNS,Domain Name被译为域名)是因特网的一项核心服务,它作为可以将域名和IP地址相互映射的一个分布式数据库,能够使人更方便的访问互联网,而不用去记住能够被机器直接读取的IP数串。(百度百科)例如:一个公司的 Web 网站可看作是它在网上的门户,而域名就相当于其门牌地址,通常域名都使用该公司的名称或简称。例如上面提到的微软公司的域名,类似的还有:IBM 公司的域名是 www.ibm.com、Oracle 公司的域名是 www.oracle.com、Cisco公司的域名是 www.cisco.com 等。

 

HTTP协议

 

超文本传输协议(HTTP,HyperText Transfer Protocol)是互联网上应用最为广泛的一种网络协议。所有的 WWW(万维网) 文件都必须遵守这个标准。设计 HTTP 最初的目的是为了提供一种发布和接收 HTML 页面的方法。(百度百科)

 

1.2 运输层

运输层(transport layer)的主要任务就是负责向两台主机进程之间的通信提供通用的数据传输服务。应用进程利用该服务传送应用层报文。“通用的”是指并不针对某一个特定的网络应用,而是多种应用可以使用同一个运输层服务。由于一台主机可同时运行多个线程,因此运输层有复用和分用的功能。所谓复用就是指多个应用层进程可同时使用下面运输层的服务,分用和复用相反,是运输层把收到的信息分别交付上面应用层中的相应进程。

 

运输层主要使用以下两种协议:

 

传输控制协议 TCP(Transmission Control Protocol)--提供面向连接的,可靠的数据传输服务。

用户数据协议 UDP(User Datagram Protocol)--提供无连接的,尽最大努力的数据传输服务(不保证数据传输的可靠性)。

TCP 与 UDP 的对比见问题三。

 

1.3 网络层

在 计算机网络中进行通信的两个计算机之间可能会经过很多个数据链路,也可能还要经过很多通信子网。网络层的任务就是选择合适的网间路由和交换结点, 确保数据及时传送。 在发送数据时,网络层把运输层产生的报文段或用户数据报封装成分组和包进行传送。在 TCP/IP 体系结构中,由于网络层使用 IP 协议,因此分组也叫 IP 数据报 ,简称 数据报。

 

这里要注意:不要把运输层的“用户数据报 UDP ”和网络层的“ IP 数据报”弄混。另外,无论是哪一层的数据单元,都可笼统地用“分组”来表示。

 

这里强调指出,网络层中的“网络”二字已经不是我们通常谈到的具体网络,而是指计算机网络体系结构模型中第三层的名称.

 

互联网是由大量的异构(heterogeneous)网络通过路由器(router)相互连接起来的。互联网使用的网络层协议是无连接的网际协议(Intert Protocol)和许多路由选择协议,因此互联网的网络层也叫做网际层或IP层。

 

1.4 数据链路层

数据链路层(data link layer)通常简称为链路层。两台主机之间的数据传输,总是在一段一段的链路上传送的,这就需要使用专门的链路层的协议。 在两个相邻节点之间传送数据时,数据链路层将网络层交下来的 IP 数据报组装程帧,在两个相邻节点间的链路上传送帧。每一帧包括数据和必要的控制信息(如同步信息,地址信息,差错控制等)。

 

在接收数据时,控制信息使接收端能够知道一个帧从哪个比特开始和到哪个比特结束。这样,数据链路层在收到一个帧后,就可从中提出数据部分,上交给网络层。 控制信息还使接收端能够检测到所收到的帧中有误差错。如果发现差错,数据链路层就简单地丢弃这个出了差错的帧,以避免继续在网络中传送下去白白浪费网络资源。如果需要改正数据在链路层传输时出现差错(这就是说,数据链路层不仅要检错,而且还要纠错),那么就要采用可靠性传输协议来纠正出现的差错。这种方法会使链路层的协议复杂些。

 

1.5 物理层

在物理层上所传送的数据单位是比特。 物理层(physical layer)的作用是实现相邻计算机节点之间比特流的透明传送,尽可能屏蔽掉具体传输介质和物理设备的差异。 使其上面的数据链路层不必考虑网络的具体传输介质是什么。“透明传送比特流”表示经实际电路传送后的比特流没有发生变化,对传送的比特流来说,这个电路好像是看不见的。

 

在互联网使用的各种协中最重要和最著名的就是 TCP/IP 两个协议。现在人们经常提到的TCP/IP并不一定单指TCP和IP这两个具体的协议,而往往表示互联网所使用的整个TCP/IP协议族。

1.6 总结一下

上面我们对计算机网络的五层体系结构有了初步的了解,下面附送一张七层体系结构图总结一下。

 

 TCP 三次握手和四次挥

为了准确无误地把数据送达目标处,TCP协议采用了三次握手策略。

2.1 TCP 三次握手漫画图解

 

 

2.2 为什么要三次握手两次握手可不可以

1)三次握手的目的是建立可靠的通信信道,说到通讯,简单来说就是数据的发送与接收,而三次握手最主要的目的就是双方确认自己与对方的发送与接收是正常的。

 

第一次握手:Client 什么都不能确认;Server 确认了对方发送正常

 

第二次握手:Client 确认了:自己发送、接收正常,对方发送、接收正常;Server 确认了:自己接收正常,对方发送正常

 

第三次握手:Client 确认了:自己发送、接收正常,对方发送、接收正常;Server 确认了:自己发送、接收正常,对方发送、接收正常

 

所以三次握手就能确认双发收发功能都正常,缺一不可。

2)我们知道,3次握手完成两个重要的功能,既要双方做好发送数据的准备工作(双方都知道彼此已准备好),也要允许双方就初始序列号进行协商,这个序列号在握手过程中被发送和确认。

现在把三次握手改成仅需要两次握手,死锁是可能发生的。作为例子,考虑计算机S和C之间的通信,假定C给S发送一个连接请求分组,S收到了这个分组,并发 送了确认应答分组。按照两次握手的协定,S认为连接已经成功地建立了,可以开始发送数据分组。可是,C在S的应答分组在传输中被丢失的情况下,将不知道S 是否已准备好,不知道S建立什么样的序列号,C甚至怀疑S是否收到自己的连接请求分组。在这种情况下,C认为连接还未建立成功,将忽略S发来的任何数据分 组,只等待连接确认应答分组。而S在发出的分组超时后,重复发送同样的分组。这样就形成了死锁。

2.3 为什么要传回 SYN

接收端传回发送端所发送的 SYN 是为了告诉发送端,我接收到的信息确实就是你所发送的信号了。

 

SYN 是 TCP/IP 建立连接时使用的握手信号。在客户机和服务器之间建立正常的 TCP 网络连接时,客户机首先发出一个 SYN 消息,服务器使用 SYN-ACK 应答表示接收到了这个消息,最后客户机再以 ACK(Acknowledgement[汉译:确认字符 ,在数据通信传输中,接收站发给发送站的一种传输控制字符。它表示确认发来的数据已经接受无误。 ])消息响应。这样在客户机和服务器之间才能建立起可靠的TCP连接,数据才可以在客户机和服务器之间传递。

2.4四次挥手

 

TCP的连接的拆除需要发送四个包,因此称为四次挥手(four-way handshake)。客户端或服务器均可主动发起挥手动作,在socket编程中,任何一方执行close()操作即可产生挥手操作。

客户端-发送一个 FIN,用来关闭客户端到服务器的数据传送

服务器-收到这个 FIN,它发回一 个 ACK,确认序号为收到的序号加1 。和 SYN 一样,一个 FIN 将占用一个序号

服务器-关闭与客户端的连接,发送一个FIN给客户端

客户端-发回 ACK 报文确认,并将确认序号设置为收到序号加1

2.5为什么建立连接协议是三次握手,而关闭连接却是四次握手呢?

这是因为服务端的LISTEN状态下的SOCKET当收到SYN报文的连接请求后,它可以把ACK和SYN(ACK起应答作用,而SYN起同步作用)放在一个报文里来发送。但关闭连接时,当收到对方的FIN报文通知时,它仅仅表示对方没有数据发送给你了;但未必你所有的数据都全部发送给对方了,所以你可能未必会马上会关闭SOCKET,也即你可能还需要发送一些数据给对方之后,再发送FIN报文给对方来表示你同意现在可以关闭连接了,所以它这里的ACK报文和FIN报文多数情况下都是分开发送的。

2.6为什么TIME_WAIT状态还需要等2MSL后才能返回到CLOSED状态?

(1)可靠的实现TCP全双工链接的终止。

 

这是因为虽然双方都同意关闭连接了,而且握手的4个报文也都协调和发送完毕,按理可以直接回到CLOSED状态(就好比从SYN_SEND状态到ESTABLISH状态那样);但是因为我们必须要假想网络是不可靠的,你无法保证你最后发送的ACK报文会一定被对方收到,因此对方处于LAST_ACK状态下的SOCKET可能会因为超时未收到ACK报文,而重发FIN报文,所以这个TIME_WAIT状态的作用就是用来重发可能丢失的ACK报文。

(2)允许老的重复的分节在网络中消逝。

假 设在12.106.32.254的1500端口和206.168.1.112.219的21端口之间有一个TCP连接。我们关闭这个链接,过一段时间后在 相同的IP地址和端口建立另一个连接。后一个链接成为前一个的化身。因为它们的IP地址和端口号都相同。TCP必须防止来自某一个连接的老的重复分组在连 接已经终止后再现,从而被误解成属于同一链接的某一个某一个新的化身。为做到这一点,TCP将不给处于TIME_WAIT状态的链接发起新的化身。既然 TIME_WAIT状态的持续时间是MSL的2倍,这就足以让某个方向上的分组最多存活msl秒即被丢弃,另一个方向上的应答最多存活msl秒也被丢弃。 通过实施这个规则,我们就能保证每成功建立一个TCP连接时。来自该链接先前化身的重复分组都已经在网络中消逝了。

2.7MSL与TTL

MSL是Maximum Segment Lifetime英文的缩写,中文可以译为“报文最大生存时间”,他是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。因为tcp报文(segment)是ip数据报(datagram)的数据部分,具体称谓请参见《数据在网络各层中的称呼》一文,而ip头中有一个TTL域,TTL是time to live的缩写,中文可以译为“生存时间”,这个生存时间是由源主机设置初始值但不是生存的具体时间,而是存储了一个ip数据报可以经过的最大路由数,每经过一个处理他的路由器此值就减1,当此值为0则数据报将被丢弃,同时发送ICMP报文通知源主机。RFC 793中规定MSL为2分钟,实际应用中常用的是30秒,1分钟和2分钟等。

2MSL即两倍的MSL,TCP的TIME_WAIT状态也称为2MSL等待状态,当TCP的一端发起主动关闭,在发出最后一个ACK包后,即第3次握手完成后发送了第四次握手的ACK包后就进入了TIME_WAIT状态,必须在此状态上停留两倍的MSL时间,等待2MSL时间主要目的是怕最后一个ACK包对方没收到,那么对方在超时后将重发第三次握手的FIN包,主动关闭端接到重发的FIN包后可以再发一个ACK应答包。在TIME_WAIT状态时两端的端口不能使用,要等到2MSL时间结束才可继续使用。当连接处于2MSL等待阶段时任何迟到的报文段都将被丢弃。不过在实际应用中可以通过设置SO_REUSEADDR选项达到不必等待2MSL时间结束再使用此端口。

TTL与MSL是有关系的但不是简单的相等的关系,MSL要大于等于TTL。

TCP,UDP 协议的区别

 

 

UDP 在传送数据之前不需要先建立连接,远地主机在收到 UDP 报文后,不需要给出任何确认。虽然 UDP 不提供可靠交付,但在某些情况下 UDP 确是一种最有效的工作方式(一般用于即时通信),比如: QQ 语音、 QQ 视频 、直播等等

TCP 提供面向连接的服务。在传送数据之前必须先建立连接,数据传送结束后要释放连接。 TCP 不提供广播或多播服务。由于 TCP 要提供可靠的,面向连接的传输服务(TCP的可靠体现在TCP在传递数据之前,会有三次握手来建立连接,而且在数据传递时,有确认、窗口、重传、拥塞控制机制,在数据传完后,还会断开连接用来节约系统资源),这一难以避免增加了许多开销,如确认,流量控制,计时器以及连接管理等。这不仅使协议数据单元的首部增大很多,还要占用许多处理机资源。TCP 一般用于文件传输、发送和接收邮件、远程登录等场景。

四 TCP 协议如何保证可靠传输

 

1)应用数据被分割成 TCP 认为最适合发送的数据块。

2)TCP 给发送的每一个包进行编号,接收方对数据包进行排序,把有序数据传送给应用层。

3)校验和: TCP 将保持它首部和数据的检验和。这是一个端到端的检验和,目的是检测数据在传输过程中的任何变化。如果收到段的检验和有差错,TCP 将丢弃这个报文段和不确认收到此报文段。

4)TCP 的接收端会丢弃重复的数据。

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

6)拥塞控制: 当网络拥塞时,减少数据的发送。

7)ARQ协议: 也是为了实现可靠传输的,它的基本原理就是每发完一个分组就停止发送,等待对方确认。在收到确认后再发下一个分组。

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

4.1 ARQ协议

自动重传请求(Automatic Repeat-reQuest,ARQ)是OSI模型中数据链路层和传输层的错误纠正协议之一。它通过使用确认和超时这两个机制,在不可靠服务的基础上实现可靠的信息传输。如果发送方在发送后一段时间之内没有收到确认帧,它通常会重新发送。ARQ包括停止等待ARQ协议和连续ARQ协议。

 

停止等待ARQ协议

停止等待协议是为了实现可靠传输的,它的基本原理就是每发完一个分组就停止发送,等待对方确认(回复ACK)。如果过了一段时间(超时时间后),还是没有收到 ACK 确认,说明没有发送成功,需要重新发送,直到收到确认后再发下一个分组;

在停止等待协议中,若接收方收到重复分组,就丢弃该分组,但同时还要发送确认;

优点: 简单

 

缺点: 信道利用率低,等待时间长

 

  1. 无差错情况:

 

发送方发送分组,接收方在规定时间内收到,并且回复确认.发送方再次发送。

2) 出现差错情况(超时重传): 停止等待协议中超时重传是指只要超过一段时间仍然没有收到确认,就重传前面发送过的分组(认为刚才发送过的分组丢失了)。因此每发送完一个分组需要设置一个超时计时器,其重传时间应比数据在分组传输的平均往返时间更长一些。这种自动重传方式常称为 自动重传请求 ARQ 。另外在停止等待协议中若收到重复分组,就丢弃该分组,但同时还要发送确认。连续 ARQ 协议 可提高信道利用率。发送维持一个发送窗口,凡位于发送窗口内的分组可连续发送出去,而不需要等待对方确认。接收方一般采用累积确认,对按序到达的最后一个分组发送确认,表明到这个分组位置的所有分组都已经正确收到了。

 

 

 

 

 

 

 

 

3) 确认丢失和确认迟到

 

确认丢失:确认消息在传输过程丢失

 

 

 

 

当A发送M1消息,B收到后,B向A发送了一个M1确认消息,但却在传输过程中丢失。而A并不知道,在超时计时过后,A重传M1消息,B再次收到该消息后采取以下两点措施:

 

I.丢弃这个重复的M1消息,不向上层交付。

Ii.向A发送确认消息。(不会认为已经发送过了,就不再发送。A能重传,就证明B的确认消息丢失)。

确认迟到 :确认消息在传输过程中迟到。

 

 A发送M1消息,B收到并发送确认。在超时时间内没有收到确认消息,A重传M1消息,B仍然收到并继续发送确认消息(B收到了2份M1)。此时A收到了B第二次发送的确认消息。接着发送其他数据。过了一会,A收到了B第一次发送的对M1的确认消息(A也收到了2份确认消息)。处理如下:

I.A收到重复的确认后,直接丢弃。

II.B收到重复的M1后,也直接丢弃重复的M1。

连续ARQ协议

连续 ARQ 协议可提高信道利用率。发送方维持一个发送窗口,凡位于发送窗口内的分组可以连续发送出去,而不需要等待对方确认。接收方一般采用累计确认,对按序到达的最后一个分组发送确认,表明到这个分组为止的所有分组都已经正确收到了。

 

优点: 信道利用率高,容易实现,即使确认丢失,也不必重传。

 

缺点: 不能向发送方反映出接收方已经正确收到的所有分组的信息。 比如:发送方发送了 5条 消息,中间第三条丢失(3号),这时接收方只能对前两个发送确认。发送方无法知道后三个分组的下落,而只好把后三个全部重传一次。这也叫 Go-Back-N(回退 N),表示需要退回来重传已经发送过的 N 个消息。

4.2 滑动窗口和流量控制

TCP 利用滑动窗口实现流量控制。流量控制是为了控制发送方发送速率,保证接收方来得及接收。 接收方发送的确认报文中的窗口字段可以用来控制发送方窗口大小,从而影响发送方的发送速率。将窗口字段设置为 0,则发送方不能发送数据。

4.3 拥塞控制

在某段时间,若对网络中某一资源的需求超过了该资源所能提供的可用部分,网络的性能就要变坏。这种情况就叫拥塞。拥塞控制就是为了防止过多的数据注入到网络中,这样就可以使网络中的路由器或链路不致过载。拥塞控制所要做的都有一个前提,就是网络能够承受现有的网络负荷。拥塞控制是一个全局性的过程,涉及到所有的主机,所有的路由器,以及与降低网络传输性能有关的所有因素。相反,流量控制往往是点对点通信量的控制,是个端到端的问题。流量控制所要做到的就是抑制发送端发送数据的速率,以便使接收端来得及接收。

 

为了进行拥塞控制,TCP 发送方要维持一个 拥塞窗口(cwnd) 的状态变量。拥塞控制窗口的大小取决于网络的拥塞程度,并且动态变化。发送方让自己的发送窗口取为拥塞窗口和接收方的接受窗口中较小的一个。

 

TCP的拥塞控制采用了四种算法,即 慢开始 、拥塞避免 、快重传 和 快恢复。在网络层也可以使路由器采用适当的分组丢弃策略(如主动队列管理 AQM),以减少网络拥塞的发生。

慢开始:慢开始算法的思路是当主机开始发送数据时,如果立即把大量数据字节注入到网络,那么可能会引起网络阻塞,因为现在还不知道网络的符合情况。经验表明,较好的方法是先探测一下,即由小到大逐渐增大发送窗口,也就是由小到大逐渐增大拥塞窗口数值。cwnd初始值为1,每经过一个传播轮次,cwnd加倍。

 

拥塞避免: 拥塞避免算法的思路是让拥塞窗口cwnd缓慢增大,即每经过一个往返时间RTT就把发送放的cwnd加1.

快重传与快恢复:在 TCP/IP 中,快速重传和恢复(fast retransmit and recovery,FRR)是一种拥塞控制算法,它能快速恢复丢失的数据包。没有 FRR,如果数据包丢失了,TCP 将会使用定时器来要求传输暂停。在暂停的这段时间内,没有新的或复制的数据包被发送。有了 FRR,如果接收机接收到一个不按顺序的数据段,它会立即给发送机发送一个重复确认。如果发送机接收到三个重复确认,它会假定确认件指出的数据段丢失了,并立即重传这些丢失的数据段。有了 FRR,就不会因为重传时要求的暂停被耽误。当有单独的数据包丢失时,快速重传和恢复(FRR)能最有效地工作。当有多个数据信息包在某一段很短的时间内丢失时,它则不能很有效地工作。

 

 

五 在浏览器中输入url地址 ->> 显示主页的过程

 

总体来说分为以下几个过程:

DNS解析

TCP连接

发送HTTP请求

服务器处理请求并返回HTTP报文

浏览器解析渲染页面

连接结束

具体可以参考下面这篇文章:

https://segmentfault.com/a/1190000006879700

六 状态码

 

 

七 HTTP协议

7.1 HTTP协议与各种协议的关系

 

7.2 HTTP长连接,短连接

在HTTP/1.0中默认使用短连接。也就是说,客户端和服务器每进行一次HTTP操作,就建立一次连接,任务结束就中断连接。当客户端浏览器访问的某个HTML或其他类型的Web页中包含有其他的Web资源(如JavaScript文件、图像文件、CSS文件等),每遇到这样一个Web资源,浏览器就会重新建立一个HTTP会话。

 

而从HTTP/1.1起,默认使用长连接,用以保持连接特性。使用长连接的HTTP协议,会在响应头加入这行代码:

 

Connection:keep-alive

在使用长连接的情况下,当一个网页打开完成后,客户端和服务器之间用于传输HTTP数据的TCP连接不会关闭,客户端再次访问这个服务器时,会继续使用这一条已经建立的连接。Keep-Alive不会永久保持连接,它有一个保持时间,可以在不同的服务器软件(如Apache)中设定这个时间。实现长连接需要客户端和服务端都支持长连接。

 

HTTP协议的长连接和短连接,实质上是TCP协议的长连接和短连接。

 

—— 《HTTP长连接、短连接究竟是什么?》

https://www.cnblogs.com/gotodsp/p/6366163.html

7.3 HTTP断点续传

HTTP1.1协议(RFC2616)中定义了断点续传相关的HTTP头 Range和Content-Range字段,一个最简单的断点续传实现大概如下: 
  1客户端下载一个1024K的文件,已经下载了其中512K 
  2网络中断,客户端请求续传,因此需要在HTTP头中申明本次需要续传的片段: 
        Range:bytes=512000- 
      这个头通知服务端从文件的512K位置开始传输文件 
  3 服务端收到断点续传请求,从文件的512K位置开始传输,并且在HTTP头中增加: 
    Content-Range:bytes 512000-/1024000 
    并且此时服务端返回的HTTP状态码应该是206,而不是200。 

7.4 HTTP是不保存状态的协议,如何保存用户状态?

HTTP 是一种不保存状态,即无状态(stateless)协议。也就是说 HTTP 协议自身不对请求和响应之间的通信状态进行保存。那么我们保存用户状态呢?Session 机制的存在就是为了解决这个问题,Session 的主要作用就是通过服务端记录用户的状态。典型的场景是购物车,当你要添加商品到购物车的时候,系统不知道是哪个用户操作的,因为 HTTP 协议是无状态的。服务端给特定的用户创建特定的 Session 之后就可以标识这个用户并且跟踪这个用户了(一般情况下,服务器会在一定时间内保存这个 Session,过了时间限制,就会销毁这个Session)。

 

在服务端保存 Session 的方法很多,最常用的就是内存和数据库(比如是使用内存数据库redis保存)。既然 Session 存放在服务器端,那么我们如何实现 Session 跟踪呢?大部分情况下,我们都是通过在 Cookie 中附加一个 Session ID 来方式来跟踪。

 

Cookie 被禁用怎么办?

 

最常用的就是利用 URL 重写把 Session ID 直接附加在URL路径的后面。

7.5 Cookie的作用是什么?和Session有什么区别?

Cookie 和 Session都是用来跟踪浏览器用户身份的会话方式,但是两者的应用场景不太一样。

Cookie 一般用来保存用户信息 比如①我们在 Cookie 中保存已经登录过得用户信息,下次访问网站的时候页面可以自动帮你登录的一些基本信息给填了;②一般的网站都会有保持登录也就是说下次你再访问网站的时候就不需要重新登录了,这是因为用户登录的时候我们可以存放了一个 Token 在 Cookie 中,下次登录的时候只需要根据 Token 值来查找用户即可(为了安全考虑,重新登录一般要将 Token 重写);③登录一次网站后访问网站其他页面不需要重新登录。Session 的主要作用就是通过服务端记录用户的状态。 典型的场景是购物车,当你要添加商品到购物车的时候,系统不知道是哪个用户操作的,因为 HTTP 协议是无状态的。服务端给特定的用户创建特定的 Session 之后就可以标识这个用户并且跟踪这个用户了。

Cookie 数据保存在客户端(浏览器端),Session 数据保存在服务器端。

Cookie 存储在客户端中,而Session存储在服务器上,相对来说 Session 安全性更高。如果使用 Cookie 的一些敏感信息不要写入 Cookie 中,最好能将 Cookie 信息加密然后使用到的时候再去服务器端解密。

7.6 URI和URL的区别是什么?

URI(Uniform Resource Identifier) 是同一资源标志符,可以唯一标识一个资源。

URL(Uniform Resource Location) 是同一资源定位符,可以提供该资源的路径。它是一种具体的 URI,即 URL 可以用来标识一个资源,而且还指明了如何 locate 这个资源。

URI的作用像身份证号一样,URL的作用更像家庭住址一样。URL是一种具体的URI,它不仅唯一标识资源,而且还提供了定位该资源的信息。

7.7 HTTP 和 HTTPS 的区别?

1)端口 :HTTP的URL由“http://”起始且默认使用端口80,而HTTPS的URL由“https://”起始且默认使用端口443。

2)安全性和资源消耗: HTTP协议运行在TCP之上,所有传输的内容都是明文,客户端和服务器端都无法验证对方的身份。HTTPS是运行在SSL/TLS之上的HTTP协议,SSL/TLS 运行在TCP之上。所有传输的内容都经过加密,加密采用对称加密,但对称加密的密钥用服务器方的证书进行了非对称加密。所以说,HTTP 安全性没有 HTTPS高,但是 HTTPS 比HTTP耗费更多服务器资源。

对称加密:密钥只有一个,加密解密为同一个密码,且加解密速度快,典型的对称加密算法有DES、AES等;

非对称加密:密钥成对出现(且根据公钥无法推知私钥,根据私钥也无法推知公钥),加密解密使用不同密钥(公钥加密需要私钥解密,私钥加密需要公钥解密),相对对称加密速度较慢,典型的非对称加密算法有RSA、DSA等。

八 常用的熟知端口号

 

九 HTTPS

 

三.算法

1.所有排序算法

//冒泡排序

void bubbleSort(int a[], int n) {

    for (int i = 0; i < n; i++)

        for (int j = 1; j < n - i; j++) {

            if (a[j - 1] > a[j])

                swap(a[j - 1], a[j]);

        }

}

 

//插入排序

void insertSort(int a[], int n) {

    for (int i = 1; i < n; i++) {

        for (int j = i - 1; j >= 0; --j) {

            if (a[j] > a[j + 1])

                swap(a[j], a[j + 1]);

        }

    }

}

 

//选择排序

void selectSort(int a[], int n) {

 

    for (int i = 0; i < n; i++) {

        int min = a[i];

        int flag = i;

        for (int j = i; j < n; j++) {

            if (a[j] < min) {

                min = a[j];

                flag = j;

            }

        }

        swap(a[i], a[flag]);

    }

}

/**************************************/

/*************************************/

int partition(int a[], int start, int end) {

    int pivot = a[end];

    int i = start - 1;

    int j = start;

    while (j < end) {

        if (a[j] < pivot) {

            ++i;

            swap(a[j], a[i]);

        }

        j++;

    }

    swap(a[i + 1], a[end]);

    return i + 1;

}

 

//快速排序

void quickSort(int a[], int start, int end) {

    //int lhs = start;

    //int rhs = end;

    if (start < end) {

        int q = partition(a, start, end);

        quickSort(a, start, q - 1);

        quickSort(a, q + 1, end);

    }

}

 

//归并排序

void merge(int a[], int start, int mid, int end) {

    int temp[end + 1];

    int i = start;

    int j = mid + 1;

    int index = 0;

    while (i <= mid && j <= end) {

        if (a[i] > a[j])

            temp[index++] = a[j++];

        else

            temp[index++] = a[i++];

    }

    while (i <= mid) {

        temp[index++] = a[i++];

    }

    while (j <= end)

        temp[index++] = a[j++];

    for (int i = 0, j = start; j <= end; i++, j++)

        a[j] = temp[i];

}

 

void mergeSort(int a[], int start, int end) {

    if (start < end) {

        int mid = (start + end) / 2;

        mergeSort(a, start, mid);

        mergeSort(a, mid + 1, end);

        merge(a, start, mid, end);

    }

}

 

void heapAdjust(int a[], int start, int len) {

    int child;     // 子结点的位置 = 2 * 父结点的位置 + 1

    // 得到子结点中键值较大的结点

    for (; 2 * start + 1 < len; start = child) {

        child = 2 * start + 1;

        if (child < len - 1 && a[child + 1] > a[child])

            child++;

        // 如果较大的子结点大于父结点那么把较大的子结点往上移动,替换它的父结点

        if (a[start] < a[child])

            swap(a[start], a[child]);

        else

            break;

    }

 

}

 

//堆排序

void heapSort(int a[], int n) {

    // 调整序列的前半部分元素,调整完之后第一个元素是序列的最大的元素

    for (int i = n / 2 - 1; i >= 0; i--)

        heapAdjust(a, i, n);

    for (int i = n - 1; i > 0; i--) {

        // 将第1个元素与当前最后一个元素交换,保证当前的最后一个位置的元素都是现在的这个序列中最大的

        swap(a[0], a[i]);

        // 不断缩小调整heap的范围,每一次调整完毕保证第一个元素是当前序列的最大值

        heapAdjust(a, 0, i);

    }

}

//希尔排序

void shellSort(int a[], int size){

    int h = 1;

    while (h < size / 3) {

        h = 3 * h + 1;

    }

    while (h >= 1) {

        for (int i = h; i < size; i++) {

            for (size_t j = i; j >= h && a[j] < a[j - h]; j -= h) {

                std::swap(a[j], a[j - h]);

            }

        }

        h = h / 3;

    }

   

}

 

  1. BFS、DFS
  2. Dijkstra

4.Kmp算法

5.计算机计算除法的方式(乘法)

6.背包

7.红黑树

 

  • Python、go
  • 数据库

1.数据库建立索引的规则

普遍的规则:

 

1)表的主键、外键必须有索引;

 

2)数据量超过300的表应该有索引;

 

3)经常与其他表进行连接的表,在连接字段上应该建立索引;

 

4)经常出现在Where子句中的字段,特别是大表的字段,应该建立索引;

 

5)索引应该建在选择性高的字段上;

 

6)索引应该建在小字段上,对于大的文本字段甚至超长字段,不要建索引;

 

7)复合索引的建立需要进行仔细分析;尽量考虑用单字段索引代替:

 

   A、正确选择复合索引中的主列字段,一般是选择性较好的字段;

 

   B、复合索引的几个字段是否经常同时以AND方式出现在Where子句中?单字段查询是否极少甚至没有?如果是,则可以建立复合索引;否则考虑单字段索引;

 

   C、如果复合索引中包含的字段经常单独出现在Where子句中,则分解为多个单字段索引;

 

   D、如果复合索引所包含的字段超过3个,那么仔细考虑其必要性,考虑减少复合的字段;

 

   E、如果既有单字段索引,又有这几个字段上的复合索引,一般可以删除复合索引;

 

8)频繁进行数据操作的表,不要建立太多的索引;

 

9)删除无用的索引,避免对执行计划造成负面影响;

 

   以上是一些普遍的建立索引时的判断依据。一言以蔽之,索引的建立必须慎重,对每个索引的必要性都应该经过仔细分析,要有建立的依据。因为太多的索引与不充分、不正确的索引对性能都毫无益处:在表上建立的每个索引都会增加存储开销,索引对于插入、删除、更新操作也会增加处理上的开销。另外,过多的复合索引,在有单字段索引的情况下,一般都是没有存在价值的;相反,还会降低数据增加删除时的性能,特别是对频繁更新的表来说,负面影响更大

六.其他

1.多线程调度,效率最高

有多种算法

  1. 先来先服务
  2. 最短优先
  3. 最高优先权优先(FPF)调度算法

此算法常被用于批处理系统中,作为作业调度算法,也作为多种操作系统中的进程调度算法,还可用于实时系统中。当把该算法用于作业调度时,系统将从后备队列中选择若干个优先权最高的作业装入内存。当用于进程调度时,该算法是把处理机分配给就绪队列中优先权最高的进程,这时,又可进一步把该算法分成如下两种。

第一种:非抢占式优先权算法

在这种方式下,系统一旦把处理机分配给就绪队列中优先权最高的进程后,该进程便一直执行下去,直至完成;或因发生某事件使该进程放弃处理机时,系统方可再将处理机重新分配给另一优先权最高的进程。这种调度算法主要用于批处理系统中;也可用于某些对实时性要求不严的实时系统中。

第二种:抢占式优先权调度算法

在这种方式下,系统同样是把处理机分配给优先权最高的进程,使之执行。但在其执行期间,只要又出现了另一个其优先权更高的进程,进程调度程序就立即停止当前进程(原优先权最高的进程)的执行,重新将处理机分配给新到的优先权最高的进程。因此,在采用这种调度算法时,是每当系统中出现一个新的就绪进程i 时,就将其优先权Pi与正在执行的进程j 的优先权Pj进行比较。如果Pi≤Pj,原进程Pj便继续执行;但如果是Pi>Pj,则立即停止Pj的执行,做进程切换,使i 进程投入执行。显然,这种抢占式的优先权调度算法能更好地满足紧迫作业的要求,故而常用于要求比较严格的实时系统中,以及对性能要求较高的批处理和分时系统中。

  1. 高响应比优先调度算法

该优先权的变化规律可描述为:

 

 

由于等待时间与服务时间之和就是系统对该作业的响应时间,故该优先权又相当于响应比RP。据此,又可表示为:

 

 

由上式可以看出:

(1) 如果作业的等待时间相同,则要求服务的时间愈短,其优先权愈高,因而该算法有利于短作业。

(2) 当要求服务的时间相同时,作业的优先权决定于其等待时间,等待时间愈长,其优先权愈高,因而它实现的是先来先服务。

(3) 对于长作业,作业的优先级可以随等待时间的增加而提高,当其等待时间足够长时,其优先级便可升到很高,从而也可获得处理机。

简言之,该算法既照顾了短作业,又考虑了作业到达的先后次序,不会使长作业长期得不到服务。因此,该算法实现了一种较好的折衷。当然,在利用该算法时,每要进行调度之前,都须先做响应比的计算,这会增加系统开销。

5)3.1 时间片轮转法

在早期的时间片轮转法中,系统将所有的就绪进程按先来先服务的原则排成一个队列,每次调度时,把CPU 分配给队首进程,并令其执行一个时间片。时间片的大小从几ms 到几百ms。当执行的时间片用完时,由一个计时器发出时钟中断请求,调度程序便据此信号来停止该进程的执行,并将它送往就绪队列的末尾;然后,再把处理机分配给就绪队列中新的队首进程,同时也让它执行一个时间片。这样就可以保证就绪队列中的所有进程在一给定的时间内均能获得一时间片的处理机执行时间。换言之,系统能在给定的时间内响应所有用户的请求。

  1. 多级反馈队列调度算法

前面介绍的各种用作进程调度的算法都有一定的局限性。如短进程优先的调度算法,仅照顾了短进程而忽略了长进程,而且如果并未指明进程的长度,则短进程优先和基于进程长度的抢占式调度算法都将无法使用。而多级反馈队列调度算法则不必事先知道各种进程所需的执行时间,而且还可以满足各种类型进程的需要,因而它是目前被公认的一种较好的进程调度算法。在采用多级反馈队列调度算法的系统中,调度算法的实施过程如下所述。

(1) 应设置多个就绪队列,并为各个队列赋予不同的优先级。第一个队列的优先级最高,第二个队列次之,其余各队列的优先权逐个降低。该算法赋予各个队列中进程执行时间片的大小也各不相同,在优先权愈高的队列中,为每个进程所规定的执行时间片就愈小。例如,第二个队列的时间片要比第一个队列的时间片长一倍,……,第i+1个队列的时间片要比第i个队列的时间片长一倍。

(2) 当一个新进程进入内存后,首先将它放入第一队列的末尾,按FCFS原则排队等待调度。当轮到该进程执行时,如它能在该时间片内完成,便可准备撤离系统;如果它在一个时间片结束时尚未完成,调度程序便将该进程转入第二队列的末尾,再同样地按FCFS原则等待调度执行;如果它在第二队列中运行一个时间片后仍未完成,再依次将它放入第三队列,……,如此下去,当一个长作业(进程)从第一队列依次降到第n队列后,在第n 队列便采取按时间片轮转的方式运行。

(3) 仅当第一队列空闲时,调度程序才调度第二队列中的进程运行;仅当第1~(i-1)队列均空时,才会调度第i队列中的进程运行。如果处理机正在第i队列中为某进程服务时,又有新进程进入优先权较高的队列(第1~(i-1)中的任何一个队列),则此时新进程将抢占正在运行进程的处理机,即由调度程序把正在运行的进程放回到第i队列的末尾,把处理机分配给新到的高优先权进程。

7)电梯调度算法

2.文件一致性验证

https://www.kancloud.cn/swordfly/linux/482799

  • C++
  1. 多态的实现

多态简单来说,是指在继承层次中,父类的指针可以具有多种形态——当它指向某个子类对象是,通过它能够调用到子类的函数,而非父类的函数。

一个接口,多种实现。使用继承+虚函数,使用virtual关键字修饰成员函数。New一个子类对象,赋给父类的指针和引用。(实现机制:虚函数表)

2.虚函数

虚函数表属于类,类的所有对象共享这个类的虚函数表。虚函数表存储在进程的只读数据段。每个对象都有一个默认的指针vptr指向虚函数表。确定vptr指向的具体对象类型,加上偏移量,就可以找到具体调用的函数。虚函数指针一般都放在对象内存布局的第一个位置上,这是为了保证在多层继承或多重继承的情况下能以最高效率取到虚函数表。当vptr位于对象内存最前面是,对象的地址即为虚函数指针地址。我们可以取得虚函数指针的地址。

3内存对齐

内存对齐只是指数据在内存时的起始地址是否是某个值得整数倍。如果只是放在内存中,是否对齐本身并没有什么问题。未提示读取、写入的时候。访问一个不对齐的数据可能会导致程序运行效率慢,结果出错,甚至是程序宕掉。

原则:

(1)数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如说是数组,结构体等)的整数倍开始(比如int在32位机为4字节, 则要从4的整数倍地址开始存储),基本类型不包括struct/class/uinon。

(2)结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部"最宽基本类型成员"的整数倍地址开始存储.(struct a里存有struct b,b里有char,int ,double等元素,那b应该从8的整数倍开始存储.)。

(3)收尾工作:结构体的总大小,也就是sizeof的结果,.必须是其内部最大成员的"最宽基本类型成员"的整数倍.不足的要补齐.(基本类型不包括struct/class/uinon)。

(4)sizeof(union),以结构里面size最大元素为union的size,因为在某一时刻,union只有一个成员真正存储于该地址

4.c++对象模型

在c++中有两种数据成员,静态和非静态的,有三种成员函数:静态、非静态和虚函数。

5.malloc/free和new/delete的区别

 1)new/delete是c++里才有的。

 2)new/delete通常来说是操作符,就是"+","-"一样。

 3)new/delete是可以重载的,而重载之后,就成为了函数。

 4)malloc在申请内存的时候,必须要提供申请的长度,而且返回的指针是void*型,必须要强转成需要的类型。

5)当new/delete在类中被重载的时候,可以自定义申请过程,比如记录所申请内存的总长度,以及跟踪每个对象的指针。

6) new/delete,其实内部也调用了malloc/free。

7)new 在申请内存的同时,还会调用对象的构造函数,而 malloc 只会申请内存;同样,delete 在释放内存之前,会调用对象的析构函数,而 free 只会释放内存。

共同点:

1) 都必须配对使用。

2) 都是申请内存,释放内存。

3) free和delete可以释放NULL指针。

注意点:

new/delete与malloc/free不能混合使用。

6.const关键字

Const对象一旦创建其值就无法改变,所以const对象必须初始化。

7.指针和引用的区别:

指针存储的是所指变量的地址,而引用是变量的别名,引用不能为空,必须初始化。

8.c++堆和栈的区别

栈:由程序自动向操作系统申请分配以及回收,速度快,使用方便。但程序员无法控制。若分配失败,则提示栈溢出错误。注意,const局部变量也存储在栈区内,栈区向地址减小的方向增长。

堆:程序员向操作系统申请一块内存,当系统收到程序的申请时,会遍历一个记录空闲内存地址的链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。分配的速度较慢地址不连续,容易碎片化。此外,由程序员申请,同时也必须由程序员负责销毁,否则则导致内存泄露。

9.关键字static

Static变量:存储在静态存储区。作用:第一个作用是限定作用域;第二个作用是保持变量内容持久化。

1. C 语言中static的用法:

  1. 全局静态变量:

   用法:在全局变量前加上关键字static,全局变量就定义成一个全局静态变量。

   内存中的位置:静态存储区,在整个程序运行期间一直存在。

   初始化:未经初始化的全局静态变量会被自动初始化为0(自动对象的值是任意的,除非他被显式初始化)。

   作用域:全局静态变量在声明他的文件之外是不可见得,准确的说是从定义之处开始,到文件结尾。

  1. 局部静态变量:

   用法:在局部变量前加上关键字static,局部变量就成为一个局部静态变量。

   内存的位置:静态存储区。

   初始化:未经初始化的全局静态变量会被自动初始化为0.

   作用域:作用域仍未局部作用域,当定义它的函数或者语句块结束的时候,作用域结束。但是当局部静态变量离开作用域后,并没有销毁,而是仍然驻留在内存当中,只不过我们不能再对它进行访问,直到该函数再次被调用,并且值不变。

  1. 静态函数:

在函数返回类型前加关键字static,函数就定义成静态成员函数。函数的定义和生命在默认情况下都是extern的,但静态函数只是在声明他的文件当中可见,不能被其他文件所用。

  1. C++中static的用法:
  1. 类的静态成员:

class A{

private:

     static int val;

};
在cpp中必须对它进行初始化,初始化时使用作用域运算符来标明他所属类,其属于该类的所有成员共有,只有一个拷贝。

  1. 类的静态成员函数:

class A{

private:

    static int func(int x);

}

实现的时候也不需要static的修饰,因为static是声明性关键字;类的静态函数是该类的范畴内的全局函数,不能访问类的私有成员,只能访问类的静态成员,不需要类的实例即可调用;实际上,他就是增加了类的访问权限的全局函数。

void A::func(int);

静态成员函数可以继承和覆盖,但无法是虚函数。

3)只在cpp内有效的全局变量:在cpp文件的全局范围内声明:static int val =0;

这个变量的含义是该cpp内有效,但是其他的cpp文件不能访问这个变量;如果两个cpp文件声明了同名的全局静态变量,那么他们实际上是独立的两个变量。

  1. 只在cpp内有效的全局函数:

函数的实现使用static修饰,那么这个函数只可在本cpp内使用,不会同其他cpp中的同名函数引起冲突。   

warning:不要在头文件中声明static的全局函数,不要在cpp内声明非static的全局函数,如果你要在多个cpp中复用该函数,就把它的声明提到头文件里去,否则cpp内部声明需加上static修饰。(说的逻辑不对?)

10.用过的设计模式,举几个例子

singleton:static成员函数中构造local static变量的方法来实现,因为static成员只能初始化一次。

template<typename T>

class Singleton

{

public:

    static T& getInstance()

    {

        if (!value_)

        {

            value_ = new T();

        }

        return *value_;

    }

private:

    Singleton();

    ~Singleton();

    static T* value_;

};

template<typename T>

T* Singleton<T>::value_ = NULL;

11.c++深拷贝和浅拷贝

浅拷贝,即在定义一个类A,使用类似A obj;  A obj1(obj);或者A obj1 = obj; 时候,由于没有自定义拷贝构造函数,C++编译器自动会产生一个默认的拷贝构造函数。这个默认的拷贝构造函数采用的是“位拷贝”(浅拷贝),而非“值拷贝”(深拷贝)的

方式,如果类中含有指针变量,默认的拷贝构造函数必定出错。

用一句简单的话来说就是浅拷贝,只是对指针的拷贝,拷贝后两个指针指向同一个内存空间,深拷贝不但对指针进行拷贝,而且对指针指向的内容进行拷贝,经深拷贝后的指针是指向两个不同地址的指针。

假如有一个成员变量的指针,char *m_data;

其一,浅拷贝只是拷贝了指针,使得两个指针指向同一个地址,这样在对象块结束,调用函数析构的时,会造成同一份资源析构2次,即delete同一块内存2次,造成程序崩溃。

其二,浅拷贝使得obj.m_data和obj1.m_data指向同一块内存,任何一方的变动都会影响到另一方。

其三,在释放内存的时候,会造成obj1.m_data原有的内存没有被释放(这句话,刚开始我不太理解,如果没有走自定义的拷贝构造函数,申请内存空间,A obj1(obj);也不走默认构造函数,走的是默认的拷贝构造函数,何来分配空间直说,更不会造成obj1.m_data原有的内存没有被释放,这里刚开始我一直有疑问),造成内存泄露。

事实是这样的,当delete obj.m_data, obj.m_data内存被释放后,由于之前obj.m_data和obj1.m_data指向的是同一个内存空间,obj1.m_data所指的空间不能在被利用了,delete obj1.m_data也不会成功,一直已经无法操作该空间,所以导致内存泄露。

深拷贝采用了在堆内存中申请新的空间来存储数据,这样每个可以避免指针悬挂12.volatile的作用

volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。声明时语法:int volatile vInt; 当要求使用 volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。而且读取的数据立刻被保存。例如:

 

volatile int i=10;

int a = i;

...

// 其他代码,并未明确告诉编译器,对 i 进行过操作

5

int b = i;

 volatile 指出 i 是随时可能发生变化的,每次使用它的时候必须从 i的地址中读取,因而编译器生成的汇编代码会重新从i的地址读取数据放在 b 中。而优化做法是,由于编译器发现两次从 i读数据的代码之间的代码没有对 i 进行过操作,它会自动把上次读的数据放在 b 中。而不是重新从 i 里面读。这样以来,如果 i是一个寄存器变量或者表示一个端口数据就容易出错,所以说 volatile 可以保证对特殊地址的稳定访问。

13.构造函数和析构函数可以调用虚函数吗

构造函数跟析构函数里面都可以调用虚函数,编译器不会报错。C++ primer中说到最好别用。

由于类的构造次序是由基类到派生类,所以在构造函数中调用虚函数,虚函数是不会呈现出多态的。

类的析构是从派生类到基类,当调用继承层次中某一层次的类的析构函数时意味着其派生类部分已经析构掉,所以也不会呈现多态。

因此如果在基类中声明的纯虚函数并且在基类的析构函数中调用之,编译器会发生错误。

 

14.友元函数

友元函数不含this指针,所以友元函数访问对象中的成员要通过对象名。友元函数可以在类内实现也可以在类外实现。

15.构造函数和拷贝构造函数能否为虚函数

不能,只有析构函数可以定义为虚函数,构造函数不能定义为虚函数。构造函数是在对象完全构造之前运行的,在构造函数运行的时候,对象的动态类型还不完整。

16.类对象所占内存空间

空的类是会占用内存空间的,而且大小是1,原因是C++要求每个实例在内存中都有独一无二的地址。

1)类内部的成员变量:

* 普通的变量:是要占用内存的,但是要注意对齐原则(这点和struct类型很相似)。

* static修饰的静态变量:不占用内存,原因是编译器将其放在全局变量区。

2)类内部的成员函数:

* 普通函数:不占用内存。

* 虚函数:要占用4个字节,用来指定虚函数的虚拟函数表的入口地址。所以一个类的虚函数所占用的地址是不变的,和虚函数的个数是没有关系。

17.关于析构函数和纯虚函数

纯虚析构函数和普通纯虚函数的区别在于,纯虚析构函数需要提供函数的实现,而一般纯虚函数不能有实现,原因在于,纯虚析构函数最终需要被调用,以析构基类对象,虽然是抽象类没有实体。而如果不提供该析构函数的实现,将使得在析构过程中,析构无法完成而导致析构异常的问题。

通过父类指针去析构子类对象,分三种情况:

1)父类的析构函数不是虚函数,这种情况下,将只会调用A的析构函数而不会调用子类的析构函数,前面的文章中有提到过,非虚函数是通过类型来寻址的,这样的析构将会导致析构畸形,这是笔试面试中经常遇到的考点。

2)父类的析构函数是普通的虚函数,这种情况下,会很正常,从子类一直析构到基类,最后完成析构,这就是针对A的修改方法之一。

3)父类的析构函数是纯析构函数,重点,在这种情况之下,由于析构函数首先是虚函数,所以会按2的方法从子类一直析构到父类,但是,又由于父类的析构函数是纯虚函数,如果没有实现体,导致父类无法析构,最终也导致了析构畸形,因此,特殊的地方就在于这里,纯虚析构函数需要提供一个实现体,以完成对象的析构。

18.数组名是数组首元素的地址

a[10];

 a=&a[0];

 a+2=&a[2];

 *(a+2)=a[2];

19.大端模式和小端模式

举一个例子,比如数字0x12 34 56 78在内存中的表示形式。

1)大端模式:Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。

(其实大端模式才是我们直观上认为的模式,和字符串存储的模式差类似)

低地址 --------------------> 高地址

0x12  |  0x34  |  0x56  |  0x78

2)小端模式:Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。

低地址 --------------------> 高地址

0x78  |  0x56  |  0x34  |  0x12

20.void *

void 指针可以指向任意类型的数据,就是说可以用任意类型的指针对void指针赋值

 

  1. 左值、右值、右值引用

1.左值:

对于早期的c语言,提到左值意味着:

  1. 它指定一个对象,所以引用内存中的地址。
  2. 它可用在赋值运算符的左侧,左值(lvalue)中的l源自left。

但是后来,标准中新增了const限定符。用const创建的变量不可修改。因此,const标识符满足上面的第一项,但是不满足第二项。一方面c继续把标识对象的表达式定义为左值,一方面某些左值却不能放在赋值运算符的左侧。此时,标准对左值的定义已经不能满足当前的状况。为此,c标准新增了一个术语:可修改的左值(modifiable lvalue),用于标识可修改的对象。所以赋值运算符的左侧应该是可修改的左值。

  1. 右值:

指的是能赋值给可修改左值的量,且本身不是左值。

考虑下面的语句:

bmw=2002;

这里bmw是可修改的左值,2002是右值。右值可以是常量、变量或其他可求值的表达式(如函数调用)。实际上,当前标准在描述这一概念是使用的是表达式的值(value of an expression),而不是右值。

示例:

int ex;

int why;

int zee;

const int two=2;

why=42;

zee=why;

ex=tow*(why+zee);

这里,ex、why和zee都是可修改的左值(或对象定位值),它们可用于赋值运算符的左侧和右侧。two是不可修改的左值,它只能用于赋值运算符的右侧(在该例中,two被初始化为2,这里的=运算符表示初始化而不是赋值,因此并未违反规则)。同时42是右值,它不能引用某指定内存的位置。另外,why和zee是可修改的左值,表达式(why+zee)是右值,该表达式不能表示特定内存位置,而且也不能给它赋值。它只是程序计算的一个临时值,在计算完毕后便会被丢弃。

  1. 右值引用

为了支持移动操作,c++新标准引入了一种新的引用类型——右值引用。所谓右值引用就是必须绑定到右值的引用。我们通过&&而不是&来获得右值引用。如我们将要看到的,右值引用有一个重要的性质:只能绑定到一个将要销毁的对象。因此,我们可以自由地将一个右值引用的资源“移动”到另一个对象中。

回忆一下,左值和右值是表达式的属性。一些表达式生成或要求左值,而另外一些则生成或者要求右值。一般而言,一个左值表达式表示的是一个对象的身份,而一个右值表达式表示的是对象的值。

类似任何引用,一个右值引用也不过是某个对象的另一个名字而已。午我们所知,对于常规引用(为了与右值引用区分开),我们可以称之为左值引用,我们不能将其绑定到要求转换的表达式、字面常量或是返回右值的表达式。右值引用有着完全相反的绑定特性:我们可以将一个右值绑定到这类表达式上,但不能将一个右值引用直接绑定到一个左值上。

int i=42;

int &r=i;       //正确:r引用i。

int &&rr=i     //错误:不能将一个右值绑定到一个左值上。

int &r2=i*42;   //错误:i*42是一个右值。

const int &r3=i*42; //正确:我们可以将一个const的引用绑定到一个右值上。

Int &&rr2=i*42;   //正确:将rr2绑定到乘法结果上。

返回左值引用的函数,连同赋值、下标、解引用和前置递增/递减运算符,都是返回左值表达式的例子。我们可以将一个左值引用绑定到这类表达式的结果上。

返回非引用类型的函数,连同算术、关系、位以及后置递增/递减运算符,都生成右值。我们不能将一个左值引用绑定到这类表达式上,但我们可以将一个const的左值引用或者一个右值引用绑定到这类表达式上。

左值持久,右值短暂

考察左值和右值表达式的列表,两者相互区别之处就很明显了,左值有持久的状态,而右值要么是字面常量,要么是表达式求值过程中创建的临时对象。

由于右值引用只能绑定到临时对象,我们得知:所有引用的对象将要销毁,该对象没有其他用户。这两个特性意味着:使用右值引用的代码可以自由地接管所引用的对象的资源。

标准库move函数

虽然不能将一个右值引用直接绑定到一个左值上,但我们可以显示地将一个左值转换为对应的右值引用类型。我们还可以通过调用一个名为move的新标准库函数来获得绑定到左值上的右值引用,次函数在头文件utility中。Move函数来返回给定对象的右值引用。

         Int &&r3=std::move(rr1); 

move调用告诉编译器:我们有一个左值,但我们希望像一个右值一样处理它。我们必须认识到,调用move就意味着承诺:除了对rr1赋值或销毁它外,我们将不再使用它。在调用move之后,我们不能对移后源对象的值做任何假设。

(我们可以销毁一个移后源对象,也可以赋新值,但不能使用一个移后源对象的值。)

  1. extern 关键字

1.用法:

(1)extern c:以c的规则进行编译。

(2)C++语言支持分离式编译机制,该机制允许将程序分割为若干个文件,每个文件可被独立编译。为了将程序分为许多文件,则需要在文件中共享代码,例如一个文件的代码可能需要另一个文件中中定义的变量。在C语言中,修饰符extern用在变量或者函数的声明前,用来说明“此变量/函数是在别处定义的,要在此处引用”。

2.

extern 修饰变量的声明

如果文件a.c需要引用b.c中变量int v,就可以在a.c中声明extern int v,然后就可以引用变量v。

这里需要注意的是,被引用的变量v的链接属性必须是外链接(external)的,也就是说a.c要引用到v,不只是取决于在a.c中声明extern int v,还取决于变量v本身是能够被引用到的。

这涉及到c语言的另外一个话题--变量的作用域。能够被其他模块以extern修饰符引用到的变量通常是全局变量。

还有很重要的一点是,extern int v可以放在a.c中的任何地方,比如你可以在a.c中的函数fun定义的开头处声明extern int v,然后就可以引用到变量v了,只不过这样只能在函数fun作用域中引用v罢了,这还是变量作用域的问题。对于这一点来说,很多人使用的时候都心存顾虑。好像extern声明只能用于文件作用域似的。

extern 修饰函数声明

从本质上来讲,变量和函数没有区别。函数名是指向函数二进制块开头处的指针。

如果文件a.c需要引用b.c中的函数,比如在b.c中原型是int fun(int mu),那么就可以在a.c中声明extern int fun(int mu),然后就能使用fun来做任何事情。

就像变量的声明一样,extern int fun(int mu)可以放在a.c中任何地方,而不一定非要放在a.c的文件作用域的范围中。

对其他模块中函数的引用,最常用的方法是包含这些函数声明的头文件。使用extern和包含头文件来引用函数有什么区别呢?extern的引用方式比包含头文件要简洁得多!extern的使用方法是直接了当的,想引用哪个函数就用extern声明哪个函数。

这样做的一个明显的好处是,会加速程序的编译(确切的说是预处理)的过程,节省时间。在大型C程序编译过程中,这种差异是非常明显的。

extern修饰符可用于指示c或者c++函数的调用规范

比如在C++中调用C库函数,就需要在C++程序中用extern “C”声明要引用的函数。

这是给链接器用的,告诉链接器在链接的时候用C函数规范来链接。主要原因是C++和C程序编译完成后在目标代码中命名规则不同。

为了支持分离式编译,C++允许将声明和定义分离开来。变量的声明规定了变量的类型和名字,即使一个名字为程序所知,一个文件如果想使用别处定义的名字则必须包含对那个名字的声明。定义则负责创建与名字关联的实体,定义还申请存储空间。

  1. 如何防止头文件被重复包含
  1. 使用条件编译,所有头文件格式如下

#ifndef _HEADERNAME_H

#define _HEADERNAME_H

...//(头文件内容)

#endif

  1. windows平台下,使用宏命令#progma once

24.什么是内存泄露?野指针?内存越界?如何避免。

1.内存泄露:程序员动态申请的内存没有释放。

2.野指针:指向内存被释放的内存或者没有访问权限的内存的指针。

成因:

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

char *p = NULL;

char *str = new char(100);

  1. 指针p被free或者delete之后,没有置为null。
  2. 指针操作超越了变量的作用范围。这种情况让人防不胜防

class A

{

public:

void Func(void){ cout << “Func of class A” << endl; }

};

void Test(void)

{

  A  *p;

  if(...){

 A  a;

p = &a; // 注意 a 的生命期

  }

     p->Func(); // p是“野指针”

}

内存越界:何谓内存访问越界,简单的说,你向系统申请了一块内存,在使用这块内存的时候,超出了你申请的范围。可能发生内存的改写,影响附近内存块的数据安全。

  1. 出core文件
  1. 程序崩溃时程序运行时的保存的内存信息的coredump文件,可以通过sysctl或者/proc中来设置core文件的文件名以及生成的路径等。一般的coredump文件为ELF格式,coredump包含了程序运行时的内存,寄存器状态,堆栈指针,内存管理信息等。许多程序和操作系统出错时会自动生成一个core文件。coredump可以用在很多场合,使用Linux系统在跑一些压力测试或者系统负载一大的话,系统就hang住了或者干脆system panic。这时唯一能帮助你分析和解决问题的就是coredump了。通常进程或者内核收到。
  2. 相关命令

ulimit命令改变shell的资源限制,显示shell的资源限制,coredump项值为coredump文件大小单位blocks(4kbytes),程序崩溃时的行为不可按平常时的行为来估计,比如缓冲区溢出等错误可能导致堆栈被破坏,因此经常会出现某个变量的值被修改成乱七八糟的,然后程序用这个大小去申请内存就可能导致程序比平常时多占用很多内存。因此无论程序正常运行时占用的内存多么少,要保证生成Core文件还是将大小限制设为unlimited为好。一般使用ulimit -c unlimited不限制coredump文件大小,生成文件太小gdb时候会报错。

开启系统的coredump步骤为:

第一步,开启内核宏支持coredump函数(对应的进程重新编译并编译中-g要加上才能正常显示gdb信息)

 

第二步,命令设置coredump文件大小,ulimit -c unlimited表示coredump没有限制或者ulimit -c 1024支持文件大小1024k,如果进程脱离终端利用getrlimit,setrlimit如下所示

 

第三步,设置文件生成路径以及文件名。可以/proc/sys/kernel/core_pattern和/proc/sys/kernel/core_uses_pi来设置或者sysctl -w kernel.core_uses_pid =0 sysctl -w kernel.core_pattern = /var/core.%e.%p设置

3)调式

如上述步骤成功则会生成对应的core文件,如果是大型服务器中core文件可以直接gdb进行调试,这里只说明在嵌入式中如何利用gdb达到调试的目的。对应目录生成的core文件从系统中拷贝出来类似tftp命令 ftp或者利用u盘拷贝。嵌入式中不能拷贝出来coredump文件那之前设置都是白费的。

成功获取core文件,并将拷贝的core文件放入对应的process的程序工程目录下同一目录下且工程目录有process生成bin文件,cd到process的目录

XXX-XXX-XX-gdb bin core

进入gdb模式,调试中可能有一些库要用到,所以还要设置gdb中调用库的库文件的绝对路径,一般linux嵌入式一般是生成的文件系统作为调用路径

initially, you will see a lot of error messages. They can be ignored.  Now on the gdb prompt, type:

(gdb) set solib-absolute-prefix SRCPATH/targets/PROFILE/fs.install

(gdb) bt

bt之后可以看见打印的堆栈信息。

  1. 设计模式及实现(c++)

singleton:单例模式。使用static成员函数中构造local static变量的方法来实现,因为static成员只能初始化一次。

template<typename T>

class Singleton

{

public:

    static T& getInstance()

    {

        if (!value_)

        {

            value_ = new T();

        }

        return *value_;

    }

private:

    Singleton();

    ~Singleton();

    static T* value_;

};

template<typename T>

T* Singleton<T>::value_ = NULL;

  1. c++四种强制类型转换

1)const_cast

常量指针被转化成非常量的指针,并且仍然指向原来的对象;常量引用被转换成非常量的引用,并且仍然指向原来的对象。

2)static_cast

static_cast作用和c语言风格强制转换的 效果基本一样,由于没有运行时类型检查来保证转换的安全性,所以这类型的强制转换和c语言风格的强制转换都有安全隐患;

用于类层次结构中基类和派生类之间指针或引用的转换。(进行上行转换:把派生类的指针或引用转换成基类表示是安全的,进行下行转换:把基类指针或引用转换成派生类表示时,由于没有动态类型检查,所以是不安全的。);

用于基本数据类型之间的转换;

static_cast不能转换调原有类型的const、volatile、或者_unaligned属性;

c++的任何隐式转换都是使用static_cast来实现。

3)dynamic_cast

会在程序运行时对类型转换对“运行期类型信息”进行检查。

4)reinterpret_cast

用来处理无关类型转换的, 通常为操作数的位模式提供较低层次的重新解释。(仅仅是重新解释了给出的对象的比特模型,并没有进行二进制的转换),用在任意指针之间的转换,引用之间的转换,指针和足够大的int型之间的转换,整数到指针的转换。

  1. c++抽象类
  1. 定义: 纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0”。
  2. 引入原因

第一:为了方便使用多态特性,我们常常需要在基类中定义虚拟函数。

第二:在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。

为了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数(方法:virtual ReturnType Function()= 0;),则编译器要求在派生类中必须予以重载以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。这样就很好地解决了上述两个问题。

  • 网络

总体概述

一 OSI与TCP/IP各层的结构与功能,都有哪些协议?

学习计算机网络时我们一般采用折中的办法,也就是中和 OSI 和 TCP/IP 的优点,采用一种只有五层协议的体系结构,这样既简洁又能将概念阐述清楚。

 

结合互联网的情况,自上而下地,非常简要的介绍一下各层的作用。

1.1 应用层

应用层(application-layer)的任务是通过应用进程间的交互来完成特定网络应用。应用层协议定义的是应用进程(进程:主机中正在运行的程序)间的通信和交互的规则。对于不同的网络应用需要不同的应用层协议。在互联网中应用层协议很多,如域名系统DNS,支持万维网应用的 HTTP协议,支持电子邮件的 SMTP协议等等。我们把应用层交互的数据单元称为报文。

 

域名系统

 

域名系统(Domain Name System缩写 DNS,Domain Name被译为域名)是因特网的一项核心服务,它作为可以将域名和IP地址相互映射的一个分布式数据库,能够使人更方便的访问互联网,而不用去记住能够被机器直接读取的IP数串。(百度百科)例如:一个公司的 Web 网站可看作是它在网上的门户,而域名就相当于其门牌地址,通常域名都使用该公司的名称或简称。例如上面提到的微软公司的域名,类似的还有:IBM 公司的域名是 www.ibm.com、Oracle 公司的域名是 www.oracle.com、Cisco公司的域名是 www.cisco.com 等。

 

HTTP协议

 

超文本传输协议(HTTP,HyperText Transfer Protocol)是互联网上应用最为广泛的一种网络协议。所有的 WWW(万维网) 文件都必须遵守这个标准。设计 HTTP 最初的目的是为了提供一种发布和接收 HTML 页面的方法。(百度百科)

 

1.2 运输层

运输层(transport layer)的主要任务就是负责向两台主机进程之间的通信提供通用的数据传输服务。应用进程利用该服务传送应用层报文。“通用的”是指并不针对某一个特定的网络应用,而是多种应用可以使用同一个运输层服务。由于一台主机可同时运行多个线程,因此运输层有复用和分用的功能。所谓复用就是指多个应用层进程可同时使用下面运输层的服务,分用和复用相反,是运输层把收到的信息分别交付上面应用层中的相应进程。

 

运输层主要使用以下两种协议:

 

传输控制协议 TCP(Transmission Control Protocol)--提供面向连接的,可靠的数据传输服务。

用户数据协议 UDP(User Datagram Protocol)--提供无连接的,尽最大努力的数据传输服务(不保证数据传输的可靠性)。

TCP 与 UDP 的对比见问题三。

 

1.3 网络层

在 计算机网络中进行通信的两个计算机之间可能会经过很多个数据链路,也可能还要经过很多通信子网。网络层的任务就是选择合适的网间路由和交换结点, 确保数据及时传送。 在发送数据时,网络层把运输层产生的报文段或用户数据报封装成分组和包进行传送。在 TCP/IP 体系结构中,由于网络层使用 IP 协议,因此分组也叫 IP 数据报 ,简称 数据报。

 

这里要注意:不要把运输层的“用户数据报 UDP ”和网络层的“ IP 数据报”弄混。另外,无论是哪一层的数据单元,都可笼统地用“分组”来表示。

 

这里强调指出,网络层中的“网络”二字已经不是我们通常谈到的具体网络,而是指计算机网络体系结构模型中第三层的名称.

 

互联网是由大量的异构(heterogeneous)网络通过路由器(router)相互连接起来的。互联网使用的网络层协议是无连接的网际协议(Intert Protocol)和许多路由选择协议,因此互联网的网络层也叫做网际层或IP层。

 

1.4 数据链路层

数据链路层(data link layer)通常简称为链路层。两台主机之间的数据传输,总是在一段一段的链路上传送的,这就需要使用专门的链路层的协议。 在两个相邻节点之间传送数据时,数据链路层将网络层交下来的 IP 数据报组装程帧,在两个相邻节点间的链路上传送帧。每一帧包括数据和必要的控制信息(如同步信息,地址信息,差错控制等)。

 

在接收数据时,控制信息使接收端能够知道一个帧从哪个比特开始和到哪个比特结束。这样,数据链路层在收到一个帧后,就可从中提出数据部分,上交给网络层。 控制信息还使接收端能够检测到所收到的帧中有误差错。如果发现差错,数据链路层就简单地丢弃这个出了差错的帧,以避免继续在网络中传送下去白白浪费网络资源。如果需要改正数据在链路层传输时出现差错(这就是说,数据链路层不仅要检错,而且还要纠错),那么就要采用可靠性传输协议来纠正出现的差错。这种方法会使链路层的协议复杂些。

 

1.5 物理层

在物理层上所传送的数据单位是比特。 物理层(physical layer)的作用是实现相邻计算机节点之间比特流的透明传送,尽可能屏蔽掉具体传输介质和物理设备的差异。 使其上面的数据链路层不必考虑网络的具体传输介质是什么。“透明传送比特流”表示经实际电路传送后的比特流没有发生变化,对传送的比特流来说,这个电路好像是看不见的。

 

在互联网使用的各种协中最重要和最著名的就是 TCP/IP 两个协议。现在人们经常提到的TCP/IP并不一定单指TCP和IP这两个具体的协议,而往往表示互联网所使用的整个TCP/IP协议族。

1.6 总结一下

上面我们对计算机网络的五层体系结构有了初步的了解,下面附送一张七层体系结构图总结一下。

 

 TCP 三次握手和四次挥

为了准确无误地把数据送达目标处,TCP协议采用了三次握手策略。

2.1 TCP 三次握手漫画图解

 

 

2.2 为什么要三次握手两次握手可不可以

1)三次握手的目的是建立可靠的通信信道,说到通讯,简单来说就是数据的发送与接收,而三次握手最主要的目的就是双方确认自己与对方的发送与接收是正常的。

 

第一次握手:Client 什么都不能确认;Server 确认了对方发送正常

 

第二次握手:Client 确认了:自己发送、接收正常,对方发送、接收正常;Server 确认了:自己接收正常,对方发送正常

 

第三次握手:Client 确认了:自己发送、接收正常,对方发送、接收正常;Server 确认了:自己发送、接收正常,对方发送、接收正常

 

所以三次握手就能确认双发收发功能都正常,缺一不可。

2)我们知道,3次握手完成两个重要的功能,既要双方做好发送数据的准备工作(双方都知道彼此已准备好),也要允许双方就初始序列号进行协商,这个序列号在握手过程中被发送和确认。

现在把三次握手改成仅需要两次握手,死锁是可能发生的。作为例子,考虑计算机S和C之间的通信,假定C给S发送一个连接请求分组,S收到了这个分组,并发 送了确认应答分组。按照两次握手的协定,S认为连接已经成功地建立了,可以开始发送数据分组。可是,C在S的应答分组在传输中被丢失的情况下,将不知道S 是否已准备好,不知道S建立什么样的序列号,C甚至怀疑S是否收到自己的连接请求分组。在这种情况下,C认为连接还未建立成功,将忽略S发来的任何数据分 组,只等待连接确认应答分组。而S在发出的分组超时后,重复发送同样的分组。这样就形成了死锁。

2.3 为什么要传回 SYN

接收端传回发送端所发送的 SYN 是为了告诉发送端,我接收到的信息确实就是你所发送的信号了。

 

SYN 是 TCP/IP 建立连接时使用的握手信号。在客户机和服务器之间建立正常的 TCP 网络连接时,客户机首先发出一个 SYN 消息,服务器使用 SYN-ACK 应答表示接收到了这个消息,最后客户机再以 ACK(Acknowledgement[汉译:确认字符 ,在数据通信传输中,接收站发给发送站的一种传输控制字符。它表示确认发来的数据已经接受无误。 ])消息响应。这样在客户机和服务器之间才能建立起可靠的TCP连接,数据才可以在客户机和服务器之间传递。

2.4四次挥手

 

TCP的连接的拆除需要发送四个包,因此称为四次挥手(four-way handshake)。客户端或服务器均可主动发起挥手动作,在socket编程中,任何一方执行close()操作即可产生挥手操作。

客户端-发送一个 FIN,用来关闭客户端到服务器的数据传送

服务器-收到这个 FIN,它发回一 个 ACK,确认序号为收到的序号加1 。和 SYN 一样,一个 FIN 将占用一个序号

服务器-关闭与客户端的连接,发送一个FIN给客户端

客户端-发回 ACK 报文确认,并将确认序号设置为收到序号加1

2.5为什么建立连接协议是三次握手,而关闭连接却是四次握手呢?

这是因为服务端的LISTEN状态下的SOCKET当收到SYN报文的连接请求后,它可以把ACK和SYN(ACK起应答作用,而SYN起同步作用)放在一个报文里来发送。但关闭连接时,当收到对方的FIN报文通知时,它仅仅表示对方没有数据发送给你了;但未必你所有的数据都全部发送给对方了,所以你可能未必会马上会关闭SOCKET,也即你可能还需要发送一些数据给对方之后,再发送FIN报文给对方来表示你同意现在可以关闭连接了,所以它这里的ACK报文和FIN报文多数情况下都是分开发送的。

2.6为什么TIME_WAIT状态还需要等2MSL后才能返回到CLOSED状态?

(1)可靠的实现TCP全双工链接的终止。

 

这是因为虽然双方都同意关闭连接了,而且握手的4个报文也都协调和发送完毕,按理可以直接回到CLOSED状态(就好比从SYN_SEND状态到ESTABLISH状态那样);但是因为我们必须要假想网络是不可靠的,你无法保证你最后发送的ACK报文会一定被对方收到,因此对方处于LAST_ACK状态下的SOCKET可能会因为超时未收到ACK报文,而重发FIN报文,所以这个TIME_WAIT状态的作用就是用来重发可能丢失的ACK报文。

(2)允许老的重复的分节在网络中消逝。

假 设在12.106.32.254的1500端口和206.168.1.112.219的21端口之间有一个TCP连接。我们关闭这个链接,过一段时间后在 相同的IP地址和端口建立另一个连接。后一个链接成为前一个的化身。因为它们的IP地址和端口号都相同。TCP必须防止来自某一个连接的老的重复分组在连 接已经终止后再现,从而被误解成属于同一链接的某一个某一个新的化身。为做到这一点,TCP将不给处于TIME_WAIT状态的链接发起新的化身。既然 TIME_WAIT状态的持续时间是MSL的2倍,这就足以让某个方向上的分组最多存活msl秒即被丢弃,另一个方向上的应答最多存活msl秒也被丢弃。 通过实施这个规则,我们就能保证每成功建立一个TCP连接时。来自该链接先前化身的重复分组都已经在网络中消逝了。

2.7MSL与TTL

MSL是Maximum Segment Lifetime英文的缩写,中文可以译为“报文最大生存时间”,他是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。因为tcp报文(segment)是ip数据报(datagram)的数据部分,具体称谓请参见《数据在网络各层中的称呼》一文,而ip头中有一个TTL域,TTL是time to live的缩写,中文可以译为“生存时间”,这个生存时间是由源主机设置初始值但不是生存的具体时间,而是存储了一个ip数据报可以经过的最大路由数,每经过一个处理他的路由器此值就减1,当此值为0则数据报将被丢弃,同时发送ICMP报文通知源主机。RFC 793中规定MSL为2分钟,实际应用中常用的是30秒,1分钟和2分钟等。

2MSL即两倍的MSL,TCP的TIME_WAIT状态也称为2MSL等待状态,当TCP的一端发起主动关闭,在发出最后一个ACK包后,即第3次握手完成后发送了第四次握手的ACK包后就进入了TIME_WAIT状态,必须在此状态上停留两倍的MSL时间,等待2MSL时间主要目的是怕最后一个ACK包对方没收到,那么对方在超时后将重发第三次握手的FIN包,主动关闭端接到重发的FIN包后可以再发一个ACK应答包。在TIME_WAIT状态时两端的端口不能使用,要等到2MSL时间结束才可继续使用。当连接处于2MSL等待阶段时任何迟到的报文段都将被丢弃。不过在实际应用中可以通过设置SO_REUSEADDR选项达到不必等待2MSL时间结束再使用此端口。

TTL与MSL是有关系的但不是简单的相等的关系,MSL要大于等于TTL。

TCP,UDP 协议的区别

 

 

UDP 在传送数据之前不需要先建立连接,远地主机在收到 UDP 报文后,不需要给出任何确认。虽然 UDP 不提供可靠交付,但在某些情况下 UDP 确是一种最有效的工作方式(一般用于即时通信),比如: QQ 语音、 QQ 视频 、直播等等

TCP 提供面向连接的服务。在传送数据之前必须先建立连接,数据传送结束后要释放连接。 TCP 不提供广播或多播服务。由于 TCP 要提供可靠的,面向连接的传输服务(TCP的可靠体现在TCP在传递数据之前,会有三次握手来建立连接,而且在数据传递时,有确认、窗口、重传、拥塞控制机制,在数据传完后,还会断开连接用来节约系统资源),这一难以避免增加了许多开销,如确认,流量控制,计时器以及连接管理等。这不仅使协议数据单元的首部增大很多,还要占用许多处理机资源。TCP 一般用于文件传输、发送和接收邮件、远程登录等场景。

四 TCP 协议如何保证可靠传输

 

1)应用数据被分割成 TCP 认为最适合发送的数据块。

2)TCP 给发送的每一个包进行编号,接收方对数据包进行排序,把有序数据传送给应用层。

3)校验和: TCP 将保持它首部和数据的检验和。这是一个端到端的检验和,目的是检测数据在传输过程中的任何变化。如果收到段的检验和有差错,TCP 将丢弃这个报文段和不确认收到此报文段。

4)TCP 的接收端会丢弃重复的数据。

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

6)拥塞控制: 当网络拥塞时,减少数据的发送。

7)ARQ协议: 也是为了实现可靠传输的,它的基本原理就是每发完一个分组就停止发送,等待对方确认。在收到确认后再发下一个分组。

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

4.1 ARQ协议

自动重传请求(Automatic Repeat-reQuest,ARQ)是OSI模型中数据链路层和传输层的错误纠正协议之一。它通过使用确认和超时这两个机制,在不可靠服务的基础上实现可靠的信息传输。如果发送方在发送后一段时间之内没有收到确认帧,它通常会重新发送。ARQ包括停止等待ARQ协议和连续ARQ协议。

 

停止等待ARQ协议

停止等待协议是为了实现可靠传输的,它的基本原理就是每发完一个分组就停止发送,等待对方确认(回复ACK)。如果过了一段时间(超时时间后),还是没有收到 ACK 确认,说明没有发送成功,需要重新发送,直到收到确认后再发下一个分组;

在停止等待协议中,若接收方收到重复分组,就丢弃该分组,但同时还要发送确认;

优点: 简单

 

缺点: 信道利用率低,等待时间长

 

  1. 无差错情况:

 

发送方发送分组,接收方在规定时间内收到,并且回复确认.发送方再次发送。

2) 出现差错情况(超时重传): 停止等待协议中超时重传是指只要超过一段时间仍然没有收到确认,就重传前面发送过的分组(认为刚才发送过的分组丢失了)。因此每发送完一个分组需要设置一个超时计时器,其重传时间应比数据在分组传输的平均往返时间更长一些。这种自动重传方式常称为 自动重传请求 ARQ 。另外在停止等待协议中若收到重复分组,就丢弃该分组,但同时还要发送确认。连续 ARQ 协议 可提高信道利用率。发送维持一个发送窗口,凡位于发送窗口内的分组可连续发送出去,而不需要等待对方确认。接收方一般采用累积确认,对按序到达的最后一个分组发送确认,表明到这个分组位置的所有分组都已经正确收到了。

 

 

 

 

 

 

 

 

3) 确认丢失和确认迟到

 

确认丢失:确认消息在传输过程丢失

 

 

 

 

当A发送M1消息,B收到后,B向A发送了一个M1确认消息,但却在传输过程中丢失。而A并不知道,在超时计时过后,A重传M1消息,B再次收到该消息后采取以下两点措施:

 

I.丢弃这个重复的M1消息,不向上层交付。

Ii.向A发送确认消息。(不会认为已经发送过了,就不再发送。A能重传,就证明B的确认消息丢失)。

确认迟到 :确认消息在传输过程中迟到。

 

 A发送M1消息,B收到并发送确认。在超时时间内没有收到确认消息,A重传M1消息,B仍然收到并继续发送确认消息(B收到了2份M1)。此时A收到了B第二次发送的确认消息。接着发送其他数据。过了一会,A收到了B第一次发送的对M1的确认消息(A也收到了2份确认消息)。处理如下:

I.A收到重复的确认后,直接丢弃。

II.B收到重复的M1后,也直接丢弃重复的M1。

连续ARQ协议

连续 ARQ 协议可提高信道利用率。发送方维持一个发送窗口,凡位于发送窗口内的分组可以连续发送出去,而不需要等待对方确认。接收方一般采用累计确认,对按序到达的最后一个分组发送确认,表明到这个分组为止的所有分组都已经正确收到了。

 

优点: 信道利用率高,容易实现,即使确认丢失,也不必重传。

 

缺点: 不能向发送方反映出接收方已经正确收到的所有分组的信息。 比如:发送方发送了 5条 消息,中间第三条丢失(3号),这时接收方只能对前两个发送确认。发送方无法知道后三个分组的下落,而只好把后三个全部重传一次。这也叫 Go-Back-N(回退 N),表示需要退回来重传已经发送过的 N 个消息。

4.2 滑动窗口和流量控制

TCP 利用滑动窗口实现流量控制。流量控制是为了控制发送方发送速率,保证接收方来得及接收。 接收方发送的确认报文中的窗口字段可以用来控制发送方窗口大小,从而影响发送方的发送速率。将窗口字段设置为 0,则发送方不能发送数据。

4.3 拥塞控制

在某段时间,若对网络中某一资源的需求超过了该资源所能提供的可用部分,网络的性能就要变坏。这种情况就叫拥塞。拥塞控制就是为了防止过多的数据注入到网络中,这样就可以使网络中的路由器或链路不致过载。拥塞控制所要做的都有一个前提,就是网络能够承受现有的网络负荷。拥塞控制是一个全局性的过程,涉及到所有的主机,所有的路由器,以及与降低网络传输性能有关的所有因素。相反,流量控制往往是点对点通信量的控制,是个端到端的问题。流量控制所要做到的就是抑制发送端发送数据的速率,以便使接收端来得及接收。

 

为了进行拥塞控制,TCP 发送方要维持一个 拥塞窗口(cwnd) 的状态变量。拥塞控制窗口的大小取决于网络的拥塞程度,并且动态变化。发送方让自己的发送窗口取为拥塞窗口和接收方的接受窗口中较小的一个。

 

TCP的拥塞控制采用了四种算法,即 慢开始 、拥塞避免 、快重传 和 快恢复。在网络层也可以使路由器采用适当的分组丢弃策略(如主动队列管理 AQM),以减少网络拥塞的发生。

慢开始:慢开始算法的思路是当主机开始发送数据时,如果立即把大量数据字节注入到网络,那么可能会引起网络阻塞,因为现在还不知道网络的符合情况。经验表明,较好的方法是先探测一下,即由小到大逐渐增大发送窗口,也就是由小到大逐渐增大拥塞窗口数值。cwnd初始值为1,每经过一个传播轮次,cwnd加倍。

 

拥塞避免: 拥塞避免算法的思路是让拥塞窗口cwnd缓慢增大,即每经过一个往返时间RTT就把发送放的cwnd加1.

快重传与快恢复:在 TCP/IP 中,快速重传和恢复(fast retransmit and recovery,FRR)是一种拥塞控制算法,它能快速恢复丢失的数据包。没有 FRR,如果数据包丢失了,TCP 将会使用定时器来要求传输暂停。在暂停的这段时间内,没有新的或复制的数据包被发送。有了 FRR,如果接收机接收到一个不按顺序的数据段,它会立即给发送机发送一个重复确认。如果发送机接收到三个重复确认,它会假定确认件指出的数据段丢失了,并立即重传这些丢失的数据段。有了 FRR,就不会因为重传时要求的暂停被耽误。当有单独的数据包丢失时,快速重传和恢复(FRR)能最有效地工作。当有多个数据信息包在某一段很短的时间内丢失时,它则不能很有效地工作。

 

 

五 在浏览器中输入url地址 ->> 显示主页的过程

 

总体来说分为以下几个过程:

DNS解析

TCP连接

发送HTTP请求

服务器处理请求并返回HTTP报文

浏览器解析渲染页面

连接结束

具体可以参考下面这篇文章:

https://segmentfault.com/a/1190000006879700

六 状态码

 

 

七 HTTP协议

7.1 HTTP协议与各种协议的关系

 

7.2 HTTP长连接,短连接

在HTTP/1.0中默认使用短连接。也就是说,客户端和服务器每进行一次HTTP操作,就建立一次连接,任务结束就中断连接。当客户端浏览器访问的某个HTML或其他类型的Web页中包含有其他的Web资源(如JavaScript文件、图像文件、CSS文件等),每遇到这样一个Web资源,浏览器就会重新建立一个HTTP会话。

 

而从HTTP/1.1起,默认使用长连接,用以保持连接特性。使用长连接的HTTP协议,会在响应头加入这行代码:

 

Connection:keep-alive

在使用长连接的情况下,当一个网页打开完成后,客户端和服务器之间用于传输HTTP数据的TCP连接不会关闭,客户端再次访问这个服务器时,会继续使用这一条已经建立的连接。Keep-Alive不会永久保持连接,它有一个保持时间,可以在不同的服务器软件(如Apache)中设定这个时间。实现长连接需要客户端和服务端都支持长连接。

 

HTTP协议的长连接和短连接,实质上是TCP协议的长连接和短连接。

 

—— 《HTTP长连接、短连接究竟是什么?》

https://www.cnblogs.com/gotodsp/p/6366163.html

7.3 HTTP断点续传

HTTP1.1协议(RFC2616)中定义了断点续传相关的HTTP头 Range和Content-Range字段,一个最简单的断点续传实现大概如下: 
  1客户端下载一个1024K的文件,已经下载了其中512K 
  2网络中断,客户端请求续传,因此需要在HTTP头中申明本次需要续传的片段: 
        Range:bytes=512000- 
      这个头通知服务端从文件的512K位置开始传输文件 
  3 服务端收到断点续传请求,从文件的512K位置开始传输,并且在HTTP头中增加: 
    Content-Range:bytes 512000-/1024000 
    并且此时服务端返回的HTTP状态码应该是206,而不是200。 

7.4 HTTP是不保存状态的协议,如何保存用户状态?

HTTP 是一种不保存状态,即无状态(stateless)协议。也就是说 HTTP 协议自身不对请求和响应之间的通信状态进行保存。那么我们保存用户状态呢?Session 机制的存在就是为了解决这个问题,Session 的主要作用就是通过服务端记录用户的状态。典型的场景是购物车,当你要添加商品到购物车的时候,系统不知道是哪个用户操作的,因为 HTTP 协议是无状态的。服务端给特定的用户创建特定的 Session 之后就可以标识这个用户并且跟踪这个用户了(一般情况下,服务器会在一定时间内保存这个 Session,过了时间限制,就会销毁这个Session)。

 

在服务端保存 Session 的方法很多,最常用的就是内存和数据库(比如是使用内存数据库redis保存)。既然 Session 存放在服务器端,那么我们如何实现 Session 跟踪呢?大部分情况下,我们都是通过在 Cookie 中附加一个 Session ID 来方式来跟踪。

 

Cookie 被禁用怎么办?

 

最常用的就是利用 URL 重写把 Session ID 直接附加在URL路径的后面。

7.5 Cookie的作用是什么?和Session有什么区别?

Cookie 和 Session都是用来跟踪浏览器用户身份的会话方式,但是两者的应用场景不太一样。

Cookie 一般用来保存用户信息 比如①我们在 Cookie 中保存已经登录过得用户信息,下次访问网站的时候页面可以自动帮你登录的一些基本信息给填了;②一般的网站都会有保持登录也就是说下次你再访问网站的时候就不需要重新登录了,这是因为用户登录的时候我们可以存放了一个 Token 在 Cookie 中,下次登录的时候只需要根据 Token 值来查找用户即可(为了安全考虑,重新登录一般要将 Token 重写);③登录一次网站后访问网站其他页面不需要重新登录。Session 的主要作用就是通过服务端记录用户的状态。 典型的场景是购物车,当你要添加商品到购物车的时候,系统不知道是哪个用户操作的,因为 HTTP 协议是无状态的。服务端给特定的用户创建特定的 Session 之后就可以标识这个用户并且跟踪这个用户了。

Cookie 数据保存在客户端(浏览器端),Session 数据保存在服务器端。

Cookie 存储在客户端中,而Session存储在服务器上,相对来说 Session 安全性更高。如果使用 Cookie 的一些敏感信息不要写入 Cookie 中,最好能将 Cookie 信息加密然后使用到的时候再去服务器端解密。

7.6 URI和URL的区别是什么?

URI(Uniform Resource Identifier) 是同一资源标志符,可以唯一标识一个资源。

URL(Uniform Resource Location) 是同一资源定位符,可以提供该资源的路径。它是一种具体的 URI,即 URL 可以用来标识一个资源,而且还指明了如何 locate 这个资源。

URI的作用像身份证号一样,URL的作用更像家庭住址一样。URL是一种具体的URI,它不仅唯一标识资源,而且还提供了定位该资源的信息。

7.7 HTTP 和 HTTPS 的区别?

1)端口 :HTTP的URL由“http://”起始且默认使用端口80,而HTTPS的URL由“https://”起始且默认使用端口443。

2)安全性和资源消耗: HTTP协议运行在TCP之上,所有传输的内容都是明文,客户端和服务器端都无法验证对方的身份。HTTPS是运行在SSL/TLS之上的HTTP协议,SSL/TLS 运行在TCP之上。所有传输的内容都经过加密,加密采用对称加密,但对称加密的密钥用服务器方的证书进行了非对称加密。所以说,HTTP 安全性没有 HTTPS高,但是 HTTPS 比HTTP耗费更多服务器资源。

对称加密:密钥只有一个,加密解密为同一个密码,且加解密速度快,典型的对称加密算法有DES、AES等;

非对称加密:密钥成对出现(且根据公钥无法推知私钥,根据私钥也无法推知公钥),加密解密使用不同密钥(公钥加密需要私钥解密,私钥加密需要公钥解密),相对对称加密速度较慢,典型的非对称加密算法有RSA、DSA等。

八 常用的熟知端口号

 

九 HTTPS

 

三.算法

1.所有排序算法

//冒泡排序

void bubbleSort(int a[], int n) {

    for (int i = 0; i < n; i++)

        for (int j = 1; j < n - i; j++) {

            if (a[j - 1] > a[j])

                swap(a[j - 1], a[j]);

        }

}

 

//插入排序

void insertSort(int a[], int n) {

    for (int i = 1; i < n; i++) {

        for (int j = i - 1; j >= 0; --j) {

            if (a[j] > a[j + 1])

                swap(a[j], a[j + 1]);

        }

    }

}

 

//选择排序

void selectSort(int a[], int n) {

 

    for (int i = 0; i < n; i++) {

        int min = a[i];

        int flag = i;

        for (int j = i; j < n; j++) {

            if (a[j] < min) {

                min = a[j];

                flag = j;

            }

        }

        swap(a[i], a[flag]);

    }

}

/**************************************/

/*************************************/

int partition(int a[], int start, int end) {

    int pivot = a[end];

    int i = start - 1;

    int j = start;

    while (j < end) {

        if (a[j] < pivot) {

            ++i;

            swap(a[j], a[i]);

        }

        j++;

    }

    swap(a[i + 1], a[end]);

    return i + 1;

}

 

//快速排序

void quickSort(int a[], int start, int end) {

    //int lhs = start;

    //int rhs = end;

    if (start < end) {

        int q = partition(a, start, end);

        quickSort(a, start, q - 1);

        quickSort(a, q + 1, end);

    }

}

 

//归并排序

void merge(int a[], int start, int mid, int end) {

    int temp[end + 1];

    int i = start;

    int j = mid + 1;

    int index = 0;

    while (i <= mid && j <= end) {

        if (a[i] > a[j])

            temp[index++] = a[j++];

        else

            temp[index++] = a[i++];

    }

    while (i <= mid) {

        temp[index++] = a[i++];

    }

    while (j <= end)

        temp[index++] = a[j++];

    for (int i = 0, j = start; j <= end; i++, j++)

        a[j] = temp[i];

}

 

void mergeSort(int a[], int start, int end) {

    if (start < end) {

        int mid = (start + end) / 2;

        mergeSort(a, start, mid);

        mergeSort(a, mid + 1, end);

        merge(a, start, mid, end);

    }

}

 

void heapAdjust(int a[], int start, int len) {

    int child;     // 子结点的位置 = 2 * 父结点的位置 + 1

    // 得到子结点中键值较大的结点

    for (; 2 * start + 1 < len; start = child) {

        child = 2 * start + 1;

        if (child < len - 1 && a[child + 1] > a[child])

            child++;

        // 如果较大的子结点大于父结点那么把较大的子结点往上移动,替换它的父结点

        if (a[start] < a[child])

            swap(a[start], a[child]);

        else

            break;

    }

 

}

 

//堆排序

void heapSort(int a[], int n) {

    // 调整序列的前半部分元素,调整完之后第一个元素是序列的最大的元素

    for (int i = n / 2 - 1; i >= 0; i--)

        heapAdjust(a, i, n);

    for (int i = n - 1; i > 0; i--) {

        // 将第1个元素与当前最后一个元素交换,保证当前的最后一个位置的元素都是现在的这个序列中最大的

        swap(a[0], a[i]);

        // 不断缩小调整heap的范围,每一次调整完毕保证第一个元素是当前序列的最大值

        heapAdjust(a, 0, i);

    }

}

//希尔排序

void shellSort(int a[], int size){

    int h = 1;

    while (h < size / 3) {

        h = 3 * h + 1;

    }

    while (h >= 1) {

        for (int i = h; i < size; i++) {

            for (size_t j = i; j >= h && a[j] < a[j - h]; j -= h) {

                std::swap(a[j], a[j - h]);

            }

        }

        h = h / 3;

    }

   

}

 

  1. BFS、DFS
  2. Dijkstra

4.Kmp算法

5.计算机计算除法的方式(乘法)

6.背包

7.红黑树

 

  • Python、go
  • 数据库

1.数据库建立索引的规则

普遍的规则:

 

1)表的主键、外键必须有索引;

 

2)数据量超过300的表应该有索引;

 

3)经常与其他表进行连接的表,在连接字段上应该建立索引;

 

4)经常出现在Where子句中的字段,特别是大表的字段,应该建立索引;

 

5)索引应该建在选择性高的字段上;

 

6)索引应该建在小字段上,对于大的文本字段甚至超长字段,不要建索引;

 

7)复合索引的建立需要进行仔细分析;尽量考虑用单字段索引代替:

 

   A、正确选择复合索引中的主列字段,一般是选择性较好的字段;

 

   B、复合索引的几个字段是否经常同时以AND方式出现在Where子句中?单字段查询是否极少甚至没有?如果是,则可以建立复合索引;否则考虑单字段索引;

 

   C、如果复合索引中包含的字段经常单独出现在Where子句中,则分解为多个单字段索引;

 

   D、如果复合索引所包含的字段超过3个,那么仔细考虑其必要性,考虑减少复合的字段;

 

   E、如果既有单字段索引,又有这几个字段上的复合索引,一般可以删除复合索引;

 

8)频繁进行数据操作的表,不要建立太多的索引;

 

9)删除无用的索引,避免对执行计划造成负面影响;

 

   以上是一些普遍的建立索引时的判断依据。一言以蔽之,索引的建立必须慎重,对每个索引的必要性都应该经过仔细分析,要有建立的依据。因为太多的索引与不充分、不正确的索引对性能都毫无益处:在表上建立的每个索引都会增加存储开销,索引对于插入、删除、更新操作也会增加处理上的开销。另外,过多的复合索引,在有单字段索引的情况下,一般都是没有存在价值的;相反,还会降低数据增加删除时的性能,特别是对频繁更新的表来说,负面影响更大

六.其他

1.多线程调度,效率最高

有多种算法

  1. 先来先服务
  2. 最短优先
  3. 最高优先权优先(FPF)调度算法

此算法常被用于批处理系统中,作为作业调度算法,也作为多种操作系统中的进程调度算法,还可用于实时系统中。当把该算法用于作业调度时,系统将从后备队列中选择若干个优先权最高的作业装入内存。当用于进程调度时,该算法是把处理机分配给就绪队列中优先权最高的进程,这时,又可进一步把该算法分成如下两种。

第一种:非抢占式优先权算法

在这种方式下,系统一旦把处理机分配给就绪队列中优先权最高的进程后,该进程便一直执行下去,直至完成;或因发生某事件使该进程放弃处理机时,系统方可再将处理机重新分配给另一优先权最高的进程。这种调度算法主要用于批处理系统中;也可用于某些对实时性要求不严的实时系统中。

第二种:抢占式优先权调度算法

在这种方式下,系统同样是把处理机分配给优先权最高的进程,使之执行。但在其执行期间,只要又出现了另一个其优先权更高的进程,进程调度程序就立即停止当前进程(原优先权最高的进程)的执行,重新将处理机分配给新到的优先权最高的进程。因此,在采用这种调度算法时,是每当系统中出现一个新的就绪进程i 时,就将其优先权Pi与正在执行的进程j 的优先权Pj进行比较。如果Pi≤Pj,原进程Pj便继续执行;但如果是Pi>Pj,则立即停止Pj的执行,做进程切换,使i 进程投入执行。显然,这种抢占式的优先权调度算法能更好地满足紧迫作业的要求,故而常用于要求比较严格的实时系统中,以及对性能要求较高的批处理和分时系统中。

  1. 高响应比优先调度算法

该优先权的变化规律可描述为:

 

 

由于等待时间与服务时间之和就是系统对该作业的响应时间,故该优先权又相当于响应比RP。据此,又可表示为:

 

 

由上式可以看出:

(1) 如果作业的等待时间相同,则要求服务的时间愈短,其优先权愈高,因而该算法有利于短作业。

(2) 当要求服务的时间相同时,作业的优先权决定于其等待时间,等待时间愈长,其优先权愈高,因而它实现的是先来先服务。

(3) 对于长作业,作业的优先级可以随等待时间的增加而提高,当其等待时间足够长时,其优先级便可升到很高,从而也可获得处理机。

简言之,该算法既照顾了短作业,又考虑了作业到达的先后次序,不会使长作业长期得不到服务。因此,该算法实现了一种较好的折衷。当然,在利用该算法时,每要进行调度之前,都须先做响应比的计算,这会增加系统开销。

5)3.1 时间片轮转法

在早期的时间片轮转法中,系统将所有的就绪进程按先来先服务的原则排成一个队列,每次调度时,把CPU 分配给队首进程,并令其执行一个时间片。时间片的大小从几ms 到几百ms。当执行的时间片用完时,由一个计时器发出时钟中断请求,调度程序便据此信号来停止该进程的执行,并将它送往就绪队列的末尾;然后,再把处理机分配给就绪队列中新的队首进程,同时也让它执行一个时间片。这样就可以保证就绪队列中的所有进程在一给定的时间内均能获得一时间片的处理机执行时间。换言之,系统能在给定的时间内响应所有用户的请求。

  1. 多级反馈队列调度算法

前面介绍的各种用作进程调度的算法都有一定的局限性。如短进程优先的调度算法,仅照顾了短进程而忽略了长进程,而且如果并未指明进程的长度,则短进程优先和基于进程长度的抢占式调度算法都将无法使用。而多级反馈队列调度算法则不必事先知道各种进程所需的执行时间,而且还可以满足各种类型进程的需要,因而它是目前被公认的一种较好的进程调度算法。在采用多级反馈队列调度算法的系统中,调度算法的实施过程如下所述。

(1) 应设置多个就绪队列,并为各个队列赋予不同的优先级。第一个队列的优先级最高,第二个队列次之,其余各队列的优先权逐个降低。该算法赋予各个队列中进程执行时间片的大小也各不相同,在优先权愈高的队列中,为每个进程所规定的执行时间片就愈小。例如,第二个队列的时间片要比第一个队列的时间片长一倍,……,第i+1个队列的时间片要比第i个队列的时间片长一倍。

(2) 当一个新进程进入内存后,首先将它放入第一队列的末尾,按FCFS原则排队等待调度。当轮到该进程执行时,如它能在该时间片内完成,便可准备撤离系统;如果它在一个时间片结束时尚未完成,调度程序便将该进程转入第二队列的末尾,再同样地按FCFS原则等待调度执行;如果它在第二队列中运行一个时间片后仍未完成,再依次将它放入第三队列,……,如此下去,当一个长作业(进程)从第一队列依次降到第n队列后,在第n 队列便采取按时间片轮转的方式运行。

(3) 仅当第一队列空闲时,调度程序才调度第二队列中的进程运行;仅当第1~(i-1)队列均空时,才会调度第i队列中的进程运行。如果处理机正在第i队列中为某进程服务时,又有新进程进入优先权较高的队列(第1~(i-1)中的任何一个队列),则此时新进程将抢占正在运行进程的处理机,即由调度程序把正在运行的进程放回到第i队列的末尾,把处理机分配给新到的高优先权进程。

7)电梯调度算法

2.文件一致性验证

https://www.kancloud.cn/swordfly/linux/482799

  • C++
  1. 多态的实现

多态简单来说,是指在继承层次中,父类的指针可以具有多种形态——当它指向某个子类对象是,通过它能够调用到子类的函数,而非父类的函数。

一个接口,多种实现。使用继承+虚函数,使用virtual关键字修饰成员函数。New一个子类对象,赋给父类的指针和引用。(实现机制:虚函数表)

2.虚函数

虚函数表属于类,类的所有对象共享这个类的虚函数表。虚函数表存储在进程的只读数据段。每个对象都有一个默认的指针vptr指向虚函数表。确定vptr指向的具体对象类型,加上偏移量,就可以找到具体调用的函数。虚函数指针一般都放在对象内存布局的第一个位置上,这是为了保证在多层继承或多重继承的情况下能以最高效率取到虚函数表。当vptr位于对象内存最前面是,对象的地址即为虚函数指针地址。我们可以取得虚函数指针的地址。

3内存对齐

内存对齐只是指数据在内存时的起始地址是否是某个值得整数倍。如果只是放在内存中,是否对齐本身并没有什么问题。未提示读取、写入的时候。访问一个不对齐的数据可能会导致程序运行效率慢,结果出错,甚至是程序宕掉。

原则:

(1)数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如说是数组,结构体等)的整数倍开始(比如int在32位机为4字节, 则要从4的整数倍地址开始存储),基本类型不包括struct/class/uinon。

(2)结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部"最宽基本类型成员"的整数倍地址开始存储.(struct a里存有struct b,b里有char,int ,double等元素,那b应该从8的整数倍开始存储.)。

(3)收尾工作:结构体的总大小,也就是sizeof的结果,.必须是其内部最大成员的"最宽基本类型成员"的整数倍.不足的要补齐.(基本类型不包括struct/class/uinon)。

(4)sizeof(union),以结构里面size最大元素为union的size,因为在某一时刻,union只有一个成员真正存储于该地址

4.c++对象模型

在c++中有两种数据成员,静态和非静态的,有三种成员函数:静态、非静态和虚函数。

5.malloc/free和new/delete的区别

 1)new/delete是c++里才有的。

 2)new/delete通常来说是操作符,就是"+","-"一样。

 3)new/delete是可以重载的,而重载之后,就成为了函数。

 4)malloc在申请内存的时候,必须要提供申请的长度,而且返回的指针是void*型,必须要强转成需要的类型。

5)当new/delete在类中被重载的时候,可以自定义申请过程,比如记录所申请内存的总长度,以及跟踪每个对象的指针。

6) new/delete,其实内部也调用了malloc/free。

7)new 在申请内存的同时,还会调用对象的构造函数,而 malloc 只会申请内存;同样,delete 在释放内存之前,会调用对象的析构函数,而 free 只会释放内存。

共同点:

1) 都必须配对使用。

2) 都是申请内存,释放内存。

3) free和delete可以释放NULL指针。

注意点:

new/delete与malloc/free不能混合使用。

6.const关键字

Const对象一旦创建其值就无法改变,所以const对象必须初始化。

7.指针和引用的区别:

指针存储的是所指变量的地址,而引用是变量的别名,引用不能为空,必须初始化。

8.c++堆和栈的区别

栈:由程序自动向操作系统申请分配以及回收,速度快,使用方便。但程序员无法控制。若分配失败,则提示栈溢出错误。注意,const局部变量也存储在栈区内,栈区向地址减小的方向增长。

堆:程序员向操作系统申请一块内存,当系统收到程序的申请时,会遍历一个记录空闲内存地址的链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。分配的速度较慢地址不连续,容易碎片化。此外,由程序员申请,同时也必须由程序员负责销毁,否则则导致内存泄露。

9.关键字static

Static变量:存储在静态存储区。作用:第一个作用是限定作用域;第二个作用是保持变量内容持久化。

1. C 语言中static的用法:

  1. 全局静态变量:

   用法:在全局变量前加上关键字static,全局变量就定义成一个全局静态变量。

   内存中的位置:静态存储区,在整个程序运行期间一直存在。

   初始化:未经初始化的全局静态变量会被自动初始化为0(自动对象的值是任意的,除非他被显式初始化)。

   作用域:全局静态变量在声明他的文件之外是不可见得,准确的说是从定义之处开始,到文件结尾。

  1. 局部静态变量:

   用法:在局部变量前加上关键字static,局部变量就成为一个局部静态变量。

   内存的位置:静态存储区。

   初始化:未经初始化的全局静态变量会被自动初始化为0.

   作用域:作用域仍未局部作用域,当定义它的函数或者语句块结束的时候,作用域结束。但是当局部静态变量离开作用域后,并没有销毁,而是仍然驻留在内存当中,只不过我们不能再对它进行访问,直到该函数再次被调用,并且值不变。

  1. 静态函数:

在函数返回类型前加关键字static,函数就定义成静态成员函数。函数的定义和生命在默认情况下都是extern的,但静态函数只是在声明他的文件当中可见,不能被其他文件所用。

  1. C++中static的用法:
  1. 类的静态成员:

class A{

private:

     static int val;

};
在cpp中必须对它进行初始化,初始化时使用作用域运算符来标明他所属类,其属于该类的所有成员共有,只有一个拷贝。

  1. 类的静态成员函数:

class A{

private:

    static int func(int x);

}

实现的时候也不需要static的修饰,因为static是声明性关键字;类的静态函数是该类的范畴内的全局函数,不能访问类的私有成员,只能访问类的静态成员,不需要类的实例即可调用;实际上,他就是增加了类的访问权限的全局函数。

void A::func(int);

静态成员函数可以继承和覆盖,但无法是虚函数。

3)只在cpp内有效的全局变量:在cpp文件的全局范围内声明:static int val =0;

这个变量的含义是该cpp内有效,但是其他的cpp文件不能访问这个变量;如果两个cpp文件声明了同名的全局静态变量,那么他们实际上是独立的两个变量。

  1. 只在cpp内有效的全局函数:

函数的实现使用static修饰,那么这个函数只可在本cpp内使用,不会同其他cpp中的同名函数引起冲突。   

warning:不要在头文件中声明static的全局函数,不要在cpp内声明非static的全局函数,如果你要在多个cpp中复用该函数,就把它的声明提到头文件里去,否则cpp内部声明需加上static修饰。(说的逻辑不对?)

10.用过的设计模式,举几个例子

singleton:static成员函数中构造local static变量的方法来实现,因为static成员只能初始化一次。

template<typename T>

class Singleton

{

public:

    static T& getInstance()

    {

        if (!value_)

        {

            value_ = new T();

        }

        return *value_;

    }

private:

    Singleton();

    ~Singleton();

    static T* value_;

};

template<typename T>

T* Singleton<T>::value_ = NULL;

11.c++深拷贝和浅拷贝

浅拷贝,即在定义一个类A,使用类似A obj;  A obj1(obj);或者A obj1 = obj; 时候,由于没有自定义拷贝构造函数,C++编译器自动会产生一个默认的拷贝构造函数。这个默认的拷贝构造函数采用的是“位拷贝”(浅拷贝),而非“值拷贝”(深拷贝)的

方式,如果类中含有指针变量,默认的拷贝构造函数必定出错。

用一句简单的话来说就是浅拷贝,只是对指针的拷贝,拷贝后两个指针指向同一个内存空间,深拷贝不但对指针进行拷贝,而且对指针指向的内容进行拷贝,经深拷贝后的指针是指向两个不同地址的指针。

假如有一个成员变量的指针,char *m_data;

其一,浅拷贝只是拷贝了指针,使得两个指针指向同一个地址,这样在对象块结束,调用函数析构的时,会造成同一份资源析构2次,即delete同一块内存2次,造成程序崩溃。

其二,浅拷贝使得obj.m_data和obj1.m_data指向同一块内存,任何一方的变动都会影响到另一方。

其三,在释放内存的时候,会造成obj1.m_data原有的内存没有被释放(这句话,刚开始我不太理解,如果没有走自定义的拷贝构造函数,申请内存空间,A obj1(obj);也不走默认构造函数,走的是默认的拷贝构造函数,何来分配空间直说,更不会造成obj1.m_data原有的内存没有被释放,这里刚开始我一直有疑问),造成内存泄露。

事实是这样的,当delete obj.m_data, obj.m_data内存被释放后,由于之前obj.m_data和obj1.m_data指向的是同一个内存空间,obj1.m_data所指的空间不能在被利用了,delete obj1.m_data也不会成功,一直已经无法操作该空间,所以导致内存泄露。

深拷贝采用了在堆内存中申请新的空间来存储数据,这样每个可以避免指针悬挂12.volatile的作用

volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。声明时语法:int volatile vInt; 当要求使用 volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。而且读取的数据立刻被保存。例如:

 

volatile int i=10;

int a = i;

...

// 其他代码,并未明确告诉编译器,对 i 进行过操作

5

int b = i;

 volatile 指出 i 是随时可能发生变化的,每次使用它的时候必须从 i的地址中读取,因而编译器生成的汇编代码会重新从i的地址读取数据放在 b 中。而优化做法是,由于编译器发现两次从 i读数据的代码之间的代码没有对 i 进行过操作,它会自动把上次读的数据放在 b 中。而不是重新从 i 里面读。这样以来,如果 i是一个寄存器变量或者表示一个端口数据就容易出错,所以说 volatile 可以保证对特殊地址的稳定访问。

13.构造函数和析构函数可以调用虚函数吗

构造函数跟析构函数里面都可以调用虚函数,编译器不会报错。C++ primer中说到最好别用。

由于类的构造次序是由基类到派生类,所以在构造函数中调用虚函数,虚函数是不会呈现出多态的。

类的析构是从派生类到基类,当调用继承层次中某一层次的类的析构函数时意味着其派生类部分已经析构掉,所以也不会呈现多态。

因此如果在基类中声明的纯虚函数并且在基类的析构函数中调用之,编译器会发生错误。

 

14.友元函数

友元函数不含this指针,所以友元函数访问对象中的成员要通过对象名。友元函数可以在类内实现也可以在类外实现。

15.构造函数和拷贝构造函数能否为虚函数

不能,只有析构函数可以定义为虚函数,构造函数不能定义为虚函数。构造函数是在对象完全构造之前运行的,在构造函数运行的时候,对象的动态类型还不完整。

16.类对象所占内存空间

空的类是会占用内存空间的,而且大小是1,原因是C++要求每个实例在内存中都有独一无二的地址。

1)类内部的成员变量:

* 普通的变量:是要占用内存的,但是要注意对齐原则(这点和struct类型很相似)。

* static修饰的静态变量:不占用内存,原因是编译器将其放在全局变量区。

2)类内部的成员函数:

* 普通函数:不占用内存。

* 虚函数:要占用4个字节,用来指定虚函数的虚拟函数表的入口地址。所以一个类的虚函数所占用的地址是不变的,和虚函数的个数是没有关系。

17.关于析构函数和纯虚函数

纯虚析构函数和普通纯虚函数的区别在于,纯虚析构函数需要提供函数的实现,而一般纯虚函数不能有实现,原因在于,纯虚析构函数最终需要被调用,以析构基类对象,虽然是抽象类没有实体。而如果不提供该析构函数的实现,将使得在析构过程中,析构无法完成而导致析构异常的问题。

通过父类指针去析构子类对象,分三种情况:

1)父类的析构函数不是虚函数,这种情况下,将只会调用A的析构函数而不会调用子类的析构函数,前面的文章中有提到过,非虚函数是通过类型来寻址的,这样的析构将会导致析构畸形,这是笔试面试中经常遇到的考点。

2)父类的析构函数是普通的虚函数,这种情况下,会很正常,从子类一直析构到基类,最后完成析构,这就是针对A的修改方法之一。

3)父类的析构函数是纯析构函数,重点,在这种情况之下,由于析构函数首先是虚函数,所以会按2的方法从子类一直析构到父类,但是,又由于父类的析构函数是纯虚函数,如果没有实现体,导致父类无法析构,最终也导致了析构畸形,因此,特殊的地方就在于这里,纯虚析构函数需要提供一个实现体,以完成对象的析构。

18.数组名是数组首元素的地址

a[10];

 a=&a[0];

 a+2=&a[2];

 *(a+2)=a[2];

19.大端模式和小端模式

举一个例子,比如数字0x12 34 56 78在内存中的表示形式。

1)大端模式:Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。

(其实大端模式才是我们直观上认为的模式,和字符串存储的模式差类似)

低地址 --------------------> 高地址

0x12  |  0x34  |  0x56  |  0x78

2)小端模式:Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。

低地址 --------------------> 高地址

0x78  |  0x56  |  0x34  |  0x12

20.void *

void 指针可以指向任意类型的数据,就是说可以用任意类型的指针对void指针赋值

 

  1. 左值、右值、右值引用

1.左值:

对于早期的c语言,提到左值意味着:

  1. 它指定一个对象,所以引用内存中的地址。
  2. 它可用在赋值运算符的左侧,左值(lvalue)中的l源自left。

但是后来,标准中新增了const限定符。用const创建的变量不可修改。因此,const标识符满足上面的第一项,但是不满足第二项。一方面c继续把标识对象的表达式定义为左值,一方面某些左值却不能放在赋值运算符的左侧。此时,标准对左值的定义已经不能满足当前的状况。为此,c标准新增了一个术语:可修改的左值(modifiable lvalue),用于标识可修改的对象。所以赋值运算符的左侧应该是可修改的左值。

  1. 右值:

指的是能赋值给可修改左值的量,且本身不是左值。

考虑下面的语句:

bmw=2002;

这里bmw是可修改的左值,2002是右值。右值可以是常量、变量或其他可求值的表达式(如函数调用)。实际上,当前标准在描述这一概念是使用的是表达式的值(value of an expression),而不是右值。

示例:

int ex;

int why;

int zee;

const int two=2;

why=42;

zee=why;

ex=tow*(why+zee);

这里,ex、why和zee都是可修改的左值(或对象定位值),它们可用于赋值运算符的左侧和右侧。two是不可修改的左值,它只能用于赋值运算符的右侧(在该例中,two被初始化为2,这里的=运算符表示初始化而不是赋值,因此并未违反规则)。同时42是右值,它不能引用某指定内存的位置。另外,why和zee是可修改的左值,表达式(why+zee)是右值,该表达式不能表示特定内存位置,而且也不能给它赋值。它只是程序计算的一个临时值,在计算完毕后便会被丢弃。

  1. 右值引用

为了支持移动操作,c++新标准引入了一种新的引用类型——右值引用。所谓右值引用就是必须绑定到右值的引用。我们通过&&而不是&来获得右值引用。如我们将要看到的,右值引用有一个重要的性质:只能绑定到一个将要销毁的对象。因此,我们可以自由地将一个右值引用的资源“移动”到另一个对象中。

回忆一下,左值和右值是表达式的属性。一些表达式生成或要求左值,而另外一些则生成或者要求右值。一般而言,一个左值表达式表示的是一个对象的身份,而一个右值表达式表示的是对象的值。

类似任何引用,一个右值引用也不过是某个对象的另一个名字而已。午我们所知,对于常规引用(为了与右值引用区分开),我们可以称之为左值引用,我们不能将其绑定到要求转换的表达式、字面常量或是返回右值的表达式。右值引用有着完全相反的绑定特性:我们可以将一个右值绑定到这类表达式上,但不能将一个右值引用直接绑定到一个左值上。

int i=42;

int &r=i;       //正确:r引用i。

int &&rr=i     //错误:不能将一个右值绑定到一个左值上。

int &r2=i*42;   //错误:i*42是一个右值。

const int &r3=i*42; //正确:我们可以将一个const的引用绑定到一个右值上。

Int &&rr2=i*42;   //正确:将rr2绑定到乘法结果上。

返回左值引用的函数,连同赋值、下标、解引用和前置递增/递减运算符,都是返回左值表达式的例子。我们可以将一个左值引用绑定到这类表达式的结果上。

返回非引用类型的函数,连同算术、关系、位以及后置递增/递减运算符,都生成右值。我们不能将一个左值引用绑定到这类表达式上,但我们可以将一个const的左值引用或者一个右值引用绑定到这类表达式上。

左值持久,右值短暂

考察左值和右值表达式的列表,两者相互区别之处就很明显了,左值有持久的状态,而右值要么是字面常量,要么是表达式求值过程中创建的临时对象。

由于右值引用只能绑定到临时对象,我们得知:所有引用的对象将要销毁,该对象没有其他用户。这两个特性意味着:使用右值引用的代码可以自由地接管所引用的对象的资源。

标准库move函数

虽然不能将一个右值引用直接绑定到一个左值上,但我们可以显示地将一个左值转换为对应的右值引用类型。我们还可以通过调用一个名为move的新标准库函数来获得绑定到左值上的右值引用,次函数在头文件utility中。Move函数来返回给定对象的右值引用。

         Int &&r3=std::move(rr1); 

move调用告诉编译器:我们有一个左值,但我们希望像一个右值一样处理它。我们必须认识到,调用move就意味着承诺:除了对rr1赋值或销毁它外,我们将不再使用它。在调用move之后,我们不能对移后源对象的值做任何假设。

(我们可以销毁一个移后源对象,也可以赋新值,但不能使用一个移后源对象的值。)

  1. extern 关键字

1.用法:

(1)extern c:以c的规则进行编译。

(2)C++语言支持分离式编译机制,该机制允许将程序分割为若干个文件,每个文件可被独立编译。为了将程序分为许多文件,则需要在文件中共享代码,例如一个文件的代码可能需要另一个文件中中定义的变量。在C语言中,修饰符extern用在变量或者函数的声明前,用来说明“此变量/函数是在别处定义的,要在此处引用”。

2.

extern 修饰变量的声明

如果文件a.c需要引用b.c中变量int v,就可以在a.c中声明extern int v,然后就可以引用变量v。

这里需要注意的是,被引用的变量v的链接属性必须是外链接(external)的,也就是说a.c要引用到v,不只是取决于在a.c中声明extern int v,还取决于变量v本身是能够被引用到的。

这涉及到c语言的另外一个话题--变量的作用域。能够被其他模块以extern修饰符引用到的变量通常是全局变量。

还有很重要的一点是,extern int v可以放在a.c中的任何地方,比如你可以在a.c中的函数fun定义的开头处声明extern int v,然后就可以引用到变量v了,只不过这样只能在函数fun作用域中引用v罢了,这还是变量作用域的问题。对于这一点来说,很多人使用的时候都心存顾虑。好像extern声明只能用于文件作用域似的。

extern 修饰函数声明

从本质上来讲,变量和函数没有区别。函数名是指向函数二进制块开头处的指针。

如果文件a.c需要引用b.c中的函数,比如在b.c中原型是int fun(int mu),那么就可以在a.c中声明extern int fun(int mu),然后就能使用fun来做任何事情。

就像变量的声明一样,extern int fun(int mu)可以放在a.c中任何地方,而不一定非要放在a.c的文件作用域的范围中。

对其他模块中函数的引用,最常用的方法是包含这些函数声明的头文件。使用extern和包含头文件来引用函数有什么区别呢?extern的引用方式比包含头文件要简洁得多!extern的使用方法是直接了当的,想引用哪个函数就用extern声明哪个函数。

这样做的一个明显的好处是,会加速程序的编译(确切的说是预处理)的过程,节省时间。在大型C程序编译过程中,这种差异是非常明显的。

extern修饰符可用于指示c或者c++函数的调用规范

比如在C++中调用C库函数,就需要在C++程序中用extern “C”声明要引用的函数。

这是给链接器用的,告诉链接器在链接的时候用C函数规范来链接。主要原因是C++和C程序编译完成后在目标代码中命名规则不同。

为了支持分离式编译,C++允许将声明和定义分离开来。变量的声明规定了变量的类型和名字,即使一个名字为程序所知,一个文件如果想使用别处定义的名字则必须包含对那个名字的声明。定义则负责创建与名字关联的实体,定义还申请存储空间。

  1. 如何防止头文件被重复包含
  1. 使用条件编译,所有头文件格式如下

#ifndef _HEADERNAME_H

#define _HEADERNAME_H

...//(头文件内容)

#endif

  1. windows平台下,使用宏命令#progma once

24.什么是内存泄露?野指针?内存越界?如何避免。

1.内存泄露:程序员动态申请的内存没有释放。

2.野指针:指向内存被释放的内存或者没有访问权限的内存的指针。

成因:

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

char *p = NULL;

char *str = new char(100);

  1. 指针p被free或者delete之后,没有置为null。
  2. 指针操作超越了变量的作用范围。这种情况让人防不胜防

class A

{

public:

void Func(void){ cout << “Func of class A” << endl; }

};

void Test(void)

{

  A  *p;

  if(...){

 A  a;

p = &a; // 注意 a 的生命期

  }

     p->Func(); // p是“野指针”

}

内存越界:何谓内存访问越界,简单的说,你向系统申请了一块内存,在使用这块内存的时候,超出了你申请的范围。可能发生内存的改写,影响附近内存块的数据安全。

  1. 出core文件
  1. 程序崩溃时程序运行时的保存的内存信息的coredump文件,可以通过sysctl或者/proc中来设置core文件的文件名以及生成的路径等。一般的coredump文件为ELF格式,coredump包含了程序运行时的内存,寄存器状态,堆栈指针,内存管理信息等。许多程序和操作系统出错时会自动生成一个core文件。coredump可以用在很多场合,使用Linux系统在跑一些压力测试或者系统负载一大的话,系统就hang住了或者干脆system panic。这时唯一能帮助你分析和解决问题的就是coredump了。通常进程或者内核收到。
  2. 相关命令

ulimit命令改变shell的资源限制,显示shell的资源限制,coredump项值为coredump文件大小单位blocks(4kbytes),程序崩溃时的行为不可按平常时的行为来估计,比如缓冲区溢出等错误可能导致堆栈被破坏,因此经常会出现某个变量的值被修改成乱七八糟的,然后程序用这个大小去申请内存就可能导致程序比平常时多占用很多内存。因此无论程序正常运行时占用的内存多么少,要保证生成Core文件还是将大小限制设为unlimited为好。一般使用ulimit -c unlimited不限制coredump文件大小,生成文件太小gdb时候会报错。

开启系统的coredump步骤为:

第一步,开启内核宏支持coredump函数(对应的进程重新编译并编译中-g要加上才能正常显示gdb信息)

 

第二步,命令设置coredump文件大小,ulimit -c unlimited表示coredump没有限制或者ulimit -c 1024支持文件大小1024k,如果进程脱离终端利用getrlimit,setrlimit如下所示

 

第三步,设置文件生成路径以及文件名。可以/proc/sys/kernel/core_pattern和/proc/sys/kernel/core_uses_pi来设置或者sysctl -w kernel.core_uses_pid =0 sysctl -w kernel.core_pattern = /var/core.%e.%p设置

3)调式

如上述步骤成功则会生成对应的core文件,如果是大型服务器中core文件可以直接gdb进行调试,这里只说明在嵌入式中如何利用gdb达到调试的目的。对应目录生成的core文件从系统中拷贝出来类似tftp命令 ftp或者利用u盘拷贝。嵌入式中不能拷贝出来coredump文件那之前设置都是白费的。

成功获取core文件,并将拷贝的core文件放入对应的process的程序工程目录下同一目录下且工程目录有process生成bin文件,cd到process的目录

XXX-XXX-XX-gdb bin core

进入gdb模式,调试中可能有一些库要用到,所以还要设置gdb中调用库的库文件的绝对路径,一般linux嵌入式一般是生成的文件系统作为调用路径

initially, you will see a lot of error messages. They can be ignored.  Now on the gdb prompt, type:

(gdb) set solib-absolute-prefix SRCPATH/targets/PROFILE/fs.install

(gdb) bt

bt之后可以看见打印的堆栈信息。

  1. 设计模式及实现(c++)

singleton:单例模式。使用static成员函数中构造local static变量的方法来实现,因为static成员只能初始化一次。

template<typename T>

class Singleton

{

public:

    static T& getInstance()

    {

        if (!value_)

        {

            value_ = new T();

        }

        return *value_;

    }

private:

    Singleton();

    ~Singleton();

    static T* value_;

};

template<typename T>

T* Singleton<T>::value_ = NULL;

  1. c++四种强制类型转换

1)const_cast

常量指针被转化成非常量的指针,并且仍然指向原来的对象;常量引用被转换成非常量的引用,并且仍然指向原来的对象。

2)static_cast

static_cast作用和c语言风格强制转换的 效果基本一样,由于没有运行时类型检查来保证转换的安全性,所以这类型的强制转换和c语言风格的强制转换都有安全隐患;

用于类层次结构中基类和派生类之间指针或引用的转换。(进行上行转换:把派生类的指针或引用转换成基类表示是安全的,进行下行转换:把基类指针或引用转换成派生类表示时,由于没有动态类型检查,所以是不安全的。);

用于基本数据类型之间的转换;

static_cast不能转换调原有类型的const、volatile、或者_unaligned属性;

c++的任何隐式转换都是使用static_cast来实现。

3)dynamic_cast

会在程序运行时对类型转换对“运行期类型信息”进行检查。

4)reinterpret_cast

用来处理无关类型转换的, 通常为操作数的位模式提供较低层次的重新解释。(仅仅是重新解释了给出的对象的比特模型,并没有进行二进制的转换),用在任意指针之间的转换,引用之间的转换,指针和足够大的int型之间的转换,整数到指针的转换。

  1. c++抽象类
  1. 定义: 纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0”。
  2. 引入原因

第一:为了方便使用多态特性,我们常常需要在基类中定义虚拟函数。

第二:在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。

为了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数(方法:virtual ReturnType Function()= 0;),则编译器要求在派生类中必须予以重载以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。这样就很好地解决了上述两个问题。

  • 网络

总体概述

一 OSI与TCP/IP各层的结构与功能,都有哪些协议?

学习计算机网络时我们一般采用折中的办法,也就是中和 OSI 和 TCP/IP 的优点,采用一种只有五层协议的体系结构,这样既简洁又能将概念阐述清楚。

 

结合互联网的情况,自上而下地,非常简要的介绍一下各层的作用。

1.1 应用层

应用层(application-layer)的任务是通过应用进程间的交互来完成特定网络应用。应用层协议定义的是应用进程(进程:主机中正在运行的程序)间的通信和交互的规则。对于不同的网络应用需要不同的应用层协议。在互联网中应用层协议很多,如域名系统DNS,支持万维网应用的 HTTP协议,支持电子邮件的 SMTP协议等等。我们把应用层交互的数据单元称为报文。

 

域名系统

 

域名系统(Domain Name System缩写 DNS,Domain Name被译为域名)是因特网的一项核心服务,它作为可以将域名和IP地址相互映射的一个分布式数据库,能够使人更方便的访问互联网,而不用去记住能够被机器直接读取的IP数串。(百度百科)例如:一个公司的 Web 网站可看作是它在网上的门户,而域名就相当于其门牌地址,通常域名都使用该公司的名称或简称。例如上面提到的微软公司的域名,类似的还有:IBM 公司的域名是 www.ibm.com、Oracle 公司的域名是 www.oracle.com、Cisco公司的域名是 www.cisco.com 等。

 

HTTP协议

 

超文本传输协议(HTTP,HyperText Transfer Protocol)是互联网上应用最为广泛的一种网络协议。所有的 WWW(万维网) 文件都必须遵守这个标准。设计 HTTP 最初的目的是为了提供一种发布和接收 HTML 页面的方法。(百度百科)

 

1.2 运输层

运输层(transport layer)的主要任务就是负责向两台主机进程之间的通信提供通用的数据传输服务。应用进程利用该服务传送应用层报文。“通用的”是指并不针对某一个特定的网络应用,而是多种应用可以使用同一个运输层服务。由于一台主机可同时运行多个线程,因此运输层有复用和分用的功能。所谓复用就是指多个应用层进程可同时使用下面运输层的服务,分用和复用相反,是运输层把收到的信息分别交付上面应用层中的相应进程。

 

运输层主要使用以下两种协议:

 

传输控制协议 TCP(Transmission Control Protocol)--提供面向连接的,可靠的数据传输服务。

用户数据协议 UDP(User Datagram Protocol)--提供无连接的,尽最大努力的数据传输服务(不保证数据传输的可靠性)。

TCP 与 UDP 的对比见问题三。

 

1.3 网络层

在 计算机网络中进行通信的两个计算机之间可能会经过很多个数据链路,也可能还要经过很多通信子网。网络层的任务就是选择合适的网间路由和交换结点, 确保数据及时传送。 在发送数据时,网络层把运输层产生的报文段或用户数据报封装成分组和包进行传送。在 TCP/IP 体系结构中,由于网络层使用 IP 协议,因此分组也叫 IP 数据报 ,简称 数据报。

 

这里要注意:不要把运输层的“用户数据报 UDP ”和网络层的“ IP 数据报”弄混。另外,无论是哪一层的数据单元,都可笼统地用“分组”来表示。

 

这里强调指出,网络层中的“网络”二字已经不是我们通常谈到的具体网络,而是指计算机网络体系结构模型中第三层的名称.

 

互联网是由大量的异构(heterogeneous)网络通过路由器(router)相互连接起来的。互联网使用的网络层协议是无连接的网际协议(Intert Protocol)和许多路由选择协议,因此互联网的网络层也叫做网际层或IP层。

 

1.4 数据链路层

数据链路层(data link layer)通常简称为链路层。两台主机之间的数据传输,总是在一段一段的链路上传送的,这就需要使用专门的链路层的协议。 在两个相邻节点之间传送数据时,数据链路层将网络层交下来的 IP 数据报组装程帧,在两个相邻节点间的链路上传送帧。每一帧包括数据和必要的控制信息(如同步信息,地址信息,差错控制等)。

 

在接收数据时,控制信息使接收端能够知道一个帧从哪个比特开始和到哪个比特结束。这样,数据链路层在收到一个帧后,就可从中提出数据部分,上交给网络层。 控制信息还使接收端能够检测到所收到的帧中有误差错。如果发现差错,数据链路层就简单地丢弃这个出了差错的帧,以避免继续在网络中传送下去白白浪费网络资源。如果需要改正数据在链路层传输时出现差错(这就是说,数据链路层不仅要检错,而且还要纠错),那么就要采用可靠性传输协议来纠正出现的差错。这种方法会使链路层的协议复杂些。

 

1.5 物理层

在物理层上所传送的数据单位是比特。 物理层(physical layer)的作用是实现相邻计算机节点之间比特流的透明传送,尽可能屏蔽掉具体传输介质和物理设备的差异。 使其上面的数据链路层不必考虑网络的具体传输介质是什么。“透明传送比特流”表示经实际电路传送后的比特流没有发生变化,对传送的比特流来说,这个电路好像是看不见的。

 

在互联网使用的各种协中最重要和最著名的就是 TCP/IP 两个协议。现在人们经常提到的TCP/IP并不一定单指TCP和IP这两个具体的协议,而往往表示互联网所使用的整个TCP/IP协议族。

1.6 总结一下

上面我们对计算机网络的五层体系结构有了初步的了解,下面附送一张七层体系结构图总结一下。

 

 TCP 三次握手和四次挥

为了准确无误地把数据送达目标处,TCP协议采用了三次握手策略。

2.1 TCP 三次握手漫画图解

 

 

2.2 为什么要三次握手两次握手可不可以

1)三次握手的目的是建立可靠的通信信道,说到通讯,简单来说就是数据的发送与接收,而三次握手最主要的目的就是双方确认自己与对方的发送与接收是正常的。

 

第一次握手:Client 什么都不能确认;Server 确认了对方发送正常

 

第二次握手:Client 确认了:自己发送、接收正常,对方发送、接收正常;Server 确认了:自己接收正常,对方发送正常

 

第三次握手:Client 确认了:自己发送、接收正常,对方发送、接收正常;Server 确认了:自己发送、接收正常,对方发送、接收正常

 

所以三次握手就能确认双发收发功能都正常,缺一不可。

2)我们知道,3次握手完成两个重要的功能,既要双方做好发送数据的准备工作(双方都知道彼此已准备好),也要允许双方就初始序列号进行协商,这个序列号在握手过程中被发送和确认。

现在把三次握手改成仅需要两次握手,死锁是可能发生的。作为例子,考虑计算机S和C之间的通信,假定C给S发送一个连接请求分组,S收到了这个分组,并发 送了确认应答分组。按照两次握手的协定,S认为连接已经成功地建立了,可以开始发送数据分组。可是,C在S的应答分组在传输中被丢失的情况下,将不知道S 是否已准备好,不知道S建立什么样的序列号,C甚至怀疑S是否收到自己的连接请求分组。在这种情况下,C认为连接还未建立成功,将忽略S发来的任何数据分 组,只等待连接确认应答分组。而S在发出的分组超时后,重复发送同样的分组。这样就形成了死锁。

2.3 为什么要传回 SYN

接收端传回发送端所发送的 SYN 是为了告诉发送端,我接收到的信息确实就是你所发送的信号了。

 

SYN 是 TCP/IP 建立连接时使用的握手信号。在客户机和服务器之间建立正常的 TCP 网络连接时,客户机首先发出一个 SYN 消息,服务器使用 SYN-ACK 应答表示接收到了这个消息,最后客户机再以 ACK(Acknowledgement[汉译:确认字符 ,在数据通信传输中,接收站发给发送站的一种传输控制字符。它表示确认发来的数据已经接受无误。 ])消息响应。这样在客户机和服务器之间才能建立起可靠的TCP连接,数据才可以在客户机和服务器之间传递。

2.4四次挥手

 

TCP的连接的拆除需要发送四个包,因此称为四次挥手(four-way handshake)。客户端或服务器均可主动发起挥手动作,在socket编程中,任何一方执行close()操作即可产生挥手操作。

客户端-发送一个 FIN,用来关闭客户端到服务器的数据传送

服务器-收到这个 FIN,它发回一 个 ACK,确认序号为收到的序号加1 。和 SYN 一样,一个 FIN 将占用一个序号

服务器-关闭与客户端的连接,发送一个FIN给客户端

客户端-发回 ACK 报文确认,并将确认序号设置为收到序号加1

2.5为什么建立连接协议是三次握手,而关闭连接却是四次握手呢?

这是因为服务端的LISTEN状态下的SOCKET当收到SYN报文的连接请求后,它可以把ACK和SYN(ACK起应答作用,而SYN起同步作用)放在一个报文里来发送。但关闭连接时,当收到对方的FIN报文通知时,它仅仅表示对方没有数据发送给你了;但未必你所有的数据都全部发送给对方了,所以你可能未必会马上会关闭SOCKET,也即你可能还需要发送一些数据给对方之后,再发送FIN报文给对方来表示你同意现在可以关闭连接了,所以它这里的ACK报文和FIN报文多数情况下都是分开发送的。

2.6为什么TIME_WAIT状态还需要等2MSL后才能返回到CLOSED状态?

(1)可靠的实现TCP全双工链接的终止。

 

这是因为虽然双方都同意关闭连接了,而且握手的4个报文也都协调和发送完毕,按理可以直接回到CLOSED状态(就好比从SYN_SEND状态到ESTABLISH状态那样);但是因为我们必须要假想网络是不可靠的,你无法保证你最后发送的ACK报文会一定被对方收到,因此对方处于LAST_ACK状态下的SOCKET可能会因为超时未收到ACK报文,而重发FIN报文,所以这个TIME_WAIT状态的作用就是用来重发可能丢失的ACK报文。

(2)允许老的重复的分节在网络中消逝。

假 设在12.106.32.254的1500端口和206.168.1.112.219的21端口之间有一个TCP连接。我们关闭这个链接,过一段时间后在 相同的IP地址和端口建立另一个连接。后一个链接成为前一个的化身。因为它们的IP地址和端口号都相同。TCP必须防止来自某一个连接的老的重复分组在连 接已经终止后再现,从而被误解成属于同一链接的某一个某一个新的化身。为做到这一点,TCP将不给处于TIME_WAIT状态的链接发起新的化身。既然 TIME_WAIT状态的持续时间是MSL的2倍,这就足以让某个方向上的分组最多存活msl秒即被丢弃,另一个方向上的应答最多存活msl秒也被丢弃。 通过实施这个规则,我们就能保证每成功建立一个TCP连接时。来自该链接先前化身的重复分组都已经在网络中消逝了。

2.7MSL与TTL

MSL是Maximum Segment Lifetime英文的缩写,中文可以译为“报文最大生存时间”,他是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。因为tcp报文(segment)是ip数据报(datagram)的数据部分,具体称谓请参见《数据在网络各层中的称呼》一文,而ip头中有一个TTL域,TTL是time to live的缩写,中文可以译为“生存时间”,这个生存时间是由源主机设置初始值但不是生存的具体时间,而是存储了一个ip数据报可以经过的最大路由数,每经过一个处理他的路由器此值就减1,当此值为0则数据报将被丢弃,同时发送ICMP报文通知源主机。RFC 793中规定MSL为2分钟,实际应用中常用的是30秒,1分钟和2分钟等。

2MSL即两倍的MSL,TCP的TIME_WAIT状态也称为2MSL等待状态,当TCP的一端发起主动关闭,在发出最后一个ACK包后,即第3次握手完成后发送了第四次握手的ACK包后就进入了TIME_WAIT状态,必须在此状态上停留两倍的MSL时间,等待2MSL时间主要目的是怕最后一个ACK包对方没收到,那么对方在超时后将重发第三次握手的FIN包,主动关闭端接到重发的FIN包后可以再发一个ACK应答包。在TIME_WAIT状态时两端的端口不能使用,要等到2MSL时间结束才可继续使用。当连接处于2MSL等待阶段时任何迟到的报文段都将被丢弃。不过在实际应用中可以通过设置SO_REUSEADDR选项达到不必等待2MSL时间结束再使用此端口。

TTL与MSL是有关系的但不是简单的相等的关系,MSL要大于等于TTL。

TCP,UDP 协议的区别

 

 

UDP 在传送数据之前不需要先建立连接,远地主机在收到 UDP 报文后,不需要给出任何确认。虽然 UDP 不提供可靠交付,但在某些情况下 UDP 确是一种最有效的工作方式(一般用于即时通信),比如: QQ 语音、 QQ 视频 、直播等等

TCP 提供面向连接的服务。在传送数据之前必须先建立连接,数据传送结束后要释放连接。 TCP 不提供广播或多播服务。由于 TCP 要提供可靠的,面向连接的传输服务(TCP的可靠体现在TCP在传递数据之前,会有三次握手来建立连接,而且在数据传递时,有确认、窗口、重传、拥塞控制机制,在数据传完后,还会断开连接用来节约系统资源),这一难以避免增加了许多开销,如确认,流量控制,计时器以及连接管理等。这不仅使协议数据单元的首部增大很多,还要占用许多处理机资源。TCP 一般用于文件传输、发送和接收邮件、远程登录等场景。

四 TCP 协议如何保证可靠传输

 

1)应用数据被分割成 TCP 认为最适合发送的数据块。

2)TCP 给发送的每一个包进行编号,接收方对数据包进行排序,把有序数据传送给应用层。

3)校验和: TCP 将保持它首部和数据的检验和。这是一个端到端的检验和,目的是检测数据在传输过程中的任何变化。如果收到段的检验和有差错,TCP 将丢弃这个报文段和不确认收到此报文段。

4)TCP 的接收端会丢弃重复的数据。

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

6)拥塞控制: 当网络拥塞时,减少数据的发送。

7)ARQ协议: 也是为了实现可靠传输的,它的基本原理就是每发完一个分组就停止发送,等待对方确认。在收到确认后再发下一个分组。

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

4.1 ARQ协议

自动重传请求(Automatic Repeat-reQuest,ARQ)是OSI模型中数据链路层和传输层的错误纠正协议之一。它通过使用确认和超时这两个机制,在不可靠服务的基础上实现可靠的信息传输。如果发送方在发送后一段时间之内没有收到确认帧,它通常会重新发送。ARQ包括停止等待ARQ协议和连续ARQ协议。

 

停止等待ARQ协议

停止等待协议是为了实现可靠传输的,它的基本原理就是每发完一个分组就停止发送,等待对方确认(回复ACK)。如果过了一段时间(超时时间后),还是没有收到 ACK 确认,说明没有发送成功,需要重新发送,直到收到确认后再发下一个分组;

在停止等待协议中,若接收方收到重复分组,就丢弃该分组,但同时还要发送确认;

优点: 简单

 

缺点: 信道利用率低,等待时间长

 

  1. 无差错情况:

 

发送方发送分组,接收方在规定时间内收到,并且回复确认.发送方再次发送。

2) 出现差错情况(超时重传): 停止等待协议中超时重传是指只要超过一段时间仍然没有收到确认,就重传前面发送过的分组(认为刚才发送过的分组丢失了)。因此每发送完一个分组需要设置一个超时计时器,其重传时间应比数据在分组传输的平均往返时间更长一些。这种自动重传方式常称为 自动重传请求 ARQ 。另外在停止等待协议中若收到重复分组,就丢弃该分组,但同时还要发送确认。连续 ARQ 协议 可提高信道利用率。发送维持一个发送窗口,凡位于发送窗口内的分组可连续发送出去,而不需要等待对方确认。接收方一般采用累积确认,对按序到达的最后一个分组发送确认,表明到这个分组位置的所有分组都已经正确收到了。

 

 

 

 

 

 

 

 

3) 确认丢失和确认迟到

 

确认丢失:确认消息在传输过程丢失

 

 

 

 

当A发送M1消息,B收到后,B向A发送了一个M1确认消息,但却在传输过程中丢失。而A并不知道,在超时计时过后,A重传M1消息,B再次收到该消息后采取以下两点措施:

 

I.丢弃这个重复的M1消息,不向上层交付。

Ii.向A发送确认消息。(不会认为已经发送过了,就不再发送。A能重传,就证明B的确认消息丢失)。

确认迟到 :确认消息在传输过程中迟到。

 

 A发送M1消息,B收到并发送确认。在超时时间内没有收到确认消息,A重传M1消息,B仍然收到并继续发送确认消息(B收到了2份M1)。此时A收到了B第二次发送的确认消息。接着发送其他数据。过了一会,A收到了B第一次发送的对M1的确认消息(A也收到了2份确认消息)。处理如下:

I.A收到重复的确认后,直接丢弃。

II.B收到重复的M1后,也直接丢弃重复的M1。

连续ARQ协议

连续 ARQ 协议可提高信道利用率。发送方维持一个发送窗口,凡位于发送窗口内的分组可以连续发送出去,而不需要等待对方确认。接收方一般采用累计确认,对按序到达的最后一个分组发送确认,表明到这个分组为止的所有分组都已经正确收到了。

 

优点: 信道利用率高,容易实现,即使确认丢失,也不必重传。

 

缺点: 不能向发送方反映出接收方已经正确收到的所有分组的信息。 比如:发送方发送了 5条 消息,中间第三条丢失(3号),这时接收方只能对前两个发送确认。发送方无法知道后三个分组的下落,而只好把后三个全部重传一次。这也叫 Go-Back-N(回退 N),表示需要退回来重传已经发送过的 N 个消息。

4.2 滑动窗口和流量控制

TCP 利用滑动窗口实现流量控制。流量控制是为了控制发送方发送速率,保证接收方来得及接收。 接收方发送的确认报文中的窗口字段可以用来控制发送方窗口大小,从而影响发送方的发送速率。将窗口字段设置为 0,则发送方不能发送数据。

4.3 拥塞控制

在某段时间,若对网络中某一资源的需求超过了该资源所能提供的可用部分,网络的性能就要变坏。这种情况就叫拥塞。拥塞控制就是为了防止过多的数据注入到网络中,这样就可以使网络中的路由器或链路不致过载。拥塞控制所要做的都有一个前提,就是网络能够承受现有的网络负荷。拥塞控制是一个全局性的过程,涉及到所有的主机,所有的路由器,以及与降低网络传输性能有关的所有因素。相反,流量控制往往是点对点通信量的控制,是个端到端的问题。流量控制所要做到的就是抑制发送端发送数据的速率,以便使接收端来得及接收。

 

为了进行拥塞控制,TCP 发送方要维持一个 拥塞窗口(cwnd) 的状态变量。拥塞控制窗口的大小取决于网络的拥塞程度,并且动态变化。发送方让自己的发送窗口取为拥塞窗口和接收方的接受窗口中较小的一个。

 

TCP的拥塞控制采用了四种算法,即 慢开始 、拥塞避免 、快重传 和 快恢复。在网络层也可以使路由器采用适当的分组丢弃策略(如主动队列管理 AQM),以减少网络拥塞的发生。

慢开始:慢开始算法的思路是当主机开始发送数据时,如果立即把大量数据字节注入到网络,那么可能会引起网络阻塞,因为现在还不知道网络的符合情况。经验表明,较好的方法是先探测一下,即由小到大逐渐增大发送窗口,也就是由小到大逐渐增大拥塞窗口数值。cwnd初始值为1,每经过一个传播轮次,cwnd加倍。

 

拥塞避免: 拥塞避免算法的思路是让拥塞窗口cwnd缓慢增大,即每经过一个往返时间RTT就把发送放的cwnd加1.

快重传与快恢复:在 TCP/IP 中,快速重传和恢复(fast retransmit and recovery,FRR)是一种拥塞控制算法,它能快速恢复丢失的数据包。没有 FRR,如果数据包丢失了,TCP 将会使用定时器来要求传输暂停。在暂停的这段时间内,没有新的或复制的数据包被发送。有了 FRR,如果接收机接收到一个不按顺序的数据段,它会立即给发送机发送一个重复确认。如果发送机接收到三个重复确认,它会假定确认件指出的数据段丢失了,并立即重传这些丢失的数据段。有了 FRR,就不会因为重传时要求的暂停被耽误。当有单独的数据包丢失时,快速重传和恢复(FRR)能最有效地工作。当有多个数据信息包在某一段很短的时间内丢失时,它则不能很有效地工作。

 

 

五 在浏览器中输入url地址 ->> 显示主页的过程

 

总体来说分为以下几个过程:

DNS解析

TCP连接

发送HTTP请求

服务器处理请求并返回HTTP报文

浏览器解析渲染页面

连接结束

具体可以参考下面这篇文章:

https://segmentfault.com/a/1190000006879700

六 状态码

 

 

七 HTTP协议

7.1 HTTP协议与各种协议的关系

 

7.2 HTTP长连接,短连接

在HTTP/1.0中默认使用短连接。也就是说,客户端和服务器每进行一次HTTP操作,就建立一次连接,任务结束就中断连接。当客户端浏览器访问的某个HTML或其他类型的Web页中包含有其他的Web资源(如JavaScript文件、图像文件、CSS文件等),每遇到这样一个Web资源,浏览器就会重新建立一个HTTP会话。

 

而从HTTP/1.1起,默认使用长连接,用以保持连接特性。使用长连接的HTTP协议,会在响应头加入这行代码:

 

Connection:keep-alive

在使用长连接的情况下,当一个网页打开完成后,客户端和服务器之间用于传输HTTP数据的TCP连接不会关闭,客户端再次访问这个服务器时,会继续使用这一条已经建立的连接。Keep-Alive不会永久保持连接,它有一个保持时间,可以在不同的服务器软件(如Apache)中设定这个时间。实现长连接需要客户端和服务端都支持长连接。

 

HTTP协议的长连接和短连接,实质上是TCP协议的长连接和短连接。

 

—— 《HTTP长连接、短连接究竟是什么?》

https://www.cnblogs.com/gotodsp/p/6366163.html

7.3 HTTP断点续传

HTTP1.1协议(RFC2616)中定义了断点续传相关的HTTP头 Range和Content-Range字段,一个最简单的断点续传实现大概如下: 
  1客户端下载一个1024K的文件,已经下载了其中512K 
  2网络中断,客户端请求续传,因此需要在HTTP头中申明本次需要续传的片段: 
        Range:bytes=512000- 
      这个头通知服务端从文件的512K位置开始传输文件 
  3 服务端收到断点续传请求,从文件的512K位置开始传输,并且在HTTP头中增加: 
    Content-Range:bytes 512000-/1024000 
    并且此时服务端返回的HTTP状态码应该是206,而不是200。 

7.4 HTTP是不保存状态的协议,如何保存用户状态?

HTTP 是一种不保存状态,即无状态(stateless)协议。也就是说 HTTP 协议自身不对请求和响应之间的通信状态进行保存。那么我们保存用户状态呢?Session 机制的存在就是为了解决这个问题,Session 的主要作用就是通过服务端记录用户的状态。典型的场景是购物车,当你要添加商品到购物车的时候,系统不知道是哪个用户操作的,因为 HTTP 协议是无状态的。服务端给特定的用户创建特定的 Session 之后就可以标识这个用户并且跟踪这个用户了(一般情况下,服务器会在一定时间内保存这个 Session,过了时间限制,就会销毁这个Session)。

 

在服务端保存 Session 的方法很多,最常用的就是内存和数据库(比如是使用内存数据库redis保存)。既然 Session 存放在服务器端,那么我们如何实现 Session 跟踪呢?大部分情况下,我们都是通过在 Cookie 中附加一个 Session ID 来方式来跟踪。

 

Cookie 被禁用怎么办?

 

最常用的就是利用 URL 重写把 Session ID 直接附加在URL路径的后面。

7.5 Cookie的作用是什么?和Session有什么区别?

Cookie 和 Session都是用来跟踪浏览器用户身份的会话方式,但是两者的应用场景不太一样。

Cookie 一般用来保存用户信息 比如①我们在 Cookie 中保存已经登录过得用户信息,下次访问网站的时候页面可以自动帮你登录的一些基本信息给填了;②一般的网站都会有保持登录也就是说下次你再访问网站的时候就不需要重新登录了,这是因为用户登录的时候我们可以存放了一个 Token 在 Cookie 中,下次登录的时候只需要根据 Token 值来查找用户即可(为了安全考虑,重新登录一般要将 Token 重写);③登录一次网站后访问网站其他页面不需要重新登录。Session 的主要作用就是通过服务端记录用户的状态。 典型的场景是购物车,当你要添加商品到购物车的时候,系统不知道是哪个用户操作的,因为 HTTP 协议是无状态的。服务端给特定的用户创建特定的 Session 之后就可以标识这个用户并且跟踪这个用户了。

Cookie 数据保存在客户端(浏览器端),Session 数据保存在服务器端。

Cookie 存储在客户端中,而Session存储在服务器上,相对来说 Session 安全性更高。如果使用 Cookie 的一些敏感信息不要写入 Cookie 中,最好能将 Cookie 信息加密然后使用到的时候再去服务器端解密。

7.6 URI和URL的区别是什么?

URI(Uniform Resource Identifier) 是同一资源标志符,可以唯一标识一个资源。

URL(Uniform Resource Location) 是同一资源定位符,可以提供该资源的路径。它是一种具体的 URI,即 URL 可以用来标识一个资源,而且还指明了如何 locate 这个资源。

URI的作用像身份证号一样,URL的作用更像家庭住址一样。URL是一种具体的URI,它不仅唯一标识资源,而且还提供了定位该资源的信息。

7.7 HTTP 和 HTTPS 的区别?

1)端口 :HTTP的URL由“http://”起始且默认使用端口80,而HTTPS的URL由“https://”起始且默认使用端口443。

2)安全性和资源消耗: HTTP协议运行在TCP之上,所有传输的内容都是明文,客户端和服务器端都无法验证对方的身份。HTTPS是运行在SSL/TLS之上的HTTP协议,SSL/TLS 运行在TCP之上。所有传输的内容都经过加密,加密采用对称加密,但对称加密的密钥用服务器方的证书进行了非对称加密。所以说,HTTP 安全性没有 HTTPS高,但是 HTTPS 比HTTP耗费更多服务器资源。

对称加密:密钥只有一个,加密解密为同一个密码,且加解密速度快,典型的对称加密算法有DES、AES等;

非对称加密:密钥成对出现(且根据公钥无法推知私钥,根据私钥也无法推知公钥),加密解密使用不同密钥(公钥加密需要私钥解密,私钥加密需要公钥解密),相对对称加密速度较慢,典型的非对称加密算法有RSA、DSA等。

八 常用的熟知端口号

 

九 HTTPS

 

三.算法

1.所有排序算法

//冒泡排序

void bubbleSort(int a[], int n) {

    for (int i = 0; i < n; i++)

        for (int j = 1; j < n - i; j++) {

            if (a[j - 1] > a[j])

                swap(a[j - 1], a[j]);

        }

}

 

//插入排序

void insertSort(int a[], int n) {

    for (int i = 1; i < n; i++) {

        for (int j = i - 1; j >= 0; --j) {

            if (a[j] > a[j + 1])

                swap(a[j], a[j + 1]);

        }

    }

}

 

//选择排序

void selectSort(int a[], int n) {

 

    for (int i = 0; i < n; i++) {

        int min = a[i];

        int flag = i;

        for (int j = i; j < n; j++) {

            if (a[j] < min) {

                min = a[j];

                flag = j;

            }

        }

        swap(a[i], a[flag]);

    }

}

/**************************************/

/*************************************/

int partition(int a[], int start, int end) {

    int pivot = a[end];

    int i = start - 1;

    int j = start;

    while (j < end) {

        if (a[j] < pivot) {

            ++i;

            swap(a[j], a[i]);

        }

        j++;

    }

    swap(a[i + 1], a[end]);

    return i + 1;

}

 

//快速排序

void quickSort(int a[], int start, int end) {

    //int lhs = start;

    //int rhs = end;

    if (start < end) {

        int q = partition(a, start, end);

        quickSort(a, start, q - 1);

        quickSort(a, q + 1, end);

    }

}

 

//归并排序

void merge(int a[], int start, int mid, int end) {

    int temp[end + 1];

    int i = start;

    int j = mid + 1;

    int index = 0;

    while (i <= mid && j <= end) {

        if (a[i] > a[j])

            temp[index++] = a[j++];

        else

            temp[index++] = a[i++];

    }

    while (i <= mid) {

        temp[index++] = a[i++];

    }

    while (j <= end)

        temp[index++] = a[j++];

    for (int i = 0, j = start; j <= end; i++, j++)

        a[j] = temp[i];

}

 

void mergeSort(int a[], int start, int end) {

    if (start < end) {

        int mid = (start + end) / 2;

        mergeSort(a, start, mid);

        mergeSort(a, mid + 1, end);

        merge(a, start, mid, end);

    }

}

 

void heapAdjust(int a[], int start, int len) {

    int child;     // 子结点的位置 = 2 * 父结点的位置 + 1

    // 得到子结点中键值较大的结点

    for (; 2 * start + 1 < len; start = child) {

        child = 2 * start + 1;

        if (child < len - 1 && a[child + 1] > a[child])

            child++;

        // 如果较大的子结点大于父结点那么把较大的子结点往上移动,替换它的父结点

        if (a[start] < a[child])

            swap(a[start], a[child]);

        else

            break;

    }

 

}

 

//堆排序

void heapSort(int a[], int n) {

    // 调整序列的前半部分元素,调整完之后第一个元素是序列的最大的元素

    for (int i = n / 2 - 1; i >= 0; i--)

        heapAdjust(a, i, n);

    for (int i = n - 1; i > 0; i--) {

        // 将第1个元素与当前最后一个元素交换,保证当前的最后一个位置的元素都是现在的这个序列中最大的

        swap(a[0], a[i]);

        // 不断缩小调整heap的范围,每一次调整完毕保证第一个元素是当前序列的最大值

        heapAdjust(a, 0, i);

    }

}

//希尔排序

void shellSort(int a[], int size){

    int h = 1;

    while (h < size / 3) {

        h = 3 * h + 1;

    }

    while (h >= 1) {

        for (int i = h; i < size; i++) {

            for (size_t j = i; j >= h && a[j] < a[j - h]; j -= h) {

                std::swap(a[j], a[j - h]);

            }

        }

        h = h / 3;

    }

   

}

 

  1. BFS、DFS
  2. Dijkstra

4.Kmp算法

5.计算机计算除法的方式(乘法)

6.背包

7.红黑树

 

四 Python、go

五 数据库

1.数据库建立索引的规则

普遍的规则:

 

1)表的主键、外键必须有索引;

 

2)数据量超过300的表应该有索引;

 

3)经常与其他表进行连接的表,在连接字段上应该建立索引;

 

4)经常出现在Where子句中的字段,特别是大表的字段,应该建立索引;

 

5)索引应该建在选择性高的字段上;

 

6)索引应该建在小字段上,对于大的文本字段甚至超长字段,不要建索引;

 

7)复合索引的建立需要进行仔细分析;尽量考虑用单字段索引代替:

 

   A、正确选择复合索引中的主列字段,一般是选择性较好的字段;

 

   B、复合索引的几个字段是否经常同时以AND方式出现在Where子句中?单字段查询是否极少甚至没有?如果是,则可以建立复合索引;否则考虑单字段索引;

 

   C、如果复合索引中包含的字段经常单独出现在Where子句中,则分解为多个单字段索引;

 

   D、如果复合索引所包含的字段超过3个,那么仔细考虑其必要性,考虑减少复合的字段;

 

   E、如果既有单字段索引,又有这几个字段上的复合索引,一般可以删除复合索引;

 

8)频繁进行数据操作的表,不要建立太多的索引;

 

9)删除无用的索引,避免对执行计划造成负面影响;

 

   以上是一些普遍的建立索引时的判断依据。一言以蔽之,索引的建立必须慎重,对每个索引的必要性都应该经过仔细分析,要有建立的依据。因为太多的索引与不充分、不正确的索引对性能都毫无益处:在表上建立的每个索引都会增加存储开销,索引对于插入、删除、更新操作也会增加处理上的开销。另外,过多的复合索引,在有单字段索引的情况下,一般都是没有存在价值的;相反,还会降低数据增加删除时的性能,特别是对频繁更新的表来说,负面影响更大

六.其他

1.多线程调度,效率最高

有多种算法

  1. 先来先服务
  2. 最短优先
  3. 最高优先权优先(FPF)调度算法

此算法常被用于批处理系统中,作为作业调度算法,也作为多种操作系统中的进程调度算法,还可用于实时系统中。当把该算法用于作业调度时,系统将从后备队列中选择若干个优先权最高的作业装入内存。当用于进程调度时,该算法是把处理机分配给就绪队列中优先权最高的进程,这时,又可进一步把该算法分成如下两种。

第一种:非抢占式优先权算法

在这种方式下,系统一旦把处理机分配给就绪队列中优先权最高的进程后,该进程便一直执行下去,直至完成;或因发生某事件使该进程放弃处理机时,系统方可再将处理机重新分配给另一优先权最高的进程。这种调度算法主要用于批处理系统中;也可用于某些对实时性要求不严的实时系统中。

第二种:抢占式优先权调度算法

在这种方式下,系统同样是把处理机分配给优先权最高的进程,使之执行。但在其执行期间,只要又出现了另一个其优先权更高的进程,进程调度程序就立即停止当前进程(原优先权最高的进程)的执行,重新将处理机分配给新到的优先权最高的进程。因此,在采用这种调度算法时,是每当系统中出现一个新的就绪进程i 时,就将其优先权Pi与正在执行的进程j 的优先权Pj进行比较。如果Pi≤Pj,原进程Pj便继续执行;但如果是Pi>Pj,则立即停止Pj的执行,做进程切换,使i 进程投入执行。显然,这种抢占式的优先权调度算法能更好地满足紧迫作业的要求,故而常用于要求比较严格的实时系统中,以及对性能要求较高的批处理和分时系统中。

  1. 高响应比优先调度算法

该优先权的变化规律可描述为:

 

 

由于等待时间与服务时间之和就是系统对该作业的响应时间,故该优先权又相当于响应比RP。据此,又可表示为:

 

 

由上式可以看出:

(1) 如果作业的等待时间相同,则要求服务的时间愈短,其优先权愈高,因而该算法有利于短作业。

(2) 当要求服务的时间相同时,作业的优先权决定于其等待时间,等待时间愈长,其优先权愈高,因而它实现的是先来先服务。

(3) 对于长作业,作业的优先级可以随等待时间的增加而提高,当其等待时间足够长时,其优先级便可升到很高,从而也可获得处理机。

简言之,该算法既照顾了短作业,又考虑了作业到达的先后次序,不会使长作业长期得不到服务。因此,该算法实现了一种较好的折衷。当然,在利用该算法时,每要进行调度之前,都须先做响应比的计算,这会增加系统开销。

5)3.1 时间片轮转法

在早期的时间片轮转法中,系统将所有的就绪进程按先来先服务的原则排成一个队列,每次调度时,把CPU 分配给队首进程,并令其执行一个时间片。时间片的大小从几ms 到几百ms。当执行的时间片用完时,由一个计时器发出时钟中断请求,调度程序便据此信号来停止该进程的执行,并将它送往就绪队列的末尾;然后,再把处理机分配给就绪队列中新的队首进程,同时也让它执行一个时间片。这样就可以保证就绪队列中的所有进程在一给定的时间内均能获得一时间片的处理机执行时间。换言之,系统能在给定的时间内响应所有用户的请求。

  1. 多级反馈队列调度算法

前面介绍的各种用作进程调度的算法都有一定的局限性。如短进程优先的调度算法,仅照顾了短进程而忽略了长进程,而且如果并未指明进程的长度,则短进程优先和基于进程长度的抢占式调度算法都将无法使用。而多级反馈队列调度算法则不必事先知道各种进程所需的执行时间,而且还可以满足各种类型进程的需要,因而它是目前被公认的一种较好的进程调度算法。在采用多级反馈队列调度算法的系统中,调度算法的实施过程如下所述。

(1) 应设置多个就绪队列,并为各个队列赋予不同的优先级。第一个队列的优先级最高,第二个队列次之,其余各队列的优先权逐个降低。该算法赋予各个队列中进程执行时间片的大小也各不相同,在优先权愈高的队列中,为每个进程所规定的执行时间片就愈小。例如,第二个队列的时间片要比第一个队列的时间片长一倍,……,第i+1个队列的时间片要比第i个队列的时间片长一倍。

(2) 当一个新进程进入内存后,首先将它放入第一队列的末尾,按FCFS原则排队等待调度。当轮到该进程执行时,如它能在该时间片内完成,便可准备撤离系统;如果它在一个时间片结束时尚未完成,调度程序便将该进程转入第二队列的末尾,再同样地按FCFS原则等待调度执行;如果它在第二队列中运行一个时间片后仍未完成,再依次将它放入第三队列,……,如此下去,当一个长作业(进程)从第一队列依次降到第n队列后,在第n 队列便采取按时间片轮转的方式运行。

(3) 仅当第一队列空闲时,调度程序才调度第二队列中的进程运行;仅当第1~(i-1)队列均空时,才会调度第i队列中的进程运行。如果处理机正在第i队列中为某进程服务时,又有新进程进入优先权较高的队列(第1~(i-1)中的任何一个队列),则此时新进程将抢占正在运行进程的处理机,即由调度程序把正在运行的进程放回到第i队列的末尾,把处理机分配给新到的高优先权进程。

7)电梯调度算法

2.文件一致性验证

https://www.kancloud.cn/swordfly/linux/482799

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值