类的6个默认成员函数
如果一个类中什么成员都没有,简称为空类。空类中什么都没有吗?并不是的,任何一个类在我们不写的情 况下,都会自动生成下面6个默认成员函数。
构造函数:
定义:构造函数是一个特殊的成员函数,名字与类名相同,创建类 类型对象时由编译器自动调用,保证每个数据成员都有 一个合适的初始值,并且在对象的生命周期内只调用一次。
特性:
1.不是开空间创建对象,而是初始化对象。
-
函数名与类名相同。
-
无返回值。
-
对象实例化时 编译器自动调用对应的构造函数。
5.构造函数可以重载。
cpp
#include <iostream>
#include <assert.h>
using namespace std;
class Date
{
private:
int _year;
int _month;
int _day;
public:
//1.无参构造函数
Date()
{
}
// 2.带参构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
};
int main()
{
Date d1; // 调用无参构造函数
Date d2(2015, 1, 1); // 调用带参的构造函数
// 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
// 以下代码的函数:声明了d3函数,该函数无参,返回一个日期类型的对象
Date d3();
return 0;
}
根据不同的初始化需求,去选择构造函数:
6.如果类中没有显式定义构造函数 ,则C++编译器会自动生成一个无参的默认构造函数 ,一旦用户显式定义编译器将不再生成。
cpp
class Date
{
private:
int _year;
int _month;
int _day;
void MPrintf()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
};
int main()
{
// 没有定义构造函数,对象也可以创建成功,因此此处调用的是编译器生成的默认构造函数
Date d1;
d1.MPrintf();
return 0;
}
7.无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参 构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认成员函数。
默认成员函数分为:
为什么默认构造函数只能有一个?
回答:
当同时有无参构造函数和全缺省构造函数时,在实例化过程中编译器无法判断选择哪一种构造函数。
cpp
//为什么默认构造函数只能有一个?
class Date
{
private:
int _year;
int _month;
int _day;
public:
//1.无参构造函数
Date()
{
}
// 2.全缺省构造函数
Date(int year=2018, int month=1, int day=1)
{
_year = year;
_month = month;
_day = day;
}
void MPrintf()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
};
int main()
{
// 调用无参构造函数
Date d1;
//全缺省构造函数--注释1.无参构造函数
Date d2(1111);
d2.MPrintf();
Date d3(2222, 2);
d3.MPrintf();
Date d4(3333, 3, 3);
d4.MPrintf();
return 0;
}
此时编译器是无法确定选择哪一种默认构造函数;
全缺省构造函数结果:
8.默认构造函数多用于自定义类型
对于自定义类型(复杂情况)我们会用构造函数的默认生成,更方便
cpp
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;
}
调试结果:
对于内置类型,默认构造函数时不进行内容的改变,只保留随机值。
对于自定义类型,我们设置了他的默认成员函数,随机值进行了改变,在以后多次使用tmie类型是,他的初始化内容都会改变成自己一开始设置的初始化内容。
内置和自定义混合的,可以给内置缺省
此处不是初始化(空间没有创造就不能算是初始化,只能算是声明缺省值)
解释:默认构造没有参数传递,使用其原本的缺省值
由此得出:
默认生成构造函数:
1.内置类型成员不做处理
2.自定义类型成员,会去调用它的默认构造(不用传参数的构造)
建议:每个类都提供一个默认构造函数(内置类型->缺省构造)
析构函数:
定义:与构造函数功能相反,析构函数不是完成对象的销毁 ,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。
特征:
-
析构函数名是在类名前 加上字符 ~。
-
无参数无返回值。
-
一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
-
对象生命周期结束时,C++编译系统系统自动调用析构函数。
拷贝构造函数:
定义:只有单个形参 ,该形参是对本类类型对象的引用(一般常用const修饰 ),在用已存在的类 类型对象创建新对象时由编译器自动调用。(将已近实例化好的类型对象拷贝给将要实例化的新的对象的构造函数)
特征:
-
拷贝构造函数是构造函数的一个重载形式。
-
拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用。
为什么只传一个参数?
回答:其在结构体内部,本身还有一个隐形的This指针。
必须使用引用的原因?
回答:避免出现使用传值方式会引发无穷递归调用。--在拷贝时先准备启用拷贝构造函数,传值时会调用拷贝构造函数。这样会形成无穷递归调用。(不理解直接记成:拷贝构造函数必用引用)
使用const的原因?
回答:在传递过程中,将可读可写的修改成const(只读)模式。将其权限缩小,避免因为自己思路的问题而导致原本值的改变。
- 若未显示定义,系统生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝。
代码:
cpp
#include <iostream>
using namespace std;
class Date
{
public:
//构造函数-全缺省
Date(int year = 2018, 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;
//}
void Mprintf()
{
cout << _year << "/" << _month<<"/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Mprintf();
Date d2(d1);
d2.Mprintf();
return 0;
}
结果:
通过两次运行结果对比发现貌似这个拷贝构造函数有无都一样?
其实不然,C语言本身是对一些内置类型(int char等)在编译器的底层是有类似拷贝构造函数的结构的。
但是,面对一些自定义类型,或者申请有申请空间的情况,C语言不能满足需求
那就需要我们自己写专门针对自己的类的类型的拷贝构造函数
如以下代码:
cpp
//开辟一个字符串空间
class String
{
public:
String(const char* str = "jack")
{
_str = (char*)malloc(strlen(str) + 1);
strcpy(_str, str);
}
//析构函数
~String()
{
cout << "~String()" << endl;
free(_str);
}
private:
char* _str;
};
int main()
{
String s1("hello");
String s2(s1);
}
此时拷贝的运行结果就是错误。
什么情况下用拷贝构造函数:
自己实现了析构函数释放空间,就需要实现拷贝构造函数;
拷贝构造应用场景:
获取x天后的日期:
cpp
//拷贝构造应用场景
class Date
{
public:
//构造函数--全缺省
Date(int year = 2018, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//打印函数
void Mprintf()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
int GetMonthDay(int year,int month)
{
assert(month > 0 && month < 13);
//正常情况下当前月数返回的天数,月从1开始,所以多加一位占位
int monthArray[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
//闰年2月天数情况
if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400) == 0))
{
return 29;
}
else
{
return monthArray[month];
}
}
//x天后的日期
Date GetAfterXDay(int x)
{
//因为不想改变初始日期所以使用拷贝
Date tmp = *this;
tmp._day += x;
while (tmp._day > GetMonthDay(tmp._year, tmp._month))
{
tmp._day -= GetMonthDay(tmp._year, tmp._month);
++tmp._month;
if (tmp._month == 13)
{
tmp._year++;
tmp._month = 1;
}
}
return tmp;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2222, 2, 2);
//d1.GetAfterXDay(100)调用类d1里的GetAfterXDay函数
//返回一个Date类型的对象再拷贝到(Date d2=返回值)d2中
Date d2 = d1.GetAfterXDay(100);
d1.Mprintf();
d2.Mprintf();
return 0;
}
赋值运算符重载
运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类 型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)(对于比较类,不需要更改值,常用const修饰)
用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不 能改变其含义
.* 、:: 、sizeof 、?: (三目运算符)、. 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
公共成员:
要在类外调用还得将声明对象的private改成public
cpp
class Date
{
public:
Date(int year = 1111, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//private:
int _year;
int _month;
int _day;
};
//d1与d2位置分别对应==的两端,不能修改
//参数和操作数(函数形参位置)是成正比的
bool operator==(const Date& d1, const Date& d2)
{
return d1._year == d2._year
&& d1._month == d2._month
&& d1._day == d2._day;
}
int main()
{
Date d1(2018, 9, 26);
Date d2(2018, 9, 27);
cout << (d1 == d2) << endl;
//d1==d2会自动call对于函数地址
//改变成->operator==(d1,d2)进行比较
}
为什么要在输出是加括号:
回答:因为<< 和== 优先级不同cout << d1 == d2 << endl;只会先识别cout << d1
成员函数:
cpp
class Date
{
public:
Date(int year = 1111, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//因为是成员函数,所以有其自身的this
// 完全展开后:bool operator==(Date* this, const Date& d2)
bool operator==(const Date& d2)
{
return _year == d2._year
&& _month == d2._month
&& _day == d2._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2018, 9, 26);
Date d2(2018, 9, 27);
d1 == d2;
//因为是成员函数,其内部自身有隐藏this所以值传递一个值
d1.operator==(d2);
cout << (d1 == d2) << endl;
cout << d1.operator==(d2) << endl;
}
并不局限于==除了**.* 、 ::** 、 sizeof 、 ?: (三目运算符) 、 . 这5个外都可以用operator(运算符)更改
运算符重载的复用:
代码:
cpp
//运算符重载的复用
class Date
{
public:
Date(int year = 1111, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//因为是成员函数,所以有其自身的this
// 完全展开后:bool operator==(Date* this, const Date& d2)
bool operator==(const Date& d2)
{
return _year == d2._year
&& _month == d2._month
&& _day == d2._day;
}
//复用!!
//由于已知了==,根据==就能写出!=(> < >= <=(小于或等于)同理)
bool operator!=(const Date& d)
{
return !(*this == d);
//相等1再!->false
//不等0再!->true
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2018, 9, 26);
Date d2(2018, 9, 27);
cout << (d1!=d2) << endl;//显示1,true
return 0;
}
赋值运算符重载 =
主要特点:
-
参数类型
-
返回值
-
检测是否自己给自己赋值
-
返回*this
-
一个类如果没有显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝。
赋值运算使用:
常用域自己实现了析构函数释放空间,就需要使用自己编写的赋值运算符重载;
cpp
class Date
{
public:
Date(int year = 1111, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Mprintf()
{
cout << _year << "/" << _month<<"/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
class String
{
public:
//拷贝
String(const char* str = "")
{
_str = (char*)malloc(strlen(str) + 1);
strcpy(_str, str);
}
//析构
~String()
{
cout << "~String()" << endl;
free(_str);
}
void Mprintf()
{
cout << _str << endl;
}
private:
char* _str;
};
int main()
{
Date d1(2222, 9, 26);
Date d2(3333, 9, 27);
String s1("hello");
String s2("world");
d1.Mprintf();
d2.Mprintf();
s1.Mprintf();
s2.Mprintf();
//编译器生成的默认赋值重载函数已经可以完成字节序的值拷贝了
d1 = d2;
s1 = s2;//报错
//此时,自己没有写赋值重载函数,当s2赋值给s1后
//s1和s2同时指向相同的一块空间(s2开出的)
//后序清理资源,清理s2的时候,s1所指向的空间也会改变
//所以错误,编译器不通过
d1.Mprintf();
s1.Mprintf();
return 0;
}
连续赋值
有返回值用于支持这里的连续赋值,保持运算符的特性
eg.i=j=k;连续赋值的顺序是先j=k后返回j再i=j;
所以由于以上连续赋值的可能性,也要考虑赋值运算符重载的连续赋值的可能。
cpp
class Date
{
public:
Date(int year = 1111, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Mprintf()
{
cout << _year << "/" << _month<<"/" << _day << endl;
}
//d3=d2=d1
//返回的是赋值的左操作数所以返回*this
//返回后该操作数的生命周期还存在所以用引用返回
Date& operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(1111, 1, 1);
Date d2(2222, 2, 2);
Date d3(3333, 3, 3);
d1.Mprintf();//1111,1,1
d1 = d2 = d3;
d1.Mprintf();//3333,3,3
return 0;
}
自己给自己赋值
有时不小心的操作可能会造成自己给自己赋值的情况
所以还需要在自己写的赋值情况中加入if判断
总结:
赋值重载和拷贝构造区别:
赋值重载是多个已近定义出来的对象
拷贝构造是一个已经实例化的对象初始化另一个未实例化的对象
在使用引用返回时,不能盲目的为了追求使用引用返回而使用静态变量static
静态变量static在整体函数调用中,只会初始化他的第一次,再次走到static内一步时,不会再初始化,而是使用其之前的值;