当我们了解了什么是类,以及类的成员函数,接下来就是类和对象的收尾工作,这篇博客会解释上篇类的成员函数中提到的初始化列表和友元函数。
说初始化列表前,大家有没有想过一个问题,对象的定义是在我们写的代码语句,比如A a,那成员变量的定义在什么地方。
class B
{
void Print()
{
cout<<"Print"<
其实成员定义的地方就在初始化列表,而为什么要弄一个初始化列表来定义这些变量呢?这个问题可能要到后面才能体会到。实际上构造函数由两部分组成,函数体和初始化列表,函数体内是成员变量赋值的地方,而初始化列表是成员变量定义的地方,定义时可以赋初始值。如下,我们来看看初始化列表的语法,第一行写:分号开头,后面行用逗号开头,然后写变量名,括号内写上初值,如果是自定义类型,例如d1是Date类型,d1()这就不是简单的赋初值了,而是一次显示的调用构造函数,()括号内是给对应构造函数的传参。
那为什么要专门划分一个地方弄一个定义变量的地方呢?
因为有一些变量必须在定义的时候初始化,例如const成员变量,引用成员变量,没有默认构造函数自定义成员变量,const成员变量和引用成员变量都只能在定义的时候初始化,初始化后就不能再更改,所以我们使用者必须可以决定用什么数据初始化它们,不然我们根本无法使用这些变量,而没有默认构造函数的自定义成员变量也是如此,传什么参数给构造函数必须得让使用者决定。构造函数不就是初始化成员的吗,所以给构造函数增加了一个专门定义成员变量的地方-初始化列表。
Date(int year, int month, int day)
:_day(day)
,_month(month)
,_year(year)
{
_day = 2;
}
注意的是,即便我们像下面一样没写初始化列表,也还会走一趟初始化列表,调试的时候你会发现执行流会在进入构造函数体内前跳到成员变量声明的地方,如果写了就会按照变量声明顺序执行初始化列表(大家这里可以去试试,有一些大厂面试题就考过这个顺序的知识点)。
Date(int year, int month, int day)
{
_day = 2;
}
什么是友元函数呢?我们先前知道成员变量有访问限定符的限制,例如protect和private都无法在类外访问,可以我们有时候就是要访问这个私有变量,怎么办呢?方法1 用公有成员函数替你把变量访问到,但这已经破坏了封装 ,方法2 改为友元函数
class Date
{
friend void Print1(Date& d1);
private:
int _a;
};
void Print1(Date& d1)
{
cout << d1._a << endl;
}
void Print2(Date& d1)
{
cout << d1._a << endl;
}
Print1可以访问,而Print2却不可以访问,原因就是我们对Print1进行了友元声明。
我在实际使用时遇到个和友元函数相关的场景,因为友元函数是类内声明,类外定义的,然后如果我把整个类放在一个命名空间中,然后友元函数的定义我又无意间放在了命名空间外,且未说明是my_Date命名空间域内的。如下:
namespace my_Date
{
class Date
{
public:
friend void Print1();
public:
int _a = 1;
int _b=2;
};
}
void Print1()
{
cout << 1 << endl;
}
这个时候友元函数声明处,就会说找不到该函数定义,我想是因为它在命名空间内没找到,外面虽然有个像的,但是又没有指明说是属于my_Date域内的,友元函数声明也不敢将这个定义视为是自己的,只好来个像是警告一样提醒找不到该函数定义。最好的解决办法就是把定义放在命名空间内,或者定义处指定命名空间域。
1 友元类
class Date
{
friend class Time;
public:
private:
int _a=1;
};
class Time
{
public:
void Print1(Date& d1)
{
cout << d1._a << endl;
}
private:
int _a;
};
void test1()
{
Time t1;
Date d1;
t1.Print1(d1);
}
在Date内声明对Time类进行友元声明,这样Time类就是Date类的友元类,Time可以在自己成员函数内访问通过Date类对象访问Date类的私有成员,但这种关系是单向的。
2 内部类
至于什么是内部类呢,也就是在Date类内再定义一个类,例如叫Time类,内部类有个特性是,Time类天生是Date类的友元类。也就是说Time类也可以在自己的成员函数内通过Date类对象访问Date类的私有成员。
当然,我理解的类外访问基本上就是通过对象去访问成员了。
class Date
{
public:
class Time
{
public:
void Print1(Date& d1)
{
cout << d1._a << endl;
}
private:
int _a;
};
public:
private:
int _a=1;
};
此外内部类既然是定义在类内的,也就天然受到Date类的访问限定符的限制,如果被private修饰,那都不能在类外访问该类。
我们在接触类和对象前就已经接触过静态变量,这个静态变量也就是把自己放在了静态区,使得生命周期变长,而类内的静态成员分为静态成员函数和静态成员变量。静态成员函数和静态成员变量都有一个特性就是,可以不用实例化对象来访问,这就有意思了,不用对象来访问是说我类声明写好,变量就实例化好了?确实如此。
1 静态成员变量
先来看看如何使用静态成员变量的初始化
class Date
{
private:
int _a;
static int _b;
};
int Date::_b = 2;
不是放在初始化列表,而是要在类外定义给初值。有个说法是说静态成员不是对象的成员,不应该放在初始化列表中初始化,所以放在类外定义,可能大佬就是这么规定的。
原因:
2 静态成员函数
静态成员被认为是属于整个类的,不需要实例化对象就能访问的。
值得注意的是静态成员函数并没有this指针,而要访问非静态成员变量都是要用this指针访问的,所以不能访问非静态成员变量,而静态成员函数在类内访问静态成员变量是可以的,因为我们是在类内访问的,不受访问限定符和类域限制就可以访问了。从而推出,静态成员函数不能调用非静态成员函数。
老实说真要我把静态成员的必要性说的明明白白,我也做不到,我才刚开始写代码,并没有接触太多用到静态成员的场景。
静止单参数的构造函数进行隐式类型转换。
有一个特殊的场景,如下代码。
class Date
{
public:
Date(int a=2)
:_a(a)
{
cout << "Date._a:" <
要弄清是拿2直接构造d2还是构造+拷贝构造的形式我们还要在测试一下。
这里显示d1,d2各自调用一次构造函数,并没有调用拷贝构造函数,这说明编译器嫌拿2构造临时对象(这一步是隐式类型转换,也只有单个参数的构造函数,如Date类,才支持将一个其它类型,例如int类型的2,调用构造函数去构造成Date类型的对象),然后再拷贝构造给d2是非常低效的,就主动优化了。但是有时候隐式类型转换是有危害的,explicit关键字就是禁止单参构造函数进行隐式类型转换,会导致Date d2=3这句写法会报错。首先这句话就是一个标准的隐式类型转换,类似int a=12.4; 可是Date d2=3不是会优化吗,将构造+拷贝构造优化成直接构造,直接构造就没有隐式类型转换了,但是编译器还是要检查Date d2=3这句标准的隐式类型转换是否符合语法,符合语法才会去进行优化。总不能上来就构造d2吧,不得检查检查语法吗,加了explicit关键字就说明不可以进行隐式类型转换,那Date d2=3这句标准的隐式类型转换就会报错,就到不了后面的优化操作了。
至于为什么不禁止多参数的构造进行隐式类型转换,个人觉得是多参数的构造函数就不支持隐式类型转换。c++11貌似支持,而且加了explicit也会禁止多参数的构造函数进行隐式类型转换。