从零开始c++精讲:第二篇——类和对象

文章目录

  • 一、类的定义
  • 二、类的访问限定符及封装
  • 三、类的作用域
  • 四、类的实例化
  • 五、类对象模型
    • 5.1计算对象的大小
    • 5.2结构体内存对齐规则
  • 六、this指针
    • 6.1简介
    • 6.2 this指针的特性
  • 七、类的6个默认函数
    • 7.1构造函数
    • 7.2析构函数
    • 7.3拷贝构造函数
    • 7.4赋值运算符重载
      • 7.4.1运算符重载
      • 7.4.2赋值运算符重载
  • 八、const成员
  • 九、取地址及const取地址操作符重载
  • 九、知识实战:日期类简单实现
  • 十、构造函数补充
    • 10.1构造函数体赋值
    • 10.2初始化列表
    • 10.3explicit关键字
  • 十一、static成员
  • 十二、友元
  • 十三、内部类
  • 十四、拷贝对象时的一些编译器优化


一、类的定义

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

class为定义类的关键字,ClassName为类的名字{}中为类的主体,注意类定义结束时后面分号不能省略
类体中内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的方法或者
成员函数。

类的两种定义方式

  1. 声明和定义全部放在类体中,需注意:成员函数如果在类中定义,编译器可能会将其当成内
    联函数处理。
    从零开始c++精讲:第二篇——类和对象_第1张图片
  2. 类声明放在.h文件中,成员函数定义放在.cpp文件中,注意:成员函数名前需要加类名::
    从零开始c++精讲:第二篇——类和对象_第2张图片
    一般情况下,更期望采用第二种方式。

二、类的访问限定符及封装

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

从零开始c++精讲:第二篇——类和对象_第3张图片

【面试题】
问题:C++中struct和class的区别是什么?
答:其他方式也可以的,主要看公司要求。一般都是加个前缀或者后缀标识区分就行。比特就业课
解答:C++需要兼容C语言,所以C++中struct可以当成结构体使用。另外C++中struct还可以用来
定义类。和class定义类是一样的,区别是struct定义的类默认访问权限是public,class定义的类
默认访问权限是private。注意:在继承和模板参数列表位置,struct和class也有区别,后序给大
家介绍。

我这里写一个简单的栈的对象,代码不完全,只是做教学使用
我们要定义一个栈对象,可以在对象名Stack前面加一个class,你也可以在Stack前面加一个struct

但是要注意,struct默认成员都是公有的public。但是class则默认成员都是私有的

如果采用了class,而且你不对成员的权限加说明,你是都访问不了成员的,如下图:
从零开始c++精讲:第二篇——类和对象_第4张图片

我们这里再加一下权限,就可以访问public的成员了,但是private的成员依然不可以访问

从零开始c++精讲:第二篇——类和对象_第5张图片

三、类的作用域

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

class Person
{
	public:
		void PrintPersonInfo();
	private:
		char _name[20];
		char _gender[3];
		int _age;
};
// 这里需要指定PrintPersonInfo是属于Person这个类域
void Person::PrintPersonInfo()
{
	cout << _name << " "<< _gender << " " << _age << endl;
}

像上面这种void Person::PrintPersonInfo(),一般是长函数声明和定义分离
分离后前面要指明是哪个类域

如果是比较短的函数,你可以直接定义在类里面,这种函数默认是inline的

四、类的实例化

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

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

  2. 一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量

class Date {
	public:
		void Init(int year, int month, int day)
		{
			_year = year;
			_month = month;
			_day = day;
		}
	private:
		int _year;//这里默认前面加个下划线,方便函数里面使用的(不是硬性要求,但建议这样做)
		int _month;
		int _day;
};

int main()
{
	//类和对象的关系是一对多
	//举个例子:狗——边牧
	//狗是一个类,边牧是一个对象

	Date d;//Date对象的实例化,定义出一个实体d
	d.Init(2024, 1, 7);

	cout << sizeof(d) << endl;//打印12,即3个int型成员变量year、month、day的大小
	//成员函数的大小是不计算的
	//不同对象调用类的函数,这个函数地址都是一样的,即不同对象调用的类函数是同一个函数
	
	//需要注意的是,不同对象的成员变量是不一样的,这点和类函数是不同的
	return 0;
}

从零开始c++精讲:第二篇——类和对象_第6张图片

五、类对象模型

5.1计算对象的大小

class Date {
	public:
		void Init(int year, int month, int day)
		{
			_year = year;
			_month = month;
			_day = day;
		}
	private:
		int _year;//这里默认前面加个下划线,方便函数里面使用的(不是硬性要求,但建议这样做)
		int _month;
		int _day;
};

class Empty1 {//一个空类

};

class Empty2 {//一个无成员变量的类
	public:
		void func()
		{

		}
};

int main()
{
	//类和对象的关系是一对多
	//举个例子:狗——边牧
	//狗是一个类,边牧是一个对象

	Date d;//Date对象的实例化,定义出一个实体d
	d.Init(2024, 1, 7);

	cout << sizeof(d) << endl;//打印12,即3个int型成员变量year、month、day的大小
	//成员函数的大小是不计算的
	//不同对象调用类的函数,这个函数地址都是一样的,即不同对象调用的类函数是同一个函数
	//需要注意的是,不同对象的成员变量是不一样的,这点和类函数是不同的
	cout << sizeof(Date) << endl;//类大小也是12


	//空类、无成员变量的类,对象大小为1字节,这1字节不存储有效数据
	//因为成员函数不算在对象大小里面,只要没有成员变量,大小都是1
	Empty1 e1;
	cout << sizeof(e1) << endl;//打印1

	Empty2 e2;
	cout << sizeof(e2) << endl;//打印1
	return 0;
}

从零开始c++精讲:第二篇——类和对象_第7张图片

5.2结构体内存对齐规则

  1. 第一个成员在与结构体偏移量为0的地址处。
  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
    注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
    VS中默认的对齐数为8
  3. 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
  4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整
    体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍

【面试题】

  1. 结构体怎么对齐? 为什么要进行内存对齐?
  2. 如何让结构体按照指定的对齐参数进行对齐?能否按照3、4、5即任意字节对齐?
  3. 什么是大小端?如何测试某台机器是大端还是小端,有没有遇到过要考虑大小端的场景

六、this指针

6.1简介

class Date {
	public:
		void Init(int year, int month, int day)
		{
			_year = year;
			_month = month;
			_day = day;
		}
	private:
		int _year;//这里默认前面加个下划线,方便函数里面使用的(不是硬性要求,但建议这样做)
		int _month;
		int _day;
	public:
		void Print() {//这里Print函数虽然没有参数,但实际调用会处理成Print(Date* this)
			//this是一个形参,就是当前调用这个函数的对象
			cout << _year << "-" << _month << "-" << _day;
			//这里会默认处理成
			//cout << this->_year << "-" << this->_month << "-" << this->_day;
		}
};

class Empty1 {//一个空类

};

class Empty2 {//一个无成员变量的类
	public:
		void func()
		{

		}
};

int main()
{
	Date d1;
	Date d2;
	d1.Init(2023, 1, 1);
	d2.Init(2024, 1, 3);
	d1.Print();//这里Print函数虽然没有参数,但实际调用会处理成d1.Print(&d1)
	cout << endl;
	d2.Print();
}

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

【面试题】

  1. this指针存在哪里?

答:this指针是个形参,它和函数的局部变量一样,存在栈帧上面

  1. this指针可以为空吗?下面两段代码运行结果是什么

2答:从零开始c++精讲:第二篇——类和对象_第9张图片
上图左边代码解释:

class A
{
public:
	void Print()
	{
		cout << "Print()" << endl;
	}
	int _a;
};
int main()
{
	
	A* p = nullptr;
	p->Print();//可以正常运行,打印Print()
	//指针用->,对象用.
	//成员函数的地址不在对象中
	//找成员函数,本质就是找成员函数的地址,
	//这个找地址是在编译链接阶段找的,通过函数名去找的
	
	//成员变量存在对象中
	p->_a=1;//报错,这里访问nullptr的_a,就有问题了,
	//_a是属于对象的,和前面成员函数不属于对象不一样
	
	return 0;
}

从零开始c++精讲:第二篇——类和对象_第10张图片

上图右边代码解释:

class A
{
public:
	void Print()
	{
		cout << _a << endl;//如果是空指针进来,相当于this._a变成了nullptr._a,就会报错
		cout << "Print()" << endl;
	}
	int _a;
};
int main()
{
	
	A* p = nullptr;
	p->Print();//可以正常运行,打印Print()
	//指针用->,对象用.
	//成员函数的地址不在对象中
	//找成员函数,本质就是找成员函数的地址,
	//这个找地址是在编译链接阶段找的,通过函数名去找的
	

	//p->_a=1;
	//成员变量存在对象中
	return 0;
}

从零开始c++精讲:第二篇——类和对象_第11张图片
为什么第一次没有报错,这里报错了?
因为我们第一次访问了Print这个成员函数,并没有用this调用东西。
而我们这次要访问this._a,但是我们这里p是一个nullptr传参过去,不调用成员变量就不会报错,但是你用nullptr调用成员变量就会有问题了。

6.2 this指针的特性

  1. this指针的类型:类类型* const,即成员函数中,不能给this指针赋值。
  2. 只能在“成员函数”的内部使用
  3. this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给
    this形参。所以对象中不存储this指针。
  4. this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传
    递,不需要用户传递

七、类的6个默认函数

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

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

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

class Date {};

在这里插入图片描述

7.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 公有方法给对象设置日期,但如果每次创建对象时都调用该方法设置
信息,未免有点麻烦,那能否在对象创建时,就将信息传递进去呢?

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

从零开始c++精讲:第二篇——类和对象_第12张图片

class Date
{
public:
	// 1.无参构造函数
	Date()
	{}
	// 2.带参构造函数
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//构造函数特性:
	//1.函数名和类名相同
	//2.无返回值
	//3.对象实例化时编译器自动调用对应的构造函数
	//4.构造函数可以重载


	private:
		int _year;
		int _month;
		int _day;
};
int main()
{
	Date d1;//调用无参构造函数
	
	Date d2(2024,1,8);//调用带参构造函数(相当于是一个初始化)
	return 0;
}

当然,如果有时忘了初始化,但是又想每次都默认有一个初始化,我们可以用到缺省参数

也就是把这里的Date()构造函数填一些默认参数变成Date(int year=1, int month=1, int day=1)
从零开始c++精讲:第二篇——类和对象_第13张图片

ps:这里的Date()和Date(int year=1, int month=1, int day=1)构成重载
重载就是函数名相同,参数名不同。

这里如果无参调用Date()会有歧义,
因为编译器不知道你是要用无参构造函数Date()
还是没有写参数的Date(int year=1, int month=1, int day=1)
从零开始c++精讲:第二篇——类和对象_第14张图片

class Date
{
public:
	
	// 带参构造函数
	Date(int year=1, int month=1, int day=1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//构造函数特性:
	//1.函数名和类名相同
	//2.无返回值
	//3.对象实例化时编译器自动调用对应的构造函数
	//4.构造函数可以重载
	void Print() {
		cout << _year <<"-"<< _month <<"-"<< _day<<endl;
	}

	private:
		int _year;
		int _month;
		int _day;

};
int main()
{
	Date d1;//调用无参构造函数
	d1.Print();

	Date d2(2024,1,8);//调用带参构造函数(相当于是一个初始化)
	d2.Print();

	Date d3(2024, 1);
	d3.Print();

	return 0;
}

从零开始c++精讲:第二篇——类和对象_第15张图片


class Stack {
	public:
		Stack(size_t capacity=3) {
			_a = (int*)malloc(sizeof(int)*capacity);
			if (nullptr == _a) {
				perror("malloc申请失败");
				return;
			}
			_capacity = capacity;
			_size = 0;
		}

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

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

上面这段代码,如果你不写构造函数,默认不会初始化
从零开始c++精讲:第二篇——类和对象_第16张图片

所以,大家在用的时候,最好还是要记得初始化一下。

默认生成的构造函数,会处理自定义类型(调用自定义类型的构造或者赋一个默认值),但是内置类型处理不处理取决于编译器。建议不管是什么类型,你都初始化一下,防止编译器没有给你初始化后面报错。
从零开始c++精讲:第二篇——类和对象_第17张图片
一般情况都需要我们自己写构造函数。如果成员是自定义类型,或者声明时给了缺省值,可以考虑不写构造函数,让编译器自己生成构造函数

在这里插入图片描述

class Date
{
public:
	/*
	// 如果用户显式定义了构造函数,编译器将不再生成
	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类中构造函数屏蔽后,代码可以通过编译,因为编译器生成了一个无参的默认构造函数
	// 将Date类中构造函数放开,代码编译失败,因为一旦显式定义任何构造函数,编译器将不再生成
	// 无参构造函数,放开后报错:error C2512: “Date”: 没有合适的默认构造函数可用
	Date d1;
	return 0;
}

从零开始c++精讲:第二篇——类和对象_第18张图片

class Time
{
public:
	Time()
	{
		cout << "Time()" << endl;
		_hour = 0;
		_minute = 0;
		_second = 0;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
private:
	// 基本类型(内置类型)
	int _year;
	int _month;
	int _day;
	// 自定义类型
	Time _t;
};
int main()
{
	Date d;
	return 0;
}

从零开始c++精讲:第二篇——类和对象_第19张图片
这里Date创建对象d,由于我们自己没有写构造函数,所以编译器自动生成了一个。
对于我们的内置类型的int的_year、_month、_day我们编译器都没有初始化
但是对于我们的自定义类型的_t,我们编译器是默认把它初始化成了0

在这里插入图片描述
从零开始c++精讲:第二篇——类和对象_第20张图片
这里我们写了一个无参的构造函数,还有一个全缺省的构造函数。你可以理解为可以不传参就调用的构造函数就是默认构造函数。所以这里就产生歧义,编译器无法通过了。

ps:三种默认构造函数:
从零开始c++精讲:第二篇——类和对象_第21张图片

解决办法:你可以把全缺省改成半缺省,或者写一个

7.2析构函数

概念:
在这里插入图片描述
注意:析构函数是进行资源的清理,不是资源的销毁!

特性:
从零开始c++精讲:第二篇——类和对象_第22张图片

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;
	}
	~Date()//析构函数,类名前加一个波浪号~
	{
		cout << "~Date()" << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};
// 以下测试函数能通过编译吗?
int main()
{
	Date d1;
}

析构函数会在对象生命周期结束后自动调用,我这里打印一下做个示例
从零开始c++精讲:第二篇——类和对象_第23张图片

这个析构函数的作用体现在哪了呢?
比如说我们的,如果我们用完一个指针,而那个指针指向的malloc出来的空间,我们很多时候会忘了destroy它,这样就会造成内存泄露。
内存泄露是不会报错的,这就导致有时其实代码有问题,但是我们没发现。

但如果我们写了一个析构函数,就会在对象生命周期结束默认进行资源的清理

typedef int DataType;
class Stack
{
public:
	Stack(size_t capacity = 3)
	{
		_array = (DataType*)malloc(sizeof(DataType) * capacity);
		if (NULL == _array)
		{
			perror("malloc申请空间失败!!!");
			return;
		}
		_capacity = capacity;
		_size = 0;
	}
	void Push(DataType data)
	{
		// CheckCapacity();
		_array[_size] = data;
		_size++;
	}
	// 其他方法...
	~Stack()
	{
		if (_array)
		{
			free(_array);
			_array = NULL;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	DataType* _array;
	int _capacity;
	int _size;
};
int main()
{
	Stack s;
	s.Push(1);
	s.Push(2);
}

在这里插入图片描述
从零开始c++精讲:第二篇——类和对象_第24张图片


class Time
{
public:
	~Time()
	{
		cout << "~Time()" << endl;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
private:
	// 基本类型(内置类型)
	int _year = 1970;
	int _month = 1;
	int _day = 1;
	// 自定义类型
	Time _t;
};
int main()
{
	Date d;
	return 0;
}

从零开始c++精讲:第二篇——类和对象_第25张图片

7.3拷贝构造函数

概念:

从零开始c++精讲:第二篇——类和对象_第26张图片
特征:
拷贝构造函数也是特殊的成员函数,其特征如下:

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

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

class Date
{
public:
	Date(int year, int month, int day) {//构造函数
		_year = year;
		_month = month;
		_day = day;
	}
	Date(Date& d) {//拷贝构造函数(构造函数的一个重载)
	//拷贝构造函数的参数必须有且仅有一个同类型对象的引用
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	void Print() {
		cout << _year << "-" << _month << "-" << _day;
	}
private:
	// 基本类型(内置类型)
	int _year = 1970;
	int _month = 1;
	int _day = 2;
};

int main()
{
	Date d1(2024,1,11);
	Date d2(d1);//用d1来拷贝构造d2,这里会调用拷贝构造函数
	d2.Print();
	return 0;
}

从零开始c++精讲:第二篇——类和对象_第27张图片
关于拷贝构造第二点特征:为什么使用传值方式编译器直接报错,会引发无穷递归调用?

如果我现在使用传值,那我现在d2要调用拷贝构造
从零开始c++精讲:第二篇——类和对象_第28张图片
但是调用这个函数之前,你要先传参。但是d1传参给d的时候,传参又会引发一个新的拷贝构造。
(简单来说,d2要拷贝构造d1,但是d1传参过去又创造了新的拷贝构造d…)

而如果你是用引用来做参数:
这里d2会调用拷贝构造,然后d1传参给引用dd,而引用dd就是d1的别名,所以不会再往下调用拷贝构造了,这里就不会出现无穷递归了。
从零开始c++精讲:第二篇——类和对象_第29张图片

ps:普通传值拷贝的缺点:
我们在传一些较大的自定义类型时,有时候是传的原先变量的类型。
这种方式又称为“浅拷贝”(也就是值拷贝),也就是参数改变不影响原先变量。
ps:深拷贝是指拷贝指向的资源,让我有和你一样大的空间,一样的值。

class Date
{
public:
	Date(int year,int month,int day) {
		_year = year;
		_month = month;
		_day = day;
	}
	void Print() {
		cout << _year << "-" << _month << "-" << _day;
	}
private:
	// 基本类型(内置类型)
	int _year=1970 ;
	int _month=1 ;
	int _day =2;
};

void func(Date d)
{//该种方式又称为“浅拷贝”
	d.Print();
}

int main()
{
	Date d1(2024,1,11);
	d1.Print();
	return 0;
}

从零开始c++精讲:第二篇——类和对象_第30张图片
对于Date类,直接传值拷贝好像没啥问题。但是如果你遇到一些特别的类,比如栈类

举个例子:现在我们调用一个Date类,和一个栈类
然后对象d1和对象st1分别调用了func1和func2
从零开始c++精讲:第二篇——类和对象_第31张图片
对于Date类,把三个int型的参数传过去就传过去了,然后函数结束以此销毁。
从零开始c++精讲:第二篇——类和对象_第32张图片
但是对于栈类(stack),栈有指针类型的_a,也有int型的_top和_capacity
传值调用先复制过去
从零开始c++精讲:第二篇——类和对象_第33张图片

但是这里有区别的是,我们值拷贝的_a指向一块空间,和我们原先的_a指向是同一块空间。

从零开始c++精讲:第二篇——类和对象_第34张图片
然后c++是有析构函数的,析构函数会在出了作用域自动调用
在出了func2这个作用域,析构函数会把st中的_a指向的空间释放一次
从零开始c++精讲:第二篇——类和对象_第35张图片
然后出了main函数的作用域,析构函数又会把st1中的_a指向区域再释放一次
从零开始c++精讲:第二篇——类和对象_第36张图片

就相当于一块空间被释放了两次,但是谁又能保证,第一次释放后,有没有其他程序用了那块空间。但是你第二次释放,相当于把别人的空间释放掉了。这就很离谱了。
所以,对于栈这种类,你用浅拷贝(传值)就很有问题。

解决办法:

注意:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。

对于栈类,我们的拷贝构造对于自定义类型_a(指针)就是要进行深拷贝,深拷贝是指拷贝指向的资源,让我有和你一样大的空间,一样的值。对于_a的拷贝就是再申请一块同样大小的空间,放同样的数据进去。

而对于内置类型的_top和_capacity就直接浅拷贝即可。
从零开始c++精讲:第二篇——类和对象_第37张图片
这样的话,在函数调用结束,自动调用析构函数时,两次释放的是不同空间。就不会造成对同一个空间释放两次的情况了。

拷贝构造函数典型调用场景:
1.使用已存在对象创建新对象
2.函数参数类型为类类型对象
3.函数返回值类型为类类型对象
简言之:一般自己要开空间的基本都要自己写深拷贝

ps:拷贝构造和赋值拷贝的辨析

7.4赋值运算符重载

7.4.1运算符重载

对于内置类型,可以使用运算符,比如"=="。但是自定义类型就无法直接比较(编译器也不知道你的自定义类型的比较规则),自定义类型的比较,只能你自己来写比较函数了。

而不同人写的同一个功能的函数,可能函数的命名又是不同的。
为了统一起来方便辨认,推出了运算符重载

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

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

函数原型:返回值类型 operator操作符(参数列表)
即,operate+运算符 作为函数名,这样就很容易知道不同人写的函数是相做什么了。

比如下面代码中 bool operate==很容易就知道是一个判断相等的函数

class Date
{
public:
	Date(int year = 1900, 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)
{//operate==很容易就知道是一个判断相等的函数
	return d1._year == d2._year
		&& d1._month == d2._month
		&& d1._day == d2._day;
}
int main()
{
	Date d1(2018, 9, 26);
	Date d2(2018, 9, 27);
	cout << (d1 == d2) << endl;
}

从零开始c++精讲:第二篇——类和对象_第38张图片
上面代码main函数中,d1==d2,实际编译器会自动给你调operate= =(d1,d2)这个函数

所以这里运算符重载的好处就体现出来了,你写一个运算符重载的函数,然后你可以调operater+运算符来调用这个函数。更方便的是你写完运算符重载函数就可以直接写算符,编译器会帮你自动调用这个运算符重载函数。

ps:运算符重载只能有一个参数,因为对象调用这个运算符函数,
然后参数是另一个运算符作用的对象

7.4.2赋值运算符重载

在这里插入图片描述

class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._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;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2023, 1, 14);
	Date d2(2022, 1, 1);

	//一个已存在的对象去拷贝初始化另一个对象
	Date d3(d1);//拷贝构造
	d3.print();

	cout << endl;

	//两个已存在的对象拷贝
	 d2 = d3;//赋值重载,这里两个自定义类型的复制调用了Date& operator=(const Date& d)
	d2.print();

}

从零开始c++精讲:第二篇——类和对象_第39张图片
如果我们没有写赋值运算符重载,编译器也会默认生成一个
可以看到,我把赋值运算符重载那块代码注释掉,代码也是可以通过的
从零开始c++精讲:第二篇——类和对象_第40张图片
这里区分自定义类型的比较==,如果你要比较自定义类型必须写一个比较的重载函数,不然编译器不知道你的比较规则是什么。

另外,也不是所有的都可以不写operator=,
和拷贝构造类似,内置类型值拷贝,自定义类型调用它的赋值。
比如Date MyQueue可以不写operator=,默认生成的operator=即可用
但是Stack必须自己实现operator=,实现深拷贝。


八、const成员

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

从零开始c++精讲:第二篇——类和对象_第41张图片
上面这段代码,我们用const Date创建了d1,就意味着d1是不能被修改了
但是我们d1.Print()又调用了Print()函数,会传一个const Date*类型的d1地址过去

这里Print()函数表面是没有参数的,但实际是有一个Date* this的参数在里面
传参的时候你把一个Const Date变成了Date,这就相当于把权限给放大了。
权限只能缩小,不能放大!
ps:权限可以缩小意味着不管是const或者非const修饰的,都可以调用const修饰的

但你const修饰的,调用非const修饰的这样就会报错了。

那我们要修改就要把Print函数也加一个const,但由于前面有返回类型和作用域限制符,已经比较乱了,所以是规定把const放在函数后面

在这里插入图片描述
从零开始c++精讲:第二篇——类和对象_第42张图片

九、取地址及const取地址操作符重载

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

class Date
{
public:
	Date* operator&()
	{
		return this;

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

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

九、知识实战:日期类简单实现

我们这里写一个简单的日期加天数得到新日期的运算符重载

Date.h文件

#pragma once
#include
using namespace std;
class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1);
	Date(const Date& d);
	Date& operator=(const Date& d);
	void print();
	// 获取某年某月的天数
	int GetMonthDay(int year, int month);
	bool operator<(const Date& y);
	bool operator>(const Date& y);
	bool operator<=(const Date& y);
	bool operator>=(const Date& y);
	bool operator==(const Date& d);
	bool operator!=(const Date& y);
	Date& operator+=(int day);
	Date& operator-=(int day);
	int operator-(const Date& d);
	Date operator++(int);
private:
	int _year;
	int _month;
	int _day;
};

Date.cpp文件

#define _CRT_SECURE_NO_WARNINGS
#include"Date.h"
#include


Date::Date(int year , int month , int day)
{
		_year = year;
		_month = month;
		_day = day;
		if (_year < 1 || _month>12 || _day<1 || _day>GetMonthDay(_year,_month)) {
			/*assert(false);*/
			this->print();
			cout << "日期非法" << endl;
		}
}

Date::Date(const Date& d)
{
		_year = d._year;
		_month = d._month;
		_day = d._day;
}

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

void Date:: print() {
	cout << _year << "-" << _month << "-" << _day;
}



bool Date::operator!=(const Date& y) {
	return !(*this == y);
}

Date& Date::operator=(const Date & y)
{
	_year = y._year;
	_month = y._month;
	_day = y._day;
	return *this;
}

bool Date::operator>=(const Date& y)
{
	return *this > y || *this == y;
}


bool Date::operator<(const Date& y)
{
	return !(*this >= y);
}

bool Date::operator<=(const Date& y)
{
	return !(*this > y);
}

bool Date::operator>(const Date& y)
{
	if (this->_year > y._year) {
		return true;
	}
	if (this->_month > y._month) {
		return true;
	}
	if (this->_day > y._day) {
		return true;
	}
	return false;
}
	// 获取某年某月的天数
int Date::GetMonthDay(int year, int month)
	{
		static int days[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30,31 };
		int day = days[month];
		if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
		{
			day += 1;
		}
		return day;
}
Date& Date::operator+=(int day) {
		_day += day;
		while (_day > GetMonthDay(_year, _month)) {
			_day -= GetMonthDay(_year, _month);
			_month++;
			if (_month == 13) {
				_year++;
				_month = 1;
			}
		}
		return *this;//this是一个指针,指向当前对象,对它解引用才是当前对象。
}

Date& Date::operator-=(int day) {
	_day -= day;
	while (_day <= 0) {
		--_month;
		if (_month == 0) {
			--_year;
			_month = 12;
		}
		_day += GetMonthDay(_year, _month);
	}
	return *this;
}

Date Date::operator++(int)
{
	Date tmp(*this);
	*this += 1;
	return tmp;
}

int Date::operator-(const Date& d)
{
	//假设左大右小
	int flag = 1;
	Date max = *this;
	Date min = d;

	if (*this < d)//左小右大
	{
		max = d;
		min = *this;
		flag = -1;
	}

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


测试用例

int main()
{
	Date d1(2023, 10, 24);
	d1.print();
	
	cout << endl;

	Date d2(2100, 3, 4);
	d2.print();

	cout << endl;
	cout << d2 - d1 << endl;

}

从零开始c++精讲:第二篇——类和对象_第43张图片

十、构造函数补充

10.1构造函数体赋值

在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。

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

虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化,构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值

10.2初始化列表

初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。

class Date
{
public:
	Date(int year, int month, int day, int ret)
		: _year(year)
		, _month(month)
		, _day(day)
		, _ret(ret)
		,_n(1)
	{
	}
private:
	int _year;
	int _month;
	int _day;
	int& _ret;//引用必须使用初始化列表初始化
	const int _n;//const修饰的成员变量也必须在初始化列表初始化
};
int main()
{
	Date d1(2024, 1, 16,0);//对象整体定义
	//每个成员变量在哪里定义?——初始化列表中
}

从零开始c++精讲:第二篇——类和对象_第44张图片

需要注意引用、const修饰的成员变量、自定义类型成员(没有默认构造函数)必须在初始化列表初始化

也可以一部分初始化在函数体内,也可以一部分初始化在初始化列表中。

class Date
{
public:
	Date(int year, int month, int day, int ret)
		:_ret(ret)
		,_n(1)
	{	//剩下3个成员没有在初始化列表中显示写出来定义,
		//但它也会定义,只是内置类型默认给的是随机值
		//如果是自定义类型成员会去调用它的默认构造函数
		//函数体内初始化
		_year = year;
		_month=month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
	int& _ret;//引用必须使用初始化列表初始化
	const int _n;//const修饰的成员变量也必须在初始化列表初始化
};

在这里插入图片描述

class Time
{
public:
	Time(int hour = 0)
		:_hour(hour)
	{
		cout << "Time()" << endl;
	}
private:
	int _hour;
};

class Date
{
public:
	Date(int day)
	{}
private:
	int _day;
	Time _t;
	//没有写默认构造函数,但对于自定义类型也会默认去调用它的默认构造函数
	//ps:不是说我们不写构造函数,编译器自动生成的就是默认构造
	//默认构造函数有三种——1.无参、2.全缺省、3.默认生成的
};

int main()
{
	Date d(1);
}

从零开始c++精讲:第二篇——类和对象_第45张图片

初始化列表解决的问题:
1.必须在定义的地方显示初始化,比如:引用、const、没有默认构造的自定义成员
2.有些自定义成员想要显示初始化(不想用默认的初始化)

在这里插入图片描述
从零开始c++精讲:第二篇——类和对象_第46张图片

这里为什么第二个打印的是随机值?
因为我们什么是先是a2,所以我们在初始化的时候也是先初始化a2,而a2是用a1进行初始化的(此时a1还未初始化,a1还是个随机值),所以a2是随机值。

然后是用传参过来的1初始化a1,a1=1

ps:建议什么和初始化顺序保持一致,不然会出现理解问题,我上面这个代码是为了举例,正常写肯定不建议这样写。

10.3explicit关键字

构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用

也就是说,你那个单参数的构造函数中,参数是什么类型,什么类型就可以转那个自定义类型。

class A
{
public:
	A(int a)
		:_a(a)
	{}
	int _a = 0;
};
int main()
{
	A aa1(1);//正常构造
	
	A aa2 = 2;//内置类型对象,隐式转换成自定义类型对象
	//相当于先构造A(2),再把aa3=A(2)
	//但是注意!只要发生类型转换,都会产生临时变量,上面说的的A(2)其实就是临时变量

	return 0;
}

从零开始c++精讲:第二篇——类和对象_第47张图片
可以看到上面代码也是可以调试运行的

但是用explicit修饰构造函数,将会禁止构造函数的隐式转换。

从零开始c++精讲:第二篇——类和对象_第48张图片
可以看到,在单参数的构造函数前加一个explicit关键字,就不再支持转换了

十一、static成员

在这里插入图片描述

面试题:实现一个类,计算程序中创建出了多少个类对象

class A
{
public:
	A() { ++_scount; }
	A(const A& t) { ++_scount; }
	~A() { --_scount; }
	static int GetACount() { return _scount; }//静态成员函数特点:没有this指针,可以不通过对象调用
private:
	static int _scount;//类里面声明
	//相当于这个类的专属全局变量,static修饰的成员变量,不属于某个具体对象,它属于所有对象
};

int A::_scount = 0;//类外面定义

int main() {
	A a1;
	A a2;
	//static修饰的函数可以通过对象访问
	cout << a1.GetACount() << endl;
	
	//也可以通过类名访问
	cout << A::GetACount()<<endl;

	//还可以用匿名对象进行访问
	//匿名对象格式:类名()
	cout << A().GetACount() << endl;
	//但这里需要注意的是,匿名对象本身也是一个对象,在你使用匿名对象调用函数时,对象个数也会随之+1
	return 0;
}

从零开始c++精讲:第二篇——类和对象_第49张图片

从零开始c++精讲:第二篇——类和对象_第50张图片
静态成员函数不可以调用非静态的成员函数(也不可以访问其他静态的成员函数),
因为静态的没有this指针

但非静态成员函数可以调用静态成员函数。

十二、友元

友元这个东西简单来说就是,对于一个类里面私有的成员,你可以进行一些声明,让一些友元函数可以用这些私有的成员。

就像你家的私有的东西别人不能用,但是你可以声明给你的好友,好朋友是可以用你的东西的。

class Date
{
public:
	Date(int year, int month, int day)
		: _year(year)
		, _month(month)
		, _day(day)
	{}
// d1 << cout; -> d1.operator<<(&d1, cout); 不符合常规调用
// 因为成员函数第一个参数一定是隐藏的this,所以d1必须放在<<的左侧
	ostream& operator<<(ostream& _cout)
	{
		_cout << _year << "-" << _month << "-" << _day << endl;
		return _cout;
	}
private:
	int _year;
	int _month;
	int _day;
};

友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在
类的内部声明,声明时需要加friend关键字。

从零开始c++精讲:第二篇——类和对象_第51张图片
但友元这个东西也有不好的地方,本来私有的东西别人是禁止访问的,结果你搞了个友元让别人可以随便改,它是破坏了封装的,也增加了耦合,不宜多用

十三、内部类

从零开始c++精讲:第二篇——类和对象_第52张图片
从零开始c++精讲:第二篇——类和对象_第53张图片
从零开始c++精讲:第二篇——类和对象_第54张图片
上图中B就是一个普通类,它只是受A的类域和访问限定符限制
如果你直接访问B,是不能定义B类的对象的
从零开始c++精讲:第二篇——类和对象_第55张图片
你必须指明内部类是属于哪个类的,编译器才能找到那个内部类
可以看到,我们指明了内部类B是属于A类的,这里就不再报错了。
从零开始c++精讲:第二篇——类和对象_第56张图片
当然了,如果你把内部类置为private,那这个内部类只有你自己能用,别人就用不了了。

另外,内部类天生就是外部类的友元,它是可以调用内部类的私有成员的。
从零开始c++精讲:第二篇——类和对象_第57张图片
这就有点像,外部类是老爸,内部类是儿子,对于家里的私有东西,儿子是可以用老爸的。

十四、拷贝对象时的一些编译器优化

先来看构造的一些补充知识点

class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "A(int a)" << endl;
	}
	A(const A& aa)
		:_a(aa._a)
	{
		cout << "A(const A& aa)" << endl;
	}
	A& operator=(const A& aa)
	{
		cout << "A& operator=(const A& aa)" << endl;
		if (this != &aa)
		{
			_a = aa._a;
		}
		return *this;
	}
	~A()
	{
		cout << "~A()" << endl;
	}
private:
	int _a;
};

void f1(A aa)
{}

A f2()
{
	A aa;
	return aa;
}
int main()
{
	A aa1(1);//构造
	A aa2(aa1);//拷贝构造
	
	A aa3 = aa1;//也是拷贝构造
	//一个已经存在的对象拷贝初始化另一个要创建的对象是拷贝构造

	aa2 = aa3;//赋值拷贝
	//两个已经存在的对象,才是赋值拷贝
	return 0;
}

在这里插入图片描述

一些优化:
连续的构造+构造=构造
构造+拷贝构造=构造
拷贝构造+拷贝构造=拷贝构造

class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "A(int a)" << endl;
	}
	A(const A& aa)
		:_a(aa._a)
	{
		cout << "A(const A& aa)" << endl;
	}
	A& operator=(const A& aa)
	{
		cout << "A& operator=(const A& aa)" << endl;
		if (this != &aa)
		{
			_a = aa._a;
		}
		return *this;
	}
	~A()
	{
		cout << "~A()" << endl;
	}
private:
	int _a;
};

void f1(A aa)
{}

A f2()
{
	A aa;
	return aa;
}
int main()
{
	A aa1 = 1;//不同类型之间赋值
	//第一步:先用1构造一个临时对象,
	//第二步:再用临时对象拷贝构造aa1
	//第三步:编译器优化——在同一个表达式中,构造+拷贝构造=构造

	//优化的三种情况:
	//连续的构造+构造=构造
	//构造+拷贝构造=构造
	//拷贝构造+拷贝构造=拷贝构造
	cout << "--------" << endl;

	const A& aa2 = 2;
	//先用2构造一个临时对象,然后给临时对象取别名aa2(不会调用拷贝构造)
	cout << "--------" << endl;

	A aa3;//构造
	f1(aa3);//拷贝构造
	cout << "--------" << endl;

	f1(A(2));//构造+拷贝构造=构造
	cout << "--------" << endl;
	
	A aa4 = f2();//拷贝构造+拷贝构造=拷贝构造
	cout << "--------" << endl;

	// 一个表达式中,连续拷贝构造+赋值重载->无法优化
	aa1 = f2();
	cout << "--------" << endl;
	return 0;
}

从零开始c++精讲:第二篇——类和对象_第58张图片


你可能感兴趣的:(C++快速入门,c++,类和对象,构造函数,赋值重载,拷贝构造,static,友元函数)