对于普通函数,无法直接访问类的私有成员,还有时候需要普通函数来访问类的私有成员以简化代码,这时就需要友元函数登场了。
一个相等性函数
下面代码实现一个 DayOfYear 的类,它记录一个日期。并在其中加入一个相等性函数(判断两个日期是否相等)。
#include <iostream>
using namespace std;
class DayOfYear
{
public:
void input();
void output();
void set(int newMonth, int newDay);
// 前条件:newMonth 和 newDay 构成一个可能的日期
// 后条件:日期根据传递的实参来重置
int getMonth();
// 返回月份,1代表1月,2代表2月,依次类推
int getDay();
// 返回天数
private:
void checkDate();
int month;
int day;
};
// 相等性函数
bool equal(DayOfYear date1, DayOfYear date2);
// 前条件:date1 和 date2 已被赋值
// 如果 date1 和 date2 表示相同的日期,就返回true,否则返回false
int main()
{
DayOfYear today, bachBirthday;
cout<<"Enter today's date:\n";
today.input();
cout<<"Today's date is ";
today.output();
bachBirthday.set(3, 21);
cout<< "J. S. Bach's birthday is ";
bachBirthday.output();
if ( equal(today, bachBirthday) )
cout<<"Happy Birthday Johann Sebastian!\n";
else
cout<<"Happy Unbirthday Johann Sebastian!\n";
return 0;
}
bool equal(DayOfYear date1, DayOfYear date2)
{
return ( date1.getMonth() == date2.getMonth() &&
date1.getDay() == date2.getDay() );
}
// 使用 iostream
void DayOfYear::input()
{
cout<<"Enter the month as a number: ";
cin>>month;
cout<<"Enter the day of the month: ";
cin>>day;
checkDate();
}
void DayOfYear::output()
{
cout<< "month = " <<month
<< ", day = " << day << endl;
}
void DayOfYear::set(int newMonth, int newDay)
{
month = newMonth;
day = newDay;
checkDate();
}
void DayOfYear::checkDate()
{
if( (month < 1) || (month > 12) || (day < 1) || (day > 31) )
{
cout<<"Illegal date. Aborting program.\n";
exit(1); // 用于退出程序
}
}
int DayOfYear::getMonth()
{
return month;
}
int DayOfYear::getDay()
{
return day;
}
相等性函数 equal
的定义很直观。如两个日期代表同一个月,以及一个月中的同一天,两个日期就相等。 equal
的定义使用取值函数 getMonth
和 getDay
来比较两个对象代表的月份和天数。
友元函数
上面的相等性函数 equal
使用取值函数 get
访问私有成员变量,如果能直接访问成员变量,代码会变得更简洁高效。
equal
更简单有效的定义如下:
bool equal(DayOfYear date1, DayOfYear date2)
{
return ( date1.month == date2.month && date1.day === date2.day );
}
这样做虽然能使代码更简洁,但其是非法的。因为成员变量 month
和 day
是 DayOfYear
类的私有成员。私有成员通常不能在一个函数的主体中引用,除非函数是一个成员函数。
现在的问题是,equal
并非 DayOfYear
的成员函数。但有一个办法可以为非成员函数赋予和成员函数一样的访问权限。让 equal
函数成为 DayOfYear
的友元,上述 equal
定义就完全合法了。
类的友元函数不是这个类的成员函数,而是一个 ”友好“ 的函数,它能像成员函数那样访问类的私有成员。友元函数可以直接读取成员变量的值,甚至能直接更改成员变量的值。
在类定义中声明友元函数只需在函数声明前添加关键字 friend
。友元函数不是成员函数,其本质上仍然是普通函数,只是被特别授予了访问类的数据成员的权限。友元的定义和调用方式与普通函数无异。
下面在之前的代码中将相等性函数 equal
更新为友元函数。
#include <iostream>
using namespace std;
class DayOfYear
{
public:
// 相等性函数
friend bool equal(DayOfYear date1, DayOfYear date2);
// 前条件:date1 和 date2 已被赋值
// 如果 date1 和 date2 表示相同的日期,就返回true,否则返回false
void input();
void output();
void set(int newMonth, int newDay);
// 前条件:newMonth 和 newDay 构成一个可能的日期
// 后条件:日期根据传递的实参来重置
int getMonth();
// 返回月份,1代表1月,2代表2月,依次类推
int getDay();
// 返回天数
private:
void checkDate();
int month;
int day;
};
int main()
{
DayOfYear today, bachBirthday;
cout<<"Enter today's date:\n";
today.input();
cout<<"Today's date is ";
today.output();
bachBirthday.set(3, 21);
cout<< "J. S. Bach's birthday is ";
bachBirthday.output();
if ( equal(today, bachBirthday) )
cout<<"Happy Birthday Johann Sebastian!\n";
else
cout<<"Happy Unbirthday Johann Sebastian!\n";
return 0;
}
bool equal(DayOfYear date1, DayOfYear date2)
{
return ( date1.month == date2.month &&
date1.day == date2.day );
}
// 使用 iostream
void DayOfYear::input()
{
cout<<"Enter the month as a number: ";
cin>>month;
cout<<"Enter the day of the month: ";
cin>>day;
checkDate();
}
void DayOfYear::output()
{
cout<< "month = " <<month
<< ", day = " << day << endl;
}
void DayOfYear::set(int newMonth, int newDay)
{
month = newMonth;
day = newDay;
checkDate();
}
void DayOfYear::checkDate()
{
if( (month < 1) || (month > 12) || (day < 1) || (day > 31) )
{
cout<<"Illegal date. Aborting program.\n";
exit(1); // 用于退出程序
}
}
int DayOfYear::getMonth()
{
return month;
}
int DayOfYear::getDay()
{
return day;
}
友元函数可以简化函数定义,并提高效率。对于使用成员还是非成员函数,一个简单的使用规则如下:
- 如果任务与单个对象密切相关,就使用成员函数。
- 如果任务涉及多个对象,而且对象被均匀地使用,就使用非成员函数。
const 参数修饰符
”传引用“ 参数效率上优于 ”传值“ 参数。传值参数是局部变量,被初始化成实参的值,所以调用函数时会存在实参的两个拷贝。而传引用参数只是占位符,会被实参取代,所以只存在实参的一个拷贝。对于简单类型(比如 int
或 double
)的参数,这种效率上的差异可以忽略不计。但对于类类型的参数,两者效率上的区别有时就非常明显,必须引起重视。
如果使用传引用参数,而且函数不更改参数,就可为参数做上标记,让编译器知道参数不应更改。为此,要在参数类型前添加修饰符 const
。这样的参数是常量参数。
之前代码中的相等性函数 equal
并不会对参数进行修改,我们可以将其改为传引用的常量参数。
#include <iostream>
using namespace std;
class DayOfYear
{
public:
// 相等性函数
friend bool equal(const DayOfYear &date1, const DayOfYear &date2);
// 前条件:date1 和 date2 已被赋值
// 如果 date1 和 date2 表示相同的日期,就返回true,否则返回false
void input();
void output();
void set(int newMonth, int newDay);
// 前条件:newMonth 和 newDay 构成一个可能的日期
// 后条件:日期根据传递的实参来重置
int getMonth();
// 返回月份,1代表1月,2代表2月,依次类推
int getDay();
// 返回天数
private:
void checkDate();
int month;
int day;
};
int main()
{
DayOfYear today, bachBirthday;
cout<<"Enter today's date:\n";
today.input();
cout<<"Today's date is ";
today.output();
bachBirthday.set(3, 21);
cout<< "J. S. Bach's birthday is ";
bachBirthday.output();
if ( equal(today, bachBirthday) )
cout<<"Happy Birthday Johann Sebastian!\n";
else
cout<<"Happy Unbirthday Johann Sebastian!\n";
return 0;
}
bool equal(const DayOfYear &date1, const DayOfYear &date2)
{
return ( date1.month == date2.month &&
date1.day == date2.day );
}
// 使用 iostream
void DayOfYear::input()
{
cout<<"Enter the month as a number: ";
cin>>month;
cout<<"Enter the day of the month: ";
cin>>day;
checkDate();
}
void DayOfYear::output()
{
cout<< "month = " <<month
<< ", day = " << day << endl;
}
void DayOfYear::set(int newMonth, int newDay)
{
month = newMonth;
day = newDay;
checkDate();
}
void DayOfYear::checkDate()
{
if( (month < 1) || (month > 12) || (day < 1) || (day > 31) )
{
cout<<"Illegal date. Aborting program.\n";
exit(1); // 用于退出程序
}
}
int DayOfYear::getMonth()
{
return month;
}
int DayOfYear::getDay()
{
return day;
}
常量参数是自动错误检查的一种形式。函数定义不慎更改了常量参数,编译器会报错。参数修饰符 const
适合任何参数,但通常只将它用于传引用的类参数(偶尔也用于其他一些参数,前提是传递的实参值较大)。
调用成员函数时,调用对象的行为和传引用参数很相似,因为成员函数调用可更改调用对象的值。我们可以将 const
修饰符标记成员函数,表示成员函数不应更改调用对象的值。函数代码不慎更改了调用对象的值,编译器会报错。
在成员函数的情况下,关键字 const
要放到函数声明的后面,刚好在末尾的分号之前,比如对之前代码的 output
用 const
修饰。函数声明部分如下:
void output() const;
函数定义部分如下:
void DayOfYear::output() const
{
cout<< "month = " <<month
<< ", day = " << day << endl;
}
对于 const
在类中的使用有一个准则,要么都用,要么都不用。即为特定类型的参数使用了 const
之后,对于同类型的其他所有参数,如果它们不由函数调用更改,那么也应该使用 const
。此外,凡是不更改调用对象值的成员函数都应使用 const
修饰符。之所以制定这个规矩,原因和函数调用中的函数调用有关。例如以下 guarantee
函数定义”
void guarantee( const Money& price )
{
cout<< 2 * price.getValue() <<endl;
}
不为成员函数 getValue
的函数声明加上 const
修饰符,guarantee
函数会在大多数编译器上报错。成员函数 getValue
不更改调用对象 price
。但当编译器处理 guarantee
的函数定义时,它认为 getValue
确实(或至少有可能)会更改 price
的值。这时由于当编译器翻译 guarantee
的函数定义时,对于成员函数 getValue
,它目前唯一知道的只有 getValue
的函数声明。在函数声明中,如果不包含告诉编译器调用对象不会被更改的 const
,编译器就假定调用对象会被更改。所以,一旦为 Money
类型的参数使用了修饰符 cosnt
,就要为不更改调用对象值的所有 Money
成员函数都使用 const
。