C++11是 C++ 的第二个主要版本,也是自 C++98 以来最重要的更新。
1. 关键字:auto和decltype
在C++11中,引入了两个非常有用的关键字:auto
和decltype
,它们极大地增强了类型推断的能力。下面我详细讲解一下这两个关键字的使用和区别。
auto
auto
关键字用于自动推断变量的类型。编译器会根据初始化表达式来推断变量的类型,这可以使代码更加简洁和易于维护。
基本用法
auto x = 10; // x 被推断为 int
auto y = 3.14; // y 被推断为 double
auto z = "hello"; // z 被推断为 const char*
用在循环中
auto
在基于范围的for循环中非常有用,特别是当循环变量的类型较长或复杂时:
std::vector<int> vec = {1, 2, 3, 4, 5};
for (auto it = vec.begin(); it != vec.end(); ++it) {
std::cout << *it << " ";
}
推断函数返回类型
在C++14及以后的版本中,可以用auto
来推断函数的返回类型:
auto add(int a, int b) {
return a + b; // 返回类型被推断为 int
}
decltype
decltype
关键字用于查询表达式的类型。与auto
不同,decltype
不进行类型推断,而是直接获取表达式的类型。
基本用法
int a = 10;
decltype(a) b = 20; // b 的类型是 int
用在函数返回类型中
有时我们需要在模板中获取表达式的类型:
template <typename T, typename U>
auto add(T t, U u) -> decltype(t + u) {
return t + u; // 返回类型是 t + u 的类型
}
用在复杂表达式中
decltype
可以处理更复杂的表达式,从而获取精确的类型:
std::vector<int> vec = {1, 2, 3, 4, 5};
decltype(vec[0]) x = vec[0]; // x 的类型是 int&
区别与联系
auto
用于变量声明和函数返回类型推断,通过初始化表达式来确定变量的类型。decltype
用于获取表达式的类型,通常用于需要精确类型信息的地方。
2. 默认函数(defaulted functions)和删除函数(deleted functions)
在C++11中,引入了默认函数(defaulted functions)和删除函数(deleted functions)的概念,这使得开发者可以更精细地控制类的特殊成员函数(如构造函数、析构函数、复制构造函数和赋值操作符)的生成和行为。这两个特性极大地增强了代码的可读性和安全性。
Defaulted Functions
默认函数允许显式地指定编译器生成默认的实现。这对于特殊成员函数(如默认构造函数、复制构造函数、移动构造函数等)特别有用。
用法
使用= default
来声明默认函数。
class MyClass {
public:
MyClass() = default; // 默认构造函数
MyClass(const MyClass&) = default; // 默认复制构造函数
MyClass& operator=(const MyClass&) = default; // 默认复制赋值操作符
~MyClass() = default; // 默认析构函数
};
何时使用
- 当你希望类具有某些默认行为,但又想显式地表明这是编译器生成的。
- 可以提升代码可读性和维护性。
- 当你需要默认实现而没有显式提供任何特殊行为时。
示例
class Base {
public:
Base() = default;
virtual ~Base() = default;
};
class Derived : public Base {
public:
Derived() = default;
~Derived() override = default;
};
Deleted Functions
删除函数用于显式禁止某些函数的调用。使用= delete
来声明删除函数。
用法
class MyClass {
public:
MyClass() = default;
MyClass(const MyClass&) = delete; // 禁止复制构造函数
MyClass& operator=(const MyClass&) = delete; // 禁止复制赋值操作符
};
何时使用
- 禁止不需要的函数调用,防止错误使用。
- 明确表明某些操作是不可行的,从而增强代码的安全性和意图表达。
示例
class NonCopyable {
public:
NonCopyable() = default;
NonCopyable(const NonCopyable&) = delete;
NonCopyable& operator=(const NonCopyable&) = delete;
};
void func(NonCopyable obj) {
// 这里会产生编译错误,因为复制构造函数被删除了
}
int main() {
NonCopyable obj1;
NonCopyable obj2 = obj1; // 错误,复制构造函数被删除
}
总结
- 默认函数(defaulted functions):通过
= default
显式声明,指示编译器生成默认实现。 - 删除函数(deleted functions):通过
= delete
显式声明,禁止特定函数的调用。
3. 关键字:final
和override
C++11引入了两个新的关键字:final
和override
,这两个关键字用于更好地管理类继承和虚函数的行为,从而使代码更加安全和易于维护。
override
override
关键字用于显式表明一个虚函数重写了基类中的虚函数。这可以帮助编译器检查函数签名是否匹配基类中的虚函数,从而避免一些常见的错误。
基本用法
class Base {
public:
virtual void func() {}
};
class Derived : public Base {
public:
void func() override { // 重写基类的 func 函数
// 实现代码
}
};
何时使用
- 当你重写基类中的虚函数时,应始终使用
override
。 - 可以帮助捕捉函数签名不匹配的问题。
示例
class Base {
public:
virtual void func(int) {}
};
class Derived : public Base {
public:
void func(int) override { // 正确重写
// 实现代码
}
// void func(double) override {} // 错误:没有重写基类的任何函数
};
在上述示例中,如果派生类中的func
函数签名与基类不匹配,编译器会产生错误,防止潜在的逻辑错误。
final
final
关键字用于禁止类的进一步继承或虚函数的进一步重写。
禁止类继承
class Base final {
// 实现代码
};
// class Derived : public Base { // 错误:无法继承 final 类
// };
禁止虚函数重写
class Base {
public:
virtual void func() final {
// 实现代码
}
};
class Derived : public Base {
public:
// void func() override {} // 错误:无法重写 final 虚函数
};
何时使用
- 当你不希望某个类被进一步继承时,可以将其声明为
final
。 - 当你不希望某个虚函数在派生类中被重写时,可以将其声明为
final
。
示例
class Base {
public:
virtual void show() final {
// 基类的最终实现
}
};
class Derived : public Base {
public:
// void show() override {} // 错误:无法重写 final 虚函数
};
另一个示例展示如何使用final
禁止类的继承:
class NonInheritable final {
// 实现代码
};
// class AttemptInherit : public NonInheritable { // 错误:无法继承 final 类
// };
总结
override
:用于显式表明一个虚函数重写了基类中的虚函数,有助于捕捉函数签名不匹配的问题。final
:用于禁止类的进一步继承或虚函数的进一步重写,确保特定行为不被改变。
这些关键字增强了C++代码的可读性、安全性和维护性。
4. 尾置返回类型(trailing return type)
在C++11中,引入了尾置返回类型(trailing return type),这为函数的返回类型提供了一种新的声明方式,特别是当返回类型依赖于函数参数时,这种方式显得非常有用。
尾置返回类型简介
尾置返回类型将返回类型放置在函数签名的尾部,而不是函数名之前。这种语法使用箭头符号->
来连接函数参数列表和返回类型。
基本语法
auto functionName(parameters) -> returnType {
// 函数体
}
示例
auto add(int a, int b) -> int {
return a + b;
}
使用场景
依赖于模板参数的返回类型
在模板函数中,有时返回类型依赖于模板参数。使用尾置返回类型可以简化这种情况:
template <typename T, typename U>
auto multiply(T t, U u) -> decltype(t * u) {
return t * u;
}
在这个例子中,返回类型是decltype(t * u)
,其类型依赖于模板参数T
和U
。
与decltype
结合使用
当函数返回类型比较复杂或依赖于函数参数时,尾置返回类型与decltype
结合使用非常方便:
#include <vector>
template <typename Container>
auto getFirstElement(Container& c) -> decltype(c[0]) {
return c[0];
}
int main() {
std::vector<int> vec = {1, 2, 3};
int first = getFirstElement(vec);
return 0;
}
在这个例子中,getFirstElement
函数返回类型是decltype(c[0])
,即容器c
中第一个元素的类型。
尾置返回类型的优点
- 提升可读性:当返回类型较复杂时,尾置返回类型可以使函数签名更清晰。
- 与
auto
结合:在函数模板中,使用auto
和尾置返回类型可以方便地处理依赖于模板参数的返回类型。 - 处理复杂类型:对于返回类型依赖于参数的情况,使用尾置返回类型可以更直观地表达。
总结
- 尾置返回类型:通过将返回类型放在参数列表之后,可以更清晰地表达复杂返回类型,特别是在返回类型依赖于函数参数时。
- 与
decltype
结合:在模板和依赖于参数的函数中,尾置返回类型与decltype
结合使用非常方便。 - 优点:提高代码可读性,方便处理复杂返回类型。
尾置返回类型为C++11引入的一个强大特性,帮助开发者更清晰地表达函数的返回类型,特别是在模板编程和泛型编程中显得尤为有用。
5. 右值引用(rvalue references)
C++11引入了右值引用(rvalue references),这是C++语言中的一项重大改进,极大地提升了性能和资源管理的效率。右值引用及其相关特性(如移动语义和完美转发)为开发者提供了更强大的工具来优化代码。
左值和右值
在讨论右值引用之前,先了解左值和右值的概念:
- 左值(lvalue):表示一个对象在内存中的位置,可以被取地址。例如变量、数组元素、对象的成员等。
- 右值(rvalue):表示一个临时对象或字面值,不在内存中有固定地址。例如字面值、临时对象的结果等。
右值引用简介
右值引用使用双&&符号声明。它允许开发者捕获和操作右值,从而实现高效的资源管理。
基本语法
int&& rvalue_ref = 10; // 10 是右值
移动语义
移动语义是右值引用最重要的应用之一。传统的复制操作需要深拷贝数据,而移动操作可以“偷取”资源,从而避免不必要的复制。
移动构造函数
移动构造函数是接受右值引用参数的构造函数,用于转移资源所有权。
class MyClass {
public:
MyClass(int size) : data(new int[size]), size(size) {}
// 移动构造函数
MyClass(MyClass&& other) noexcept : data(other.data), size(other.size) {
other.data = nullptr;
other.size = 0;
}
~MyClass() {
delete[] data;
}
private:
int* data;
int size;
};
移动赋值运算符
移动赋值运算符用于转移资源所有权,并释放当前对象的资源。
class MyClass {
public:
MyClass& operator=(MyClass&& other) noexcept {
if (this != &other) {
delete[] data; // 释放当前对象的资源
data = other.data;
size = other.size;
other.data = nullptr;
other.size = 0;
}
return *this;
}
};
标准库中的应用
C++标准库也广泛使用右值引用和移动语义,例如std::vector
和std::string
的实现。
std::move
std::move
用于将左值转换为右值引用,从而触发移动语义。
#include <iostream>
#include <utility> // std::move
#include <vector>
int main() {
std::vector<int> vec1 = {1, 2, 3};
std::vector<int> vec2 = std::move(vec1); // vec1 的内容被移动到 vec2 中
std::cout << "vec1 size: " << vec1.size() << std::endl; // vec1 现在为空
std::cout << "vec2 size: " << vec2.size() << std::endl; // vec2 现在包含原 vec1 的内容
return 0;
}
完美转发
完美转发允许函数模板精确传递其参数,以保持参数的左值或右值属性。实现完美转发的关键是使用右值引用和std::forward
。
示例
#include <iostream>
#include <utility>
void process(int& x) {
std::cout << "Lvalue reference: " << x << std::endl;
}
void process(int&& x) {
std::cout << "Rvalue reference: " << x << std::endl;
}
template <typename T>
void forwarder(T&& arg) {
process(std::forward<T>(arg));
}
int main() {
int a = 10;
forwarder(a); // 调用 process(int&)
forwarder(20); // 调用 process(int&&)
return 0;
}
在这个示例中,forwarder
函数根据参数的左值或右值属性,使用std::forward
将其传递给合适的process
重载版本。
总结
- 右值引用(rvalue references):使用双&&符号,可以捕获和操作右值。
- 移动语义(move semantics):通过移动构造函数和移动赋值运算符实现资源高效转移。
- 标准库应用:广泛应用于标准库中,如
std::vector
和std::string
。 std::move
:将左值转换为右值引用以触发移动语义。- 完美转发(perfect forwarding):使用右值引用和
std::forward
实现参数的精确传递。
右值引用和移动语义是C++11的一项重要特性,为开发者提供了强大的工具来优化代码性能和资源管理。
6. 移动构造函数和移动赋值运算符
C++11引入了移动构造函数和移动赋值运算符,它们允许对象的资源所有权被“移动”而不是复制,从而提高了程序的性能。下面详细讲解这些特性的定义、用法以及限制。
定义
移动构造函数
移动构造函数是接受右值引用(rvalue reference)参数的构造函数。其主要目的是从另一个对象“偷取”资源,而不是复制资源。
class MyClass {
public:
// 移动构造函数
MyClass(MyClass&& other) noexcept;
};
移动赋值运算符
移动赋值运算符是接受右值引用参数的赋值运算符。它的作用是将资源从一个对象转移到另一个对象,并释放当前对象的资源。
class MyClass {
public:
// 移动赋值运算符
MyClass& operator=(MyClass&& other) noexcept;
};
用法
移动构造函数的实现
#include <iostream>
class MyClass {
public:
MyClass(int size) : data(new int[size]), size(size) {
std::cout << "Constructor\n";
}
// 移动构造函数
MyClass(MyClass&& other) noexcept : data(other.data), size(other.size) {
other.data = nullptr;
other.size = 0;
std::cout << "Move Constructor\n";
}
~MyClass() {
delete[] data;
}
private:
int* data;
int size;
};
int main() {
MyClass obj1(10);
MyClass obj2(std::move(obj1)); // 调用移动构造函数
return 0;
}
移动赋值运算符的实现
#include <iostream>
class MyClass {
public:
MyClass(int size) : data(new int[size]), size(size) {
std::cout << "Constructor\n";
}
// 移动构造函数
MyClass(MyClass&& other) noexcept : data(other.data), size(other.size) {
other.data = nullptr;
other.size = 0;
std::cout << "Move Constructor\n";
}
// 移动赋值运算符
MyClass& operator=(MyClass&& other) noexcept {
if (this != &other) {
delete[] data; // 释放当前对象的资源
data = other.data;
size = other.size;
other.data = nullptr;
other.size = 0;
std::cout << "Move Assignment Operator\n";
}
return *this;
}
~MyClass() {
delete[] data;
}
private:
int* data;
int size;
};
int main() {
MyClass obj1(10);
MyClass obj2(20);
obj2 = std::move(obj1); // 调用移动赋值运算符
return 0;
}
使用noexcept
移动构造函数和移动赋值运算符通常被声明为noexcept
,以便编译器在需要保证不抛出异常的情况下优化这些操作。标准库中的许多容器在执行某些操作(如重新分配内存)时,依赖于移动操作不会抛出异常。
MyClass(MyClass&& other) noexcept;
MyClass& operator=(MyClass&& other) noexcept;
常见问题
自身赋值检查
在实现移动赋值运算符时,需要检查是否是自身赋值。
if (this != &other) {
// 执行移动操作
}
移动后的对象状态
被移动后的对象必须保持一个有效的状态,尽管其内部资源可能已经被转移。这通常意味着将被移动对象的资源指针设为nullptr
或其他适当的默认值。
默认操作符和构造函数
如果没有定义自己的移动构造函数和移动赋值运算符,编译器会自动生成默认的移动操作。但是,如果类定义了任何自定义的析构函数、复制构造函数或复制赋值运算符,编译器将不会自动生成移动构造函数和移动赋值运算符。
总结
- 移动构造函数:通过右值引用参数将资源从一个对象转移到另一个对象,避免了不必要的深拷贝。
- 移动赋值运算符:将资源从一个对象转移到另一个对象,并释放当前对象的资源,避免了不必要的深拷贝。
noexcept
:标记移动操作不抛出异常,允许编译器进行更好的优化。- 自我赋值检查:确保移动赋值运算符中避免自身赋值。
- 移动后的对象状态:保证被移动后的对象保持有效状态。
7. 作用域枚举(scoped enums)
C++11引入了作用域枚举(scoped enums),也称为强类型枚举(strongly typed enums),通过关键字enum class
或enum struct
来声明。这种新枚举类型解决了传统枚举类型的一些缺点,使得代码更加类型安全和清晰。
定义
传统枚举类型存在命名冲突和隐式转换的问题,作用域枚举通过将枚举值限定在枚举类型的作用域内,并且不允许隐式转换为整数类型来解决这些问题。
基本语法
enum class EnumName {
Enumerator1,
Enumerator2,
Enumerator3
};
或
enum struct EnumName {
Enumerator1,
Enumerator2,
Enumerator3
};
示例
#include <iostream>
enum class Color {
Red,
Green,
Blue
};
enum class Fruit {
Apple,
Orange,
Banana
};
int main() {
Color color = Color::Red;
Fruit fruit = Fruit::Apple;
// 使用作用域枚举,必须限定作用域
if (color == Color::Red) {
std::cout << "Color is Red" << std::endl;
}
// 使用作用域枚举,避免了命名冲突
// if (color == fruit) { // 错误:不同枚举类型不能比较
// std::cout << "Color and Fruit are the same" << std::endl;
// }
return 0;
}
在这个示例中,Color
和Fruit
是两个不同的枚举类型,它们的枚举值在各自的作用域内,避免了命名冲突。
用法
类型安全
作用域枚举不允许隐式转换为整数类型,这样可以避免错误的比较或赋值操作。
enum class Status {
OK,
Error
};
void process(Status status) {
if (status == Status::OK) {
// 处理成功
}
}
int main() {
// process(0); // 错误:不能将整数隐式转换为 Status
process(Status::OK); // 正确
return 0;
}
指定枚举类型的底层类型
可以显式指定枚举的底层类型,如int
、char
等,这样可以控制枚举的大小和范围。
enum class Color : char {
Red,
Green,
Blue
};
enum class Permissions : unsigned int {
Read = 1,
Write = 2,
Execute = 4
};
使用枚举值
访问作用域枚举的枚举值时,需要使用枚举类型的作用域。
enum class Direction {
North,
South,
East,
West
};
Direction dir = Direction::North;
转换为整数类型
虽然作用域枚举不允许隐式转换为整数类型,但可以显式转换。
int main() {
Color color = Color::Green;
int colorValue = static_cast<int>(color); // 显式转换
std::cout << "Color value: " << colorValue << std::endl;
return 0;
}
常见问题
不支持隐式转换
作用域枚举不能隐式转换为整数类型,必须显式转换。这虽然提高了类型安全性,但在某些场景下可能需要额外的转换操作。
enum class Color {
Red,
Green,
Blue
};
int main() {
Color color = Color::Red;
// int value = color; // 错误:不能隐式转换
int value = static_cast<int>(color); // 正确:显式转换
return 0;
}
作用域限定
使用作用域枚举时,必须使用作用域限定符来访问枚举值,这在某些情况下可能显得冗长。
enum class Color {
Red,
Green,
Blue
};
int main() {
Color color = Color::Red; // 必须使用 Color::
return 0;
}
总结
- 定义:通过
enum class
或enum struct
定义作用域枚举,解决了传统枚举的命名冲突和隐式转换问题。 - 用法:
- 类型安全:作用域枚举不允许隐式转换为整数类型,避免错误的比较或赋值。
- 指定底层类型:可以显式指定枚举的底层类型,如
int
、char
等。 - 使用枚举值时需要使用作用域限定符。
- 显式转换:可以显式转换为整数类型。
- 限制:
- 不支持隐式转换为整数类型。
- 使用时必须使用作用域限定符,可能显得冗长。
8. constexpr
关键字
C++11引入了constexpr
关键字,用于定义在编译时可求值的常量表达式函数和变量。这种特性使得程序在编译阶段即可进行更多的计算,从而提高运行时的性能。为了支持constexpr
,C++11还引入了字面值类型(literal types)的概念,这些类型可以用于常量表达式中。
constexpr
定义
constexpr
变量
constexpr
变量是常量,在编译时就能确定其值。
constexpr int max_value = 100;
constexpr
函数
constexpr
函数是在编译时可求值的函数。函数体内的所有操作必须是编译时可求值的。
constexpr int factorial(int n) {
return n <= 1 ? 1 : (n * factorial(n - 1));
}
用法
constexpr
变量
constexpr
变量可以用于需要常量表达式的地方,例如数组大小、模板参数等。
constexpr int array_size = 10;
int arr[array_size];
constexpr
函数
constexpr
函数可以在编译时进行计算,从而提高程序的性能。
constexpr int square(int x) {
return x * x;
}
int main() {
constexpr int result = square(5); // 编译时求值
int arr[square(3)]; // 编译时求值,用于数组大小
return 0;
}
字面值类型(Literal Types)
字面值类型是可以用于常量表达式的类型。C++11规定以下类型是字面值类型:
- 基本数据类型,如
int
、char
、float
等。 - 指针类型,但指向非字面值类型的指针除外。
- 枚举类型。
- 拥有constexpr构造函数的类类型,且所有数据成员都是字面值类型。
constexpr
构造函数
类的构造函数可以声明为constexpr
,使其能够在编译时被调用。
struct Point {
int x, y;
constexpr Point(int x, int y) : x(x), y(y) {}
};
constexpr Point origin() {
return Point(0, 0);
}
int main() {
constexpr Point p = origin(); // 编译时求值
return 0;
}
常见问题
constexpr
函数的限制
constexpr
函数的函数体必须是一个单一的return语句(C++14之前)。constexpr
函数只能调用其他constexpr
函数或是编译时可求值的函数。constexpr
函数的参数和返回值必须是字面值类型。
constexpr int add(int a, int b) {
return a + b;
}
// 错误示例:不能包含非字面值类型
/*
constexpr int* get_pointer(int* ptr) {
return ptr;
}
*/
constexpr
变量的限制
constexpr
变量的初始化表达式必须是一个常量表达式。
constexpr int value = 10; // 正确
// constexpr int value2 = get_value(); // 错误,get_value() 不是常量表达式
类的字面值类型限制
- 数据成员必须是字面值类型。
- 构造函数必须是
constexpr
。 - 类中的所有成员函数如果在常量表达式中被调用,必须是
constexpr
。
class MyClass {
public:
int x;
constexpr MyClass(int val) : x(val) {}
constexpr int get_value() const { return x; }
};
// 错误示例:包含非字面值类型的数据成员
/*
class MyNonLiteralClass {
std::string str; // std::string 不是字面值类型
public:
constexpr MyNonLiteralClass(const char* s) : str(s) {}
};
*/
总结
constexpr
变量:在编译时可求值的常量变量,用于需要常量表达式的地方。constexpr
函数:在编译时可求值的函数,提升程序性能。- 字面值类型:可以用于常量表达式的类型,包括基本数据类型、指针类型、枚举类型和拥有
constexpr
构造函数的类类型。 - 限制:
constexpr
函数和变量的表达式必须在编译时可求值。constexpr
函数的参数和返回值必须是字面值类型。- 类的字面值类型的所有数据成员必须是字面值类型,并且构造函数必须是
constexpr
。
9. 列表初始化(list initialization)
C++11引入了列表初始化(list initialization),也称为统一初始化(uniform initialization),它提供了一种统一且直观的语法来初始化对象、数组和容器。这种初始化方式使用大括号{}
,可以减少类型转换和意外的隐式转换错误。
定义
列表初始化使用大括号{}
来初始化对象。这种方式可以用于内置类型、类类型、数组、容器等。
int a{5}; // 直接初始化(direct initialization)
int b = {6}; // 拷贝初始化(copy initialization)
std::vector<int> v{1, 2, 3}; // 初始化STL容器
用法
基本类型
列表初始化可以用于基本数据类型,如int
、double
等。
int x{10}; // 直接初始化
int y = {20}; // 拷贝初始化
double z{3.14}; // 直接初始化
char ch{'A'}; // 直接初始化
数组
可以使用列表初始化来初始化数组。
int arr1[3] = {1, 2, 3}; // 拷贝初始化
int arr2[]{4, 5, 6}; // 直接初始化
int arr3[5]{}; // 值初始化,所有元素为0
类类型
对于类类型,列表初始化会尝试匹配一个接受相应参数的构造函数。
#include <iostream>
#include <vector>
class MyClass {
public:
MyClass(int a, int b) : x(a), y(b) {}
int x, y;
};
int main() {
MyClass obj1{1, 2}; // 直接初始化
std::vector<int> vec{1, 2, 3}; // 直接初始化STL容器
std::cout << "obj1.x = " << obj1.x << ", obj1.y = " << obj1.y << std::endl;
return 0;
}
标准容器
列表初始化也可以用于标准容器,如std::vector
、std::list
等。
#include <vector>
#include <list>
int main() {
std::vector<int> vec{1, 2, 3, 4}; // 初始化std::vector
std::list<std::string> lst{"one", "two", "three"}; // 初始化std::list
return 0;
}
结构体和联合体
列表初始化也适用于结构体和联合体。
struct Point {
int x, y;
};
union Data {
int i;
float f;
};
int main() {
Point p{1, 2}; // 结构体的列表初始化
Data d{.f = 3.14f}; // 联合体的列表初始化
return 0;
}
限制
窄化转换
列表初始化禁止会导致数据丢失的窄化转换。例如,将浮点数转换为整数。
int x{3.14}; // 错误:窄化转换
如果初始化列表中包含任何会导致窄化转换的元素,编译器将报错。
花括号初始化与构造函数冲突
当类同时定义了一个接受std::initializer_list
的构造函数和其他构造函数时,列表初始化会优先调用std::initializer_list
构造函数。
#include <initializer_list>
#include <iostream>
class MyClass {
public:
MyClass(int a, int b) {
std::cout << "Regular constructor\n";
}
MyClass(std::initializer_list<int> il) {
std::cout << "Initializer_list constructor\n";
}
};
int main() {
MyClass obj1{1, 2}; // 调用initializer_list构造函数
return 0;
}
在这个示例中,obj1
初始化时将调用initializer_list
构造函数,而不是接受两个整数参数的构造函数。
动态数组
列表初始化不能直接用于动态数组的初始化。
int* arr = new int[3]{1, 2, 3}; // 错误:C++11不支持这种语法
不过可以通过其他方式实现相似效果:
#include <memory>
int main() {
std::unique_ptr<int[]> arr(new int[3]{1, 2, 3}); // 使用unique_ptr包装动态数组
return 0;
}
总结
- 定义:列表初始化使用大括号
{}
进行对象初始化,提供了统一的语法。 - 用法:
- 基本类型:可以直接初始化基本数据类型。
- 数组:可以初始化静态数组。
- 类类型:可以初始化类对象,优先调用接受
std::initializer_list
的构造函数。 - 标准容器:可以初始化标准库容器,如
std::vector
、std::list
等。 - 结构体和联合体:可以初始化结构体和联合体。
- 限制:
- 窄化转换:禁止会导致数据丢失的窄化转换。
- 构造函数冲突:优先调用接受
std::initializer_list
的构造函数。 - 动态数组:不能直接用于动态数组的初始化。
10. 委托构造函数(delegating constructors)和继承构造函数(inheriting constructors)
C++11引入了委托构造函数(delegating constructors)和继承构造函数(inheriting constructors)两项新特性,简化了构造函数的编写和继承过程。下面详细讲解这些特性的定义、用法以及限制。
委托构造函数(Delegating Constructors)
定义
委托构造函数允许一个构造函数调用同一类中的另一个构造函数,从而避免重复代码。它们通过成员初始化列表进行调用。
示例
#include <iostream>
class MyClass {
public:
MyClass() : MyClass(0) { // 委托给MyClass(int)
std::cout << "Default constructor\n";
}
MyClass(int value) : x(value) {
std::cout << "Parameterized constructor\n";
}
private:
int x;
};
int main() {
MyClass obj1; // 调用默认构造函数
MyClass obj2(42); // 调用参数化构造函数
return 0;
}
在这个示例中,默认构造函数委托给参数化构造函数MyClass(int value)
,避免了重复的初始化代码。
用法
委托构造函数通常用于避免代码重复,特别是在多个构造函数共享相似的初始化逻辑时。
class Rectangle {
public:
Rectangle() : Rectangle(0, 0) {}
Rectangle(int width, int height) : width(width), height(height) {}
private:
int width, height;
};
常见问题
- 一个构造函数只能委托给同一类中的另一个构造函数。
- 委托构造函数的初始化列表中不能包含除委托调用之外的任何成员初始化。
class MyClass {
int x, y;
public:
// 错误:不能同时初始化成员和委托
// MyClass() : x(0), MyClass(0) {}
// 正确
MyClass() : MyClass(0) {}
MyClass(int value) : x(value), y(value) {}
};
继承构造函数(Inherited Constructors)
定义
继承构造函数允许派生类自动继承基类的构造函数。这减少了在派生类中显式定义构造函数的需求,简化了代码。
示例
#include <iostream>
class Base {
public:
Base(int x) {
std::cout << "Base constructor\n";
}
};
class Derived : public Base {
public:
using Base::Base; // 继承Base类的构造函数
};
int main() {
Derived d(42); // 调用Base(int)构造函数
return 0;
}
在这个示例中,Derived
类继承了Base
类的构造函数,使得我们可以直接用基类的构造函数来初始化派生类对象。
用法
继承构造函数用于简化派生类的构造函数定义,特别是当派生类不需要增加额外的初始化逻辑时。
class A {
public:
A(int) {}
A(double) {}
};
class B : public A {
public:
using A::A; // 继承所有的A类构造函数
};
常见问题
- 继承构造函数不能与派生类中显式声明的构造函数冲突。
- 如果基类构造函数是私有的或受保护的,派生类不能直接继承这些构造函数。
- 如果基类有构造函数默认参数,派生类的构造函数继承时会将这些默认参数一起继承。
class Base {
public:
Base(int x) {}
Base(double y, int z = 0) {}
};
class Derived : public Base {
public:
using Base::Base;
// 不能有与继承构造函数冲突的显式构造函数
// Derived(int x) : Base(x) {} // 错误:与继承的Base(int)冲突
};
总结
-
委托构造函数:一个构造函数调用同一类中的另一个构造函数,避免重复代码。使用成员初始化列表进行调用。
- 用法:用于简化多个构造函数共享相似初始化逻辑的类。
- 限制:一个构造函数只能委托给同一类中的另一个构造函数,且不能同时初始化成员和委托。
-
继承构造函数:派生类自动继承基类的构造函数,减少派生类显式定义构造函数的需求。
- 用法:用于简化派生类的构造函数定义,特别是当派生类不需要增加额外初始化逻辑时。
- 限制:不能与派生类中显式声明的构造函数冲突,不能继承私有或受保护的基类构造函数,继承时基类构造函数的默认参数也会被继承。
11. brace-or-equal initializers(大括号或等号初始化器)
C++11引入了brace-or-equal initializers(大括号或等号初始化器),这为类的数据成员提供了一种新的初始化方式。通过这种方式,可以在类的定义中直接为数据成员提供默认值。这样做简化了构造函数的编写,并且可以避免未初始化的数据成员问题。
定义
Brace-or-equal initializers指的是在类定义中,使用大括号{}
或等号=
为数据成员提供初始值。
class MyClass {
public:
int a = 10; // 等号初始化器
int b{20}; // 大括号初始化器
std::string str = "Hello"; // 等号初始化器
};
用法
基本用法
在类定义中直接为数据成员提供初始值:
class Example {
public:
int x = 5; // 等号初始化器
int y{10}; // 大括号初始化器
double z = 3.14; // 等号初始化器
std::string s{"text"}; // 大括号初始化器
};
int main() {
Example e;
std::cout << e.x << ", " << e.y << ", " << e.z << ", " << e.s << std::endl; // 输出:5, 10, 3.14, text
return 0;
}
与构造函数结合使用
当一个类有多个构造函数时,brace-or-equal initializers可以确保数据成员在所有构造函数中都得到初始化。
class Example {
public:
int x = 5; // 等号初始化器
int y{10}; // 大括号初始化器
Example() = default; // 使用默认构造函数
Example(int x) : x(x) {} // 自定义构造函数,y使用默认初始化器
};
int main() {
Example e1;
Example e2(20);
std::cout << e1.x << ", " << e1.y << std::endl; // 输出:5, 10
std::cout << e2.x << ", " << e2.y << std::endl; // 输出:20, 10
return 0;
}
与初始化列表结合使用
如果构造函数的初始化列表中明确初始化了数据成员,则brace-or-equal initializers将被忽略。
class Example {
public:
int x = 5; // 等号初始化器
int y{10}; // 大括号初始化器
Example(int a, int b) : x(a), y(b) {} // 初始化列表覆盖默认值
};
int main() {
Example e(15, 25);
std::cout << e.x << ", " << e.y << std::endl; // 输出:15, 25
return 0;
}
限制
只在类定义中有效
brace-or-equal initializers只能在类的定义中使用,而不能在函数体或其他作用域中使用。
class Example {
public:
int x = 5; // 正确:类定义中的brace-or-equal initializer
void func() {
// int y = 10; // 错误:不能在函数体中使用brace-or-equal initializer
}
};
不能用于动态分配的成员
brace-or-equal initializers不能用于动态分配的成员,如指针。
class Example {
public:
int* ptr = new int(5); // 允许,但要小心内存泄漏
~Example() {
delete ptr; // 确保释放动态分配的内存
}
};
继承和覆盖
在继承关系中,基类的brace-or-equal initializers不会自动应用于派生类,除非派生类没有显式初始化该成员。
class Base {
public:
int x = 5; // 等号初始化器
};
class Derived : public Base {
public:
int y = 10; // 等号初始化器
Derived() : x(20) {} // 错误:不能在派生类初始化列表中初始化基类成员
};
总结
- 定义:brace-or-equal initializers是指在类定义中使用大括号
{}
或等号=
为数据成员提供默认值。 - 用法:
- 为类的数据成员提供默认初始值,简化构造函数的编写。
- 确保数据成员在所有构造函数中都得到初始化。
- 与构造函数初始化列表结合使用时,初始化列表中的初始化优先。
- 限制:
- 只能在类定义中使用,不能在函数体或其他作用域中使用。
- 不能用于动态分配的成员。
- 在继承关系中,基类的brace-or-equal initializers不会自动应用于派生类。
12. 关键字 nullptr
C++11 引入了新的关键字 nullptr
来表示空指针,以取代传统的 NULL
宏定义。使用 nullptr
能够避免许多与空指针相关的类型安全问题。
定义
nullptr
是一种特殊类型 std::nullptr_t
的常量,专门用于表示空指针。它可以隐式转换为任意指针类型和成员指针类型。
用法
基本用法
使用 nullptr
替代 NULL
或 0
来表示空指针:
int* ptr = nullptr; // 使用 nullptr 初始化指针
与函数重载结合使用
由于 nullptr
是一种特殊类型,它可以在函数重载中起到区分作用,从而避免不明确的调用。
void f(int) {
std::cout << "Function f(int) called\n";
}
void f(int*) {
std::cout << "Function f(int*) called\n";
}
int main() {
f(0); // 调用 f(int)
f(nullptr); // 调用 f(int*)
return 0;
}
与智能指针结合使用
nullptr
可以与智能指针(如 std::unique_ptr
和 std::shared_ptr
)结合使用,以确保初始化为空指针。
#include <memory>
std::unique_ptr<int> ptr1 = nullptr;
std::shared_ptr<int> ptr2 = nullptr;
限制
不支持隐式转换为整数类型
与 NULL
不同,nullptr
不能隐式转换为整数类型。这种特性增强了类型安全性,避免了许多潜在的错误。
void f(int) {
std::cout << "Function f(int) called\n";
}
int main() {
f(NULL); // 正确:NULL 可以隐式转换为 0
// f(nullptr); // 错误:nullptr 不能隐式转换为 int
return 0;
}
不能用于非指针类型的初始化
nullptr
只能用于指针类型和成员指针类型的初始化,不能用于其他类型的初始化。
int x = nullptr; // 错误:nullptr 不能用于非指针类型的初始化
优点
- 类型安全:
nullptr
是一种特定的类型std::nullptr_t
,避免了使用NULL
时的类型不明确问题。 - 重载解析:在函数重载时,
nullptr
能够明确区分指针重载和整数重载,避免不明确的调用。 - 代码可读性:使用
nullptr
使得代码更加清晰,明确指出了变量是一个指针,而不是整数。
总结
- 定义:
nullptr
是一种特殊类型std::nullptr_t
的常量,用于表示空指针。 - 用法:
- 代替
NULL
或0
表示空指针。 - 与函数重载结合使用,以区分指针重载和整数重载。
- 与智能指针结合使用,确保初始化为空指针。
- 代替
- 限制:
- 不能隐式转换为整数类型。
- 不能用于非指针类型的初始化。
- 优点:
- 提高类型安全性。
- 避免不明确的函数调用。
- 增强代码可读性。
nullptr
提供了一个更安全和明确的方式来表示空指针,使得C++代码更加健壮和易于维护。
13. 整型数据类型 long long
C++11 引入了新的整型数据类型 long long
,提供了一种比 long
更长的整数类型,以便能够表示更大范围的整数。
定义
long long
是一种标准的整数类型,保证至少有 64 位宽度。它可以表示的整数范围至少是 -2^63 到 2^63 - 1(在无符号的情况下范围是 0 到 2^64 - 1)。
用法
声明与初始化
可以像其他基本数据类型一样声明和初始化 long long
类型的变量。
long long bigNumber = 9223372036854775807LL; // 使用 LL 或 ll 后缀表示 long long 常量
unsigned long long veryBigNumber = 18446744073709551615ULL; // 使用 ULL 或 ull 表示无符号 long long 常量
使用长整数类型
long long
类型可以用于任何需要处理大整数的场景,例如高精度计算、大数运算等。
#include <iostream>
int main() {
long long a = 123456789012345LL;
long long b = 987654321098765LL;
long long c = a * b;
std::cout << "a = " << a << std::endl;
std::cout << "b = " << b << std::endl;
std::cout << "c = " << c << std::endl;
return 0;
}
限制
兼容性
虽然 long long
是 C++11 标准引入的类型,但在一些旧的编译器中可能不被支持。确保使用支持 C++11 标准的编译器。
性能
由于 long long
占用的内存比 int
和 long
多,因此在大量使用 long long
类型时,可能会影响程序的性能和内存占用。
与其他类型的比较
int
通常是 32 位,范围是 -2^31 到 2^31 - 1。long
在某些平台上也是 32 位,在其他平台上是 64 位。long long
至少是 64 位,确保了更大的整数范围。
类型转换
与其他整数类型一样,long long
可以与其他整数类型进行隐式和显式转换。
int main() {
int i = 12345;
long long ll = i; // 隐式转换
int j = static_cast<int>(ll); // 显式转换
return 0;
}
总结
- 定义:
long long
是 C++11 引入的标准整数类型,至少有 64 位宽度。 - 用法:
- 声明与初始化:使用
LL
或ll
后缀表示long long
常量。 - 大数运算:适用于需要处理大整数的场景。
- 声明与初始化:使用
- 限制:
- 兼容性:确保使用支持 C++11 的编译器。
- 性能:由于占用内存较大,可能影响程序的性能和内存占用。
- 类型比较:比
int
和long
有更大的范围,至少是 64 位。
14. char16_t
和 char32_t
两种新的字符类型
C++11 引入了 char16_t
和 char32_t
两种新的字符类型,以支持更广泛的字符编码和国际化。这些类型与 Unicode 标准兼容,分别用于表示 UTF-16 和 UTF-32 编码的字符。
定义
char16_t
:用于表示 16 位宽的字符类型,对应 UTF-16 编码。char32_t
:用于表示 32 位宽的字符类型,对应 UTF-32 编码。
用法
声明与初始化
可以使用 char16_t
和 char32_t
声明变量,并初始化为相应的字符常量。
char16_t c16 = u'中'; // 使用 u 前缀表示 UTF-16 编码的字符
char32_t c32 = U'中'; // 使用 U 前缀表示 UTF-32 编码的字符
字符串字面量
C++11 引入了 u
和 U
前缀来表示 UTF-16 和 UTF-32 字符串字面量。
char16_t str16[] = u"你好"; // UTF-16 编码的字符串
char32_t str32[] = U"你好"; // UTF-32 编码的字符串
使用标准库支持
标准库提供了一些支持 char16_t
和 char32_t
的工具和函数。例如,可以使用 std::u16string
和 std::u32string
来处理这些类型的字符串。
#include <iostream>
#include <string>
int main() {
std::u16string u16str = u"Hello, UTF-16";
std::u32string u32str = U"Hello, UTF-32";
std::cout << "UTF-16 string length: " << u16str.length() << std::endl;
std::cout << "UTF-32 string length: " << u32str.length() << std::endl;
return 0;
}
限制
不同平台的支持
尽管 C++11 标准引入了 char16_t
和 char32_t
类型,但不同编译器和平台的支持情况可能有所不同。确保使用支持 C++11 的编译器和标准库。
与现有库的兼容性
某些旧的库可能不直接支持 char16_t
和 char32_t
类型,因此在使用这些库时需要进行适当的转换。
#include <iostream>
#include <string>
#include <codecvt>
#include <locale>
int main() {
std::u16string u16str = u"你好";
std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t> convert;
std::string utf8str = convert.to_bytes(u16str);
std::cout << "UTF-8 string: " << utf8str << std::endl;
return 0;
}
类型转换
可以使用标准库函数和工具进行不同字符类型之间的转换,例如从 char
转换为 char16_t
或 char32_t
。
#include <codecvt>
#include <locale>
#include <string>
int main() {
std::string utf8str = "Hello";
std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t> convert16;
std::u16string u16str = convert16.from_bytes(utf8str);
std::wstring_convert<std::codecvt_utf8<char32_t>, char32_t> convert32;
std::u32string u32str = convert32.from_bytes(utf8str);
return 0;
}
总结
- 定义:
char16_t
:16 位宽的字符类型,对应 UTF-16 编码。char32_t
:32 位宽的字符类型,对应 UTF-32 编码。
- 用法:
- 声明与初始化:使用
u
和U
前缀初始化字符和字符串。 - 字符串字面量:使用
u
和U
前缀表示 UTF-16 和 UTF-32 字符串字面量。 - 标准库支持:使用
std::u16string
和std::u32string
处理 UTF-16 和 UTF-32 字符串。
- 声明与初始化:使用
- 限制:
- 不同平台的支持情况可能有所不同。
- 某些旧的库可能不直接支持
char16_t
和char32_t
类型。
- 类型转换:
- 使用标准库函数和工具进行字符类型之间的转换。
15. Type aliases(类型别名)
Type aliases(类型别名)是 C++11 引入的一个特性,允许程序员为现有类型定义一个新的名称。这种机制有助于提高代码的可读性、简化复杂类型的书写,并且有助于提高代码的可维护性。
定义
在 C++ 中,类型别名可以通过 using
关键字或 typedef
关键字来定义。
使用 using
定义类型别名:
// 使用 using 定义类型别名
using myInt = int;
using ptr = int*;
using Array = std::array<int, 5>;
int main() {
myInt x = 5;
ptr p = &x;
Array arr = {1, 2, 3, 4, 5};
return 0;
}
使用 typedef
定义类型别名:
// 使用 typedef 定义类型别名
typedef int myInt;
typedef int* ptr;
typedef std::array<int, 5> Array;
int main() {
myInt x = 5;
ptr p = &x;
Array arr = {1, 2, 3, 4, 5};
return 0;
}
用法和优势
-
增强可读性:通过为复杂的类型定义简明的别名,提高代码的可读性和理解性。
// 更好的可读性 using EmployeeID = std::string; using Salary = double;
-
简化复杂类型:简化复杂模板类型或嵌套类型的书写。
// 简化复杂类型 using IntVector = std::vector<int>; using Matrix = std::vector<std::vector<int>>;
-
提高代码的可维护性:当需要修改类型时,只需在别名处修改,而不必在整个代码库中查找和替换。
// 提高可维护性 using StudentID = int;
限制
-
与现有代码兼容性:在旧的代码库中,可能已经使用了大量的
typedef
,而不是using
,这种情况下可能需要考虑兼容性问题。 -
不支持模板别名:目前标准库尚不支持使用
using
定义模板别名,这意味着类型别名不能直接用于模板化的别名定义。// 不能直接用于模板化的别名定义 template <typename T> using MyContainer = std::vector<T>; // 错误:标准库不支持模板别名
-
可移植性问题:尽管 C++11 标准引入了
using
来定义类型别名,但不同的编译器和标准库实现可能在支持程度和实现细节上有所不同。
16. 可变参数模板(variadic templates)
C++11 引入了可变参数模板(variadic templates)的特性,它允许模板函数或类模板接受任意数量的参数。这种特性使得编写能够处理任意数量参数的通用代码变得更加简单和灵活。
定义
可变参数模板允许定义一个模板,其中的模板参数数量可以是可变的。这种特性通过省略号 ...
来实现,可以用在函数模板和类模板中。
用法
函数模板示例
#include <iostream>
// 基本情况:递归终止函数模板
template<typename T>
void print(T value) {
std::cout << value << std::endl;
}
// 递归调用函数模板
template<typename T, typename... Args>
void print(T value, Args... args) {
std::cout << value << ", ";
print(args...); // 递归调用自身,打印剩余参数
}
int main() {
print(1, 2.5, "Hello", 'a');
return 0;
}
类模板示例
#include <iostream>
// 递归调用类模板
template<typename T>
class TuplePrinter {
public:
static void print(const T& value) {
std::cout << value << std::endl;
}
};
template<typename T, typename... Args>
class TuplePrinter<T, Args...> {
public:
static void print(const T& value, const Args&... args) {
std::cout << value << ", ";
TuplePrinter<Args...>::print(args...); // 递归调用自身,打印剩余参数
}
};
int main() {
TuplePrinter<int, double, std::string>::print(1, 2.5, "Hello");
return 0;
}
限制
- 递归深度:编译器对递归深度有限制,过深的递归可能导致编译错误或运行时错误。
- 参数包展开的顺序:参数包的展开顺序是从左到右的,这在某些情况下可能影响代码的设计。
- 调用参数数量的限制:函数或类模板的实际调用时,参数数量不受特别限制,但是过多的参数可能增加代码复杂性和维护难度。
优点
- 通用性:能够处理任意数量的参数,使代码更加通用和灵活。
- 代码复用:通过递归调用自身,实现参数包的展开,可以避免重复代码。
17. 联合体(union)
C++11 引入了一种新的联合体(union)特性,称为广义联合体(generalized unions),它允许联合体包含非静态数据成员,包括具有非平凡构造函数和非平凡析构函数的成员。这使得联合体能够更加灵活地与现代 C++ 的对象模型相结合。
定义
在传统的 C++ 中,联合体中的成员只能是简单的数据类型,且不能包含构造函数和析构函数。C++11 引入了广义联合体,允许在联合体中定义具有非静态数据成员的类类型。
用法
定义广义联合体
#include <iostream>
#include <string>
union MyUnion {
int intValue;
double doubleValue;
std::string stringValue;
MyUnion() {} // 需要自定义默认构造函数
~MyUnion() {} // 需要自定义析构函数
};
int main() {
MyUnion u;
u.intValue = 42;
std::cout << "intValue: " << u.intValue << std::endl;
u.doubleValue = 3.14;
std::cout << "doubleValue: " << u.doubleValue << std::endl;
u.stringValue = "Hello";
std::cout << "stringValue: " << u.stringValue << std::endl;
return 0;
}
构造和析构函数
广义联合体中的类类型成员可以具有自定义的构造函数和析构函数,这使得可以在联合体的生命周期内正确地管理和使用这些成员。
注意事项
-
成员之间的生命周期:联合体中的各个成员共享同一块内存,因此只能同时使用一个成员。在切换使用不同成员时,应注意其生命周期的管理。
-
默认构造函数和析构函数:广义联合体中如果包含非平凡构造函数或析构函数的成员,需要手动定义联合体的默认构造函数和析构函数,以正确管理内存和资源的释放。
下限制
-
不同编译器的支持情况:尽管 C++11 标准引入了广义联合体的概念,但不同的编译器对其支持程度和实现细节可能有所不同,特别是在复杂类型和对象模型的处理上。
-
复杂性:使用广义联合体可能增加代码的复杂性和维护成本,特别是在多线程和内存管理方面需要特别注意。
18. 广义 PODs
C++11 引入了广义 PODs(Plain Old Data,普通旧数据)的概念,分为两类:平凡类型(trivial types) 和 标准布局类型(standard-layout types)。这些类型对于 C++ 对象模型的理解和优化具有重要意义。
平凡类型(Trivial Types)
平凡类型(trivial types)是指具有以下特性的类型:
- 平凡默认构造函数:可以通过默认构造函数进行初始化,并且不执行任何操作。
- 平凡复制构造函数:可以通过复制构造函数进行复制,并且不执行任何操作。
- 平凡赋值操作符:可以通过赋值操作符进行赋值,并且不执行任何操作。
- 平凡析构函数:可以通过析构函数进行销毁,并且不执行任何操作。
平凡类型的对象可以在内存中进行按位拷贝和移动,并且它们的行为不会受到其特殊成员函数(如自定义构造函数、析构函数)的影响。
标准布局类型(Standard-layout Types)
标准布局类型(standard-layout types)是指具有以下特性的类型:
- 所有非静态数据成员都具有相同的访问权限。
- 类型没有虚函数或虚基类。
- 所有非静态数据成员都位于相同的访问控制级别(private、protected、public)。
- 基类和派生类之间没有空白(padding)。
标准布局类型的对象可以进行按位拷贝和移动,并且它们的布局与 C 语言中的结构体类似,便于与 C 语言进行互操作和内存布局的控制。
用法
示例:平凡类型和标准布局类型
#include <iostream>
// 平凡类型示例
struct TrivialType {
int x;
double y;
// 默认构造函数、复制构造函数、赋值操作符、析构函数都是平凡的
};
// 标准布局类型示例
struct StandardLayoutType {
private:
int x;
public:
double y;
};
int main() {
// 平凡类型
TrivialType trivial1;
TrivialType trivial2 = trivial1; // 拷贝构造函数可以按位拷贝
// 标准布局类型
StandardLayoutType slt;
slt.y = 3.14;
std::cout << "Standard Layout Type: " << slt.y << std::endl;
return 0;
}
下限制
-
标准布局的复杂性:确保类型满足标准布局的要求可能会限制一些高级的 C++ 特性,如多继承和虚函数。
-
特定的平台依赖:平凡类型和标准布局类型的定义在不同的编译器和平台上可能会有所不同,特别是在对齐、内存布局和对象大小等方面。
示例解释
在示例中,TrivialType
和 StandardLayoutType
分别展示了平凡类型和标准布局类型的定义和用法。这些类型对于在 C++ 中进行低级别的内存操作和与 C 语言进行交互非常有用,同时也有助于编译器进行优化,例如进行按位拷贝和优化内存布局。
19. Unicode 字符串字面量
C++11 引入了对 Unicode 字符串字面量的支持,这使得在源代码中直接使用 Unicode 字符和字符串变得更加方便和直观。在传统的 C++ 中,处理 Unicode 字符通常需要使用宽字符(wchar_t)或者库函数(如 std::wstring
),而使用 Unicode 字符串字面量可以简化这些操作。
定义
Unicode 字符串字面量使用 u8
, u
, U
和 L
前缀来标识不同的 Unicode 编码方式:
u8
:UTF-8 编码。u
:UTF-16 编码,根据目标平台可能是 16 位或 32 位。U
:UTF-32 编码,通常是 32 位。L
:宽字符,与平台相关。
用法
示例:使用 Unicode 字符串字面量
#include <iostream>
int main() {
// UTF-8 字符串字面量
const char* utf8Str = u8"Hello, 你好";
// UTF-16 字符串字面量
const char16_t* utf16Str = u"Hello, 你好";
// UTF-32 字符串字面量
const char32_t* utf32Str = U"Hello, 你好";
// 宽字符字符串字面量
const wchar_t* wideStr = L"Hello, 你好";
// 输出字符串
std::cout << "UTF-8: " << utf8Str << std::endl;
std::wcout << "UTF-16: " << utf16Str << std::endl;
std::wcout << "UTF-32: " << utf32Str << std::endl;
std::wcout << "Wide Character: " << wideStr << std::endl;
return 0;
}
注意事项
-
字符集支持:编译器和运行环境必须支持所选择的字符集编码,特别是对于 UTF-16 和 UTF-32,需要确保平台支持正确的字符宽度和编码方式。
-
字符集转换:如果需要将不同编码的 Unicode 字符串转换为其他编码,可能需要使用库函数来处理,例如
std::wstring_convert
或者其他第三方库。
下限制
-
编译器支持:虽然 C++11 标准引入了对 Unicode 字符串字面量的支持,但不同的编译器和标准库实现可能在支持程度和细节上有所不同。
-
字符宽度和编码:选择合适的字符宽度和编码方式应根据目标平台和需求来确定,以确保在不同的环境中都能正常工作。
示例解释
在示例中,使用了不同类型的 Unicode 字符串字面量来存储和输出包含中文字符的字符串。这种方式使得在代码中直接使用 Unicode 字符变得更加简单和直观,而无需依赖于平台特定的宽字符类型或库函数。
20. 用户定义字面量(user-defined literals)
C++11 引入了用户定义字面量(user-defined literals)的特性,允许程序员自定义新的字面量后缀,以扩展语言内置的字面量表示法。这种特性使得程序员能够定义自己的字面量,并且可以通过简单的语法将其与现有的语言特性结合使用。
定义
用户定义字面量允许程序员为各种类型定义新的后缀,这些后缀可以附加到字面量常量(如整数、浮点数、字符串等),从而实现用户自定义的语法糖。定义用户定义字面量时,需要使用以下形式:
返回类型 operator"" 后缀名称 (参数列表)
其中:
operator""
是固定的前缀用于声明用户定义字面量。后缀名称
是用户定义的后缀名称,可以是任何有效的标识符。参数列表
是可选的参数列表,用于指定字面量常量的值。
用法
示例:定义和使用用户定义字面量
#include <iostream>
// 定义一个字符串字面量后缀 "_s"
// 返回一个 std::string 对象
std::string operator"" _s(const char* str, size_t length) {
return std::string(str, length);
}
// 定义一个整数字面量后缀 "_square"
// 返回平方值
long long operator"" _square(unsigned long long num) {
return num * num;
}
int main() {
// 使用字符串字面量后缀 "_s"
auto str = "Hello"_s;
std::cout << "String: " << str << std::endl;
// 使用整数字面量后缀 "_square"
auto result = 10_square;
std::cout << "Square of 10: " << result << std::endl;
return 0;
}
注意事项
-
后缀名称的命名规则:后缀名称必须是有效的标识符,不能与现有的语言关键字或标准库中的字面量后缀冲突。
-
参数列表的使用:参数列表允许您从字面量中提取有关字面量的信息,例如字符串的长度或整数的值。
下限制
-
全局命名空间:用户定义字面量必须在全局命名空间中定义,不能在类或命名空间中定义。
-
不同编译器的支持:尽管 C++11 标准引入了用户定义字面量的概念,但不同的编译器和标准库实现可能在支持程度和细节上有所不同。
示例解释
在示例中,定义了两个用户定义字面量后缀:_s
和 _square
。_s
后缀允许将字符串字面量转换为 std::string
对象,而 _square
后缀允许计算整数字面量的平方值。这些自定义字面量后缀提供了一种方便和直观的方法,用于增强代码的可读性和表达能力。
21. 属性(attributes)
C++11 引入了属性(attributes)的概念,它允许程序员向声明或类型添加元数据信息,以提供编译器额外的信息或指示。属性提供了一种在语言层面上扩展和影响编译器行为的机制,这些信息可以用于优化、静态分析或者在运行时对代码进行特殊处理。
定义
属性通常使用方括号 [[ ]]
包裹,并紧跟在声明或类型之后。例如:
[[attribute-list]] declaration
其中,attribute-list
是一个或多个属性的列表,每个属性可以有参数。
用法
常见的标准属性
C++ 标准库和一些编译器提供了一些标准的属性,例如:
[[noreturn]]
:指示函数不返回值。[[deprecated]]
:标记已经过时的函数或类型。[[nodiscard]]
:提醒编译器检查函数调用结果是否被忽略。[[maybe_unused]]
:告诉编译器忽略未使用的变量或参数警告。
示例
[[noreturn]] void doSomething() {
// 函数不返回值,通常是无限循环或者抛出异常
throw "Error";
}
[[deprecated("Use newFunction instead")]] void oldFunction() {
// 这个函数已经过时,不建议继续使用
}
[[nodiscard]] int getValue() {
return 42;
}
void processValue([[maybe_unused]] int x) {
// 可能不使用 x,但是希望保留参数以后使用的可能性
}
int main() {
oldFunction(); // 使用过时函数,会收到警告
int value = getValue(); // 获取值但没有使用,会收到警告
processValue(10); // 虽然参数可能未使用,但不会收到警告
return 0;
}
下限制
-
编译器支持:不同的编译器对属性的支持程度和具体的实现细节可能有所不同。
-
属性的交互性:某些属性可能与特定的编译器或标准库版本有关,需要检查确保它们在目标平台上的支持情况。
自定义属性
除了标准属性之外,C++ 还允许程序员定义自己的属性,通过宏或者特定的语法来实现。这种灵活性使得属性可以根据特定项目或框架的需要进行定制,从而提供更加精确的编译器指示和优化信息。
属性作为 C++ 的一种扩展机制,有助于提高代码的可维护性、可读性和性能,并且在一些框架和库中已经广泛应用,例如 Boost 库和一些高性能计算库。
22. Lambda 表达式(Lambda expressions)
C++11 引入了 Lambda 表达式(Lambda expressions)的特性,它是一种在 C++ 中定义匿名函数的方式,使得编写简洁、灵活的代码变得更加方便。Lambda 表达式允许在需要函数对象的地方定义一个匿名函数,而无需显式地编写函数的名称和返回类型。
定义
Lambda 表达式的基本语法如下:
[capture](parameters) -> return_type { body }
其中:
capture
:捕获列表,用于捕获外部变量,可以为空。parameters
:参数列表,类似函数的参数列表。return_type
:返回类型,可以省略,根据函数体推断。body
:函数体,类似函数体的语句块。
用法
示例:使用 Lambda 表达式
#include <iostream>
int main() {
// Lambda 表达式示例 1:不捕获变量
auto func1 = []() {
std::cout << "Hello, Lambda!" << std::endl;
};
func1(); // 调用 Lambda 表达式
// Lambda 表达式示例 2:捕获外部变量
int x = 10;
auto func2 = [x]() {
std::cout << "Value of x: " << x << std::endl;
};
func2(); // 调用 Lambda 表达式,输出 Value of x: 10
// Lambda 表达式示例 3:带参数和返回值
auto add = [](int a, int b) -> int {
return a + b;
};
int result = add(5, 3);
std::cout << "5 + 3 = " << result << std::endl;
return 0;
}
捕获列表
Lambda 表达式可以捕获外部变量,捕获方式有三种:
[ ]
:不捕获任何外部变量。[var]
:捕获变量var
的值,值传递。[&var]
:捕获变量var
的引用,引用传递。
返回类型推断
如果 Lambda 表达式的函数体只有一条 return
语句,且不需要显式指定返回类型,编译器可以推断出返回类型,因此可以省略返回类型的指定。
下限制
-
复杂性:Lambda 表达式虽然提供了方便的语法糖,但过度使用可能会导致代码难以阅读和维护,尤其是在复杂的逻辑或多层嵌套中。
-
捕获的生命周期:如果捕获的变量在 Lambda 表达式执行完毕后可能被销毁,需要特别注意捕获的方式,以避免悬空引用。
-
模板化:Lambda 表达式可以与模板函数结合使用,但需要注意模板推导的规则和与函数对象的差异。
示例解释
在示例中,展示了 Lambda 表达式的基本用法,包括如何定义不带捕获、带捕获的 Lambda 函数,以及如何使用参数和返回值。Lambda 表达式特别适合用于需要临时性函数对象的场景,例如算法函数、回调函数或者简单的函数对象替代品。
23. noexcept
指示符(specifier)和 noexcept
运算符(operator)
C++11 引入了 noexcept
指示符(specifier)和 noexcept
运算符(operator),用于指示函数是否会抛出异常,从而提高代码的可靠性和性能。
noexcept
指示符(Specifier)
noexcept
指示符用于声明一个函数是否会抛出异常。它的基本语法如下:
return_type function_name(parameters) noexcept;
其中:
return_type
:函数返回类型。function_name
:函数名称。parameters
:函数参数列表。noexcept
:指示符,表示该函数不会抛出异常。
示例:
#include <iostream>
#include <vector>
void mayThrow() {
throw "Exception";
}
void noThrow() noexcept {
std::cout << "No exceptions will be thrown." << std::endl;
}
int main() {
try {
mayThrow();
} catch (...) {
std::cout << "Exception caught!" << std::endl;
}
noThrow(); // 不会抛出异常
return 0;
}
在上面的示例中,noThrow()
函数使用了 noexcept
指示符,表示该函数不会抛出异常。如果函数内部确实抛出了异常(例如 mayThrow()
函数),而没有被 try-catch
块捕获,程序会调用 std::terminate()
来终止程序的执行。
noexcept
运算符(Operator)
noexcept
运算符用于在运行时检查表达式是否可能抛出异常。它的语法如下:
bool expression() noexcept;
示例:
#include <iostream>
#include <vector>
int main() {
std::vector<int> v = {1, 2, 3, 4, 5};
bool isNoexcept = noexcept(v.push_back(6));
std::cout << "Is push_back() noexcept? " << std::boolalpha << isNoexcept << std::endl;
return 0;
}
在上面的示例中,noexcept(v.push_back(6))
运算符用于检查 std::vector::push_back()
是否可能抛出异常。如果 push_back()
函数确实为 noexcept
,则返回 true
,否则返回 false
。
下限制
-
异常安全性:使用
noexcept
指示符和运算符有助于提高代码的异常安全性,但仍需谨慎处理异常情况,特别是在资源管理和多线程环境中。 -
函数调用:当函数声明为
noexcept
时,编译器会尝试优化代码,以避免不必要的异常检查和堆栈展开,从而提高性能。 -
移动语义:许多标准库中的函数(如移动构造函数和移动赋值运算符)通常使用
noexcept
来确保移动操作不会抛出异常,从而提高容器的性能。
示例解释
在示例中,展示了如何使用 noexcept
指示符来声明函数不会抛出异常,并通过 noexcept
运算符来检查特定函数调用是否为 noexcept
。这些特性对于优化和提高代码的可靠性特别有用,尤其是在需要高效处理异常和资源管理的情况下。
24. 关键字alignof
和 alignas
C++11 引入了 alignof
和 alignas
这两个关键字,用于控制和查询内存对齐(alignment)的行为,以及指定对象的对齐方式。
alignof
关键字
alignof
关键字用于查询类型或表达式的对齐要求(alignment requirement),即对象在内存中的对齐边界(alignment boundary)。它的语法如下:
alignof(type)
alignof(expression)
其中:
type
:要查询对齐要求的类型。expression
:要查询对齐要求的表达式。
示例:
#include <iostream>
struct MyStruct {
int a;
double b;
};
int main() {
std::cout << "Alignment of int: " << alignof(int) << std::endl;
std::cout << "Alignment of double: " << alignof(double) << std::endl;
std::cout << "Alignment of MyStruct: " << alignof(MyStruct) << std::endl;
return 0;
}
在上面的示例中,alignof(int)
和 alignof(double)
分别打印出 int
和 double
类型的对齐要求(通常是 4 字节和 8 字节),而 alignof(MyStruct)
打印出结构体 MyStruct
的对齐要求。
alignas
关键字
alignas
关键字用于指定对象或类型的对齐方式。它可以用于变量、非静态数据成员或者类型本身。它的语法如下:
alignas(alignment) entity
其中:
alignment
:对齐方式,可以是常量表达式。entity
:要指定对齐方式的变量、非静态数据成员或者类型。
示例:
#include <iostream>
struct alignas(16) AlignedStruct {
int a;
double b;
};
int main() {
alignas(32) char buffer[64]; // 以 32 字节对齐的字符数组
std::cout << "Alignment of buffer: " << alignof(decltype(buffer)) << std::endl;
AlignedStruct s;
std::cout << "Alignment of AlignedStruct: " << alignof(AlignedStruct) << std::endl;
return 0;
}
在上面的示例中,alignas(16) AlignedStruct
指定了 AlignedStruct
结构体以 16 字节对齐,而 alignas(32) char buffer[64]
指定了 buffer
字符数组以 32 字节对齐。
下限制
-
平台依赖性:对齐要求和实际的对齐方式在不同的平台和编译器上可能会有所不同,特别是在涉及到 SIMD(Single Instruction, Multiple Data)指令和硬件优化时更为显著。
-
内存浪费:使用过大的对齐值可能会导致内存浪费,尤其是在对齐数据结构或数组时需要考虑到结构体成员的排列和空白填充。
-
兼容性:某些旧版编译器可能不支持或者支持有限,需要根据实际项目的需求和目标平台进行选择和使用。
示例解释
在示例中,展示了如何使用 alignof
查询不同类型的对齐要求,并使用 alignas
指定结构体和数组的对齐方式。这些功能对于需要精确控制内存布局和对齐的场景非常有用,例如在高性能计算、嵌入式系统或者与硬件交互的应用中。
25. 多线程内存模型(multithreaded memory model)
C++11 引入了多线程内存模型(multithreaded memory model),这是一组规则和约定,用于指导多线程程序中不同线程之间的内存访问行为,确保程序在多线程环境下的正确性和可预测性。
定义与讲解
多线程内存模型主要涉及到以下几个关键点:
-
原子操作(Atomic operations):原子操作是指不会被其他线程中断的操作,要么完全执行成功,要么完全不执行。C++11 引入了
std::atomic
类模板,用于支持原子操作,确保多线程环境下对共享变量的安全访问。 -
内存顺序(Memory ordering):内存顺序指定了不同线程之间操作执行的顺序和可见性,确保多线程程序的行为不会因为编译器优化或者硬件优化而出现不一致。C++11 提供了一组标准的内存顺序,如
std::memory_order_relaxed
、std::memory_order_acquire
、std::memory_order_release
等,允许程序员在需要时指定操作的执行顺序。 -
互斥量(Mutexes)和条件变量(Condition variables):C++11 标准库提供了
std::mutex
、std::lock_guard
、std::unique_lock
等机制,用于实现线程之间的互斥访问和条件等待,确保共享资源的安全访问和线程间的协调。
用法
示例:使用原子操作和互斥量
#include <iostream>
#include <thread>
#include <atomic>
std::atomic<int> counter(0);
std::mutex mtx;
void incrementAtomic() {
for (int i = 0; i < 1000000; ++i) {
counter.fetch_add(1, std::memory_order_relaxed);
}
}
void incrementMutex() {
for (int i = 0; i < 1000000; ++i) {
std::lock_guard<std::mutex> lock(mtx);
counter++;
}
}
int main() {
std::thread t1(incrementAtomic);
std::thread t2(incrementAtomic);
t1.join();
t2.join();
std::cout << "Atomic counter: " << counter << std::endl;
counter = 0; // 重置计数器
std::thread t3(incrementMutex);
std::thread t4(incrementMutex);
t3.join();
t4.join();
std::cout << "Mutex counter: " << counter << std::endl;
return 0;
}
在上面的示例中,incrementAtomic
和 incrementMutex
函数分别使用原子操作和互斥量对共享变量 counter
进行递增操作。使用原子操作可以避免显式的互斥量,但需要确保操作的顺序和可见性。使用互斥量可以确保线程安全,但会带来一定的性能开销。
下限制
-
复杂性和调试:多线程编程增加了程序的复杂性,特别是在调试和定位竞态条件(race conditions)时,可能需要额外的工具和技术支持。
-
性能开销:使用互斥量和原子操作会带来一定的性能开销,特别是在高并发的情况下,需要仔细设计和优化程序结构。
-
死锁和饥饿:不正确使用互斥量可能导致死锁(deadlock)或者线程饥饿(starvation),因此需要遵循良好的并发编程实践。
示例解释
多线程内存模型为 C++ 提供了一套强大的工具和规范,帮助程序员编写线程安全的并发代码。通过使用原子操作、互斥量和条件变量,程序可以在多核心和多线程环境下安全地共享和访问数据,确保数据的一致性和可靠性。
26. 线程局部存储(Thread-local storage,TLS)
C++11 引入了线程局部存储(Thread-local storage,TLS)的概念,允许程序员在多线程应用中创建变量,使每个线程都拥有其自己的变量副本,从而提供了一种线程安全的数据共享和访问方式。
定义与讲解
线程局部存储允许程序员定义的变量,每个线程都有其自己的副本,而不是所有线程共享一个变量。这在多线程应用程序中特别有用,因为它可以避免竞态条件和显式的同步操作,提高程序的并发性能和可维护性。
用法
在 C++11 中,可以通过 thread_local
关键字来声明线程局部变量:
thread_local int tls_variable; // 声明一个线程局部变量 tls_variable
示例:
#include <iostream>
#include <thread>
thread_local int tls_variable; // 声明一个线程局部变量 tls_variable
void threadFunction(int threadId) {
tls_variable = threadId; // 设置线程局部变量的值
std::cout << "Thread " << threadId << " TLS variable: " << tls_variable << std::endl;
}
int main() {
std::thread t1(threadFunction, 1);
std::thread t2(threadFunction, 2);
t1.join();
t2.join();
return 0;
}
在上面的示例中,tls_variable
是一个线程局部变量,每个线程都有自己的副本。在 threadFunction
函数中,每个线程分别将 tls_variable
的值设置为线程的 ID,并打印出来。由于是线程局部变量,因此每个线程看到的 tls_variable
值是独立的,不会互相影响。
下限制
-
内存开销:线程局部存储可能会增加程序的内存使用量,因为每个线程都需要维护其自己的变量副本。
-
跨平台兼容性:不同的编译器和操作系统对线程局部存储的支持程度可能有所不同,需要注意平台特定的实现细节。
-
静态初始化限制:C++11 规定线程局部变量必须是 POD(Plain Old Data)类型或者具有常量初始化,因此在使用非静态初始化或者自定义类型时需要谨慎处理。
示例解释
线程局部存储为多线程编程提供了一种便捷和高效的方法来管理线程私有的数据,避免了显式的同步操作和全局变量的并发访问问题。通过 thread_local
关键字,程序员可以在多线程环境下更加安全和高效地共享和访问数据,从而提高程序的并发性能和可维护性。
27. 范围 for 循环(range-based for loop)
C++11 引入了范围 for 循环(range-based for loop),这是一种方便的语法糖,用于遍历容器和其他支持迭代器的数据结构中的元素。虽然 C++11 标准库本身提供了对标准容器的支持,但如果要使用 Boost 库提供的范围 for 循环,需要包含相应的 Boost 头文件和命名空间。
定义与讲解
范围 for 循环的语法如下:
for (declaration : range) {
// 使用 declaration 处理 range 中的每个元素
}
其中:
declaration
是对迭代元素的声明,通常是一个引用(或者值,取决于元素是否可以被复制)。range
是要遍历的范围,可以是标准容器、数组、初始化列表或者其他支持迭代器的数据结构。
Boost 库中的范围 for 循环
Boost 库扩展了 C++ 的功能,包括提供了对范围 for 循环的支持。为了使用 Boost 提供的范围 for 循环,需要包含相应的头文件和使用 Boost 命名空间。例如:
#include <iostream>
#include <vector>
#include <boost/range/algorithm.hpp> // 包含 Boost 范围算法头文件
#include <boost/range/adaptor/filtered.hpp> // 包含 Boost 范围适配器头文件
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
// 使用 Boost 提供的范围 for 循环遍历容器
for (int& x : boost::range::adaptors::filter(vec, [](int n) { return n % 2 == 0; })) {
std::cout << x << " ";
}
std::cout << std::endl;
return 0;
}
在上面的示例中,使用了 Boost 提供的 boost::range::adaptors::filter
来过滤偶数,并结合范围 for 循环输出符合条件的元素。
下限制
-
依赖 Boost 库:使用 Boost 扩展功能需要安装 Boost 库,并包含相应的头文件。这增加了项目的依赖性和配置复杂性。
-
学习曲线:Boost 库提供了丰富的功能和模块,学习和理解其使用可能需要额外的时间和精力。
-
兼容性:虽然 Boost 是一个广泛使用的库,但不同版本的 Boost 可能会有不同的特性和行为,需要确保选择合适的版本与编译器配合使用。
范围 for 循环作为 C++11 提供的语法糖,通过简化遍历容器和其他支持迭代器的数据结构的代码,提高了代码的可读性和简洁性,同时在 Boost 库中提供了更多灵活和强大的功能来扩展其应用场景。
28. 语言特性static_assert
在 C++11 中,static_assert
是一种编译时断言(compile-time assertion),用于在编译期间对某些条件进行静态检查。它在程序编译时检查条件是否为真,如果条件为假,则导致编译失败,并显示用户指定的错误消息。
定义与讲解
static_assert
的语法如下:
static_assert(constant_expression, "error message");
其中:
constant_expression
是一个常量表达式,如果在编译时求值为false
,则触发断言失败。"error message"
是一个字符串字面值,用于指定断言失败时的错误消息。
示例
#include <iostream>
// 静态断言示例:确保 int 类型占用的字节数为 4
static_assert(sizeof(int) == 4, "int must be 4 bytes");
int main() {
std::cout << "Size of int: " << sizeof(int) << " bytes" << std::endl;
return 0;
}
在这个例子中,static_assert(sizeof(int) == 4, "int must be 4 bytes");
断言会在编译时检查 int
类型是否占用 4 个字节,如果不是,编译器将显示错误消息 "int must be 4 bytes"
并终止编译。
使用 Boost 库的 static_assert
Boost 库没有单独提供 static_assert
,因为 C++11 标准已经原生支持。因此,使用 Boost 库的场景可能更多地涉及到其它功能和模块的使用,而不是 static_assert
。
下限制
-
编译时检查:
static_assert
只能在编译时进行检查,无法用于运行时的条件检查。 -
错误消息:错误消息必须是一个字符串字面值,无法动态生成,因此需要在断言中提前定义好错误信息。
-
常量表达式:条件表达式必须是一个常量表达式,在编译期间能够确定其值,否则无法进行静态检查。
static_assert
在 C++11 中作为一种强大的工具,用于确保代码在编译期间满足某些重要条件,比如类型大小、预期值等,从而提高了代码的健壮性和可靠性。
C/C++Linux服务器开发/高级架构师 大厂面试题、学习资料、教学视频和学习路线图(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),↓↓↓↓↓↓见下面文章底部点击免费领取↓↓↓↓↓↓