本篇学习继续记录阅读《Effective C++中文版(第三版)》感悟所得,算是这本书的终结章。

五、实现

第26条:尽可能延后变量定义式的出现时间

在C++中,尽可能延后变量定义式的出现时间是一种编程建议,旨在提高代码的可读性、可维护性和性能。以下是尽可能延后变量定义式的出现时间的一些原因:

可读性:将变量定义放在它们实际使用的位置附近可以提高代码的可读性,因为读者无需在代码中跳跃来查找变量的定义和用途。这使得代码更容易理解和跟踪。

可维护性:如果变量在定义时立即初始化,可以减少出现未初始化变量和意外赋值的可能性。这有助于减少错误和提高代码的可维护性。

性能:在某些情况下,延后变量定义可以提高性能,因为编译器可以更好地优化变量的生命周期和内存使用。例如,如果变量仅在循环或条件语句中使用,将其定义放在循环或条件语句内部可以减少不必要的内存分配和构造函数调用。

以下是一个将变量定义尽可能延后到实际使用位置的示例:

#include <iostream>
#include <vector>

int main() {
    // 不推荐:在实际使用之前定义变量
    std::vector<int> numbers;
    numbers = {1, 2, 3, 4, 5};

    // 推荐:在实际使用时定义变量
    std::vector<int> numbers2 = {1, 2, 3, 4, 5};

    return 0;
}   

在这个示例中,我们展示了两种定义变量的方法。在第一种方法中,我们在实际使用之前定义了变量numbers。在第二种方法中,我们在实际使用时定义了变量numbers2。第二种方法更易于阅读和维护,因为变量的定义和初始化紧密相关。

总之,在C++中,尽可能延后变量定义式的出现时间有助于提高代码的可读性、可维护性和性能。在实际开发过程中,您应该尽量将变量定义放在它们实际使用的位置附近,并在定义时立即初始化它们。这将使您的代码更易于理解和维护。

第27条:尽可能少做转型动作

要尽可能规避一切转型操作,无论是显示转换还是隐式转换,如果不得不转换,建议使用安全转换。

为什么?

在C++中,尽可能减少类型转换(type casting)是一种编程建议,因为过多的类型转换可能导致代码难以理解、维护和调试。以下是尽可能减少类型转换的一些原因:

可读性:过多的类型转换会让代码变得复杂和混乱,使得阅读和理解代码更加困难。类型转换可能会掩盖代码的实际意图,从而降低可读性。

类型安全:类型转换可能会破坏C++的类型安全性,导致潜在的错误和未定义行为。例如,将指针类型强制转换为不兼容的类型可能导致内存访问错误、数据损坏和程序崩溃。

维护性:过多的类型转换可能会导致代码难以维护和调试。类型转换可能会隐藏潜在的错误和问题,使得在修改和扩展代码时更容易出错。

性能:某些类型转换(如浮点数到整数的转换)可能导致性能损失,因为编译器需要生成额外的指令来执行转换。尽可能减少类型转换可以避免这些性能损失。

怎么做?

在C++中,您应该尽量避免不必要的类型转换,并使用更安全和更具可读性的替代方法。以下是一些建议:

使用自动类型推断(如auto关键字)来避免显式类型转换。

使用类型安全的转换(如static_cast、dynamic_cast或reinterpret_cast)而不是C风格的强制类型转换。

优先使用C++的强类型枚举(enum class)而不是普通枚举(enum),以避免意外的类型转换。

在设计类和接口时考虑类型安全和一致性,以减少类型转换的需求。

总之,在C++中,尽可能减少类型转换可以提高代码的可读性、类型安全性和维护性。在实际开发过程中,您应该尽量避免不必要的类型转换,并使用更安全和更具可读性的替代方法。

auto\enum class\static_cast

以下是一个C++代码示例,演示了如何避免不必要的类型转换并使用更安全和更具可读性的替代方法:

#include <iostream>
#include <vector>

enum class Color { Red, Green, Blue }; // 强类型枚举

void process_color(Color color) {
    switch (color) {
        case Color::Red:
            std::cout << "Processing red color" << std::endl;
            break;
        case Color::Green:
            std::cout << "Processing green color" << std::endl;
            break;
        case Color::Blue:
            std::cout << "Processing blue color" << std::endl;
            break;
    }
}

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5};

    // 使用auto关键字避免显式类型转换
    for (auto number : numbers) {
        std::cout << number << std::endl;
    }

    // 使用强类型枚举避免意外的类型转换
    Color color = Color::Red;
    process_color(color);

    // 使用static_cast进行类型安全的转换
    int int_value = 42;
    double double_value = static_cast<double>(int_value);

    return 0;
}   

在这个示例中,我们展示了以下几种避免不必要的类型转换的方法:

  1. 使用auto关键字进行自动类型推断,避免显式类型转换。
  2. 使用强类型枚举(enum class)而不是普通枚举(enum),以避免意外的类型转换。
  3. 使用static_cast进行类型安全的转换,而不是C风格的强制类型转换。

这些方法有助于提高代码的可读性、类型安全性和维护性。在实际开发过程中,您应该尽量避免不必要的类型转换,并使用更安全和更具可读性的替代方法。

为什么使用强类型枚举比普通枚举好?

在C++中,使用强类型枚举(enum class)比普通枚举(enum)更好,因为强类型枚举提供了更强的类型安全、更好的封装和更少的意外类型转换。以下是强类型枚举和普通枚举之间的一些区别:

类型安全:强类型枚举不会隐式地转换为整数或其他枚举类型。这可以防止意外的类型转换和错误。相反,普通枚举可以隐式地转换为整数或其他枚举类型,这可能导致错误和未定义行为。

封装:强类型枚举的枚举值在其自己的作用域内,不会与全局作用域冲突。这提供了更好的封装和命名空间管理。相反,普通枚举的枚举值位于全局作用域,可能与其他枚举值或变量发生冲突。

以下是一个使用普通枚举和强类型枚举的示例,演示了它们之间的区别:

include <iostream>

/ 普通枚举
num OldColor { Red, Green, Blue };

/ 强类型枚举
num class NewColor { Red, Green, Blue };

oid process_old_color(int color) {
   std::cout << "Processing old color: " << color << std::endl;


nt main() {
   // 普通枚举可以隐式地转换为整数
   OldColor old_color = Red;
   process_old_color(old_color); // 输出:Processing old color: 0

   // 强类型枚举不会隐式地转换为整数(需要显式类型转换)
   NewColor new_color = NewColor::Red;
   // process_old_color(new_color); // 编译错误:无法将NewColor转换为int
   process_old_color(static_cast<int>(new_color)); // 输出:Processing old color: 0

   return 0;

为什么使用 static_case 比较好?

在C++中,static_cast是一种类型转换操作符,用于在相关类型之间执行编译时类型转换。static_cast比C风格的强制类型转换和其他C++类型转换操作符(如reinterpret_cast)更安全,因为它在编译时执行类型检查,确保转换是有效的。以下是使用static_cast的一些优点:

类型安全:static_cast在编译时执行类型检查,确保转换是有效的。这有助于避免类型转换错误和未定义行为。相反,C风格的强制类型转换和reinterpret_cast不执行类型检查,可能导致错误的类型转换和未定义行为。

可读性:static_cast明确表示类型转换的意图,使代码更易于阅读和理解。相反,C风格的强制类型转换和其他C++类型转换操作符可能会让代码变得复杂和混乱。

以下是一个使用static_cast和C风格强制类型转换的示例:

#include <iostream>

int main() {
    double double_value = 3.14;

    // 使用C风格的强制类型转换(不安全)
    int int_value1 = (int)double_value;
    std::cout << "C-style cast: " << int_value1 << std::endl;

    // 使用static_cast进行类型转换(更安全)
    int int_value2 = static_cast<int>(double_value);
    std::cout << "static_cast: " << int_value2 << std::endl;

    return 0;
}   

第28条:避免返回 handles 指向对象内部成分

什么是 handles ?

“handles”是一个通用术语,用于表示指向对象内部成分的引用或指针。返回handles可能会导致悬挂引用、内存访问错误和未定义行为,特别是在返回局部变量或临时对象的引用或指针时。

具体含义

在C++中,”避免返回handles指向对象内部成分”是一种编程建议,意味着您应该避免从函数返回指向对象内部成分(如成员变量)的引用或指针。这样做可以确保对象的封装性和安全性,防止外部代码意外地修改或访问对象的内部状态。

以下是一个返回指向对象内部成分的示例,可能导致悬挂引用:

#include <iostream>
#include <string>

const char* get_first_char(const std::string& str) {
    return &str[0]; // 返回指向对象内部成分的指针(不安全)
}

int main() {
    const char* first_char = get_first_char("Hello, World!");
    std::cout << "First char: " << *first_char << std::endl; // 访问悬挂引用(未定义行为)

    return 0;
}   

在这个示例中,我们从get_first_char函数返回了一个指向std::string对象内部成分的指针。这可能导致悬挂引用,因为在函数返回后,std::string对象可能已经被销毁。在main函数中,我们尝试访问这个悬挂引用,这可能导致未定义行为。

为了避免这个问题,您应该避免返回指向对象内部成分的引用或指针,而是返回对象的副本或通过其他安全的方法访问对象的状态。例如,您可以返回字符而不是指向字符的指针:

#include <iostream>
#include <string>

char get_first_char(const std::string& str) {
    return str[0]; // 返回字符(安全)
}

int main() {
    char first_char = get_first_char("Hello, World!");
    std::cout << "First char: " << first_char << std::endl; // 访问字符(安全)

    return 0;
}   

第29条:为”异常安全”而努力是值得的

在C++中,”为’异常安全’而努力是值得的”是一种编程建议,意味着您应该尽量使代码在异常情况下仍然能够正确地运行。异常安全代码可以在异常发生时保持数据的一致性、避免资源泄漏和未定义行为。

异常安全通常分为以下三个级别:

基本异常安全(basic exception safety):保证在异常发生时不会泄漏资源,程序仍然处于有效状态。然而,对象的状态可能已经改变,不再满足预期的不变式。

强异常安全(strong exception safety):保证在异常发生时不会泄漏资源,程序仍然处于有效状态,对象的状态保持不变。这意味着操作具有事务性质,要么成功完成,要么完全不起作用。

不抛异常(no-throw exception safety):保证操作不会抛出任何异常,始终成功完成。这是最高级别的异常安全性,但在实践中很难实现。

第30条:透彻了解 inlining 的里里外外

在C++中,”透彻了解inlining的里里外外”是一种建议,意味着您应该理解内联(inlining)的概念、优点、缺点以及如何在实际编程中正确使用它。

内联(inlining)是一种编译器优化技术,它将函数调用替换为函数体的实际代码。这样做可以消除函数调用的开销,提高程序的执行速度。然而,内联也可能导致代码膨胀,因为函数体被复制到每个调用点。过度内联可能导致指令缓存未命中和性能下降。

以下是关于内联的一些关键概念:

内联函数:在C++中,您可以使用inline关键字显式地将函数声明为内联函数。这是一个建议性指令,编译器可以选择是否实际内联函数。通常,编译器会根据函数的大小和复杂性来决定是否进行内联。

inline int add(int a, int b) {
    return a + b;
}   

隐式内联:对于类的成员函数,如果它们在类定义中实现,编译器会隐式地将它们视为内联函数。这意味着您不需要显式地使用inline关键字,编译器会自动考虑内联这些成员函数。

class MyClass {
public:
    int add(int a, int b) {
        return a + b;
    }
};  

内联的优缺点:内联可以提高程序的执行速度,因为它消除了函数调用的开销。然而,内联可能导致代码膨胀,因为函数体被复制到每个调用点。在实际编程中,您需要权衡内联的优缺点,以获得最佳性能。

总之,在C++中,透彻了解inlining的里里外外意味着您需要理解内联的概念、优点、缺点以及如何在实际编程中正确使用它。在实际开发过程中,您应该根据需要为函数添加适当的内联指令,以提高程序的执行速度和性能。同时,要注意权衡内联的优缺点,以避免代码膨胀和性能下降。

第31条:将文件间的编译依存关系降至最低

在C++中,”将文件间的编译依存关系降至最低”是一种编程建议,意味着您应该尽量减少源文件之间的依赖关系,使它们尽可能地独立。这样做可以提高代码的可维护性、可读性和编译速度。

以下是减少文件间编译依存关系的一些方法:

  1. 使用前向声明:尽可能使用前向声明(forward declaration)而不是包含完整的头文件。这可以减少头文件的依赖关系,提高编译速度。

    // 前向声明
    class MyClass;

    // 使用MyClass的指针或引用
    void process_object(const MyClass& obj);

    这一点和OC一样,也是有前向声明。

  2. 将实现与接口分离:将类的实现(成员函数的定义)与接口(类定义和成员函数声明)分离。这可以通过将实现放在源文件(.cpp)中,将接口放在头文件(.h)中来实现。这样,当实现发生变化时,只需要重新编译源文件,而不需要重新编译所有依赖于头文件的文件。

六、继承与面向对象设计

第32条:确定你的 public 继承塑模出 is-a 关系

在C++中,”确定你的public继承塑模出is-a关系”是一种面向对象设计建议,意味着您应该确保使用public继承表示两个类之间的”is-a”(是一个)关系。这样做可以确保继承关系的语义正确性,提高代码的可读性和可维护性。

“is-a”关系表示一个类(派生类)是另一个类(基类)的特殊类型。在这种情况下,派生类继承基类的属性和行为,并可以扩展或覆盖它们。这符合面向对象编程的继承原则,即子类应该是父类的特殊化或扩展。

以下是一个使用public继承表示”is-a”关系的示例:

#include <iostream>

// 基类:动物
class Animal {
public:
    virtual void make_sound() const {
        std::cout << "The animal makes a sound" << std::endl;
    }
};

// 派生类:狗(Dog is-an Animal)
class Dog : public Animal {
public:
    void make_sound() const override {
        std::cout << "The dog barks" << std::endl;
    }
};

int main() {
    Dog dog;
    Animal& animal = dog; // 将Dog视为Animal(is-a关系)

    animal.make_sound(); // 输出:The dog barks

    return 0;
}   

第33条:避免遮掩继承而来的名称

#include <iostream>

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

class Derived : public Base {
public:
    // 遮掩继承而来的名称(不推荐)
    void print() const {
        std::cout << "Derived print" << std::endl;
    }
};

int main() {
    Derived derived;
    Base& base = derived;

    // 调用派生类中的print()函数(可能导致错误的行为)
    base.Base::print(); // 输出:Base print

    return 0;
}   

第34条:区分接口继承和实现继承

基类里的函数,分为待被覆写,和不被覆写

在C++中,”区分接口继承和实现继承”是一种面向对象设计建议,意味着您应该在设计类继承时明确区分接口(interface)继承和实现(implementation)继承。这样做可以提高代码的可读性、可维护性和灵活性。

接口继承:接口继承是指派生类继承基类的接口(成员函数的声明),但不继承其实现(成员函数的定义)。这使得派生类可以根据需要提供自己的实现,覆盖或扩展基类的功能。接口继承通常通过继承纯虚函数(抽象函数)来实现。

实现继承:实现继承是指派生类继承基类的接口和实现。这使得派生类可以直接使用基类的功能,无需提供自己的实现。实现继承通常通过继承虚函数或非虚函数来实现。

以下是一个区分接口继承和实现继承的示例:

#include <iostream>

// 基类:定义接口(纯虚函数)
class Animal {
public:
    virtual void make_sound() const = 0; // 纯虚函数
};

// 派生类:实现接口(接口继承)
class Dog : public Animal {
public:
    void make_sound() const override {
        std::cout << "The dog barks" << std::endl;
    }
};

// 基类:定义接口和实现(虚函数)
class Shape {
public:
    virtual void draw() const {
        std::cout << "Drawing a shape" << std::endl;
    }
};

// 派生类:继承接口和实现(实现继承)
class Circle : public Shape {
    // 使用基类的draw()函数实现(无需覆盖或提供自己的实现)
};

int main() {
    Dog dog;
    dog.make_sound(); // 输出:The dog barks

    Circle circle;
    circle.draw(); // 输出:Drawing a shape

    return 0;
}    

上面给make_sound函数也加了 const 是什么意思?

在C++中,为成员函数添加const关键字表示该成员函数是一个常量成员函数,它不会修改对象的状态。换句话说,常量成员函数不能修改对象的任何数据成员,也不能调用其他非const成员函数。这有助于确保在调用常量成员函数时对象的状态保持不变,提高代码的可读性和安全性。

在上述示例中,我们将make_sound函数声明为const成员函数,表示它不会修改Animal对象的状态。这使得我们可以在const对象或const引用上调用make_sound函数,确保对象的状态保持不变。

函数加了const相当于是好好先生自我声明。

第35条:考虑virtual函数以外的其他选择

异构和多态

异构(heterogeneous)和多态(polymorphism)是计算机科学和面向对象编程中的两个概念。以下是它们的定义和解释:

异构(Heterogeneous):异构是指由不同类型的元素组成的集合或数据结构。在编程中,异构通常用于描述具有不同类型元素的容器、数组或集合。这些容器可以包含不同类型的对象,如整数、浮点数、字符串或自定义类型的实例。在C++中,您可以使用std::tuple、std::variant或std::any等容器来表示异构数据。
例如,以下代码展示了一个异构的std::tuple:

#include <iostream>
#include <tuple>
#include <string>

int main() {
    // 定义一个包含int、double和std::string类型元素的异构tuple
    std::tuple<int, double, std::string> hetero_tuple(42, 3.14, "Hello, World!");

    // 访问tuple中的元素
    std::cout << "int: " << std::get<0>(hetero_tuple) << std::endl;
    std::cout << "double: " << std::get<1>(hetero_tuple) << std::endl;
    std::cout << "string: " << std::get<2>(hetero_tuple) << std::endl;

    return 0;
}   

多态(Polymorphism):多态是面向对象编程的一个核心特性,它允许使用一个接口来表示不同类型的对象。多态可以让您编写更通用、可重用和灵活的代码,因为您可以在运行时根据对象的实际类型来调用适当的函数或方法。
在C++中,多态通常通过虚函数(virtual function)和继承来实现。派生类可以覆盖基类的虚函数,从而提供自己的实现。通过基类的指针或引用,您可以访问派生类的实现,实现多态行为。

以下是一个使用多态的示例:

#include <iostream>

// 基类:定义接口(虚函数)
class Animal {
public:
    virtual void make_sound() const {
        std::cout << "The animal makes a sound" << std::endl;
    }
};

// 派生类:实现接口
class Dog : public Animal {
public:
    void make_sound() const override {
        std::cout << "The dog barks" << std::endl;
    }
};

int main() {
    Dog dog;
    Animal* animal_ptr = &dog;

    // 通过基类指针访问派生类的实现(多态)
    animal_ptr->make_sound(); // 输出:The dog barks

    return 0;
}   

解释

在C++中,”考虑virtual函数以外的其他选择”是一种编程建议,意味着在实现多态行为时,您应该考虑使用除了虚函数(virtual function)之外的其他方法。虽然虚函数是实现多态的一种常用方法,但它们可能会导致运行时开销、代码膨胀和维护问题。根据具体需求,您可以使用其他方法来实现多态行为,如模板、函数指针、回调函数或策略模式等。

以下是一些使用除了虚函数之外的其他方法实现多态行为的示例:

  1. 模板(Template)

  2. 函数指针(Function Pointer)

第36条:绝不重新定义继承而来的 non-virtual 函数

在C++中,”绝不重新定义继承而来的non-virtual函数”是一种编程建议,意味着您不应该在派生类中重新定义(覆盖)基类中的非虚(non-virtual)函数。这样做可能导致不一致的行为和意外的结果,因为非虚函数不具有多态性。

当您在派生类中重新定义基类中的非虚函数时,基类和派生类将具有相同名称但具有不同实现的函数。然而,由于非虚函数不具有多态性,通过基类的指针或引用调用这个函数时,将始终调用基类中的实现,而不是派生类中的实现。这可能导致意外的行为和错误。

第37条:绝不重新定义继承而来的缺省参数值

在C++中,”绝不重新定义继承而来的缺省参数值”是一种编程建议,意味着在派生类中,您不应该为继承自基类的虚拟函数重新定义默认参数值。这是因为在C++中,虚拟函数的默认参数值是静态绑定的,而不是动态绑定的。这可能导致在调用虚拟函数时出现意外的行为。

以下是一个重新定义继承而来的默认参数值的示例,可能导致意外的行为:

#include <iostream>

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

class Derived : public Base {
public:
    // 重新定义继承而来的默认参数值(不推荐)
    void print(int value = 100) override {
        std::cout << "Derived: " << value << std::endl;
    }
};

int main() {
    Derived obj;
    Base* base_ptr = &obj;

    // 调用虚拟函数(可能导致意外的行为)
    base_ptr->print(); // 输出:Derived: 42,而不是Derived: 100

    return 0;
}   

在这个示例中,我们在派生类Derived中重新定义了继承自基类Base的虚拟函数print的默认参数值。然而,当我们通过基类指针base_ptr调用虚拟函数时,使用的是基类的默认参数值(42),而不是派生类的默认参数值(100)。这可能导致意外的行为和错误。

为了避免这个问题,您应该遵循”绝不重新定义继承而来的缺省参数值”的建议,并在派生类中不修改基类虚拟函数的默认参数值。如果需要使用不同的默认值,您可以考虑使用其他方法,如重载函数或提供额外的参数。

总之,在C++中,绝不重新定义继承而来的缺省参数值意味着您不应该为继承自基类的虚拟函数重新定义默认参数值。这可以避免在调用虚拟函数时出现意外的行为和错误。在实际开发过程中,您应该遵循这个建议,以确保代码的正确性和可维护性。

第38条:通过复合塑模出 has-a 或 ‘根据某物实现出’

在C++中,”通过复合塑模出has-a或’根据某物实现出’”是一种面向对象设计建议,意味着您应该优先使用组合(composition)来表示类之间的关系,而不是继承。组合是指一个类包含另一个类的对象作为成员变量,从而表示”has-a”(有一个)或”根据某物实现出”的关系。这样做可以提高代码的可维护性、灵活性和可重用性。

组合允许您将类的功能和行为分解为更小的、可重用的部分。这些部分可以在多个类之间共享,而不需要使用继承。组合还可以让您在运行时更改类的行为,通过更改组合对象来实现不同的功能。

以下是一个使用组合表示类之间关系的示例:

#include <iostream>

class Engine {
public:
    void start() const {
        std::cout << "Engine starts" << std::endl;
    }
};

class Car {
public:
    void start() const {
        engine_.start();
        std::cout << "Car starts" << std::endl;
    }

private:
    Engine engine_; // Car has-an Engine(通过组合表示关系)
};

int main() {
    Car car;
    car.start(); // 输出:Engine starts,然后输出:Car starts

    return 0;
}   

在这个示例中,我们定义了两个类:Engine和Car。我们使用组合来表示Car类与Engine类之间的关系,即Car具有一个Engine对象作为成员变量。这使得我们可以在Car类中使用Engine类的功能,而无需使用继承。

总之,在C++中,通过复合塑模出has-a或’根据某物实现出’意味着您应该优先使用组合来表示类之间的关系,而不是继承。这有助于提高代码的可维护性、灵活性和可重用性。在实际开发过程中,您可以根据需要使用组合或其他设计模式(如继承、委托等)来实现类的功能和行为。

第39条:明智而审慎地使用 private 继承

这个操作很骚,尽量不要用。

在C++中,”智而审慎地使用private继承”是一种编程建议,意味着在使用private继承时,您应该谨慎地权衡其优缺点,并确保它在特定场景下确实有助于提高代码的可读性、可维护性和灵活性。

private继承表示一个类是另一个类的特殊实现,而不是”is-a”(是一个)关系。与public继承不同,private继承不会将基类的接口暴露给派生类的用户。相反,派生类仅继承基类的实现,并可以在需要时覆盖或扩展它。在某些情况下,private继承可以用于实现代码复用和继承的实现细节,而不影响派生类的接口。

然而,请注意,private继承可能会导致继承层次结构变得复杂和难以理解。在许多情况下,您可以使用组合(composition)或委托(delegation)来实现相同的目的,而不是使用private继承。

以下是一个使用private继承的示例:

#include <iostream>

// 基类:提供通用的实现
class Base {
public:
    void common_operation() const {
        std::cout << "Base: common operation" << std::endl;
    }
};

// 派生类:使用private继承实现代码复用
class Derived : private Base {
public:
    void do_something() const {
        common_operation(); // 使用基类的实现
        std::cout << "Derived: do something" << std::endl;
    }
};

int main() {
    Derived obj;
    obj.do_something(); // 输出:Base: common operation,然后输出:Derived: do something

    return 0;
}   

第40条:明智而审慎地使用多重继承

什么意思?

在C++中,”明智而审慎地使用多重继承”是一种编程建议,意味着在使用多重继承时,您应该谨慎地权衡其优缺点,并确保它在特定场景下确实有助于提高代码的可读性、可维护性和灵活性。

多重继承是指一个类可以从多个基类继承特性和行为。虽然多重继承提供了更大的灵活性,但它也可能导致一些问题,如菱形继承问题(diamond problem)、名称冲突和复杂的继承层次结构。

以下是一个使用多重继承的示例:

#include <iostream>

class Printer {
public:
    void print() const {
        std::cout << "Printer: print" << std::endl;
    }
};

class Scanner {
public:
    void scan() const {
        std::cout << "Scanner: scan" << std::endl;
    }
};

// 使用多重继承实现一个多功能设备(printer + scanner)
class MultiFunctionDevice : public Printer, public Scanner {
public:
    void copy() const {
        std::cout << "MultiFunctionDevice: copy" << std::endl;
    }
};

int main() {
    MultiFunctionDevice mfd;
    mfd.print(); // 调用Printer类的print()函数
    mfd.scan();  // 调用Scanner类的scan()函数
    mfd.copy();  // 调用MultiFunctionDevice类的copy()函数

    return 0;
}   

什么叫菱形继承问题?

菱形继承问题(也称为钻石问题)是指在面向对象编程中,当一个类从两个或多个类继承,这些类又从同一个基类继承时,可能会导致的问题。菱形继承问题主要涉及基类的二义性和重复继承,因为派生类会继承多个基类的实例。

#include <iostream>

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

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

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

// 菱形继承
class MostDerived : public Derived1, public Derived2 {
};

int main() {
    MostDerived obj;

    // obj.print(); // 编译错误:二义性,不知道调用哪个基类的print()函数

    return 0;
}   

七、模板与泛型编程

第41条:了解隐式接口和编译器多态

在C++中,隐式接口和编译器多态是指一种通过模板和操作符重载实现的编译时多态性。这种多态性在编译时解析,允许您编写更通用和可重用的代码,同时避免了虚函数带来的运行时开销。

隐式接口:隐式接口是指通过模板参数和操作符重载实现的一种灵活的接口。它允许您编写适用于多种类型的通用代码,而无需使用继承或虚函数。隐式接口可以在编译时根据实际类型生成特定的实现,从而提高性能。

编译器多态:编译器多态是指通过模板实现的编译时多态性。编译器根据模板参数的类型生成特定的实现,从而避免了运行时类型检查和虚函数调用。编译器多态可以提高代码的执行速度,同时保持灵活性和可重用性。

以下是一个使用隐式接口和编译器多态的示例:

#include <iostream>

// 通用的print函数,适用于任何具有<<操作符的类型
template <typename T>
void print(const T& obj) {
    std::cout << obj << std::endl;
}

int main() {
    int int_value = 42;
    double double_value = 3.14;
    std::string string_value = "Hello, World!";

    // 使用隐式接口和编译器多态调用print函数
    print(int_value);       // 输出:42
    print(double_value);    // 输出:3.14
    print(string_value);    // 输出:Hello, World!

    return 0;
}   

第42条:了解 typename 的双重含义

在C++中,”了解typename的双重含义”是一种编程建议,意味着您应该理解typename关键字在不同上下文中的用途和含义。typename关键字主要有两种用途:
一种是在模板定义中表示类型参数;另一种是在依赖类型名称(dependent type name)的上下文中指示类型名称。

  1. 在模板定义中表示类型参数:typename关键字可以在模板定义中用作类型参数的声明,表示一个通用的类型。这与class关键字具有相同的含义,但typename更明确地表示类型参数。

    // 使用typename表示类型参数
    template
    void print(const T& obj) {

     std::cout << obj << std::endl;
    

    }

    在这个示例中,我们使用typename关键字在模板函数print中声明了一个类型参数T。这表示print函数可以接受任何类型的参数。

  2. 指示依赖类型名称:在模板中,typename关键字还可以用于指示依赖于模板参数的类型名称。