C++: Default member function of class ——copy constructor && assignment operator overloading

Table of Contents

I. Introduction

2. Copy constructor

Concept of copy constructor

Copy constructor characteristics

Explanation feature 2: The copy constructor has only one parameter and must be passed by reference. Using the pass-by-value method will cause infinite recursive calls.

Explanation feature 3: If no definition is displayed, the system generates a default copy constructor. The default copy constructor function copies the object in byte order according to the memory storage. This type of copy is called shallow copy, or value copy.

Three forms of copy construction

Summary of copy construction

3. Assignment operator overloading

Operator overloading

Assignment operator overloading

4. Encouragement


1. Foreword

In the class we studied earlier, we will define member variables and member functions. These functions we define ourselves are ordinary member functions. , but what if there is nothing in the class we define? Is it true that there is nothing inside? as follows:

class Date {};

If there are no members in a class, it is called empty class for short. Is there nothing in the empty class? No, any class will automatically generate 6 default member functions if we don’t write them.

[Concept of default member function]: If the user does not implement it explicitly, the member function generated by the compiler is called the default member function.

?The last blog has explained the use of constructors & destructors in detail, so this blog will continue to explain the overloading of copy construction and assignment operators in depth. ?

2. Copy Constructor

Copy constructor concept

When we create an object,can we create a new object that is exactly the same as another object?

int main()
{
Date d1(2022, 5, 18);
    //Copy the data of d1 to d2 and let d2 initialize it
Date d2(d1);
return 0;
}

Can the value of d2 be the same as d1? That is to say, I use d1 to initialize d2, and the function called at this time is the copy constructor.
Copy constructor: There is only a single formal parameter, which is a reference to an object of this class type (generally commonly used const modification), and is determined by the compiler when creating a new object with an existing class type object. Automatic call

Copy constructor characteristics

The copy constructor is also a special member function with the following characteristics:

  1. The copy constructor is an overloaded form of the constructor.
  2. The copy constructor has only one parameter and must be passed by reference. Using the pass-by-value method will cause infinite recursive calls.
  3. If no definition is shown, the system generates a default copy constructor. The default copy constructor object copies the object in byte order according to memory storage. This type of copy is called shallow copy, or value copy.

The following is the copy constructor:

Date(Date & d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}

Explanation feature 2: The copy constructor has only one parameter One parameter must be passed by reference. Passing by value will cause infinite recursive calls

Why does passing parameters by value cause infinite recursion?
Let’s first take an ordinary func function as an example:

//Pass parameters by value
voidFunc(Date d)
{}
int main()
{
Date d1(2022, 5, 18);
Func(d1);
return 0;
}

The parameters passed in this function call are passed by value. In C language, passing the actual parameter to the formal parameter means copying the value of the actual parameter to the formal parameter. However, my actual parameter d1 is of a custom type and needs to call the copy constructor. Passing parameters by value requires calling copy construction, but what if I don’t want to call copy construction? You need to pass parameters by reference, because at this time d is the alias of d1

void Func(Date & amp; d) {}

Now let’s go back to our example: If I don’t pass a reference or parameter, the copy constructor will be called crazily:

Date(Date d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}

In order to avoid infinite recursive calls to the copy constructor, a reference must be added. After adding the reference, d is an alias of d1, and there is no copy constructor. Pass-by-value parameters of the same type require copy construction

Date(const Date & d) {}
//It is best to add const to protect d

Explanation feature 3: If no definition is displayed, the system generates a default copy constructor. The default copy constructor function copies the object in byte order according to memory storage. This type of copy is called shallow copy, or value copy

Look at the following code:

class Date
{
public:
//Constructor
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//copy constructor
    /*
Date(const Date & d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
    */
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
voidFunc(Date & d)
{
d.Print();
}
int main()
{
Date d1(2023, 10, 23);
Date d2(d1);
Func(d1);
d2.Print();
}

Operation effect diagram display:

Why didn’t I write a copy constructor here? Will it automatically complete the copy construction? From this we have to think deeply, copy construction is different from construction and destruction. Construction and destruction are only processed for custom types but not for built-in types. The default copy construction is for members of built-in types. Value copying will be completed, shallow copy,that is, copying the built-in type members of d1 to d2 by bytes.

From this we know that we do not need to write copy constructors for members of built-in types such as the date class. Does that mean that all classes do not require us to write copy constructors? Consider the following example:

Stack class (Class that requires deep copy)

class Stack
{
public:
\t//Constructor
Stack(int capacity = 10)
{
_a = (int*)malloc(sizeof(int) * capacity);
assert(_a);
_top = 0;
_capacity = capacity;
}
//Do not write copy construction, the compiler calls the default copy construction
/*
Stack(const Stack & st)
{
_a = st._a;
_top = st._top;
_capacity = st._capacity;
}
*/
//destructor
~Stack()
{
cout << "~Stack():" <<this << endl;
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
int* _a;
int _top;
int _capacity;
};
int main()
{
Stack st1(10);
Stack st2(st1);
}

An error occurred as a result of the operation:

We saw through debugging that the run crashed. It can be seen that the copy constructor of the stack cannot be written like the date class and the compiler can call the default copy constructor (even if the copy constructor is written according to the date class pattern, an error will occur). Because at this time st1 (pointer) and st2 (pointer) point to the same space, it can be seen through debugging:

St1 and st2 pointing to the same space will cause a huge problem: There is an error in the destructor, because my st2 will be destructed first, and after the destruction, my st1 will be destructed, but my st1 points to The space has been destructed by st2, because they both point to the same space. There will be problems if I release the same space twice. There are problems with destruction, and there are also problems with additions, deletions, checks and modifications, which will be discussed later.
In fact, the copy structure of the stack I just wrote is shallow copy. The copy structure of the real stack should be completed using deep copy. I will write a blog post about this part later. For a detailed explanation, let’s have a brief introduction here.
In summary, we can know that shallow copy is no problem for date classes, but if the members of the class point to a space, shallow copy cannot be used.

String class (Class that requires deep copy)

class MyString {
public:
//Default constructor
MyString(const char* str = "winter")
{
_str = (char*)malloc(sizeof(char)*(strlen(str) + 1));
if (_str == nullptr)
{
perror("malloc fail!");
exit(-1);
}
strcpy(_str, str);
}

// destructor
~MyString()
{
cout << "~String()" << endl;
free(_str);
}

void MyPrintf()
{
cout << _str << endl;
//printf("%s\
", _str);
}

private:
char* _str;
};

int main()
{
MyString s1("hello C + + ");
MyString s2(s1);
s1.MyPrintf();
cout << endl;
s2.MyPrintf();
cout << endl;
}

As shown in the picture: pointing to the same space

So what problems will arise? This will cause the space pointed to by _str to be released twice, causing the program to crash.

Add deep copy constructor:

class MyString {
public:
//Default constructor
MyString(const char* str = "winter")
{
_str = (char*)malloc(sizeof(char)*(strlen(str) + 1));
if (_str == nullptr)
{
perror("malloc fail!");
exit(-1);
}
strcpy(_str, str);
}

// destructor
~MyString()
{
cout << "~String()" << endl;
free(_str);
}

//copy constructor
MyString(const MyString & s)
{

// Apply for a space of the same size as the original object for the new object
_str = (char*)malloc(sizeof(char) * (strlen(s._str) + 1));
if (_str == nullptr)
{
perror("malloc fail!");
exit(-1);
}

//Copy the data of the original object to the new object one by one
strcpy(_str, s._str);
}
void MyPrintf()
{
cout << _str << endl;
//printf("%s\
", _str);
}

private:
char* _str;
};

int main()
{
MyString s1("hello C + + ");
MyString s2(s1);
s1.MyPrintf();
cout << endl;
s2.MyPrintf();
cout << endl;
}

Summary:

1: You can observe whether there is an explicit destructor in the current class. If it exists, it means that the current class involves resource management [resource management refers to Just apply for space in the heap]At this time, you must implement the copy structure yourself to achieve a deep copy; If resource management is not involved, directly use the compiler to automatically generate Just make a shallow copy

2: Like the Date class, there are only built-in types such as [Year], [Month], and [Day]Shallow copyThat’s it; for more complex ones, such as: Linked lists, binary trees, hash tables, etc., all involve resource management, so deep copies must be considered

Three forms of copy construction

After a deep understanding of copy construction, let’s take a look at the three forms of copy construction.

1. When using an object of a class to initialize another object of the same class

Date d1;
Date d2(d1);
Date d3 = d2; //Copy construction will also be called

When instantiating objects d2 and d3, the copy constructor is called. In the end, the results after initialization are the same.

2. When the formal parameter of the function is an object of the class and the function is called to combine the formal parameters and actual parameters

void func(Date d) //The formal parameter is an object of the class
{
d.Print();
}

int main(void)
{
Date d1;
func(d1); //Passing parameters triggers copy construction
\t
return 0;
}

The formal parameter of the function func() is an object of the class. At this time, when the function is called from the outside and the corresponding parameters are passed in, copy construction will be triggered.

3. When the return value of the function is an object and the function execution is completed and returns to the caller

Date func2()
{
Date d(2023, 3, 24);
return d;
}

int main(void)
{
Date d1 = func2();
d1.Print();

return 0;
}

It can be seen that this method will also trigger copy construction. When the function returns an object of Date class internally, and the outside world uses a Date type object to receive it, copy construction will be triggered.

Summary of copy construction

Summary:

1. The copy constructor is the more difficult to understand among the six default member functions. The main thing is to clarify whether [built-in types] and [custom types] will call the copy construction mechanism. There are two main points when implementing this copy construct:One is to receive references in the formal parameter part, otherwise it will cause infinite recursion; the other is to modify it with const in front, which can prevent Issues of misuse and permission amplification
2. For general classes, it is enough to generate a copy structure by yourself. Only classes like Stack that directly manage resources by themselves need to implement deep copies by themselves.

3. Assignment operator overloading

Operator overloading

The following date classes:

class Date
{
public:
\t//Constructor
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};

Can I compare the size of date class objects in the following way?

Obviously this is not possible, as can be seen from the warning prompted by the wavy line. We all know that built-in types can be compared directly, but custom types cannot be compared directly through the above operators.In order to allow custom types to use various operators, operator overloading was proposed. rule.

C++ introduces operator overloading in order to enhance the readability of the code. Operator overloading is a function with a special function name. It also has its return value type, function name and parameter list. Its return value type and parameter list are similar to ordinary functions. .

  • The function name is: the keyword operator is followed by the operator symbol that needs to be overloaded.
  • Function parameters: operator operands
  • Function return value: the result after the operator operation
  • Function prototype: return value type operator (parameter list)
  • Notice:
  • The parameters of the operator overloaded function are determined by the operator. For example, the operator == should have two parameters, the double-operand operator has two parameters, and the single-operand operator (+ + or –) has one parameter.

For example, I am now writing an operator overload for date comparison equality (Passing parameters by value will trigger copy construction, so you need to add a reference, preferably const, to improve efficiency):

bool operator==(const Date & amp; d1, const Date & amp; d2) //Avoid calling copy constructor by passing parameters by value
{
return d1._year == d2._year & amp; & amp;
d1._month == d2._month & amp; & amp;
d1._day == d2._day;
}

Observe my screenshot carefully: After all, I have written the operator overloading, but how can I call the operator overloading in the same way as calling a normal function? What’s the point of naming operator overloading instead, so the real call should be as follows:

When calling, perform operator operations directly with built-in types, and the compiler will automatically process it to call operator overloading.

  • Note: Is the above operator overloading complete? Of course not, it stands to reason that we should put the operator overloaded function as a member function in the class.

Moreover, the parameters here cannot be written as above:

If you directly put the operator overloaded function into the class, the compiler will report an error (the operator function has too many parameters). The reason for the error is that the member function has an implicit this pointer. This also means that there are 3 actual parameters, so we need to write one less parameter:

bool operator==(const Date & amp; d)//The compiler will process it as bool operator(Date* const this, const Date & amp; d)
{
return _year == d._year & amp; & amp;
_month == d._month & amp; & amp;
_day == d._day;
}

And I also need to make changes when calling member functions:

if (d1.operator==(d2))
{
cout << "==" << endl;
}

As before, in order to highlight the significance of operator overloading, we can directly operate operators like built-in types when calling, because the compiler will handle it for us:

if (d1 == d2)//The compiler will process the corresponding overloaded operator call if (d1.operator==(d2)) or if (d1.operator==( & amp;d1, d2) )
{
cout << "==" << endl;
}

  • Now, let’s write a date class comparison to practice:
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//Date comparison size
bool operator<(const Date & d)
{
if (_year > d._year ||
_year == d._year & amp; & amp; _month > d._month ||
_year == d._year & amp; & amp; _month == d._month & amp; & amp; _day > d._day)
return false;
else
return true;
}
void Printf()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023, 10, 23);
cout << "The date of d1 is: " << endl;
d1.Printf();
Date d2(2022, 10, 22);
cout << "The date of d2 is: " << endl;
d2.Printf();
cout << endl;
cout << "Comparison of the dates between d1 and d2:";
if (d1 < d2)
{
cout << "<" << endl;
}
else
{
cout << ">" << endl;
}
Date d3(d1);
d3 = d2;
return 0;
}

Next, let’s summarize the points to note about operator overloading:

  1. New operators cannot be created by concatenating other symbols: such as operator@
  2. The overloaded operator must have a class type (operator overloading is only available for custom type members) or an enumeration type operand
  3. The meaning of operators used for built-in types cannot be changed. For example: the built-in integer type + cannot change its meaning
  4. When used as an overloaded function of a class member, its formal parameters appear to be 1 less than the number of operands. The operator of the member function has a default formal parameter this, which is limited to the first formal parameter
  5. .* , :: , sizeof , ?: ,. Note that the above five operators cannot be overloaded. This often appears in multiple-choice questions in written exams.

Assignment operator overloading

We have learned about copy construction before, which is to use an object of the same type to initialize another object. What if I don’t want to use copy construction?

int main()
{
Date d1(2022, 5, 17);
Date d2(2022, 5, 20);
Date d3(d1); //Copy construction -- an existing object initializes another object to be created
d2 = d1; //Assignment overloading/copying -- assignment between two existing objects
}

Can I directly take d1 and assign it to d2? This is what we are talking about assignment operator overloading. Assignment operator overloading is somewhat similar to the operator overloading above. With the foundation of operator overloading, it is very simple to write an assignment overloading.

//d2 = d1; -> d2.operator=( & amp;d2, d1);
void operator=(const Date & d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}

However, there are certain problems with the assignment overloading here. The assignment in our C language supports consecutive assignments, as follows:

int i = 0, j, k;
k = j = i;

We assign i to j, then use j as the return value and assign it to k. You must know that C++ is based on C. Does the assignment overloading we just wrote support consecutive waits?


Obviously it is not supported. The reason is that when I assign d1 to d2, there is no return value to assign to d3, which causes an error. The correction is as follows:

In addition: the assignment overloading here can be further improved:

  • Improvement 1: The assignment overload we just wrote is return by value. Return by value will generate a copy and call the copy constructor. If it goes out of scope and wants its object to still exist, we can return it by reference:
  • Improvement 2: There may be a situation like this: d1=d1. It is not necessary to call the assignment overloaded function when assigning a value to yourself like this, so we can also add an if condition. judge.

The correction is as follows:

 //d2 = d1; -> d2.operator=( & amp;d2, d1);
Date & operator=(const Date & d)
{
if (this != & amp;d) //It is not recommended to write if (*this != d), what if != is not overloaded? , because here is a comparison of objects
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
  • Notice:

Operator assignment is also the default member function. If we don’t write assignment overloading, the compiler will generate it by default. However, the compiler still completes value copy or shallow copy. For example, this date class does not need to write assignment overloading.

Assignment overloading is the same as copy construction. If we don’t write it, it will complete the value copy of the built-in type. But we have to write it like the stack, because we have to write a deep copy assignment overloading. The reason is the same as the copy construction. similar. The specific implementation will come later when we really talk about deep copy.

  • Additional:
void TestDate2()
{
Date d1(2022, 5, 18);
Date d3 = d1; //Equivalent to Date d3(d1);
}

Date d3 = d1 is copy construction, not assignment.
Taking one object and initializing another object is copy construction. The following d2 =d1 is the assignment:

void TestDate2()
{
Date d1(2022, 5, 18);
    Date d2(2022, 5, 20);
Date d3 = d1; //Equivalent to Date d3(d1); is a copy construct
    d2 = d1; //Two existing objects are the assignment
}

4. Encourage each other

The following is my understanding of the default member functions of C++ classes—-copy constructor & assignment operation overloading. If there are friends who don’t understand and find problems, Please say it in the comment area. At the same time, I will continue to update my understanding of the default member function of the C++ class——-const member & & & const address operator overloading. Please continue to follow me. oh! ! !

The knowledge points of the article match the official knowledge files, and you can further learn related knowledge. Algorithm skill tree Home page Overview 56907 people are learning the system