对于普通函数,无法直接访问类的私有成员,还有时候需要普通函数来访问类的私有成员以简化代码,这时就需要友元函数登场了。
下面代码实现一个 DayOfYear 的类,它记录一个日期。并在其中加入一个相等性函数(判断两个日期是否相等)。
#include
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
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;
}
友元函数可以简化函数定义,并提高效率。对于使用成员还是非成员函数,一个简单的使用规则如下:
”传引用“ 参数效率上优于 ”传值“ 参数。传值参数是局部变量,被初始化成实参的值,所以调用函数时会存在实参的两个拷贝。而传引用参数只是占位符,会被实参取代,所以只存在实参的一个拷贝。对于简单类型(比如 int 或 double)的参数,这种效率上的差异可以忽略不计。但对于类类型的参数,两者效率上的区别有时就非常明显,必须引起重视。
如果使用传引用参数,而且函数不更改参数,就可为参数做上标记,让编译器知道参数不应更改。为此,要在参数类型前添加修饰符 const。这样的参数是常量参数。
之前代码中的相等性函数 equal 并不会对参数进行修改,我们可以将其改为传引用的常量参数。
#include
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。