一、再谈构造函数
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 初始化列表
📖定义:
初始化列表:以一个冒号开始,接着是以一个逗号分割的数据成员列表,每个成员变量后面跟一个放在括号中的初始值或表达式,初始化列表是构造函数的一部分。
class Date { public: Date(int year, int month, int day) : _year(year) , _month(month) , _day(day) {} private: int _year; int _month; int _day; };
小Tips: 每个成员变量在初始化列表中最多只能出现一次(初始化只能初始化一次)。出了下面提到的三个类型的成员变量外,其他的成员变量可以不出现在初始化列表中,此时编译器对内置类型(没有默认值的情况下)不做处理(一般是随机值),对自定义类型会调用它的默认构造,内置类型如果给了默认值,则编译器会使用这个默认值。
📖必须经过初始化列表
类中包含以下成员,必须放在初始化列表进行初始化:
- 引用成员变量
- const成员变量
- 自定义类型成员(且该类没有默认构造函数)
其中引用成员变量和const成员变量,都有一个共同的特征:必须在定义的时候初始化。初始化列表就是对象中成员变量定义的位置。
class A {//A类中没有默认构造函数 public: A(int a) :_a(a) {} private: int _a; }; class B { public: B(int a, int& ref) :_aobj(a) ,_ref(ref) ,_n(10) {} private: A _aobj; // 没有默认构造函数 int& _ref; // 引用 const int _n; // const };
小Tips:尽量使用初始化列表初始化,因为不管是否使用初始化列表,对于自定义类型的成员变量,一定会先使用初始化列表初始化。
📖初始化列表能代替函数体内赋值嘛?
class Stack { public: Stack(int capacity = 10) :_top(0) , _capacity(capacity) , _a((int*)malloc(_capacity*sizeof(int))) { //下面这些功能都是初始化列标无法完成的,因此需要用到构造函数的函数体 if (_a == nullptr)`在这里插入代码片` { perror("malloc fail"); exit(-1); } cout << 11111111111111111111 << endl; memset(_a, 0, _capacity * sizeof(int));//把空间中的数据全部设置为0 } private: int* _a; int _top; int _capacity; };
如上面的代码所示,初始化列表并不能完成所有工作,有时候对于成员变量不仅要完成初始化,还要对初始化的结果进行合理性检查等操作,因此初始化列表不能代替函数体内赋值。
📖初始化顺序
成员变量在类中的声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。
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(); }
上面代码中,因为A类中成员变量的声明顺序是_a2、_a1,所以在初始化列表中先去初始化_a2,但是_a2是用_a1来初始化的,_a1此时还没有被初始化,所以是随机值,接下来再去用a初始化_a1,所以最终打印出来的结果_a1是1,而_a2是随机值。
1.3 explicit关键字
构造函数不仅可以构造与初始化对象,对于单个参数或除第一个参数无缺省值其余均有默认值的构造函数,还有类型转换的作用。
class A { public: A(int x) :_a(x) { cout << "A(int x)" << endl; } A(const A& x) :_a(x._a) { cout << "A(const A& x)" << endl; } private: int _a; }; int main() { A a2 = 2; return 0; }
上面代码中,A类只有一个单参数的构造函数,因此该构造函数是支持隐式类型转换的,A a2 = 2;本质上就是隐式类型转换,把一个整型2,转换成自定义类型A。具体过程是:首先在隐式转换过程中会产生一个临时的中间变量,这里就是用2去调用构造函数,得到一个A类型的临时中间变量,然后再用这个A类型的中间变量去调用拷贝构造,最终完成a2的创建。一般比较新的编译器,对这种连续的调用构造、拷贝构造进行了优化,会用2去调用构造函数完成a2的创建。
打印的结果确实说明只调用了构造函数。此时可能会有朋友产生怀疑了,觉得A a2 = 2;本来就是直接用2去调用构造函数创建a2,压根不存在什么先创建临时的中间变量,别急,可以通过引用来验证。
这里我们把一个整型3,赋值给一个A类型的引用,起初我们没加const程序报错了,后面加上const程序没有报错。为什么?就是因为这里会首先用3去调用构造函数,创建一个A类型的临时中间变量,前面的文章说过,临时的中间变量具有常性,这里的a3就是这个临时中间变量的别名,所以要在a3的前面加上const进行修饰。
📖使用场景
//string是字符串类 string name1("张三"); //直接构造 string name2 = "张三"; //构造+拷贝构造,优化成构造
class list { public: void push_back(const string& str) {} }; int main() { list l1; string name2("李四"); l1.push_back(name2); l1.push_back("李四"); return 0; }
如上面的代码,我们在插入值的时候,因为push_back函数的参数是string类型的对象引用,意味着要插入一个string类型的对象,如果不支持隐式类型转化,在插入string对象的过程中,我们就要先创建一个string类型的对象,然后再去插入,支持隐式类型转换的话,我们就无需创建string类型的对象,而是直接把一个字符串插入,就像l1.push_back("李四");这样,先用"李四"创建一个临时的中间变量,临时中间变量具有常性。此时就体现出了,在不修改对象的情况下,给形参加上const的优越性。push_back函数的形参用引用,是为了避免调用拷贝构造,一旦碰到形参是引用的,就要仔细考虑要不要加const进行修饰,引用和权限问题永远是并存的。
📖explicit关键字
如果想要禁止上面提到的隐式类型转换,可以在构造函数的前面加上explicit关键字进行修饰。
class A { public: explicit A(int x) :_a(x) { cout << "A(int x)" << endl; } private: int _a; };
此时就不能把一个整型赋值给A类型的对象。智能指针就不希望发生这种隐式类型转换,具体的我们后面再说。
二、static成员
📖先看一个场景
有一个A类,现在要统计程序中正在使用的A类型的对象有多少个,即当前程序中,创建后没有被销毁的A对象的个数。
很多朋友第一时间想到的就是定义一个全局的整型变量并初始化为0,然后在构造函数中++,在析构函数中--,像下面这样:
int _scount = 0;//全局的变量用来统计个数 class A { public: A() { cout << "A()" << endl; ++_scount; } A(const A& t) { cout << "A(const A& t)" << endl; ++_scount; } ~A() { cout << "~A()" << endl; --_scount; } public: int _a = 10; }; A a1;//第一个,调用的普通构造 A Func(A aa)//形参是类对象,则需要调用拷贝构造//第四个 { cout << __LINE__ << ":" << _scount << endl; return aa; } int main() { cout << __LINE__ << ":" << _scount << endl; A a2;//第二个,调用的普通构造 static A a3;//第三个,调用的普通构造 Func(a3); //函数传值返回会创建一个临时的中间变量,也是调用拷贝构造,用现有的aa对象去创建一个新的对象,所以是拷贝构造//第五个 cout << __LINE__ << ":" << _scount << endl; return 0; }
注意:全局对象先于局部对象进行构造,局部对象按照出现的顺序进行构造,无论是否为static,析构的顺序是按照析构造的相反顺序析构,只需注意static会延长对象的生命周期,所以会放在局部对象之后进行析构。
上面这种方法可以帮我们统计出当前程序中“存活”的A类对象,但是也有一个缺陷,这里的计数器变量_scount 是一个全局的,意味着我们可以在程序中的任何地方对它进行修改,这样就会导致我们统计出来的数量不准确。
为了解决上面的问题,我们可以考虑用C++的封装性,即把这个计数器变量_scount 变成A类的静态成员变量。
class A { public: A() { cout << "A()" << endl; ++_scount; } A(const A& t) { cout << "A(const A& t)" << endl; ++_scount; } ~A() { cout << "~A()" << endl; --_scount; } public: int _a = 10; static int _scount; };