从 C++98 到 C++11,C++11 标准经历了十几年的沉淀。尽管后来更新速度越来越快,又出现了 C++14、C++17 等等,但 C++11 是必学的经典标准。
主要特性目录(加粗的是实现了以前没有的重要功能,或者确实很好用的):
- 关键字及新语法
1.1. auto 关键字及用法
1.2. nullptr 关键字及用法
1.3. for 循环语法 - STL 容器
2.1. std::array
2.2. std::forward_list
2.3. std::unordered_map
2.4. std::unordered_set - 多线程
3.1. std::thread
3.2. std::atomic
3.3. std::condition_variable - 智能指针内存管理
4.1. std::shared_ptr
4.2. std::weak_ptr - 其他
5.1. std::function、std::bind 封装可执行对象
5.2. lambda 表达式
1. 关键字及新语法
1.1. auto关键字及用法
auto 并没有让 C++ 成为弱类型语言,只是使用 auto 的时候,编译器根据上下文,确定 auto 变量的真正类型。
auto AddTest(int a, int b) { // auto 可以作为函数的返回值类型
return a + b;
}
int main() {
auto index = 10; // 自动识别类型为 int
auto res = AddTest(1,2);
std::cout << "index:" << index << std::endl; // index:10
std::cout << "res:" << ret << std::endl; // res: 3
}
但是注意 auto 作为函数返回值时,必须用于定义函数,不能仅仅声明函数。
1.2. nullptr 关键字及用法
在 nullptr 出现之前存在什么问题呢?首先,NULL 不是一个关键字,而只是一个宏(macro):
#define NULL 0
考虑这样一个函数重载的情形:
void foo(int) {} // #1
void foo(char*) {} // #2
int main() {
foo(NULL); // 调用 #1 还是 #2 ?
}
为了解决这种二义性,C++11 引入了关键字 nullptr,它作为一种空指针常量。foo(nullptr);
会毫无异议地调用函数 #2。
1.3. for 循环语法
原先 C++ 的 for 循环是没有像 foreach 那样的用法的。现在 C++ 11 的 for 循环语法再结合 auto 关键字可以大大简化开发代码:
int main() {
int numbers[] = {1, 2, 3, 4, 5};
for (auto number : numbers)
std::cout << number << std::endl;
}
2. STL容器
2.1. std::array
相对于数组而言,std::array 增加了 STL 的各种迭代器、算法、操作方法等,更加安全、方便。
相对于 std::vector 而言,std::array 提供了静态数组,编译时确定大小、更轻量、更效率,当然也比 std::vector 有更多局限性。
std::array 差不多就是 std::vector 和 普通数组的中和版本。
#include <array>
int main() {
std::array<int, 4> arrayDemo = {1, 2, 3, 4};
for (auto it : arrayDemo)
std::cout << it << std::endl;
int arrayDemoSize = sizeof(arrayDemo);
std::cout << "arrayDemo size:" << arrayDemoSize << std::endl; // 16
return 0;
}
2.2. std::forward_list
std::forward_list 为新增的线性表,与 list 区别在于它是单向链表。forward_list 可以看作是对 C 语言风格的单链表的封装,仅提供有限的接口,和 C 中它的实现相比,基本上不会有任何开销。当不需要双向迭代的时候,与 std::list 相比,该容器具有更高的空间利用率。
#include <forward_list>
int main() {
std::forward_list<int> numbers = {1, 2, 3, 4, 5, 4, 4};
for (auto number : numbers)
std::cout << number << " "; // 1 2 3 4 5 4 4
numbers.remove(4); // 移除值为 4 的节点
std::cout << "numbers after remove:" << std::endl;
for (auto number : numbers)
std::cout << number << " "; // 1 2 3 5
return 0;
}
2.3. std::unordered_map
2.4. std::unordered_set
std::unordered_set 的数据存储结构也是 hashtable+list 的方式,此外,std::unordered_set 在插入时不会像 std::set 一样自动排序。
#include <unordered_set>
#include <set>
int main() {
std::unordered_set<int> unorder_set;
unorder_set.insert(7);
unorder_set.insert(5);
unorder_set.insert(3);
unorder_set.insert(4);
unorder_set.insert(6);
for (auto itor : unorder_set)
std::cout << itor << " "; // 7 5 3 4 6
std::set<int> set;
set.insert(7);
set.insert(5);
set.insert(3);
set.insert(4);
set.insert(6);
for (auto itor : set)
std::cout << itor << " "; // 3 4 5 6 7
}
3. 多线程
在 C++11 以前,C++ 的多线程编程均需依赖系统或第三方接口实现,一定程度上影响了代码的移植性。C++11 中,引入了 boost 库中多线程的部分内容,形成标准后的接口与 boost 库基本没有变化,这样方便了使用者切换使用 C++ 标准接口。
3.1. std::thread
C++11 的 std::thread 解决了 boost::thread 中参数限制的问题,这大概都是得益于 C++11 的可变参数的设计风格。
thread 类内的三个方法:
- join:等待线程完成其执行。当 a 线程调用此方法时,主线程就被停止执行,直到 a 线程执行完毕。
- detach:容许线程从线程句柄独立开来执行。当此方法被调用后,执行的线程从线程对象中分离,已不再被一个线程对象所表达。C++ 线程对象可以被销毁,同时 OS 执行的线程可以继续。
- swap:交换 2 个 thread 对象。
3.2. std::atomic
当我们在编程中想对共享资源进行保护时,很自然地就会想到加锁,但是锁机制会大大增加时间开销。
std::atomic 为 C++11 封装的原子数据类型。从功能上看,原子数据类型不会发生数据竞争,能直接用在多线程中而不必我们用户对其进行添加互斥资源锁的操作。从实现上,大家可以理解为这些原子类型内部自己加了锁。
下面例子中,我们使用 100 个线程, 模拟一万次网页点击:
#include <atomic>
#include <thread>
#include <iostream>
#include <list>
// 用原子数据类型作为共享资源的数据类型
std::atomic_int total(0);
//long total = 0;
void click(){
for(int i=0; i<100;++i)
// 仅仅是数据类型的不同而已,对其的访问形式与普通数据类型的资源并无区别
total += 1;
}
int main()
{
// 创建100个线程模拟点击统计
std::list<std::thread> threads;
for(int i=0; i<100;++i)
threads.push_back(std::thread(click));
for (auto& th : threads)
th.join();
std::cout<<"result:"<<total<<std::endl; // result:10000
return 0;
}
3.3. std::condition_variable
线程休眠在多线程编程中使用非常频繁,经常需要等待一些异步执行的条件的返回结果。当 std::condition_variable 对象的某个 wait 函数被调用的时候,它使用 std::unique_lock(通过 std::mutex)来锁住当前线程。当前线程会一直被阻塞,直到另外一个线程在相同的 std::condition_variable 对象上调用了 notification 函数来唤醒当前线程。
#include <iostream> // std::cout
#include <thread> // std::thread
#include <mutex> // std::mutex, std::unique_lock
#include <condition_variable> // std::condition_variable
std::mutex mtx; // 全局互斥锁
std::condition_variable cv; // 全局条件变量
bool ready = false; // 全局标志位
void do_print_id(int id) {
std::unique_lock <std::mutex> lck(mtx); // 进入休眠
while (!ready) // 如果标志位不为 true, 则等待
cv.wait(lck); // 进入休眠, 当前线程被阻塞, 当全局标志位变为 true 之后,
// go() 函数内调用 notify_all() 函数后, 线程被唤醒, 继续往下执行打印线程编号 id
std::cout << "thread " << id << '\n';
}
void go() {
std::unique_lock <std::mutex> lck(mtx);
ready = true; // 设置全局标志位为 true
cv.notify_all(); // 唤醒所有线程
}
int main() {
std::thread threads[10];
for (int i = 0; i < 10; ++i)
threads[i] = std::thread(do_print_id, i);
std::cout << "10 threads ready to race...\n";
go();
for (auto & th:threads)
th.join();
}
输出结果(顺序是不一定的):
10 threads ready to race...
thread 9
thread 2
thread 3
thread 8
thread 7
thread 6
thread 5
thread 4
thread 1
thread 0
4. 智能指针内存管理
智能指针及其作用
简单画一下指针、智能指针对象和计数器之间的关系:
5. 其他
5.1. std::function、std::bind 封装可执行对象
std::bind 用来将可调用对象与其参数一起进行绑定。绑定后可以使用 std::function 进行保存,并延迟到我们需要的时候调用:
- 将可调用对象与其参数绑定成一个仿函数;
- 可绑定部分参数。
在绑定部分参数的时候,通过使用 std::placeholders 来决定空位参数将会是调用发生时的第几个参数。
#include <iostream> // std::cout
#include <functional> // std::function
class A {
public:
int i_ = 0; // C++11 允许非静态(non-static)数据成员在其声明处(在其所属类内部)进行初始化
void output(int x, int y) {
std::cout << x << " " << y << std::endl;
}
};
int main() {
A a;
// fr 保存了指代的函数 output(),可以在之后的程序过程中调用
// std::bind 第一个参数为对象函数指针,表示函数相对于类的首地址的偏移量
// 第二个参数为对象指针
// 最后两个为参数占位符,表示 std::bind 封装的可执行对象可以接受两个参数
std::function<void(int, int)> fr = std::bind(&A::output, &a, std::placeholders::_1, std::placeholders::_2);
// 调用成员函数
fr(1, 2); // 1 2
// 绑定成员变量
std::function<int&(void)> fr2 = std::bind(&A::i_, &a);
fr2() = 100; // 对成员变量进行赋值
std::cout << a.i_ << std::endl; // 100
}
5.2. lambda 表达式
lambda 表达式用于定义并创建匿名的函数对象,以简化编程工作。可以拿来当作 inline 函数使用。lambda 的语法形式如下:
[...] (...) ... {...}
- [] 内是一个 capture,可以在 lambda 内部访问的"nonstatic 外部变量",如果没有要访问的变量,可以为空。static 变量是可以直接被访问的。
- () 内是参数,和函数参数一样。
- ... 是 mutable 或 exception 声明或者返回类型。如果其中之一出现,那么必须出现 ()。
- {} 内是函数体,在这里面写明 lambda 要完成的工作。
int main() {
int xxx= 10;
auto f = [xxx] (int a) { cout << "hello, world " << a << xxx << endl; };
f(12);
return 0;
}