C++入门3——类与对象2(类的6个默认成员函数)

目录

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

2. 构造函数

2.1 构造函数的概念

2.2 构造函数的特性

3. 析构函数

3.1 析构函数的概念

3.2 析构函数的特性

4.拷贝构造函数

4.1 拷贝构造函数的概念

4.2 拷贝构造函数的特性

5.赋值运算符重载函数

5.1运算符重载函数

5.2 赋值运算符重载函数

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

前置++:

后置++:

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


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; // 日
};

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

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

(本篇完) 

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

docker一键部署EFK系统(elasticsearch filebeat kibana metricbeat es-head)

EFK日志系统搭建 EFK日志系统介绍功能需求搭建elasticsearch集群规划前提部署核对证书及权限 EFK日志系统介绍 Elasticsearch 是一个实时的、分布式的可扩展的搜索引擎&#xff0c;允许进行全文、结构化搜索&#xff0c;它通常用于索引和搜索大量日志数据&#xff0c;也可用于…

Vue3-路由详解

文章目录 路由对路由的理解安装 Vue Router基本切换效果两个注意点路由器工作模式to的两种写法命名路由嵌套路由路由传参query参数params参数 路由的props配置replace属性编程式导航重定向 更多相关内容可查看 路由 附git地址&#xff1a;https://gitee.com/its-a-little-bad/…

打印机的ip不同且连不上

打印机的ip不同且连不上 1.问题分析2.修改网段3.验证网络 1.问题分析 主要是打印机的网段和电脑不在同一个网段 2.修改网段 3.验证网络

CATO原理中的数学与魔术(十一)——Parity Principle及其应用二:集合的可视化...

早点关注我&#xff0c;精彩不错过&#xff01; 上篇文章中&#xff0c;我们已经进入了CATO原理魔术介绍的深水区&#xff0c;是第3个系列Parity Principle中集合性质的章节&#xff0c;聊到了关于张数和求和集合性质&#xff0c;并对性质之间的偏序关系&#xff0c;性质之间的…

基于单片机的微型嵌入式温度测量仪的设计与实现分析

摘要 &#xff1a; 作为信息技术中重要的技术手段之一嵌入式单片机系统已经被应用到越来越多不同的行业领域中。如&#xff0c;各种手持监测设备、智能家电设备等。当前展开对单片机的微型嵌入式温度测量仪的设计和实现研究&#xff0c;从微型嵌入式单片机相关理论入手&#xf…

桶形畸变和枕形畸变

桶形畸变和枕形畸变是两种常见的光学畸变现象&#xff0c;主要发生在使用广角镜头或远摄镜头拍摄时。这些畸变是因为镜头的光学特性不能完美地将光线汇聚到一个共同的焦点上&#xff0c;导致图像的不同部分在形状上发生扭曲。下面分别对这两种畸变进行详细描述&#xff1a; 桶…

Ceph集群存储案例

Ceph是一种可靠的、可扩展的、统一的、分布式的存储系统。Ceph高度可靠、易于管理且免费。Ceph提供了非凡的可扩展性——数以千计的客户端访问PB到EB的数据。Ceph存储集群相互通信以动态复制和重新分配数据。目前众多云厂商都在使用Ceph&#xff0c;应用广泛。如&#xff1a;华…

[线程与网络] 网络编程与通信原理(四):深入理解传输层UDP与TCP协议

&#x1f338;个人主页:https://blog.csdn.net/2301_80050796?spm1000.2115.3001.5343 &#x1f3f5;️热门专栏:&#x1f355; Collection与数据结构 (92平均质量分)https://blog.csdn.net/2301_80050796/category_12621348.html?spm1001.2014.3001.5482 &#x1f9c0;Java …

研发效能DevOps: Ubuntu 部署 JFrog 制品库

目录 一、实验 1.环境 2.Ubuntu 部署 JFrog 制品库 3.Ubuntu 部署 postgresql数据库 4.Ubuntu 部署 Xray 5. 使用JFrog 增删项目 二、问题 1.Ubuntu 如何通过apt方式部署 JFrog 制品库 2.Ubuntu 如何通过docker方式部署 JFrog 制品库 3.安装jdk报错 4.安装JFrog Ar…

用follow.it为您的网站添加邮箱订阅功能(附2024版教程)

多数情况下网站用户浏览一次就不会来了&#xff08;即使用户已收藏您的网站&#xff09;&#xff0c;因为用户很可能已把您的网站忘了。那么怎么样才能抓住网站回头客&#xff0c;让用户再次回到您的网站呢&#xff1f;除了提供更优质的原创内容外&#xff0c;比较好的方法是给…

第十九讲:动态内存分配

第十九讲&#xff1a;动态内存分配 1.为什么要有动态内存分配2.malloc和free2.1malloc2.1.1函数原型2.1.2函数使用 2.2free2.2.1函数原型2.2.2函数使用2.2.3函数使用注意事项2.2.3.1注意点12.2.3.2注意点22.2.3.3注意点32.2.3.4注意点4 2.3malloc和free使用注意事项2.3.1内存覆…

zynq PS点灯

摸鱼碎碎念&#xff1a; 需要做ADC采集并在TFT屏幕实时显示波形&#xff08;简易示波器&#xff09; 发现只使用fpga实现比较困难 使用的是zynq&#xff0c;刚好来把arm部分也学到 参考视频 与 教材文档 01_Zynq SoC FPGA的诞生_哔哩哔哩_bilibili (这是俺点开AXI4接口协…

【小白专用24.5.30已验证】Composer安装php框架thinkPHP6的安装教程

一、框架介绍 1、框架简介和版本选择 Thinkphp是一种基于php的开源web应用程序开发框架ThinkPHP框架&#xff0c;是免费开源的、轻量级的、简单快速且敏捷的php框架。你可以免费使用TP框架&#xff0c;甚至可以将你的项目商用&#xff1b; ThinkPHP8.0 是目前框架正式版的最新版…

windows上CMake、Mingw和VSCode配置调试C/C++代码

大型项目里经常使用CMake&#xff0c;我在Windows平台上开发&#xff0c;使用的代码编辑器是VSCode&#xff0c;我使用的是mingw的编译器&#xff0c;以前使用的是一个脚本来进行编译&#xff1a; run.bat cmake -G "MinGW Makefiles" .. mingw32-makeVSCode可以方…

Linux实验报告(二)——Linux系统中的常用命令

目录 一、实验名称&#xff1a; 二、仪器、设备&#xff1a; 三、参考资料&#xff1a; 四、实验目的&#xff1a; 五、实验内容&#xff08;步骤&#xff09;&#xff1a; 六、实验数据&#xff08;程序&#xff09;记录&#xff1a; ​编辑 ​编辑 七、实验结果分析…

蒙自源六一儿童节特别活动:美食盛宴,快乐无限

蒙自源始终坚持以用户为中心&#xff0c;致力于为消费者提供健康美味的米线。据悉&#xff0c;蒙自源的每家门店店长都会在每天早晨亲自熬制一锅鲜美的汤底&#xff0c;确保顾客能够享受到最新鲜、最美味的米线。为了庆祝六一&#xff0c;蒙自源特意为孩子们准备了一场别开生面…

云数融合与大数据技术在日常生活中的创新应用探索

前言 移动云模型服务产品在中国移动旗下主要包括云计算、大数据、人工智能等服务&#xff0c;它依托广泛的算力资源(4N31X)、丰富的网络接入资源和高品质云专网&#xff0c;实现算网端资源一站式开通&#xff0c;构建企业级一体化解决方案。 文章目录 前言云计算的日常应用智…

六一儿童节与AIGC:科技与童趣的奇妙融

随着人工智能生成内容&#xff08;AIGC&#xff09;技术的发展&#xff0c;越来越多的应用和网站专门为儿童提供学习、游戏和绘画方面的支持。这些平台不仅能够提高孩子们的学习兴趣&#xff0c;还能激发他们的创造力。在六一儿童节即将到来之际&#xff0c;让我们来介绍几款利…

力扣刷题--485. 最大连续 1 的个数【简单】

题目描述 给定一个二进制数组 nums &#xff0c; 计算其中最大连续 1 的个数。 示例 1&#xff1a; 输入&#xff1a;nums [1,1,0,1,1,1] 输出&#xff1a;3 解释&#xff1a;开头的两位和最后的三位都是连续 1 &#xff0c;所以最大连续 1 的个数是 3. 示例 2: 输入&…

颠覆传统:探索Web3对传统计算机模式的冲击

随着Web3技术的崛起&#xff0c;传统计算机模式正面临着前所未有的冲击与挑战。Web3作为下一代互联网的代表&#xff0c;以其去中心化、安全可信的特性&#xff0c;正在颠覆着传统计算机模式的种种假设和局限性。本文将深入探讨Web3对传统计算机模式的冲击&#xff0c;并探索其…