构造函数?
初始化列表
前面已然介绍过构造函数,但并未完全结束,构造函数有很多种写法,有带缺省参数的,有全缺省的,不带缺省参数的…但用前面的方法,都是对里面成员变量的一种赋值,也就是说,这是给类中每一个成员变量一个初始值
但这并不是初始化,这里要分清楚什么是初始化,什么是赋初值
简单来说,它们的一个区别就是初始化只能初始化一次,但是赋值可以多次赋值
因此构造函数中就引入了初始化列表的概念,初始化列表可以做到给类内的成员函数初始化,下面写一个具体的例子来表示
#include <iostream> using namespace std; class Date { public: Date(int year = 1900, int month = 1, int day = 1) : _year(year) , _month(month) , _day(day){} void Print() { cout << _year << "/" << _month << "/" << _day << endl; } private: int _year; int _month; int _day; };
上面就是对初始化列表的概述,通过这样的写法可以对参数进行初始化
这里就体现了初始化和赋值的区别:
- 如果采用的是初始化,那么每个成员变量在初始化列表中只能初始化一次
- 类内如果有引用成员函数,const成员变量和自定义成员且没有默认构造函数时,就要写在初始化列表的位置
- 尽量使用初始化列表进行初始化,不管是否用初始化列表,对于自定义类型的成员变量都会先使用初始化列表进行初始化
- 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
下面来对第二条进行解读,同时也能更好的理解上面的原理
- 有const成员变量
#include <iostream> using namespace std; class Date { public: //Date(int year = 1900, int month = 1, int day = 1) // : _year(year) // , _month(month) // , _day(day){} Date(int year = 1900, int month = 1, int day = 1, int tmp = 1) { _year = year; _month = month; _day = day; _tmp = tmp; } void Print() { cout << _year << "/" << _month << "/" << _day << endl; } private: int _year; int _month; int _day; const int _tmp; };
这里的构造函数对吗?很显然是不能编译通过的,原因也很简单,这里的_tmp是const修饰的变量,怎么能给它赋值?解决方法有两个,第一个是可以给它在声明的时候就给它一个值,这是可以的,但使用初始化列表可以完美解决这个问题
#include <iostream> using namespace std; class Date { public: Date(int year = 1900, int month = 1, int day = 1, int tmp = 1) : _year(year) , _month(month) , _day(day) , _tmp(tmp) {} //Date(int year = 1900, int month = 1, int day = 1, int tmp = 1) //{ // _year = year; // _month = month; // _day = day; // //_tmp = tmp; //} void Print() { cout << _year << "/" << _month << "/" << _day << endl; cout << "const tmp " << _tmp << endl;; } private: int _year; int _month; int _day; const int _tmp = 1; };
这里也能更好理解赋值和初始化的区别,从const上就可以很好的体现出来
- 引用
还有一大使用场景就是在引用中可以体现,这里就不再写错误的示范了
class Date { public: Date(int year = 1900, int month = 1, int day = 1, int tmp = 1) : _year(year) , _month(month) , _day(day) , _tmp(tmp) {} //Date(int year = 1900, int month = 1, int day = 1, int tmp = 1) //{ // _year = year; // _month = month; // _day = day; // //_tmp = tmp; //} void Print() { cout << _year << "/" << _month << "/" << _day << endl; cout << "const tmp " << _tmp << endl;; } private: int _year; int _month; int _day; int& _tmp; };
explicit关键字
C++提供了关键字explicit,可以阻止不应该允许的经过转换构造函数进行的隐式转换的发生,声明为explicit的构造函数不能在隐式转换中使用
在了解explicit前,就必须先知道隐式转换是什么了
显式转换一定不陌生,就是强行转换,因此隐式转换就是编译器自动发生转换
比如说下面这样的例子:
int main() { int i = 0; double d = 1.2; int& p = d; return 0; }
这里编译是通不过的,原因是因为d是double类型因此编译不通过吗?其实不然
在编译器的处理中,会把d先生成一个临时变量,再进行赋值,而我们都知道临时变量是具有常性的,因此这里并不能把一个常属性的数交给引用来处理
因此这里只需要加上const,进行权限的缩小,就可以用常引用接纳常性变量了
const int& p = d;
其实,上述过程中把变量d从double类型转换到int类型就是一个隐式类型转换,我们并没有进行转换,但是编译器依旧自己转换了,并且生成了临时变量导赋值失败
而explicit的存在就是不能让隐式转换存在,因此我们不能把类型局限在int和double类型,要加入类的类型
class A { public: A(int a = 10) :_a(a) {} private: int _a; }; int main() { A a; a = 2; return 0; }
看看上面的代码发生了什么?把2赋给了一个类?如果你知道了隐式类型转换,那么就不难理解这段代码,这就是把2进行隐式类型转换,把它转换成了类A,里面成员变量_a的值是2,然后又进行了一次赋值
那么explicit就可以登场了,在初始化列表前面加上explicit:
class A { public: explicit A(int a = 10) :_a(a) {} private: int _a; }; int main() { A a; a = 2; return 0; }
此时,隐式转换就不存在了,因此这里的赋值就不复存在了,因为2在这里真的就是一个数字2
匿名对象
C++中引入了匿名对象的概念,简单来说就是没有名字的对象
class A { public: explicit A(int a = 10) :_a(a) {} private: int _a; }; int main() { A(2); return 0; }
这里看起来很奇怪,但这是可以通过的,C++内部是允许这样做的,它和有名对象的区别在于:
匿名对象的生命周期只在这一行,结束后就进行析构,而正常生成的对象需要在main函数结束后才会进行析构,普通生成的对象的生命周期是它的局部域
那匿名对象有什么用?
假设有这样的情景
class A { public: explicit A(int a = 10) :_a(a) {} void Print() { cout << "Print" << endl; } private: int _a; }; int main() { A a1; a1.Print(); A().Print(); // 匿名对象 return 0; }
假如这里我只需要调用类内的成员函数print,但如果正常来说我是需要创建一个对象,再通过对象去引用这个类内的成员函数,这是十分繁琐的,如果使用匿名对象,我不关注这个对象是谁,这个对象是多少,我只关心能不能引用类内的成员函数,因此就可以这样使用,相比起使用定义对象的方法来看,这样的方法的生命周期更短