【问题标题】:C++ member variable semanticsC++ 成员变量语义
【发布时间】:2015-05-08 00:06:27
【问题描述】:

我有一个相当简单的问题,想知道答案是什么。

我的笼统问题: 当你声明一个成员变量时实际发生了什么,无论是公共的还是私有的,以及变量类型的所有排列,例如static vs const vs 常规变量?

class some_class
{
private:
    static const std::string str;
public:
...
}

我有点意识到,在 C++ 中没有非变量的概念,也就是说,我被教导相信在 Java 等语言中存在非构造变量。在 Java 中可能也是如此,但这不是我被教导思考事物的方式,所以我试图想出正确的方式来思考这些未初始化的变量。

public class main {
    public static void main(String[] args) {
        String str; // A kind of non-variable, or non-constructed variable (refers to null).
        str = new String(); // Now this variable actually refers to an object rather than null, it is a constructed variable.
    }
}

由于 C++ 允许您通过初始化列表初始化构造函数中的成员变量,并且我已通过使用调试器向自己证明,该变量在通过初始化列表初始化之前不存在(显式或默认情况下) ),那么,当你声明成员变量时,实际上在幕后发生了什么?

【问题讨论】:

    标签: java c++


    【解决方案1】:

    棘手的问题——根据不同的观点,它是模棱两可的。

    从伪机器的角度来看,通常向类添加非静态普通旧数据类型会使该类类型更大。编译器还计算出如何对齐它和相对内存偏移量,以相对于生成的机器代码中的对象来寻址它。

    这是伪机器级别,因为在机器级别,数据类型实际上并不存在:只是原始位和字节、寄存器、指令等。

    当你添加一个非原始的用户定义类型时,这会递归并且编译器生成指令来访问成员的成员等等。

    从更高的层次来看,将成员添加到类可以使该成员可以从类的实例(对象)中访问。构造函数初始化这些成员,而析构函数销毁它们(递归触发具有非平凡析构函数的成员的析构函数,对于构造阶段的构造函数也是如此)。

    但您的示例是静态成员。对于静态成员,它们存储在机器级别的数据段中,编译器生成代码以从数据段访问这些静态成员。

    其中一些可能有点令人困惑。 C++ 与作为硬件级语言的 C 共享其遗产,其静态编译和链接会影响其设计。因此,尽管它可以非常高级,但它的许多构造仍然与硬件、编译器和链接器的工作方式相关,而在 Java 中,该语言可以做出一些更明智的选择,以便在没有语言的情况下为程序员提供便利设计在一定程度上反映了所有这些事情。

    【讨论】:

    • 但是声明本身,在 C++ 中不算作构造对象,并且直到你在定义中显式构造它(静态地或通过初始化器列表)才存在,对吗?所以在 C++ 中没有简单的非构造变​​量这样的东西。每个变量都是一个构造对象(至少通过默认构造函数构造)?
    • @FranciscoAguilera:引用除外。
    • @FranciscoAguilera 可以澄清的一件事是,无论语法如何,在 Java 或 C++ 中,无论您是在构造函数中初始化变量还是在类定义中方便直接地初始化变量,非静态变量都不会t 存在于内存中,直到该类的实例被实例化。如果它直接在类定义中,你可以把它想象成语言/编译器为你做额外的工作来生成“构造函数代码”。所有这些都归结为类似的机器代码,这基本上是为了方便。
    • @FranciscoAguilera 当您了解编译器和链接器的内部工作原理时,这也变得不那么奇怪了。在 C++ 中,当您声明一个静态成员时,它实际上还没有定义。然后你必须定义它。粗略地说,这是因为当你添加一个静态成员时,你基本上是在教编译器一个“符号”来查找。其他编译单元看到该符号,但由链接器负责获取符号和定义并将它们放在一起,以便定义(并可能初始化)静态的适当代码可以链接在一起。
    • @FranciscoAguilera 这类似于您在 C++ 中需要区分头文件/源文件的方式。头文件告诉包括它的那些符号是什么以及需要如何访问它们。粗略地说,他们看不到 cpp 源文件中的代码。因此,标头教导了符号是什么以及访问它们的规则是什么,以便编译器可以检查一些错误。然后源文件提供更多信息,链接器将所有这些内容挂钩并为您提供最终结果。
    【解决方案2】:

    是和不是。

    Java 中类类型的变量真的是一个指针。与 C 和 C++ 指针不同,它不支持指针算术(但这对于成为指针来说不是必需的——例如,Pascal 中的指针也不支持算术)。

    因此,当您在 Java 中定义类类型变量:String str; 时,它几乎等同于在 C++ 中定义指针:String *str;。然后,您可以为其分配一个新的(或现有的)String 对象,如您所展示的。

    现在,通过显式使用指针(或引用)在 C++ 中实现大致相同的效果肯定是可能。不过也有区别。如果使用指针,则必须显式取消引用该指针以获取它所引用的对象。如果您使用引用,则必须初始化该引用——一旦这样做,该引用就永远不能引用除了初始化它的对象之外的任何对象。

    C++ 中对于const 变量也有一些特殊的规则。在许多情况下,您只是为一个值定义一个符号名称:

    static const int size = 1234;
    

    ...并且您永远不会以需要它具有地址的方式使用该变量(例如,获取其地址),通常根本不会为其分配地址。换句话说,编译器将知道您与该名称关联的值,但是当编译完成时,编译器将在您使用该名称的任何地方替换该值,因此变量(因此)基本上消失了(尽管如果您让编译器生成调试信息,它通常会保留足够的信息以正确了解和显示其名称/类型)。

    C++ 确实有另一种情况,即变量是一个little,就像一个已声明但未初始化的Java“僵尸”对象。如果您从一个对象移动:object x = std::move(y);,在移动完成后,移动源(在本例中为 y)可能处于它存在的一个相当奇怪的状态,但是 about all您真正可以使用它为它分配一个新值。举例来说,在字符串的情况下,它可能是一个空字符串——但它也可以完全保留它在移动之前的值,或者它可以包含一些其他值(例如,目标字符串在移动之前保存的值)。

    然而,这有点不同——即使你不知道它的状态,它仍然是一个应该保持其类的不变量的对象——例如,如果你从一个字符串中移动,然后询问字符串的长度,该长度应该与字符串实际包含的长度相匹配——如果(例如)你打印出来,你不知道会打印出什么字符串,但是你应该得到一个等价的NullPointerException——如果它是一个空字符串,它就不会打印出任何东西。如果是非空字符串,则打印出来的数据长度要与.size()所表示的一致,以此类推。

    另一个明显相似的 C++ 类型是指针。未初始化的指针不指向对象。指针 本身 是存在的——它只是没有引用任何东西。尝试取消引用它可能会给出某种错误消息,告诉您您已尝试使用空指针 - 但除非它具有静态存储持续时间,或者您已明确初始化它,否则无法保证它会是空指针 - 尝试取消引用它可能会给出垃圾值、抛出异常或几乎任何其他事情(即,它是未定义的行为)。

    【讨论】:

    • 很棒,而且非常彻底的回应。我喜欢您如何提供更高级别的概述,而 ike 则对编译器/链接器语义进行了较低级别的了解。我真的希望我能将两者都标记为答案:)
    猜你喜欢
    • 2012-04-29
    • 2011-09-09
    • 1970-01-01
    • 2018-03-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多