C++编译器的“浅拷贝”和赋值

7
我正在上一门使用C++的面向对象编程课程。
在我们的教材中有这样一句话:
如果我们没有声明拷贝构造函数,编译器会插入代码来实现浅拷贝。如果我们没有声明分配运算符,编译器会插入代码来实现浅分配。
我想知道的是,这是否属实,这种所谓的编译器机制具体叫什么,以及它是如何工作的。
这不是关于拷贝构造函数的问题,而是关于编译器行为的问题。
编辑> 更多背景信息
文本中定义的"拷贝构造函数"的含义:
拷贝构造函数的定义包含逻辑:
1. 对所有非资源成员变量执行浅拷贝。 2. 为每个新资源分配内存。 3. 将源资源的数据复制到新创建的资源中。
文本中定义的"资源"的含义:
一个对象在运行时分配的内存表示该对象类的资源。此资源的管理需要额外的逻辑,这对于不访问资源的简单类来说是不必要的。这些额外的逻辑确保资源被正确处理,并经常被称为深拷贝和分配。

2
没有任何上下文,这句话听起来最多是令人困惑的。 - juanchopanza
该语句为真。 - user3344003
4个回答

8
更准确地说,编译器定义了一个默认的复制构造函数和一个默认的复制赋值运算符。它们将通过简单调用所有成员变量的复制构造函数来复制/构造新对象。
对于像int和float这样的基本类型,通常不会出现问题。
但是对于指针来说,情况就不同了。当第一个对象删除该指针时,另一个对象的指针将无效!
如果某个成员变量无法被复制(例如,您使用std::unique_ptr来解决上述问题),则默认的复制赋值/构造函数将无法工作。你如何复制无法复制的东西?这将导致编译器错误。
如果您定义自己的复制构造函数/赋值运算符,则可以进行“深度复制”。您可以:
  • 创建一个新对象,而不是复制指针。
  • 显式地“浅拷贝”一个指针。
  • 根据实际需要混合上述两种方法!
  • 在复制的对象中使用默认/自定义值初始化成员变量,而不是复制原始对象中的任何内容。
  • 完全禁用复制。
  • 等等等等

正如您所看到的,有许多原因可以实现(或明确禁止)自己的复制赋值运算符、复制构造函数、它们的移动对应函数和析构函数。实际上,有一个众所周知的C++习语“五个规则”(以前是三个规则),可以指导您何时这样做。


4
很多原因?哪些原因?如果你发现自己在实现特殊成员函数,很可能是你在某个地方犯了设计错误。 - juanchopanza
如果类不可复制,则默认的复制赋值/构造函数将会失败,而不是默认情况下显式标记为delete。http://coliru.stacked-crooked.com/a/40da9a87d79923e9 - NathanOliver
  1. 我会转述指针的部分:它并不总是“坏消息”。只有在对象负责释放资源(例如,在其析构函数中调用delete)时,才会出现问题(导致未定义的行为和运行时错误)。
  2. 我还要提到,对于定义自定义复制操作的类型(例如std::vector),它会调用它们的自定义行为。例如,向量的构造函数会复制其内容。
- Андрей Беньковский
虽然您的回答很有见地,但我仍然不确定编译器正在“插入”哪些代码。我不确定您所说的“默认构造函数”是什么意思。 - bigcodeszzer

7

是的,这是真的,并且被称为浅复制。关于它的工作原理,我们假设你有一个指针变量,并将其分配给另一个指针变量。这只会复制 指针 而不是它所指向的内容,这就是浅复制。一个 深度 复制将创建一个新的指针,并复制第一个指针所指向的实际内容。

像这样:

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;,那么这两个指针都将变成无效的。如果这两个指针在某个类的不同对象中,则它们之间没有真正的连接,第二个对象将不知道第一个对象是否已删除其内存。


1
@hyde std::string类(以及其他容器类,如std::vector)已经实现了复制构造函数和赋值运算符来进行深度复制。 - Some programmer dude
我理解你的意思,这是一个非常清晰的答案...但我仍然不确定编译器正在“插入”哪些代码。 - bigcodeszzer
1
@hyde 复制数组仍然是浅复制。它按原样复制数组的所有元素,这意味着如果数组是指针数组,它就像其他指针一样工作,它复制指针而不是它们所指向的内容。 - Some programmer dude
@JoachimPileborg 经过思考,我不得不让步。浅拷贝与深拷贝的整个概念仅适用于引用类型,因此谈论字符串或数组成员只会使问题更加混乱...现在清理上面的评论。 - hyde

2

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

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中已经说明:
隐式声明的类X的拷贝构造函数将采用如下形式 X::X(const X&),如果:
— X的每个直接基类B都有一个拷贝构造函数,其第一个参数是const B&或const volatile B&,并且
— 对于X的所有非静态数据成员,如果是类类型M(或其数组),则该类类型都有一个拷贝构造函数,其第一个参数是const M&或const volatile M&。
否则,隐式声明的拷贝构造函数将采用X::X(X&)的形式。

12.8-28表明:
对于非联合类X,隐式定义的拷贝/移动赋值运算符执行其子对象的逐成员拷贝/移动赋值操作。按照它们在类定义中声明的顺序进行操作。

那么默认构造函数的情况下,编译器会插入什么代码呢? - bigcodeszzer

0

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

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 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
};

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

这可能是我能够展示这种情况的最佳方式。我只显示了此类的头文件接口,因为 c++ 的源代码或编译成目标代码不是问题所在。这两个版本在预编译阶段的定义差异决定了编译器在开始将源代码编译成目标代码之前的工作方式。

要记住的是,标准库中的字符串和容器确实实现了它们自己的复制构造函数和赋值运算符!但是如果一个类具有 int、float、double 等基本类型,则同样的概念适用于编译器的行为。因此,编译器将根据其声明以同样的方式处理简单类。

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
};

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接