不做c++了,备份下自己整理的高频率内容。
目录
Static:
1 全局静态变量:全局变量前加上static变成全局静态变量
存储在静态存储区,在整个程序运行期间存在
初始化:未初始化的全局静态变量会自动初始化为0
作用域:全局静态变量在声明他的其他文件是不可见得
2 局部静态变量: 局部变量前加上static变成局部静态变量
存储在静态存储区
初始化:未初始化的局部静态变量会自动初始化为0
在程序执行到该对象的声明处时只有首次会初始化,每次的值保持到下一次调用,直到 下次赋新值。
3 静态函数:在函数返回前加上static被定义为静态函数。函数的定义和声明在默认情况下 都是extern的,但是静态函数只是在声明他的文件中可见,不能被其他文件所用。
静态函数,只可在本cpp内使用,不会同其他cpp的同名函数引起冲突。
4 类的静态成员变量:静态成员变量可以实现为多个对象之间的数据共享,并且使用静态成 员变量还不会破坏隐藏的原则,保证了安全性。因此,静态成员变量是类的对象共 享的成员,而不是某个对象的成员,对多个对象来说,静态成员只存储一处,供所有对 象共用。
5 类的静态成员函数:静态成员函数和静态成员变量一样,它们都属于类的静态成员,它们 都不是对象成员,因此,对静态成员的引用不需要用对象名。
在静态成员函数的实现中不能直接引用类中说明的非静态成员,可以引用类中说明的静 态成员。如果需要引用非静态成员,可以通过对象来引用。
C++ & c的区别:
设计思想上:c++是面向对象的语言,c是面向过程的结构化编程语言
语法上:
C++支持范式编程,比如模本类,函数模板等
增加了类型安全的功能,比如强制类型转换
具有封装,继承和多态三种特性
指针和引用:
1 指针有自己的一块空间,而引用只是一个别名
2 使用sizeof看一个指针的大小是4,而引用则是被引用对象的大小
3 指针可以被初始化为NULL,而引用必须被初始化且必须是一个已有对象的引用
4 作为参数传递时,指针需要被解引用才可以对对象操作,而直接对引用的修改都会改变引用所指的对象
5 可以有const指针,没有const引用
6 指针可以指向其他对象,引用不能改变
7 指针可以有多级指针,而引用没有
8 指针和引用使用++运算符的意义不一样
为什么析构函数必须是虚函数
在实现多态时,当一个类被作为基类并且基类对派生类的对象进行操作,在析构时防止只析构基类而不析构派生类的状况发生。把基类的析构函数设计为虚函数可以在基类的指针指向派生类对象时,用基类的指针删除派生类对象,避免内存泄漏。
C++默认的析构函数不是虚函数,是因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。而对于不会被继承的类来说,其析构函数如果是虚函数,会浪费内存。因此c++默认的析构函数不是虚函数,只有当需要当做父类时,设置为虚函数。
C++析构函数的作用
析构函数和构造函数对应,当对象结束其生命周期,如对象所在的函数已调用完毕,系统会自动执行析构函数。
析构函数与类名相同,只是在函数名前面加一个取反符~,以区别构造函数。它不在参数,也没有返回值(包括void),只能有一个析构函数,不能重载。
如果用户没有编写一个析构函数,编译系统会自动生成一个缺省的析构函数,它也不进行任何操作。所以许多简单的类中没有用显示的析构函数。
如果一个类中有指针,且在使用的过程中动态的申请了内存,那么最好显示析构函数在销毁类之前,释放掉申请的内存空间,防止内存泄漏。
类析构顺序:1派生类本身的析构函数2对象成员的析构函数3 基类析构函数
静态函数和虚函数的区别:
静态函数在编译的时候就已经确定,虚函数在运行的时候动态绑定。虚函数因为用了虚函数表机制,所以会增加一次内存开销。
重载和重写:
重载:在同一作用域中,两个函数名相同,但是参数列表不同(类型、个数),返回值没有要求。
重写:子类继承父类。父类中的函数是虚函数,在子类中重新定义了这个函数,这种情况是重写。
虚函数和多态
多态的实现主要分为静态多态和动态多态,静态多态主要是重载,在编译的时候就已经确定。动态多态时用虚函数表机制实现的,在运行期间动态绑定。举个例子:使用父类的指针去调用子类中重写了的父类中的虚函数的时候,会调用子类重写过后的函数。
虚函数的实现:在有虚函数的类,类的最开始部分是一个虚函数表的指针,这个指针指向了一个虚函数表,表中放了虚函数的地址,实际的虚函数在代码段(.text)中。当子类继承了父类的时候也会继承其虚函数表,当子类重写父类中虚函数时候,会将其继承到的虚函数表中的地址替换为重新写的函数地址。使用虚函数,会增加内存开销,降低效率。
普通函数与函数模板的调用规则
1 如果函数模板和普通函数都可以实现,优先调用普通函数
2 可以通过空模板参数列表来强制调用函数模板
3 函数模板也可以发生重载
4 如果函数模板可以产生更好的匹配,优先调用函数模板
define和const
const用途:用来定义常量、修饰函数参数、修饰函数返回值,可以避免被修改,提高程序的健壮性。
define用途:是宏定义,在预编译的时候会进行替换,可以通过#undef取消定义,可以防止头文件重复引用。
区别:const定义的数据有数据类型,而宏没有数据类型。编译器可以对const常量进行类型检查。而对宏定义只进行字符替换,没有类型安全检查,所以字符替换时可能出错
define和typedef
1 执行时间不同:define是宏定义在预编译的时候会进行替换,而typedef是在编译阶段,具有类型安全检查的功能
2 功能不同:typedef用来定义类型的别名,这些类型不止包括内部类型(int,char等),还包括自定义类型(如struct),便于记忆;而define不止可以为类型起别名,还可以定义常量,变量等
3 作用域不同:define没有作用域的限制,只要之前预定义过的宏,在以后的程序中都可以使用;而typedef有自己的作用域
4 对指针的操作
Typedef int * pint;
#define PINT int *
pint s1, s2; //s1和s2都是int型指针
PINT s3, s4; //相当于int * s3,s4;只有一个是指针。
const修饰的作用
- const修饰函数:在类中将成员函数修饰为const表明在该函数体内,不能修改对象的数据成员而且不能调用非const函数。为什么不能调用非const函数?因为非const函数可能修改数据成员,const成员函数是不能修改数据成员的,所以在const成员函数内只能调用const函数。
- const修饰函数参数:防止传入的参数代表的内容在函数体内被改变,但仅对指针和引用有意义
- const修饰函数返回值:也是用const来修饰返回的指针或引用,保护指针指向的内容或引用的内容不被修改,也常用于运算符重载。归根究底就是使得函数调用表达式不能作为左值。
编写类String
class String
{
public:
String(const char *str = NULL); // 普通构造函数
String(const String &other); // 拷贝构造函数
~ String(void); // 析构函数
String & operator =(const String &other); // 赋值函数
private:
char *m_data; // 用于保存字符串
};
String::String(const char *str )
{
if(str==nullptr)
{
m_data=new char[1];
*m_data='\0';
}
else
{
int len=strlen(str);
m_data=new char[len+1];
strcpy(m_data,str);
}
}
String::String(const String &other)
{
int len=strlen(other.m_data);
m_data=new char[len+1];
strcpy(m_data,other.m_data);
}
String& String::operator=(const String& other)
{
if(this==other)
return *this;
delete []m_data;
int len=strlen(other.m_data);
m_data=new char[len+1];
strcpy(m_data,other.m_data);
return *this;
}
智能指针
智能指针主要用于管理在堆上分配的内存,它将普通的指针封装为一个栈对象。当栈对象的生存周期结束后,会在析构函数中释放掉申请的内存,从而防止内存泄漏。C++ 11中最常用的智能指针类型为shared_ptr,它采用引用计数的方法,记录当前内存资源被多少个智能指针引用。该引用计数的内存在堆上分配。当新增一个时引用计数加1,当过期时引用计数减一。只有引用计数为0时,智能指针才会自动释放引用的内存资源。对shared_ptr进行初始化时不能将一个普通指针直接赋值给智能指针,因为一个是指针,一个是类。可以通过make_shared函数或者通过构造函数传入普通指针。并可以通过get函数获得普通指针。
map的key定义为结构体?
A: Key用结构体变量,需要重写排序函数。如果排序函数出错,整个Map序列错乱。
B: Key用结构体变量,不用担心键值被修改带来的影响。
C: 用指针,无需重写排序算法
D: 用指针,可以修改结构体的值,value的值不受影响。Find也不会受影响。
E: 用指针,有被delete的风险
C++空类默认产生?
1 对于空类,编译器不会生成任何的成员函数,只会生成1个字节的占位符
2 编译器只会在需要的时候生成6个成员函数:一个缺省的构造函数、一个拷贝构造函数、一个析构函数、一个赋值运算符、一对取址运算符和一个this指针。
extern
- 当它与"C"一起连用时,是为了实现C和C++的混合编程,函数声明前加上extern "C"后,则编译器就会按照C语言的方式编译函数,这样C语言中就可以调用C++的函数了
2.extern可以置于变量或者函数前,以标示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义。
hash表如何rehash,以及怎么处理其中保存的资源
C++的hash表中有一个负载因子loadFactor,当loadFactor<1时,hash表查找的期望复杂度为O(1). 因此,每次往hash表中添加元素时,我们必须保证是在loadFactor <1的情况下,才能够添加。
因此,当Hash表中loadFactor==1时,Hash就需要进行rehash。rehash过程中,会模仿C++的vector扩容方式,Hash表中每次发现loadFactor ==1时,就开辟一个原来桶数组的两倍空间,称为新桶数组,然后把原来的桶数组中元素全部重新哈希到新的桶数组中。
hash表
hash表的实现主要包括构造哈希和处理哈希冲突两个方面:
对于构造哈希来说,主要包括直接地址法、平方取中法、除留余数法等。
对于处理哈希冲突来说,最常用的处理冲突的方法有开放定址法、再哈希法、链地址法、建立公共溢出区等方法。
开放定址法: 当发生地址冲突时,按照某种方法继续探测哈希表中的其他存储单元,直到找到空位置为止。
再哈希法:当发生哈希冲突时使用另一个哈希函数计算地址值,直到冲突不再发生。这种方法不易产生聚集,但是增加计算时间,同时需要准备许多哈希函数。
链地址法:将所有哈希值相同的Key通过链表存储。key按顺序插入到链表中
建立公共溢出区:采用一个溢出表存储产生冲突的关键字。如果公共溢出区还产生冲突,再采用处理冲突方法处理
其中开放定址法包扩:线性探测再散列,平方探测再散列,随机探测再散列。
哈希表的桶个数使用质数,可以最大程度减少冲突概率,使哈希后的数据分布的更加均匀。如果使用合数,可能会造成很多数据分布会集中在某些点上,从而影响哈希表效率
i++和++i效率比较
++i效率更高:
//++i 的版本 { *this = *this + 1; return *this; }
//i++ 的版本 { INT oldvalue = *this; *this = *this + 1; return oldvalue; }
Linux虚拟地址空间
为了防止不同进程同一时刻在物理内存中运行而对物理内存的争夺和践踏,采用了虚拟内存。
虚拟内存技术使得不同进程在运行过程中,它所看到的是自己独自占有了当前系统的4G内存。所有进程共享同一物理内存,每个进程把自己需要的虚拟内存空间映射并存储到物理内存上。 。
在C++中,虚拟内存分为代码段、数据段、BSS段、堆区、文件映射区以及栈区六部分。
代码段:包括只读存储区和文本区,其中只读存储区存储字符串常量,文本区存储程序的机器代码。
数据段:存储程序中已初始化的全局变量和静态变量
bss 段:存储未初始化的全局变量和静态变量(局部+全局),以及所有被初始化为0的全局变量和静态变量。
堆区:调用new/malloc函数时在堆区动态分配内存,同时需要调用delete/free来手动释放申请的内存。
映射区:存储动态链接库以及调用mmap函数进行的文件映射
栈:使用栈空间存储函数的返回地址、参数、局部变量、返回值
用户空间与内核空间
32位Linux内核虚拟地址空间划分0~3G为用户空间,3~4G为内核空间
为什么需要区分内核空间与用户空间?
在 CPU 的所有指令中,有些指令是非常危险的,如果错用,将导致系统崩溃,
在内核态下,进程运行在内核地址空间中,此时 CPU 可以执行任何指令,
在用户态下,进程运行在用户地址空间中,被执行的代码要受到 CPU 的诸多检查
如何从用户空间进入内核空间?
一般都通过系统调用(e.g.比如应用程序告诉内核需要读取磁盘上的文件)
进程线程
进程是对运行时程序的封装,是系统进行资源调度和分配的的基本单位,实现了操作系统的并发;
线程是进程的子任务,是CPU调度和分派的基本单位,用于保证程序的实时性,实现进程内部的并发;每个线程都独自占用一个虚拟处理器:独自的寄存器组,指令计数器和处理器状态。每个线程完成不同的任务,但是共享同一地址空间,打开的文件队列和其他内核资源。
系统开销:在创建或撤消进程时,系统都要为之分配或回收资源,如内存空间、I/o设备等。因此,操作系统所付出的开销将显著地大于在创建或撤消线程时的开销,进程切换的开销也远大于线程切换的开销
通信:同一进程中的多个线程具有相同的地址空间,所以同步和通信比较容易,
进程间不会相互影响 ;进程一个线程挂掉将导致整个进程挂掉
创建线程的方式
1 通过函数
2 通过类对象创建线程
3 通过lambda表达式创建线程
父子进程
子进程继承:
用户号和用户组号
环境
堆栈
共享内存
打开文件的描述符
进程组号
信号处理方式
当前工作目录
根目录
资源终端
管道
1 普通管道PIPE:
- 它是半双工的
- 它只能用于具有亲缘关系的进程之间的通信
- 它可以看成是一种特殊的文件,并不属于其他任何文件系统,并且只存在于内存中。
2 命名管道FIFO:
- FIFO可以在无血缘关系的进程之间交换数据
- FIFO有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中。
fork()与vfock()
1. fork ():子进程拷贝父进程的数据段,代码段
vfork ( ):子进程与父进程共享数据段
2. fork ()父子进程的执行次序不确定
vfork 保证子进程先运行,在调用exec 或exit 之前与父进程数据是共享的,在它调用exec或exit 之后父进程才可能被调度运行。
3. vfork 如果在调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁。
软连接和硬连接
1,软链接又称之为符号连接(Symbolic Link),可以理解成快捷方式。它和windows下的快捷方式的作用是一样的。
硬链接可以理解为cp -p 加 同步更新。
2 硬链接和原来的文件没有什么区别,而且共享一个 inode 号(文件在文件系统上的唯一标识);而软链接不共享 inode,也可以说是个特殊的 inode,所以和原来的 inode 有区别。
3若原文件删除了,则该软连接则不可以访问,而硬连接则是可以的。
4.由于符号链接的特性,导致其可以跨越磁盘分区,但硬链接不具备这个特性.
Linux内核主要由五个子系统组成?
进程调度,内存管理,虚拟文件系统,网络接口,进程间通信。
进程调度(SCHED):控制进程对CPU的访问。当需要选择下一个进程运行时, 由调度程序选择最值得运行的进程;
内存管理(MM)允许多个进程安全的共享主内存区域;
虚拟文件系统(VirtualFileSystem,VFS)隐藏了各种硬件的具体细节,为所有的设备提供了统一的接口,VFS提供了多达数十种不同的文件系统;
网络接口(NET)提供了对各种网络标准的存取和各种网络硬件的支持;
进程间通讯(IPC) 支持进程间各种通信机制。
Linux进程调度
linux内核将进程分成两个级别:普通进程和实时进程。实时进程的优先级都高于普通进程,除此之外,它们的调度策略也有所不同
1、SCHED_FIFO:先进先出。直到先被执行的进程变为非可执行状态,后来的进程才被调度执行。(在这种策略下,先来的进程可以执行sched_yield系统调用,自愿放弃CPU,以让权给后来的进程);
2、SCHED_RR:轮转调度。内核为实时进程分配时间片,在时间片用完时,让下一个进程使用CPU;
死锁:
1. 线程试图对同一个互斥量A加锁两次。
2. 线程1拥有A锁,请求获得B锁;线程2拥有B锁,请求获得A锁
互斥条件:一个资源每次只能被一个进程使用。
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
不可剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
解决:
1 一次性分配所有资源,这样就不会再有请求了
2当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源
死锁代码:
#include "pch.h"
#include <iostream>
#include <mutex>
class myTestMutex
{
private:
std::mutex m_mutex;
int m;
public:
myTestMutex(int m_m) : m(m_m) {}
~myTestMutex() {}
void Add(int number)
{
m_mutex.lock();
m += number;
m_mutex.unlock();
}
void PlusOne()
{
m_mutex.lock();
Add(1);
m_mutex.unlock();
}
};
int main()
{
myTestMutex test(0);
//调用Add方法是没有问题的,而且有原子操作的效果。
test.Add(10);
//但是一定调用PlusOne方法就出现问题了,因为进入PlusOne方法的时候会调用m_mutex.lock()方法,
//还没有调用m_mutex.unlock()方法就马上进入Add方法了,然后Add方法马上又调用了m_mutex.lock(),
//然后m_mutex.lock()因为前面在PlusOne方法调用m_mutex.lock()方法,所以就出现阻塞,然后在Add方法就走不下去了,
//然后因为PlusOne方法调用了Add方法,所以导致PlusOne方法也走不下去了,出现死锁了。
test.PlusOne();
}
结构体对齐,字节对齐
1、原因:
1)平台原因:不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2)性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
2、规则
1)数据成员对齐规则:第一个数据成员放在offset为0的地方,每个数据成员存储的起始位置要从该成员(每个成员本身)大小的整数倍开始。
2)结构(或联合)的整体对齐规则:结构体的总大小,也就是sizeof的结果,.必须是其内部最大成员的整数倍。
3)结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储
线程池
什么是线程池
线程池是一种多线程处理技术。线程池先创建好若干线程,并管理这些线程。当有新的任务到来时,将任务添加到一个已创建的空闲线程中执行。线程池所创建的线程优先级都是一样的,所以需要使用特定线程优先级的任务不宜使用线程池。
线程池的优点和应用
线程池统一管理线程的方式减少了频繁创建和销毁线程的系统调度开销,很大程度上提高了服务器处理并发任务的性能。
线程池适用于频繁的任务调度,如处理HTTP请求,任务多,并且任务周期小
IO复用
select、poll、epoll都是IO多路复用的机制,先是监听多个文件描述符FD,一旦某个FD就绪,就可以进行相应的读写操作
select:
- 单个进程可监视的fd数量被限制,即能监听端口的大小有限。
- 对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低:
- 需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大
Poll
描述fd集合的方式不同,poll使用pollfd结构而不是select的fd_set结构,它没有最大连接数的限制。
epoll
- 没有最大并发连接的限制,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口);
- 效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数
Level_triggered(水平触发):当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据一次性全部读写完,那么下次调用 epoll_wait()时,它还会通知你在上没读写完的文件描述符上继续读写,当然如果你一直不去读写,它会一直通知你
Edge_triggered(边缘触发):当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完,那么下次调用epoll_wait()时,它不会通知你
排序时间复杂度
操作系统中的buffer与cache
Buffer和Cache翻译成中文分别是“缓冲”和“缓存”。buffer和cache都是一部分内存,内存的作用就是缓解CPU和IO(如,磁盘)的速度差距的。CPU计算了一些数据后,在内存中进行临时存放,到一定数量后再统一放到硬盘中,这时要用的内存就是buffer;CPU要计算时,需要把数据从磁盘中读出来,临时先放到内存中,这部分内存就是cache。 cache 是为了弥补高速设备和低速设备的鸿沟而引入的中间层,最终起到加快访问速度的作用。而 buffer 的主要目的进行流量整形,把突发的大数量较小规模的 I/O 整理成平稳的小数量较大规模的 I/O,以减少响应次数
volatile 关键字
- 声明的类型变量表示可以被某些编译器未知的因素更改
- 编译器对访问该变量的代码就不再进行优化
用处:
1) 中断服务程序中修改的供其它程序检测的变量需要加 volatile;
2) 多线程共享的标志应该加 volatile;
3) 存储器映射的硬件寄存器加 volatile
五层协议
应用层 :为特定应用程序提供数据传输服务,例如 HTTP、DNS 等协议。数据单位为报文。
传输层 :为进程提供通用数据传输服务。由于应用层协议很多,定义通用的传输层协议就可以支持不断增多的应用层协议。传输层包括两种协议:传输控制协议 TCP,提供面向连接、可靠的数据传输服务,数据单位为报文段;用户数据报协议 UDP,提供无连接、尽最大努力的数据传输服务,数据单位为用户数据报。TCP 主要提供完整性服务,UDP 主要提供及时性服务。
网络层 :为主机提供数据传输服务。而传输层协议是为主机中的进程提供数据传输服务。网络层把传输层传递下来的报文段或者用户数据报封装成分组。
数据链路层 :网络层针对的还是主机之间的数据传输服务,而主机之间可以有很多链路,链路层协议就是为同一链路的主机提供数据传输服务。数据链路层把网络层传下来的分组封装成帧。
物理层 :考虑的是怎样在传输媒体上传输数据比特流,而不是指具体的传输媒体。物理层的作用是尽可能屏蔽传输媒体和通信手段的差异,使数据链路层感觉不到这些差异
类和结构体
1、存储类型:结构体是值类型,类是引用类型
- 继承性:结构体成员不能从继承自其他类或结构体,也不能被其他类或结构体继承(但可以实现接口),然而类可以
3、初始化:类可以在声明的时候初始化,结构不能在申明的时候初始化(不能在结构中初始化字段),否则报错。
4、构造函数:类和结构都有自己默认的构造函数。在类中,一旦我们编写了带参数构造函数,默认构造函数就不存在了。当我们要调用不带参数的构造函数来初始化对象时,我们必须再自己编写一个不带参数的构造函数。但是在结构中,始终存在一个不带参数的默认构造函数,并且,这个构造函数是不可替代的,不能重写,也不能覆盖,在结构中,我们只能编写带参数的构造函数,不能编写不带参数的构造函数。
5、析构函数:类有析构函数,但是结构没有析构函数。
Union,struct有什么区别
- struct和union都是由多个不同的数据类型成员组成, 但在任何同一时刻, union中只存放了一个被选中的成员; 而struct的所有成员都存在。在struct中,各成员都占有自己的内存空间,它们是同时存在的,一个struct变量的总长度等于所有成员长度之和,遵从字节对其原则; 在Union中,所有成员不能同时占用它的内存空间,它们不能同时存在 , Union变量的长度等于最长的成员的长度。
- 对于union的不同成员赋值, 将会对其它成员重写, 原来成员的值就不存在了,所以,共同体变量中起作用的成员是最后一次存放的成员; 而对于struct的不同成员赋值是互不影响的。
TCP哪些措施保证可性?
1、序列号、确认应答、超时重传
数据到达接收方,接收方需要发出一个确认应答,表示已经收到该数据段,并且确认序号会说明了它下一次需要接收的数据序列号。如果发送发迟迟未收到确认应答,那么可能是发送的数据丢失,也可能是确认应答丢失,这时发送方在等待一定时间后会进行重传。这个时间一般是2*RTT(报文段往返时间)+一个偏差值。
2、窗口控制
TCP会利用窗口控制来提高传输速度,意思是在一个窗口大小内,不用一定要等到应答才能发送下一段数据,窗口大小就是无需等待确认而可以继续发送数据的最大值。如果不使用窗口控制,每一个没收到确认应答的数据都要重发。
3 高速重发控制/快速重传(重复确认应答)
快速重传:在遇到3次重复确认应答(高速重发控制)时,代表收到了3个报文段,但是这之前的1个段丢失了,便对它进行立即重传。
4、拥塞控制
慢启动:定义拥塞窗口,一开始将该窗口大小设为1,之后每次收到确认应答(经过一个rtt),将拥塞窗口大小*2。
拥塞避免:设置慢启动阈值,一般开始都设为65536。拥塞避免是指当拥塞窗口大小达到这个阈值,拥塞窗口的值不再指数上升,而是加法增加(每次确认应答/每个rtt,拥塞窗口大小+1),以此来避免拥塞。
将报文段的超时重传看做拥塞,则一旦发生超时重传,我们需要先将阈值设为当前窗口大小的一半,并且将窗口大小设为初值1,然后重新进入慢启动过程。
粘包问题
发送方发送的若干包数据到接收方时粘成一包,从接受缓冲区来看就是后一包数据的头紧接着前一包数据的尾。
粘包出现的原因
1发送方等待到缓冲区满才将包发送出去
2接收方不及时接受缓冲区的包,造成多个包接收
1定长数据流:服务器客户端提前协商,每个消息定长,不足的空白字符补足
2特殊结束符:双方协商定义一个特殊的分隔符号 比如@ # $_$等 只要没有发送分隔符就意味着一条数据没有结束
3协议相对最成熟额数据传递方式,由服务器开发者提供固定格式的协议标准,双方都按照协议进行发送接收数据的。
GET和POST
GET - 从指定的资源请求数据。
POST - 向指定的资源提交要被处理的数据
差别:1.GET参数通过URL传递,POST放在Request body中。
2.GET产生的URL地址可以被收藏书签,而POST不可以
3.GET请求在URL中传送的参数是有长度限制的,而POST没有
3.GET请求会被浏览器主动缓存,而POST不会
4.GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留
5.GET在浏览器后退时是无害的,而POST会再次提交请求
udp的connect
udp的connect主要起到设置目标IP/端口的作用,这样做的结果,并不需要每次都使用设定对方地址的sendto或者recvfrom系统调用,而只需要使用send或recv系统调用。
1 提高发送效率
2 :在高并发服务中可以增加系统稳定性
http无状态无连接,cookie和session
session和cookie的目的相同,都是为了克服http协议无状态的缺陷,但完成的方法不同。session通过cookie,在客户端保存session id,而将用户的其他会话消息保存在服务端的session对象中,与此相对的,cookie需要将所有信息都保存在客户端。因此cookie存在着一定的安全隐患
Cookie:服务端可以在响应头中添加 Set-Cookie 字段,将cookie值发送给客户端,浏览器在收到这个响应时,会自动将cookie保存起来,下次再发送请求时,会将这个cookie附带在请求头的Cookie字段中发给服务器.
Session:客户端第一次请求session对象时,服务器会创建一个session,并通过特殊算法算出一个session的ID,用来标识该session对象,然后将这个session序列放置到set-cookie中发送给浏览器,当浏览器再去访问服务器时,就会携带着session的id,服务器发现浏览器带session的id过来,就会使用之对应的session为之服务。
DNS协议
将域名和IP地址相互映射的一个分布式数据库,能够使人更方便地访问互联网
ARP
地址解析协议ARP(Address Resolution Protocol),负责完成逻辑地址向物理地址的动态映射,将32位逻辑地址(IP地址)转换为48位的物理地址(MAC地址)
ARP是通过ARP缓存来执行这种转换的。当在ARP缓存中没有找到地址时,则向网络发送一个广播请求,网络上所有的主机和路由器都接收和处理这个ARP请求,但是只有相同IP地址的接收到广播请求的主机或路由器,发回一个ARP应答分组,应答中包含它的IP地址和物理地址,并保存在请求主机的ARP缓存中。其他主机或路由器都丢弃此分组
从输入URL到页面展示的详细过程
1、输入网址
2、DNS解析
3、建立tcp连接
4、客户端发送HTTP请求
5、服务器处理请求
6、服务器响应请求
7、浏览器展示HTML
8、浏览器发送请求获取嵌入在 HTML 中的资源
对称加密和非对称加密:
1 对称加密过程和解密过程使用的同一个密钥,但非对称加密采用了两个密钥,一般使用公钥进行加密,使用私钥进行解密。
2 对称加密解密的速度比较快,适合数据比较长时的使用。非对称加密和解密花费的时间长、速度相对较慢,只适合对少量数据的使用
3 对称加密的通信双方使用相同的秘钥,如果一方的秘钥遭泄露,那么整个通信就会被破解。而非对称加密使用一对秘钥,一个用来加密,一个用来解密,而且公钥是公开的,秘钥是自己保存的,不需要像对称加密那样在通信之前要先同步秘钥。非对称加密安全性更好
以太网帧最小长度为64字节
TCP:
以太网帧首部 14byte
IP头 20byte
TCP头 20byte
填充 (6byte)
以太网帧尾部 4byte
一共58byte 不够64byte需要填充6byte
UDP:
以太网帧首部 14byte
IP头 20byte
UDP头 8byte
填充 (18byte)
以太网帧尾部 4byte
一共46byte 不够64byte需要填充18byte
子网掩码
是一种用来指明一个IP地址的哪些位标识的是主机所在的子网,以及哪些位标识的是主机的位掩码。子网掩码不能单独存在,它必须结合IP地址一起使用。子网掩码,将某个IP地址划分成网络地址和主机地址两部分。
IP地址分为五类,A类保留给政府机构,B类分配给中等规模的公司,C类分配给任何需要的人,D类用于组播:224-239,E类用于实验:240-255,各类可容纳的地址数目不同。
A、B、C三类IP地址的特征:当将IP地址写成二进制形式时,A类地址的第一位总是O,B类地址的前两位总是10,C类地址的前三位总是110
网关和路由
1网关:一个大概念,不具体特指一类产品,只要连接两个不同的网络的设备都可以叫网关,所以网关它可以是路由器,交换机或者是PC任一;
2路由器:一般特指能够实现路由寻找和转发的特定类产品,路由器很显然能够实现网关的功能,而路由器的主要功能是把网络分给更多的设备使用网络。
补充:
中继器、集线器:-第1层(物理层)
网桥、交换机:第2层(数据链路层)
路由器:第3层(网络层)
Http报文的格式
一个HTTP请求报文由请求行(request line)、请求头部(header)、空行和请求数据4个部分组成
1.请求行由请求方法字段、URL字段和HTTP协议版本字段3个字段组成,它们用空格分隔。
2.请求头部由关键字/值对组成,每行一对,关键字和值用英文冒号“:”分隔。请求头部通知服务器有关于客户端请求的信息。
3.最后一个请求头之后是一个空行,发送回车符和换行符,通知服务器以下不再有请求头。
4.请求数据不在GET方法中使用,而是在POST方法中使用。
HTTP响应也由三个部分组成,分别是:状态行、消息报头、响应正文
红黑树的左旋和右旋
左旋:假设待左旋的结构中,P为父节点,S为右孩子。左旋操作后,S代替P节点的位置,P节点成为S节点的左孩子,S节点的左孩子成为P节点的右孩子。
右旋:假设待右旋的结构中,P为父节点,S为左孩子。右旋操作后,S代替P节点的位置,P节点成为S节点的右孩子,S节点的右孩子成为P节点的左孩子。
b树和b+树
B树就是通过降低树的深度,将二叉树的“瘦高”变成“矮胖”:
1.每个节点存储多个元素。
2.采用多叉树
B+Tree是在B树基础上的一种优化,B+Tree中,所有数据记录节点都是按照键值大小顺序存放在同一层的叶子节点上,叶子节点由一条链相连,而非叶子节点上只存储key值信息,这样可以大大加大每个节点存储的key值数量,降低B+Tree的高度,所以相同的内存b+树就可以存放更多的索引数据了
Gdb生成调试信息
一般来说GDB主要调试的是C/C++的程序。要调试C/C++的程序,首先在编译时,我们必须要把调试信息加到可执行文件中。使用编译器(cc/gcc/g++)的 -g 参数可以做到这一点。如:
gcc -g hello.c -o hello
g++ -g hello.cpp -o hello
启动GDB 的方法
gdb program
program 也就是你的执行文件,一般在当前目录下。
程序运行 run 启动程序
5 设置断点
5.1 简单断点
break 设置断点,可以简写为b
b 10 设置断点,在源程序第10行
b func 设置断点,在func函数入口处
查询所有断点
info b
条件断点
一般来说,为断点设置一个条件,我们使用if关键词,后面跟其断点条件。
设置一个条件断点
b test.c:8 if intValue = 5
删除停止点
delete [range...] 删除指定的断点,如果不指定断点号,则表示删除所有的断点。range 表示断点号的范围(如:3-7)。其简写命令为d。
调试代码
start 只执行一步
run 运行程序,可简写为r
next 单步跟踪,函数调用当作一条简单语句执行,可简写为n
step 单步跟踪,函数调进入被调用函数体内,可简写为s
finish 退出进入的函数
until 在一个循环体内单步跟踪时,这个命令可以运行程序直到退出循环体,可简写为u。
continue 继续运行程序,可简写为c,直接停在断点的位置。
查看运行时数据
print 打印变量、字符串、表达式等的值,可简写为p
p count 打印count的值
ptype width -- 查看变量width的类型
自动显示 变量会自动显示。相关的GDB命令是display。
display 变量名
查看代码
l --list
l 10(行号)或者函数名
l filename:行号或者函数名
Linux命令:
curl ifconfig.me 查看本机公网IP
网络相关命令:
ping
netstat
telnet(远程登录协议,为用户提供了在本地计算机上完成远程主机工作的能力,可以查看某个端口是否可访问)
df和du区别
du:用于查找文件和目录的磁盘使用情况的命令
df:用于显示 Linux 系统的磁盘利用率
读取其他进程内存数据
读取其他进程内存数据,需要用到的windows API函数:
BOOL ReadProcessMemory(
HANDLE hProcess,
LPCVOID lpBaseAddress,
LPVOID lpBuffer,
DWORD nSize,
LPDWORD lpNumberOfBytesRead
);
ReadProcessMemory是一个内存操作函数, 其作用为根据进程句柄读入该进程的某个内存空间, 当函数读取成功时返回1, 失败则返回0。
fopen:文本方式和二进制方式打开文件的区别
1、使用二进制方式进行读文件时,会原封不动的读出全部的内容,写文件的时候,会把内存缓冲区的内容原封不动的写到文件中。
2、使用文本方式进行读文件时,会将回车换行符号CRLF(0x0D OxOA)全部转换成单个的换行符号LF(OxOA),写文件的时候,会将换行符号LF( OxOA)全部转换成回车换行符号CRLF(0x0D OxOA)。
构造函数和析构函数能不能被显示调用
- C++中, 构造函数和析构函数可以被显示调用. 显示调用默认构造函数的语法: a.A::A();(不能写成a.A();) , 显示调用非默认构造函数的语法: a.A::A(7);(不能写成a.A(7);); 显示调用析构函数的语法: a.A::~A();(可以写成a.~A();)
- 显示调用构造函数和析构函数就像调用一般的函数一样, 并不意味着创建或销毁对象;
- 如果构造函数中动态分配了空间, 则显示调用构造函数会造成内存泄露. 创建对象时的隐式构造函数调用已经为对象分配了动态内存,当用创建好的对象显示调用构造函数时, 对象指向的动态内存更新为显示调用时所分配的, 对象生命周期结束时析构函数所释放掉的是后一次分配的动态内存, 也就是说创建对象时隐式构造函数调用所分配的那块内存泄漏了
- 如果析构函数中释放动态分配的空间, 则会造成多次释放同一内存, 会出现严重错误.
补充:什么情况下我们需要手动调用类的析构函数?如果在创建对象的时候采用动态申请(new),那么需要显式的调用类的析构函数
深拷贝浅拷贝
浅拷贝只是对指针的拷贝,拷贝后两个指针指向同一个内存空间,深拷贝不但对指针进行拷贝,而且对指针指向的内容进行拷贝,经深拷贝后的指针是指向两个不同地址的指针
纯虚函数
1作用:
定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。
应该在什么情况下使用纯虚函数?
1、当想在基类中抽象出一个方法,且该基类只做能被继承,而不能被实例化;
2、这个方法必须在派生类(derived class)中被实现。
2实现方式:
纯虚函数的声明,是在虚函数声明的结尾加 = 0
,没有函数体。在派生类没有重新定义虚函数之前是不能调用的。
class CShape
{
public:
virtual void Show() = 0;
};
lib和dll比较
1 静态链接库的优点
(1) 代码装载速度快,执行速度略比动态链接库快;
(2) 只需保证在开发者的计算机中有正确的.LIB文件,在以二进制形式发布程序时不需考虑在用户的计算机上.LIB文件
2 动态链接库的优点
(1) 更加节省内存并减少页面交换;
(2) DLL文件与EXE文件独立,只要输出接口不变,更换DLL文件不会对EXE文件造成任何影响;
(3) 不同编程语言编写的程序只要按照函数调用约定就可以调用同一个DLL函数;
(4)适用于大规模的软件开发。
3 不足之处
(1) 静态链接生成的可执行文件体积较大,包含相同的公共代码,造成浪费;
(2) 使用动态链接库的应用程序不是自完备的,它依赖的DLL模块也要存在。
页面置换算法
地址映射过程中,若在页面中发现所要访问的页面不在内存中,则产生缺页中断。当发生缺页中断时,如果操作系统内存中没有空闲页面,则操作系统必须在内存选择一个页面将其移出内存,以便为即将调入的页面让出空间。
而用来选择淘汰哪一页的规则叫做页面置换算法。
1 最佳置换算法(OPT)
2 先进先出页面置换算法(FIFO)
3最近最久未使用置换算法(LRU)
Linux进程调度
linux内核将进程分成两个级别:普通进程和实时进程。实时进程的优先级都高于普通进程,除此之外,它们的调度策略也有所不同
1、SCHED_FIFO:先进先出。直到先被执行的进程变为非可执行状态,后来的进程才被调度执行。(在这种策略下,先来的进程可以执行sched_yield系统调用,自愿放弃CPU,以让权给后来的进程);
2、SCHED_RR:轮转调度。内核为实时进程分配时间片,在时间片用完时,让下一个进程使用CPU;
sizeof注意事项
{int i=5;
int j = sizeof(++i+i++);
printf("%d,%d\n",i,j); } //输出 5,4
sizeof是运算符,它在编译时起作用,而不是运行时。
也就是说,在编译时,就得到了j==4,运行时,++i+i++根本没有执行过。
Gcc常用编译
https://www.runoob.com/w3cnote/gcc-parameter-detail.html
比如:
-g:指示编译器在编译的时候,产生调试信息
-Wall:会打开一些很有用的警告选项,建议编译时加此选项
-llibrary:指定库
-Idir:指定头文件路径
-o:定制目标名称
MMU
内存管理单元:
1 负责虚拟内存和物理内存的映射
2 设置修改内存访问级别
strcpy memcpy
|
char
*
strcpy
(
char
*strDest,
const
char
*strSrc )
{
assert
( (strDest != NULL) && (strSrc != NULL) );
char
*address = strDest;
while
( (*strDest++ = * strSrc++) != ‘\0’ );
return
address;
}
二叉树的先序遍历
https://www.cnblogs.com/SunZhR/p/6594168.html
单例模式
//懒汉式
class Singleton_lazy {
private:
Singleton_lazy() { //1.构造函数
cout << "懒汉式创建" << endl;
}
private:
static Singleton_lazy *pSingleton; //2.静态对象
public:
static Singleton_lazy* getInstance() { //3.提供静态对外接口
if (pSingleton == NULL) {
pSingleton = new Singleton_lazy;
}
return pSingleton;
}
};
Singleton_lazy * Singleton_lazy::pSingleton = NULL; //外部初始化
//饿汉式
class Singleton_hungry {
private:
Singleton_hungry() {
cout << "我是饿汉构造!" << endl;
}
private:
static Singleton_hungry*pSingleton;//指向对象的指针
public:
static Singleton_hungry*getInstance() {
return pSingleton;
}
};
Singleton_hungry* Singleton_hungry::pSingleton = new Singleton_hungry;
int main()
{
cout << "main函数" << endl;
Singleton_lazy *p1 = Singleton_lazy::getInstance();
Singleton_lazy *p2 = Singleton_lazy::getInstance();
if (p1 == p2) {
cout << "两个指针指向同一块内存空间,是单例!" << endl;
}
else {
cout << "不是单例模式!" << endl;
}
return 0;
}