C++类和对象(中)

本文介绍了C++中的构造函数、析构函数、拷贝构造函数的概念和作用,以及运算符重载和const修饰的成员函数。构造函数用于对象初始化,析构函数在对象生命周期结束时释放资源,拷贝构造函数解决对象复制时的问题,运算符重载使符号适应更多类型,const成员函数允许const对象调用。
摘要由CSDN通过智能技术生成

之前我们对C++的类和对象已经入门了,今天我们来看看类和对象中各种个样的默认函数。

一个类就算里面什么也没有,里面也有6个默认的成员函数

默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。

构造函数

对于Date类:

class Date
{
public:
	void Init()
	{

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

int main()
{
	Date S;
	S.Init();
}

我们可以调用Init()函数,进行初始化。但这会有一个问题:每次都要调用Init()函数,十分麻烦。
我们可不可以在对象实例化的时候,就完成对象的初始化呢?
答案是可以,这就是构造函数的作用。

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

它有如下特征:

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

所以我们就不用Init()函数了:

class Date
{
public:
	Date()//无参构造函数
	{
		cout << "Date()" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date S;
}

我们来看看会不会自动调用:
在这里插入图片描述
还有第五点:

  1. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦
    用户显式定义编译器将不再生成

我们来看看:

class Date
{
public:
	//Date()//无参构造函数
	//{
	//	cout << "Date()" << endl;
	//}

	//Date(int year, int month, int day)
	//{

	//}

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

在这里插入图片描述我们把带参的构造函数放开:

class Date
{
public:
	//Date()//无参构造函数
	//{
	//	cout << "Date()" << endl;
	//}

	Date(int year, int month, int day)
	{

	}

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

在这里插入图片描述这是因为我们显示构造了之后,编译器就不会自动生成默认构造函数,编译器就会报错。
但是还有一个问题,编译器自动生成的默认构造函数,虽然可以编译通过但好像并没有对成员变量进行初始化。
这就涉及到第六点了:

6.:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类
型,如:int/char…,自定义类型就是我们使用class/struct/union等自己定义的类型,
编译器生成默认的构造函数会对自定类型成员进行初始化,但是内置类型不会进行处理。

下面这段代码也证明了这一点:

class Time
{
public:
	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 d;
	return 0;
}

在这里插入图片描述我们看到了,编译器就只对我们自定义的Time _t进行默认构造函数初始化。
但是:C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在
类中声明时可以给默认值

class Time
{
public:
	Time()
	{
		cout << "Time()" << endl;
		_hour = 0;
		_minute = 0;
		_second = 0;
	}
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;
}

在这里插入图片描述
我们来看第七点:

  1. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个
    注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为
    是默认构造函数

所以下面这段代码会有问题:

class Date
{
public:
	Date()
	{
		_year = 1900;
		_month = 1;
		_day = 1;
	}
	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;
}

在这里插入图片描述
总结一下

  1. 函数名类名相同。
  2. 无返回值
  3. 对象实例化时编译器自动调用对应的构造函数。
  4. 构造函数可以重载
  5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦
    用户显式定义编译器将不再生成。
  6. :C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类
    型,如:int/char…,自定义类型就是我们使用class/struct/union等自己定义的类型,
    编译器生成默认的构造函数会对自定类型成员进行初始化,但是内置类型不会进行处理。
    (C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在
    类中声明时可以给默认值
    。)
  7. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个
    注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为
    是默认构造函数

析构函数

我们用构造函数来初始化对象,如果我们要释放对象资源,我们就要用一个新的函数:析构函数
我们先来看看析构函数的规则:

  1. 析构函数名是在类名前加上字符 ~。
  2. 无参数无返回值类型。
  3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构
    函数不能重载。
  4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。
class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		cout << "Date()" << endl;
		_year = year;
		_month = month;
		_day = day;
	}

	~Date()//Date的析构函数
	{
		cout << "~Date()" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1;
}

在这里插入图片描述

  1. 编译器自动生成的析构函数,,会对自定类型成员调用它的析构函数。
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;
}

在这里插入图片描述
我们来看看第六点:

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

比如我们经常写栈,经常会用malloc,realloc开辟空间,像这样的类一定就要自己写析构函数。

再来总结一下:

  1. 析构函数名是在类名前加上字符 ~。
  2. 无参数无返回值类型。
  3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构
    函数不能重载。
  4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。
  5. 编译器自动生成的析构函数,,会对自定类型成员调用它的析构函数。
  6. 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如
    有资源申请时,一定要写,否则会造成资源泄漏。
  7. 析构遵守先进后出,(最先创建的对象最后销毁)

拷贝函数

我们经常会有这种情况:建立好了一个对象,要把这个对象赋值拷贝一个全新的对象
在这里插入图片描述
在函数传参时我们经常会说:形参是实参的一份临时拷贝。就和上面的代码原理类似。
但因为在类中我们引入了
析构函数
,在生命周期结束时会自动调用释放资源,函数的传参就会出现问题。

class Stack
{
public:

	Stack(size_t n = 4)
	{
		if (n == 0)
		{
			_data = nullptr;
			_top = _capacity = 0;
		}
		else
		{
			_data = (int*)malloc(sizeof(int) * n);
			if (_data == nullptr)
			{
				perror("malloc fail");
				exit(-1);
			}
			_top = 0;
			_capacity = n;
		}
		
	}

	~Stack()
	{
		free(_data);
		_data = nullptr;
		_top = _capacity = 0;
	}

private:
	int* _data;
	int _top;
	int _capacity;
};

void Print(Stack S)
{

}

int main()
{
	Stack S1;
	Print(S1);
}

这段代码似乎好像没什么问题,但是:
在这里插入图片描述这是为什么呢?首先我们要知道一点实参和形参都会调用析构函数,这个代码出现问题的地方就是开辟的动态数组_data。
我们把实参和形参的地址对比一下:
在这里插入图片描述
我们发现实参数组的地址和形参数组的地址都是一样的,意思就是析构函数会对同一地址的数组进行两次释放。这是非常危险的,为了解决这样问题,C++创建了拷贝函数的概念

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

来看一下它的特征:

  1. 拷贝构造函数是构造函数的一个重载形式。
  2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用(用指针其实也可以),使用传值方式编译器直接报错,因为会引发无穷递归调用
class Stack
{
public:

	Stack(size_t n = 4)
	{
		if (n == 0)
		{
			_data = nullptr;
			_top = _capacity = 0;
		}
		else
		{
			_data = (int*)malloc(sizeof(int) * n);
			if (_data == nullptr)
			{
				perror("malloc fail");
				exit(-1);
			}
			_top = 0;
			_capacity = n;
		}
		
	}

	Stack(const Stack& S)//拷贝构造函数
	{
		_data = (int*)malloc(sizeof(int) * S._capacity);
		if (_data == nullptr)
		{
			perror("malloc fail");
			exit(-1);
		}
		_top = S._top;
		_capacity = S._capacity;

		memcpy(_data, S._data, sizeof(int)*S._capacity);
	}

	~Stack()
	{
		free(_data);
		_data = nullptr;
		_top = _capacity = 0;
	}

private:
	int* _data;
	int _top;
	int _capacity;
};

void Print(Stack S)
{

}

int main()
{
	Stack S1;
	Print(S1);
}

我们此时再来看实参数组的地址和形参数组的地址:
在这里插入图片描述
实参和形参数组的地址不一样了,解决了二次释放的问题。
以后凡是要传参的地方都会先调用拷贝构造函数
如果拷贝构造函数的参数不是引用:
在这里插入图片描述编译器会直接报错,这是因为:
在这里插入图片描述会无穷无尽的递归,造成栈溢出。
有一个问题,如果我们不写拷贝构造函数,那传参的时候还会拷贝构造吗?
答案是是的,但只会进行最简单的浅拷贝(只负责把数据的表面值传过来),如果像是栈这样的传参,我们自己要写拷贝构造函数将实参的地址和形参的地址区分开来,这样的拷贝叫做深拷贝

运算符重载

我们都知道 +,可以表示两个数的相加,无论是整形,浮点形,**+**都可以直接使用让两个数相加。
但现在我想让 +号可以让两个日期类相加
这就涉及到了 运算符重载 了,让符号可以为更多的类型服务。

bool operator==(const Date& d1, const Date& d2)
{
	return d1._year == d2._year
	&& d1._month == d2._month
	&& d1._day == d2._day;
}

这时候 == 就可以用来判断两个日期类是否相等了。

const修饰的成员函数

我们一般写程序的时候对象都是一般的对象

class Date
{
public:
	Date(int year=2023, int month=23, int day=12)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}
private:
	int _day;
	int _month;
	int _year;
};

int main()
{
	Date H;//一个普通的对象
	H.Print();
}

但有时候为了保护我们的对象我们会在对象前面加上const防止对象被修改。
在这里插入图片描述但这时候程序会报错,因为H这时候是一个const对象,const的H不能调用非const的Print()函数。 为了可以让Print()变成const成员:
在这里插入图片描述这时候H就可以调用Print()函数了,**此时大家都是const,权限平移。我们之前谈到过权限可以缩小平移,但不能放大。**意思就是:

非const对象可以调用const成员函数。(权限的缩小)
const对象可以调用const成员函数。(权限的平移)
const对象可以调用非const成员函数。(权限的放大)

取地址及const取地址操作符重载

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

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

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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值