【Effective C++】阅读笔记4

1. 确保公有继承中有is-a的关系

Is-a关系理解

该关系就是派生类应该具备基类的所有特性,并且可以替代基类对象使用,例如猫和狗都是动物的派生类,因为猫和狗都和动物形成了is-a关系,猫和狗都是动物。

在该关系下,派生类应该能够在任何需要基类对象的地方无缝的替代基类对象,同时不可以影响代码的正确性

符合is-a关系的公有继承

  • Dog继承子Animal,符合Is-a关系,因为Dog是可以代替Animal的
#include <iostream>class Animal {
public:virtual void speak() const {std::cout << "Animal sound!" << std::endl;}virtual ~Animal() = default;
};class Dog : public Animal {  // Dog “is-a” Animal
public:void speak() const override {std::cout << "Woof!" << std::endl;}
};void makeAnimalSpeak(const Animal& animal) {animal.speak();
}int main() {Dog dog;makeAnimalSpeak(dog);  return 0;
}

不符合is-a关系的继承

派生类不可以完全替代基类,这样的继承就不合理,例如如果让企鹅继承鸟,那么就不符合该继承关系,因为企鹅不会飞

#include <iostream>class Bird {
public:virtual void fly() const {std::cout << "Flying high!" << std::endl;}virtual ~Bird() = default;
};class Penguin : public Bird {  
public:void fly() const override {std::cout << "I can't fly!" << std::endl;}
};int main() {Penguin penguin;penguin.fly();  // 输出:I can't fly!return 0;
}

解决思路

公有继承中,派生类应该遵循基类的行为,并且支持基类的所有接口,派生类不应该改变基类的行为

如果继承仅仅是复用基类的某些功能,而不是建立在Is-a关系上的,此时最后的办法是组合,组合可以让类包含其他类的对象,避免因为继承而引入不合理的关系

  • Penguin中包含了bird对象,这样就可以复合实现bird对象的功能,而不用继承bird

#include <iostream>class Bird {
public:void layEggs() const {std::cout << "Laying eggs!" << std::endl;}
};class Penguin {
public:Penguin(const Bird& bird) : bird_(bird) {}void layEggs() const {bird_.layEggs();  // 使用组合而非继承}void swim() const {std::cout << "Swimming in water!" << std::endl;}private:Bird bird_;
};int main() {Bird bird;Penguin penguin(bird);penguin.layEggs();  penguin.swim();     return 0;
}

总结与反思

  • 确保继承是符合is-a关系的,要确保派生类可以完全替代基类
  • 派生类应该遵循里氏替换原则,派生类遵循基类所有行为和约定
  • 优先使用组合而非继承,如果需要复用基类的实现而非定义is-a关系的时候,选择组合而不是继承

2. 使用类类型代替字符串进行参数传递

使用类类型的优点

  • 安全性:因为类类型在编译的时候是会检查的,确保的传递的参数有效,这样就有效的避免运行时候的错误
  • 类类型的使用可以让代码结构更加的清晰,字符串作为参数传递的时候容易出现拼写错误,但是类类型不会有类似的问题

错误事例:使用字符串作为参数

代码中的主要问题就是如果拼写错误,那么传递的内容肯定无法按照预期执行

#include <iostream>
#include <string>void printGender(const std::string& gender) {if (gender == "male") {std::cout << "This person is male." << std::endl;}else if (gender == "female") {std::cout << "This person is female." << std::endl;}else {std::cout << "Unknown gender!" << std::endl;}
}int main() {printGender("male");printGender("female");printGender("unknown");  return 0;
}

解决方法:使用类类型替代字符串

通过使用一个的Gender的枚举类来替代字符串,避免字符串的错误

#include <iostream>enum class Gender {Male,Female,Unknown
};void printGender(Gender gender) {switch (gender) {case Gender::Male:std::cout << "This person is male." << std::endl;break;case Gender::Female:std::cout << "This person is female." << std::endl;break;default:std::cout << "Unknown gender!" << std::endl;break;}
}int main() {printGender(Gender::Male);printGender(Gender::Female);printGender(Gender::Unknown); return 0;
}

如果涉及到复杂的参数,可以定义一个类或者结构体来封装对应的数据

Person类中性别类型就是使用枚举类型,一方面可以提高类型安全,另一方面也减少了错误 

#include <iostream>
#include <string>enum class Gender {Male,Female,Unknown
};class Person {
public:Person(const std::string& name, Gender gender) : name_(name), gender_(gender) {}void printInfo() const {std::cout << "Name: " << name_ << ", Gender: ";switch (gender_) {case Gender::Male:std::cout << "Male";break;case Gender::Female:std::cout << "Female";break;default:std::cout << "Unknown";break;}std::cout << std::endl;}private:std::string name_;Gender gender_;
};int main() {Person person("Alice", Gender::Female);person.printInfo();  return 0;
}

总结反思

  • 传递参数的时候优先使用类或者结构体,避免使用基础类型来表示复杂信息
  • 通过传递类型,编译器可以进行检查,这样就减少了运行时候的错误
  • 对于复杂的参数,类成员可以通过类或者枚举进行封装,从而增强代码的逻辑性

3. 使用const防止修改

原因

变量、参数或者返回值声明为const可以防止意外修改,同时在大规模开发中快速得知哪些数据是不可以被修改;其次编译器也会对const修饰的数据进行高效优化,从另一方面也就提高了代码的性能。

const修饰函数参数

通过const修饰函数参数可以防止修改输入的字符串

#include <iostream>
#include <string>void printName(const std::string& name) {  // 使用 const 引用name = "Bob";//错误std::cout << "Name: " << name << std::endl;
}int main() {std::string myName = "Alice";printName(myName);return 0;
}

使用const修饰成员函数

通过将类内的函数声明为const成员函数,也就表示了该函数不会改变对象的状态(调用该成员函数的时候,函数内部不会对该对象的任何成员变量进行更改

#include <iostream>class Person {
public:Person(const std::string& name) : name_(name) {}void printName() const {  // 声明为 conststd::cout << "Name: " << name_ << std::endl;name_ = "Alice"; //错误,无法修改}private:std::string name_;
};int main() {Person person("Bob");person.printName(); return 0;
}

使用const修饰返回值

在返回类型中加入const可以防止外部代码访问数据,保持数据的封装性和安全性

#include <iostream>
#include <vector>class Data {
public:void addValue(int value) {values_.push_back(value);}const std::vector<int>& getValues() const {  // 返回 const 引用return values_;}private:std::vector<int> values_;
};int main() {Data data;data.addValue(42);const std::vector<int>& values = data.getValues();values.push_back(10);  // 导致编译错误for (int val : values) {std::cout << val << " ";  // 输出:42}return 0;
}

 总结反思

  • 优先使用const,通过const修饰的变量、函数参数以及成员函数,可以防止类中的成员被意外修改
  • 编译器会对const修饰的数据进行更多的优化,从而提高程序的性能
  • 通过const返回值来保护对象数据,可以保护类的封装性

4. 了解并利用C++的类型转换

隐式类型转换

隐式类型转换就是编译器自动执行的时候进行的类型转换

int intValue = 42;
double doubleValue = intValue;  // 隐式转换

显式类型转换

程序员指定转换成某种类型,主要有两种方式,一种是强制类型转换,一种就是通过标准库中的函数进行转换

double doubleValue = 42.0;
int intValue = (int)doubleValue;  // C 风格的强制转换
intValue = static_cast<int>(doubleValue);  // 使用 static_cast

C++的类型转换运算符

  • static_cast:安全的静态转换的,一般用于类的上行和下行转换
  • dynamic_cast:安全的多态类型转换
  • const_cast:用于去掉对象的const限定符,允许修改const对象的值,使用的时候需要小心
  • reinterpret_cast:执行低级别的重新解释类型,一般用于指针和整数之间的转换

上行转换与下行转换

上行转换

具体指的就是将派生类对象转换为基类对象,上行转换一般是安全的,编译器是可以自动完成,因为派生类总是拥有基类的所有特性。

上行转换主要用在需要基类类型的地方传递派生类对象,例如将派生类对象存储在接受基类对象的容器中,或者将派生类对象传递给基类指针或者引用参数

  • Derived对象derived,转换为Base*类型的指针baseptr,这个转换就是上行转换,派生类到基类的转化
  • 输出的还是派生类的内容,因为print()是一个虚函数,其内部使用的是动态绑定
  • 补充说明:该处最终还是打印派生类print()内容详细原因
    • 创建虚函数表
      • 编译器检测到Base类的print()函数被声明为虚函数,那么就会为Base类和Derived类都生成虚函数表
      • Base类的虚函数表是包含Base::print()地址,Derived类的虚函数表包含Derived::print()的地址(因为对虚函进行了重写)
    • 上行转换的发生
      • Derived对象转换为一个Base指针,但是这里的basePtr还是指向Derived对象(下面调试图可以看到),但是basePtr的类型是Base*类型
    • 最后basePtr->print()调用的时候,虽然该指针是basePtr类型但是指向的是Derived对象,所以虚函数表中的Print()实际指向的是Derived::print(),所以最后输出的就是“Derived class”

class Base {
public:virtual void print() const {std::cout << "Base class" << std::endl;}
};class Derived : public Base {
public:void print() const override {std::cout << "Derived class" << std::endl;}
};int main() {Derived derived;Base* basePtr = &derived;  basePtr->print();          return 0;
}

下行转换

下行转换就是指将基类对象转换为派生类对象。下行转换通常用于基类指针或者引用,需要将其转换为派生类指针或者引用从而访问派生类特有的成员,一般需要dynamic_cast来确保安全性

  • dynamic_cast将Base*类型的指针转换为Derived*类型的指针,这也就是下行转换
  • 使用dynamic_cast可以确保basePtr实际上指向的是一个Derived对象;如果basePtr不指向Derived,那么dynamic_cast就会返回一个空指针,从而避免错误操作

class Base {
public:virtual ~Base() {}  // 必须有虚析构函数以支持动态多态
};class Derived : public Base {
public:void specificFunction() {std::cout << "Derived specific function" << std::endl;}
};int main() {Base* basePtr = new Derived();       // 上行转换,Derived 转换为 BaseDerived* derivedPtr = dynamic_cast<Derived*>(basePtr);  // 下行转换if (derivedPtr) {  // 确保转换成功derivedPtr->specificFunction();  }else {std::cout << "转换失败!" << std::endl;}delete basePtr;return 0;
}

上行转换与下行转换总结

  • 上行转换:派生类转基类,安全但是经常隐式进行
  • 下行转换:激烈转换为派生类类,需要使用dynamic_cast确保安全

static_cast进行上行和下行转换(前提是安全的)

class Base {
public:virtual ~Base() {}
};class Derived : public Base {};void func(Base* b) {Derived* d = static_cast<Derived*>(b);  // 使用 static_cast 进行下行转换
}

dynamic_cast类型转换

使用该种类型转换方式,运行的时候会检查对象类型,如果转换不安全的话会返回nullptr,这样就可以避免一些潜在的错误

class Base {
public:virtual ~Base() {}
};class Derived : public Base {};void func(Base* b) {Derived* d = dynamic_cast<Derived*>(b);if (d) {// 转换成功,d 可以安全使用} else {// 转换失败,b 不是 Derived 类型}
}

const_cast类型转换

该转换会将const限定符去掉,使用之前需要确定原对象实际上不是const,否则就会导致未定义的行为

void func(const int* p) {int* modifiableP = const_cast<int*>(p);*modifiableP = 42;  // 修改了原来的 const 对象,可能会导致未定义行为
}

总结反思

  • 优先使用 static_cast 和 dynamic_cast:避免使用 C 风格的强制转换,因为它们不提供类型安全检查
  • 小心使用 const_cast 和 reinterpret_cast:这些转换更复杂,使用不当可能导致潜在的错误和未定义行为
  • 使用时注意查阅使用方法

5. 移动语义与完美转发结合使用

移动语义

移动语义就是通过移动资源,而不是复制资源来提高其性能的,在处理大对象或者资源管理的时候尤其有用

移动语义具体是通过右值引用实现,也就是&&,其与传统拷贝构造不同,移动构造函数不会创建副本,而是直接移动资源

#include <iostream>
#include <vector>class MyVector {
public:std::vector<int> data;// 默认构造函数MyVector() : data() {}// 拷贝构造函数MyVector(const MyVector& other) : data(other.data) {std::cout << "Copy constructor called" << std::endl;}// 移动构造函数MyVector(MyVector&& other) noexcept : data(std::move(other.data)) {std::cout << "Move constructor called" << std::endl;}
};int main() {MyVector v1;v1.data.push_back(1);v1.data.push_back(2);MyVector v2 = std::move(v1); return 0;
}

完美转发

完美转发就是将参数原样转到到另一个函数的行为,保持其左值或者右值的属性,主要应用在需要传递任意类型参数的场景

完美转发通常使用转发引用实现,也就是在模板函数中定义参数为T&&,然后结合forward<T>来实现

#include <iostream>
#include <utility>template<typename T>
void wrapper(T&& arg) {process(std::forward<T>(arg));  // 完美转发
}void process(int& x) {std::cout << "Lvalue processed: " << x << std::endl;
}void process(int&& x) {std::cout << "Rvalue processed: " << x << std::endl;
}int main() {int a = 10;wrapper(a);          // 调用 process(int&)wrapper(20);         // 调用 process(int&&)return 0;
}

移动语义结合完美转发

通过模板函数,保持左值或者右值的同时,利用移动语义避免不必要的复制

#include <iostream>
#include <vector>
#include <utility>class MyContainer {
public:std::vector<int> data;MyContainer(std::vector<int>&& vec) : data(std::move(vec)) {}// 移动构造函数MyContainer(MyContainer&& other) noexcept : data(std::move(other.data)) {}
};template<typename T>
MyContainer createContainer(T&& arg) {return MyContainer(std::forward<T>(arg));  // 完美转发
}int main() {std::vector<int> vec = { 1, 2, 3 };MyContainer c1 = createContainer(std::move(vec));  // 移动构造return 0;
}

总结反思

  • 移动语义可以减少不必要的复制,提高程序性能
  • 完美转发允许函数接受参数,然后原样的传递给其他函数,从而保持参数的类型

6. 复制和移动操作中保持对象的一致性

对象一致性重要性分析

 首先对象一致性就是让对象在其生命周期内保持有效状态的能力,当复制或者移动对象的时候,确保源对象和目标对象之间的状态和资源管理是安全

例如在复制构造函数和赋值运算符的时候,需要小心管理资源,需要重点关注两个问题,其一是自我赋值,也就是如果一个对象在赋值的时候与自身相同,可能会导致资源意外释放,其二资源管理,确保每个对象都有独立的资源,避免多个对象指向同一块内存

  • 赋值运算符中,使用判断语句检查是否自我赋值,避免在释放当前对象资源的时候误删了自己
  • 赋值之前,释放旧资源,确保每个对象都有独立的内存
#include <iostream>
#include <cstring>class MyString {
public:char* data;// 构造函数MyString(const char* str) {data = new char[strlen(str) + 1];strcpy(data, str);}// 复制构造函数MyString(const MyString& other) {data = new char[strlen(other.data) + 1];strcpy(data, other.data);}// 赋值运算符MyString& operator=(const MyString& other) {if (this != &other) {  // 防止自我赋值delete[] data;  // 释放旧资源data = new char[strlen(other.data) + 1];strcpy(data, other.data);}return *this;}// 析构函数~MyString() {delete[] data;}
};

移动构造函数和移动赋值运算符

移动操作的时候,需要确保资源的有效转移和源对象的安全状态是一样重要的

移动构造函数移动赋值运算符中,需要将源对象的指针设置为nullptr,从而确保源对象不再拥有原来的资源,这样就可以避免双重释放的风险

通过将资源所有权从源对象转移到目标对象,从而避免不必要的内存分配和赋值

#include <iostream>
#include <utility>class MyString {
public:char* data;// 构造函数MyString(const char* str) {data = new char[strlen(str) + 1];strcpy(data, str);}// 移动构造函数MyString(MyString&& other) noexcept : data(other.data) {other.data = nullptr;  // 确保源对象不再拥有资源}// 移动赋值运算符MyString& operator=(MyString&& other) noexcept {if (this != &other) {delete[] data;  // 释放旧资源data = other.data;other.data = nullptr;  // 确保源对象不再拥有资源}return *this;}// 复制构造函数和赋值运算符同上...// 析构函数~MyString() {delete[] data;}
};

总结与反思

  • 赋值运算符中始终要检查自我赋值
  • 复制和移动操作中,要适当的管理的资源,避免内存泄漏和双重释放
  • 利用RAII原则,同时使用智能指针等资源管理工具简化对资源的管理
  • 程序设计阶段的时候就需要考虑该问题

7. 使用智能指针替代原始指针

原始指针问题

  • 如果分配的内存没有正确释放,那么就会导致内存泄漏
  • 多个指针指向同一块内存的时候,可能会导致双重释放,导致未定义的行为
  • 当指针指向对象被释放后,指针仍然存在,有可能导致访问无效内存

std::unique_ptr的使用

独占式智能指针,表示该指针拥有唯一所有权,可以确保资源的正确释放

#include <iostream>
#include <memory>class MyClass {
public:MyClass() {std::cout << "MyClass constructed" << std::endl;}~MyClass() {std::cout << "MyClass destructed" << std::endl;}
};int main() {std::unique_ptr<MyClass> ptr1(new MyClass());  // 使用 unique_ptr// std::unique_ptr<MyClass> ptr2 = ptr1;  // 编译错误,不能复制std::unique_ptr<MyClass> ptr2 = std::move(ptr1);  // 转移所有权// ptr1 现在为空,ptr2 拥有 MyClass 的所有权return 0;
}

std::shared_ptr智能指针

该智能指针,表示对同一个资源的多个所有权,通过引用计数的方式来管理资源的生命周期

#include <iostream>
#include <memory>class MyClass {
public:MyClass() {std::cout << "MyClass constructed" << std::endl;}~MyClass() {std::cout << "MyClass destructed" << std::endl;}
};int main() {std::shared_ptr<MyClass> ptr1(new MyClass());  // 创建一个 shared_ptr{std::shared_ptr<MyClass> ptr2 = ptr1;  // 共享所有权std::cout << "ptr1 use count: " << ptr1.use_count() << std::endl;  // 输出 2}  // ptr2 超出作用域,引用计数减 1std::cout << "ptr1 use count: " << ptr1.use_count() << std::endl;  // 输出 1return 0;
}

8. 避免遮掩继承而来的名称

名称遮掩理解

名称遮掩就是派生类的成员函数或者数据成员覆盖的了基类的同名成员,从而使得基类中的成员无法通过派生类对象直接访问

出现该种情况主要有以下情况

  • 派生类定义了与基类同名的成员变量
  • 派生类定义了与基类同名的成员函数,即使参数列表不同
  • 派生类定义了与基类同名的typedef或者嵌套类

成员函数名称隐蔽分析

在派生类Derived中定义的Print函数,该函数就遮蔽了Base基类中所有同名的Print函数,所以传入参数3.14就会导致编译器错误

#include <iostream>class Base {
public:void print(int i) const {std::cout << "Base print(int): " << i << std::endl;}void print(double d) const {std::cout << "Base print(double): " << d << std::endl;}
};class Derived : public Base {
public:void print(int i) const {std::cout << "Derived print(int): " << i << std::endl;}
};int main() {Derived d;d.print(42);    d.print(3.14);    return 0;
}

方法1:通过使用基类作用域运算符来显式调用基类的成员

#include <iostream>int main() {Derived d;d.print(42);           // 调用 Derived::print(int)d.Base::print(3.14);   // 调用 Base::print(double)return 0;
}

 方法2:使用using声明引入基类的所有重载函数

也就是通过using声明将基类中的所有同名函数引入到派生类的作用域中,注意是在派生类中引用

class Derived : public Base {
public:using Base::print;  // 引入 Base 类的所有 print 重载版本void print(int i) const {std::cout << "Derived print(int): " << i << std::endl;}
};

总结反思

  • 派生类中定义成员的时候,要注意其与基类成员的同名问题,避免意外的遮蔽
  • 在派生类中可以使用using声明,从而避免遮蔽基类的重载版本
  • 显式的调用基类成员,如果需要调用基类成员,可以使用基类作用域解析运算符Base::来指定

9. 区分接口继承和实现继承

接口继承就是子类继承基类的接口,也就是成员函数的声明,子类可以提供自己的实现;实现继承管就是子类不仅继承了基类的接口,还继承了基类的具体实现,可以直接使用基类的功能,不需要重新写这些成员函数

接口继承

接口继承一般都是通过在基类中声明纯虚函数实现,纯虚函数就是只声明这个接口不实现,所以派生类必须重写这些函数来具体实现

#include <iostream>class Shape {
public:virtual void draw() const = 0;  // 纯虚函数,表示接口继承virtual ~Shape() = default;
};class Circle : public Shape {
public:void draw() const override {std::cout << "Drawing a circle." << std::endl;}
};class Square : public Shape {
public:void draw() const override {std::cout << "Drawing a square." << std::endl;}
};int main() {Circle circle;Square square;circle.draw();  square.draw();  return 0;
}

 实现继承

基类中已经提供了函数的实现,派生类根据自己是否决定是否重写这些函数,这些函数同样是虚函数

#include <iostream>class Animal {
public:virtual void makeSound() const {std::cout << "Animal sound." << std::endl;}virtual ~Animal() = default;
};class Dog : public Animal {
public:void makeSound() const override {std::cout << "Woof!" << std::endl;}
};class Cat : public Animal {// Cat 没有重写 makeSound(),将直接使用基类的实现
};int main() {Dog dog;Cat cat;dog.makeSound();  cat.makeSound();  return 0;
}

 接口继承和实现继承的设计原则

  • 接口继承:基类定义纯虚函数,派生类必须同自己的实现
  • 实现继承:基类提供了虚函数的默认实现,派生类可以直接实现这个功能
  • 使用纯虚函数进行接口继承
  • 使用虚函数的默认实现进行实现继承

接口继承和实现继承结合

真实应用场景中,可以根据自身需要,灵活的决定接口继承和实现继承的配合使用

#include <iostream>class Document {
public:virtual void open() const = 0;      // 纯虚函数,接口继承virtual void save() const {         // 虚函数,提供默认实现std::cout << "Saving document." << std::endl;}virtual ~Document() = default;
};class TextDocument : public Document {
public:void open() const override {std::cout << "Opening text document." << std::endl;}// 使用默认的 save() 实现
};class Spreadsheet : public Document {
public:void open() const override {std::cout << "Opening spreadsheet." << std::endl;}void save() const override {std::cout << "Saving spreadsheet." << std::endl;}
};int main() {TextDocument txtDoc;Spreadsheet sheet;txtDoc.open();   txtDoc.save();   sheet.open();    sheet.save();    return 0;
}

10 考虑virtual函数之外的其他选择

虚函数的局限性分析

首选使用虚函数会有性能开销,因为虚函数表的使用在运行的时候会有开销,尤其是在需要频繁调用的场景中

其次虚函数要求在类层次结构中实现多态,这就可能导致了设计复杂构造和析构期间,虚函数不会表现出多态行为,因为对象还没有完全构造或者销毁。

函数对象替代虚函数

一般使用模板类实现,使用函数对象可以在编译期间确定调用哪个函数,这样就避免了运行时候的虚函数调用开销

class PrintStrategy {
public:template <typename Func>void setPrintFunction(Func func) {printFunction = func;}void print() const {printFunction();}private:std::function<void()> printFunction;
};int main() {PrintStrategy strategy;strategy.setPrintFunction([]() { std::cout << "Printing as text." << std::endl; });strategy.print();  strategy.setPrintFunction([]() { std::cout << "Printing as image." << std::endl; });strategy.print();  return 0;
}

使用模板多态

通过模板,允许在编译的时候就知道使用函数的哪一个版本,模板多态没有虚函数运行时候的开销

#include <iostream>template <typename T>
class Printer {
public:void print(const T& item) const {item.print();}
};class TextPrinter {
public:void print() const {std::cout << "Printing text." << std::endl;}
};class ImagePrinter {
public:void print() const {std::cout << "Printing image." << std::endl;}
};int main() {Printer<TextPrinter> textPrinter;textPrinter.print(TextPrinter());  Printer<ImagePrinter> imagePrinter;imagePrinter.print(ImagePrinter());  return 0;
}

策略模式代替多态

通过将行为封装在不同类中,在运行的时候选择不同的策略,从而实现动态多态的一种方式

#include <iostream>
#include <memory>class PrintStrategy {
public:virtual void print() const = 0;virtual ~PrintStrategy() = default;
};class TextPrintStrategy : public PrintStrategy {
public:void print() const override {std::cout << "Printing text." << std::endl;}
};class ImagePrintStrategy : public PrintStrategy {
public:void print() const override {std::cout << "Printing image." << std::endl;}
};class Document {
public:void setPrintStrategy(std::unique_ptr<PrintStrategy> strategy) {printStrategy = std::move(strategy);}void print() const {if (printStrategy) {printStrategy->print();}}private:std::unique_ptr<PrintStrategy> printStrategy;
};int main() {Document doc;doc.setPrintStrategy(std::make_unique<TextPrintStrategy>());doc.print();  doc.setPrintStrategy(std::make_unique<ImagePrintStrategy>());doc.print();  return 0;
}

不使用虚函数场景分析

  • 如果函数调用的时候开销较大,那么虚函数就可能不合适了,可以通过函数对象或者模版多态;
  • 如果对象的行为在运行的时候反复变化,那么此时就需要使用策略模式
  • 根据需求以及性能,合理选择实现多态的方法

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/2819.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

嵌入式实验2--数码管显示(基础+进阶)

一、数码管显示 0.想实现效果&#xff1a; 数码管交替或同时显示数字 1.电路设计 使用proteus 设计电路 1.1 STM32最小系统 搭建STM32工作基础条件 1.2 数码管电路 1.2.1 数码管 a.共阴极 abg dp都是led灯&#xff0c;这些led灯的公共端接地&#xff0c;就是共阴极&…

【LeetCode:633. 平方数之和 + 双指针】

在这里插入代码片 &#x1f680; 算法题 &#x1f680; &#x1f332; 算法刷题专栏 | 面试必备算法 | 面试高频算法 &#x1f340; &#x1f332; 越难的东西,越要努力坚持&#xff0c;因为它具有很高的价值&#xff0c;算法就是这样✨ &#x1f332; 作者简介&#xff1a;硕…

SpringCloud-Eureka注册中心

假如我们的服务提供者user-service部署了多个实例&#xff0c;如图&#xff1a; 大家思考几个问题&#xff1a; order-service在发起远程调用的时候&#xff0c;该如何得知user-service实例的ip地址和端口&#xff1f;有多个user-service实例地址&#xff0c;order-service调用…

5. 推导仿真

5.1 DCDC电源(以buck电路为例)相关公式推导计算-电感 (1)计算电感的饱和电流和 电感的感值 三角号I:纹波电流 饱和电流:有条件

MySQL rand()函数、rand(n)、生成不重复随机数

文章目录 一、rand()与rand(n)二、rand()使用示例2.1、rand()与order by/group by使用随机排序分组2.2、round()与rand()的组合使用2.3、rand与ceiling的组合使用2.4、rand与floor组合使用2.5、rand与md5组合使用 三、总结3.1、rand()与rand(n)的区别 有时候我们想要生成一个唯…

告别局域网限制!轻松远程访问本地Paperless-ngx文档管理系统远程办公

前言&#xff1a;本文主要介绍如何在Linux系统本地Docker部署Paperless-ngx开源文档管理系统&#xff0c;并结合cpolar内网穿透工具解决本地部署后因为没有公网IP受到局域网访问限制&#xff0c;在异地也能随时远程访问的困扰。 Paperless-ngx是一个开源的文档管理系统&#x…

koa项目实战 == 实现注册登录鉴权

一. 项目的初始化 1 npm 初始化 npm init -y生成package.json文件: 记录项目的依赖 2 git 初始化 git init生成’.git’隐藏文件夹, git 的本地仓库 3 创建 ReadMe 文件 二. 搭建项目 1 安装 Koa 框架 npm install koa2 编写最基本的 app 创建src/main.js const Koa…

中烟创新:以AI审核平台助力烟草行业高效发展

企业对于高效、准确且标准化的文档审核流程需求日益迫切。传统的人工审核方式&#xff0c;不仅耗时费力&#xff0c;而且易受个人主观因素影响&#xff0c;导致审核标准难以统一&#xff0c;影响了工作效率与审核质量。北京中烟创新科技有限公司&#xff08;简称&#xff1a;“…

SpringBoot框架:作业管理技术新解

4 系统设计 4.1系统概要设计 作业管理系统并没有使用C/S结构&#xff0c;而是基于网络浏览器的方式去访问服务器&#xff0c;进而获取需要的数据信息&#xff0c;这种依靠浏览器进行数据访问的模式就是现在用得比较广泛的适用于广域网并且没有网速限制要求的B/S结构&#xff0c…

Chrome 插件怎么安装与下载?超详细

原文链接&#xff1a; https://www.chajianxw.com/tutorial/how-to-install-chrome-plugin.html 前言 国内因为无法访问 Chrome 应用商店的缘故&#xff0c;导致很多优秀的扩展程序无法正常下载与安装。本文将手把手教你如何通过离线安装Chrome 插件&#xff0c;全文图文讲解…

实现图书管理系统

1. 图书管理系统菜单 如上图给用户选项 1. 管理员 2. 普通用户 2. 实现基本框架 右键点src&#xff0c;选择new&#xff0c;选择Package命名三个包 book operation user 1.先选择book包&#xff0c;new两个类 book bookList 在book类中定义书的基本属性&#xff0c;并重写…

黑马官网最新2024前端就业课V8.5笔记---CSS篇(1)

Css 定义 层叠样式表 (Cascading Style Sheets,缩写为 CSS),是一种 样式表 语言,用来描述 HTML 文档的呈现(美化内容) Css 引入方式 内部样式表&#xff1a; CSS 代码写在 style 标签里面 外部样式表: 开发使用 CSS 代码写在单独的 CSS 文件中(.css) 在 HTML 使用 link …

下载pytorch报错:没有发现某个版本

报错如下&#xff1a; 解决&#xff1a; 降级python版本&#xff0c;显卡小于等于 11.3 时&#xff0c;下载pytorch gpu时&#xff0c;python版本选择&#xff1a;3.7即可。

cuda、pytorch-gpu安装踩坑!!!

前提&#xff1a;已经安装了acanoda cuda11.6下载 直接搜索cuda11.6 acanoda操作 python版本3.9 conda create -n pytorch python3.9conda activate pytorch安装Pytorch-gpu版本等包 要使用pip安装&#xff0c;cu116cuda11.6版本 pip install torch1.13.1cu116 torchvi…

音视频入门基础:H.264专题(17)——FFmpeg源码中,获取H.264视频的profile的实现

音视频入门基础&#xff1a;H.264专题系列文章&#xff1a; 音视频入门基础&#xff1a;H.264专题&#xff08;1&#xff09;——H.264官方文档下载 音视频入门基础&#xff1a;H.264专题&#xff08;2&#xff09;——使用FFmpeg命令生成H.264裸流文件 音视频入门基础&…

LeetCode --- 421周赛

题目列表 3334. 数组的最大因子得分 3335. 字符串转换后的长度 I 3336. 最大公约数相等的子序列数量 3337. 字符串转换后的长度 II 一、数组的最大因子得分 数据范围足够小&#xff0c;可以用暴力枚举移除的数字&#xff0c;得到答案&#xff0c;时间复杂度为O(n^2)&#…

Linux下Java的多种方式安装

Linux下Java的多种方式安装 博客&#xff1a; www.lstar.icu 开源地址 Gitee 地址&#xff1a; https://gitee.com/lxwise/iris-blog_parent Github 地址&#xff1a; https://github.com/lxwise/iris-blog_parent 序言 Java是一门面向对象的编程语言&#xff0c;不仅吸收了…

易灵思fpga pwm生成报错

避免复杂总线 选择正确板子 这个是是全部执行 但是不会自动保存 注意设置 或者使用其他文本显示工具 可能约束会掉 注意复位后没有程序 注意软件不同电脑可能报错 问题未知 尽量简单逻辑

JavaEE初阶--servlet篇(三)HttpServlet/response/request对应方法使用

文章目录 1.总括说明2.httpservlet父类2.1方法介绍2.2dopost方法的演示2.3doput方法的演示 3.HttpServletRequest类3.1方法说明3.2方法使用演示3.3getparameter方法使用3.4使用form表单的方式3.5jackson获取参数 4.HttpResponse类4.1设置状态码4.2自动进行刷新4.3重定向跳转4.3…

矩阵起源 CEO 王龙出席 1024 超互联(苏州)总部节点发布会

10月24日&#xff0c;矩阵起源 CEO & 创始人王龙出席了由中关村超互联新基建产业创新联盟、数字人民币研究院联合主办&#xff0c;世纪互联承办的“超互联&#xff08;苏州&#xff09;总部节点发布会”&#xff0c;并分享了矩阵起源及世纪互联在多模态AI数据智能平台与超互…