前言:
时隔多日,继续学习C++,类和对象;
大致内容:初始化列表,explicit关键字,static成员,友元,匿名对象
一、再探构造函数
1、接上篇,我们实现构造函数时,初始化成员变量主要在函数体内部进行赋值;构造函数初始化还有一种方法就是初始化列表,初始化列表的使用方式是以一个冒号开始,接着一个逗号分隔的数据成员列表,在每一个“成员变量”后面跟一个放在括号中的初始值和表达式。
2、每一个成员变量在初始化列表中只能出现一次,语法理解上初始化列表可以理解为是每一个成员变量定义初始化的地方。
3、引用成员变量、cosnt成员变量、没有默认构造函数的类类型变量,这些必须在初始化列表位置进行初始化,不然会编译报错。
4、尽量使用初始化列表因为不在初始化列表初始化的成员也会走初始化列表(如果这个成员在声明的时候给了缺省值,初始化列表会使用这个缺省值进行初始化);(如果没有给缺省值,对于没有显示在初始化列表的内置类型成员变量是否初始化就取决于编译器了);对于没有在初始化列表初始化的自定义类型成员变量会调用这个成员类型的默认拷贝构造,如果没有就编译错误。
5、初始化列表中按照成员变量在类中声明顺序进行初始化,跟成员在初始化列表出现的先后顺序无关(建议声明顺序和初始化列表顺序保持一致)。
初始化列表
1、
初始化列表可以理解为每一个成员变量定义和初始化的地方。
初始化列表使用:
class Data { Data(int year = 1,int month = 1, int day = 1) :_year(year) ,_month(month) ,_day(day) {} private: int _year; int _month; int _day; };
2、
引用成员变量 、const成员变量和没有默认拷贝构造的类类型变量必须使用初始化列表 :
#include<iostream> using namespace std; class A { public: A(int a) :_a(a) {} //没有默认拷贝构造函数 private: int _a; }; class B { public: B(int b = 1, int& x) :_b(b) ,_i(x) ,_ci(520) ,_A(11) {} private: int _b; int& _i; //引用成员变量 const int _ci; //const成员变量 A _A; //没有默认拷贝构造的类类型成员变量 }; int main() { int x = 0; B b(11, x); return 0; }
3、
初始化列表中按照成员变量在类中声明的顺序进行初始化
class A { public: A(int a) :_a1(a) , _a2(_a1) {} void Print() { cout << _a1 << " " << _a2 << endl; } private: int _a2 = 2; int _a1 = 2; }; int main() { A aa(1); aa.Print(); }
二、类型转换
1、C++支持内置类型隐式类型转换成类类型对象,需要有相关内置类型为参数的构造函数。
2、构造函数前面加上explicit 就不再支持隐式类型转换。
3、类类型的对象之间也可以隐式转换,需要相应的构造函数支持
cout << _a1 << " " << _a2 << endl; } int Get() const { return _a1 + _a2; } private: int _a1 = 1; int _a2 = 2; }; class B { public: B(const A& a) :_b(a.Get()) {} private: int _b = 0; }; int main() { // 1构造⼀个A的临时对象,再⽤这个临时对象拷⻉构造aa3 // 编译器遇到连续构造+拷⻉构造->优化为直接构造 A aa1 = 1; aa1.Print(); const A& aa2 = 1; // C++11之后才⽀持多参数转化 A aa3 = { 2,2 }; // aa3隐式类型转换为b对象 B b = aa3; const B& rb = aa3; return 0; }
explicit关键字
explicit修饰的构造函数,将会禁止构造函数的隐式类型转换。
class Data { public: explicit Data(int year) :_year(year) {} private: int _year; }; int main() { Data y = 1000; return 0; }
三、static成员
static,
1、static修饰的成员变量,称之为静态成员变量,静态成员变量一要在类外面进行初始化。
2、静态成员变量是所有类对象共享的,不属于某个对象,(不存放在对象中,存放在静态区)。
3、用static修饰的成员函数,叫做静态成员函数,(静态成员函数没有隐藏的this指针)。
4、在静态成员函数可以访问其他的静态成员,但是不能访问非静态的成员(静态成员函数没有this指针)。
5、非静态的成员函数,可能访问静态成员和非静态成员。
6、突破类域就可以访问静态成员,可以通过(类名::静态成员)或者(对象.静态成员)来访问静态成员变量和静态成员函数。
7、静态成员也是类的成员,也受到public、private、protected这些访问限定符的限制。
8、静态成员变量不能在声明位置给缺省值来初始化,(缺省值是给构造函数初始化列表的),静态成员变量不属于某个对象,不走构造函数的初始化列表。
静态成员变量:
静态成员变量,存储在静态区,属于类,要在类外面进行定义和初始化;(不能再声明位置给缺省值)
这里定义一个静态成员,记录创建了多少个类对象。
class A { public: A(int a = 1) :_a(a) { ++i; } ~A() { --i; } static int GetI() { return i; } private: int _a; static int i; }; void Func1() { A aa1; cout << A::GetI() << endl; } int A::i = 0; int main() { A a1; cout << A::GetI() << endl; Func1(); cout << A::GetI() << endl; return 0; }
静态成员函数:
静态成员函数,没有隐藏的this指针,所以就无法访问其他的非静态成员;
突破类域,就能够访问静态成员,不需要像其他非静态成员一样通过类对象来访问(类名::静态成员)或者(对象.静态成员)
一道小小的编程题来,练习练习:
求1+2+3+...+n_牛客题霸_牛客网 (nowcoder.com)
代码如下:
class A { public: A() { i++; num += i; } static int GetNum() { return num; } private: static int i; static int num; }; int A::i = 0; int A::num = 0; class Solution { public: int Sum_Solution(int n) { A sum[n]; return A::GetNum(); } };
四、友元
友元,提供了一种突破封装的方式,带来了一些便利,同时也会破坏了封装,不宜多使用。
友元函数
在实现运算符重载 operator<< 和operator>>时,因为有隐藏的this指针的存在,我们不能将其重载成成员函数(cout输出流对象和隐藏的this指针抢占第一个参数的位置);而如果重载成全局函数,我们又没办法访问类的私有成员变量了;所以就使用友元来解决这个问题。
友元函数可以直接访问类的私有成员,它定义在类外部的普通函数,不属于任何类,但是需要在类内部声明,声明时加friend关键字;定义的时候不需要加friend关键字。
class Data { //类中声明 friend ostream& operator<<(ostream& out, const Data& d); friend istream& operator>>(istream& in, Data& d); public: Data(int year = 1, int month = 1, int day = 1) :_year(year) ,_month(month) ,_day(day) {} private: int _year; int _month; int _day; }; //类外定义 ostream& operator<<(ostream & out, const Data & d) { out << d._year << "-" << d._month << "-" << d._day << endl; return out; } istream& operator>>(istream& in, Data& d) { cout << "依次输入年月日" << endl; in >> d._year >> d._month >> d._day; return in; } int main() { Data d1; cin >> d1; cout << d1; return 0; }
注意:
1、友元函数可以访问类的私有和保护成员,但不是类的成员函数
2、友元函数不能用const修饰(const修饰的是隐藏的this指针)
3、友元函数可以在类的任何地方声明,不受类访问限定符的限制
4、一个函数可以是多个函数的友元
5、友元函数的调用和普通函数一样
友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类的非公有成员
1、友元关系是单向的
比如,下面代码中,Time类中声明Data类是其友元类,那么就可以在Data类中直接访问Time类的私有成员;但是不能再Time中直接访问Data类的私有成员。
2、友元关系不能传递
3、友元关系不能继承(继承现在还没学习到)
class Time { friend class Data; public: Time(int hour = 1, int minute = 1, int second = 1) :_hour(hour) , _minute(minute) , _second(second) {} private: int _hour; int _minute; int _second; }; class Data { public: Data(int year = 1, int month = 1, int day = 1) :_year(year) , _month(month) , _day(day) {} void SetTime(int hour, int minute, int second) { //可以直接访问Time类的私有成员 //如果不是友元就会直接报错 _t._hour = hour; _t._minute = minute; _t._second = second; } private: int _year; int _month; int _day; Time _t; };
如果上面代码不在Time类中声明Data是友元类,则会出错
五、内部类
内部类
1、如果一个类定义在另一个类的内部,这个定义在类内部的类就是内部类;
内部类是一个独立的类,它只是受到外部类类域限制和类访问限定符限制,所以外部类定义的对象中不包含内部类(在计算类大小时,内部类不计算在其中)。
2、内部类默认就是外部类的友元类(可以直接访问外部类的私有成员)。
3、内部类本质上也是一种封装,如果A类和B类联系紧密,A类实现出来就是给B类用的,那就可以考虑将A类设计为B的内部类(如果放到private / protected位置,那么A类就只是B类 的专属类,其他地方无法使用)。
再回头看这一道题:
求1+2+3+...+n_牛客题霸_牛客网 (nowcoder.com)
这里我们实现的类A就是为了实现类Solution求1+到n,就可以写成内部类
代码如下:
class Solution { class A { public: A() { _i++; _ret+=_i; } }; static int _i; static int _ret; public: int Sum_Solution(int n) { A arr[n]; return _ret; } }; int Solution::_i=0; int Solution::_ret=0;
六、匿名对象
1、匿名对象,用类型直接定义出来的对象;之前定义的 类型 对象名 定义出来的时有名对象。
2、匿名对象生命周期只在当前这一行,(一般临时定义对象使用一下,可以定义匿名对象)。
匿名对象生命周期
class A { public: A(int a = 1) :_a(a) {} ~A() { cout << "~A()" << endl; } private: int _a; }; int main() { A a1(11);//有名对象,生命周期是当前函数栈帧 A(22); //无名对象,生命周期为当前这一行 cout << "-------------------------------" << endl; return 0; }
匿名对象使用
当我们不想要创建类对象,而要调用类的成员函数,就可以使用匿名对象:
class A { public: A(int a = 1) :_a(a) {} ~A() { cout << "~A()" << endl; } void Test() { cout << "我一直都在" << endl; } private: int _a; }; int main() { A a1(11);//有名对象,生命周期是当前函数栈帧 A(22); //无名对象,生命周期为当前这一行 cout << "-------------------------------" << endl; A().Test();//调用类的成员函数 return 0; }
七、编译器优化
1、现代编译器会为了尽可能提高程序的效率,在不影响正确性的情况下会尽可能减少一些传参和传返回值的过程中可以省略的拷贝。
2、如何优化C++标准并没有严格规定,各个编译器会根据情况自行处理。当前主流的相对新一点的编译器对于连续一个表达式步骤中的连续拷贝会进行合并优化,有些更新更"激进"的编译器还会进行跨行跨表达式的合并优化。