关于函数返回的 char 指针的最佳实践
best practice regarding char pointers returned by functions
处理 return malloc 指向 C 字符串的函数时,最佳做法是什么?
这是一个例子:
FILE *f;
char *tmp;
for (i = 0; i <= 10; i++) {
tmp = concat(fn, i);
f = fopen(tmp, "w");
free(tmp);
// read f, do something useful ...
fclose(f);
}
char* concat(char *a, int b)
returns 指向包含 a
和 b
的串联的新 C 字符串的指针。
我不仅必须指定一个临时指针然后传递给 fopen
,我还必须每次都 free(tmp)
。我宁愿喜欢这样的东西:
FILE *f;
char *tmp;
for (i = 0; i <= 10; i++) {
f = fopen(concat(fn, i), "w");
// read f, do something useful ...
fclose(f);
}
但这当然会导致内存泄漏。那么这里的最佳实践是什么?像 concat(char *a, int b, char *result)
这样的结果应该是为生成的 C 字符串预分配的内存?此解决方案有其缺点,例如 result
.
的大小有限或不是最佳大小
你的第一个代码片段,你在其中保存返回的指针并free
它,当你使用它时,是使用函数返回 malloc'ed 内存的正确方法。
有几个 POSIX 函数,例如 strdup
和 getline
,以这种方式工作,所以这是一个众所周知的习惯用法。
备选方案是:
- Return 指向静态缓冲区的指针。这样做的缺点是它不是线程安全的,也不能在一个表达式中调用两次。
- 接受一个指向适当大小缓冲区的指针,在这种情况下,适当大小缓冲区由调用者决定。
这两种方法都在行业中使用。在您的示例中,人们可以假设生成的文件名的最大大小并以这种方式使用本地数组:
for (int i = 0; i <= 10; i++) {
char filename[1024];
snprintf(filename, sizeof filename, "%s%d", fn. i);
FILE *f = fopen(filename, "w");
if (f != NULL) {
// read f, do something useful ...
fclose(f);
} else {
// report the error?
}
}
请注意,可以使用 if (snprintf(filename, sizeof filename, "%s%d", fn. i) >= (int)sizeof filename)
检测截断。
如果不对文件名长度做假设或者文件名组成方法比较复杂,返回分配的字符串可能是更合适的选择,但也应该进行内存分配错误测试:
for (int i = 0; i <= 10; i++) {
char *filename = concat(fn, i);
if (filename == NULL) {
/* handle the error */
...
// break / continue / return -1 / exit(1) ...
}
FILE *f = fopen(filename, "w");
if (f == NULL) {
/* report this error, using `filename` for an informative message */
} else {
// read f, do something useful...
// keep `filename` available for other reporting
fclose(f);
}
free(filename);
}
如果您还没有准备好执行所有这些簿记,您可能应该使用具有更精细的对象生命周期管理或垃圾收集器的不同语言。
最后,使用 C99 复合文字,您可以定义 concat
以适合您的简化用例:
char *concat(char *dest, const char *s, int b) {
sprintf(dest, "%s%d", s, b);
return dest;
}
#define CONCAT(a, b) concat((char[strlen(a) + 24]){""}, a, b)
CONCAT
定义了一个适当大小的未命名局部变量 length char
数组,并将字符串 a
和 int b
的串联构造到其中。我将大小写更改为大写以强调 a
在扩展中被计算两次的事实,因此不应该是涉及副作用的表达式。
您可以在第二个代码片段中按预期使用此宏:
FILE *f;
char *tmp;
for (i = 0; i <= 10; i++) {
f = fopen(CONCAT(fn, i), "w");
// read f, do something useful ...
fclose(f);
}
我可能不会推荐这种用法,但这只是我的意见。
如果您知道字符串的最大长度,您也可以这样做:
char* concat_(char* buf, size_t s, char *a, int b)
{
/* ... your code ... */
return buf;
}
#define concat(a, b) concat_((char[100]){""}, 100, (a), (b))
宏分配一个未命名的局部变量并将其传递给 concat_
函数。然后这个函数可以做它以前做的任何事情,只是 return 指向同一个对象的指针。没有 malloc
,没有 free
,不用担心(如果缓冲区太大,可能会炸毁堆栈)。
编辑:
请注意,正如 Gerhardh 在评论中指出的那样,这会创建一个 local 对象(具有自动存储持续时间),因此指针 returned 由宏仅在实际调用该宏的同一块内有效。因此,您不能将该指针用作调用宏的函数的 return 值。
更坚固的设计:
FILE *open_file_number(const char *str, int number)
{
size_t size = strlen(str) + 32;
char *path = malloc(size);
if (path == NULL)
{
return NULL;
}
snprintf(path, size, "%s%d", str, number);
FILE *file = fopen(path, "w");
free(path);
return file;
}
for (i = 0; i <= 10; i++)
{
FILE *file = open_file_number(some_path, i);
if (file != NULL)
{
// Do your stuff
fclose(file);
}
}
What is best practice when dealing with functions which return malloc'd pointers to C-strings?
最佳做法:不要使用它们。几乎可以肯定,期望调用者 free
返回数据的库设计不当,只有极少数例外。我们从 40 年的 C 语言历史中了解到这一点,糟糕的编写库已经造成了数以百万计的内存泄漏错误。
理智、有用的库 API 设计的基本规则是:
谁分配谁负责收拾自己的烂摊子
由于 C 没有 RAII 或 constructors/destructors,不幸的是,这意味着 sane 库需要为您提供清理功能,您需要记住调用它。如果它不提供这样的功能,您可能需要考虑编写包装函数来执行此操作 - 为它们纠正错误的库设计。
如果您是实现库的人,则应尽可能将内存分配留给调用者。传统上,这是通过函数获取指向它写入的缓冲区的指针来完成的。然后将它留给调用者分配足够的内存(如 strcpy
/strcat
),或者提供一个具有最大缓冲区大小的变量,之后函数 returns 缓冲区的大小它实际使用(如 fgets
)。
在您的示例中,设计合理的 concat
可能看起来像
const char* concat (char* restrict dst, const char* restrict src, int i);
其中 src
是源字符串,i
是要添加的整数,dst
是调用者提供的足够大的缓冲区。可选地,为方便起见,函数 returns 相当于 dst
的指针。上面还实现了适当的 const 正确性 加上带有 restrict 的微优化,这意味着传递的指针不允许重叠。
用法:
char buf [LARGE_ENOUGH];
fp = fopen(concat(buf, foo, i), "w");
处理 return malloc 指向 C 字符串的函数时,最佳做法是什么?
这是一个例子:
FILE *f;
char *tmp;
for (i = 0; i <= 10; i++) {
tmp = concat(fn, i);
f = fopen(tmp, "w");
free(tmp);
// read f, do something useful ...
fclose(f);
}
char* concat(char *a, int b)
returns 指向包含 a
和 b
的串联的新 C 字符串的指针。
我不仅必须指定一个临时指针然后传递给 fopen
,我还必须每次都 free(tmp)
。我宁愿喜欢这样的东西:
FILE *f;
char *tmp;
for (i = 0; i <= 10; i++) {
f = fopen(concat(fn, i), "w");
// read f, do something useful ...
fclose(f);
}
但这当然会导致内存泄漏。那么这里的最佳实践是什么?像 concat(char *a, int b, char *result)
这样的结果应该是为生成的 C 字符串预分配的内存?此解决方案有其缺点,例如 result
.
你的第一个代码片段,你在其中保存返回的指针并free
它,当你使用它时,是使用函数返回 malloc'ed 内存的正确方法。
有几个 POSIX 函数,例如 strdup
和 getline
,以这种方式工作,所以这是一个众所周知的习惯用法。
备选方案是:
- Return 指向静态缓冲区的指针。这样做的缺点是它不是线程安全的,也不能在一个表达式中调用两次。
- 接受一个指向适当大小缓冲区的指针,在这种情况下,适当大小缓冲区由调用者决定。
这两种方法都在行业中使用。在您的示例中,人们可以假设生成的文件名的最大大小并以这种方式使用本地数组:
for (int i = 0; i <= 10; i++) {
char filename[1024];
snprintf(filename, sizeof filename, "%s%d", fn. i);
FILE *f = fopen(filename, "w");
if (f != NULL) {
// read f, do something useful ...
fclose(f);
} else {
// report the error?
}
}
请注意,可以使用 if (snprintf(filename, sizeof filename, "%s%d", fn. i) >= (int)sizeof filename)
检测截断。
如果不对文件名长度做假设或者文件名组成方法比较复杂,返回分配的字符串可能是更合适的选择,但也应该进行内存分配错误测试:
for (int i = 0; i <= 10; i++) {
char *filename = concat(fn, i);
if (filename == NULL) {
/* handle the error */
...
// break / continue / return -1 / exit(1) ...
}
FILE *f = fopen(filename, "w");
if (f == NULL) {
/* report this error, using `filename` for an informative message */
} else {
// read f, do something useful...
// keep `filename` available for other reporting
fclose(f);
}
free(filename);
}
如果您还没有准备好执行所有这些簿记,您可能应该使用具有更精细的对象生命周期管理或垃圾收集器的不同语言。
最后,使用 C99 复合文字,您可以定义 concat
以适合您的简化用例:
char *concat(char *dest, const char *s, int b) {
sprintf(dest, "%s%d", s, b);
return dest;
}
#define CONCAT(a, b) concat((char[strlen(a) + 24]){""}, a, b)
CONCAT
定义了一个适当大小的未命名局部变量 length char
数组,并将字符串 a
和 int b
的串联构造到其中。我将大小写更改为大写以强调 a
在扩展中被计算两次的事实,因此不应该是涉及副作用的表达式。
您可以在第二个代码片段中按预期使用此宏:
FILE *f;
char *tmp;
for (i = 0; i <= 10; i++) {
f = fopen(CONCAT(fn, i), "w");
// read f, do something useful ...
fclose(f);
}
我可能不会推荐这种用法,但这只是我的意见。
如果您知道字符串的最大长度,您也可以这样做:
char* concat_(char* buf, size_t s, char *a, int b)
{
/* ... your code ... */
return buf;
}
#define concat(a, b) concat_((char[100]){""}, 100, (a), (b))
宏分配一个未命名的局部变量并将其传递给 concat_
函数。然后这个函数可以做它以前做的任何事情,只是 return 指向同一个对象的指针。没有 malloc
,没有 free
,不用担心(如果缓冲区太大,可能会炸毁堆栈)。
编辑: 请注意,正如 Gerhardh 在评论中指出的那样,这会创建一个 local 对象(具有自动存储持续时间),因此指针 returned 由宏仅在实际调用该宏的同一块内有效。因此,您不能将该指针用作调用宏的函数的 return 值。
更坚固的设计:
FILE *open_file_number(const char *str, int number)
{
size_t size = strlen(str) + 32;
char *path = malloc(size);
if (path == NULL)
{
return NULL;
}
snprintf(path, size, "%s%d", str, number);
FILE *file = fopen(path, "w");
free(path);
return file;
}
for (i = 0; i <= 10; i++)
{
FILE *file = open_file_number(some_path, i);
if (file != NULL)
{
// Do your stuff
fclose(file);
}
}
What is best practice when dealing with functions which return malloc'd pointers to C-strings?
最佳做法:不要使用它们。几乎可以肯定,期望调用者 free
返回数据的库设计不当,只有极少数例外。我们从 40 年的 C 语言历史中了解到这一点,糟糕的编写库已经造成了数以百万计的内存泄漏错误。
理智、有用的库 API 设计的基本规则是:
谁分配谁负责收拾自己的烂摊子
由于 C 没有 RAII 或 constructors/destructors,不幸的是,这意味着 sane 库需要为您提供清理功能,您需要记住调用它。如果它不提供这样的功能,您可能需要考虑编写包装函数来执行此操作 - 为它们纠正错误的库设计。
如果您是实现库的人,则应尽可能将内存分配留给调用者。传统上,这是通过函数获取指向它写入的缓冲区的指针来完成的。然后将它留给调用者分配足够的内存(如 strcpy
/strcat
),或者提供一个具有最大缓冲区大小的变量,之后函数 returns 缓冲区的大小它实际使用(如 fgets
)。
在您的示例中,设计合理的 concat
可能看起来像
const char* concat (char* restrict dst, const char* restrict src, int i);
其中 src
是源字符串,i
是要添加的整数,dst
是调用者提供的足够大的缓冲区。可选地,为方便起见,函数 returns 相当于 dst
的指针。上面还实现了适当的 const 正确性 加上带有 restrict 的微优化,这意味着传递的指针不允许重叠。
用法:
char buf [LARGE_ENOUGH];
fp = fopen(concat(buf, foo, i), "w");