C++初阶学习-类和对象(下篇)

文章介绍了C++中构造函数的作用,强调了初始化列表在对象初始化中的重要性,特别是对于const成员、引用成员和自定义类型成员的初始化。同时,讨论了explicit关键字用于防止隐式类型转换,以及static成员的特性和友元的概念,包括友元函数和友元类,展示了它们如何打破封装并提供额外的功能。最后,文章再次强调了封装在面向对象编程中的意义。
摘要由CSDN通过智能技术生成

初始化列表-再谈构造函数

回顾构造函数

前言

C++初阶学习-类和对象(上篇)的学习中我们知道了构造函数中有三个默认成员函数,而且构造函数是支持函数重载的,若编译器识别到类中有构造函数时(无论是否是默认构造),都不再会自动生成默认构造函数。
回顾完这个后,我们来看下面这段代码:

#include<iostream>
using namespace std;
class Time
{
public:
	Time(int hour = 0)//默认构造
	{
		_hour = hour;
	}
private:
	int _hour;
};
class Date
{
public:
	Date(int year, int hour)//构造
	{
		_year = year;
		Time t(hour);
		_t = t;
	}
private:
	int _year;
	Time _t;
};
int main()
{
	Date d1(1,0);
	return 0;
}

我们分析一下这段代码:
Date类的成员变量中有一个自定义类型Time,并且Time类中是有默认构造函数(全缺省的有参),而Date类中没有默认构造函数只有构造函数,因此,实例化对象时,通常地,我们会认为初始化对象的过程是通过传参数给构造函数,并在函数体内“ 初始化成员变量 ”,对于自定义类型,我们看从代码逻辑实现上看,也是同样的通过实例化出一个对象t在赋值给_t,但事实真的是如此吗?

在这里插入图片描述
我们对上述代码做一下小改变,将Time类中的默认成员函数去掉缺省值变成构造函数,我们看看这样会发生什么

#include<iostream>
using namespace std;
class Time
{
public:
	Time(int hour)//构造
	{
		_hour = hour;
	}
private:
	int _hour;
};
class Date
{
public:
	Date(int year, int hour)//构造
	{
		_year = year;
		Time t(hour);
		_t = t;
	}
private:
	int _year;
	Time _t;
};
int main()
{
	Date d1(1,0);
	return 0;
}

运行结果:
在这里插入图片描述
奇怪?为什么会这样呢?不是在函数体内实体化了吗?不是通过传参给构造函数初始化了吗?怎么会没有默认构造函数可用呢?
事实上,上述的过程,其实也不是一个对象初始化的过程,而只能叫赋值过程。让我们来一步一步来探讨:

  • 首先对初始化的定义重新再加深一下
    虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量
    的初始化,构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。

也就是说,要想真正(虽然某些场景可以但要理解这个真正)初始化对象,不能通过构造函数,要通过默认构造函数,并且,如果要初始化带有自定义类型的对象,连默认构造函数也不能做到,还要通过编译器自动生成的 “默认生成构造函数“ 才能成功初始化自定义类型,而且前提还是要在自定义类型的含有默认构造函数的情况下。很绕,也很坑,那到底有没有别的方法能够来初始化这个对象呢?
下面就引出内容—初始化列表,我们先来了解什么是初始化列表,才能对上述问题做出解答。

初始化列表

我们知道,对于有些变量或函数来说,可以先声明,后定义,也就是不直接初始化。但是像是const修饰的变量和引用这些在声明时就一定要定义,即出生就要初始化。而初始化列表,就可以理解为成员变量定义的地方,也就是说,每个成员变量出生的时候(实例化出对象时),都一定要经过初始化列表这个过程(无论有没有显式定义)。

  • 初始化列表的基本格式为:
    以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个成员变量后面跟一个放在括号中的初始值或表达式。
Date(int year=1, int hour=4)
		:_year(year),_t(hour)
	{
	//函数体内放不放都无所谓,后面会提到
	}
  • 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
//错误代码
Date(int year=1, int hour=4)
		:_year(year),_t(hour),_year(year)//不能多次初始化
	{
	//函数体内放不放都无所谓,后面会提到
	}
  • 类中包含以下成员,必须放在初始化列表位置进行初始化:
  1. 引用成员变量
  2. const成员变量
  3. 自定义类型成员(且该类没有默认构造函数时)

当成员变量中有const修饰的成员或者是引用时,母庸质疑,他们一出生就必须要被初始化,因此初始化列表中一定要有他们的位置:

class A
{
public:
	A(int &x)
	:_a(10),b(x)
	{
	}
private:
	const int _a;
	int &b;
};

而对于自定义类型成员,初始化列表会去调用它的默认构造函数或者构造函数,看到这,你是否想到这一点是不是跟默认生成构造函数特性十分相像

  • 若初始化列表没有显示定义时,会去调用自定义类型的默认构造函数
  • 若有显示定义时,则通过传参数去调用构造函数(或者默认构造)
  • 对于内置类型,如果有显示定义则会初始化,没有就不会

看到这,如果你还是懵的状态,没关系,我们来回忆一下前面的代码。
首先是:Time类型有默认构造函数的时候:

在这里插入图片描述
事实并非我们先前所想,函数体内并不属于初始化,而是赋值,让我们再看回Time类没有默认构造函数的情况
在这里插入图片描述
到这里,你是否恍然大悟,为什么前面的代码会报错
而对于内置类型(非const和非引用),你可以选择在函数体内进行初始化,也可以选择在初始化列表初始化。

  • 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
    在这里插入图片描述

总结:
初始化列表,是成员变量定义的地方,因此推荐初始化都在初始化列表内完成(其中const修饰,引用和自定义类型成员变量一定要在初始化列表完成)而内置类型(非const非引用),若没有遇到特殊场景,也都在初始化列表内显示定义完成。
特殊场景比如有:
在这里插入图片描述

值得一提的是,是否还记得C++11打的一个补丁,就是在成员变量声明时可以提供缺省值,实际上,缺省值是给初始化列表的,若初始化列表显示定义了,则缺省值就不再有用。
在这里插入图片描述

explicit关键字

构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用。
下面我们通过代码来认识:

//我们平常见的隐式类型转换:
int a=10;
double b=a;
//面向对象的隐式类型转换:
class Date
{
public:
	Date(int year)
		:_year(year)//构造
	{
	}
private:
	int _year = 1;
};
int main()
{
	Date d1(2023);
	Date d2 = 2023;//隐式类型转换
	return 0;
}

我们都知道隐式类型转换存在着临时变量的产生(见引用部分),所以这里的隐式类型转换要通过以下过程:首先,创造一个Date类的临时变量,并让2023作为参数传给构造函数,然后通过拷贝构造函数将临时变量拷贝给d2,这样就完成了转换(前提是单参数的拷贝构造)这样做没有别的好处,仅是看着方便,可根据需求来。
由于这个过程包含1次构造+1次拷贝构造,因此编译器对其进行了优化,直接优化成1次构造
由于临时变量具有常性,因此很多地方都需要注意用const修饰(比如形参),避免权限的放大。
而explicit关键字,仅仅是为了来阻止隐式类型转换的发生:

class Date
{
public:
	explicit Date(int year)//阻止发生隐式类型转换
		:_year(year)//构造
	{
	}
private:
	int _year = 1;
};
int main()
{
	Date d1(2023);
	Date d2 = 2023;//隐式类型转换
	return 0;
}

编译运行结果为:
在这里插入图片描述
有些时候会认为这样的代码可读性不好,因此就会用explicit关键字阻止这种类型转换的发生。

Static成员

概念

声明为static的类成员称为类的静态成员。用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化。(不能在初始化列表初始化)
在这里插入图片描述

特性

  1. 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
    在这里插入图片描述
    它属于整个类,所有对象都只有静态区的这一个。

  2. 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明

  3. 静态成员也是类的成员,受public、protected、private 访问限定符的限制

  4. 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问

  5. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
    在这里插入图片描述

友元

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

友元函数

类和对象(中篇)中,我们尝试着实现一个完整的日期类,我们现在再丰富一下功能,重载 << 使得能够打印日期(而不再是调用Print函数)
首先,我们来简单介绍一下cout和cin流
在这里插入图片描述
在这里插入图片描述

我们知道,cout和cin打印和输出时能够自动识别类型,这其实是重载的功劳
在这里插入图片描述

参考这个,我们想要支持打印自定义类型,我们也来动手重载<<
由于属于重载运算符,按照惯例,我们当然会把其作为成员函数。但会遇到如下问题:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
因此我们只能选择在全局中重载:
在这里插入图片描述
但又遇到一个新问题,那就是类外不可以访问私有成员变量,那如何解决新问题呢?
有两种方法:

  1. 写一个获取成员变量的函数
  2. 利用友元
    在这里插入图片描述
    友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。
    这样一来,即使是属于全局的函数也可以发访问私有成员变量
    完整代码:
class Date
{
	friend ostream& operator<<(ostream& out, const Date& d);
	friend istream& operator>>(istream& out, Date& d);
	public:
	...//省略,见上篇
	private:
	...//省略,见上篇
	};
ostream& operator<< (ostream & out, const Date &d)
{
	out << d._year << "-" << d._month << "-" << d._day << endl;
	return out;
}
istream& operator>> (istream& in, Date& d)
{
	in >> d._year >> d._month >> d._day;
	assert(d.CheckDate());
	return in;		
}

友元函数说明:

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

友元类

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

  • 友元关系是单向的,不具有交换性。
    比如下述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
  • 友元关系不能传递
    如果C是B的友元, B是A的友元,则不能说明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;
};
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. 内部类可以定义在外部类的public、protected、private都是可以的。
  2. 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
  3. sizeof(外部类)=外部类,和内部类没有任何关系。
class A
{
private:
 	static int k;
	 int h;
public:
 class B // B天生就是A的友元
 {
 public:
 	void foo(const A& a)
 {
 	cout << k << endl;//OK
 	cout << a.h << endl;//OK
 }
 };
};
int A::k = 1;
int main()
{
    A::B b;
    A a;
    b.foo(a);
    return 0;
}

再次理解封装

现实生活中的实体计算机并不认识,计算机只认识二进制格式的数据。如果想要让计算机认识现实生活中的实体,用户必须通过某种面向对象的语言,对实体进行描述,然后通过编写程序,创建对象后计算机才可以认识。比如想要让计算机认识洗衣机,就需要:

  1. 用户先要对现实中洗衣机实体进行抽象—即在人为思想层面对洗衣机进行认识,洗衣机有什么属性,有那些功能,即对洗衣机进行抽象认知的一个过程
  2. 经过1之后,在人的头脑中已经对洗衣机有了一个清醒的认识,只不过此时计算机还不清楚,想要让计算机识别人想象中的洗衣机,就需要人通过某种面相对象的语言(比如:C++、Java、Python等)将洗衣机用类来进行描述,并输入到计算机中
  3. 经过2之后,在计算机中就有了一个洗衣机类,但是洗衣机类只是站在计算机的角度对洗衣机对象进行描述的,通过洗衣机类,可以实例化出一个个具体的洗衣机对象,此时计算机才能洗衣机是什么东西。
  4. 用户就可以借助计算机中洗衣机对象,来模拟现实中的洗衣机实体了。
    在类和对象阶段,大家一定要体会到,类是对某一类实体(对象)来进行描述的,描述该对象具有那些属性,那些方法,描述完成后就形成了一种新的自定义类型,才用该自定义类型就可以实例化具体的对象。
    在这里插入图片描述
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值