后端面试知识点总结 C++基础知识

C++基础知识

c和c++的区别

C面向过程,C++面向对象

  • 面向过程语言:性能比面向对象高,因为类调用时需要实例化,比较消耗资源,但是没有面向对象易维护,易复用,易扩展。
  • 面向对象语言:易维护,易复用,易扩展,由于具有面向对象的特性,可以设计出低耦合的系统使得系统更加灵活。但是性能低。
  • c++仍然是以c位基础:区块、语句、预处理器、内置数据类型、数组、指针等都来自c。但是c++有模板、异常、重载、stl库、分装继承多态等部分。

后缀名不同

一些关键字有区别:

  • C中的struct中不能有函数,但是C++中可以有
  • malloc:返回值为void*,C中可以直接赋值给任何类型指针,但是C++中必须强制类型转换。

返回值

  • C中如果一个函数没有返回值默认返回int,C++中没有返回值需要指定void

缺省参数

  • C不支持缺省参数

函数重载

  • C不支持函数重载,C++支持函数重载,常用于处理实现功能类似但是类型不同的问题。

C++和java联系和区别

  • 都是面向对象的语言,都支持封装,继承和多态
  • java不提供指针直接访问内存,程序更加安全
  • java的类是单继承的,C++支持多重继承;但是java的接口可以多继承
  • java有自动内存管理机制,不需要程序员手动释放无用内存。

指针引用

为什么使用指针

  • 能够动态分配内存,实现内存自由管理
  • 方便使用数组和字符串
  • 值传递不如指针传递高效

指针和引用的区别

  • 指针是地址引用是别名
  • 指针本身也是一个对象占有一块内存;引用不占有内存,指向的对象可以更改,引用初始化时必须指向一个已经存在的对象

const&static

const和define区别

  • 编译器处理不同:
    • 宏定义在预处理阶段展开,不能对宏定义进行调试,生命周期结束于编译时期
    • const是一个“编译运行时”概念,在程序运行中使用起作用
  • 起作用的方式不同:
    • 宏定义只是简单的字符替换,没有类型检查,存在边界的错误;const对应数据类型,进行类型检查
  • 存储方式:
    • 宏定义进行展开,它定义的宏常量在内存中有若干个备份,占用代码段空间;const定义的只读变量在程序运行过程中只有一份备份,占用数据段空间。
  • 宏定义不能调试,因为在预编译阶段会被替换;const可以进行调试
  • 宏定义可以使用#undef取消符号定义;const不能重定义
  • define可以用来防止头文件重复引用;const不能
  • define不可以用于类成员变量的定义,但是可以用于全局变量;const可以用于类成员变量的定义,只要一定义,不可修改
  • define可以用表达式作为名称;const采用一个普通的常量名称。

const 作用

  • 修饰变量,说明该变量不可以被改变;
  • 修饰指针,分为指向常量的指针(pointer to const)和自身是常量的指针(常量指针,const pointer);
  • 修饰引用,指向常量的引用(reference to const),用于形参类型,即避免了拷贝,又避免了函数对值的修改;
  • 修饰成员函数,说明该成员函数内不能修改成员变量。

const成员函数和static成员函数的区别

  • const成员函数:由于传入函数内部的this指针是一个const,所以在函数内部不能通过this指针访问类的非静态成员变量,当然如果硬要访问也可以,在类成员变量前加mutable修饰就行了。
  • static成员函数:对于这个类和他的所有对象来说只有一个static成员函数实体。由于static成员函数不在内部传入this指针,所以它也无法访问类的非静态成员变量。另外,static成员函数可以直接由类名或者对象名调用。

const与指针

  • 常量指针:const修饰的是指针,指针本身是一个常量地址不可以被改变,但是指针所指向的对象是可以改变的。
  • 指向常量的指针:const修饰的是指针指向的对象,对象不可以被改变,但是指针可以改变指向的对象。
  • 指向常量的常量指针:不论是指针本身还是指向的对象都不可以被改变。

static的作用

  • 对于全局变量:改变全局变量的作用域,使其他的编译单元不可见(data全局初始化区)
  • 对于局部变量:改变他的存储位置(由栈到静态存储区)和生命周期(函数结束到程序结束)
  • 成员变量:成为类的全局变量,需要在类外初始化,只有唯一的实例,被所有的类对象共享。
  • 成员函数:成为类的静态成员函数被所有类对象共享,可以直接用类名调用,没有隐含this指针

static const int a

·static const int a可以在类内进行初始化,而static int a不可以

this指针

  • this 指针是一个隐含于每一个非静态成员函数中的特殊指针。它指向调用该成员函数的那个对象。
  • 当对一个对象调用成员函数时,编译程序先将对象的地址赋给 this 指针,然后调用成员函数,每次成员函数存取数据成员时,都隐式使用 this 指针。
  • 当一个成员函数被调用时,自动向它传递一个隐含的参数,该参数是一个指向这个成员函数所在的对象的指针。
  • this 指针被隐含地声明为: ClassName *const this,这意味着不能给 this 指针赋值;在 ClassName 类的 const 成员函数中,this 指针的类型为:const ClassName* const,这说明不能对 this 指针所指向的这种对象是不可修改的(即不能对这种对象的数据成员进行赋值操作);
  • this 并不是一个常规变量,而是个右值,所以不能取得 this 的地址(不能 &this)。
  • 在以下场景中,经常需要显式引用指针:
    1. 为实现对象的链式引用;
    2. 为避免对同一对象进行赋值操作;
    3. 在实现一些数据结构时,如 list

inline 内联函数

特征

  • 相当于把内联函数里面的内容写在调用内联函数处;
  • 相当于不用执行进入函数的步骤,直接执行函数体;
  • 相当于宏,却比宏多了类型检查,真正具有函数特性;
  • 编译器一般不内联包含循环、递归、switch 等复杂操作的内联函数;
  • 在类声明中定义的函数,除了虚函数的其他函数都会自动隐式地当成内联函数。

使用

inline 使用

// 声明1(加 inline,建议使用)
inline int functionName(int first, int second,...);

// 声明2(不加 inline)
int functionName(int first, int second,...);

// 定义
inline int functionName(int first, int second,...) {/****/};

// 类内定义,隐式内联
class A {
    int doA() { return 0; }         // 隐式内联
}

// 类外定义,需要显式内联
class A {
    int doA();
}
inline int A::doA() { return 0; }   // 需要显式内联

编译器对 inline 函数的处理步骤

  1. 将 inline 函数体复制到 inline 函数调用点处;
  2. 为所用 inline 函数中的局部变量分配内存空间;
  3. 将 inline 函数的的输入参数和返回值映射到调用方法的局部变量空间中;
  4. 如果 inline 函数有多个返回点,将其转变为 inline 函数代码块末尾的分支(使用 GOTO)。

inline优缺点

优点

  1. 内联函数同宏函数一样将在被调用处进行代码展开,省去了参数压栈、栈帧开辟与回收,结果返回等,从而提高程序运行速度。
  2. 内联函数相比宏函数来说,在代码展开时,会做安全检查或自动类型转换(同普通函数),而宏定义则不会。
  3. 在类中声明同时定义的成员函数,自动转化为内联函数,因此内联函数可以访问类的成员变量,宏定义则不能。
  4. 内联函数在运行时可调试,而宏定义不可以。

缺点

  1. 代码膨胀。内联是以代码膨胀(复制)为代价,消除函数调用带来的开销。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。
  2. inline 函数无法随着函数库升级而升级。inline函数的改变需要重新编译,不像 non-inline 可以直接链接。
  3. 是否内联,程序员不可控。内联函数只是对编译器的建议,是否对函数内联,决定权在于编译器。

C++ 中 struct 和 class

总的来说,struct 更适合看成是一个数据结构的实现体,class 更适合看成是一个对象的实现体。

区别

  • 最本质的一个区别就是默认的访问控制
    1. 默认的继承访问权限。struct 是 public 的,class 是 private 的。
    2. struct 作为数据结构的实现体,它默认的数据访问控制是 public 的,而 class 作为对象的实现体,它默认的成员变量访问控制是 private 的。

union联合体类型

  • 联合(union)是一种节省空间的特殊的类,一个 union 可以有多个数据成员,但是在任意时刻只有一个数据成员可以有值。当某个成员被赋值后其他成员变为未定义状态。联合有如下特点:
    • 默认访问控制符为 public
    • 可以含有构造函数、析构函数
    • 不能含有引用类型的成员
    • 不能继承自其他类,不能作为基类
    • 不能含有虚函数
    • 匿名 union 在定义所在作用域可直接访问 union 成员
    • 匿名 union 不能包含 protected 成员或 private 成员
    • 全局匿名联合必须是静态(static)的

enum枚举类型

  • 限定作用域的枚举类型
enum class open_modes{input, output, append};
  • 不限定作用域的枚举类型
enum color {red, yellow, green};
enum {floatPrec=6, doublePrec=10;

extern关键字

  • extern c :告诉编译器在编译的时候按着c的规则去编译
  • extern :所声明的函数和变量可以在本模块和其他模块一起使用

volatile关键字

volatile int i = 10;
  • volatile关键字是一种类型修饰符,用它声明的类型变量可以被某些编译器未知的因素改变(操作系统,硬件等)。所以使用该关键字告诉编译器不应对这样的对象进行优化
  • volatile关键字声明的变量,每次访问都必须从内存中取值(没有被volatile修饰的变量,可能由于被编译器优化,从CPU寄存器中取值)
  • const编译期保证代码中没有被修改的地方,运行的时候不受限制)可以是volatile(编译期告诉编译器不优化该变量,在运行期每次都从内存中取值):表示一个变量在程序编译期不能被修改并且不能被优化,在程序运行期变量值可能会被修改,每次使用到该变量值都需要从内存中读取,防止意外错误
  • 指针可以是volatile

explicit关键字

  • explicit修饰构造函数时,可以防止隐式转换或者复制初始化
  • explicit修饰转换函数时,可以防止隐式转换,但按语境转换除外

使用

struct A{
	A(int){}
	operator bool() const {return true;}
};
struct B{
	explicit B(int){}
	explicit operator bool() const {return true;}
};
int main{
    B b1(1);	//OK,直接初始化
    B b2 = 1;	//Error,被explict修饰的构造函数不能复制初始化
}

using

  • 一条using声明语句一次只引入命名空间的一个成员,这使得我们可以清楚知道程序所引用的到底是哪个命名空间里的函数,比如
using namespace_name::name;
  • 构造函数的using声明:在C++11中,可以直接重用其直接基类定义的构造函数,比如
class Derived:Base{
public:
	using Base:Base;
};
  • 如上,对于基类的每个构造函数,编译器都生成一个与之对应(形参列表完全相同)的派生类构造函数,生成如下类型构造函数:
Derived(params):Base(args){}
  • using指示使得某个特定命名空间中所有名字都可见,这样就无需再为他们都加上任何前缀了,比如
using namespace std;

应当尽量少使用using指示污染命名空间,尽量多使用using声明

::范围解析运算符

  • 分类
    • 全局作用域符(::name):用于类型名称(类,类成员,成员函数,变量等)前,表示作用域为全局命名空间
    • 类作用域符(class::name):用于表示指定类型的作用域范围是具体某个类的
    • 命名空间作用域符(namespace::name):用于表示指定类型的作用域范围是具体某个命名空间的

右值引用

  • 左值和右值的概念

    • 左值:能对表达式取地址、或具名对象/变量。一般指表达式结束后依然存在的持久对象。
    • 右值:不能对表达式取地址,或匿名对象。一般指表达式结束就不再存在的临时对象。
      • C++11:右值是由两个概念构成,将亡值和纯右值。纯右值是用于识别临时变量和一些不跟对象关联的值,比如1+3产生的临时变量值,2、true等,而将亡值通常是指具有转移语义的对象,比如返回右值引用T&&的函数返回值等
  • 它实现了转移语义和精确传递。它的主要目的有两个方面:

    • 消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率
    • 能够更简洁明确地定义泛型函数
  • 总结一个类型T

    • 左值引用, 使用 T&, 只能绑定左值
    • 右值引用, 使用 T&&, 只能绑定右值
    • 常量左值, 使用 const T&, 既可以绑定左值又可以绑定右值
    • 已命名的右值引用,编译器会认为是个左值
    • 编译器有返回值优化,但不要过于依赖

野指针

  • 访问一个已销毁或者访问受限的内存区域的指针

  • 产生的原因

    • 指针定义时未被初始化
    • 指针被释放时没有置空
    • 指针操作超越变量作用域:不要返回指向栈内存的指针或者引用,因为栈内存在函数结束的时候会被释放

函数指针

  • 定义:函数指针是指向函数的指针变量,c在编译的时候每一个函数都会有一个入口地址,函数指针就是指向这个地址。
  • 用途:调用函数和做函数的参数,比如回调函数

重载和覆盖

  • 重载:两个函数名相同,但是参数列表不同的函数,在同一个作用域中
  • 覆盖:子类继承父类,重写父类中的虚函数

strcpy和strlen

  • strlen计算字符串长度,返回长度
  • sltcpy逐个拷贝字符,因为没有指定长度所以会导致拷贝越界,安全版本为strncpy

assert()

断言,是宏,而非函数。assert 宏的原型定义在 (C)、(C++)中,其作用是如果它的条件返回错误,则终止程序执行。可以通过定义 NDEBUG 来关闭 assert,但是需要在源代码的开头,include 之前。

assert() 使用

#define NDEBUG          // 加上这行,则 assert 不可用
#include 

assert( p != NULL );    // assert 不可用

函数的调用过程

使用到了三个常用的寄存器,EIP为指令指针,即指向下一条即将执行的指令的地址;EBP为基址地址,常用来指向栈底;ESP为栈顶指针,常用来指向栈顶。

  • 函数调用
    - 将调用函数的参数从右往左(第一个参数在栈顶,可以动态变化参数个数,出栈的时候最左边的先出来)压入帧栈中,call指令跳转到子函数起始地址
  • 保存现场
    - 将call指令后一条指令的地址压入栈中,实际上就是把EIP指针入栈
    - 将EDP入栈,因为每个函数都有自己的栈区域,所以栈基址不是一样的,现在需要进入新函数,为了不覆盖调用函数的EDP,将它压入栈中。
    - 将ESP作为EBP,即将此时的栈顶地址作为该函数的栈基址
    - 再分别将EBX,ESI,EDI压入栈中,他们分别为基址寄存器,源变址寄存器,目的变址寄存器
    - 执行子函数
  • 恢复现场
    - 分别弹出EDI,ESI,EBX的值
    - 将EDP的值赋给ESP,弹出EBP的值
    - 执行call返回断点

pragram pack(n)

  • 设定structunion以及类成员变量以n字节方式对齐,也就是让数据在内存中连续存储,同时以n字节对齐

使用

#pragram pack(push)	//保持对齐状态
#pragram pack(4)	//设定为4字节对齐
struct test{
	char m1;
	double m4;
	int m3;
};
#pragram pack(pop)	//恢复对齐状态

使用C实现C++的类

  • 使用C实现C++的对象特性(封装、继承、多态)
    • 封装:使用函数指针把属性和方法封装到结构体中
    • 继承:结构体嵌套
    • 多态:父类和子类方法的函数指针不同

friend友元类和函数

  • 能访问私有成员
  • 破环封装性
  • 友元关系不能传递
  • 友元关系的单向性
  • 友元声明的形式和数量不受限制

构造函数和初始化

  • **默认构造函数(无参数):**如果创建一个类没有写任何构造函数,系统自动生成默认的构造函数,或者写一个不带任何参数的构造函数。
  • **一般构造函数:**可以有各种参数形式,一个类可以有多个构造函数,前提是参数的个数或者类型不同
  • **拷贝构造函数:**参数为类对象本身的引用,用于根据一个已经存在的对象复制出一个新的该类的对象,一般在函数中会将已存在对象的数据成员的值复制一份到新创建的对象中。参数是(const &)类型的。
    • 用类的一个对象取初始化另一个对象时
    • 当函数的形参是类的对象时(值传递),如果是引用则不会使用拷贝构造函数
    • 当函数的返回值是类的对象时,如果是引用则不会使用拷贝构造函数

=dufault:将拷贝控制成员定义为=default显式要求编译器生成合成的版本

=delete:将拷贝构造函数和拷贝赋值运算符定义为删除的函数,阻止拷贝

=0:将虚函数定义为纯虚函数

构造函数/拷贝构造函数/赋值运算符

  • 构造函数:对象不存在,自己进行初始化工作
  • 拷贝构造函数:对象不存在,使用其他对象进行初始化
  • 赋值运算符:对象存在,用其他的对象给他赋值

直接初始化和复制初始化

直接初始化直接调用于实参匹配的构造函数

复制初始化总是调用复制构造函数,复制初始化首先使用指定构造函数创建一个临时对象,然后用复制构造函数将那个临时对象复制到正在创建的对象。

初始化和赋值

而初始化是要创建一个新的对象,并且其初值来自于另一个已存在的对象。

赋值操作是在两个已经存在的对象间进行的。

初始化列表

  • C++构造函数的初始化列表可以使得代码更加简洁,并且少了一次调用默认构造函数的过程,可以用于全部成员变量,也可以只用于部分成员变量
  • 成员变量的初始化顺序和初始化列表中列出的变量的顺序无关,只和成员变量再类中声明的顺序有关
  • 还有一个重要的功能 就是初始化const成员变量。初始化const成员变量的唯一方法就是使用初始化列表
  • 引用类型,引用必须再定义的时候初始化,并且不能重新赋值,所以也要写在初始化列表里面

initializer_list列表初始化

  • 使用花括号初始化器列表初始化一个对象,其中对应构造函数接受一个std::initializer_list参数

C++异常处理

C++中异常事件发生时,程序使用throw关键字抛出异常表达式,抛出点称为异常出现点,有操作系统为程序设置当前异常对象,然后执行程序的当前异常处理代码块,在包含了异常出现点的最内层的try块,一次匹配catch语句中的异常对象(只进行类型匹配,catch的参数有时候在catch语句中并不会使用到)。

若异常匹配成功,则执行catch块内的异常处理语句,然后执行try…catch之后的代码。如果当前的try…catch找不到匹配该异常对象的catch语句,则有更外层的try…catch块来处理该异常;如果当前函数内所有的try…catch块都不能匹配该异常,则递归回退到调用栈的上一层处理该异常。

如果一直回退到主函数都无法处理,则调用系统函数terminate()终止程序。

// 一些异常类型
exception		最常用的异常类,只报告异常的发生而不报告任何额外的信息
runtime_error	只有在运行的时候才能检测该异常
overflow_error	运行时错误:计算上溢
underflow_error	运行时错误:计算下溢
logic_error		逻辑错误
invalid_argument	逻辑错误:参数无效
out_of_range	逻辑错误:使用一个超出有效范围的值
bad_alloc		内存动态分配错误
try{
	语句
}
catch(异常类型){
	异常处理代码
}
catch(异常类型){
	异常处理代码
}

C++11 新特性

  • nullptr:替代null,专门用来区分空指针
  • 类型推导:auto(变量) ,decltype(表达式)
  • 区间迭代:for(auto i :arr)
  • 初始化列表:initializer listvector num{1,2,3}
  • 智能指针
  • lambda表达式
    • [捕获区](参数区){代码区};比如sort(arr.begin(),arr.end(),[](int a, int b){return a > b;});
    • 捕获一般有以下几种用法:
      • [a,&b]:其中a以复制捕获,b以引用捕获
      • [this]:以引用捕获当前对象(*this)
      • [&]:以引用捕获所有用于lambda体内的自动变量,并以引用捕获当前对象,如果存在
      • [=]:以复制捕获
      • []:不捕获,大多数情况下适用
  • 右值引用和移动move语义:
    • C++中的变量要么是左值(通俗指非临时变量,&引用),要么是右值(通俗指临时对象,&&引用)。
    • 右值引用实现了转移语义和完美转发,主要目的有两个方面
      • 消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率
      • 能够更简洁的定义泛型函数
    • 移动语义:可以将资源(堆,系统对象等)从一个对象转移到另一个对象,这样可以减少不必要的临时对象的创建、拷贝及销毁
    • std::move:将左值变成一个右值,它是将对象的状态或者所有权从一个对象转移到另一个对象,只是转义,没有内存拷贝。
    • 完美转发:std::forward()函数可以对左值引用,右值引用,常左值引用,常右值引用进行完美转发。
  • 新增容器:array,forward_list,无序容器(通过哈希实现)(unordered_map/multimap,unordered_set/multiset)

C++ STL

Standard Template Library,标准模板库。

主要由六大组件组成:

  • 容器(Containers):各种数据结构如(顺序容器)vector,deque,forward_list,list;(关联容器)set,multiset,map,multimap;(无序关联容器)unordered_set,unordered_multiset,unordered_map,unordered_multimap
  • 算法(Algorithm):各种常用算法如sort,search,copy等
  • 迭代器(Iterators):泛型指针。
  • 仿函数(functors):行为类似函数,可以作为算法的某种策略
  • 适配器(Adapters):一种来修饰容器或仿函数或迭代器接口的东西如stack,queue,priority_queue
  • 分配器(Allocators):负责空间配置和管理

分配器(Allocators)

构造和析构的基本工具

construct()接收一个指针和初值value,该函数的用途是将初值设定到指针所指的空间上。C++的placement new运算符可用来完成。

destory()有两个版本,第一个版本接收一个指针,准备将该指针所指之物释放;第二版本接受first和last两个迭代器,将之间的对象析构掉。

空间配置和释放

对象构造前的空间配置和对象析构后的空间释放,由负责:

  • 向内存堆要求空间
  • 考虑多线程状态
  • 考虑内存不足的应变措施
  • 考虑内存碎片问题

C++内存配置由::operator new()::operator delete()完成

为了考虑到内存碎片问题,SGI设计了双层级配置器,第一层配置器(__malloc_alloc_template)直接使用malloc()free();第二级配置器(__default_alloc_template)视情况使用不同的策略:

  • 如果配置区块大于128bytes,视之为足够大,直接调用第一级配置器
  • 如果小于,视之为过小,为了降低额外负担和内存碎片,使用内存池(memory pool)方式
    • 维护16个自由链表(free lists),负责16种小型区块的次配置能力,内存池以malloc()配置而得

STL内存池(Memory Pool)

chunk_alloc()函数以end_free-start_free来判断内存池的大小,如果大小充足,就直接调用20个区块返回给free list,如果不足就提供一个以上的区块,这时候引用传递的nobjs参数会被修改为实际能够调用的区块数。如果一个也无法提供,就利用malloc()函数从heap中配置内存,增加内存池大小。新大小为需求量的两倍,再加上随着配置次数增加而越来越大的附加量。

迭代器(Iterators)和traits编程技巧

一种方法,能够依序巡访某个容器所含的各个元素,而又无需暴露该容器的内部表述方式。

迭代器是一种行为类似指针的对象,而指针的各种行为中最常见也最重要的便是解引用(dereference)和成员访问(member access)。

  • 可以使用模板函数的参数推导功能来进行迭代器的关联类型(associated type),但是参数推导功能只能推导参数,而无法推导函数的返回值类型;
  • 可以声明内嵌类型,但是并不是所有的迭代器都是class type,比如原生指针就不行。所以如果不是class type,就无法为它定义内嵌类型。
  • 所以还需要针对**原生指针T*和const原生指针const T***定义特化版

但是迭代器的关联类型并不只是”迭代器所指对象的类型“一种而已,最常用的关联类型有5种。

traits(特性)

也被称为特性萃取技术,就是指提取”被传进的对象“对应的返回类型,让同一个接口实现对应的功能

在STL种,算法和容器是分离的,两者通过迭代器连接,算法在实现时并不知道传进来的是什么,traits技法相当于在接口和实现之间加了一层封装,来隐藏一些细节并协助调用合适的方法,这需要一些技巧(比如偏特化)。

根据经验,最常用的迭代器关联类型有5种:

// 内嵌类型
template<class T>
struct iterator_traits{
    // 由于C++语言默认情况下,假定通过作用域运算符访问的名字不是类型,所以想要访问的是类型的时候
    // 必须使用typename告诉编译器这是一个类型
	typedef typename T::iterator_category	iterator_category;
	typedef typename T::value_type			value_type;
	typedef typename T::difference_type		difference_type;
	typedef typename T::pointer				pointer;
	typedef typename T::reference			reference;
};
// T*特化版
template<class T>
struct iterator_traits<T*>{
    typedef random_access_iterator_tag	iterator_category;
    typedef T							value_type;
    typedef ptrdiff_t					difference_type;
    typedef T*							pointer;
    typedef T&							refernce;
};
// const T*特化版
template<class T>
struct iterator_traits<const T*>{
    typedef random_access_iterator_tag	iterator_category;
    typedef T							value_type;
    typedef ptrdiff_t					difference_type;
    tyepdef const T*					pointer;
    typedef const T&					reference;
};

通过iterator_traits的封装,则对任意类型(不论是迭代器,还是原生指针int*或const int*),都可以通过它获取正确的关联类型信息。

  • value_type:指迭代器所指对象的类型
  • difference_type:用来表示两个迭代器之间的距离
  • reference_type:指引用类型const T&
  • pointer_type:指针类型T*
  • iterator_category:迭代器根据移动特性和操作,可以分为五类
    • Input Iterator:这种迭代器所指的对象,不允许改变,只读
    • Output Iterator:只写
    • Forward Iterator:允许”写入型“算法(例如replace())在此种迭代器所形成的区间上进行读写操作
    • Bidirectional Iterator:可双向移动,某些算法需要逆向走访某个迭代器区间(例如逆向拷贝某范围内的元素)
    • Random Access Iterator:前四种迭代器都只提供一部分指针算术能力(前三支持opertor++,第四还支持operatr--),第五种则涵盖所有指针算术能力(p+n,p-n,p[n],p1-p2,p1

顺序容器(sequential containers)

  • vector:
    • 底层数组
    • 随机读改,尾部增删O(1),头部增删O(N),支持随机访问
    • 无序可重复
  • list:
    • 底层双向链表
    • 指定位置增删O(1),不支持随机访问
    • 无序可重复
  • forward_list:
    • 底层单向链表
    • 指定位置增删O(1),不支持随机访问
    • 无序可重复
  • deque:
    • 底层双端队列(一个中央控制器+多个缓冲区)
    • 头尾增删O(1),支持随机访问
    • 无序可重复

vector

相比于数组的静态空间,Vector是动态空间,随着元素的加入,内部机制会自动扩充空间以容纳新元素。

vector支持随机存取,提供的是Random_access_iterators,也就是vector的迭代器是普通指针。

vector的扩容(在 VS 下是 1.5倍,在 GCC 下是 2 倍)

  • 如果原大小为0,则配置1(各元素大小);

  • 如果原大小不为1,则配置原大小的两倍(前半段用来放原数据,后半段用来放新数据);(开辟一块新的空间而不是在原来空间的后面添加)

  • 将原来vector的内容拷贝到新的vector中;再新的vector插入新元素

  • 析构原来的vector,并调整迭代器指向新的vector。(一旦引起空间重新配置,所有指向原来vector的迭代器就都失效了)

插入时首先检查备用空间大小是否大于新增元素个数m

  • 如果大于,则先把插入点之后的所有元素向后移动m位,然后在插入点插入新增元素;
  • 如果小于,则先按照上述扩容步骤(max(old_size,n))进行扩容,然后将插入点之前的数据复制到新空间,再把新增元素填入新空间,最后把旧vector中插入点之后的元素复制到新空间。

使用vector的注意事项

在一个vector的尾部之外的任何位置添加元素,都需要重新移动元素。同时可能会引起整个对象存储空间的重新分配。

频繁调用push_back()可能会引起对象的重新分配,并将旧的空间元素移动到新的空间,这十分耗费时间。

list

任何位置的元素插入和删除的时间复杂度都是常数时间。

// 底层实现为双向链表
template <class T>
struct __list_node{
	typedef void* void_pointer;
	void_pointer prev;
	void_pointer next;
	T data;
};

由于底层实现为双向链表,迭代器必须具有前后移动的能力,所以list提供的迭代器为bidirectional_iterator双向迭代器,并且插入和结合操作都不会导致原有的list迭代器的失效,删除操作时只会导致”指向被删除的那个元素“的迭代器失效。

在SGI STL中,list的实现不仅是双向链表,同时还是一个环状双向链表,所以只需要一个指针就可以完整的遍历整个链表。此时只需要在最后一个节点的后面加上一个空白节点,就可以实现STL要求的**”前闭后开“**区间的要求。

List的排序算法不能使用STL的算法sort(),必须使用自己的sort()成员函数,因为STL的sort()函数只接受RandomAccessIterator

forward_list

单向链表,任何位置的插入和删除的时间复杂度都是O(1),不支持随机访问。迭代器为forward_iterator

元素插入使用push_front(),所以其元素次序于插入次序相反。

deque队列

vector是一个单向开口的连续线性空间,deque是一个双向开口的连续线性空间(双端队列),即可以在头尾两端分别进行插入和删除操作。

相对vector的区别:

  • deque允许以常数时间内对头部进行元素的插入和删除操作
  • deque没有容量(capacity)概念,因为是以动态的分段连续空间组合而成,随时可以增加一段新的空间并链接起来

虽然deque也提供Random Access Iterator,但是他的迭代器并不是普通的指针,这影响了他的计算效率。

对deque进行排序操作时,为了最高效率,可以将它完整的复制到一个vector中,排序之后在复制回deque。

deque的中控器

deque由一段一段的定量连续空间组成,一旦有必要在deque的头部或者尾部添加新空间,便配置一定量的连续空间,串接在整个deque的头部或者尾部。同时使用主控维护其整体连续的假象。

deque采用一块所谓的map来作为主控,其中每个元素都是一个指针,指向另一端连续线性空间,称为缓冲区。缓冲区才是deque的存储空间主体。

deque的迭代器

T*	cur;	// 指向所指缓冲区中的当前元素
T*	first;	// 指向所指缓冲区中的头
T*	last;	// 指向所指缓冲区的尾(含备用空间)
map_pointer node;	// 指向map中指向该缓冲区的节点

deque::begin()	//返回迭代器start,指向deque中第一个元素
deque::end()	//返回迭代器end,指向deque中最后一个元素

deque的插入与扩容

当进行push_back()操作时,当插入的元素正好使缓冲区满时,进行扩容操作,新开辟一个缓冲区,并将end指向新开辟的缓冲区

进行push_front()操作时,首先使用start.cur != start.first判断第一缓冲区是否还有备用空间,如果有就直接在备用空间上构造元素;如果没有就配置一个新的缓冲区,并将start指向新开辟的缓冲区。

在扩容过程中,如果map的尾部的节点备用空间不足,就配置一个更大的,拷贝原来的过去,然后释放;如果头部节点空间不足,也是如此。

容器适配器(container adapter)

stack

是一种先进后出(FILO)的数据结构,只有一个出口,只能在其顶端操作O(1),不允许遍历,stack没有迭代器。

其底层使用deque或者list实现。因为其以底层容器完成其所有工作,而具有这种”修改某物接口,形成另一种风貌“的性质的结构称为适配器。

queue

是一种先进先出(FIFO)的数据结构,其有两个出口,允许在头尾两端进行操作(尾部插入,头部取出)O(1),不允许遍历,没有迭代器。

底层使用deque或者list实现。

priority_queue

是一种有序的queue,允许在头尾两端进行操作(尾部插入,头部取出)O(logN),不允许遍历,没有迭代器。

底层使用vector(max-heap或min-heap)实现。

有序关联容器

STL的关联式容器主要有set(集合)和map(映射)两大类,以及他们的衍生体multiset和multimap,底层均以RB-tree(红黑树)实现。RB-tree也是一个独立容器,但是不开放给外界使用。

关联式容器即每个元素都有一个键(key)和一个值(value)。当元素被插入到关联式容器中时,容器内部结构(RB-tree)便依据键值大小,将其放置在合适的位置。关联式容器没有头部和尾部的概念。

set

所有元素都会根据元素的键值自动被排序,set元素的键值就是value实值。

set不允许存在两个相同的元素。

底层使用RB-tree实现,存在constant bidirectional iterators,即不允许通过迭代器对集合内元素进行修改。

与list类似,当对元素进行增删操作时,除了被删除的那个迭代器之外,其他迭代器全部有效。

map

所有元素都会按照元素的键值自动排序。map的所有元素都是pair,同时拥有键值(key)和实值(value)。

pair的第一个元素被视为键值,第二个元素为实值。map不允许存在两个相同的键值。

底层使用RB-tree实现,存在constant bidirectional iterators,即不允许通过迭代器对集合内元素进行修改。

与list类似,当对元素进行增删操作时,除了被删除的那个迭代器之外,其他迭代器全部有效。

multiset

与set的唯一差别就是允许键值重复,底层的插入操作使用的是RB-tree的insert_equal()而不是insert_unique()

迭代器为constant bidirectional iterators

multimap

与map的唯一差别就是允许键值重复,底层的插入操作使用的是RB-tree的insert_equal()而不是insert_unique()

迭代器为constant bidirectional iterators

无序关联容器

底层使用hash_table实现,其中哈希表里的元素节点被称为桶(bucket),桶内维护的是链表,但不是STL中的链表,而是自行维护的一个node。bucket聚合体也即hash_table使用vector实现。

template <class Value>
struct __hashtable_node{
	__hashtable_node* next;
	Value val;
};

所有无序关联容器的迭代器为forward iterators

unordered_set

是含有键值类型唯一对象集合的关联容器。搜索、插入和删除都拥有平均O(1)时间复杂度。

在内部,元素并不以任何特别顺序排序,而是组织进桶中。元素被放进哪个桶完全依赖其值的哈希。这允许对单独元素的快速访问,因为哈希一旦确定,就准确指代元素被放入的桶。

不可修改容器元素(即使通过非 const 迭代器),因为修改可能更改元素的哈希,并破坏容器。

unordered_map

一种关联容器,含有带唯一键的键值对pair。搜索、插入和删除都拥有平均常数时间复杂度。

元素在内部不以任何特定顺序排序,而是组织进桶中。元素放进哪个桶完全依赖于其键的哈希。这允许对单独元素的快速访问,因为一旦计算哈希,则它准确指代元素所放进的桶。

unordered_multiset

是关联容器,含有可能非唯一 Key 类型对象的集合。搜索、插入和删除拥有平均常数时间复杂度。

元素在内部并不以任何顺序排序,只是被组织到桶中。元素被放入哪个桶完全依赖其值的哈希。这允许快速访问单独的元素,因为一旦计算哈希,它就指代放置该元素的准确的桶。

插入操作使用insert_equal(),set使用的是insert_unique()

unordered_multimap

一种关联容器,含有可能非唯一键的键值对pair。搜索、插入和删除都拥有平均常数时间复杂度。

元素在内部不以任何特定顺序排序,而是组织到桶中。元素被放进哪个桶完全依赖于其关键的哈希。这允许到单独元素的快速访问,因为哈希一旦计算,则它指代元素被放进的准确的桶。

算法

以有限的步骤解决逻辑或者数学上的问题,称为算法。Algorithm。

STL算法的一般形式:所有泛型算法的前两个参数都是一对迭代器(iterators),通常称为first和last,用于标示算法的操作区间。

STL中习惯采用前闭后开区间表示法,写成[first,last),表示区间涵盖first至last(不含last)之间的所有元素。当last==last时,表示一个空区间。

sort()

排序算法接受两个RandomAccessIterators(随机存取迭代器),然后将区间内的所有元素以递增方式由小到大重新排列;也可以接受一个仿函数作为排序标准。

可以对vector,deque进行排序,因为他们的迭代器都属于RandomAccessIterators。对list进行排序需要使用他们自己的sort()成员函数。

STL中的sort()算法,数据量大时使用快速排序,分段递归排序。数据量小于某个门槛(16)时,为了避免快排的递归调用带来过大的额外负荷,使用插排。如果递归层次过深,还会使用堆排

插入排序

以双层循环的形式进行。外循环遍历整个序列,每次迭代决定出一个子区间;内循环遍历子区间,将子区间内的每一个“逆转对”倒转过来。

当数据量很少时效果很好,原因是因为STL中实现了一些技巧,同时不需要进行递归调用等操作,会减少额外负担。

void __insertion_sort(iterator first, iterator last){
	if(first == last) return;
	for(iterator i = first + 1; i != last; i++)			// 外循环
		__linear_insert(first, i, value_type(first));	// [first, i)形成一个子区间
}

// 辅助函数
void __linear_insert(iterator first, iterator last, T*){
    T value = *last;			// 记录尾元素
    if(value < *first){			// 当尾元素比头部小时
        copy_backward(first, last, last+1);		// 就一次性全部向后移动一位(****)
    	*first = value;
    }
    else						// 尾元素大于头部
        __unguarded_linear_insert(last, value);
}

// 辅助函数
void __unguarded_linear_insert(iterator last, T value){
    iterator next = last;
    --next;
    // 内循环
    // 注意,一旦不再出现逆转对,循环就结束,因为之后的都必定不是逆转对了,也就是都是排好序的了
    while(value < *next){
        *last = *next;
        last = next;
        --next;
    }
    *last = value;
}

快速排序

快速排序是目前已知最快的排序法,平均复杂度尾O(NlogN),最坏情况为O(N2)。不过内省排序(IntroSort)(一种与median-of-three QuickSort极为类似的算法)可以将最坏情况推进到O(NlogN)

快速排序步骤为:

  • 如果S的元素个数为0或者1,结束
  • 取S中的任何一个元素,当作基准pivot(v)
  • 将S分割为L,R两端,使L内的每个元素都小于或者等于v,R内的每一个元素都大于或者等于v
  • 对L,R递归执行快排

分段的原则通常采用median-of-three,即取(首、尾,中央三值的中位数)

T& __median(const T& a, const T& b, const T& c){
	if(a < b)
        if(b < c)
            return b;
    	else if(a < c)
            return c;
    	else
            return a;
    else if(a < c)
        return a;
    else if(b < c)
        return c;
    else
        return b;
}

分割时使用两个迭代器,first向尾部移动,last向头部移动,当*first大于或者等于基准时就停下来,*last小于或者等于基准时也停下来,然后检验两个迭代器是否交错,如果first仍然在左,last仍然在右,就将两者交换,然后各自调整一个位置(向中间逼近),再继续进行相同的行为。如果发现两个迭代器交错了,表示已经调整完毕。

iterator __unguarded_partition(iterator first, iterator last, T pivot){
	while(true){
        while(*first < value) ++first;		// first找到>=pivot就停下来
        --last;
        while(pivot < *last) --last;
        if(first>=last)	return first;		// 交错,结束
        iter_swap(first, last);				// 大小值互换
        ++first;
    }	
}

内省排序

不当的基准值选择,可能会导致不当的分割,导致快排时间复杂度恶化为O(N2)。内省排序的行为再大多数情况下和median-of-three QuickSort一致,但是当分割行为有恶化为二次的倾向时,能够自我检测转而使用堆排。

void __introsort_loop(iterator first, iterator last, T*, size depth_limit){
	while(last-first > 16){
        if(depth_limit == 0){			// 超过深度限制就进行堆排
        	partial_sort(first, last, last);
            return;
        }
        --depth_limit;		// 分了一层了
        // 使用median-of-three quicksort进行分割
        iterator cut = __unguarded_partition(first, last, T(__median(*first, *(first+(last-first)/2), *(last-1))));
        // 对右半段递归进行sort
        __introsort_loop(cut, last, value_type(first), depth_limit);
        last = cut;
        // 开始新的循环对做半段进行sort
    }
}

void sort(iterator first, iterator last){
    if(first != last){
        __introsort_loop(first, last, value_type(first), __lg(last-first)*2);	// lg函数为找到2^k <= n的最大值k,控制层数
        __final_insertion_sort(first, last);	// 经过上一步之后,每个子序列都有相当程度的排序,但尚未完成排序,这里进行最后的处理
    }
}

void __final_insertion_sort(iterator first, iterator last){
    if(last - first > __stl_threshold){
        __insertion_sort(first, first+__stl_threshold);
        __unguarded_insertion_sort(first+__stl_threshold, last);		// 里面调用__unguarded_linear_insert进行排序
    }
    else
        __insertion_sort(first, last);
}

C++ 智能指针

包含在#include 头文件中,使用方法shared_ptr

shared_ptr

  • 多个智能指针可以共享同一个对象,对象的最后一个拥有者有责任销毁对象,并清理与该对象相关的所有资源。
  • 实现共享式拥有(shared ownship)概念。多个智能指针指向相同对象,该对象和其相关资源会在“最后一个reference”时被释放,为了在结构较复杂的情景中执行上述工作,标准库提供了weak_ptr, bad_weak_ptr, enable_shared_ptr等辅助类
  • 支持定制型删除器,可防范cross-dll问题(对象在一个dll中被new创建,但在另一个dll中被delete销毁),自动解除互斥锁

实现

sing namespace std;

template <typename T>
class Shared_ptr
{
private:
    size_t* m_count;
    T* m_ptr;

public:
    //构造函数
    Shared_ptr() : m_ptr(nullptr), m_count(new size_t)
    {}
    Shared_ptr( T* ptr ) : m_ptr(ptr), m_count(new size_t)
    {
        cout<<"空间申请:"<<ptr<<endl;
        *m_count = 1;
    }
    //析构函数
    ~Shared_ptr()
    {
        --(*m_count);
        if(*m_count == 0)
        {
            cout<<"空间释放:"<<m_ptr<<endl;
            delete m_ptr;
            delete m_count;
            m_ptr = nullptr;
            m_count = nullptr;
        }
    }

    //拷贝构造函数
    Shared_ptr( const Shared_ptr& ptr )
    {
        m_count = ptr.m_count;
        m_ptr = ptr.m_ptr;
        ++(*m_count);
    }

    //拷贝赋值运算符
    void operator=( const Shared_ptr& ptr )
    {
        Shared_ptr(std::move(ptr));
    }

    //移动构造函数
    Shared_ptr( Shared_ptr&& ptr ) : m_ptr(ptr.m_ptr), m_count(ptr.m_count)
    {
        ++(*m_count);
    }

    //移动赋值运算符
    void operator=( Shared_ptr&& ptr )
    {
        Shared_ptr(std::move(ptr));
    }

    //解引用运算符
    T& operator*()
    {
        return *m_ptr;
    }

    //箭头运算符
    T* operator->()
    {
        return m_ptr;
    }

    //重载布尔值操作
    operator bool()
    {
        return m_ptr == nullptr;
    }

    T* get()
    {
        return m_ptr;
    }

    size_t use_count()
    {
        return *m_count;
    }

    bool unique()
    {
        return *m_count == 1;
    }

    void swap( Shared_ptr& ptr )
    {
        std::swap(*this, ptr);
    }

};

shared_ptr实现原理

使用引用计数(reference counting),引用计数就是所有管理同一个裸指针(raw pointer)的shared_ptr,都共享一个引用计数器。

每当一个shared_ptr被赋值(或者拷贝构造)给其他的shared_ptr时,这个共享的应用计数器就加1;

当一个shared_ptr被析构或者被用于管理其他裸指针时,这个计数器就减1;

如果此时发现引用计数器为0,那么说明它是管理这个指针的最后一个shared_ptr了,于是释放指针指向的资源。

unique_ptr

  • 是一种在异常时可以避免资源泄漏的智能指针。采用独占式拥有,意味着可以确保一个对象何其相应的资源同一时间只被一个pointer拥有。一旦拥有者被销毁或变成empty,或者开始拥有另一个对象,先前拥有的那个对象就会被销毁,其任何相应资源亦会被释放。
  • 实现独占式拥有(exclusive ownership)或者严格拥有(strict ownership)概念,保证同一时间内只有一个智能指针可以指向该对象。对于避免内存泄漏(如new后忘记delete)特别有用。
  • 可以移交拥有权(具有转移语义),但是不会复制和共享(无法得到指向同一个对象的两个unique_ptr)
  • 用于取代auto_ptr

unique_ptr如何实现独占的

  • unique_ptr的构造函数被声明为explicit,禁止隐式类型转换的行为
  • unique_ptr的拷贝构造和拷贝赋值均被声明为delete,无法实现拷贝和赋值操作

weak_ptr

  • 允许共享但是不拥有某对象,一旦最末一个拥有该对象的智能指针失去了所有权,任何weak_ptr都会自动成空(empty)。
  • 在default和copy构造函数之外,weak_ptr只提供“接收一个shared_ptr”的构造函数
  • 可以打破环状引用(cycles of references,两个其实已经没有被使用的对象彼此互指,使之看似还在被使用的状态)的问题。

auto_ptr(已弃用)

  • 缺乏语言特性比如“针对构造和赋值”的move语义
  • 赋值和分配会改变资源的所有权,不符合人的直觉,即源指针必须将所有权赋予目标指针,存在潜在的内存崩溃问题
  • stl中无法使用auto_ptr,因为容器中的元素必须支持可复制和可赋值

unique_ptr和auto_ptr区别

  • auto_ptr可以复制拷贝,复制拷贝后的所有权转移;unique_ptr没有复制拷贝语义,但是实现了move语义,可以对所有权安全的转移。
  • auto_ptr不能用来管理数组(析构调用delete),unique_ptr可以用来管理数组(析构调用delete[])

智能指针的循环引用

class B;
class A{
public:
	shared_ptr<B> ptr;
};
class B{
public:
	shared_ptr<A> ptr;
};
int main(){
	while(true){
		shared_ptr<A> pa(new A());
		shared_ptr<B> pb(new B());
		pa->ptr = pb;
		pb->ptr = pa;
	}
	return 0;
}

上例中class A和class B的对象各自被两个智能指针管理,也就是A和B的引用计数都是2。当循环结束时,pa和pb的析构函数被调用,但是A对象和B对象仍然被一个智能指针管理,他们的引用计数变成1,于是这两个对象都无法释放,造成内存泄漏。

解决方法:将class A和class B中的shared_ptr改成weak_ptr即可,由于weak_ptr不会更改shared_ptr的引用计数,所以在pa和pb析构时,会正确的释放内存。

C++ 强制转换

static_cast

  • 用于比较自然且低风险的转换
  • 用于非多态类型的转换
  • 不执行运行时类型检查(安全性不如dynamic_cast)
  • 通常用于转换数据类型
  • 可以在整个类层次结构中移动指针,子类转换为父类安全(向上),父类转换为子类不安全;向上转换是一种隐式类型转换。
  • 不能用于不同类型指针之间的互相转换,不能用于整形和指针之间的转换,不能用于不同类型引用之间的转换

dynamic_cast

  • 用于多态类型的转换
  • 执行运行时类型检查
  • 只适用于指针和引用
  • 对不明确的指针的转换将失败(返回nullptr),但是不引发异常
  • 可以在整个类层次结构中移动指针,包括向上或者向下转换
  • 在进行引用的强制转换时,如果发现转换不安全,就会抛出一个异常,通过处理异常,就能发现不安全的转换。

const_cast

  • 用于删除const,volatile和__unaligned特性

reinterpret_cast

  • 用于位的简单重新解释
  • 滥用会带来风险,除非所需转换本身时低级别的,否则应使用其他类型
  • 允许将任何指针转换为任何其他指针类型(如char*到int* ,但是本身并不安全)
  • 允许将任何整数类型直接转换为任何指针类型以及反向转换
  • 不能丢掉const,volatile和__unaligned特性
  • 一个实际用途是在哈希函数中,即通过让两个不同的值几乎不以相同的索引结尾方式将值映射到索引(因为其允许将指针视为整数类型)。

bad_cast

  • 强制转换为引用类型失败,dynamic_cast运算符引发bad_cast异常。

C++ 面向对象

面向对象程序设计(Object-Oriented Programming, OOP)是种具有对象概念的程序编程典范,同时也是一种程序开发的抽象方针。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Odnx5PBa-1602215424643)(C:\Users\free\AppData\Roaming\Typora\typora-user-images\image-20200816092406117.png)]

面向对象三大特性:封装(Encapsulation),继承(Inheritance),多态(Polymorphism)

封装(Encapsulation)

  • 把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏
    • public成员:可以被任意实体访问
    • protected成员:只允许被子类和本类的成员函数访问
    • private成员:只允许被本类的成员函数、友元类或者友元函数访问

继承(Inheritance)

  • 基类(父类)——>派生类(子类)

多态

  • 多态,即多种状态(形态)。简单来说,我们可以将多态定义为消息以多种形式显示的能力
  • 多态是以封装和继承为基础的
  • C++ 多态分类及实现:
    • 静态多态(编译期)
      • 函数重载,运算符重载,类模板,函数模板
    • 动态多态(运行期)
      • 虚函数和继承
  • 为什么只有指针和引用能够完成多态?
    • 因为引用或者指针既可以指向基类对象,也可以指向派生类对象中的基类部分。

静态多态(编译期/早绑定)

  • 函数重载
class A{
public:
	void fun(int a);
	voif fun(int a, int b);
};

动态多态(运行期/晚绑定)

  • 把基类成员函数设置成虚函数,基类指针在绑定派生类的对象后就可以使用派生类覆盖过的方法。

  • 虚函数:使用virtual修饰成员函数,使其成为虚函数

    • 普通函数(非类成员函数)不能是虚函数
    • 静态函数(static)不能是虚函数
    • 构造函数不能是虚函数(因为在调用构造函数时,虚表指针并没有在对象的内存空间中,必须要构造函数调用完成才会形成虚表指针)
    • 内联函数不能是表现多态时的虚函数
  • 过程

    • 通过类的继承和虚函数机制在运行期间判断选择调用哪一个子类的对应函数(动态联编)举个例子:基类指针指向一个子类对象的时候,当父类调用的子类函数是虚函数的时候,就会调用子类重写过后的虚函数。

    • 隐藏变量__vfptr,类型为void **

      它应该指向一个void *的数组,这个void *的数组,就是我们的虚函数表。

虚析构函数

  • 析构函数可以是虚函数,是为了解决基类的指针指向派生类对象,并用基类的指针删除派生类对象(删除基类指针时会调用派生类析构函数,防止基类指针无法释放派生类内存资源)
  • 用来做基类的类的析构函数一般都是虚析构函

构造函数是否可以为虚函数

不可以,因为虚函数的调用需要调用vptr,而vptr是存放在对象内存中的,如果构造函数可以是虚函数那么构造函数的vptr是没有地方存放的。

虚函数,纯虚函数

  • 虚函数 类中如果声明了虚函数,那么这个函数是实现的,哪怕是空实现,他的作用就是为了能让这个函数在他的子类中可以被覆盖(override),这样就可以使用晚绑定实现多态了

  • 纯虚函数 一种特殊的虚函数,在基类中不能对虚函数做出有意义的实现,而把他声明为纯虚函数,他的实现留给该基类的派生类去做

virtual int A() = 0;
  • 纯虚函数是一个接口,是个函数的声明而已,要留到子类里面实现
  • 虚函数在子类里面可以不进行重写;纯虚函数必须在子类实现才可以实例化子类
  • 虚函数的类用于”继承“,继承接口的同时也继承了父类的实现;纯虚函数关注的是接口的统一性,实现由子类完成
  • 带纯虚函数的类被称为抽象类,这种类不能直接生成对象,而只能被被继承,并重写其虚函数后,才能使用。抽象类被继承后,子类可以继续是抽象类,也可以是普通类
  • 虚基类是虚继承中的基类

final和override

  • final:用于限制某个类不能被继承,或者某个虚函数不能被重写

    • final只能用来修饰虚函数,并且要放到类或者函数的后面。

    • class A{
      	virtual void foo() final;	// 限定foo()不能被重写
      	void bar() final;			// 错误,final只能修饰虚函数
      };
      class B final : A{				// 限定B不能被继承
      	void foo();					// 错误,foo()不能被重写
      };
      class C :B{};					// 错误,B不能被继承
      
  • override:确保派生类中声明的重写函数于基类的虚函数有相同的签名,同时也明确表名将会重写基类的虚函数,还可以防止因疏忽而把原来想重写基类的虚函数声明为重载。

  • 关键字要放在方法后面

构造函数可以是虚函数吗?

不能

  • 构造一个对象的时候,必须知道对象的实际类型,而虚函数行为是在运行期间确定实际类型的
  • 虚函数的执行依赖于虚函数表,而虚函数表在构造函数中进行初始化工作,即初始化vfptr,让他指向正确的虚函数表。

虚函数指针、虚函数表

  • 虚函数指针(Virtual Function Pointer, vfptr):在含有虚函数类的对象中,指向虚函数表,在运行时确定
  • 虚函数表(Virtual Function Table):在程序只读数据段(.rodata section)即内存常量区存放虚函数指针,如果派生类实现了基类的某个虚函数,则在虚表中覆盖原本基类的那个虚函数指针,在编译时根据类的声明创建
class base1{
public:
	int base1_1;
	int base1_2;
	virtual void base1_fun1(){}
	virtual void base1_fun2(){}
};
class derived1:public base1{
public:
	int derive1_1;
	int derive1_2;
};

隐藏变量__vfptr,类型为void **

它应该指向一个void *的数组,这个void *的数组,就是我们的虚函数表。

虚继承

  • 虚继承用于解决多继承条件下的菱形继承问题(浪费存储空间、存在二义性)
  • 底层实现原理与编译器相关,一般通过虚基类指针虚基类表实现,每个虚继承的子类都有一个虚基类指针(占用一个指针的存储空间,4字节)和虚基类表(不占用类对象的存储空间)(需要强调的是,虚基类依旧会在子类里面存在拷贝,只是仅仅最多存在一份而已,并不是不在子类里面了);当虚继承的子类被当做父类继承时,虚基类指针也会被继承
  • 实际上,vbptr 指的是虚基类表指针(virtual base table pointer),该指针指向了一个虚基类表(virtual table),虚表中记录了虚基类与本类的偏移地址;通过偏移地址,这样就找到了虚基类成员,而虚继承也不用像普通多继承那样维持着公共基类(虚基类)的两份同样的拷贝,节省了存储空间。

菱形继承

#include =
using namespace std;
class A
{
public:
	int a;
};
class B : public A
{
public:
	int b;
};
class C : public A
{
public:
	int c;
};
class D : public B, public C
{
public:
	int d;
};
int main()
{
	D d;
	d.a = 5;
	cout << d.a << endl;
	return 0;
}

编译一哈:

main.cpp(26): error C2385: 对“a”的访问不明确
main.cpp(26): note: 可能是“a”(位于基“A”中)
main.cpp(26): note: 也可能是“a”(位于基“A”中)
main.cpp(27): error C2385: 对“a”的访问不明确
main.cpp(27): note: 可能是“a”(位于基“A”中)
main.cpp(27): note: 也可能是“a”(位于基“A”中)

这种就叫菱形继承,D中将会有两份A的副本如图:

指的就是D这个类被实例化了之后,对象d想访问基类A的成员a的时候,竟然不知道应该是通过B来找还是通过C来找

后端面试知识点总结 C++基础知识_第1张图片

那这个时候我们该如何访问到经过B的A的成员a呢?代码2.2给出了一种解决方案,直接d.B::a :

cout << d.B::a << endl;

输出为:

5

对这两个a取地址:发现这两个地址是随机的,但是两个地址之间的地址位置差值是一样的

	cout << &d.B::a << endl;
	cout << &d.C::a << endl;

虚函数解决

在需要继承的基类前加virtual关键字修饰该基类,使其成为虚基类,见代码2.4:

#include 
using namespace std;
class A
{
public:
	int a;
};
class B : virtual public A
{
public:
	int b;
};
class C : virtual public A
{
public:
	int c;
};
class D : public B, public C
{
public:
	int d;
};
int main()
{
	D d;
        cout << &d.a << endl; 
	cout << &d.B::a << endl;
	cout << &d.C::a << endl;
	return 0;
}

我们可以发现,无论指不指定经过的类,a都只会在d中有一份副本了。

各种继承下的类大小

#include 
using namespace std;
class Base
{
private:
	int base;
public:
	Base() {}
	virtual ~Base() {}
	virtual void func() {}
};
class Dervied : public Base
{
private:
	int dervied;
public:
	Dervied() {}
	~Dervied() {}
};

int main()
{
	cout << sizeof(Base) << endl;
	cout << sizeof(Dervied) << endl;
	return 0;
}

输出为:

8
12

好,我们来看看8是怎么来的,很简单:sizeof(int) + sizeof(void **)

就是base变量和虚函数指针的长度之和。

那为什么Dervied是12呢?

因为Dervied本身还有一个dervied变量呀。

好,我们再来看看菱形继承的(见代码5.2):

#include 
using namespace std;
class Base
{
private:
	int base;
public:
	Base() {}
	virtual ~Base() {}
};
class Dervied1 : public Base
{
private:
	int dervied1;
public:
	Dervied1() {}
	virtual ~Dervied1() {}
};
class Dervied2 : public Base
{
private:
	int dervied2;
public:
	Dervied2() {}
	virtual ~Dervied2() {}
};
class Final : public Dervied1, public Dervied2
{
private:
	int final;
public:
	Final() {}
	~Final() {}
};

int main()
{
	cout << sizeof(Base) << endl;
	cout << sizeof(Dervied1) << endl;
	cout << sizeof(Dervied2) << endl;
	cout << sizeof(Final) << endl;
	return 0;
}

(注:在C++11中,final是一个关键字,可以用来指定一个类不可再被继承)

输出结果为:

8
12
12
28

为什么呢?

我们来分析一下:

Base的大小为sizeof(Base::base) + sizeof(Base::__vfptr) = 8

Dervied1的大小为sizeof(Dervied1::dervied1) + sizeof(Base) = 12

Dervied2的大小为sizeof(Dervied2::dervied2) +sizeof(Base) = 12

Final的大小为sizeof(Dervied1)+sizeof(Dervied2) + sizeof(Final::final) = 28

OK,这个好判别。

那虚继承呢?

我们把代码5.2稍微改一下,就可以得出代码5.3:

#include 
using namespace std;
class Base
{
private:
	int base;
public:
	Base() {}
	virtual ~Base() {}
};
class Dervied1 : virtual public Base
{
private:
	int dervied1;
public:
	Dervied1() {}
	virtual ~Dervied1() {}
};
class Dervied2 : virtual public Base
{
private:
	int dervied2;
public:
	Dervied2() {}
	virtual ~Dervied2() {}
};
class Final : public Dervied1, public Dervied2
{
private:
	int final;
public:
	Final() {}
	~Final() {}
};

int main()
{
	cout << sizeof(Base) << endl;
	cout << sizeof(Dervied1) << endl;
	cout << sizeof(Dervied2) << endl;
	cout << sizeof(Final) << endl;
	return 0;
}

输出为:

8
16
16
28

为啥?

Base就不用讲了,和上面是一样的。

那Derived1呢?16 = sizeof(Base) + sizeof(Dervied1::dervied1) +sizeof (Dervied1::__dervied1)

Dervied2同理

那为什么Final是28呢?

请记住,因为是虚继承,Final只会有一份Base的副本,所以大小就是:

sizeof(Base::base)+sizeof(Base::__vfptr)+sizeof(Dervied1::dervied1)+sizeof(Dervied1::__vfptr)+sizeof(Derived2::dervied2)+sizeof(Dervied2::__vfptr)+sizeof(Final::final) = 28

虚继承和虚函数区别

  • 相同之处:都使用了虚指针(均占用类的存储空间)和虚表(均不占用类的存储空间)
  • 不同之处:
    • 虚继承:
      • 虚基类依旧存在派生类中,只占用存储空间,只有一份。
      • 虚基类表存储的是虚基类相对直接继承类的偏移
    • 虚函数:
      • 虚函数不占用存储空间
      • 虚函数表存储的是虚函数地址

模板类、成员模板、虚函数

  • 模板类可以使用虚函数
  • 一个类(无论是普通类还是类模板)的成员模板(本身是模板的成员函数)不能是虚函数

抽象类、接口类、聚合类

  • 抽象类:含有纯虚函数的类
  • 接口类:仅含有纯虚函数的抽象类
  • 聚合类:用户可以直接访问其成员,并且具有特殊的初始化语法形式。满足如下特点:
    • 所有成员都是 public
    • 没有定义任何构造函数
    • 没有类内初始化
    • 没有基类,也没有 virtual 函数

C++ 内存管理

C++内存分配

  • 计算机中的程序内存分布图如下图所示
    后端面试知识点总结 C++基础知识_第2张图片

  • 栈(Stack):由编译器自动分配和释放,存放函数运行时分配的局部变量(包括局部常量,不可修改性由编译器保证,可以const_cast)、函数参数值、返回数据、返回地址等。操作类似于数据结构中的栈

  • 动态链接库:用于在程序运行期间加载和卸载动态链接库

  • 堆(Heap):一般由程序员分配,如果程序员没有释放,程序结束时可能由操作系统回收。malloc(),calloc(),free()操作的就是这块内存

  • 全局数据区(global data):存放**全局变量、静态变量(包括静态局部和静态全局)**等,这块内存有读写权限,因此他们的值在程序运行期间可以任意改变。程序结束之后由系统释放

  • 常量区(文字常量区):存放一般的常量、常量字符串,这块内存只有读权限,因此在运行期间不能改变。程序结束后由系统释放

  • 代码区:存放函数体的二进制代码

三种内存分配方式

  • 从全局数据区分配:内存在程序编译的时候已经分配好,这块内存在程序的整个运行期间都存在,例如全局变量,静态变量
  • 在栈上分配:在执行函数时,函数内局部变量的存储单元可以在栈上创建,函数执行结束后,这些内存单元会自动被释放;栈内存分配运算内置于处理器的指令集,效率高,但是分配的内存容量有限
  • 从堆上分配:动态内存分配,程序在运行的时候使用malloc或者new申请的内存,程序自行负责何时用free或者delete释放内存。在堆上申请空间,有责任回收它,否则会出现内存泄漏

堆和栈的区别

  • 碎片问题:堆,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低;栈没有这个问题
  • 生长方向:堆向上;栈向下
  • 分配方式:堆是动态分配的,没有静态分配的堆;栈有两种方式:静态分配由编译器完成,比如全局变量的分配,动态分配由alloca函数进行,但是栈的动态分配由编译器进行释放,无需手动操作
  • 分配效率:堆的分配是c/c++函数库提供的,机制很复杂,为了分配一块内存,库函数首先会搜索有没有足够大的空间,如果没有就调用系统功能增加程序数据段的内存空间然后返回;栈式机器系统提供的数据结构,计算机在底层对栈提供支持,比如分配专门的寄存去存放栈的地址,压栈和出栈都有专门的指令执行,所以栈的效率很高
  • 管理方式:堆由程序员手动申请和释放;栈由编译器进行管理
//下式表示new在堆上申请一块内存,然后在栈中存放一个指向这块堆内存的指针p
int* p = new int[6];

什么时候用堆,什么时候用栈

  • 与堆相比,栈不会导致内存碎片,分配效率高:如果少量数据需要频繁的操作,那么在程序中动态申请少量栈内存会获得很好的性能提升
  • 堆可以申请的内存大很多:与堆相比,栈的使用不是那么灵活,如果分配大量的内存空间,应当使用堆。

内存管理

  • malloc:申请指定字节数的内存。申请到的内存中的初始值不确定
  • calloc:为指定长度的对象,分配能容纳其指定个数的内存。申请到的内存的每一位(bit)都初始化为 0
  • realloc:更改以前分配的内存长度(增加或减少)。当增加长度时,可能需将以前分配区的内容移到另一个足够大的区域,而新增区域内的初始值则不确定
  • alloca:在栈上申请内存。程序在出栈的时候,会自动释放内存。但是需要注意的是,alloca不具可移植性, 而且在没有传统堆栈的机器上很难实现。alloca不宜使用在必须广泛移植的程序中

malloc和free

  • 用于分配、释放内存
//申请内存,并确认申请是否成功
char *str = (char*)malloc(100);
assert(str!=nullptr);
//释放内存并置空指针
free(p);
p = nullptr;

malloc和free底层实现

  • malloc内部实现:将可用的内存块连接为一个空闲链表,调用malloc的时候就遍历这个空闲链表找到一个合适的内存块,将内存块一分为二,大小合适的那一块返回给用户,剩下的内存块再连进空闲链表;free的时候再把释放的内存块连入空闲链表,如果有太多的小的内存分块,那么malloc函数会请求延时并对空闲链表进行整理,将相邻的空闲块合并成大的空闲块

new和delete

  • new/new[]:完成两件事,先底层调用malloc分配了内存,然后调用构造函数(创建对象)
  • delete/delete[]:也完成两件事,先调用析构函数(清理资源),然后底层调用free释放空间
  • new在申请内存时会自动计算所需字节数,而malloc则需要我们自己输入申请内存空间的字节数
T* t = new T();
delete t;

new和delete的底层实现

在底层通过调用operator new/delete全局函数来实现申请/释放空间。

new底层

operator new函数通过malloc来申请空间,当malloc申请空间成功时则直接返回;申请空间失败时抛出bad_alloc类型异常。

对于内置类型new:new和malloc基本类似;唯一不同的是new失败会抛异常,malloc失败会返回NULL

对于自定义类型new:调用operator new函数申请空间;在申请的空间上执行构造函数,完成对象的构造。

new[N]:调用operator new[]函数,在operator new[]函数中实际调用operator new函数完成N个对象空间的申请;在申请的空间上执行N次构造函数。

delete底层

operator delete函数通过free来释放空间。

对于内置类型delete:和free基本类似

对于自定义类型delete:在空间上执行析构函数,完成对象中资源的清理工作;调用operator delete函数释放对象的空间

对于自定义类型delete[]:在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理;调用operator delete[]释放空间,内部使用operator delete进行释放。

对于数组类型怎么删除的:需要在new[]的时候保存数组的维度,c++的做法是在分配数组空间时多分配4个字节大小的空间,专门保存数组的大小,在delete[]时就可以取出这个数,然后调用析构。

malloc和new、free和delete区别

  • 申请的位置不同:
    • malloc在堆上动态分配内存
    • new在自由存储区上为对象动态分配内存空间
  • 类型不同
    • malloc是一个函数,只能分配内置类型,不能调用构造和析构
    • new是操作符,可以为类分配内存空间
  • 类型检查
    • malloc不关心创建的对象类型,只负责开辟指定大小的内存然后把起始地址返回给调用者,调用者需要自己进行类型转化
    • new需要指定类型不需要指定大小,并会初始化。
  • 能否重载
    • malloc不能重载
    • new可以

定位new(placement new)

  • 允许向new传递额外的地址参数,从而在预先指定的内存区域创建对象
new (place_address) type
new (place_address) type (initializers)
new (place_address) type [size]
new (place_address) type [size] {braced initializer list}
  • place_address是一个指针
  • initializers提供一个(可能为空)以逗号分隔的初始值列表

delete this 合法吗?

  • 合法,但是
    • 必须保证 this 对象是通过 new(不是 new[]、不是 placement new、不是栈上、不是全局、不是其他对象成员)分配的
    • 必须保证调用 delete this 的成员函数是最后一个调用 this 的成员函数
    • 必须保证成员函数的 delete this 后面没有调用 this 了

如何定义一个只能在堆上(栈上)生成对象的类?

只能在堆上

  • 方法:将析构函数设置为私有
  • 原因:C++ 是静态绑定语言,编译器管理栈上对象的生命周期,编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性。若析构函数不可访问,则不能在栈上创建对象

只能在栈上

  • 方法:将new和delete设置为私有
  • 原因:在堆上生成对象,使用 new 关键词操作,其过程分为两阶段:第一阶段,使用 new 在堆上寻找可用内存,分配给对象;第二阶段,调用构造函数生成对象。将 new 操作设置为私有,那么第一阶段就无法完成,就不能够在堆上生成对象

内存池(Memory Pool)

应用程序可以通过系统的内存分配调用预先一次性申请适当大小的内存作为一个内存池,之后应用程序自己对内存的内配和释放可以通过这个内存池来完成。只有当内存池大小需要动态扩展时,才需要再调用系统的内存分配函数。

内存池可以分为单线程内存池和多线程内存池。也可以分为固定(每次分配的内存单元大小确认)内存池和可变(每次分配的大小可变)内存池。

默认内存管理函数的不足

使用默认的new/delete和malloc/free在堆上分配和释放内存会有一些额外的开销。应用程序频繁的在堆上分配和释放内存,就会导致性能的损失,并且会使系统中出现大量的内存碎片,降低内存的使用率。

内存池工作原理

固定内存池由一系列固定大小的内存块组成,每一个内存块又包含了固定数量和大小的内存单元

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YrHaOnNm-1602215424659)(C:/Users/free/Desktop/面试/interviewmd/assets/post/others/memorypool.png)]

在内存池初次生成时,只向系统申请了一个内存块,返回的指针作为整个内存池的头指针。之后随着应用程序对内存的不断需求,内存池判断需要动态扩大时,才再次向系统申请新的内存块,并把所有这些内存块通过指针链接起来

当应用程序需要通过该内存池分配一个单元大小的内存时,只需要简单遍历所有的内存池块头信息,快速定位到还有空闲单元的那个内存池块。然后根据该块的块头信息直接定位到第1个空闲的单元地址,把这个地址返回,并且标记下一个空闲单元即可;当应用程序释放某一个内存池单元时,直接在对应的内存池块头信息中标记该内存单元为空闲单元即可。

相当于系统管理内存相比,内存池主要由以下优点:

  • 针对特殊情况,例如需要频繁释放固定大小的内存对象时,不需要复杂的分配算法和多线程保护。也不需要维护内存空闲表的额外开销,从而获得较高的性能
  • 由于开辟一定数量的连续内存空间作为内存吃块,因而从一定程度上提高了程序局部性,提升了系统性能
  • 比较容易控制页边界对齐和内存字节对齐,没有内存碎片的问题

内存池实现

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Cd0Aq8hf-1602215424660)(C:/Users/free/Desktop/面试/interviewmd/assets/post/others/memorypoolstruct.png)]

运行机制

  • 在运行过程中,内存池可能会有多个内存块,这些内存块是从进程堆中开辟的一个较大的连续内存区域,有一个MemoryBlock结构体和多个可供分配的内存单元组成,所有内存块组成一个内存块链表,MemoryPool中的pBlock指针就是这个链表的头。每个内存块中的pNext指针指向下一个内存块。
  • 每个内存块由两部分组成,即一个MemoryBlock结构体和多个内存分配单元(固定大小nUnitSize),MemoryBlock只维护还未分配单元的信息。nFree记录内存块中还有多少单元没有分配,nFirst记录下一个可供分配的单元的编号,每个可分配单元的头两个字节记录了紧跟它之后的下一个自由分配单元的编号,这样利用每个自由分配单元的头两个字节,所有的自由分配单元被链接起来。
  • 当有新的内存请求,先遍历内存块链表,直到找到某个还有自由分配单元的内存块(nFree>0)。取得nFirst值,然后根据这个编号定位到该自由分配单元的起始位置(固定大小可以通过编号定位),将该位置起始2字节数据赋值为nFirst成员,同时nFree-1,将定位到的内存单元起始地址作为此次内存请求的返回地址返回。
  • 如果找不到自由分配单元(第一次请求内存或者内存分配单元都被分配时会发生),就从进程堆上申请一个新的内存块。然后对该内存块进行初始化
    • 设置MemoryBlock结构体的nSize为所有内存分配单元的大小
    • nFree为n-1,nFirst为1(因为单元0已经马上被分配出去)
    • 将编号0后面的所有自由分配单元连接起来。从aData作为第0个单元的地址开始,每个nUnitSize大小取其头两个字节,记录期之后的自由分配的编号。
    • 返回内存块的第9个分配单元的起始地址,即aData地址
  • 当某个被分配单元delete回收时,该单元并不会返回给堆,而是返回给MemoryPool。MemroyPool遍历其维护的内存块链表,判断该单元的起始地址是否在某个内存块中。如果在,就把该内存分配单元加到这个内存块的MemoryBlock维护的自由分配单元链表头部,同时nFree+1;如果不在,说明这个被回收的单元不属于这个MemoryPool。
  • 回收之后,如果内存块的所有内存分配单元都是自由的即nFree=n,那么这个内存块就会被移出MemoryPool并作为一个整体返回给进程堆;如果还有非自由分配单元,这时就把该内存块移动到MemoryPool内存块链表的头部,这样可以减少MemoryPool的遍历次数。

编译与链接

#include 
int main(){
	print("Hello world!");
	return 0;
}
  • 在Unix系统中,由编译器把源文件转换为目标文件
gcc -o hello hello.c
g++ -o hello hello.cpp

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6iym7Tc8-1602215424662)(C:\Users\free\AppData\Roaming\Typora\typora-user-images\image-20200818131431353.png)]

  • 预处理阶段:处理以#开头的预处理命令,生成.i/.ii文件

  • g++ -E hello.cpp -o hello.i
    
  • 编译阶段:编译器进行词法分析、语法分析、语义分析、中间代码生成、优化、目标代码生成等,生成.s汇编文件

  • g++ -S hello.cpp -o hello.s
    
  • 汇编阶段:汇编器将汇编码生成机器码,生成.o目标文件

  • g++ -c hello.cpp -o test.o
    
  • 链接:链接器进行地址和空间分配、符号决议、重定位等,生成.out可执行目标程序

  • ld -o hello.out hello.o ...libraries...
    

编译步骤

过程可以分为词法分析、语法分析、语义分析、中间代码生成、优化、目标代码生成等步骤

词法分析

词法分析器读入组成源程序的字符流,并将其组成有意义的词素的序列。形如这样的词法单元。(token-name是由语法分析使用的抽象符号,attribute-value是指向符号表中关于这个词法单元的条目,符号表条目的信息会被语义分析和代码生成步骤使用。)

比如赋值语句:position=initial+rate*60经过词法分析之后的到词法单元序列:<=><+><*><60>

语法分析

语法分析器使用由词法分析器生成的各词法单元的第一个分量来创建树形的中间表示。该中间表示给出了词法分析产生的词法单元的语法结构。常用的表示方法是语法树,树中每个内部节点表示一个运算,而该节点的子节点表示运算的分量。

以上赋值语句表示为语法树为:

语义分析

数据的含义就是语义,简单的来说,数据就是符号。数据本身没有任何意义,只有被赋予含义的数据才能被使用,这时候数据就转换为了信息,而数据的含义就是语义。

语法分析器使用语法树和符号表中的信息来检查源程序是否和语言定义的语义一致,他同时收集类型信息,并存放在语法树或者符号表中,以便在中间代码生成过程中使用。

语法分析一个重要部分就是类型转换,比如很多语言要求数组下标必须是整数,如果使用浮点数那就必须报错。

中间代码生成

在源程序翻译成目标代码的过程中,一个编译器可能构造出一个或者多个中间表示。这些中间表示可以由多种形式。语法树是一种中间表示形式,他们通常在语法分析和语义分析中使用。

在源程序的语法分析和语义分析完成之后,很多编译器生成一个明确的低级的或类机器语言的中间表示。该中间表示有两个重要的性质:1易于生成;2能够轻松的翻译为目标机器的语言。

代码优化

代码优化试图改进中间代码,以便生成更好的目标的代码,即更快更短或者能耗更低。

代码生成

代码生成以中间表示形式作为输入,并把它映射为目标语言。如果目标语言是及其代码,则必须为每个变量选择寄存器或者内存位置,中间指令则被翻译为能够完成相同任务的机器指令序列。

代码生成的一个至关重要的方面是合理分配寄存器以存放变量的值。

目标文件存储结构

  • File Header:文件头,描述整个文件的文件属性(包括文件是否可执行、是静态链接或动态连接及入口地址、目标硬件、目标操作系统等)
  • .text section:代码段,执行语句编译成的机器代码
  • .data section:数据段,已初始化的全局变量和局部静态变量
  • .bss section:BSS 段(Block Started by Symbol),未初始化的全局变量和局部静态变量(因为默认值为 0,所以只是在此预留位置,不占空间)
  • .rodate section:只读数据段,存放只读数据,一般是程序里面的只读变量(如 const 修饰的变量)和字符串常量
  • comment section:注释信息段,存放编译器版本信息
  • .note.GNU-stack section:堆栈提示段

静态链接

  • 静态链接器以一组可重定位目标文件为输入,生成一个完全链接的可执行目标文件作为输出。链接器主要完成以下两个任务:
    • 符号解析:每个符号对应于一个函数、一个全局变量或一个静态变量,符号解析的目的是将每个符号引用与一个符号定义关联起来
    • 重定位:链接器通过把每个符号定义与一个内存位置关联起来,然后修改所有对这些符号的引用,使得它们指向这个内存位置

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0z5ZmgLM-1602215424664)(C:\Users\free\AppData\Roaming\Typora\typora-user-images\image-20200816093642433.png)]

动态链接

  • 静态链接有以下两个问题:
    • 当静态库更新时那么整个程序都要重新进行链接;
    • 对于printf这种标准函数库,如果每个程序都要有代码,这会极大浪费资源。
  • 共享库是为了解决静态库的这两个问题而设计的,在 Linux 系统中通常用 .so 后缀来表示,Windows 系统上它们被称为 DLL。它具有以下特点:
    • 在给定的文件系统中一个库只有一个文件,所有引用该库的可执行目标文件都共享这个文件,它不会被复制到引用它的可执行文件中;
    • 在内存中,一个共享库的 .text 节(已编译程序的机器代码)的一个副本可以被不同的正在运行的进程共享。
    • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Sbxl5T7D-1602215424666)(C:\Users\free\AppData\Roaming\Typora\typora-user-images\image-20200816093702187.png)]

链接的接口-符号

  • 在链接中,目标文件之间相互拼合实际上是目标文件之间对地址的引用,即对函数和变量的地址的引用。函数和变量统称为符号(Symbol),函数名或变量名就是符号名(Symbol Name)。

CoreDump

Core Dump也叫做核心转储,当程序运行过程中发生异常,程序异常退出时,由操作系统把程序当前的内存状况存储到一个core文件中,叫做core dump。

使用ulimit -c unlimited可以开启核心转储功能。

使用gdb [exe_path] [core_path]打开core文件查看出错信息

函数的调用过程

使用到了三个常用的寄存器,EIP为指令指针,即指向下一条即将执行的指令的地址;EBP为基址地址,常用来指向栈底;ESP为栈顶指针,常用来指向栈顶。

  • 函数调用
    • 将调用函数的参数压入帧栈中,call指令跳转到子函数起始地址
  • 保存现场
    • 将call指令后一条指令的地址压入栈中,实际上就是把EIP指针入栈
    • EDP入栈,因为每个函数都有自己的栈区域,所以栈基址不是一样的,现在需要进入新函数,为了不覆盖调用函数的EDP,将它压入栈中。
    • ESP作为EBP,即将此时的栈顶地址作为该函数的栈基址
    • 再分别将EBX,ESI,EDI压入栈中,他们分别为基址寄存器,源变址寄存器,目的变址寄存器
  • 执行子函数
  • 恢复现场
    • 分别弹出EDI,ESI,EBX的值
    • EDP的值赋给ESP,弹出EBP的值
    • 执行call返回断点

临时变量作为函数返回值

函数可以使用临时变量作为函数返回值,但是将一个指向临时变量的指针作为函数的返回值是有问题的,因为临时变量在函数返回时会被销毁,所以指针也就指向了一块无意义的地址空间。

拷贝构造函数将临时变量拷贝到保存返回值的外部存储单元中,然后临时对象在函数结束时被销毁。

C++11并发多线程编程

c++11新标准线程库

  • 以往windows:CreateThread(),linux:pthread_create();不能跨平台

  • c++语言本身增加了多线程支持,可移植性(跨平台);

    • 库 #include

      • thread
        • 标准库里的类;myprint:是一个可调用对象
        • thread mytobj(myprint);//创建了线程,从myprint开始执行
      • join()
        • join阻塞,阻塞主线程,让主线程等待子线程执行完毕,子线程执行完毕后主线程再开始运行。如果主线程执行完毕但是子线程没执行完毕,报异常。
      • detach()分离
        • 传统多线程主线程需要等待子线程,然后自己退出;detach:主线程和子线程分离,主线程不必等
        • 为什么引入detach:创建很多子线程,让主线程逐个等待子线程结束,不太好,引入了detach
        • 一旦detach后,与这个主线程关联地thread对象就会失去与这个主线程的关联,此时这个子线程就会驻留在后台(守护进程)运行了,子线程相当于被系统接管了,子进程执行完毕后由运行时库负责清理该线程相关的资源。
      • joinable()
        • 判断是否已经join或者detach过

创建线程的不同手法

  • 用类以及一个问题范例

    class TA {
    public:
    	int &m_i;
    	TA(int &i) :m_i(i) {
    		cout << "ta的构造函数" << endl;
    	}
    
    	TA(const TA&ta) :m_i(ta.m_i)
    	{
    		cout << "ta的拷贝构造函数" << endl;
    	}
    	~TA()
    	{
    		cout << "ta的析构函数" << endl;
    	}
    	void operator()()
    	{
    // 		cout << "我的线程开始执行了" << endl;
    // 
    // 
    // 		cout << "我得线程执行完毕了" << endl;
    		cout << "m_i的值为" << m_i << endl;
    		cout << "m_i的值为" << m_i << endl;
    		cout << "m_i的值为" << m_i << endl;
    		cout << "m_i的值为" << m_i << endl;
    		cout << "m_i的值为" << m_i << endl;
    		cout << "m_i的值为" << m_i << endl;
    		cout << "m_i的值为" << m_i << endl;
    
    	}
    };
    
    int main()
    {
    		int myi = 7;
    		TA ta(myi);//传入的是引用,所以一旦主线程执行完毕之后回收内存,那么分离执行的子线程中就失去了myi的对象
    		thread mytobj2(ta);	
    	
    		mytobj2.join();
    		//mytobj2.detach();
    		cout << "i love china" << endl;
        	return 0;
    }
    
  • 用类成员函数

    class A
    {
    public:
    	int m_i;
    	//类型转换构造函数,可以直接传入一个整形构造
    	A(int a) :m_i(a) {
    		cout << "[A::A(int a)构造函数执行]" <<this<<"thread id="<<std::this_thread::get_id()<< endl;
    	}
    	A(const A & a) :m_i(a.m_i) {
    		cout << "[A::A(cosnt int a)构造函数执行]" <<this << "thread id=" << std::this_thread::get_id() << endl;
    	}
    	~A() {
    		cout << "[A::~A()析构函数执行]" <<this << "thread id=" << std::this_thread::get_id() << endl;
    	}
    
    	void thread_work(int num)
    	{
    		cout<<"子线程work函数执行"<<this<< "thread id=" << std::this_thread::get_id() << endl;
    	}
    };
    
    int main()
    {
    	A myobj(1);
    	std::thread mytobj(&A::thread_work,myobj,16);//&成员函数函数名,this指针,变量
    	mytobj.join();
    	cout << "i love china" << endl;
        return 0;
    }
    
  • 用lambda表达式

    int main()
    {
    	auto mylamthread = [] {
    		cout << "我的线程3开始了" << endl;
    		cout << "我的线程3结束了" << endl;
    	};
    	thread mytobj4(mylamthread);
    	mytobj4.join();
    
    	
    	cout << "i love china" << endl;
    
    	cout << "i love china" << endl;
    	cout << "i love china" << endl;
    	cout << "i love china" << endl;
    	cout << "i love china" << endl;
        return 0;
    }
    
    
  • 传递临时对象作为线程参数

    • 不用引用和指针传递,字符串指针会出现野指针,所以我们想将char[]隐式转换成string引用,但隐式类型转换对象在子线程中构造,会导致主线程中的对象已经被析构了子线程中的构造函数还没运行。
      • 事实:只要用临时构造的A类对象作为参数传递给线程,那么一定能够在主线程执行完毕前把线程参数构建出来从而确保即便detach了子线程也会安全运行。如果不传入临时对象,那么会出现
    • 总结
      • 若传递int这种简单的类型,直接值传递
      • 若传递类对象,避免隐式类型转换,全部都在创建线程的过程中就构建出临时对象来,然后在函数参数里用引用来接收,否则系统还会多构造一次对象。
  • 线程id

    • 线程id概念,每一个线程都对应这一个线程id;获取线程号:std::this_thread::get_id()
class A
{
public:
	int m_i;
	//类型转换构造函数,可以直接传入一个整形构造
	A(int a) :m_i(a) {
		cout << "[A::A(int a)构造函数执行]" <<this<<"thread id="<<std::this_thread::get_id()<< endl;
	}
	A(const A & a) :m_i(a.m_i) {
		cout << "[A::A(cosnt int a)构造函数执行]" <<this << "thread id=" << std::this_thread::get_id() << endl;
	}
	~A() {
		cout << "[A::~A()析构函数执行]" <<this << "thread id=" << std::this_thread::get_id() << endl;
	}
};

void myprint(const int i, const A & pmybug)
{
	cout << "son thread parameter add ="<<&pmybug << endl;//打印pmybug地址
	cout << "son thread id =" << std::this_thread::get_id() << endl;
	return;
}

int main()
{
	int mvar = 1;
	int mysecondpar = 12;
	cout << "father thread id="<<std::this_thread::get_id() << endl;
	thread mytobj(myprint, mvar, A(mysecondpar));//将mysecondpar转成A类型对象传递给myprint
												//在创建线程的同时临时构造对象的方法传递参数是可行的
	mytobj.join();
	cout << "i love china" << endl;


    return 0;
}

  • 向线程传递主线程中类对象,还可以修改主线程中的对象
    • 用std::ref(对象)
class A
{
public:
	int m_i;
	//类型转换构造函数,可以直接传入一个整形构造
	A(int a) :m_i(a) {
		cout << "[A::A(int a)构造函数执行]" <<this<<"thread id="<<std::this_thread::get_id()<< endl;
	}
	A(const A & a) :m_i(a.m_i) {
		cout << "[A::A(cosnt int a)构造函数执行]" <<this << "thread id=" << std::this_thread::get_id() << endl;
	}
	~A() {
		cout << "[A::~A()析构函数执行]" <<this << "thread id=" << std::this_thread::get_id() << endl;
	}
};

void myprint(const int i, A & pmybug)
{
	cout << "son thread parameter add ="<<&pmybug << endl;//打印pmybug地址
	cout << "son thread id =" << std::this_thread::get_id() << endl;
	pmybug.m_i = 199;
	return;
}

int main()
{
	int mvar = 1;
	int mysecondpar = 12;
	cout << "father thread id="<<std::this_thread::get_id() << endl;
	A myobj(10);
	thread mytobj(myprint, mvar, std::ref(myobj));//将mysecondpar转成A类型对象传递给myprint
												//在创建线程的同时临时构造对象的方法传递参数是可行的
	mytobj.join();
	cout << "i love china" << endl;


    return 0;
}

创建多个线程、数据共享问题

创建和等待多个线程

  • 创建十个线程,十个线程开始执行

    • a.多个线程执行的顺序是乱的,跟操作系统内部对线程的运行调度机制有关
    • b.主线程阻塞等待所有子线程运行结束后再结束
    • c.用容器管理大量线程
    int main()
    {
    		for (int i = 0; i < 10; i++)
    		{
    			mythreads.push_back(thread(myprint, i));//创建并开始执行线程
    		}
    		for (auto iter = mythreads.begin(); iter != mythreads.end(); ++iter)
    		{
    			iter->join();
    		}
    		cout << "i love china" << endl;
        return 0;
    }
    

数据共享问题分析

  • 只读数据是安全稳定的,不需要特别的处理手段

    vector<int>g_v = { 1,2,3 };//共享数据
    
    void myprint(int inum)
    {
    	cout << "id为" << std::this_thread::get_id() << "的线程 打印G_V值" << g_v[0] << g_v[1] << g_v[2] << endl;
    	return;
    }
    
    int main()
    {
    		for (int i = 0; i < 10; i++)
    		{
    			mythreads.push_back(thread(myprint, i));//创建并开始执行线程
    	
    		}
    	
    		for (auto iter = mythreads.begin(); iter != mythreads.end(); ++iter)
    		{
    			iter->join();
    		}
    	
    		cout << "i love china" << endl;
    
        return 0;
    }
    
  • 有读有写的数据:2个线程写,8个线程读,如果代码没有特别的处理,那程序肯定崩溃

    • 最简单的不崩溃处理,读的时候不写,写的时候不读
    • 写的步骤分10小步,由于任务切换导致各种诡异事件发生
  • 其他案例

    • 北京-深圳 火车 ,10个窗口卖票 ,其中两个窗口同时都要订99号座位

共享数据的保护代码案例

  • 网络游戏服务器。两个自己创建的线程
    • 一个线程收集玩家命令(用一个数字代表玩家发来的命令),并把命令数据写到一个队列中。
    • 一个线程从队列中去除玩家发送的命令,解析然后执行玩家需要的动作
    • 准备用成员函数作为线程函数
    • 代码解决问题:引入一个c++解决多线程保存共享数据问题的第一个概念”互斥量“
class A {
public:
	//把收到的消息入到一个队列的线程
	void inMsgRecvQueue()
	{
		for (int i = 0; i < 100000; i++)
		{
			cout << "inMsgRecvQueue,插入一个元素" << i << endl;
			msgRevcQuere.push_back(i);//假设数字i就是收到的命令,插入消息队列

		}
	}

	//把数据从消息队列中取走并处理的线程
	void outMsgRecvQueue()
	{
		for (int i = 0; i < 100000; i++)
		{
			if (!msgRevcQuere.empty())
			{
				//消息不为空
				int command = msgRevcQuere.front();//返回第一个元素
				msgRevcQuere.pop_front();//移除第一个元素,但不返回
				//处理数据
				//。。。。
			}
			else {
				cout << "outMsgRecvQueue执行,但目前消息队列为空" << i << endl;
			}
		}
		cout << "end" << endl;
	}

private:
	std::list<int>msgRevcQuere;
};

int main()
{
    A myobj;//创建对象
	std::thread myOutMsgobj(&A::outMsgRecvQueue, &myobj);//成员函数创建线程
	std::thread myInMsgobj(&A::inMsgRecvQueue, &myobj);

	myOutMsgobj.join();//阻塞
	myInMsgobj.join();

		return 0;
}

互斥量mutex

  • 互斥量基本概念

    • 互斥量就是一个类对象,理解成一把锁,多个线程尝试用lock()成员函数来加锁这把锁头,只有一个线程可以锁定成功(成功的标志是lock()函数返回了,如果没有成功,那么阻塞在lock()不断尝试加锁)
    • 互斥量使用要小心,保护数据不多也不少
  • 互斥量的用法

    • lock(),unlock()
      • 操作步骤:先lock()操作共享数据,再unlock();
      • 两者需要成对使用
      • 由lock忘记unlock的问题,有时候很难排查,为了防止大家忘记unlock(),引入了一个叫std::lock_guard的类模板:你忘了unlock不要紧,我替你unlock();
    class A {
    public:
    	//把收到的消息入到一个队列的线程
    	void inMsgRecvQueue()
    	{
    		for (int i = 0; i < 100000; i++)
    		{
    			cout << "inMsgRecvQueue,插入一个元素" << i << endl;
    			my_mutex.lock();//锁住写操作
    			msgRevcQuere.push_back(i);//假设数字i就是收到的命令,插入消息队列
    			my_mutex.unlock();
    		}
    	}
    
    	bool outMsgLULProc(int &command)//将读数据和删除数据提取到一个函数中去
    	{
    		my_mutex.lock();//lock
    		if (!msgRevcQuere.empty())
    		{
    			//消息不为空
    			command = msgRevcQuere.front();//返回第一个元素
    			msgRevcQuere.pop_front();//移除第一个元素,但不返回
    			my_mutex.unlock();							 //处理数据
    			return true;						 //。。。。
    		}
    		my_mutex.unlock();//每一个分支都需要unlock
    		return false;
    	}
    
    	//把数据从消息队列中去除的线程
    	void outMsgRecvQueue()
    	{
    		int command = 0;
    		for (int i = 0; i < 100000; i++)
    		{
    			bool result = outMsgLULProc(command);
    			if (result == true)
    			{
    				cout << "outMsgRecvQueue()执行,取出一个元素" << command << endl;
    				//可以考虑可疑进行命令处理
    			}
    			else {
    				cout << "outMsgRecvQueue执行,但目前消息队列为空" << i << endl;
    			}
    		}
    		cout << "end" << endl;
    	}
    
    private:
    	std::list<int>msgRevcQuere;
    	std::mutex my_mutex;
    };
    
    • std::lock_guard类模板:直接取代lock()和unlcok();也就是说,用了lock_guard后,可以不用unlock了

      • lock_guard再构造函数的时候调用lock(),在生命周期结束后析构函数调用unlock();
      bool outMsgLULProc(int &command)
      	{
      		std::lock_guard<std::mutex> sbguard(my_mutex);//lock_guard构造函数里执行了mutex::lock(),由于lock_gurad是一个局部对象,所以在函数结束的时候执行析构函数的时候调用mutex::unlock()
      		if (!msgRevcQuere.empty())
      		{
      			command = msgRevcQuere.front();//返回第一个元素
      			msgRevcQuere.pop_front();//移除第一个元素,但不返回
      			return true;						 //。。。。
      		}
      		return false;
      	}
      

死锁

  • 概念:有两把互斥锁(死锁产生的前提条件,至少两个互斥量):金锁(jinlock)银锁(yinlock)

    • //两个线程A,B
    • 1.线程A执行的时候,这个线程先锁金锁,成功,然后去lock银锁。。。这时候出现了线程切换,线程B开始执行,先锁银锁,成功,然后去lock金锁。
      此时此刻,死锁产生
    • 2.A拿不到银锁,B拿不到金锁,都阻塞,也不将自己的锁解开,产生死锁
  • 死锁演示

    class A {
    public:
    	void inMsgRecvQueue()
    	{
    		for (int i = 0; i < 100000; i++)
    		{
    			cout << "inMsgRecvQueue,插入一个元素" << i << endl;
    				my_mutex1.lock();//实际代码中,两个lock不一定挨着
    				my_mutex2.lock();
    				msgRevcQuere.push_back(i);//假设数字i就是收到的命令,插入消息队列
    				my_mutex2.unlock();
    				my_mutex1.unlock();
    			//...很长一段处理代码
    		}
    	}
    
    	bool outMsgLULProc(int &command)
    	{
    		my_mutex2.lock();
    		my_mutex1.lock();
    		if (!msgRevcQuere.empty())
    		{
    			command = msgRevcQuere.front();//返回第一个元素
    			msgRevcQuere.pop_front();//移除第一个元素,但不返回
    			my_mutex1.unlock();
    			my_mutex2.unlock();
    			return true;						 //。。。。
    		}
    		my_mutex1.unlock();
    		my_mutex2.unlock();
    		return false;
    	}
    	
    	void outMsgRecvQueue()
    	{
    		int command = 0;
    		for (int i = 0; i < 100000; i++)
    		{
    			bool result = outMsgLULProc(command);
    			if (result == true)
    			{
    				cout << "outMsgRecvQueue()执行,取出一个元素" << command << endl;
    				//可以考虑可疑进行命令处理
    			}
    			else {
    				cout << "outMsgRecvQueue执行,但目前消息队列为空" << i << endl;
    			}
    		}
    		cout << "end" << endl;
    	}
    
    private:
    	std::list<int>msgRevcQuere;
    	std::mutex my_mutex1;
    	std::mutex my_mutex2;
    };
    
    int main()
    {
        A myobj;
        std::thread myOutMsgobj(&A::outMsgRecvQueue,&myobj);
        std::thread myInMsgobj(&A::inMsgrecvQueue,&myobj);
        
        myOutMsgobj.join();
        myInMsgobj.join();
        
        reutrn 0;
    }
    
    • 死锁简单例子
    #include 
    #include //线程头文件
    #include 
    using namespace std;
    
    std::mutex lock1;
    std::mutex lock2;
    
    void mythread1(int mypar)
    {
    	for (int i = 0; i < mypar; i++)
    	{
    		lock1.lock();
    		lock2.lock();
    		cout <<"1:"<< i << endl;
    		lock1.unlock();
    		lock2.unlock();
    	}
    }
    
    void mythread2(int mypar)
    {
    	for (int i = 0; i < mypar; i++)
    	{
    		lock1.lock();
    		lock2.lock();
    		cout << "2:" << i << endl;
    		lock1.unlock();
    		lock2.unlock();
    	}
    }
    
    int main()
    {
    	std::thread t1(mythread1,10000);
    	std::thread t2(mythread2,10000);
    
    	t1.join();
    	t2.join();
    
    	return 0;
    }
    
    
  • 死锁的一般解决方案:

    • 只要保证两个互斥量上锁的顺序一致,就不会死锁
    • std::lock()函数模板
      • 能力:一次锁住两个或者两个以上的互斥量(至少两个,多个不行,1个不行);
      • 不存在因为锁头的顺序问题导致的死锁风险问题
      • 原理,std::lock():要么两个互斥量都缩住,要么两个互斥量都没锁柱,一旦有一个没锁住就会解锁另一个已经锁住的互斥量。
    		std::lock(my_mutex1, my_mutex2);
    // 		my_mutex2.lock();
    // 		my_mutex1.lock();
    
  • std::lock_guard的std::adopt_lock参数

    • adopt_lock是一个结构体对象,起标记作用,标记已经此锁已经lock
    std::lock(my_mutex1, my_mutex2);
    std::lock_guardsbgurad1(my_mutex1,std::adopt_lock); //用一个大括号包含需要加锁的代码段,提前结束lock_guard的生命周期
    std::lock_guardsbgurad2(my_mutex2,std::adopt_lock); 
    

unique取代lock_guard

  • unique_lock取代lock_guard

    • unique_lock是类模板,工作中一般用lock_guard(推荐使用);
    • unique_lock比lock_guard灵活很多
  • unique_lock第二个参数

    • std::adopt_lock:表视互斥量已经被lock了(必须把互斥量提前lock了),假设调用方已经lock成功才能使用
    • std::try_to_lock:尝试用mutex的lock去锁定这个mutex,但如果没有锁定成功,我也会立即返回,并不会阻塞在那里
    • std::defer_lock:不能提前lock,否则会报异常,初始化一个没有加锁的mutex
  • unique_lock的成员函数

    • lock(),加锁

    • unlock(),解锁

    • try_lock(),尝试给互斥量加锁,如果拿不到锁,则返回false,如果拿到了锁,返回true,这个函数不阻塞,可以执行其他任务

    • release(),返回它所管理的mutex对象指针,并释放所有权;也就是说,这个unique_lock和mutex不在有联系

      •   //不要和unlock混淆,unique_lock初始化时候绑定,用release释放绑定
        		//一旦释放绑定,如果mutex处于锁定状态,需要手动解锁
        		//有时候需要unlock(),因为lock锁住的代码段越少,执行越快,整个效率运行程序越高。
        
  • 所有权传递 std::unique_lockstd::mutexsbguard1(std::move(sbgurad));//移动语义

std::call_once

  • c++11引入的函数,该函数第二个参数是一个函数名
  • 功能是能够保证函数a只被调用一次。
  • 具备互斥量这种能力,而且效率上比互斥量消耗的资源更少;
  • 需要和一个标集结合使用,std::once_flag;
std::once_flag g_flag;//系统定义的标记

class MyCAS {

	static void CreateInstance()//只被调用一次的函数
	{
		m_instance = new MyCAS();
		static CGAEhuishou cl;
	}
private:
	MyCAS()
	{
	}

	static MyCAS * m_instance;//静态成员变量

public:
	static MyCAS *GetInstance()
	{
		std::call_once(g_flag, CreateInstance);//假设两个线程同时开始执行到这一行,其中一个线程要等另一个线程执行完createinstance后才能决定是否调用createinstance
		cout << "call_once执行完毕" << endl;
		return m_instance;
	}

	class CGAEhuishou {
	public:
		~CGAEhuishou()
		{
			if (MyCAS::m_instance)
			{
				delete MyCAS::m_instance;
				MyCAS::m_instance = NULL;
			}
		}
	};//类中套类,用来释放对象

	void func()
	{
		cout << "测试" << endl;
	}
};

条件变量

condition_variable、wait()、notify_one()

  • 实际上是一个类,等待一个条件达成,需要和互斥量来配合工作
  • wait()用来等一个东西,
    • 当其他线程用notify_one()将阻塞的wait叫醒后,wait就开始恢复干活了
    • wait()不断尝试获得互斥锁,如果获取不到,那么流程就卡住还等着获取锁 ,如果获取到那么wait继续执行b
    • 如果第二个参数lambda表达式返回的是true,那么wait直接返回,往下运行
    • 如果第二个参数lambda表达式返回的是false,那么wait()将解锁互斥量,并堵塞在本行,堵到其他某个线程调用notify_one()成员函数为止
  • notify_all()
    • 同时唤醒两个wait
#include "stdafx.h"
#include 
#include //线程头文件
#include 
#include 
#include 

using namespace std;

std::mutex resource_mutex;
std::once_flag g_flag;//系统定义的标记


class A {
public:
	//把收到的消息入到一个队列的线程
	void inMsgRecvQueue()
	{
		for (int i = 0; i < 100000; i++)
		{			
			std::unique_lock<std::mutex>sbguard(my_mutex);
			cout << "inMsgRecvQueue,插入一个元素" << i << endl;
			msgRevcQuere.push_back(i);//假设数字i就是收到的命令,插入消息队列
			my_cond.notify_all();//我们尝试把wait()线程唤醒,执行完这行,那么outMsgRecvQueue()里面的wait就会被唤醒
		}
	}
	//把数据从消息队列中去除的线程
	void outMsgRecvQueue()
	{
		int command = 0;
		while (true)
		{
			std::unique_lock<std::mutex> sbguard(my_mutex);
			my_cond.wait(sbguard, [this] {//一个lambda表达式就是一个可调用对象
				if (!msgRevcQuere.empty())
					return true;
				return false;
			});

			command = msgRevcQuere.front();
			msgRevcQuere.pop_front();
			cout << "outMsgRecvQueue()执行,取出一个元素" << command <<"thread id"<<std::this_thread::get_id() << endl;
			sbguard.unlock();//

		}
	}

private:
	std::list<int>msgRevcQuere;
	std::mutex my_mutex;
	std::condition_variable my_cond;//生成一个条件变量
};




int main()
{
	A myobj;//创建对象
	std::thread myOutMsgobj(&A::outMsgRecvQueue, &myobj);//成员函数创建线程
	std::thread myOutMsgobj2(&A::outMsgRecvQueue, &myobj);
	std::thread myInMsgobj(&A::inMsgRecvQueue, &myobj);

	myOutMsgobj.join();//阻塞
	myOutMsgobj2.join();
	myInMsgobj.join();

	return 0;
}

例子:轮流打印整数

//notify_one()(随机唤醒一个等待的线程)
//notify_all()(唤醒所有等待的线程)

#include 
#include 
#include 
#include 
using namespace std;

std::mutex data_mutex;//互斥锁
std::condition_variable data_var;//条件变量
bool flag = true;
void printfA() {
    int i = 1;
    while(i <= 100) {
        //休息1秒
        //std::this_thread::sleep_for(std::chrono::seconds(1));
        std::unique_lock<std::mutex> lck(data_mutex);
        data_var.wait(lck,[]{return flag;});//等待flag=true才打印奇数
        std::cout<<"A " << i <<endl;
        i += 2;
        flag = false;
        data_var.notify_all();
    }
}

void printfB() {
    int i = 2;
    while(i <= 100) {
        std::unique_lock<std::mutex> lck(data_mutex);
        data_var.wait(lck,[]{return !flag;});//等待flag=false才打印偶数
        std::cout<<"B " << i <<endl;
        i += 2;
        flag = true;
        data_var.notify_all();
    }
}
int main() {
    // freopen("in.txt","r",stdin);
    std::thread tA(printfA);
    std::thread tB(printfB);
    tA.join();
    tB.join();
    return 0;
}

你可能感兴趣的:(知识点总结,c++)