C++类和对象(下)

一. 再谈构造

1. 再谈构造

在之前我们讲过构造函数的初始化和赋值,自定义类型和内置类型的区别,以及不同的构造方法比如:无参构造,全缺省构造。
现在我们再补充一下构造函数的小细节

  1. 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化
#include <iostream>

using std::cin;
using std::cout;
using std::endl;

class Time {
public:
	Time(int hour = 0, int minute = 0, int second = 0)
		:_hour(hour),
		_minute(minute),
		_second(second)
	{}
private:
	int _hour;
	int _minute;
	int _second;
};

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

int main()
{
	Date d1(1949, 10, 1);

	return 0;
}
  1. 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
class A {
public:
	A(int a)
		:_a1(a),
		_a2(_a1)
	{ }

	void Print()
	{
		cout << _a1 << " " << _a2 << endl;
 	}
private:
	int _a2;
	int _a1;
};

int main()
{
	A aa(10);
	aa.Print();
	return 0;
}

打印结果是 10 -858993460 说明确实初始化顺序和类声明的顺序没有关系

上述就是构造函数的一些细节上的讲解了,下面就让我们再去了解一个和构造函数相关的关键字吧

2. explicit关键字

构造函数不仅可以构造与初始化对象,对于接收单个参数的构造函数,还具有类型转换的作用。接收单个参数和加上explicit关键字的构造函数具体表现:

  1. 构造函数只有一个参数
class Date {
public:
	explicit Date(int day)
		:_day(day)
	{ }

	void Print()
	{
		cout << _day << endl;
	}
private:
	int _day;
};

int main()
{
	//正常调用
	Date d1(10);
	d1.Print();

	// 没有带explicit下的隐式类型转换,不会报错
	/*Date d2 = 20;
	d2.Print();*/

	// 现在看一下带了explicit下是什么样的
	Date d3 = 30;
	d3.Print();

	// 下面就是带了explicit隐式类型转换的报错信息
	/*C2440"初始化":无法从"int"转换为"Date"
	E0415不存在从"int"转换到"Date"的适当构造函数*/
	return 0;
}
  1. 构造函数有多个参数,除第一个参数没有默认值外,其余参数都有默认值
class Date {
public:
	explicit Date(int day, int month = 1, int year = 2024)
		:_day(day), _month(month), _year(year)
	{
	}

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

int main()
{
	// 正常调用
	Date d1(10, 5, 2024);
	d1.Print();

	// 没有带explicit下的隐式类型转换,不会报错
	/*Date d2 = 20; // 相当于 Date d2(20, 1, 2024);
	d2.Print();*/

	// 现在看一下带了explicit下是什么样的
	Date d3 = 30; // 试图进行隐式转换
	d3.Print();

	// 下面就是带了explicit隐式类型转换的报错信息
	/*C2440"初始化":无法从"int"转换为"Date"
	E0415不存在从"int"转换到"Date"的适当构造函数*/
	return 0;
}
  1. 全缺省构造函数
#include <iostream>
using namespace std;

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

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

int main()
{
	// 正常调用
	Date d1(10, 5);
	d1.Print();

	// 没有带explicit下的隐式类型转换,不会报错
	/*Date d2 = 20; // 相当于 Date d2(20, 1);
	d2.Print();*/

	// 现在看一下带了explicit下是什么样的
	Date d3 = 30; // 试图进行隐式转换
	d3.Print();

	// 下面就是带了explicit隐式类型转换的报错信息
	/*C2440"初始化":无法从"int"转换为"Date"
	E0415不存在从"int"转换到"Date"的适当构造函数*/
	return 0;
}

根据上述代码,我们得知了 explicit 的核心作用就是禁止构造时的隐式类型转换,那么为什么C++要搞这个关键字呢?还要为什么 explicit 要禁止隐式类型转换
其原因是隐式类型会导致有的bug难以发现,可能会将原本错误代码因为隐式类型转换的原因成功运行,根本原因是防止编译器 “自作聪明” 因为 布尔类型在计算机中是整型的子类,所以有可能在编写数学运算相关的代码时布尔类型会被隐式类型转换为整型
因此,除非你明确希望允许隐式转换,否则给所有单参数构造函数加上 explicit 是一种标准且优秀的防御性编程习惯。

二. static成员

1. 概念

声明为 static 的类成员称为类的静态成员,用 static 修饰的成员变量,称之为静态成员变量;用 static 修饰的成员函数,称之为 静态成员函数 。静态成员变量一定要在类外进行初始化

那么static成员有什么用呢?有一道比较有名的面试题他的要求就是实现一个类,计算程序中创建出了多少个类对象。这一道面试题就需要用到 static

class A {
public:
	A()
	{ 
		_a++;
	}
	A(const A& t)
	{
		_a++;
	}
	~A()
	{
		--_a;
	}
	static int GetACount()
	{
		return _a;
	}
private:
	static int _a;
};
// 注意:static 修饰的成员函数定义需要在类外定义,声明是在类内声明
// 如果是多文件编码时比如:test.h test.cpp,那么声明则是在.h文件中定义则在.cpp中
int A::_a;


int main()
{
	cout << A::GetACount() << endl;

	A a1, a2;
	A a3(a1);

	cout << A::GetACount() << endl;
}

这里为什么可以使用static成员来实现,原理是什么

这里是因为 static修饰过的成员函数生命周期是随着整个程序消亡而消亡的,所以这里可以使用static来实现

2. 特性

  1. 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
  2. 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
  3. 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
  4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
  5. 静态成员也是类的成员,受public、protected、private 访问限定符的限制

三. 友元

友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
友元分为:友元函数和友元类

1. 友元函数

友元函函数的作用是什么?为什么C++要创建友元?
如果大家自己或者看别人实现一个自定义类时基本会看到重载 operator >> 和 operator <<,而他们在实现时都是类外实现然后使用 friend 后面跟着 operator >> 和 operator <<,为什么呢?这是因为如果在类内里面实现 >> 和 << 的重载那么在调用重载的 >> 后 << 是这样写的 obj << cout;很反人类,因为你写 cout << obj;,如果它是成员函数,编译器会把它解析为 obj.operator<<(cout);那么就会编译报错。

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

	std::ostream& operator<< (std::ostream& out)
	{
		out << _year << "-" << _month << "-" << _day;

		return out;
	}
	std::istream& operator>> (std::istream& in)
	{
		in >> _year;
		in >> _month;
		in >> _day;

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

int main()
{
	Date d1;

	//这个不会报错
	d1 << cout;

	//这个就会显示你没有重载operator << 或 >>
	//cout << d1;
	return 0;
}

我们看看友元是如何处理上述问题的

class Date {

	friend std::ostream& operator<< (std::ostream& out, const Date& d);
	friend std::istream& operator>> (std::istream& in, const Date& d);
public:
	Date(int year = 2000, int month = 1, int day = 1)
		:_year(year),
		_month(month),
		_day(day)
	{
	}

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

std::ostream& operator<< (std::ostream& out,const Date& d)
{
	out << d._year << "-" << d._month << "-" << d._day;

	return out;
}
std::istream& operator>> (std::istream& in, const Date& d)
{
	in >> d._year;
	in >> d._month;
	in >> d._day;

	return in;
}

int main()
{
	Date d1;

	//这个不会报错
	cout << d1;

	//这个就会显示你没有重载operator << 或 >>
	//d1 << cout;

	return 0;
}

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

说明:

  • 友元函数可访问类的私有和保护成员,但不是类的成员函数
  • 友元函数不能用const修饰
  • 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
  • 一个函数可以是多个类的友元函数
  • 友元函数的调用与普通函数的调用原理相同

2. 友元类

友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。

  • 友元关系是单向的,不具有交换性。
    比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
  • 友元关系不能传递
  • 如果B是A的友元,C是B的友元,则不能说明C时A的友元。
  • 友元关系不能继承,在继承位置再给大家详细介绍。

其使用方法和友元函数一样,只是将函数换成了类罢了

class Time {
	friend class Date;
public:
	Time(int hour = 0, int minute = 0, int second = 0)
		:_hour(hour),
		_minute(minute),
		_second(second)
	{}
private:
	int _hour;
	int _minute;
	int _second;
};

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

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
		cout << _t._hour << "-" << _t._minute << "-" << _t._second << endl;
	}

private:
	int _year;
	int _month;
	int _day;

	Time _t;
};

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

这里需要注意的是Time声明了Date类,那么Date类就可以使用Time类的私有成员但是Time不能使用Date的私有成员,很反人类。这里我想吐槽一下这个友元的翻译,明明是单方面的索取还要叫友元,遇到这种朋友最好还是远离他,所以也和这里的吐槽一样友元是能不用就最好不要用

四. 内部类

1. 概念

如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。

注意:
内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。

2. 特性

  1. 内部类可以定义在外部类的public、protected、private都是可以的。
  2. 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
  3. sizeof(外部类)=外部类,和内部类没有任何关系。
class A
{
public:
	class B {
	public:
		void Print(const A& a)
		{
			cout << k << endl;//OK
			cout << a._h << endl;//OK
		}
	};
private:
	static int k;
	int _h;
};

int A::k;

int main()
{
	A::B b;
	b.Print(A());
	return 0;
}

总结: 类是对某一类实体(对象)来进行描述的,描述该对象具有那些属性,那些方法,描述完成后就形成了一种新的自定义类型,才用该自定义类型就可以实例化具体的对象。
比如:我要炒青椒炒肉,那么我就需要先将青椒炒肉这个实体进行抽象(即在人为思想层面对青椒炒肉进行认识),青椒炒肉有什么属性,有哪些步骤,即对青椒炒肉进行抽象认知的一个过程。


如有错误,请帮我指出
谢谢

更多推荐