【问题标题】:How Many default methods does a class have?一个类有多少个默认方法?
【发布时间】:2022-03-30 17:14:09
【问题描述】:

抱歉,这可能看起来很简单,但有人问过我这个问题,我不确定。

一个空的C++类有什么功能?

构造函数, 复制构造函数, 任务, 析构函数?

是这样吗?还是还有更多?

【问题讨论】:

  • 现在在 3 次采访中被问到过。

标签: c++


【解决方案1】:

在C++03中有4个:

  • 默认构造函数:仅在以下情况下声明 没有用户定义的构造函数 宣布。使用时定义

  • 复制构造函数 - 仅在用户未声明时才声明。如果使用则定义

  • 复制赋值运算符同上

  • 析构函数同上

在 C++11 中还有两个:

  • 移动构造函数
  • 移动赋值运算符

编译器也可能无法生成其中的一些。例如,如果类包含例如引用(或任何其他无法复制分配的内容),则编译器将无法为您生成复制分配运算符。 For more information read this

【讨论】:

  • C++0x中是否会隐式声明移动构造函数和移动赋值运算符,这实际上是悬而未决的; Dave Abrahams 和 Bjarne Stroustrup 在 10 月的 WG21 邮件中有关于此主题的论文。有人担心隐式声明它们可能会破坏遗留代码。
  • 我应该认为将引用作为成员变量会阻止默认赋值运算符的生成,不会导致复制构造函数出现问题。
  • @James:听起来很有趣。您能否提供一个讨论该主题的链接?
  • 它们都在the October 2010 mailing on the WG21 website 中列出。参见 Bjarne Stroustrup 的“To Move or Not To Move”(N3174)和 Dave Abraham 的“Implicit Move Must Go”(N3153)。我还没有时间阅读它们中的任何一个。 Anthony Williams 简要总结了要在 Batavia on his blog 讨论的问题。
  • 我还没有时间阅读 Stroustrup 的论文,但 Abrahams 的论文提出了一个非常令人信服的案例,即没有隐式移动函数。不过,看看结果如何会很有趣。如果他们能够在不破坏现有代码的情况下让隐式移动工作,那就太好了,但似乎没有明显的方法可以做到这一点。
【解决方案2】:

如果我定义以下类

class X
{};

编译器会定义以下方法:

X::X()  {}                    // Default constructor. It takes zero arguments (hence default).
X::~X() {}                    // Destructor
X::X(X const& rhs) {};        // Copy constructor
X& operator=(X const& rhs)
{return *this;}              // Assignment operator.

// Since C++11 we also define move operations
X::X(X&& rhs) {};            // Move Constructor
X& operator=(X&& rhs)
{return *this;}              // Move Assignment

注意:
如果定义了 ANY 构造函数,则不会构建默认构造函数。
如果用户定义了替代方法,则不会构建其他方法。

更有趣的是当我们有成员和基时的默认实现:

class Y: public X
{
    int    a;      // POD data
    int*   b;      // POD (that also happens to be a pointer)
    Z      z;      // A class
};

// Note: There are two variants of the default constructor.
//       Both are used depending on context when the compiler defined version
//       of the default constructor is used.
//
//       One does `default initialization`
//       One does `zero initialization`

// Objects are zero initialized when
//   They are 'static storage duration'
//   **OR** You use the braces when using the default constructor
Y::Y()      // Zero initializer
    : X()   // Zero initializer
    , a(0)
    , b(0)
    , z()   // Zero initializer of Z called.
{}

// Objects are default initialized when
//    They are 'automatic storage duration'
//    **AND** don't use the braces when using the default constructor
Y::Y()
    :X    // Not legal syntax trying to portray default initialization of X (base class)
    //,a  // POD: uninitialized.
    //,b  // POD: uninitialized.
    ,z    // Not legal syntax trying to portray default initialization of z (member)
{}
//
// Note: It is actually hard to correctly zero initialize a 'automatic storage duration'
//       variable (because of the parsing problems it tends to end up a a function
//       declaration). Thus in a function context member variables can have indeterminate
//       values because of default initialization. Thus it is always good practice to 
//       to initialize all members of your class during construction (preferably in the
//       initialization list).
//
// Note: This was defined this way so that the C++ is backward compatible with C.
//       And obeys the rule of don't do more than you need too (because we want the C++
//       code to be as fast and efficient as possible.


Y::Y(Y const& rhs)
    :X(rhs)              // Copy construct the base
    ,a(rhs.a)            // Copy construct each member using the copy constructor.
    ,b(rhs.b)            // NOTE: The order is explicitly defined
    ,z(rhs.z)            //       as the order of declaration in the class.
{}

Y& operator=(Y const& rhs)
{
    X::operator=(rhs);   // Use base copy assignment operator
    a  = rhs.a;          // Use the copy assignment operator on each member.
    b  = rhs.b;          // NOTE: The order is explicitly defined
    z  = rhs.z;          //       as the order of declaration in the class.
    return(*this);
}

Y::~Y()
{
    Your Code first
}
// Not legal code. Trying to show what happens.
  : ~z()
  , ~b() // Does nothing for pointers.
  , ~a() // Does nothing for POD types
  , ~X() ; // Base class destructed last.


// Move semantics:
Y::Y(Y&& rhs)
    :X(std::move(rhs))   // Move construct the base
    ,a(std::move(rhs.a)) // Move construct each member using the copy constructor.
    ,b(std::move(rhs.b)) // NOTE: The order is explicitly defined
    ,z(std::move(rhs.z)) //       as the order of declaration in the class.
{}

Y& operator=(Y&& rhs)
{
    X::operator=(std::move(rhs));   // Use base move assignment operator
    a  = std::move(rhs.a);          // Use the move assignment operator on each member.
    b  = std::move(rhs.b);          // NOTE: The order is explicitly defined
    z  = std::move(rhs.z);          //       as the order of declaration in the class.
    return(*this);
}

【讨论】:

  • 您的赋值运算符定义是否存在缺陷,因为您在非 void 函数中没有 return 语句。
  • 并且赋值运算符返回常量引用、值或void。
  • @Let_Me_Be:不。它返回一个引用。分配后尝试使用对象。
  • @Martin 如果你说的是(a=b).something(),那么是的,通过返回引用你可以做到这一点,但这绝对是一个非常糟糕的做法。另外请注意,通过返回常量引用,您仍然可以调用常量方法。
  • @Let:不,这是一个非常好的做法。在可能的情况下,重载运算符的语义应与内置运算符的语义相匹配。 使用赋值返回的引用可能是个坏主意,但这并不意味着赋值运算符不应该首先返回引用。
【解决方案3】:

只是为了扩展Armen Tsirunyan answer,这里是方法的签名:

// C++03
MyClass();                                     // Default constructor
MyClass(const MyClass& other);                 // Copy constructor
MyClass& operator=(const MyClass& other);      // Copy assignment operator
~MyClass();                                    // Destructor

// C++11 adds two more
MyClass(MyClass&& other) noexcept;             // Move constructor
MyClass& operator=(MyClass&& other) noexcept;  // Move assignment operator

【讨论】:

    【解决方案4】:

    【讨论】:

    • 虽然此链接可能会回答问题,但最好在此处包含答案的基本部分并提供链接以供参考。如果链接页面发生更改,仅链接答案可能会失效。请看这里:Why and how are some answers deleted?
    【解决方案5】:

    是吗?

    是的,就是这样。

    编译器默认生成

    • 默认构造函数
    • 复制构造函数
    • 复制赋值运算符
    • 析构函数

    一堂课

    使用Clang的-ast-dump选项可以看到默认生成的构造函数、复制构造函数和赋值操作符

    prasoon@prasoon-desktop ~ $ cat empty.cpp && clang++ -cc1 -ast-dump empty.cpp
    class empty
    {};
    
    int main()
    {
       empty e;
       empty e2 = e;
       {
         empty e3;
         e3 = e;
       }
    
    }
    typedef char *__builtin_va_list;
    class empty {
        class empty;
        inline empty() throw(); //default c-tor
        //copy c-tor
        inline empty(empty const &) throw() (CompoundStmt 0xa0b1050 <empty.cpp:1:7>)
    
        //assignment operator   
        inline empty &operator=(empty const &) throw() (CompoundStmt 0xa0b1590 <empty.cpp:1:7>
      (ReturnStmt 0xa0b1578 <col:7>
        (UnaryOperator 0xa0b1558 <col:7> 'class empty' prefix '*'
          (CXXThisExpr 0xa0b1538 <col:7> 'class empty *' this))))
    
    
    };
    

    【讨论】:

    • 就是,默认析构函数只是调用所有成员的析构函数。如果不这样做,所有成员都会在父对象被破坏时泄漏。
    • @Ashley 实际上不是,析构函数没有调用任何东西。这是在析构函数之外完成的。
    • 默认的析构函数确实调用了一些东西——所有成员的析构函数。它只是在析构函数的末尾隐式完成,所以你不必自己写出来。这仍然是一堆调用,这实际上是一个子程序。它也是仍然是析构函数的一部分,无论你是否自己编写。我看不出它是如何“在”析构函数之外的,因为用户析构函数和成员的自动销毁总是结合在一起。
    • @Ashley Nope,这是在析构函数之外完成的。构造函数也是如此(这就是存在初始化列表的原因),一旦进入构造函数的主体,所有成员和祖先的构造函数都已被调用。
    • @Let_Me_Be:说它是析构函数是正确的。 “在执行析构函数的主体并销毁主体内分配的所有自动对象后,X 的析构函数调用X 直接成员的析构函数,即X 直接基类的析构函数类,如果 X 是最派生类 (12.6.2) 的类型,则它的析构函数调用 X 的虚拟基类的析构函数。” (C++03 12.4/6)
    猜你喜欢
    • 1970-01-01
    • 2019-12-30
    • 1970-01-01
    • 2010-11-27
    • 2012-10-23
    • 1970-01-01
    • 2017-01-22
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多