深入理解私有成员
更多内容,请参考《Exceptional C++ Style》,Item 16。
例1 :
int Twice(int i) {return 2 * i;}
class X {
public:
double Twice(double d) {return 2 * d;}
private:
int Twice(int i) {return 2 * i;}
};
int main(int argc, char *argv[])
{
X x;
x.Twice(21); //error: `int X::Twice(int)' is private
}
红色一行提示编译错误。
例 2 :
int Twice(int i) {return 2 * i;}
class Y {
public:
double Twice(double d) {return 2 * d;}
private:
unsigned short Twice(unsigned short i) {return 2 * i;}
};
int main(int argc, char *argv[])
{
Y y;
y.Twice(21); //error: ambiguous
}
红色一行提示有两个候选函数,无法裁决。
乍一看上面两个例子,可能会觉得有些惊讶。上述的解释很简单: 重载决议发生在可访问性检查之前。编译器在对 x.Twice(21)或 y.Twice(21)这个调用进行决议时,主要有三个步骤:
1) 名字查找:首先寻找一个至少包含一个名为Twice的实体的作用域。由于在X和Y中都有Twice实体,编译器就不在查找外层作用域。因此上述例子中全局域中的int Twice(int i) {return 2 * i;}永远都不会成为候选函数。
2) 重载决议:在候选函数中选择一个最佳匹配。对于例1,int Twice(int)是最佳匹配。对于例2,两个候选函数的匹配都是通过标准转换,没有最佳匹配,因此编译器提示错误:call of overloaded `Twice(int)' is ambiguous。
3) 可访问性检查:最后编译器会进行可访问性检查。在例1中,最佳匹配是一个private成员函数,因此不被允许。
私有成员是否真的从外部无法访问?请看下面的代码示例:
例 3:
class Z;
typedef int (Z::*pCalc) ();
class Z {
public:
Z(): private_(1) {}
template void f(const T& t) {}
void printVal() {cout << private_ << "/n";}
pCalc returnCalc() {return &Z::calc;}
private:
int private_;
int calc() {return 2 * private_;}
};
namespace {
struct YY{};
}
template<>void Z::f(const YY& y) //特化Z::f
{
private_ = 2;
}
int main(int argc, char *argv[])
{
Z z;
z.printVal();
z.f(YY()); //private member (private_) is changed
z.printVal();
pCalc p = z.returnCalc();
int i = (z.*p)(); //private member function is called
cout << i << "/n";
}
class Z中,定义了私有变量 private_和成员函数 calc。
由于任何成员模板都可以针对任何类型进行特化。因此上例中使用了一个特殊的类型来特化(前提是其他任何人都不会使用这个类型来特化该成员模板),也即为所特化的成员模板所属的类写了一个成员函数,而成员函数具有对其所属类的所有成员的访问权(包括 private成员)!
而通过 public成员返回私有成员函数的函数指针(故意将私有函数指针泄漏出去),外部代码就可以访问私有成员函数!
因此,对于 private 成员变量和成员函数,我们都从外部进行了改变或者调用!
总结: private 到底在多大程度上是私有的呢?
private成员的名字只对其所属类的其它成员或友元来说是 可访问的,这里的其它成员也包括成员模板的任何显式特化(不管它的某个给定的显式特化是否为意料之中的)。不过,如果某段代码具有对某个成员的访问权,该段代码就可以通过泄漏该成员的指针的方式将其访问权限授予外界的任何代码。
private成员对于任何能够看到其所属类定义的代码来说都是 可见的。这意味着它的函数参数的类型必须事先声明(即便它们可能对于当前编译单元来说是无用的),另外它还会参与名字查找和重载决议,因而可能会使调用变得无效或具有二义性(即便它本身可能永远不被调用)。