C++: Classes and objects (below) - this pointer, (copy) constructor, destructor, copy operator overloading

Table of contents

One, this pointer

1.1 Introduction

1.2 Questions

1.3 Features

Second, the constructor

2.1 Concept

2.2 Features

2.3 Grammar

2.4 Notes

3. Destructor

3.1 Concept

3.2 Features

3.3 Examples

Fourth, the copy constructor

4.1 Concept

4.2 Features

4.3 Examples

4.4 Deep and shallow copy

5. Assignment operator overloading

5.1 Concept

5.2 Syntax

5.3 Examples


One, this pointer

1.1 Introduction

First look at a piece of code

#include <iostream>

class Person
{
public:
    void Init(const std::string& name, int age)
    {
        _name = name;
        _age = age;
    }

    void Print()
    {
        std::cout << "Name: " << _name << ", Age: " << _age << std::endl;
    }

private:
    std::string _name;
    int _age;
};

int main()
{
    Person p1, p2;
    p1.Init("John", 30);
    p2.Init("Alice", 25);
    p1.Print(); // Output: Name: John, Age: 30
    p2.Print(); // Output: Name: Alice, Age: 25
    return 0;
}

1.2 Questions

There are two member functions Init and Print in the Person class , and there is no distinction between different objects in the function body, so how to distinguish which object is calling?

C++ designers propose to use the this pointer to solve this problem. When we call member functions, the C++ compiler will internally add a hidden pointer parameter, that is, a pointer , to each non-static member function . This pointer is a constant pointer to the address of the current object , which points to the object that called the member function. In the function body, all operations on member variables are accessed through pointers. It's just that all operations are transparent to the user, that is, the user does not need to pass it, and the compiler completes it automatically.thisthisthis

 As shown in the figure, when we define a class member function, it should be defined in the first format above. The second is that the compiler automatically passes the constant pointer to the member calling the non-static member function in the parameter list. The above figure is just In order for readers to experience the process, member attributes can be used directly inside the function. When the function parameter name conflicts with the member attribute, the way of this->member attribute can be used to distinguish!!!

In member functions, we can use thispointers to access member variables and member functions of the current object. For example, if there is a member variable and function called in the class value, we can use it this->valueto clearly indicate that the member variable is accessed instead of the function. In addition, by returning in a member function *this, we can implement chained calls, improving the readability and simplicity of the code.

1.3 Features

  • thisThe type of the pointer is , that is , the pointer 类类型* constcannot be assigned a value in the member function , because it points to the address of the current object, and it is not allowed to point to other objects.this
  • thisPointers can only be used inside member functions, not in non-member functions of a class or global functions.
  • thisA pointer is essentially an implicit parameter of a member function. When an object calls a member function, the compiler will pass the address of the object as an actual parameter to the thispointer. Therefore, the object itself does not store thispointers.

Second, the constructor

Empty class : There are no member properties and member functions in the class.

In C++, the class automatically generates some default member functions for you if you don't define them in the class, if you don't define them explicitly. These default member functions include:

  • Default Constructor
  • Default Destructor
  • Default Copy Constructor
  • Default Copy Assignment Operator
  • Default Move Constructor
  • Default Move Assignment Operator

In this article, we mainly introduce the first four functions, and then introduce other functions.

2.1 Concept

We can refer to the above code to expand the introduction.

class Person
{
public:
    void Init(const std::string& name, int age)
    {
        _name = name;
        _age = age;
    }

    void Print()
    {
        std::cout << "Name: " << _name << ", Age: " << _age << std::endl;
    }

private:
    std::string _name;
    int _age;
};

When we create an object, we have to call the Init function to assign values ​​to the object properties every time. However, if we call this method to assign values ​​every time we create an object, it seems a little cumbersome. Can we set the information when the object is created? go in? This creates our constructor .

  • Function: Used to initialize the default value of the member variable when creating the object.
  • Usage: When you create a class object, if you do not explicitly provide a constructor, the compiler will automatically generate a default constructor for you. The default constructor has no parameters, and it initializes member variables to their default values ​​​​of their corresponding types (for example, 0 for numeric types, nullptr for pointer types, and members of class objects call their own constructors to initialize). The implementation may be different in different compilers. Some compilers do not deal with built-in types, which are random values. Members of class objects will call their constructors.
  • The constructor is a special member function with the same name as the class name, which is automatically called by the compiler when creating a class type object to ensure that each data member has a suitable initial value, and is called only once in the entire life cycle of the object .

2.2 Features

The constructor is a special member function. It should be noted that although the name of the constructor is called construction, the main task of the constructor is not to create a space to create an object, but to initialize the object and initialize the member properties in the object.

feature:

  • 1. The function name is the same as the class name.
  • 2. No return value.
  • 3. The compiler automatically calls the corresponding constructor when the object is instantiated.
  • 4. The constructor can be overloaded .

2.3 Grammar

#include <iostream>
#include <string>

class Person
{
public:
    // 1.无参构造函数
    Person()
    {}

    // 2.带参构造函数
    Person(const std::string& name, int age)
    {
        _name = name;
        _age = age;
    }

    // 3.打印个人信息
    void PrintInfo()
    {
        std::cout << "Name: " << _name << ", Age: " << _age << std::endl;
    }

private:
    std::string _name;
    int _age;
};

From the above code, we can temporarily divide constructors into parameterless constructors and parameterized constructors. Let’s take a look at how to use these two parameters to initialize objects.

int main()
{
    // 调用无参构造函数创建对象
    Person p1;
    p1.PrintInfo();

    // 调用带参构造函数创建对象
    Person p2("John", 30);
    p2.PrintInfo(); // Output: Name: John, Age: 30

    return 0;
}

When using no-argument construction to initialize an object, you can directly use the class name + object name . When using parameter construction, you need to pass in the corresponding parameters to initialize member properties.

remind

Person person();

When instantiating an object with no-argument construction, do not add () after the object. This will cause the compiler to be unable to identify whether this is an instantiation of an object using no-argument construction or a function that declares that the return value is a Person type. It is best not to use this ! ! !

If we do not actively write a constructor when defining a class, the system will automatically generate a default constructor, that is, a no-argument constructor. Once the user explicitly defines that the compiler will no longer generate, that is, the user implements the parameter or no parameter, the compiler no longer implements the default (no parameter) constructor.

If you annotate the no-argument constructor of the above code, leaving only the argument-argument constructor, then you can only instantiate the object by calling the argument-argument constructor, and you cannot use the no-argument constructor.

2.4 Notes

Both the parameterless constructor and the default constructor are called default constructors, and there can only be one default constructor. Note: No-argument constructors, full default constructors, and constructors that we did not write to be generated by the compiler by default can all be considered default constructors .

class Date {
public:
    Date() {
        _year = 1900;
        _month = 1;
        _day = 1;
    }

    Date(int year = 1900, int month = 1, int day = 1) {
        _year = year;
        _month = month;
        _day = day;
    }

private:
    int _year;
    int _month;
    int _day;
};

// 以下测试函数能通过编译吗?
void Test() {
    Date d1;
}

The answer is no.

Because at this time, the no-argument constructor and the full default constructor can instantiate objects for this line of code, causing ambiguity and failing to compile! ! !

3. Destructor

3.1 Concept

The destructor is a special member function in C++, which is used to clean up and release resources when the object is destroyed. Its name is prefixed with a tilde (~) before the class name, for example, if the class name is ClassName, then the name of the destructor is ~ClassName.

The role of the destructor is to deal with the aftermath of the object. When the life cycle of the object ends (for example, the object goes out of scope, is explicitly deleted, or the program exits), the destructor will be called automatically.

3.2 Features

Destructors have the following characteristics:

  1. Destructors have no return value, including void, and no parameters.
  2. A class can have one and only one destructor, and it cannot be overloaded.
  3. If you don't explicitly define a destructor, the compiler will automatically generate a default destructor for you.
  4. If there are dynamically allocated resources in the class (such as memory on the heap, file handles, etc.), these resources should be released in the destructor to avoid memory leaks and resource leaks.

3.3 Examples

#include <iostream>

class MyClass {
public:
    // 构造函数
    MyClass() {
        std::cout << "Constructor called." << std::endl;
    }

    // 析构函数
    ~MyClass() {
        std::cout << "Destructor called." << std::endl;
    }
};

int main() {
    std::cout << "Creating object..." << std::endl;
    MyClass obj; // 创建对象,调用构造函数

    std::cout << "Object will be destroyed..." << std::endl;
    // 在这里,obj超出了作用域,对象的生命周期结束,析构函数被自动调用

    return 0;
}
Creating object...
Constructor called.
Object will be destroyed...
Destructor called.

This proves that the object's constructor and destructor are called when the object is created and destroyed respectively. The call of the destructor can ensure that the object completes the necessary cleanup work when it is destroyed, releases resources, and avoids resource leaks.

Reminder: If you do not manually write a destructor, the system will automatically generate a destructor, but the system's own destructor is implemented in time and space, and does not do anything. Of course, if there is no space created by the heap area inside the class object, just use the one generated by the system. But if there is space opened up in the heap area, it needs to be released manually inside the destructor, otherwise it will easily cause memory leaks! ! ! Secondly, if there are other class member attributes in the class object, the destructor of the class member attribute will be called automatically when the object is destroyed, and there is no need to manage it in the destructor of the class object! ! !

Fourth, the copy constructor

4.1 Concept

The copy constructor is a special constructor in C++, which is used to create a new object when the object is copied, and copy the value of the original object to the new object. Its function is to generate a new object, which has the same content as the original object, but they are independent, and modifying the content of one object will not affect the other object.

If you don't explicitly define a copy constructor, the compiler will generate a default copy constructor for you. The default copy constructor copies the values ​​of member variables one by one, and makes a shallow copy of the pointer members in the class (that is, copies the value of the pointer instead of copying the object pointed to by the pointer). If there are resources in the class that need deep copying (such as dynamically allocated memory), you need to define a copy constructor to complete the deep copying, otherwise a piece of space will be released repeatedly in the destructor and cause errors.

4.2 Features

  • The copy constructor is an overloaded form of the constructor.
  • The parameter of the copy constructor is only one and must be a reference to a class type object, and the compiler will directly report an error if the value-passing method is used, because it will cause infinite recursive calls.
ClassName(const ClassName& other);

4.3 Examples

Suppose we have a class MyClassand we try to define a bad copy constructor that takes parameters by value:

In the above example, we defined a MyClassclass called and tried to define a copy constructor using pass-by-value. When we try to create obj2an object using the copy constructor, it results in an infinite recursive call, resulting in a stack overflow.

This happens because the pass-by-value method calls the copy constructor itself to create a copy of the passed parameter, and then creates a copy of the parameter again during the call to the copy constructor, resulting in infinite recursion.

In order to avoid infinite recursive calls, the parameters of the copy constructor must be received by reference, so that only the reference of the object will be passed when the copy constructor is called, and no new copy will be created.

The following is the corrected sample code, using the reference method to define the correct copy constructor:

#include <iostream>

class MyClass {
public:
    // 正确的拷贝构造函数
    MyClass(const MyClass& obj) {
        std::cout << "Copy constructor called." << std::endl;
    }
};

int main() {
    MyClass obj1;
    MyClass obj2 = obj1; // 正确,使用引用方式传递参数
    MyClass obj3(obj2);    //此种方式也可以

    return 0;
}

4.4 Deep and shallow copy

Shallow copy (Shallow Copy) : Shallow copy means that when copying an object, only the value of the member variable in the object is copied, including the value of the pointer member variable. This means that the new object and the original object will share the same resources, rather than creating separate resource copies for the new object. If the original object contains pointer members pointing to heap memory, the pointer members of the new object and the original object point to the same heap memory after shallow copying, resulting in two objects managing the same resource, which may lead to resource release problems and potential errors .

Deep copy (Deep Copy) : Deep copy means that when an object is copied, an independent resource copy will be created for the new object instead of shared resources. If the original object has a pointer member pointing to heap memory, deep copy will allocate memory for the pointer member of the new object separately, and copy the content pointed to by the original object pointer to the new memory. In this way, the two objects have their own independent resources, and modifying the resources of one object will not affect the other object.

#include <iostream>
#include <cstring>
#include <cstdlib>

class Person {
public:
    // 构造函数
    Person(const char* name, int age) {
        _name = (char*)malloc(strlen(name) + 1);
        strcpy(_name, name);
        _age = age;
    }

    // 拷贝构造函数(浅拷贝)
    Person(const Person& other) {
        _name = other._name; // 浅拷贝,共享资源
        _age = other._age;
    }

    // 深拷贝构造函数(深拷贝)
    Person(const Person& other) {
        _name = (char*)malloc(strlen(other._name) + 1); // 深拷贝,为新对象分配独立资源
        strcpy(_name, other._name);
        _age = other._age;
    }

    // 析构函数
    ~Person() {
        free(_name);
    }

    // 打印个人信息
    void PrintInfo() {
        std::cout << "Name: " << _name << ", Age: " << _age << std::endl;
    }

private:
    char* _name;
    int _age;
};

int main() {
    // 创建一个Person对象
    Person p1("John", 30);

    // 浅拷贝
    Person p2(p1);
    p1.PrintInfo(); // Output: Name: John, Age: 30
    p2.PrintInfo(); // Output: Name: John, Age: 30

    // 修改p1的值
    p1 = Person("Alice", 25);
    p1.PrintInfo(); // Output: Name: Alice, Age: 25
    p2.PrintInfo(); // Output: Name: Alice, Age: 30(由于浅拷贝,p2共享p1的资源,也被修改为Alice)

    // 深拷贝
    Person p3(p1);
    p1.PrintInfo(); // Output: Name: Alice, Age: 25
    p3.PrintInfo(); // Output: Name: Alice, Age: 25(由于深拷贝,p3拥有独立的资源,不受p1的修改影响)

    return 0;
}

5. Assignment operator overloading

5.1 Concept

Assignment operator overloading is a special function that allows assignment operations between members of custom classes in C++. By overloading the assignment operator, we can implement custom assignment behavior between class objects to ensure correct copying of objects and resource management.

5.2 Syntax

返回类型 operator=(const 类名& 另一个对象) {
    // 赋值操作的实现
    // 返回对象本身的引用
}

Among them, the return type is usually a reference type, which can support continuous assignment operations. The argument is a constreference representing the object on the right-hand side of the assignment operator passed in.

5.3 Examples

#include <iostream>
#include <cstring>

class Person {
public:
    Person(const char* name, int age) {
        _name = new char[strlen(name) + 1];
        strcpy(_name, name);
        _age = age;
    }

    // 拷贝构造函数
    Person(const Person& other) {
        _name = new char[strlen(other._name) + 1];
        strcpy(_name, other._name);
        _age = other._age;
    }

    // 赋值运算符重载
    Person& operator=(const Person& other) {
        if (this == &other) { // 自我赋值检测
            return *this;
        }
        delete[] _name; // 释放旧资源

        _name = new char[strlen(other._name) + 1];
        strcpy(_name, other._name);
        _age = other._age;

        return *this; // 返回对象本身的引用
    }

    ~Person() {
        delete[] _name;
    }

    void PrintInfo() {
        std::cout << "Name: " << _name << ", Age: " << _age << std::endl;
    }

private:
    char* _name;
    int _age;
};

int main() {
    Person p1("John", 30);
    Person p2("Alice", 25);

    p1.PrintInfo(); // Output: Name: John, Age: 30
    p2.PrintInfo(); // Output: Name: Alice, Age: 25

    p2 = p1; // 赋值操作

    p1.PrintInfo(); // Output: Name: John, Age: 30
    p2.PrintInfo(); // Output: Name: John, Age: 30(p2被赋值为p1的内容)

    return 0;
}

In this example, we Personoverloaded the assignment operator in the class. In the overloaded function, we first check if a self-assignment has occurred (the object itself is assigned to itself), and if so, we directly return a reference to the object. Then free the old resources (delete the old _namememory), then reallocate the memory and copy the new content.

Guess you like

Origin blog.csdn.net/weixin_57082854/article/details/132123515