本篇博客包含构造函数,默认构造函数,析构函数,默认析构函数,拷贝赋值函数,和运算符重载。

假设这里存在一个类里面什么都不包含。

class A
{
};

难道在这个类中真的什么都不会含有吗?答案是否定的即使这个类中没有任何一个东西,编译器也会自动的生成6个默认函数。

类和对象的使用(中)_拷贝构造函数

至于什么是默认函数,下面我会指出。

构造函数

在用c去写一个栈的时候,在很多情况下,我们可能会忘记将栈给初始化,导致出现错误。而c++中为了防止这种情况增加了一个构造函数。

构造函数是特殊的成员函数,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。

除此之外构造函数还有以下的特征:

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

下面我就用一个日期类来演示

#include<iostream>
using namespace std;
class Data
{
public:
	Data(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()
{
	Data d1(2023, 7, 12);
	Data d2(2023, 7, 13);
	d1.Print();
	d2.Print();
	return 0;
}

类和对象的使用(中)_析构函数_02

从运行结果可以看到,两个日期对象已经被成功初始化了。从这里可以看到构造函数的一个特点就是会自己调用。那么默认构造函数又是什么呢?依旧以上面的代码举例子,如果我在生成d1对象的时候没有传递三个参数,甚至于没有传递参数那么这个函数又会调用吗?答案显然是否定的。


而默认构造函数也就是在构造函数的基础上传递参数能够运行,不传递参数也能够运行总言之:无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为 是默认构造函数。

例如下面这样的函数

int main()
{
	Data d0;//不传递任何的形参
	//需要记住如果你不传递任何的形参,那么不用使用括号,如果使用括号会让编译器不知道这是一个对象,还是一个函数
	Data d1(2023);//传递1个参数
	Data d2(2023, 7);//传递两个参数
	Data d3(2023, 7, 12);//传递三个参数
	d0.Print();
	d1.Print();
	d2.Print();
	d3.Print();
	return 0;
}

类和对象的使用(中)_析构函数_03

那么如果我不写构造函数,编译器会自己生成构造函数吗?答案是肯定的,而且由编译器自己生成的构造函数也是一个默认构造函数。但是由编译器自己生成的构造函数对于内置类型(int,double,char等)是不会做任何处理的,但是对于自定义类型会去自动的调用自定义类型的构造函数。但是在C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。

即在定义类的时候就给内置类型的变量一个值。

例如在之前的学习中我写过的使用栈区模拟实现一个队列,那么这个队列类中就含有两个自定义类(栈),那么这个队列我们就不用写默认构造函数,因为编译器自己生成的构造函数会去调用栈的构造函数。例如下面这样

class stack
{
public:
	stack(int b = 0)//这里写一个全缺省是有作用的
		//如果外面要使用栈的人知道这个栈要储存多少数据,那么就可以通过构造函数,在一开始便
		//创建足够多的空间,如果不知道那就默认一开始不分配空间
	{
		if (b == 0)
		{
			_a = NULL;
			_capacity = 0;
		}
		else
		{
			_a = (int*)malloc(sizeof(int) * b);
			if (_a == NULL)
			{
				perror("malloc fail:");
				exit(-1);
			}
			_capacity = b;
		}
		_top = 0;
		cout << "调用了栈的构造函数" << endl;
	}
	void Push(int x)
	{
		if (_capacity == _top)
		{
			int newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
			int* tmp = (int*)realloc(_a, sizeof(int) * (newcapacity));
			if (tmp == NULL)
			{
				perror("realloc fail");
				exit(-1);
			}
			_a = tmp;
			_capacity = newcapacity;
		}
		_a[_top] = x;
		_top++;
	}
	void Pull()
	{
		assert(_top > 0);
		_top--;
	}
	int Top()
	{
		assert(_top > 0);
		return _a[_top - 1];
	}
private:
	int* _a;
	int _capacity;
	int _top;
};
class MyQueue
{
private:
	stack a;
	stack b;
};

类和对象的使用(中)_拷贝构造函数_04

从运行结果就可以看到,编译器自己生成的构造函数调用了stack的构造函数,如果将stack函数删除也会出现错误。

析构函数

既然构造函数对应的是c中的初始化功能,那c中的销毁功能有没有对应的函数呢?答案是肯定的这个函数也就是析构函数。

首先析构函数有以下特性:

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

依旧以栈为例子:

	~stack()
	{
		free(_a);
		_a = NULL;
		_capacity = 0;
		_top = 0;
		cout << "调用了析构函数" << endl;
	}

类和对象的使用(中)_拷贝构造函数_05

对比于构造函数,析构函数显然简单很多,析构函数不需要返回值,和参数并且不可被重载,并且在对象生命周期结束时,自动调用。

并且和构造函数一样对于像myqueue一样的数据结构依旧也会去调用自定义类型成员变量的析构函数。

除此之外如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如

Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。申请资源也就是使用malloc等函数向堆中申请空间。

拷贝构造函数

在使用栈的时候,如果有一个函数要使用栈这个变量然后我们将栈传递过去会发生什么呢?例如下图这样:

void func(stack )
{

}
int main()
{
	stack a(100);//首先在这里我先给这个栈申请了400个字节的空间
	func(a);//将这个栈当作形参传递给func
	return 0;
}//这样写会不会出问题呢?

类和对象的使用(中)_构造函数_06

那么为什么会出现这个问题呢?去分析代码首先创建了一个对象a为栈空间为400个字节。

然后将a作为参数传递了过去,问题也就出现在这里,在这里的拷贝为浅拷贝,即编译器底层是将a这个空间里面储存的所有的数据给传递了过去,然后再func中b中储存的内容和a一样,但是a中的int指针指向的空间,和b中int*指针指向的空间是同一个,然后在func函数结束之后,系统会自动地调用b的析构函数,将int*指针指向的那片空间释放掉,但是这片空间还有一个指针也指向它,那就是主函数中的a对象,在主函数结束之后再次调用a对象的析构函数然后就导致了错误。至于如何验证可以通过调试监视证明。

类和对象的使用(中)_构造函数_07

从这里可以看到a对象中指针指向的那片1空间为0x117d050.

类和对象的使用(中)_构造函数_08

而func函数中b对象中的_a指针指向的也是这片空间。

那么第一种解决方法也就是传递a对象的引用,但是依旧存在问题那就是这个func函数没有对数据做出改变,但是如果是要对数据做出改变呢?那么就会出现形参影响到了实参。为了应对这种情况c++便增加了拷贝构造函数。

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

拷贝构造函数的特性:

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

  1. 拷贝构造函数是构造函数的一个重载形式。
  2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
  3. 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。注意:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。(和myqueue类一样)
  4. 编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显式实现吗?当然像日期类这样的类是没必要的。但是涉及到开辟空间(资源分配)的类例如stack就要自己写拷贝构造函数

注意:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请

时,则拷贝构造函数是一定要写的,否则就是浅拷贝。

那么对于第二点我来详细讲解一下,一日期类为例子

类和对象的使用(中)_拷贝构造函数_09

类和对象的使用(中)_析构函数_10

可以看到如果如果使用的不是引用就会出现错误。

那为何会出错呢?下图解释:

类和对象的使用(中)_析构函数_11

首先我们要使用d1去拷贝一个一模一样的d2,但是在调用拷贝构造函数的时候,传参的过程中再次涉及到了将d1拷贝给形参,这个拷贝会再次调用函数,而这个再次调用函数,又会出现将d1拷贝给拷贝函数函数2的情况。由此也就出现了不断的传值拷贝。但是如果你使用的是引用,那么就不会出现传参过程再次调用拷贝函数的情况。

所以在写拷贝构造函数的时候,参数要传引用并且有const修饰。防止别人修改原来的d1。

对于拷贝构造函数的第三和第四点也很好理解,即如果在一个类中,它的内置成员变量全都是内置类型,那么你即使使用值拷贝也不会出现问题,但是如果一个类中是含有int*即malloc生成的变量那么这个时候的拷贝构造函数,便要求我们自己去写,否则就会出现我一开始提到的那个问题。

下面我就来写栈的拷贝构造函数,即可结局我一开始提出的那个问题。

stack(const stack& a)
	{
		_a = (int*)malloc(sizeof(int) * a._capacity);
		if (_a == NULL)
		{
			perror("malloc fain");
			exit(-1);
		}
		_capacity = a._capacity;
		_top = a._top;
		memcpy(_a, a._a, (sizeof(int) * a._capacity));//使用memcpy复制值
	}

类和对象的使用(中)_拷贝构造函数_12

类和对象的使用(中)_构造函数_13

可以看到在对象b中的int* 指针指向的空间和对象a中的int* 指针指向的已经不是同一片空间了。并且改变b也不会影响a。

最后拷贝构造函数一般用于下面的情况:

1.使用已存在对象创建新对象

2.函数参数类型为类类型对象

  3.函数返回值类型为类类型对象

操作符重载

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

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

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

注意: 1.不能通过连接其他符号来创建新的操作符:比如operator@

2.重载操作符必须有一个类类型参用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义作为类成员函数重时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this。

3.   .*  ::  sizeof  ? :  . 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。

下面一一解析这三点

第一点:在使用运算符重载的时候不能自己去定义一个运算符,例如下面这样

	void operator@()
	{

	}

会无法运行

第二点在重载操作符时,如果是针对内置类型的运算符(如加号 +,减号 - 等),其含义不能改变,并且至少需要有一个类类型的参数。当这些操作符作为类的成员函数进行重载时,函数的参数个数看起来比操作数数量要少1,因为成员函数的第一个参数是被隐藏的 this 指针,用于指向调用该成员函数的对象本身。

例如下面我要重载实现日期+一个整型并且会改变原来的日期:

	Data& operator+=(int day)//注意这里完成的是+=相当于日期类 = 日期类(本身)+天数
		//是要改变原日期类的所以这里返回的是原日期类的引用
	{
		//使用进位的方法
		_day += day;
		while (_day > GetMonthDay(_year, _month))
		{
			_day -= GetMonthDay(_year, _month);
			_month++;
			if (_month == 13)
			{
				_year++;
				_month = 1;
			}
		}
		return *this;//this指针指向的就是现在要加的那个日期
	}

运行截图:

类和对象的使用(中)_析构函数_14

除了重载+=外还可以重载+,-,等等。

我会在我的下一篇博客完善日期类。

希望这篇博客能对您有所帮助,如果您发现了任何错误,欢迎指出感谢。