【C++初阶】类与对象(二)

目录

  • 前言:
  • 一、构造函数
    • [1.1 构造函数概念](#1.1 构造函数概念)
    • [1.2 为什么有构造函数](#1.2 为什么有构造函数)
    • [1.3 构造函数的写法及使用](#1.3 构造函数的写法及使用)
    • [1.4 默认构造函数](#1.4 默认构造函数)
    • [1.5 哪些可为默认构造函数](#1.5 哪些可为默认构造函数)
  • 二、析构函数
    • [2.1 析构函数概念](#2.1 析构函数概念)
    • [2.2 为什么有析构函数](#2.2 为什么有析构函数)
    • 2.3析构函数的写法及使用
    • [2.4 默认析构函数](#2.4 默认析构函数)
  • 三、拷贝构造函数
    • [3.1 拷贝构造函数概念](#3.1 拷贝构造函数概念)
    • [3.2 为什么有拷贝构造函数](#3.2 为什么有拷贝构造函数)
    • [3.3 拷贝构造函数的写法及使用](#3.3 拷贝构造函数的写法及使用)
    • [3.4 默认拷贝构造函数](#3.4 默认拷贝构造函数)
  • 四、赋值运算符重载
    • [4.1 运算符重载](#4.1 运算符重载)
    • [4.2 赋值运算符重载](#4.2 赋值运算符重载)
      • [4.2.1 赋值运算符重载概念](#4.2.1 赋值运算符重载概念)
      • [4.2.2 赋值运算符重载写法及使用](#4.2.2 赋值运算符重载写法及使用)
      • [4.2.3 默认赋值运算符重载](#4.2.3 默认赋值运算符重载)
    • [4.3 日期类的实现](#4.3 日期类的实现)
      • [4.3.1 创建类和函数声明](#4.3.1 创建类和函数声明)
      • [4.3.2 全缺省的构造函数](#4.3.2 全缺省的构造函数)
      • [4.3.3 获取某年某月的天数](#4.3.3 获取某年某月的天数)
      • [4.3.4 析构、拷贝构造和赋值运算符重载](#4.3.4 析构、拷贝构造和赋值运算符重载)
      • [4.3.5 日期+=天数](#4.3.5 日期+=天数)
      • [4.3.6 日期-=天数](#4.3.6 日期-=天数)
      • [4.3.7 日期+天数和日期-天数](#4.3.7 日期+天数和日期-天数)
      • [4.3.8 前置++和后置++](#4.3.8 前置++和后置++)
      • [4.3.9 前置--和后置--](#4.3.9 前置--和后置--)
      • [4.3.10 !=、>=、<、<=运算符重载](#4.3.10 !=、>=、<、<=运算符重载)
      • [4.3.11 日期-日期 返回天数](#4.3.11 日期-日期 返回天数)
    • [4.4 日期类的全部代码](#4.4 日期类的全部代码)
      • [4.4.1 Date.h](#4.4.1 Date.h)
      • [4.4.2 Date.cpp](#4.4.2 Date.cpp)
      • [4.4.3 test.cpp](#4.4.3 test.cpp)
  • 五、const成员函数
    • [5.1 const成员函数概念](#5.1 const成员函数概念)
    • [5.2 const成员函数写法](#5.2 const成员函数写法)
    • [5.3 关于const的权限问题](#5.3 关于const的权限问题)
  • 六、取地址及const取地址操作符重载

前言:

类有6个默认成员函数,简单介绍下进入正题,分别是:

  • 构造函数------初始化
  • 析构函数------清理
  • 拷贝构造函数------使用同类对象初始化创建对象
  • 赋值运算符重载------一个对象赋值给另一个对象
  • const成员函数------修饰this指针
  • 取地址重载------获取对象地址

默认的意思是我们自己不写编译器自动生成,当然我们也可以自己写。所以,分析默认成员函数主要就围绕两个点:
1、写,应该怎样实现;
2、不写,是否符合要求。

一、构造函数

1.1 构造函数概念

构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用 ,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。

例如:

c 复制代码
class Date
{
public:
//   构造函数
	Date(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(2023, 10, 29);
	d1.Print();
	return 0;
}

运行结果:

1.2 为什么有构造函数

以前写C语言的时候,总要有个先初始化的函数,如果忘记写了,就会导致程序运行失败。虽然不要忘记写初始化是非常要注意的事情,但是仍然还是有失误的时候。所以呢,C++的创始人引入构造函数这个概念,在对象创建时,把要初始化的信息设置进去,同时也比以前更加方便。

1.3 构造函数的写法及使用

构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象

构造函数有以下基本的特征:

1.函数名与类名相同

2.无返回值

3.对象实例化时编译器自动调用 对应的构造函数

4.可以重载

构造函数支持重载让它有多种写法,概括一下就是有带参和无参的情况

1️⃣没有带参数:

c 复制代码
Date()
	{
		_year = 4;
		_month = 5;
		_day = 6;
	}
	/
	Date d1;

注意:没有参数时d1后面是不能带括号的,如果带括号就变成声明函数了。

2️⃣有带参数:

c 复制代码
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	/
	Date d1(2023, 10, 29);

构造函数参数可以是缺省参数

c 复制代码
	Date(int year = 2020, int month = 9, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	/
	Date d1;

1.4 默认构造函数

如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。

1️⃣我们将构造函数注释掉:

c 复制代码
/*Date(int year = 2020, int month = 9, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}*/

运行结果:

为什么会是随机值呢?这个问题后面会解决。但是可以确定在我们没写构造函数时,编译器会自动调用生成的构造函数。

2️⃣再写个栈类作对比:

cpp 复制代码
class Stack
{
public:
	Stack(int capacity = 3)
	{
		_a = (int*)malloc(sizeof(int) * capacity);
		if (_a == nullptr)
		{
			perror("malloc fail");
			exit(-1);
		}
		_top = 0;
		_capacity = capacity;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};
int main()
{
	Stack st1;
	return 0;
}

用构造函数初始化后值的结果:

数组_a是动态开辟的空间,没有放数据,所以是随机值。其他的被初始化后有相应的值。

栈类的构造函数也注释掉,看会发生什么:

cpp 复制代码
	/*Stack(int capacity = 3)
	{
		_a = (int*)malloc(sizeof(int) * capacity);
		if (_a == nullptr)
		{
			perror("malloc fail");
			exit(-1);
		}
		_top = 0;
		_capacity = capacity;
	}*/

也是随机值,既然如此,那么我们不写构造函数时编译器自动生成的默认构造函数好像没什么用,其实不是这样的。

3️⃣再创建一个类,两个栈实现队列:

cpp 复制代码
class Stack
{
public:
	Stack(int capacity = 3)
	{
		_a = (int*)malloc(sizeof(int) * capacity);
		if (_a == nullptr)
		{
			perror("malloc fail");
			exit(-1);
		}
		_top = 0;
		_capacity = capacity;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};
class Queue
{
	Stack st1;
	Stack st2;
	int size;
};
int main()
{
	Queue q1;
	return 0;
}

队列类没有写它的构造函数,为什么_top和_capacity不是随机值呢?因为内置类型的成员不做处理,自定义类型的成员会去调用它的构造函数。 这里就可以解释前面的日期类不写构造函数为什么是随机值了,因为日期类的成员变量都是内置类型,所以编译器不做处理,就为随机值。队列里有两个成员是自定义类型,会去调用自定义类型的成员的构造函数,如果自定义类型的成员我们也没有写,就是随机值。

内置类型:int float double ...

自定义类型:struct class ...

如果类里面都是内置类型的成员,又想要默认的构造函数,该怎么办?

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

cpp 复制代码
class Date
{
public:
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year = 2023;
	int _month = 1;
	int _day = 1;
};
int main()
{
	Date d1;
	d1.Print();
	return 0;
}

1.5 哪些可为默认构造函数

1.没有传参的:

cpp 复制代码
	Date()
	{
		_year = 1;
		_month = 2;
		_day = 3;
	}

2.全缺省的:

cpp 复制代码
	Date(int year = 2020, int month = 9, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

3.自己不写,编译器自动生成的

以上这些都可以叫做默认构造函数,总结一下就是没有传参的为默认构造函数。

注意:写的时候只能出现其中一个,重复出现会报错

二、析构函数

2.1 析构函数概念

与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作

例如:

cpp 复制代码
class Date
{
public:
	//构造函数
	Date(int year = 2020, int month = 9, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//析构函数
	~Date()
	{
		cout << "~Date()" << endl;
		_year = 0;
		_month = 0;
		_day = 0;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	d1.Print();
	return 0;
}

打印~Date()就说明调用了析构函数。

2.2 为什么有析构函数

与构造函数的理由相反,前面的是防止忘记初始化,这里是防止忘记清理。其实清理对内置类型不作处理的(后面还会谈到),所以日期类不写析构函数让编译器自动生成就可以了。但是如果我们有申请空间,比如malloc,它不会随着程序结束而结束,要我们自己主动释放才行,否则会内存泄漏,所以这种情况必须自己写析构函数。

2.3析构函数的写法及使用

先了解下析构函数的几个特点:

1.析构函数名是在类名前加上字符 ~

2.无参数无返回值类型

3.一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载

4.对象生命周期结束时,C++编译系统系统自动调用析构函数

因为析构函数不支持重载,所以写法只有一种:(日期类)

cpp 复制代码
	//析构函数
	~Date()
	{
		cout << "~Date()" << endl;
	}

再来看看栈类:写法与以前的销毁函数一样

cpp 复制代码
class Stack
{
public:
	//构造函数
	Stack(int capacity = 3)
	{
		_a = (int*)malloc(sizeof(int) * capacity);
		if (_a == nullptr)
		{
			perror("malloc fail");
			exit(-1);
		}
		_top = 0;
		_capacity = capacity;
	}
	//析构函数
	~Stack()
	{
		cout << "~Stack()" << endl;
		free(_a);
		_a = nullptr;
		_top = 0;
		_capacity = 0;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};
int main()
{
	Stack st1;
	return 0;
}

打印出这个结果说明调用了析构函数

2.4 默认析构函数

当我们不写时,编译器自动调用生成的析构函数。对内置类型不处理,对自定义类型调用它的析构函数。

cpp 复制代码
class Stack
{
public:
	Stack(int capacity = 3)
	{
		_a = (int*)malloc(sizeof(int) * capacity);
		if (_a == nullptr)
		{
			perror("malloc fail");
			exit(-1);
		}
		_top = 0;
		_capacity = capacity;
	}
	~Stack()
	{
		cout << "~Stack()" << endl;
		free(_a);
		_a = nullptr;
		_top = 0;
		_capacity = 0;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};
class Queue
{
	Stack st1;
	Stack st2;
	int size;
};
int main()
{
	Queue q1;
	return 0;
}

运行结果:

一个是st1的,另一个是st2的

注意:如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。

三、拷贝构造函数

3.1 拷贝构造函数概念

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

cpp 复制代码
	//拷贝构造函数
	Date(Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

3.2 为什么有拷贝构造函数

当我们在调用一个函数传参时,采用传值调用(可以用引用,但是这里我们是要拷贝参数,毕竟是在拷贝构造函数的知识点里),形参是实参的一份拷贝,如果是内置类型,没有啥问题;如果传的是对象,会自动调用拷贝构造函数。

上面的例子是传值,即实参拷贝给形参,拷贝的是对象。
总之,不管哪种情形,只要是拷贝对象,就会自动调用拷贝构造函数

1️⃣日期类: 调用Func函数

cpp 复制代码
class Date
{
public:
	Date(int year = 2020, int month = 9, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Func(Date d)
	{
		Print();
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	d1.Func(d1);
	return 0;
}

d1是实参,d是形参,d1拷贝给d,然后调用Func函数

运行结果:

没有什么问题。

2️⃣看看栈类会发生什么: 调用Func函数

cpp 复制代码
class Stack
{
public:
	//构造函数
	Stack(int capacity = 3)
	{
		_a = (int*)malloc(sizeof(int) * capacity);
		if (_a == nullptr)
		{
			perror("malloc fail");
			exit(-1);
		}
		_top = 0;
		_capacity = capacity;
	}
	//析构函数
	~Stack()
	{
		free(_a);
		_a = nullptr;
		_top = 0;
		_capacity = 0;
	}
	void Func(Stack st)
	{
		cout << "Func(Stack st1)" << endl;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};
int main()
{
	Stack st1;
	st1.Func(st1);
	return 0;
}

st1是实参,st是形参,st1拷贝给st,然后调用Func函数

运行结果:

编译器报错了,那么理由是什么呢?

如图:

st是st1的一份拷贝,st1的值赋给了st,但是两个_a的指向同一块空间,调用Func函数出了这个函数的作用域,自动调用析构函数,把这块空间清理了,然后程序运行结束,再次自动调用析构函数,这块空间相当于被清理了两次。前面第一次st的_a指向这块空间被释放了,出Func函数,st1的_a变成野指针了,程序结束再释放就导致程序崩溃。

以上的写法都是浅拷贝,即值传递。日期类没有问题,但是栈类会出现前面报错的情况,解决栈类的问题,就要用深拷贝,在这种情况下必须要有拷贝构造函数。

3.3 拷贝构造函数的写法及使用

先了解下拷贝构造函数的特点:

拷贝构造函数是构造函数的一个重载形式。

拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用

日期类:

cpp 复制代码
	//拷贝构造函数
	Date(Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

如果参数没有加引用,为什么会出现无穷递归?

先简单看一段代码(正确的):

cpp 复制代码
class Date
{
public:
	Date(int year = 2023, int month = 10, int day = 29)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Date(Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	Date d2(d1);
	return 0;
}

如果拷贝构造函数变成这样:

cpp 复制代码
	Date(Date d)

我们用图来分析:

所以参数必须是类类型对象的引用 ,后面的栈类的也一样。前面有Func函数的例子只不过多调用了一次Func函数,其他不变。

画图演示一下:

栈类:

cpp 复制代码
	//拷贝构造函数------深拷贝
	Stack(Stack& st)
	{
		_a = (int*)malloc(sizeof(int) * st._capacity);
		if (_a == nullptr)
		{
			perror("malloc fail");
			exit(-1);
		}
		memcpy(_a, st._a, sizeof(int) * st._capacity);
		_top = st._top;
		_capacity = st._capacity;
	}

深拷贝不仅是拷贝值,还开辟了另一块空间,这样st和st1的_a的地址就不一样了,说明它们不是指向同一块空间。

验证一下:

运行结果:

3.4 默认拷贝构造函数

若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。

1️⃣日期类:没写拷贝构造(可写可不写)

cpp 复制代码
class Date
{
public:
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year = 2022;
	int _month = 10;
	int _day = 17;
};
int main()
{
	Date d1;
	Date d2(d1);
	d2.Print();
	return 0;
}

注意:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。

2️⃣栈类:必须写拷贝构造

cpp 复制代码
class Stack
{
public:
	//构造函数
	Stack(int capacity = 3)
	{
		_a = (int*)malloc(sizeof(int) * capacity);
		if (_a == nullptr)
		{
			perror("malloc fail");
			exit(-1);
		}
		_top = 0;
		_capacity = capacity;
	}
	//析构函数
	~Stack()
	{
		free(_a);
		_a = nullptr;
		_top = 0;
		_capacity = 0;
	}
	//拷贝构造函数
	Stack(Stack& st)
	{
		_a = (int*)malloc(sizeof(int) * st._capacity);
		if (_a == nullptr)
		{
			perror("malloc fail");
			exit(-1);
		}
		memcpy(_a, st._a, sizeof(int) * st._capacity);
		_top = st._top;
		_capacity = st._capacity;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};
int main()
{
	Stack st1;
	Stack st2(st1);
	return 0;
}

对于内置类型,会进行值传递;对于的自定义类型,会调用它的拷贝构造。

3️⃣队列类:

cpp 复制代码
class Stack
{
public:
	//构造函数
	Stack(int capacity = 3)
	{
		_a = (int*)malloc(sizeof(int) * capacity);
		if (_a == nullptr)
		{
			perror("malloc fail");
			exit(-1);
		}
		_top = 0;
		_capacity = capacity;
	}
	//析构函数
	~Stack()
	{
		free(_a);
		_a = nullptr;
		_top = 0;
		_capacity = 0;
	}
	//拷贝构造函数
	Stack(Stack& st)
	{
		_a = (int*)malloc(sizeof(int) * st._capacity);
		if (_a == nullptr)
		{
			perror("malloc fail");
			exit(-1);
		}
		memcpy(_a, st._a, sizeof(int) * st._capacity);
		_top = st._top;
		_capacity = st._capacity;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};
class Queue
{
	Stack st1;
	Stack st2;
	int size = 6;
};
int main()
{
	Queue q1;
	Queue q2(q1);
	return 0;
}

补充:

1.拷贝构造函数的参数的类型前面一般都要加上const,防止被修改(如果值有变化就不加)。

2.在一个函数内返回一个对象,这个对象是局部变量,出作用域就销毁了,所以返回的是它的拷贝,也会自动调用拷贝构造函数。

3.大多数情况都是可以传值(拷贝)的,但是使用引用效率更高,所以能使用引用就使用引用。

四、赋值运算符重载

4.1 运算符重载

常见的运算符有:

!= 、>、<、>=、<=、== ...

我们可以对两个数进行比较:

cpp 复制代码
int main()
{
	int x = 1;
	int y = 2;
	int z = x > y;
	cout << z << endl;// 0  
	return 0;
}

如果要两个对象进行比较:

说明两个对象不能直接进行比较,因为对象是自定义类型,这个类型里面有多个成员变量,不确定谁大谁小,所以不能直接比。

这种情况要用函数来区别对象内的成员的大小,才能具体分出哪个对象大哪个对象小。

cpp 复制代码
class Date
{
public:
	Date(int year = 2023, int month = 10, int day = 30)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
//判断相等
bool Equal(const Date& x, const Date& y)
{
	//...
}
//判断是否大于
bool Greater(const Date& x, const Date& y)
{
	//...
}
int main()
{
	Date d1;
	Date d2(2022, 6, 6);
	d1.Print();
	d2.Print();

	//判断相等
	bool ret = Equal(d1, d2);
	//判断是否大于
	bool ret1 = Greater(d1, d2);
	cout << ret << endl;
	cout << ret1 << endl;
	return 0;
}

先来实现判断相等的函数:

这个比较简单,无非就是两个对象的年与年相等、月与月相等、天与天相等就好了,满足返回true,有一个条件不满足返回false。

cpp 复制代码
bool Equal(const Date& x, const Date& y)
{
	return x._year == y._year
		&& x._month == y._month
		&& x._day == y._day;
}

实现判断是否大于的函数:

用if,else 语句,先判断年,年大返回true,否则不进这个判断;年相等比较月,月大返回true,否则不进这个判断;年和月相等比较天,天大返回true,否则不进这个判断。三个判断没有一个满足,返回false。

cpp 复制代码
bool Greater(const Date& x, const Date& y)
{
	if (x._year > y._year)
	{
		return true;
	}
	else if (x._year == y._year && x._month > y._month)
	{
		return true;
	}
	else if (x._year == y._year 
		&& x._month == y._month 
		&& x._day > y._day)
	{
		return true;
	}
	else
	{
		return false;
	}
}

这里我们使用的函数名(Equal、Greater)能够一眼看出这个函数是干什么的,但是不是所有人都规范使用函数名,有的人用的函数名(compare1、compare2),怎么区分哪个是判断相等、哪个是判断是否大于呢?更严重的是使用中文拼音来作函数名的,非常不规范。

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

函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)

cpp 复制代码
//判断相等
bool operator==(const Date& x, const Date& y)
{
	return x._year == y._year
		&& x._month == y._month
		&& x._day == y._day;
}
//判断是否大于
bool operator>(const Date& x, const Date& y)
{
	if (x._year > y._year)
	{
		return true;
	}
	else if (x._year == y._year && x._month > y._month)
	{
		return true;
	}
	else if (x._year == y._year 
		&& x._month == y._month 
		&& x._day > y._day)
	{
		return true;
	}
	else
	{
		return false;
	}
}
/
	//判断相等
	bool ret = operator==(d1, d2);
	//判断是否大于
	bool ret1 = operator>(d1, d2);

运行结果:

这样写函数名就比以前好区分了,而且更规范。

其实还可以修改,让代码看起来更简洁:

cpp 复制代码
	//判断相等
	bool ret = d1 == d2;
	//判断是否大于
	bool ret1 = d1 > d2;

或者是:

cpp 复制代码
	//判断相等
	cout << (d1 == d2) << endl;
	//判断是否大于
	cout << (d1 > d2) << endl;

前面的的代码是有错误的:

1️⃣类成员变量是私有的,类外不能访问,所以把函数写在类里。

但是这时编译器有报错了:

2️⃣作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this

正确代码:

cpp 复制代码
class Date
{
public:
	Date(int year = 2023, int month = 10, int day = 30)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
	//判断相等
	bool operator==(const Date& y)
	{
		return _year == y._year
			&& _month == y._month
			&& _day == y._day;
	}
	//判断是否大于
	bool operator>(const Date& y)
	{
		if (_year > y._year)
		{
			return true;
		}
		else if (_year == y._year && _month > y._month)
		{
			return true;
		}
		else if (_year == y._year
			&& _month == y._month
			&& _day > y._day)
		{
			return true;
		}
		else
		{
			return false;
		}
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	Date d2(2022, 6, 6);
	d1.Print();
	d2.Print();
	//判断相等
	bool ret = d1 == d2;
	//判断是否大于
	bool ret1 = d1 > d2;
	cout << ret << endl;
	cout << ret1 << endl;
	return 0;
}

运行结果:

4.2 赋值运算符重载

4.2.1 赋值运算符重载概念

有了前面的基础,我们现在能够知道运算符重载是具有特殊函数名的函数。前面学过大于运算符重载、相等运算符重载,那么,接下来学习的赋值运算符重载也有些类似,但又有所不同。

赋值运算符重载与前面的构造、析构和拷贝构造一样,是特殊的成员函数,自动调用。它可以实现两个原本已经存在的对象之间进行赋值操作。如果没有写,编译器会生成一个默认赋值运算符重载。

4.2.2 赋值运算符重载写法及使用

赋值运算符重载格式:

1.参数类型:const T&,传递引用可以提高传参效率

2.返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值

3.检测是否自己给自己赋值

4.返回*this :要复合连续赋值的含义

cpp 复制代码
   //  赋值运算符重载
	Date& operator=(const Date& d)
	{              
		if (this != &d)
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
		return *this;
	}

实现两个对象之间的赋值:

cpp 复制代码
class Date
{
public:
	Date(int year = 2023, int month = 10, int day = 30)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Date& operator=(const Date& d)
	{
		if (this != &d)
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
		return *this;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	Date d2(2022, 9, 1);
	d1.Print();
	d2.Print();
	printf("\n");
	d1 = d2;// d2赋给d1
	d1.Print();
	d2.Print();
	return 0;
}

运行结果:

赋值运算符只能重载成类的成员函数不能重载成全局函数

原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。

4.2.3 默认赋值运算符重载

1️⃣用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。

将赋值运算符重载注释掉:

cpp 复制代码
	/*Date& operator=(const Date& d)
	{
		if (this != &d)
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
		return *this;
	}*/

运行结果:

2️⃣内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。

cpp 复制代码
class Date
{
public:
	Date(int year = 2023, int month = 10, int day = 30)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Date& operator=(const Date& d)
	{
		cout << "Date& operator=(const Date& d)" << endl;
		if (this != &d)
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
		return *this;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
class Time
{
	Date d1;
	int size = 10;
};
int main()
{
	Time t1;
	Time t2;
	t2 = t1;
	return 0;
}

t2与t1是一样的,但是我们这里主要看是否有调用对应类的赋值运算符重载完成赋值。

运行结果:

补充:如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现。

4.3 日期类的实现

4.3.1 创建类和函数声明

分为3个文件写,将声明、定义和测试区分。在头文件创建一个Date类,私有区域定义成员变量年、月、日;公有区域声明成员函数。

cpp 复制代码
class Date
{
public:
	//打印
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
	//构造
	Date(int year = 2023, int month = 10, int day = 28);
	//析构
	//~Date();
	//获取某年某月的天数
	int Getmonthday(int year, int day);
	//拷贝构造
	//Date(Date& d);
	// 赋值运算符重载
	Date operator=(const Date& d);

	// 日期+=天数
	Date& operator+=(int day);
	// 日期+天数
	Date operator+(int day);
	// 日期-=天数
	Date& operator-=(int day);
	// 日期-天数
	Date operator-(int day);

	//前置++
	Date& operator++();
	//后置++
	Date operator++(int);
	//前置--
	Date& operator--();
	//后置--
	Date operator--(int);

	// >运算符重载
	bool operator>(const Date& d);
	// ==运算符重载
	bool operator==(const Date& d);
	// >=运算符重载
	bool operator>=(const Date& d);
	// <运算符重载
	bool operator<(const Date& d);
	// <=运算符重载
	bool operator<=(const Date& d);
	// !=运算符重载
	bool operator!=(const Date& d);

	// 日期-日期 返回天数
	int operator-(const Date& d);
private:
	int _year;
	int _month;
	int _day;
};

4.3.2 全缺省的构造函数

声明和定义分离,定义的函数名前要加类名和作用域限定符(后面的也一样),表示成员函数是这个类的。构造函数采用全缺省,要注意声明给缺省值,定义不能给,否则容易出错(两边给的缺省值一样还好说,不一样就问题大了,编译器不知道用哪边的)。

cpp 复制代码
Date::Date(int year, int month, int day)
{
	if (year <= 0 || month >= 13 || month <= 0 || day <= 0
		|| day >= Getmonthday(year, month))
	{
		cout << "日期非法" << endl;
	}
	_year = year;
	_month = month;
	_day = day;
}

注意:年月日都是从1开始的

先加个条件判断日期是否合法,合法进行初始化,否则打印出日期非法,非法仍然可以打印出日期,打印出日期非法的作用是提醒编程者给的缺省值的不合理。

判断的括号里有Getmonthday函数,接下来分析

4.3.3 获取某年某月的天数

某年某月的天数是不固定的,所以要写个函数来获取该年该月的天数,方便后面的函数使用。

首先定义一个数组,数组内容是从1月到12月的天数,为了方便操作,数组的第一个元素给0,总之什么也不是,这样1月就从下标1开始(数组下标从0开始的)。定义一个变量叫day,等于数组下标,下标就是月份。然后判断平年、闰年,是闰年day+1,因为前面是数组的二月天数初始化为28;不是闰年就不进这个判断。返回day,获取某年某月的天数。

cpp 复制代码
int Date::Getmonthday(int year, int month)
{
	static int arr[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
	int day = arr[month];
	if (month == 2 && ((year % 100 != 0 && year % 4 == 0) || year % 400 == 0))
	{
		day += 1;
	}
	return day;
}

4.3.4 析构、拷贝构造和赋值运算符重载

因为日期类的成员变量都是内置类型,所以析构、拷贝构造和赋值运算符重载我们可以不写,让编译器默认生成。

cpp 复制代码
//析构
//Date::~Date()
//{
//	_year = 0;
//	_month = 0;
//	_day = 0;
//}

//拷贝构造
//Date::Date(const Date& d)
//{
//	_year = d._year;
//	_month = d._month;
//	_day = d._day;
//}

// 赋值运算符重载
//Date Date::operator=(const Date& d)
//{
//	if (this != &d)
//	{
//		_year = d._year;
//		_month = d._month;
//		_day = d._day;
//	}
//	return *this;
//}

4.3.5 日期+=天数

该年该月的日期加上天数,如果比该年该月的天数大,进入循环,先减去该年该月的天数,月再加1,里面补充个判断,如果月等于13,年加1,月更新到1月;天数减到在该年该月的范围内,跳出循环,返回日期。

cpp 复制代码
Date& Date::operator+=(int day)
{
	_day += day;
	while (_day > Getmonthday(_year, _month))
	{
		_day -= Getmonthday(_year, _month);//
		_month++;//
		if (_month == 13)
		{
			_year++;
			_month = 1;
		}
	}
	return *this;
}

4.3.6 日期-=天数

该年该月的日期减去天数,如果天数小于等于0,进入循环。先月减1,再加上该年该月的天数,在两者之间加判断,如果月等于0,年减1,月更新到12月;天数加到在该年该月的范围内,跳出循环,返回日期。

cpp 复制代码
Date& Date::operator-=(int day)
{
	_day -= day;
	while (_day <= 0)
	{
		_month--;
		if (_month == 0)
		{
			_year--;
			_month = 12;
		}
		_day += Getmonthday(_year, _month);//
	}
	return *this;
}

4.3.7 日期+天数和日期-天数

复用前面的函数即可

cpp 复制代码
Date Date::operator+(int day)
{
	Date tmp(*this);//自动调用拷贝构造
	tmp += day;//复用 日期+=天数
	return tmp;//返回的是tmp的拷贝
}
Date Date::operator-(int day)
{
	Date tmp(*this);//自动调用拷贝构造
	tmp -= day;//复用 日期-=天数
	return tmp;//返回的是tmp的拷贝
}

4.3.8 前置++和后置++

前置++是先加1,后使用。虽然++在操作数的前面,但是在运算符重载的规则里它的++还是在operator关键字的后面。

cpp 复制代码
Date& Date::operator++()
{
	*this = *this + 1;
	return *this;
}

后置++是先使用后加1,返回的值是还没加1的值。

cpp 复制代码
Date Date::operator++(int)
{
	Date tmp(*this);
	*this = *this + 1;
	return tmp;
}

4.3.9 前置--和后置--

与前面同:

cpp 复制代码
//前置--
Date& Date::operator--()
{
	*this = *this - 1;
	return *this;
}
//后置--
Date Date::operator--(int)
{
	Date tmp(*this);
	*this = *this - 1;
	return tmp;
}

4.3.10 !=、>=、<、<=运算符重载

前面学习过>和= = 的运算符重载,接下来的就是> 和 ==的运算符重载的复用,使用逻辑反操作运和逻辑与两个运算符即可快速解决。

cpp 复制代码
// >=运算符重载
bool Date::operator>=(const Date& d)
{
	return *this > d && *this == d;
}

// <运算符重载
bool Date::operator<(const Date& d)
{
	return !(*this > d && *this == d);
}

// <=运算符重载
bool Date::operator<=(const Date& d)
{
	return !(*this > d);
}

// !=运算符重载
bool Date::operator!=(const Date& d)
{
	return !(*this == d);
}

4.3.11 日期-日期 返回天数

刚开始不确定哪个日期比较大,先假设一个日期是大的,另一个是小的(如果判断错误就互换数据)。用一个变量统计天数,设置一个循环,小的日期不等于大的日期,计数++,直到相等跳出循环,返回天数。

cpp 复制代码
int Date::operator-(const Date& d)
{
	Date max = *this;
	Date min = d;
	int flag = 1;
	if (*this < d)
	{
		max = d;
		min = *this;
		flag = -1;
	}
	int count = 0;
	while (max != min)
	{
		min++;//复用前面的函数
		count++;
	}
	return count * flag;
}

4.4 日期类的全部代码

4.4.1 Date.h

cpp 复制代码
#include <iostream>
using namespace std;
class Date
{
public:
	//打印
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
	//构造
	Date(int year = 2023, int month = 10, int day = 30);
	//析构
	//~Date();
	//获取某年某月的天数
	int Getmonthday(int year, int day);
	//拷贝构造
	//Date(Date& d);
	// 赋值运算符重载
	//Date operator=(const Date& d);

	// 日期+=天数
	Date& operator+=(int day);
	// 日期+天数
	Date operator+(int day);
	// 日期-=天数
	Date& operator-=(int day);
	// 日期-天数
	Date operator-(int day);

	//前置++
	Date& operator++();
	//后置++
	Date operator++(int);
	//前置--
	Date& operator--();
	//后置--
	Date operator--(int);

	// >运算符重载
	bool operator>(const Date& d);
	// ==运算符重载
	bool operator==(const Date& d);
	// >=运算符重载
	bool operator>=(const Date& d);
	// <运算符重载
	bool operator<(const Date& d);
	// <=运算符重载
	bool operator<=(const Date& d);
	// !=运算符重载
	bool operator!=(const Date& d);

	// 日期-日期 返回天数
	int operator-(const Date& d);
private:
	int _year;
	int _month;
	int _day;
};

4.4.2 Date.cpp

cpp 复制代码
#include "Date.h"

// 获取某年某月的天数
int Date::Getmonthday(int year, int month)
{
	static int arr[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
	int day = arr[month];
	if (month == 2 && ((year % 100 != 0 && year % 4 == 0) || year % 400 == 0))
	{
		day += 1;
	}
	return day;
}

//构造
Date::Date(int year, int month, int day)
{
	if (year <= 0 || month >= 13 || month <= 0 || day <= 0
		|| day >= Getmonthday(year, month))
	{
		cout << "日期非法" << endl;
	}
	_year = year;
	_month = month;
	_day = day;
}

//析构
//Date::~Date()
//{
//	_year = 0;
//	_month = 0;
//	_day = 0;
//}

//拷贝构造
//Date::Date(const Date& d)
//{
//	_year = d._year;
//	_month = d._month;
//	_day = d._day;
//}

// 赋值运算符重载
//Date Date::operator=(const Date& d)
//{
//	if (this != &d)
//	{
//		_year = d._year;
//		_month = d._month;
//		_day = d._day;
//	}
//	return *this;
//}

// 日期+=天数
Date& Date::operator+=(int day)
{
	_day += day;
	while (_day > Getmonthday(_year, _month))
	{
		_day -= Getmonthday(_year, _month);//
		_month++;//
		if (_month == 13)
		{
			_year++;
			_month = 1;
		}
	}
	return *this;
}

// 日期+天数
Date Date::operator+(int day)
{
	Date tmp(*this);//自动调用拷贝构造
	tmp += day;//复用 日期+=天数
	return tmp;//返回的是tmp的拷贝
}

// 日期-=天数
Date& Date::operator-=(int day)
{
	_day -= day;
	while (_day <= 0)
	{
		_month--;
		if (_month == 0)
		{
			_year--;
			_month = 12;
		}
		_day += Getmonthday(_year, _month);//
	}
	return *this;
}

// 日期-天数
Date Date::operator-(int day)
{
	Date tmp(*this);//自动调用拷贝构造
	tmp -= day;//复用 日期-=天数
	return tmp;//返回的是tmp的拷贝
}

//前置++
Date& Date::operator++()
{
	*this = *this + 1;
	return *this;
}

//后置++
Date Date::operator++(int)
{
	Date tmp(*this);
	*this = *this + 1;
	return tmp;
}

//前置--
Date& Date::operator--()
{
	*this = *this - 1;
	return *this;
}

//后置--
Date Date::operator--(int)
{
	Date tmp(*this);
	*this = *this - 1;
	return tmp;
}

// >运算符重载
bool Date::operator>(const Date& 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;
}

// ==运算符重载
bool Date::operator==(const Date& d)
{
	return _year == d._year && _month == d._month && _day == d._day;
}

// >=运算符重载
bool Date::operator>=(const Date& d)
{
	return *this > d && *this == d;
}

// <运算符重载
bool Date::operator<(const Date& d)
{
	return !(*this > d && *this == d);
}

// <=运算符重载
bool Date::operator<=(const Date& d)
{
	return !(*this > d);
}

// !=运算符重载
bool Date::operator!=(const Date& d)
{
	return !(*this == d);
}

// 日期-日期 返回天数
int Date::operator-(const Date& d)
{
	Date max = *this;
	Date min = d;
	int flag = 1;
	if (*this < d)
	{
		max = d;
		min = *this;
		flag = -1;
	}
	int count = 0;
	while (max != min)
	{
		min++;//复用前面的函数
		count++;
	}
	return count * flag;
}

4.4.3 test.cpp

cpp 复制代码
#include "Date.h"
int main()
{
	Date d1;
	d1.Print();
	
	Date d2(d1);
	d2.Print();

	Date d3(2020, 2, 3);
	d1 = d3;
	d1.Print();

	d1 += 100;
	d1.Print();

	Date ret = d2 + 100;
	d2.Print();
	ret.Print();

	d2 -= 100;
	d2.Print();

	Date ret1 = d3 - 100;
	d3.Print();
	ret1.Print();

	d3 = d3 + 1;
	d3.Print();

	Date ret2 = d3 + 1;
	ret2.Print();

	d2 = d2 - 1;
	d2.Print();
	printf("\n");
	d1.Print();
	d2.Print();
	bool ret3 = d1 > d2;
	cout << ret3 << endl;
	bool ret4 = d1 == d2;
	cout << ret4 << endl;
	bool ret5 = d1 >= d2;
	cout << ret5 << endl;
	bool ret6 = d1 < d2;
	cout << ret6 << endl;
	bool ret7 = d1 <= d2;
	cout << ret7 << endl;
	bool ret8 = d1 != d2;
	cout << ret8 << endl;
	
	int count = d1 - d2;
	cout << count << endl;
	return 0;
}

运行结果:

五、const成员函数

5.1 const成员函数概念

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

5.2 const成员函数写法

先来看代码:

cpp 复制代码
    // const成员函数
	bool operator==(const Date& d) const
	{
		return _year == d._year
			&& _month == d._month
			&& _day == d._day;
	}

就是在函数参数的后面加个const,是不是看起来有点奇怪,但是这个是语法规定的,只能这样写。

5.3 关于const的权限问题

主要有以下几个问题:

1.const对象可以调用非const成员函数吗?

2.非const对象可以调用const成员函数吗?

3.const成员函数内可以调用其它的非const成员函数吗?

4.非const成员函数内可以调用其它的const成员函数吗?

先来一段代码:

cpp 复制代码
class Date
{
public:
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	/*void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}*/
	
	void Func() const
	{
		//Print();
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2023, 11, 3);
	
	d1.Func();
	return 0;
}

这个代码没问题

我们来对比下对象有const和没const的情况分别调用的函数是不是const成员函数:

成员函数调用其他成员函数的对比:

补充:

通常情况下,如果该函数里面没有修改成员变量,都可以是const成员函数;反之就不能是const成员函数。

如果是全局函数,不能使用const修饰,因为它不是类里面的成员函数,所以没有this指针(const修饰的是this指针)

六、取地址及const取地址操作符重载

最后两个默认成员函数比较简单,一般不用自己定义 ,编译器默认会生成。

cpp 复制代码
    // 可写可不写
	Date* operator&()
	{
		return this;
	}
	// 可写可不写
	const Date* operator&()const
	{
		return this;
	}
	
	cout << &d1 << endl;

运行结果:

一般情况不需要写,极少情况下要让别人获取到指定的内容才需要自己写。

相关推荐
北极无雪16 分钟前
Spring源码学习(拓展篇):SpringMVC中的异常处理
java·开发语言·数据库·学习·spring·servlet
猿小猴子28 分钟前
Python3 爬虫 中间人爬虫
开发语言·爬虫·python
lly20240635 分钟前
Swift 方法
开发语言
T0uken37 分钟前
【QT Quick】C++交互:QML对象操作
c++·qt·交互
YONG823_API1 小时前
电商平台数据批量获取自动抓取的实现方法分享(API)
java·大数据·开发语言·数据库·爬虫·网络爬虫
寂柒1 小时前
C++——模拟实现stack和queue
开发语言·c++·算法·list
熬夜学编程的小王1 小时前
C++类与对象深度解析(一):从抽象到实践的全面入门指南
c++·git·算法
CV工程师小林1 小时前
【算法】DFS 系列之 穷举/暴搜/深搜/回溯/剪枝(下篇)
数据结构·c++·算法·leetcode·深度优先·剪枝
蜜桃小阿雯2 小时前
JAVA开源项目 旅游管理系统 计算机毕业设计
java·开发语言·jvm·spring cloud·开源·intellij-idea·旅游
Benaso2 小时前
Rust 快速入门(一)
开发语言·后端·rust