【问题标题】:Do constructors reside in objects?构造函数是否驻留在对象中?
【发布时间】:2020-08-20 09:03:28
【问题描述】:

我现在正在阅读 Stroustrup 的“The C++ Programming Language, 4th edition”,文中有这样几行:

"我使用标准库 memcpy() 来复制 源到目标。这是一个低级的,有时非常讨厌的功能。应该使用 仅当复制的内存中没有具有构造函数或析构函数的对象时,因为 memcpy() 对类型一无所知。”

据我所知,类对象是一个连续的内存块,仅包含类表示,即仅包含其类的数据成员。在我看来,所有成员函数(构造函数、析构函数、赋值等)似乎都存储在“其他地方”,而不是在这个类对象中。但是在这些话之后,在我看来,构造函数也存储在类对象中,也就是说,类对象的内存块中有一些字节代表相应类的构造函数。真的是这样吗?我对什么类对象的内存块的看法有误吗?

【问题讨论】:

  • Stroustrup 只想说,构造函数永远不会被调用。因此,使用 memcpy 你不会得到你的类的有效对象
  • @Thrasher 我真的认为类对象的内存块只包含类的数据成员吗?
  • 是的,基本上。继承有点复杂(但本质上相同)。
  • 关于之前的评论 - 使用虚函数时添加了vitual table pointer
  • 自然语言可能是模棱两可的,我可以看出你的困惑来自哪里。但不,这不是 Stroustrup 的意思。他的意思是这些对象不是使用构造函数/析构函数进行初始化/取消初始化的类型。这样的类型不仅仅是内存块。它们的构造函数/析构函数可以强制执行某些仅 memcpy 无法继承的不变量。

标签: c++ object constructor


【解决方案1】:

不,构造函数不驻留在对象中。它们是在构建对象期间调用的特殊函数,它们的作用是(过于简单化地)将一块原始内存转换为对象(也就是类的实例)。

问题是构造函数可以做一些不平凡的事情,比如

  • 对象包含一个指针成员,构造函数初始化该指针,使其指向动态分配的内存;
  • 该对象管理一个执行线程(如自 C++11 以来的 std::thread),并且构造函数初始化该线程;
  • 对象管理一个互斥体,构造函数初始化该互斥体;

在所有这些情况下,每个对象都包含某种句柄,它表示存在于对象本身之外的一些资源。构造函数初始化该句柄,以便它引用特定资源(例如,一块动态分配的内存)。成员函数在调用时假定资源已由构造函数正确分配,并操作该资源(例如,将数据存储在动态分配的内存中、挂起线程、锁定或解锁互斥锁)。当对象不再存在时,析构函数假定资源已被正确分配和使用,然后释放该资源。析构函数完成后,托管资源不再存在 - 任何访问它的尝试都会导致未定义的行为。

从设计的角度来看;

  • 构造函数建立了一组不变量(包括析构函数在内的所有成员函数都可以假定始终为真的一组条件);
  • 除析构函数之外的其他成员函数维护这些不变量(即,它们可能会临时更改内容,但在返回之前,它们确保不变量仍然有效)。这允许 - 除其他外 - 成员函数一个接一个地被调用;
  • 析构函数最终作为对象被销毁过程的一部分被调用。它依赖于仍然有效的构造函数建立的不变量,并释放资源。

现在,想象一下如果 - 在构造之后 - memcpy() 用于将数据从一个对象复制到另一个对象,会发生什么。这不涉及构造函数。但它按值复制句柄(指向动态分配内存的指针等)。最终结果是两个不同的对象最终拥有相同资源集的句柄。当调用任一对象的成员函数时,对两个对象的操作都会影响另一个对象使用的资源。

只要两个对象都存在,这可能会给开发人员(或者更糟糕的是,最终用户)带来意外,因为更改一个对象会产生神奇地更改另一个对象的效果。这两个对象以非预期的方式交互。 (而且,如果这些事情是有意为之,通常会产生难以控制的不良副作用)。

当其中任何一个对象不复存在时,情况会变得更糟。

考虑如果其中一个对象不再存在,但另一个对象仍然存在并被使用(例如,调用成员函数)会发生什么情况。当为第一个被销毁的对象调用析构函数时,它会愉快地释放托管资源。这就是它的工作。然而,幸存对象的成员函数愉快地假定资源仍然存在,无法(在标准 C++ 中)检测到资源不再存在,并继续对其进行操作。这总是会导致未定义的行为 - 例如,在动态分配的内存中操作数据,这些内存已被释放,就程序而言不再存在。

最终,第二个对象也将不复存在,其析构函数将释放其资源。但是该资源已经被释放(由析构函数在调用第一个对象时)并且再次释放它会导致未定义的行为。与任何其他成员函数一样,析构函数无法检测到资源已被释放,因此无法阻止。

此外,memcpy() 对于复制包含的对象或从其他管理资源的对象继承的对象是不安全的。例如,具有std::string 类型成员的对象不能用memcpy() 安全地复制,因为std::string 动态管理内存。同样,具有基类的类又具有std::string 成员不能使用memcpy() 复制。

使用memcpy() 进行复制不安全的情况不胜枚举。 “memcpy() 对类型一无所知”的简短描述是对此的粗略总结。 memcpy() 只是复制对象占用的内存,不会克隆这些对象引用的任何资源(在这些对象之外)。

有些类型(例如 POD 类型或 - 在最近的 C++ 标准中 - 普通类型)可以使用 memcpy() 安全地复制对象。但是,如果一个对象在其自身之外管理资源,则使用memcpy() 复制它通常是不安全的。

【讨论】:

    猜你喜欢
    • 2013-10-14
    • 1970-01-01
    • 2017-01-13
    • 2015-05-06
    • 2015-11-05
    • 1970-01-01
    • 2017-09-05
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多