服务器开发需要以下几个方面的知识:
学习高并发服务器开发,需要对以上的知识有深入的理解和掌握,并进行不断的实践和优化。可以通过阅读相关的书籍和论文、参加相关的课程和培训、参与开源项目等方式进行学习和实践。同时,可以积极参加相关技术社区和论坛,与行业内的专家和开发者进行交流和学习。
基础语法:包括变量、数据类型、运算符、控制流程语句等。
面向对象编程:包括类、继承、多态、虚函数等概念和应用。
模板编程:包括函数模板、类模板、元编程等概念和应用。
标准库:包括STL容器、算法、迭代器、流、文件等的使用。
异常处理:包括异常的概念、处理方式、异常安全性等。
并发编程:包括多线程、互斥量、条件变量、原子操作等。
内存管理:包括动态内存分配、智能指针、内存泄漏、内存安全等。
性能优化:包括算法优化、编译器优化、代码结构优化、内存优化等。
操作系统相关:包括进程、线程、同步机制、IO模型等。
高级应用:包括网络编程、图形界面、游戏开发、嵌入式开发、数据科学等
C++基础知识
C++面向对象编程
C++高级特性
C++标准库
并发编程
C++网络编程
区别 | 含参数的宏 | 函数 |
---|---|---|
运算方式 | 函数调用时,先求出实参表达式的值,然后带入形参。 | 使用带参的宏只是进行简单的字符替换。 |
作用时期不同 | 函数调用是在程序运行时进行的,分配临时的内存单元; | 而宏替换则是在编译时进行的,在展开时不分配内存单元,不进行值的传递处理,也没有“返回值”的概念。 |
类型检查 | 对函数中的实参和形参都要定义类型,二者的类型数量要求一致; | 宏没有类型检查,宏名无类型,它的参数也无类型,只是一个符号代表,展开时带入指定的字符即可。 |
返回结果 | 调用函数只可得到一个返回值 | 而用宏可以得到几个结果。 |
程序增长 | 函数调用不使源程序变长。 | 使用宏时,宏替换后源程序增长 |
占用程序运行时间 | 函数调用占运行时间(分配单元、保留现场、值传递、返回)。 | 宏替换不占运行时间,只占编译时间; |
区别 | sizeof | strlen |
---|---|---|
性质不同 | sizeof是关键字(保留字),就是已被C语言本身使用,不能作其它用途使用的字 | strlen是C语言标准库函数 |
计算时期不同 | 编译器在编译时就计算出了sizeof的结果 | 而strlen函数必须在运行时才能计算出来。 |
计算结果不同 | sizeof计算的是数据类型占内存的大小 | strlen计算的是字符串实际的长度,strlen只能测量字符串 计算字符串 str 的长度,直到空结束字符,但不包括空结束字符 |
区别 | strcpy | memcpy |
---|---|---|
复制内容不同 | strcpy只能复制字符串 | memcpy可以复制任意内容,例如字符数组、整型、结构体、类等 |
复制方法不同 | strcpy不需要指定长度,它遇到被复制字符的串结束符"\0"才结束,如果空间不够,就会引起踩内存(访问不应该访问的内存地址)。 | memcpy则是根据其第3个参数决定复制的长度。 |
用途不同 | 通常在复制字符串时用strcpy | 需要复制其他类型数据时则一般用memcpy,由于字符串是以“\0”结尾的,所以对于在数据中包含“\0”的数据只能用memcpy |
static:
extern:
static
修饰时,可通过extern
关键字在本文件对其声明,即可使用。#include
assert(src != NULL);//断言 括号内部成立上面事情不发生,否则报错
作用:解决预防性编程的问题,
例如参数传入一个指针为NULL时,程序就会奔溃时,我们可以增加assert来防御这种问题。在联调中assert会显示崩溃的信息,加快联调速度,也能对参数问题进行判断。assert只能在debug版起作用,发布版不生效。综上所述assert就是预防性编程一个重要的宏,能加快联调速度。
struct 一般用于描述一个数据结构集合,而 class 是对一个对象数据的封装
struct 中默认的访问控制权限是 public 的,而 class 中默认的访问控制权限是 private 的
在继承关系中,struct 默认是公有继承,而 class 是私有继承
class 关键字可以用于定义模板参数,就像 typename,而 struct 不能用于定义模板参数
template<typename T, typename Y>//可以把typename换成class
int Func(const T &t, const Y &y) {
//TODO
}
C++中的struct是对C中的struct的扩充,它们在声明时的区别如下:
struct | C | C++ |
---|---|---|
成员函数 | 不能有 | 可以 |
静态成员 | 不能有 | 可以 |
访问控制 | 默认public不可修改 | 默认public/private/protected |
继承关系 | 不可以继承 | 可从类或者其他结构体继承 |
初始化 | 不能直接初始化数据成员 | 可以 |
作用 | 具体说明 |
---|---|
1.定义全局静态变量和局部静态变量 | 在变量前面加上static关键字。初始化的静态变量会在数据段分配内存,未初始化的静态变量会在BSS段分配内存。直到程序结束,静态变量始终会维持前值。只不过全局静态变量和局部静态变量的作用域不一样 |
2.定义为静态变量 | 静态变量只能在本源文件中使用 |
3.定义静态函数 | 在函数返回类型前加上static关键字,函数即被定义为静态函数。静态函数只能在本源文件中使用 |
4.定义类中的静态成员变量 | 使用静态数据成员,它既可以被当成全局变量那样去存储,但又被隐藏在类的内部。类中的static静态数据成员拥有一块单独的存储区,而不管创建了多少个该类的对象。所有这些对象的静态数据成员都共享这一块静态存储空间 |
5.定义类中的静态成员函数 | 与静态成员变量类似,类里面同样可以定义静态成员函数。只需要在函数前加上关键字static即可。如静态成员函数也是类的一部分,而不是对象的一部分。所有这些对象的静态数据成员都共享这一块静态存储空间 |
C++里作用域可分为6种:全局,局部,类,语句,命名空间和文件作用域
所在空间考虑:除了局部变量在栈上外,其他都在静态存储区。因为静态变量都在静态存储区,所以下次调用函数的时候还是能取到原来的值。
生命周期:局部变量在栈上,出了作用域就回收内存。而全局变量、静态全局变量、静态局部变量都在静态存储区,直到程序结束才会回收内存。
在C++中,导入C函数的关键字是extern,表达形式为extern “C”,主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言的进行编译,而不是C++的
由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;
而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包括函数名。
C++和C语言类似,一个C++程序从源码到执行文件,有四个过程,预编译、编译、汇编、链接。
预编译:
编译:
汇编:这个过程主要是将汇编代码转变成机器可以执行的指令
链接:将不同的源文件产生的目标文件进行链接,从而形成一个可以执行的程序
静态链接:是在链接的时候就已经把要调用的函数或者过程链接到了生成的可执行文件中,就算你在去把静态库删除也不会影响可执行程序的执行;生成的静态链接库,Windows下以.lib为后缀,Linux下以.a为后缀
动态链接:是在链接的时候没有把调用的函数代码链接进去,而是在执行的过程中,再去找要链接的函数,生成的可执行文件中没有函数代码,只包含函数的重定位信息。所以当你删除动态库时,可执行程序就不能运行。生成的动态链接库,Windows下以.dll为后缀,Linux下以.so为后缀
区别 | 指针 | 引用 |
---|---|---|
空间 | 指针是地址,有存储空间 | 就是别名,无存储空间 |
多级 | 指针可以有多级 | 引用只能是一级 |
是否为NULL | 指针可以指向NULL | 引用不可以为NULL |
初始化 | 指针可以在定义的时候不初始化 | 引用必须在定义的时候初始化 |
改变指向 | 指针初始化之后可以再改变指向 | 引用初始化后不可以再改变指向 |
空间大小 | sizeof 的运算结果不同,指针就是4/8字节 | 引用是被引⽤用对象的大⼩ |
自增运算 | 指针使用自增运算是指针指向向后偏移一个存储单元 | 引用自增是将对应的值+1 |
类型检查 | 无 | 引用比指针多了类型检查 |
判空操作 | 指针作为函数参数时,指针需要检查是否为空 | 引用作为函数参数时,引用不需要见检查是否为空 |
区别 | 数组 | 指针 |
---|---|---|
1.存储方式 | 数组在内存中是连续存放的,开辟一块连续的内存空间。数组是根据数组的下进行访问的,数组的存储空间,不是在静态区就是在栈上 | 指针很灵活,它可以指向任意类型的数据。指针的存储空间不能确定。 |
2.所占存储空间的内存大小 | 数组所占存储空间的内存大小:sizeof(数组名)/sizeof(数据类型) | 在32位平台下,无论指针的类型是什么,sizeof(指针名)都是4,在64位平台下,无论指针的类型是什么,sizeof(指针名)都是8 |
传参方式有这三种:值传递、引用传递、指针传递
指针:变量,独立,可变,可空,替身,无类型检查
指针从本质上讲是一个变量,变量的值是另一个变量的地址,指针在逻辑上是独立的,它可以被改变的,包括指针变量的值(所指向的地址)和指针变量的值对应的内存中的数据(所指向地址中所存放的数据)
引用:别名,依赖,不变,非空,本体,有类型检查
引用从本质上讲是一个别名,是另一个变量的同义词,它在逻辑上不是独立的,它的存在具有依附性,所以引用必须在一开始就被初始化(先有这个变量,这个实物,这个实物才能有别名),而且其引用的对象在其整个生命周期中不能被改变,即自始至终只能依附于同一个变量(初始化的时候代表的是谁的别名,就一直是谁的别名,不能变)
区别:
指针参数传递本质上是值传递,它所传递的是一个地址值。
引用参数传递过程中,被调函数的形式参数也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。
引用传递和指针传递是不同的,任何对于引用参数的处理都会通过一个间接寻址的方式操作到主调函数中的相关变量。而对于指针传递的参数,如果改变被调函数中的指针地址,它将应用不到主调函数的相关变量。
函数指针就是指向函数的指针变量。每个函数都有一个入口地址,该入口地址就是函数指针所指向的地址。
int func(int a);
int (*f)(int a);
f = &func;
函数指针的应用场景:回调callback,
我们调用别人提供的 API函数应用程序编程接口称为Call;如果别人的库里面调用我们的函数,就叫Callback
可以,因为在编译时对象就绑定了函数地址,和指针空不空没关系。
//给出实例
class animal{
public:
void sleep(){ cout << "animal sleep" << endl; }
void breathe(){ cout << "animal breathe haha" << endl; }
};
class fish :public animal{
public:
void breathe(){ cout << "fish bubble" << endl; }
};
int main(){
animal *pAn=nullptr;
pAn->breathe(); // 输出:animal breathe haha
fish *pFish = nullptr;
pFish->breathe(); // 输出:fish bubble
return 0;
}
野指针:就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
产生原因:释放内存后指针不及时置空,依然指向原来的内存,那么可能出现非法访问的错误。
避免方法:
空指针:空指针是一个指向空地址的指针,指针没有指向任何有效的内存地址,可以通过将指针初始化为null或0来创建空指针。空指针通常用于指示指针没有指向任何对象。
如若你尝试打印指向空的指针的地址值,这时你会发现输出的结果为0x00000000,表示不指向任何有效空间。
野指针:野指针是一个指向未知或无效地址的指针,指针指向一个没有分配的内存地址。或指向一个已经释放的内存地址。野指针通常是由于未初始化指针或者指针越界引起的,其使用是非法的。最为常见的产生野指针的情况有三种:
区别在于:
总的来说,野指针是由于程序员疏忽或者代码逻辑错误引起的,因此在编程时应该避免这种错误,以确保程序的正确性和可靠性。可以通过正确地初始化和管理指针,以及使用安全的编程实践来避免野指针的出现。
不同点 | 指针函数 | 函数指针 |
---|---|---|
1.定义不同 | 指针函数本质是一个函数,其返回值为指针 | 函数指针本质是一个指针,其指向一个函数 |
2.写法不同 | int *fun(int x,int y); | int (*fun)(int x,int y); |
3.使用不同 |
//指针函数示例
typedef struct _Data{
int a;
int b;
}Data;
//指针函数
Data* f(int a,int b){
Data * data = new Data;
//...
return data;
}
int main(){
//调用指针函数
Data * myData = f(4,5);
//Data * myData = static_cast(f(4,5));
//...
}
//函数指针示例
int add(int x,int y){
return x+y;
}
//函数指针
int (*fun)(int x,int y);
//赋值
fun = add;
//调用
cout << "(*fun)(1,2) = " << (*fun)(1,2);
//输出结果
//(*fun)(1,2) = 3
内联函数的作用:是将内联函数的调用表达式用内联函数体来替换。避免函数调用的开销
区别 | 宏定义 | 内联函数 |
---|---|---|
1.是否为函数 | 宏定义不是函数,但是使用起来像函数。预处理器用复制宏代码的方式代替函数的调用,省去了函数压栈退栈过程提高了效率 | 而内联函数本质上是一个函数,内联函数一般用于函数体的代码比较简单的函数,不包含复杂的控制语句,while、switch,并且内联函数本身不能直接调用自身 |
2.工作方式不同 | 宏函数是在预编译的时候把所有的宏名用宏体来替换,简单的说就是字符串替换 | 而内联函数则是在编译的时候进行代码插入,编译器会在每处调用内联函数的地方直接把内联函数的内容展开,这样可以省去函数的调用的开销,提高效率 |
3.是否有类型检查 | 宏定义是没有类型检查的,无论对还是错都是直接替换; | 而内联函数在编译的时候,编译器会检查参数的类型和数量是否正确, |
4.编译方式不同 | 内联函数是在编译时被展开的 | 内联函数是在编译时被展开的 |
注:类型检查指,验证接收的是否为合适的数据类型以及赋值是否合乎类型要求。
总的来说,内联函数相对于宏函数更加安全、灵活、易于调试和维护。但是,内联函数在函数体过长时会增加代码的大小,从而降低程序的性能。因此,在实际编程中,需要根据具体情况选择合适的方式来提高程序的执行效率。
const用于定义常量;而define用于定义宏,而宏也可以用于定义常量。都用于常量定义时,它们的区别有:
区别 | const | define |
---|---|---|
编译器处理方式 | const生效于编译的阶段确定其值的大小 | define生效于预处理阶段。 |
类型检查 | const定义的常量是带类型的, | define定义的常量不带类型,因此define定义的常量不利于类型检查。宏只作替换,不做计算,不做表达式求解。 |
内存空间 | const定义的常量,在C语言中是存储在内存中(静态存储区)、需要额外的内存空间的,在程序运行过程中内存中只有一个拷贝。 | define定义的常量,运行时是直接的操作数,有多少次使用就进行多少次替换,在内存中会有多个拷贝,消耗内存大。 |
总结来说const比define更加安全,
相同点:拿空间换时间,提高程序的执行效率
区别 | define | inline |
---|---|---|
展开时机 | 内联函数在编译时展开 | 宏是由预处理器对宏进行展开 |
类型检查 | 内联函数会检查参数类型,所以内联函数更安全 | 宏定义不检查函数参数 |
是否为函数 | inline是函数 | 宏不是函数 |
设置的内联函数编译器不一定会满足要求,这取决于函数大小或者函数是否调用自身 | 宏在定义时要小心处理宏参数,一般情况是把参数用括弧括起来 |
常函数:类的成员函数后面加 const,表明这个函数不会对这个类对象的数据成员(准确地说是非静态数据成员)作任何改变。
不同点 | 前置(++i) | 后置(i++) |
---|---|---|
赋值顺序不同 | ++i 是先加后赋值 | i++ 是先赋值后加 |
效率不同 | 前置++不产生临时对象 | 后置++中tmp是一个临时对象,会造成一次构造函数和一次析构函数的额外开销 |
是否作为左值 | i++不能作为左值 | 而++i 可以作为左值 |
相同点:两者都不是原子操作(不会被线程调度机制打断的操作)
知识易错点:
- 空类占用内存空间:1字节
- explicit作用:关闭函数的类型自动转换(防止隐式转换)
- 当初始化列表时,被初始化的顺序是声明是的顺序不是列表顺序。
- final关键字用处:当前我这个类就是最终类,我不想让别的类再继承我自己。
我的博客总结:https://blog.csdn.net/weixin_49167174/article/details/130046439
什么是内存对齐:计算机系统对基本类型数据在内存中存放的位置有限制,它们会要求这些数据的首地址的值是某个数k(通常它为4或8)的倍数,这就是所谓的内存对齐。
为什么要进行内存对齐?
为了使CPU能够对变量进行快速的访问,变量的起始地址应该具有某些特性,
设置了内存对齐的,类型数据只能存放在按照对齐规则的内存中,处理器在取数据时一次性就能将数据读出来了,而且不需要做额外的操作(比如剔除不要的数据),提高了效率。
内存对齐的规则?
内存对齐的使用场景?
内存对齐应用与3种数据类型:struct、class、union
在C++中,内存分成5个区:堆、栈、全局存储区、常量存储区、代码区(自由存储区)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u5Kdq6ax-1693106684783)(https://s2.loli.net/2023/07/21/E4CsR9h6bxBPS5g.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5WfiPoFS-1693106684785)(https://s2.loli.net/2023/08/26/7xUIEAKLpYqDJMC.png)]
内存总共分为五大分区:栈区,堆区,全局静态区,常量文本区,程序代码区。
从低地址到高地址,一个程序由代码段、数据段、BSS段、堆、共享区、栈等组成。
(1)malloc申请的内存在堆上,使用free释放。new申请的内存在自由存储区,用delete释放
(2)堆(heap)是c语言和操作系统的术语。堆是操作系统所维护的一块特殊内存,它提供了动态分配的功能,当程序运行时调用malloc()时就会从中分配,调用free可把内存交换。而自由存储区是C++中通过new和delete动态分配和释放对象的抽象概念,通过new来申请的内存区域可称为自由存储区。基本上,所有的C++编译器默认用堆来实现自由存储区,也即是缺省的全局运算符new和delete也许会按照malloc和free的方式来实现,这时由new运算符分配的对象,说它在堆上也对,说它在自由存储区也对。
总结:
区别 | new | malloc |
---|---|---|
性质不同 | new是操作符 | malloc是函数 |
是否调用析构与构造函数函数 | 在调用时先分配内存,再调用构造函数,释放时调用析构函数 | malloc没构造函数与析构函数 |
申请内存大小是否给定 | new会调用构造函数,不用指定申请内存的大小, | malloc需要给定申请内存的大小 |
返回类型 | new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。 | malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型 |
是否能被重载 | new可以被重载 | malloc不行 |
内存分配失败的返回值 | new发生错误抛出异常,更直接和安全 | malloc返回null |
申请的内存所在位置不同 | new操作符从自由存储区(free store)上为对象动态分配内存空间 | malloc函数从堆上动态分配内存 |
new操作符:
operatornew
函数分配一块足够大的原始未命名的内存空间以便存储特定类型的对象。delete操作符:
new/delete会调用对象的构造函数/析构函数以完成对象的构造/析构,而malloc则不会。
malloc:
new:
常见的内存错误:
应对策略:
内存泄露是什么?:
就是申请了一块内存空间,使用完毕后没有释放掉。
如何检测?:
有六个内存顺序选项可应用于对原子类型的操作:
面向对象是一种编程思想,把一切东西看成是一个个对象,把这些对象拥有的属性变量和操作这些属性变量的函数打包成一个类来表示。
面向对象四大特点:抽象、封装、继承、多态
构造函数可以分为4类:默认构造函数、初始化构造函数、拷贝构造函数、移动构造函数。
默认构造函数和初始化构造函数:在定义类的对象的时候,完成对象的初始化工作(有了有参的构造了,编译器就不提供默认的构造函数)。
拷贝构造函数:赋值构造函数默认实现的是值拷贝(浅拷贝)
移动构造函数:用于将其他类型的变量,隐式转换为本类对象
//将int类型的r转换为Student类型的对象,对象的age为r,num为1004.
Student(int r) { int num=1004; int age= r; }
调用时机:拷贝构造函数在对象需要进行拷贝时会被调用,对象拷贝包含以下几种情况:
浅拷贝:只做简单的值拷贝,如果内存开辟在堆区,析构时会发生重复释放内存的情况。
深拷贝:为了解决浅拷贝的问题,在堆区另外申请内存空间,进行拷贝操作,需要自定义拷贝构造函数。
浅拷贝:将源对象的值拷贝到目标对象中去,本质上来说源对象和目标对象共用一份实体,只是所引用的变量名不同,地址其实还是相同的。
深拷贝:拷贝的时候先开辟出和源对象大小一样的空间,然后将源对象里的内容拷贝到目标对象中去,这样两个指针就指向了不同的内存位置。并且里面的内容是一样的。这样不会出现指针悬挂问题。
两个指针先后去调用析构函数,分别释放自己所指向的位置。即为每次增加一个指针,便申请一块新的内存,并让这个指针指向新的内存,深拷贝情况下,不会出现重复释放同一块内存的错误。
深拷贝的实现:赋值运算符的重载传统实现:
这种方法解决了我们的指针悬挂问题,通过不断的开空间让不同的指针指向不同的内存,以防止同一块内存被释放两次的问题。
string( const string& s ) {
//_str = s._str;
_str = new char[strlen(s._str) + 1];
strcpy_s( _str, strlen(s._str) + 1, s._str );
}
string &operator=(const string& s) {
if (this != &s) {
//this->_str = s._str;
delete[] _str;
this->_str = new char[strlen(s._str) + 1];
strcpy_s(this->_str, strlen(s._str) + 1, s._str);
}
return *this;
}
类中的成员可以分为三种类型,分别为public成员、protected成员、public成员。类中可以直接访问自己类的public、protected、private成员,但类对象只能访问自己类的public成员。
public继承:
protected继承:
private继承:
public继承:
protected继承:
private继承:
c++类内可以定义引用成员变量,但要遵循以下三个规则:
C++11中新增了移动构造函数。
移动操作的概念对对象管理它们使用的存储空间很有用的,
多态通过虚函数实现,虚函数通过虚函数表实现,
多态:多态是通过虚函数实现的,是一种通过动态绑定实现对不同的类调用不同的函数接口。由于派生类重写基类方法,然后用基类引用指向派生类对象,调用方法时候会进行动态绑定。
多态分为静态多态和动态多态:
静态多态(重载、模板):
动态多态(虚函数、纯虚函数、虚析构函数、虚函数表):
关于动态绑定:
在使用基类的引用/指针调用虚函数时,就会发生动态绑定。所谓动态绑定,就是在运行时,虚函数会根据绑定对象的实际类型,选择调用函数的版本。
virtual 返回值类型 函数名(参数列表) = 0;
)c语言中不允许有同名函数,因为编译时函数命名是一样的,不像c++会添加参数类型和返回类型作为函数编译后的名称,进而实现重载。
如果要用c语言显现函数重载,可通过以下方式来实现:
假设类B、类C都继承了相同的类A,另外我们还有类D,类D通过多重继承机制继承了类B和类C。
继承关系的形状类似于菱形称为菱形继承,菱形继承存在的问题是多重继承导致数据重复冗余、浪费资源,可以继承基类Base时添加virtual
关键字利用虚继承可以解决:如果继承基类时用virtual来标注,C++会保证在子类对象中只有一个基类的子对象会被创建,解决重复冗余。
虚继承是解决C++多重继承问题(菱形继承问题)的一种手段,从不同途径继承同一基类,会在子类中存在多份拷贝。
这将存在两个问题:
虚基表:存放相对偏移量,用来找虚基类
在被继承的类前面加上virtual关键字,这时被继承的类称为虚基类
class A
class B1:public virtual A;
class B2:public virtual A;
虚继承的类可以被实例化
class Animal {/* ... */ };
class Tiger : virtual public Animal { /* ... */ };
class Lion : virtual public Animal { /* ... */ };
int main( )
{
Liger lg;
/*既然我们已经在Tiger和Lion类的定义中声明了"virtual"关键字,于是下面的代码编译OK */
int weight = lg.getWeight();
}
关于多态,简而言之就是用父类型的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。
如果调用非虚函数,则无论实际对象是什么类型,都执行基类类型所定义的函数。非虚函数总是在编译时根据调用该函数的对象,引用或指针的类型而确定。
C++中的虚函数的作用主要是实现了多态的机制。
纯虚函数是在基类中声明的虚函数,在基类中实现纯虚函数的方法是在函数原型后加“=0” virtualvoid GetName() =0。它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。
区别 | 虚函数 | 纯虚函数 |
---|---|---|
定义位置 | 而只含有虚函数的类不能被称为抽象类。 | 含有纯虚函数的类被称为抽象类,虚函数和纯虚函数可以定义在同一个类中 |
是否可被是直接使用 or 必须重载再使用 | 虚函数可以被直接使用,也可以被子类重载以后, | 而纯虚函数必须在子类中实现该函数才可以使用,因为纯虚函数在基类有声明而没有定义。 |
定义方式不同 | 虚函数的定义形式:virtual{}; | 纯虚函数的定义形式:virtual { } = 0; |
虚析构作用:使用父类指针释放子类对象时可以让子类的析构函数和父类的析构函数同时被调用到。
虚析构:释放基类指针时可以释放掉子类的空间,防止内存泄漏。将可能会被继承的父类的析构函数设置为虚函数,可以保证当我们new一个子类,然后使用基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄漏。如果基类的析构函数不是虚函数,在特定情况下会导致派生来无法被析构。
常见的不不能声明为虚函数的有:
普通函数(非成员函数),静态成员函数,内联成员函数,构造函数,友元函数。
为什么C++不支持普通函数为虚函数?
普通函数(非成员函数)只能被overload,不能被override,声明为虚函数也没有什么意思,因此编译器会在编译时绑定函数。
为什么C++不支持构造函数为虚函数?
这个原因很简单,主要是从语义上考虑,所以不支持。因为构造函数本来就是为了明确初始化对象成员才产生的,然而virtual function主要是为了再不完全了解细节的情况下也能正确处理对象。
为什么C++不支持内联成员函数为虚函数?
内联函数就是为了在代码中直接展开,减少函数调用花费的代价,虚函数是为了在继承后对象能够准确的执行自己的动作,这是不可能统一的。
为什么C++不支持静态成员函数为虚函数?
静态成员函数对于每个类来说只有一份代码,所有的对象都共享这一份代码,他也没有要动态绑定的必要性。
静态成员函数属于一个类而非某一对象,没有this指针,它无法进行对象的判别
为什么C++不支持友元函数为虚函数?
因为C++不支持友元函数的继承,对于没有继承特性的函数没有虚函数的说法。
虚函数表是一个存储虚函数地址的数组,以NULL结尾。
虚表(vftable)在编译阶段生成,对象内存空间开辟以后,写入对象中的 vfptr,然后调用构造函数。即:虚表在构造函数之前写入。
除了在构造函数之前写入之外,我们还需要考虑到虚表的二次写入机制,
通过此机制让每个对象的虚表指针都能准确的指向到自己类的虚表,为实现动多态提供支持。
抽象类的定义如下:有纯虚函数的类就叫做抽象类。
纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0”,
抽象类有如下几个特点:
模板实例化:模板的实例化分为显示实例化和隐式实例化,
前者是研发人员明确的告诉模板应该使用什么样的类型去生成具体的类或函数,
后者是在编译的过程中由编译器来决定使用什么类型来实例化一个模板不管是显示实例化或隐式实例化,最终生成的类或函数完全是按照模板的定义来实现的
模板具体化:
当模板使用某种类型类型实例化后生成的类或函数不能满足需要时,可以考虑对模板进行具体化。
具体化时可以修改原模板的定义,当使用该类型时,按照具体化后的定义实现,具体化相当于对某种类型进行特殊处理。
仿函数(functor)又称为函数对象(function object)是一个能行使函数功能的类。
()
的类。class Func{
public:
void operator() (const string& str) const {
cout << str << endl;
}
};
Func myFunc;
myFunc("helloworld!");
>>>helloworld!
STL中已经提供了的一些仿函数,这些仿函数所产生的对象、用法和一般函数完全相同,使用内建函数对象时需要提前引入头文件#include
主要包括:算术仿函数、关系仿函数、逻辑仿函数
plus、minus、multiplies、divides、modulus、negate
equal_to、not_equal_to、greater、greater_equal、less、less_equal
logical_and、logical_or、logical_not
class MyConpare{
public:
bool operator()(int num1 ,int num2){
return num1 > num2;
}
};
void main(){
//1.自主实现仿函数,实现sort降序排序
vector<int> v1;
v1.push_back(40);
v1.push_back(10);
v1.push_back(50);
v1.push_back(30);
v1.push_back(20);
sort(v1.begin(), v1.end(), MyConpare());
//2.使用内建函数对象(仿函数),实现sort降序排序
vector<int> v2;
v2.push_back(80);
v2.push_back(60);
v2.push_back(100);
v2.push_back(90);
v2.push_back(70);
sort(v2.begin(), v2.end(), greater<int>());
return 0;
}
STL由6部分组成:容器(Container)、算法(Algorithm)、 迭代器(Iterator)、仿函数(Function object)、适配器(Adaptor)、空间配制器(Allocator),容器和算法通过迭代器可以进行无缝地连接。
标准模板库STL主要由6大组成部分:
容器(Container)
是一种数据结构, 如list, vector, 和deques,以模板类的方法提供。为了访问容器中的数据,可以使用由容器类输出的迭代器。
算法(Algorithm)
是用来操作容器中的数据的模板函数。例如,STL用sort()来对一 个vector中的数据进行排序,用find()来搜索一个list中的对象, 函数本身与他们操作的数据的结构和类型无关,因此他们可以用于从简单数组到高度复杂容器的任何数据结构上。
迭代器(Iterator)
提供了访问容器中对象的方法。例如,可以使用一对迭代器指定list或vector中的一定范围的对象。 迭代器就如同一个指针。事实上,C++ 的指针也是一种迭代器。 但是,迭代器也可以是那些定义了operator*()以及其他类似于指针的操作符方法的类对象;
仿函数(Function object)
仿函数又称之为函数对象, 其实就是重载了操作符的struct,没有什么特别的地方。
适配器(Adaptor)
简单的说就是一种接口类,专门用来修改现有类的接口,提供一中新的接口;或调用现有的函数来实现所需要的功能。主要包括3中适配器Container Adaptor、Iterator Adaptor、Function Adaptor。
空间配制器(Allocator)
为STL提供空间配置的系统。其中主要工作包括两部分:
(1)对象的创建与销毁;
(2)内存的获取与释放。
顺序容器
容器并非排序的,元素的插入位置同元素的值无关。包含vector、deque、list,具体实现原理如下:
(1)vector 头文件
动态数组,元素在内存连续存放。随机存取任何元素都能在常数时间完成。在尾端增删元素具有较佳的性能。
(2)deque 头文件
双向队列,元素在内存连续存放。随机存取任何元素都能在常数时间完成(仅次于vector)。在两端增删元素具有较佳的性能(大部分情况下是常数时间)。
(3)list 头文件
双向链表,元素在内存不连续存放。在任何位置增删元素都能在常数时间完成。不支持随机存取。
关联式容器
元素是排序的;插入任何元素都按相应的排序规则来确定其位置;在查找时具有非常好的性能;通常以平衡二叉树的方式实现。包含set、multiset、map、multimap,具体实现原理如下:
(1)set/multiset 头文件
set 即集合。set中不允许相同元素,multiset中允许存在相同元素。
(2)map/multimap 头文件
map与set的不同在于map中存放的元素有且仅有两个成员变量,一个名为first,另一个名为second, map根据first值对元素从小到大排序,并可快速地根据first来检索元素。
**注意:**map同multimap的不同在于是否允许相同first值的元素。
容器适配器
封装了一些基本的容器,使之具备了新的函数功能,比如把deque封装一下变为一个具有stack功能的数据结构。这新得到的数据结构就叫适配器。包含stack,queue,priority_queue,具体实现原理如下:
(1)stack 头文件
栈是项的有限序列,并满足序列中被删除、检索和修改的项只能是最进插入序列的项(栈顶的项)。后进先出。
(2)queue 头文件
队列。插入只可以在尾部进行,删除、检索和修改只允许从头部进行。先进先出。
(3)priority_queue 头文件
优先级队列。内部维持某种有序,然后确保优先级最高的元素总是位于头部。最高优先级元素总是第一个出列。
程序包括数据结构和相应的算法,而数据结构作为存储数据的组织形式,与内存空间有着密切的联系。
在C++ STL中,空间配置器便是用来实现内存空间分配的工具(一般是内存,也可以是硬盘等空间),他与容器联系紧密,每一种容器的空间分配都是通过空间分配器alloctor实现的。
STL中常用的容器有vector、deque、list、map、set、multimap、multiset、unordered_map、unordered_set等。容器底层实现方式及时间复杂度分别如下:
vector
采用一维数组实现,元素在内存连续存放,不同操作的时间复杂度为:
插入: O(N)
查看: O(1)
删除: O(N)
deque
采用双向队列实现,元素在内存连续存放,不同操作的时间复杂度为:
插入: O(N)
查看: O(1)
删除: O(N)
list
采用双向链表实现,元素存放在堆中,不同操作的时间复杂度为:
插入: O(1)
查看: O(N)
删除: O(1)
map、set、multimap、multiset
上述四种容器采用红黑树实现,红黑树是平衡二叉树的一种。不同操作的时间复杂度近似为:
插入: O(logN)
查看: O(logN)
删除: O(logN)
unordered_map、unordered_set、unordered_multimap、 unordered_multiset
上述四种容器采用哈希表实现,不同操作的时间复杂度为: 插入: O(1),最坏情况O(N)
查看: O(1),最坏情况O(N)
删除: O(1),最坏情况O(N)
**注意:**容器的时间复杂度取决于其底层实现方式。
用过,常用容器迭代器失效情形如下。
迭代器的作用
(1)用于指向顺序容器和关联容器中的元素
(2)通过迭代器可以读取它指向的元素
(3)通过非const迭代器还可以修改其指向的元素
迭代器和指针的区别
**迭代器不是指针,是类模板,表现的像指针。**他只是模拟了指针的一些功能,重载了指针的一些操作符,–>、++、–等。迭代器封装了指针,是一个”可遍历STL( Standard Template Library)容器内全部或部分元素”的对象,本质是封装了原生指针,是指针概念的一种提升,提供了比指针更高级的行为,相当于一种智能指针,他可以根据不同类型的数据结构来实现不同的++,–等操作。
迭代器返回的是对象引用而不是对象的值,所以cout只能输出迭代器使用取值后的值而不能直接输出其自身。
迭代器产生的原因
Iterator类的访问方式就是把不同集合类的访问逻辑抽象出来,使得不用暴露集合内部的结构而达到循环遍历集合的效果。
这是主要考察迭代器失效的问题。
首先必须弄清楚两个概念:
(1)capacity:该值在容器初始化时赋值,指的是容器能够容纳的最大的元素的个数。还不能通过下标等访问,因为此时容器中还没有创建任何对象。
(2)size:指的是此时容器中实际的元素个数。可以通过下标访问0-(size-1)范围内的对象。
resize和reserve区别主要有以下几点:
(1)resize既分配了空间,也创建了对象;reserve表示容器预留空间,但并不是真正的创建对象,需要通过insert()或push_back()等创建对象。
(2)resize既修改capacity大小,也修改size大小;reserve只修改capacity大小,不修改size大小。
(3)两者的形参个数不一样。 resize带两个参数,一个表示容器大小,一个表示初始值(默认为0);reserve只带一个参数,表示容器预留的大小。
可能产生 的问题
容器是一种动态分配内存空间的一个变量集合类型变量。在一般的程序函数里,局部容器,参数传递容器,参数传递容器的引用,参数传递容器指针都是可以正常运行的,而在动态链接库函数内部使用容器也是没有问题的,但是给动态库函数传递容器的对象本身,则会出现内存堆栈破坏的问题。
产生问题的原因 容器和动态链接库相互支持不够好,动态链接库函数中使用容器时,参数中只能传递容器的引用,并且要保证容器的大小不能超出初始大小,否则导致容器自动重新分配,就会出现内存堆栈破坏问题。
vector和list区别在于底层实现机理不同,因而特性和适用场景也有所不同。
vector:一维数组
特点:元素在内存连续存放,动态数组,在堆中分配内存,元素连续存放,有保留内存,如果减少大小后内存也不会释放。
优点:和数组类似开辟一段连续的空间,并且支持随机访问,所以它的查找效率高其时间复杂度O(1)。
缺点:由于开辟一段连续的空间,所以插入删除会需要对数据进行移动比较麻烦,时间复杂度O(n),另外当空间不足时还需要进行扩容。
list:双向链表
特点:元素在堆中存放,每个元素都是存放在一块内存中,它的内存空间可以是不连续的,通过指针来进行数据的访问。
优点:底层实现是循环双链表,当对大量数据进行插入删除时,其时间复杂度O(1)。
缺点:底层没有连续的空间,只能通过指针来访问,所以查找数据需要遍历其时间复杂度O(n),没有提供[]操作符的重载。
应用场景
vector拥有一段连续的内存空间,因此支持随机访问,如果需要高效的随即访问,而不在乎插入和删除的效率,使用vector。
list拥有一段不连续的内存空间,如果需要高效的插入和删除,而不关心随机访问,则应使用list。
新增元素
Vector通过一个连续的数组存放元素,如果集合已满,在新增数据的时候,就要分配一块更大的内存,将原来的数据复制过来,释放之前的内存,再插入新增的元素。
插入新的数据分在最后插入push_back和通过迭代器在任何位置插入,
这里说一下通过迭代器插入,通过迭代器与第一个元素的距离知道要插入的位置,即int index=iter-begin()。这个元素后面的所有元素都向后移动一个位置,在空出来的位置上存入新增的元素。
删除元素
删除和新增差不多,也分两种,删除最后一个元素pop_back和通过迭代器删除任意一个元素erase(iter)。
通过迭代器删除还是先找到要删除元素的位置,即int index=iter-begin();这个位置后面的每个元素都想前移动一个元素的位置。同时我们知道erase不释放内存只初始化成默认值。
删除全部元素clear:只是循环调用了erase,所以删除全部元素的时候,不释放内存。内存是在析构函数中释放的。
迭代器iteraotr
迭代器iteraotr是STL的一个重要组成部分,通过iterator可以很方便的存储集合中的元素.STL为每个集合都写了一个迭代器, 迭代器其实是对一个指针的包装,实现一些常用的方法,如++,–,!=,==,*,->等, 通过这些方法可以找到当前元素或是别的元素. vector是STL集合中比较特殊的一个,因为vector中的每个元素都是连续的,所以在自己实现vector的时候可以用指针代替。
map、hashtable、deque、list实现机理分别为红黑树、函数映射、双向队列、双向链表,他们的特性分别如下:
map实现原理
map内部实现了一个红黑树(红黑树是非严格平衡的二叉搜索树,而AVL是严格平衡二叉搜索树),红黑树有自动排序的功能,因此map内部所有元素都是有序的,红黑树的每一个节点都代表着map的一个元素。因此,对于map进行的查找、删除、添加等一系列的操作都相当于是对红黑树进行的操作。map中的元素是按照二叉树(又名二叉查找树、二叉排序树)存储的,特点就是左子树上所有节点的键值都小于根节点的键值,右子树所有节点的键值都大于根节点的键值。使用中序遍历可将键值按照从小到大遍历出来。
hashtable(也称散列表,直译作哈希表)实现原理
hashtable采用了函数映射的思想记录的存储位置与记录的关键字关联起来,从而能够很快速地进行查找。这决定了哈希表特殊的数据结构,它同数组、链表以及二叉排序树等相比较有很明显的区别,它能够快速定位到想要查找的记录,而不是与表中存在的记录的关键字进行比较来进行查找。
deque实现原理
deque内部实现的是一个双向队列。元素在内存连续存放。随机存取任何元素都在常数时间完成(仅次于vector)。所有适用于vector的操作都适用于deque。在两端增删元素具有较佳的性能(大部分情况下是常数时间)。
list实现原理
list内部实现的是一个双向链表。元素在内存不连续存放。在任何位置增删元素都能在常数时间完成。不支持随机存取。无成员函数,给定一个下标i,访问第i个元素的内容,只能从头部挨个遍历到第i个元素。
迭代器和指针之间的区别
**迭代器不是指针,是类模板,表现的像指针。**他只是模拟了指针的一些功能,重载了指针的一些操作符,–>、++、–等。迭代器封装了指针,是一个”可遍历STL( Standard Template Library)容器内全部或部分元素”的对象,本质是封装了原生指针,是指针概念的一种提升,提供了比指针更高级的行为,相当于一种智能指针,他可以根据不同类型的数据结构来实现不同的++,–等操作。
迭代器返回的是对象引用而不是对象的值,所以cout只能输出迭代器使用取值后的值而不能直接输出其自身。
vector和list特性
vector特性 动态数组。元素在内存连续存放。随机存取任何元素都在常数时间完成。在尾端增删元素具有较大的性能(大部分情况下是常数时间)。
list特性 双向链表。元素在内存不连续存放。在任何位置增删元素都能在常数时间完成。不支持随机存取。
vector增删元素
对于vector而言,删除某个元素以后,该元素后边的每个元素的迭代器都会失效,后边每个元素都往前移动一位,erase返回下一个有效的迭代器。
list增删元素
对于list而言,删除某个元素,只有“指向被删除元素”的那个迭代器失效,其它迭代器不受任何影响。
vector 一维数组(元素在内存连续存放)
是动态数组,在堆中分配内存,元素连续存放,有保留内存,如果减少大小后,内存也不会释放;如果新增大小当前大小时才会重新分配内存。
扩容方式:
a. 倍放开辟三倍的内存
b. 旧的数据拷贝到新的内存
c. 释放旧的内存
d. 指向新内存
list 双向链表(元素存放在堆中)
元素存放在堆中,每个元素都是放在一块内存中,它的内存空间可以是不连续的,通过指针来进行数据的访问,这个特点,使得它的随机存取变得非常没有效率,因此它没有提供[ ]操作符的重载。但是由于链表的特点,它可以很有效的支持任意地方的删除和插入操作。
特点:
a. 随机访问不方便
b. 删除插入操作方便
常见时间复杂度
(1)vector插入、查找、删除时间复杂度分别为:O(n)、O(1)、O(n);
(2)list插入、查找、删除时间复杂度分别为:O(1)、O(n)、O(1)。
map和unordered_map的区别在于他们的实现基理不同。
map实现机理
map内部实现了一个红黑树(红黑树是非严格平衡的二叉搜索树,而AVL是严格平衡二叉搜索树),
unordered_map实现机理
unordered_map内部实现了一个哈希表(也叫散列表),通过把关键码值映射到Hash表中一个位置来访问记录,查找时间复杂度可达O(1),其中在海量数据处理中有着广泛应用。因此,元素的排列顺序是无序的。
map是关联式容器,它们的底层容器都是红黑树。map 的所有元素都是 pair,同时拥有实值(value)和键值(key)。pair 的第一元素被视为键值,第二元素被视为实值。所有元素都会根据元素的键值自动被排序。不允许键值重复。
map的特性如下
(1)map以RBTree作为底层容器;
(2)所有元素都是键+值存在;
(3)不允许键重复;
(4)所有元素是通过键进行自动排序的;
(5)map的键是不能修改的,但是其键对应的值是可以修改的。
set是一种关联式容器,其特性如下:
(1)set以RBTree作为底层容器
(2)所得元素的只有key没有value,value就是key
(3)不允许出现键值重复
(4)所有的元素都会被自动排序
(5)不能通过迭代器来改变set的值,因为set的值就是键,set的迭代器是const的
map和set一样是关联式容器,其特性如下:
(1)map以RBTree作为底层容器
(2)所有元素都是键+值存在
(3)不允许键重复
(4)所有元素是通过键进行自动排序的
(5)map的键是不能修改的,但是其键对应的值是可以修改的
综上所述,map和set底层实现都是红黑树;map和set的区别在于map的值不作为键,键和值是分开的。
左值指既能够出现在等号左边,也能出现在等号右边的变量。
左值是可寻址的变量,有持久性;
右值则是只能出现在等号右边的变量。
右值一般是不可寻址的常量,或在表达式求值过程中创建的无名临时对象,短暂性的。
左值和右值主要的区别之一是左值可以被修改,而右值不能。
int a; // a 为左值
a = 3; // 3 为右值
int x = 6; // x是左值,6是右值
int &y = x; // 左值引用,y引用x
int &z1 = x * 6; // 错误,x*6是一个右值
const int &z2 = x * 6; // 正确,可以将一个const引用绑定到一个右值
int &&z3 = x * 6; // 正确,右值引用
int &&z4 = x; // 错误,x是一个左值
Qt的核心机制包括:元对象系统、属性系统、信号与槽
元对象系统:
元对象系统(meta-object)提供了用于内部对象通讯的信号与槽(signals & slots)机制,运行时类型信息,以及动态属性系统(dynamic property system),整个元对象系统基于三个东西建立:
moc工具读取c++源文件。如果它找到一个或多个包含Q_OBJECT宏的类声明,它会生成另一个c++源文件,其中包含每个类的元对象代码。生成的源文件要么#include到类的源文件中,要么(更常见的情况)编译并链接到类的实现。
属性系统:
如同很多编译器厂商提供的编译器一样,Qt也提供了一个精妙的属性系统。
然而,作为一个独立于编译器和架构的库,Qt不依赖于诸如__property或[property]这样的非标准的编译器特性。Qt的这套属性系统特性可以用于任何Qt支持的编译器与架构。它基于元对象系统(Meta-Object System),这套系统同时也提供信号与槽机制用于对象间通讯。
信号与槽:信号与槽机制是Qt的核心特性,也是与其他框架最大的不同之处,Qt的元对象系统使得信号与槽机制得以实现。
信号与槽的具体流程,可以将信号和槽理解成 命令-执行,即信号就是命令,槽就是执行命令。
参考文章:https://blog.csdn.net/ddllrrbb/article/details/88374350
优点:
缺点:
一个长度为N的整形数组,数组中每个元素的取值范围是[0,n-1],判断该数组否有重复的数,请说一下你的思路并手写代码?
浅析红黑树(RBTree)原理及实现_芮小谭的博客-CSDN博客
注:堆雪差炮击,统计快归西
请问求第k大的数的方法以及各自的复杂度是怎样的,另外追问一下,当有相同元素时,还可以使用什么不同的方法求第k大的元素?
请你来介绍一下各种排序算法及时间复杂度?
请你说一说你知道的排序算法及其复杂度?
请问海量数据如何去取最大的k个?
请问快排的时间复杂度最差是多少?什么时候时间最差?
请问稳定排序哪几种?
请你介绍一下快排算法;以及什么是稳定性排序,快排是稳定性的吗;快排算法最差情况推导公式?
请你来说一说hash表的实现,包括STL中的哈希桶长度常数?
请你回答一下hash表如何rehash,以及怎么处理其中保存的资源?
请你说一下哈希表的桶个数为什么是质数,合数有何不妥?
请你说一下解决hash冲突的方法?
请你说一说哈希冲突的解决方法?
给你一个字符串,找出第一个不重复的字符,如“abbbabcd”,则第一个不重复就是c?
请问加密方法都有哪些?
什么是LRU缓存?
请你说一说洗牌算法?
Linux文件的基本权限就有九个,分别是owner/group/others三种身份各有自己的read/write/execute权限
修改权限指令:chmod
举例:文件的权限字符为 -rwxrwxrwx 时,这九个权限是三个三个一组。其中,我们可以使用数字来代表各个权限。
各权限的分数对照如下:
r | w | x |
---|---|---|
4 | 2 | 1 |
每种身份(owner/group/others)各自的三个权限(r/w/x)分数是需要累加的,
例如当权限为: [-rwxrwx—] ,则分数是:
owner = rwx = 4+2+1 = 7
group = rwx = 4+2+1 = 7
others= — = 0+0+0 = 0
所以我们设定权限的变更时,该文件的权限数字就是770!变更权限的指令chmod的语法是这样的:
定义不同
软链接又叫符号链接,这个文件包含了另一个文件的路径名。可以是任意文件或目录,可以链接不同文件系统的文件。
硬链接就是一个文件的一个或多个文件名。把文件名和计算机文件系统使用的节点号链接起来。因此我们可以用多个文件名与同一个文件进行链接,这些文件名可以在同一目录或不同目录。
限制不同
硬链接只能对已存在的文件进行创建,不能交叉文件系统进行硬链接的创建;
软链接可对不存在的文件或目录创建软链接;可交叉文件系统;
创建方式不同
硬链接不能对目录进行创建,只可对文件创建;
软链接可对文件或目录创建;
影响不同
删除一个硬链接文件并不影响其他有相同 inode 号的文件。
删除软链接并不影响被指向的文件,但若被指向的原文件被删除,则相关软连接被称为死链接(即 dangling link,若被指向路径文件被重新创建,死链接可恢复为正常的软链接)。
GDB调试:gdb调试的是可执行文件,在gcc编译时加入 -g ,告诉gcc在编译时加入调试信息,这样gdb才能调试这个被编译的文件 gcc -g tesst.c -o test
GDB命令格式:
quit:退出gdb,结束调试
list:查看程序源代码
list 5,10:显示5到10行的代码
list test.c:5, 10: 显示源文件5到10行的代码,在调试多个文件时使用
list get_sum: 显示get_sum函数周围的代码
list test,c get_sum: 显示源文件get_sum函数周围的代码,在调试多个文件时使用
reverse-search:字符串用来从当前行向前查找第一个匹配的字符串
run:程序开始执行
help list/all:查看帮助信息
break:设置断点
break 7:在第七行设置断点
break get_sum:以函数名设置断点
break 行号或者函数名 if 条件:以条件表达式设置断点
watch 条件表达式:条件表达式发生改变时程序就会停下来
next:继续执行下一条语句 ,会把函数当作一条语句执行
step:继续执行下一条语句,会跟踪进入函数,一次一条的执行函数内的代码
**条件断点:**break if 条件 以条件表达式设置断点
**多进程下如何调试:**用set follow-fork-mode child 调试子进程
或者set follow-fork-mode parent 调试父进程
在进行网络通信时是否需要进行字节序转换?
相同字节序的平台在进行网络通信时可以不进行字节序转换,但是跨平台进行网络数据通信时必须进行字节序转换。
原因如下:网络协议规定接收到得第一个字节是高字节,存放到低地址,所以发送时会首先去低地址取数据的高字节。小端模式的多字节数据在存放时,低地址存放的是低字节,而被发送方网络协议函数发送时会首先去低地址取数据(想要取高字节,真正取得是低字节),接收方网络协议函数接收时会将接收到的第一个字节存放到低地址(想要接收高字节,真正接收的是低字节),所以最后双方都正确的收发了数据。而相同平台进行通信时,如果双方都进行转换最后虽然能够正确收发数据,但是所做的转换是没有意义的,造成资源的浪费。而不同平台进行通信时必须进行转换,不转换会造成错误的收发数据,字节序转换函数会根据当前平台的存储模式做出相应正确的转换,如果当前平台是大端,则直接返回不进行转换,如果当前平台是小端,会将接收到得网络字节序进行转换。
网络字节序
网络上传输的数据都是字节流,对于一个多字节数值,在进行网络传输的时候,先传递哪个字节?也就是说,当接收端收到第一个字节的时候,它将这个字节作为高位字节还是低位字节处理,是一个比较有意义的问题; UDP/TCP/IP协议规定:把接收到的第一个字节当作高位字节看待,这就要求发送端发送的第一个字节是高位字节;而在发送端发送数据时,发送的第一个字节是该数值在内存中的起始地址处对应的那个字节,也就是说,该数值在内存中的起始地址处对应的那个字节就是要发送的第一个高位字节(即:高位字节存放在低地址处);由此可见,多字节数值在发送之前,在内存中因该是以大端法存放的; 所以说,网络字节序是大端字节序; 比如,我们经过网络发送整型数值0x12345678时,在80X86平台中,它是以小端发存放的,在发送之前需要使用系统提供的字节序转换函数htonl()将其转换成大端法存放的数值;
小端模式:低的有效字节存储在低的存储器地址。小端一般为主机字节序;常用的X86结构是小端模式。很多的ARM,DSP都为小端模式。
大端模式:高的有效字节存储在低的存储器地址。大端为网络字节序;KEIL C51则为大端模式。
有些ARM处理器还可以由硬件来选择是大端模式还是小端模式。
如何判断:我们可以根据联合体来判断系统是大端还是小端。因为联合体变量总是从低地址存储。
操作系统如何管理内存:
物理内存:物理内存有四个层次,分别是寄存器、高速缓存、主存、磁盘。
寄存器:速度最快、量少、价格贵。
高速缓存:次之。
主存:再次之。
磁盘:速度最慢、量多、价格便宜。
操作系统会对物理内存进行管理,有一个部分称为内存管理器(memory manager),它的主要工作是有效的管理内存,记录哪些内存是正在使用的,在进程需要时分配内存以及在进程完成时回收内存。
虚拟内存:操作系统为每一个进程分配一个独立的地址空间,但是虚拟内存。虚拟内存与物理内存存在映射关系,通过页表寻址完成虚拟地址和物理地址的转换。
操作系统如何申请内存:
从操作系统角度来看,进程分配内存有两种方式,分别由两个系统调用完成:*brk和mmap
说堆栈溢出是什么,会怎么样?
简述操作系统中malloc的实现原理?
说说进程空间从高位到低位都有些什么?
32位系统能访问4GB以上的内存吗?
参考回答
LRU算法:LRU算法用于缓存淘汰。思路是将缓存中最近最少使用的对象删除掉
实现方式:利用链表和hashmap。
当需要插入新的数据项的时候,如果新数据项在链表中存在(一般称为命中),则把该节点移到链表头部,如果不存在,则新建一个节点,放到链表头部,若缓存满了,则把链表最后一个节点删除即可。
在访问数据的时候,如果数据项在链表中存在,则把该节点移到链表头部,否则返回-1。这样一来在链表尾部的节点就是最近最久未访问的数据项。
一个linux的线程大概占8M内存。
linux的栈是通过缺页来分配内存的,不是所有栈地址空间都分配了内存。因此,8M是最大消耗,实际的内存消耗只会略大于实际需要的内存(内部损耗,每个在4k以内)。
说说进程、线程、协程是什么,区别是什么?
有了进程,为什么还要有线程?
说说多线程和多进程的不同?
多线程和单线程有什么区别,多线程编程要注意什么,多线程加锁需要注意什么?
说说进程同步的方式?
请你说说Linux的fork的作用?
互斥量能不能在进程中使用?
说说sleep和wait的区别?
1、sleep是一个延时函数,让进程或线程进入休眠。
休眠完毕后继续运行。在linux下面,sleep函数的参数是秒,而windows下面sleep的函数参数是毫秒
2、wait是父进程回收子进程PCB资源的一个系统调用。
进程一旦调用了wait函数,就立即阻塞自己本身,然后由wait函数自动分析当前进程的某个子进程是否已经退出,当找到一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞,直到有一个出现为止。
区别:
1、sleep是一个延时函数,让进程或线程进入休眠。休眠完毕后继续运行。
2、wait是父进程回收子进程PCB(Process Control Block)资源的一个系统调用
说说线程池的设计思路,线程池中线程的数量由什么确定?
进程和线程相比,为什么慢?
管道、信号量、消息队列、共享内存、套接字、剪切板、油槽
请你说说什么是孤儿进程,什么是僵尸进程,如何解决僵尸进程?
请你说说什么是守护进程,如何实现?
说说进程通信的方式有哪些?
说说进程有多少种状态?
进程通信中的管道实现原理是什么?
简述mmap的原理和使用场景?
协程是轻量级线程,轻量级表现在哪里?
说说常见信号有哪些,表示什么含义?
说说线程间通信的方式有哪些?
说说线程同步方式有哪些?
说说什么是信号量,有什么作用?
进程、线程的中断切换的过程是怎样的?
请你说说线程有哪些状态,相互之间怎么转换?
说说什么是死锁,产生的条件,如何解决?
单核机器上写多线程程序,是否要考虑加锁,为什么?
简述互斥锁的机制,互斥锁与读写的区别?
简述自旋锁和互斥锁的使用场景?
select的特点:
注:select的出现具有跨时代的意义,其将原先并发的机制由多线程、多进程的形式改为单个进程就可以实现并发管理。
epoll的特点:
总结:poll特点
epoll与select的区别:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-whHo0LFC-1693106684788)(https://s2.loli.net/2023/07/21/OY7PDNuFUbGRWns.png)]
select,poll,epoll都是IO多路复用的机制,I/O多路复用就是通过一种机制,可以监视多个文件描述符,一旦某个文件描述符就绪(一般是读就绪或者写就绪),能够通知应用程序进行相应的读写操作。
区别:
(1)poll与select不同,通过一个pollfd数组向内核传递需要关注的事件,故没有描述符个数的限制,pollfd中的events字段和revents分别用于标示关注的事件和发生的事件,故pollfd数组只需要被初始化一次。
(2)select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。
(3)select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把当前进程往设备等待队列中挂一次,而epoll只要一次拷贝,而且把当前进程往等待队列上挂也只挂一次,这也能节省不少的开销。
select,epoll的使用场景:都是IO多路复用的机制,应用于高并发的网络编程的场景。I/O多路复用就是通过一种机制,可以监视多个文件描述符,一旦某个文件描述符就绪(一般是读就绪或者写就绪),能够通知应用程序进行相应的读写操作。
select,epoll的区别:
(1)每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大;而epoll保证了每个fd在整个过程中只会拷贝一次。
(2)每次调用select都需要在内核遍历传递进来的所有fd;而epoll只需要轮询一次fd集合,同时查看就绪链表中有没有就绪的fd就可以了。
(3)select支持的文件描述符数量太小了,默认是1024;而epoll没有这个限制,它所支持的fd上限是最大可以打开文件的数目,这个数字一般远大于2048。
epoll水平触发与边缘触发的区别
LT模式(水平触发)下,只要这个fd还有数据可读,每次 epoll_wait都会返回它的事件,提醒用户程序去操作;
而在ET(边缘触发)模式中,它只会提示一次,直到下次再有数据流入之前都不会再提示了,无论fd中是否还有数据可读。
在高性能的I/O设计中,有两个比较著名的模式Reactor和Proactor模式,其中Reactor模式用于同步I/O,而Proactor运用于异步I/O操作。
Reactor模式:Reactor模式应用于同步I/O的场景。Reactor中读操作的具体步骤如下:
读取操作:
(1)应用程序注册读就需事件和相关联的事件处理器
(2)事件分离器等待事件的发生
(3)当发生读就需事件的时候,事件分离器调用第一步注册的事件处理器
(4)事件处理器首先执行实际的读取操作,然后根据读取到的内容进行进一步的处理
Proactor模式:Proactor模式应用于异步I/O的场景。Proactor中读操作的具体步骤如下:
(1)应用程序初始化一个异步读取操作,然后注册相应的事件处理器,此时事件处理器不关注读取就绪事件,而是关注读取完成事件,这是区别于Reactor的关键。
(2)事件分离器等待读取操作完成事件
(3)在事件分离器等待读取操作完成的时候,操作系统调用内核线程完成读取操作,并将读取的内容放入用户传递过来的缓存区中。这也是区别于Reactor的一点,Proactor中,应用程序需要传递缓存区。
(4)事件分离器捕获到读取完成事件后,激活应用程序注册的事件处理器,事件处理器直接从缓存区读取数据,而不需要进行实际的读取操作。
区别:从上面可以看出,Reactor中需要应用程序自己读取或者写入数据,而Proactor模式中,应用程序不需要用户再自己接收数据,直接使用就可以了,操作系统会将数据从内核拷贝到用户区。
同步与异步的区别:
同步:是所有的操作都做完,才返回给用户结果。即写完数据库之后,再响应用户,用户体验不好。
异步:不用等所有操作都做完,就响应用户请求。即先响应用户请求,然后慢慢去写数据库,用户体验较好。
阻塞与非阻塞的区别:
阻塞:调用者调用了某个函数,等待这个函数返回,期间什么也不做,不停的检查这个函数有没有返回,必须等这个函数返回后才能进行下一步动作。
非阻塞:非阻塞等待,每隔一段时间就去检查IO事件是否就绪。没有就绪就可以做其他事情。
BIO(Blocking I/O):阻塞IO。调用者调用了某个函数,等待这个函数返回,期间什么也不做,不停的检查这个函数有没有返回,必须等这个函数返回后才能进行下一步动作。
NIO(New I/O):同时支持阻塞与非阻塞模式,NIO的做法是叫一个线程不断的轮询每个IO的状态,看看是否有IO的状态发生了改变,从而进行下一步的操作。
服务器端函数:
(1)socket创建一个套接字
(2)bind绑定ip和port
(3)listen使套接字变为可以被动链接
(4)accept等待客户端的链接
(5)write/read接收发送数据
(6)close关闭连接
客户端函数:
(1)创建一个socket,用函数socket()
(2)bind绑定ip和port
(3)连接服务器,用函数connect()
(4)收发数据,用函数send()和recv(),或read()和write()
(5)close关闭连接
路由可分为静态&动态路由。静态路由由管理员手动维护;动态路由由路由协议自动维护。
路由选择算法的必要步骤:
1)向其它路由器传递路由信息;
2)接收其它路由器的路由信息;
3)根据收到的路由信息计算出到每个目的网络的最优路径,并由此生成路由选择表;
4)根据网络拓扑的变化及时的做出反应,调整路由生成新的路由选择表,同时把拓扑变化以路由 信息的形式向其它路由器宣告。
两种主要算法:距离向量法(Distance Vector Routing)和链路状态算法(Link-State Routing)。
由此可分为距离矢量(如:RIP、IGRP、EIGRP)&链路状态路由协议(如:OSPF、IS-IS)。 路由协议是路由器之间实现路由信息共享的一种机制,它允许路由器之间相互交换和维护各 自的路由表。当一台路由器的路由表由于某种原因发生变化时,它需要及时地将这一变化通 知与之相连接的其他路由器,以保证数据的正确传递。路由协议不承担网络上终端用户之间 的数据传输任务。
1)RIP 路由协议:RIP 协议最初是为 Xerox 网络系统的 Xerox parc 通用协议而设计的,是 Internet 中常用的 路由协议。RIP 采用距离向量算法,即路由器根据距离选择路由,所以也称为距离向量协议。 路由器收集所有可到达目的地的不同路径,并且保存有关到达每个目的地的最少站点数的路 径信息,除到达目的地的最佳路径外,任何其它信息均予以丢弃。同时路由器也把所收集的 路由信息用 RIP 协议通知相邻的其它路由器。这样,正确的路由信息逐渐扩散到了全网。RIP 使用非常广泛,它简单、可靠,便于配置。但是 RIP 只适用于小型的同构网络,因 为它允许的最大站点数为 15,任何超过 15 个站点的目的地均被标记为不可达。而且 RIP 每 隔 30s 一次的路由信息广播也是造成网络的广播风暴的重要原因之一。
2)OSPF 路由协议:0SPF 是一种基于链路状态的路由协议,需要每个路由器向其同一管理域的所有其它路 由器发送链路状态广播信息。在 OSPF 的链路状态广播中包括所有接口信息、所有的量度和 其它一些变量。利用 0SPF 的路由器首先必须收集有关的链路状态信息,并根据一定的算法 计算出到每个节点的最短路径。而基于距离向量的路由协议仅向其邻接路由器发送有关路由 更新信息。与 RIP 不同,OSPF 将一个自治域再划分为区,相应地即有两种类型的路由选择方式: 当源和目的地在同一区时,采用区内路由选择;当源和目的地在不同区时,则采用区间路由 选择。这就大大减少了网络开销,并增加了网络的稳定性。当一个区内的路由器出了故障时 并不影响自治域内其它区路由器的正常工作,这也给网络的管理、维护带来方便。
3)BGP 和 BGP4 路由协议:BGP 是为 TCP/IP 互联网设计的外部网关协议,用于多个自治域之间。它既不是基于纯 粹的链路状态算法,也不是基于纯粹的距离向量算法。它的主要功能是与其它自治域的 BGP 交换网络可达信息。各个自治域可以运行不同的内部网关协议。BGP 更新信息包括网络号/ 自治域路径的成对信息。自治域路径包括到达某个特定网络须经过的自治域串,这些更新信 息通过 TCP 传送出去,以保证传输的可靠性。为了满足 Internet 日益扩大的需要,BGP 还在不断地发展。在最新的 BGP4 中,还可以 将相似路由合并为一条路由。
4)IGRP 和 EIGRP 协议:EIGRP 和早期的 IGRP 协议都是由 Cisco 发明,是基于距离向量算法的动态路由协议。 EIGRP(Enhanced Interior Gateway Routing Protocol)是增强版的 IGRP 协议。它属于动态内部网 关路由协议,仍然使用矢量-距离算法。但它的实现比 IGRP 已经有很大改进,其收敛特性 和操作效率比 IGRP 有显著的提高。它的收敛特性是基于 DUAL ( Distributed Update Algorithm ) 算法的。DUAL 算法使得路径 在路由计算中根本不可能形成环路。它的收敛时间可以与已存在的其他任何路由协议相匹敌
Enhanced IGRP 与其它路由选择协议之间主要区别包括:收敛宽速(Fast Convergence)、 支持变长子网掩模(Subnet Mask)、局部更新和多网络层协议。执行 Enhanced IGRP 的路由 器存储了所有其相邻路由表,以便于它能快速利用各种选择路径(Alternate Routes)。如果没有合适路径,Enhanced IGRP 查询其邻居以获取所需路径。直到找到合适路径,EnhancedIGRP 查询才会终止,否则一直持续下去。
EIGRP 不作周期性更新。取而代之,当路径度量标准改变时,Enhanced IGRP 只发送局 部更新(Partial Updates)信息。局部更新信息的传输自动受到限制,从而使得只有那些需 要信息的路由器才会更新。基于以上这两种性能,因此 Enhanced IGRP 损耗的带宽比 IGRP 少得多。
(1)在浏览器中输入www.qq.com域名,操作系统会先检查自己本地的hosts文件是否有这个网址映射关系,如果有,就先调用这个IP地址映射,完成域名解析。
(2)如果hosts里没有这个域名的映射,则查找本地DNS解析器缓存,是否有这个网址映射关系,如果有,直接返回,完成域名解析。
(3)如果hosts与本地DNS解析器缓存都没有相应的网址映射关系,首先会找TCP/IP参数中设置的首选DNS服务器,在此我们叫它本地DNS服务器,此服务器收到查询时,如果要查询的域名,包含在本地配置区域资源中,则返回解析结果给客户机,完成域名解析,此解析具有权威性。
(4)如果要查询的域名,不由本地DNS服务器区域解析,但该服务器已缓存了此网址映射关系,则调用这个IP地址映射,完成域名解析,此解析不具有权威性。
(5)如果本地DNS服务器本地区域文件与缓存解析都失效,则根据本地DNS服务器的设置(是否设置转发器)进行查询,如果未用转发模式,本地DNS就把请求发至13台根DNS,根DNS服务器收到请求后会判断这个域名(.com)是谁来授权管理,并会返回一个负责该顶级域名服务器的一个IP。本地DNS服务器收到IP信息后,将会联系负责.com域的这台服务器。这台负责.com域的服务器收到请求后,如果自己无法解析,它就会找一个管理.com域的下一级DNS服务器地址(qq.com)给本地DNS服务器。当本地DNS服务器收到这个地址后,就会找qq.com域服务器,重复上面的动作,进行查询,直至找到www.qq.com主机。
(6)如果用的是转发模式,此DNS服务器就会把请求转发至上一级DNS服务器,由上一级服务器进行解析,上一级服务器如果不能解析,或找根DNS或把转请求转至上上级,以此循环。不管是本地DNS服务器用是是转发,还是根提示,最后都是把结果返回给本地DNS服务器,由此DNS服务器再返回给客户机。
从客户端到本地DNS服务器是属于递归查询,而DNS服务器之间就是的交互查询就是迭代查询
通过修改本机host来干预域名解析,
打开浏览器,输入一个域名。比如输入www.163.com,这时,你使用的电脑会发出一个DNS请求到本地DNS服务器。本地DNS服务器一般都是你的网络接入服务器商提供,比如中国电信,中国移动。
DNS请求到达本地DNS服务器之后,本地DNS服务器会首先查询它的缓存记录,如果缓存中有此条记录,就可以直接返回结果。如果没有,本地DNS服务器还要向DNS根服务器进行查询。
根DNS服务器没有记录具体的域名和IP地址的对应关系,而是告诉本地DNS服务器,你可以到域服务器上去继续查询,并给出域服务器的地址。
本地DNS服务器继续向域服务器发出请求,在这个例子中,请求的对象是.com域服务器。.com域服务器收到请求之后,也不会直接返回域名和IP地址的对应关系,而是告诉本地DNS服务器,你的域名的解析服务器的地址。
最后,本地DNS服务器向域名的解析服务器发出请求,这时就能收到一个域名和IP地址对应关系,本地DNS服务器不仅要把IP地址返回给用户电脑,还要把这个对应关系保存在缓存中,以备下次别的用户查询时,可以直接返回结果,加快网络访问。
DNS劫持就是通过劫持了DNS服务器,通过某些手段取得某域名的解析记录控制权,进而修改此域名的解析结果,导致对该域名的访问由原IP地址转入到修改后的指定IP,其结果就是对特定的网址不能访问或访问的是假网址,从而实现窃取资料或者破坏原有正常服务的目的。DNS劫持通过篡改DNS服务器上的数据返回给用户一个错误的查询结果来实现的。
DNS劫持症状:在某些地区的用户在成功连接宽带后,首次打开任何页面都指向ISP提供的“电信互联星空”、“网通黄页广告”等内容页面。还有就是曾经出现过用户访问Google域名的时候出现了百度的网站。这些都属于DNS劫持。
网关即网络中的关卡,我们的互联网是一个一个的局域网、城域网、等连接起来的,在连接点上就是一个一个网络的关卡,即我们的网关,他是保证网络互连的,翻译和转换,使得不同的网络体系能够进行。
网内通信,即通信双方都位处同一网段中,数据传输无需经过路由器(或三层交换机),即可由本网段自主完成。
假设发送主机的ARP表中并无目的主机对应的表项,则发送主机会以目的主机IP地址为内容,广播ARP请求以期获知目的主机MAC地址,并通过交换机(除到达端口之外的所有端口发送,即洪泛(Flooding))向全网段主机转发,而只有目的主机接收到此ARP请求后会将自己的MAC地址和IP地址装入ARP应答后将其回复给发送主机,发送主机接收到此ARP应答后,从中提取目的主机的MAC地址,并在其ARP表中建立目的主机的对应表项(IP地址到MAC地址的映射),之后即可向目的主机发送数据,将待发送数据封装成帧,并通过二层设备(如交换机)转发至本网段内的目的主机,自此完成通信。
OSI七层模型 | 功能 | 对应的网络协议 | TCP/IP四层概念模型 |
---|---|---|---|
应用层 | 文件传输,文件管理,电子邮件的信息处理 | HTTP、TFTP, FTP, NFS, WAIS、SMTP | 应用层 |
表示层 | 确保一个系统的应用层发送的消息可以被另一个系统的应用层读取,编码转换,数据解析,管理数据的解密和加密。 | Telnet, Rlogin, SNMP, Gopher | 应用层 |
会话层 | 负责在网络中的两节点建立,维持和终止通信。 | SMTP, DNS | 应用层 |
传输层 | 定义一些传输数据的协议和端口。 | TCP, UDP | 传输层 |
网络层 | 控制子网的运行,如逻辑编址,分组传输,路由选择 | IP, ICMP, ARP, RARP, AKP, UUCP | 网络层 |
数据链路层 | 主要是对物理层传输的比特流包装,检测保证数据传输的可靠性,将物理层接收的数据进行MAC(媒体访问控制)地址的封装和解封装 | FDDI, Ethernet, Arpanet, PDN, SLIP, PPP,STP。HDLC,SDLC,帧中继 | 数据链路层 |
物理层 | 定义物理设备的标准,主要对物理连接方式,电气特性,机械特性等制定统一标准。 | IEEE 802.1A, IEEE 802.2到IEEE 802. | 数据链路层 |
三次握手:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-B69GP0pb-1693106684788)(https://s2.loli.net/2023/07/21/CzKN71gwJXTMSL6.png)]
四次挥手:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ECgKZrZp-1693106684789)(https://s2.loli.net/2023/07/21/RbiKruFBQWpl1GL.png)]
TCP通过三次握手建立链接:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TaWIqrln-1693106684789)(https://s2.loli.net/2023/07/21/6zIfcWVyiQbMDEa.png)]
TCP通过四次挥手关闭链接:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bQosu8Bj-1693106684792)(https://s2.loli.net/2023/07/21/WmOypPzDe2osvJQ.png)]
主机每次发送数据时,TCP就给每个数据包分配一个序列号并且在一个特定的时间内等待接收主机对分配的这个序列号进行确认,如果发送主机在一个特定时间内没有收到接收主机的确认,则发送主机会重传此数据包。接收主机利用序列号对接收的数据进行确认,以便检测对方发送的数据是否有丢失或者乱序等,接收主机一旦收到已经顺序化的数据,它就将这些数据按正确的顺序重组成数据流并传递到高层进行处理。
具体步骤如下:
(1)为了保证数据包的可靠传递,发送方必须把已发送的数据包保留在缓冲区;
(2)并为每个已发送的数据包启动一个超时定时器;
(3)如在定时器超时之前收到了对方发来的应答信息(可能是对本包的应答,也可以是对本包后续包的应答),则释放该数据包占用的缓冲区;
(4)否则,重传该数据包,直到收到应答或重传次数超过规定的最大次数为止。
(5)接收方收到数据包后,先进行CRC校验,如果正确则把数据交给上层协议,然后给发送方发送一个累计应答包,表明该数据已收到,如果接收方正好也有数据要发给发送方,应答包也可方在数据包中捎带过去。
TCP Tahoe/Reno
最初的实现,包括慢启动、拥塞避免两个部分。基于重传超时(retransmission timeout/RTO)和重复确认为条件判断是否发生了丢包。两者的区别在于:Tahoe算法下如果收到三次重复确认,就进入快重传立即重发丢失的数据包,同时将慢启动阈值设置为当前拥塞窗口的一半,将拥塞窗口设置为1MSS,进入慢启动状态;而Reno算法如果收到三次重复确认,就进入快重传,但不进入慢启动状态,而是直接将拥塞窗口减半,进入拥塞控制阶段,这称为“快恢复”。
而Tahoe和Reno算法在出现RTO时的措施一致,都是将拥塞窗口降为1个MSS,然后进入慢启动阶段。
TCP BBR(Bottleneck Bandwidth and Round-trip propagation time)
BBR是由Google设计,于2016年发布的拥塞算法。以往大部分拥塞算法是基于丢包来作为降低传输速率的信号,而BBR则基于模型主动探测。该算法使用网络最近出站数据分组当时的最大带宽和往返时间来建立网络的显式模型。数据包传输的每个累积或选择性确认用于生成记录在数据包传输过程和确认返回期间的时间内所传送数据量的采样率。该算法认为随着网络接口控制器逐渐进入千兆速度时,分组丢失不应该被认为是识别拥塞的主要决定因素,所以基于模型的拥塞控制算法能有更高的吞吐量和更低的延迟,可以用BBR来替代其他流行的拥塞算法,例如CUBIC。
TCP可靠性中最重要的一个机制是处理数据超时和重传。TCP协议要求在发送端每发送一个报文段,就启动一个定时器并等待确认信息;接收端成功接收新数据后返回确认信息。若在定时器超时前数据未能被确认,TCP就认为报文段中的数据已丢失或损坏,需要对报文段中的数据重新组织和重传。
TCP主要提供了检验和、序列号/确认应答、超时重传、最大消息长度、滑动窗口控制等方法实现了可靠性传输。
通过检验和的方式,接收端可以检测出来数据是否有差错和异常,假如有差错就会直接丢弃TCP段,重新发送。TCP在计算检验和时,会在TCP首部加上一个12字节的伪首部。检验和总共计算3部分:TCP首部、TCP数据、TCP伪首部
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-STTGDTHm-1693106684793)(https://s2.loli.net/2023/07/21/CR5unz1gtfKGU2a.png)]
这个机制类似于问答的形式。比如在课堂上老师会问你“明白了吗?”,假如你没有隔一段时间没有回应或者你说不明白,那么老师就会重新讲一遍。其实计算机的确认应答机制也是一样的,发送端发送信息给接收端,接收端会回应一个包,这个包就是应答包。
上述过程中,只要发送端有一个包传输,接收端没有回应确认包(ACK包),都会重发。或者接收端的应答包,发送端没有收到也会重发数据。这就可以保证数据的完整性。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5VrRX8Om-1693106684794)(https://s2.loli.net/2023/07/21/pD7AWUVsNuwM5jS.png)]
在建立TCP连接的时候,双方约定一个最大的长度(MSS)作为发送的单位,重传的时候也是以这个单位来进行重传。理想的情况下是该长度的数据刚好不被网络层分块。
我们上面提到的超时重传的机制存在效率低下的问题,发送一个包到发送下一个包要经过一段时间才可以。所以我们就想着能不能不用等待确认包就发送下一个数据包呢?这就提出了一个滑动窗口的概念。
窗口的大小就是在无需等待确认包的情况下,发送端还能发送的最大数据量。这个机制的实现就是使用了大量的缓冲区,通过对多个段进行确认应答的功能。通过下一次的确认包可以判断接收端是否已经接收到了数据,如果已经接收了就从缓冲区里面删除数据。
在窗口之外的数据就是还未发送的和对端已经收到的数据。那么发送端是怎么样判断接收端有没有接收到数据呢?或者怎么知道需要重发的数据有哪些呢?通过下面这个图就知道了。
滑动窗口协议是传输层进行流控的一种措施,接收方通过通告发送方自己的窗口大小,从而控制发送方的发送速度,从而达到防止发送方发送速度过快而导致自己被淹没的目的。
TCP的滑动窗口解决了端到端的流量控制问题,允许接受方对传输进行限制,直到它拥有足够的缓冲空间来容纳更多的数据。
TCP在发送数据时会设置一个计时器,若到计时器超时仍未收到数据确认信息,则会引发相应的超时或基于计时器的重传操作,计时器超时称为重传超时(RTO) 。另一种方式的重传称为快速重传,通常发生在没有延时的情况下。若TCP累积确认无法返回新的ACK,或者当ACK包含的选择确认信息(SACK)表明出现失序报文时,快速重传会推断出现丢包,需要重传。
如果第一次握手消息丢失,那么请求方不会得到ack消息,超时后进行重传
如果第二次握手消息丢失,那么请求方不会得到ack消息,超时后进行重传
如果第三次握手消息丢失,那么Server 端该TCP连接的状态为SYN_RECV,并且会根据 TCP的超时重传机制,会等待3秒、6秒、12秒后重新发送SYN+ACK包,以便Client重新发送ACK包。而Server重发SYN+ACK包的次数,可以设置/proc/sys/net/ipv4/tcp_synack_retries修改,默认值为5.如果重发指定次数之后,仍然未收到 client 的ACK应答,那么一段时间后,Server自动关闭这个连接。
client 一般是通过 connect() 函数来连接服务器的,而connect()是在 TCP的三次握手的第二次握手完成后就成功返回值。也就是说 client 在接收到 SYN+ACK包,它的TCP连接状态就为 established (已连接),表示该连接已经建立。那么如果 第三次握手中的ACK包丢失的情况下,Client 向 server端发送数据,Server端将以 RST包响应,方能感知到Server的错误。
TIME_WAIT状态也成为2MSL等待状态。每个具体TCP实现必须选择一个报文段最大生存时间MSL(Maximum Segment Lifetime),它是任何报文段被丢弃前在网络内的最长时间。这个时间是有限的,因为TCP报文段以IP数据报在网络内传输,而IP数据报则有限制其生存时间的TTL字段。
对一个具体实现所给定的MSL值,处理的原则是:当TCP执行一个主动关闭,并发回最后一个ACK,该连接必须在TIME_WAIT状态停留的时间为2倍的MSL。这样可让TCP再次发送最后的ACK以防这个ACK丢失(另一端超时并重发最后的FIN)。
这种2MSL等待的另一个结果是这个TCP连接在2MSL等待期间,定义这个连接的插口(客户的IP地址和端口号,服务器的IP地址和端口号)不能再被使用。这个连接只能在2MSL结束后才能再被使用。
理论上,四个报文都发送完毕,就可以直接进入CLOSE状态了,但是可能网络是不可靠的,有可能最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文
两个理由:
保证客户端发送的最后一个ACK报文段能够到达服务端。
这个ACK报文段有可能丢失,使得处于LAST-ACK状态的B收不到对已发送的FIN+ACK报文段的确认,服务端超时重传FIN+ACK报文段,而客户端能在2MSL时间内收到这个重传的FIN+ACK报文段,接着客户端重传一次确认,重新启动2MSL计时器,最后客户端和服务端都进入到CLOSED状态,若客户端在TIME-WAIT状态不等待一段时间,而是发送完ACK报文段后立即释放连接,则无法收到服务端重传的FIN+ACK报文段,所以不会再发送一次确认报文段,则服务端无法正常进入到CLOSED状态。
防止“已失效的连接请求报文段”出现在本连接中。
客户端在发送完最后一个ACK报文段后,再经过2MSL,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失,使下一个新的连接中不会出现这种旧的连接请求报文段。
假设客户端分别发送了两个数据包D1和D2给服务端,由于服务端一次读取到的字节数是不确定的,故可能存在以下4种情况。
(1)服务端分两次读取到了两个独立的数据包,分别是D1和D2,没有粘包和拆包;
(2)服务端一次接收到了两个数据包,D1和D2粘合在一起,被称为TCP粘包;
(3)服务端分两次读取到了两个数据包,第一次读取到了完整的D1包和D2包的部分内容,第二次读取到了D2包的剩余内容,这被称为TCP拆包;
(4)服务端分两次读取到了两个数据包,第一次读取到了D1包的部分内容D1_1,第二次读取到了D1包的剩余内容D1_2和D2包的整包。
如果此时服务端TCP接收滑窗非常小,而数据包D1和D2比较大,很有可能会发生第五种可能,即服务端分多次才能将D1和D2包接收完全,期间发生多次拆包。
HTTP Keep-Alive
在http早期,每个http请求都要求打开一个tpc socket连接,并且使用一次之后就断开这个tcp连接。使用keep-alive可以改善这种状态,即在一次TCP连接中可以持续发送多份数据而不会断开连接。通过使用keep-alive机制,可以减少tcp连接建立次数,也意味着可以减少TIME_WAIT状态连接,以此提高性能和提高httpd服务器的吞吐率(更少的tcp连接意味着更少的系统内核调用,socket的accept()和close()调用)。但是,keep-alive并不是免费的午餐,长时间的tcp连接容易导致系统资源无效占用。配置不当的keep-alive,有时比重复利用连接带来的损失还更大。所以,正确地设置keep-alive timeout时间非常重要。
TCP KEEPALIVE
链接建立之后,如果应用程序或者上层协议一直不发送数据,或者隔很长时间才发送一次数据,当链接很久没有数据报文传输时如何去确定对方还在线,到底是掉线了还是确实没有数据传输,链接还需不需要保持,这种情况在TCP协议设计中是需要考虑到的。TCP协议通过一种巧妙的方式去解决这个问题,当超过一段时间之后,TCP自动发送一个数据为空的报文给对方,如果对方回应了这个报文,说明对方还在线,链接可以继续保持,如果对方没有报文返回,并且重试了多次之后则认为链接丢失,没有必要保持链接。
TCP的keepalive机制和HTTP的keep-alive机制是说的完全不同的两个东西,tcp的keepalive是在ESTABLISH状态的时候,双方如何检测连接的可用行。而http的keep-alive说的是如何避免进行重复的TCP三次握手和四次挥手的环节。
端到端通信是针对传输层来说的,传输层为网络中的主机提供端到端的通信。因为无论tcp还是udp协议,都要负责把上层交付的数据从发送端传输到接收端,不论其中间跨越多少节点。只不过tcp比较可靠而udp不可靠而已。所以称之为端到端,也就是从发送端到接收端。
它是一个网络连接,指的是在数据传输之前,在发送端与接收端之间(忽略中间有多少设备)为数据的传输建立一条链路,链路建立以后,发送端就可以发送数据,知道数据发送完毕,接收端确认接收成功。 也就是说在数据传输之前,先为数据的传输开辟一条通道,然后在进行传输。从发送端发出数据到接收端接收完毕,结束。
端到端通信建立在点到点通信的基础之上,它是由一段段的点到点通信信道构成的,是比点到点通信更高一级的通信方式,完成应用程序(进程)之间的通信。
端到端的优点:
链路建立之后,发送端知道接收端一定能收到,而且经过中间交换设备时不需要进行存储转发,因此传输延迟小。
端到端传输的缺点:
(1)直到接收端收到数据为止,发送端的设备一直要参与传输。如果整个传输的延迟很长,那么对发送端的设备造成很大的浪费。
(2)如果接收设备关机或故障,那么端到端传输不可能实现。
点到点通信是针对数据链路层或网络层来说的,因为数据链路层只负责直接相连的两个节点之间的通信,一个节点的数据链路层接受ip层数据并封装之后,就把数据帧从链路上发送到与其相邻的下一个节点。 点对点是基于MAC地址和或者IP地址,是指一个设备发数据给与该这边直接连接的其他设备,这台设备又在合适的时候将数据传递给与它相连的下一个设备,通过一台一台直接相连的设备把数据传递到接收端。
直接相连的节点对等实体的通信叫点到点通信。它只提供一台机器到另一台机器之间的通信,不会涉及到程序或进程的概念。同时点到点通信并不能保证数据传输的可靠性,也不能说明源主机与目的主机之间是哪两个进程在通信。
由物理层、数据链路层和网络层组成的通信子网为网络环境中的主机提供点到点的服务
点到点的优点:
(1)发送端设备送出数据后,它的任务已经完成,不需要参与整个传输过程,这样不会浪费发送端设备的资源。
(2)即使接收端设备关机或故障,点到点传输也可以采用存储转发技术进行缓冲。
点到点的缺点:
点到点传输的缺点是发送端发出数据后,不知道接收端能否收到或何时能收到数据。
在一个网络系统的不同分层中,可能用到端到端传输,也可能用到点到点传输。如Internet网,IP及以下各层采用点到点传输,4层以上采用端到端传输。
区别 | TCP | UDP |
---|---|---|
1.是否有建立连接 | TCP协议是有连接的,意思是开始传输实际数据之前TCP的客户端和服务器端必须通过三次握手建立连接,会话结束之后也要结束连接。 | 而UDP是无连接的 |
2.保证数据按序发送 | TCP协议保证数据按序发送,按序到达,提供超时重传来保证可靠性, | 但是UDP不保证按序到达,甚至不保证到达,只是努力交付,即便是按序发送的序列也不保证按序送到。 |
3.协议首部字节数不同 | TCP协议所需资源多,TCP首部需20个字节(不算可选项) | UDP首部字段只需8个字节 |
4.流量控制和拥塞控制 | TCP有流量控制和拥塞控制 | UDP没有,网络拥堵不会影响发送端的发送速率 |
5.一对一或多对多 | TCP是一对一的连接 | 而UDP则可以支持一对一,多对多,一对多的通信 |
6.面向服务不同 | TCP面向的是字节流的服务 | UDP面向的是报文的服务 |
/*TCP头定义,共20个字节*/
typedef struct _TCP_HEADER {
short m_sSourPort; // 源端口号16bit
short m_sDestPort; // 目的端口号16bit
unsigned int m_uiSequNum; // 序列号32bit
unsigned int m_uiAcknowledgeNum; // 确认号32bit
short m_sHeaderLenAndFlag; // 前4位:TCP头长度;中6位:保留;后6位:标志位
short m_sWindowSize; // 窗口大小16bit
short m_sCheckSum; // 检验和16bit
short m_surgentPointer; // 紧急数据偏移量16bit
}__attribute__((packed))TCP_HEADER, *PTCP_HEADER;
/*
TCP头中的选项定义
kind(8bit)+Length(8bit,整个选项的长度,包含前两部分) +内容(如果有的话)
KIND =
1表示 无操作NOP,无后面的部分
2表示 maximum segment 后面的LENGTH就是maximum segment选项的长度(以byte为单位,1+1+内容部分长度)
3表示 windows scale 后面的LENGTH就是 windows scale选项的长度(以byte为单位,1+1+内容部分长度)
4表示 SACK permitted LENGTH为2,没有内容部分
5表示这是一个SACK包 LENGTH为2,没有内容部分
8表示时间戳,LENGTH为10,含8个字节的时间戳 */
typedef struct _TCP_OPTIONS {
char m_ckind;
char m_cLength;
char m_cContext[32];
}__attribute__((packed))TCP_OPTIONS, *PTCP_OPTIONS;
//UDP头部结构如下
/*UDP头定义,共8个字节*/
typedef struct _UDP_HEADER {
unsigned short m_usSourPort; // 源端口号16bit
unsigned short m_usDestPort; // 目的端口号16bit
unsigned short m_usLength; // 数据包长度16bit
unsigned short m_usCheckSum; // 校验和16bit
}__attribute__((packed))UDP_HEADER, *PUDP_HEADER;
TCP和UDP协议都是传输层协议。二者的区别主要有:
HTTP:是互联网上应用最为广泛的一种网络协议,是一个客户端和服务器端请求和应答的标准(TCP),用于从WWW服务器传输超文本到本地浏览器的传输协议。http的连接很简单,是无状态的.
HTTPS:是以安全为目标的HTTP通道,简单讲是HTTP的安全版,即HTTP下加入SSL层,HTTPS的安全基础是SSL,因此加密的详细内容就需要SSL。是建立一个信息安全通道,来保证数据传输的安全、确认网站的真实性。
HTTP与HTTPS的区别:
SSL是一种安全套接层协议,是Web浏览器与Web服务器之间安全交换信息的协议,提供两个基本的安全服务:鉴别与保密。
SSL协议的三个特性:
1、保密:在握手协议中定义了会话密钥后,所有的消息都被加密;
2、鉴别:可选的客户端认证,和强制的服务器端认证;
3、完整性:传送的消息包括消息完整性检查。
HTTP Referer是header的一部分,当浏览器向web服务器发送请求的时候,一般会带上Referer,告诉服务器该网页是从哪个页面链接过来的,服务器因此可以获得一些信息用于处理。
www.baidu.com
链接,那么点击进入这个www.baidu.com
,它的header信息里就有:Referer= http://www.google.com200 : 从状态码发出的请求被服务器正常处理。
204 : 服务器接收的请求已成功处理,但在返回的响应报文中不含实体的主体部分【即没有内容】。
206 : 部分的内容(如:客户端进行了范围请求,但是服务器成功执行了这部分的干请求)。
301 : 跳转,代表永久性重定向(请求的资源已被分配了新的URI,以后已使用资源,现在设置了URI)。
302 : 临时性重定向(请求的资源已经分配了新的URI,希望用户本次能够使用新的URI来进行访问)。
303 : 由于请求对应的资源存在的另一个URI(因使用get方法,定向获取请求的资源)。
304 : 客户端发送附带条件的请求时,服务器端允许请求访问资源,但因发生请求未满足条件的情况后,直接返回了304
307 : 临时重定向【该状态码与302有着相同的含义】。
400 : 请求报文中存在语法错误(当错误方式时,需修改请求的内容后,再次发送请求)。
401 : 发送的请求需要有通过HTTP认证的认证信息。
403 : 对请求资源的访问被服务器拒绝了。
404 : 服务器上无法找到请求的资源。
500 : 服务器端在执行请求时发生了错误。
503 : 服务器暂时处于超负载或者是正在进行停机维护,现在无法处理请求。
Cookie与Session都是会话的一种方式。它们的典型使用场景比如“购物车”,当你点击下单按钮时,服务端并不清楚具体用户的具体操作,为了标识并跟踪该用户,了解购物车中有几样物品,服务端通过为该用户创建Cookie/Session来获取这些信息。
加密过程:
认证过程:
介绍一下数据库分页
介绍一下SQL中的聚合函数
表跟表是怎么关联的?
说一说你对外连接的了解?
说一说数据库的左连接和右连接?
SQL中怎么将行转成列?
谈谈你对SQL注入的理解?
将一张表的部分数据更新到另一张表,该如何操作呢?
WHERE和HAVING有什么区别?
WHERE和HAVING有什么区别?
说一说你对MySQL索引的理解?
索引有哪几种?
如何创建及保存MySQL的索引?
MySQL怎么判断要不要加索引?
只要创建了索引,就一定会走索引吗?
如何判断数据库的索引有没有生效?
如何评估一个索引创建的是否合理?
索引是越多越好吗?
数据库索引失效了怎么办?
所有的字段都适合创建索引吗?
说一说索引的实现原理?
介绍一下数据库索引的重构过程?
MySQL的索引为什么用B+树?
联合索引的存储结构是什么,它的有效方式是什么?
MySQL的Hash索引和B树索引有什么区别?
聚簇索引和非聚簇索引有什么区别?
什么是联合索引?
select in语句中如何使用索引?
模糊查询语句中如何使用索引?
说一说你对数据库事务的了解?
事务有哪几种类型,它们之间有什么区别?
MySQL的ACID特性分别是怎么实现的?
谈谈MySQL的事务隔离级别?
MySQL的事务隔离级别是怎么实现的?
事务可以嵌套吗?
如何实现可重复读?
如何解决幻读问题?
MySQL事务如何回滚?
了解数据库的锁吗?
介绍一下间隙锁?
InnoDB中行级锁是怎么实现的?
数据库在什么情况下会发生死锁?
说说数据库死锁的解决办法
说一说你对数据库优化的理解?
该如何优化MySQL的查询?
怎样插入数据才能更高效?
表中包含几千万条数据该怎么办?
MySQL的慢查询优化有了解吗?
说一说你对explain的了解?
explain关注什么?
介绍一下数据库设计的三大范式?
说一说你对MySQL引擎的了解?
说一说你对redo log、undo log、binlog的了解?
谈谈你对MVCC的了解?
MySQL主从同步是如何实现的?
单例模式定义
保证一个类仅有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。
那么我们就必须保证:
(1)该类不能被复制。
(2)该类不能被公开的创造。
那么对于C++来说,它的构造函数,拷贝构造函数和赋值函数都不能被公开调用。
单例模式实现方式
单例模式通常有两种模式,分别为懒汉式单例和饿汉式单例。两种模式实现方式分别如下:
(1)懒汉式设计模式实现方式(2种)
a. 静态指针 + 用到时初始化
b. 局部静态变量
(2)饿汉式设计模式(2种)
a. 直接定义静态对象
b. 静态指针 + 类外初始化时new空间实现
工厂设计模式的定义
定义一个创建对象的接口,让子类决定实例化哪个类,而对象的创建统一交由工厂去生产,有良好的封装性,既做到了解耦,也保证了最少知识原则。
工厂设计模式分类
工厂模式属于创建型模式,大致可以分为三类,简单工厂模式、工厂方法模式、抽象工厂模式。听上去差不多,都是工厂模式。下面一个个介绍:
(1)简单工厂模式
它的主要特点是需要在工厂类中做判断,从而创造相应的产品。当增加新的产品时,就需要修改工厂类。
有一家生产处理器核的厂家,它只有一个工厂,能够生产两种型号的处理器核。客户需要什么样的处理器核,一定要显示地告诉生产工厂。下面给出一种实现方案
简单工厂模式可以根据需求,动态生成使用者所需类的对象,而使用者不用去知道怎么创建对象,使得各个模块各司其职,降低了系统的耦合性。
**缺点:**就是要增加新的核类型时,就需要修改工厂类。这就违反了开放封闭原则:软件实体(类、模块、函数)可以扩展,但是不可修改。
(2)工厂方法模式
所谓工厂方法模式,是指定义一个用于创建对象的接口,让子类决定实例化哪一个类。Factory Method使一个类的实例化延迟到其子类。
**举例:**这家生产处理器核的产家赚了不少钱,于是决定再开设一个工厂专门用来生产B型号的单核,而原来的工厂专门用来生产A型号的单核。这时,客户要做的是找好工厂,比如要A型号的核,就找A工厂要;否则找B工厂要,不再需要告诉工厂具体要什么型号的处理器核了。下面给出一个实现方案:
优点: 扩展性好,符合了开闭原则,新增一种产品时,只需增加改对应的产品类和对应的工厂子类即可。
**缺点:**每增加一种产品,就需要增加一个对象的工厂。如果这家公司发展迅速,推出了很多新的处理器核,那么就要开设相应的新工厂。在C++实现中,就是要定义一个个的工厂类。显然,相比简单工厂模式,工厂方法模式需要更多的类定义。
(3)抽象工厂模式
**举例:**这家公司的技术不断进步,不仅可以生产单核处理器,也能生产多核处理器。现在简单工厂模式和工厂方法模式都鞭长莫及。抽象工厂模式登场了。它的定义为提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。具体这样应用,这家公司还是开设两个工厂,一个专门用来生产A型号的单核多核处理器,而另一个工厂专门用来生产B型号的单核多核处理器,下面给出实现的代码:
优点: 工厂抽象类创建了多个类型的产品,当有需求时,可以创建相关产品子类和子工厂类来获取。
缺点: 扩展新种类产品时困难。抽象工厂模式需要我们在工厂抽象类中提前确定了可能需要的产品种类,以满足不同型号的多种产品的需求。但是如果我们需要的产品种类并没有在工厂抽象类中提前确定,那我们就需要去修改工厂抽象类了,而一旦修改了工厂抽象类,那么所有的工厂子类也需要修改,这样显然扩展不方便。
装饰器计模式的定义
指在不改变现有对象结构的情况下,动态地给该对象增加一些职责(即增加其额外功能)的模式,它属于对象结构型模式。
优点
(1)装饰器是继承的有力补充,比继承灵活,在不改变原有对象的情况下,动态的给一个对象扩展功能,即插即用;
(2)通过使用不用装饰类及这些装饰类的排列组合,可以实现不同效果;
(3)装饰器模式完全遵守开闭原则。
缺点
装饰模式会增加许多子类,过度使用会增加程序得复杂性。
装饰模式的结构与实现
通常情况下,扩展一个类的功能会使用继承方式来实现。但继承具有静态特征,耦合度高,并且随着扩展功能的增多,子类会很膨胀。如果使用组合关系来创建一个包装对象(即装饰对象)来包裹真实对象,并在保持真实对象的类结构不变的前提下,为其提供额外的功能,这就是装饰模式的目标。下面来分析其基本结构和实现方法。
装饰模式主要包含以下角色:
(1)抽象构件(Component)角色:定义一个抽象接口以规范准备接收附加责任的对象。
(2)具体构件(ConcreteComponent)角色:实现抽象构件,通过装饰角色为其添加一些职责。
(3)抽象装饰(Decorator)角色:继承抽象构件,并包含具体构件的实例,可以通过其子类扩展具体构件的功能。
(4)具体装饰(ConcreteDecorator)角色:实现抽象装饰的相关方法,并给具体构件对象添加附加的责任。
装饰模式的结构图如下图所示:
请观察者设计模式,如何实现?
点评:了解职位由来是做好该职位的前提,务必要了解。此外,建议用“咱们公司”来代替“贵公司”的说法,以拉近彼此距离。
点评:详细了解考核职位的方式,才是面试官最想听到的问题。它不仅体现了你对自己认真负责的态度,也让招聘方感受到了你严谨的思维方式。
点评:面试环节不仅仅是双方交换信息的过程,也是彼此拉近距离的机会。适时与面试官(也可能是你未来的老板)主动示好,会增加面试的印象分。
点评:关注公司的未来,也就是关心自己的未来。职业目标的设立不能只是一句空话,要借助平台的未来逐步实现。最后一定要了解一下公司或者部门未来的规划