对于某些本来应该是未定义行为的事物,却获得未定义行为

时间:2018-08-23 17:07:52

标签: c++

class Sample
{
public:
    int *ptr;

    Sample(int i)
    {
      ptr = new int(i);
    }
    ~Sample()
    {
      delete ptr;
    }
    void PrintVal()
    {
      cout << "The value is " << *ptr;
    }
};
  void SomeFunc(Sample x)
  {
    cout << "Say i am in someFunc " << endl;
  }
  int main()
  {
    Sample s1 = new Sample(10);
    SomeFunc(s1);
    s1.PrintVal();
  }

我认为应该发生的两件事:

  1. s1可以使用参数化构造函数进行初始化。
  2. PrintVal()中*ptr的值应为10。

对于1)我得到invalid conversion from 'Sample*' to 'int' [-fpermissive]。我正确地调用了构造函数,为什么会这样呢? 对于2)的情况,我得到的是垃圾值,或者程序遇到分段错误。这不会发生,因为应该删除SomeFunc的本地对象ptr的{​​{1}}而不是x的{​​{1}},因为它是通过值而不是引用传递的。 IIRC通过对象传递值将对象的副本发送到函数的接收参数。

1 个答案:

答案 0 :(得分:1)

您的代码确实具有未定义的行为。但是,让我们从头开始。

Sample s1 = new Sample(10);

这是此行中发生的事情:

  1. 在堆上分配了一个Sample对象,并且new表达式返回指向它的指针Sample*
  2. 您无法将Sample*分配给类型Sample的变量。但是Sample具有允许从int隐式构造的构造函数。如果使用-fpermissive编译器选项(提示:不要!),则编译器允许将指针隐式转换为整数-毕竟,指针只是一个内存地址,也就是一个数字。
  3. 相应地,s1通过将堆Sample对象的内存地址解释为整数(如果sizeof(Sample*) > sizeof(int)则将其截断)来构造。那就是最后以*(s1.ptr)开头的值。

重申关键点:在这一行中,您没有实例化一个Sample对象,而是两个。 Bug 1 :永远不会删除在堆上创建的一个。那是内存泄漏。

SomeFunc(s1);

Sample中没有任何内容阻止编译器生成默认的副本构造函数和默认的副本分配运算符。重要:指针的“默认”是指复制指针,而不是复制其后的对象。所以:

  1. s1被复制以调用SomeFunc()。该副本在函数中以x的形式提供。由于存在默认的指针复制,因此s1x都指向同一个int对象。
  2. x在函数末尾超出范围,析构函数运行并删除int对象。

我们还没有确定相当,但是我们已经接近了。

s1.PrintVal();

该函数尝试访问指针后面的int对象,但该对象已被删除。 s1.ptr是一个悬空指针。 错误2 :取消引用悬空指针是未定义的行为。

所有这些都是由于看似无害的隐式指针到int转换……这就是为什么默认情况下它是编译器错误的原因,至少在非古代编译器中是如此。