一、再谈构造函数
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++11允许我们给成员变量一个缺省值,但是C++11以前是怎么做的呢?
这里引入一个新的概念:初始化列表
初始化列表定义在构造函数的函数名和函数体之间,以一个冒号开始,接着是一个用逗号分隔的数据成员列表,每个成员变量后面跟着一个放在括号中的初始值或表达式,例如:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
像这样,才能称之为真正的初始化
但是,如果一个成员变量既给了缺省值,又在初始化列表中显式定义了,那它最后的值如何呢?
通过监视窗口我们可以观察到:
如果一个成员变量既有缺省值又在初始化列表中定义了,那么就按照初始化列表中的值进行初始化
如果一个成员变量有缺省值,但是没在初始化列表中定义,那么就用它的缺省值初始化
如果一个成员变量既没有缺省值又没在初始化列表中定义,那么就给一个随机值
初始化列表有以下几个特点:
(1)每个成员变量在初始化列表中只能出现一次,因为只能初始化一次
(2)类中的以下几个成员变量,必须放在初始化列表中进行初始化
- 引用类型的成员变量
- const成员变量
- 没有默认构造函数的自定义类型成员变量
原因如下:
对于引用类型的成员变量,我们必须在声明变量时就给出它的引用对象
对于const成员变量前面已经提到过了,也是必须在声明时就初始化
对于没有默认构造函数的自定义类型成员变量,我们在初始化时需要传参
(3)尽量多使用初始化列表去初始化,因为不管你是否使用,对于自定义类型成员变量都一定会先使用初始化列表去初始化
例如:
可以看到,Date类里有一个Time类的成员函数,虽然我们没有在初始化列表中初始化它,它也会使用初始化列表去调用自己的默认构造函数。
如果该自定义类型成员变量的构造函数不是无参的或者全缺省的,我们就需要手动将该变量添加至初始化列表中并给出参数。
总之,我们平时写构造函数的时候尽量用初始化列表来初始化成员变量即可
(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();
}
最后打印的结果是什么呢?
可能很多人认为是 1 1,实际上是:
因为_a2比_a1先声明,所以在初始化列表中也是_a2先初始化
最后,拷贝构造函数因为也是构造函数,所以它也有初始化列表
1.3 explicit关键字
我们知道,内置类型变量在发生类型转换的时候会生成一个临时的常性变量,例如:
int a = 1;
double b = a;
但是,类型转换不止能发生在内置类型中,内置类型也可以转换成自定义类型,这里就和构造函数扯上关系了。
一个类的构造函数,不仅起到初始化成员变量的作用,对于单个参数或第一个参数无缺省值的半缺省构造函数来说,它还具有类型转换的作用。
或许没看懂,那就举一个例子
像这样,对象aa1在创建时正常调用构造函数,aa2又是怎么回事?为什么一个自定义类型能被内置类型初始化?
前面说到,内置类型在发生类型转换的时候会生成一个临时的常性变量,这里也是一样
首先编译器使用1作为参数调用构造函数,创建一个临时变量,再用这个临时变量调用拷贝构造函数对aa2赋值
所以aa1只调用了一次构造函数,而aa2这一行代码调用了一次构造函数和一次拷贝构造函数
这种方式既影响代码可读性,又增加了消耗,有什么办法可以禁止构造函数类型转换呢?
这里引入explicit关键字,在构造函数的前面加上它,即可禁止类型转换了
这是单参数的构造函数,对于第一个参数无缺省值的半缺省构造函数也是同理
只要是只传递一个参数的构造函数,用这种方式都会发生类型转换
另外,对于需要传递多个参数的构造函数,在C++11后也开始支持类型转换了,例如:
如果不想类型转换,用explicit修饰构造函数即可
二、static成员
2.1 概念
static修饰的类成员称为类的静态成员,static修饰的成员变量称为静态成员变量,static修饰的成员函数称为静态成员函数。
有一道面试题:实现一个类,计算程序中创建过多少个类对象
可以看到,使用静态成员变量和静态成员函数可以很轻松的解决这个问题
2.2 特性
根据上面的图,我们可以得出以下几点
(1)静态成员不属于某个对象,而是属于所有对象、属于整个类,存放在静态区
所以我们上面可以直接使用类名和作用域限定符来访问静态成员函数GetCount,当然也可以创建一个对象来访问静态成员函数,但是有点多此一举了
(2)静态成员变量必须在类外进行定义和初始化,不需要添加static,在类中声明时才需要加
(3)静态成员函数没有隐式的this指针形参,所以不能访问任何非静态成员
(4)静态成员也是类的成员,受public、protected和private访问限定符的限制
像上面,因为_count是私有的,我们只能用函数来获取它,如果它是公有的,我们也可以直接访问
三、友元
当我们在类外定义了一个函数,想要访问类中的私有成员变量怎么办呢?这里就涉及到友元
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
友元又分为:友元函数和友元类
3.1 友元函数
我们知道,cout和流插入运算符可以实现将内置类型打印的效果,那么假设我想将流插入运算符重载,让自定义类型也可以使用呢?
我们试试在类中实现流插入运算符的重载函数
可以看到,实现的重载函数没有起效
这是因为,成员函数的第一个变量是this指针,在重载函数中对应第一个变量的是第一个操作数
所以如果像这样就可以正常运行了
但是这样也太怪了吧,和平时用cout一点也不一样,有没有别的办法呢?
我们只好在类外去实现流插入运算符的重载函数了,但是类外的函数又没办法访问类内的私有成员
像这种必须定义在类外,但是又需要访问类内的私有成员的函数,就需要友元来解决了
友元函数可以直接访问类内的私有成员,需要在类的内部进行声明,声明时需要加friend关键字
需要说明以下几点:
- 虽然友元函数可以访问类的私有和保护成员,但不是类的成员函数
- 友元函数不能用const修饰
- 友元函数可以在类的任何地方声明,不受类访问限定符限制
- 一个函数可以同时是多个类的友元函数
- 友元函数的调用和普通函数一样
3.2 友元类
和友元函数类似,我们也可以在类中声明一个友元类
友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的非公有成员
需要注意以下几点:
- 友元关系是单向的,不具有交换性
比如上面,Date类是Time类的友元,所以可以直接在Date类中访问Time类的私有成员变量;
但是不代表Time类是Date类的友元,不能在Time类中访问Date类的私有成员变量
- 友元关系不能传递
例如A是B的友元,B是C的友元,不代表A就是C的友元了
- 友元关系不能继承
这里在讲到继承后再给大家详细介绍
四、内部类
4.1 概念
如果一个类定义在另一个类的内部,这个定义在内部的类就称为内部类。
内部类是一个独立的类,它不属于外部类,我们更不能通过外部类的对象去访问内部类的成员
外部类对内部类没有任何优越的访问权限。
内部类与外部类的关联在于:
(1)内部类受外部类的类域限制
例如我们想创建一个内部类类型的变量,需要用作用域限定符
(2)内部类天生就是外部类的友元,但是外部类不是内部类的友元
4.2 特性
(1)内部类定义在外部类的public、protected和private中都是可以的
(2)内部类可以直接访问外部类中的静态成员,而不需要借助外部类的对象或类名
(3)外部类的大小不包括内部类
例如
可以看到,外部类A的大小并没有包括内部类B,所以可以知道内部类的空间也是独立的
五、匿名对象
有时候我们可能只需要调用一次某个类的成员函数,为此如果特意去创建一个对象的话就太麻烦了
这里就可以用到匿名对象。我们平时创建一个对象可能是这样的:
如果要创建一个匿名对象的话,是这样的:
顾名思义,匿名对象在创建的时候是不用取名字的。
匿名对象的特点在于,它的生命周期只在这一行,一旦程序走到了下一行,就会自动调用析构函数销毁。
假设此时我们要调用一次A类中的Print函数,就可以用匿名对象去调用,而不用特意创建一个对象
对于各种一次性的对象创建,我们都可以使用匿名对象。
完.