C++:为什么必须声明私有函数?

30

C++中的类为什么必须声明它们的私有函数?这背后是否有实际的技术原因(在编译时起到了什么作用),或者仅仅是出于一致性考虑?


你是在问函数本身为什么要被声明,还是在问为什么必须为这些函数加上 private - GManNickG
2
@GManNickG 第一个问题。我只是不知道为什么其他类,包括我的头文件,都必须知道它的私有函数。 - Ancurio
1
privatevirtual 是正交的概念。函数被声明为 private 并不意味着它不是 virtual(事实上,有一个整个习惯用语围绕着仅有 private virtualpublic 非虚拟成员函数),因此,是的,其存在会影响虚表。 - David Rodríguez - dribeas
1
@DavidRodríguez-dribeas 很好的观点。我实际上不知道虚拟私有函数可以在子类中被覆盖,并且其基类调用新版本而不暴露可调用性(这甚至是一个词吗?) - Ancurio
2
@Ancurio:你想要的词是“可访问性”。 - GManNickG
显示剩余2条评论
7个回答

21
我问为什么私有函数需要声明,因为它们对其他翻译单元没有贡献(既不增加对象大小也不增加虚表项)。如果您考虑一下,这类似于在文件中将某些函数声明为 static。从外部看不到,但对编译器本身很重要。编译器希望在使用函数之前知道其签名。这就是为什么首先要声明函数的原因。请记住,C++ 编译器是一次通过,这意味着必须在使用之前声明所有内容。
从程序员的角度来看,声明私有函数仍然不完全没有用处。想象一下,有 2 个类,其中一个是另一个类的 friend。被友好的那个类需要知道这个类的私有成员是什么样子的,否则它们无法使用它。
至于为什么 C++ 是以这种方式设计的,我首先会说有历史原因:无法在 C 中对结构体进行切片,因此采用了 C++,使您无法切片类(并被其他从 C++ 分支出的语言采用)。我也猜测这是关于简单性的问题:想象一下,在哪里可以制定一种编译方法,您可以将类分解为不同的头文件,让源文件知道它,而又可以防止其他人向您的类添加内容。
最后需要注意的是,private 函数如果是 virtual 的,则可能会影响虚表大小。

1
是的,我习惯于通过使用静态C函数来隐藏C中的实现,这就是为什么我希望能够在cpp文件中声明(在任何实现之前)私有函数,就像在C中的静态声明一样。 - Ancurio
@Ancurio,尽管你说的绝对有道理,不幸的是C++不支持将类定义分为两部分。 - Shahbaz
2
在使用之前声明签名只是与它们必须在类本身中的原因有些模糊的关系。被朋友区分的类不需要知道其他类的私有信息,它的成员函数定义需要知道这些私有信息。这也可以稍后/在类外完成。而且,您可以使用模板执行有趣的操作,这可以说是将类定义切片为两个部分。这提醒我,具有内联函数的类定义是双通行的,而不是单通行的。这就是为什么您可以在成员函数定义中使用成员/函数之前声明该成员的原因。 - Mooing Duck
1
@MooingDuck,确实,我知道类声明本身的传递不是一次性的。然而,这仍然适用,因为大多数情况下,您在类之外定义函数,在类被声明之后。就像我在上面的评论中说的那样,如果您可以将私有成员/函数的声明放在内部头文件中,那将是很好的,但是没有直接的支持。(您始终可以有一个void *internal;,但那只是一个解决方法)。 - Shahbaz
@Shahbaz:所以,正如OP所问,为什么没有支持它的呢? - Mooing Duck
@MooingDuck,除了历史原因(在C中无法切片struct,被C++采用,所以你无法切片class(并被其他从C++分支出的语言采用)),我猜这是为了简单性。想象一下,如果你要设计一个计划,让你可以将类分成不同的头文件,让每个人都知道它,并防止其他人向你的类添加东西,那会有多困难。 - Shahbaz

20
你必须在类的定义中声明所有成员,这样编译器才知道哪些函数被允许成为成员。否则,另一个程序员可能会(无意地?)添加成员,犯错误并违反对象的保证,导致未定义的行为和/或随机崩溃。

10
我认为你刚刚描述了Python中的类 =) - JoeFish

3

有几个问题需要考虑:

  • C ++不允许在初始定义之后重新打开类以声明其中的新成员。
  • C ++不允许在组成程序的不同翻译单元中具有不同的类定义。

因此:

  • .cpp文件想要声明为类的任何私有成员函数都需要在.h文件中定义,每个使用该类的用户也会看到。

从实际的二进制兼容性角度来看:如David在评论中所说,私有的virtual函数会影响这个类及使用它作为基类的任何类的vtable的大小和布局。因此,即使编译无法调用它们的代码,编译器仍需了解它们。

C ++可以被发明成另一种方式来允许.cpp文件重新打开类并添加某些特定类型的额外成员函数,并要求实现安排以确保这不会破坏二进制兼容性吗?可以放宽一次定义规则,允许在某些方面不同的定义吗?例如,静态成员函数和非虚非静态成员函数。

可能两者都可以。我认为没有任何技术障碍,尽管当前的ODR非常严格,关于什么使定义“不同”(因此对于允许非常相似的定义之间的二进制不兼容性,实现非常慷慨)。我认为介绍这种例外规则的文本会很复杂。

最终可能取决于“设计者想要那样”,也可能是某人尝试过并遇到了我没有想到的障碍。


当前的模块提案已经针对隐私问题提供了解决方案,因为它们增加了另一个“层”或“房间”的可见性:类本身、模块、以及“外部代码”。private将不会泄露到模块定义之外,编译器仍然可以知道所有内容。 - Xeo
“C++不允许在类的初始定义后重新打开该类以声明新成员。”这是不正确的:14.7.3 显式特化 [temp.expl.spec] “以下任何一个的显式特化:(...)-类或类模板的成员类模板-类或类模板的成员函数模板可以通过声明来声明(...)”“显式特化应在封闭专用模板的命名空间中声明。” - curiousguy
@curiousguy:不太确定你的意思。我认为成员模板的特化不是一个“新成员”,因为模板本身就是成员。我可能错了。如果我错了,那么模板显然是我所说的例外情况,因为除了在类外特化成员模板之外,你当然也可以在类外实例化成员模板。在任何情况下,我都认为类没有被“重新打开”,我认为这对提问者没有帮助。 - Steve Jessop
你可以将其视为“无限成员的‘潜在’声明”,但正如我上面所说,我认为这样做是不正确的。我相信只有一个成员,那就是模板本身。向函数模板添加特化版本永远不会影响模板是否被查找规则选中,它只会影响实例化的生成方式。这就是为什么通常应该重载函数模板而不是对其进行特化的原因,尽管在类成员的情况下,一旦定义了类,您就没有选择了,因为它不能重新打开以添加重载。 - Steve Jessop
无论如何,我们正在争论短语“重新打开类以添加新成员”的正确定义,这与问题无关。根据我在这些评论中定义的短语含义(将成员模板特化不是“重新打开以添加新成员”),我在答案中所说的是正确的。根据您对该短语的定义,该语句是错误的,但可以通过添加额外的模棱两可来使其变为真实。无论哪种方式,问题的答案都是:“因为如果您在类定义中省略了私有成员,则没有机制可以稍后添加它们”。 - Steve Jessop
显示剩余3条评论

2

访问级别并不影响可见性。私有函数对外部代码可见,并且可能会被重载解析所选中(这将导致访问冲突错误):

class A {
    void F(int i) {}
public:
    void F(unsigned i) {}
};

int main() {
    A a;
    a.F(1); // error, void A::F(int) is private
}

想象一下当这个工作时会有多么混乱:

class A {
public:
    void F(unsigned i) {}
};

int main() {
    A a;
    a.F(1);
}

// add private F overload to A
void A::F(int i) {}

但是将其更改为第一个代码会导致重载决议选择不同的函数。那么下面的例子呢?

class A {
public:
    void F(unsigned i) {}
};

// add private F overload to A
void A::F(int i) {}

int main() {
    A a;
    a.F(1);
}

这里有另一个相关问题的例子:

// A.h
class A {
public:
    void g() { f(1); }
    void f(unsigned);
};

// A_private_interface.h
class A;
void A::f(int);

// A.cpp
#include "A_private_interface.h"
#include "A.h"

void A::f(int) {}
void A::f(unsigned) {}

// main.cpp
#include "A.h"

int main() {
    A().g();
}

1
嗯,这似乎只是指出重载规则需要改变。 - GManNickG
@GManNickG 要考虑可见性并且不选择不可访问的方法?C++对其工作方式有充分的理由。例如:https://dev59.com/ZUbRa4cB1Zd3GeqP4eCK - bames53
我认为重载规则不需要考虑可访问性。它们可能只需要定义为仅考虑在使用点之前声明的成员函数-因此,如果您的私有成员函数在类定义中,则会停止客户端代码中的a.F(1)编译,如果它仅在A.cpp中添加,则不会影响客户端代码,但会影响.cpp文件。这已经是命名空间的工作方式(如果添加更多重载,它们可能会被选中)。需要一些复杂性,例如双相查找会发生什么。 - Steve Jessop
@SteveJessop 当然可以这样做,但我觉得这将是一个相当重大的变化。例如,目前类成员的作用域是整个类,即使在声明类成员之前也是如此。我认为不必要增加这种复杂性,因为它只解决了C ++编译模型的一个特殊情况,并且已经有解决这个特殊情况的变通方法。 - bames53
在C++11中,可以使用= delete语法显式删除重载。 - balki
显示剩余2条评论

1
一个原因是在C++中,友元可以访问你的私有成员。为了让友元能够访问它们,友元必须知道它们的存在。

@Neal:嗯...那就是“友元”的该死目的 - Xeo

0

类的私有成员仍然是类的成员,因此必须声明它们,因为其他公共成员的实现可能依赖于该私有方法。声明它们将允许编译器将对该函数的调用理解为成员函数调用。

如果您有一个仅在 .cpp 文件中使用且不依赖于类的其他私有成员的直接访问的方法,请考虑将其移动到匿名命名空间中。然后,它不需要在头文件中声明。


1
是的,但是为什么规范要求它们必须被声明呢?你没有回答这个问题。已经有规则说在调用之前必须声明函数,所以不是这个原因。(命名空间的提示倒是一个好主意) - Mooing Duck

0

有几个原因说明为什么必须声明私有函数。

第一个编译时错误检查

访问修饰符的目的是在编译时捕获某些类(没有双关语)的编程错误。私有函数是指,如果有人从类外部调用它们,那将是一个错误,您希望尽早了解这一点。

第二个强制转换和继承

来自C++标准:

3 [注意:私有基类的成员可能作为继承的成员名称不可访问,但可以直接访问。由于指针转换(4.10)和显式转换(5.4)的规则,从派生类的指针到不可访问的基类的指针的转换可能是非法的,如果使用隐式转换,则使用显式转换是合法的。

第三个友元

朋友们互相展示他们的私人部位。另一个类可以通过友元调用私有方法。

第四个普遍的理智和良好的设计

曾经与其他100名开发人员一起工作过吗?拥有标准和一般规则集有助于维护可维护性。对于组内的每个人来说,声明某些内容为私有具有特定的含义。

这也涉及到良好的面向对象设计原则。什么需要暴露,什么不需要。


3
这不是Ancurio想要知道的。他想知道为什么我们不能一开始就将私有函数保持为“秘密”。如果你不知道一个函数,就不能调用它。 - Nordic Mainframe
编译器需要知道它是私有的,这样当其他人尝试调用它时,它可以发出编译时警告...没有什么是真正的秘密。 - nate_weldon
1
@nate_weldon 这是真的,但如果私有非虚函数没有在公共头文件(因此也没有文档)中声明,那么就不会有人尝试调用它。除了我自己...在这种情况下,那个警告(实际上不是错误吗?)确实有意义。 - Ancurio
接受朋友请求是可以的,因为朋友会互相展示他们的私人信息。 - nate_weldon
1
从标准3中可以看出:私有基类的成员可能作为继承成员名称而无法访问,但可以直接访问。由于指针转换(4.10)和显式转换(5.4)的规则,如果使用隐式转换,则从派生类指针到不可访问的基类指针的转换可能是不合法的,但如果使用显式转换,则是合法的。 - nate_weldon
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接