类与对象(下)

构造函数新理解

构造函数函数体赋值

根据前面讲的构造函数,在创建对象的时候,编译器可以通过调用构造函数,通过函数内的语句给对象中的各个成员变量一个合适的初始值。

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

这样子对象中的各个成员变量都有了一个初始值,但是这个并不能称为类对象成员的初始化,只能称之为赋初值。因为初始化对于变量来说只能初始化一次,而调用构造函数,在构造函数的函数体中可以对成员变量的值进行多次改变。

那么既然构造函数只能称为赋初值,那么初始化是怎样进行的呢?
就是我们下面要介绍的初始化列表:

初始化列表

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

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、如果一个类中包含以下的变量,必须在初始化列表的位置进行初始化:
(1) 引用成员变量。这里的引用变量是声明,而不是定义,所以不会报错(引用必须引用已经定义的变量,这里当作声明编译器不会报错,在用类定义对象之后直接引用)。
(2) const成员变量。const变量只能初始化一次,所以必须在初始化列表中初始化。
(3) 自定义类型成员,这里的自定义类型成员必须是没有默认的构造函数,如果有默认构造函数,编译器会先去调用它的默认构造函数,不会调用自定义列表。

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

private:
	int _a;
};

class B
{
public:
	B(int a, int ref)
		: _obj(a)
		, _ref(ref)
		, _n(10)
	{}

private:
	A _obj;//没有默认构造函数
	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();
}

来看上面这段代码会输出什么?
在这里插入图片描述
可以看出_a1 = 1,_a2 = 随机值

这是因为在声明的时候_a2在前面,_a1在后面,因此在初始化列表中,先把_a2初始化为_a1,此时_a1为随机值赋值给_a2;然后把_a1赋值为1。

explicit关键字

构造函数对于单个参数的构造函数,具有隐式类型转换的作用。

class Date
{
public:
	Date(int year)
		:_year(year)
	{}

private:
	int _year;
	int _month;
	int _day;
};
void TestDate()
{
	Date d1(2018);
	// 用一个整形变量给日期类型对象赋值
	// 实际编译器背后会用2019构造一个无名对象,最后用无名对象给d1对象进行赋值
	d1 = 2019;
}

从代码中的 d1 = 2019,对于在main函数中的" = "应该属于默认的赋值运算符重载,此时赋值的类型应当也是日期类,但是此时却赋值了一个2019,并且能够成功运行。

这是因为对于 d1 = 2019在语法意义上,是先构造,在拷贝构造。
在老的编译器上,这个表达式会先构造出一个临时对象A tmp(2019),然后再对tmp进行一个拷贝构造A d1(tmp),这样子这个表达式才能成功运行。
在新的编译器上,是直接调用构造函数,A d1(2019)。

但是这样子代码的可读性不够好,可以用explicit修饰构造函数,禁止对单参数的构造函数产生隐式类型转换。

	explicit Date(int year)
		:_year(year)
	{}

static成员

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

特性

(1) 静态成员为所有类对象所共享,不属于某个具体的实例。

class A
{
public:
	A()
	{
		++_count;
	}
private:
	static int _count;
	int a;
	int b;
};

int A::_count = 0;//静态变量初始化

int main()
{
	A a1;
	cout << sizeof(a1) << endl;
	return 0;
}

在这里插入图片描述

在A类中,我们定义了三个变量,一个是静态变量_count,另外还有两个int变量,那么通过计算A类的大小发现,A类的大小为8,也就是两个int的大小,并没有把_count计算在内。因此可以证明静态成员变量并不属于某一个具体的对象。

(2) 静态成员变量必须在类外初始化,定义时不添加static关键字。
在这里插入图片描述
如果初始化的时候不加变量的类型则会报错,因此在对static成员变量初始化的时候,必须加上成员类型,不需要加static,因为在声明的时候已经告诉编译器这是一个静态变量。
在这里插入图片描述
在类中的是static变量的声明,而初始化则是在类外。如果static变量是公有的,那么在任何时候都可以访问;如果static是私有的,那么只会在初始化的时候访问一次,其他时候不能访问。
(3) 访问静态成员变量有两种方式:
类名::静态成员
对象.静态成员
(4) 静态成员函数没有隐藏的this指针,不能访问任何非静态成员。因为静态成员函数是存在静态区的,不属于任何对象,自然也不会有this指针。

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

	static int GetCount()
	{
		return _count;
	}
private:
	static int _count;
	int a;
	int b;
};

_count静态成员变量是私有的,那么想要访问就必须用一个公有的函数来进行访问。
在这里插入图片描述
如果在静态函数中想要访问非静态成员,编译器就会报错,静态函数中没有对象,而非静态成员的访问必须是有特定对象的。

static成员有两个重要的点:
(1) 静态成员不可以调用非静态成员函数。因为非静态成员函数是与特定对象相对的,静态成员函数没有this指针,是无法调用非静态成员函数,这属于权限的放大。
(2) 非静态成员可以调用静态成员函数。因为静态成员函数是属于所有对象的,那么任何一个对象都可以调用。这属于权限的缩小。

C++11的成员初始化

在C++11中,支持给非静态成员变量在声明时进行初始化赋值,但是要注意,这里并不是初始化,依旧是声明,相当于是在声明的时候给成员变量一个缺省值。

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

	int sum()
	{
		GetCount();
		return 0;
	}

	static int GetCount()
	{
		return _count;
	}
private:
	int a = 10;
	int b = 20;
	static int _count;

};

也就是说,如果成员变量在初始化列表中,就用初始化列表的值初始化,如果不在初始化列表中,就用这个缺省值来进行初始化。

注意:静态成员变量不支持初始化赋值
在这里插入图片描述

友元

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

友元函数

尝试在日期类中重载“<<”运算符

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;
};

然后在主函数中尝试正常使用“<<”运算符
在这里插入图片描述
发现编译器会报错,因为在重载的成员函数里面,this指针在第一个参数的位置,也就是左操作数,而cout是右操作数,只有将上面的式子中左右操作数颠倒一下才可以正常使用。在这里插入图片描述
但是这就不符合cout的使用习惯,也与C++的语法不符合,要想解决这个问题,就必须把这个成员函数写在全局,也就是类的外面。
在这里插入图片描述
写在外面是可以解决上述问题,但是又带来了新的问题,类的成员变量是私有的,在类外是不能访问的。那么这里就需要友元来解决了。

友元函数是定义在类外的全局函数,可以直接访问类的私有成员。友元函数需要在类的内部声明,声明时需要加friend关键字。

class Date
{
	//友元函数声明
	friend ostream& operator<<(Date d, ostream& _cout);
public:
	Date(int year, int month, int day)
		: _year(year)
		, _month(month)
		, _day(day)
	{}


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

ostream& operator<<(Date d, ostream& _cout)
{
	_cout << d._year << "-" << d._month << "-" << d._day << endl;
	return _cout;//有返回值是为了能够连续输出
}

注意:
(1) 友元函数能够访问类的私有成员和保护成员,但是它本身不属于类的成员函数。
(2) 友元函数不能用const修饰。
(3) 一个函数可以是多个类的友元函数。

友元类

友元类本质上和友元函数的思想是一样的,就是在另一个类中可以访问声明友元类的成员变量或函数。

在一个类中声明友元类和声明友元函数是一样的,在声明前面加上friend(如果声明的友元类定义在该类的下方,要加上前置声明)。

class Date;//前置声明

class Time
{
	friend class Date;//在时间类中将日期类声明为友元类
public:
	Time(int hour = 0, int minute = 0, int second = 0)
		: _hour(hour)
		, _minute(minute)
		, _second(second)
	{}
private:
	int _hour;
	int _minute;
	int _second;
};

如上代码,就可以在日期类中直接访问时间类的私有变量。
在这里插入图片描述
对于友元类,有这样几个特性:
1、友元类的所有成员函数可以是另一个类的友元函数,都可以访问另一个类的非公有成员。
2、友元关系是单向的,不具有交换性。就是说日期类是时间类的友元,日期类可以访问时间类的非公有成员;但是在时间类中并不能访问日期类的非公有成员。
3、友元关系不具有传递性。比如:B是A的友元,C是B的友元,并不能认为C是A的友元。

内部类

概念:如果一个类定义在另一个类的内部,定义在一个类的内部的类称为内部类,内部类外部的类称为外部类。

class A
{
public:
	class B
	{
	public:
		void func(const A& a)
		{
			cout << _k << endl;//内部类访问外部类的静态成员是可以的
			couot << a.b << endl;//访问A的非公有成员也是可以的
		}
	};
private:
	static int _k;
	int b;
};

内部类是外部类的友元类,可以通过外部类的对象参数访问外部类中的所有成员;但是要注意的是外部类并不是内部类的友元,内部类是一个独立的类,不属于外部类。也就是说,外部类不能访问内部类中的成员,外部类没有任何访问内部类的权限。

内部类特性:
1、内部类可以是public、private、protect成员
2、内部类可以直接访问外部类中的static成员、枚举成员,不需要加外部类的对象名。
3、sizeof(外部类) = 外部类,与内部类没有关系,上面讲了,内部类是独立的,不属于外部类。所以计算外部类的大小的时候与内部类无关。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值