C函数中返回字符数组

291 阅读4分钟

内容简介

在阅读本篇文章之前,建议大家看一下下面2篇文章:

这篇文章主要分享三个点:

1、为什么作为局部变量的字符数组不能直接返回,而字符指针却可以?

2、当字符数组是局部变量的时候,函数如何返回它?

3、字符数组(char [])和字符指针(char *)如何互转?

局部变量的字符数组

在C中如果我们直接返回字符数组,编译会直接报警告。如下示例:

char * fork_user_name()
{
    char name[] = "veryitman";
    return name;
}

在Xcode中编译警告信息是这样的:

Address of stack memory associated with local variable 'name' returned

在Linux上面GCC编译显示警告是这样的:

warning: function returns address of local variable [-Wreturn-local-addr]

无论哪种警告信息,基本意思都是告诉我们不应该返回一个局部变量 name 的地址(函数内部的变量在栈内存上)。

如果我们修改一下代码,将 char 改为指针变量 char * ,示例如下:

char * fork_user_name2()
{
    char *name = "veryitman";
    return name;
}

无论是Linux的GCC还是Xcode的Clang编译器都不会报出警告。

首先我们要知道,常量是放在数据段里面的。

这里比较特殊,局部变量 name 保存在栈中,但是字符串 veryitman 的值是一个常量,保存在常量区。即便函数返回了,数据段里面的常量数据也还不会消亡,它会直到程序结束才会消失,其内存空间直到程序运行结束才会被释放。 所以,返回的地址是一个实际存在的有效地址。

char * fork_user_name()
{
    char name[] = "veryitman";
    return name;
}

char * fork_user_name2()
{
    char *name = "veryitman";
    return name;
}

int main()
{
    printf("fork_user_name: %s\n", fork_user_name());
    printf("fork_user_name2: %s\n", fork_user_name2());
    
    return 0;
}

用GCC编译、运行后的打印结果,如下:

fork_user_name: (null)
fork_user_name2: veryitman

总之,在函数中的局部变量只要是返回类似 int[]char[]long[] 地址的,都是不正确的做法。

一切皆有可能

下面例子是不正确的,如下:

char * v_string()
{
    char rest[10] = {'\0'};
    return rest;
}

1、使用 static

在C语言中,用 static 限定外部变量与函数,该外部变量或者函数除了对该所在的文件可见外,其他文件都无法访问。 而用 static 声明内部变量,则该变量是某个特定函数的局部变量,只能在该函数中使用。但它与自动变量不同的是,不管其所在函数是否被调用,它一直存在,而不像自动变量那样,随着所在函数的被调用和退出而存在和消失。换句话说,static 类型的内部变量是一种只能在某个特定函数中使用但一直占据存储空间的变量。

所以使用static修饰一下,就没有问题了。示例如下:

char * v_string()
{
    static char rest[10] = {'\0'};
    return rest;
}

2、使用 malloc

这种方式可以解决这个问题,是因为使用 malloc 分配的内存是在堆上而不是在栈内存上面。但是要记得将其在调用方使用 free 释放申请的内存空间,否则容易造成内存泄漏问题。

具体可以看看 双宿双飞的 malloc 和 free 这篇文章。

char * v_string()
{
    char *p = (char *)malloc(10 * sizeof(char));
    p = "\0";
    return p;
}

3、全局变量

这个很好理解。全局变量在程序真个生命周期中都是有效的,所以使用全局变量也可以解决类似问题。

但是这种方案就会让这个封装的方法不够内聚,因为它依赖了全局变量。

char g_rest[100];
char * v_string()
{
    strcpy(g_rest, "verytiamn");
    return g_rest;
}

4、返回形参指针变量

在Linux Kernel(内核源码版本5.0.7)中,函数 strcpy 的实现如下:

#ifndef __HAVE_ARCH_STRCPY
/**
 * strcpy - Copy a %NUL terminated string
 * @dest: Where to copy the string to
 * @src: Where to copy the string from
 */
#undef strcpy
char *strcpy(char *dest, const char *src)
{
	char *tmp = dest;

	while ((*dest++ = *src++) != '\0')
		/* nothing */;
	return tmp;
}
EXPORT_SYMBOL(strcpy);
#endif

参考内核实现,我们可以修改一下自己的代码,示例如下:

char * v_string(char *s1, char *s2)
{
    char *tmp = s1;
    // 省略...
    return tmp;
}

这里补充另外一个知识点,函数 strcpy 在glibc和Linux Kernel中实现不一样。

在glibc的新版中(2.29版本),本质是调用了函数 memcpy, 实现如下:

#include <stddef.h>
#include <string.h>

#undef strcpy

#ifndef STRCPY
# define STRCPY strcpy
#endif

/* Copy SRC to DEST.  */
char * STRCPY (char *dest, const char *src)
{
  return memcpy (dest, src, strlen (src) + 1);
}
libc_hidden_builtin_def (strcpy)

包括 strncpy 在glibc和Linux Kernel中实现也不一样,有兴趣的可以去看看源码。

字符数组和字符指针的互转

char [] 转 char *

这种情况下,可以直接进行赋值,示例如下:

int main()
{
	char c_str_array[] = "veryitman.com";
    char *p_str;
    p_str = c_str_array;
    printf("p_str: %s\n", p_str);
	return 0;
}

char * 转 char []

是不是也可以直接进行赋值呢?撸段代码看看,如下:

int main()
{
	char c_str_array[] = "veryitman.com";
    char *p_str = "veryitman.com";
    c_str_array = p_str;
    printf("c_str_array: %s\n", c_str_array);
	return 0;
}

很遗憾,编译报错,GCC编译错误截图如下: [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fzeCmR8f-1581588023962)(www.veryitman.com/upload/imag…)]

Clang编译错误如下:

Array type 'char [14]' is not assignable

可以考虑使用 strncpy 来实现,示例代码如下:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

int main()
{
    char c_str_array[] = "veryitman.com";
    char *p_str = "veryitman.com";
    strncpy(c_str_array, p_str, strlen(p_str));
    printf("c_str_array: %s\n", c_str_array);
    return 0
}

时间可以改变一切,但你总得做点什么!