目录
1. 再谈构造
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; };
而真正的初始化是在初始化列表中进行的,初始化列表的位置是在构造函数”{}“的前面,是由一个冒号后面跟着多个用逗号隔开的成员变量,每个成员变量后面都有括号,括号里面就是初始值,初始化的次序与成员列表次序无关,是按照成员的声明顺序进行初始化,如下:
Date(int year, int month, int day) :_year(year) ,_month(month) ,_day(day) {}
而且像下面三种成员必须使用初始化列表进行初始化:
- const 成员变量:因为const成员具有常性不能进行赋值,而函数体是进行赋值的,所以只能在初始化列表进行初始化(具体与初始化列表的行为有关,待会叙述)
- 引用成员变量:因为引用在定义的时候必须初始化,并且引用的实体不能更改,如果在函数体里进行,本质上是一个赋值重载。
- 没有默认构造的自定义类型:因为自定义类型会调用默认构造进行初始化,如果没有构造就必须显示的初始化。
1.2 初始化列表的行为
我想大家应该都很困惑,为什么在初始化列表里面是初始化而在函数体里面就是赋值?初始化列表的执行时间是在函数体之前的,当我们调用构造函数的时候,编译器会首先去按照声明顺序进行默认的初始化,如果在初始化列表中有构造初始值则不会执行默认初始化,而去执行显式的初始化,所以无论我们有没有去写初始化列表,都是会在函数体执行之前初始化,这也就解释了为什么有些成员只能在初始化列表里面进行初始化和初始化列表的顺序与初始化顺序无关。而初始化列表对于编译器只是一个参考,可以认为编译器在执行的时候如果你(列表)有,我就按你的来,如果没有我就按照我自己的来。而const成员一旦执行了初始化就不能进行赋值,所以在语法上规定了const必须人为的给构造初始值,这也就是为什么const必须在初始化列表进行初始化了。
而当我们执行到函数体的时候所有成员其实早就执行默认初始化初始化完成了,这个时候去进行所谓的”初始化“其实都是赋值(拷贝或者进行赋值重载)。
无论什么时候我们都建议使用初始化列表进行初始化,首先就是不容易出错,其次就是规则上的统一。
1.3 explicit关键字
如果构造函数只有单个参数或者除第一个参数无默认值其余均有默认值,在一些情况下会存在隐式类型转换。比如
class A { public: A(int a) :_a(a) {} private: int _a; }; // class B { public: B(int a, int b = 1, int c = 1) :_a(a) ,_b(b) ,_c(c) {} private: int _a; int _b; int _c; }; int main() { A a1 = 1; //这俩种情况都会发生隐式类型转换 转换成一个无名类型的对象进行拷贝 B b1 = 1; //C++11 B b2 = { 1, 2, 3}; //c++11支持的括号初始化,将数组进行类型转换再构造, //想了解可以看我的c++11专栏 }
编辑
对于代码中提到的括号初始化可以看我的这一篇文章:【C++11】 统一的列表初始化( {}初始化 )_子亦半截诗的博客-CSDN博客
回归正题,上述的这种类型转换有的时候我们并不希望发生,所以就有了explicit关键字,用这个关键字修饰构造函数,将不会发生这种类型转换,同时C++11的括号初始化特性也会被禁用。
class A { public: explicit A(int a) :_a(a) {} private: int _a; }; // class B { public: explicit B(int a, int b = 1, int c = 1) :_a(a) , _b(b) , _c(c) {} private: int _a; int _b; int _c; }; int main() { A a1 = 1; //报错:E0415 不存在从 "int" 转换到 "A" 的适当构造函数 B b1 = 1; //报错:E0415 不存在从 "int" 转换到 "B" 的适当构造函数 //C++11 B b2 = { 1, 2, 3 }; //报错:E2334 复制列表初始化不能使用标记为“显式”的构造函数 }
报错:编辑
2. 类中的static成员
类中的静态成员和类中其他的成员有很大的区别,非静态成员变量的初始化在初始化列表里面进行,但是静态成员变量不能在初始化列表中初始化,因为static成员并不属于某个对象,static成员被存放在静态区,被同类型的所有对象共享
2.1 静态成员变量
静态成员变量属于同类型的所有对象,它的定义和初始化在类外进行。在定义的时候需要在变量名前面加上 类名:: 。
int A::_a = 0; class A { public: private: static int _a; };
2.2 静态成员函数
由于静态函数属于每一个类对象,而不属于某个对象,所以静态成员函数也就没有this指针,所以也就不能访问类的非静态成员。非静态成员函数可以调用静态成员函数,但是静态成员函数不能调用非静态函数。可能有人要疑问了:非静态成员函数不也是被共享的吗,为什么不能被静态成员函数访问呢?这是因为非静态成员函数需要传入this指针,而静态成员函数是没有this指针的。
int A::_a = 0; class A { public: static void test() { } void test1() { test(); } private: static int _a; };
3. 友元
友元是一种突破封装的方式,让外部成员访问自己私有成员的一种方式,但过度使用会破坏封装。
3.1 友元函数
友元函数的声明就是在类中声明函数时在前面加上friend关键字,声明成友元函数这个函数就能访问对象的私有成员。在一些特殊情况下需要使用,就比如”<<“流写入符号重载,由于"<<"的左操作数必须是ostream对象,所以第一个参数就必须是ostream类型,但是如果定义成成员函数,第一个参数就变成this指针了,这会导致我们使用”<<“的时候就变成” 对象<<cout “,这与我们的习惯就不符,所以流写入的符号重载必须定义成全局函数,而且还必须要能访问类的私有成员,这个时候友元函数就可以发挥作用了。如下:
class Date { friend ostream& operator<<(ostream& _cout, const Date& d); friend istream& operator>>(istream& _cin, Date& d); private: int _year; int _month; int _day; }; ostream& operator<<(ostream& _cout, const Date& d) { _cout << d._year << ' ' << d._month << ' ' << d._day << endl; return _cout; } istream& operator>>(istream& _cin, Date& d) { _cin >> d._year; _cin >> d._month; _cin >> d._day; return _cin; }
- 友元函数可以访问类的所有成员,但不是类的成员
- 友元函数不可以用const修饰
- 友元函数不受访问限定符限制
3.1 友元类
如果一个类A声明了一个友元类B,那么B类中的所有成员都可以访问A类的所有成员,但A类不能访问B类的成员。友元关系不能传递和继承(继承以后再说),比如C是D的友元,D是E的友元,但C不是E的友元。
class A { friend class B; private: int _aa; int _ab; int _ac; }; class B { void print() { cout << _atest._aa << ' ' << _atest._ab << ' ' << _atest._ac << endl; } private: A _atest; };
4. 内部类
当一个类定义在另一个类的内部,这个类就被称之为内部类。内部类相当于外部类的友元类,可以通过对象参数访问内部类中的所有成员,访问静态成员的时候不需要对象参数就可以直接访问,但是外部类不能访问内部类的成员。内部类是一个独立的类,对外部类的大小没有任何影响。外部成员对内部类的访问会受到访问限定修饰符的限制。
class A { public: class B { void print(const A& a) { cout << a._a; } }; private: int _a; };
5. 匿名对象
匿名对象的定义就是在对象后面加上一个括号,匿名对象不用取名字,声明周期也只有一行,使用完后就自动调用析构函数销毁了。
class A { public: static int _a; }; int A::_a = 0; int main() { A(); //匿名对象 cout << A()._a; }
6. 对象拷贝时候的编译器优化
在我们传参或者传值返回的时候可能会进行频繁的构造和拷贝构造,如果对象较大,会造成极大的资源消耗,所以为此编译器做出一些优化。(只针对构造,赋值重载不会进行优化)
class A { public: A(int a = 0) { cout << "A(int a = 0)" << endl; } A(const A& aa) { cout << "A(const A & aa)" << endl; } ~A() { cout << "~A()" << endl; } }; void f1(A a) {} A f2() { A a; return a; } int main() { A a; f1(1); //连续构造+拷贝构造->直接构造 f1(A(2)); //连续构造+拷贝构造->直接构造 A a1 = f2(); //拷贝构造+拷贝构造->优化为一个拷贝构造 }
编辑