【C++】-- 构造函数、析构函数、拷贝构造函数、赋值运算符重载函数

【C++】-- 构造函数、析构函数、拷贝构造函数、赋值运算符重载函数,第1张

目录

一、构造函数

1.构造函数定义及特性

2.编译器自动生成的默认构造函数

二、析构函数 

1.析构函数定义及特性 

2.多对象的析构顺序 

3.编译器自动生成的默认析构函数

三、拷贝构造函数 

1.拷贝构造函数定义及特性

2.编译器自动生成的拷贝构造函数

四、赋值运算符重载函数

1.运算符重载 

2.赋值运算符重载 

3.const修饰类的成员函数

五、取地址 *** 作符重载和const取地址 *** 作符重载

六、总结 

1.构造函数和析构函数

2.拷贝构造和赋值运算符重载 

3.取地址 *** 作符重载和const取地址 *** 作符重载


来吧 学习使我快乐~ 

假如一个类中既没有成员变量也没有成员函数,那么这个类就是空类,空类并不是什么都没有,因为所有类都会生成如下6个默认成员函数:

一、构造函数 1.构造函数定义及特性

对于日期类对象,我们可能会忘记调用Init函数进行初始化,C++为了解决这个问题,引入构造函数来进行初始化。

#include
using namespace std;

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

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

int main()
{
	Date d1;
	d1.Init(2022, 4, 8);//如果忘记调用Init函数,d1就不不会被初始化

    return 0;
}

构造函数是一个特殊的成员函数,名字与类名相同,编译器创建类类型对象时会自动调用构造函数,保证每个数据成员都有 一个合适的初始值,并且在对象的生命周期内只调用一次。主要任务是初始化对象,而不是开空间创建对象。构造函数最主要的特点是能够自动调用。

特性:

(1)函数名与类名相同。
(2)无返回值。
(3)对象实例化时编译器自动调用对应的构造函数。
(4)构造函数可以重载。

(5)如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义了编译器将不再生成。

默认构造函数是不用传参就可以调用的构造函数,分为:

(1)我们没写,编译器默认自动生成

(2)我们写的无参默认构造函数

(3)我们写的带参全缺省默认构造函数 

 这3种默认构造函数只能有一个,(2)和(3)不能同时存在的原因:当定义一个不带参数的类对象时,编译器不能确定到底要调用我们写的无参默认构造函数还是要调用我们写的带参全缺省默认构造函数,会报“对重载函数的调用不明确错误”。

(1)我们没写,编译器默认自动生成

#include
using namespace std;

class Date
{
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1;//调用编译器自动生成的默认构造函数

	return 0;
}

只不过d1的3个成员变量都是随机值:

(2)我们写的无参构造函数

#include
using namespace std;

class Date
{
public:
    
	//1.无参默认构造函数:初始化对象
	Date()
	{
		_year = 2022;
		_month = 4;
		_day = 8;
	}
	
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1;

	return 0;
}

(3)带参默认构造函数

#include
using namespace std;

class Date
{
public:
    //2.带参全缺省默认构造函数:初始化对象
	Date(int year = 2022, int month = 4, int day= 8)
	{
		_year = year;
		_month = month;
		_day = day;
	}

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

int main()
{
	Date d1;//调用带参默认构造函数

	return 0;
}

一般情况下会把无参默认构造函数和带参默认构造函数合二为一,因为这样能适应各种场景:

#include
using namespace std;

class Date
{
public:
    //无参带参二合一默认构造函数:初始化对象
	Date(int year = 2022, int month = 4, int day = 8)
	{
		_year = year;
		_month = month;
		_day = day;
	}

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

int main()
{
	Date d1;//调用二合一默认构造函数
	Date d2(2050, 9, 6);//调用二合一默认构造函数
    Date d3(2030);//只给一个参数

	return 0;
}

大多数情况下,我们都需要自己写构造函数,因为默认生成的构造函数不一定好用。

2.编译器自动生成的默认构造函数

在上一小节中,我们不写默认构造函数时,编译器自动生成的默认构造函数使得类对象的初始化成为随机值,初始化成随机值又没有什么用,那么编译器自动生成的默认构造函数到底有什么用呢?

C++把类型分成内置类型和自定义类型:

(1)内置类型就是语法已经定义好的类型:如int、char、long、short...等类型,编译器生成默认的构造函数对内置类型不做处理。

(2)自定义类型是使用class/struct/union自己定义的类型:而对自定义类型成员会调用的它的默认成员函数。

如下代码中,Date类没写构造函数,但会对Date自定义类型成员_t调用_t的默认成员函数,这里调用了_t的默认构造函数:

#include
using namespace std;

class Time
{
public:
	Time()
	{
		cout << "Time()" << endl;
		_hour = 0;
		_minute = 0;
		_second = 0;
	}

private:
	int _hour;
	int _minute;
	int _second;
};

class Date
{
private:
	// 基本类型(内置类型)
	int _year;
	int _month;
	int _day;

	// 自定义类型
	Time _t;
};

int main()
{
	Date d;
	return 0;
}

虽然对象d的3个成员变量_year、_month、_day被初始化成随机值

但是打印了Time( ),说明调用了Date类的自定义类型成员_t的默认成员函数Time( )。

二、析构函数  1.析构函数定义及特性 

析构函数用来完成类的资源清理工作,编译器在销毁对象时,会自动调用析构函数。

特性:

(1)析构函数名是在类名前加上字符 ~。

(2)无参数无返回值。(析构函数不能重载,一个类有且仅有一个析构函数)

(3)一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。 

(4)对象生命周期结束时,C++编译系统系统自动调用析构函数。 

什么时候对象会被销毁?对象出了定义对象的函数作用域以后,生命周期到了,对象会被销毁。

Date类不需要清理,Date类的析构函数什么也不做。年月日是属于对象的,而对象属于栈帧,当对象生命周期结束后,栈帧销毁了,对象所占用的空间就还给 *** 作系统了。

如何证明编译器销毁Date类对象时自动调用了析构函数呢?在析构函数内部加打印,程序执行完毕后,如果编译器调用了析构函数,那么一定会打印。

#include
using namespace std;

class Date
{
public:
	//无参带参二合一默认构造函数:初始化对象
	Date(int year = 2022, int month = 4, int day = 8)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	
    //析构函数:清理资源
    ~Date()
	{
		cout << "~Date()" << endl;//在析构函数内打印
	}

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

int main()
{
	Date d1;//调用二合一默认构造函数
	Date d2(2050, 9, 6);//调用二合一默认构造函数
	Date d3(2030);//只给一个参数

	return 0;
}

打印了, 证明编译器销毁Date类对象时自动调用了析构函数:

既然日期类的析构函数什么也不做,那么我们自己可以不显式定义日期类的析构函数,由编译器自动生成就好了。

但是栈的析构函数就需要我们自己显式定义,它构造对象时申请了堆的空间,对象销毁时需要释放申请的堆空间,因此Stack类的析构函数中需要free:

#include 
#include 
using namespace std;

typedef int STDataType;

class Stack
{
public:
	//构造函数
	Stack(int capacity = 4)
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * 4);
		_size = 0;
		_capacity = capacity;
	}

	//析构函数:清理资源
	~Stack()
	{
        
		free(_a);
		_a = nullptr;
		_size = _capacity = 0;
	}

public:
	STDataType* _a;
	int _size;
	int _capacity;

};

int main()
{

	Stack st; //定义一个对象

	return 0;
}

假如类中没有析构函数,需要写destroy函数,假如忘记调用destroy函数,会造成内存泄漏,并且不会反馈出来,不易察觉。C++很重视内存泄漏,因此将析构函数作为默认成员函数。有了析构函数之后就可以不用写destroy函数了,也就不需要考虑是否忘记调用destroy函数。

2.多对象的析构顺序 

 假如这个类有多个对象,那么析构的先后顺序是什么?

#include 
#include 
using namespace std;

typedef int STDataType;

class Stack
{
public:
	//构造函数
	Stack(int capacity = 4)
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * 4);
		_size = 0;
		_capacity = capacity;
	}

	//析构函数:清理资源
	~Stack()
	{
		cout << this << endl;
		free(_a);
		_a = nullptr;
		_size = _capacity = 0;
	}

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

};

int main()
{

	Stack st1; //定义一个对象st1
	cout << "&st1:"<< & st1 << endl;

	Stack st2; //定义一个对象st2
	cout << "&st2:" << &st2 << endl;
	
	return 0;
}

发现调用析构函数时,先打印的是st2的地址,说明后创建的对象先析构。

因为对象时定义在函数中的,函数调用会建立栈帧,因此栈帧中的对象构造和析构顺序也要遵循后进先出的原则。

3.编译器自动生成的默认析构函数

当不写析构函数时,编译器会自动生成默认的析构函数,不过这个默认的析构函数什么也不做,不需要清理资源。那么编译器自动生成的默认析构函数到底有什么用呢?

同析构函数

(1)对于内置类型,不会处理

(2)对于自定义类型,会调用它的析构函数

如下代码, Date类没写析构函数,但会对Date自定义类型成员_t调用_t的默认成员函数,这里调用了_t的默认析构函数: 

#include
using namespace std;

class Time
{
public:
	Time()
	{
		_hour = 0;
		_minute = 0;
		_second = 0;
	}

	~Time()
	{
		cout << "~Time()" << endl;
	}

private:
	int _hour;
	int _minute;
	int _second;
};

class Date
{
private:
	// 基本类型(内置类型)
	int _year;
	int _month;
	int _day;

	// 自定义类型
	Time _t;
};

int main()
{
	Date d;
	return 0;
}

三、拷贝构造函数  1.拷贝构造函数定义及特性

定义:把同类型的对象当做参数传给当前对象叫做拷贝构造函数,即类拿自己的一个对象去构造同类型的一个对象,完成对象的拷贝初始化。 

#include
using namespace std;

class Date
{
public:
	//构造函数
	Date(int year = 2022, int month = 4, int day = 8)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	//析构函数:清理资源
	~Date()
	{
		cout << "~Date()" << endl;//在析构函数内打印
	}

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

int main()
{
	Date d1(2022, 9, 6);//调用构造函数
	Date d2;//调用构造函数
	Date d3();

	return 0;
}

d3的定义方式没有调用构造函数,构造不出来对象: 

拷贝构造函数也是构造函数,函数名和类型名相同,参数是同类型对象的引用,由编译器自动调用。

特性:

(1)拷贝构造函数是构造函数的重载。
(2)拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用。

思考:为什么必须使用引用传参而不是传值传参?

①如果是内置类型的拷贝,传值拷贝即形参是实参的一份临时拷贝。

 如下使用场景:用a和b初始化x和y,x和y是a和b的一份临时拷贝

#include
using namespace std;

void Swap(int x, int y)
{
	int temp = x;
	int y = x;
	int y = temp;
}

int main()
{
	int a = 1;
	int b = 2;
    
    //传值拷贝并不能实现真正的交换
	Swap(a, b);
}

②如果是自定义类型的拷贝,那么传参使用传值,就是一个拷贝构造,而要调用拷贝构造就要先传参,如此循环往复,以至无穷。

如下,假如拷贝构造函数使用传值传参:

Date d4(d1);

为了解决自定义类型传值拷贝带来的的无穷递归问题,使用引用传参调用拷贝构造之前先传参,引用是参数对象内存的别名,当前对象就是this,把参数的值依次拷贝复制给当前对象。

#include
using namespace std;

class Date
{
public:
	//构造函数
	Date(int year = 2022, int month = 4, int day = 8)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	//拷贝构造函数
	Date(Date& d)//Date(Date* this, Date& d)
                 //d4是this,       形参d是d1的引用,是d1内存的别名
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

	//析构函数:清理资源
	~Date()
	{
		cout << "~Date()" << endl;//在析构函数内打印
	}

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

int main()
{
	Date d1(2022, 9, 6);//调用构造函数
	Date d2;//调用构造函数
	//Date d3();//没有调用构造函数,拷贝不出来对象
	Date d4(d1);//Date(&d4,Date& d1)

	return 0;
}

如果不用引用传参,而用指针传参,能不能实现?虽然可以实现,但是用指针就不是拷贝构造了,因为传的参数不是同类型对象,而是同类型对象的地址,所以调用时必须取地址:

#include
using namespace std;

class Date
{
public:
	//构造函数
	Date(int year = 2022, int month = 4, int day = 8)
	{
		_year = year;
		_month = month;
		_day = day;
	}

    //传指针
	Date(Date* d)//Date(Date* this, Date* d)
	{
		_year = d->_year;
		_month = d->_month;
		_day = d->_day;
	}

	//析构函数:清理资源
	~Date()
	{
		cout << "~Date()" << endl;//在析构函数内打印
	}

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

int main()
{
	Date d1(2022, 9, 6);//调用构造函数
	Date d2;//调用构造函数
	//Date d3();
	Date d4(&d1);//调用时必须取地址

	return 0;
}

因此,对于自定义类型的对象,一般推荐使用引用传参,虽然传值传参也可以,但是要调用拷贝构造,如f1使用传值传参,f2使用引用传参:

#include
using namespace std;

class Date
{
public:
	//构造函数
	Date(int year = 2022, int month = 4, int day = 8)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	//拷贝构造函数
	Date(Date& d)//Date(Date* this, Date& d)
				 //d4是this,       形参d是d1的引用,是d1内存的别名
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;

		cout << "call copy constructors" << endl;
	}

	//析构函数:清理资源
	~Date()
	{
		cout << "~Date()" << endl;//在析构函数内打印
	}

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

void f1(Date d)
{
	cout << "f1" << endl;
}

void f2(Date& d)
{

}

int main()
{
	Date d1(2022, 9, 6);//调用构造函数
	f1(d1);
	//f2(d1);
	
	return 0;
}

 发现f1使用传值传参时先调用了拷贝构造完成传参,再调用f1函数:

拷贝构造函数参数类型推荐加上const,是对形参权限的缩小,使得形参不能被修改,如果要修改参数的值。可以不加const。

2.编译器自动生成的拷贝构造函数

若未显式定义,系统会生成默认拷贝构造函数。 同构造函数和析构函数不同:

(1)拷贝构造函数对内置类型依次按照字节序完成拷贝,即浅拷贝或值拷贝。

假如不写拷贝构造函数,打印d1和d4:

#include
using namespace std;

class Date
{
public:
	//构造函数
	Date(int year = 2022, int month = 4, int day = 8)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	//拷贝构造函数
	//Date(Date& d)//Date(Date* this, Date& d)
	//			 //d4是this,       形参d是d1的引用,是d1内存的别名
	//{
	//	_year = d._year;
	//	_month = d._month;
	//	_day = d._day;
	//
	//	cout << "call copy constructors" << endl;
	//}

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

	//析构函数:清理资源
	~Date()
	{
		cout << "~Date()" << endl;//在析构函数内打印
	}

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

int main()
{
	Date d1(2022, 9, 6);//调用构造函数
	Date d4(d1);

    d1.Print();
	d4.Print();

	return 0;
}

 发现d1和d4都被打印了,d4构造成功了,完成了拷贝构造,说明我们没写拷贝构造函数,但是编译器自动生成了一个拷贝构造函数。

(2)对于自定义类型,不显式定义拷贝构造函数,会引发两个问题:

①调用析构函数时,这块空间被free了两次

②其中一个对象插入删除数据,都会导致另一个对象也插入删除了数据

以栈为例, 不显式定义拷贝构造函数,会引发程序崩溃:

#include 
#include 
using namespace std;

typedef int STDataType;

class Stack
{
public:
	//构造函数
	Stack(int capacity = 4)
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * 4);
		_size = 0;
		_capacity = capacity;
	}

	//析构函数:清理资源
	~Stack()
	{
		cout << this << endl;
		free(_a);
		_a = nullptr;
		_size = _capacity = 0;
	}

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

};

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

	return 0;
}

程序崩了:

栈的3个成员变量类型都是内置类型int,但栈是一个管理资源的类,st2虽然什么值都没给,但是调用了它的构造函数,构造函数给capacity了一个默认值4,构造函数会拿这个默认值去开辟空间,_a就指向了这块空间,由于没有显式定义拷贝构造函数,因此对象st2的成员变量_a拷贝的是st1的成员变量_a指针,即把st1的_a指针的值,拷贝给了st2的_a,那么两个指针的值是一样的,st1的_a和st2的_a指向同一块空间。

造成程序崩溃的原因:调用析构函数,这块空间被free了两次:后定义的先析构,st2先析构,free(_a)就把这块空间释放了,这块空间就被归还给了 *** 作系统,再把_a置空了。再析构st1时,free(_a)还要释放这块空间,同一块空间被释放了两次。

另外,由于共用同一块空间,st1和st2无论谁插入删除数据,都会导致对方也插入删除数据。

因此,像Stack这样的类,编译器默认生成的拷贝构造完成的是浅拷贝,不满足需求,需要我们自己实现深拷贝。

当没有显式定义拷贝构造函数时,对于内置类型成员会完成浅拷贝,对于自定义类型成员,会调用自定义类型的拷贝构造函数完成拷贝。

如下所示:

#include 
using namespace std;

class A
{
public:
	//构造函数
	A(int a = 1)
	{
		_a = a;
	}

	//拷贝构造函数
	A(const A& a)
	{
		cout << "A(const A& a)" << endl;
		_a = a._a;
	}

	//析构函数
	~A()
	{
		cout << "~A()" << endl;
	}
	
private:
	int _a;
};

class Date
{
public:
	//构造函数
	Date(int year = 2022, int month = 4, int day = 8)
	{
		_year = year;
		_month = month;
		_day = day;

        cout << "Date()" << endl;
	}

	//拷贝构造函数
	//Date(Date& d)//Date(Date* this, Date& d)
	//			 //d4是this,       形参d是d1的引用,是d1内存的别名
	//{
	//	_year = d._year;
	//	_month = d._month;
	//	_day = d._day;
	//
	//	cout << "call copy constructors" << endl;
	//}

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

	//析构函数:清理资源
	~Date()
	{
		cout << "~Date()" << endl;//在析构函数内打印
	}

private:
	int _year;
	int _month;
	int _day;

	A _a;
};

int main()
{
	Date d1(2022, 9, 6);//调用构造函数
	Date d4(d1);

	return 0;
}

经过监视和打印,发现d4被构造成功了,并且调用了A类的拷贝构造函数:

 

 总结:

(1)像Date这样的类,需要的是浅拷贝,那么默认生成的拷贝构造就够用了,不需要自己写。

(2)像Stack这样的类,需要深拷贝,因为浅拷贝会导致析构两次,程序崩溃等问题,需要自己写。

四、赋值运算符重载函数 1.运算符重载 

定义:内置类型,语言层面本就支持运算符,但是自定义类型,默认不支持运算符。C++运算符重载的目的是为了能够让自定义类型可以像内置类型一样使用运算符,需要哪个运算符,就重载哪个运算符。

运算符重载和函数重载,虽然都使用了重载,但是两者之间没有关联:

(1)函数重载时支持定义同名函数

(2)运算符重载是为了让自定义类型可以像内置类型一样去使用运算符。

 函数原型:返回值类型 operator *** 作符(参数列表)

特性:

(1)不能通过连接其他符号来创建新的 *** 作符:比如operator@ (2)重载 *** 作符必须有一个类类型或者枚举类型的 *** 作数 (3)用于内置类型的 *** 作符,其含义不能改变 (4)作为类成员的重载函数时,其形参看起来比 *** 作数数目少1个的成员函数 *** 作符有一个默认的形参this,默认为第一个形参 (5)".*" 、"::" 、"sizeof" 、"?:" 、"." 这5个运算符不能重载

(6)不能把运算符定义成全局,如果参数是私有成员变量就不能访问该参数了,要写成成员函数。

运算符重载实例: 

(1)==运算符重载

#include
using namespace std;

class Date
{
public:
	//构造函数
	Date(int year = 2022, int month = 4, int day = 8)
	{
		_year = year;
		_month = month;
		_day = day;
	}

//成员变量公有
public:
	int _year;
	int _month;
	int _day;
};

//operator==运算符重载
bool operator==(Date x1, Date x2)
{
	return x1._year == x2._year
		&& x1._month == x2._month
		&& x1._day == x2._day;
}

int main()
{
	Date d1(2022, 4, 12);
	Date d2(2022, 4, 13);

    //两种调用方式:
	//1.可读性不强
    operator==(d1, d2);

    //2.当编译器看到==自定义类型,会去检查日期类有没有==的重载运算符,如果有重载会转换成operator==(d1, d2)去调用operator==函数
	d1 == d2;

	return 0;
}

但以上写法只能针对成员变量属性是公有的情况,如果成员变量属性是私有,全局的operator==定义就会因无法访问成员变量而报错。但把operator==定义成成员函数就会只有一个显式的参数(d2),并且调用方式也会发生变化:

#include
using namespace std;

class Date
{
public:
	//构造函数
	Date(int year = 2022, int month = 4, int day = 8)
	{
		_year = year;
		_month = month;
		_day = day;
	}

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

//成员变量私有
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2022, 4, 12);
	Date d2(2022, 4, 13);

    //两种调用方式:
    //1.调用方式相对于全局 *** 作符重载函数发生了变化,编译器会把调用转换成d1.operate==(&d1,d2)
	d1.operator==(d2);

    //2.先在全局看有没有operator==函数,如果没有就会去类里面重载成d1.operate==(&d1,d2)
	d1 == d2;

	return 0;
}

(2)其他运算符重载请见下一篇文章【C++】-- 实现Date类的各种运算符重载

2.赋值运算符重载 

赋值运算符重载和拷贝构造的区别:

(1)拷贝构造创建了一个之前不存在的对象,是用同类对象初始化的拷贝。

(2)定义:赋值运算符重载虽然也是拷贝行为,但是拷贝时该对象已经存在且被初始化,现在把一个对象赋值拷贝给另一个对象。

如下是拷贝构造还是赋值运算符重载呢?

Date d6 = d1;

现在定义d6,说明之前d6不存在,因此是拷贝构造。 

特性: 

(1)参数类型:返回引用

(2)返回值:返回*this

(3)检测:需要判断是否自己给自己赋值

(4)一个类如果没有显式定义赋值运算符重载,编译器也会默认生成,完成浅拷贝

  

现在想实现将d2的值赋值给d1:

int main()
{
	Date d1(2022, 9, 6);//调用构造函数
	Date d2;

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

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

	return 0;
}

赋值运算符重载函数应该怎么写呢?如果这么写的话,传值需要调拷贝构造,调完拷贝构造再执行operator=这个函数,很麻烦:

//d1 = d2
void operator=(Date d)
{

}

因此要用传引用

    //赋值运算符重载函数
	//d1 = d2; //d1.operator=(&d1,d2)
	void operator=(const Date& d)//void operator=(Date* this,const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

 假如有这样的连续赋值场景,会从右向左结合,两次调用赋值运算符重载:

d1 = d2 = d3;

对于以下表达式,将k先赋值给j,j = k的返回值是j,再将j赋值给i,最后表达式的返回值是i:

i = j = k;

同理,执行d2 = d3时,d2作为this,d3作为d,返回值是d2,要返回d2的值,只需要返回*this即可:

    //赋值运算符重载函数
	//d1 = d2; //d1.operator=(&d1,d2)
	Date operator=(const Date& d)//void operator=(Date* this,const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;

		return *this;
	}

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

	//析构函数:清理资源
	~Date()
	{
		cout << "~Date()" << endl;//在析构函数内打印
	}

不过对于上面的传值返回Date operator=(const Date& d),会生成一个临时对象,会把*this拷贝给临时对象,再拿临时对象作为表达式的返回值,但是用*this构造初始化另外一个对象,会调用拷贝构造函数,为了验证会调用拷贝构造函数,将拷贝构造函数放开:

#include 
using namespace std;

class A
{
public:
	//构造函数
	A(int a = 1)
	{
		_a = a;
	}

	//拷贝构造函数
	A(const A& a)
	{
		cout << "A(const A& a)" << endl;
		_a = a._a;
	}

	//析构函数
	~A()
	{
		cout << "~A()" << endl;
	}
	
private:
	int _a;
};

class Date
{
public:
	//构造函数
	Date(int year = 2022, int month = 4, int day = 8)
	{
		_year = year;
		_month = month;
		_day = day;

		cout << "Date()" << endl;
	}

	//拷贝构造函数
	Date(Date& d)//Date(Date* this, Date& d)
				 //d4是this,       形参d是d1的引用,是d1内存的别名
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	
		cout << "call copy constructors" << endl;
	}
	
    //赋值运算符重载函数
	//d1 = d2; //d1.operator=(&d1,d2)
	Date operator=(const Date& d)//void operator=(Date* this,const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;

		return *this;
	}

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

	//析构函数:清理资源
	~Date()
	{
		cout << "~Date()" << endl;//在析构函数内打印
	}

private:
	int _year;
	int _month;
	int _day;

	A _a;
};

int main()
{
	Date d1(2022, 9, 6);//调用构造函数
	Date d2;
	Date d3;

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

	d1 = d2 = d3;

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

	return 0;
}

发现调用了拷贝构造函数: 

为了减少拷贝构造,传引用返回有一个前提,如果出了函数作用域,引用还在,就可以使用传引用返回,现在出了复制运算符重载函数,*this还在,因为*this是d2,d2是在main函数中创建的,生命周期在赋值运算符重载函数外面,出了赋值运算符重载函数,d2还在,因此将Date operator=(const Date& d)改为使用引用返回就不会调拷贝构造

    //赋值运算符重载函数
	//d1 = d2; //d1.operator=(&d1,d2)
	Date& operator=(const Date& d)//void operator=(Date* this,const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;

		return *this;
	}

没有调用拷贝构造函数: 

 现在还需要检查可能的误 *** 作,比如对象是否自己给自己赋值,如下面代码:

d1 = d1;

赋值运算符重载函数可以加一个判断:

    //d1 = d2; //d1.operator=(&d1,d2)
	Date& operator=(const Date& d)//void operator=(Date* this,const Date& d)
	{

		if (this != &d)//对d取地址,判断this的值和d的地址是否相同,如果不是自己给自己赋值,才需要拷贝
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
		
		return *this;
	}

就算我们不写赋值运算符,编译器也会默认生成,同拷贝构造一样:

(1)对于内置类型,会完成浅拷贝,比如Date类不需要我们写赋值运算符重载,Stack类需要自己写。

(2)对于自定义类型,会调用自定义类型的赋值运算符重载完成拷贝。

3.const修饰类的成员函数

 定义:将const修饰的类成员函数称为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。

比如有如下场景:假如把Date类的operator==运算符重载函数写错了

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

将其中的一个"=="错写成"=" :

bool operator==(const Date& d) //bool operator==(Date* this,const Date& d)
{
	return (_year == d._year)
		&& (_month = d._month)
		&& (_day == d._day);
}

虽然编译没有问题,但是这会导致this的值被修改了,并且执行结果也错误:

int main()
{
	Date d1(2022, 9, 6);
	Date d2(2022, 3, 6);

	cout << (d1 == d2) << endl;
	d1.Print();
	d2.Print();

	return 0;
}

这不符合要求,仅仅是比较而已,但是被比较对象的值却被修改了。const最大的作用是保护对象和变量,d2传给了d,d是d2的别名,const已经保护了d,那d1如何保护呢?由于this是隐含的,那么const为了保护this,应该如何加?把const加在成员函数的后面,叫做const修饰成员函数

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

现在编译就会报错了:

这里const修饰的是*this,函数中不小心改变的成员变量,编译时就会被检查出来,在成员函数中,如果不需要修改成员变量的成员函数,建议都加上const。

思考:

(1) const对象可以调用非const成员函数吗?

          不可以,因为this指针是const的,传到非const形参中,是权限放大,不允许
(2)非const对象可以调用const成员函数吗?

          可以,因为this指针是非const的,传到const形参中,是权限缩小,允许
(3)const成员函数内可以调用其它的非const成员函数吗?

         不可以,因为this指针是const的,传到非const形参中,是权限放大,不允许
(4)非const成员函数内可以调用其它的const成员函数吗?

         可以,因为this指针是非const的,传到const形参中,是权限缩小,允许 

五、取地址 *** 作符重载和const取地址 *** 作符重载

这两个 *** 作符一般不需要重载,编译器默认生成的已经够用,重载没有价值。 

#include 
using namespace std;

class Date
{
public:
	//构造函数
	Date(int year = 2022, int month = 4, int day = 8)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	//取地址 *** 作符重载--取对象地址
	Date* operator&()
	{
		return this;
	}

	//const取地址 *** 作符重载--取const对象地址
	const Date* operator&() const
	{
		return this;
	}

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

int main()
{
	Date d1(2022, 9, 6);
	Date d2(2022, 3, 6);

	cout << &d1 << endl;
	cout << &d2 << endl;

	return 0;
}

只是取地址而已: 

六、总结  1.构造函数和析构函数

如果我们不写,编译器对内置类型不做处理,自定义类型会调用它的构造函数和析构函数进行处理

2.拷贝构造和赋值运算符重载 

如果我们不写,内置类型会完成浅拷贝,自定义类型会调用它的拷贝构造函数和赋值运算符重载函数。

3.取地址 *** 作符重载和const取地址 *** 作符重载

一般不需要重载,编译器默认生成的已经够用,重载没有价值。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/676116.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-19
下一篇 2022-04-19

发表评论

登录后才能评论

评论列表(0条)

保存