目录
一、构造函数之初始化列表
接类和对象(中)的构造函数继续说
我们晓得构造函数的实质就是初始化对象。
那么实现构造函数时,初始化成员变量有两种方式:
- 初始化列表初始化
- 函数体内初始化
上一节里面,我们写构造函数实现初始化时一直用的都是函数体内初始化,不过这种方法有局限性,故此现在我们来聊一下初始化列表初始化
为啥说函数体内初始化有局限性嘞?
因为有三种变量必须放在初始化列表里面初始化:
- 引用成员变量
- const成员变量
- 没有默认构造的类类型的变量
不过嘞,C++11里面支持在成员变量声明的位置给缺省值,这个缺省值的作用正是给没有显示在初始化列表初始化的成员使用的
说人话就是,即使上面三种变量没有在初始化列表里面初始化,但是只要它们在声明的时候有缺省值,也能编译通过,不会报错。
对于两种初始化的方式,我们推荐的还是尽量使用初始化列表初始化,
因为无论你用不用初始化列表,所有的成员变量也都会走初始化列表。
接下来用一张图,让大家更清晰的感受一下,之间的逻辑
先考虑初始化列表,没有初始化列表再考虑有无缺省值,没有缺省值再去调用自定义类型中的默认构造函数
总结来说就是:
- 每个构造函数都有初始化列表,无论我们是否显示写初始化列表
- 每个成员变量都要走初始化列表初始化,无论我们在不在初始化列表里面初始化这个成员变量
class Time
{
public:
Time(int hour)
:_hour(hour)
{
cout << "Time()" << endl;
}
private:
int _hour;
};
class Date
{
public:
Date(int& x, int year = 1, int month = 1, int day = 1)
:_year(year)
,_month(month)
, _t(12) //既有缺省值又有初始化,那么优先考虑初始化列表
, _ref(x)
, _n(1) //没有缺省值,const成员变量必须在初始化列表里面初始化
{
_month = month;
}
void Print() const
{
cout << _year << "-" << _month << "-" << _day << "-" << _ref << "-" << _n << endl;
}
private:
int _year; //初始化列表里面初始化
int _month; //放到函数体里面初始化,但是最终还会走初始化列表
int _day = 2; //_day没有在初始化列表里面初始化,但是有缺省值,故会采用它的缺省值初始化
Time _t = 3; // 没有默认构造,但是有缺省值,故不会报错
int& _ref; // 引⽤成员变量,引用的是传过来的实参i(即形参x)
const int _n; // const成员变量
};
int main()
{
int i = 0;
Date d1(i); // 类实例化对象
d1.Print(); //最后结果应该是,1,1,2,0,1
return 0;
}
注!初始化列表是按照成员变量在类中声明的顺序进行初始化的,跟成员变量在初始化列表里面初始化的顺序无关
建议声明顺序和初始化列表当中的顺序保持一致
二、类型转换
类型转换在后期学习中,是很常用的。
之前学const引用的时候,提到过int转成double时,会有一个临时对象来存储中间值,因为临时对象具有常性不能被修改,因此需要用const修饰。
以上呢,int转成double就是类型转换,不过它们是内置类型互相转换,这里我们讨论的是与自定义类型的转换
在具体讲之前,我们要明确一点的是,能够转换一定是因为二者之间有相应的联系,如果两种类型毫无联系而言自然是不能够转换的
对于想与类类型有联系的话,都是借助于构造函数的
这里我们讲两种类型转换:
- 内置类型隐式类型转换为类类型对象
- 类类型的对象之间的转换
内置类型转类类型:
之前有提到需要借助构造函数,那么具体怎么来理解呢?
内置类型想要转换为类类型,前提是,有用内置类型去构造自定义类型(即构造函数的参数是相关内置类型)
有个小点就是,当编译器遇到连续构造和拷贝构造的时候,就会优化成直接构造
类类型之间的转换:
由于类类型对象的转换同上面是一个原理,这里就不再过多赘述,我用代码给大家解释
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
{
return _a1 + _a2;
}
private:
int _a1 = 1;
int _a2 = 2;
};
class B
{
public:
B(const A& a) //因为涉及到类类型的转换,故其构造函数的参数一定得是A类
:_b(a.Get())
{}
private:
int _b = 0;
};
int main()
{
//内置类型转类类型:
A aa1 = 1; // 1构造⼀个A的临时对象,再⽤这个临时对象拷⻉构造aa3 ,编译器遇到连续构造+拷⻉构造->优化为直接构造
const A& aa2 = 1; //const引用同理
A aa3 = { 2,2 }; // C++11之后才⽀持多参数转化
//类类型之间的转换:
B b = aa3; // aa3隐式类型转换为b对象 ,原理跟上⾯类似
const B& rb = aa3;
return 0;
}
总结来说就是:
两个类型想要转换必然要有联系,而构造函数就是这个联系的桥梁,但是一旦构造函数前面加上explicit就将不再支持隐式类型转换了
三、static成员
static修饰的成员,称之为静态成员
static修饰的成员就两个:
- static修饰成员变量,称之为静态成员变量
- static修饰成员函数,称之为静态成员函数
静态成员变量的特点:
- 静态成员变量的初始化一定是在类外
- 静态成员变量是共享的,所有类对象都能够访问,不属于某个具体对象
- 静态成员变量是不存在对象中,它是存放在静态区的
由于静态成员函数是不属于某个对象里面的,所以也就不存在给缺省值初始化这一说了,因为它压根就不走构造函数的初始化列表
静态成员函数的特点:
- 静态成员函数是没有this指针的
- 正是因为没有this指针,所以静态成员函数是不能够访问非静态的成员,只能够访问静态成员
这里有个绕的小知识点,静态的只能访问静态的,非静态的能访问静态的也能够访问非静态的
那么说这么多,怎么去访问静态成员嘞?
其实也挺简单,通过 类名::静态成员 或者 对象.静态成员 的方式就能够访问静态成员变量和静态成员函数了
最后再注意一点,因为静态成员也是类里面的成员,所以其也受到访问限定符的限制
静态成员函数,突破类域也能够调用
//实现一个类,计算程序里面创建了多少个类的对象
class A
{
public:
A()
{
++_scount;
}
A(const A& t)
{
++_scount;
}
~A() //析构函数
{
--_scount;
}
static int GetACount() //提供一个共有的静态成员函数,里面放静态变量,其他类或者类外就能够访问了
{
return _scount;
}
private:
static int _scount;// 类⾥⾯声明
};
// 类外⾯初始化
int A::_scount = 0; //也需要指明在哪个类里面
int main()
{
cout << A::GetACount() << endl; //0
A a1, a2;
A a3(a1);
cout << A::GetACount() << endl; //3,说明A类里面创建了3个对象
cout << a1.GetACount() << endl; //3
//cout << A::_scount << endl; //不能够这么写,会报错,因为_scount虽然不属于类,但其也是类里面的成员被private限制
return 0;
}
四、友元
之前我们有用到过友元,友元其实就是提供一种突破类访问限定符的一种方式
我们可以形象的理解为,在一个类里面,我们友元声明了一个函数,就相当于告诉这个类,这个函数是我们的friend,它能够访问我们的成员变量
友元分为两种:
- 友元函数
- 友元类
关于友元函数:
- 友元函数可以访问类中私有的成员,只需要在类中声明一下即可
- 友元函数只是一种声明,并不是类里面的成员函数
- 友元函数是不受访问限定符限制的,它可以在类中任意地方声明
- 一个函数,可以是多个类的友元函数
// 前置声明,否则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中成员),B又是C的友元,那么这并不代表A就是C的友元了(即A不能够访问到C中的成员)
class A
{
// 友元声明
friend class B; //友元类
private:
int _a1 = 1;
int _a2 = 2;
};
class B
{
public:
void func1(const A& aa)
{
cout << aa._a1 << endl;
cout << _b1 << endl;
}
void func2(const A& aa)
{
cout << aa._a2 << endl;
cout << _b2 << endl;
}
private:
int _b1 = 3;
int _b2 = 4;
};
int main()
{
A aa;
B bb;
bb.func1(aa); //B类能够访问到A
bb.func1(aa);
return 0;
}
最重要的一点是,虽然友元给我们提供了遍历,但是友元还是能不用就不用,因为友元会增加耦合性破坏封装
五、内部类
内部类啊在C++中很少用,在Java中用的比较多
内部类正如其名就是把一个类写入到另一个类的里面,那么里面的那个类就称之为内部类。
那么同理,既然有内部类就一定有外部类,它们是同时存在的
内部类和外部类之间的关系:
- 外部类是不包含内部类里面的成员的
- 内部类默认是外部类的友元(即内部类可以访问到外部类里面的私有成员)
- 内部类只是受到外部类的类域限制和访问限定符限制
内部类的本质其实也就是封装,如果说我们实现一个类只是为了给另一个类使用,我们就可以考虑用内部类来实现这一操作。而且只要把这个内部类放到private里面,这就变成了一个专属内部类,其他地方都用不了
六、匿名对象
匿名对象还是比较重要的,后续会经常用到
我们先来了解一下啥是匿名对象吧
如图啊,很明显定义出名字的就是有名,没有定义名字的自然就是匿名对象了。
那设计出这个匿名对象的目的是什么呢?
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "调用一次构造函数" << endl;
}
~A()
{
cout << "调用一次析构函数" << endl;
}
private:
int _a;
};
class Solution {
public:
int Sum_Solution(int n) {
//...
return n;
}
};
int main()
{
//定义有名对象:
A aa1;
A aa2(2);
//A aa1(); // 不能这么定义对象,因为编译器⽆法识别下⾯是⼀个函数声明,还是对象定义
A(); //定义匿名对象
A(1);//定义匿名对象
Solution s1; //照以前,想要调用一个成员函数,必须先定义出一个对象
cout << s1.Sum_Solution(10) << endl;
cout << Solution().Sum_Solution(10) << endl; //但是有个匿名对象后,我们可以直接写成这样
return 0;
}
目的就是上面代码的倒数第二行,可以简化代码。
不过匿名对象的应用场景还不止这个,那以后再继续说还有哪些场景会用到,现在我们掌握这一个场景就够了
当我们把上面这代码运行后,会得到如下
这里就要说一个匿名对象很重要的特点了
匿名对象的生命周期只在当前这一行,当下一行之前会析构掉