C++类与对象深入之运算符重载与const及初始化列表详解

运算符是程序中最最常见的操作,例如对于内置类型的赋值我们直接使用=赋值即可,因为这些编译器已经帮我们做好了,但是对象的赋值呢?能直接赋值吗

一:运算符重载

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

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

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

相等运算符重载

对内置类型我们想要判断两个变量是否相等我们可以直接使用相等运算符,但是如果是一个自定义类型呢?那么这时候就需要重载运算符了。

下面重载一个全局的 operator ==:

class Date
{
public:
	// 默认生成的析构函数,内置类型成员不做处理,自定义类型成员会去调用它的析构函数
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print(){
		cout << _year << "-" << _month << "-" << _day << endl;
	}
	int GetYear(){
		return _year;
	}
	int GetMonth(){
		return _month;
	}
	int GetDay(){
		return _day;
	}
private:
	int _year;
	int _month;
	int _day;
};
bool operator==( Date& d1,  Date& d2)
{
	return d1.GetYear() == d2.GetYear()
		&& d1.GetMonth() == d2.GetMonth()
		&& d1.GetDay() == d2.GetDay();
}
int main(){
	Date d1(2022, 5, 16);
	Date d2(2022, 5, 16);
	if (operator==(d1, d2)){
		cout << "==" << endl;
	}
	if (d1 == d2){ // 编译器会处理成对应重载运算符调用 if (operator==(d1, d2)){
		cout << "==" << endl;
	}
	system("pause");
	return 0;
}

我们把运算符重载成全局的时候,面对私有成员不可类外访问,我们提供三个函数接口,当然还有别的处理方式。我们可以上述“运算符重载可以判断两个自定义日期类是否相等

我们看到主函数中两处调用重载运算符,两种写法都可以,第二种方法更简单,编译器会自动处理成第一种方式。

我们还可以重载成类的成员函数,作为类成员重载函数时,其形参看起来比操作数目少1个

class Date
{
public:
	// 默认生成的析构函数,内置类型成员不做处理,自定义类型成员会去调用它的析构函数
	Date(int year = 1, int month = 1, int day = 1){
		_year = year;
		_month = month;
		_day = day;
	}
	void Print(){
		cout << _year << "-" << _month << "-" << _day << endl;
	}
	bool operator==(const Date& d)
	{
		return _year == d._year
			&& _month == d._month
			&& _day == d._day;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2022, 5, 16);
	Date d2(2022, 5, 16);
	if (d1.operator==(d2)){
		cout << "==" << endl;
	}
	if (d1 == d2){ // 编译器会处理成对应重载运算符调用 if (d1.operator==(d2))
		cout << "==" << endl;
	}
	system("pause");
	return 0;
}

”“解释:重载成成员函数的时候,成员函数里隐藏了this指针,形参列表其实是(Date * this, const Date & d2),函数调用时左操作数是this指针指向的对象!

同样地,我们看到主函数中两处调用重载运算符,两种写法都可以,第二种方法更简单,编译器会自动处理成第一种方式。

赋值运算符重载

C++编译器至少给一个类添加4个函数:

  • 默认构造函数(无参,函数体为空)
  • 默认析构函数(无参,函数体为空)
  • 默认拷贝构造函数,对属性进行值拷贝
  • 赋值运算符 operator=, 对属性进行值拷贝

赋值运算符主要有4点:

  • 参数类型
  • 返回值
  • 检查是否自己给自己赋值
  • 返回 *this

代码示例:

class Date
{
public:
	// 默认生成的析构函数,内置类型成员不做处理,自定义类型成员会去调用它的析构函数
	Date(int year = 1, int month = 1, int day = 1){
		_year = year;
		_month = month;
		_day = day;
	}
	void Print(){
		cout << _year << "-" << _month << "-" << _day << endl;
	}
	// d2 = d1; -> d2.operator=(&d2, d1)
	// d1 = d1
	Date& operator=(const Date& d)
	{
		if (this != &d)
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
		return *this;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2022, 5, 16);
	Date d2;
	Date d3(d1); // 拷贝构造  -- 一个存在的对象去初始化另一个要创建的对象
	d2 = d1;     // 赋值重载/复制拷贝 -- 两个已经存在对象之间赋值
	d1 = d1;
	system("pause");
	return 0;
}

”“解释:类对象d1给d2赋值,特别注意赋值重载函数的返回值,和检查是否自己给自己赋值!

我们要区分拷贝构造和赋值重载:拷贝构造是一个存在的对象去初始化另一个要创建的对象,而赋值重载是两个已经存在对象之间赋值。

如下列监视列表我们可以看出,结果d1.d2,d3都是一样的。

正如一开始所说的,如果一个类中没有显示定义赋值运算符重载,编译器也会生成一个,完成对象的浅拷贝。既然是浅拷贝就有局限,如果类中有属性指向堆区,做赋值操作时也会出现深浅拷贝问题。

小于运算符重载

下面我们比较日期类的大小:

代码示例:

class Date
{
public:
	// 默认生成的析构函数,内置类型成员不做处理,自定义类型成员会去调用它的析构函数
	Date(int year = 1, int month = 1, int day = 1){
		_year = year;
		_month = month;
		_day = day;
	}
	void Print(){
		cout << _year << "-" << _month << "-" << _day << endl;
	}
	bool operator<(const Date& d){
	if ((_year < d._year)
	|| (_year == d._year && _month < d._month)
	|| (_year == d._year && _month == d._month && d._day < d._day))
	{
	return true;
	}
	else{
	return false;
	}
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2022, 4, 16);
	Date d2(2022, 5, 16);
	if (d1 < d2){ // 编译器会处理成对应重载运算符调用 if (d1.operator<(d2))
		cout << "<" << endl;
	}
	system("pause");
	return 0;
}
<
请按任意键继续. . .

二:const成员

const修饰类的成员函数

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

//this指针的本质是一个指针常量,指针的指向不可修改

//如果想让指针指向的值也不可以修改,需要声明常函数

我们见下面一段代码:

void Func(const Date& d)
{
	d.Print(); // d.Print(&d); -> const Date*   
	//传的是一个指针指向的内容不可以改变的指针,
	//而单纯的this指针是指向不可以改变,所以权限放大了  必须在Print函数后加const,把this指针权限进一步放小
	cout << &d << endl;
}
void TestDate3()
{
	Date d1(2022, 5, 18);
	d1.Print(); // d1.Print(&d1); -> Date* 
	Func(d1);
	cout << &d1 << endl;
}

代码解释:如果Print不是常函数,那么在TestDate3()函数中调用Print函数不会报错,但是如果在Func函数中调用就会报错,这是因为在TestDate3()函数中调用Print函数传过去d1的地址,用this指针接收,权限缩小。而在Func函数中,d指针指向的内容不可以改变,而在形参this指向的内容可以改变,所以权限放大。所以必须给Print函数加上const,以表示常函数。

⭐️⭐️⭐️⭐️⭐️⭐️建议:

建议成员函数中不修改成员变量的成员函数,都可以加上const, 普通对象和const对象都可以调用。

三:cin、cout重载

我们都知道了cin、cout对于内置类型可以自动识别其类型进行输入输出,这是因为在库函数中提供了对应的重载!

下面我们直接看代码:

	class Date
{
public:
	// 友元函数
	friend std::ostream& operator<<(std::ostream& out, const Date& d);
	friend std::istream& operator>>(std::istream& out, Date& d);
	//........
}

首先我们定义成全局函数,必须在对应的类中声明友元,这样全局函数才可以访问类中成员!

std::ostream& operator<<(std::ostream& out, const Date& d)
{
	out << d._year << "-" << d._month << "-" << d._day << endl;
	return out;
}
std::istream& operator>>(std::istream& in, Date& d)
{
	in >> d._year >> d._month >> d._day;
	return in;
}

四:初始化列表

C++提供了初始化列表语法,用来初始化属性

构造函数赋初值

在创建对象的时候,编译器通过调用构造函数,给对象中各个成员变量一个合适的初值。

class Person {
public:
	Person(int a, int b, int c) {
		m_A = a;
		m_B = b;
		m_C = c;
	}
private:
	int m_A;
	int m_B;
	int m_C;
};
int main() {
	Person p(1, 2, 3);
	system("pause");
	return 0;
}

上述代码我们通过构造函数赋值的方法来“初始化”。

注意:上述代码中调用构造函数后,对象中已经有了一个初始值,但是我们不能将之称为类对象成员的初始化,只能称为赋值,因为初始化只可以初始化一次,而构造函数体内可以赋值多次

初始化列表

class Person {
public:
	//初始化列表方式初始化
	Person(int a, int b, int c) 
		: m_A(a)
		, m_B(b)
		, m_C(c) 
	{}
private:
	int m_A;
	int m_B;
	int m_C;
};
int main() {
	Person p(1, 2, 3);
	system("pause");
	return 0;
}

初始化列表:以一个冒号开始,以逗号分隔,每部分由成员变量后面跟上一个放在括号里的初始值或者表达式。

注意:

每个成员变量在初始化列表中只能出现一次(即初始化只一次)。

类中包含以下成员,就必须在初始化列表位置进行初始化。

  • 引用成员变量
  • const成员变量
  • 自定义类型成员(该类没有对应的默认构造函数)

我们这里先这么理解一下:初始化列表可以认为就是对象的成员变量定义的地方,对于上面的三种成员只能在定义初始化,而其他的内置类型变量,/可以在定义时初始化,也可以定义时不初始化,后面再赋值修改。

下面我们直接上代码:

int value = 10;
class A
{
public:
	A(int x)
		:_x(x)
	{}①
	/*A(int x = 0)
		:_x(x)
	{}*/②
private:
	int _x;
};
class Date
{
public:
	Date(int year, int n, int a)
		:_n(n)
		, _ref(value)
		,_aa(a)//①:当自定义类型没提供默认构造时,就在想用我们提供的值去初始化的时候,就在这里显式的去调用它的构造函数
		//如果有对应的默认构造,就不用写这行。不在初始化列表初始化,自动调用默认构造函数初始化
	{
		_year = year;
		//如果不在初始化列表初始化 ,但是我们还想去改变里面变量的值,只能这么玩
		//A aa(a);  调用默认构造 ②
		//_aa = aa;//赋值  这里使用了默认提供的赋值运算符重载?   这种方式麻烦  就用下面这种好
	}
private:
	int _year; // 声明
	const int _n;
	int& _ref;
	A _aa;
};
int main()
{
	Date d1(2022, 5, 20); // 对象定义
	system("pause");
	return 0;
}

代码解释:上述代码中,_n、_ref、_aa都是要在初始化列表初始化的变量,见代码注释

⭐️⭐️⭐️:上面在初始化的时候都比较麻烦,因此我们建议尽量在初始化列表初始化,如下代码:

int value = 10;
class A
{
public:
	A(int x)
		:_x(x)
	{}
private:
	int _x;
};
class Date
{
public:
	Date(int year, int n, int a)
		:_n(n)
		, _ref(value)
		, _year(year)
		, _aa(a)//当自定义类型没提供默认构造时,就在这里显式的去调用它的构造函数
	{}
	//总结:建议尽量在初始化列表初始化
private:
	int _year; // 声明
	const int _n;
	int& _ref;
	A _aa;
};
int main()
{
	Date d1(2022, 5, 20); // 对象定义
	system("pause");
	return 0;
}

正如我们代码注释的地方所说,当自定义类型没提供默认构造时,我们又想用我们提供的值去初始化的时候,就需要我们去手动的调用它的构造函数,,,,,,

初始化结果如下:

如果有对应的默认构造函数,我们也可以不用写这行。这时候就不在初始化列表初始化,编译器自动调用默认构造函数初始化,初始化为随机值还是确定值要看是什么类型的默认构造函数。

int value = 10;
class A
{
public:
	A(int x = 10)//全缺省默认构造
		:_x(x)
	{}
private:
	int _x;
};
class Date
{
public:
	Date(int year, int n, int a)
		:_n(n)
		, _ref(value)
		, _year(year)
		//, _aa(a)  这时候我们在A类中提供了全缺省的默认构造函数,就可以不写这行代码
	{}
private:
	int _year; // 声明
	const int _n;
	int& _ref;
	A _aa;
};
int main()
{
	Date d1(2022, 5, 20); // 对象定义
	system("pause");
	return 0;
}

如上述代码,我们没写, _aa(a)这一行,但我们提供了默认构造函数,结果表明依然可以初始化。

⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️⭐️:

下面我们再看一个例子:

class Stack
{
public:
	Stack(int capacity = 0)
	{
		_a = (int*)malloc(sizeof(int)*capacity);
		_top = 0;
		_capacity = capacity;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};
class MyQueue
{
public:
	MyQueue(int size = 100)
		:_size(size)//如果这里什么都没写,那么默认初始化列表就表示_st1就调用_st1的默认构造,_st2同理,
		//_size如果给了缺省值,就用缺省值初始化,没给就是随机值,
		//如果显式(int size = 1)、_size(size)写了  就用显式的这个值初始化
	{}
private:
	Stack _st1;
	Stack _st2;
	size_t _size = 1000; // 缺省值  如果上面哪个地方给了缺省参数,这里的这个缺省值也没用了
};
int main()
{
	MyQueue mq;
	return 0;
}

代码解释:在MyQueue类中声明_size的时候给了一个缺省值,然后在默认构造函数的地方也给了缺省形参,还在初始化列表中对_size进行初始化。如下结果:

class Stack
{
public:
	Stack(int capacity = 0)
	{
		_a = (int*)malloc(sizeof(int)*capacity);
		_top = 0;
		_capacity = capacity;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};
class MyQueue
{
public:
	MyQueue(int size = 100)
		//:_size(size)//如果这里什么都没写,那么默认初始化列表就表示_st1就调用_st1的默认构造,_st2同理,
		//_size如果给了缺省值,就用缺省值初始化,没给就是随机值,
		//如果显式(int size = 1)、_size(size)写了  就用显式的这个值初始化
	{}
private:
	Stack _st1;
	Stack _st2;
	size_t _size = 1000; // 缺省值  如果上面哪个地方给了缺省参数,这里的这个缺省值也没用了
};
int main()
{
	MyQueue mq;
	return 0;
}

如果上述_size没有在初始化列表初始化,那么_size就被声明时候给的缺省值初始化。如下结果:

我们再看三段代码,看看他们的不同之处:

class Stack
{
public:
	Stack(int capacity = 0)
	{
		_a = (int*)malloc(sizeof(int)*capacity);
		_top = 0;
		_capacity = capacity;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};
class MyQueue
{
public:
	MyQueue(int size = 100)
		:_size(size)
	{}
private:
	Stack _st1;
	Stack _st2;
	size_t _size = 1000; 
};
int main()
{
	MyQueue mq(10);
	return 0;
}
class Stack
{
public:
	Stack(int capacity = 0)
	{
		_a = (int*)malloc(sizeof(int)*capacity);
		_top = 0;
		_capacity = capacity;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};
class MyQueue
{
public:
	MyQueue(int size)
		:_size(size)
	{}
private:
	Stack _st1;
	Stack _st2;
	size_t _size = 1000; 
};
int main()
{
	MyQueue mq(10);
	return 0;
}
class Stack
{
public:
	Stack(int capacity = 0)
	{
		_a = (int*)malloc(sizeof(int)*capacity);
		_top = 0;
		_capacity = capacity;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};
class MyQueue
{
public:
	MyQueue(int size = 100)
	{}
private:
	Stack _st1;
	Stack _st2;
	size_t _size = 1000; 
};
int main()
{
	MyQueue mq(10);
	return 0;
}

explicit关键字

构造函数不仅可以构造和初始化对象,对于单个参数的构造函数,还具有类型转换的作用。

下面我们还是看一段代码:

class Date
{
public:
	/*explicit Date(int year)
	:_year(year)
	{
	cout << "Date(int year)" << endl;
	}*/
	Date(int year)
		:_year(year)
	{
		cout << "Date(int year)" << endl;
	}
private:
	int _year;
};
int main()
{
	Date d2 = 2022; // 构造 + 拷贝构造 -》 优化 合二为一
	system("pause");
	return 0;
}

代码解释:当没有在有参构造前面加explicit的时候,程序不会报错,以Date d2 = 2022这种方式来调用构造函数,其实是先调用有参构造,在调用拷贝构造函数完成!

⚠️如果我们加上explicit关键字:

class Date
{
public:
	explicit Date(int year)
	:_year(year)
	{
	cout << "Date(int year)" << endl;
	}
private:
	int _year;
};
int main()
{
	//Date d1(2022); // 构造
	Date d2 = 2022; // 构造 + 拷贝构造 -》 优化 合二为一
	//Date& d6 = 2022;//一开始就说了对常数取别名要加const
	//const Date& d6 = 2022;//整型2022被不同类型区别名时,前面就说了,临时变量具有常性,此时是引用的2022的临时变量的别名
	system("pause");
	return 0;
}

程序报错:

这是因为这其中发生了隐式类型转换,当加上explicit时,就阻止了这个转换!

⭐️再比如说:

class Date
{
public:
	explicit Date(int year)
	:_year(year)
	{
	cout << "Date(int year)" << endl;
	}
private:
	int _year;
};
int main()
{
	const Date& d6 = 2022;//整型2022被不同类型区别名时,前面就说了,临时变量具有常性,此时是引用的2022的临时变量的别名
	system("pause");
	return 0;
}

我们引用类型和引用实体不是同一个类型的时候,我们需要加上const,这是在前文就说过(前文查看),这其中发生隐式类型转换的时候产生了临时变量,需要加上const,那么这个时候加上explicit就阻止了这个转换,所以报错!

⭐️总结:反正隐式转换法中会有类型的转换,explicit可以阻止这种转换!

到此这篇关于C++类与对象深入之运算符重载与const及初始化列表详解的文章就介绍到这了,更多相关C++类与对象内容请搜索编程学习网以前的文章希望大家以后多多支持编程学习网!

本文标题为:C++类与对象深入之运算符重载与const及初始化列表详解

基础教程推荐