类和对象 01【C++】

目录

  • 一、 类的引入
    • 1. 类的定义
    • 2. 类的访问限定符及封装
      • (1) 访问限定符
      • (2) 封装
    • 3. 类的实例化
    • 4. 类对象模型
      • (1) 计算类对象的大小
      • (2) 类对象的存储方式
    • 5. this指针
  • 二、 类的6个默认成员函数
    • 1. 构造函数
    • 2. 析构函数
    • 3. 拷贝构造函数
    • 4. 赋值运算符重载
    • 5. 取地址重载
    • 6. const取地址重载

前言:

C语言是面向过程的,关注的是过程,逐步解决问题的过程。

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

一、 类的引入

C语言中的结构体里面只能定义变量,而C++的结构体里面不仅可以定义变量,也可以定义函数。

struct Node 
{
	//成员变量
	int a;
	int b;

	//成员函数
	void Print() 
	{
		cout << "C++ class" << endl;
	}
};

1. 类的定义

class className //className 是类名
{
	//类的内容 由 成员变量和成员函数组成
	//...

};	//注意这里的分号

在这里插入图片描述

在成员函数(类的方法)中,根据人为的一些规则来区分形参和成员变量

例如: 定义日期类的时候

class Date
{
	void Init(int year, int month, int day)
	{
		year = year;	//这里我们就会比较容易搞混
		month = month;
		day = day;
	}

	int year;
	int month;
	int day;
};

所以为方便区分参数和成员变量,我们通常会给成员变量前加上一个下划线 _成员变量 (其他方式也可以) 来区分

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

	int _year;	//通过 _成员变量 来区分
	int _month;
	int _day;
};

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

(1) 访问限定符

在这里插入图片描述

  • public 修饰的成员可以在类的外面直接被访问
  • protect 和 private 修饰的成员在类外面不能直接被访问
  • 访问权限的作用域:从访问限定符开始直到下一个访问限定符出现为止
  • 如果后面没有访问限定符,作用域就到 } (即类的结束)
  • 在没有写访问限定符时,class的默认访问为private,struct 的默认访问为public

例如:

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

	int _year;	//通过 _成员变量 来区分
	int _month;
	int _day;
};

int main() 
{
	Date d1;
	d1.Init(2024,1,31);		//error class的默认访问为private
	return 0;
}

改正:加上public

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

	int _year;	//通过 _成员变量 来区分
	int _month;
	int _day;
};

上述的成员变量和成员函数都可以访问到了。

C++ 中 struck 也是可以上访问限定符的

int main() 
{
	// struct Date d;
	// class Date d; 这样写也可以
	Date d1;	//但是一般为了方便会把这里的class给省略
	return 0;
}

关于类的定义两种方式:

  1. 声明和定义全部放在类体中,当成员函数如果在类中定义,编译器可能会将其当成内联函数处理
  2. 类的声明放在 . h 文件按中,成员函数定义放在 . cpp 文件中

在第二种方法中 涉及到 (命名空间)类域

类的作用域: 类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用 ::作用域操作符指明成员属于哪个类域

//test.h
// 声明
class Date
{
private:
	//成员变量
	int _year;	//通过 _成员变量 来区分
	int _month;
	int _day;
public:
	//成员函数
	void Init(int year, int month, int day);
	void Print_Date();
};

//test.cpp
//函数定义
void Date::Init(int year, int month, int day)	//注意这里的Date类域,说明是Date类中的成员函数
{
	_year = year;
	_month = month;
	_day = day;
}

void Date::Print_Date() //加上Date类域
{
	cout << _year << "/" << _month << "/" << _day << endl;
}

// 上述 先去局部去找,再去全局找,最后去Date类中找

C++中struct 和 class 的区别:

相同点:类的成员都可以有成员函数

不同点:struct 定义类的默认访问为 public (兼容C语言) ;class 定义类的默认访问为 private。

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

(2) 封装

面向对象的三大特性:封装、继承、多态

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

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

3. 类的实例化

用类的类型创建对象的过程,称为类的实例化。通过实例化,我们可以得到一个具有类定义属性和方法的对象,并且可以对这个对象进行操作。

  • 声明出一个类并没有分配实际的内存空间
  • 一个类可以实例化出多个对象,实例化出的对象,占用实际的物理空间,存储类成员变量
  • 设计出类但没有进行实例化并不占内存空间
	Date._day;	// error Date类是没有空间,只有实例化出对象才有具体年龄
//实例化对象的正确方法
	Date d1;	//定义对象,实例化
	

( 定义的一个标志 是:开空间 )

4. 类对象模型

(1) 计算类对象的大小

class A 
{
public:
	void fu1() {}
private:
	int _a;
};

class B 
{
public:
	void fu1() {}
};

class C 
{};
int main() 
{
	A a;
	B b;
	C c;
	cout << sizeof(a) << endl;	// 4
	cout << sizeof(b) << endl;	// 1
	cout << sizeof(c) << endl;	// 1
	return 0;
}

通过计算类的对象,一个类的大小,就是该类成员变量之和,前提在内存对齐下

空类的大小是1,编译器给空类一个字节来标识这个类的对象

结构体内存对齐规则:

  • 首先第一个成员在 结构体偏移量为0的地址处
  • 其他成员变量要对齐到(对齐数)的整数倍处,对齐数 = 编译器默认对齐数大小 与 该成员大小 之中的较小值
  • 结构体的总体大小为:最大对齐数( 就是所有变量类型最大者 与 默认默认对齐数中较小的一方) 的整数倍
  • 如果嵌套了结构体,嵌套的结构体对齐到自己最大对齐数 的整数倍,结构体的整体大小就是所有最大对齐数(包括嵌套结构体的对齐数) 的整数倍

内存对齐:会导致空间浪费,为什么要内存对齐呢?请猛击

(2) 类对象的存储方式

	Date d1;
	Date d2;

	d1._day;
	d2._day;

	//定义出的变量地址不同
	d1.Print_Date();
	d2.Print_Date();
	// d1 与 d2 的 Print_Date地址相同

在这里插入图片描述

存储方式

在这里插入图片描述

例题:思考下方运行结果

	Date a1;
	Date* p1 = &a1;
	p1->f1();

	Date* p2 = nullptr;
	p2->f1();

	//A、编译错误
	//B、运行错误
	//C、运行正常

答案是 运行正常,因为 虽然有箭头,但没有进行解引用( f1并没有在p2指向的内存空间里面,而是在公共代码区)

5. this指针

先看下方问题:

在这里插入图片描述

这里我们首先要知道

在这里插入图片描述

所以去访问实例对象的,那么C++又是怎样分出d1和d2的呢?

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

编译器处理

在这里插入图片描述

所以是通过隐藏的this指针来进行区分d1和d2的

this指针存在哪里? 我们发现this指针出现在函数的形参中,又形参和局部变量 存在栈帧里面,所以this指针存在栈里面

// 1.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class A
{
    public:
    void Print()
    {
        cout << "Print()" << endl;
    }
    private:
    int _a;
};
int main()
{
    A* p = nullptr;
    p->Print();
    return 0;
}

//答案是 C 正常运行

原因是:Print函数在公共代码区,不在p指向的内存空间中,所以并没有触发p指针解引用去内存找,Print函数地址在编译的时候就有了。

如果 (p->_a++; 这里会造成 B、运行崩溃)

如果(*p).Print(); 答案是 正常运行和p->Print();一样

// 下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行
class A
{
    public:
    void Print()
    {
        cout << _a << endl;
    }
    private:
    int _a;
};
int main()
{
    A* p = nullptr;
    p->Print();
    return 0;
}

//答案是 B

this指针的传递是没有问题的,但是空指针的访问会导致运行崩溃

小总结

  • Date this 其实是 Date const this** ,即this指针指向的内容可以进行修改,但是this指针本身是不可以改变的,不能给this指针赋值

  • 隐藏的this指针我们不能显示写出来,但是在函数内部可以用

    	void Print_Date()
    	{
    		cout << this->_year << "/" << this->_month << "/" << this->_day << endl;
    	}
    
    	//包括
    	void Init(int year, int month, int day)
    	{
    		this->_year = year;
    		this->_month = month;
    		this->_day = day;
    	}
    
  • this指真本质上是"成员函数"的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针

  • this指针是"成员函数"第一个隐含的指针形参,

  • 一般情况由编译器通过 ecx 寄存器自动传递,不需要用户传递

  • 隐藏的this指针我们不能显示写出来,但是在函数内部可以用

    	void Print_Date()
    	{
    		cout << this->_year << "/" << this->_month << "/" << this->_day << endl;
    	}
    
    	//包括
    	void Init(int year, int month, int day)
    	{
    		this->_year = year;
    		this->_month = month;
    		this->_day = day;
    	}
    
  • this指真本质上是"成员函数"的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针

  • this指针是"成员函数"第一个隐含的指针形参,

  • 一般情况由编译器通过 ecx 寄存器自动传递,不需要用户传递

二、 类的6个默认成员函数

1. 构造函数

当一个类什么成员都没有的时候称为空类
实际上空类并不是什么的都没有,当类在什么都没有写时,编译器会自动生成6个默认成员函数。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。

class Date{};

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

构造函数虽然名字叫构造,但实际上主要任务并不是开空间创建对象,而是初始化对象

注意:

  • 函数名与类名相同
  • 没有返回值(返回类型不用写)
  • 对象实例化时编译器自动调用对应的构造函数
  • 构造函数可以重载
#include<iostream>

using namespace std;

class Date
{
public:

	//无参的构造函数
	Date()	//特殊的成员函数
	{
		_year = 1;
		_month = 2;
		_day = 3;
	}

	//带参数的构造函数
	Date(int year,int month,int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}

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

int main()
{
	Date d1;	//对象实例化的时候 编译器自动调用无参构造函数
	d1.Print_Date();

	Date d2(1,1,1);	//调用带参的构造函数
	d2.Print_Date();
	return 0;
}

在这里插入图片描述
无参构造Date d1(); 不能这样的的原因是:不能与函数声明区分开 Date func(); 这样无参数的函数声明区分开,而带参是可以区分开的 因为Date func(int…);这里是有类型的

注意
C++规定对象实例化的时候必须调用构造函数,如果类中没有显示定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显示定义了,编译器将不再生成。

对象实例化需要调用相应的构造函数吗,对于上述的两个构造函数其实也是可以通过缺省函数写成一个

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

当没有显示定义构造函数的时候

	Date d1;	
	d1.Print_Date();

打印
在这里插入图片描述
发现打印结果竟然是随机值(没有初始化)
原因是:
C++把类型分为 内置类型(基本类型) 和 自定义类型。内置类型就是语言提供的数据类型,如:int,float…;自定义类型 使用 class/struct/union 等自己定义的类型。C++98规定默认生成的构造函数对内置类型不做处理,自定义类型回去调用自己的默认构造


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_Date()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}

private:
	//内置类型(基本类型)
	int _year = 1;	//类声明时给值
	int _month = 1;
	int _day = 1 ;

	//自定义类型
	Time _t;

};

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

打印:
在这里插入图片描述

默认生成的构造函数对内置类型不做处理,自定义类型回去调用自己的默认构造
然而,内置类型没有初始化
所以 在C++11中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值

在这里插入图片描述
分析一个类型成员和初始化需求:

  • 需要写构造函数我们就自己写;不需要时就是用编译器自己生成的
  • 对于绝大多数情况下都需要我们自己实现构造函数

严格上讲默认构造函数有三种:
第一个是编译器默认生成的,第二种是无参构造函数,第三种是全缺省构造函数

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


	void Print_Date() 
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	d1.Print_Date();
	return 0;
}

当编译上述代码时:
在这里插入图片描述
因为写了构造函数,编译器就不会自动生成相应的默认构造函数,因为对于无参和全缺省只能存在一个,一般情况下,建议提供全缺省构造函数

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

2. 析构函数

(1) 析构函数的概念
析构函数:析构函数不是对对象本身销毁,局部对象销毁工作是由编译器完成的,而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作

(2) 析构函数时特殊的成员函数,其特征如下:

  • 析构函数名是在类名前加上字符~
~Date(){}	//析构函数
  • 析构函数没有参数,没有返回值类型
  • 一个类只能有一个析构函数;若没有显示定义,系统会自动生成默认的析构函数;注意析构函数不能重载
  • 当对象生命周期结束时,C++编译时系统会自动调用析构函数。
    调用析构函数的小细节:
    因为局部变量和函数一般是在函数栈帧里面的,即析构函数,后定义的先析构
class Date 
{
public:
	Date(int year = 1)
	{
		_year = year;
	}
	~Date()
	{
		cout << "~Date()->" <<_year<< endl;	//调用析构的时候
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(1); //析构函数 后 定义的 先 析构
	Date d2(2);
	return 0;
}

打印结果:
在这里插入图片描述
根据打印结果也发现,后定义的先进行析构

	Date d1(1); //析构函数 后 定义的 先 析构
	Date d2(2);
	static Date d3(3);

打印结果:
在这里插入图片描述
上述增加了一个 static Date d3(3); 却没有按照后定义的先进行析构,以为它们存放的区域是不同的,static Date d3(3);(存放在静态区的)其生命周期是全局的,所以main函数销毁的时候会调用d3的析构函数,当然在调用d3的析构前,需要先去销毁局部的
再次修改一下:

void func() 
{
	Date d3(3);	//局部的
	static Date d4(4);	//全局的
}
int main()
{
	Date d1(1); //析构函数 后 定义的 先 析构
	Date d2(2);
	func();
	return 0;
}

打印结果:
在这里插入图片描述
析构,先局部再全局
小总结:
析构函数的调用顺序:
局部对象(后定义的先析构) --> 局部的静态对象 --> 全局对象(后定义的先析构)

  • 默认生成的析构函数和构造函数类似
    内置类型不做处理,自定义类型的成员去调用它的析构函数
  • 局部成员,即函数需要进入函数栈帧,所以析构函数是后定义的先析构( 满足后进先出 )

分析下方代码:

#include<iostream>

using namespace std;

class Time 
{
public:
	~Time() 
	{
		cout << "Time()" << endl;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date 
{
public:
	Date(int year = 10, int month = 10, int day = 10)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print_Date() 
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}

private:
	//内置类型
	int _year = 1;
	int _month = 1;
	int _day = 1;

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

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

程序运行结果:

Time()

发现main方法中没有直接创建Time类对象,最后却调用了Time类的析构函数
原因是:在main函数中创建了Date对象 d1,而d1中包含4个成员变量,其中_year,_month,_day是内置类型成员,而_t是Time类对象,所以在d1销毁时,要将的d1中包含的Time类的_t对象进行销毁,所以要调用Time类的析构函数。
所以不能直接调用Time类的析构函数,首先是编译器先生成一个Date类的默认的析构函数,进而在其内部调用Time类的析构函数。
要保证其内部每个定义对象都可以正确销毁

3. 拷贝构造函数

在类和对象中,我们可能会想在创建对象时,可否创建一个 与 已经存在对象一模一样的新对象呢?
拷贝构造函数:在C++中,拷贝构造函数是一种特殊的构造函数,用于创建并初始化一个对象,作为另一个已经存在对象的副本。当创建对象需要基于已存在对象的内容时,拷贝构造函数就会被调用。
例如:

#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_Date()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
	//将d1拷贝构造给d2
	Date(Date & d)	//拷贝构造,这里必须使用引用,Date(Date d)会导致无穷递归的
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
private:
	int _year = 1;
	int _month = 1;
	int _day = 1;
};

int main() 
{
	Date d1(1970,1,1);
	Date d2(d1);	//拷贝构造用一个相同类型的其他对象(即,日期类 类型对象)进行构造
	return 0;
}

调试查看结果发现;
在这里插入图片描述
完成了拷贝构造

上述的拷贝构造函数形参为什么使用引用呢?
因为,C++中,对于自定义的类型传值传参时都会调用拷贝构造

首先先看 传值 传参 和 传引用 传参

关于传值传参调用拷贝构造
在这里插入图片描述
传引用传参就不用调用拷贝构造函数
在这里插入图片描述

当使用传值传参的时候,再去调用拷贝构造引起的无穷递归的原因是:
调用拷贝构造,要先传参,这里因为是传值传参,会形成一个新的拷贝构造。当按值传递对象到函数(包括拷贝构造函数)时,C++默认会调用拷贝构造函数来创建函数参数的副本。如果拷贝构造函数的实现本身又试图使用按值传递的方式来复制其参数,这就会导致无穷递归。
如图:
在这里插入图片描述
所以 使用引用可以解决无穷递归问题
在这里插入图片描述
当我们使用引用后,但是不想因为引用取别名去修改原来的值,所以使用了const进行常量修饰,相当于缩小了权限

	Date(const Date &d)	//const进行修饰,让指向的内容只能读,不能修改
	{
		this->_year = d._year;
		this->_month = d._month;
		this->_day = d._day;
	}

拷贝构造与普通构造函数的一个区别:
先看下方代码:

//拷贝构造与构造的一个区别
#include<iostream>

using namespace std;

class Date 
{
public:
	void Print_Date() 
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}

	//构造函数
	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(1970,1,1);
	Date d2(d1);

	d1.Print_Date();
	d2.Print_Date();

	return 0;
}

打印结果;
在这里插入图片描述
上述代码中并没有写拷贝构造,调用d2的Print_Date时候发现,打印结果和d1的一样
我们知道,拷贝构造也是默认成员函数,当我们没有手动写时,编译器会生成一个默认成员函数。
当没有写任何的构造函数,编译器会默认生成;写了任何构造函数,编译器不在生成构造函数。
构造函数:编译自动生成的,对于内置类型不做处理,自定义类型去调用它的默认构造函数。C++11的补丁 内置类型成员变量在类中声明时可以给默认值
拷贝构造函数:
对默认生成的内置类型进行处理
对于自定义类型,当写了拷贝构造函数,没有写构造函数发现;
在这里插入图片描述
我们可以使用

	//强制生成构造函数
	Time() = default;
class Time 
{
public:
	~Time() 
	{
		cout << "~Time() " << endl;
	};
	//强制生成构造函数
	Time() = default;
	//拷贝构造函数
	Time(const Time& d)
	{
		cout << "Time(const Time& d)" << endl;
		_hour = d._hour;
		_minute = d._minute;
		_second = d._second;
	}
private:
	int _hour;
	int _minute;
	int _second;
};

小总结:

  • 拷贝构造函数是构造函数的一个重载形式
  • 拷贝构造函数的参数只有一个,必须是类 类型对象的引用,如果使用传值的方式编译器直接报错(引发了无穷递归)
  • 当没有显示定义,编译器会生成默认的拷贝构造函数,内置类型是按照字节序方式直接拷贝的,而自定义类型是调用相应的拷贝构造函数完成的。
  • 像上述日期类是没必要显示实现拷贝构造函数的,但是像malloc动态开辟空间(在堆上)需要我们显示实现拷贝构造函数。
    注意:在(堆空间)不能浅拷贝(会造成二次free,容易出错),需要进行深拷贝。
    例如:
    浅拷贝:
    在这里插入图片描述
    深拷贝:
//深拷贝
	Stack(const Stack& s) 
	{
		DataType* tmp = (DataType*)malloc(s._capacity*(sizeof(DataType)));
		if (tmp == nullptr)
		{
			perror("malloc fail");
			exit(-1);
		}
		memcpy(tmp,s._a,s.size*(sizeof(DataType)));
		_a = tmp;
		_size = s.size;
		_capacity = s._capacity;
	}

在这里插入图片描述

4. 赋值运算符重载

前言:
先看一段代码:

#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 DateEqual(const Date& x,const Date& y) 
{
	return x._year == y._year
		&& x._month == y._month
		&& x._day == y._day;
}

bool DateLess(const Date& x,const Date& y) 
{
	if (x._year < y._year) 
	{
		return true;
	}
	else if (x._year == y._year)
	{
		if (x._month < y._month) 
		{
			return true;
		}
		else if (x._month == y._month)
		{
			//if (x._day < y._day) 
			//{
			//	return true;
			//}
			return x._day < y._day;
		}
	}
	return false;
}

int main() 
{
	Date d1(1970,2,1);
	Date d2(1970,2,8);
	DateEqual(d1,d2);	//比较日期是否相等
	DateLess(d1,d2);	//判断d1对象的日期是否小
	cout << DateEqual(d1, d2) << endl << DateLess(d1, d2) << endl;
	return 0;
}

上述代码,我们发现,对于自定义类型的比较,我们需要函数来实现,但是实现这些函数存在一些问题,给函数取名的问题,会造成代码的可读性变差。

C++为了增加代码的可读性,增加了运算符重载,运算符重载是具有特殊函数名的函数。有和普通函数类似的返回值类型,函数名字,参数列表

// 返回值类型 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;
	}
	int _year;	//注意内置类型是公有的
	int _month;
	int _day;
};

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)
	{
		if (x._month < y._month)
		{
			return true;
		}
		else if (x._month == y._month)
		{
			return x._day < y._day;
		}
	}
	return false;
}

int main()
{
	Date d1(1970, 2, 1);
	Date d2(1970, 2, 8);
	//运算符重载  返回值类型 operator操作符(参数列表...){}
	//参数顺序最好保持一致
	cout << operator==(d1,d2) << endl; //cout << ( d1 == d2 )<< endl;
	cout << operator<(d1, d2) << endl; //cout << ( d1 < d2 ) << endl;

	cout << ( d1 == d2 )<< endl;	//注意要加上括号,因为<<优先级比较高	
	cout << ( d1 < d2 ) << endl;
	//发现上述自定义类型也是可以使用运算符的
	return 0;
}

在这里插入图片描述
注意:上述的内置类型变成了公有,因为函数实现在类的外面

当然运算符重载函数可以放到类里面
看下方代码

#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& 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) 
		{
			if (_month < y._month) 
			{
				return true;
			}
			else if (_month == y._month)
			{
				return _day < y._day;
			}
		}
		return false;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(1970,1,1);
	Date d2(2000,1,1);
	cout << d1.operator==(d2) << endl;
	cout << d1.operator<(d2) << endl;

	cout << (d1 == d2) << endl;	//cout << d1.operator==(d2) << endl;
	cout << (d1 < d2) << endl;	//cout << d1.operator<(d2) << endl;

	return 0;
}

在这里插入图片描述
注意:

  • 重载操作符必须有一个类 类型参数
  • 作为类成员函数重载时,形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
  • .* :: sizeof ?: . 以上五个运算符不能重载

上述的 .* 操作符的一个使用介绍:

class ob 
{
public:
	void func() 
	{
		cout << "void func()" << endl;
	}
};
//我们平时 typedef void(*)() pobfunc;  
//函数指针和数组指针需要特殊定义
typedef void (ob::*pobfunc)();	//函数指针(成员函数指针)
int main() 
{
	pobfunc p = &ob::func;	//注意这里的取地址&
	ob temp;
	//对于普通的函数指针我们可以(*p)();去调用函数
	(temp.*p)();	//这里使用 .*是去调用成员函数 (目的是把this传过去)
	
	return 0;
}

赋值运算符重载

	d1 = d2;		//已经存在的对象,一个拷贝赋值给另一个,重载运算符

赋值运算符重载格式

Ⅰ 参数类型: const T&

对于内置类型的连续赋值每次赋值都会有返回值
赋值运算符重载有返回值类型: T& ,有返回值类型的目的是为了支持连续赋值

	//...
	Date& operator=(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
		
		return *this;	//注意这里返回的是*this,不是this(因为this是指针)
	}

在这里插入图片描述
如果上述使用 operator=返回Date,编译器可能需要创建一个临时对象来返回结果,这样可能会导致不必要的拷贝操作,尤其对于在大型对象的情况下,甚至有可能会影响到程序的运行。

所以使用引用Date&返回,可以避免复制这些对象,避免不必要的拷贝,支持连续赋值操作

Ⅲ 检测是否自己个自己赋值:

	//可能会出现自己给自己赋值的情况
	//d1 = d1;

所以为了避免自己给自己赋值的情况:

	Date operator=(const Date& d)
	{
		if (this != &d)	//防止自己给自己赋值,注意这里是取地址
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
		return *this;	//注意这里返回的是*this,不是this(因为this是指针)
	}

返回*this:要进行链式赋值

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

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

因为:赋值运算符重载,当类中没有手动写的时候,编译器会生成一个默认的,此时类外手动生成的赋值重载全局函数,编译器调用的时候,不知道去调用哪一个。所以故赋值运算符重载只能是类的成员函数。

在这里插入图片描述
在这里插入图片描述
建议:写+的时候,去复用+=

5. 取地址重载

这个默认成员函数一般不用手动实现,由编译器默认生成

手动生成:

//取地址重载
//一般情况不需要我们手动实现,由编译器默认生成
class A 
{
public:
	A* operator&()
	{
		return this;//返回隐藏this指针
	}
};
int main() 
{
	A a1;
	cout << &a1 << endl;
}

特殊情况:不想要别人拿到我的地址

class A 
{
public:
	A* operator&()
	{
		//return this;//返回隐藏this指针
		return nullptr;	//注意这里返回空指针
	}
	const A* operator&() const
	{
		return this;
	}
};
int main() 
{
	A a1;
	const A a2;
	cout << &a1 << endl;
	cout << &a2 << endl;
}

在这里插入图片描述
上述的普通地址就获取不到了

6. const取地址重载

这个默认成员函数一般不用手动实现,由编译器默认生成

那么 手动生成:

//取地址重载
//一般情况不需要我们手动实现,由编译器默认生成
class A 
{
public:
	const A* operator&() const //第二个const修饰的是this
	{
		return this;
	}

};
int main() 
{
	const A a2;
	cout << &a2 << endl;
}

特殊情况:
让const取地址重载返回假的地址

class A 
{
public:
	A* operator&()
	{
		return this;//返回隐藏this指针
		//return nullptr;
	}
	const A* operator&() const
	{
		return (const A*)(0x0012ff40);
	}
};
int main() 
{
	A a1;
	const A a2;
	cout << &a1 << endl;
	cout << &a2 << endl;
}
	const A* operator&() const
	{
		int a = 1;
		return (const A*)&a;
		//return (const A*)(0x0012ff40);
	}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Android13 针对low memory killer内存调优

引入概念 在旧版本的安卓系统中&#xff0c;当触发lmk&#xff08;low memory killer&#xff09;的时候一般认为就是内存不足导致&#xff0c;但是随着安卓版本的增加lmk的判断标准已经不仅仅是内存剩余大小&#xff0c;io&#xff0c;cpu同样会做评判&#xff0c;从而保证设备…

nc开发刚导入项目eclipse出现莫名其妙的错误,红叉,感叹号,文件missing

解决类出现红叉 解决感叹号&#xff0c;文件missing 其他问题 右上角的视图&#xff0c;要选择java&#xff0c;如果是javaEE也会有一些文件没有展示出来。

Py之pydantic:pydantic的简介、安装、使用方法之详细攻略

Py之pydantic&#xff1a;pydantic的简介、安装、使用方法之详细攻略 目录 pydantic的简介 1、Pydantic V1.10 vs. V2 pydantic的安装 pydantic的使用方法 1、简单的示例 pydantic的简介 pydantic是使用Python类型提示进行数据验证。快速且可扩展&#xff0c;Pydantic与您…

MT8788|MTK8788安卓核心板参数_4G联发科MTK模块

MT8788核心板是一款功能强大的4G全网通安卓智能模块。该模块采用了联发科AIOT芯片平台&#xff0c;具有长达8年的生命周期。MT8788模块内置了12nm制程的八核处理器&#xff0c;包括4个Cortex A73和4个Coretex A53&#xff0c;主频最高可达2.0GHZ。标配内存为4GB64GB&#xff0c…

ARM 之十六 详解 CMSIS 版本变迁、各组件使用示例

目前,CMSIS 已经发展到了第六版,其目录结构也发生了重大的变化。在不断发展中,很多原来 CMSIS 的组件被不断独立出去,并因此成立了很多开源社区,今天就来学习一下! 由于 CMSIS 已经包含了相当丰富的文档,因此,本文重点学习版本之间的变化以及一些实际使用示例。 什么是…

Stable Diffusion 绘画入门教程(webui)

文章目录 一、前言二、做出的效果三、SD使用流程1、大模型2、关键字3、调参数 一、前言 随着mj和sd绘画软件发布之后&#xff0c;AI绘画开始爆火&#xff0c;很多小伙伴已经挖掘出很多的玩法&#xff0c;哪怕最基础的AI美女、AI壁纸、真人漫改等等都赚的盆满钵满&#xff0c;当…

(十一)【Jmeter】线程(Threads(Users))之setUp 线程组

简述 操作路径如下: 作用:在正式测试开始前执行预加载或预热操作,为测试做准备。配置:设置预加载或预热操作的采样器、循环次数等参数。使用场景:确保在正式测试开始前应用程序已经达到稳定状态,减少测试结果的偏差。优点:提供预加载或预热操作,确保测试的准确性。缺…

Linux常用指令汇总

还是计算机方面的基础知识。可能汇总的并不全面。 新话题的草稿已经打好了&#xff0c;明后天测试好了应该会发。 大家期待一下。 原文地址&#xff1a;Linux常用指令汇总 - Pleasure的博客 下面是正文内容&#xff1a; 主要因为我的基础实在太差了&#xff0c;自己总结了一…

Python 进阶语法:JSON

1 什么是 JSON&#xff1f; 1.1 JSON 的定义 JSON 是 JavaScript Object Notation 的简写&#xff0c;字面上的意思是 JavaScript 对象标记。本质上&#xff0c;JSON 是轻量级的文本数据交换格式。轻量级&#xff0c;是拿它与另一种数据交换格式XML进行比较&#xff0c;相当轻…

矩阵通课程,帮助你全方位学习搭建新媒体矩阵的技巧

从选择平台、确定矩阵布局&#xff0c;到打造运营团队、制定内容策略、复盘矩阵运营&#xff0c;矩阵通为企业带来新媒体矩阵建设全攻略&#xff0c;助力企业从零起步搭建矩阵。 了解课程详细内容&#xff0c;也可前往“新榜矩阵通”服务号查看。

Android轻量级进程间通信Messenger源码分析

一. 概述 Android中比较有代表性的两大通信机制&#xff1a;1. 线程间Handler通信 2. 进程间Binder通信&#xff0c;本篇文章中我们在理解AIDL原理的基础上来解读一下Messenger的源代码&#xff0c; 并结合示例Demo加深理解。 在看本篇文章前&#xff0c;建议先查阅一下笔者的…

Golang - 从源码到二进制:探索在国产CPU架构上交叉编译Minio的方法

文章目录 前置知识交叉编译Go 支持的所有操作系统和体系结构组合列出 Go 支持的所有操作系统和体系结构组合 大端、小端minio使用的go版本ABI 官方下载目标编译loongarch架构下的minio编译mipsle架构下的minio编译sw64架构下的minio 前置知识 交叉编译 交叉编译是指在一台主机…

ComfyUI 基础教程(十二):AI换脸神器 InstantID ComfyUI, 保姆级安装与使用测评

AI换脸技术发展迅速,roop,Reactor,ipadapter-faceid,photomaker,InstantID,换脸神器更新非常快。 对比lora训练,faceID,intantID,IPA,在人脸风格的迁移上,是目前AI换脸的主要方式。而最新出的InstantID,只需单个图像即可实现ID保留生成,相似度极高。 instantid特…

springboot项目之java.lang.NullPointerException: null问题

没有任何提示&#xff0c;只有一行报错: RuntimeException-[java.lang.NullPointerException] 问题定位: 最后发现是 controller文件&#xff0c;service定义一行少加了关键字 final 导致的&#xff0c; 补充之后就完美解决啦&#xff01; 参考 springboot项目之java.lang.Nu…

基于vue的个性化推荐餐饮系统Springboot

项目&#xff1a;基于vue的个性化推荐餐饮系统Springboot 摘要 现代信息化社会下的数据管理对活动的重要性越来越为明显&#xff0c;人们出门可以通过网络进行交流、信息咨询、查询等操作。网络化生活对人们通过网上购物也有了非常大的考验&#xff0c;通过网上进行点餐的人也…

软件实例分享,饭店餐饮会员卡管理系统怎么弄会员充值怎么记账

软件实例分享&#xff0c;饭店餐饮会员卡管理系统怎么弄会员充值怎么记账 一、前言 以下软件教程以 佳易王餐饮会员管理系统软件V16为例说明 软件文件下载可以点击最下方官网卡片——软件下载——试用版软件下载 1、会员可以登记电子会员卡或使用vip卡片 2、卡类型可以自由…

SICTF Round#3 Web方向 题解WP

100&#xff05;_upload 题目描述&#xff1a;小茂夫说&#xff1a;一直上传恶意文件尊嘟要生气了&#xff0c;世事莫固守&#xff0c;转变思路求突破 开题&#xff0c;注意有个文件包含 题目把后缀过滤死了&#xff0c;无法上传php后缀文件。文件内容些许过滤&#xff0c;短…

微信小程序uniapp校园在线报修系统维修系统java+python+nodejs+php

管理员的主要功能有&#xff1a; 1.管理员输入账户登陆后台 2.个人中心&#xff1a;管理员修改密码和账户信息 3.用户管理&#xff1a;对注册的用户信息进行删除&#xff0c;查询&#xff0c;添加&#xff0c;修改 4.维修工管理&#xff1a;对维修工信息进行添加&#xff0c;修…

【递归】【后续遍历】【迭代】【队列】Leetcode 101 对称二叉树 100. 相同的树

【递归】【后续遍历】Leetcode 101 对称二叉树 100 相同的树 101. 对称二叉树解法一&#xff1a; 递归&#xff1a;后序遍历 左右中解法二&#xff1a; 迭代法&#xff0c;用了单端队列 100. 相同的树解法一&#xff1a;深度优先 ---------------&#x1f388;&#x1f388;对称…

关于Windows 11中的Microsoft Defender,看这篇就差不多了

Windows内置的安全应用程序Microsoft Defender可保护你的计算机免受恶意代理和病毒的攻击。然而,在某些情况下,你可能想要禁用它,例如在测试第三方安全应用程序时。我们将向你展示如何在Windows 11上永久禁用Microsoft Defender。 何时应永久禁用Microsoft Defender防病毒 …