第一章 使用C++11让程序更加简洁、更现代
可调用对象
函数指针
void functionPointer()
{
std::cout << "functionPointer" << std::endl;
}
void(*func)(void) = &functionPointer;
func();
具有 operator() 成员函数的类对象(仿函数)
class Test
{
public:
void operator()(int a,int b)
{
std::cout << a << "===" << b << std::endl;
}
};
Test t;
t(5,5);
可被转换为函数指针的类对象
class Test
{
public:
using testT = void(*)(void);
static void func(void)
{
std::cout << "===func===" << std::endl;
}
operator testT(void)
{
return func;
}
};
Test t;
t();
类成员(函数)指针
class Test
{
public:
void mem_func()
{
}
int _testNum;
};
auto memFunc = &Test::mem_func;
auto dataFunc = &Test::_testNum;
Test T;
(T.*memFunc)();
T.*dataFunc = 123;
std::function
std::function 是可调用对象的包装器,它是一个类模板。可以容纳除了类成员(函数)指针之外的所有可调用对象。通过指定它的模板参数。它可以用统一的方式处理函数、函数对象、函数指针,并允许保存和延迟执行它们。
使用例子
/*普通函数绑定*/
class Task
{
public:
void setCurrentCallBack(std::function<void(int, int)> &callBack)
{
_addCallBack = callBack;
}
void tryToExecute()
{
_addCallBack(5, 5);
_addCallBack(10, 5);
_addCallBack(15, 10);
}
std::function<void(int, int)> _addCallBack;
};
/*测试代码*/
std::shared_ptr<Task> task = std::make_shared<Task>();
std::function<void(int, int)> callBack = testFuncion;
task->setCurrentCallBack(callBack);
task->tryToExecute();
std::bind
std::bind 用来将可以调用对象与其参数一起进行绑定。绑定后的结果可以使
用 std::function 进行保存。并将调用延迟到任何我们需要的时候。
主要有两大作用:
- 将可调用对象与其参数一起绑定成一个仿函数
- 将多元 (参数个数为n,n>1)可调用对象转换成医院或者()可调用对象
,即只绑定部分参数
std::bind和std::function结合使用
class Task
{
public:
void setCurrentCallBack(std::function<void(int, int)> &callBack)
{
_addCallBack = callBack;
}
void tryToExecute()
{
_addCallBack(5, 5);
_addCallBack(10, 5);
_addCallBack(15, 10);
}
std::function<void(int, int)> _addCallBack;
};
class FBindAndFunction
{
public:
FBindAndFunction()
{
}
~FBindAndFunction()
{
}
void setTask(const std::shared_ptr<Task> &task)
{
_task = task;
std::function<void(int, int)> callBack = std::bind(&FBindAndFunction::bindFunction, this, std::placeholders::_1, std::placeholders::_2);
//std::placeholders::_1, std::placeholders::_2 是占位符,_1对应函数的第一个参数 _2则是对应第二个。
_task->setCurrentCallBack(callBack);
}
void MainTest()
{
std::shared_ptr<Task> task = std::make_shared<Task>();
setTask(task);
task->tryToExecute();
}
private:
std::shared_ptr<Task> _task;
};
第二章 使用C++11改进程序性能
右值引用
标记为 T && (T为具体类型 例如 int && )
左值和右值
左值: 表达式结束之后依然存在的持久的对象。
右值:表达式结束后就不在存在的临时对象。
一个区分左值和右值的方法是 若能对表达式取地址,则是左值,反之则是右值 所有的具名变量或者对象都是左值;
int a = 1; //a 是左值 1 是右值(可以对a 取地址,但没法对1取地址)
class Test ; Test t; //t 是左值
右值概念组成
- 纯右值
非引用变量返回的临时变量、运算表达式产生的临时变量、原始字面量和lambda 表达式都是纯右值。 - 将亡值
将亡值则是C++11 新增的、与右值引用相关的表达式。例如要被移动的对象、T && 函数返回值和转换为 T&&类型的转换函数的返回值。
右值引用
对一个右值进行应用的类型。且无论声明左值引用还是右值引用都必须立即进行初始化,因为引用类型本身并不拥有所绑定对象的内存,只是该对象的一个别名。且通过右值应用的声明,该右值的声明周期和右值引用类型变量的声明周期一样,只要该变量(右值引用类型变量)还活着。该右值临时量也回一直存活下去
int a = 5;
int &leftRa = a;//leftRa 为左值引用类型 且必须初始化
int &&rightR = 10; //rightR 为右值引用类型
右值引用优化类型,避免深拷贝
当类中有需要分配堆内存的变量的时候,尽量重写拷贝构造函数。达到深拷贝的效果。
移动构造函数: 参数是一个右值引用类型(T &&)。此处没有深拷贝,只有浅拷贝。这样就避免了对临时对象的深拷贝。提高了性能。这里的 T&& 用来根据参数是左值还是右值来建立分支。如果是临时值,则会选择移动构造函数,只是进行浅拷贝,不许对其进行深拷贝。
#pragma once
#include <iostream>
class FDeepCopyTest
{
public:
FDeepCopyTest();
~FDeepCopyTest();
FDeepCopyTest(const FDeepCopyTest&other);//类中有需要申请堆内存的变量,需要深拷贝
FDeepCopyTest(FDeepCopyTest &&other);//移动构造
private:
int *_ptr;
};
#include "FDeepCopyTest.h"
FDeepCopyTest::FDeepCopyTest()
:_ptr(new int(10))
{
}
/*重写拷贝构造函数*/
FDeepCopyTest::FDeepCopyTest(const FDeepCopyTest&other)
:_ptr(new int(*other._ptr))
{
}
/*移动构造*/
FDeepCopyTest::FDeepCopyTest(FDeepCopyTest &&other)
:_ptr(other._ptr)
{
}
FDeepCopyTest::~FDeepCopyTest()
{
if (_ptr)
{
delete _ptr;
_ptr = nullptr;
}
}
FDeepCopyTest getDeepCopyTestObject(bool flag)
{
FDeepCopyTest testA;
FDeepCopyTest testB;
if (flag)
{
return testA;
}
// else
return testB;
}
void DeepCopyTest()
{
auto test = getDeepCopyTestObject(false);
//未重写拷贝构造之前。是一个浅拷贝。test和 testB 中的
// _ptr 都指向同一个内存。重写了拷贝构造之后,达到深拷贝效果。各自持有一块内存。
{
FDeepCopyTest::FDeepCopyTest(FDeepCopyTest &&other)
:_ptr(other._ptr)
}
//上面是一个移动构造函数。当参数类型是一个临时值的时候(例如上面的 getDeepCopyTestObject 返回值)就会调用此函数,从而延长了该临时的生命周期。且只是浅拷贝。达到了性能优化的效果。
//打印出 test的地址以及 testB的地址即可知道效果。如下图
}
运行截图
在 testB 下面 以及 test 变量下面加上如下语句 ,打印地址即可。
printf(“getDeepCopyTestObject—%p\n”, testB);
printf(“DeepCopyTest—%p\n”, test);
- 未使用移动构造
可以看到两块内存地址不一致。 - 使用移动构造
可以看到两块变量地址是一致的
注意
在提供移动构造函数的时候也提供一个拷贝构造函数,以防移动不成功的时候还能拷贝构造。
move语义
移动语义是通过右值引用来匹配临时值的。
C++11 中提供了 std::move 方法来将左值转换为右值。move 只是将对象的状态或者所有权从一个对象转移到另一个对象。只是转移,没有内存拷贝。
move实际上并不能移动任何东西。它唯一的功能是将一个左值强制转换为一个右值引用。
假定一个 vector的容量很大,需要赋值给另外一个容器。那么拷贝的代价很大,性能较低,此时可以使用std::move 方法。提高性能。
std::vector<int> ve;//假定容量很大
std::vector<int> testV = std::move(ve);
forward 和完美转发
编译器会将已经命名的右值引用视为左值,而将未命名的右值引用视为右值。
一个右值引用参数作为函数的形参。在函数内部再转发该参数的时候,它已经变成一个左值了,并不是它原来的类型了。
完美转发 是指在函数模板中,完全依照模板的参数的类型(即保持右值,左值特征),将参数传递给函数模板中调用的另一个函数
template<class T>
void printValue(T &value)
{
std::cout << "l-value" << value << std::endl;
}
template<class T>
void printValue(T &&value)
{
std::cout << "r-value" << value << std::endl;
}
template<class T>
void forwardValue(T &&value)
{
printValue(value);
}
void test()
{
int value = 10;
printValue<int>(value);
printValue<int>(25);
forwardValue<int>(30);
}
运行截图:
运行可知,forwardValue 那里传入 的是一个右值,但是由于在forwardValue 那里进行转发 时候,由于此右值已经命名(value),则是转发成了左值。都不能按照参数的本来的类型进行转发
此时可用std::forwar 语义来解决转发类型丢失问题。
将forwardValue改为
template<class T>
void forwardValue(T &&value)
{
printValue(std::forward<T>(value));//此处进行完美转发
}
备注
std::move 则是无条件的转化为右值。std::forward 则是保持原有类型。
emplace_back减少内存拷贝和移动
emplace_back 能就地通过参数构造对象,不需要拷贝和移动内存。
使用的前提是 对象必须具有对应的构造函数
class ETest
{
public:
ETest()
:_valueA(0)
,_valueB(0)
{
}
ETest(int a, int b)
:_valueA(a)
,_valueB(b)
{
}
private:
int _valueA;
int _valueB;
};
void emplace_backTest()
{
std::vector<ETest> vec;
vec.emplace_back(5, 5);
}
unordered container 无序容器
例如 unordered_map unordered_set 等无序容器。
内部是一个 hash table;省去了排序,效率更高。
由于无序容器内部是 hash table,因此无序容器的 key 需要提供 hash_value函数,且对于自定义 key 需要提供 hash 函数和比较函数
#include <iostream>
#include <vector>
#include <bitset>
#include <string>
#include <utility>
#include <unordered_map>
struct Key
{
std::string first;
std::string second;
};
struct KeyHash
{
std::size_t operator()(const Key& k) const
{
return std::hash<std::string>()(k.first) ^
(std::hash<std::string>()(k.second) << 1);
}
};
struct KeyEqual
{
bool operator()(const Key& lhs, const Key& rhs) const
{
return lhs.first == rhs.first && lhs.second == rhs.second;
}
};
int main(void)
{
std::unordered_map<std::string, std::string> m1;
std::unordered_map<int, std::string> m2 = { { 1, "foo" }, { 2, "bar" }, { 3, "baz" } };
std::unordered_map<int, std::string> m3 = m2;
std::unordered_map<int, std::string> m4 = std::move(m2);
std::vector<std::pair<std::bitset<8>, int>> v = { { 0x12, 1 }, { 0x01, -1 } };
std::unordered_map<std::bitset<8>, double> m5(v.begin(), v.end());
std::unordered_map<Key, std::string, KeyHash, KeyEqual> m6 =
{ { { "John", "Doe" }, "example" }, { { "Mary", "Sue" }, "another" } };
system("pause");
return 0;
}
第四章 使用C++11 解决内存泄漏的问题
std::shared_ptr 共享的智能指针
std::shared_ptr 使用引用计数,每一个 std::shared_ptr 的拷贝都指向相同的内存。在最后一个std::shared_ptr 析构的时候,内存才会被释放
std::shared_ptr 的基本用法
- 初始化
使用std::make_shared辅助函数和reset方法来初始化std::shared_ptr
class TestClass
{
public:
TestClass()
{
}
~TestClass()
{
}
};
std::shared_ptr<TestClass> child = std::make_shared<TestClass>();
指定删除器
std::shared_ptr<int> ptr(new int(10));
int * p = ptr.get();
void DeleteIntPtr(int *p)
{
if(p)
{
delete p;
}
}
std::shared_ptr<int>p(new int,DeleteIntPtr);
分配数组对象
当使用 std::shared_ptr 管理动态数组的时候,需要指定删除器,因为std::shared_ptr 的默认删除器不支持数组对象。
std::shared_ptr<int> ptr(new int[10],[](int *p){delete []p;});
使用 std::shared_ptr 需要注意的问题
1) 不要使用一个原始指针初始化多个 std::shared_ptr
2) 不要在函数实参中创建std::shared_ptr
function(std::shared_ptr<int>(new int,g());
因为C++的函数参数的计算顺序在不同的编译器不同的调用约定下可能是不一样的,一般是从右到左,但也有可能是从左到右,所以可能的过程是先 new int,然后调用g(),如果恰好g()发生异常,而std::shared_ptr还没有创建,则int 则发生内存泄漏。正确的做法是先创建智能指针,代码如下
std::shared_ptr<int> p(new int()):
function(p,g());
3) 通过 shared_from_this() 返回this指针。不要将 this 指针作为std::shared_ptr 返回出来。因为this指针本质上是一个裸指针。因此,这样可能会导致重复析构 。
第五章 使用C++11 让多线程开发变得简单
线程
线程的创建
使用std::thread 创建线程。只需要提供线程函数或者函数对象即可,并且可以同时指定线程函数的的参数
#include<thread>
void threadFunc()
{
//.....
}
int main()
{
std::thread t(threadFunc):
t.join();
return 0;
}
上例中的 join 函数将会阻塞线程。 直到线程函数执行结束。如果函数有返回值,返回值将被忽略
线程的 detach() 函数用来将线程与线程对象分离
通过 detach,线程与线程对象分离了,让线程作为后台线程去执行,当前线程也不会被阻塞,只是在调用detach 之后,就无法在于线程发现联系,也无法再通过join函数来等待线程执行完毕
注意: std::thread 出了作用域之后将会析构,此时如果线程函数如果还没有执行完毕则会发生错误,因此需要保证线程函数的生命周期在 std::thread 对象的生命周期之内。
线程的基本用法
#include <thread>
int main(void)
{
std::thread t([]() {
for (auto iter = 0;iter<5;++iter)
{
std::cout << "I am a thread test" << std::endl;
} std::this_thread::sleep_for(std::chrono::seconds(3));
//休眠3秒
std::this_thread::get_id();//在线程函数中获取执行线程ID
});
t.join();//阻塞在这里
t.get_id();//获取当前线程ID
t.hardware_concurrency();//CPU核心数目 最合适线程数目
system("pause");
return 0;
}
互斥量
C++11 提供如下4种语义的互斥量
- std::mutex 独占的互斥量,不能递归使用
- std::timed_mutex 带超时的独占互斥量,不能递归使用
- std::recursive_mutex 递归互斥量,不带超时功能
- std::recursive_timed_mutex 带超时的递归互斥量
尽量使用 std::lock_guard 来辅助互斥量。因为 lock_guard 在构造的时候会自动锁定互斥量,而在退出作用域进行析构的时候会自动解锁互斥量,
递归锁允许同一个线程多次获得该互斥锁。可以用来解决同一线程需要多次互斥量时的死锁的问题。
#include <thread>
#include <mutex>
int GlobalNumber = 1;
std::mutex GlobalMutex;
void LockAgain()
{
std::lock_guard<std::mutex> lockA(GlobalMutex);
GlobalNumber++;
}
void threadFunc()
{
std::lock_guard<std::mutex> lockB(GlobalMutex);
LockAgain();//此时死锁 因为lockB已经加锁,
//而LockAgain 中的 lockA 无法加锁,阻塞在这里 ,造成死锁 改用上述提到的 recursive_mutex 即可解决死锁问题
GlobalNumber++;//
}
int main(void)
{
std::thread t(threadFunc);
t.join();
system("pause");
return 0;
}
递归锁注意问题:
- 需要用到递归锁的多线程互斥处理往往本身就是可以简化的,允许递归互斥很容易放纵复杂逻辑的产生,从而导致一些多线程同步引起的晦涩问题
- 递归锁比非递归锁效率低一些
- 递归锁虽然允许同一个线程多次获得同一个互斥量,可重复获得的最大次数并未具体说明,一旦超过一定次数,再对lock进行调用就会抛出 std::system错误
条件变量
一种用于等待的同步机制,它能阻塞一个或多个线程,直到收到另一个线程发出的通知或者超时,才会唤醒当前阻塞的线程。且条件变量需要和互斥量配合起来用
std::condition_variable 配合 std::unique_lock 来进行wait操作
std::condition_variable_any可以和任意带有lock,unlock语义的mutex(互斥锁) 搭配使用,比较灵活,但是效率比 std::condition_variable 差一些
条件变量的使用过程:
1)拥有条件变量的线程获取互斥量
2)循环检查某个条件,如果条件不满足,则阻塞直到条件满足;如果条件满足,则向下继续执行
3) 某个线程满足执行条件之后调用 notify_one 或者 notify_all 唤醒一个或者所有的等待线程
条件变量会先检查判断式是否满足条件,如果满足条件,则重新获取mutex,然后结束wait,继续往下执行。如果不满足条件,则释放mutex,然后将线程置为waiting状态,继续等待
std::unique_lock<std::mutex> uniqueLocker(_mutextTaskQueue);
//std::condition_variable 配合 std::unique_lock
//进行wait操作。
_conditionNotFull.wait(
uniqueLocker, [&]() {return _taskQueue.size() != _maxTaskQueueSize;}
);
//任务队列没满 继续执行
_taskQueue.push(task);
_conditionNotEmpty.notify_one();
原子变量
std::atomic 可以使用任意类型参数作为模板参数。
std::atomic value;//原子操作,无法被打断
call_once/once_flag
#include <iostream>
#include <thread>
#include <mutex>
std::once_flag flag;
void do_once()
{
std::call_once(flag,
[](){
std::cout << "run once" << std::endl;
});
}
int main()
{
std::thread t1(do_once);
std::thread t2(do_once);
std::thread t3(do_once);
t1.join();
t2.join();
t3.join();
system("pause");
return 0;
}
std::mutex GlobalMutex;
bool IsOnce = false;
void do_once()
{
std::lock_guard<std::mutex> locker(GlobalMutex);
if (IsOnce)
{
return;
}
std::cout << "run once" << std::endl;
IsOnce = true;
}
异步操作
异步操作相关类 std::future 、std::promise 、std::package_task 。
std::future作为异步结果的传输通道,可以很方便的获取线程函数的返回值。std::promise 用来包装一个值,将数据和std::future绑定起来,方便线程赋值;std::package_task用来包装一个可调用对象,将函数和std::future绑定起来,以便异步调用
std::future
因为一个异步操作的结果不能马上获取,只能在未来某个时候从某个地方获取。这个异步操作的结果是一个未来的期待值,所以被称为std::future;
可以通过同步等待的方式来获取结果,可以通过查询std::future的状态来获取异步操作的结果。future_status 有如下三种状态
- deferred 异步操作还没开始
- ready 异步操作已经开始
- timeout 异步操作超时
std::future_status status;
std::future future;//注意 此处错误 ,此例只是演示
// std::future_status 的用法
do
{
status = future.wait_for(std::chrono::seconds(1));
if (status == std::future_status::deferred)
{
//...
}
else if(status == std::future_status::timeout)
{
//...
}
else if (status == std::future_status::ready)
{
//...
}
} while (status != std::future_status::ready);
get wait wait_for //三种获取std::future的结果
其中get等待异步操作结束并返回结果。wait只是等待异步操作完成,没有返回值,wait_for 是超时等待返回结果
加粗样式