【初识C++】2.2类与对象(中)

1.类的六个默认函数

如果说一个类什么成员都没有,简称空类。但是空类中并不是什么都没有,任何一个类创建完成的情况下,都会自动生成一下六个默认函数

calss Date
{};

就算是这样的一个空类,也会编译器也会自动生成六个默认函数

6个默认函数
初始化清理
构造函数主要完成初始化工作
析构函数主要完成清理工作
拷贝复制
拷贝构造是使用同类型对象初始化创建对象
赋值重载主要是把一个对象赋值给另外的对象
取地址重载
主要是普通对象和const对象取地址这两个很少会自己实现

接下来就让我们讲讲这六个函数的用处

2.构造函数

2.1概念

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

2.2特性

构造函数是特殊的成员函数,需要注意的是,构造函数的虽然名称叫构造,但是需要注意的是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
其特征如下:
1 . 函数名与类名相同。
2 . 无返回值。
3 . 对象实例化时编译器自动调用对应的构造函数。
4 . 构造函数可以重载。
接下来我们继续用我们的Date类型举例

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

int main()
{
	Date d1;//调用无参构造函数
	Date d2(10);//调用带参构造函数
	Date d3(1, 2, 3);
	return 0;
	//注意:如果通过无参构造函数创建对象的时候,对象后面不需要加括号,否则就变成了函数声明
	//Date d3();这是一个函数的声明,返回值是Date类型,函数名是d3,函数无参
}

通过调试
在这里插入图片描述
还有程序的运行结果我们可以看到在三个对象的创建过程中,确实调用了构造函数,并且就算不传参数它也会自动调用只是定义,它其实也是默认调用了构造函数
在这里插入图片描述

5.如果类中没有显式定义构造函数,C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义了,则编译器将不再生成。所以我们没有定义构造函数,对象也可以创建成功,因为创建过程中会调用编译器自动生成的构造函数
6. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认成员函数
7. 关于编译器生成的默认成员函数,大家可能会有疑惑,因为在Date类型对象的创建过程中,如果我们调用的是编译器自动生成的默认函数,就会发现对象中的_year_month\day还都是随机值。也就是说系统生成的构造函数好像没有啥作用。

其实在这个地方,C++将类型分为内置类型(基本类型)和自定义类型进行区别对待。内置类型就是语法已经定义好的类型,如int/char/float等。自定义类型是我们使用class/union/struct自己定义出来的类型。当我们的自定义类型(类型1)初始化时,如果这个自定义类型中还套有另外一个自定义类型(类型2),那么编译器生成的默认构造函数会让类型2自动调用类型2的默认构造函数,这里比较绕,具体和下图一起结合理解
类型1 Date 类型2 Stack
实验1:此时我们没有放入自定义类型

class Stack//类型2(是1个栈)
{
public:
	Stack(int capacity = 4)//Stack的默认构造函数
	{
		if (capacity <= 0)
		{
			capacity = 0;
			int* _a = nullptr;
			_capacity = _size = 0;
		}
		else
		{
			_a = (int*)malloc(sizeof(int) * capacity);
			_size = 0;
			_capacity = capacity;
		}
	}
private:
	int* _a;
	int _size;
	int _capacity;
};

class Date//类型1
{
//此时我们没有显示定义Date的默认构造函数
private:
	int _year;
	int _month;
	int _day;
	//Stack _st;//没有放入我们的自定义类型
};

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

通过调试我们看一下结果
在这里插入图片描述
此时程序已达末尾但是d1并没有显著改变,里面的值还是随机值。

实验2
在Date类型的成员变量中添加一个Stack类型(类型2)的成员,然后创建一个Date类型的对象

class Stack//类型2(是1个栈)
{
public:
	Stack(int capacity = 4)//Stack的默认构造函数
	{
		if (capacity <= 0)
		{
			capacity = 0;
			int* _a = nullptr;
			_capacity = _size = 0;
		}
		else
		{
			_a = (int*)malloc(sizeof(int) * capacity);
			_size = 0;
			_capacity = capacity;
		}
	}
private:
	int* _a;
	int _size;
	int _capacity;
};

class Date//类型1
{
//此时我们没有显示定义Date的默认构造函数
private:
	int _year;
	int _month;
	int _day;
	Stack _st;
};

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

调试
在这里插入图片描述
此时我们并没有显式定义Date的默认构造函数,但是Date中的_year,_month,_day被初始化为0,而其中的自定义类型Stack _st调用了自己的默认构造函数。

结论:编译器自动生成的默认构造函数并不是什么事情都不做。编译器默认生成的构造函数,会对自定义类型成员调用它的默认构造函数
注:实验2中这样的情况,内置类型可能会改变初始化为0,也可能继续是随机值,根据编译器不同而定

8.成员变量的命名风格
(一)用_(下划线开头)

class Date
{
private:
	int _year;
	int _month;
	int _day;
	Stack _st;
};

(二)用m_(下划线)开头

class Date
{
private:
	int m_year;
	int m_month;
	int m_day;
	Stack _st;
};

目的
将成员变量和成员函数的形参区分开来,防止混淆

class Date
{
public:
     Date(int year)
     {
        year = year;
     }
private:
	int year;
}

若不加以区分,很容易出现形参和成员变量名字相同情况。

3.析构函数

3.1概念

析构函数:与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。

3.2特性

析构函数是特殊的成员函数
其特性如下:
1.析构函数名是在类名前加上字符~。
2.没有参数,并且没有返回值。
3.一个类只有一个析构函数。若没有显式定义,系统会自动生成默认的析构函数。
4.对象的声明周期结束时,C++编译器会自动调用析构函数。

class Stack
{
public:
	Stack(int capacity = 4)
	{
		if (capacity <= 0)
		{
			capacity = 0;
			int* _a = nullptr;
			_capacity = _size = 0;
		}
		else
		{
			_a = (int*)malloc(sizeof(int) * capacity);
			_size = 0;
			_capacity = capacity;
		}
	}
	~Stack()//析构函数
	{
		free(_a);
		_a = nullptr;
		_capacity = _size = 0;
	}
private:
	int* _a;
	int _size;
	int _capacity;
};

~Stack()就是我们的析构函数,当一个Stack类型的对象声明周期结束时,编译器会自动调用,可以释放堆上的空间,并且将指针置为空。防止忘记free导致内存泄漏。
5.和构造函数相同,编译器自动生成的析构函数,会对自定义类型成员调用它的析构函数

4.拷贝构造函数

4.1概念

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

4.2特征

拷贝构造函数也是特殊的成员函数,其特征如下:
1.拷贝构造函数是构造函数的一个重载形式
2.拷贝构造函数的参数只有一个并且必须要引用传参,使用传值会引发无穷递归调用

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;
			
	}
private:
	int _year;
	int _month;
	int _day;
    };


int main()
{
	Date d1;//使用构造函数创建对象
	Date d2(d1);//使用拷贝构造函数创建对象
	return 0;
}

假若传值而非引用会发生以下这种情况
Date 的形参d是d1的临时拷贝,d的构建就等同于const Date d(d1),因为此时创建了d,则就要取调用d的拷贝构造函数,从而发生无穷递归调用

在这里插入图片描述
`
3.若未显示定义,系统生成默认的拷贝函数。默认的拷贝函数对象按照存储的字节序完成浅拷贝,又是可能会发生错误。当对象中包含自定义类型时,会调用自定义类型的默认拷贝函数。
例如:

class Stack
{
	int* a;
	int* size;
	int* capacity;
};


int main()
{
	Stack st1;
	Stack st2(st1);
	return 0;
}

如图我们创建两个栈,st2 是 st1 的拷贝,系统默认调用拷贝函数进行浅拷贝,则st2.a = st1.a,则此时两个栈都指向同一块空间,公用同一个数组,改变st1就等于改变st2。假如Stack类包含析构函数,则st2销毁时调用一次,st1销毁时还得调用一次.但是他们两个指向同一块空间,同一块空间释放两次就会造成系统报错。

在这里插入图片描述

所以像Stack这样的类,编译器默认生成的拷贝构造函数无法满足我们的需求,需要我们自己实现深拷贝。深拷贝是一个复杂的过程,具体后面讲解

5.赋值运算符重载

5.1概念

C++为了增加代码的可读性引入运算符重载,运算符重载是具有特殊函数名的函数,让自定义类型可以像内置类型一样使用运算符,需要哪个运算符就重载哪个运算符
函数名字:关键字operator 后面接需要重载的运算符符号
函数原型:返回值类型 operator操作符(参数列表)

.* :: sizeof ?: . 注意以上五个运算符不能重载

5.2特性

//在类型创建中进行的运算符重载
void operator=(const Date&d);
//此时d1 = d2 等价与 d1.operator=(d2);
void operator=(const Date&d)
{
    _year = d.year;
    _month = d.month;
    _day = d.day;
}

注意:以上函数时存在问题的,因为我们在实际运用过程中还可能遇到连续赋值的情况,例如a=b=c,在这种情况下我们的函数就无法满足需求。所以我们对以上函数进行优化

Date& operator=(const Date&d)
{
    _year = d.year;
    _month = d.month;
    _day = d.day;
    return *this;
}

我们进行连续赋值d1 = d2 = d3 ,此时d2和d3之间调用运算符重载函数,按道理来说应该返回d2,而此时我们只有d2的地址(this),所以我们需要解引用来返回d2。
这里我们还要思考,传值返回,编译器会生成一个临时对象接收运算符重载函数的返回值,此时就等于又创建了一个Date类型的对象,又要调用拷贝函数,所以返回d2也不是最优解
为了提高效率,我们选择传引用返回,此时返回值是d2的引用,这样就不需要再创建新的对象来接收返回值
判断是否传引用返回的重要依据是,返回值出了函数作用域生命周期是否结束,结束传值,没结束传引用

为了防止出现自己给自己赋值降低效率的现象,我们可以加一个判断语句进行优化

Date& operator=(const Date& d)
{
	if (this != &d)//检查是否自己给自己赋值
	{
		_year = d.year;
		_month = d.month;
		_day = d.day;
		return *this;
	}
}

以上即是完整的函数运算符重载函数的写法

与其他默认函数相同,即使我们不写编译器也会自动生成一个默认成员函数。 编译器默认生成赋值运算符跟拷贝构造的特性是一样的
a、针对内置类型,会完成浅拷贝,也就是说像Date这样的类不需要我们自己写,但是像Stack这样的类,就得我们自己写。
b、针对自定义类型,则会调用自定义类型自己的运算符重载完成拷贝

6.总结

构造函数和析构函数的特性是类似的,编译器对内置类型不做处理,自定义类型调用它们自己的析构函数
拷贝构造和赋值重载的特性是类似的,编译器对内置类型浅拷贝,而自定义类型会调用它的拷贝构造和赋值重载

下面再思考一个小问题

Date d1;
Date d2 = d1;//等价与 Date d2(d1)

请问这个调用的是拷贝构造函数还是赋值重载函数呢
拷贝构造 : 拿一个已经存在的对象去构造初始化另一个要创建的对象
赋值重载 : 两个已经存在的对象之间的拷贝
所以调用的是拷贝构造函数

最后我们完整的创建一个Date类

//Date。h
#pragma once
#include <iostream>
#include <assert.h>

using std::cout;
using std::cin;
using std::endl;



class Date
{
public:
	//构造函数
	Date(int year = 1, int month = 1, int day = 1);
	//打印日期
	void Print();
	//析构、拷贝构造、赋值重载可以不写,默认生成的就够用了

	//+操作符重载
	Date operator+(int day);
	//-操作符重载
	Date operator-(int day);
	//+=操作符重载
	Date& operator+=(int day);
	//-=操作符重载
	Date& operator-= (int day);
	//++d前置加加重载
	Date& operator++();
	//后置加加重载,int没什么用,只是占位符用于区别前置和后置,构成函数重载
	Date operator++(int);
	//--前置减减重载
	Date& operator--();
	//--后置减减重载
	Date operator--(int);
	
private:
	int _year;
	int _month;
	int _day;
};


inline int GetMonthDay(int year, int month)
{
	
	static int month_day[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
	int day = month_day[month];
	//判断是否是闰年二月
	if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
	{
		day = 29;
	}
	return day;
}
//Date.cpp
#include "Date.h"
Date::Date(int year, int month, int day)
{
	//保证日期的合法性
	if (year > 0 &&
		month > 0 && month < 13 &&
		day > 0 && day <= GetMonthDay(year, month))
	{
		_year = year;
		_month = month;
		_day = day;
	}
	else
	{
		cout << "日期不合法" << endl;
		assert(false);
	}

}

void Date::Print()
{
	printf("%d year %d month %d day\n", _year, _month, _day);
}

Date Date::operator+(int day)
{
	Date ret(*(this));
	ret._day += day;
	//如果大于该月天数
	while (ret._day > GetMonthDay(ret._year, ret._month))
	{
		ret._day -= GetMonthDay(ret._year, ret._month);
		ret._month += 1;
		//如果month大于12,year+1
		if (ret._month > 12)
		{
			ret._month -= 12;
			ret._year += 1;
		}
	}
	return ret;
}


Date Date::operator-(int day)
{
	Date ret(*this);
	ret._day -= day;
	while (ret._day <= 0)
	{
		ret._month -= 1;
		if (ret._month < 1)
		{
			ret._month += 12;
			ret._year -= 1;
		}
		ret._day += GetMonthDay(ret._year, ret._month);
	}
	return ret;
 }

 Date& Date::operator-=(int day)
{
	 Date ret(*this);
	 (*this) = (*this) - day;
	 return (*this);
}


 Date& Date::operator+=(int day)
 {
	 Date ret(*this);
	 (*this) = (*this) + day;
	 return (*this);
 }
 
 Date& Date::operator++()
 {
	 return *this += 1;
 }

 Date Date::operator++(int)
 {
	 Date ret(*this);
	 *this += 1;
	 return (*this);
 }

 Date& Date::operator--()
 {
	 return *this -= 1;
 }

 Date Date::operator--(int)
 {
	 Date ret(*this);
	 *this -= 1;
	 return (*this);
 }

以上即是博客的全部内容,感谢观看。

上一篇:【初识C++】2.1类与对象(上)

下一篇:【初识C++】2.3类与对象(下)

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小白在进击

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值