一、构造函数
在类与对象(二)中,我们详细讲了6个默认成员函数,其中对于构造函数也花了大篇幅进行讲解,但是还有一些内容没有提及到,下面将会对构造函数的函数体赋值、初始化列表、explicit关键字进行讲解。
1、函数体赋值
构造函数体内的赋值,只能称之为赋初值,能够在函数体内对一个成员变量进行多次赋值,而初始化过程只能初始化一次,在构造函数的函数体中赋值,并不是成员变量的初始化过程。
函数体内对成员变量单次赋值:
函数体内对同一成员变量多次赋值:
我们只需要记住:
构造函数的函数体内的赋值,并不是成员变量的初始化,在函数体内的赋值可以对同一成员变量进行多次赋值。
2、初始化列表
在讲初始化之前,我们需要明白成员变量在何时被定义/初始化?
不是在类中的声明处:
也不是构造函数的函数体内的赋值处:
前面提到过,类中成员变量的初始化是在类对象的实例化时完成的,说的直白一点,就是当我们创建一个类对象时,编译器就完成了成员变量的初始化。
那么深入一点,这个初始化到底在什么地方进行的呢?
答案就是在初始化列表内进行。
概念
在构造函数函数体之前,以一个冒号 : 开始,接着是一个以逗号 , 分隔的数据成员列表,每个成员变量后面跟一个放在括号内的初始化的值或表达式。
class Date
{
public:
Date(int year, int month, int day)
//初始化列表
:_year(year)
, _month(month)
, _day(day)
{
//赋值修改---赋初值---可以多次赋值
}
private:
int _year;
int _month;
int _day;
};
注意事项
①我们在初始化列表对成员变量进行初始化,因此每个成员变量最多在初始化列表中出现一次。
在初始化列表中对同一成员变量多次初始化则会报错:
②成员变量放入初始化列表的显式实现可以省略,若省略,编译器会默认将所省略的成员变量拉入初始化列表,按照成员变量的声明顺序依次进行初始化,省略的部分成员变量为随机值,显式初始化的成员变量就初始化为我们设置的值/表达式的值。
关于部分置入初始化列表的成员变量的初始化顺序的解读:
初始化顺序取决于成员变量声明的次序,不是初始化列表出现的顺序!
声明的顺序与初始化列表中的顺序最好保持一致。
③在类中成员变量的声明时可以赋予的缺省值,实际上就是作用在初始化列表处。若显式实现了初始化列表,且初始值不同于缺省值。那么初始化列表中的初始值优先级高于缺省值。
但是需要注意:如果赋予的缺省值与自己在初始化列表设置的初始化值不相同,那么初始化列表的设置值优先,这一点与前面所讲的声明处缺省值设置与函数体内的赋值修改的顺序是一样的。
我们也可以证明上面的观点:
通俗一点,缺省值起一个辅助的作用,而设置的初始值权限更高。有初始值则选初始值,无初始值默认缺省值,二者都没有,并且函数体内没有进行赋值修改,那么就会初始化随机值。
④当类中成员变量包含const修饰的成员变量、自定义类型成员变量(该类型没有默认构造函数时)、引用成员变量时,必须在初始化列表位置对它们进行初始化,不能在函数体内初始化。其他的成员变量随意在初始化列表/函数体内初始化。
不在初始化列表中进行上述变量的初始化:
有自定义类型成员变量,但是该成员变量的自定义类型没有默认构造函数,如上图的A类型,那么在赋值修改中对_aa进行初始化时,就会因为调用不到A类型的默认构造函数而报错。
在初始化列表中对const修饰、自定义类型(无默认构造函数)、引用的成员变量进行初始化:
⑤尽量使用初始化列表进行成员变量的初始化,因为不管你是否使用初始化列表,对于自定义类型的成员变量,一定会先使用初始化列表初始化。编译器在执行构造函数时,会依照初始化列表-->赋值修改的顺序进行执行。
我们可以既写初始化列表又写赋值修改,依照编译器对构造函数的执行顺序,最后得到的对象的成员变量值,是赋值修改后的值:
初始化列表未写出的成员变量为随机值--->写出的成员变量按声明次序初始化为列表中设置的初始值--->执行函数体内的赋值修改,将指定的成员变量赋予指定值。
⑥对于初始化列表的设置初始值,可以是一个值,也可以是表达式。如下图:
⑦初始化列表中的定义/初始化,变量与括号中的初始值必须类型匹配。
那么问题来了,前面自定义类型成员变量在初始化列表中初始化,我们写的是:
_aa(1)
但是1是整型,_aa初始化应该是自定义类型,为什么能成功执行程序?
这里就涉及到临时变量的创建:本质是类型的转换,整型1通过构造转换为自定义类型A的临时变量,再通过拷贝构造将这个临时变量拷贝给_aa。
单参数的构造函数隐式类型转换
如:A aa = 2; ---> 2构造一个临时对象,再通过临时对象拷贝构造给aa。
这个是非常好用且便捷的,举例如下:
同时A aa(1); <==> A aa = 1;两种方式也都是可行的。
我们手动写一个拷贝构造:
并没有调用A的拷贝构造,为什么?
编译器会优化同一个表达式的连续构造的过程,合N步为1。
多参数的构造函数隐式类型转换
C++11支持下图写法:
缺省值的类型总结
3、explicit关键字
作用于构造函数,使构造函数失去隐式类型转换的能力。
直接加在构造函数名之前即可。
二、static成员
概念
static静态成员,在类中被static修饰的成员变量,是属于整个类所有的,不属于某一个对象,而是属于所有对象。静态成员变量在类中声明后,无法赋予缺省值,需要在类外进行初始化。
用static修饰的成员函数,为静态成员函数,静态成员函数只有一个作用,就是访问静态成员变量,静态成员函数无法访问非静态成员变量。
特性
①静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区;
②静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明;
③类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问;
④静态成员函数没有隐藏的this指针,不能访问任何非静态成员;
⑤静态成员也是类的成员,受public、protected、private 访问限定符的限制。
全局变量记录创建某个类对象个数:(风险、不使用)
//记录创建了几个A类类型对象
int n = 0;
class A
{
public:
A()
{
n++;
}
A(const A& a)
{
n++;
}
private:
int _a;
};
A Func()
{
A aa;
return aa;
}
int main()
{
A aa1;
A aa2;
Func();
cout << n << endl;
return 0;
}
//但是在全局中创建的n来记录类对象个数,别人可以轻松修改
//因此就封装到类中
对于全局变量n而言,我们虽然能够得到对象个数,但是n能够被别人轻易修改,具有风险,因此我们选择使用静态成员变量来记录。
静态成员变量记录创建的类对象个数
//静态成员变量,在类外定义
int A::n = 0;
class A
{
public:
A()
{
n++;
}
A(const A& a)
{
n++;
}
static int GetN()//获取n的值
{
return n;
}
private:
static int n;//静态成员变量n,无法赋予缺省值
};
那我们如何输出n的值呢?
n属于整个类,也属于每一个类中对象,因此有两种方式:
类::n或者对象.n来访问,但是n又是私有成员变量,不能在类外访问,因此需要在类中写一个静态成员函数GetN来获取n的值:
全部代码如下:
class A
{
public:
A()
{
n++;
}
A(const A& a)
{
n++;
}
static int GetN()
{
return n;
}
private:
static int n;//静态成员变量n,无法赋予缺省值
};
//静态成员变量,在类外定义
int A::n = 0;
A Func()
{
A aa;
return aa;
}
int main()
{
A aa1;
A aa2;
Func();
cout << A::GetN() << endl;
cout << aa1.GetN() << endl;
cout << aa2.GetN() << endl;
return 0;
}
三、友元
友元提供了一种突破封装的方式,有时提供了便利,能够让类外的函数/一个类调用另一个类的成员,但是增加了耦合度,破坏了封装,慎用。
1、友元函数
典型的如之前学到的日期类中的输出输入operator<</operator>>的实现。如果放在类中实现,由于隐含的this指针在第一个参数位置的存在,使得cout位于<<右侧,位置会显得奇怪,因此我们一般将函数放在类外,类中使用友元对该函数进行声明,这样函数就能够调用类中成员。
友元函数能够直接访问类的私有成员,是定义在类外的一个普通函数,不属于任何类,但是需要在类的内部进行友元声明,声明方式为friend + 函数声明。
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;
}
特点
①友元函数可以访问类的私有、保护成员,但是不属于类的成员函数。
②友元函数不能用const修饰。
③友元函数的声明可以在类中任意位置,不受类访问限定符的限制。
④一个函数可以是多个类的友元函数。
⑤友元函数的调用与普通函数调用原理相同。
2、友元类
友元类的所有成员函数都是另一个类的友元函数,能够访问另一个类的成员。
特点
①友元类不具有交换性:Time类是Date类的友元类,那么Time能够访问Date类的成员,但是Date类不能够访问Time类的成员,除非Date类是Time类的友元类。
②友元类不具有传递性:A类是B类的友元类,B类是C类的友元类,不能得到A类是C类的友元类的结果!
③友元类不具有继承性。
class Time
{
friend class Date; // 声明Date类为Time类的友元类,则在日期类中就直接访问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;
};
友元类就类似于日常的交友,A是B的朋友,那么A拥有B的访问权,但是这并不代表着B能够拥有A的访问权。
双向友元大概率才能称得上是真正的朋友。
四、内部类
一个类定义在另一个类的内部,那么这个类被称为内部类。
内部类是一个独立的类,虽然定义在外部类的内部,但是并不属于外部类。即不能通过外部类的对象访问内部类的成员。外部类不是内部类的友元类/朋友。
但是内部类是外部类的友元类,内部类的实例化对象能够访问外部类的成员。
特点
①内部类的定义不受外部类的类访问限定符public、private、protected的限制。
②内部类可以直接访问外部类的static成员,不需要外部类的对象/类名。
③sizeof(外部类) == 外部类(不需要管内部类) 。外部类中定义内部类,但是并没有内部类对象实例化,因此没有开辟内部类空间,因此对于外部类大小计算仍然只需要计算外部类成员。不需要管定义的内部类。
五、匿名对象
//匿名对象
class A
{
public:
A(int a = 0)
:_a1(a)
{
cout << "A(int a = 0)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
void Func()
{
cout << "Func()" << endl;
}
private:
int _a1;
};
int main()
{
//有名对象
A aa1;
A aa2(5);
//匿名对象
A();
A(9);
//匿名对象的应用:匿名调用函数会便捷一些
//有名调用函数
A aa3;
aa3.Func();
//匿名调用函数
A().Func();
return 0;
}
特点
匿名对象的生命周期只存在于定义匿名对象的那一行。
六、拷贝对象时的一些编译器优化
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;
}
对于A类以及两个返回值、传参与A类对象相关的函数f1、f2,对于对象实例化以及调用f1、f2函数时,编译器会对构造、拷贝构造的过程进行相应的优化: