【C++】类和对象(二)

一、类的默认成员函数

如果一个类中什么成员都没有,简称为空类。空类中什么都没有吗?并不是的,任何一个类在我们不写的情况下,编译器会自动生成下面6个默认成员函数。(C++11中有8个,最重要的还是前4个)

class A
{};
image-20220127170427527

二、构造函数

2.1 概念

思考下:

对于 Date 类,可以通过 SetDate 公有的方法设置对象中的数据值,但是如果每次创建对象都调用 SetDate 方法设置对象中的数据值,未免有点麻烦。

还有一个问题是,有时候因为疏忽忘记调用 SetDate 方法去设置对象中的数据值,从而在后面使用其它接口访问对象中的数据时,可能导致程序崩溃。

那能否在对象创建时,就将对象中数据的初始值设置好呢?

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

class Date {
public:
	void SetDate(int year, int month, int day) {
		_year = year;
		_month = month;
		_day = day;
	}
    
	void Display() {
		cout << _year << "-" << _month << "-" << _day << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1;
	d1.SetDate(2021, 10, 9); // 每次创建对象,都要调用该方法去设置对象中成员变量的初始值
	d1.Display();

	Date d2;
	d2.SetDate(2022, 10, 9); // 每次创建对象,都要调用该方法去设置对象中成员变量的初始值
	d2.Display();

	return 0;
}

2.2 特性

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

其特征如下:

  1. 构造函数名与类名相同。

  2. 无返回值。

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

  4. 构造函数可以「重载」。

    👉示例

    class Date {
    public:
    	// 1. 无参构造函数(默认构造函数)
    	Date() {
    		_year = 0;
    		_month = 0;
    		_day = 0;
    	}
    
    	// 2. 带参构造函数
    	Date(int year, int month, int day) {
    		_year = year;
    		_month = month;
    		_day = day;
    	}
    
    private:
    	int _year;
    	int _month;
    	int _day;
    };
    
    int main() {
    	Date d1;              // 调用无参构造函数(默认构造函数)
    	Date d2(2021, 10, 9); // 调用带参的构造函数
    
    	// 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
    	// 以下代码的函数:声明了d3函数,该函数无参,返回一个日期类型的对象
    	// Date d3(); // error
    	return 0;
    }
    
  5. 如果类中没有显式定义构造函数,则 C++ 编译器会自动生成一个无参的默认构造函数,一旦用户显式定义,编译器将不再生成。

    👉示例1:编译器自动生成的默认构造函数

    class Date {
    private:
    	int _year;
    	int _month;
    	int _day;
    };
    
    int main()
    {
    	// Date类没有显式定义构造函数,对象也可以创建成功,此处调用的是编译器生成的默认构造函数
    	Date d;
    }
    

    👉示例2:这段程序会编译报错,为什么呢?

    因为用户显式定义了构造函数,编译器将不再生成默认构造函数了。

    当实例化对象 d 时,会自动调用默认构造函数,但这里只有带参构造函数,没有默认构造函数。

    class Date {
    public:
        // 用户显式定义了构造函数,编译器将不再生成默认构造函数
    	Date (int year, int month, int day) {
            _year = year;
            _month = month;
            _day = day;
    	}
    
    private:
    	int _year;
    	int _month;
    	int _day;
    };
    
    int main() {
    	Date d; // error:没有合适的默认构造函数可用
    }
    
  6. 默认构造函数:不传参数就可以调用的构造函数。

    比如:「无参构造函数」和「全缺省构造函数」,并且默认构造函数只能有一个

    注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认成员函数。

    👉示例

    class A {
    public:
    	// 这里本来是构成函数重载的,但是它们俩不能同时存在,为什么呢?
    
    	A() { 
    		_a = 10;
    	}
    
    	A(const int a = 20) {
    		_a = a;
    	}
    
    private:
    	int _a;
    };
    
    int main() {
    	A a; // error: 包含多个默认构造函数,有歧义,对重载函数的调用不明确
    	return 0;
    }
    
  7. 关于编译器生成的默认成员函数,大家可能会有一个疑惑:用户没有显式定义构造函数的情况下,编译器会自动生成一个默认构造函数。在对象实例化时,调用编译器生成的默认构造函数,对象中的成员变量却被初始化成「随机值」了。这样看来,编译器生成的默认构造函数并没有什么用?

    重点

    解答:C++ 把类型分成「内置类型」(基本类型) 和「自定义类型」。

    • 内置类型:语法已经定义好的类型:如 int / char / int* / char* …
    • 自定义类型:我们使用 class / struct / union 自己定义的类型

    总结:我们不显式定义构造函数,编译器自动生成的默认构造函数做了一个 “ 偏心 ” 的处理,内置类型成员不处理(为随机值),自定义类型成员调用它的默认构造函数去初始化。

    👉示例

    class A {
    public:
    	A() { _a = 0; }
    
    private:
    	int _a;
    };
    
    class B {
    public:
    	B() { _b = 0; }
    
    private:
    	int _b; // 基本类型(内置类型)
    	A a;    // 自定义类型
    };
    
    int main()
    {
    	B b;
    	return 0;
    }
    

    重点

    构造函数在初始化列表中,它会做这两件事:

    1. 对于类中的内置类型成员 ------> 不处理(为随机值,除非声明时给了缺省值 - C++11)
    2. 对于类中的自定义类型成员 —> 自动调用它的默认构造函数(不要参数就可以调用的,比如 无参构造函数全缺省构造函数

2.3 补充:成员变量的命名风格

成员变量的命名风格:

👉示例:我们看看这个函数,是不是很懵逼?

class Date {
public:
	Date(int year) {
		year = year; // 这里的year到底是成员变量,还是函数形参?

        // 编译器把这里处理成了:函数形参year自己给自己赋值,所以并没有初始化成员变量year
        // 那怎么解决呢?-- 指定一下 this->year = year; 或者 给成员变量加个标识
	}
private:
	int year;
};

int main() {
	Date d(1); // 对象d中的成员变量year还是随机值,并没有被初始化
	return 0;
}

正确写法:给成员变量加个标识 _year = year;

class Date {
public:
	Date(int year) {
		_year = year;
	}
private:
	int _year;
};

三、析构函数

3.1 概念

析构函数:与构造函数功能相反,析构函数不是完成「对象的销毁」(局部对象销毁工作是由编译器完成的),而是在对象在生命周期结束时(出作用域)会自动调用析构函数,完成对象中一些「资源的清理」工作(一般是动态开辟的内存)。


3.2 特性

析构函数是特殊的成员函数。其特征如下:

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

  2. 无参数,无返回值。

  3. 一个类「有且只有一个」析构函数。若未显式定义,编译器会自动生成一个析构函数。

  4. 对象生命周期结束时,C++ 编译系统会「自动调用」析构函数,完成对象中资源的清理工作。

    👉示例:析构函数的使用

    class SeqList {
    public:
        // 默认构造函数
    	SeqList(int capacity = 10) {
    		_pData = (int*)malloc(capacity * sizeof(int));
    		assert(_pData);
    		
    		_size = 0;
    		
    		_capacity = capacity;
    	}
    
        // 注意:这里必须要显式的写析构函数,否则会发生内存泄漏
        
        // 析构函数:完成对象中资源的清理工作
    	~SeqList() {
    		if (_pData) {
    			free(_pData);   // 释放堆上的空间
    			_pData = NULL;  // 将指针置为空
    			_capacity = 0;
    			_size = 0;
    		}
    	}
    
    private:
    	int* _pData;
    	size_t _size;
    	size_t _capacity;
    };
    
  5. 重要】如果没有显式定义,编译器自动生成的析构函数,它会做哪些事情呢?

    1. 对于类中的内置类型成员 ------> 不处理

    2. 对于类中的自定义类型成员 —> 调用它的析构函数完成清理工作


四、拷贝构造函数

4.1 概念

在创建对象时,能否创建出一个与某个同类对象一模一样的新对象呢?

构造函数:

  • 只有「单个形参」,该形参是「对本类类型对象的引用」(一般常用 const 修饰)
  • 在用「已存在的同类对象」来初始化「新创建的对象」时,由编译器自动调用。

拷贝构造函数也是构造函数。

注意:如果实现了拷贝构造函数,但没有显式的定义构造函数,编译器就不会再生成构造函数了。


4.2 特性

拷贝构造函数也是特殊的成员函数,其特征如下:

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

  2. 拷贝构造函数的「参数只有一个」且必须使用「引用传参」,且建议使用 const 常引用,保护形参不会被改变。(否则形参的改变将会影响到实参)

    注意:若使用传值方式会引发无穷递归调用。如图:

    image-20220417210026255

    👉示例:拷贝构造函数的正确写法

    class Date
    {
    public:
    	// 默认构造函数
    	Date (int year = 1, int month = 1, int day = 1)
    	{
    		_year = year;
    		_month = month;
    		_day = day;
    	}
    
    	// 拷贝构造函数
    	Date(const Date& d) // 单参数,必须是类类型对象的引用,否则引发无穷递归调用
    	{
    		_year = d._year;
    		_month = d._month;
    		_day = d._day;
    	}
    
    private:
    	int _year;
    	int _month;
    	int _day;
    };
    
    int main()
    {
        Date d1(2021, 10, 9);
        Date d2(d1); // 这里d2调用拷贝构造函数完成拷贝,d2和d1的值是一样的。
        // 这样写也可以:Date d2 = d1;
        return 0;
    }	
    
  3. 若用户未显示定义,编译器会自动生成一个拷贝构造函数。 按对象的「内存存储字节序」完成拷贝,这种拷贝我们叫做「浅拷贝」,或者值拷贝。

    重要

    如果没有显式定义,编译器自动生成的拷贝构造函数,它会做哪些事情呢?

    1. 对于类中的内置类型成员 ------> 值拷贝
    2. 对于类中的自定义类型成员 —> 自动调用它的拷贝构造函数来完成拷贝初始化

    👉示例:编译器自动生成的拷贝构造函数

    class Date {
    public:
    	// 默认构造函数
    	Date (int year = 1, int month = 1, int day = 1) {
    		_year = year;
    		_month = month;
    		_day = day;
    	}
    
    private:
    	int _year;
    	int _month;
    	int _day;
    };
    
    int main() {
        Date d1(2021, 10, 9);
        
        // 这里d2调用编译器自动生成的拷贝构造函数,完成【浅拷贝】,d2和d1的值也是一样的。
        Date d2(d1);
        return 0;
    }	
    
  4. 思考:编译器自动生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,我们还需要自己实现吗?

    解答:像日期类这样的类是没必要的。但有些需要「深拷贝」的类,其内部往往是很复杂的,是需要用户显式定义拷贝构造函数来完成「深拷贝」的。

    👉示例:需要深拷贝的类

    class MyString {
    public:
    	// 默认构造函数
    	MyString(const char* str = "winter") {
    		_str = (char*)malloc(strlen(str) + 1);
    		strcpy(_str, str);
    	}
        
    	// 析构函数
    	~MyString() {
    		cout << "~String()" << endl;
    		free(_str);
    	}
        
        // 拷贝构造函数
    	MyString(const MyString& s) {
    		
            // 给新对象申请一段和原对象一样大小的空间
    		_str = (char*)malloc(strlen(s._str) + 1);
    		
            // 把原对象的数据一一拷贝给新对象
    		strcpy(_str, s._str);
    	}
    
    private:
    	char* _str;
    };
    
    int main() {
    	MyString s1("hello");
    	MyString s2(s1);
    }
    

    这里必须是深拷贝,编译器默认生成的拷贝构造函数是浅拷贝,会导致两个 MyString 对象中的字符指针 _str 指向的是同一个字符数组。(因为浅拷贝只拷贝了 _str 数组指针的 4 个字节的内容)

    image-20220417220249003

    如图:指向了同一块空间

    image-20220417221717913

    那么会引发什么问题呢?会导致 _str 指向的空间被释放两次,引发程序崩溃。

    image-20220417221809176

    所以在上述类中必须要显式定义拷贝构造函数,否则编译器默认生成的拷贝构造函数无法正常完成拷贝。


五、运算符重载

5.1 概念

C++为了让「自定义类型」可以像「内置类型」一样去使用某个运算符,并增强代码的可读性,引入了运算符重载,运算符重载是具有特殊函数名的函数,其返回值类型与参数列表与普通的函数类似。

  • 函数名:关键字 operator 后面接需要重载的运算符符号。比如 operator= / operator++

  • 一个类要重载哪些运算符,是看你需要哪些运算符,以及重载了该运算符有没有意义。

  • 运算符重载和函数重载,都有重载这个词,但是它们之间没有关联。

注意

  • 不能通过连接其他符号来创建新的操作符:比如:operator@
  • 重载操作符必须有一个「类类型」或者「枚举类型」的操作数
  • 用于内置类型的操作符,其含义不能改变,例如:内置的整型相加 +,不能改变其含义
  • .*::sizeof?:. 注意以上 5 个运算符不能重载。(这个经常在笔试选择题中出现)
  • 运算符的操作数有几个,就有几个参数,比如双目运算符有两个操作数;三目运算符有三个操作数。

运算符重载,一般有两种方式

  • 重载成类的成员函数(形参数目看起来比该运算符需要的参数少一个,因为成员函数有隐含的「this指针」,且函数的「第一个」形参就是「this指针」)
  • 重载成类的友元函数(必须有一个参数要是类的对象)(一般不这样做,而是重载成成员函数)

1、运算符重载成成员函数

👉示例

运算符默认都是给「内置类型」变量用的,如果「自定义类型」的变量想要用这些运算符,必须要自己对这些运算符进行重载。

即根据该自定义类型的特点,写一个函数去重新定义这个运算符的行为,就能够像内置类型一样去用这些运算符了。

比如:

  • 判断自定义类型 Date 类对象 d1 和 d2 是否相等
  • 判断自定义类型 Date 类对象 d1 是否小于 d2
class Date {
public:
	// 默认构造函数
	Date(int year = 1, int month = 1, int day = 1) {
		_year = year;
		_month = month;
		_day = day;
	}
	
    // 这里需要注意,左操作数是:this指针指向的调用本函数的对象
    
    // d1 == d2
    // 编译器会解释为:bool operator==(Date* const this, const Date& d)
	bool operator==(const Date& d) {
		return _year == d._year
			&& _month == d._month
			&& _day == d._day;
	}

    // d1 < d2
    // 编译器会解释为:bool operator<(Date* const this, const Date& d)
    bool 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;
	}
    
private:
	int _year;
	int _month;
	int _day;
};

int main() {
	Date d1(2021, 10, 9);
	Date d2(2022, 10, 9);
	
    // 判断自定义类型 Date 类对象 d1 和 d2 是否相等
	cout << (d1 == d2) << endl;
    // 编译器会解释为:d1.operator==(d2); --> d1.operator==(&d1, d2);
	
    // 判断自定义类型 Date 类对象 d1 是否小于 d2
	cout << (d1 < d2) << endl;
    // 编译器会解释为:d1.operator<(d2); --> d1.operator<(&d1, d2);
    
	return 0;
}

5.2 赋值运算符重载

思考:想要把某一个对象的值赋值给另外一个对象,该如何做呢?

赋值运算符:用于两个「已经定义出来的对象」间的拷贝赋值(要和拷贝构造区分开)

其特征如下:

  1. 参数类型

  2. 返回值类型:类对象的引用

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

  4. 返回 *this

  5. 一个类如果没有显式定义赋值运算符重载,编译器会自动生成一个,完成对象按「字节序」的「值拷贝

重要

如果没有显式定义,编译器自动生成的赋值运算符重载函数,它会做哪些事情呢?

  1. 对于类中的内置类型成员 ------> 值拷贝
  2. 对于类中的自定义类型成员 —> 调用它的赋值重载函数来完成赋值初始化

1、赋值运算符如何支持连续赋值

👉示例

观察下面这段代码:

class Date {
public:
	// 默认构造函数
	Date(int year = 1, int month = 1, int day = 1) {
		_year = year;
		_month = month;
		_day = day;
	}
	
    // 赋值运算符重载
	void operator=(const Date& d) {
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main() {
	Date d1(2021, 10, 9);
	Date d2(2022, 10, 9);
	d1 = d2; // 把d2赋值给d1
	return 0;
}

上述代码虽然可以运行通过,达到把 d2 赋值给 d1 的效果,但是,思考一下:

赋值运算符要支持连续赋值,该怎么办呢?

// 比如内置类型:
int i = 1, j = 2,k = 3;
i = j = k; // 连续赋值

// 注意,这个表达式是从右往左执行的:
// 先执行 j = k,把 k 赋值给 j,再执行 i = j,把 j = k 这个表达式的返回值 j 赋值给 i

所以我们写的赋值运算符重载函数必须要有返回值,才能支持连续赋值:

👇赋值运算符重载函数 - 正确写法

// 比如:d2 = d3
Date& Doperator=(const Date& d) {  // 引用传值,引用返回,为了减少拷贝构造
    // 防止自己给自己赋值
    if(this != &d) {
        _year = d._year;
    	_month = d._month;
    	_day = d._day;
    }
    return *this; // 返回d2(this指针指向d2,所以*this就是d2)
}

// ...

Date d1(2021, 10, 11);
Date d2(2022, 10, 11);
Date d3(2023, 10, 11);
d1 = d2 = d3; // 连续赋值
// 先执行 d2 = d3,即 d2.operator=(d3),函数会返回 d2 的引用
// 再执行 d1 = d2(其中d2是d2=d3的返回值)

2、深拷贝与浅拷贝

👉思考:编译器自动生成的默认赋值运算符重载函数已经可以完成字节序的值拷贝了,我们还需要自己实现吗?

解答:像日期类这样的类是没必要的。但有些需要「深拷贝」的类,其内部往往是很复杂的,是需要用户显式定义赋值运算符重载函数来完成「深拷贝」的。

class MyString {
public:
	// 默认构造函数
	MyString(const char* str = "winter") {
		_str = (char*)malloc(strlen(str) + 1);
		strcpy(_str, str);
	}
    
	// 析构函数
	~MyString() {
		cout << "~String()" << endl;
		free(_str);
	}

    // 必须要用户显示定义赋值运算符重载函数,编译器生成的是不能正常完成赋值操作的
    // s1 = s2
    MyString& operator=(MyString s)
    {
        swap(_str, s._str);
        return *this;
    }
    
private:
	char* _str;
};

int main() {
	MyString s1("hello");
	MyString s2("world");
    s1 = s2;
}

5.3 常见的运算符重载
应用:实现一个日期类

1、Date.h 文件:

#pragma once

#include<iostream>
#include<cassert>
using namespace std;

class Date
{
public:
	// 获取某年某月的天数
	int GetDays(int year, int month)
	{
		assert(month >= 1 && month <= 12);

		// 每月的天数(这个函数会被频繁调用,每次进来都要重新定义数组,所以将其定义为静态的)
		// 默认是平年
        static int days[] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
		
        // 如果是闰年的2月(能被4整除且不能被100整除,或者能被400z
        if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0)
        {
            if (month == 2)
            {
                return 29;
            }
        }
		return days[month];
	}

	// 默认构造函数
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;

		// 判断日期是否合法
		if (!(_year >= 0
			&& (_month >= 1 && _month <= 12)
			&& (_day >= 1 && _day <= GetDays(year, month))))
		{
			cout << _year << "/" << _month << "/" << _day << " -- ";
			cout << "非法日期" << endl;
		}
	}

	/* 拷贝构造、赋值运算符、析构函数用编译器自动生成的就可以了(因为Date类是浅拷贝) */
	
	// 日期 += 天数 --> d1 += 100
	Date& operator+=(int day);
    
    // 日期 + 天数 --> d1 + 100
	Date operator+(int day);

	// 日期 -= 天数 --> d1 -= 100
	Date& operator-=(int day);
    
    // 日期 - 天数 --> d1 - 100
	Date operator-(int day);

	// 日期 - 日期(返回相差天数) --> d1 - d2
	int operator-(const Date& d);

	// 注意:按正常的运算符重载规则,无法区分前置++和后置++
	// 为了区分,这里做了一个特殊处理,给后置++增加了一个int参数
	// 这个参数仅仅是为了区分,使前置++和后置++构成重载

	// 前置++
	Date& operator++(); // 编译器会解释为:Date& operator++(Date* const this);

	// 后置++
	Date operator++(int); // 编译器会解释为:Date& operator++(Date* const this, int);

	// 前置--
	Date& operator--();

	// 后置--
	Date operator--(int);
    
	// >运算符重载
	bool 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 operator==(const Date& d)
	{
		return _year == d._year && _month == d._month && _day == d._day;
	}

    /* 这里我们只需要把>和==运算符重载了,下面的运算符都可以复用其代码了 */
    
	// >=运算符重载
	bool operator>=(const Date& d)
	{
		return *this > d || *this == d;
	}

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

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

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

	// 打印日期
	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};

2、Date.cpp 文件:常用运算符重载函数的定义

① 日期 += 天数(返回累加天数后的日期),比如:d1 += 100

注意:d1本身要被更改,天数累加到d1上面去

Date& Date::operator+=(int day)
{
	if (day < 0) // 如果day是负数,就向前计算,相当于 -=
	{
		return *this -= -day; // 调用-=运算符重载函数
	}

	_day += day; // 累加天数

	while (_day > GetDays(_year, _month)) // 表示当前月的天数已经过完了
	{
		_day -= GetDays(_year, _month);   // 减去当前月的天数

		_month++; // 月进位

		if (_month == 13) // 判断当前月份是否合法
		{
			_year++;      // 年进位
			_month = 1;   // 更新为1月
		}
	}

	return *this;

	/* 写法二:复用 + 运算符重载函数的代码
	*this = *this + day; // d1等价于*this,对d1进行+天数操作,再赋值给d1
	return *this;        // 返回d1
	*/
}

② 日期 + 天数(返回累加天数后的日期),比如 :d1 + 100

注意:d1本身不能被更改,天数累加到一个临时对象上面去

// 写法一:
Date Date::operator+(int day)
{
	Date tmp(*this); // 拷贝构造一份临时对象,防止调用本函数的对象被更改

	tmp._day += day; // 累加天数
	while (tmp._day > GetDays(tmp._year, tmp._month)) // 表示当前月的天数已经过完了
	{
		tmp._day -= GetDays(tmp._year, tmp._month);   // 减去当前月的天数

		tmp._month++; // 月进位

		if (tmp._month == 13) // 判断当前月份是否合法
		{
			tmp._year++;      // 年进位
			tmp._month = 1;   // 更新为1月
		}
	}

	return tmp; // 返回临时对象
}

// 写法二:
Date Date::operator+(int day)
{
	/* 复用 += 运算符重载函数的代码 */

	Date tmp(*this); // 拷贝构造一份临时对象
	tmp += day;      // 对临时对象进行 += 天数操作
	return tmp;      // 返回临时对象
}

日期 -= 天数(返回累减天数后的日期),比如:d1 -= 100

Date& Date::operator-=(int day)
{
	if (day < 0) // 如果day小于0,就往后计算,相当于 +=
	{
		return *this += -day; // 调用+=运算符重载函数
	}

	_day -= day; // 累减天数

	while (_day <= 0) // 说明天数不够减了,需要向上一个月去借
	{
		_month--; // 月份-1
		if (_month == 0)
		{
			_year--;
			_month = 12;
		}
		_day += GetDays(_year, _month); // 借上一个月的天数
	}

	return *this;
}

④ 日期 - 天数(返回累减天数后的日期),比如:d1 - 100

Date Date::operator-(int day)
{
	/* 复用 -= 运算符重载函数的代码 */

	Date tmp(*this); // 拷贝构造一份临时对象
	tmp -= day;      // 对临时对象进行 -= 天数操作
	return tmp;      // 返回临时对象
}

⑤ 前置++ 和 后置++

注意:按正常的运算符重载规则,无法区分 前置++ 和 后置++,为了区分,这里做了一个特殊处理,给 后置++ 增加了一个 int 参数,这个参数仅仅是为了区分,使 前置++ 和 后置++ 构成重载。

// 前置++
// ++d1
Date& Date::operator++()
{
	/* 复用 += 运算符重载函数的代码 */

	*this += 1;
	return *this;
}

// 后置++
// d1++
Date Date::operator++(int)
{
	Date tmp(*this); // 保存当前对象自减前的值

	*this += 1; // 复用 += 运算符重载函数的代码

	return tmp; // 返回当前对象自减前的值
}

⑥ 前置-- 和 后置–

// 前置--
// --d1
Date& Date::operator--()
{
	/* 复用 -= 运算符重载函数的代码 */

	*this -= 1;
	return *this;
}

// 后置--
// d1--
Date Date::operator--(int)
{
	Date tmp(*this); // 保存当前对象自减前的值

	*this -= 1; // 复用 -= 运算符重载函数的代码

	return tmp; // 返回当前对象自减前的值
}

⑦ 日期 - 日期(返回相差的天数,有正负之分),比如:d1 - d2

int Date::operator-(const Date& d)
{
	// 思路:让小的日期不断往后++,直到等于大的日期,统计加了多少次,就相差多少天
	
	// 加一个flag变量来控制天数的正负
	// 大的日期 - 小的日期 = 正的天数
	// 小的日期 - 大的日期 = 负的天数

	// 判断出大的日期和小的日期
	Date max = *this;
	Date min = d;
	int flag = 1;

	if (max < min)
	{
		max = d;
		min = *this;
		flag = -1;
	}

	// 让小的日期累加天数,加了多少次,说明就相差了多少天
	int count = 0;
	while (min != max)
	{
		++min;
		++count;
	}

	return flag * count;
}

六、const

6.1 const 修饰类成员函数
  • 将 const 修饰的类成员函数称之为 「const 成员函数

  • const 修饰类成员函数,实际修饰的是该成员函数隐含的「this 指针」,表明在该成员函数中不能修改任何当前对象的成员变量

    image-20220421151526945

👉示例

下面代码编译不通过,是为什么呢?

class Date {
public:
	// 默认构造函数
	Date (int year = 1, int month = 1, int day = 1) {
		_year = year;
		_month = month;
		_day = day;
	}

    // 打印日期
    // void Print(Date* const this)
	void Print() {
		cout << _year << "/" << _month << "/" << _day << endl;
	}
    
private:
	int _year;
	int _month;
	int _day;
};

int main() {
    Date d1(2021, 1, 1);       // 普通对象
	const Date d2(2022, 1, 1); // const修饰的常对象

	d1.Print();

	d2.Print(); // d2.Print(&d2);
    // error : 不能将“this”指针从“const Date”转换为“Date&”
    
    return 0;
}	

原理分析:

image-20220421152819104

所以:const 对象是不能调非 const 成员函数的。


总结

  1. const 修饰成员函数是有好处的,这样 const 对象可以调用,非 const 对象也可以调用。
  2. 那是不是所有成员函数都要加 const 呢?不是。
  3. 要看成员函数的功能,如果成员函数是修改型(会修改成员变量),那就不能加;如果成员函数是只读型(不会修改成员变量),那就可以加。

思考

  1. const 对象(只读)可以调用非 const 成员函数(可读可写)吗?

—> 不能

  1. 非 const 对象(可读可写)可以调用 const 成员函数(只读)吗?

    —> 可以

  2. const 成员函数内(只读)可以调用其它的非 const 成员函数(可读可写)吗?

    —> 不能

  3. 非 const 成员函数内(可读可写)可以调用其它的 const 成员函数(只读)吗?

    —> 可以


6.2 取地址及 const 取地址运算符重载

6 大默认成员函数,还剩下 2 个,普通对象和 const 对象取地址运算符重载,但这 2 个不重要,很少会自己实现,编译器默认会生成。

class Date {
public:
    // 编译器会解释为:Date* operator&(Date* const this)
	Date* operator&() // 普通对象取地址运算符重载函数
	{
		return this;
	}
    
    // 编译器会解释为:const Date* operator&(const Date* const this)
	const Date* operator&() const // const对象取地址运算符重载函数
	{
		return this;
	}
    
private:
	int _year;
	int _month;
	int _day;
};

int main() {
    Date d1(2021, 1, 1);
    const Date d2(2021, 1, 1);
    
    cout << &d1 << endl;
    cout << &d2 << endl;
}

如果用户不想让别人获取到对象的地址,就可以自己实现。但这种场景一般不存在。

class Date {
public:
	Date* operator&() {
		return nullptr;
	}
    
	const Date* operator&() const {
		return nullptr;
	}
    
private:
	int _year;
	int _month;
	int _day;
};

七、补充:编译器对传值传参和传值返回的优化

传值传参和传值返回都会生成一个拷贝对象,但编译器可能会进行优化。

函数传值传参

void f(Widget w)
{}

int main()
{
    f(Widget());
    // Widget(); // 匿名对象,它没有名字,特点是:它的声明周期只在这一行
}

这里本来是:先构造匿名对象,然后用匿名对象拷贝构造对象 w,但一般情况下编译器会进行优化,直接拿构造出来的匿名对象当作对象 w 来用了。

所以这里:只有一次构造,没有拷贝构造。

注意:下面示例中,传值传参是有一次拷贝构造哦

void f(Widget w)
{}

int main()
{
    Widget x; // 构造对象x
    f(x);     // 用对象x拷贝构造对象w
}

函数传值返回

Widget f()
{
    Widget w;
    return w;
}

int main()
{
    Widget x = f();
}

一般情况下,一个对象拷贝构造出一个临时对象,一个临时对象再去拷贝构造另一个对象,编译器会进行优化(不同编译器情况可能会有所不同),把中间对象优化掉,直接用第一个对象拷贝构造第三个对象(直接把临时对象当成对象 x 来用了)

所以这里的传值返回,只有一次拷贝构造。

image-20220422110909884

总结:在传值传参和传值返回的过程中,只要是在一个表达式调用的连续步骤中,就可能产生优化。


笔试题

  • 以下代码共调用多少次拷贝构造函数: ( D )

    Widget f(Widget u) { 
         Widget v(u);
         Widget w=v;
         return w;
    }
    
    int main() {
         Widget x;
         Widget y=f(f(x));
    }
    

    A .1

    B .3

    C .5

    D .7 ✔


  • 14
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 24
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 24
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值