为什么在C++类中使用成员变量的前缀

Voi*_*ter 141 c++ coding-style naming-conventions

许多C++代码使用语法约定来标记成员变量.常见的例子包括

  • 公共成员的m_ memberName(根本使用公共成员)
  • _ memberName为私人成员或所有成员

其他人尝试在使用成员变量时强制使用this-> member.

根据我的经验,大多数较大的代码库都无法一致地应用这些规则.

在其他语言中,这些惯例远没有那么普遍.我只偶尔在Java或C#代码中看到它.我想我从未在Ruby或Python代码中看到它.因此,似乎有一种趋势,即更多现代语言不对成员变量使用特殊标记.

这个约定今天在C++中是否仍然有用,或者它只是一个时代错误.特别是因为它在库之间使用不一致.没有其他语言显示没有成员前缀可以做到吗?

Jas*_*ams 224

我赞成前缀做得很好.

我认为(系统)匈牙利表示法负责前缀获得的大部分"坏说法".

在强类型语言中,这种表示法在很大程度上是毫无意义的,例如在C++"lpsz"中告诉你,你的字符串是一个指向nul终止字符串的长指针,当:分段架构是古代历史时,C++字符串是通过nul终止的常用约定指针char数组,并不是很难知道"customerName"是一个字符串!

但是,我确实使用前缀来指定变量的用法(主要是"Apps匈牙利语",虽然我更喜欢避免使用匈牙利语,因为它与匈牙利语系统有着不良和不公平的关联),这是一个非常方便的节省时间减少bug的方法.

我用:

  • m为成员
  • c表示常量/ readonlys
  • p指针(和指针指针的pp)
  • v表示不稳定
  • s为静态
  • 我索引和迭代器
  • e为事件

在我希望明确类型的地方,我使用标准后缀(例如List,ComboBox等).

这使得程序员在看到/使用变量时就会知道变量的用法.可以说最重要的情况是指针的"p"(因为用法从var变为var->,你必须更加小心指针 - NULL,指针算术等),但所有其他都非常方便.

例如,您可以在一个函数中以多种方式使用相同的变量名:(这里是一个C++示例,但它同样适用于许多语言)

MyClass::MyClass(int numItems)
{
    mNumItems = numItems;
    for (int iItem = 0; iItem < mNumItems; iItem++)
    {
        Item *pItem = new Item();
        itemList[iItem] = pItem;
    }
}
Run Code Online (Sandbox Code Playgroud)

你可以在这里看到:

  • 成员和参数之间没有混淆
  • 索引/迭代器和项之间没有混淆
  • 使用一组明确相关的变量(项目列表,指针和索引),避免了诸如"count","index"等通用(模糊)名称的许多缺陷.
  • 与"itemIndex"和"itemPtr"等替代方案相比,前缀减少了键入(更短,并且更好地使用自动完成功能)

"iName"迭代器的另一个重点是我从不用错误的索引索引数组,如果我在另一个循环中复制一个循环,我就不必重构其中一个循环索引变量.

比较这个不切实际的简单例子:

for (int i = 0; i < 100; i++)
    for (int j = 0; j < 5; j++)
        list[i].score += other[j].score;
Run Code Online (Sandbox Code Playgroud)

(这很难阅读并经常导致使用"i",其中"j"是预期的)

有:

for (int iCompany = 0; iCompany < numCompanies; iCompany++)
    for (int iUser = 0; iUser < numUsers; iUser++)
       companyList[iCompany].score += userList[iUser].score;
Run Code Online (Sandbox Code Playgroud)

(它更具可读性,并消除了对索引的所有困惑.在现代IDE中使用自动完成功能,这也可以快速轻松地输入)

下一个好处是代码片段不需要理解任何上下文.我可以将两行代码复制到电子邮件或文档中,阅读该片段的任何人都可以区分所有成员,常量,指针,索引等.我不必添加"哦,小心,因为'data'是一个指向指针的指针,因为它被称为'ppData'.

出于同样的原因,我不必为了理解它而将我的眼睛从一行代码中移开.我不必搜索代码来查找'data'是本地,参数,成员还是常量.我不必将手移到鼠标上,因此我可以将指针悬停在"数据"上,然后等待工具提示(有时从未出现)弹出.所以,程序员可以阅读和理解的代码显著更快,因为他们没有时间浪费在寻找上下或等待.

(如果你不认为你浪费时间上下搜索工作,找一些你一年前编写的代码并且从那时起就没有看过.打开文件并在没有阅读的情况下向下跳一半.看看如何在你不知道某件事是成员,参数还是本地之前,你可以从这一点开始阅读.现在跳到另一个随机位置...这就是我们整天踩着别人的代码时所做的一切或试图了解如何调用他们的功能)

'm'前缀也避免了(恕我直言)丑陋而冗长的"this->"符号,以及它所保证的不一致性(即使你小心你通常最终会混合使用'this-> data'和'data'在同一个类中,因为没有强制执行一致的名称拼写).

'this'符号旨在解决歧义 - 但为什么有人故意编写可能含糊不清的代码?歧义迟早导致错误.在某些语言中,"this"不能用于静态成员,因此您必须在编码风格中引入"特殊情况".我更喜欢有一个适用于所有地方的简单编码规则 - 明确,明确和一致.

最后一个主要好处是Intellisense和自动完成.尝试在Windows窗体上使用Intellisense来查找事件 - 您必须滚动数百个神秘的基类方法,您永远不需要调用它们来查找事件.但如果每个事件都有一个"e"前缀,它们将自动列在"e"下的一个组中.因此,前缀可以将智能感知列表中的成员,信息,事件等分组,从而更快,更轻松地找到您想要的名称.(通常,一个方法可能有大约20-50个值(本地,参数,成员,consts,事件)在其范围内可访问.但在输入前缀后(我现在想要使用索引,所以我输入'i. ..'),我只有2-5个自动完成选项.'额外打字'人员归属于前缀和有意义的名称大大减少了搜索空间并显着加快了开发速度)

我是一个懒惰的程序员,上面的约定为我节省了很多工作.我可以更快地编写代码并且减少错误,因为我知道应该如何使用每个变量.


反对的论点

那么,有什么缺点?针对前缀的典型参数是:

  • "前缀方案是坏/恶".我同意"m_lpsz"及其同类经过深思熟虑并完全没用.这就是为什么我建议使用精心设计的符号来支持您的要求,而不是复制不适合您的背景的东西.(使用适合工作的工具).

  • "如果我改变了我必须重命名的东西的用法".是的,当然你这样做,这就是重构的全部内容,以及为什么IDE有重构工具来快速,轻松地完成这项工作.即使没有前缀,改变变量的用法几乎肯定意味着它的名称应该被改变.

  • "前缀只是让我迷惑".在您学习如何使用它之前,每个工具都是如此.一旦你的大脑习惯了命名模式,它就会自动过滤掉你的信息,你不会真的想到前缀已经存在了.但是在你真正变得"流利"之前,你必须坚持使用这样的方案一两个星期.当很多人看到旧代码并开始怀疑他们如何管理没有良好的前缀方案时.

  • "我可以看看代码来解决这个问题".是的,但是你不需要浪费时间在代码的其他地方寻找或记住它的每一个细节,当你的眼睛已经集中在那里的答案是正确的.

  • (某些)只需等待工具提示弹出我的变量就可以找到这些信息.是.在支持的情况下,对于某些类型的前缀,当您的代码干净地编译时,在等待之后,您可以通读描述并找到前缀将立即传达的信息.我觉得前缀是一种更简单,更可靠,更有效的方法.

  • "这更像打字".真?一个整体人物更多?或者是 - 使用IDE自动完成工具,它通常会减少输入,因为每个前缀字符都会显着缩小搜索空间.按"e",您的班级中的三个事件将以智能感知方式弹出.按"c",列出五个常量.

  • "我可以用this->而不是m".嗯,是的,你可以.但这只是一个更丑陋,更冗长的前缀!只有它带来了更大的风险(特别是在团队中),因为编译器它是可选的,因此它的使用经常是不一致的.m另一方面,它是简洁,明确,明确而不是可选的,所以使用它犯错误要困难得多.

  • "s is for static"听起来很像匈牙利语的"糟糕"形式. (19认同)
  • `最重要的情况是指针的"p"(因为用法从var变为var->,你必须对指针更加小心.我完全不同意.如果我使用指针错误,它只是赢了编译(`void*`可能是双指针的异常).整个` - >`over` .`​​足以告诉我它是一个指针.另外,如果你使用自动完成,你的编辑器可能有声明工具提示,无需为变量信息添加前缀.无论如何,好的答案. (14认同)
  • 我的意思是要读到Hungarien Notation的问题只是因为Simonyi被误解了.他写了一个前缀应该用来表示一个变量的类型,他的意思是"类型",就像"种类的东西"而不是文字数据类型.后来,微软的平台人员选择了它并提出了lpsz ...其余的都是历史...... (6认同)
  • @Mehrdad:我不认为`z`在C++这样的语言中经常有用,其中那种低级实现细节应该封装在一个类中,但在C中(零终止是一个重要的区别)我同意和你.IMO我们使用的任何方案都应根据需要进行调整,以最好地满足我们自己的需求 - 因此,如果零终止影响您对变量的使用,那么将"z"声明为有用的前缀并没有错. (6认同)
  • 赞成清晰,全面和有趣的解释,但是这里几乎没有提示如何节省C++中的时间YET在许多其他语言中仍然大部分未被使用. (5认同)
  • `lps`可能是多余的,但`z`不是.知道字符串是否为零终止当然是件好事. (4认同)
  • 我还要为基于** 1的数字,计数和索引添加**`n` **。对我来说,这非常有用。公平地说,索引应该全部基于0(并带有前缀“ i”),但是事情还是会发生。前缀“ n”有助于利用索引算法捕获错误。例如,`int iLastItem = nDataItems-1;`应该对您的内部类型检查器看起来不错,而`m_TreeIndices [nLeftChild] =(iParent&gt; = 1)?iRightChild:null;`完全不应该。 (2认同)
  • @JasonWilliams:那些日子我一直在重构代码.我希望我更多地使用汽车.而且不仅仅是打字少.Auto强制您不要提交某种类型.从长远来看,这确实有所帮助.恕我直言当然.不知道你正在使用哪种类型不是问题,实际上它是一个很好的"功能". (2认同)
  • 在命名方面,一看到“numItems”,我就畏缩了。变量不应该是(隐含的)介词短语(“number (of) items”)。相反,它们应该是名词或名词短语,例如“itemCount”。 (2认同)
  • 感谢你的回答。我正在采用你的风格。当应用多个前缀时,您是否包含所有前缀,例如“mpsMyVar”?我猜是按字母顺序排列的? (2认同)
  • @SpaghettiCat:好问题.对于C++,是的(例如'p'是本地指针,'mp'是成员指针,因此存在显着差异).在C#中,const或static意味着它是一个成员,所以我只是自己使用'm','c'或's'.所以我调整方案以最适合我使用它的每种语言.按照你喜欢的顺序排序,只是尝试保持一致, (2认同)

jal*_*alf 111

我通常不会为成员变量使用前缀.

我曾经使用过m前缀,直到有人指出"C++已经有成员访问的标准前缀:this->.

这就是我现在用的东西.也就是说,当存在歧义时,我添加this->前缀,但通常不存在歧义,我可以直接引用变量名.

对我来说,这是两全其美的.我有一个我可以在需要时使用的前缀,我可以随时将它留出来.

当然,明显的反驳是"是的,但是你不能一眼就看出一个变量是否是一个类成员".

我说"那么什么?如果你需要知道,你的班级可能有太多的状态.或者功能太大而复杂".

在实践中,我发现这非常有效.作为额外的奖励,它允许我轻松地将局部变量提升为类成员(或其他方式),而无需重命名.

最重要的是,它是一致的!我不需要做任何特别的事情或记住任何保持一致性的约定.


顺便说一句,您不应该为班级成员使用前导下划线.您会非常不安地接近实现保留的名称.

该标准保留所有名称以双下划线或下划线开头,后跟大写字母.它还保留全局名称空间中以单个下划线开头的所有名称.

因此,具有前导下划线后跟小写字母的类成员是合法的,但是对于以大写字母开头的标识符,或者以其他方式违反上述规则之一,您迟早会做同样的事情.

因此,避免引导下划线更容易.如果要在变量名中编码范围,请使用后缀下划线,或者m_只是m前缀或只是前缀.

  • 惊讶于这个答案的投票少于前缀一. (8认同)
  • @mbarnett:不,下划线后跟大写是保留*一般*,而不仅仅是在全局命名空间中. (3认同)

Jua*_*uan 45

您必须小心使用前导下划线.保留一个单词中大写字母前的前导下划线.例如:

_Foo

_L

都是保留字

_foo

_l

不是.在其他情况下,不允许使用小写字母前导下划线.在我的特定情况下,我发现_L碰巧被Visual C++ 2005保留,并且碰撞产生了一些意想不到的结果.

关于标记局部变量是多么有用,我对此深感不安.

以下是有关保留标识符的链接: 在C++标识符中使用下划线有哪些规则?

  • 但它们可以作为成员变量名称.我没有前缀下划线,因为规则太混乱了,我过去已经烧了. (12认同)
  • 这些不是保留字.它们是保留名称.如果它们是保留字,则根本不能使用它们.因为它们是保留名称,您可以使用它们,但风险自负. (10认同)
  • 实际上,_foo和_l都在命名空间范围内保留. (4认同)

jke*_*eys 33

我更喜欢postfix下划线,比如:

class Foo
{
   private:
      int bar_;

   public:
      int bar() { return bar_; }
};
Run Code Online (Sandbox Code Playgroud)

  • 我会说它比"mBar"或"m_bar"要简单得多. (6认同)
  • 但是你有`vector <int> v_;`并写'v_.push_back(5)`也很难看 (5认同)
  • 有趣.起初看起来有点难看,但我可以看到它有什么好处. (4认同)
  • 这是Google C++风格. (4认同)

Gru*_*bel 20

最近我一直倾向于更喜欢m_前缀而不是没有前缀,原因并不是它对标记成员变量很重要,但它避免了歧义,比如你有以下代码:

void set_foo(int foo) { foo = foo; }

原因不起作用,只foo允许一个.所以你的选择是:

  • this->foo = foo;

    我不喜欢它,因为它导致参数阴影,你不再可以使用g++ -Wshadow警告,它也更长,然后键入m_.当你有a int foo;和a 时,你还会遇到变量和函数之间的命名冲突int foo();.

  • foo = foo_; 要么 foo = arg_foo;

    已经使用了一段时间,但它使得参数列表难看,文档不应该在实现中处理名称歧义.此处还存在变量和函数之间的命名冲突.

  • m_foo = foo;

    API文档保持干净,您不会在成员函数和变量之间产生歧义,然后更短this->.唯一的缺点是它会使POD结构变得丑陋,但是由于POD结构首先不会出现名称歧义,因此不需要将它们与它们一起使用.拥有唯一的前缀也使一些搜索和替换操作更容易.

  • foo_ = foo;

    应用的大多数优点m_,但我出于审美原因拒绝它,尾随或前导下划线只是使变量看起来不完整和不平衡.m_只是看起来更好.使用m_也更具扩展性,因为您可以使用g_全局变量和s_静态变量.

PS:你没有m_在Python或Ruby中看到的原因是因为两种语言都强制执行自己的前缀,Ruby @用于成员变量和Python需要self..

  • @underscore_d 参数名称是类的公共接口的一部分。这应该是您最后添加奇怪的命名约定的地方。另外,单字母变量名很糟糕,应该不惜一切代价避免,只有很少的例外(i在循环中)。 (3认同)
  • 公平地说,您至少错过了 2 个其他选项,例如 (a) 仅对成员使用诸如“foo”之类的全名,而对参数或其他本地/一次性的名称使用单字母或短名称,例如“int f”;或 (b) 在 _parameters_ 或其他局部变量前添加一些前缀。不过,关于“m_”和 pod 的观点很好;在大多数情况下,我独立地倾向于遵循这两条准则。 (2认同)

小智 10

我不能说它是多么广泛,但是就个人而言,我总是(并且总是)用'm'为我的成员变量加前缀.例如:

class Person {
   .... 
   private:
       std::string mName;
};
Run Code Online (Sandbox Code Playgroud)

这是我使用的唯一形式的前缀(我非常反匈牙利符号),但多年来它一直保持着我的优势.顺便说一句,我通常讨厌在名称中使用下划线(或其他任何地方),但确实对预处理器宏名称进行了例外处理,因为它们通常都是大写的.

  • @MartinBeckett:你应该把那个场景中的'a`大都化 - 否则你就不会这样做.`mApData`(`m`前缀,然后变量名是'apData`). (10认同)
  • 使用m而不是m_(或_)的问题与camel情况的当前时尚有关,因此难以读取某些变量名称. (5认同)
  • 我的理解是,它是camelCase,它使用m代表'apData'之类的变量令人困惑 - 它变成'mapData'而不是'm_apData'.我使用_camelCase作为受保护/私有成员变量,因为它很突出 (2认同)

Old*_*ier 10

阅读成员函数时,了解每个变量的"拥有"对于理解变量的含义是绝对必要的.在这样的函数中:

void Foo::bar( int apples )
{
    int bananas = apples + grapes;
    melons = grapes * bananas;
    spuds += melons;
}
Run Code Online (Sandbox Code Playgroud)

...很容易看到苹果和香蕉的来源,但是葡萄,甜瓜和土豆呢?我们应该查看全局命名空间吗?在课堂宣言中?变量是此对象的成员还是此对象的类的成员?如果不知道这些问题的答案,就无法理解代码.而在更长的功能中,即使是苹果和香蕉等局部变量的声明也可能在混乱中丢失.

为全局变量,成员变量和静态成员变量(可能分别为g_,m_和s_)预先设置一致标签可以立即澄清情况.

void Foo::bar( int apples )
{
    int bananas = apples + g_grapes;
    m_melons = g_grapes * bananas;
    s_spuds += m_melons;
}
Run Code Online (Sandbox Code Playgroud)

这些可能需要一些人习惯一下 - 然后,编程中没有什么?有一天甚至{和}看起来很怪异.一旦你习惯了它们,它们可以帮助你更快地理解代码.

(使用"this->"代替m_是有意义的,但更加冗长且具有视觉上的破坏性.我不认为它是标记成员变量的所有用途的一个很好的选择.)

对上述论证的可能反对意见是将论证扩展到类型.知道变量的类型"对于理解变量的含义是绝对必要的"也可能是真的.如果是这样,为什么不为每个标识其类型的变量名称添加前缀?有了这个逻辑,你最终会得到匈牙利符号.但是很多人发现匈牙利的记谱法很费劲,很难看,而且没有任何帮助.

void Foo::bar( int iApples )
{
    int iBananas = iApples + g_fGrapes;
    m_fMelons = g_fGrapes * iBananas;
    s_dSpuds += m_fMelons;
}
Run Code Online (Sandbox Code Playgroud)

匈牙利语确实告诉我们一些关于代码的新内容.我们现在明白Foo :: bar()函数中有几个隐式转换.现在代码的问题在于匈牙利前缀添加的信息的值相对于视觉成本较小.C++类型系统包含许多功能,可以帮助类型一起工作或引发编译器警告或错误.编译器可以帮助我们处理类型 - 我们不需要注释.我们可以很容易地推断出Foo :: bar()中的变量可能是数字的,如果这就是我们所知道的,这对于获得对函数的一般理解是足够好的.因此,知道每个变量的精确类型的价值相对较低.然而,像"s_dSpuds"(甚至只是"dSpuds")这样的变量的丑陋是很棒的.因此,成本效益分析拒绝匈牙利表示法,而g_,s_和m_的好处压倒了许多程序员眼中的成本.


小智 7

成员前缀的主要原因是区分成员函数local和具有相同名称的成员变量.如果您使用具有该事物名称的getter,这将非常有用.

考虑:

class person
{
public:
    person(const std::string& full_name)
        : full_name_(full_name)
    {}

    const std::string& full_name() const { return full_name_; }
private:
    std::string full_name_;
};
Run Code Online (Sandbox Code Playgroud)

在这种情况下,成员变量不能被称为full_name.您需要将成员函数重命名为get_full_name()或以某种方式装饰成员变量.

  • 这就是我加前缀的原因。在我看来,我认为 `foo.name()` 比 `foo.get_name()` 更具可读性。 (2认同)

Eri*_*ric 6

我不认为一种语法比另一种语法具有实际价值.如你所提到的,这一切都归结为源文件的统一性.

我发现这些规则很有趣的唯一一点就是当我需要两个名为identicaly的东西时,例如:

void myFunc(int index){
  this->index = index;
}

void myFunc(int index){
  m_index = index;
}
Run Code Online (Sandbox Code Playgroud)

我用它来区分这两者.此外,当我包装调用时,例如来自Windows Dll,来自Dll的RecvPacket(...)可能包含在我的代码中的RecvPacket(...)中.在这些特殊情况下,使用类似"_"的前缀可能会使两者看起来很相似,很容易识别哪个是哪个,但编译器不同


Dan*_*lau 6

一些响应侧重于重构,而不是命名约定,作为提高可读性的方法.我觉得不能替代另一个人.

我认识程序员对使用本地声明感到不舒服; 他们更喜欢将所有声明放在一个块的顶部(如在C中),因此他们知道在哪里找到它们.我已经发现,在作用域允许的情况下,声明首次使用它们的变量会减少我花时间向后查找声明的时间.(即使对于小函数,这对我也是如此.)这使我更容易理解我正在查看的代码.

我希望它与成员命名约定的关系非常清楚:当成员统一为前缀时,我永远不必回头看; 我知道甚至不会在源文件中找到声明.

我确信我没有开始喜欢这些风格.然而,随着时间的推移,在一直使用它们的环境中工作,我优化了我的想法以利用它们.我认为,鉴于使用一致,许多目前感觉不舒服的人也可能会更喜欢他们.


Ken*_*art 5

其他人尝试在使用成员变量时强制使用this-> member

这通常是因为没有前缀.编译器需要足够的信息来解析有问题的变量,因为前缀或this关键字是唯一的名称.

所以,是的,我认为前缀仍然有用.举个例子,我更喜欢输入'_'来访问成员而不是'this->'.

  • 为什么这是代码味道?我会说这是非常普遍和合理的,特别是在构造者方面. (6认同)
  • 编译器无论如何都可以解决它...局部变量会隐藏大多数语言中较高范围的变量.这是人类阅读代码的(可疑的)好处.任何体面的IDE都会以不同的方式突出显示本地/成员/全局,因此不需要这种东西 (3认同)
  • 构造函数应该(通常)在其初始化列表中设置locals.在那里,参数不会影响字段名称,但两者都是可访问的 - 所以你可以编写`struct Foo {int x; Foo(int x):x(x){...}};` (3认同)
  • 确切地。当地人会隐藏班级成员。考虑一个设置这些成员的构造函数。通常将参数命名为与成员相同的名称是有意义的。 (2认同)
  • 我假设当你做`Foo(int x,bool blee)时出现问题:x(x){if(blee)x + = bleecount; } // oops,忘记了这个 - >`我更喜欢将我的成员变量称为有用的东西,然后给出与它们缩写名称匹配的构造函数参数:`Foo(int f):foo(f){...}` (2认同)

Mr.*_*ill 5

这些惯例就是这样.大多数商店使用代码约定来简化代码可读性,因此任何人都可以轻松查看一段代码并快速解读公共和私人成员之间的事物.