1 C++11 的发展背景
C++11,先前被称作 C++0x,是 C++ 编程语言的正式标准,它取代了第二版标准ISO/IEC 14882:2003。C++11 的发展背景涉及多个方面,包括语言本身的需求、技术发展的推动以及编程社区的努力。
首先,随着计算机技术的快速发展,软件应用的复杂性和规模都在不断增长,对编程语言的要求也越来越高。C++ 作为一种广泛使用的编程语言,其旧有的标准在某些方面已经无法满足现代软件开发的需求。因此,对 C++ 语言进行改进和扩展,使其更加适应现代软件开发的需求,成为了 C++ 社区的重要任务。
其次,C++ 社区一直在努力推动语言的发展。自 C++ 标准委员会成立以来,他们就一直在不断地收集来自全球的编程人员和专家的反馈和建议,以期对 C++ 语言进行改进。这些建议涵盖了语言的各个方面,包括语法、库函数、性能等。
最后,C++11 的提出和制定也受到了技术发展的推动。在新的计算模型和硬件平台上,C++ 需要提供更多的功能来支持并发编程、泛型编程等现代编程技术。同时,随着编程范式的发展,C++ 也需要提供更多的工具和机制来支持面向对象编程、函数式编程等不同的编程风格。
在 C++11 中,右值引用和转移语义是一个重要的改进点。在旧标准 C++ 语言中,临时量(术语为右值,因其出现在赋值表达式的右边)可以做参数传给函数,但只能被接受为 const & 类型。这样函数便无法区分传给 const & 的是真正的右值还是普通 const 变量。而且,由于类型为 const &,函数也无法改变所传对象的值。C++11 增加了右值引用这种新的引用类型,允许函数接受非 const 值,从而改变其值,并允许创建转移语义。
此外,C++11 还扩展了C++标准程序库,并入了大部分的 C++ Technical Report 1 程序库(数学的特殊函数除外)。这使得 C++11 在标准库方面更加丰富和强大,满足了更多复杂的编程需求。
总的来说,C++11 的发展背景是多方面的,它既是语言自身发展的需要,也是技术发展和编程社区努力的结果。C++11 的推出,标志着 C++ 语言进入了一个新的发展阶段,为现代软件开发提供了更强大的支持和更丰富的功能。
2 C++11 的主要新特性概览
2.1 语言特性改进
(1)auto 关键字
在 C++11 中,auto 关键字被引入用于自动类型推导。这意味着在声明变量时,程序员不必显式指定变量的类型,编译器会根据初始化表达式的类型自动推导。这极大地简化了代码,尤其是在处理复杂类型或模板时。
示例:
auto x = 5; // x 是 int 类型
auto y = 3.14; // y 是 double 类型
auto z = 'a'; // z 是 char 类型
auto it = vec.begin(); // it 是迭代器类型,取决于 vec 的类型
(2)nullptr
nullptr 是 C++11 中引入的一个新关键字,用于表示空指针。它替代了传统上使用宏定义的 NULL(通常定义为 0 或 (void*)0)。使用 nullptr 可以更好地区分空指针和整数零,提高了代码的安全性和可读性。
示例:
int* ptr = nullptr; // 使用 nullptr 表示空指针
if (ptr == nullptr) {
// 处理空指针情况
}
(3)基于范围的 for 循环(Range-based for loop)
基于范围的 for 循环是 C++11 中引入的一种新循环结构,它简化了对容器(如数组、向量、列表等)的遍历。程序员不需要手动管理迭代器或索引,只需要指定循环变量和要遍历的容器。
示例:
std::vector<int> vec = {1, 2, 3, 4, 5};
for (auto& element : vec) {
element *= 2; // 遍历 vec 中的每个元素,并将其乘以 2
}
(4)Lambda 表达式
Lambda 表达式是 C++11 中引入的一个强大特性,它允许程序员定义匿名函数对象(即没有名称的函数)。Lambda 表达式特别适用于需要简短函数对象的情况,如在算法中作为回调函数使用。
示例:
auto add = [](int a, int b) { return a + b; }; // 定义一个 lambda 表达式,用于加法
int sum = add(3, 4); // 调用 lambda 表达式,sum 为 7
// 使用 lambda 表达式在 std::for_each 中
std::vector<int> numbers = {1, 2, 3, 4, 5};
std::for_each(numbers.begin(), numbers.end(), [](int num) {
std::cout << num << ' '; // 打印每个数字
});
(5)decltype
decltype 是 C++11 中引入的一个运算符,用于在编译时推断表达式的类型。它允许程序员获取表达式结果的类型,并可以用作类型别名或变量声明的类型。
示例:
int x = 5;
decltype(x) y = x * 2; // y 的类型是 int,且值为 10
(6)constexpr
constexpr 是 C++11 中引入的一个关键字,用于指定常量表达式。这意味着该表达式在编译时就能被计算出一个常量值,并且在程序的整个生命周期内保持不变。constexpr 可以用于变量、函数或类的构造函数。
示例:
constexpr int fact(int n) {
return n > 0 ? n * fact(n - 1) : 1;
}
constexpr int x = fact(5); // x 的值在编译时就能确定,为 120
(7)函数模板默认参数
在 C++11 中,函数模板可以具有默认参数。这意味着在实例化模板时,如果未提供某些参数,编译器会使用默认值。这增加了函数模板的灵活性,减少了模板特化的需要。
示例:
template <typename T, typename Alloc = std::allocator<T>>
class MyVector {
// ...
};
// 使用默认分配器创建 MyVector 实例
MyVector<int> vec;
2.2 内存管理和安全性
(1)右值引用
右值引用是 C++11 中用于支持移动语义的新特性。它使用 && 符号来声明,允许引用一个临时对象(右值)。移动语义允许对象将其资源(如内存所有权)从一个对象转移到另一个对象,而不是进行复制,从而提高了性能。
示例:
#include <iostream>
#include <string>
#include <vector>
class StringWrapper {
public:
StringWrapper() {}
StringWrapper(std::string&& s) : str(std::move(s)) {
std::cout << "Move constructor called.\n";
}
StringWrapper& operator=(StringWrapper&& other) {
if (this != &other) {
str = std::move(other.str);
std::cout << "Move assignment operator called.\n";
}
return *this;
}
private:
std::string str;
};
int main()
{
std::string s = "Hello";
StringWrapper wrapper1(std::move(s)); // 使用移动构造函数
StringWrapper wrapper2;
wrapper2 = std::move(wrapper1); // 使用移动赋值运算符
return 0;
}
(2)move 构造函数和 move 赋值运算符
move 构造函数和 move 赋值运算符是专门用于处理右值引用的构造函数和赋值运算符。它们使得对象能够利用移动语义进行初始化或赋值,而不是进行深拷贝。
示例(已在上面的右值引用示例中展示):
在上面的 StringWrapper 类中,定义了一个接受右值引用的构造函数和一个 move 赋值运算符。
(3)智能指针
智能指针是 C++11 中用于自动管理动态分配内存的类模板。它们提供了类似指针的接口,但会在适当的时候自动删除所指向的对象。
- unique_ptr:独占所有权的智能指针。
- shared_ptr:共享所有权的智能指针,通过引用计数来管理对象生命周期。
- weak_ptr:观察shared_ptr所指向的对象,不控制对象的生命周期。
unique_ptr 的示例如下:
#include <iostream>
#include <memory>
int main()
{
std::unique_ptr<int> smartPtr(new int(5));
std::cout << *smartPtr << std::endl; // 输出5
// 当smartPtr离开作用域时,它会自动删除所指向的int
return 0;
}
shared_ptr 和 weak_ptr 的示例如下:
#include <iostream>
#include <memory>
struct Foo {
Foo() { std::cout << "Foo::Foo\n"; }
~Foo() { std::cout << "Foo::~Foo\n"; }
};
int main()
{
std::shared_ptr<Foo> sp1(new Foo()); // 引用计数为1
{
std::shared_ptr<Foo> sp2 = sp1; // 复制shared_ptr,引用计数为2
std::weak_ptr<Foo> wp = sp1; // 创建weak_ptr,不影响引用计数
} // sp2离开作用域,引用计数减为1
// wp离开作用域,不影响引用计数
// sp1离开作用域,引用计数减为0,Foo对象被删除
return 0;
}
(4)初始化列表
初始化列表提供了一种统一的初始化语法,可用于初始化各种数据类型,包括数组、结构体和类等。
示例:
#include <iostream>
#include <vector>
#include <initializer_list>
class MyClass {
public:
MyClass(std::initializer_list<int> list) {
for (int val : list) {
data.push_back(val);
}
}
void print() const {
for (int val : data) {
std::cout << val << ' ';
}
std::cout << std::endl;
}
private:
std::vector<int> data;
};
int main()
{
MyClass myObj{1, 2, 3, 4, 5}; // 使用初始化列表
myObj.print(); // 输出:1 2 3 4 5
return 0;
}
(5)类型安全的枚举
C++11 引入了强类型枚举(也称为作用域枚举),它们使用 enum class 或 enum struct 关键字定义。这种枚举类型不会自动转换为整型,且其枚举值的作用域限定在枚举类型内部,从而提高了类型安全性。
示例:
#include <iostream>
enum class Color {
RED,
GREEN,
BLUE
};
int main()
{
Color myColor = Color::RED;
// 以下代码会报错,因为Color::RED不是一个int类型
// int intColor = Color::RED;
// 如果需要显式转换到整型,可以使用static_cast
int intColor = static_cast<int>(myColor);
std::cout << "My color is: ";
switch (myColor) {
case Color::RED:
std::cout << "Red";
break;
case Color::GREEN:
std::cout << "Green";
break;
case Color::BLUE:
std::cout << "Blue";
break;
default:
std::cout << "Unknown";
}
std::cout << std::endl;
return 0;
}
在上面的例子中,Color 是一个强类型枚举。开发者不能直接将其值赋给一个整型变量,而必须使用 static_cast 进行显式转换。此外,Color 的枚举值(如 Color::RED)的作用域被限制在 Color 枚举类型内部,这有助于避免命名冲突和提高代码的可读性。
2.3 泛型编程增强
(1)类型别名(Type Aliases)
类型别名是 C++11 引入的一个新特性,它通过 using 关键字为复杂的类型定义一个新的、更简洁的名字。从而提供了比传统的 typedef 更直观、更灵活的语法。
样例:
#include <iostream>
#include <vector>
// 使用 using 定义类型别名
using Vec = std::vector<int>;
int main()
{
Vec v; // 等同于 std::vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
for (int i : v) {
std::cout << i << ' ';
}
std::cout << std::endl;
return 0;
}
(2)模板别名(Template Aliases)
模板别名允许为模板实例化定义别名,这有助于简化复杂的模板表达式,特别是在处理嵌套模板或模板模板参数时。
样例:
#include <iostream>
#include <vector>
// 模板别名
template<typename T>
using Vec = std::vector<T>;
int main()
{
Vec<int> vi; // 等同于 std::vector<int> vi;
Vec<double> vd; // 等同于 std::vector<double> vd;
// ... 使用 vi 和 vd ...
return 0;
}
(3)std::tuple
std::tuple 是一个固定大小的异构序列容器,它允许将不同类型的对象组合成一个单一的对象。这在编写泛型代码时特别有用,尤其是当需要返回多个值或存储不同类型的元素时。
样例:
#include <iostream>
#include <tuple>
#include <string>
int main()
{
// 创建一个包含不同类型元素的 tuple
std::tuple<int, double, std::string> t(1, 3.14, "Hello");
// 使用 std::get 访问 tuple 中的元素
std::cout << std::get<0>(t) << ", "
<< std::get<1>(t) << ", "
<< std::get<2>(t) << std::endl;
return 0;
}
(4)std::forward
std::forward 是一个用于完美转发(Perfect Forwarding)的函数模板,它保持了传递给它的值的类别(lvalue 或 rvalue)。在模板编程中,这是非常有用的,特别是当需要保持传递给包装函数(wrapper function)的参数的原始类别时。
样例:
#include <iostream>
#include <utility> // for std::forward
void process(int& i) {
std::cout << "Lvalue int\n";
}
void process(int&& i) {
std::cout << "Rvalue int\n";
}
template<typename T>
void wrapper(T&& t) {
// 使用 std::forward 完美转发 t
process(std::forward<T>(t));
}
int main()
{
int x = 5;
wrapper(x); // 输出 "Lvalue int"
wrapper(std::move(x)); // 输出 "Rvalue int"
return 0;
}
在这个例子中,wrapper 函数模板接收一个右值引用包装类型 T&&。通过 std::forward<T>(t),可以确保传递给 process 的参数保持了其原始的类别(lvalue 或 rvalue),从而可以正确地调用重载的 process 函数。
2.4 标准库扩展
(1)容器初始化
C++11 引入了列表初始化(List Initialization),允许使用花括号 {} 来初始化容器,如 std::vector、std::list、std::array 等。这种初始化方式不仅更加直观,还能防止某些类型的错误,比如无意中插入了额外的元素。
样例:
#include <vector>
#include <iostream>
int main()
{
std::vector<int> vec = {1, 2, 3, 4, 5}; // 使用列表初始化
for (int num : vec) {
std::cout << num << ' ';
}
std::cout << std::endl;
return 0;
}
(2)无序容器
C++11 引入了基于哈希的无序容器,如 std::unordered_map 和 std::unordered_set。这些容器在插入、删除和查找元素时通常比基于红黑树的有序容器(如 std::map 和 std::set)更快,因为它们的复杂度在平均情况下是常数时间。但是,它们不保证元素的顺序。
样例:
#include <unordered_map>
#include <iostream>
#include <string>
int main()
{
std::unordered_map<std::string, int> umap;
umap["apple"] = 1;
umap["banana"] = 2;
umap["cherry"] = 3;
for (const auto& pair : umap) {
std::cout << pair.first << ": " << pair.second << std::endl;
}
return 0;
}
(3)正则表达式库
C++11 引入了 <regex> 头文件,提供了正则表达式库,用于处理复杂的文本匹配和搜索任务。这个库提供了正则表达式类、匹配算法以及相关的迭代器,使得在 C++ 中使用正则表达式变得更加简单和直接。
样例:
#include <iostream>
#include <regex>
#include <string>
int main()
{
std::string s = "Hello, 123 World!";
std::regex e ("\\d+"); // 匹配一个或多个数字
// 查找所有匹配项
for (std::sregex_iterator i = std::sregex_iterator(s.begin(), s.end(), e);
i != std::sregex_iterator(); ++i) {
std::smatch match = *i;
std::string match_str = match.str();
std::cout << match_str << std::endl;
}
return 0;
}
(4)线程库
C++11 引入了 <thread> 头文件,提供了基本的线程支持,允许程序员编写多线程程序。这包括创建线程、等待线程完成、同步线程等操作。
样例:
#include <iostream>
#include <thread>
#include <chrono>
void thread_function() {
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "Hello from thread!\n";
}
int main()
{
std::thread t(thread_function);
t.join();
return 0;
}
(5)原子操作
C++11 提供了原子类型和原子操作,以支持多线程环境下的并发编程。原子操作是不可中断的操作,即在多线程环境中,一个线程执行原子操作时,不会被其他线程打断。这有助于防止数据竞争和其他并发问题。
样例:
#include <iostream>
#include <atomic>
#include <thread>
std::atomic<int> counter(0);
void increment() {
for (int i = 0; i < 1000; ++i) {
++counter;
}
}
int main()
{
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << "Final counter value: " << counter << std::endl;
return 0;
}
2.5 其他特性
(1)统一的初始化语法
在 C++11 中,引入了大括号{}作为统一的初始化语法,可以用于初始化各种类型,包括数组、容器、自定义类型等。这种语法不仅使得代码更加简洁易读,也解决了一些旧式初始化方法存在的问题(比如隐式类型转换)。
样例:
std::vector<int> vec = {1, 2, 3, 4, 5}; // 初始化vector
int arr[3] = {1, 2, 3}; // 初始化数组
struct Point { int x, y; };
Point p = {10, 20}; // 初始化自定义类型
(2)用户定义的字面量
用户定义的字面量允许程序员为字面量添加自定义后缀,并定义该后缀的行为。这通常用于创建类型安全的字面量,或者为字面量提供特定语义。
样例:
constexpr long double operator"" _km(long double km) {
return km * 1000.0L; // 转换为米
}
int main()
{
long double distance = 5.0_km; // 使用自定义字面量后缀
std::cout << distance << " meters\n"; // 输出:5000 meters
return 0;
}
(3)对齐控制
C++11 提供了对变量和内存对齐的控制。这主要通过 alignas 关键字和 alignof 运算符实现。对齐控制可以影响对象的布局和性能,特别是在处理硬件访问和 SIMD 操作(单指令多数据(Single Instruction, Multiple Data),是一种并行计算的技术,它允许处理器同时对多个数据元素执行相同的操作)时尤为重要。
样例:
struct alignas(16) AlignedStruct {
int data;
};
int main()
{
std::cout << "Alignment of AlignedStruct: " << alignof(AlignedStruct) << std::endl;
return 0;
}
(4)范围 for 循环的 begin 和 end 函数
在 C++11 中,范围for循环(也称为基于范围的 for 循环)被引入,用于遍历容器或数组的元素。为了支持自定义类型,需要为这些类型提供 begin() 和 end() 成员函数或全局函数,以返回迭代器。
样例(假设有一个自定义容器类 MyContainer):
#include <iostream>
#include <iterator>
class MyContainer {
public:
int data[5] = { 1, 2, 3, 4, 5 };
// 自定义begin函数
int* begin() { return data; }
// 自定义end函数
int* end() { return data + 5; }
};
int main()
{
MyContainer cont;
// 使用范围for循环遍历自定义容器
for (const auto& elem : cont) {
std::cout << elem << ' ';
}
return 0;
}
(5)对原始字符串字面量的支持
原始字符串字面量允许定义包含换行符和引号等字符的字符串,而无需对它们进行转义。这通过前缀R和一个可选的括号内的分隔符来实现。
样例:
const char* rawStr = R"(Hello,
world!
This is a raw string.)";
std::cout << rawStr << std::endl;
(6)对 Unicode 的深入支持
C++11 增强了对 Unicode 字符和字符串的处理能力,包括新的字符字面量(如 \uXXXX 和 \UXXXXXXXX),以及用于处理宽字符和 UTF-8、UTF-16、UTF-32 编码的字符串的函数。如模板类 std::wstring_convert 提供了在宽字符字符串(如std::wstring)和其他类型的字符串(如std::string)之间进行转换的功能。
但是在 C++17 中,std::wstring_convert 和 std::wbuffer_convert 被标记为已弃用,并在 C++20 中被移除。这是因为 C++ 标准库试图减少对特定编码的依赖,并鼓励使用更通用的解决方案。如果正在使用 C++17 或更高版本,并且需要处理编码转换,可能需要使用第三方库,如 ICU(International Components for Unicode),或者依赖于平台特定的 API。