7. C++11
范围for
使用格式
vector<int> v = { 1,2,3,4,5 };
for (auto e : v)
{cout << e << " ";
}
cout << endl;
底层原理,使用迭代器
vector<int> v = { 1,2,3,4,5 };
auto it = v.begin();
while (it != v.end())
{cout << *it << " ";++it;
}
cout << endl;
右值引用的移动语义
什么是左值,什么又是右值?
左值,可以取地址,可以对内容进行修改
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;
}
右值,不可以取地址,不可以对内容进行修改,例如:临时变量
- 函数返回值(不能是左值引用返回)
- 表达式返回值
- 匿名对象
- 字面常量
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;
}
什么是左值引用,什么是右值引用?
无论左值引用还是右值引用,都是给对象取别名
需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址
也就是说例如:不能取字面量10的地址,但是rr1引用后,可以对rr1取地址,也可以修改rr1。如果不想rr1被修改,可以用const int&& rr1 去引用
把字面量存储在了栈上,后面使用时取地址就是栈空间上的地址,修改也是
这是一种特性
- 左值引用只能引用左值,不能引用右值
- 但是const左值引用既可引用左值,也可引用右值
- 右值引用只能右值,不能引用左值
- 但是右值引用可以move以后的左值
当需要用右值引用引用一个左值时,可以通过move函数将左值转化为右值,move本质是一个函数,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义
右值引用的使用场景,如何提高效率
左值引用的使用场景:做参数和做返回值都可以提高效率。(对于自定义类型)
左值引用的短板:但是当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回, 只能传值返回
例如: string operator+(const string& str)
传值返回就会导致,两次拷贝构造,在编译器优化后可能会优化掉第一次的拷贝构造,但是返回值做为参数的拷贝构造是少不了的
这个返回值是一个临时对象,也就是右值,将亡值,这个临时对象在拷贝之后也是要析构的,可不可以利用一下这个资源呢?
通过右值引用和移动语义解决上述问题
移动构造和移动赋值
在bit::string中增加移动构造,移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己。
不仅仅有移动构造,还有移动赋值
namespace kele
{class string{public:string(const char* str = "")//构造:_size(strlen(str)){cout << "构造" << endl;_capacity = _size == 0 ? 3 : _size;_str = new char[_capacity + 1];strcpy(_str, str);}string(const string& str)//拷贝构造:_str(nullptr){cout << "string(const string & str)//拷贝构造" << endl;string tmp(str._str);swap(tmp);}string(string&& str)//移动构造:_str(nullptr),_size(0),_capacity(0){cout << "string(string&& str)//移动构造" << endl;swap(str);}string& operator=(string& str)//赋值重载{if (this != &str){cout << "string& operator=(string str)//赋值重载" << endl;string tmp(str._str);swap(tmp);return *this;}}string& operator=(string&& str)//移动赋值重载{if (this != &str){cout << "string& operator=(string&& str)//移动赋值重载" << endl;swap(str);return *this;}}void reserve(size_t n = 0){if (n > _capacity){char* tmp = new char[n + 1];strcpy(tmp, _str);delete[] _str;_str = tmp;_capacity = n;}}string& operator+=(const string& s){size_t n = s._size;if (_size + n > _capacity){reserve(_capacity + n);}strcpy(_str + _size, s._str);_size += n;return *this;}string operator+(const string& str){string tmp(_str);tmp += str;return tmp;}~string(){delete[] _str;_size = _capacity = 0;}void swap(string& s){std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);}private:char* _str;size_t _size;size_t _capacity;};
}
完美转发
模板中的&& 万能引用:
模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值
模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力
但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值
因为如果要进行移动构造需要对其内容做修改(必须是左值)
我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发(forward)
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; }template<typename T>
void PerfectForward(T&& t)
{//Fun(t);Fun(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;
}
push和emplace系列的区别
// vector
void push_back (const value_type& val); // 拷贝构造对象插入
void push_back (value_type&& val); // 移动构造(构造 + 交换)对象插入
template <class... Args>
void emplace_back (Args&&... args); // 构造对象插入
利用模板可变参数和万能引用拿到构造对象所需的参数,再调用对应的对象构造函数
虽然相对于push_back而言,emplace_back有效率上的提升,但是移动拷贝的代价足够小,所有提升不大,但是对于一些包含大量内置类型的类还是有不错的提升的
对于深拷贝的类,移动构造足够快,但是对于浅拷贝的类,emplace更有优势
lambda
lambda表达式书写格式:
[capture-list] (parameters) mutable ->return-type { statement }
捕捉列表 参数列表 可变性 返回值类型 函数体
auto sub_lambda = [](int x, int y) {return x - y; };
- [capture-list] : 捕捉列表该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda 函数使用
- (parameters):参数列表与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略
- mutable:可变性默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)
- ->returntype:返回值类型用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导
- {statement}:函数体在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量
注意: 在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。
因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情。
lambda表达式实际上可以理解为无名函数(匿名),该函数无法直接调用,如果想要直接调用,可借助auto将其赋值给一个变量。
捕获列表说明:
捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用
- [var]:表示值传递方式捕捉变量var
- [=]:表示值传递方式捕获所有父作用域中的变量(包括this)
- [&var]:表示引用传递捕捉变量var
- [&]:表示引用传递捕捉所有父作用域中的变量(包括this)
- [this]:表示值传递方式捕捉当前的this指针
注意:
- 父作用域指包含lambda函数的语句块
- 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。
- 捕捉列表不允许变量重复传递,否则就会导致编译错误。
- 在块作用域以外的lambda函数捕捉列表必须为空。
- 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错
- lambda表达式之间不能相互赋值,即使看起来类型相同
class
int __cdecl main(void)'::
2’::<lambda_1> lambda表达式类型
实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()
function/bind(了解)
function包装器也叫作适配器。C++中的function本质是一个类模板,也是一个包装器
函数指针的类型非常的难用
仿函数又太复杂了,麻烦
lambda是匿名对象,没法显示类型
这些都各有弊端,而且因为是不同的类型,当类模板接收这些类型,还需要实例化不同的几份,所有,能不能把他们都包装一下,形成统一的类型呢?
std::function在头文件中
template <class Ret, class... Args>
class function<Ret(Args...)>;
模板参数说明: Ret: 被调用函数的返回类型 Args…:被调用函数的形参
typedef function<int(int,int)> func_t;
// 等价于
using func_t = function<int(int,int)>;
例子:
#include <functional>
#include <string>
#include <map>typedef function<int(int, int)> func_t;typedef int(*func_ptr)(int, int);int sub_fun(int x, int y)
{return x - y;
}class sub_class
{
public:int operator()(int x, int y){return x - y;}
};int main()
{auto sub_lambda = [](int x, int y) {return x - y; };func_t sub[] = { sub_fun, sub_class(), sub_lambda };cout << sub[0](1, 3) << endl;cout << sub[1](1, 3) << endl;cout << sub[2](1, 3) << endl;map<string, func_t> m = {{"函数指针", sub_fun },{"仿函数", sub_class() },{"lambda表达式", sub_lambda },};cout << m["函数指针"](1,3);cout << m["仿函数"](1,3);cout << m["lambda表达式"](1,3);return 0;
}
function包装类内的成员函数
对于静态成员函数
function<int(int, int)> f1 = sub_class::static_sub;
对于普通成员函数
function<int(sub_class*, int, int)> f1 = &sub_class::sub;
// 等价于
function<int(sub_class, int, int)> f2 = &sub_class::sub;
因为普通成员函数参数的第一个是this指针,所以要有class*,也可以用class(为了方便bind)
拓展知识:
- 对于非静态成员函数,函数指针的赋值需要使用&
- 使用 .* (实例对象)或者 ->*(实例对象指针)调用类成员函数指针所指向的函数
非静态成员函数指针可以不传this的原因是,使用时必须实例化
对于虚函数,编译时函数指针的地址是未定义的(待实例化确定g++)
拓展中的拓展:
在类内的成员变量也可以当作函数,返回值是变量,参数是类,可以function和bind,实在是想不出使用场景[笑哭]
class sub_class
{
public:static int _sub(int x, int y){return x - y;}int sub(int x, int y){return x - y;}
};int main()
{function<int(int, int)> f1 = &sub_class::_sub;// 对于类静态成员函数,需要指定类域,可以不加&cout << f1(1, 3) << endl;function<int(sub_class*, int, int)> f2 = &sub_class::sub;// 对于成员函数,隐藏了this指针,而且需要加&符号sub_class pc;cout << f2(&pc, 1, 3) << endl;// 匿名对象属于临时变量,右值不能取地址function<int(sub_class, int, int)> f3 = &sub_class::sub;// 对于成员函数,隐藏了this指针,而且需要加&符号cout << f3(sub_class(), 1, 3) << endl;// 编译器的特殊处理,传类型,其实如果成员函数使用了类内成员是临时变量的// 绑定bind// 可以改变参数的位置,也可以改变参数的数量,通过写死function<int(int, int)> f4 = bind(&sub_class::sub, sub_class(), placeholders::_1, placeholders::_2);cout << f4(1, 3) << endl;return 0;
}
bind
std::bind函数定义在头文件中,是一个函数模板,它就像一个函数包装器(适配器),接受一个可调用对象(callable object),生成一个新的可调用对象来“适应”原对象的参数列表
一般而言,我们用它可以把一个原本接收N个参数的函数fn,通过绑定一些参数,返回一个接收M个(M 可以大于N,但这么做没什么意义)参数的新函数。同时,使用std::bind函数还可以实现参数顺序调整等操作
相当于对函数进行封装,来调整参数的位置顺序,也可以通过写死一些参数,减少参数个数
// 原型如下:
template <class Fn, class... Args>
bind (Fn&& fn, Args&&... args); /* unspecified */
// with return type (2)
template <class Ret, class Fn, class... Args>
bind (Fn&& fn, Args&&... args); /* unspecified */
使用格式:
auto newCallable = bind(callable,arg_list)
newCallable本身是一个可调用对象,arg_list是一个逗号分隔的参数列表,对应给定的 callable的参数。当我们调用newCallable时,newCallable会调用callable,并传给它arg_list中的参数
arg_list中的参数可能包含形如n的名字,其中n是一个整数,这些参数是“占位符”,表示 newCallable的参数,它们占据了传递给newCallable的参数的“位置”。数值n表示生成的可调用对 象中参数的位置:_1为newCallable的第一个参数,_2为第二个参数,以此类推。
void func(int a, int b, int c)
{cout << a << endl;cout << b << endl;cout << c << endl;
}int main()
{function<void(int, int, int)> f1 = bind(func, placeholders::_1, placeholders::_2, placeholders::_3);f1(1, 2, 3);function<void(int, int)> f2 = bind(func, placeholders::_1, 2, placeholders::_2);// placeholders的参数数字对应的是函数调用时要填入参数的顺序和个数f2(1, 3);return 0;
}
线程库
thread
在C++11之前,涉及到多线程问题,都是和平台相关的,比如windows和linux下各有自己的接 口,这使得代码的可移植性比较差。C++11中最重要的特性就是对线程进行支持了,使得C++在 并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类的概念。要使用标准库中的 线程,必须包含< thread >头文件
函数名 | 功能 |
---|---|
thread() | 构造一个线程对象,没有关联任何线程函数,即没有启动任何线程 |
thread(fn, args1, args2, …) | 构造一个线程对象,并关联线程函数fn,args1,args2,…为线程函数的参数 |
jion() | 该函数调用后会阻塞住线程,当该线程结束后,主线程继续执行 |
detach() | 在创建线程对象后马上调用,用于把被创建线程与线程对象分离开,分离的线程变为后台线程,创建的线程的"死活"就与主线程无关 |
get_id() | 获取线程id |
jionable() | 线程是否还在执行,joinable代表的是一个正在执行中的线程 |
-
当创建一个线程对象后,没有提供线程函数,该对象实际没有对应任何线程
-
线程对象可以关联一个线程,用来控制线程以及获取线程的状态
-
当创建一个线程对象后,并且给线程关联线程函数,该线程就被启动,与主线程一起运行
-
线程函数一般情况下可按照以下三种方式提供
-
函数指针
-
lambda表达式
-
函数对象
-
-
thread类是防拷贝的,不允许拷贝构造以及赋值,但是可以移动构造和移动赋值,即将一个线程对象关联线程的状态转移给其他线程对象,转移期间不影响线程的执行
-
可以通过jionable()函数判断线程是否是有效的,如果是以下任意情况,则线程无效
- 采用无参构造函数构造的线程对象
- 线程对象的状态已经转移给其他线程对象
- 线程已经调用jion或者detach结束
线程函数参数
线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的
因此:即使线程参数为引用类型,在线程中修改后也不能修改外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参
两种解决方式:1. std::ref(参数) 2. 传地址
注意:如果是类成员函数作为线程参数时,必须将this作为线程函数参数
在线程内获取线程id,this_thread::get_id();
多线程最主要的问题是共享数据带来的问题(即线程安全),下面是解决线程安全的工具
atomic
头文件
C++11中引入了原子操作,程序员不需要对原子类型变量进行加锁解锁操作,线程能够对原子类型变量互斥的访问
以使用atomic类模板,定义出需要的任意原子类型,只适用于整形类型(bool类型除外),提供++ – 操作
不允许原子类型进行拷贝构造、移动构造以及 operator=
mutex
头文件
C++11提供的最基本的互斥量,该类的对象之间不能拷贝,也不能进行移动。mutex最常用的三个函数
函数名 | 函数功能 |
---|---|
lock() | 上锁:锁住互斥量 |
unlock() | 解锁:释放对互斥量的所有权 |
try_lock() | 尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会被阻塞 |
拓展:
std::recursive_mutex :其允许同一个线程对互斥量多次上锁(即递归上锁),来获得对互斥量对象的多层所有权, 释放互斥量时需要调用与该锁层次深度相同次数的 unlock()
手动释放锁,经常容易导致忘记解锁而死锁,或者因为抛异常可能会导致锁没有释放,又或者有多个出作用域的地方每个地方都要手动释放锁比较麻烦,所以就有了RAII(资源获得即初始化)思想封装的guard系列锁
lock_guard
lock_guard类模板主要是通过RAII的方式,对其管理的互斥量进行了封装,在需要加锁的地方,只需要用上述介绍的任意互斥体实例化一个lock_guard,调用构造函数成功上锁,出作用域前,lock_guard对象要被销毁,调用析构函数自动解锁,可以有效避免死锁问题
lock_guard的缺陷:太单一,用户没有办法对该锁进行控制
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所管理的互斥量的指针)
condition_variable
条件变量属于资源,只能构造
函数 | 函数功能 |
---|---|
void wait(unique_lock& lck) | 在条件变量的等待队列中睡眠,并且释放锁 |
void notify_one() | 从条件变量的等待队列中唤醒一个线程 |
void notify_all() | 广播,唤醒条件变量的等待队列中的所有线程 |
练习题:
支持两个线程交替打印,一个打印奇数,一个打印偶数
#include <thread>
#include <mutex>
#include <condition_variable>
void two_thread_print()
{std::mutex mtx;condition_variable c;int n = 100;bool flag = true;thread t1([&]() {int i = 0;while (i < n){unique_lock<mutex> lock(mtx);c.wait(lock, [&]()->bool {return flag; });cout << i << endl;flag = false;i += 2; // 偶数c.notify_one();}});thread t2([&]() {int j = 1;while (j < n){unique_lock<mutex> lock(mtx);c.wait(lock, [&]()->bool {return !flag; });cout << j << endl;j += 2; // 奇数flag = true;c.notify_one();}});t1.join();t2.join();
}
int main()
{two_thread_print();return 0;
}
STL中的一些变化
容器中新增了<unordered_map><unordered_set>
std::initializer_list,统一列表初始化
移动构造
emplace系列
线程库<condition_variable>
包装器
8. 异常(考察少)
用法
异常是一种处理错误的方式,当一个函数发现自己无法处理的错误时就可以抛出异常,让函数的直接或间接的调用者处理这个错误
throw抛异常 try catch对异常进行捕获
try 块中放置可能抛出异常的代码,try 块中的代码被称为保护代码
异常的抛出和匹配原则
- 异常是通过抛出对象而引发的,该对象的类型决定了应该激活哪个catch的处理代码
- 被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个
- 抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象, 所以会生成一个拷贝对象,这个拷贝的临时对象会在被catch以后销毁。(这里的处理类似于函数的传值返回)
- catch(…)可以捕获任意类型的异常,问题是不知道异常错误是什么
- 实际中抛出和捕获的匹配原则有个例外,并不都是类型完全匹配,可以抛出的派生类对象, 使用基类捕获,这个在实际中非常实用
在函数调用链中异常栈展开匹配原则
- 首先检查throw本身是否在try块内部,如果是再查找匹配的catch语句。如果有匹配的,则调到catch的地方进行处理。
- 没有匹配的catch则退出当前函数栈,继续在调用函数的栈中进行查找匹配的catch。
- 如果到达main函数的栈,依旧没有匹配的,则终止程序。上述这个沿着调用链查找匹配的 catch子句的过程称为栈展开。所以实际中我们最后都要加一个catch(…)捕获任意类型的异常,否则当有异常没捕获,程序就会直接终止。
- 找到匹配的catch子句并处理以后,会继续沿着catch子句后面继续执行
异常的重新抛出 在catch块内部最后throw就可以继续向外抛出
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;
}
异常规范
- 异常规格说明的目的是为了让函数使用者知道该函数可能抛出的异常有哪些。 可以在函数的后面接throw(类型),列出这个函数可能抛掷的所有异常类型
- 函数的后面接throw(),表示函数不抛异常。
- 若无异常接口声明,则此函数可以抛掷任何类型的异常
// 这里表示这个函数会抛出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;
throw()规范是给程序员看的,给调用接口的人知道会抛什么类型的异常,不强制不影响程序运行
但是如果是C++11的关键字noexcept,表示函数不会抛异常,如果函数内部运行时抛异常了,就会直接崩溃
自定义异常体系
实际使用中很多公司都会自定义自己的异常体系进行规范的异常管理,因为一个项目中如果大家 随意抛异常,那么外层的调用者基本就没办法玩了,所以实际中都会定义一套继承的规范体系。 这样大家抛出的都是继承的派生类对象,捕获一个基类就可以了
利用的是类的继承,派生类重写基类的成员函数,再利用基础对象引用接受,利用的还是多态
// 传派生类,基类接受,利用多态
class Exception
{
public:Exception(size_t errid, const string& errmsg):_errid(errid),_errmsg(errmsg){}virtual string what() const{return _errmsg;}
protected:size_t _errid;string _errmsg;
};class Sql_Exception : public Exception
{
public:Sql_Exception(size_t id, const string& errmsg, const string& sql):Exception(id,errmsg),_sql(sql){}virtual string what() const{string str;str += _errmsg;str += " : ";str += _sql;return str;}private:string _sql;
};int main()
{try {Sql_Exception p(1, "写错了", "qeqawr");throw p;}catch (Exception& str){cout << str.what() << endl;}return 0;
}
C++ 提供了一系列标准的异常,定义在中,我们可以在程序中使用这些标准的异常。它们是以父 子类层次结构组织起来的
异常的优缺点
异常安全问题
- 构造函数完成对象的构造和初始化,最好不要在构造函数中抛出异常,否则可能导致对象不完整或没有完全初始化
- 析构函数主要完成资源的清理,最好不要在析构函数内抛出异常,否则可能导致资源泄漏(内 存泄漏、句柄未关闭等)
- C++中异常经常会导致资源泄漏的问题,比如在new和delete中抛出了异常,导致内存泄漏,在lock和unlock之间抛出了异常导致死锁,C++经常使用RAII来解决以上问题,关于RAII我们智能指针这节进行讲解
异常的优点:
-
异常对象定义好了,相比错误码的方式可以清晰准确的展示出错误的各种信息,甚至可以包含堆栈调用的信息,这样可以帮助更好的定位程序的bug
-
返回错误码的传统方式有个很大的问题就是,在函数调用链中,深层的函数返回了错误,那么我们得层层返回错误,最外层才能拿到错误,而抛异常会直接跳到捕获他的位置
-
很多的第三方库都包含异常,比如boost、gtest、gmock等等常用的库,那么我们使用它们也需要使用异常
-
部分函数使用异常更好处理,比如构造函数没有返回值,不方便使用错误码方式处理。比如 T& operator[]这样的函数,如果pos越界了只能使用异常或者终止程序处理,没办法通过返回值表示错误
异常的缺点:
-
异常会导致程序的执行流乱跳,并且非常的混乱,并且是运行时出错抛异常就会乱跳。这会导致我们跟踪调试时以及分析程序时,比较困难
-
异常会有一些性能的开销。当然在现代硬件速度很快的情况下,这个影响基本忽略不计
-
C++没有垃圾回收机制,资源需要自己管理。有了异常非常容易导致内存泄漏、死锁等异常安全问题。这个需要使用RAII来处理资源的管理问题。学习成本较高
-
C++标准库的异常体系定义得不好,导致大家各自定义各自的异常体系,非常的混乱
-
异常尽量规范使用,否则后果不堪设想,随意抛异常,外层捕获的用户苦不堪言。所以异常规范有两点:一、抛出异常类型都继承自一个基类。二、函数是否抛异常、抛什么异常,都使用 func() throw();的方式规范化
总结:异常总体而言,利大于弊,所以工程中我们还是鼓励使用异常的。另外OO的语言基本都是用异常处理错误,这也可以看出这是大势所趋。
9. 智能指针
发展历史
在C++98中,auto_ptr管理权转移的思想,被诟病auto_ptr导致原对象悬空
C++11带来了
- unique_ptr(不让拷贝和赋值)
- shared_ptr(通过引用计数的方式来实现多个shared_ptr对象之间共享资源)
- weak_ptr(解决shared_ptr造成的循环引用问题)
- C++ 98 中产生了第一个智能指针auto_ptr.
- C++ boost给出了更实用的scoped_ptr和shared_ptr和weak_ptr.
- C++ TR1,引入了shared_ptr等。不过注意的是TR1并不是标准版。
- C++ 11,引入了unique_ptr和shared_ptr和weak_ptr。需要注意的是unique_ptr对应boost 的scoped_ptr。并且这些智能指针的实现原理是参考boost中的实现的
RAII
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。
这种做法有两大好处
- 不需要显式地释放资源
- 采用这种方式,对象所需的资源在其生命期内始终保持有效
auto_ptr/unique_ptr/shared_ptr/weak_ptr特点是什么?解决了什么问题?
智能指针的原理
- RAII特性
- 重载operator*和opertaor->,具有像指针一样的行为
auto_ptr、unique_ptr、shared_ptr和weak_ptr是C++中用于管理动态分配内存的几种智能指针,它们各自具有不同的特点和用途,主要解决了动态内存管理中的内存泄漏、悬空指针、内存访问错误和异常安全性等问题。
1. auto_ptr(C++98引入,C++11废弃)
特点:
- 独占所有权:auto_ptr 实现了独占式所有权,即一个 auto_ptr 对象拥有对动态分配对象的唯一所有权。
- 所有权转移:当 auto_ptr 被拷贝或被赋值时,会发生所有权的转移,原 auto_ptr 将失去对对象的所有权。
- 不支持数组:auto_ptr 不支持管理动态分配的数组,因为其析构函数中调用的是 delete 而不是 delete[]。
解决的问题:
- 一定程度上解决了内存泄漏的问题,通过所有权转移和自动析构来释放内存。
- 但由于存在所有权转移的特性,使得它在容器和复杂数据结构中的使用变得复杂且不安全,因此最终被 C++11 废弃。
2. unique_ptr(C++11引入)
特点:
- 独占所有权:与 auto_ptr 类似,unique_ptr 也实现了独占式所有权。
- 禁止拷贝:但 unique_ptr 禁止了拷贝构造和拷贝赋值操作,只能通过移动语义来转移所有权。
- 支持自定义删除器:unique_ptr 允许指定一个自定义的删除器,以便在销毁对象时执行特定的清理操作。
解决的问题:
- 彻底解决了 auto_ptr 在所有权转移方面的问题,使得 unique_ptr 在容器和复杂数据结构中的使用更加安全和方便。
- 提供了更严格的内存管理机制,减少了内存泄漏和悬空指针的风险。
3. shared_ptr(C++11引入)
特点:
- 共享所有权:多个 shared_ptr 可以共享同一个动态分配的对象,通过引用计数来管理对象的生命周期。
- 自动资源释放:当最后一个 shared_ptr 被销毁或重置时,对象会被自动释放。
- 可定制删除器:与 unique_ptr 类似,shared_ptr 也允许指定自定义的删除器。
解决的问题:
- 解决了多个对象需要共享同一块内存资源的问题,通过引用计数来确保资源在不再需要时得到释放。
- 提高了代码的可靠性和可维护性,减少了内存泄漏和悬空指针的风险。
4. weak_ptr(C++11引入)
特点:
- 弱引用:weak_ptr 是一种对 shared_ptr 所管理对象的弱引用,它不会增加对象的引用计数。
- 避免循环引用:weak_ptr 主要用于解决 shared_ptr 之间的循环引用问题,通过弱引用来打破循环,确保对象在不再需要时能够被正确释放。
- 需要转换:weak_ptr 不直接管理对象,它需要通过 lock 方法转换成 shared_ptr 才能访问对象。
解决的问题:
- 解决了 shared_ptr 之间的循环引用导致的内存泄漏问题。
- 提供了对共享资源的非拥有性观察接口,使得在某些情况下可以安全地访问资源而不影响资源的生命周期。
模拟实现
#pragma once
#include <functional>
#include <atomic>// auto_ptr
namespace kele
{template<class T>class auto_ptr{public:auto_ptr(T* ptr):_ptr(ptr){}auto_ptr(auto_ptr<T>& aptr):_ptr(aptr._ptr){aptr._ptr = nullptr;}auto_ptr<T>& operator=(auto_ptr<T>& aptr){if (this != &aptr){_ptr = aptr._ptr;aptr._ptr = nullptr;}return *this;}~auto_ptr(){delete _ptr;}T& operator*() { return *_ptr; }T* operator->() { return _ptr; }private:T* _ptr;};template<class T>class unique_ptr{typedef unique_ptr<T> self_ptr;public:unique_ptr(T* ptr) :_ptr(ptr){}template<class del>unique_ptr(T* ptr, del delfunc) :_ptr(ptr), _del(delfunc){}unique_ptr(self_ptr& aptr) = delete;self_ptr& operator=(self_ptr& aptr) = delete;unique_ptr(self_ptr&& aptr):_ptr(nullptr){swap(_ptr, aptr._ptr);swap(_del, aptr._del);}self_ptr& operator=(self_ptr&& aptr){if (this != &aptr){swap(_ptr, aptr._ptr);swap(_del, aptr._del);}return *this;}~unique_ptr(){_del(_ptr);}T& operator*() { return *_ptr; }T* operator->() { return _ptr; }private:T* _ptr;std::function<void(T*)> _del = [](T* ptr) {//if(ptr != nullptr)delete ptr; };};template<class T>class shared_ptr{typedef shared_ptr<T> self_ptr;public:shared_ptr(T* ptr) :_ptr(ptr), _ref_count(new std::atomic<size_t>(1)){}// 对于lambda表达式的接收就不能用引用template<class del>shared_ptr(T* ptr, del delfunc) : _ptr(ptr), _del(delfunc),_ref_count(new std::atomic<size_t>(1)){}shared_ptr(self_ptr& aptr):_ptr(aptr._ptr), _del(aptr._del), _ref_count(aptr._ref_count){(*_ref_count)++;}self_ptr& operator=(self_ptr& aptr){if (this != &aptr){_ptr = aptr._ptr;_del = aptr._del;_ref_count = aptr._ref_count;(*_ref_count)++;}return *this;}shared_ptr(self_ptr&& aptr):_ptr(nullptr), _ref_count(new std::atomic<size_t>(1)){swap(_ptr, aptr._ptr);swap(_del, aptr._del);swap(_ref_count, _ref_count);}self_ptr& operator=(self_ptr&& aptr){if (this != &aptr){swap(_ptr, aptr._ptr);swap(_del, aptr._del);swap(_ref_count, _ref_count);}return *this;}~shared_ptr(){(*_ref_count)--;if(*_ref_count == 0)_del(_ptr);}T& operator*(){return *_ptr;}T& operator*() { return *_ptr; }T* operator->() { return _ptr; }private:T* _ptr;// 定制删除器std::function<void(T*)> _del = [](T* ptr) {//if(ptr != nullptr)cout << "delete" << endl;delete ptr;};std::atomic<size_t>* _ref_count; // 引用计数};template<class T>class weak_ptr{typedef weak_ptr<T> self_ptr;public:weak_ptr(T* ptr) :_ptr(ptr){}weak_ptr(self_ptr& aptr):_ptr(aptr._ptr){}weak_ptr(shared_ptr<T>& aptr):_ptr(aptr.get()){}self_ptr& operator=(self_ptr& aptr){if (this != &aptr){_ptr = aptr._ptr;}return *this;}self_ptr& operator=(shared_ptr<T>& aptr){_ptr = aptr.get();return *this;}~weak_ptr(){cout << "weak delete" << endl;}T& operator*() { return *_ptr;}T* operator->(){ return _ptr;}private:T* _ptr;};
}
什么是循环引用?怎么解决?
因为节点内部的两个shared_ptr引用计数变成了2,在delete的时候,引用计数不会减到0,导致内存泄漏
定制删除器(了解)
auto_ptr 对于析构函数是用delete方式删除,但是对于需要delete[]的类型怎么办?对于不是在堆上开辟的空间,怎么办?
例如:文件描述符类型,资源
所以针对不同的使用场景就需要不同的删除器,由使用者提供,可以是lambda,函数指针,仿函数
对于不同的类型可以使用function包装器统一封装
注意lambda不能用&传参,lambda表达式是匿名函数,属于右值
拓展思考:
shared_ptr的引用计数的增减都是原子的线程安全的,但是shared_ptr指向的资源并不是线程安全的,需要加锁
对于循环引用,这样的场景下,如果有别的weak_ptr指向一个已经被delete的对象(由于shared_ptr管理的引用计数为0),这会导致野指针问题,weak_ptr还不知道是不是野指针,无法判断,这么搞?
思考方案:shared_ptr开两个引用计数,一个属于shared_ptr的引用计数shared_ref,另一个属于weak_ptr的弱引用计数weak_ref,如果shared_ptr对象delete,shared_ref–,如果weak_ptr对象delete,weak_ref–。如果shared_ref为0,weak_ref不等于0,空间不释放,把对象交给weak_ptr管理,shared_ref和weak_ref都等于0释放空间
10. 类型转换(考察少)
C语言的强制类型转换,关闭或挂起了正常的类型检查,所以很不安全,建议避免使用强制类型转换,建议是用C++的
四种类型转换
- static_cast(静态强制转化)
- reinterpret_cast(重译强制转化)
- const_cast(常量强制转化)
- dynamic_cast(动态强制转化)
使用场景
static_cast(相关类型的转换)
用于非多态类型的转换(静态转换),编译器隐式执行的任何类型转换都可用 static_cast
但它不能用于两个不相关的类型进行转换
double d = 12.34;
int a = static_cast<int>(d);
cout<<a<<endl;
reinterpret_cast(不同类型的转换)
操作符通常为操作数的位模式提供较低层次的重新解释,用于将一种类型转换为另一种不同的类型
int a = 12;
// 这里使用static_cast会报错,应该使用reinterpret_cast
//int *p = static_cast<int*>(a);
int* p = reinterpret_cast<int*>(a);
cout << p << endl;
const_cast(const关系的改变)
最常用的用途就是删除变量的const属性,方便赋值
const int a = 2;
int* p = const_cast< int*>(&a );
*p = 3;
cout<<a <<endl;
dynamic_cast(多态类型基类到父类的转换)
dynamic_cast用于将一个父类对象的指针/引用转换为子类对象的指针或引用(动态转换)
向上转型:子类对象指针/引用->父类指针/引用(不需要转换,赋值兼容规则)
向下转型:父类对象指针/引用->子类指针/引用(用dynamic_cast转型是安全的)
注意:
-
dynamic_cast只能用于父类含有虚函数的类
原因:dynamic_cast是运行时类型检查,需要运行时类型信息(RTTI),而这个信息是存储与类的虚函数表关系紧密,只有一个类定义了虚函数,才会有虚函数表
-
dynamic_cast会先检查是否能转换成功,能成功则转换,不能则返回0
class A
{
public:virtual void f() {}
};
class B : public A
{};
void fun(A* pa)
{// dynamic_cast会先检查是否能转换成功,能成功则转换,不能则返回B* pb1 = static_cast<B*>(pa);B* pb2 = dynamic_cast<B*>(pa);cout << "pb1:" << pb1 << endl;cout << "pb2:" << pb2 << endl;
}
int main()
{A a;B b;fun(&a);fun(&b);return 0;
}
pb1:010FFBD8
pb2:00000000
pb1:010FFBCC
pb2:010FFBCC
RTTI(了解)
RTTI:(Run-time Type identification)的简称,即:运行时类型识别。
C++通过以下方式来支持RTTI:
- typeid运算符
- dynamic_cast运算符
- decltype
11. IO流(考察少)
接口
功能对应关系
C | C++ |
---|---|
scanf/printf | istream/ostream/iostream |
fscanf/fprintf | ifstream/ofstream/fstream |
sscanf/sprintf | istringstream/ostringstream/stringstream |
fstream(文件操作)
函数名 | 作用 |
---|---|
构造 | 创建空对象/打开文件 |
析构 | 如果打开了文件关闭 |
open/close | 打开文件,指定文打开方式/关闭文件 |
read/write | 读/写 |
operator>>/operator<< | 如果类型重写了>>和<<,可以读/写 |
seekg/seekp | 设置读写指针的位置 |
flush | 刷新缓冲区 |
stringstream(字符串操作)
和上面的接口类似,就是没有open和close,多了一个str()返回c字符串,getline()
库的意义
- 面向对象
- 支持自定义类型的流插入和流提取
利用继承多态
完