一篇带你理解类与对象(C++)

目录

1.面向过程和面向对象初步认识

2.类的引入

3.类的定义

4.类的访问限定符及封装

4.1 访问限定符

4.2 封装

5.类的实例化

6.类对象模型

6.1 如何计算类对象的大小

7.this指针

7.1 this指针的引出

7.2 this指针的特性

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

9..构造函数

9.1概念

9.2 特征

10.析构函数

10.1 概念

10.2 特性

11.拷贝构造函数

11.1 概念

11.2 特征

12.赋值运算符重载函数

12.1 运算符重载(是否重载这个运算符是看这个运算符对这个类是否有意义)

12.2 赋值运算符重载

13.const成员

14.取地址以及const取地址操作符重载

15.日期类的实现

Date.h

Date.cpp

 16 流插入和流提取运算符重载

16.1自定义类型流插入的实现

16.2 自定义类型流提取的实现


1.面向过程和面向对象初步认识

C语言是面向对象的,关注的书过程,分析出求解问题的步骤,,通过函数调用逐步解决问题

C++是基于面向对象的,关注的是对象,将一件事情拆分为不同的对象,靠对象之间的交互完成

例:

什么是面向过程?

我们通过使用美团app点餐,分为这几个步骤: 挑选商品,点餐,派单,送餐

什么是面向对象?

我们通过app点餐,通过商家 骑手 用户这几个对象的交互来完成我们点餐的过程

以上通过简单的描述大家应该对面向过程和面向对象有一定的了解了吧

2.类的引入

C语言结构体中只能定义变量,在C++中,结构体内不仅可以定义变量,也可以定义函数。比如: 之前在数据结构初阶中,用C语言方式实现的栈,结构体中只能定义变量;现在以C++方式实现, 会发现struct中也可以定义函数。

C++兼容C,struct以前的用法都可以使用,同时struct升级成了类

#include <iostream>

typedef int DataType;
struct Stack
{
public:
	void Init(int defaultcapacity = 4)
	{
		int* _a = (int*)malloc(sizeof(int) * defaultcapacity);
		if (_a == nullptr)
		{
			perror("Init::malloc");
			return;
		}

		int _top = 0;
		int _capacity = defaultcapacity;
	}

	void Push(DataType x)
	{
		_a[_top++] = x;
	}

	void Destroy()
	{
		free(a);
		_a = nullptr;
		_top = 0;
		_capacity = 0;
	}

private:
	int* _a;
	int _top;
	int _capacity;
};

int main()
{
	Stack N1;
	N1.Init();
	N1.Push(1);
	N1.Push(2);
	N1.Push(3);
	N1.Push(4);
	N1.Destroy();

	return 0;
}

注:

成员变量命名规则的建议:

例如:

你能区分那个是那个吗?这里是形参year传给类成员变量year

因为这里可读性太低了,所以我们一般在类成员变量前加上下划线(/)以示辨别

#include <iostream>

class Stack
{
public:
	void Init(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
	}

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

上面结构体的定义,在C++中更喜欢用class来代替

3.类的定义

类该如何定义呢

class className
{
 // 类体:由成员函数和成员变量组成
 
};  // 一定要注意后面的分号

class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分号不能省略。

类体中内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的方法或者成员函数。

类的两种定义方式:

1. 声明和定义全部放在类体中,需注意:成员函数如果在类中定义,默认都是inline(内联函数),但是内联函数成不成立就要看编译器了

class Person
{
public:
	void Init()
	{
		cout << _name << " " << _sex << " " << _age << endl;
	}
public:
	char* _name;
	char* _sex;
	int _age;
};

2. 类声明放在.h文件中,成员函数定义放在.cpp文件中,注意:成员函数名前需要加类名::,不然编译器会找不到

注:一般情况下,更期望采用第二种方式。

4.类的访问限定符及封装

4.1 访问限定符

C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选 择性的将其接口提供给外部的用户使用。  

【访问限定符说明】
​​​​​​
1. public修饰的成员在类外可以直接被访问

2. protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)

3. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止

4. 如果后面没有访问限定符,作用域就到 } 即类结束。

5. class的默认访问权限为private,struct为public(因为struct要兼容C)

注:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别

C++中struct和class的区别是什么?

解答:C++需要兼容C语言,所以C++中struct可以当成结构体使用。另外C++中struct还可以用来 定义类。和class定义类是一样的,区别是struct定义的类默认访问权限是public,class定义的类 默认访问权限是private。注意:在继承和模板参数列表位置,struct和class也有区别,后序给大 家介绍。

4.2 封装

面向对象的三大特性:封装、继承、多态。 在类和对象阶段,主要是研究类的封装特性,那什么是封装呢?

封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来 和对象进行交互。

封装本质上是一种管理,让用户更方便使用类。比如:对于电脑这样一个复杂的设备,提供给用 户的就只有开关机键、通过键盘输入,显示器,USB插孔等,让用户和计算机进行交互,完成日 常事务。但实际上电脑真正工作的却是CPU、显卡、内存等一些硬件元件。

对于计算机使用者而言,不用关心内部核心部件,比如主板上线路是如何布局的,CPU内部是如 何设计的等,用户只需要知道,怎么开机、怎么通过键盘和鼠标与计算机进行交互即可。因此计 算机厂商在出厂时,在外部套上壳子,将内部实现细节隐藏起来,仅仅对外提供开关机、鼠标以 及键盘插孔等,让用户可以与计算机进行交互即可。

形象的理解就是:我去看兵马俑,我只能站在坑外面看而不能进去,这就是封装,如果大家都可以进去的话,就避免不了有一些素质不高的人在哪乱画乱动,封装的好处就是为了更好的管理这些旅游景点

在C++语言中实现封装,可以通过类将数据以及操作数据的方法进行有机结合,通过访问权限来 隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用。

C语言的数据和操作数据的方法是分开的它没有那么严格,但相对的使用C语言程序员就需要更严谨

例:

C

C++

5.类的实例化

用类类型创建对象的过程,称为类的实例化

1. 类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没 有分配实际的内存空间来存储它;比如:入学时填写的学生信息表,表格就可以看成是一个 类,来描述具体学生信息。

2. 一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量 Person类是没有空间的,只有Person类实例化出的对象才有具体的年龄。

3. 做个比方。类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,只设计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象 才能实际存储数据,占用物理空间

int main()
{
	//实例化变量/定义变量,可以实例化多个对象
	Stack s1;
	Stack s2;
	s2.top = 1;

	//top是声明,不能存数据
	//Stack::top = 1;
	return 0;
}

6.类对象模型

6.1 如何计算类对象的大小

只保存成员变量,成员函数存放在公共的代码段

class A
{
public:
	void Print()
	{
		cout << _a << endl;
	}

public:
	int _a;
};

int main()
{
	A a1;
    A a2;
    A a3;
    
	cout << sizeof(a1) << endl;
    cout << sizeof(A); << ednl;
}

使用结构体名或结构体变量名大小都是一样的,类的大小是不包含函数的,因为函数存放在公共代码段

例:

class A
{
public:
	void Print()
	{
		cout << _a << endl;
	}

public:
	int _a;
};

int main()
{
	A a1;
	A a2;
	A a3;

	a1._a = 1;
	a1.Print();
	a2._a = 1;
	a2.Print();
}

我们可以来转汇编看一下

可以看到a1和a2 call的都是同一个地址,所以这里就可以证明虽然是不同的对象,但是用的函数都是一样的

就像我们使用设计图纸所做出来的房子一样,每家都有厨房,厕所(成员变量),但是不可能每家都要一个篮球场(函数成员),为了节省空间我们将篮球场做成公共的

// 类中既有成员变量,又有成员函数
class A1 {
public:
    void f1() {}
private:
    char _ch;
    double _d;
};

// 类中仅有成员函数
class A2 {
public:
    void f2() {}
};

// 类中什么都没有---空类
class A3
{};

int main()
{
    cout << sizeof(A1) << endl;
    //虽然,我们说函数不算类的大小,但是它占位需要1字节
    cout << sizeof(A2) << endl;
    cout << sizeof(A3) << endl;

    cout << &A1 << endl;
    cout << &A2 << endl;
    return 0;
}

A2和A3如果没有占空间的话,为什么还有地址呢,所以为了占位,它需要1字节的空间

我们有设计图纸,已经在这块地皮上楼房只是说楼房里面没有住人和装修,但是占地已经使用了空间

7.this指针

7.1 this指针的引出

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

	void Print()
	{

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

int main()
{
	Date d1, d2;
	d1.Init(2022, 1, 11);
	d2.Init(2022, 1, 12);
	d1.Print();
	d2.Print();
	return 0;
}

我们上面说了,Print是放在公共代码段的,现在d1,d2去公共代码段调用同一个函数,为什么结果会不一样,因为在Print的参数表中隐藏了一个this指针

函数会这样处理

//this指针不能显示在形参和实参中的,但是可以在函数内部使用
void Init(int year, int month, int day)
{
	cout << _year << "-" << _month << "-" << _day << endl;
}

//编译器会成员函数的处理
void Print(Date* const this)
{
	cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
}

可以看到const在*的后面,即:this指向的地址不能改变,但可以改变地址中的内容

对于上述类,有这样的一个问题: Date类中有 Init 与 Print 两个成员函数,函数体中没有关于不同对象的区分,那当d1调用 Init 函 数时,该函数是如何知道应该设置d1对象,而不是设置d2对象呢?

C++中通过引入this指针解决该问题,即:C++编译器给每个“非静态的成员函数“增加了一个隐藏 的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量” 的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编 译器自动完成。

7.2 this指针的特性

1. this指针的类型:类类型* const,即成员函数中,不能给this指针赋值。

例:

class Date
{
public:
	void Init(int year, int month, int day)
	{
        //this = nullptr;   //这是不行的
		cout << _year << "-" << _month << "-" << _day << endl;
	}

	void Print()
    {
	    cout << _year << "-" << _month << "-" << _day << endl;
    }
private:
	int _year;
	int _month;
	int _day;
};

2. 只能在“成员函数”的内部使用

例:


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

	void Print()
    {
	    cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
    }
private:
	int _year;
	int _month;
	int _day;
};

3. this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给 this形参。所以对象中不存储this指针。

4. this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传 递,不需要用户传递

 this指针是存在哪的?

答案:栈中。因为this指针一个形参,在传参时也需要进行压栈操作

看图:

下面两个题目大家思考一下:

创建栈

这里可以看到C++相比于C更简洁,但是从本质上来看C和C++并没有太大的区别

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

如果一个类中什么成员都没有,简称为空类

空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员 函数。

默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。

class Date {};

9..构造函数

9.1概念

对于以下Date类:

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.Init(2022, 7, 5);
	d1.Print();
	Date d2;
	d2.Init(2022, 7, 6);
	d2.Print();
	return 0;
}

对于Date类,可以通过 Init 公有方法给对象设置日期,但如果每次创建对象时都调用该方法设置 信息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢?

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

9.2 特征

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

其特征如下:

1. 函数名与类名相同。

2. 无返回值(也不需要写void)。

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

struct Stack
{
public:
	Stack(int capacity = 4)
	{
        cout << "Stack" << endl;
		_a = (int*)malloc(sizeof(int) * capacity);
		if (nullptr == _a)
		{
			perror("Stack:;malloc");
			return;
		}

		_top = 0;
		_capacity = capacity;
	}

private:
	int* _a;
	int _top;
	int _capacity;
};

int main()
{
    Stack st1;
    
    return 0;
}

测试运行:

可以看到,确实是自动调用了

注:

4. 构造函数可以重载。

class Date
{
public:
	Date()
	{}

	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;
	d1.Print();
	Date d2;
	d2.Print();
	return 0;
}

可以看到,构造函数可以构成重载

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

class Date
{
public:

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:

	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1;
	return 0;
}

可以看到函数还是随机值

这是因为C++规定编译器生成的默认构造函数,只会初始化构造类型,而不会初始化内置类型

6.关于编译器生成的默认成员函数,很多童鞋会有疑惑:不实现构造函数的情况下,编译器会 生成默认的构造函数。但是看起来默认构造函数又没什么用?d对象调用了编译器生成的默 认构造函数,但是d对象_year/_month/_day,依旧是随机值。也就说在这里编译器生成的 默认构造函数并没有什么用??

解答:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类 型,如:int/char/double...,自定义类型就是我们使用class/struct/union等自己定义的类型,看看 下面的程序,就会发现编译器生成默认的构造函数会对自定类型成员_t调用的它的默认成员 函数。

我们不写,编译器默认生成构造函数,内置类型不做处理,自定义类型会去调用它的默认构造函数

有些编译器也会处理内置函数,但是那是个性化处理,不是所有编译器都会处理

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

class Date
{
public:
	void Print()
	{
		cout << _year <<' ' << _month << ' ' << _day << endl;
	}
private:
	// 基本类型(内置类型)
	int _year = 1;
	int _month = 1;
	int _day = 1;
};
int main()
{
	Date d;
	d.Print();
	return 0;
}

tips:

在类中给声明赋初始值,不是给它们初始化的意思,是给默认构造函数的形参赋予缺省值

7. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。 注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为 是默认构造函数。

所以这两个只能留一个

结论:

  • 一般情况下,有内置类型成员的,就需要自己些构造函数,不能使用编译器生成的
  • 全部都是自定义类型可以考虑让编译器自己生成

10.析构函数

10.1 概念

通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没呢的?

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

10.2 特性

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

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

2. 无参数无返回值类型。

struct Stack
{
public:
	Stack(int capacity = 4)
	{
		cout << "Stack" << endl;
		_a = (int*)malloc(sizeof(int) * capacity);
		if (nullptr == _a)
		{
			perror("Stack:;malloc");
			return;
		}

		_top = 0;
		_capacity = capacity;
	}

	~Stack()
	{
		cout << "~Stack" << endl;
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}

private:
	int* _a;
	int _top;
	int _capacity;
};


int main()
{
	Stack st1;
	return 0;
}

运行:

3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数(内置类型不做处理,自定义类型会去调用它的析构函数)。

注意:析构函数不能重载

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

结论:

  • 一般情况下,有动态申请资源,就需要显示写析构函数释放资源
  • 没有动态申请资源,不需要写析构函数
  • 需要释放资源的成员都是自定义类型,不需要写析构

11.拷贝构造函数

11.1 概念

那在创建对象时,可否创建一个与已存在对象一某一样的新对象呢?

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

11.2 特征

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

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

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

class Date
{
public:
	Date(const Date& d)
	{
		cout << "const Date& d" << endl;
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

	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;
	Date d2(d1);
	return 0;
}

这里解释一下为什么拷贝构造函数中需要使用const Date& d这种形式来接收形参

为什么不能使用Date d呢?

  • 内置类型,不会去调用拷贝构造函数拷贝
  • 自定义类型,必须借用拷贝构造函数来完成拷贝

看图:

因为我们传的是自定义类型,当我们将d1传到函数Date的途中,d1就会被调用到新的拷贝构造函数中,重复前面的步骤,不断递归,最后肯定会栈溢出,但是编译器不允许这样传参,会直接报错

解决方法:

  • 传指针:任何类型的指针都是内置类型,拷贝时不会调用拷贝构造函数   
  • 传引用:直接使用d1的别名

这里建议使用引用,并且加个const(类中不受访问限定符的影响)

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

  • 编译器自动生成的默认拷贝构造函数,会对内置类型完成值拷贝/浅拷贝
  • 自定义类型,调用它的拷贝构造函数
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(2024, 9, 7);
	Date d2(d1);
	return 0;
}

对于这种日期类还是可以使用默认构造函数的

但是有另一种情况

因为默认拷贝构造函数是浅拷贝,遇到动态申请空间的情况就会出现问题

struct Stack
{
public:
	Stack(int capacity = 4)
	{
		cout << "Stack" << endl;
		_a = (int*)malloc(sizeof(int) * capacity);
		if (nullptr == _a)
		{
			perror("Stack:;malloc");
			return;
		}

		_top = 0;
		_capacity = capacity;
	}

	~Stack()
	{
		cout << "~Stack" << endl;
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}

private:
	int* _a;
	int _top;
	int _capacity;
};


int main()
{
	Stack st1;
	Stack st2(st1);
	return 0;
}

这里的问题:

(1)析构函数对同一个空间清理了两次,会报错

(2)改变一个空间的内容会对另外一个空间造成影响

如图:

指向同一块空间

这里使用浅拷贝肯定不行

这里栈得我们手动写一个深拷贝,后期我们会学到深拷贝来解决这里的问题

这里给大家浅浅讲一下深拷贝

深拷贝就是为了弥补浅拷贝的缺点而存在的

如图:

代码实现:

struct Stack
{
public:
	Stack(int capacity = 4)
	{
		cout << "Stack" << endl;
		_a = (int*)malloc(sizeof(int) * capacity);
		if (nullptr == _a)
		{
			perror("Stack:;malloc");
			return;
		}

		_top = 0;
		_capacity = capacity;
	}

	//深拷贝需要自己手动实现
	Stack(const Stack& st1)
	{
		_a = (int*)malloc(sizeof(int) * st1._capacity);
		if (nullptr == _a)
		{
			perror("Stack::malloc");
			return;
		}

		memcpy(_a, st1._a, sizeof(int) * st1._top);
		_capacity = st1._capacity;
		_top = st1._top;
	}

	~Stack()
	{
		cout << "~Stack" << endl;
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}

private:
	int* _a;
	int _top;
	int _capacity;
};


int main()
{
	Stack st1;
	Stack st2(st1);
	return 0;
}

这里看几个情况:

(1).

1. void func1(Date& d);
2. void func2(Date d);
  • 如果没有动态资源的类类型,这上面两种形式肯定是第一个好,因为使用引用为形参在语法上是直接使用d的空间,不会去额外开辟栈帧,这样会减少消耗,加快程序运行效率

(2).

1.
Stack& func1()
{
    static Stack st;
    return st;
}

2.
Stack func1()
{
    Stack st;
    return st;
}

3.
Stcak& func1()
{
    Stack st;
    return st;
}

int main()
{
    Stack ret = func1();
    return 0;
}

(1).st出栈帧后不会被销毁,因为st存放在静态区,故而可以使用引用作为返回值

(2)(3).在2,3中只能使用3这种形式

  • 解释(2):调用func1函数,在func1函数中创建一个变量st,然后返回st变量,返回时编译器会产生一个临时变量来保存st的内容,这时就需要调用拷贝构造函数,将st的内容拷贝给这个临时变量,通过这个临时变量将st的内容给ret,因为是深拷贝,所以会增加空间的使用
  • 解释(3):因为st是临时变量出栈帧结束就销毁了,销毁之后st会调用析构函数来清理它的资源,因为是引用,所以不会产生临时变量,这时ret和st指向的内容相同,即编译器报错

12.赋值运算符重载函数

12.1 运算符重载(是否重载这个运算符是看这个运算符对这个类是否有意义)

C++为什么要加入运算符重载函数呢?

1. C++提供的一些运算符只能供 内置类型/基本类型使用,自定义类型使用不了,因为内置类型原本就是C++本来就有的,创造C++的大佬知道怎么实现它们的运算符,而自定义类型是我们自己定义的所以,需要我们自己实现,运算符重载就是 让自定义类型支持运算符

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

d1 == d2; //书写简单,可读性高
assignment(d1, d2); //书写复杂,可读性低

函数名字为:关键字operator后面接需要重载的运算符符号。

函数原型:返回值类型 operator操作符(参数列表)

注意:

  • 不能通过连接其他符号来创建新的操作符:比如operator@
  • 重载操作符必须有一个类类型参数(自定义类型)
  • 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
  • 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
  • .*  ::  sizeof  ?:  . 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。

下面我通过创建日期类来了解运算符重载函数

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

    bool operator==(const Date& d2)
    {
	    return _year == d2._year
		    && _month == d2._month
		    && _day == d2._day;
    }

	void Print()
	{
		cout << _year << ' ' << _month << ' ' << _day << endl;
	}

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

int main()
{
	Date d1(2024, 9, 10);
	Date d2(2024, 9, 10);

	//operator==(d1, d2);与下面那个效果一样
	int ret = d1 == d2;//自定义类型使用运算符
	cout << ret << endl;
	return 0;
}

这里我使用的是全局的运算符重载函数,但是有一定的缺陷,保证不了类的封装性,类中的对象全部公开了

注:operator==(d1, d2);和d1 == d2是一样的

我们看看底层实现(汇编代码):

这里有两个办法

1.将运算符重载函数写成日期类成员函数,写到类中不会受访问限定符的影响

2.在日期类中声明operator==为友元函数

第二个办法我们后面在讲,这里说明第一个方法

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

	bool operator==(const Date& d2)
	{
		return _year == d2._year
			&& _month == d2._month
			&& _day == d2._day;
	}

	void Print()
	{
		cout << _year << ' ' << _month << ' ' << _day << endl;
	}

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

int main()
{
	Date d1(2024, 9, 10);
	Date d2(2024, 9, 10);

	//d1.operator==(d2);与下面那个效果一样
	int ret = d1 == d2;、
	cout << ret << endl;
	return 0;
}

写成成员函数要注意operator==的有操作数是this指针,所以operator==函数参数只有一个,但实际上有两个,只是this指针不能显示在参数表和函数调用中(形参和实参)

12.2 赋值运算符重载

1. 赋值运算符重载格式

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

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

检测是否自己给自己赋值

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

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)
		{
			_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(2024, 9, 10);
	Date d2;

	d2 = d1;
	d2.Print();

	return 0;
}

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

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

// 赋值运算符重载成全局函数,注意重载成全局函数时没有this指针了,需要给两个参数
Date& operator=(Date& d1, const Date& d2)
{
	if (&d1 != &d2)
	{
		d1._year = d2._year;
		d1._month = d2._month;
		d1._day = d2._day;
	}

	return d1;
}

int main()
{
	Date d1(2024, 9, 10);
	Date d2;

	d2 = d1;
	d2.Print();

	return 0;
}

编译失败:error C2801: “operator =”必须是非静态成员

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

3. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注 意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符 重载完成赋值。

默认生成的赋值重载函数的作用和拷贝构造一样:

  1. 内置类型进行值拷贝/深拷贝
  2. 自定义类型调用它的赋值重载函数
class Time //我这里将std全展开了,如果写time会和库中的time冲突
{
public:
	Time(int hour = 1, int minute = 1, int second = 1)
	{
		_hour = hour;
		_minute = minute;
		_second = second;
	}
	Time& operator=(const Time& T)
	{
		if (this != &T)
		{
			_hour = T._hour;
			_minute = T._minute;
			_second = T._second;
		}
		
		return *this;
	}

//private:
	int _hour;
	int _minute;
	int _second;
};

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;
		cout << _T._hour << ' ' << _T._minute << ' ' << _T._second << endl;
	}

private:
	int _year;
	int _month;
	int _day;

	Time _T;
};

int main()
{
	Date d1(2024, 9, 10);
	Date d2;

	d2 = d1;
	d2.Print();

	return 0;
}

总结:

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

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

2.赋值前检查是否在给自己赋值

C++支持d1 = d1这样赋值的情况,这种赋值是没有意义的,这种操作只会增加空间复杂度,所以我们可以在赋值函数中增加一个判断语句,如果是d1 = d1这种情况,返回d1就可以了

3.函数的返回值使用引用返回

实际上若是出现d2 = d1;这种情况我们没有必要有返回值,因为在函数内已经通过this指针改变了对象d2,但是为了支持连续赋值d3 = d2 = d1 ,我们就需要为函数设置一共返回值,显而易见的我们应该返回运算符的左操作数,即*this

如果返回的*this出作用域不会被销毁,那么为了避免不必要的消耗,最好使用引用返回

4.参数类型设置为引用,并使用const修饰

 赋值运算符重载函数第一个参数默认是this指针,第二个参数是运算符的右操作数

由于是自定义类型,我们若使用传值传参,这里系统就会自动调用一次拷贝构造函数,所以我们这里使用引用传递(第一个参数this是不显示的,我们管不了)

第二个操作数原本就是给第一个操作数赋值的,所以这里我们还能在它前面加上一个const防止它的数据被篡改

5.拷贝构造函数和赋值运算符

拷贝构造函数:用一个已经存在的对象初始化另一个对象

赋值运算符重载:两个存在对象之间的赋值拷贝

例:

int main()
{
    Date d1(2024, 9, 11);
    Date d2(d1);  //或 d2 = d1; 拷贝构造函数
    Date d3(2024, 9, 12);
    d3 = d1; // 或d3.operator(d1); 赋值运算符重载函数
    return 0;
}

13.const成员

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

 

class Date
{
public:
    void Print() const
    {
        cout << _year << "年" << _month 
        << "月" << _day << "日" << endl;
    }
private:
    int _year;
    int _month;
    int _day;
}

实际编译器的处理,这段代码只是给予理解的,实际上this指针是不能显现的

class Date
{
public:
    void Print(const Date *this) 
    {
        cout << _year << "年" << _month 
        << "月" << _day << "日" << endl;  
    }
private:
    int _year;
    int _month;
    int _day;
}

请思考下面的几个问题:

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

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

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

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

解释:

1. 我们将const对象传进非const函数中,函数中的this指针没有被const修饰,而我们传进来的形参是被const所修饰的不能随意篡改内容,但是一旦传进函数中,形参的权限就被放大了,不受const的限制了

2.将非const对象传进const成员函数中,属于权限的缩小,这样是可行的

3.在const成员函数内调用非const成员函数,是将被const修饰的指针this的值赋值给另一个非const成员函数,这属于权限的放大,不可行

4.在非const成员函数内调用const成员函数,是将指针this的值赋值给另一个const成员函数,这属于权限的缩小,是可行的

14.取地址以及const取地址操作符重载

这两个默认成员函数一般不用重新定义 ,编译器默认会生成。

class Date
{ 
public:
    Date* operator&()
    {
        return this ;
    }
    const Date* operator&()const
    {
         return this ;
    }
private :
    int _year ; // 年
    int _month ; // 月
    int _day ; // 日
};

这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需 要重载,比如想让别人获取到指定的内容!

15.日期类的实现

Date.h

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1);
    //获取这个月的天数
	int GetMonthDay(int _year, int _month);
    //日期 += 天数
	Date& operator+=(int day);
    //日期 -= 天数
	Date& operator-=(int day);
    //日期 + 天数
	Date operator+(int day) const;
    //日期 - 天数
	Date operator-(int day) const;
    //日期 == 日期
	bool operator==(const Date& d) const;
    //日期 >= 日期
	bool operator>=(const Date& d) const;
    //日期 > 日期
	bool operator>(const Date& d) const;
    //日期 <= 日期
	bool operator<=(const Date& d) const;
    //日期 < 日期
	bool operator<(const Date& d) const;
    //日期 != 日期
	bool operator!=(const Date& d) const;
    //日期 - 日期
    Date operator-(const Date& d);
    //前置++
    Date& operator++();
    //后置++
    Date operator++(int);
	void Print();

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

Date.cpp

Date::Date(int year, int month, int day)
{
	if (month > 0 && month < 13 
		&& day > 0 && day <= GetMonthDay(month, day))
	{
		_year = year;
		_month = month;
		_day = day;
	}
	else
	{
		cout << "无效天数,请重新输入" << endl;
		assert(false);
	}
}

//获取这个月的天数
int Date::GetMonthDay(int year, int month)
{
	int MonthDay[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31, };

	if (month == 2 && (year % 4 == 0 && year % 100 != 0 || year % 400 == 0))
	{
		return 29;
	}
	else
	{
		return MonthDay[month];
	}
}

//日期 - 天数
Date& Date::operator+=(int day)
{
	if (day < 0)
	{
		return *this -= -day;
	}

	_day += day;
	while (_day > GetMonthDay(_year,_month))
	{
		if (_month == 13)
		{
			++_year;
			_month = 1;
		}
		_day -= GetMonthDay(_year, _month);
		++_month;
	}

	return *this;
}

//日期 -= 天数
Date& Date::operator-=(int day)
{
	if (day < 0)
	{
		return *this += -day;
	}

	_day -= day;
	while (_day < 0)
	{
		_day += GetMonthDay(_year, _month);
		--_month;
		if (_month == 0)
		{
			--_year;
			_month = 12;
		}
	}

	return *this;
}

//日期 + 天数
Date Date::operator+(int day) const
{
	//Date tmp = *this;
	//tmp += 100;
	//return tmp;

	Date tmp = *this;
	tmp._day += day;
	while (tmp._day > GetMonthDay(tmp._year, tmp._month))
	{
		tmp._day -= GetMonthDay(tmp._year, tmp._month);
		++tmp._month;
		if (tmp._month == 13)
		{
			++tmp._year;
			tmp._month = 1;
		}
	}

	return tmp;

}

//日期 - 天数
Date Date::operator-(int day) const
{
	//Date tmp = *this;
	//tmp -= 100;
	//return tmp;

	Date tmp = *this;
	tmp._day -= day;

	while (tmp._day < 0)
	{
		tmp._day += GetMonthDay(tmp._year, tmp._month);
		--tmp._month;
		if (tmp._month == 0)
		{
			--tmp._year;
			tmp._month = 12;
		}
	}
	return tmp;
}

//日期 == 日期
bool Date::operator==(const Date& d) const
{
	return _year == d._year
		&& _month == d._month
		&& _day == d._day;
}

//日期 != 日期
bool Date::operator!=(const Date& d) const
{
	return !(*this == d);
}

//日期 >= 日期
bool Date::operator>=(const Date& d) const
{
	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;
	}

	return false;
}

//日期 <= 日期
bool Date::operator<=(const Date& d) const
{
	return *this < d || *this == d;
}

//日期 < 日期
bool Date::operator<(const Date& d) const
{
	return !(*this >= d);
}

//日期 > 日期
bool Date::operator>(const Date& d) const
{
	return !(*this <= d);
}

//前置++,没有创建新的变量
Date& Date::operator++() 
{
	*this += 1;
	return *this;
}

//后置++
//创建了两个新对象,因为需要构成重载,所以在后置++上的参数表中需要带上int
//一般前置++用的多一些,所以在后置上加int
Date Date::operator++(int)
{
	Date tmp = *this;
	*this += 1;

	return tmp;
}

//日期 - 日期
Date Date::operator-(const Date& d)
{
	Date max = *this;
	Date min = d;
	int flag = 1;

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

	int n = 0;
	while (max != min)
	{
		++min;
		++n;
	}

	return n * flag;
}

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

 16 流插入和流提取运算符重载

我们知道cin和cout是在iostream库中的

通过这张图可知cin是定义istream中,而cout定义在osteam中

看图:

这张图解释了

cout会自动识别内置类型,是因为它会去调用与之对应的运算符重载函数。

cout可以直接支持内置类型,是因为它已经在库中实现了

16.1自定义类型流插入的实现

class Date
{
public:
    void operator<<(ostream& out)
    {
	    out << _year << "年" << _month << "月" << _day << "日" << endl;
    }
private:
    int _year;
    int _month;
    int _day;
};

int main()
{
    Date d1(2024, 9, 10);
    //d1.operator<<(cout);
    d1 << cout;
    return 0;
}

但是这样看起来是不是很奇怪我们能不能将d1<<cout 改为 cout<<d1;

将operator<<函数改为全局函数

class Date
{
private:
    int _year;
    int _month;
    int _day;
};

void operator<<(ostream& out,const Date& d)
{
	out << _year << "年" << _month << "月" << _day << "日" << endl;
}

int main()
{
    Date d1(2024, 9, 10);
    //d1.operator<<(cout);
    d1 << cout;
    return 0;
}

这里写成全局函数有两个要注意的点

1. 因为日期类中的成员变量是私有的,不能在类外使用

2. 写成全局函数,参数需要都显示出来

这里有两个解决方法:

1. 我们可以将类成员变量写成函数 

例:

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

    int getyear()
    {
        return _year;
    }

    int getmonth()
    {
        return _month;
    }

    int getday()
    {
        return _day;
    }
private:
    int _year;
    int _month;
    int _day;
};

void operator<<(ostream& out,const Date& d)
{
	out << d.getyear() << "年" << d.getmonth() << "月" << d.getday() << "日" << endl;
}

int main()
{
    Date d1(2024, 9, 10);
    //cout.operator<<(d1);
    cout << d1;
    return 0;
}

2.在类中声明友元函数

class Date
{
//声明友元函数
friend void operator<<(ostream& out, const Date& d);
public:
    Date(int year = 1, int month = 1, int day = 1)
    {
		_year = year;
		_month = month;
		_day = day;
	}
private:
    int _year;
    int _month;
    int _day;
};

void operator<<(ostream& out,const Date& d)
{
	out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
}

int main()
{
    Date d1(2024, 9, 10);
    //cout.operator<<(d1);
    cout << d1;
    return 0;
}

声明成友元函数就可以使用类中的资源了

cout连续打印:cout << d1 << d2 << d3; ,这时重载函数就需要一个返回值来完成这一操作

cout << d1 << d2 << d3;的顺序是  d1先输出,然后d2,d3。它们的返回值是cout 即:ostream

连续赋值的函数实现:

class Date
{
//声明友元函数,在类中哪定义都可以
friend ostream& operator<<(ostream& out, const Date& d);
public:
    Date(int year = 1, int month = 1, int day = 1)
    {
		_year = year;
		_month = month;
		_day = day;
	}
private:
    int _year;
    int _month;
    int _day;
};

ostream& operator<<(ostream& out,const Date& d)
{
	out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
    
    return out;
}

int main()
{
    Date d1(2024, 9, 10);
    Date d2, d3;
    cout << d1 << d2 << d3;

    return 0;
}

16.2 自定义类型流提取的实现

class Date
{
//声明友元函数,在类中哪定义都可以
friend istream operator>>(istream& in, Date& d);
public:
    Date(int year = 1, int month = 1, int day = 1)
    {
		_year = year;
		_month = month;
		_day = day;
	}
private:
    int _year;
    int _month;
    int _day;
};

istream& operato>>(istream& in, Date& d)
{
    int year, month, day;
	in >> year >> month >> day;
    
    if(month > 0 && month < 13
    && day > 0 && day < d.GetMonthDay(year, month))
    {
        d._year = year;
        d._month = month;
        d._day = day;
    }

    return in;
}

int main()
{
    Date d1(2024, 9, 10);
    Date d2, d3;
    cin >> d1 >> d2 >> d3;
    cout << d1 < d2 << d3;

    return 0;
}

istream& operato>>(istream& in, Date& d);这条语句中的参数都不可以加const

in是向对象d中写数据,d不能加const

写数据的过程中会改变in中的一些状态值,in不能加const

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值