【CPP】CPP中类 -- class 的基本使用

这里是oldking呐呐,感谢阅读口牙!先赞后看,养成习惯!

个人主页:oldking呐呐
专栏主页:深入CPP语法口牙


9 类 – class

9.1 什么是类
  • 在CPP中,class被定义为类的关键字,类的基础定义和结构体一样,咱可以看下一小节,类中定义的变量我们一般称其为类的成员或是属性,而类中定义的函数我们一般称为类的方法或成员函数
  • 类是面向对象的一种设计,其中封装了我们对对象的"描述",或者说对象的"特征"
  • 比方说我们要描述一个游戏玩家,那么这个玩家至少要有以下"特征"
    • 玩家在地图中的位置
    • 玩家的速度
    • 等等
  • 在本节我们也会不断地举玩家这个例子
9.2 类的声明
class player
{
    int x, y;
    int speed;
};

int main()
{
    player player1; //创建一个player变量
}
  • player player1; //创建一个player变量一般称创建一个新变量的过程为"实例化",新的变量称为"对象",具体的我们后面小节再提

  • 有些公司会习惯在成员变量前加_,目的是为了区分成员函数和成员变量,CPP不做强制规定,比方说下面的这个例子,加了_之后咱就能一眼看出来谁是成员了,还能避免起义

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

int main()
{
	date d;
	d.InitDate(2024, 7, 10);

	return 0;
}
  • CPP中struct被升级为和class同等的地位,同样可以在其中定义函数,但大多数情况下咱还是用class,具体可以看后面的小节会详细提到

  • 定义在类中的函数默认会带上inline

  • 类的基础声明其实和结构体没什么两样,同样的不能省略末尾的;,同样的以{}来包含类的主体,以下我们来讲讲类的进阶玩法

9.3 类内参数的使用
  • 假设此时玩家移动了,我们想计算出玩家移动后的位置,我们定义一个Move函数
  • 注意:这里如果想调用类内的参数,像结构体一样在变量后加.就行
void Move(player& player1, int& xa, int& ya)    //xa,xy为位置偏移量
{
    player1.x += xa * speed;
    player1.y += ya * speed;
}
int main()
{
	player player1;
	player1.x = 10;
	player1.y = 0;
	player1.speed = 2;

	Move(player1, 1, 1);

	return 0;
}
  • 但如果你此时调用Move函数你会发现编译器报错了
    请添加图片描述

  • 这里报错的原因是,如果我们想使用player中的变量,需要给player中的变量打上public的标签,来表明这个变量允许给类的外部使用

void Move(player& player1, int& xa, int& ya)    //xa,xy为位置偏移量
{
public:
    player1.x += xa * speed;
    player1.y += ya * speed;
}
  • 此时编译器就不会再报错了
    请添加图片描述

  • Move既然只给player用,那我们就可以把Move封装进player中,以后这个Move就专门给player用了,封装进player中之后,player中的变量不用打上public的标签也能正常调用Move,能让变量更加安全一些(当然,以下示例中没有删除掉public是因为创建完变量后还要初始化)

  • 默认类中参数为"私有"

  • 打上public标签之后的类中参数将会变成"公有"

  • 那么将函数封装进类中,此时我们称这个函数为"方法"

class player
{
public:
	int x, y;
	int speed;

	void Move(int xa, int ya)
	{
		x += xa * speed;
		y += ya * speed;
	}
};
  • 此时我们如果想使用这个方法,只需要在变量后加.即可
int main()
{
	player player1;
	player1.x = 10;
	player1.y = 0;
	player1.speed = 2;

	player1.Move(1, 1);

	return 0;
}
9.4 类与访问限定符
  • 前面咱也提到了一部分内容
    • public:即公开,被修饰的成员或者方法可以在类外被访问
    • private:即私有,被修饰的成员或者方法只能在类内被访问
    • protected:即保护,被修饰的成员和方法具有和private一样的属性,具体和private有什么不同需要在继承才能体现出来
  • class默认成员/方法为private,struct默认成员和方法为public
  • 一般情况下成员变量都会被设计为private,只有一些对外的接口才会被设计为public,具体可以看后面的小节,咱们会讲一个叫封装性的东西,里面会体现得比较深刻
  • 习惯上成员定义在下面,函数定义在上面,当然这只是习惯,不强制
9.5 类内函数成员的声明定义分离
  • 参考示例
class date
{
private:
	int year;
	int month;
	int day;
public:
	void InitDate(int year, int month, int day);
};

void date::InitDate(int year, int month, int day)
{
	year = year;
	month = month;
	day = day;
}

int main()
{
	date d;
	d.InitDate(2024, 7, 10);

	return 0;
}
  • 当然,定义前面一定要加类名声明下空间,否则类内函数的声明会找不到定义(别忘了给函数声明public),注意:声明和定义分离不会加inline
    请添加图片描述
9.6 类的实例化
  • 实例化即字面意思,就是把类的声明实例成一个实际的对象

原本类的声明是不会开辟空间的,只有当作一个"模板"实实在在地新建一个对象的时候才会开辟空间(注意:实例化一个对象的时候,方法是不会包含在对象里的,一个对象里只有类的成员变量,不会有成员函数,成员函数放在一个叫公共代码区的地方,要用方法的时候calljmp(汇编指令)直接跳转到函数的地址就完事了,节约空间嘛,要不然一个对象可老大了)

class date
{
private:
	int _year;
	int _month;
	int _day;
public:
	void InitDate(int year, int month, int day);
};

void date::InitDate(int year, int month, int day)
{
	_year = year;
	_month = month;
	_day = day;
}

int main()
{
	date d; // 这里就是实例化了一个对象
	date d1; // 这里就是又实例化出了一个对象,这俩对象不是同一个对象
	d.InitDate(2024, 7, 10);

	return 0;
}
9.7 类的大小
  • 类大小的计算还是根据C语言结构体的那一套逻辑来算的,无非就是对其数啊啥的,这里就不多说了
  • 有一个特殊情况,如果类里面一个成员变量都没有,那这个类应该是多大?
class test1
{

};

class test2
{
	int func()
	{
		return 1;
	}
};

int main()
{
	cout << sizeof(test1) << endl;
	cout << sizeof(test2) << endl;

	return 0;
}

请添加图片描述

可以看到,编译器还是为这个类分配了空间的,虽然就1字节而已

  • 这分配的1字节的目的就是为了纯纯地占位标识,来代表这个类存在过而已
9.8 类中的this指针

咱有没有考虑过,咱知道,类的方法在底层是和对象分离的,那函数怎么知道要使用哪个对象里的参数呢?
这时候this指针的作用就体现出来了

  • this是CPP中的一个关键字,一般情况下是不可见的

  • 直接看实例

class date
{
private:
	int _year;
	int _month;
	int _day;
public:

	//void InitDate(date* const this, int year, int month, int day)
	//{
	//  this->_year = year;
	//  this->_month = month;
	//  this->_day = day;
	//}
	//上面这个才是这个成员函数定义原本的样子

	void InitDate(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	//void print(date* const this)
	//{
	//  cout << this->_year << endl;
	//  cout << this->_month << endl;
	//  cout << this->_day << endl;
	//}
	//上面这个才是这个成员函数定义原本的样子

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

int main()
{
	date d1;

	//d1.InitDate(&d1, 2024, 7, 10);
	//d1.print(&d1);
	//以上才是成员函数调用原本的样子
	d1.InitDate(2024, 7, 10);
	d1.print();



	date d2;

	//d2.InitDate(&d2, 2024, 8, 20);
	//d2.print(&d2);
	//以上才是成员函数调用原本的样子
	d2.InitDate(2024, 8, 20);
	d2.print();

	return 0;
}
  • 也就是说在调用成员函数的时候编译器暗中把对象的地址给成员函数了,然后成员函数就知道应该用谁的成员变量了

  • CPP规定,不能在函数实参和形参显示地写this指针(编译器会自己处理的),但是在函数体内的变量里就可以显示地写this指针
    请添加图片描述

class date
{
private:
	int _year;
	int _month;
	int _day;
public:
	void InitDate(int year, int month, int day)
	{
		this->_year = year;
		this->_month = month;
		this->_day = day;
	}

	void print()
	{
		cout << this->_year << endl;
		cout << this->_month << endl;
		cout << this->_day << endl;
	}
};
  • 其实除了一些特殊情况,日常使用中咱一般不会在函数体里头用this指针

  • this指针传统上讲是放在栈上的,但因为this又经常用,所以VS会把this放在寄存器里面
    请添加图片描述

  • 经典坑例题:

class A
{
public:
	void print()
	{
		cout << this << endl;
		cout << "A::print()" << endl;
	}
private:
	int _a;
};

int main()
{
	A* p = nullptr;
	p->print();

	return 0;
}
  • 问这里是正常编译通过,还是编译报错,还是运行崩溃

  • 答:我们从外表看,似乎确实是给p解引用访问了,但从底层看,其实p压根就没有解引用,因为成员函数压根就不在对象里,虽然这里都没有实例化对象哈,但没实例化也不影响啊,函数都没开在对象里啊,从底层看,这里其实就是call了这个函数而已,this指针虽然直接传p这个空指针,但是也没有对this指针解引用,所以这里就是正常编译通过
    请添加图片描述

请添加图片描述

class A
{
public:
	void print()
	{
		cout << this << endl;
		cout << "A::print()" << endl;
		cout << _a << endl;
	}
private:
	int _a;
};

int main()
{
	A* p = nullptr;
	p->print();

	return 0;
}
  • 但这上面的情况就是,明显地对this指针解引用了,this指针为空指针,解引用会导致程序崩溃
    请添加图片描述
9.9 类与结构体 – class VS struct
  • 上文我们提到过类与结构体非常相似,事实上排除默认给类内参数提供public的话,类和结构体并没有本质区别,事实上他俩就是一样的,如果咱写过C实现 [贪吃蛇][贪吃蛇链接] 的话,其实在其项目中结构体的使用思想就是面向对象的思想,和类的使用是非常相似的(函数也可以封装进结构体成为方法)
  • 上文我们提到过,我们可以使用public将默认为"私有"的类内参数改为"公有",而结构体的参数默认为"公有",那么相反,我们可以使用private将结构体内参数改为"私有",能够看到,参数改为私有之后调用就会报错了
struct struct_player
{
private:
	int x, y;
	int speed;

	void Move(int xa, int ya)
	{
		x += xa * speed;
		y += ya * speed;
	}
};
int main()
{
	struct struct_player player2;
	player2.x = 10;
	player2.y = 0;
	player2.speed = 2;

	player2.Move(1, 1);

	return 0;
}

请添加图片描述

  • 虽然在代码上类与结构体并没有本质上差别,但实际在使用中,他们俩的差别还是非常大的
  • CPP存在结构体这个玩意,纯粹是为了兼容C而做出的妥协,如果你只使用CPP中"更新"的内容,那完全可以删除结构体而不影响使用,甚至说你可以用#define把所有的struct替换成class,那你的代码中就不会存在struct这个东西了
  • 所以说我们在实际使用中是使用类还是结构体,纯粹是个人的编程风格,你想把struct当作class用,也没问题,你想把class当作struct用,也没问题,纯粹取决于自己
  • 比较通用的风格是,结构体只用来储存数据,不用来做任何"继承"操作(后面的文章咱们会提到的),例如我们想保存一个玩家在地图上的位置
typedef struct position
{
	int x, y, z;
	void Move(int& x, int& y, int& z, struct offset& os)
	{
		x += os.x;
		y += os.y;
		z += os.z;
	}
}position;
  • 这里这个结构体仅仅只是数据的载体,而非类
  • 比方说游戏CS2中,有类player,阵营分为"反恐精英"和"恐怖分子",每个阵营的玩家有不同的模型,击杀提示,带不带"包"等等,两个阵营的玩家统统继承自player这个大类,而结构体只用来存数据,它不是一个比较广的概念
  • 数据集用结构体,继承用类
    请添加图片描述
9.10 类的实操 – log类(日志类)
  • 日志类用于根据消息的不同等级,向控制台输出不同等级的信息

  • 起初日志会分为三个等级,分别为"错误",“警告”,“消息及跟踪”

  • 我们需要在log类内定义方法,使创建新log变量的时候能让其拥有某个具体等级,算是…“继承”?

  • 向这个类传不同等级的信息而使其输出不同的等级的信息

  • 信息等级不匹配变量的等级就报错

class Log 
{
public: //将枚举类型转为公共使得其他函数也能用它
	enum LogLevel //枚举类型用来规定等级
	{
		info, //普通消息
		warning, //警告
		erorr, //报错
	};

private:
	LogLevel Level = info; //当前变量的等级(记得给个默认等级)

//把方法转为公共,否则不能被类外的其他函数调用
//如果你在类中定义多个函数且是层层调用的话,只用公共最外层的接口就行了
//不转为公共的话这些函数就只能被类中其他函数调用了
public: 
	void SetLogLevel(const LogLevel seting_level) //用于实例化变量后赋予其他 等级
	{
		Level = seting_level;
	}

	void Warn(const char* a) //如果当前log等级大于warning的话就允许输出warn信息
	{
		if (Level >= warning)
		{
			std::cout << "[WARNING]: " << a << std::endl;
		}
		else //否则输出等级错误
		{
			std::cout << "Log_Level error" << std::endl;
		}
	}

	void Err(const char* a) //如果当前log等级大于error的话就允许输出err信息
	{
		if (Level >= erorr)
		{
			std::cout << "[ERROR]: " << a << std::endl;
		}
		else //否则输出等级错误
		{
			std::cout << "Log_Level error" << std::endl;
		}
	}

	void Info(const char* a) //如果当前log等级大于info的话就允许输出info信息
	{
		if (Level >= info)
		{
			std::cout << "[INFO]: " << a << std::endl;
		}
		else //否则输出等级错误
		{
			std::cout << "Log_Level error" << std::endl;
		}
	}
};
int main()
{
	Log log;

	log.SetLogLevel(log.info); //设置初始等级
	log.Info("hello world!"); //打印符合该等级下的信息

	log.Err("hello world!"); //打印不符合该等级下的信息

	return 0;
}

请添加图片描述

  • 不难看出,定义类这个操作只是创建一个"模具",真正造一个能用的变量出来还需要把一些参数扔到模具里面套(某种角度上说还是结构体那套逻辑)
9.11 类的默认成员函数
  • 默认成员函数:即不需要用户显式实现,类定义完之后,编译器会默认在类中添加的隐式成员函数,当然,如果默认成员函数达不到目的,用户也可以显式地实现出来
9.11.1 类与构造函数
  • 我们先看看下面这段代码
typedef class player
{
public:
	int X, Y;

	void place()
	{
		std::cout << X << "," << Y << std::endl;
	}
}player;


int main()
{
	player p;
	p.place();

	return 0;
}
  • 在这段代码中,我们可以得到以下结果
    请添加图片描述

  • 不难看出这是个随机值,包括如果我们调用内存,也会发现这玩意是个随机值
    请添加图片描述

  • 所以此时,我们需要一个方法能在实例化的时候就自动为成员赋值,这种在实例化的时候自动就会调用的方法就被称为 – 构造函数

//书写规范
typedef class player
{
public:
	int X, Y;

	player() //这个就是构造函数
	{
		X = 0;
		Y = 0;
	}

	void place()
	{
		std::cout << X << "," << Y << std::endl;
	}
}player;
  • 区别于普通的函数,构造函数不需要返回值,名称与类名称相同
  • 我们来看看加了构造函数后的结果
int main()
{
	player p;
	p.place();

	return 0;
}

请添加图片描述

  • 值得注意的是,如果你没有在类中定义构造函数,类会默认会有一个构造函数,只是这个构造函数啥也没执行罢了

  • 当然,构造函数中一样可以包括参数

typedef class player
{
public:
	int X, Y;

	player()
	{
		X = 0;
		Y = 0;
	}

	player(int x, int y)
	{
		X = x;
		Y = y;
	}

	void place()
	{
		std::cout << X << "," << Y << std::endl;
	}
}player;

int main()
{
	player p(1, 2);
	p.place();

	return 0;
}
  • 上面这种写法被称作函数重载,咱上面提过了

请添加图片描述

  • 当然,要是觉得设计一个无参的构造函数还是麻烦了,那我们可以直接用全缺省函数,调用逻辑和无参是相同的,当然还是要记住,全缺省函数和无参函数不能构成函数重载,因为调用有歧义
class player
{
public:
	int X, Y;

	player(int x = 0, int y = 0)
	{
		X = x;
		Y = y;
	}

	void place()
	{
		std::cout << X << "," << Y << std::endl;
	}
};

int main()
{
	player p(1, 2);
	p.place();

	player p1;
	p1.place();

	return 0;
}

请添加图片描述

  • 前面也提到过,如果你不手动定义构造函数(必须是无参构造函数或者全缺省构造函数),那编译器会默认生成一个无参且啥也不执行的构造函数,如果手动定义了全缺省或者是无参构造函数中的一个,那编译器就不会生成空的啥也不执行的默认的无参构造函数,因为构成歧义

  • 所以我们总结一下,以下三个,实例化对象的时候只能选择一个,不能同时存在

    • 用户显式定义的无参构造函数
    • 用户显式定义的全缺省构造函数
    • 由编译器默认生成的隐式的无参的空的构造函数
  • 上面列举的三种构造函数,我们称为默认构造函数,默认构造函数中的"默认"是对于实例化对象来说的,不是编译器默认给的才叫默认构造函数,而是实例化对象时不传任何东西进来就会调用的构造函数才叫默认构造函数

  • 值得一提的是,如果成员变量的类型是CPP的内置类型,那要不要手动定义默认构造函数,这一点其实是无所谓的,看具体要求来,反正编译器不会报错,但如果是自定义类型(结构体或者是其他什么的玩意),他会自动调用这个自定义类型的默认构造函数,具体初始化方式需要到后面的小节再说,即初始化列表,如果自定义变量没有其默认构造函数,那就会报错
    请添加图片描述

  • 正确写法

struct Info
{
public:
	char name[20];
	char sex[10];
	int age;

	Info()
	{
		age = 18;
		strcpy(name, "zhangsan");
		strcpy(sex, "male");
	}
};

class player //这个函数就不需要手动设定默认构造函数
{
public:
	Info player_info;

	void place()
	{
		std::cout << player_info.name << std::endl;
	}
};

int main()
{
	player p1;
	p1.place();

	return 0;
}

请添加图片描述

  • 也就是说,如果成员变量全是自定义类型变量,且这个自定义类型拥有默认构造函数,那当下就不需要自己写默认构造函数,只需要编译器自己生成的这个空的就行了

  • 如果你把一个函数包装进类中,你不想让这个类实例化,那我们甚至可以将构造函数放进private中,这样当这个类实例化的时候就直接报错了

class player1
{
private:
	player1()
	{
	
	}

public:
	static void print(int x)
	{
		std::cout << x << std::endl;
	}

};

int main()
{
	player p(1, 2);
	p.place();

	player1 p1;

	return 0;
}

请添加图片描述

  • 这时候我们就只能调用它的public下的static方法了

static方法 – 静态方法,具体可看下一小节

请添加图片描述

  • 最后我们总结一下以上的几个小点
    • 构造函数名和类名相同
    • 没有返回值(返回值处啥也别写)
    • 对象实例化时系统自动调用其对应的构造函数
    • 构造函数可以重载
    • 类中没有显式定义构造函数的时候,CPP编译器会自动生成一个无参的构造函数,一旦用户显式定义了构造函数,编译器就不再自动生成无参的构造函数
    • 用户显式定义的无参构造函数,用户显式定义的全缺省构造函数,由编译器默认生成的隐式的无参的空的构造函数,这三个函数统称为默认构造函数,实例化对象的时候只能选择一个,不能同时存在
    • 对内置类型的成员变量初始化不做要求,但自定义类型的成员变量一定要手动定义"自定义类型自己的"默认构造函数来初始化
    • 如果不想用户实例化对象,可以把默认构造函数包含在private下,这样用户在实例化的时候就会报错
9.11.2 类与析构函数 – 构造函数的孪生兄弟
  • 在构造函数的时候我们提到过构造函数只在创建对象时执行,那么析构函数则相反,析构函数只在对象销毁时执行

  • 构造函数一般用在创建对象时为内存初始化(清零),相对应的,析构函数就是在销毁对象的时候为内存初始化?(清零,或者说清理用过的内存)

typedef class player
{
public:
	int X, Y;

	player()
	{
		X = 0;
		Y = 0;
	}

	player(int x, int y)
	{
		X = x;
		Y = y;
	}

	//这个带波浪号的函数就是析构函数,写法和构造函数几乎相同
	//注意这里不是标准意义上的销毁成员,现在只是在模拟销毁成员
	~player() 
	{
		X = 0;
		Y = 0;
		std::cout << "已销毁" << std::endl;
	}

	void place()
	{
		std::cout << X << "," << Y << std::endl;
	}
}player;
  • 析构函数只在对象销毁的时候执行,现在我们将对象创建在函数里,函数结束的时候对象就会自动被销毁
void fun()
{
	player p(1, 2);
	p.place();
}
  • 可以看到函数在没有结束的时候,内部成员还是有值的
    请添加图片描述

  • 如果现在跳出函数
    请添加图片描述

  • 在内存中这两个成员的值就被清理掉了

  • 这就和free()一样,手动申请的东西就要自己删掉,只不过销毁对象的时候系统帮你自动调用析构函数了而已

  • 当然析构函数也可以像常规方法一样被手动调用,只不过一般也没人会这么做就是了

  • 以上我们写的析构函数不是实际意义上的析构函数,比方这个p对象,哪怕我们不去调用析构函数p对象中的值依旧会因为栈帧的销毁而销毁,常规情况下,析构函数需要做的是销毁开辟在堆中的资源,因为堆中的资源需要自己销毁,它自己是销毁不掉的,可以说析构函数存在的意义就是想实现开辟在堆中的资源"自动地手动销毁"

  • PS:后定义的变量先析构

  • 比方这种情况就是先析构p1,后析构p

int main()
{
	player p;
	p.place();

	player p1;
	p1.place();

	return 0;
}
  • 和构造函数相同,如果我们不写析构函数,对于内置类型变量,编译器生成的默认析构函数不会对其做任何处理,自定义类型会调用他自己析构函数

  • 哪怕我们显式地写析构函数,自定义类型依旧会调用他自己的析构函数,所以说不论我们写不写析构函数,自定义类型调用的析构函数总和当前对象的析构函数无关,他一定会调用他自己的析构函数

  • 析构函数有且只有一个

9.11.3 类与拷贝构造
  • 假设我们写了一个函数
void func1(int x)
{
	cout << x << endl;
}
  • 我们知道在函数被调用的时候,形参x将会是一个全新的变量,独立于函数内,因此需要对x开辟空间啥啥的,x接受的,只是一个外部变量传进来的值,对于外面长什么样,是谁传的值进来,完全不做不考虑
  • 这是对于CPP内置类型来讲的
  • 如果是用户自定义类型,则会有一些区别,具体看以下这个例子
class player
{
private:
	int _x;
	int _y;

public:
	player(int x = 0, int y = 0)
	{
		_x = x;
		_y = y;
	}

	player(const player& p)//这个就是拷贝构造函数
	{
		_x = p._x;
		_y = p._y;
	}

	int GetX()
	{
		return _x;
	}

	int GetY()
	{
		return _y;
	}
};

void func1(player p)
{
	cout << p.GetX() << "," << p.GetY() << endl;
}
  • 同样的传值进函数,区别在于传的是自定义类型,前面我们说过,内置类型传值传参进行了传值的步骤,这个步骤是编译器自动帮你做的,而在自定义类型中,编译器不能全权自动帮你做传值,他只能自动帮你调用一个用于传值的函数,这个函数我们称为"拷贝构造函数"

  • 在上面这个例子中,形参p独立于函数外,不会被函数外影响到,创建p这个对象后,它需要接受外部的值,于是编译器自动调用了形参p中的一个构造函数 – 拷贝构造函数

  • 构造函数不是用来初始化内部成员的么,那此时,p在"实例化"的时候发现,你这是想整个把一个外部的player类对象一次性全部拷贝过来,于是调用了这个特殊的构造函数 – “拷贝构造函数”,它大部分和一般构造函数相同,区别在于第一个参数必须是同类型的对象的引用,拷贝嘛,当然得是同类型对象才能拷贝嘛,然后以只读外部参数的引用的方式,把值拿到自己手上,就完成了一次自定义类型的拷贝

  • 以上说这么多,其实就只是想表明一个规定:CPP规定对于自定义类型对象进行拷贝(传值调用)的行为,必须要调用拷贝构造函数

  • 拷贝构造也可以像下面这样调用

int main()
{
	player p1(1, 2);
	
	//调用方式1
	player p2(p1);

	//调用方式2
	player p3 = p1;

	return 0;
}
  • 为什么拷贝构造的第一个参数一定得是引用?

  • 我们假设,第一个参数不是引用,而是一个需要开辟空间的对象,此时就会和上面矛盾,传值要调用拷贝构造,结果拷贝构造又要传值,那传值又得调用拷贝构造,层层往复,好家伙,被玩成无限递归了,如果用引用,那压根就没有新的对象被创建,创建的只是一个函数外部对象的别名而已,那也就不存在无限递归了
    请添加图片描述

  • 为什么要加const?

  • 拷贝!对于"目标"对象,"源"对象是只读的,读出值然后赋值给"目标"对象,加个const,也能避免我们自身可能会写错的问题,万一你就写反了呢,本来要被赋值的对象,变成了给别人赋值的对象,那就完蛋了
    请添加图片描述

  • 未显式定义拷贝构造,编译器会自动生成拷贝构造,自动生成的拷贝构造会对内置类型完成值拷贝/浅拷贝(即在内存里一个字节一个字节地拷贝),对于其自定义类型的成员变量,又会调用它自己的拷贝构造

  • 总结一下

    • 拷贝构造是一个特殊的构造函数
    • 拷贝构造的第一个参数一定是自身类类型的引用,如果有其他的参数,那其他的参数都必须得有默认值
    • 拷贝构造函数是构造函数的一个重载
    • 拷贝构造函数的第一个参数必须是类类型对象的引用,使用传值的方式会直接导致报错,会导致无穷递归调用
    • CPP规定,自定义类型对象进行拷贝行为必须要调用拷贝构造函数,所以自定义类型的传值传参和传值返回都会调用拷贝构造完成,这就导致了如果进行将拷贝构造改为传值传参的操作的时候,就会无限递归
    • 未显式定义拷贝构造,编译器会自动生成拷贝构造,自动生成的拷贝构造只会完成值拷贝/浅拷贝(即在内存里一个字节一个字节地拷贝),对于其自定义类型的成员变量,又会调用它自己的拷贝构造
  • 事实上,自己定义拷贝构造是有一定意义所在的,对于player类来说,编译器自己生成的那个只能浅拷贝的拷贝构造就够用了,但如果你的对象中包含了指针,那就麻烦大了,因为浅拷贝是一个字节一个字节地拷贝,他会直接把指针也拷贝进去,如果这个指针指向了一个堆中的区域,然后你还定义了一个析构函数用来释放堆区域的空间,两个对象指向同一个堆的区域,对象被销毁的时候执行了两次析构函数,析构函数释放了两次堆的区域,那就完蛋了,堆区怎么能释放两次呢,所以说,对于拷贝这个步骤,需要用户能够自己定义,让用户自己把控一下深浅,免得出现拷贝过浅抑或是过深导致的问题

  • 所以换句话说,还是尽可能地少用传值传参,如果在函数里参数就是只读的,设计成const type&会更合理一些

  • 传值返回同样也会开临时对象,所以同样会调用拷贝构造,所以如果能用传引用返回,还是建议用传引用返回,特别是那些开在堆上的值什么的,特别方便

  • 还有一个小技巧,如果这个类已经实现了析构了,证明已经需要有手动开辟的资源需要清理了,那基本上就需要实现拷贝构造,否则就不需要

9.11.4 类与赋值运算符重载
9.11.4.1 运算符重载
  • 运算符重载和函数重载很像,函数重载用在函数传入不同参数时调用不同函数,而运算符则是处理不同操作数时功能不同,我们举个例子

  • 一般来讲,><对于内置类型int或者float啥啥的都是能用的,但你对于一个date类对象咋用??

  • 于是我们可以自己定义运算符重载,让这个><能对date类对象使用,当然,怎么去比较纯纯是用户定义的

  • 运算符重载是一个有特定名字的函数

  • 需要注意,运算符重载的参数个数,取决于这个运算符在CPP规定中是几元运算符,比方说+是一个双目运算符,那设计运算符重载的时候就只能设计成双目运算符,如果对++设计运算符重载,那就只能设计成单目运算符

  • 假设我想设计一个<的运算符重载,比较玩家信息中的年龄大小

  • (string咱们后面再提)

class Info
{
private:
	string _name;
	string _sex;
	int _age;

public:
	Info(int age = 18, string name = "zhangsan", string sex = "male")
	{
		_age = 18;
		_name = name;
		_sex = sex;
	}

	void SetAge(int age = 18)
	{
		_age = age;
	}

	int GetAge()//提供接口,只读
	{
		return _age;
	}
};

class player
{
private:
	int X, Y;
	Info player_info;

public:
	player(int x = 0, int y = 0, int age = 18)
	{
		X = x;
		Y = y;
		player_info.SetAge(age);
	}

	int GetAge()
	{
		return player_info.GetAge();
	}
};

bool operator < (player p1, player p2)
{
	return p1.GetAge() < p2.GetAge();
}

int main()
{
	player p1(1, 2, 99);
	player p2(2, 3, 66);

	//cout << operator < (p1, p2) << endl;
	//以上的写法也行,毕竟本质上是函数嘛,只是一般没人这么用就是了,下面的和上面的是等价的
	cout << (p1 < p2) << endl;

	return 0;
}

请添加图片描述

  • 不难看出,在调用运算符重载的时候,第一个操作数传给函数第一个形参,第二个操作数传给函数第二个形参

  • 当然,还有一种更加好的方式,咱们可以直接把运算符重载定义在类里面,而不是在全局

class Info
{
private:
	string _name;
	string _sex;
	int _age;

public:
	Info(int age = 18, string name = "zhangsan", string sex = "male")
	{
		_age = 18;
		_name = name;
		_sex = sex;
	}

	void SetAge(int age = 18)
	{
		_age = age;
	}

	int GetAge()
	{
		return _age;
	}
};

class player
{
private:
	int X, Y;
	Info player_info;

public:
	player(int x = 0, int y = 0, int age = 18)
	{
		X = x;
		Y = y;
		player_info.SetAge(age);
	}

	int GetAge()
	{
		return player_info.GetAge();
	}
	//因为this指针的原因,这里写一个参数就行了
	bool operator < (player p2)//直接打包进player里了
	{
		return player_info.GetAge() < p2.GetAge();
	}
};

int main()
{
	player p1(1, 2, 99);

	player p2(2, 3, 66);

	cout << p1.operator<(p2) << endl;//这样调用的时候会有一点区别
	cout << (p1 < p2) << endl;//这样调用的时候编译器会自己检查运算符重载

	return 0;
}

请添加图片描述

  • 如果运算符重载在全局,要么你就得把成员变量设计成全局,要么就得单独设计一个Get()方法,所以把运算符重载定义进类里面是一个非常明智的选择,放进类里面后,成员变量依旧可以是private,而这个运算符重载当作对外的接口用就行

  • 当然,要注意,运算符重载之后,优先级和结合性还是不会变的,重载的运算符或者操作符不能用CPP内置之外的符号,比方说"@"

  • 两个运算符重载之间又可以构成函数重载

  • 重载操作符至少要有一个类类型的参数,也就是说一定不能这么写bool operator < (int x, int y)

  • 而且运算符重载还是要重载一些有意义的内容,要是瞎搞,乱重载那就完蛋了

  • 还有,.* :: sizeof ?: .都不能重载

关于.*,这是一个在CPP引入的一个新操作符,具体用法需要看下面的例子

class player
{
public:
	void func1()
	{
		cout << "func1()" << endl;
	}
};

typedef void (*PF1)();//普通函数指针应该长这样

typedef void (player::*PF)(); //这个玩意称作成员函数指针类型,typedef出来专门用来调用player的成员函数

int main()
{
	//一般想要定义一个成员函数指针应该这么做
	void (player::*pf1)() = nullptr;

	//但现在也可以用typedef完成
	PF pf2 = nullptr;

	//如果我们想定义一个类域中的函数,CPP规定必须要用取地址符
	PF pf3 = &player::func1;

	//全局的函数指针回调直接采用C的模式就可以
	//(*pf)();

	//假设我们需要回调这个成员函数指针,就不能采用全局函数指针的模式,因为成员函数指针有this指针,而且必须要知道给哪个对象用函数嘛,直接调用肯定不行

	player p;//咱们得先实例化一个p出来,对对象p使用回调
	(p.*pf3)();//在这个地方就用上.*这个操作符了

	return 0;
}

请添加图片描述

  • 不过一般对成员函数的回调的使用场景是非常少的,这个知道就行,忘了再查就行

  • PS:重载++运算符时,前置++和后置++都是operator ++没办法很好地区分,所以CPP规定,后置++重载时,加一个int形参,跟前置++构成函数重载,方便区分,当然,这个参数加不加形参名都无所谓,因为他仅作为区分,不接受值

	//前置++
	//++d1;
	//实际在调用的时候会转换成这种形式
	//d1.operator++();
	Date& operator++();

	//后置++
	//d1++;
	//实际在调用的时候会转换成这种形式,括号里面是啥无所谓,反正不用
	//d1.operator++(1);
	Date operator++(int);
9.11.4.2 赋值运算符重载
  • 赋值运算符重载也是一个默认成员函数,用于完成两个"已经存在"的对象的赋值重载拷贝,拷贝构造是另一个对象实例化的时候用来初始化这个对象的,而赋值运算符重载是给另一个已经实例化完了的对象赋值
class player
{
private:
	int _x;
	int _y;

public:
	player(int x = 0, int y = 0)
	{
		_x = x;
		_y = y;
	}

	player(const player& p)
	{
		_x = p._x;
		_y = p._y;
		//p._x = _x;
	}

	//这里如果只读的话,可以加const,读写可以不加
	//建议还是写成引用,减少拷贝构造的调用
	//这里返回值可以写成当前对象的引用,返回this的解引用就行,这里不需要传值返回,传值返回会生成拷贝,实在没必要
	const player& operator = (const player& p)
	{
		_x = p._x;
		_y = p._y;
		return *this;
	}

	int GetX()
	{
		return _x;
	}

	int GetY()
	{
		return _y;
	}
};

int main()
{
	player p1(1, 2);
	player p2(3, 4);

	//拷贝构造
	player p3(p1);
	player p4 = p2;

	//赋值重载拷贝 
	p3 = p4;

	//当然,因为我们为赋值运算符重载设计了返回值,所以可以连续赋值
	p4 = p3 = p1;

	return 0;
}
  • 如果赋值运算符重载没有显式实现时,编译器会默认生成一个只会进行浅拷贝/值拷贝的赋值运算符重载,对于自定义类型成员,会调用他自己的拷贝构造,同样,指针类型的成员会免疫浅拷贝,所以对于栈这样的类还是要深拷贝的,还是要自己写赋值重载,还是同样的,简单来说如果实现了析构,那就几乎是必须实现复制重载和拷贝构造
9.11.5 取地址运算符重载
9.11.5.1 const成员函数
  • 我们知道,公有成员函数有修改私有成员变量的权限,但如果给出以下场景就会出问题
    请添加图片描述

  • 本质上,上述场景中,是遇到了权限放大的问题,const对象无法修改,如果不限制成员函数,可能会使得const对象通过成员函数而修改

  • 本质上,d1.Print();这个语句的原型应该是d1.Print(&d1),&d1的类型是const Date*,即d1不能被修改,但函数里,定义本质上是Date* const类型的实参在接收实参&d1,Date* const允许解引用后的值被修改,不允许指针被修改,我们对比一下

  • 实参 – const Date* – 不允许d1的值被修改

  • 形参 – Date* const – 允许d1的值被修改

  • 所以这里出现了权限放大的问题

  • 但如果形参类型改成const Date* const那就不存在这样的问题了

  • 但实参的第一个参数是规定死的Date* const,所以就只能做出以下妥协的规定

  • CPP规定,如果需要成员函数禁止修改this指针指向的对象的值,需要在函数声明和定义语句后加上const

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

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

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

int main()
{
	const Date d1(2024, 7, 14);
	d1.Print();

	return 0;
}

或者

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

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

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

void Date::Print() const
{
	cout << _year << "/" << _month << "/" << _day << endl;
}

int main()
{
	const Date d1(2024, 7, 14);
	d1.Print();

	return 0;
}

请添加图片描述

  • 当然哈,构造函数一定不能加const,加了咋修改啊(地铁老人手机)
  • 一般我们对成员函数和运算符重载的态度是,能加const就加,毕竟加了还能让const对象也可以访问
9.11.5.2 取地址运算符重载
  • 取地址运算符重载是一个默认成员函数,一般情景下不需要自己手动定义,因为默认给的取地址就完全够用了,但免不了有些特殊的时候,所以一般会这么写
	//这个作为默认成员函数,显式实现后编译器便不再自动生成
	Date* operator&()
	{
		return this;
	}

	//这个不是默认成员函数,假设没有显式实现上面的默认成员函数,但却实现了下面带const的重载,传普通对象进来的时候也不会调用下面这个带const的函数
	const Date* operator&() const
	{
		return this;
	}
  • 想给普通对象取地址,就返回普通地址,想给const对象取地址,就返回const对象的指针,编译器会调用最匹配的

  • 重载这个东西从各种角度上讲都很方便,可以极大提高程序的封装性,比方说我不想让用户取到地址,我就可以这么改,刻意的传空指针回去(当然一般不会这么干哈)

	Date* operator&()
	{
		return nullptr;
	}

	const Date* operator&() const
	{
		return nullptr;
	}
9.12 构造函数特别篇(重要) – 初始化列表
  • 变量的声明 – 告诉编译器变量的类型和名称

  • 变量的定义 – 为当前变量分配空间

  • CPP规定,声明变量允许不带初始值,说人话就是以下这个例子

	int a;
  • 这个例子中a没有给初始值,意味着a没有被初始化,但却开辟了空间,所以a中一定为随机值

  • 不难看出,这里int a;的操作,是变量的声明与定义和一的局面,意味着只要这么写,编译器会帮你声明和定义一次性完成

  • 但在类中,成员变量一定会被初始化

  • 我们来看看下面这个类

class A
{
public:
	A(const A& aa)
	//这条注释的位置也是(原因下文会提)
	{
		_aa = aa._aa;
		_bb = aa._bb;
	}

	A(int a = 1, int b = 1)
	//初始化其实隐藏在构造函数的下面,也就是这条注释的位置
	{
		_aa = a;
		_bb = b;
	}

	void Print()
	{
		cout << _aa << _bb << endl;
	}

private:
	//以下仅仅只是变量声明,和定义
	int _aa;
	int _bb;
};
  • 也就是说,上面这个例子中,成员变量其实包括了声明,定义,初始化也存在(下面会说),这个函数体里的赋值压根就不能说是初始化,只能说是后期赋值

  • 为什么说类的成员变量一定有初始化?上面的例子中我们没有手动初始化,编译器会自动帮我们加一个默认初始化

  • 默认初始化即以默认值初始化,而默认值其实就是内存中的随机值

  • 和我们在C中的建议一样,成员变量建议初始化再使用

  • 而初始化方式就得用户显式定义出来,就定义在默认构造函数下面,我们称之为初始化列表

class A
{
public:
	A(const A& aa)
	{
		_aa = aa._aa;
		_bb = aa._bb;
	}

	A(int a = 1, int b = 1)
		:_aa(a)
		,_bb(b)
	{
		_aa = a;
		_bb = b;
	}

	void Print()
	{
		cout << _aa << _bb << endl;
	}

private:
	int _aa;
	int _bb;
};
  • 格式就像是上面这样,放在构造函数下面,且在函数体上面,以:开始,接着要写成员变量(只能写一个,不能重复写相同的变量),之间要记得打,号,于是写出来就像是一个列表一样的玩意,每个成员变量后面打上括号,注意,括号里面可以写值,也可以写其他变量,甚至是表达式和函数,如果不写东西,那就以默认值初始化
class A
{
public:
	A(const A& aa)
	{
		_aa = aa._aa;
		_bb = aa._bb;
	}

	A(int a = 1, int b = 1)
		:_aa() //以默认值初始化
		,_bb(b) //用其他变量初始化
		,_cc(1) //用值初始化
		,_dd((int*)malloc(4)) //用表达式/函数初始化
	{
		//如果没有开辟空间之类的玩意,成员变量确保都正确初始化了,在当前情境中,函数体里面写不写都无所谓
		//当然,这里因为写了malloc,所以还是要在里面检查一下
		if (_dd == nullptr)
		{
			perror("malloc fail");
			exit(0);
		}
	}

	void Print()
	{
		cout << _aa << _bb << endl;
	}

private:
	int _aa;
	int _bb;
	int _cc;
	int* _dd;
};
  • 讲几个特殊的例子

  • 我们知道,有几个类型是必须初始化的,里面一定不能是随机值

  • 即引用和const对象

  • const成员因为不能够修改,所以只能在初始化的时候为其赋初值,引用也是如此
    请添加图片描述

  • 在类里面也是一样,const成员变量和引用是一定要初始化的,不初始化就会报错
    请添加图片描述

  • const成员变量/引用成员一旦被声明定义,所有的构造函数下面都要写初始化列表,因为对象被初始化的时候,我们并不知道他要用哪个构造函数初始化,而const成员/引用成员必须初始化,所以一旦const成员变量/引用成员一旦被声明定义,所有的构造函数下面都要写初始化

class A
{
public:
	A(const A& aa)
		:ce(2)
		,rf(_bb)
	{
		_aa = aa._aa;
		_bb = aa._bb;
	}

	A(int a = 1, int b = 1)
		:_aa()
		,_bb(b)
		,_cc(1)
		,_dd((int*)malloc(4))
		,ce(2)
		,rf(a)
	{
		if (_dd == nullptr)
		{
			perror("malloc fail");
			exit(0);
		}
	}

	void Print()
	{
		cout << _aa << _bb << endl;
	}

private:
	int _aa = 1; //C++11新特性,下面会提(懒得粘示例了,一并写了)
	int _bb;
	int _cc;
	int* _dd;
	const int ce;
	int& rf;
};
  • C++11引入了一个新特性,即可以在成员变量声明出给缺省值(一定不要混淆!这里不是初始化!),这个缺省值主要是给没有在初始化列表中初始化的成员用的,上面的_aa就是一个好例子(我们假设_aa没有显式定义在初始化列表中),没有给出显式定义来初始化的话,一般编译器会直接用默认值初始化,除非你写了缺省值(缺省值也可以写表达式和函数)

  • 如果你像上面的例子一样,只要你显式定义了初始化,即便里面什么都没写,他也依然会被使用(即用默认值初始化),给了缺省值也没用

请添加图片描述

  • 尽量地使用初始化列表初始化,毕竟初始化列表才是成员变量正规的初始化方式,如果自定义成员变量没有出现在初始化列表里,那他会调用自己的构造函数进行初始化,如果连他自己的构造函数都没有,那就会报错

  • 总结一下:

    • 每个成员都要走初始化列表
      1. 在初始化列表初始化的成员 – 显式写
      2. 没有在初始化列表初始化的成员 – 不显式写
        1. 有缺省值就用缺省值初始化
        2. 没有缺省值
          1. 如果是内置类型,就用默认值初始化
          2. 如果是自定义类型,就用自定义类型的构造函数初始化,如果自定义类型没有构造函数,就报错
      3. const成员和引用成员必须在初始化列表初始化,否则编译报错
  • 还有一个点需要注意:初始化列表中按照成员声明顺序进行初始化,跟成员在初始化列表出现的顺序无关,所以建议声明和初始化列表顺序保持一致

9.13 类型转换
  • 给出以下场景,问:A a1 = 1是个什么玩意,怎么做到的
class A
{
public:
	A(const A& aa)
	{
		_aa = aa._aa;
	}

	A(int a = 1)
	{
		_aa = a;
	}

	void Print()
	{
		cout << _aa << endl;
	}

private:
	int _aa;
};

int main()
{
	A a1 = 1;

	return 0;
}
  • 这里其实用到了一个叫做"隐式类型转换"的玩意,就是字面意思

  • 前面我们说过,类似于一下这种类型转换的,都会产生临时变量

	int a = 1;
	double b = a;

	//等同于:
	//double atmp = (double)a;
	//double b = atmp;

所以

	A a1 = 1;

	//等同于:
	//A tmp(1); -- (注意这条语句)
	//A a1 = tmp;
  • 不难发现,它调用了一个构造函数来完成这个类型转换的步骤并生成临时变量,然后调用拷贝构造传给目标对象

  • 不过像以上这个场景,一般编译器会优化,将"调用1次或者多次构造函数+调用拷贝构造"直接优化成"直接调用构造函数"

  • 像下面这另外一个场景编译器就不会优化了

	const A& ra = 1;
  • 这里ra是临时变量的引用,1直接调用构造被类型转换为A类型,并生成临时变量.然后因为临时变量具有常性,不能被修改,所以加一个const

  • 类型转换在很多地方是非常有用的,特别是成员函数中自定义类型参数的传递,我们看下面这个例子

class A
{
public:
	A(const A& aa)
	{
		_aa = aa._aa;
	}

	A(int a = 1)
	{
		_aa = a;
	}

	void Print()
	{
		cout << _aa << endl;
	}

private:
	int _aa;
};

class Stack
{
public:
	void Push(const A& a)
	{
		//...
	}

private:
	A _arr[10];
	int _top;
};


int main()
{
	Stack st;

	A a1;
	st.Push(a1); //一般咱们会这么用

	st.Push(1); //注意这条语句

	return 0;
}
  • 这条语句在语法上是完全合法的,本质上还是将类型转换之后将临时对象传给Push的形参

  • 不难看出,如果没有类型转换,这里传参其实会很难办的,还得手动开一个对象,再往里传,直接传值简单粗暴,用着方便

  • 不过使用还是要注意,只有单参数支持类型转换,多参数默认不支持,不过在C++11之后,可以这么写了

class A
{
public:
	A(const A& aa)
	{
		_aa = aa._aa;
		_bb = aa._bb;
	}

	A(int a = 1, int b = 1)
	{
		_aa = a;
		_bb = b;
	}

	void Print()
	{
		cout << _aa << _bb << endl;
	}

private:
	int _aa;
	int _bb;
};

class Stack
{
public:
	void Push(const A& a)
	{
		//...
	}

private:
	A _arr[10];
	int _top;
};


int main()
{
	A a1 = { 1, 2 };

	const A& ra = { 1, 2 };

	Stack st;

	st.Push(a1);
	st.Push({1, 2});

	return 0;
}
  • 只要构造和拷贝构造写好,就完全可以这么传值

  • 当然,如果你不想让类型转换发生,你也可以在函数前加``这么个关键字(貌似是只能在构造函数上用)

	explicit A(const A& aa)
	{
		_aa = aa._aa;
		_bb = aa._bb;
	}

	explicit A(int a = 1, int b = 1)
	{
		_aa = a;
		_bb = b;
	}

9.14 类 与 static
  • static修饰的成员变量一般被称为静态成员变量,静态成员变量一定要在类外进行初始化,物理上存在静态区里

  • static标记的成员变量 – 多个实例化对象共享该成员变量的空间,是类的一部分而不是实例化对象的一部分,一般称为静态成员变量

  • 注意:用static修饰的成员变量不能在声明出给缺省值,声明处给的缺省值是给初始化列表用的,静态成员压根就不会走初始化列表

  • 和函数中的静态变量一样,无论外部如何改变,类中的静态变量有且只存在一个

  • 如果要使用静态成员变量,除了加public标签,还可以通过一个叫叫静态成员函数一样的东西访问,当然静态成员函数仅可以访问静态成员变量

  • static标记的成员函数(方法) – 与被static标记的成员变量一样,是类的一部分而不是实例化对象的一部分,仅可以用于静态成员变量,一般称为静态成员函数

  • CPP允许非静态成员函数访问静态成员变量,不允许静态成员函数访问常规成员变量(因为静态成员函数没有this指针,所以访问不了普通成员变量)
    请添加图片描述

  • 事实上,静态成员变量相当于是在全局的,只不过他被类域限定住了作用域而已

  • 要调用静态成员函数,必须要指定类域

class player
{
public:
	int func1()
	{
		hair++;
	}
	static int func2()
	{
		hair++;
		//x++;
	}

private:
	int x;
	int y;
	static int hair;
};

int player::hair = 1;

int main()
{
	player::func2();//指定类域

	return 0;
}
  • 封装性:

    • 封装性是指将对象的数据成员和成员函数打包在一起,对外部隐藏对象的内部实现细节,只暴露必要的接口,class本身成员默认自带的private属性就很好的表现了其封装性
    • 封装性也可以体现为某个和成员变量因为使用了static,此成员变成了静态成员变量及函数,和类本身绑定在一起,成为了形容类这个东西本身的属性
  • 比方说以下这个例子:

    • 在这个例子中,我们定义手机这个类
    • 在苹果鼎盛时期,国产手机有一种模仿苹果的浪潮,此时手机屏幕类型普遍为:刘海屏,那我们可以认为手机类的属性一定附带有刘海屏这个特征,这和其他配置无关
class Phone
{
public:
	enum screenType
	{
		Bang_screen, //刘海屏(0)
		hole_screen, //挖孔屏(1)
	};

private:
	static screenType screentype; // 静态成员变量

public:
	// 静态成员函数,用于设置屏幕类型
	static void setScreenType(const screenType type)
	{
		screentype = type;
	}

	// 静态成员函数,用于获取屏幕类型
	static screenType getScreenType()
	{
		return screentype;
	}
};

//初始化屏幕类型
Phone::screenType Phone::screentype = Phone::Bang_screen;
//    类型       | 需要初始化的成员名 |     需要赋的值

int main()
{
	Phone xiaomi;
	Phone::setScreenType(Phone::Bang_screen);
	std::cout << Phone::getScreenType() << std::endl;

	std::cout << "一段时间后,挖孔屏逐渐变成了主流" << std::endl;

	//将手机屏幕设置为挖孔屏
	Phone::setScreenType(Phone::hole_screen);
	std::cout << Phone::getScreenType() << std::endl;

	return 0;
}

请添加图片描述

  • 其实咱也能看到,调用静态成员函数的时候,全都和对象没有任何关系,所有的操作都是在类上完成的

  • 以上这个例子仅仅用来理解静态成员变量和静态成员函数与类的关系,其处于绑定状态,静态成员变量和静态成员函数属于类的一部分,对象只是共享空间,逻辑上我们认为对象继承了类的属性罢了

  • PS:在结构体中也可以遵循这套理论

9.15 友元
  • 友元是一个CPP引入的关键字,其提供了一种突破类访问限定符封装的访问方式,友元分为:友元函数和友元类,在函数声明或者类声明的前面加friend关键字就行,并且要把友元声明放到一个类里面

  • 外部友元函数此时就可以访问类的私有/保护成员,友元函数仅仅只是一种声明,并不是类的成员函数!!

  • 友元函数可以在类的任何地方声明,不收类访问限定符的约束

  • 一个函数可以是多个类的友元函数

class player
{
public:
	friend void Func1();//这就是友元声明

private:
	int _x;
	int _y;
};

void Func1()//Func1此时就可以正常访问player的私有成员
{
	player p;
	p._x = 0;
}
  • 注意:不能像下面这个例子中这么干!
    请添加图片描述

  • 如果你想让Func2访问类player中的私有函数,就只能把类A标记为友元类

class player
{
public:
	friend void Func1();
	//friend void A::Func2();
	friend class A;

private:
	int _x;
	int _y;
};

void Func1()
{
	player p;
	p._x = 0;
}

class A
{
public:
	void Func2()
	{
		player p;
		p._x = 1;
	}
};
  • 当然,这么做缺点也是有的,就是类A中的所有函数现在都能访问player的私有成员了

  • 这里提一个比较有意思的点,这和函数一样哈
    请添加图片描述

  • 这个情境中,编译是不会通过的,因为Func1()压根不认识class B,所以我们可以加声明,让Func1()认识一下class B

class A;
class B;

class A
{
public:
	friend void Func1(const A& a, const B& b);

private:
	int _x;
	int _y;
};

class B
{
public:
	friend void Func1(const A& a, const B& b);

private:
	int _p;
	int _q;
};

void func1(const A& a, const B& b)
{
	
}

int main()
{

	return 0;
}
  • 注意,友元是不具有交换性的,A是B的友元,但B不一定是A的友元
  • 而且友元关系不能传递,A是B的友元,B是C的友元,但A不是C的友元
  • 友元会增加耦合度,降低封装性,建议少用,除非不得不用
9.16 内部类
  • 类可以定义在一个类的内部,此时类里的类被称为内部类,内部类和全局的类相比,它仅仅只是受类域和访问限定符限制,访问方式有一些区别,其余和普通类没啥区别
  • 注意:只是定义在外部类中的类被称为内部类,所以外部类实例化的时候并不会实例化内部类!(除非外部类有内部类的成员变量)
  • 内部类默认就是外部类的友元
class player
{
public:
	class hair //这个就是内部类,hair默认就是player的友元
	{
		int color;
		int lenth;
	};

private:
	int x;
	int y;
};

int main()
{
	player::hair h1; //内部类一样可以在外部实例化(除非定义在私有或者保护)

	return 0;
}
  • 定义在私有的情况
    请添加图片描述
9.17 匿名对象
  • 看以下情景
class player
{
public:
	player(int x = 1, int y = 2)
		:_x(x)
		,_y(y)
	{
	
	}

private:
	int _x = 1;
	int _y = 2;
};

int main()
{
	player p1(3, 4); //有名对象

	player(5, 6); //这个就是匿名对象,和C创建匿名的结构体类似
	player(); //这个也是

	return 0;
}
  • 匿名对象类似于临时对象

  • 实际操作中,匿名对象一般用来临时调用一下函数,即以下情景

class player
{
public:
	player(int x = 1, int y = 2)
		:_x(x)
		,_y(y)
	{
	
	}

	int func1()
	{
		return 1;
	}

private:
	int _x = 1;
	int _y = 2;
};

int main()
{
	player p;
	cout << p.func1() << endl; // 常规调用方式

	cout << player().func1() << endl; // 匿名对象调用方式

	return 0;
}
  • 匿名对象的生命周期只在创建的这一行,到下一行就销毁了,本来就是临时调用一下嘛
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值