全称是 Resource Acquisition Is Initialization , 即“资源获取即初始化”,其核心是把资源和对象的生命周期绑定:对象创建获取资源,对象销毁释放资源。这就是的资源也有了生命周期,有了自动回收的功能。lock_guard 都利用了 RAII机制来实现。
防止内存泄露的方式有 RAII、智能指针。
大端就是高字节在高地址,低字节在低地址。
小端就是低字节在高地址,高字节在低地址。
// 大端小端区分
bool isBigEndian() {
union NUM {
int a;
char b; // 如果是大端 b 就是最高位 ,小端就是最低位
}num;
num.a = 0x1234;
if(num.b == 0x12)
{
return true;
}
return false;
}
大端小端转换
//无符号整型16位
uint16_t bswap_16(uint16_t x) {
return ((x & 0x00ff) << 8) | (x & 0xff00) >> 8) ;
}
//无符号整型32位
uint32_t bswap_32(uint32_t x) {
return ((x & 0xff000000) >> 24)| ((x & 0x00ff0000) >> 8) | \
((x & 0x0000ff00) << 8) | ((x & 0x000000ff) << 24) ;
}
main.cc
翻译成一个ASCII码的中间件文件 main.i
main.i
文件 翻译成一个 ASCII 汇编文件 main.s
.obj、.o、.a、.ko
) :运行汇编器,将 main.s
翻译成一个可重定位目标文件main.o
.exe、.elf、.axf
等) : 运行链接器,将main.o
中使用到的目标文件组合起来,创建一个可执行的文件根本区别:是在编译期还是在是执行期完成链接、装入动作。链接的主要内容就是把各个模块之间相互引用的部分都处理好,
使得各个模块之间能够正确地衔接
静态库:之所以叫做静态库,是因为在链接阶段,当使用链接器将由汇编生成的目标文件构造成一个可执行的输出文件时,它只是复制静态库中里这个即将生成的可执行文件所引用到的目标模块。静态库特点总结:
静态库对函数库的链接是放在编译时期完成的。
程序在运行时与函数库再无瓜葛,移植方便。
浪费空间和资源,因为所有相关的目标文件与牵涉到的函数库被链接合成一个可执行文件。
动态库:动态库在程序编译时并不会被连接到目标代码中,而是在程序运行是才被载入。不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例,规避了空间浪费问题。动态库在程序运行是才被载入,也解决了静态库对程序的更新、部署和发布页会带来麻烦。用户只需要更新动态库即可,增量更新
动态库把对一些库函数的链接载入推迟到程序运行的时期。
可以实现进程之间的资源共享。(因此动态库也称为共享库)
将一些程序升级变得简单。
甚至可以真正做到链接载入完全由程序员在程序代码中控制(显示调用)。
static全局变量与普通的全局变量有什么区别:static全局变量只初使化一次,防止在其他文件单元中被引用;
static局部变量和普通局部变量有什么区别:static局部变量只被初始化一次,下一次依据上一次结果值;
程序的局部变量存在于(堆栈)中,全局变量存在于(静态区 )中,动态申请数据存在于( 堆)中。
extern全局变量(用extern修饰的变量只是说明该变量在其他地方定义,所以在其他地方一定要用明确的定义如int a,并且不能用static修饰)、static全局变量和static局部变量的生存期都是“永久”,区别只是可见域不同。extern全局变量可见区域是工程,static全局变量可见区域是文件,而static局部变量的可见区域是块。
阻止编译器为了提高速度将一个变量缓存到寄存器内而不写回。
阻止编译器调整操作volatile
变量的指令顺序。
注意:即使 volatile
能够阻止编译器调整顺序, 也无法阻止CPU动态调度换序(reorder
)
断言主要用于检查逻辑上不可能的情况。例如,它们可用于检查代码在开始运行之前所期望的状态,或者在运行完成后检查状态。与正常的错误处理不同,断言通常在运行时被禁用。
assert 是个宏而非函数,如果条件返回错误,则会抛出异常,最后会调用 abort 终止程序,发送的是 SIGABRT,可以通过宏 NODEBUG 来关闭 assert,但是需要设置在源代码的开头。
#define assert(expr) \
(static_cast (expr) \
? void (0) \
: __assert_fail (#expr, __FILE__, __LINE__, __ASSERT_FUNCTION))
extern
void __assert_fail (const char *__assertion,
const char *__file,
unsigned int __line,
const char *__function)
{
__THROW __attribute__ ((__noreturn__));
}
在判断失败时会调用 __assert_fail 来抛出异常,在C++中异常的底层是用 abort 实现的。
new:不仅仅是分配内存,还包括了对象类型转换以及初始化
malloc
返回 void*
类型,并且在内存不足时,返回NULL
指针
当开辟的空间小于 128K
时,调用brk()
函数,malloc
的底层实现是系统调用函数 brk()
当开辟的空间大于 128K
时,mmap()
系统调用函数来在虚拟地址空间中(堆和栈中间,称为“文件映射区域”的地方)找一块空间来开辟
operator new
底层也是由malloc
实现。 malloc
底层是由slab
实现。
对于POD
类型对象,使用new
创建的对象,也是可以使用free
来释放销毁对象,原因就是在于这个对象是POD
类型。没有其他资源需要释放,或者文件描述符需要关闭,因此可以不调用析构函数而使用free
来替代delete
。尽管可以,但是不推荐,这样的代码并不健壮。
new
在C++里是可以用 malloc + placement
替代的,或者说等效于这两个。
// 更好的展示 malloc 与 new 的区别与联系
class Foo {
public:
Foo(int a=0, int b=0): a(a),b(b) { }
~Foo() { std::cout<<"dtor"<<std::endl; }
private:
int a;
int b;
};
int main(int argc, char const *argv[])
{
// new = placement new + malloc
Foo* foo = static_cast<Foo*>(::malloc(sizeof(Foo)));
new(foo)Foo(1,1);
// delete
foo->~Foo();
::free(foo);
return 0;
}
// 运行结束,无内存泄露
除了new
和malloc
,还有什么方式可以在堆中分配内存么,以及怎么释放?
mmap
,munmap
多态的三个条件:继承,重写(override
),基类引用指向派生类对象。
静态多态
重载,在编译时期就可以确定
模板技术:比如 CRTP
,它是使用子类来作为基类的模板参数,在基类中调用子类的方法。
template<typename Derived>
class Base{
// 其他忽略
Derived& convert()
{
return static_cast<Derived&>(*this);
}
};
这个也从侧面说明static_cast
可以让子类转换为父类,要使得保证安全,前提是转换后调用子类的方法中没有使用子类独有的数据成员
动态多态
运行时才确定调用的是哪个函数。核心关键在于虚函数:子类重写基类的虚方法,定义指向子类对象的基类指针。这个基类指针的行为直到运行时才能确定调用的是子类还是基类的方法,这就是多态。
实现原理是:虚函数表和虚函数指针vptr
。详情
overload
)和重写(override
)重载:允许多个同名函数,而这些函数的参数列表不同,函数模板这么实现的,在编译期间就能确定。
C++函数重载底层实现原理是C++利用 name mangling
技术,来改变函数名,区分参数不同的同名函数。编译器通过函数名和其参数类型识别重载函数。不能仅仅基于不同的返回类型而实现函数重载,是因为经过 name mangling
后得到的函数名与返回值类型是无关的。
void func(int a) { }
int func(int a, int b) { return a+b; }
int main(int argc, char const *argv[])
{
return 0;
}
比如,如上的代码。在经过编译后,得到的符号表如下:
$ objdump -t main.o
0000000000000000 g F .text 000000000000000e _Z4funci
000000000000000e g F .text 0000000000000018 _Z4funcii
0000000000000026 g F .text 0000000000000016 main
其中, 前缀 __z
是规定,4
是函数名的字符个数,i
是第一个函数的参数类型int
,ii
是第二个函数的参数类型int, int
。由此可见也是与返回值无关的。
重写override
:是指子类重新定义父类的方法,子类的函数名和参数列表以及返回值必须与父类的完全一致。对于虚函数的重写,c++11中新定义了一个关键词override
,就是为了让子类在重写父类的虚函数方法时,如何参数列表发生更改可以让编译器报错。
static
static
变量都是在全局数据区分配内存,声明周期直到程序运行结束.
四类static
static
变量和static
函数:都已经被匿名命名空间取代,作用是不能外部文件使用static
变量:在数据区.data
分配内存,首次初始化以后,以后调用都不会再初始化,作用域仅局限于函数,生命周期直到程序运行结束.data
为什么要引入static
需要一个数据对象为整个类而非某个对象服务,同时又不能破坏类的封装特性,因此将静态成员隐藏在类的内部,提供静态成员函数接口,因为共享,可以节省内存。
空类有六个成员函数
class Empty {
public:
Empty();
Empty(const Empty& );
~Empty();
Empty& operator=(consy Empty& );
Empty* operator& (); // 取地址运算符
const Empty* operator& () const; // const 类型取地址运算符
};
构造函数可以是虚函数吗?析构函数可以是虚函数吗?
虚函数对应一个vtbl
,这个vtbl
实际是存储在对象的内存空间.如果构造函数是虚的,对象的构造就需要vtbl
来调用,而虚函数表又是在对象构造后才能建立,因此构造函数不能是虚函数.
而析构函数在使用多态的继承中一般都是虚析构函数.为的是能正确选择析构函数.
c++
的深拷贝如何理解
在类中有指针时并且内部分配资源.经过浅拷贝后,最终会造成资源一次分配,多次释放.造成系统崩溃.
main函数执行之前,主要就是初始化系统相关资源:
static
变量和global
全局变量,即.data
段的内容short
,int
,long
等为0
,bool
为FALSE
,指针为NULL
等等,即.bss
段的内容main
之前调用构造函数argc
,argv
等传递给main
函数,然后才真正运行main
函数main函数执行之后:
atexit
注册一个函数,它会在main 之后执行; int main(int argc, char const *argv[]){
const char* str = "name";
sizeof(str); // 取的是指针str的长度,是8
strlen(str); // 取的是这个字符串的长度,不包含结尾的 \0。大小是4
return 0;
}
char* strcpy(char* dest, const char* src);
char* strncpy(char* dest, const char* src, size_t n);
void* memcpy (void* dest, const void* src, size_t n);
前面两个函数是以字符为单位,而mmemcpy
是以字节为单位。
strcpy
和memcpy
主要有以下3方面的区别。
strcpy
只能复制字符串,而memcpy
可以复制任意内容,例如字符数组、整型、结构体、类等。strcpy
不需要指定长度,它遇到被复制字符的串结束符'\0'
才结束,所以容易溢出。memcpy
则是根据其第3个参数决定复制的长度,而且如果字符串数据中包含'\0'
,只能用memcpy
。strncpy
:在复制字符串时,memcpy
更加类似于strncpy
。
strncpy
和memcpy
很相似,只不过它在一个终止的空字符处停止。当 n > strlen(src)
时,dst[strlen(len)] ~ dst[n-1]
都是\0
;当 n<=strlen(src)
时,复制前src
的n个字符。这里隐藏了一个事实,就是dst
指向的内存一定会被写n个字符。
memcpy
需要注意的是:
dest
指针要分配足够的空间,也即大于等于 n
字节的空间。如果没有分配空间,会出现断错误。dest
和 src
所指的内存空间不能重叠(如果发生了重叠,使用 memmove()
会更加安全)。动手实现 memcpy
:
void* myMemcpy(void* dst, const void* src, size_t n) {
if(dst ==nullptr || src==nullptr) return nullptr;
if(src == dst) retrun src;
char* pdst = static_cast<char*>(dst);
const char* psrc = static_cast<const char*>(src);
// 发生重叠时,从后向前复制
if(psrc < pdst && pdst < psrc + n)
{
for(int i=n-1; i >=0; --i) pdst[i] = psrc[i];
}
else
{
for(int i=0; i < n; ++i) pdst[i] = psrc[i];
}
return pdst;
}
// 使用
int main(int argc, char const *argv[])
{
char buf[12]={0};
char str[] = "hello world cpp";
myMemcpy(str, str+6, 9);
std::cout<<str<<std::endl;
return 0;
}
都是是指向无效内存区域(这里的无效指的是"不安全不可控")的指针,访问行为将会导致未定义行为。
野指针
野指针,指的是没有被初始化过的指针
int main(void) {
int* p; // 未初始化
std::cout<< *p << std::endl; // 未初始化就被使用
return 0;
}
因此,为了防止出错,对于指针初始化时都是赋值为 nullptr
,这样在使用时编译器就会直接报错,产生非法内存访问。
悬空指针
悬空指针,指针最初指向的内存已经被释放了的一种指针。
int main(void) {
int * p = nullptr;
int* p2 = new int;
p = p2;
delete p2;
}
此时 p和p2就是悬空指针,指向的内存已经被释放。继续使用这两个指针,行为不可预料。需要设置为p=p2=nullptr
。此时再使用,编译器会直接保错。
避免野指针比较简单,但悬空指针比较麻烦。c++引入了智能指针,C++智能指针的本质就是避免悬空指针的产生。
#include
void *malloc(size_t size);
void *calloc(size_t nmemb, size_t size);
void *realloc(void *ptr, size_t size);
void free(void *ptr);
malloc
最常用的分配内存函数,分配size
个字节。分配的内存是未初始化的,如果size==0
,要么返回NULL
,要么返回一个独一无二的指针,能被free
释放。
realloc
:用来改变已有内存区的大小,而不改变内容。新的大小为参数size
,即newsize。
ptr==NULL
:realloc(NULL,size)
就相当于malloc(size)
size==0
:realloc(ptr, 0)
就相当于free(ptr)
ptr==NULL && size==0
:危险。newsize > oldsize
,那么增加的内存是未初始化的,原来的内存内容保持不变,即[ptr, ptr+oldsize)
内部不变,[ptr+oldsize, ptr+newsize)
是初始化的内容。如果newsize < oldsize
,尾部的内容就被切去,释放,只是剩下前面。
因此 realloc
之后不能给这个内存区初始化.
calloc
分配nmemb
个元素,每个元素大小是size
个字节的连续内存。
nmemb*size
的连续内存区0
。如果size==0
,要么返回NULL
,要么返回一个独一无二的指针,能被free
释放。
为了减小编译依赖加快编译速度和生成二进制文件的大小,C/C++ 项目中一般在 *.h 文件对于指针类型(包括智能指针) 尽量使用前置声明,而不是直接包含对应类的头文件。例如:
//Test.h
//在这里使用A的前置声明,而不是直接包含A.h文件
class A;
class Test
{
public:
Test();
~Test();
private:
A*
};
g++ 编译器是默认开启 copy elison
选项的。如果要关闭这个选项,使用 -fno-elide-constructors
。copy elision
主要包括以下两项内容:
1. 返回值优化
即通过将返回对象所占空间的直接构造到他们本来要复制/移动到的对象中去,依次来避免拷贝/移动操作。返回值优化包括具名返回值优化 NRVO
与 匿名返回值优化 URVO
,区别在于返回值是具名的局部变量(NRVO
)还是无名的临时对象(URVO
)
URVO
与 彻底 Copy elision
class Copyable {
public:
Copyable() { std::cout<<"default ctor"<<std::endl; }
Copyable(const Copyable& rhs) = delete;
Copyable(Copyable&& rhs) = delete;
};
Copyable return_urvo_value() {
return Copyable{}; // since c++17 ok
}
int main(int argc, char const *argv[]) {
auto x = return_urvo_value();
return 0;
}
上述代码在C++17中是可以编译通过的。在 C++17 之前,并没有明确的提出在什么情况下,可以彻底进行 Copy Elision
(这里的彻底的指的是包括不进行检查是否有可用的 copy/move 构造函数)。在C++17中,对于匿名对象(or 临时对象)不论是传递参数,还是以返回值返回时,都不会调用拷贝/移动构造。因此上面的这段代码在C++17是可以正常编过的,而在C++14会编译出错。
$ g++ main.cc -std=c++14 -o main && ./main
main.cc: In function ‘Copyable return_urvo_value()’:
main.cc:29:19: error: use of deleted function ‘Copyable::Copyable(Copyable&&)’
29 | return Copyable{};
| ^
main.cc:24:3: note: declared here
24 | Copyable(Copyable&& rhs) = delete;
| ^~~~~~~~
main.cc: In function ‘int main(int, const char**)’:
main.cc:34:31: error: use of deleted function ‘Copyable::Copyable(Copyable&&)’
34 | auto x = return_urvo_value();
| ^
main.cc:24:3: note: declared here
24 | Copyable(Copyable&& rhs) = delete;
| ^~~~~~~~
自然,只要将上面代码中的如下两行注释掉,即可正常编译,并且 Copyable
的构造函数都是只被调用一次,即copy elision
起作用了。 注意:Copyable
的复制/移动构造函数必须同时可访问。
Copyable(const Copyable& rhs) = delete;
Copyable(Copyable&& rhs) = delete;
因此,在C++17以前,对于 urvo
不在乎是否返回的对象的复制/移动构造函数是否存在或者可访问,copy elision
都能生效。而在 C++14
之前,返回的对象可以没有复制/移动构造函数,但是必须可以访问。
nrvo
在 nrvo
时,返回对象的复制/移动构造函数必须可访问。否则编译不过。
class Copyable {
public:
Copyable() { std::cout<<"default ctor"<<std::endl; }
Copyable(const Copyable& rhs) = delete;
Copyable(Copyable&& rhs) = delete;
};
Copyable return_urvo_value() {
return Copyable{};
}
Copyable return_nrvo_value() {
Copyable local;
return local;
}
int main(int argc, char const *argv[]) {
auto x = return_urvo_value();
auto y = return_nrvo_value();
return 0;
}
如上代码,即使是C++17也会编译失败,必须将如下两行代码注释掉,使得 Copyable
对象的复制/移动构造函数可访问。copy elision
才能生效:Copyable
的默认构造函数只调用一次。
// Copyable(const Copyable& rhs) = delete;
// Copyable(Copyable&& rhs) = delete;
2. 右值拷贝优化
右值拷贝优化,当某一个类的临时对象以值传递给该类的另一个对象时,也可以直接利用该临时对象的来避免拷贝/移动操作。在上面的基础上,加上如下的代码:
void pass_by_value(Copyable rhs) {
}
int main(int argc, char const *argv[]) {
auto x = return_urvo_value();
auto y = return_nrvo_value();
pass_by_value(Copyable());
return 0;
}
最终的输出也是调用默认三次构造函数:
$ g++ main.cc -std=c++11 -o main && ./main
default ctor
default ctor
default ctor
到此,copy elision
基本分析结束。如果想查看没有copy elision
作用下的输出,开启-fno-elide-constructors
。
对于一些没有拷贝/移动构造的对象,如 unique_ptr
、 atomic
等。现在我们能够定义一个工厂函数,即使没有复制或移动构造函数都可以返回一个对象。例如,以下通用工厂函数:
template <typename T, typename... Args>
T make_instance(Args&& ... args)
{
return T{ std::forward<Args>(args)... };
}
int main()
{
int i = make_instance<int>(42);
// std::unique_ptr 实现了 移动构造函数,因此可以编译成功
auto up = make_instance<std::unique_ptr<int>>(new int{ 42 });
// 禁止了复制构造函数,但是也没有实现移动构造函数,因此要到 C++17 才能编译过
auto ai = make_instance<std::atomic<int>>(42);
return 0;
}
参考
std::vector 与 std::list
对于如下代码,使用 std::bind 绑定类的成员函数并调用。
class Foo {
public:
Foo()=default;
void add(const int& lhs, const int& rhs)
{
std::cout<< (lhs + rhs)<<std::endl;;
}
};
int main(int argc, char const *argv[])
{
Foo foo1;
// 绑定并且执行
std::bind(&Foo::add, &foo1, 1, 2)();
return 0;
}
最后内部执行的代码如下:
template<typename _Res, typename _MemFun, typename _Tp, typename... _Args>
constexpr _Res
__invoke_impl(__invoke_memfun_deref, _MemFun&& __f, _Tp&& __t, _Args&&... __args)
{
return ((*std::forward<_Tp>(__t)).*__f)(std::forward<_Args>(__args)...);
}
其中
_t
,类型是 Foo*&
函数对象指针,指向的foo1
对象。_f
是函数指针,指向的就是add
成员函数__invoke_memfun_deref
:是用来标记是哪种把绑定方式,比如上述代码中的绑定对象成员函数因此,最终,调用可以简化为如下:
foo1.add(1,2);
整个std::bind
最终的执行代码如下:
// 直接传入函数调用: std::bind(func, arg1, arg2); 或者静态成员函数
// 很明显,这里没有类对象
template<typename _Res, typename _Fn, typename... _Args>
constexpr _Res
__invoke_impl(__invoke_other, _Fn&& __f, _Args&&... __args)
{ return std::forward<_Fn>(__f)(std::forward<_Args>(__args)...); }
// 上面介绍过
template<typename _Res, typename _MemFun, typename _Tp, typename... _Args>
constexpr _Res
__invoke_impl(__invoke_memfun_deref, _MemFun&& __f, _Tp&& __t, _Args&&... __args)
{
return ((*std::forward<_Tp>(__t)).*__f)(std::forward<_Args>(__args)...);
}
// 下面几种没见过调用
template<typename _Res, typename _MemFun, typename _Tp, typename... _Args>
constexpr _Res
__invoke_impl(__invoke_memfun_ref, _MemFun&& __f, _Tp&& __t, _Args&&... __args)
{ return (__invfwd<_Tp>(__t).*__f)(std::forward<_Args>(__args)...); }
template<typename _Res, typename _MemPtr, typename _Tp>
constexpr _Res
__invoke_impl(__invoke_memobj_ref, _MemPtr&& __f, _Tp&& __t)
{ return __invfwd<_Tp>(__t).*__f; }
template<typename _Res, typename _MemPtr, typename _Tp>
constexpr _Res
__invoke_impl(__invoke_memobj_deref, _MemPtr&& __f, _Tp&& __t)
{ return (*std::forward<_Tp>(__t)).*__f; }
Lambda
lambda可以理解为是仿函数的语法糖。
int main(int argc, char const *argv[])
{
auto add = [](int a, int b) { return a+b; };
int c = add(1,2);
return 0;
}
对于上面的lambda
函数,在gdb调试,经过编译的到的函数类型:
(gdb) s
<lambda(int, int)>::operator()(int, int) const (__closure=0x7fffffffe6d3, a=1, b=2) at main.cpp:24
24 auto add = [](int a, int b) { return a+b; };
lambda
可以看作是匿名的函数对象,并且 lambda
表达式默认是 const
属性。
class Foo {
public:
Foo()
{
[](){std::cout<<"123"<<std::endl; }();
}
};
int main(int argc, char const *argv[])
{
Foo foo;
return 0;
}
在类中调用lambda
表达式,编译出来的类型如下:
Foo::Foo()::{lambda()#1}::operator()() const
而实际上,lambda与operator本质上也是一样的,如下代码:
class Foo {
public:
Foo()
{
// 以下两个设计等价
[this](const char* str){this->print(str); }("lambda");
Unmaed(this)("operator");
}
void print(const char* str) {
std::cout<<str<<std::endl;
}
private:
struct Unmaed
{
Unmaed(Foo* foo): foo_(foo) { }
void operator()(const char* str) const
{
foo_->print(str);
}
Foo* foo_;
};
};
lambda
可以重载,但是 std::bind
无法区别重载
void f(int) {}
void f(double) {}
auto g = [] { f(1); }; // OK
auto g = std::bind(f, 1); // 错误
auto g = std::bind(static_cast<void(*)(int)>(f), 1); // OK
为此必须指定对应的函数指针类型。lambda
闭包类的 operator()
采用的是能被编译器内联的常规的函数调用。而std::bind
采用的是一般不会被内联的函数指针调用,这意味着 lambda
比 std::bind
运行得更快。
std::bind
的参数,绑定的是 std::bind
,而不是std::bind
内部管理的函数void f(std::chrono::steady_clock::time_point t, int i)
{
std::this_thread::sleep_until(t);
std::cout << i;
}
auto g = [](int i)
{
f(std::chrono::steady_clock::now() + std::chrono::seconds(3), i);
};
g(1); // 3秒后打印1
// 用std::bind实现相同效果,但存在问题
auto h = std::bind(f,
std::chrono::steady_clock::now() + std::chrono::seconds(3),
std::placeholders::_1);
h(1); // 3秒后打印1,但3秒指的是调用std::bind后的3秒,而非调用f后的3秒
计算时间的表达式作为实参被传递给std::bind
,因此计算发生在调用std::bind
的时刻,而非调用其绑定的函数的时刻。
在 c++14
中,完全没有理由使用 std::bind
,c++11
由于特性受限,存在两个使用场景:
c++11
缺少的移动捕获operator()
是模板时,如果将此函数作为参数使用,用 std::bind
绑定才能接受任意类型参数struct X {
template<typename T>
void operator()(const T&) const;
};
X x;
auto f = std::bind(x, _1); // f可以接受任意参数类型
// c++14 做法
X a;
auto f = [a](const auto& x) { a(x); };
std::bind
传入的参数默认情况下是 “值传递”,想要使用引用传递需要std::ref
。详细可以参考下面的代码:
class Foo {
public:
Foo()
{
std::cout<<"default"<<std::endl;
}
Foo(const Foo& rhs)
{
std::cout<<"ctor"<<std::endl;
}
};
void add(const Foo& lhs, const Foo& rhs) {
}
int main(int argc, char const *argv[])
{
std::cout<<std::boolalpha;
Foo foo1;
Foo foo2;
std::cout<<"bind: pass by value"<<std::endl;
auto func = std::bind(add, foo1, foo2);
std::cout<<"bind: pass by ref"<<std::endl;
auto func = std::bind(add, std::ref(foo1), std::ref(foo2));
std::cout<<"lambda "<<std::endl;
[&foo1, &foo2]{ add(foo1, foo2);}();
return 0;
}
上面的代码输出:
$ g++ -g -O0 main.cc -o main && ./main
default
default
bind: pass by value
ctor
ctor
bind: pass by ref
lambda
可以看到std::bind
在默认情况下,是依靠值传递,使用了std::ref
来包裹传入参数才是使用引用传递。用 gdb
调试,可以跟踪到发生构造 Foo
对象的位置:
template<typename _UHead>
constexpr _Head_base(_UHead&& __h) : _Head(std::forward<_UHead>(__h)) { }
// 整个调用链如下:
#0 Foo::Foo (this=0x7fffffffe068, rhs=...) at main.cc:13
#1 0x0000555555555a8c in std::_Head_base<1ul, Foo, true>::_Head_base<Foo&> (this=0x7fffffffe068, __h=...) at /usr/include/c++/9/tuple:87
#2 0x00005555555559e8 in std::_Tuple_impl<1ul, Foo>::_Tuple_impl<Foo&> (this=0x7fffffffe068, __head=...) at /usr/include/c++/9/tuple:349
#3 0x00005555555558f3 in std::_Tuple_impl<0ul, Foo, Foo>::_Tuple_impl<Foo&, Foo&, void> (this=0x7fffffffe068, __head=...)
at /usr/include/c++/9/tuple:218
#4 0x0000555555555815 in std::tuple<Foo, Foo>::tuple<Foo&, Foo&, true> (this=0x7fffffffe068, __a1=..., __a2=...) at /usr/include/c++/9/tuple:969
#5 0x00005555555556fc in std::_Bind<void (*(Foo, Foo))(Foo const&, Foo const&)>::_Bind<Foo&, Foo&>(void (*&&)(Foo const&, Foo const&), Foo&, Foo&)
(this=0x7fffffffe060, __f=@0x7fffffffe010: 0x5555555551ea <add(Foo const&, Foo const&)>) at /usr/include/c++/9/functional:467
#6 0x0000555555555571 in std::bind<void (&)(Foo const&, Foo const&), Foo&, Foo&> (__f=
@0x5555555551ea: {void (const Foo &, const Foo &)} 0x5555555551ea <add(Foo const&, Foo const&)>) at /usr/include/c++/9/functional:812
#7 0x00005555555552b7 in main (argc=1, argv=0x7fffffffe198) at main.cc:30
左值引用,也就是“常规引用”,不能绑定到要转换的表达式,字面常量,或返回右值的表达式。而右值引用恰好相反,可以绑定到这类表达式,但不能绑定到一个左值上。
右值引用就是必须绑定到右值的引用,通过&&获得。右值引用只能绑定到一个将要销毁的对象上,因此可以自由地移动其资源。
返回左值的表达式包括返回左值引用的函数及赋值,下标,解引用和前置递增/递减运算符;
返回右值的包括返回非引用类型的函数及算术,关系,位和后置递增/递减运算符。可以看到左值的特点是有持久的状态,而右值则是短暂的
void func(int&& index, int idx) {
if(idx > 3) return;
func(index++, ++idx); // Ok
func(++index, ++idx); // Error
std::cout<< std::is_rvalue_reference<decltype(index)>::value <<std::endl;
}
在上面的代码中,++index
产生的是左值,而 index++
产生的是右值。因此上面的可以编译成功,下面的编译不过。
【注意】:已经命名的右值,编译器会认为是左值。
变量都是左值,即使变量是右值引用类型
int&& ref1 = 1; // ok
int&& ref2 = ref1; // error
因为 ref2
是右值引用类型的变量,不能将其绑定到左值ref1
上。ref1
与 ref2
是左值,因为他们都是变量,但是变量类型是右值引用类型,即这两个变量只能绑定到右值上。
基类和子类之间转换:
static_cast
的使用,当且仅当类型之间可隐式转化时,static_cast
的转化才是合法的。有一个例外,那就是类层次间的向下转型,static_cast
可以完成类层次间的向下转型,但是向下转型无法通过隐式转换完成。
向上转换安全:子类指针转换成父类指针是安全的;
向下转换不安全:父类指针转换成子类指针是不安全的。
static_cast
不能进行无关类型(如非基类和子类)指针之间的转换。
class Base{ };
class Derived : public base{ /**…*/ };
Base* B = new Base;
Derived* D = static_cast(B); // 不安全
```
为什么不安全?
D指向本质上还是B的对象模型,D指向的内存模型中可能存在B没有的成员变量。如果 `D->foo()` 中使用了 `D` 的成员变量,那么这个函数调用就是不安全的。因此,向下转换是安全的。
static_cast
还可以在左值和右值之间显示地转换。虽然不能隐式地将左值转换为右值,但是可以使用static_cast
显示地将左值转换为右值。
基本数据类型转换: enum
, int
, char
, float
等。安全性问题由开发者来保证。
把空指针转换成目标类型的空指针
int* iptr = static_cast<int*>(::malloc(sizoef(int)));
把任何类型的表达式转换成void类型:static_cast
static_cast
不能去掉类型的const、volitale
属性(用const_cast
)
隐式转换都建议使用 static_cast
进行标明和替换
专门用于将多态基类的指针或引用强制转换为派生类的指针或引用,而且能够检查转换的安全性。对于不安全的指针转换,转换结果返回 nullptr 指针。
使用特点:
基类必须要有虚函数,因为dynamic_cast
是运行时类型检查,需要运行时类型信息,而这个信息是存储在类的虚函数表中,只有一个类定义了虚函数,才会有虚函数表
对于下行转换,dynamic_cast
是安全的(当类型不一致时,转换过来的是空指针),而static_cast
是不安全的(当类型不一致时,转换过来的是错误意义的指针,可能造成踩内存,非法访问等各种问题), reinterpreter_cast
下行转换是可以转换,但是不安全。
相同基类不同子类之间的交叉转换,转换结果是是 nullptr
class Base
{
public:
virtual void fun() { }
};
class Drived : public base {
public:
int i;
};
Base *Bptr = new Drived();//语句0
Derived *Dptr1 = static_cast<Derived*>(Bptr); //语句1;
Derived *Dptr2 = dynamic_cast<Derived*>(Bptr); //语句2;
此时语句1和语句2都是安全的,因为此时 Bptr
确实是指向的派生类的内存模型,所以两个类型转换都是安全的。Dptr1
和 Dptr2
可以尽情访问 Drived
类中的成员,绝对不会出问题。但是如果此时语句0更改为如下表达:
Base* Bptr = new Base(); `
那么 Bptr
指向的是Base
对象内存模型。因此语句1是不安全的,因为如果访问子类的数据成员,其行为将是未定义。而语句2返回的是 nullptr
,更加直观的告诉用户不安全。
用于进行各种不同类型的指针之间、不同类型的引用之间以及指针和能容纳指针的整数类型之间的转换。转换时执行的是逐 byte
复制的操作。
reinterpret_cast
是从底层对数据仅仅进行重新解释,但没有进行二进制的转换,依赖具体的平台,可移植性差;reinterpret_cast
可以将整型转换为指针,也可以把指针转换为数组;reinterpret_cast
可以在指针和引用里进行肆无忌惮的转换;// RAII 技术封装
class RefCount {
public:
RefCount() : reference_{0}
{ }
~RefCount() {
this->decrementRef();
}
void IncrementRef()
{
++reference_;
}
bool decrementRef()
{
if (--reference_ == 0) {
delete this;
return true;
}
return false;
}
int64_t use_count() {
return reference_;
}
private:
std::atomic<int64_t> reference_;
};
template <typename T>
class SharedPtr
{
public:
SharedPtr() : ptr_(nullptr) {}
explicit SharedPtr(T* ptr)
: ptr_(ptr),
ref_(new RefCount)
{
if (ref_) ref_->IncrementRef();
}
SharedPtr(const SharedPtr& other)
: ptr_(other.ptr_),
ref_(other.ref_)
{
if (ref_) ref_->IncrementRef();
}
SharedPtr(SharedPtr&& other) noexcept {
ptr_ = other.ptr_;
ref_ = other.ref_;
other.ptr_ = nullptr;
other.ref_ = nullptr;
}
SharedPtr& operator=(const SharedPtr& other) {
if (this == &other || *this == other)
return *this;
reset();
ptr_ = other.ptr_;
ref_ = other.ref_;
ref_->IncrementRef();
return *this;
}
SharedPtr& operator=(SharedPtr&& other) noexcept {
if (this == &other || *this == other) return *this;
reset();
ptr_ = other.ptr_;
ref_ = other.ref_;
other.ptr_ = nullptr;
other.ref_ = nullptr;
return *this;
}
~SharedPtr()
{
if (ref_) this->decrementRef();
}
T& operator*() const { return *ptr_; }
T* operator->() const { return ptr_; }
explicit operator bool() const { return !!ptr_; }
T* get() const { return ptr_; }
void reset() {
if (ptr_) {
this->decrementRef();
// ptr_ = nullptr;
}
}
void decrementRef() {
if(ref_ && ptr_) {
if(ref_->decrementRef()) {
delete ptr_;
ptr_ = nullptr;
}
}
}
int64_t use_count() {
return ref_->use_count();
}
bool unique() {
return use_count() == 1;
}
void swap(SharedPtr & other) {
std::swap(ptr_, other.ptr_);
std::swap(ref_, other.ref_);
}
friend inline bool operator==(SharedPtr const& lhs, SharedPtr const& rhs) {
return lhs.ptr_ == rhs.ptr_;
}
friend inline bool operator!=(SharedPtr const& lhs, SharedPtr const& rhs) {
return lhs.ptr_ != rhs.ptr_;
}
friend inline bool operator<(SharedPtr const& lhs, SharedPtr const& rhs) {
return lhs.ptr_ < rhs.ptr_;
}
private:
T* ptr_;
RefCount* ref_;
};
int main(int argc, char const *argv[]) {
SharedPtr<int> iptr (new int);
SharedPtr<int> iptr2(iptr);
SharedPtr<int> iptr3(std::move(iptr));
SharedPtr<int> iptr4 = iptr2;
SharedPtr<int> iptr5 = std::move(iptr3);
std::cout<<iptr5.use_count()<<std::endl; // 3
return 0;
}
序列式容器
序列式容器会失效的原因是因为其存储都是连续的,因此删除或者插入一个元素都有可能导致其他元素的迭代器失效。
vector
erase
会导致删除节点之后的全部失效push_back
时,之前的end()
操作得到的迭代器失效insert/push_back
导致capacity()
改变,那么之前的first()/end()
得到的迭代器会失效insert
一个元素,如果空间没有分配,那么插入节点之前的迭代器位置有效,之后的失效。
简而言之:导致内存分配的全会失效,导致元素移动的会局部失效
deque
关联式容器
std::unorder_map 解决冲突方式是 拉链法(数组的每个元素都连着一个链表?):将所有产生冲突的关键字所对应的数据全部存储在同一个线性链表中(bucket)。这个方法解决数据存储位置发生冲突的哈希表,整个存储结构如图 1 所示。
其中 p_i表示存储的各个键值对。
当使用无序容器存储键值对时,会先申请一整块连续的存储空间,但此空间并不用来直接存储键值对,而是存储各个链表的头指针,各键值对真正的存储位置是各个链表的节点。STL
标准库默认选用vector
容器存储各个链表的头指针。STL
标准库中,将图 1 中的各个链表称为桶 bucket,每个桶都有自己的编号(从 0 开始)。当有新键值对存储到无序容器中时,整个存储过程分为如下几步:
key
的值带入设计好的哈希函数,会得到一个哈希值: H = hash(key);H % n
),该结果即表示应将此键值对存储到的桶的编号;其他解决冲突的方法
开放地址法:
H = ( h a s h ( k e y ) + d ) % m H =(hash(key) + d) \% m H=(hash(key)+d)%m
其中m
是哈希表的表长,d
是一个增量,当产生冲突时,选择以下三种方法一种获取d的值,然后计算,直到计算出的hash 值不存在冲突。
再哈希法rehasp
当通过哈希函数求得的哈希地址同其他关键字产生冲突时,使用另一个哈希函数计算,直到冲突不再发生
上述的拉链法解决了哈希冲突的问题,但是当插入元素很多,产生了严重哈希冲突时,就会导致某个链表长度越来越长,进而导致哈希表的查找就会退化为链表,效率降低为O(n)的时间复杂度。
哈希表存储结构还有一个重要的属性,称为负载因子load factor
,用于衡量容器存储键值对的空/满程度:即负载因子越大,意味着容器越满,即各链表中挂载着越多的键值对,这无疑会降低容器查找目标键值对的效率;反之,负载因子越小,容器肯定越空,但并不一定各个链表中挂载的键值对就越少。
负载因子的计算方法为:负载因子 = 容器存储的总键值对 / 桶数
STL
中默认情况下,无序容器的最大负载因子为 1.0。如果操作无序容器过程中,使得最大复杂因子超过了默认值,则容器会自动增加桶数,并重新进行哈希,以此来减小负载因子的值。需要注意的是,此过程会导致容器迭代器失效,但指向单个键值对的引用或者指针仍然有效。
因此当插入元素过多,使得负载因子的大于1.0,就会产生rehash行为,来改善即将下降的效率。
STL中的 unordered_map 的 rehash 策略
一下代码节选自g++的STL库:
// __n_bkt is current bucket count, __n_elt is current element count,
// and __n_ins is number of elements to be inserted. Do we need to
// increase bucket count? If so, return make_pair(true, n), where n
// is the new bucket count. If not, return make_pair(false, 0).
std::pair<bool, size_t>
_M_need_rehash(size_t __n_bkt, size_t __n_elt, size_t __n_ins) noexcept {
if (__n_elt + __n_ins >= _M_next_resize) {
long double __min_bkts = (__n_elt + __n_ins) / (long double)_M_max_load_factor;
if (__min_bkts >= __n_bkt)
// 这个是需要rehash 时返回的策略
return std::make_pair(true,
_M_next_bkt(std::max<size_t>(__builtin_floor(__min_bkts) + 1,
__n_bkt * _S_growth_factor)));
_M_next_resize = __builtin_floor(__n_bkt * (long double)_M_max_load_factor);
return std::make_pair(false, 0);
}
return std::make_pair(false, 0);
}
size_t _M_next_bkt(size_t __n) noexcept {
const auto __max_width = std::min<size_t>(sizeof(size_t), 8); // 8 个字节
const auto __max_bkt = size_t(1) << (__max_width * __CHAR_BIT__ - 1); // 2 ^ 63
size_t __res = __clp2(__n); // 计算大于等于 n 的最小的2的幂
if (__res == __n) __res <<= 1;
if (__res == 0) __res = __max_bkt;
// Set next resize to the max value so that we never try to rehash again
// as we already reach the biggest possible bucket number.
// Note that it might result in max_load_factor not being respected.
if (__res == __max_bkt)
_M_next_resize = size_t(-1);
else
_M_next_resize = __builtin_ceil(__res * (long double)_M_max_load_factor);
return __res;
}
其中,在这个类的前面有定义:
- _M_max_load_factor 初始化为 1.0
- static const size_t _S_growth_factor = 2;
整个扩容的策略大致是按照2倍的策略增长,但是并不严格按照。在MSVC中按照的是8倍的扩充策略。
STL
中的 哈希表 是线程不安全的(其实 STL
库都是线程不安全的)。 比如两个线程同时向 std::unordered_map 中插入数据,当发生rehash时,如果不加锁,可能导致两个线程都会产生rehash。
如何优化 多线程读写操作?这里要借鉴下java的分段锁。
红黑树的规则
条件1和2说明,红色节点均是内部节点,其父节点及左、右孩子节点必然存在;另外条件3说明,红节点之父必为黑色,因此树中任一通路都不含相邻的红节点
红黑树的效率为什么比AVL树高?如果只有查询操作哪种树的效率高?
红黑树的效率高,是因为不需要像AVL
树那样,为了维护高度平衡性,而不断地动态调整以维护左右子树高度差不超过1.红黑树降低了对平衡度的要求,以减少每次插入/删除操作时的动态调整次数.但是就查询效率而言,是不如AVL
树的.
void *Memcpy(void *dst, const void *src, size_t1 size)
{
char *psrc; //源地址
char *pdst; //目标地址
if(NULL == dst || NULL == src)
{
return NULL;
}
if((src < dst) && (char *)src + size > (char *)dst) //源地址在前,对应上述情况2,需要自后//向前拷贝
{
psrc = (char *)src + size - 1;
pdst = (char *)dst + size - 1;
while(size--)
{
*pdst-- = *psrc--;
}
}
else //源地址在后,对应上述第一种情况,直接逐个拷贝*pdst++ = *psrc++即可
{
psrc = (char *)src;
pdst = (char *)dst;
while(size--)
{
*pdst++ = *psrc++;
}
}
return dst;
}
答:自己动手实现memcpy()时就需要考虑地址重叠的情况。我们来看个简单的例子。有一个5个元素的数组,不妨设为int arr = {1,2,3,4,5};考虑2种情况:
1)源地址是arr[2],目标地址是arr[0],自前向后拷贝3个元素后arr为{3,4,5,4,5}
2)源地址是arr[0],目标地址是arr[2],自前向后拷贝3个元素后arr为{1,2,1,2,3}
第一种情况,由低地址向高地址逐个将源地址的元素拷贝到目标地址就行,容易;
第二种情况需要注意,如果是按第一种情况由低地址拷贝到高地址,需要分3个步骤把arr[0]=1,arr[1]=2,arr[2]=3三个元素逐个拷贝,重点在于第一步是将arr[0]拷贝到arr[2]的位置,这样就会把原来的arr[2]=3改为arr[2]=1,覆盖了原来的值,因此在这种情况,我们需要自后向前拷贝,也就是高地址向低地址拷贝。也就是第一步将arr[2]放到arr[4],第二步将arr[1]放到arr[3],第一步将arr[0]放到arr[2].
解析:
1、固定数组
在函数体内分配(不带static)是在栈
中的
2、固定数组是全局变量和带static前缀的局部数组是在全局数据
的
3、固定数组在类中分配是在堆中的
4、动态数组
(通过malloc或者new出来的空间)不管在函数体中、类中、全局变量都是在堆
中
解析:
1)首先char与int都分为signed与unsigned类型,默认情况下都是signed类型。
2)从长字节数据类型转换为短字节数据类型,会产生截断:
如从4字节的int类型转换成1个字节的char类型,则取int数据的最低的一个字节,将这个字节的数据赋给char型数据,且是有符号的
,即首位为符号位;而如果是从int转换成unsigned char类型,则整个一个字节都是数据,没有符号位。
1)从短字节类型转换为长字节类型,从char转换为int:则在前面的三个字节补符号位,即补上0xffffff
(char的首位为1),或0x000000
(char的首位为0)。从unsigned char转换为int,则前面补上0x000000
.
解析:
在C语言中,static作用:“改变生命周期” 或者 “改变作用域”。有以下特性:
1)static局部变量:局部变量为动态存储,即指令执行到定义处才分配内存,将一个变量声明为函数的局部变量,使其变为静态存储方式(静态数据区),那么这个局部变量在函数执行完成之后不会被释放,而是继续保留在内存中。
2)static全局变量:全局变量即定义{}
外面,其本身就是静态变量,编译时就分配内存,这只会**改变其连接方式,**使其只在本文件内部有效,而其他文件不可连接或引用该变量。
3)static函数:对函数的连接方式产生影响,使得函数只在本文件内部有效,对其他文件是不可见的。这样的函数又叫作静态函数。使用静态函数的好处是,不用担心与其他文件的同名函数产生干扰,另外也是对函数本身的一种保护机制。如果想要其他文件可以引用本地函数,则要在函数定义时使用关键字extern,表示该函数是外部函数,可供其他文件调用。另外在要引用别的文件中定义的外部函数的文件中,使用extern声明要用的外部函数即可。
到了C++的时候,static多了几个其他的作用:
4)static类成员变量
:表示这个成员为全类所共有,对类的所有对象只有一份拷贝,可以借助类名直接访问。
5)static类成员函数
:表示这个函数为全类所共有,而且只能访问静态成员变量,因为这个函数不接收this指针。
解析:主要有以下区别
1)用``#define MAX 255`定义的常量是没有类型的(不进行类型安全检查,可能会产生意想不到的错误),所给出的是一个立即数,编译器只是把所定义的常量值与所定义的常量的名字联系起来,define所定义的宏变量在预处理****阶段的时候进行替换,在程序中使用到该常量的地方都要进行拷贝替换;
用``const float MAX = 255`;定义的常量有类型****(编译时会进行类型检查)名字,存放在内存的静态区域中,在编译时确定其值。在程序运行过程中const变量只有一个拷贝,而#define所定义的宏变量却有多个拷贝,所以宏定义在程序运行过程中所消耗的内存要比const变量的大得多;
2)用define定义的常量是不可以用指针变量去指向的,用const定义的常量是可以用指针去指向该常量的地址的;
3)用define可以定义一些简单的函数(宏替换只作替换,不做计算,不做表达式求解),const是不可以定义函数的.
4)宏定义的作用范围仅限于当前文件。 而默认状态下,const对象只在文件内有效,当多个文件中出现了同名的const变量时,等同于在不同文件中分别定义了独立的变量。 如果想在多个文件之间共享const对象,必须在变量定义之前添加extern关键字(在声明和定义时都要加)。
volatile关键词的作用是影响编译器编译的结果,用volatile声明的变量表示该变量随时可能发生变化,与该变量有关的运算,不要进行编译优化,以免出错。
解析:嵌入式问的不多,当作了解。
回忆一下小时候查字典的步骤,索引和字典目录的概念是一致的。字典目录可以让我们不用翻整本字典就找到我们需要的内容页数,然后翻到那一页就可以。索引也是一样,索引是对记录按照多个字段进行排序的一种展现。对表中的某个字段建立索引会创建另一种数据结构,其中保存着字段的值,每个值还包括指向与它相关记录的指针。这样,就不必要查询整个数据库,自然提升了查询效率。同时,索引的数据结构是经过排序的,因而可以对其执行二分查找,那就更快了
先说优点:
1)大大加快数据的检索速度,这也是创建索引的最主要的原因
2)加速表和表之间的连接,特别是在实现数据的参考完整性方面特别有意义。
3)在使用分组和排序子句进行数据检索时,同样可以显著减少查询中分组和排序的时间。
再说缺点:
1)创建索引需要耗费一定的时间,但是问题不大,一般索引只要build一次
2)索引需要占用物理空间,特别是聚集索引,需要较大的空间
3)当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,降低了数据的维护速度,这个是比较大的问题。
解析:这是本人在面试CVTE遇到的面试题。
1、支持随机存取迭代器的(连续存储空间)vector、deque(双向存取vector)使用STL的sort函数。
2、不支持随机存取迭代器的(链式非连续存储空间)list(双向链表)、slist(单向链表forward_list),不能使用STL的sort函数,因此都会在类中定义sort()成员函数,使用对象名调用即可。
3、关系型容器中基于红黑树的set、multiset、map、multimap,本身就有自动从大到小排序的功能。所以不需要sort函数。
4、stack、queue没有迭代器,各元素的出入口特定,不能进行排序。
5、基于哈希表的(hash)unordered_set/multiset/map/multimap,都是未排序的,当然因为计算hash再存储的特性,也不需要进行排序。
解析:建立连接的时候,服务器在LISTEN状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。
而关闭连接时,服务器收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,而自己也未必全部数据都发送给对方了,所以己方可以立即关闭,也可以发送一些数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,因此,己方ACK和FIN一般都会分开发送,从而导致多了一次。
解析:MSL(Maximum Segment Lifetime),TCP允许不同的实现可以设置不同的MSL值。
第一,保证客户端发送的最后一个ACK报文能够到达服务器,因为这个ACK报文可能丢失,站在服务器的角度看来,我已经发送了FIN+ACK报文请求断开了,**客户端还没有给我回应,**应该是我发送的请求断开报文它没有收到,于是服务器又会重新发送一次,而客户端就能在这个2MSL时间段内收到这个重传的报文,接着给出回应报文,并且会重启2MSL计时器。
第二,防止类似与“三次握手”中提到了的“已经失效的连接请求报文段”出现在本连接中。客户端发送完最后一个确认报文后,在这个2MSL时间中,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失。这样新的连接中不会出现旧连接的请求报文。