【Effective C++】阅读笔记3

1. 成员变量声明为Private

建议将成员变量声明为Private,然后再public中提供调用该数据的接口

设置成Private的原因分析

  • 类内成员变量被声明为Private,那么就可以外部代码直接访问或者修改内部数据
  • 通过公共接口获取内部数据,这样可以减少对外部代码的影响
  • 直接将成员变量设置为public,可能会导致数据不一致或者逻辑错误,将成员变量设置为Private就可以避免外界随意修改数据,从而确保数据的完整性

将成员变量设置为public错误事例分析 

外部代码可以随意修改变量的数值,与此同时还可以将其设置负数 

#include <iostream>
#include <string>class Person {
public:std::string name;  int age;           
};int main() {Person p;p.name = "Alice";p.age = -5;  std::cout << "名字:" << p.name << ",年龄:" << p.age << std::endl;return 0;
}

 解决方法:成员变量声明为Private,同时提供公共接口

  • 获取成员变量只可以通过getter方法访问
  • 设置成员变量则是通过setAge,该方法内部会检查数据的有效性

#include <iostream>
#include <string>class Person {
public:Person(const std::string& name, int age) : name_(name) {setAge(age);  // 使用 setter 进行初始化}// Getterstd::string getName() const { return name_; }int getAge() const { return age_; }// Settervoid setAge(int age) {if (age >= 0) {  // 检查年龄有效性age_ = age;}else {std::cerr << "错误:年龄不能为负数" << std::endl;}}private:std::string name_;int age_;
};int main() {Person p("Alice", 25);std::cout << "名字:" << p.getName() << ",年龄:" << p.getAge() << std::endl;p.setAge(-5);  // 试图设置无效年龄std::cout << "名字:" << p.getName() << ",年龄:" << p.getAge() << std::endl;return 0;
}

成员变量声明为Private的好处

还可以支持数据验证和日志记录功能,也就是通过接口访问成员变量的时候,可以添加验证逻辑或者日志逻辑,从而更好的实现数据管理;

成员变量设置为Private后,还可以延迟初始化,避免不必要的消耗

 2. 优先使用非成员非友元函数

主要是为了减少类的复杂性,如果一个函数在实现功能的时候不需要访问类的私有成员,那么就可以将这个类设计成为非成员非友元函数

问题分析

例如在一个表示分数的类中,实现了*运算符重载,此时如果将*运算符重载成为成员函数,那么就会增加这个类的复杂性

#include <iostream>class Rational {
public:Rational(int numerator = 0, int denominator = 1): numerator_(numerator), denominator_(denominator) {}Rational operator*(const Rational& rhs) const {return Rational(numerator_ * rhs.numerator_, denominator_ * rhs.denominator_);}void print() const {std::cout << numerator_ << "/" << denominator_ << std::endl;}private:int numerator_;int denominator_;
};int main() {Rational a(1, 2);Rational b(3, 4);Rational result = a * b;result.print(); return 0;
}

解决方法:如果一个类并不需要访问私有成员(因为a,b的数值都是构造时就赋值了),此时就可以将其设计为非成员非友元函数

  • operator*定义成非成员函数,然后声明为Rational的友元,这样就可以访问其私有成员
  • 通过这样的方法简化结构的同时,还可以访问其成员 
class Rational {
public:Rational(int numerator = 0, int denominator = 1): numerator_(numerator), denominator_(denominator) {}void print() const {std::cout << numerator_ << "/" << denominator_ << std::endl;}private:int numerator_;int denominator_;// 提供访问私有成员的友元声明friend Rational operator*(const Rational& lhs, const Rational& rhs);
};// 非成员运算符重载
Rational operator*(const Rational& lhs, const Rational& rhs) {return Rational(lhs.numerator_ * rhs.numerator_, lhs.denominator_ * rhs.denominator_);
}int main() {Rational a(1, 2);Rational b(3, 4);Rational result = a * b;result.print(); return 0;
}

适合使用该方法场景分析

  • 不依赖对象内部状态的操作,例如只是数学计算、全局功能等
  • 运算符重载
  • 类中的一些辅助函数

总结反思

  • 优先使用非成员非友元函数:如果一个函数不需要访问类的私有成员,将它设计为非成员非友元函数更好
  • 减少类的复杂性:使用非成员函数可以减少类的职责和耦合度,使类的设计更清晰
  • 运算符重载优先使用非成员实现:运算符重载时,优先考虑非成员实现,除非必须访问类的私有成员
  • 提供友元函数访问私有成员:在非成员函数需要访问私有数据时,可以将其声明为友元函数

 3. 如果所有参数都需要类型转换,使用非成员函数

C++中运算符重载时,可能会需要对操作数进行隐式类型转换,当所有操作数都需要进行类型转换的时候,最好选择非成员函数实现运算符重载,因为成员函数的运算符重载只会对右侧参数进行隐式类型转换,而非成员函数则允许对所有参数进行隐式类型转换。

成员函数运算符重载限制分析

成员函数实现运算符重载的时候,隐式类型转换只会应用右侧的参数,也就是说,左侧的参数必须和对象类型匹配,否则编译器不会进行隐式类型转换

  • 下述代码编译的时候,编译器会尝试将2作为*左侧的操作数(this对象,也就是Rational类型)
  • 但是2不是Rational类型,所以最终肯定会导致编译失败

#include <iostream>class Rational {
public:Rational(int numerator = 0, int denominator = 1): numerator_(numerator), denominator_(denominator) {}// 成员函数重载运算符*Rational operator*(const Rational& rhs) const {return Rational(numerator_ * rhs.numerator_, denominator_ * rhs.denominator_);}void print() const {std::cout << numerator_ << "/" << denominator_ << std::endl;}private:int numerator_;int denominator_;
};int main() {Rational r1(1, 2);Rational result = 2 * r1;result.print();return 0;
}

解决方法:使用非成员函数运算符重载

因为非成员函数可以允许对所有参数进行隐式类型转换的,这样就可以让编译器将2隐式转换为Rational类型,从而避免了成员函数的类型限制

#include <iostream>class Rational {
public:Rational(int numerator = 0, int denominator = 1): numerator_(numerator), denominator_(denominator) {}void print() const {std::cout << numerator_ << "/" << denominator_ << std::endl;}private:int numerator_;int denominator_;// 友元声明,允许非成员函数访问私有成员friend Rational operator*(const Rational& lhs, const Rational& rhs);
};// 非成员函数重载运算符*
Rational operator*(const Rational& lhs, const Rational& rhs) {return Rational(lhs.numerator_ * rhs.numerator_, lhs.denominator_ * rhs.denominator_);
}int main() {Rational r1(1, 2);Rational result = 2 * r1;  // 允许隐式类型转换result.print();  return 0;
}

使用场景分析

  • 非成员函数运算符重载
    • 所有参数可能都需要进行隐式类型转换:例如 int * Rational,这时非成员函数更灵活
    • 对称的二元运算符:像 +-*/ 等对称运算符,通常适合实现为非成员函数
    • 允许访问私有成员:通过 friend 声明,可以让非成员函数访问类的私有数据成员,确保函数实现的灵活性
  • 成员函数运算符重载
    • 赋值相关运算符:如 =+=-=*=/=,因为它们会改变左侧操作数的值,通常应实现为成员函数
    • 单目运算符:如前置和后置 ++--、取地址 &、解引用 * 等,通常适合实现为成员函数,因为它们通常只对一个操作数进行操作

4. 实现一个不抛异常的swap函数

自我实现swap函数的原因

首先std库中提供的swap函数默认是通过拷贝构造函数和赋值运算符来实现,但是这些操作有可能会出现异常,其次主要目的就是减少性能开销,提高代码安全性

错误分析,使用默认swap交换一个有动态资源的类

  • swap使用的是拷贝构造和赋值与赋值运算符,那么就很有可能触发内存分配和释放操作
  • 代码中如果构造和赋值运算符抛出异常,那么swap就可能引发异常,最终导致w1和w2状态不一致
  • 中断原因分析:widget类中name_是一个指针类型,当使用swap交换两个widget对象中的name_指针时,一个对象被销毁后,其会释放name_指针指向的内存,而另一个对象也会释放这块已经释放的内存,所以导致了双重释放,最终导致了异常中断
    • 双重释放的根源在于,在没有定义深拷贝的时候,swap使用的是浅拷贝,所以只是简单的拷贝了其数值,所以就造成了多个对象持有相同的指针,所以最终在析构的时候就会重复释放同一块内存,最终引发异常

#include <iostream>
#include <string>
#include <algorithm>  // for std::swapclass Widget {
public:Widget(const std::string& name) : name_(new std::string(name)) {}~Widget() { delete name_; }// 打印 name_void print() const {std::cout << "Widget 名称: " << *name_ << std::endl;}private:std::string* name_;
};int main() {Widget w1("Alice");Widget w2("Bob");std::swap(w1, w2);  // 使用 std::swap,默认调用拷贝构造和赋值w1.print();w2.print();return 0;
}

解决方法:实现一个成员函数swap来直接交换数据成员,这样就可以有效避免不必要的拷贝和赋值

  • 通过交换指针,实现两个对象动态资源的交换,这样就避免了浅拷贝的问题
  • 通过非成员函数调用类中的swap函数,从而确保std::swap兼容

#include <iostream>
#include <string>
#include <utility>  // for std::swapclass Widget {
public:Widget(const std::string& name) : name_(new std::string(name)) {}~Widget() { delete name_; }// 提供一个不抛异常的 swap 成员函数void swap(Widget& other) noexcept {std::swap(name_, other.name_);  // 直接交换指针,不抛异常}// 打印 name_void print() const {std::cout << "Widget 名称: " << *name_ << std::endl;}private:std::string* name_;
};// 提供一个非成员的 swap 函数,以便与 std::swap 兼容
void swap(Widget& lhs, Widget& rhs) noexcept {lhs.swap(rhs);  // 使用 Widget 的成员 swap
}int main() {Widget w1("Alice");Widget w2("Bob");swap(w1, w2);  // 使用自定义 swap 函数w1.print();w2.print();return 0;
}

总结反思

  • 优先自己定义一个没有异常的swap函数,同时通过noexcept声明来确保swap函数不会抛出异常
  • 实现一个非成员swap函数,也就是通过调用成员swap函数,从而确保与std库中的swap函数兼容,使得可以无缝衔接到自己类函数中

5. 尽量延后变量定义的时间

减少性能开销,当定义个类对象的时候,构造函数会立即调用,有可能会涉及到资源分配,所以适当的将变量定义延后,可以避免不必要的初始化

错误分析

例如代码中,即使最终a不是大于b的,变量Result和temp还是被创建了,这就浪费了内存空间

#include <iostream>
#include <string>int main() {int a = 5;int b = 10;int result = 0;  // 提前定义变量std::string temp = "未使用的字符串";  // 提前定义变量if (a > b) {result = a + b;}std::cout << "Result: " << result << std::endl;return 0;
}

解决方法:延后变量的定

也就是说,只有在变量Result和temp需要的时候才被定义,这样就可以避免不必要的内存分配

int main() {int a = 5;int b = 10;if (a > b) {int result = a + b;  // 延后定义变量std::cout << "Result: " << result << std::endl;}if (a == 5) {std::string temp = "延迟定义的字符串";  // 延后定义变量std::cout << "Temp: " << temp << std::endl;}return 0;
}

总结反思

  • 尽量延后变量的定义,减少性能损耗
  • 变量定义在首次使用的地方,可以增强代码可读性

6. 尽量少进行转换

转换增加的劣势

  • 频繁的类型转换会增加代码的复杂程度,使得代码难以理解和维护
  • 运行时的类型转换和多层次的隐式类型转换会损耗性能,最终会拖慢程序的运行速度

隐式类型转换问题分析

  • 代码中如果将double转换成int类型,那么double后面的小数点数据就会丢失

#include <iostream>void printDouble(double value) {std::cout << "Double 值: " << value << std::endl;
}int main() {int intValue = 10;printDouble(intValue);  // 隐式转换 int 到 doubledouble doubleValue = 5.99;int truncatedValue = doubleValue;  // 隐式转换 double 到 intstd::cout << "截断后的 int 值: " << truncatedValue << std::endl;return 0;
}

方法:使用显式类型转换,明确自己想要转换成何种类型

  • 例如可以使用static_cast表明转换意图
#include <iostream>int main() {double doubleValue = 5.99;int truncatedValue = static_cast<int>(doubleValue);  std::cout << "截断后的 int 值: " << truncatedValue << std::endl;return 0;
}

技巧:要避免将基础类型转换为自定义类型的行为

  • 使用explicit关键字避免构造函数进行隐式类型转换,也就是只有在显式声明的时候才可以将double转换为complex对象

  • 隐式类型转换分析
    • Complex c2 = 3.0:double到complex的隐式类型转换
    • 首先explicit关键字的作用就是在一个类中接受单个参数的构造函数,编译器允许使用该函数进行隐式类型转换,所以在这个类中c1对象可以通过传入单个参数完成构造
    • c2报错的原因在于编译器需要从double隐式转换到complex,但是此时构造函数被标记为explicit,所以造成了编译器报错,阻止了这种隐式转换的发生
#include <iostream>class Complex {
public:Complex(double real, double imaginary) : real_(real), imaginary_(imaginary) {}// 避免隐式转换的构造函数explicit Complex(double real) : real_(real), imaginary_(0) {}void print() const {std::cout << "Complex 数: " << real_ << " + " << imaginary_ << "i" << std::endl;}private:double real_;double imaginary_;
};int main() {Complex c1(3.0);  // 合法// Complex c2 = 3.0;  // 编译错误,防止隐式转换c1.print();return 0;
}

总结反思

  • 代码中减少隐式类型转换的使用,从而避免隐式类型转换而导致的错误
  • 多使用显示类型转换,例如可以使用static_cast或者dynamic_cast等,以确保代码意图明确
  • 使用explicit关键字,防止构造函数的隐式类型转换

7. 避免返回对象内部的指针或者引用

直接返回对象的内部指针或者引用会使得对象的实现细节暴露,这样就会导致数据不安全和未定义的行为,特别是当返回指针或者引用是一个局部变量的时候,如果无意中修改了类中的数据,这样就可能会导致问题。

错误分析:返回对象的内部指针或者引用

  • getName函数返回了name_指针,暴露了Person类中的细节
  • 外部可以通过namePtr直接访问或者间接修改Person类的私有数据,最终破坏其封装性

#include <iostream>
#include <string>class Person {
public:Person(const std::string& name) : name_(name) {}// 返回内部指针(不安全)const std::string* getName() const {return &name_;}private:std::string name_;
};int main() {Person p("Alice");const std::string* namePtr = p.getName();std::cout << *namePtr << std::endl;  // 输出:Alice// 非法操作:尽管是 const,外部代码可以修改底层数据或产生悬空指针风险return 0;
}

解决方法:返回name数据的副本,保证不对原始对象的影响

#include <iostream>
#include <string>class Person {
public:Person(const std::string& name) : name_(name) {}// 返回副本std::string getName() const {return name_;}private:std::string name_;
};int main() {Person p("Alice");std::string name = p.getName();std::cout << name << std::endl; // 修改 name 的副本不会影响 Person 对象的 name_name = "Bob";std::cout << p.getName() << std::endl; return 0;
}

返回指针或者引用保证安全方法:使用const限制修改

通过返回数据引用,从而使得const从而确保调用者无法修改返回的内容


#include <iostream>
#include <string>class Person {
public:Person(const std::string& name) : name_(name) {}// 返回 const 引用,防止修改const std::string& getName() const {return name_;}private:std::string name_;
};int main() {Person p("Alice");const std::string& nameRef = p.getName();std::cout << nameRef << std::endl; nameRef = "Bob"; return 0;
}

 使用智能指针安全的返回指针和引用

针对于动态资源分配,使用智能指针可以更好的管理资源分配

  • 通过智能指针管理动态分配的资源,同时通过const限制修改权限
#include <iostream>
#include <memory>
#include <string>class Person {
public:Person(const std::string& name) : name_(std::make_shared<std::string>(name)) {}// 返回智能指针std::shared_ptr<const std::string> getName() const {return name_;}private:std::shared_ptr<const std::string> name_;
};int main() {Person p("Alice");std::shared_ptr<const std::string> namePtr = p.getName();std::cout << *namePtr << std::endl;  // 输出:Alice// *namePtr = "Bob";  // 编译错误,无法修改return 0;
}

总结反思

  •  要避免返回内部数据的指针或引用,尽量选择返回数据的副本,从而保证类的封装性和安全性
  • 如果必须返回引用或者指针,那么使用const限制访问权限,防止其调用修改内部数据
  • 如果是动态资源,需要分配内存空间,那么优先使用智能指针对资源进行管理,从而避免内存泄漏或者悬空指针的情况

8. 为“异常安全”努力

核心意思就是实现在抛出异常的时候保证程序运行安全,也就是在抛出异常的时候要确保状态的完整性和一致性

异常安全的三种保护标准

  • 基本保证:也就是即使发生异常,程序不会发生资源泄漏或者数据不会发生资源泄漏或者数据损坏,即使程序在异常发生后不能恢复正常操作
  • 强烈保证:要求程序在异常后可以退回到异常发生前的状态,也就是操作要么成功要么完全失败,类似于数据库中的原子性
  • 不抛出异常安全保证:承诺某个函数绝对不会抛出异常,也是最强的安全保证,可以通过noexcept关键字声明不抛出异常的函数

方法1:使用RAII

通过RAII机制,保证即使发生异常的时候,也可以保证资源正常释放

#include <iostream>
#include <memory>
#include <string>class Person {
public:Person(const std::string& name) : name_(std::make_unique<std::string>(name)){}void printName() const {std::cout << "姓名: " << *name_ << std::endl;}private:std::unique_ptr<std::string> name_;
};int main() {try {Person p("Alice");p.printName();}catch (...) {std::cout << "异常发生" << std::endl;}return 0;
}

方法2:使用noexcept保证不抛出异常

利用该关键字声明一个函数不会抛出异常,通过noexcept提高代码的稳定性和性能

  • swap函数使用noexcept声明不抛出异常,从而保证swap操作异常的安全性
#include <iostream>
#include <utility>
#include <vector>class Widget {
public:Widget(int value) : value_(value) {}void swap(Widget& other) noexcept {std::swap(value_, other.value_);}private:int value_;
};int main() {Widget w1(1);Widget w2(2);w1.swap(w2);  // 不抛出异常的 swap 操作return 0;
}

方法3:使用事务式编程 

也就是进行操作之前,先创建一个副本或者备份,目的就是确保操作成功后替换原始对象,这样即使操作过程中发生异常的时候,程序依然可以恢复原状

class Transaction {
public:Transaction(const std::string& data) : data_(data) {}void setData(const std::string& newData) {// 先创建备份,保证操作失败时能够恢复std::string backup = data_;// 假设此操作可能抛出异常processData(newData);// 更新成功才替换原数据data_ = newData;}void printData() const {std::cout << "数据: " << data_ << std::endl;}private:void processData(const std::string& newData) {if (newData.empty()) throw std::runtime_error("无效的数据");}std::string data_;
};int main() {Transaction transaction("初始数据");try {transaction.setData("");}catch (const std::exception& e) {std::cout << "异常: " << e.what() << std::endl;}transaction.printData();  // 输出:新数据或初始数据return 0;
}

总结反思

  • 优先使用智能指针管理动态内存资源,从而减少资源泄漏的风险
  • 优先使用noexcept,可以避免其抛出异常
  • 要谨慎操作状态修改,使用备份或者事务机制,从而确保即使发生异常也可以回滚

9. 理解Inline函数的利弊

内联函数就是将函数代码直接嵌入到调用点,从而避免函数调用的开销,编译器会在调用点替换函数调用为函数体代码,这样就减少了调用堆栈的实践。

内联函数的优点分析

  • 减少函数调用时的开销
    • 因为调用函数的时候,程序会进行一系列的操作,例如压栈、跳转到函数地址、返回值处理等,这些都会增加函数的开销
  • 提高性能
    • 对于一些小型、频繁调用的函数使用内联函数可以显著的提高性能
class Rectangle {
public:Rectangle(int width, int height) : width_(width), height_(height) {}// 内联 getter 函数inline int getWidth() const { return width_; }inline int getHeight() const { return height_; }private:int width_;int height_;
};int main() {Rectangle rect(10, 20);int area = rect.getWidth() * rect.getHeight();  // 内联后无调用开销return 0;
}

内联函数的缺点

  • 增加代码体积
    • 内联函数会显著增加代码的体积,也就会导致指令缓存压力增加,最终降低性能
  • 编译时间加长
    • 内联增加了编译器的负担,特别是当函数定义在头文件中时,每次编译都会将函数体嵌入每个调用点,导致编译时间变长,且链接时间也可能增加
  • 内联函数多不方便进行调试

内联函数适用的场景

小型而且简单函数,因为这样的函数逻辑简单,编译器更好对其进行优化;对于一些执行频繁而且对性能要求高的小函数,内联函数也可以显著的提高其性能。

不适合内联函数分析

  • 复杂大型的函数
  • 递归函数
    • 因为递归的调用次数在编译期间是未知的,递归的内联会导致代码膨胀
  • 虚函数
    • 调用的时候需要通过虚函数表进行解析,编译器通常无法再编译期间确定调用的具体函数

总结反思

  • 小函数适合使用内联,相反大且复杂的函数不适合内联
  • 递归和虚函数不适合内联,因为递归次数是不确定,且虚函数通常需要通过表去查找,内联通常会被编译器忽略
  • 编译器自动优化,编译器会自动识别哪些函数适合内联

10. 将文件间的编译依赖降到最低

文件之间编译过度依赖代价

  • 增加编译时间
    • 因为每次修改头文件,或者所有直接或间接依赖该头文件的文件都需要重新编译,这可能导致整个项目的编译时间显著增加。随着项目规模增长,频繁的头文件依赖会拖慢编译速度
  • 耦合度增加
    • 修改一个文件,会影响其他所有与之相关的文件
  • 代码难以维护

方法1:使用前置声明

头文件中尽量使用前置声明,而不是选择包含完整的头文件,使用前置声明一个类的存在,而不需要包含其完整定义,从而减少头文件之间的依赖关系

  • 前置声明适用于指针或者引用成员变量的声明
// Employee.h
#include <string>class Department;  // 前向声明 Department 类class Employee {
public:Employee(const std::string& name);void setDepartment(Department* dept);  // 使用前向声明的指针
private:std::string name_;Department* department_;
};
// Department.h
#include <string>class Department {
public:Department(const std::string& name);
private:std::string name_;
};

方法2:使用#include在源文件中实现依赖

也就是说将头文件的依赖放在源文件中,而不是头文件中,这样就可以减少头文件的包含。只有在类的成员函数需要某个类的定义时,才在.cpp文件中使用#include所需头文件即可

// Employee.h (头文件中只放前置声明)
#include <string>class Department;  // 使用前向声明class Employee {
public:Employee(const std::string& name);void setDepartment(Department* dept);
private:std::string name_;Department* department_;
};
// Employee.cpp
#include "Employee.h"
#include "Department.h"  // 只在 .cpp 文件中包含Employee::Employee(const std::string& name) : name_(name), department_(nullptr) {}void Employee::setDepartment(Department* dept) {department_ = dept;
}

方法3:pimpl方法

在类中使用指针指向该类的方法,将类的实现细节隐藏在源文件中,从而降低头文件的依赖关系;简单可以理解成调用的时候,直接通知封装好的指针

// Widget.h
#include <memory>
class WidgetImpl;  // 前向声明 WidgetImpl 类class Widget {
public:Widget();~Widget();void doSomething();private:std::unique_ptr<WidgetImpl> pImpl;  // Pimpl 指针
};
// Widget.cpp
#include "Widget.h"
#include "WidgetImpl.h"  // 只有在实现文件中包含实现类Widget::Widget() : pImpl(std::make_unique<WidgetImpl>()) {}Widget::~Widget() = default;void Widget::doSomething() {pImpl->performTask();
}

总结反思

  • 头文件使用前向声明,减少对编译的依赖
  • 尽量在.cpp文件中使用#include,而不是头文件
  • pimpl收发,通过指针调用,隐藏细节

 

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

我在命令行下学日语

同一个动作重复 300 遍&#xff0c;肌肉就会有记忆&#xff0c;重复 600 遍&#xff0c;脊柱就会有记忆&#xff0c;学完五十音图不熟练&#xff0c;经常遗忘或者要好几秒才想得起来一个怎么办&#xff1f;没关系&#xff0c;我做了个命令行下的小游戏 KanaQuiz 来帮助你记忆&a…

c++:vector

一、vector是什么&#xff1f; 1.1 vector的介绍 vector是表示可变大小数组的序列容器。 就像数组一样&#xff0c;vector也采用的连续存储空间来存储元素。也就是意味着可以采用下标对vector的元素进行访问&#xff0c;和数组一样高效。但是又不像数组&#xff0c;它的大小是…

一键切换暗黑模式,这些代码片段你不可错过

文章目录 前言正文1.多主题切换2.使用 SASS 实现轻松深色模式3.动画切换浅色与深色模式4.纯 CSS 主题切换5.GitHub 风格的深色模式切换6.持久深色模式7.基本 Vue 响应式切换8.创意灯泡切换 总结 前言 如今&#xff0c;许多网站设计师都会为用户提供浅色和深色模式的选择。这不…

雷军救WPS“三次”,WPS注入新生力量,不再“抄袭”微软

救WPS“三次” 1989年&#xff0c;求伯君用128万行代码编写出了WPS1.0&#xff0c;宣告了中国自主办公时代的开启。 那时候&#xff0c;雷军还在武汉大学深造&#xff0c;他早就把求伯君当成了自己的榜样&#xff0c;这一来二去的&#xff0c;雷军和WPS之间也就结下了不解之缘…

[MySQL#10] 索引底层(1) | Page | 页目录

目录 1. 初识索引 2. 认识磁盘 3. MySQL与磁盘交互基本单位 4. 索引的理解 1. 重谈Page 2. 为什么IO交互要用Page 3. 有主键的表插入数据时的排序 4. 单个Page与多个Page 4.1 单个Page 4.2 多个Page 目录 单Page目录 多Page目录 在看本文之前&#xff0c;可以回顾…

sklearn 实现随机森林分类器 - python 实现

python sklearn 实现随机森林分类器 from sklearn.ensemble import RandomForestClassifier from sklearn.datasets import load_iris # 加载数据集 irisload_iris() x,yiris.data,iris.target print("x y shape:",x.shape,y.shape) # 创建并训练模型 model Random…

Altium Designer使用技巧(二)

一、创建类 1、按DC键&#xff0c;打开对象类。 2、右键添加一个类。命名为PWR。 3、将所有的电源类&#xff0c;全部添加到新创建的类中&#xff0c;从非成员类中点选到成员类中。 4、右下角点panes ,点PCB。 5、然后在左边单击PWR&#xff0c;点连接&#xff0c;可显示或…

<十六>Ceph mon 运维

Ceph 集群有故障了&#xff0c;你执行的第一个运维命令是什么&#xff1f; 我猜测是ceph -s 。无论执行的第一个命令是什么&#xff0c;都肯定是先检查Mon。 在开始之前我们有必要介绍下Paxos协议&#xff0c;毕竟Mon就是靠它来实现数据唯一性。 一&#xff1a; Paxos 协议 1…

NPOI 操作详解(操作Excel)

目录 1. 安装 NPOI 2. 使用 NPOI 创建新 Excel 文件 3. 设置列宽和行高 1. 设置列宽 2. 设置行高 3. 同时设置列宽和行高 4. 设置统一的行高 5. 设置统一的列宽 6. 应用统一的行高和列宽 4. 合并单元格 5. 设置单元格样式&#xff08;字体、边框、背景色等&#xf…

多处理机调度(李昂学长视频总结)25新增考点

多处理机定义&#xff1a;多处理机指的是某个计算机系统中有多个cpu&#xff0c;在多处理机调度中&#xff0c;多处理机一般指的是共享存储器处理机&#xff0c;其两个或更多的cpu全部共享一个公用的RAM。 根据系统中的处理机相同与否&#xff0c;可将多处理机系统分为如下两类…

少儿编程培训市场突破500亿元:教育新蓝海的崛起与未来展望

近年来&#xff0c;随着科技的迅速发展和家长对教育方式的重视&#xff0c;少儿编程市场成为一片新的蓝海。据最新市场调研报告显示&#xff0c;2024年中国少儿编程培训市场规模已突破500亿元&#xff0c;预计未来五年将持续增长。这一趋势反映了少儿编程教育的迅速崛起&#x…

【大数据学习 | kafka】producer的参数与结构

1. producer的结构 producer&#xff1a;生产者 它由三个部分组成 interceptor&#xff1a;拦截器&#xff0c;能拦截到数据&#xff0c;处理完毕以后发送给下游&#xff0c;它和过滤器不同并不是丢弃数据&#xff0c;而是将数据处理完毕再次发送出去&#xff0c;这个默认是不…

【论文速读】Optimization-based Prompt Injection Attack to LLM-as-a-Judge

基于优化的提示词注入攻击 摘要引言问题描述LLM-as-a-judge威胁模型攻击者知道什么 JUDGEDECEIVER 细节概述生成影子候选回复公式化为优化问题Target-aligned generation lossTarget-enhancement lossAdversarial perplexity loss优化问题 求解优化问题 摘要 LLM-as-a-Judge 利…

人工智能证书合集

本文将对目前市面上主流官方机构颁发的人工智能证书进行整理和介绍&#xff0c;由于整理的证书较多&#xff0c;本文共一万八千多字&#xff0c;请根据自己的考证需求阅读对应部分的内容&#xff0c;希望本文对人工智能行业的从业人员和计划从事人工智能相关岗位工作的人员有所…

Java入门8——二维数组

今天的内容算是数组的收尾~~ 从下次开始就要开始学习类和对象了&#xff0c;冲冲冲&#xff01; 首先二维数组&#xff0c;也很好理解&#xff0c;就是把几个一维数组拼在一起了&#xff0c;我们用代码来熟悉一下~ public class javaSchool {public static void main(String[…

自动售饮料机控制电路的设计

自动售饮料机控制电路的设计 1 设计目的 &#xff08;1&#xff09;熟悉数字电路的应用。 &#xff08;2&#xff09;掌握常常利用逻辑运算器及D触发器的逻辑功能及利用方式。 &#xff08;3&#xff09;熟悉电路仿真软件Multisim 利用。 &#xff08;4&#xff09;了解自动售饮…

高速高精运动控制解决方案亮相2024 NEPCON亚洲电子展!

■展会名称&#xff1a; NEPCON ASIA 2024 亚洲电子生产设备暨微电子工业展览会&#xff08;以下简称“亚洲电子展”&#xff09; ■展会日期 2024年11月6 -8日 ■展馆地点 中国深圳国际会展中心(宝安) ■展位号 11号馆-11A24 11月6日至8日&#xff0c;亚洲电子展将在中…

Flask轻松上手:从零开始搭建属于你的Web应用

目录 一、准备工作 二、安装Flask 三、创建你的第一个Flask应用 创建一个新的Python文件 编写Flask应用代码 运行Flask应用 四、创建一个简单的博客系统 定义路由和文章列表 创建模板文件 运行并测试博客系统 五、使用数据库存储用户信息 安装Flask-SQLAlchemy 修…

STM32开发 —— 新工程创建思路终于清晰了

目 录 工程创建三步法一、工程文件夹创建二、管理工程项三、配置工程参数 工程创建三步法 从ST官网下载好stm32标准库或HAL库&#xff0c;HAL库目录如下。 在Keil开发环境中创建STM32工程&#xff0c;分三大步即可完成工程的创建&#xff1a; 一步&#xff1a;在本地磁盘创建…

Java SpringBoot调用大模型AI构建AI应用

本文是一个用springboot 结合spring mvc 和spring ai alibaba 调用国产大模型通义千问的具体例子&#xff0c;按照这个做能够快速的搞定Java应用的调用。 然后就可以把这类应用泛化到所有的涉及到非结构化数据结构化的场景中。 Spring AI&#xff1a;简化Java中大模型调用的框…