【C++】——类和对象(构造函数,析构函数,拷贝构造函数,赋值运算符重载)

                                                  创作不易,多多支持! 

前言

相信你对这几个知识点有点混淆,相信看完以后,你会对此有一个清晰的认识。

一 类的6个默认成员函数

如果我们写一个类,但是类里面什么都没有,我们称之为空类。

其实这个类也不完全为空,因为编译器会类中自动生成这6个成员函数。

所以这几个成员函数也叫作默认成员函数,我们不去实现,编译器会生成。

接下来我们一个一个说明

二  初始化和清理

2.1  构造函数

1 .我们知道构造函数是执行初始化的操作,要是我们像以前一样写一个初始化函数去初始化也是可以的,下面用一个日期类去演示

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
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;//类的实例化
	d1.Init(2024, 7, 5);调用函数初始化
	d1.Print();
	Date d2;
	d2.Init(2024, 7, 6);
	d2.Print();
	return 0;
}

从代码中我们可以看出 如果我们要初始化我们就需要每次都调用这个初始化函数,这就会显得非常的麻烦,那有没有更加便捷的方法呢?

构造函数是一种特殊的成员函数,函数名与类名相同不需要返回值,在类实例化时自动调用,每个类只调用一次,我们可以用这个自动调用的特性去让我们的初始化变的非常方便

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class Date
{
public:
	Date()//无参构造函数
	{
		_year = 2024;
		_month = 7;
		_day = 5;
	}
	Date(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 d2(2024, 4, 24);调用有参
	d1.Print();
	d2.Print();
	return 0;
}

 在上面代码中我们并没有去调用里面的构造函数,我们可以看看输出的结果是什么

这里就体现了自动调用的特性。其中我们可以看出无参调用的时候有的人会这样写

Date d1();

这其实就错误了,因为编译器不知道你是调用函数还是类的实例化,所以不能这样写 

2. 对于构造函数因为是编译器默认生成的,所以即使我们不写,那么编译器也会自动生成一个,但是如果我们写了,那么编译器就不会生成了 

为了更好理解,下面给出相应代码

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class Date
{
public:
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	d1.Print();
	return 0;
}

这串代码是不会报错的,因为编译器会自动生成一个默认的构造函数,这样在类实例化的时候会调用这个默认的构造函数

但是如果我们这样写

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class Date
{
public:
	Date(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;
	d1.Print();
	return 0;
}

 可以看出这样就不行了,因为我们写了一个带参的构造函数,但是我们要初始化无参的就没了,因为我们写了构造函数,那么编译器就不会再生成默认的构造函数

但是不要想着编译器会给你那么方便,他虽然可以帮你自动调用,你没写也可以自动生成,但是它自动生成的默认构造函数是不会给你初始化的,这一点要尤其注意

我们可以看第一段代码, 是可以运行通过的,但是运行的结果却是

 可以看出结果不是我们想的那样,它并没有完成初始化,所以我们可以得出默认的构造函数对于内置类型是不处理的,内置类型就是(int/double/char...)之类的类型。

3.  那这个默认的构造函数没用吗?并不是,对于自定义类型,这个默认构造函数会去调用这个自定义类型的默认构造函数

可能这段话看起来非常绕,那下面我们看一点代码就清楚了

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class Time
{
public:
	Time()//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 d1;
	return 0;
}

 

之所以有这样一个结果就是在Date中有一个默认构造函数,这个是由编译器自动生成的,这个函数对于内置类型是不处理的,但是对于自定义类型,会去调用它的默认构造函数,所以就会打印出这个 Time()

4.  其实这让我们也非常难以接受,所以在后面c++又对它进行了补丁,可以在成员变量声明的时候给缺省值

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
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
{
public:
	void Print()
	{
		cout << _year <<'-'<< _month <<'-' << _day << endl;
	}
private:
	//内置类型
	int _year=2024;
	int _month=7;
	int _day=21;
	//自定义类型
	Time _t;
};
int main()
{
	Date d1;
	d1.Print();
	return 0;
}

注意这里虽然是在声明的时候给了值,但是这里并不代表是定义,也就是没有给空间,给空间还是要在实例化的时候给空间

5.  这里还有一个点要分清楚,默认成员函数只包括(全缺省构造函数,无参构造函数,我们没写编译器默认生成的构造函数)对于不是这些类型的都不能算是默认构造函数

所以如果我们写一个无参的构造函数,再写一个全缺省的构造函数,那么这个编译器就会报错,因为再类实例化的时候,它不知道调用哪一个,这一点也是要注意的

2.2 析构函数

析构函数也是特殊的成员函数 ,他和构造函数相反,他是负责清理的,但是两者的特性是差不多的 对象在销毁时会自动调用析构函数,完成对象中资源的清理工作

 析构函数特性

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

和构造函数一样,对于内置类型不处理,对于内置类型去调用它的析构函数

如果是对于这么日期类,里面只包含内置类型,那么也可以不写析构函数,因为出了作用域,内置类型的变量会随着栈的销毁而销毁 ,但是如果涉及到申请资源那么就需要用到析构函数了

#include<iostream>
using namespace std;

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;
}

这里和上面的构造函数一样,就不做太多的说明了。

三 拷贝复制

3.1 拷贝构造函数

 拷贝构造函数只有单个形参,该形参是对本类类型对象的引用一般用const修饰。

拷贝构造函数特性:

1.拷贝构造函数是构造函数的一种重载

2.拷贝构造函数的参数只有一个,而且这个形参必须是该类的引用,如果用传值会导致无穷递归

#include<iostream>
using namespace std;
class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	// Date(const Date& d)   // 正确写法
	Date(const Date d)   // 错误写法:编译报错,会引发无穷递归
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	Date d2(d1);
	return 0;
}

 在解释无穷递归的原因之前,我们得先来了解一下内置类型的拷贝和自定义类型的拷贝的区别

内置类型的拷贝就在底层就是一个字节一个字节的拷贝,也就是浅拷贝,那如果自定义类型也是浅拷贝的话,有可能会发生意想不到的后果

比如如果说自定义类型里面有申请空间开辟的数组,那么就会发生两次释放空间的问题

那如果避免这个问题了,这里我们就需要深拷贝,深拷贝就是再开辟一片空间,把他们分开,这样就不会导致重复释放了,所以这里我们就需要用拷贝构造函数去实现这一功能

所以我们对于自定义类型不管里面是不是有申请空间的变量,我们都去调用它的拷贝构造函数

 那么回到无穷递归这个问题,如果我们要拷贝自定义类型,那么编译器会去调用拷贝构造函数,那么就会传参,那么我们传参又会去调用新的拷贝构造,调用新的拷贝构造又会传参,那么就陷入无穷递归了

所以我们需要用到引用,但是这个引用我们不能去改变这个值,所以用const

所以拷贝构造函数的形式就是

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

 其他的形式都不是拷贝构造

拷贝构造和构造函数也有点相似的地方,比如我们没有写拷贝构造函数的时候,编译器会去调用默认生成的拷贝构造,不过这个拷贝构造不会完成深拷贝,只是简单的值拷贝,也就是浅拷贝

3.2  赋值运算符重载

赋值运算符重载是具有特殊函数名的函数

函数名为:operator后面接需要重载的运算符符号(+,-,*,[])

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

但是要注意有几个符号是不能重载的

.*   ::   sizeof   ?:   .

 还需要注意的是

1.不能通过连接其他符号来创建新的操作符:比如operator@
2.重载操作符必须有一个类类型参数
3.用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
4.作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐
藏的this

 1.运算符重载

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class Date
{
public:
	bool operator==(const Date& d2)//运算符重载,这里只有一个参数
                                   //其实还有一个隐含的参数this
                                   //如果把该函数放在外面就没有this,但是就确保不了封装性了
	{
		return _year == d2._year
		&& _month == d2._month
		&& _day == d2._day;
	}
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//private:
	int _year;
	int _month;
	int _day;
};

void Test()
{
	Date d1(2018, 9, 26);
	Date d2(2018, 9, 27);
	cout << (d1 == d2) << endl;
}
int main()
{
	Test();
	return 0;
}

上面的代码就是运算符重载的一个实例,那赋值运算符重载其实道理也大差不差

 但是其中也有些许细节

参数类型 const T& ,传递引用可以提高传参效率
返回值类型 T& ,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
检测是否自己给自己赋值
返回 *this :要复合连续赋值的含义

 按照上面的格式,我们可以写出一个相应代码

2. 赋值运算符重载

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class Date
{
public:
	Date& operator=(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
		return *this;
	}
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_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 d2(2024, 7, 40);
	d1 = d2;
	d1.Print();
	d2.Print();
	return 0;
}

 3.赋值运算符重载只能定义成成员函数,不能定义成全局函数

因为运算符重载也是默认的成员函数所以,编译器会自己生成一个,那么我们自己如果在全局再写一个就会导致冲突,所以运算符重载必须写成成员函数,不能写成全局函数

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 =”必须是非静态成员

4. 如果我们不写赋值运算符重载,那么编译器会自动生成一个,这个自动生成的对内置类型完成值拷贝,对于自定义类型会去调用对应类的赋值运算符重载

与拷贝构造函数类似,对于只有内置类型的类来说,写不写都可以,但是如果涉及到申请资源的变量那么就得自己写完成深拷贝的函数

5.  前置++与后置++重载

对于这两个直接看代码理解吧

class Date
{
public:
	
	Date& operator++()//前置++
	{
		_day += 1;
		return *this;
	}
	Date operator++(int)后置++
	{
		Date tmp = *this;
		_day+=1;
		return tmp;

	}
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year <<'-' << _month <<'-' << _day << endl;
	}
	private:
	int _year;
	int _month;
	int _day;
};

后置++需要一个int形参去构成函数重载,这里只是构成重载没有任何作用

注意后置是先返回,后++,所以这得用一个临时对象保存,返回的时候不能用引用返回,因为返回的是临时对象,用引用返回会出现未定义行为

四  取地址重载

class Date
{ 
public :
 Date* operator&()//不加const
 {
 return this;
 }
 const Date* operator&()const//加const
 {
 return this ;
 }
private :
 int _year ; // 年
 int _month ; // 月
 int _day ; // 日
};

这里分为加const和不加const

 编译器默认生成的是不加const,所以一般这种重载我们不写交给编译器,但是如果有特殊的要求则需要自己手动写,比如想人获取指定的内容

const 成员函数 

用const修饰的成员函数称之为const成员函数,使用const修饰的成员函数不能修改类的成员变量,也不能调用非const成员函数

const修饰类成员函数,实际上修饰该成员函数的隐含指针this,表明在该成员函数中不能对类的任何成员进行修改

语法声明为:void fun() const;

class Date
{
public:
	void fun()const
	{
		_year = 6;//尝试对成员变量进行赋值
	}
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year <<'-' << _month <<'-' << _day << endl;
	}
	private:
	int _year;
	int _month;
	int _day;
};

 

对于const对象调用const成员函数时,会调用const版本函数,而使用非const对象调用const成员函数时,会调用非const版本函数

这也就是一一对应

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class Date
{
public:
	void fun()const
	{
		cout << "const" << endl;
	}
	void fun()
	{
		cout << "非const" << endl;
	}
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year <<'-' << _month <<'-' << _day << endl;
	}
	private:
	int _year;
	int _month;
	int _day;
};


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

可以看出结果也是一一对应的。

在这里还有一个点就是

🎈const成员函数不能调用非const成员函数,因为这是 权限放大

在const成员函数里它承诺了不能修改成员变量,如果去调用非const,非const又可以修改,这就违法了const成员变量的约定

🎈非const成员函数可以调用const成员函数,权限缩小是可以的

在非const可以修改也可以不修改,那在里面调用const成员函数,const成员函数规定不能修改,在非const里面并不矛盾,可以包容,所以是合理的

🎈const对象不可以调用非const成员函数

const对象里面的成员函数被隐式的看成了const成员函数,因此就和上面的道理是差不多的了

🎈非const对象可以调用const成员函数

 一句话就是,如果内部不涉及修改的,用const修饰,如果涉及修改的就不能加const

 

 相信看到这里,你会对构造函数,析构函数,拷贝构造函数,赋值运算符重载有一个更深的认识

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-04-25 16:40:01       94 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-04-25 16:40:01       100 阅读
  3. 在Django里面运行非项目文件

    2024-04-25 16:40:01       82 阅读
  4. Python语言-面向对象

    2024-04-25 16:40:01       91 阅读

热门阅读

  1. android 解决webView底部留白问题

    2024-04-25 16:40:01       37 阅读
  2. C语言中的动态内存管理

    2024-04-25 16:40:01       36 阅读
  3. 使用 MediaCodec 在 Android 上进行硬解码

    2024-04-25 16:40:01       33 阅读
  4. uniapp 小程序 多张图片生成海报以及下载海报

    2024-04-25 16:40:01       29 阅读
  5. 代码随想录第34天: 贪心part03

    2024-04-25 16:40:01       35 阅读
  6. 济南软件企业认定标准

    2024-04-25 16:40:01       33 阅读
  7. 前端中的同步和异步任务详细说明

    2024-04-25 16:40:01       37 阅读
  8. C#WPF通过串口(232协议)调用基恩士打标机进行打标

    2024-04-25 16:40:01       30 阅读
  9. 常用的 Spring Boot 注解及其作用

    2024-04-25 16:40:01       33 阅读
  10. 英语语法速成(4)

    2024-04-25 16:40:01       27 阅读
  11. 李沐63_束搜索——自学笔记

    2024-04-25 16:40:01       29 阅读
  12. 用Python搭建一个猜数字小游戏

    2024-04-25 16:40:01       40 阅读