C++ 编译器 'shallow' 副本和赋值

C++ compiler 'shallow' copies and assignments

我正在学习 class 使用 C++ 进行面向对象编程。

在我们的文字中说,

If we do not declare a copy constructor, the compiler inserts code that implements a shallow copy. If we do not declare an assignment operator, the compiler inserts code that implements a shallow assignment.

我想知道的是,这是否属实,所提到的编译器机制实际上是什么,它是如何工作的。

不是关于复制构造函数的问题,而是关于编译器行为的问题。

编辑> 更多上下文

复制构造函数 如文中所定义:

The definition of a copy constructor contains logic that

  1. performs a shallow copy on all of the non-resource instance variables
  2. allocates memory for each new resource
  3. copies data from the source resource(s) to the newly created resource(s)

资源 由文本定义

Memory that an object allocates at run-time represents a resource of that object's class.

The management of this resource requires additional logic that was unnecessary for simpler classes that do not access resources. This additional logic ensures proper handling of the resource and is often called deep copying and assignment.

没错,也确实叫浅拷贝。至于它是如何工作的,假设您有一个指针变量,并将其分配给另一个指针变量。这只复制 pointer 而不是它指向的内容,这是一个浅拷贝。 深度复制会创建一个新指针,并复制第一个指针指向的实际内容。

像这样:

int* a = new int[10];

// Shallow copying
int* b = a;   // Only copies the pointer a, not what it points to

// Deep copying
int* c = new int[10];
std::copy(a, a + 10, c);  // Copies the contents pointed to by a

关于指针的浅拷贝问题应该很明显:在上面的例子中b初始化之后,你有两个指针都指向同样的记忆。如果有人然后 delete[] a; 那么两个指针都变得无效。如果两个指针在某些class的不同对象中,那么指针之间没有真正的联系,第二个对象将不知道第一个对象是否删除了它的内存。

更准确的说法是编译器定义了一个default拷贝构造函数和一个default拷贝赋值运算符。这些将通过简单地为所有成员变量调用复制构造函数来 copy/construct 新对象。

  • 对于像 ints 和 floats 这样的原语,这通常不是问题。
  • 不过,对于指针。这是个坏消息!当第一个对象删除该指针时会发生什么?现在其他对象的指针无效了!
  • 如果一个成员变量不能被复制(也许你用了一个std::unique_ptr来解决上面的问题),那么默认的复制assignment/ctor是行不通的。你怎么能复制无法复制的东西?这将导致编译器错误。

如果您定义了自己的复制 constructor/assignment 运算符,则可以改为创建 "deep copy"。您可以:

  • 创建一个新对象,而不是复制指针
  • 显式"shallow copy"一个指针
  • 根据您的实际需要混合以上两种!
  • 使用复制对象中的 default/custom 值初始化成员变量,而不是复制原始对象中的任何内容。
  • 完全禁止复制
  • 一直持续不断

如您所见,您有很多理由想要实现(或明确禁止)您自己的复制赋值运算符、复制构造函数、它们的移动对应物和析构函数。事实上,有一个著名的 C++ 惯用法,称为 The Rule of Five(以前称为 3 规则)可以指导您决定何时执行此操作。

浅拷贝的代码是对每个字段的简单赋值。如果:

class S {
  T f;
};
S s1, s2;

s1=s2; 这样的赋值等同于下面发生的事情:

class S {
    T f;
  public:
    S &operator=(const S&s) {
      this->f = s.f; // and such for every field, whatever T is
    }
};
S s1, s2;
s1=s2;

这是标准草案12.8-8中的表述:

The implicitly-declared copy constructor for a class X will have the form X::X(const X&) if

— each direct or virtual base class B of X has a copy constructor whose first parameter is of type const B& or const volatile B&, and

— for all the non-static data members of X that are of a class type M (or array thereof), each such class type has a copy constructor whose first parameter is of type const M& or const volatile M&.123

Otherwise, the implicitly-declared copy constructor will have the form X::X(X&)

12.8-28 说:

The implicitly-defined copy/move assignment operator for a non-union class X performs memberwise copy/move assignment of its subobjects. [...] in the order in which they were declared in the class definition.

我将使用基本的 class 来尽我所知定义编译器的行为。

class Student sealed {
private:
    std::string m_strFirstName;
    std::string m_strLastName;

    std::vector<unsigned short> m_vClassNumbers;
    std::vector<std::string> m_vTeachers;

    std::vector<unsigned short> m_vClassGrades;

public:
    Student( const std::string& strFirstName, const std::string& strLastName );

    std::string getFirstName() const;
    std::string getLastName() const;

    void setClassRoster( std::vector<unsigned short>& vClassNumbers );
    std::vector<unsigned short>& getClassRoster() const;

    void setClassTeachers( std::vector<std::string>& vTeachers );
    std::vector<std::string>& getClassTeachers() const;

    void setClassGrades( std::vector<unsigned short>& vGrades );
    std::vector<unsigned short>& getGrades() const;

    // Notice That These Are Both Commented Out So The Compiler Will
    // Define These By Default. And These Will Make Shallow / Stack Copy
    // Student( const Student& c ); // Default Defined 
    // Student& operator=( const Student& c ); // Default Defined
};

此 class 的默认声明版本将构造复制构造函数和相等运算符。

class Student sealed {
private:
    std::string m_strFirstName;
    std::string m_strLastName;

    std::vector<unsigned short> m_vClassNumbers;
    std::vector<std::string> m_vTeachers;

    std::vector<unsigned short> m_vClassGrades;

public:
    Student( const std::string& strFirstName, const std::string& strLastName );

    std::string getFirstName() const;
    std::string getLastName() const;

    void setClassRoster( std::vector<unsigned short>& vClassNumbers );
    std::vector<unsigned short>& getClassRoster() const;

    void setClassTeachers( std::vector<std::string>& vTeachers );
    std::vector<std::string>& getClassTeachers() const;

    void setClassGrades( std::vector<unsigned short>& vGrades );
    std::vector<unsigned short>& getGrades() const;     

private:
    // These Are Not Commented Out But Are Defined In The Private Section
    // These Are Not Accessible So The Compiler Will No Define Them
    Student( const Student& c ); // Not Implemented
    Student& operator=( const Student& c ); // Not Implemented
};

这个 class 的第二个版本不会,因为我将它们都声明为私有的!

这可能是我可以证明这一点的最佳方式。我只展示了这个 class 的头文件接口,因为要编译的 c++ 源代码或代码并不重要。这两者在预编译阶段如何定义的不同决定了编译器在开始将源代码编译为目标代码之前将如何工作。

请记住,标准库字符串和容器确实实现了它们自己的复制构造函数和赋值运算符!但是,如果 class 具有基本类型(如 int、float、double 等),则相同的概念适用于编译器的行为。因此,编译器将根据其类型以相同的方式处理 Simple class宣言.

class Foo {
private:
    int   m_idx;
    float m_fValue;

public:
    explicit Foo( float fValue );

    // Foo( const Foo& c ); // Default Copy Constructor
    // Foo& operator=( const Foo& c ); // Default Assignment Operator
};

第二个版本

class Foo {
private:
    int   m_idx;
    float m_fValue;

public:
    explicit Foo( float fValue );

private:
    Foo( const Foo& c ); // Not Implemented
    Foo& operator=( const Foo& c ); // Not Implemented
};

编译器会以同样的方式对待这个class;它不会定义其中任何一个,因为它们由于被声明为私有而不会被实现。