6

我缺乏 C++ 经验,或者更确切地说,我在垃圾收集语言方面的早期学习现在真的让我很痛苦,我在使用 C++ 中的字符串时遇到了问题。

说得很清楚,使用 std::string 或 equlivents 不是一种选择——这一直是 char* 。

所以:我需要做的非常简单,基本上可以归结为连接字符串。在运行时,我有 2 个类。

一个类包含基本文件名形式的“类型”信息。

在标题中:

char* mBaseName;

后来,在 .cpp 中,它加载了从其他地方传入的信息。

mBaseName = attributes->BaseName;

第二个类以基本文件名后缀的形式提供版本信息,它是一个静态类,目前实现如下:

static const char* const suffixes[] = {"Version1", "Version", "Version3"}; //etc.

static char* GetSuffix()
{
    int i = 0;
    //perform checks on some data structures
    i = somevalue;
   return suffixes[i];
}

然后,在运行时基类创建它需要的文件名:

void LoadStuff()
{
    char* suffix = GetSuffix();
    char* nameToUse = new char[50];
    sprintf(nameToUse, "%s%s",mBaseName,suffix);

    LoadAndSetupData(nameToUse);
}

您可以立即看到问题。nameToUse 永远不会被删除,内存泄漏。

后缀是一个固定列表,但基本文件名是任意的。创建的名称需要在“LoadStuff()”的末尾持续存在,因为不清楚随后何时以及如何使用它。

我可能担心太多,或者很愚蠢,但是类似于 LoadStuff() 的代码也发生在其他地方,所以需要解决。这很令人沮丧,因为我对事物的工作方式知之甚少,无法看到一个安全且“非黑客”的解决方案。在 C# 中,我只想写:

LoadAndSetupData(mBaseName + GetSuffix());

并且不需要担心。

非常感谢任何意见、建议或建议。

更新

我调用 LoadAndSetupData() 的代码的问题在于,在某些时候它可能会复制文件名并将其保存在本地,但实际的实例化是异步的,LoadAndSetupData 实际上将事物放入队列中,至少在那时,它期望传入的字符串仍然存在。

我不控制此代码,因此无法更新它的功能。

4

13 回答 13

3

Seeing now that the issue is how to clean up the string that you created and passed to LoadAndSetUpData()

I am assuming that:

  1. LoadAndSetUpData() does not make its own copy
  2. You can't change LoadAndSetUpData() to do that
  3. You need the string to still exist for some time after LoadAndSetupData() returns

Here are suggestions:

  1. Can you make your own queue objects to be called? Are they guaranteed to be called after the ones that use your string. If so, create cleanup queue events with the same string that call delete[] on them

  2. Is there a maximum number you can count on. If you created a large array of strings, could you use them in a cycle and be assured that when you got back to the beginning, it would be ok to reuse that string

  3. Is there an amount of time you can count on? If so, register them for deletion somewhere and check that after some time.

The best thing would be for functions that take char* to take ownership or copy. Shared ownership is the hardest thing to do without reference counting or garbage collection.

于 2008-11-10T16:56:44.570 回答
3

The thing to remember with C++ memory management is ownership. If the LoadAndSetupData data is not going to take ownership of the string, then it's still your responsibility. Since you can't delete it immediately (because of the asynchronicity issue), you're going to have to hold on to those pointers until such time as you know you can delete them.

Maintain a pool of strings that you have created:

  • If you have some point in time where you know that the queue has been completely dealt with, you can simply delete all the strings in the pool.
  • If you know that all strings created after a certain point in time have been dealt with, then keep track of when the strings were created, and you can delete that subset. - If you can somehow find out when an individual string has been dealt with, then just delete that string.

class StringPool
{
    struct StringReference {
        char *buffer;
        time_t created;
    } *Pool;

    size_t PoolSize;
    size_t Allocated;

    static const size_t INITIAL_SIZE = 100;

    void GrowBuffer()
    {
        StringReference *newPool = new StringReference[PoolSize * 2];
        for (size_t i = 0; i < Allocated; ++i)
            newPool[i] = Pool[i];
        StringReference *oldPool = Pool;
        Pool = newPool;
        delete[] oldPool;
    }

public:

    StringPool() : Pool(new StringReference[INITIAL_SIZE]), PoolSize(INITIAL_SIZE)
    {
    }

    ~StringPool()
    {
        ClearPool();
        delete[] Pool;
    }

    char *GetBuffer(size_t size)
    {
        if (Allocated == PoolSize)
            GrowBuffer();
        Pool[Allocated].buffer = new char[size];
        Pool[Allocated].buffer = time(NULL);
        ++Allocated;
    }

    void ClearPool()
    {
        for (size_t i = 0; i < Allocated; ++i)
            delete[] Pool[i].buffer;
        Allocated = 0;
    }

    void ClearBefore(time_t knownCleared)
    {
        size_t newAllocated = 0;
        for (size_t i = 0; i < Allocated; ++i)
        {
            if (Pool[i].created < knownCleared)
            {
                delete[] Pool[i].buffer;
            }
            else
            {
                Pool[newAllocated] = Pool[i];
                ++newAllocated;
            }
        }
        Allocated = newAllocated;
    }

    // This compares pointers, not strings!
    void ReleaseBuffer(char *knownCleared)
    {
        size_t newAllocated = 0;
        for (size_t i = 0; i < Allocated; ++i)
        {
            if (Pool[i].buffer == knownCleared)
            {
                delete[] Pool[i].buffer;
            }
            else
            {
                Pool[newAllocated] = Pool[i];
                ++newAllocated;
            }
        }
        Allocated = newAllocated;
    }

};
于 2008-11-10T16:57:27.297 回答
3

编辑:这个答案并没有完全解决他的问题——我在这里提出了其他建议: C++ 字符串操作

他的问题是他需要将他创建的 char* 的范围扩展到函数之外,直到异步作业完成。

原答案:

在 C++ 中,如果我不能使用标准库或 Boost,我仍然有一个这样的类:

template<class T>
class ArrayGuard {
  public:
    ArrayGuard(T* ptr) { _ptr = ptr; }
    ~ArrayGuard() { delete[] _ptr; }
  private:
    T* _ptr;
    ArrayGuard(const ArrayGuard&);
    ArrayGuard& operator=(const ArrayGuard&);
}

你像这样使用它:

char* buffer = new char[50];
ArrayGuard<char *> bufferGuard(buffer);

缓冲区将在作用域结束时被删除(返回或抛出)。

对于动态大小的数组的简单数组删除,我希望将其视为在作用域结束时释放的静态大小的数组。

保持简单——如果您需要更高级的智能指针,请使用 Boost。

如果您的示例中的 50 是可变的,这将很有用。

于 2008-11-10T16:44:14.237 回答
2

If you must use char*'s, then LoadAndSetupData() should explicitly document who owns the memory for the char* after the call. You can do one of two things:

  1. Copy the string. This is probably the simplest thing. LoadAndSetupData copies the string into some internal buffer, and the caller is always responsible for the memory.

  2. Transfer ownership. LoadAndSetupData() documents that it will be responsible for eventually freeing the memory for the char*. The caller doesn't need to worry about freeing the memory.

I generally prefer safe copying as in #1, because the allocator of the string is also responsible for freeing it. If you go with #2, the allocator has to remember NOT to free things, and memory management happens in two places, which I find harder to maintain. In either case, it's a matter of explicitly documenting the policy so that the caller knows what to expect.

If you go with #1, take a look at Lou Franco's answer to see how you might allocate a char[] in an exception-safe, sure to be freed way using a guard class. Note that you can't (safely) use std::auto_ptr for arrays.

于 2008-11-10T16:52:14.780 回答
2

由于 std::string 不是一个选项,无论出于何种原因,您是否研究过智能指针?见提升

但我只能鼓励你使用 std::string。

基督教

于 2008-11-10T16:41:20.013 回答
2

由于您需要 nameToUse 在函数之后仍然存在,因此您无法使用 new,我要做的是返回一个指向它的指针,因此调用者可以在以后不再需要它时“删除”它。

char * LoadStuff()
{
    char* suffix = GetSuffix();
    char* nameToUse = new char[50];
    sprintf("%s%s",mBaseName,suffix);

    LoadAndSetupData(nameToUse);
    return nameToUse;
}

然后:

char *name = LoadStuff();
// do whatever you need to do:
delete [] name;
于 2008-11-10T16:41:53.663 回答
1

There is no need to allocate on heap in this case. And always use snprintf:

char nameToUse[50];
snprintf(nameToUse, sizeof(nameToUse), "%s%s",mBaseName,suffix);
于 2008-11-10T16:58:42.397 回答
0

First: Why do you need for the allocated string to persist beyond the end of LoadStuff()? Is there a way you can refactor to remove that requirement.

Since C++ doesn't provide a straightforward way to do this kind of stuff, most programming environments use a set of guidelines about pointers to prevent delete/free problems. Since things can only be allocated/freed once, it needs to be very clear who "owns" the pointer. Some sample guidelines:

1) Usually the person that allocates the string is the owner, and is also responsible for freeing the string.

2) If you need to free in a different function/class than you allocated in, there must be an explicit hand-off of ownership to another class/function.

3) Unless explicitly stated otherwise, pointers (including strings) belong to the caller. A function, constructor, etc. cannot assume that the string pointer it gets will persist beyond the end of the function call. If they need a persistent copy of the pointer, they should make a local copy with strdup().

What this boils down to in your specific case is that LoadStuff() should delete[] nameToUse, and the function that it calls should make a local copy.

One alternate solution: if nameToUse is going to be passed lots of places and needs to persist for the lifetime of the program, you could make it a global variable. (This saves the trouble of making lots of copies of it.) If you don't want to pollute your global namespace, you could just declare it static local to the function:

static char *nameToUse = new char[50];
于 2008-11-10T16:49:36.503 回答
0

你可以在这里结合一些想法。

根据您模块化应用程序的方式,可能有一个方法(main?),其执行决定了 nameToUse 可定义为固定大小局部变量的范围。您可以将指针(&nameToUse[0] 或简单地 nameToUse)传递给需要填充它的其他方法(因此也传递大小)或使用它,知道当具有局部变量的函数退出或您的程序以任何其他方式终止。

这与使用动态分配和删除几乎没有区别(因为保存该位置的指针必须或多或少以相同的方式进行管理)。在许多情况下,本地分配更直接,并且在将最大所需生存期与特定函数执行的持续时间相关联没有问题时非常便宜。

于 2008-11-10T19:09:41.837 回答
0

在 LoadStuff 的范围之外,究竟在哪里使用了 nameToUse?如果有人在 LoadStuff 之后需要它,它需要传递它,以及内存释放的责任

如果你会按照你的建议在 c# 中完成它

LoadAndSetupData(mBaseName + GetSuffix()); 

那么什么都不会引用 LoadAndSetupData 的参数,因此您可以安全地将其更改为

char nameToUse[50];

正如马丁建议的那样。

于 2008-11-10T16:42:27.360 回答
0

您将不得不管理为 nameToUse 分配的内存的生命周期。将它包装在诸如 std::string 之类的类中会使您的生活更简单一些。

我想这是一个小小的愤怒,但由于我想不出任何更好的解决方案来解决您的问题,我会指出另一个潜在的问题。在复制或连接字符串时,您需要非常小心地检查正在写入的缓冲区的大小。strcat、strcpy 和 sprintf 等函数很容易覆盖其目标缓冲区的末尾,从而导致虚假的运行时错误和安全漏洞。

抱歉,我自己的经验主要是在 Windows 平台上,他们引入了这些函数的“安全”版本,称为 strcat_s、strcpy_s 和 sprintf_s。它们的许多相关功能也是如此。

于 2008-11-10T16:46:56.197 回答
0

谢谢大家的回答。我没有选择一个作为“答案”,因为这个问题没有具体的解决方案,而且无论如何最好的讨论都是我和其他人。

您的建议都很好,您对我的问题的笨拙非常耐心。我相信你可以看到,这是一个更复杂问题的简化,还有更多与我给出的示例有关的事情,因此它的一些部分可能并不完全有意义。

为了您的利益,我决定暂时“作弊”以摆脱困境。我说基本名称是任意的,但这并不完全正确。事实上,它们也是一组有限的名称,只是一组可能会在某些时候改变的有限名称,所以我试图解决一个更普遍的问题。

现在我将把“静态”解决方案扩展到后缀并建立一个可能的名称表。这非常“hacky”,但会起作用,而且可以避免重构大量我无法做到的复杂代码。

反馈非常好,非常感谢。

于 2008-11-10T17:35:26.287 回答
-1

我并不完全清楚 LoadAndSetupData 的定义位置,但看起来它保留了自己的字符串副本。因此,您应该在调用 LoadAndSetupData 后删除本地分配的副本,并让它管理自己的副本。

或者,确保 LoadAndSetupData 清理你给它分配的 char[]。

我的偏好是让另一个函数保留自己的副本并管理它,这样您就不会为另一个类分配对象。

编辑:由于您使用具有固定大小 [50] 的 new,因此您不妨按照建议将其设为本地,并让 LoadAndSetupData 制作自己的副本。

于 2008-11-10T16:41:35.270 回答