类和对象 01【C++】

news2024/11/17 23:08:05

目录

  • 一、 类的引入
    • 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);
	}

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

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

相关文章

gem5学习(23):经典缓存——Classic Caches

目录 一、Interconnects 1、Crossbars 二、Debugging 官网教程&#xff1a;gem5: Classic caches 默认缓存是一个带有MSHR&#xff08;未命中状态保持寄存器&#xff09;和WB&#xff08;写缓冲区&#xff09;的非阻塞缓存&#xff0c;用于读取和写入未命中。缓存还可以启用…

常用驱动适配总结

I2C驱动 I2C中主要包含5个管脚&#xff0c;包括&#xff1a;复位信号&#xff0c;中断信号&#xff0c;I2C时钟信号&#xff0c;I2C数据信号&#xff0c;触摸屏供电信号&#xff08;我的电路供电没有控制信号&#xff09;。设备树相对简单了很多。 SCL和SDA管脚是需要在i2c的…

WebRTC最新版报错解决:city.wav:missing and no known rule to make it (二十六)

简介: CSDN博客专家,专注Android/Linux系统,分享多mic语音方案、音视频、编解码等技术,与大家一起成长! 优质专栏:Audio工程师进阶系列【原创干货持续更新中……】🚀 优质专栏:多媒体系统工程师系列【原创干货持续更新中……】🚀 人生格言: 人生从来没有捷径,只…

Chrome关闭时出现弹窗runtime error c++R6052,且无法关闭

环境&#xff1a; Chrome 版本121 Win10专业版 问题描述&#xff1a; Chrome关闭时出现弹窗runtime error cR6052&#xff0c;且无法关闭 解决方案&#xff1a; 1.任务管理器打开&#xff0c;强制结束进程 2.再次打开谷歌浏览器&#xff0c;打开设置关于Chrome&#xff0…

Chrome插件精选 — 缓存清理

Chrome实现同一功能的插件往往有多款产品&#xff0c;逐一去安装试用耗时又费力&#xff0c;在此为某一类型插件挑选出比较好用的一款或几款&#xff0c;尽量满足界面精致、功能齐全、设置选项丰富的使用要求&#xff0c;便于节省一个个去尝试的时间和精力。 1. Chrome清理大师…

MySQL|MySQL基础(求知讲堂-学习笔记【详】)

MySQL基础 目录 MySQL基础一、 MySQL的结构二、 管理数据库1&#xff09;查询所有的数据库2&#xff09;创建数据库3&#xff09;修改数据库的字符编码4&#xff09;删除数据库5&#xff09;切换操作的数据库 三、表的概念四、字段的数据类型4.1 整型4.2 浮点型(float和double)…

机试笔记-害死人不偿命的(3n+1)猜想

来自 “胡凡《算法笔记》” 比较容易理解的题目 应该要想到用while&#xff08;&#xff09;循环&#xff0c;因为题设中给出了当 n 1的时候脱离计算的要求&#xff0c;所以以后计算的过程中应该想到用while&#xff08;&#xff09;的循环作为测试条件

【linux】【Shell】Linux基础命令集

目录 一、常用操作命令 二、编辑命令 三、挂载磁盘命令 一、常用操作命令 1、cd :更改文件目录命令 1.切换到主目录cd2.切换到目录/tmpcd /tmp3.切换到当前目录 dir目录cd dir4.切换到根目录cd /5.切换到上一级目录cd ..6.切换到二级目录cd ../..7.切换到主目录&#xff0…

打造纯Lua组件化开发模式:Unity xLua框架详解

在传统的Unity开发中&#xff0c;通常会使用C#来编写游戏逻辑和组件。但是&#xff0c;随着Lua在游戏开发中的应用越来越广泛&#xff0c;我们可以将游戏逻辑和组件完全用Lua来实现&#xff0c;实现纯Lua的组件化开发模式。这样做的好处是可以更加灵活地修改游戏逻辑&#xff0…

【算法与数据结构】1020、130、LeetCode飞地的数量 被围绕的区域

文章目录 一、1020、飞地的数量二、130、被围绕的区域三、完整代码 所有的LeetCode题解索引&#xff0c;可以看这篇文章——【算法和数据结构】LeetCode题解。 一、1020、飞地的数量 思路分析&#xff1a;博主认为题目很抽象&#xff0c;非常难理解。想了好久&#xff0c;要理解…

行测:国考省考行测:图形推理,四面体,正六面体的图形推理方法,箭头唯一法

国考省考行测&#xff1a;图形推理 2022找工作是学历、能力和运气的超强结合体! 公务员特招重点就是专业技能&#xff0c;附带行测和申论&#xff0c;而常规国考省考最重要的还是申论和行测&#xff0c;所以大家认真准备吧&#xff0c;我讲一起屡屡申论和行测的重要知识点 遇到…

(十二)【Jmeter】线程(Threads(Users))之tearDown 线程组

简述 操作路径如下: 作用:在正式测试结束后执行清理操作,如关闭连接、释放资源等。配置:设置清理操作的采样器、执行顺序等参数。使用场景:确保在测试结束后应用程序恢复到正常状态,避免资源泄漏或对其他测试的影响。优点:提供清理操作,确保测试环境的整洁和可重复性…

水经注下载注记地图, mars3d加载底图

使用 水经微图 &#xff08;公司提供的&#xff0c;需付费&#xff0c;我也没有这个东西&#xff09;下载注记地图&#xff1b; 1、选择下载 选择区域&#xff1a; 根据需求进行选择&#xff0c;两边都可以选择&#xff0c;看个人喜欢&#xff1b;这里以澳门为演示 选择地图…

从零开始手写mmo游戏从框架到爆炸(二十一)— 战斗系统二

导航&#xff1a;从零开始手写mmo游戏从框架到爆炸&#xff08;零&#xff09;—— 导航-CSDN博客 上一章&#xff08;从零开始手写mmo游戏从框架到爆炸&#xff08;二十&#xff09;— 战斗系统一-CSDN博客&#xff09;我们只是完成了基本的战斗&#xff0c;速度属性并没有…

学习数仓工具 dbt

DBT 是一个有趣的工具&#xff0c;它通过一种结构化的方式定义了数仓中各种表、视图的构建和填充方式。 dbt 面相的对象是数据开发团队&#xff0c;提供了如下几个最有价值的能力&#xff1a; 支持多种数据库通过 select 来定义数据&#xff0c;无需编写 DML构建数据时&#…

Facebook的未来蓝图:数字社交的下一个篇章

在数字化时代&#xff0c;社交媒体已经成为人们日常生活中不可或缺的一部分。而在众多的社交媒体平台中&#xff0c;Facebook一直处于领先地位&#xff0c;不断探索着数字社交的新领域和新形式。随着科技的不断发展和社会的不断变革&#xff0c;Facebook正在谱写着数字社交的未…

Vue路由缓存问题

路由缓存问题的产生 VueRouter允许用户在页面中创建多个视图&#xff08;多级路由&#xff09;&#xff0c;并根据路由参数来动态的切换视图。使用带参数的路由时&#xff0c;相同的组件实例将被重复使用。因为两个路由都渲染同一个组件&#xff0c;比起销毁再创建&#xff0c;…

谷歌掀桌子!开源Gemma:可商用,性能超过Llama 2!

2月22日&#xff0c;谷歌在官网宣布&#xff0c;开源大语言模型Gemma。 Gemma与谷歌最新发布的Gemini 使用了同一架构&#xff0c;有20亿、70亿两种参数&#xff0c;每种参数都有预训练和指令调优两个版本。 根据谷歌公布的测试显示&#xff0c;在MMLU、BBH、GSM8K等主流测试…

数据结构·顺序表

1数据结构简介 学习数据结构与算法之前&#xff0c;一般是先学数据结构&#xff0c;方便之后学习算法&#xff0c;那么数据结构拆开介绍&#xff0c;就是数据 和 结构&#xff0c;数据&#xff0c;生活中到处都是&#xff0c;结构&#xff0c;就是数据存储的方式&#xff0c;即…

React 事件处理 ( this问题 参数传递 ref)

React事件的命名采用小驼峰方式&#xff08;cameCase&#xff09;,而不是小写 使用JSX语法时你需要传入一个函数作为事件处理函数&#xff0c;而不是一个字符串 你不能通过返回false 的方式阻止默认行为。你必须显示式的使用preventDefault 1 this 需要谨慎对待JSX回调函数中的…