C++知识点整理

最近刚刚秋招面试完,发现自己很多C++基础知识忘记了,现在简单整理一下,方便自己以后查看。

文章目录

第一部分:基础知识

1、基本概念

  1. C语言:面向过程的程序设计:从上向下,逐步求精,按顺序一步一步解决问题
  2. C++:基于对象的程序设计 和 面向对象的程序设计
    把结构叫做类,定义一个对象(包括变量和方法)-----> 基于对象的程序设计
    继承和多态-----> 面向对象的程序设计
  3. C++的三大特性:封装,继承,多态
  4. 工程文件构成:
    源文件 .cpp .c .cxx(GNU) .cc(GNU) .m .mm (obj)
    头文件 .h .hpp 声明和实现都在里面,可减少编译次数

该类的调用者只需要include该.hpp文件即可,无需再将cpp加入到project中进行编译。而实现代码将直接编译到调用者的obj文件中,不再生成单独的obj,采用hpp将大幅度减少调用project中的cpp文件数与编译次数,也不用再发布lib与dll文件,因此非常适合用来编写公用的开源库。

  • .hpp是一般模板类的头文件。
  • 一般来说,.h里面只有声明,没有实现,而.hpp里声明实现都有,后者可以减少.cpp的数量。
  • .h里面可以有using namespace std,而.hpp里则无。
  • 不可包含全局对象和全局函数。
  1. 可移植性
    可执行文件跨平台不可用,源代码可移植
    g++编译的四个步骤:
    预处理:生成.i的文件
    编译:将预处理后的文件转换成汇编语言,生成.s文件
    汇编:变为目标代码(机器代码)生成.o的文件
    链接:连接目标代码,生成可执行程序
    在这里插入图片描述

2、inline

适用于函数体很小,需要频繁调用的函数

  1. inline影响编译器,在编译阶段对inline函数进行处理,将调用函数动作替换为函数本体,提升性能
  2. inline只是我们给编译器的建议,编译器可以做也可以不做,太长可能就不做了
  3. 内联函数放在头文件中,这样不同cpp都可以通过#include把这个内联函数源代码包含进来,以便找到函数本体源代码替换,其他函数这样做会报重定义错误,inline不会
  4. 类似于constexpr, 也要写的很简单,编译阶段直接求解,可看成更严格的inline函数
  5. #define A **, 最好用inline替换define
  6. 直接在类定义中实现的成员函数,被当成inline类型的
  7. 优缺点:可能代码膨胀,所以inline要尽可能小

3、引用与指针

都可以间接操控对象
1)指针定义时可不初始化,引用一定要初始化,且已经绑定,不可变更
2)指针是一个实体,而引用仅是个别名;
3)指针可以被重新赋值,指向另外一个对象;而引用总是指向(代表)它最初获得的那个对象。
4)“sizeof 引用”得到的是所指向的变量(对象)的大小,而“sizeof 指针”得到的是指针本身的大小;
5)指针可以有多级,但是引用只能是一级

4、可调用对象

定义:()是函数调用的标记,()叫做”函数调用运算符,像函数调用一样使用该对象

class A
{
public:
	//重载()
	int operator()(int v) const
	{
		if(v<0) return 0;
		return v;
	}
}

A a();  //构造函数
a(2);  //函数调用

1)函数指针
void (*pf)(int) = &func;
pf(3);
2)具有operator()成员函数的类对象
A a;
a(20);
3)类成员函数指针
A a;
void (A::*myfunc)(int) = &A::func;
a.*myfunc(12);

function可以绑定同一函数类型的可调用对象
#include <functional>

function<int(int)> f1 = func;  //函数指针, func不能有函数重载,可以通过函数指针解决
function<int(int)> f1 = a;     //类对象,因为()重载
function<int(int)> f1 = A();   //用类名生成一个对象,()重载
f1(5);
map<string, function<int(int)>> m = {{'a', func}, {'b', a}, {'c', A()}};
m['a'](12);

不同调用对象可以具有相同的函数类型: int(int)
函数指针:
int (*p)(int x);
p = finc;

5、lambda表达式

也是一种可调用对象,作为一种匿名函数,可以在函数内部定义

一、格式:
[捕捉列表](参数列表)->返回类型{函数体}auto f = [](int a)->int{
	return a+1;
};
cout << f(1) << endl;

返回类型后置语法,返回值特别明显时,返回值可以省略
auto f = [](int a){
	return a+1;
};
参数可以有默认值
auto f = [](int a=7){
	return a+1;
};
没有参数时,参数列表()可以省略,[]一定要时刻包含,调用方法和普通函数相同,函数体末尾分号不能省略;
auto f = []{};

二、捕获列表
a) [] 不捕获任何变量,不能捕获函数内的变量,但不包括静态局部变量
int i;
static int j = 9;
auto f = []{
	return i;  // 找不到i
	return j;   //可以找到局部静态变量
};
b) [&]捕获外部作用域所有变量,并作为引用在函数体内使用
int i=9;
auto f = [&]{
	i = 5;
	return i;  
};
//此时i = 5
c) [=]: 按值捕获外部作用域所有变量
int i=9;
auto f = [=]{
	//i = 5;    //非法,不能赋值
	return i;  
};
d) [this]: 一般用于类中,捕获当前类中this指针,让lambda表达式有类成员函数的权限
class A
{
public:
	void func(int x,int y)
	{
		auto f = [this]  //this可替换成= 或 &
		{
			return m_i;
		}
		cout << f() << endl;
	}	
	int m_i = 5;
}

A a;
a.func(2,3);

e)[变量名1,变量名2]:只捕获这些变量
[&变量名1&变量名2]:按引用捕获这些变量
class A
{
public:
	void func(int x,int y)
	{
		auto f = [this, x, y]  //this可替换成= 或 &
		{
			return m_i;
		}
		cout << f() << endl;
	}	
	int m_i = 5;
}

f)[=,&变量名]: 按值捕获所有外部变量,但按引用捕获所指变量

g)[&,变量名]: 按引用捕获所有外部变量,但按值捕获所指变量
总结:对可捕获变量的控制很细致

三、延迟调用易出错细节


int i=9;
auto f = [=]{    //i=9保存了个副本,后面i改变,这里的输入不变
	return i;  
};
i = 10;
cout << f() << endl;    //9
解决办法:引用捕获

四、lambda表达式中的mutable
int i=9;
auto f = [=](){  mutable
	//i = 5;    //非法,不能赋值
	i = 6; //加mutable就可以改了
	return i;  
};

五、std::function, std::bind
std::function<int(int)> fc = [](int c){return v;};
cout << fc(1) << endl;
std::function<int(int)> fc2 = std::bind( //bind第一个参数时函数指针,第二个参数开始就是真正的参数
	[](int c){return c;},
	16
);

六、不捕获任何变量的函数表达式,可以转换为普通函数指针
using functype = int(*)(int);
functype fp = [](int c){return c;};
cout << fp(19)<< endl;

6、auto

变量自动类型推断
1)推断发生在编译期,不会影响程序执行性能
2)定义时初始化
3) 引用、const属性会被丢弃
4)不能用于函数参数、普通成员变量
5)推数组: int[] --> int*
6) 推函数:
auto f = func; void(*)(double) 函数指针
auto &f = func; void(&)(double) 函数引用

7、decltype

作用:返回操作数的数据类型
1)decltype的自动类型推断发生在编译阶段
2) decltype不会真正计算表达式的值, 可以推导函数返回值类型

1)保留const 引用属性
const int i=0;
const int *iy = i;
auto j1 = i;  //int 引用属性和const属性都抛弃
decltype(i) j2 = 15; //const int,  const属性会返回
decltype(iy) j3 = j2; //const int &

decltype(CT::i) a;   //i的类型
decltype(CT) d; //CT

2decltype后的圆括号中非变量,时表达式
返回表达式的结果对应的类类型:如果表达式结果能作为赋值语句左边的值,decltype返回的就是引用
delcltype((变量))结果永远是引用
int* pi;
decltype(*pi) k2 = i;  //int&

3)括号内是函数,返回值类型,不需要调用函数
int func(){return 1;}
decltype(func()) v = 17;  //int
decltype(func) v;  //int(void), 代表一种可调用对象
function<decltype<func>> ftmp = func;

decltype(A().func()) aaa;  //int, func函数的返回类型,不够构建类A,也不用调用函数,就可以知道返回类型

using boost::typeindex::type_id_with_cvr;
cout << type_id_with_cvr<decltype(k2)>().pretty_name() << endl;   //输出k2类型是什么

用途:
1)主要用于模板编程中 
template <typename T> 
class CT
{
public:
	decltype(T().begin) iter; //T = const vector<int>---> vector<int>::const_iterator
	//T = vector<int> ---->vector<int>::iterator
}

2)通过变量表达式抽取变量类型
vector<int>::size_type mysize = myvector.size();
decltype(myvector>::size_type mysize = myvector.size();

3)函数后置返回类型语法
auto add(int i,int j)->decltype(i+j)
{
	return i + j;
}
template<typename T>
auto FuncTemp(T &tv) -> decltype(func(tv))
{
	return func(tv);
}
4)decltype(auto)用法
C++14
放在返回类型的位置,返回和输入类型完全相同的类型,auto去引用,不行,只能用decltype(auto)
template<typename T>
decltype(auto) FuncTemp(T &tv)
{
	return tv;
}
int a = 10;
decltype(FuncTemp(a)) b = a;  //b  int&

8、const char* , char const *, char * const

char str[] = "I love ";
const char* p = str; //p指向的内容不能通过p修改, char是固定的

char* const p = str; //p一旦指向的str,就不能指向其他内存

const char* const p = str; //p的指向不能变,指向的内容也不能通过p改变

第二部分:类

1, explicit关键字

对于单参数构造函数,一般都声明为explicit,禁止隐式类型转换初始化

Time A = {12,13,15}; //三参数构造和函数

Time B = 10; //单参数构造函数

构造函数:

explicit Time(int tmp);

2, 构造函数初始化列表:

比赋值效率更高,少了一个赋值和拷贝的过程,初始化顺序为头文件变量定义顺序,不是初始化列表写的顺序

3、直接在类定义中实现的成员函数,被当成inline类型的,能不能inline成功还是取决于系统

inline函数,多次包含头文件,不会报函数重定义的错误

4、成员函数末尾的const:常量成员函数

声明和定义都需要加const,告诉系统这个成员函数不更改类中的任何成员变量,即不更改类的任何状态

定义const对象:const Time abc;

abc只能调用const成员函数,不能调用普通非const成员函数

const修饰符不能放在普通函数后面

5、mutable(不稳定,容易改变):为了突破const的限制

mutable修饰成员变量,表示这个成员变量永远可以被修改,可以在const成员函数中修改这个变量

6、返回自身对象的引用 this

Time& Time::add_hour(int tmphour)

{
hour_ += tmphour;
return *this; //把对象自己返回了
}

编译器实际还传了一个this指针:
Time& Time::add_hour(Time*const this, int tmphour)
{
hour_ 实际是this->hour_
}

Time mytime;
mytime.add_hour(tmphour);
mytime.add_hour(tmphour).add_hour(tmphour).add_hour(tmphour); //返回自己,可以这样调用

全局函数、静态函数没有this指针,this指针只能在成员中使用;
Timeconst this,this只能只想当前对象,不能指向其他的;
在const成员函数中是const Time
const this,指向的对象内容也不能修改了

7、static成员和成员函数

static默认有初值,为0

普通局部变量加static:局部静态变量

全局变量加static:也在静态存储区,限制该全局变量作用域只能在本文件中

如果不加static,这个cpp可调用其他cpp的全局变量,只需extern 变量,就可以调用了

static在类中:

1)表明类的成员变量是属于整个类的,不属于对象,就是说如果我们在莫格对象中修改了这个成员变量,在其他对象中也能看到修改的结果。所有对象共享同一个副本,这种变量的引用 类名::成员变量名

2)成员函数+static,调用类名::成员函数, 静态成员函数只能操控静态成员变量

静态成员变量声明时还没分配内存,定义时才分配内存,一般在某一个.cpp源文件的开头来定义这个静态成员变量,这样就能保证在调用任何函数前这个变量都有值

Time.h中定义 static int mystatic;

例如在main.cpp中

int Time::mystatic = 15; //静态成员变量定义,定义时不需要static,只能在一个cpp中定义

静态成员函数实现时不加static,里面不能使用非static的成员变量

8、类相关非成员函数

Time.cpp中非time成员的普通函数
void A(Time tmp)
{
}
Time.h中声明
Class Time
{
};
void A(Time tmp);
main.cpp 中使用
Time m;
A(m); //直接调用

9、变量类内初始化

time.h
int var = {0}; //一般不这么写,不规范
const成员变量初始化:
cosnt int ctest = 0;
否则,一定要在构造函数初始化列表写const值初始化
不能通过赋值语句初始化

10、=default,=delete

time.h中: Time()=default; //编译器为我们自动生成函数体
析构、拷贝构造等可以用default
但说实话自己写也不麻烦
Time()=delete;,禁止系统生成合成的默认构造函数

11、拷贝构造函数和赋值构造函数、析构函数

对象不存在,且没用别的对象来初始化,就是调用了构造函数;
对象不存在,且用别的对象来初始化,就是拷贝构造函数(上面说了三种用它的情况!)
对象存在,用别的对象来给它赋值,就是赋值函数。

对象没有初始化过,通过另外一个对象给这个对象初始化,不要声明为explicit
调用拷贝构造函数主要有以下场景:
对象作为函数的参数,以值传递的方式传给函数。 
对象作为函数的返回值,以值的方式从函数返回
使用一个对象给另一个对象初始化

Time A;
Time A2 = A;
Time A3(A);Time A3{A};Time A3 = {A};

赋值构造函数,当前类A已经初始化,另一个B会覆盖现有的值,调用赋值构造函数
Time A,C;
Time B; //默认构造
B = A;   //赋值构造
TIme C = A;
B = C;   //赋值构造
Class String
{
public:
	String(constchar*ch=NULL);//默认构造函数
	String(constString&str);//拷贝构造函数
	~String(void);
	String&operator=(constString&str);//赋值函数
private:
	char*m_data;
};
类String 的拷贝构造函数与赋值函数
  // 拷贝构造函数
  String::String(const String &other)
  {
	  // 允许操作other 的私有成员m_data
	  int length = strlen(other.m_data);
	  m_data = new char[length+1];
	  strcpy(m_data, other.m_data);
  }
  // 赋值函数
  String & String::operator =(const String &other)
  {
	  // (1) 检查自赋值
	  if(this == &other)
	  	return *this;
	  // (2) 释放原有的内存资源
	  delete [] m_data;
	  // (3)分配新的内存资源,并复制内容
	  int length = strlen(other.m_data);
	  m_data = new char[length+1];
	  strcpy(m_data, other.m_data);
	  //memcpy(m_data,other.m_data,(length+1) *sizeof (char ));  
	  // (4)返回本对象的引用
	  return *this;
  }
  

析构函数不能被重载,无参无返回值,new delete对应,对象销毁时调用,默认里面{}
char* p = new char[100];
析构里delete []p;
先定义的先初始化,销毁时后销毁

12、派生类构造函数调用顺序

class 子类名:public 父类名
{};
new 子类; //定义子类对象时,先调用父类构造函数,再调用子类构造函数
继承方式

13、overwrite、overload、override

Overwrite(重写):是指派生类的函数屏蔽了与其同名的基类函数,规则如下:

  • 如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。
  • 如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。
    1)在子类成员函数中,用“父类::函数名”强制调用父类函数
    2)using 父类::函数名,public,protected在子类中都可见
    父类同名函数在子类中以重载方式来使用,类型个数总有一个不同才叫重载
    Overload 重载
    在C++程序中,可以将语义、功能相似的几个函数用同一个名字表示,但参数不同(包括类型、顺序不同),即函数重载。
    (1)相同的范围(在同一个类中);
    (2)函数名字相同;
    (3)参数不同;
    请注意,重载解析中不考虑返回类型
    Override(覆盖):是指派生类函数覆盖基类函数,特征是:
    (1)不同的范围(分别位于派生类与基类);
    (2)函数名字相同;
    (3)参数相同;
    (4)基类函数必须有virtual 关键字。

14、基类指针->虚函数->override关键字/final关键字

父类指针可以new子类对象
human* phuman = new Men;
phuman->父类成员函数(); //ok
phuman->子类成员函数(); //no
但不能调用子类成员函数

需求:希望只定义一个父类对象指针,就能调用父类、子类的同名函数
解决:虚函数 override
human* phuman = new Men;
phuman->子类成员函数eat();//men的eat
phuman->human::eat(); //也可以调用父类的函数,加上父类::即可
delete phuman ;
phuman = new Women;
phuman->子类成员函数eat();//women的eat
eat()父类中成名为virtual,子类重新实现这个函数,子类中可以不标注virtual,最好标上,如果子类没实现,默认调用父类的eat(),
一旦eat在父类中声明为virtual,所有派生类eat默认都是virtual

override关键字:虚函数专用,派生类中用了这个关键字,编译器就认为这个eat就是覆盖了父类中的同名函数,那么编译器就会在父类中找同名同参的虚函数,如果没找到,编译器会报错。如果不小心写错了,编译器会发现。
men.h
virtual int eat() overrride;

final:final也是虚函数专用,使用在“父类”,声明为final,子类中不能覆盖
父类.h
virtual int eat() final;

虚函数执行的是 动态绑定 ,在程序运行时才知道调用了哪个子类eat()虚函数,一定要用指针来操作,用对象操作在编译就知道了,Men man;men.eat(); 没意义

15、多态性

动多态:通过父类指针,只有到运行时,找到动态绑定到父类指针上的对象,这个对象可能时父类可能是子类,运行时期的多态性
静多态:模板

16、纯虚函数

纯虚函数,在基类中不定义,但要求任何派生类中都必须定义其实现
基类不能实例化,不能定义对象,抽象类:主要用于用于统一管理子类对象,子类没实现,也会变成纯虚函数,不能实例了
基类:
virtual int eat() = 0;

17、虚函数的构造函数、析构函数

析构不定义为virtual
只执行父类析构,不执行子类析构,会导致子类内存泄漏
析构定义为virtual:先执行子类析构、再调用父类析构函数
构造函数不能为虚:默认就是先调用父类,再调用子类构造

虚函数会增加内存开销,类里定义虚函数,编译器会给这个类增加虚函数表,表里存放虚函数指针

18、友元

友元函数:
类中:
friend 函数名func()
func()被声明为类的友元,func中可使用类中的private、protected函数和变量
友元类:
你是我的友元类,你可以在成员函数中访问我的所有成员

//class C;   //声明:C在其他地方有定义
#include "C.h"
class A
{
private:
	data;
	//友元类
	friend class C;   
	//友元成员函数
	friend void C::func(A& a);
};

class C{
public: 
	void func(A& a)
	{
		a.data = 1;
	}
};

1)友元不能被继承
2)友元没有传递性
3)友元关系是单向的

友元成员函数:
写的时候注意代码组织结构
公有的成员函数才能称为其他类的友元函数

友元优点:允许在某些情况下访问private、protected,更灵活
缺点:破坏封装性、降低了类的可靠性和可维护性

19、RTTI、dynamic_cast、typeid

RTTI:运行时类型识别,程序能够使用基类的指针或引用来检查这些所指的对象实际的派生类型,可以看成系统提供给我们的一种能力,通过两个运算符来体现:
dynamic_cast:能够将基类的指针或引用安全的转换为派生类的指针或引用
typeid: 返回指针所指对象的实际类型
要想让这两个运算生效,基类中至少有一个虚函数,才有虚函数表,这两个运算符才有用

父类指针指向子类对象,却不能调用子类非虚函数,这时用虚函数来解决不好,最好把指针转换成子类类型,这就需要dynamic_cast

Human *phuman = new Men;
Men *p = (Men*)(phuman);  //强转,不安全
WoMen *p = (WoMen*)(phuman);  //强转,不安全,也能成功
//所以要用dynamic_cast,如果能转成功,说明实际就是,可以做安全检查
Men *p = dynamic_cast<Men*>(phuman );
if(p!=nullptr)
{
	cout <<"是men类型“;
}else{
	cout <<"不是men类型“;
}

对于引用,如果转换失败,系统会抛出std::bag_cast异常

Human &phuman = new Men;
//所以要用dynamic_cast,如果能转成功,说明实际就是,可以做安全检查
try{
	Men &p = dynamic_cast<Men&>(phuman );
}
catch(std::bad_cast)
	cout <<"不是men类型“;

typeid运算符
拿到对象信息,返回常量对象的引用 type_info
const type_info &tp = typeid(*phuman);
typeid(*phuman).name()
typeid(&phuman).name()
typeid(19.6).name()
int a;
typeid(a).name()

typeid主要用来判断两个指针是否指向同一类型的对象

20、虚函数表

类里有虚函数,编译器就会对改类生成虚函数表
虚函数表里有很多项,每一项都是一个指针,每个指针指向这个类里各个虚函数的入口地址;
虚函数表项里,第一个表项很特殊,不是指向虚函数的入口地址,而是这个类所关联的type_info对象
Human *phuman = new Men;
phuman对象里有一个我们看不见的虚函数指针,这个指针指向的是这个对象所在的类Men里的虚函数表
虚函数表在构造函数中生成,先调用父类构造生成父类虚函数表,再调用子类构造生成子类虚函数表,子类虚函数表是再父类表上覆盖新增一些项
下面的例子,base是基类:有虚函数g,h,f
Derive是派生类,有虚函数f,g1,hi
base *p = new Derive();
p指向对象derive的虚函数表就是下面这样的,前面还有一个type_info没有画出来Base

21 基类与派生类的关系

1)派生类对象简述
包含派生类成员变量、函数
包含派生类所继承的基类子对象,子对象中包含基类的成员变量成员函数(所以基类指针可以new派生类对象,把派生类对象当成基类对象用,编译器帮我们做了隐式类型转换)
2)派生类构造函数:
通过子类初始化列表给父类传递参数
B(int i, int j , int k ) : A(i,j), m_valueB(k){};
先执行父类构造函数、再子类构造函数,释放时相反
派生类名::派生类名(参数总表)
:基类名(参数表),内嵌子对象(参数表)
{
派生类新增成员的初始化语句; //也可出现地参数列表中
}
派生类构造函数执行的次序:
基类–>成员类–>子类
3)不想当基类的类
类名加final
class A final{
};

4)静态类型与动态类型
Human *phuman = new Men();
静态类型:变量声明时的类型,编译时已知
动态类型:这个指针或引用所代表的内存中的对象类型,Men,在运行时才知道

5)复制兼容
赋值兼容规则中所指的替代包括以下的情况:
派生类的对象可以赋值给基类对象。 //调用基类的拷贝构造或赋值运算
派生类的对象可以初始化基类的引用。
派生类对象的地址可以赋给指向基类的指针。
在替代之后,派生类对象就可以作为基类的对象使用,但只能使用从基类继承的成员。

22、左值和右值

左值:能用在赋值等号左侧的东西,额能够代表一个地址,一块内存区域
右值:不能作为左值的就是右值

左值:
(a=4=8;
&a;
iter++;//迭代器是左值
//看一个运算符再字面上能不能操作i++,i是左值

引用分类:
1)左值引用:绑定到左值
2)const引用:左值引用的一种
const int &ref = value;
3)右值引用:绑定到右值
对象侧重于临时变量
int && ref = 3; //绑定到常量
ref = 5;

const引用可以绑定到右值
const int &c = 1;//生成一个临时变量
能绑定到左值上的引用都绑定不到右值
int &&r = i*100;
--i:左值    (--i) = 100;效率更高
i--:右值    //先产生一个临时变量tmpi,给i+1,返回这个临时变量

右值引用的目的:提高程序运行效率,把拷贝对象变成移动对象(移动构造函数)
e.g. A要拷给B,A不需要了,直接把A的内存块转给B,不是拷贝一遍(B分配内存,A拷B,释放A)
移动对象如何发生:需要的参数是&&右值引用

4)std::move
把左值强制转换为右值,就可以绑到右值引用了
unique智能指针,就是这样用的

int i = 10;
int **ri = std::move(i);
i = 20;
ri = 15;

string st = "i love";
string def = std::move(st);
//st为”“, def为i love,触发了string的移动构造函数,把st的内容转到了def
string &&def = std::move(st);
//st里的内容不变,因为不会触发移动构造函数,只是把st和def绑到一起
st = "abd";  //后面建议就不使用st了,这种做法不好

23、临时对象

临时对向,很快就释放掉的对象
希望有效减少临时对象的产生,i++是很难避免的临时对象,下面介绍怎么避免因书写而产生的临时对象怎么解决
1)以传值方式给函数传参
通过传递常量引用代替传值,减少了一次临时对象的创建(类的话就会调用一次拷贝构造函数)。这个改动也许很小,但对多继承的对象来说在构建时要递归调用所有基类的构造函数,这对于性能来说是个很大的消耗。
2)类型转换生成的临时对象,隐式类型转换以保证函数调用成功

Time A;
A = 1000//这里产生了一个临时对象,1000调用构造函数生成一个临时类对象
/**解决方法**/
Time A = 1000//定义时初始化,定义A对象,系统为A预留了空间,1000在A的预留空间里构造,节省了空间

//C++只会为const 引用(const string& str)产生临时变量,不会为非const引用产生临时变量,就不允许隐式类型转换了

3)函数返回对象的时候
当函数需要返回一个对象,它会在栈中创建一个临时对象,存储函数的返回值。

TempObj tm(10), sum;
sum = Double(tm);

Time Double(Time& tm)
{
	return Time(tm);
}
/**解决**/
TempObj tm(10);
TempObj sum = Double(tm);
TempObj &&sum = Double(tm);  //或者用右值来接


//类外运算符重载
num operator+(num &num1, num& num2){
	return num(num1.n1+num2.n1, num1.n2+num2.n2);
	//而不是
	/*
	num res;
	res.n1 = ...;
	res.n2 = ...;
	return res;
	/*
}

24、移动构造函数、移动赋值运算符

移动:所有权转移,临时对象马上要销毁了,保存有用的数据(内存保存)供后续使用,移动到其他变量,一块内存的所有者变更
拷贝构造:Time::Time(const Time &tmp){}
移动构造:Time::Time(const Time &&tmp){}
//移动构造的输入是马上要释放的临时对象,调用之后这个对象就不能用了
移动构造函数演示

class A
{
public:
	//构造
	A():m_pb(new B())
	{}
	//拷贝构造
	A(const A& tmpA):m_pb(new B(*(tmpA.m_pb))
	{}
	//移动构造:指针转移和打断
	//noexcept :通知标准库,我们的移动构造函数不抛出异常,可以提高编译器工作效率
	A(const A&& tmpA) noexcept :m_pb(tmpA.m_pb)  //当前对象指向老对象的内存
	{
		tmpA.m_pb = nullptr;  //打断老对象的指向,这样在tmpA析构的时候就不会释放这段内存空间
	}
	//赋值运算
	A& operator=(const A&  src)
	{
		if(this == &src)
			return *this;
		delete m_pb;
		m_pb = new B(*(src.m_pb));
		return *this;
	}
	//移动赋值运算
	A& operator=(const A&& src) noexcept
	{
		if(this == &src)
			return *this;
		delete m_pb;           //先删除自己的
		m_pb = src.m_pb;       //直接拿过来使用
		src.m_pb = nullptr;   //斩断源头
		return *this;
	}
private:
	B* m_pb;
};

A a = getA();
A a1(a);   //正常调用构造函数
//如果希望a的对象移动给a1,a以后不再用了,就可以写一个移动拷贝构造
A a2 = std::move(a);   //调用移动构造函数
A &&a2 = std::move(a);   //不会产生新对象,不会调用移动构造函数,只是定义一个引用而已
A b = getA();
A a3;   //构造函数
a3 = std::move(b);  //移动赋值运算

合成的移动操作(系统默认的)
1)有自己的拷贝构造或者析构,编译器就没有默认的移动构造
2)如果没定义移动构造,系统会调用默认拷贝构造
3)自己什么都没写,有默认的
总结:
1)分配大量内存的类,尽量给类增加移动构造和移动赋值
2)尽量加上noexcept
3)输入变量的内存需要手动置空,写法注意一下
4)没有移动,拷贝代替

25、继承的构造函数、多继承

一个类只继承直接基类的构造函数,默认、拷贝、移动构造函数是不能被继承的
class B:public A
{
using A::A; //继承A的构造函数,把基类的构造函数生成对应的派生类构造函数;
//相当于生成了这样一个构造函数
//B(int i, int j, int k):A(i,jk){}
}

1)多重继承,父亲中函数重名,增加作用域
ctest.A::info();
自己的类中定义了info,遮蔽父类的同名函数
2)静态成员变量在继承中的调用
A继承grand
C继承A、B
C ctest;
grand有静态成员g_static;
grand::g_static = 1;
A::g_static = 1;
C::g_static = 1;
ctest::g_static = 3;
都是可以的
3)派生类构造函数与析构函数
派生类构造函数初始化列表只初始化他的直接基类,每个类都这样,传递下来就都能初始化
先初始化grand,A,B,C
析构正好相反
4)从多个父类继承构造函数

class A{
A(int m){};
}
class B{
B(int m){};
}
class C:public A, public B{
using A::A;   //继承A的构造函数,C(int m):A(m){}
using B::B;   //继承B的构造函数,C(int m):B(m){} 
//会报错,和上面的函数重名了
//如果一个类从基类中继承了相同的构造函数,C必须定义自己的版本
C(int m):A(m),B(m){};  //这样才可以
}

5)多继承的类型转换
多继承类型转换,这些都是成立的

Grand *pg = new C(1,2,3);
A * pa = new C(1,2,3);
B *pb = new C(2,3,4);
C myc(2,3,4);
Grand mygrand(myc);

26、虚继承

虚继承
问题提出:上面的情况,C继承了两次grand,命名冲突、变量冗余;
虚基类:无论这个类在继承体系中出现多少次,派生类中,都只包含一个共享的虚基类子内容

  • 这种虚继承只对C有意义,对A、A2没有意义

  • 每个Grand的子类都要虚继承Grand类,才能保证Grand的孙类能够虚继承Grand

A:virtual public Grand{}
A2:  virtual public Grand{}
C:public A,public A2,public B{}
C构造函数要改一下,C初始化Grand,A和A2对Grand的初始化没有意义了,但A里还是要写
C(int i, int j, int k):A(i),A2(i),  Grand(i){}
  • 虚基类Grand是由最底层对象来初始化,这里是C
  • 有虚基类,虚基类最先初始化,再按照继承列表初始化

总结:不太提倡多重继承,容易出现二义性

27、类型转换运算符

class A{
explicit operator int() const    //显示类型转换运算符
{
	return m_i;
}
//类型转换运算符,可以把本类对象转成其他类型
operator int() const
{
	return m_i;
}
int m_i;
}

/**main.cpp**/
A a;
int k = static_cast<int>(a) + 1;  //显示类型转换运算符
int k = a + 5;  //类型转换运算符

建议一个类中只有一个类型转换运算符

28、类成员函数指针

1)对于普通成员函数
2) 虚函数
3)静态成员函数
类名::*函数指针变量名

class CT
{
public:
	void func(int a){}
	virtual void vfunc(int a){}
	static void sfunc(int a){}
	int m_a;
	static int m_static ;
};
---------普通函数指针-----------
void (CT::*fpointpt)(int);    //类成员函数指针定义,变量名fpointpt
fpointpt = &CT::func;   //指针被赋值
注意:TODO:成员函数属于类,不属于对象,没有对象也可以定义这个指针,但是如果要使用这个指针,需要绑定到对象上
//使用
CT ct, *pct;
pct = &ct;
(ct.*fpointpt)(100);  //调用
(pct->*fpointpt)(100);//调用

------虚函数指针---------------
void (CT::*fpointpt)(int) = &CT::vfunc; 
//也必须要绑定到类对象上才能用
//使用
CT ct, *pct;
pct = &ct;
(ct.*fpointpt)(100);  //调用
(pct->*fpointpt)(100);//调用

------静态成员函数---------------
使用 *函数指针变量名  声明指针,&类名::成员函数名 获取地址
void (*myfpointstatic)(int) = &CT::sfunc;
myfpointstatic(100);

-----类成员变量指针-------------
int CT::*mp = &CT::m_a;   //不是指向内存中的地址,而是与类指针之间的偏移量
CT test;
test.*mp = 100;

-------类静态成员变量--------
int CT::m_static = 1; //定义
int *st = &CT::m_static; //是一个真正的内存地址
*st = 190;   //= CT::m_static = 190;

第三部分:模板与泛型

1、模板概念、函数模板

  1. 泛型编程:独立于特定类型编写代码,使用时,提供实例需要的类习惯或值
  2. 模板是泛型编程的基础,转变发生再编译时
  3. 模板支持将类型作为参数的程序设计方式
  4. 模板分为函数模板类模板

函数模板:

template<typename T, typename Q>     //typename 可用class代替
T func(T a, T b)
{
	T add = a + b;
	return add;
}
  1. T到低是什么类型,编译器在编译时根据针对func的调用来确定

  2. 模板参数有时是推断出来的,如果推断不出来,就要用<>主动提供模板参数,类模板参数推断不出来
    int h = func(3,1); //系统可以推断出来int
    int h = func(3.1,1); //报错,推断不出

  3. 模板函数可以时inline, 函数模板定义一般都放在.h中;

  4. 模板定义并不会导致编译器生成 代码,只有在我们调用这个函数模板时,编译器为我们实例化一个特定版本的函数,编译器才会生成代码,这时需要能够找到函数体, 所以一般模板放在.h中,这样#include就可以使用了,多个.cpp 多次include不会重定义,没问题,普通函数就有问题了

  5. 非类型模板参数:代表一个值,而不是类型
    template<typename T, int S>

template<typename T, int a, int b>
inline int func(T c)
{
	int res = (int)c + a + b;
	return res;
}

int res = func<12,13>();    //显式指定模板参数
int a = 12;
int res = func<int, a, 13>(12);  //报错,只能是常量,实例化模板实在编译时确定的,a是运行时才知道的
res = func<int,12,13>(12);

//系统能推断出来就不需要显式指定
template<unsigned L1, unsigned L2>
int charcmp(const char(&pi)[L1] , const char(&p2)[L2])
{
	return strcmp(p1,p2);
}
int res = charcmp("tes", "te");  //L1 = 3,L2=2;能推断出来

2、类模板

概述:用类模板实例化一个特定的类,编译器不能自己推断参数类型,<>自己提供
e.g. vector<double_>

  1. 实例化类模板时,必须有类的全部信息,包括类模板中成员函数的函数体,都要放在.h函数中,#include就直接包含进来了
  2. 类模板的成员函数:写在.h中,可以写在类模板定义中的话(class{}中),会被隐式声明为inline,也可以放在下面。一旦被实例化, 那么这个模板的每个实例都会有自己版本的成员函数
  3. 一个类模板可能有很多成员函数,如果后续没使用,就不会实例化
  4. 非类型模板参数的限制:浮点数(float,double)、类类型 不能做非类型模板参数
-----myvector.h文件-----
定义类模板
template<typename T>
class myvector
{
public:
	typedef T* myiterator; //迭代器, vector iterator
	myvector();
	//在类模板内部使用模板名不需提供模板参数
	myvector& operator=(const myvector&);
	//myvector<T>& operator=(const myvector<T>&);
	myiterator mybegin();
	myiterator myend();
	//void func(){ ... }  //inline的
};
//外部实现
template<typename T>
void myvector<T>::func()
{
	...
}
template<typename T>
void myvector<T>::myvector()
{
	
}
template<typename T>
myvector<T>& myvector<T>::operator=(const myvector<T>&)
{
	return *this;
}

--------非类型模板参数的类------
template<typename T, int size = 10>
class A{
public:
	T arr[size];
	void myfunc();
}
template<typename T, int size>
void A<T,size>::myfunc()
{
}
-------main.cpp-------
#include "myvector.h"
//类型模板参数的类
myvector<int> vec;

//非类型模板参数的类
A<int,100> mp;   //100
A<int> mp2;    //10

3、typename使用场合、函数指针做其他函数参数

1)typename使用场合:后面跟的是一个类型

  • template<typename T, int a, int b>
  • 访问typedef定义的类型成员时
    typedef T* myiterator; 是一个类的类型成员
    //不加typename不知道myiterator是类型还是静态成员,typename告诉编译器这时一个类型
 template<typename T>
 typename myvector<T>::myiterator myvector<T>::mybegin()
 {}

2)函数指针做其他函数参数

int mf(int a, int b)
{
	return 1;
}
typedef int (*FunType)(int,int);
void testfun(int i,int j, FunType funpt)
{
	int res = funpt(i,j);
}
----调用------
testfun(1,2,mf);

用函数模板代替typedef

//定义时可以提供缺省值
template <typename T, typename F=mf>
void testfun(T i,T j, F funpt)
{
	int res = funpt(i,j);
}

testfun(1,2,mf);
mf也可以是一个类,可调用对象就可以

4、成员函数模板、显式实例化

//类模板
template <typename C>
class A
{
public:
	//成员函数模板可以直接推断
	template <typename T>
	void fun(T m)        //成员函数模板T
	{
		T res = m;
	}
}
//外部声明怎么写
template <typename C>
template <typename T>
void A<C>::fun(T m)        //成员函数模板T
{
	T res = m;
}

-----main.cpp-----------
A<float> a;  //类的模板给定
a.fun(3);  //此时才实例化,成员函数模板可以直接推断

显式实例化:不太推荐
为了防止在多个.cpp文件中都实例化相同的模板,编译久、代码大,C++11提出:显式实例化

test.cpp
#include "A.h"
模板显式实例化,这里定义一次
template A<float>;    //实例化一个A<float>

其他cpp声明
test2.cpp
#include "A.h"
extern template A<float>;  //声明,不会再生成这种实例化文本了

5、用模板定义变量using,typedef

写法一:
template<typename mp>
struct map_s{
typedef std::map<std::string, mp> type;
};
写法二:这种写法太麻烦了,用using比较简单
template<typename T>
using str_map_t = std::map<std::string, T>

/**用法**/
map_s<int>::type map1;   //std::map<std::string, int>

typedef unsigned int uint_t;
using uint_t = unsigned int;

using定义模板类型和普通类型
template<typename T>
using myfun = int(*)(T,T);

using myfun = int(*)(int,int);

6、模板全特化、偏特化

特化:对特殊类型进行特殊对待

*********泛化版本*********
template<typename T, typename U>
struct TC{
	void func()
	{
		cout << "泛化版本" <<endl;
	}
}
当T和U都为int时,做个特化版本
*********全特化:所有类型(T和U)都用具体类型代替*********
template<>
struct TC<int,int>{
	void func()
	{
		cout << "int int 的特化版本" << endl;
	}
}

template<>
struct TC<double,int>{
	void func()
	{
		cout << "double int 的特化版本" << endl;
	}
}

全特化一个成员函数,而不是整个类
template<>
void TC<double,int>::func()
{}

*********类模板偏特化***********************
template<typename T, typename U, typename W>
struct TC{
	void func()
	{
		cout << "泛化版本" <<endl;
	}
}

template<typename U>
struct TC<int,U,double>{
	void func()
	{
		cout << "	偏特化版本" <<endl;
	}
}

函数模板特化:只能全特化,不能偏特化
template<>
int func(int& a, double& b)
{
}
重复定义时,编译器选择:普通函数优先,特化优先、泛化模板

7、可变参模板

可变参模板:允许模板中有0~任意个 参数

template <typename... T>
void func(T... args){
	cout << sizeof...(args) << endl;
	cout << sizeof...(T) << endl;
}

template <typename U, typename... T>
void func2(U first, T... args){
	cout << sizeof...(args) << endl;
	cout << sizeof...(T) << endl;
	func2(args...); //递归调用
}

func();
func(10,20);
func(10,2,1,"abd");

第四部分:智能指针

1、直接内存管理new/delete

  • new动态分配内存,分配再堆上
vector<int> *pv = new vector<int>{1,2,3,4,5};
string *ps = new string(5,'a');

string *ps = new string(); //初值为“”
int *pv = new int();//初值为0
delete pv;
pv = nullptr;
const int *pc = new const int(100);

1)只能delete一次
2)delete之后赋值为nullptr是个好习惯

int *pv = new int();//初值为0
delete pv;
pv = nullptr;

VS下的MFC应用程序能够再程序退出的时候,帮助我们发现内存泄漏
ubuntu下Valgrind可以用于检测内存泄漏

  • new/delete时运算符,不是函数,相对于free/malloc,new/delete可以对堆上分配的内存进行初始化和释放
  • sizeof也是运算符,只能计算栈上的
    空类:1byte //类对象的地址
    有构造和析构的类:只有成员函数,不占用类的内存,还是1,有成员变量,就占内存了
A* pA = new A();  //leak 1个字节
A* pA = new A[2]();   //leak 6个字节
//为什么多出了4个字节?记录的是数组尺寸2, 如果没析构函数,就2字节了
delete[] pA;   //这时才知道尺寸是2

int * A= new int[2];  //leak 8, 内置类型,不用记录数组尺寸
  • 裸指针:直接用new返回的指针(上面的都是裸指针),灵活,容易出错,因此需要引入智能指针,对裸指针进行包装,自动释放所指向对象的内存

2、shared_ptr

工作原理:引用计数,最后一个指向该内存的shared_ptr不指向时,释放这块内存

******常规初始化*****
shared_ptr<int> pi(new int(100));
shared_ptr<int> pi2 = new int(100);  //不可以,禁止隐式类型转换 explicit,一般带=的都是隐式类型转换
shared_ptr<int> pi3 = make_shared<int>(100);  //make_shared高效
shared_ptr<string> pi3 = make_shared<string>(5,'a');

裸指针可以初始化智能指针,但不推荐
int* pi = new int;
shared_ptr<int> pi4(pi);  //no
shared_ptr<int> pi(new int);  //ok

*****引用计数的增加和减小******
auto p1 = make_shared<int>(100);  //1
auto p2(p1);  //2
1)函数传值引用计数+1,传引用就不增加了
2)作为函数返回值,有变量接+1,没有->不增加
//减小的情况
1)p2 = 新对象
2)局部的shared_ptr离开作用域

**********常用函数**********
int count = p1.use_count();   //引用计数
p1.unique()//该指针是否独占该内存
p1.reset();   //p1 == nullptr, 如果是唯一指向该对象的,还要释放内存
p1.reset(new int(100)); //原count-1,指向新对象
//空指针可通过reset初始化
*p1;    //解引用
int *p = p1.get();  //返回裸指针,不要这么做,小心,如果之恶能指针释放了该对象,裸指针也就无效了
std::swap(p1,p2);
p1.swap(p2);  //交换两个智能指针指向的对象

*******删除器**************
shared_ptr<int> p(new int(12), myDelete);
shared_ptr<int> p(new int(12), [](int *p){
	delete p;
});
void myDelete(int* p)
{
	delete p;
}
有些情况默认删除器解决不了,用shared_ptr管理动态数组,需自己指定
shared_ptr<A> pA(new A[10], [](A *p){delete []p;});
shared_ptr<A> pA(new A[10], std::default_delete<A[]>());
shared_ptr<A[]> pA(new A[10]);
两个shared_ptr删除器不同,但指向对象相同,也是同一类型:可以放入同一容器
make_shared不方便自己制定删除器

3、weak_ptr

概述:辅助shared_ptr进行工作,指向一个由shared_ptr管理的对象。但不控制对象的生存周期,不改变shared_ptr的引用计数,shared_ptr完全不管是否有weak_ptr是否指向对象。

  • 用于监视shared_ptr,可以监控这个对象是否存在.
  • 解决了一个引用计数导致的问题:在存在循环引用的时候会出现内存泄漏。
auto p1 = make_shared<int>(100);
weak_ptr<int> p1w(p1);  //弱引用计数改变,强引用计数不变

********常用操作***********
shared_ptr<int> pi2 = p1w.lock(); //检查所指对象是否存在,如果不存在 == nullptr
if(pi2 != nullptr)   //对象存在

int res = pi2.use_count(); //当前对象的强引用计数
if(pi2.expired())
	cout << "对象已经不存在了, 资源已经释放,返回true" << endl;
pi2.reset(); //pi2置为空,弱引用计数-1
*******尺寸********
裸指针:4字节
weak_ptr 和shared_ptr: 8字节,下图有解释

智能指针内存

4、shared_ptr使用陷阱

  1. 慎用裸指针
    1)不存在裸指针到智能指针的隐式转换,只能用裸指针初始化一个智能指针输入函数;
    2)把一个裸指针绑定到一个shared_ptr之后,内存管理的责任就交给shared_ptr,就不应该再用裸指针访问内存了
func(p); //wrong
func(shared_ptr<int>(p)); //临时构造一个shared_ptr,返回之后临时变量没了,内存系统回收了,下面*p不可用
*p = 45;   //不可以
//应该这么写
shared_ptr<int> p2(p);
func(p);
*p2 = 45;
	 3)不要用裸指针初始化多个智能指针
int *p = new int(100);
shared_ptr<int> p1(p);  //1
shared_ptr<int> p2(p);  //1
两个强引用都为1,无关联,会被释放两次,系统产生异常
//应该这样写
shared_ptr<int> p1(new int(100));  //1
shared_ptr<int> p2(p1);  //2
  1. 慎用get()返回裸指针:
    1) get()返回的指针不能delete,否则会异常,重复delete
    2) 不能将其他智能指针绑到get返回的指针上,也会重复删除

  2. 不要把类对象指针this作为shared_ptr返回

    shared_ptr<CT> getselt()
    {
    	return shared_ptr<CT>(this);  //相当于用裸指针初始化智能指针了
    }
    shared_ptr<CT> p1(new CT);
    shared_ptr<CT> p2 = p->getself(); //错误
    //应该这样写
    shared_ptr<CT> getselt()
    {
    	return shared_from_this();//调用了weak_ptr, 返回了lock()
    }
    
  3. 避免循环引用:可能导致内存泄漏,用weak_ptr解决

    class CB;
    class CA{
    public:
    	shared_ptr<CB> m_b;  ----> weak_ptr<CB> m_b;
    	~CA(){}
    }
    
    class CB{
    public:
    	shared_ptr<CA> m_a;
    	~CB(){}
    }
    
    *****main******
    shared_ptr<CA> pa(new CA);
    shared_ptr<CB> pb(new CB);
    pa->m_b = pb;   //指向CB  2
    pb->m_a = pa;   //指向CA  2
    //离开作用域后,pa、pb引用从2-1
    
    互相指向,最后这两个对象都没释放,引用计数都没法变成0,都是1
    解决方法:把其中一个变成weak_ptr
    shared_ptr<CA> pa(new CA);
    shared_ptr<CB> pb(new CB);
    pa->m_b = pb;   //指向CB  2
    pb->m_a = pa;   //指向CA  1
    //离开作用域后,pa引用从1-0,对象即将释放,pa->m_b -1, pb也释放了
    
  4. 性能说明:
    shared_ptr、weak_ptr的尺寸是裸指针的2倍,多了个控制块,指向同一内存的公用一个控制块,由第一个指向这个对象的shared_ptr创建。
    控制块创建时机:

    • make_shared可以创建
    • 用裸指针创建shared_ptr

    移动语义:快一些

    shared_ptr<int> p1(new int(100));
    shared_ptr<int> p2(std::move(p1));  //1, p1为nullptr
    shared_ptr<int> p3;
    p3 = std::move(p2);  //1, p2==nullptr
    

5、unique_ptr

  1. 优先考虑使用unique_ptr, 独占内存,同一时刻只有一个unique_ptr指针指向这个对象
**********初始化***************
unique_ptr<int> pi;   //nullptr
unique_ptr<int> p2(new int(100));
unique_ptr<int> p3 = make_unique<int>(100);  //C++14, 不支持指定删除器

************常用操作*************
1)不支持拷贝、赋值操作
unique_ptr<int> p1(new int());
unique_ptr<int> p2(p1);  //no
unique_ptr<int> p3 = p1; //no
unique_ptr<int> p4; p4 = p1;  //no

2)移动
unique_ptr<int> p2 = std::move(p1);  //ok, p1 ==nullptr

3)release()放弃对指针的控制权,智能指针nullptr,返回裸指针,裸指针可手动delete,也可给其他智能指针赋值
unique_ptr<int> p2(p1.release());  //p1==nullptr
p2.release(); //导致内存泄漏,没有delete
int* pp = p2.release();   delete pp;    //ok

4)reset()  指针置空且释放
p1.reset();
带参数,释放现有对象指向新对象
p1.reset(p2.release());

5)p1 == nullptr, 置空且释放

6)指向数组
unique_ptr<int[]> p1(new int[10]);  //这种写法可以释放数组内存
p1[0] = 12;  p1[2] = 1; 

7get()返回智能指针中的裸指针,内存释放,考虑到有些函数参数需要裸指针,所以引入参数
int* pp = p1.get();
*pp = 1;
delete pp; //wrong

8)解引用,swap()

9) 转成shared_ptr: unique_ptr是右值,可直接赋值给shared_ptr
```cpp
auto func()
{
	return unique_ptr<int>(new int(100));  //右值
}
shared_ptr<int> p1 = func();
unique_ptr<int> p2(new int());
shared_ptr<int> p3 = std::move(p2);
  1. 删除器:其实就是一个可调用对象,如函数、类重载()
    注意:unique_ptr中的删除器会影响unique_ptr类型,不能放在同一个容器中,而shared_ptr可以
void mydelete(string * p)
{
	delete p;
	p = nullptr;
}

typedef void(*fp)(string *);  //using fp = void(*)(string *); 
unique_ptr<string, fp> p1(new string("1"), mydelete);

unique_ptr<string, decltype(mydelete)*> p1(new string("1"), mydelete);

auto mydel = [](string *p){delete p; p = nullptr;}
unique_ptr<string, decltype(mydel )*> p1(new string("1"), mydel );
  1. unique_ptr尺寸
    unique_ptr 一般和裸指针尺寸一样(小且快),如果增加了自己的删除器,尺寸可能增加:
    1)lambda表达式,尺寸不变
    2)定义函数删除器,尺寸增加,慎用删除器

  2. 智能指针选择
    如果程序要使用多个指向同一对象的指针,选shared_ptr, 否则选unique_ptr,效率更高

第五部分:内存高级话题

1、C++内存分区

把内存分为5个区域
1)栈:函数内局部变量,编译器自动分配和释放
2)堆:程序员new、delete。忘记释放后,程序结束系统会回收
3)全局、静态存储区:全局变量和static变量,程序结束后系统释放
4)常量存储区:“ilove”
5)程序代码区
栈:空间有限,分配快
堆:不超过实际物理内存,都可以分配,速度慢一些

2、new、delete的新认识

  1. new对象有和没有括号的区别
    1)空类没区别
    2)有成员变量,没构造函数,把一些和成员变量有关的内存清零,不是整个对象内存清零
    3)有构造函数,没区别

  2. new做了什么
    1)调用了operator new函数:malloc分配内存
    2)调用构造函数
    void temp = operator new(sizeof(A));
    A
    p = static_cast<A*>(temp*);
    pa->A::A();
    数组:A*pa = new A[3];
    malloc分配一次内存,调用3次构造函数

  3. delete做了什么
    1)析构函数
    2)operator delete(): free内存

  4. 内存分配细节
    1)一块内存的delete不止影响这些内存,周边也影响。delete删除一块内存(不是连续空间),可能需要和其他空闲块合并生成更大的一个空闲块。
    2)int* a = new int(10); delete a;
    编译器delete怎么知道new的大小,这个是怎么记录的呢,编译器自动做了很多处理,比如记录分配出的自己数等等,这个内存周围一点的某个字节记录了这个值,下表看一下编译器都记录了什么东东,这个内存是连续的,尤其是多次频繁申请小块内存,这会造成内存的浪费

  5. operator new()怎么写
    <C++对象模型探索>

class A
{
public:
	//重载这个函数
	static void *operator_new(size_t size);
	static void operator_delete(void *phead);
}
void *A::operator_new(size_t size)
{
	//...
	A *ppoint = (*A)malloc(size);
	return ppoint;
}
//数组,调用多次构造函数,但operator_new[]只调用一次,前面多了4个字节,用来记录数组大小,new A[3], 7字节 = 3 + 4(尺寸,int)
void *A::operator_new[](size_t size)
{
	//...
	A *ppoint = (*A)malloc(size);
	return ppoint;
}

3、内存池

按照刚刚的说法,malloc存在内存浪费,频繁调用小块内存,浪费
内存池:
1)减少malloc的次数
2)减少malloc的调用次数,会有一些效率和速度的提升,但比较小
实现原理:
用malloc申请一大块内存,当你要分配的时候,从这一大块内存中一点一点分配给你,分配的差不多的时候,迈永malloc分配申请一大块内存…
目的:减少内存浪费,提高运行效率
针对一个类的内存池实现代码:针对一个类的内存池
A* pa = new A(); delete pa;

class A
{
public:
	static void *operator_new(size_t size);
	static int m_iCount;
	static int m_iMallocCount;
	A* next;
	static A* m_freePosi;
	static int m_sTrunkCount;
}
int A::m_iCount = 0;
int A::m_iMallocCount = 0;
A::A* m_freePosi = nullptr;
int A::m_sTrunkCount = 5;

void *A::operator new(size_t size)
{
	A* tmplink;
	if(tmplink == nullptr)
	{
		size_t realsize = m_sTrunkCount  *  size;
		m_freePosi = reinterpret_cast<A*>(new char[realseize]);
		tmplink = m_freePosi ;
		//分配出5快内存,链表起来,供后续使用
		for(;tmplink != &m_freePosi [m_sTrunkCount  -1];tmplink++)
		{
			tmplink->next = tmplink +1;
		}
		tmplink->next = nullptr;
		m_iMallocCount ++;
	}
	tmplink = m_freePosi ;
	m_freePosi =m_freePosi ->next;
	m_iCount ++;
	return tmplink ;
}

delete不想写了。。。。。

4、嵌入式指针

一般和内存池代码一起使用
工作原理:借用A对象所占用内存空间中的前四个字节,用来链住空闲的内存块(类A对象至少4个字节),一旦某一块被分配出去,那么这块的前四个字节就不再需要,此时这四个字节就可以被重复使用(替换前面的next指针)
代码演示:sizeof超过4字节的类就可以安全的使用嵌入式指针

class A
{
int m_i;
int m_j;

struct obj
{
	struct obj *next;  //这个就是嵌入式指针
}}void func()
{
	A test;   //8byte
	A::obj *ptemp;
	ptemp = (A::obj*)&test;  //ptemp->next与ptemp地址相同
	ptemp->next = nullptr;  //借用test的首地址保存ptemp->next
}

因此可以用嵌入式指针作为快与块之间的链接,对内存池代码进行改进

class A
{
public:
	static void *operator_new(size_t size);
	static int m_iCount;
	static int m_iMallocCount;
	
	struct obj{
		struct obj *next;
	}
	obj* m_freePosi = nullptr;
	//static A* m_freePosi;
	static int m_sTrunkCount;
}

5、全局new、delete重载

void *operator new(size_t size)
{
	return malloc(size);
}
void *operator new[](size_t size)
{
	return malloc(size);
}

void operator delete(void *phead)
{
	free(phead);
}

void operator delete[](void *phead)
{
	free(phead);
}

定位new:在已经分配好的内存中构造对象
格式:new (地址) 类类型()
void mymemPoint = (void)new char[sizeof(A)];
A * obj = new (mymemPoint ) A();
obj ->~A(); //手工调用析构时可以的,调用构造就要用到定位new

第六部分:STL模板库

1、基础介绍

STL是C++标准库的重要组成部分;
泛型编程:使用模板为主要的变成手段来编写代码
STL组成:
1)容器:vector、list、map
2)迭代器
3)算法:search,sort,copy
4)内存分配器:vector<int,分配器> v;
5) 其他:适配器、仿函数等

2、容器

stl容器分为三类:
1)顺序容器:array,vector,deque,list,forward_llist,
2) 关联容器:键/值,特别适合查找,稚嫩恶搞控制插入内容,但不能控制插入位置,set,map, mutiset,multimap,红黑树
3)无序容器:C++11,元素位置不重要,重要的是这个元素是否在容器里。也属于一种关联容器,unordered_map,unordered_set, 哈希表
在这里插入图片描述

  1. array:内存空间内连续,大小固定,申请是指定大小
array<string,5> mystring={"I", "Lo"};
mystring[0] = "ch";
//对象地址是连续的,比较短的string,直接存在对象首地址了(string 28byte),存不下的,对象指向的内容存在于其他位置

在这里插入图片描述

  1. vector
    1)往后卖弄增加和删除元素很快
    2)向中间插入元素可能导致后面元素移动,效率低
    3)查找速度不太快
  • vector内存空间是连续的,vector有一个空间的概念,每个空间可以放入一个元素.
  • 内存不够了,重新找一个能容纳所有的内存,删除原来的,全部拷贝到新的内存中;智能的地方在于可以自动找一个大一点的,这样不用每次push_back都重新分配内存
  • 元素个数size;容器容量capacity()>=size();
  • 往中间插入元素,还要重新构造,效率很低,中间删除还可以,只是地址移动,没有重新构造:
  • 改进:myvector.reserve(10); //capacity()=10;通过预留空间提高程序运行效率
  1. deque双端队列
    在头部尾部插入删除数据很快,中间插入需要移动其他元素(线性表顺序存储结构,分段连续内存)
    在这里插入图片描述
  2. 栈:后进先出, queue:先入先出
    deque实际上包含stack和queue功能
  3. list
    双向链表:不需要内存空间连续,任何位置插入删除快,查找慢
    vector与list区别:
    vector中间开始插入慢,内存不够时,重新找内存,对原来析构,新内存中重新构建对象,list快
    vector能高效随机存取和定位,比如访问第五个元素,list慢

vector
  vector与数组类似,拥有一段连续的内存空间,并且起始地址不变。便于随机访问,时间复杂度为O(1),但因为内存空间是连续的,所以在进入插入和删除操作时,会造成内存块的拷贝,时间复杂度为O(n)。

此外,当数组内存空间不足,会采取扩容,通过重新申请一块更大的内存空间进行内存拷贝。

List
  list底层是由双向链表实现的,因此内存空间不是连续的。根据链表的实现原理,List查询效率较低,时间复杂度为O(n),但插入和删除效率较高。只需要在插入的地方更改指针的指向即可,不用移动数据。

  1. forward_list:C++11
    单向链表,少了一个方向的指针,尤其是元素多时,节省内存

  2. set、map:红黑树
    通过key查找value很快,不允许key相同,key希望相同,要用multimap
    1)插入时,因为容器需要找到合适的位置,相对慢一点
    2)查找很快logN

  3. unordered_map, unordered_set: 哈希表
    O(1)的查找和插入,相对于set和map,数据是无序的
    哈希表,插入不够时,也有扩容操作,防止某个Buckets上单词太多,减慢查找速度
    set.bucket_count(); //bucket数量
    for(inti=0;i<myset.bucket_count();++i)
    {
    myset.bucket_size(i); //每个bucket里有元素的数量
    }
    在这里插入图片描述

3、内存分配器

和容器紧密关联,一起使用。内存分配器扮演内存池的角色,通过大量减少malloc的调用来节省内存,升值还可能提高分配效率。一般使用系统缺省的分配器。

list<int,std::allocator<int>> mylist;
list内存并不是连续的,说明缺省的没采用内存池机制;分配器可直接编码使用
allocator<int> all;
int *p = all.allocate(3); //allocate()用来分配原始的未构造的内存,这段内存能保存3个类型为int的对象
all.deallocate(p,3);//释放内存

分配器可以多容器共用,也可以每个容器都有自己的分配器,取决于代码怎么写

4、迭代器

遍历容器全部或部分元素的对象
迭代器的分类:依据移动特性
1)输出迭代器
2)输入迭代器struct input_iterator_tag
3)前向迭代器
4)双向迭代器
5)随机访问迭代器
2345是有继承关系的,3继承2,4继承3,5继承4
stack,queue没有迭代器
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

5、算法

算法是搭配迭代器使用的,和容器无关,之和迭代器有关,增加算法开发的弹性,算法会判断出迭代器类型,不同类型对应不同的处理。编写不同代码来处理不同种类迭代器,直接影响算法执行效率,这也是为什么给迭代器分类的原因。
用法举例:

void myfunc(int i)  //参数类型是容器中元素类型
{
	cout << i<< endl;
}

//for_each
vector<int> myvvector= {10,20,30};
//不断迭代两个迭代器之间的元素,然后拿到这个元素,调用myfunc
for_each(myvector.begin(), myvector.end(), myfunc());  //myfunc是可调用对象即可, 可调用对象调用同一f(input)

//find    左闭右开区间查找
vector<int>::iterator finditer = find(myvector.begin(), myvector.begin()+2, 400);  //+2 30
if(finditer != myvector.begin()+2)  找到了

map<int string> mymap;
mymap.insert(make_mair(1,"da");
if(mymap.find(1) != mymap.end());    //优先使用自己的成员函数

//find_if  返回大于10的第一个元素
auto res = find_if(myvector.begin(), myector.end(), [](int val){
if(val>10)
	return true;
return false;
});

//sort
sort(myvector.begin(), myector.end());   //默认升序
sort(myvector.begin(), myector.begin()+3);   //对前三个元素排序

bool func(int i,int j)
{
	return i<j;//从小到大
}
sort(myvector.begin(), myector.end(),func);

class A
{
	public:
	bool operator()(int i,int j)
	{
		return i > j;
	}
}
A a;
sort(myvector.begin(), myector.end(),a);

list<int> mylist = {3,5,6,2};
mylist.sort(func);
sort只适合顺序容器

6、仿函数(函数对象)

函数对象在stl中和算法配合使用,从而实现一些特定功能,服务于算法,包括
1)函数
2)可调用对象
3)lambda表达式
包含标准库提供的函数对象
在这里插入图片描述

#include <functional>

plus<int>();  //加圆括号,生成一个临时对象,就是一个可调用对象

自定义函数对象
class A
{
	public:
	bool operator()(int i,int j)
	{
		return i > j;
	}
}
A a;
sort(myvector.begin(), myector.end(),a);

用系统提供的
sort(myvector.begin(), myector.end(),greater<int>());   //关系运算类

7、适配器

  1. 基本概念:类似于转接头,起桥梁作用
    把一个既有的东西进行适当的改造,增减点东西,就构成了一个适配器

  2. 容器适配器:类模板, stack, queue

  3. 算法适配器:绑定器binder

    vector<int> myvector = {3,4,56,4,43};
    int count_ = count(myvector.begin(),myvector.end(),4);  //4出现的次数
    
    class A
    {
    	public:
    	bool operator()(int i)
    	{
    		return 4 < i;
    	}
    }
    A a;
    int count_ = count_if(myvector.begin(),myvector.end(),a);  //大于4元素个数
    
    auto bf = bind(less<int>, 4, placeholders::_1);  
    //less<int>第一个参数固定为4,第二个参数,用这里的placeholders::_1表示
    int count_ = count_if(myvector.begin(),myvector.end(),bind(less<int>(), //临时对象
     4, 
     placeholders::_1
     ));  //大于4元素个数
    
  4. 迭代器适配器:
    reverse_iterator:反向迭代器

第七部分:多线程

1、基本概念

  1. 并发:多个任务同时发生;
    上下文切换:多核执行4个任务
    在这里插入图片描述
  2. 可执行程序:有执行权限的文件x
  3. 进程:windows下双击一个可执行程序运行;linux下./文件名,一个可执行程序运行起来,就创建了一个进程
  4. 线程:每个进程都有一个主线程,唯一的主线程,与进程同步,可以理解成代码的执行通路;
    多线程:多走不同的代码执行路径,一个线程代表一个新的执行通路,线程不是愈多越好,线程切换需要时间,线程最大不超过200-300个
  5. 多进程并发:
    多进程之间的通信:同一电脑上:管道,文件,消息队列,共享内存; 不同电脑上socket通信技术
  6. 多线程并发:
    • 轻量级的进程,一个进程中的所有线程共享内存,全局变量、指针、引用都可在线程之间传递,所以,使用多线程开销远远小于多进程
    • 共享内存带来的问题:数据一致性问题
  7. 总结:
    • 线程启动速度快、轻量级
    • 系统资源开销小,执行速度快,共享内存这种通信方式比其他的通信都要快

2、线程创建、join, detach

进程结束的标志:主线程结束, 所以要想保持子线程,主线程不能结束

#include <thread>
1--------------创建线程---------
void func()
{
	cout << "" << endl;
}
int main()
{
	thread myth(func);   //创建线程,线程开始执行
	myth.join();  //阻塞主线程,让主线程等待子线程执行完毕,汇合,主线程才能往下走
	//如果主线程结束了,但子线程还没执行完,这个程序是不稳定的,不合格,主线程需要等待子线程都执行完成后,自己才能最终退出
	cout << "主线程收尾,所有线程执行结束" << endl;
}


int main()
{
	thread myth(func);   //创建线程,线程开始执行
	myth.detach();  //主线程和子线程不再有关联,自己执行自己的,主线程执行完,子线程也不会退出,自己在后台执行,由C++运行时刻接管,由运行时库负责清理该线程占用的资源(守护线程)
	/**一旦调用detach(),就不饿能join,一旦join就不能join和detach,joinable返回bool,true表示可以join或detach,false表示不可以**/
	if(myth.joinable()) ....;
	cout << "主线程结束" << endl;
	return 0;
}


创建线程的方法:可调用对象就可创建
class A
{
	int &m_i;
	A(int &t):m_i(t){}
	void operator()	()
	{
		//线程内容
		cout << m_i << endl;
	}
};

类对象创建线程
int t = 9;
A a(t);
thread obj(a);   //A重载()
obj.detach();  //主线程结束,t销毁,引用调用,就会由问题
//主线程结束后,A对象也被销毁,但这个对象a实际上被复制到线程中去,a被销毁,但复制的A还存在,所以如果类中没有引用指针的话,这个类就可以正常使用

lambda表达式创建线程
auto m = []{
...
};
thread obj(m);
obj.join();

在这里插入图片描述

3、线程传参

一、传递临时对象作为线程参数
void myprint(int& a , char* b)
{
	cout << a <<  b;
}

int mvar = 1;
int& mvary = mvar;
char buf[] = "jsklsl";
thread obj(myprint, mvar,buf);
obj.detach();   //子线程脱离,此时a不是mvar的地址,不是引用,而是传值,因此子线程的a是安全的;但是指针b和buf是一个地址,主线程结束后,b是不安全的
//detach,就不要传引用和指针
总结:
1)若传递int这种简单类型参数,建议是值传递,不要用引用
2)如果传类对象,避免隐式类型转换(在子线程中隐式转换,构建对象),全部在构建宪曾这一行就构建出临时对象来(主线程构建对象),然后再函数参数里用引用来接
thread obj(myprint, mvar,A(m), string(buf));
void myprint(int& a ,const A& c, const string& b)
3)建议不适用detach,使用join(), 这样就不存在局部变量失效导致线程对内存的非法引用

线程id获取: std::this_thread::get_id()

二、传递类对象、智能指针作为线程参数传递
传类对象
void func(const A& pmybuf)
{
	pmybuf.m_i =  122;    //m_i为mutable
}
A a(1);
thread obj(func,a);
obj.join();
//a.m_i没有被修改,因为引用传递在多线程总退化为值传递,a复制了一个临时对象传给线程参数,执行了一个拷贝构造函数
如果就是向传一个引用,应该用std::ref, 就一定不能用detach了
void func(A &pmybuf){pmybuf.m_i =  122;}
thread obj(func,std::ref(a));

传智能指针
void func(unique_ptr<int> pmybuf){...}
unique_ptr<int> my(new int(100));
std::thread obj(func2, std::move(my));
obj.join();   //执行到这里,内存被释放了

成员函数指针做线程函数
class A{
public:
	void thread_work(int i){}
}
A a(1);
thread obj(&A::thread_work,a, 13);  //类拷贝构造,可以用detach
thread obj(&A::thread_work,std::ref(a), 13); //这样就不能用detach了
thread obj(&A::thread_work,&a, 13); //这样就不能用detach了
obj.join();

4、创建和等待多个线程

a)多个线程执行是顺序是乱的,与内部切换机制有关
b)把thread放到容器里,方便对大量线程的操作和管理
c)多线程只读共享数据,这是没问题的
d) 有读有写,2个线程写,8个读,程序崩溃
写的动作分为10步,由于任务切换,导致诡异的事情发生,可能崩溃

vector<thread> mythreads;

for(int i=0;i<10;++i)
{
	mythreads.push_back(thread(func, i));  //创建并开始执行
}
for(auto iter = mythreads.begin();iter!=mythreads.end();++iter)
{
	iter->join();
}

5、mutex,lock

经验判断保护哪段数据
没调用一次lock,对应一次unlock(),非对称数量的调用会导致代码不稳定

#include <mutex>

class A{
public:
	void in()
	{
		for(int i=0;i<10000;++i)
		{
			m_mutex.lock();
			data.push_back(i);
			m_mutex.unlock();
		}
	}
	void out()
	{
		for(int i=0;i<10000;++i)
		{
			m_mutex.lock();
			if(!data.empty()){
				int font = data.front();
				data.pop_back();
				m_mutex.unlock();
				return true;
			}
			m_mutex.unlock();
			return false;
		}
	}
private:
	std::list<int> data;
	std::mutex m_mutex;
}

int main()
{
	A obj;
	std::thread out(&A::out, &obj);
	std::thread in(&A::in,&obj);
	obj.join();
}

6、std::lock_guard、死锁、死锁解决方案

  1. std::lock_guard类模板: 自动unlock,取代了lock和unlock
	void in()
	{
		for(int i=0;i<10000;++i)
		{
			std::lock_guard<std::mutex> sbguard(m_mutex);
			data.push_back(i);
		}
	}
	void out()
	{
		for(int i=0;i<10000;++i)
		{
			std::lock_guard<std::mutex> sbguard(m_mutex);
			//构造函数执行lock,return时执行析构,unlock,如果想提前析构
			{
					std::lock_guard<std::mutex> sbguard(m_mutex);
					....函数体
			}
			if(!data.empty()){
				int font = data.front();
				data.pop_back();
				return true;
			}
			return false;
		}
	}
  1. 死锁:
thread1:
m_mutex1.lock();
m_mutex2.lock();
。。。
m_mutex2.unlock();
m_mutex1.unlock();

thread2:
m_mutex2.lock();
m_mutex1.lock();
。。。
m_mutex1.unlock();
m_mutex2.unlock();

解决方案1:保证两个互斥量上锁顺序一致,就不会死锁

thread1:
std::lock_guard<std::mutex> sbguard(m_mutex1);
std::lock_guard<std::mutex> sbguard(m_mutex2);
。。。

thread2:
std::lock_guard<std::mutex> sbguard(m_mutex1);
std::lock_guard<std::mutex> sbguard(m_mutex2);
。。。

解决方案2:std::lock():要么两个互斥量都锁住,要么都锁住,可以避免死锁。如果只锁了一个,另一个没成功,这个也解锁

std::lock(m_mutex1, m_mutex2);
....
m_mutex1.unlock();
m_mutex2.unlock();

解决方案3:std::lock()与lock_guard结合
std::adopt_lock是个结构体对象,表示对象已经lock过了

std::lock(m_mutex1, m_mutex2);
std::lock_guard<std::mutex> sbguard(m_mutex1 , std::adopt_lock);   //构造时就不调用lock,但析构时unlock
std::lock_guard<std::mutex> sbguard(m_mutex2 , std::adopt_lock);
....

不加以用std::lock,还是一个一个lock好了,注意顺序

7、unique_lock

unique_lock更灵活,但效率和占用内存比lock_guard大一些

1)std::adopt_lock
std::unique_lock<std::mutex> sbguard(m_mutex2 , std::adopt_lock);  //和lock_guard一样,需要先lock

2)std::unique_lock<std::mutex> sbguard(m_mutex2 , std::try_to_lock);  //拿到锁,正常,没拿到程序也不会卡在这里,可以干点别的事
if(sbgurad.owns_lock()) ...操作数据
else cout << "代码执行,不会卡在这里" << endl;

3)std::unique_lock<std::mutex> sbguard(m_mutex2 , std::defer_lock); //初始化一个没有加锁的mutex
sbguard.lock();   //不用自己unlock
......
sbguard.unlock();   //可能需要提前unlock
非共享代码
sbguard.lock();   //不用自己unlock
......

4try_lock(), 常使给互斥量加锁,如果拿不到锁,返回false,拿到了,返回true,并不阻塞
std::unique_lock<std::mutex> sbguard(m_mutex2 , std::defer_lock); //初始化一个没有加锁的mutex
if(sbguard.try_lock())
{
	.....
}
else{
	其他的事
}

5release(): 返回他所管理的mutex指针,,并释放所有权,unique_lock和mutex不再有关系
std::unique_lock<std::mutex> sbguard(m_mutex2);
std::mutex *p = sbguard.release();
........
p->unlock();   //自己负责unlock

6)unique_lock所有权可以转移,不能复制
std::unique_lock<std::mutex> sbguard(m_mutex2);
std::unique_lock<std::mutex> sbguard2(std::move(sbguard);

锁柱的代码少,粒度细,执行效率高
锁住的代码多,粒度粗,执行效率低

8、设计模式、call_once

比较常见的单例设计模式:整个项目中,有某个或者某些特殊的类,属于该类的对象,我们只能定义一个,不能创建多个

class CAS{
private:
	CAS(){}  //私有化构造函数
	static CAS *m_instance;
public:
	static CAS *getInstance()
	{
		if(m_instance ==NULL){
			m_instance = new CAS();
			static releaseCAS c;
		}
	}
	void func()
	{cout << "test" << endl;}

	class releaseCAS{
		public:
		~releaseCAS(){
			if(CAS::m_instance)
			{
				delete CAS::m_instance ;
				CAS::m_instance  = nullptr;
			}
		}
	};
}
static CAS *m_instance = NULL;
CAS *pa = CAS::getInstance();
CAS *pb = CAS::getInstance();  //pa pb都指向同一个对象

std::call_once:第二个参数是函数名a(),保证函数a之只能被调用一次。
具备互斥量的能力,且比互斥量消耗的功能更少。
需要和std::once_flag一起使用

std::once_flag g_flag;

static void oncefunc()
{
	....只需要调用一次的函数
} 
构造函数
{
	std::call_once(g_flag, oncefunc);  //第一个线程,另一个卡住,执行这个线程调用这个函数,flag置1,第二个线程过来,跳过这个函数执行
	
}

9、condition_variable,wait(),notify_one(),notify_all()

条件变量:
线程A:等待一个条件满足
线程B:向消息队列中放消息

class A{
	in_thread()
	{
		for(int i=0;i<10000;++i){
			std::unique_lock<std::mutex> sbguard(m_mutex1);
			data.push_back(i);
			cond.notify_one();   //唤醒wait,进行lambda判断
			.....其他处理
		}
	}

	out_thread()
	{
		while(true){
			std::unique_lock<std::mutex> sbguard(m_mutex1);
			notify_one之后,wait被唤醒:
			1)wait如果没有第二个参数,cond.wait(sbguard);,那么一定会走下来,一定加锁
			2)如果有第二个参数
			返回true,获得锁,wait返回,流程走下来,此时还是锁着的;
			返回false,解锁互斥量,堵塞到本行,直到其他线程调用notify_one()再次判断条件是否成立
			cond.wait(sbguard, [this]{
				if(!data.empty()) 
					return true;
				return false;
			}
			//m_mutex1是锁着的
			共享内存处理....
			sbguard.unlock();
		}
	}
private:
	std::condition_variable cond;
	std::mutex m_mutex1;
} 
  1. notify_one: 只有正好卡在wait的时候,notify_one才能唤醒,就算唤醒了,true,也不一定能获取锁成功,可能notify_one唤醒多次,wait才获取锁成功一次
  2. out_thread处理不过来,list中数据很多,怎么处理
  3. notify_all(): 如果有两个线程再wait,notify_one只能唤醒一个线程,不一定唤醒哪一个。改成notify_all,全部wait唤醒,这里例子里没差别,因为就算都唤醒,也只有一个能拿到锁;如果 wait绑定的锁不同,就有效果了

10、async、future返回值

希望线程返回一个结果;
std::async是一个函数模板,用来启动一个异步任务,之后返回一个future对象
future在线程结束的时候得到结果,举个例子吧!

int mythread()
{
	std::chron:::mlliseconds dura(50000);
	std::this_thread::sleep_for(dura);
	return 5;
}

int main()
{
	std::future<int> res = std::async(mythread); //创建线程,并开始执行
	cout << res.get() << endl;  //mythread没执行完,卡在get()这里,子线程执行完了,才能往下走
	//res.wait();
	return 0;
}

1)上述程序通过future的get函数等待线程结束并返回结果. get()只能调用一次
2)wait等待线程返回,卡在这,但不反悔结果,类似join()
3)额外向async传递std::launch类型参数,来达到特别的目的

  • std::launch::deferred:表示线程入口函数调用被延迟到wait或get调用时执行,并没有创建新线程,是在主线程中执行的
A a;
std::future<int> res = std::async(std::launch::deferred,&A::func, &a, 2);  //线程没有创建
cout << res.get() << endl;  //这里创建、执行,返回结果
  • std::launch::async: 再调用std::async的时候创建线程,立即执行
  • async创建的子线程,会等待子线程结束,主线程再返回,这点很好

4)future其他函数

std::future_status status = res.wait_for(std::chrono::seconds(1)); //等待1s,线程实际5s, 没返回,status == timeout
if(status == std::future_status::timeout)
{
	cout << "此时线程没执行完";
}else if(status == std::future_status::ready)
{
	cout << "此时线程执行完";
}

5)多个线程想获得返回结果,get只能调用一次,很明显不行,因此用std::shared_future
感兴趣的可以查一下使用方法,就不演示啦

11、package_task, promise

package_task: 把可调用对象包装起来,方便调用

std::package_task<int(int)> mypt(mythread);
std::thread t1(std::ref(mypt),1);
t1.join();
std::future<int> res = mypt.get_future();  //多了个get_future接口
cout << res.get() << endl;

vector<std::package_task<int(int)>> mytask;
std::package_task<int(int)> mypt([](int tmp){函数体});
mytask.push_back(std::move(mypt));
std::package_task<int(int)> mypt2;
auto iter = mytast.begin();
mypt2 = std::move(*iter);
mytask.erase(iter);
mypt2(123);

std::promise: 和future结合,可以通过函数传引用参数返回多个结果值

void mythread(std::promise<int> &temp, int calc)
{
	calc++;
	//...... 5s
	int res = calc;   //保存计算结果
	temp.set_value(res);   //将结果保存在temp
}

int main()
{
	std::promise<int> myprom;
	std::thread t1(mythread, std::ref(myprom), 90);  
	t1..join();  //用thread创建线程,就要join,不然最后会报异常,async可以不用,get等待,不会报异常
	//获取结果值
	std::future<int> fu1 = myprom.get_future();  //promise和future绑定,用于获取线程返回值
	auto res = fu1.get();   //没有join(), 就在get等待
}
可以实现线程间数据传递

void mythread2(std::future<int> &fu1)
{
	int t = fu1.get();
}
int main()
{
	std::promise<int> myprom;
	std::thread t1(mythread, std::ref(myprom), 90);  
	t1..join();  //用thread创建线程,就要join,不然最后会报异常,async可以不用,get等待,不会报异常
	
	std::future<int> fu1 = myprom.get_future();  //promise和future绑定,用于获取线程返回值
	std::thread t2(mythread2, std::ref(ful));
	t2.join();
}

12、原子操作

  1. 问题提出:
    只是一个读操作,汇编可能几条语句,在执行到中间被打断,内存中可能会出现不可预料的结果
    e.g.如,初始为4,一个读,一个写6,读的时候可能得到一个不可预料的结果(不是4也不是6)
    可以通过引入互斥量来解决,但执行效率会减慢,因为加锁也是需要时间的嘛。也可以通过原子操作。
  2. 概念
    原子操作:在多线程中,不会被打断的程序执行片段,可以理解为不需要互斥量加锁的多线程并发技术。就是不可分割的操作。
    e.g. 通过原子操作,我们不需加锁,比如写线程m++,读线程要么得到加之前的值,要么得到之后的值,不会得到中间值
  • 注意:互斥量加锁是针对代码段,原子操作针对的是一个变量的读写
  1. 范例:能用原子操作的话,就不要用互斥量了,原子效率更高
std::atomic来代表原子操作

std::atomic<int> count = 0;
int thread1()
{
	for(int i=0;i<10000;++i)
		count++;    //对应原子操作,不会被打断
}

int thread2()
{
	for(int i=0;i<10000;++i)
		count++;
}

thread obj1(thread1);
thread obj2(thread2);
cout << count << endl;    //20000
std::atomic<bool> flag = false;   //线程退出标记,防止读和写乱掉

void thread1()
{
	std::chron:::mlliseconds dura(5000);
	while(flag == false)
		cout << std::this_thread::sleep_for(dura);
}

main:
thread obj1(mythread);
thread obj2(mythread);
std::chron:::mlliseconds dura(2000);
std::this_thread::sleep_for(dura);
flag = true;
obj1.join();
obj2.join();
return 0;
  1. 用途:一般用于多线程的计数、统计等简单的操作
  • 针对++,–,+=…这些运算 a=a+1不是原子操作;

  • cout << a << endl; //读a是原子操作,但这条语句不是原子操作,输出的过程中a的值可能改变

  • 原子是不能赋值的

    auto b = a; //no
    atomic<int> c = a; //no
    只能这样:
    auto d(a.load());  //load()以原子方式读atomic对象的值
    

13、async深入

std::async是更高层次上的异步操作,使我们不用关注线程创建内部细节,就能方便的获取异步执行状态和结果,还可以指定线程创建策略,应该用std::async替代线程的创建,让它成为我们做异步操作的首选。
std::thread,如果系统资源紧张,创建线程可能失败,那么执行std::thread可能导致整个程序崩溃,而且想拿到线程返回值也不容易,async可以解决这两个问题
std::async一般不叫创建线程,叫异步任务
两者最明显的区别是有时候async不会创建线程
比如std::async(std:launch::defered, func),不创建新线程
std:launch::async:强制一定创建新线程
默认是std:launch::defered|std:launch::async,系统自行决定这两个哪个生效,怎么判断是哪个生效呢

std::future<int> res = std::async(mythread);
std::future_status status = res.wait_for(std::chrono::seconds(2));
if(status == std::future_status::timemout){
}else if(status == std::future_status::ready)

{}else if(status == std::future_status::deferred)

前两种创建新线程,后一种没有创建

14、其他

  1. 线程池:程序启动时,一次性创建所有线程(4,8,100-200),统一管理调度,循环利用线程
    就不写啦,有需要的时候再补充
  2. 线程数量建议: = cpu,cpu✖2, cpu✖4
  3. windows临界区类似于C++11的mutex
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值