《Effective Modren C++》 进阶学习(上)

《Effective Modren C++》 进阶学习(上)

文章目录

  • 《Effective Modren C++》 进阶学习(上)
    • 引言
    • 1. 理解模板类型推导
    • 2. 理解auto类型推导
    • 3. 理解decltype
    • 4. 学会查看类型推导结果
    • 5. 优先考虑auto而非显式类型声明
    • 6. auto推导若非己愿,使用显式类型初始化惯用法
    • 7. 区别使用 () 和 {} 创建对象
    • 8. 优先考虑nullptr而非0和NULL
    • 9. 优先考虑别名声明而非typedef
    • 10. 优先考虑限域枚举而非未限域枚举
    • 11. 优先考虑使用deleted函数而非使用未定义的私有声明
    • 12. 使用override声明重写函数
    • 13. 优先考虑const_iterator而非iterator
    • 14. 如果函数不抛出异常请使用noexcept
    • 15. 尽可能的使用constexpr
    • 16. 让const成员函数线程安全
    • 17. 理解特殊成员函数的生成

引言


  作为一名有追求的程序猿,一定是希望自己写出的是最完美的、无可挑剔的代码。那完美的标准是什么,我想不同的设计师都会有自己的一套标准。而在实际编码中,如何将个人的标准愈发完善,愈发得到同事的认可,一定需要不断积累。如何积累,一定是从细微处着手,观摩优秀的代码,学习现有的框架,汲取前人留下的智慧。

  本篇是拜读《Effective Modren C++》后的笔记。《Effective Modren C++》是由世界顶级C++技术权威专家Scott Meyers所著, 旨在帮助开发者更好地理解和应用现代C++的特性和最佳实践。该书是Scott Meyers继《Effective C++》和《More Effective C++》之后的续集,针对C++11、C++14和C++17引入的新特性进行了深入讲解。

1. 理解模板类型推导


模板类型推导(template type deduction)指的是编译器通过函数参数的类型来推断模板参数的类型,从而确定函数模板的实例化类型。某些情况下,ParamType并不是和函数参数类型一样,而是依据参数推导出的(划重点)

使用模板:

template<typename T>
void f(ParamType param); // ParamType 写法上包含T

f(expr);  // 从expr推导ParamType和T

一些情况下,ParamTypeexpr的类型相同;但是也存在两者不同的情况,此时T的推导也有所不同。分三种场景来分析:

场景一:ParamType是指针或引用但不是通针引用
在这种场景下,类型推导会如下进行:

  • 如果expr类型是引用,忽略引用部分。
  • 剩下的部分决定T,然后T与形参匹配得到ParamType。

举个例子,模板如下:

template<typename T>
void f(T & param); //param是一个引用

声明如下变量:

int x=27; //x是int
const int cx=x; //cx是const int
const int & rx=cx; //rx是指向const int的引用

当将如上变量传递给f时,推导如下:

f(x);  //T是int,param的类型是int&
f(cx); //T是const int,param的类型是const int &
f(rx); //T是const int,param的类型是const int &

场景二:ParamType是通用引用
当ParamType是通用引用,情况会变得复杂。类型推导如下进行:

  • 如果expr是左值,TParamType都会被推导为左值引用。
    第一,这是模板类型推导中唯一一种TParamType都被推导为引用的情况。
    第二,虽然ParamType被声明为右值引用类型,但是最后推导的结果它是左值引用。
  • 如果expr是右值,就使用场景一的推导规则。

举个例子:

template<typename T>
void f(T&& param);  //param现在是一个通用引用类型

int x=27;           //如之前一样
const int cx=x;     //如之前一样
const int & rx=cx;  //如之前一样

f(x);      //x是左值,所以T是int&
           //param类型也是int&

f(cx);     //cx是左值,所以T是const int &
           //param类型也是const int&

f(rx);     //rx是左值,所以T是const int &
           //param类型也是const int&

f(27);     //27是右值,所以T是int
           //param类型就是int&&

场景三:ParamType既不是指针也不是引用
ParamType既不是指针也不是引用时,通过传值(pass-by-value)的方式处理:

template<typename T>
void f(T param); //以传值的方式处理param

此时param会拷贝形参,因此对param的修改不会影响到原参数。类型推导如下进行:

  • 和之前一样,如果expr的类型是一个引用,忽略这个引用部分。
  • 如果忽略引用之后exprconst,那就再忽略const。如果它是volatile,也会被忽略(关于volatile的细节参考Item40)
int x=27;          //如之前一样
const int cx=x;    //如之前一样
const int & rx=cx; //如之前一样
f(x);    //T和param都是int
f(cx);   //T和param都是int
f(rx);   //T和param都是int

当形参为指向const的指针或者指向const的引用时,在类型推导const会被保留。如下示例:

template<typename T>
void f(T param); //传值

const char* const ptr = //ptr是一个常量指针,指向常量对象
" Fun with pointers";

此种情况,T会被推导为const char*,指针自身的const会被忽略,指向的数据为常量会被保留。

数组实参

  • 当数组作为实参,在场景三时,会被转化为指针形式推导。
  • 当数组作为实参,在场景一时,会被推到为数组的引用。
const char array[] = "hello world"; 

template<typename T>
void f1(T param);   //传值

template<typename T>
void f2(T & param); //传引用

f1(array);  //被推导为const char *
f2(array);  //被推到为const char(&)[12]

函数实参
在函数作为实参时,也会被转化为指针推导。

void someFunc(int, double); //someFunc是一个函数,类型是void(int,double)

template<typename T>
void f1(T param);   //传值

template<typename T>
void f2(T & param); //传引用

f1(someFunc); //param被推导为指向函数的指针,类型是void(*)(int, double)
f2(someFunc); //param被推导为指向函数的引用,类型为void(&)(int, bouel)

小结

  • 在模板类型推导时,有引用的实参会被视为无引用,他们的引用会被忽略
  • 对于通用引用的推导,左值实参会被特殊对待
  • 对于传值类型推导,实参如果具有常量性和易变性会被忽略
  • 在模板类型推导时,数组或者函数实参会退化为指针,除非它们被用于初始化引用

2. 理解auto类型推导


在大部分情况下auto推导与模板类型推导一致,仅当变量使用花括号初始化时,auto能够推导成std::initializer_list,而模板类型推导则无法推导。

auto x1=27;   //类型是int,值是27
auto x2(27);  //同上
auto x3={27}; //类型是std::initializer_list<int>,值是{27}
auto x4{27};  //同上
auto x={11,23,9}; //x的类型是std::initializer_list<int>
auto x5={1,2,3.0}; //错误!存在不同类型,auto类型推导不能工作

小结

  • 当用auto声明的变量使用花括号进行初始化,auto推导的类型为std::initializer_list。这一点是模板类型无法做到的。

3. 理解decltype


decltype是一种类型推导工具,用于获取表达式的类型而不执行该表达式。

通常被用于推导变量的类型和表达式的类型。

int a = 1;
const int& x = a;
decltype(a) b = 2; // 推导出变量a的类型为int,b的类型也为int
decltype(x) c = b; // 推导出变量x的类型为const int&,c的类型也为const int&

int a = 1, b = 2;
decltype(a+b) c = 3; // 推导出表达式a+b的类型为int,c的类型也为int

小结

  • 如果表达式是一个变量名,则decltype推导出来的类型就是该变量的类型,而不是该变量的值的类型。
  • auto不同的是: auto在推导时会丢弃const和引用,decltype则可以保留类型的const和引用限定符,即推导出的类型与表达式的类型一致。

4. 学会查看类型推导结果


《Effective Modren C++》提供了三种查看类型推导的方式:

  • 编辑时,通过IDE编辑器
    一些IDE编辑器支持显示程序代码中变量,函数,参数的类型。
  • 编译时,通过编译器诊断信息
    通过编译器出错时提供的错误消息也可以查看推导结果。
  • 运行时,通过C++提供的接口typeid或者Boost.TypeIndex

但是编译器的打印的类型并不是完全可靠的!

5. 优先考虑auto而非显式类型声明


auto声明变量必须初始化,否则报错。(解决局部变量未初始化)

② 比起std::function, auto更省空间且快捷方便保存一个闭包的lambda表达式。

③ 对于STL容器遍历中,auto会避免异常隐蔽的错误。如《Effective Modren C++》举的例子:

std::unordered_map<std::string,int> m;
...
for(const std::pair<std::string,int>& p : m)
{
...
}

std::unordered_map的key是一个常量,所以std::pair的类型不是std::pair<std::string,int>而是 std::pair<const std::string,int>。为了对齐类型,编译器会创建一个临时对象,这个临时对象的类型是p想绑定到的对象的类型,即m中元素的类型,然后把p的引用绑定到这个临时对象上。在每个循环迭代结束时,临时对象将会销毁。如此一来,便会发生难以排查得bug。

使用auto可以避免这些很难被意识到的类型不匹配的错误:

for(const auto & p : m)
{
...
}

小结
auto在使用时确实方便,但其也会降低代码可读性。因此在使用时可参考如下场景使用

  • 复杂类型名称较长: 当变量的类型名称非常冗长或复杂时,使用auto可以简化代码并提高可读性。例如,当类型名称包含模板或嵌套类型时,使用auto可以减少输入错误。
  • 类型明确可推导: 当变量的初始化表达式明确地指示了变量的类型时,使用auto可以简化代码,并减少重复输入类型名称的工作。这对于使用迭代器、范围基于循环或返回自动类型推导函数的函数等情况特别有用。
  • 类型变化频繁: 当代码中的类型可能经常改变时,使用auto可以使代码更加灵活和易于维护。如果变量的初始化表达式更改了类型,使用auto可以避免手动更改变量声明。

6. auto推导若非己愿,使用显式类型初始化惯用法


auto在推导时,可能返回的是引用类型,可能导致引用的对象被修改。因此在使用时,需要格外注意,可以通过显式初始化来规避此类问题。

#include <iostream>
#include <vector>
using namespace std;
 
int main()
{
    std::vector<bool> array {false, false, false,false};

    bool value1 = array[1];  // value1 为bool
    auto value2 = array[2];  // value2 为std::vector<bool>::reference
    auto value3 = static_cast<bool>(array[3]); // value 为bool
	
    value1 = true;
    value2 = true;
    value3 = true;
	
    for (auto i: array) {
        std::cout << " " << i;
    }
    
    return 0;
}

输出

 0 0 1 0

上述代码来看,修改value2的值时会直接修改到array[2],原因是value2auto推导的类型是std::vector<bool>::reference,即引用类型。而value3同样用auto,加上类型转换就无此问题(只是这样还不如直接用bool声明变量)。

7. 区别使用 () 和 {} 创建对象


C++初始化方式
C++的语法中,初始化的方式主要有三种方式:

int x(0);      // 使用()初始化
int y = 0;     // 使用=初始化
int z{0};      // 使用{}初始化

另外也常用到一种,=和{}配合的初始化

int z = {0};   // 使用=和{}

需要注意的是=在初始化时,并不是作为赋值运算符的,举一个自定义类的例子来说明:

Widget w1;       //调用默认构造函数
Widget w2 = w1;  //不是赋值运算符,调用拷贝构造函数Widget(const &Widget),未定义时自动生成
w1 = w2;         //是一个赋值运算符,调用operator=函数
  • 括号初始化也可以用于为非静态数据成员指定默认初始值。C++11允许"="初始化也拥有这种能力:
class widget {
...
private:
    int x = 0;   // 正确
    int y{0};    // 正确
    int z(0);    // 错误
};
  • 不可拷贝的对象,初始化时不可使用=赋值,但可以使用{}、()
std::vector<int> ai1{0};  // 没问题,调用构造函数
std::atomic<int> ai2(0);  // 没问题,调用构造函数
std::atomic<int> ai3 = 0; // 错误!调用的拷贝函数

从上述看,在C++中这三种方式都被指派为初始化表达式,但是只有花括号任何地方都能被使用。因此花括号初始化又叫统一初始化。

{}不允许变窄转换,()和=无此禁忌
在使用{}初始化时,不允许内置类型隐式的变窄转换(narrowing conversion),()=不检查变窄转换。

double x,y,z;
int sum1{x + y + z};  //错误!三个double的和不能用来初始化int类型的变量
int sum2(x + y + z);  // 没问题
int sum3 = x + y + z; // 没问题

{}能避免C++ 最令人头疼的解析问题(most vexing parse)
C++规定任何能被决议为一个声明的表达式必须被决议为声明,因此在使用()初始化变量时,一些情况会被编译器识别为函数声明。

作为对比,使用有参数的构造函数。

Widget w1(10); // 没问题,使用实参10调用Widget的一个构造函数

需要初始化一个无参数的构造函数对象时,会变成函数声明。

Widget w1(); // 有问题,会被识别为函数声明,期望是用无参构造函数构造对象

解决方法,可使用{}初始化,就无此问题。

Widget w1{}; // 正确,调用无参构造函数构造对象

{}使用时的缺点

  • 上述描述了{}的种种优点,但其也存在一些缺点。原因在于第2节中描述,auto声明变量使用{}初始化时,会被推导为std::initializer_list

  • 另外,在构造函数有参数情况中,若不包含std::initializer_list参数或者 构造未传入实参(){}产生一样的效果,否则{}优先匹配std::initializer_list参数的构造函数。

class Widget {
public:
    Widget(int i, bool b); // 同上
    Widget(int i, double d); // 同上
    Widget(std::initializer_list<long double> il); 
…
};

Widget w1(10, true);  // 使用小括号初始化
                      // 调用第一个构造函数

Widget w2{10, true};  // 使用花括号初始化
                      // 调用第三个构造函数
                      // (10 和 true 转化为long double)
                      
Widget w3(10, 5.0);   // 使用小括号初始化
                      // 调用第二个构造函数
                      
Widget w4{10, 5.0};   // 使用花括号初始化
                      // 调用第三个构造函数
                      // (10 和 5.0 转化为long double)
  • 除此之外,在使用{}初始化时,参数能够被转换initializer_list,拷贝构造函数和移动构造函数都会被std::initializer_list构造函数优先匹配。
class Widget {
    public:
    Widget(int i, bool b);
    Widget(int i, double d);
    Widget(std::initializer_list<long double> il);
    operator float() const; // convert to float 
};

Widget w5(w4); // 使用小括号,调用拷贝构造函数
Widget w6{w4}; // 使用花括号,调用std::initializer_list构造函数
Widget w7(std::move(w4)); // 使用小括号,调用移动构造函数
Widget w8{std::move(w4)}; // 使用花括号,调用std::initializer_list构造函数
  • 接着上述,在使用{}初始化时,只要参数能强转换为initializer_list<T>的T类型,就会只匹配std::initializer_list构造函数。因此,遇到变窄转换会编译报错。
class Widget {
public:
    Widget(int i, bool b);
    Widget(int i, double d);
    Widget(std::initializer_list<bool> il); // element type is now bool
    … 
};

Widget w{10, 5.0}; // 编译器匹配initializer_list构造。编译错误!要求变窄转换

只有当传入的参数在编译器上无法转换成std::initializer_list<T>中的T类型,才会匹配普通的构造函数。

class Widget {
public:
    Widget(int i, bool b);
    Widget(int i, double d);
    Widget(std::initializer_list<std::string> il);
…
};
  
Widget w1(10, true); // 使用小括号初始化,调用第一个构造函数
Widget w2{10, true}; // 使用花括号初始化,调用第一个构造函数
Widget w3(10, 5.0);  // 使用小括号初始化,调用第二个构造函数
Widget w4{10, 5.0};  // 使用花括号初始化,调用第二个构造函数
  • 最后在使用空参数{}初始化时,会匹配默认构造函数,只有传入{}才会匹配initializer_list构造函数。
class Widget {
    public:
    Widget();
    Widget(std::initializer_list<int> il);
    ...
};

Widget w1;   // 调用默认构造函数
Widget w2{}; // 同上
Widget w3(); // 最令人头疼的解析!声明一个函数
Widget w3({}); // 匹配initializer_list构造函数
Widget w4{{}}; // 同上

小结
{}初始化看上去内容很庞大,综合上述内容,主要注意以下几点:

  • {}初始化能够在编译阶段杜绝变窄转换,另外也能避免C++最令人头疼的解析。
  • 在构造重载匹配中,只要参数能够强转std::initializer_list<T>T,就会匹配std::initializer_list构造函数,即便有更加匹配的构造函数。
  • 对于数值类型的std::vector来说使用花括号初始化和小括号初始化会造成巨大的不同。
  • 当使用{}初始化无参数时,会优先匹配默认构造函数,如果要匹配std::initializer_list构造函数,需要传入{}

8. 优先考虑nullptr而非0和NULL


选择优先使用nullptr有如下原因:

  • 类型安全。0是整型,NULL类型不确定。两者未明确被指名是指针类型,在使用时可能会带来类型转换等问题。而nullptr为明确的空指针类型。
  • 避免重载解析歧义。传统的 0NULL在函数重载中会引起歧义。而 nullptr的类型是 std::nullptr_t,与整数类型有差异,可以显式地指定指针的空值,避免重载解析歧义。
  • nullptr看起来更舒服_

9. 优先考虑别名声明而非typedef


优先选择使用别名(alias),主要原因在于别名可以被模版化,而typedef不行。

// 别名实现模板
template<typename T>
using MyAllocList = std::list<T,MyAlloc<T>>;
MyAllocList<Widget> lw;
  
// typedef 实现模版 
template<typename T>
    struct MyAllocList {
    typedef std::list<T, MyAlloc<T>> type;
};
MyAllocList<Widget>::type lw;

10. 优先考虑限域枚举而非未限域枚举


首先了解未限域枚举和限域枚举:

/// 未限域枚举 black, white, red 和 Color在相同作用域
enum Color 
{ 
    black, 
    white, 
    red 
};

// 限域枚举 black, white, red 限制在Color域内
enum class Color 
{ 
    black, 
    white,  
    red 
}; 

两者差异在于: 未限域枚举的枚举常量 (black、white) 与枚举类型(Color)在同一作用域;限域枚举的枚举常量(black、white)在枚举类型的作用域下。

限域枚举优点:
① 枚举名不会污染命名空间,即变量名与枚举名一致不会报错(限域枚举使用为Color::black,不会影响声明black变量)。当然遵循命名规范未限域枚举命名可以避免此问题。
② 限域枚举的枚举名是强类型,未限域枚举中的枚举名会隐式转换为整型(现在,也可以转换为浮点类型)

11. 优先考虑使用deleted函数而非使用未定义的私有声明


在阻止类的某些特定成员函数被外部调用时,有两种常见的方法:使用 private访问修饰符将其声明为私有,或者使用 delete关键字将其声明为已删除。一般情况,优先考虑delete,原因如下:

  • delete明确表示该成员函数被删除或禁止使用。
      C++11中实现一个空类,编译器会自动声明六个函数:默认构造函数、析构函数、拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符。
      由于编译器会自动生成上述函数,导致即使不定义,第三方仍然可以调用编译器自动生成的这些函数,这不是期望的动作!若使用private声明这些函数,还要实现其函数定义; 而delete只需要声明即可。

  • delete明确不可传入某些类型参数
      例如参数为int类型,但实际传入bool参数也会强转调用,可以通过delete阻止。

bool isLucky(int number);    // 原始版本
bool isLucky(char) = delete; // 拒绝char
bool isLucky(bool) = delete; // 拒绝bool

if (isLucky('a')) …  // 错误! 调用deleted函数
if (isLucky(true)) … // 错误!

小结

  • delete可以指定,当传入的类型不对时,编译报错。从而在编译期规避类型隐式转换带来的问题。

12. 使用override声明重写函数


C++中子类可以重写基类的虚函数,但两者必须完全相同,才会被编译器认定为是重写的函数; 否则会被认定为子类自身的函数成员,且编译器不会提醒。override可以解决此问题。

class Base {
public:
    virtual int quiet()
    { }
};

class Derived : public Base {
public:
     // 重写父类接口quiet
     int quite() {}            // a.不符预期, 编译器不报错
     int quite() override { }  // b.不符预期, 编译器报错
};

如上,预期设计是子类重写基类的quiet接口,但实际上子类接口拼写错误。a在编译时不会提示错误,b在加上override后,明确声明此为重写接口,编译器在查询基类,编译报错无此接口。

小结

  • override可以明确此函数是重写的基类虚函数接口,当基类不存在此接口时就会编译报错。可以规避在声明子类接口时没有和基类保持一致,又难以察觉,导致子类接口在运行中没有被调用到这种低级问题。

13. 优先考虑const_iterator而非iterator


STL const_iterator等价于指向常量的指针。它们都指向不能被修改的值。标准实践是能加上const就加上,这也指示我们对待const_iterator应该如出一辙。

14. 如果函数不抛出异常请使用noexcept


noexcept是一个函数修饰符,用于指示函数不会抛出异常。使用noexcept修饰的函数被称为不抛异常的函数。

使用noexcept有以下几个原因:

  • 性能优化:当一个函数被标记为noexcept时,编译器可以进行一些优化,因为它知道函数不会抛出异常。这样可以提高程序的性能。
  • 接口约束:noexcept可以作为函数的接口约束,告诉调用者函数不会抛出异常。这样可以帮助调用者更好地处理异常情况,或者在编译时进行静态检查。
  • 异常安全性:在C++中,异常安全性是一个重要的概念,指的是程序在遇到异常时能够正确地处理资源的释放和状态的恢复。如果一个函数被标记为noexcept,那么调用该函数的代码就可以放心地假设函数不会抛出异常,从而更容易实现异常安全性。

使用noexcept修饰的函数必须确保不会抛出任何异常,否则程序将会终止。因此,在使用noexcept修饰函数时,需要仔细考虑函数的实现,确保不会出现意外的异常抛出。

15. 尽可能的使用constexpr


constexpr是用于声明常量表达式的关键字。常量表达式是在编译时求值的表达式,可用于变量函数和构造函数。

constexpr int y = 10;
int arr[y]; // 合法:y是一个编译时常量

比起const,推荐使用constexpr的理由如下:

  • 编译时计算。使用constexpr声明的常量可以在编译时计算其值,而不需要在运行时计算。这意味着编译器可以优化代码,在编译阶段直接替换常量的值,从而减少运行时的计算开销。
  • 常量表达式。constexpr常量可以在编译时被用作常量表达式,例如作为数组大小、模板参数或其他需要常量表达式的上下文中使用。这样可以提高代码的灵活性和可读性。
  • 编译时错误检查。使用constexpr可以在编译时对常量表达式进行类型检查和错误检查。如果在常量表达式中使用了不允许的操作或无效的值,编译器会在编译时发出错误或警告,帮助我们及早发现并修复问题。

16. 让const成员函数线程安全


const成员函数意味着只读,因此这种函数在使用时会被默认为线程安全。但在实际编码中,实现的const成员函数可能存在线程不安全的情况。

class Polynomial {
public:
    using RootsType = std::vector<double>;
    RootsType roots() const
    {
        if (!rootsAreVaild) { // 如果缓存不可⽤
            rootsAreVaild = true; // ⽤`rootVals`存储它们
        } 
        return rootVals;
    }

private:
    mutable bool rootsAreVaild{ false }; // initializers 的更多信息
    mutable RootsType rootVals{}; // 请查看条款7
};

上述代码会修改成员变量rootsAreVaild,假如多线程使用,会存在同时修改此成员,导致线程不安全。因此roots()接口虽然是const,但其依然线程不安全,规避的方法,可以用互斥量或者原子变量。

总结

  • 假如函数被声明为const,就应该被设计为线程安全的接口。其内部实现尽量不要有修改共享资源的操作(即尽量不要有修改公共变量的操作,否则用锁保护),且内部尽量少的调用其他的函数,因为被调用的函数也可能存在线程不安全的风险。

17. 理解特殊成员函数的生成


在C++术语中,特殊成员函数是指自己生成的函数。C++98有四个:默认构造函数、析构函数、拷贝构造函数和拷贝赋值函数。C++11又增加
两个特殊函数:移动构造函数和移动赋值函数。

class Widget {
public:
...
  Widget();    // 默认构造函数
  ~Widget();   // 析构函数
  Widget(const Widget&);       // 拷贝函数
  Widget& operator=(Widget&);  // 拷贝赋值函数
  Widget(Widget&&);            // 移动构造函数 
  Widget& operator=(Widget&&); // 移动赋值函数
...
};

先了解一下C++11默认生成的成员函数,会有什么默认操作:

  • 默认构造函数(Default Constructor)
    如果类没有任何构造函数,则编译器会自动生成默认构造函数。默认构造函数不执行任何操作,仅初始化成员变量。如果成员变量是内置类型,则执行默认初始化;如果成员变量是类类型,则调用相应的默认构造函数进行初始化。
  • 析构函数(Destructor)
    自动生成的析构函数主要负责删除由对象所拥有的资源。对于类内部申请的资源,如动态分配的内存或打开的文件句柄等,编译器会在析构函数中自动释放这些资源。如果类没有显式声明析构函数,则会生成默认的析构函数,执行成员的析构操作。
  • 拷贝构造函数(Copy Constructor)
    自动生成的拷贝构造函数执行的是浅拷贝,即逐个成员变量的进行拷贝。如果类中存在指针成员变量,则拷贝后的对象和原对象将共享相同的内存区域,这可能引发潜在的问题,需要注意。
  • 拷贝赋值操作符(Copy Assignment Operator)
    自动生成的拷贝赋值操作符执行的是浅拷贝,即逐个成员变量的进行拷贝。与拷贝构造函数类似,可能存在共享资源的问题。
  • 移动构造函数(Move Constructor)和移动赋值操作符(Move Assignment Operator)
    C++11引入了移动语义,使得在某些情况下可以使用移动操作来取代拷贝操作,提高效率。生成的移动构造函数和移动赋值操作符会对成员进行从一个对象到另一个对象的转移,而不是简单的进行值拷贝。

Rule of Three规则规定:如果类中声明了拷⻉构造函数,拷⻉赋值运算符,或者析构函数三者之⼀,就应该也声明其余两个。它来源于⻓期的观察,即⽤⼾接管拷⻉操作的需求⼏乎都是因为该类会做其他资源的管理,这也⼏乎意味着1)⽆论哪种资源管理如果能在⼀个拷⻉操作内完成,也应该在另⼀个拷⻉操作内完成2)类析构函数也需要参与资源的管理(通常是释放)

总结

  • 具体的原因可参考原文第17项,好的编程习惯应该显示的明确六个特殊成员的存在方式。需要使用默认的实现,则用default声明;不希望某个成员函数被调用,则使用delete声明;需要自定义实现,则自定义实现接口。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Vue3】组件通信以及各种方式的对比

方式一&#xff1a;props 「父」向「子」组件发送数据 父组件&#xff1a; 定义需要传递给子组件的数据&#xff0c;并使用 v-bind 指令将其绑定到子组件的 props 上。 <template><child-component :message"parentMessage" /> </template><sc…

6. ping在windows中的常见用法

&#xff08;1&#xff09;ping简介 1.ping简介 &#xff08;2&#xff09;在windows上用法 1.直接ping 对方IP&#xff08;无参数时&#xff09; 2.ping -t IP (长ping) 3.ping -n 包数量 4.ping -l 字节大小 IP 5.如何批量的ping一个网段&#xff1f; &#xff08;1&a…

24计算机考研调剂 | 【官方】山东工商学院

山东工商学院 考研调剂招生信息 招生专业&#xff1a; 学院概况&#xff1a; 计算机科学与技术学院始建于1999年&#xff0c;拥有计算机科学与技术一级学科硕士点,在2022软科中国最好学科排名中&#xff0c;计算机科学与技术学科位列全国第104位。在2022年“软科”中国大学专…

【MySQL】2.MySQL数据库的基本操作

目录 数据库基本操作 查看数据库信息 查看数据库结构 显示数据表的结构&#xff08;字段&#xff09; 常用的数据类型 数据库管理操作 SQL语句概述 SQL分类 1.DDL&#xff1a;数据定义语言 1.1创建数据库和表 创建数据库 创建数据表 1.2删除数据库和表 删除数据表…

音视频领域首个,阿里云推出华为鸿蒙 HarmonyOS NEXT 版音视频 SDK

近日&#xff0c;阿里云在官网音视频终端 SDK 栏目发布适配 HarmonyOS NEXT 的操作文档和 SDK&#xff0c;官宣 MediaBox 音视频终端 SDK 全面适配 HarmonyOS NEXT。 此外&#xff0c;阿里云播放器 SDK 也在华为开发者联盟官网鸿蒙生态伙伴 SDK 专区同步上线&#xff0c;面向所…

SpringCloud-记

目录 什么是SpringCloud 什么是微服务 SpringCloud的优缺点 SpringBoot和SpringCloud的区别 RPC 的实现原理 RPC是什么 eureka的自我保护机制 Ribbon feigin优点 Ribbon和Feign的区别 什么是SpringCloud Spring Cloud是一系列框架的有序集合。它利用Spring Boot的开发…

STM32之HAL开发——系统定时器(SysTick)

系统定时器&#xff08;SysTick&#xff09;介绍 SysTick—系统定时器是属于 CM3 内核中的一个外设&#xff0c;内嵌在 NVIC 中。系统定时器是一个 24bit的向下递减的计数器&#xff0c;计数器每计数一次的时间为 1/SYSCLK&#xff0c;一般我们设置系统时钟 SYSCLK等于 72M。当…

人工智能之Tensorflow变量作用域

在TensoFlow中有两个作用域&#xff08;Scope&#xff09;&#xff0c;一个时name_scope ,另一个是variable_scope。variable_scope主要给variable_name加前缀&#xff0c;也可以给op_name加前缀&#xff1b;name_scope给op_name加前缀。 variable_scope 通过所给的名字创建或…

【电路笔记】-场效应管(FET)电流源

场效应管(FET)电流源 文章目录 场效应管(FET)电流源1、概述2、偏置结 FET2.1 N沟道JFET偏置2.2 N沟道JFET输出特性3、JFET 作为恒流源4、JFET 零电压偏置5、JFET 负电压偏置6、FET 恒流源示例17、JFET电流源8、FET 恒流源示例29、FET 恒流源示例310、总结FET 恒流源使用 JFET 和…

Java学习笔记NO.26

T3.以面向对象的思想&#xff0c;编写自定义类描述IT从业者。 设定属性包括&#xff1a;姓名&#xff0c;年龄&#xff0c;技术方向&#xff0c;工作年限&#xff1b; 方法包括&#xff1a;工作。 要求&#xff1a; (1)设置属性的私有访问权限&#xff0c;通过公有的get,set…

业务服务:redisson

文章目录 前言一、配置1. 添加依赖2. 配置文件/类3. 注入redission3. 封装工具类 二、应用1. RedisUtils工具类的基本使用 三、队列1. 工具类2. 普通队列3. 有界队列&#xff08;限制数据量&#xff09;4. 延迟队列&#xff08;延迟获取数据&#xff09;5. 优先队列&#xff08…

Netty - 五种 I/O 多路复用机制 select、poll、epoll、kqueue、iocp(windows) 对比

文章目录 Preselect、poll、epoll、kqueue、iocp(windows) Pre 高性能网络编程 - select、 poll 、epoll 、libevent select、poll、epoll、kqueue、iocp(windows) 这里我将对比一下常见的多路复用技术&#xff1a;select、poll、epoll、kqueue 和 IOCP&#xff08;Windows&a…

分区表索引失效导致业务异常

业务无法正常进行&#xff0c;查看数据库后台进程&#xff0c;发现有大量阻塞 QL_ID WAIT_CLASS EVENT ------------- --------------- ------------------------- 1cpk7srb6cr0r User I/O db file scattered read 279knu21n06x6…

音视频开发之旅(78)- Docker使用和交互流程

目录 1.Docker是什么 2.DockerFile的使用 3.常用命令 4.Docker和Web服务的交互流程 5.资料 一、Docker是什么 Docker通过轻量级的容器化技术&#xff0c;使得应用程序及其依赖可以打包在一个可移植的容器中运行&#xff0c;确保应用在不同环境下的一致性和效率。 1.1 核心…

中断(NVIC)的使用--EXTI--TIM

目录 中断是什么 轮询 中断 中断调用情况 中断的分类 内部中断&#xff08;TIM、UART等&#xff09; tim.c tim.h 外部中断EXTI exti.c exti.h 中断是什么 在处理事件的时候有两种方式&#xff1a;轮询和中断。 轮询 顾名思义&#xff0c;就是每轮都询问一次。比如…

结构体类型详细讲解(附带枚举,联合)

前言&#xff1a; 如果你还对结构体不是很了解&#xff0c;那么本篇文章将会从 为什么存在结构体&#xff0c;结构体的优点&#xff0c;结构体的定义&#xff0c;结构体的使用与结构体的大小依次介绍&#xff0c;同样会附带枚举与联合体 目录 为什么存在结构体&#xff1a; 结构…

毕业设计:日志记录编写(3/17起更新中)

目录 3/171.配置阿里云python加速镜像&#xff1a;2. 安装python3.9版本3. 爬虫技术选择4. 数据抓取和整理5. 难点和挑战 3/241.数据库建表信息2.后续进度安排3. 数据处理和分析 3/17 当前周期目标&#xff1a;构建基本的python环境&#xff1a;运行爬虫程序 1.配置阿里云pytho…

【C++】如何用一个哈希表同时封装出unordered_set与unordered_map

&#x1f440;樊梓慕&#xff1a;个人主页 &#x1f3a5;个人专栏&#xff1a;《C语言》《数据结构》《蓝桥杯试题》《LeetCode刷题笔记》《实训项目》《C》《Linux》《算法》 &#x1f31d;每一个不曾起舞的日子&#xff0c;都是对生命的辜负 目录 前言 1.哈希桶源码 2.哈希…

(三维重建学习)已有位姿放入colmap和3D Gaussian Splatting训练

这里写目录标题 一、colmap解算数据放入高斯1. 将稀疏重建的文件放入高斯2. 将稠密重建的文件放入高斯 二、vkitti数据放入高斯 一、colmap解算数据放入高斯 运行Colmap.bat文件之后&#xff0c;进行稀疏重建和稠密重建之后可以得到如下文件结构。 1. 将稀疏重建的文件放入高…

windows10 WSL启动Ubuntu虚拟机,安装DolphinScheduler

文章目录 1. 启动WSL与虚拟机2. 安装Docker与DolphinScheduler容器 1. 启动WSL与虚拟机 使用管理员权限运行命令&#xff1a; Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux重启后即可创建虚拟机 在Microsoft Store中搜索Ubuntu&…
最新文章