V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
LuckyPocketWatch
V2EX  ›  C++

为什么这段代码会报错

  •  
  •   LuckyPocketWatch · 2022-11-17 17:31:26 +08:00 · 1860 次点击
    这是一个创建于 496 天前的主题,其中的信息可能已经有所发展或是发生改变。

    首先是个自定义类

    class Widget{
    private:
        int n;
    public:
        Widget(int v):n(v) {std::cout<<"第"<<n<<"已经构造";}
        ~Widget(){std::cout<<"第"<<n<<"已经析构";}
        void show_n()const{std::cout<<"当前值为"<<n;}
    

    然后首先调用

    Widget* widget = static_cast<Widget*>(::operator new(sizeof(Widget)));
    widget = new Widget(10);   //语句 1
    widget->show_n();
    widget->~Widget();
    ::operator delete(widget);
    

    这段代码编译运行正常,win10+VS2019,64 位,运行结果为

    第 10 已经构造 当前值位 10 第 10 已经析构

    然后另一段测试代码,用 operator new 开辟可以存放多个 Widget 的内存

    Widget* widget = static_cast<Widget*>(::operator new(sizeof(Widget)*10));
    Widget* widget_tmp = widget;
    for(int i = 0 ; i < 10 ; ++i){
        widget_tmp = new Widget(i); //语句 2
        widget_tmp += 1;
    }
    

    这段代码编译运行同样没有问题,运行把每个构造函数中的语句都正确的打印出来了

    然后是

    (widget + 5) -> show_n();  //语句 3
    

    这句直接报错,VS 提示我访问了错误的内存

    然后我把语句 2 改成了

        new (widget_tmp) People(i);  //语句 4
    

    这样就语句 3 就不会报错,也正确的打印的输出信息,开始以为是申请多个对象内存要用 new[],于是我把申请内存的语句改为

    Widget* widget = static_cast<Widget*>(::operator new[](sizeof(Widget)));
    

    我发现问题还是一样,如果使用语句 2 生成对象就报错,必须采用语句 4 的方式

    我向问下为什么语句 1 和语句 2 用相同的方法生成对象,但语句 3 却要报错?

    9 条回复    2022-11-18 10:30:46 +08:00
    hsfzxjy
        1
    hsfzxjy  
       2022-11-17 17:44:38 +08:00 via Android   ❤️ 1
    widget = new Widget(10);

    你这句并不是在刚刚 new 出来的空间中初始化一个对象,而是另创建了一个对象,并把它的地址赋给 widget ,之前 new 的空间已经被泄漏了。

    理解了这一点你就知道第二段为什么错了。
    ysc3839
        2
    ysc3839  
       2022-11-17 18:00:37 +08:00
    请发完整代码,我自己测试语句 3 并没有问题。

    “为什么语句 1 和语句 2 用相同的方法生成对象”
    并不是相同的方法。语句 1 那段代码,先 new 了一块内存,把地址保存到 widget 变量中,又 new 了一个 Widget 对象,覆盖了 widget 变量,最后手动调用 Widget 的析构函数,然后释放 Widget 对象所用的内存,一开始 new 的那块内存泄漏了。
    语句 2 那段代码,先 new 了一块内存,把地址保存到 widget 变量中,再把这个地址赋值给 widget_tmp 变量,然后每次循环 new 了一个 Widget 对象,覆盖了 widget_tmp 变量。
    语句 3 是把 widget 那块内存中 +5 偏移量的地址作为 this 指针传递给 Widget::show_n()。这么做是未定义行为,因为没有调用构造函数。
    语句 4 是在 widget_tmp 那块内存上调用构造函数,也叫做 placement new ,和语句 1 2 都不一样。
    L4Linux
        3
    L4Linux  
       2022-11-17 19:11:26 +08:00
    这段代码问题大到我无力吐嘈了。别用 new/delete 得了。不直接和 kernel 打交道的场景,用 std::make_unique/shared 完全可以。
    newmlp
        4
    newmlp  
       2022-11-17 19:16:15 +08:00
    要使用偏移量必须是连续分配的内存吧,for 里面 new 出来的内存不一定就是连续的啊
    littlewing
        5
    littlewing  
       2022-11-17 19:43:55 +08:00
    widget_tmp = new Widget(i); //语句 2
    并没有在 Widget* widget = static_cast<Widget*>(::operator new(sizeof(Widget)*10)); 申请的内存上 new 啊
    DiamondY
        6
    DiamondY  
       2022-11-17 20:02:37 +08:00
    5 楼是对的,widget_tmp = new Widget(i)后,widget 与 widget_tmp 就没啥关系了,widget 是个野指针
    leonshaw
        7
    leonshaw  
       2022-11-17 20:24:50 +08:00
    new 是申请新内存构造对象,placement new 是在给定内存构造对象
    joshu
        8
    joshu  
       2022-11-17 20:32:53 +08:00
    这代码槽点确实很多
    改成这样至少是能跑的
    核心问题就是 5 楼所说的


    #include <iostream>

    class Widget{
    private:
    int n;
    public:
    Widget(int v):n(v) {std::cout<<"第"<<n<<"已经构造"<< std::endl;}
    ~Widget(){std::cout<<"第"<<n<<"已经析构"<< std::endl;}
    void show_n()const{std::cout<<"当前值为"<<n << std::endl;}
    };


    int main() {
    Widget** widget = static_cast<Widget**>(::operator new(sizeof(Widget*)*10));
    for(int i = 0 ; i < 10 ; ++i){
    *(widget+i) = new Widget(i); //语句 2
    }

    (*(widget + 5)) -> show_n();

    return 0;
    }

    请注意,原代码里的
    Widget* widget = static_cast<Widget*>(::operator new(sizeof(Widget)*10));
    它所执行的操作是分配一个可以供 10 个 Widget 存放的内存空间,一个 Widget 是 4 字节
    而你思路上试图做的是 widget=new Widget(i)是试图在把这 10 个 Widget 空间的前两块( 64 位系统指针是 8 字节)赋给一个新的指针,当 widget+=1 的时候,再做 widget=new Widget(i)的时候是把这个空间块的第 2 、3 块赋给一个新的指针
    请特别注意,之前第一步操作的时候存的指针值已经被破坏了!
    wanmyj
        9
    wanmyj  
       2022-11-18 10:30:46 +08:00
    代码无力吐槽,OP 的 C++是参考 CSharp 学的吗,C++的 new 和指针都没弄清楚,从哪里抄的代码直接就用了,没有理解代码背后每个变量都在干嘛,这种条件下写代码,就算练习也只能加深错误的理解,用在产品上更非常危险的,不只是导致各种问题,更给后面的改正带来巨大 tech debt
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   5329 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 482ms · UTC 09:21 · PVG 17:21 · LAX 02:21 · JFK 05:21
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.