[C++] Six default member functions in classes and objects (middle) (1)

Table of contents

1. The six default member functions of the class

2. Constructor

2.1 The concept of constructor

2.2 Features

2.2.1 Overloading of constructors:

2.2.2 All default constructors:

3. Destructor

3.1 The concept of destructor

3.2 Features

4. Copy constructor

4.1 The concept of copy constructor

4.2 Features


1. The six default member functions of the class

If there are no members in a class, it is simply called an empty class. Is there really nothing in the empty class? No, when any class does not write anything, the compiler will automatically generate the following 6 default member functions.

2. Constructor

2.1 The concept of constructor

Let's take a look at the initialization of the date class here:

class Date
{
public:
    void Init(int year, int month, int day)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    void Print()
    {
    	cout << _year << "/" << _month << "/" << _day << endl;
    }

private:
    int _year;
    int _month;
    int _day;
};
int main()
{
    Date d1;
    d1.Init(2022, 7, 5);
    d1.Print();
    
    return 0;
}

operation result:

We are new to C++, so we must initialize like this.

If we instantiate too many objects and forget to initialize the objects, the result of the program running may be random values, or problems may occur.

Here the C++ patriarch thought of it and designed a constructor for us.

Let's first look at the result of forgetting to initialize and print directly:

Here is a random value, so why is this? Let's look down.

The constructor is a special member function with the same name as the class name, which is automatically called by the compiler when creating a class type object to ensure that each data member has a suitable initial value, and is called only once in the entire life cycle of the object .

2.2 Features

The constructor is a special member function. It should be noted that although the name of the constructor is called construction, the main task of the constructor is not to open space to create objects, but to initialize objects.
Its characteristics are as follows:
1. The function name is the same as the class name.
2. No return value (not void, no need to write). 3. The compiler automatically calls the corresponding constructor
when the object is instantiated . 4. The constructor can be overloaded.

Let's first write a date class constructor to see:

class Date
{
public:
	Date()//构造函数,无参构造
	{
		cout << "Date()" << endl;
		_year = 1;
		_month = 1;
		_day = 1;
	}

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


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

Let's test it out:

The constructor is not called in our main function, but the mark we made is printed here, and here we experimented that the constructor is automatically called when the object is instantiated.
Let's see what happens when we comment out the constructor we wrote:

We can see that after commenting out, it can still be printed out, but it is just a random value. Because when we don't write, the compiler will automatically generate a default constructor and call it automatically.

C++ divides types into built-in types (basic types): such as int, char, double, int*... (custom type* is also);

Custom types: such as class, struct, union....

And here we can see that members of built-in types will not be processed. In C++11, member variables are supported to give default values, which can be regarded as filling loopholes.

2.2.1 Overloading of constructors:

class Date
{
public:
	Date()
	{
		cout << "Date()" << endl;
		_year = 1;
		_month = 1;
		_day = 1;
	}
	
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}

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


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

int main()
{
    Date d1;
	d1.Print();
	Date d2(2023, 8, 1);//这里初始化必须是这样写,这是语法
	d2.Print();

    return 0;
}

operation result:

Note : When we instantiate an object, when the called constructor has no parameters, we cannot add parentheses after the object, the syntax stipulates.

If written like this, the compiler can't tell whether this is a function declaration or a call . d2 will not be confused because there is value passing, and the function declaration will not appear in that way.

2.2.2 All default constructors:

We can actually combine the above two constructors into one

class Date
{
public:
	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;
};
int main()
{
    Date d1;
	d1.Print();

	Date d2(2023, 8, 1);
	d2.Print();

	Date d3(2023, 9);
	d3.Print();

    return 0;
}

operation result:

The full default constructor is the most applicable. No-argument construction and full default can exist at the same time, but it is not recommended to write this way. Although no error is reported, we do not want to pass parameters when calling full default. The compiler does not know which construction we want to call, which will cause ambiguity.

Let's look at the problem of implementing a queue with two stacks:

class Stack
{
public:
	Stack(int n = 4)
	{
		if (n == 0)
		{
			_a = nullptr;
			_size = -1;
			_capacity = 0;
		}
		else
		{
			int* tmp = (int*)realloc(_a, sizeof(int) * n);
			if (tmp == nullptr)
			{
				perror("realloc fail");
				exit(-1);
			}
			_a = tmp;

			_size = -1;
			_capacity = n;
		}
	}

	void Push(int n)
	{
		if (_size + 1 == _capacity)
		{
			int newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
			int* tmp = (int*)realloc(_a, sizeof(int) * newcapacity);
			if (nullptr == tmp)
			{
				perror("realloc fail:");
				exit(-1);
			}
			_a = tmp;
			_capacity = newcapacity;
		}

		_a[_size++] = n;
	}

	int Top()
	{
		return _a[_size];
	}

	void Pop()
	{
		assert(_size > -1);
		_size--;
	}

	void Destort()
	{
		free(_a);
		_a = nullptr;
		_size = _capacity = 0;
	}

	bool Empty()
	{
		return _size == -1;
	}

private:
	int* _a;
	int _size;
	int _capacity;
};
class MyQueue
{
private:
	Stack _pushst;
	Stack _popst;

};

Under normal circumstances, we need to write the constructor by ourselves to determine the initialization method. The member variables are all user-defined types, so we can consider not writing the constructor. The default constructor of the custom type will be called .

Summary: No-argument constructors, full default constructors, and constructors generated by default by the compiler if we don’t write them can all be considered as default constructors, and there can only be one default constructor (multiple coexistence will cause ambiguity) .

3. Destructor

3.1 The concept of destructor

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.

3.2 Features

The destructor is a special member function, and its characteristics are as follows:
1. The destructor name is the character ~ added before the class name.
2. No parameters and no return type.
3. A class can have only one destructor. If not explicitly defined, the system will automatically generate a default destructor (the built-in type will not be processed, and the custom type will call its own destructor). Note: Destructors cannot be overloaded.
4. When the life cycle of the object ends, the C++ compilation system will automatically call the destructor.

Let's first look at the destructor of the date class:

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
        cout << "Date(int year = 1, int month = 1, int day = 1)" << endl;
		_year = year;
		_month = month;
		_day = day;
	}

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

	~Date()
	{
		cout << "~Date()" << endl;
		_year = 0;
		_month = 0;
		_day = 0;
	}

private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	Date d2;

    return 0;
}

operation result:

We can see here that the destructor is also called automatically.

We do not write, the compiler automatically generates a default destructor.

The calling order of the destructor is similar to that of the stack, and the ones that are instantiated later are destructed first.

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.

Let's draw a picture to see:

The destructor in the stack replaces the destruction of the stack:

class Stack
{
public:
	Stack(int n = 4)
	{
		if (n == 0)
		{
			_a = nullptr;
			_top = -1;
			_capacity = 0;
		}
		else
		{
			int* tmp = (int*)realloc(_a, sizeof(int) * n);
			if (tmp == nullptr)
			{
				perror("realloc fail");
				exit(-1);
			}
			_a = tmp;

			_top = -1;
			_capacity = n;
		}
	}

	~Stack()
	{
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
	//void Destort()
	//{
	//	free(_a);
	//	_a = nullptr;
	//	_top = _capacity = 0;
	//}

	void Push(int n)
	{
		if (_top + 1 == _capacity)
		{
			int newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
			int* tmp = (int*)realloc(_a, sizeof(int) * newcapacity);
			if (nullptr == tmp)
			{
				perror("realloc fail:");
				exit(-1);
			}
			_a = tmp;
			_capacity = newcapacity;
		}

		_a[_top++] = n;
	}

	int Top()
	{
		return _a[_top];
	}

	void Pop()
	{
		assert(_top > -1);
		_top--;
	}

	bool Empty()
	{
		return _top == -1;
	}

private:
	int* _a;
	int _top;
	int _capacity;
};
class MyQueue
{
private:
	Stack _pushst;
	Stack _popst;

};

For things like the stack, our destructor replaces the destruction function, and the destructor will be called automatically. In the past, we needed to manually call the interface of the destruction function, but now we don’t need to call it.

Therefore, the biggest advantage of constructors and destructors is that they are called automatically.

4. Copy constructor

4.1 The concept of copy constructor

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

4.2 Features

The copy constructor is also a special member function, and its characteristics are as follows:
1. The copy constructor is an overloaded form of the constructor.
2. The parameter of the copy constructor is only one and must be a reference to the same type of object , and the compiler will cause infinite recursive calls when using the pass-by-value method.
3. If not explicitly defined, the compiler will generate a default copy constructor. The default copy constructor object is copied in byte order according to memory storage. This kind of copy is called shallow copy, or value copy.

Copy construction is like copying and pasting.

The parameter of the copy constructor is only one and must be a reference to a class type object, and the compiler will cause infinite recursive calls when using the pass-by-value method.

Copying by value will cause infinite recursion, so let's write a copy constructor.

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	//拷贝构造
	Date(Date d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

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

	~Date()
	{
		_year = 0;
		_month = 0;
		_day = 0;
	}

private:
	int _year;
	int _month;
	int _day;
};
void func(Date d)
{
	d.Print();
}

int main()
{
	Date d1(2023, 8, 2);
	func(d1);

    return 0;
}

The copy of the built-in type is a direct copy, and the copy of the custom type needs to be completed by calling the copy construction.

In vs2019, the compiler for passing parameters by value will report an error:

Therefore, if we write a copy constructor, the formal parameter must be a reference of the same type:

The reference is to alias the variable, and the order of the automatic call of the destructor is to destruct after the definition. When copying, d1 has not been destructed, so the reference can be used, so that it will not cause recursive copying.

Let's mask out the copy constructor we wrote and see what happens:

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	//拷贝构造
	//Date(Date& d)
	//{
	//	_year = d._year;
	//	_month = d._month;
	//	_day = d._day;
	//}

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

	~Date()
	{
		_year = 0;
		_month = 0;
		_day = 0;
	}

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

int main()
{
	Date d1(2023, 8, 2);
	Date d2(d1);
	d2.Print();
	
    return 0;
}

operation result:

 We found that if we don’t write it, we can still copy it. This is because if we don’t write it, the compiler generates a copy constructor by default. For shallow copies like the date class, the default generated constructor can realize copying.

Let's look at the copy construction of the stack again:

typedef int DataType;
class Stack
{
public:
	Stack(int n = 4)
	{
		if (n == 0)
		{
			_a = nullptr;
			_size = -1;
			_capacity = 0;
		}
		else
		{
			int* tmp = (int*)realloc(_a, sizeof(int) * n);
			if (tmp == nullptr)
			{
				perror("realloc fail");
				exit(-1);
			}
			_a = tmp;

			_size = -1;
			_capacity = n;
		}
	}

	//拷贝构造
	Stack(Stack& s)
	{
		_a = s._a;
		_size = s._size;
		_capacity = s._capacity;
	}

	~Stack()
	{
		free(_a);
		_a = nullptr;
		_size = _capacity = 0;
	}

	void Push(int n)
	{
		if (_size + 1 == _capacity)
		{
			int newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
			int* tmp = (int*)realloc(_a, sizeof(int) * newcapacity);
			if (nullptr == tmp)
			{
				perror("realloc fail:");
				exit(-1);
			}
			_a = tmp;
			_capacity = newcapacity;
		}

		_a[_size++] = n;
	}

	int Top()
	{
		return _a[_size];
	}

	void Pop()
	{
		assert(_size > -1);
		_size--;
	}

	bool Empty()
	{
		return _size == -1;
	}

private:
	int* _a;
	int _size;
	int _capacity;
};
int main()
{
	Stack s1;
	Stack s2(s1);
	
    return 0;
}

Here we write the copy construction for the stack, let's try the copy construction:

Why is an exception raised here?

Let's debug and see:

Here we can see that the address of _a of s1 is the same as that of _a of s2. When s2 is copied, it will be destructed. After _a of s2 is released, s1 will call the destructor again. When you release _a, the space of _a has been released, which will cause a null pointer exception.

Therefore, for objects with space application, deep copy must be performed when writing copy construction.

Let's correct the code:

typedef int DataType;
class Stack
{
public:
	Stack(int n = 4)
	{
		if (n == 0)
		{
			_a = nullptr;
			_size = -1;
			_capacity = 0;
		}
		else
		{
			int* tmp = (int*)realloc(_a, sizeof(int) * n);
			if (tmp == nullptr)
			{
				perror("realloc fail");
				exit(-1);
			}
			_a = tmp;

			_size = -1;
			_capacity = n;
		}
	}

	//拷贝构造
	Stack(Stack& s)
	{
		cout << "Stack(Stack& s)" << endl;
		//深拷贝
		_a = (DataType*)malloc(sizeof(DataType) * s._capacity);
		if (nullptr == _a)
		{
			perror("malloc fail:");
			exit(-1);
		}

		memcpy(_a, s._a, sizeof(DataType) * (s._size+1));
		_size = s._size;
		_capacity = s._capacity;
	}

	~Stack()
	{
		free(_a);
		_a = nullptr;
		_size = _capacity = 0;
	}

	void Push(int n)
	{
		if (_size + 1 == _capacity)
		{
			int newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
			int* tmp = (int*)realloc(_a, sizeof(int) * newcapacity);
			if (nullptr == tmp)
			{
				perror("realloc fail:");
				exit(-1);
			}
			_a = tmp;
			_capacity = newcapacity;
		}

		_a[_size++] = n;
	}

	int Top()
	{
		return _a[_size];
	}

	void Pop()
	{
		assert(_size > -1);
		_size--;
	}

	bool Empty()
	{
		return _size == -1;
	}

private:
	int* _a;
	int _size;
	int _capacity;
};

operation result:

Summary: Like Date, we don’t need to implement the copy structure, and the one generated by default can be used; Stack needs us to implement the copy structure of deep copy, and the default one will cause problems; there is no need to write copy for all members of custom types Construction, the copy constructor of the custom type will be called.

Extension:

Guess you like

Origin blog.csdn.net/Ljy_cx_21_4_3/article/details/132089939