从C语言到C++⑤(第二章_类和对象_中篇)(6个默认成员函数+运算符重载+const成员)

目录

0. 引入6个默认成员函数

1. 构造函数(默认成员函数)

1.1 构造函数的概念

1.2 构造函数的特性和用法

1.3 默认构造函数

2. 析构函数(默认成员函数)

2.1 析构函数概念

2.2 析构函数特性

3. 拷贝构造函数(默认成员函数)

3.1 拷贝构造函数概念

3.2 拷贝构造函数特性和用法

3.3 默认生成的拷贝构造

4. 运算符重载

4.1  运算符重载的概念

4.2 运算符重载示例

5. 赋值运算符重载(默认成员函数)

5.1 赋值运算符重载概念

5.2 赋值运算符重载使用

5.3 默认生成的赋值运算符重载

6. const 成员

6.1 const 成员的作用

6.2 const 修饰类的成员函数

7. 取地址及const取地址操作符重载(两个默认成员函数)

7.1 取地址及const取地址操作符重载(自己实现)

7.2 取地址及const取地址操作符重载(默认生成)

本篇完。


0. 引入6个默认成员函数

如果一个类中什么成员都没有,简称为空类。
空类中真的什么都没有吗?并不是,任何类在什么都不写时,
编译器会自动生成以下 6 个默认成员函数。
C++类中有6个默认函数,分别是:
构造函数析构函数拷贝构造函数赋值运算符重载取地址const取地址运算符重载
这六个函数是很特殊的函数,如果我们不自己实现,编译器就会自己实现。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。

对于默认成员函数,如果我们不主动实现,编译器会自己生成一份。

比如我们在上一篇里举过的一个 Stack 的例子,如果需要初始化和清理,"构造函数" 和 "析构函数" 就可以帮助我们完成。构造函数就类似于 Init,而析构函数就类似于 Destroy。


1. 构造函数(默认成员函数)

对于以下 Date
#include <iostream>
using namespace std;

class Date 
{
public:
    void Init(int year, int month, int day)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    void Print()
    {
        printf("%d-%d-%d\n", _year, _month, _day);
    }

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

int main()
{
    Date d1;
    d1.Init(2023, 4, 23);
    d1.Print();

    Date d2;
    d2.Init(2022, 5, 2);
    d2.Print();

    return 0;
}
        对于Date 类,可以通过 Init 公有方法给对象设置日期,但如果每次创建对象时都调用该方法设置信息,未免有点麻烦,而且写其它类的时候可能会忘记初始化,会出现程序崩溃的情况, 那能否在对象创建时,就将信息设置进去呢?

1.1 构造函数的概念

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

构造函数的意义:能够保证对象被初始化。

构造函数是特殊的成员函数,主要任务是初始化对象,而不是开空间。

(虽然构造函数的名字叫构造)


1.2 构造函数的特性和用法

构造函数是特殊的成员函数,主要特征如下:

① 构造函数的函数名和类名是相同的

② 构造函数无返回值(也不用写void)

③ 构造函数可以重载

④ 会在对象实例化时自动调用对象定义出来。

构造函数的用法:

#include <iostream>
using namespace std;

class Date 
{
public:
    Date()
    {
        _year = 1;
        _month = 1;
        _day = 1;
    }

    Date(int year, int month, int day)
    {
        _year = year;
        _month = month;
        _day = day;
    }

    void Print()
    {
        printf("%d-%d-%d\n", _year, _month, _day);
    }

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

int main()
{
    Date d1; // 对象实例化,此时触发构造,调用无参构造函数
    d1.Print();

    Date d2(2023, 5, 2); // 对象实例化,此时触发构造,调用带参构造函数
    // 这里如果调用带参构造函数,我们需要传递三个参数(这里我们没设缺省) 。
    //如果想传几个就传几个可以自己设置重载
    d2.Print();

    // 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
    // 以下代码的函数:声明了d3函数,该函数无参,返回一个日期类型的对象
    Date d3();
    // warning C4930: “Date d3(void)”: 未调用原型函数(是否是有意用变量定义的?)

    //构造函数是特殊的,不是常规的成员函数,不能直接调d1.Data();

    return 0;
}

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

        对于上面的d1中,如果只有带参构造函数就会报错,对于d2,如果只有无参构造函数就会报错,所以把自己写的构造函数都删除之后d1可以运行,d2会报错。


1.3 默认构造函数

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

    //全缺省构造函数 也是 默认构造函数 (一般写全缺省,不写上面那个)
    Date(int year = 1, int month = 1, int day = 1) 
    {
        _year = year;
        _month = month;
        _day = day;
    }

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

无参构造函数、全缺省构造函数都被称为默认构造函数。并且默认构造函数只能有一个。

注意事项:

        ① 无参构造函数、全缺省构造函数、我们没写编译器默认生成的无参构造函数,这三个都可以认为是默认构造函数。

        ② 语法上无参和全缺省可以同时存在,但如果同时存在会引发二义性:无参的构造函数和全缺省的构造函数都成为默认构造函数,并且默认构造参数只能有一个,语法上他们两个可以同时存在,但是如果有对象定义去调用就会报错。

        关于编译器生成的默认成员函数,很多人会有疑惑:不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?d 对象调用了编译器生成的默认构造函数,但是d 对象 _year/_month/_day ,依旧是随机值。也就说在这里编译器生成的 默认构造函数并没有什么用?? 解答:
C++把类型分成 内置类型(基本类型)和自定义类型
内置类型就是语言提供的数据类型,如:int/char/指针等等,
自定义类型就是我们使用class/struct等自己定义的类型,

C++ 规定:我们不写编译器默认生成构造函数,对于内置类型的成员变量,不做初始化处理。

但是对于自定义类型的成员变量会去调用它的默认构造函数(不用参数就可以调的)初始化。

如果没有默认构造函数(不用参数就可以调用的构造函数)就会报错。

#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()
	{
		printf("%d %d %d\n", _year, _month, _day);
	}

private:

	int _year;// 基本类型(内置类型)
	int _month;
	int _day;

	Time _t;// 自定义类型
};

int main()
{
	Date d;
	d.Print();
	return 0;
}

        很多人吐槽不写构造函数编译器会默认生成的这个特性设计得不好,因为没有对内置类型和自定义类型统一处理,不处理内置类型成员变量,只处理自定义类型成员变量。

        但是覆水难收,所以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()
	{
		printf("%d %d %d\n", _year, _month, _day);
	}

private:
	
	int _year = 1;// 基本类型(内置类型)
	int _month = 1;
	int _day = 1;
	//注意这里不是初始化,是给默认构造函数缺省值

	Time _t;// 自定义类型
};

int main()
{
	Date d;
	d.Print();
	return 0;
}

需要注意的是,上面代码中如果自定义类型Time没有写构造函数,编译器也什么都不会处理。 

总结:
构造函数分为三类:
①无参构造函数、
②全缺省构造函数、
③我们没写编译器默认生成的构造函数,
        这三类都可以认为是默认构造函数。并且默认构造函数只能有一个。 一般的类都不会让编译器默认生成构造函数,一般显示地写一个全缺省,非常好用, 特殊情况才会默认生成。

2. 析构函数(默认成员函数)

2.1 析构函数概念

        通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没呢的?
        析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会 自动调用 析构函数,完成对象中资源的清理工作。

以前我们写数据结构的时候经常忘记调用 destroy 函数,但是现在我们有析构函数了。


2.2 析构函数特性

构造函数是特殊的成员函数,主要特征如下:

① 析构函数名是在类名前面加上字符 

② 析构函数既没有参数也没有返回值(因为没有参数,所以也不会构成重载问题)

③ 一个类的析构函数有且仅有一个(如果不写系统会默认生成一个析构函数)

④ 析构函数在对象生命周期结束后,会自动调用。(和析构函数是对应的构造函数是在对象实例化时自动调用)

#include <iostream>
using namespace std;

class Date 
{
public:
    Date(int year = 1, int month = 1, int day = 1) 
    {
        _year = year;
        _month = month;
        _day = day;
    }
    void Print() 
    {
        printf("%d-%d-%d\n", _year, _month, _day);
    }

    ~Date() 
    {
        cout << "~Date()" << endl;// 日期类没有资源需要清理,所以只打印下知道调用了
    }

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

int main()
{
    Date d1;
    Date d2(2023, 5, 2);

    return 0;
}

d1 和 d2 都会调用析构函数:

        拿 Stack 来举个例子,体会下构造函数和析构函数的用处,我们知道,栈是需要 destroy 清理开辟的内存空间的。

#include<iostream>
#include<stdlib.h>
using namespace std;

typedef int StackDataType;
class Stack 
{
public:
    Stack(int capacity = 4) // 这里只需要一个capacity就够了,默认给4(利用缺省参数)
    {
        _array = (StackDataType*)malloc(sizeof(StackDataType) * capacity);
        if (_array == NULL) 
        {
            cout << "Malloc Failed!" << endl;
            exit(-1);
        }
        _top = 0;
        _capacity = capacity;
    }

    ~Stack() // 这里就用的上析构函数了,我们需要清理开辟的内存空间(防止内存泄漏)
    {
        free(_array);
        _array = nullptr;//下面这两行可以不写,这个野指针已经没人能访问到了
        _top = _capacity = 0;//但写了也是个好习惯
    }

private:
    int* _array;
    size_t _top;
    size_t _capacity;
};

int main(void)
{
    Stack s1;
    Stack s2(20); //初始capacity给20

    return 0;
}

        代码解读:我们在设置栈的构造函数时,定义容量 capacity 时利用缺省参数默认给个4的容量,这样用的时候默认就是4,如果不想要4可以自己传。如此一来,就可以保证了栈被定义出来就一定被初始化,用完后会自动销毁。以后就不会有忘记调用 destroy 而导致内存泄露的惨案了,这里的析构函数就可以充当销毁的作用。

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

        有没有想过,这里是先析构 s1 还是先析构 s2?既然都这样问了,应该是先析构 s2 了 ,没错没错,栈帧和栈里面的对象都符合栈的性质,析构的顺序在局部的栈中是相反的,栈帧销毁清理资源时 s2 先清理,然后再清理 s1 。(可以在析构函数打印参数看看)(贴两个图)

这张图3也是全局的:

        如果我们不自己写析构函数,让编译器自动生成,那么这个默认析构函数和默认构造函数类似: ① 对于 "内置类型" 的成员变量:不作处理,② 对于 "自定义类型" 的成员变量:会调用它对应的析构函数。

        可能有人要说帮我都销毁掉不就好了?举个最简单的例子,迭代器,析构的时候是不释放的,因为不需要析构函数来管,所以默认不对内置类型处理是正常的,这么一来默认生成的析构函数不就没有用了吗?

        有用,他对内置类型的成员类型不作处理,会在一些情况下非常的有用。比如说:以前我们写过的:两个栈实现一个队列用C++非常方便:数据结构与算法⑨(第三章_下)队列的概念和实现(力扣:225+232+622)_GR C的博客-CSDN博客还可以去试着用C++写一下:力扣232. 用栈实现队列。

232. 用栈实现队列 - 力扣(LeetCode)


3. 拷贝构造函数(默认成员函数)

我们在创建对象的时候,能不能创建一个与已存在对象一模一样的新对象呢?

Date d1(2023, 5, 3);
d1.Print();

Date d2(d1);//把d1拷贝给d2
d2.Print();

Date d3 = d1;//把d1拷贝给d3 (这也是拷贝构造,后面学的赋值是两个已存在的对象)
d3.Print();

当然可以,这时我们就可以用拷贝构造函数。


3.1 拷贝构造函数概念

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

Date(const Date& d) // 这里要用引用,否则就会无穷递归下去
{         
	_year = d._year;
	_month = d._month;
	_day = d._day;
}

3.2 拷贝构造函数特性和用法

拷贝构造函数也是一个特殊的构造函数,所以他符合构造函数的一些特性:

① 拷贝构造函数是构造函数的一个重载形式。函数名和类名相同,没有返回值。

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

拷贝构造函数的用法:

#include <iostream>
using namespace std;

class Date 
{
public:
    Date(int year = 1, int month = 1, int day = 1) 
    {
        _year = year;
        _month = month;
        _day = day;
    }

    Date(const Date& d)// 这里一定要用引用,否则就会无穷递归下去,加const是为了原来的d1被修改
    {
        _year = d._year;
        _month = d._month;
        _day = d._day;
    }

    void Print() 
    {
        printf("%d-%d-%d\n", _year, _month, _day);
    }

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

int main()
{
    Date d1(2023, 5, 3);
    d1.Print();

    Date d2(d1);//把d1拷贝给d2
    d2.Print();

    Date d3 = d1;//把d1拷贝给d3
    d3.Print();

    return 0;
}

 为什么必须使用引用传参呢?

调用拷贝构造,需要先传参数,传值传参又是一个拷贝构造。

调用拷贝构造,需要先传参数,传值传参又是一个拷贝构造。

调用拷贝构造,需要先传参数,传值传参又是一个拷贝构造。

……

一直在传参这里出不去了,所以这个递归是一个无穷无尽的。

注意:如果参数在函数体内不需要改变,建议把 const 加上。


3.3 默认生成的拷贝构造

默认生成拷贝构造:

① 内置类型的成员,会完成按字节序的拷贝(把每个字节依次拷贝过去)。

② 自定义类型成员,会再调用它的拷贝构造。

        拷贝构造我们不写生成的默认拷贝构造函数,对于内置类型和自定义类型都会拷贝处理。但是处理的细节是不一样的,这个跟构造函数和析构函数是不一样的。

(把上面的代码中自己写的拷贝构造屏蔽了,运行结果还是一样:)

#include <iostream>
using namespace std;

class Date 
{
public:
    Date(int year = 1, int month = 1, int day = 1) 
    {
        _year = year;
        _month = month;
        _day = day;
    }

    //Date(const Date& d)// 这里一定要用引用,否则就会无穷递归下去,加const是为了原来的d1被修改
    //{
    //    _year = d._year;
    //    _month = d._month;
    //    _day = d._day;
    //}

    void Print() 
    {
        printf("%d-%d-%d\n", _year, _month, _day);
    }

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

int main()
{
    Date d1(2023, 5, 3);
    d1.Print();

    Date d2(d1);//把d1拷贝给d2
    d2.Print();

    Date d3 = d1;//把d1拷贝给d3
    d3.Print();

    return 0;
}

        所以为什么要写拷贝构造?写它有什么意义?这里没有什么意义。当然,这并不意味着我们都不用写了,有些情况还是不可避免要写的比如实现栈的时候,栈的结构问题,导致这里如果用默认的拷贝构造,会程序崩溃。按字节把所有东西都拷过来会产生问题,如果 Stack st1 拷贝出另一个 Stack st2(st1) ,会导致他们都指向那块开辟的内存空间,导致他们指向的空间被析构两次,导致程序崩溃,然而问题不止这些。

        其实这里的字节序拷贝是浅拷贝,下面几章我们会详细讲一下深浅拷贝,这里的深拷贝和浅拷贝先做一个大概的了解。对于常见的类,比如日期类,默认生成的拷贝构造能用。但是对于栈这样的类,默认生成的拷贝构造不能用。


4. 运算符重载

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

运算符重载简单来说:就是能让自定义类型和内置类型一样使用运算符。


4.1  运算符重载的概念

运算符重载是具有特殊函数名的函数,能让自定义类型和内置类型一样使用运算符。

函数名为 :关键字 operator 后面接需要重载的运算符符号 。 比如:
operator+
operator>
operator==
函数原型:返回值类型  operator 操作符 ( 参数列表 )
返回值类型:看操作符运算后返回的值是什么。

参数:操作符有几个操作数,它就有几个参数。

 注意事项:

  • 不能通过连接其他符号来创建新的操作符,比如operator@,只能对已有的运算符进行重载,也不能对内置类型进行重载。
  • 重载操作符必须有一个类类型或枚举类型的操作数。
  • 用于内置类型的操作符,其含义不能改变。比如内置的整型 +,不能改变其含义。
  • 作为类成员的重载函数时,其形参看起来比操作数数目少 1,成员函数的操作符有一个默认的形参 this,限定为第一个形参。
  • 不支持运算符重载的 5 个运算符:(这个经常在笔试选择题中出现)
.          (点运算符)
 
::         (域运算符)
 
.*         (点星运算符)(目前博客没讲过的)
 
?:         (条件运算符)
 
sizeof

        虽然点运算符( . )不能重载,但是箭头运算符( -> )是支持重载的,解引用(*)是可以重载的,不能重载的是点星运算符( .* )


4.2 运算符重载示例

我们重载一个判断日期类相等的运算符:==

#include <iostream>
using namespace std;
 
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;
};
 
bool operator==(const Date& d1, const Date& d2) 
{
	return d1._year == d2._year
		&& d1._month == d2._month 
		&& d1._day == d2._day;
}
 
int main() 
{
	Date d1(2023, 5, 2);
	Date d2(2023, 5, 3);
	
	cout << (d1 == d2) << endl;//这里的流插入运算符比我们重载的==优先级高,所以要加括号
 
	return 0;
}

        这里运算符重载成全局的,不得不将成员变成是公有的,得把 private 注释掉,那么问题来了,封装性如何保证?这里其实可以用 "友元" 来解决,如果现在不知道也没关系,后面会讲。用友元也是不好的,所以一般直接重载成成员函数:

#include <iostream>
using namespace std;
 
class Date 
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	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(2023, 5, 2);
	Date d2(2023, 5, 3);
	
	cout << (d1 == d2) << endl;
//编译器自动转化为:
//  cout << (d1.operator==(d2)) << endl;
 
	return 0;
}

        既然要当成员函数,就得明白这里的 this 指的是谁。需要注意的是,左操作数是 this 指向的调用函数的对象。(关于运算符重载我们下一篇还会完整的实现一个日期类,重载各种运算符,比如日期减日期)


5. 赋值运算符重载(默认成员函数)

5.1 赋值运算符重载概念

赋值运算符重载主要是把一个对象赋值给另一个对象。

如果你不写,编译器会默认生成。

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

要分清和拷贝构造的区别:

int main()
{
	// 一个已经存在的对象初始化一个马上创建实例化的对象
	Date d1(2023, 5, 3);

	Date d2(d1);  // 拷贝构造
	Date d3 = d1;  // 拷贝构造

	// 两个已经存在的对象,之间进行赋值拷贝
	Date d4(2023, 5, 4);
	d1 = d4; // 赋值 让 d1 和 d4 一样

	return 0;
}

5.2 赋值运算符重载使用

赋值运算符重载主要有以下四点:

① 参数类型

② 返回值

③ 检查是否给自己复制

④ 返回 *this

#include <iostream>
using namespace std;

class Date 
{
public:
	Date(int year = 1, int month = 1, int day = 1) 
	{
		_year = year;
		_month = month;
		_day = day;
	}

	Date& operator=(const Date& d) 
	{
		if (this != &d)  // 防止自己跟自己赋值(这里的&d是取地址)
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
		return *this;   // 返回左操作数d1
	}

	void Print()
	{
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}

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

int main()
{
	Date d1(2023, 5, 3);
	Date d2(2023, 5, 4);

	d1 = d2;

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

        自己给自己赋值是无意义的,这里加 if 语句来判断就是为了防止极端情况下,自己给自己赋值,加上这条判断后就算遇到自己给自己赋值,就会不做处理,直接跳过。

        因为出了作用域 *this 还在,所以我们可以使用引用来减少拷贝。(因为传值返回不会直接返回对象,而是会生成一个拷贝的对象,这里减少了两次拷贝构造的调用)


5.3 默认生成的赋值运算符重载

        赋值运算符重载是默认成员函数,所以如果一个类没有显式定义赋值运算符重载,编译器默认生成复制重载,跟拷贝构造做的事情完全类似:

① 内置类型成员,会完成字节序值拷贝 —— 浅拷贝。

② 对于自定义类型成员变量,会调用它的 operator= 赋值。

把我们自己写的赋值运算符重载注释掉:

#include <iostream>
using namespace std;

class Date 
{
public:
	Date(int year = 1, int month = 1, int day = 1) 
	{
		_year = year;
		_month = month;
		_day = day;
	}

	//Date& operator=(const Date& d) 
	//{
	//	if (this != &d)  // 防止自己跟自己赋值(这里的&d是取地址)
	//	{
	//		_year = d._year;
	//		_month = d._month;
	//		_day = d._day;
	//	}
	//	return *this;   // 返回左操作数d1
	//}

	void Print()
	{
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}

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

int main()
{
	Date d1(2023, 5, 3);
	Date d2(2023, 5, 4);

	d1 = d2;

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

        既然编译器会自己默认生成,已经可以完成字节序的值拷贝了,我们还需要自己实现吗?当然像日期这样的类是没有必要的,但有时候还是需要自己实现的。比如下面的情况:

#include<iostream>
using namespace std;
typedef int DataType;
class Stack
{
public:
	Stack(int capacity = 10)
	{
		_array = (DataType*)malloc(capacity * sizeof(DataType));
		if (_array == nullptr)
		{
			perror("malloc申请空间失败");
			return;
		}
		_size = 0;
		_capacity = capacity;
	}

	void Push(const DataType& data)
	{
		_array[_size] = data;
		_size++;
	}

	~Stack()
	{
		if (_array)
		{
			free(_array);
			_array = nullptr;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	DataType* _array;
	int _size;
	int _capacity;
};
int main()
{
	Stack s1;
	s1.Push(1);
	s1.Push(2);
	s1.Push(3);
	s1.Push(4);
	Stack s2;
	s2 = s1;
	return 0;
}
如果类中未涉及到资源管理,赋值运算符是否实现都可以,一旦涉及到资源管理则必须要实现。

6. const 成员

6.1 const 成员的作用

上面定义的日期类,普通对象对它调用 Print ,是可以调得动的。

#include <iostream>
using namespace std;

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

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

int main()
{
	Date d1(2023, 5, 3);
	const Date d2(2023, 5, 4);

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

如果这个对象是 const 的呢?这样编译就报错了:

error C2662: “void Date::Print(void)”: 不能将“this”指针从“const Date”转换为“Date &”

这块报错的原因是什么?这里涉及的问题是 "权限的放大" ,这个知识点在下面这篇博客讲过:

从C语言到C++②(第一章_C++入门_中篇)缺省参数+函数重载+引用_GR C的博客-CSDN博客

此时可以使用 const 修饰类的成员函数来解决这种情况。


6.2 const 修饰类的成员函数

将 const 修饰的类成员函数,我们称之为 const 成员函数。

const 修饰类成员函数,实际修饰的是该成员函数隐含的 this 指针,

表明在该成员函数中不能对类的任何成员进行修改。

这里我们可以在函数后面加 const,保持权限的统一:

#include <iostream>
using namespace std;

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	void Print() const //这里语法就是加在这里的,虽然有点奇怪
	{
		//void Print(Date* const this)变成了void Print(const Date* const this)
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}

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

int main()
{
	Date d1(2023, 5, 3);
	const Date d2(2023, 5, 4);

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

权限的放大会报错,这里d1是权限的缩小,d2没有改变权限。

        使用建议:建议能加上 const 都加上,这样普通对象和 const 对象都可以调用了。但是,如果要修改成员变量的成员函数是不能加的,比如日期类中 += ++ 等等实现。它是要修改的,加不了就算了。


7. 取地址及const取地址操作符重载(两个默认成员函数)

        这两个运算符一般不需要重载,因为它是默认成员函数,编译器会自己默认生成。一般用编译器自己生成的就够了,所以这里只是简单演示一下使用:

7.1 取地址及const取地址操作符重载(自己实现)

#include <iostream>
using namespace std;

class Date 
{
public:
	Date(int year, int month, int day) 
	{
		_year = year;
		_month = month;
		_day = day;
	}

	Date* operator&() 
	{
		return this;
	}

	const Date* operator&() const
	{
		return this;
	}

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


int main()
{
	Date d1(2023, 5, 3);
	cout << &d1 << endl;// 取出d1的地址

	const Date d2(2023, 5, 4);
	cout << &d2 << endl;// 取出d2的地址

	return 0;
}


7.2 取地址及const取地址操作符重载(默认生成)

直接把我们写的注释掉:

#include <iostream>
using namespace std;

class Date 
{
public:
	Date(int year, int month, int day) 
	{
		_year = year;
		_month = month;
		_day = day;
	}

	//Date* operator&() 
	//{
	//	return this;
	//}

	//const Date* operator&() const
	//{
	//	return this;
	//}

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


int main()
{
	Date d1(2023, 5, 3);
	cout << &d1 << endl;// 取出d1的地址

	const Date d2(2023, 5, 4);
	cout << &d2 << endl;// 取出d2的地址

	return 0;
}

只有特殊情况才需要重载,比如你不想让别人取到你的地址:

可以放到私有,或者自己实现返回空指针:

#include <iostream>
using namespace std;

class Date 
{
public:
	Date(int year, int month, int day) 
	{
		_year = year;
		_month = month;
		_day = day;
	}

	Date* operator&() 
	{
		return nullptr;
	}

	const Date* operator&() const
	{
		return nullptr;
	}

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

int main()
{
	Date d1(2023, 5, 3);
	cout << &d1 << endl;// 取出d1的地址

	const Date d2(2023, 5, 4);
	cout << &d2 << endl;// 取出d2的地址

	return 0;
}


本篇完。

下一篇:从C语言到C++⑥(第二章_类和对象_中篇_续)大练习(日期类)+笔试选择题。

(穿越回来复习顺便放个下篇链接)

从C语言到C++⑥(第二章_类和对象_中篇_续)大练习(日期类)+笔试选择题_GR_C的博客-CSDN博客

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

GR鲸鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值