类和对象【2】默认成员函数

文章目录

  • 引言
  • 构造函数
    • 定义
    • 默认构造函数及相关问题
  • 析构函数
  • 拷贝构造
    • 定义
    • 使用时可能引发的问题
  • 赋值运算符重载
    • 运算符重载
    • 赋值运算符重载
  • 总结

引言

在上一篇文章中,初步介绍了类和对象:戳我看初识类和对象
不难发现,类类型极大的方便了用户的使用以及与对象之间的交互。比如我们之前实现的链表、栈和队列等。

对于内置类型,有操作符可以实现初始化、赋值、等操作。但是对于自定义类型,要实现初始化、赋值、销毁等操作就需要我们通过函数去实现了。但是每个类都实现并且使用时显式调用这些基础普遍的函数是很麻烦的,所以在一个类类型被创建时,会自动生成6个默认成员函数:构造函数、析构函数、拷贝构造、赋值重载、取地址与const取地址,并且在需要用到这些功能的地方自动调用。
类和对象【2】默认成员函数_第1张图片

本篇文章重点介绍前四个默认成员函数:

构造函数

构造函数的函数名与类名相同,它的作用是初始化类对象。
构造函数会在类对象创建时由编译器自动调用,以保证每个类对象都有一个合适的初始值。在每个对象的生命周期内只调用一次构造函数。

定义

构造函数的函数名与类名相同,无返回值,支持函数重载与缺省参数
例如上一篇文章中简单写的日期类Date的构造函数:

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

int main()
{
	Date d1(2023, 2, 10);
	return 0;
}

在这里插入图片描述

默认构造函数及相关问题

构造函数虽然是自动调用,但需要我们去实现,如果没有实现,编译器会生成一个无参的默认构造函数。
默认构造函数是无参或全缺省的构造函数,对于存在默认构造函数的类类型,在创建类对象时就不需要传递参数(像上例中那样传参)。

需要注意的是:当定义了一个构造函数后,编译器就不会生成无参的默认构造函数。所以如果在上例中不给初始值的创建类对象就会报错:

int main()
{
	Date d1(2023, 2, 10);//正确代码:给初始值会调用有参的构造函数
	//Date d2; 错误代码:无合适的默认构造函数可用
	return 0;
}

类和对象【2】默认成员函数_第2张图片
这种情况下,我们就可以将这个带参的构造函数定义为全缺省的默认构造函数,这样在调用时就可以不用给初始值而使用默认构造的缺省参数值:

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

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

int main()
{
	Date d1(2023, 2, 10);
	Date d2;
	return 0;
}

类和对象【2】默认成员函数_第3张图片
那么,编译器生成的无参的默认构造函数会实现什么效果呢?
对于内置类型,这个无参的默认构造不能实现给初始值,对于自定义类型会调用其自身的默认构造函数。
这就导致这个自动生成的默认构造很鸡肋,并不能实现我们初始化类对象的目的。所以在C11标准中,我们可以在类对象中声明变量时就给一个默认值,这个默认值会在编译器中生成的默认构造中用于初始化类对象(这个默认值其实是在构造函数的初始化列表中利用的,后续会讲到):

class Date
{
public:
private:
	int _year = 2023;
	int _month = 2;
	int _day = 10;
};

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

在这里插入图片描述

析构函数

析构函数在对象生命周期结束时被自动调用,用于销毁类对象中的资源。(注意这里是销毁对象中的资源,而不是对象本身)
对于类类型中的资源全部是内置类型,没有申请空间,则编译器会在该类对象生命周期结束后自动释放这块空间;而对于有申请空间的类,就必须要实现析构函数去释放资源的空间了:

析构函数的函数名为~类名,无返回值,不能重载。
如这个栈对象的析构:

class Stack
{
public:
	Stack()
	{
		_top = 0;
		_capacity = 10;
		_data = new int[_capacity] {0}; //new动态申请空间
	}
	~Stack()
	{
		_top = 0;
		_capacity = 0;
		delete[] _data; //delete释放new申请的空间
	}
private:
	int* _data;
	int _top;
	int _capacity;
};
int main()
{
	Stack s1;
	return 0;
}

类和对象【2】默认成员函数_第4张图片
当然,当没有实现析构函数时,编译器会自动生成一个析构函数。这个默认生成的析构函数对于内置类型当然不做处理,对于自定义类型就会调用其自身的析构函数。

拷贝构造

定义

拷贝构造是构造函数的一种重载形式,只有单个形参,即本类类型的引用(一般用const修饰,以可以传常量)。
在用一个类对象初始化创建同类型新类对象时编译器自动调用(即在任何需要拷贝类对象的时候):

class Date
{
public:
	Date(int year = 2023, int month = 2, int day = 10)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
private:
	int _year = 2023;
	int _month = 2;
	int _day = 10;
};
int main()
{
	Date d1;
	Date d2(d1);
	return 0;
}

类和对象【2】默认成员函数_第5张图片

使用时可能引发的问题

  1. 拷贝构造函数的参数必须传引用,不能传值
    拷贝构造函数会在发生类对象拷贝操作时自动调用,除了上面提到的用一个类对象初始化一个新对象之外,还有值传参时的拷贝,返回值时的拷贝,强制类型转换时的拷贝等。
    如果在实现拷贝构造函数时使用值传递,就会发生要拷贝调拷贝构造,拷贝构造时又会发生拷贝行为,再调,又发生拷贝行为…… 就会无限调用下去,所以拷贝构造函数必须传引用。

  2. 编译器自动生成的拷贝构造产生的问题
    当然,如果不实现拷贝构造函数,编译器也会生成一个拷贝构造函数。这个自动生成的拷贝构造是按照逐字节拷贝的方式实现的,也就是说对于没有申请空间的变量,是可以实现拷贝的,例如这个日期类:

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

int main()
{
	Date d1;
	Date d2(d1);
	return 0;
}

类和对象【2】默认成员函数_第6张图片
但是,当类对象中存在申请资源的情况时,默认生成的拷贝构造就只能将指向空间的指针变量逐字节的拷贝过去,而不是我们希望的再开一块空间去拷贝申请的空间中的内容。
不但如此,由于有两个类对象的成员变量指向这同一块空间,在结束编译后就会调用两次析构函数,对这块空间释放两次,这显然会出现问题。例如这个栈类:

class Stack
{
public:
	Stack()
	{
		_top = 0;
		_capacity = 10;
		_data = new int[_capacity] {0};
	}
	~Stack()
	{
		_top = 0;
		_capacity = 0;
		delete[] _data;
	}
private:
	int* _data;
	int _top;
	int _capacity;
};
int main()
{
	Stack s1;
	Stack s2(s1);
	return 0;
}

类和对象【2】默认成员函数_第7张图片
类和对象【2】默认成员函数_第8张图片
这种情况下就必须自己实现拷贝构造函数了,后面会讲到。

赋值运算符重载

在介绍运算符重载前,我们需要先了解运算符重载的知识:

运算符重载

对于内置类型,有多种多样的操纵符帮助我们实现对内置类型的操作。例如两个int的相加,使用a + b就可以实现,不仅很简洁,且具有很高的可读性;
但对于自定义类型,我们只能写一些函数来实现。例如Add(Date& d, int day),调用时也只能Add(d, 100);这样显然可读性不高,且使用也很麻烦。所以C++引入了运算符重载:

运算符重载是具有特殊函数名的函数,其参数列表与返回值与普通函数一致:返回值 operator操作符(参数列表);(operator是一个关键字,由于运算符重载)

需要注意的是:

  1. 不能通过连接其他符号来创建新的操作符:比如operator@;
  2. 重载操作符必须有一个类类型参数;
  3. 用于内置类型的运算符,其含义不能改变(也是为了可读性);
  4. 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this;
  5. .* :: sizeof ?: . 以上5个运算符不能重载。
class Date
{
public:
	Date(int year = 2023, int month = 2, int day = 10)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	bool operator==(const Date& d)
	{
		return _year == d._year
			&& _month == d._month
			&& _day == d._day;
	}
private:
	int _year = 2023;
	int _month = 2;
	int _day = 10;
};

int main()
{
	Date d1;
	Date d2(d1);
	cout << (d1 == d2) << endl; //相当于d1.operator==(d2)
	return 0;
}

类和对象【2】默认成员函数_第9张图片
在上面的代码中,重载了==用来判断两个日期类是否相等。
因为在类外访问不到私有成员变量,所以将这个重载定义在类中。d1 == d2本质上就是调用对象d1的成员函数:d1.operator==(d2)

赋值运算符重载

赋值运算符重载即对 = 进行重载

  1. 只有一个参数const T&
  2. 返回值 T& ,方便其实现连续赋值的情况,使其更像 = ;
  3. 赋值运算符重载必须为成员函数,而不能是全局函数,因为它是默认成员函数,如果在全局的话就会与类中自动生成的冲突(其他默认成员函数同理);
  4. 编译器自动生成的赋值运算符重载只能实现逐字节的赋值,也就是浅拷贝(与默认生成的拷贝构造类似):
class Date
{
public:
	Date(int year = 2023, int month = 2, int day = 10)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	bool operator==(const Date& d)
	{
		return _year == d._year
			&& _month == d._month
			&& _day == d._day;
	}
	Date& operator=(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	    return *this;
	}
private:
	int _year = 2023;
	int _month = 2;
	int _day = 10;
};

int main()
{
	Date d1;
	Date d2(2023, 5, 21);
	d2 = d1;
	return 0;
}

类和对象【2】默认成员函数_第10张图片

总结

到此,关于默认成员函数的介绍就结束了
相信大家可以意识到,默认成员函数确实可以使我们使用类对象时更加方便

如果大家认为我对某一部分没有介绍清楚或者某一部分出了问题,欢迎大家在评论区提出

如果本文对你有帮助,希望一键三连哦

希望与大家共同进步哦

你可能感兴趣的:(C++初阶,开发语言,c++,类和对象)