C++递归+vector扩容的隐藏bug

问题简述

递归给vector添加元素与赋值时,由于vector扩容后地址会变,程序中记录了vector的旧地址,导致赋值失效

代码

这个问题是在写线段树的时候发现的
刨除无关紧要的线段树的部分,留下能产生bug的代码

#include<bits/sdtc++.h>
using namespace std;
using LL = long long;

struct Tree {
    std::vector<int> ls, rs;//ls[i]为i号节点的左子节点编号,rs[i]为i的右子节点编号
    int dfs(int pos,int x) {//pos为当前节点编号
        if (pos == 0) {//当前节点编号为0说明还没有这个节点,
            ls.push_back(0);
            rs.push_back(0);
            printf("ls.size=%zu,ls.capacity=%zu\n", ls.size(), ls.capacity());
//            printf("rs.size=%d,rs.capacity=%d\n",rs.size(),rs.capacity());
            pos = (int) ((int) ls.size() - 1);//初始两个元素,确保不会负数溢出;
        }
        if (x < 10) {
            
            ls[pos] = dfs(ls[pos], ++x);//问题在于这一行
                    }
//        else if(x<20){
//            rs[pos]=dfs(rs[pos],++x);
//        }
            printf("current node:%d ls:%d,rs:%d\n", pos, ls[pos], rs[pos]);
            return pos;
        }

};
int main() {
    Tree tree;
    tree.ls.resize(2);//0号节点不用
    tree.rs.resize(2);
    tree.dfs(1,1);//1为根节点
    return 0;
}

这段代码递归地调用dfs函数,创建每个节点的左子节点,2是1的作子节点,3是2的左子节点…,10是9的左子节点.每次创建节点后,输出当前ls的大小以及容量,整个树创建完成后,递归返回时,倒序输出每个节点编号以及其左子节点和右子节点编号,没有则默认值为0.理论上输出是这样的

期望输出

在这里插入图片描述

实际上输出是这样的

实际输出

在这里插入图片描述

分析

经过了大概四个小时的调试研究,发现问题出现在

    ls[pos] = dfs(ls[pos], ++x);

程序运行到这一行时,就已经读取的ls[pos]的地址并且暂存了起来,等待函数递归返回时赋值给这个地址,但是问题在于,在后续的递归中,ls可能会扩容,扩容后ls数组被放到了新的地方,刚刚暂存的地址指向了一块现在为开辟的空间,这样递归返回值就不能赋值到正确的地方.
以上猜想的证据就是在实际输出中,ls最后一次扩容是在ls.size=9的时候,此时最大节点编号为8,而 在递归返回的输出中,刚好节点8就是最后一个能正确记录自己左儿子的节点,节点7和以前的节点都没有记录自己的左儿子.
这是因为,节点8是在最后一次扩容后的节点,ls[8]的地址是最新的地址,是有效的地址,编号小于等于7的节点使用的都是ls最后一次扩容前的旧地址

验证

在每次创建新节点时打印ls的首地址

            printf("ls.size=%zu,ls.capacity=%zu,ls.location=%d\n", ls.size(), ls.capacity(),&ls[0]);

结果

D:\CLionCode\LanQiaoCode\cmake-build-debug\test1.exe
ls.size=3,ls.capacity=4,ls.location=-536402288  
ls.size=4,ls.capacity=4,ls.location=-536402288  
ls.size=5,ls.capacity=8,ls.location=-536402864  
ls.size=6,ls.capacity=8,ls.location=-536402864  
ls.size=7,ls.capacity=8,ls.location=-536402864  
ls.size=8,ls.capacity=8,ls.location=-536402864  
ls.size=9,ls.capacity=16,ls.location=-536402240 
ls.size=10,ls.capacity=16,ls.location=-536402240
ls.size=11,ls.capacity=16,ls.location=-536402240
current node:10 ls:0,rs:0                       
current node:9 ls:10,rs:0                       
current node:8 ls:9,rs:0                        
current node:7 ls:0,rs:0                        
current node:6 ls:0,rs:0                        
current node:5 ls:0,rs:0                        
current node:4 ls:0,rs:0                        
current node:3 ls:0,rs:0                        
current node:2 ls:0,rs:0                        
current node:1 ls:0,rs:2                        

可以看到,在扩容之后,ls的首地址改变了,猜想正确

解决方法

方法一

既然是由于ls扩容导致地址更新引起的问题,那么我直接在一开始就给vector充足的容量,这样ls就不用扩容,也就地址也就不用更新

int main() {
    Tree tree;
    tree.ls.reserve(100);//这个函数可以重置vector的容量,但是其已经有的元素数量不变
    tree.rs.reserve(100);//预设充足的容量
    tree.ls.resize(2);//0号节点不用
    tree.rs.resize(2);
    tree.dfs(1,1);//1为根节点
    return 0;
}

输出

D:\CLionCode\LanQiaoCode\cmake-build-debug\test1.exe
ls.size=3,ls.capacity=100,ls.location=579036816 
ls.size=4,ls.capacity=100,ls.location=579036816 
ls.size=5,ls.capacity=100,ls.location=579036816 
ls.size=6,ls.capacity=100,ls.location=579036816 
ls.size=7,ls.capacity=100,ls.location=579036816 
ls.size=8,ls.capacity=100,ls.location=579036816 
ls.size=9,ls.capacity=100,ls.location=579036816 
ls.size=10,ls.capacity=100,ls.location=579036816
ls.size=11,ls.capacity=100,ls.location=579036816
current node:10 ls:0,rs:0                       
current node:9 ls:10,rs:0                       
current node:8 ls:9,rs:0                        
current node:7 ls:8,rs:0                        
current node:6 ls:7,rs:0                        
current node:5 ls:6,rs:0
current node:4 ls:5,rs:0
current node:3 ls:4,rs:0
current node:2 ls:3,rs:0
current node:1 ls:2,rs:0

进程已结束,退出代码0

可以看到,ls没有扩容,首地址也没变,每个节点也都能找到它的左子节点

方法二

既然是由于程序运行时记录的ls[pos] 太旧了导致的问题,那么我让程序赋值的时候直接用最新的ls地址不就可以了吗

if (x <10) {
            int a=dfs(ls[pos],++x);
            ls[pos]=a;
//            ls[pos] = dfs(ls[pos], ++x);
        }

将递归用的语句改成这样,ls[pos]的地址就不会在递归之前被暂存起来,而是在赋值的时候直接取最新的地址用姐可以解决问题

** 以上两种办法可以混用,或者根据要写的程序的要求选择一种用**

完美的代码

#include <bits/stdc++.h>
using namespace std;
using LL = long long;

struct Tree {
    std::vector<int> ls, rs;//ls[i]为i号节点的左子节点编号,rs[i]为i的右子节点编号
    int dfs(int pos,int x) {//pos为当前节点编号
        if (pos == 0) {//当前节点编号为0说明还没有这个节点,
            ls.push_back(0);
            rs.push_back(0);
            printf("ls.size=%zu,ls.capacity=%zu,ls.location=%d\n", ls.size(), ls.capacity(),&ls[0]);
//            printf("rs.size=%d,rs.capacity=%d\n",rs.size(),rs.capacity());
            pos = (int) ((int) ls.size() - 1);//初始两个元素,确保不会负数溢出;
        }
        if (x <10) {
            //方法二:地址现取现用
            int a=dfs(ls[pos],++x);
            ls[pos]=a;
//            ls[pos] = dfs(ls[pos], ++x);
        }
//        else if(x<20){
//            rs[pos]=dfs(rs[pos],++x);
//        }
            printf("current node:%d ls:%d,rs:%d\n", pos, ls[pos], rs[pos]);
            return pos;
        }

};
int main() {
    Tree tree;
    //方法1:给vector充足的容量
    tree.ls.reserve(100);//这个函数可以重置vector的容量,但是其已经有的元素数量不变
    tree.rs.reserve(100);//预设充足的容量
    tree.ls.resize(2);//0号节点不用
    tree.rs.resize(2);
    tree.dfs(1,1);//1为根节点
    return 0;
}

结语

本来想刷点数据结构的题,结果调个代码三四个小时差点调到汇编代码去了.
网上找了半天资料也没有关于这个bug的任何资料,纯纯折磨.
还有一个匪夷所思的点是,这份代码在clion中运行就会有问题,但是在vscode中运行就能够正常输出.用的同一个电脑,同一个mingw,同样的g++编译器路径.我也不知道为什么,不管怎么样,以后要用递归和vector的时候一定要留点心了

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值