目录
前言
这篇博客主要讲了C++的初始化列表,explicit关键字,static成员,友元及内部类;新人创作者,希望各位大佬指出不足之处,本篇博客的代码已经上传到gitee了,欢迎有需要的老铁们自取(本篇博客代码)
1.初始化列表
谈到对象的初始化,我们前面的博客说到过类的构造函数可以对这个类的对象做初始化操作,那么是否还有别的初始化方式呢?其实是有的,初始化列表就是另外一种对对象进行初始化操作的方式,以日期类的初始化为例,初始化列表的使用方法如下:
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
,_month(month)
,_day(day)
{}
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
1.2 初始化列表就是对象的成员变量定义的地方
初始化列表的价值到底是什么呢?为什么要有初始化列表呢?先来记一个结论,初始化列表就是对象的成员变量定义的地方,对象定义时,自动调用初始化列表。
我们规定一下:在构造函数进行初始化操作叫做在函数体内部进行初始化,在初始化列表初始化时叫做在对象定义的时候进行初始化。
你可能会问?为什么要专门规定一个对象的成员变量初始化的地方呢?普通的变量不管是在函数体内部初始化还是在初始化列表初始化,都没有什么影响,而有些特殊的成员变量,比如引用成员变量,const成员变量,没有默认构造函数的自定义类型成员变量是不能在函数体内部初始化的。
可以看到我们上面的const成员变量和引用成员变量在函数体内部初始化时就报错了,但是在初始化列表初始化就没有报错;没有默认构造函数的自定义类型作为类的成员变量,如果不在初始化列表初始化,编译器也会报错:
记住有三类成员变量不能在函数体内部进行初始化:
- 引用成员变量
- const成员变量
- 自定义类型成员(该类没有默认构造函数)
所以我们去初始化我们对象的成员变量,建议都在初始化列表舒适化,防止报错。
1.3 不显式写出初始化列表,编译器会自动生成
如果我们不显式写出初始化列表,编译器会自动生成,对于内置类型,我们会对他进行初始化,对于自定义类型,会去调用它的默认构造函数。
比如上图的MyQueue类中有内置类型Stack,我们在MyQueue类中没有显式写出初始化列表,但是编译器会自动生成,对于内置成员变量,我们自动初始化成0了,对于自定义成员变量Stack,我们调用了它的默认构造函数。
1.4 初始化列表中的初始化顺序
成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。
class A
{
public:
A(int a)
:_a1(a)
, _a2(_a1)
{}
void Print() {
cout << _a1 << " " << _a2 << endl;
}
private:
int _a2;
int _a1;
};
int main()
{
A aa(1);
aa.Print();
}
我们来运行一下上面的代码,看看会打印出什么?
为什么会打印出这个结果呢?为什么不是1 1呢?而是1 -858993460一串随机值呢?因为初试化列表的顺序是成员变量声明的顺序,而不是初始化列表中的先后顺序,这里是先初始化_a2,初始化_a2是用_a1去初始化,而此时_a1还没有被赋值,所以是随机值,与是随机值就赋值给了_a2,而后面再用变量a去初始化_a1,a值为1,被赋值给了_a1。
2.explicit关键字
先来运行一下下面的代码:
class Date
{
public:
Date(int year)
:_year(year)
{}
void Print()
{
cout << _year << endl;
}
private:
int _year;
};
int main()
{
Date d1(2022);
Date d2 = 2022;
d1.Print();
d2.Print();
}
运行结果为:
你会不会感觉到奇怪,为什么可以把2022直接赋值给自定义类型d2,其实这里发生了隐式类型的转换,编译器是先拿2022去构造一个Date类型的对象,再把这个Date类型的对象拷贝构造给d2;同时在这里vs2019编译器(稍微老一点的编译器不会)也会进行优化,将拷贝构造和普通的构造合二为一,变成调用一次构造函数:我们可以来看看:
接下来我们言归正传,说一下我们要讲的explicit关键字,用explicit修饰构造函数,将会禁止单参构造函数的隐式转换。
3.static成员
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态的成员变量一定要在类外进行初始化
3.1 静态成员变量
下面我们来看一下用到静态成员的一个实例:
// 统计A类型对象调用了多少次构造和拷贝构造
class A
{
public:
A()
{
++_count1;
}
A(const A& aa)
{
++_count2;
}
// 静态成员变量属于整个类,属于类的所有对象
static int _count1;
static int _count2; // 声明
};
// 定义,静态的成员变量一定要在类外进行初始化
int A::_count1 = 0;
int A::_count2 = 0;
A Func(A a)
{
A copy(a);
return copy;
}
int main()
{
A a1;
A a2 = Func(a1);
// 静态成员变量属于整个类,属于类的所有对象,所以可以用对象去访问,也可以用类的域限定符访问
cout << &a1._count1 << endl;
cout << &a2._count1 << endl;
cout << &a1._count2 << endl;
cout << &a2._count2 << endl;
cout << &A::_count1 << endl;
cout << &A::_count2 << endl;
}
可以看到我们上面的这段代码,使用了静态成员变量,运行一下,看看结果,他们打印出的地址都是一样的,我们发现不管是用类去访问对象,还是用类的域限定符访问静态成员变量,他们访问的都是同一个变量(地址都相同),所以说静态成员变量属于整个类,属于类的所有对象。
我们再来看看使用了静态成员的类的大小是如何的:
同时我们注意到我们的静态成员变量一定要在类外再初始化一下!这是和别的普通成员变量不同的一点!
3.2 静态成员函数
如果我们的静态成员变量是私有的,那又该怎么办呢?我们就不能通过刚刚哪种方法去得到_count1和_count2的值了,此时我们可以定义一个公有的函数去获取私有的成员变量的值:
int GetCount1()
{
return _count1;
}
int GetCount2()
{
return _count2;
}
其实上面的还不是最优的方案,因为上面的我们只能通过对象去调用这个函数,你不是很方便,如何做出改进呢?就是将这个成员函数变成静态成员函数,他也可以像上面访问静态成员变量一样去使用类的域限定符访问静态成员函数:
static int GetCount1()
{
return _count1;
}
static int GetCount2()
{
return _count2;
}
静态成员函数没有隐藏的this指针,不能访问任何非静态成员
4.友元
有时候我们需要访问类里面的私有成员和保护成员,这个时候就要用到友元,友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
4.1 友元函数
还是以日期类为例子,我们实现一个日期类的"<<"来看看吧,"<<"是流插入运算符,我们将他写成一个可以输出日期类的年月日的形式,
//注: cout只是我们的一个调制好的ostream类的一个全局对象.内置类型的打印已经写好了,所以我们只需要把类里需要打印的通过内置类型打印好即可.
class Date
{
public:
Date(int year = 2022, int month = 5, int day = 28)
{
_year = year;
_month = month;
_day = day;
}
ostream& operator<<(ostream& out)
{
out << _year << "_" << _month << "_" << _day << endl;
return out;
}
private:
int _year;
int _month;
int _day;
};
如果以上述的形式重载<<运算符,那么我们在使用的时候只能像下面一样使用:
int main()
{
Date a1;
a1 << cout;//日期类的<<的使用
return 0;
}
因为像上面的流插入运算符的重载写法,它的左操作数是隐含的this指针,右操作数是ostream类的对象,所以只能采用a1 << cout的形式去使用流插入运算符,这样用起来就非常别扭,因此我们在这里要用到友元函数的形式:
class Date
{
public:
friend ostream& operator<<(ostream& out, Date& d);
Date(int year = 2022, int month = 5, int day = 28)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& out, Date& d)//这样写是因为默认左操作数为第一个参数,右操作数为第二个参数
{
out << d._year << "_" << d._month << "_" << d._day << endl;
return out;
}
int main()
{
Date a1;
cout << a1;//正确的使用
return 0;
}
那么采用我们如上的写法就可以不那么别扭地去使用<<的运算符重载,在这里我们将运算符重载函数写成Date类的友元函数,使得运算符重载函数可以去访问日期类的私有成员变量。
注意:
- 友元函数可访问类的私有和保护成员,但不是类的成员函数
- 友元函数不能用const修饰(因为const修饰的是this指针,而友元函数没有this指针)
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
- 一个函数可以是多个类的友元函数
- 友元函数的调用与普通函数的调用和原理相同
4.2 友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的私有成员。
class Date;
class Time
{
friend class Date; // 声明日期类为时间类的友元类,则在日期类中就直接访问Time类中的私有成员变量
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 Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{
}
void SetTimeOfDate(int hour, int minute, int second)
{
// 直接访问时间类私有的成员变量
_t._hour = hour;
_t._minute = minute;
_t._second = second;
}
private:
int _year;
int _month;
int _day;
Time _t;
};
如上面的时间类中,我们定义了日期类是事件类的友元,所以日期类可以访问事件类的私有成员变量。
注意:
- 友元关系是单向的,不具有交换性。比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
- 友元关系不能传递。如果B是A的友元,C是B的友元,则不能说明C时A的友元。
5.内部类
如果一个类定义在另一个类的内部,这个内部类就叫做内部类。
class A
{
private:
int h;
public:
class B // B天生就是A的友元
{
public:
void foo(const A& a)
{
cout << a.h << endl;//OK
}
private:
int _b;
};
};
比如上面代码中的B类就是A类的内部类,内部类的特性是,内部类B天生就是A的友元,也就是说B可以访问A的私有成员。并且内部类B不占用A类对象的空间:
内部类的特性:
- 内部类天生是外部类的友元。比如上面的B类就是A类的友元,并且由于友元的特性,A类不能访问B类的内置成员变量。
- 内部类可以定义在外部类的public、protected、private都是可以的。比如我们的B类可以定义在A中的public、protected、private中。
- sizeof(外部类)=外部类,和内部类没有任何关系。
我们可以这样理解,内部类其实就是一层友元关系,和友元唯一不同的地方就是如果我们要去创建一个B对象的话,是要受访问限定符的限制:
6.连续的拷贝构造会优化
在一个表达式中,连续步骤的构造+拷贝构造,或者拷贝构造+拷贝构造,单打的编译器就可能会优化,将这两次构造合二为一!
我们来看一道题目吧:
先说答案,调用了7次,为什么呢?这7次是哪七次呢?
首先是第一层f(x),传参给Widget u的时候,调用了一次拷贝构造,然后widegt v(u),调用了第二次,widget w = v调用了第三次;返回w用了第四次,而f(x)再次作为参数传给widget u,这是第五次,但第五次和第四次是属于同一步骤,所以编译器会合二为一,接着加上widget v(u)的第六次,widget w = v的第七次,return w的第八次和widget y = f(f(x))的第九次,但第八次和第九次是属于同一步骤,所以编译器会合二为一,因此只有七次。
这种情况并不是C++的内部标准,它只是比较高级的编译器内部的处理,对于比较老的编译器是没有的(大家可以用VC6.0试试应该是没有的)。