Classes and Objects: Constructors and Destructors

1. Definition of constructor and destructor

Constructor: is a function called after the class is initialized. It contains parameters and can be overloaded.

Destructor: A function called when a class is destroyed. It contains no parameters, cannot be overloaded, and is called only once.

Among them, the syntax of the destructor is:

name()

{

information……

}

The thin dog function syntax only needs to add the “~” symbol before the constructor.

~name()

{

information……

}

Let’s take a look at a simple example to get a preliminary understanding of these two functions.

#include<iostream>
using namespace std;



class dog
{
public:

    dog()

    {

        cout << "The constructor was called!" << endl;

    }

    ~dog()

    {

        cout << "The destructor was called!" << endl;

    }

};

void test()
{
    dog a;
}

int main()
{

    test();

    system("pause");

    return 0;

}

Running the above code, you will get the following results:

The constructor was called!

The destructor was called!

Please press any key to continue…

If we do not use the test function (remember to add parentheses when using the test function!), then we will get the following effect:

#include<iostream>
using namespace std;



class dog
{
public:

    dog()

    {

        cout << "The constructor was called!" << endl;

    }

    ~dog()

    {

        cout << "The destructor was called!" << endl;

    }

};

void test()
{
    dog a;
}

int main()
{

    dog a;

    system("pause");

    return 0;

}

The running results are as follows:

The constructor was called!

Please press any key to continue…

The destructor was called!

You’ll notice that the call to the destructor occurs after the program has run.

This is because after the function call is completed, the memory of the function body will be automatically released, and the destructor will be used.

If the dog class is created directly in the main function, it will be stored in the stack area and will be destroyed by the compiler after the entire program is run.

2. Classification of constructors

Constructors can be divided into default constructors, parameterized constructors and copy constructors; among them, copy constructors are divided into shallow copy and deep copy.

Default constructor

The following is a default constructor:

dog()
{
cout << "Default constructor called!" << endl;
}

Such a constructor takes no parameters and can also be called a parameterless constructor.

In the compiler, if you do not declare a constructor, the compiler will automatically generate an empty-implemented constructor for you, which has the same effect as:

dog()
{
    //I am empty, hee hee...
}

This will not be displayed in the code area, the compiler will automatically write it for you.

It is worth noting that when calling the default constructor, do not add parentheses and write it as follows:

dog a();

The default constructor does not require you to add parentheses. On the contrary, if you add parentheses, the compiler will regard this declaration as the declaration of the function body, which means “creating a function of the dog class”, and the compiler will report an error because there is no function body below, instead of “call dog’s constructor”.

In order to distinguish between the two, you can only choose to remove the parentheses and write it as follows:

dog a;

Parameterized constructor

The parameterized constructor, just like its name, is a constructor with parameters. Its general syntax is:

dog(int a)
{
cout << "The parameterized function was called!" << endl;
}

Among them, a is the formal parameter of the constructor, which we need to input.

There is no difference between the parameterized constructor and the default constructor on the whole. The only outstanding thing is that the parameterized constructor has more parameters. It can initialize the values of some members when building the class, which is convenient for us to operate. For details, please refer to the example below:

#include<iostream>
using namespace std;

class dog
{
public:
int age;
dog(int a)
{
age = a;
cout << "The parameterized constructor was called!" << endl;
}


};

void test()
{
    dog a(10);
    cout<<"a's age is:"<<"a.age<<endl;
}

int main()
{
    test();
    system("pause");
    return 0;
}

You can create multiple parameters at the same time in a parameterized constructor so that you can achieve the desired effect.

It is worth noting that when you build a parameterized constructor or a default constructor, the compilation will no longer create a default constructor for you, but will use the constructor you created.

We will introduce this feature in detail in the shallow copy and deep copy sections later.

Copy constructor

As we mentioned before, copy constructors are divided into two types: shallow copy and deep copy. In order to facilitate your understanding, the author will temporarily put aside the differences between the two and introduce them in subsequent paragraphs.

The purpose of using the copy function is to copy members of one class to another class.

The general syntax is as follows:

dog(const dog & amp; b)
{
age = b.age;
cout << "Copy constructor call!" << endl;
}

Through this operation, we can copy the age in b to the age of another class (provided that the age attribute is public).

Several places need to remind you:

1. About const modifier:

Here we use the const modifier, which is intended to protect the members in dog b from being modified (this is necessary).

2. Regarding the use of quotation marks:

The reference character here is intended to save memory space, and there is no need to transfer a copy of the members in b to the function.

3. Regarding the use of parameters:

Multiple formal parameters can be imported here and used according to the situation.

After understanding the three constructors, we will learn three ways to call it.

3. Classification of calling methods

The calling methods are divided into bracket method, explicit method and implicit conversion method.

  • Brackets

The bracket method is a more direct way to call the constructor.

Based on the above code, we can achieve it through the following test01 function:

 void test01()

{

      dog a;//Don’t use dog a()!

      dog b(10);//Call of parameterized constructor

      dog c(b);//Call of copy constructor

}

You can intuitively distinguish the three based on the differences in the constructor parameters (that is, the contents within the brackets), so we call this calling method the bracket method.

  • Explicit method

The explicit method, as its name suggests, implements constructor calls through obvious assignment operations.

void test02()

{

      dog a;//Default

      dog b=dog(10);//with parameters, equivalent to dog b(10)

      dog c=dog(b);//Copy, equivalent to dog c(b)

}
Anonymous object

It is worth noting that if you use a statement similar to the following alone:

dog(10);

The compiler will treat dog(10) as “Anonymous object“.

Anonymous means without a name.

It only specifies values for members within a certain class, but this class does not have a specific name to refer to, so it is called an anonymous object.

If no object receives the anonymous object, the anonymous object is recycled immediately after the current line ends.

From the above examples, we can see that compared to the bracket method, the explicit method only has one more assignment operation.

Note:

Don’t use copy constructors to construct anonymous objects!
The compiler will automatically treat something like dog (a) as dog a, which will lead to problems such as redefinition and reduced code readability.

  • Implicit conversion method

The implicit conversion method eliminates some cumbersome representations. Examples are as follows:

void test03()
{
    dog a = 10;//equal to dog a=dog (10);
dog b = a;//equal to dog b=dog a;
dog c = b;//equal to dog c=dog b;
cout << "a is: " << a.age << endl << "b is: " << b.age << endl << "c is: " << c.age < <endl;
}

The output is:

a is: 10

b is: 10

c is: 10

It is worth mentioning that in the line of code dog a=10;, it is equivalent to us constructing an anonymous object dog(10), but because there is a known object a to receive it, the behavior here is considered reasonable. .

  • Characteristics of the three methods

Generally speaking, we use the bracket method more because it is simple, easy to understand and easy to operate.

Of course, all three methods are reasonable and available, and you can choose according to your personal preference.

However, when you’re working with other programmers, it’s best to choose based on your actual needs.

(Don’t embarrass your colleagues qaq…especially avoid using copy functions to construct anonymous objects)

You can copy the above test01, test02, and test03 functions into the main function to test the running effect. I will not go into details here.

4. Constructor calling rules

When you create a class, the compiler will automatically construct a default constructor, copy constructor, and destructor for you, but they are all empty implementations (that is, the function body is empty and does not perform any operation).

When you set one of these three functions yourself, the compiler will call the function you constructed instead of the function implemented by you.

What’s interesting is that when you set a parameterized constructor but not a default constructor, the compiler will no longer call the default constructor, but will only call the parameterized constructor you have built. Constructor.

If this happens, some of your actions will be limited.

For example:

class person
{
public:
person()
{
cout << "Default constructor called!" << endl;
}
    /*Without this paragraph, an error will be reported when you use statements like dog a; elsewhere.
     Because after there is a parameterized constructor, the compiler will not construct a default function, but it will still provide a copy constructor. */

~person()
{
cout << "Destructor call!" << endl;
}
person(int age)
{
m_age = age;
cout << "Call of parameterized constructor!" << endl;
}
person(const person & p)
{
m_age = p.m_age;
cout << "Call of copy constructor!" << endl;
}/*If you remove this section, the system will automatically write these codes for you (just without Cout)*/

int m_age;
};

Therefore, if you are going to use a constructor, it is best to construct all three functions (or just provide a default constructor).

Perhaps this table can help you better understand the relationship between the three:

Set the above three functions to A, B, and C, which represent default construction, parameterized construction, and copy construction respectively.
Then there are:

Do we provide it? Compiler provided?
A
B
C
Do we provide it? Compiler provided?
A X
B√X
C
Do we provide it? Compiler provided?
A
B
C√X

Let’s look at a specific example:

#include<iostream>
using namespace std;

class person
{
public:
    int m_age;
person()
{
cout << "Default constructor called!" << endl;
}
~person()
{
cout << "Destructor call!" << endl;
}
person(int age)
{
m_age = age;
cout << "Call of parameterized constructor!" << endl;
}
person(const person & p)
{
m_age = p.m_age;
cout << "Call of copy constructor!" << endl;
}

};

void test01()
{
person p;
p.m_age = 18;
person p1(10);
person p2(p1);
cout << "p's age:" << p.m_age << endl;
cout << "p1's age:" << p1.m_age << endl;
cout << "p2's age:" << p2.m_age << endl;
}

int main()
{
test01();
system("pause");
return 0;
}

The running result is:

Default constructor call!
Constructor call with parameters!
p’s age: 18
p1’s age: 10
p2’s age: 10
Destructor call!
Destructor call!
Destructor call!

5. Timing of calling copy constructor

Copy functions generally have three uses:

  1. Initialize a new object using an existing object
  2. Pass values to function parameters using value passing method
  3. Value method returns local object

Let’s look at these three uses in turn.

  • Use an existing object to initialize a new object

This usage is relatively common and has been used many times above, so I only provide sample code here:

void test01()
{
person p1(10);
person p2(p1);
cout << "The age of p2 is: " << p2.m_age << endl;
}

In this code, we first use the parameterized constructor to construct the p1 class, and set the m_age in it to 10, and then use the copy constructor to copy p1 to p2, so that the m_age in p2 is also set. is 10.

  • Pass values to function parameters using value passing

In addition to being used as an initialization object, the copy function can also pass values to a function through value transfer.

For example:

void test_(person p)
{
}

void test02()
{
person p3;//Default constructor call
test_(p3);//Copy function call
}

In this code, the default constructor (called when person p3; is called) is first called, and then p3 is passed through the test_ function.

The value is passed to the formal parameter p.

After the operation is completed, p in the test_ function is first released and the destructor is called once; then the test02 function is completed, p3 is released, and the destructor is called again.

  • Returns local object by value

Similar to the above function, in a function, we can also use the copy constructor to return the value of the local object.

person dowork1()
{
person p(10);
return person(p);//In x86, the brackets can be omitted here
}

void test03()
{
person p = dowork1();
cout << "The value of p: " <<p.m_age<< endl;
}

(If your compiler is the x86 version, the brackets in person(p) can be omitted)

The running result is:

A parameterized function is called!
The copy constructor was called!
The destructor was called!
Value of p: 10
The destructor was called!

Here we can also more intuitively see the order in which the two functions are released, which is related to the last-in-first-out function of the stack (you can move to another article I will write soon: the functions of the four memory areas).

6. Deep copy and shallow copy

Deep copy: Create space in memory and then copy.

Shallow copy: direct copy.

This part is important, so I hope readers will read it carefully.

  • Shallow copy

The shallow copy part is relatively simple. If there is no special declaration, the copy constructor we construct is a shallow copy.

(Including the copy constructor set by the compiler for us)

  • Deep copy

Deep copy achieves the purpose of copying by opening up a space in the memory.

According to our content above, in most compilers, if we do not set a copy constructor, the compiler will automatically write a copy constructor for us.

However, this copy constructor is a shallow copy and has some defects.

Let’s look at an example:

#include<iostream>
using namespace std;

//Shallow copy: copy directly
//Deep copy: open up space and then copy
class person {
public:
int m_age;
int* m_height;
person()
{
cout << "Default constructor called!" << endl;
}

person(const person & p)
{
m_age = p.m_age;
//m_height = p.m_height; written by the compiler, will cause duplication
//Points to the same memory and is released twice! Illegal operation
m_height = new int(*p.m_height);//Create space for copy operation
cout << "Copy constructor called!" << endl;
}

person(int age, int height)
{
m_age = age;
m_height = new int(height);//Create space to prepare for setting the initial value of the member
cout << "Call of parameterized constructor!" << endl;
}

~person()
{
if (m_height != nullptr)
{
delete m_height;
m_height = nullptr;
}//If m_height is released, release the space it occupies.
cout << "Destructor call!" << endl;
}
};

void test01()
{
person p1(18, 160);//Set initial value
cout << "p1 age:" << p1.m_age << "Height is:" << *p1.m_height << endl;
person p2(p1);//Call of deep copy
cout << "p2 age:" << p2.m_age << "Height is:" << *p2.m_height << endl;
}

int main()
{
test01();
system("pause");
return 0;
}

In the above program, we first set the members m_age and m_height, where m_age is of type int and m_height is of type int*.

Then, we built a copy constructor and opened up a space for copying the pointer type m_height.

Next, we created a parameterized constructor to set member values, and in this function, we also opened up a space to perform the copy operation.

Next, we use the destructor to release the memory occupied by the copied member during the copy process.

Finally, the test01 function is called and the entire program is completed.

Let’s take a look at the running results:

Constructor call with parameters!
p1 age: 18 height: 160
The copy constructor was called!
p2 Age: 18 Height: 160
Destructor call!
Destructor call!

In the above case, we can easily find:

We constructed the member m_height as a pointer.

After constructing the pointer member m_height, we can use deep copy to copy the member.

person(const person & amp; p)
{
m_age = p.m_age;
//m_height = p.m_height; written by the compiler, will cause duplication
//Points to the same memory and is released twice! Illegal operation
m_height = new int(*p.m_height);
cout << "Copy constructor called!" << endl;
}

But it is worth noting that if we declare nothing, we write like this:

person(const person & amp; p)
{
cout << "Copy constructor called!" << endl;
}

Or let the compiler write it for us. In both cases, the compiler will report an error.

The reason is that the constructor called at this time is a shallow copy. It will completely copy the members of the previous class to the other class. Doing so will cause great security risks, that is, the m_height in the two different classes will be different. point to the same memory.

When we use the destructor to release the memory later, there will be illegal operations of releasing the same memory multiple times.

~person()
{
if (m_height != nullptr)
{
delete m_height;
m_height = nullptr;
}
cout << "Destructor call!" << endl;
}

Therefore, we must declare in the copy constructor:

m_height = new int(*p.m_height);

This allows the copy constructor to open up new memory when it is called, so that the data in the two memories are the same but the addresses are different, thereby avoiding the subsequent destructor function to release the same space multiple times.

7. Inline functions

Inline functions are typically used with classes.

If you are careful enough, after you create a member function and move the mouse cursor over the name of the function, the compiler will prompt you with the following:

inline type class_name::func_name()

Among them, type is the function type, class_name and fuc_name are the class name and function name respectively.

The inline here is exactly the declaration of the inline function.

What are inline functions?

To put it simply, if a function is inline, the compiler will copy the code of the function to every place where it is used.

Here is a simple example using inline functions:

#include<iostream>
using namespace std;

inline int max(int x, int y)
{
return x > y ? x : y;
}

int main()
{
cout << max(20, 10) << endl;
cout << max(70, 10) << endl;
cout << max(0, 10) << endl;
return 0;
}

The output is:

20

70

10

The meaning of inline functions

Inline functions essentially trade code space efficiency for time efficiency.

Since the compiler will copy the code to every place where the inline function is used, the call of the inline function will be different from the call of the ordinary function. It will be called directly where the function is used, saving running time.

Therefore, inline functions are generally functions with a small number of lines, and loop statements and switch statements are generally not used in functions.

If the inline function is too large, it will cause a double waste of space and time, which is meaningless;

The use of loops and switch statements in inline functions will produce unexpected effects. Sometimes the statements will run normally, and the loops and switches will not be triggered. I will not explain it in detail here, and readers can try it by themselves.

Also, the definition of an inline function must appear before it is called.

Also, the member functions in the class are all inline functions.

8. Use of this pointer

First of all, make it clear that this is a pointer.

It can refer to objects in the current class for purposes of differentiation.

For example:

//Example: Compare attribute values of two dogs (attribute value: age*height)
class dog
{
int age;
double height;
public:
dog(int a=1,double h=1.0)
{
cout << "Constructor call!" << endl;
age = a;
height = h;
}//Use the constructor to initialize age and height
int cal_dog()
{
return age*height;
}
void setage(int a)
{
this->age = a;//Equivalent to age=a;
}

void setheight(double h)
{
this->height = h;//This is equivalent to height=h;
}

bool compare(dog dog1)
{
return this->cal_dog() > dog1.cal_dog();//If the former is greater than the latter, return true, which is equivalent to a judgment
}
};

int main()
{
dog dog1;
dog dog2;
dog1.setage(10);
dog1.setheight(23.0);
dog2.setage(10);
dog2.setheight(30.0);
cout << "The attribute value of dog1 is: " << dog1.cal_dog() << endl;
cout << "The attribute value of dog2 is: " << dog2.cal_dog() << endl;

if (dog1.compare(dog2))
{
cout << "The attribute value of dog1 is greater than the attribute value of dog2!" << endl;
}

if (!(dog1.compare(dog2)))
{
cout << "The attribute value of dog1 is less than or equal to the attribute value of dog2!" << endl;
}
\t
return 0;
}

//The type of this pointer can be understood as Box*. 

You can also use this pointer to refer to the entire object, for example:

void krdog(dog & amp;d)
{
    *this=d;
}

This usage can be used for operations such as comparing members between two classes.

9. Pointer to class

We can operate on a class by creating a pointer to the class.

#include<iostream>
using namespace std;

class Box {
double length;
double height;
double width;
public:
Box(double l=1.0,double h=1.0,double w=1.0)
{
cout << "Call constructor!" << endl;
length = l;
height = h;
width = w;
}
double calvol()
{
return length * width * height;
}
};

int main()
{
Box box1(1.3, 2.4, 7.8);//With the constructor, there is no need to use the setheight function
cout << "The volume of box1 is:" << box1.calvol() << endl;
Box* prebox=nullptr;//Create a box-type pointer
prebox = & amp;box1;//Let prebox point to box1
cout << "The volume of prebox is: " << prebox->calvol() << endl;//prebox->calvol is equivalent to box1->calvol
system("pause");
return 0;
}

10. Static members

In a class, we can create a static member (variable and function). For all objects, there is only one static member.

For example:

#include<iostream>
using namespace std;

//Static member variables: When we declare a member of a class as static, it means that no matter how many objects of the class are created, there is only one copy of the static member.
//Static members are shared among all objects of the class.
//If there are no other initialization statements, all static data will be initialized to zero when the first object is created.
//We cannot place the initialization of static members in the definition of the class,
//However, static variables can be initialized by redeclaring them outside the class by using the scope resolution operator::
//For example:
class dog {
int age;//instance variable
double height;//instance variable
public:
static int count;//Static variable count is used to count the number of dogs
dog(int a=1,double h=1.0)
{
cout << "Constructor call!" << endl;
age = a;
height = h;
count + + ;//Every time the constructor is called, count + +, the counter increases by one
}
double calvol()
{
return age * height;
}
static int getcount()
{
return count;
}
};


int dog::count = 0;//Define static member variables outside the class (must be done, otherwise the compiler will report an error) -> allocate memory for it

int main()
{
dog dog1(1, 2.0);
dog dog2(2, 3.0);
cout << "dog1 attribute value:" << dog1.calvol() << endl;
cout << "dog2 attribute value:" << dog2.calvol() << endl;
cout << "Equivalent to the following! count is: " << dog::count << endl;
cout << "A total of" << dog::getcount() << "Only a dog!" << endl;//It must be used like this, because inside dog, there is only one count
return 0;
}

//When calling static member variables and static member functions, you can declare its scope dog::, because for the dog class, regardless of the situation within it,
//There is only one count, whether it is dog1.count or dog2.count.
//But it is recommended to use dog::count, which can better highlight the characteristics of static member variables. 

At this point, we have finished learning the general concepts of constructors, destructors and classes. Next, we will enter the study of inheritance and polymorphism.

I believe you are ready. Starting from the following content, we will further get into contact with the core concepts of C++. I hope you will gain something after reading the article.

(Creation is not easy, thank you for your support!)