类与对象(中)

目录

前言:

一、构造函数

1、概念

2、特性介绍

二、析构函数 

1、概念

2、特性

三、拷贝构造函数

1、概念

2、特性

四、赋值运算符重载 

1、运算符重载

2、赋值运算符重载

3、前置++与后置++的重载 

总结:


前言:

当一个类什么都不写的时候,我们通常叫它空类,那么空类中真的什么都没有吗?

事实上,任何一个类在什么都没写的时候,都会生成六个默认成员函数,分别为构造函数,析构函数,拷贝构造函数,赋值重载函数,以及普通对象与const对象取地址函数。

由于大多数后两个函数不需要我们手动操作,所以今天主要讲解的内容是前面四个函数。为了方便,我们会以Date类为例子,更加方便让大家理解。

理解构造函数,析构函数,拷贝构造函数,赋值重载函数对理解C++雷雨对象有十分重要的作用。

一、构造函数

1、概念

构造函数是一种特殊的函数,它用于创建和初始化对象时调用。它通常与类关联,用于在创建类的实例时执行特定的初始化操作。构造函数的名称与类名称相同,并且没有返回类型。在创建对象时,构造函数会自动调用,并且可以接收参数来初始化对象的属性。这有助于确保每个数据成员都有个合适的初始值,并且在对象整个生命周期中只调用一次。

所以大家别被它的名字所误导,构造函数并不执行构造功能,它的主要作用只是进行初始化。

2、特性介绍

2.1、函数名必须要与类名一致。

2.2、无返回值。

2.3、在对象实例化时编译器会自动调用一遍对应的构造函数。

2.4、构造函数可以多个重载。(例:)

class Date
{
public:
	Date()//一个没有参数的构造函数,我们称之为无参构造函数
	{
		cout << "Date()" << endl;
	}

	Date(int year, int month, int day)//带参构造函数
	{
		_year = year;
		_month = month;
		_day = day;
		cout << "Date(int year, int month, int day)" << endl;

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

int main()
{
	Date A;
	Date B(1800, 1, 1);
	return 0;
}

当我们实例化A时,编译器就会默认调用符合A的无参构造函数,当我们实例化B时,由于B更加满足带参构造函数,所以会调用带参构造函数。

 2.5、如果类中没有显示定义构造函数,那么编译器会自动生成一个无参的默认构造函数,一旦用户显式定义了,编译器就不会再生成。

class Date
{
public:
	Date(int year, int month, int day)//带参构造函数
	{
		_year = year;
		_month = month;
		_day = day;
		cout << "Date(int year, int month, int day)" << endl;	
	}

	void Print()
	{
		cout << _year << ' ' << _month << ' ' << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

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

当我们显式定义了构造函数,且没手动写一个无参构造函数时,此时A就不能进行无参数的初始化,因为编译器也没用生成构造函数,A找不到它适合的构造函数,编译就会报错。

class Date
{
public:
	//Date(int year, int month, int day)//带参构造函数
	//{
	//	_year = year;
	//	_month = month;
	//	_day = day;
	//	cout << "Date(int year, int month, int day)" << endl;	
	//}

	void Print()
	{
		cout << _year << ' ' << _month << ' ' << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

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

但当我们把显式定义的带参构造函数注释掉时,就可以正常进行编译,这就是编译器默认生成了一个无参构造函数的结果。

2.6、编译器生成的默认构造函数,会对自定义类型自动调用自定义类型自己的构造函数,对于内置类型来说,则不会自动调用。为了弥补这个缺点,在C++11中又规定可以在内置类型声明时给予默认值(这个默认值的作用具体的我们会在类与对象下中讲解到)。

 但实际上,由于只对自定义类型调用默认构造,倘若自定义类型中仍然有自定义类型成员参数,调用时函数就会层层递进,直至不再包含自定义类型成员函数为止。倘若自定义类型中没有可以调用的有效的构造函数,就会编译报错。

2.7、当一个构造函数是无参的构造函数与全缺省的构造函数,就被我们称为默认构造函数。(包括编译器默认生成的构造函数其实也是无参的,自然就可以被叫做默认构造函数) 。(确定有默认构造函数是为了当我们在实例化一个类时,只确定对象名,允许不给参数让对象初始化)

二、析构函数 

1、概念

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

2、特性

2.1、析构函数名是类名前面加上字符~。
2.2、析构函数没有参数,也没有返回类型
2.3、一个类只能拥有一个析构函数,所以析构函数不能重载。当我们没有显式定义析构函数时,编译器会自动生成一个默认析构函数。

2.4、 对象的生命周期结束时,编译器会自动调用析构函数

2.5、如果类中没有涉及申请资源,析构函数可以不写,直接使用编译器默认生成的析构函数就行。但如果涉及到了资源申请,一定要写,否则可能会造成资源泄露。

class Time
{
public:
	~Time()
	{
		cout << "~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 d;
	return 0;
}

对于以上代码,输出的结果是~Time(),这是因为我们生成了一个Date类的对象,在该对象生命周期结束时,会自动调用编译器默认生成的析构函数,由于该对象成员参数有一个自定义类型,就会该自定义类型变量的析构函数,于是打印出了~Time()。

三、拷贝构造函数

1、概念

拷贝构造函数与构造函数的差别主要就是名字上面的拷贝造成的。拷贝就是用一个已经存在的东西复制出一份原本不存在的东西。

拷贝构造函数就是指只有单个形参(这个形参就是原本存在的东西),且该形参是对本类类型对象的引用(一般还会加上const修饰),在用已存在对象创建新对象时,编译器自动调用的构造函数。

2、特性

2.1、拷贝构造函数是构造函数的一个重载形式。

2.2、拷贝构造函数的参数只有一个,且必须为该类类对象的引用,不能使用传值,会引发无穷递归导致编译器直接报错。

2.3、若没有显示定义,则编译器会生成一个默认的拷贝构造函数。这个默认生成的拷贝构造函数将会按照字节序,对内存存储进行拷贝。

2.4、对内置类型来说会调用默认拷贝构造函数进行浅拷贝,对自定义类型还是会调用该类型的拷贝构造函数。

2.5、若类中没有涉及内存资源申请的话,可以直接使用编译器默认生成的拷贝构造函数进行浅拷贝,但如果涉及了内存的管理,就必须进行手动的编写拷贝构造函数。

class Date
{
public:
	Date(int year, int minute, 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;
};

int main()
{
	Date A(1900, 1, 1);
	Date B(A);
	return 0;
}

 对于以上代码,在vs2022,64位环境下的运行结果为:

我们可以看到,定义A的时候调用了 构造函数,之后定义B时,由于A已经存在,所以直接调用了拷贝构造函数。最后生命周期结束时,由于B最后定义,所以最先析构,随后进行A的析构。

让我们来稍微深入理解一下:


class Date
{
public:
	Date(int year, int minute, 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(1900, 1, 1);
	Test(d1);
	return 0;
}

结果为:

在这个过程中,我们会先对d1进行定义与初始化,随后就会调用构造函数,随后调用test函数的时候,给定义初始化temp的时候,又会调用拷贝构造函数,随后在返回的时候,会生成temp的副本来进行传值返回,生成副本的时候,已经退出了test函数,于是temp在进行了一次对副本的拷贝构造函数之后,立马又进行了析构。

四、赋值运算符重载 

我们要讲述的运算符重载与复制运算符重载,其实都是函数重载的一种。只不过运算符的重载需要关键字operator的帮助。

1、运算符重载

C++ 为了增强代码的可读性引入了运算符重载 运算符重载是具有特殊函数名的函数 ,也具有其
返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator 后面接需要重载的运算符符号。
函数原型:返回值类型  operator 操作符 ( 参数列表)
但我们要注意的是:
我们不能修改内置类型的运算符的含义,例如整形的加减乘除的含义不能改变。运算符重载与我们前面所讲的构造,析构函数相似,一切对自定义类型参数的修改归根结底都要追溯到对内置类型的更改。对自定义类型的初始化要追溯到内置类型,对自定义类型进行加减乘除操作时,我们实际上也是通过对自定义类型中的内置类型参数进行内置类型的加减乘除操作。
且重载的操作符至少都应该有一个类参数类型。作为类成员函数的重载时,与成员函数相似,形参看起来比实际的操作数少一,因为成员函数的第一个参数都应该是隐藏起来的this。
特别注意,有五个操作符是不能进行重载的:‘    . *   '      ‘   ::  ’        ‘   sizeo0f    ’      ‘  :?  ’       ‘   .    ’
 

2、赋值运算符重载

赋值运算符重载是特殊的运算符重载,是自定义类型中进行赋值操作的操作符。它有着特定的参数类型与返回类型:const T&与T&,传引用返回是为了提高传递的效率 为了满足连续的赋值,在返回时需要返回*this,因为*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)//传引用返回,提高效率。参数类型固定为(const Date&)
	{
		if (this != &d)//如果是自己与自己赋值,可以直接免去内置类型复制操作,节省空间
		{
			_year = d._year;//归根结底还是对内置类型的运算,所以重载时不能改变内置类型=的含义。
			_month = d._month;
			_day = d._day;
		}

		return *this;//返回*this,即对象本身。
	}
private:
	int _year;
	int _month;
	int _day;
};

 与拷贝构造函数类似,当没有显式定义时,就会生成一个默认的赋值运算符重载函数,以字节序逐内存的进行拷贝(浅拷贝)。所以赋值运算符重载在没有涉及内存管理时也不需要手动实现编写,但如果涉及到了内存的管理分配,就必须手动搓一个函数出来了。

3、前置++与后置++的重载 

由于都是通过关键字operator来进行运算符的重载,所以当我们进行前置++与后置++的重载时,就难免弄混。为了解决这个问题,我们规定,重载后置++时,需要在参数列表中加一个int类型的形参(此参数只是为了分辨函数的重载,并无实际作用):

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;
		return *this;
	}
	// 后置++:
	// 前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确重载
	// C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传递
		// 注意:后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将this保存一份,然后给this + 1
		// 而temp是临时对象,因此只能以值的方式返回,不能返回引用
		Date operator++(int)
	{
		Date temp(*this);
		_day += 1;
		return temp;
	}
private:
	int _year;
	int _month;
	int _day;
};

总结:

类的六个默认成员函数是理解C++类的关键,理解了这六个函数,尤其是前四个,就可以对C++类的实现与作用有质的提升。

我们在类与对象中主要讲解了类的六个默认函数的前四个,由于后两个编译器都会自动生成,并不需要我们重新定义。只有特殊情况,才会需要我们手动定义,一般来说,都直接使用编译器默认生成的就行。 

作者本人语言功底有限,有些讲解的粗糙,甚至不好理解,请大家谅解。

若有讲解之处,请大家提出来,感谢大家监督!

相关推荐

  1. python对象

    2024-04-24 13:00:03       17 阅读
  2. 面向对象——对象

    2024-04-24 13:00:03       14 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-04-24 13:00:03       16 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-04-24 13:00:03       16 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-04-24 13:00:03       15 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-04-24 13:00:03       18 阅读

热门阅读

  1. React vs React Native写法上的不同

    2024-04-24 13:00:03       14 阅读
  2. 20240423-线程基础

    2024-04-24 13:00:03       13 阅读
  3. C++orm使用插曲——MySQL保留字

    2024-04-24 13:00:03       16 阅读
  4. 如何在 Docker 和 DigitalOcean Kubernetes 上部署 Kafka

    2024-04-24 13:00:03       10 阅读
  5. 深入理解Kubernetes:kube-scheduler源码解析

    2024-04-24 13:00:03       13 阅读
  6. DNS 服务器不同类型有什么作用?

    2024-04-24 13:00:03       15 阅读
  7. 项目开发的详细步骤(精华版)

    2024-04-24 13:00:03       12 阅读
  8. FlinkSQL State的生命周期

    2024-04-24 13:00:03       11 阅读