从 auto 到 Lambda:全面解析 C++11 核心新特性

在介绍 C++11 之前,我们先回顾一下 C++98和C++03。C++98 作为 C++ 的第一个国际标准,奠定了这门语言的基础结构和核心特性,比如类、继承、模板、异常处理等。这些特性使得 C++ 成为一门强大的、面向对象的编程语言,广泛应用于系统/应用软件、游戏开发、实时系统等领域。C++03 则是对 C++98 进行了修订,主要解决标准的疑义和错误,没有引入新特性。

然而,随着软件开发的不断进化,C++98和C++03 在表达能力、编程便利性和性能方面显示出了局限性。比如:对并发编程的支持不够强大,模板编程有时显得过于复杂,资源管理(尤其是内存管理)易于出错等。这些局限性促使了 C++11 标准的诞生,它被视为 C++ 的一次重大更新,于 2011 年正式发布。这次更新标志着 C++ 进入现代化的重要一步,引入了许多新特性和改进,旨在使 C++ 更易于使用,更灵活,同时提高了代码的安全性和性能。

另外,学习 C++11 新特性需要你有简单的 C、C++编程基础,还不了解的朋友可以看这两篇文章:如何学习 C 语言如何快速学习 C++文章通俗易懂而且有丰富的代码示例帮助理解。非常值得一看!!

C++11 新增了很多新特性值得我们学习,包含如下:

核心语言增强

自动类型推断 (auto)

auto 关键字让编译器能够自动推断变量的类型(通过变量初始化时的表达式来确定类型),简化了变量声明的语法。

语法:
auto variable_name = expression;
示例:
auto x = 5; // x 是 int
auto y = 3.14; // y 是 double

获取表达式的类型(decltype)

语法
decltype(expression) variableName;

这里,expression 是你要查询类型的表达式,而 variableName 是使用该表达式类型声明的变量名称。

示例代码:

基础示例

int x = 5;
decltype(x) y = x; // y 的类型是int

在这个例子中,decltype(x)将y的类型推导为x的类型,即int

结合 auto 使用:

auto x = 1; // x 的类型是int
decltype(x) y = x; // y 的类型也是int

用于复杂表达式

decltype 特别有用于表达式的类型不明显时:

std::vector<int> vec;
decltype(vec.begin()) it = vec.begin(); // it 的类型是 std::vector<int>::iterator

基于范围的 for 循环

C++11引入了基于范围的for循环(Range-based for loop),这是一个用于遍历序列(如数组、容器等)的语法糖。它简化了迭代序列中每个元素的代码书写方式,使代码更加简洁易读。

基本语法:
for (declaration : range) {
    // 循环体
}
  • declaration:用于迭代序列中每个元素的变量声明。这个变量的类型可以是序列元素的类型,也可以是(auto)自动类型推导。
  • range:要迭代的序列,可以是数组、容器(如std::vector、std::list等)或任何支持begin()和end()方法的对象。
示例:

遍历数组

#include <iostream>

int main() {
    int array[] = {1, 2, 3, 4, 5};
    for (int element : array) {
        std::cout << element << ' ';
    }
    // 输出: 1 2 3 4 5
    return 0;
}

遍历容器(如std::vector)

#include <iostream>
#include <vector>

int main() {
    std::vector<int> vec = {10, 20, 30, 40, 50};

    for (int element : vec) {
        std::cout << element << ' ';
    }
    // 输出: 10 20 30 40 50
    return 0;
}

使用 auto 自动类型推导

#include <vector>
#include <iostream>

int main() {
    std::vector<std::string> words = {"Hello", "World", "!"};

    for (auto word : words) {
        std::cout << word << ' ';
    }
    // 输出: Hello World !
    return 0;
}

注意事项:

如果你需要在循环中修改序列中的元素,请使用引用&来声明变量。

std::vector<int> vec = {1, 2, 3};
for (int& num : vec) {
    num *= 2; // 修改元素值
}

如果不需要修改元素,并且元素类型较大时,考虑使用常量引用const &来避免不必要的拷贝,提高效率。

for (const auto& word : words) {
    std::cout << word << ' ';
}

基于范围的for循环是 C++11 中引入的一项便利特性,通过简化集合的遍历操作,它让代码更加简洁,增强了代码的可读性和易用性。

统一初始化

C++11 引入了统一初始化(Uniform Initialization),这是一种使用花括号 {} 进行变量初始化的语法。它提供了一种一致的语法来初始化任何对象。

语法:
Type variable{value1, value2, ...};

基本类型的初始化:

int a{10};
double b{3.14};

聚合类型(如结构体和数组)的初始化:

struct Point {
    int x, y;
};
Point p{10, 20};

int arr[]{1, 2, 3, 4, 5};

容器的初始化

#include <vector>
std::vector<int> v{1, 2, 3, 4, 5};

类对象的初始化

class MyClass {
public:
    MyClass(int x, double y) : x_(x), y_(y) {}
private:
    int x_;
    double y_;
};

MyClass obj{5, 3.14};

初始器列表(Initializer Lists)

初始器列表是C++11引入的一项特性,它进一步扩展了统一初始化的能力,特别是对于容器和自定义类对象的初始化。它允许构造函数接收一个由花括号{}包围的元素列表,从而提供了一种简洁且强大的初始化方式。

包含必要头文件

要使用初始器列表,你需要包含<initializer_list>头文件。

#include <initializer_list>
语法

初始器列表主要通过在类构造函数中使用std::initializer_list<T>类型的参数来实现,其中T是列表中元素的类型。

class ClassName {
public:
    ClassName(std::initializer_list<T> list);
};

示例代码:

自定义类的初始器列表

假设有一个代表简单整数集合的类,我们希望能够在创建对象时直接用一组整数来初始化这个集合:

#include <initializer_list>
#include <iostream>
#include <vector>

class IntSet {
    std::vector<int> elements;
public:
    IntSet(std::initializer_list<int> list) : elements(list) {
        std::cout << "Initialized with elements: ";
        for (int elem : elements) {
            std::cout << elem << " ";
        }
        std::cout << std::endl;
    }
};

int main() {
    IntSet mySet = {1, 2, 3, 4, 5};
    return 0;
}
// 输出: Initialized with elements: 1 2 3 4 5 

函数参数为初始器列表:

函数也可以接受std::initializer_list<T>类型的参数,这在需要传递一组值时非常有用:

#include <initializer_list>
#include <iostream>

void print(std::initializer_list<int> vals) {
    for (auto val : vals) {
        std::cout << val << " ";
    }
    std::cout << std::endl;
}

int main() {
    print({10, 20, 30, 40, 50});
    return 0;
}
// 输出: 10 20 30 40 50 
总结

初始器列表(Initializer Lists)为C++11提供了一种强大的初始化机制,特别是在初始化需要一组值的对象时。通过使用初始器列表,可以极大地简化代码,提高可读性和可维护性。这一特性在自定义类、函数参数传递时尤为有用。

nullptr 关键字

在 C++11 中,nullptr 是一个特殊的字面量,用于表示空指针。它是对之前 C++ 版本中使用整数 0 或宏 NULL 来表示空指针的改进。nullptr 的引入提供了一种类型安全的方式来表示没有指向任何对象的指针。

定义及使用:
int* ptr = nullptr;
if (ptr == nullptr) {
    // 检查 ptr 是否为空
}
为什么需要 nullptr?
  • 类型安全:在 C++11 之前,NULL 通常被定义为 0,这意味着它实际上是一个整数。这可能导致类型混淆和错误,特别是在函数重载的情况下。nullptr 明确地表示一个空指针,不会与整数混淆。
  • 更好的语义nullptr 直观地表示指针为空,改善了代码的可读性和意图表达。
示例代码:

使用 nullptr 初始化指针

int* ptr = nullptr; // ptr 是一个指向 int 的空指针

函数重载中 nullptr 的优势

void func(int) {
    std::cout << "func(int) called" << std::endl;
}
void func(int*) {
    std::cout << "func(int*) called" << std::endl;
}

int main() {
    func(0);        // 调用 func(int)
    func(nullptr);  // 明确调用 func(int*)
    return 0;
}

在这个例子中,使用 nullptr 可以明确地调用接受指针参数的重载函数版本,避免了潜在的歧义。

通过引入nullptr,C++11 提高了代码的类型安全性和清晰度,明确区分了整数 0 和空指针的概念。

长长整形(Long Long Int)

在 C++11 之前,整型的最大大小受限于long int,其大小至少为32位。为了支持更大的整数,C++11引入了long long intunsigned long long int类型,保证至少64位的大小。

示例:
long long int bigNumber = 9223372036854775807LL; // LL 表示这是一个 long long 类型的字面量
unsigned long long int bigUnsignedNumber = 18446744073709551615ULL; // ULL 表示这是一个 unsigned long long 类型的字面量

无符号字面量

无符号字面量就是用来表示无符号整数的字面量。在C++中,可以通过在整数后面添加Uu来创建无符号整型字面量。

注意:字面量(Literal)是指在源代码中直接表示其值的固定值的表示法。字面量可以是数字、字符、字符串或其他固定值。

示例:
unsigned int x = 123U; // U 表示无符号整数

用户自定义字面量

C++11引入了用户自定义字面量(User-Defined Literals, UDL),允许开发者定义自己的字面量操作符,为字面量赋予新的含义。这通过定义一个以_开头的字面量操作符函数实现。

语法:
return_type operator "" _customSuffix(const char*);
示例:

定义一个将字符串转换为复数的自定义字面量:

#include <iostream>
#include <complex>

// 定义自定义字面量 _i,用于创建复数
std::complex<double> operator"" _i(long double d) {
    return std::complex<double>(0, d); // 第一个参数表示所构造复数的实部,第二个参数代表虚部。
}

int main() {
    auto c = 3.14_i; // 使用自定义字面量创建一个复数,将会调用operator"" _i 函数
    std::cout << "Real part: " << c.real() << ", Imaginary part: " << c.imag() << std::endl;
    return 0;
}

在这个例子中,_i是自定义的字面量操作符,它将跟随它的数字转换为一个复数。这使得代码更加直观和易于理解。

强类型枚举 (enum class)

C++11 引入了强类型枚举,也称为作用域枚举(scoped enums),使用 enum class 关键字定义。与传统的枚举(unscoped enums)相比,强类型枚举具有更好的类型安全性,不会隐式地转换为整型,枚举值必须在作用域内访问,并且可以指定底层类型。

语法
enum class EnumName : UnderlyingType {
    enumerator1,
    enumerator2,
    ...
};
  • EnumName 是枚举类型的名称。
  • UnderlyingType 是用来表示枚举值的底层类型,通常是某种整型(如int、unsigned int、short等)。如果没有该字段,则底层类型默认为 int
强类型枚举定义
enum class Color : unsigned int {
    Red,
    Green,
    Blue
};

enum class StatusCode : char {
    Ok = 'O',
    Error = 'E',
    Unknown = 'U'
};

简单例子
#include <iostream>

// 传统枚举
enum Color { Red, Green, Blue };

// 强类型枚举  底层类型为 int
enum class StrongColor { Red, Green, Blue };

int main() {
    Color c = Red; // 直接访问
    // StrongColor sc = Red; // 错误:Red 不在作用域内
    StrongColor sc = StrongColor::Red; // 正确:使用作用域访问
    
    // int colorInt = sc; // 错误:不能隐式转换为整型
    int colorInt = static_cast<int>(sc); // 正确:需要显式转换
    
    std::cout << colorInt << std::endl; // 输出:0,假设 StrongColor::Red 底层对应的整数值为 0
    return 0;
}

在这个例子中,强类型枚举 StrongColor 的使用增加了类型安全性,避免了与整型之间的隐式转换,并且强制使用枚举类名作为作用域来访问枚举值。这些特性有助于避免命名冲突和提高代码清晰度。

常量表达式 (constexpr)

C++11 引入了 constexpr 关键字,用于定义常量表达式。这个关键字可以用于变量、函数和构造函数,允许在编译时进行计算,而不是运行时。这对于提高程序的性能非常有用,因为它允许在编译期间执行更多的计算,减少运行时的工作量。

语法

定义常量表达式变量

// type 变量类型
constexpr type variable = value;

// 定义常量
constexpr int max_size = 100; 

定义常量表达式函数

// type 函数的返回值类型
constexpr type function_name(parameters) {
    // 函数体
}

定义常量表达式函数:
constexpr int square(int x) {
    return x * x;
}

常量表达式函数必须返回一个常量表达式,函数体中只能有一条返回语句,且不能包含任何形式的循环、分支(除了条件运算符)等。

声明类构造函数:允许类类型在编译时被初始化。构造函数体必须为空,所有成员初始化都必须使用常量表达式。

class Point {
public:
    constexpr Point(double xVal = 0, double yVal = 0) : x(xVal), y(yVal) {}
    constexpr double getX() const { return x; }
    constexpr double getY() const { return y; }

private:
    double x, y;
};

void main(){
  constexpr Point p(9.0, 27.0);
  constexpr double x = p.getX(); // 在编译时计算
}
使用 constexpr 的优点包括:
  • 性能提升:通过在编译时而不是运行时计算值,可以提高程序的运行效率。
  • 类型安全:与宏相比,constexpr 提供了更强的类型安全。
  • 更广泛的用途constexpr 变量、函数和对象可以用在需要编译时常量的上下文中,如数组大小、模板参数等。

默认和删除函数

C++11引入了两个重要的特性:允许显式地声明默认构造函数和析构函数,以及允许删除函数。这些特性提供了对类行为更细致的控制,特别是在管理资源、实现单例模式或防止对象拷贝时非常有用。

默认函数(= default)

在C++11之前,如果你希望类有一个默认的构造函数、拷贝构造函数、拷贝赋值运算符或析构函数,你通常不需要做任何事情;编译器会为你自动生成这些。然而,一旦你定义了任何构造函数,编译器就不会自动生成默认构造函数了。C++11通过= default关键字允许你显式地要求编译器为你生成这些函数,即使你已经定义了其他构造函数。

语法:

使用关键字 = default 来声明

class ClassName {
public:
    ClassName() = default; // 默认构造函数
    ClassName(const ClassName&) = default; // 默认拷贝构造函数
    ClassName& operator=(const ClassName&) = default; // 默认拷贝赋值运算符
    ~ClassName() = default; // 默认析构函数
};

代码示例

class MyClass {
public:
    MyClass() = default; // 显式声明使用编译器生成的默认构造函数
    MyClass(int value) : data(value) {} // 自定义构造函数
    // ...
private:
    int data;
};
删除函数(= delete)

C++11允许你显式地禁用类的某些函数(比如拷贝构造函数或拷贝赋值运算符),只需将它们声明为= delete。这对于防止对象被无意拷贝或赋值非常有用,尤其在设计只能移动不能拷贝的资源管理类时。

语法:

class ClassName {
public:
    ClassName(const ClassName&) = delete; // 禁用拷贝构造函数
    ClassName& operator=(const ClassName&) = delete; // 禁用拷贝赋值运算符
};

代码示例

class NonCopyable {
public:
    NonCopyable() = default;
    NonCopyable(const NonCopyable&) = delete; // 禁止拷贝
    NonCopyable& operator=(const NonCopyable&) = delete; // 禁止赋值
    // ...
};
总结:

= default= delete是C++11中引入的两个关键特性,它们提供了对类默认行为的显式控制。通过= default,你可以明确地告诉编译器为类生成默认的构造函数、析构函数或拷贝/赋值运算符,即使定义了其他构造函数。通过= delete,你可以防止类的拷贝或赋值,这在设计不可拷贝的资源管理类或单例类时特别有用。这两个特性让类的设计意图更加清晰,同时也有助于避免潜在的错误。

委托构造函数

在 C++11 中,委托构造函数(Delegating Constructors)是一种允许一个构造函数在同一个类中调用另一个构造函数的功能,目的是为了减少代码重复,提高代码复用性。这允许构造函数之间的代码共享,从而避免在每个构造函数中重复相同的初始化代码。

语法

委托构造函数的语法相当直接,就是在构造函数的初始化列表中调用同一个类的另一个构造函数。

class ClassName {
public:
    ClassName(参数列表) : ClassName(其他参数列表) {
        // 构造函数体
    }
};

这里,构造函数通过在其初始化列表中调用另一个构造函数(即委托给另一个构造函数),实现对对象的初始化。

代码示例

考虑一个简单的 Rectangle 类,它有两个成员变量:长度和宽度。我们可以使用委托构造函数来确保所有的构造逻辑都通过一个主要的构造函数来执行,避免代码的重复。

class Rectangle {
public:
    // 主构造函数
    Rectangle(double width, double height) : width(width), height(height) {
        // 这里可以包含一些特定的初始化逻辑
        std::cout << "Rectangle(double, double)" << std::endl;
    }
    // 委托构造函数,委托给主构造函数
    Rectangle(double side) : Rectangle(side, side) {
        // 注意:这里的初始化逻辑会在主构造函数之后执行
        std::cout << "Rectangle(double)" << std::endl;
    }
    void area() const {
        std::cout << "Area: " << width * height << std::endl;
    }
private:
    double width, height;
};

int main() {
    Rectangle square(5); // 使用委托构造函数
    square.area(); // 输出: Area: 25
    
    Rectangle rectangle(4, 5); // 使用主构造函数
    rectangle.area(); // 输出: Area: 20
    return 0;
}

在这个例子中,Rectangle类有三个构造函数:

  • 一个是接受两个参数(宽度和高度)的主构造函数。
  • 另外两个是委托构造函数,一个不带参数,默认构造一个宽度和高度都为0的矩形;另一个只带一个参数,构造一个正方形。

这样的设计让构造函数的初始化逻辑更加集中,如果需要修改初始化逻辑,只需要修改主构造函数即可,提高了代码的可维护性。

继承构造函数

在C++11中,引入了继承构造函数的概念,这允许派生类继承并直接使用基类的构造函数,而不需要在派生类中重新定义相同的构造函数。这个特性通过简化代码,避免不必要的重复,提高了代码的可维护性。

语法

要在派生类中继承基类的构造函数,你可以使用using 声明。基本语法如下:

class Derived : public Base {
public:
    using Base::Base;
};

这里,Derived 类通过 using Base::Base;声明,继承了 Base 类所有的构造函数。

代码示例

考虑以下基类Person,它有一个构造函数,接受一个表示人名的字符串参数:

#include <iostream>
#include <string>

class Person {
public:
    std::string name;
    Person(std::string n) : name(n) {
        std::cout << "Person(" << name << ")" << std::endl;
    }
};

现在,我们定义一个 Employee 类,它是 Person 的派生类,并且我们想让 Employee 类能够直接使用 Person 类的构造函数:

class Employee : public Person {
public:
    using Person::Person; // 继承构造函数
    void printName() {
        std::cout << "Employee Name: " << name << std::endl;
    }
};

接着,我们可以这样使用 Employee 类:

int main() {
    Employee emp("John Doe");
    emp.printName(); // 输出:Employee Name: John Doe
    return 0;
}

在这个例子中,Employee 类继承了 Person 类的构造函数,所以我们可以直接使用一个字符串参数来构造 Employee 对象。这就避免了在 Employee 类中重新定义一个接受相同参数的构造函数。

显式虚函数重载(override)

C++11引入了一种新的方式来控制虚函数的重载,称为“显式虚函数重载”(Explicit Virtual Function Override)。这通过在派生类中的成员函数声明时使用override关键字实现。这个关键字明确指出一个成员函数意图重写一个基类的虚函数。使用override可以提高代码的可读性,并且帮助编译器检查派生类是否真正重载了基类中的虚函数,避免因拼写错误或函数签名不匹配而导致的问题。

语法:
class Base {
public:
    virtual void func();
};

class Derived : public Base {
public:
    void func() override; // 明确指明重载基类的虚函数
};
代码示例:
#include <iostream>

class Base {
public:
    virtual void sayHello()  {
        std::cout << "Hello from Base" << std::endl;
    }
};

class Derived : public Base {
public:
    void sayHello()  override { // 正确重载 Base 的 sayHello
        std::cout << "Hello from Derived" << std::endl;
    }
};

int main() {
    Base* base = new Derived();
    base->sayHello(); // 输出: Hello from Derived
    delete base;
    return 0;
}

在这个例子中,Derived 类通过 override 关键字明确表示 sayHello 函数重写了 Base 类中的虚函数。如果签名不匹配,编译器将报错。

final 关键字

在C++11中,final关键字被引入作为类和虚函数的新修饰符。当final用于类时,它阻止该类被继承。当用于虚函数时,它阻止该函数在派生类中被进一步重写。这提供了一种明确表达设计意图的方式,并能够避免不必要的运行时错误。

用于类

final 用于一个类时,任何尝试继承该类的行为都将导致编译错误。

语法:

class Base final { 
  /* 类定义实现 */  
};

代码示例:

class Base final {};

class Derived : public Base { // 这将导致编译错误
};

在这个示例中,尝试从 Base 类派生 Derived 类会导致编译错误,因为 Base 类被标记为 final。

用于虚函数

final 用于虚函数时,它表示该函数不能在任何派生类中被重写。

语法:

virtual void function() final;

示例:

class Base {
public:
    virtual void show() final {
        std::cout << "Base show" << std::endl;
    }
};

class Derived : public Base {
public:
    void show() override { // 这将导致编译错误
        std::cout << "Derived show" << std::endl;
    }
};

在这个示例中,Derived 类尝试重写 Base 类中的 show 函数会导致编译错误,因为 show 函数在 Base 类中被标记为 final。

使用场景:

final关键字的使用场景主要包括:

  • 性能优化:防止类的继承或虚函数的重写可以让编译器进行更多的优化,因为编译器知道没有更多的派生类或重写的函数需要考虑。
  • 设计安全:当你的设计不希望或不需要继承或重写时,使用final可以防止其他开发者不小心破坏你的设计意图。

final关键字的引入增强了C++的类型安全性和性能优化能力,同时也提供了更明确的类设计意图表达方式。

Lambda 表达式

C++11 引入了 Lambda 表达式,为 C++ 程序员提供了一种方便的匿名函数对象创建方式。Lambda 表达式广泛用于简化代码,尤其是在需要小段函数逻辑作为参数传递给算法或线程时。

基本语法

Lambda 表达式的基本语法如下

[ capture ] ( parameters ) -> return_type {
    // Function body
}
  • capture:捕获列表,定义了 Lambda 函数体外部变量的访问方式。可以是值捕获、引用捕获或隐式捕获等。
  • parameters:参数列表,与普通函数的参数列表相同。可以为空。
  • return_type:返回类型,可以省略,编译器会自动推导返回类型。
  • Function body:函数体,包含了 Lambda 表达式的逻辑。

示例:

1. 基本Lambda表达式

不带参数,自动推导返回类型:

auto greet = []() { std::cout << "Hello, World!" << std::endl; };
greet();  // 调用Lambda表达式

2. 带参数的Lambda表达式

auto add = [](int x, int y) { return x + y; };
std::cout << "3 + 4 = " << add(3, 4) << std::endl;

3. 指定返回类型的Lambda表达式

auto divide = [](double x, double y) -> double {
    if (y == 0) return 0; // 防止除以0
    return x / y;
};
std::cout << "5.0 / 2.0 = " << divide(5.0, 2.0) << std::endl;

4. 捕获外部变量

值捕获:

int x = 4;
// [x] 以值的方式捕获变量x
auto square = [x]() { return x * x; };
std::cout << "Square of 4 is " << square() << std::endl;

引用捕获:

int total = 0;
std::vector<int> numbers = {1, 2, 3, 4};
std::for_each(numbers.begin(), numbers.end(), [&total](int x) {
    total += x;  // 'total'通过引用捕获,可以修改其值
});
std::cout << "Total: " << total << std::endl;

5. 捕获所有外部变量

使用[=]捕获所有外部变量(通过值),使用[&]捕获所有外部变量(通过引用)。

总结:

Lambda表达式是C++11中引入的强大特性,它提供了一种便捷的方式来定义和使用匿名函数。通过捕获列表,Lambda表达式可以捕获并使用定义它们的作用域中的变量。Lambda表达式广泛应用于标准库算法、异步编程和事件处理等场景。

尾返回类型

C++11引入了尾返回类型(Trailing Return Type)的概念,允许开发者将函数的返回类型声明在函数参数列表之后。这在某些情况下,尤其是当返回类型依赖于函数参数类型的时候,变得非常有用。使用尾返回类型,你可以利用auto关键字和->运算符来指定返回类型。

尾返回类型的基本语法
auto functionName(parameters) -> returnType;

这里,returnType是一个类型表达式,它描述了函数的返回类型,并且它可以使用函数参数。

示例代码:

简单示例:

在最简单的形式中,尾返回类型让函数的声明更清晰:

auto add(int x, int y) -> int {
    return x + y;
}

尽管在这个简单例子中使用尾返回类型可能看起来没有必要,它展示了基本的语法结构。

依赖于模板参数的返回类型:

尾返回类型特别有用于模板编程中,当函数的返回类型依赖于其模板参数时:

template <typename T, typename U>
auto add(T x, U y) -> decltype(x + y) {
    return x + y;
}

在这个例子中,add 函数的返回类型是 T 和 U 类型相加的结果类型。使用 decltype 关键字和尾返回类型,我们可以精确地指定这个返回类型。

使用于lambda表达式:

尾返回类型同样适用于C++11中的 lambda 表达式,允许在更复杂的场景下指定返回类型:

auto getLambda = []() -> std::function<int(int, int)> {
    return [](int x, int y) -> int { return x + y; };
};

这个例子中,getLambda是一个返回类型为std::function<int(int, int)>的 lambda 表达式,它本身返回另一个计算两个整数和的 lambda 表达式。

为什么需要尾返回类型?

在C++11之前,函数的返回类型必须在函数名之前声明,这在大多数情况下工作得很好。然而,对于返回类型依赖于函数参数的情况(特别是模板编程中),这种方法就显得力不从心了。尾返回类型提供了一种灵活的方式来声明这些依赖于参数的返回类型,使得函数签名更加清晰和灵活。

尾返回类型是现代C++(C++11及以后版本)中推荐的一种高级特性,它在编写泛型代码和lambda表达式时特别有用。通过这种方式,C++程序员可以编写出更清晰、更灵活、更安全的代码。

内联命名空间(Inline Namespaces)

C++11引入了内联命名空间(Inline Namespaces)的概念,这是一种特殊的命名空间,其成员在外层命名空间中也可以直接访问,无需通过内联命名空间的名称。这个特性主要用于版本控制和兼容性,允许库开发者在不破坏现有代码基础上引入新版本的API。

语法

使用inline关键字来定义内联命名空间:

namespace outer {
    inline namespace inner {
        // void func() {}
    }
}

在这个示例中,inner是一个内联命名空间,outer是它的外层命名空间。由于inner被声明为内联的,所以outer命名空间中的代码可以直接访问 inner 中的成员,无需显式地通过 inner 命名空间的名称。

代码示例

假设我们有一个库,该库提供了一个函数 foo。随着时间的推移,库的版本更新,我们添加了一个新的实现,但我们想保持对旧版本的兼容。

namespace Library {
    // 旧版本
    namespace Version1 {
        void foo() {
            std::cout << "Version 1 of foo\n";
        }
    }

    // 新版本
    inline namespace Version2 {
        void foo() {
            std::cout << "Version 2 of foo\n";
        }
    }
}

int main() {
    Library::foo(); // 直接访问最新版本
    Library::Version1::foo(); // 显式访问旧版本
    return 0;
}

在这个示例中,Version2 是一个内联命名空间。当我们调用 Library::foo() 时,由于 Version2 是内联的,编译器会直接在 Library 的内联命名空间中查找 foo 函数,因此调用的是 Version2 中的 foo。如果需要显式调用旧版本的 foo 函数,可以通过完整的命名空间路径 Library::Version1::foo() 来实现。

使用场景:

内联命名空间的一个典型应用场景是库版本管理。通过将最新版本的定义放在内联命名空间中,库的用户可以不做任何修改地自动使用最新版本的功能。同时,如果需要引用特定版本的定义,也可以通过指定命名空间的名称来实现。

注意事项:
  • 一个命名空间内只能有一个内联命名空间是活跃的,也就是说,如果有多个内联命名空间,只有最后一个声明为内联的命名空间才会被视为活跃的。
  • 内联命名空间主要用于版本控制和向后兼容性,不推荐在日常编程中过度使用。

内联命名空间提供了一种优雅的方式来处理库和应用程序在不同版本间的平滑过渡和兼容性问题,是 C++11 中对库设计者非常有用的一个特性。

静态断言(static_assert)

C++11 引入了静态断言(static_assert),这是一种在编译时进行断言检查的机制。静态断言让开发者能够在编译期间检查表达式是否为真,如果表达式结果为假,则编译器会产生一个编译错误并显示开发者提供的消息。这个特性特别适合用于检查模板参数、常量表达式、等编译时信息,以确保代码的正确性和类型安全。

语法

static_assert 的基本语法如下:

static_assert(常量表达式, 错误消息);
  • 常量表达式:需要在编译时求值的表达式,结果为布尔值。如果表达式的结果为 false,则编译器会产生一个错误。
  • 错误消息:这是一个字符串字面量,用于在常量表达式的求值结果为 false 时提供错误信息。
示例代码:

检查类型大小 : 使用 static_assert 来确保特定类型的大小符合预期:

static_assert(sizeof(int) == 4, "int类型必须是4字节大小");

这个static_assert会检查 int 类型是否是4字节(32位)大小。如果在某些平台上 int 不是4字节大小,编译器将会报错。

模板参数约束:
在模板编程中,static_assert 可以用来约束模板参数:

template<typename T>
class MyArray {
    /*
    std::is_arithmetic<T>::value 用于在编译时检查一个类型是否为算术类型(即整数类型和浮点类型)。
    如果类型是算术类型,std::is_arithmetic<T>::value会是true,否则是false。
    */
    static_assert(std::is_arithmetic<T>::value, "MyArray只能用于算术类型");
    // 类实现...
};

这个例子中,static_assert用来确保MyArray模板只能用于算术类型(如int、float等)。如果尝试用非算术类型实例化MyArray,编译器将报错。

验证编译时条件:
static_assert 也可以用于验证其他编译时能够确定的条件:

constexpr int getNumber() { return 5; }
static_assert(getNumber() == 5, "函数getNumber必须返回5");

这里,static_assert验证getNumber函数是否总是返回5。由于getNumber被声明为constexpr,它的返回值可以在编译时确定,因此适合用于static_assert。

右值引用

C++11引入了右值引用,这是一个指向临时对象(即右值)的引用,允许开发者安全地从临时对象中移动数据,而不仅仅是复制。这个特性是实现移动语义(Move Semantics)和完美转发(Perfect Forwarding)的基础,对于提高程序性能尤其在涉及大量数据操作时非常关键。

语法:

右值引用使用&&符号来声明。与左值引用(使用&)不同,右值引用绑定到临时对象上。

Type&& name = expression;
  • Type:变量的类型。
  • name:变量的名称。
  • expression:必须是一个右值表达式,通常是一个临时对象或者是通过std::move转换的。
示例

简单的右值引用示例:

int&& rref = 5; // 5 是一个整数字面量,是右值

// 使用 std::move 转换左值为右值引用
int x = 10;
int&& moved_x = std::move(x);

右值引用主要用于实现移动语义和完美转发

移动语义(Move Semantics)

移动语义是 C++11 引入的一项重要特性,它允许资源(如动态分配的内存)在对象之间转移,而不是复制。这一特性显著提高了性能,尤其是在处理大型数据对象时,因为它避免了不必要的复制操作。移动语义主要通过两个新的构造函数实现:移动构造函数和移动赋值操作符,它们都使用了右值引用这一特性。

移动构造函数

移动构造函数允许从一个即将被销毁的对象中“窃取”资源。它的参数是该对象类型的右值引用。

语法

// ClassName 表示类名
ClassName(ClassName&& other) ;
移动赋值操作符

移动赋值操作符允许将一个即将被销毁的对象的资源赋值给另一个对象。它的参数也是该对象类型的右值引用。

语法:

ClassName& operator=(ClassName&& other) ;
示例代码

假设有一个简单的 Buffer 类,它管理一块动态分配的内存:

class Buffer {
public:
    Buffer(size_t size) : size(size), data(new int[size]) {}
    
    // 移动构造函数
    Buffer(Buffer&& other) noexcept : size(other.size), data(other.data) {
        other.data = nullptr; // 防止析构时释放内存
    }
    
    // 移动赋值操作符
    Buffer& operator=(Buffer&& other) noexcept {
        if (this != &other) {
            delete[] data; // 释放原来的资源
            data = other.data; // 窃取资源
            size = other.size;
            other.data = nullptr; // 防止析构时释放内存
        }
        return *this;
    }
    
    ~Buffer() { delete[] data; }
    
private:
    size_t size;
    int* data;
};

在这个例子中,Buffer 类定义了一个移动构造函数和一个移动赋值操作符。当一个 Buffer 对象通过移动构造函数或移动赋值操作符与另一个 Buffer 对象交互时,它实际上是从源对象窃取了资源(这里是指针 data),而不是复制资源。这样可以避免不必要的复制开销,并提高性能。

使用 std::move 触发移动语义:

要触发移动操作,通常需要使用 std::move 函数将对象显式转换为右值引用,如下所示:

Buffer buffer1(1024); // 创建一个Buffer对象
Buffer buffer2(std::move(buffer1)); // 使用移动构造函数

在这个示例中,std::move(buffer1) 将 buffer1 转换为右值引用,从而允许使用移动构造函数来初始化 buffer2。这意味着 buffer1 的资源被转移到了 buffer2,并且 buffer1 不再拥有这些资源。

总结

移动语义通过引入移动构造函数和移动赋值操作符,以及配合右值引用,为 C++ 提供了一种高效的资源管理方式。这改善了 C++ 程序的性能,特别是在涉及到大量数据处理的场景中。通过减少不必要的数据复制,移动语义使得资源的转移变得更加高效和直接。

完美转发(Perfect Forwarding)

C++11 引入的完美转发是一种技术,允许函数将其接收到的参数以完全不变的形式转发给另一个函数。这意味着参数的左值、右值特性和类型都被保持不变。完美转发非常有用,尤其是在模板编程和泛型编程中,它可以帮助我们编写能够接受任意参数并将其正确转发的代码。

使用 std::forward

完美转发通常通过 std::forward 实现,它允许你根据模板参数的类型来保持参数的左值或右值特性。

语法:

template<typename T>
void wrapper(T&& arg) {
    // 使用 std::forward<T> 来完美转发 arg
    callee(std::forward<T>(arg));
}
  • T&& 表示一个通用引用(universal reference),它可以绑定到左值或右值。
  • std::forward<T>(arg) 负责保持 arg 的原始类型,确保参数 arg 的值类别(左值或右值)被保持不变地转发。

示例代码:

假设我们有以下 callee 函数,它有两个重载,分别处理左值和右值:

void callee(const std::string& arg) {
    std::cout << "callee called with a left value: " << arg << std::endl;
}

void callee(std::string&& arg) {
    std::cout << "callee called with a right value: " << arg << std::endl;
}

现在,我们使用完美转发来实现 wrapper 函数,它可以将其接收到的参数以原样转发给 callee:

template<typename T>
void wrapper(T&& arg) {
    callee(std::forward<T>(arg));
}

int main() {
    std::string lv = "left value";
    
    wrapper(lv);  // 调用 callee(const std::string&)
    waapper(std::move(lv)); // 调用 callee(std::string&&)
    wrapper("right value");  // 调用 callee(std::string&&)
    return 0;
}

在 main 函数中,wrapper 首先以一个左值字符串调用,接着使用std::move(lv)调用,然后以一个右值字符串字面量调用。由于使用了 std::forward,wrapper 能够保持参数的原始值类别(左值或右值),因此能够触发 callee 的正确重载。

总结:

完美转发是一个非常有用的技术,它使得模板函数能够接受任意类型的参数,并将这些参数以其原始的左值或右值状态传递给其他函数。这在编写泛型代码或模板库时尤其重要,因为它允许代码以一种类型安全且效率高的方式处理各种调用场景。通过结合使用通用引用(T&&)std::forward,开发者可以编写出既灵活又高效的模板函数。

标准库增强

并发编程

线程(std::thread)

C++11 引入了原生线程支持,通过 <thread> 头文件提供了 std::thread 类,使得创建和管理线程变得直接且易于使用。这是对 C++ 标准库的重要扩展,允许直接在 C++ 代码中实现多线程编程,而不再依赖于操作系统特定的线程库。

基本使用:

要使用 std::thread,你需要创建一个 std::thread 对象并向其传递一个函数,这个函数将在新线程中执行。

示例代码:创建并启动一个线程

#include <iostream>
#include <thread>

// 线程执行的函数
void threadTask() {
    std::cout << "Hello, Thread!" << std::endl;
}

int main() {
    // 创建并启动新线程
    std::thread t(threadTask);
    // 等待线程完成任务
    t.join();
    return 0;
}

传递参数:

如果你需要向线程函数传递参数,可以直接将它们作为 std::thread 构造函数的参数传递。

示例代码:向线程函数传递参数

#include <iostream>
#include <thread>

void printMessage(const std::string& message) {
    std::cout << message << std::endl;
}

int main() {
    std::string message = "Hello from the thread with arguments!";
    // message作为参数传递给线程函数
    std::thread t(printMessage, message);
    t.join();
    return 0;
}

线程管理:

加入线程 : 当你创建一个线程时,你需要决定程序何时等待这个线程完成其工作。调用 std::thread 对象的 join() 方法,会使当前线程(通常是主线程或创建该子线程的线程)暂停执行,直到被 join() 的那个线程完成执行。这保证了两个重要的事项

  • 线程同步:确保所有必要的线程操作在程序继续之前完成。
  • 资源回收:一旦子线程完成执行,系统会回收它使用的所有资源。

使用方法

t.join();

分离线程 : 有时,你可能希望线程“独立”执行,而不是等待它结束。通过调用线程对象的 detach() 方法,你可以实现这一点。分离线程意味着:

  • 线程的自主性:分离的线程会在自己的执行流中独立运行,主线程(或任何其他线程)不会等待它结束。
  • 资源管理:一旦分离的线程完成其任务,它占用的资源将由操作系统自动回收。你不需要(也不能)对其调用 join()。

使用方法

t.detach();

获取线程ID:
每个线程都有一个唯一的 ID,可以通过 get_id() 方法获取。

std::cout << "Thread ID: " << t.get_id() << std::endl;

当前线程的 ID

可以使用 std::this_thread::get_id() 获取当前线程的 ID。

std::cout << "Current thread ID: " << std::this_thread::get_id() << std::endl;

线程休眠:

std::this_thread::sleep_for() 函数可以使当前线程暂停执行指定的时间。

#include <chrono>
// 使当前线程休眠 1 秒
std::this_thread::sleep_for(std::chrono::seconds(1));
多线程同步

C++11 引入了多线程同步的机制,来帮助程序员控制并发执行的线程之间的执行顺序,确保数据的一致性和防止竞态条件。这包括了互斥锁(mutexes)条件变量(condition variables)、以及原子操作(atomic operations)等。下面是这些同步机制的基本介绍和示例。

1. 互斥锁(Mutex)

互斥锁(mutex)是用于管理对共享资源的访问的同步原语。当多个线程尝试同时访问同一个资源时,互斥锁确保每次只有一个线程能够访问该资源,从而防止数据竞争和保证数据的一致性。

C++11 在 <mutex> 头文件中提供了几种类型的互斥锁:

std::mutex

std::mutex 提供了基本的互斥锁功能。

基本语法

std::mutex mtx;
mtx.lock();   // 加锁
// 临界区代码
mtx.unlock(); // 解锁

主要成员函数:

  • lock() : 锁定互斥锁。如果互斥锁已被其他线程锁定,则调用线程将阻塞,直到互斥锁变为可用。
  • unlock() : 解锁互斥锁,使其变为可用状态。
  • try_lock(): 尝试锁定互斥锁而不阻塞。如果互斥锁已经被其他线程锁定,则立即返回 false;如果成功锁定,则返回 true。

示例代码:使用 std::mutex 保护共享数据

#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx; // 创建互斥锁
int shared_data = 0; // 共享数据

void incrementSharedData() {
    mtx.lock(); // 加锁
    ++shared_data;
    std::cout << std::this_thread::get_id() << " incremented shared_data to " << shared_data << std::endl;
    mtx.unlock(); // 解锁
}

int main() {
    std::thread t1(incrementSharedData);
    std::thread t2(incrementSharedData);
    t1.join();
    t2.join();
    return 0;
}

std::recursive_mutex

std::recursive_mutex是可递归的互斥锁,允许同一个线程多次对同一个互斥锁对象加锁。它维护了一个锁计数和拥有线程的标识,当计数降到 0 时锁被释放。

std::recursive_mutex rec_mtx;
rec_mtx.lock();   // 第一次加锁
rec_mtx.lock();   // 第二次加锁,合法
// 临界区代码
rec_mtx.unlock(); // 第一次解锁
rec_mtx.unlock(); // 第二次解锁,锁被完全释放

std::timed_mutex

std::timed_mutex提供了基本的互斥锁功能,并支持尝试加锁一段时间。如果在指定时间内没有获取到锁,操作会失败并返回。

std::timed_mutex tm_mtx;
if (tm_mtx.try_lock_for(std::chrono::seconds(1))) {
    // 临界区代码
    tm_mtx.unlock();
}

std::recursive_timed_mutex

std::recursive_timed_mutex结合了std::recursive_mutex的可递归特性和std::timed_mutex的定时尝试加锁功能。

std::recursive_timed_mutex rt_mtx;
if (rt_mtx.try_lock_for(std::chrono::seconds(1))) {
    // 临界区代码,可以多次加锁
    rt_mtx.unlock();
}

使用注意:

  • 在使用互斥锁时,推荐使用std::lock_guardstd::unique_lock等 RAII(Resource Acquisition Is Initialization)封装,以自动管理锁的加锁和解锁过程,避免因异常而导致的死锁问题。

  • 根据具体需求选择合适的互斥锁类型。比如,如果不需要递归加锁或定时尝试加锁的功能,使用最简单的std::mutex即可。

C++11的互斥锁类型提供了灵活的同步机制,帮助开发者在多线程程序中安全地管理对共享数据的访问。

2. 条件变量(Condition Variable)

C++11中的条件变量也是线程同步的一种机制,用于在某些条件发生变化时通知一个或多个正在等待的线程。条件变量通常与互斥量(mutex)一起使用,以确保线程安全地访问共享数据。条件变量主要通过std::condition_variable类提供。

条件变量常见接口

1. wait

该函数用于等待一个条件成立。它会原子地释放锁并使当前线程挂起,直到被其他线程通过 notify_one 或 notify_all 唤醒。一旦当前线程被唤醒,wait 会再次获取锁。

语法:

cv.wait(unique_lock<std::mutex>& lock);

2. wait_for

等待条件变量被通知一段时间。如果在指定的时间内条件变量没有被通知,则超时并返回。

语法:

cv.wait_for(unique_lock<std::mutex>& lock, duration);

3. wait_until
等待直到某个时间点,如果条件变量在这个时间点之前没有被通知,则超时并返回。

语法:

cv.wait_until(unique_lock<std::mutex>& lock, time_point);

4. notify_one
用于唤醒一个等待(挂起)在条件变量上的线程。如果有多个线程在等待,只有一个会被随机唤醒。

语法:

cv.notify_one();

5. notify_all

该函数用于唤醒所有等待(挂起)在条件变量上的线程。

语法:

cv.notify_all();

示例代码:使用 std::condition_variable 实现线程同步

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>

std::mutex mtx;
std::condition_variable cv;
bool ready = false; // 条件变量关联的条件

void doPrint() {
    // std::unique_lock :锁管理工具,自动管理互斥锁的锁定与解锁。
    std::unique_lock<std::mutex> lck(mtx);
    while (!ready) 
        cv.wait(lck); // 等待条件成立,即:ready 变为true
    std::cout << "Thread " << std::this_thread::get_id() << " is running\n";
}

void go() {
    std::unique_lock<std::mutex> lck(mtx);
    ready = true;
    cv.notify_all(); // 通知所有等待的线程
}

int main() {
    std::thread threads[10];
    for(int i = 0; i < 10; ++i)
        threads[i] = std::thread(doPrint);

    std::cout << "10 threads ready to race...\n";
    go(); // 让所有线程开始执行
    for(auto& th : threads) th.join();
    return 0;
}

3. 原子操作(Atomic)

原子操作是指不可分割的操作,这些操作要么完全执行,要么完全不执行,不会出现部分执行的情况。这对于多线程编程至关重要,因为它们可以用来保护在多线程环境中共享的数据,而无需使用互斥锁。C++11 通过 <atomic> 头文件引入了原子类型和操作。

原子类型:

C++11 提供了一系列原子类型,如 std::atomic_int, std::atomic_long, std::atomic_bool 等,以及一个模板类 std::atomic<T>,允许创建任意类型 T 的原子对象。

基本用法

#include <atomic>
std::atomic<int> count(0);  // 原子整型变量

主要成员函数:

  • store(): 存储(赋值)一个值到原子对象。
  • load(): 从原子对象加载(获取)一个值。
  • exchange(): 原子地替换原子对象的值。
  • compare_exchange_weak() 和 compare_exchange_strong(): 比较原子对象的值,如果与期望值相同,则替换为新值。
  • fetch_add(), fetch_sub(), fetch_or(), fetch_and(), fetch_xor(): 原子地执行加、减、或、与、异或操作。

示例代码:

简单原子操作

#include <iostream>
#include <atomic>
#include <thread>
#include <vector>

std::atomic<int> count(0);  // 原子计数器

void increment() {
    for (int i = 0; i < 10000; ++i) {
        count.fetch_add(1);  // 原子地增加计数器
    }
}
int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 10; ++i) {
        threads.push_back(std::thread(increment));
    }
    for (auto& th : threads) {
        th.join();
    }
    std::cout << "Final count: " << count.load() << std::endl;
    return 0;
}

这个例子创建了 10 个线程,每个线程都对一个原子计数器执行 10000 次增加操作。因为 count 是原子类型,所以即使多个线程同时修改它,最终的结果也是准确的,不会发生数据竞争。

比较并交换

#include <atomic>
#include <iostream>

std::atomic<int> value(10);

void check_and_increase() {
    int oldValue = value.load();

    while (!value.compare_exchange_weak(oldValue, oldValue + 1)) {
        // 循环直到成功更新
    }
}

int main() {
    check_and_increase();
    std::cout << "Value: " << value << std::endl;
    return 0;
}

这个例子展示了如何使用 compare_exchange_weak 来原子地更新一个值,只有当当前值等于期望的旧值时,才将其更新为新值,否则重试操作。

value.compare_exchange_weak(oldValue, oldValue + 1)
这是一个原子的比较并交换操作,它尝试将 value 的当前值与 oldValue 比较:

  • 如果相等(说明期间 value 的值未被其他线程改变),则将 value 更新为 oldValue + 1。

  • 如果不相等(说明期间 value 的值被其他线程改变了),操作失败,oldValue 被更新为 value 的新值,然后循环再尝试。

    compare_exchange_weak方法返回 true 表示成功更新,false 表示更新失败。

通过使用原子操作,可以在多线程环境中安全地操作共享数据,而无需引入可能导致性能下降的互斥锁。

std::once_flag 和 std::call_once

在C++11中,std::once_flagstd::call_once共同提供了一种线程安全的方式来执行一次性初始化。这种机制尤其用于延迟初始化和单例模式中,确保某个函数或某段初始化代码在多线程环境下仅被执行一次,无论有多少线程尝试。

std::once_flag:

std::once_flag是一个不能被复制的类型,用来与 std::call_once 一起标记某个函数或初始化代码是否已经被执行。每个std::once_flag对象通常与一次性初始化任务相关联。

std::call_once:

std::call_once函数接受一个 std::once_flag 对象和一个要执行的函数,保证无论有多少线程尝试调用std::call_once,该函数仅被执行一次。

语法:

void call_once(std::once_flag& flag, Callable&& func, Args&&... args);
  • flag:一个std::once_flag对象,标记func是否被执行过。
  • func:要执行的函数或可调用对象。
  • args:传递给func的参数列表。

示例代码:

下面是一个使用std::once_flagstd::call_once实现的线程安全的延迟初始化示例:

#include <iostream>
#include <thread>
#include <mutex>

std::once_flag flag; // 用于标记延迟初始化是否执行

void do_once() {
    std::cout << "Called once" << std::endl;
}

void do_work() {
    // 尝试执行do_once,由于使用了std::once_flag,
    // 即使有多个线程调用do_work,do_once也只会执行一次
    std::call_once(flag, do_once);
}

int main() {
    std::thread t1(do_work);
    std::thread t2(do_work);
    std::thread t3(do_work);
    t1.join();
    t2.join();
    t3.join();
    return 0;
}

在这个示例中,do_work 函数尝试执行 do_once 函数,但由于 do_once 与一个 std::once_flag 对象 flag 相关联,并通过 std::call_once 来调用,因此无论有多少线程调用 do_work,do_once 只会被执行一次。这种模式对于资源的延迟初始化非常有用,尤其是在资源初始化开销较大或只有在真正需要时才应被初始化的情况下。

std::future和std::promise

在C++11中,std::futurestd::promise是处理异步操作的两个重要类。它们协同工作,提供了一种从异步操作中获取结果的机制。

std::promise:

std::promise对象可以存储某一类型的值,该值可以在将来某个时刻被获取。通过 std::promise,你可以在一个线程中设置一个值,然后在另一个线程中通过与之关联的 std::future 对象来获取这个值。

语法:

//  T:存储的值的类型。
std::promise<T> promise;

主要接口:

  • set_value(const T&):设置一个值,该值可以通过关联的 std::future 对象来获取。
  • get_future():返回一个std::future<T>对象,用于获取通过 set_value 设置的值。

std::future:

std::future 对象持有一个异步操作的结果。它从关联的 std::promise 对象获取值或异常。

语法:

//  T:存储的值的类型。
std::future<T> future = promise.get_future();

value = future.get();

主要接口:

  • get():获取由 std::promise 设置的值。调用get()会阻塞,直到值被设置。
  • wait():等待异步操作完成,但不获取结果。

示例代码:

下面是一个使用std::promisestd::future来传递异步操作结果的简单示例:

#include <iostream>
#include <future>
#include <thread>

// 异步任务:计算一个数的平方
void compute(std::promise<int>&& prom, int x) {
    int result = x * x;
    prom.set_value(result);  // 在子线程中设置结果
}

int main() {
    std::promise<int> prom;  // 创建一个std::promise<int>对象
    std::future<int> fut = prom.get_future();  // 获取与promise关联的future

    std::thread th(compute, std::move(prom), 10);  // 创建一个线程执行异步任务

    // 获取异步操作的结果
    int value = fut.get();  // 阻塞,直到异步操作完成并设置了结果
    std::cout << "The square of 10 is " << value << std::endl;

    th.join();  // 等待子线程完成
    return 0;
}

在这个示例中,compute 函数在一个新线程中执行,并计算一个数的平方,然后通过传入的 std::promise 对象设置结果。主线程通过与 promise 对象关联的 future 对象等待并获取这个结果。

总结:

std::promisestd::future为 C++11 引入的异步编程提供了强大的支持,允许在不同线程之间传递数据和状态信息。它们使得编写并发程序变得更加简洁和安全,尤其是在需要从异步操作中获取结果时。

打包任务(packaged_task)

C++11引入了std::packaged_task,它是一个模板类,用于封装任何可以调用的目标(比如函数、lambda表达式、绑定表达式或其他函数对象),以便异步调用。std::packaged_task将调用的结果存储为一个std::future对象,这样就可以在未来某个时刻获取该结果。这使得std::packaged_task成为实现任务异步执行并获取其结果的强大工具。

语法:

std::packaged_task<ReturnType(ArgsTypes...)>
  • ReturnType:调用的返回类型。
  • ArgsTypes:调用的参数类型列表。

主要接口:

  • operator()(Args…):执行封装的任务。
  • get_future():返回一个std::future<ReturnType>对象,用于获取任务的结果。

示例代码:

假设我们有一个函数,计算两个整数的和,并希望异步执行这个函数并获取结果:

#include <iostream>
#include <future>
#include <thread>

// 一个简单的函数,计算两个整数的和
int sum(int a, int b) {
    return a + b;
}

int main() {
    // 封装sum函数到packaged_task中
    std::packaged_task<int(int, int)> task(sum);
    // 获取与packaged_task关联的future对象,以便之后获取结果
    std::future<int> result = task.get_future();
    // 在一个新线程中执行任务
    std::thread th(std::move(task), 2, 3); // 传递参数2和3给sum函数
    // 等待任务完成并获取结果
    std::cout << "The sum is: " << result.get() << std::endl; // 输出:The sum is: 5 
    // 等待线程完成
    th.join();
    return 0;
}

在这个例子中,我们创建了一个 std::packaged_task 对象 task,它封装了 sum 函数。通过调用 task.get_future(),我们获得了一个std::future对象,它将在task执行完成后包含 sum 函数的返回值。然后,我们创建了一个线程th,并将task(通过std::move移动)和sum函数需要的参数传递给这个线程,以异步执行task。通过result.get(),我们阻塞等待任务完成,并获取sum函数的结果。

总结:

std::packaged_task 是C++11中处理异步任务的强大工具。它允许程序员封装任何可调用的目标,以便异步执行,同时通过std::future提供了一种机制来获取异步操作的结果。这种模式非常适用于需要将计算密集型任务移出主线程以避免阻塞主执行流的场景。

异步(Async)

C++11 通过引入 std::async 函数提供了一种更简洁、更高层的方式来创建异步任务。std::async 会启动一个异步任务,该任务可以在新线程中执行或延迟执行,具体取决于给定的策略参数,并返回一个 std::future 对象,用于访问异步操作的结果。

函数声明:

普通版本的函数声明

template <class F, class... Args>
std::future<typename std::result_of<F(Args...)>::type> async(F&& f, Args&&... args);

指定启动策略的重载版本的函数声明

template <class F, class... Args>
std::future<typename std::result_of<F(Args...)>::type> async(std::launch policy, F&& f, Args&&... args);
  • policy:指定执行策略,可以是 std::launch::async(强制在新线程中执行任务)、std::launch::deferred(延迟执行任务,直到调用 std::future::get 或 std::future::wait)、或者这两者的位或组合。
  • f:要异步执行的函数或可调用对象。
  • args:传递给 f 的参数。

返回值是std::future 类型的对象,它持有异步任务的结果。

示例代码:

异步执行任务

#include <iostream>
#include <future>

int compute(int x, int y) {
    return x + y;
}

int main() {
    // 启动异步任务,自动选择执行策略
    std::future<int> result = std::async(compute, 2, 3);

    // 做一些其他的事情...

    // 获取异步任务的结果,如果任务尚未完成,则这里会阻塞等待
    std::cout << "The result is: " << result.get() << std::endl;
    return 0;
}

使用执行策略

#include <iostream>
#include <future>
#include <thread>

void task() {
    std::cout << "Task runs in a thread." << std::endl;
}

int main() {
    // 强制在新线程中启动异步任务
    std::future<void> f1 = std::async(std::launch::async, task);

    // 延迟执行任务,直到调用 get() 或 wait()
    std::future<void> f2 = std::async(std::launch::deferred, task);

    // 此时,第一个任务已经在新线程中执行
    
    // 第二个任务将在调用 get() 或 wait() 时在当前线程(主线程)中执行
    
    // 等待第一个任务完成
    f1.get();
    
    // 启动第二个任务,并等待它完成
    f2.get();
    return 0;
}

总结:

std::async 提供了一种便捷的方式来执行异步任务,无需直接处理线程的创建和管理。通过返回一个 std::future 对象,它允许以线程安全的方式访问异步任务的结果。使用 std::async 可以使并发编程变得更简单、更直观。

线程局部存储(Thread Local Storage,TLS)

C++11 引入了线程局部存储(Thread Local Storage,TLS),允许数据在每个线程中都有自己的独立实例。这意味着每个线程都有自己的数据副本,修改一个线程中的数据不会影响到其他线程中的相同数据。这是通过thread_local关键字来实现的,它指定了变量的存储期为线程的生命周期。

语法:

thread_local Type variableName = initialValue;
  • thread_local:关键字,用于声明线程局部存储变量。
  • Type:变量的类型。
  • variableName:变量的名称。
  • initialValue(可选):变量的初始值。

示例代码

以下是一个展示如何使用thread_local关键字的简单示例:

#include <iostream>
#include <thread>
#include <vector>

// 定义一个线程局部变量
thread_local int counter = 0;

void incrementCounter(const std::string& threadName) {
    ++counter; // 访问和修改线程局部变量
    std::cout << "Counter in " << threadName << ": " << counter << std::endl;
}

int main() {
    std::thread t1([&]() {
        incrementCounter("t1");
        incrementCounter("t1");
    });

    std::thread t2([&]() {
        incrementCounter("t2");
        incrementCounter("t2");
    });

    t1.join();
    t2.join();

    return 0;
}

在这个示例中,counter是一个线程局部存储变量,因为它前面有thread_local关键字。这意味着每个线程调用 incrementCounter 函数时,都会修改它自己的 counter 副本,而不是共享一个全局 counter。因此,尽管 incrementCounter 函数在两个不同的线程中都被调用了两次,每个线程的输出将独立地显示 counter 从1递增到2,证明每个线程都有自己的 counter 副本。

总结:

使用 thread_local 声明的变量为每个线程提供了一个独立的变量副本,这有助于减少对全局状态的依赖,从而使代码在并发环境中更安全、更容易理解。线程局部存储特别适用于保持线程的状态或避免不必要的锁争用,提高程序的效率和性能。

函数包装器(function wrapper)

在C++11中,std::function是一个函数包装器(function wrapper),它提供了一种通用、类型安全的方式来存储和调用任何可调用对象,包括普通函数、Lambda 表达式、函数指针、以及具有 operator() 成员函数的对象(如函数对象或类实例)。

语法

std::function 的语法如下:

std::function<ReturnType(ArgumentTypes...)>

其中 ReturnType 是可调用对象返回的类型,ArgumentTypes... 是可调用对象接受的参数类型列表。

示例代码:

1. 存储和调用普通函数

#include <iostream>
#include <functional>

void print(int x) {
    std::cout << x << std::endl;
}

int main() {
    std::function<void(int)> func = print;
    func(10);  // 输出:10
    return 0;
}

2. 存储和调用Lambda表达式

#include <functional>
#include <iostream>

int main() {
    std::function<int(int, int)> add = [](int a, int b) { return a + b; };
    std::cout << "3 + 4 = " << add(3, 4) << std::endl; // 输出:3 + 4 = 7
    return 0;
}

3. 存储和调用成员函数

#include <functional>
#include <iostream>

class MyClass {
public:
    int triple(int x) {
        return 3 * x;
    }
};

int main() {
    MyClass obj;
    std::function<int(MyClass&, int)> func = &MyClass::triple;
    
    std::cout << "3 tripled is " << func(obj, 3) << std::endl; // 输出:3 tripled is 9
    return 0;
}

4. 存储和调用函数对象

#include <functional>
#include <iostream>

struct Adder {
    int operator()(int a, int b) {
        return a + b;
    }
};

int main() {
    std::function<int(int, int)> func = Adder();
    std::cout << "1 + 2 = " << func(1, 2) << std::endl; // 输出:1 + 2 = 3
    return 0;
}
总结

std::function 是 C++11 提供的一个非常灵活的机制,它使得函数的存储、传递和调用变得非常简单和统一。无论是普通函数、成员函数、Lambda 表达式还是函数对象,都可以用 std::function 来处理。这种统一的接口使得编写接受函数作为参数的泛型代码变得更加容易和直观。

绑定器(std::bind)

C++11标准引入了std::bind,这是一个非常有用的函数适配器,它位于<functional>头文件中。std::bind可以被用来将一个函数或可调用对象(如函数指针、成员函数指针、Lambda表达式、函数对象等)与其参数绑定,生成一个新的可调用对象。这意味着你可以预设某些参数的值,创建一个新的函数版本,这个新版本只需要剩余的参数即可被调用。

基本用法
auto newCallable = std::bind(callable, arg1, arg2, ..., argN);
  • callable:原始的可调用对象,可以是函数指针、成员函数指针、Lambda 表达式或其他函数对象。
  • arg1, arg2, …, argN:要绑定的参数列表,可以是具体的值或引用,也可以是 std::placeholders::_1, std::placeholders::_2, … 来占位,这表示该位置的参数将在新生成的可调用对象被调用时指定。

示例:

1. 绑定普通函数

#include <iostream>
#include <functional>

void print(int n1, int n2, const std::string& str) {
    std::cout << n1 << ", " << n2 << ", " << str << std::endl;
}

int main() {
    auto f = std::bind(print, 1, 2, "Hello");
    f(); // 输出:1, 2, Hello
    return 0;
}

2. 使用占位符

使用 std::placeholders 中的占位符,可以在绑定时留下未指定的参数,这些参数需要在新可调用对象被调用时提供:

#include <iostream>
#include <functional>

void print(int n1, int n2, const std::string& str) {
    std::cout << n1 << ", " << n2 << ", " << str << std::endl;
}

int main() {
    using namespace std::placeholders; // 对于 _1, _2, _3...
    auto f = std::bind(print, _2, _1, "Bound");
    f(3, 5); // 输出:5, 3, Bound
    return 0;
}

3. 绑定类的成员函数

对于类的成员函数,std::bind 也能够被用来绑定,但需要提供成员函数的地址作为第一个参数,而第二个参数是要绑定的对象的指针或引用,之后的参数则是成员函数的参数,这些参数可以是具体的值,也可以是占位符。

#include <iostream>
#include <functional>

class MyClass {
public:
    void memberFunc(int x) {
        std::cout << "Member function called with " << x << std::endl;
    }
};

int main() {
    MyClass obj;
    auto f = std::bind(&MyClass::memberFunc, &obj, 100);
    f(); // 输出:Member function called with 100
    return 0;
}
总结:

std::bind 是 C++11 引入的一个功能强大的工具,它使得函数调用更加灵活,允许预先绑定参数,创建新的可调用对象。但在 C++11 以后,Lambda 表达式因其更简洁的语法和更好的性能,通常被推荐为更好的替代方案。

智能指针

C++11 引入了几种智能指针类型,这些智能指针主要存在于 <memory> 头文件中,它们自动管理内存,帮助避免内存泄露,使得资源管理更加安全和容易。智能指针的类型包括 std::unique_ptrstd::shared_ptrstd::weak_ptr

std::unique_ptr

std::unique_ptr是一种独占所有权的智能指针,确保同一时刻只有一个智能指针实例可以指向一个给定的对象。当std::unique_ptr被销毁时,它所指向的对象也会被销毁。

语法

创建 std::unique_ptr 的基本语法如下:

std::unique_ptr<Type> ptr(new Type(arguments));

主要特性:

  • 独占所有权:一个 std::unique_ptr 同时只能拥有一个对象的所有权。
  • 自动资源管理:std::unique_ptr 负责自动释放其所拥有的对象。
  • 不可复制:为保证资源独占性,std::unique_ptr 不能被复制,但可以被移动,从而转移资源所有权。

示例代码:

创建和使用 std::unique_ptr

#include <memory>
#include <iostream>

class MyClass {
public:
    MyClass() { std::cout << "MyClass created\n"; }
    ~MyClass() { std::cout << "MyClass destroyed\n"; }
    void doSomething() { std::cout << "Doing something\n"; }
};

int main() {
    std::unique_ptr<MyClass> myPtr(new MyClass());
    myPtr->doSomething();
    // 当 myPtr 离开作用域时,MyClass 的实例会自动被销毁
}

在这个例子中,我们通过 new MyClass() 显式地创建了一个 MyClass 的实例,并将其传递给 std::unique_ptr<MyClass> 的构造函数来初始化 myPtr。

转移 std::unique_ptr 的所有权:

std::unique_ptr<MyClass> ptr1(new MyClass());
std::unique_ptr<MyClass> ptr2 = std::move(ptr1);
// 现在 ptr2 拥有 MyClass 实例的所有权,而 ptr1 为空(nullptr)

在这个例子中,使用 std::move 将 ptr1 的所有权转移给 ptr2。之后,ptr1 变为 nullptr。

C++14 开始,推荐使用 std::make_unique 函数来创建 std::unique_ptr,因为这种方式更安全,可以防止潜在的内存泄漏:

auto ptr = std::make_unique<Type>(arguments);
std::shared_ptr

std::shared_ptr 是一种引用计数的智能指针,也称共享型智能指针,它允许多个 std::shared_ptr 实例共享同一个对象的所有权。当最后一个拥有对象的 std::shared_ptr 被销毁或重置时,对象会被自动删除。

语法

使用构造函数创建 std::shared_ptr

std::shared_ptr<Type> ptr(new Type(args...));

也可以使用 std::make_shared 模板函数来创建std::shared_ptrstd::make_shared函数在C++11 引入的。

auto ptr = std::make_shared<Type>(args...);
  • Type:要创建的对象的类型。
  • args:传递给对象构造函数的参数列表。

代码示例:

#include <iostream>
#include <memory>

class MyClass {
public:
    MyClass() { std::cout << "MyClass created\n"; }
    ~MyClass() { std::cout << "MyClass destroyed\n"; }
};

int main() {
    // std::shared_ptr<MyClass> sharedPtr1(new MyClass());
    std::shared_ptr<MyClass> sharedPtr1 = std::make_shared<MyClass>();
    {
        std::shared_ptr<MyClass> sharedPtr2 = sharedPtr1; // 共享所有权
        std::cout << "Inside block\n";
    } // sharedPtr2 被销毁,对象不会被删除,因为 sharedPtr1 仍然存在
    std::cout << "Outside block\n";
} // sharedPtr1 被销毁,对象现在被删除
std::weak_ptr

std::weak_ptr是一种非拥有(弱)引用计数的智能指针,它指向由某个 std::shared_ptr 所管理的对象。它不会增加对象的引用计数,这样就避免了潜在的循环引用问题。

语法

你可以通过从一个 std::shared_ptr 或另一个 std::weak_ptr 创建一个 std::weak_ptr:

std::weak_ptr<Type> weakPtr(sharedPtr);

示例代码

#include <iostream>
#include <memory>

class B; // 前向声明

class A {
public:
    std::shared_ptr<B> bPtr;
    ~A() { std::cout << "A destroyed\n"; }
};

class B {
public:
    std::weak_ptr<A> aPtr; // 使用 weak_ptr 解决循环引用
    ~B() { std::cout << "B destroyed\n"; }
};

int main() {
    std::shared_ptr<A> a(new A());
    std::shared_ptr<B> b(new B());
    a->bPtr = b;
    b->aPtr = a; // B 持有 A 的弱引用,不增加引用计数
} // a 和 b 能够正确被销毁,避免了循环引用导致的内存泄露

在这个示例中,A 和 B 相互引用。如果它们都使用 std::shared_ptr 进行引用,则会创建循环引用,导致对象无法被正确销毁。通过让 B 中的 A 引用成为 std::weak_ptr,我们打破了循环引用,使对象能够在不再被需要时正确地被销毁。

总结:

std::shared_ptr 和 std::weak_ptr 提供了强大的内存管理功能,帮助避免了内存泄露和循环引用问题。std::shared_ptr 通过引用计数机制管理对象生命周期,而 std::weak_ptr 允许对这些对象进行弱引用,这对于实现如缓存、观察者模式等功能非常有用。

异常处理

noexcept 关键字

noexcept 是 C++11 引入的一个关键字,用于指定函数不会抛出异常。它有两种主要用法:一种是作为异常规范,用来标明函数不会抛出任何异常;另一种是作为运算符,用来检查表达式是否可能抛出异常。

作为异常规范

noexcept 用作异常规范时,它直接跟在函数声明的参数列表之后。这表明函数保证不抛出任何异常。如果函数违反了这一保证,即它抛出了异常,程序将调用 std::terminate,通常会导致程序终止。

语法:

void function() noexcept;

示例:

void noThrow() noexcept {
    // 这个函数保证不会抛出异常
}

int main() {
    try {
        noThrow(); // 安全调用,不会抛出异常
    } catch (...) {
        // 这里不会被执行
    }
    return 0;
}

作为运算符

noexcept 运算符用来检查一个表达式是否保证不抛出异常。它的结果是一个编译时的布尔值,如果表达式保证不抛出异常,结果为 true,否则为 false

语法

noexcept(expression)

示例:

void mayThrow() {
    throw std::runtime_error("Error");
}

void noThrow() noexcept {
    // 不会抛出异常
}

int main() {
    std::cout << std::boolalpha;
    std::cout << "mayThrow() noexcept? " << noexcept(mayThrow()) << std::endl; // 输出: false
    std::cout << "noThrow() noexcept? " << noexcept(noThrow()) << std::endl;  // 输出: true
    return 0;
}

noexcept 规范和 noexcept 运算符的区别:

  • noexcept 规范:用来标明一个函数不会抛出任何异常。如果函数声明为 noexcept 但抛出了异常,程序将调用 std::terminate。
  • noexcept 运算符:用来检查一个表达式是否保证不抛出异常。这对于模板编程和泛型编程中根据是否可能抛出异常来进行不同的代码路径优化特别有用。

使用 noexcept 的优势:

  • 性能优化:编译器可以对标记为 noexcept 的函数进行更多优化,因为它知道这些函数不会抛出异常。
  • 异常安全保证:通过明确指出哪些函数是不会抛出异常的,可以帮助编写更清晰、更健壮的代码。

总之,noexcept是 C++11 引入的一个重要特性,它提高了异常安全性,并且在编写需要异常保证的函数时提供了更多的灵活性。

异常传递工具

在 C++11 中,引入了一组异常传递工具,允许在程序的不同部分之间传递异常信息。这些工具主要包括 std::exception_ptrstd::current_exceptionstd::rethrow_exception,它们定义在 <exception> 头文件中。这些机制特别适用于多线程编程,其中异常可能在一个线程中抛出并需要在另一个线程中被捕获和处理。

std::exception_ptr

std::exception_ptr 是一个智能指针,用于存储和传递异常对象的信息。它可以捕获任何抛出的异常,并允许在稍后的时间点重新抛出该异常,无论异常的类型如何。

std::current_exception

std::current_exception 用于捕获当前抛出的异常,并返回一个 std::exception_ptr,指向该异常对象的拷贝。如果当前没有异常被抛出,它返回一个空的 std::exception_ptr。

std::rethrow_exception

std::rethrow_exception 接受一个 std::exception_ptr 作为参数,并重新抛出由该指针所指向的异常。这允许在异常被捕获后的任何时间点重新抛出相同的异常。

示例代码

下面的代码演示了如何使用这些异常传递工具来捕获、存储、传递和重新抛出异常:

#include <iostream>
#include <stdexcept>
#include <exception>
#include <string>

void processException(std::exception_ptr eptr) {
    try {
        if (eptr) {
            std::rethrow_exception(eptr); // 重新抛出异常
        }
    } catch (const std::exception& e) {
        std::cout << "Handled exception: " << e.what() << std::endl;
    }
}

int main() {
    std::exception_ptr eptr;
    
    try {
        throw std::runtime_error("A runtime error occurred");
    } catch (...) {
        eptr = std::current_exception(); // 捕获并存储当前异常
    }
    processException(eptr); // 处理存储的异常
    return 0;
}

在这个示例中

  • 首先,在 main 函数中,一个 std::runtime_error 异常被抛出。
  • 接着使用 catch (…) 捕获这个异常,并通过调用 std::current_exception 将其存储在 std::exception_ptr 中。
  • 然后,将这个 std::exception_ptr 传递给 processException 函数。
  • 在 processException 函数中,使用 std::rethrow_exception 重新抛出异常,然后在另一个 catch 块中捕获并处理它。

重新抛出异常而不是直接处理,主要是为了:

  • 保持灵活性:可以在更合适的地方或时间处理异常。
  • 保存信息:保留完整的异常信息,便于后续调试和诊断。
  • 统一处理:方便在程序的一个集中地点处理所有异常。

这套异常传递工具为异常的传递和处理提供了极大的灵活性,尤其是在复杂的程序结构或多线程环境中。

总结:

本篇文章旨在为热衷于掌握 C++11 新特性的朋友们提供一份实用的学习指南。通过介绍各项新特性并辅以实际代码示例,希望初学者能够不仅理解这些特性背后的概念,还能学会如何在实际项目中应用它们

C++11 的新特性覆盖了语言核心和标准库的方方面面,从简化代码书写、提高性能,到增强代码的安全性和可读性,接下来我们简单来回顾下上面所讲的。

  • 自动类型推断 (auto 和 decltype) 使得变量声明更加简洁,让编译器为我们做更多的工作。
  • 基于范围的 for 循环 让遍历容器和序列变得更加直观。
  • 统一的初始化方式 和 初始器列表 为各种对象和容器的初始化提供了一致的语法。
  • 智能指针(如 std::unique_ptr, std::shared_ptr 和 std::weak_ptr)管理动态分配的内存,使得资源管理更加安全和方便。
  • 并发编程 特性(包括 std::thread, std::async 等)允许我们更好地利用现代多核处理器的计算能力。
  • 异常处理 得到增强,noexcept 关键字和新的异常传递工具让异常的处理和传递更加灵活和安全。
  • Lambda 表达式 和 函数对象 让编写匿名函数变得简单,为 STL 算法等的使用提供了巨大的便利。

最后:

如果你对 C/C++/Go/Java 语言学习 + 计算机基础 + Linux 编程(系统编程和网络编程) + 容器技术(docker、k8s) 等内容感兴趣,不妨关注我的公众号—「跟着小康学编程」。这里会定时更新相关的技术文章,文章通俗易懂,感兴趣的读者可以关注一下,具体可访问:关注小康微信公众号

另外大家在阅读文章的时候,如果觉得有问题的或者有不理解的知识点,欢迎大家评论区询问,我看到就会回复。大家也可以加我的微信:jkfwdkf,备注「加群」,有任何不理解得都可以咨询。

反正收藏了你也不看,点个赞意思一下得了.

  • 14
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值