mst2

C/C++语言基础

extern 关键字作用
extern声明变量或者函数时,它告诉编译器去其他文件中寻找定义或者实现。
extern “C”的作用:为了实现C++、C的混合编程,使C++中能够调用C写的函数。它告诉C++编译器按照C的编译、链接规范来编译。因为C++编译器为了实现函数重载的功能,对函数名的编译和C编译器不一样,所以要加上extern “C”.

static关键字作用 参考链接
一种是面向过程的程序设计中的static,不涉及类。修饰变量时表示的是一个静态变量,在全局数据区分配内存,只在文件内可见,而文件之外是不可见的。修饰函数中表示静态函数,不能在其他文件中使用。
一种是面向对象程序设计中的static, 涉及到类。在类的数据成员前面加上static关键字,就是类的静态成员变量,是属于类的,在全局数据区分配内存,被所有的对象共享。在类的函数前面加上static,表示静态成员函数。不和任何对象有联系,没有this指针。

volatile关键字的作用 参考链接
volatile是一种类型修饰符,遇到这个关键字声明的变量, 编译器对访问该变量的代码不再进行优化。访问寄存器要比访问内存块,因此CPU总是优先访问寄存器。但是有时候可能内存中的数据发生了变化,而寄存器还保存原来的值。为了防止这种情况,使用volatile来声明变量时,系统总是从内存中读取数据,而不会从寄存器中读取。

const关键字的作用
const关键字所修饰的表示的是一个常量。取代C中的宏定义,声明的时候必须初始化。const修饰的变量不可以被修改。
const修饰指针时。const int * 和int * const
const 修饰引用或指针做函数的形参
const 修改引用或指针做函数的返回值
const修饰成员变量时,必须在构造函数列表中初始化
const修饰成员函数时,说明该函数不应该修改非静态成员

new与malloc的区别
new 分配内存按照数据类型进行分配,malloc分配内存按照大小分配。
给对象分配内存时,new 会调用构造函数,而malloc不会
new返回的是指向对象的指针,而malloc返回的是(void * )
new是一个操作符可以重载,而malloc是一个库函数
new分配的内存用delete销毁,malloc用free销毁。delete销毁时调用析构函数,而free不会。
new如果分配失败会抛出bad_malloc异常,而malloc失败会返回NULL。

malloc分配内存不够时,可以用realloc扩容,new不可以。

#define和const定义常量的区别
define宏是在预处理阶段展开,const常量是在编译运行时候使用。
define宏不做类型检查,仅仅是展开替换。const常量有具体的类型,编译的时候执行类型检查。
const定义的变量需要分配内存,在常量区分配。define定义的不占有内存。

指针和引用的区别
指针保存的是对象的地址,引用是对象的别名
指针需要通过解引用来间接访问,而引用是直接访问。
引用在定义的时候必须初始化,而指针则不需要。
指针在赋值后还可以改变,而引用不能改变。
有常量指针,而没有常量引用.

结构体中内存对齐?
从0位置开始存储
变量存储的起始位置是该变量大小的整数倍
结构体总的大小是其最大元素的整数倍,不足的后面要补齐
结构体中包含结构体,从结构体中最大元素的整数倍开始存
如果加入pragma pack(n) ,取n和变量自身大小较小的一个

内联函数有什么优点?内联函数与宏定义的区别?
宏定义在预编译的时候就会进行宏替换
内联函数在编译阶段,在调用内联函数的地方进行替换,减少了函数的调用过程,但是使得编译文件变大。因此,内联函数适合简单函数,对于复杂函数,即使定义了内联编译器可能也不会按照内联的方式进行编译。
内联函数相比宏定义更安全,内联函数可以检查参数,而宏定义只是简单的文本替换。因此推荐使用内联函数,而不是宏定义。
使用宏定义函数要特别注意给所有单元都加上括号,#define MUL(a, b) a * b,这很危险,正确写法:#define MUL(a, b) ((a) * (b))

纯虚函数的介绍?
纯虚函数是用在虚函数后加上 = 0来声明的。
纯虚函数只提供声明,没有实现。纯虚函数是起到声明接口的作用。
声明虚函数的类是抽象类,不能实例化为对象。继承抽象类的子类必须重写类的纯虚函数。否则该子类还是抽象类。
虽然抽象类不能实例化,但是抽象类可以有构造函数。

C++多态的实现?
多态分为静态多态和动态多态。静态多态是通过重载和模板技术实现,在编译的时候确定。动态多态通过虚函数和继承关系来实现,执行动态绑定,在运行的时候确定。

虚函数的作用
1.虚函数用于实现多态。
2.虚函数在设计上还具有封装和抽象的作用。比如抽象工厂模式。

谈谈虚函数表?
基类指针在调用所指对象的虚函数时,就会去查找该对象的虚函数表。虚函数表的地址在每个对象的首地址。查找该虚函数表中该函数的指针进行调用。
每个对象中保存的只是一个虚函数表的指针,C++内部为每一个类维持一个虚函数表,该类的对象的vptr指针都指向这同一个虚函数表。
虚函数表中为什么就能准确查找相应的函数指针呢?因为继承的时候,派生类的虚函数表直接从基类也继承过来,如果覆盖了其中的某个虚函数,那么虚函数表的指针就会被替换,因此可以根据指针准确找到该调用哪个函数。

为什么要有虚析构函数?
当基类指针指向派生类对象时,delete指针销毁对象时,如果析构函数没有定义为虚析构函数,则会调用基类的析构函数,显然只能销毁部分数据。如果要调用所指对象的析构函数,就需要将该对象的析构函数定义为虚函数,销毁时通过虚函数表找到对应的析构函数。

构造函数可以是虚函数吗?
不可以。因为虚函数的执行依赖于虚函数表,调用虚函数需要通过对象中指向虚函数表的vptr指针。而对象的vptr指针是在构造函数中初始化的。所以,如果构造函数是虚函数,构造对象时,vptr指针还没有初始化,将没办法进行。

析构函数能够抛出异常吗?
如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如资源泄漏的问题。
通常异常发生时,c++的机制会调用已经构造对象的析构函数来释放资源,此时若析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,会造成程序崩溃的问题。

必须在构造函数初始化式里进行初始化的数据成员有哪些?
const常量成员,因为常量只能初始化不能赋值,所以必须放在初始化列表里面
引用类型,引用必须在定义的时候初始化,并且不能重新赋值,所以也要写在初始化列表里面
没有默认构造函数的类类型,因为使用初始化列表可以不必调用默认构造函数来初始化,而是直接调用拷贝构造函数初始化

智能指针怎么实现的?
构造函数中计数初始化为1
拷贝构造函数中计数值加1
赋值运算符中,左边的对象引用计数减一,右边的对象引用计数加一
析构函数中引用计数减一
在赋值运算符和析构函数中,如果减一后为0,则调用delete释放对象

C++四种类型转换:static_cast, dynamic_cast, const_cast, reinterpret_cast
const_cast用于将const变量转为非const
static_cast用的最多,对于各种隐式转换,非const转const,void * 转指针等, static_cast能用于多态向上转化,如果向下转能成功但是不安全,结果未知
dynamic_cast用于动态类型转换。只能用于含有虚函数的类,用于类层次间的向上和向下转化。只能转指针或引用。向下转化时,如果是非法的对于指针返回NULL,对于引用抛异常。要深入了解内部转换的原理。
reinterpret_cast几乎什么都可以转,比如将int转指针,可能会出问题,尽量少用

为什么不使用C的强制转换?
C的强制转换表面上看起来功能强大什么都能转,但是转化不够明确,不能进行错误检查,容易出错

C++内存管理
C++内存分为那几块?(栈区,堆区,常量区,静态/全局数据区区,代码区)

堆区和栈区的区别?
申请方式上:栈区由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。堆区一般由程序员手动分配释放。
申请大小的限制:堆的空间比较灵活,也比较大
申请效率上:栈由系统自动分配,速度较快。堆是由new/malloc分配的内存,一般速度比较慢,而且容易产生内存碎片
内存空间的增长方式:栈是从高地址往低地址增长,而堆是从低地址往高地址增长。

如何定位内存泄漏?
在windows平台下通过CRT中的库函数进行检测
在可能泄漏的调用前后生成块的快照,比较前后的状态,定位泄漏的位置

Linux下通过工具valgrind检测
内存泄露定位、检测原理?
自己重载new操作符,用list或者map对分配的内存进行收集,如果释放了,就删除节点,最后检测容器里面还有没有节点,有就是泄露了。可以在重载的new中记录是哪一行的代码分配的内存被泄露了,这样就可以定位到内存泄露的位置。

如何判断大小端?

union un
{
     
  int i;
  char ch;
};

void fun()
{
     
  union un test;
  test.i = 1;
  if(test.ch == 1)
    cout << "小端" << endl
  else
    cout << "大端" << endl;
}

++i是否是原子操作?
明显不是,++i主要有三个步骤,把数据从内存放在寄存器上,在寄存器上进行自增,把数据从寄存器拷贝会内存,每个步骤都可能被中断。

没有考虑重叠的strcpy

char* strcpy(char *dst, const char *src)
{
     
  assert(dst != NULL);
  assert(src != NULL);
  char *ret = dst;
  while((*dst++ = *src++) != '\0')
  	;
  return ret;
}

考虑重叠的strcpy

char strcpy(char *dst, const char *src)
{
     
  assert((dst != NULL) && (src != NULL));
  char *ret = dst;
  int size = strlen(src) + 1;
  if(dst > src || dst < src + len)
  {
     
    dst = dst + size - 1;
    src = src + size - 1;
    while(size--)
    {
     
      *dst-- = *src--;
    }
  }
  else
  {
     
    while(size--)
    {
     
      *dst++ = *src++;
    }
 }
  return ret;
}

手写memcpy

void *memcpy(void *dst, const void *src, size_t size)

{

if(dst == NULL || src == NULL)

{

return NULL;

}

void* res = dst;

char* pdst = (char*)dst;

char* psrc = (char*)src;

if(pdst > psrc && pdst < psrc + size) //重叠

{

  pdst = pdst + size - 1;

  psrc = pdst + size - 1;

  while(size--)

  {

      *pdst-- = *psrc--;

  }

}

else //无重叠

{

  while(size--)

  {

      *dst++ = *src++;

  }

}

return ret;

}

手写strcat函数

char strcat(char *dst, const char *src)

{

char *ret = dst;

while(*dst != '\0')

++dst;

while((*dst++ = *src++) != '\0');

return ret;

}

手写strcmp

int strcmp(const char *str1, const char *str2)

{

while((*str1 == *str2) && (*str1 != ‘\0’))

{

  ++str1;

  ++str2;

}

return *str1 - *str2;

}

STL中内存池的实现
STL内存分配分为一级分配器和二级分配器,一级分配器就是采用malloc分配内存,二级分配器采用内存池。
二级分配器设计的非常巧妙,分别给8k,16k,…, 128k等比较小的内存片都维持一个空闲链表,每个链表的头节点由一个数组来维护。需要分配内存时从合适大小的链表中取一块下来。假设需要分配一块10K的内存,那么就找到最小的大于等于10k的块,也就是16K,从16K的空闲链表里取出一个用于分配。释放该块内存时,将内存节点归还给链表。
如果要分配的内存大于128K则直接调用一级分配器。
为了节省维持链表的开销,采用了一个union结构体,分配器使用union里的next指针来指向下一个节点,而用户则使用union的空指针来表示该节点的地址。

STL里set和map是基于什么实现的。红黑树的特点?
set和map都是基于红黑树实现的
红黑树的定义:
(1) 节点是红色或者黑色;
(2) 父节点是红色的话,子节点就不能为红色;
(3) 从根节点到每个页子节点路径上黑色节点的数量相同;
(4) 根是黑色的,NULL节点被认为是黑色的。

红黑树是一种平衡二叉查找树,与AVL树的区别是什么?(AVL树是完全平衡的,红黑树基本上是平衡的。)
为什么选用红黑数呢?因为红黑数是平衡二叉树,其插入和删除的效率都是N(logN),与AVL相比红黑数插入和删除最多只需要3次旋转,而AVL树为了维持其完全平衡性,在坏的情况下要旋转的次数太多。

Linux操作系统
进程与线程
进程和线程的联系区别?
进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位.线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源.
一个程序至少有一个进程,一个进程至少有一个线程,线程的划分尺度小于进程
进程在执行过程中拥有独立的内存单元,而多个线程共享所在进程的地址空间和其它资源
线程执行开销小,但不利于资源的管理和保护,而进程正相反

什么时候用多进程?什么时候用多线程?
需要频繁创建销毁的优先用线程
需要进行大量计算的优先使用线程
强相关的处理用线程,弱相关的处理用进程

什么叫强相关、弱相关?理论上很难定义,给个简单的例子就明白了。
一般的Server需要完成如下任务:消息收发、消息处理。“消息收发”和“消息处理”就是弱相关的任务,而“消息处理”里面可能又分为“消息解码”、“业务处理”,这两个任务相对来说相关性就要强多了。因此“消息收发”和“消息处理”可以分进程设计,“消息解码”、“业务处理”可以分线程设计。
当然这种划分方式不是一成不变的,也可以根据实际情况进行调整。
可能要扩展到多机分布的用进程,多核分布的用线程

LINUX中进程和线程使用的几个函数?
进程:fork, exec, wait
线程:pthread_create、pthread_exit、pthread_jion、pthread_kill; pthread_mutex_lock、pthread_mutex_unlock、pthread_cond_init、pthread_cond_signal、pthread_cond_wait

Linux线程同步
互斥锁、条件变量、读写锁、信号量

互斥锁和自旋锁的区别?
互斥锁得不到资源的时候阻塞,不占用cpu资源。自旋锁得不到资源的时候,不停的查询,而然占用cpu资源。

Linux进程间通讯
管道( pipe ):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
命名管道 (FIFO) : 有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
消息队列( message queue ) : 消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点
信号量:信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据,有XSI信号量和POSIX信号量,POSIX信号量更加完善。
共享内存( shared memory ) :共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。(原理一定要清楚,常考)
信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生,常见的信号。
套接字( socket ) : 套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。

匿名管道与命名管道的区别?
匿名管道只能在具有公共祖先的两个进程间使用

共享内存映射mmap?
mmap是内存文件映射,将一个文件映射到进程的地址空间,用户进程的地址空间的管理是通过vm_area_struct结构体进行管理的。mmap通过映射一个相同的文件到两个不同的进程,就能实现这两个进程的通信,采用该方法可以实现任意进程之间的通信。mmap也可以采用匿名映射,不指定映射的文件,但是只能在父子进程间通信。

常见的信号有哪些?
SIGINT,SIGKILL(不能被捕获),SIGTERM(可以被捕获),SIGSEGV,SIGCHLD,SIGALRM

进程调度算法?
先来先服务FIFO、最高优先级算法HPF、时间片轮转算法、多级队列反馈法

exit()与_exit()区别
exit()清理后进入内核,_exit()直接陷入内核。

什么是孤儿进程?
一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作

什么是僵尸进程?
一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。

僵尸进程的危害?
僵死进程的PID还占据着。系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程。

如何避免僵尸进程?
父进程调用wait/waitpid函数等待子进程结束然后回收。
通过信号机制。子进程退出时向父进程发送SIGCHILD信号,父进程处理SIGCHILD信号。在信号处理函数中调用wait进行处理僵尸进程。
fork两次。子进程退出,从而孙进程成为孤儿进程,他的父进程变为init进程,通过init进程可以处理僵尸进程。

Linux内存管理
页面替换算法?
最佳置换算法、先进先出页面置换算法、改进型FIFO算法、最近不常使用算法、最久未使用算法(LRU)、时钟替换算法

Linux IO模型
五种IO模型 参考链接
阻塞IO,非阻塞IO,IO复用,信号驱动式IO,异步IO

死锁
死锁产生的必要条件?
互斥条件:一个资源每次只能被一个进程使用
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系

死锁的避免?
加锁顺序:如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生
加锁时限:个可以避免死锁的方法是在尝试获取锁的时候加一个超时时间
死锁检测:当一个线程请求锁失败时,这个线程可以遍历锁的关系图看看是否有死锁发生

Linux常用的命令与shell编程
与CPU,内存,磁盘相关的命令(top,free, df, fdisk)
网络相关的命令netstat,tcpdump等
sed, awk, grep三个超强大的命名,分别用与格式化修改,统计,和正则查找
ipcs和ipcrm命令
查找当前目录以及字母下以.c结尾的文件,且文件中包含”hello world”的文件的路径

Linux命令 在一个文件中,倒序打印第二行前100个大写字母
cat filename | head -n 2 | tail -n 1 | grep ‘[[:upper:]]’ -o | tr -d ‘\n’| cut -c 1-100 | rev

MSL是什么状态?
主动关闭的Socket端会进入TIME_WAIT状态,并且持续2MSL时间长度,MSL就是maximum segment lifetime(最大分节生命期),这是一个IP数据包能在互联网上生存的最长时间,超过这个时间将在网络中消失。MSL在RFC 1122上建议是2分钟,而源自berkeley的TCP实现传统上使用30秒,因而,TIME_WAIT状态一般维持在1-4分钟。

HTTP
http/https 1.0、2.0、1.1、2.0
http的主要特点
简单快速:当客户端向服务器端发送请求时,只是简单的填写请求路径和请求方法即可,然后就可以通过浏览器或其他方式将该请求发送就行了
灵活: HTTP 协议允许客户端和服务器端传输任意类型任意格式的数据对象
无连接:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接,采用这种方式可以节省传输时间。(当今多数服务器支持Keep-Alive功能,使用服务器支持长连接,解决无连接的问题)
无状态:无状态是指协议对于事务处理没有记忆能力,服务器不知道客户端是什么状态。即客户端发送HTTP请求后,服务器根据请求,会给我们发送数据,发送完后,不会记录信息。(使用 cookie 机制可以保持 session,解决无状态的问题)

http1.1的特点
默认持久连接节省通信量,只要客户端服务端任意一端没有明确提出断开TCP连接,就一直保持连接,可以发送多次HTTP请求
管线化,客户端可以同时发出多个HTTP请求,而不用一个个等待响应
断点续传ftghh

http2.0的特点
HTTP/2采用二进制格式而非文本格式
HTTP/2是完全多路复用的,而非有序并阻塞的——只需一个HTTP连接就可以实现多个请求响应
使用报头压缩,HTTP/2降低了开销
HTTP/2让服务器可以将响应主动“推送”到客户端缓存中

get/post 区别
get重点在从服务器上获取资源,post重点在向服务器发送数据
get传输数据是通过URL请求,以field(字段)= value的形式,置于URL后,并用”?”连接,多个请求数据间用”&”连接,这个过程用户是可见的。post传输数据通过Http的post机制,将字段与对应值封存在请求实体中发送给服务器,这个过程对用户是不可见的。
Get传输的数据量小,因为受URL长度限制,但效率较高。Post可以传输大量数据,所以上传文件时只能用Post方式
get是不安全的,因为URL是可见的,可能会泄露私密信息,如密码等。post较get安全性较高

返回状态码
200:请求被正常处理
204:请求被受理但没有资源可以返回
206:客户端只是请求资源的一部分,服务器只对请求的部分资源执行GET方法,相应报文中通过Content-Range指定范围的资源。
301:永久性重定向
302:临时重定向
303:与302状态码有相似功能,只是它希望客户端在请求一个URI的时候,能通过GET方法重定向到另一个URI上
304:发送附带条件的请求时,条件不满足时返回,与重定向无关
307:临时重定向,与302类似,只是强制要求使用POST方法
400:请求报文语法有误,服务器无法识别
401:请求需要认证
403:请求的对应资源禁止被访问
404:服务器无法找到对应资源
500:服务器内部错误
503:服务器正忙

http 协议头相关
http数据由请求行,首部字段,空行,报文主体四个部分组成
首部字段分为:通用首部字段,请求首部字段,响应首部字段,实体首部字段

浏览器中输入一个URL发生什么,用到哪些协议?
浏览器中输入URL,首先浏览器要将URL解析为IP地址,解析域名就要用到DNS协议,首先主机会查询DNS的缓存,如果没有就给本地DNS发送查询请求。DNS查询分为两种方式,一种是递归查询,一种是迭代查询。如果是迭代查询,本地的DNS服务器,向根域名服务器发送查询请求,根域名服务器告知该域名的一级域名服务器,然后本地服务器给该一级域名服务器发送查询请求,然后依次类推直到查询到该域名的IP地址。DNS服务器是基于UDP的,因此会用到UDP协议。
得到IP地址后,浏览器就要与服务器建立一个http连接。因此要用到http协议,http协议报文格式上面已经提到。http生成一个get请求报文,将该报文传给TCP层处理。如果采用https还会先对http数据进行加密。TCP层如果有需要先将HTTP数据包分片,分片依据路径MTU和MSS。TCP的数据包然后会发送给IP层,用到IP协议。IP层通过路由选路,一跳一跳发送到目的地址。当然在一个网段内的寻址是通过以太网协议实现(也可以是其他物理层协议,比如PPP,SLIP),以太网协议需要直到目的IP地址的物理地址,有需要ARP协议。

Ping和TraceRoute实现原理
Ping是通过发送ICMP报文回显请求实现
TraceRoute通过发送UDP报文,设置目的端口为一个不可能的值,将IP首部中的TTL分别设置从1到N,每次逐个增加,如果收到端口不可达,说明到达目的主机,如果是因为TTL跳数超过,路由器会发送主机不可达的ICMP报文。

数据库
SQL语言(内外连接,子查询,分组,聚集,嵌套,逻辑)
量数据问题

常见的题目

  1. 十亿整数(随机生成,可重复)中前K最大的数
    类似问题的解决方法思路:首先哈希将数据分成N个文件,然后对每个文件建立K个元素最小/大堆(根据要求来选择)。最后将文件中剩余的数插入堆中,并维持K个元素的堆。最后将N个堆中的元素合起来分析。可以采用归并的方式来合并。在归并的时候为了提高效率还需要建一个N个元素构成的最大堆,先用N个堆中的最大值填充这个堆,然后就是弹出最大值,指针后移的操作了。当然这种问题在现在的互联网技术中,一般就用map-reduce框架来做了。
    大数据排序相同的思路:先哈希(哈希是好处是分布均匀,相同的数在同一个文件中),然后小文件装入内存快排,排序结果输出到文件。最后建堆归并。

1.根据熟悉的语言,谈谈两种语言的区别?
主要浅谈下C/C++和PHP语言的区别:
1)PHP弱类型语言,一种脚本语言,对数据的类型不要求过多,较多的应用于Web应用开发,现在好多互联网开发公司的主流web后台开发语言,主要框架为mvc模型,如smarty,yaf,升级的PHP7速度较快,对服务器的压力要小很多,在新浪微博已经有应用,对比很明显。
2)C/C++开发语言,C语言更偏向硬件底层开发,C++语言是目前为止我认为语法内容最多的一种语言。C/C++在执行速度上要快很多,毕竟其他类型的语言大都是C开发的,更多应用于网络编程和嵌入式编程。

2.volatile是干啥用的,(必须将cpu的寄存器缓存机制回答的很透彻),使用实例有哪些?(重点)
1)访问寄存器比访问内存单元要快,编译器会优化减少内存的读取,可能会读脏数据。声明变量为volatile,编译器不再对访问该变量的代码优化,仍然从内存读取,使访问稳定。
总结:volatile关键词影响编译器编译的结果,用volatile声明的变量表示该变量随时可能发生变化,与该变量有关的运算,不再编译优化,以免出错。

3)一个参数既可以是const还可以是volatile吗?解释为什么。
可以。一个例子是只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。

4)一个指针可以是volatile 吗?解释为什么。
可以。尽管这并不很常见。一个例子当中断服务子程序修该一个指向一个buffer的指针时。
下面的函数有什么错误:
int square(volatile int *ptr) {
return ptr * ptr;
}
下面是答案:
这段代码有点变态。这段代码的目的是用来返指针
ptr指向值的平方,但是,由于
ptr指向一个volatile型参数,编译器将产生类似下面的代码:
int square(volatile int *ptr){
int a,b;
a = *ptr;
b = ptr;
return a * b;
}
由于
ptr的值可能被意想不到地该变,因此a和b可能是不同的。结果,这段代码可能返不是你所期望的平方值!正确的代码如下:
long square(volatile int *ptr){
int a;
a = *ptr;
return a * a;
}

3.static const等等的用法,(能说出越多越好)(重点)
首先说说const的用法(绝对不能说是常数)
1)在定义的时候必须进行初始化
2)指针可以是const 指针,也可以是指向const对象的指针
3)定义为const的形参,即在函数内部是不能被修改的
4)类的成员函数可以被声明为常成员函数,不能修改类的成员变量
5)类的成员函数可以返回的是常对象,即被const声明的对象
6)类的成员变量是常成员变量不能在声明时初始化,必须在构造函数的列表里进行初始化
(注:千万不要说const是个常数,会被认为是外行人的!!!!哪怕说个只读也行)

下面的声明都是什么意思?
const int a; a是一个常整型数
int const a; a是一个常整型数
const int *a; a是一个指向常整型数的指针,整型数是不可修改的,但指针可以
int * const a; a为指向整型数的常指针,指针指向的整型数可以修改,但指针是不可修改的
int const * a const; a是一个指向常整型数的常指针,指针指向的整型数是不可修改的,同时指针也是不可修改的
通过给优化器一些附加的信息,使用关键字const也许能产生更紧凑的代码。合理地使用关键字const可以使编译器很自然地保护那些不希望被改变的参数,防止其被无意的代码修改。简而言之,这样可以减少bug的出现。

Const如何做到只读?
这些在编译期间完成,对于内置类型,如int, 编译器可能使用常数直接替换掉对此变量的引用。而对于结构体不一定。

再说说static的用法(三个明显的作用一定要答出来)
1)在函数体,一个被声明为静态的变量在这一函数被调用过程中维持其值不变。
2)在模块内(但在函数体外),一个被声明为静态的变量可以被模块内所用函数访问,但不能被模块外其它函数访问。它是一个本地的全局变量。
3)在模块内,一个被声明为静态的函数只可被这一模块内的其它函数调用。那就是,这个函数被限制在声明它的模块的本地范围内使用
4)类内的static成员变量属于整个类所拥有,不能在类内进行定义,只能在类的作用域内进行定义
5)类内的static成员函数属于整个类所拥有,不能包含this指针,只能调用static成员函数

static全局变量与普通的全局变量有什么区别?static局部变量和普通局部变量有什么区别?static函数与普通函数有什么区别?
static全局变量与普通的全局变量有什么区别:static全局变量只初使化一次,防止在其他文件单元中被引用;
static局部变量和普通局部变量有什么区别:static局部变量只被初始化一次,下一次依据上一次结果值;
static函数与普通函数有什么区别:static函数在内存中只有一份,普通函数在每个被调用中维持一份拷贝

5.指针和引用的区别
1)引用是直接访问,指针是间接访问。
2)引用是变量的别名,本身不单独分配自己的内存空间,而指针有自己的内存空间
3)引用绑定内存空间(必须赋初值),是一个变量别名不能更改绑定,可以改变对象的值。
总的来说:引用既具有指针的效率,又具有变量使用的方便性和直观性

  1. 关于静态内存分配和动态内存分配的区别及过程
  1. 静态内存分配是在编译时完成的,不占用CPU资源;动态分配内存运行时完成,分配与释放需要占用CPU资源;
    2)静态内存分配是在栈上分配的,动态内存是堆上分配的;
    3)动态内存分配需要指针或引用数据类型的支持,而静态内存分配不需要;
    4)静态内存分配是按计划分配,在编译前确定内存块的大小,动态内存分配运行时按需分配。
    5)静态分配内存是把内存的控制权交给了编译器,动态内存把内存的控制权交给了程序员;
    6)静态分配内存的运行效率要比动态分配内存的效率要高,因为动态内存分配与释放需要额外的开销;动态内存管理水平严重依赖于程序员的水平,处理不当容易造成内存泄漏。
  1. 分别设置和清除一个整数的第三位?
 #define BIT3 (0x1<<3)
static int a;

void set_bit3(void){
      
    a |= BIT3;
} 

void clear_bit3(void){
      
    a &= ~BIT3;
} 
  1. 用预处理指令#define 声明一个常数,用以表明1年中有多少秒
    #define SECONDS_PER_YEAR (60 * 60 * 24 * 365)UL

  2. 预处理器标识#error的目的是什么?
    抛出错误提示,标识外部宏是否被定义!

  3. 用变量a给出下面的定义
    一个有10个指针的数组,该指针指向一个函数,该函数有一个整型参数并返回一个整型数 int (*a[10])(int);

  4. 中断是嵌入式系统中重要的组成部分,这导致了很多编译开发商提供一种扩展—让标准C支持中断。具代表事实是,产生了一个新的关键字 __interrupt

  5. C语言同意一些令人震惊的结构,下面的结构是合法的吗,如果是它做些什么?
    int a = 5, b = 7, c;
    c = a+++b; 等同于 c = a++ + b;
    因此, 这段代码持行后a = 6, b = 7, c = 12。

  6. 用struct关键字与class关键定义类以及继承的区别
    (1)定义类差别
    struct关键字也可以实现类,用class和struct关键字定义类的唯一差别在于默认访问级别:默认情况下,struct成员的访问级别为public,而class成员的为private。语法使用也相同,直接将class改为struct即可。
    (2)继承差别
    使用class保留字的派生类默认具有private继承,而用struct保留字定义的类某人具有public继承。其它则没有任何区别。
    主要点就两个:默认的访问级别和默认的继承级别 class都是private

28.派生类与虚函数概述
(1) 派生类继承的函数不能定义为虚函数。虚函数是希望派生类重新定义。如果派生类没有重新定义某个虚函数,则在调用的时候会使用基类中定义的版本。
(2)派生类中函数的声明必须与基类中定义的方式完全匹配。
(3) 基类中声明为虚函数,则派生类也为虚函数。

  1. 虚函数与纯虚函数区别
    1)虚函数在子类里面也可以不重载的;但纯虚必须在子类去实现
    2)带纯虚函数的类叫虚基类也叫抽象类,这种基类不能直接生成对象,只能被继承,重写虚函数后才能使用,运行时动态动态绑定!

30.深拷贝与浅拷贝
浅拷贝:
char ori[]=“hello”;char *copy=ori;
深拷贝:
char ori[]=“hello”; char *copy=new char[]; copy=ori;
浅拷贝只是对指针的拷贝,拷贝后两个指针指向同一个内存空间,深拷贝不但对指针进行拷贝,而且对指针指向的内容进行拷贝,经深拷贝后的指针是指向两个不同地址的指针。

浅拷贝可能出现的问题:
1) 浅拷贝只是拷贝了指针,使得两个指针指向同一个地址,这样在对象块结束,调用函数析构的时,会造成同一份资源析构2次,即delete同一块内存2次,造成程序崩溃。
2) 浅拷贝使得两个指针都指向同一块内存,任何一方的变动都会影响到另一方。
3) 同一个空间,第二次释放失败,导致无法操作该空间,造成内存泄漏。

  1. stl各容器的实现原理(必考)
  1. Vector顺序容器,是一个动态数组,支持随机插入、删除、查找等操作,在内存中是一块连续的空间。在原有空间不够情况下自动分配空间,增加为原来的两倍。vector随机存取效率高,但是在vector插入元素,需要移动的数目多,效率低下。
    注:vector动态增加大小时是以原大小的两倍另外配置一块较大的空间,然后将原内容拷贝过来,然后才开始在原内容之后构造新元素,并释放原空间。因此,对vector空间重新配置,指向原vector的所有迭代器就都失效了。
  2. Map关联容器,以键值对的形式进行存储,方便进行查找。关键词起到索引的作用,值则表示与索引相关联的数据。红黑树的结构实现,插入删除等操作都在O(logn)时间内完成。
  3. Set是关联容器,set每个元素只包含一个关键字。set支持高效的关键字检查是否在set中。set也是以红黑树的结构实现,支持高效插入、删除等操作。

32.哪些库函数属于高危函数,为什么?
strcpy 赋值到目标区间可能会造成缓冲区溢出!

33.STL有7种主要容器:vector,list,deque,map,multimap,set,multiset

34.你如何理解MVC。简单举例来说明其应用。
MVC模式是observer 模式的一个特例,现在很多都是java的一些框架,MFC的,PHP的。

35.C++特点是什么,多态实现机制?(面试问过)多态作用?两个必要条件?
C++中多态机制主要体现在两个方面,一个是函数的重载,一个是接口的重写。接口多态指的是“一个接口多种形态”。每一个对象内部都有一个虚表指针,该虚表指针被初始化为本类的虚表。所以在程序中,不管你的对象类型如何转换,但该对象内部的虚表指针是固定的,所以呢,才能实现动态的对象函数调用,这就是C++多态性实现的原理。
多态的基础是继承,需要虚函数的支持,简单的多态是很简单的。子类继承父类大部分的资源,不能继承的有构造函数,析构函数,拷贝构造函数,operator=函数,友元函数等等
作用:
隐藏实现细节,代码能够模块化;接口重用:为了类在继承和派生的时候正确调用。
必要条件:

  1. 一个基类的指针或者引用指向派生类的对象;2.虚函数

  2. 多重继承有什么问题? 怎样消除多重继承中的二义性?
    1)增加程序的复杂度,使程序的编写和维护比较困难,容易出错;
    2)继承类和基类的同名函数产生了二义性,同名函数不知道调用基类还是继承类,C++中使用虚函数解决这个问题
    3)继承过程中可能会继承一些不必要的数据,对于多级继承,可能会产生数据很长
    可以使用成员限定符和虚函数解决多重继承中函数的二义性问题。

37.求两个数的乘积和商数,该作用由宏定义来实现
#define product(a,b) ((a)*(b))
#define divide(a,b) ((a)/(b))

38.什么叫静态关联,什么叫动态关联
多态中,静态关联是程序在编译阶段就能确定实际执行动作,程序运行才能确定叫动态关联

39.什么叫智能指针?常用的智能指针有哪些?智能指针的实现?
智能指针是一个存储指向动态分配(堆)对象指针的类,构造函数传入普通指针,析构函数释放指针。栈上分配,函数或程序结束自动释放,防止内存泄露。使用引用计数器,类与指向的对象相关联,引用计数跟踪该类有多少个对象共享同一指针。创建类的新对象时,初始化指针并将引用计数置为1;当对象作为另一对象的副本而创建,增加引用计数;对一个对象进行赋值时,减少引用计数,并增加右操作数所指对象的引用计数;调用析构函数时,构造函数减少引用计数,当引用计数减至0,则删除基础对象。

std::auto_ptr,不支持复制(拷贝构造函数)和赋值(operator =),编译不会提示出错。
C++11引入的unique_ptr, 也不支持复制和赋值,但比auto_ptr好,直接赋值会编译出错。
C++11或boost的shared_ptr,基于引用计数的智能指针。可随意赋值,直到内存的引用计数为0的时候这个内存会被释放。还有Weak_ptr

40.枚举与#define 宏的区别
1)#define 宏常量是在预编译阶段进行简单替换。枚举常量则是在编译的时候确定其值。
2)可以调试枚举常量,但是不能调试宏常量。
3)枚举可以一次定义大量相关的常量,而#define 宏一次只能定义一个。

41.介绍一下函数的重载
重载是在不同类型上作不同运算而又用同样的名字的函数。重载函数至少在参数个数,参数类型, 或参数顺序上有所不同。

42.派生新类的过程要经历三个步骤
1.吸收基类成员 2.改造基类成员 3.添加新成员

43.面向对象的三个基本特征,并简单叙述之?
1)封装:将客观事物抽象成类,每个类对自身的数据和方法实行2)继承3)多态:允许一个基类的指针或引用指向一个派生类对象

44.多态性体现都有哪些?动态绑定怎么实现?
多态性是一个接口,多种实现,是面向对象的核心。 编译时多态性:通过重载函数实现。运行时多态性:通过虚函数实现,结合动态绑定。

45.虚函数,虚函数表里面内存如何分配?
编译时若基类中有虚函数,编译器为该的类创建一个一维数组的虚表,存放是每个虚函数的地址。基类和派生类都包含虚函数时,这两个类都建立一个虚表。构造函数中进行虚表的创建和虚表指针的初始化。在构造子类对象时,要先调用父类的构造函数,初始化父类对象的虚表指针,该虚表指针指向父类的虚表。执行子类的构造函数时,子类对象的虚表指针被初始化,指向自身的虚表。每一个类都有虚表。虚表可以继承,如果子类没有重写虚函数,那么子类虚表中仍然会有该函数的地址,只不过这个地址指向的是基类的虚函数实现。派生类的虚表中虚函数地址的排列顺序和基类的虚表中虚函数地址排列顺序相同。当用一个指针/引用调用一个函数的时候,被调用的函数是取决于这个指针/引用的类型。即如果这个指针/引用是基类对象的指针/引用就调用基类的方法;如果指针/引用是派生类对象的指针/引用就调用派生类的方法,当然如果派生类中没有此方法,就会向上到基类里面去寻找相应的方法。这些调用在编译阶段就确定了。当涉及到多态性的时候,采用了虚函数和动态绑定,此时的调用就不会在编译时候确定而是在运行时确定。不在单独考虑指针/引用的类型而是看指针/引用的对象的类型来判断函数的调用,根据对象中虚指针指向的虚表中的函数的地址来确定调用哪个函数。

  1. 纯虚函数如何定义?含有纯虚函数的类称为什么?为什么析构函数要定义成虚函数?
    纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。纯虚函数是虚函数再加上= 0。virtual void fun ()=0。含有纯虚函数的类称为抽象类在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。如果析构函数不是虚函数,那么释放内存时候,编译器会使用静态联编,认为p就是一个基类指针,调用基类析构函数,这样子类对象的内存没有释放,造成内存泄漏。定义成虚函数以后,就会动态联编,先调用子类析构函数,再基类。

  2. C++中哪些不能是虚函数?
    1)普通函数只能重载,不能被重写,因此编译器会在编译时绑定函数。
    2)构造函数是知道全部信息才能创建对象,然而虚函数允许只知道部分信息。
    3)内联函数在编译时被展开,虚函数在运行时才能动态绑定函数。
    4)友元函数 因为不可以被继承。
    5)静态成员函数 只有一个实体,不能被继承。父类和子类共有。

  3. 类型转换有哪些?各适用什么环境?dynamic_cast转换失败时,会出现什么情况(对指针,返回NULL.对引用,抛出bad_cast异常)?
    静态类型转换,static_cast,基本类型之间和具有继承关系的类型。double类型转换成int。将子类对象转换成基类对象。
    常量类型转换,const_cast, 去除指针变量的常量属性。
    无法将非指针的常量转换为普通变量。
    动态类型转换,dynamic_cast,运行时进行转换分析的,并非在编译时进行。dynamic_cast转换符只能用于含有虚函数的类。dynamic_cast用于类层次间的向上转换和向下转换,还可以用于类间的交叉转换。在类层次间进行向上转换,即子类转换为父类,此时完成的功能和static_cast是相同的,因为编译器默认向上转换总是安全的。向下转换时,dynamic_cast具有类型检查的功能,更加安全。类间的交叉转换指的是子类的多个父类之间指针或引用的转换。该函数只能在继承类对象的指针之间或引用之间进行类型转换,或者有虚函数的类。

  4. 如何判断一段程序是由C 编译程序还是由C++编译程序编译的?
    #ifdef __cplusplus
    cout<<“C++”;
    #else
    cout<<“c”;
    #endif

  5. 为什么要用static_cast转换而不用c语言中的转换?
    Static_cast转换,它会检查类型看是否能转换,有类型安全检查。
    比如,这个在C++中合法,但是确实错误的。
    A* a= new A;
    B* b = (B*)a;

  6. 操作符重载(+操作符),具体如何去定义?
    除了类属关系运算符”.”、成员指针运算符”.*”、作用域运算符”::”、sizeof运算符和三目运算符”?:”以外,C++中的所有运算符都可以重载。
    <返回类型说明符> operator <运算符符号>(<参数表>){}
    重载为类的成员函数和重载为类的非成员函数。参数个数会不同,应为this指针。

  7. 内存对齐的原则?
    A.结构体的大小为最大成员的整数倍。
    B.成员首地址的偏移量为其类型大小整数倍。

  8. 内联函数与宏定义的区别?
    内联函数是用来消除函数调用时的时间开销。频繁被调用的短小函数非常受益。
    A. 宏定义不检查函数参数,返回值什么的,只是展开,相对来说,内联函数会检查参数类型,所以更安全。
    B. 宏是由预处理器对宏进行替代,而内联函数是通过编译器控制来实现的

  9. 动态分配对象和静态分配对象的区别?
    动态分配就是用运算符new来创建一个类的对象,在堆上分配内存。
    静态分配就是A a;这样来由编译器来创建一个对象,在栈上分配内存。

  10. explicit是干什么用的 ?
    构造器 ,可以阻止不应该允许的经过转换构造函数进行的隐式转换的发生。explicit是用来防止外部非正规的拷贝构造的,要想不存在传值的隐式转换问题。

  11. 内存溢出有那些因素?
    (1) 使用非类型安全(non-type-safe)的语言如 C/C++ 等。
    (2) 以不可靠的方式存取或者复制内存缓冲区。
    (3) 编译器设置的内存缓冲区太靠近关键数据结构。

  12. new与malloc的区别,delete和free的区别?
    1.malloc/free是C/C++语言的标准库函数,new/delete是C++的运算符
    2.new能够自动分配空间大小,malloc传入参数。

  13. new/delete能进行对对象进行构造和析构函数的调用进而对内存进行更加详细的工作,而malloc/free不能。
    既然new/delete的功能完全覆盖了malloc/free,为什么C++还保留malloc/free呢?因为C++程序经常要调用C函数,而C程序只能用malloc/free管理动态内存。

  14. 必须使用初始化列表初始化数据成员的情况
    1.是对象的情况;
    2.const修饰的类成员;
    3.引用成员数据;
    类成员变量的初始化不是按照初始化表顺序被初始化,是按照在类中声明的顺序被初始化的。

59.深入谈谈堆和栈
1).分配和管理方式不同 :
堆是动态分配的,其空间的分配和释放都由程序员控制。
栈由编译器自动管理。栈有两种分配方式:静态分配和动态分配。静态分配由编译器完成,比如局部变量的分配。动态分配由alloca()函数进行分配,但是栈的动态分配和堆是不同的,它的动态分配是由编译器进行释放,无须手工控制。
2).产生碎片不同
对堆来说,频繁的new/delete或者malloc/free势必会造成内存空间的不连续,造成大量的碎片,使程序效率降低。
对栈而言,则不存在碎片问题,因为栈是先进后出的队列,永远不可能有一个内存块从栈中间弹出。
3).生长方向不同
堆是向着内存地址增加的方向增长的,从内存的低地址向高地址方向增长。
栈是向着内存地址减小的方向增长,由内存的高地址向低地址方向增长。

60.内存的静态分配和动态分配的区别?
时间不同。静态分配发生在程序编译和连接时。动态分配则发生在程序调入和执行时。
空间不同。堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。alloca,可以从栈里动态分配内存,不用担心内存泄露问题,当函数返回时,通过alloca申请的内存就会被自动释放掉。

  1. 模版怎么实现?模版作用?
    实现:template void swap(T& a, T& b){}
    作用:将算法与具体对象分离,与类型无关,通用,节省精力

  2. 多重类构造和析构的顺序
    记住析构函数的调用顺序与构造函数是相反的。

  3. 迭代器删除元素的会发生什么?
    迭代器失效

  4. 静态成员函数和数据成员有什么意义?
    1)非静态数据成员,每个对象都有自己的拷贝。而静态数据成员被当作是类的成员,是该类的所有对象所共有的,在程序中只分配一次内存只有一份拷贝,所以对象都共享,值对每个对象都是一样的,它的值可以更新。
    2)静态数据成员存储在全局数据区,所以不能在类声明中定义,应该在类外定义。由于它不属于特定的类对象,在没有产生类对象时作用域就可见,即在没有产生类的实例时,我们就可以操作它。
    3)静态成员函数与静态数据成员一样,都是在类的内部实现,属于类定义的一部分。因为普通成员函数总是具体的属于具体对象的,每个有this指针。静态成员函数没有this指针,它无法访问属于类对象的非静态数据成员,也无法访问非静态成员函数。静态成员之间可以互相访问,包括静态成员函数访问静态数据成员和访问静态成员函数;
    4)非静态成员函数可以任意地访问静态成员函数和静态数据成员;
    5)没有this指针的额外开销,静态成员函数与类的全局函数相比,速度上会有少许的增长;
    6)调用静态成员函数,可以用成员访问操作符(.)和(->)为一个类的对象或指向类对象的指调用静态成员函数。

68.析构函数可以抛出异常吗?为什么不能抛出异常?除了资源泄露,还有其他需考虑的因素吗?
C++标准指明析构函数不能、也不应该抛出异常。C++异常处理模型最大的特点和优势就是对C++中的面向对象提供了最强大的无缝支持。那么如果对象在运行期间出现了异常,C++异常处理模型有责任清除那些由于出现异常所导致的已经失效了的对象(也即对象超出了它原来的作用域),并释放对象原来所分配的资源, 这就是调用这些对象的析构函数来完成释放资源的任务,所以从这个意义上说,析构函数已经变成了异常处理的一部分。
1)如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如资源泄漏的问题。
2)通常异常发生时,c++的机制会调用已经构造对象的析构函数来释放资源,此时若析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,会造成程序崩溃的问题。

  1. 拷贝构造函数作用及用途?什么时候需要自定义拷贝构造函数?
    一般如果构造函数中存在动态内存分配,则必须定义拷贝构造函数。否则,可能会导致两个对象成员指向同一地址,出现“指针悬挂问题”。

  2. 100万个32位整数,如何最快找到中位数。能保证每个数是唯一的,如何实现O(N)算法?
    1).内存足够时:快排
    2).内存不足时:分桶法:化大为小,把所有数划分到各个小区间,把每个数映射到对应的区间里,对每个区间中数的个数进行计数,数一遍各个区间,看看中位数落在哪个区间,若够小,使用基于内存的算法,否则 继续划分

  3. C++虚函数是如何实现的?
    使用虚函数表。 C++对象使用虚表, 如果是基类的实例,对应位置存放的是基类的函数指针;如果是继承类,对应位置存放的是继承类的函数指针(如果在继承类有实现)。所以 ,当使用基类指针调用对象方法时,也会根据具体的实例,调用到继承类的方法。

  4. C++的虚函数有什么作用?
    虚函数作用是实现多态,虚函数其实是实现封装,使得使用者不需要关心实现的细节。在很多设计模式中都是这样用法,例如Factory、Bridge、Strategy模式。

74.MFC中CString是类型安全类吗,为什么?
不是,其他数据类型转换到CString可以使用CString的成员函数Format来转换

74.动态链接库的两种使用方法及特点?
1).载入时动态链接,模块非常明确调用某个导出函数,使得他们就像本地函数一样。这需要链接时链接那些函数所在DLL的导入库,导入库向系统提供了载入DLL时所需的信息及DLL函数定位。
2)运行时动态链接。

二、服务器编程
1.多线程和多进程的区别
1)进程数据是分开的:共享复杂,需要用IPC,同步简单;多线程共享进程数据:共享简单,同步复杂
2)进程创建销毁、切换复杂,速度慢 ;线程创建销毁、切换简单,速度快
3)进程占用内存多, CPU利用率低;线程占用内存少, CPU利用率高
4)进程编程简单,调试简单;线程 编程复杂,调试复杂
5)进程间不会相互影响 ;线程一个线程挂掉将导致整个进程挂掉
6)进程适应于多核、多机分布;线程适用于多核
线程所私有的:
线程id、寄存器的值、栈、线程的优先级和调度策略、线程的私有数据、信号屏蔽字、errno变量、

  1. 多线程锁的种类有哪些?
    a.互斥锁(mutex)b.递归锁 c.自旋锁 d.读写锁

  2. 自旋锁和互斥锁的区别?
    当旋锁被其他线程占用时,其他线程并不是睡眠状态,而是不停的消耗CPU,获取锁;互斥锁则不然,保持睡眠,直到互斥锁被释放激活。
    自旋锁,递归调用容易造成死锁,对长时间才能获得到锁的情况,使用自旋锁容易造成CPU效率低,只有内核可抢占式或SMP情况下才真正需要自旋锁。

4.进程间通信和线程间通信
1).管道 2)消息队列 3)共享内存 4)信号量 5)套接字 6)条件变量

5.多线程程序架构,线程数量应该如何设置?
应尽量和CPU核数相等或者为CPU核数+1的个数

6.什么是原子操作,gcc提供的原子操作原语,使用这些原语如何实现读写锁?
原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch。

8.有一个计数器,多个线程都需要更新,会遇到什么问题,原因是什么,应该如何做?如何优化?
有可能一个线程更新的数据已经被另外一个线程更新了,更新的数据就会出现异常,可以加锁,保证数据更新只会被一个线程完成。

9.如果select返回可读,结果只读到0字节,什么情况?
某个套接字集合中没有准备好,可能会select内存用FD_CLR清为0.

  1. connect可能会长时间阻塞,怎么解决?
    1.使用定时器;(最常用也最有效的一种方法)
    2.采用非阻塞模式:设置非阻塞,返回之后用select检测状态。

11.keepalive 是什么东西?如何使用?
keepalive,是在TCP中一个可以检测死连接的机制。
1).如果主机可达,对方就会响应ACK应答,就认为是存活的。
2).如果可达,但应用程序退出,对方就发RST应答,发送TCP撤消连接。
3).如果可达,但应用程序崩溃,对方就发FIN消息。
4).如果对方主机不响应ack, rst,继续发送直到超时,就撤消连接。默认二个小时。

12.socket什么情况下可读?
1.socket接收缓冲区中已经接收的数据的字节数大于等于socket接收缓冲区低潮限度的当前值;对这样的socket的读操作不会阻塞,并返回一个大于0的值(准备好读入的数据的字节数).
2.连接的读一半关闭(即:接收到对方发过来的FIN的TCP连接),并且返回0;
3.socket收到了对方的connect请求已经完成的连接数为非0.这样的soocket处于可读状态;
4.异常的情况下socket的读操作将不会阻塞,并且返回一个错误(-1)。

13.udp调用connect有什么作用?
1).因为UDP可以是一对一,多对一,一对多,或者多对多的通信,所以每次调用sendto()/recvfrom()时都必须指定目标IP和端口号。通过调用connect()建立一个端到端的连接,就可以和TCP一样使用send()/recv()传递数据,而不需要每次都指定目标IP和端口号。但是它和TCP不同的是它没有三次握手的过程。
2).可以通过在已建立连接的UDP套接字上,调用connect()实现指定新的IP地址和端口号以及断开连接。

  1. socket编程,如果client断电了,服务器如何快速知道?
    使用定时器(适合有数据流动的情况);
    使用socket选项SO_KEEPALIVE(适合没有数据流动的情况);
    1)、自己编写心跳包程序,简单的说就是自己的程序加入一条线程,定时向对端发送数据包,查看是否有ACK,根据ACK的返回情况来管理连接。此方法比较通用,一般使用业务层心跳处理,灵活可控,但改变了现有的协议;
    2)、使用TCP的keepalive机制,UNIX网络编程不推荐使用SO_KEEPALIVE来做心)跳检测。
    keepalive原理:TCP内嵌有心跳包,以服务端为例,当server检测到超过一定时间(/proc/sys/net/ipv4/tcp_keepalive_time 7200 即2小时)没有数据传输,那么会向client端发送一个keepalive packet。

三、liunx操作系统

1.熟练netstat tcpdump ipcs ipcrm
netstat:检查网络状态,tcpdump:截获数据包,ipcs:检查共享内存,ipcrm:解除共享内存

2.共享内存段被映射进进程空间之后,存在于进程空间的什么位置?共享内存段最大限制是多少?
将一块内存映射到两个或者多个进程地址空间。通过指针访问该共享内存区。一般通过mmap将文件映射到进程地址共享区。
存在于进程数据段,最大限制是0x2000000Byte

5.动态链接和静态链接的区别?
动态链接是只建立一个引用的接口,而真正的代码和数据存放在另外的可执行模块中,在可执行文件运行时再装入;而静态链接是把所有的代码和数据都复制到本模块中,运行时就不再需要库了

6.32位系统一个进程最多有多少堆内存
32位意味着4G的寻址空间,Linux把它分为两部分:最高的1G(虚拟地址从0xC0000000到0xffffffff)用做内核本身,成为“系统空间”,而较低的3G字节(从0x00000000到0xbffffff)用作各进程的“用户空间”。每个进程可以使用的用户空间是3G。虽然各个进程拥有其自己的3G用户空间,系统空间却由所有的进程共享。从具体进程的角度看,则每个进程都拥有4G的虚拟空间,较低的3G为自己的用户空间,最高的1G为所有进程以及内核共享的系统空间。实际上有人做过测试也就2G左右。

7.写一个c程序辨别系统是64位 or 32位
void* number = 0; printf("%d\n",sizeof(&number));
输出8就是64位 输出4就是32位的 根据逻辑地址判断的

8.写一个c程序辨别系统是大端or小端字节序
union{ short value; char a[sizeof(short)];}test;

test.value= 0x0102;
if((test.a[0] == 1) && (test.a[1] == 2)) cout << “big”<

9.信号:列出常见的信号,信号怎么处理?
1).进程终止的信号 2).跟踪进程的信号 3).与进程例外事件相关的信号等
对于信号的处理或者执行相关的操作进行处理或者直接忽略

10.i++ 是否原子操作?并解释为什么?
答案肯定不是原子操作,i++主要看三个步骤
首先把数据从内存放到寄存器上,在寄存器上进行自增处理,放回到寄存器上,每个步骤都可能会被中断分离开!

11.说出你所知道的各类linux系统的各类同步机制(重点),什么是死锁?如何避免死锁(每个技术面试官必问)
1).原子操作 2).信号量(其实就是互斥锁也就是锁的机制)3).读写信号量(就是读写锁) 4).自旋锁 5.内核锁 6).顺序锁
死锁就是几个进程申请资源,出现了循环等待的情况!
避免死锁的方法:
1).资源是互斥的 2).不可抢占 3)占有且申请 4).循环等待

13、如何实现守护进程?
1)创建子进程,父进程退出
2)在子进程中创建新会话
3)改变当前目录为根目
4)重设文件权限掩码
5) 关闭文件描述符
6) 守护进程退出处理
当用户需要外部停止守护进程运行时,往往会使用 kill命令停止该守护进程。所以,守护进程中需要编码来实现kill发出的signal信号处理,达到进程的正常退出。

14、linux的任务调度机制是什么?
Linux 分实时进程和普通进程,实时进程应该先于普通进程而运行。实时进程:
1) FIFO(先来先服务调度)
2) RR(时间片轮转调度)。
每个进程有两个优先级(动态优先级和实时优先级),实时优先级就是用来衡量实时进程是否值得运行的。 非实时进程有两种优先级,一种是静态优先级,另一种是动态优先级。实时进程又增加了第三种优先级,实时优先级。优先级越高,得到CPU时间的机会也就越大。

15、标准库函数和系统调用的区别?
系统调用:是操作系统为用户态运行的进程和硬件设备(如CPU、磁盘、打印机等)进行交互提供的一组接口,即就是设置在应用程序和硬件设备之间的一个接口层。inux内核是单内核,结构紧凑,执行速度快,各个模块之间是直接调用的关系。linux系统上到下依次是用户进程->linux内核->硬件。其中系统调用接口是位于Linux内核中的,整个linux系统从上到下可以是:用户进程->系统调用接口->linux内核子系统->硬件,也就是说Linux内核包括了系统调用接口和内核子系统两部分;或者从下到上可以是:物理硬件->OS内核->OS服务->应用程序,操作系统起到“承上启下”作用,向下管理物理硬件,向上为操作系服务和应用程序提供接口,这里的接口就是系统调用了。
库函数:把函数放到库里。是把一些常用到的函数编完放到一个lib文件里,供别人用。别人用的时候把它所在的文件名用#include<>加到里面就可以了。一类是c语言标准规定的库函数,一类是编译器特定的库函数。
系统调用是为了方便使用操作系统的接口,而库函数则是为了人们编程的方便。

16、系统如何将一个信号通知到进程?
内核给进程发送信号,是在进程所在的进程表项的信号域设置对应的信号的位。进程处理信号的时机就是从内核态即将返回用户态度的时候。执行用户自定义的信号处理函数的方法很巧妙。把该函数的地址放在用户栈栈顶,进程从内核返回到用户态的时候,先弹出信号处理函数地址,于是就去执行信号处理函数了,然后再弹出,才是返回进入内核时的状态。

  1. fork()一子进程程后父进程的全局变量能不能使用?
    fork后子进程将会拥有父进程的几乎一切资源,父子进程的都各自有自己的全局变量。不能通用,不同于线程。对于线程,各个线程共享全局变量。

  2. 使用udp和tcp进程网络传输,为什么tcp能保证包是发送顺序,而 udp无法保证?
    因为TCP发送的数据包是按序号发送,有确认机制和丢失重传机制,而udp是不可靠的发送机制,发送的对应端口的数据包不是按顺序发送的。

  3. epoll哪些触发模式,有啥区别?
    epoll有EPOLLLT和EPOLLET两种触发模式,LT是默认的模式,ET是“高速”模式。LT模式下,只要这个fd还有数据可读,每次 epoll_wait都会返回它的事件,提醒用户程序去操作,而在ET(边缘触发)模式中,它只会提示一次,直到下次再有数据流入之前都不会再提示了,无论fd中是否还有数据可读。所以在ET模式下,read一个fd的时候一定要把它的buffer读光,也就是说一直读到read的返回值小于请求值。
    也就是说在LT模式的情况下一定要确认收发的数据包的buffer是不是足够大如果收发数据包大小大于buffer的大小的时候就可能会出现数据丢失的情况。

  4. tcp与udp的区别(必问)为什么TCP要叫做数据流?
    1).基于连接与无连接
    2).对系统资源的要求(TCP较多,UDP少)
    3).UDP程序结构较简单
    4).流模式与数据报模式
    5).TCP保证数据正确性,UDP可能丢包,TCP保证数据顺序,UDP不保证
    6).TCP有拥塞控制和流量控制,UDP没有
    TCP提供的是面向连接、可靠的字节流服务。当客户和服务器彼此交换数据前,必须先在双方之间建立一个TCP连接,之后才能传输数据。TCP提供超时重发,丢弃重复数据,检验数据,流量控制等功能,保证数据能从一端传到另一端。
    是一个简单的面向数据报的运输层协议。UDP不提供可靠性,它只是把应用程序传给IP层的数据报发送出去,但是并不能保证它们能到达目的地。由于UDP在传输数据报前不用在客户和服务器之间建立一个连接,且没有超时重发等机制,故而传输速度很快

5.流量控制和拥塞控制的实现机制
网络拥塞现象是指到达通信子网中某一部分的分组数量过多,使得该部分网络来不及处理,以致引起这部分乃至整个网络性能下降的现象,严重时甚至会导致网络通信业务陷入停顿,即出现死锁现象。拥塞控制是处理网络拥塞现象的一种机制。数据的传送与接收过程当中很可能出现收方来不及接收的情况,这时就需要对发方进行控制,以免数据丢失。

  1. 滑动窗口的实现机制
    滑动窗口机制,窗口的大小并不是固定的而是根据我们之间的链路的带宽的大小,这个时候链路是否拥护塞。接受方是否能处理这么多数据了。 滑动窗口协议,是TCP使用的一种流量控制方法。该协议允许发送方在停止并等待确认前可以连续发送多个分组。由于发送方不必每发一个分组就停下来等待确认,因此该协议可以加速数据的传输。

7.epoll和select的区别?
1)select在一个进程中打开的最大fd是有限制的,由FD_SETSIZE设置,默认值是2048。不过 epoll则没有这个限制,内存越大,fd上限越大,1G内存都能达到大约10w左右。
2)select的轮询机制是系统会去查找每个fd是否数据已准备好,当fd很多的时候,效率当然就直线下降了,epoll采用基于事件的通知方式,一旦某个fd数据就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,高效。
3)select还是epoll都需要内核把FD消息通知给用户空间,epoll是通过内核于用户空间mmap同一块内存实现的,而select则做了不必要的拷贝

  1. 网络中,如果客户端突然掉线或者重启,服务器端怎么样才能立刻知道?
    若客户端掉线或者重新启动,服务器端会收到复位信号,每一种tcp/ip得实现不一样,控制机制也不一样。

  2. TTL是什么?有什么用处,通常那些工具会用到它?ping? traceroute? ifconfig? netstat?
    TTL是Time To Live,每经过一个路由就会被减去一,如果它变成0,包会被丢掉。它的主要目的是防止包在有回路的网络上死转,浪费网络资源。ping和traceroute用到它。

10.linux的五种IO模式/异步模式.
1)同步阻塞I/O
2)同步非阻塞I/O
3)同步I/O复用模型
4)同步信号驱动I/O
5)异步I/O模型

  1. 请说出http协议的优缺点.
    1.支持客户/服务器模式。2.简单快速:客户向服务器请求服务时,只需传送请求方法和路径,通信速度很快。3.灵活:HTTP允许传输任意类型的数据对象。4.无连接:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。5.无状态:HTTP协议是无状态协议。无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,导致每次连接传送的数据量增大。缺点就是不够安全,可以使用https完成使用

12.NAT类型,UDP穿透原理。
1)Full cone NAT (全克隆nat):一对一NAT一旦一个内部地址(iAddr:port1)映射到外部地址(eAddr:port2)。
2)Address-Restricted cone NAT(地址受限克隆nat):任意外部主机(hostAddr:any)都能通过给eAddr:port2发包到达iAddr:port1的前提是:iAddr:port1之前发送过包到hostAddr:any. "any"也就是说端口不受限制
3). Port-Restricted cone NAT:内部地址(iAddr:port1)映射到外部地址(eAddr:port2),所有发自iAddr:port1的包都经eAddr:port2向外发送。一个外部主机(hostAddr:port3)能够发包到达iAddr:port1的前提是:iAddr:port1之前发送过包到hostAddr:port3.
4). Symmetric NAT(对称NAT):同内部IP与port的请求到一个特定目的地的IP地址和端口,映射到一个独特的外部来源的IP地址和端口。同一个内部主机发出一个信息包到不同的目的端,不同的映射使用外部主机收到了一封包从一个内部主机可以送一封包回来

13.大规模连接上来,并发模型怎么设计
Epoll+线程池(epoll可以采用libevent处理)

16.流量控制与拥塞控制的区别,节点计算机怎样感知网络拥塞了?
拥塞控制是把整体看成一个处理对象的,流量控制是对单个的节点。
感知的手段应该不少,比如在TCP协议里,TCP报文的重传本身就可以作为拥塞的依据。依据这样的原理, 应该可以设计出很多手段。

红黑树的定义和解释?B树的基本性质?
红黑树:
性质1. 节点是红色或黑色。
性质2. 根节点是黑色。
性质3. 每个叶子结点都带有两个空的黑色结点(被称为黑哨兵),如果一个结点n的只有一个左孩子,那么n的右孩子是一个黑哨兵;如果结点n只有一个右孩子,那么n的左孩子是一个黑哨兵。
性质4 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
性质5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。

B树:
1.所有非叶子结点至多拥有两个儿子(Left和Right);
2.所有结点存储一个关键字;
3.非叶子结点的左指针指向小于其关键字的子树,右指针指向大于其关键字的子树;

常见的加密算法?
对称式加密就是加密和解密使用同一个密钥。
非对称式加密就是加密和解密所使用的不是同一个密钥,通常有两个密钥,称为“公钥”和“私钥”,它们两个必需配对使用。
DES:对称算法,数据加密标准,速度较快,适用于加密大量数据的场合;
MD5的典型应用是对一段Message产生fingerprint(指纹),以防止被“篡改”。
RSA是第一个既能用于数据加密也能用于数字签名的算法。

  1. https?
    HTTP下加入SSL层,HTTPS的安全基础是SSL。

11.描述一种hash table的实现方法
1) 除法散列法: p ,令 h(k ) = k mod p ,这里, p 如果选取的是比较大的素数,效果比较好。而且此法非常容易实现,因此是最常用的方法。最直观的一种,上图使用的就是这种散列法,公式: index = value % 16,求模数其实是通过一个除法运算得到的。
2) 平方散列法 :求index频繁的操作,而乘法的运算要比除法来得省时。公式: index = (value * value) >> 28 (右移,除以2^28。记法:左移变大,是乘。右移变小,是除)
3) 数字选择法:如果关键字的位数比较多,超过长整型范围而无法直接运算,可以选择其中数字分布比较均匀的若干位,所组成的新的值作为关键字或者直接作为函数值。
) 斐波那契(Fibonacci)散列法:平方散列法的缺点是显而易见的,通过找到一个理想的乘数index = (value * 2654435769) >> 28
冲突处理:令数组元素个数为 S ,则当 h(k) 已经存储了元素的时候,依次探查 (h(k)+i) mod S , i=1,2,3…… ,直到找到空的存储单元为止(或者从头到尾扫描一圈仍未发现空单元,这就是哈希表已经满了,发生了错误。当然这是可以通过扩大数组范围避免的)。

13、hash,任何一个技术面试官必问(例如为什么一般hashtable的桶数会取一个素数?如何有效避免hash结果值的碰撞)
不选素数的话可能会造成hash出值的范围和原定义的不一致

14.什么是平衡二叉树?
左右子树都是平衡二叉树,而且左右子树的深度差值的约对值不大于1。

15.数组和链表的优缺点
数组,在内存上给出了连续的空间。链表,内存地址上可以是不连续的,每个链表的节点包括原来的内存和下一个节点的信息(单向的一个,双向链表的话,会有两个)。链表的:
A. 内存空间占用的少。
B. 数组内的数据可随机访问,但链表不具备随机访问性。
C. 查找速度快
链表优于数组的:
A. 插入与删除的操作方便。
B. 内存地址的利用率方面链表好。
C. 方便内存地址扩展。

  1. 4G的long型整数中找到一个最大的,如何做?
    每次从磁盘上尽量多读一些数到内存区,然后处理完之后再读入一批。减少IO次数,自然能够提高效率。分批读入选取最大数,再对缓存的最大数进行快排。

  2. 有千万个string在内存怎么高速查找,插入和删除?
    对千万个string做hash,可以实现高速查找,找到了,插入和删除就很方便了。关键是如何做hash,对string做hash,要减少碰撞频率。

20.100亿个数,求最大的1万个数,并说出算法的时间复杂度
在内存中维护一个大小为10000的最小堆,每次从文件读一个数,与最小堆的堆顶元素比较,若比堆顶元素大,则替换掉堆顶元素,然后调整堆。最后剩下的堆内元素即为最大的1万个数,算法复杂度为O(NlogN)

25.哈希表冲突解决方法?
常见的hash算法如下:

  1.  数字分析法 2.平方取中法 3.分段叠加法4.除留余数法 5.伪随机法
    

解决冲突的方法:

  1.  开放地址法
    

也叫散列法,主要思想是当出现冲突的时候,以关键字的结果值作为key值输入,再进行处理,依次直到冲突解决
线性地址再散列法
当冲突发生时,找到一个空的单元或者全表
二次探测再散列
冲突发生时,在表的左右两侧做跳跃式的探测
伪随机探测再散列
2. 再哈希法
同时构造不同的哈希函数
3. 链地址法
将同样的哈希地址构造成一个同义词的链表
4. 建立公共溢出区
建立一个基本表和溢出区,凡是和基本元素发生冲突都填入溢出区

Http与Https的区别
1、https协议需要到CA (Certificate Authority,证书颁发机构)申请证书,一般免费证书较少,因而需要一定费用。(原来网易官网是http,而网易邮箱是https。)
2、http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议。
3、http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。
4、http的连接很简单,是无状态的。Https协议是由SSL+Http协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。(无状态的意思是其数据包的发送、传输和接收都是相互独立的。无连接的意思是指通信双方都不长久的维持对方的任何信息。)
1、浏览器输入一个地址。到页面展示中间经历了哪些东西?
  #这个问题前端面试基本上百分百问的。测试的话,基础的功能面试可能不会问。自动化的话基本上也会问的。
  1、游览器输入url。先解析url地址是否合法
  2、游览器检查是否有缓存(游览器缓存-系统缓存-路由器缓存)。如果有,直接显示。如果没有,跳到第三步。
  3、在发送http请求前,需要域名解析(DNS解析),解析获取对应过的ip地址。
  4、游览器向服务器发起tcp链接,与游览器简历tcp三次握手
  5、握手成功后,游览器向服务器发送http请求,请求数据包
  6、服务器收到处理的请求,将数据返回至游览器
  7、游览器收到http响应。
  8、游览器解析响应。如果响应可以缓存,则存入缓存
  9、游览器发送请求获取嵌入在HTML中的资源(html,css,JavaScript,图片,音乐等),对于未知类型,会弹出对话框
  10、游览器发送异步请求
  11、页面全部渲染结束。

2、GET和POST的区别:
  #这个问题。我相信只要你说你做过接口测试,基本上都被问到过。
  简单来说:GET产生一个TCP数据包,POST产生两个TCP数据包
  严格的说:对于GET方式的请求,游览器会把http header和data一并发送出去,服务器响应200(返回数据);
  而对于POST请求。游览器先发送header,服务器响应100 continue,游览器再发送data,服务器响应200 ok(返回数据)
  注:千万别说什么POST比GET安全什么的。这样一下子面试官就知道你的底子了。

3、cookies机制和session机制的区别:
  1、cookies数据保存在客户端。session数据保存在服务端
  2、cookies可以减轻服务器压力,但是不安全,容易进行cookies欺骗
  3、session安全一点,但是占用服务器资源。

4、HTTP、状态码:
  200:成功
  302:重定向
  404:请求失败,请求希望得到的资源违背在服务器发现。(只要不是新手写的demo,一般404都是你路径写错了,或者未区分大小写啥的)
  502:无效的响应(基本上就是Tomcat没启好)
  400:请求没有进入到后台服务里(一般都是前端的锅)

6、http和https的区别:
  HTTPS = HTTP + SSL
  1、https有ca证书,http一般没有
  2、http是超文本传输协议,信息是明文传输。https则是具有安全性的ssl加密传输协议
  3、http默认80端口,https默认443端口。

http请求由三部分组成,分别是:请求行、消息报头、请求正文
HTTP(超文本传输协议)是一个基于请求与响应模式的、无状态的、应用层的协议,常基于TCP的连接方式,HTTP1.1版本中给出一种持续连接的机制,绝大多数的Web开发,都是构建在HTTP协议之上的Web应用。
1、常用的HTTP方法有哪些?
GET、POST、PUT、HEAD、DELETE、OPTIONS
GET: 用于请求访问已经被URI(统一资源标识符)识别的资源,可以通过URL传参给服务器。
POST:用于传输信息给服务器,主要功能与GET方法类似,但一般推荐使用POST方式。
PUT: 传输文件,报文主体中包含文件内容,保存到对应URI位置。
HEAD: 获得报文首部,与GET方法类似,只是不返回报文主体,一般用于验证URI是否有效。
DELETE:删除文件,与PUT方法相反,删除对应URI位置的文件。
OPTIONS:查询相应URI支持的HTTP方法。

2、GET方法与POST方法的区别
1、get重点在从服务器上获取资源,post重点在向服务器发送数据;
2、get传输数据是通过URL请求,以field(字段)= value的形式,置于URL后,并用"?“连接,多个请求数据间用”&"连接,如http://127.0.0.1/Test/login.action?name=admin&password=admin,这个过程用户是可见的;post传输数据通过Http的post机制,将字段与对应值封存在请求实体中发送给服务器,这个过程对用户是不可见的;
3、Get传输的数据量小,因为受URL长度限制,但效率较高;Post可以传输大量数据,所以上传文件时只能用Post方式
4、get是不安全的,因为URL是可见的,可能会泄露私密信息,如密码等;post较get安全性较高
5、get方式只能支持ASCII字符,向服务器传的中文字符可能会乱码;post支持标准字符集,可以正确传递中文字符。

3、HTTP请求报文与响应报文格式
请求报文包含三部分:
a、请求行:包含请求方法、URI、HTTP版本信息
b、请求首部字段
c、请求内容实体
响应报文包含三部分:
a、状态行:包含HTTP版本、状态码、状态码的原因短语
b、响应首部字段
c、响应内容实体

4、常见的HTTP相应状态码
返回的状态
1xx:指示信息–表示请求已接收,继续处理
2xx:成功–表示请求已被成功接收、理解、接受
3xx:重定向–要完成请求必须进行更进一步的操作
4xx:客户端错误–请求有语法错误或请求无法实现
5xx:服务器端错误–服务器未能实现合法的请求
200:请求被正常处理
204:请求被受理但没有资源可以返回
206:客户端只是请求资源的一部分,服务器只对请求的部分资源执行GET方法,相应报文中通过Content-Range指定范围的资源。
301:永久性重定向
302:临时重定向
303:与302状态码有相似功能,只是它希望客户端在请求一个URI的时候,能通过GET方法重定向到另一个URI上
304:发送附带条件的请求时,条件不满足时返回,与重定向无关
307:临时重定向,与302类似,只是强制要求使用POST方法
400:请求报文语法有误,服务器无法识别
401:请求需要认证
403:请求的对应资源禁止被访问
404:服务器无法找到对应资源
500:服务器内部错误
503:服务器正忙

5、HTTP1.1版本新特性
a、默认持久连接节省通信量,只要客户端服务端任意一端没有明确提出断开TCP连接,就一直保持连接,可以发送多次HTTP请求
b、管线化,客户端可以同时发出多个HTTP请求,而不用一个个等待响应
c、断点续传原理

6、常见HTTP首部字段
a、通用首部字段(请求报文与响应报文都会使用的首部字段)
Date:创建报文时间
Connection:连接的管理
Cache-Control:缓存的控制
Transfer-Encoding:报文主体的传输编码方式
b、请求首部字段(请求报文会使用的首部字段)
Host:请求资源所在服务器
Accept:可处理的媒体类型
Accept-Charset:可接收的字符集
Accept-Encoding:可接受的内容编码
Accept-Language:可接受的自然语言
c、响应首部字段(响应报文会使用的首部字段)
Accept-Ranges:可接受的字节范围
Location:令客户端重新定向到的URI
Server:HTTP服务器的安装信息
d、实体首部字段(请求报文与响应报文的的实体部分使用的首部字段)
Allow:资源可支持的HTTP方法
Content-Type:实体主类的类型
Content-Encoding:实体主体适用的编码方式
Content-Language:实体主体的自然语言
Content-Length:实体主体的的字节数
Content-Range:实体主体的位置范围,一般用于发出部分请求时使用

7、HTTP的缺点与HTTPS
a、通信使用明文不加密,内容可能被窃听
b、不验证通信方身份,可能遭到伪装
c、无法验证报文完整性,可能被篡改

https的SSL过程
客户端浏览器在使用HTTPS方式与Web服务器通信时有以下几个步骤,如图所示。
(1)客户使用https的URL访问Web服务器,要求与Web服务器建立SSL连接。
(2)Web服务器收到客户端请求后,会生成一对公钥和私钥,并把公钥放在证书中发给客户端浏览器。
(3)客户端浏览器根据双方同意的SSL连接的安全等级,建立会话密钥,然后用公钥将会话密钥加密,并传送给服务器。
(4)Web服务器用自己的私钥解密出会话密钥。
(5)Web服务器利用会话密钥加密与客户端之间的通信。

8、HTTP优化
利用负载均衡优化和加速HTTP应用
利用HTTP Cache来优化网站http请求由三部分组成,分别是:请求行、消息报头、请求正文

HTTP(超文本传输协议)是一个基于请求与响应模式的、无状态的、应用层的协议,常基于TCP的连接方式,HTTP1.1版本中给出一种持续连接的机制,绝大多数的Web开发,都是构建在HTTP协议之上的Web应用。

指针与地址的区别?

区别:

1指针意味着已经有一个指针变量存在,他的值是一个地址,指针变量本身也存放在一个长度为四个字节的地址当中,而地址概念本身并不代表有任何变量存在.

2 指针的值,如果没有限制,通常是可以变化的,也可以指向另外一个地址.

地址表示内存空间的一个位置点,他是用来赋给指针的,地址本身是没有大小概念,指针指向变量的大小,取决于地址后面存放的变量类型.

指针与数组名的关系?

其值都是一个地址,但前者是可以移动的,后者是不可变的.

指针和引用的区别(一般都会问到)

相同点:1. 都是地址的概念;

指针指向一块内存,它的内容是所指内存的地址;引用是某块内存的别名。

区别:1. 指针是一个实体,而引用仅是个别名;

  1. 引用使用时无需解引用(*),指针需要解引用;

  2. 引用只能在定义时被初始化一次,之后不可变;指针可变;

  3. 引用没有 const,指针有 const;

  4. 引用不能为空,指针可以为空;

  5. “sizeof 引用”得到的是所指向的变量(对象)的大小,而“sizeof 指针”得到的是指针本身(所指向的变量或对象的地址)的大小;

  6. 指针和引用的自增(++)运算意义不一样;

8.从内存分配上看:程序为指针变量分配内存区域,而引用不需要分配内存区域。

迭代器与普通指针有什么区别

智能指针的原理,

智能指针:实际指行为类似于指针的类对象 ,它的一种通用实现方法是采用引用计数的方法。

1.智能指针将一个计数器与类指向的对象相关联,引用计数跟踪共有多少个类对象共享同一指针。

2.每次创建类的新对象时,初始化指针并将引用计数置为1;

3.当对象作为另一对象的副本而创建时,拷贝构造函数拷贝指针并增加与之相应的引用计数;

4.对一个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数(如果引用计数为减至0,则删除对象),并增加右操作数所指对象的引用计数;这是因为左侧的指针指向了右侧指针所指向的对象,因此右指针所指向的对象的引用计数+1;

5.调用析构函数时,构造函数减少引用计数(如果引用计数减至0,则删除基础对象)。

6.实现智能指针有两种经典策略:一是引入辅助类,二是使用句柄类。这里主要讲一下引入辅助类的方法

其他:override和overload的区别,

override(重写)

1、方法名、参数、返回值相同。

2、子类方法不能缩小父类方法的访问权限。

3、子类方法不能抛出比父类方法更多的异常(但子类方法可以不抛出异常)。

4、存在于父类和子类之间。

5、方法被定义为final不能被重写。

overload(重载)

1、参数类型、个数、顺序至少有一个不相同。

2、不能重载只有返回值不同的方法名。

3、存在于父类和子类、同类中。

Overload是重载的意思,Override是覆盖的意思,也就是重写。

重载Overload表示同一个类中可以有多个名称相同的方法,但这些方法的参数列表各不相同(即参数个数或类型不同)。

重写Override表示子类中的方法可以与父类中的某个方法的名称和参数完全相同,通过子类创建的实例对象调用这个方法时,将调用子类中的定义方法,这相当于把父类中定义的那个完全相同的方法给覆盖了,这也是面向对象编程的多态性的一种表现。

子类覆盖父类的方法时,只能比父类抛出更少的异常,或者是抛出父类抛出的异常的子异常,因为子类可以解决父类的一些问题,不能比父类有更多的问题。子类方法的访问权限只能比父类的更大,不能更小。如果父类的方法是private类型,那么,子类则不存在覆盖的限制,相当于子类中增加了一个全新的方法。

1 指针的四要素

1指针变量,表示一个内存地址,通常为逻辑地址,与实际的物理地址还有一个映射关系.

2指针变量的长度,在WIN32下为四个字节,

3指针指向的变量

该内存地址空间下存放的变量,具体内容可能是各种类型的变量.

4 指针指向的变量的长度,以该内存地址空间开始的内存空间大小.

##数据结构算法:

链表、树、哈希表、有效避免hash结果值的碰撞

排序算法性能比较

##操作系统:

linux的内存管理机制,内存寻址方式,什么叫虚拟内存,内存调页算法,任务调度算法、

Linux虚拟内存的实现需要6种机制的支持:地址映射机制、内存分配回收机制、缓存和刷新机制、请求页机制、交换机制和内存共享机制

内存管理程序通过映射机制把用户程序的逻辑地址映射到物理地址。当用户程序运行时,如果发现程序中要用的虚地址没有对应的物理内存,就发出了请求页要求。如果有空闲的内存可供分配,就请求分配内存(于是用到了内存的分配和回收),并把正在使用的物理页记录在缓存中(使用了缓存机制)。如果没有足够的内存可供分配,那么就调用交换机制;腾出一部分内存。另外,在地址映射中要通过TLB(翻译后援存储器)来寻找物理页;交换机制中也要用到交换缓存,并且把物理页内容交换到交换文件中,也要修改页表来映射文件地址。

进程和线程、进程间及线程通信方式、共享内存的使用实现原理

死锁必要条件及避免算法、

1、资源不能共享,只能由一个进程使用。

2、请求与保持(Hold andwait):已经得到资源的进程可以再次申请新的资源。

3、不可剥夺(Nopre-emption):已经分配的资源不能从相应的进程中被强制地剥夺。

4、循环等待:系统中若干进程组成环路,该环路中每个进程都在等待相邻进程正占用的资源

处理死锁的策略:1.忽略该问题。例如鸵鸟算法,该算法可以应用在极少发生死锁的的情况下。为什么叫鸵鸟算法呢,因为传说中鸵鸟看到危险就把头埋在地底下,可能鸵鸟觉得看不到危险也就没危险了吧。跟掩耳盗铃有点像。2.检测死锁并且恢复。3.仔细地对资源进行动态分配,以避免死锁。4.通过破除死锁四个必要条件之一,来防止死锁产生。)

动态链接和静态链接的区别、

动态链接是只建立一个引用的接口,而真正的代码和数据存放在另外的可执行模块中,在运行时再装入;而静态链接是把所有的代码和数据都复制到本模块中,运行时就不再需要库了。

c程序辨别系统是16位or32位,大端or小端字节序、

16or32

法一:int k=~0;

if((unsigned int)k >63356) cout<<“at least 32bits”<

else cout<<“16 bits”<

法二://32为系统

int i=65536;

cout<

int j=65535;

cout<

大or小

  1. Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。

  2. Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。

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

1)大端模式:

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

0x12 | 0x34 | 0x56 | 0x78

2)小端模式:

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

0x78 | 0x56 | 0x34 | 0x12

32bit宽的数0x12345678在Little-endian模式以及Big-endian模式)CPU内存中的存放方式(假设从地址0x4000开始存放)为:

内存地址小端模式存放内容大端模式存放内容

0x40000x780x12

0x40010x560x34

0x40020x340x56

0x40030x120x78

4)大端小端没有谁优谁劣,各自优势便是对方劣势:

小端模式 :强制转换数据不需要调整字节内容,1、2、4字节的存储方式一样。

大端模式 :符号位的判定固定为第一个字节,容易判断正负。

BOOL IsBigEndian()

{

int a = 0x1234;  

char b =  *(char *)&a;  //通过将int强制类型转换成char单字节,通过判断起始存储位置。即等于 取b等于a的低地址部分  

if( b == 0x12)  

{  

    return TRUE;  

}  

return FALSE;  

}

联合体union的存放顺序是所有成员都从低地址开始存放,利用该特性可以轻松地获得了CPU对内存采用Little-endian还是Big-endian模式读写:

BOOL IsBigEndian()

{

union NUM  

{  

    int a;  

    char b;  

}num;  

num.a = 0x1234;  

if( num.b == 0x12 )  

{  

    return TRUE;  

}  

return FALSE;  

}

一般操作系统都是小端,而通讯协议是大端的。

常见CPU的字节序

Big Endian : PowerPC、IBM、Sun

Little Endian : x86、DEC

ARM既可以工作在大端模式,也可以工作在小端模式。

常见的信号、系统如何将一个信号通知到进程、

信号机制是进程之间相互传递消息的一种方法,信号全称为软中断信号,也有人称作软中断。

进程之间可以互相通过系统调用kill发送软中断信号。

SIGHUP 1 A 终端挂起或者控制进程终止

SIGINT 2 A 键盘中断(如break键被按下)

SIGQUIT 3 C 键盘的退出键被按下

SIGILL 4 C 非法指令

SIGABRT 6 C 由abort(3)发出的退出指令

SIGFPE 8 C 浮点异常

SIGKILL 9 AEF Kill信号

SIGSEGV 11 C 无效的内存引用

SIGPIPE 13 A 管道破裂: 写一个没有读端口的管道

信号机制是异步的;当一个进程接收到一个信号时,它会立刻处理这个信号,而不会等待当前函数甚至当前一行代码结束运行。信号有几十种,分别代表着不同的意义。信号之间依靠它们的值来区分,但是通常在程序中使用信号的名字来表示一个信号。在Linux系统中,这些信号和以它们的名称命名的常量均定义在/usr/include/bits/signum.h文件中。(通常程序中不需要直接包含这个头文件,而应该包含。)

信号事件的发生有两个来源:硬件来源(比如我们按下了键盘或者其它硬件故障);软件来源,最常用发送信号的系统函数是kill, raise, alarm和setitimer以及sigqueue函数,软件来源还包括一些非法运算等操作。

发送信号的主要函数有:kill()、raise()、 sigqueue()、alarm()、setitimer()以及abort()。

进程可以通过三种方式来响应一个信号:(1)忽略信号,即对信号不做任何处理,其中,有两个信号不能忽略:SIGKILL及SIGSTOP;(2)捕捉信号。定义信号处理函数,当信号发生时,执行相应的处理函数;(3)执行缺省操作,

linux系统的各类同步机制、linux系统的各类异步机制、

如何实现守护进程

守护进程最重要的特性是后台运行。

  1. 在后台运行。

为避免挂起控制终端将Daemon放入后台执行。方法是在进程中调用fork使父进程终止,让Daemon在子进程中后台执行。

if(pid=fork())

exit(0); //是父进程,结束父进程,子进程继续

  1. 脱离控制终端,登录会话和进程组

有必要先介绍一下Linux中的进程与控制终端,登录会话和进程组之间的关系:进程属于一个进程组,进程组号(GID)就是进程组长的进程号(PID)。登录会话可以包含多个进程组。这些进程组共享一个控制终端。这个控制终端通常是创建进程的登录终端。控制终端,登录会话和进程组通常是从父进程继承下来的。我们的目的就是要摆脱它们,使之不受它们的影响。方法是在第1点的基础上,调用setsid()使进程成为会话组长:

setsid();

说明:当进程是会话组长时setsid()调用失败。但第一点已经保证进程不是会话组长。setsid()调用成功后,进程成为新的会话组长和新的进程组长,并与原来的登录会话和进程组脱离。由于会话过程对控制终端的独占性,进程同时与控制终端脱离。

  1. 禁止进程重新打开控制终端

现在,进程已经成为无终端的会话组长。但它可以重新申请打开一个控制终端。可以通过使进程不再成为会话组长来禁止进程重新打开控制终端:

if(pid=fork()) exit(0); //结束第一子进程,第二子进程继续(第二子进程不再是会话组长)

  1. 关闭打开的文件描述符

进程从创建它的父进程那里继承了打开的文件描述符。如不关闭,将会浪费系统资源,造成进程所在的文件系统无法卸下以及引起无法预料的错误。按如下方法关闭它们:

for(i=0;i 关闭打开的文件描述符close(i);>

  1. 改变当前工作目录

进程活动时,其工作目录所在的文件系统不能卸下。一般需要将工作目录改变到根目录。对于需要转储核心,写运行日志的进程将工作目录改变到特定目录如 /tmpchdir("/")

  1. 重设文件创建掩模

进程从创建它的父进程那里继承了文件创建掩模。它可能修改守护进程所创建的文件的存取位。为防止这一点,将文件创建掩模清除:umask(0);

  1. 处理SIGCHLD信号

处理SIGCHLD信号并不是必须的。但对于某些进程,特别是服务器进程往往在请求到来时生成子进程处理请求。如果父进程不等待子进程结束,子进程将成为僵尸进程(zombie)从而占用系统资源。如果父进程等待子进程结束,将增加父进程的负担,影响服务器进程的并发性能。在Linux下可以简单地将 SIGCHLD信号的操作设为SIG_IGN。

signal(SIGCHLD,SIG_IGN);

这样,内核在子进程结束时不会产生僵尸进程。这一点与BSD4不同,BSD4下必须显式等待子进程结束才能释放僵尸进程。

标准库函数和系统调用的区别,

1、系统调用

系统调用提供的函数如open, close, read, write, ioctl等,需包含头文件unistd.h。以write为例:其函数原型为 size_t write(int fd, const void *buf, size_t nbytes),其操作对象为文件描述符或文件句柄fd(file descriptor),要想写一个文件,必须先以可写权限用open系统调用打开一个文件,获得所打开文件的fd,例如fd=open(/"/dev/video/", O_RDWR)。fd是一个整型值,每新打开一个文件,所获得的fd为当前最大fd加1。Linux系统默认分配了3个文件描述符值:0-standard input,1-standard output,2-standard error。

系统调用通常用于底层文件访问(low-level file access),例如在驱动程序中对设备文件的直接访问。

系统调用是操作系统相关的,因此一般没有跨操作系统的可移植性。

系统调用发生在内核空间,因此如果在用户空间的一般应用程序中使用系统调用来进行文件操作,会有用户空间到内核空间切换的开销。事实上,即使在用户空间使用库函数来对文件进行操作,因为文件总是存在于存储介质上,因此不管是读写操作,都是对硬件(存储器)的操作,都必然会引起系统调用。也就是说,库函数对文件的操作实际上是通过系统调用来实现的。例如C库函数fwrite()就是通过write()系统调用来实现的。

这样的话,使用库函数也有系统调用的开销,为什么不直接使用系统调用呢?这是因为,读写文件通常是大量的数据(这种大量是相对于底层驱动的系统调用所实现的数据操作单位而言),这时,使用库函数就可以大大减少系统调用的次数。这一结果又缘于缓冲区技术。在用户空间和内核空间,对文件操作都使用了缓冲区,例如用fwrite写文件,都是先将内容写到用户空间缓冲区,当用户空间缓冲区满或者写操作结束时,才将用户缓冲区的内容写到内核缓冲区,同样的道理,当内核缓冲区满或写结束时才将内核缓冲区内容写到文件对应的硬件媒介。

2、库函数调用

标准C库函数提供的文件操作函数如fopen, fread, fwrite, fclose,fflush, fseek等,需包含头文件stdio.h。以fwrite为例,其函数原型为size_t fwrite(const void *buffer,size_t size, size_t item_num, FILE *pf),其操作对象为文件指针FILE *pf,要想写一个文件,必须先以可写权限用fopen函数打开一个文件,获得所打开文件的FILE结构指针pf,例如pf=fopen(/"~/proj/filename/",/“w/”)。实际上,由于库函数对文件的操作最终是通过系统调用实现的,因此,每打开一个文件所获得的FILE结构指针都有一个内核空间的文件描述符fd与之对应。同样有相应的预定义的FILE指针:stdin-standard input,stdout-standard output,stderr-standard error。

库函数调用通常用于应用程序中对一般文件的访问。

库函数调用是系统无关的,因此可移植性好。

由于库函数调用是基于C库的,因此也就不可能用于内核空间的驱动程序中对设备的操作

fd和PCB,

32位系统一个进程最多有多少堆内存,

五种I/O 模式,

五种I/O 模式:

【1】 阻塞I/O (Linux下的I/O操作默认是阻塞I/O,即open和socket创建的I/O都是阻塞I/O)

【2】 非阻塞 I/O (可以通过fcntl或者open时使用O_NONBLOCK参数,将fd设置为非阻塞的I/O)

【3】 I/O 多路复用 (I/O多路复用,通常需要非阻塞I/O配合使用)

【4】 信号驱动 I/O (SIGIO)

【5】 异步 I/O

Apache 模型(Process Per Connection,简称PPC),TPC(ThreadPer Connection)模型,以及 select 模型和 poll 模型,epoll模型

一般来说,程序进行输入操作有两步:

1.等待有数据可以读

2.将数据从系统内核中拷贝到程序的数据区。

对于sock编程来说:

     第一步:   一般来说是等待数据从网络上传到本地。当数据包到达的时候,数据将会从网络层拷贝到内核的缓存中;



     第二步:   是从内核中把数据拷贝到程序的数据区中。

阻塞I/O模式 //进程处于阻塞模式时,让出CPU,进入休眠状态

    阻塞 I/O 模式是最普遍使用的 I/O 模式。是Linux系统下缺省的IO模式。



   大部分程序使用的都是阻塞模式的 I/O 。



   一个套接字建立后所处于的模式就是阻塞 I/O 模式。(因为Linux系统默认的IO模式是阻塞模式)

对于一个UDP 套接字来说,数据就绪的标志比较简单:

(1)已经收到了一整个数据报

(2)没有收到。

而 TCP 这个概念就比较复杂,需要附加一些其他的变量。

   一个进程调用 recvfrom  ,然后系统调用并不返回知道有数据报到达本地系统,然后系统将数据拷贝到进程的缓存中。(如果系统调用收到一个中断信号,则它的调用会被中断)

我们称这个进程在调用recvfrom一直到从recvfrom返回这段时间是阻塞的。当recvfrom正常返回时,我们的进程继续它的操作

非阻塞模式I/O //非阻塞模式的使用并不普遍,因为非阻塞模式会浪费大量的CPU资源。

   当我们将一个套接字设置为非阻塞模式,我们相当于告诉了系统内核: “当我请求的I/O 操作不能够马上完成,你想让我的进程进行休眠等待的时候,不要这么做,请马上返回一个错误给我。”

  我们开始对 recvfrom 的三次调用,因为系统还没有接收到网络数据,所以内核马上返回一个EWOULDBLOCK的错误。



  第四次我们调用 recvfrom 函数,一个数据报已经到达了,内核将它拷贝到我们的应用程序的缓冲区中,然后 recvfrom 正常返回,我们就可以对接收到的数据进行处理了。

  当一个应用程序使用了非阻塞模式的套接字,它需要使用一个循环来不听的测试是否一个文件描述符有数据可读(称做 polling(轮询))。应用程序不停的 polling 内核来检查是否 I/O操作已经就绪。这将是一个极浪费 CPU资源的操作。这种模式使用中不是很普遍。

例如:

      对管道的操作,最好使用非阻塞方式!

I/O多路复用 //针对批量IP操作时,使用I/O多路复用,非常有好。

   在使用 I/O 多路技术的时候,我们调用select()函数和 poll()函数或epoll函数(2.6内核开始支持),在调用它们的时候阻塞,而不是我们来调用 recvfrom(或recv)的时候阻塞。

   当我们调用 select函数阻塞的时候,select 函数等待数据报套接字进入读就绪状态。当select函数返回的时候,也就是套接字可以读取数据的时候。这时候我们就可以调用 recvfrom函数来将数据拷贝到我们的程序缓冲区中。

    对于单个I/O操作,和阻塞模式相比较,select()和poll()或epoll并没有什么高级的地方。



   而且,在阻塞模式下只需要调用一个函数:



                        读取或发送函数。



              在使用了多路复用技术后,我们需要调用两个函数了:



                         先调用 select()函数或poll()函数,然后才能进行真正的读写。



   多路复用的高级之处在于::



         它能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就可以返回。

IO 多路技术一般在下面这些情况中被使用:

1、当一个客户端需要同时处理多个文件描述符的输入输出操作的时候(一般来说是标准的输入输出和网络套接字),I/O 多路复用技术将会有机会得到使用。

2、当程序需要同时进行多个套接字的操作的时候。

3、如果一个 TCP 服务器程序同时处理正在侦听网络连接的套接字和已经连接好的套接字。

4、如果一个服务器程序同时使用 TCP 和 UDP 协议。

5、如果一个服务器同时使用多种服务并且每种服务可能使用不同的协议(比如 inetd就是这样的)。

异步IO模式有::

  1、信号驱动I/O模式



   2、异步I/O模式

信号驱动I/O模式 //自己没有用过。

   我们可以使用信号,让内核在文件描述符就绪的时候使用 SIGIO 信号来通知我们。我们将这种模式称为信号驱动 I/O 模式。

为了在一个套接字上使用信号驱动 I/O 操作,下面这三步是所必须的。

(1)一个和 SIGIO信号的处理函数必须设定。

(2)套接字的拥有者必须被设定。一般来说是使用 fcntl 函数的 F_SETOWN 参数来

进行设定拥有者。

(3)套接字必须被允许使用异步 I/O。一般是通过调用 fcntl 函数的 F_SETFL 命令,O_ASYNC为参数来实现。

   虽然设定套接字为异步 I/O 非常简单,但是使用起来困难的部分是怎样在程序中断定产生 SIGIO信号发送给套接字属主的时候,程序处在什么状态。

1.UDP 套接字的 SIGIO 信号 (比较简单)

在 UDP 协议上使用异步 I/O 非常简单.这个信号将会在这个时候产生:

1、套接字收到了一个数据报的数据包。

2、套接字发生了异步错误。

    当我们在使用 UDP 套接字异步 I/O 的时候,我们使用 recvfrom()函数来读取数据报数据或是异步 I/O 错误信息。

2.TCP 套接字的 SIGIO 信号 (不会使用)

      不幸的是,异步 I/O 几乎对 TCP 套接字而言没有什么作用。因为对于一个 TCP 套接字来说,SIGIO 信号发生的几率太高了,所以 SIGIO 信号并不能告诉我们究竟发生了什么事情。

在 TCP 连接中, SIGIO 信号将会在这个时候产生:

l 在一个监听某个端口的套接字上成功的建立了一个新连接。

l 一个断线的请求被成功的初始化。

l 一个断线的请求成功的结束。

l 套接字的某一个通道(发送通道或是接收通道)被关闭。

l 套接字接收到新数据。

l 套接字将数据发送出去。

l 发生了一个异步 I/O 的错误。

一个对信号驱动 I/O 比较实用的方面是NTP(网络时间协议 Network TimeProtocol)服务器,它使用 UDP。这个服务器的主循环用来接收从客户端发送过来的数据报数据包,然后再发送请求。对于这个服务器来说,记录下收到每一个数据包的具体时间是很重要的。

因为那将是返回给客户端的值,客户端要使用这个数据来计算数据报在网络上来回所花费的时间。图 6-8 表示了怎样建立这样的一个 UDP 服务器。

异步I/O模式 //比如写操作,只需用写,不一定写入磁盘(这就是异步I/O)的好处。异步IO的好处效率高。

  当我们运行在异步 I/O 模式下时,我们如果想进行 I/O 操作,只需要告诉内核我们要进行 I/O 操作,然后内核会马上返回。具体的 I/O 和数据的拷贝全部由内核来完成,我们的程序可以继续向下执行。当内核完成所有的 I/O 操作和数据拷贝后,内核将通知我们的程序。

异步 I/O 和 信号驱动I/O的区别是:

    1、信号驱动 I/O 模式下,内核在操作可以被操作的时候通知给我们的应用程序发送SIGIO 消息。



    2、异步 I/O 模式下,内核在所有的操作都已经被内核操作结束之后才会通知我们的应用程序。

select,poll,epoll

. Epoll 是何方神圣?

Epoll 可是当前在 Linux 下开发大规模并发网络程序的热门人选, Epoll 在 Linux2.6 内核中正式引入,和 select 相似,其实都 I/O 多路复用技术而已,并没有什么神秘的。

其实在Linux 下设计并发网络程序,向来不缺少方法,比如典型的 Apache 模型( Process Per Connection ,简称PPC ), TPC ( ThreadPer Connection )模型,以及 select 模型和 poll 模型,那为何还要再引入 Epoll 这个东东呢?那还是有得说说的 …

  1. 常用模型的缺点

如果不摆出来其他模型的缺点,怎么能对比出 Epoll 的优点呢。

2.1 PPC/TPC 模型

这两种模型思想类似,就是让每一个到来的连接一边自己做事去,别再来烦我。只是 PPC 是为它开了一个进程,而 TPC 开了一个线程。可是别烦我是有代价的,它要时间和空间啊,连接多了之后,那么多的进程 / 线程切换,这开销就上来了;因此这类模型能接受的最大连接数都不会高,一般在几百个左右。

2.2 select 模型

  1. 最大并发数限制,因为一个进程所打开的 FD (文件描述符)是有限制的,www.linuxidc.com 由FD_SETSIZE 设置,默认值是 1024/2048 ,因此 Select 模型的最大并发数就被相应限制了。自己改改这个 FD_SETSIZE ?想法虽好,可是先看看下面吧 …

  2. 效率问题, select 每次调用都会线性扫描全部的 FD 集合,这样效率就会呈现线性下降,把 FD_SETSIZE 改大的后果就是,大家都慢慢来,什么?都超时了??!!

  3. 内核 / 用户空间内存拷贝问题,如何让内核把 FD 消息通知给用户空间呢?在这个问题上 select 采取了内存拷贝方法。

2.3 poll 模型

基本上效率和select 是相同的,select 缺点的 2 和 3 它都没有改掉。

  1. Epoll 的提升

把其他模型逐个批判了一下,再来看看 Epoll 的改进之处吧,其实把 select 的缺点反过来那就是 Epoll 的优点了。

3.1. Epoll 没有最大并发连接的限制,上限是最大可以打开文件的数目,这个数字一般远大于 2048, 一般来说这个数目和系统内存关系很大,具体数目可以 cat /proc/sys/fs/file-max 察看。

3.2. 效率提升, Epoll 最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中, Epoll 的效率就会远远高于 select 和 poll 。

3.3. 内存拷贝, Epoll 在这点上使用了“共享内存 ”,这个内存拷贝也省略了。

  1. Epoll 为什么高效

Epoll 的高效和其数据结构的设计是密不可分的,这个下面就会提到。

首先回忆一下select 模型,当有I/O 事件到来时,select 通知应用程序有事件到了快去处理,而应用程序必须轮询所有的 FD 集合,测试每个 FD 是否有事件发生,并处理事件;代码像下面这样:

int res = select(maxfd+1, &readfds,NULL, NULL, 120);

if (res > 0)

{

for (int i = 0; i 

}

// if(res == 0) handle timeout, res < 0handle error

Epoll 不仅会告诉应用程序有I/0事件到来,还会告诉应用程序相关的信息,这些信息是应用程序填充的,因此根据这些信息应用程序就能直接定位到事件,而不必遍历整个FD 集合。

int res = epoll_wait(epfd, events, 20,120);

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

{

handleEvent(events[n]);

}

  1. Epoll 关键数据结构

前面提到Epoll 速度快和其数据结构密不可分,其关键数据结构就是:

struct epoll_event {

__uint32_tevents;      // Epoll events



epoll_data_tdata;      // User data variable

};

typedef union epoll_data {

void *ptr;



int fd;



__uint32_t u32;



__uint64_t u64;

} epoll_data_t;

可见epoll_data 是一个 union 结构体 , 借助于它应用程序可以保存很多类型的信息 :fd 、指针等等。有了它,应用程序就可以直接定位目标了。

socket服务端的实现,select和epoll的区别(必问)

select的本质是采用32个整数的32位,即3232= 1024来标识,fd值为1-1024。当fd的值超过1024限制时,就必须修改FD_SETSIZE的大小。这个时候就可以标识32max值范围的fd。

对于单进程多线程,每个线程处理多个fd的情况,select是不适合的。

1.所有的线程均是从1-32*max进行扫描,每个线程处理的均是一段fd值,这样做有点浪费

2.1024上限问题,一个处理多个用户的进程,fd值远远大于1024

所以这个时候应该采用poll,

poll传递的是数组头指针和该数组的长度,只要数组的长度不是很长,性能还是很不错的,因为poll一次在内核中申请4K(一个页的大小来存放fd),尽量控制在4K以内

epoll还是poll的一种优化,返回后不需要对所有的fd进行遍历,在内核中维持了fd的列表。select和poll是将这个内核列表维持在用户态,然后传递到内核中。但是只有在2.6的内核才支持。

epoll更适合于处理大量的fd ,且活跃fd不是很多的情况,毕竟fd较多还是一个串行的操作

epoll哪些触发模式,有啥区别?(必须非常详尽的解释水平触发和边缘触发的区别,以及边缘触发在编程中要做哪些更多的确认)

epoll可以同时支持水平触发和边缘触发(Edge Triggered,只告诉进程哪些文件描述符刚刚变为就绪状态,它只说一遍,如果我们没有采取行动,那么它将不会再次告知,这种方式称为边缘触发),理论上边缘触发的性能要更高一些,但是代码实现相当复杂。

epoll同样只告知那些就绪的文件描述符,而且当我们调用epoll_wait()获得就绪文件描述符时,返回的不是实际的描述符,而是一个代表就绪描述符数量的值,你只需要去epoll指定的一个数组中依次取得相应数量的文件描述符即可,这里也使用了内存映射(mmap)技术,这样便彻底省掉了这些文件描述符在系统调用时复制的开销。

另一个本质的改进在于epoll采用基于事件的就绪通知方式。在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。

惊群现象,

举一个很简单的例子,当你往一群鸽子中间扔一块食物,虽然最终只有一个鸽子抢到食物,但所有鸽子都会被惊动来争夺,没有抢到食物的鸽子只好回去继续睡觉,等待下一块食物到来。这样,每扔一块食物,都会惊动所有的鸽子,即为惊群。对于操作系统来说,多个进程/线程在等待同一资源是,也会产生类似的效果,其结果就是每当资源可用,所有的进程/线程都来竞争资源,造成的后果:

1)系统对用户进程/线程频繁的做无效的调度、上下文切换,系统系能大打折扣。

2)为了确保只有一个线程得到资源,用户必须对资源操作进行加锁保护,进一步加大了系统开销。

什么是惊群

最常见的例子就是对于socket描述符的accept操作,当多个用户进程/线程监听在同一个端口上时,由于实际只可能accept一次,因此就会产生惊群现象,当然前面已经说过了,这个问题是一个古老的问题,新的操作系统内核已经解决了这一问题。



linux内核解决惊群问题的方法



对于一些已知的惊群问题,内核开发者增加了一个“互斥等待”选项。一个互斥等待的行为与睡眠基本类似,主要的不同点在于:

1)当一个等待队列入口有 WQ_FLAG_EXCLUSEVE 标志置位, 它被添加到等待队列的尾部. 没有这个标志的入口项, 相反, 添加到开始.

2)当 wake_up 被在一个等待队列上调用时, 它在唤醒第一个有 WQ_FLAG_EXCLUSIVE 标志的进程后停止。

也就是说,对于互斥等待的行为,比如如对一个listen后的socket描述符,多线程阻塞accept时,系统内核只会唤醒所有正在等待此时间的队列的第一个,队列中的其他人则继续等待下一次事件的发生,这样就避免的多个线程同时监听同一个socket描述符时的惊群问题。

块设备和字符设备有什么区别,

(1) 字符设备:提供连续的数据流,应用程序可以顺序读取,通常不支持随机存取。相反,此类设备支持按字节/字符来读写数据。举例来说,调制解调器是典型的字符设备。

(2) 块设备:应用程序可以随机访问设备数据,程序可自行确定读取数据的位置。硬盘是典型的块设备,应用程序可以寻址磁盘上的任何位置,并由此读取数据。此外,数据的读写只能以块(通常是512B)的倍数进行。与字符设备不同,块设备并不支持基于字符的寻址。

两种设备本身并没用严格的区分,主要是字符设备和块设备驱动程序提供的访问接口(file I/O API)是不一样的。本文主要就数据接口、访问接口和设备注册方法对两种设备进行比较。

用户态和内核态的区别

虽然用户态下和内核态下工作的程序有很多差别,但最重要的差别就在于特权级的不同,即权力的不同。运行在用户态下的程序不能直接访问操作系统内核数据结构和程序,

当我们在系统中执行一个程序时,大部分时间是运行在用户态下的,在其需要操作系统帮助完成某些它没有权力和能力完成的工作时就会切换到内核态,

linux文件系统:inode,inode存储了哪些东西,目录名,文件名存在哪里

inode包含文件的元信息,具体来说有以下内容:

* 文件的字节数

* 文件拥有者的User ID

* 文件的Group ID

* 文件的读、写、执行权限

* 文件的时间戳,共有三个:ctime指inode上一次变动的时间,mtime指文件内容上一次变动的时间,atime指文件上一次打开的时间。

* 链接数,即有多少文件名指向这个inode

* 文件数据block的位置

inode也会消耗硬盘空间,所以硬盘格式化的时候,操作系统自动将硬盘分成两个区域。一个是数据区,存放文件数据;另一个是inode区(inode table),存放inode所包含的信息。

每个inode节点的大小,一般是128字节或256字节。inode节点的总数,在格式化时就给定,一般是每1KB或每2KB就设置一个inode。假定在一块1GB的硬盘中,每个inode节点的大小为128字节,每1KB就设置一个inode,那么inode table的大小就会达到128MB,占整块硬盘的12.8%。

每个inode都有一个号码,操作系统用inode号码来识别不同的文件。

这里值得重复一遍,Unix/Linux系统内部不使用文件名,而使用inode号码来识别文件。对于系统来说,文件名只是inode号码便于识别的别称或者绰号。

表面上,用户通过文件名,打开文件。实际上,系统内部这个过程分成三步:首先,系统找到这个文件名对应的inode号码;其次,通过inode号码,获取inode信息;最后,根据inode信息,找到文件数据所在的block,读出数据。

一般情况下,文件名和inode号码是"一一对应"关系,每个inode号码对应一个文件名。但是,Unix/Linux系统允许,多个文件名指向同一个inode号码。

这意味着,可以用不同的文件名访问同样的内容;对文件内容进行修改,会影响到所有文件名;但是,删除一个文件名,不影响另一个文件名的访问。这种情况就被称为"硬链接"(hard link)。

ln命令可以创建硬链接:ln 源文件 目标文件

文件A和文件B的inode号码虽然不一样,但是文件A的内容是文件B的路径。读取文件A时,系统会自动将访问者导向文件B。因此,无论打开哪一个文件,最终读取的都是文件B。这时,文件A就称为文件B的"软链接"(soft link)或者"符号链接(symbolic link)。

这意味着,文件A依赖于文件B而存在,如果删除了文件B,打开文件A就会报错:“No such file or directory”。这是软链接与硬链接最大的不同:文件A指向文件B的文件名,而不是文件B的inode号码,文件B的inode"链接数"不会因此发生变化。

ln -s命令可以创建软链接。:ln -s 源文文件或目录 目标文件或目录

http://www.ruanyifeng.com/blog/2011/12/inode.html

/proc存在哪里(存在内存上)

/proc 文件系统是一个虚拟文件系统,通过它可以使用一种新的方法在 Linux® 内核空间和用户空间之间进行通信。在 /proc 文件系统中,我们可以将对虚拟文件的读写作为与内核中实体进行通信的一种手段,但是与普通文件不同的是,这些虚拟文件的内容都是动态创建的

http://www.ibm.com/developerworks/cn/linux/l-proc.html

##网络: TCP和UDP区别、

key:TCP是一种面向连接的、可靠的、字节流服务

1.面向链接:TCP面向链接,面向连接意味着两个使用TCP的应用(通常是一个客户和一个服务器)在彼此交换数据之前必须通过三次握手先建立一个TCP连接。在一个TCP中仅有两方彼此通信,多播和广播不能用于TCP。UDP是不可靠的传输,传输前不需要建立链接,可以应用多播和广播实现一对多的通信。

2.可靠性:TCP提供端到端的流量控制,对收到的数据进行确认,采用超时重发,对失序的数据进行重新排序等机制保证数据通信的可靠性。而UDP是一种不可靠的服务,接收方可能不能收到发送方的数据报。

3.TCP是一种流模式的协议,UDP是一种数据报模式的协议。进程的每个输出操作都正好产生一个UDP数据报,并组装成一份待发送的IP数据报。TCP应用程序产生的全体数据与真正发送的单个IP数据报可能没有什么联系。TCP会有粘包和半包的现象。

4.效率上:速度上,一般TCP速度慢,传输过程中需要对数据进行确认,超时重发,还要对数据进行排序。UDP没有这些机制所以速度快。数据比例,TCP头至少20个字节,UDP头8个字节,相对效率高。组装效率上:TCP头至少20个字节,UDP头8个字节,系统组装上TCP相对慢。

5.用途上:用于TCP可靠性,http,ftp使用。而由于UDP速度快,视频,在线游戏多用UDP,保证实时性

对于第三点的理解。TCP可能发送100个“包”,而接收到50个“包”,不是丢“包”了,而是每次接受的“包”都比发送的多,其实TCP并没有包的概念。例如,每次发10个字节,可能读得时候一次读了20个字节。TCP是一种流模式的协议,在接收到的缓存中按照发送的包得顺序自动按照顺序拼接好,因为数据基本来自同一个主机,而且是按照顺序发送过来的,TCP的缓存中存放的就是,连续的数据。感觉好像是多封装了一步比UDP。而UDP因为可能两个不同的主机,给同一个主机发送,(一个端口可能收到多个应用程序的数据),或者按照TCP那样合并数据,必然会造成数据错误。我觉得关键的原因还是,TCP是面向连接,而UDP是无连接的,这就导致,TCP接收的数据为一个主机发来且有序无误的,而UDP可能是多个主机发来的无序,可能错误的。

TCP和UDP头部字节定义,

TCP和UDP三次握手和四次挥手状态及消息类型,

time_wait,close_wait状态产生原因,keepalive,

TIME_WAIT:表示收到了对方的FIN报文,并发送出了ACK报文。 TIME_WAIT状态下的TCP连接会等待2*MSL(Max Segment Lifetime,最大分段生存期,指一个TCP报文在Internet上的最长生存时间。每个具体的TCP协议实现都必须选择一个确定的MSL值,RFC 1122建议是2分钟,但BSD传统实现采用了30秒,Linux可以cat /proc/sys/net/ipv4/tcp_fin_timeout看到本机的这个值),然后即可回到CLOSED 可用状态了。如果FIN_WAIT_1状态下,收到了对方同时带FIN标志和ACK标志的报文时,可以直接进入到TIME_WAIT状态,而无须经过FIN_WAIT_2状态。

如果使用了nginx代理,那么系统TIME_WAIT的数量会变得比较多,这是由于nginx代理使用了短链接的方式和后端交互的原因,使得nginx和后端的ESTABLISHED变得很少而TIME_WAIT很多。这不但发生在安装nginx的代理服务器上,而且也会使后端的app服务器上有大量的TIME_WAIT。查阅TIME_WAIT资料,发现这个状态很多也没什么大问题,但可能因为它占用了系统过多的端口,导致后续的请求无法获取端口而造成障碍。

虽然TIME_WAIT会造成一些问题,但是要完全枪毙掉它也是不正当的,虽然看起来这么做没什么错。具体可看这篇文档:

http://hi.baidu.com/tim_bi/blog/item/35b005d784ca91d5a044df1d.html

所以目前看来最好的办法是让每个TIME_WAIT早点过期。

在linux上可以这么配置:

#让TIME_WAIT状态可以重用,这样即使TIME_WAIT占满了所有端口,也不会拒绝新的请求造成障碍

echo “1” > /proc/sys/net/ipv4/tcp_tw_reuse

#让TIME_WAIT尽快回收,我也不知是多久,观察大概是一秒钟

echo “1” > /proc/sys/net/ipv4/tcp_tw_recycle

很多文档都会建议两个参数都配置上,但是我发现只用修改tcp_tw_recycle就可以解决问题的了,TIME_WAIT重用TCP协议本身就是不建议打开的。

不能重用端口可能会造成系统的某些服务无法启动,比如要重启一个系统监控的软件,它用了40000端口,而这个端口在软件重启过程中刚好被使用了,就可能会重启失败的。linux默认考虑到了这个问题,有这么个设定:

#查看系统本地可用端口极限值

cat /proc/sys/net/ipv4/ip_local_port_range

用这条命令会返回两个数字,默认是:32768 61000,说明这台机器本地能向外连接61000-32768=28232个连接,注意是本地向外连接,不是这台机器的所有连接,不会影响这台机器的80端口的对外连接数。但这个数字会影响到代理服务器(nginx)对app服务器的最大连接数,因为nginx对app是用的异步传输,所以这个环节的连接速度很快,所以堆积的连接就很少。假如nginx对app服务器之间的带宽出了问题或是app服务器有问题,那么可能使连接堆积起来,这时可以通过设定nginx的代理超时时间,来使连接尽快释放掉,一般来说极少能用到28232个连接。

因为有软件使用了40000端口监听,常常出错的话,可以通过设定ip_local_port_range的最小值来解决:

echo “40001 61000” > /proc/sys/net/ipv4/ip_local_port_range

但是这么做很显然把系统可用端口数减少了,这时可以把ip_local_port_range的最大值往上调,但是好习惯是使用不超过32768的端口来侦听服务,另外也不必要去修改ip_local_port_range数值成1024 65535之类的,意义不大。

因为使用了nginx代理,在windows下也会造成大量TIME_WAIT,当然windows也可以调整:

在注册表(regedit)的HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters上添加一个DWORD类型的值TcpTimedWaitDelay,值就是秒数,即可。

windows默认是重用TIME_WAIT,我现在还不知道怎么改成不重用的,本地端口也没查到是什么值,但这些都关系不大,都可以按系统默认运作。


TIME_WAIT状态

根据TCP协议,主动发起关闭的一方,会进入TIME_WAIT状态,持续2*MSL(Max Segment Lifetime),缺省为240秒,在这个post中简洁的介绍了为什么需要这个状态。

值得一说的是,对于基于TCP的HTTP协议,关闭TCP连接的是Server端,这样,Server端会进入TIME_WAIT状态,可想而知,对于访问量大的Web Server,会存在大量的TIME_WAIT状态,假如server一秒钟接收1000个请求,那么就会积压240*1000=240,000个TIME_WAIT的记录,维护这些状态给Server带来负担。当然现代操作系统都会用快速的查找算法来管理这些TIME_WAIT,所以对于新的TCP连接请求,判断是否hit中一个TIME_WAIT不会太费时间,但是有这么多状态要维护总是不好。

HTTP协议1.1版规定default行为是Keep-Alive,也就是会重用TCP连接传输多个request/response,一个主要原因就是发现了这个问题。还有一个方法减缓TIME_WAIT压力就是把系统的2*MSL时间减少,因为240秒的时间实在是忒长了点,对于Windows,修改注册表,在HKEY_LOCAL_MACHINE\ SYSTEM\CurrentControlSet\Services\ Tcpip\Parameters上添加一个DWORD类型的值TcpTimedWaitDelay,一般认为不要少于60,不然可能会有麻烦。

对于大型的服务,一台server搞不定,需要一个LB(Load Balancer)把流量分配到若干后端服务器上,如果这个LB是以NAT方式工作的话,可能会带来问题。假如所有从LB到后端Server的IP包的source address都是一样的(LB的对内地址),那么LB到后端Server的TCP连接会受限制,因为频繁的TCP连接建立和关闭,会在server上留下TIME_WAIT状态,而且这些状态对应的remote address都是LB的,LB的source port撑死也就60000多个(2^16=65536,1~1023是保留端口,还有一些其他端口缺省也不会用),每个LB上的端口一旦进入Server的TIME_WAIT黑名单,就有240秒不能再用来建立和Server的连接,这样LB和Server最多也就能支持300个左右的连接。如果没有LB,不会有这个问题,因为这样server看到的remote address是internet上广阔无垠的集合,对每个address,60000多个port实在是够用了。

一开始我觉得用上LB会很大程度上限制TCP的连接数,但是实验表明没这回事,LB后面的一台Windows Server 2003每秒处理请求数照样达到了600个,难道TIME_WAIT状态没起作用?用Net Monitor和netstat观察后发现,Server和LB的XXXX端口之间的连接进入TIME_WAIT状态后,再来一个LB的XXXX端口的SYN包,Server照样接收处理了,而是想像的那样被drop掉了。翻书,从书堆里面找出覆满尘土的大学时代买的《UNIX Network Programming, Volume 1, Second Edition: Networking APIs: Sockets and XTI》,中间提到一句,对于BSD-derived实现,只要SYN的sequence number比上一次关闭时的最大sequence number还要大,那么TIME_WAIT状态一样接受这个SYN,难不成Windows也算BSD-derived?有了这点线索和关键字(BSD),找到这个post,在NT4.0的时候,还是和BSD-derived不一样的,不过Windows Server 2003已经是NT5.2了,也许有点差别了。

做个试验,用Socket API编一个Client端,每次都Bind到本地一个端口比如2345,重复的建立TCP连接往一个Server发送Keep-Alive=false的HTTP请求,Windows的实现让sequence number不断的增长,所以虽然Server对于Client的2345端口连接保持TIME_WAIT状态,但是总是能够接受新的请求,不会拒绝。那如果SYN的Sequence Number变小会怎么样呢?同样用Socket API,不过这次用Raw IP,发送一个小sequence number的SYN包过去,Net Monitor里面看到,这个SYN被Server接收后如泥牛如海,一点反应没有,被drop掉了。

按照书上的说法,BSD-derived和Windows Server 2003的做法有安全隐患,不过至少这样至少不会出现TIME_WAIT阻止TCP请求的问题,当然,客户端要配合,保证不同TCP连接的sequence number要上涨不要下降。


Socket中的TIME_WAIT状态

在高并发短连接的server端,当server处理完client的请求后立刻closesocket此时会出现time_wait状态然后如果client再并发2000个连接,此时部分连接就连接不上了,用linger强制关闭可以解决此问题,但是linger会导致数据丢失,linger值为0时是强制关闭,无论并发多少多能正常连接上,如果非0会发生部分连接不上的情况!(可调用setsockopt设置套接字的linger延时标志,同时将延时时间设置为0。)

TCP/IP的RFC文档。TIME_WAIT是TCP连接断开时必定会出现的状态。

是无法避免掉的,这是TCP协议实现的一部分。

在WINDOWS下,可以修改注册表让这个时间变短一些

time_wait的时间为2msl,默认为4min.

你可以通过改变这个变量:

TcpTimedWaitDelay

把它缩短到30s

TCP要保证在所有可能的情况下使得所有的数据都能够被投递。当你关闭一个socket时,主动关闭一端的socket将进入TIME_WAIT状态,而被动关闭一方则转入CLOSED状态,这的确能够保证所有的数据都被传输。当一个socket关闭的时候,是通过两端互发信息的四次握手过程完成的,当一端调用close()时,就说明本端没有数据再要发送了。这好似看来在握手完成以后,socket就都应该处于关闭CLOSED状态了。但这有两个问题,首先,我们没有任何机制保证最后的一个ACK能够正常传输,第二,网络上仍然有可能有残余的数据包(wandering duplicates),我们也必须能够正常处理。

通过正确的状态机,我们知道双方的关闭过程如下

假设最后一个ACK丢失了,服务器会重发它发送的最后一个FIN,所以客户端必须维持一个状态信息,以便能够重发ACK;如果不维持这种状态,客户端在接收到FIN后将会响应一个RST,服务器端接收到RST后会认为这是一个错误。如果TCP协议能够正常完成必要的操作而终止双方的数据流传输,就必须完全正确的传输四次握手的四个节,不能有任何的丢失。这就是为什么socket在关闭后,仍然处于 TIME_WAIT状态,因为他要等待以便重发ACK。

如果目前连接的通信双方都已经调用了close(),假定双方都到达CLOSED状态,而没有TIME_WAIT状态时,就会出现如下的情况。现在有一个新的连接被建立起来,使用的IP地址与端口与先前的完全相同,后建立的连接又称作是原先连接的一个化身。还假定原先的连接中有数据报残存于网络之中,这样新的连接收到的数据报中有可能是先前连接的数据报。为了防止这一点,TCP不允许从处于TIME_WAIT状态的socket建立一个连接。处于TIME_WAIT状态的socket在等待两倍的MSL时间以后(之所以是两倍的MSL,是由于MSL是一个数据报在网络中单向发出到认定丢失的时间,一个数据报有可能在发送图中或是其响应过程中成为残余数据报,确认一个数据报及其响应的丢弃的需要两倍的MSL),将会转变为CLOSED状态。这就意味着,一个成功建立的连接,必然使得先前网络中残余的数据报都丢失了。

由于TIME_WAIT状态所带来的相关问题,我们可以通过设置SO_LINGER标志来避免socket进入TIME_WAIT状态,这可以通过发送RST而取代正常的TCP四次握手的终止方式。但这并不是一个很好的主意,TIME_WAIT对于我们来说往往是有利的。

客户端与服务器端建立TCP/IP连接后关闭SOCKET后,服务器端连接的端口

状态为TIME_WAIT

是不是所有执行主动关闭的socket都会进入TIME_WAIT状态呢?

有没有什么情况使主动关闭的socket直接进入CLOSED状态呢?

主动关闭的一方在发送最后一个 ack 后

就会进入 TIME_WAIT 状态 停留2MSL(max segment lifetime)时间

这个是TCP/IP必不可少的,也就是“解决”不了的。

也就是TCP/IP设计者本来是这么设计的

主要有两个原因

1。防止上一次连接中的包,迷路后重新出现,影响新连接

(经过2MSL,上一次连接中所有的重复包都会消失)

2。可靠的关闭TCP连接

在主动关闭方发送的最后一个 ack(fin) ,有可能丢失,这时被动方会重新发

fin, 如果这时主动方处于 CLOSED 状态 ,就会响应 rst 而不是 ack。所以

主动方要处于 TIME_WAIT 状态,而不能是 CLOSED 。

TIME_WAIT 并不会占用很大资源的,除非受到攻击。

还有,如果一方 send 或 recv 超时,就会直接进入 CLOSED 状态

socket-faq中的这一段讲的也很好,摘录如下:

2.7. Please explain the TIME_WAIT state.

什么是滑动窗口,超时重传,

列举你所知道的tcp选项,

connect会阻塞检测及防止,socket什么情况下可读?

connect会阻塞,怎么解决?(必考必问)

最通常的方法最有效的是加定时器;也可以采用非阻塞模式。

设置非阻塞,返回之后用select检测状态)

如果select返回可读,结果只读到0字节,什么情况?

某个套接字集合中没有准备好,可能会select内存用FD_CLR清该位为0;

socket什么情况下可读?

每次读操作返回前都要检查是否还有剩余数据没读完,如果是的话保持数据有效标志,不这样设计的话会出现明显的不一致,那就是数据在读缓冲但没有读有效标志。

keepalive是什么东东?如何使用?

设置Keepalive参数,检测已中断的客户连接

在TCP中有一个Keep-alive的机制可以检测死连接,原理很简单,TCP会在空闲了一定时间后发送数据给对方:

1.如果主机可达,对方就会响应ACK应答,就认为是存活的。

2.如果可达,但应用程序退出,对方就发RST应答,发送TCP撤消连接。

3.如果可达,但应用程序崩溃,对方就发FIN消息。

4.如果对方主机不响应ack, rst,继续发送直到超时,就撤消连接。这个时间就是默认

的二个小时。

UDP中使用connect的好处:

1:会提升效率.前面已经描述了.2:高并发服务中会增加系统稳定性.原因:假设client A 通过非connect的UDP与serverB,C通信.B,C提供相同服务.为了负载均衡,我们让A与B,C交替通信.A 与 B通信IPa:PORTa<----> IPb:PORTbA 与 C通信IPa:PORTa’<---->IPc:PORTc

假设PORTa 与 PORTa’相同了(在大并发情况下会发生这种情况),那么就有可能出现A等待B的报文,却收到了C的报文.导致收报错误.解决方法内就是采用connect的UDP通信方式.在A中创建两个udp,然后分别connect到B,C.

长连接和短连接,

DNS和HTTP协议,HTTP请求方式,

cookie,session,localstroage,

一致性哈希负载均衡,

描述在浏览器中敲入一个网址并按下回车后所发生的事情,

PING命令

ping命令所利用的原理是这样的:网络上的机器都有唯一确定的IP地址,我们给目标IP地址发送一个数据包,对方就要返回一个同样大小的数据包,根据返回的数据包我们可以确定目标主机的存在,可以初步判断目标主机的操作系统等。

##数据库: 谈谈你对数据库中索引的理解,索引和主键区别

聚集索引一个表只能有一个,而非聚集索引一个表可以存在多个。

聚集索引存储记录是物理上连续存在,而非聚集索引是逻辑上的连续,物理存储并不连续。

聚集索引:该索引中键值的逻辑顺序决定了表中相应行的物理顺序。

聚集索引确定表中数据的物理顺序。聚集索引类似于电话簿,后者按姓氏排列数据。由于聚集索引规定数据在表中的物理存储顺序,因此一个表只能包含一个聚集索引。但该索引可以包含多个列(组合索引),就像电话簿按姓氏和名字进行组织一样。

   聚集索引使用注意事项

定义聚集索引键时使用的列越少越好。

• 包含大量非重复值的列。

.• 使用下列运算符返回一个范围值的查询:BETWEEN、>、>=、< 和 <=。

• 被连续访问的列。

• 回大型结果集的查询。

• 经常被使用联接或 GROUP BY 子句的查询访问的列;一般来说,这些是外键列。对 ORDER BY 或 GROUP BY 子句中指定的列进行索引,可以使 SQL Server 不必对数据进行排序,因为这些行已经排序。这样可以提高查询性能。

• OLTP 类型的应用程序,这些程序要求进行非常快速的单行查找(一般通过主键)。应在主键上创建聚集索引。

聚集索引不适用于:

• 频繁更改的列 。这将导致整行移动(因为 SQL Server 必须按物理顺序保留行中的数据值)。这一点要特别注意,因为在大数据量事务处理系统中数据是易失的。

• 宽键 。来自聚集索引的键值由所有非聚集索引作为查找键使用,因此存储在每个非聚集索引的叶条目内。

非聚集索引:数据存储在一个地方,索引存储在另一个地方,索引带有指针指向数据的存储位置。

非聚集索引中的项目按索引键值的顺序存储,而表中的信息按另一种顺序存储(这可以由聚集索引规定)。对于非聚集索引,可以为在表非聚集索引中查找数据时常用的每个列创建一个非聚集索引。有些书籍包含多个索引。例如,一本介绍园艺的书可能会包含一个植物通俗名称索引,和一个植物学名索引,因为这是读者查找信息的两种最常用的方法。



一个通俗的举例,说明两者的区别



其实,我们的汉语字典的正文本身就是一个聚集索引。比如,我们要查“安”字,就会很自然地翻开字典的前几页,因为“安”的拼音是“an”,而按照拼音排序汉字的字典是以英文字母“a”开头并以“z”结尾的,那么“安”字就自然地排在字典的前部。如果您翻完了所有以“a”开头的部分仍然找不到这个字,那么就说明您的字典中没有这个字;同样的,如果查“张”字,那您也会将您的字典翻到最后部分,因为“张”的拼音是“zhang”。也就是说,字典的正文部分本身就是一个目录,您不需要再去查其他目录来找到您需要找的内容。我们把这种正文内容本身就是一种按照一定规则排列的目录称为“聚集索引”。

如果您认识某个字,您可以快速地从自动中查到这个字。但您也可能会遇到您不认识的字,不知道它的发音,这时候,您就不能按照刚才的方法找到您要查的字,而需要去根据“偏旁部首”查到您要找的字,然后根据这个字后的页码直接翻到某页来找到您要找的字。但您结合“部首目录”和“检字表”而查到的字的排序并不是真正的正文的排序方法,比如您查“张”字,我们可以看到在查部首之后的检字表中“张”的页码是672页,检字表中“张”的上面是“驰”字,但页码却是63页,“张”的下面是“弩”字,页面是390页。很显然,这些字并不是真正的分别位于“张”字的上下方,现在您看到的连续的“驰、张、弩”三字实际上就是他们在非聚集索引中的排序,是字典正文中的字在非聚集索引中的映射。我们可以通过这种方式来找到您所需要的字,但它需要两个过程,先找到目录中的结果,然后再翻到您所需要的页码。我们把这种目录纯粹是目录,正文纯粹是正文的排序方式称为“非聚集索引”。

第一:聚集索引的约束是唯一性,是否要求字段也是唯一的呢?

分析:如果认为是的朋友,可能是受系统默认设置的影响,一般我们指定一个表的主键,如果这个表之前没有聚集索引,同时建立主键时候没有强制指定使用非聚集索引,SQL会默认在此字段上创建一个聚集索引,而主键都是唯一的,所以理所当然的认为创建聚集索引的字段也需要唯一。

结论:聚集索引可以创建在任何一列你想创建的字段上,这是从理论上讲,实际情况并不能随便指定,否则在性能上会是恶梦。

第二:为什么聚集索引可以创建在任何一列上,如果此表没有主键约束,即有可能存在重复行数据呢?

粗一看,这还真是和聚集索引的约束相背,但实际情况真可以创建聚集索引。

分析其原因是:如果未使用 UNIQUE 属性创建聚集索引,数据库引擎将向表自动添加一个四字节 uniqueifier 列。必要时,数据库引擎 将向行自动添加一个 uniqueifier 值,使每个键唯一。此列和列值供内部使用,用户不能查看或访问。

第三:是不是聚集索引就一定要比非聚集索引性能优呢?

如果想查询学分在60-90之间的学生的学分以及姓名,在学分上创建聚集索引是否是最优的呢?

答:否。既然只输出两列,我们可以在学分以及学生姓名上创建联合非聚集索引,此时的索引就形成了覆盖索引,即索引所存储的内容就是最终输出的数据,这种索引在比以学分为聚集索引做查询性能更好。

第四:在数据库中通过什么描述聚集索引与非聚集索引的?

索引是通过二叉树的形式进行描述的,我们可以这样区分聚集与非聚集索引的区别:聚集索引的叶节点就是最终的数据节点,而非聚集索引的叶节仍然是索引节点,但它有一个指向最终数据的指针。

第五:在主键是创建聚集索引的表在数据插入上为什么比主键上创建非聚集索引表速度要慢?

有了上面第四点的认识,我们分析这个问题就有把握了,在有主键的表中插入数据行,由于有主键唯一性的约束,所以需要保证插入的数据没有重复。我们来比较下主键为聚集索引和非聚集索引的查找情况:聚集索引由于索引叶节点就是数据页,所以如果想检查主键的唯一性,需要遍历所有数据节点才行,但非聚集索引不同,由于非聚集索引上已经包含了主键值,所以查找主键唯一性,只需要遍历所有的索引页就行,这比遍历所有数据行减少了不少IO消耗。这就是为什么主键上创建非聚集索引比主键上创建聚集索引在插入数据时要快的真正原因。

现在普通关系数据库用得数据结构是什么类型的数据结构,

B+树

MySQL索引背后的数据结构及算法原理

索引的优点和缺点,

建立索引的优点

1.大大加快数据的检索速度;

2.创建唯一性索引,保证数据库表中每一行数据的唯一性;

3.加速表和表之间的连接;

4.在使用分组和排序子句进行数据检索时,可以显著减少查询中分组和排序的时间。

索引的缺点

1.索引需要占物理空间。

2.当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,降低了数据的维护速度。

唯一索引

唯一索引是不允许其中任何两行具有相同索引值的索引。

当现有数据中存在重复的键值时,大多数数据库不允许将新创建的唯一索引与表一起保存。数据库还可能防止添加将在表中创建重复键值的新数据。例如,如果在 employee 表中职员的姓 (lname) 上创建了唯一索引,则任何两个员工都不能同姓。

主键索引

数据库表经常有一列或列组合,其值唯一标识表中的每一行。该列称为表的主键。

在数据库关系图中为表定义主键将自动创建主键索引,主键索引是唯一索引的特定类型。该索引要求主键中的每个值都唯一。当在查询中使用主键索引时,它还允许对数据的快速访问。

聚集索引

在聚集索引中,表中行的物理顺序与键值的逻辑(索引)顺序相同。一个表只能包含一个聚集索引。

如果某索引不是聚集索引,则表中行的物理顺序与键值的逻辑顺序不匹配。与非聚集索引相比,聚集索引通常提供更快的数据访问速度。

关系型数据库和非关系数据库的特点,

简单来说,关系模型指的就是二维表格模型,而一个关系型数据库就是由二维表及其之间的联系所组成的一个数据组织。

非关系型数据库提出另一种理念,例如,以键值对存储,且结构不固定,每一个元组可以有不一样的字段,每个元组可以根据需要增加一些自己的键值对,这 样就不会局限于固定的结构,可以减少一些时间和空间的开销。使用这种方式,用户可以根据需要去添加自己需要的字段,这样,为了获取用户的不同信息,不需要 像关系型数据库中,要对多表进行关联查询。仅需要根据id取出相应的value就可以完成查询。但非关系型数据库由于很少的约束,他也不能够提供像SQL 所提供的where这种对于字段属性值情况的查询。并且难以体现设计的完整性。他只适合存储一些较为简单的数据,对于需要进行较复杂查询的数据,SQL数 据库显的更为合适。

关系型数据库的最大特点就是事务的一致性:传统的关系型数据库读写操作都是事务的,具有ACID的特点,这个特性使得关系型数据库可以用于几乎所有对一致性有要求的系统中,如典型的银行系统。

但是,在网页应用中,尤其是SNS应用中,一致性却不是显得那么重要,用户A看到的内容和用户B看到同一用户C内容更新不一致是可以容忍的,或者 说,两个人看到同一好友的数据更新的时间差那么几秒是可以容忍的,因此,关系型数据库的最大特点在这里已经无用武之地,起码不是那么重要了。

相反地,关系型数据库为了维护一致性所付出的巨大代价就是其读写性能比较差,而像微博、facebook这类SNS的应用,对并发读写能力要求极 高,关系型数据库已经无法应付(在读方面,传统上为了克服关系型数据库缺陷,提高性能,都是增加一级memcache来静态化网页,而在SNS中,变化太 快,memchache已经无能为力了),因此,必须用新的一种数据结构存储来代替关系数据库。

关系数据库的另一个特点就是其具有固定的表结构,因此,其扩展性极差,而在SNS中,系统的升级,功能的增加,往往意味着数据结构巨大变动,这一点关系型数据库也难以应付,需要新的结构化数据存储。

于是,非关系型数据库应运而生,由于不可能用一种数据结构化存储应付所有的新的需求,因此,非关系型数据库严格上不是一种数据库,应该是一种数据结构化存储方法的集合。

必须强调的是,数据的持久存储,尤其是海量数据的持久存储,还是需要一种关系数据库这员老将。

非关系型数据库分类:

主要分为以下几类:

面向高性能并发读写的key-value数据库:

key-value数据库的主要特点即使具有极高的并发读写性能,Redis,Tokyo Cabinet,Flare就是这类的代表

面向海量数据访问的面向文档数据库:

这类数据库的特点是,可以在海量的数据中快速的查询数据,典型代表为MongoDB以及CouchDB

面向可扩展性的分布式数据库:

这类数据库想解决的问题就是传统数据库存在可扩展性上的缺陷,这类数据库可以适应数据量的增加以及数据结构的变化

关系型数据库和非关系型数据库

乐观锁与悲观锁的区别,

悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。[1] 悲观锁假定其他用户企图访问或者改变你正在访问、更改的对象的概率是很高的,因此在悲观锁的环境中,在你开始改变此对象之前就将该对象锁住,并且直到你提交了所作的更改之后才释放锁。悲观的缺陷是不论是页锁还是行锁,加锁的时间可能会很长,这样可能会长时间的限制其他用户的访问,也就是说悲观锁的并发访问性不好。

乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。[1] 乐观锁不能解决脏读的问题。 乐观锁则认为其他用户企图改变你正在更改的对象的概率是很小的,因此乐观锁直到你准备提交所作的更改时才将对象锁住,当你读取以及改变该对象时并不加锁。可见乐观锁加锁的时间要比悲观锁短,乐观锁可以用较大的锁粒度获得较好的并发访问性能。但是如果第二个用户恰好在第一个用户提交更改之前读取了该对象,那么当他完成了自己的更改进行提交时,数据库就会发现该对象已经变化了,这样,第二个用户不得不重新读取该对象并作出更改。这说明在乐观锁环境中,会增加并发用户读取对象的次数。

从数据库厂商的角度看,使用乐观的页锁是比较好的,尤其在影响很多行的批量操作中可以放比较少的锁,从而降低对资源的需求提高数据库的性能。再考虑聚集索引。在数据库中记录是按照聚集索引的物理顺序存放的。如果使用页锁,当两个用户同时访问更改位于同一数据页上的相邻两行时,其中一个用户必须等待另一个用户释放锁,这会明显地降低系统的性能。interbase和大多数关系数据库一样,采用的是乐观锁,而且读锁是共享的,写锁是排他的。可以在一个读锁上再放置读锁,但不能再放置写锁;你不能在写锁上再放置任何锁。锁是目前解决多用户并发访问的有效手段。

乐观锁与悲观锁的区别

数据库范式

1NF的定义为:符合1NF的关系中的每个属性都不可再分,1NF是所有关系型数据库的最基本要求,

2NF在1NF的基础之上,消除了非主属性对于码的部分函数依赖。

3NF在2NF的基础之上,消除了非主属性对于码的传递函数依赖。也就是说, 如果存在非主属性对于码的传递函数依赖,则不符合3NF的要求。

BCNF范式在 3NF 的基础上消除主属性对于码的部分与传递函数依赖。

解释一下关系数据库的第一第二第三范式?

数据库日志类型作用

MySQL日志文件分类

1.错误日志(Error Log)

2.二进制日志(Binary Log & Binary Log Index)

3.通用查询日志(query log)

4.慢查询日志(slow query log)

5.Innodb的在线 redo 日志(innodb redo log)

6.更新日志(update log)

innodb和myisam的区别

innodb,聚集索引,支持外键和事务(commit)、回滚(rollback)和崩溃修复能力(crash recovery capabilities)的事务安全(transaction-safe (ACID compliant)),不支持全文索引,不支持计数,统计的时候会遍历,

myisam,非聚集索引,不支持外键事务,支持全文索引,支持计数,查询效果较好

你对innodb哪里最熟悉

innodb的索引有哪几种类型

4种,hash,b-tree,spatial,full-text

  1. B-Tree索引

最常见的索引类型,基于B-Tree数据结构。B-Tree的基本思想是,所有值(被索引的列)都是排过序的,每个叶节点到跟节点距离相等。所以B-Tree适合用来查找某一范围内的数据,而且可以直接支持数据排序(ORDER BY)。但是当索引多列时,列的顺序特别重要,需要格外注意。InnoDB和MyISAM都支持B-Tree索引。InnoDB用的是一个变种B+Tree,而MyISAM为了节省空间对索引进行了压缩,从而牺牲了性能。

  1. Hash索引

基于hash表。所以这种索引只支持精确查找,不支持范围查找,不支持排序。这意味着范围查找或ORDER BY都要依赖server层的额外工作。目前只有Memory引擎支持显式的hash索引(但是它的hash是nonunique的,冲突太多时也会影响查找性能)。Memory引擎默认的索引类型即是Hash索引,虽然它也支持B-Tree索引。

Hash 索引仅仅能满足"=",“IN"和”<=>"查询,不能使用范围查询。

例子:

CREATE TABLE testhash (

fname VARCHAR(50) NOT NULL,

lname VARCHAR(50) NOT NULL,

KEY USING HASH(fname)

) ENGINE =MEMORY;

  1. Spatial (R-Tree)(空间)索引

只有MyISAM引擎支持,并且支持的不好。可以忽略。

  1. Full-text索引

主要用来查找文本中的关键字,而不是直接与索引中的值相比较。Full-text索引跟其它索引大不相同,它更像是一个搜索引擎,而不是简单的WHERE语句的参数匹配。你可以对某列分别进行full-text索引和B-Tree索引,两者互不冲突。Full-text索引配合MATCH AGAINST操作使用,而不是一般的WHERE语句加LIKE。

http://segmentfault.com/q/1010000003832312

B TREE 和B+TREE的区别

innodb有全文索引吗

没有,myisam有

union和join

JOIN用于按照ON条件联接两个表,主要有四种:

INNER JOIN:内部联接两个表中的记录,仅当至少有一个同属于两表的行符合联接条件时,内联接才返回行。我理解的是只要记录不符合ON条件,就不会显示在结果集内。

LEFT JOIN / LEFT OUTER JOIN:外部联接两个表中的记录,并包含左表中的全部记录。如果左表的某记录在右表中没有匹配记录,则在相关联的结果集中右表的所有选择列表列均为空值。理解为即使不符合ON条件,左表中的记录也全部显示出来,且结果集中该类记录的右表字段为空值。

RIGHT JOIN / RIGHT OUTER JOIN:外部联接两个表中的记录,并包含右表中的全部记录。简单说就是和LEFT JOIN反过来。

FULL JOIN / FULL OUTER JOIN: 完整外部联接返回左表和右表中的所有行。就是LEFT JOIN和RIGHT JOIN和合并,左右两表的数据都全部显示。

JOIN的基本语法:

Select table1.* FROM table1 JOIN table2 ON table1.id=table2.id

UNION运算符

将两个或更多查询的结果集组合为单个结果集,该结果集包含联合查询中的所有查询的全部行。UNION的结果集列名与UNION运算符中第一个Select语句的结果集的列名相同。另一个Select语句的结果集列名将被忽略。

其中两种不同的用法是UNION和UNION ALL,区别在于UNION从结果集中删除重复的行。如果使用UNION ALL 将包含所有行并且将不删除重复的行。

相同点:在某些特定的情况下,可以用join实现union all的功能,这种情况是有条件的,当出现这种情况的时候选择union all还是group by就可以看情况或者看两者的消耗而决定。

http://chengheng1984.blog.163.com/blog/static/17947412201012215738844/

http://www.51testing.com/html/14/446214-249265.html

##海量数据处理: bitmap

Map-Reduce原理,

BloomFilter原理、

它实际上是一个很长的二进制向量和一系列随机映射函数(Hash函数)。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。Bloom Filter广泛的应用于各种需要查询的场合中,如Orocle的数据库,Google的BitTable也用了此技术。

Bloom Filter特点:

不存在漏报(False Negative),即某个元素在某个集合中,肯定能报出来。

可能存在误报(False Positive),即某个元素不在某个集合中,可能也被爆出来。

确定某个元素是否在某个集合中的代价和总的元素数目无关。

Trie树原理、 单词搜索树

B+树原理,

LSM树原理,

大数据处理

https://cloud.tencent.com/developer/article/1443523

1、 一个C++源文件从文本到可执行文件经历的过程

对于C/C++编写的程序,从源代码到可执行文件,一般经过下面四个步骤:

1).预处理,产生.ii文件

2).编译,产生汇编文件(.s文件)

3).汇编,产生目标文件(.o或.obj文件)

4).链接,产生可执行文件(.out或.exe文件)

2、#include 的顺序以及尖叫括号和双引号的区别

  1. #include的顺序的区别:

头文件的引用顺序对于程序的编译还是有一定影响的。如果要在文件a.h中声明一个在文件b.h中定义的变量,而不引用b.h。那么要在a.c文件中引用b.h文件,并且要先引用b.h,后引用a.h,否则汇报变量类型未声明错误,也就是常见的某行少个“;”符号。

  1. #include尖括号和双引号的区别:

1)#include <> ,认为该头文件是标准头文件。编译器将会在预定义的位置集查找该头文件,这些预定义的位置可以通过设置查找路径环境变量或者通过命令行选项来修改。使用的查找方式因编译器的不同而差别迥异。

2)#include “”,认为它是非系统头文件,非系统头文件的查找通常开始于源文件所在的路径。查找范围大于<>。

3、进程和线程,为什么要有线程

1、和进程相比,它是一种非常"节俭"的多任务操作方式。在linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种"昂贵"的多任务工作方式。(资源)

2、运行于一个进程中的多个线程,它们之间使用相同的地址空间,而且线程间彼此切换所需时间也远远小于进程间切换所需要的时间。据统计,一个进程的开销大约是一个线程开销的30倍左右。(切换效率)

3、线程间方便的通信机制。对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过进程间通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进城下的线程之间贡献数据空间,所以一个线程的数据可以直接为其他线程所用,这不仅快捷,而且方便。(通信)

除以上优点外,多线程程序作为一种多任务、并发的工作方式,还有如下优点:

1、使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。(CPU设计保证)

2、改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序才会利于理解和修改。(代码易维护)

4、C++11有哪些新特性

1)关键字及新语法:auto、nullptr、for

2)STL容器:std::array、std::forward_list、std::unordered_map、std::unordered_set

3)多线程:std::thread、std::atomic、std::condition_variable

4)智能指针内存管理:std::shared_ptr、std::weak_ptr

5)其他:std::function、std::bind和lamda表达式

5、为什么可变参数模板至关重要,右值引用,完美转发,lambda

6、malloc的原理,brk系统调用干什么的,mmap呢

malloc的实现方案:

1)malloc 函数的实质是它有一个将可用的内存块连接为一个长长的列表的所谓空闲链表。

2)调用 malloc()函数时,它沿着连接表寻找一个大到足以满足用户请求所需要的内存块。 然后,将该内存块一分为二(一块的大小与用户申请的大小相等,另一块的大小就是剩下来的字节)。 接下来,将分配给用户的那块内存存储区域传给用户,并将剩下的那块(如果有的话)返回到连接表上。

3)调用 free 函数时,它将用户释放的内存块连接到空闲链表上。

4)到最后,空闲链会被切成很多的小内存片段,如果这时用户申请一个大的内存片段, 那么空闲链表上可能没有可以满足用户要求的片段了。于是,malloc()函数请求延时,并开始在空闲链表上检查各内存片段,对它们进行内存整理,将相邻的小空闲块合并成较大的内存块。

brk和mmap:

从操作系统角度来看,进程分配内存有两种方式,分别由两个系统调用完成:brk和mmap(不考虑共享内存)。

1、brk是将数据段(.data)的最高地址指针_edata往高地址推;

2、mmap是在进程的虚拟地址空间中(堆和栈中间,称为文件映射区域的地方)找一块空闲的虚拟内存。

这两种方式分配的都是虚拟内存,没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系。

在标准C库中,提供了malloc/free函数分配释放内存,这两个函数底层是由brk,mmap,munmap这些系统调用实现的。

7、C++的内存管理方式,STL的allocator,最新版本默认使用的分配器

C++的内存管理方式:

在c++中内存主要分为5个存储区:

栈(Stack):局部变量,函数参数等存储在该区,由编译器自动分配和释放.栈属于计算机系统的数据结构,进栈出栈有相应的计算机指令支持,而且分配专门的寄存器存储栈的地址,效率分高,内存空间是连续的,但栈的内存空间有限。

堆(Heap):需要程序员手动分配和释放(new,delete),属于动态分配方式。内存空间几乎没有限制,内存空间不连续,因此会产生内存碎片。操作系统有一个记录空间内存的链表,当收到内存申请时遍历链表,找到第一个空间大于申请空间的堆节点,将该节点分配给程序,并将该节点从链表中删除。一般,系统会在该内存空间的首地址处记录本次分配的内存大小,用于delete释放该内存空间。

全局/静态存储区:全局变量,静态变量分配到该区,到程序结束时自动释放,包括DATA段(全局初始化区)与BSS段(全局未初始化段)。其中,初始化的全局变量和静态变量存放在DATA段,未初始化的全局变量和静态变量存放在BSS段。BSS段特点:在程序执行前BSS段自动清零,所以未初始化的全局变量和静态变量在程序执行前已经成为0.

文字常量区:存放常量,而且不允许修改。程序结束后由系统释放。

程序代码区:存放程序的二进制代码

SGI 版本STL的默认配置器std::alloc

参见:《STL源码剖析》

1)考虑到小型区块所可能造成的内存碎片问题,SGI设计了双层配置器。第一级配置器直接使用malloc()和free();第二级则视情况采取不同的策略:当配置区块超过128bytes时,视为“足够大”,便调用第一级配置器;当配置区块小于128bytes时,视之为“过小”,为了降低额外负担,便采用memory pool(内存池)整理方式,而不在求助于第一级配置器。

2)内存池的核心:内存池和16个自由链表(各自管理8,16,…,128bytes的小额区块)。在分配一个小区块时,首先在所属自由链表中寻找,如果找到,直接抽出分配;若所属自由链表为空,则请求内存池为所属自由链表分配空间;默认情况下,为该自由链表分配20个区块,若内存池剩余容量不足,则分配可分配的最大容量;若内存池连一个区块都无法分配,则调用chunk_alloc为内存池分配一大块区块;若内存不足,则尝试调用malloc分配,否则返回bad_alloc异常。

8、hash表的实现,包括STL中的哈希桶长度常数。

hash表的实现主要涉及两个问题:散列函数和碰撞处理。

1)hash function (散列函数)。最常见的散列函数:f(x) = x % TableSize .

2)碰撞问题(不同元素的散列值相同)。解决碰撞问题的方法有许多种,包括线性探测、二次探测、开链等做法。SGL版本使用开链法,使用一个链表保持相同散列值的元素。

虽然开链法并不要求表格大小必须为质数,但SGI STL仍然以质数来设计表格大小,并且将28个质数(逐渐呈现大约两倍的关系)计算好,以备随时访问,同时提供一个函数,用来查询在这28个质数之中,“最接近某数并大于某数”的质数。

9、hash表如何rehash,怎么处理其中保存的资源

先想想为什么需要rehash:

因为,当loadFactor(负载因子)<=1时,hash表查找的期望复杂度为O(1). 因此,每次往hash表中添加元素时,我们必须保证是在loadFactor <1的情况下,才能够添加。

模仿C++的vector扩容方式,Hash表中每次发现loadFactor==1时,就开辟一个原来桶数组的两倍空间(称为新桶数组),然后把原来的桶数组中元素全部转移过来到新的桶数组中。注意这里转移是需要元素一个个重新哈希到新桶中的。

10、Redis的rehash怎么做的,为什么要渐进rehash,渐进rehash怎么实现的

为了避免rehash对服务器造成影响,服务器不是一次将ht[0]里面的所有键值对全部rehash到ht[1],而是分多次、渐进式地将ht[0]里面的键值对慢慢地rehash到ht[1].

以下是哈希表渐进式 rehash 的详细步骤:

为 ht[1] 分配空间, 让字典同时持有 ht[0] 和 ht[1] 两个哈希表。

在字典中维持一个索引计数器变量 rehashidx , 并将它的值设置为 0 , 表示 rehash 工作正式开始。

在 rehash 进行期间, 每次对字典执行添加、删除、查找或者更新操作时, 程序除了执行指定的操作以外, 还会顺带将 ht[0] 哈希表在 rehashidx 索引上的所有键值对 rehash 到 ht[1] , 当 rehash 工作完成之后, 程序将 rehashidx 属性的值增一。

随着字典操作的不断执行, 最终在某个时间点上, ht[0] 的所有键值对都会被 rehash 至 ht[1] , 这时程序将 rehashidx 属性的值设为 -1 , 表示 rehash 操作已完成。

渐进式 rehash 的好处在于它采取分而治之的方式, 将 rehash 键值对所需的计算工作均滩到对字典的每个添加、删除、查找和更新操作上, 从而避免了集中式 rehash 而带来的庞大计算量。

11、Redis的定时机制怎么实现的,有哪些弊端,你将如何改进这个弊端

Redis服务器是一个事件驱动程序,服务器需要处理以下两类事件:文件事件(服务器对套接字操作的抽象)和时间事件(服务器对定时操作的抽象)。Redis的定时机制就是借助时间事件实现的。

一个时间事件主要由以下三个属性组成:id:时间事件标识号;when:记录时间事件的到达时间;timeProc:时间事件处理器,当时间事件到达时,服务器就会调用相应的处理器来处理时间。一个时间事件根据时间事件处理器的返回值来判断是定时事件还是周期性事件。

弊端:Redis对时间事件的实际处理时间并不准时,通常会比时间事件设定的到达事件稍晚一些。

改进:多线程?一个处理文件事件,一个处理时间事件? (不确定)。

12、Redis是单线程的,为什么这么高效

虽然Redis文件事件处理器以单线程方式运行,但是通过使用I/O多路复用程序来监听多个套接字,文件事件处理器既实现了高性能的网络通信模型,又可以很好地与Redis服务器中其他同样以单线程运行的模块进行对接,这保持了Redis内部单线程设计的简单性。

13、Redis的数据类型有哪些,底层怎么实现

1)字符串:整数值、embstr编码的简单动态字符串、简单动态字符串(SDS)

2)列表:压缩列表、双端链表

3)哈希:压缩列表、字典

4)集合:整数集合、字典

5)有序集合:压缩列表、跳跃表和字典

14、Redis和memcached的区别

Redis和memcached的区别:

1)数据类型 :redis数据类型丰富,支持set liset等类型;memcache支持简单数据类型,需要客户端自己处理复杂对象

2)持久性:redis支持数据落地持久化存储;memcache不支持数据持久存储。)

3)分布式存储:redis支持master-slave复制模式;memcache可以使用一致性hash做分布式。

4)value大小不同:memcache是一个内存缓存,key的长度小于250字符,单个item存储要小于1M,不适合虚拟机使用

5)数据一致性不同:redis使用的是单线程模型,保证了数据按顺序提交;memcache需要使用cas保证数据一致性。CAS(Check and Set)是一个确保并发一致性的机制,属于“乐观锁”范畴;原理很简单:拿版本号,操作,对比版本号,如果一致就操作,不一致就放弃任何操作

6)cpu利用:redis单线程模型只能使用一个cpu,可以开启多个redis进程

15、TCP的模型,状态转移

TCP四层模型:

状态转移:

熟悉三次握手 和 四次释放的TCP状态转移。

16、用过哪些设计模式,单例模式,观察者模式的多线程安全问题

设计模式

1)Template Method模式:《effective c++》 条款35 :借助Non-virtual Interface手法实现Template Method模式

2)Strategy模式:《effective c++》 条款35:借助Function Pointers 实现Strategy模式、借助std::function完成Strategy模式、古典Strategy模式

17、用过多线程吗,以前的多线程代码还能怎么优化,线程池的实现

线程的创建

1#include
2
3int pthread_create(pthread_t *restrict thread,
4 const pthread_attr_t *restrict attr,
5 void *(start_routine)(void),
6 void *restrict arg);
线程终止:

从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。

一个线程可以调用pthread_cancel终止同一进程中的另一个线程。

线程可以调用pthread_exit终止自己。

线程池的实现:

18、epoll怎么实现的,reactor模型组成

epoll实现:

第一步:epoll_create()系统调用。此调用返回一个句柄,之后所有的使用都依靠这个句柄(eventpoll的对象)来标识。

1struct eventpoll{
2 …
3 /红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件/
4 struct rb_root rbr;
5 /双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件/
6 struct list_head rdlist;
7 …
8};
第二步:epoll_ctl()系统调用。通过此调用向epoll对象中添加、删除、修改感兴趣的事件,返回0标识成功,返回-1表示失败。

第三部:epoll_wait()系统调用。通过此调用收集收集在epoll监控中已经发生的事件。

Reactor模型:

1)Handle:即操作系统中的句柄,是对资源在操作系统层面上的一种抽象,它可以是打开的文件、一个连接(Socket)、Timer等。由于Reactor模式一般使用在网络编程中,因而这里一般指Socket Handle,即一个网络连接。

2)Synchronous Event Demultiplexer(同步事件复用器):阻塞等待一系列的Handle中的事件到来,如果阻塞等待返回,即表示在返回的Handle中可以不阻塞的执行返回的事件类型。这个模块一般使用操作系统的select来实现。

3)Initiation Dispatcher:用于管理Event Handler,即EventHandler的容器,用以注册、移除EventHandler等;另外,它还作为Reactor模式的入口调用Synchronous Event Demultiplexer的select方法以阻塞等待事件返回,当阻塞等待返回时,根据事件发生的Handle将其分发给对应的Event Handler处理,即回调EventHandler中的handle_event()方法。

4)Event Handler:定义事件处理方法:handle_event(),以供InitiationDispatcher回调使用。

5)Concrete Event Handler:事件EventHandler接口,实现特定事件处理逻辑。

20、手撕代码:1)给定一个数字数组,返回哈夫曼树的头指针。2)最长公共连续子序列。

21、随便挑一个自己收获最多比赛或者项目介绍,收获了什么

22、单核机器上写多线程程序,是否需要考虑加锁,为什么?

23、线程需要保存哪些上下文,SP、PC、EAX这些寄存器是干嘛用的

24、HTTP和HTTPS的区别,HTTPS有什么特点,带来的好处和坏处,怎么实现的

25、线程间的同步方式,最好说出具体的系统调用

1)互斥量(mutex)

1#include
2
3int pthread_mutex_destroy(pthread_mutex_t *mutex); //销毁
4int pthread_mutex_init(pthread_mutex_t *restrict mutex,
5 const pthread_mutexattr_t *restrict attr); //初始化
6pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
7
8int pthread_mutex_lock(pthread_mutex_t *mutex); //上锁
9int pthread_mutex_trylock(pthread_mutex_t *mutex); //尝试上锁
10int pthread_mutex_unlock(pthread_mutex_t *mutex); //解锁
2)条件变量(Condition Variable)

1#include
2
3int pthread_cond_destroy(pthread_cond_t *cond); //销毁
4int pthread_cond_init(pthread_cond_t *restrict cond,
5 const pthread_condattr_t *restrict attr); //初始化
6pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
7
8int pthread_cond_timedwait(pthread_cond_t *restrict cond,
9 pthread_mutex_t *restrict mutex,
10 const struct timespec *restrict abstime);
11int pthread_cond_wait(pthread_cond_t *restrict cond,
12 pthread_mutex_t *restrict mutex);
13int pthread_cond_broadcast(pthread_cond_t *cond);
14int pthread_cond_signal(pthread_cond_t *cond);
3)信号量(Semaphore)

1#include
2
3int sem_init(sem_t *sem, int pshared, unsigned int value);
4int sem_wait(sem_t *sem);
5int sem_trywait(sem_t *sem);
6int sem_post(sem_t * sem);
7int sem_destroy(sem_t * sem);
调用sem_wait()可以获得资源,使semaphore的值减1,如果调用sem_wait()时semaphore的值已经是0,则挂起等待。如果不希望挂起等待,可以调用sem_trywait()。调用sem_post()可以释放资源,使semaphore的值加1,同时唤醒挂起等待的线程。

26、哈希表的桶个数为什么是质数,合数有何不妥?

质数比合数更容易避免冲撞,也就是说使用质数时,哈希效果更好,原始数据经哈希后分布更均匀。

其余时间聊项目,聊拼多多使用的技术。比较重要的一点是大家的项目经历,项目经历并不仅仅是摆在那里证明自己做过项目,要首先对项目有全局上的了解,再对自己负责的部分了如指掌,最好用到了什么组件和技术都去了解他们的原理,那么在面试的时候就有很多很多聊的了。

(二)腾讯二面面经

1、redis的主从复制怎么做的

Redis旧版复制功能只有同步和命令传播。新版复制功能加入了部分同步的功能。

1)同步:

2)命令传播:

当主服务器会将自己执行的写命令,也即是造成主从服务器不一致的那条写命令,发送给从服务器执行,当从服务器执行了相同的写命令之后,主从服务器将再次回到一致状态。

3)部分同步:(断线后重复制)

复制偏移量:通过对比主从服务器的复制偏移量,程序可以很容易地知道主从服务器是否处于一致状态。

复制积压缓冲区:主服务保存最近的写命令到复制积压缓冲区,是一个先进先出队列

服务器运行ID:从服务器记录上次同步的主服务器的Id。

3、如何把一个文件快速下发到100w个服务器

gossip算法?Gossip有众多的别名“闲话算法”、“疫情传播算法”、“病毒感染算法”、“谣言传播算法”。

4、如何判断一个图是否连同?

DFS、BFS、并查集

5、ubuntu开机的时候系统做了什么

1)加载BIOS

BIOS程序首先检查,计算机硬件能否满足运行的基本条件,这叫做”硬件自检”。硬件自检完成后,BIOS把控制权转交给下一阶段的启动程序。

2)读取MBR

计算机读取该设备的第一个扇区,也就是读取最前面的512个字节。如果这512个字节的最后两个字节是0x55和0xAA,表明这个设备可以用于启动;如果不是,表明设备不能用于启动,控制权于是被转交给”启动顺序”中的下一个设备。

3)Bootloader

在这种情况下,计算机读取”主引导记录”前面446字节的机器码之后,不再把控制权转交给某一个分区,而是运行事先安装的”启动管理器”(boot loader),由用户选择启动哪一个操作系统。

Boot Loader 就是在操作系统内核运行之前运行的一段小程序。通过这段小程序,我们可以初始化硬件设备、建立内存空间的映射图,从而将系统的软硬件环境带到一个合适的状态,以便为最终调用操作系统内核做好一切准备。

Boot Loader有若干种,其中Grub、Lilo和spfdisk是常见的Loader。Linux环境中,目前最流行的启动管理器是Grub。

4)加载内核

内核的加载,内核加载后,接开始操作系统初始化,根据进程的优先级启动进程。

MySQL数据库如何插入或修改一个字段
1 --增:向数据表添加字段
命令::alter table 数据表名 add 字段名 数据类型【约束】 after 字段名-是放在谁的后面;

2 —改:分为两种情况:
一是只修改数据表类型或约束
命令::alter table 数据表名 modify 字段名 新的数据类型【新的约束】;

二是修改整个字段
命令::alter table 数据表名 change 旧的字段名 新的字段名 新的数据类型【约束】;

3 —查;
命令:desc 数据表名;

4–删:把字段从数据表中移除
命令 alter table drop 字段名;

14.MySQL的两个主流引擎,并介绍它们的区别。
答:主流的引擎有两个,分别是 InnoDB和 MyISAM。其中 InnoDB支持事务,支持外键约束,它还支持行锁(比如select…for update语句,会触发行锁,但是锁定的是索引不是记录)。 MyISAM不支持事务,不支持外键,它是数据库默认的引擎。 InnoDB保存表的行数,如果看这个表有多少行的时候, InnoDB扫描整张表, MyISAM则是直接读取保存的行数即可。删除表的时候 InnoDB是一行一行的删,而 MyISAM则是重建表。 InnoDB适合频繁修改以及安全性要求较高的应用, MyISAM适合查询为主的应用。在我们的项目中使用的是 InnoDB。

socket编程有哪几种方式,具体如何实现的
一.Select模型: 轮询fd_set集合

利用select函数,实现对I/O 的管理。最初设计该模型时,主要面向的是某些使用UNIX操作系统的计算机,它们采用的是Berkeley套接字方案。Select模型已集成到 Winsock 1.1中,它使那些想避免在套接字调用过程中被无辜“锁定”的应用程序,采取一种有序的方式,同时进行对多个套接字的管理。
二.异步选择
应用程序可以在一个套接字上接收以WINDOWS消息为基础的网络事件通知。该模型的实现方法是通过调用WSAAsynSelect函数 自动将套接字设置为非阻塞模式,并向WINDOWS注册一个或多个网络时间,并提供一个通知时使用的窗口句柄。当注册的事件发生时,对应的窗口将收到一个基于消息的通知。

三.事件选择
Winsock 提供了另一个有用的异步I/O模型。和WSAAsyncSelect模型类似的是,它也允许应用程序在一个或多个套接字上,接收以事件为基础的网络事件通知。
基本思想是将每个套接字都和一个WSAEVENT对象对应起来,并且在关联的时候指定需要关注的哪些网络事件。一旦在某个套接字上发生了我们关注的事件(FD_READ和FD_CLOSE),与之相关联的WSAEVENT对象被Signaled。

四.重叠I/O模型

readfile或者writefile的调用马上就会返回,这时候你可以去做你要做的事,系统会自动替你完成readfile或者writefile,在你调用了readfile或者writefile后,你继续做你的事,系统同时也帮你完成readfile或writefile的操作,这就是所谓的重叠。

五.完成端口模型
只有在你的应用程序需要同时管理数百乃至上千个套接字的时候,而且希望随着系统内安装的CPU数量的增多,应用程序的性能也可以线性提升,才应考虑采用“完成端口”模型。

完成端口内部提供了线程池的管理,可以避免反复创建线程的开销,同时可以根据CPU的个数灵活的决定线程个数,而且可以让减少线程调度的次数从而提高性能。

python

Python的内存管理机制和垃圾清理机制
简单来说python的内存管理机制有三种

1)引用计数

2)垃圾回收

3)内存池

接下来我们来详细讲解这三种管理机制

1,引用计数:

引用计数是一种非常高效的内存管理手段,当一个pyhton对象被引用时其引用计数增加1,当其不再被引用时引用计数减1,当引用计数等于0的时候,对象就被删除了。

2,垃圾回收(这是一个很重要知识点):

① 引用计数
引用计数也是一种垃圾回收机制,而且是一种最直观,最简单的垃圾回收技术。
在Python中每一个对象的核心就是一个结构体PyObject,它的内部有一个引用计数 ob_refcnt,当python的某个对象引用计数为0。就说明没有任何引用指向该对象,该对象就成为要被回收的垃圾了。

举个栗子: 当一个对象被创建出来,他的引用计数就会+1,当对象被引用的时候,计数继续增加,当引用它的对象被删除的时候,它的引用计数就会减少。直到变为0,此时垃圾回收机制就会把它回收。但是一旦出现循环引用,我们就得采取新的办法了。

② 标记清除
标记清除用来解决循环引用产生的问题,循环引用只有在容器对象才会产生,比如字典,元祖,列表等。首先为了追踪对象,需要每个容器对象维护两个额外的指针,用来将容器对象组成一个链表,指针分别指向前后两个容器对象,这样可以将对象的循环引用摘除,就可以得出两个对象的有效计数。

③ 分代回收
了解分类回收,首先要了解一下,GC的阈值,所谓阈值就是一个临界点的值。
随着你的程序运行,Python解释器保持对新创建的对象,以及因为引用计数为零而被释放掉的对象的追踪。从理论上说,创建==释放数量应该是这样子。但是如果存在循环引用的话,肯定是创建>释放数量,当创建数与释放数量的差值达到规定的阈值的时候,当当当当~分代回收机制就登场啦。
分代回收思想将对象分为三代(generation 0,1,2)
0代表幼年对象,
1代表青年对象,
2代表老年对象。
根据弱代假说(越年轻的对象越容易死掉,老的对象通常会存活更久。)
新生的对象被放入0代,如果该对象在第0代的一次gc垃圾回收中活了下来,那么它就被放到第1代里面(它就升级了)。如果第1代里面的对象在第1代的一次gc垃圾回收中活了下来,它就被放到第2代里面。

从上一次第0代gc后,如果分配对象的个数减去释放对象的个数大于threshold0,那么就会对第0代中的对象进行gc垃圾回收检查。

从上一次第1代gc后,如果第0代被gc垃圾回收的次数大于threshold1,那么就会对第1代中的对象进行gc垃圾回收检查。

从上一次第2代gc后,如果第1代被gc垃圾回收的次数大于threshold2,那么就会对第2代中的对象进行gc垃圾回收检查。

gc每一代垃圾回收所触发的阈值可以自己设置。

3,内存池

Python的内存机制呈现金字塔形状,-1,-2层主要有操作系统进行操作
第0层是C中的malloc,free等内存分配和释放函数进行操作
第1层和第2层是内存池,有python接口函数,PyMem_Malloc函数实现,当对象小于256k的时由该层直接分配内存
第3层是最上层,也就是我们对python对象的直接操作
Python在运行期间会大量地执行malloc和free的操作,频繁地在用户态和核心态之间进行切换,这将严重影响Python的执行效率。为了加速Python的执行效 率,Python引入了一个内存池机制,用于管理对小块内存的申请和释放。

4,调优手段

1.手动垃圾回收
2.避免循环引用(手动解循环引用和使用弱引用)
3.调高垃圾回收阈值

谈一下单例模式。
单例模式有两种实现模式:

1)懒汉模式: 就是说当你第一次使用时才创建一个唯一的实例对象,从而实现延迟加载的效果。

2)饿汉模式: 就是说不管你将来用不用,程序启动时就创建一个唯一的实例对象。

答:确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,单例模式是一种对象创建型模式。简单的说就是保证只有一个对象,节约内存空间,我们可以通过修改类中的 __new__方法,实现一个简单的单例类。

利用切片操作,实现一个trim()函数,去除字符串首尾的空格,注意不要调用str的strip()方法.
正解1:

def trim(s): while s[:1] == ’ ': s = s[1:] while s[-1:] == ’ ': s = s[:-1] return s
正解2:

def trim(s): if s[:1] == ’ ': s = trim(s[1:]) if s[-1:] == ’ ': s = trim(s[:-1]) return s
容易写错的方法:

def trim(s): while s[0] == ’ ': s = s[1:] while s[-1] == ’ ‘: s = s[:-1] return s
解释:当s=’'时,s[0]和s[-1]会报IndexError: string index out of range,但是s[:1])和s[-1:]不会。

请设计一个decorator,它可作用于任何函数上,并打印该函数的执行时间.

-- coding: utf-8 -- import time, functools def metric(fn): @functools.wraps(fn) def wrapper(*args, **kw): time0 = time.time() ret = fn(*args, **kw) time1 = time.time() print(’%s executed in %s ms’ % (fn.name, time1-time0)) return ret return wrapper

装饰器的实质是什么?或者说为什么装饰器要写2层嵌套函数,里层函数完全就已经实现了装饰的功能为什么不直接用里层函数名作为装饰器名称?

答:装饰器是要把原来的函数装饰成新的函数,并且返回这个函数本身的高阶函数

python下多线程的限制以及多进程中传递参数的方式

python多线程有个全局解释器锁(global interpreter lock),这个锁的意思是任一时间只能有一个线程使用解释器,跟单cpu跑多个程序一个意思,大家都是轮着用的,这叫“并发”,不是“并行”。

多进程间共享数据,可以使用 multiprocessing.Value 和 multiprocessing.Array

python多线程与多进程的区别:

在UNIX平台上,当某个进程终结之后,该进程需要被其父进程调用wait,否则进程成为僵尸进程(Zombie)。所以,有必要对每个Process对象调用join()方法 (实际上等同于wait)。对于多线程来说,由于只有一个进程,所以不存在此必要性。

多进程应该避免共享资源。在多线程中,我们可以比较容易地共享资源,比如使用全局变量或者传递参数。在多进程情况下,由于每个进程有自己独立的内存空间,以上方法并不合适。此时我们可以通过共享内存和Manager的方法来共享资源。但这样做提高了程序的复杂度,并因为同步的需要而降低了程序的效率。

请写出一段Python代码实现删除一个list里面的重复元素

l = [1,1,2,3,4,5,4] >>> list(set(l)) [1, 2, 3, 4, 5] 或者 d = {} for x in mylist: d[x] = 1 mylist = list(d.keys())
利用map()函数,把用户输入的不规范的英文名字,变为首字母大写,其他小写的规范名字。输入:[‘adam’, ‘LISA’, ‘barT’],输出:[‘Adam’, ‘Lisa’, ‘Bart’]:
def normalize(name): return name[0].upper()+name[1:].lower() def normalizeList(inputlist): return list(map(normalize, inputlist))
Python是如何进行内存管理的?
  http://developer.51cto.com/art/201007/213585.htm
  Python引用了一个内存池(memory pool)机制,即Pymalloc机制(malloc:n.分配内存),用于管理对小块内存的申请和释放
内存池(memory pool)的概念:
  当 创建大量消耗小内存的对象时,频繁调用new/malloc会导致大量的内存碎片,致使效率降低。内存池的概念就是预先在内存中申请一定数量的,大小相等 的内存块留作备用,当有新的内存需求时,就先从内存池中分配内存给这个需求,不够了之后再申请新的内存。这样做最显著的优势就是能够减少内存碎片,提升效率。
内存池的实现方式有很多,性能和适用范围也不一样。
python中的内存管理机制——Pymalloc:
  python中的内存管理机制都有两套实现,一套是针对小对象,就是大小小于256bits时,pymalloc会在内存池中申请内存空间;当大于256bits,则会直接执行new/malloc的行为来申请内存空间。
  关于释放内存方面,当一个对象的引用计数变为0时,python就会调用它的析构函数。在析构时,也采用了内存池机制,从内存池来的内存会被归还到内存池中,以避免频繁地释放动作。

解释一下python的and-or语法
http://www.kuqin.com/diveinto_python_document/apihelper_andor.html
与C表达式 bool ? a : b类似,但是bool and a or b,当 a 为假时,不会象C表达式 bool ? a : b 一样工作
应该将 and-or 技巧封装成一个函数:

def choose(bool, a, b): return (bool and [a] or [b])[0]
因为 [a] 是一个非空列表,它永远不会为假。甚至 a 是 0 或 ‘’ 或其它假值,列表[a]为真,因为它有一个元素。

how do I iterate over a sequence in reverse order
for x in reversed(sequence): … # do something with x…
如果不是list, 最通用但是稍慢的解决方案是:

for i in range(len(sequence)-1, -1, -1): x = sequence[i]
Python如何实现单例模式?其他23种设计模式python如何实现?

Python里面如何拷贝一个对象?
http://blog.csdn.net/sharkw/article/details/1934090
标准库中的copy模块提供了两个方法来实现拷贝.一个方法是copy,它返回和参数包含内容一样的对象.
使用deepcopy方法,对象中的属性也被复制

Python里面search()和match()的区别?
match()函数只检测RE是不是在string的开始位置匹配,search()会扫描整个string查找匹配, 也就是说match()只有在0位置匹配成功的话才有返回,如果不是开始位置匹配成功的话,match()就返回none

有两个序列a,b,大小都为n,序列元素的值任意整形数,无序;
要求:通过交换a,b中的元素,使[序列a元素的和]与[序列b元素的和]之间的差最小。

分别计算a,b序列的和;
求a序列和与b序列和的差值的一半,记为half;
在和值大的序列中找出一个与和值小的序列中的元素max的差值最接近half的元素,记为min;
将max与min互换即可。
描述元类的概念。Python有没有接口?元类和Java的接口有什么异同?
python里无接口类型,定义接口类(抽象类)只是一个人为规定,在编程过程自我约束。
元类是类的模板,重在帮助创建类。接口是重在提供思路,后续进行实现。

6.Python里面如何拷贝一个对象?(赋值,浅拷贝,深拷贝的区别)

答:赋值(=),就是创建了对象的一个新的引用,修改其中任意一个变量都会影响到另一个。

浅拷贝:创建一个新的对象,但它包含的是对原始对象中包含项的引用(如果用引用的方式修改其中一个对象,另外一个也会修改改变){1,完全切片方法;2,工厂函数,如list();3,copy模块的copy()函数}

深拷贝:创建一个新的对象,并且递归的复制它所包含的对象(修改其中一个,另外一个不会改变){copy模块的deep.deepcopy()函数}

7.介绍一下except的用法和作用?

答:try…except…except…[else…][finally…]

执行try下的语句,如果引发异常,则执行过程会跳到except语句。对每个except分支顺序尝试执行,如果引发的异常与except中的异常组匹配,执行相应的语句。如果所有的except都不匹配,则异常会传递到下一个调用本代码的最高层try代码中。

try下的语句正常执行,则执行else块代码。如果发生异常,就不会执行

如果存在finally语句,最后总是会执行。

8.Python中pass语句的作用是什么?

答:pass语句不会执行任何操作,一般作为占位符或者创建占位程序,whileFalse:pass

9.介绍一下Python下range()函数的用法?

答:列出一组数据,经常用在for in range()循环中

10.如何用Python来进行查询和替换一个文本字符串?

答:可以使用re模块中的sub()函数或者subn()函数来进行查询和替换,

格式:sub(replacement, string[,count=0])(replacement是被替换成的文本,string是需要被替换的文本,count是一个可选参数,指最大被替换的数量)

import re

p=re.compile(‘blue|white|red’)

print(p.sub(‘colour’,'blue socks and red shoes’))

colour socks and colourshoes

print(p.sub(‘colour’,'blue socks and red shoes’,count=1))

colour socks and redshoes

subn()方法执行的效果跟sub()一样,不过它会返回一个二维数组,包括替换后的新的字符串和总共替换的数量

11.Python里面match()和search()的区别?

答:re模块中match(pattern,string[,flags]),检查string的开头是否与pattern匹配。

re模块中research(pattern,string[,flags]),在string搜索pattern的第一个匹配值。

print(re.match(‘super’, ‘superstition’).span())

(0, 5)

print(re.match(‘super’, ‘insuperable’))

None

print(re.search(‘super’, ‘superstition’).span())

(0, 5)

print(re.search(‘super’, ‘insuperable’).span())

(2, 7)

12.用Python匹配HTML tag的时候,和有什么区别?

答:术语叫贪婪匹配( )和非贪婪匹配( )

例如:

test

:

test

:

13.Python里面如何生成随机数?

答:random模块

随机整数:random.randint(a,b):返回随机整数x,a

random.randrange(start,stop,[,step]):返回一个范围在(start,stop,step)之间的随机整数,不包括结束值。

随机实数:random.random( ):返回0到1之间的浮点数

random.uniform(a,b):返回指定范围内的浮点数。

14.有没有一个工具可以帮助查找python的bug和进行静态的代码分析?

答:PyChecker是一个python代码的静态分析工具,它可以帮助查找python代码的bug, 会对代码的复杂度和格式提出警告

Pylint是另外一个工具可以进行codingstandard检查

15.如何在一个function里面设置一个全局的变量?

答:解决方法是在function的开始插入一个global声明:

def f()

global x

16.单引号,双引号,三引号的区别

答:单引号和双引号是等效的,如果要换行,需要符号(),三引号则可以直接换行,并且可以包含注释

如果要表示Let’s go 这个字符串

单引号:s4 = ‘Let’s go’

双引号:s5 = “Let’s go”

s6 = ‘I realy like“python”!’

这就是单引号和双引号都可以表示字符串的原因了

python2和python3区别?列举5个

1、Python3 使用 print 必须要以小括号包裹打印内容,比如 print(‘hi’)

Python2 既可以使用带小括号的方式,也可以使用一个空格来分隔打印内容,比 如 print ‘hi’

2、python2 range(1,10)返回列表,python3中返回迭代器,节约内存

3、python2中使用ascii编码,python中使用utf-8编码

4、python2中unicode表示字符串序列,str表示字节序列

python3中str表示字符串序列,byte表示字节序列

5、python2中为正常显示中文,引入coding声明,python3中不需要

6、python2中是raw_input()函数,python3中是input()函数

列出python中可变数据类型和不可变数据类型,并简述原理

不可变数据类型:数值型、字符串型string和元组tuple

不允许变量的值发生变化,如果改变了变量的值,相当于是新建了一个对象,而对于相同的值的对象,在内存中则只有一个对象(一个地址),如下图用id()方法可以打印对象的id

可变数据类型:列表list和字典dict;

允许变量的值发生变化,即如果对变量进行append、+=等这种操作后,只是改变了变量的值,而不会新建一个对象,变量引用的对象的地址也不会变化,不过对于相同的值的不同对象,在内存中则会存在不同的对象,即每个对象都有自己的地址,相当于内存中对于同值的对象保存了多份,这里不存在引用计数,是实实在在的对象。

s = “ajldjlajfdljfddd”,去重并从小到大排序输出"adfjl"

set去重,去重转成list,利用sort方法排序,reeverse=False是从小到大排

list是不 变数据类型,s.sort时候没有返回值,所以注释的代码写法不正确

字典根据键从小到大排序

dict={“name”:“zs”,“age”:18,“city”:“深圳”,“tel”:“1362626627”}

filter方法求出列表所有奇数并构造新列表,a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

filter() 函数用于过滤序列,过滤掉不符合条件的元素,返回由符合条件元素组成的新列表。该接收两个参数,第一个为函数,第二个为序列,序列的每个元素作为参数传递给函数进行判,然后返回 True 或 False,最后将返回 True 的元素放到新列表

请尽可能列举python列表的成员方法,并给出一下列表操作的答案:

(1) a=[1, 2, 3, 4, 5], a[::2]=?, a[-2:] = ?

(2) 一行代码实现对列表a中的偶数位置的元素进行加3后求和?

(3) 将列表a的元素顺序打乱,再对a进行排序得到列表b,然后把a和b按元素顺序构造一个字典d。

用python实现统计一篇英文文章内每个单词的出现频率,并返回出现频率最高的前10个单词及其出现次数,并解答以下问题?(标点符号可忽略)

(1) 创建文件对象f后,解释f的readlines和xreadlines方法的区别?

(2) 追加需求:引号内元素需要算作一个单词,如何实现?

简述python GIL的概念, 以及它对python多线程的影响?编写一个多线程抓取网页的程序,并阐明多线程抓取程序是否可比单线程性能有提升,并解释原因。

Python里面如何拷贝一个对象?(赋值,浅拷贝,深拷贝的区别)

答:赋值(=),就是创建了对象的一个新的引用,修改其中任意一个变量都会影响到另一个。

浅拷贝:创建一个新的对象,但它包含的是对原始对象中包含项的引用(如果用引用的方式修改其中一个对象,另外一个也会修改改变){1,完全切片方法;2,工厂函数,如list();3,copy模块的copy()函数}

深拷贝:创建一个新的对象,并且递归的复制它所包含的对象(修改其中一个,另外一个不会改变){copy模块的deep.deepcopy()函数}

介绍一下except的用法和作用?

答:try…except…except…[else…][finally…]

执行try下的语句,如果引发异常,则执行过程会跳到except语句。对每个except分支顺序尝试执行,如果引发的异常与except中的异常组匹配,执行相应的语句。如果所有的except都不匹配,则异常会传递到下一个调用本代码的最高层try代码中。

try下的语句正常执行,则执行else块代码。如果发生异常,就不会执行,如果存在finally语句,最后总是会执行。

介绍一下Python下range()函数的用法?

答:列出一组数据,经常用在for in range()循环中

如何用Python来进行查询和替换一个文本字符串?

答:可以使用re模块中的sub()函数或者subn()函数来进行查询和替换,

格式:sub(replacement, string[,count=0])(replacement是被替换成的文本,string是需要被替换的文本,count是一个可选参数,指最大被替换的数量)

import re

p=re.compile(‘blue|white|red’)

print(p.sub(‘colour’,'blue socks and red shoes’))

colour socks and colourshoes

print(p.sub(‘colour’,'blue socks and red shoes’,count=1))

colour socks and redshoes

subn()方法执行的效果跟sub()一样,不过它会返回一个二维数组,包括替换后的新的字符串和总共替换的数量。

设计模式:

  1.   单例模式:
    

实现方式:

a) 将被实现的类的构造方法设计成private的。

b) 添加此类引用的静态成员变量,并为其实例化。

c) 在被实现的类中提供公共的CreateInstance函数,返回实例化的此类,就是b中的静态成员变量。

应用场景:

优点:
1.在单例模式中,活动的单例只有一个实例,对单例类的所有实例化得到的都是相同的一个实例。这样就 防止其它对象对自己的实例化,确保所有的对象都访问一个实例
2.单例模式具有一定的伸缩性,类自己来控制实例化进程,类就在改变实例化进程上有相应的伸缩性。
3.提供了对唯一实例的受控访问。
4.由于在系统内存中只存在一个对象,因此可以 节约系统资源,当 需要频繁创建和销毁的对象时单例模式无疑可以提高系统的性能。
5.允许可变数目的实例。
6.避免对共享资源的多重占用。
缺点:
1.不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态。
2.由于单利模式中没有抽象层,因此单例类的扩展有很大的困难。
3.单例类的职责过重,在一定程度上违背了“单一职责原则”。
4.滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。
使用注意事项:
1.使用时不能用反射模式创建单例,否则会实例化一个新的对象
2.使用懒单例模式时注意线程安全问题
3.单例模式和懒单例模式构造方法都是私有的,因而是不能被继承的,有些单例模式可以被继承(如登记式模式)
适用场景:
单例模式只允许创建一个对象,因此节省内存,加快对象访问速度,因此对象需要被公用的场合适合使用,如多个模块使用同一个数据源连接对象等等。如:
1.需要频繁实例化然后销毁的对象。
2.创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
3.有状态的工具类对象。
4.频繁访问数据库或文件的对象。
以下都是单例模式的经典使用场景:
1.资源共享的情况下,避免由于资源操作时导致的性能或损耗等。如上述中的日志文件,应用配置。
2.控制资源的情况下,方便资源之间的互相通信。如线程池等。
应用场景举例:
1.外部资源:每台计算机有若干个打印机,但只能有一个PrinterSpooler,以避免两个打印作业同时输出到打印机。内部资源:大多数软件都有一个(或多个)属性文件存放系统配置,这样的系统应该有一个对象管理这些属性文件
2. Windows的TaskManager(任务管理器)就是很典型的单例模式(这个很熟悉吧),想想看,是不是呢,你能打开两个windows task manager吗? 不信你自己试试看哦~
3. windows的Recycle Bin(回收站)也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例。
4. 网站的计数器,一般也是采用单例模式实现,否则难以同步。
5. 应用程序的日志应用,一般都何用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。
6. Web应用的配置对象的读取,一般也应用单例模式,这个是由于配置文件是共享的资源。
7. 数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。数据库软件系统中使用数据库连接池,主要是节省打开或者关闭数据库连接所引起的效率损耗,这种效率上的损耗还是非常昂贵的,因为何用单例模式来维护,就可以大大降低这种损耗。
8. 多线程的线程池的设计一般也是采用单例模式,这是由于线程池要方便对池中的线程进行控制。
9. 操作系统的文件系统,也是大的单例模式实现的具体例子,一个操作系统只能有一个文件系统。
10. HttpApplication 也是单位例的典型应用。熟悉ASP.Net(IIS)的整个请求生命周期的人应该知道HttpApplication也是单例模式,所有的HttpModule都共享一个HttpApplication实例.

  1.   策略模式:
    

实现方式:

a) 提供公共接口或抽象类,定义需要使用的策略方法。(策略抽象类)

b) 多个实现的策略抽象类的实现类。(策略实现类)

c) 环境类,对多个实现类的封装,提供接口类型的成员量,可以在客户端中切换。

d) 客户端 调用环境类 进行不同策略的切换。

注:Jdk中的TreeSet和 TreeMap的排序功能就是使用了策略模式。

策略模式的优点
  (1)策略模式提供了管理相关的算法族的办法。策略类的等级结构定义了一个算法或行为族。恰当使用继承可以把公共的代码移到父类里面,从而避免代码重复。

(2)使用策略模式可以避免使用多重条件(if-else)语句。多重条件语句不易维护,它把采取哪一种算法或采取哪一种行为的逻辑与算法或行为的逻辑混合在一起,统统列在一个多重条件语句里面,比使用继承的办法还要原始和落后。

策略模式的缺点
  (1)客户端必须知道所有的策略类,并自行决定使用哪一个策略类。这就意味着客户端必须理解这些算法的区别,以便适时选择恰当的算法类。换言之,策略模式只适用于客户端知道算法或行为的情况。

(2)由于策略模式把每个具体的策略实现都单独封装成为类,如果备选的策略很多的话,那么对象的数目就会很可观。

  1.   代理模式:
    

一)静态代理

实现方式:

a) 为真实类和代理类提供的公共接口或抽象类。(租房)

b) 真实类,具体实现逻辑,实现或继承a。(房主向外租房)

c) 代理类,实现或继承a,有对b的引用,调用真实类的具体实现。(中介)

d) 客户端,调用代理类实现对真实类的调用。(租客租房)

二)动态代理

实现方式:

a) 公共的接口(必须是接口,因为Proxy类的newproxyinstance方法的第二参数必须是个接口类型的Class)

b) 多个真实类,具体实现的业务逻辑。

c) 代理类,实现InvocationHandler接口,提供Object成员变量,和Set方法,便于客户端切换。

d) 客户端,获得代理类的实例,为object实例赋值,调用Proxy.newproxyinstance方法在程序运行时生成继承公共接口的实例,调用相应方法,此时方法的执行由代理类实现的Invoke方法接管。

jdk动态代理使用的局限性:
通过反射类Proxy和InvocationHandler回调接口实现的jdk动态代理,要求委托类必须实现一个接口,但事实上并不是所有类都有接口,对于没有实现接口的类,便无法使用该方方式实现动态代理。

  1.   观察者模式:
    

观察者模式是对象的行为模式,又叫发布-订阅(Publish/Subscribe)模式、模型-视图(Model/View)模式、源-监听器(Source/Listener)模式或从属者(Dependents)模式。

实现方式:

a) 角色抽象类(提供对观察者的添加,删除和通知功能)。

b) 角色具体类,实现a,维护一个c的集合(对角色抽象类的实现)。

c) 观察者抽象类(被角色通知后实现的方法)。

d) 观察者实现类,实现c(多个)。

注:JDK提供了对观察者模式的支持,使用Observable类和Observer接口

两种模型(推模型和拉模型):

■  推模型是假定主题对象知道观察者需要的数据;而拉模型是主题对象不知道观察者具体需要什么数据,没有办法的情况下,干脆把自身传递给观察者,让观察者自己去按需要取值。

■  推模型可能会使得观察者对象难以复用,因为观察者的update()方法是按需要定义的参数,可能无法兼顾没有考虑到的使用情况。这就意味着出现新情况的时候,就可能提供新的update()方法,或者是干脆重新实现观察者;而拉模型就不会造成这样的情况,因为拉模型下,update()方法的参数是主题对象本身,这基本上是主题对象能传递的最大数据集合了,基本上可以适应各种情况的需要。

  1.   装饰模式:
    

实现方式:

a) 抽象的被装饰角色 (所有的角色都要直接或间接的实现本角色)

b) 具体的被装饰角色,实现或继承a (被功能扩展的角色)

c) 装饰角色,实现或继承a (本类有对a的引用,所有的具体装饰角色都需要继承这个角色)

d) 多个具体修饰角色 ,继承c(对被装饰角色的功能扩展,可以任意搭配使用)

意图:

动态地给一个对象添加一些额外的职责。就增加功能来说,Decorator模式相比生成子类更为灵活。该模式以对客 户端透明的方式扩展对象的功能。

适用环境:

(1)在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。

(2)处理那些可以撤消的职责。

(3)当不能采用生成子类的方法进行扩充时。一种情况是,可能有大量独立的扩展,为支持每一种组合将产生大量的 子类,使得子类数目呈爆炸性增长。另一种情况可能是因为类定义被隐藏,或类定义不能用于生成子类。

  1.  适配器模式:
    

适配器模式把一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作。

  1.   类适配器(子类继承方式)
    

实现方式:

a) 目标抽象角色(定义客户要用的接口)

b) 适配器(实现a继承c,作为一个转换器被客户调用)

c) 待适配器(真正需要被调用的)

d) 客户端(借用a的实例调用c的方法)

  1. 对象适配器(对象的组合方式)
    

实现方式:

a) 目标抽象角色(定义客户要用的接口)

b) 适配器(实现a,维护一个c的引用,作为一个转换器被d调用)

c) 待适配器(真正需要被调用的)

d) 客户端(此类,借用a类的实例调用c类的方法,类似静态代理,但是解决的问题不同)

  1. 缺省的方式
    

实现方式:

a) 抽象接口

b) 实现a的适配器类(空实现)

c) 客户端,继承b,调用b中的方法,不必直接实现a(直接实现a需要实现a中的所有的方法)

适配器模式的优点:

  1. 更好的复用性
    

系统需要使用现有的类,而此类的接口不符合系统的需要。那么通过适配器模式就可以让这些功能得到更好的复用。

  1. 更好的扩展性
    

在实现适配器功能的时候,可以调用自己开发的功能,从而自然地扩展系统的功能。

适配器模式的缺点:
  过多的使用适配器,会让系统非常零乱,不易整体进行把握。比如,明明看到调用的是A接口,其实内部被适配成了B接口的实现,一个系统如果太多出现这种情况,无异于一场灾难。因此如果不是很有必要,可以不使用适配器,而是直接对系统进行重构。

  1. 命令模式

将一个请求封装为一个对象,从而可用不同的请求对客户进行参数化;对请求排队或记录日志,以及支持可撤销的操作

将“发出请求的对象”和”接收与执行这些请求的对象”分隔开来。

实现方式:

a) 抽象的命令角色 , 如:菜单(规定可以点哪些菜)

b) 具体的命令角色(实现a 维护一个对c的引用),如:订单(已点的菜)

c) 接收者(具体执行命令的角色),实际操作时,很常见使用"聪明"命令对象,也就是直接实现了请求,而不是将工作委托给c (弊端?) 如:厨师接收订单后做菜

d) 调用者(维护一个对a的引用),如:服务员负责点菜并把订单推给厨师

e) 客户端 调用d发出命令进而执行c的方法,如:顾客点餐

效果:
1)、command模式将调用操作的对象和实现该操作的对象解耦
2)、可以将多个命令装配成一个复合命令,复合命令是Composite模式的一个实例
3)、增加新的command很容易,无需改变已有的类
适用性:
1)、抽象出待执行的动作以参数化某对象
2)、在不同的时刻指定、排列和执行请求。如请求队列
3)、支持取消操作
4)、支持修改日志
5)、用构建在原语操作上的高层操作构造一个系统。支持事物

  1. 组合模式

将对象组合成树形结构以表示“部分整体”的层次结构。组合模式使得用户对单个对象和复杂对象的使用具有一致性。

实现方式:

a) 抽象的构件接口 (规范执行的方法),b及c都需实现此接口,如:Junit中的Test接口

b) 叶部件(实现a,最小的执行单位),如:Junit中我们所编写的测试用例

c) 组合类(实现a并维护一个a的集合[多个b的组合]),如:Junit中的 TestSuite

d) 客户端 可以随意的将b和c进行组合,进行调用

什么情况下使用组合模式:

当发现需求中是体现部分与整体层次结构时,以及你希望用户可以忽略组合对象与单个对象的不同,统一地使用组合结构中的所有对象时,就应该考虑组合模式了。

  1. 简单工厂模式

就是建立一个工厂类,对实现了同一接口的一些类进行实例的创建。简单工厂模式的实质是由一个工厂类根据传入的参数,动态决定应该创建哪一个产品类(这些产品类继承自一个父类或接口)的实例。

实现方式:

a) 抽象产品类(也可以是接口)

b) 多个具体的产品类

c) 工厂类(包括创建a的实例的方法)

优点:

工厂类是整个模式的关键.包含了必要的逻辑判断,根据外界给定的信息,决定究竟应该创建哪个具体类的对象.通过使用工厂类,外界可以从直接创建具体产品对象的尴尬局面摆脱出来,仅仅需要负责“消费”对象就可以了。而不必管这些对象究竟如何创建及如何组织的.明确了各自的职责和权利,有利于整个软件体系结构的优化。

缺点:

由于工厂类集中了所有实例的创建逻辑,违反了高内聚责任分配原则,将全部创建逻辑集中到了一个工厂类中;它所能创建的类只能是事先考虑到的,如果需要添加新的类,则就需要改变工厂类了。当系统中的具体产品类不断增多时候,可能会出现要求工厂类根据不同条件创建不同实例的需求.这种对条件的判断和对具体产品类型的判断交错在一起,很难避免模块功能的蔓延,对系统的维护和扩展非常不利;

  1. 模板方法模式

实现方式:

a) 父类模板类(规定要执行的方法和顺序,只关心方法的定义及顺序,不关心方法实现)

b) 子类实现类(实现a规定要执行的方法,只关心方法实现,不关心调用顺序)

优点:

    1)封装不变部分,扩展可变部分:把认为不变部分的算法封装到父类实现,可变部分则可以通过继承来实现,很容易扩展。

    2)提取公共部分代码,便于维护。

   3)行为由父类控制,由子类实现。

缺点:

    模板方法模式颠倒了我们平常的设计习惯:抽象类负责声明最抽象、最一般的事物属性和方法,实现类实现具体的事物属性和方法。在复杂的项目中可能会带来代码阅读的难度。

gcc 和 g++的区别

简单来说,gcc与g++都是GNU(组织)的一个编译器。需要注意以下几点:

gcc与g++都可以编译c代码与c++代码。但是:后缀为.c的,gcc把它当做C程序,而g++当做是C++程序;后缀为.cpp的,两者都会认为是C++程序。

编译阶段,g++会调用gcc,对于c++代码,两者是等价的,但是因为gcc命令不能自动和C++程序使用的库联接,所以通常用g++来完成链接。

编译可以用gcc/g++,而链接可以用g++或者gcc -lstdc++。因为gcc命令不能自动和C++程序使用的库联接(当然可以选择手动链接,使用命令如下),所以通常使用g++来完成联接。但在编译阶段,g++会自动调用gcc,二者等价。

gcc main.cpp -lstdc++
gcc编译的四个步骤, 以最简单的hello.c为例子

预处理-E/.i、编译-S/.s,汇编-c/.o,连接/可执行文件 gcc -E hello.c -o hello.i gcc -S hello.i -o hello.s gcc -c hello.s -o hello.o gcc hello.o -o hello

一步到位:gcc hello.c 这条命令隐含执行了 (1)预处理 (2)编译 (3)汇编 (4)链接 这里未指定输出文件,默认输出为a.out gcc编译C源码有四个步骤: 预处理 ----> 编译 ----> 汇编 ----> 链接 现在我们就用gcc的命令选项来逐个剖析gcc过程。 1)预处理(Pre-processing) 在该阶段,编译器将C源代码中的包含的头文件如stdio.h添加进来 参数:”-E” 用法:gcc -E hello.c -o hello.i 作用:将hello.c预处理输出hello.i文件。 2)编译(Compiling) 第二步进行的是编译阶段,在这个阶段中,gcc首先要检查代码的规范性、是否有语法错误等,以确定代码的实际要做的工作,在检查无误后,gcc把代码翻译成汇编语言。 参数:”-S” 用法:gcc –S hello.i –o hello.s 作用:将预处理输出文件hello.i汇编成hello.s文件。 3)汇编(Assembling) 汇编阶段是把编译阶段生成的”.s”文件转成二进制目标代码“.o”文件 参数:“-c” 用法:gcc –c hello.s –o hello.o 作用:将汇编输出文件hello.s编译输出hello.o文件。 4)链接(Link) 在成功编译之后,就进入了链接阶段。 用法:gcc hello.o –o hello 作用:将编译输出文件hello.o链接成最终可执行文件hello。 运行该可执行文件,出现正确的结果如下。 >>> ./hello Hello World!

C++11包含大量的新特性:包含lambda表达式,类型推导keyword : auto、decltype,和模板的大量改进。

decltype实际上有点像auto的反函数,auto能够让你声明一个变量。而decltype则能够从一个变量或表达式中得到类型

nullptr是为了解决原来C++中NULL的二义性问题而引进的一种新的类型,由于NULL实际上代表的是0,

简化的for循环,能够用于遍历数组、容器、string以及由begin和end函数定义的序列(即有Iterator),for (auto p : m)

lambda表达式,能够用于创建并定义匿名的函数对象,以简化编程工作。Lambda的语法例如以下: [函数对象參数](操作符重载函数參数)->返回值类型{函数体}

vector iv{5, 4, 3, 2, 1};
int a = 2, b = 1;

for_each(iv.begin(), iv.end(), [b](int &x){cout<<(x + b)<

for_each(iv.begin(), iv.end(), [=](int &x){x *= (a + b);}); // (2)

for_each(iv.begin(), iv.end(), [=](int &x)->int{return x * (a + b);});// (3)

[]内的參数指的是Lambda表达式能够取得的全局变量。(1)函数中的b就是指函数能够得到在Lambda表达式外的全局变量,假设在[]中传入=的话,即是能够取得全部的外部变量,如(2)和(3)Lambda表达式

()内的參数是每次调用函数时传入的參数。

->后加上的是Lambda表达式返回值的类型。如(3)中返回了一个int类型的变量

变长參数的模板,C++11中引入了变长參数模板,所以发明了新的数据类型:tuple,tuple是一个N元组。能够传入1个, 2个甚至多个不同类型的数据

auto t1 = make_tuple(1, 2.0, “C++ 11”);
auto t2 = make_tuple(1, 2.0, “C++ 11”, {1, 0, 2});
避免了从前的pair中嵌套pair的丑陋做法。使得代码更加整洁

更加优雅的初始化方法,在引入C++11之前。仅仅有数组能使用初始化列表,其它容器想要使用初始化列表,仅仅能用下面方法:

int arr[3] = {1, 2, 3}
vector v(arr, arr + 3);
在C++11中,我们能够使用下面语法来进行替换:

int arr[3]{1, 2, 3};
vector iv{1, 2, 3};
map{ {1, “a”}, {2, “b”}};
string str{“Hello World”};
什么是智能指针?智能指针的原理

将基本类型指针封装为类对象指针(这个类肯定是个模板,以适应不同基本类型的需求),并在析构函数里编写delete语句删除指针指向的内存空间。

智能指针是一个类,这个类的构造函数中传入一个普通指针,析构函数中释放传入的指针。智能指针的类都是栈上的对象,所以当函数(或程序)结束时会自动被释放,

智能指针就是一种栈上创建的对象,函数退出时会调用其析构函数,这个析构函数里面往往就是一堆计数之类的条件判断,如果达到某个条件,就把真正指针指向的空间给释放了。

注意事项:

不能将指针直接赋值给一个智能指针,一个是类,一个是指针。

常用的智能指针

智能指针在C++11版本之后提供,包含在头文件中,shared_ptr、unique_ptr、weak_ptr

1)std::auto_ptr,有很多问题。 不支持复制(拷贝构造函数)和赋值(operator =),但复制或赋值的时候不会提示出错。所以可能会造成程序崩溃,比如

auto_ptr p1(new string (“auto”) ; //#1
auto_ptr p2; //#2
p2 = p1; //#3
在语句#3中,p2接管string对象的所有权后,p1的所有权将被剥夺。前面说过,这是好事,可防止p1和p2的析构函数试图刪同—个对象; 但如果程序随后试图使用p1,这将是件坏事,因为p1不再指向有效的数据。如果再访问p1指向的内容则会导致程序崩溃。

auto_ptr是C++98提供的解决方案,C+11已将将其摒弃,摒弃auto_ptr的原因,一句话总结就是:避免潜在的内存崩溃问题。

  1. C++11引入的unique_ptr, 也不支持复制和赋值,但比auto_ptr好,直接赋值会编译出错。实在想赋值的话,需要使用:std::move。例如:

std::unique_ptr p1(new int(5)) // #4
std::unique_ptr p2 = p1; // 编译会出错 //#5
std::unique_ptr p3 = std::move(p1); // 转移所有权, 现在那块内存归p3所有, p1成为无效的指针. //#6
编译器认为语句#5非法,因此,unique_ptr比auto_ptr更安全。

但unique_ptr还有更聪明的地方。 有时候,会将一个智能指针赋给另一个并不会留下危险的悬挂指针。当程序试图将一个 unique_ptr 赋值给另一个时,如果源 unique_ptr 是个临时右值,编译器允许这么做;如果源 unique_ptr 将存在一段时间,编译器将禁止这么做

unique_ptr pu1(new string (“hello world”));
unique_ptr pu2;
pu2 = pu1; // #1 not allowed
unique_ptr pu3;
pu3 = unique_ptr(new string (“You”)); // #2 allowed
其中#1留下悬挂的unique_ptr(pu1),这可能导致危害。而#2不会留下悬挂的unique_ptr,因为它调用 unique_ptr 的构造函数,该构造函数创建的临时对象在其所有权让给 pu3 后就会被销毁。这种随情况而已的行为表明,unique_ptr 优于允许两种赋值的auto_ptr 。

  1. C++11或boost的shared_ptr,基于引用计数的智能指针。可随意赋值,直到内存的引用计数为0的时候这个内存会被释放。

4)C++11或boost的weak_ptr,弱引用。 引用计数有一个问题就是互相引用形成环,这样两个指针指向的内存都无法释放。需要手动打破循环引用或使用weak_ptr。顾名思义,weak_ptr是一个弱引用,只引用,不计数。如果一块内存被shared_ptr和weak_ptr同时引用,当所有shared_ptr析构了之后,不管还有没有weak_ptr引用该内存,内存也会被释放。所以weak_ptr不保证它指向的内存一定是有效的,在使用之前需要检查weak_ptr是否为空指针。

智能指针的作用

C++程序设计中使用堆内存是非常频繁的操作,堆内存的申请和释放都由程序员自己管理。程序员自己管理堆内存可以提高了程序的效率,但是整体来说堆内存的管理是麻烦的,C++11中引入了智能指针的概念,方便管理堆内存。使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,野指针,程序发生异常时内存泄露等问题等,使用智能指针能更好的管理堆内存。

1、C和C++的区别

1)C是面向过程的语言,是一个结构化的语言,考虑如何通过一个过程对输入进行处理得到输出;C++是面向对象的语言,主要特征是“封装、继承和多态”。封装隐藏了实现细节,使得代码模块化;派生类可以继承父类的数据和方法,扩展了已经存在的模块,实现了代码重用;多态则是“一个接口,多种实现”,通过派生类重写父类的虚函数,实现了接口的重用。

2)C和C++动态管理内存的方法不一样,C是使用malloc/free,而C++除此之外还有new/delete关键字。

3)C++支持函数重载,C不支持函数重载

4)C++中有引用,C中不存在引用的概念

2、C++中指针和引用的区别

1)指针是一个新的变量,存储了另一个变量的地址,我们可以通过访问这个地址来修改另一个变量;

引用只是一个别名,还是变量本身,对引用的任何操作就是对变量本身进行操作,以达到修改变量的目的

2)引用只有一级,而指针可以有多级

3)指针传参的时候,还是值传递,指针本身的值不可以修改,需要通过解引用才能对指向的对象进行操作

引用传参的时候,传进来的就是变量本身,因此变量可以被修改

3、结构体struct和共同体union(联合)的区别

结构体:将不同类型的数据组合成一个整体,是自定义类型

共同体:不同类型的几个变量共同占用一段内存

1)结构体中的每个成员都有自己独立的地址,它们是同时存在的;

共同体中的所有成员占用同一段内存,它们不能同时存在;

2)sizeof(struct)是内存对齐后所有成员长度的总和,sizeof(union)是内存对齐后最长数据成员的长度、

结构体为什么要内存对齐呢?

1.平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常

2.硬件原因:经过内存对齐之后,CPU的内存访问速度大大提升。

4、#define和const的区别

1)#define定义的常量没有类型,所给出的是一个立即数;const定义的常量有类型名字,存放在静态区域

2)处理阶段不同,#define定义的宏变量在预处理时进行替换,可能有多个拷贝,const所定义的变量在编译时确定其值,只有一个拷贝。

3)#define定义的常量是不可以用指针去指向,const定义的常量可以用指针去指向该常量的地址

4)#define可以定义简单的函数,const不可以定义函数

5、重载overload,覆盖(重写)override,隐藏(重定义)overwrite,这三者之间的区别

1)overload,将语义相近的几个函数用同一个名字表示,但是参数列表(参数的类型,个数,顺序不同)不同,这就是函数重载,返回值类型可以不同

特征:相同范围(同一个类中)、函数名字相同、参数不同、virtual关键字可有可无

2)override,派生类覆盖基类的虚函数,实现接口的重用,返回值类型必须相同

特征:不同范围(基类和派生类)、函数名字相同、参数相同、基类中必须有virtual关键字(必须是虚函数)

3)overwrite,派生类屏蔽了其同名的基类函数,返回值类型可以不同

特征:不同范围(基类和派生类)、函数名字相同、参数不同或者参数相同且无virtual关键字

6、new、delete、malloc、free之间的关系

new/delete,malloc/free都是动态分配内存的方式

1)malloc对开辟的空间大小严格指定,而new只需要对象名

2)new为对象分配空间时,调用对象的构造函数,delete调用对象的析构函数

既然有了malloc/free,C++中为什么还需要new/delete呢?

运算符是语言自身的特性,有固定的语义,编译器知道意味着什么,由编译器解释语义,生成相应的代码。

库函数是依赖于库的,一定程度上独立于语言的。编译器不关心库函数的作用,只保证编译,调用函数参数和返回值符合语法,生成call函数的代码。

malloc/free是库函数,new/delete是C++运算符。对于非内部数据类型而言,光用malloc/free无法满足动态对象都要求。new/delete是运算符,编译器保证调用构造和析构函数对对象进行初始化/析构。但是库函数malloc/free是库函数,不会执行构造/析构。

7、delete和delete[]的区别

delete只会调用一次析构函数,而delete[]会调用每个成员的析构函数

用new分配的内存用delete释放,用new[]分配的内存用delete[]释放

多态, 虚函数, 纯虚函数

多态:不同对象接收相同的消息产生不同的动作。多态包括 编译时多态和 运行时多态

运行时多态是:通过继承和虚函数来体现的。 编译时多态:运算符重载上。 封装可以隐藏实现细节,使得代码模块化;继承可以扩展已存在的代码模块(类);它们的目的都是为了——代码重用。多态也有代码重用的功能,还有解决项目中紧耦合的问题,提高程序的可扩展性。C++实现多态的机制很简单,在继承体系下,将父类的某个函数给成虚函数(即加上virtual关键字),在派生类中对这个虚函数进行重写,利用父类的指针或引用调用虚函数。通过指向派生类的基类指针或引用,访问派生类中同名覆盖成员函数。对于虚函数调用来说,每一个对象内部都有一个虚表指针,在构造子类对象时,执行构造函数中进行虚表的创建和虚表指针的初始化,该虚表指针被初始化为本类的虚表。所以在程序中,不管你的对象类型如何转换,但该对象内部的虚表指针是固定的,所以呢,才能实现动态的对象函数调用,这就是C++多态性实现的原理。 需要注意的几点总结(基类有虚函数): 1、每一个类都有虚表,单继承的子类拥有一张虚表,子类对象拥有一个虚表指针;若子类是多重继承(同时继承多个基类),则子类维护多张虚函数表(针对不同基类构建不同虚表),该子类的对象也将包含多个虚表指针。

2、虚表可以继承,如果子类没有重写虚函数,那么子类虚表中仍然会有该函数的地址,只不过这个地址指向的是基类的虚函数实现。如果基类3个虚函数,那么基类的虚表中就有三项(虚函数地址),派生类也会有虚表,至少有三项,如果重写了相应的虚函数,那么虚表中的地址就会改变,指向自身的虚函数实现。如果派生类有自己的虚函数,那么虚表中就会添加该项。 3、派生类的虚表中虚函数地址的排列顺序和基类的虚表中虚函数地址排列顺序相同。

第一:编译器在发现Father 类中有虚函数时,会自动为每个含有虚函数的类生成一份虚函数表,也叫做虚表,该表是一个一维数组,虚表里保存了虚函数的入口地址。

第二:编译器会在每个对象的前四个字节中保存一个虚表指针,即(vptr),指向对象所属类的虚表。在程序运行时的合适时机,根据对象的类型去初始化vptr,从而让vptr指向正确的虚表,从而在调用虚函数时,能找到正确的函数。

第三:所谓的合适时机,在派生类定义对象时,程序运行会自动调用构造函数,在构造函数中创建虚表并对虚表初始化。在构造子类对象时,会先调用父类的构造函数,此时,编译器只“看到了”父类,并为父类对象初始化虚表指针,令它指向父类的虚表;当调用子类的构造函数时,为子类对象初始化虚表指针,令它指向子类的虚表。

虚函数: 在基类中用virtual的成员函数。允许在派生类中对基类的虚函数重新定义。 基类的虚函数可以有函数体,基类也可以实例化。 虚函数要有函数体,否则编译过不去。 虚函数在子类中可以不覆盖。 构造函数不能是虚函数。

纯虚函数:基类中为其派生类保留一个名字,以便派生类根据需要进行定义。 包含一个纯虚函数的类是抽象类。 纯虚函数后面有 = 0; 抽象类不可以实例化。但可以定义指针。 如果派生类如果不是先基类的纯虚函数,则仍然是抽象类。 抽象类可以包含虚函数。

8、STL库用过吗?常见的STL容器有哪些?算法用过几个?

STL包括两部分内容:容器和算法

容器即存放数据的地方,比如array, vector,分为两类,序列式容器和关联式容器

序列式容器,其中的元素不一定有序,但是都可以被排序,比如vector,list,queue,stack,heap, priority-queue, slist

关联式容器,内部结构是一个平衡二叉树,每个元素都有一个键值和一个实值,比如map, set, hashtable, hash_set

算法有排序,复制等,以及各个容器特定的算法

迭代器是STL的精髓,迭代器提供了一种方法,使得它能够按照顺序访问某个容器所含的各个元素,但无需暴露该容器的内部结构,它将容器和算法分开,让二者独立设计。

9、const知道吗?解释一下其作用

const修饰类的成员变量,表示常量不可能被修改

const修饰类的成员函数,表示该函数不会修改类中的数据成员,不会调用其他非const的成员函数

const函数只能调用const函数,非const函数可以调用const函数

10、虚函数是怎么实现的

每一个含有虚函数的类都至少有有一个与之对应的虚函数表,其中存放着该类所有虚函数对应的函数指针(地址),

类的实例对象不包含虚函数表,只有虚指针;

派生类会生成一个兼容基类的虚函数表。

11、堆和栈的区别

1)栈 stack 存放函数的参数值、局部变量,由编译器自动分配释放

堆heap,是由new分配的内存块,由应用程序控制,需要程序员手动利用delete释放,如果没有,程序结束后,操作系统自动回收

2)因为堆的分配需要使用频繁的new/delete,造成内存空间的不连续,会有大量的碎片

3)堆的生长空间向上,地址越大,栈的生长空间向下,地址越小

12、关键字static的作用

1)函数体内: static 修饰的局部变量作用范围为该函数体,不同于auto变量,其内存只被分配一次,因此其值在下次调用的时候维持了上次的值

2)模块内:static修饰全局变量或全局函数,可以被模块内的所有函数访问,但是不能被模块外的其他函数访问,使用范围限制在声明它的模块内

3)类中:修饰成员变量,表示该变量属于整个类所有,对类的所有对象只有一份拷贝

4)类中:修饰成员函数,表示该函数属于整个类所有,不接受this指针,只能访问类中的static成员变量

注意和const的区别!!!const强调值不能被修改,而static强调唯一的拷贝,对所有类的对象

13、STL中map和set的原理(关联式容器)

map和set的底层实现主要通过红黑树来实现

红黑树是一种特殊的二叉查找树

1)每个节点或者是黑色,或者是红色

2)根节点是黑色

3) 每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!]

4)如果一个节点是红色的,则它的子节点必须是黑色的

5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。

特性4)5)决定了没有一条路径会比其他路径长出2倍,因此红黑树是接近平衡的二叉树。

14、#include #include “file.h” 的区别

前者是从标准库路径寻找

后者是从当前工作路径

15、什么是内存泄漏?面对内存泄漏和指针越界,你有哪些方法?

动态分配内存所开辟的空间,在使用完毕后未手动释放,导致一直占据该内存,即为内存泄漏。

方法:malloc/free要配套,对指针赋值的时候应该注意被赋值的指针是否需要释放;使用的时候记得指针的长度,防止越界

16、定义和声明的区别

声明是告诉编译器变量的类型和名字,不会为变量分配空间

定义需要分配空间,同一个变量可以被声明多次,但是只能被定义一次

17、C++文件编译与执行的四个阶段

1)预处理:根据文件中的预处理指令来修改源文件的内容

2)编译:编译成汇编代码

3)汇编:把汇编代码翻译成目标机器指令

4)链接:链接目标代码生成可执行程序

18、STL中的vector的实现,是怎么扩容的?

vector使用的注意点及其原因,频繁对vector调用push_back()对性能的影响和原因。

vector就是一个动态增长的数组,里面有一个指针指向一片连续的空间,当空间装不下的时候,会申请一片更大的空间,将原来的数据拷贝过去,并释放原来的旧空间。当删除的时候空间并不会被释放,只是清空了里面的数据。对比array是静态空间一旦配置了就不能改变大小。

vector的动态增加大小的时候,并不是在原有的空间上持续新的空间(无法保证原空间的后面还有可供配置的空间),而是以原大小的两倍另外配置一块较大的空间,然后将原内容拷贝过来,并释放原空间。在VS下是1.5倍扩容,在GCC下是2倍扩容。

在原来空间不够存储新值时,每次调用push_back方法都会重新分配新的空间以满足新数据的添加操作。如果在程序中频繁进行这种操作,还是比较消耗性能的。

19、STL中unordered_map和map的区别

map是STL中的一个关联容器,提供键值对的数据管理。底层通过红黑树来实现,实际上是二叉排序树和非严格意义上的二叉平衡树。所以在map内部所有的数据都是有序的,且map的查询、插入、删除操作的时间复杂度都是O(logN)。

unordered_map和map类似,都是存储key-value对,可以通过key快速索引到value,不同的是unordered_map不会根据key进行排序。unordered_map底层是一个防冗余的哈希表,存储时根据key的hash值判断元素是否相同,即unoredered_map内部是无序的。

20、C++的内存管理

在C++中,内存被分成五个区:栈、堆、自由存储区、静态存储区、常量区

栈:存放函数的参数和局部变量,编译器自动分配和释放

堆:new关键字动态分配的内存,由程序员手动进行释放,否则程序结束后,由操作系统自动进行回收

自由存储区:由malloc分配的内存,和堆十分相似,由对应的free进行释放

全局/静态存储区:存放全局变量和静态变量

常量区:存放常量,不允许被修改

21、 构造函数为什么一般不定义为虚函数?而析构函数一般写成虚函数的原因 ?

1、构造函数不能声明为虚函数

1)因为创建一个对象时需要确定对象的类型,而虚函数是在运行时确定其类型的。而在构造一个对象时,由于对象还未创建成功,编译器无法知道对象的实际类型,是类本身还是类的派生类等等

2)虚函数的调用需要虚函数表指针,而该指针存放在对象的内存空间中;若构造函数声明为虚函数,那么由于对象还未创建,还没有内存空间,更没有虚函数表地址用来调用虚函数即构造函数了

2、析构函数最好声明为虚函数

首先析构函数可以为虚函数,当析构一个指向派生类的基类指针时,最好将基类的析构函数声明为虚函数,否则可以存在内存泄露的问题。

如果析构函数不被声明成虚函数,则编译器实施静态绑定,在删除指向派生类的基类指针时,只会调用基类的析构函数而不调用派生类析构函数,这样就会造成派生类对象析构不完全。

子类析构时,要调用父类的析构函数吗?

析构函数调用的次序时先派生类后基类的。和构造函数的执行顺序相反。并且析构函数要是virtual的,否则如果用父类的指针指向子类对象的时候,析构函数静态绑定,不会调用子类的析构。

不用显式调用,会自动调用

22、静态绑定和动态绑定的介绍

静态绑定和动态绑定是C++多态性的一种特性

1)对象的静态类型和动态类型

静态类型:对象在声明时采用的类型,在编译时确定

动态类型:当前对象所指的类型,在运行期决定,对象的动态类型可变,静态类型无法更改

2)静态绑定和动态绑定

静态绑定:绑定的是对象的静态类型,函数依赖于对象的静态类型,在编译期确定

动态绑定:绑定的是对象的动态类型,函数依赖于对象的动态类型,在运行期确定

只有虚函数才使用的是动态绑定,其他的全部是静态绑定

23、 引用是否能实现动态绑定,为什么引用可以实现

可以。因为引用(或指针)既可以指向基类对象也可以指向派生类对象,这一事实是动态绑定的关键。用引用(或指针)调用的虚函数在运行时确定,被调用的函数是引用(或指针)所指的对象的实际类型所定义的。

24、深拷贝和浅拷贝的区别

深拷贝和浅拷贝可以简单的理解为:如果一个类拥有资源,当这个类的对象发生复制过程的时候,如果资源重新分配了就是深拷贝;反之没有重新分配资源,就是浅拷贝。

25、 什么情况下会调用拷贝构造函数(三种情况)

系统自动生成的构造函数:普通构造函数和拷贝构造函数 (在没有定义对应的构造函数的时候)

生成一个实例化的对象会调用一次普通构造函数,而用一个对象去实例化一个新的对象所调用的就是拷贝构造函数

调用拷贝构造函数的情形:

1)用类的一个对象去初始化另一个对象的时候

2)当函数的参数是类的对象时,就是值传递的时候,如果是引用传递则不会调用

3)当函数的返回值是类的对象或者引用的时候

举例:

#include
#include
using namespace std;
class A{
private:
int data;
public:
A(int i){ data = i;} //自定义的构造函数
A(A && a); //拷贝构造函数
int getdata(){return data;}
};
//拷贝构造函数
A::A(A && a){
data = a.data;
cout <<“拷贝构造函数执行完毕”< }
//参数是对象,值传递,调用拷贝构造函数
int getdata1(A a){
return a.getdata();
}
//参数是引用,引用传递,不调用拷贝构造函数
int getdata2(A &a){
return a.getdata();
}
//返回值是对象类型,会调用拷贝构造函数
A getA1(){
A a(0);
return a;
}
//返回值是引用类型,会调用拷贝构造函数,因为函数体内生成的对象是临时的,离开函数就消失
A& getA2(){
A a(0);
return a;
}
int main(){
A a1(1);
A b1(a1); //用a1初始化b1,调用拷贝构造函数
A c1=a1; //用a1初始化c1,调用拷贝构造函数
int i=getdata1(a1); //函数形参是类的对象,调用拷贝构造函数
int j=getdata2(a1); //函数形参类型是引用,不调用拷贝构造函数
A d1=getA1(); //调用拷贝构造函数
A e1=getA2(); //调用拷贝构造函数
return 0;
}
26、 C++的四种强制转换

类型转化机制可以分为隐式类型转换和显示类型转化(强制类型转换)

(new-type) expression

new-type (expression)

隐式类型转换比较常见,在混合类型表达式中经常发生;四种强制类型转换操作符:

static_cast、dynamic_cast、const_cast、reinterpret_cast

1)static_cast :编译时期的静态类型检查

static_cast < type-id > ( expression )

该运算符把expression转换成type-id类型,在编译时使用类型信息执行转换,在转换时执行必要的检测(指针越界、类型检查),其操作数相对是安全的

2)dynamic_cast:运行时的检查

用于在集成体系中进行安全的向下转换downcast,即基类指针/引用->派生类指针/引用

dynamic_cast是4个转换中唯一的RTTI操作符,提供运行时类型检查。

dynamic_cast如果不能转换返回NULL

dynamic_cast转为引用类型的时候转型失败会抛bad_cast

源类中必须要有虚函数,保证多态,才能使用dynamic_cast(expression)

3)const_cast

去除const常量属性,使其可以修改 ; volatile属性的转换

4)reinterpret_cast

通常为了将一种数据类型转换成另一种数据类型

27、调试程序的方法

windows下直接使用vs的debug功能

linux下直接使用gdb,我们可以在其过程中给程序添加断点,监视等辅助手段,监控其行为是否与我们设计相符

28、extern“C”作用

extern "C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言的进行编译,而不是C++的。

29、typdef和define区别

#define是预处理命令,在预处理是执行简单的替换,不做正确性的检查

typedef是在编译时处理的,它是在自己的作用域内给已经存在的类型一个别名

typedef (int*) pINT;

#define pINT2 int*

效果相同?实则不同!实践中见差别:pINT a,b;的效果同int *a; int *b;表示定义了两个整型指针变量。而pINT2 a,b;的效果同int *a, b;表示定义了一个整型指针变量a和整型变量b。

30、volatile关键字在程序设计中有什么作用

volatile是“易变的”、“不稳定”的意思。volatile是C的一个较为少用的关键字,它用来解决变量在“共享”环境下容易出现读取错误的问题。

变量如果加了voletile修饰,则会从内存中重新装载内容,而不是直接从寄存器中拷贝内容。

在本次线程内,当读取一个变量时,为了提高读取速度,编译器进行优化时有时会先把变量读取到一个寄存器中;以后,再读取变量值时,就直接从寄存器中读取;当变量值在本线程里改变时,会同时把变量的新值copy到该寄存器中,以保持一致。当变量因别的线程值发生改变,上面寄存器的值不会相应改变,从而造成应用程序读取的值和实际的变量值不一致。

volatile可以避免优化、强制内存读取的顺序,但是volatile并没有线程同步的语义,C++标准并不能保证它在多线程情况的正确性。C++11开始有一个很好用的库,那就是atomic类模板,在头文件中,多个线程对atomic对象进行访问是安全的,并且提供不同种类的线程同步。它默认使用的是最强的同步,所以我们就使用默认的就好。

31、引用作为函数参数以及返回值的好处

对比值传递,引用传参的好处:

1)在函数内部可以对此参数进行修改

2)提高函数调用和运行的效率(所以没有了传值和生成副本的时间和空间消耗)

如果函数的参数实质就是形参,不过这个形参的作用域只是在函数体内部,也就是说实参和形参是两个不同的东西,要想形参代替实参,肯定有一个值的传递。函数调用时,值的传递机制是通过“形参=实参”来对形参赋值达到传值目的,产生了一个实参的副本。即使函数内部有对参数的修改,也只是针对形参,也就是那个副本,实参不会有任何更改。函数一旦结束,形参生命也宣告终结,做出的修改一样没对任何变量产生影响。

用引用作为返回值最大的好处就是在内存中不产生被返回值的副本。

但是有以下的限制:

1)不能返回局部变量的引用。因为函数返回以后局部变量就会被销毁

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

3)可以返回类成员的引用,但是最好是const。因为如果其他对象可以获得该属性的非常量的引用,那么对该属性的单纯赋值就会破坏业务规则的完整性。

32、纯虚函数

纯虚函数是只有声明没有实现的虚函数,是对子类的约束,是接口继承

包含纯虚函数的类是抽象类,它不能被实例化,只有实现了这个纯虚函数的子类才能生成对象

普通函数是静态编译的,没有运行时多态

33、什么是野指针

野指针不是NULL指针,是未初始化或者未清零的指针,它指向的内存地址不是程序员所期望的,可能指向了受限的内存

成因:

1)指针变量没有被初始化

2)指针指向的内存被释放了,但是指针没有置NULL

3)指针超过了变量了的作用范围,比如b[10],指针b+11

33、线程安全和线程不安全

线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可以使用,不会出现数据不一致或者数据污染。

线程不安全就是不提供数据访问保护,有可能多个线程先后更改数据所得到的数据就是脏数据。

34、C++中内存泄漏的几种情况

内存泄漏是指己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

1)类的构造函数和析构函数中new和delete没有配套

2)在释放对象数组时没有使用delete[],使用了delete

3)没有将基类的析构函数定义为虚函数,当基类指针指向子类对象时,如果基类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有正确释放,因此造成内存泄露

4)没有正确的清楚嵌套的对象指针

35、栈溢出的原因以及解决方法

栈溢出是指函数中的局部变量造成的溢出(注:函数中形参和函数中的局部变量存放在栈上)

栈的大小通常是1M-2M,所以栈溢出包含两种情况,一是分配的的大小超过栈的最大值,二是分配的大小没有超过最大值,但是接收的buf比原buf小。

1)函数调用层次过深,每调用一次,函数的参数、局部变量等信息就压一次栈

2)局部变量体积太大。

解决办法大致说来也有两种:

1> 增加栈内存的数目;如果是不超过栈大小但是分配值小的,就增大分配的大小

2> 使用堆内存;具体实现由很多种方法可以直接把数组定义改成指针,然后动态申请内存;也可以把局部变量变成全局变量,一个偷懒的办法是直接在定义前边加个static,呵呵,直接变成静态变量(实质就是全局变量)

36、C++标准库vector以及迭代器

每种容器类型都定义了自己的迭代器类型,每种容器都定义了一队命名为begin和end的函数,用于返回迭代器。

迭代器是容器的精髓,它提供了一种方法使得它能够按照顺序访问某个容器所含的各个元素,但无需暴露该容器的内部结构,它将容器和算法分开,让二者独立设计。

38、C++中vector和list的区别

vector和数组类似,拥有一段连续的内存空间。vector申请的是一段连续的内存,当插入新的元素内存不够时,通常以2倍重新申请更大的一块内存,将原来的元素拷贝过去,释放旧空间。因为内存空间是连续的,所以在进行插入和删除操作时,会造成内存块的拷贝,时间复杂度为o(n)。

list是由双向链表实现的,因此内存空间是不连续的。只能通过指针访问数据,所以list的随机存取非常没有效率,时间复杂度为o(n); 但由于链表的特点,能高效地进行插入和删除。

vector拥有一段连续的内存空间,能很好的支持随机存取,因此vector::iterator支持“+”,“+=”,“<”等操作符。

list的内存空间可以是不连续,它不支持随机访问,因此list::iterator则不支持“+”、“+=”、“<”等

vector::iterator和list::iterator都重载了“++”运算符。

总之,如果需要高效的随机存取,而不在乎插入和删除的效率,使用vector;

如果需要大量的插入和删除,而不关心随机存取,则应使用list。

39、C语言的函数调用过程

函数的调用过程:

1)从栈空间分配存储空间

2)从实参的存储空间复制值到形参栈空间

3)进行运算

形参在函数未调用之前都是没有分配存储空间的,在函数调用结束之后,形参弹出栈空间,清除形参空间。

数组作为参数的函数调用方式是地址传递,形参和实参都指向相同的内存空间,调用完成后,形参指针被销毁,但是所指向的内存空间依然存在,不能也不会被销毁。

当函数有多个返回值的时候,不能用普通的 return 的方式实现,需要通过传回地址的形式进行,即地址/指针传递。

传值:传值,实际是把实参的值赋值给行参,相当于copy。那么对行参的修改,不会影响实参的值 。

传址: 实际是传值的一种特殊方式,只是他传递的是地址,不是普通的赋值,那么传地址以后,实参和行参都指向同一个对象,因此对形参的修改会影响到实参。

40、C++中的基本数据类型及派生类型

1)整型 int

2)浮点型 单精度float,双精度double

3)字符型 char

4)逻辑型 bool

5)控制型 void

基本类型的字长及其取值范围可以放大和缩小,改变后的类型就叫做基本类型的派生类型。派生类型声明符由基本类型关键字char、int、float、double前面加上类型修饰符组成。

类型修饰符包括:

short 短类型,缩短字长

long 长类型,加长字长

signed 有符号类型,取值范围包括正负值

unsigned 无符号类型,取值范围只包括正值

41、友元函数和友元类

友元提供了不同类的成员函数之间、类的成员函数和一般函数之间进行数据共享的机制。

通过友元,一个不同函数或者另一个类中的成员函数可以访问类中的私有成员和保护成员。

友元的正确使用能提高程序的运行效率,但同时也破坏了类的封装性和数据的隐藏性,导致程序可维护性变差。

1)友元函数

有元函数是可以访问类的私有成员的非成员函数。它是定义在类外的普通函数,不属于任何类,但是需要在类的定义中加以声明。

friend 类型 函数名(形式参数);

一个函数可以是多个类的友元函数,只需要在各个类中分别声明。

2)友元类

友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的隐藏信息(包括私有成员和保护成员)。

friend class 类名;

使用友元类时注意:

(1) 友元关系不能被继承。

(2) 友元关系是单向的,不具有交换性。若类B是类A的友元,类A不一定是类B的友元,要看在类中是否有相应的声明。

(3) 友元关系不具有传递性。若类B是类A的友元,类C是B的友元,类C不一定是类A的友元,同样要看类中是否有相应的申明

c++函数库中一些实用的函数

  1. __gcd(x, y)

求两个数的最大公约数,如__gcd(6, 8) 就返回2。

  1. reverse(a + 1, a + n + 1)

将数组中的元素反转。a 是数组名,n是长度,跟 sort 的用法一样。值得一提的是,对于字符型数组也同样适用。

  1. unique(a + 1, a + n + 1)

去重函数。跟sort的用法一样。不过他返回的值是最后一个数的地址,所以要得到新的数组长度应该这么写: _n = unique(a + 1, a + n + 1) - a - 1.

4.lower_bound(a + 1, a + n + 1, x); upper_bound(a + 1, a + n + 1, x)

lower_bound是查找数组中第一个大于等于x的数,返回该地址,同理也是 pos = lower_bound(a + 1, a + n + 1, x) - a

upper_bound是查找第一个大于x的数,用法和lower_bound一样

复杂度是二分的复杂度,O(logn)。(其实就是代替了手写二分)

5.fill(a + 1, a + n + 1, x)

例如

int数组:fill(arr, arr + n, 要填入的内容);

vector也可以:fill(v.begin(), v.end(), 要填入的内容);

fill(vector.begin(), cnt, val); // 从当前起始点开始,将之后的cnt个元素赋值为val。

memset(arr, val, cnt); // 在头文件里。

将数组a中的每一个元素都赋成x,跟memset的区别是,memset函数按照字节填充,所以一般memset只能用来填充char型数组,(因为只有char型占一个字节)如果填充int型数组,除了0和-1,其他的不能。

●多进程和多线程的区别

进程它是具有独立地址空间的,优点就是隔离度好,稳定,因为它是操作系统管理的,进程和进程之间是逻辑隔离的,只要操作系统不出问题的话,一个进程的错误一般不会影响到其它进程,缺点就是信息资源共享麻烦。而线程只是进程启动的执行单元,它是共享进程资源的,创建销毁、切换简单,速度很快,占用内存少,CPU利用率高。但是需要程序员管控的东西也比较多,相互影响出问题的机率较大,一个线程挂掉将导致整个进程挂掉,所以从程序员的角度来讲,我们只能看到某种代码是线程安全的,而没有说进程安全的。

●在进程和线程上,应该怎么选择

我们平时在写代码的时候一般使用线程会比较多,像需要频繁创建销毁的,要处理大量运算、数据,又要能很好的显示界面和及时响应消息的优先选择多线程,因为像这些运算会消耗大量的CPU,常见的有算法处理和图像处理。还有一些操作允许并发而且有可能阻塞的, 也推荐使用多线程. 例如SOCKET, 磁盘操作等等。进程一般来说更稳定,而且它是内存隔离的,单个进程的异常不会导致整个应用的崩溃,方便调试,像很多服务器默认是使用进程模式的。

●线程之间是如何通信的

一个是使用全局变量进行通信,还有就是可以使用自定义的消息机制传递信息。其实因为各个线程之间它是共享进程的资源的,所以它没有像进程通信中的用于数据交换的通信方式,它通信的主要目的是用于线程同步,所以像一些互斥锁啊临界区啊CEvent事件对象和信号量对象都可以实现线程的通信和同步。

●进程之间是如何通信的

进程间的通信方式有PIPE管道,信号量,消息队列,共享内存,还可以通过 socket套接字进行通信。根据信息量大小的不同可以分为低级通信和高级通信,在选择上,如果用户传递的信息较少.或是需要通过信号来触发某些行为的,一般用信号机制就能解决,如果进程间要求传递的信息量比较大或者有交换数据的要求,那么就要使用共享内存和套接字这些通信方式。

名词解释:

管道其实是存在于内存中的一种特殊文件,它不属于文件系统,有自己的数据结构,根据使用范围还可分为无名管道和命名管道。

共享内存是通过将共享的内存缓冲区直接附加到进程的虚拟地址空间中来实现的,它是利用内存缓冲区直接交换信息,不需要复制,很快捷、信息量大。

消息队列缓冲是由系统调用函数来实现消息发送和接收之间的同步,它允许任意进程通过共享消息队列来实现进程间通信.但是信息的复制需要耗费大量CPU,所以不适用于信息量大或操作频繁的场合。

●线程同步和线程异步

同步是指一个线程要等待另一个线程执行完之后才开始执行当前的线程。

异步是指一个线程去执行,它的下一个线程不必等待它执行完就开始执行。

一般一个进程启动的多个不相干线程,它们之间的相互关系就为异步,比如游戏有图像和背景音乐,图像是由玩家操作的 而背景音乐是系统循环播放,它们两个线程之间没什么关系各干各的,这就是线程异步。至于同步的话指的是多线程同时操作一个数据,这个时候需要对数据添加保护,这个保护就是线程的同步

同步使用场景:对于多个线程同时访问一块数据的时候,必须使用同步,否则可能会出现不安全的情况,有一种情况不需要同步技术,那就是原子操作,也就是说操作系统在底层保证了操作要么全部做完,要么不做。

异步的使用场景:当只有一个线程访问当前数据的时候。比如观察者模式,它没有共享区,主题发生变化后通知观察者更新,主题继续做自己的事情,不需要等待观察者更新完成后再工作。

●多线程同步和互斥有几种实现方法,分别适用什么情况

线程同步的话有临界区,互斥量,信号量,事件。

临界区适合一个进程内的多线程访问公共区域或代码段时使用。

互斥量是可以命名的,也就是说它可以适用不同进程内多线程访问公共资源时使用。所以在选择上如果是在进程内部使用的话,用临界区会带来速度上的优势并且能够减少资源占用量。

信号量与临界区和互斥量不同,它是允许多个线程同时访问公共资源的,它相当于操作系统的PV操作,它会事先设定一个最大线程数,如果线程占用数达到最大,那么其它线程就不能再进来,如果有部分线程释放资源了,那么其它线程才能进来访问资源。

事件是通过通知操作的方式来保持线程同步。

注意:互斥量,事件,信号量都是内核对象,可以跨进程使用。

●C++多线程有几种实现方法

直接使用WIN32 API CreateThread,或者用C运行库_beginthread创建线程,MFC的话用AfxBeginThread. 还有就是运用第三方线程库,比如boost的thread等等。

_beginthread和CreateThread的区别:_beginthread内部调用了CreateThread.

如果你的程序只调用 Win32 API/SDK ,就放心用 CreateThread,如果要用到C++运行时库,那么就要使用_beginthreadex,因为C++运行库有一些函数里面使用了全局变量,beginthreadex 为这些全局变量做了处理,使得每个线程都有一份独立的“全局”量,在这种情况下使用CreateThread的话就会出现不安全的问题

●进程有哪几种状态,状态转换图,及导致转换的事件

●死锁

概念:进程间进行通信或相互竞争系统资源而产生的永久阻塞,若无外力作用将永远处在死锁状态。

产生原因:(1)系统资源不足;(2)进程运行推进顺序与速度不同也可能导致死锁;(3)资源分配不当;

产生死锁四个必要条件:

(1) 互斥条件:就是一个资源每次只能被一个进程使用。

(2) 请求与保持条件:一个进程在请求其它资源而阻塞时,但是它对自己已获得的资源又保持不放。

(3) 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。

(4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

预防死锁和避免死锁的方法:在系统设计、进程调度方面注意不让产生死锁的四个必要条件成立,确定资源的合理分配算法,避免进程永远占用系统资源,对资源分配要进行合理的规划。

●多线程中栈与堆是公有的还是私有的

因为线程是共享进程的资源的,所以栈是私有的,堆是公有的。

●线程池的概念

线程池就是一堆已经创建好的线程,最大数目一定,然后初始后都处于空闲状态,当有新任务进来时就从线程池中取出空闲线程处理任务,任务完成之后又重新放回去,当线程池中的所有线程都在任务时,只能等待有线程结束任务才能继续执行。

1、Socket通信流程

Socket通信流程

服务端:创建socket()、绑定socket和端口号bind(),监听端口llisten(),接收来自客户端的请求accpet(),从socket中读取字符recv(),关闭close()

客户端: 创建socket()、 连接指定端口connect(), 发送数据send() 关闭close()

C/S通信方式

2、字节序分为大端字节序和小端字节序。
大端字节序:最高有效位存储于最低内存地址处,最低有效位存储于最高内存地址。
小端字节序:最高有效位存储于最高内存地址处,最低有效位存储于最低内存地址。

主机字节序:不同的主机有不同的字节序。
(1)x86硬件架构下的主机为小端字节序
(2)Motorola 68000为大端字节序
(3)ARM架构的主机字节序是可配置的

网络字节序:网络字节序规定为大端字节序。

3、进程间通讯的方式:剪切板、命名管道、匿名通道、油槽。

4、线程间通讯的方式:全局变量、自定义消息、事件。

5、线程间同步的方式:临界区、互斥量、信号量、事件。

C++ REST SDK

C++ REST SDK是微软开源的一套客户端-服务器通信库,提供了URI构造/解析,JSON编解码,HTTP客户端、HTTP服务端,WebSocket客户端,流式传输,oAuth验证等C++类,方便C++语言编写的客户端程序访问互联网服务。其中HTTP服务端相关的类是最近新增的(尚处于beta测试阶段),这些类拓展了C++ REST SDK的功能,现在不仅能开发客户端程序,也能做服务端开发了。
访问barcode服务器就是使用c++ rest sdk

URI
通一资源标志符(Uniform Resource Identifier, URI),表示的是web上每一种可用的资源,如 HTML文档、图像、视频片段、程序等都由一个URI进行定位的。

URL
是URI的一个子集。它是Uniform Resource Locator的缩写,译为“统一资源定位 符”。
通俗地说,URL是Internet上描述信息资源的字符串,主要用在各种WWW客户程序和服务器程序上。
采用URL可以用一种统一的格式来描述各种信息资源,包括文件、服务器的地址和目录等。URL是URI概念的一种实现方式。

ICE

ICE的产生就是源于.NET、CORBA及WEB SERVICE这些中间件的不足,它可以支持不同的系统,如WINDOWS、LINUX等,也可以支持在多种开发语言上使用,

如C++、C、JAVA、RUBY、PYTHON、VB等,服务端可以是上面提到的任何一种语言实现的,客户端也可以根据自己的实际情况选择不同的语言实现,如服务端采用C语言实现,

而客户端采用JAVA语言实现,底层的通讯逻辑通过ICE的封装实现,我们只需要关注业务逻辑。

3、ICE是如何工作的?

Ice 是一种面向对象的中间件平台,这意味着 Ice为构建面向对象的客户-服务器应用提供了工具、API 和库支持。要与Ice持有的对象进行通信,

客户端必须持有这个对象的代理(与CORBA的引用是相同的意思),这里的代理指的是这个对象的实例,ICE在运行时会定位到这个对象,然后寻找或激活它,再把In参数传给远程对象,再通过Out参数获取返回结果。

1 安装ICE。svn:http://terminal-svn4.hytera.com/Terminal/PC_Application/02_Sourcecode/Tools/commom/OpenSource/ice

可以安装在加密盘,重启后也能用

2.将工程中创建的slice(.ice)文件拷贝到ICE的目录下(E:\Tools_Release\ICE_3.6.1.2\Ice-3.6.1)

3.使用dos在ICE的安装目录下启动slice2cpp (E:\Tools_Release\ICE_3.6.1.2\Ice-3.6.1)

4.对照编译说明敲指令,例如:

D:\ZeroC\Ice-3.6.1>bin\slice2cpp.exe -Islice .\g2pc_Licence.ice

5.在ICE的安装目录下获取.cpp、.h文件

11、TCP/IP、OSI协议

TCP/IP:数据链路层、 网络层、传输层、 应用层

OSI:物理层、数据链路层、网络层、传输层、会话层、表示层、应用层

6、TCP/IP三次握手:

syn (Synchronize) 标记的包,SYN=1则告诉B请求建立连接.

seq是序列号,这是为了连接以后传送数据用的,

ack是对收到的数据包的确认,值是等待接收的数据包的序列号=seq+1。

过程分析

第一次握手:主机A发送位码为syn=1,随机产生seq number=1234567的数据包到服务器,主机B由SYN=1知道,A要求建立联机;

第二次握手:主机B收到请求后要确认联机信息,向A发送ack=Seq+1,ack number=(主机A的seq+1),syn=1,随机产生seq=7654321的包

第三次握手:主机A收到后检查ack number是否正确,即第一次发送的seq number+1,以及位码ack是否为1,若正确,主机A会再发送ack number=(主机B的seq+1),,主机B收到后确认seq值与ack=1则连接建立成功。

过程状态

第一次握手:建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认;
第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;

第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。 完成三次握手,客户端与服务器开始传送数据.

四、HTTP通信传输

客户端输入URL回车,DNS解析域名得到服务器的IP地址,服务器在80端口监听客户端请求,端口通过TCP/IP协议(可以通过Socket实现)建立连接。HTTP属于TCP/IP模型中的运用层协议,所以通信的过程其实是对应数据的入栈和出栈。

报文从运用层传送到运输层,运输层通过TCP三次握手和服务器建立连接,四次挥手释放连接。

为什么需要三次握手呢?为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。

比如:client发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致延误到连接释放以后的某个时间才到达server。本来这是一个早已失效的报文段,但是server收到此失效的连接请求报文段后,就误认为是client再次发出的一个新的连接请求,于是就向client发出确认报文段,同意建立连接。假设不采用“三次握手”,那么只要server发出确认,新的连接就建立了,由于client并没有发出建立连接的请求,因此不会理睬server的确认,也不会向server发送数据,但server却以为新的运输连接已经建立,并一直等待client发来数据。所以没有采用“三次握手”,这种情况下server的很多资源就白白浪费掉了。

为什么需要四次挥手呢?TCP是全双工模式,当client发出FIN报文段时,只是表示client已经没有数据要发送了,client告诉server,它的数据已经全部发送完毕了;但是,这个时候client还是可以接受来server的数据;当server返回ACK报文段时,表示它已经知道client没有数据发送了,但是server还是可以发送数据到client的;当server也发送了FIN报文段时,这个时候就表示server也没有数据要发送了,就会告诉client,我也没有数据要发送了,如果收到client确认报文段,之后彼此就会愉快的中断这次TCP连接。

五、HTTPS实现原理

https://blog.csdn.net/xiaoming100001/article/details/81109617

SSL建立连接过程

client向server发送请求https://baidu.com,然后连接到server的443端口,发送的信息主要是随机值1和客户端支持的加密算法。

server接收到信息之后给予client响应握手信息,包括随机值2和匹配好的协商加密算法,这个加密算法一定是client发送给server加密算法的子集。

随即server给client发送第二个响应报文是数字证书。服务端必须要有一套数字证书,可以自己制作,也可以向组织申请。区别就是自己颁发的证书需要客户端验证通过,才可以继续访问,而使用受信任的公司申请的证书则不会弹出提示页面,这套证书其实就是一对公钥和私钥。传送证书,这个证书其实就是公钥,只是包含了很多信息,如证书的颁发机构,过期时间、服务端的公钥,第三方证书认证机构(CA)的签名,服务端的域名信息等内容。

客户端解析证书,这部分工作是由客户端的TLS来完成的,首先会验证公钥是否有效,比如颁发机构,过期时间等等,如果发现异常,则会弹出一个警告框,提示证书存在问题。如果证书没有问题,那么就生成一个随即值(预主秘钥)。

客户端认证证书通过之后,接下来是通过随机值1、随机值2和预主秘钥组装会话秘钥。然后通过证书的公钥加密会话秘钥。

传送加密信息,这部分传送的是用证书加密后的会话秘钥,目的就是让服务端使用秘钥解密得到随机值1、随机值2和预主秘钥。

服务端解密得到随机值1、随机值2和预主秘钥,然后组装会话秘钥,跟客户端会话秘钥相同。

客户端通过会话秘钥加密一条消息发送给服务端,主要验证服务端是否正常接受客户端加密的消息。

同样服务端也会通过会话秘钥加密一条消息回传给客户端,如果客户端能够正常接受的话表明SSL层连接建立完成了。

问题:

1.怎么保证保证服务器给客户端下发的公钥是真正的公钥,而不是中间人伪造的公钥呢?

2.证书如何安全传输,被掉包了怎么办?

数字证书内容

包括了加密后服务器的公钥、权威机构的信息、服务器域名,还有经过CA私钥签名之后的证书内容(经过先通过Hash函数计算得到证书数字摘要,然后用权威机构私钥加密数字摘要得到数字签名),签名计算方法以及证书对应的域名。

验证证书安全性过程

当客户端收到这个证书之后,使用本地配置的权威机构的公钥对证书进行解密得到服务端的公钥和证书的数字签名,数字签名经过CA公钥解密得到证书信息摘要。

然后证书签名的方法计算一下当前证书的信息摘要,与收到的信息摘要作对比,如果一样,表示证书一定是服务器下发的,没有被中间人篡改过。因为中间人虽然有权威机构的公钥,能够解析证书内容并篡改,但是篡改完成之后中间人需要将证书重新加密,但是中间人没有权威机构的私钥,无法加密,强行加密只会导致客户端无法解密,如果中间人强行乱修改证书,就会导致证书内容和证书签名不匹配。

那第三方攻击者能否让自己的证书显示出来的信息也是服务端呢?(伪装服务端一样的配置)显然这个是不行的,因为当第三方攻击者去CA那边寻求认证的时候CA会要求其提供例如域名的whois信息、域名管理邮箱等证明你是服务端域名的拥有者,而第三方攻击者是无法提供这些信息所以他就是无法骗CA他拥有属于服务端的域名。

六、运用与总结

安全性考虑:

HTTPS协议的加密范围也比较有限,在黑客攻击、拒绝服务攻击、服务器劫持等方面几乎起不到什么作用

SSL证书的信用链体系并不安全,特别是在某些国家可以控制CA根证书的情况下,中间人攻击一样可行

中间人攻击(MITM攻击)是指,黑客拦截并篡改网络中的通信数据。又分为被动MITM和主动MITM,被动MITM只窃取通信数据而不修改,而主动MITM不但能窃取数据,还会篡改通信数据。最常见的中间人攻击常常发生在公共wifi或者公共路由上。

成本考虑:

SSL证书需要购买申请,功能越强大的证书费用越高

SSL证书通常需要绑定IP,不能在同一IP上绑定多个域名,IPv4资源不可能支撑这个消耗(SSL有扩展可以部分解决这个问题,但是比较麻烦,而且要求浏览器、操作系统支持,Windows XP就不支持这个扩展,考虑到XP的装机量,这个特性几乎没用)。

根据ACM CoNEXT数据显示,使用HTTPS协议会使页面的加载时间延长近50%,增加10%到20%的耗电。

HTTPS连接缓存不如HTTP高效,流量成本高。

HTTPS连接服务器端资源占用高很多,支持访客多的网站需要投入更大的成本。

HTTPS协议握手阶段比较费时,对网站的响应速度有影响,影响用户体验。比较好的方式是采用分而治之,类似12306网站的主页使用HTTP协议,有关于用户信息等方面使用HTTPS。

7、指针和引用的区别:

a、引用被创建时必须进行初始化,指针可以在任何时候初始化。

b、不能有空引用,但可以有空指针。

c、一旦引用被初始化就不能改变引用的关系,指针可以改变指向的对象。

8、程序和进程的区别:

a、程序是指令的有序集合,其本身没有任何运行的含义,是一个静态的概念。可以作为一种软件资料长期存在。

b、进程是程序在处理机上的一次执行过程,是动态的概念。是有一定生命期的。进程是程序的应用实例。

9、MFC CStirng相关函数源码

int strlen(const char *str)

{

int iLength = 0;

while(*str++)

{

++iLength;

}

return (iLength);

}

char *strcpy(char *strDst, const char *strSrc)

{

char *p = strDst;

while (*p++ = *strSrc++)

{

;

}

return (strDst);

}

char *strcat(char *strDst, const char *strSrc)

{

char *p = strDst;

while(*p++)

{

;

}

while (*p++ = *strSrc++)

{

;

}

return (strDst);

}

int strcmp(const char *str1, const char *str2)

{

int ret = 0;

while (!(ret = (unsigned char)str1 - (unsigned char)str2) && *str1)

{

++str1;

++str2;

}

if (ret < 0)

{

ret = -1;

}

else if (ret > 0)

{

ret = 1;

}

return ret;

}

10、Boost中的智能指针
智能指针有两种分类:

1.不带引用计数的智能指针

auto_ptr、unique_ptr、scoped_ptr

2.带引用计数的智能指针

shared_ptr(强智能指针)、weak_ptr(弱智能指针)

其中我们最主要用的就是带引用计数的智能指针,因为它的引用计数可以方便我们对函数进行适当时间的析构,避免发生没存泄露问题(说起这个内存泄漏问题,如何在windows上进行内存泄露的快速定位。几种方法:1.可以在VS调到DEBUG版本,然后利用它自身的宏 _CrtDumpMemoryLeaks()就可以再运行结果出爆出内存泄漏的大概地方 2.有一个插件吧,它是专门用来检测内存泄漏的,你们可以去下载一下 VISUAL LEAK PETECTOR(VLD),就是这个软件,然后你在后面每次写代码之前包含一下它的头文件,这样的话运行过程中要是有内存泄露问题,它在结果显示部分会给你清楚的显示出泄露了多少字节,泄露的准确行数再哪里,总之用起来很方便。对了,关于如何去判断内存泄漏我也说出一种方法吧,其实真正的内存泄漏并不是说你malloc,new了以后没有free,delete,要是你觉得这个就算是内存泄露的真正含义的话,那你就是个外行,真正的内存泄漏是指在系统运行的期间,某种原因会一直损耗其内存,直到其消耗至空为止,这才是真正的内存泄漏,所以说我们可以打开其任务管理器区查看其没存使用情况和虚拟内存空间的占用率,要是不停的再增长的话,那肯定就内存泄露了。)

对于智能指针早成的困扰,我想你应该也知道,就是智能指针的交叉引用,那么如何解决呢,其实很简单,就是奖两个中的一个改成weak_ptr就行啦,毕竟weak_ptr它是依赖于shared_ptr存活的。

1 什么是 RPC ?
我们使用开源的buttonRPC库

RPC (Remote Procedure Call)即远程过程调用,是分布式系统常见的一种通信方法。它允许程序调用另一个地址空间(通常是共享网络的另一台机器上)的过程或函数,而不用程序员显式编码这个远程调用的细节。
除 RPC 之外,常见的多系统数据交互方案还有分布式消息队列、HTTP 请求调用、数据库和分布式缓存等。
其中 RPC 和 HTTP 调用是没有经过中间件的,它们是端到端系统的直接数据交互。
简单的说

RPC就是从一台机器(客户端)上通过参数传递的方式调用另一台机器(服务器)上的一个函数或方法(可以统称为服务)并得到返回的结果。
RPC会隐藏底层的通讯细节(不需要直接处理Socket通讯或Http通讯)。
客户端发起请求,服务器返回响应(类似于Http的工作方式)RPC在使用形式上像调用本地函数(或方法)一样去调用远程的函数(或方法)。
2 为什么我们要用RPC?
RPC 的主要目标是让构建分布式应用更容易,在提供强大的远程调用能力时不损失本地调用的语义简洁性。为实现该目标,RPC 框架需提供一种透明调用机制让使用者不必显式的区分本地调用和远程调用。

3 RPC需要解决的三个问题
RPC要达到的目标:远程调用时,要能够像本地调用一样方便,让调用者感知不到远程调用的逻辑。

Call ID映射。我们怎么告诉远程机器我们要调用哪个函数呢?在本地调用中,函数体是直接通过函数指针来指定的,我们调用具体函数,编译器就自动帮我们调用它相应的函数指针。但是在远程调用中,是无法调用函数指针的,因为两个进程的地址空间是完全不一样。所以,在RPC中,所有的函数都必须有自己的一个ID。这个ID在所有进程中都是唯一确定的。客户端在做远程过程调用时,必须附上这个ID。然后我们还需要在客户端和服务端分别维护一个 {函数 <–> Call ID} 的对应表。两者的表不一定需要完全相同,但相同的函数对应的Call ID必须相同。当客户端需要进行远程调用时,它就查一下这个表,找出相应的Call ID,然后把它传给服务端,服务端也通过查表,来确定客户端需要调用的函数,然后执行相应函数的代码。
序列化和反序列化。客户端怎么把参数值传给远程的函数呢?在本地调用中,我们只需要把参数压到栈里,然后让函数自己去栈里读就行。但是在远程过程调用时,客户端跟服务端是不同的进程,不能通过内存来传递参数。甚至有时候客户端和服务端使用的都不是同一种语言(比如服务端用C++,客户端用Java或者Python)。这时候就需要客户端把参数先转成一个字节流,传给服务端后,再把字节流转成自己能读取的格式。这个过程叫序列化和反序列化。同理,从服务端返回的值也需要序列化反序列化的过程。
网络传输。远程调用往往是基于网络的,客户端和服务端是通过网络连接的。所有的数据都需要通过网络传输,因此就需要有一个网络传输层。网络传输层需要把Call ID和序列化后的参数字节流传给服务端,然后再把序列化后的调用结果传回客户端。只要能完成这两者的,都可以作为传输层使用。因此,它所使用的协议其实是不限的,能完成传输就行。尽管大部分RPC框架都使用TCP协议,但其实UDP也可以,而gRPC干脆就用了HTTP2。Java的Netty也属于这层的东西。
4 实现高可用RPC框架需要考虑到的问题
既然系统采用分布式架构,那一个服务势必会有多个实例,要解决如何获取实例的问题。所以需要一个服务注册中心,比如在Dubbo中,就可以使用Zookeeper作为注册中心,在调用时,从Zookeeper获取服务的实例列表,再从中选择一个进行调用;
如何选择实例呢?就要考虑负载均衡,例如dubbo提供了4种负载均衡策略;
如果每次都去注册中心查询列表,效率很低,那么就要加缓存;
客户端总不能每次调用完都等着服务端返回数据,所以就要支持异步调用;
服务端的接口修改了,老的接口还有人在用,这就需要版本控制;
服务端总不能每次接到请求都马上启动一个线程去处理,于是就需要线程池;
5 理论结构模型

RPC 服务端通过RpcServer去导出(export)远程接口方法,而客户端通过RpcClient去导入(import)远程接口方法。客户端像调用本地方法一样去调用远程接口方法,RPC 框架提供接口的代理实现,实际的调用将委托给代理RpcProxy。代理封装调用信息并将调用转交给RpcInvoker去实际执行。在客户端的RpcInvoker通过连接器RpcConnector去维持与服务端的通道RpcChannel,并使用RpcProtocol执行协议编码(encode)并将编码后的请求消息通过通道发送给服务端。

RPC 服务端接收器RpcAcceptor接收客户端的调用请求,同样使用RpcProtocol执行协议解码(decode)。

解码后的调用信息传递给RpcProcessor去控制处理调用过程,最后再委托调用给RpcInvoker去实际执行并返回调用结果。

主流的RPC框架
服务治理型
dubbo:是阿里巴巴公司开源的一个Java高性能优秀的服务框架,使得应用可通过高性能的 RPC 实现服务的输出和输入功能,可以和 Spring框架无缝集成。dubbo 已经与12年年底停止维护升级。
dubbox:是当当团队基于dubbo升级的一个版本。是一个分布式的服务架构,可直接用于生产环境作为SOA服务框架。dubbox资源链接
motan:是新浪微博开源的一个Java框架。它诞生的比较晚,起于2013年,2016年5月开源。Motan 在微博平台中已经广泛应用,每天为数百个服务完成近千亿次的调用。motan资源链接
腾讯Tars

阿里dubbo

开源buttonrpc

远程调用在平时开发中都经常会用到,一般常用的是http,webservice,rmi等 RPC 方式

http和rpc最大的区别:http是超文本传输协议,rpc更底层 是tcp的传输协议

远程调用的区别无非在于,通讯协议:http,tcp等等,序列号方式:json,xml,hessian,protobuf;json-rpc,基于http通讯的json序列号话的调用方式。

联系

http 也是rpc的一种方式,http和rpc的关系更像is-a 的关系。

区别

性能:

http 传输包含除了自身参数,还有一系列的头信息,request和response 信息。rpc的这些信息占用的字节较少

效率

http每次都需要三次握手,rpc则不需要 并且是长链接

每次访问要经过dns,lvs,ng,rpc只需要经过注册中心找到服务提供者即可。可以走内网。

负载均衡

http 需要经过ng或者lvs

rpc走自带的负载均衡服务。

总结如何选择:

http:对外不系统,简单高效的,系统设计初期。第三方api等等。

rpc:更多的是内部系统之间的交互,且对性能要求比较高的系统。

常用的rpc方式:

Grpc,gg的基于http2.0的传输协议,protobuf 序列化方式。

dubbo:阿里的远程调用协议,无非就是生产者,消费者,注册中心等等。具体写过小的demo而已,内部实现没有看过源码,有时间要研究研究

RSF:我司的一个内部使用的系统调用方式。生产者,消费者注册到统一的平台,可以指定协议列表,序列号方式是二进制,可以进行流控,监控,负载均衡,熔断,权限控制等等操作,还是非常方便的。具体细节有时间还是看看的。

先写这么多,还是希望有时间多看看原理性的东西,而不是搬运工,多一点自己的思考,形成自己的知识体系。

12、数据库

sqlite原理:https://www.kancloud.cn/kangdandan/sqlite/64326

SQL:structured query language 结构化查询语言。专门对数据库进行查找、增加、修改、删除、统计的操作语言。

CURD 增删查改 create update retrieve delete。

书写风格,关键字大小写都行,建议大写。表名大小写都行,但是在一些数据库中不区分大小写,建议小写。

(重要)基本语法。

查找

SELECT 字段1,字段2,字段3,… FROM 表名; python中返回值形如[(1, 502班, 小明, 男), (), ()]。

字段比较多时简写为 SELECT * FROM 表名; 由于数据库执行时会把*转换为字段再执行,性能极微小下降。

SELECT * FROM 表名 WHERE 字段1 = 过滤值,字典2=过滤值 ; where限定条件查找。

添加

INSERT 字段1,字段2,… INTO 表名 VALUES (1, “小明”, “男”);

简写 INSERT INTO 表名 VALUES (1, “小明”, “男”);

修改

UPDATE 表名 SET 字段1=新值,字段2=新值 WHERE 字段1 = 过滤值;

注意没有where条件限定行的话将会更新整张表。

删除

DELETE FROM 表名; 注意会删除整张表。

DELETE FROM 表名 WHERE 字段1 = 过滤值; 限定条件删除某些行。

创建表

CREATE TABLE 表名 {

字段类型 字段名 其它关键字(主键 备注),

INT id PRIMARY KEY,

VARCHAR(20) username

13、Hadoop大数据

1、Hadoop是什么

1.1、小故事版本的解释

小明接到一个任务:计算一个100M的文本文件中的单词的个数,这个文本文件有若干行,每行有若干个单词,每行的单词与单词之间都是以空格键分开的。对于处理这种100M量级数据的计算任务,小明感觉很轻松。他首先把这个100M的文件拷贝到自己的电脑上,然后写了个计算程序在他的计算机上执行后顺利输出了结果。

后来,小明接到了另外一个任务,计算一个1T(1024G)的文本文件中的单词的个数。再后来,小明又接到一个任务,计算一个1P(1024T)的文本文件中的单词的个数……

面对这样大规模的数据,小明的那一台计算机已经存储不下了,也计算不了这样大的数据文件中到底有多少个单词了。机智的小明上网百度了一下,他在百度的输入框中写下了:大数据存储和计算怎么办?按下回车键之后,出现了有关Hadoop的网页。

看了很多网页之后,小明总结一句话:Hadoop就是存储海量数据和分析海量数据的工具。

1.2、稍专业点的解释

Hadoop是由java语言编写的,在分布式服务器集群上存储海量数据并运行分布式分析应用的开源框架,其核心部件是HDFS与MapReduce。

   HDFS是一个分布式文件系统:引入存放文件元数据信息的服务器Namenode和实际存放数据的服务器Datanode,对数据进行分布式储存和读取。

MapReduce是一个计算框架:MapReduce的核心思想是把计算任务分配给集群内的服务器里执行。通过对计算任务的拆分(Map计算/Reduce计算)再根据任务调度器(JobTracker)对任务进行分布式计算。

1.3、记住下面的话:

   Hadoop的框架最核心的设计就是:HDFS和MapReduce。HDFS为海量的数据提供了存储,则MapReduce为海量的数据提供了计算。

 把HDFS理解为一个分布式的,有冗余备份的,可以动态扩展的用来存储大规模数据的大硬盘。

 把MapReduce理解成为一个计算引擎,按照MapReduce的规则编写Map计算/Reduce计算的程序,可以完成计算任务。

2、Hadoop能干什么

大数据存储:分布式存储

日志处理:擅长日志分析

ETL:数据抽取到oracle、mysql、DB2、mongdb及主流数据库

机器学习: 比如Apache Mahout项目

搜索引擎:Hadoop + lucene实现

数据挖掘:目前比较流行的广告推荐,个性化广告推荐

Hadoop是专为离线和大规模数据分析而设计的,并不适合那种对几个记录随机读写的在线事务处理模式。

实际应用:

(1)Flume+Logstash+Kafka+Spark Streaming进行实时日志处理分析

(2)酷狗音乐的大数据平台

3、怎么使用Hadoop

3.1、Hadoop集群的搭建

无论是在windows上装几台虚拟机玩Hadoop,还是真实的服务器来玩,说简单点就是把Hadoop的安装包放在每一台服务器上,改改配置,启动就完成了Hadoop集群的搭建。

3.2、上传文件到Hadoop集群

Hadoop集群搭建好以后,可以通过web页面查看集群的情况,还可以通过Hadoop命令来上传文件到hdfs集群,通过Hadoop命令在hdfs集群上建立目录,通过Hadoop命令删除集群上的文件等等。

3.3、编写map/reduce程序

通过集成开发工具(例如eclipse)导入Hadoop相关的jar包,编写map/reduce程序,将程序打成jar包扔在集群上执行,运行后出计算结果。

http://hadoop.apache.org/docs/r1.0.4/cn/quickstart.html

冒泡排序:

void BubbleSort(SORT_T * sort)

{

int i, j;

int temp;

//循环遍历整个数组

for (i = 0; i < sort->len - 1; i++)

{

//从最后的数据开始依次比较

for (j = sort->len - 1; j > i; j–)

{

//如果前面的数据大于后面的数据则调换

if (sort->arr[j - 1] > sort->arr[j])

{

temp = sort->arr[j - 1];

sort->arr[j - 1] = sort->arr[j];

sort->arr[j] = temp ;

}

}

}

}

选择排序:

void SelectionSort(SORT_T * sort)

{

int i, j, min;

int tmp;

//循环遍历整个数组

for (i = 0; i < sort->len - 1 ; i++)

{

min = i;//记录最小的下标

for ( j = i + 1; j < sort->len ; j ++ )

{

if (sort->arr[j] < sort->arr[min])

{

min = j;

}

}

if (min != i)

{

tmp = sort->arr[i];

sort->arr[i] = sort->arr[min];

sort->arr[min] = tmp;

}

}

}

插入排序:

void InsertSort(SORT_T* sort)

{

int i, j, tmp;

for (i = 1; i < sort->len; i++)

{

tmp = sort->arr[i];//将每次需要插入的元素保存到变量tmp中,

j = i - 1;//变量j表示要插入的位置,将当前位置序号为i的数插入序号i-1(即前一个数的位置)

while (j >= 0 && sort->arr[j] > tmp) //如果序号j元素大于变量tmp(需要插入的元素),则将序号为j的元素向后移,

{

sort->arr[j + 1] = sort->arr[j];

j–;//判断前一个数据是否需要向后移动,通过while找个一个比tmp值小的元素,

}

sort->arr[j + 1] = tmp;//再序号为j的元素进行插入

}

}

shell排序:

void ShellSort(SORT_T* sort)

{

int i, j;

int tmp, r;

for (r = sort->len / 2; r >= 1; r = r / 2) //化组排序,将数组分为多个序列

{

for ( i = r; i < sort->len; ++i)//按设置的间距r,分别比较对应的元素

{

tmp = sort->arr[i];

j = i - r;

while (j >= 0 && sort->arr[j] > tmp)

{

sort->arr[j + r] = sort->arr[j];

j = j - r;

}

sort->arr[j + r] = tmp;

}

}

}

DevOps(Development和Operations的组合词)

https://blog.csdn.net/javashareauthor/article/details/99429579

是一组过程、方法与系统的统称,用于促进开发(应用程序/软件工程)、技术运营和质量保障(QA)部门之间的沟通、协作与整合。它是一种重视“软件开发人员(Dev)”和“IT运维技术人员(Ops)”之间沟通合作的文化、运动或惯例。透过自动化“软件交付”和“架构变更”的流程,来使得构建、测试、发布软件能够更加地快捷、频繁和可靠。

实现DevOps需要什么?

硬性要求:工具上的准备

上文提到了工具链的打通,那么工具自然就需要做好准备。现将工具类型及对应的不完全列举整理如下:

代码管理(SCM):GitHub、GitLab、BitBucket、SubVersion

构建工具:Ant、Gradle、maven

自动部署:Capistrano、CodeDeploy

持续集成(CI):Bamboo、Hudson、Jenkins

配置管理:Ansible、Chef、Puppet、SaltStack、ScriptRock GuardRail

容器:Docker、LXC、第三方厂商如AWS

编排:Kubernetes、Core、Apache Mesos、DC/OS

服务注册与发现:Zookeeper、etcd、Consul

脚本语言:python、ruby、shell

日志管理:ELK、Logentries

系统监控:Datadog、Graphite、Icinga、Nagios

性能监控:AppDynamics、New Relic、Splunk

压力测试:JMeter、Blaze Meter、loader.io

预警:PagerDuty、pingdom、厂商自带如AWS SNS

HTTP加速器:Varnish

消息总线:ActiveMQ、SQS

应用服务器:Tomcat、JBoss

Web服务器:Apache、Nginx、IIS

数据库:MySQL、Oracle、PostgreSQL等关系型数据库;cassandra、mongoDB、redis等NoSQL数据库

项目管理(PM):Jira、Asana、Taiga、Trello、Basecamp、Pivotal Tracker

敏捷开发的四条原则

1、递增,而不是连续的:如果开发实践是真正的敏捷精神,那么交付的工作软件是一小部分一小部分递增的。不必等到一个阶段完全完成后才开始另一个,工作也不是向大的发布日期而努力。完成的工作,但并不是业务最终期限,驱动着敏捷交付。但敏捷精神也承认业务操纵着最后截止日期。
2、避免不必要的开销:如果实践仍然是真正的敏捷精神,那么团队就致力于尽可能多地减少项目计划和文档。与其讨论要做什么,然后再写下来,不如赶紧动手去做,否则,就是在浪费时间在工作的工作上。在工作对工作中,敏捷精神有利于实际的工——作交付工作软件。而且它也值面对面的交流通过邮件和其他书面文件。
3、协作:根据需求,团队成员一直与其它人进行交互,以及一些外部利益相关者。在敏捷教练世界中,整个团队的负责人Lisa Crispin能够解决所有问题,在问题出现之前 。真正的敏捷精神团队是自助的。他们分配需要做的工作。虽然每个成员承担的任务都在他们的专业技能范围内,他们还是需要与团队协作的。没有人的工作是孤立的,也没有团队本身是独立工作的。没有业务利益相关者,以及诸如用户体验方面的外部专家的重大投入,团队就不可能使项目向前发展,
4、说真话:为了保证真正的敏捷,团队探讨的与项目相关的一切都要是真实的。在一些至关重要的专业领域,如冲刺测试的编码技能,他们承认存在差距。关于实际生产力,他们的要讲事实;这也就是说,在y时间内,团队是否有能力做到x。他们承认错误。说真话是一项挑战,因为他们害怕承认缺点会让他们显得很弱。但敏捷精神知道说出事实需要勇气。承认问题需要信心,然后快速地去解决问题。

微服务架构:

单体架构

单体架构是最简单的软件架构,常用于传统的应用软件开发以及传统 Web 应用。传统 Web 应用,一般是将所有功能模块都打包(jar、war)在一个 Web 容器(JBoss、Tomcat)中部署、运行。随着业务复杂度增加、技术团队规模扩大,在一个单体应用中维护代码,会降低开发效率,即使是处理一个小需求,也需要将所有机器上的应用全部部署一遍,增加了运维的复杂度。

SOA 架构

当某一天使用单体架构发现很难推进需求的开发、以及日积月累的技术债时,很多企业会开始做单体服务的拆分,拆分的方式一般有水平拆分和垂直拆分。垂直拆分是把一个应用拆成松耦合的多个独立的应用,让应用可以独立部署,有独立的团队进行维护;水平拆分是把一些通用的,会被很多上层服务调用的模块独立拆分出去,形成一个共享的基础服务,这样拆分可以对一些性能瓶颈的应用进行单独的优化和运维管理,也在一定程度上防止了垂直拆分的重复造轮子。

SOA 也叫面向服务的架构,从单体服务到 SOA 的演进,需要结合水平拆分及垂直拆分。SOA 强调用统一的协议进行服务间的通信,服务间运行在彼此独立的硬件平台但是需通过统一的协议接口相互协作,也即将应用系统服务化。举个易懂的例子,单体服务如果相当于一个快餐店,所有的服务员职责都是一样的,又要负责收银结算,又要负责做汉堡,又要负责端盘子,又要负责打扫,服务员之间不需要有交流,用户来了后,服务员从前到后负责到底。SOA 相当于让服务员有职责分工,收银员负责收银,厨师负责做汉堡,保洁阿姨负责打扫等,所有服务员需要用同一种语言交流,方便工作协调。

微服务也是一种服务化,不过其和 SOA 架构的服务化概念也是有区别的,可以从以下几个关键字来理解:

松耦合:每个微服务内部都可以使用 DDD(领域驱动设计)的思想进行设计领域模型,服务间尽量减少同步的调用,多使用消息的方式让服务间的领域事件来进行解耦。

轻量级协议:Dubbo 是 SOA 的开源的标准实现之一,类似的还有像 gRPC、Thrift 等。微服务更倾向于使用 Restful 风格的 API,轻量级的协议可以很好地支持跨语言开发的服务,可能有的微服务用 Java 语言实现,有的用 Go 语言,有的用 C++,但所有的语言都可以支持 Http 协议通信,所有的开发人员都能理解 Restful 风格 API 的含义。

高度自治和持续集成:从底层的角度来说,SOA 更加倾向于基于虚拟机或者服务器的部署,每个应用都部署在不同的机器上,一般持续集成工具更多是由运维团队写一些 Shell 脚本以及提供基于共同协议(比如 Dubbo 管理页面)的开发部署页面。微服务可以很好得和容器技术结合,容器技术比微服务出现得晚,但是容器技术的出现让微服务的实施更加简便,目前 Docker 已经成为很多微服务实践的基础容器。因为容器的特色,所以一台机器上可以部署几十个、几百个不同的微服务。如果某个微服务流量压力比其他微服务大,可以在不增加机器的情况下,在一台机器上多分配一些该微服务的容器实例。同时,因为 Docker 的容器编排社区日渐成熟,类似 Mesos、Kubernetes 及 Docker 官方提供的 Swarm 都可以作为持续集成部署的技术选择。

其实从架构的演进的角度来看,整体的演进都是朝着越来越轻量级、越来越灵活的应用方向发展,甚至到近两年日渐成熟起来的 Serverless(无服务)架构。从单体服务到分层的服务,再到面向服务、再到微服务甚至无服务,对于架构的挑战是越来越大。

https://blog.csdn.net/valada/article/details/80993643

DDD(Domain-Drive Design)领域驱动设计

使用领域驱动设计的理念,工程师们的关注点需要从 CRUD 思维中跳出来,更多关注通用语言的设计、实体以及值对象的设计。至于数据仓库,会有更多样化的选择。分布式系统中数据存储服务是基础,微服务的领域拆分、领域建模可以让数据存储方案的选择更具灵活性。

Redis C++编程实例
https://www.cnblogs.com/fire909090/p/6770059.html

https://www.cnblogs.com/chinxi/p/6184885.html

Windows下配置Git

1、从git官网下载windows版本的git:http://git-scm.com/downloads

2、一般使用默认设置即可:一路next,git安装完毕!

3、但是如果这时你打开windows的cmd,在里面打git命令会提示“不是内部或外部命令,也不是可运行的程序”,想要直接在windows的cmd里使用git命令要多加如下两步

3.1、找到git安装路径中bin的位置,如:D:\Program Files\Git\bin

    找到git安装路径中git-core的位置,如:D:\Program Files\Git\libexec\git-core;

    注:"D:\Program Files\Git\"是安装路径,可能与你的安装路径不一样,要按照你自己的路径替换"D:\Program Files\Git\"

3.2、右键“计算机”->“属性”->“高级系统设置”->“环境变量”->在下方的“系统变量”中找到“path”->选中“path”并选择“编辑”->将3.1种找到的bin和git-core路径复制到其中->保存并退出

    注:“path”中,每个路径之间要以英文输入状态下的分号——“;”作为间隔

c++使用MYSQL教程
1、安装mysql_essential.msi

2、添加包含路径include,库目录

3、写代码,如TestMySql

https://blog.csdn.net/xiyangsu2617/article/details/83243948

ServiceMesh(服务网格)概念在社区里头非常火,有人提出2018年是ServiceMesh年,还有人提出ServiceMesh是下一代的微服务架构基础。作为架构师,如果你现在还不了解ServiceMesh的话,是否感觉有点落伍了?那么到底什么是ServiceMesh?它诞生的背景是什么?它解决什么问题?企业是否适合引入ServiceMesh?根据近年在一线互联网企业的实践和思考,从个人视角出发,我为大家一一解答这些问题。

微服务架构的核心技术问题

在业务规模化和研发效能提升等因素的驱动下,从单块应用向微服务架构的转型(如下图所示),已经成为很多企业(尤其是互联网企业)数字化转型的趋势。

图片发自简书App

在微服务模式下,企业内部服务少则几个到几十个,多则上百个,每个服务一般都以集群方式部署,这时自然产生两个问题(如下图所示):

图片发自简书App
一、服务发现:服务的消费方(Consumer)如何发现服务的提供方(Provider)?

二、负载均衡:服务的消费方如何以某种负载均衡策略访问集群中的服务提供方实例?

作为架构师,如果你理解了这两个问题,可以说就理解了微服务架构在技术上的最核心问题。

三种服务发现模式

服务发现和负载均衡并不是新问题,业界其实已经探索和总结出一些常用的模式,这些模式的核心其实是代理(Proxy,如下图所以),以及代理在架构中所处的位置,

图片发自简书App

在服务消费方和服务提供方之间增加一层代理,由代理负责服务发现和负载均衡功能,消费方通过代理间接访问目标服务。根据代理在架构上所处的位置不同,当前业界主要有三种不同的服务发现模式:

模式一:传统集中式代理

图片发自简书App

这是最简单和传统做法,在服务消费者和生产者之间,代理作为独立一层集中部署,由独立团队(一般是运维或框架)负责治理和运维。常用的集中式代理有硬件负载均衡器(如F5),或者软件负载均衡器(如Nginx),F5(4层负载)+Nginx(7层负载)这种软硬结合两层代理也是业内常见做法,兼顾配置的灵活性(Nginx比F5易于配置)。

这种方式通常在DNS域名服务器的配合下实现服务发现,服务注册(建立服务域名和IP地址之间的映射关系)一般由运维人员在代理上手工配置,服务消费方仅依赖服务域名,这个域名指向代理,由代理解析目标地址并做负载均衡和调用。

国外知名电商网站eBay,虽然体量巨大,但其内部的服务发现机制仍然是基于这种传统的集中代理模式,国内公司如携程,也是采用这种模式。

模式二:客户端嵌入式代理

图片发自简书App

这是很多互联网公司比较流行的一种做法,代理(包括服务发现和负载均衡逻辑)以客户库的形式嵌入在应用程序中。这种模式一般需要独立的服务注册中心组件配合,服务启动时自动注册到注册中心并定期报心跳,客户端代理则发现服务并做负载均衡。

Netflix开源的Eureka(注册中心)[附录1]和Ribbon(客户端代理)[附录2]是这种模式的典型案例,国内阿里开源的Dubbo也是采用这种模式。

模式三:主机独立进程代理

这种做法是上面两种模式的一个折中,代理既不是独立集中部署,也不嵌入在客户应用程序中,而是作为独立进程部署在每一个主机上,一个主机上的多个消费者应用可以共用这个代理,实现服务发现和负载均衡,如下图所示。这个模式一般也需要独立的服务注册中心组件配合,作用同模式二。

图片发自简书App

Airbnb的SmartStack[附录3]是这种模式早期实践产品,国内公司唯品会对这种模式也有探索和实践。

三种服务发现模式的比较

上面介绍的三种服务发现模式各有优劣,没有绝对的好坏,可以认为是三种不同的架构风格,在不同的公司都有成功实践。下表总结三种服务发现模式的优劣比较,业界案例和适用场景建议,供架构师选型参考:

图片发自简书App
服务网格ServiceMesh

所谓的ServiceMesh,其实本质上就是上面提到的模式三~主机独立进程模式,这个模式其实并不新鲜,业界(国外的Airbnb和国内的唯品会等)早有实践,那么为什么现在这个概念又流行起来了呢?我认为主要原因如下:

上述模式一和二有一些固有缺陷,模式一相对比较重,有单点问题和性能问题;模式二则有客户端复杂,支持多语言困难,无法集中治理的问题。模式三是模式一和二的折中,弥补了两者的不足,它是纯分布式的,没有单点问题,性能也OK,应用语言栈无关,可以集中治理。

微服务化、多语言和容器化发展的趋势,企业迫切需要一种轻量级的服务发现机制,ServiceMesh正是迎合这种趋势诞生,当然这还和一些大厂(如Google/IBM等)的背后推动有关。

模式三(ServiceMesh)也被形象称为边车(Sidecar)模式,如下图,早期有一些摩托车,除了主驾驶位,还带一个边车位,可以额外坐一个人。在模式三中,业务代码进程(相当于主驾驶)共享一个代理(相当于边车),代理除了负责服务发现和负载均衡,还负责动态路由、容错限流、监控度量和安全日志等功能,这些功能是具体业务无关的,属于跨横切面关注点(Cross-Cutting Concerns)范畴。

图片发自简书App
在新一代的ServiceMesh架构中(下图上方),服务的消费方和提供方主机(或者容器)两边都会部署代理SideCar。ServiceMesh比较正式的术语也叫数据面板(DataPlane),与数据面板对应的还有一个独立部署的控制面板(ControlPlane),用来集中配置和管理数据面板,也可以对接各种服务发现机制(如K8S服务发现)。术语数据面板和控制面板,估计是偏网络SDN背景的人提出来的。

图片发自简书App
上图左下角,每个主机上同时居住了业务逻辑代码(绿色表示)和代理(蓝色表示),服务之间通过代理发现和调用目标服务,形成服务之间的一种网络状依赖关系,控制面板则可以配置这种依赖调用关系,也可以调拨路由流量。如果我们把主机和业务逻辑剥离,就出现一种网格状架构(上图右下角),服务网格由此得名。

图片发自简书App

Istio[附录4]是Google/IBM等大厂支持和推进的一个ServiceMesh标准化工作组,上图是Istio给出的ServiceMesh参考架构。Istio专注在控制面板的架构、功能、以及控制面板和数据面板之间API的标准化,它的控制面板功能主要包括:

Istio-Manager:负责服务发现,路由分流,熔断限流等配置数据的管理和下发

Mixer:负责收集代理上采集的度量数据,进行集中监控

Istio-Auth:负责安全控制数据的管理和下发

Envoy[附录5]是目前Istio主力支持的数据面板代理,其它主流代理如nginx/kong等也正在陆续加入这个阵营。kubernetes是目前Isito主力支持的容器云环境。

我的建议

目前我本人并不特别看好ServiceMesh,也不是特别建议企业在生产上试水ServiceMesh,主要原因如下:

ServiceMesh其实并不是什么新东西,本质就是上面提到的服务发现模式三~主机独立进程模式,这个模式很早就有公司在探索和实践,但是一直没有普遍流行起来,说明这个模式也是存在落地挑战的。从表面上看,模式三是模式一和模式二的折中,同时解决了模式一和模式二存在的问题,但是在每个主机上独立部署一个代理进程,是有很大运维管理开销的,一方面是规模化部署的问题(考虑服务很多,机器也很多的场景);另一方面是如何监控治理的问题,代理挂了怎么办?你的团队是否具备自动化运维和监控的能力?另外开发人员在服务调试的时候,会依赖于这个独立的代理,调试排错比较麻烦,这个问题怎么解决?

Istio的确做了一些标准化工作,但是没有什么特别的创新,可是说换汤不换药,就是把模式三规范化和包装了一下。透过现象看本质,Google/IBM等行业大厂在背后推Isito/ServiceMesh,背后有一些市场利益诉求考虑,例如Google要推进它的kubernates和公有云生态。

ServiceMesh在年初声音比较大,最近渐渐安静下来,我听到国内只有一些大厂(华为,新浪微博,蚂蚁金服等)在试水,实际生产级落地的案例聊聊无几。大多数企业对ServiceMesh只是观望,很多架构师对ServiceMesh实际落地都存在疑虑。

所以我的个人建议,对于大部分企业(一般运维和研发能力不是特别强),采用模式一~集中代理模式就足够了。这个模式比较传统不新鲜,但是在很多一线企业已经切实落地,我甚至认为,除了一些大厂,大部分中小企业的服务发现架构采用的就是集中代理。我本人经历过三家互联网公司,大的有eBay,中等有携程,小的有拍拍贷,都是采用集中式代理模式,而且玩得都很好。我的架构理念很简单,对于生产级应用,不追新,老实采用大部分企业落地过的方案。

模式一的最大好处是集中治理,应用不侵入,语言栈无关,另外因为模式一是集中部署的,不像模式三是分布式部署,所以模式一的运维开销也远小于模式三。对于模式一,大家最大的顾虑是性能和单点问题,其实性能还是OK的,如果架构和容量规划合理的话,实际生产中经过集中代理的性能开销一般可以控制在小于10个ms,eBay和携程等大流量企业的成功实践已经验证了这点。单点问题一般建议采用两层负载结构,例如硬件F5+软件nginx两层负载,F5以主从HA部署,nginx则以集群多实例部署,这种架构兼顾了高可用和配置的灵活性。

另外,模式一还可以和服务注册中心结合,从而降低手工配置的复杂性,实现DevOps研发自助部署,一种方案如下图所示:

图片发自简书App

服务启动时自动注册到服务注册中心并定期报心跳,Proxy则定期到服务注册中心同步实例。这种方式下,不需要为每个服务申请一个域名,只需一个泛域名即可,消费者访问服务时采用服务名+泛域名即可,整个服务上线流程可以做到DevOps研发自助。目前社区流行的一些开源代理如traefik[附录7]和kong[附录8]等都支持和多种服务注册中心(Consul/Eureka/Etcd/Zookeeper等)进行集成。目前这种方案在拍拍贷有初步成功实践,采用kong[附录7]和自研服务注册中心Radar[附录8],同时和容器云调度平台配合,实现了研发全自助式发布上线。

结论

  1. 服务注册发现和负载均衡是微服务架构在技术上的根本问题,解决的办法是采用代理Proxy。根据代理在架构上的位置不同,服务发现代理一般有三种模式:

模式一:集中式代理

模式二:客户端嵌入式代理

模式三:主机独立进程代理 这三种模式没有绝对的好还之分,只是三种不同的架构风格,各有优劣和适用场景,在不同企业都有成功落地案例。

  1. ServiceMesh本质上就是模式三~主机独立进程代理,它结合了模式一和模式二的优势,但是分布式部署运维管理开销大。Istio对ServiceMesh的架构、功能和API进行了标准化。

  2. ServiceMesh还在演进中,生产落地仍有挑战,一般企业不建议生产级使用。集中式代理最成熟,对于一般中小企业,建议从集中式代理开始,等达到一定规模和具备一定的研发运维能力,再根据需要考虑其它服务发现模式。

  3. 架构师不要盲目追新,在理解微服务架构原理的基础上,可以学习和试点新技术,但是对于生产级应用,应该以成熟稳定,有大规模落地案例作为选型第一准则。

K8S
kubernetes首先,他是一个全新的基于容器技术的分布式架构领先方案。Kubernetes(k8s)是Google开源的容器集群管理系统(谷歌内部:Borg)。在Docker技术的基础上,为容器化的应用提供部署运行、资源调度、服务发现和动态伸缩等一系列完整功能,提高了大规模容器集群管理的便捷性。

https://blog.csdn.net/metheir/article/details/84452582
https://www.jianshu.com/p/707c8ebb8616

虚拟机:
1、安装vmware后,打开虚拟机,选择之前保存的*.ovf文件。
2、输入root、root登录。
3、虚拟机设置,选项,共享文件夹。添加本地文件夹。
4、cd …/
5、ls 查看共享文件夹下的文件。

Makefile 简介
Makefile 是一种常用于编译的脚本语言。它可以更好更方便的管理你的项目的代码编译,节约编译时间(没改动的文件不编译)。
注意 Makefile 文件命令必须是 Makefile 或者 makefile,并使用 make 命令编译。

范例1:
ALL : hello.out #ALL 表示最终生成的目标文件

hello.out : hello.c #hello.out : 依赖于 hello.c
gcc hello.c -o hello.out #命令

范例2:
SRC = $(wildcard ./*.c) #定义变量
OBJ = $(patsubst %.c, %.o, $(SRC))

ALL : hello.out

hello.out : $(OBJ)
gcc $(OBJ) -o hello.out

$(OBJ) : $(SRC)
gcc -c $(SRC) -o $(OBJ)

范例3:
SRC = $(wildcard ./*.c)
OBJ = $(patsubst %.c, %.o, $(SRC))

ALL : hello.out

hello.out : $(OBJ)
gcc $< -o $@

$(OBJ) : $(SRC) #源 OBJ : 依赖于 SRC
gcc -c $< -o $@ # $<表示当前语句中的依赖项 $@表示当前语句中的源

.PHONY: clean
clean:
-rm -f $(OBJS)

二、1个规则

  1. 语法规则
    目标…: 依赖…
    命令1
    命令2

  2. 目标
    目标即要生成的文件。如果目标文件的更新时间晚于依赖文件的更新时间,则说明依赖文件没有改动,目标文件不需要重新编译。否则重新编译并更新目标。

  3. 依赖
    即目标文件由哪些文件生成。如果依赖条件中存在不存在的依赖条件,则会寻找其它规则是否可以产生依赖条件。

例如:规则一是生成目标 hello.out 需要使用到依赖条件 hello.o,但是 hello.o 不存在。则 Makefile 会寻找到一个生成 hello.o 的规则二并执行。

  1. 命令
    即通过执行该命令,由依赖文件生成目标文件。

注意每条命令前必须有且仅有一个 tab 保持缩进,这是语法要求。

  1. ALL
    Makefile 文件默认只生成第一个目标文件即完成编译,但是我们可以通过 “ALL” 指定需要生成的目标文件。

  2. 示例
    针对以上所说的,先写一个示例让大家了解一下,首先准备一个 hello.c:

#include

int main()
{
printf(“Hello World !\n”);
return 0;
}
然后写一个 Makefile

ALL: hello.out

hello.out: hello.c
gcc hello.c -o hello.out
编译并执行:

$ make
gcc hello.c -o hello.out
$ ./hello.out
Hello World !

三、2个函数

  1. wildcard
    例如

SRC = $(wildcard ./*.c)
匹配目录下所有的 .c 文件,并将其赋值给 SRC 变量。

  1. patsubst
    pat 是 pattern 的缩写,subst 是 substring 的缩写。例如

OBJ = $(patsubst %.c, %.o, $(SRC))
这个函数有三个参数,意思是取出 SRC 中所有的值,然后将 “.c” 替换为 “.o”,最后赋值给 OBJ 变量。

  1. 示例
    通过上面两个函数,加入我们目录下有很多个 “.c” 后缀的源文件,就不需要写很多条规则语句了,而是可以像下面这样写

SRC = $(wildcard *.c)
OBJ = $(patsubst %.c, %.o, $(SRC))

ALL: hello.out

hello.out: $(OBJ)
gcc $(OBJ) -o hello.out

$(OBJ): $(SRC)
gcc -c $(SRC) -o $(OBJ)
这里我们先将所有的 “.c” 文件编译为 “.o” 文件,这样后面更改某个 “.c” 文件时,其它的 “.c” 文件将不再编译,而只是编译有更改的 “.c” 文件,可以大大节约大项目中的编译速度。

四、3个变量
Makefile 中也有一些已经定义好的常用变量,这里介绍其中常用的3个。

  1. $@
    表示规则中目标,例如 hello.out。

  2. $<
    表示规则中的第一个依赖条件,例如 hello.c

  3. $^
    表示规则中的所有依赖条件,由于我们示例中都只有一个依赖条件,这种情况下 $^ 和 $< 区别不大。

  4. 示例
    使用这些变量替换上面写的 Makefile,即是:

SRC = $(wildcard *.c)
OBJ = $(patsubst %.c, %.o, $(SRC))

ALL: hello.out

hello.out: $(OBJ)
gcc $< -o $@

$(OBJ): $(SRC)
gcc -c $< -o $@

五、其它常用功能

  1. 代码清理 clean
    我们可以编译一条属于自己的 clean 语句,来清理 make 命令所产生的所有文件。例如

SRC = $(wildcard *.c)
OBJ = $(patsubst %.c, %.o, $(SRC))

ALL: hello.out

hello.out: $(OBJ)
gcc $< -o $@

$(OBJ): $(SRC)
gcc -c $< -o $@

clean:
-rm -rf $(OBJ) hello.out
这样我们就可以使用 clean 命令来清理生成的文件了:

$ ls
hello.c hello.o hello.out Makefile
$ make clean
rm -rf hello.o hello.out
$ ls
hello.c Makefile

  1. 伪目标 .PHONY
    上面我们写了一个 clean 语句,使得我们执行 “make clean” 命令的时候,可以清理我们生成的文件。

但是假如还存在一个文件名就是 clean 文件,那么我们再执行 “make clean” 命令的时候就只是显示

$ make clean
make: `clean’ is up to date.
解决方法就是我们使用伪目标,这样就可以避免出现上面的问题了,例如:

SRC = $(wildcard *.c)
OBJ = $(patsubst %.c, %.o, $(SRC))

ALL: hello.out

hello.out: $(OBJ)
gcc $< -o $@

$(OBJ): $(SRC)
gcc -c $< -o $@

clean:
-rm -rf $(OBJ) hello.out

.PHONY: clean ALL

gcc 和 g++的区别

简单来说,gcc与g++都是GNU(组织)的一个编译器。需要注意以下几点:

gcc与g++都可以编译c代码与c++代码。但是:后缀为.c的,gcc把它当做C程序,而g++当做是C++程序;后缀为.cpp的,两者都会认为是C++程序。

编译阶段,g++会调用gcc,对于c++代码,两者是等价的,但是因为gcc命令不能自动和C++程序使用的库联接,所以通常用g++来完成链接。

编译可以用gcc/g++,而链接可以用g++或者gcc -lstdc++。因为gcc命令不能自动和C++程序使用的库联接(当然可以选择手动链接,使用命令如下),所以通常使用g++来完成联接。但在编译阶段,g++会自动调用gcc,二者等价。

gcc main.cpp -lstdc++
gcc编译的四个步骤, 以最简单的hello.c为例子

预处理-E/.i、编译-S/.s,汇编-c/.o,连接/可执行文件 gcc -E hello.c -o hello.i gcc -S hello.i -o hello.s gcc -c hello.s -o hello.o gcc hello.o -o hello

一步到位:gcc hello.c 这条命令隐含执行了 (1)预处理 (2)编译 (3)汇编 (4)链接 这里未指定输出文件,默认输出为a.out gcc编译C源码有四个步骤: 预处理 ----> 编译 ----> 汇编 ----> 链接 现在我们就用gcc的命令选项来逐个剖析gcc过程。 1)预处理(Pre-processing) 在该阶段,编译器将C源代码中的包含的头文件如stdio.h添加进来 参数:”-E” 用法:gcc -E hello.c -o hello.i 作用:将hello.c预处理输出hello.i文件。 2)编译(Compiling) 第二步进行的是编译阶段,在这个阶段中,gcc首先要检查代码的规范性、是否有语法错误等,以确定代码的实际要做的工作,在检查无误后,gcc把代码翻译成汇编语言。 参数:”-S” 用法:gcc –S hello.i –o hello.s 作用:将预处理输出文件hello.i汇编成hello.s文件。 3)汇编(Assembling) 汇编阶段是把编译阶段生成的”.s”文件转成二进制目标代码“.o”文件 参数:“-c” 用法:gcc –c hello.s –o hello.o 作用:将汇编输出文件hello.s编译输出hello.o文件。 4)链接(Link) 在成功编译之后,就进入了链接阶段。 用法:gcc hello.o –o hello 作用:将编译输出文件hello.o链接成最终可执行文件hello。 运行该可执行文件,出现正确的结果如下。 >>> ./hello Hello World!

C++11包含大量的新特性:包含lambda表达式,类型推导keyword : auto、decltype,和模板的大量改进。

decltype实际上有点像auto的反函数,auto能够让你声明一个变量。而decltype则能够从一个变量或表达式中得到类型

nullptr是为了解决原来C++中NULL的二义性问题而引进的一种新的类型,由于NULL实际上代表的是0,

简化的for循环,能够用于遍历数组、容器、string以及由begin和end函数定义的序列(即有Iterator),for (auto p : m)

lambda表达式,能够用于创建并定义匿名的函数对象,以简化编程工作。Lambda的语法例如以下: [函数对象參数](操作符重载函数參数)->返回值类型{函数体}

vector iv{5, 4, 3, 2, 1};
int a = 2, b = 1;

for_each(iv.begin(), iv.end(), [b](int &x){cout<<(x + b)<

for_each(iv.begin(), iv.end(), [=](int &x){x *= (a + b);}); // (2)

for_each(iv.begin(), iv.end(), [=](int &x)->int{return x * (a + b);});// (3)

[]内的參数指的是Lambda表达式能够取得的全局变量。(1)函数中的b就是指函数能够得到在Lambda表达式外的全局变量,假设在[]中传入=的话,即是能够取得全部的外部变量,如(2)和(3)Lambda表达式

()内的參数是每次调用函数时传入的參数。

->后加上的是Lambda表达式返回值的类型。如(3)中返回了一个int类型的变量

变长參数的模板,C++11中引入了变长參数模板,所以发明了新的数据类型:tuple,tuple是一个N元组。能够传入1个, 2个甚至多个不同类型的数据

auto t1 = make_tuple(1, 2.0, “C++ 11”);
auto t2 = make_tuple(1, 2.0, “C++ 11”, {1, 0, 2});
避免了从前的pair中嵌套pair的丑陋做法。使得代码更加整洁

更加优雅的初始化方法,在引入C++11之前。仅仅有数组能使用初始化列表,其它容器想要使用初始化列表,仅仅能用下面方法:

int arr[3] = {1, 2, 3}
vector v(arr, arr + 3);
在C++11中,我们能够使用下面语法来进行替换:

int arr[3]{1, 2, 3};
vector iv{1, 2, 3};
map{ {1, “a”}, {2, “b”}};
string str{“Hello World”};
什么是智能指针?智能指针的原理

将基本类型指针封装为类对象指针(这个类肯定是个模板,以适应不同基本类型的需求),并在析构函数里编写delete语句删除指针指向的内存空间。

智能指针是一个类,这个类的构造函数中传入一个普通指针,析构函数中释放传入的指针。智能指针的类都是栈上的对象,所以当函数(或程序)结束时会自动被释放,

智能指针就是一种栈上创建的对象,函数退出时会调用其析构函数,这个析构函数里面往往就是一堆计数之类的条件判断,如果达到某个条件,就把真正指针指向的空间给释放了。

注意事项:

不能将指针直接赋值给一个智能指针,一个是类,一个是指针。

常用的智能指针

智能指针在C++11版本之后提供,包含在头文件中,shared_ptr、unique_ptr、weak_ptr

1)std::auto_ptr,有很多问题。 不支持复制(拷贝构造函数)和赋值(operator =),但复制或赋值的时候不会提示出错。所以可能会造成程序崩溃,比如

auto_ptr p1(new string (“auto”) ; //#1
auto_ptr p2; //#2
p2 = p1; //#3
在语句#3中,p2接管string对象的所有权后,p1的所有权将被剥夺。前面说过,这是好事,可防止p1和p2的析构函数试图刪同—个对象; 但如果程序随后试图使用p1,这将是件坏事,因为p1不再指向有效的数据。如果再访问p1指向的内容则会导致程序崩溃。

auto_ptr是C++98提供的解决方案,C+11已将将其摒弃,摒弃auto_ptr的原因,一句话总结就是:避免潜在的内存崩溃问题。

  1. C++11引入的unique_ptr, 也不支持复制和赋值,但比auto_ptr好,直接赋值会编译出错。实在想赋值的话,需要使用:std::move。例如:

std::unique_ptr p1(new int(5)) // #4
std::unique_ptr p2 = p1; // 编译会出错 //#5
std::unique_ptr p3 = std::move(p1); // 转移所有权, 现在那块内存归p3所有, p1成为无效的指针. //#6
编译器认为语句#5非法,因此,unique_ptr比auto_ptr更安全。

但unique_ptr还有更聪明的地方。 有时候,会将一个智能指针赋给另一个并不会留下危险的悬挂指针。当程序试图将一个 unique_ptr 赋值给另一个时,如果源 unique_ptr 是个临时右值,编译器允许这么做;如果源 unique_ptr 将存在一段时间,编译器将禁止这么做

unique_ptr pu1(new string (“hello world”));
unique_ptr pu2;
pu2 = pu1; // #1 not allowed
unique_ptr pu3;
pu3 = unique_ptr(new string (“You”)); // #2 allowed
其中#1留下悬挂的unique_ptr(pu1),这可能导致危害。而#2不会留下悬挂的unique_ptr,因为它调用 unique_ptr 的构造函数,该构造函数创建的临时对象在其所有权让给 pu3 后就会被销毁。这种随情况而已的行为表明,unique_ptr 优于允许两种赋值的auto_ptr 。

  1. C++11或boost的shared_ptr,基于引用计数的智能指针。可随意赋值,直到内存的引用计数为0的时候这个内存会被释放。

4)C++11或boost的weak_ptr,弱引用。 引用计数有一个问题就是互相引用形成环,这样两个指针指向的内存都无法释放。需要手动打破循环引用或使用weak_ptr。顾名思义,weak_ptr是一个弱引用,只引用,不计数。如果一块内存被shared_ptr和weak_ptr同时引用,当所有shared_ptr析构了之后,不管还有没有weak_ptr引用该内存,内存也会被释放。所以weak_ptr不保证它指向的内存一定是有效的,在使用之前需要检查weak_ptr是否为空指针。

智能指针的作用

C++程序设计中使用堆内存是非常频繁的操作,堆内存的申请和释放都由程序员自己管理。程序员自己管理堆内存可以提高了程序的效率,但是整体来说堆内存的管理是麻烦的,C++11中引入了智能指针的概念,方便管理堆内存。使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,野指针,程序发生异常时内存泄露等问题等,使用智能指针能更好的管理堆内存。

1、C和C++的区别

1)C是面向过程的语言,是一个结构化的语言,考虑如何通过一个过程对输入进行处理得到输出;C++是面向对象的语言,主要特征是“封装、继承和多态”。封装隐藏了实现细节,使得代码模块化;派生类可以继承父类的数据和方法,扩展了已经存在的模块,实现了代码重用;多态则是“一个接口,多种实现”,通过派生类重写父类的虚函数,实现了接口的重用。

2)C和C++动态管理内存的方法不一样,C是使用malloc/free,而C++除此之外还有new/delete关键字。

3)C++支持函数重载,C不支持函数重载

4)C++中有引用,C中不存在引用的概念

2、C++中指针和引用的区别

1)指针是一个新的变量,存储了另一个变量的地址,我们可以通过访问这个地址来修改另一个变量;

引用只是一个别名,还是变量本身,对引用的任何操作就是对变量本身进行操作,以达到修改变量的目的

2)引用只有一级,而指针可以有多级

3)指针传参的时候,还是值传递,指针本身的值不可以修改,需要通过解引用才能对指向的对象进行操作

引用传参的时候,传进来的就是变量本身,因此变量可以被修改

3、结构体struct和共同体union(联合)的区别

结构体:将不同类型的数据组合成一个整体,是自定义类型

共同体:不同类型的几个变量共同占用一段内存

1)结构体中的每个成员都有自己独立的地址,它们是同时存在的;

共同体中的所有成员占用同一段内存,它们不能同时存在;

2)sizeof(struct)是内存对齐后所有成员长度的总和,sizeof(union)是内存对齐后最长数据成员的长度、

结构体为什么要内存对齐呢?

1.平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常

2.硬件原因:经过内存对齐之后,CPU的内存访问速度大大提升。

4、#define和const的区别

1)#define定义的常量没有类型,所给出的是一个立即数;const定义的常量有类型名字,存放在静态区域

2)处理阶段不同,#define定义的宏变量在预处理时进行替换,可能有多个拷贝,const所定义的变量在编译时确定其值,只有一个拷贝。

3)#define定义的常量是不可以用指针去指向,const定义的常量可以用指针去指向该常量的地址

4)#define可以定义简单的函数,const不可以定义函数

5、重载overload,覆盖(重写)override,隐藏(重定义)overwrite,这三者之间的区别

1)overload,将语义相近的几个函数用同一个名字表示,但是参数列表(参数的类型,个数,顺序不同)不同,这就是函数重载,返回值类型可以不同

特征:相同范围(同一个类中)、函数名字相同、参数不同、virtual关键字可有可无

2)override,派生类覆盖基类的虚函数,实现接口的重用,返回值类型必须相同

特征:不同范围(基类和派生类)、函数名字相同、参数相同、基类中必须有virtual关键字(必须是虚函数)

3)overwrite,派生类屏蔽了其同名的基类函数,返回值类型可以不同

特征:不同范围(基类和派生类)、函数名字相同、参数不同或者参数相同且无virtual关键字

6、new、delete、malloc、free之间的关系

new/delete,malloc/free都是动态分配内存的方式

1)malloc对开辟的空间大小严格指定,而new只需要对象名

2)new为对象分配空间时,调用对象的构造函数,delete调用对象的析构函数

既然有了malloc/free,C++中为什么还需要new/delete呢?

运算符是语言自身的特性,有固定的语义,编译器知道意味着什么,由编译器解释语义,生成相应的代码。

库函数是依赖于库的,一定程度上独立于语言的。编译器不关心库函数的作用,只保证编译,调用函数参数和返回值符合语法,生成call函数的代码。

malloc/free是库函数,new/delete是C++运算符。对于非内部数据类型而言,光用malloc/free无法满足动态对象都要求。new/delete是运算符,编译器保证调用构造和析构函数对对象进行初始化/析构。但是库函数malloc/free是库函数,不会执行构造/析构。

7、delete和delete[]的区别

delete只会调用一次析构函数,而delete[]会调用每个成员的析构函数

用new分配的内存用delete释放,用new[]分配的内存用delete[]释放

多态, 虚函数, 纯虚函数

多态:不同对象接收相同的消息产生不同的动作。多态包括 编译时多态和 运行时多态

运行时多态是:通过继承和虚函数来体现的。 编译时多态:运算符重载上。 封装可以隐藏实现细节,使得代码模块化;继承可以扩展已存在的代码模块(类);它们的目的都是为了——代码重用。多态也有代码重用的功能,还有解决项目中紧耦合的问题,提高程序的可扩展性。C++实现多态的机制很简单,在继承体系下,将父类的某个函数给成虚函数(即加上virtual关键字),在派生类中对这个虚函数进行重写,利用父类的指针或引用调用虚函数。通过指向派生类的基类指针或引用,访问派生类中同名覆盖成员函数。对于虚函数调用来说,每一个对象内部都有一个虚表指针,在构造子类对象时,执行构造函数中进行虚表的创建和虚表指针的初始化,该虚表指针被初始化为本类的虚表。所以在程序中,不管你的对象类型如何转换,但该对象内部的虚表指针是固定的,所以呢,才能实现动态的对象函数调用,这就是C++多态性实现的原理。 需要注意的几点总结(基类有虚函数): 1、每一个类都有虚表,单继承的子类拥有一张虚表,子类对象拥有一个虚表指针;若子类是多重继承(同时继承多个基类),则子类维护多张虚函数表(针对不同基类构建不同虚表),该子类的对象也将包含多个虚表指针。

2、虚表可以继承,如果子类没有重写虚函数,那么子类虚表中仍然会有该函数的地址,只不过这个地址指向的是基类的虚函数实现。如果基类3个虚函数,那么基类的虚表中就有三项(虚函数地址),派生类也会有虚表,至少有三项,如果重写了相应的虚函数,那么虚表中的地址就会改变,指向自身的虚函数实现。如果派生类有自己的虚函数,那么虚表中就会添加该项。 3、派生类的虚表中虚函数地址的排列顺序和基类的虚表中虚函数地址排列顺序相同。

第一:编译器在发现Father 类中有虚函数时,会自动为每个含有虚函数的类生成一份虚函数表,也叫做虚表,该表是一个一维数组,虚表里保存了虚函数的入口地址。

第二:编译器会在每个对象的前四个字节中保存一个虚表指针,即(vptr),指向对象所属类的虚表。在程序运行时的合适时机,根据对象的类型去初始化vptr,从而让vptr指向正确的虚表,从而在调用虚函数时,能找到正确的函数。

第三:所谓的合适时机,在派生类定义对象时,程序运行会自动调用构造函数,在构造函数中创建虚表并对虚表初始化。在构造子类对象时,会先调用父类的构造函数,此时,编译器只“看到了”父类,并为父类对象初始化虚表指针,令它指向父类的虚表;当调用子类的构造函数时,为子类对象初始化虚表指针,令它指向子类的虚表。

虚函数: 在基类中用virtual的成员函数。允许在派生类中对基类的虚函数重新定义。 基类的虚函数可以有函数体,基类也可以实例化。 虚函数要有函数体,否则编译过不去。 虚函数在子类中可以不覆盖。 构造函数不能是虚函数。

纯虚函数:基类中为其派生类保留一个名字,以便派生类根据需要进行定义。 包含一个纯虚函数的类是抽象类。 纯虚函数后面有 = 0; 抽象类不可以实例化。但可以定义指针。 如果派生类如果不是先基类的纯虚函数,则仍然是抽象类。 抽象类可以包含虚函数。

8、STL库用过吗?常见的STL容器有哪些?算法用过几个?

STL包括两部分内容:容器和算法

容器即存放数据的地方,比如array, vector,分为两类,序列式容器和关联式容器

序列式容器,其中的元素不一定有序,但是都可以被排序,比如vector,list,queue,stack,heap, priority-queue, slist

关联式容器,内部结构是一个平衡二叉树,每个元素都有一个键值和一个实值,比如map, set, hashtable, hash_set

算法有排序,复制等,以及各个容器特定的算法

迭代器是STL的精髓,迭代器提供了一种方法,使得它能够按照顺序访问某个容器所含的各个元素,但无需暴露该容器的内部结构,它将容器和算法分开,让二者独立设计。

9、const知道吗?解释一下其作用

const修饰类的成员变量,表示常量不可能被修改

const修饰类的成员函数,表示该函数不会修改类中的数据成员,不会调用其他非const的成员函数

const函数只能调用const函数,非const函数可以调用const函数

10、虚函数是怎么实现的

每一个含有虚函数的类都至少有有一个与之对应的虚函数表,其中存放着该类所有虚函数对应的函数指针(地址),

类的实例对象不包含虚函数表,只有虚指针;

派生类会生成一个兼容基类的虚函数表。

11、堆和栈的区别

1)栈 stack 存放函数的参数值、局部变量,由编译器自动分配释放

堆heap,是由new分配的内存块,由应用程序控制,需要程序员手动利用delete释放,如果没有,程序结束后,操作系统自动回收

2)因为堆的分配需要使用频繁的new/delete,造成内存空间的不连续,会有大量的碎片

3)堆的生长空间向上,地址越大,栈的生长空间向下,地址越小

12、关键字static的作用

1)函数体内: static 修饰的局部变量作用范围为该函数体,不同于auto变量,其内存只被分配一次,因此其值在下次调用的时候维持了上次的值

2)模块内:static修饰全局变量或全局函数,可以被模块内的所有函数访问,但是不能被模块外的其他函数访问,使用范围限制在声明它的模块内

3)类中:修饰成员变量,表示该变量属于整个类所有,对类的所有对象只有一份拷贝

4)类中:修饰成员函数,表示该函数属于整个类所有,不接受this指针,只能访问类中的static成员变量

注意和const的区别!!!const强调值不能被修改,而static强调唯一的拷贝,对所有类的对象

13、STL中map和set的原理(关联式容器)

map和set的底层实现主要通过红黑树来实现

红黑树是一种特殊的二叉查找树

1)每个节点或者是黑色,或者是红色

2)根节点是黑色

3) 每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!]

4)如果一个节点是红色的,则它的子节点必须是黑色的

5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。

特性4)5)决定了没有一条路径会比其他路径长出2倍,因此红黑树是接近平衡的二叉树。

14、#include #include “file.h” 的区别

前者是从标准库路径寻找

后者是从当前工作路径

15、什么是内存泄漏?面对内存泄漏和指针越界,你有哪些方法?

动态分配内存所开辟的空间,在使用完毕后未手动释放,导致一直占据该内存,即为内存泄漏。

方法:malloc/free要配套,对指针赋值的时候应该注意被赋值的指针是否需要释放;使用的时候记得指针的长度,防止越界

16、定义和声明的区别

声明是告诉编译器变量的类型和名字,不会为变量分配空间

定义需要分配空间,同一个变量可以被声明多次,但是只能被定义一次

17、C++文件编译与执行的四个阶段

1)预处理:根据文件中的预处理指令来修改源文件的内容

2)编译:编译成汇编代码

3)汇编:把汇编代码翻译成目标机器指令

4)链接:链接目标代码生成可执行程序

18、STL中的vector的实现,是怎么扩容的?

vector使用的注意点及其原因,频繁对vector调用push_back()对性能的影响和原因。

vector就是一个动态增长的数组,里面有一个指针指向一片连续的空间,当空间装不下的时候,会申请一片更大的空间,将原来的数据拷贝过去,并释放原来的旧空间。当删除的时候空间并不会被释放,只是清空了里面的数据。对比array是静态空间一旦配置了就不能改变大小。

vector的动态增加大小的时候,并不是在原有的空间上持续新的空间(无法保证原空间的后面还有可供配置的空间),而是以原大小的两倍另外配置一块较大的空间,然后将原内容拷贝过来,并释放原空间。在VS下是1.5倍扩容,在GCC下是2倍扩容。

在原来空间不够存储新值时,每次调用push_back方法都会重新分配新的空间以满足新数据的添加操作。如果在程序中频繁进行这种操作,还是比较消耗性能的。

19、STL中unordered_map和map的区别

map是STL中的一个关联容器,提供键值对的数据管理。底层通过红黑树来实现,实际上是二叉排序树和非严格意义上的二叉平衡树。所以在map内部所有的数据都是有序的,且map的查询、插入、删除操作的时间复杂度都是O(logN)。

unordered_map和map类似,都是存储key-value对,可以通过key快速索引到value,不同的是unordered_map不会根据key进行排序。unordered_map底层是一个防冗余的哈希表,存储时根据key的hash值判断元素是否相同,即unoredered_map内部是无序的。

20、C++的内存管理

在C++中,内存被分成五个区:栈、堆、自由存储区、静态存储区、常量区

栈:存放函数的参数和局部变量,编译器自动分配和释放

堆:new关键字动态分配的内存,由程序员手动进行释放,否则程序结束后,由操作系统自动进行回收

自由存储区:由malloc分配的内存,和堆十分相似,由对应的free进行释放

全局/静态存储区:存放全局变量和静态变量

常量区:存放常量,不允许被修改

21、 构造函数为什么一般不定义为虚函数?而析构函数一般写成虚函数的原因 ?

1、构造函数不能声明为虚函数

1)因为创建一个对象时需要确定对象的类型,而虚函数是在运行时确定其类型的。而在构造一个对象时,由于对象还未创建成功,编译器无法知道对象的实际类型,是类本身还是类的派生类等等

2)虚函数的调用需要虚函数表指针,而该指针存放在对象的内存空间中;若构造函数声明为虚函数,那么由于对象还未创建,还没有内存空间,更没有虚函数表地址用来调用虚函数即构造函数了

2、析构函数最好声明为虚函数

首先析构函数可以为虚函数,当析构一个指向派生类的基类指针时,最好将基类的析构函数声明为虚函数,否则可以存在内存泄露的问题。

如果析构函数不被声明成虚函数,则编译器实施静态绑定,在删除指向派生类的基类指针时,只会调用基类的析构函数而不调用派生类析构函数,这样就会造成派生类对象析构不完全。

子类析构时,要调用父类的析构函数吗?

析构函数调用的次序时先派生类后基类的。和构造函数的执行顺序相反。并且析构函数要是virtual的,否则如果用父类的指针指向子类对象的时候,析构函数静态绑定,不会调用子类的析构。

不用显式调用,会自动调用

22、静态绑定和动态绑定的介绍

静态绑定和动态绑定是C++多态性的一种特性

1)对象的静态类型和动态类型

静态类型:对象在声明时采用的类型,在编译时确定

动态类型:当前对象所指的类型,在运行期决定,对象的动态类型可变,静态类型无法更改

2)静态绑定和动态绑定

静态绑定:绑定的是对象的静态类型,函数依赖于对象的静态类型,在编译期确定

动态绑定:绑定的是对象的动态类型,函数依赖于对象的动态类型,在运行期确定

只有虚函数才使用的是动态绑定,其他的全部是静态绑定

23、 引用是否能实现动态绑定,为什么引用可以实现

可以。因为引用(或指针)既可以指向基类对象也可以指向派生类对象,这一事实是动态绑定的关键。用引用(或指针)调用的虚函数在运行时确定,被调用的函数是引用(或指针)所指的对象的实际类型所定义的。

24、深拷贝和浅拷贝的区别

深拷贝和浅拷贝可以简单的理解为:如果一个类拥有资源,当这个类的对象发生复制过程的时候,如果资源重新分配了就是深拷贝;反之没有重新分配资源,就是浅拷贝。

25、 什么情况下会调用拷贝构造函数(三种情况)

系统自动生成的构造函数:普通构造函数和拷贝构造函数 (在没有定义对应的构造函数的时候)

生成一个实例化的对象会调用一次普通构造函数,而用一个对象去实例化一个新的对象所调用的就是拷贝构造函数

调用拷贝构造函数的情形:

1)用类的一个对象去初始化另一个对象的时候

2)当函数的参数是类的对象时,就是值传递的时候,如果是引用传递则不会调用

3)当函数的返回值是类的对象或者引用的时候

举例:

#include
#include
using namespace std;
class A{
private:
int data;
public:
A(int i){ data = i;} //自定义的构造函数
A(A && a); //拷贝构造函数
int getdata(){return data;}
};
//拷贝构造函数
A::A(A && a){
data = a.data;
cout <<“拷贝构造函数执行完毕”< }
//参数是对象,值传递,调用拷贝构造函数
int getdata1(A a){
return a.getdata();
}
//参数是引用,引用传递,不调用拷贝构造函数
int getdata2(A &a){
return a.getdata();
}
//返回值是对象类型,会调用拷贝构造函数
A getA1(){
A a(0);
return a;
}
//返回值是引用类型,会调用拷贝构造函数,因为函数体内生成的对象是临时的,离开函数就消失
A& getA2(){
A a(0);
return a;
}
int main(){
A a1(1);
A b1(a1); //用a1初始化b1,调用拷贝构造函数
A c1=a1; //用a1初始化c1,调用拷贝构造函数
int i=getdata1(a1); //函数形参是类的对象,调用拷贝构造函数
int j=getdata2(a1); //函数形参类型是引用,不调用拷贝构造函数
A d1=getA1(); //调用拷贝构造函数
A e1=getA2(); //调用拷贝构造函数
return 0;
}
26、 C++的四种强制转换

类型转化机制可以分为隐式类型转换和显示类型转化(强制类型转换)

(new-type) expression

new-type (expression)

隐式类型转换比较常见,在混合类型表达式中经常发生;四种强制类型转换操作符:

static_cast、dynamic_cast、const_cast、reinterpret_cast

1)static_cast :编译时期的静态类型检查

static_cast < type-id > ( expression )

该运算符把expression转换成type-id类型,在编译时使用类型信息执行转换,在转换时执行必要的检测(指针越界、类型检查),其操作数相对是安全的

2)dynamic_cast:运行时的检查

用于在集成体系中进行安全的向下转换downcast,即基类指针/引用->派生类指针/引用

dynamic_cast是4个转换中唯一的RTTI操作符,提供运行时类型检查。

dynamic_cast如果不能转换返回NULL

dynamic_cast转为引用类型的时候转型失败会抛bad_cast

源类中必须要有虚函数,保证多态,才能使用dynamic_cast(expression)

3)const_cast

去除const常量属性,使其可以修改 ; volatile属性的转换

4)reinterpret_cast

通常为了将一种数据类型转换成另一种数据类型

27、调试程序的方法

windows下直接使用vs的debug功能

linux下直接使用gdb,我们可以在其过程中给程序添加断点,监视等辅助手段,监控其行为是否与我们设计相符

28、extern“C”作用

extern "C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言的进行编译,而不是C++的。

29、typdef和define区别

#define是预处理命令,在预处理是执行简单的替换,不做正确性的检查

typedef是在编译时处理的,它是在自己的作用域内给已经存在的类型一个别名

typedef (int*) pINT;

#define pINT2 int*

效果相同?实则不同!实践中见差别:pINT a,b;的效果同int *a; int *b;表示定义了两个整型指针变量。而pINT2 a,b;的效果同int *a, b;表示定义了一个整型指针变量a和整型变量b。

30、volatile关键字在程序设计中有什么作用

volatile是“易变的”、“不稳定”的意思。volatile是C的一个较为少用的关键字,它用来解决变量在“共享”环境下容易出现读取错误的问题。

变量如果加了voletile修饰,则会从内存中重新装载内容,而不是直接从寄存器中拷贝内容。

在本次线程内,当读取一个变量时,为了提高读取速度,编译器进行优化时有时会先把变量读取到一个寄存器中;以后,再读取变量值时,就直接从寄存器中读取;当变量值在本线程里改变时,会同时把变量的新值copy到该寄存器中,以保持一致。当变量因别的线程值发生改变,上面寄存器的值不会相应改变,从而造成应用程序读取的值和实际的变量值不一致。

volatile可以避免优化、强制内存读取的顺序,但是volatile并没有线程同步的语义,C++标准并不能保证它在多线程情况的正确性。C++11开始有一个很好用的库,那就是atomic类模板,在头文件中,多个线程对atomic对象进行访问是安全的,并且提供不同种类的线程同步。它默认使用的是最强的同步,所以我们就使用默认的就好。

31、引用作为函数参数以及返回值的好处

对比值传递,引用传参的好处:

1)在函数内部可以对此参数进行修改

2)提高函数调用和运行的效率(所以没有了传值和生成副本的时间和空间消耗)

如果函数的参数实质就是形参,不过这个形参的作用域只是在函数体内部,也就是说实参和形参是两个不同的东西,要想形参代替实参,肯定有一个值的传递。函数调用时,值的传递机制是通过“形参=实参”来对形参赋值达到传值目的,产生了一个实参的副本。即使函数内部有对参数的修改,也只是针对形参,也就是那个副本,实参不会有任何更改。函数一旦结束,形参生命也宣告终结,做出的修改一样没对任何变量产生影响。

用引用作为返回值最大的好处就是在内存中不产生被返回值的副本。

但是有以下的限制:

1)不能返回局部变量的引用。因为函数返回以后局部变量就会被销毁

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

3)可以返回类成员的引用,但是最好是const。因为如果其他对象可以获得该属性的非常量的引用,那么对该属性的单纯赋值就会破坏业务规则的完整性。

32、纯虚函数

纯虚函数是只有声明没有实现的虚函数,是对子类的约束,是接口继承

包含纯虚函数的类是抽象类,它不能被实例化,只有实现了这个纯虚函数的子类才能生成对象

普通函数是静态编译的,没有运行时多态

33、什么是野指针

野指针不是NULL指针,是未初始化或者未清零的指针,它指向的内存地址不是程序员所期望的,可能指向了受限的内存

成因:

1)指针变量没有被初始化

2)指针指向的内存被释放了,但是指针没有置NULL

3)指针超过了变量了的作用范围,比如b[10],指针b+11

33、线程安全和线程不安全

线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可以使用,不会出现数据不一致或者数据污染。

线程不安全就是不提供数据访问保护,有可能多个线程先后更改数据所得到的数据就是脏数据。

34、C++中内存泄漏的几种情况

内存泄漏是指己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

1)类的构造函数和析构函数中new和delete没有配套

2)在释放对象数组时没有使用delete[],使用了delete

3)没有将基类的析构函数定义为虚函数,当基类指针指向子类对象时,如果基类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有正确释放,因此造成内存泄露

4)没有正确的清楚嵌套的对象指针

35、栈溢出的原因以及解决方法

栈溢出是指函数中的局部变量造成的溢出(注:函数中形参和函数中的局部变量存放在栈上)

栈的大小通常是1M-2M,所以栈溢出包含两种情况,一是分配的的大小超过栈的最大值,二是分配的大小没有超过最大值,但是接收的buf比原buf小。

1)函数调用层次过深,每调用一次,函数的参数、局部变量等信息就压一次栈

2)局部变量体积太大。

解决办法大致说来也有两种:

1> 增加栈内存的数目;如果是不超过栈大小但是分配值小的,就增大分配的大小

2> 使用堆内存;具体实现由很多种方法可以直接把数组定义改成指针,然后动态申请内存;也可以把局部变量变成全局变量,一个偷懒的办法是直接在定义前边加个static,呵呵,直接变成静态变量(实质就是全局变量)

36、C++标准库vector以及迭代器

每种容器类型都定义了自己的迭代器类型,每种容器都定义了一队命名为begin和end的函数,用于返回迭代器。

迭代器是容器的精髓,它提供了一种方法使得它能够按照顺序访问某个容器所含的各个元素,但无需暴露该容器的内部结构,它将容器和算法分开,让二者独立设计。

38、C++中vector和list的区别

vector和数组类似,拥有一段连续的内存空间。vector申请的是一段连续的内存,当插入新的元素内存不够时,通常以2倍重新申请更大的一块内存,将原来的元素拷贝过去,释放旧空间。因为内存空间是连续的,所以在进行插入和删除操作时,会造成内存块的拷贝,时间复杂度为o(n)。

list是由双向链表实现的,因此内存空间是不连续的。只能通过指针访问数据,所以list的随机存取非常没有效率,时间复杂度为o(n); 但由于链表的特点,能高效地进行插入和删除。

vector拥有一段连续的内存空间,能很好的支持随机存取,因此vector::iterator支持“+”,“+=”,“<”等操作符。

list的内存空间可以是不连续,它不支持随机访问,因此list::iterator则不支持“+”、“+=”、“<”等

vector::iterator和list::iterator都重载了“++”运算符。

总之,如果需要高效的随机存取,而不在乎插入和删除的效率,使用vector;

如果需要大量的插入和删除,而不关心随机存取,则应使用list。

39、C语言的函数调用过程

函数的调用过程:

1)从栈空间分配存储空间

2)从实参的存储空间复制值到形参栈空间

3)进行运算

形参在函数未调用之前都是没有分配存储空间的,在函数调用结束之后,形参弹出栈空间,清除形参空间。

数组作为参数的函数调用方式是地址传递,形参和实参都指向相同的内存空间,调用完成后,形参指针被销毁,但是所指向的内存空间依然存在,不能也不会被销毁。

当函数有多个返回值的时候,不能用普通的 return 的方式实现,需要通过传回地址的形式进行,即地址/指针传递。

传值:传值,实际是把实参的值赋值给行参,相当于copy。那么对行参的修改,不会影响实参的值 。

传址: 实际是传值的一种特殊方式,只是他传递的是地址,不是普通的赋值,那么传地址以后,实参和行参都指向同一个对象,因此对形参的修改会影响到实参。

40、C++中的基本数据类型及派生类型

1)整型 int

2)浮点型 单精度float,双精度double

3)字符型 char

4)逻辑型 bool

5)控制型 void

基本类型的字长及其取值范围可以放大和缩小,改变后的类型就叫做基本类型的派生类型。派生类型声明符由基本类型关键字char、int、float、double前面加上类型修饰符组成。

类型修饰符包括:

short 短类型,缩短字长

long 长类型,加长字长

signed 有符号类型,取值范围包括正负值

unsigned 无符号类型,取值范围只包括正值

41、友元函数和友元类

友元提供了不同类的成员函数之间、类的成员函数和一般函数之间进行数据共享的机制。

通过友元,一个不同函数或者另一个类中的成员函数可以访问类中的私有成员和保护成员。

友元的正确使用能提高程序的运行效率,但同时也破坏了类的封装性和数据的隐藏性,导致程序可维护性变差。

1)友元函数

有元函数是可以访问类的私有成员的非成员函数。它是定义在类外的普通函数,不属于任何类,但是需要在类的定义中加以声明。

friend 类型 函数名(形式参数);

一个函数可以是多个类的友元函数,只需要在各个类中分别声明。

2)友元类

友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的隐藏信息(包括私有成员和保护成员)。

friend class 类名;

使用友元类时注意:

(1) 友元关系不能被继承。

(2) 友元关系是单向的,不具有交换性。若类B是类A的友元,类A不一定是类B的友元,要看在类中是否有相应的声明。

(3) 友元关系不具有传递性。若类B是类A的友元,类C是B的友元,类C不一定是类A的友元,同样要看类中是否有相应的申明

c++函数库中一些实用的函数

  1. __gcd(x, y)

求两个数的最大公约数,如__gcd(6, 8) 就返回2。

  1. reverse(a + 1, a + n + 1)

将数组中的元素反转。a 是数组名,n是长度,跟 sort 的用法一样。值得一提的是,对于字符型数组也同样适用。

  1. unique(a + 1, a + n + 1)

去重函数。跟sort的用法一样。不过他返回的值是最后一个数的地址,所以要得到新的数组长度应该这么写: _n = unique(a + 1, a + n + 1) - a - 1.

4.lower_bound(a + 1, a + n + 1, x); upper_bound(a + 1, a + n + 1, x)

lower_bound是查找数组中第一个大于等于x的数,返回该地址,同理也是 pos = lower_bound(a + 1, a + n + 1, x) - a

upper_bound是查找第一个大于x的数,用法和lower_bound一样

复杂度是二分的复杂度,O(logn)。(其实就是代替了手写二分)

5.fill(a + 1, a + n + 1, x)

例如

int数组:fill(arr, arr + n, 要填入的内容);

vector也可以:fill(v.begin(), v.end(), 要填入的内容);

fill(vector.begin(), cnt, val); // 从当前起始点开始,将之后的cnt个元素赋值为val。

memset(arr, val, cnt); // 在头文件里。

将数组a中的每一个元素都赋成x,跟memset的区别是,memset函数按照字节填充,所以一般memset只能用来填充char型数组,(因为只有char型占一个字节)如果填充int型数组,除了0和-1,其他的不能。

●多进程和多线程的区别

进程它是具有独立地址空间的,优点就是隔离度好,稳定,因为它是操作系统管理的,进程和进程之间是逻辑隔离的,只要操作系统不出问题的话,一个进程的错误一般不会影响到其它进程,缺点就是信息资源共享麻烦。而线程只是进程启动的执行单元,它是共享进程资源的,创建销毁、切换简单,速度很快,占用内存少,CPU利用率高。但是需要程序员管控的东西也比较多,相互影响出问题的机率较大,一个线程挂掉将导致整个进程挂掉,所以从程序员的角度来讲,我们只能看到某种代码是线程安全的,而没有说进程安全的。

●在进程和线程上,应该怎么选择

我们平时在写代码的时候一般使用线程会比较多,像需要频繁创建销毁的,要处理大量运算、数据,又要能很好的显示界面和及时响应消息的优先选择多线程,因为像这些运算会消耗大量的CPU,常见的有算法处理和图像处理。还有一些操作允许并发而且有可能阻塞的, 也推荐使用多线程. 例如SOCKET, 磁盘操作等等。进程一般来说更稳定,而且它是内存隔离的,单个进程的异常不会导致整个应用的崩溃,方便调试,像很多服务器默认是使用进程模式的。

●线程之间是如何通信的

一个是使用全局变量进行通信,还有就是可以使用自定义的消息机制传递信息。其实因为各个线程之间它是共享进程的资源的,所以它没有像进程通信中的用于数据交换的通信方式,它通信的主要目的是用于线程同步,所以像一些互斥锁啊临界区啊CEvent事件对象和信号量对象都可以实现线程的通信和同步。

●进程之间是如何通信的

进程间的通信方式有PIPE管道,信号量,消息队列,共享内存,还可以通过 socket套接字进行通信。根据信息量大小的不同可以分为低级通信和高级通信,在选择上,如果用户传递的信息较少.或是需要通过信号来触发某些行为的,一般用信号机制就能解决,如果进程间要求传递的信息量比较大或者有交换数据的要求,那么就要使用共享内存和套接字这些通信方式。

名词解释:

管道其实是存在于内存中的一种特殊文件,它不属于文件系统,有自己的数据结构,根据使用范围还可分为无名管道和命名管道。

共享内存是通过将共享的内存缓冲区直接附加到进程的虚拟地址空间中来实现的,它是利用内存缓冲区直接交换信息,不需要复制,很快捷、信息量大。

消息队列缓冲是由系统调用函数来实现消息发送和接收之间的同步,它允许任意进程通过共享消息队列来实现进程间通信.但是信息的复制需要耗费大量CPU,所以不适用于信息量大或操作频繁的场合。

●线程同步和线程异步

同步是指一个线程要等待另一个线程执行完之后才开始执行当前的线程。

异步是指一个线程去执行,它的下一个线程不必等待它执行完就开始执行。

一般一个进程启动的多个不相干线程,它们之间的相互关系就为异步,比如游戏有图像和背景音乐,图像是由玩家操作的 而背景音乐是系统循环播放,它们两个线程之间没什么关系各干各的,这就是线程异步。至于同步的话指的是多线程同时操作一个数据,这个时候需要对数据添加保护,这个保护就是线程的同步

同步使用场景:对于多个线程同时访问一块数据的时候,必须使用同步,否则可能会出现不安全的情况,有一种情况不需要同步技术,那就是原子操作,也就是说操作系统在底层保证了操作要么全部做完,要么不做。

异步的使用场景:当只有一个线程访问当前数据的时候。比如观察者模式,它没有共享区,主题发生变化后通知观察者更新,主题继续做自己的事情,不需要等待观察者更新完成后再工作。

●多线程同步和互斥有几种实现方法,分别适用什么情况

线程同步的话有临界区,互斥量,信号量,事件。

临界区适合一个进程内的多线程访问公共区域或代码段时使用。

互斥量是可以命名的,也就是说它可以适用不同进程内多线程访问公共资源时使用。所以在选择上如果是在进程内部使用的话,用临界区会带来速度上的优势并且能够减少资源占用量。

信号量与临界区和互斥量不同,它是允许多个线程同时访问公共资源的,它相当于操作系统的PV操作,它会事先设定一个最大线程数,如果线程占用数达到最大,那么其它线程就不能再进来,如果有部分线程释放资源了,那么其它线程才能进来访问资源。

事件是通过通知操作的方式来保持线程同步。

注意:互斥量,事件,信号量都是内核对象,可以跨进程使用。

●C++多线程有几种实现方法

直接使用WIN32 API CreateThread,或者用C运行库_beginthread创建线程,MFC的话用AfxBeginThread. 还有就是运用第三方线程库,比如boost的thread等等。

_beginthread和CreateThread的区别:_beginthread内部调用了CreateThread.

如果你的程序只调用 Win32 API/SDK ,就放心用 CreateThread,如果要用到C++运行时库,那么就要使用_beginthreadex,因为C++运行库有一些函数里面使用了全局变量,beginthreadex 为这些全局变量做了处理,使得每个线程都有一份独立的“全局”量,在这种情况下使用CreateThread的话就会出现不安全的问题

●进程有哪几种状态,状态转换图,及导致转换的事件

●死锁

概念:进程间进行通信或相互竞争系统资源而产生的永久阻塞,若无外力作用将永远处在死锁状态。

产生原因:(1)系统资源不足;(2)进程运行推进顺序与速度不同也可能导致死锁;(3)资源分配不当;

产生死锁四个必要条件:

(1) 互斥条件:就是一个资源每次只能被一个进程使用。

(2) 请求与保持条件:一个进程在请求其它资源而阻塞时,但是它对自己已获得的资源又保持不放。

(3) 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。

(4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

预防死锁和避免死锁的方法:在系统设计、进程调度方面注意不让产生死锁的四个必要条件成立,确定资源的合理分配算法,避免进程永远占用系统资源,对资源分配要进行合理的规划。

●多线程中栈与堆是公有的还是私有的

因为线程是共享进程的资源的,所以栈是私有的,堆是公有的。

●线程池的概念

线程池就是一堆已经创建好的线程,最大数目一定,然后初始后都处于空闲状态,当有新任务进来时就从线程池中取出空闲线程处理任务,任务完成之后又重新放回去,当线程池中的所有线程都在任务时,只能等待有线程结束任务才能继续执行。

1、Socket通信流程

Socket通信流程

服务端:创建socket()、绑定socket和端口号bind(),监听端口llisten(),接收来自客户端的请求accpet(),从socket中读取字符recv(),关闭close()

客户端: 创建socket()、 连接指定端口connect(), 发送数据send() 关闭close()

C/S通信方式

2、字节序分为大端字节序和小端字节序。
大端字节序:最高有效位存储于最低内存地址处,最低有效位存储于最高内存地址。
小端字节序:最高有效位存储于最高内存地址处,最低有效位存储于最低内存地址。

你可能感兴趣的:(笔记)