BLOG

Record, summarize, and improve.

C++基础

关键字

alignas & alignof

alignas 用于指定一个类型或变量的对齐要求。你可以使用它来强制某个对象或类型具有特定的对齐方式。

alignof 用于查询某个类型的对齐要求。它返回一个 std::size_t 类型的值,表示该类型的对齐值。

你可以在类(结构或联合)或者单个成员上使用alignas。 遇到多个alignas说明符时,编译器将选择最严格的说明符(即具有最大值的说明符)。

#include <iostream>

struct alignas(16) Bar
{
    int i;                  // 4 bytes
    int n;                  // 4 bytes
    alignas(4) char arr[3];
    short s;                // 2 bytes
};

int main()
{
    std::cout << alignof(Bar) << std::endl; // output: 16
}

auto

auto 用于自动推导变量的类型。它可以让编译器根据变量的初始化值推断出其数据类型,从而简化代码并提高可读性。以下是关于 auto 关键字的用法和注意事项的总结:

用法:

  1. auto 用于声明变量时,自动推导其类型。
  2. 语法:auto 变量名 = 表达式;

注意事项:

  1. auto 关键字只能用于初始化值明确的情况,因为编译器需要根据初始化值推断变量类型。
  2. auto 推导的变量类型会与初始化表达式的类型相同,包括顶层 const、引用和指针等。
  3. auto 不能用于函数参数、函数返回值、类成员变量等只能用于局部变量。
  4. 对于复杂的表达式,可能会导致类型推导不准确,因此在这些情况下可能需要显式指定变量类型。
  5. auto 可以与范围循环 (for-range loop) 一起使用,for (auto xxx) 值拷贝, for (auto& xxx) 引用元素

示例代码:

int main() {
    int intValue = 5;
    auto autoValue = intValue;         // 类型推导为 int

    const double doubleValue = 3.14;
    auto autoConstValue = doubleValue; // 类型推导为 const double

    std::vector<int> intVector = {1, 2, 3};
    for (auto it = intVector.begin(); it != intVector.end(); ++it) {
        // 使用 auto 在范围循环中简化代码
        std::cout << *it << " ";
    }

    return 0;
}

总之,auto 关键字可以让编译器根据变量的初始化值自动推断其类型,从而简化代码书写。它在局部变量的声明中非常有用,但需要注意初始化值明确且类型推导准确。

auto 推导的变量类型与其对应的初始化表达式类型保持一致,包括了顶层 const、引用和指针等特性。这有助于避免类型不匹配引发的错误。

int main() {
    int x = 5;
    const int y = 10;
    int& ref = x;
    const int* ptr = &y;

    auto a = x;   // 推导为 int
    auto b = y;   // 推导为 const int
    auto c = ref; // 推导为 int(引用的目标类型)
    auto d = ptr; // 推导为 const int*(指针的类型)

    return 0;
}

decltype

decltype 用于获取表达式的类型,而不需要实际执行该表达式。它可以在编译时获取表达式的类型,并用于声明变量、定义函数返回类型、模板类型推导等。以下是关于 decltype 关键字的用法和注意事项的总结:

用法:

  1. decltype 用于获取表达式的类型。
  2. 语法:decltype(表达式),返回表达式的类型。

注意事项:

  1. decltype 获取的是表达式的类型,而不会对表达式进行求值。
  2. 当表达式是一个变量、函数调用、成员访问等时,decltype 会返回对应表达式的类型。
  3. 如果表达式是左值,decltype 返回的类型也是左值引用类型。如果不希望得到引用类型,可以使用 std::remove_reference 进行修饰。
  4. decltype 可以在不执行表达式的情况下获得类型,因此适用于需要在编译时确定类型的场景
  5. 在使用 decltype 时,要注意表达式是否合法,因为它需要在编译时就能确定表达式的类型。

示例代码:

int main() {
    int x = 5;
    const double y = 3.14;

    decltype(x) a = 10;    // 推导为 int
    decltype(y) b = 2.0; // 推导为 const double
    decltype(x + y) c = x + y; // 推导为 double

    return 0;
}

总之,decltype 关键字用于在编译时获取表达式的类型,而不需要实际执行表达式。它在需要在编译时确定类型的场景下非常有用,可以避免手动指定类型而提高代码的灵活性和可维护性。

decltype 类型说明符生成指定表达式的类型。 decltype 类型说明符与 auto 关键字一起,主要对编写模板库的开发人员有用。 使用decltype声明其返回类型的函数模板取决于其模板参数的类型。 或者,使用decltype 声明一个函数模板,该模板包装对另一个函数的调用,然后返回包装函数的返回类型。

int var;
const int&& fx();
struct A { double x; }
const A* a = new A();

接下来,检查由下表中四个 decltype 语句返回的类型。

语句 类型 说明
decltype(var); int 变量 var 的类型。
decltype(fx()); const int&& 对 const int 的 rvalue 引用
decltype(a->x); double 成员访问的类型。
decltype((a->x)); const double& 内部括号导致语句作为表达式而不是成员访问计算。 由于 a 声明为 const 指针,因此类型是对 const double 的引用。

typeid

typeid 用于获取表达式的类型信息。它可以用于运行时获取表达式的类型,以及进行类型比较等操作。以下是关于 typeid 关键字的用法和注意事项的总结:

用法:

  1. typeid 用于获取表达式的类型信息。
  2. 语法:typeid(表达式),返回一个 std::type_info 对象,该对象包含有关类型的信息。

注意事项:

  1. typeid 关键字的返回类型是 std::type_info 类型,该类型定义在 <typeinfo> 头文件中。
  2. 使用 typeid 获取类型信息通常用于运行时进行类型比较或动态决策。
  3. typeid 通常用于多态类层次结构中,以判断对象的实际类型。
  4. typeid 可以用于比较两个对象是否具有相同的类型,但不适用于比较继承关系。
  5. typeid 对于内置类型、自定义类型、多态类型等都适用。

示例代码:

#include <iostream>
#include <typeinfo>

class Base {
public:
    virtual ~Base() {}
};

class Derived : public Base {
};

int main() {
    int intValue = 5;
    double doubleValue = 3.14;
    Base* basePtr = new Derived;

    const std::type_info& intTypeInfo = typeid(intValue);
    const std::type_info& doubleTypeInfo = typeid(doubleValue);
    const std::type_info& basePtrTypeInfo = typeid(basePtr);

    std::cout << "int type: "     << intTypeInfo.name()     << std::endl;
    std::cout << "double type: "  << doubleTypeInfo.name()  << std::endl;
    std::cout << "basePtr type: " << basePtrTypeInfo.name() << std::endl;

    delete basePtr;

    return 0;
}

总之,typeid 关键字用于在运行时获取表达式的类型信息,适用于多态类层次结构中的类型判断、类型比较等场景。通过获取 std::type_info 对象,可以获得关于类型的名称等信息。

decltype、typeid、auto异同

decltype

  • decltype 用于在编译时获取表达式的类型,而不需要实际执行表达式。
  • 主要用途是在模板编程、泛型代码中,以及对变量类型进行推导,例如函数返回类型、变量类型等。
  • decltype 不会进行实际的运行时操作,它只根据表达式的类型推导出类型信息。
  • 适用于静态类型检查、泛型编程和模板元编程。

typeid

  • typeid 用于在运行时获取表达式的类型信息,通常用于类型比较和多态类层次结构中。
  • 主要用途是在运行时判断对象的实际类型,用于类型安全的运行时决策
  • typeid 返回的是一个 std::type_info 对象,可以用于获取类型的名称等信息。
  • 适用于多态类层次结构、类型比较和动态类型判断。

auto

  • auto 用于自动推导变量的类型,根据初始化值的类型推断变量类型
  • 主要用途是简化代码,避免手动指定变量类型,提高代码的可读性和灵活性。
  • auto 推导的变量类型会与初始化值的类型相同,包括顶层 const、引用和指针等。
  • 适用于局部变量、范围循环、模板类型推导等场景。

try & throw & catch

  • try:用于包含可能会引发异常的代码块。如果代码块中的某些操作引发了异常,那么程序会跳转到对应的 catch 语句。
  • throw:用于引发异常,通常是当检测到某种错误情况时。throw 后面可以跟随异常对象,可以是内置类型、类实例等。
  • catch:用于捕获异常。当 try 块中出现 throw 后,程序会跳转到对应的 catch 块来处理异常。每个 catch 块可以指定捕获特定类型的异常。

可以使用任何类型的对象作为throw 表达式的操作数。 该对象一般用于传达有关错误的信息。 大多数情况下,建议使用std::exception类或标准库中定义的派生类之一。 如果其中的类不合适,建议从std::exception派生自己的异常类。

#include <iostream>
#include <stdexcept>

void divide(int a, int b) {
    if (b == 0) {
        throw std::runtime_error("Division by zero"); // 引发异常
    }
    std::cout << "Result: " << a / b << std::endl;
}

int main() {
    try {
        divide(10, 0); // 传入0,会引发除零异常
    }
    catch (const std::runtime_error& e) { // 捕获std::runtime_error类型的异常
        std::cerr << "Caught an exception: " << e.what() << std::endl;
    }
    catch (...) { // 捕获所有其他类型的异常
        std::cerr << "Caught an unknown exception" << std::endl;
    }

    std::cout << "Program continues after exception handling." << std::endl;
    return 0;
}
  • 如果没有找到匹配的 catch 块,程序会调用 std::terminate,导致程序非正常终止。
  • 捕获异常时,通常使用引用(如 const std::runtime_error& e)来避免对象拷贝。
  • throw 的对象可以是内置数据类型(如 int)或自定义异常类实例。

const_cast

const_cast 用于在指针或引用上去除常量属性,从而允许对常量对象进行修改。以下是关于 const_cast 关键字的用法和注意事项的总结:

用法:

  1. const_cast 用于去除指针或引用的常量属性,从而可以修改原本被声明为 const 的对象。
  2. 语法:
    • 对指针:const_cast<目标类型>(指针)
    • 对引用:const_cast<目标类型>(引用)

注意事项:

  1. 使用 const_cast 可能会导致未定义行为,尤其是如果原本的对象真的是常量,去除常量属性后进行修改可能会引发问题。
  2. 使用 const_cast 修改原本声明为 const 的对象会违反程序的逻辑,因此应该谨慎使用。
  3. const_cast 只能用于去除顶层的 const 属性,不能用于去除内部的 const 属性。
  4. 尽量避免使用 const_cast,应该尽量遵循常量对象不应该被修改的原则。

示例代码:

int main() {
    const int x = 5;
    const int* ptr = &x;

    int* mutablePtr = const_cast<int*>(ptr); // 去除常量属性
    *mutablePtr = 10;                        // 可能导致未定义行为,修改了本应该是常量的对象

    return 0;
}

总之,const_cast 关键字用于去除指针或引用的常量属性,从而允许修改原本被声明为 const 的对象。然而,使用 const_cast 需要非常谨慎,应该遵循良好的编程实践,尽量避免修改常量对象,以保持代码的可靠性和一致性。


constexpr

constexpr 用于声明常量表达式(Constant Expression)。它可以在编译时计算表达式的值,并用于声明常量、函数、构造函数等。以下是关于 constexpr 关键字的用法和注意事项的总结:

用法:

  1. constexpr 用于声明在编译时可计算的常量表达式。
  2. 语法:
    • 对于变量:constexpr 数据类型 变量名 = 表达式;
    • 对于函数:constexpr 返回类型 函数名(参数列表) { 函数体 }

注意事项:

  1. constexpr 变量必须在编译时就能计算出其值,因此表达式中不能包含运行时才能确定的值,如用户输入、动态分配内存等。
  2. constexpr 函数的参数和返回值都必须是字面值类型(Literal Type),并且函数体必须足够简单以便在编译时求值
  3. constexpr 变量在编译期间进行计算,因此它在运行时不会占用任何存储空间
  4. constexpr 函数可以在编译时被调用,其结果也会在编译时计算
  5. constexpr 变量可以用于模板参数、数组大小、枚举等需要在编译时确定值的场景

示例代码:

constexpr int factorial(int n) {
    return n <= 1 ? 1 : n * factorial(n - 1);
}

int main() {
    constexpr int x = 5;
    constexpr int y = factorial(x); // 编译时计算阶乘的结果
    static_assert(y == 120, "Factorial calculation is incorrect"); // 静态断言

    return 0;
}

总之,constexpr 关键字用于声明常量表达式,其值在编译时就能确定。它在编译期间进行计算,可以用于声明常量、函数、模板参数等,以及在需要在编译时确定值的场景中使用。


dynamic_cast

dynamic_cast 是 C++ 中用于执行动态类型转换(Runtime Type Casting)的关键字。它用于在运行时判断指针或引用指向的对象是否是指定的类型,从而进行安全的类型转换。以下是关于 dynamic_cast 关键字的用法和注意事项的总结:

用法:

  1. dynamic_cast 只能用于多态类层次结构,即至少有一个虚函数的类继承关系中,通常用于基类和派生类之间的类型转换。
  2. 语法:dynamic_cast<目标类型*>(表达式),返回一个目标类型的指针。如果转换失败,返回一个空指针。
  3. 当执行 dynamic_cast 转换时,会在运行时检查目标类型是否与表达式指向的对象的实际类型兼容。如果类型不兼容,转换失败。

注意事项:

  1. dynamic_cast 仅适用于指针和引用类型。不能用于非指针、非引用类型。
  2. 如果基类没有虚函数,就不能使用 dynamic_cast 进行转换
  3. 转换成功时,返回的指针或引用与原始指针/引用共享相同的地址
  4. 对于指针,如果转换失败,返回的是空指针。对于引用,如果转换失败,会抛出 std::bad_cast 异常。
  5. dynamic_cast 运行时开销较大,因为它需要在运行时进行类型检查,可能会影响性能
  6. 建议在使用 dynamic_cast 之前,首先尝试使用 dynamic_cast,如果转换失败再考虑其他的设计方式。

示例代码:

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

class Derived : public Base {
public:
    void print() override {
        std::cout << "Derived" << std::endl;
    }
};

int main() {
    Base* basePtr = new Derived;

    Derived* derivedPtr = dynamic_cast<Derived*>(basePtr);
    if (derivedPtr) {
        derivedPtr->print(); // 转换成功
    } else {
        std::cout << "Dynamic cast failed" << std::endl;
    }

    delete basePtr;
    return 0;
}

在上述示例中,使用 dynamic_castBase 类型的指针转换为 Derived 类型的指针。如果转换失败,会输出"Dynamic cast failed"。注意,这个示例需要使用多态的类层次结构。


static_cast

static_cast 是 C++ 中用于执行静态类型转换(Static Type Casting)的关键字。它用于进行编译时类型转换,可以用于各种基本数据类型、指针、引用以及用户自定义类型之间的转换。以下是关于 static_cast 关键字的用法和注意事项的总结:

用法:

  1. static_cast 用于执行一般的编译时类型转换,包括数字类型之间的转换、父子类之间的指针或引用类型转换等。
  2. 语法:static_cast<目标类型>(表达式),返回一个目标类型的值。

注意事项:

  1. static_cast 不能用于多态类层次结构的基类到派生类的指针或引用转换。如果需要在多态类层次结构中进行类型转换,应该使用 dynamic_cast
  2. 对于指针转换,如果源指针是空指针,转换后的指针也会是空指针。
  3. 转换后的结果可能会造成信息丢失或截断,因此在进行类型转换时要谨慎,确保不会造成不期望的结果。
  4. static_cast 不会执行运行时检查,它仅在编译时执行类型转换,因此在进行类型转换时需要确保类型之间的关系是正确的。
  5. 可以用于将整数类型转换为浮点类型,或浮点类型转换为整数类型。在这种情况下,要注意可能会导致精度损失或截断。

示例代码:

int main() {
    int intValue = 5;
    double doubleValue = static_cast<double>(intValue); // 整数到浮点数的转换

    double anotherDoubleValue = 3.14;
    int truncatedIntValue = static_cast<int>(anotherDoubleValue); // 浮点数到整数的转换,可能截断小数部分

    return 0;
}

总之,static_cast 关键字用于常规的编译时类型转换,但要注意类型之间的关系和可能引发的信息丢失或截断问题。


reinterpret_cast

reinterpret_cast 是 C++ 中用于执行重新解释类型转换(Type Reinterpretation)的关键字。它可以将一个指针或整数类型转换为另一种指针类型,甚至可以将不同类型的指针互相转换,但需要特别小心使用。以下是关于 reinterpret_cast 关键字的用法和注意事项的总结:

用法:

  1. reinterpret_cast 用于执行各种类型之间的强制转换,包括指针、整数、指针和整数之间的转换。
  2. 语法:reinterpret_cast<目标类型>(表达式),返回一个目标类型的值。

注意事项:

  1. reinterpret_cast 通常用于底层操作,如将指针转换为整数,进行位操作等。它执行的是低级别的类型转换,不进行任何类型检查。
  2. 使用 reinterpret_cast 可能会导致未定义行为,因为它可以将不同类型的指针互相转换,可能会破坏类型系统的规则。
  3. reinterpret_cast 对于不兼容的类型转换,例如将指针转换为不相关类型的指针,或者将整数转换为指针,通常是不安全的。
  4. 除非你完全理解操作的底层细节,并且非常清楚所进行的类型转换不会引发问题,否则不建议滥用 reinterpret_cast

示例代码:

int main() {
    int intValue = 42;
    double* doublePointer = reinterpret_cast<double*>(&intValue); // 将整数指针转换为双精度浮点数指针

    uintptr_t intValueAsInt = reinterpret_cast<uintptr_t>(&intValue); // 将指针转换为整数类型

    // 不安全的使用,可能会引发未定义行为
    int* unsafePointer = reinterpret_cast<int*>(intValueAsInt);

    return 0;
}

总之,reinterpret_cast 关键字用于低级别的类型转换,可以将不同类型的指针和整数进行互相转换。但由于可能引发未定义行为和类型安全问题,它应该谨慎使用,仅用于底层操作和特殊情况。


explicit

在 C++11 之前,explicit 关键字只能用于修饰构造函数,用于防止隐式类型转换。然而,从 C++11 开始,explicit 关键字也可以用于修饰转换函数,以防止隐式的类型转换。用于修饰类的单参数构造函数,目的是防止隐式类型转换。

以下是 explicit 关键字的用法总结:

  1. 避免隐式类型转换: 当一个类的构造函数被标记为 explicit 时,这个构造函数就不能用于隐式地进行类型转换。这样可以避免一些不明确或不期望的类型转换,从而提高代码的可读性和安全性。
  2. 防止二义性问题: 如果一个类有多个构造函数,并且这些构造函数的参数之间存在某种关联,不加 explicit 关键字可能会导致二义性问题。通过使用 explicit,可以明确指定哪个构造函数可以被用于类型转换。
  3. 强制显式构造: 使用 explicit 关键字可以强制调用者在进行对象构造时明确指定构造函数,从而提醒他们构造对象的方式。

以下是一个示例,说明 explicit 关键字的用法:

class MyClass {
public:
    explicit MyClass(int value) : data(value) {}
private:
    int data;
};

int main() {
    // 使用显式构造调用
    MyClass obj1(5);
    
    // 错误,不能隐式转换
    // MyClass obj2 = 10;
    
    // 正确,使用显式构造
    MyClass obj3 = MyClass(10);
    
    return 0;
}


mutable

mutable 用于修饰类的成员变量。它的主要作用是允许在一个被声明为 const 的成员函数内部修改被标记为 mutable 的成员变量。以下是关于 mutable 关键字的用法和注意事项的总结:

用法:

  1. mutable 用于修饰类的成员变量,使其在 const 成员函数内部仍然可以被修改。
  2. mutable 可以用于任何非静态的成员变量,无论其类型是基本类型还是自定义类型。

注意事项:

  1. mutable 关键字用于在 const 成员函数中修改成员变量,但并不影响类的不变性。也就是说,修改 mutable 成员变量不会破坏类的语义。
  2. mutable 应谨慎使用,应该只用于确实需要在 const 成员函数内修改状态的情况。过度使用 mutable 可能会破坏代码的清晰性。
  3. mutable 只影响在 const 成员函数中的行为,非 const 成员函数中仍然可以随意修改成员变量。

示例代码:

class MyCounter {
public:
    MyCounter(int initialValue) : value(initialValue) {}

    int getValue() const {
        // 在 const 成员函数内修改 mutable 变量
        mutableValue++;
        return value;
    }

private:
    int value;
    mutable int mutableValue = 0; // 标记为 mutable 的成员变量
};

int main() {
    const MyCounter counter(5);
    int result = counter.getValue(); // 不会引发编译错误,mutable 变量可以被修改

    return 0;
}

总之,mutable 关键字允许在 const 成员函数中修改被标记为 mutable 的成员变量。这可以用于在一些特定情况下,保持类的不变性同时又需要在 const 成员函数内部修改状态的需求。


noexcept

noexcept用于指定函数是否可能引发异常。它可以用于函数声明、函数定义、表达式和类型上,用于在编译时指示函数是否可能抛出异常。以下是关于 noexcept 关键字的用法和注意事项的总结:

用法:

  1. noexcept 用于指定一个函数是否为不抛出异常的函数,或者一个表达式是否在运行时抛出异常。
  2. 语法:
    • 在函数声明和定义中:返回类型 函数名(参数列表) noexcept,或者 返回类型 函数名(参数列表) noexcept(表达式)
    • 在表达式中:表达式 noexcept

注意事项:

  1. noexcept 关键字指示函数是否可能引发异常,而不是是否实际引发异常。如果函数被声明为 noexcept,但实际引发了异常,程序将会终止。
  2. 如果一个函数被标记为 noexcept,并且在函数内部发生了异常,std::terminate 会被调用,导致程序终止。
  3. 在标记为 noexcept 的函数中,应该尽量避免调用可能引发异常的函数,以保证程序的稳定性。
  4. 使用 noexcept 可以在一些情况下优化编译器的代码生成,因为编译器知道函数不会引发异常,可以进行更有效的优化。
  5. noexcept 关键字可以用于移动构造函数、移动赋值运算符等,以支持异常安全的移动操作。

示例代码:

void foo() noexcept {
    // 不会引发异常
}

void bar() noexcept(false) {
    throw std::runtime_error("An error occurred"); // 可能引发异常
}

int main() {
    try {
        bar(); // 在此处调用 bar,std::terminate 会被调用
    } catch (const std::exception& e) {
        std::cerr << "Exception caught: " << e.what() << std::endl;
    }

    return 0;
}

noexcept(noexcept(...)) 这种用法允许在函数内部检查另一个函数是否会抛出异常。外部的 noexcept 将根据内部的判断来确定当前函数的异常安全性。

void anotherFunction() noexcept(noexcept(Allocator()));  // 如果 Allocator() 不会抛出异常,则 anotherFunction() 也不会抛出

template <typename T> void func() noexcept(noexcept(T()));  // 根据 T 的构造函数决定是否抛出

总之,noexcept 关键字用于指定函数是否可能引发异常。在使用时,需要明确考虑函数内部可能引发的异常情况,以及如何处理异常,以保证程序的稳定性和可靠性


static_assert

static_assert 用于进行编译时的静态断言(Static Assertion)。它用于在编译时检查某个条件是否为真,如果条件不满足,编译会失败并显示错误消息。以下是关于 static_assert 关键字的用法和注意事项的总结:

用法:

  1. static_assert 用于在编译时对一个布尔表达式进行断言,如果表达式为假,则会导致编译失败。
  2. 语法:static_assert(表达式, 错误消息),其中表达式是要检查的条件,错误消息是在编译失败时显示的错误信息。

注意事项:

  1. static_assert 是在编译时进行的,它不会影响程序运行时的行为。
  2. 断言的条件必须在编译时可确定,不能依赖于运行时的值。
  3. 错误消息是可选的,但可以提供有关断言失败的更详细信息,以帮助开发者定位问题。
  4. static_assert 可以在函数内部、类内部、命名空间内部等地方使用。
  5. static_assert 通常用于检查编译时的常量表达式,例如检查类型大小、某些宏是否定义等。

示例代码:

template <typename T, size_t Size>
class MyArray {
    static_assert(Size > 0, "Size must be greater than 0");
public:
    // ...
private:
    T data[Size];
};

int main() {
    MyArray<int, 0> array; // 编译失败,Size 必须大于 0
    return 0;
}

总之,static_assert 关键字用于在编译时进行断言,检查条件是否满足。它在编译期间提供了一种方式来检查代码的正确性,以便尽早发现潜在的问题。


thread_local

thread_local 用于声明线程局部变量(Thread-Local Variable)线程局部变量是指每个线程拥有一份独立的变量副本,各个线程之间互不影响。以下是关于 thread_local 关键字的用法和注意事项的总结:

用法:

  1. thread_local 用于声明线程局部变量,使得每个线程都有一份独立的变量副本。
  2. 语法:thread_local 数据类型 变量名 = 初始值;

注意事项:

  1. thread_local 变量在不同线程中拥有独立的存储副本,各个线程对该变量的修改不会影响其他线程的副本。
  2. thread_local 变量在不同线程中初始化,因此其初始化过程可能与线程的创建时机有关。
  3. thread_local 变量可以在全局范围、命名空间、类内部等地方声明,但不能在函数内部声明。
  4. thread_local 变量通常用于需要在线程之间保持状态独立性的场景,如线程池中的线程、多线程任务等。
  5. 不同编译单元中的 thread_local 变量可能存在内存对齐问题,因此最好将其声明为 extern,并在一个源文件中进行定义。

示例代码:

#include <iostream>
#include <thread>

thread_local int threadLocalVar = 0;

void threadFunction() {
    threadLocalVar++; // 每个线程修改自己的副本
    std::cout << "Thread-local variable: " << threadLocalVar << std::endl;
}

int main() {
    std::thread t1(threadFunction);
    std::thread t2(threadFunction);

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

    return 0;
}

总之,thread_local 关键字用于声明线程局部变量,使得每个线程都拥有一份独立的变量副本。这可以在多线程场景中保持状态的独立性,避免了线程间的变量冲突问题

__threadthread_local 都用于实现 线程局部存储 (Thread-Local Storage, TLS),使变量在每个线程中都有独立的实例,但它们有一些区别:

1. 标准化和可移植性

  • __thread:是 GCC 和 Clang 编译器的非标准扩展,并不在 ANSI C 或 ISO C++ 标准中。因此,它的可移植性较差,仅在支持该扩展的编译器和平台(主要是 Linux、Unix 系统)上有效。
  • thread_local:是 C++11 引入的标准关键字,属于 C++ 标准的一部分,具备良好的可移植性。只要编译器支持 C++11 及以上标准,就可以使用 thread_local,包括 Windows、Linux 和 macOS。

2. 语言支持

  • __thread:只能用于 C 和 C++ 代码。
  • thread_local:仅支持 C++11 及以上,在 C 中无法使用。

3. 应用范围

  • __thread:只能用于 POD类型(Plain Old Data types)变量,不能用于具有非平凡构造函数、析构函数、拷贝构造函数等的复杂类型对象。
  • thread_local:可以应用于 任何类型 的变量,包括复杂对象。它会自动调用对象的构造函数和析构函数,确保每个线程的对象正确构建和销毁。

4. 生命周期和销毁

  • __thread:在线程生命周期结束时,编译器并不会自动调用其析构函数,因此它对非POD类型对象的支持有限。
  • thread_local:在 C++ 中,支持自动管理对象的生命周期,每个线程退出时会自动销毁thread_local对象并调用析构函数。

5. 兼容性和平台支持

  • __thread:主要支持 ELF 文件格式的系统(如 Linux)。在 Windows 平台上不直接支持,需使用 __declspec(thread) 关键字。
  • thread_local:适用于多种平台和编译器,包括 Windows、Linux 和 macOS。

简要总结

C++11 及以上的环境中,优先使用 thread_local,因为它是标准化的、类型安全的并且有更好的可移植性。而 __thread 适合在仅需支持 GCC 或 Clang 的 C/C++ 项目中使用。


typename

typename如果模板定义中的名称是依赖于模板参数的限定名称,则必须使用关键字;如果限定名称不依赖,则为可选。 有关详细信息,请参阅模板和名称解析

typename 可由任何类型在模板声明或定义中的任何位置使用。 除非是模板基类基类的模板参数,否则不允许在基类列表中使用它。

template <class T>
class C1 : typename T::InnerType // Error - typename not allowed.
{};
template <class T>
class C2 : A<typename T::InnerType>  // typename OK.
{};

typename 关键字也可以替代模板参数列表中的 class 使用。 例如,下面的语句在语义上是等效的:

template<class T1, class T2>...
template<typename T1, typename T2>...

示例

// typename.cpp
template<class T> class X
{
   typename T::Y m_y;   // treat Y as a type
};

int main()
{
}

在模板定义中,有三种类型的名字。

  • 本地声明的名称,包括模板本身的名称和模板定义内声明的任何名称。
  • 来自模板定义外的包围范围的名称。
  • 以某种方式依赖于模板参数的名称,被称为依赖性名称。

虽然前两个名字也与类和函数的作用域有关,但在模板定义中需要特殊的名字解析规则来处理依赖性名字的额外复杂性。这是因为在模板被实例化之前,编译器对这些名字知之甚少,因为它们可能是完全不同的类型,这取决于使用了哪些模板参数。非从属名称是按照通常的规则在模板的定义点上查找的。这些名字是独立于模板参数的,对所有模板的特殊化都要进行一次查询。从属名称在模板被实例化之前不会被查询,并且为每一个特殊化单独查询。

如果一个类型依赖于模板参数,它就是依赖性的。具体而言,如果类型是以下项,则属于依赖类型:

  • 模板自变量本身:
    T
  • 具有包括依赖类型的限定的限定名:
    T::myType
  • 当非限定部分标识依赖类型时的限定名:
    N::T
  • 其基类型属于依赖类型的不变或可变类型:
    const T
  • 基于依赖类型的指针、引用、数组或函数指针类型:
    T *, T &, T [10], T (*)()
  • 大小基于模板参数的数组:
    template <int arg> class X {
    	int x[arg] ; // dependent type
    }
  • 从模板参数构造的模板类型:
    T<int>, MyTemplate<T>

模板参数上的名称和表达式依赖项分为类型依赖项或值依赖项,具体取决于模板参数是类型参数还是值参数。 此外,在模板自变量上有类型依赖项的模板中声明的任何标识符都被视为依赖值,使用值依赖表达式初始化的整数类型或枚举类型也是如此。

类型依赖和值依赖表达式是涉及类型依赖或值依赖的变量的表达式。 这些表达式可能有不同的语义,具体取决于用于模板的参数。


volatile

volatile(不稳定的)限定一个对象可被外部进程(操作系统、硬件或并发线程等)改变,声明时的语法如下:

int volatile nVint;

这样的声明是不能达到最高效的,因为它们的值随时会改变,系统在需要时会经常读写这个对象的值。因此常用于像中断处理程序之类的异步进程进行内存单元访问。

可以使用 volatile 限定符提供对异步过程(如中断处理程序)使用的内存位置的访问权。

如果将 struct 成员标记为 volatile,则 volatile 将传播到整个结构。 如果结构不具有可通过使用一个指令在当前体系结构上复制的长度,则此结构上可能完全丢失 volatile

如果满足下列条件之一,则 volatile 关键字可能对字段不起作用:

  • 可变字段的长度超过可使用一条指令在当前体系结构上复制的最大大小。
  • 最外层包含 struct 的长度 - 或如果它是可能嵌套的 struct 的成员 - 超过可使用一条指令在当前体系结构上复制的最大大小。

尽管处理器不会对不可缓存的内存访问重新排序,但必须将不可缓存的变量标记为 volatile,从而保证此编译器不会对内存访问重新排序。

声明为 volatile 的对象不在某些优化中使用,因为它们的值可以随时更改。 系统在请求易失对象时始终读取该对象的当前值,即使前面的指令要求从同一对象获取值也是如此。 此外,对象的值会立即在赋值时写入。


inline、__inline 和 __forceinline

inline 和 __inline 说明符指示编译器将函数体的副本插入到调用函数的每个位置。

仅当编译器的成本收益分析显示有价值时,才会进行插入(称为内联展开或内联)。 内联展开以代码大小较大的潜在成本最大程度地减少函数调用开销。

__forceinline 关键字会重写成本收益分析,改为依赖于程序员的判断。 使用 __forceinline 时应小心谨慎。 不加选择地使用 __forceinline 可能会形成较大代码但是仅获得边际性能提升,或是在某些情况下,甚至会损失性能(例如,由于较大可执行文件的分页会增加)。

编译器将内联扩展选项和关键字视为建议。 不保证会对函数进行内联。 无法强制编译器对特定函数进行内联(即使使用 __forceinline 关键字)。 使用 /clr 进行编译时,如果对函数应用了安全特性,则编译器不会对函数进行内联。

为了与以前的版本兼容,除非指定了编译器选项 /Za(禁用语言扩展),否则 _inline 和 _forceinline 分别是 __inline 和 __forceinline 的同义词。

inline 关键字告知编译器,内联展开是首选操作。 但是,编译器可以创建函数的单独实例(实例化),并创建标准调用链接而不是内联插入代码。 可能会出现这种行为的两种情况是:

  • 递归函数。
  • 在翻译单元中的其他位置通过指针引用的函数。

这些原因可能会干扰内联(其他原因可能也会这样,由编译器自行决定);你不应依赖于 inline 说明符使函数进行内联。

编译器可以在多个翻译单元中将头文件中定义的内联函数作为一个可调用函数来创建,而不是扩展它。 编译器为链接器标记生成的函数,防止出现单一定义规则 (ODR) 冲突。

与普通函数一样,内联函数中的自变量计算没有明确顺序。 事实上,这可能与使用普通函数调用协议传递时的自变量计算顺序不同。

/Ob 编译器优化选项有助于确定是否实际进行内联函数展开。

/LTCG 是否在源代码中请求跨模块内联。

// when_to_use_inline_functions.cpp
class Point
{
public:
    // Define "accessor" functions as
    //  reference types.
    unsigned& x();
    unsigned& y();
private:
    unsigned _x;
    unsigned _y;
};

inline unsigned& Point::x()
{
    return _x;
}

inline unsigned& Point::y()
{
    return _y;
}

int main()
{
}

假设坐标操作是此类客户端中相对常见的操作,则将两个访问器函数(前面示例中的 x 和 y)指定为 inline 通常将节省下列操作的开销:

  • 函数调用(包括参数传递和在堆栈上放置对象地址)
  • 保留调用者的堆栈帧
  • 设置新的堆栈帧
  • 返回值通信
  • 还原旧堆栈帧
  • 返回

内联函数类似于宏,因为在编译时进行调用会展开函数代码。 但是,内联函数是通过编译器分析的,而宏是通过预处理器展开的。 因此,存在很多重大差异:

  • 内联函数遵循对正常函数强制执行的所有类型安全协议。
  • 用来指定内联函数的语法与任何其他函数相同,只不过它们在函数声明中包含 inline 关键字。
  • 计算一次作为内联函数的参数传递的表达式。 在某些情况下,作为宏的自变量传递的表达式可计算多次。

下面的示例演示了将小写字母转换为大写字母的宏:

// inline_functions_macro.c
#include <stdio.h>
#include <conio.h>

#define toupper(a) ((a) >= 'a' && ((a) <= 'z') ? ((a)-('a'-'A')):(a))

int main() {
   char ch;
   printf_s("Enter a character: ");
   ch = toupper( getc(stdin) );
   printf_s( "%c", ch );
}

这意味着,程序将等待两个或三个字符,而在理想情况下,它只应等待一个字符。

内联函数纠正了前面所述的问题:

// inline_functions_inline.cpp
#include <stdio.h>
#include <conio.h>

inline char toupper( char a ) {
   return ((a >= 'a' && a <= 'z') ? a-('a'-'A') : a );
}

int main() {
   printf_s("Enter a character: ");
   char ch = toupper( getc(stdin) );
   printf_s( "%c", ch );
}

namespace

namespace

有可能在不同头文件中用了相同的名字来命名所定义的类或函数,命名空间把一些全局实体分别放在不同空间中,从而与其他全局实体分隔开来

namespace py = pybind11; 用短的名字代替长的名字

namespace

调用带有命名空间的函数或变量,需要在前面加上命名空间的名称

name::code;

//定义一个新的命名空间,或为已有的命名空间增加新的元素
namespace namespace_name {
   // 代码声明
}

//调用name命名空间中的code
name::code;  // code 可以是变量或函数

virtual

  • virtual用于基类声明此虚函数需要重写
  • 使用基类的指针p指向派生类的对象时,调用的p的一个方法(比如print)时,调用的是基类里面的print方法,如果使用virtual关键字,则可以调用派生类里的print方法
  • 纯虚函数所在的类被称为抽象类,只能用来被继承而不能实例化。一个虚拟函数只是提供了一个可被子类型改写的接口,派生类必须实现每一个纯虚函数。在基类中实现纯虚函数的方法是在函数原型后加 =0,virtual void funtion1()=0
  • 虚继承就是为了解决菱形继承中,B,C都继承了A,D继承了B,C,那么D关于 A的引用只有一次,而不是 普通继承的对于A引用了两次,格式:可以采用public、protected、private三种不同的继承关键字进行修饰,只要确保包含virtual就可以了。
class A {
  void f1(){};
};

class B : public virtual  A {
 void f2(){};
};

构建对象时,先会执行构造函数,然后再初始化虚函数表

在C++中,当一个子类继承自一个父类时,子类会继承父类的虚函数表。子类的虚函数表会在父类的虚函数表的基础上增加或者覆盖一些虚函数。

当创建一个子类的对象时,会按照以下顺序进行虚函数表的初始化:

  1. 分配子类对象的内存空间。
  2. 在子类对象的内存空间中添加一个指向子类虚函数表的指针(vptr)。
  3. 调用父类的构造函数,之后编译器会初始化父类的虚函数表。
  4. 调用子类的构造函数,覆盖或者增加子类自己的虚函数,
  5. 初始化子类的虚函数表。

声明pure virtual函数的目的是为了让derived class只继承函数接口

声明impure virtal函数的目的,是让derived class继承该函数的接口和缺省实现

声明non-virtual函数的目的是为了让derived class继承函数的接口及一份强制实现


override final

override用于派生类声明此虚函数需要重写覆盖,用法Tick read(PacketPtr pkt) override;

final声明当前派生类此虚函数为最后一次覆盖


using

using 是 C++ 中的一个关键字,用于引入符号,别名或者重载基类成员到派生类的作用域中。以下是关于 using 关键字的用法和注意事项的总结:

用法:

  1. using 用于引入基类成员、命名空间成员、别名等,以使其在派生类的作用域内可见。
  2. 语法:
    • 引入基类成员:using 基类名::成员名;
    • 引入命名空间成员:using 命名空间名::成员名;
    • 定义别名:using 新名字 = 原名;

注意事项:

  1. using 可以用于在派生类中引入基类的成员,使其可以通过派生类访问,但不会重载派生类的同名成员。
  2. using 也可以用于引入命名空间的成员,使其在当前作用域内可见。
  3. 使用 using 定义别名可以简化类型名称,提高代码可读性。
  4. 在派生类中引入基类成员时,注意成员的可访问性权限。基类成员必须在派生类的公有或保护部分才能通过 using 引入。
  5. 在使用 using 定义别名时,别名的命名应当有意义,易于理解。

示例代码:

#include <iostream>
class Base {
public:
    void foo() {
        std::cout << "Base::foo()" << std::endl;
    }
};

class Derived : public Base {
public:
    using Base::foo; // 在派生类中引入基类的成员函数 foo
};

int main() {
    Derived d;
    d.foo(); // 调用引入的基类成员函数

    return 0;
}

总之,using 关键字可以用于引入基类成员、命名空间成员或定义别名。它在派生类中引入基类成员时可以使代码更加清晰,也可以用于在命名空间中引入特定的成员,或者简化类型名称。

using、typedef异同

usingtypedef 都用于创建类型别名,但在一些细节和用法上有一些区别。以下是它们之间的主要区别:

1. 语法:

  • using 的语法更加灵活,可以用于定义别名、引入基类成员、引入命名空间成员等。
  • typedef 的语法相对固定,只能用于定义别名。

2. 模板化别名:

  • using 支持模板化的别名,可以用于定义通用的别名模板。
  • typedef 不支持直接定义模板化的别名,需要通过间接的方法来实现。

3. 作用域:

  • using 在类作用域中引入基类成员或命名空间成员时,可以限定成员的可见性,避免引入多余的符号。
  • typedef 不具备这种精确的作用域控制能力。

4. 兼容性:

  • using 是 C++11 引入的新特性,较老的编译器可能不支持。但在支持的环境下,它是更现代的做法。
  • typedef 是 C++ 早期就存在的特性,几乎所有的 C++ 编译器都支持。

5. 可读性:

  • using 的语法相对于 typedef 更加直观和易读,能够更清晰地表达类型别名的意图。

const

const关键字有多种用途,下面是一些常见的用途:

  1. 常量声明:使用const关键字可以声明一个常量。这意味着变量的值不能被修改。例如:
const int x = 5;
  1. 函数参数:使用const关键字可以将函数参数声明为只读,这意味着函数不能修改参数的值。这可以防止函数意外地修改传递给它的参数。例如:
void myFunction(const int x) {
  // x的值不能在函数中修改
}
  1. 函数返回值:使用const关键字可以将函数的返回值声明为只读,这意味着调用函数的代码不能修改返回值。例如:
const int myFunction() {
  return 5;
}
  1. 成员函数:使用const关键字可以将成员函数声明为只读,这意味着函数不能修改对象的状态。这可以防止在只读上下文中意外地修改对象。例如:
class MyClass {
public:
  int getValue() const {
    // 不能修改对象的状态
    return value;
  }
private:
  int value;
};

注意:const关键字用于修饰成员函数,表示该函数不会修改对象的状态

由于静态成员函数不属于任何对象,因此它不包含this指针,也就是说,它不能访问对象的非静态成员变量和非静态成员函数。因此,在静态成员函数中使用const关键字是没有意义的,因为const成员函数必须具体到某一实例,而静态成员函数不属于任何对象,也就不存在实例化的问题。

因此,const关键字不能与static关键字同时使用,因为它们的含义是相互矛盾的。如果在静态成员函数中使用const关键字,编译器会报错。

常量成员函数可以访问非常量成员变量和非常量成员函数,因为常量成员函数不会修改对象的状态。常量成员函数也可以在中访问和调用静态成员变量和静态成员函数,因为它们不属于任何对象。

  1. 指针和引用:使用const关键字可以创建指向常量的指针或引用,这意味着指针或引用不能用于修改变量的值。例如:
int x = 5;
const int* ptr = &x;
// *ptr不能用于修改x的值

如果const位于星号的左侧,则const就是用来修饰指针所指向的变量,即指针指向为常量

如果const位于星号的右侧,const就是修饰指针本身,即指针本身是常量。

const 修饰引用,则不能通过常引用修改其引用的变量的值

  1. 静态断言:使用const关键字可以在编译时执行静态断言,这是一种在编译时检查代码的技术。静态断言通常用于验证程序的一些假设或约束条件。例如:
#include <type_traits>

template <typename T>
void process(T value)
{
    // 静态断言:确保 T 是可复制构造类型
    static_assert(std::is_copy_constructible<T>::value, "T must be copy constructible");

    // 继续处理 value ...
}

int main()
{
    int x = 5;
    process(x); // 正常运行

    struct NonCopyable {
        NonCopyable(const NonCopyable&) = delete;
    };

    NonCopyable nc;
    process(nc); // 编译错误:T must be copy constructible

    return 0;
}
  1. 模板参数:使用const关键字可以在模板中使用常量参数,这些参数在编译时就被确定了。常量模板参数可以用于控制模板的行为,例如指定数组大小或指定算法的行为。例如:
template <int N>
void myFunction(const int (&arr)[N]) {
  // 接受一个大小为N的数组
}


static

静态变量具有全局变量的生命周期,但只能作用于自己的作用域

static关键字三种特性:

  • 缩减作用域,全局 → 局部
  • 加长生命周期,一直到程序结束
  • 默认清零

下面是static关键字在C++中的所有用途:

静态变量
  • 静态局部变量:在函数内部声明的静态局部变量仅在第一次调用函数时初始化,并在函数退出后保留其值。它们对于在多次函数调用之间保持状态非常有用。
  • 静态全局变量:在全局作用域中声明的静态变量只在文件的生命周期内可见,无法被其他文件访问
  • 静态成员变量:
    • 在类中声明的静态成员变量属于类而不是实例,所有类的实例共享相同的静态成员变量。这对于在多个类实例之间共享数据非常有用。
    • 静态成员变量必须在类外部进行定义和初始化。
  • 静态类:
    • C++中的 static 关键字也可以用于类的声明,以表示该类在编译时对其他文件不可见。这在某些情况下用于实现封装。
  • 命名空间中的静态变量:使用static关键字可以在命名空间中创建静态变量,这些变量只能在当前文件中访问。静态变量通常用于实现文件内部的全局状态。

在函数内部使用static修饰的变量被称为静态局部变量。这些变量在函数调用时不会被销毁,而是会一直存在于内存中,直到程序结束。静态局部变量的作用域仅限于定义它的函数内部。静态局部变量的初始化只会在第一次调用函数时执行一次。例如:

#include <iostream>

using namespace std;

void myFunction() {
  static int count = 0; // 静态变量
  count++;
  cout << "调用函数次数为:" << count << endl;
}

int main() {
  myFunction();
  myFunction();
  myFunction();
  return 0;
}

// 输出:
// 调用函数次数为:1
// 调用函数次数为:2
// 调用函数次数为:3

静态成员变量属于类,而不是属于类的对象,可以通过类和对象去调用。所有类的对象共享同一个静态成员变量,且静态成员变量的值只有一份。静态成员变量必须在类外部进行初始化,且只能初始化一次。

#include <iostream>

using namespace std;

class MyClass {
  public:
    static int count; // 静态成员变量
};

int MyClass::count = 0; // 静态成员变量的初始化

int main() {
  MyClass obj1;
  MyClass obj2;
  obj1.count = 5;
  cout << "obj1.count = " << obj1.count << endl; // 输出 5
  cout << "obj2.count = " << obj2.count << endl; // 输出 5
  obj2.count = 10;
  cout << "obj1.count = " << obj1.count << endl; // 输出 10
  cout << "obj2.count = " << obj2.count << endl; // 输出 10
  return 0;
}

拷贝构造函数的问题

在使用包含静态成员的类时,有时候会调用拷贝构造函数生成临时的隐藏的类对象,而这个临时对象在消亡时会调用析构函数有可能会对静态变量做操作(例如total_num--),可是这些对象在生成时却没有执行构造函数中的total_num++的操作。解决方案是为这个类写一个拷贝构造函数,在该拷贝构造函数中完成total_num++的操作。

namespace MyNamespace {
  static int x = 0;
}

静态函数

使用static关键字可以创建静态函数,这些函数只能在当前文件中访问。静态函数通常用于实现文件内部的辅助函数。

  • 静态成员函数

    使用static关键字可以创建静态成员函数,这些函数可以在不创建类对象的情况下调用。静态成员函数通常用于实现与类相关的全局函数。

    静态成员函数可以直接通过类名调用,不需要创建类的对象,可以通过对象去调用。

    静态成员函数只能访问类的静态成员和其他静态函数。

    静态成员函数没有this指针,因此不能访问非静态成员和非静态函数

  • 出现在类体外的函数定义不能指定关键字static
  • 非静态成员函数可以任意地访问静态成员函数和静态数据成员
  • 属于类作用域的函数如果不访问对象的数据则可为静态函数
  • 静态方法不能是const方法

例如:

#include <iostream>

using namespace std;

class MyClass {
  private:
    int x;
  public:
    static int count; // 静态成员变量
    static void myStaticFunction() { // 静态成员函数
      cout << "静态成员变量 count = " << count << endl;
      // cout << "非静态成员变量 x = " << x << endl; // 错误!不能访问非静态成员
    }
};

int MyClass::count = 0; // 静态成员变量的初始化

int main() {
  MyClass::myStaticFunction();
  return 0;
}

// 输出:
// 静态成员变量 count = 0

局部变量 全局变量 成员变量 外部函数 成员函数 入参/返回值 指针/引用
const 定义在栈上,可以通过指针读写 常量 常量成员 - 声明不修改成员变量 传入/返回常量 常指针/引用
static 延长局部变量生命周期 缩短全局变量作用域到当前文件 缩短作用域到class 缩减作用域到当前文件 缩减作用域到class - 与变量一样
静态局部常量 当前文件常量 class常量 - - - 同变量

new/delete

new操作符从自由存储区(free store)上为对象动态分配内存空间,自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。

堆是操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配,C语言使用malloc从堆上分配内存,使用free释放已分配的对应内存。

自由存储区不等于堆,堆是一个实际的区域,而自由存储区是一个更上层的概念。通常new确实是在堆上申请内存,但是程序员可以自己重载new操作符,使用其他内存来实现自由存储(这并不常见)。另外,c++ primer plus这本书上有提到布局new,可以为对象在栈上分配内存。总的来说,自由存储区是new申请的区间的概念。

//使用NEW运算符在堆中开辟一块新空间,例程如下
#include <iostream>

using std::cout;

class A {
  public:
		A(){cout<<"构造函数执行完毕\n";};
};

int main() {
		A *p;     //声明一个属于A类的指针p
		p=new A;  //使用new运算符创建一块堆中空间,它的大小由类A的数据成员的类型和数量
							//来定,由于堆在内存中都是匿名的,因此这里没有为它命名,只是将它的内存
							//地址赋给了指针p
		// compile time array
		int ctarray[ 3 ]; //allocated on stack
		// run time array
		int* rtarray = new int[ 3 ]; //allocated on heap

		return 0;
}
Image in a image block

主要用法和特点如下:

  1. 动态分配单个对象

    可以使用new关键字动态分配单个对象的内存空间,并返回指向该对象的指针。分配的内存空间在使用完毕后需要用delete关键字释放,否则会导致内存泄漏。

int *p = new int;   // 动态分配一个int类型的对象
*p = 10;            // 给对象赋值
cout << *p << endl; // 输出对象的值
delete p;           // 释放内存空间
  1. 动态分配数组

    可以使用new关键字动态分配数组的内存空间,并返回指向数组的指针。分配的内存空间在使用完毕后需要用delete[]关键字释放,否则会导致内存泄漏。

int n = 5;
int *p = new int[n];          // 动态分配一个包含5个int类型的数组
for (int i = 0; i < n; i++) {
  p[i] = i + 1;               // 给数组元素赋值
  cout << p[i] << " ";        // 输出数组元素
}
cout << endl;
delete[] p;                   // 释放内存空间
  1. operator new

    operator newnew是有区别的。

    new是一个C++关键字,用于在堆上动态分配内存并构造对象。使用new时,会自动调用构造函数来初始化对象。

    operator new是一个C++库函数,用于在堆上分配一块指定大小的内存。它只负责分配内存,不会调用构造函数来初始化对象。因此,如果使用operator new来分配内存,需要手动调用构造函数来初始化对象。

    通常情况下,你应该使用new来分配内存并构造对象,而不是直接使用operator new。这样可以确保对象被正确地初始化,并且可以避免一些常见的内存管理错误。

    可以使用operator new关键字在指定的内存地址上构造对象。定位new可以用于在已分配的内存空间上构造对象,或在内存映射文件上构造对象。

#include <iostream>

using namespace std;

class MyClass {
  public:
    MyClass() { // 构造函数
      cout << "MyClass constructed" << endl;
    }
    ~MyClass() { // 析构函数
      cout << "MyClass destroyed" << endl;
    }
};

int main() {
  void *p = operator new(sizeof(MyClass)); // 分配内存空间
  MyClass *q = new (p) MyClass();          // 在指定的内存地址上构造对象
  q->~MyClass();                           // 显式调用析构函数
  operator delete(p);                      // 释放内存空间
  return 0;
}

// 输出:
// MyClass constructed
// MyClass destroyed
  1. palcement new

    对一块未初始化的内存进行处理,获得想要的对象,这是需要求助于placement new()。placement new()是C++标准库的一部分,被申明在头文件<new>

    placement new()的函数原型不是void* placement new(std::size_t, void* __p);

    placement new只是operator new()的一个重载,多了一个已经申请好的空间,由void* __p指定;

    用法是new (addr) constructor(),对addr指定的内存空间调用构造函数进行初始化。为何称为placement new,从其用法可以看出只是用于调用构造函数。

    定位放置new操作的语法形式不同于普通的new操作。

    例如,一般都用如下语句A* p = new A;申请空间,而定位放置new操作则使用如下语句A* p = new (ptr) A;申请空间,其中ptr就是程序员指定的内存首地址。如下程序。

#include <iostream>

using namespace std;

class A {
    int num;
public:
    A() {
        cout<<"A's constructor"<<endl;
    }
    ~A() {
        cout<<"~A"<<endl;
    }
    void show(){
        cout<<"num:"<<num<<endl;
    }
};

int main() {
    char mem[100];
    mem[0]='A';
    mem[1]='\0';
    mem[2]='\0';
    mem[3]='\0';
    cout<<(void*)mem<<endl;
    A* p = new (mem) A; // placement new
    cout<<p<<endl;
    p->show();
    p->~A();
    getchar();
}

// 程序运行结果: 
 0024F924 
 A’s constructor 
 0024F924 
 num:65 
 ~A

注意以下几点:

  • 用定位放置new操作,既可以在栈(stack)上生成对象,也可以在堆(heap)上生成对象。如本例就是在栈上生成一个对象。
  • 使用语句A* p=new (mem) A;定位生成对象时,指针p和数组名mem指向同一片存储区
  • 定位生成对象时,会自动调用类A的构造函数,但是由于对象的空间不会自动释放(对象实际上是借用别人的空间),所以必须显示的调用类的析构函数,如本例中的p->~A()。

  1. 重载new和delete

    可以在类中重载newdelete关键字,以便自定义内存分配和释放的方式。重载newdelete可以用于实现内存池、垃圾回收等高级功能。

#include <iostream>

using namespace std;

class MyClass {
  public:
    void *operator new(size_t size) { // 重载new关键字
      cout << "MyClass::new" << endl;
      void *p = malloc(size);         // 分配内存空间
      return p;
    }
    void operator delete(void *p) {   // 重载delete关键字
      cout << "MyClass::delete" << endl;
      free(p);                        // 释放内存空间
    }
};

int main() {
  MyClass *p = new MyClass();         // 动态分配内存空间 
  delete p;                           // 释放内存空间 
  return 0; 
}

// 输出: 
// MyClass::new 
// MyClass::delete


operator

  • 操作符重载实现类成员函数

    重载的操作符在类体中被声明,声明方式如同普通成员函数一样,只不过他的名字包含关键字operator,以及紧跟其后的一个C++预定义的操作符。

    可以用如下的方式来声明一个预定义的==操作符:

    class person{
    private:
        int age;
        public:
        person(int a)
        {
           this->age=a;
        }
       inline bool operator == (const person &ps) const;
    };
    
    inline bool person::operator==(const person &ps) const
    {
    
         if (this->age==ps.age)
            return true;
         return false;
    }
    
    #include<iostream>
    
    using namespace std;
    
    int main()
    {
      person p1(10);
      person p2(20);
      if(p1==p2)
     cout<<”the age is equal!< return 0;
    }
  • 操作符重载实现非类成员函数(全局函数)

    对于全局重载操作符,代表左操作数的参数必须被显式指定。例如:

    #include<iostream>
    
    using namespace std;
    
    class person {
    public:
    	int age;
    
    public:
    	person(int _age = 0)
    		:age(_age)
    	{
    		cout << "person(int _age )"<< endl;
    	}
    
    	person(person& ps)
    	{
    		*this = ps;
    	}
    };
    
    bool operator==(person& p1, person const & p2)   //全局重载操作符==
    {
    	if (p1.age == p2.age)
    	{
    		return true; 	 //满足要求
    	}
    	return false;
    }
    
    int main() {
    	person rose;
    	person jack;
    	rose.age = 18;
    	jack.age = 23;
    	if (rose == jack)
    	{
    		cout << " is equal " << endl;
    	}
    	cout << "not equal " << endl;
    	return 0;
    }
  • 如何决定把一个操作符重载为类成员函数还是全局空间的成员呢?
    • 如果一个重载操作符是类成员,那么只有当与它的左操数是该类的对象时,该操作符才会被调用。如果该操作符的左操作数必须是其他的类型,则操作符必须被重载为全局空间的成员。
    • C++要求赋值=,下标[],调用(), 和成员指向-> 操作符必须被定义为类成员操作符。任何把这些操作符定义为名字空间成员的定义都会被标记为编译时刻错误。
    • 如果有一个操作数是class类型,如string类型,那么对于对称操作符比如=操作符最好定义为全局空间成员
  • 操作符重载适用范围,除了下面不可重载的运算符:
    • .(成员访问运算符)
    • . , ->(成员指针访问运算符)
    • ::(域运算符)
    • sizeof(长度运算符)
    • ?:(条件运算符)
    • #(预处理符号)
    • typeid(对象类型操作符)
  • 重载运算符的限制
    • 只有C++预定义的操作符才可以被重载;
    • 对于内置类型的操作符,它的预定义不能改变,即不能改变操作符原来的功能;
    • 重载操作符不能改变他们的操作符优先级;
    • 重载操作符不能改变操作数的个数;
    • 除了对()操作符外,对其他重载操作符提供缺省实参都是非法的
  • 注意

    当返回值不是本函数内定义的局部变量时就可以返回一个引用。在通常情况下,引用返回值只用在需对函数的调用重新赋值的场合,也就是对函数的返回值重新赋值的时候。(以重载=为例)

    如果返回值:返回的局部对象,在赋值语句结束之后释放,函数返回时保存的临时变量为该对象; 如果返回引用:返回的局部对象,在函数返回时释放,函数返回时保存的临时变量为该对象的引用(地址);

    在增量运算符中,放上一个整数形参,就是后增量运行符,它是值返回,对于前增量没有形参,而且是引用返回

    class Test {
    public:
    	Test(int x = 3) { 
    		m_value = x;
    	}
    	Test& operator++();   //前增量
    	Test operator++(int); //后增量
    private:
    	int m_value;
    };
    
    Test& Test::operator++() {
    	m_value++;     //先增量
    	return *this;  //返回当前对象
    }
    
    Test Test::operator++(int) {
    	Test temp(*this); //创建临时对象
    	m_value++;        //再增量
    	return temp;      //返回临时对象
    }

C++标准库

堆和栈

在 C 和 C++ 中,堆和栈的区别主要体现在以下几个方面:

  • 声明方式:堆内存使用 malloc 等函数动态分配,栈内存由编译器自动分配。
  • 释放方式:堆内存需要程序员手动释放,栈内存由编译器自动释放。
  • 存储位置:堆内存位于程序可用内存的最高地址,栈内存位于程序可用内存的最低地址。
  • 存储方式:堆内存使用链表结构,栈内存使用连续的地址空间。
  • 适用场景:堆内存用于存储动态分配的数据结构,栈内存用于存储函数调用时使用的参数、局部变量等。

模板

Class模板

类模板template <typename> class class-name { xxx };

类模板的成员函数、成员类和静态数据成员可以从其成员定义中显式实例化。

显式实例化只能出现在模板的封闭命名空间中,除非它使用qualified-id:

namespace N
{
    template<class T>
    class Y // template definition
    {
        void mf() {}
    };
}

// template class Y<int>; // error: class template Y not visible in the global namespace
using N::Y;
// template class Y<int>; // error: explicit instantiation outside
                          // of the namespace of the template
template class N::Y<char*>;       // OK: explicit instantiation
template void N::Y<double>::mf(); // OK: explicit instantiation

如果同一组模板参数之前出现过显式特化,则显式实例化无效。

当代码在需要完整定义类型的上下文中引用模板时,或者当类型的完整性影响代码并且该特定类型尚未显式实例化时,就会发生隐式实例化。例如,当构造此类型的对象时,而不是构造指向此类型的指针时。

这适用于类模板的成员:除非在程序中使用该成员,否则它不会被实例化,并且不需要定义。

template<class T>
struct Z // template definition
{
    void f() {}
    void g(); // never defined
};

template struct Z<double>; // explicit instantiation of Z<double>
Z<int> a;                  // implicit instantiation of Z<int>
Z<char>* p;                // nothing is instantiated here

p->f(); // implicit instantiation of Z<char> and Z<char>::f() occurs here.
        // Z<char>::g() is never needed and never instantiated:
        // it does not have to be defined

如果在实例化时已声明但未定义类模板,则实例化会产生不完整的类类型:

template<class T>
class X;    // declaration, not definition

X<char> ch; // error: incomplete type X<char>

模板特化

完全特化

#include <iostream>

// 类模板
template <typename T>
class MyClass {
public:
    void display() {
        std::cout << "Generic template" << std::endl;
    }
};

// 特化版本
template <>
class MyClass<int> {
public:
    void display() {
        std::cout << "Specialized template for int" << std::endl;
    }
};

int main() {
    MyClass<double> obj1;
    MyClass<int> obj2;

    obj1.display(); // 使用通用模板
    obj2.display(); // 使用特化模板

    return 0;
}

部分特化

#include <iostream>

// 通用类模板
template <typename T, typename U>
class Pair {
public:
    void display() {
        std::cout << "Generic template" << std::endl;
    }
};

// 部分特化:当两个参数类型相同时
template <typename T>
class Pair<T, T> {
public:
    void display() {
        std::cout << "Partial specialization for same types" << std::endl;
    }
};

int main() {
    Pair<int, double> obj1;
    Pair<int, int> obj2;

    obj1.display(); // 通用模板
    obj2.display(); // 部分特化

    return 0;
}

模板参数可以有默认值。

#include <iostream>

// 类模板带默认参数
template <typename T = int, typename U = double>
class Pair {
public:
    T first;
    U second;

    Pair(T f, U s) : first(f), second(s) {}

    void display() {
        std::cout << "First: " << first << ", Second: " << second << std::endl;
    }
};
Member模板

模板声明(函数变量(C++14 起) )可以出现在非本地类的任何类、结构或联合的成员规范内。

#include <algorithm>
#include <iostream>
#include <string>
#include <vector>

struct Printer
{
    // generic functor
std::ostream& os;
    Printer(std::ostream& os) : os(os) {}
    template<typename T>
    void operator()(const T& obj) { os << obj << ' '; } // member template
};

int main()
{
std::vector<int> v{1,2,3};
std::for_each(v.begin(), v.end(), Printer(std::cout));
std::string s{"abc"};
    std::ranges::for_each(s, Printer(std::cout));
}

成员模板的部分特化可能出现在类范围和封闭的命名空间范围中。显式特化可以出现在主模板可能出现的任何范围中。

struct A
{
    template<class T> struct B;        // primary member template
    template<class T> struct B<T*> {}; // OK: partial specialization
//  template<> struct B<int*> {};      // OK via CWG 727: full specialization
};
template<> struct A::B<int*> {};       // OK
template<class T> struct A::B<T&> {};  // OK

如果封闭类声明又是一个类模板,则当在类主体之外定义成员模板时,它需要两组模板参数:一组用于封闭类,另一组用于自身:

template<typename T1>
struct string
{
    // member template function
    template<typename T2>
    int compare(const T2&);
    // constructors can be templates too
    template<typename T2>
    string(conststd::basic_string<T2>& s) {/*...*/ }
};
// out of class definition of string<T1>::compare<T2>
template<typename T1> // for the enclosing class template
template<typename T2> // for the member template
int string<T1>::compare(const T2& s) {/* ... */ }

成员函数模板

析构函数复制构造函数不能是模板。如果声明了可以使用复制构造函数的类型签名实例化的模板构造函数,则使用隐式声明的复制构造函数

成员函数模板不能是虚拟的,并且派生类中的成员函数模板不能覆盖基类中的虚拟成员函数。

class Base
{
    virtual void f(int);
};

struct Derived : Base
{
    // this member template does not override Base::f
    template<class T> void f(T);

    // non-template member override can call the template:
    void f(int i) override
    {
         f<>(i);
    }
};

可以声明同名的非模板成员函数和模板成员函数。如果发生冲突(当某些模板专门化与非模板函数签名完全匹配时),除非提供显式模板参数列表,否则该名称和类型的使用将引用非模板成员。

template<typename T>
struct A
{
    void f(int); // non-template member

    template<typename T2>
    void f(T2); // member template
};

// template member definition
template<typename T>
template<typename T2>
void A<T>::f(T2)
{
    // some code
}

int main()
{
    A<char> ac;
    ac.f('c'); // calls template function A<char>::f<char>(char)
    ac.f(1);   // calls non-template function A<char>::f(int)
    ac.f<>(1); // calls template function A<char>::f<int>(int)
}

成员函数模板的类外定义必须与类内部的声明等效(有关等效性的定义,请参阅函数模板重载),否则将被视为重载。

struct X
{
    template<class T> T good(T n);
    template<class T> T bad(T n);
};

template<class T> struct identity { using type = T; };

// OK: equivalent declaration
template<class V>
V X::good(V n) { return n; }

// Error: not equivalent to any of the declarations inside X
template<class T>
T X::bad(typename identity<T>::type n) { return n; }

转化函数模板

用户定义的转换函数可以是模板。

struct A
{
    template<typename T>
    operator T*(); // conversion to pointer to any type
};

// out-of-class definition
template<typename T>
A::operator T*() { return nullptr; }

// explicit specialization for char*
template<>
A::operator char*() { return nullptr; }

// explicit instantiation
template A::operator void*();

int main()
{
    A a;
    int* ip = a.operator int*(); // explicit call to A::operator int*()
}

重载解析期间,名称查找无法找到转换函数模板的特化。相反,会考虑所有可见的转换函数模板,并且使用模板参数推导(对转换函数模板具有特殊规则)生成的每个特化,就像通过名称查找找到的一样。

派生类中的 using 声明不能引用基类中模板转换函数的特化。

用户定义的转换函数模板不能具有推导的返回类型:

struct S //c++ 14起
{
    operator auto() const { return 10; } // OK
    template<class T> operator auto() const { return 42; } // error
};
变量模板

从变量模板实例化的变量称为实例化变量。从静态数据成员模板实例化的静态数据成员称为实例化静态数据成员

变量模板可以通过命名空间范围内的模板声明来引入,其中变量声明声明变量。

template<class T>
constexpr T pi = T(3.1415926535897932385L); // variable template

template<class T>
T circular_area(T r) // function template
{
    return pi<T> * r * r; // pi<T> is a variable template instantiation
}

当在类范围内使用时,变量模板声明静态数据成员模板。

using namespace std::literals;
struct matrix_constants
{
    template<class T>
    using pauli = hermitian_matrix<T, 2>; // alias template

    template<class T> // static data member template
    static constexpr pauli<T> sigmaX = {{0, 1}, {1, 0}};

    template<class T>
    static constexpr pauli<T> sigmaY = {{0, -1i}, {1i, 0}};

    template<class T>
    static constexpr pauli<T> sigmaZ = {{1, 0}, {0, -1}};
};

与其他静态成员一样,可能需要静态数据成员模板的定义。这样的定义是在类定义之外提供的。命名空间范围内的静态数据成员的模板声明也可以是类模板的非模板数据成员的定义:

struct limits
{
    template<typename T>
    static const T min; // declaration of a static data member template
};

template<typename T>
const T limits::min = { }; // definition of a static data member template

template<class T>
class X
{
    static T s; // declaration of a non-template static data member of a class template
};

template<class T>
T X<T>::s = 0; // definition of a non-template data member of a class template

除非变量模板被显式专门化或显式实例化,否则当在需要变量定义存在的上下文中引用变量模板的专门化时,或者如果定义的存在影响程序的语义,即如果该变量是表达式常量求值所必需的(可以不使用定义)。

如果表达式的常量求值需要变量,则变量定义的存在被认为会影响程序的语义,即使不需要表达式的常量求值或者常量表达式求值不使用定义。

笔记

在 C++14 中引入变量模板之前,参数化变量通常被实现为类模板的静态数据成员或返回所需值的 constexpr 函数模板。

变量模板不能用作模板模板参数

函数模板

模板函数定义

template <typename T> T Max(T a, T b) { xxx }

缩写函数模板

当占位符类型( autoConcept auto )出现在函数声明或函数模板声明的参数列表中时,该声明声明一个函数模板,并且为每个占位符发明的一个模板参数将附加到模板参数列表中:

void f1(auto); // same as template<class T> void f1(T)
void f2(C1 auto); // same as template<C1 T> void f2(T), if C1 is a concept
void f3(C2 auto...); // same as template<C2... Ts> void f3(Ts...), if C2 is a concept
void f4(const C3 auto*, C4 auto&); // same as template<C3 T, C4 U> void f4(const T*, U&);

template<class T, C U>
void g(T x, U y, C auto z); // same as template<class T, C U, C W> void g(T x, U y, W z);

缩写函数模板可以像所有函数模板一样进行特化。

template<>
void f4<int>(const int*, const double&); // specialization of f4<int, const double>

函数模板实例化

函数模板本身不是类型或函数。仅包含模板定义的源文件不会生成任何代码。为了出现任何代码,必须实例化模板:必须确定模板参数,以便编译器可以从类模板生成实际的函数(或类)。

template 返回类型名称<参数列表> (参数列表); (1)
template 返回类型名称(参数列表); (2)
extern template 返回类型名称<参数列表> (参数列表); (3)
extern template 返回类型名称(参数列表); (4)

1)显式实例化定义(如果显式指定每个非默认模板参数,则没有模板参数推导

2)显式实例化定义,对所有参数进行模板实参推导

3)显式实例化声明(如果显式指定每个非默认模板参数,则没有模板参数推导)

4)显式实例化声明,对所有参数进行模板实参推导

显式实例化定义强制实例化它们所引用的函数或成员函数。它可以出现在程序中模板定义之后的任何位置,并且对于给定的参数列表,只允许在程序中出现一次,不需要诊断。

显式实例化声明(外部模板)可防止隐式实例化:否则会导致隐式实例化的代码必须使用程序中其他位置提供的显式实例化定义。 (自 C++11 起)

如果可以从函数参数推导出尾随模板参数,则可以在函数模板特化或成员函数模板特化的显式实例化中保留未指定的尾随模板参数:

template<typename T>
void f(T s)
{
std::cout << s << '\n';
}

template void f<double>(double); // instantiates f<double>(double)
template void f<>(char);         // instantiates f<char>(char),template argument deduced
template void f(int);            // instantiates f<int>(int), template argument deduced

函数模板或类模板的成员函数的显式实例化不能使用inlineconstexpr 。如果显式实例化的声明命名了隐式声明的特殊成员函数,则程序格式错误。

构造函数的显式实例化不能使用模板参数列表(语法(1) ),这也不是必需的,因为它们可以被推导(语法(2) )。

预期析构函数的显式实例化必须命名该类的选定析构函数。 (自 C++20 起)

显式实例化声明不会抑制内联函数、自动声明、引用和类模板特化的隐式实例化。 (因此,当作为显式实例化声明的主题的内联函数使用 ODR 时,它会被隐式实例化以进行内联,但在此翻译单元中不会生成其外联副本)

具有默认参数的函数模板的显式实例化定义不是对参数的使用,并且不会尝试初始化它们:

char* p = 0;
 
template<class T>
T g(T x = &p) { return x; }
 
template int g<int>(int); // OK even though &p isn’t an int.

隐式实例化

隐式实例化

当代码在需要函数定义存在的上下文中引用函数时,或者如果定义的存在影响程序的语义(C++11 起) ,并且该特定函数尚未显式实例化,则会发生隐式实例化。如果可以从上下文中推导出模板参数列表,则不必提供模板参数列表。

运行这段代码

#include <iostream>
 
template<typename T>
void f(T s)
{
    std::cout << s << '\n';
}
 
int main()
{
    f<double>(1); // instantiates and calls f<double>(double)
    f<>('a');     // instantiates and calls f<char>(char)
    f(7);         // instantiates and calls f<int>(int)
    void (*pf)(std::string) = f; // instantiates f<string>(string)
    pf("∇");                     // calls f<string>(string)
}
如果表达式需要对函数进行常量求值,则函数定义的存在被认为会影响程序的语义,即使不需要表达式的常量求值或者常量表达式求值不使用定义。 (自 C++11 起)
template<typename T>
constexpr int f() { return T::value; }
 
template<bool B, typename T>
void g(decltype(B ? f<T>() : 0));
template<bool B, typename T>
void g(...);
 
template<bool B, typename T>
void h(decltype(int{B ? f<T>() : 0}));
template<bool B, typename T>
void h(...);
 
void x()
{
    g<false, int>(0); // OK: B ? f<T>() : 0 is not potentially constant evaluated
    h<false, int>(0); // error: instantiates f<int> even though B evaluates to false
                      // and list-initialization of int from int cannot be narrowing
}

注意:完全省略<>允许重载解析来检查模板和非模板重载。

模板参数推导

为了实例化函数模板,必须知道每个模板参数,但不必指定每个模板参数。如果可能,编译器将从函数参数中推断出缺少的模板参数。当尝试调用函数以及获取函数模板的地址时,就会发生这种情况。

template<typename To, typename From>
To convert(From f);
 
void g(double d) 
{
    int i = convert<int>(d);    // calls convert<int,double>(double)
    char c = convert<char>(d);  // calls convert<char,double>(double)
    int(*ptr)(float) = convert; // instantiates convert<int, float>(float)
}

这种机制使得使用模板运算符成为可能,因为除了将其重写为函数调用表达式之外,没有任何语法可以为运算符指定模板参数。

#include <iostream>
 
int main() 
{
    std::cout << "Hello, world" << std::endl;
    // operator<< is looked up via ADL as std::operator<<,
    // then deduced to operator<<<char, std::char_traits<char>> both times
    // std::endl is deduced to &std::endl<char, std::char_traits<char>>
}

模板参数推导发生在函数模板名称查找(可能涉及参数相关查找)之后和重载决策之前。

有关详细信息,请参阅模板参数推导

显式模板参数

函数模板的模板参数可以从以下位置获得

  • 模板实参推导
  • 默认模板参数
  • 明确指定,可以在以下上下文中完成:
    • 在函数调用表达式中
    • 当获取函数地址时
    • 当函数的引用被初始化时
    • 当指向成员函数的指针形成时
    • 明确的专业化
    • 在显式实例化中
    • 在朋友宣言中

无法为重载运算符转换函数和构造函数显式指定模板参数,因为它们是在不使用函数名称的情况下调用的。

指定的模板实参必须在种类上与模板形参相匹配(即类型对类型、非类型对非类型、模板对模板)。参数的数量不能多于参数的数量(除非一个参数是参数包,在这种情况下,每个非参数包都必须有一个参数) (C++11 起) 。

指定的非类型参数必须与相应的非类型模板参数的类型匹配,或者可以转换为它们

不参与模板参数推导的函数参数(例如,如果显式指定相应的模板参数)会被隐式转换为相应函数参数的类型(如通常的重载解析)。

如果有其他参数,则显式指定的模板参数包可以通过模板参数推导来扩展: (自 C++11 起)
template<class... Types>
void f(Types... values);
 
void g()
{
    f<int*, float*>(0, 0, 0); // Types = {int*, float*, int}
}
模板参数替换

当所有模板参数都已被指定、推导或从默认模板参数获取时,函数参数列表中模板参数的每次使用都将替换为相应的模板参数。

函数模板的替换失败(即,未能用推导或提供的模板实参替换模板参数)会从重载集中删除该函数模板。这允许使用模板元编程通过多种方式操作重载集:有关详细信息,请参阅SFINAE 。

替换后,数组和函数类型的所有函数参数都将调整为指针,并且所有顶级 cv 限定符都会从函数参数中删除(如常规函数声明中一样)。

删除顶级 cv 限定符不会影响函数中出现的参数类型:

template<class T>
void f(T t);
 
template<class X>
void g(const X x);
 
template<class Z>
void h(Z z, Z* zp);
 
// two different functions with the same type, but 
// within the function, t has different cv qualifications
f<int>(1);       // function type is void(int), t is int
f<const int>(1); // function type is void(int), t is const int
 
// two different functions with the same type and the same x
// (pointers to these two functions are not equal,
//  and function-local statics would have different addresses)
g<int>(1);       // function type is void(int), x is const int
g<const int>(1); // function type is void(int), x is const int
 
// only top-level cv-qualifiers are dropped:
h<const int>(1, NULL); // function type is void(int, const int*) 
                       // z is const int, zp is const int*
函数模板重载

函数模板和非模板函数可能会重载。

非模板函数始终与具有相同类型的模板特化不同。不同函数模板的特化总是彼此不同,即使它们具有相同的类型。具有相同返回类型和相同参数列表的两个函数模板是不同的,可以通过它们的显式模板参数列表来区分。

当使用类型或非类型模板参数的表达式出现在函数参数列表或返回类型中时,该表达式仍然是函数模板签名的一部分,以便进行重载:

template<int I, int J>
A<I+J> f(A<I>, A<J>); // overload #1
 
template<int K, int L>
A<K+L> f(A<K>, A<L>); // same as #1
 
template<int I, int J>
A<I-J> f(A<I>, A<J>); // overload #2

如果包含模板参数的两个函数定义在ODR下相同,则两个涉及模板参数的表达式被称为等效,也就是说,这两个表达式包含相同的标记序列,其名称通过名称查找解析为相同的实体,但模板参数可能除外以不同的方式命名。两个lambda 表达式永远不等价。 (自 C++20 起)

template<int I, int J>
void f(A<I+J>); // template overload #1
 
template<int K, int L>
void f(A<K+L>); // equivalent to #1

在确定两个依赖表达式是否等效时,仅考虑所涉及的依赖名称,而不考虑名称查找的结果。如果同一模板的多个声明在名称查找结果中不同,则使用第一个这样的声明:

template<class T>
decltype(g(T())) h(); // decltype(g(T())) is a dependent type
 
int g(int);
 
template<class T>
decltype(g(T())) h()
{                  // redeclaration of h() uses earlier lookup
    return g(T()); // although the lookup here does find g(int)
}
 
int i = h<int>(); // template argument substitution fails; g(int)
                  // was not in scope at the first declaration of h()

两个函数模板被认为是等效的,如果

  • 它们在同一范围内声明
  • 他们有相同的名字
  • 它们具有等效的模板参数列表,这意味着列表具有相同的长度,并且对于每个相应的参数对,以下所有条件均成立:
    • 两个参数属于同一类型(都是类型、都是非类型或都是模板)
    • 它们要么都是参数包,要么都不是(自 C++11 起)
    • 如果非类型,则它们的类型是等效的,
    • 如果是模板,则它们的模板参数是等效的,
    • 如果用概念名称声明一个,则它们都是,并且概念名称是等效的。(自 C++20 起)
  • 返回类型和参数列表中涉及模板参数的表达式是等效的
    • 模板参数列表后面的 require 子句中的表达式(如果存在)是等效的(自 C++20 起)
    • 函数声明符后面的 require 子句中的表达式(如果存在)是等效的(自 C++20 起)

涉及模板参数的两个潜在求值(C++20 起)表达式如果不等价,则称为功能等效,但对于任何给定的模板参数集,两个表达式的求值会产生相同的值。

如果两个函数模板等效,则认为它们在功能上等效,但返回类型和参数列表中涉及模板参数的一个或多个表达式在功能上等效

此外,两个函数模板在功能上是等效的,但如果它们的约束指定不同,但它们接受并满足同一组模板参数列表,则它们是不等效的 (自 C++20 起)

如果程序包含功能等效但不等效的函数模板声明,则该程序是格式错误的;无需诊断。

// equivalent
template<int I>
void f(A<I>, A<I+10>); // overload #1
template<int I>
void f(A<I>, A<I+10>); // redeclaration of overload #1
 
// not equivalent
template<int I>
void f(A<I>, A<I+10>); // overload #1
template<int I>
void f(A<I>, A<I+11>); // overload #2
 
// functionally-equivalent but not equivalent
// This program is ill-formed, no diagnostic required
template<int I>
void f(A<I>, A<I+10>);      // overload #1
template<int I>
void f(A<I>, A<I+1+2+3+4>); // functionally equivalent

当相同的函数模板特化匹配多个重载函数模板时(这通常是由模板参数推导引起的),将执行重载函数模板的部分排序以选择最佳匹配。

具体来说,部分排序发生在以下情况:

1)调用函数模板特化的重载决策

template<class X>
void f(X a);
template<class X>
void f(X* a);
 
int* p;
f(p);

2)当采用函数模板特化的地址时:

template<class X>
void f(X a);
template<class X>
void f(X* a);
 
void (*p)(int*) = &f;

3)当选择作为函数模板特化的放置操作符删除来匹配放置操作符新时:

4)当友元函数声明显式实例化显式特化引用函数模板特化时:

template<class X>
void f(X a);  // first template f
template<class X>
void f(X* a); // second template f
template<>
void f<>(int *a) {} // explicit specialization
 
// template argument deduction comes up with two candidates:
// f<int*>(int*) and f<int>(int*)
// partial ordering selects f<int>(int*) as more specialized

通俗地说,“A 比 B 更专业”意味着“A 接受的类型比 B 少”。

形式上,为了确定任意两个函数模板中哪一个更专业,部分排序过程首先按如下方式转换两个模板之一:

  • 对于每个类型、非类型和模板参数,包括参数包, (自 C++11 起)生成唯一的虚构类型、值或模板并将其替换为模板的函数类型
  • 如果所比较的两个函数模板中只有一个是成员函数,并且该函数模板是某个类A的非静态成员,则新的第一个参数将插入到其参数列表中。给定cv作为函数模板的 cv 限定符,将ref作为函数模板的 ref 限定符(C++11 起) ,新参数类型为cv A&除非ref&& ,或者ref不存在且第一个另一个模板的参数具有右值引用类型,在本例中,类型为cv A&& (C++11 起) 。这有助于运算符的排序,这些运算符作为成员函数和非成员函数进行查找:
struct A {};
 
template<class T>
struct B
{
    template<class R>
    int operator*(R&); // #1
};
 
template<class T, class R>
int operator*(T&, R&); // #2
 
int main()
{
    A a;
    B<A> b;
    b * a; // template argument deduction for int B<A>::operator*(R&) gives R=A 
           //                             for int operator*(T&, R&), T=B<A>, R=A
 
    // For the purpose of partial ordering, the member template B<A>::operator*
    // is transformed into template<class R> int operator*(B<A>&, R&);
 
    // partial ordering between 
    //     int operator*(   T&, R&)  T=B<A>, R=A
    // and int operator*(B<A>&, R&)  R=A 
    // selects int operator*(B<A>&, A&) as more specialized
}

如上所述对两个模板之一进行转换后,使用转换后的模板作为实参模板,另一个模板的原始模板类型作为参数模板来执行模板实参推导。然后使用第二个模板(转换后)作为参数,使用原始形式的第一个模板作为参数来重复该过程。

用于确定顺序的类型取决于上下文:

  • 在函数调用的上下文中,类型是函数调用具有参数的函数参数类型(不考虑默认函数参数、参数包(C++11 起)和省略号参数 - 请参阅下面的示例)
  • 在调用用户定义的转换函数的上下文中,使用转换函数模板的返回类型
  • 在其他上下文中,使用函数模板类型

上面列表中的每种类型都是从参数模板中推导出来的。在推导开始之前,参数模板的各个参数P以及参数模板对应的参数A调整如下:

  • 如果PA之前都是引用类型,则确定哪个更符合 cv 限定(在所有其他情况下,为了部分排序目的,cv 限定将被忽略)
  • 如果P是引用类型,则将其替换为引用的类型
  • 如果A是引用类型,则将其替换为引用的类型
  • 如果P是 cv 限定的,则P将被替换为其自身的 cv 限定版本
  • 如果A是 cv 限定的,则A将被替换为其自身的 cv 限定版本

经过这些调整后,从A中推导P是在从类型中进行模板参数推导之后完成的。

如果P是函数参数包,则将参数模板的每个剩余参数类型的类型A与函数参数包的 declarator-id 的类型P进行比较。每次比较都会推导出由函数参数包扩展的模板参数包中后续位置的模板参数。
如果A是从函数参数包转换而来,则将其与参数模板的每个剩余参数类型进行比较。
(自 C++11 起)

如果转换后的 template-1 的参数A可用于推导 template-2 的相应参数P ,但反之则不然,则就由此推导的类型而言,此AP更专业P/A对。

如果两个方向推演都成功,并且原始PA是引用类型,则进行附加测试:

  • 如果A是左值引用并且P是右值引用,则A被认为比P更专业
  • 如果AP更符合 cv 条件,则A被认为比P更专业

在所有其他情况下,就该P/A对推导的类型而言,两个模板都不比另一个模板更专业。

在考虑了两个方向上的每个PA之后,如果对于所考虑的每种类型,

  • 对于所有类型,template-1 至少与 template-2 一样专业
  • 对于某些类型,template-1 比 template-2 更专业
  • 对于任何类型,template-2 都不比 template-1 更专业,或者对于任何类型至少不那么专业

那么 template-1 比 template-2 更专业。如果切换模板顺序后上述条件成立,则 template-2 比 template-1 更专业。否则,两个模板都不比另一个更专业。

在平局的情况下,如果一个函数模板具有尾随参数包,而另一个函数模板没有,则具有省略参数的函数模板被认为比具有空参数包的函数模板更专业。 (自 C++11 起)

如果在考虑所有对重载模板之后,有一个明显比所有其他模板更专业,则选择该模板的专业化,否则编译将失败。

在以下示例中,虚构参数将称为 U1、U2:

template<class T>
void f(T);        // template #1
template<class T>
void f(T*);       // template #2
template<class T>
void f(const T*); // template #3

void m()
{
    const int* p;
    f(p); // overload resolution picks: #1: void f(T ) [T = const int *]
          //                            #2: void f(T*) [T = const int]
          //                            #3: void f(const T *) [T = int]

    // partial ordering:

    // #1 from transformed #2: void(T) from void(U1*): P=T A=U1*: deduction ok: T=U1*
    // #2 from transformed #1: void(T*) from void(U1): P=T* A=U1: deduction fails
    // #2 is more specialized than #1 with regards to T

    // #1 from transformed #3: void(T) from void(const U1*): P=T, A=const U1*: ok
    // #3 from transformed #1: void(const T*) from void(U1): P=const T*, A=U1: fails
    // #3 is more specialized than #1 with regards to T

    // #2 from transformed #3: void(T*) from void(const U1*): P=T* A=const U1*: ok
    // #3 from transformed #2: void(const T*) from void(U1*): P=const T* A=U1*: fails
    // #3 is more specialized than #2 with regards to T

    // result: #3 is selected
    // in other words, f(const T*) is more specialized than f(T) or f(T*)
}
template<class T>
void f(T, T*);   // #1
template<class T>
void f(T, int*); // #2

void m(int* p)
{
    f(0, p); // deduction for #1: void f(T, T*) [T = int]
             // deduction for #2: void f(T, int*) [T = int]

    // partial ordering:

    // #1 from #2: void(T,T*) from void(U1,int*): P1=T, A1=U1: T=U1
    //                                            P2=T*, A2=int*: T=int: fails

    // #2 from #1: void(T,int*) from void(U1,U2*): P1=T A1=U1: T=U1
    //                                             P2=int* A2=U2*: fails

    // neither is more specialized w.r.t T, the call is ambiguous
}
template<class T>
void g(T);  // template #1
template<class T>
void g(T&); // template #2

void m()
{
    float x;
    g(x); // deduction from #1: void g(T ) [T = float]
          // deduction from #2: void g(T&) [T = float]

    // partial ordering:

    // #1 from #2: void(T) from void(U1&): P=T, A=U1 (after adjustment), ok

    // #2 from #1: void(T&) from void(U1): P=T (after adjustment), A=U1: ok

    // neither is more specialized w.r.t T, the call is ambiguous
}
template<class T>
struct A { A(); };

template<class T>
void h(const T&); // #1
template<class T>
void h(A<T>&);    // #2

void m()
{
    A<int> z;
    h(z); // deduction from #1: void h(const T &) [T = A<int>]
          // deduction from #2: void h(A<T> &) [T = int]

    // partial ordering:

    // #1 from #2: void(const T&) from void(A<U1>&): P=T A=A<U1>: ok T=A<U1>

    // #2 from #1: void(A<T>&) from void(const U1&): P=A<T> A=const U1: fails

    // #2 is more specialized than #1 w.r.t T

    const A<int> z2;
    h(z2); // deduction from #1: void h(const T&) [T = A<int>]
           // deduction from #2: void h(A<T>&) [T = int], but substitution fails

    // only one overload to choose from, partial ordering not tried, #1 is called
}

由于调用上下文仅考虑有显式调用参数的参数,因此将忽略那些没有显式调用参数的函数参数包、 (C++11 起)省略号参数和具有默认参数的参数:

template<class T>
void f(T);         // #1
template<class T>
void f(T*, int = 1); // #2

void m(int* ip)
{
    int* ip;
    f(ip); // calls #2 (T* is more specialized than T)
}
template<class T>
void g(T);       // #1
template<class T>
void g(T*, ...); // #2

void m(int* ip)
{
    g(ip); // calls #2 (T* is more specialized than T)
}
template<class T, class U>
struct A {};

template<class T, class U>
void f(U, A<U, T>* p = 0); // #1
template<class U>
void f(U, A<U, U>* p = 0); // #2

void h()
{
    f<int>(42, (A<int, int>*)0); // calls #2
    f<int>(42);                  // error: ambiguous
}
template<class T>
void g(T, T = T()); // #1
template<class T, class... U>
void g(T, U...);    // #2

void h()
{
    g(42); // error: ambiguous
}
template<class T, class... U>
void f(T, U...); // #1
template<class T>
void f(T);       // #2

void h(int i)
{
    f(&i); // calls #2 due to the tie-breaker between parameter pack and no parameter
           // (note: was ambiguous between DR692 and DR1395)
}
template<class T, class... U>
void g(T*, U...); // #1
template<class T>
void g(T);        // #2

void h(int i)
{
    g(&i); // OK: calls #1 (T* is more specialized than T)
}
template<class... T>
int f(T*...);    // #1
template<class T>
int f(const T&); // #2

f((int*)0); // OK: selects #2; non-variadic template is more specialized than
            // variadic template (was ambiguous before DR1395 because deduction
            // failed in both directions)
template<class... Args>
void f(Args... args);        // #1
template<class T1, class... Args>
void f(T1 a1, Args... args); // #2
template<class T1, class T2>
void f(T1 a1, T2 a2);        // #3

f();        // calls #1
f(1, 2, 3); // calls #2
f(1, 2);    // calls #3; non-variadic template #3 is more
            // specialized than the variadic templates #1 and #2

在部分排序过程中的模板参数推导过程中,如果参数未在任何考虑用于部分排序的类型中使用,则模板参数不需要与参数匹配

template<class T>
T f(int); // #1
template<class T, class U>
T f(U);   // #2

void g()
{
    f<int>(1); // specialization of #1 is explicit: T f(int) [T = int]
               // specialization of #2 is deduced:  T f(U) [T = int, U = int]

    // partial ordering (only considering the argument type):

    // #1 from #2: T(int) from U1(U2): fails
    // #2 from #1: T(U) from U1(int): ok: U=int, T unused

    // calls #1
}

包含模板参数包的函数模板的部分排序与这些模板参数包的推导参数的数量无关。

template<class...>
struct Tuple {};

template<class... Types>
void g(Tuple<Types...>);      // #1
template<class T1, class... Types>
void g(Tuple<T1, Types...>);  // #2
template<class T1, class... Types>
void g(Tuple<T1, Types&...>); // #3

g(Tuple<>());            // calls #1
g(Tuple<int, float>());  // calls #2
g(Tuple<int, float&>()); // calls #3
g(Tuple<int>());         // calls #3

要编译对函数模板的调用,编译器必须在非模板重载、模板重载和模板重载的特化之间做出决定。

template<class T>
void f(T);      // #1: template overload
template<class T>
void f(T*);     // #2: template overload
 
void f(double); // #3: non-template overload
template<>
void f(int);    // #4: specialization of #1
 
f('a');        // calls #1
f(new int(1)); // calls #2
f(1.0);        // calls #3
f(1);          // calls #4
函数重载与函数特化

请注意,只有非模板和主模板重载参与重载解析。专业化不是重载并且不被考虑。只有在重载决策选择最匹配的主函数模板之后,才会检查其特化以确定是否是更好的匹配。

template<class T>
void f(T);    // #1: overload for all types
template<>
void f(int*); // #2: specialization of #1 for pointers to int
template<class T>
void f(T*);   // #3: overload for all pointer types
 
f(new int(1)); // calls #3, even though specialization of #1 would be a perfect match

在订购翻译单元的头文件时记住这条规则很重要。有关函数重载和函数特化之间相互作用的更多示例,请展开如下:这里

类型别名、别名模板

类型别名是引用先前定义的类型的名称(类似于typedef )。

别名模板是指代一系列类型的名称。

别名声明是具有以下语法的声明

using 标识符attr (可选) = type-id ; (1)
template <模板参数列表>
using 标识符attr (可选) = type-id ;
(2)
template <模板参数列表> requires 约束
using 标识符attr (可选) = type-id ;
(3) (自 C++20 起)

1)类型别名声明引入了一个名称,该名称可以用作type-id表示的类型的同义词。它不会引入新类型,也不能更改现有类型名称的含义。类型别名声明和typedef声明之间没有区别。此声明可能出现在块作用域、类作用域或命名空间作用域中。

2)别名模板是一种模板,当特殊化时,相当于用别名模板的模板参数替换type-id中的模板参数的结果。

template<class T>
struct Alloc {};
 
template<class T>
using Vec = vector<T, Alloc<T>>; // type-id is vector<T, Alloc<T>>
 
Vec<int> v; // Vec<int> is the same as vector<int, Alloc<int>>

当专门化别名模板的结果是依赖template-id时,后续替换适用于该 template-id:

template<typename...>
using void_t = void;
 
template<typename T>
void_t<typename T::foo> f();
 
f<int>(); // error, int does not have a nested type foo

专门化别名模板时生成的类型不允许直接或间接使用其自己的类型:

template<class T>
struct A;

template<class T>
using B = typename A<T>::U; // type-id is A<T>::U

template<class T>
struct A { typedef B<T> U; };

B<short> b; // error: B<short> uses its own type via A<short>::U

在推导模板模板形参时,永远不会通过模板实参推导来推导出别名模板。

不可能部分显式地专门化别名模板。

与任何模板声明一样,别名模板只能在类范围或命名空间范围内声明。

出现在别名模板声明中的lambda 表达式的类型在该模板的实例化之间是不同的,即使 lambda 表达式不相关也是如此。

template<class T>
using A = decltype([] {}); // A<int> and A<char> refer to different closure types

STL

STL(标准模板库)由 C++ 标准库中的三个主要部分组成:

  1. 容器(Containers):
    容器是用于存储和管理数据的数据结构。STL 提供了多种类型的容器,包括向量(vector)、链表(list)、映射(map)、集合(set)、队列(queue)等。每种容器都具有不同的特点和适用场景,可以根据需要选择合适的容器进行数据存储和操作。
  2. 算法(Algorithms):
    算法是用于执行各种操作和算法的函数模板集合。STL 中的算法涵盖了排序、搜索、变换、合并等常见的操作,它们可以与不同类型的容器一起使用,使开发人员无需重新实现这些常见的操作,从而提高了代码的复用性和效率。
  3. 迭代器(Iterators):
    迭代器是一种抽象的访问容器元素的方式,类似于指针。STL 中的迭代器可以用于遍历容器的元素,使开发人员可以以一种统一的方式对不同类型的容器进行遍历和操作。迭代器使算法与容器的耦合度降低,提高了代码的灵活性。

此外,STL 还包括一些辅助组件,如分配器(Allocators)用于内存管理,函数对象(Function Objects)用于实现可调用对象,以及一些辅助类和工具函数等。这些组件共同构成了 C++ 标准库中强大的标准模板库。

Containers – Template Class

特征:通用数据结构(可以将它们用于任何类型)

Sequence containers

线性排列

vector, deque, list (头文件<vector> <deque> <list>)

Image in a image block
vector<T> v; //empty vector
vector<T> v(n, value);//vector with n copies of value
vector<T> v(n);//vector with n copies of default for T

push_back

  • push_back 是一个成员函数,用于向容器末尾添加一个元素,可以是通过复制构造函数生成的临时对象或者已存在的对象的拷贝。
  • 当你希望将已存在的对象添加到容器中,或者添加临时对象时,可以使用 push_back

emplace_back

  • emplace_back 是一个成员函数,允许你直接在容器中构造对象,避免了不必要的拷贝或移动操作。
  • emplace_back 接受构造函数的参数,并在容器内直接构造对象,这可以在一次函数调用中完成对象的构造和插入操作,减少了中间步骤。
  • 当你想要避免额外的复制或移动操作,并且可以直接在容器中构造对象时,推荐使用 emplace_back

以下是两者的示例用法:

#include <iostream>
#include <vector>

class MyClass {
public:
    MyClass(int value) : data(value) {
        std::cout << "Constructor called for value: " << value << std::endl;
    }

private:
    int data;
};

int main() {
    std::vector<MyClass> vec;

    vec.push_back(MyClass(1)); // 调用构造函数,然后拷贝临时对象到容器中
    vec.emplace_back(2); // 在容器中直接构造对象,避免了拷贝操作

    return 0;
}

总之,push_back 用于向容器添加已存在的对象或临时对象的拷贝,而 emplace_back 则允许在容器中直接构造对象,避免了不必要的拷贝操作,提高了效率。在能够使用 emplace_back 的情况下,它通常是更好的选择。

Container adapters(容器适配器,即上述容器的封装):stack, queue, priority_queue (头文件<stack> <queue>)

C++11:array (C-style array),forward_list (singly linked list) (头文件<array> <forward_list>)

  1. vector(向量)
    • 底层数据结构:动态数组。
    • 元素的访问:支持随机访问,可以通过索引访问元素。
    • 插入和删除操作:在尾部进行高效,但在中间或开头较慢,因为需要移动后续元素。
    • 动态增长:可以动态增长大小,但在插入元素时可能触发重新分配和复制。
  2. deque(双端队列)
    • 底层数据结构:双端队列,通常由多个动态数组组成的数组。
    • 元素的访问:支持随机访问,可以通过索引访问元素。
    • 插入和删除操作:在头部和尾部高效,但在中间较慢。
    • 动态增长:可以动态增长大小,但不像vector那样频繁触发重新分配。
  3. list(链表)
    • 底层数据结构:双向链表。
    • 元素的访问:只支持顺序访问,不能通过索引直接访问元素。
    • 插入和删除操作:在任何位置都很高效,因为不需要移动元素。
    • 动态增长:大小可以动态增长。
  4. array(数组)
    • 底层数据结构:固定大小的数组,大小在创建时确定。
    • 元素的访问:支持随机访问,可以通过索引访问元素。
    • 插入和删除操作:不支持插入或删除元素,因为大小是固定的。
  5. forward_list(单向链表)
    • 底层数据结构:单向链表。
    • 元素的访问:只支持顺序访问,不能通过索引直接访问元素。
    • 插入和删除操作:在任何位置都很高效,因为不需要移动元素。
    • 动态增长:大小可以动态增长。

Associative containers(关联容器)

提供基于键的快速数据检索

set, multiset, map, multimap (头文件<set> <map>)

Image in a image block
  1. set
    • set 是一个有序的关联容器,存储唯一的键(key)。
    • 键值对的键即为元素本身,不能有重复元素。
    • 底层实现通常是红黑树,因此元素是有序的。
    • 提供了高效的查找、插入和删除操作。
    • 适用于需要维护唯一值集合的情况。
  2. multiset
    • multiset 是一个有序的关联容器,可以存储重复的键(key)。
    • 键值对的键即为元素本身,可以有重复元素。
    • 底层实现通常是红黑树,因此元素是有序的。
    • 提供了高效的查找、插入和删除操作。
    • 适用于需要维护值的多重集合的情况。
  3. map
    • map 是一个有序的关联容器,存储键值对,每个键都是唯一的。
    • 提供了高效的查找、插入和删除操作。
    • 底层实现通常是红黑树,因此元素是有序的。
    • 适用于需要键值映射的情况,例如字典或关联数组。
  4. multimap
    • multimap 是一个有序的关联容器,可以存储重复的键值对。
    • 提供了高效的查找、插入和删除操作。
    • 键值对的键即为元素的键,可以有重复元素。
    • 底层实现通常是红黑树,因此元素是有序的。
    • 适用于需要多个键映射到相同值的情况。

C++11:unordered_set, unordered_multiset (hash table),unordered_map, unordered_multimap (hash table) (头文件<unordered_set> <unordered_map>

  • <array> template < class T, size_t N > class array;
  • <vector> template < class T, class Alloc = allocator<T> > class vector;
  • <deque> template < class T, class Alloc = allocator<T> > class deque;
  • <forward_list> template < class T, class Alloc = allocator<T> > class forward_list;
  • <list> template < class T, class Alloc = allocator<T> > class list;
  • <set> template < class T, class Compare = less<T>, class Alloc = allocator<T> > class set;

Container adaptors(容器适配器)

容器适配器为顺序容器提供了不同的接口。

stack LIFO stack (class template)

queue FIFO queue (class template)

priority_queue FIFO Priority queue (class template)

在C++ 中容器被定义为:在数据存储上,有一种对象类型,它可以持有其它对象或指向其它对像的指针,这种对象类型就叫做容器。很简单,容器就是保存其它对象的对象,当然这是一个朴素的理解,这种“对象”还包含了一系列处理“其它对象”的方法,因为这些方法在程序的设计上会经常被用到,所以容器也体现了一个好处,就是“容器类是一种对特定代码重用问题的良好的解决方案”。

容器还有另一个特点是容器可以自行扩展。在解决问题时我们常常不知道我们需要存储多少个对象,也就是说我们不知道应该创建多大的内存空间来保存我们的对象。显然,数组在这一方面也力不从心。容器的优势就在这里,它不需要你预先告诉它你要存储多少对象,只要你创建一个容器对象,并合理的调用它所提供的方法,所有的处理细节将由容器来自身完成。它可以为你申请内存或释放内存,并且用最优的算法来执行您的命令。

容器是随着面向对象语言的诞生而提出的,容器类在面向对象语言中特别重要,甚至它被认为是早期面向对象语言的基础。在现在几乎所有的面向对象的语言中也都伴随着一个容器集,在C++ 中,就是标准模板库(STL )。

和其它语言不一样,C++ 中处理容器是采用基于模板的方式。标准C++ 库中的容器提供了多种数据结构,这些数据结构可以与标准算法一起很好的工作,这为我们的软件开发提供了良好的支持!

STL 对定义的通用容器分三类:顺序性容器、关联式容器和容器适配器。

  • 顺序性容器

    顺序性容器 是一种各元素之间有顺序关系的线性表,是一种线性结构的可序群集。顺序性容器中的每个元素均有固定的位置,除非用删除或插入的操作改变这个位置。这个位置和元素本身无关,而和操作的时间和地点有关,顺序性容器不会根据元素的特点排序而是直接保存了元素操作时的逻辑顺序。比如我们一次性对一个顺序性容器追加三个元素,这三个元素在容器中的相对位置和追加时的逻辑次序是一致的。

  • 关联式容器

    关联式容器 和顺序性容器不一样,关联式容器是非线性的树结构,更准确的说是二叉树结构。各元素之间没有严格的物理上的顺序关系,也就是说元素在容器中并没有保存元素置入容器时的逻辑顺序。但是关联式容器提供了另一种根据元素特点排序的功能,这样迭代器就能根据元素的特点“顺序地”获取元素。

    关联式容器另一个显著的特点是它是以键值的方式来保存数据,就是说它能把关键字和值关联起来保存,而顺序性容器只能保存一种(可以认为它只保存关键字,也可以认为它只保存值)。这在下面具体的容器类中可以说明这一点。

  • 容器适配器

    容器适配器 是一个比较抽象的概念, C++的解释是:适配器是使一事物的行为类似于另一事物的行为的一种机制。容器适配器是让一种已存在的容器类型采用另一种不同的抽象类型的工作方式来实现的一种机制。其实仅是发生了接口转换。那么你可以把它理解为容器的容器,它实质还是一个容器,只是他不依赖于具体的标准容器类型,可以理解是容器的模版。或者把它理解为容器的接口,而适配器具体采用哪种容器类型去实现,在定义适配器的时候可以由你决定。

    迭代器为了统一遍历类似数组和链表而抽象出来的概念,通过统一的方法可以实现遍历数组和链表

Sequence containers(序列容器)
Headers Operations <array> <vector> <deque> <forward_list> <list>
Members array(数组) vector(数组) deque(双端队列) forward_list(前向链表) list(双向链表)
constructor implicit vector deque forward_list list
destructor implicit ~vector ~deque ~forward_list ~list
operator= implicit operator= operator= operator= operator=
iterators 返回指向容器中第一个元素的迭代器 begin begin begin begin beginbefore_begin
iterators 返回对容器中最后一个元素的引用 end end end end end
iterators rbegin rbegin rbegin rbegin rbegin
iterators rend rend rend rend rend
const iterators 返回指向容器中第一个元素的const_iterator cbegin cbegin cbegin cbegin cbegincbefore_begin
const iterators 返回指向容器中末尾之后元素的const_iterator cend cend cend cend cend
const iterators crbegin crbegin crbegin crbegin crbegin
const iterators crend crend crend crend crend
capacity size size size size size
capacity max_size max_size max_size max_size max_size
capacity empty empty empty empty empty
capacity resize resize resize resize resize
capacity shrink_to_fit shrink_to_fit shrink_to_fit shrink_to_fit shrink_to_fit
capacity capacity capacity capacity capacity capacity
capacity reserve reserve reserve reserve reserve
element access front front front front front
element access back back back back
element access operator[] operator[] operator[]
element access 返回容器对象中位置n处的元素的引用 at at at
modifiers assign assign assign assign
modifiers 在位置n插入新元素 emplace emplace emplace emplace_after emplace
modifiers insert insert insert insert_after insert
modifiers erase erase erase_after erase
modifiers 在末尾构造并插入元素(就地构造) emplace_back emplace_back emplace_back
modifiers 在容器末尾添加一个新元素,复制/移动到新元素 push_back push_back push_back
modifiers pop_back pop_back pop_back
modifiers emplace_front emplace_front emplace_front
modifiers push_front push_front push_front
modifiers pop_front pop_front pop_front
modifiers 删除所有元素 clear clear clear clear clear
modifiers swap swap swap swap swap
list operations splice splice_after
list operations remove remove
list operations remove_if remove_if
list operations unique unique
list operations merge merge
list operations sort sort
list operations reverse reverse
observers get_allocator get_allocator get_allocator get_allocator
observers data data
Associative containers(关联容器)
Headers <set> <set> <map> <map> <unordered_set> <unordered_set> <unordered_map> <unordered_map>
Members set(集合) multiset(元素可重复) map(字典) multimap(元素可重复) unordered_set(无序版本) unordered_multiset(无序版本) unordered_map(无序版本) unordered_multimap(无序版本)
constructor set multiset map multimap unordered_set unordered_multiset unordered_map unordered_multimap
destructor ~set ~multiset ~map ~multimap ~multimap ~unordered_multiset ~unordered_map ~unordered_multimap
assignment operator= operator= operator= operator= operator= operator= operator= operator=
iterators begin begin begin begin begin begin begin begin
iterators end end end end end end end end
iterators rbegin rbegin rbegin rbegin rbegin rbegin rbegin rbegin
iterators rend rend rend rend rend rend rend rend
const iterators cbegin cbegin cbegin cbegin cbegin cbegin cbegin cbegin
const iterators cend cend cend cend cend cend cend cend
const iterators crbegin crbegin crbegin crbegin crbegin crbegin crbegin crbegin
const iterators crend crend crend crend crend crend crend crend
capacity size size size size size size size size
capacity max_size max_size max_size max_size max_size max_size max_size max_size
capacity empty empty empty empty empty empty empty empty
capacity reserve reserve reserve reserve reserve reserve reserve reserve
element access at at at
element access operator[]
modifiers emplace emplace emplace emplace emplace emplace emplace emplace
modifiers emplace_hint emplace_hint emplace_hint emplace_hint emplace_hint emplace_hint emplace_hint emplace_hint
modifiers insert insert insert insert insert insert insert insert
modifiers erase erase erase erase erase erase erase erase
modifiers clear clear clear clear clear clear clear clear
modifiers swap swap swap swap swap swap swap swap
operations count count count count count count count count
operations find find find find find find find find
operations equal_range equal_range equal_range equal_range equal_range equal_range equal_range equal_range
operations lower_bound lower_bound lower_bound lower_bound lower_bound lower_bound lower_bound lower_bound
operations upper_bound upper_bound upper_bound upper_bound upper_bound upper_bound upper_bound upper_bound
observers get_allocator get_allocator get_allocator get_allocator get_allocator get_allocator get_allocator get_allocator
observers key_comp key_comp key_comp key_comp key_comp key_comp key_comp key_comp
observers value_comp value_comp value_comp value_comp value_comp value_comp value_comp value_comp
observers key_eq key_eq key_eq key_eq key_eq key_eq key_eq key_eq
observers hash_function hash_function hash_function hash_function hash_function hash_function hash_function hash_function
buckets bucket bucket bucket bucket bucket bucket bucket bucket
buckets bucket_count bucket_count bucket_count bucket_count bucket_count bucket_count bucket_count bucket_count
buckets bucket_size bucket_size bucket_size bucket_size bucket_size bucket_size bucket_size bucket_size
buckets max_bucket_count max_bucket_count max_bucket_count max_bucket_count max_bucket_count max_bucket_count max_bucket_count max_bucket_count
hash policy rehash rehash rehash rehash rehash rehash rehash rehash
hash policy load_factor load_factor load_factor load_factor load_factor load_factor load_factor load_factor
hash policy max_load_factor max_load_factor max_load_factor max_load_factor max_load_factor max_load_factor max_load_factor max_load_factor

Algorithms – Template Function

特征:通用算法(可以将它们用于几乎任何数据结构)

算法设计模式:

Image in a image block
  • 定义一系列算法,封装每个算法,并使它们可以互换
  • 策略使算法能够独立于使用它的客户端而变化

Iterators – 容器和算法之间的粘合剂

特征

  • 指定容器中的位置(通用指针)
  • 允许遍历容器
迭代器设计模式
Image in a image block
  • 提供一种顺序访问聚合对象的元素而不暴露其底层表示的方法
  • 迭代器模式提供的抽象允许您修改集合实现而不进行任何更改

输入/输出流

Image in a image block

该库和它的类的层次结构被分割在不同的文件中。
<ios>、<istream>、<ostream>、<streambuf>和<iosfwd>通常不直接包含在大多数C++程序中。它们描述了层次结构的基类,并被包含派生类的库的其他头文件自动包含。
<iostream>声明了用于通过标准输入和输出进行通信的对象(包括cin和cout)。
<fstream>定义了文件流类(如模板basic_ifstream或class ofstream),以及与这些类一起使用的内部缓冲区对象(basic_filebuf)。这些类用于使用流来操作文件。
<sstream>:该文件中定义的类用于操作字符串对象,就像它们是流一样。
<iomanip>声明了一些带有参数的标准操纵器,与提取和插入操作符一起使用,以修改内部标志和格式化选项。

原子和线程库

<atomic> Atomic (header)

<thread> Thread (header)

<mutex> Mutex (header)

<condition_variable> Condition variable (header)(条件变量)

<future> Future (header)

C标准库

来自C标准库的所有头文件,以另一个名称包含在C++标准中。将原名称移去“.h”并在开头处加上“c”作为新的名称。例如“time.h”改成“ctime”。C++标准库的头文件与C标准库的头文件的唯一区别是,函数位于std::命名空间(虽然很少编译器严格遵守,通常的做法是同时放在全局与 std 内,例如 printf 和 std::printf 两者均有)。

杂项

<algorithm> Standard Template Library: Algorithms (library)

<chrono> Time library (header)

<codecvt> Unicode conversion facets (header)

<complex> Complex numbers library (header)

<exception> Standard exceptions (header)

<functional> Function objects (header)

<initializer_list> Initializer list (header)

<iterator> Iterator definitions (header)

<limits> Numeric limits (header)

<locale> Localization library (header)

<memory> Memory elements (header)

<new> Dynamic memory (header)

<numeric> Generalized numeric operations (header)

<random> Random (header)

<ratio> Ratio header (header)

<regex> Regular Expressions (header)

<stdexcept> Exception classes (header)

<string> Strings (header)

<system_error> System errors (header)

<tuple> Tuple library (header)

<typeindex> Type index (header)

<typeinfo> Type information (header)

<type_traits> type_traits (header)

<utility> Utility components (header)

<valarray> Library for arrays of numeric values (header)

C++扩展

以下是C++目前常见的扩展,按照功能分类:

  • 标准库扩展:TR1、C++11、C++14、C++17、C++20
  • 通用库:Boost
  • GUI库:Qt、MFC
  • 网络库:ACE
  • 数学库:Eigen
  • 计算几何库:CGAL
  • 计算机视觉库:OpenCV
  • 数据序列化库:Protocol Buffers
  • 测试框架:Google Test
  • JSON库:RapidJSON
  • 其他库:Loki、POCO、STLport
非标准的C++扩展
  1. GNU 扩展:GCC 编译器提供的一些非标准扩展,如 attribute、__builtin_XXX 等。
  2. Microsoft 扩展:Visual Studio 编译器提供的一些非标准扩展,如 __declspec、__uuidof 等。
  3. Clang 扩展:Clang 编译器提供的一些非标准扩展,如 __has_feature、__builtin_XXX 等。

  • CUDA:CUDA是一种并行计算平台和编程模型,用于在NVIDIA GPU上加速计算密集型应用程序。CUDA提供了一个C++扩展,可以在C++中编写GPU加速的代码。
  • OpenMP:OpenMP是一种并行编程模型,用于在共享内存系统上并行执行代码。OpenMP提供了一组C++扩展,可以在C++中编写并行代码。
  • TBB:TBB是Intel Threading Building Blocks的缩写,是一个C++库,用于并行化代码。它提供了一组C++扩展,可以在C++中编写并行代码,例如并行循环、并行排序、并行算法等等。
  • MPI:MPI是一种消息传递接口,用于在分布式内存系统上并行执行代码。MPI提供了一组C++扩展,可以在C++中编写并行代码。
  • ATL:ATL是Active Template Library的缩写,是一个C++库,用于开发COM组件和ActiveX控件。它提供了很多有用的类和功能,如COM对象、COM接口、注册表、类库等。
  • WTL:WTL是Windows Template Library的缩写,是一个C++库,用于开发Windows GUI应用程序。它提供了很多有用的类和功能,如窗口、对话盒、控件、消息映射、多文档。
  • ACE:ACE是一个C++框架,用于开发高性能、可扩展的网络应用程序。它提供了许多有用的类和功能,例如事件处理、并发、线程池、消息。
  • POCO:POCO是一个C++库,提供了许多有用的类和功能,如网络、XML、数据库、加密、日志记录等。它的目标是提供一组轻量级的、跨平台的C++帮助开发高质量的应用程序。
  • ROOT:ROOT是一个C++库,用于数据分析、可视化和存储。它提供了很多有用的类和功能,如数据结构、绘图、统计分析、文件I/O等能力为霈。而开发的,但已经被广泛应用于其他领域。
  • GSL:GSL是Guidelines Support Library的缩写,是一个C++库,用于实际C++ Core Guidelines中的建议。它提供了许许多多有用的类和功能,如各种数学函数、运算器、仿真器、算法等。
  • asio:Asio 是一个跨平台的 C++ 网络编程库,它提供了基于事件驱动的异步 I/O 操作,支持 TCP、UDP、SSL 等协议,可以用于开发高性能、可扩展的网络应用程序。

继承

继承属性

一个派生类继承了所有的基类方法,但下列情况除外:

  • 基类的构造函数、析构函数和拷贝构造函数。
  • 基类的重载运算符。
  • 基类的友元函数。

继承类型

  • 公有继承(public):当一个类派生自公有基类时,基类的公有成员也是派生类的公有成员,基类的保护成员也是派生类的保护成员,基类的私有成员不能直接被派生类访问,但是可以通过调用基类的公有保护成员来访问。
  • 保护继承(protected): 当一个类派生自保护基类时,基类的公有保护成员将成为派生类的保护成员。
  • 私有继承(private):当一个类派生自私有基类时,基类的公有保护成员将成为派生类的私有成员。