【C++】类和对象(下)

👀樊梓慕:个人主页

 🎥个人专栏:《C语言》《数据结构》《蓝桥杯试题》《LeetCode刷题笔记》《实训项目》《C++》

🌝每一个不曾起舞的日子,都是对生命的辜负

目录

前言

1.初始化列表

1.1引入

1.2初始化列表

1.3explicit关键字 

2.Static成员

2.1概念

2.2特性

3.友元

3.1友元函数

3.2友元类

4.内部类

5.编译器的一些优化


前言

本篇文章是类和对象部分的收官之作,主要讲解初始化列表、构造函数的一些补充知识,static成员,上篇文章提到的友元函数,内部类以及如何理解封装。

欢迎大家📂收藏📂以便未来做题时可以快速找到思路,巧妙的方法可以事半功倍。

=========================================================================

GITEE相关代码:🌟fanfei_c的仓库🌟

=========================================================================

1.初始化列表

1.1引入

在学习这部分知识前,我们不妨先回忆下之前成员变量是如何初始化的?

比如:

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

但这样的初始化形式难免会遇到一些不能解决的类型,比如引用成员变量和const成员变量以及自定义类型成员(且该类没有默认构造函数时)。

如下图所示,该位置的意义是成员变量的声明而不是定义。

那C++中我们都知道应该是在创建对象时整体定义的。

但是每个成员变量是在什么地方定义的呢?

如果像是引用成员变量和const成员变量这种必须在定义时就初始化的变量怎么办?

这是C++引用了初始化列表这一概念。

1.2初始化列表

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

比如:

class Date
{
public:
	Date(int year, int month, int day)
		: _year(year)
		, _month(month)
		, _day(day)
	{}//花括号中和以前一样可以实现构造函数的功能
private:
	int _year;
	int _month;
	int _day;
};

1. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)。

2. 类中包含以下成员,必须放在初始化列表位置进行初始化:

  • 引用成员变量
  • const成员变量
  • 自定义类型成员(且该类没有默认构造函数时) 
class A
{
public:
	A(int a)//需要传参的构造函数不是默认构造函数哦!
		:_a(a)
	{}
private:
	int _a;
};
class B
{
public:
	B(int a, int ref)
		:_aobj(a)//_aobj初始化为a    就不需要默认构造函数了
		, _ref(ref)//_ref初始化为ref
		, _n(10)//n初始化为10
	{}
private:
	A _aobj; // 没有默认构造函数
	int& _ref; // 引用
	const int _n; // const
};

3. 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。 

并且以前的这种写法,这个地方的缺省值是给初始化列表的。

注意:每个成员变量都会在初始化列表定义,不管你再初始化列表里写没写,未指定时,默认内置类型赋为随机值,自定义类型会去调用默认构造。 

4. 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。

请看下面的代码:

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(1);
	aa.Print();
} 
//A.输出1 1
//B.程序崩溃
//C.编译不通过
//D.输出1 随机值

答案:D 

如定义所讲,成员变量在类中的生命次序就是初始化列表中的初始化顺序,所以根据声明次序,初始化列表中先执行_a2(_a1) ,再执行_a1(a),导致在执行_a2(_a1) 这句时,_a1的值还未可知,所以导致最后的结果为D。

  • 我们建议声明和初始化列表顺序保持一致,避免出现理解问题。

那讲到这,很多同学会有疑惑,既然初始化列表这么好用,函数体初始化还有存在的必要么? 

  • 有必要!

因为有些初始化或者检查(malloc、memset、perror)工作,初始化列表也不能全部搞定。

建议80%-100%初始化列表搞定,剩下配合函数体初始化使用。

1.3explicit关键字 

请看下面的代码:

A aa1 = 1;

这中间的过程是怎样的呢?

实际上发生了一次隐式类型转换。 

编译器会先对1构造一个临时对象,然后再将该临时对象拷贝构造给aa1。

💥但是这个过程是有前提的💥

 前提是由A类的单参数构造函数(只有一个参数或者是多参缺省)支持。

比如:

class A {
	A(int a)//单参数构造函数
		:_a(a)
	{}
};
class A {
	A(int a,int b=1,int c=1)// 多参缺省构造函数
		:_a(a)
        ,_b(b)
        ,_c(c)
	{}
};

 如果不想让转换发生,就需要在构造函数前加explicit关键字

比如:

class A {
	explicit A(int a)
		:_a(a)
	{}
};

2.Static成员

2.1概念

声明为static的类成员称为类的静态成员:

  • 用static修饰的成员变量,称之为静态成员变量;
  • 用static修饰的成员函数,称之为静态成员函数。

静态成员变量一定要在类外进行初始化。

静态成员函数和静态成员变量,本质是受限制的全局变量和全局函数,受类域和访问限定符的限制。

2.2特性

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

一个小知识:类名()这种写法叫做匿名对象,他的生命周期只在这一行,如A()。 

来看两道问题:

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

答:不能,因为没有this指针。

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

答:可以。

3.友元

友元提供了一种突破封装的方式,有时提供了便利。

但是友元会增加耦合度,破坏了封装,所以友元不宜多用。

友元分为:友元函数和友元类。

3.1友元函数

在运算符重载那一篇文章中📢樊梓慕->运算符重载,我们提到过友元函数的概念。

我们知道一般的运算符重载我们可以放到类内部实现来避免成员变量私有的问题。

但当我们想要重载流运算符时却遇到了问题,因为如果流运算符也在类内部重载的话,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关键字。 

用法如下:

class Date
{
	friend ostream& operator<<(ostream& _cout, const Date& d);//友元的声明
	friend istream& operator>>(istream& _cin, Date& d);
public:
	Date(int year = 1900, int month = 1, int day = 1)
		: _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;
	_cin >> d._month;
	_cin >> d._day;
	return _cin;
}
int main()
{
	Date d;
	cin >> d;
	cout << d << endl;
	return 0;
}

友元函数的使用需要注意以下问题:

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

3.2友元类

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

1.友元关系是单向的,不具有交换性

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

(Time说(声明)Date是我的朋友,他可以来我家玩,但是Date并不一定这么认为)

2.友元关系不能传递

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

3.友元关系不能继承

4.内部类

概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。

内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员

外部类对内部类没有任何优越的访问权限。

注意:内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。(外部类认为内部类是他的朋友,但内部类并不这么认为)

特性:

  • 内部类可以定义在外部类的public、protected、private都是可以的。
  • 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
  • sizeof(外部类)=外部类,和内部类没有任何关系。

5.编译器对连续构造、拷贝构造次数的优化

较为新版的编译器会对构造、拷贝构造的次数进行优化。

也就是说他会较为智能的节省拷贝与拷贝构造的次数。

注意:本篇文章所讲的编译器优化不适用所有编译器,只是一般情况,还有一些编译器跨表达式也可以优化。

比如:

同一个表达式中(需要特别注意是在同一个表达式中)

  • 构造+构造 -> 构造
  • 构造+拷贝构造 -> 构造
  • 拷贝构造+拷贝构造 -> 拷贝构造

下面我已经构建好了一些优化的场景,大家可以学习下。

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 func1(A aa1) {}
A func2()
{
	A aa;
	return aa;
}
int main()
{
	// 1、先用1构造一个临时对象,再用临时对象拷贝构造aa1
	A aa1 = 1;// 构造+拷贝构造->构造
	// 2、先用2构造一个临时对象,再用临时对象拷贝构造aa2
	//这个例子证明了优化是现实存在的
	//因为aa2不能直接引用常量,所以这里实际上先构造了一个临时对象,然后将该临时对象拷贝构造给aa2
	const A& aa2 = 2;	// 构造+拷贝构造->构造
	//3、未在同一表达式,不优化(跨表达式在一些编译器上也会优化,但建议大家还是默认不优化)
	A aa(1);//构造
	func1(aa);//拷贝构造
	//4、先构造了一个临时对象,然后将该临时对象拷贝构造给形参aa1
	func1(A(2));//构造+拷贝构造->构造 
	//5、单参数传参支持这样写,先构造一个临时对象,再将该临时对象拷贝构造给形参aa1
	func1(3);//构造+拷贝构造->构造 
	//6、函数内部,先构造aa,然后调用拷贝构造保存aa的值,出作用域析构,将之前拷贝构造再拷贝构造给aa3
	A aa3 = func2();//拷贝构造+拷贝构造->拷贝构造

	/*********************************注意***********************************/
	A aa4(aa1);  // 拷贝构造
	A aa5 = aa1; // 拷贝构造 or 赋值拷贝

	// 两个已经存在的对象拷贝,赋值拷贝
	aa4 = aa5;
	return 0;
}

=========================================================================

如果你对该系列文章有兴趣的话,欢迎持续关注博主动态,博主会持续输出优质内容

🍎博主很需要大家的支持,你的支持是我创作的不竭动力🍎

🌟~ 点赞收藏+关注 ~🌟

========================================================================= 

文章出处登录后可见!

已经登录?立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
乘风的头像乘风管理团队
上一篇 2023年12月11日
下一篇 2023年12月11日

相关推荐