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

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

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

(本篇完) 

 

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

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

相关文章

《Java数据结构》--单链表详解

一.单链表的概念 1.概念 单链表是一种物理存储结构是非连续&#xff0c;非线性的但是在逻辑结构上是连续且线性的&#xff0c;链表是通过一个个结点来实现的&#xff0c;使每个相邻结点之间存在一定关系来将所有结点串起来&#xff0c;在物理存储上像是一条链子。 2.链表的原…

《Ai企业知识库》-rasa X安装使用

背景&#xff1a; Rasa X 是一个为构建、改进和管理对话式AI助手而设计的工具。它是Rasa开源机器学习框架的一个扩展&#xff0c;旨在实现“对话驱动开发”&#xff08;Conversation-Driven Development&#xff09;。Rasa X 主要特点包括&#xff1a; 交互式学习&#xff1a;…

二叉树的实现(递归实现)

前言&#xff1a;本文讲解通过递归的方式实现二叉树的一些基本接口。 目录 通过左右子树的方式实现二叉树&#xff1a; 二叉树的遍历&#xff1a; 求二叉树结点的个数&#xff1a; 二叉树所有节点的个数&#xff1a; 二叉树叶子节点的个数&#xff1a; 求第k层节点的节点…

传输层安全性 (TLS)

传输层安全 (TLS) 旨在提供传输层的安全性。TLS 源自称为安全套接字层 (SSL)的安全协议。 TLS 确保任何第三方都无法窃听或篡改任何消息。 TLS 有几个好处&#xff1a; ● 加密&#xff1a; TLS/SSL 可以帮助使用加密来保护传输的数据。 ● 互操作性&#xff1a; TLS/S…

PHP框架 Laravel

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

第八大奇迹

目录 题目描述 输入描述 输出描述 输入输出样例 示例 输入 输出 运行限制 原题链接 代码思路 题目描述 在一条 R 河流域&#xff0c;繁衍着一个古老的名族 Z。他们世代沿河而居&#xff0c;也在河边发展出了璀璨的文明。 Z 族在 R 河沿岸修建了很多建筑&#xff0c…

[Algorithm][动态规划][简单多状态DP问题][买卖股票的最佳时机 III][买卖股票的最佳时机 Ⅳ]详细讲解

目录 1.买卖股票的最佳时机 III1.题目链接2.算法原理详解3.代码实现 2.买卖股票的最佳时机 IV1.题目链接2.算法原理详解3.代码实现 1.买卖股票的最佳时机 III 1.题目链接 买卖股票的最佳时机 III 2.算法原理详解 注意&#xff1a;本题为了便于初始化&#xff0c;有较多细节服…

Java之Writer类:探索Java中的输出流

哈喽&#xff0c;各位小伙伴们&#xff0c;你们好呀&#xff0c;我是喵手。运营社区&#xff1a;C站/掘金/腾讯云&#xff1b;欢迎大家常来逛逛 今天我要给大家分享一些自己日常学习到的一些知识点&#xff0c;并以文字的形式跟大家一起交流&#xff0c;互相学习&#xff0c;一…

在CentOS 8上卸载与安装MySQL 8的详细步骤

关键词&#xff1a;MySQL 8安装、CentOS 8、YUM源配置、卸载MySQL、MySQL残留文件删除、首次登录MySQL临时密码、服务状态检查、MySQL社区服务器 阅读建议&#xff1a;本文适合需要在CentOS 8操作系统上部署最新MySQL 8数据库的系统管理员或开发者阅读。文中步骤简洁清晰&#…

Spring+SpringBoot面试总结(近两万字)

SpringSpringBoot面试总结 一、Spring Bean1.1、bean的生命周期&#xff08;对象的创建使用销毁&#xff09;1.1.1、准备工作1.1.2、创建Bean对象1.1.3、注册销毁 1.2、 bean的作用域1.2.1、配置方式 1.3、 spring 自动装配 bean 有哪些方式&#xff08;存疑存疑&#xff09;1.…

452. 用最少数量的箭引爆气球(中等)

452. 用最少数量的箭引爆气球 1. 题目描述2.详细题解3.代码实现3.1 Python3.2 Java 1. 题目描述 题目中转&#xff1a;452. 用最少数量的箭引爆气球 2.详细题解 引爆所有气球&#xff0c;弓箭数要最少&#xff0c;那么每支弓箭尽量多的引爆气球&#xff0c;采用贪心策略。对于…

基于小波熵阈值的心电信号R波检测算法(MATLAB)

心脏兴奋电活动过程可由心电信号(ECG)来反映&#xff0c;心电信号也是医学上对心血管疾病诊断的重要科学依据。心电信号具有一定的随机性且一般情况下十分微弱&#xff0c;在信号采集、放大及变换过程中&#xff0c;心电信号容易受到人体呼吸及检测仪器等因素影响&#xff0c;从…

在ARM开发板上,栈大小设置为2MB(常用设置)里面存放的数据

系列文章目录 在ARM开发板上&#xff0c;栈大小设置为2MB&#xff08;常用设置&#xff09;里面存放的数据 在ARM开发板上&#xff0c;栈大小设置为2MB&#xff08;常用设置&#xff09;里面存放的数据 系列文章目录 在ARM开发板上&#xff0c;栈&#xff08;Stack&#xff09;…

linux下cp和mv命令显示进度条

1.查看当前系统下coreutils工具包的版本号&#xff1a; [rootk8s-master ~]# rpm -qa | grep -w coreutils coreutils-8.22-24.el7_9.2.x86_64当前版本为8.22。 因为cp 和 mv 命令由 coreutils 软件包提供&#xff0c;所以需要重新下载 coreutils 软件包配置补丁 2.下载core…

148.栈与队列:前K个高频元素(力扣)

代码解决 class Solution { public:// 自定义比较类&#xff0c;用于优先队列&#xff08;小顶堆&#xff09;class mycomparison{public:// 重载操作符&#xff0c;用于比较两个pair&#xff0c;基于pair的第二个值&#xff08;频率&#xff09;bool operator()(const pair&l…

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

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

UE_地编教程_创建地形洞材质

个人学习笔记&#xff0c;不喜勿喷。侵权立删&#xff01; 使用地形洞材质来遮罩地形上特定位置的可视性和碰撞。如要在山脉侧面创建进入洞穴的入口&#xff0c;此操作将非常有用。可使用地形材质和地形洞材质的相同材质&#xff0c;但注意&#xff1a;对比不使用不透明蒙版的…

基于Cloudflare/CloudDNS/GitHub使用免费域名部署NewBing的AI服务

部署前准备&#xff1a; Cloudflare 账号 https://dash.cloudflare.com/login CloudDNS 账号 https://www.cloudns.net/ GitHub 账号 https://github.com/Harry-zklcdc/go-proxy-bingai Cloudflare 部署 Worker CloudDNS 获取免费二级域名 GitHub New Bing Ai 项目 https://git…

Linux系统硬盘分区

文章目录 一、硬盘和分区1.1 硬盘的概念1.2 硬盘分区的类别1.3 硬盘分区的方式1.3.1 MBR分区1.3.2 GPT分区 1.4 硬盘分区的意义1.4.1 分区的作用1.4.2 分区的缺点 二、如何建立分区2.1 分区命令2.1.1 fdisk命令2.1.2 gdisk命令 2.2 建立分区2.2.1 建立MBR分区建立主分区建立扩展…

电脑如何在网页上下载视频 浏览器如何下载网页视频

对于现代职场人士而言&#xff0c;在日常生活中难免需要下载各种短视频&#xff0c;IDM下载加速器可以轻松获取抖音、快手等平台的无水印短视频文件。 Internet Download Manager&#xff0c;简称IDM。功能强大的网络下载器。您不需要多余的操作&#xff0c;IDM 能捕获您的下载…