【C++】类和对象(下)
【C++】类和对象(下)
前言:虽说类和对象都很抽象,但是相较于类和对象中,下没有那么难,接下来再次深入了解一下呢~
一、再探构造函数
-
在之前,实现构造函数时,初始化成员变量最主要使用函数体内赋值,在这里我们探讨另一个方式:初始化列表以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
-
每个成员变量在初始化列表中只能出现一次 ,语法理解上初始化列表可以认为是每个成员变量定义初始化的地方
-
引用成员变量,const成员变量,没有默认构造的类类型成员变量,必须放在初始化列表位置进行初始化的成员使用
-
C++11支持在成员变量声明的位置给缺省值,这个缺省值主要是给没有显示在初始化列表的成员使用的
class Time
{public:Time(int hour):_hour(hour){cout << "Time()" << endl;}
private:int _hour;}
class Data
{public://Data后面的括号的内容就是初始化的表达式Data(int& x,int year=1,int month=1,int day=1):_year=year(year),_month=month(month),_day=day(day),_t(12),_ref(x),_n(1){}private://声明的位置int _year;int _month;int _day;Time _t;//没有默认构造int& _ref;//引用const int _n;//const成员变量
}
- 尽量使用初始化列表初始化,因为即使不在初始化列表的成员也会走初始化列表,若这个成员在声明处给了缺省值,初始化列表会用这个缺省值初始化。若没有缺省值,对没有显示在初始化列表的内置类型成员是否初始化取决于编译器;对没有显示在初始化列表的自定义类型成员会调用这个成员类型的默认构造,若没有默认构造就会编译错误。
- 初始化列表中按照成员变量在类中声明顺序进行初始化,跟成员在初始化列表出现的先后顺序没有关系。(因此建议初始化列表和声明顺序一致)
class Data
{public:Data(int year=2,int month=2,int day=2):_year(year),_month(month),_day(day){}private://声明给缺省值->初始化列表//这里不是初始化列表,这里是给缺省值,这个缺省值是给初始化列表的//若初始化列表没有显示初始化值,默认就会用这个缺省值初始化int _year=1;int _month=1;int _day=1;}
成员变量走初始化列表的逻辑图
用初始化列表的优点:
- 效率方面:避免不必要的默认构造和赋值操作;对于引用成员变量,const成员变量,没有默认构造的类类型成员变量必须初始化时赋值,不能先默认构造再赋值
- 代码简洁性和可读性:简洁表达初始化意图,清晰的看到所有成员变量的初始化情况;遵循类的设计语义,明确成员变量的初始状态。
二、类型转换
- C++支持内置类型隐式类型转换为类类型对象,需要有相关内类型为参数的构造函数
- 构造函数前面加explicit就不再支持隐式类型转换。
explicit关键字拓展:用于修饰类的构造函数,以防止隐式类型转换。在没有explicit关键字时,单参数的构造函数(或除第一个参数外其余参数都有默认值的多参数构造函数)可以用来隐式转换
class Data
{
public://单参数构造函数,没有使用explicit修饰,具有类型转换作用 //explicit修饰函数构造函数后,禁止类型转换,运行会报错//explicit Data(int year)Data(int year):_year(year){}}
- 类类型的对象之间也可以隐式转换,需要相应的构造函数支持
class A
{
public:A(int a1):_a1(a1){}A(int a1, int a2):_a1(a1), _a2(a2){}void Print(){cout << _a1 << " " << _a2 << endl;}int Get() const//const修饰函数,实际修饰的是该成员函数隐含的this指针{return _a1 + _a2;}
private:int _a1 = 1;int _a2 = 2;
};
int main()
{// 1构造⼀个A的临时对象,再⽤这个临时对象拷⻉构造aa3 // 编译器遇到连续构造+拷⻉构造->优化为直接构造 A aa1 = 3;aa1.Print();const A& aa2 = 1;// C++11之后才⽀持多参数转化 A aa3 = { 3,5 };aa3.Print();
图形理解:
三、static成员
- 用static修饰成员变量,称之为静态成员变量,静态成员一定要在类外进行初始化
- 静态成员变量为所有类对象所共享,不属于某个具体的对象,不存在对象中,存放在静态区
- 静态成员函数,可以访问其他静态成员,但是不能访问非静态的,因为没有this指针。
class Data
{
public://没有this指针,func1访问不了func2static void func1(){}void func2(){}
private:int _year;int _month;int _day;
}
- 非静态的成员函数,可以访问其他的静态成员和静态成员函数。
- 突破类域就可以访问静态成员,可以通过类名(::)静态成员或者对象(.)静态成员来访问静态成员变量和静态成员函数。
- 静态成员也是类的成员,受public、protected、private访问限定符的限制。
- 静态成员变量不能在声明位置给缺省值初始化,因为缺省值是个构造函数初始化列表的,静态成员变量不属于某个对象,不走构造函数初始化列表。
class A
{
public:Sum(){_ret+=_i;++_i;}static int GetRet(){return _ret;}
private:int _ret;//类内声明static int _i;
}
//类外初始化
int A::_i=0;
四、友元函数
- 友元提供了一种突破类访问限定符封装的方式,友元分为:友元函数和友元类,在函数声明或者类声明的前面加friend,并且把友元声明放到一个类的里面。
- 外部友元函数可访问类的私有和保护成员,友元函数仅仅是一种声明,它不是类的成员函数。
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制。
- 一个函数可以是多个类的友元函数。
- 友元类中的成员函数都可以是另一个类的友元函数,都可以访问另一个类中的私有成员和保护成员(这个点会在继承看的更明显)
- 友元类关系是单向的,不具有交换性,如A是B的友元,但B不是A的友元(你是我的好朋友,但我不是你的好朋友);也不可以传递(我是你的好朋友,但我的另一个好朋友不是你的好朋友)
- 友元提供了便利但破坏了封装性!
示例:
#include<iostream>
using namespace std;
// 前置声明,都则A的友元函数声明编译器不认识B
class B;
class A
{// 友元声明 friend void func(const A& aa, const B& bb);
private:int _a1 = 1;int _a2 = 2;
};
class B
{// 友元声明 friend void func(const A& aa, const B& bb);
private:int _b1 = 3;int _b2 = 4;
};
void func(const A& aa, const B& bb)
{cout << aa._a1 << endl;cout << bb._b1 << endl;
}
int main()
{A aa;B bb;func(aa, bb);return 0;
}
五、内部类
- 如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,跟定义在全局相比,他只是受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类。
- 内部类默认是外部类的友元类。
- 内部类本质也是一种封装,当A类跟B类紧密关联,A类实现出来主要就是给B类使用,那么可以考虑把A类设计为B的内部类,如果放到private/protected位置,那么A类就是B类的专属内部类,其他地方都用不了。
示例:
#include<iostream>
using namespace std;class A
{
private:static int _k;int _h = 1;
public:class B // B默认就是A的友元 {public:void foo(const A& a){cout << _k << endl; cout << a._h << endl; }};
};
int A::_k = 1;
int main()
{cout << sizeof(A) << endl;A::B b;A aa;b.foo(aa);return 0;
}
六、匿名对象
- 用类型(实参)定义出来的对象叫做匿名对象,相比之前我们定义的类型对象名(实参)定义出来的叫有名对象
- 匿名对象生命周期只在当前一行,一般临时定义一个对象当前⽤一下即可,就可以定义匿名对象。
class A
{
public:A(int a = 0):_a(a){cout << "A(int a)" << endl;}~A(){cout << "~A()" << endl;}
private:int _a;
};
int main()
{A aa1;//有名对象// 不能这么定义对象,因为编译器无法识别下面是一个函数声明,还是对象定义 //A aa1();// 但是我们可以这么定义匿名对象,匿名对象的特点不用取名字, // 但是他的⽣命周期只有这一⾏,我们可以看到下一⾏他就会⾃动调用析构函数 A();A(1);A aa2(2);}