【问题标题】:Why is declaration and definition defined this way in Effective C++?为什么在 Effective C++ 中以这种方式定义声明和定义?
【发布时间】:2016-01-06 09:42:20
【问题描述】:

Effective C++(第 3 版)中,第 2 项(首选 constenuminline#define)中,类特定常量的代码段如下:

class GamePlayer {
private:
    static const int NumTurns = 5;    // constant declaration
    int scores[NumTurns];             // use of constant
    ...
};

然后这本书说(用我自己的话来说)static const int NumTurns = 5; 不是一个定义,这通常是 C++ 对类成员的要求,除非它是一个从未使用过地址的静态整数常量。如果上述情况不适用于常量,或者编译器出于任何原因坚持定义,则应在实现文件中提供定义,如下所示:

const int GamePlayer::NumTurns;    // definition of NumTurns; see
                                   // below for why no value is given

根据这本书(也是我自己的话),定义中没有给出任何值,因为它已经在声明中给出了。

这让我以为我已经知道声明和定义的定义感到困惑(在问这个问题之前我在谷歌上仔细检查过):

  • 为什么static const int NumTurns = 5 不是一个定义? NumTurns这里不是初始化为5的值吗,是不是声明和定义一起出现的时候才叫初始化?
  • 为什么static 整数常量不需要定义?
  • 为什么第二个代码 sn-p 在没有定义值的情况下被认为是定义,但包含该值的类内部的声明仍然不是一个(基本上回到我的第一个问题)?
  • 初始化不是定义吗?为什么这里没有违反“唯一定义”规则?

我可能只是在这一点上感到困惑,所以有人可以从头开始重新教育我:为什么这两行代码声明和定义而不是另一行,那里有任何初始化实例?初始化也是定义吗?

致谢:代码 sn-ps 直接引用自书中。

编辑:额外引用What is the difference between a definition and a declaration?

  • 一个声明引入了标识符和类型
  • 定义实例化和实现

所以,是的……这似乎不是这里发生的事情。

编辑 2:我认为编译器可能会优化静态整数常量,方法是不将其存储在内存中,而只是在代码中内联替换它。但是如果使用NumTurns地址,既然实例化已经存在,为什么声明不自动变为声明+定义?

编辑 3: (此编辑与原始问题的关系不大,但仍然很突出。我把它放在这里,这样我就不需要为下面的每个答案复制粘贴到 cmets 中。请在 cmets 中回答我. 谢谢!)

感谢您的回答。现在我的头脑更清晰了,但编辑 2 的最后一个问题仍然存在:如果编译器在程序中检测到需要定义的条件(例如,程序中使用了&NumTurns),为什么不自动将static const int NumTurns = 5; 重新解释为声明和定义,而不是仅声明?它具有程序中其他任何地方的定义所具有的所有语法。

我来自学校的 Java 背景,并且不需要以上述方式为静态成员进行单独的定义。我知道 C++ 是不同的,但我想知道为什么上面是这样的。如果地址从不使用,则静态整数成员被内联替换,这对我来说更像是一种优化而不是基本功能,所以为什么我需要解决它(在条件不存在时提供单独的语句作为定义即使原始语句的语法足够了)而不是相反(编译器将原始语句视为定义,因为语法足够,所以需要它)?

【问题讨论】:

  • @juanchopanza 已编辑和澄清。
  • 归结为包含类定义的翻译单元是否需要自己为常量分配内存,或者它是否信任其他翻译单元会创建一个包含该常量的对象,而当前翻译单元可以将其视为外部。只有当您有定义(无值)时,当前翻译单元才会在正在创建的对象中分配空间。这样,一旦链接,所有翻译单元都会访问同一个变量。
  • 您对声明给出的默认值感到困惑,而不是定义。您也可以在定义中给出值,但您不能省略定义(这实际上使NumTurns 存在)。声明中的= 5 只设置了一个default,它确实not 设置了一个值。定义 (const int GamePlayer::NumTurns;) 使用该默认值;它也可以提供非默认初始化。
  • @DevSolar 它确实设置了一个 value,因为该常量可以通过内联替换该值在类中使用。但是,是的,这个值在与类一起定义之前在外部(或用于地址)是不可用的。
  • Effective C++ 本身解释说(正如@TonyD 所说)只有 definitions 实际上会导致编译器为某些东西分配内存,但你不会'在您进行第二次编辑之前,您甚至都不会提及记忆。我同意这令人困惑,但这里的问题是您试图让迈耶斯的示例符合您对“定义”和“声明”,而不是试图理解并应用您正在阅读的书中给出的他的定义

标签: c++ effective-c++


【解决方案1】:

免责声明:我不是标准的大师。

我建议你阅读以下两篇:

http://www.stroustrup.com/bs_faq2.html#in-class
和:
What is the difference between a definition and a declaration?

为什么 static const int NumTurns = 5 不是一个定义? NumTurns 不是 这里初始化为 5 的值,不就是当一个 声明和定义一起出现,它被称为 初始化?

在高层次上,定义(相对于声明)实例化或实现实体(变量、类、函数)。
在变量的情况下,定义使变量被分配到程序内存中。
在函数案例 - 定义给出了可以编译为汇编指令的指令。*

代码行

static const int NumTurns = 5;

默认情况下不会在程序内存中分配NumTurns,所以它只是声明。为了在程序内存中创建(唯一的)NumTurns 实例,您必须提供所需的定义

const int MyClass::NumTurns;  

比亚恩语录:

当(且仅当)静态成员具有 类外定义:

class AE {
    // ...
public:
    static const int c6 = 7;
    static const int c7 = 31;
};

const int AE::c7;   // definition

int f()
{
    const int* p1 = &AE::c6;    // error: c6 not an lvalue
    const int* p2 = &AE::c7;    // ok
    // ...
}

为什么静态整数常量不需要定义?

如果您想获取他们的地址,他们会这样做。

初始化不是定义吗?

没有。初始化是在某些实体(原始、对象)中设置初始值的行为。

void someFunc (){
  int x; //x is declared, but not initialized
  int y = 0; //y is declared+ initialized.
}

为什么这里没有违反“唯一定义”规则?

为什么会这样?整个代码中仍有一个NumTurns。符号MyClas::NumTurns 只出现一次。该定义仅使变量出现在程序内存中。 ODR 规则指定任何符号只能声明一次。

编辑: 这个问题基本上归结为“为什么编译器不能自己决定?” 我不是委员会成员,所以我不能给出完全合法的答案。
我的聪明猜测是让编译器决定事情不是 C++ 哲学。 当您让编译器决定时,例如在哪里声明一个静态整数常量, 作为开发者,我可能会提出以下问题:

1) 如果我的代码是仅标头库,会发生什么情况? 在哪里我的编译器应该声明这个变量?在它遇到的第一个 cpp 文件中? 在包含 main 的文件中?

2) 如果编译器决定在哪里声明变量,我有任何保证吗 该变量将与其他静态变量(我已手动声明)一起声明,从而保持 缓存局部性紧张吗?

一旦我们认同“让编译器疯狂”的心态,就会有越来越多的问题提出来 我认为这是 Java 和 C++ 的根本区别。

Java:让 JVM 完成它的启发式分析。
C++:让开发人员进行分析。

最终,在 C++ 中,编译器会检查一切是否有意义并将代码转换为二进制, 不做工作,而不是开发人员。

*或字节码,如果您使用托管 C++(嘘...)

【讨论】:

  • “托管 C++”?哇,我不知道有这样的东西存在。来自 Wikipedia 的 Google 的摘要将其逐字定义为“一组现已弃用的 Microsoft 偏差”,它同时具有可预测性、缓解性和搞笑性。
  • @underscore_d 你仍然可以将 C++ 与 microsoft CLR 一起使用,它没有被弃用。我个人认为,如果 C++ 先使用静态编译器进行编译,然后使用 JIT 编译进行热点优化(去虚拟化、动态缓存管理等),C++ 会更快,那么您可以从这两种方法中获得最佳效果跨度>
  • 感谢您的信息!
  • @underscore_d 它不再被称为“托管 C++”,已被弃用。当前产品是 C++/CLI,您可以通过 Visual Studio 获得。
  • @DavidHaim 嗨,感谢您的回答。我还有一个未解决的问题,请参阅编辑 3
【解决方案2】:

单一定义规则的后果之一是类的静态成员只能有一个定义。但是,如果有多个编译单元定义它,那么程序中的每个编译单元都会有一个定义。最终结果是,如果不违反单一定义规则,则声明不能成为定义。在实践中,链接器通常不够聪明,无法解析此类多重定义。

静态整数常量不需要定义的原因是它没有必要。如果值是在类定义中初始化的,编译器可以在使用它时替换初始化值。实际上,这意味着该值不需要实际占用程序中的内存位置(只要没有代码计算该常量的地址,在这种情况下就需要定义)。

声明、定义、初始化实际上是独立的(尽管相关)概念。声明告诉编译器某些东西存在。定义是一种导致该事物存在的声明类型(因此具有其他声明可见性的代码可以引用它)-例如,为其分配内存。初始化是赋予值的行为。这种区别实际上发生在语言的其他部分。例如;

#include <iostream>
int main()
{
     int x;   //  declaration and definition of x

     std::cout << x << '\n';    // undefined behaviour as x is uninitialised

     x = 42;   // since x is not yet initialised, this assignment has an effect of initialising it

     std::cout << x << '\n';    // OK as x is now initialised
}

实际上,初始化可以是声明的一部分,但不是必须的。

已编辑以回复原始问题中的“编辑 3”:

C++ 有一个单独的编译模型。 Java 的模型依赖于 C++ 模型所不具备的能力(更智能的链接器、运行时链接)。在 C++ 中,如果一个编译单元看到一个声明但没有定义,编译器只是假设定义在另一个编译单元中。通常(有很多构建链)链接器稍后会检测是否存在必要的定义,因此链接阶段会失败。相反,如果每个需要定义存在的编译单元实际上都创建了一个,编译器将打破“一个定义规则”和一个典型的愚蠢链接器 - 其中还不够聪明,无法将重复定义的东西折叠成一个定义- 会抱怨多重定义的符号。

【讨论】:

  • the initialisation of const statics within class definitions came into existence in recent standards. 好吧,我不这么认为。
  • @Peter 嗨,感谢您的回答。我还有一个未解决的问题,请参阅编辑 3
  • 回答已编辑以回应您的“编辑 3”。是的,C++ 与 Java 不同。
【解决方案3】:

为什么 static const NumTurns = 5 不是一个定义? NumTurns这里不是初始化为5的值吗,不是声明和定义一起出现的时候才叫初始化吗?

这是一个错误的问题。声明是通知编译器类型/变量/函数/任何存在的地方,以及它是什么。定义是指示编译器实际分配存储以保留所述实体。

由于该成员是在类的声明中“定义”的(即 - 那时没有创建类的实例),所以这是一个声明。

为了将其称为定义所依赖的等号只是结构成员的默认值,而不是初始化。

为什么静态整数常量不需要定义?

你自己回答了那个问题。这是因为编译器可以避免为它们分配任何存储空间,而只需将它们放入使用的代码中。

为什么第二个代码 sn-p 在没有定义值的情况下被认为是一个定义,但包含该值的类内部的声明仍然不是一个(基本上回到我的第一个问题)?

正如我之前所说,这是因为第二个代码为变量分配了存储空间。

初始化不是定义吗?为什么这里没有违反“唯一定义”的规则?

因为初始化不是定义。

如果编译器在程序中检测到需要定义的条件(例如,在程序中使用 &NumTurns),为什么不自动重新解释 static const int NumTurns = 5;作为声明和定义而不是仅声明?

因为定义分配存储。更具体地说,因为如果编译器这样做,那么在不同的编译单元中就会有多个存储分配。这很糟糕,因为在链接期间,链接器只需要一个。链接器看到同一变量的多个定义具有相同的范围,并且不知道它具有相同的值。它所知道的只是它不能将它们全部合并到一个位置。

为了避免这个问题,它让您手动进行定义。您在 cpp 文件中定义它(即 - 不在标题中),从而将分配解析为链接器可以承受的特定文件。

【讨论】:

  • 您好,感谢您的回答。我还有一个未解决的问题,请参阅编辑 3
  • @thegreatjedi,如果您得到了答案,请接受您认为最有用的答案。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-05-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-10-14
相关资源
最近更新 更多