4.构造函数补充知识点(重要)----初始化列表:
在构造函数知识点位置,我们曾经提到过初始化列表,默认构造函数是会调用它的自定义类型的默认构造函数的,但假如其自定义类型成员是显式构造函数,而不是默认构造的情况下,编译器是不会让其通过运行的,但我们同时还想自定义让其不调用默认而是调用我们想要赋予的数值,想要解决这个问题,就要使用我们的初始化列表
1.初始化列表的形式:
本质上,初始化列表就是构造函数的一部分,它是在构造函数的函数体前加上一块为成员变量赋值的过程,如下:
class Date { public: Date(int year,int month,int day) :_year(year), _month(month), _day(day) { //;构造函数的函数体内容 } };
注意构造函数的几个书写的注意点:
1.构造函数的成员赋值用的是括号而不是等号
2.在函数体里仍可以调整类的成员的数值
3.我们所谓的C11支持给类的成员加上缺省值,实际上就是将缺省值先赋给初始化列表,然后通过初始化列表来进行赋值
4.对于仍然需要动态开辟的成员来说,动态开辟最好还是写在构造函数的函数体里最好,这样方便我们去书写开辟检验的检查程序
2.对初始化列表的理解:
初始化列表可以理解为是每一个成员定义的地方,这与给缺省值的声明是不同的,初始化列表用于解决当成员中由const/引用这写创建的时候必须为其初始化并且我们还想给其我们想给的对象的时候,将其放在缺省值仅仅是声明,相当于没赋值,对于引用和const来说这是没法通过的,故我们利用初始化列表对这些特殊成员进行定义处理,从而解决了问题,也包括自定义成员,在没有默认构造的情况下,在初始化列表可以对其显式构造传参起到了定义的作用,这很好的解决了我们上面的问题。比如我们可以在初始化列表阶段给自定义类型这样赋值a(1),这样我们就可以随心所欲的构造初始化我们的自定义成员
总结:我们可以通过调试的过程发现:初始化列表是每一个成员定义的地方,不管你写或者不写,每一个成员都要走初始化列表,内置类型赋随机值,自定义类型调用其默认构造,且要理清关系,初始化列表就是构造函数的一部分,它的本质是为其提供两种不同的初始化方式。
C++11支持的给缺省值,本质上就是将这个缺省值给初始化列表进行定义的,如果初始化列表没有显示给值,就用缺省值,如果显示给值了,就不用这个缺省值。
初始化列表我们需要注意的点:
1.每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
2.类包含以下成员必须将其放在初始化列表的位置进行初始化:
A.引用成员变量
B.const成员变量
C.自定义类型成员变量(且改类没有默认的构造函数显性调用或者我们想要为其赋值的时候)
3.尽量使用初始化列表进行初始化,因为不管你是否使用初始化列表,对于自定义类型的成员变量,一定会使用初始化列表进行初始化,但特殊情况下要使用初始化列表与函数混合使用(动态开辟)
4.成员变量在类中的声明次序就是其在初始化列表阶段对成员变量定义初始化的顺序,与其在初始化列表的顺序无关,只与声明顺序有关
第四点,我们的可以通过调试得出答案。
5.单参数构造函数的隐式类型转换:
注意其条件,它适用于只含有一个参数的构造函数,也就是说,我们初始化的时候只为其赋一个值,无论是缺省值也好抑或是手动赋值也好好。
例如:下面的例子:
#include<iostream> using namespace std; class A { private: int _a; char _c; double _b; public: A(int a) :_a (a) { cout << 1 << endl; } }; int main() { A a1(1); A a2 = 1; const A& a3 = 3; return 0; }
在这里,我们用三种方式创建,其结果为:
我们发现他们都可以通过并且都调用了构造函数,第一个形式不多说了,是很常规的构造方式,第二个形式是因为首先会调用一个构造函数生成a2,然后再将1赋值给a2,对于编译器来说不如直接将1作为参数直接构造初始化a2,故它直接调用一次构造就出来了,第三个形式是由于3本身为常量,拷贝的过程中的临时变量具有常属性,所以拿const修饰的变量去接收即可。
这样的隐式类型转换有的时候会让人感觉很乱,这种隐式类型转换有时候是可以避免的,我们可以使用explicit在构造函数前,加上之后就不支持隐式类型转换了,如下:
#include<iostream> using namespace std; class A { private: int _a; char _c; double _b; public: explicit A(int a) :_a (a) { cout << 1 << endl; } }; int main() { A a1(1); A a2 = 1; const A& a3 = 3; return 0; }
报错如下:
C++11中不仅支持单参数,也支持多参数的隐式转换,用{ }符号来写入参数,若不想发生,同样也写explicit即可,这一点与单参数相同,如下:
#include<iostream> using namespace std; class A { private: int _a; char _c; double _b; public: A(int a,int c,int b) : _a(a), _b(b), _c(c) { cout << 1 << endl; } }; int main() { A a1{ 1,2,3 }; A a2 = { 10,20,30 }; return 0; }
6.匿名对象:
匿名对象即是不拿名字,直接拿类型创建出来的对象,和常规的有名对象不同,有名对象的作用域在当前的局部域,而匿名对象的作用域仅仅在这一行中了,它的特点是调用一次构造后又会调用一次析构,注意最重要的一点,匿名对象也是一种临时变量,故具有常属性
1.匿名对象的作用
用匿名对象可以起到传参数的作用,可以让代码更加的简化,因为有时我们定义一个对象就是为了调用函数,为了一个函数而去创建一个变量,不如直接使用匿名的对象去调用函数,这样就根本不用定义一个对象就能直接调用函数。
实例程序如下:
#include<iostream> int s = 0; using namespace std; class A { private: int _a; char _c; double _b; public: A(int a=10,int c=20,int b=30) : _a(a), _b(b), _c(c) { cout << 1 << endl; s++; } ~A() { cout << 1 << endl; } }; int main() { const A& x = A(1, 20, 3); A a1(1,2,3); A a2 = { 10,20,30 }; A arr1[10] = { A(1,2,3),A(20,30,40),A(52,60,20)}; cout << s << endl; return 0; }
比如在这里,我们完全可以利用一个数组来统计s的数值的增加,或者是为了调用某个值而匿名对象,比如说在这里就是为了得到s的累加而构建许多个匿名对象去处理。
2.匿名对象的一种延长生命周期的写法:
看下面的程序(注意匿名对象具有常属性,也就是具有const的属性限制,故拿非const去接收是不可以的)
#include<iostream> using namespace std; class A { private: int _a; char _c; double _b; public: A(int a,int c,int b) : _a(a), _b(b), _c(c) { cout << 1 << endl; } ~A() { cout << 1 << endl; } }; int main() { const A& x = A(1, 20, 3); A a1(1,2,3); A a2 = { 10,20,30 }; return 0; }
在这行代码中,通过调试,你会发现第一个匿名对象在调用完构造函数之后没有立刻调用析构函数,也就是说,引用接收的匿名对象实际上延长了它的生命周期,让它有了名字而不是匿名的了吗,这便是匿名对象延长生命周期的写法。
7.static成员:
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量,用static修饰的函数,称之为静态成员函数,静态成员变量一定要在类外进行初始化。
1. static修饰的特点:
1.由static修饰的静态成员为所有类成员所共享的,不属于某个具体的对象,存放在静态区,不纳入计算对象的大小
2.静态成员变量必须在类外进行定义,定义时不添加static关键字,类中只是声明,且不要给缺省值
3.类静态成员可以用类名::静态成员/对象.静态成员来访问,这也进一步证实了静态成员变量属于这个类,也同时被所有这个类的对象所共享。
4.静态成员函数没有隐藏的this指针,不能访问任何非静态成员(这也进一步说明了其没有this指针)
5.静态成员虽然不属于哪个具体的类,但依旧受类的public,protected,private访问限定符的限制
#include<iostream> using namespace std; class A { private: int _a; char _c; double _b; static int s; public: A(int a=10,int c=20,int b=30) : _a(a), _b(b), _c(c) { cout << 1 << endl; s++; } ~A() { cout << 1 << endl; } }; int A::s = 100;//这表示,这个全局变量是由全部这个类所共享的,故要加访问限定符 int s = 0; int main() { const A& x = A(1, 20, 3); A a1(1,2,3); A a2 = { 10,20,30 }; A arr1[10] = { A(1,2,3),A(20,30,40),A(52,60,20)}; int m = sizeof(A); cout << m << endl; return 0; }
如下,我们这里的s即为对应的静态成员变量,别忘了,定义静态成员变量的时候一定要加上A::的访问限定符号,要不然没法让编译器知道你这是在这个类里面的静态成员变量。
如下:
void static Func() { cout << "hello world" << endl; _a = 30; }
报错信息为:
这里便证实了,静态成员函数里面是没有隐藏的this指针的,故我们访问对应的成员是非法的。但这一条也同时说明哪怕是匿名对象也可以使用静态成员函数,因为根本不能访问里面的成员,只要在返回类型后加static即可
8.友元:
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。我们还是提倡
友元分为:友元函数和友元类
1.友元类的通用写法:
friend 类+对象名称/friend 函数声明
友元的意义即声明一块位置与当前的类建立一个联系,让对应的类或者函数可以直接访问友元里面的成员变量。
我们举出下面的一个例子:
使用友元进行<< >>的运算符重载:
void operator<<(ostream& out) { out<< } void operator>>(istream& in) { in>> }
这样写,我们会遇到一个很关键的问题,成员变量都是私有的,没法访问变量,故我们在这里使用友元来让其可以访问。
如下:
class A { friend void operator<<(ostream& out) const; }
注意,友元是可以放在类的任意位置的,不受条件限定符的限制。
注意写全局变量时,都要用引用,这样作用于本体,不需要调用一次拷贝构造的过程。
9.内部类:
如果一个类定义在另一个类的内部,这个类就叫做内部类,内部类是一个独立的类,和外部类都是独立的,在计算对象大小的时候相互不把对方计入到自己的成员里面,我们是不能通过外部类的对象去访问内部类的成员,外部类对内部类没有任何优越的访问权限,想要访问内部类,必须通过函数给内部类的成员带出来。
注意:内部类是外部类的友元类,内部类可以通过外部类的对象参数直接访问外部类的所有成员,但外部类不能使用内部类的成员,只能由函数带出来。注意,想要创建一个内部类的对象,要加上外部类的访问限定符,例如:
A::这样的访问限定符,要不然没法创建。
1.内部类的特性:
1.内部类可以创建在外部类的public,protected,private都是可以的
2.注意内部类可以直接访问外部类中的static成员,不需要外部类象/类名
3.sizeof(外部类)=外部类,和内部类没有任何关系
内部类受外部类的类域和访问限定符的限制,其他他们两个是独立的类,倘如B类在A类的public,可以经过访问限定符创建B类,但倘若B不在public,是访问不到B的,必须通过创建函数才能创建B类。
如下:
#include<iostream> using namespace std; class A { private: int _a; char _c; double _b; static int s; class B { private: int s = 0; public: void print() { cout << 1 << endl; } }; public: A(int a=10,int c=20,int b=30) : _a(a), _b(b), _c(c) { cout << 1 << endl; s++; } ~A() { cout << 1 << endl; } void static Func() { cout << "hello world" << endl; } void }; int A::s = 100;//这表示,这个全局变量是由全部这个类所共享的,故要加访问限定符 int s = 0; int main() { const A& x = A(1, 20, 3); A a1(1,2,3); A a2 = { 10,20,30 }; A arr1[10] = { A(1,2,3),A(20,30,40),A(52,60,20)}; int m = sizeof(A); cout << m << endl; A::B q; return 0; }
报错信息为:
10.拷贝对象时的一些编译器优化:
1.既构造又拷贝构造时,编译器按直接构造处理优化,但C++并没有规定优化,主要看编译器
故一个表达式,连续的步骤里,连续的构造会被合并
2.传值返回:用未定义的对象接收时,直接给这个未定义的对象一次拷贝构造,而不用先拷贝再拷贝构造两次了
总结:
以上便是我对类和对象的全部理解,其实还有很多地方理解十分的粗糙和肤浅,但我会继续努力,争取在C++上更进一步。