关键字:自加自减运算符重载,初始化列表,explicit关键字,static成员,内部类。
自加自减运算符重载
与其他的运算符不同,自加运算符与自减运算符是所有运算符中的一对奇葩——它们都只对一个对象进行操作,但是却又有两种不同的操作模式——前置与后置。
这里我们以自加运算符为例。
我们假设d1是date类的一个对象,那么++d1与d1++应该是不一样的,它们虽然都完成了
d1 = d1 + 1
//=与+运算符都已经重载
这样的操作,但是,前者的返回值应该是对象d1被修改后的值,而后者的返回值应该是对象d1被修改前的值。
但是按照我们目前所了解的关于运算符重载的知识,++d1与d1++应该都等价于
d1.operator++();
但这显然是不合理的。
所以,为了解决这个问题,C++中允许在重载自加或者自减运算符时,将使用一个无用的int类型作为形参来表示后置自加或者自减运算。
也可以这样来表达:
编译器处理前置++或--时,会调用参数个数正常的重载函数;处理后置++或--时,编译器会调用使用int类型作为占位符的重载函数。
这并没有什么原因,这是C++的语法规定。?
class date {
private:
int _year;
int _month;
int _day;
public:
date(int year = 1999, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
void print() {
cout << _year << '/' << _month << '/' << _day << endl;
}
date& operator++();
date operator++(int);
};
date& date::operator++() {
++_day;
if (_day > 30) {
_day = 1;
++_month;
if (_month > 12) {
_month = 1;
++_year;
}
}
return *this;
}
date date::operator++(int) {
date tmp(*this);
++(*this);
return tmp;
}
int main() {
date d1;
date d2(2001, 4, 1);
d1.print();
d2.print();
cout << "++d1" << endl;
d2 = ++d1;
d1.print();
d2.print();
cout << "d1++" << endl;
d2 = d1++;
d1.print();
d2.print();
return 0;
}
与赋值运算符的重载不同,自加自减运算符是可以重载为全局函数的。但前提是类中被操作的成员变量为public类型。
实际上,C++中只能在类中实现运算符重载的运算符只有四个:
赋值运算符????????=
函数调用运算符????????()
下标运算符? ? ? ? []
指针访问类成员的运算符? ? ? ? ->
前置与后置的效率分析
通过分析自加运算符的两种方式的重载代码,结合我们之前所讲过的关于传引用与传值两者的效率差别,我们很容易得出前置加加的效率高于后置加加的结论。
不过实践出真知,我们可以通过以下代码来进行验证:
class date {
private:
int _year;
int _month;
int _day;
public:
date(int year = 1999, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
void print() {
cout << _year << '/' << _month << '/' << _day << endl;
}
date& operator++();
date operator++(int);
};
date& date::operator++() {
++_day;
if (_day > 30) {
_day = 1;
++_month;
if (_month > 12) {
_month = 1;
++_year;
}
}
return *this;
}
date date::operator++(int) {
date tmp(*this);
++(*this);
return tmp;
}
int main() {
date d1;
date d2;
size_t begin1 = clock();
for (size_t i = 0; i < 1000000; ++i) {
++d1;
}
size_t end1 = clock();
size_t begin2 = clock();
for (size_t i = 0; i < 1000000; ++i) {
d2++;
}
size_t end2 = clock();
cout << "++d1: " << end1 - begin1 << endl;
cout << "d2++: " << end2 - begin2 << endl;
return 0;
}
运行结果:
果然与我们所预测的一样,前置++的效率明显高于后置++。
这主要是由于前置++是以引用作为返回值,而后置++则不是,所以后置++的值在返回过程中会比前置++多进行了一次拷贝。
构造函数再探
初始化列表引入
众所周知,C++虽然有很多问题被人诟病,但是其效率却是实打实的能打。
class Time {
private:
int _hour;
int _min;
public:
Time(int hour = 1, int min = 1);
~Time() {
cout << "endtime" << endl;
}
};
Time::Time(int hour, int min) {
_hour = hour;
_min = min;
cout << "time" << endl;
}
class date {
private:
int _year;
int _month;
int _day;
Time _t;
public:
date(int year = 1999, int month = 1, int day = 1)
{
//_t(24, 1);
}
void print() {
cout << _year << '/' << _month << '/' << _day << endl;
}
date& operator++();
date operator++(int);
};
?通过对以上代码中date类的构造函数进行反汇编,我们得到以下结果:
date(int year = 1999, int month = 1, int day = 1)
000A2080 push ebp
000A2081 mov ebp,esp
000A2083 sub esp,0CCh
000A2089 push ebx
000A208A push esi
000A208B push edi
000A208C push ecx
000A208D lea edi,[ebp-0Ch]
000A2090 mov ecx,3
000A2095 mov eax,0CCCCCCCCh
000A209A rep stos dword ptr es:[edi]
000A209C pop ecx
000A209D mov dword ptr [this],ecx
000A20A0 mov ecx,offset _A50DC28E_源@cpp (0AF02Ah)
000A20A5 call @__CheckForDebuggerJustMyCode@4 (0A1398h)
000A20AA push 1
000A20AC push 1
000A20AE mov ecx,dword ptr [this]
000A20B1 add ecx,0Ch
000A20B4 call Time::Time (0A1320h)
{
//_t(24, 1);
}
000A20B9 mov eax,dword ptr [this]
000A20BC pop edi
000A20BD pop esi
000A20BE pop ebx
000A20BF add esp,0CCh
000A20C5 cmp ebp,esp
000A20C7 call __RTC_CheckEsp (0A129Eh)
000A20CC mov esp,ebp
000A20CE pop ebp
000A20CF ret 0Ch
可以看到,在进入函数体之前,函数便已经调用了Time类的构造函数,这也是为什么如果我们想在构造函数的函数体中使用成员变量 _t 的默认构造函数却会报错的原因——一个类对象的构造函数在其生命周期内只调用一次。
再向深处去思考,我们会发现这样的情况是非常浪费资源的。因为类类型的成员变量已经被创建了,我们无法再通过该类构造函数为其初始化,推求其次,只能通过给其相应变量赋值或者通过拷贝构造函数给其赋值等方法。但不论哪种方法,都是对类类型成员变量进行了多余的操作。
并且,如果类类型成员变量所在的类并没有默认构造函数的话,我们当前类的构造函数的调用也可能会出现一些问题......
对于以上种种问题,C++引入了初始化列表。也即我们在自加自减运算符重载中使用的:
date(int year = 1999, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
初始化只能进行一次,成员变量的初始化在其构造函数传参时便已经完成,而构造函数体内进行的只能称之为赋初值。
初始化列表
初始化列表:
以一个冒号开始,以逗号分隔数据成员列表,每个成员变量后边跟一个放在括号中的初始值或表达式。
注意事项:
1.每个成员变量在初始化列表中只能出现一次,也即,初始化只能初始化一次。
2.类中包含的以下成员必须在初始化列表中进行初始化:
? ? ? ? 引用成员变量
? ? ? ? const成员变量
? ? ? ? 无默认构造函数的自定义类型成员?
3.尽量使用初始化列表进行初始化,因为所有类类型的成员都会在初始化阶段初始化,即使该成员没有出现在构造函数的初始化列表中。
4.成员变量在类中的声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后顺序无关。
class date {
private:
int _year;
int _month;
int _day;
//Time _t;
public:
date(int year = 1999, int month = 1, int day = 1)
: _day(day)
, _month(month)
, _year(year)
{}
void print() {
cout << _year << '/' << _month << '/' << _day << endl;
}
date& operator++();
date operator++(int);
};
?
?图示是对于date类构造函数的反汇编,可见,虽然我们在初始化列表中将成员变量_day的初始化写在了成员变量_year的前边,但是编译器在执行时仍然是按照成员变量的声明顺序,先对_year进行的初始化。
我们也可以通过对构造函数进行这样的改进得出更直观的结果。
date(int a = 3)
: _day(a++)
, _month(a++)
, _year(a++)
{}
void print() {
cout << _year << '/' << _month << '/' << _day << endl;
}
运行结果:
explicit关键字
构造函数不仅可以构造与初始化对象,对于单个参数的构造函数,它还具有类型转换的作用。
比如下方代码中,我们令date类只拥有一个成员变量,而后,将一个int类型的变量给date类的对象赋值,这在我们的了解中显然是不可能的,但实际并非如此:
class date {
private:
int _year;
//int _month;
//int _day;
//Time _t;
public:
date(int a = 3)
: _year(a)
{
cout << "create" << endl;
}
void print() {
cout << _year << endl;
}
};
int main() {
date d1(2001);
d1 = 2003;
d1.print();
return 0;
}
运行结果:
从运行结果以及反汇编可以看出,在d1 = 2003中,编译器调用了类date的构造函数,使用2003作为参数构造了一个无名对象,然后使用这个无名对象给d1进行了赋值。赋值完成后,这个无名对象就被销毁了。
就像我们在C语言中经常碰到的隐式类型转换一样,这里也可以理解为一种隐式的自动转换。
不过这种转换很多时候并不是我们想看见的,它会导致代码的可读性大大降低,所以C++提供了explicit关键字用以阻止这种隐式转换。
class date {
private:
int _year;
//int _month;
//int _day;
//Time _t;
public:
explicit date(int a = 3)
: _year(a)
{
cout << "create" << endl;
}
void print() {
cout << _year << endl;
}
};
int main() {
date d1(2001);
d1 = 2003;
d1.print();
return 0;
}
?需要注意的是:
1.explicit关键字只是用来修饰构造函数的,而且它只会出现在类中的构造函数的声明之前,如果实现与声明分离时,explicit关键字出现在实现之前会导致报错。
2.explicit关键字是用来阻止由构造函数定义的隐式转换的。
使用建议:
如果真切没有使用隐式转换的需求,且构造函数是单参可调用的,那么最好使用explicit关键字修饰构造函数。如果构造函数时多参可调用的,由于多参构造函数是没有隐式转换的,所以没必要声明explicit。?
static成员
概念
声明为static的类成员称为类的静态成员。
用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。
静态成员变量一定要在类外进行初始化。
特性
1.类的静态成员被该类所有的类对象共享,并不属于哪个具体的实例。
2.静态成员变量在类中只是声明,必须在类外定义,定义时不用添加static关键字。
3.类静态成员可以使用 [类名] :: [静态成员] 或者 [对象] . [静态成员] 的方法来访问。
4.静态成员函数没有隐藏的this指针,不能访问任何非静态成员。
5.静态成员与类的普通成员一样,也有public、protected、private 3种访问级别,也可以具有返回值。
静态成员变量?
假设我们需要一种方法,知道当前程序中有多少个date类的对象,我们就可以使用静态成员变量来完成这个功能。
class date {
private:
int _year;
int _month;
int _day;
public:
static int _count;
date(int year = 1999, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{
++date::_count;
}
~date() {
--date::_count;
}
void print() {
cout << _year << "/" << _month << "/" << _day << endl;
}
};
int date::_count = 0;
int main() {
date d1;
date d2;
cout << date::_count << endl;
{
date d3;
cout << date::_count << endl;
}
cout << date::_count << endl;
return 0;
}
运行结果:
static成员变量的内存不是在类声明时分配的,也不是在创建对象时(这个当然了,没看到上面代码中定义在全局中,那时候对象还没创建呢),而是在类外初始化的时候分配的。所以,没有在类外初始化的static成员变量不能使用。
static成员变量并不包含在对象当中,不占用对象的内存,而是在所有对象之外开辟内存,即是不创建对象也能访问。
static成员变量与static变量类似,存储在内存的全局数据区。
所以static成员变量并不影响类的大小。
这里就是对之前的类的大小计算的补充。
如上边的特性所说,静态成员变量会受到访问权限的限定,如果我们将静态变量使用private修饰,那么我们上方代码中的做法明显是不可取的了。
所以,我们可以使用一个函数来专门输出静态成员变量_count的值:
class date {
private:
int _year;
int _month;
int _day;
static int _count;
public:
date(int year = 1999, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{
++date::_count;
}
~date() {
--date::_count;
}
void print() {
cout << _year << "/" << _month << "/" << _day << endl;
}
int Count() {
return _count;
}
};
int date::_count = 0;
int main() {
date d1;
date d2;
cout << d1.Count() << endl;
{
date d3;
cout << d1.Count() << endl;
}
cout << d1.Count() << endl;
return 0;
}
小问题:为什么在类内静态成员变量可以直接访问?
????????每一个类都定义了一个新的作用域,类的所有成员都在这个作用域之中。类的静态成员变量声明于类中,它本身就是属于这个作用域的,自然就不需要再进行域解析运算了。
但是,问题来了,如果在尚没有对象创建的时候,或者在我们当前所创建的对象都无法触及的作用域中,我们该如何取得_count的值呢?
这就需要用到静态成员函数了。
静态成员函数
class date {
private:
int _year;
int _month;
int _day;
static int _count;
public:
date(int year = 1999, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{
++date::_count;
}
~date() {
--date::_count;
}
void print() {
cout << _year << "/" << _month << "/" << _day << endl;
}
static int Count() {
return _count;
}
};
int date::_count = 0;
int main() {
cout << date::Count() << endl;
date d1;
date d2;
cout << d1.Count() << endl;
{
date d3;
cout << d1.Count() << endl;
}
cout << d1.Count() << endl;
return 0;
}
静态成员函数于非静态成员函数最大的差别在于,它没有this指针。所以它也就无法对类的非静态成员进行默认访问。
我们再上一篇经过,const成员函数的const实质上修饰的是成员函数中的this指针,所以,静态成员函数不能被设置为const。
由以上特性,我们也很容易得出:
静态成员函数不可以调用成员函数。
成员函数可以调用静态成员函数。
PS.当然这里只是指默认情况下,如果静态成员函数传入了类的对象,它还是可以通过类的对象来访问成员函数的。
成员变量的缺省?
C++允许类的非静态成员变量在声明时给其写上一个缺省值。
class date {
private:
int _year = 1999;
int _month = 1;
int _day = 1;
static int _count;
public:
date() {
}
void print() {
cout << _year << "/" << _month << "/" << _day << endl;
}
};
int main() {
date d1;
d1.print();
return 0;
}
运行结果:
友元
友元是C++提供的一种突破封装的方式。
存在即为合理,友元为了优化某些问题而存在,有时会提供便利,但是有时也会导致一些额外的困扰。
友元会提高耦合度,破坏了封装性,所以友元不宜多用。
友元分为友元函数与友元类两大类。
友元函数
当我们在完成了一个类的定义后,一般情况下,所有的成员函数都是被private所修饰的。但是有些时候,我们可能会有一些需求在类外对这些私有的成员变量进行访问,此时,就只能去提供一些公有的方法。但是使用方法去访问成员变量终究是没有直接对成员变量进行访问的效率高。
不用说什么将我们要访问的变量修改成公有的,有些时候我们的需求可能是对所有的变量进行直接访问,并且次数很多,就比如以下这个相对来说较为实用的例子:
流提取与流插入运算符的重载
分析: 如我们所知,我们无法通过cout << [对象] 的方法来输出对象。所以我们需要去尝试重载operator<<,但是,在重载过程中我们会发现,operator<<无法重载成为成员函数。
因为在平常使用中往往是cout占据 << 的第一个参数位置,但是重载成为成员函数的过程中,隐含的this指针会将cout的第一个参数位置抢占了。非静态成员函数隐含的this指针默认成为了第一个参数,也即左操作数。
这明显与我们实际使用中的格式需求是相悖的。只有cout作为流插入运算符的第一个形参对象,才可以完成正常的打印输出。
所以,我们要将operator<<重载为全局函数。但是这样会导致在类外无法访问成员。
这时就需要使用友元函数来解决了。
?错误的实例——成员函数:
class date {
private:
int _year;
int _month;
int _day;
public:
date(int year = 1999, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
void operator<<(ostream& _cout) {
_cout << _year << "/" << _month << "/" << _day;
}
};
int main() {
date d1;
cout << d1;
return 0;
}
可以看到,由于隐式参数this的传入,C++编译器并没有找到对应的可调用函数,由此报错。
但有意思的是,这样的重载函数实际上也是可以调用的:
int main() {
date d1;
d1 << cout;
return 0;
}
输出结果:
?这样虽然输出了正确的结果,但却违背了我们重载函数的规则——含义不能改变。
依照C++重载的约定,流插入运算符重载时,第一个参数必须是ostream&,第二个参数才应该是我们要打印的内容。
此时就需要使用友元函数:
友元函数可以直接访问类的私有成员,它是定义在类外的普通函数。友元函数并不属于任何类,但需要在类中声明,声明时需要加friend关键字。
class date {
private:
int _year;
int _month;
int _day;
public:
date(int year = 1999, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
friend ostream& operator<<(ostream& _cout, const date& d);
};
ostream& operator<<(ostream& _cout,const date& d1) {
_cout << d1._year << "/" << d1._month << "/" << d1._day;
return _cout;
}
int main() {
date d1;
cout << d1 << endl;
cout << "date:" << d1 << endl;
cout << d1 << "hello" << endl;
return 0;
}
友元函数可以访问类的私有成员,但它并不是类的成员函数。
友元函数不能用const修饰,因为它并没有传入this指针。
友元函数可以在类定义的任何地方声明,不受到访问限定符的限制。
一个函数可以是多个类的友元函数。
友元函数的调用与普通函数的调用与原理相同。
?友元类
在有些特殊情况下,一个类可能要频繁访问另一个类的私有成员,这时候简单的友元函数已经不足以满足它了,我们可以将一个类直接声明为另一个类的友元类。
class date;
class Time {
friend class date;
private:
int _hour;
int _minute;
int _second;
public:
Time(int hour = 1, int minute = 1, int second = 0);
void print() const{
cout << " " << _hour << ":" << _minute << ":" << _second;
}
};
Time::Time(int hour, int minute, int second)
:_hour(hour)
,_minute(minute)
,_second(second)
{}
class date {
private:
int _year;
int _month;
int _day;
Time _t;
public:
date(int year = 1999, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
,_t()
{}
void setTime(int hour, int minute, int second) {
_t._hour = hour;
_t._minute = minute;
_t._second = second;
}
friend ostream& operator<<(ostream& _cout, const date& d);
};
ostream& operator<<(ostream& _cout,const date& d1) {
_cout << d1._year << "/" << d1._month << "/" << d1._day;
//_cout << " " << d1._t._hour << ":"
// << d1._t._minute << ":" << d1._t.second;
d1._t.print();
return _cout;
}
int main() {
date d1;
d1.setTime(12, 0, 0);
cout << d1;
return 0;
}
?可见,我们在date类的setTime函数中直接访问了本属于Time类的私有成员。但是如果我们在date类的友元函数operator<<中访问Time类的私有成员的话,就会报错。
这是由于友元关系是不可传递的。
关于友元关系我们共有以下几点补充:
1.友元关系是单向的,不具有交换性。
? ? ? ? 你是我的朋友,但我不一定拿你当朋友。
????????Time声明了date是它的朋友,date可以取走使用Time的一些隐私物品,但date并没有将Time声明为朋友,所以Time在date那里并没有朋友才会有的权限。
2.友元关系不可传递。
? ? ? ? 我朋友的朋友,不是我的朋友。
? ? ? ? A声明B为朋友,B声明C为朋友,B可以访问A的隐私成员,C可以访问B的隐私成员,但C却不可以访问A的隐私成员。
3.友元关系不能继承。
内部类
概念
如果一个类定义在另一个类的内部,这个定义在内部的类就被称为内部类,易得,另一个类被称为外部类。
内部类是一个独立的类,它不属于外部类,也不可以通过外部类的对象去调用内部类。外部类对内部类并没有任何的访问权限。
内部类是外部类的友元,内部类可以通过外部类的对象参数访问外部类中的所有成员。
特性
1.内部类是外部类的友元,所以内部类可以定义在外部类的任何位置,不受访问限定符的影响。
2.内部类可以直接访问外部类的static成员、枚举成员,并不需要外部类的对象/类名。
3.外部类的大小与内部类没有任何关系。
class A {
private:
static int count;
int k = 12;
public:
class B {
public:
void fun() {
cout << count << endl;
}
};
};
int A::count = 10;
int main() {
A::B b;
b.fun();
return 0;
}
?
?
|