在上一篇中我们讨论了什么是面向对象编程,类的定义,作用域,实例化,访问限定符,大小计算,以及一个很重要的知识this指针。
https://blog.csdn.net/hellooworldd/article/details/122641166?spm=1001.2014.3001.5501
下面介绍进一步的内容。
类的6个默认成员函数
c++类中如果一个成员没有,那么这个类叫空类,但是这个类虽然我们在代码中什么东西都看不见,但是编译器会给我们生成六个默认成员函数: 1构造函数,2析构函数,3拷贝构造函数,4赋值重载函数,56还有两个取地址重载函数。这六个函数我们主要看前四个函数。后两个函数我们自己很少实现,这里并不提及。
这里我们以日期类举例来实现前四个默认成员函数,其他的类都可以迁移。
#pragma once
#include<iostream>
using namespace std;
class Date
{
public:
// 获取某年某月的天数
int GetMonthDay(int year, int month);
// 全缺省的构造函数
Date(int year = 1900, int month = 1, int day = 1);
// 拷贝构造函数
// d2(d1)
Date(const Date& d);
// 赋值运算符重载
// d2 = d3 -> d2.operator=(&d2, d3)
Date& operator=(const Date& d);
// 析构函数
~Date();
// 日期+=天数
Date& operator+=(int day);
// 日期+天数
Date operator+(int day);
// 日期-天数
Date operator-(int day);
// 日期-=天数
Date& operator-=(int day);
// 前置++
Date& operator++();
// 后置++
Date operator++(int);
// 后置--
Date operator--(int);
// 前置--
Date& operator--();
// >运算符重载
bool operator>(const Date& d);
// ==运算符重载
bool operator==(const Date& d);
// >=运算符重载
inline bool operator >= (const Date& d);
// <运算符重载
bool operator < (const Date& d);
// <=运算符重载
bool operator <= (const Date& d);
// !=运算符重载
bool operator != (const Date& d);
// 日期-日期 返回天数
int operator-(const Date& d);
private:
int _year;
int _month;
int _day;
};
以上是我们需要实现的所有函数,此类主要涉及年月日的计算比较等。一开始看不懂没关系,下面我们一个一个举例。
构造函数
在用类创建对象时,我们创建对象后对象的个类信息并没有被设置,虽然我们可以使用一些成员函数对其进行设置,但是比较麻烦,所以我们就有了构造函数,在创建对象时对对象初始化。
构造函数是特殊的成员函数,需要注意的是,构造函数的虽然名称叫构造,但是需要注意的是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
构造函数有以下特征:
-
函数名与类名相同。
-
无返回值。
-
对象实例化时编译器自动调用对应的构造函数。
-
构造函数可以重载。
-
如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
-
无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认成员函数。
-
当我们没写构造函数时,编译器会生成默认构造函数,他对内置类型不做处理(像char int double等),对自定义类型会调用它的构造函数(如class struct等类)。
所以日期类的构造函数可以这样写
Date::Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
if (!(_year >= 0 && (_month > 0 && _month < 13) && (_day > 0 && _day <= GetMonthDay(_year, _month))))
{
cout << "非法输入" << endl;
cout << _year << "-" << _month << "-" << _day << endl;
}
}
析构函数
既然我们在创建对象时已经可以自动初始化对象了,那么我们是不是可以在对象不使用或者或出了作用域后自动销毁呢?这就用到了我们接下来讲的析构函数,此函数会在销毁时自动调用,完成类的资源清理工作。
其特征如下:
-
析构函数名是在类名前加上字符 ~。
-
无参数无返回值。
-
一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
-
对象生命周期结束时,C++编译系统系统自动调用析构函数。
-
编译器生成的默认析构函数,对自定类型成员调用它的析构函数。
日期类中成员变量均会随栈帧销毁而销毁所以可以不调用析构函数,只需要用默认生成的析构函数就可以了,但是我们也可以写出来。
Date::~Date()
{
cout << "~Date()" << endl;
}
在有需要释放空间的类中可以使用如free或者delete等在析构函数中自动销毁。
拷贝构造函数
当我们在创建一个对象时,想要创建一个和一个已经创造的对象一模一样的对象时,我们可以用拷贝构造函数。
拷贝构造函数有以下特征:
-
拷贝构造函数是构造函数的一个重载形式。
-
拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用。
-
若未显示定义,系统生成默认的拷贝构造函数。默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝
-
编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,所以日期类其实我们可以不用写拷贝构造。
注:篇幅受限本文并不涉及深拷贝知识,想要了解请自行学习或在本博客以后的文章中搜索。
我们可以用日期类来演示以下拷贝构造:
Date::Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
运算符重载
C++为了增强代码的可读性引入了运算符重载**,**运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型operator操作符(参数列表)
要注意以下几点
不能通过连接其他符号来创建新的操作符:比如operator@
重载操作符必须有一个类类型或者枚举类型的操作数
用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不能改变其 含义
作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的
操作符有一个默认的形参this,限定为第一个形参
.* 、:: 、sizeof 、?: 、. 注意以上5个运算符不能重载。
下面给出日期类的代码:
Date& Date:: operator+=(int day)
{
if (day < 0)
{
*this -= (-day);
return *this;
}
_day += day;
while (_day > GetMonthDay(_year, _month))
{
day -= GetMonthDay(_year, _month);
_month++;
if (_month == 13)
{
_month == 1;
_year++;
}
}
return *this;
}
Date Date::operator+(int day)
{
Date tmp(*this);
tmp += day;
return tmp;
}
Date& Date::operator-=(int day)
{
if (day < 0)
{
*this += (-day);
return *this;
}
_day -= day;
while (_day <= 0)
{
--_month;
if (_month == 0)
{
--_year;
_month = 12;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
Date Date::operator-(int day)
{
Date tmp(*this);
tmp -= day;
return tmp;
}
// 前置++
Date& Date::operator++()
{
*this += 1;
return *this;
}
// 后置++
Date Date::operator++(int)
{
Date tmp(*this);
*this+=1;
return tmp;
}
// 后置--
Date Date::operator--(int)
{
Date tmp(*this);
*this -= 1;
return tmp;
}
// 前置--
Date& Date::operator--()
{
*this -= 1;
return *this;
}
// >运算符重载
bool Date::operator>(const Date& d)
{
if (_year > d._year)
{
return true;
}
else if (_year == d._year && _month > d._month)
{
return true;
}
else if(_year == d._year&&_month==d._month&&_day>d._day)
{
return true;
}
else
{
return false;
}
}
// ==运算符重载
bool Date::operator==(const Date& d)
{
return ((_year == d._year) && (_month == d._month) && (_day == d._day));
}
// >=运算符重载
inline bool Date::operator >= (const Date& d)
{
return *this > d || *this == d;
}
// <运算符重载
bool Date::operator < (const Date& d)
{
return !(*this >= d);
}
// <=运算符重载
bool Date::operator <= (const Date& d)
{
return !(*this > d);
}
// !=运算符重载
bool Date::operator != (const Date& d)
{
return !(*this == d);
}
// 日期-日期 返回天数
int Date::operator-(const Date& d)
{
Date max = *this;
Date min = d;
int flag = 1;
if (*this < d)
{
max = d;
min = *this;
flag = -1;
}
int count = 0;
while (max != min)
{
min++;
count++;
}
return count * flag;
}
这里需要注意的是由于前置++和后置++都只有一个隐含的this指针为参数,所以为了区别,将后置++的参数列表添加一个int占位符用来区分,这个地方编译器会自动识别,只需要记住格式就可以。当然–运算符也类似。
赋值运算符重载
赋值运算符重载需要考虑几个因素
-
参数类型
-
返回值
-
检测是否自己给自己赋值
-
返回*this
-
一个类如果没有显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝。
以下为详细代码
Date&Date:: operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
简单提一下取地址及const取地址操作符重载
这两个默认成员函数一般不用重新定义 ,编译器默认会生成。
class Date
{
public :
Date* operator&()
{
return this ;
}
const Date* operator&()const
{
return this ;
}
private :
int _year ; // 年
int _month ; // 月
int _day ; // 日
};
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容。
补充知识
将const修饰的类成员函数称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。当这个成员函数你并不想对成员变量进行更改时可以再函数名后加const有利于检测出是否你误用修改了成员变量。
如比较大小的运算符重载函数
bool Date::operator>(const Date& d)const
{
if (_year > d._year)
{
return true;
}
else if (_year == d._year && _month > d._month)
{
return true;
}
else if(_year == d._year&&_month==d._month&&_day>d._day)
{
return true;
}
else
{
return false;
}
}