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

news2024/9/23 23:29:30

目录

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.1 赋值运算符重载使用

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

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. 用栈实现队列 - 力扣(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.1 赋值运算符重载使用

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

① 参数类型

② 返回值年

③ 检查是否给自己复制

④ 返回 *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.2 默认生成的赋值运算符重载

赋值运算符重载是默认成员函数,所以如果一个类没有显式定义赋值运算符重载,

编译器默认生成复制重载,跟拷贝构造做的事情完全类似:

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

② 对于自定义类型成员变量,会调用它的 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;
}

本篇完(下一篇续上此篇大练习:实现日期类)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/488548.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

附录2-购物车案例

目录 1 效果 2 接口数据 3 App.vue 4 HEADER.vue 5 COUNT.vue 6 GOODS.vue 7 FOOTER.vue 1 效果 由四种子组件和一个App.vue构成 2 接口数据 返回结果如下 {"status": 200,"message": "获取购物车列表数据成功&#xff01;",&q…

idea中的debug操作详解

行断点 默认模式 方法断点 菱形&#xff0c;加在方法前&#xff0c;用的比较多的地方&#xff1a;加在接口前会进入这个接口的实现类。 异常断点 如果说你的程序抛了某个异常&#xff0c;你需要知道在哪里抛出的&#xff0c;可以直接设置异常断点&#xff0c;设置后程序会…

Shell脚本之循环语句(for、while、until)

目录 一、echo命令二 for循环语句三 while循环语句结构(迭代&#xff09;四. until 循环语句结构五.continue和break和exit 一、echo命令 ?echo -n 表示不换行输出 echo -e 输出转义字符&#xff0c;将转义后的内容输出到屏幕上 常见转义字符&#xff1a; 二 for循环语句 用法…

Baumer工业相机堡盟工业相机如何通过BGAPI SDK设置自动亮度调整BrightnessAuto(自动曝光自动增益)(C++)

自动亮度调整项目场景 Baumer工业相机堡盟相机是一种高性能、高质量的工业相机&#xff0c;可用于各种应用场景&#xff0c;如物体检测、计数和识别、运动分析和图像处理。 Baumer的万兆网相机拥有出色的图像处理性能&#xff0c;可以实时传输高分辨率图像。此外&#xff0…

北斗哨兵北斗短报文远程监控系统解决方案

一、项目背景 随着社会发展各行各业都会遇到各种各样的安全问题&#xff0c;监控系统作为安防的第一线安防设备也已广泛部署&#xff0c;然而地处偏僻的监控区域往往面临着难以提供电力供应以及网络供应的问题&#xff0c;类似于山区环境监测&#xff0c;工地监测等复杂的环境布…

Web自动化测试流程:从入门到精通,帮你成为测试专家

B站首推&#xff01;2023最详细自动化测试合集&#xff0c;小白皆可掌握&#xff0c;让测试变得简单、快捷、可靠https://www.bilibili.com/video/BV1ua4y1V7Db 目录 摘要&#xff1a; 步骤一&#xff1a;选取测试工具 步骤二&#xff1a;编写测试用例 步骤三&#xff1a;编…

5月4号软件资讯更新合集.....

&#x1f680; Layui 2.8.2 发布 更新日志 table 修复 autoSort: true 时&#xff0c;更改 table.cache 未同步到 data 属性的问题 修复 多级表头存在 hide 表头属性时&#xff0c;执行完整重载可能出现的错位问题 修复 未开启 page 属性时底边框缺失问题 优化 打印内容中…

大型游戏剧本杀小程序app

大型游戏剧本杀小程序的发展趋势主要表现为以下几个方面&#xff1a; 社交互动&#xff1a;未来大型游戏剧本杀小程序将会更加注重社交互动&#xff0c;为用户提供更多的沟通方式和社交场景&#xff0c;以增强玩家间的互动和参与感。 智能化和AR/VR技术应用&#xff1a…

网络基础项目——全网互通实验

作者简介:一名云计算网络运维人员、每天分享网络与运维的技术与干货。 座右铭:低头赶路,敬事如仪 个人主页:网络豆的主页​​​​​​ 前言 本章将会讲解网络基础项目——全网互通实验。 一.实验项目图 二.实验要求 1.全网互通,所有PC机能访问服务器(ping)2.路由器…

DA-Net:用于视网膜血管分割的双分支Transformer和自适应条带上采样

文章目录 DA-Net: Dual Branch Transformer and Adaptive Strip Upsampling for Retinal Vessels Segmentation摘要本文方法整体框架Transformer LayerAdaptive Strip Upsampling Block 实验结果消融实验 DA-Net: Dual Branch Transformer and Adaptive Strip Upsampling for R…

【Python习题集6】类与对象

类与对象 一、实验内容二、实验总结 一、实验内容 1.设计一个Circle类来表示圆&#xff0c;这个类包含圆的半径以及求面积和周长的函数。在使用这个类创建半径为1~10的圆&#xff0c;并计算出相应的面积和周长。 半径为1的圆&#xff0c;面积: 3.14 周长: 6.28 半径为2的圆&am…

【虹科案例】虹科任意波形发生器板卡在声场模拟实验中的应用

声场模拟实验介绍 声场模拟实验是一种通过模拟不同环境下的声场特征来模拟真实世界中声音传输情况的实验方法。通过模拟不同环境下的声场特征&#xff0c;如空间分布、强度、频率等&#xff0c;来模拟真实世界中的声音传输情况&#xff0c;从而对声学相关问题进行研究。 在声…

Java集合框架知识总结

前言 Java集合框架主要由两个接口及其下面的实现类构成&#xff0c;这两个接口分别是Map接口和Collection接口&#xff0c;下面先通过其对应的UML类图看下这两个接口的具体实现&#xff0c;如下 1、Map接口 Map接口的主要实现有我们熟悉的HashMap、HashTable以及TreeMap、Con…

信息技术发展

OSI网络标准协议 物理层&#xff1a;联网的媒介 RS232 FDDI 数据链路层: 网络层接收到的数据分割成可被物理层传输的帧 IEEE802.3/.2 ATM 网络层&#xff1a;网络地址翻译成对应的物理地址&#xff0c;路由 IP ICMP IGMP IPX ARP 传输层&#xff1a;端到端的错误恢复和流量控制…

轻松提升投标技术分?smardaten高性价比原型服务受热捧

日前&#xff0c;某个交通领域的软件公司A遇到了难题&#xff0c;十多个在跟的项目需要在一个月内完成投标准备工作。 应用软件“强甲方需求”的大环境下&#xff0c;A公司又陷“投标高峰期”。 更具挑战性的是&#xff0c;其中&#xff0c;有5个项目要求应标企业提供真实系统的…

一文介绍Linux EAS

能量感知调度&#xff08;Energy Aware Scheduling&#xff0c;简称EAS&#xff09;是目前Android手机中Linux线程调度器的基础功能&#xff0c;它使调度器能预测其决策对CPU能耗的影响。依靠CPU的能量模型&#xff08;Energy Model&#xff0c;简称EM&#xff09;&#xff0c;…

steam搬砖,适合个人操作的创业项目

这个项目主就是在Steam平台买进CSGO道具&#xff0c;再放到网易buff平台卖出。因为进价低出价高&#xff0c;所以每卖出一件道具&#xff0c;利润都相当可观。 关键这玩意背靠Steam这个超大平台&#xff0c;日活几千万&#xff0c;几乎覆盖了市面上的所有热门游戏&#xff0c;…

动态规划——逆序对

逆序对Time Limit: 1000 MSMemory Limit: 5000 KB Description 给定一个长度为N的int型数组a[0,1,2,...N-1], 请计算逆序对个数.当i<j且a[i]>a[j], 则称a[i]与a[j]是一对逆序对.Input 第一行输入M表示包含M组测试数据&#xff0c;每组先输入N (N<50000), 接着输入N…

ios app真机测试到上架App Store详细教程-必看

​转载&#xff1a;https://blog.csdn.net/p312011150/article/details/89374401 ios app真机测试到上架App Store详细教程-必看 Appuploader常见问题 转存失败 重新上传 取消 上架基本需求资料 1、苹果开发者账号&#xff08;如还没账号先申请-苹果开发者账号申请教程&…

​​​​魔兽服务端自定义创建传送门教程

魔兽服务端自定义创建传送门教程 大家好我是艾西,今天跟大家分享下魔兽自定义传送门怎么创建。玩过魔兽的朋友都知道,魔兽这游戏内容多地图也非常大,一个老魔兽玩家很熟悉跑副本的情况下从这个地图到下一个地图都得跑半个小时,更何况对于很多得新手小伙伴了,所有顾及到大…