类和对象(中)
创始人
2024-05-03 17:45:29
0

 原文再续,书接上回!! 继续类和对象的学习。

目录

 构造函数

 析构函数

拷贝构造 

赋值重载

运算符重载

 const成员

取地址及const取地址操作符重载  


当我们没有向类中写入任何成员的时候(也就是空类),类中就什么也没有吗?答案是否定的,任何类在什么都不写时,编译器会自动生成6个默认成员函数。

这六个默认成员函数,是用户不显示实现,编译器会自动生成的成员函数。你可以想象它们六兄弟是替你守江山的忠诚卫士,前4个兄弟用户实现的情景还比较多见。老五老六很少需要用户动手实现。

 构造函数

注意:构造函数名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象

构造函数主要完成初始化工作,它有如下特性:(代码演示以日期类为例)

●函数名与类名相同,没有返回值。 注意:如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成!。 不显示定义构造函数:
#include using namespace std;class Date
{
public:void Print(){cout << _year << "-" << _month << "-" << _day << endl;}
private:int _year;int _month;int _day;
};int main()
{Date d1;d1.Print();return 0;
}

在没有写构造函数的情况下,应该调用默认的构造函数初始化对象。但是从结果来看好像编译器并没有帮我们完成这件事,这里先带着疑问继续探索!

无参构造:
  //无参构造Date(){//...}

注意:通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明 。

    Date d1;d1.Print();

效果和不显示定义是一样的!

有参构造:
   //有参构造Date(int year, int month, int day){_year = year;_month = month;_day = day;}
    Date d3(2020,10,3);d3.Print();

全缺省构造:
 //全缺省构造   Date(int year = 2020, int month = 12, int day = 26){_year = year;_month = month;_day = day;}
    Date d4;d4.Print();

    Date d2(1,1,1);d2.Print();

半缺省构造:

    //半缺省构造Date(int year, int month = 10, int day = 3){_year = year;_month = month;_day = day;}
    Date d4(1999);d4.Print();

●构造函数可以重载。

class Date
{
public:Date(){_year = 1900;_month = 1;_day = 1;}Date(int year, int month, int day){_year = year;_month = month;_day = day;}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}
private:int _year;int _month;int _day;
};
int main()
{Date d1;d1.Print();Date d2(2000,2,3);d2.Print();return 0;
}

●对象实例化时编译器自动调用对应的构造函数,保证每个数据成员都有 一个合适的初值。

问题:在未显示定义构造的情景下,编译器生成的默认构造好像并没有什么作用。

答:默认生成的构造函数只处理自定义类型,内置类型不做处理。

内置类型:内置类型就是语言提供的数据类 型,如:int/char...
自定义类型:使用class/struct/union等自己定义的类型。
#include using namespace std;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 d1;return 0;
}

 通过调试发现,编译器生成默认的构造函数会对自定类型成员_t调用它的默认成员函数!!内置类型确实没有做任何处理。根据这一现象,内置类型成员变量在类中声明时可以给默认值

	//内置类型int _year = 2001;int _month = 10;int _day = 3;//自定义类型Time _t;

注意:这里给的是缺省值,并不是初始化!

小总结:

♠构造函数在不显示定义的情况下,会自动生成。

♠默认构造函数对内置类型不做处理,自定义成员调用它自己的默认构造函数。为了同时处理内置类型,可以在声明的时候给缺省值。

构造函数在对象整个生命周期内只调用一次

♠注意:不仅仅是编译器自动生成的构造函数才是默认的,无参、全缺省、用户没显示定义编译器自动生成的构造函数,都是默认构造函数!

 析构函数

注意:析构函数的任务不是完成对对象本身的销毁(局部对象销毁工作是由编译器完成的),而是对象在销毁时会自动调用析构函数,完成对象中资源的清理工作

析构函数主完成对象中资源的清理,其特征如下: (代码演示以顺序表为例)

●析构函数名是在类名前加上字符 ~,无参数无返回值类型。
typedef int DataType;
class SeqList
{
public://全缺省构造SeqList(int capacity = 3){_arr = (DataType*)malloc(sizeof(DataType) * capacity);if (nullptr == _arr){perror("malloc:");exit(-1);}_capacity = capacity;_size = 0;}void PushBack(DataType data){_arr[_size] = data;_size++;}void Print(){for (int i = 0; i < _size; i++){cout << _arr[i] << " ";}}//析构~SeqList(){if (_arr){free(_arr);_arr = NULL;_capacity = 0;_size = 0;}}
private:DataType* _arr;int _capacity;int _size;
};
int main()
{SeqList s;s.PushBack(1);s.PushBack(2);s.PushBack(3);s.Print();return 0;
}

 默认构造完成初始化工作!

向顺序表中插入数据!

打印顺序表中的数据!

●对象生命周期结束时,C++编译系统系统自动调用析构函数!

一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。 还是上面的代码,将显示定义的析构函数删掉,当对象生命周期结束时,自动调用析构函数,但是并没有完成资源清理的任务!

原因:内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可。对象销毁时,要保证每个自定义对象都可以正确销毁,创建哪个类的对象则调用该类的析构函数,销毁那个类的对象则调用该类的析构函数。

	class Time{public:~Time(){cout << "~Time()析构函数调用" << endl;_hour = 0;_minute = 0;_second = 0;}private:int _hour = 1;int _minute = 1;int _second = 1;};class Date{private:// 基本类型(内置类型)int _year = 1970;int _month = 1;int _day = 1;// 自定义类型Time _t;};int main(){Date d;return 0;}

注意:析构函数不能重载! 总结:如果类中没有申请资源,可以不写析构函数,使用编译器生成的默认析构函数就可以,就好比日期类。当类中有资源申请时,就要考虑资源泄露的问题,一定要写析构函数,比如顺序表类,栈类等。

拷贝构造 

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

 拷贝构造函数主要完成的工作是,创建对象时,创建一个与已存在对象一某一样的新对象。

先从语法来看,拷贝构造函数的参数只有一个,是类类型对象的引用。拷贝构造函数是构造函数的一个重载形式。(代码演示以日期类为例)

class Date
{
public:Date(int year = 2000, int month = 10, int day = 3){_year = year;_month = month;_day = day;}Date(const Date& d) {_year = d._year;_month = d._month;_day = d._day;}void Print(){cout << _year << "/" << _month << "/" << _day<

 拷贝构造函数的函数名也和类名相同,不同点是参数类型不同,所以说和构造函数是函数重  载。

这里肯定有小伙伴存有疑问,拷贝构造函数的参数我一定要传本类类型的引用吗,我想用传值方式试试!!

	Date(const Date d) {_year = d._year;_month = d._month;_day = d._day;}

报错的原因是什么呢?

答:参数传值,形参是实参的一份拷贝,所以传值会引发对象的拷贝,倒置无穷递归。

class Date
{
public://构造函数Date(int year = 2000, int month = 10, int day = 3){_year = year;_month = month;_day = day;}//拷贝构造Date(const Date& d) {cout << "传值,形参拷贝实参,调用拷贝构造Date"<

 传引用:

	Fun2(d3);

 传引用,形参是实参的别名。没有引发对象的拷贝!

上面的程序很好的证明,传值方式会调用拷贝构造,因为形参拷贝实参时用已存在的类类型对象创建新对象。而调用拷贝构造传值还会重复这个动作,就引发了无穷递归!

 ●不显示定义拷贝构造函数。

注意:编译器默认生成的拷贝构造函数,内置类型是按照字节方式直接拷贝的,自定 义类型是调用它自己拷贝构造函数完成拷贝的。
//不显示定义默认拷贝构造
class Time
{
public:Time(){_hour = 12;_minute = 30;_second = 30;}Time(const Time& t){_hour = t._hour;_minute = t._minute;_second = t._second;}
private:int _hour;int _minute;int _second;
};
class Date
{
private:// 基本类型int _year = 2001;int _month = 10;int _day = 3;// 自定义类型Time _t;
};int main()
{Date d1;Date d2(d1);return 0;
}

通过调试发现,默认生成的拷贝构造和构造、析构不同的地方是,它不仅仅拷贝自定义类型,内置类型的数据也进行了拷贝!

妙哉,妙哉!这样说来不用自己动手写拷贝构造,用编译器自动生成的不就可以了?

像日期类这样不设计资源申请的,用编译器自动生成的拷贝是没有问题的。但是如果涉及到资源申请,则拷贝构造就一定要写!比如下面的场景:(代码以栈为例)

typedef int DataType;
class Stack
{
public://构造函数Stack(size_t capacity = 10){_array = (DataType*)malloc(capacity * sizeof(DataType));if (nullptr == _array){perror("malloc");exit(-1);}_size = 0;_capacity = capacity;}void Push(const DataType& data){_array[_size] = data;_size++;}//析构函数~Stack(){if (_array){free(_array);_array = nullptr;_capacity = 0;_size = 0;}}
private:DataType* _array;size_t _size;size_t _capacity;
};
int main()
{Stack s1;s1.Push(1);Stack s2(s1);return 0;
}

 内置类型是按照字节方式直接拷贝, 我们通过直观的发现问题似乎也不大,就是s2对象拷贝的空间和对象s1是一块空间。

继续调试,程序崩溃掉了,错误大意是断言失败!

原因是在对象生命周期结束时,C++编译系统系统自动调用析构函数! 先建立的对象后销毁,后建立的对象先销毁!也就是说在析构的过程中,存在同一块地址空间二次释放的问题。上述的拷贝方式称为浅拷贝,面对这种场景,浅拷贝显然是有问题的。需要用户显示定义拷贝构造函数!(针对上述问题,显示定义拷贝构造如下)

	//显示定义拷贝构造Stack(const Stack& s){_array = (DataType*)malloc(s._capacity * sizeof(DataType));for (int i = 0; i < s._size; i++){_array[i] = s._array[i];}_size = s._size;_capacity = s._capacity;}

 这种拷贝方式叫做深拷贝!

分别向两个对象中插入数据也不会相互影响:

	s1.Push(2);s1.Push(3);s1.Push(4);s2.Push(5);s2.Push(6);s1.print();s2.print();

除了上述的代码情景,还有一种场景没有谈到,当函数返回值类型为类类型对象时,也会调用拷贝构造函数。

class Date
{
public:Date(int year, int minute, int day){cout << "调用构造Date():" << endl;}Date(const Date& d){cout << "调用拷贝构造Date(const Date& d):" << endl;}~Date(){cout <<"调用析构~Date():"<< endl;}
private:int _year;int _month;int _day;
};
Date Test(Date d)
{return d;
}
int main()
{Date d1(2022,12,28);Test(d1);return 0;
}

 小总结:

●涉及资源的申请,就要写拷贝构造,反之可以不写。

●拷贝构造调用场景如:

  ♦使用已存在对象创建新对象。   ♦函数参数类型为类类型对象。   ♦函数返回值类型为类类型对象。 ●拷贝构造函数的参数只有一个必须是类类型对象的引用。编译器默认生成的拷贝构造函数,内置类型按照字节方式拷贝的,自定义类型调用其拷贝构造函数。

赋值重载

赋值重载也是默认成员函数之一,相对于其它函数,函数名比较特殊,函数名字为:关键字operator后面接赋值重载的运算符符号

语法:返回值类型 operator操作符(参数列表)。 参数类型:const T&,传递引用可以提高传参效率。 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值。
#include using namespace std;class Date
{
public://构造函数Date(int year = 1900, int month = 1, int day = 1){cout << "构造函数Date()" << endl;_year = year;_month = month;_day = day;}//拷贝构造Date(const Date& d){cout << "拷贝构造Date(const Date& d)" << endl;_year = d._year;_month = d._month;_day = d._day;}//赋值重载Date& operator=(const Date& d){cout << "赋值重载" << endl;if (this != &d){_year = d._year;_month = d._month;_day = d._day;}return *this;}
private:int _year;int _month;int _day;
};
int main()
{Date d1(2001,10,3);Date d2(d1);Date d3;d3 = d2;return 0;
}

 观察上述代码效果,调用了显示定义的赋值重载,完成了对象间的赋值。那么既然赋值重载也是默认成员函数,不显示定义,编译器会自动生成。

屏蔽掉显示定义的赋值重载函数后,对象间依然完成了赋值:

 其实,当用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝注 意:内置类型直接赋值,而自定义类型调用对应类的赋值运算符重载完成赋值。

class Time
{
public:
Time()
{_hour = 1;_minute = 1;_second = 1;
}Time& operator=(const Time& t){cout << "_t赋值重载operator=" << endl;if (this != &t){_hour = t._hour;_minute = t._minute;_second = t._second;}return *this;}
private:int _hour;int _minute;int _second;
};
class Date
{
private:// 基本类型int _year = 2001;int _month = 10;int _day = 3;// 自定义类型Time _t;
};
int main()
{Date d1;Date d2;d1 = d2;return 0;
}

 

 对于上述日期类,没有涉及到资源的申请,赋值重载是否实现都可以; 一旦涉及到资源管理则必须要实现。 (代码演示以栈为例)

typedef int DataType;
class Stack
{
public:Stack(int capacity = 10){_arr = (DataType*)malloc(capacity * sizeof(DataType));if (nullptr == _arr){perror("malloc faile:");exit(-1);}_size = 0;_capacity = capacity;}void Push(const DataType& data){_arr[_size] = data;_size++;}~Stack(){if (_arr){free(_arr);_arr = nullptr;_capacity = 0;_size = 0;}}
private:DataType* _arr;int _size;int _capacity;
};
int main()
{Stack s1;s1.Push(1);s1.Push(2);Stack s2;s2 = s1;return 0;
}

 出现的错误和拷贝构造资源申请,未显示定义拷贝构造的错误类似。

当s1赋值给s2时,编译器会将s1中内容原封不动的拷贝到s2中,这样会导致两个问题:

♠s2原本的内存空间丢失,造成了内存泄露。

♠s1/s2共享同一块内存空间,最后销毁时导致同一快空间释放了两次。

显示定义: 

typedef int DataType;
class Stack
{
public:Stack(int capacity = 10){_arr = (DataType*)malloc(capacity * sizeof(DataType));if (nullptr == _arr){perror("malloc faile:");exit(-1);}_size = 0;_capacity = capacity;}Stack& operator=(const Stack& s){if (this != &s){_arr = (DataType*)malloc(10 * sizeof(DataType));for (int i = 0; i < s._size; i++){_arr[i] = s._arr[i];}_size = s._size;_capacity = s._capacity;}return *this;}void Push(const DataType& data){_arr[_size] = data;_size++;}~Stack(){if (_arr){free(_arr);_arr = nullptr;_capacity = 0;_size = 0;}}
private:DataType* _arr;int _size;int _capacity;
};
int main()
{Stack s1;s1.Push(1);s1.Push(2);Stack s2;s2 = s1;return 0;
}

● 赋值运算符只能重载成类的成员函数不能重载成全局函数 。

用户在类外实现全局的赋值运算符重载,会和编译器在类中生成的默认赋值运算符重载冲突,故赋值运算符重载只能是类的成员函数。

运算符重载

除了赋值运算符之外,像+、-、*、/等这样的运算符一直伴随着我们的学习生活,对于我们来说“见其知其意”,为了增强代码的可读性引入了运算符重载。

运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。 

以日期类为例,学习运算符重载:

 Date.h(预览日期类大概功能)

#pragma once#include using namespace std;class Date
{
public://全缺省构造Date(int year = 2020, int month = 12, int day = 28){_year = year;_month = month;_day = day;}//计算某年某月的天数int GetMonthDay(int year, int month){//平年各月的天数int LeapYear[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };//闰年2月29天if (month == 2 && ((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0)){return 29;}return LeapYear[month];}//赋值重载//析构函数//拷贝构造void Print(){cout << _year << "/" << _month << "/" << _day << endl;}//运算符重载bool operator==(const Date& d) ;bool operator> (const Date& d);bool operator>=(const Date& d);bool operator<(const Date& d);bool operator<=(const Date& d);bool operator!=(const Date& d);//日期+=天数Date& operator+=(int day);//日期+天数Date operator+(int day);//日期-天数Date& operator-=(int day);//日期-天数Date operator-(int day);//前后置++Date& operator++();Date operator++(int);//前后置--Date& operator--();Date operator--(int);//日期-日期 返回天数int operator-(Date date);
private:int _year;int _month;int _day;
};

♥默认成员函数

日期类的实现不涉及资源的申请,拷贝构造,赋值运算符重载,析构函数使用编译器自动生成的即可。

♥计算某年某月的天数

重点在于区分平年和闰年的2月,闰年的2月有29天,平年的2月有28天。其余的月份是相同的。

int GetMonthDay(int year, int month){//平年各月的天数int LeapYear[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };//闰年2月29天if (month == 2 && ((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0)){return 29;}return LeapYear[month];}

♥运算符重载

语法:返回值类型 operator操作符(参数列表)。

形参看起来少一个,其实还有隐藏的this指针。

bool  Date::operator==(const Date& d)
{return _year == d._year&& _month == d._month&& _day == d._day;
}
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;}return false;
}
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 || *this == d;
}
bool  Date::operator!=(const Date& d)
{return !(*this == d);
}

小技巧:实现了前两个运算符重载后,后面的重载可以复用前面已经实现过的。但是一定要确保已经实现的没有错误!

♥日期+、+=、-、-=天数

这里要注意的环节就是,日期+-过程中的进位、借位问题。生活常识我们都知道,一个日期的天数、月份、年份均不能是负数,是0也不合适,例如:2020年0月0日,-1年-4月-6日,每个月的天数不能超过该年月的天数,其年份的月数也不可能超过12!还有要注意的就是,-和-=,+和+=的区别,-=和+=是改变了自身的数据,-和+是在源数据的基础上通过+、-操作获得一个数据。

//日期+=天数
Date& Date::operator+=(int day)
{_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 ret(*this);ret += day;return ret;
}//日期-天数
Date& Date::operator-=(int day)
{//要减去的天数可能大于月天数_day -= day;while (_day <= 0){_month--;if (_month == 0){_year--;_month = 12;}//_day是负数_day += GetMonthDay(_year, _month);}return *this;
}
//日期-天数
Date  Date::operator-(int day)
{Date tmp(*this);tmp -= day;return tmp;
}

♥前后置++、--

为了让前置++(--)与后置++(--)形成能正确重载 。C++规定:后置++(--)重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器 自动传递!!!
Date& Date::operator++()
{*this += 1;return *this;
}
Date  Date::operator++(int)
{Date ret(*this);*this += 1;return ret;
}
// 前置
Date& Date::operator--()
{*this -= 1;return *this;
}// 后置
Date Date::operator--(int)
{Date tmp = *this;*this -= 1;return tmp;
}

♥日期-日期

两个日期的相减可以计算出这之间的天数,首先要处理的就是大小日期的问题。当小日期-大日期的时候就说明得出的天数是我们已经度过的过去天数,大日期-小日期就是未来在过多少天才能到达大日期。这里用了一个标记变量来标记是哪一种情况。计算日期的天数,只要小日期还不等于大日期,就计数,最后得到的天数和标记就是我们要得到的综合信息。

//日期-日期 返回天数
int  Date::operator-(Date date)
{Date max = *this;Date min = date;int flag = 1;int count = 0;if ((*this) < date){max = date;min = *this;int flag = -1;}while (max != min){++min;++count;}return flag * count;
}

 const成员

const修饰的成员函数称之为const成员函数,const实际修饰的是该成员函数隐含this指针,表明在该成员函数中不能对类的任何成员进行修改。

例如:

class Date
{
public:Date(int year, int month, int day) {_year = year;_month = month;_day = day;}void Print() const{cout << "Print()const" << endl;_year = 2021;_month = 10;_day = 3;}
private:int _year; int _month; int _day; 
};

const修饰this指针

void Print() const  等价于 void Print(const Date* this)

♣ const对象可以调用非const成员函数吗? 权限的放大,不可以! ♣非const对象可以调用const成员函数吗? 权限的缩小,可以! ♣ 非const成员函数内可以调用其它的const成员函数
bool  Date::operator==(const Date& d) const
{return _year == d._year&& _month == d._month&& _day == d._day;
}
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;}return false;
}
bool  Date::operator>=(const Date& d) 
{return *this > d || *this == d;
}

可以,权限的缩小。

♣const成员函数内可以调用其它的非const成员函数吗?

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;}return false;
}
bool  Date::operator>=(const Date& d) const
{return *this > d || *this == d;
}

不可以,权限的放大。

取地址及const取地址操作符重载  

在开头就介绍过,这两个默认成员函数使用编译器生成的默认取地址的重载即可,很少需要自己实现,知道有这两兄弟即可。
 Date* operator&(){return this ;}const Date* operator&()const{return this ;}

相关内容

热门资讯

监控摄像头接入GB28181平... 流程简介将监控摄像头的视频在网站和APP中直播,要解决的几个问题是:1&...
Windows10添加群晖磁盘... 在使用群晖NAS时,我们需要通过本地映射的方式把NAS映射成本地的一块磁盘使用。 通过...
protocol buffer... 目录 目录 什么是protocol buffer 1.protobuf 1.1安装  1.2使用...
在Word、WPS中插入AxM... 引言 我最近需要写一些文章,在排版时发现AxMath插入的公式竟然会导致行间距异常&#...
【PdgCntEditor】解... 一、问题背景 大部分的图书对应的PDF,目录中的页码并非PDF中直接索引的页码...
Fluent中创建监测点 1 概述某些仿真问题,需要创建监测点,用于获取空间定点的数据࿰...
educoder数据结构与算法...                                                   ...
MySQL下载和安装(Wind... 前言:刚换了一台电脑,里面所有东西都需要重新配置,习惯了所...
修复 爱普生 EPSON L4... L4151 L4153 L4156 L4158 L4163 L4165 L4166 L4168 L4...
MFC文件操作  MFC提供了一个文件操作的基类CFile,这个类提供了一个没有缓存的二进制格式的磁盘...