C++ ostringstream 奇怪行为

4

最近我遇到了一些关于c++代码的奇怪问题。 我用一个最小化的例子重现了这个情况。 我们有一个Egg类:

class Egg
{
private:
    const char* name;
public:
    Egg() {};
    Egg(const char* name) {
        this->name=name;
    }
    const char* getName() {
        return name;
    }
};

我们还有一个Basket类来容纳Eggs。
const int size = 15;
class Basket
{
private:
    int currentSize=0;
    Egg* eggs;
public:
    Basket(){
        eggs=new Egg[size];
    }
    void addEgg(Egg e){
        eggs[currentSize]=e;
        currentSize++;
    }
    void printEggs(){
        for(int i=0; i<currentSize; i++)
        {
            cout<<eggs[i].getName()<<endl;
        }
    }
    ~Basket(){
        delete[] eggs;
    }
};

以下是一个按预期工作的示例。

 Basket basket;
 Egg egg1("Egg1");
 Egg egg2("Egg2");

 basket.addEgg(egg1);
 basket.addEgg(egg2);
 basket.printEggs();
 //Output: Egg1 Egg2

这是预期的结果,但如果我想要添加N个鸡蛋并根据某些循环变量生成名称,我会遇到以下问题。
 Basket basket;
 for(int i = 0; i<2; i++) {
    ostringstream os;
    os<<"Egg"<<i;
    Egg egg(os.str().c_str());
    basket.addEgg(egg);
 }
 basket.printEggs();
 //Output: Egg1 Egg1

如果我将循环条件改为i<5,那么得到的结果是"Egg4 Egg4 Egg4 Egg4 Egg4"。它会将最后添加的Egg保存在动态Egg数组的所有索引中。

经过Google一番搜索,我发现在Egg中给char* name变量分配一个固定的大小,并在构造函数中使用strcpy可以解决这个问题。

这是“修复后”的Egg类。

class Egg
{
private:
     char name[50];
public:
    Egg(){};
    Egg(const char* name)
    {
        strcpy(this->name, name);
    }
    const char* getName()
    {
        return name;
    }
};

现在的问题是为什么啊? 提前感谢您。 这里是代码的链接

4
如果你正在使用C++,使用std::string要比旧的C字符串函数更好。 - tadman
是的,我对C++不是很擅长。只是想为这种情况创建一个示例。 - Borislav Stoilov
1
我同意,使用std::stringstd::vector,并编写自己的复制构造函数,因为我猜测你正在经历未定义行为。 - JVApen
std::string比C风格的字符串更加宽容,因此如果可以的话,请使用它们,除非您有非常充分的理由不这样做。当您操作原始字符指针时,这种错误非常普遍。 - tadman
你的指针混乱,并且违反了“三法则”(https://dev59.com/eG855IYBdhLWcg3wvXDd)。你应该获得一些关于如何在C++中使用指针和数组的基本知识。 - Teivaz
让我注意一下,这不是关于如何做到这一点的问题,而是为什么我观察到这个问题。 - Borislav Stoilov
4个回答

5
让我们仔细看一下这个表达式:os.str().c_str()
函数str按值返回一个字符串,并以这种方式使用它,使返回的字符串成为一个临时对象,其生存期仅限于表达式结束。一旦表达式结束,字符串对象被销毁并不存在。
您传递给构造函数的指针是临时字符串对象的内部字符串的指针。一旦字符串对象被销毁,该指针将不再有效,使用它会导致未定义的行为
当您想要使用字符串时,简单的解决方案当然是使用std::string。更复杂的解决方案是使用数组并复制字符串的内容,在它消失之前(就像您在“fixed” Egg 类中所做的)。但请注意,“fixed”解决方案使用固定大小的数组容易出现缓冲区溢出问题。

谢谢,这让事情变得清晰了很多,所以 'ostringstream' 每次创建时都使用相同的内存位置?因为它在每次迭代中重新创建。 还有一件事,你说我们会得到未定义的行为,但为什么?因为我在数组中复制了指针,对吧?循环中的变量被销毁,但是一个副本存储在 egg 数组中(结果发现它不是副本,而是相同的指针)。 - Borislav Stoilov
1
@BorislavStoilov 问题在于你的变量是一个指针。指针本身不做任何事情,只是指向另一个变量。而那个变量已经被销毁了。任何试图通过该指针访问它都会触发未定义行为。 - Revolver_Ocelot
1
@BorislavStoilov 编译器需要为循环的每个迭代分配ostringstream对象的空间,但是为什么要浪费宝贵的周期呢?当它可以在每次迭代中重复使用相同的空间时。因此,是的,它只是在每次迭代中重复使用相同的空间,并调用构造函数/析构函数。 - Some programmer dude
@BorislavStoilov 关于UB,你复制的是指针而不是内容(在原始的“非固定”代码中),这就是问题所在。指针就像它听起来的那样,是指向某些内存的指针,如果该内存不存在,则指针将不再有效。无论你有多少个指针副本,它们都指向同一块内存。 - Some programmer dude

2
在第一种情况下,你复制了指向字符串的指针。
在第二种情况下,使用strcpy(),你实际上是深度复制了这个字符串。

好的,我没有详细解释,让我澄清一下。在第一种情况下,你复制了指向用ostringstream创建的字符串的指针。当它超出范围时会发生什么?
未定义行为

但指针不是完全不同的吗? 在循环“Egg egg(os.str().c_str());”中,每次都会创建一个新的Egg吗? - Borislav Stoilov
@BorislavStoilov 我更新了,抱歉。是的,但由于您遇到了UB,我们无法确定行为如何。我猜测该函数重用其内存,因此最新的副本仍然存在。顺便问一下,好问题,+1。 - gsamaras

1

os.str() 是一个类型为 std::string 的匿名临时对象,一旦该匿名临时对象超出作用域(即语句结束),访问由 .c_str() 指向的内存的行为是未定义的。你的第二个情况可以工作,因为 strcpy(this->name, name); 在临时对象超出作用域之前复制了由 .c_str() 指向的数据。但是代码仍然很脆弱:固定大小的字符缓冲区容易溢出。(一个微不足道的修复方法是使用 strncpy)。

但是要正确修复,利用 C++ 标准库:将 name 的类型设为 std::string,将 getName 的返回类型设为 const std::string&,并使用像 std::list<Egg> 这样的容器来保存篮子中的鸡蛋。


0

在你的Egg构造函数中,你没有复制字符串,只是一个指针,它是字符串的起始地址。

发生了这样的事情,你的所有ostrings实例都在同一个地方分配它们的缓冲区,一次又一次。而且,在构造for循环和输出打印for循环之间,缓冲区没有被覆盖。

这就是为什么最终所有的Egg都有它们的name指针指向同一个位置,而那个位置包含了最后一个构建的名称。


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