文章目录
类的6个默认成员函数
如果一个类中什么成员都没有,简称空类。
但是空类中在编译器编译时也会生成6个默认成员函数;
默认成员函数:就是当我们不写这个六个构造函数时,编译器会默认生成这六个默认构造函数。
构造函数
构造函数的概念
构造函数:是一种特殊的成员函数,名字与类名相同,创建类类型对象(对象实例化时)由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象周期内只调用一次。
比如:
class Date
{
public:
Date(int year = 0, 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;
};
构造函数的特性
一:构造函数的函数名与类名相同。
二:构造函数无返回值
并不是我们默认为返回值为void,而是没有返回值(void不用写)。
三:对象初始化时编译器自动调用对应的构造函数来初始化。
四:构造函数支持函数重载
我们可以有多种初始化对象的方式,编译器会根据我们所传递的参数去调用对应的构造函数。
但是当我们创建一个类时,写了以上两个默认构造函数构成重载时,编译器会主动调用对应的构造函数,但是由与两个都属与默认构造函数,编译器无法了解到底该调用哪个默认构造函数。
五:无参的构造函数,全缺省的构造函数与i以及我们不写编译器自动生成的构造函数都称为默认构造函数,并且默认构造函数只能有一个;
结论:只要不是依靠我们传参进行初始化的构造函数就是默认构造函数。
六:如果类中没有显示定义构造函数,则C++编译器会主动生成一个无参的默认构造函数,如果我们显式定义了,则编译器就不再生成。
比如:
当我们不写构造函数时,查看类成员的初始化值。
#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;
return 0;
}
运行结果如下:
C++类型分类:
1:内置类型: int / doube / char / 指针./ Stack(编译器会调用它函数成员里面的构造函数)
2: 自定义类型:struct/class
由运行结果可知:
编译器自动生成的默认构造机制:
a: 对内置类型成员不做处理。
b: 自定义类型成员,编译器会去带哦用它们自己的默认构造函数。
结论:大部分情况下,我们最好是自己写构造函数。
析构函数
析构函数的概念
析构函数:与构造函数功能相反,析构函数不是完成对对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会主动调用析构函数,完成对象中资源的清理工作。
析构函数的特性
一:析构函数名在类名前加上字符~。
二:无参无返回类型。
三:一个类只能与有一个析构函数,如果没有显示定义,系统会自动生成默认的析构函数,
注意:析构函数不能重载
四:对象生命周期结束时,C++编译系统生成析构函数。
编译器自动生成的析构函数机制:
1:内置函数类型不处理。(函数栈帧机制会主动销毁)
2:自定义类类型成员会主动调用该成员的析构函数,函数运行结束,栈里面的空间还在,需要写析构函数进行空间释放。
结论:
编译器默认生成机制,实际是对自定义类型的类的(构造,析构)函数的调用机制,对内置函数类型不做处理。
例如:
我们创建了一个默认构造函数,并动态开辟了一个数组。
#include <iostream>
using namespace std;
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 3)
{
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (NULL == _array)
{
perror("malloc申请空间失败!\n");
return;
}
_capacity = capacity;
_size = 0;
}
void push(DataType data)
{
_array[_size] = data;
_size++;
}
~Stack()
{
cout << "~Stack()" << endl;
if (_array)
{
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
int _capacity;
int _size;
};
int main()
{
Stack s;
s.push(1);
s.push(2);
return 0;
}
运行结果如下:
我们知道当一个类对象销毁时,其中的局部变量也会随着对象的销毁而销毁,但是经过动态开辟出来的数组并不会随之销毁,而是保存下来。这样容易造成内存泄漏等问题,此时编译器会主动调用析构函数对数组进行销毁。
五:先构造的后析构,后构造的先析构;
先后创建两个对象aa1,aa2,再分别对他们进行析构:
public:
B(int a = 0)
:_a(a) //对a进行初始化。
{
cout << "B( int a = 0)->" << _a<< endl;
}
~B()
{
cout << "~A()->" << _a << endl;
}
private:
int _a;
};
int main()
{
B aa1(1);
B aa2(2);
return 0;
}
运行如下:
结论:先定义的对象后析构,只是由于对象定义是在函数中的,也符合函数栈帧先进后出的特点,在函数栈帧结束时,才会调用析构函数,否则将会造成内存泄露。
如果我们在定义类的时加上了staic会将如何?
**结论:
1:全局变量,静态变量都存在于静态区。
2:全局变量在main函数建立时就已经先进行初始化了。
3:全局变量,静态变量也符合先定义后销毁。
拷贝析构函数**
拷贝析构函数的概念
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类型对象创建对象时由编译器自动调用。
如果引用不加const的话,那么d1的值就可以被改变,那么这就与拷贝构造函数相违背,所以平常写引用构造函数时要加const修饰。
拷贝析构函数的特性
1:拷贝构造函数是构造函数的一个重载形式。
拷贝函数函数名与构造函数相同,函数功能与构造函数类相似,只是函数形参,与定义的方式不同.
2:拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器会直接报错因为会引发无穷递归调用。
如果使用传值传参:
如果要调用拷贝大构造函数——先传参——传值传参进行本类类型对象的拷贝——调用拷贝构造函数,这样反复循环,没有终点。
3: 如果为显示定义,编译器会生成默认的拷贝构造函数。
#include <iostream>
using namespace std;
typedef int DataType;
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._month;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022,9,28);
Date d2(d1);
return 0;
}
调试如下:
我们并没有写拷贝构造函数但是编译器在编译时会默认生成调用拷贝构造函数。
编译器自动生成的拷贝构造函数机制:
1:编译器自动生成的拷贝构造函数对内置类型会完成浅拷贝。(值拷贝)
2:对于自定义类型,会去调用它们自己的默认拷贝构造函数完成拷贝。
编译器自动生成的拷贝构造函数不能实现深拷贝
例如:对于自定义类型,当我们使用默认拷贝构造函数时并打印两个对象的数组地址:
class Stack
{
public:
Stack(size_t capacity = 3)
{
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (NULL == _array)
{
perror("malloc申请空间失败!\n");
return;
}
_capacity = capacity;
_size = 0;
}
void Print()
{
cout << _array << endl;
}
void push(DataType data)
{
_array[_size] = data;
_size++;
}
~Stack()
{
cout << "~Stack()" << endl;
if (_array)
{
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
int _capacity;
int _size;
};
int main()
{
Stack s;
s.push(1);
s.Print();
Stack s1(s);
s1.Print();
return 0;
}
调试结果如下:
通过编译器生成的默认拷贝构造函数拷贝的Stack 两个对象的数组地址相同。
这是由于:s1对象使用s拷贝构造,而Stack没有显示定义拷贝构造函数,则编译器会产生一份默认的拷贝构造函数,而默认拷贝构造函数是按照值拷贝的,即是原封不动的拷贝到s1中,所以s1,s2的指向同一块内存空间。
当程序退出时,s和s1会调用析构函数,s1中的数组空间先被销毁,之后s中的数组空间又会被重新销毁,同一块空间重新被销毁两次会造成程序的崩溃。
总结:
1:如果类中没有设计资源申请时(比如动态开辟数组),拷贝构造函数写不写都可以。
2:一旦涉及到资源申请时,则拷贝构造函数一定要写,不然就是浅拷贝,会造成程序的崩溃。
赋值运算符重载
运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,具有其返回值类型(与普通的函数类似),函数名字,以及参数列表。
函数名字:关键字operator后面接需要重载的运算符符号
**函数原型:**返回值类型operator操作符(形参列表)
注意:
1:不能通过连接其他符号来创建新的操作符:比如operator@.
2:对于内置类型的操佐夫,重载后其函数一不能改变。
3:作为类成员的重载函数,函数有一个默认的形参this,指向第一个形参。
4: sizeof / :: / .* / ?: / . 这五个运算符不能重载。
比如:运算符重载,当我们调**用d1d2**运算符重载时,**可以默认为d1.operator(&d1,&d2),**此时运算重载的第一个形参默认为this指针作为传递调用对象d1的地址。
class Date
{
public:
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
bool operator==(const Date& d)// 运算符重载函数
{
return _year == d._year
&&_month == d._month
&&_day == d._day;
}
private:
int _year;
int _month;
int _day;
};
我们可以将代码简短且频繁调用的成员函数定义在类里面来作为内联函数,也可以将该运算符放在类外面,但此时无法在类外访问类中的成员变量,可以将类中的成员设为公有,但会损失类的封装性,我可以在类里面用上友元函数。此外,在类外并没有this指针,那么我们形参必须显示两个。
class Date
{
friend bool operator==(const Date& d1, const Date& d2);
public:
//构造函数会频繁调用,所以直接放在类里面定义作为inlinel;
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// d1=d3;
void operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
bool operator==(const Date& d1, const Date& d2)
{
return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day;
}
但是对于赋值运算符来说,如果不显示实现,编译器会生成一个默认的。打此我们再在类外自己实现一个全局的赋值运算符重载,就会和编译器在类中的生成默认运算符冲突,所以赋值运算符只能是类的成员函数。
例如:我们创建了一个全局的赋值运算符重载,编译不通过.
赋值运算符重载
以=运算符重载为例:
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// d1=d3;
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
void Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year;
int _month;
int _day;
};
注意:
1:首先:赋值运算符重载的第一个形参默认的是this指针(指的是调用类的地址),第二个形参指的是我们赋值运算符的右操作数。
假如我们使用传值传参,会额外调用一次拷贝构造函数,所以函数的第二个参数最好使用引用传参。
然后:第二个参数,对于赋值运算符的右操作符,它是给其他对象赋值而不用改变,且一般不用考虑函数调用成员的权限限制,一边拿加上const。
比如:当我们使用传值传参。
由运行结果可知:若用传值传参,会额外调用一次拷贝构造函数。
2:函数的返回值使用引用返回:
为了支持赋值运算符的连续赋值,我们需要为函数设置一个返回值,此时的返回值应该是赋值运算符的左操作符,即this指针所指向的对象,并且们实在类外定义一个类的,因为当函数返回时,而这个this指针所指向的对象没有被销毁,所以可以使用引用返回。
比如:
赋值运算符引用返回和引用传参。
该赋值运算符支持连续赋值 d1 = d2 = d3;
4:赋值前检查不能给自己赋值
因为自己给自己赋值是多余的操作,在赋值操作前可先判断能否给自己赋值,加上你好操作。
5:引用返回的是this
在函数体内我们只能通过this指针来访问到左操作数,赋完值后返回得this。
6: 当用户没有显示实现时,编译器会生成一个默认赋值运算符函数,以值得到方式继续宁拷贝。
注意:内置类型是直接赋值的,二自定义类行需要调用对应类的赋值运算符重载完成赋值。
const成员
const修饰类的成员函数
将const修饰的”成员函数“称为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该函数成员中不能对类的任何成员进行改变。
1:富有const修饰的类调用函数时调用函数时会出现错误。
比如:带有const修饰的类d2调用不带const修饰的成员函数。
根据指针的特性:类类型为Date* const this
结论:当d2的地址传递给this指针时,类型有const Date变成了Date const(权限增大编译不通过),而d1可以传过的原因是d1的地址类型为Date 可以传给Date const(权限由大变小)。
所以当我们在printf函数的生命和定义处加上cosnt;(const修饰的是this指针);**
加入const后:
1:d1: 由Date* const this 传递为 const Date* cconst (权限缩小)。
d2: 由 const Date* const this 传递为const Date *const this 权限不变(注意:权限的增大与减小之对指针和引用有作用。)
总结:为了避免调用时this指针权限范围问题,在this不改变时同一在函数的声明和定义处加上const.
日期类的实现
以下为日期类中所包含的成员函数和成员变量:
class Date
{
public:
//求某月的天数;
GetMonthDay(int year, int month)
// 构造函数
Date(int year = 0, int month = 1, int day = 1);
// 打印函数
void Print() const;
// 日期+=天数
Date& operator+=(int day);
// 日期+天数
Date operator+(int day) const;
// 日期-=天数
Date& operator-=(int day);
// 日期-天数
Date operator-(int day) const;
// 前置++
Date& operator++();
// 后置++
Date operator++(int);
// 前置--
Date& operator--();
// 后置--
Date operator--(int);
// 日期的大小关系比较
bool operator>(const Date& d) const;
bool operator>=(const Date& d) const;
bool operator<(const Date& d) const;
bool operator<=(const Date& d) const;
bool operator==(const Date& d) const;
bool operator!=(const Date& d) const;
// 日期-日期
int operator-(const Date& d) const;
// 析构,拷贝构造,赋值重载可以不写,使用默认生成的即可
private:
int _year;
int _month;
int _day;
};
获取某月天数和构造函数
inline int GetMonthDay(int year, int month) //获取某年某月的天数吗,会被频繁调用,所以直接在类里面定义作为
//inline;
{
static int DayArray[] = { 0 , 31,28,31,30,31,30,31,31,30,31,30,31 };
int day = DayArray[month];
if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
{
day = 29;
}
return day;
}
bool Date::CheckDate()
{
if (_year >= 1 && _month > 0 && _month < 13 && _day>0 && _day <= GetMonthDay(_year, _month))
{
return true;
}
else
{
return false;
}
}
Date(int year, int month, int day)//这里不采用默认构造,因为要指定日期传参。
{
_year = year;
_month = month; //这里面的函数出了栈就被销毁了,不需要析构函数。
_day = day;
if (!CheckDate())
{
cout << "日期非法\n" << endl; //检测用户初始化的日期是否违法。
}
}
注意:GetMonthDay函数中的细节:
1:函数多次被调用,将他设置为内联函数。
2:函数中存储每月天数的数组最好用static修饰,因为static存储在静态区,不会随着函数的销毁而销毁,避免每次调用函数都需要重新开辟数组。
3:逻辑中应先判断month == 2是否为真,再看是否为闰年。(这样写可以提高运行效能)。
打印函数
void Date::Print() const
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
日期 += 天数
Date& Date::operator +=(int day) // d1 += d1 + = 100;有返回值需要连续赋值
{
_day += day;
while (_day > GetMonthDay(this->_year, this->_month))
{
_day -= GetMonthDay(_year, _month); //需要考虑天数再考虑月数,因为某个月的天数是根据月数算的。
_month += 1;
if ( _month > 12)
{
_year += 1;
_month = 1;
}
}
return *this;
};
日期 + 天数
对于日期 + 天数来说,基本的过程和日期+=天数过程相似。
唯一的区别就是它加上天数并不会改变this本身,所以我们要取一个变量ret去加天数,返回ret改变的类。
Date Date::operator+(int day) //加自己不改变,改变的是拷贝对象,加了之后拷贝对象会消失。
{
Date ret = *this; //赋值是两个已经存在的对象初始化,拷贝对象是一个存在一个不存的的类进行初始化。
ret += day; //赋值运算符重载。
return ret; //返回的是ret的拷贝,并不会让this指向的对象发生改变,只要求它的值。
}
那么如果我们先实现日期+天数,然后用日期+=天数曲去复用呢?
日期+天数如图:
总结:用日期+=天数去复用日期+天数时,这样比直接写日期 + 天数多了两次类的拷贝,增加了程序的负担,降低效率。
日期 -= 天数
日期 -天数 时天数与月份只有1没有0天,有0天就要改变月份,当月份为0时,就要变为12,年减1,最后要注意负的_day天加上上个月份的总天数就等于真正的日期。
Date& Date::operator-=(int day) //如果删的大于这个月的天数就将上一个月补,如果月份为零就为12;
{
_day -= day; //—_day此时为负数;
while (_day <= 0)
{
_month -= 1;
if (_month == 0)
{
_month = 12;
_year -= 1;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
日期 - 天数
日期-天数 返回的是返回值,没有改变this指针及其指向的内容,所以我们要加上const。
此外,因为和日期-=天数的实现方式相似,所以我们可以进行复用。
// 日期-天数
Date Date::operator-(int day) const
{
Date tmp(*this);// 拷贝构造tmp,用于返回
// 复用operator-=
tmp -= day;
return tmp;
}
注意:因为日期-天数这个函数重载tmp出了函数作用域就被销毁了,所以不能用引用传参。
前置++
前置++返回的是this指针指向的内容的值,返回的是this指针本身。
Date& Date::operator++()
{
*this = (*this) + 1;
return *this;
}
后置++
后置++返回的是this指针改变前的值,所以要用一个变量保留。
Date Date::operator++(int)
{
Date tmp(*this); //返回相加前的值。
*this = *this + 1;
return tmp;
}
总结:
1:前置++ 和后置++的this指针都发生了改变,返回值不同。
2:为了区分成员函数重载,C++给后置++的参数上加上了一个int型的参数,使用后置++并不需要给int参数传参。
3:前置++thisthis指针出了函数作用域并未销毁,所以可以加上&,后置++tmp对象出了函数作用域就被销毁了,所以不能用&。
前置–
// 前置--
Date& Date::operator--()
{
*this -= 1;
return *this;
}
后置–
// 后置--
Date Date::operator--(int)
{
Date tmp(*this);
// 复用operator-=
*this -= 1;
return tmp;
}
总结:前置-- 与后置–和前置++与后置++的原理相似。
日期类的大小关系比较
>运算符的重载
因为答案都是ture,所以将日期类的大小判断需要分多个条件将多个变量控制变量用|| , &&放在一起。
bool Date::operator>(const Date& d) const
{
if ((_year > d._year) || (_year == d._year && _month > d._month) || ( _year == d._year && _month == d._month&&_day >d._day))
{
return true;
}
else
{
return false;
}
}
==运算符的重载
bool Date::operator == (const Date& x) const
{ //引用传值相对与传值传参,可以提高效率,减少空间浪费。
//const不能x1不修改时使用。防止书写错误。
return _year == x._year
&& _month == x._month
&& _day == x._day;
}
>=运算符的重载
bool Date:: operator >=( const Date& d) const
{
return ((*this) > d)|| ((*this) == d));
}
<运算符的重载
bool Date:: operator < ( const Date& d) const
{
return !(*this >= d);
}
<=运算符的重载
bool Data:: operator<=(const Date& d)const
{
return !(*this > date);
}
!=运算符的重载
bool Date:: operator!= operator(const Date&d) const
{
return ((*this) == d);
}
总结:对于以上自定义类型重载(除了内置类型),当我们需要时便可显示实现,也可以进行复用,return 符合操作符的内容范围。
日期 - 日期
日期-日期即先求出两个日期类中较小的类,然后将让将小的类一直加1并计算加1的次数直至与日期较大的类相等,flag则控制最后返回值的正负,如果前一个数小于后一个,则最后的答案应该为负值。
int Date:: operator-(const Date& d) const
{
Date max = *this;
Date min = d;
int flag = 1 //控制正负
if( max < d)
{
max = d;
min = *this;
flag = -1;
}
int gap = 0;
while( min < max)
{
min += 1;
gap++;
}
return gap*flag;
}