Construction and Deconstruction of classes
After understanding the class definition syntax, class members and access member control, we will discuss the life cycle of an object, which involves two crucial activities, creation and destruction.
For the actions of creating and destroying objects, we have talked about how to create an object on the stack, how to create an object on the heap, and how to destroy the objects created on the heap, create through new and destroy through delete.
So how do I initialize an object when I create it?
When destroying this object, how to clean up some resources held by this object?
Cleaning up resources means that a member of this object may be a pointer to a memory or a file handle. If these resources are not cleaned up, it may lead to resource leakage.
Constructor
First, let's look at how to initialize an object when creating a class object: you need to use a constructor
- Constructor is a special function used to construct an object, which is called to initialize the object when it is created.
- When the constructor is not defined, the compiler automatically generates the default version without parameters, and the function body is empty.
- When executing the constructor, first execute its initialization list, and then execute the function body
- The object of a class whose initialization is in the order of member variable declaration
- Constructors can be overloaded or delegated
class Student { public: Student(); Student(uint32_t id); Student(uint32_t id, uint32_t age); void SetId(uint32_t id) { m_id = id; } uint32_t GetId() { return m_id; } private: uint32_t m_id = 0; uint32_t m_age; }; /* Constructor implementation. If there is no constructor implementation and only the constructor declaration, the compiler will report an error with or without definition */ Student::Student(uint32_t id, uint32_t age) : m_id(id), m_age(age) { // to do } Student::Student():Student(0,18) // Delegate, calling the previous constructor to initialize { // to do } Student::Student(uint32_t id):Student(id,18) // Single argument version constructor { // to do }
Destructor
- Destructors are used to clean up objects before they are destroyed, such as freeing memory, closing files, etc
- Called immediately at the end of the object's lifetime, and then frees up the space occupied by the object
- If the destructor is not defined, the compiler automatically generates the default version, and the function body is empty
- Using local objects to implement RAII mechanism
class TimePerf { public: TimePerf(const char *name); ~TimePerf(); private: const char* m_name; int64_t m_startTime; }; TimePerf::TimePerf(const char *name):m_name(name), m_startTime(GetCurTime()) { } TimePerf::~TimePerf() { cout << m_name << "consume time:" << GetCurTime() - m_startTime << endl; } class FileGuard { public: FileGuard(string filePath, ios::openmode mode):m_fs(filePath, mode){} ~FileGuard() { m_fs.close(); } fstream& File() { return m_fs; } private: fstream m_fs; //The file operation class of the standard library, when constructed, gives the file path to m_fs, responsible for opening the file }; int main() { { TimePerf timePerf("test"); } //timePerf is destructed here, which can print the program time FileGuard file(".\\test.txt", ios::out); //Open file printf("isopen:%d\n", file.File().is_open()); // fstream method is called_ open file.File()<<"This is test case!"; return 0; } // At the end of the function, the file object calls the destructor to close the file
Special keywords
- Use the default keyword to require the compiler to generate a default constructor
- Use the explicit keyword to deny that the object is implicitly constructed (single parameter constructors need special attention)
- Use the delete keyword to delete some constructors to avoid the object being constructed that does not meet the expected
class Student { public: Student() = default; //Requires the compiler to generate a default constructor explict Student(uint32_t id):m_id(id), m_age(18) {} //Requires the compiler to reject implicit conversions Student(char id) = delete; //Requires the compiler to remove such constructors private: uint32_t m_id; uint32_t m_age; }; void foo(Student stud) { // to do } int main() { Student stud(id); uint32_t id = 4; foo(id); //wrong // If the implicit conversion is not rejected through the explicit keyword // The parameter of foo function is student object, which can be accessed through uint32_ The object is constructed with a single parameter of type T. when the foo function passes in uint32_t parameter, the compiler will implement implicit conversion. Through this parameter, call the single parameter constructor of Student object to construct and initialize // This implicit conversion should be avoided as much as possible char id2 = 4; Student stud(id2); // wrong // If such constructor is not deleted through the delete keyword // The compiler allows upward type conversion, and the char type will be promoted upward to uint32_t type to complete construction and initialization // But char is a signed type, uint32_t is an unsigned type. If a negative number is passed in, it will be converted to a very large number when converting upward, which is not expected. }
Copy constructor / copy constructor
Class has two special constructors: copy constructor and move constructor.
The copy constructor is used to copy an existing object. The formal parameter is the const reference of the object of this class, and initialize a new object of the same kind with the existing object.
If the copy constructor is not defined, the compiler automatically generates the default copy constructor for copy by bit, that is, shallow copy.
If you do not want the object to be copied, you can use the delete keyword to reject it
class Student { public: Student(uint32_t id, uint32_t age):m_id(id), m_age(age) { cout<<"Student(uint32_t id, uint32_t age)\n"; } Student(const Student& stud) //copy constructor { m_id = stud.m_id; m_age = stud.m_age; cout << "Student(const Student& stud)\n"; } //Student(const Student& stud) = delete; // Reject copy private: uint32_t m_id; uint32_t m_age; }; void Func1(Student obj) //All objects calling this function will have a copy structure when passing parameters. If you don't want to have a copy structure, change the input parameter to the reference of the object { cout << "Func1()" << endl; } Student Func2() //All objects calling this function will have a copy construction when the function returns { cout << "Func2()" << endl; return Student(3,4); //Copy the temporary object to another object and return it. The temporary object will be destroyed at the end of the function //If a move constructor is defined, the move constructor is triggered } int main() { cout << "main()" << endl; Student stud(1,2); Student stud1(stud); Func1(stud); //A copy construction occurs, and the formal parameter stud y is assigned to the argument obj Student stud2 = Func2(); // The compiler generates the default version assignment operator, which is equivalent to the default copy constructor //The copy constructor is triggered. Unlike the following example, if the move constructor is defined, the move constructor is triggered return 0; }
move constructor
- The move constructor is used to transfer the resources of a temporary object to another new object being created
- When a temporary object is used to copy and construct a new object in the destruction stage, the move constructor can be triggered to be called to avoid copying
- The move constructor is a special constructor whose parameter is the right value reference of the object of this class (without const modifier)
- The move constructor generated by the compiler by default is the same as the default copy constructor, which can be rejected with the delete keyword
What is a temporary object? It is an object that will die, such as the return value of a function.
class Test { public: Test():m_ptr(new int(3)) {} //During construction, a memory space is applied for initializing the pointer m_ptr ~Test() { if (m_ptr != nullptr) { delete m_ptr; } } Test(const Test& obj) { m_ptr = new int(0); // Deep copy, re apply for a section of memory space if (obj.m_ptr != nullptr) { *m_ptr = *obj.m_ptr; } } Test(Test&& obj):m_ptr(obj.m_ptr) { obj.m_ptr = nullptr; } //Move the copy constructor, take over the resources of obj, and empty the pointer member of obj private: int *m_ptr; }; Test Func1() { return Test(); //A temporary object is created. When it returns, the class defines a move constructor, which triggers the call of the move constructor } void Func2(Test obj) {} int main() { Test obj1; //A memory space was requested Func2(obj1); //The copy constructor is triggered, creating a new memory space Test obj2 = Func1(); //The move constructor was triggered Test obj3(obj1); // The copy constructor was triggered return 0; }
Object a has a member variable m_ptr points to memory 0x12345678
The figure on the left shows a shallow copy. Since it is copied by bit, the member variable b.M of the shallow copy object b_ PTR and a.m_ptr points to the same address. If the a object is destroyed at this time, a.m_ The memory pointed to by PTR will also be reclaimed, so reusing the b object will cause problems
The figure in the middle is a deep copy. The b object copied out from the deep copy will re apply for a section of memory space during construction, a.m_ptr and b.m_ptr points to the same content and different addresses.
The figure on the right shows the mobile structure. The mobile structure takes over the object resources that will die out and moves the member variable m of the constructed object b_ PTR will point to m of the original object a_ PTR points to the address of a.m_ptr = nullptr null.
Static member of class, constant object, this pointer
- The static member variable of a class is shared by all objects, has a static lifetime, and does not occupy the memory of a specific object
- Static member variables can only be defined and initialized outside the class
- Static member functions can only access static member variables. Static member functions can be called by class name or object
- Constant members can only be initialized through the initial value or initialization list in the class. Initialization by assignment in the constructor is not allowed
- Constant members occupy the memory of an object because constant members of different objects can be initialized to different values.
- Member functions marked const can only be called by objects marked const
- Member functions marked const can only access data and cannot modify data
Object also has a special pointer, this pointer:
- this pointer points to the current object itself, which is the same as the first address of the object.
- Implicit in the parameter list of each non static member function of the class
- Static member functions are not bound to objects, so there is no this pointer
Operator overloading of class
- If special control is required for the assignment operation of existing objects, it can be realized by overloading the assignment operator.
- The compiler will generate a default version assignment operator, which has the same effect as the default copy constructor, and can be rejected with the delete keyword
- By overloading function operators, you can make objects become functions with data, also known as imitation functions. The function operator here refers to a pair of parentheses ()
- Note: new/delete is an operator and can be overloaded
The format of the overloaded function of the assignment constructor or assignment operator is the same as that of the copy constructor type, and the passed parameter is the object reference modified by const. The difference is that the assignment constructor has a return value type and returns a reference to this class of object.
class Test { public: Test& operator=(const Test& rhs) { if(this != &rhs) { //Copy the data of the rhs object return *this; } } }; int main(void) { Test obj1, obj2; obj1 = obj2; //Trigger assignment structure return 0; }
class Less { public: Less(int thresh):threshold(thresh) {} bool operator()(int value) {return value < threshold;} // Overloaded function operator () private: int threshold; }; void Filter(int* array, int num, Less fn) // Print out all values smaller than threshold { for (int i = 0; i < num; i++) { if (fn(array[i])) { cout << array[i] << endl; } } } int main() { int array[5] = {1, -4, -10, 0, -1}; Less less_than(1); Filter(array, 5, less_than); return 0; }
Why overload function operators?
You can disguise an object like Less as a function pointer. At this time, if we template the parameters of the Filter function, we can pass the function pointer or the object to make the Filter function more general.
Then, unlike the function pointer, the object that overloads the function operator holds the data threshold
Class composition and forward declaration
How to use an existing class to create a new class?
Answer: combination and inheritance
- A member of a class can be an object of another class, which implements a more complex abstraction (has-a) based on the existing abstraction. It is an inclusive relationship, such as car and wheel
- The constructor of a composite class needs to initialize not only the basic type member data in this class, but also the object members
- The initialization order of composite class objects is the order of their declarations, and the destruction order is the opposite
class Wheel { public: Wheel(radius):m_radius(radius){} private: float m_radius; }; class Car { public: Car(const Wheel& wheel, int num):m_wheelNum(num), m_wheel(wheel) {} private: int m_wheelNum; Wheel m_wheel; }; int main() { Wheel wheel(0.5); Car car(wheel, 4); return 0; }
Forward declaration is used for a class to be referenced before declaration, but only for reference. It is not allowed to operate on this class object.
As shown in the following figure, class B is defined in the header file of class B, class A is defined in the header file of class A, and its member data is class B object. At this time, the preceding item declaration needs to be made in the header file of class A, otherwise an error will be reported during compilation, and the class B declaration cannot be found. However, in the header file of class A, class B objects cannot be operated because the compiler cannot see the specific definition of class B. in the cpp file of class A, the header file of class B is referenced. At this time, class B objects can be operated.
Inheritance and Derive
- Inheritance is an important form of code reuse and software abstraction in object-oriented programming
- Inheritance allows you to define new classes on the basis of classes. The inherited class is called the base class, and the new class is called the derived class (is-a)
- Derived classes have all the properties of the base class and can make the necessary adjustments to add new properties to themselves
- A base class can derive from multiple base classes, and inheritance can be passed down. A derived class can also derive from multiple base classes
- There is no difference between derived classes and class definitions, except that a class inheritance list is added
class Base { public: Base(int a):m_a(a){} int GetA() {return m_a;} private: int m_a; } class Derived:public Base { public: Derived(int a, int b):Base(a), m_b(b) {} int GetB() {return m_b;} // A GetA function is implied private: int m_b; }
Class member access restrictions
Friend relationships cannot be inherited. Your father's friends are not necessarily your friends
If you need to explicitly indicate the access to the members of the base class, you can add a scope qualifier.
A base class defines a member, whether member function or member variable, whose name may be the same as the new name in the derived class. How to access the members of the base class at this time?
The compiler will find this variable in the derived class by default. If you want to access the of the base class, you can add a scope qualifier
class Base1 { public: Base1(int a):m_a(a){} protected: int m_a; }; class Base2 { public: Base2(int a):m_a(a) {} protected: int m_a; }; class Derived:public Base1, public Base2 { public: Derived(int a, int b):Base1(a), Base2(b) {} void Print() { cout << Base1::m_a << "," << Base2::m_a << endl; } };
Assignment compatibility rule
As we said earlier, the derived class and the base class are the relationship of is-a, that is, the general relationship. For example, buses and private cars are cars for users. Users can treat these derived class objects as base class objects. How can we do this in the program? How can we treat the bus as an ordinary car in the program,
- C + + stipulates that public derived class objects (derived class objects are derived from the base class through public) can be used as base class objects and can be implicitly converted into base class objects. Object slice.
- The derived class object can initialize the reference of the base class, and the pointer of the derived class can be implicitly converted to the pointer of the base class. The reverse of the above rules does not apply.
- Only members inherited from the base class can be accessed through the base class object name and pointer
class Man { public: Man(int strength, string name) : m_strength(strength), m_name(name) {} int GetStrength() { return m_strength; } string GetName() { return m_name; } void Print() { cout << "name : " << m_name << "," << "strength : " << m_strength << endl; } protected: int m_strength; string m_name; }; class SuperMan : public Man { public: SuperMan(int strength, int flySpeed, string name) : Man(strength, name), m_flySpeed(flySpeed) {} int GetFlySpeed() {return m_flySpeed;} void Print() { cout << "name : " << m_name << "," << "strength : " << m_strength << "," << "fly : " << m_flySpeed <<endl; } private: int m_flySpeed; }; bool CheckAbility(Man& a) //The object of superMan class is passed in, but it is implicitly converted into a base class, and the print function of Man class is called { a.Print(); if (a.GetStrength() >= 5) { return true; } return false; } bool CheckAbility(Man* a) { a->Print(); if (a->GetStrength() >= 5) { return true; } return false; } int main() { SuperMan cl(100, 100, "cl"); if (CheckAbility(cl)) { cout << cl.GetName() << " good at job" << endl; } else { cout << cl.GetName() << " not good at job" << endl; } return 0; }
When the derived class calls the CheckAbility function, the print of the overridden derived class fails. This is because the compiler compiles static code. Here, the compiler does not know what is passed in. It only knows that what I need is a Man type. When chaining, it statically binds the print address of Man.
How to solve this problem? It will be explained in polymorphism.
Construction and Deconstruction of derived classes
- By default, a derived class needs to define its own constructor and will not inherit the constructor of the base class
- The using keyword can be used to inherit the base class constructor, but only the base class members can be initialized. The new members of its derived class can be initialized by using the initial value in the class
- The execution order of derived class constructors is
– 1. Base class constructor
– 2. The remainder of the derived class initialization list
– 3. Constructor body - If the base class constructor has parameters or no default parameters, it needs to be explicitly called. Otherwise, the base class constructor can not be explicitly called.
The keyword using can be used to inherit non private member functions of the parent class
For example, a derived class overrides a function with the same name in the parent class, but needs to retain the function of the parent class. It can be inherited through using Base::func
- Destructor of derived class