C++ 1x (本教程中指 C++ 11 / 14, 甚至 C++ 17) 为传统 C++ 注入的大量特性使得整个 C++ 变得更加像一门现代化的语言。C++ 1x 不仅仅增强了 C++ 语言自身的可用性,auto 关键字语义的修改使得我们更加有信心来操控极度复杂的模板类型。同时还对语言运行期进行了大量的强化,Lambda 表达式的出现让 C++ 具有了『匿名函数』的『闭包』特性,而这一特性几乎在现代的编程语言(诸如 Python/Swift/… )中已经司空见惯,右值引用的出现解决了 C++ 长期以来被人诟病的临时对象效率问题等等。
C++ 1x 为自身的标准库增加了非常多的工具和方法,诸如在语言层面上提供了 std::thread 支持了并发编程,在不同平台上不再依赖于系统底层的 API,实现了语言层面的跨平台支持;std::regex提供了完整的正则表达式支持等等。
从 C++ 11 开始,被弃用的主要特性:
注意:弃用不等于废弃,只是用于暗示程序员这些特性将从未来的标准中消失,应该尽量避免使用。但是,已弃用的特性依然是标准库的一部分,并且出于兼容性的考虑,这些特性其实会『永久』保留。
-
弃用的特性(1):如果一个类有析构函数,为其生成拷贝构造函数和拷贝赋值运算符的特性被弃用了。
-
弃用的特性(2):不再允许字符串字面值常量赋值给一个
char *
。如果需要用字符串字面值常量赋值和初始化一个char *
,应该使用const char *
或者auto
。
char *str = "hello world!"; // 将出现弃用警告。
-
弃用的特性(3):C++98 异常说明、
unexpected_handler
、set_unexpected()
等相关特性被弃用,应该使用noexcept
。 -
弃用的特性(4):
auto_ptr
被弃用,应使用unique_ptr
。 -
弃用的特性(5):
register
关键字被弃用。 -
弃用的特性(6):bool 类型的 ++ 操作被弃用。
-
弃用的特性(7):C 语言风格的类型转换被弃用,应该使用
static_cast
、reinterpret_cast
、const_cast
来进行类型转换。
还有一些其他诸如参数绑定(C++11 提供了 std::bind
和 std::function
)、export
等特性也均被弃用。前面提到的这些特性如果你从未使用或者听说过,也请不要尝试去了解他们,应该向新标准靠拢,直接学习新特性。毕竟,技术是向前发展的。
- Part 1
- C++11/14 简介
- 概述
- 教程目录
- 被弃用的特性
- 与 C 的兼容性
- C++11/14 简介
- Part 2
- 语言可用性的强化
- nullptr 与 constexpr
当需要使用 NULL 时候,请养成直接使用 nullptr 的习惯,当需要使用 NULL 时候,请养成直接使用 nullptr 的习惯
constexpr是C++11中新增的关键字,其语义是“常量表达式”,也就是在编译期可求值的表达式。 - 类型推导
- auto
- decltype
decltype 关键字是为了解决 auto 关键字只能对变量进行类型推导的缺陷而出现的。
decltype(表达式)
auto x = 1; auto y = 2; decltype(x+y) z; // z 是一个 int 型的
- 尾返回类型、auto 与 decltype 配合
对于函数返回类型:
因为程序员在使用这个模板函数的时候,必须明确指出返回类型。template<typename R, typename T, typename U> R add(T x, U y) { return x+y }
C++ 11 还引入了一个叫做尾返回类型(trailing return type),利用 auto 关键字将返回类型后置:
从 C++ 14 开始是可以直接让普通函数具备返回值推导,因此下面的写法变得合法:template<typename T, typename U> auto add(T x, U y) -> decltype(x+y) { return x+y; }
template<typename T, typename U> auto add(T x, U y) { return x+y; }
- 区间迭代
- 基于范围的 for 循环
int array[] = {1,2,3,4,5}; for(auto &x : array) { std::cout << x << std::endl; }
- 初始化列表
- std::initializer_list
初始化列表除了用在对象构造上,还能将其作为普通函数的形参#include <initializer_list> class Magic { public: Magic(std::initializer_list<int> list) {} }; Magic magic = {1,2,3,4,5}; std::vector<int> v = {1, 2, 3, 4};
- 统一初始化语法
- 模板增强
- 外部模板
- 尖括号 >
- 类型别名模板
在传统 C++ 中,typedef
可以为类型定义一个新的名称,但是却没有办法为模板定义一个新的名称。C++ 11 使用 using 引入了下面这种形式的写法,并且同时支持对传统 typedef 相同的功效
typedef int (*process)(void *); // 定义了一个返回类型为 int,参数为 void* 的函数指针类型,名字叫做 process using process = int(*)(void *); // 同上, 更加直观 template <typename T> using NewType = SuckType<int, T, 1>; // 合法
- 默认模板参数
在 C++11 中提供了一种便利,可以指定模板的默认参数:
template<typename T = int, typename U = int> auto add(T x, U y) -> decltype(x+y) { return x+y; }
- 变长参数模板
template<typename... Ts> class Magic; template<typename... Args> void magic(Args... args) { std::cout << sizeof...(args) << std::endl; }
- 面向对象增强
- 委托构造
C++ 11 引入了委托构造的概念,这使得构造函数可以在同一个类中一个构造函数调用另一个构造函数,从而达到简化代码的目的:
class Base { public: int value1; int value2; Base() { value1 = 1; } Base(int value) : Base() { // 委托 Base() 构造函数 value2 = 2; } }; int main() { Base b(2); std::cout << b.value1 << std::endl; std::cout << b.value2 << std::endl; }
- 继承构造
无须重写父类的所有需要调用的构造函数
class Base { public: int value1; int value2; Base() { value1 = 1; } Base(int value) : Base() { // 委托 Base() 构造函数 value2 = 2; } }; class Subclass : public Base { public: using Base::Base; // 继承构造 }; int main() { Subclass s(3); std::cout << s.value1 << std::endl; std::cout << s.value2 << std::endl; }
- 显式虚函数重载
- override
当重载虚函数时,引入 override 关键字将显式的告知编译器进行重载,编译器将检查基函数是否存在这样的虚函数,否则将无法通过编译 - final
是为了防止类被继续继承以及终止虚函数继续重载引入的。
- override
- 显式禁用默认函数
class Magic { public: Magic() = default; // 显式声明使用编译器生成的构造 Magic& operator=(const Magic&) = delete; // 显式声明拒绝编译器生成构造 Magic(int magic_number); }
- 委托构造
- 强类型枚举
在传统 C++ 中,枚举类型并非类型安全,枚举类型会被视作整数,则会让两种完全不同的枚举类型可以进行直接的比较(虽然编译器给出了检查,但并非所有),甚至枚举类型的枚举值名字不能相同,这不是我们希望看到的结果。
C++ 11 引入了枚举类(enumaration class),并使用 enum class 的语法进行声明:enum class new_enum : unsigned int { value1, value2, value3 = 100, value4 = 100 };
- nullptr 与 constexpr
- 语言可用性的强化
- Part 3
-
语言运行期的强化
- lambda 表达式
[捕获列表](参数列表) mutable(可选) 异常属性 -> 返回类型 { // 函数体 }
- lambda 表达式基础
- 值捕获
- 引用捕获
- 隐式捕获
& 或 = 向编译器声明采用 引用捕获或者值捕获 - 表达式捕获(C++ 14)
- 泛型 lambda
https://www.cnblogs.com/wzjhoutai/p/6714481.html
- lambda 表达式基础
- lambda 表达式
-
函数对象包装器
- std::function
- std::bind/std::placeholder
int foo(int a, int b, int c) { ; } int main() { // 将参数1,2绑定到函数 foo 上,但是使用 std::placeholders::_1 来对第一个参数进行占位 auto bindFoo = std::bind(foo, std::placeholders::_1, 1,2); // 这时调用 bindFoo 时,只需要提供第一个参数即可 bindFoo(1); }
-
右值引用
右值引用是 C++ 11 引入的与 Lambda 表达式齐名的重要特性之一。它的引入解决了 C++ 中大量的历史遗留问题,消除了诸如 std::vector、std::string 之类的额外开销,也才使得函数对象容器 std::function 成为了可能。- 左值、右值的纯右值、将亡值、右值
要弄明白右值引用到底是怎么一回事,必须要对左值和右值做一个明确的理解。
左值(lvalue, left value),顾名思义就是赋值符号左边的值。准确来说,左值是表达式(不一定是赋值表达式)后依然存在的持久对象。
右值(rvalue, right value),右边的值,是指表达式结束后就不再存在的临时对象。
而 C++ 11中为了引入强大的右值引用,将右值的概念进行了进一步的划分,分为:纯右值、将亡值。
纯右值(prvalue, pure rvalue),纯粹的右值,要么是纯粹的字面量,例如 10, true;要么是求值结果相当于字面量或匿名临时对象,例如 1+2。非引用返回的临时变量、运算表达式产生的临时变量、原始字面量、Lambda 表达式都属于纯右值。
将亡值(xvalue, expiring value),是 C++ 11 为了引入右值引用而提出的概念(因此在传统 C++中,纯右值和右值是统一个概念),也就是即将被销毁、却能够被移动的值。
将亡值可能稍有些难以理解,我们来看这样的代码:
std::vector<int> foo() { std::vector<int> temp = {1, 2, 3, 4}; return temp; } std::vector<int> v = foo();
在这样的代码中,函数 foo 的返回值 temp 在内部创建然后被赋值给 v,然而 v 获得这个对象时,会将整个 temp 拷贝一份,然后把 temp 销毁,如果这个 temp 非常大,这将造成大量额外的开销(这也就是传统 C++ 一直被诟病的问题)。在最后一行中,v 是左值、foo() 返回的值就是右值(也是纯右值)。
但是,v 可以被别的变量捕获到,而 foo() 产生的那个返回值作为一个临时值,一旦被 v 复制后,将立即被销毁,无法获取、也不能修改。将亡值就定义了这样一种行为:临时的值能够被识别、同时又能够被移动。
- 右值引用和左值引用
需要拿到一个将亡值,就需要用到右值引用的申明:T &&,其中 T 是类型。右值引用的声明让这个临时值的生命周期得以延长、只要变量还活着,那么将亡值将继续存活。
C++11 提供了 std::move 这个方法将左值参数无条件的转换为右值,有了它我们就能够方便的获得一个右值临时对象,例如:
#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 = s1; // 非法, s1 在全局上下文中没有声明 std::string&& rv1 = std::move(lv1); // 合法, std::move 可以将左值转移为右值 std::cout << "rv1 = " << rv1 << std::endl; // string, const std::string& lv2 = lv1 + lv1; // 合法, 常量左值引用能够延长临时变量的生命周期 // lv2 += "Test"; // 非法, 引用的右值无法被修改 std::cout << "lv2 = "<<lv2 << std::endl; // string,string std::string&& rv2 = lv1 + lv2; // 合法, 右值引用延长临时对象的生命周期 rv2 += "string"; // 合法, 非常量引用能够修改临时变量 std::cout << "rv2 = " << rv2 << std::endl; // string,string,string, reference(rv2); // 输出左值 }
注意:rv2 虽然引用了一个右值,但由于它是一个引用,所以 rv2依然是一个左值。
- 移动语义
传统 C++ 通过拷贝构造函数和赋值操作符为类对象设计了拷贝/复制的概念,但为了实现对资源的移动操作,调用者必须使用先复制、再析构的方式,否则就需要自己实现移动对象的接口。试想,搬家的时候是把家里的东西直接搬到新家去,而不是将所有东西复制一份(重买)再放到新家、再把原来的东西全部销毁,这是非常反人类的一件事情。
传统的 C++ 没有区分『移动』和『拷贝』的概念,造成了大量的数据移动,浪费时间和空间。右值引用的出现恰好就解决了这两个概念的混淆问题,例如:
#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; else return 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; }
在上面的代码中:
首先会在 return_rvalue 内部构造两个 A 对象,于是获得两个构造函数的输出;
函数返回后,产生一个将亡值,被 A 的移动构造(A(A&&))引用,从而延长生命周期,并将这个右值中的指针拿到,保存到了 obj 中,而将亡值的指针被设置为 nullptr,防止了这块内存区域被销毁。
从而避免了无意义的拷贝构造,加强了性能。再来看看涉及标准库的例子:#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; }
- 完美转发
前面我们提到了,一个声明的右值引用其实是一个左值。这就为我们进行参数转发(传递)造成了问题:
void reference(int& v) { std::cout << "左值" << std::endl; } void reference(int&& v) { std::cout << "右值" << std::endl; } template <typename T> void pass(T&& v) { std::cout << "普通传参:"; reference(v); // 始终调用 reference(int& ) } int main() { std::cout << "传递右值:" << std::endl; pass(1); // 1是右值, 但输出左值 std::cout << "传递左值:" << std::endl; int v = 1; pass(v); // v是左引用, 输出左值 return 0; }
对于 pass(1) 来说,虽然传递的是右值,但由于 v 是一个引用,所以同时也是左值。因此 reference(v) 会调用 reference(int&),输出『左值』。而对于pass(v)而言,v是一个左值,为什么会成功传递给 pass(T&&) 呢?
这是基于引用坍缩规则的:在传统 C++ 中,我们不能够对一个引用类型继续进行引用,但 C++ 由于右值引用的出现而放宽了这一做法,从而产生了引用坍缩规则,允许我们对引用进行引用,既能左引用,又能右引用。但是却遵循如下规则:函数形参类型 实参参数类型 推导后函数形参类型 T& 左引用 T& T& 右引用 T& T&& 左引用 T& T&& 右引用 T&& 因此,模板函数中使用 T&& 不一定能进行右值引用,当传入左值时,此函数的引用将被推导为左值。更准确的讲,无论模板参数是什么类型的引用,当且仅当实参类型为右引用时,模板参数才能被推导为右引用类型。这才使得 v 作为左值的成功传递。
完美转发就是基于上述规律产生的。所谓完美转发,就是为了让我们在传递参数的时候,保持原来的参数类型(左引用保持左引用,右引用保持右引用)。为了解决这个问题,我们应该使用 std::forward 来进行参数的转发(传递):
#include <iostream> #include <utility> void reference(int& v) { std::cout << "左值引用" << std::endl; } void reference(int&& v) { std::cout << "右值引用" << std::endl; } template <typename T> void pass(T&& v) { std::cout << "普通传参:"; reference(v); std::cout << "std::move 传参:"; reference(std::move(v)); std::cout << "std::forward 传参:"; reference(std::forward<T>(v)); } int main() { std::cout << "传递右值:" << std::endl; pass(1); std::cout << "传递左值:" << std::endl; int v = 1; pass(v); return 0; }
输出结果为:
传递右值: 普通传参:左值引用 std::move 传参:右值引用 std::forward 传参:右值引用 传递左值: 普通传参:左值引用 std::move 传参:右值引用 std::forward 传参:左值引用
无论传递参数为左值还是右值,普通传参都会将参数作为左值进行转发,所以 std::move 总会接受到一个左值,从而转发调用了reference(int&&) 输出右值引用。
唯独 std::forward 即没有造成任何多余的拷贝,同时完美转发(传递)了函数的实参给了内部调用的其他函数。
这里 std::forward 和 std::move 一样,没有做任何事情,std::move 单纯的将左值转化为右值,std::forward 也只是单纯的将参数做了一个类型的转换,从是实现来看,std::forward(v) 和 static_cast<T&&>(v) 是完全一样的。
- 左值、右值的纯右值、将亡值、右值
-
- Part4
- 对标准库的扩充: 新增容器
- std::array
- std::forward_list
- std::unordered_set
- std::unordered_map
- std::tuple
- 基本操作
- 运行期索引
- 合并与迭代
- 对标准库的扩充: 新增容器
- Part 5
- 对标准库的扩充: 智能指针和引用计数
- 引用计数
- std::shared_ptr
std::shared_ptr 是一种智能指针,它能够记录多少个 shared_ptr 共同指向一个对象,从而消除显示的调用 delete,当引用计数变为零的时候就会将对象自动删除。
但还不够,因为使用 std::shared_ptr 仍然需要使用 new 来调用,这使得代码出现了某种程度上的不对称。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); // 非法, 不允许直接赋值 // 构造了一个 std::shared_ptr auto pointer = std::make_shared<int>(10); foo(pointer); std::cout << *pointer << std::endl; // 11 // 离开作用域前,shared_ptr 会被析构,从而释放内存 return 0; }
- std::make_shared
- std::unique_ptr 是一种独占的智能指针,它禁止其他智能指针与其共享同一个对象,从而保证了代码的安全:
std::unique_ptr<int> pointer = std::make_unique<int>(10); // make_unique 从 C++14 引入 std::unique_ptr<int> pointer2 = pointer; // 非法
- std::weak_ptr
- 对标准库的扩充: 智能指针和引用计数
- Part 6
- 对标准库的扩充: 正则表达式库
- 正则表达式简介
- 普通字符
- 特殊字符
- 限定符
- std::regex 及其相关
- std::regex
- std::regex_match
- std::match_results
- 正则表达式简介
- 对标准库的扩充: 正则表达式库
- Part 7
- 对标准库的扩充: 语言级线程支持,编译需要使用 -pthread 选项,
g++ main.cpp -std=c++14 -pthread
- std::thread 用于创建一个执行的线程实例,所以它是一切并发编程的基础,使用时需要包含头文件,它提供了很多基本的线程操作,例如get_id()来获取所创建线程的线程 ID,例如使用 join() 来加入一个线程等等,例如:
#include <iostream> #include <thread> void foo() { std::cout << "hello world" << std::endl; } int main() { std::thread t(foo); t.join(); return 0; }
- std::mutex
C++11 还为互斥量提供了一个 RAII 语法的模板类std::lock_gurad。RAII 在不失代码简洁性的同时,很好的保证了代码的异常安全性。
void some_operation(const std::string &message) { static std::mutex mutex; std::lock_guard<std::mutex> lock(mutex); // ...操作 // 当离开这个作用域的时候,互斥锁会被析构,同时unlock互斥锁 // 因此这个函数内部的可以认为是临界区 }
由于 C++保证了所有栈对象在声明周期结束时会被销毁,所以这样的代码也是异常安全的。无论 some_operation() 正常返回、还是在中途抛出异常,都会引发堆栈回退,也就自动调用了 unlock()。
- std::unique_lock
- std::future
- std::packaged_task
- std::condition_variable
- 对标准库的扩充: 语言级线程支持,编译需要使用 -pthread 选项,
- Part 8
- 其他杂项
- 新类型
- long long int
- noexcept 的修饰和操作
- 字面量
- 原始字符串字面量
- 自定义字面量
- 其他杂项
- Part 9
- 扩展主题: C++17 简介
- 主要入选特性
- 非类型模板参数的 auto
- std::variant<>
- 结构化绑定(Structured bindings)
- 变量声明的强化
- 未入选特性
- Concepts
- 主要入选特性
- 扩展主题: C++17 简介