"C++ Advanced Programming" reading notes (eight, nine: classes and objects)

1. References

2. It is recommended to read the book "21 Days to Learn C++" to get started. The link of the notes is as follows

1. Write the class

  • When you write a class, you need to specify behaviors or methods (which apply to objects of the class), and you also need to specify properties or data members (which every object will contain). There are two elements to writing a class: defining the class itself and defining the methods of the class

1.1 Class Definition

  • A class definition is a C++ statement and must end with a semicolon

    // SpreadsheetCell.h
    class SpreadsheetCell {
          
          
    public:
        // 类支持的方法
        void setValue(double inValue);
        double getValue() const; // 最好将不改变对象的任何数据成员的成员函数声明为 const
    private:
        double mValue; // 类的数据成员
    };
    
  • class members

    • A class can have many members. Members can be member functions (methods, constructors, or destructors), member variables (also known as data members), member enumerations , type aliases , and nested classes , etc.
    • Member functions and data members cannot have the same name
  • Access control

    • Every method and member in a class can be described with one of three access specifiers: public, protected, or private. An access specifier is applied to all members declared after it until another access specifier is encountered
    • Similar to classes, structures in C++ can also have methods. In fact, the only difference is that the default access specifier for structs is public, while the default for classes is private

insert image description here

  • declaration order

    • Members and access control specifiers may be declared in any order : C++ imposes no restrictions such as member functions before data members, or public before private. Additionally, access specifiers can be reused
  • In-class member initializers

    • Member variables can be initialized directly in the class definition, as follows: In the SpreadsheetCell class definition, directly initialize mValue to 0
    class SpreadsheetCell {
          
          
    public:
        ...
    private:
        double mValue = 0;
    };
    

1.2 Define the method

  • Class definitions must precede method definitions, usually class definitions are in header files, and methods are defined in source files that include header files

    #include "SpreadsheetCell.h"
    
    void SpreadsheetCell::setValue(double inValue) {
          
          
        mValue = inValue;
    }
    double SpreadsheetCell::getValue() const {
          
          
        return mValue;
    }
    
  • access data members

    • Non-static methods of a class are always executed on a specific object of the class, and all data members of the class to which the object belongs can be accessed in the method body of the class
    • If two different objects call setValue(), this line of code (once for each object) will change the value of the variable in the two different objects
    mValue = inValue;
    
  • this pointer

    • Every normal method call is passed a pointer to the object, which is the this pointer called the "hidden" parameter
    • Use this pointer to access data members or call methods, or pass it to other methods or functions
    • It is also sometimes used to disambiguate names . For example, you can use value instead of mValue as the data member of the SpreadsheetCell class, and use value instead of inValue as the parameter of the setValue() method
    void SpreadsheetCell::setValue(double value) {
          
          
        // value = value;
        this->value = value; // 避免名称歧义
    }
    
    • If a method of an object calls a function that takes a pointer to the object as a parameter, the function can be called using the this pointer
    void printCell(const SpreadsheetCell &cell) {
          
          
        cout << cell.getString() << endl;
    }
    
    void SpreadsheetCell::setValue(double value) {
          
          
        this->value = value;
        printCell(*this); // 该指针指向 setValue() 操作的 SpreadsheetCell 对象
        // cout << *this << endl; // 重载了 << 后可使用该行输出,不用编写 printCell 函数
    }
    

1.3 Use object

  • Objects can be created and used in two ways: on the stack or on the heap

  • objects on the stack

    • Creating an object is similar to declaring a simple variable, the difference is that the variable type is a class name
    • The "." operator allows calling the method of the object. If there are public data members in the object, it can also be accessed with the dot operator, but the use of public data members is not recommended
    SpreadsheetCell myCell;
    myCell.setValue(6);
    cout << "cell: " << myCell.getValue() << endl;
    
  • objects in the heap

    • When an object is created in the heap, its members are accessed through the "arrow" operator
    SpreadsheetCell *myCellp = new SpreadsheetCell();
    myCellp->setValue(3.7);
    cout << "cell: " << myCellp->getValue() << endl;
    // cout << "cell: " << (*myCellp).getValue() << endl; // 与上一行等价
    
    // 用 new 为某个对象分配内存,使用完对象后,要用 delete 销毁对象(或使用智能指针自动管理内存)
    delete myCellp;
    myCellp = nullptr;
    

2. Object life cycle

  • The life cycle of an object involves 3 activities: creation, destruction and assignment

2.1 Creating objects

  • Objects are created when they are declared (if on the stack) or when space is allocated explicitly using new, new[], or smart pointers. When an object is created, an embedded object is created at the same time
    • When the myClass object is created in the main() function, an embedded string object is created at the same time. When the object containing it is destroyed, the string is also destroyed
    • It is best to assign initial values ​​to variables when declaring them, and similarly, objects should also be initialized . This is provided by declaring and writing a method called a constructor, in which the initialization of the object can be performed
    #include <string>
    
    class myClass {
          
          
    private:
        std::string mName;
    };
    
    int main() {
          
          
        myClass obj;
        return 0;
    }
    
2.1.1 Writing constructors
  • The constructor has no return type, and the parameters are optional. The constructor without parameters is called the default constructor . It can be a no-argument constructor, or let all parameters use default values. In many cases, a default constructor must be provided , and failure to do so will result in a compiler error
    class SpreadsheetCell {
          
          
    public:
        SpreadsheetCell(double initialValue); // 构造函数
    };
    
    // 必须提供构造函数的实现
    SpreadsheetCell::SpreadsheetCell(double initialValue) {
          
          
        setValue(initialValue);
    }
    
2.1.2 Using constructors
  • Using constructors on the stack

    SpreadsheetCell myCell(5);
    cout << "cell: " << myCell.getValue() << endl;
    
    // 不要显式调用构造函数
    SpreadsheetCell myCell.SpreadsheetCell(5); // 编译错误
    // 后来也不能调用构造函数
    SpreadsheetCell myCell;
    myCell.SpreadsheetCell(5); // 编译错误
    
  • Using constructors on the heap

    • Note: A pointer to a SpreadsheetCell object can be declared in the heap without calling the constructor immediately, but objects in the stack will call the constructor when they are declared
    auto smartCellp = make_unique<SpreadsheetCell>(4);
    // ...do something with the cell, 不需要删除智能指针
    
    // 不推荐下面使用原始指针的方式
    SpreadsheetCell *myCellp = new SpreadsheetCell(5);
    delete myCellp;
    myCellp = nullptr;
    
2.1.3 Provide multiple constructors
  • Multiple constructors can be provided in a class. All constructors have the same name (class name), but different constructors have different numbers of parameters or different parameter types
  • C++ supports delegating constructors, which allow constructor initializers to call other constructors of the same class
    class SpreadsheetCell {
          
          
    public:
        SpreadsheetCell(double initialValue);
        SpreadsheetCell(std::string_view initialValue);
    };
    
2.1.4 Default constructor
  • The default constructor has no parameters, also known as a parameterless constructor. Use the default constructor to initialize data members without specifying a value
  • When is a default constructor needed?
    • Creating an array of objects needs to complete two tasks: allocate memory contiguous space for all objects, and call the default constructor for each object
    • C++ does not provide any syntax for code that creates an array to directly call a different constructor
    • If you want to store classes in standard library containers such as vector, you also need a default constructor
    // 该行没有定义 SpreadsheetCell 类的默认构造函数,无法通过编译
    SpreadsheetCell cells[3];
    // 基于堆栈的数组可使用下列初始化器的方式绕过上面的限制
    SpreadsheetCell cells[3] = {
          
          SpreadsheetCell(0), SpreadsheetCell(23), SpreadsheetCell(41)};
    
  • How to write default constructor?
    class SpreadsheetCell {
          
          
    public:
        SpreadsheetCell(); // 默认构造函数
    };
    
    SpreadsheetCell::SpreadsheetCell() {
          
           // 默认构造函数的实现
        mValue = 0; // 如果使用类内成员初始化方式则可省略该行代码
    }
    
    SpreadsheetCell myCell; // 在堆栈中创建对象时,调用默认构造函数不需要使用圆括号
    myCell.setValue(6);
    cout << "cell: " << myCell.getValue() << endl;
    
    // 对于堆中的对象,可以这样使用默认构造函数
    auto smartCellp = make_unique<SpreadsheetCell>();
    
  • Compiler generated default constructor?
    • If no constructor is specified, the compiler will automatically generate a no-argument constructor
    • The compiler no longer automatically generates a default constructor if either a default constructor or another constructor is declared

default constructor = no-argument constructor

  • The default constructor does not just mean that if no constructor is declared, a constructor will be automatically generated
  • And it also means: if there are no parameters, the constructor uses the default value
  • explicit default constructor
    • If a class requires some explicit constructor that takes parameters and a default constructor that does nothing, you must explicitly write an empty default constructor
    class SpreadsheetCell {
          
          
    public:
       SpreadsheetCell() = default; // 显式默认构造函数
    };
    
  • Explicitly delete the default constructor
    • You can define a class with only static methods. This class does not have any constructors, and you don’t want the compiler to generate a default constructor. In this case, you can explicitly delete the default constructor.
    class MyClass {
          
          
    public:
       MyClass() = delete; // 显式默认构造函数
    };
    
2.1.5 Constructor initializers
  • C++ provides another way to initialize data members in constructors, called constructor initializers
    SpreadsheetCell::SpreadsheetCell(double initialValue) : mValue(initialValue) {
          
          }
    
  • Some programmers like to provide initial values ​​in the constructor body (even if it is not efficient). However, some data types must be initialized in the constructor initializer or using an in-class initializer , which is summarized in the following diagram

insert image description here

  • Order of initialization of data members : in the order in which they appear in the class definition, not in the order of constructor initializers
    class Foo {
          
          
    public:
        Foo(double value);
    private:
        double mValue;
    };
    
    Foo::Foo(double value) : mValue(value) {
          
          
        cout << "Foo::mValue = " << mValue << endl;
    }
    
    // 将 Foo 对象作为自己的一个数据成员
    class MyClass {
          
          
    public:
        MyClass(double value);
    private:
        // 必须保持下列顺序,否则输出结果未知
        double mValue;
        Foo mFoo;
    };
    // 其构造器函数首先在 mValue 中存储给定的值,然后将 mValue 作为实参来调用 Foo 构造函数
    MyClass::MyClass(double value) : mValue(value), mFoo(mValue) {
          
          
        cout << "MyClass::mValue = " << mValue << endl;
    }
    MyClass instance(1.2);
    
    // 输出
    Foo::mValue = 1.2
    MyClass::mValue = 1.2
    
2.1.6 Copy constructor
  • There is a special constructor in C++ called a copy constructor that allows the object created to be an exact copy of another object

    • If no copy constructor is written, C++ automatically generates one, initializing each data member of the new object with the value of the corresponding data member in the source object
    • If the data members are objects, initialization means calling their copy constructor
    • In most cases, the constructor generated by the compiler is sufficient, and there is no need to write the copy constructor yourself
    class SpreadsheetCell {
          
          
    public:
        // 复制构造函数采用源对象的 const 引用作为参数
        SpreadsheetCell(const SpreadsheetCell &src);
    };
    
  • When to call the copy constructor

    • The default way of passing function parameters in C++ is by value, which means that a function or method receives a copy of some value or object. Therefore, whenever an object is passed to a function or method, the compiler will call the copy constructor of the new object to initialize
    • You can avoid the overhead of copy constructors by passing arguments as const references
  • call the copy constructor explicitly

    SpreadsheetCell myCell1(4);
    SpreadsheetCell myCell2(myCell1);
    
  • pass object by reference

    • To avoid copying an object when passing an object to a function or method , have the function or method take a reference to the object as a parameter. Passing objects by reference is usually more efficient than passing objects by value because: only the address of the object needs to be copied, not the entire content of the object (preferably by const reference)
    • primitive types such as int and double should be passed by value , passing these types by const reference gets nothing
2.1.7 Initializer List Constructor
#include <iostream>
#include <initializer_list>
#include <stdexcept>
#include <vector>

using namespace std;

class EvenSequence {
    
    
public:
    // 初始化列表构造函数
    EvenSequence(initializer_list<double> args) {
    
    
        if (args.size() % 2 != 0) {
    
    
            throw invalid_argument("initializer_list should "
                "contain even number of elements.");
        }
        mSequence.reserve(args.size());
        // 将顺序遍历 initializer_list 并将其元素添加到 mSequence 中
        for (const auto &value : args) {
    
    
            mSequence.push_back(value);
        }
    }
    // 用于打印 mSequence 中的所有元素
    void dump() const {
    
    
        for (const auto& value : mSequence) {
    
    
            cout << value << ", ";
        }
        cout << endl;
    }

private:
    vector<double> mSequence;
};

int main() {
    
    
    EvenSequence p1 = {
    
    1.0, 2.0, 3.0, 4.0, 5.0, 6.0}; // 等号可省略
    p1.dump();
    
    try {
    
    
        EvenSequence p2 = {
    
    1.0, 2.0, 3.0};
    } catch (const invalid_argument& e) {
    
    
        cout << e.what() << endl;
    }
    
    return 0;
}
2.1.8 Delegating constructors
  • Delegating constructors allow constructors to call other constructors of the same class . However, this call cannot be placed in the constructor body, but must be placed in the constructor initializer, and must be the only member initializer in the list
SpreadsheetCell::SpreadsheetCell(string_view initialValue) 
               : SpreadsheetCell(stringToDouble(initialValue)) {
    
    }
2.1.9 Summarizing compiler-generated constructors

insert image description here

2.2 Destroy objects

  • When an object is destroyed, two things happen: the object's destructor is called, and the memory occupied by the object is freed . Object cleanup can be performed in the destructor, such as freeing dynamically allocated memory or closing file handles . If no destructor is declared, the compiler will automatically generate one, and the destructor will destroy members one by one, and then delete the object
  • Objects on the stack are destroyed in the reverse order of declaration (construction order) , which also applies if an object is a data member of another object: data member objects are destroyed in the reverse order of their declaration in the class
  • Objects allocated on the heap are not automatically destroyed without the help of smart pointers. The object pointer must be deleted using delete, thus calling the destructor and freeing the memory

2.3 Object assignment

  • "Copying" in C++ only happens when an object is initialized. If an object that already has a value is overwritten, the more precise term is "assignment"
    • The copy tool provided by C++ is the copy constructor : this is a constructor that can only be used when creating an object, not for object assignment
    • C++ provides a method for all classes to perform assignment: the assignment operator , which actually overloads the = operator for the class

      The assignment operator here is also called the copy assignment operator, because both the objects on the left and the right continue to exist. The reason for this distinction is that there is also a move assignment operator, and when the assignment ends, the object on the right will be destroyed

    SpreadsheetCell myCell(5), anotherCell;
    anotherCell = myCell;
    
2.3.1 Declaring assignment operators
  • The assignment operator is similar to the copy constructor, taking a const reference to the source object . In this case, refer to the source object as rhs, which stands for "right side" of the equals sign, with the object calling the assignment operator on the left side of the equals sign
  • Unlike the copy constructor, the assignment operator returns a reference to the SpreadsheetCell object
    class SpreadsheetCell {
          
          
    public:
        SpreadsheetCell& operator= (const SpreadsheetCell &rhs);
    };
    
2.3.2 Defining assignment operators
  • The assignment operator is implemented similarly to the copy constructor, with some important differences. First, the copy constructor is called only at initialization time , when the target object does not have a valid value, and the assignment operator can overwrite the current value of the object
  • Allows assigning the value of an object to itself in C++
    SpreadsheetCell cell(4);
    cell = cell;
    
    • To prevent self-assignment, assignment operators usually detect self-assignment at the beginning of the method, and return immediately if self-assignment is found
    SpreadsheetCell& SpreadsheetCell::operator= (const SpreadsheetCell &rhs) {
          
          
        // this 是一个指向左边对象的指针,&rhs 是一个指向右边对象的指针
        if (this == &rhs) {
          
          
            return *this; // this 指针指向执行方法的对象,因此 *this 就是对象本身
        }
    }
    

2.4 The difference between copy and assignment

  • A copy constructor is used for declarations and an assignment operator is used for assignment statements

    SpreadsheetCell myCell(5);
    SpreadsheetCell anotherCell(myCell); // 复制构造函数
    SpreadsheetCell aThirdCell = myCell; // 也是复制构造函数
    anotherCell = myCell; // 此处,anotherCell 已经构建,因此会调用 operator =
    
  • return object by value

    string SpreadsheetCell::getString() const {
          
          
        return doubleToString(mValue);
    }
    
    // 复制构造函数 & 赋值运算符
    SpreadsheetCell myCell2(5);
    string s1;
    // 当 getString() 返回 mString 时,编译器实际上调用 string 复制构造函数
    // 创建一个未命名的临时字符串对象,将结果赋给 s1 时,会调用 s1 的赋值运算符
    s1 = myCell2.getString(); 
    
    // 复制构造函数
    SpreadsheetCell myCell3(5);
    // 此时 s2 调用的是复制构造函数
    string s2 = myCell3.getString();
    

    With move semantics, the compiler can use the move constructor instead of the copy constructor, which is more efficient

  • Copy constructor and object members

    • If an object contains other objects, the compiler-generated copy constructor recursively calls the copy constructor of each contained object
    • When assigning values ​​to data members in the function body of the copy constructor, the assignment operator is used instead of the copy constructor because they are already initialized

3. Tomomoto

  • C++ allows a class to declare other classes, member functions or non-member functions of other classes as friends, and friends can access the protected and private data members and methods of the class
  • A class, method, or function cannot declare itself as a friend of other classes and access non-public names of those classes
  • Fiend classes and methods can be easily abused: friends can violate the principle of encapsulation and expose the internals of the class to other classes or functions
    // 现在 Bar 类的所有成员可以访问 Foo 类的 private 和 protected 数据成员和方法
    class Foo {
          
          
        friend class Bar;
        // ...
    }
    
    // 也可将 Bar 类的一个特定方法作为友元
    class Foo {
          
          
        friend void Bar::processFoo(const Foo &foo);
        // ...
    }
    
    // 独立函数也可成为类的友元
    class Foo {
          
          
        // 类中的 friend 声明可以用作函数的原型
        friend void dumpFoo(const Foo &foo);
        // ...
    }
    
    void dumpFoo(const Foo &foo) {
          
          
        // 将 Foo 对象的所有数据转存到控制台,希望可以访问 Foo 对象的内部数据成员
    }
    

4. Dynamic memory allocation of objects

4.1 Spreadsheet class

  • Here the Spreadsheet class uses numbers in both directions, here is the first definition of this simple Spreadsheet class
    #include <cstddef>
    #include "SpreadsheetCell.h"
    
    class Spreadsheet {
          
          
    public:
        Spreadsheet(size_t width, size_t height);
        void setCellAt(size_t x, size_t y, const SpreadsheetCell &cell);
        SpreadsheetCell& getCellAt(size_t x, size_t y);
    private:
        bool inRange(size_t value, size_t upper) const;
        size_t mWidth = 0;
        size_t mHeight = 0; 
        SpreadsheetCell* *mCells = nullptr;
    };
    
  • dynamically allocate two-dimensional array
    Spreadsheet::Spreadsheet(size_t width, size_t height) : mWidth(width), mHeight(height) {
          
          
        mCells = new SpreadsheetCell*[mWidth];
        for (size_t i = 0; i < mWidth; ++i) {
          
          
            mCells[i] = new SpreadsheetCell[mHeight];
        }
    }
    
  • The memory allocated by the Spreadsheet object whose stack name is s1 is as follows

insert image description here

  • Set and get methods
    void Spreadsheet::setCellAt(size_t x, size_t y, const SpreadsheetCell &cell) {
          
          
        // 使用 inRange 类检测电子表格中的 x 和 y 坐标是否有效
        if (!inRange(x, mWidth) || !inRange(y, mHeight)) {
          
          
            throw std::out_of_range("");
        }
        mCells[x][y] = cell;
    }
    SpreadsheetCell& Spreadsheet::getCellAt(size_t x, size_t y) {
          
          
        if (!inRange(x, mWidth) || !inRange(y, mHeight)) {
          
          
            throw std::out_of_range("");
        }
        return mCells[x][y];
    }
    
  • Considering the repetition of setCellAt and getCellAt in the above code, define the following methods to avoid repetition
    void verifyCoordinate(size_t x, size_t y) const;
    
    void Spreadsheet::verifyCoordinate(size_t x, size_t y) const {
          
          
        if (x >= mWidth || y >= mHeight) {
          
          
            throw std::out_of_range("");
        }
    }
    
    void Spreadsheet::setCellAt(size_t x, size_t y, const SpreadsheetCell &cell) {
          
          
        // 使用 inRange 类检测电子表格中的 x 和 y 坐标是否有效
        verifyCoordinate(x, y);
        mCells[x][y] = cell;
    }
    
    SpreadsheetCell& Spreadsheet::getCellAt(size_t x, size_t y) {
          
          
        verifyCoordinate(x, y);
        return mCells[x][y];
    }
    

4.2 Freeing memory using destructors

  • If dynamically allocated memory is no longer needed, it must be freed. If memory was dynamically allocated for the object, the memory is freed in the destructor . When an object is destroyed, the compiler ensures that the destructor is called. Below is the Spreadsheet class definition with destructor
    class Spreadsheet {
          
          
    public:
        Spreadsheet(size_t width, size_t height);
        ~Spreadsheet(); // 析构函数的定义
    };
    
    // 析构函数的实现
    Spreadsheet::~Spreadsheet() {
          
          
        for (size_t i = 0; i < mWidth; ++i) {
          
          
            delete[] mCells[i];
        }
        delete[] mCells;
        mCells = nullptr;
    }
    
  • Implicitly mark noexcept for destructors, because they should not throw exceptions, destructors use noexcept implicitly, and there is no need to specifically add this keyword
  • You can write any code in the destructor, but it is better to have the destructor only release memory or clean up other resources

4.3 Handling copying and assignment

  • If you don't write your own copy constructor or assignment operator, C++ will auto-generate: the compiler-generated method recursively calls the copy constructor or assignment operator of the object's data members

    • However, for primitive types such as int, double, and pointers, only superficial (or bitwise) copying or assignment is provided : just copying or assigning data members directly from the source object to the target object, when dynamically allocating memory for the object, doing so will cause problems
    • For example, in the following code, when s1 is passed to the function printSpreadsheet(), the spreadsheet s1 is copied to initialize s
    #include "SpreadsheetCell.h"
    
    void printSpreadsheet(Spreadsheet s) {
          
          
        // ...
    }
    
    int main() {
          
          
        Spreadsheet s1(4, 3);
        printSpreadsheet(s1);
    
        return 0;
    }
    
    • Spreadsheet contains a pointer variable: mCells. The surface copy of the Spreadsheet provides a copy of the mCells pointer to the target object, but does not copy the underlying data. The end result is that both s and s1 have a pointer to the same data , as shown in the figure below
      insert image description here

    • If s modifies what mCells points to, this change will also be reflected in s1. What's worse, when the function printSpreadsheet() exits, the destructor of s will be called to release the memory pointed to by mCells, and now the memory pointed to by the pointer owned by s1 is no longer valid, which is called a dangling pointer
      insert image description here

    • Incredibly, it gets even worse when assignment is used . Assuming the following code is written, after the first line, when two objects are constructed, the layout of the memory is as follows

    Spreadsheet s1(2, 2), s2(4, 3);
    s1 = s2;
    

    insert image description here

    • After the assignment statement is executed, the layout of the memory is as shown in the figure below
      insert image description here

    • Now, not only mCells in s1 and s2 point to the same memory, but also the memory pointed to by s1 earlier is abandoned, this is called memory leak , that's why deep copy is done in assignment operator

    • As can be seen, it is not always correct to rely on C++'s default copy constructor or assignment operator

    After dynamically allocating memory in a class, you should write your own copy constructor and assignment operator to provide deep memory copying

  • Copy constructor of the Spreadsheet class

    • Notice the use of the delegating constructor. The constructor initializer for this copy constructor first delegates to the non-copy constructor to allocate the appropriate amount of memory . The copy constructor body then copies the actual value, making a deep copy of the dynamically allocated two-dimensional array of mCells
    class Spreadsheet {
          
          
    public:
        // 复制构造函数的声明
        Spreadsheet(const Spreadsheet& src) : Spreadsheet(src.mWidth, src.mHeight) {
          
          
            // ...
        }
    };
    
    // 复制构造函数的定义
    Spreadsheet::Spreadsheet(const Spreadsheet &src) : Spreadsheet(src.mWidth, src.mHeight) {
          
          
        for (size_t i = 0; i < mWidth; ++i) {
          
          
            for (size_t j = 0; j < mHeight; ++j) {
          
          
                mCells[i][j] = src.mCells[i][j];
            }
        }
    }
    
  • Assignment Operators for the Spreadsheet Class

    • Safe handling of exceptions through three phases
      • The first stage creates a temporary copy. This does not modify the state of the current Spreadsheet object, so if an exception occurs at this stage, there will be no problem
      • The second phase uses the swap() function to swap the created temporary copy with the current object. swap() never throws an exception
      • The third phase destroys the temporary object (which now contains the original object due to the swap) to clean up any memory
    class Spreadsheet {
          
          
    public:
        // 复制构造函数的声明
        Spreadsheet& operator= (const Spreadsheet &rhs);
        friend void swap(Spreadsheet &first, Spreadsheet &second) noexcept;
    };
    
    void swap(Spreadsheet &first, Spreadsheet &second) noexcept {
          
          
        // 使用 std::swap() 工具函数交换每个数据成员
        using std::swap;
        swap(first.mWidth, second.mWidth);
        swap(first.mHeight, second.mHeight);
        swap(first.mCells, second.mCells);
    }
    
    Spreadsheet& Spreadsheet::operator= (const Spreadsheet &rhs) {
          
          
        // 检查自赋值
        if (this == &rhs) {
          
          
            return *this;
        }
        // 对右边进行复制,称为 temp,然后用这个副本替代 *this
        Spreadsheet temp(rhs);
        swap(*this, temp);
        return *this;
    }
    
  • Disallow assignment and pass-by-value

    • Copy constructors and assignment operators can be marked private without providing any implementation, thus disabling copying and assignment

4.4 Handling movement with move semantics

4.4.1 Rvalue references
  • In C++, an lvalue is a quantity whose address can be taken, such as a named variable. Because it often appears on the left side of an assignment statement, it is called an lvalue. In addition, all quantities that are not lvalues ​​are rvalues, such as literals, temporary objects, or temporary values , usually rvalues ​​​​are on the right side of the assignment operator
    // 4 * 2 的结果为右值,是一个临时值,将在语句执行完毕时销毁
    int a = 4 * 2;
    
  • An rvalue reference is a reference to an rvalue, a concept that only applies when the rvalue is a temporary object . Purpose of rvalue references: To provide specific functions that can be selected when temporary objects are involved . With rvalue references, certain operations that involve copying large numbers of values ​​can be performed by simply copying pointers to those values
    • Functions can use && as part of a parameter specification to specify an rvalue reference parameter. Normally, a temporary object is treated as a const type&, but when a function overload uses an rvalue reference, the temporary object can be resolved for use in the function overload. The following example illustrates this. The code first defines two handleMessage() functions, one that receives lvalue references and the other that receives rvalue references
    void handleMessage(std::string& message) {
          
          
        cout << "handleMessage with lvalue reference: " << message << endl;
    }
    
    void handleMessage(std::string&& message) {
          
          
        cout << "handleMessage with rvalue reference: " << message << endl;
    }
    
    std::string a = "Hello ";
    std::String b = "World";
    handleMessage(a); // 调用左值引用
    handleMessage(a + b); // a + b 的结果是临时的,故调用右值引用
    handleMessage("Hello World"); // 字面量不是左值,故调用右值引用
    
  • An rvalue reference parameter (string && message) is never bound to an lvalue (b). But you can use std:move() to convert an lvalue to an rvalue , forcing the compiler to call the rvalue reference version of the handleMessage() function
    handleMessage(std::move(b));
    
  • Named variables are lvalues, and in the handleMessage() function, the rvalue reference parameter message itself is an lvalue because it has a name . If you want to pass this lvalue reference parameter to another function as an rvalue, you need to use std::move() to convert the lvalue to an rvalue
    • For example, suppose you want to add the following function, using an rvalue reference parameter
    void helper(std::string&& message) {
          
          }
    
    void handleMessage(std::string&& message) {
          
          
        // helper(message); // message 具有名称为左值,导致编译错误
        helper(std::move(message));
    }
    
4.4.2 Implementing move semantics
  • Move semantics are implemented with rvalue references. To add move semantics to a class, a move constructor and a move assignment operator need to be implemented . Move constructors and move assignment operators should be marked with the noexcept qualifier, which tells the compiler that they will not throw any exceptions
    class Spreadsheet {
          
          
    public:
        Spreadsheet(Spreadsheet&& src) noexcept; // 移动构造函数声明
        Spreadsheet& operator= (Spreadsheet&& rhs) noexcept; // 移动赋值声明
    private:
        void cleanup() noexcept;
        void moveFrom(Spreadsheet& src) noexcept;
    };
    
    // 在析构函数和移动赋值运算符中调用
    void Spreadsheet::cleanup() noexcept {
          
          
        for (size_t i = 0; i < mWidth; ++i) {
          
          
            delete[] mCells[i];
        }
        delete[] mCells;
        mCells = nullptr;
        mWidth = mHeight = 0;
    }
    // 把成员变量从源对象移动到目标对象,接着重置源对象
    void Spreadsheet::moveFrom(Spreadsheet& src) noexcept {
          
          
        // 假设 Spreadsheet 类有一个名为 mName 的 std::string 数据成员
        // 移动对象数据成员
        mName = std::move(src.mName);
    
        // 浅复制对象
        mWidth = src.mWidth;
        mHeight = src.mHeight;
        mCells = src.mCells;
        
        // Reset the source object, because ownership has been moved!
        src.mWidth = 0;
        src.mHeight = 0;
        src.mCells = nullptr;
    }
    
    // 移动构造函数定义
    Spreadsheet::Spreadsheet(Spreadsheet&& src) noexcept {
          
          
        cout << "Move constructor" << endl;
        moveFrom(src);
    }
    
    // 移动赋值运算符定义
    Spreadsheet& Spreadsheet::operator=(Spreadsheet&& rhs) noexcept {
          
          
        cout << "Move assignment operator" << endl;
        
        if (this == &rhs) {
          
          
            return *this;
        }
    
        cleanup();
        moveFrom(rhs);
        
        return *this;
    }
    

    If memory is dynamically allocated in a class, you should generally implement: destructor, copy constructor, move constructor, copy assignment operator, and move assignment operator, this is called the "rule of 5"

4.4.3 Implementing swap functions using move semantics
  • Consider the swap() function, which swaps two objects, as an example of using move semantics to improve performance. The swapCopy() implementation below does not make field move semantics
    void swapCopy(T& a, T& b) {
          
          
        T temp(a);
        a = b;
        b = temp;
    }
    
  • The above implementation first copies a to temp, then b to a, and finally temp to b. If copying of class T is expensive, this swap implementation will severely impact performance. With move semantics, the swap() function avoids all copying
void swapMove(T& a, T& b) {
    
    
    T temp(std::move(a));
    a = std::move(b);
    b = std::move(temp);
}

4.5 Zero Rule

  • The "rule of zero" states: When designing a class, it should be such that it does not require the above five special member functions. You should avoid having any old-style, dynamically allocated memory. Instead, use modern constructs such as standard library containers . For example, in the Spreadsheet class, replace the SpreadsheetCell** data member with vector<vector<SpreadsheetCelI>>. The vector handles memory automatically, so there is no need for the above 5 special member functions

5. More about methods

5.1 Static methods

  • Like data members, methods sometimes apply to entire class objects rather than individual objects, in which case static methods can be written like static data members. Take the SpreadsheetCell class as an example, this class has two helper methods stringToDouble() and doubleToString(). These two methods have no access to specific object information and thus can be static
    class SpreadsheetCell {
          
          
    private:
        static std::string doubleToString(double inValue);
        static double stringToDouble(std::string_view inString);
    };
    
  • There is no need to repeat the static keyword before the method definition. Static methods do not belong to a specific object, so there is no this pointer . When a static method is called with a specific object, the static method will not access the non-static data members of this object
  • Static methods are just like ordinary functions, the only difference is that static methods can access private and protected static data members of the class . If other objects of the same type are visible to the static method (such as passing a pointer or reference to the object), then the static method can also access the private and protected non-static data members of other objects

5.2 const methods

  • The value of a const (constant) object cannot be changed . If you use const objects, references to const objects, and pointers to const objects , the compiler will not allow calls to any methods of the object unless those methods promise not to change any data members. To ensure that a method does not change data members, the method itself can be marked with the const keyword. The following SpreadsheetCell class contains methods marked const that do not change any data members
    class SpreadsheetCell {
          
          
    public:
        double getValue() const; // 最好将不改变对象的任何数据成员的成员函数声明为 const
    };
    
    // const 规范是方法原型的一部分,必须放在方法的定义中
    double SpreadsheetCell::getValue() const {
          
          
        return mValue;
    }
    
  • You cannot declare a static method as const , because a static method has no instance of the class, so it is impossible to change the value inside
  • Non-const objects can call both const and non-const methods. However, const objects can only call const methods
  • const objects are also destroyed and their destructors are called, so destructors should not be marked as const
mutable data member
  • Assuming the spreadsheet application wants to get how often data is read, add a counter to the SpreadsheetCell class that counts the number of getValue() and getString() calls. But doing so makes the compiler think that these methods are non-const, the solution is: set the counter variable as mutable, tell the compiler to allow this value to be changed in the const() method
    class SpreadsheetCell {
          
          
    public:
        double getValue() const; // 最好将不改变对象的任何数据成员的成员函数声明为 const
    private:
        double mValue = 0;
        mutable size_t mNumAccesses = 0;
    };
    
    // 定义
    double SpreadsheetCell::getValue() const {
          
          
        mNumAccesses++;
        return mValue;
    }
    

5.3 Method overloading

  • Multiple constructors can be written in a class, all of which have the same name and differ only in the number or types of parameters. In C++, the same can be done for any method or function, functions or methods can be overloaded: use the name of a function or method for multiple functions, but differ in the type or number of arguments
  • C++ does not allow overloading of method names based solely on the method's return type , because in many cases it is impossible for the compiler to determine which method instance to call
5.3.1 const-based overloading
  • Methods can be overloaded based on const. Write two methods with the same name and the same parameters, one of which is const and the other is not const
    • If it is a const object, call the const method
    • If it is a non-const object, call the non-const method
5.3.2 Explicitly removing overloads
  • Overloaded methods can be explicitly deleted, which can be used to prohibit calls to member functions with specific parameters
    class MyClass {
          
          
    public:
        void foo(int i);
    };
    
  • Can be called in the following way
    MyClass c;
    c.foo(123);
    c.foo(1.23);
    
  • On the third line, the compiler converts the double value (1.23) to an integer value (1), and then calls foo(int i). The compiler may issue a warning, but still perform this implicit conversion. Explicitly removing the double instance of foo() prevents the compiler from performing this conversion
    class MyClass {
          
          
    public:
        void foo(int i);
        void foo(double d) = delete;
    };
    

5.4 Inline methods

  • Function or method calls should not be implemented in the generated code as if calling a separate block of code. Instead, the compiler should insert the method body or function body directly into the place where the method or function is called. This process is called inline, and functions or methods with this behavior are called inline methods or inline functions. Inlining is safer than using #define macros. You can specify a method or function as inline by using the inline keyword before the name of the method or function definition
    // 提示编译器,用实际方法体替换对 getValue() 调用,而不是生成代码进行函数调用
    inline double SpreadsheetCell::getValue() const {
          
          
        mNumAccesses++;
        return mValue;
    }
    
  • If an inline function or method is written, the definition and prototype should be placed in the header file
  • C++ provides an alternative syntax for declaring inline methods, which does not use the inline keyword at all, but places the method definition directly in the class definition
    class SpreadsheetCell {
          
          
    public:
        double getValue() const {
          
          
            mNumAccesses++;
            return mValue;
        }
    };
    

5.5 Default parameters

  • Similar to method overloading, default parameters can specify default values ​​for parameters of functions or methods in the prototype
    • If the user specifies these parameters, the default values ​​will be ignored
    • If the user omits these parameters, the default values ​​will be used
  • There is a limitation: you can only provide a continuous list of default parameters starting from the rightmost parameter , otherwise the compiler will not be able to match missing parameters with default parameters
    class Spreadsheet {
          
          
    public:
        Spreadsheet(size_t width = 100; size_t height = 100);
    };
    
  • The Spreadsheet constructor can be called with 0, 1 or 2 arguments, although there is only one non-copying constructor
    Spreadsheet s1;
    Spreadsheet s2(5);
    Spreadsheet s3(5, 6);
    
  • Anything that can be done with default parameters can be done with method overloading. You can write 3 different constructors, each with a different number of parameters. However, default parameters allow three different numbers of parameters in one constructor

6. Different data member types

6.1 Static data members

  • Sometimes it is unnecessary to have all objects of a class contain a copy of a variable. Data members may only be meaningful to the class, and it is inappropriate for each object to have a copy of it, static data members solve this problem
    • Static data members belong to the class but not the data members of the object, and the static data members can be regarded as global variables of the class
    class Spreadsheet {
          
          
    private:
        static size_t sCounter;
    };
    
  • Not only do you list static class members in the class definition, you also need to allocate memory for them in the source file, usually the one that defines the class methods. Static members can also be initialized here, but note that unlike ordinary variables and data members, they are initialized to 0 by default. static pointers are initialized to nullptr
    // 为 sCounter 分配空间并初始化为 0
    size_t Spreadsheet::sCounter; // 若采用下述内联变量则可删除该行代码
    
6.1.1 Inline variables
  • Starting with C++17, static data members can be declared inline. This has the advantage of not having to allocate space for them in the source file
    class Spreadsheet {
          
          
    private:
        static inline size_t sCounter = 0;
    };
    
6.1.2 Accessing static data members within class methods
  • Inside a class method, static data members can be used like normal data members
    class Spreadsheet {
          
          
    public:
        size_t getId() const;
    private:
        static size_t sCounter;
        size_t mId = 0;
    };
    
    Spreadsheet::Spreadsheet(size_t width, size_t height) : mId(sCounter++), mWidth(width), mHeight(height) {
          
          
        mCells = new SpreadsheetCell*[mWidth];
        for (size_t i = 0; i < mWidth; ++i) {
          
          
            mCells[i] = new SpreadsheetCell[mHeight];
        }
    }
    
  • As can be seen, the constructor has access to sCounter as if it were a normal member
6.1.3 Accessing static data members outside a method
  • Access control qualifiers apply to static data members: sCounter is private and therefore not accessible outside of class methods. If sCounter is public, it can be accessed outside the class method
  • If you want to access static data members, you should implement static get/set methods

6.2 Static constant data members

  • Data members in a class can be declared as const, which means that after creation and initialization, the value of the data member cannot be changed. If a constant applies only to the class, you should use a static constant (static const or const static) data member instead of a global constant
    class Spreadsheet {
          
          
    public:
        static const size_t kMaxHeight = 100;
        static const size_t kMaxWidth = 100;
    };
    

6.3 Reference data members

  • Below is the new Spreadsheet class definition that uses forward declarations to inform the compiler about the SpreadsheetApplication class
    class SpreadsheetApplication;
    
    class Spreadsheet {
          
          
    public:
        Spreadsheet(size_t width, size_t height, SpreadsheetApplication& theApp);
    private:
        SpreadsheetApplication& mTheApp;
    };
    

6.4 Constant Reference Data Members

  • Just like ordinary references can refer to constant objects, reference members can also refer to constant objects. For example, to have a Spreadsheet contain only constant references to the Application object, simply declare mTheApp as a constant reference in the class definition
  • Constant references to SpreadsheetApplication data members can only be used to call constant methods on the SpreadsheetApplication object
    class Spreadsheet {
          
          
    public:
        Spreadsheet(size_t width, size_t height, const SpreadsheetApplication& theApp);
    private:
        const SpreadsheetApplication& mTheApp;
    };
    

7. Nested classes

  • Another class definition can be provided within the definition of a class . For example, assuming that the SpreadsheetCell class is actually part of the Spreadsheet class, we might as well rename SpreadsheetCell to Cell and define both as
    class Spreadsheet {
          
          
    public:
        class Cell {
          
          
        public:
            Cell() = default;
            Cell(double initialValue);
        };
        Spreadsheet(size_t width, size_t height, const SpreadsheetApplication& theApp)
    };
    
  • The Cell class definition is now inside the Spreadsheet class, so references to Cell outside the Spreadsheet class must qualify the name with the Spreadsheet:: scope, even in method definitions
    Spreadsheet::Cell::Cell(double initialValue) : mValue(initialValue) {
          
          }
    
  • If the nested Cell class is directly and fully defined in the Spreadsheet class, the definition of the Spreadsheet class will be slightly bloated. To solve this problem, you only need to add a forward declaration for Cell in the Spreadsheet, and then define the Cell class independently
    class Spreadsheet {
          
          
    public:
        class Cell;
    
        Spreadsheet(size_t width, size_t height, const SpreadsheetApplication& theApp)
    };
    
    class Spreadsheet::Cell {
          
          
    public:
        Cell() = default;
        Cell(double initialValue);
    };
    

Nested classes have access to all private or protected members of the enclosing class, while enclosing classes can only access public members of the nested class

8. Create a stable interface

  • In C++, classes are the main unit of abstraction, and the principles of abstraction should be applied to classes, separating interface and implementation as much as possible . Specifically, all data members should be made private and corresponding getter and setter methods should be provided . This is how the SpreadsheetCell class is implemented:
    • Set mVaue to private
    • setValue() and getValue() are used to set or get these values

8.1 Using interface classes and implementation classes

  • C++ syntax requires public interfaces and private (or protected) data members and methods to be placed in a class definition , thereby exposing certain internal implementation details of the class to clients. The disadvantage of this approach is that if you have to add new non-public methods or data members to the class, all client code must be recompiled , which is a burden for larger projects
  • Solution: You can create a clear interface and hide all implementation details to get a stable interface. The basic principle is to define two classes for each class you want to write: the interface class and the implementation class
    • Implementing class : Same as already written class
    • Interface class : Provides the same public method as the implementation class, but there is only one data member: a pointer to the implementation class object. This is called the bridge pattern, and the implementation of the interface class method just calls the equivalent method of the implementing class object. The effect of this is that no matter how the implementation changes, it will not affect the public interface class, thereby reducing the need for recompilation. Clients using the interface class do not need to be recompiled when the implementation changes (and only the implementation changes). If it is a data member passed by value, client code must be recompiled when the definition of the implementing class changes

Guess you like

Origin blog.csdn.net/qq_42994487/article/details/131294353