1

类与对象(下篇)

 2 years ago
source link: https://blog.csdn.net/weixin_46873777/article/details/120976070
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

再谈构造函数

1、构造函数赋值问题

前面我们谈到,构造函数赋值,都是通过有参函数来赋值的。虽然我们通过调用构造函数使对象已经有了一个初始值,但不能将其称为类对象成员的初始化

class A
{
public:
	A(){}
	A(int a)
	{
		_a = a;
	}
private:
	int _a;
};
class Date
{
public:
	Date(int a, int b)
	{
		//函数体内初始化
		_aa = A(a);
		_b = b;
	}
private:
	A _aa;//自定义类型
	int _b;//内置类型
};

重点

构造函数体中的语句只能将其称作为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值

2、初始化列表

  • 那怎么去初始化?
    这里我们就要用到初始化列表

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

举例:Date(int a, int b):_aa(a),_b(b)

class A
{
public:
	A(){}
	A(int a)
	{
		_a = a;
	}
private:
	int _a;
};
class Date
{
public:
	Date(int a, int b):_aa(a),_b(b)
	{
		//函数体内初始化 
//=》对于自定义类型成员来说,改用初始化列表初始化可以提高效率
		/*_aa = A(a);
		_b = b;*/
	}
private:
	A _aa;//自定义类型
	int _b;//内置类型
};

但是我们在初始化列表的时候,需要注意初始化的顺序。如果顺序相反,会导致出现随机值的问题。

class A
{
public:
	A(int a) //_a1和_a2的初始化顺序相反
		:_a2(a)
		,_a1(_a2)		 
	{}
	void Print() {
		cout << _a1 << " " << _a2 << endl;
	}
private:
	int _a1;
	int _a2;
};
int main()
{
	A person(10);
	person.Print();
	return 0;
}

初始化顺序必须与在类中声明的顺序一样,否则会出现随机值

在这里插入图片描述
还有一点就是以下成员必须使用初始化列表进行初始化:

  • 引用成员变量
  • const成员变量
  • 自定义类型成员(该类没有默认构造函数)
class A
{
public:
	A(int a):_a(a)
	{}
private:
	int _a;
};
class B
{
public:
	B(int a, int ref):_aobj(a),_ref(ref),_n(10)
	{}
private:
	A _aobj; // 没有默认构造函数
	int& _ref; // 引用
	const int _n; // const
};

还有一点就是,无法用初始化列表初始化的成员变量,我们可以函数内部初始化,列表初始化和函数内部初始化搭配着用,灵活运用

在这里插入图片描述

  1. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
  2. 类中包含以下成员,必须放在初始化列表位置进行初始化:
  • 引用成员变量
  • const成员变量
  • 自定义类型成员(该类没有默认构造函数
  1. 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
  2. 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关

explicit关键字

构造函数不仅可以构造与初始化对象,对于单个参数的构造函数,还具有类型转换的作用

在这里插入图片描述
构造函数对于单个参数的隐式转换可能会带来我们不想要的。所以我们这里可以加explicit关键字去避免隐式转换。

在这里插入图片描述

用explicit修饰构造函数,将会禁止单参构造函数的隐式转换

static成员

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

  • 静态成员变量位于数据段(内存分布下一节再讲,这里先说明一下)

static的作用

  1. 修饰全局变量和全局函数,改变链接属性,只在当前文件可见
  2. 修饰局部变量,改变声明周期

上面的特性在C++中依旧有用,C++兼容c的这些特性

  1. 修饰成员变量和成员函数,成员变量属于整个类,所有相对像共享,成员函数没有this指针

我们先来看一道面试题:实现一个类,计算中程序中创建出了多少个类对象

int countC = 0;
int countCC = 0;
class A
{
public:
	A()
	{
		++countC;
	}

	A(const A& a)
	{
		++countCC;
	}
};
A f(A a)
{
	A ret(a);//拷贝构造
	return ret;
}
int main()
{
	A a1 = f(A());//匿名对象传参时,不会调用拷贝构造函数
	A a2;
	A a3;
	a3 = f(a2);
	cout << countC << endl;
	cout << countCC << endl;
	return 0;
}

我们这里轻松就能想到用全局变量来计数。但我们使用static具有封装性,使用更安全。

class A
{
public:
	A()
	{
		++countC;
	}

	A(const A& a)
	{
		++countCC;
	}
	//静态成员函数没有this指针
	static int GetACountC() { return countC; }
	static int GetACountCC() { return countCC; }
private:
	static int countC;
	// 存在静态区,属于整个类,也属于每个定义出来的对象共享
	// 跟全局变量比较,他受类域和访问限定符限制,更好体现封装,别人不能轻易修改他
	static int countCC;
};
//静态成员变量不能在构造函数初始化,在全局位置定义初始化
int A::countC = 0;
int A::countCC = 0;
A f(A a)
{
	A ret(a);//拷贝构造
	return ret;
}
int main()
{
	A a1 = f(A());//匿名对象传参时,不会调用拷贝构造函数
	A a2;
	A a3;
	a3 = f(a2);
	//属于整个类,也属于每个定义出来的对象共享
	//我们可以使用类::静态成员或者对象.静态成员来访问
	cout << A::GetACountC() << endl;
	cout << A::GetACountCC() << endl;
	return 0;
}
  • 我们在来看看静态成员为什么无法调用非静态成员?

由于static成员没有this指针,而对象的实例化之后,都是有隐藏的this指针去调用函数或者成员变量,所以这里报错(需要注意)
在这里插入图片描述
给你们留两个问题

  1. 静态成员函数可以调用非静态成员函数吗?

不能,因为静态成员没有隐藏的this指针(this指针忘记的自己看看前几节博客)

  1. 非静态成员函数可以调用类的静态成员函数吗?

可以,因为非静态成员有this指针

总结

  1. 静态成员为所有类对象所共享不属于某个具体的实例
  2. 静态成员变量必须在类外定义,定义时不添加static关键字
  3. 类静态成员即可用类名::静态成员或者对象.静态成员来访问
  4. 静态成员函数没有隐藏的this指针不能访问任何非静态成员
  5. 静态成员和类的普通成员一样,也有public、protected、private3种访问级别,也可以具有返回值

友元是什么?

差不多是这个意思,也就是一个类可以去访问例外一个类里面的成员,不过指的是单方面去访问。

我们先来看一个问题,现在我们尝试去重载operator<<,然后发现我们没办法将operator<<重载成成员函数。

因为cout的输出流对象和隐含的this指针在抢占第一个参数的位置this指针默认第一个参数也就是左操作数了。但是实际使用中cout需要是第一个形参对象,才能正常使用。所以我们要将operator<<重载成全局函数。但是这样的话,又会导致类外没办法访问成员,那么这里就需要友元来解决。operator>>同理

class Date
{
public:
	//必须弄为友元,类外才能访问成员
	friend ostream& operator<<(ostream& _cout, const Date& d);//加const,输出运算符重载,不需要改变
	friend istream& operator>>(istream& _cin, Date& d);//输入运算符重载,需要改变
	Date(int year, int month, int day)
		: _year(year)
		, _month(month)
		, _day(day)
	{}
private:
	int _year;
	int _month;
	int _day;
};
//输出流
ostream& operator<<(ostream& _cout, const Date& d)
{
	_cout << d._year << "-" << d._month << "-" << d._day;
	return _cout;
}
//输入流
istream& operator>>(istream& _cin, Date& d)
{
	_cin >> d._year >> d._month >> d._day;
	return _cin;
}
int main()
{
	Date d(2017, 12, 24);
	cout << d;
	cin >> d;
	return 0;
}

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

class Date
{
	friend void Print(const Date& d);
private:
	int _year = 2021;//声明,这里是缺省值,不是初始化的值,更不是定义
	int _month = 10;
	int _day = 27;
};
void Print(const Date& d)
{
	cout << d._year << " " << d._month << " " << d._day << endl;
}
int main()
{
	Date d;
	Print(d);//友元函数调用
	return 0;
}

友元函数跟普通函数调用方法一样。
在这里插入图片描述
还有一点性质,一个函数可以是多个类的友元函数
在这里插入图片描述

友元函数总结

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

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

  • 友元关系是单向的,不具有交换性。

比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行

  • 友元关系不能传递

如果B是A的友元,C是B的友元,则不能说明C是A的友元。

class Time
{
	friend class Date;   // 声明日期类为时间类的友元类,则在日期类中就直接访问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;
};

// 友元关系是单向的,Date是Time的友元,在Date类中可以使用对象访问Time的私有保护成员
// 但是Time不是Date的友元,在Time类中不可以使用对象访问Date的私有保护成员
class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
		: _year(year)
		, _month(month)
		, _day(day)
	{}

	void SetTimeOfDate(int hour, int minute, int second)
	{
		// 直接访问时间类私有的成员变量
		_t._hour = hour;
		_t._minute = minute;
		_t._second = second;
		cout << _t._hour << " " << _t._minute << " " << _t._second << endl;
	}

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

int main()
{
	Date d;
	d.SetTimeOfDate(16, 50, 30);
	return 0;
}

在这里插入代码片
最后分享一张图片,对于类和对象的理解。
在这里插入图片描述

没说的:内部类,再次理解封装,这些自己下去看看就好,要理解,不要去死记硬背类与对象。

在这里插入图片描述
点赞富三代


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK