C++11

发布时间:2024年01月05日

目录

1.列表初始化

2.声明

3.范围for

4.STL中的一些变化

5.右值引用

5.1左值引用和右值引用

5.2左值引用与右值引用比较

5.3右值引用的应用场景

5.4完美转发

6.新的类功能

7.可变参数模板

8.lamba表达式

9.包装器

10.线程库

10.1简单介绍

10.2线程函数参数

10.3原子性操作库(atomic)

10.4 lock_guard与unique_lock

11.异常

1.概念

2.异常的使用

3.自定义异常体系

4.异常的优缺点

12.智能指针

1.概念

2.引发问题

3.解决


1.列表初始化

使用{}进行初始化

class Date
{
public:
	Date(int year, int month, int day)
		:_year(year), _month(month), _day(day){}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	int x1{ 1 };				//{}初始化 
	int* p = new int[4]{0};		//用于new表达式
	Date d{2024,1,2};			//用于初始化对象(调用构造函数)
}

2.声明

2.1auto:用来实现自动类型推断,要求必须显示初始化

int main()
{
	vector<int> v1;
	//vector<int>::iterator it1 = v1.begin();
	auto it2 = v1.begin();//和上面等价
}

2.2decltype:将变量的类型声明为表达式的类型

int main()
{
	const int x1 = 1;
	double x2 = 2.2;
	decltype(x1 * x2)x3;//x3为double类型
	decltype(&x1)p;//p为int*类型
}

2.3nullptr:表示空指针(出于清晰和安全角度)

3.范围for

用于遍历集合的一种循环结构

int main()
{
	vector<int> v1{ 1,2,3,4,5,6 };
	//for (auto& num : v1)	//加上&可以修改v1里面的元素
	for(auto num : v1)
	{
		cout << num <<" ";
	}
}

4.STL中的一些变化

5.右值引用

5.1左值引用和右值引用

a.左值和左值引用

--左值是一个数据表达式 (可出现在赋值表达式的左边/右边) ~~>可以取地址的就是左值

--左值引用: 给左值取别名

int main()
{
	// 以下的p、b、c、*p都是左值
	int* p = new int(0);
	int b = 1;
	const int c = 2;

	// 以下几个是对上面左值的左值引用
	int*& rp = p;
	int& rb = b;
	const int& rc = c;
	int& pvalue = *p;

	return 0;
}

b.右值和右值引用

--右值 : 如字面常量, 表达式的返回值 (右值不能出现在赋值表达式的左边) ~~>右值不能取地址

--右值引用: 给右值取别名

int main()
{
	double x = 1.1, y = 2.2;
	// 以下几个都是常见的右值
	10;
	x + y;
	fmin(x, y);

	// 以下几个都是对右值的右值引用
	int&& rr1 = 10;
	double&& rr2 = x + y;
	double&& rr3 = fmin(x, y);

	// 这里编译会报错:error C2106: “=”: 左操作数必须为左值
	10 = 1;
	x + y = 1;
	fmin(x, y) = 1;

	return 0;
}

5.2左值引用与右值引用比较

a.左值引用只能引用左值,但是const左值引用都可以引用

int main()
{
	int a = 0, b = 1;
	int& ref1 = a;
    //int& ref2 = a + b; 报错
	const int& ref2 = a + b;
}

b.右值引用只能引用右值,但是可以引用move后的左值

int main()
{
	int a = 0, b = 1;
	int&& ref1 = (a + b);
	//int&& ref2 = a; 报错
	int&& ref2 = move(a);
}

c.结论

左值引用: 直接减少了拷贝 1.引用传参 2.传引用返回

右值引用: 间接减少了拷贝 1.解决传值返回(将将亡值的资源转移) ~~>识别出左值还是右值(不再深拷贝, 直接移动拷贝)

5.3右值引用的应用场景

a.区分左值和右值得意义

右值: 内置类型: 纯右值 ????????自定义类型: 将亡值

左值拷贝和右值拷贝的区别: 内置类型的区别不大 自定义类型的区别很大

b.左值拷贝与右值拷贝

左值拷贝: 深拷贝, 不会去修改原来的 ~~> 拷贝构造 拷贝赋值

右值拷贝: 当是将亡值的时候, 不去深拷贝, 进行资源的转移 ~~>移动构造 移动赋值

5.4完美转发

万能引用:使用模板,使其可以接收左值,右值

完美转发:std::forward,在传参得过程中,保留对象原生属性

void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }

// 1.模板中的&&而是万能引用,其既能接收左值又能接收右值。
// 2.右值引用后, 其属性变为左值,不然无法转移资源,但是还可能会出现继续往下传递的场景, 
需要保持它右值的属性
// 3.std::forward 完美转发在传参的过程中保留对象原生类型属性
template<typename T>
void PerfectForward(T&& t)
{
	Fun(std::forward<T>(t));
}

int main() 
{
	PerfectForward(10);           // 右值
	int a;
	PerfectForward(a);			  // 左值
	PerfectForward(std::move(a)); // 右值
	const int b = 8;
	PerfectForward(b);			  // const 左值
	PerfectForward(std::move(b)); // const 右值
	return 0;

}

6.新的类功能

a.默认成员函数:

  1. 构造函数
  2. 析构函数
  3. 拷贝构造函数
  4. 拷贝赋值重载
  5. 取地址重载
  6. const 取地址重载
  7. 移动构造(新增)
  8. 移动复制(新增)

移动构造函数和移动赋值运算符重载

  • 如果你没有自己实现移动构造函数且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
  • 如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
  • 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值

b.类成员变量初始化:?C++11允许在类定义时给成员变量初始缺省值

c.强制/禁止生成默认成员函数

--default:强制生成默认成员函数

--delete:禁止生成默认成员函数(一般用于IO流, 底层有缓冲区, 不想被拷贝)

class Person
{
public:
	Person(const char* name = "", int age = 0)
		:_name(name)
		, _age(age)
	{}
	//提供的拷贝构造就不会生产移动构造了
	Person(const Person& p)
		:_name(p._name)
		, _age(p._age)
	{}
	//使用default指定生产移动构造
	Person(Person && p) = default;

    //使用delete显示该函数的生成
    //Person(const Person& p) = delete;

private:
	string _name;
	int _age;
};

d.继承和多态中的final与override

--final修饰一个类~~>这个类不能被继承 修饰成员函数~~>这个成员函数不能被重写

--override修饰派生类的虚函数 用来检查是否被重写

// final:修饰虚函数,表示该虚函数不能再被重写
class Car
{
public:
    virtual void Drive() final {}
};
 
class Benz :public Car
{
public:
    virtual void Drive() {cout << "Benz-舒适" << endl;}
};

// override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错
class Car{
public:
    virtual void Drive(){}
};
 
class Benz :public Car {
public:
    virtual void Drive() override {cout << "Benz-舒适" << endl;}
};

7.可变参数模板

// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}

1.递归函数方式展开参数包

// 递归终止函数
template <class T>
void ShowList(const T& t)
{
	cout << t << endl;
}
// 展开函数
template <class T, class ...Args>
void ShowList(T value, Args... args)
{
	cout << value << " ";
	ShowList(args...);
}
int main()
{
	ShowList(1);
	ShowList(1, 'A');
	ShowList(1, 'A', std::string("sort"));
	return 0;
}

2.逗号表达式展开参数包

template <class T>
void PrintArg(T t)
{
	cout << t << " ";
}
//展开函数
template <class ...Args>
void ShowList(Args... args)
{
	int arr[] = { (PrintArg(args), 0)... };
	cout << endl;
}
int main()
{
	ShowList(1);
	ShowList(1, 'A');
	ShowList(1, 'A', std::string("sort"));
	return 0;
}

展开函数中的逗号表达式:(printarg(args), 0),也是按照这个执行顺序,先执行printarg(args),再得到逗号表达式的结果0。同时还用到了C++11的另外一个特性——初始化列表,通过初始化列表来初始化一个变长数组, {(printarg(args), 0)...}将会展开成((printarg(arg1),0),(printarg(arg2),0), (printarg(arg3),0), etc... ),最终会创建一个元素值都为0的数组

8.lamba表达式

1.语法

lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement}

  • [capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
  • (parameters): 参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略
  • mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)
  • ->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导
  • {statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。

一般不建议省略参数列表

捕捉列表: 捕捉的是外面对象的拷贝

mutable: 加上后可以修改传值过来的东西

2.lamba表达式的大小

lamba的大小是1,原因: lamba是生成仿函数的对象的类型, 并且这个仿函数是一个空类, 大小就是1个字节

lamba会被编译器处理成仿函数, 编译器会生成一个仿函数的类

lamba就相当于是仿函数, 它的所有参数都会作为哪个仿函数类的参数

注:针对每个lamba生成的类名不一样(属于不同的类)~~>不能互相赋值

3.lamba表达式的使用

--达到仿函数的作用

struct Goods
{
	string _name; ?// 名字
	double _price; // 价格
	int _evaluate; // 评价
	Goods(const char* str, double price, int evaluate)
		:_name(str)
		, _price(price)
		, _evaluate(evaluate)
	{}
};

//lamba表达式
int main()
{
	vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,
	3 }, { "菠萝", 1.5, 4 } };
	sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
		return g1._price < g2._price; });
	sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
		return g1._price > g2._price; });
}

--捕捉列表与mutable的使用 (mutable: 易变的)

int main()
{
	int x = 2, y = 3;
	//传值捕捉
	auto swap = [x,y]() mutable 
		{
			int tmp = x;
			x = y;
			y = tmp;
		};
	swap();
	cout << x << " " << y<< endl;

	//引用捕捉
	auto swap2 = [&x, &y]() 
		{
			int tmp = x;
			x = y;
			y = tmp;
		};
	swap2();
	cout << x << " " << y << endl;

	//混合捕捉
	auto func1 = [&x, y]() {};
	//全部引用捕捉
	auto func2 = [&]() {};
	//全部传值捕捉
	auto func3 = [=]() {};
	//全部引用捕捉,x传值捕捉
	auto func1 = [&,x]() {};

}

--其它

int main()
{
	// 最简单的lambda表达式, 该lambda表达式没有任何意义
	[] {};

	// 省略参数列表和返回值类型,返回值类型由编译器推导为int
	int a = 3, b = 4;
	[=] {return a + 3; };

	// 省略了返回值类型,无返回值类型
	auto fun1 = [&](int c) {b = a + c; };
	fun1(10);
	cout << a << " " << b << endl;

	// 各部分都很完善的lambda函数
	auto fun2 = [=, &b](int c)->int {return b += a + c; };
	cout << fun2(10) << endl;

	// 复制捕捉x
	int x = 10;
	auto add_x = [x](int a) mutable { x *= 2; return a + x; };
	cout << add_x(10) << endl;
	return 0;
}

总结

lambda表达式实际上可以理解为无名函数,该函数无法直接调用,如果想要直接调用,可借助auto将其赋值给一个变量。

在块作用域以外的lambda函数捕捉列表必须为空

lambda表达式之间不能相互赋值,即使看起来类型相同

其本质是一个仿函数

9.包装器

1.function包装器:?也叫作适配器。C++中的function本质是一个类模板,也是一个包装器

std::function在头文件<functional>
// 类模板原型如下
template <class T> function; ??// undefined
template <class Ret, class... Args>
class function<Ret(Args...)>;
模板参数说明:
Ret: 被调用函数的返回类型
Args…:被调用函数的形参

--使用:function可以对函数指针, 仿函数, lamba表达式进行包装, 提供统一的类型

int f(int a,int b){
	return a + b;
}

struct Functor{
public:
	int operator()(int a, int b){return a + b;}
};

int main()
{
	function<int(int, int)> f1 = f;
	function<int(int, int)> f2 = Functor();
	function<int(int, int)> f3 = [](int a, int b) {return a + b; };
	cout << f1(3,4) << endl;

	map<string, function<int(int, int)>> FuncMap;
	FuncMap["函数指针"] = f;
	FuncMap["仿函数"] = Functor();
	FuncMap["lamba表达式"] = [](int a, int b) {return a + b; };
	cout << FuncMap["lamba表达式"](3, 4) << endl;

	return 0;
}

成员函数的包装

--非静态的成员函数, 需要加上& 其参数列表有this指针, 写的时候也需要加上

--调用的时候, 加一个对象

class Plus 
{
public:
	Plus(int rate = 2)
		:_rate(rate) {}
	static int plusi(int a, int b) { return a + b; }//静态成员函数
	double plusd(int a, int b) { return (a + b) * _rate; }//非静态成员函数

private:
	int _rate = 2;
};


int main()
{
	//function<int(int, int)> f1 = &Plus::plusi;
	function<int(int, int)> f1 = Plus::plusi;//静态成员函数
	function<double(Plus,int, int)> f2 = &Plus::plusd;//非静态成员函数
	f1(3, 4);
	f2(Plus(), 3, 4);

	return 0;
}

2.bind包装器:是一个函数模板, 可以调整参数 ~~> 参数的顺序,个数

template <class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
// with return type (2)
template <class Ret, class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);

调整参数的顺序,个数(

void Print(int a, int b){
	cout << a <<" "<< b << endl;
}

int main(){
	//function<void(int, int)> f1 = bind(Print, placeholders::_2, placeholders::_1);
	auto f1 = bind(Print, placeholders::_2, placeholders::_1);
	auto f2 = bind(Print, 1,2);
	f1(10, 20); //打印20 10
    f2();       //打印1 2

	return 0;
}

绑定成员函数

class Sub {
public:
	Sub(int x = 3) {}
	int func(int a,int b){return a - b;}
};

int main()
{
	Sub s;
	function<int(Sub, int, int)> f1 = &Sub::func;
	cout << f1(Sub(), 4, 3)<< endl;

	function<int(int, int)> f2 = bind(&Sub::func, s, placeholders::_1, placeholders::_2);
	cout << f2(4,3)<< endl;

	function<int(Sub, int)> f3 = bind(&Sub::func, placeholders::_1, 100, placeholders::_2);
	cout << f3(s, 4) << endl;

	return 0;
}

3个参数, 1个参数显示的传递了

10.线程库

10.1简单介绍

函数名功能
thread()构造一个线程对象
thread(fn,args1,args2,...)构造一个线程对象,并关联线程函数fn. args1,args2,...为线程函数的参数
get_id()获取线程id
jionable()线程是否还在执行,joinable代表的是一个正在执行中的线程
jion()该函数调用后会阻塞线程,当该线程结束后,主线程继续执行
detach()在创建线程对象后马上调用,用于把被创建的线程与线程对象分离开,分离的线程变为后台线程,创建的线程的"死活"就与主线程无关

1.线程是操作系统的一个概念, 线程对象可以关联一个线程,用来控制线程以及获取线程的状态

2.当创建一个线程对象后,没有提供线程函数,该对象实际没有对应任何线程

3.?当创建一个线程对象后,并且给线程关联线程函数,该线程就被启动,与主线程一起运行。

#include<thread>
void Thread(int a) { cout << "Thread1"<<a<< endl; }
class TF {
public:
	void operator()() { cout << "Thread 3"; }
};

int main() 
{
	TF t;
	thread t1(Thread,10);//函数指针
	thread t2([]() {cout << "Thread2" << endl; });//lamba表达式
	thread t3(t);//函数对象

	t1.join(), t2.join(), t3.join();
	return 0;
}

4.thread类是防拷贝的,不允许拷贝构造以及赋值,但是可以移动构造和移动赋值,即将一个
线程对象关联线程的状态转移给其他线程对象,转移期间不影响线程的执行

5. 可以通过jionable()函数判断线程是否是有效的,如果是以下任意情况,则线程无效

  • 采用无参构造函数构造的线程对象
  • 线程对象的状态已经转移给其他线程对象
  • 线程已经调用jion或者detach结束

10.2线程函数参数

线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,因此:即使线程参数为引用类型,在
线程中修改后也不能修改外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参。? ? ?注意:如果是类成员函数作为线程参数时,必须将this作为线程函数参数

#include <thread>
void ThreadFunc1(int& x)
{
	x += 10;
}
void ThreadFunc2(int* x)
{
	*x += 10;
}
int main()
{
	int a = 10;
	// 在线程函数中对a修改,不会影响外部实参,因为:线程函数参数虽然是引用方式,但其实际引用的是线程栈中的拷贝
	thread t1(ThreadFunc1, a);
	t1.join();
	cout << a << endl;
	// 如果想要通过形参改变外部实参时,必须借助std::ref()函数
	thread t2(ThreadFunc1, std::ref(a));
	t2.join();
	cout << a << endl;
	// 地址的拷贝
	thread t3(ThreadFunc2, &a);
	t3.join();
	cout << a << endl;
	return 0;
}

10.3原子性操作库(atomic)

多线程最主要的问题是共享数据带来的问题(即线程安全)。如果共享数据都是只读的,那么没问
题,因为只读操作不会影响到数据,更不会涉及对数据的修改,所以所有线程都会获得同样的数
据。但是,当一个或多个线程要修改共享数据时,就会产生很多潜在的麻烦(可加锁解决)

#include <iostream>
using namespace std;
#include <thread>
#include <atomic>
atomic_long sum{ 0 };
void fun(size_t num)
{
	for (size_t i = 0; i < num; ++i)
		sum++; //原子操作
}

int main()
{
	cout << "Before joining, sum = " << sum << std::endl;
	thread t1(fun, 1000000);
	thread t2(fun, 1000000);
	t1.join();
	t2.join();
	cout << "After joining, sum = " << sum << std::endl;
	return 0;
}

在C++11中,程序员不需要对原子类型变量进行加锁解锁操作,线程能够对原子类型变量互斥的
访问。更为普遍的,程序员可以使用atomic类模板,定义出需要的任意原子类型。

atmoic<T> t; ??// 声明一个类型为T的原子类型变量t

注意:原子类型通常属于"资源型"数据,多个线程只能访问单个原子类型的拷贝,因此在C++11
中,原子类型只能从其模板参数中进行构造,不允许原子类型进行拷贝构造、移动构造以及
operator=等
,为了防止意外,标准库已经将atmoic模板类中的拷贝构造、移动构造、赋值运算
符重载默认删除掉了

#include <atomic>
int main()
{
	atomic<int> a1(0);
	//atomic<int> a2(a1); ?// 编译失败
	atomic<int> a2(0);
	//a2 = a1; ???????// 编译失败
	return 0;
}

10.4 lock_guard与unique_lock

10.4.1 mutex的种类

1.std::mutex

函数名功能
lock()上锁:锁住互斥量
unlock()解锁:释放对互斥量的所有权
try_lock()尝试锁住互斥量,如果互斥量被其它线程占有,则当前线程也不会被阻塞

调用lock():

  • 如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前,该线程一直拥有该锁
  • 如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住
  • 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)

调用try_lock():

  • 如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock释放互斥量
  • 如果当前互斥量被其他线程锁住,则当前调用线程返回 false,而并不会被阻塞掉
  • 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)

2. std::recursive_mutex

其允许同一个线程对互斥量多次上锁(即递归上锁),来获得对互斥量对象的多层所有权,
释放互斥量时需要调用与该锁层次深度相同次数的 unlock(),除此之外,
std::recursive_mutex 的特性和 std::mutex 大致相同。

3. std::timed_mutex
try_lock_for():
接受一个时间范围,表示在这一段时间范围之内线程如果没有获得锁则被阻塞住(与
std::mutex 的 try_lock() 不同,try_lock 如果被调用时没有获得锁则直接返回
false),如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超
时(即在指定时间内还是没有获得锁),则返回 false。
try_lock_until():
接受一个时间点作为参数,在指定时间点未到来之前线程如果没有获得锁则被阻塞住
如果在此期间其他线程释放了锁,则该线程可以获得对互斥量的锁,如果超时(即在指
定时间内还是没有获得锁),则返回 false。

4. std::recursive_timed_mutex

用于实现多线程数据同步。允许同一线程多次对锁进行加锁操作,而不会产生死锁。这意味着线程可以在不释放锁的情况下多次进入同一个互斥区域。

10.4.2 lock_guard

lock_guard类模板主要是通过RAII的方式,对其管理的互斥量进行了封装,在需要加锁的地方,只需要用上述介绍的任意互斥体实例化一个lock_guard,调用构造函数成功上锁,出作用域前,lock_guard对象要被销毁,调用析构函数自动解锁,可以有效避免死锁问题

template<class _Mutex>
class lock_guard
{
public:
	// 在构造lock_gard时,_Mtx还没有被上锁
	explicit lock_guard(_Mutex& _Mtx)
		: _MyMutex(_Mtx)
	{
		_MyMutex.lock();
	}
	// 在构造lock_gard时,_Mtx已经被上锁,此处不需要再上锁
	lock_guard(_Mutex& _Mtx, adopt_lock_t)
		: _MyMutex(_Mtx)
	{}
	~lock_guard() _NOEXCEPT
	{
		_MyMutex.unlock();
	}
	lock_guard(const lock_guard&) = delete;
	lock_guard& operator=(const lock_guard&) = delete;
private:
	_Mutex& _MyMutex;
};

lock_guard的缺陷:太单一,用户没有办法对该锁进行控制,因此C++11又提供了
unique_lock。

10.4.3 unique_lock

与lock_guard不同的是,unique_lock更加的灵活,提供了更多的成员函数:

  • 上锁/解锁操作:lock、try_lock、try_lock_for、try_lock_until和unlock
  • 修改操作:移动赋值、交换(swap:与另一个unique_lock对象互换所管理的互斥量所有权)、释放(release:返回它所管理的互斥量对象的指针,并释放所有权)
  • 获取属性:owns_lock(返回当前对象是否上了锁)、operator bool()(与owns_lock()的功能相同)、mutex(返回当前unique_lock所管理的互斥量的指针)。

11.异常

1.概念

异常是一种处理错误的方式,当一个函数发现自己无法处理的错误时就可以抛出异常,让函数的
直接或间接的调用者处理这个错误

  • throw: 当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。
  • catch: 在您想要处理问题的地方,通过异常处理程序捕获异常.catch 关键字用于捕获异常,可以有多个catch进行捕获。
  • try: try 块中的代码标识将被激活的特定异常,它后面通常跟着一个或多个 catch 块。

如果有一个块抛出一个异常,捕获异常的方法会使用?try?和?catch?关键字。try 块中放置可能抛
出异常的代码,try 块中的代码被称为保护代码。使用 try/catch 语句的语法如下所示:

try
{
?// 保护的标识代码
}catch( ExceptionName e1 )
{
?// catch 块
}catch( ExceptionName e2 )
{
?// catch 块
}catch( ExceptionName eN )
{
?// catch 块
}

2.异常的使用

2.1异常的抛出和捕获

异常的抛出和匹配原则:

  1. 异常是通过抛出对象而引发的,该对象的类型决定了应该激活哪个catch的处理代码。
  2. 被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个。
  3. 抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象,所以会生成一个拷贝对象,这个拷贝的临时对象会在被catch以后销毁。(这里的处理类似于函数的传值返回)
  4. catch(...)可以捕获任意类型的异常,问题是不知道异常错误是什么。
  5. 实际中抛出和捕获的匹配原则有个例外,并不都是类型完全匹配,可以抛出的派生类对象,使用基类捕获

???????在函数调用链中异常栈展开匹配原则:

  1. 首先检查throw本身是否在try块内部,如果是再查找匹配的catch语句。如果有匹配的,则调到catch的地方进行处理。
  2. 没有匹配的catch则退出当前函数栈,继续在调用函数的栈中进行查找匹配的catch。
  3. ?如果到达main函数的栈,依旧没有匹配的,则终止程序。上述这个沿着调用链查找匹配的catch子句的过程称为栈展开。所以实际中我们最后都要加一个catch(...)捕获任意类型的异常,否则当有异常没捕获,程序就会直接终止
  4. ?找到匹配的catch子句并处理以后,会继续沿着catch子句后面继续执行。
double Division(int a, int b)
{
	if (b == 0)
		throw "Division by zero condition!";
	else
		return ((double)a / (double)b);
}

void Func() 
{
	int x, y;
	cin >> x >> y;
	cout << Division(x, y) << endl;
}

int main() 
{
	try
	{
		Func();
	}
	catch (const char* str)
	{
		cout << str << endl;
	}
	catch (...) 
	{
		cout << "未知错误" << endl;
	}
	return 0;
}

2.2异常的重新抛出

有可能单个的catch不能完全处理一个异常,在进行一些校正处理以后,希望再交给更外层的调用
链函数来处理,catch则可以通过重新抛出将异常传递给更上层的函数进行处理

double Division(int a, int b)
{
	// 当b == 0时抛出异常
	if (b == 0)
	{
		throw "Division by zero condition!";
	}
	return (double)a / (double)b;
}
void Func()
{
	// 这里可以看到如果发生除0错误抛出异常,另外下面的array没有得到释放。
	// 所以这里捕获异常后并不处理异常,异常还是交给外面处理,这里捕获了再
	// 重新抛出去。
	int* array = new int[10];
	try {
		int len, time;
		cin >> len >> time;
		cout << Division(len, time) << endl;
	}
	catch (...)
	{
		cout << "delete []" << array << endl;
		delete[] array;
		throw;
	}
	// ...
	cout << "delete []" << array << endl;
	delete[] array;
}
int main()
{
	try
	{
		Func();
	}
	catch (const char* errmsg)
	{
		cout << errmsg << endl;
	}
	return 0;
}

2.3异常安全

  • 构造函数完成对象的构造和初始化,最好不要在构造函数中抛出异常,否则可能导致对象不完整或没有完全初始化
  • 析构函数主要完成资源的清理,最好不要在析构函数内抛出异常,否则可能导致资源泄漏(内存泄漏、句柄未关闭等)
  • C++中异常经常会导致资源泄漏的问题,比如在new和delete中抛出了异常,导致内存泄漏,在lock和unlock之间抛出了异常导致死锁,C++经常使用RAII来解决以上问题

2.4 异常规范

  1. 异常规格说明的目的是为了让函数使用者知道该函数可能抛出的异常有哪些。 可以在函数的后面接throw(类型),列出这个函数可能抛掷的所有异常类型。
  2. 函数的后面接throw(),表示函数不抛异常。
  3. 若无异常接口声明,则此函数可以抛掷任何类型的异常
// 这里表示这个函数会抛出A/B/C/D中的某种类型的异常
void fun() throw(A,B,C,D);
// 这里表示这个函数只会抛出bad_alloc的异常
void* operator new (std::size_t size) throw (std::bad_alloc);
// 这里表示这个函数不会抛出异常
void* operator delete (std::size_t size, void* ptr) throw();
// C++11 中新增的noexcept,表示不会抛异常
thread() noexcept;
thread (thread&& x) noexcept;

3.自定义异常体系

抛出的都是继承的派生类对象,捕获一个基类就可以了

(下面简单写一个封装的Exception,抛出的是基类对象,捕获的也是基类对象,根据捕获的对象打印错误信息)

class Exception 
{
public:
	Exception(int errid,const string& errmsg)
		:_errid(errid)
		,_errmsg(errmsg)
	{}

	int GetMid() const
	{
		return _errid;
	}

	const string& GetMsg() const
	{
		return _errmsg;
	}
private:
	int _errid;
	string _errmsg;
};

double Division(int a , int b)
{
	if (b == 0)
		throw Exception(1, "除0错误");
	else
		return ((double)a / (double)b);
}

void Func() 
{
	int x, y;
	cin >> x >> y;
	cout << Division(x,y)<<endl;
}

int main() 
{
	try 
	{
		Func();
	}
	catch (const Exception& e) 
	{
		cout << e.GetMsg() << endl;
	}
	catch(...) 
	{
		cout << "未知错误" << endl;
	}

	return 0;
}

抛出的都是继承的派生类对象,捕获一个基类可以处理类型不匹配的问题(避免了乱抛异常)

--抛出派生类, 使用基类捕获(天然的支持类型转换) + 多态(重写获取错误信息) 根据指向的对象来调用不同的函数(指向基类调基类的)

将基类的成员private改为protected ~~> 让子类能拿到_errmsg,_errid

继承的构造函数: 必须得调用父类的构造函数 (派生类的这几个函数, 要求必须去复用父类的)

4.异常的优缺点

异常的优点

  1. 异常对象定义好了,相比错误码的方式可以清晰准确的展示出错误的各种信息,甚至可以包含堆栈调用的信息,这样可以帮助更好的定位程序的bug
  2. 返回错误码的传统方式有个很大的问题就是,在函数调用链中,深层的函数返回了错误,那
    么我们得层层返回错误,最外层才能拿到错误
  3. 很多的第三方库都包含异常,比如boost、gtest、gmock等等常用的库,那么我们使用它们
    也需要使用异常
  4. 部分函数使用异常更好处理,比如构造函数没有返回值,不方便使用错误码方式处理。比如
    T& operator这样的函数,如果pos越界了只能使用异常或者终止程序处理,没办法通过返回
    值表示错误

异常的缺点

  1. 异常会导致程序的执行流乱跳,并且非常的混乱,并且是运行时出错抛异常就会乱跳。这会导致我们跟踪调试时以及分析程序时,比较困难。
  2. 异常会有一些性能的开销。当然在现代硬件速度很快的情况下,这个影响基本忽略不计。
  3. C++没有垃圾回收机制,资源需要自己管理。有了异常非常容易导致内存泄漏、死锁等异常安全问题。这个需要使用RAII来处理资源的管理问题。学习成本较高。
  4. C++标准库的异常体系定义得不好,导致大家各自定义各自的异常体系,非常的混乱。
  5. 异常尽量规范使用,否则后果不堪设想,随意抛异常,外层捕获的用户苦不堪言。所以异常规范有两点:一、抛出异常类型都继承自一个基类。二、函数是否抛异常、抛什么异常,都使用 func() throw();的方式规范化

12.智能指针

1.概念

智能指针:用来解决内存泄漏的问题(构造的时候获取资源,析构的时候释放资源)

template<class T>
class SmartPtr
{
public:
	//构造函数获取资源
	SmartPtr(T* ptr)
		:_ptr(ptr){}

	//析构函数释放资源
	~SmartPtr()
	{
		cout << "delete _ptr" << endl;
		delete _ptr;
	}
private:
	T* _ptr;
};

int main() 
{
	SmartPtr<int> sp1(new int);
	SmartPtr<int> sp2(new int);

	return 0;
}

要像指针一样使用:可以解引用,访问

	T& operator*() 
	{
		return *_ptr;
	}

	T* operator->() 
	{
		return _ptr;
	}

2.引发问题

1.拷贝问题:因为智能指针是浅拷贝,释放的时候导致重复释放,需要引用计数

2.线程安全问题

  • 智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时++或--,这个操作不是原子的,引用计数原来是1,++了两次,可能还是2.这样引用计数就错乱了。会导致资源未释放或者程序崩溃的问题。所以只能指针中引用计数++、--是需要加锁的,也就是说引用计数的操作是线程安全的。
  • 智能指针管理的对象存放在堆上,两个线程中同时去访问,会导致线程安全问题。

3.重复引用问题

两个或多个智能指针对象相互持有对方的引用,导致它们所指向的内存资源无法被正确释放的现象

(引用计数无法减到0)如下面链表链接情况

析构后n1和n2的计数最后为1,释放不了节点

(出了作用域,n1,n2的引用计数--,但是n1的_next管着n2,n2的_pre管着n1,计数到不了0,无法释放)

(若只有n1的_next链接n2,出了作用域n2计数--,n1计数--到0,释放n1,n1的_next释放,n2计数--到0,n2释放)

3.解决

1.1 unique_ptr:防拷贝

//防拷贝
unique_ptr(const unique_ptr<T>& up) = delete;

1.2 shared_ptr:引用计数

引用计数:每有一个指针指向这个资源的时候,计数++,当计数减少到0时,释放资源

a.计数问题:这个计数得是公共的

方案1:静态成员计数(不行)

静态成员变量属于所有对象, 多个对象可能管理多个资源,每个资源应该配对一个引用计数

方案2:在构造的时候,new一个计数,让这些对象指向资源也指向对象

template<class T>
class shared_ptr
{
private:
	T* _ptr;
	int* _pcount;
};

c.设计思想

1.引用计数

  • 拷贝构造:引用计数++
  • 析构函数:引用计数--
  • 赋值运算符:不给自己赋值, 释放之前资源(被赋值计数--), 赋值, 赋值计数++

2.线程安全:加锁保护++,--

3.重复引用:weak_ptr不提供计数

namespace code
{
	template<class T>
	class shared_ptr
	{
	public:
		//构造函数获取资源,析构函数释放资源
		shared_ptr(T* ptr = nullptr)
			:_ptr(ptr)
			,_pcount(new int(1))
			,_pmutex(new mutex)
		{}

		void AddRef()
		{
			_pmutex.lock();
			++(*_pcount);
			_pmutex.unlock();
		}

		void Release()
		{
			_pmutex.lock();
			bool DeleteMutex = false;

			if(--(*_pcount) == 0 && _ptr)
			{
				delete _ptr;
				delete _pcount;
				DeleteMutex = true;
			}
			_pmutex.unlock();

			if(DeleteMutex)
			{
				delete _pmutex;
			}
		}

		~shared_ptr()
		{
			Release();
		}
		//2.像指针一样使用
		T& operator*()
		{
			return *_ptr;
		}

		T* operator->()
		{
			return _ptr;
		}

		T* get()const
		{
			return _ptr;
		}

		//3.引用计数解决拷贝构造问题
		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			,_pcount(sp._pcount)
			,_pmutex(sp._pmutex)
		{
			//拷贝 + 计数++
			AddRef();
		}

		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			//不给自己赋值
			if(_ptr != sp._ptr)
			{
				//释放资源
				Release();
				//赋值
				_ptr = sp._ptr;
				_pcount = sp._pcount;
				_pmutex = sp._pmutex;
				//计数++
				AddRef();
			}
			return *this;
		}

		//4.weak_ptr解决重复引用问题
		template<class T>
		class weak_ptr
		{
		public:
			weak_ptr()
				:_ptr(nullptr)
			{}

			weak_ptr(const shared_ptr<T>& sp)
				:_ptr(sp.get())
			{}

			weak_ptr<T>& operator=(const shared_ptr<T>& sp)
			{
				_ptr = sp.get();
				return *this;
			}

			T& operator*()
			{
				return *_ptr;
			}

			T* operator->()
			{
				return _ptr;
			}

		private:
			T* _ptr;
		};

	private:
		T* _ptr;
		int* _pcount;
		mutex _pmutex;
	};
}

文章来源:https://blog.csdn.net/m0_70402487/article/details/134835752
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。