类与对象(四)
目录
1.构造函数
1.1初始化列表
1.2 隐式类型转换
2.静态成员
2.1 静态成员变量
2.2静态成员函数
3.友元
3.1 友元函数
3.2 友元类
4.内部类
5.匿名对象
6.拷贝对象时的一些编译器优化
1.构造函数
1.1初始化列表
我们在将构造函数的时候讲过构造函数是对一个对象整体的初始化,但是我们可以看到,在进入构造函数之前,我们的成员变量其实是已经初始化了的。在构造函数内部的代码执行之前,成员变量就已经被初始化了,这说明构造函数体内的代码只是再对这些成员变量赋初值而已,而不是初始化这些成员变量,变量的初始化只有一次,就是在定义这个变量的时候。
那么成员变量的初始化是在什么时候完成的呢?
我们可以看到,在调用构造函数之前我们都还没有完成成员变量的初始化,而已进入构造函数,在执行构造函数的指令之前,成员变量就已经完成初始化了,这是什么原因呢?
一个对象的初始化分为两层,第一层是初始化列表,然后才是构造函数体内的赋值。
初始化列表就是完成成员变量初始化的工作的。
初始化列表的格式:
在构造函数体前面,以一个冒号开始,接着是一个以逗号分隔的成员列表,每一个成员变量后面跟一个放在括号中的初始值或者表达式。
Date(int year = 1, int month = 1, int day = 1) :_year(2) ,_month(2) ,_day(2) { _year = year; _month = month; _day = day; }
这就是一个日期类的初始化列表。
初始化列表的注意:
1.一个变量只能在初始化列表中出现一次。因为一个变量初始化只有一次
2.我们不一定要把所有的成员变量都写在初始化列表中。
3.所有的成员都会走初始化列表,如果我们自己写在初始化列表中就是显式,我们不写的话编译器也会隐式的写到初始化列表中进行初始化。
4.对于隐式的初始化列表的成员变量,如果是内置类型就初始化为随机值,如果是自定义类型就会去调用他的默认构造进行初始化。
首先要理解的一点就是,类中的所有成员都会走初始化列表,不是显式就是隐式,所有成员的变量的初始化都是在初始化列表中完成的。
这很容易理解,就拿我们上面的日期类来说,我们并没有写初始化列表,但是在进入构造函数体之前所有成员变量都已经初始化了。
对于自定义类型的成员变量,我们如果不显式写在初始化列表中对其初始化的话,就会调用它的默认构造进行初始化。
有了初始化列表,我们就能对构造函数进行优化了,就是能够在初始化列表中初始化的变量尽量都用初始化列表。因为就算我们不写在初始化列表中,他们也都会隐式在初始化列表初始化,而这样一来,相对于比直接在初始化列表定义还多了一次赋值操作。
在上面的日期类中我们也可以看出,构造函数的缺省参数是在函数体内起作用而不是在初始化列表中起作用
我们可以拿Stack 和MyQueue类来看一下是不是这样的。
我们能看到,就算我们自己实现的MyQueue构造函数没有去对两个栈类型成员初始化,他在进入到构造函数体之前也已经去调用了栈的默认构造来完成这两个栈类型成员的初始化。
那么了解到这里了,我们是不是能对MyQueue的构造函数实现一个优化呢?我们在创建对象时可以直接传参数来指定两个栈类型成员的容量,这就可以用到我们的初始化队列。 因为我们上面的栈的默认构造的参数给了缺省值是4,如果我们想要一个 MyQueue 对象,他的两个栈的初始容量就是6,我们就可以这么来写他的构造函数
MyQueue(int size = 4) :pushST(size) ,popST(size) { _size = size; }
我们联想前面讲过的成员声明的时候的缺省值,这时候我们就可以观察一下这个缺省值到底是在构造函数体内起作用还是在初始化列表起作用。
以我们的日期类来举例。假如我们这样定义日期类
class Date { public: Date(int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } private: int _year=10; int _month=10; int _day=10; };
我们可以观察一下函数参数的缺省值和成员声明的缺省值分别是在那里起作用的。
首先我们可以看到的是,当我们的程序进行到断点的时候,在按一下 f10 就跳到了函数声明中,这时候我们就能得出结论:成员声明的缺省值就是在初始化列表中起作用的。
接着调试走完构造函数
构造函数的缺省参数是在构造函数体内起作用的。
这里我们该将成员变量声明的缺省理解为显式成员列表还是隐式呢?
验证的方法也很简单,就是我们在此基础上在写显式的初始化列表,如果声明的缺省是显式的,那么这时候编译器就会报错,如果缺省是隐式的,那么编译器就会以显式的初始化列表为准,而不生成这些成员的隐式初始化列表。
class Date { public: Date(int year = 1, int month = 1, int day = 1) : _year(20) ,_month(20) ,_day(20) { _year = year; _month = month; _day = day; } private: int _year=10; int _month=10; int _day=10; };
这里编译器执行的是我们显式写出来的初始化列表,这就说明了成员声明的缺省值是在我们没有显式的写在初始化列表时才会去使用的,或者 显式初始化列表的优先级大于声明缺省,大于随机值。当然,这是对于内置类型来说的,自定义类型声明时该怎么给出缺省呢?其实是一样的,简单的我们可以直接用匿名对象来给缺省,比如我们的Stack类
private: Stack pushST=Stack(10); Stack popST=Stack(10); int _size;
既然C++有初始化列表,那么肯定是尤其需求的,需求在哪里呢?或者说哪些类型的成员变量是必须初始化给初值的呢?
第一个必须在初始化列表中定义的就是const 修饰的成员变量
当我们类中定义了const 修饰的成员变量时,我们就比如在初始化列表中对其初始化,
class A { public: A(int a = 1, int b = 1) { _a = a; _b = b; } private: const int _a; int _b; };
如果我们定义了这样的一个类,在构造函数我们没有对 _a 初始化,这时候用这个类创建对象就会出问题
这是为什么呢?我们在C语言就学过了const 的作用,const 修饰的叫做常变量,是具有常属性的变量,const 修饰的变量只有一次赋值的机会,就是在定义的时候进行初始化,而且是必须初始化,初始化之后它的值就不能修改了,因为它具有常属性。
那么他作为成员变量也是一样的,必须在定义它的时候初始化给一个值,成员变量的定义和初始化就发生在初始化列表,所以对于这种const修饰的成员变量,我们是必须显式的写在初始化列表的,要不就是声明变量的时候给一个缺省值,但是这种做法在实际中的应用并不多,因为缺省值是固定的,不像显式的初始化列表我们可以通过构造函数传参定义他的值。
两种方法:
1.声明给缺省值
class A { public: A(int a = 1, int b = 1) { _b = b; } private: const int _a=10; int _b; };
2.显式的初始化列表定义初始化
class A { public: A(int a, int b = 1) :_a(a) ,_b(b) { } private: const int _a; int _b; };
更推荐第二种方法。
第二个必须在初始化列表中初始化定义的就是引用成员
与const修饰的变量相似,引用变量的性质也是只能在定义的时候引用一个实体,往后就不能在修改引用的对象了。
那么对于引用的成员变量怎么传参和初始化呢?
class A { public: A(int& ra, int b = 10) :_ra(ra) , _b(b) { } private: int& _ra; int _b; }; int main() { int c; A a(c, 5); return 0; }
第三个必须在初始化列表中定义初始值的就是没有默认构造的类对象成员变量
我们前面说了类类型的成员变量如果没有显式写在初始化列表中,编译器会隐式地在初始化列表中调用他的默认构造函数进行初始化 ,那么如果这个类对象没有默认构造函数,那么编译器就会报错
class A { public: A(int a) { _a = a; } private: int _a; }; class B { public: B(int a) //A类没有默认构造,编译器会报错 { _a = a; } private: int _a; A aa; };
这时候正确的写法就是在初始化列表中调用A类的构造函数对其初始化。
class B { public: B(int a=10,int aa=10) :_aa(A(aa)) ,_a(a) {} private: int _a; A _aa; };
这里的过程成就是,首先创建一个匿名的A类对象,然后用A类自动生成地拷贝构造函数对成员变量_aa进行拷贝构造。
这里也应证了我们之前的一个建议,就是一个类最好要有默认构造
1.2 隐式类型转换
什么是隐式类型转换呢?我们来看下面的两行代码
double b = 1.23; int i = b;
为什么我们能用double类型的值对 int 类型的 i 初始化呢?就是因为这其中发生了隐式类型转换。编译器首先会生成一个 b 强制转换成的 const int 类型的的临时变量,然后用这个临时变量对 i 初始化,这里的临时变量是具有常性的,但是我们是可以用一个const int 类型的变量去给一个int类型的变量初始化的,大家千万不要被之前讲的指针和引用的权限放大缩小给绕进去了,权限的放大和缩小只存在于指针和引用,对于复制和初始化是没有任何影响的。
那么下面的一段代码是否也是隐式类型转换呢?
class Day { public: Day(int day) :_day(day) { } private: int _day; }; int main() { Day d1 = 5; return 0; }
通过调试我们可以发现,这样赋值是没问题的,这就说明了这之间发生了隐式类型转换
这里的隐式类型转换的过程是什么样的呢?
首先编译器会去看一下Day类中有没有能传一个参数的构造函数,如果有,编译器就会用这个值通过构造函数生成一个 const Day 类型的临时变量,然后调用拷贝构造,用这个临时变量拷贝出一个d1对象出来。但是如果Day类中没有能传单参数调用的构造函数,这时候就无法进行类型转换,因为编译器无法用这个整形来构造一个Day类的临时对象。
但是这样一来,这个隐式类型转换的过程中就发生了一次构造和一次拷贝构造,这是不是有点降低效率了,因为这里的这个临时变量是没有很大的价值的,不像函数传值返回时的临时变量。对于这种连续的构造过程,以前的编译器执行起来就是一次构造加一次拷贝构造,而现在的编译器就会对这种行为进行优化,将这两次构造转换为一次直接构造,直接用这个整型去构造d1对象。
但是如果是引用的初始化,我们就只需要一次构造函数用这个整形来构造一个const Day的临时变量,所以必须是const Day类型的引用。
const Day& d1 = 5;
如果我们不想让这种隐式类型转换发生,我们可以用一个关键字 explicit 来修饰构造函数,这样就禁止了这种隐式类型转换,不能用被 explicit 修饰的这个构造函数进行隐式类型转换。
这种单参数的隐式构造在C++98就已经支持了
但是C++98只支持用单参数的构造函数来发生隐式类型转换,对其他的构造函数支不支持。
而在C++11标准中,支持了多参数的、半缺省、全缺省的构造函数发生隐式类型转换。
那么多参数的隐式类型转换要怎么实现呢?
那我们的日期类来举例,我们的日期类是自己写了一个全缺省的构造函数,这就意味着我们可以传一个参数、传两个参数或者传三个参数来进行隐式类型转换
Date d1 = 12; Date d2 = { 12,12 }; Date d3 = { 12,12,12 };
对于这种多个参数的隐式类型转换,我们要用花括号括起来,表示它是一个整体。
同时,这种操作在原来的编译器中也是一次构造加一次拷贝构造,而现在的编译器就及逆行了优化,直接用参数来构造对象,省去了一次拷贝构造的消耗。
2.静态成员
静态成员顾名思义就是用static修饰声明的成员,分为静态成员变量和静态成员函数。
2.1 静态成员变量
静态成员变量有一个很常用的应用场景,就是统计一个类一共构造了多少个对象,创建对象无非就是构造或者拷贝构造,我们只要在这些构造函数中进行计数,就能够统计出该类创建的对象个数。其实这种计数功能我们用一个全局变量也能实现,但是全局变量最大的问题就是他的全局性,所有的函数都可以用它,这样一来误操作的概率就会很大,不安全。如果我们是定义在类中私有的话,那么就只能通过成员函数来访问和修改,这样一来就更加可控。
class A { public: static int GetN() { return N; } A(int a=0) :_a(a) { } private: static int N; int _a; };
如上,在A类中,我们定义了一个静态成员函数 GetN() 和静态成员变量 N ,那么对于静态变量的第一个疑问就是:是否每一个对象中都会存储一个 N ?我们首先要知道,静态变量是存在静态区的,而类对象一般是在函数中创建的局部变量,一般是存在栈中的,从这一点我们就能知道对象中是不可能会存一个静态变量N的,那么是不是对象中会拷贝一个N呢?这也没必要,因为这个类的所有的对象共享一个 N ,当我们想要通过某个对象去访问 公共的 N 时,编译器会去静态区找到N并且返回,编译器是知道 N 的地址的,所以同时,我们每个对象中是不会存储 N 的拷贝的,存了也没意义,因为难道某一个对象修改了N ,难道所有的对象都要对自己体内的N的拷贝修改一遍吗?这样的工作量太大了,而且是完全没有意义的,反正需要访问的时候编译器也能找到。
那么这样一来,这个类的大小或者说用这个类创建的对象的大小就是不包含静态成员变量的大小的,上面的A 类的大小就是4 个字节
同时我们可以先把N设置成共有的,然后用A类创建两个对象来看一下他们的N 是不是访问的同一个。
那么在类中的静态成员变量和在局部域或者全局域定义的静态变量有什么区别呢?首先,局部域中定义的静态变量它的作用域和生命周期都是局部范围,出了他的局部返回就销毁了。而对于全局的静态变量和类中的静态成员变量,他们的生命周期都是全局的,但是全局的静态变量它的作用域也是全局的,不受限制,而类中声明的静态变量的作用域则要受到类域的限制,只能在类域中对其进行操作。
搞清楚了他的生命周期和作用域,下一个问题就是静态变量的定义。首先肯定要排除在初始化列表中,因为初始化列表是在变量定义的时候进行初始化,而静态变量是所有的对象所共享的并不是说每创建一个对象调用构造函数的时候就要对他初始化一次,同时,编译器也不会将静态变量隐式地送上初始化列表。 那么我们要把静态变量的定义写在构造函数体内吗?构造函数体是对成员变量赋值,而不是定义和初始化,对于静态变量而言,我们可以在构造函数内对它进行修改计数,但是我们不能在构造函数内对其赋一个相同的值,为什么呢?如果每调用一次构造函数,静态变量就变回了一个固定的值了,那么就不能完成计数的功能了?
A(int a=0) :_a(a) { N++; //有意义的 N = 2;//无意义的 }
那么这时候就只有在全局进行静态成员变量的定义了,在全局定义的时候一定要指定类域。
int A::N = 0;
为什么不能在局部域中定义呢?前面说了,类域中的静态成员变量的生命周期是全局的,如果在局部域中定义的话,就相当于定义了一个局部的静态变量而不是全局的静态变量了,这不符合他的性质。
我们在全局定义了静态成员变量,那么是不是意味着,即使我们不通过类去创建对象,这个静态变量也是存在的,那么我们要怎么访问呢? 访问起来也很简单,我们只需要指定类域就可以了,对于静态成员变量,我们可以直接指定类域,也可以通过类对象或类类型的指针来间接指定类域,那么之前我们玩过的,类类型的空指针能否访问到静态成员变量呢?我们可以试一下(将N设置为共有,测试一下是否能通过类域和空指针来访问)
cout
还没有评论,来说两句吧...