构造函数
概念
class Date{
public:
void SetDate(int year, int month, int day){
_year = year;
_month = month;
_day = day;
}
void Display(){
cout <<_year<< "-" <<_month << "-"<< _day <<endl;
}
private:
int _year;
int _month;
int _day;
};
int main(){
Date d1;
d1.SetDate(2018,5,1);
d1.Display();
Date d2;
d2.SetDate(2018,7,1);
d2.Display();
return 0;
}
大家看上面的函数,我们可以发现如果每个对象被创建之后都需要调用成员函数才能够给其赋值,那这会十分的麻烦,我们有什么办法可以解决呢?
我们可以使用构造函数来解决,构造函数是一个特殊的成员函数,名字与类名相同,在我们创建对象时由编译器自动调用,保证每个数据成员都有 一个合适的初始值,并且在对象的生命周期内只调用一次。
特性
- 函数名与类名相同;
- 无返回值;
class Date{
public:
//构造函数,与类名相同,无返回值
Date(int year, int month, int day){
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
- 构造函数可以重载,对象实例化时编译器自动调用对应的构造函数;
class Date{
public :
//1.无参构造函数
Date (){}
//2.带参构造函数 1
Date (int year, int month, int day ){
_year = year ;
_month = month ;
_day = day ;
}
//3.带参构造函数 2
Date (int year, int month, int day, int num){
_year = year ;
_month = month ;
_day = day ;
}
private :
int _year ;
int _month ;
int _day ;
};
void TestDate(){
Date d1; //调用无参构造函数
Date d2 (2015, 1, 1); //调用带参的构造函数 1
Date d3 (2015, 2, 2, 520);//调用带参的构造函数 2
//注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
//以下代码的函数:声明了d4函数,该函数无参,返回一个日期类型的对象
Date d4();
}
- 如果类中没有显式定义构造函数,则 C++ 编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成;
- 无参的构造函数、全缺省的构造函数、我们没写编译器默认生成的构造函数都称为默认构造函数,但是默认构造函数只能有一个;
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;
}
- 在 C++ 中把类型分成内置类型(基本类型)和自定义类型。内置类型就是语法已经定义好的类型;自定义类型就是我们使用 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;
}
- 类的构造函数调用顺序为:全局对象后于局部对象进行构造,静态对象后于普通对象进行构造;
析构函数
概念
析构函数与构造函数功能相反,但是析构函数不是对对象的销毁(对于局部对象的销毁工作一般由编译器完成),而是对象在销毁时会自动调用析构函数,完成对象的一些资源清理工作,例如使用malloc
手动开辟的内存空间等,这些都属于析构函数需要清理的资源;
特性
- 析构函数名是在类名前加上字符
~
; - 析构函数无参数无返回值;
//定义一个顺序表类
typedef int DataType;
class SeqList{
public :
//析构函数,在析构函数中主要是清理类占用的资源空间
~SeqList()
{
//资源清理
}
- 一个类有且只有一个析构函数,若未显式定义,系统会自动生成默认的析构函数;
- 对象生命周期结束时,C++ 编译系统会自动调用析构函数;
//定义一个顺序表类
typedef int DataType;
class SeqList{
public :
//构造函数
SeqList (int capacity = 10){
//手动开辟内存空间
_pData = (DataType*)malloc(capacity * sizeof(DataType));
//如果空间申请失败,就会退出程序
assert(_pData);
_size = 0;
_capacity = capacity;
}
//析构函数,在析构函数中主要是清理类占用的资源空间
~SeqList(){
if (_pData){
free(_pData ); // 释放堆上的空间
_pData = nullptr; // 将指针置为空
_capacity = 0;
_size = 0;
}
}
private :
int* _pData ;
size_t _size;
size_t _capacity;
};
- 析构函数同样和构造函数一样,在系统调用构造函数时,析构函数会首先为自定义类型调它们的析构函数;
- 类的析构函数调用完全按照构造函数调用的相反顺序进行调用,全局对象先于局部对象进行析构,静态对象先于普通对象进行析构;
拷贝构造函数
概念
拷贝构造函数只有单个形参,该形参是对本类类型对象的引用(一般常用const
修饰,至于为什么要用引用,后面再说),在用已存在的类类型对象创建新对象时由编译器自动调用;
特性
- 拷贝构造函数是构造函数的一个重载形式;
- 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用,下图为错误结果,下方代码为正确写法;
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;
}
- 若未显示定义,系统生成默认的拷贝构造函数,默认的拷贝构造函数将对成员变量进行简单的直接赋值拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝。不管是自己写的还是系统生成的,在遇到自定义类型时,都会自动调用自定义类型的拷贝构造函数。
- 当我们在实现简单的类的对象拷贝时,可以不自己实现拷贝构造函数,使用系统生成的即可,但是如果存在资源分配的成员变量时,再使用系统生成的拷贝构造函数,那么会出错的,下面的代码就是因为这种原因出错的;
在下面的代码中,对象 s1 申请一片空间存放 “hello” 内容,假设空间首地址为 0x11,那么 s1._str = 0x11;此时拷贝构造 s2,由于我们没有显示定义拷贝构造函数,系统会自动生成,然后直接将 s1 的内容复制给 s2,所以 s2._str = 0x11;当对象生命周期结束之后,系统自动调用析构函数,首先执行free(s2._str)
,然后执行free(s1._str)
,那么问题就来了,此时将 0x11 空间释放了两次,第一次正确释放,0x11 不属于我们了,当第二次释放时,就会出问题了,而在以后,我么可以通过深拷贝来进行解决;
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");
//用已经存在的 s1 对象来拷贝构造一个新对象 s2
String s2(s1);
}
运算符重载
概念
相信大家都知道函数重载,也就是同名不同参的函数执行不同的操作,在 C++ 中,运算符也是可以重载的,比如说,在我们的日期类中,当我们想要比较两个日期的大小时,直接使用大于小于号是不能比较的,此时我们可以对运算符进行重载,然后在函数中写出比较规则,这样我们就可以使用运算符来比较自定义类型了;
在进行运算符重载时需要用到关键字operator
,使用语法如下:
返回值类型 operator运算符(参数1, 参数2, ...){
//执行的运算操作
}
注意事项
- 不能通过连接其他符号来创建新的操作符,只能重载已有的合法运算符:比如
operator@
是错误的; - 重载操作符必须有一个类类型或者枚举类型的操作数;
- 内置类型的操作符,其含义不能改变,例如:内置的整型
+
,不能改变其含义,使其成为做减法运算的操作符; - 作为类成员的重载函数时,其形参要比操作数数目少一个,因为成员函数有一个默认的形参
this
,该形参限定为第一个参数,this
指针指向调用对象或者从左向右的第一个操作数; .*
、::
、sizeof
、?:
、.
,注意以上 5 个运算符不能重载,这是规定,没有为什么;- 在全局定义的运算符重载无法调用私有中的成员变量,这该怎么办呢?等后面我们学了友元即可解决;
- 下面是一个日期类,在类中存在一个比较运算符重载,使其可以比较两个日期是否相等:
class Date{
public:
//全缺省的构造函数
Date(int year = 1900, 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){
//这里的操作其实是 this->_year == d2._year
return _year == d2._year
&& _month == d2._month
&& _day == d2._day;
}
private:
int _year;
int _month;
int _day;
};
void Test (){
Date d1(2018, 9, 26);
Date d2(2018, 9, 27);
//正常的调用
cout<< d1.operator==(d2) << endl;
//这是一种简写形式,this 指针自动指向从左向右的第一个操作数
cout<< d1 == d2 << endl;
}
- 当我们在重载前置加加和后置加加时,会发现无法重载,因为这两个函数名字相同,参数相同,只有返回值不同,这样是无法重载的,此时我们可以进行如下操作来解决:
//以日期类来举例
//前置加加,无参,this 指针指向调用对象或者操作数
//返回一个对象的引用,这是因为前置加加返回的是加完之后的结果,所以可以使用引用
date& operator++(){
//内容
}
//后置加加,有参,但是不用传任何参数,编译器会自动识别,this 指针指向调用对象或者操作数
//返回一个对象的拷贝,因为后置是先使用再加加,所以需要定义一个临时对象来保存未加加的内容,并将其返回,而临时变量我们无权访问,也就不能使用引用
date operator++(int){
//内容
}
赋值运算符重载
当我们在定义 d1、d2 对象之后,想要将 d2 的内容赋值给 d1,者能否实现呢?经过检验是可以的,因为在 C++ 的类中,存在一个赋值运算符重载函数,它的功能就是将一个类的值全部赋给另一个类,这个函数我们可以显示的定义,也可以由系统自动生成;代码如下:
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;
}
//赋值运算符重载
Date& operator=(const Date& d){
if(this != &d){
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
private:
int _year ;
int _month ;
int _day ;
};
- 该成员函数只有一个参数,另一个为默认的
this
指针,该指针指向调用的对象或者从左向右第一个操作数;
//将 d2 的值赋给 d1
//完整写法
d1.operator=(d2);
//简化写法
d1 = d2;
- 函数的返回值是一个对象的引用,这样可以进行链式赋值赋值,如下:
//先进行 d3 的值赋给 d2,此时返回一个对象的引用
//不管是返回的是 d2 还是 d3 都是相等的,然后再赋值给 d1
d1 = d2 = d3;
- 可以在函数中做一个小小的优化,当
this
指针和参数的地址相同时,说明是将某个对象赋值给本身,可以跳过不执行; - 其实赋值运算符重载和拷贝构造函数差不多相同,赋值运算符重载是将一个对象的值赋给另一个已存在的对象,拷贝构造函数是用一个对象的值来创建一个新的对象;
- 赋值运算符重载函数当我们没定义时,系统会自动生成一个,但是注意的是,当对象当中存在资源时,就不能直接使用系统自动生成的,因为会出错,原因和拷贝构造函数一样;
const
与取地址
const
修饰类的成员函数
将const
修饰的类成员函数称之为const
成员函数;const
修饰类成员函数,实际上是修饰该成员函数隐含的this
指针,表明在该成员函数中不能对类的任何成员进行修改,也就是说,在指针常量类的类型* const this
的基础上,变成了常量指针常量const 类的类型* const this
,也就变成了既不能修改指向也不能修改指向的内容。
是用方法如下:
class Date{
public :
void Display(){
cout<< "Display()" << endl;
cout<< "year:" << _year << endl;
cout<< "month:" << _month << endl;
cout<< "day:" << _day << endl;
}
//将const写在函数的后面
void Display() const{
cout<<"Display() const" << endl;
cout<< "year:" <<_year<< endl;
cout<< "month:" <<_month<< endl;
cout<< "day:" <<_day << endl;
}
private :
int _year ; // 年
int _month ; // 月
int _day ; // 日
};
void Test (){
Date d1;
d1.Display();
const Date d2;
d2.Display();
}
注意事项
const
对象不可以调用非const
成员函数;- 非
const
对象可以调用const
成员函数; const
成员函数内不可以调用其它的非const
成员函数;- 非
const
成员函数内可以调用其它的const
成员函数;
取地址操作
在类中,对于有无const
修饰的取地址函数,我们都不需要去管,系统会自动生成两个默认的取地址重载函数;因为如果我们手动操作的话,很容易造成错误,比如在返回的时候并不是返回this
指针指向的地址,而是返回一个未知的地址,那么会造成未定义行为,更有甚者,若是返回一个病毒的入口地址,那么会造成很严重的后果。
class Date{
public :
Date* operator&(){
//此处容易出错,若是随便一个之都能返回,很容易给木马病毒造成漏洞
return this;
}
const Date* operator&()const{
//此处容易出错,若是随便一个之都能返回,很容易给木马病毒造成漏洞
return this;
}
private :
int _year ; // 年
int _month ; // 月
int _day ; // 日
};
实例应用
下面,我么来使用前面学的内容来完成一个日期类的代码,主要实现日期之间的运算,具体代码如下:
#include<iostream>
using namespace std;
class Date{
public:
// 获取某年某月的天数
int GetMonthDay(int year, int month) {
//以数组写出每月的天数,月份与下表对应
int marr[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
//需要注意的是二月的天数有变化,区分闰年和非闰年的不同
if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0) {
if (month == 2) {
return marr[month] + 1;
}
return marr[month];
}
return marr[month];
}
// 全缺省的构造函数
Date(int year = 1, int month = 1, int day = 1) {
//检测输入日期是否符合常理,若果有误,设置为默认值
if (year < 1 || month < 1 || month > 12 || day < 1 || day > GetMonthDay(year, month)) {
_year = 1;
_month = 1;
_day = 1;
cout << "输入日期有误,已设为:1-1-1" << endl;
}
else {
_year = year;
_month = month;
_day = day;
}
}
// 拷贝构造函数
Date(const Date& d) {
_year = d._year;
_month = d._month;
_day = d._day;
}
// 赋值运算符重载
Date& operator=(const Date& d) {
if (this != &d) {
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
// 析构函数
~Date() {}
// 日期+=天数
Date& operator+=(int day) {
//看看当前月份剩余天数是否满足所加天数
day = day - GetMonthDay(_year, _month) + _day;
//若果不满足就循环向上加
while (day > 0) {
//月份变化,如果超过12,则向上加一年,并将月份置为一月
if (++_month > 12) {
_month = 1;
_year++;
}
_day = 0;
day = day - GetMonthDay(_year, _month);
}
//最终日期为剩余的不大于当前月份的天数
_day = GetMonthDay(_year, _month) + day;
//此处返回的是一个外部变量,所以可以返回引用
return *this;
}
// 日期+天数
Date operator+(int day) {
//复用上面的代码,不改变当前日期的前提下,返回一个相加之后的日期
Date tmp(*this);
tmp += day;
//此处返回的是一个局部变量,所以不能返回引用
return tmp;
}
// 日期-天数
Date operator-(int day) {
//复用代码
Date tmp(*this);
tmp -= day;
return tmp;
}
// 日期-=天数
Date& operator-=(int day) {
//看看当前月份天数是否足够减
day = day - _day;
//如果不够减,那么就循环变化
while (day >= 0) {
//月份减一,如果减一后小于1,那么就年份减一,并将月份置为十二月
if (--_month < 1) {
_month = 12;
//如果年份减一后小于1,那么就将日期置为默认值
if (--_year < 1) {
_year = 1;
_month = 1;
_day = 1;
}
}
_day = GetMonthDay(_year, _month);
day = day - _day;
}
//最终日期为不够减的相反数
_day = -day;
return *this;
}
// 前置++
Date& operator++() {
*this += 1;
return *this;
}
// 后置++
Date operator++(int) {
Date tmp(*this);
*this += 1;
return tmp;
}
// 后置--
Date operator--(int) {
Date tmp(*this);
*this -= 1;
return tmp;
}
// 前置--
Date& operator--() {
*this -= 1;
return *this;
}
// >运算符重载
bool operator>(const Date& d) {
//如果年份大于,那么返回真
if (_year > d._year) {
return true;
}
//如果年份相等,那么比较月份
else if (_year == d._year) {
//如果月份大于,返回真
if (_month > d._month) {
return true;
}
//如果月份相等,那么比较天数
else if (_month == d._month) {
//天数大于,返回真
if (_day > d._day) {
return true;
}
}
}
//否则返回假
return false;
}
// ==运算符重载
bool operator==(const Date& d) {
//年月日都相等才返回真
if (_year == d._year && _month == d._month && _day == d._day) {
return true;
}
return false;
}
// >=运算符重载
inline bool operator >= (const Date& d) {
if (*this > d) {
return true;
}
else if (*this == d) {
return true;
}
return false;
}
// <运算符重载
bool operator < (const Date& d) {
if (*this >= d) {
return false;
}
return true;
}
// <=运算符重载
bool operator <= (const Date& d) {
if (*this > d) {
return false;
}
return true;
}
// !=运算符重载
bool operator != (const Date& d) {
if (*this == d) {
return false;
}
return true;
}
// 日期-日期 返回天数
int operator-(const Date& d) {
//用小日期一天一天加,直到等于大日期,返回所需的相加天数,需要注意正负
if (*this >= d) {
int count = 0;
Date date(d);
while ((date + count) != *this) {
count++;
}
return count;
}
else {
int count = 0;
while ((*this + count) != d) {
count++;
}
return -count;
}
}
void print() const {
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main() {
return 0;
}