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

news2024/10/6 18:35:09

目录

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

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

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

(本篇完) 

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

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

相关文章

探索文档解析技术,推动大模型训练与应用

探索文档解析技术&#xff0c;推动大模型训练与应用 0. 前言1. CCIG 20241.1 会议简介1.2 大模型技术及其前沿应用论坛1.3 走进合合信息 2. 大模型时代2.1 大模型的发展与应用2.2 大模型面临的挑战 3. 文档解析技术3.1 文档解析技术难点3.2 TextIn 文档解析算法流程 4. 大模型时…

UE5 Http Server

前言 最近要用UE 作为一个服务器去接收来自外部的请求&#xff0c;从而在UE中处理一些内容&#xff0c;但是之前只做过请求&#xff0c;哪整过这玩意&#xff0c;短期内还得出结果&#xff0c;那怎么搞嘞&#xff0c;本着省事的原则就找找呗&#xff0c;有没有现成的&#xff0…

【基础算法总结】位运算

位运算 1.基础位运算2.常见用法总结3.面试题 01.01. 判定字符是否唯一4.丢失的数字5.两整数之和6.只出现一次的数字 II7.面试题 17.19. 消失的两个数字 点赞&#x1f44d;&#x1f44d;收藏&#x1f31f;&#x1f31f;关注&#x1f496;&#x1f496; 你的支持是对我最大的鼓励…

CCIG 2024:合合信息文档解析技术突破与应用前景

目录 背景当前大模型训练和应用面临的问题训练Token耗尽训练语料质量要求高LLM文档问答应用中文档解析不精准 合合信息的文档解析技术1. 具备多文档元素识别能力2. 具备版面分析能力3. 高性能的文档解析4. 高精准、高效率的文档解析文档多板式部分示例 文档解析典型技术难点元素…

Go Modules 使用

文章参考https://blog.csdn.net/wohu1104/article/details/110505489 不使用Go Modules&#xff0c;所有的依赖包都是存放在 GOPATH /pkg下&#xff0c;没有版本控制。如果 package 没有做到完全的向前兼容&#xff0c;会导致多个项目无法运行(包版本需求不同)。 于是推出了g…

JVM学习-类加载过程(一)

概述 在Java中数据类型分为基本数据类型和引用数据类型&#xff0c;基本数据类型由虚拟机预先定义&#xff0c;引用数据类型则需要进行类的加载按Java虚拟机规范&#xff0c;从class文件加载到内存中的类&#xff0c;到类卸载出内存为止&#xff0c;它的整个生命周期包含以下7…

Java 异常处理中try-catch块、finally子句以及自定义异常的使用

Java 异常处理是 Java 语言中非常重要的一部分&#xff0c;用来处理程序运行过程中可能发生的各种异常情况&#xff0c;确保程序的稳定性和可靠性。 一、Java 异常处理概述 异常是程序运行过程中出现的非正常情况。Java 使用异常类&#xff08;Exception 类及其子类&#xff…

建WordPress主题官网模板

蓝色的中文WordPress企业模板 https://www.zhanyes.com/qiye/6305.html 暗红色WordPress律师事务所网站模板 https://www.zhanyes.com/qiye/23.html 红色大banner图WordPress外贸网站模板 https://www.zhanyes.com/waimao/27.html

【C语言】探索文件读写函数的全貌

&#x1f308;个人主页&#xff1a;是店小二呀 &#x1f308;C语言笔记专栏&#xff1a;C语言笔记 &#x1f308;C笔记专栏&#xff1a; C笔记 &#x1f308;喜欢的诗句:无人扶我青云志 我自踏雪至山巅 &#x1f525;引言 本章将介绍文件读取函数的相关知识和展示使用场景&am…

AI自动化办公:批量将Excel表格英文内容翻译为中文

有一个50列的表格&#xff0c;里面都是英文&#xff0c;要翻译成中文&#xff1a; 在ChatGPT中输入提示词&#xff1a; 你是一个开发AI大模型应用的Python编程专家&#xff0c;要完成以下任务的Python脚本&#xff1a; 打开Excel文件&#xff1a;"F:\AI自媒体内容\AI行业…

harbor -- docker私有仓库安装配置

1 安装docker-compose $ curl -L "https://get.daocloud.io/docker/compose/releases/download/v1.25.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose $ chmod x /usr/local/bin/docker-compose 2 安装配置harbor $ wget https://g…

JS-51-Node.js10-yarn

一、yarn的简介 Yarn 是一款 JavaScript 的包管理工具&#xff08;npm的代替方案&#xff09;&#xff0c;是 Facebook, Google, Exponent 和 Tilde 开发的一款新的 JavaScript 包管理工具。 正如 Yarn 官网的介绍&#xff0c;Yarn 的具有速度快 、安全 、可靠 的优点&#x…

嘉立创使用gif

新建原理图 边框设置2 新建pcb图 放置焊盘 排列焊盘 新建符号 封号向导 新建封装 封装向导 符号与封装联结 原件查找 drc设计规则&#xff08;线之间的距离等 布线冲突 顶底层切换 T ,B 顶底连线&#xff0c;自动创造过孔 铺铜 泪滴 网格大小 吸附 元件库

民国漫画杂志《时代漫画》第33期.PDF

时代漫画33.PDF: https://url03.ctfile.com/f/1779803-1248635648-d8235b?p9586 (访问密码: 9586) 《时代漫画》的杂志在1934年诞生了&#xff0c;截止1937年6月战争来临被迫停刊共发行了39期。 ps: 资源来源网络!

任何图≌自己这一几何最起码常识推翻直线公理让R外标准实数一下子浮出水面

黄小宁 h定理&#xff1a;点集AB≌B的必要条件是A≌B。 证&#xff1a;若AB则A必可恒等变换地变为BA≌A&#xff0c;而恒等变换是保距变换。证毕。 如图所示R轴即x轴各元点x沿x轴正向不保距平移变为点y2x就使x轴沿本身拉伸&#xff08;放大&#xff09;变换为y2x轴不≌x轴&…

PDF盖骑缝章

在PDF文件上加盖骑缝章&#xff0c;您可以采取以下几种方法之一&#xff1a; 使用Adobe Acrobat&#xff1a; 打开Adobe Acrobat软件&#xff0c;加载PDF文件。在工具栏选择“工具”选项&#xff0c;找到“骑缝章”或“印章”工具。选择或上传您的骑缝章图片&#xff0c;将其放…

区块链技术:金融行业的信任基石

一、引言 在数字化转型的浪潮中,区块链技术以其独特的优势,正逐渐成为金融行业的信任基石。本文将深入探讨区块链技术在金融行业的应用,通过具体案例的细化和完善,分析其特点和影响。 二、区块链技术的核心特点 1. 去中心化:区块链技术通过分布式账本,实现去中心化的数…

乡村振兴,教育先行:加强农村教育事业发展,提高农民素质,培育新时代新型农民,为美丽乡村建设提供人才保障

目录 一、引言 二、农村教育事业发展的重要性 1、提高农民素质&#xff0c;促进全面发展 2、推动农村经济社会发展 3、传承乡村文化&#xff0c;促进文化振兴 三、加强农村教育事业发展的策略 1、加大教育投入&#xff0c;改善教育设施 2、普及义务教育&#xff0c;提高…

栈和队列题目练习

本节小编选了两道题来加深对栈和队列的认识理解&#xff01; 有效的括号 方法1&#xff1a;直接用栈的结构&#xff08;动态数组&#xff09; 本题可以用栈这个结构来解答&#xff0c;将(,{,[ 左括号压入栈中&#xff0c;然后取出栈顶元素与右括号),},]匹配。不匹配的话&…

雅特力串口485模式TCDT/TSDT说明

TCDT&#xff08;发送器完成延迟时间&#xff08;transmit complete delay time&#xff09;占了5个bit位&#xff0c;则最大值为 2 5 − 1 31 2^{5} -1 31 25−131&#xff0c;时间单位是 1 16 波特率周期 \frac{1}{16}波特率周期 161​波特率周期 波特率为9600&#xff…