C + + classes and objects (six default member functions in the class)

Default member function of class

If a class has no members, we call it an empty class. But there is not nothing in the empty class. The compiler will automatically generate six default member functions. If we implement the default member function, the compiler will not generate by default.
The purpose of the default member function is to prevent us from forgetting to do some necessary operations.

Constructor

concept

The constructor is a special member function with the same name as the class name. It is automatically called by the compiler when creating an object and appears only once in the object declaration cycle.
It should be noted that although the constructor is called construct, it does not create an object, but initializes an object.
The constructor is embodied in that as long as the variable is created, it will call the function and its special parameter passing method.

features

1. The function name is the same as the class name.
2. No return value.
3. When an object is instantiated, the compiler automatically calls the corresponding constructor.
4. The constructor can be overloaded. (multiple constructors can be defined in a class, and different initializations can be carried out according to different parameter properties)

Define your own constructor

Class

If we write the default member function ourselves, the compiler will not generate it automatically. It will judge that the constructor has been generated by the user according to the fact that there is no return value and the function name is the same as the class name.

class Data
{
private:
	int _year;
	int _month;
	int _day;
public:
	Data(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
};
int main()
{
	Data d1(2022,3,1);
	d1.Print();
}


Here is the constructor we wrote ourselves.

Parameter transmission method

Data d1(2022,3,1);

This code represents the use of the constructor. At this time, the value of d1 will be initialized. The printed result is:

When we write the constructor ourselves, we can write it as parameterless, full default or semi default.
Even if it is written as no parameter, the program will call the constructor when creating the variable. (it will be called whenever a variable is created) but it doesn't make any sense. We build our own constructor to initialize by passing parameters.

Note: when it is written in the full default form, the parameter transfer method of d1 cannot be written as Data d1(), but as Data d1;
When written in the form of semi default, the rules are consistent with the parameters passed by the semi default function.
Write your own constructor. It is recommended to write it in full default form.

Compiler generated constructor

When we don't define the constructor ourselves, the compiler will automatically generate a constructor. And no parameter transfer is required.

Define variable mode

Data d1;

Normal definition is enough.

Initialization rule

Before explaining the initialization rules, we need to understand one concept: the default constructor. (the previous six default member functions are distinguished)
The default constructor refers to the constructor that can be called without passing parameters, such as parameterless constructor and full default constructor. The constructor generated by the compiler by default belongs to parameterless constructor, which also belongs to the default constructor.

1. For the built-in types in the class, such as int and double, the compiler will not initialize them, but store a random value.
2. For the custom type in the class (mainly refers to the class in the class here), the compiler will go back and call the default constructor of the class to initialize it. If the class does not have a default constructor, the compiler will report an error.

class A
{
public:
private:
	int _a;
	int _b;
};
class Data
{
private:
	int _year;
	int _month;
	int _day;
	A a;
public:
	/*Data(int year=2022, int month=2, int day=4)
	{
		_year = year;
		_month = month;
		_day = day;
	}*/
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
};
int main()
{
	Data d1;
}

In this code, we do not implement our own constructor in the Data class, so we use the constructor automatically generated by the compiler. The way to define variables is: Data d1, and there is no need to pass parameters.
In object d1, there is a built-in variable: int_ year ,int _month,int _day. These three are not initialized and are random values.
There is also a user-defined type A a. due to the existence of user-defined types, the compiler will look for the default constructor in class A to initialize a. Note that the default constructor is not the constructor. When a is defined in a class, it cannot be initialized as it is defined in the main function
When there is no default constructor in this class, an error will be reported (generally, there will be no default constructor if the constructor is self-defined and not all default).
Class A does not have its own defined constructor, so it is a constructor automatically generated by the compiler. As mentioned above, the constructor automatically generated by the compiler is the default constructor. The default constructor generated by the compiler does not deal with built-in types, so in a_ A and_ The values of b are random.
It would be more obvious to change class A to this:

class A
{
public:
	A()
	{
		cout << "call A Default constructor for" << endl;
	}
private:
	int _a;
	int _b;
};

At this time, A() is the constructor of class A. since it can be called without parameters, when the compiler initializes a, it will call A() in its class. The running result is:

However, the purpose of calling the constructor is generally initialization, so the constructor in A should have initialization function.

class A
{
public:
	A()
	{
		_a = 10;
		_b = 20;
	}
private:
	int _a;
	int _b;
};

We can see the initialized a:

You can also see that the built-in type is not initialized.

Destructor

concept

The destructor is opposite to the constructor. The destructor does not complete the destruction of the object (the destruction of the object is controlled by the stack frame). The destruction of the local object is completed by the compiler, and the destructor will be called automatically when the object is destroyed to complete some resource cleaning operations in the object.

characteristic

1. The destructor name is preceded by a ~.
2. No parameter, no return value.
3. A class has only one destructor. If it is not defined by itself, the compiler will automatically generate the default destructor.
4. At the end of the object life cycle, the C + + compilation system automatically calls the destructor.

Define your own destructor

When you write your own destructor, the compiler will not automatically generate the destructor. The compiler will judge whether the user has implemented the destructor according to the ~ class name and no return value.
The resources cleaned up by the destructor are usually the space opened up in the heap, and the destruction of variables is completed by the destruction of the function stack frame.

class Stack
{
public:
	Stack(int capacity = 4)
	{
		_a = (int*)malloc(sizeof(int) * capacity);
		if (_a == nullptr)
		{
			cout << "malloc fail\n" << endl;
			exit(-1);
		}

		_top = 0;
		_capacity = capacity;
	}
	~Stack()
	{
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	int* _a;
	size_t _top;
	size_t _capacity;
};
int main()
{
	Stack a;
}

First, we use our own constructor to initialize a this.
Then we define the destructor ourselves. After the life cycle of a ends (i.e. the main function runs), the compiler will automatically call the destructor we define ourselves to clean up the space of the opened heap area.

We see that when the program runs to line 33, it will enter the destructor to clean up the space opened by a in the heap.
In order to reflect the final call of destructor, we can print something in the destructor.

We should also pay attention to the order of destruct, which is to destruct the objects defined first and then destruct the objects defined first.

    Stack a;
	Stack b;

At this time, the order of destruct is destruct b first and then destruct a.

Destructor automatically generated by compiler

When we don't write a destructor ourselves, the compiler will automatically generate a destructor.
The destructor automatically generated by the compiler cleans up space in a similar way to the constructor.

1. The built-in type is not cleaned.
2. A custom type (here refers to a class) will call the destructor of the class to clean up.

class Stack
{
public:
	Stack(int capacity = 4)
	{
		_a = (int*)malloc(sizeof(int) * capacity);
		if (_a == nullptr)
		{
			cout << "malloc fail\n" << endl;
			exit(-1);
		}

		_top = 0;
		_capacity = capacity;
	}
	~Stack()
	{
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	int* _a;
	size_t _top;
	size_t _capacity;
};
class MyStack
{
public:
private:
    int p;
	Stack pushstack;
	Stack popstack;
};
int main()
{
	Stack a;
	Stack b;
	MyStack mq;
}

In this code, the class mq is defined, but we do not define our own destructor in the class mq, so we will call the destructor generated by the compiler by default for processing. According to the rules, the p variable will not be processed (the destruction of the p variable is completed by the function stack frame). pushstack and popstack will call the destructor of their class to destroy the space in the heap.
If there is no destructor defined in this class, the space in this heap can only be free when the program ends.

copy constructor

concept

The copy constructor is an overloaded constructor with only a single parameter. This parameter is a reference to an object of this class type (commonly used const decoration). It is automatically called by the compiler when creating a new object with an existing class type object.

characteristic

1. Copy constructor is an overloaded form of constructor.
2. There is only one parameter of the copy constructor, and the parameter must be passed by reference. Using the value passing method will cause infinite recursive calls.

Call method

    Data d1(2022,3,5);
	Data d2(d1);

What is shown here is to copy the contents in d1 to d2.

Define your own copy constructor

class Data
{
private:
	int _year;
	int _month;
	int _day;
public:
	Data(int year=2022, int month=2, int day=4)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Data(Data& aa)
	{
		_year = aa._year;
		_month = aa._month;
		_day = aa._day;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
};
int main()
{
	Data d1(2022,3,5);
	Data d2(d1);
	d2.Print();
}

This code uses the copy constructor:

Note that when you write your own copy constructor, the formal parameters must be written as reference types.
This is because once written as a value passing type, an infinite loop call occurs
Because the meaning of value transfer is to open up another space (aa), copy the content of variable d1 to the newly opened space, and then assign a value to the content in d2. When copying the content of d1, it is equivalent to a copy structure. This copy structure needs to pass the value first and go down all the time.
To sum up, the process of passing arguments to formal parameters is also a copy construction. Data aa(d1).

Compiler generated copy constructor

When we do not implement the copy constructor, the compiler generates it itself.
Its rule is to complete the copy in the order of bytes stored in memory.
In other words, in the above program, if we do not implement the copy constructor ourselves, Data d2(d1) can also complete the copy construction.
So does it mean that we don't need to implement the copy constructor ourselves? Of course not, my treasure.

class Stack
{
public:
	Stack(int capacity = 4)
	{
		_a = (int*)malloc(sizeof(int) * capacity);
		if (_a == nullptr)
		{
			cout << "malloc fail\n" << endl;
			exit(-1);
		}

		_top = 0;
		_capacity = capacity;
	}
	~Stack()
	{
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	int* _a;
	size_t _top;
	size_t _capacity;
};
int main()
{
	Stack a;
	Stack a(b);
}

If it is a class defined in this way (opening up space in the heap), the program will crash.

This is because during copy construction, the pointer in b is a copy of the pointer in a, both of which point to the same space. When changing the content pointed to by the pointer in a, the content pointed to by the pointer in b will also be changed. More importantly, in the process of decomposition, the block memory space will be released twice, so an error will be reported.

Rules for compiler generated copy constructors:
1. For built-in type members, copy in byte order will be completed (as in the above two examples)
2. For a custom member, its copy structure will be called.

class A
{
private:
int _a;
public:
	A()
	{
		cout << "The default constructor was called" << endl;
	}
	A(A& a)
{
	cout << "Copy construct called" << endl;
}
};
class Data
{
private:
	int _year;
	int _month;
	int _day;
	A a;
public:
	Data(int year = 2022, int month = 2, int day = 4)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
};
int main()
{
	Data d1(2022, 3, 5);
	Data d2(d1);
}

Here, there is a custom type A in the Data class. From the printed results, we can see that the copy constructor is called.

Of course, this is for printing effect. The normal copy constructor should be written as follows:

A(A& a)
	{
		_a = a._a;
	}

This can be done_ Copy of a.

Call timing of copy constructor
1. Initialize one object with another.
2. Function parameter transfer.
3. Return by value transfer.

Operator overload (including assignment operator overload)

concept

Assignment operator overloading is a case of operator overloading. To understand assignment operator overloading, you need to understand operator overloading first.
In order to enhance the readability of the code, C + + introduces operator overloading. Operator overloading is a function with special function name, as well as its return type, function name and parameter list.

characteristic

1. The function name is: the keyword operator is followed by the operator symbol that needs to be overloaded.
2. Function prototype: return value type operator (parameter list)
3. You cannot create a new operator by connecting other symbols, such as operator @.
4. The overloaded operator must have an operand of class type or enumeration type.
5. The meaning of operators used for built-in types cannot be changed. For example, the meaning of built-in type + cannot be changed.
6. When an overloaded function is 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.
7..*,::, sizeof,?:,. The above five operators cannot be overloaded, which often appears in written multiple-choice questions.

usage method

    Data d1(2022, 1, 3);
	Data d2(2022, 3, 15);
	d1 < d2;

Here we define a date class, but it is not allowed to directly compare the sizes of d1 and d2, which requires us to create an operator overloaded function.
Because you want to compare dates, you need to compare them_ year,_ month,_ day these are private types, so the function should be defined in the class.
Note that if we define a function in a class like this:

void operator<(const Data&d1,const Data&d2);

The compiler will report an error because we ignored the this pointer, which must receive variables.
Therefore, the correct writing method is:

class Data
{
private:
	int _year;
	int _month;
	int _day;
public:
	Data(int year = 0, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << " " << _month << " " << _day << endl;
	}
	bool operator<(const Data& d)//Operator overloaded function
	{
		if (_year < d._year)
		{
			return true;
		}
		else if (_month < d._month)
		{
			return true;
		}
		else if (_day < d._day)
		{
			return true;
		}
		else
		{
			return false;
		}
	}
};
int main()
{
	Data d1(2022, 1, 3);
	Data d2(2022, 3, 15);
	d1 < d2;
	cout << (d1 < d2) << endl;
}

Note: this pointer receives the address of d1, and d and d2 are the same variable.
We can also compare sizes in the form of functions.

d1.operator<(d2);
d1<d2;

The two pieces of code are equivalent.
If_ year, etc. can be accessed outside the class, so you can also define the operator overloaded function in the global, but you can't use this pointer to pass parameters (just pass parameters normally). D1 < D2 will first find the function in the global, and then find it in the class.

Assignment overloaded function

Define the overload function of assignment

The writing form of the assignment overload function is the same as the example "<" above.

Data& operator=(Data& d)
	{
		if (this != &d)
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
		return *this;
    }

Return value
1. The returned object can be continuously assigned.
2.this pointer is created in the function and will be destroyed at the end of the function, but the content (d1) pointed to by this pointer will not be. Therefore, it can be returned by passing reference to reduce unnecessary copies.

Compiler generated overloaded assignment functions

1. The built-in type member will complete the byte order value copy.
2. For a user-defined type variable, its assignment overloaded function will be called for assignment.

This is very similar to the copy constructor.

class A {
private: int _a;
public:
	void operator=(A& d)
	{
		cout << "Call the assignment overload function" << endl;
	}
};
class Data
{
private:
	int _year;
	int _month;
	int _day;
	A a;
public:
	Data(int year = 0, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << " " << _month << " " << _day << endl;
	}
};
int main()
{
	Data d1(2022, 1, 3);
	Data d2(2022, 3, 15);
	d1 = d2;
	d1.Print();
}

The result of this program is:

You can see that the assignment overloaded function in A is called.
Note that only a shallow copy is completed here. When a pointer to the heap space appears in the class, we still need to define our own assignment overload function, which is the same as the copy constructor, which will not be repeated here.

The difference between copy construction and assignment overload

Does the following code call copy construction or assignment overload?

Data d2(2022,2,2);
Data d1=d2;

The answer is the copy constructor. The difference between the two is that the copy constructor is to assign a value to an object just created, while the assignment overload is to operate on two existing objects.

Overload of address fetching and const address fetching operators

concept

They belong to the category of operator overloading. These two functions generally do not need to be defined by themselves. The compiler will generate them by default, so they are put together.
These two operators need to be overloaded only in special cases, such as when you want others to get the specified content.

Data* operator&()
	{
		return this;
	}
	const Data* operator&() const
	{
		return this;
	}

operators overloading

Data d1(2022,2,2);
cout<<d1;

This code is wrong when the operator overload function is not written.
Operator overloading cannot be defined in the class, because for cout < < D1, the first thing that must be passed in is the this pointer, but cout is passed to the this pointer here, so an error will occur and can only be defined in the global. But we have to visit_ year, which requires adding friend functions.

class Data
{
	friend void operator<<(ostream& out, const Data& d);
private:
	int _year;
	int _month;
	int _day;
public:
	Data(int year = 0, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
};
void operator<<(ostream& out, const Data& d)
{
	out << d._year<<" " << d._month << " " << d._day;
}
int main()
{
	Data d1(2022, 1, 3);
	Data d2(2022, 3, 15);
	cout << d1;
}

const member

If the object we want to print has constant properties, such as:

const Data d1(2022,2,2);
d1.Print();

It is impossible to Print because the default parameter in Print is Data* const this, and now the incoming parameter is const data * &d1, which belongs to the enlargement of permission, so it cannot be printed.
At the same time, this cannot be reflected in the definition of formal parameters. In such cases, const can be added after the function.

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

Then you can print normally:

summary
Member function plus const is good. It is recommended to add everything you can.
In this way, both ordinary objects and const objects can be called.
But if you want to modify the function of member variables, don't add it.

Keywords: C++ Back-end

Added by CrazeD on Sat, 22 Jan 2022 11:25:38 +0200