拥有所有私有成员,然后是所有受保护成员,然后是所有公共成员会更好吗?还是反过来?还是应该有多个私有、受保护和公共标签,以便操作可以与构造函数等分开?在做这个决定时我应该考虑哪些问题?
16 回答
我把公共接口放在第一位,但我并不总是这样做。我过去常常这样做,先是私有的,然后是受保护的,然后是公开的。回想起来,并没有多大意义。
作为一个类的开发者,你可能很熟悉它的“内部结构”,但是这个类的用户并不在意,或者至少他们不应该在意。他们最感兴趣的是班级能为他们做什么,对吧?
所以我把公众放在首位,通常按功能/效用来组织它。我不希望他们必须费力地浏览我的界面才能找到与 X 相关的所有方法,我希望他们以有条理的方式看到所有这些东西。
我从不使用多个公共/受保护/私人部分 - 在我看来太混乱了。
谷歌赞成这个顺序:“Typedefs and Enums, Constants, Constructors, Destructor, Methods,包括静态方法,Data Members,包括静态数据成员。”
Matthew Wilson(需要 Safari 订阅)推荐以下顺序:“Construction, Operations, Attributes, Iteration, State, Implementation, Members, and my favorite, Not to be implemented。”
他们提供了很好的理由,而且这种方法似乎是相当标准的,但无论你做什么,都要保持一致。
这是我的意见,我敢打赌,大多数人都会同意,公共方法应该优先考虑。OO 的核心原则之一是您不必关心实现。仅查看公共方法就应该告诉您使用该类所需知道的一切。
编码风格是令人惊讶的激烈对话的来源,考虑到这一点,我冒着提供不同意见的风险:
代码应该写成对人类来说最易读。我完全同意这里多次给出的声明。
偏差是我们正在进行的滚动。
为了帮助该类的用户了解如何使用它,应该编写和维护适当的文档。用户永远不需要阅读源代码就可以使用该类。如果这样做(手动或使用源内文档工具),那么在源中定义公共和私有类成员的顺序对用户来说并不重要。
然而,对于需要理解代码的人来说,在代码审查、拉取请求或维护期间,顺序非常重要——规则很简单:
项目应在使用前定义
这既不是编译器规则,也不是严格的公共与私有规则,而是常识 - 人类可读性规则。我们按顺序阅读代码,如果每次看到使用的类成员都需要来回“玩弄”,但不知道它的类型,例如,它会对代码的可读性产生不利影响。
严格区分私有与公有违反了这条规则,因为私有类成员将在任何公共方法中使用后出现。
与往常一样,首先为人类编写代码。考虑将使用你的类的人,并将最重要的成员/枚举/typedefs/对他们来说最重要的东西放在顶部。
通常这意味着公共成员处于顶部,因为这是您班级的大多数消费者最感兴趣的。其次是受保护的,其次是私人。通常。
有一些例外。
有时初始化顺序很重要,有时需要在公共之前声明私有。有时,继承和扩展一个类更为重要,在这种情况下,受保护的成员可能会放在更高的位置。而且,当对遗留代码进行单元测试时,有时公开公共方法会更容易——如果我不得不犯下这种近乎罪恶的行为,我会将它们放在类定义的底部。
但它们是相对罕见的情况。
我发现大多数时候“公共、受保护、私有”对您班级的消费者最有用。这是一个不错的基本规则。
但它不是通过访问来订购,而是更多地根据消费者的兴趣订购。
我通常首先定义接口(要读取),即公共的,然后是受保护的,然后是私有的。现在,在许多情况下,我向前迈进了一步,并且(如果我可以处理的话)使用 PIMPL 模式,从真实类的接口中完全隐藏所有私有内容。
class Example1 {
public:
void publicOperation();
private:
void privateOperation1_();
void privateOperation2_();
Type1 data1_;
Type2 data2_;
};
// example 2 header:
class Example2 {
class Impl;
public:
void publicOperation();
private:
std::auto_ptr<Example2Impl> impl_;
};
// example2 cpp:
class Example2::Impl
{
public:
void privateOperation1();
void privateOperation2();
private: // or public if Example2 needs access, or private + friendship:
Type1 data1_;
Type2 data2_;
};
您会注意到我在私有(以及受保护)成员后面加上下划线。PIMPL 版本有一个内部类,外部世界甚至看不到它的操作。这使类接口完全干净:只有真正的接口被暴露。无需争论秩序。
由于必须构建动态分配的对象,因此在类构建期间存在相关成本。此外,这对于不打算扩展但在层次结构方面存在一些缺点的类也非常有效。受保护的方法必须是外部类的一部分,因此您不能真正将它们推入内部类。
我倾向于遵循POCO C++ 编码风格指南。
我认为这都是关于可读性的。
有些人喜欢按固定顺序对它们进行分组,这样每当您打开类声明时,您就可以快速知道在哪里寻找例如公共数据成员。
总的来说,我觉得最重要的事情应该放在第一位。对于所有类中的 99.6%,粗略来说,这意味着公共方法,尤其是构造函数。然后是公共数据成员,如果有的话(记住:封装是个好主意),然后是任何受保护的和/或私有的方法和数据成员。
这是大型项目的编码标准可能涵盖的内容,检查一下可能是个好主意。
在我们的项目中,我们不是根据访问权限对成员进行排序,而是根据使用情况对成员进行排序。我的意思是,我们对使用的成员进行排序。如果公共成员使用同一类中的私有成员,则该私有成员通常位于公共成员前面的某个位置,如下面(简单)示例所示:
class Foo
{
private:
int bar;
public:
int GetBar() const
{
return bar;
}
};
这里,成员bar放在成员GetBar()之前,因为前者被后者使用。这可能会导致多个访问部分,如下例所示:
class Foo
{
public:
typedef int bar_type;
private:
bar_type bar;
public:
bar_type GetBar() const
{
return bar;
}
};
bar_type成员被bar成员使用,看到了吗?
为什么是这样?我不知道,如果您在实现中的某个地方遇到一个成员并且您需要有关该成员的更多详细信息(并且 IntelliSense 再次搞砸了),您可以在您工作的上方某处找到它,这似乎更自然。
在实践中,它很少重要。这主要是个人喜好问题。
将公共方法放在首位是非常流行的,表面上是为了让类的用户更容易找到它们。但是标题永远不应该是您的主要文档来源,因此围绕用户将查看您的标题的想法建立“最佳实践”似乎对我来说没有意义。
如果人们正在修改类,则更有可能出现在您的标题中,在这种情况下,他们应该关心私有接口。
无论您选择哪种方式,都要让您的标题干净且易于阅读。能够轻松找到我碰巧正在寻找的任何信息,无论我是该课程的用户还是该课程的维护者,都是最重要的事情。
请注意(取决于您的编译器和动态链接器),您可以通过仅添加到类的末尾(即接口的末尾)而不删除或更改任何其他内容来保持与以前版本的共享库的兼容性。(对于 G++ 和 libtool 也是如此,GNU/Linux 共享库的三部分版本控制方案反映了这一点。)
还有一个想法是您应该对类的成员进行排序,以避免由于内存对齐而浪费空间;一种策略是将成员从最小到最大排序。我从来没有在 C++ 或 C 中做过这个。
对于将使用您的类首先列出公共接口的人来说,这真的很有帮助。这是他们关心并可以使用的部分。受保护的和私人的可以跟随。
在公共接口中,可以方便地将构造函数、属性访问器和修改器以及操作符分组到不同的组中。
将私有字段放在首位。
使用现代 IDE,人们无需阅读该类来弄清楚它的公共接口是什么。
他们只是为此使用智能(或类浏览器)。
如果有人正在阅读类定义,通常是因为他们想了解它是如何工作的。
在这种情况下,了解这些领域最有帮助。它告诉你对象的部分是什么。
总的来说,你的公共接口应该放在任何东西之前,因为这是你的类的用户应该感兴趣的主要/唯一的事情。(当然,实际上这并不总是成立,但这是一个好的开始。)
其中,成员类型和常量是最好的,其次是构造运算符、操作,然后是成员变量。
二进制兼容性
对类成员进行排序有几个具体的原因。这些与二进制兼容性有关。
二进制兼容性主要影响对系统 DLL 和设备驱动程序的更改。如果您对这些不感兴趣,请忽略此答案。
公共成员必须先于私人成员。这样您就可以混合和更改私有成员,而不会影响公共数据的位置。
新的公众成员必须排在最后。这又避免了影响现有公众成员的地位。
相同的顺序适用于 vtable 成员。
除此之外,没有理由不遵循您自己/您同事的偏好。
完全取决于您的喜好。没有“正确的方法”。
在我自己的宠物项目中使用 C++ 时,我个人遵守约定,将访问修饰符放在每个成员或方法声明之前。