C++入门3——类与对象(2)

1.类的6个默认成员函数 

如果一个类中什么成员都没有,简称为空类。可是空类中真的什么都没有吗?

其实并不是的,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。 默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。

10cc6ae830cc460293c48d89a2fc6c29.png

类的6个默认成员函数编译器都会自己生成,如果编译器生成的默认成员函数能够满足我们的需求,我们就无需再自己实现;

相反,如果编译器生成的默认成员函数不能满足我们的需求,我们就必须要自己实现了。

本篇博客正是介绍类的这6个默认成员函数都有哪些特性,讲述什么情况下只需使用默认成员函数,什么情况下需要自己实现以及要怎样实现的问题!

2. 构造函数

2.1 构造函数的概念

如下代码,我们定义一个日期类并且调用成员函数:

#include <iostream>
using namespace std;class Data
{
public:void Init(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()
{Data d1;d1.Init(2023, 5, 23);d1.Print();Data d2;d2.Init(2022, 5, 23);d2.Print();return 0;
}

 44e1ffcedc9c467aadcdcaf42e7d37aa.png

按照我们之前学过的,按部就班地先调用初始化成员函数,再调用打印成员函数,运行结果也中规中矩地跑出来了。

可是有一天,我需要很多个Data变量,写代码又太急躁,在创建某个Data变量时忘记调用Init成员函数了,如下所示:

#include <iostream>
using namespace std;class Data
{
public:void Init(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()
{Data d1;d1.Init(2023, 5, 23);d1.Print();Data d2;d2.Print();return 0;
}

结果d2出现了随机值:

1e18b406df5f416aaf074c75e432021d.png

通过以上赘述:对于Data类,可以通过Init公有方法给对象设置日期,如果忘记一次初始化就会导致bug的产生,那就不得不每次创建对象时都调用该方法设置信息,可是这样是不是有些太麻烦了呢?有没有方法在对象创建时,就将对象设置进去呢?

答案当然是有的,这就引出了C++的1个默认成员函数——构造函数:

构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次。

2.2 构造函数的特性

构造函数是特殊的成员函数,需要注意的是,构造函数没有用我们经常熟悉的Init来命名,虽然名称叫构造,但是构造函数的主要任 务并不是开空间创建对象,而是初始化对象。

其特征如下:

1. 函数名与类名相同;

2. 无返回值;

3. 对象实例化时编译器自动调用对应的构造函数;

4. 构造函数可以重载;

验证如下:

#include <iostream>
using namespace std;class Data
{
public://退出历史舞台:/*void Init(int year, int month, int day){_year = year;_month = month;_day = day;}*///1. 函数名与类名相同;2. 无返回值;Data(int year, int month, int day){_year = year;_month = month;_day = day;}//4. 构造函数可以重载;Data(){_year = 8;_month = 8;_day = 8;}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}private:int _year;int _month;int _day;
};int main()
{//调用含参构造函数:Data d1(2023, 5, 23); //3. 对象实例化时编译器自动调用对应的构造函数;d1.Print();//调用无参构造函数:Data d2;   //注意这里不能用诸如:Data d2();不能加(),因为会与函数声明产生歧义;d2.Print();return 0;
}

0ec1ea8c46d54512a7b8f4b376ae0b7b.png

当然这里也完全可以用到缺省参数:

#include <iostream>
using namespace std;class Data
{
public:Data(int year = 8, int month = 8, int day = 8){_year = year;_month = month;_day = day;}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}private:int _year;int _month;int _day;
};int main()
{Data d1(2023, 5, 23); d1.Print();Data d2(2023, 5);d2.Print();Data d3;d3.Print();return 0;
}

27c42a08303444c2a7c7c03cd85b2fb5.png

5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。

代码验证如下:

#include <iostream>
using namespace std;class Data
{
public:如果显示定义,编译器将不再生成//Data(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()
{Data d1;d1.Print();return 0;
}

将Date类中构造函数屏蔽后,代码可以通过编译,因为编译器生成了一个无参的默认构造函数:

a623d63f50a747b791ab574543849818.png

将Date类中构造函数放开,代码编译失败,因为一旦显式定义任何构造函数,编译器将不再生成无参构造函数,放开后报错:error C2512: “Date”: 没有合适的默认构造函数可用:

830c8ed1f42a4d059858ba4bc84f8d85.png


这时你可能要问了:

在不显示定义构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数似乎并没有什么用处呀!?

上面d1对象调用了编译器生成的默认构造函数,但是d1的对象_year/_month/_day,结果显示依旧是随机值,上面的运行结果就是铁铮铮的事实呀!这不是恰恰证明了这里编译器生成的默认构造函数并没有什么卵用吗?

这就涉及到了构造函数的第6个特性:

6.C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:int/char...,自定义类型就是我们使用class/struct/union等自己定义的类型,编译器生成默认的构造函数会对自定义类型成员调用的它的默认成员函数,而内置类型则不做处理。

代码验证如下:

#include <iostream>
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
{public:
void Print()
{cout << _year << "-" << _month << "-" << _day << endl;
}
private:// 基本类型(内置类型)int _year;int _month;int _day;// 自定义类型Time _t;
};
int main()
{Date d;d.Print();return 0;
}

2c14a15d9be9457d8eddcce7c59bddbb.png

说到这里,我又有些不解,同样都是变量,为什么还要分自定义类型调用它的默认成员函数,内置类型却不做处理呢?这难道不是一件画蛇添足的事情吗?

这次不否定了,说的确实有道理,所以在C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。

代码验证如下:

#include <iostream>
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
{
public:void Print(){cout << _year << "-" << _month << "-" << _day << endl;}
private:// 基本类型(内置类型)// C++11支持,声明时给缺省值int _year = 2023;int _month = 5;int _day = 23;// 自定义类型Time _t;
};
int main()
{Date d;d.Print();return 0;
}

3120721a56154ceaa8705d2cb793f407.png


思考如下代码能否正常运行:

#include <iostream>
using namespace std;
class Date
{
public:Date(){_year = 2023;_month = 5;_day = 23;}Date(int year = 2023, int month = 5, int day = 23){_year = year;_month = month;_day = day;}
private:int _year;int _month;int _day;
};
// 以下测试函数能正常运行吗?
void Test()
{Date d1;
}

答案是否定的:

d656dc64cf62454db57c305c43dbf8e2.png

针对以上现象,可以引出构造函数的第7个特性:

7.无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。 注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。 

3. 析构函数

3.1 析构函数的概念

通过上面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没的呢?这就需要我们学习析构函数了:

析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。

3.2 析构函数的特性

析构函数也是特殊的成员函数,其特征如下:

1. 析构函数名是在类名前加上字符 ~;

2. 无参数无返回值类型;

3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:与构造函数不同的是,析构函数不能重载;

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

#include <iostream>
using namespace std;class Data
{
public:Data(int year = 2023, int month = 5, int day = 23){_year = year;_month = month;_day = day;}void Ptint(){cout << _year << "-" << _month << "-" << _day << endl;}//1. 析构函数名是在类名前加上字符 ~;2. 无参数无返回值类型;~Data(){cout << "~Data" << endl;}private:int _year;int _month;int _day;
};
int main()
{Data d;d.Ptint();//这里为调用~Data,4. 对象生命周期结束时,C++编译系统自动调用析构函数;return 0;
}

8a52bdecb899413c8864f56c085965be.png

当然,Data类并不需要析构函数,这里只是为了证明C++自动调用了析构函数。

我们将析构函数用到顺序表中,可能会对析构函数有更深刻的理解:

#include <iostream>
using namespace std;typedef int DataType;class SeqList
{
public:SeqList(){cout << "已经调用了SeqList()构造函数;" << endl;_a = (DataType*)malloc(sizeof(DataType) * 4);if (_a == nullptr){perror("malloc failed");//如果扩容失败,说明原因exit(-1);}_size = 0;//当size≥capacity时就动态开辟空间_capacity = 4;//初始化数组容量为4}~SeqList(){cout << "已经调用了~SeqList()析构函数;" << endl;free(_a);_a = nullptr;_size = _capacity = 0;}private:int* _a;int _size;int _capacity;
};int main()
{SeqList sl;return 0;
}

5bc4ac37324a40f48897bdce2e1c6215.png


对于第3条特性,系统自动生成默认的析构函数,会不会完成一些事情呢?

5. 答案与构造函数相似,编译器生成的默认析构函数,对自定义类型成员调用它的析构函数,而内置类型则不做处理。

代码验证如下:

#include <iostream>
using namespace std;class Time
{
public:~Time(){cout << "已经调用了~Time()析构函数" << endl;}
private:int _hour;int _minute;int _second;
};
class Date
{
private:// 基本类型(内置类型)int _year = 2023;int _month = 5;int _day = 23;// 自定义类型Time _t;
};
int main()
{Date d;return 0;
}

运行结果为:

c3aa8ce8ea1245e68bad3d05be46c610.png

对以上结果和第3、第5条特性的详细解释:

程序运行结束后输出:“已经调用了~Time()析构函数”,在main中根本没有直接创建Time类的对象,为什么最后会调用Time类的析构函数?

因为:main中创建了Date对象d,而d中包含4个成员变量,其中_year, _month, _day三个是内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可;

而_t是Time类对象,所以在d销毁时,要将其内部包含的Time类的_t对象销毁,所以要调用Time类的析构函数;

但是:main函数中不能直接调用Time类的析构函数,实际要释放的是Date类对象,所以编译器会调用Date类的析构函数,而Date没有显式提供,则编译器会给Date类生成一个默认的析构函数,目的是在其内部调用Time类的析构函数;

即:当Date对象销毁时,要保证其内部每个自定义对象都可以正确销毁,main函数中并没有直接调用Time类析构函数,而是显式调用编译器为Date类生成的默认析构函数;

注意:创建哪个类的对象则调用该类的构造函数,销毁哪个类的对象则调用该类的析构函数

6. 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如 Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如SeqList类。 

4.拷贝构造函数

4.1 拷贝构造函数的概念

电视剧中以及现实中,双胞胎的例子不在少数,我们甚至可以说简直他们就是一个模子里刻出来的!那么,在创建对象时,可否创建一个与已存在对象一某一样的新对象呢?答案的肯定的。

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

4.2 拷贝构造函数的特性

1. 拷贝构造函数是构造函数的一个重载形式。即:拷贝构造函数是一个特殊的构造函数;

2. 拷贝构造函数的参数只有一个且必须是类 类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用;

代码验证如下:

不考虑特性2,我们偏偏就要直接传值调用:

#include <iostream>
using namespace std;
class Date
{
public:Date(int year = 2023, int month = 7, int day = 7){_year = year;_month = month;_day = day;}Date( Date d)   {_year = d._year;_month = d._month;_day = d._day;}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}private:int _year;int _month;int _day;
};
int main()
{Date d1;d1.Print();Date d2(d1);d2.Print();return 0;
}

会发现程序报错:

b31d40394d984383bd2ed4c7383d1dc6.png

这是为什么呢?答案就像特征2中所说的,在此过程中引发了无穷递归调用:

当我们直接传值调用时,会发生先传值再调用拷贝构造函数的情况,即:

1a006970791e4df69d80e6f568086dfa.png

所以正确应该如特性2所说的那样:

#include <iostream>
using namespace std;
class Date
{
public:Date(int year = 2023, int month = 7, int day = 7){_year = year;_month = month;_day = day;}// Date( Date d)   // 错误写法:编译报错,会引发无穷递归Date( Date& d)   // 正确写法{_year = d._year;_month = d._month;_day = d._day;}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}private:int _year;int _month;int _day;
};
int main()
{Date d1;d1.Print();Date d2(d1);d2.Print();return 0;
}

c645327e9ba447eabf769d8e8016f1ca.png

那么在概念中又提到:(一般常用const修饰),这是为什么呢?

这是为了防止我们在定义拷贝构造函数时写反了:

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

那么运行结果不但不会正确,反而会偷鸡不成蚀把米:

e2ca2a2a701447fca1d4aece104a34ee.png

所以加上const,即使出现了这样的低级错误,编译器就会报错,我们也能及时发现:

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

869fcebb101943eab5ebba1c98d195c5.png

正确代码:

#include <iostream>
using namespace std;
class Date
{
public:Date(int year = 2023, int month = 7, int day = 7){_year = year;_month = month;_day = day;}// Date( Date d)   // 错误写法:编译报错,会引发无穷递归Date( const Date& d)   // 正确写法{_year = d._year;_month = d._month;_day = d._day;}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}private:int _year;int _month;int _day;
};
int main()
{Date d1;d1.Print();Date d2(d1);d2.Print();return 0;
}

3.与构造函数和析构函数相似,若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。

在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。

代码验证如下:

#include <iostream>
using namespace std;
class Time
{
public:Time(){_hour = 8;_minute = 8;_second = 8;}Time(const Time& t){_hour = t._hour;_minute = t._minute;_second = t._second;cout << "已经调用了!Time::Time(const Time&)" << endl;}
private:int _hour;int _minute;int _second;
};
class Date
{
public:void Print(){cout << _year << "-" << _month << "-" << _day << endl;}
private:// 基本类型(内置类型)int _year = 2023;int _month = 7;int _day = 7;// 自定义类型Time _t;
};
int main()
{Date d1;// 用已经存在的d1拷贝构造d2,此处会调用Date类的拷贝构造函数// 但Date类并没有显式定义拷贝构造函数,则编译器会给Date类生成一个默认的拷贝构造函数Date d2(d1);d2.Print();return 0;
}

66d29c0c52404b548d981f17b4d16f1d.png


与前面的构造函数和析构函数相似的问题:编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显式实现吗?

当然像上面的Data类这样的类是没必要的。那么像顺序表之类的类呢?验证如下:

#include <iostream>
using namespace std;typedef int DataType;class SeqList
{
public:SeqList(){cout << "已经调用了SeqList()构造函数;" << endl;_a = (DataType*)malloc(sizeof(DataType) * 4);if (_a == nullptr){perror("malloc failed");//如果扩容失败,说明原因exit(-1);}_size = 0;//当size≥capacity时就动态开辟空间_capacity = 4;//初始化数组容量为4}//打印void Print(){for (int i = 0; i < _size; i++){cout << _a[i] << endl;}}//尾插void PushBack(const DataType& x){_a[_size] = x;_size++;}~SeqList(){cout << "已经调用了~SeqList()析构函数;" << endl;free(_a);_a = nullptr;_size = _capacity = 0;}private:int* _a;int _size;int _capacity;
};int main()
{SeqList sl1;sl1.PushBack(1);sl1.PushBack(2);sl1.PushBack(3);sl1.PushBack(4);sl1.Print();SeqList sl2(sl1);sl2.Print();return 0;
}

运行结果如图:

7e3a76d24fa24a6eb89a9fd691544b0c.png

我们可以看到,程序崩溃了!这是为什么呢?

打开监视窗口看一下sl1和sl2的内存地址:

a665e2e2d22f42d2910c10bb75c0a469.png

发现二者的地址相同,所以我们就知道了: 

1.sl1对象调用构造函数创建,在构造函数中,申请了(_capacity)4个元素的空间,然后里面存储了4个元素:1 2 3 4;

2. sl2对象使用sl1对象拷贝构造,而SeqList类没有显示定义拷贝构造函数,则编译器会给SeqList类生成一份默认的拷贝构造函数,默认拷贝构造函数是按照值拷贝的,即将sl1中的内容原封不动地拷贝到sl2中。因此sl1与sl2指向了同一块内存空间;

3. 当程序退出时,sl2和sl1要销毁。sl2先销毁,sl2销毁时调用析构函数,已经将0x00b59580的空间释放了,但是sl1并不知道,到sl1销毁时,会将0x00b59580的空间再释放一次(正如3.2的第5条特性说的那样),一块内存空间多次释放,必然会导致bug的产生。


现在我已经知道原因了,那么正确的代码应该怎么写呢?这就需要用到深拷贝去解决(关于深拷贝后面会有详解):

//自定义拷贝构造函数,不用编译器默认生成的(深拷贝)SeqList( const SeqList& sl){_a = (DataType*)malloc(sizeof(DataType) * 4);//我也开辟一个空间if (_a == nullptr){perror("malloc failed");//如果扩容失败,说明原因exit(-1);}memcpy(_a, sl._a, sizeof(int) * sl._capacity);_size = sl._size;_capacity = sl._capacity;}

32d8e2931faa4aba9c7fd30cce45486b.png

所以,我们应该要明白:

4.类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请 时,则拷贝构造函数是一定要写的,否则就是浅拷贝。

5.赋值运算符重载函数

5.1运算符重载函数

在学习赋值运算符重载之前,我们先来了解一下运算符重载:

通过上面的学习,我们已经知道了内置类型和自定义类型的区别,思考这样一个问题:

显而易见,内置类型对象可以直接用各种运算符,内置类型是语言自己定义的,编译直接转换成指令

举个简单的例子,内置类型的int类型2和1,编译器可以轻松知道2>1;内置类型的double类型2.2和1.1,编译器轻松知道2.2>1.1,诸如此类......

那么问题来了,我们通篇写的Data类对象,我这时候需要判断2012年7月7日与2013年7月7日哪个日期更大,编译器能直接判断出2012年7月7日<2013年7月7日吗?显然是不能的!因为自定义类型编译器不支持直接转换成指令。

那么这时候就需要我们自己写一个函数来实现:

写一个大于比较函数:

bool Greater(Data d1, Data d2)
{if (d1._year > d2._year){return true;}else if (d1._year == d2._year && d1._month > d2._month){return true;}else if (d1._year == d2._year && d1._month ==d2._month && d1._day > d2._day){return true;}return false;
}

再比如我写一个等于比较的函数:

bool Equal(Data d1, Data d2)
{return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day;
}

运行一下:

#include <iostream>
using namespace std;
class Data
{
public:Data(int year = 2012, int month = 7, int day = 7){_year = year;_month = month;_day = day;}Data(const Data& d){_year = d._year;_month = d._month;_day = d._day;}void Ptint(){cout << _year << "-" << _month << "-" << _day << endl;}//private:int _year;int _month;int _day;
};//布尔类型(bool)用于表示真(true)和假(false)的值。
//它只有两个取值:true 和 false,分别对应 1 和 0 。
bool Greater(Data d1, Data d2)
{if (d1._year > d2._year){return true;}else if (d1._year == d2._year && d1._month > d2._month){return true;}else if (d1._year == d2._year && d1._month ==d2._month && d1._day > d2._day){return true;}return false;
}bool Equal(Data d1, Data d2)
{return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day;
}int main()
{Data d1(2013, 7, 7);Data d2(2012, 7, 7);cout << Greater(d1, d2) << endl;cout << Equal(d1, d2) << endl; return 0;
}

40c3044b923f4d26a6af88b477fb7d38.png

说到这里,我们来说一个题外话,关于函数的命名,其实在C语言中我们就遇到过很多了,一个函数命名就如同给自己的孩子取名字一样,比如上面的判断大于和判断相等函数,我能用Greater、Equal,为什么就不能用DaYu、DengYu,或者Compare1、Compare2,又或者func1、func2呢?

这些确实都是可以的呀!我创建的函数,我乐意怎样取名我就怎样取名!

可是话说回来,你的孩子在你口中叫狗蛋儿、在老师口中叫张三,有一天你去开家长会,老师问你是谁的家长,你说你是狗蛋儿的家长,你这样说老师会知道张三就是狗蛋儿,狗蛋儿就是张三吗?

话再说回来,你写的函数叫DaYu、DengYu,而你的同事要用这个函数,你写的DaYu、DengYu,同事能知道这是个什么函数吗?

所以,为了规避这种情况,增强代码的可读性,C++引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。

函数名字为:关键字operator后面接需要重载的运算符符号。

函数原型:返回值类型 operator操作符(参数列表)

因此上述两个函数就可以写为(更加规范,我们加上const和&):

//bool Greater(Data d1, Data d2)
bool operator>(const Data& d1, const Data& d2)
{if (d1._year > d2._year){return true;}else if (d1._year == d2._year && d1._month > d2._month){return true;}else if (d1._year == d2._year && d1._month ==d2._month && d1._day > d2._day){return true;}return false;
}//bool Equal(Data d1, Data d2)
bool operator == (const Data& d1, const Data& d2)
{return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day;
}
int main()
{Data d1(2013, 7, 7);Data d2(2012, 7, 7);/*cout << Greater(d1, d2) << endl;cout << Equal(d1, d2) << endl; */cout << operator > (d1, d2) << endl;cout << operator == (d1, d2) << endl;return 0;
}

09a4e81684514c94a4c1a3efa58519c0.png

以及为了令自定义类型更贴合与内置类型一样让编译器自己计算,直接转为指令:

int main()
{Data d1(2013, 7, 7);Data d2(2012, 7, 7);/*cout << Greater(d1, d2) << endl;cout << Equal(d1, d2) << endl; *//*cout << operator > (d1, d2) << endl;cout << operator == (d1, d2) << endl;*/bool ret1 = d1 > d2;   //d1>d2吗?是为1,否为0;bool ret2 = d1 == d2;  //d1=d2吗?是为1,否为0;int a = 3 > 2;         //3>2吗?是为1,否为0;int b = 3 == 2;        //3=2吗?是为1,否为0;cout << ret1 << endl << ret2 << endl;cout << a << endl << b << endl;return 0;
}

6c14fcf3156e47f4be3a01e2d23d84ae.png


仔细观察我们上面写的Data类,可以发现我把private注释掉了,那现在我把注释关掉:

c95e1a2b39d84902b7952dc26cc6ed78.png

成员变量变私有了该怎么办呢?其实C++常用的解决方法是直接将函数放到类里面,因为类里面可以随便访问private:

class Data
{
public:Data(int year = 2012, int month = 7, int day = 7){_year = year;_month = month;_day = day;}Data(const Data& d){_year = d._year;_month = d._month;_day = d._day;}bool operator == (const Data& d1, const Data& d2){return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day;}void Ptint(){cout << _year << "-" << _month << "-" << _day << endl;}private:int _year;int _month;int _day;
};

可是这样写发现还是编译不通过:

02d4b6013b5f4a4dad00c0501e5e4edb.png

它说函数参数太多?!我放到类外面参数就不多,怎么放到类里面就多参数了呢?

对!因为存在一个隐含的this指针(详细请看C++入门2——类与对象(1)中的3),所以这时就可以这样修改:

class Data
{
public:Data(int year = 2012, int month = 7, int day = 7){_year = year;_month = month;_day = day;}Data(const Data& d){_year = d._year;_month = d._month;_day = d._day;}bool operator>(const Data& d2){if (_year > d2._year){return true;}else if (_year == d2._year && _month > d2._month){return true;}else if (_year == d2._year && _month == d2._month && _day > d2._day){return true;}return false;}bool operator == ( const Data& d2){return _year == d2._year && _month == d2._month && _day == d2._day;}void Ptint(){cout << _year << "-" << _month << "-" << _day << endl;}private:int _year;int _month;int _day;
};

那么在函数调用时,编译器就会帮我们负重前行:

int main()
{Data d1(2013, 7, 7);Data d2(2012, 7, 7);bool ret1 = d1 > d2;    //d1.operator>(d2)--->d1.operator>(&d1,d2)bool ret2 = d1 == d2;   //d1.operator==(d2)--->d1.operator==(&d1,d2)cout << ret1 << endl << ret2 << endl;return 0;
}

所以运算符重载归纳有以下特点:

1. 不能通过连接其他符号来创建新的操作符:比如operator@;

2. 重载操作符必须有一个类类型参数;

3. 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义;

4. 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this;

5.  .*    ::     sizeof     ?:     .    以上5个运算符不能重载。

5.2 赋值运算符重载函数

铺了那么多前戏,终于来到我们要学习的赋值运算符重载函数了:

我们知道,拷贝构造就是将一个已经初始化的变量A拷贝到未初始化的变量B中,

那么如果存在两个都已经初始化的变量A、B,我想把A的值拷贝到B,显然就不能再用拷贝构造了,要用到我们就要开始讲的赋值运算符重载:

1. 赋值运算符重载格式:

参数类型:const T&,传递引用可以提高传参效率

返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值检测是否自己给自己赋值

返回*this :要复合连续赋值的含义

(详解精华都在代码里):

#include <iostream>
using namespace std;
class Data
{
public:Data(int year = 2012, int month = 7, int day = 7){_year = year;_month = month;_day = day;}Data(const Data& d){_year = d._year;_month = d._month;_day = d._day;}//d1=d2;d1传给this,d2传给d//返回值应该是什么类型呢?当然是Data类型;应该返回d1的地址,所以用&引用返回Data& operator=(const Data& d){//判断是否为自己给自己赋值,&放到这里不是引用,是取地址://判断d2的地址是否与d1地址相等if (this != &d){_year = d._year;_month = d._month;_day = d._day;}return *this;//返回d1的地址}void Ptint(){cout << _year << "-" << _month << "-" << _day << endl;}private:int _year;int _month;int _day;
};int main()
{Data d1(2023, 7, 7);Data d2(2022, 8, 8);//拷贝构造:一个已经存在的对象去拷贝初始化另一个对象Data d3(d2);d1.Ptint();d2.Ptint();d3.Ptint();cout << endl;//赋值运算符重载:两个已经存在的对象拷贝d1 = d2;//运算自定义类型,就要用到运算符重载operator=d1.Ptint();d2.Ptint();d3.Ptint();
}

bfc851358b7a4f188d2536f11d56f2a8.png


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

 原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现 一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值 运算符重载只能是类的成员函数。

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

4.如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必 须要实现。

这些特性与拷贝构造函数有极大的相似性,这里不再过多赘述。

 5.3 前置++和后置++重载

在C语言中,我们已经知道了:前置++,先加后用;后置++,先用后加 这样的基本常识

那么我们学了运算符重载,现在我要自定义类型Data前置++和后置++,要怎么定义和实现呢?

前置++:

前置++为先+1后使用;所以前置++的返回值应该是返回+1之后的结果;

故前置++重载函数的实现为:

// 前置++:返回+1之后的结果Date& operator++()//返回d1,所以返回值类型当然为Date类型{_day += 1;return *this;//this指向的对象函数结束后不会销毁,故以&引用方式返回提高效率}

后置++:

后置++为先使用后+1;所以后置++的返回值应该为+1之前的旧值,故需在实现时需要先将this保存一份,然后给this+1;

那么问题来了,前置++和后置++都是一元运算符,实现起来两个函数名字相同,都是operator++,怎么才能区分这两个函数呢?

为了让前置++与后置++形成能正确重载,C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传递

故后置++重载函数的实现为:

	Date operator++(int){Date temp(*this);// 先将this保存一份,然后给this + 1_day += 1;return temp;    // temp是临时对象,出了作用域就会被销毁,//因此只能以值的方式返回,不能返回引用}

验证:

#include <iostream>
using namespace std;
class Date
{
public:Date(int year = 2023, int month = 7, int day = 7){_year = year;_month = month;_day = day;}// 前置++:返回+1之后的结果Date& operator++()//返回d1,所以返回值类型当然为Date类型{_day += 1;return *this;//this指向的对象函数结束后不会销毁,故以&引用方式返回提高效率}Date operator++(int){Date temp(*this);// 先将this保存一份,然后给this + 1_day += 1;return temp;    // temp是临时对象,出了作用域就会被销毁,//因此只能以值的方式返回,不能返回引用}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}
private:int _year;int _month;int _day;
};
int main()
{Date d1;Date d2(2022, 7, 7);d1 = ++d2;//前置++,编译器默认调用不带(int)参数的operator++d1.Print();d1 = d2++;//后置++,编译器默认调用带(int)参数的operator++d1.Print();return 0;
}

042923162f184534aa7a272a212b0bb8.png

小知识:在C语言中前置++和后置++二者的效率高低可能并不明显,但是在C++中,一般来说前置++的效率要高于后置++


6.(&)取地址操作符重载函数和const取地址操作符重载函数

终于来到类的最后两个默认成员成员函数了,二者的形式为:

class Date
{
public://(&)取地址操作符重载Date* operator&(){return this;}//const(&)取地址操作符重载const Date* operator&()const{return this;}
private:int _year; // 年int _month; // 月int _day; // 日
};

这两个默认成员函数一般不用重新定义 ,编译器默认会生成。

这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需 要重载,比如想让别人获取到指定的内容!所以这里就不再过多介绍这两个默认成员函数。

(本篇完) 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/17362.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

libmodbus开发库介绍

目录 功能概要源码获取源码内容结构源码与移植 功能概要 libmodbus是一个免费的跨平台支持RTU和TCP的Modbus库&#xff0c;遵循LGPL V2.1协议。libmodbus支持Linux、Mac Os X、FreeBSD、QNX和Windows等操作系统。libmodbus可以向符合Modbus协议的设备发送和接收数据&#xff0…

《Python侦探手册:用正则表达式破译文本密码》

在这个信息爆炸的时代&#xff0c;每个人都需要一本侦探手册。阿佑今天将带你深入Python的正则表达式世界&#xff0c;教你如何像侦探一样&#xff0c;用代码破解文本中的每一个谜题。从基础的字符匹配到复杂的数据清洗&#xff0c;每一个技巧都足以让你在文本处理的领域中成为…

系统思考—战略沙盘推演咨询服务

今日与JSTO团队一起学习了《战略沙盘推演咨询服务》。通过沙盘体验&#xff0c;我深刻感受到组织与战略就像一张皮的正反两面。在转型过程中&#xff0c;即使战略非常明确&#xff0c;团队成员由于恐惧和顾虑&#xff0c;往往不愿意挑战新的业务&#xff0c;从而难以实现战略目…

VasDolly图形工具-Android多渠道打包福利

简介 基于腾讯VasDolly最新版本3.0.6的图形界面衍生版本&#xff0c;旨在更好的帮助开发者构建多渠道包 使用 下载并解压工具包&#xff0c;找到Startup脚本并双击启动图形界面&#xff08;注意&#xff1a;本地需安装java环境&#xff09; 渠道格式说明 txt文件&#xff…

Qt | QTabBar 类(选项卡栏)

01、上节回顾 Qt | QStackedLayout 类(分组布局或栈布局)、QStackedWidget02、简介 1、QTabBar类直接继承自 QWidget。该类提供了一个选项卡栏,该类仅提供了一个选项卡, 并没有为每个选项卡提供相应的页面,因此要使选项卡栏实际可用,需要自行为每个选项卡设置需要显示的页…

山东大学软件学院项目实训-创新实训-基于大模型的旅游平台(二十一)- 微服务(1)

微服务 1.认识微服务 SpringCloud底层是依赖于SpringBoot的&#xff0c;并且有版本的兼容关系&#xff0c;如下&#xff1a; 2. 服务拆分 需求 &#xff1a; 把订单信息和用户信息一起返回 从订单模块向用户模块发起远程调用 &#xff0c; 把查到的结果一起返回 步骤 &…

多态(难的起飞)

注意 virtual关键字&#xff1a; 1、可以修饰原函数&#xff0c;为了完成虚函数的重写&#xff0c;满足多态的条件之一 2、可以菱形继承中&#xff0c;去完成虚继承&#xff0c;解决数据冗余和二义性 两个地方使用了同一个关键字&#xff0c;但是它们互相一点关系都没有 虚函…

JAVASE总结一

1、 2、引用也可以是成员变量&#xff08;实例变量&#xff09;&#xff0c;也可以是局部变量&#xff1b;引用数据类型&#xff0c;引用&#xff0c; 我们是通过引用去访问JVM堆内存当中的java对象&#xff0c;引用保存了java对象的内存地址&#xff0c;指向了JVM堆内存当中…

AURIX TC3xx单片机介绍-启动过程介绍1

从各个域控制器硬件解决方案来看,MPU可能来自多个供应商,有瑞萨,有NXP等,但对于MCU来说,基本都采用英飞凌TC3xx。 今天我们就来看一下TC3xx的启动过程,主要包含如下内容: uC上电过程中,会经过一个上电时序,从复位状态“脱离”出来;Boot Firmware是复位后第一个执行的…

Python实现对Word文档内容出现“重复标题”进行自动去重(4)

前言 本文是该专栏的第4篇,后面会持续分享Python办公自动化干货知识,记得关注。 在本专栏上一篇文章《Python实现对Word文档内容出现“重复标题”进行自动去重(3)》中,笔者有详细介绍使用python对word文档内容的目标文本进行自动去重。只不过本文要介绍的“去重方法”与上…

计算机专业必考之计算机指令设计格式

计算机指令设计格式 例题&#xff1a; 1.设相对寻址的转移指令占3个字节&#xff0c;第一字节为操作码&#xff0c;第二&#xff0c;第三字节为相对偏移量&#xff0c; 数据在存储器以低地址为字地址的存放方式。 每当CPU从存储器取出一个字节时候&#xff0c;自动完成&…

正点原子[第二期]Linux之ARM(MX6U)裸机篇学习笔记-24.1,2 SPI驱动实验-SPI协议介绍

前言&#xff1a; 本文是根据哔哩哔哩网站上“正点原子[第二期]Linux之ARM&#xff08;MX6U&#xff09;裸机篇”视频的学习笔记&#xff0c;在这里会记录下正点原子 I.MX6ULL 开发板的配套视频教程所作的实验和学习笔记内容。本文大量引用了正点原子教学视频和链接中的内容。…

计算机组成原理易混淆知识点总结(持续更新)

目录 1.机器字长&#xff0c;存储字长与指令字长 2.指令周期,机器周期,时钟周期 3.CPI,IPS,MIPS 4.翻译程序和汇编程序 5.计算机体系结构和计算机组成的区别和联系 6.基准程序执行得越快说明机器的性能越好吗? 1.机器字长&#xff0c;存储字长与指令字长 不同的机器三者…

AI智能体|扣子Coze文生图功能接入微信公众号

大家好&#xff0c;我是无界生长。 AI智能体&#xff5c;扣子Coze文生图功能接入微信公众号本文分享了如何将Coze平台的文生图功能接入微信公众号的详细操作流程&#xff0c;包括创建图像流、创建并配置Bot、设置提示词和开场白、调试、发布等步骤。如果看完还没学会的话&…

网页图片加载慢的求解指南

网页/图片加载慢的求解指南 一、前言与问题描述 今天刚换上华为的HUAWEI AX3 Pro New&#xff0c;连上WIFI后测速虽然比平时慢&#xff0c;但是也不算太离谱&#xff0c;如下图所示&#xff1a; 估计读者们有也和作者一样&#xff0c;还没意识到事情的严重性&#x1f601;。 …

08Django项目--用户管理系统--查(前后端)

对应视频链接点击直达 TOC 一些朋友加我Q反馈&#xff0c;希望有每个阶段的完整项目代码&#xff0c;那从今天开始&#xff0c;我会上传完整的项目代码。 用户管理&#xff0c;简而言之就是用户的增删改查。 08项目点击下载&#xff0c;可直接运行&#xff08;含数据库&…

PHP框架 Laravel

现在因为公司需求&#xff0c;需要新开一个Laravel框架的项目&#xff0c;毫无疑问&#xff0c;我又被借调过去了&#xff0c;最近老是被借调&#xff0c;有点阴郁&#xff0c;不过反观来看&#xff0c;这也是好事&#xff0c;又可以复习和巩固一下自己的知识点&#xff0c;接下…

大数据开发面试题【Spark篇】

115、Spark的任务执行流程 driver和executor&#xff0c;结构式一主多从模式&#xff0c; driver&#xff1a;spark的驱动节点&#xff0c;用于执行spark任务中的main方法&#xff0c;负责实际代码的执行工作&#xff1b;主要负责&#xff1a;将代码逻辑转换为任务、在executo…

编译qt5.15.2(mac/windows)的mysql驱动(附带编译好的文件)

文章目录 0 背景1 编译过程2 福利 0 背景 因为需要连接到mysql数据库&#xff0c;所以需要连mysql驱动。 1 编译过程 1&#xff0c;打开文件/Users/mac/Qt5.14.2/5.14.2/Src/qtbase/src/plugins/sqldrivers/sqldrivers.pro&#xff0c;注释掉QMAKE_USE mysql&#xff1b; 如…

【Linux-LCD 驱动】

Linux-LCD 驱动 ■ Framebuffer 简称 fb■ LCD 驱动程序编写■ 1、LCD 屏幕 IO 配置■ 2、LCD 屏幕参数节点信息修改■ 3、LCD 屏幕背光节点信息■ 4、使能 Linux logo 显示 ■ 设置 LCD 作为终端控制台■ 1、设置 uboot 中的 bootargs■ 2、修改/etc/inittab 文件 ■ LCD 背光…