C++八股

C++

C/C++内存有几种类型

C中,内存分为5个区:堆(malloc),栈(局部变量,函数参数),程序代码区(二进制代码),全局/静态存储区(全局变量,static变量)和常量存储区(常量)。
C++额外加了自由存储区。
全局变量,static会初始化为缺省值,而堆和栈上的变量是随机的,不稳定的。

  • 栈,在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
  • 堆,就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。
  • 全局/静态存储区,内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。它主要存放静态数据(局部static变量,全局static变量)、全局变量和常量。
  • 常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量字符串,不允许修改。
  • 代码区,存放着程序的二进制代码
静态内存分配和 动态内存分配

静态内存是指程序开始运行时,由编译器自动分配和释放空间。程序中的各种变量,在程序编译时都需要分配空间,当函数调用完,空间自动释放。而动态内存是在堆上开辟的,必须由程序员申请释放

  1. 静态内存分配是在编译时完成的,不需要占用CPU资源;动态分配内存是在运行时完成的,动态内存的分配与释放需要占用CPU资源;
  2. 静态内存分配是在栈上分配的,动态内存是堆上分配的;
  3. 动态内存分配需要指针或引用数据类型的支持,而静态内存分配不需要;
  4. 静态分配内存需要在编译前确定内存块的大小,而动态分配内存不需要编译前确定内存大小,根据运行时环境确定需要的内存块大小,按照需要分配内存即可。可以这么说,静态内存分配是按计划分配,而动态内存分配是按需分配。
  5. 静态分配内存是把内存的控制权交给了编译器,而动态内存是把内存的控制权交给了程序员;
    静态分配内存适合于编译时就已经可以确定需要占用内存多少的情况,而在编译时不能确定内存需求量时可使用动态分配内存;但静态分配内存的运行效率要比动态分配内存的效率要高,因为动态内存分配与释放需要额外的开销;动态内存管理水平严重依赖于程序员的水平,如果处理不当容易造成内存泄漏。
动态内存函数

1.free函数
free 函数用于释放动态开辟的内存;
如果参数 ptr 指向的空间不是动态开辟的,此时free函数的行为是未定义的;
如果参数 ptr 是NULL指针,则函数无动作;
2.malloc 函数
这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针;
如果开辟成功,返回一个指向开辟的空间的首地址的指针(无类型指针);
如果开辟失败,返回一个NULL指针,因此malloc函数的返回值,一定要做检查;
返回值的类型时void*,所以malloc函数并不知道开辟空间的类型,具体在使用时由使用者自己决定(强制转换);
如果参数size为0,malloc函数的行为是标准未定义的,取决于编译器;
3.realloc 函数
void* realloc(void* ptr,size_t size);
ptr是需要调整的内存地址,size是调整之后的空间大小;
返回值为调整后的内存的起始地址;
函数在调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间;
//对于内存空间的调整,存在两种情况:
1.原有空间之后的空间足够满足新空间的开辟;
2.原有空间之后没有足够大的空间满足新空间的开辟;
//函数调整失败的情况:
1.realloc失败的时候,返回NULL;
2.realloc失败的时候,原来的内存不改变,不会释放也不会移动;
4.calloc函数
void* calloc(size_t num,size_t size);
函数的功能是为num个大小为size的元素开辟一块空间,并将空间的每个字节初始化为0;
与函数malloc的区别在于,calloc会在返回地址前把申请的空间的每个字节初始化为全0;

5.new函数
new是一个运算符,在标准库函数中就有,不需要导入头文件。
new运算符则会自动计算出所需要申请空间的大小。
new运算符则可以直接返回对应数据类型的地址。
new失败会抛出异常。

内存错误

1.内存泄漏和内存溢出
C++ 内存泄漏与溢出
 内存溢出 OOM (out of memory),是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个int,但给它存了long才能存下的数,那就是内存溢出。
 内存泄露 memory leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。最终的结果就是导致OOM。
 内存泄漏的情况:
1,malloc/free或者new/delete未成对出现 通过局部分配的内存
2,未在调用者函数体内释放
3,没有将基类的析构函数定义为虚函数,当基类的指针指向子类时,delete该对象时,不会调用子类的析构函数 没有正确地清除嵌套的对象指针
4.由于程序运行时出现不可遇见的错误,导致内存泄漏
2 二次释放
尝试释放已经被释放的内存,可能导致程序崩溃。
3 悬空指针
访问超出动态分配内存范围的内存,可能导致未定义行为。
4 野指针

野指针与悬空指针

野指针就是指针指向的位置不可知的。(随机的、不正确的、没有明确限制的)
有三种情况
1.指针未定义,指针在被定义的时候,如果程序不对其进行初始化的话,它会指向随机区域,因为任何指针变量(除了static修饰的指针变量)在被定义的时候是不会被置空的,它的默认值是随机的。
2.指针操作超越变量作用域
3.指针被释放时没有被置空
5 内存越界访问
越界访问以及防止越界访问的方式
6 竞争条件
7 内存破坏 (不重要)
C++常见的三种内存破的场景和分析

智能指针

C++智能指针详解
智能指针是C++用于管理动态分配对象的一种指针类型,它能够自动地分配和释放内存,避免内存泄漏和悬挂指针的问题,常见的智能指针有unique_ptr,shared_ptr,weak_ptr 和 auto_ptr(已经弃用)
智能指针背后的核心概念是动态分配内存的所有权。智能指针实际上是一个对象,在对象的外面包围了一个拥有该对象的普通指针。这个包围的常规指针称为裸指针。
shared_ptr 强智能指针,可以改变资源的引用计数
weak_ptr 弱智能指针,不会改变资源的引用计数
unique_ptr 独占式智能指针,只能转移,不能赋值
智能指针使用
智能指针被称为可以拥有或管理它所指向的对象。当需要让单个指针拥有动态分配的对象时,可以使用独占指针。对象的所有权可以从一个独占指针转移到另一个指针,其转移方式为:对象始终只能有一个指针作为其所有者。当独占指针离开其作用域或将要拥有不同的对象时,它会自动释放自己所管理的对象
shared_ptr采用了引用计数器,允许多个指针指向同一个对象。每一个shared_ptr的拷贝都指向相同的内存,并共同维护同一个引用计数器,记录同一个实例被引用的次数。每使用他一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,自动删除所指向的堆内存。shared_ptr内部的引用计数是线程安全的,但是对象的读取需要加锁。
weak_ptr 一般不单独使用,是和shared_ptr搭配使用的,甚至于,我们可以将 weak_ptr 类型指针视为 shared_ptr 指针的一种辅助工具,借助 weak_ptr 类型指针, 我们可以获取 shared_ptr 指针的一些状态信息,比如有多少指向相同的 shared_ptr 指针、shared_ptr 指针指向的堆内存是否已经被释放等等。最重要的是解决了强引用指针的循环引用问题。

malloc/free 和 new/delete的区别

malloc/free 维护堆的分配,new/delete维护自由存储区,但基本上C++编译器默认使用堆来实现自由存储,也就是缺省的全局运算符new和delete也许会按照malloc和free来被实现。new/delete除了调用malloc/free 分配/释放 空间,还会调用对象构造/析构函数。
new操作符会根据类型自动计算所需内存大小,并进行类型匹配,返回的是对象类型指针,而malloc需要手动计算内存大小,并使用强制类型转换,返回的是void指针。
内存泄漏的检测:new可以通过异常机制检测内存分配失败,而malloc在分配失败时返回NULL,需要手动检查。

堆和栈的区别

(1)堆存放动态分配的对象——那些在程序运行时动态分配的对象,比如说new出来的对象,其生存期由程序控制
(2)栈用来保存定义在函数内的非static对象,如局部变量,仅在其定义的程序块运行时才存在
(3)静态内存用来保存static对象,类static数据成员以及定义在任何函数外部的变量,static对象在使用前分配,程序结束时销毁
(4)栈和静态内存的对象由编译器自动创建和销毁
(5)堆的生长方向向上地址越来越大,栈的生长空间向下,地址越来越小
(6)对空间因为频繁的分配和释放操作,会产生内存碎片。
(7)栈会比堆更快一些,操作系统会在底层对栈提供支持,会分配专门的寄存器存放栈的地址。堆的操作是由C/C++函数库提供的,在分配堆内存是会使用一定算法寻找合适大小的内存。

堆和自由存储区的区别

总的来说,是C语言和操作系统的术语,是操作系统维护的一块动态分配内存;自由存储区是C++中通过new与delete动态分配和释放对象的抽象概念,他们并不是完全一样。
从技术上来说,堆是语言和操作系统的术语,堆是操作系统所维护的一块特殊内存,它提供了动态分配的功能,当运行程序调用malloc()时就会从中分配,稍后调用free就可以把内存交还。而自由存储是C++通过new和delete动态分配和释放对象的抽象概念,通过new来申请的内存区域可称为自由存储区。基本上C++编译器默认使用堆来实现自由存储,也就是缺省的全局运算符new和delete也许会按照malloc和free来被实现,这时藉由new运算分配符分配的对象,说它在堆上对,说他在自由存储区上也对。

全局/静态存储区、常量存储区

全局/静态存储区:全局变量和静态变量被分配到同一块内存中;
常量存储区:存放的是常量,是不允许修改的。
静态存储是main函数运行前分配内存并初始化;常量存储是固化在执行文件上的数据。
常量存储区里面的数据是放在代码段里的,不占内存。静态存储区是在内存空间中的,在其所属的类(或文件)中是全局的。
注意:
C和C++中,const修饰的变量所在的存储区是不一样的。
在C中,const修饰的变量存储在“栈”中。
在C++中,const修饰的变量被定义为一个常量是放在代码段里的,是在常量存储区中的。

静态变量

存储在静态数据区的变量会在程序刚开始运行时就完成初始化,也是唯一的一次初始化。共有两种变量存储在静态存储区:全局变量和 static 变量。

define

使用#define可以用来定义宏,宏可分为两种,一种是不带参数的宏定义,这也就是使用#define定义标识符。第二种是带参数的宏定义,
define替换规则:
1.在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换
2.替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替
3.最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程
注意:
宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归
当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索
在这里插入图片描述

static

static详解

const

const详解

static 和 const

在这里插入图片描述

const 与 #define 的区别

(1)编译器处理方式不同
define宏是在预处理阶段展开。
const常量是编译、运行阶段使用。
(2)类型和安全检查不同
define宏没有类型,不做任何类型检查,仅仅是展开,存在边界的错误。
const常量有具体的类型,在编译阶段会执行类型检查。
(3)存储方式不同
define宏仅仅是展开,有多少地方使用,就展开多少次,不会分配内存,它定义的宏常量在内存中有若干个备份,占用代码段空间。
const常量在程序运行过程中只有一份备份,占用数据段空间,会在内存中分配(可以是堆中也可以是栈中)。
(4)调试和可读性
使用 const 可以提供更好的调试信息,因为常量有类型信息。
使用 #define 定义的常量在代码中只是简单的文本替换,可能会使代码可读性较差。

inline和 const的区别

内联函数(inline)与宏定义(#define)使用说明及区别

Struct

C++ Struct(结构体)详解

Union

union 联合体 定义

struct 和class区别

1.默认访问权限:struct中的成员默认为公共,而class的成员默认为私有。
2.默认继承方式:struct的继承方式默认为公共,而class中的继承方式默认为私有
3.使用习惯:struct适用于简单的数据结构,class适用于复杂的数据类型和实现面向对象编程
4.成员变量和成员函数:struct中的成员变量和成员函数默认为公共,而class中的成员变量和成员函数默认为私有
5.访问控制:struct中的成员外部可直接访问,而class中的成员需要使用公共成员来访问。
6.默认的构造函数和析构函数:class会自动生成默认的构造函数和析构函数,而struct不会。

程序的编译的过程

程序的编译过程中就是将用户的文本形式的源代码(C/C++)转换成计算机可以直接执行的机器代码的过程,主要经过四个过程:预处理,编译,汇编,链接。
在这里插入图片描述
C/C++语言预处理,编译,汇编,链接的详细过程讲解

C++函数调用的过程

(1)参数拷贝,注意顺序是从右到左
(2)保存调用函数的下一条指令
(3)跳转到fun()函数
(4)移动ebp,esp形成新的栈帧结构
(5)压栈形成临时变量并执行相关操作
(6)return一个值
(7)出栈
(8)恢复main函数的栈帧结构
(9)返回main函数

类是一种用户自定义的数据类型,它由数据成员和成员函数组成。数据成员存储对象的状态,成员函数定义对象的行为。
通过类,我们可以创建多个对象,每个对象都拥有相同的数据成员和成员函数,但可能存在不同的状态和行为。‘

数据封装:类可以将数据和操作封装在一起,提供对外统一的接口,隐藏底层实现细节。(封装)
代码复用:通过创建对象,可以重复使用已经定义好的类,减少代码的冗余。
继承与多态:通过继承机制,可以创建新的类来扩展或修改现有类的功能。多态则允许以统一的方式使用不同的派生类对象。
注意:类的静态成员函数和类的数据成员,虚函数,
1.访问控制
(1)继承方式
派生类可以定义在基类成员,但是派生类的成员函数不一定有权访问从基类继承来的成员。
类的继承后方法属性变化:(权限只能大变小,权限小继承不变)
使用private继承,父类的所有方法在子类中变为private;
使用protected继承,父类的protected和public方法在子类中变为protected,private方法不变;
使用public继承,父类中的方法属性不发生改变;
(2)访问方式
友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员
公有成员可以被该类中的函数、子类的函数、其友元函数访问,也可以由该类的对象访问
私有成员只能由该类中的函数、其友元函数访问,不能被任何其他访问,该类的对象也不能访问.
受保护成员可以被该类中的函数、子类的函数、以及其友元函数访问,但不能被该类的对象访问
2.构造函数
造函数是一种特殊的成员函数,用于创建对象时进行初始化。它与类同名,没有返回类型,并且可以有参数。当我们创建一个对象时,构造函数被自动调用,用于初始化对象的数据成员。
(1)默认构造函数 Student()
(2)有参构造函数 Student(int num,int age)
(3)拷贝构造函数 Student(const Student&)
(4)转换构造函数 Student(int r) //形参时其他类型变量,且只有一个形参
(5)移动构造函数 Student(Student&&)
(6)委托构造函数
C++11 引入了委托构造的概念,某个类型的一个构造函数可以委托同类型的另一个构造函数对对象进行初始化。
拷贝构造函数参数中为什么需要加const和引用
引用:
如果通过按值传递机制,在传递对象之前,编译器需要创建该对象的副本。因此,编译器为了处理拷贝构造函数的这条调用语句,需要调用拷贝构造函数来创建拷贝对象的副本。但是,由于是按值传递,第二次调用同样需要创建副本,因此还得调用拷贝构造函数,就这样持续不休。最终得到的是对拷贝构造函数的无穷调用。
const:
其实,这里,如果不去改变实参的值的话,不加const的效果和加const的效果是一样的,而且不加const编译器也不会报错,因为函数的形参是引用,则调用函数时不需要复制实参,函数是直接访问调用函数中的实参变量的。但是为了整个程序的安全,还是加上const,防止对实参的意外修改~所以这里再将复制构造函数原型改为以下这种形式:

拷贝构造函数和移动构造函数区别
拷贝构造函数用于从一个已存在的对象创建一个新的对象,即复制构造函数。它会复制原始对象的所有成员变量的值,从而创建一个新的、与原始对象相同的对象。
移动构造函数用于从一个右值引用的临时对象创建一个新的对象。它会“窃取”原始对象的资源(例如指针或文件句柄),并将其移动到新对象中,从而避免复制大量数据,提高性能和减少内存使用。
构造函数的执行顺序及析构函数的执行顺序
(1)派生类的构造函数/析构函数执行顺序
创造一个子类对象时(new一个子类对象或在栈中构造),先执行父类的构造函数,再执行自身的构造函数。如果子类继承多个父类,则按照继承的顺序从左到右调用父类构造函数(类继承列表中的越靠前的父类,其构造函数越先执行,第一个父类的构造函数首先执行),析构的顺序与构造的顺序相反。
(2)虚拟继承下的构造函数/析构函数顺序

  1. 首先执行虚基类的构造函数,多个虚基类按照背继承的顺序构造
  2. 执行基类的构造函数,多个基类的构造函数按照被继承的顺序构造:(虚基类的构造优先于普通基类)
  3. 执行成员对象的构造函数,多个成员对象的构造函数按照申明的顺序构造;
  4. 执行派生类自己的构造函数;

3.继承
继承中的作用域
1.在继承体系中基类和派生类都有独立的作用域。
2.子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用基类::基类成员显示访问)
3.需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。

基类和派生类对象赋值转换
派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或切割。基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(RunTime Type Information)的dynamic_cast 来进行识别后进行安全转换。
菱形继承
当其中某些类中有相同的成员变量或者函数时,就会导致在类中访问出现不明确,所以也就出现了菱形继承中的二义性问题在这里插入图片描述
解决方法:
(1)使用作用域::的方式访问:不过此方式下,只能方法访问到派生类不同继承于基类的对象,并没有从根本上解决菱形继承问题。
(2)虚继承的方式:在派生类对象中只保留一份共享基类子对象 的方式来消除二义性和数据冗余。虚拟继承使得虚基类子对象被单独存储,并通过指针访问,而不是嵌入到每个派生类对象中,因此不会在每个派生类之间产生冗余数据。虚拟继承使得虚基类子对象被单独存储,并通过指针访问,而不是嵌入到每个派生类对象中,因此不会在每个派生类之间产生冗余数据。
C++中类所占的内存大小以及成员函数的存储位置
同一个类创建的多个对象,其数据成员是各用各的,互不相通(静态成员变量是共享的)。
成员函数是共享共用的,多个对象共用一份代码,所有类成员函数和非成员函数代码存放在代码区。
不论成员函数在类内定义还是在类外定义,成员函数的代码段都用同一种方式存储。
4.虚基类
虚基类是为了解决菱形继承导致的数据冗余和二义性。如果将公共基类说明为虚基类。那么,对同一个虚基类的构造函数只调用一次,且仅且在第一次出现时候调用,这样从不同的路径继承的虚基类的成员在内存中就只拥有一个拷贝。从而解决了以上的二义性问题。
如果在虚基类中定义有带形参的构造函数,并且没有定义默认形式的构造函数,则整个继承结构中,所有直接或间接的派生类都必须在构造函数的成员初始化表中列出对虚基类构造函数的调用,以初始化在虚基类中定义的数据成员。
5.抽象类
6.虚函数
虚函数和和普通成员函数区别
1.普通函数是编联的,而调用虚函数是动态联编的
2.联编的作用:程序调用函数,编译器决定使用哪个可执行代码块。(所谓联编就是将函数名和函数体的程序连接到一起的过程)
3.静态编联:在编译的时候就确定了函数的地址,然后call就调用了。(静态联编本质是系统用实参与形参进行匹配,对于重名的重载函数根据参数上的差异进行区分,然后进行联编,从而实现编译时的多态。函数的选择基于指向对象的指针类型或者引用类型。)
4.动态编联: 首先需要取到对象的首地址,然后再解引用取到虚函数表的首地址后,再加上偏移量才能找到要调的虚函数,然后call调用。(动态联编本质上是运行阶段执行的联编,当程序调用某一个函数时,系统会根据当前的对象类型去寻找和连接其程序的代码。函数的选择基于对象的类型。)
为什么要把基类的析构函数定义为虚函数?
7.多态实现
8.C++ 重载、重写(覆盖)、隐藏的定义与区别

C/C++引用和指针的区别

指针是一个实体,需要分配内存空间,引用只是变量的别名,不需要分配内存空间。
引用在定义的时候必须被初始化,并且不能够被改变,指针在定义的时候不一定要初始化,并且指向的空间可变。
有多级指针,却只能有一级引用
自增和引用的自增运算结果不一样,(指针是指向下一空间,在引用时引用的变量加1)
sizeof引用得到的时指向的变量(对象)大小,而sizeof指针得到的是指针本身的大小
引用访问一个变量时直接访问,而指针访问一个变量时间接访问。
引用只是对指针进行了简单的封装,它的底层依旧是通过指针实现的。

C++中的指针传参和引用传参

指针参数传递本质上是值传递,它所传递的是一个地址值,值传递的过程中,被调函数的形式参数作为被调用函数的局部变量处理,会在栈中开辟内存空间以存放由主调函数传递进来的实参值,从而形成实参的一个副本,值传递的特点是,被调用函数对形式参数的任何操作都是作为局部变量进行的,不会影响主调函数的实参变量的值(形参指针变了,实参不会变)
引用参数传递过程中,被调用的形式参数也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调
函数放进来的实参变量的地址,被调用函数对形参的任何操作都会被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实际变量。

纯虚函数

纯虚函数是只有声明没有实现的虚函数,是对子类的约束,是接口继承
包含纯虚函数的类是抽象类,他不能被实例化,只有实现了这个纯虚函数的子类才能生成对象。

深拷贝和浅拷贝

浅拷贝就是将对象的指针进行简单的复制,原对象和副本指向的是相同的资源。
而深拷贝是新开辟一块空间,将原对象的资源复制到新的空间,并返回该空间的地址。
深拷贝可以避免重复事发和写冲突,比如野指针问题!

inline关键字以及和宏定义的区别

inline是内联的意识,可以定义比较小的函数,因为函数频繁调用会占用很多栈空间,进行栈出栈操作也耗费计算资源,所有可以用inline关键字修饰频繁调用的小函数。编译器会在编译阶段嵌入内联函数的调用语块中。
1.内联函数在编译时展开,而宏在预编译时展开
2.在编译的时候,内联函数直接被嵌入目标代码中去了,而宏只是一个简单的文本替换
3.内联函数可以进行诸如类型安全检测,语句是否是正确等编译功能,而宏不具有这样的功能
4.宏不是函数,而inline是函数
5.宏在定义时,要小心处理宏参数,一般用括号括起来。否则容易出现二义性,而内联函数不会出现二义性。
6.inline可以不展开,而宏一定要展开。inline指示对编译器而言只是一个建议

一个函数或者可执行文件的生成过程或者编译过程是怎样的

预处理,编译,汇编,链接

  • 预处理:对预处理命令进行替换等预处理操作
  • 编译:代码优化和生成汇编代码
  • 汇编:将汇编代码转为机器语言
  • 链接:将目标文件彼此链接起来。
STL组件 (底层实现,使用方法,相关问题)
容器

1.顺序容器
顺序容器主要包含:
(1)array<T,N>(数组容器):表示可以存储 N 个 T 类型的元素,是 C++ 本身提供的一种容器。此类容器一旦建立,其长度就是固定不变的,这意味着不能增加或删除元素,只能改变某个元素的值。**它就是再C++普通数组的基础上,添加了一些成员函数和全局变量。**在使用上,它比普通数组更安全,且效率没有更差。不过,和其他容器不同,array容器大小固定,无法动态扩展和收缩,它只允许访问或替换存储的元素。
(2)vector(向量容器):用来存放 T 类型的元素,是一个长度可变的序列容器,即在存储空间不足时,会自动申请更多的内存。使用此容器,在尾部增加或删除元素的效率最高(时间复杂度为 O(1) 常数阶),在其它位置插入或删除元素效率较差(时间复杂度为 O(n) 线性阶,其中 n 为容器中元素的个数)。
深入理解STL中的vector
(3)deque(双端队列容器):和 vector 非常相似,区别在于使用该容器不仅尾部插入和删除元素高效,在头部插入或删除元素也同样高效,时间复杂度都是 O(1) 常数阶,但是在容器中某一位置处插入或删除元素,时间复杂度为 O(n) 线性阶。
deque底层实现原理剖析
(4)list(链表容器):是一个长度可变的、由 T 类型元素组成的序列,它以双向链表的形式组织元素,在这个序列的任何地方都可以高效地增加或删除元素(时间复杂度都为常数阶 O(1)),但访问容器中任意元素的速度要比前三种容器慢,这是因为 list 必须从第一个元素或最后一个元素开始访问,需要沿着链表移动,直到到达想要的元素。
(5)forward_list(正向链表容器):和 list 容器非常类似,只不过它以单链表的形式组织元素,它内部的元素只能从第一个元素开始访问,是一类比链表容器快、更节省内存的容器。
STL forward_list 模拟实现

2.关联容器
注意:multi的意思是允许重复
(1)set, multiset
set就是集合,STL的set用二叉树实现,集合中的每个元素只出现一次(参照数学中集合的互斥性),并且是排好序的(默认按键值升序排列)
(2)map, multimap
是C++中STL中的一个关联容器,以键值对来存储数据,数据类型自己定义。它的内部数据结构是红黑树。
map的简单使用
map排序
(3)unordered_map的使用以及与map和hash_map

哈希相关

常见哈希函数
1.直接定址法
取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B 优点:简单、均匀 缺点:需要事先知道关键字的分布情况 使用场景:适合查找比较小且连续的情况。
2.除留余数法
设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址
3.平方取中法
假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址; 再比如关键字为4321,对它平方就是18671041,抽取中间的3位671(或710)作为哈希地址 平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况。
4.折叠法
折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况
5.随机数法
选择一个随机函数,取关键字的随机函数值为它的哈希地址,即H(key) = random(key),其中random为随机数函数。通常应用于关键字长度不等时采用此法
哈希冲突
对于两个数据元素的关键字,经过哈希函数映射之后得到了相同的值,即:不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。

哈希冲突解决方法
详情
1.开发地址法
(1)线性探测法
当我们的所需要存放值的位置被占了,我们就往后面一直加1并对m取模直到存在一个空余的地址供我们存放值,取模是为了保证找到的位置在0~m-1的有效空间之中。
(2)平方探测法
当我们的所需要存放值的位置被占了,会前后寻找而不是单独方向的寻找。
2.再哈希法
同时构造多个不同的哈希函数,等发生哈希冲突时就使用第二个、第三个……等其他的哈希函数计算地址,直到不发生冲突为止。虽然不易发生聚集,但是增加了计算时间。
3.链接地址法
将所有哈希地址相同的记录都链接在同一链表中。
4.建立公共溢出区
将哈希表分为基本表和溢出表,将发生冲突的都存放在溢出表中。

计网

TCP的三次握手与四次挥手的详细介绍
三次握手

(1)client向server发送请求报文,该报文包含SYN(同步) = 1,client_seq(客户端序列号)=任意值x
(2)server端接收到了这个请求,并分配资源,同时给client返回一个ACK报文,这个报文中呢包含了这些字段,标志位SYN和ACK(同意请求)都为1,而ack为x+1,server_seq(服务端序列号) = 任意值y
(3)client收到server发送的ACK信息后,会看到server端 ack = x+1,也会发送一个ACK报文给server端。 包括ACK=1,seq = x+1,ack = y+ 1

四次挥手

TCP断开连接通常是一方主动,一方被动,我们假设client主动
(1)向Server发送FIN报文,其中FIN=1,client_seq = 任意值u,client进入FIN_WAIT第一阶段
(2)Server返回一个ACK报文,ack = u + 1,server_seq = u + 1,client进入FIN_WAIT第二阶段,Server进入CLOSE_WAIT阶段
(3)Server发送完所有数据时,会给client发送FIN报文,Server进入LAST_ACK
(4)client收到FIN报文时,会发送ACK报文,进入TIME_WAIT状态,当Server收到后会进入CLOSED状态,当client等待2MSL(报文最大生存时间)后,没有接受到消息后,client会断开连接。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值