C++ classes and objects 2: default member functions

 We can see through the this pointer that C++ actually hides a lot of things, and it will take care of many things during compilation. As the most important class and object, does it also hide more things that we usually don’t see? What about things?

We create an empty class and put nothing in it.

class Text{};

 It looks like there is nothing, but in fact there are default member functions in it, not only there, but also a full 6

Table of contents

Default member function:

 1. Constructor:

 Default Generated Constructor Traits

Default constructor:

2. Destructor:

The application method of the destructor:

3. Copy constructor:

4. Assignment operator overloading

Operator overloading:

Overloading of assignment operator:

 Self-implemented assignment overloading:

The assignment overload generated by the compiler by default:

Stream insertion and stream extraction operator overloading

 Talking about Friends:

const member

5 and 6. Address and const address operator overloading


Default member function:

 1. Constructor:

The function is initialization, although its name is construction, but it is only initialized directly after the object is created . It is a very strange function, very special. The constructor should be named the same as the current class name .

First of all, when we don't write the constructor, the compiler will directly write a default constructor for it.

Constructors can be overloaded , which means that multiple constructors and multiple initialization schemes can be provided

To sum it up briefly:

Constructor features:

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.


 Taking the date class as an example, we can overload the constructor in combination with the default value.

class Date
{
public:

	Date(int year=2022, int month=12, int day=29)
	{
		_year = year ;
		_month= month ;
		_day = day   ;
	}

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day  << endl;
	}

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


 Default Generated Constructor Traits

 If no constructor is explicitly defined in the class, the C++ compiler will automatically generate a default constructor without parameters . Once
the user explicitly defines the compiler will no longer generate

So what about the default constructor generated by the compiler?

Let's comment out the overloaded constructor and print it directly

 Findings are random values.

Then the question arises, if the compiler generates a default constructor itself, and this constructor can only give a random value, isn't it useless at all? But in fact, the constructor is very "double-standard", as for what it looks like as follows.

Double standard constructor:

Constructors do nothing for built-in types . It only initializes non-built-in types, and non-built-in types exclude the types that come with the language such as int char, so when the created object is a custom type such as the current Date or structure , its default member function will be called for the members of the custom type , otherwise, the constructor will ignore the non-built-in types at all, and the non-built-in types will store random values. For example, the member variables inside our date class are all random values.

When we call the default constructor with a custom type, the effect is as follows:


class Text
{
public:
	Text()
	{
		cout << "这个自定义类型的构造函数已被调用" <<  endl;
	}

private:
	int text;

};


class Date
{
public:

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day  << endl;
	}

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

We can see that when creating an object with the Date class, the Tx inside is directly initialized, that is, an object of this type is directly created, and the basis for creation is that the constructor inside Class A is triggered.

C11 patch added for this feature

Obviously this kind of double-standard pain is not a little bit, so a patch was added in the C11 version, allowing us to give a default value when declaring the built-in type, which looks a lot like initialization here. , but in fact his operation logic is the logic of the default value, that is, the default value when no parameter is passed

 Although it is not very convenient, it can be regarded as a remedy, at least it will not put random values ​​in it


Default constructor:

We will have a preconceived concept misunderstanding, that is, we think that only when we do not write it is called the default constructor by the compiler itself, but in fact the real concept of the default constructor is a no-argument constructor and a full default constructor Functions , that is, the following two functions are also considered default constructors.

 The summary is: the one that does not pass parameters is the default constructor

There is one and only one default constructor , and when we try to circumvent this rule, an error will be reported.

 


2. Destructor:

The constructor is to create the object, then the destruction of the object is the destructor

class Date
{
public


    //构造函数
    Date()
    {}

    //析构函数
    ~Date()
    {}


	void Print()
	{
		cout << _year << "-" << _month << "-" << _day  << endl;
	}

private:
	int _year;
	int _month;
	int _day;

};

Destructor: Contrary to the function of the constructor, the destructor does not complete the destruction of the object itself, and the local object destruction is
done by the compiler. When the object is destroyed, it will automatically call the destructor to complete the cleanup of resources in the object.

Characteristics of the destructor:

1. The destructor name is the character ~ before the class name
2. No parameters and no return value type.
3. A class can have only one destructor. If not explicitly defined, the system will automatically generate a default destructor. Note: The destructor cannot be overloaded
4. When the life cycle of the object ends, the C++ compilation system automatically calls the destructor.

The destructor faces a custom type, and its destructor is called.

The destruction order of the destructor: the one defined later is destroyed first, which conforms to the nature of the stack

Example:

 Answer: BADC

Analysis : The destruction order of the destructor is the later destruction first, then we first observe the creation order of this question: CABD

However, various modifiers will affect the declaration cycle of the variable and the creation scope of the variable. C is a global variable, which is the variable created at the beginning of the program, and it will be destroyed at the end. And D is that Static changes the survival scope of local variables, so static variables will be destructed after local variables are destructed, so the answer is BADC.


The application method of the destructor:

The destructor will call the constructor of the custom type, and the constructor itself is to clean up and release the space, so we only need to follow the following method when using it

If there is no resource application in the class, the destructor can not be written, and the default destructor generated by the compiler is used directly, such as the Date class; when there is a resource application, it must be written, otherwise it will cause resource leakage, such as the Stack class


3. Copy constructor:

When we intend to create a new object again in the form of d1, we will use the copy constructor at this time.

So in C++, when such a requirement arises, the copy constructor will be used.

Copy constructor : There is only a single formal parameter, which is a reference to the object of this class type (usually const decoration is commonly used), and is automatically
called by the compiler when creating a new object with an existing class type object.

1. The copy constructor is also a special member function, which is an overloaded form of the constructor

2. The copy constructor has and can only have one parameter, and its parameter type is a reference to an object of this type. Calling by value will directly report an error because infinite recursion will occur.

// Date(const Date& d) // 正确写法
Date(const Date& d) // 错误写法:编译报错,会引发无穷递归
{
    _year = d._year;
    _month = d._month;
    _day = d._day;
}

Why does infinite recursion happen? What exactly is call by value doing wrong?

Let's first look at when the copy constructor will be triggered:

Date d1(2022,9,26)

Date d2(d1);

 What will happen if our copy constructor is wrongly written as follows?

Date(const Date d)

The first is the most basic feature of call-by-value, that is, the generation of formal parameters. We review here again that formal parameters are copies of actual parameters . At this time, the copy generated by the pass-by-value call is equivalent to triggering the condition of the copy constructor again, that is, creating a new object of the same type among the objects created by the same class.

 Summary: When the copy constructor is called by value, the copy constructor will be triggered again and again because of the copy of the formal parameters, and the same type of reference should be used when it is used correctly.

When explicitly declaring the copy constructor, const should be added to the parameter to prevent the error of reverse writing, because the reference is an alias of the variable, if it occurs, it will be changed to the original object being copied, const can effectively solve this problem question.


So how exactly is the copy constructor copied?

Similarly, since it is an overload of the constructor, its essence is also double-standard. When we use the copy constructor generated by the compiler by default, its copy situation is as follows:

For built-in variables, it will copy the built-in variables inside the original object to the target variable byte by byte, but this copy is essentially a shallow copy.

shallow copy:

Shallow copy is more like acquiring and overwriting, much like memcpy, let’s review the basic implementation of memcpy: memcopy uses char* to copy one by one in order to implement overwriting and copying suitable for memory, which can be effectively guaranteed The correctness of copying can also cause problems when facing pointers.

When we want to use the copy constructor to realize the copy between two objects, if there is a pointer inside the object of this type, the shallow copy principle of the copy constructor will only copy the original pointer and give it to the new pointer, so that Sub-words are different from our basic need to open up additional space, and will crash when the destructor is called, because the compiler destructs the same space twice.

And here, in order to avoid a series of problems caused by shallow copy, we need to use deep copy, that is, we write our own copy constructor to open up space.

When the constructor copy function is dealing with a custom type, it will directly call its own copy constructor.

 Typical calling scenarios of the copy constructor:
 1. Create a new object using an existing object
     2. The function parameter type is a class type object
       3. The function return value type is a class type object


4. Assignment operator overloading

Assignment operator overloading is the same as the previous member function, and will generate a default one when it is not explicitly called, but before we understand it, let's sort out the concept of operator overloading


Operator overloading:

Addition, subtraction, multiplication, and division operators can only calculate variables conveniently. In C++, in order to play with custom variable types, the function of operator overloading is set, that is, to allow custom objects to use operators.

This function can realize some calculation effects of custom types, such as comparing size, adding, subtracting, multiplying and dividing, or taking our Date type as an example, realizing the difference in days between two Dates.

The function name is: the keyword operator followed by the operator symbol that needs to be overloaded.
Function prototype: return value type operator operator (parameter list)

 reload process

 For example, a simple operator that checks whether two classes are equal, let's overload it for fun:

Unlike writing functions, when we write formal parameters, we should not write call by value, otherwise we will call the copy constructor and work in vain, so we still need to use references, the same reason as the copy constructor, we should try to prevent The problem of making a mistake occurs, add a const to it.

Then, according to our logic of seeking equality, it should not be difficult to write

 But an error was reported here, and we realized that we were trying to access private variables.

 I'm forced to take a break, so what should I do?

Let's ignore this problem for now, let's take a look at the print format if our overloaded operator takes effect

 

This will report a bunch of errors, because the operation priority of the stream insertion operator is higher than ==.

Adding parentheses can effectively avoid error reporting problems caused by the operation order of streams.

 

Back to the question just now, how do we get private member variables?

Friends can be used here, but later on, we can open the corner and use functions to take them out, that is, create member functions directly inside the class, and return the value of the private member variable.

But this is relatively dull. In fact, a better way is to put our redirection function directly into the class.

But the problem appeared again, and the compilation still failed like this. Why?

 Why is this too many parameters? Isn't it reasonable that the operator I'm using requires two operands? The problem here is actually caused by the existence of this pointer. The member function will have This as a parameter by default , so there are actually three parameters here, so it is very simple, we can delete one directly.

Here, the compiler is still very smart. When the compiler detects that we use this operator and global variables, it will directly use this form:

Overloading of assignment operator:

 Self-implemented assignment overloading:

 The previous foreshadowing is over, so we officially enter our topic, the overloading of the assignment assignment operator.

Unlike the copy constructor, copying between two existing objects is assignment overloading.

int main ()
{
    Date d1(2022,12,31);
    Date d2(2023,1,1);

    d1 = d2;
}

Digression:

So here comes a tricky question, is this copy construction or assignment overloading?

Date d3 = d2;

But in fact, there is nothing tricky, this is still a copy structure, d4 does not even have an entity, how can it be called an assignment?

Then let's try to implement the overload of assignment

	//赋值操作符重载,为了防止触发拷贝构造,使用传引用
	void operator = (const Date& d1)
	{
		_year = d1._year;
		_month = d1._month;
		_day = d1._day;
	}

There is no problem with writing in this way, but it does not implement chained access. A normal assignment operator should implement the function of chained assignment, that is, i = j = 10

Then we write a version with a return value

	//实现链式访问版本
	Date operator = (const Date& d1)
	{
		_year = d1._year;
		_month = d1._month;
		_day = d1._day;
	}

But this is still not good enough, why?

Because some redundant copies have occurred, we should use the reference reasonably and set the return value as a reference. This can reduce the occurrence of copying. Note that in this process, the this pointer is destroyed, not *this, and the d1 pointed to by *this is still not destroyed.

	//优化版本
	Date& operator = (const Date& d1)
	{
		_year = d1._year;
		_month = d1._month;
		_day = d1._day;
	}

The assignment overload generated by the compiler by default:

Since assignment overloading is a member of the default member function, what is the default effect of the compiler when we don't write it?

The assignment overload function generated by the compiler by default has the same versatility as the copy constructor . It is enough for assigning date classes that do not need to write additional destructors, but the same is that its principle is the same as that of copy construction. The function is the same, what happens is a shallow copy. If we use the default assignment overload function to assign values ​​to the stack type, the problem of deconstructing the same space twice will also occur, so we need to write the assignment overload function ourselves for this type of type.

But we found that, in fact, the endian copy generated by the compiler itself has been realized. Do we still need to spend so much effort to overload one by ourselves?

Let's take the assignment overloading of the stack as an example, we use the assignment overloading function generated by the compiler by default

 Why did it crash?

reason:

So the solution is: directly free the assigned space, re-open a space with the same size as the source, use memcpy to copy the data, and then return a this pointer.

Taking Stack insertion and expansion as an example, it is necessary to avoid the occurrence of tricky angles, that is, the operation of assigning values ​​to ourselves , so we need to check

	Stack& operator =(const Stack& st1)
	{
		if (this != &st1)
		{
			free(_a);
			int* tmp = (int*)malloc(st1._capacity * sizeof(int));
			if (tmp == nullptr)
			{
				perror("malloc fail!");
				exit(-1);
			}
			_a = tmp;
			memcpy(_a, st1._a, st1._top);
			_top = st1._top;
			_capacity = st1._capacity;
		}
		return *this;
	}

 The this pointer will not be destroyed if it leaves the scope of this function, it still exists, so for the optimal solution, we can use the reference

Next is to write the self-incrementing overload, ++ and --

Then there is a headache here. Auto-increment is a single-operand single-operator. How does the compiler know whether it is pre-position or post-position?

So C++ gave a mark:

 But here, this int is only for marking, no need to pass parameters, and no need for self-increment.


Stream insertion and stream extraction operator overloading

> It supports built-in variable overloading in C++. In order to realize the flow insertion and extraction of classes, we can overload them. If you need to call Cout, C++ supports this type, which is ostream: we use this type to create a variable: ostream out

Correspondingly, this ostream is the output, and the input is the istream

When overloading is equivalent to rewriting the stream output, it is written as follows

 But after reloading, it will report an error

 

 If you change the wording, it won't

This is very painful, I did not reload you to let you do acrobatics, your appearance is not in line with the usage habits!

The reason is that the this pointer occupies the first operand by default

Therefore, when overloading these two operators, it is generally not written as a member function, because the Date object defaults to the left operand, which does not conform to the usage habits, and the first operand written in the class will definitely be the this pointer, so in general, it is not Write the stream insertion overload as a member function.

Then take it out and write:

 But an error was reported here, the reason is actually the repeated definition of the global function

 Here, the declaration and definition of our project are separated, so there will be an old problem, similar to the problem of multiple inclusion of header files:

 

Both files contain head.h once, resulting in the redefinition of this overloaded function. Different from our solution at the time, we used pragma once to prevent repeated calls, but pragma once can only prevent the file from being expanded twice, instead of not expanding

Of course, not only this overloaded function will report such an error here, but other global functions, including global variables, will also have this problem

 Solution:

1. Separation of declaration and definition

Declarations and definitions can also be resolved, because the definition and declaration will enter the symbol table.

2. Add a static

The reason why static can solve this problem also needs to be attributed to its basic definition: when static modifies global variables, it will affect the life cycle of variables. Of course, when modifying global functions and variables, their link attributes will also be modified, that is, modified to Visible only in the current file

In summary: don't define global variables inside .h

 After this problem is solved, another problem we face is how to access private variables. Friends can be used here.


 Talking about Friends:

Friends are equivalent to a green channel to access private variables.

Friends can be declared anywhere in the class to provide channels for functions on the whitelist, just add a friend in front of the required functions.

 But this actually breaks the original intention of our encapsulation. Generally, we only use friends when we have to use them.


const member

When we create an object of a class with const type, calling its member function will report an error

The reason is actually the power amplification

Let's review the format of the hidden this pointer: Date* const this

And the format of the parameter passing object: const Date d2

Pass in the member function, that is, the current object itself, that is, the address of d2 and its type is const Date*

Compare two types, one is to add const to the this pointer itself, and the other is to add const to the object pointed to by this pointer

 

 So what should we do now? Even const objects can't call their own member functions!

The solution is : C++ allows adding a const after the member function, allowing object types like d2 to use member functions normally. The const modification added here is not the this pointer itself, but the variable it points to, that is, it becomes: const Date* const this is easy to understand, and it is to lock the pointer and the variable it points to. Living

 But here is a question, how can the unpretentious object d1 continue to be called after the modification we made to d2?

The reason is the reduction of permissions. Although this formal parameter has been clearly locked by const, it does not prevent the initialization when passing parameters. And the only time a const object can be changed is when it is initialized .

For this reason, I also wrote a sample:

  The const-modified "member function" is called a const member function. The const-modified class member function actually modifies the implicit this pointer of the member function, indicating that any member of the class cannot be modified in the member function.

To sum up : For those member variables that do not change internally, that is, *this object data, these member functions should be added with const

Anyway, follow the principle of not letting go of one


5 and 6. Address and const address operator overloading

These two masters don’t need to pay much attention. The default generated ones are enough. We don’t need to manually reload them. Let’s be lazy here.

 Of course, it is necessary to prevent tricky requirements, such as when not returning the address , it still needs to be overloaded

 


At this point, all member functions are outlined! But it's not over yet! Classes and objects are really hard to chew, right! But the rest are just some details!

Thanks for reading! Hope to help you a little bit!

 

Guess you like

Origin blog.csdn.net/m0_53607711/article/details/128482624