书籍见《现代 C++ 教程:高速上手 C++11/14/17/20》 - 书栈网 · BookStack
介绍 - 《现代 C++ 教程:高速上手 C++11/14/17/20》 - 书栈网 · BookStack
记录几个常见的新特性。c++11/14/17/20标准。
第二章
constexpr
用于表示一个表达式是恒定的值,便于编译器优化。
constexpr int fibonacci(const int n) {
return n == 1 || n == 2 ? 1 : fibonacci(n-1)+fibonacci(n-2);
}
if/switch 声明优化
if括号里面声明的变量可以在if大括号里使用了
// 将临时变量放到 if 语句内
if (const std::vector<int>::iterator itr = std::find(vec.begin(), vec.end(), 3);
itr != vec.end()) {
*itr = 4;
}
auto
可以自动推导变量的类型,不能用于函数的参数传递
auto i = 5; // i 被推导为 int
auto arr = new auto(10); // arr 被推导为 int *
decltype
可以自动推导变量的类型,说不清他和auto的区别
auto x = 1;
auto y = 2;
decltype(x+y) z;
尾返回类型
可以不用显示指明函数返回值的类型,由编译器自动推导
template<typename T, typename U>
auto add2(T x, U y) -> decltype(x+y){
return x + y;
}
for区间迭代
for也可以像java一样了,很方便
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> vec = {1, 2, 3, 4};
if (auto itr = std::find(vec.begin(), vec.end(), 3); itr != vec.end()) *itr = 4;
for (auto element : vec)
std::cout << element << std::endl; // read only
for (auto &element : vec) {
element += 1; // writeable
}
for (auto element : vec)
std::cout << element << std::endl; // read only
}
强枚举类型
避免同一命名空间不同的emun类进行比较,加个关键字enum class就好了
enum class new_enum : unsigned int {
value1,
value2,
value3 = 100,
value4 = 100
};
第三章
Lambda 表达式
匿名函数,可以把他当作没有名字的函数。
[捕获列表](参数列表) mutable(可选) 异常属性 -> 返回类型 {
// 函数体
}
这里的捕获列表,相当于你要从外界传入一个值到函数里面去,函数里面可能会一直使用(这样就不用多占一个参数列表的位置)。
这个外界传入的值分两种,一个是值捕获,复制了一份到函数内部去
void lambda_value_capture() {
int value = 1;
auto copy_value = [value] {
return value;
};
value = 100;
auto stored_value = copy_value();
std::cout << "stored_value = " << stored_value << std::endl;
// 这时, stored_value == 1, 而 value == 100.
// 因为 copy_value 在创建时就保存了一份 value 的拷贝
}
一个是引用捕获,外面的函数值变化,里面的也变化
void lambda_value_capture() {
int value = 1;
auto copy_value = [value] {
return value;
};
value = 100;
auto stored_value = copy_value();
std::cout << "stored_value = " << stored_value << std::endl;
// 这时, stored_value == 1, 而 value == 100.
// 因为 copy_value 在创建时就保存了一份 value 的拷贝
}
右值引用 && 和左值引用 &
左值(lvalue, left value),顾名思义就是赋值符号左边的值。准确来说, 左值是表达式(不一定是赋值表达式)后依然存在的持久对象。 比方说, int a=10 +5; 这里的a就算左值
右值(rvalue, right value),右边的值,是指表达式结束后就不再存在的临时对象。比方说,vector<int> getResult(); vector<int> vct=getResult(); 这里getResult()返回的对象,是临时构造的,这个语句之后,就要销毁了。
纯右值(prvalue, pure rvalue),纯粹的右值,要么是纯粹的字面量,例如 10
, true
; 要么是求值结果相当于字面量或匿名临时对象,例如 1+2
。非引用返回的临时变量、运算表达式产生的临时变量、 原始字面量、Lambda 表达式都属于纯右值。
将亡值(xvalue, expiring value),是 C++11 为了引入右值引用而提出的概念(因此在传统 C++中, 纯右值和右值是同一个概念),也就是即将被销毁、却能够被移动的值。
这个就是一个将亡值temp
std::vector<int> foo() {
std::vector<int> temp = {1, 2, 3, 4};
return temp;
}
std::vector<int> v = foo();
示例
#include <iostream>
#include <string>
void reference(std::string& str) {
std::cout << "左值" << std::endl;
}
void reference(std::string&& str) {
std::cout << "右值" << std::endl;
}
int main()
{
std::string lv1 = "string,"; // lv1 是一个左值
// std::string&& r1 = lv1; // 非法, 右值引用不能引用左值
std::string&& rv1 = std::move(lv1); // 合法, std::move可以将左值转移为右值
std::cout << rv1 << std::endl; // string,
const std::string& lv2 = lv1 + lv1; // 合法, 常量左值引用能够延长临时变量的生命周期
// lv2 += "Test"; // 非法, 常量引用无法被修改
std::cout << lv2 << std::endl; // string,string
std::string&& rv2 = lv1 + lv2; // 合法, 右值引用延长临时对象生命周期
rv2 += "Test"; // 合法, 非常量引用能够修改临时变量
std::cout << rv2 << std::endl; // string,string,string,Test
reference(rv2); // 输出左值
return 0;
}
一个&,左值引用,类似于c++参数传递的引用一样,都指向一个对象,只是对象的类别是左值。
两个&,右值应用,类别是右值,也是指向同一个对象。
move
#include <iostream>
class A {
public:
int *pointer;
A():pointer(new int(1)) {
std::cout << "构造" << pointer << std::endl;
}
A(A& a):pointer(new int(*a.pointer)) {
std::cout << "拷贝" << pointer << std::endl;
} // 无意义的对象拷贝
A(A&& a):pointer(a.pointer) {
a.pointer = nullptr;
std::cout << "移动" << pointer << std::endl;
}
~A(){
std::cout << "析构" << pointer << std::endl;
delete pointer;
}
};
// 防止编译器优化
A return_rvalue(bool test) {
A a,b;
if(test) return a; // 等价于 static_cast<A&&>(a);
else return b; // 等价于 static_cast<A&&>(b);
}
int main() {
A obj = return_rvalue(false);
std::cout << "obj:" << std::endl;
std::cout << obj.pointer << std::endl;
std::cout << *obj.pointer << std::endl;
return 0;
}
这里,第二个构造函数,就是新建一个pointer指向int指针,其实可以直接声明一个指针,指向int,而不是先复制,再释放,没有意义。
#include <iostream> // std::cout
#include <utility> // std::move
#include <vector> // std::vector
#include <string> // std::string
int main() {
std::string str = "Hello world.";
std::vector<std::string> v;
// 将使用 push_back(const T&), 即产生拷贝行为
v.push_back(str);
// 将输出 "str: Hello world."
std::cout << "str: " << str << std::endl;
// 将使用 push_back(const T&&), 不会出现拷贝行为
// 而整个字符串会被移动到 vector 中,所以有时候 std::move 会用来减少拷贝出现的开销
// 这步操作后, str 中的值会变为空
v.push_back(std::move(str));
// 将输出 "str: "
std::cout << "str: " << str << std::endl;
return 0;
}
像这样,把str内的属性啥的,搬家到v.back()里,原来的str的值就没有了。
第四章 容器
array
与vector相比,std::array
对象的大小是固定的,std::vector
是自动扩容的,当存入大量的数据后,并且对容器进行了删除操作, 容器并不会自动归还被删除元素相应的内存,这时候就需要手动运行 shrink_to_fit()
释放这部分内存。
array的使用说明
std::array<int, 4> arr = {1, 2, 3, 4};
arr.empty(); // 检查容器是否为空
arr.size(); // 返回容纳的元素数
// 迭代器支持
for (auto &i : arr)
{
// ...
}
// 用 lambda 表达式排序
std::sort(arr.begin(), arr.end(), [](int a, int b) {
return b < a;
});
// 数组大小参数必须是常量表达式
constexpr int len = 4;
std::array<int, len> arr = {1, 2, 3, 4};
// 非法,不同于 C 风格数组,std::array 不会自动退化成 T*
// int *arr_p = arr;
无序容器 unordered_map
unordered_set
map和set是用红黑数实现,
插入和搜索的平均复杂度均为 O(log(size)),
而无序容器中的元素是不进行排序的,内部通过 Hash 表实现,插入和搜索元素的平均复杂度为 O(constant)
, 在不关心容器内部元素顺序时,能够获得显著的性能提升。
#include <iostream>
#include <string>
#include <unordered_map>
#include <map>
int main() {
// 两组结构按同样的顺序初始化
std::unordered_map<int, std::string> u = {
{1, "1"},
{3, "3"},
{2, "2"}
};
std::map<int, std::string> v = {
{1, "1"},
{3, "3"},
{2, "2"}
};
// 分别对两组结构进行遍历
std::cout << "std::unordered_map" << std::endl;
for( const auto & n : u)
std::cout << "Key:[" << n.first << "] Value:[" << n.second << "]\n";
std::cout << std::endl;
std::cout << "std::map" << std::endl;
for( const auto & n : v)
std::cout << "Key:[" << n.first << "] Value:[" << n.second << "]\n";
}
元组 tuple
存储不同类型的数据。
std::make_tuple
: 构造元组std::get
: 获得元组某个位置的值std::tie
: 元组拆包
#include <tuple>
#include <iostream>
auto get_student(int id)
{
// 返回类型被推断为 std::tuple<double, char, std::string>
if (id == 0)
return std::make_tuple(3.8, 'A', "张三");
if (id == 1)
return std::make_tuple(2.9, 'C', "李四");
if (id == 2)
return std::make_tuple(1.7, 'D', "王五");
return std::make_tuple(0.0, 'D', "null");
// 如果只写 0 会出现推断错误, 编译失败
}
int main()
{
auto student = get_student(0);
std::cout << "ID: 0, "
<< "GPA: " << std::get<0>(student) << ", "
<< "成绩: " << std::get<1>(student) << ", "
<< "姓名: " << std::get<2>(student) << '\n';
double gpa;
char grade;
std::string name;
// 元组进行拆包
std::tie(gpa, grade, name) = get_student(1);
std::cout << "ID: 1, "
<< "GPA: " << gpa << ", "
<< "成绩: " << grade << ", "
<< "姓名: " << name << '\n';
}
第五章 智能指针
std::shared_ptr
/std::unique_ptr
/std::weak_ptr
使用它们需要包含头文件 <memory>
std::shared_ptr
std::shared_ptr
是一种智能指针,它能够记录多少个 shared_ptr
共同指向一个对象,从而消除显示的调用 delete
,当引用计数变为零的时候就会将对象自动删除。
std::make_shared
就能够用来消除显式的使用 new
,所以std::make_shared
会分配创建传入参数中的对象, 并返回这个对象类型的std::shared_ptr
指针。例如:
#include <iostream>
#include <memory>
void foo(std::shared_ptr<int> i)
{
(*i)++;
}
int main()
{
// auto pointer = new int(10); // illegal, no direct assignment
// Constructed a std::shared_ptr
auto pointer = std::make_shared<int>(10);
foo(pointer);
std::cout << *pointer << std::endl; // 11
// The shared_ptr will be destructed before leaving the scope
return 0;
}
std::shared_ptr
可以通过 get()
方法来获取原始指针,通过 reset()
来减少一个引用计数, 并通过use_count()
来查看一个对象的引用计数。例如:
auto pointer = std::make_shared<int>(10);
auto pointer2 = pointer; // 引用计数+1
auto pointer3 = pointer; // 引用计数+1
int *p = pointer.get(); // 这样不会增加引用计数
std::cout << "pointer.use_count() = " << pointer.use_count() << std::endl; // 3
std::cout << "pointer2.use_count() = " << pointer2.use_count() << std::endl; // 3
std::cout << "pointer3.use_count() = " << pointer3.use_count() << std::endl; // 3
pointer2.reset();
std::cout << "reset pointer2:" << std::endl;
std::cout << "pointer.use_count() = " << pointer.use_count() << std::endl; // 2
std::cout << "pointer2.use_count() = " << pointer2.use_count() << std::endl; // 0, pointer2 已 reset
std::cout << "pointer3.use_count() = " << pointer3.use_count() << std::endl; // 2
pointer3.reset();
std::cout << "reset pointer3:" << std::endl;
std::cout << "pointer.use_count() = " << pointer.use_count() << std::endl; // 1
std::cout << "pointer2.use_count() = " << pointer2.use_count() << std::endl; // 0
std::cout << "pointer3.use_count() = " << pointer3.use_count() << std::endl; // 0, pointer3 已 reset
简单来说,就是使用std::make_share<T>代替new ,就不用自己手动delete了,当多建立一个指针指向创建的对象的时候,他自己会加一,让没有指向对象的时候,他会自动帮你delete。
std::unique_ptr
只允许一个指向该对象的指针,可以用move来转移给其他指针。
std::unique_ptr<int> pointer = std::make_unique<int>(10); // make_unique 从 C++14 引入
std::unique_ptr<int> pointer2 = pointer; // 非法
#include <iostream>
#include <memory>
struct Foo {
Foo() { std::cout << "Foo::Foo" << std::endl; }
~Foo() { std::cout << "Foo::~Foo" << std::endl; }
void foo() { std::cout << "Foo::foo" << std::endl; }
};
void f(const Foo &) {
std::cout << "f(const Foo&)" << std::endl;
}
int main() {
std::unique_ptr<Foo> p1(std::make_unique<Foo>());
// p1 不空, 输出
if (p1) p1->foo();
{
std::unique_ptr<Foo> p2(std::move(p1));
// p2 不空, 输出
f(*p2);
// p2 不空, 输出
if(p2) p2->foo();
// p1 为空, 无输出
if(p1) p1->foo();
p1 = std::move(p2);
// p2 为空, 无输出
if(p2) p2->foo();
std::cout << "p2 被销毁" << std::endl;
}
// p1 不空, 输出
if (p1) p1->foo();
// Foo 的实例会在离开作用域时被销毁
}
第七章 并行与并发
std::thread 用于创建进程,getid() 获取当前进程的pid,join() 等待所有子进程结束。
#include <iostream>
#include <thread>
int main() {
std::thread t([](){
std::cout << "hello world." << std::endl;
});
t.join();
return 0;
}
互斥量和临界区
std::mutex
是 C++11 中最基本的 mutex
类,通过实例化 std::mutex
可以创建互斥量, 而通过其成员函数 lock()
可以进行上锁,unlock()
可以进行解锁。这样容易导致死锁,要在程序的所有退出接口都要unlock() 包括异常处理的地方。C++11 还为互斥量提供了一个 RAII 语法的模板类 std::lock_gurad。
#include <iostream>
#include <thread>
int v = 1;
void critical_section(int change_v) {
static std::mutex mtx;
std::lock_guard<std::mutex> lock(mtx);
// 执行竞争操作
v = change_v;
// 离开此作用域后 mtx 会被释放
}
int main() {
std::thread t1(critical_section, 2), t2(critical_section, 3);
t1.join();
t2.join();
std::cout << v << std::endl;
return 0;
}
注意要用static,在函数内部,static只会初始化一次,调用的时候只改一次。
但这个方法有个问题,就是只能有一个进程持有这个锁,这个函数内所有操作都是互斥的,没了并发的特性,所以推荐使用的是下一个函数,std::unique_lock。
#include <iostream>
#include <thread>
int v = 1;
void critical_section(int change_v) {
static std::mutex mtx;
std::unique_lock<std::mutex> lock(mtx);
// 执行竞争操作
v = change_v;
std::cout << v << std::endl;
// 将锁进行释放
lock.unlock();
// 在此期间,任何人都可以抢夺 v 的持有权
// 开始另一组竞争操作,再次加锁
lock.lock();
v += 1;
std::cout << v << std::endl;
}
int main() {
std::thread t1(critical_section, 2), t2(critical_section, 3);
t1.join();
t2.join();
return 0;
}
其实都一样了,没啥太大区别,只是调用的函数不一样。
条件变量
生产者消费者问题,这章的代码最好在linux环境下运行,windows会找不到头文件。
#include <queue>
#include <chrono>
#include <mutex>
#include <thread>
#include <iostream>
#include <condition_variable>
int main() {
std::queue<int> produced_nums;
std::mutex mtx;
std::condition_variable cv;
bool notified = false; // 通知信号
// 生产者
auto producer = [&]() {
for (int i = 0; ; i++) {
std::this_thread::sleep_for(std::chrono::milliseconds(900));
std::unique_lock<std::mutex> lock(mtx);
std::cout << "producing " << i << std::endl;
produced_nums.push(i);
notified = true;
cv.notify_all(); // 此处也可以使用 notify_one
}
};
// 消费者
auto consumer = [&]() {
while (true) {
std::unique_lock<std::mutex> lock(mtx);
while (!notified) { // 避免虚假唤醒
cv.wait(lock);
}
// 短暂取消锁,使得生产者有机会在消费者消费空前继续生产
lock.unlock();
std::this_thread::sleep_for(std::chrono::milliseconds(1000)); // 消费者慢于生产者
lock.lock();
while (!produced_nums.empty()) {
std::cout << "consuming " << produced_nums.front() << std::endl;
produced_nums.pop();
}
notified = false;
}
};
// 分别在不同的线程中运行
std::thread p(producer);
std::thread cs[2];
for (int i = 0; i < 2; ++i) {
cs[i] = std::thread(consumer);
}
p.join();
for (int i = 0; i < 2; ++i) {
cs[i].join();
}
return 0;
}
mutex锁,用于保护生产出来的商品,条件变量用于指示条件,我也没看懂为什么要这么写。。。
总结
看着吓死人,其实没有很难。比较重要的是智能指针和匿名函数那个地方,好多新的c++代码都用了这些,不了解下还真看不懂代码。