文章目录
- 前言
- 一、初始化列表
- 概念
- 使用注意
- 实际运用
- explicit关键字
- 初始化列表的总结
- 二、static成员
- static成员的概念
- static成员的特性
- static的一个实用场景
- 三、友元
- 友元函数
- 友元类
- 四、内部类
- 概念
- 特性
- 五、匿名对象
- 六、再次理解封装和面向对象
- 总结
前言
Hello,本篇应该是类和对象系列里面较为轻松的一篇了,尤其是在经历了中篇的洗礼之后(,但是不可掉以轻心,要说这个下篇比上篇简单,那也是没有的
继续加油!
一、初始化列表
概念
前文我们讲构造函数的时候,提了一嘴初始化列表,不如我们先来看一下其概念吧:
以⼀个冒号开始,接着是⼀个以逗号分隔的数据成员列表,每个"成员变量"后⾯跟⼀个放在括号中的初始值或表达式
我们来对比一下,应该能让你有更加深刻的认识:
首先是我们之前的初始化方式
class Date
{
public:Date(int year, int month, int day){_year = year;_month = month;_day = day;}private:int _year;int _month;int _day;
};
这种方法赋初值当然有效,可是严格意义上来说并不能称其为初始化,因为初始化只能初始化一次,而函数体内却可以多次赋值
现在再来看看用初始化列表来赋初值
#include <iostream>
using namespace std;class Date
{
public://构造函数Date(int year = 1900, int month = 1, int day = 1):_year(year),_month(month),_day(day){}
private:int _year;int _month;int _day;
};
使用注意
-
每个成员变量在初始化列表中只能出现一次
原因是初始化只能进行一次,所以同一个成员变量在初始化列表中不能多次出现 -
类中包含以下成员,必须放在初始化列表进行初始化
引用成员变量、const成员变量、自定义类型成员(该类没有默认构造函数)
对于引用成员变量,引用类型的变量在定义时就必须给其一个初始值,所以引用成员变量必须使用初始化列表对其进行初始化
对于const成员变量,被const修饰的变量也必须在定义时就给其一个初始值,也必须使用初始化列表进行初始化
对于自定义类型成员(该类没有默认构造函数),若一个类没有默认构造函数,那么我们在实例化该类对象时就需要传参对其进行初始化,否则就会报编译错误 -
尽量使用初始化列表初始化
因为语法理解上初始化列表可以认为是每个成员变量定义初始化的地方,所以无论你是否使用初始化列表,都会走这么一个过程 -
成员变量在类中声明的次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后顺序无关
我后面会举个具体例子说明,但是建议你声明顺序和初始化列表顺序保持⼀致。
实际运用
这个真的很复杂,要梳理清楚很困难
先来看一连串的代码吧:
int a = 10;
int& b = a;// 创建时就初始化const int a = 10;// correct 创建时就初始化
const int b;// error 创建时未初始化class A //该类没有默认构造函数
{
public:A(int val) //注:这个不叫默认构造函数(需要传参调用){_val = val;}
private:int _val;
};class B
{
public:B():_a(2021) //必须使用初始化列表对其进行初始化{}
private:A _a; //自定义类型成员(该类没有默认构造函数)
};
以上代码很好的解释了为什么三种特殊情况下必须要用初始化列表来初始化
接着看以下代码:
// 使用初始化列表
int a = 10// 在构造函数体内初始化(不使用初始化列表)
int a;
a = 10;// 对于自定义类型使用初始化列表
class Time
{
public:Time(int hour = 0){_hour = hour;}
private:int _hour;
};class Test
{
public:// 使用初始化列表Test(int hour):_t(hour)// 调用一次Time类的构造函数{}// 在构造函数体内初始化(不使用初始化列表)Test(int hour)//初始化列表调用一次Time类的构造函数(不使用初始化列表但也会走这个过程){ Time t(hour);// 调用一次Time类的构造函数_t = t;// 调用一次Time类的赋值运算符重载函数}
private:Time _t;
};
通过以上代码,你就明白了为什么说尽量使用初始化列表来初始化,其目的就是为了节省效率
更明确的说对于内置类型,两种方式没差,如上;对于自定义类型,因为不管如何都会走一遍初始化列表,所以直接在初始化列表初始化能最大化地提高效率,如上
再看如下代码:
#include<iostream>
using namespace std;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); // _a1是1,_a2是随机值aa.Print();
}
我们分析下为什么会是这个输出,其实,你只要记着上面说的,成员变量的初始化跟定义顺序有关系,跟初始化列表的出现顺序毫无关系,一切就明白了
首先初始化列表里_a1,_a2都有明确的初始化值,于是缺省失效,然后我们再来看定义顺序,_a2先定义,那么_a2先初始化,初始化为_a1的值,可是这时候_a1并没有被赋值,只能是随机值,而紧接着_a1开始初始化,被赋予我们传过去的a,即为1
其实在这里,我们还提到了缺省值的概念,我们也可以来研究一下,其实我觉得你也可以自己写个程序,跑个调试,看一看缺省值、初始化列表之间的关系
#include<iostream>
using namespace std;class Test
{
public:Test():_a(2){}void GetRet(){cout << _a << endl;}
private:int _a = 1;
};int main()
{Test().GetRet(); // 这个涉及到匿名对象,我们等下会讲return 0;
}
最后输出_a的值为2,其实你调试一下就会发现一些巧妙的地方
_a一开始为1,后面才为2,最后才输出,所以,这其中奥妙就显然了
如果一个成员变量既有缺省值又在初始化列表中定义了,那么就按照初始化列表中的值进行初始化
如果一个成员变量有缺省值,但是没在初始化列表中定义,那么就用它的缺省值初始化
如果一个成员变量既没有缺省值又没在初始化列表中定义,那么就给一个随机值
explicit关键字
先来看两行代码:
int a = 1;
double& b = a; // err,内置类型变量在发生类型转换的时候会生成一个临时的常性变量
其实,内置类型也可以转换成自定义类型,这里就和构造函数扯上关系了
一个类的构造函数,不仅起到初始化成员变量的作用,对于单个参数或第一个参数无缺省值的半缺省构造函数来说,它还具有类型转换的作用。(其实多个参数也行,只是要在C++11标准后,用{ }括起来内置类型即可)
如下,第一个a1构造函数无可厚非,第二个首先1被作为参数构造了一个临时常性变量,再拷贝构造给a2
有什么办法可以禁止构造函数类型转换呢?
有,引入explicit关键字,在构造函数的前面加上它,即可禁止类型转换了
explicit A(int a)
初始化列表的总结
前面讲了那么多,下面用一张表来尝试捋清一下
一言以蔽之:
无论是否显示写初始化列表,每个构造函数都有初始化列表!
无论是否在初始化列表显式初始化,每个成员变量都要走初始化列表初始化!
二、static成员
static成员的概念
声明为static的类成员称为类的静态成员。用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化
static成员的特性
一、静态成员为所以类对象所共享,不属于某个具体的对象
举例来说,请看以下代码:
#include <iostream>
using namespace std;class Test
{
private:static int _n;
};int main()
{cout << sizeof(Test) << endl; // 1return 0;
}
结果显示计算Test类的大小为1,因为静态成员 _n 是存储在静态区的,属于整个类,也属于类的所有对象。所以计算类的大小或是类对象的大小时,静态成员并不计入其总大小之和
二、静态成员变量必须在类外定义,定义时不添加static关键字
class Test
{
private:static int _n;
};
// 静态成员变量的定义初始化
int Test::_n = 0;
这里静态成员变量 _n 虽然是私有,但是我们在类外突破类域直接对其进行了访问。这是一个特例,不受访问限定符的限制,否则就没办法对静态成员变量进行定义和初始化了,本质上是因为_n属于整个类,不单独属于某个类的对象
三、静态成员函数没有隐藏的this指针,不能访问任何非静态成员
class Test
{
public:static void Fun(){cout << _a << endl; //error不能访问非静态成员cout << _n << endl; //correct}
private:int _a; //非静态成员static int _n; //静态成员
};
其实,含有静态成员变量的类,一般含有一个静态成员函数,用于访问静态成员变量
四、静态成员和类的普通成员一样,也有public、private和protected这三种访问级别
所以当静态成员变量设置为private时,尽管我们突破了类域,也不能对其进行访问
你不妨思考一下以下两个问题
1、静态成员函数可以调用非静态成员函数吗?
2、非静态成员函数可以调用静态成员函数吗?
答案是:
问题1:不可以。因为非静态成员函数的第一个形参默认为this指针,而静态成员函数中没有this指针,故静态成员函数不可调用非静态成员函数
问题2:可以。因为静态成员函数和非静态成员函数都在类中,在类中不受访问限定符的限制
static的一个实用场景
请问如何实现⼀个类,计算程序中创建出了多少个类对象?
请看如下代码:
#include<iostream>
using namespace std;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;A a1, a2;A a3(a1);cout << A::GetACount() << endl;cout << a1.GetACount() << endl;// 编译报错:error C2248: “A::_scount”: ⽆法访问 private 成员(在“A”类中声明)//cout << A::_scount << endl;return 0;
}
三、友元
这个我们前面讲解日期类的时候提到过,输出日期类的时候,因为d1 << _cout 与我们的常用习惯不符合,所以我们把<<操作符重载为全局函数,可这样就无法访问到日期类的成员变量,于是,我们采用了友元这一方案,现在我们来详细学习下这一概念
友元本质上提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用
友元函数
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字
如上,我们再来回顾以下代码:
class Date
{// 友元函数的声明friend ostream& operator<<(ostream& out, const Date& d);friend istream& operator>>(istream& in, Date& d);
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}
private:int _year;int _month;int _day;
};
// <<运算符重载
ostream& operator<<(ostream& out, const Date& d)
{out << d._year << "-" << d._month << "-" << d._day<< endl;return out;
}
// >>运算符重载
istream& operator>>(istream& in, Date& d)
{in >> d._year >> d._month >> d._day;return in;
}
友元函数的特性:
1、友元函数可以访问类是私有和保护成员,但不是类的成员函数。
2、友元函数不能用const修饰。
3、友元函数可以在类定义的任何地方声明,不受访问限定符的限制。
4、一个函数可以是多个类的友元函数。
5、友元函数的调用与普通函数的调用原理相同
友元类
和友元函数类似,我们也可以在类中声明一个友元类,友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的非公有成员
请注意:
友元关系是单向的,不具有交换性 -> 比如上面,Date类是Time类的友元,所以可以直接在Date类中访问Time类的私有成员变量;但是不代表Time类是Date类的友元,不能在Time类中访问Date类的私有成员变量
就像爱情一样,“执子之手,与子偕老”实为可遇不可求
相比之下,“我本将心向明月,奈何明月照沟渠”才是人间常态
友元关系不能传递 -> 例如A是B的友元,B是C的友元,不代表A就是C的友元了
友元关系不能继承 -> 这里在讲到继承后再给大家详细介绍
四、内部类
概念
如果一个类定义在另一个类的内部,这个定义在内部的类就称为内部类
请注意:
1、此时的内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象区调用内部类。
2、外部类对内部类没有任何优越的访问权限。
3、内部类就是外部类的友元类,即内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
特性
一、 内部类受外部类的类域限制
假如我们想创建一个内部类类型的变量,需要用作用域限定符
二、外部类的大小不包括内部类
外部类A的大小并没有包括内部类B,所以也可以体现内部类的空间也是独立的
五、匿名对象
有时候我们可能只需要调用一次某个类的成员函数,为此如果特意去创建一个对象的话就太麻烦了,这时候就可以考虑运用匿名对象
匿名对象的特点在于,它的生命周期只在这一行,一旦程序走到了下一行,就会自动调用析构函数销毁,且在创建的时候是不用取名字的
所以对于各种一次性的对象创建,我们都可以使用匿名对象
六、再次理解封装和面向对象
C++是基于面向对象的程序,面向对象有三大特性:封装、继承、多态
C++通过类,将一个对象的属性与行为结合在一起,使其更符合人们对于一件事物的认知,将属于该对象的所有东西打包在一起。通过访问限定符的将其部分功能开放出来与其他对象进行交互,而对于对象内部的一些实现细节,外部用户不需要知道,知道了有些情况下也没用,反而增加了使用或者维护的难度,让整个事情复杂化。
所以说,封装的本质是一种更高效的管理
举个例子,我们来看一下火车站:
售票系统:负责售票—用户凭票进入,对号入座。
工作人员:售票、咨询、安检、保全、卫生等。
火车:带用户到目的地
乘客不需要知道火车的构造、票务系统是如何运作的,只要能正常方便的应用即可,知道了反而还增加管理成本,而工作人员的协调配合,才能使得让大家坐车井然有序的进行
从这可以看出,面向对象实际上是在模拟世界,这个思路打通了计算机世界和现实世界的桥梁
总结
怎么样!是不是感概终于撑过来了,现在终于可以缓口气了,接下来的内存管理会相对较为简单,且能让你在本篇的一些疑惑得到更好的解答,所以休整一下,我们马上继续出发!