1. 再谈构造函数
那上一篇文章呢,我们学了类的6个默认成员函数,其中我们第一个学的就是构造函数。
那我们先来回忆一下构造函数:
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。
也就是说,构造函数其实就是帮我们对类的成员变量赋一个初值。
举个栗子:
class Date { public: Date(int year, int month, int day) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; };
对于像这样的一个类来说:
虽然经过上述构造函数的调用之后,对象中的成员变量已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化,构造函数体中的语句只能将其称为赋值。
因为初始化只能初始化一次(即在定义的时候赋初值),而构造函数体内可以对成员变量进行多次赋值。
这里注意初始化(定义的时候赋初值)和赋值的区别。
那我们现在来看这样一个类:
class A { private: int _a1; int _a2; };
问大家一个问题:这里面的int _a1; int _a2;
是对成员变量_a1、 _a2
的声明还是定义?
这里是不是声明啊,只是声明一下A这个类里有这样两个成员变量。
那它们在哪定义呢?
🆗是不是在这个时候:
但是,这里不是对对象整体的定义嘛。
那对象的每个成员变量什么时候定义呢?
可是变量整体定义了的话,它的成员不都也定义了吗?
这些成员不都是属于这个对象的吗?
我们运行也没出什么问题。
道理好像是这样的,但是呢?看这种情况:
我们现在给这个类里面再增加一个const的成员变量。
那这时我们再去运行程序
哦豁,发生错误了,这么回事?
为什么会这样呢?
🆗,大家来想一下,const修饰的变量有什么特点:
const修饰的变量必须在定义的时候赋初值(初始化)
而我们现在有对_b进行初始化吗?
是不是没有啊,我们构造函数都没写,那编译器是会默认生成一个,但是,我们知道默认生成的根本就不会对内置类型进行处理。
那我们是不是自己写个构造函数就行了:
但是我们发现还不行,为什么呢?
因为const变量必须是在定义的时候赋初值,而我们上面说了构造函数里面只是对其赋值,并不是初始化。
那大家可能想到了:
之前文章里我们在讲解构造函数的时候说了,C++11不是允许内置类型成员变量在类中声明的时候可以给缺省值嘛。
我们来试一下:
🆗,这样确实不报错了。
但是,这是C++11之前才提出来的,那C++11之前呢?
如何解决这样的问题呢?
现在的问题是什么:
_b必须初始化,即在定义的时候赋初值,但是现在是不是没法搞啊,构造函数里只能对其赋值,并不是初始化。
那我们是不是要给成员变量也找一个定义的位置,不然像const这样的成员变量不好处理。
那成员变量的定义到底是在哪里呢?
我们可以认为,对象定义的时候,其成员变量也就定义了,但是一个对象可能有多个成员,在对象定义的地方也没法给某个成员初始化啊。
怎么办?
1.1 初始化列表
那面对上面的问题,我们的祖师爷就要去给成员变量找一个定义的地方,那最终找来找去呢,还是把目标锁定在了构造函数。
在构造函数里面呢又搞了一个东西叫做——初始化列表。
初始化列表:
以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
举个栗子:
对于上面类中const int _b的初始化我们就可以放在初始化列表进行处理:
class A { public: A(int a1, int a2, int b) :_a1(a1) ,_a2(a2) ,_b(b) { } private: int _a1; int _a2; const int _b; }; int main() { A a(1, 1, 1); return 0; }
这下我们再运行程序:
就可以了。
当然:
在构造函数体内我们还可以再为成员变量赋值
注意这里成员_b被const修饰,不能再被赋值了。
然后呢,对于初始化列表,还有一些需要我们注意的地方:
- 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
- 以下三种类成员变量,必须放在初始化列表位置进行初始化:
引用成员变量
const成员变量
没有默认构造函数的自定义类型成员
首先const成员变量:
我们上面举的例子就是const成员变量,它必须在定义的时候赋初值,所以必须在初始化列表对其进行初始化(定义的时候赋初值),当然C++11之后可以给缺省值,这样如果没有对它进行初始化编译器就会用缺省值去初始化。
然后还有引用成员变量:
这个我们在之前学习引用的时候就说了:
引用也必须在定义的时候初始化。
最后就是没有默认构造函数的自定义类型成员:
因为默认生成的构造函数对内置类型不做处理,对自定义类型会去调用它对应的默认构造函数(不需要传参的构造函数都是默认构造函数),所以如果自定义类型成员没有默认构造函数我们就需要自己去初始化它。
举个栗子:
class B { public: private: int _b; }; class A { private: int _a1; int _a2; B _bb; }; int main() { A a; return 0; }
大家看运行这个程序有问题吗?
没有问题,因为对于成员B _bb;
来说,会调用它对应的默认构造,类B我们虽然没写构造函数,但是有编译器默认生成的构造函数。
当然如果我们写了不用传参的构造函数,也可以。
但是如果这样:
此时类B是不是没有默认构造函数了。
那这时就不行了。
让_bb在初始化列表调用其构造函数进行初始化,这样就可以了。
尽量使用初始化列表初始化,因为不管你是否使用初始化列表,成员变量都会在初始化列表定义。当然我们说了C++11之后可以给缺省值,这样如果没有对它进行初始化编译器就会用缺省值去初始化。
成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
看这个程序:
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接收传过来的1,1先初始化_a1,然后_a1去初始化_a2,所以都是1,是吗?
结果是1和一个随机值。
为什么是这样?
原因就是成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
所以先初始化_a2,然后是_a1
所以是1和随机值。
1.2 explicit关键字
我们先来看这样一个类:
class A { public: A(int a) :_a1(a) {} private: int _a2; int _a1; };
那我们现在想用A这个类去创建对象:
int main() { A a1(1); return 0; }
这样肯定是可以的,去调它带参的构造函数。
那除此之外呢,其实还可以这样搞
欸,这种写法是怎么回事?
这个地方为什么
A a2 = 1;
这样也可以,1是一个整型,怎么可以直接去初始化一个类对象呢?🆗,那要告诉大家的是这里其实是一个隐式类型转换。
就跟我们之前提到的内置类型之间的隐式类型转换转化是一样的,会产生一个临时变量,我们再来回顾一下:
那这里
A a2 = 1
是如何转换的呢?这里呢也会产生一个临时变量,这个临时变量就是用1去构造出来的一个A类型的对象,然后再用这个临时对象去拷贝构造我们的a2。
那我们可以来证明一下,是不是如我所说的那样:
我们来再写一个拷贝构造:
注意拷贝构造也是有初始化列表的,因为拷贝构造函数是构造函数的一个重载形式。
那我们现在运行程序,看
A a2 = 1
是不是先用1调构造函数创建一个临时变量,然后再调拷贝构造构造a2。如果是那就跟我们上面说的一样了。
哦豁,构造确实调了,但是后面没去调拷贝构造啊。
是我们上面说的不对吗?
🆗,那其实呢,C++编译器针对自定义类型这种产生临时变量的情况,会进行优化
编译器看到你这里先拿1构造一个对象,然后再去调拷贝构造,有点太费事了,干脆优化成一步,直接拿1去构造我们要创建的对象。
当然,不一定所有的编译器都会优化,但是一般比较新一点的编译器在这里都会优化。
但是呢?口说无凭欸!
你凭什么说这里没有优化的话是会产生临时变量的,说不定人家本来就是直接去构造了呢?
那我们再来看这个代码:
A& c = 10;
这样可以吗?
🆗 ,不行直接报错了。
但是:
加个const就行了。
为什么呢?
🆗,这是不是我们之前在常引用那里讲过的啊:
这里产生了临时变量,而临时变量具有常性,所以我们加了const就行了。
欸,那不是说优化了嘛,但是这里是引用就没优化了,直接拿10去构造一个临时对象,然后c就是这个临时对象的引用,所以只有一步构造,就不用优化了。
所以这里确实是会产生临时变量的,上面那种情况确实是进行了优化。
还有一个点就是,一般来说,C++ 中的临时变量在表达式结束之后 (full expression) 就被会销毁,而这里引用去引用一个临时变量的话会延长它的声明周期的。
那上面说了这么一大堆,想告诉大家的是什么呢?
就是 构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,是支持隐式类型转换的(C++98就支持这种语法了)。
这里就可以这样:
那如果我们这里不想让它支持类型转换了,有没有什么办法呢?
🆗,这就要用到我们接下来要学的一个关键字——
explicit
我们只需在对应得构造函数前面加上
explicit
关键字:
然后:
这样写就不行了。
🆗,但是呢,我们刚才说的是对于单参数的构造函数是支持这种类型转换的,那多参数的构造函数呢?
首先我们肯定是可以这样用的:
A a(1, 1);
那这里能不能也像上面那样支持隐式类型转换呢,两个参数的构造函数,那这样去用?
那首先要告诉大家C++98是不支持多参数的构造函数进行隐式类型转换的。
不过呢,C++11对这块进行了扩展,使得多参数的构造函数也可以进行隐式类型转换,但是,要这样写:
用一个大括号括起来。
那同样的道理,如果我们不想让这里支持这种类型转换,对于多参数的构造函数,也是在前面加一个explicit
关键字:
用explicit修饰构造函数,将会禁止构造函数的隐式转换。
2. static 成员
我们来看这样一个面试题:
要求我们实现一个类,并在程序中能够计算出一共创建了多少个类对象。
那大家想一下,可以怎么做?
直接去数行不行啊?
直接数是不是有可能不靠谱啊,因为有些创造类对象的地方是不是会进行优化,这个我们上面刚刚讲过。
那还可以怎么搞呢?
大家想一下,要创建一个类对象,有哪些途径,是不是一定是通过构造函数或者拷贝构造搞出来的。
那我们是不是可以考虑利用构造函数和拷贝构造来计算创建出来的对象个数啊。
class A { public: A(int a) { } A(const A& aa) { } };
假设我们现在创建了这样几个对象:
void func(A a) {} int main() { A a1; A a2(a1); func(a1); A a3 = 1; return 0; }
那我们就可以怎么做:
🆗,定义一个全局变量n,初值为0。
然后每次调用构造函数或者拷贝构造创建对象时就让n++,那这样最后n的值是不是就是创建的对象的个数啊。
我们来测试一下:
结果是4,对不对啊。
我们分析一下其实就是4,答案没问题。
但是大家说当前这种方法好吗?
其实是不太好的,为什么?
首先这里我们用了一个全局变量,那首先第一个问题就是它可能会发生命名冲突;其次,全局的话,是不是在哪都能访问它(而C++讲究封装),都可以修改它啊,如果在其它地方不小心++了几次,结果是不是就不准了啊。
那有没有更好一点的方法呢?
那当然是有的。
应该怎么做呢?
🆗,我们把统计个数的这个变量放到类里面,这样它就属于这个类域了,就不会命名冲突了,然后如果不想让它在类外面被访问到,我们可以把它修饰成私有的就行了。
但是:
如果直接放到类里面,作为类的一个成员变量:
那它是不是就属于对象了,但我们要统计程序中创建对象的个数,这样我们每次创建一个对象n就会定义一次,是不是不行啊。
不能让它属于每个对象,是不是应该让它属于整个类啊。
2.1 静态成员变量
怎么做呢?
在它前面加一个static修饰,让它成为静态成员变量。
那这样它就不再属于某个具体对象了,而是存储在静态区,为所有类对象所共享。
但是我们发现加了static之后报错了,为什么?
因为静态成员变量是不能在这里声明的时候给缺省值的。
非静态成员变量才可以给缺省值。
大家可以想一下嘛,缺省值其实是在什么时候用的,在初始化列表用的,用来初始化对象的成员变量的,而静态成员变量我们说了,它是属于整个类的,被所有对象所共享。
类里面的是声明,那静态成员变量的初始化应该在哪?
🆗,规定静态成员变量的初始化(定义的时候赋初值)一定要在类外,定义时不添加static关键字,类中只是声明。
但是现在又有一个问题:
我们把它搞成私有的,在外面就不能访问了。
当然如果不加private修饰就可以了:
另外呢,这里除了指定类域来访问静态成员变量,还可以通过对象去访问:
因为它属于整个类,并且被所有对象共享。
还可以这样:
这个问题我们之前是不是说过啊,不能看到->
或者.
就认为一定存在解引用,还是要根据具体情况进行分析。
当然如果是私有的情况下,这样写是不是统统不行啊:
那我们就可以写一个Get方法:
成员函数是需要通过对象去调用的。
这样就可以了。
那如果我们的程序是这样的呢?
在main函数里面我们根本没有创建对象,那我们还怎么调用Getn函数呢?
难道我们专门在main函数里创建一个对象去调用Getn,然后再把结果减1:
因为main函数里的对象是我们为了调用函数而创建的对象,所以最后要减去。
2.1 静态成员函数
那有没有什么办法可以不通过对象就能调用到Getn函数呢?
那我们就可以把Getn函数搞成静态成员函数,也是在前面加一个static关键字就行了。
但是静态成员函数有一个特性:静态成员函数没有隐藏的this指针,不能访问任何非静态成员。
因为非静态成员是属于对象的,都是通过this指针去访问的,而静态成员函数是没有this指针的。
那它没有this指针,就可以不通过对象调用了,所以现在我们通过指定类域也可以调用到静态的Getn函数。
当然你还通过对象调用也还是可以的。
那我们现在在加一个东西
大家觉得现在结果会多几个对象:
🆗,13个,是不是比之前多了10个啊,因为我们又多定义了一个大小为10 的类对象数组。
2.3 练习
那接下来我们来做个题: link
这道题呢就是让我们求一个1到n的和,但是要求了一大堆,不让用这,不让用那的。
🆗,那不用就不用呗,其实借助我们刚才学的知识,就可以很巧妙的去解这道题。
怎么做呢?
我们自己呢定义这样一个类,两个静态成员变量_i和_sum,分别初始化为0,1。
然后我们调用一次构造函数,就让_sum+=_i,然后_i++,这样第一次+1,第二次+2…
那现在我们要求1+2+3…+n的和,怎么办?
是不是是需要调用n次构造函数,所以,我们直接定义一个大小为n的类对象数组就行了。
class Sum{ public: Sum() { _sum +=_i; ++_i; } static int GetSum() { return _sum; } private: static int _i; static int _sum; }; int Sum::_i=1; int Sum::_sum=0; class Solution { public: int Sum_Solution(int n) { Sum arr[n];//C99支持变长数组,可以用变量指定数组大小,但不能初始化。 return Sum::GetSum(); } };
这样就行了。
2.4 总结
那最后我们来总结一下:
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。
特性:
静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明,静态成员变量一定要在类外进行初始化
类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
静态成员函数没有隐藏的this指针,不能访问任何非静态成员
静态成员也是类的成员,受public、protected、private 访问限定符的限制