前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >C++之面向对象(下)

C++之面向对象(下)

作者头像
摘星
发布2023-04-28 09:07:40
3760
发布2023-04-28 09:07:40
举报
文章被收录于专栏:C/C++学习

前言

本文继续介绍与C++中与面向对象相关的内容,介绍了构造函数中的初始化列表、隐式类型转换、类的静态成员、友元、内部类、匿名对象以及编译器对拷贝构造的优化等概念。


一、再谈构造函数

1.构造函数体赋值

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

代码语言:javascript
复制
class Data
{
public:
	Data(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};

不能将这一过程称为初识化,只能称为赋初值,因为初始化只能初始化一次,而构造函数的函数体内可以进行多次赋值。那么对象是在什么时候进行初始化的呢?

2.初始化列表

初始化对象是由初始化列表完成的。

1.初始化列表的格式

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

代码语言:javascript
复制
class Data
{
public:
	Data(int year, int month, int day)
		: _year(year),//初始化列表
		  _month(month),
		  _day(day)
	{
	}
private:
	int _year;
	int _month;
	int _day;
};

2.注意的要点

1.每个成员变量只能在初始化列表中出现一次(初始化只能初始化一次) 2.类中包含以下成员必须包含在初始化列表中:

  • 引用成员变量 原因:引用只有一次初始化的机会,不能再改变
  • const成员变量 原因:const变量只有一次初始化的机会,不能再改变
  • 自定义类型成员变量(且该类没有默认构造函数时) 没有默认构造函数的自定义类型变量,必须要进行初始化赋值
代码语言:javascript
复制
class A
{
public:
	A(int a)
		:_a(a)
	{}
private:
	int _a;
};
class B
{
public:
	B(int a, int& ref)
		:_n(10),
		_ref(ref),
		_aobj(a)
	{}
private:
	A _aobj; // 没有默认构造函数
	int& _ref; // 引用
	const int _n; // const
};

3.尽量用初始化列表,因为不管是否显示使用初始化列表,对于自定义的成员变量,一定会先使用初始化列表进行初始化。

代码语言:javascript
复制
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;
};
int main()
{
	Date d(1);
}

4.成员变量在类中的初始化顺序与初始化列表中的顺序无关,而是与该成员变量在类中的声明顺序有关。 观察下列代码,大家认为输出结果会是什么呢? A. 输出1 1 B.程序崩溃 C.编译不通过 D.输出1 随机值

代码语言:javascript
复制
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();
}

运行结果:

在这里插入图片描述
在这里插入图片描述

可以看到输出结果是D.1 随机值 为什么会出现这样的情况呢? 答:因为成员变量初始化的顺序是由它们在类中的声明顺序决定的,而不是初始化列表中的顺序。在未进行初始化之前_a1_a2都是随机值,但是先初始化了_a2,因此_a2就被初始化为_a1的随机值,然后初始化_a1为1。

小总结

  1. 尽量使用初始化列表;
  2. 一个类尽量提供默认构造函数(最好提供全缺省)。

2.explicit关键字(隐式类型转换)

对于单个参数或者除第一个参数外其他参数都有缺省值的构造函数,有隐式类型转换的功能。 观察下面的代码:

代码语言:javascript
复制
class Date
{
public:
	Date(int year, int month = 1, int day = 1)
		:_year(year),
		_month(month),
		_day(day)
	{}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2002);
	Date d2 = 2023;//是否可以这样创建对象并进行初始化呢?
	d1.Print();
	d2.Print();
}

运行结果:

在这里插入图片描述
在这里插入图片描述

从运行结果我们可以看出,可以像上面的代码中Date d2 = 2023;一样进行创建对象并进行初始化,为什么这样的代码可以实现呢? 这是因为在这个过程中发生了隐式类型转换:

在这里插入图片描述
在这里插入图片描述

当然,先进行直接构造再进行拷贝构造是之前的编译器对这种情况进行函数调用的顺序。现在的编译器会省略拷贝构造这一步优化为用2023进行直接构造。但是如果是下面这种情况就无法进行优化:

代码语言:javascript
复制
int main()
{
	const Date& d2 = 2023;//引用的是中间的临时变量,因为临时变量具有常性,所以该对象为const对象(指针和引用的权限不能放大)
	d2.Print();
}

但是这样代码的可读性会降低,如果我们不希望构造函数中存在隐式类型转换的情况,可以使用explicit关键字禁止构造函数的隐式类型转换。

代码语言:javascript
复制
class Date
{
public:
	// 1. 单参构造函数,没有使用explicit修饰,具有类型转换作用
	// explicit修饰构造函数,禁止类型转换---explicit去掉之后,代码可以通过编译
	explicit Date(int year)//单参数构造函数
		:_year(year)
	{}
	//explicit Date(int year, int month = 1, int day = 1)// 2. 多个参数构造函数,创建对象时后面的两个参数可以不传递
	//: _year(year)
	//, _month(month)
	//, _day(day)
	//{}
	Date& operator=(const Date& d)
	{
		if (this != &d)
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
		return *this;
	}
private:
	int _year;
	int _month;
	int _day;
};
void Test()
{
	Date d1(2022);//正常进行初始化
	d1 = 2023;//使用explicit修饰构造函数,会禁止单参数的构造函数类型转换的功能
}

运行错误:

在这里插入图片描述
在这里插入图片描述

二、static成员

1.概念

声明为static的类成员称为静态类成员,用static修饰的类成员变量称为静态成员变量,用static修饰的类成员函数称为静态成员函数。 静态成员变量一定在类外进行初始化,静态成员函数中没有this指针

2.特性

  1. 静态类成员为所有类对象所共享,不在某个具体的类对象中,而是存放在静态区。
  2. 静态成员变量必须在类外定义,不用加static,类中只是声明;
  3. 类静态成员可用类名::类静态成员或者类对象名.类静态成员的方式来访问;
  4. 静态成员函数没有this指针,不能访问任何非静态成员;
  5. 静态成员也是类的成员,受publicprotectprivate的访问限定符限制。

三、友元

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

1.分类分类

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

2.友元函数

1.友元函数的引入

问题:现在尝试去重载operator<<,然后发现没办法将operator<<重载成成员函数。因为cout的输出流对象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作数了。但是实际使用中只有cout是第一个形参对象时,才能正常使用。

代码语言:javascript
复制
class Date
{
public:
	Date(int year, int month, int day)
		: _year(year)
		, _month(month)
		, _day(day)
	{}
	ostream& operator<<(ostream& _cout)
	{
		_cout << _year << "-" << _month << "-" << _day << endl;
		return _cout;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2002,03,27);
	// 因为成员函数第一个参数一定是隐藏的this,所以d1必须放在<<的左侧
	cout << d1 << endl;//不能这样调用
	d1 << cout << endl;// 但是这样调用d1 << cout;->d1.operator<<(&d1, cout); 不符合常规调用
}
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

为了可以常规调用<<,则需要将operator<<重载成全局函数,可以避免this指针抢占第一个参数位。但这样又会导致无法访问类成员,此时就需要使用友元函数。(operator>>同理)。 友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。

代码语言:javascript
复制
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;
}

2.友元函数的说明

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

3.友元类

1.友元类的引入

如果想要在一个类中访问另一个类的成员,就需要将这个类声明为另一个类的友元类。 友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。

2.友元类的说明

  • 友元关系是单向的,不具有交换性; 比如上述Time类Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
  • 友元关系不能传递; 如果C是B的友元, B是A的友元,则不能说明C时A的友元。
  • 友元关系不能继承
代码语言:javascript
复制
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;
};
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;
	}
private:
	int _year;
	int _month;
	int _day;
	Time _t;
};

四、内部类

1.概念

如果一个类定义在另一个类里面。则这个类就叫做另一个类的内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问它,外部类对内部类没有任何特殊的访问权限(即,和其他类或对象的访问限制没有区别)。 但是,内部类天生就是外部类的友元类。友元类的概念参照上文,内部类可以通过外部类的对象参数访问外部类的所有成员,但是外部类不是内部类的友元。

2.特性

  1. 内部类可以定义在外部类的publicprotectprivate的限制中;
  2. 内部类可以直接访问外部类的static成员,而不需要借助外部类的类名或者外部类的对象;
  3. sizeof(外部类)=外部类,和内部类没有关系。 观察下面代码:
代码语言:javascript
复制
class A
{
private:
	static int k;
	int h = 2;
public:
	class B // B天生就是A的友元,但是A不是B的友元
	{
	public:
		void func(const A& a)
		{
			cout << k << endl;//可以访问
			cout << a.h << endl;//可以访问
		}
	};
};
int A::k = 1;
int main()
{
	A::B b;
	b.func(A());
	return 0;
}

运行结果:

在这里插入图片描述
在这里插入图片描述

五、匿名对象

一般情况下,我们定义一个对象都会给它一个对应的一个名字。特殊的,我们可以定义一个没有名字的对象——匿名对象。 这种对象的生命周期只有它定义所在的那一行,运行到下一行就会销毁,属于一次性的对象,所以不需要命名。 观察以下代码:

代码语言:javascript
复制
class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "A(int a)" << endl;
	}
	~A()
	{
		cout << "~A()" << endl;
	}
private:
	int _a;
};
class Solution {
public:
	int Sum_Solution(int n) {
		//...
		return n;
	}
};
int main()
{
	A aa1;
	// 不能像下面这一行这样定义对象,因为编译器无法识别它是一个函数声明,还是对象定义
	//A aa1();
	// 我们可以像下一行这样定义匿名对象,匿名对象的特点是不用取名字,但是他的生命周期只有这一行,到下一行他就会自动调用析构函数
	A();
	A aa2(2);
	// 匿名对象在这样场景下就会好用(只需要使用一次,就可以进行销毁)
	Solution().Sum_Solution(10);
	return 0;
}
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

六、拷贝构造对象时编译器的优化

上文中介绍了一种拷贝构造对象时编译器的优化,接下来我们了解还有哪些拷贝构造对象时优化。 在传参传返回值的过程中,一般编译器会做一些优化,减少对象的拷贝,这个在一些场景下还是非常有用的。 一般编译器会在不影响程序结果的情况下对程序进行一些优化,观察下面代码,了解这几种可以优化的情况:

代码语言:javascript
复制
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;
	f1(aa1);
	cout << endl;
	// 传值返回
	f2();
	cout << endl;
	// 隐式类型,连续构造+拷贝构造->优化为直接构造
	f1(1);
	// 一个表达式中,连续构造+拷贝构造->优化为一个构造
	f1(A(2));
	cout << endl;
	// 一个表达式中,连续拷贝构造+拷贝构造->优化一个拷贝构造
	A aa2 = f2();
	cout << endl;
	// 一个表达式中,连续拷贝构造+赋值重载->无法优化
	aa1 = f2();
	cout << endl;
	return 0;
}
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

七、再次理解类和对象

类是对某一类实体(对象)来进行描述的,描述该对象具有那 些属性,那些方法,描述完成后就形成了一种新的自定义类型,采用该自定义类型就可以实例化出具体的对象。

在这里插入图片描述
在这里插入图片描述

总结

以上就是今天要讲的内容,本文介绍了构造函数中的初始化列表、隐式类型转换、类的静态成员、友元、内部类、匿名对象以及编译器对拷贝构造的优化等相关概念。本文作者目前也是正在学习C++相关的知识,如果文章中的内容有错误或者不严谨的部分,欢迎大家在评论区指出,也欢迎大家在评论区提问、交流。 最后,如果本篇文章对你有所启发的话,希望可以多多支持作者,谢谢大家!

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-03-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 一、再谈构造函数
    • 1.构造函数体赋值
      • 2.初始化列表
        • 1.初始化列表的格式
        • 2.注意的要点
        • 小总结
      • 2.explicit关键字(隐式类型转换)
      • 二、static成员
        • 1.概念
          • 2.特性
          • 三、友元
            • 1.分类分类
              • 2.友元函数
                • 1.友元函数的引入
                • 2.友元函数的说明
              • 3.友元类
                • 1.友元类的引入
                • 2.友元类的说明
            • 四、内部类
              • 1.概念
                • 2.特性
                • 五、匿名对象
                • 六、拷贝构造对象时编译器的优化
                • 七、再次理解类和对象
                • 总结
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档