【问题标题】:Why does the = operator work on structs without having been defined?为什么 = 运算符在没有定义的结构上工作?
【发布时间】:2010-12-07 04:57:11
【问题描述】:

我们来看一个简单的例子:

struct some_struct {
   std::string str;
   int a, b, c;
}

some_struct abc, abc_copy;
abc.str = "some text";
abc.a = 1;
abc.b = 2;
abc.c = 3;

abc_copy = abc;

那么 abc_copy 是 abc.. 的精确副本。没有定义 = 运算符怎么可能?

(在处理一些代码时,这让我感到惊讶..)

【问题讨论】:

    标签: c++ gcc operators


    【解决方案1】:

    如果您没有定义这四个方法(C++11 中的六个),编译器将为您生成它们:

    • 默认构造函数
    • 复制构造函数
    • 赋值运算符
    • 析构函数
    • 移动构造函数 (C++11)
    • 移动赋值 (C++11)

    如果你想知道为什么?
    这是为了保持与 C 的向后兼容性(因为 C 结构可以使用 = 和在声明中复制)。但它也使编写简单的类更容易。有些人会争辩说,由于“浅拷贝问题”,它增加了问题。我反对这一点的论点是,你不应该有一个拥有 RAW 指针的类。通过使用适当的智能指针,问题就消失了。

    默认构造函数(如果没有定义其他构造函数)

    编译器生成的默认构造函数会调用基类的默认构造函数,然后是每个成员的默认构造函数(按照它们被声明的顺序)

    析构函数(如果没有定义析构函数)

    以相反的声明顺序调用每个成员的析构函数。然后调用基类的析构函数。

    复制构造函数(如果没有定义复制构造函数)

    调用传递 src 对象的基类复制构造函数。然后使用 src 对象成员作为要复制的值调用每个成员的复制构造函数。

    赋值运算符

    调用传递 src 对象的基类赋值运算符。然后使用 src 对象作为要复制的值对每个成员调用赋值运算符。

    移动构造函数(如果没有定义移动构造函数)

    调用传递 src 对象的基类移动构造函数。然后使用 src 对象成员作为要移动的值调用每个成员的移动构造函数。

    移动赋值运算符

    调用传递 src 对象的基类移动赋值运算符。然后使用 src 对象作为要复制的值对每个成员调用移动赋值运算符。

    如果你这样定义一个类:

    struct some_struct: public some_base
    {   
        std::string str1;
        int a;
        float b;
        char* c;
        std::string str2;
    };
    

    编译器将构建的是:

    struct some_struct: public some_base
    {   
        std::string str1;
        int a;
        float b;
        char* c;
        std::string str2;
    
        // Conceptually two different versions of the default constructor are built
        // One is for value-initialization the other for zero-initialization
        // The one used depends on how the object is declared.
        //        some_struct* a = new some_struct;     // value-initialized
        //        some_struct* b = new some_struct();   // zero-initialized
        //        some_struct  c;                       // value-initialized
        //        some_struct  d = some_struct();       // zero-initialized
        // Note: Just because there are conceptually two constructors does not mean
        //       there are actually two built.
    
        // value-initialize version
        some_struct()
            : some_base()            // value-initialize base (if compiler generated)
            , str1()                 // has a normal constructor so just call it
            // PODS not initialized
            , str2()
       {}
    
        // zero-initialize version
        some_struct()
            : some_base()            // zero-initialize base (if compiler generated)
            , str1()                 // has a normal constructor so just call it.
            , a(0)
            , b(0)
            , c(0)   // 0 is NULL
            , str2()
            // Initialize all padding to zero
       {}
    
        some_struct(some_struct const& copy)
            : some_base(copy)
            , str1(copy.str1)
            , a(copy.a)
            , b(copy.b)
            , c(copy.c)
            , str2(copy.str2)
        {}
    
        some_struct& operator=(some_struct const& copy)
        {
            some_base::operator=(copy);
            str1 = copy.str1;
            a    = copy.a;
            b    = copy.b;
            c    = copy.c;
            str2 = copy.str2;
            return *this;
        }
    
        ~some_struct()
        {}
        // Note the below is pseudo code
        // Also note member destruction happens after user code.
        // In the compiler generated version the user code is empty
            : ~str2()
            // PODs don't have destructor
            , ~str1()
            , ~some_base();
        // End of destructor here.
    
        // In C++11 we also have Move constructor and move assignment.
        some_struct(some_struct&& copy)
                        //    ^^^^  Notice the double &&
            : some_base(std::move(copy))
            , str1(std::move(copy.str1))
            , a(std::move(copy.a))
            , b(std::move(copy.b))
            , c(std::move(copy.c))
            , str2(std::move(copy.str2))
        {}
    
        some_struct& operator=(some_struct&& copy)
                                   //    ^^^^  Notice the double &&
        {
            some_base::operator=(std::move(copy));
            str1 = std::move(copy.str1);
            a    = std::move(copy.a);
            b    = std::move(copy.b);
            c    = std::move(copy.c);
            str2 = std::move(copy.str2);
            return *this;
        } 
    };
    

    【讨论】:

    • 这已经是一个非常好的答案,但我希望看到一个使用智能指针的示例。我从来没有在 auto_ptr 上表现出色
    • @Hamy:这是构建智能指针所需的信息。如果您使用的是智能指针,那么您实际上不需要担心这一点。如果你的类中有 RAW 拥有的指针,你只需要担心上述问题。
    • 这个答案混淆了initialization的类型。如果没有初始化器,结构将是default initialized:它的 POD 类型成员将采用不确定的值。如果初始值设定项为空,则结构将是 value initialized:其 POD 类型的成员将是 zero initialized。
    【解决方案2】:

    在 C++ 中,结构相当于类,其中成员默认为公共而不是私有访问。

    如果未提供类的以下特殊成员,C++ 编译器也会自动生成它们:

    • 默认构造函数 - 无参数,默认初始化所有内容。
    • 复制构造函数 - 即与类同名的方法,它引用同一类的另一个对象。复制所有值。
    • Destructor - 在对象被销毁时调用。默认情况下什么都不做。
    • 赋值运算符 - 当一个结构/类分配给另一个时调用。这是在上述情况下被调用的自动生成的方法。

    【讨论】:

    • 如果有any用户定义的构造函数,也不提供隐式默认构造函数。
    • 隐式析构函数也调用成员和子对象的析构函数(如果有的话)
    【解决方案3】:

    为了保持与 C 的源代码兼容性,这种行为是必要的。

    C 不提供定义/覆盖运算符的能力,因此通常使用 = 运算符复制结构。

    【讨论】:

    • K&R C 根本不允许使用 = 复制结构,我不确定 C89。如果它是在 C99 中引入的,那么我认为这是由于 C++ 的影响。
    • 根据 K&R(1988 年第 2 版,第 127 页),它是由 ANSI C 引入的,但大多数现有编译器已经支持它。
    【解决方案4】:

    但它是被定义的。在标准中。如果您不提供运算符 =,则会向您提供一个。而默认运算符只是复制每个成员变量。它如何知道复制每个成员的方式?它调用它们的运算符 =(如果未定义,则默认提供...)。

    【讨论】:

      【解决方案5】:

      赋值运算符 (operator=) 是 C++ 中为结构或类隐式生成的函数之一。

      这是描述 4 个隐式生成的成员的参考:
      http://www.cs.ucf.edu/~leavens/larchc++manual/lcpp_136.html

      简而言之,隐式生成的成员执行memberwise shallow copy。这是链接页面的长版本:

      如果需要,隐式生成的赋值运算符规范如下。规范中说结果是被赋值的对象(self),并且在后置状态self的抽象值self”的值与论据from。

      // @(#)$Id: default_assignment_op.lh,v 1.3 1998/08/27 22:42:13 leavens Exp $
      #include "default_interfaces.lh"
      
      T& T::operator = (const T& from) throw();
      //@ behavior {
      //@   requires assigned(from, any) /\ assigned(from\any, any);
      //@   modifies self;
      //@   ensures result = self /\ self" = from\any\any;
      //@   ensures redundantly assigned(self, post) /\ assigned(self', post);
      //           thus
      //@   ensures redundantly assigned(result, post) /\ assigned(result', post);
      //@ }
      

      【讨论】:

      • 默认赋值运算符不能抛出,因为它不分配任何内存。 :不知道:
      • @Rob:从 12.8:10 开始的默认复制赋值运算符的定义没有提及 throw 子句。这对我来说很有意义,因为默认复制赋值运算符可以调用非默认赋值,这可能会抛出。在问题中给出的具体示例中,显然std::string::operator=(const std::string&) 可以抛出。
      【解决方案6】:

      如果您自己没有明确定义它们,编译器将为您合成一些成员。赋值运算符就是其中之一。复制构造函数是另一个,你也会得到一个析构函数。如果您不提供自己的任何构造函数,您还将获得一个默认构造函数。除此之外,我不确定还有什么,但我相信可能还有其他人(280Z28 给出的答案中的链接暗示了其他情况,但我不记得我现在在哪里读过它,所以可能只有四个)。

      【讨论】:

        【解决方案7】:

        结构基本上是其在内存中的组件的串联(为了对齐而内置了一些可能的填充)。当您将一个结构体的值分配给另一个结构体时,这些值就会被处理掉。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2013-11-24
          • 2011-11-16
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多