【C++】类的默认成员函数



前言


在C++的学习中,相信大部分人在构造函数的这块,学得不是很清楚,可能会使用,但不太懂它的一些规则,这篇文章,我会尽量的讲得简单易懂一点。但在看这篇文章之前,需要对类和对象有一个简单的认识,起码要能知道this指针是什么。不懂的可以看看这篇博客:《类与对象

首先,我们先来见一见构造函数是什么样子的,先见一见它的庐山真面目,我们才能对它进行深刻的理解和学习:
在这里插入图片描述
这里我写了一个日期类来进行讲解,请大家记住我在上图中用括号括起来的字,后面我们会对上面的话有一个全新的理解!!


1. 类的6个默认成员函数


1.1 概念

默认成员函数:用户没有显示实现编译器会自动生成的成员函数称为默认成员函数。
在这里插入图片描述
我们在上面只展示了其中一个默认成员函数,就是默认构造函数。但本章会将以上6个默认成员函数都讲完,并详细讲解它们的区别。当然重点肯定是讲构造函数,因为构造函数是最难理解的,理解了构造函数以后,其他的基本都可以会了


2. 构造函数


2.1 构造函数的概念

我们在前言部分已经初步的了解了构造函数的样子,但我们还不能说完全的理解它了。从前面不难看出,它与我们平时学的其他函数有很大的差异。

我们先来谈谈,为什么会有构造函数的这个概念。先看下面的代码:

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;//定义一个Date类型的对象
	//下面这两个函数都是我们定义在类里面的函数,也称为类的成员函数
	d1.Init(2022, 7, 5);//对d1对象进行初始化
	d1.Print();//打印d1对象的内容

	Date d2;//再定义一个Date类型的对象
	d2.Init(2022, 7, 6); //对d2对象进行初始化
	d2.Print();//打印d1对象的内容
	return 0;
}

对于Date类,可以通过 Init 公有方法给对象设置日期,但如果每次创建对象时都调用该方法设置信息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢?

所以在这里概念就来了:
构造函数是一个特殊的成员函数名字与类名相同, 创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。

2.2 构造函数的特性

构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。初始化对象这个操作主要是在初始化列表内进行,关于初始化列表,我们在下面会说到。

其特征如下:

  1. 函数名与类型相同。

  2. 无返回值。(这里的无返回值指的是不用写它的返回值,而不是说它的返回值是void,从前言部分中的默认构造函数中也能看出来)

  3. 对象实例化时编译器自动调用对应的构造函数。

  4. 构造函数可以重载。
    在这里插入图片描述

  5. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。 注意:无参构造函数 全缺省构造函数我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。 那么,如果需要传参的构造函数,我们就叫做构造函数,或者叫做其他构造函数(之所以这样叫,是因为下面有一个叫做拷贝构造的概念)。反正我们需要区分开,需要传参的构造函数不是默认构造函数!!!

  6. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数一旦用户显式定义构造函数(需要传参的构造函数)编译器将不再生成在这里插入图片描述
    这张图上的信息量有点大,请耐心看完。

  7. 关于编译器生成的默认成员函数,很多人会有疑惑:不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用。d对象调用了编译器生成的默认构造函数,但是d对象的成员_year_month_day,依旧是随机值。也就说在这里编译器生成的默认构造函数并没有什么用??
    解答:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:int /char…,自定义类型就是我们使用class / struct /union等自己定义的类型,看看下面的程序,就会发现编译器生成默认的构造函数会对自定类型成员_t调用的它的默认成员函数。
    在这里插入图片描述
    从这个例子我们可以看出,编译器生成的默认构造函数,对内置类型不处理对自定义类型会去调用该自定义类型的默认构造函数
    当然,其实在这里这个设计其实并不好,再怎么也应该对内置类型进行一下处理,所以C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。
    在这里插入图片描述
    可能会有人有疑问:为什么这个设计不好,不直接改了,而要在这里打一个补丁了,这样不是更麻烦了吗?
    其实在语言的发展史中,都要秉承着向前兼容的基本素养,尤其是C++这种被广泛使用的编程语言,如果对它之前的语法进行修改,很有可能造成其他人写好的代码发生bug。那么有人会说,Python2的语法放在Python3中也有可能会引发错误啊。这个估计是因为Python2使用的人并不多,所以Python就直接对它的语法进行了大更新,而在未来,Python升级为更高的版本,也应该不会再出现对Python3语法不兼容的这种情况了(不然得被很多人喷死)。

2.3 初始化列表

构造函数赋值

在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。

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

虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化,构造函数体中的语句只能将其称为赋初值,而不能称作初始化因为初始化只能初始化一次,而构造函数体内可以多次赋值

在上面的例子中,我一直没有引入初始化列表的概念,是因为我想先把构造函数默认构造函数的区别先讲清楚,那么到这里,如果理解了上面我所讲述的东西,那么接下来也会好理解了。

初始化列表

以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式

在前言部分我们也有见过,既然都看到这里了,也不让你重新划上去看了
在这里插入图片描述
⭐️:
所以现在就可以理解我之前为什么请你先记住我括号里面的字了吧。默认构造函数不用再多说了,我们前面已经花了大量篇幅去强调默认构造函数的概念。
而初始化列表,它是用来对类的成员进行初始化的。在函数体内进行的操作是对类的成员进行赋值。而我们在上面这里也讲了构造函数的赋值初始化列表的区别:初始化列表只能初始化一次,而构造函数体内可以多次赋值
【注意⚠️】

  1. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
  2. 类中包含以下成员,必须放在初始化列表位置进行初始化:
    • 引用成员变量
      在这里插入图片描述

    • const成员变量
      在这里插入图片描述

    • 自定义类型成员(且该类没有默认构造函数时)
      在这里插入图片描述
      那么基于这三种情况的解决方案如下(这里我默认将以上三个成员写在一个类里面):
      在这里插入图片描述
      我们这样去做就不会报错了。

  3. 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
    在这里插入图片描述
  4. 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关,我们来看下面这串代码:
class A
{
public:
	A(int a)
		:_a1(a)
		,_a2(_a1)
	{}

	void Print() {
		cout << _a1 << " " << _a2 << endl;
	}
private:
	int _a2;
	int _a1;
};
int main() {
	A aa(1);
	aa.Print();
}

A. 输出1  1
B.程序崩溃
C.编译不通过
D.输出1  随机值

在这里插入图片描述

explicit关键字

在讲这个之前,我们要先理解一下什么是隐式类型转换
在这里插入图片描述

我们在进行不同类型的变量定义或者赋值时,都会产生一个临时变量。那么怎么来验证有这个过程呢?

在这里插入图片描述
我们可以看到,我们这样写就出错了,因为i在转换成double类型的时候有一个临时变量,而这个临时变量是具有常性的,不可被修改,这里就涉及了权限放大的问题,那我们可以写成这样:
在这里插入图片描述
这样就证明出了,在进行隐式类型转换时,会先生成一个临时变量,而这个临时变量是具有常性的。不仅内置类型有这一规则,自定义类型也有这个规则。
在这里插入图片描述

在这里插入图片描述
自定义类型的隐式类型转换是由构造函数来实现的。

构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用。

class Date
{
public:
	// 1. 单参构造函数,没有使用explicit修饰,具有类型转换作用
// explicit修饰构造函数,禁止类型转换---explicit去掉之后,代码可以通过编译
	explicit Date(int year)
		:_year(year)
	{}
	/*
	// 2. 虽然有多个参数,但是创建对象时后两个参数可以不传递,没有使用explicit修饰,具有类型转换作用
	// explicit修饰构造函数,禁止类型转换
	// C++98不支持多参数传参,C++11支持,如:Date A = {1970,1,1};
	explicit Date(int year, int month = 1, int day = 1)
	: _year(year)
	, _month(month)
	, _day(day)
	{}
	*/
	Date& operator=(const Date& d)
	{
		if (this != &d)
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
		return *this;
	}
private:
	int _year;
	int _month;
	int _day;
};
void Test()
{
	Date d1(2022);
	// 用一个整形变量给日期类型对象赋值
	// 实际编译器背后会用2023构造一个无名对象,最后用无名对象给d1对象进行赋值
	d1 = 2023;
	// 将1屏蔽掉,2放开时则编译失败,因为explicit修饰构造函数,禁止了单参构造函数类型转换的作用
}

上述代码可读性不是很好,用explicit修饰构造函数,将会禁止构造函数的隐式转换


3. 拷贝构造函数


3.1 拷贝构造函数的概念

在创建对象时,我们可以用一个已存在的对象来创建一个一模一样的新对象。

拷贝构造函数: 只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。

3.2 拷贝构造函数的特性

拷贝构造函数也是特殊的成员函数,其特征如下:

  1. 拷贝构造函数是构造函数的一个重载形式(其实也就是构造函数的一种,我们在前面也强调过)。
  2. 拷贝构造函数的参数只有一个必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
    在这里插入图片描述
    因为形参是实参的一份临时拷贝,我们每次给函数传值传参的时候,都会先去调用拷贝构造函数去初始化一个对象给我们调用的函数(C++编译器会去这么做)。
    如果我们在创建拷贝构造函数时,使用传值传参的方式去创建的话,那么就会导致下面这种情况:调用拷贝构造函数时,因为需要传值过去,所以需要调用拷贝构造函数去拷贝一份临时对象…以此循环往复,就造成了无穷递归
    在这里插入图片描述
  3. 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
class Time
{
public:
	Time()
	{
		_hour = 1;
		_minute = 1;
		_second = 1;
	}
	Time(const Time& t)
	{
		_hour = t._hour;
		_minute = t._minute;
		_second = t._second;
		cout << "Time::Time(const Time&)" << endl;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
private:
	// 基本类型(内置类型)
	int _year = 1970;
	int _month = 1;
	int _day = 1;
	// 自定义类型
	Time _t;
};
int main()
{
	Date d1;
	// 用已经存在的d1拷贝构造d2,此处会调用Date类的拷贝构造函数
	// 但Date类并没有显式定义拷贝构造函数,则编译器会给Date类生成一个默认的拷贝构造函数
	Date d2(d1);
	return 0;
}

注意:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。

  1. 编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显式实现吗?当然像日期类这样的类是没必要的。那么下面的类呢?验证一下试试?
// 这里会发现下面的程序会崩溃掉?这里就需要深拷贝去解决。
typedef int DataType;
class Stack
{
public:
	Stack(size_t capacity = 10)
	{
		//为Stack类的成员_array创建一块空间
		_array = (DataType*)malloc(capacity * sizeof(DataType));
		if (nullptr == _array)
		{
			perror("malloc申请空间失败");
			return;
		}
		_size = 0;
		_capacity = capacity;
	}
	void Push(const DataType& data)
	{
		// CheckCapacity();
		_array[_size] = data;
		_size++;
	}
	~Stack()//这个叫析构函数,我们在下面会讲到
	{
		if (_array)
		{
			free(_array);
			_array = nullptr;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	DataType* _array;
	size_t _size;
	size_t _capacity;
};
int main()
{
	Stack s1;
	s1.Push(1);
	s1.Push(2);
	s1.Push(3);
	s1.Push(4);
	//这里如果我们如果使用编译器默认生成的拷贝构造函数的话,
	//那么s2对象的成员_array与s1对象的成员_array指向的就是同一块空间
	Stack s2(s1);
	//而程序结束调用析构函数时,s2和s1的_array指向的空间就会被释放两次,
	//所以程序会崩溃
	//(如果不懂析构函数没关系,我们在下面会讲到,这里你只要明白,它会释放我们创建的对象的空间就足够了)
	return 0;
}

关于对象s1和对象s2的分析如下图:
在这里插入图片描述
注意:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝

  1. 拷贝构造函数的典型调用场景:
    • 使用已存在对象创建新对象
    • 函数参数类型为类类型对象
    • 函数返回值类型为类类型对象
class Date
{
public:
	Date(int year, int month, int day)
	{
		cout << "Date(int,int,int):" << this << endl;
	}
	Date(const Date& d)
	{
		cout << "Date(const Date& d):" << this << endl;
	}
	~Date()
	{
		cout << "~Date():" << this << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
Date Test(Date d)
{
	Date temp(d);
	return temp;
}
int main()
{
	Date d1(2022, 1, 13);
	Test(d1);
	return 0;
}

在这里插入图片描述
为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用尽量使用引用。

  1. 拷贝构造函数和普通构造函数与默认构造函数的性质是相似的,对于所有的构造函数它都是可以在初始化列表对类的成员进行初始化的
    在这里插入图片描述


4. 运算符重载


4.1 运算符重载的概念

我们在对两个已经实例化过了的对象可以进行=赋值操作。=+-…这些运算符,我们知道他是对于程序中的内置类型(intchardouble等类型)进行运算使用的,一般我们自定义的类型对于+-*/等这些内置类型使用的操作符的使用是不确定的。
比方说,一个日期类里面,(day1 + day2)两个日期相加是没有任何意义的,但是两个日期相减(得到两日期相差的天数),一个日期加一个天数(得到另一个日期)。这里的天数都可以用int型来表示。

那么编译器它也不会智能到会知道我们创建一个类型要进行的+-*/等操作具体是应该怎么实现的。
而如果我们要专门去创建一些函数对这些运算操作符进行操作的话,未免会使得我们的代码过于冗余且可读性差。

所以这时候C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。

函数名字为:关键字operator后面接需要重载的运算符符号。

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

注意⚠️:

  • 不能通过连接其他符号来创建新的操作符:比如operator@
  • 重载操作符必须有一个类类型参数
  • 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
  • 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this指针
  • .* :: sizeof ?: . 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。 (第一个操作符没见过很正常,我们记一下就行,平时也不会用到)

那么我们就来看一下运算符重载具体是怎么使用的吧:

// 全局的operator==
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;
}
void Test()
{
	Date d1(2018, 9, 26);
	Date d2(2018, 9, 27);
	cout << (d1 == d2) << endl;
}
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& d2)
	// 这里需要注意的是,左操作数是this,指向调用函数的对象
	bool operator==(const Date & d2)
	{
		return _year == d2._year
			&& _month == d2._month
			&& _day == d2._day;
	}
private:
	int _year;
	int _month;
	int _day;
};

我们再来看一个比较复杂的运算符重载:

前置++(- -)和后置++(- -)

说它复杂其实也就是语法规定它们俩之间有一个没有什么关系的区别,为什么这样说呢?我们往下看:

class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	// 前置++:返回+1之后的结果
	// 注意:this指向的对象函数结束后不会销毁,故以引用方式返回提高效率
	Date& operator++()
	{
		_day += 1;//这里我们假设已经对+=进行了重载
		//这里的返回值是Date&,所以不需要创建一个临时变量来充当返回值
		return *this;
	}
	// 后置++:
	// 前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确重载
	// C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传递
	// 注意:后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将this保存一份,然后给this + 1
	//而temp是临时对象,因此只能以值的方式返回,不能返回引用
	Date operator++(int)
	{
		Date temp(*this);//拷贝构造tmp
		_day += 1;
		//因为temp是一个在这个函数中的临时变量,出了作用域就会被销毁,
		//所以这里在返回结果时会创建一个临时变量来存储tmp的数据,来返回我们调用这个函数的地方
		return temp;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d;
	Date d1(2022, 1, 13);
	d = d1++;// d: 2022,1,13   d1:2022,1,14
	d = ++d1; // d: 2022,1,15   d1:2022,1,15
	return 0;
}

从上面这串代码我们可以看出后置++(- -)的效率是要比前置++(- -)的效率要,因为后置++(- -)在返回时,会产生两次临时变量(一次是创建tmp时进行拷贝构造,一次是返回tmp时进行了拷贝构造)。而前置++(- -)则不需要进行其他什么构造。

4.2 赋值运算符重载

我们在讲一个类有6默认成员函数时有提到,其中一个默认成员函数就是赋值重载,而在这之前,我们也讲了关于操作符重载的知识,为的就是我们能够来对赋值操作符重载的理解,认真看完上面的内容,接下来的讲解理解起来也会很轻松!

  1. 赋值运算符重载格式
    • 参数类型: const T&,传递引用可以提高传参效率。
    • 返回值类型: T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值。
    • 检测是否自己给自己赋值。
    • 返回*this 要符合连续赋值的含义。
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;
	}
	//赋值操作符重载
	Date& operator=(const Date& d)
	{
		if (this != &d)
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}

		return *this;
	}
private:
	int _year;
	int _month;
	int _day;
};
  1. 赋值运算符只能重载成类的成员函数不能重载成全局函数
class Date
{
public:
 Date(int year = 1900, int month = 1, int day = 1)
 {
 _year = year;
 _month = month;
 _day = day;
 }
 int _year;
 int _month;
 int _day;
};
// 赋值运算符重载成全局函数,注意重载成全局函数时没有this指针了,需要给两个参数
Date& operator=(Date& left, const Date& right)
{
 if (&left != &right)
 {
 left._year = right._year;
 left._month = right._month;
 left._day = right._day;
 }
 return left;
}
// 编译失败:
// error C2801: “operator =”必须是非静态成员

原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。
在这里插入图片描述
3. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。

class Time
{
public:
	Time()
	{
		_hour = 1;
		_minute = 1;
		_second = 1;
	}
	Time& operator=(const Time& t)
	{
		//这里判断是否是给自己赋值,如果是,那么就跳过下面这些步骤,直接返回*this
		if (this != &t)
		{
			_hour = t._hour;
			_minute = t._minute;
			_second = t._second;
		}
		return *this;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
private:
	// 基本类型(内置类型)
	int _year = 1970;
	int _month = 1;
	int _day = 1;
	// 自定义类型
	Time _t;
};
int main()
{
	Date d1;
	Date d2;
	d1 = d2;
	return 0;
}

既然编译器生成的默认赋值运算符重载函数已经可以完成字节序的值拷贝了,还需要自己实现吗?当然像日期类这样的类是没必要的。那么下面的类呢?验证一下试试?

// 这里会发现下面的程序会崩溃掉?这里就需要深拷贝去解决。
typedef int DataType;
class Stack
{
public:
	Stack(size_t capacity = 10)
	{
		_array = (DataType*)malloc(capacity * sizeof(DataType));
		if (nullptr == _array)
		{
			perror("malloc申请空间失败");
			return;
		}
		_size = 0;
		_capacity = capacity;
	}
	void Push(const DataType& data)
	{
		// CheckCapacity();
		_array[_size] = data;
		_size++;
	}
	~Stack()
	{
		if (_array)
		{
			free(_array);
			_array = nullptr;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	DataType* _array;
	size_t _size;
	size_t _capacity;
};
int main()
{
	Stack s1;
	s1.Push(1);
	s1.Push(2);
	s1.Push(3);
	s1.Push(4);
	Stack s2;
	s2 = s1;
	return 0;
}

注意⚠️:如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现。
在这里插入图片描述

4.3 拷贝构造与赋值运算符重载的区别

注意了,这里我要讲一个比较容易搞混淆的概念。我们先来见一见下面这个代码:

class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "A(int a)" << endl;
	}
	A(const A& aa)
		:_a(aa._a)
	{
		cout << "A(const A& aa)" << endl;
	}
	A& operator=(const A& aa)
	{
		cout << "A& operator=(const A& aa)" << endl;
		if (this != &aa)
		{
			_a = aa._a;
		}
		return *this;
	}
private:
	int _a;
};
int main()
{
	A aa1(1);   // 构造
	// 一个已经存在的对象拷贝初始化另一个要创建的对象,就是拷贝构造
	// 他们两是等价的
	A aa2(aa1);  // 拷贝构造
	A aa3 = aa1; // 拷贝构造 or 赋值拷贝
	// 两个已经存在的对象拷贝,赋值拷贝
	aa2 = aa3;
	return 0;
}

在这串代码的main函数中,我们可以看到aa1是构造函数创建出来的没错,aa2是拷贝构造函数创建出来的也能看懂,在后面aa3将值赋值给aa2也可以理解。那么A aa3 = aa1;这句代码是拷贝构造还是赋值拷贝呢?
在这里插入图片描述
通过调试我们可以看到A aa3 = aa1;这句代码是一个拷贝构造

所以这里总结一下:
已经实例化的两个对象相互使用=时,表示的是赋值运算符重载,而一个已经实例化了的对象对另一个还未进行实例化的对象使用=时,这时候表示的是拷贝构造。至于说为什么会这样,都是语法规定了,这些也是为什么说C++比较难学的原因!!


5. 析构函数


好了,到这里本章最难的部分已经全都讲解完了,接下来的部分就比较简单了

5.1 析构函数的概念

通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没呢的?

析构函数: 与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作

5.2 析构函数的特性

析构函数是特殊的成员函数,其特征如下:

  1. 析构函数名是在类名前加上字符 ~
  2. 无参数,无返回值。(这里的无返回值和构造函数的特性是相似的)
  3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
  4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。

这些我们在前面的代码举例中也看到过,那么现在我们就来好好学习一下这个析构函数吧:

typedef int DataType;
class Stack
{
public:
	Stack(size_t capacity = 3)
	{
		_array = (DataType*)malloc(sizeof(DataType) * capacity);
		if (NULL == _array)
		{
			perror("malloc申请空间失败!!!");
			return;
		}
		_capacity = capacity;
		_size = 0;
	}
	void Push(DataType data)
	{
		// CheckCapacity();
		_array[_size] = data;
		_size++;
	}
	// 其他方法...
	~Stack()//析构函数
	{
		if (_array)
		{
			free(_array);
			_array = NULL;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	DataType* _array;
	int _capacity;
	int _size;
};
void TestStack()
{
	Stack s;
	s.Push(1);
	s.Push(2);
}
  1. 关于编译器自动生成的析构函数,是否会完成一些事情呢?下面的程序我们会看到,编译器生成的默认析构函数,对自定类型成员调用它的析构函数。
    在这里插入图片描述
    我们可以看到程序运行出来的结果是~Time()。而在main方法中根本没有直接创建Time类的对象,为什么最后会调用Time类的析构函数?

因为:main方法中创建了Date对象d,而d中包含4个成员变量,其中_year, _month, _day三个是内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可;而_tTime类对象,所以在d销毁时,要将其内部包含的Time类的_t对象销毁,所以要调用Time类的析构函数。但是:main函数中不能直接调用Time类的析构函数,实际要释放的是Date类对象,所以编译器会调用Date类的析构函数,而Date没有显式提供,则编译器会给Date类生成一个默认的析构函数目的是在其内部调用Time类的析构函数,即当Date对象销毁时,要保证其内部每个自定义对象都可以正确销毁。main函数中并没有直接调用Time类析构函数,而是显式调用编译器为Date类生成的默认析构函数。注意:创建哪个类的对象则调用该类的析构函数,销毁那个类的对象则调用该类的析构函数

  1. 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。


6. 取地址及cost取地址操作符重载


这两个默认成员函数一般不用重新定义,编译器默认会生成。

class Date
{
public:
	Date()
		:_year(1970)
		,_month(1)
		,_day(1)
	{}
	Date* operator&()
	{
		return (Date*)0x00000000;
	}
	//这里的const修饰的是*this
	//this指针的类型默认是Date* const this
	//这里加了const以后就变成了const Date* const this
	const Date* operator&()const
	{
		return (Date*)0x11223344;
	}
private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
};
int main()
{
	Date d1;
	printf("%p\n", &d1);
	const Date d2;
	printf("%p\n", &d2);
	return 0;
}

这两个运算符一般不需要重载,使用编译器生成的默认地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容
在这里插入图片描述

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Hyt的笔记本

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值