C++——类与对象(中)

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

如果一个类中什么成员都没有,简称为空类。
空类中真的什么都没有吗?并不是,任何类在什么都不写的时候,编译器会自动生成以下6个默认成员函数。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。

2.构造函数

2.1引入

在使用类的时候,比如对于下面的Date类,可以通过 Init 公有方法给对象设置日期,但如果每次创建对象时都调用该方法设置信息,过于麻烦,而且,我们常常会忘记初始化和销毁,这可能导致对象中的变量变成随机值

class Date
{
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()
{
	Date d1;
	d1.Print();
//这里忘记了初始化,打印出来的就是三个随机值
	return 0;
}

甚至可能导致运行崩溃,比如使用定义的栈,但忘记初始化和销毁的时候

class Stack
{
public:
	void Init()
	{
		a = nullptr;
		top = capacity = 0;
	}

	void Push(int x)
	{
		if (top == capacity)
		{
			int newcapacity = capacity == 0 ? 4 : capacity * 2;
			a = (int*)realloc(a, sizeof(int) * newcapacity);
		}
		a[top++] = x;
	}

	int Top()
	{
		return a[top - 1];
	}
private:
	int* a;
	int top;
	int capacity;
};


int main()
{

	Stack st;
	st.Push(1);
//这里没有初始化就进行了压栈操作,会导致运行崩溃
	st.Push(2);
	st.Push(3);
	st.Push(4);

	return 0;
}

为了解决以上问题,能否在对象创建时,就将信息设置进去呢?因此,C++引入了构造函数。

2.2概念

构造函数是一个 特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证
每个数据成员都有一个合适的初始值,并且 在对象整个生命周期内只调用一次
class Date
{
public:
    Date()//构造函数
    {
		_year = 1;
		_month = 1;
		_day = 1;     
 
    }
	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;
};

2.3特性

构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务 并不是开空间创建对象,而是初始化对象
其特征如下:
1. 函数名与类名相同。
2. 无返回值。(不需要写void)
3. 对象实例化时编译器 自动调用对应的构造函数。
4. 构造函数可以重载,就是说我们可以写多个构造函数,提供多种初始化方式。
5.当我们不主动去写构造函数,即类中没有显式定义构造函数时,C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。

2.3.1 Date类

class Date
{
public:
	//Date()// 1.无参构造函数
	//{
	//	_year = 1;
	//	_month = 1;
	//	_day = 1;
	//}

	//Date(int year, int month, int day)// 2.带参构造函数
	//{
	//	_year = year;
	//	_month = month;
	//	_day = day;
	//}

	Date(int year=1, int month=1, int day=1)
//利用缺省参数,上述两个函数还可以合并起来,而且不仅仅合并了,调用还更灵活了
//注意该函数不能与第一个函数共同存在
//在语法上,两函数构成函数重载,无参数和全缺省带参数,但调用的时候可能会存在歧义,存在二义性问题
	{
		_year = year;
		_month = month;
		_day = day;
	}

	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()
{
	Date d1;//可以不传参数

	//Date d3();//通过无参构造函数创建对象时,对象后面不用跟括号
	没有参数不能加(),否则会出问题,编译器分不清楚这里是要定义一个对象还是函数的声明

	Date d2(2024,8,20);//可以传3个参数
	//这里不会有歧义,函数声明中()里面给的应该是类型而不是实际的参数值

//一个类提供一个全缺省的构造函数是很棒的事情,它使得初始化很灵活
//构造函数也在公共代码区,也有this指针
//lea:取地址的意思

	Date d3(2024);//可以传一个参数

	d1.Print();
	d2.Print();
	d3.Print();
	return 0;
}

2.3.2 Stack类

class Stack
{
public:
	Stack()
//既然构造函数是为了初始化,我们可以直接将Init函数的代码拷贝过来吗?
	{
		a = nullptr;
		top = capacity = 0;
	}

	void Init()
	{
		a = nullptr;
		top = capacity = 0;
	}

	void Push(int x)
	{
		if (top == capacity)//扩容问题
		{
			cout << top << "扩容" << endl;
			size_t newcapacity = capacity == 0 ? 4 : capacity * 2;
			a = (int*)realloc(a, sizeof(int) * newcapacity);
			capacity = newcapacity;
		}
		a[top++] = x;
	}

	int Top()
	{
		return a[top - 1];
	}

	void Pop()
	{
		assert(top > 0);
		--top;
	}
	
	void Destroy()
	{
		free(a);
		a = nullptr;
		top = capacity = 0;
	}

	bool Empty()
	{
		return top == 0;
	}
private:
	int* a;
	int top;
	int capacity;
};

既然构造函数是为了初始化,那我们是不是可以直接将Init函数的代码拷贝到Stack这个构造函数中?

答案是可以的,但这样很不便利。

int main()
{
	Stack st;//这里就直接调用构造函数了
	st.Push(1);
	st.Push(2);
	st.Push(3);
	st.Push(4);

	while (!st.Empty())
	{
		cout << st.Top()<<" ";
		st.Pop();
	}
	cout << endl;

	st.Destroy();
///
	Stack st1;
	//局部变量属于栈帧里面的,建立栈帧时就会把函数中的变量等所需空间开辟出来
	for (int i = 0; i < 1000; i++)
	{
		st1.Push(i);
	}

	while (!st1.Empty())
	{
		cout << st1.Top() << " ";
		st1.Pop();
	}
	cout << endl;

	st1.Destroy();

	return 0;
}

这里对st1进行操作时,已知要压栈1000个数据,但是要是按照前面的写法,入栈时要不断地扩容,不停地使用realloc函数,扩容到后面,大概率都是异地扩容,开更大的空间,拷贝数据,释放旧空间,这是一种很挫的做法。

//也可以使用代码查看异地扩容了多少次
void Push(int x)
{
	if (top == capacity)
	{//后面大概率都是异地扩容,开更大的空间,拷贝数据,释放旧空间
		size_t newcapacity = capacity == 0 ? 4 : capacity * 2;
		int* tmp = (int*)realloc(a, sizeof(int) * newcapacity);
		if (tmp == nullptr)
		{
			perror("realloc fail");
			exit(-1);
		}
		if (tmp == a)
		{
			cout << capacity << "原地扩容" << endl;
		}
		else
		{
			cout << capacity << "异地扩容" << endl;
		}	

		a = tmp;
		capacity = newcapacity;
	}

	a[top++] = x;
}

所以我们要对Stack构造函数进行改进

Stack(size_t n = 4)//借助缺省参数
{
	cout << "Stack(size_t n = 4)" << endl;
//这个用来查看构造函数是否是在对象定义出来时就调用了

	if (n == 0)
	{
		a = nullptr;
		top = capacity = 0;
	}
	else
	{
		a = (int*)malloc(sizeof(int) * n);
		if (a == nullptr)
		{
			perror("realloc fail");
			exit(-1);
//exit(-1)  让程序退出,以-1的方式,-1就是一种异常的方式退出
//正常方式的退出都是0
		}

		top = 0;
		capacity = n;
	}
}

//这样的话已知要压栈1000个数据时,就可以直接这样定义
Stack st1(1000);

注意:C++有规定,构造函数是必须要调用的

2.3.3内置类型和自定义类型

关于 编译器生成的默认成员函数已知不实现构造函数的情况下,编译器会 生成默认的构造函数。
那么上面 Date创建的对象调用编译器生成的默认构造函数时, Date所创建的对象的_year、_month和_day,依旧是随机值。那么在这里, 编译器生成的 默认构造函数不就没有作用吗?
这里又是一个新的知识点:在调用构造函数时, C++把类型分成了内置类型(基本类型)和自定义类型。
2.3.3.1内置类型
内置类型就是语言提供的数据类型,比如:int、double、char、int*等。(Date*也是,指针都是内置类型,指针是用来存地址的)
内置类型的 成员不会进行处理(有些编译器会处理,但标准并未规定要处理)
class Date
{
public:
	Date(int year=1, int month=1, int day=1)

	{
     //没有对_year进行操作,_year就是1
		_month = month;
		_day = day;
	}

	void Init(int year, int month, int day)
	{

		_year = year;
		_month = month;
		_day = day;

	}

	void Print()
	{

		cout <<_year<<"/"<<_month <<"/"<< _day<< endl;
	}
private:
	int _year=1; //声明
	int _month=1;
	int _day=1;
//在C++11中,声明处支持给缺省值,这里不是初始化,声明不会开辟空间
//这是声明给的缺省值,这样默认生成的构造函数就可以用这个缺省值进行初始化
//这个缺省值可以理解为就是用来补内置类型的坑
};
2.3.3.2自定义类型
自定义类型就是我们使用class、struct、union等自己定义的类型,比如结构、类。

对于自定义类型的成员才会处理,会去调用这个成员的默认构造函数
 

2.3.3.3特例:用栈实现队列
综上可知,一般情况下默认生成的构造函数是没有太大价值的。
但有一种情况是非常适用的
// 两个栈实现一个队列
class MyQueue
{
private:
	Stack _pushst;
	Stack _popst;

	//Date* _ptr;
	//int _size;
};

2.3.4编译生成的默认构造的特点

构造函数,也就是默认成员函数,我们不写,编译器会自动生成

编译生成的默认构造的特点

1、我们不写才会生成,我们写了任意一个构造函数就不会生成了

2、内置类型的成员不会处理(C++11,声明支持给缺省值)

C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即: 内置类型成员变量在类中声明时可以给默认值

3、自定义类型的成员才会处理,会去调用这个成员的默认构造函数

所以一般情况都需要我们自己写构造函数,来决定初始化方式,成员变量全是自定义类型时,可以考虑不写构造函数

无参的构造函数全缺省的构造函数都称为 默认构造函数,并且默认构造函数 有且只能有一个。因为多个并存会存在调用歧义。(调用冲突、调用二义性)
注意:无参构造函数、全缺省构造函数、我们不写编译器默认生成的构造函数,都可以认为
是默认构造函数。

总结:不传参就可以调用的就是默认构造函数

2.3.4.1注意

1.

这样写会报错,因为它既不是无参构造函数,也不是全缺省构造函数,而且因为我们写了构造函数,所以编译器不会生成默认构造函数。

2.

class Stack
{
public:
	Stack(size_t n)
	{
//...
	}

private:
	int* a;
	int top;
	int capacity;
};
class MyQueue
{
private:
	Stack _pushst;
	Stack _popst;
//注意:这里不能传参	Stack _pushst(1);是错误的

};

int main()
{
	MyQueue mq;

	return 0;
}

这里也会报错,因为调用mq的构造函数时,会去调用mq中两个成员的默认构造函数,但它们没有默认构造函数,所以会报错。

3.析构函数

3.1 概念

通过前面构造函数的学习,我们知道了一个对象是怎么来的,那一个对象又是怎么没的呢?
析构函数:与构造函数功能相反,析构函数 不是完成对对象本身的销毁局部对象销毁工作是由编译器完成的。而 对象在销毁时会自动调用析构函数,完成对象中资源的清理工作
class Date
{
public:
	Date(int year=1, int month=1, int day=1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	~Date()//Date类中其实没有什么资源需要清理
//年月日是属于对象的,对象本身的数据不需要清理,对象是在栈帧里面,栈帧出了作用域就自动销毁了
	{
		cout << "~Date()" << endl;
	}
	void Print()
	{

		cout <<_year<<"/"<<_month <<"/"<< _day<< endl;
	}
private:
	int _year;//声明
	int _month;
	int _day;
};

int main()
{
	Date d1;
	//对象定义在这里不需要我们去销毁,对象在栈帧中,栈帧结束,它就跟着销毁了
	//出了作用域清栈帧就直接带走了

	//对象出了作用域、生命周期到了以后,会自动调用析构函数,完成对象中资源的清理工作。

	//~  在C语言中是按位取反的意思

	return 0;
}

调用函数会把栈帧拉开,栈帧开多大呢?编译器提前编译时就已经算好了,栈帧结束就销毁了。

而堆上的空间不手动释放就会导致内存泄露。

3.2 特性

析构函数是特殊的成员函数,其 特征如下:
1. 析构函数名是在类名前加上字符 ~。
2. 无参数无返回值类型。
3. 一个类 只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意: 析构函数不能重载,因为它没有参数
4. 对象生命周期结束时,C++编译系统系统自动调用析构函数

3.2.1

编译器自动生成的 默认析构函数,和默认构造函数类似
1.对内置类型成员不会进行处理
2.对自定类型成员会调用它的析构函数。

3.2.2

那么,哪种情况需要去显式地写析构函数?
比如栈,这样就不用去写Destroy了
清理资源,如:malloc的、fopen的,把动态申请的、显式地申请的去释放一下
class Stack
{
	~Stack()
	{
		cout << "~Stack()" << endl;
		free(a);
		a = nullptr;
		top = capacity = 0;
	}
private:
	int* a;
	int top;
	int capacity;
} 

int main()
{
	Date d1;
	Date d2;

	Stack st1;
	Stack st2;

    //先调用st2的析构,后定义的先析构
    //因为在栈帧里面,C++有规定,这些数据需要后进先出

	return 0;
}

同时要注意:析构函数调用顺序为后定义的对象先调用析构函数

3.2.3总结

如果类中 没有申请资源时析构函数可以不写,直接使用 编译器生成的默认析构函数,比如Date类;有资源申请时,一定要写,否则可能会造成资源泄漏,比如Stack类忘记使用Destroy时。
默认成员函数最大的优势:实例化对象时会自动调用

3.2.4使用默认析构函数的一种情况

// 两个栈实现一个队列
class MyQueue
{
private:
	Stack _pushst;
	Stack _popst;

	//Date* _ptr;
	//int _size;
};

int main()
{
    MyQueue mq;
   
    return 0;
}

4.拷贝构造函数

4.1引入

日常学习中,除了有初始化的需求,还有拷贝的需求。比如把当前对象拷贝一份出来。那么如何拷贝呢?是直接拷贝吗?           (赋值重载本质也是一种拷贝

4.1.1什么情况下会出现拷贝?

void func1(Date d)//将d2拷贝给了d
//实际上这里如果没有显式地写拷贝构造,就会调用默认的拷贝构造
{
	d.Print();
}


int main()
{
	Date d2(2024, 8, 23);

	func1(d2);//这里就是拷贝
	//调用时传过去,按照C语言的理解是直接拷贝
	//传值传参,传了以后开空间出来,把12byte的数据依次拷贝过去,类似memcpy

    //C语言允许结构体拷贝
	return 0;
}

但这里会出现问题,当对Date类型的对象进行拷贝时,一切正常。

但对Stack类型的对象进行拷贝时,就会出现问题

class Stack
{
public:
//...
	~Stack()
	{
		cout << "~Stack()" << endl;
		free(a);
		a = nullptr;
		top = capacity = 0;
	}

private:
	int* a;
	int top;
	int capacity;
};
void func2(Stack s)
{

}


int main()
{
	Stack s1;

	func2(s1);
	//调用建立func2的栈帧,然后进行传参,传值传参是一种值拷贝或者说是浅拷贝
	return 0;
}

4.1.2为什么会崩溃呢?

实际上是在析构函数崩溃的。C语言中也有这个问题,它叫做浅拷贝问题值拷贝(浅拷贝)会把每个byte依次拷贝过去,C语言做得就是这样的直接拷贝,默认情况下C语言还不会出问题,因为C语言不会自动去调用析构,所以在C语言中注意这个场景就不会出问题,但C++中会自动调用析构,比如上述代码,func2先结束,出了作用域,func2优先去调用析构函数,就会把a指向的空间给释放,虽然置空了,但置空的是func2中的s对象的a,不会影响main函数中的s1对象的a,而且它还会变成野指针,因为它指向的空间已经被释放了,然后回到main栈帧,还会调用析构函数,所以这块空间就会释放两次,第二次调用析构时就会崩溃。相当于两个对象同时去调用了Destroy,也因此它对于Date类是没有问题的,而对于Stack类就会出现问题。

4.1.3解决方案(Date类)

1.使用引用

void func2(Stack& s)//一个对象不会析构两次
//只是别名,不会析构
{

}

int main()
{
	Stack s1;
	func2(s1);
	return 0;
}

但这里也存在问题,比如在func2函数中进行Push操作,那么就会影响s1。

我们更期望程序不会崩溃的前提下,s的改变,比如插入一些数据,不影响s1。

2.拷贝构造函数(Date类)

C++认定这样一个问题,用同一个对象去拷贝另外一个对象时,定义了一个函数来进行解决,这个函数就叫做拷贝构造函数,它是专门完成拷贝的一个构造函数。

class Date
{
public:
	Date(Date d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
//...
}
int main()
{
    Date d2(2024, 8, 23);

	Date d3(d2);//d3是d2的拷贝
    
    //Stack s2(s1);//s2是s1的拷贝
	
	return 0;
}
4.1.3.1新的问题

然而此时又会出现新的问题,这是为什么

4.1.3.2原因

Date(Date d)
//这里调用拷贝构造,调用函数的第一件事情是传参,但是如果传参使用传值的方式会出现问题       
//C++认为传参就是一个拷贝构造,还没有调用上这个函数时就要先传参传参又会形成一个新的拷贝构造
//而新的拷贝构造又要传参,传参又要形成一个新的拷贝构造,如此无限循环

{
    _year = d._year;
    _month = d._month;
    _day = d._day;
}

为什么传值传参要调用拷贝构造,而不像C语言传结构体直接进行浅拷贝的方式一样?
原因:函数传参,如果是内置类型,直接传参没有问题,但自定义类型不能直接传,而是要去调用一个函数去传,这个函数就叫拷贝构造,调用函数可以完成深拷贝。
       比如上述Stack这样的类,如果按照浅拷贝的方式,会有析构两次的问题,也就是浅拷贝的问题。传参调用拷贝构造是为了解决C语言留下的坑。
        

        C++规定,传值传参必须要去调用拷贝构造完成拷贝,不能像C语言那样直接拷贝
        C++规定,自定义类型传参这种,它是一种对象间的拷贝初始化,需要调用拷贝构造才能完成这个过程

所以正确的方法是:

class Date
{
public:
	Date(Date& d)
	//这里必须是引用,否则会出现一个问题:无穷递归
	//引用是别名,引用的底层是指针,所以引用不涉及拷贝的问题,也因此指针也可以解决这个问题
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
//...
}
int main()
{
	
    Date d2(2024, 8, 23);

//以下两种写法是等价的,都是拷贝构造
    Date d3(d2);
	//Date d3 = d2;
    //这样写也是拷贝构造

	return 0;
}

就比如上面的func1函数,虽然没有自定义类型,但其实也会调用拷贝构造函数,也因为使用了引用,所以这里不会出现无穷递归的问题。

同时使用指针也可以解决问题,但是使用起来不够舒服,而且这个不算是显式定义拷贝构造,编译器会生成默认拷贝构造

class Date
{
public:
	Date(Date* d)
	{
		_year = d->_year;
		_month = d->_month;
		_day = d->_day;
	}
//...
}
int main()
{
	
    Date d2(2024, 8, 23);

    Date d3(&d2);
    //Date d3(d2);

	return 0;
}

4.1.4解决方案(Stack类)

class Stack
{
public:
	//Stack s2(s1);
	Stack(Stack& s)
	{//深拷贝,深一个层次的拷贝,这里要保证两个对象的a指向的是不同的空间
		a = (int*)malloc(sizeof(int) * s.capacity);
		if (a == nullptr)
		{
			perror("malloc申请空间失败!");
			return;
		}
		top = s.top;
		capacity = s.capacity;
		memcpy(a, s.a, sizeof(int) * s.capacity);
	}
//...
private:
	int* a;
	int top;
	int capacity;

};

4.1.5总结

        拷贝构造最重要的特性是,用当前类型的对象去拷贝初始化另一个对象,严格来说,分为两类拷贝,一类如Date类这样直接拷贝,另外一类就是如Stack类这样的,我们需要自己书写,来完成深拷贝,必须使用引用传参,而不能使用传值传参,传值传参会引发无穷递归

4.2概念

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

4.3特性

拷贝构造函数也是 特殊的成员函数,其 特征如下:
1. 拷贝构造函数 是构造函数的一个重载形式
2. 拷贝构造函数的 参数只有一个必须是同类型对象的引用,使用 传值方式编译器会直接报错,因为会引发 无穷递归调用
3. 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数将对象按 内存存储字节序完成拷贝,这种拷贝叫做 浅拷贝,或者 值拷贝
注意:在编译器生成的 默认拷贝构造函数中, 内置类型按照字节方式直接拷贝的,而自定
义类型是 调用其拷贝构造函数完成拷贝的。
int main()
{
	// 我们不写,编译默认生成的拷贝构造,跟之前的构造函数特性是不一样的
	// 1、内置类型, 完成值拷贝(浅拷贝)   一定程度兼容了C语言
	// 2、自定义的类型,调用它的拷贝构造

	Date d1(2024,8,25);
	Date d2=d1;

	return 0;
}

总结:Date类不需要我们实现拷贝构造,默认生成的拷贝构造就可以使用

而Stack类需要我们自己实现深拷贝的拷贝构造,使用默认生成的会出问题

那么什么时候可以使用默认的拷贝构造呢?

class MyQueue
{
private:
	Stack _pushst;
	Stack _popst;

};

int main()
{
	//MyQueue对于默认生成的几个函数都非常受用

	MyQueue mq1;

	MyQueue mq2 = mq1;

	return 0;
}
拷贝构造函数 典型调用场景
1.使用已存在对象创建新对象
2.函数参数类型为类类型对象
3.函数返回值类型为类类型对象
注意:为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用尽量使用引用。

4.3.1补充

引用传参前最好加上const

//Date d2(d1);
class Date
{
public:
	Date(const Date& d)
//一般来说,引用的形参
//只要不是要改变这个形参,或是不做输出型参数的话,都会加const缩小权限,防止它被修改(误伤)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
//...
}

因为有时可能会写错,为了避免这种情况

//Date d2(d1);
class Date
{
public:
	Date(Date& d)
	{
		d._year=_year;
		d._month=_month;
		d._day=_day;
	}

//整个写反了,相当于把d2拷贝给了d1
//而本意是想把d1拷贝给d2
//但是代码是不会报错的,所以为了避免这种情况发生,要加上const
}

5.赋值运算符重载

5.1运算符重载的引入

5.1.1问题一

要想对两个日期对象比较大小,如何比较呢?

int main()
{
	//要对两个日期对象比较大小,如何比较?
	Date d1(2023, 7, 25);
	Date d2(2023, 7, 26);

//	d1 < d2;
	//日期能不能像这样使用运算符比较大小呢?
	//当然是不能的,它们是自定义类型

	return 0;
}

5.1.2问题二

先抛开上述问题,我们来思索一下,内置类型为什么能知道运算符的使用规则?

int main()
{
	int i = 1, j = 10;
	i < j;
    //内置类型为什么知道怎么使用运算符呢?

//因为内置类型是语言原生定义的,而且内置类型都是一些比较简单的类型
//如整型、浮点型等,因为是语言原生定义的,所以它们知道比较规则

//所以在这里可以直接支持比较
	
//编译器只是个工具人,它要去围绕祖师爷本贾尼实现他的想法
	
    return 0;
}

5.1.3问题一(续)

我们接着来看第一个问题,如何比较两个日期对象的大小?

一种方式就是写一个专门用于比较日期对象的函数。

bool DateLess(const Date& x1, const Date& x2)
{
	if (x1._year < x2._year)
	{
		return true;
	}
	else if (x1._year == x2._year && x1._month < x2._month)
	{
		return true;
	}
	else if (x1._year == x2._year && x1._month == x2._month && x1._day < x2._day)
	{
		return true;
	}
	else
	{
		return false;
	}
}

int main()
{

	Date d1(2023, 7, 25);
	Date d2(2023, 7, 26);

	cout << DateLess(d1,d2) << endl;
//这样比较时就可以直接调用函数来比较

	return 0;
}
5.1.3.1注意

因为函数中调用的成员变量是私有的,所以上述代码实际会报错,这个问题需要专门解决一下

class Date
{
public:
//...
private:
	int _year;//声明
	int _month;
	int _day;
};

一、将私有的成员变量置为公有的,这也是最暴力的一种解决办法

二、在Date类中额外定义几个函数,如

class Date
{
public:
//...

GetYear()   
{
    return _year;//在Java中常会使用这种方式
//在自己的函数中直接使用这个成员函数就可以了
}

private:
	int _year;//声明
	int _month;
	int _day;
};

三、把比较日期对象的函数写进Date类中,使之由全局函数变为成员函数

5.1.4问题三

但这里还存在一些问题,比如比较日期对象这个函数的命名,不同人的命名习惯都有所不同,会产生千奇百怪的函数名,可能会使得他人看不懂函数的作用,甚至自己写完代码很久以后,都可能会遗忘函数的意义

bool DateLess(const Date& x1, const Date& x2);

bool riqixiaoyu(const Date& x1, const Date& x2);

bool Compare1(const Date& x1, const Date& x2);

5.1.5解决方案

所以为了解决这些问题增加代码的可读性,C++引入了运算符重载

//增加一个特殊的函数,叫做运算符重载
//重载就有重新定义的意思,重新定义这里的运算符重载
//编译器不知道你的自定义类型怎么比,但是写代码的自己是知道的
bool operator<(const Date& x1, const Date& x2)

{
	if (x1._year < x2._year)
	{
		return true;
	}
	else if (x1._year == x2._year && x1._month < x2._month)
	{
		return true;
	}
	else if (x1._year == x2._year && x1._month == x2._month && x1._day < x2._day)
	{
		return true;
	}
	else
	{
		return false;
	}
}

int main()
{
	Date d1(2023, 7, 25);
	Date d2(2023, 7, 26);

//  cout << d1 < d2 << endl;
//这里编译报错的原因是,流插入运算符的优先级比‘<’高
//所以会先走cout<<d1,走完之后这里的返回值和后面的又对不上,所以这里要加括号
	
    cout << (d1 < d2) << endl;//引入运算符重载后,这里就可以这样写
	//这里本质被转换成了下面这个特殊的成员函数
	
    cout << (operator<(d1,d2)) << endl;
    //可以像这样显式调用,但一般不需要,编译器会自动转换
	
    //C++规定operator加上'<'就是函数名
	//这个函数是一个特殊的成员函数,相当于对运算符、运算方式的重新定义

	return 0;
}

这样自定义类型就也可以像内置类型一样,支持这些运算符了,使用起来会很方便。

int main()
{

	Date d1(2023, 7, 25);
	Date d2(2023, 7, 26);

	int i = 1, j = 10;
	//C++会去分辨是自定义类型还是内置类型

	i < j;//这里会转换成相关指令  cmp

	d1 < d2;//这里会去调用相关函数
	//不写运算符重载,自定义类型是不支持比较的

	return 0;
}

运算符重载真正的意义是,虽然使用普通的函数也可以完成上述的操作,但是使用运算符重载后,会让可读性变强

5.2运算符重载

C++为了 增强代码的可读性引入了 运算符重载,运算符重载是 具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字 operator后面接 需要重载的运算符符号
函数原型返回值类型  operator操作符(参数列表)

5.2.1注意

一、不能通过连接其他符号来创建 新的操作符:比如operator@
二、重载操作符必须 有一个类类型参数,也就是 自定义类型参数
bool operator+(int& x1, int& x2)
{
//...
}
//比如这里,想把两个整型的加法规则更改掉就是不允许的
//因为重载操作符必须有一个类类型参数
三、用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能去改变其含义
四、作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
        此时我们再回到之前的问题一(续)的注意,在C++中,可以选择第三种方式,把 比较日期对象的函数写进Date类中,使之成为 成员函数。但这里直接把 operator<函数完全照搬地写进Date类中是行不通的。
class Date
{public:

//	d1 < d2  
//	d1.operator<(d2)
bool operator<(const Date& d)//d1是this,d2是d
{
    if (_year < d._year)
	{
		return true;
	}
	else if (_year == d._year && _month < d._month)
	{
		return true;
	}
	else if (_year == d._year && _month == d._month && _day < d._day)
	{
		return true;
	}
	else
	{
		return false;
	}
}
//...
private:
    int _year;
    int _month;
    int _day;
}

int main()
{

	Date d1(2023, 7, 25);
	Date d2(2023, 7, 26);

	cout << (d1 < d2) << endl;
//编译器先去全局找,再去类中找,找到以后再去转换成下面的样子
	cout << (d1.operator<(d2)) << endl;

	return 0;
}

五、不能改变 操作符操作数个数,一个操作符是几个操作数,那么重载的时候就有几个参数,但有一个参数this是省略掉的
       有些操作符是 两个操作数,比如+、-、<
       还有些操作符是 一个操作数,比如++
六、.*    ::(域作用限定符)    sizeof    ?:(三目操作符)    .    注意:以上这5个运算符 不能重载
* 会作为两种重载,乘以、解引用

  • 19
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值