C++类和对象:初始化列表、static成员和友元

目录

一. 初始化列表

1.1 对象实例化时成员变量的创建及初始化

1.2 初始化列表

1.3 使用初始化列表和在函数体内初始化成员变量的效率比较

1.4 成员变量的初始化顺序

1.5 explicit关键字

二. static成员

2.1 static属性的成员变量

2.2 static属性的成员函数

三. 友元

3.1 友元函数

3.2 友元类


一. 初始化列表

1.1 对象实例化时成员变量的创建及初始化

以演示代码1.1中的日期类Date为例,Date类中有一个用户显示定义的构造函数,来实现在类实例化时对成员变量赋初值。但是,成员变量并不是在构造函数的函数体内部进行创建的,在进入构造函数的函数体内部之前,成员变量会先被给一个随机的值,然后到构造函数内部改变这个随机的值来让成员变量初始化为用户希望的值。

因此,我们可以认为,Date成员变量是先被创建出来(不进行初始化),然后在被赋初值的。

演示代码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;
};

1.2 初始化列表

先将成员变量创建出来,再初始化,相比于创建时直接初始化,效率相对较低,而且,对于引用、具有const属性以及没有默认构造函数的自定义类型成员变量,其必须在定义创建的同时完成初始化。因此,在函数体内部初始化成员变量就有其局限性。

为了实现在定义创建成员变量的同时进行初始化,C++引入了初始化列表。其语法格式为:以一个冒号开始(冒号在函数声明的后面),以逗号分隔成员变量,每个成员变量后面跟一个括号表示被初始化的值。

演示代码1.2通过初始化列表,实现了对日期类三个成员变量的初始化。

演示代码1.2:

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
		//初始化列表
		: _year(year)
		, _month(month)
		, _day(day)
	{}
private:
	int _year;
	int _month;
	int _day;
};

注意,下面三种类型的成员变量只能采用初始化列表进行初始化:

  1. 引用类型成员变量。
  2. 具有const属性的成员变量。
  3. 没有默认构造函数的自定义类型成员变量。

演示代码1.2:(三种只能使用初始化列表的成员变量的初始化)

class A
{
public:
	A(int a)
		:_a(a)
	{}

private:
	int _a;
};

class B
{
public:
	B(A& a, int& ref)
		//没有默认成员函数的内置类型、引用以及const属性成员变量
		//只能用初始化列表初始化
		: _a1(a)
		, _ref(ref)
		, _n(10)
	{}

private:
	A _a1;   //没有默认成员函数的内置类型
	int& _ref;  //引用
	const int _n;  //const属性成员变量
};

1.3 使用初始化列表和在函数体内初始化成员变量的效率比较

演示代码1.3创建一个含有默认构造函数的类AA,在类A中包含类型为AA的自定义成员变量,先后显示定义两种不同形式的默认构造函数:一是在函数体内部初始化自定义成员变量,二是在初始化列表初始化自定义类型成员变量。

为了方便观察两种情况下构造函数、拷贝构造函数和赋值运算符重载函数分别调用了几次,函数应输出相关信息。运行代码,可以看到,如果在函数体内部初始化,需要调用两次调用构造函数、一次调用赋值运算符重载函数,如果使用初始化列表初始化,仅需要调用构造函数和拷贝构造函数各一次。对于较大的自定义类型成员变量,使用初始化列表可以显著提高程序运行效率。

这是因为,使用初始化列表在成员变量被定义出来的同时完成了初始化,而在函数体内部初始化则将成员变量的定义和初始化拆分为两个步骤。

演示代码1.3:

class AA
{
public:
	AA(int a = 1, int b = 2)  //构造函数
	{
		cout << "AA(int a, int b)" << endl;
		_a = a;
		_b = b;
	}

	AA(const AA& aa)  //拷贝构造函数
	{
		cout << "AA(AA& aa)" << endl;
		_a = aa._a;
		_b = aa._b;
	}

	AA& operator=(const AA& aa)  //赋值运算符重载函数
	{
		cout << "AA& operator=(const AA& aa)" << endl;
		_a = aa._a;
		_b = aa._b;

		return *this;
	}

private:
	int _a;
	int _b;
};

class A
{
public:
	//函数体内初始化
	//A(int c, int d, const AA& aa)  //这里会调用初始化列表创建_aa,但给随机初值
	//{
	//	_c = c;
	//	_d = d;
	//	_aa = aa;
	//}

	//初始化列表初始化
	A(int c, int d, const AA& aa)
		: _c(c)
		, _d(d)
		, _aa(aa)
	{}

private:
	int _c;
	int _d;
	AA _aa;
};

int main()
{
	AA aa1;
	A a1(10, 20, aa1);
	return 0;
}
图1.1  在函数体内初始化(左)和采用初始化列表初始化(右)时程序运行结果

总结:对于有默认构造函数的自定义类型成员变量,可以在函数体内部初始化,也可以使用初始化列表初始化,但为了提高程序运行的效率,一般建议采用初始化列表初始化。

1.4 成员变量的初始化顺序

成员变量初始化的顺序与声明顺序一致,与初始化列表的顺序无关。如演示代码1.4所示,虽然初始化列表中_a1在前_a2在后,但是_a2依然先被初始化,_a1初始化后的值并没有被赋给_a2。因此,程序的运行结果并不是1  1,而是1  随机值 。

建议:成员变量的声明顺序与初始化列表的顺序保持一致。

演示代码1.4:

class AA
{
public:
	AA(int a1 = 10, int a2 = 20)
		:_a1(1)
		, _a2(_a1)   //按照声明顺序,先初始化_a2再初始化_a1
	{}

	void Print()
	{
		cout << "_a1 = " << _a1 << endl;
		cout << "_a2 = " << _a2 << endl;
	}

private:
	int _a2;
	int _a1;
};

int main()
{
	AA aa(1,1);
	aa.Print();
	return 0;
}
图1.2  演示代码1.4的运行结果

1.5 explicit关键字

如果一个构造函数只有一个参数,或者除了第一个参数外每个参数都有默认的缺省值,那么,就可以通过隐式类型转换来实现对象的实例化。

演示代码1.5定义了一个类Year,通过Year y1 = 2023,实现通过隐式类型转换完成对象实例化。但是,某些时候我们不希望这种隐式类型转换的发生,C++为此提供了一个关键字explicit,在函数声明之前添加explicit,即可禁止这种隐式类型转换。

演示代码1.5:

class Year
{
public:
	//函数前加explicit,禁止隐式类型转换
	explicit Year(int year = 2023);

	void Print();

private:
	int _year;
};

Year::Year(int year)  //构造函数
	: _year(year)
{
	cout << "Year(int year = 2023)" << endl;
}

void Year::Print()
{
	cout << _year << endl;
}

int main()
{
	Year y1(2020);
	//Year y2 = 2023;   //存在隐式类型转换
	y1.Print();
	//y2.Print();
	return 0;
}

二. static成员

2.1 static属性的成员变量

  • 静态(static)成员变量属于整个类,并非单独属于某个类对象,而是所有类对象共享。
  • 必须在类外部定义static成员变量和给定初始化值,在类内部只是声明static成员变量。

通过static成员变量,可以实现统计一个类被实例化的次数。如演示代码2.1所示,在类A中定义了一个static成员变量_s_count,每当构造函数或拷贝构造函数被调用时,执行++_s_count操作,表示创建了一个类对象。运行演示代码2.1,_s_count的值为3,表示进行了3次对象实例化,调用构造函数创建对象a1第一次、调用构造函数创建对象a2第二次、调用f()函数传参时拷贝构造形参第三次。

注意:static属性的成员变量存储在静态区,在使用sizeof计算类或类对象大小时,static成员不包含在其中。

演示代码2.1:(统计类实例化次数)

//统计类对象总共被创建的次数
//构造函数和拷贝构造函数都会创建类对象
class A
{
public:
	A(int a = 5)   //构造函数
		: _a(a)
	{
		cout << "A(int a = 5)" << endl;
		++_s_count;
	}

	A(A& a)   //拷贝构造函数
		: _a(a._a)
	{
		cout << "A(A& a)" << endl;
		++_s_count;
	}

	//非静态成员函数
	int GetSumCount()
	{
		return _s_count;
	}

private:
	int _a;
	static int _s_count;
};

int A::_s_count = 0;   //初始化静态成员变量

void f(A a)
{ }

int main()
{
	A a1;
	A a2 = 1;
	f(a1);

	//通过对象调用函数获取_s_count的值
	cout << "_s_count = " << a1.GetSumCount() << endl;   

	return 0;
}
图2.1 演示代码2.1的运行结果

2.2 static属性的成员函数

static属性的成员函数严格来说并不能算是类的成员函数,其没有this指针,不能访问非static属性的成员变量或调用非static属性的成员函数。

静态成员函数有两种访问方式:(1)对象名.函数名(参数列表)、(2)类名::函数名(参数列表)

  • static成员也受访问限定符的限制,不能在类外部访问private和protect属性的static成员。
  • 虽然静态成员函数不能调用非静态成员函数,但是非静态成员函数可以调用静态成员函数。
class A
{
public:
	static void GetNum();   //静态成员函数

private:
	int _a1 = 10;
	static int _a2;
};

int A::_a2 = 20;   

void A::GetNum()
{
	//cout << _a1 << endl;   //_a1为非静态成员变量,静态成员函数不能访问
	cout << "_a2 = " << _a2 << endl;
}

int main()
{
	A a;
	A::GetNum();  //通过类名调用static属性函数
	A().GetNum();   //通过对象调用static属性函数,A()为匿名对象
	return 0;
}

三. 友元

友元可以分为友元函数和友元类。有一点要事先声明:友元是一种严重破坏封装的行为,一般建议友元要少用慎用。

3.1 友元函数

对于一个类中的private或protect属性成员,全局函数不能访问它们,但是,如果我们将某个全局函数声明为类的友元函数,那么这个全局函数就可以不受访问限定符的约束,随意访问类中的成员变量和成员函数。

友元函数的声明形式:friend 返回类型 函数名(参数列表)

演示代码3.1展示了友元函数的定义和使用,在类A中声明Print为A的友元,则全局函数Print可以访问A的私有属性成员变量并输出其值。这里还有一点需要注意:友元函数并不是某个类的成员函数,它是属于全局的。

演示代码3.1:

class A
{
	friend void Print(A& a);   //声明Print为A的友元
public:
	A(int a1 = 10, int a2 = 20)  //构造函数
		: _a1(a1)
		, _a2(a2)
	{}

private:
	int _a1;
	int _a2;
};

void Print(A& a)
{
	cout << "_a1 = " << a._a1 << endl;
	cout << "_a2 = " << a._a2 << endl;
}

int main()
{
	A a;
	Print(a);  //Print为类A的友元
	return 0;
}
图3.1 演示代码3.1的运行结果

3.2 友元类

如果定义一个类A为类B的友元,那么这个类B中的任意一个成员函数就可以访问类A中的private或public属性成员。需要注意的是,友元类没有传递性和交互性,即:

  • 如果B是A的友元,C又是B的友元,不能认为C就是A的友元。
  • 假设B是A的友元,B可以访问A的非公有属性成员,我们也不能认为A是B的友元,即一般不能在类A中访问类B的非公有属性成员。

友元类的声明方式:friend class 类名

演示代码3.2定义了一个日期类Date和一个时间类Time,声明Date类是Time类的友元,那么,我们就可以在Date类中定义一个SetTimeOfDay函数,来设置Date类的Time类型成员变量。进入调试界面,打开监视窗口,看到Time类型成员_t的每个成员变量的值都依据调用SetTimeOfDay时传递的参数改为了12。

演示代码3.2:

class Time
{
	friend class Date;  //友元类声明

public:
	Time(int hour = 1, int minute = 1, int second = 1)
		: _hour(hour)
		, _minute(minute)
		, _second(second)
	{}

private:
	int _hour;
	int _minute;
	int _second;
};

class Date
{
public:
	Date(int year = 2023, int month = 3, int day = 2)  //构造函数
		: _year(year)
		, _month(month)
		, _day(day)
	{}

	//Date类的成员函数实现对Time类非公有属性成员的访问
	void SetTimeOfDay(int hour, int minute, int second)
	{
		_t._hour = hour;
		_t._minute = minute;
		_t._second = second;
	}

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

int main()
{
	Date d;  //使用默认值完成类实例化
	d.SetTimeOfDay(12, 12, 12);  //时间设为12时12分12秒
	return 0;
}
图3.2 演示代码3.2

 

  • 7
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值