C++11 中类的部分新特性

右值引用以及部分特性已经在前面的文章讲过了,大家可以翻看。

1. 默认和删除的函数 (Defaulted and Deleted Functions)

在 C++98 中,如果你声明了任何构造函数,编译器就不会再为你生成默认的无参构造函数。有时你希望同时拥有自定义构造函数和编译器生成的默认版本,就需要手动编写一个空的默认构造函数,这既繁琐又不高效(因为编译器生成的可能更好)。

C++11 允许你显式地指示编译器生成函数的默认实现或完全禁止某个函数。

  • = default: 显式要求编译器生成该函数的默认版本。
  • = delete: 显式禁止编译器生成该函数的特定版本,任何尝试使用该函数的操作都会导致编译错误。

代码示例:

class MyClass {
public:
    // 1. 使用 default 显式指定使用编译器生成的默认构造函数
    MyClass() = default;

    // 2. 自定义一个构造函数
    MyClass(int value) : data(value) {}

    // 3. 使用 delete 禁止拷贝构造函数,使这个类不可拷贝
    MyClass(const MyClass&) = delete;

    // 4. 使用 delete 禁止某个参数类型的重载
    void process(double num) { /* ... */ }
    void process(char) = delete; // 禁止传入 char 类型

    int data;
};

int main() {
    MyClass obj1;           // 正确,调用 default 构造函数
    MyClass obj2(42);       // 正确,调用自定义构造函数
    // MyClass obj3 = obj2; // 错误!拷贝构造函数被禁用

    obj1.process(3.14);     // 正确
    // obj1.process('a');   // 错误!该重载被禁用
    return 0;
}

优点: 代码意图更清晰,控制更精确。可以轻松地创建不可拷贝、不可移动的类(如单例),或禁止不希望的隐式类型转换。


2. 委托构造函数 (Delegating Constructors)

在 C++98 中,如果一个类有多个构造函数,它们通常有共同的初始化代码,为了避免重复,你不得不将这些代码提取到一个私有的init()函数中,然后在各个构造函数中调用它。但这并非最佳实践,因为init()函数是在构造函数体内部调用,无法用于初始化常量成员或引用成员。

C++11 允许一个构造函数调用同一个类中的另一个构造函数。这被称为委托

代码示例:

class Employee {
    std::string name;
    int id;
    std::string department;

public:
    // 目标构造函数 (Target Constructor)
    Employee(const std::string& n, int i, const std::string& d)
        : name(n), id(i), department(d) {
        std::cout << "Target constructor called.\n";
    }

    // 委托构造函数:委托给目标构造函数
    Employee(const std::string& n, int i) : Employee(n, i, "Unknown") {
        std::cout << "Delegating constructor called.\n";
    }

    // 另一个委托构造函数
    Employee() : Employee("Anonymous", 0) {
        std::cout << "Default delegating constructor called.\n";
    }
};

int main() {
    Employee emp1; // 先调用 Employee("Anonymous", 0),
                   // 再委托给 Employee("Anonymous", 0, "Unknown")
    Employee emp2("Bob", 101, "IT");
    return 0;
}

执行流程: 当调用委托构造函数时,它会先执行它委托的那个构造函数的初始化列表和函数体,然后再执行自己的函数体。

优点: 避免了代码重复,提高了可维护性,并且能正确地初始化所有成员(包括const和引用)。


3. 继承构造函数 (Inheriting Constructors)

当一个派生类没有定义任何自己的构造函数时,它会自动继承基类的构造函数。但一旦你为派生类定义了任何构造函数,基类的构造函数就不会被自动继承了。在 C++98 中,如果你希望使用基类的构造函数,你必须在派生类中手动重新定义它们,这非常繁琐。

C++11 使用 using Base::Base; 来显式继承基类的所有构造函数(除默认构造函数、拷贝构造函数、移动构造函数外)。

代码示例:

class Base {
public:
    Base() { std::cout << "Base()\n"; }
    Base(int x) { std::cout << "Base(" << x << ")\n"; }
    Base(int x, const std::string& y) { std::cout << "Base(" << x << ", " << y << ")\n"; }
};

class Derived : public Base {
public:
    // 使用 using 继承 Base 的所有构造函数
    using Base::Base; // 继承 Base(int), Base(int, const std::string&)

    // 派生类可以添加自己的成员
    double extra_data = 3.14;
};

int main() {
    Derived d1;        // 错误!Base() 不会被继承,因为 Derived 没有自定义构造函数?
                       // 实际上,如果派生类没有任何构造函数(包括通过using继承的),
                       // 编译器会为它生成一个默认构造函数,该函数调用基类的默认构造函数。
                       // 这里因为使用了 `using`,情况变得复杂。通常建议明确定义需要的构造函数。

    Derived d2(10);    // 正确!调用继承的 Base(int)
    Derived d3(20, "Hello"); // 正确!调用继承的 Base(int, const std::string&)
    return 0;
}

注意: 继承的构造函数只初始化基类部分,派生类中新引入的成员会被默认初始化(如上例中的extra_data被初始化为3.14)。


4. 显式重写控制 (overridefinal)

为了更安全地使用虚函数和继承。

  • override: 显式注明一个成员函数旨在重写基类的虚函数。如果该函数没有成功重写任何虚函数(比如函数签名拼写错误),编译器将报错。
  • final: 用于类,表示该类不能被继承;用于虚函数,表示该虚函数在派生类中不能被重写。

代码示例:

class Base {
public:
    virtual void func() const;
    virtual void foo(int);
    void non_virtual();
};

class Derived : public Base {
public:
    virtual void func() const override; // 正确,成功重写
    // virtual void func() override;    // 错误!签名不匹配 (缺少 const)
    virtual void foo(int) override;     // 正确
    // void non_virtual() override;     // 错误!基类函数非虚

    void bar() final; // 错误!final 只能用于虚函数
};

class Base2 {
public:
    virtual void do_something() final; // 该函数不能再被重写
};

class Derived2 : public Base2 {
public:
    // virtual void do_something(); // 错误!Base2::do_something 是 final 的
};

class NoInheritance final { }; // 该类是 final 的
// class Try : public NoInheritance { }; // 错误!不能继承 final 类

优点: 防止意外的错误(如拼写错误导致没有重写),明确表达设计意图,增强代码可读性和安全性。


5. 类内成员初始化 (In-class Member Initialization)

C++11 允许在类定义内部直接为非静态数据成员指定默认值。

代码示例:

class Student {
private:
    std::string name = "Unknown";
    int age = 0;
    // 对于 static const 整型成员,仍然可以在类内初始化
    static const int max_grade = 100;

public:
    Student() = default; // 使用默认值初始化 name 和 age
    Student(const std::string& n) : name(n) {} // age 仍用默认值 0 初始化
    Student(const std::string& n, int a) : name(n), age(a) {}
};

优点: 简化了构造函数编写,避免了在多个构造函数中重复初始化成员,使默认值一目了然。


6. 友元模板 (Friend Templates)

在 C++98 中,让一个模板类的所有实例都成为另一个类的友元很麻烦。C++11 简化了语法。

代码示例:

template<typename T>
class Box; // 前向声明

class Treasure {
    int value;
    // 声明 Box<T> 的所有实例都是 Treasure 的友元
    template<typename U>
    friend class Box;
};

template<typename T>
class Box {
public:
    void peek(const Treasure& t) {
        std::cout << "Treasure's value: " << t.value << std::endl; // 正确,是友元
    }
};

7. 对齐控制 (alignasalignof)

C++11 提供了标准化的方式来查询和指定类型的对齐要求。

  • alignof(Type): 返回类型的对齐要求(字节数)。
  • alignas(N) / alignas(Type): 指定变量或类的对齐要求。

代码示例:

// 确保这个结构体按 16 字节对齐,这对 SIMD 指令等场景很重要
struct alignas(16) Vec4 {
    float x, y, z, w;
};

int main() {
    std::cout << "Alignment of int: " << alignof(int) << std::endl;
    std::cout << "Alignment of Vec4: " << alignof(Vec4) << std::endl; // 很可能是 16

    alignas(double) char buffer[1024]; // 让 buffer 按 double 的要求对齐
    return 0;
}

总结

C++11 对类的增强是革命性的:

  1. default/delete 提供了对特殊成员函数更精确的控制。
  2. 委托和继承构造函数减少了构造函数中的代码重复。
  3. 右值引用和移动语义解决了长期以来的性能痛点,是 C++11 的核心成就。
  4. overridefinal 使虚函数和继承的使用更加安全可靠。
  5. 类内初始化让代码更简洁直观。

更多推荐