C++ classes and objects (3)—copy constructor & operator overloading

Table of contents

1. Copy constructor

1. Definition

2. Characteristics

3. Built-in and custom types 

4. const modified parameters

5. Generate by default

Shallow copy

deep copy

6. Summary

2. Operator overloading

1. Definition 

2. Determine whether they are equal

3. Compare size

4. Assignment


Continued from the previous article:C++ classes and objects (2)—Constructor &Destructor

1. Copy constructor

1. Definition

Copy constructor: There is only a single formal parameter. This formal parameter is a reference to an object of this class (generally commonly used with const modification). It is already stored in use

Automatically called by the compiler when a new object is created for a class type object.

2. Characteristics

  • The copy constructor is an overloaded form of the constructor.
  • There is only one parameter to the copy constructor and it must be a reference to a class type object. If you use the pass-by-value method, the compiler will directly report an error because it will cause infinite recursive calls.
class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	// 拷贝构造
	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;
};
int main()
{
	date d1(2023, 2, 3);
	date d2(d1);
	d1.Print();
	d2.Print();

	return 0;
}

The output is as follows, we can see that the value of d1 is successfully copied to d2. 

If you do not add the & reference symbol, the compiler will report an error.​ 

 You can also use copy construction in this way:

Date d3 = d1;

3. Built-in and custom types 

  • For copying and passing parameters of built-in types, the compiler can copy directly.
  • Copying and passing parameters of custom types requires calling copy construction.

Why is copy construction needed?


When using a stack to implement a queue, st1 may be destructed at one time and st2 may be destructed on the other. The space pointed to by the member variable pointer _a cannot be destructed twice, and if initial values ​​are assigned to st1 and st2 respectively, they will be assigned later. will overwrite the data assigned first. Therefore, it is required that they cannot point to the same space, and each must have its own space. Therefore, C++ stipulates that copying and passing parameters of custom types requires calling copy constructors. For stack copy constructs that require deep copies (follow-up learning), At this stage, you only need to know that copy construction is required.

Custom type parameter passing:

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

	// 拷贝构造
	// Date d2(d1);
	Date(Date& d)
	{
		_year = d._year;
        _month = d._month;
        _day = d._day;
	}
private:
	int _year;
	int _month;
	int _day;
};

// 传值传参
void Func1(Date d)
{

}
// 传引用传参
void Func2(Date& d)
{

}
// 传指针
void Func3(Date* d)
{

}
int main()
{
	Date d1(2023, 2, 3);
	Func1(d1);

    //Func2(d1);
    //Func3(&d1);
	return 0;
}

Use Func1 to pass parameters by value:

Pressing F11 at Func1(d1) will jump directly to the copy construction.​ 

At the end of the copy construction, the Func1 function will be executed.​ 

If you use Func2 reference to pass parameters, there is no need for copy construction.​ 

Jump directly to the Func2 function.​ 

In addition, you can also use pointers that were commonly used in C language to pass parameters, but this method is a bit cumbersome.

Func3(&d1);

4. const modified parameters

Copy construction generally adds const. If it is not added, such as the following situation, the assignment direction is reversed, which will cause the original data to become a random value.

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

Adding const can prevent the original data from being modified when the copy direction is wrong, so it is generally customary to add const before parameters.

	Date(const Date& d)
	{
		_year = d->_year;
		_month = d->_month;
		_day = d->_day;
	}

In addition, if the copied variable is modified by const, and if the parameters of the copy construction are not modified by const, the permissions of the reference will be expanded, so the parameters must be modified with const.

5. Generate by default

Shallow copy

If not explicitly defined, the compiler will generate a default copy constructor. The default copy constructor object is stored in memory

The copy is completed in byte order. This copy is called a shallow copy, or a value copy.

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(2023, 11, 20);
	d1.Print();
	Date d2(d1);
	d2.Print();
	return 0;
}

From the output results, we can find that if the copy function is not written, the compiler will automatically copy the built-in types.

The default copy constructor generated by the compiler can already complete the endian value copy. Do I still need to implement it explicitly by myself?

Of course classes like date classes are not necessary.

What if it is a custom type? How will the program handle this?

typedef int DataType;
class Stack
{
public:
	Stack(size_t capacity = 10)
	{
		cout << "Stack(size_t capacity = 10)" << endl;

		_array = (DataType*)malloc(capacity * sizeof(DataType));
		if (nullptr == _array)
		{
			perror("malloc申请空间失败");
			exit(-1);
		}

		_size = 0;
		_capacity = capacity;
	}
	void Push(const DataType& data)
	{
		// CheckCapacity();
		_array[_size] = data;
		_size++;
	}
	~Stack()
	{
		cout << "~Stack()" << endl;
		if (_array)
		{
			free(_array);
			_array = nullptr;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	DataType* _array;
	size_t    _size;
	size_t    _capacity;
};

int main()
{
	Stack st1;
	Stack st2(st1);

	return 0;
}

 After output, the program will report an error:

If the custom type is copied according to the built-in type method, the member variable *array of the two Stack class variables st1 and st2 both point to the same space,This will cause Two questions:

  1. Inserting and deleting data will affect each other. For example, st1 inserts three data first, and the size of st1 is 3. At this time, st2 wants to insert data, but the size of st2 is 0. If data is inserted, the data inserted by st1 will be overwritten; at the same time , when st1 is destructed, the space of st1 is released. Since the member variables *array of st1 and st2 both point to the same space, inserting data into st2 at this time will cause access to wild pointers.
  2. When the program destroys the *_array space, it will be destructed twice and the program will crash.

deep copy

At this time, only deep copy can solve the copy structure of the custom type.

	Stack(const Stack& st)
	{
		_array = (DataType*)malloc(sizeof(DataType) * st._capacity);
		if (_array == nullptr)
		{
			perror("malloc fail");
			exit(-1);
		}
		memcpy(_array, st._array, sizeof(DataType) * st._size);
		_size = st._size;
		_capacity = st._capacity;
	}

st2 successfully implemented the copy construction. The addresses of st2 and st1 are different, and they no longer point to the same space.

When do you need to implement copy construction yourself?​ 

  • When you implement the destructor to release space, you need to implement copy construction.
class MyQueue
{
public:
	// 默认生成构造
	// 默认生成析构
	// 默认生成拷贝构造

private:
	Stack _pushST;
	Stack _popST;
	int _size = 0;//缺省值处理
};
  •  The two Stack type member variables are initialized by calling the default generation constructor, their destructor is called for destruction, and their copy constructor is used for copying.
  • The initialization of the integer variable _size is handled by the default value. The built-in type will be automatically destroyed after being generated. There is no need for destruction processing. You can use the default generated copy constructor.

6. Summary

Typical calling scenarios of copy constructor:

  • Create a new object using an existing object
  • The function parameter type is a class type object
  • The function return value type is a class type object

A copy constructor is generated by default:

  • Built-in types complete shallow copy/value copy (copy by byte one by one)
  • Custom type to call the copy constructor of this member.

In order to improve program efficiency, generally try to use reference types when transferring object parameters. When returning, reference types can be used according to the actual scenario.

Use quotes whenever possible.

2. Operator overloading

1. Definition 

C++ introduces operator overloading in order to enhance the readability of the code. Operator overloading is a function with a special function name< a i=3> 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: keywordoperator followed by the operator symbol that needs to be overloaded.
  • Function prototype:Return value type operator (parameter list)

Notice:

  • New operators cannot be created by concatenating other symbols: such as operator@
  • Overloaded operators must have a class type parameter
  • Operators used for built-in types cannot change their meaning. For example: the built-in integer type + cannot change its meaning.
  • When overloaded as a class member function, its formal parameters appear to be 1 less than the number of operands, because the first parameter of the member function is hidden this
  • .* :: sizeof ? : . Note that the above five operators cannot be overloaded.

We understand it bit by bit through the code:

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

//private:
	int _year;
	int _month;
	int _day;
};
//运算符重载
bool operator==(const Date& d1, const Date& d2)
{
	return d1._year == d2._year
		&& d1._month == d2._month
		&& d1._day == d2._day;
int main()
{
	Date d1(2023, 1, 1);
	Date d2(2023, 1, 1);
    //下面两种使用方式均可
	operator==(d1, d2);
	d1 == d2;//编译器会转换成去调用operator==(d1,d2);
	return 0;
}

If you want to output the function return value, the method is as follows:

cout << (d1 == d2) << endl;//必须加括号,保证优先级
cout << operator==(d1, d2) << endl;

You will find here that if the operator is overloaded to be global, the member variables need to be public, otherwise an error will be reported.

So the question is, how to ensure encapsulation?

We can choose the friend processing we will learn later. At this stage, we can directly overload the operator overloaded function into a member function.

2. Determine whether they are equal

class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
//运算符重载
// bool operator==(Date* this, const Date& d)
// 这里需要注意的是,左操作数是this,指向调用函数的对象
	bool operator==(const Date& d)
	{
		return _year == d._year
			&& _month == d._month
			&& _day == d._day;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2023, 1, 1);
	Date d2(2023, 1, 1);
//operator内置只能使用这种形式(d1 == d2,
	cout << (d1 == d2) << endl;
	return 0;
}

3. Compare size

	bool operator<(const Date& d)
	{
		if (_year < d._year)
		{
			return true;
		}
		else if (_year == d._year && _month < d._month)
		{
			return true;
		}
		else if (_year == d._year && _month == d._month && _day < d._day)
		{
			return true;
		}
		else
		{
			return false;
		}
	}

It can also be written in this short form, but the longer way above is more intuitive.

        return _year < d._year
			|| (_year == d._year && _month < d._month)
			|| (_year == d._year && _month == d._month && _day < d._day);

To judge less than or equal to, we can use the hidden this pointer and use operator overload nesting to call the operator overloads for judging less than or equal to respectively in judging less than or equal to.

// d1 <= d2
bool operator<=(const Date& d)
{
	return *this < d || *this == d;
}

In the same way, to judge greater than, greater than or equal to, and not equal to, we also reuse the above operator overloading.​ 

// d1 > d2
bool operator>(const Date& d)
{
	return !(*this <= d);
}

bool operator>=(const Date& d)
{
	return !(*this < d);
}

bool operator!=(const Date& d)
{
	return !(*this == d);
}

 4. Assignment

When the user does not implement it explicitly, the compiler will generate a default assignment operator overload, copying byte by byte in the form of value.
  • Built-in type member variables are directly assigned, while custom type member variables need to call the assignment operator overload of the corresponding class to complete the assignment. Therefore, the date class does not need to write the assignment operator overload function, but similar to using a stack to implement a queue.
  • If resource management is not involved in the class, the assignment operator can be implemented or not; once resource management is involved, it must be implemented.
  • When the assignment operator is not explicitly implemented in the class, the compiler will generate a default one. At this time, if the user overloads the assignment operator as global outside the class, it will conflict with the default assignment operator generated by the compiler. Therefore Assignment operators can only be overloaded as member functions.
class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
	void operator=(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2023, 6, 6);
	Date d2(1, 1, 1);

	d2 = d1;
	d1.Print();
	d2.Print();

	return 0;
}

 Operator= is our assignment operator overload. If we don't use reference to pass value, the custom type will call copy construction. We use reference to pass value to avoid these copy operations.

 

What if three numbers are assigned values?

d3 = d2 = d1;

 According to the right associativity of the assignment operator, assigning d1 to d2 requires a return value before assigning d3.

  The return value is returned by reference to avoid creating temporary variables and unnecessary copies during the value return process.

 

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

		return *this;
	}

What if you assign value to yourself? Then there is no need to perform the assignment operation, we can just add a judgment.​ 

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

The above is the complete version of the assignment operator.

Guess you like

Origin blog.csdn.net/m0_73800602/article/details/134517941