【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;
}
不使用虚函数场景分析
- 如果函数调用的时候开销较大,那么虚函数就可能不合适了,可以通过函数对象或者模版多态;
- 如果对象的行为在运行的时候反复变化,那么此时就需要使用策略模式
- 根据需求以及性能,合理选择实现多态的方法
相关文章:

【Effective C++】阅读笔记4
1. 确保公有继承中有is-a的关系 Is-a关系理解 该关系就是派生类应该具备基类的所有特性,并且可以替代基类对象使用,例如猫和狗都是动物的派生类,因为猫和狗都和动物形成了is-a关系,猫和狗都是动物。 在该关系下,派生类…...

浅谈mysql【8.0】链接字符串
string connectionString "serveryour_server;useryour_user;passwordyour_password;databaseyour_database;sslmodenone;allowPublicKeyRetrievaltrue;Allow User VariablesTrue;";在 C# 中配置 MySQL 数据库连接字符串时,可以通过添加多个参数来控制连…...
BERT,RoBERTa,Ernie的理解
BERT: 全称:Bidirectional Encoder Representations from Transformers。可以理解为 “基于 Transformer 的双向编码器表示”。含义:是一种用于语言表征的预训练模型。它改变了以往传统单向语言模型预训练的方式,能够联合左侧和右…...

获取 Wind 数据并进行简单的择时分析
使用Python获取Wind数据并进行简单的择时分析时,需要按照以下步骤操作。 (1)登录Wind官网,在“金融解决方案”的下拉列表里选择“金融终端”选项,如下图3.2所示。 (2)根据自己计算机的实际情况…...

小檗碱的酵母代谢工程生物合成-文献精读78
De novo production of protoberberine and benzophenanthridine alkaloids through metabolic engineering of yeast 将酵母代谢工程应用于原小檗碱和苯并啡啶类生物碱的从头合成 苄基异喹啉类生物碱的微生物合成-文献精读77 香叶醇酵母生产机器学习优化酵母-文献精读66 黄…...
文件指针和写入操作
文件指针位置 w 模式: 打开文件时,文件指针位于文件的开头。如果文件已存在,文件内容会被清空。写入的数据会从文件开头开始覆盖原有内容。 a 模式: 打开文件时,文件指针位于文件的末尾。如果文件已存在,文…...

跨越科技与文化的桥梁——ROSCon China 2024 即将盛大开幕
在全球机器人技术飞速发展的浪潮中,ROS(Robot Operating System)作为一款开源的机器人操作系统,已成为无数开发者、研究人员和企业的首选工具。为了进一步推动ROS的应用与发展,全球知名的机器人操作系统会议——ROSCon…...

springboot+shiro 权限管理
一、为什么要了解权限框架 权限管理框架属于系统安全的范畴,权限管理实现对用户访问系统的控制,按照安全规则用户可以访问而且只能访问自己被授权的资源。 目前常见的权限框架有Shiro和Spring Security,本篇文章记录springboot整合sh…...

PureMVC在Unity中的使用(含下载链接)
前言 Pure MVC是在基于模型、视图和控制器MVC模式建立的一个轻量级的应用框架,这种开源框架是免费的,它最初是执行的ActionScript 3语言使用的Adobe Flex、Flash和AIR,已经移植到几乎所有主要的发展平台,支持两个版本框架…...

25国考照片处理器使用流程图解❗
1、打开“国家公务员局”网站,进入2025公务员专题,找到考生考务入口 2、点击下载地址 3、这几个下载链接都可以 4、下载压缩包 5、解压后先看“使用说明”,再找到“照片处理工具”双击。 6、双击后会进入这样的界面,点击&…...

一位纯理科生,跨界自学中医,自行组方治好胃病、颈椎病与高血脂症,并在最权威的中国中医药出版社出版壹本专业中医图书!
这是一位铁杆中医迷, 也是《神农本草经——精注易读本》的作者。 希望更多的人能够受到启发,感受中医之神奇,敢于跨界,爱好中医,学习中医! 一个病人以自己的切身感受与诊断,并使之汤药治愈疾病&…...

运动控制 双轮差速模型轨迹规划
文章目录 一、轨迹规划1.1轨迹平滑与轮迹1.2 目标距离1.3 速度限制1.4 候选速度的计算与调整1.5 路径生成 二、双轮轨迹2.1 计算梯度2.2 计算偏移轨迹2.3 返回结果 一、轨迹规划 1.1轨迹平滑与轮迹 初始时,我们有一条由若干坐标点构成的机器人运行路径。通过对这些…...

使用 Sortable.js 库 实现 Vue3 elementPlus 的 el-table 拖拽排序
文章目录 实现效果Sortable.js介绍下载依赖添加类名导入sortablejs初始化拖拽实例拖拽完成后的处理总结 在开发过程中,我们经常需要处理表格数据,并为用户提供便捷的排序方式。特别是在需要管理长列表、分类数据或动态内容时,拖拽排序功能显得…...
MySQL索引相关介绍及优化(未完...)
如何看一条SQL语句的执行好坏? MySQL提供了自带的工具Explain可以查看sql语句的执行好坏。 explain主要的列: 1:type:这一列表示MySQL决定如何查找表中的行,查找数据行记录的大概范围。 有 system const eq_ref ref…...

【AI+教育】一些记录@2024.11.04
一、尝新 今天尝试了使用九章随时问,起因是看到快刀青衣的AI产品好用榜,里面这么介绍九章随时问:「它不是像其他产品那样,直接给你出答案。而是跟你语音对话,你会感觉更像是有一位老师坐在你的旁边,一步步…...

三维测量与建模笔记 - 2.2 射影几何
教程中H矩阵写的有问题,上图中H矩阵应该是(n1) x (m1) 共点不变性,下图中黄色方块标记的点,在射影变换前后,虽然直线的形状有所变化,但仍然相交于同一个点。 共线不变性,下图黄色标记的两个点,在…...

论文速读:简化目标检测的无源域适应-有效的自我训练策略和性能洞察(ECCV2024)
中文标题:简化目标检测的无源域适应:有效的自我训练策略和性能洞察 原文标题:Simplifying Source-Free Domain Adaptation for Object Detection: Effective Self-Training Strategies and Performance Insights 1、Abstract 本文重点关注计算…...

ros与mqtt相互转换
vda5050 VDA5050协议介绍 和 详细翻译-CSDN博客 ros与mqtt相互转换 如何转换的,通过某个中转包,获取ros的消息然后以需要的格式转换为mqtt 需要的参数 ros相关 parameters[ (ros_subscriber_type, vda5050_msgs/NodeState), (ros_subscriber_queue…...

Golang | Leetcode Golang题解之第522题最长特殊序列II
题目: 题解: func isSubseq(s, t string) bool {ptS : 0for ptT : range t {if s[ptS] t[ptT] {if ptS; ptS len(s) {return true}}}return false }func findLUSlength(strs []string) int {ans : -1 next:for i, s : range strs {for j, t : range s…...

安卓开发之数据库的创建与删除
目录 前言:基础夯实:数据库的创建数据库的删除注意事项 效果展示:遇到问题:如何在虚拟机里面找到这个文件首先,找到虚拟机文件的位置其次,找到数据库文件的位置 核心代码: 前言: 安…...

智慧医疗能源事业线深度画像分析(上)
引言 医疗行业作为现代社会的关键基础设施,其能源消耗与环境影响正日益受到关注。随着全球"双碳"目标的推进和可持续发展理念的深入,智慧医疗能源事业线应运而生,致力于通过创新技术与管理方案,重构医疗领域的能源使用模式。这一事业线融合了能源管理、可持续发…...
Auto-Coder使用GPT-4o完成:在用TabPFN这个模型构建一个预测未来3天涨跌的分类任务
通过akshare库,获取股票数据,并生成TabPFN这个模型 可以识别、处理的格式,写一个完整的预处理示例,并构建一个预测未来 3 天股价涨跌的分类任务 用TabPFN这个模型构建一个预测未来 3 天股价涨跌的分类任务,进行预测并输…...
全面解析各类VPN技术:GRE、IPsec、L2TP、SSL与MPLS VPN对比
目录 引言 VPN技术概述 GRE VPN 3.1 GRE封装结构 3.2 GRE的应用场景 GRE over IPsec 4.1 GRE over IPsec封装结构 4.2 为什么使用GRE over IPsec? IPsec VPN 5.1 IPsec传输模式(Transport Mode) 5.2 IPsec隧道模式(Tunne…...

听写流程自动化实践,轻量级教育辅助
随着智能教育工具的发展,越来越多的传统学习方式正在被数字化、自动化所优化。听写作为语文、英语等学科中重要的基础训练形式,也迎来了更高效的解决方案。 这是一款轻量但功能强大的听写辅助工具。它是基于本地词库与可选在线语音引擎构建,…...
scikit-learn机器学习
# 同时添加如下代码, 这样每次环境(kernel)启动的时候只要运行下方代码即可: # Also add the following code, # so that every time the environment (kernel) starts, # just run the following code: import sys sys.path.append(/home/aistudio/external-libraries)机…...

基于Java+VUE+MariaDB实现(Web)仿小米商城
仿小米商城 环境安装 nodejs maven JDK11 运行 mvn clean install -DskipTestscd adminmvn spring-boot:runcd ../webmvn spring-boot:runcd ../xiaomi-store-admin-vuenpm installnpm run servecd ../xiaomi-store-vuenpm installnpm run serve 注意:运行前…...
uniapp 实现腾讯云IM群文件上传下载功能
UniApp 集成腾讯云IM实现群文件上传下载功能全攻略 一、功能背景与技术选型 在团队协作场景中,群文件共享是核心需求之一。本文将介绍如何基于腾讯云IMCOS,在uniapp中实现: 群内文件上传/下载文件元数据管理下载进度追踪跨平台文件预览 二…...
加密通信 + 行为分析:运营商行业安全防御体系重构
在数字经济蓬勃发展的时代,运营商作为信息通信网络的核心枢纽,承载着海量用户数据与关键业务传输,其安全防御体系的可靠性直接关乎国家安全、社会稳定与企业发展。随着网络攻击手段的不断升级,传统安全防护体系逐渐暴露出局限性&a…...
【HarmonyOS 5】鸿蒙中Stage模型与FA模型详解
一、前言 在HarmonyOS 5的应用开发模型中,featureAbility是旧版FA模型(Feature Ability)的用法,Stage模型已采用全新的应用架构,推荐使用组件化的上下文获取方式,而非依赖featureAbility。 FA大概是API7之…...

基于江科大stm32屏幕驱动,实现OLED多级菜单(动画效果),结构体链表实现(独创源码)
引言 在嵌入式系统中,用户界面的设计往往直接影响到用户体验。本文将以STM32微控制器和OLED显示屏为例,介绍如何实现一个多级菜单系统。该系统支持用户通过按键导航菜单,执行相应操作,并提供平滑的滚动动画效果。 本文设计了一个…...