友元
采用类的机制后实现了数据的隐藏与封装,类的数据成员一般定义为私有成员,成员函数一般定义为公有的,依此提供类与外界间的通信接口。但是,有时需要定义一些函数,这些函数不是类的成员函数,但又需要频繁地访问类的私有数据成员,这时可以将这些函数定义为该函数的友元函数。除了友元函数外,还有友元类,两者统称为友元。友元的作用是提高了程序的运行效率(即减少了类型检查和安全性检查等都需要时间开销),但它破坏了类的封装性和隐藏性,使得非成员函数可以访问类的私有成员。
友元函数
友元函数可以直接访问类的私有成员,在类外定义,不属于任何类。
如上图所示,将输出流重载,使其可以输出我们自定义类型的对象。有两种做法,
第一:将输出运算符重载为类的成员函数,这种写法如下:
ostream& operator<<(ostream& os)
{
os << "_c = " << _c << endl;
os << "_i = " << _i;
return os;
}
由于是类的成员函数,故只需要一个参数,它的调用方式为:a.operator<<(cout);
,虽然实现了功能,但与我们平常的用法习惯不一样。
第二种:在类外部定义该函数,并将其声明为类的友元函数,这种方式,其使用法和我们平常的用法习惯相同。
友元类
如果一整个类的函数需要访问另一个类的私有成员,则可以将该类声明为另一个类的友元类。
class A{
public:
friend class B;//声明B为A的友元类
A(char c, int i)
:_c(c)
, _i(i)
{}
private:
char _c;
int _i;
};
class B{
public:
B(char c, int i, float f)
:_a(c,i)
, _f(f)
{}
void Print()
{
cout << "_c = " << _a._c << endl;
cout << "_i = " << _a._i << endl;
cout << "_f = " << _f << endl;
}
private:
float _f;
A _a;
};
测试如下:
友元关系在一定程度上破坏了类的封装性,所以不要滥用友元。
内联
考虑下面这个函数:
int Max(int a, int b)
{
return a>b ? a : b;
}
为这样的小操作定义一个函数的好处是:
- 阅读函数理解该函数要做什么比直接看表达式代码要容易的多;
- 如果程序中有 200处 调用了该函数,则需要修改其功能时,修改一份实现比修改 200 处的代码要简单容易得多;
- 每处的调用可以保证其实现是相同的;
- 函数可以被重用,无需在每个需要的地方编写实现代码。
看起来,这样的函数是完美无瑕的,可实际上并不如此:
函数的调用不但要维护函数的 栈帧空间,保存上下文环境,进行指令跳转,还要进行参数的拷贝(在不使用引用传参的情况下),当参数较多较大时,函数调用的开销比直接的表达式代码大得多。
C++ 中 inline
关键字正好可以解决这一点。若一个函数被定义为内联函数,则在编译时,该函数将在程序的每个调用点“内联的”展开,也就是说,函数调用会被替换为函数体的代码。比如上述的函数若被定义为内联函数:
inline int Max(int a, int b)
{
return a>b ? a : b;
}
则: int ret = Max(10, 20);
将被替换为 int ret = 10>20 ? 10 : 20;
这种方式避免了函数调用的开销。
但并不见得所有的内联函数都适合在调用点上展开,当一个函数包含 200 行代码时,若在每个调用点展开,会增大程序体积,使程序变得无比臃肿;如果一个递归的函数在调用点处被展开,那样的结果不敢想象。内联只是对编译器的一个建议,编译器可以选择忽略它,内联机制用于优化较小的、没有循环、递归结构的、被频繁调用的函数。
对于类的成员函数,默认情况下都是内联函数,当然也只是建议,对于较大的成员函数,即使它被一个隐式的 inline
修饰,它也不会成为内联函数。
宏与内联对比
C++ 中的内联机制提供了比宏更安全功能,下面来看看宏和内联有什么区别。
- 宏在预处理期有预处理器进行替换,内联则是在编译阶段将函数体展开;
- 内联函数的参数类似于函数传参,会进行类型检查,而宏的参数不会进行类型检查;
- 内联比宏安全得多,看看下面的代码:
#define Square(a) a*a
如果这样调用似乎没什么问题:
Square(10);//10*10 = 100
但如果这样调用的话,就会出现问题:
Square(10+5);//10+5*10+5 = 65
与我们期望的的 15*15=225
的结果大相径庭。那如果把宏改为下面这种是不是就行了呢?
#define Square(a) (a)*(a)
括号保证 a 作为一个整体被计算,这样似乎没有问题了,实际上,这个宏仍然存在很大的缺陷,我们如果这样调用它:
Square(++i);
我们期望 i
被加1,但实际上调用该宏函数后,i
的值被加了两次。宏既然有这么多的缺点,我们最好用内联、const代替宏;
- 宏不能调试,因为在预处理阶段,宏已经被替换。