避免在修改C字符串时出现内存泄漏

3
为了教育目的,在一些测试程序中我使用cstrings。 我想用“...”这样的占位符来缩短字符串。

也就是说,如果我的最大长度为13,则"Quite a long string"将变成"Quite a lo..."。此外,我不想破坏原始字符串-因此缩短的字符串必须是一个副本。

下面是我想出的(静态)方法。我的问题是:分配缩短字符串的内存的类是否也应负责释放它?现在我所做的是将返回的字符串存储在单独的“用户类”中,并将释放内存推迟到该用户类中。

const char* TextHelper::shortenWithPlaceholder(const char* text, size_t newSize) {
    char* shortened = new char[newSize+1];

    if (newSize <= 3) {
        strncpy_s(shortened, newSize+1, ".", newSize);
    }
    else {
        strncpy_s(shortened, newSize+1, text, newSize-3);
        strncat_s(shortened, newSize+1, "...", 3);  
    }
    return shortened;
}

2
既然你正在使用C++,为什么不使用std::string作为返回类型呢?这样内存管理会更简单。 - Don Wakefield
我没有在 OP 中看到任何必须只使用 C 字符串的要求。在我看来,@Don 的问题是合理的。 - jalf
我知道有std :: string。但是我喜欢纠结于C语言的细节;) 这只是为了个人目的。 - mats
1
strncpy等并不是C语言的“精华部分”。您应该看看libowfat,以了解C语言的真正精华部分。如果您个人不喜欢C++的基本改进,那么您的整个问题都注定要失败。您应该重新标记它,因为它不再是关于C++的问题。如果您想在C中看到问题的良好解决方案,请查看libowfat:http://www.fefe.de/libowfat/ - vog
@vog 重新标记了问题。顺便说一句,“好细的位”是讽刺的意思 - 不管怎样,感谢提供链接。 - mats
8个回答

6

像这样的函数通常采用用户传入一个char[]缓冲区的标准方法。例如,sprintf()函数就使用了这种方式,它将目标缓冲区作为参数传入。这使得调用者负责分配和释放内存,从而将整个内存管理问题集中在一个地方。


1
这是C语言的标准方法。然而,在C++中,标准方法是使用std::string。 - vog
我应该提到我使用了 pdcurses。C++ 部分用于我想为 pdcurses 库创建一些类似面向对象的“小部件”。因为你“提醒”我 C 方法,所以加 1 分。 - mats

5
为了避免缓冲区溢出和内存泄漏,您应该始终使用C++类,例如在这种情况下使用std::string
只有最后一个实例应将类转换为低级别的东西,例如char*。这将使您的代码简单且安全。只需将代码更改为:
std::string TextHelper::shortenWithPlaceholder(const std::string& text,
                                               size_t newSize) {
    return text.substr(0, newSize-3) + "...";
}

在使用 C 环境下的该函数时,您只需使用 cstr() 方法:
some_c_function(shortenWithPlaceholder("abcde", 4).c_str());

就算你已经会了C语言,也不要以相同的方式去编写C++程序。更适合的方法是将C++视为一种完全不同的语言。

That's all!


2

实际上,你应该尽可能使用std::string,但如果必须使用,请参考现有库的用法指南。

在C标准库中,最接近你所做的操作的函数是

char * strncpy ( char * destination, const char * source, size_t num );

所以我会选择这个方案:
const char* TextHelper::shortenWithPlaceholder(
    char * destination, 
    const char * source, 
    size_t newSize);

调用者负责内存管理 - 这使得调用者可以使用栈、堆、内存映射文件或其他任何来源来保存数据。您不需要记录使用new[]来分配内存,调用者也不需要知道使用delete[]而不是freedelete,甚至不需要知道更低级别的操作系统调用。将内存管理留给调用者更加灵活,更少出错。
返回指向目标的指针只是一种便利,使您可以像这样做:
char buffer[13];
printf("%s", TextHelper::shortenWithPlaceholder(buffer, source, 12));

事实上,正是这种细节使我采用了我在问题中展示的方法。在我看来,拥有这些细节并不像 C 语言那样。 - mats

2

我从未喜欢过返回指向本地分配内存的指针。我喜欢对调用我的函数进行清理方面的健康怀疑。

相反,你考虑过接受一个缓冲区,将缩短后的字符串复制到其中吗?

例如:

const char* TextHelper::shortenWithPlaceholder(const char* text, 
                                               size_t textSize, 
                                               char* short_text, 
                                               size_t shortSize)

其中 short_text 是要复制的缩短字符串的缓冲区,shortSize 是提供的缓冲区大小。为方便调用者,您还可以继续返回指向 short_textconst char*(如果 shortSize 不够大则返回 NULL)。


1
虽然这种策略避免了内存泄漏,但如果调用者不小心,它会存在缓冲区溢出的危险,这比内存泄漏更糟糕。在C++中,没有必要冒这个风险。只需使用std::string即可。 - vog
函数原型加上+1。我猜,将short_text和text的位置反转为shortenWithPlaceholder(char* short_text, size_t shortSize, const char* text, size_t textSize)可能更像string.h函数。 - mats
@vog:我理解这个问题是基于“教育目的……”的。 - Alan
@Alan:我认为为了“教育目的”而冒着缓冲区溢出的风险也是一个不好的主意。你的情况可能有所不同。 - vog

1
最灵活的方法是返回一个包装分配内存的辅助对象,这样调用者就不必担心它。该类存储指向内存的指针,并具有复制构造函数、赋值运算符和析构函数。
class string_wrapper
{
    char *p;

public:
    string_wrapper(char *_p) : p(_p) { }
    ~string_wrapper() { delete[] p; }

    const char *c_str() { return p; }

    // also copy ctor, assignment
};

// function declaration
string_wrapper TextHelper::shortenWithPlaceholder(const char* text, size_t newSize)
{
    // allocate string buffer 'p' somehow...

    return string_wrapper(p);
}

// caller
string_wrapper shortened = TextHelper::shortenWithPlaceholder("Something too long", 5);

std::cout << shortened.c_str();

大多数真实的程序使用std::string来实现此目的。


我的原始答案只是“使用std :: string”,但问题确实说这是为教育目的。 - Daniel Earwicker
1
“一个包装分配内存的辅助对象”是std::string的恰当描述! - Daniel Earwicker

0
在您的示例中,调用方别无选择,只能负责释放已分配的内存。
然而,这种习惯用法容易出错,我不建议使用它。
一种替代方法是将 shortened 更改为引用计数指针,并使该方法返回该引用计数指针而不是裸指针,从而允许您使用几乎相同的代码。

0

编辑:不,我错了。我误解了你想做什么。调用者必须删除您的实例中的内存。

C++标准规定,删除0 / NULL不会执行任何操作(换句话说,这是安全的),因此您可以删除它,无论您是否调用了该函数。编辑:我不知道为什么会漏掉这个...您的另一种选择是放置删除。在这种情况下,即使形式不好,您也应该使用放置new来保持分配/释放在同一位置(否则不一致性会使调试变得荒谬)。

话虽如此,您如何使用代码?我不知道您何时会多次调用它,但如果确实如此,则可能存在内存泄漏(我认为)如果您不记住每个不同的内存块。

我只会使用std::auto_ptrBoost :: shared_ptr。它会在退出时自动删除并可与char *一起使用。

考虑到TextHelper的分配方式,您还可以做另一件事。这是一个理论上的构造函数:

TextHelper(const char* input) : input_(input), copy(0) { copy = new char[sizeof(input)/sizeof(char)]; //mess with later }
~TextHelper() { delete copy; }

我使用的函数在TextHelper中是静态的。这只会使事情更像C语言风格。我应该在我的问题中省略任何C++参考,但已经造成了影响。 - mats

0

我认为有两种基本方式同样常见: a)TextHelper返回c字符串并忘记它。用户必须删除内存。 b)TextHelper维护已分配字符串的列表,并在销毁时释放它们。

现在这取决于您的使用模式。对我来说,b)似乎很冒险:如果TextHelper必须释放字符串,则在用户完成使用缩短字符串之前不应该这样做。您可能不知道这一点何时到来,因此您保持TextHelper处于活动状态,直到程序终止。这导致内存使用模式等同于内存泄漏。我只建议b),如果字符串在语义上属于提供它们的类,类似于std :: string :: c_str()。您的TextHelper看起来更像是一个工具箱,不应与处理的字符串相关联,因此如果我必须在两者之间选择,我会选择a)。鉴于固定的TextHelper接口,您的用户类可能是最佳解决方案。


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