36

为什么会这样编译:

class FooBase
{
protected:
    void fooBase(void);
};

class Foo : public FooBase
{
public:
    void foo(Foo& fooBar)
    {
        fooBar.fooBase();
    }
};

但这不是吗?

class FooBase
{
protected:
    void fooBase(void);
};

class Foo : public FooBase
{
public:
    void foo(FooBase& fooBar)
    {
        fooBar.fooBase();
    }
};

一方面,C++ 授予对该类的所有实例的私有/受保护成员的访问权限,但另一方面,它不授予对子类所有实例的基类的受保护成员的访问权限。这在我看来相当不一致。

我已经测试过使用 VC++ 和 ideone.com 进行编译,并且都编译了第一个而不是第二个代码片段。

4

8 回答 8

31

foo接收到一个FooBase引用时,编译器不知道参数是否是 的后代Foo,所以它必须假设它不是。Foo可以访问其他Foo对象的继承的受保护成员,而不是所有其他兄弟类。

考虑这段代码:

class FooSibling: public FooBase { };

FooSibling sib;
Foo f;
f.foo(sib); // calls sib.fooBase()!?

如果Foo::foo可以调用任意后代的protected 成员FooBase,则可以调用与FooSibling没有直接关系的protected 方法Foo。这不是受保护的访问应该如何工作。

如果Foo需要访问所有FooBase对象的受保护成员,而不仅仅是那些也被称为Foo后代的对象,则Foo需要成为以下对象的朋友FooBase

class FooBase
{
protected:
  void fooBase(void);
  friend class Foo;
};
于 2012-07-24T13:26:27.117 回答
21

C++ FAQ很好地总结了这个问题:

[你]可以扒自己的口袋,不可以扒父亲的口袋,也不能扒哥哥的口袋。

于 2013-01-04T10:00:31.983 回答
10

关键点是protected授予您访问您自己的成员副本的权限,而不是访问任何其他对象中的那些成员。这是一个常见的误解,因为我们通常会概括和状态protected授予对派生类型的成员的访问权限(没有明确说明仅对他们自己的基类......)

现在,这是有原因的,通常您不应该访问层次结构的不同分支中的成员,因为您可能会破坏其他对象所依赖的不变量。考虑一种对某些大型数据成员(受保护的)执行昂贵计算的类型和两种按照不同策略缓存结果的派生类型:

class base {
protected:
   LargeData data;
// ...
public:
   virtual int result() const;      // expensive calculation
   virtual void modify();           // modifies data
};
class cache_on_read : base {
private:
   mutable bool cached;
   mutable int cache_value;
// ...
   virtual int result() const {
       if (cached) return cache_value;
       cache_value = base::result();
       cached = true;
   }
   virtual void modify() {
       cached = false;
       base::modify();
   }
};
class cache_on_write : base {
   int result_value;
   virtual int result() const {
      return result_value;
   }
   virtual void modify() {
      base::modify();
      result_value = base::result(); 
   }
};

cache_on_read类型捕获对数据的修改并将结果标记为无效,以便重新计算下一次读取的值。如果写入次数相对较多,这是一个很好的方法,因为我们只按需执行计算(即多次修改不会触发重新计算)。cache_on_write预先计算结果,如果写入次数很少,并且您希望读取的成本具有确定性(考虑读取的低延迟),这可能是一个很好的策略。

现在,回到最初的问题。两种缓存策略都维护一组比基础更严格的不变量。在第一种情况下,额外的不变量是cached只有truedata最后一次读取后没有被修改。在第二种情况下,额外的不变量是result_value操作始终的值。

如果第三个派生类型引用了 abase并且可以data写入(如果protected允许的话),那么它会破坏派生类型的不变量。

话虽如此,语言的规范被破坏了(个人观点),因为它留下了后门来实现特定的结果。特别是,如果您从派生类型中的基类创建指向成员成员的指针,则访问被签入derived,但返回的指针是指向成员的指针base,它可以应用于任何 base对象:

class base {
protected:
   int x;
};
struct derived : base {
   static void modify( base& b ) {
      // b.x = 5;                        // error!
      b.*(&derived::x) = 5;              // allowed ?!?!?!
   }
}
于 2012-07-24T15:21:29.960 回答
3

在这两个示例中都Foo继承了受保护的方法fooBase。但是,在您的第一个示例中,您尝试从同一个类(Foo::foo调用Foo::fooBase)访问给定的受保护方法,而在第二个示例中,您尝试从另一个未声明为朋友类的类访问受保护的方法(Foo::foo尝试调用FooBase::fooBase,失败,后者受到保护)。

于 2012-07-24T13:25:06.870 回答
1

在第一个示例中,您传递了一个 Foo 类型的对象,它显然继承了 fooBase() 方法,因此能够调用它。在第二个示例中,您试图调用受保护的函数,简单地说,无论在哪个上下文中,您都不能从其声明的类实例中调用受保护的函数。在第一个示例中,您继承了受保护的方法 fooBase,因此您有权在 Foo 上下文中调用它

于 2012-07-24T13:29:05.240 回答
1

我倾向于从概念和信息的角度来看待事物。如果您的 FooBase 方法实际上被称为“SendMessage”并且 Foo 是“EnglishSpeakingPerson”并且 FooBase 是 SpeechPerson,那么您的受保护声明旨在将 SendMessage 限制在 EnglishSpeakingPersons(和子类,例如:AmericanEnglishSpeakingPerson、AustralianEnglishSpeakingPerson)之间。另一种派生自SpeakingPerson 的FrenchSpeakingPerson 将无法接收SendMessage,除非您将FrenchSpeakingPerson 声明为朋友,其中“朋友”意味着FrenchSpeakingPerson 具有从EnglishSpeakingPerson 接收SendMessage 的特殊能力(即可以理解英语)。

于 2012-07-24T14:00:36.597 回答
0

除了流浪汉的回答之外,您还可以寻求解决方法。

如果您希望子类想要调用该fooBase方法,您可以制作它static。具有所有参数的子类可以访问静态受保护方法。

于 2017-05-08T13:29:21.357 回答
0

你可以在没有这样的朋友的情况下工作......

class FooBase
{
protected:
    void fooBase(void);
    static void fooBase(FooBase *pFooBase) { pFooBase->fooBase(); }
};

这避免了必须将派生类型添加到基类。这似乎有点循环。

于 2021-06-30T09:58:32.883 回答