[C++ Drifting] Understand the object characteristics of classes and objects in one article

In C++, classes and objects are the basic concepts of object-oriented programming. A class is an abstract data type used to describe the properties and behavior of an object. An object is an instance of a class, embodying the properties and behavior of the class. This article will introduce the object characteristics of classes and objects in C++, with a focus on object references.
Insert image description here



Related links:
One article to understand the encapsulation of classes and objects, one article to understand the advanced application of reference functions
in C++

1. Constructor and destructor

When we create a class, it may have some member variables and member functions. Constructors and destructors are special member functions of a class that are used to initialize and clean up objects .

  1. The function of the constructor is to initialize the member variables of the object when creating the object. It has the same name as the class, has no return type, and can have parameters. A constructor can have multiple overloaded versions, and which constructor is used depends on the type and number of parameters passed in. The constructor is automatically called when the object is created.

  2. The role of the destructor is to clean up the object's resources when the object is destroyed. It has the same name as the class, preceded by a tilde (~), has no return type, and accepts no parameters. There can only be one destructor and cannot be overloaded. The destructor is automatically called when the object is destroyed.

Sample code:

class Person {
    
    
public:
    string name;
    int age;

    // 默认构造函数
    Person() {
    
    
        name = "Unknown";
        age = 0;
        cout << "Default constructor called" << endl;
    }

    // 带参数的构造函数
    Person(string n, int a) {
    
    
        name = n;
        age = a;
        cout << "Parameterized constructor called" << endl;
    }

    // 析构函数
    ~Person() {
    
    
        cout << "Destructor called" << endl;
    }
};

int main() {
    
    
    // 使用默认构造函数创建对象
    Person p1;
    cout << "Name: " << p1.name << ", Age: " << p1.age << endl;

    // 使用带参数的构造函数创建对象
    Person p2("John", 25);
    cout << "Name: " << p2.name << ", Age: " << p2.age << endl;

    return 0;
}

Output result:

Default constructor called
Name: Unknown, Age: 0
Parameterized constructor called
Name: John, Age: 25
Destructor called
Destructor called

Code explanation:
In the above example, we defined a Personclass named which has two member variables: nameand age. We use constructors and destructors to initialize and clean up these member variables.

First, we define a default constructor, which takes no parameters. In the default constructor, we will set nameto 0 and print a message to indicate that the constructor was called.Unknownage

Next, we define a parameterized constructor that accepts a string parameter and an integer parameter. In the constructor with parameters, we assign the passed parameter values ​​to namethe agemember variables and print a message to indicate that the constructor has been called.

In the main function, we first create an p1object named using the default constructor. Since no parameters are passed in, the default constructor is called. Then, we print out the values ​​of p1the object's nameand member variables, which are and 0 agerespectively .Unknown

Next, we create an object named using a constructor with parameters p2. We pass in strings "John"and integers 25as parameters, so the constructor with parameters is called. Then, we print out the values ​​of p2the object's nameand member variables, which are and 25 agerespectively .John

At the end of the program, the objects p1and p2out of their scope, so they are destroyed. During object destruction, the destructor is automatically called. In the example, we printed a message in the destructor to indicate that the destructor was called.


2. Classification and calling of functions

1. Classification

  1. Default constructor: If a class does not explicitly define a constructor, the compiler automatically generates a default constructor. The default constructor has no parameters and does not perform any initialization operations. It is called implicitly when creating an object.

  2. Parameterized constructor: A parameterized constructor accepts one or more parameters and uses these parameters to initialize the member variables of the object. They are called when the object is created, and which constructor is used is determined based on the type and number of parameters passed in.

  3. Copy constructor: The copy constructor is a special constructor that accepts a reference to a similar object as a parameter and uses the value of the object to initialize the newly created object. The copy constructor is called when:

    • When using one object to initialize another object
    • Pass object to function as function parameter
    • Return object from function

2. Calling method

  1. Direct call: You can call the constructor directly by adding parentheses to the class name. For example:MyClass obj(10);

  2. Implicit calling: The constructor is called implicitly when creating an object. For example: MyClass obj = MyClass(10);, where the constructor with parameters is called to create the object.

  3. Copy initialization: When one object is used to initialize another object, the copy constructor is called. For example:MyClass obj1(10); MyClass obj2 = obj1;

  4. Function parameter passing: When an object is passed to a function as a function parameter, the copy constructor is called. For example: void func(MyClass obj);, the copy constructor will be called when calling the function func(obj).

  5. Function returns object: When an object is returned from a function, the copy constructor is called. For example:MyClass func() { return MyClass(10); }

3. Sample code

#include <iostream>
using namespace std;

class MyClass {
    
    
public:
    int num;

    // 默认构造函数
    MyClass() {
    
    
        num = 0;
        cout << "Default constructor called" << endl;
    }

    // 带参数的构造函数
    MyClass(int n) {
    
    
        num = n;
        cout << "Parameterized constructor called" << endl;
    }

    // 拷贝构造函数
    MyClass(const MyClass& obj) {
    
    
        num = obj.num;
        cout << "Copy constructor called" << endl;
    }

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

int main() {
    
    
    // 直接调用构造函数
    MyClass obj1(10);

    // 隐式调用构造函数
    MyClass obj2 = MyClass(20);

    // 拷贝初始化
    MyClass obj3(obj1);

    // 函数参数传递
    void func(MyClass obj);
    func(obj1);

    // 函数返回对象
    MyClass func();
    MyClass obj4 = func();

    return 0;
}

4. Output results:

Parameterized constructor called
Parameterized constructor called
Copy constructor called
Copy constructor called
Destructor called
Destructor called
Destructor called
Destructor called

5. Code explanation

In the above example, we have defined a MyClassclass named which contains one member variable numand multiple constructors. We create multiple objects and call the constructor in different ways.

First, we create the object by calling the constructor directly obj1, which calls the constructor with parameters. We then create the object by implicitly calling the constructor obj2, which also calls the constructor with parameters.

Next, we create the object via copy-initialization obj3, which obj1initializes it with the object's value. During the copy initialization process, the copy constructor will be called.

Then, we define a function functhat accepts an MyClassobject as a parameter. When a function is called func(obj1), the copy constructor is called to obj1pass the object to the function.

Finally, we define a function functhat returns an MyClassobject. The copy constructor is called when a function is called func()and the returned object is assigned to.obj4

At the end of the program, all objects go out of their scope, so they are destroyed. During object destruction, the destructor is automatically called. In the example, we printed a message in the destructor to indicate that the destructor was called.


3. Timing of copy constructor

  1. When an object is used to initialize another object : When an existing object is used to initialize a new object, the copy constructor is called. For example:
class MyClass {
    
    
public:
    MyClass(int value) : m_value(value) {
    
    }
    MyClass(const MyClass& other) : m_value(other.m_value) {
    
    
        std::cout << "Copy constructor called" << std::endl;
    }
private:
    int m_value;
};

MyClass obj1(10);
MyClass obj2 = obj1; // 调用拷贝构造函数
  1. Passing an object to a function as a function argument : When an object is passed to a function as a function argument, the copy constructor is called. For example:
class MyClass {
    
    
public:
    MyClass(int value) : m_value(value) {
    
    }
    MyClass(const MyClass& other) : m_value(other.m_value) {
    
    
        std::cout << "Copy constructor called" << std::endl;
    }
private:
    int m_value;
};

void func(MyClass obj) {
    
    
    // Do something with obj
}

MyClass obj1(10);
func(obj1); // 调用拷贝构造函数

  1. Returning an object from a function : When an object is returned from a function, the copy constructor is called. For example:
class MyClass {
    
    
public:
    MyClass(int value) : m_value(value) {
    
    }
    MyClass(const MyClass& other) : m_value(other.m_value) {
    
    
        std::cout << "Copy constructor called" << std::endl;
    }
private:
    int m_value;
};

MyClass func() {
    
    
    MyClass obj(10);
    return obj; // 调用拷贝构造函数
}

  1. When using a class object for assignment, the copy constructor will also be called . For example:
class MyClass {
    
    
public:
    MyClass(int value) : m_value(value) {
    
    }
    MyClass(const MyClass& other) : m_value(other.m_value) {
    
    
        std::cout << "Copy constructor called" << std::endl;
    }
private:
    int m_value;
};

MyClass obj1(10);
MyClass obj2;
obj2 = obj1; // 调用拷贝构造函数

It should be noted that the compiler sometimes performs optimizations to avoid unnecessary calls to the copy constructor. This optimization is called "copy elision". In some cases, the compiler may move an object's value directly from one location to another instead of making a copy constructor call. This improves performance but does not call the copy constructor.


4. Constructor calling rules

The constructor calling rules are as follows:

  1. Default constructor : If no constructor is explicitly defined, the compiler will automatically generate a default constructor. The default constructor has no parameters and performs default initialization operations. When an object is created, if no parameters are provided, the default constructor is called.

  2. Parameterized constructor : The parameterized constructor accepts one or more parameters and uses these parameters to initialize the member variables of the object. When an object is created, if parameters are provided, the corresponding parameterized constructor is called.

  3. Copy constructor : The copy constructor accepts an object of the same type as a parameter and uses the value of the object to initialize the new object. The copy constructor can be used in scenarios such as object copy initialization, function parameter passing, and function return objects.

  4. Move constructor : The move constructor is a new feature introduced in C++11. It accepts an rvalue reference as a parameter and uses the value of the parameter to initialize a new object. Move constructors are often used to improve performance when resource ownership of an object is transferred.

The calling rules of the constructor are as follows:

  • When an object is created, the appropriate constructor is selected and called based on the type and number of parameters provided. If no parameters are provided, the default constructor is called.
  • The copy constructor is called when one object is used to initialize another object.
  • The copy constructor is called when an object is passed to a function as a function argument.
  • When an object is returned from a function, the copy constructor is called.
  • When using a class object for assignment, the copy constructor will also be called.
  • In some cases, the compiler will perform optimizations to avoid unnecessary calls to the copy constructor. This optimization is called "copy elision".

5. Deep copy and shallow copy

Shallow copy refers to copying the value of one object to another object, including the object's member variables. This means that both objects share the same memory address, and modifications to one of them will affect the other. Shallow copy only copies the surface level of the object and does not copy the resources owned by the object.

Deep copy refers to copying the value of one object to another object and allocating independent memory space for the new object. In this way, the two objects have independent memory spaces, and modifications to one object will not affect the other object. A deep copy recursively copies all member variables of an object, including resources owned by the object.

Sample code:

#include <iostream>
#include <cstring>

class Person {
    
    
public:
    Person(const char* name, int age) {
    
    
        m_name = new char[strlen(name) + 1];
        strcpy(m_name, name);
        m_age = age;
    }
    
    // 拷贝构造函数
    Person(const Person& other) {
    
    
        m_name = new char[strlen(other.m_name) + 1];
        strcpy(m_name, other.m_name);
        m_age = other.m_age;
    }
    
    // 析构函数
    ~Person() {
    
    
        delete[] m_name;
    }
    
    // 打印信息
    void printInfo() {
    
    
        std::cout << "Name: " << m_name << ", Age: " << m_age << std::endl;
    }
    
private:
    char* m_name;
    int m_age;
};

int main() {
    
    
    Person person1("Alice", 25);
    Person person2 = person1; // 浅拷贝
    person1.printInfo(); // 输出:Name: Alice, Age: 25
    person2.printInfo(); // 输出:Name: Alice, Age: 25
    
    person2.printInfo(); // 输出:Name: Alice, Age: 25
    person2.printInfo(); // 输出:Name: Alice, Age: 25
    
    person1.printInfo(); // 输出:Name: Bob, Age: 30
    person2.printInfo(); // 输出:Name: Alice, Age: 25
    
    return 0;
}

In the above example, we defined a Personclass that contains a member variable of string type m_nameand an integer member variable m_age. In the constructor, we use newoperators to m_nameallocate a separate memory space and copy the string into that memory space.

Then, we create an person1object and assign its value to person2the object. Since the default copy constructor is a shallow copy, the pointer person2of the object m_namepoints to the person1same memory space as the object. When we modify person2the object m_name, we actually modify person1the object m_name. This is the characteristic of shallow copy.

m_nameIn order to implement deep copy, we need to customize the copy constructor, allocate a separate memory space in it , and copy the string into this space. In this way, person2the object has its own independent m_namememory space, and modifications to it will not affect person1the object.

To sum up, shallow copy only copies the surface level of the object, while deep copy recursively copies all member variables of the object, including the resources owned by the object. Deep copy requires a custom copy constructor to implement.


6. Initialization list

The initialization list is a method of initializing member variables in the constructor and can be used to implement deep copying.

In the above example, we can use an initialization list to implement a deep copy without manually allocating memory and copying the string in the copy constructor.

Here is an example of using an initialization list to implement a deep copy:

#include <iostream>
#include <cstring>

class Person {
    
    
public:
    Person(const char* name, int age) : m_age(age) {
    
    
        m_name = new char[strlen(name) + 1];
        strcpy(m_name, name);
    }
    
    // 拷贝构造函数
    Person(const Person& other) : m_age(other.m_age) {
    
    
        m_name = new char[strlen(other.m_name) + 1];
        strcpy(m_name, other.m_name);
    }
    
    // 析构函数
    ~Person() {
    
    
        delete[] m_name;
    }
    
    // 打印信息
    void printInfo() {
    
    
        std::cout << "Name: " << m_name << ", Age: " << m_age << std::endl;
    }
    
private:
    char* m_name;
    int m_age;
};

int main() {
    
    
    Person person1("Alice", 25);
    Person person2 = person1; // 深拷贝
    person1.printInfo(); // 输出:Name: Alice, Age: 25
    person2.printInfo(); // 输出:Name: Alice, Age: 25
    
    person2.printInfo(); // 输出:Name: Alice, Age: 25
    person2.printInfo(); // 输出:Name: Alice, Age: 25
    
    person1.printInfo(); // 输出:Name: Alice, Age: 25
    person2.printInfo(); // 输出:Name: Alice, Age: 25
    
    return 0;
}

In the above example, we allocate a separate memory space in the constructor's initialization list and copy the string into that space. In this way, person2the object has its own independent m_namememory space, and modifications to it will not affect person1the object.

Using an initialization list can simplify the code and ensure that member variables have been correctly initialized when the object is constructed. This is useful for implementing deep copies.


7. Class objects as class members

When the member variables of one class are objects of another class, we need to copy these member variables correctly in the copy constructor.

Here is an example where Personone of the member variables of a class is Addressan object of the class:

#include <iostream>
#include <cstring>

class Address {
    
    
public:
    Address(const char* city, const char* street) {
    
    
        m_city = new char[strlen(city) + 1];
        strcpy(m_city, city);
        
        m_street = new char[strlen(street) + 1];
        strcpy(m_street, street);
    }
    
    Address(const Address& other) {
    
    
        m_city = new char[strlen(other.m_city) + 1];
        strcpy(m_city, other.m_city);
        
        m_street = new char[strlen(other.m_street) + 1];
        strcpy(m_street, other.m_street);
    }
    
    ~Address() {
    
    
        delete[] m_city;
        delete[] m_street;
    }
    
    void printInfo() {
    
    
        std::cout << "City: " << m_city << ", Street: " << m_street << std::endl;
    }
    
private:
    char* m_city;
    char* m_street;
};

class Person {
    
    
public:
    Person(const char* name, int age, const Address& address) : m_age(age), m_address(address) {
    
    
        m_name = new char[strlen(name) + 1];
        strcpy(m_name, name);
    }
    
    Person(const Person& other) : m_age(other.m_age), m_address(other.m_address) {
    
    
        m_name = new char[strlen(other.m_name) + 1];
        strcpy(m_name, other.m_name);
    }
    
    ~Person() {
    
    
        delete[] m_name;
    }
    
    void printInfo() {
    
    
        std::cout << "Name: " << m_name << ", Age: " << m_age << std::endl;
        m_address.printInfo();
    }
    
private:
    char* m_name;
    int m_age;
    Address m_address;
};

int main() {
    
    
    Address address("New York", "Broadway");
    Person person1("Alice", 25, address);
    Person person2 = person1; // 深拷贝
    person1.printInfo(); // 输出:Name: Alice, Age: 25, City: New York, Street: Broadway
    person2.printInfo(); // 输出:Name: Alice, Age: 25, City: New York, Street: Broadway
    
    return 0;
}

In the above example, Personone of the member variables of the class is Addressan object of the class. In Personthe copy constructor of the class, we use the copy constructor to copy the object correctly Address. In this way, when we copy an Personobject, Personthe object and its member variable Addressobjects will be deeply copied.

It should be noted that in Personthe destructor of the class, we only need to release m_namethe memory space of the member variables, because m_addressthe memory space of the member variables will be Addressreleased in the destructor of the class.

To sum up, when the member variables of one class are objects of another class, we need to correctly copy these member variables in the copy constructor to implement deep copy.


8. Static members

Static member variables belong to the class itself rather than to instances of the class. Therefore, there is no need to copy static member variables in the copy constructor because they are shared among all instances of the class.

Here is an example where the Personclass has a static member variable count:

#include <iostream>

class Person {
    
    
public:
    Person(const char* name, int age) : m_age(age) {
    
    
        m_name = new char[strlen(name) + 1];
        strcpy(m_name, name);
        count++;
    }
    
    Person(const Person& other) : m_age(other.m_age) {
    
    
        m_name = new char[strlen(other.m_name) + 1];
        strcpy(m_name, other.m_name);
        count++;
    }
    
    ~Person() {
    
    
        delete[] m_name;
        count--;
    }
    
    static int getCount() {
    
    
        return count;
    }
    
private:
    char* m_name;
    int m_age;
    static int count;
};

int Person::count = 0;

int main() {
    
    
    Person person1("Alice", 25);
    Person person2 = person1; // 深拷贝
    std::cout << "Count: " << Person::getCount() << std::endl; // 输出:Count: 2
    
    return 0;
}

In the above example, Personthe class has a static member variable countthat records the number of objects created Person. In the constructor, we countkeep track of the number of objects by incrementing it, and in the destructor we countupdate the number of objects by decrementing it.

In the copy constructor, we do not need to copy the static member variable countbecause it belongs to the class itself rather than an instance of the class. Therefore, only non-static member variables need to be copied in the copy constructor.

To sum up, static member variables do not need to be copied in the copy constructor because they belong to the class itself rather than instances of the class.

Guess you like

Origin blog.csdn.net/Goforyouqp/article/details/132791968