本章博客介绍C++11常用的新特性,分为非常常用的11种和比较常用的10种,读者可根据需求自行查看。
图:
目录
前言:
C++新特性
非常常用的特性:
比较常用的特性:
非常常用10种:
1. auto关键字
1.1 基本用法
1.2 与 `const`、`&` 和 `*` 结合使用
1.3 用于范围基于的 for 循环
1.4 使用注意事项
1.4.1 初始化值必须存在
1.4.2 类型一致性
1.4.3 列表初始化
1.4.4 复杂类型推断
2. 基于范围的 for 循环
2.1 基本语法
2.2 遍历数组
2.3 遍历容器
2.4 使用 `auto` 和 `const`
3. 智能指针
3.1 `std::unique_ptr`
3.3 `std::weak_ptr`
3.4 使用建议
4. 初始化列表
4.1 统一初始化
4.2 防止窄化转换
4.3 初始化容器
4.4 类成员的初始化
4.5 自定义初始化列表构造函数
5. nullptr
5.1 基本用法
5.2 与 `NULL` 和 `0` 的区别
5.2.1 类型安全
5.2.2 函数重载
5.3 使用注意事项
6. Lambda 表达式
6.1 基本语法
6.2 参数解释
6.2.1 `capture`:捕获列表
6.2.2 `parameter_list`:参数列表
6.2.3 `mutable`:可变说明符
6.2.4 `exception_attribute`:异常规范
6.2.5 `return_type`:返回类型
6.2.6 `function_body`:Lambda表达式主体
7. 静态断言(static_assert)
7.1 基本语法
7.2 使用示例
7.2.1 检查类型大小
7.2.2 检查模板类型
8. 右尖括号
8.1 C++03的问题
8.2 C++11的改进
9. using类型别名声明
9.1 基本语法
9.1.1 使用 `typedef` 声明类型别名
9.1.2 使用 `using` 声明类型别名
9.2 模板别名
9.3 使用场景
10. 默认函数和删除函数(default和delete)
11. 原始字面量
11.1 基本语法
11.2 使用示例
11.2.1 基本用法
11.2.2 包含转义字符的字符串
11.2.3 多行字符串
11.2.4 使用分隔符
比较常用10种:
1. constexpr
1.1 `constexpr`变量
1.2 `constexpr`函数
1.3 `constexpr`和数组
1.4 `constexpr`在C++14及以后
2. 强枚举类型(class enum)
2.1 基本语法
2.2 强类型
2.3 作用域
2.4 使用示例
2.5 类型转换
3. 多线程支持
3.1 线程创建与管理
3.2 线程传参
3.3 锁和同步
3.3.1 使用`std::mutex`
3.3.2 条件变量
3.3.3 原子操作
4. 用户定义字面量
4.1 基本语法
4.2 使用示例
4.2.1 整数字面量
4.2.2 浮点数字面量
4.2.3 字符串字面量
4.2.4 原始字符串字面量
5. 变长模版
5.1 语法
5.2 使用示例
5.2.1 函数模板
5.2.2 类模板
5.3 折叠表达式(C++17)
6. 委托构造函数
6.1基本语法
6.2 使用示例
6.3 注意事项
7. 属性
7.1 基础语法
7.2 常见属性
7.2.1 `[[nodiscard]]`
7.2.2 `[[noreturn]]`
7.2.3 `[[deprecated]]`和`[[deprecated("reason")]]`
7.2.4 `[[maybe_unused]]`
7.2.5 `[[fallthrough]]`
8. 继承构造函数
8.1 基本语法
8.2 使用示例
8.3 注意事项
8.3.1 派生类成员初始化
8.3.2 隐藏基类构造函数
9. 无序容器
9.1 无序容器的基本用法
9.1.1 `std::unordered_set`和`std::unordered_multiset`
9.1.2 `std::unordered_map`和`std::unordered_multimap`
9.2 哈希函数和相等函数
9.3 性能考虑
10. 移动语义和右值引用
10.1 右值和右值引用
10.2 移动语义
10.3 移动构造函数和移动赋值运算符
10.4 使用`std::move`
10.5 使用场景
auto
关键字:自动类型推断可以减少代码的复杂性,并提高可读性。
基于范围的 for
循环:允许更加简洁和安全地遍历容器。
智能指针(std::shared_ptr
, std::unique_ptr
, std::weak_ptr
): 自动管理内存,减少内存泄漏的风险。
初始化列表: 提供了一种更加一致和简洁的对象初始化方法。
nullptr:
更加清晰地表示空指针。
Lambda 表达式: 用于创建简洁的匿名函数对象。
static_assert:
在编译时执行断言。
右尖括号(>>
): 模板可以使用 >>
而不是 > >
。
类型别名声明(using
): 替代 typedef
,语法更清晰。
默认函数和删除函数(default
和 delete
): 显式地指定函数为默认实现或删除它。
原始字面量:所有字符都没有特殊含义,即所有字符都表示字面值,不进行转义
constexpr:
编译时计算表达式的值。
强类型枚举(enum class
): 更加安全和强大的枚举类型。
多线程支持:原生支持多线程和同步。
用户定义字面量: 允许定义自己的字面量。
变长模板: 支持任意数量和类型的模板参数。
委托构造函数: 在同一类中的一个构造函数里调用另一个构造函数。
属性: 提供一种新的元编程工具。
继承构造函数: 子类可以继承父类的构造函数。
无序容器: 提供了基于哈希的容器,例如 std::unordered_map
。
移动语义和右值引用: 支持移动语义,提高性能。
`auto` 关键字在 C++11 中被引入,主要用于自动类型推断,从而可以减少类型声明的冗余。当您用 `auto` 声明一个变量时,编译器会自动推断该变量的类型,基于该变量被初始化的值。
auto i = 5; // i 的类型被推断为 int
auto d = 5.0; // d 的类型被推断为 double
auto s = "hello"; // s 的类型被推断为 const char*
auto v = std::vector{1, 2, 3}; // v 的类型被推断为 std::vector
`auto` 可以和 `const`、`&`、`*` 等修饰符结合使用,来推断出正确的类型。
const auto ci = 42; // ci 的类型为 const int
auto& ri = ci; // ri 的类型为 const int&,引用到 ci
auto* pi = &ci; // pi 的类型为 const int*,指针指向 ci
`auto` 经常用于范围基于的 `for` 循环中,以避免显式声明迭代器的类型。
std::vector vec = {1, 2, 3, 4};
for (auto& elem : vec) { // elem 的类型为 int&
std::cout << elem << ' ';
}
当使用 `auto` 声明变量时,必须要有一个初始值,否则编译器无法推断出变量的类型。
auto x; // 错误,不能推断类型
当使用 `auto` 声明多个变量时,这些变量的类型必须是一致的。
auto i = 5, d = 5.0; // 错误,类型不一致
当使用大括号进行列表初始化时,`auto` 会被推断为 `std::initializer_list
auto x = {1, 2, 3}; // x 的类型为 std::initializer_list
在一些复杂的类型推断场景中,`auto` 可能会推断出非预期的类型,需要开发者注意。
std::vector vec = {1, 2, 3};
auto a = vec[0]; // a 的类型为 int
auto& b = vec[0]; // b 的类型为 int&
`auto` 关键字能够显著提高代码的可读性和编写效率,它能够在不失类型安全的前提下,减少冗余的类型声明,尤其在处理复杂类型或模板类型时尤为有用。
for
循环C++11 引入了基于范围的 `for` 循环(也被称为范围 `for` 循环),它提供了一种更简洁、更可读的方式来遍历数组和容器。
基于范围的 `for` 循环的基本语法如下:
for (declaration : expression) statement
- declaration:定义了在循环体内可见的变量,该变量代表序列中的当前元素。
- expression:表示可遍历的序列(如数组或容器)。
- statement:循环体,每次迭代都会执行。
int arr[] = {1, 2, 3, 4, 5};
for(const auto& num : arr) {
std::cout << num << ' ';
}
std::vector vec = {1, 2, 3, 4, 5};
for(const auto& num : vec) {
std::cout << num << ' ';
}
在基于范围的 `for` 循环中,经常使用 `auto` 来自动推断元素的类型,并使用 `const` 来保护元素不被修改(如果不需要修改元素的值)。
std::vector words = {"apple", "banana", "cherry"};
for(const auto& word : words) {
std::cout << word << ' ';
}
基于范围的 `for` 循环是 C++11 中的一项非常方便的特性,它简化了遍历序列(如数组和容器)的代码,并提高了代码的可读性和可维护性。与传统的 `for` 循环相比,基于范围的 `for` 循环更简洁,且减少了出错的可能性,因为不再需要手动管理迭代器或索引。
C++11中引入了三种智能指针,用于管理动态分配的对象的生命周期:`std::unique_ptr`,`std::shared_ptr`和`std::weak_ptr`。智能指针相较于原始指针,提供了自动的、异常安全的内存管理,避免了内存泄漏和悬挂指针的问题。
`std::unique_ptr` 是一种独占所有权的智能指针,每个`std::unique_ptr`都独占一个动态分配的对象。当`std::unique_ptr`被销毁时,它所指向的对象也会被销毁。
#include
#include // 必须包含此头文件来使用 std::unique_ptr
class MyClass {
public:
MyClass(int value) : value_(value) {}
void printValue() const { std::cout << "Value: " << value_ << '\n'; }
private:
int value_;
};
int main() {
std::unique_ptr ptr = std::make_unique(10); // C++14 起可用
// 或者在 C++11 中:
// std::unique_ptr ptr(new MyClass(10));
ptr->printValue(); // 输出: Value: 10
// 当 main 函数返回时,ptr 会被销毁,并自动删除所指向的对象
}
`std::unique_ptr`不能被复制,但可以被移动。这意味着你可以将`std::unique_ptr`从一个对象转移到另一个对象,但不能拷贝。
int main() {
std::unique_ptr ptr1 = std::make_unique(10);
std::unique_ptr ptr2 = std::move(ptr1); // ptr1 被移动到 ptr2
if(ptr1) ptr1->printValue(); // ptr1 现在为空,此行不会执行
if(ptr2) ptr2->printValue(); // 输出: Value: 10
// 当 main 函数返回时,ptr2 会被销毁,并自动删除所指向的对象
}
std::unique_ptr
还允许你提供一个自定义的删除器,用于在std::unique_ptr
销毁时释放资源。
管理非内存资源:
当你使用 std::unique_ptr
管理如文件、网络套接字、数据库连接等非内存资源时,可以通过自定义删除器来关闭文件、断开连接等。
使用自定义内存分配器:
当对象需要通过特定的分配器创建和销毁时,自定义删除器可以确保正确的销毁方式被调用。
#include
#include
class MyClass {
public:
MyClass() { std::cout << "MyClass constructed\n"; }
~MyClass() { std::cout << "MyClass destructed\n"; }
};
int main() {
auto deleter = [](MyClass* ptr) {
std::cout << "Custom deleter called\n";
delete ptr;
};
std::unique_ptr ptr(new MyClass, deleter);
// 输出:
// MyClass constructed
}
// 当 main 函数退出时,ptr 将离开作用域,触发自定义删除器的调用,并输出:
// Custom deleter called
// MyClass destructed
`std::shared_ptr`允许多个`std::shared_ptr`共享同一个对象。`std::shared_ptr`使用引用计数来跟踪有多少个`shared_ptr`对象共享同一块内存。当最后一个`shared_ptr`被销毁时,它所指向的对象也会被销毁。
#include
std::shared_ptr ptr1 = std::make_shared(10);
std::shared_ptr ptr2 = ptr1; // ptr1和ptr2共享同一个对象
`std::weak_ptr`是一种不控制所指向对象生存期的智能指针,它需要与`std::shared_ptr`一同使用。`std::weak_ptr`可以观察`std::shared_ptr`所管理的对象,但不会增加其引用计数。当所观察的对象被销毁时,`std::weak_ptr`可以检测到这一点。
std::shared_ptr shared = std::make_shared(10);
std::weak_ptr weak = shared; // weak观察shared
if(auto observe = weak.lock()) { // 检测所观察的对象是否还存在
// 仍存在
} else {
// 已被销毁
}
- 优先使用`std::unique_ptr`:当你不需要共享所有权时,`std::unique_ptr`是一个低开销、简单的选择。
- 需要共享所有权时使用`std::shared_ptr`:当多个指针需要共享同一个对象时,使用`std::shared_ptr`。
- 避免循环引用:当使用`std::shared_ptr`时,注意避免循环引用,因为这会导致内存泄漏。在可能的情况下,使用`std::weak_ptr`来打破循环。
智能指针是现代C++中管理动态分配的对象的推荐方式。通过在作用域结束时自动释放资源,智能指针帮助开发者避免内存泄漏和管理资源的复杂性,使得代码更加安全和易于维护。
初始化列表(Initializer List),它允许使用大括号 `{}` 进行统一且一致的初始化。初始化列表不仅能用于内置类型、自定义类型和容器,还能用于类的成员变量的初始化。
使用初始化列表,您可以对几乎所有的对象进行初始化,这使得初始化的语法更加一致。
int i{5}; // 基本类型
std::string s{"Hello"}; // 标准库类型
std::vector v{1, 2, 3, 4}; // 容器
初始化列表在某些情况下可以防止窄化转换,比如浮点数转换为整数时。
int i{4.5}; // 编译错误,防止了窄化转换
初始化列表特别适合于初始化标准容器。
std::vector vec{1, 2, 3, 4}; // 初始化 std::vector
std::map m{{1, "one"}, {2, "two"}}; // 初始化 std::map
您可以使用初始化列表来初始化类的成员变量。
class MyClass {
public:
MyClass(int a, int b) : a_{a}, b_{b} {} // 使用初始化列表初始化成员变量
private:
int a_;
int b_;
};
当类提供了接受 `std::initializer_list
class MyClass {
public:
MyClass(std::initializer_list init_list) {
for(const auto& elem : init_list) {
// 处理初始化列表中的每个元素
}
}
};
MyClass obj{1, 2, 3, 4}; // 使用初始化列表构造对象
初始化列表为 C++ 带来了一种更加简洁和一致的初始化语法,无论是基本类型、自定义类型还是容器。它使得代码更加清晰、简洁,而且更加安全,尤其是在防止窄化转换和初始化容器时。在现代 C++ 编程中,初始化列表已经成为推荐的初始化方式。
`nullptr` 是 C++11 引入的一个新关键字,用于表示指针的空值,用以替代 C++03 中的 `NULL` 宏或 `0`。
`nullptr` 主要用于给指针类型变量赋空值。`nullptr` 有自己的类型,称为 `nullptr_t`,它可以隐式转换为所有原始指针类型和指向成员的指针类型。
int* pi = nullptr;
char* pc = nullptr;
void (*func_ptr)() = nullptr;
与 `NULL` 和 `0` 相比,`nullptr` 提供了类型安全。例如,下面的代码会产生编译错误,因为 `nullptr` 不能转换为整数类型。
int i = nullptr; // 编译错误
`nullptr` 是一个字面常量,它有自己的类型 `nullptr_t`,它可以被转换为任何指针类型,但不能被转换为整数类型,这带来了类型安全的优势。
在涉及函数重载的场景中,`nullptr` 也表现出了其优越性。考虑下面的代码:
void foo(int);
void foo(char*);
foo(0); // 调用 void foo(int)
foo(nullptr); // 调用 void foo(char*)
当使用 `0` 或 `NULL` 时,编译器会将其看作整数类型,导致调用 `void foo(int)`;而使用 `nullptr` 时,会正确地调用 `void foo(char*)`。
`nullptr` 是 C++11 引入的一个关键特性,用于更加清晰、安全地表示空指针,解决了 `NULL` 和 `0` 在某些情况下可能导致的类型混淆和错误。在现代 C++ 编程中,`nullptr` 已经成为初始化和比较指针的推荐方式。
Lambda 表达式允许定义匿名的、内联的、局部函数对象。Lambda 表达式常用于短生命周期的场合,例如算法的自定义比较器,或者作为某个函数的回调函数参数。
Lambda 表达式的基本语法如下:
[capture](parameter_list) mutable(optional) exception_attribute -> return_type { function_body }
其中:
- `capture`:捕获列表,定义了从外部作用域捕获变量的方式(按值、按引用)。
- `parameter_list`:参数列表,与普通函数的参数列表相似。
- `mutable`:用于指明 Lambda 表达式是否可以修改按值捕获的变量。
- `exception_attribute`:异常规范,用于声明 Lambda 表达式可能抛出的异常。
- `return_type`:返回类型,如果省略,则由编译器自动推断。
- `function_body`:Lambda 表达式的主体。
捕获列表定义了Lambda表达式可以访问的外部作用域中的变量,并指定是按值捕获还是按引用捕获。
按值捕获:外部变量在Lambda表达式内部是不可变的,除非使用`mutable`关键字。
int value = 1;
auto lambda = [value] { std::cout << value << std::endl; }; // 按值捕获
按引用捕获:Lambda表达式可以修改外部变量的值。
int value = 1;
auto lambda = [&value] { value = 2; }; // 按引用捕获
混合捕获:可以同时使用按值和按引用捕获。
int a = 1, b = 2;
auto lambda = [a, &b] { /* ... */ };
默认按值捕获:`[=]`表示默认按值捕获所有外部变量。
int a = 1, b = 2;
auto lambda = [=] { return a + b; };
默认按引用捕获:`[&]`表示默认按引用捕获所有外部变量。
int a = 1, b = 2;
auto lambda = [&] { b = a + b; };
与普通函数的参数列表类似,用于定义Lambda表达式接收的参数。
auto lambda = [](int x, int y) { return x + y; };
`mutable`用于允许Lambda修改按值捕获的外部变量,并且可以让Lambda表达式拥有和非`const`成员函数相同的行为。
int value = 1;
auto lambda = [value]() mutable { value++; std::cout << value << std::endl; }; // Prints 2
用于指定Lambda表达式可能抛出的异常。
auto lambda = []() noexcept { /* ... */ }; // This lambda is declared as not throwing any exceptions
可以明确指定Lambda表达式的返回类型。如果省略,编译器会自动推断返回类型。
auto lambda = [](int x, int y) -> int { return x + y; };
包含Lambda表达式的实现代码,与普通函数的函数体类似。
auto lambda = [] { std::cout << "Lambda Body" << std::endl; };
理解Lambda表达式的这些组成部分可以帮助您更灵活、更有效地使用此特性,编写出更加简洁和优雅的现代C++代码。
`static_assert`是一种在编译时进行断言的机制,也被称为静态断言。`static_assert`用于在编译时检查常量表达式的值,如果表达式的值为`false`,则会产生一个编译错误,阻止代码的构建。
`static_assert`的基本语法如下:
static_assert(constant_expression, diagnostic_message);
以下是一些使用`static_assert`的示例。
static_assert(sizeof(int) == 4, "int type is not 4 bytes long!");
在模板编程中,`static_assert`特别有用,可以用来检查模板类型是否满足某些条件。
template
class MyArray {
static_assert(std::is_integral::value, "MyArray requires integral types!");
};
如果尝试使用不满足条件的类型实例化模板,如下所示:
MyArray myArray; // This will trigger a static_assert and give a compile error
编译器将产生一个错误,并显示`static_assert`中指定的`diagnostic_message`。
检查编译时常量:
constexpr int val = 10;
static_assert(val == 10, "val is not 10!");
`static_assert`是一种强大的工具,可用于在编译时执行各种检查,从而确保满足某些条件和约束。它通常用于类型检查、模板编程和验证编译时常量的值。通过在编译时捕获错误,`static_assert`可以帮助开发者更早地发现问题,提高代码质量,并减少运行时错误。
在C++11之前的标准中,模板的嵌套声明会导致一个著名的语法问题,即“右尖括号(`>>`)”。如果模板参数之间存在连续的右尖括号,编译器会将它们解释为右移运算符(`>>`),导致语法错误。
在C++03中,如果你写下如下代码:
std::vector> vec; // C++03中这是错误的
编译器会报错,因为`>>`会被解释为右移运算符。解决这个问题的方法是在两个右尖括号之间加一个空格:
std::vector > vec; // C++03中这是正确的
从C++11开始,这个问题得到了解决。编译器新增了一个特性,允许在模板参数列表中使用连续的右尖括号,而不会将其解释为运算符。因此,在C++11及以后的版本中,以下写法是完全合法的:
std::vector> vec; // C++11及以后版本中这是正确的
这个改进虽然较小,但却极大地提高了模板代码的可读性和易写性,特别是对于包含多层嵌套模板的代码。
C++11 引入了一种新的类型别名声明——`using`,作为 `typedef` 的替代方案。`using` 的语法更加清晰直观,尤其是在声明模板别名时,`using` 比 `typedef` 更具优势。
typedef std::vector IntVector;
using IntVector = std::vector;
这两种声明方式都将 `IntVector` 定义为 `std::vector
`using` 的真正优势在于可以用来声明模板别名,而 `typedef` 则无法实现这一点。
例如,我们可以创建一个模板,它可以将任何类型的 `std::vector` 转换为相应的 `std::shared_ptr`:
template
using VecSharedPtr = std::shared_ptr>;
这样,我们就可以使用 `VecSharedPtr
`using` 特别适合于以下几种场景:
- 为复杂的类型或模板类型定义简短、易读的别名。
- 为模板类型定义模板别名。
using IntMap = std::unordered_map;
using StrMap = std::unordered_map;
template
using Ptr = T*;
9.4 类型别名 vs. 继承
虽然类型别名和继承都能达到类似的效果,但它们的用途是不同的。类型别名只是给类型提供了另一个名称,而不会创建新的类型。继承会创建新的类型,并可能带来虚函数表等开销。
例如:
using IntVector = std::vector; // 类型别名,不会创建新类型
class MyVector : public std::vector {}; // 创建新类型
`using` 关键字在C++11中被引入,为类型提供了一种新的别名定义方法,特别是在定义模板别名时,`using` 显得更加灵活和直观。与 `typedef` 相比,`using` 有更清晰的语法,并且能够更好地处理复杂的模板别名,提高代码的可读性和可维护性。
在C++11中,引入了`default`和`delete`关键字,允许显式地声明编译器应该为类生成(或不生成)某些特殊成员函数。这些特殊成员函数包括默认构造函数、拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符和析构函数。
10.1 `default`
`default`关键字用于显式地要求编译器生成默认实现的特殊成员函数。这对于需要定义其他构造函数但仍希望保留默认构造函数的情况很有用。
例如,如果你声明了一个拷贝构造函数,编译器就不会自动生成默认构造函数。如果你仍然希望有一个由编译器生成的默认构造函数,可以这样做:
class MyClass {
public:
MyClass(const MyClass& other) {
// 自定义拷贝构造函数
}
MyClass() = default; // 显式声明默认构造函数
};
10.2 `delete`
`delete`关键字用于显式地禁止编译器为类生成特殊成员函数。这在你不希望类对象被拷贝或赋值时特别有用。
例如,如果你不希望你的类对象被拷贝,可以这样做:
class NonCopyable {
public:
NonCopyable(const NonCopyable& other) = delete; // 禁止拷贝构造
NonCopyable& operator=(const NonCopyable& other) = delete; // 禁止拷贝赋值
};
如果尝试对这个类进行拷贝或赋值,编译器会报错。
10.3 组合使用
`default`和`delete`也可以组合使用,以定义类的行为。例如,可以允许移动但禁止拷贝:
class MoveOnly {
public:
MoveOnly(const MoveOnly& other) = delete; // 禁止拷贝构造
MoveOnly& operator=(const MoveOnly& other) = delete; // 禁止拷贝赋值
MoveOnly(MoveOnly&& other) noexcept = default; // 允许移动构造
MoveOnly& operator=(MoveOnly&& other) noexcept = default; // 允许移动赋值
};
`default`和`delete`关键字提供了一种显式和简洁的方式来控制类的特殊成员函数。通过`default`,开发者可以显式地要求编译器为类生成默认实现的特殊成员函数;而通过`delete`,开发者可以禁止类生成特殊成员函数,防止类对象被拷贝或赋值。这两个关键字一起使得类的设计和实现更加清晰和灵活。
原始字符串字面量(Raw String Literals)是C++11中引入的一种新的字符串字面量,它使得在字符串中表示特殊字符,特别是转义字符,变得更加简单和直观。原始字符串字面量中的所有字符都没有特殊含义,即所有字符都表示字面值,不进行转义。
原始字符串字面量的基本语法如下:
R"(delimiter( raw_characters )delimiter)"
- `R"`和`"`之间的`delimiter`是一个可选的分隔符,可以是零个或多个字符。该分隔符用于标识原始字符串字面量的结束。
- `raw_characters`是原始字符串字面量中的字符序列。
下面是一个没有分隔符的原始字符串字面量的例子:
const char* s = R"(Hello, World!)";
这个原始字符串字面量等价于传统的字符串字面量:
const char* s = "Hello, World!";
原始字符串字面量非常适合用来表示包含很多转义字符的字符串,例如正则表达式:
// 原始字符串字面量
const char* regex = R"((\d\d\d)-\d\d\d)";
// 传统字符串字面量
const char* regex_equivalent = "(\\d\\d\\d)-\\d\\d\\d";
原始字符串字面量也可以很方便地表示多行字符串:
const char* multi_line_string = R"(This is a string
that spans multiple lines
without any extra effort.)";
如果原始字符串字面量中需要包含`)"`序列,那么就需要使用分隔符来避免提前结束字面量:
const char* s = R"delimiter(A string with a )" inside it)delimiter";
原始字符串字面量提供了一种简洁、清晰的方式来表示包含转义字符和多行的字符串,使得这类字符串的处理变得更加简单和直观。通过适当地使用原始字符串字面量,开发者可以避免处理繁琐的转义序列,提高代码的可读性和可维护性。
`constexpr`是C++11引入的一个关键字,用于指定一个表达式在编译时必须得到计算。当`constexpr`用于变量时,表示变量是一个编译时常量;当它用于函数或方法时,表示函数或方法是一个常量表达式,可以在需要常量表达式的地方使用,例如模板参数、数组长度、枚举值、`case`表达式等。
`constexpr`变量必须在定义时进行初始化,并且初始化的表达式也必须是一个常量表达式。
constexpr int a = 1;
constexpr int b = a + 1; // 正确
int c = 2;
constexpr int d = c + 1; // 错误,c不是一个常量表达式
`constexpr`函数是指能够在编译时计算结果的函数。这样的函数需满足以下条件:
- 函数体必须非常简单,只能包含一条`return`语句,不能有其他的语句。
- 所有传入参数和`return`语句中的表达式都必须是常量表达式。
- 从C++14开始,`constexpr`函数的限制放宽了,允许包含更多的语句,如循环和条件语句。
constexpr int add(int a, int b) {
return a + b;
}
constexpr int sum = add(1, 2); // 在编译时计算
`constexpr`也常用于数组的大小声明中,因为数组的大小必须是一个常量表达式。
constexpr int size = 10;
int arr[size]; // 正确
在C++14及以后的版本中,`constexpr`函数的限制有所减轻,允许了更复杂的计算和逻辑,例如条件语句和循环。
// C++14
constexpr int factorial(int n) {
int result = 1;
for(int i = 1; i <= n; ++i)
result *= i;
return result;
}
constexpr int val = factorial(5); // 编译时计算
`constexpr`关键字允许开发者在编译时计算表达式的值,从而提高运行时的性能。`constexpr`可以应用于变量、函数和方法,它们分别用于声明编译时常量、定义在编译时计算结果的函数以及指定可用于常量表达式的方法。在C++14及后续标准中,`constexpr`函数的使用变得更加灵活和方便。
C++11引入了一种新的枚举类型,称为强类型枚举(`enum class`),也称为作用域枚举(Scoped Enumeration)。它是对传统枚举类型(`enum`)的一个改进,提供了更强的类型安全和更好的作用域封装。
enum class EnumName : UnderlyingType {
Enumerator1,
Enumerator2,
...
};
- `EnumName` 是枚举类型的名称。
- `UnderlyingType` 是底层类型,它是可选的,用来指定枚举值的存储格式,如`int`、`char`等。如果不指定,将默认使用`int`。
- `Enumerator1`、`Enumerator2` 等是枚举项。
`enum class`是强类型的,这意味着您不能直接将其值与整数进行比较或赋值,这有助于避免意外的类型转换和逻辑错误。
enum class Color {
Red,
Green,
Blue
};
Color color = Color::Red;
if(color == 1) {} // 错误,无法与int进行比较
强类型枚举具有自己的作用域,这意味着每个枚举项都是枚举类型的一部分,不会污染外部作用域。相比于传统的`enum`,这降低了名称冲突的可能性。
enum class Color { Red };
enum class Fruit { Banana, Apple, Orange, Red }; // 这里的Red不会与Color::Red冲突
定义一个强类型枚举,并访问其枚举项:
enum class Direction { North, South, East, West };
Direction dir = Direction::North;
如果需要,可以显式地将强类型枚举转换为其底层类型,或从其底层类型转换回来:
enum class Color : char { Red = 'r', Green = 'g', Blue = 'b' };
Color c = Color::Red;
char ch = static_cast(c); // ch == 'r'
Color c2 = static_cast(ch); // c2 == Color::Red
强类型枚举`enum class`提供了更好的类型安全、作用域控制和底层类型指定,相比传统的`enum`,它降低了逻辑错误和名称冲突的风险,是现代C++中推荐使用的枚举定义方式。
C++11引入了对多线程编程的原生支持,提供了一组线程管理、同步、条件变量、原子操作等相关的类和函数,它们都位于`
C++11中,可以通过`std::thread`来创建和管理线程。你可以将一个可调用对象(函数、Lambda表达式、可调用类的对象等)传给`std::thread`的构造函数来创建一个线程。3.2 创建线程
#include
#include
void foo() {
std::cout << "Hello from foo\n";
}
int main() {
std::thread t(foo); // 创建一个新线程并执行foo函数
t.join(); // 主线程等待t线程完成
}
void print(int i, const std::string& s) {
std::cout << i << " " << s << '\n';
}
int main() {
std::thread t(print, 1, "Hello");
t.join();
}
在多线程环境中,多个线程访问共享数据时,需要使用锁来保护数据的一致性。C++11提供了`std::mutex`来实现互斥。
#include
#include
#include
std::mutex mtx;
void print_block(int n, char c) {
std::unique_lock lock(mtx); // 锁住mtx,保护std::cout
for(int i = 0; i < n; ++i) {
std::cout << c;
}
std::cout << '\n';
}
int main() {
std::thread t1(print_block, 50, '*');
std::thread t2(print_block, 50, '$');
t1.join();
t2.join();
}
`std::condition_variable`用于线程之间的同步。通常和`std::mutex`一起使用,允许一个或多个线程等待某个条件发生。
#include
#include
#include
#include
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void print() {
std::unique_lock lock(mtx);
while(!ready) {
cv.wait(lock);
}
std::cout << "printed after being notified\n";
}
int main() {
std::thread t(print);
{
std::lock_guard lock(mtx);
ready = true;
}
cv.notify_one();
t.join();
}
`
#include
#include
#include
std::atomic counter(0);
void increase() {
for(int i = 0; i < 1000; ++i) {
++counter;
}
}
int main() {
std::thread t1(increase);
std::thread t2(increase);
t1.join();
t2.join();
std::cout << counter.load() << '\n'; // 输出2000
}
C++11中的多线程支持使得C++程序员可以更容易地编写多线程程序,更加方便地进行线程管理、数据同步和线程之间的通信。在使用多线程时,一定要注意正确地使用同步机制,以避免数据竞争和死锁。
用户定义字面量(User-Defined Literals,UDL)是C++11引入的一项特性,允许开发者为常量字面量定义自己的后缀,从而提供一种便捷的方式来构造复杂对象或执行特殊计算。
用户定义字面量的基本形式如下:
ReturnType operator "" _suffix(Parameters...);
- `ReturnType`:字面量运算符的返回类型。
- `_suffix`:自定义的字面量后缀,必须以下划线`_`开头。
- `Parameters`:参数列表,取决于字面量的类型。
对于整数字面量,可以定义一个接受无符号长整型的字面量运算符:
constexpr long long operator"" _kb(unsigned long long v) {
return v * 1024;
}
// 使用
auto size = 4_kb; // size 的值为 4096
对于浮点数字面量,可以定义一个接受长双精度浮点数的字面量运算符:
constexpr double operator"" _cm(long double v) {
return v * 10.0; // 将厘米转换为毫米
}
// 使用
auto length = 2.5_cm; // length 的值为 25.0
对于字符串字面量,可以定义一个接受字符常量指针和大小的字面量运算符:
std::string operator"" _s(const char* str, std::size_t size) {
return std::string(str, str + size);
}
// 使用
auto str = "hello"_s; // str 的类型为 std::string
可以定义一个运算符来处理原始字符串字面量:
std::string operator"" _r(const char* str) {
return std::string(str);
}
// 使用
auto raw_string = R"(Hello, "world")"_r; // raw_string 的值为:Hello, "world"
用户定义字面量提供了一种方便的语法来自定义字面量的解释和表示,使得代码更具表达性和可读性。这个特性在需要定义特殊计量单位、特殊数据类型或复杂对象时特别有用。在使用用户定义字面量时,需要注意合理选择后缀,以确保代码的清晰和易于理解。
变长模板(Variadic Templates)是C++11引入的一项功能,允许模板接受可变数量的参数。这对于定义通用库,如元组、函数对象包装器和信号插槽库等,非常有用。
变长模板的参数使用省略号(`...`)表示。变长模板可以是类模板,也可以是函数模板。
template
class ClassName;
template
ReturnType FunctionName(Args... args);
下面是一个使用变长模板的函数模板例子,该函数可以接受任意数量和类型的参数,并打印它们:
#include
void print() { std::cout << std::endl; }
template
void print(const T& arg, const Args&... args) {
std::cout << arg << ' ';
print(args...);
}
int main() {
print(1, 2.0, "three", 4);
}
在上面的代码中,`print`函数是一个递归模板函数,每次递归都会处理一个参数,然后调用`print`函数处理剩下的参数,直到参数列表为空,调用无参数的`print`函数打印换行符。
变长模板也可以用于定义类模板。下面是一个变长模板类`Tuple`的简化版示例:
template
class Tuple;
template <>
class Tuple<> {
// 空元组的特化
};
template
class Tuple : private Tuple {
public:
Tuple(const Head& head, const Tail&... tail)
: Tuple(tail...), head_(head) {}
private:
Head head_;
};
int main() {
Tuple t(1, 2.0, "three");
}
在这个例子中,`Tuple`类模板使用递归继承来包含所有的值。每一层的`Tuple`都包含一个值,并继承下一层的`Tuple`。最后一层是一个特化的空`Tuple`。
在C++17中,引入了折叠表达式(Fold Expressions)来更简便地处理变长模板参数。这种语法可以用来替代手动书写的递归模板函数。
template
auto sum(Args... args) {
return (... + args); // 折叠表达式
}
int main() {
auto result = sum(1, 2, 3, 4); // 计算 1 + 2 + 3 + 4
std::cout << result << std::endl;
}
变长模板为C++模板编程提供了极大的灵活性,允许开发者编写能处理任意数量和类型的参数的泛型代码。在C++17中,通过折叠表达式,操作变长模板参数变得更为简洁和直观。
委托构造函数允许一个类的构造函数在同一类中调用另一个构造函数,这样就能避免重复编写相同的初始化代码。
class ClassName {
public:
ClassName(arg1, arg2, ...) : initializer_list { /*...*/ } // 委托构造函数的调用处
ClassName(arg1, arg2, ...) : ClassName(args), other_initializers { /*...*/ }
};
考虑一个类`Box`,它有长、宽和高三个属性,我们可以这样实现委托构造函数:
在这个例子中,有三个构造函数:
- 第一个构造函数是主构造函数,它接受三个参数来初始化`length_`,`width_`和`height_`。
- 第二个构造函数是一个委托构造函数,它接受一个参数`side`,并委托给第一个构造函数来创建一个立方体`Box`。
- 第三个构造函数是另一个委托构造函数,它没有参数,将`1.0`作为`side`参数委托给第二个构造函数来创建一个单位立方体`Box`。
通过使用委托构造函数,可以实现构造函数之间的代码复用,这使得类的实现更加简洁、一致且易于维护。这个特性对于有多个构造函数且存在重复初始化代码的类来说特别有用。
在C++11中,属性(Attributes)被引入,允许开发者向编译器传达更多的信息,以便进行更为详细和深度的代码分析和优化。它们不改变代码的语义,但可以用来告诉编译器如何处理代码,或者为工具提供额外的分析信息。
属性的基本语法结构如下:
[[attribute]]
或者,如果属性有参数:
[[attribute(parameter)]]
`[[nodiscard]]`属性用于表示函数的返回值不应该被忽略。如果程序员忽略了被`[[nodiscard]]`修饰的函数的返回值,编译器将生成警告。
[[nodiscard]] int ComputeValue() {
return 42;
}
int main() {
ComputeValue(); // 警告:忽略了带有 'nodiscard' 属性的返回值
}
`[[noreturn]]`属性用于表示函数不会返回。通常用于那些一定会抛出异常或调用`std::exit`的函数。
[[noreturn]] void MyExitFunction(const std::string& message) {
std::cerr << message << '\n';
std::exit(EXIT_FAILURE);
}
`[[deprecated]]`属性用于标记不推荐使用的项,可以选择性地提供一个字符串来解释为什么该项被弃用以及应该使用什么替代品。
[[deprecated("Use NewFunction instead")]]
void OldFunction() {
// ...
}
int main() {
OldFunction(); // 警告:'OldFunction' 已被弃用: Use NewFunction instead
}
`[[maybe_unused]]`属性用于表示一个变量、参数、静态变量或类型可能不会被使用,从而消除由此产生的未使用警告。
int main() {
[[maybe_unused]] int i = ComputeValue();
}
`[[fallthrough]]`属性用于标记在`switch`语句中有意的`case`穿透。
void Process(int value) {
switch(value) {
case 1:
// ...
[[fallthrough]]; // 表明case穿透是有意的
case 2:
// ...
}
}
属性为编译器和其他工具提供了一种机制,以更加精细的方式分析代码,识别潜在问题,或者优化生成的代码。通过合理使用属性,开发者可以更加清晰地表达代码的意图,提高代码的可读性和可维护性,并减少编程错误。
继承构造函数允许派生类继承其基类的构造函数,从而避免了为派生类编写与基类构造函数几乎相同的构造函数的需要。
继承构造函数的基本语法如下:
class Derived : public Base {
public:
using Base::Base; // 继承 Base 类的构造函数
};
考虑一个基类 `Person` 和一个从 `Person` 派生出来的类 `Student`。通过使用继承构造函数,`Student` 类可以继承 `Person` 类的构造函数。
class Person {
public:
Person(const std::string& name) : name_(name) {}
private:
std::string name_;
};
class Student : public Person {
public:
using Person::Person; // 继承 Person 的构造函数
// Student 类无需再定义其他构造函数
};
在这个例子中,`Student` 类通过 `using Person::Person;` 语句继承了 `Person` 类的构造函数。这意味着您可以像使用 `Person` 的构造函数一样使用 `Student` 的构造函数。
Student s("John Doe");
如果派生类有其他需要初始化的成员变量,那么您需要为派生类定义一个构造函数。这个构造函数可以调用基类的构造函数来进行初始化。
如果派生类定义了一个与基类构造函数签名相同的构造函数,那么基类的构造函数将被隐藏,即使有 `using` 声明。
继承构造函数是 C++11 中引入的一个特性,它允许派生类无需额外代码即可使用基类的构造函数,从而简化了代码,并减少了因代码重复而可能引入的错误。如果派生类需要进行额外的初始化工作,或者需要避免某个基类构造函数被继承,开发者仍然需要为派生类定义新的构造函数。
C++11引入了无序容器,这些无序容器是基于哈希表实现的,主要包括以下四种:
- `std::unordered_set`:一个集合,用于存储唯一的元素。
- `std::unordered_multiset`:一个多重集合,允许存储多个相同的元素。
- `std::unordered_map`:一个映射,用于存储`key-value`对,其中键是唯一的。
- `std::unordered_multimap`:一个多重映射,允许存储多个具有相同键的`key-value`对。
无序容器与有序容器(如`std::set`,`std::map`等)相比,通常能提供更快的查找、插入和删除操作。但是,它们不保证元素的有序性。
#include
int main() {
std::unordered_set uset = {1, 2, 3, 4, 5};
uset.insert(6);
if(uset.find(3) != uset.end()) {
// 3 在 uset 中找到
}
}
#include
#include
int main() {
std::unordered_map umap;
umap[5] = "five";
umap[3] = "three";
umap[8] = "eight";
umap[4] = "four";
umap[1] = "one";
auto it = umap.find(3);
if(it != umap.end()) {
// 键为3的元素在umap中找到,其值为it->second
}
}
无序容器使用哈希表来存储元素,因此需要一个哈希函数来计算元素的哈希值,以及一个相等函数来判断两个元素是否相等。通常,您可以使用STL提供的默认哈希函数和相等函数,但如果需要,您也可以提供自定义的哈希函数和相等函数。
struct MyHash {
std::size_t operator()(const std::string& s) const {
// 自定义哈希函数的实现
}
};
struct MyEqual {
bool operator()(const std::string& lhs, const std::string& rhs) const {
// 自定义相等函数的实现
}
};
std::unordered_set uset;
虽然无序容器通常能提供优越的性能,但它们也有一些需要注意的地方:
- 由于无序容器是基于哈希表实现的,它们的性能严重依赖于哈希函数的质量。一个差的哈希函数可能导致许多哈希冲突,从而降低性能。
- 无序容器不保持元素的顺序,如果需要按照一定的顺序遍历元素,那么有序容器可能更适合。
C++11中引入的无序容器提供了一种在平均情况下时间复杂度为O(1)的数据结构,以支持快速的查找、插入和删除操作。选择无序容器还是有序容器,取决于具体的应用场景和性能需求。如果不需要元素的有序性,且能够提供一个好的哈希函数,那么无序容器通常是一个更好的选择。
C++11引入了移动语义(Move Semantics)和右值引用(Rvalue Reference),它们一起为C++带来了性能的显著提升,尤其是在涉及大对象和容器操作时。
在C++中,表达式可以是左值或右值。左值有持久的存储位置,而右值通常是临时的,例如字面量和临时对象。
右值引用使用两个ampersand `&&` 定义,并且可以绑定到右值上。右值引用主要用于实现移动语义和完美转发。
int a = 42;
int&& r = 42; // 正确,42是一个右值
int&& r2 = a; // 错误,a是一个左值
移动语义允许从临时对象中“移动”资源,而不是进行昂贵的深度复制。这避免了不必要的临时对象的创建和销毁,以及资源的分配和释放。
对于那些拥有堆上资源的对象,如动态数组,移动语义特别有用。
要使类支持移动语义,通常需要实现移动构造函数和移动赋值运算符。
class MyString {
char* data_;
size_t length_;
public:
// 移动构造函数
MyString(MyString&& other) noexcept
: data_(other.data_), length_(other.length_) {
other.data_ = nullptr;
other.length_ = 0;
}
// 移动赋值运算符
MyString& operator=(MyString&& other) noexcept {
if(this != &other) {
delete[] data_;
data_ = other.data_;
length_ = other.length_;
other.data_ = nullptr;
other.length_ = 0;
}
return *this;
}
// ... 其他成员函数 ...
};
`std::move`可以将左值转换为右值,从而触发移动构造函数或移动赋值运算符。
MyString str1("Hello");
MyString str2(std::move(str1)); // 调用移动构造函数
- 当返回局部对象时,可以利用返回值优化或移动语义避免额外的拷贝。
- 当函数参数为按值传递时,如果传入一个临时对象或使用`std::move`传入,可以触发移动语义。
- 在容器中插入或删除元素,或对容器进行重新分配时,移动语义可以减少不必要的拷贝。
移动语义和右值引用是C++11引入的重要特性,能显著提高C++程序的性能,特别是在处理大型对象和容器时。它们使得资源的转移变得更加高效,减少了不必要的对象拷贝和临时对象的创建。