【问题标题】:Creating arrays in C在 C 中创建数组
【发布时间】:2014-02-14 02:46:50
【问题描述】:

我正在尝试用 C 创建一个 UNIX shell。如果它是用 Java 编写的,那将是小菜一碟,但我对 C 的经验并不丰富。C 中的数组让我有点困惑。我不确定如何声明或访问某些数据结构。

我想创建一个字符串以在每一行中读取。很简单:只是一个字符数组。我将它初始化如下:

char line[256]; //Maximum size of each line is 255 characters

要访问这个数组的一个元素,我会这样做:

line[0] = 'a'; //Sets element 0 to 'a'
fgets( line, sizeof line, stdin ); //Gets a line from stdin and places it in line

以这种方式声明和使用字符串与将其声明为指针有何不同?据我了解,C 中的数组衰减为指针。那么,以下是等价的吗?

char *line = (char*) malloc( sizeof(char) * 256 );
line[0] = 'a';
fgets( *line, sizeof(line), stdin );

什么时候使用指针字符'*',什么时候不使用?在上面的例子中,在 fgets 中包含 '*' 是必要的,还是正确的?

现在,我想创建一个字符串数组,或者更确切地说,一个指向字符串的指针数组。我会这样做吗?

char *arr[20]; // Declares an array of strings with 20 elements

我将如何访问它?

arr[0] = "hello" // Sets element zero of arr to "hello"

这是正确的吗?

如何将这个数组传递给函数?

execvp("ls", arr); // Executes ls with argument vector arr

这是正确的,还是我会使用指针 *arr?如果有,为什么?

现在更糟糕的是,我想要一个字符串数组的数组(例如,如果我想保存多个参数向量,以便在管道序列中执行多个命令)。 会这样声明吗?

char **vector_arr[20]; // An array of arrays of strings

我将如何访问这个数组的元素?

execvp("ls", vector_arr[0]); // Executes ls with first element of vector_arr as argument vector

我认为我对指针是什么,甚至数组与指针之间的关系有一个很好的理解,但是我似乎在将其与实际代码联系起来时遇到了麻烦。估计是在处理指针的时候,不知道什么时候引用*var、var、&var。

【问题讨论】:

  • 这里的问题太多了。建议阅读一本好的 C 书。
  • 实际上,我认为 OP 设法在一篇文章中解决了大多数常见的数组/字符串问题,并且这样做相当合乎逻辑和雄辩。帖子是否应该按照网站的指示进行拆分?也许。但是一个好的答案会在单个页面中提供很好的参考。
  • 它可能是一个很好的参考,但我不确定 StackOverflow 是否是关于编写参考/教程的?
  • 我认为这本质上是一个问题(数组如何在 C 中工作),涉及到几个不同的抽象级别。我可以整天在 C 中使用“字符串”,但是当我尝试抽象出数组的概念来创建字符串数组时,似乎表明对底层概念缺乏理解。
  • @John Gaughan:编写 Unix shell 是 CS 学生在操作系统入门课程中非常常见的作业。除了提供良好的数组编码练习(例如,用于命令行解析)外,它还引入了作业控制概念,可能是学生的第一个重要程序。

标签: c arrays pointers


【解决方案1】:

让我们谈谈与 C 中的数组相关的 表达式类型

数组

当你声明一个数组时

char line[256];

表达式 line 的类型为“char 的 256 元素数组”;除非该表达式是sizeof 或一元& 运算符的操作数,否则它将被转换(“衰减”)为“指向char”的类型的表达式,表达式的值将是数组的第一个元素的地址。鉴于上述声明,以下所有内容都是正确的:

 Expression             Type            Decays to            Equivalent value
 ----------             ----            ---------            ----------------
       line             char [256]      char *               &line[0]
      &line             char (*)[256]   n/a                  &line[0]
      *line             char            n/a                  line[0]
    line[i]             char            n/a                  n/a
   &line[0]             char *          n/a                  n/a
sizeof line             size_t          n/a                  Total number of bytes 
                                                               in array (256)

注意表达式line&line&line[0]都产生相同的(数组的第一个元素的地址与数组的地址相同本身),只是类型不同。在表达式&line中,数组表达式是&运算符的操作数,所以上面的转换规则不适用;我们得到一个指向 char 的 256 元素数组的指针,而不是指向 char 的指针。类型很重要;如果你写如下内容:

char line[256];
char *linep = line;
char (*linearrp)[256] = &line;

printf( "linep    + 1 = %p\n", (void *) (linep + 1) );
printf( "linearrp + 1 = %p\n", (void *) (linearrp + 1) );

每行都会得到不同的输出; linep + 1 将给出line 之后的下一个char 的地址,而linearrp + 1 将给出line 之后的下一个256 元素数组char 的地址。

表达式line 不是可修改的左值;你不能分配给它,所以像

char temp[256];
...
line = temp;

将是非法的。没有为变量lineline[0]line[256] 分开留出存储空间;没有什么可以分配给

因此,当您将数组表达式传递给函数时,函数接收的是指针值,而不是数组。在函数参数声明的上下文中,T a[N]T a[] 被解释为T *a;这三个都将a 声明为指向T 的指针。参数的“数组”在调用过程中已经丢失。

所有的数组访问都是通过指针运算完成的;表达式a[i] 被评估为*(a + i)。数组表达式a 首先按照上述规则转换为指针类型的表达式,然后我们从该地址偏移i elements 并取消引用结果。

与 Java 不同,C 不会为指向数组的指针留出与数组元素本身分开的存储空间:所有留出的内容如下:

+---+
|   | line[0]
+---+
|   | line[1]
+---+
 ...
+---+
|   | line[255]
+---+

C 也不为堆中的数组分配内存(对于任何堆定义)。如果数组声明为auto(即,块的本地且没有static 关键字),则内存将从实现为局部变量获取内存的任何地方分配(我们大多数人称之为堆栈)。如果数组在文件范围内声明或使用static 关键字声明,则内存将从不同的内存段分配,并将在程序启动时分配并保持到程序终止。

与 Java 不同的是,C 数组不包含有关其长度的元数据; C 假设您在分配数组时知道数组有多大,因此您可以自己跟踪该信息。

指针

当你声明一个指针就像

char *line;

表达式line 的类型为“指向char 的指针”(废话)。留出足够的存储空间来存储char 对象的地址。除非您在文件范围内或使用 static 关键字声明它,否则它不会被初始化并且将包含一些可能对应于或不对应于有效地址的随机位模式。鉴于上述声明,以下所有内容都是正确的:

 Expression             Type            Decays to            Equivalent value
 ----------             ----            ---------            ----------------
       line             char *          n/a                  n/a
      &line             char **         n/a                  n/a
      *line             char            n/a                  line[0]
    line[i]             char            n/a                  n/a
   &line[0]             char *          n/a                  n/a
sizeof line             size_t          n/a                  Total number of bytes
                                                               in a char pointer
                                                               (anywhere from 2 to
                                                               8 depending on the
                                                               platform)

在这种情况下,line&line 确实给了我们不同的值,以及不同的类型; line 是一个简单的标量对象,所以&line 为我们提供了该对象的地址。同样,数组访问是根据指针算术完成的,因此line[i] 的工作方式相同,无论 line 是声明为数组还是指针。

所以当你写的时候

char *line = malloc( sizeof *line * 256 ); // note no cast, sizeof expression

这是像 Java 一样工作的情况;你有一个单独的指针变量,它引用从堆中分配的存储,如下所示:

+---+ 
|   | line -------+
+---+             |
 ...              |
+---+             |
|   | line[0] <---+
+---+
|   | line[1]
+---+
 ...
+---+
|   | line[255]
+---+

与 Java 不同,当不再有对它的引用时,C 不会自动回收该内存。完成后,您必须使用 free 库函数显式释放它:

free( line );

至于你的具体问题:

fgets( *line, sizeof(line), stdin );

你什么时候使用指针字符'*',什么时候不使用?在上面的示例中,是否需要在 fgets 中包含“*”,还是正确?

这是不正确的; fgets 期望第一个参数的类型为“指向char 的指针”; 表达式 *line 的类型为char。这来自声明:

char *line; 

其次,sizeof(line) 只给你指针的大小,而不是指针指向的大小;除非您想准确读取 sizeof (char *) 字节,否则您必须使用不同的表达式来指定要读取的字符数:

fgets( line, 256, stdin );
现在,我想创建一个字符串数组,或者更确切地说,一个指向字符串的指针数组。我会这样做吗?
char *arr[20]; // Declares an array of strings with 20 elements

C 不像 C++ 或 Java 那样具有单独的“字符串”数据类型;在 C 中,string 只是一个以 0 结尾的字符值序列。它们存储char 的数组。请注意,您在上面声明的只是一个指向char 的20 元素指针数组;这些指针可以指向不是字符串的东西。

如果您的所有字符串都将具有相同的最大长度,您可以声明一个char 的二维数组,如下所示:

char arr[NUM_STRINGS][MAX_STRING_LENGTH + 1]; // +1 for 0 terminator

然后你会将每个字符串分配为

strcpy( arr[i], "some string" );
strcpy( arr[j], some_other_variable );
strncpy( arr[k], MAX_STRING_LENGTH, another_string_variable );

虽然要小心strncpy;如果源字符串比目标字符串长,它不会自动将 0 终止符附加到目标字符串。在尝试将它与字符串库的其余部分一起使用之前,您必须确保存在终止符。

如果要为每个字符串分别分配空间,可以声明指针数组,然后分配每个指针:

char *arr[NUM_STRINGS];
...
arr[i] = malloc( strlen("some string") + 1 );
strcpy( arr[i], "some string" );
...
arr[j] = strdup( "some string" ); // not available in all implementations, calls
                                  // malloc under the hood
...
arr[k] = "some string";  // arr[k] contains the address of the *string literal*
                         // "some string"; note that you may not modify the contents
                         // of a string literal (the behavior is undefined), so 
                         // arr[k] should not be used as an argument to any function
                         // that tries to modify the input parameter.

注意arr的每个元素都是一个指针值;这些指针是否指向 strings(以 0 结尾的 char 序列)取决于您。

现在更糟糕的是,我想要一个字符串数组(例如,如果我想保存多个参数向量,以便按管道序列执行多个命令)。会这样声明吗?
char **vector_arr[20]; // An array of arrays of strings

你声明的是一个指向 char 指针的指针数组;请注意,如果您不知道需要在每个元素中存储多少指向 char 的指针,这是完全有效的。但是,如果您知道每个元素的最大参数数,那么写起来可能会更清楚

char *vector_arr[20][N];

否则,您必须动态分配char * 的每个数组:

char **vector_arr[20] = { NULL }; // initialize all the pointers to NULL

for ( i = 0; i < 20; i++ )
{
  // the type of the expression vector_arr is 20-element array of char **, so
  // the type of the expression vector_arr[i] is char **, so
  // the type of the expression *vector_arr[i] is char *, so
  // the type of the expression vector[i][j] is char *, so
  // the type of the expression *vector_arr[i][j] is char

  vector_arr[i] = malloc( sizeof *vector_arr[i] * num_args_for_this_element );
  if ( vector_arr[i] )
  {
    for ( j = 0; j < num_args_for_this_element )
    {
      vector_arr[i][j] = malloc( sizeof *vector_arr[i][j] * (size_of_this_element + 1) );
      // assign the argument
      strcpy( vector_arr[i][j], argument_for_this_element );
    }
  }
}

因此,vector_arr 的每个元素都是一个指向 char 的 M 元素数组的指针的 N​​ 元素数组。

【讨论】:

  • 谢谢。一个非常翔实的答案。它确实有助于澄清一切。我想我现在掌握得很好。我已经成功解析了命令行输入并执行了单个命令。现在只需要设置管道和重定向,应该不会太难。
【解决方案2】:

你真的走在正确的轨道上。

在您的第二个示例中,您使用malloc()fgets() 命令将像这样调用:

fgets( line, sizeof(line), stdin ); /* vs. fgets( *line ... ) as you have */

这样做的原因是,在 C 中,命名数组变量始终只是一个指针。所以:

char line[256];

声明(并定义)一个名为 line 的指针,它指向在编译时分配的 256 字节内存(可能在堆栈上)。

char *line; 也声明了一个指针,但它指向的内存不是由编译器分配的。当您调用malloc 时,您会将返回值类型转换为char * 并将其分配给line,以便在堆上动态分配内存。

从功能上讲,变量line 只是一个char *(指向字符的指针),如果您查看&lt;stdio.h&gt; 文件中fgets 的声明,您会看到它的第一个期望论据:

char *fgets(char * restrict str, int size, FILE * restrict stream);

...即char *。所以你可以传递line 任何你声明它的方式(作为一个指针或作为一个数组)。

关于您的其他问题:

char *arr[20]; 声明了 20 个 未初始化 指向 char * 的指针。要使用此数组,您将遍历 arr 的元素 20 次,并为每个元素分配 malloc() 的某些结果:

arr[0] = (char *) malloc( sizeof(char*) * 256 );
arr[1] = (char *) malloc( sizeof(char*) * 256 );
...
arr[19] = (char *) malloc( sizeof(char*) * 256 );

然后您可以使用 20 个字符串中的每一个。要将第二个传递给fgets,它需要char * 作为它的第一个参数,你可以这样做:

fgets( arr[1], ... );

然后fgets 得到它所期望的char *

请注意,您必须在尝试此操作之前调用malloc(),否则arr[1] 将无法初始化。

您使用 execvp() 的示例是正确的(假设您首先使用 malloc() 分配了所有这些字符串。vector_arr[0] 是一个字符 **,这是 execvp() 所期望的。[还请记住 execvp() 期望的最后一个指针您的向量数组具有 value NULL,请参阅手册页以进行说明]。

注意execvp() 是这样声明的(参见&lt;unistd.h&gt;

int execvp(const char *file, char *const argv[]);

为了清楚起见,删除 const 属性,它也可以这样声明:

int execvp( const char *file, char **argv );

char **array 的声明在功能上等同于 char *array[]

还请记住,在我们使用malloc() 的每个示例中,您必须在某些时候使用相应的free(),否则您会泄漏内存。

我还要指出,一般来说,虽然您可以创建一个向量数组(以及向量数组的数组等),但随着您对数组进行越来越多的维度扩展,您会发现代码得到了越来越难以理解和维护。当然,在完全理解之前,您应该学习这一切的工作原理并进行练习,但是如果在设计代码的过程中您发现自己认为需要由数组组成的数组,那么您可能过于复杂了。

【讨论】:

  • 因此,据我了解,使用 arr[int] 表示法声明数组会使数组保持不变。如果是字符串,我将不得不使用字符串函数(strcpy、strcat)来修改字符串,对吗?当声明为指针但 *arr 时,该数组是动态的。在分配值之前,是否必须使用 malloc 为值创建空间?还是为数组赋值会创建空间?谢谢你的回答,帮了大忙。
  • 除非你使用const关键字,否则数组内容是not常量。 const 是一个单独的主题,所以不要担心。无论您以哪种方式声明line(是否使用malloc),您仍然可以说line[0] = 'a';(试试看!)。只是不要将编译器分配的那个传递给free()
  • 使数组成为常量 是一种不好的思考方式。把它想象成arr[int] 有一个分配的固定地址。对于*arr,指针变量 arr 有一个固定地址,但它指向的(它的值)是可变的并且可以改变。
  • 在 C 中,您必须确保在分配值之前有可用空间。如果您像这样声明行:char line[256];,您会告诉编译器为您分配 256 个字节,以便在下一行代码中使用它。 char *line; 只是说line 可以用作数组,但它没有说明line 指向的内存有多大。您可以通过使用malloc 来确定这一点,并且您必须跟踪数组的大小。 sizeof(line) 的行为会有所不同,具体取决于您声明行的方式。一个返回数组大小,另一个返回指针大小(试试看!)
  • @par:该语句“声明(并定义)一个名为 line 的指针,指向在编译时分配的 256 字节内存(可能在堆栈上)。”是不正确的;没有为与数组元素分开的指针值留出存储空间。没有与line[0]line[255] 分开的line 变量。
【解决方案3】:

这是对 OP 的部分回答。

char *line = (char*) malloc( sizeof(char) * 256 );
line[0] = 'a';
fgets( *line, sizeof(line), stdin );

fgets() 的参数有误,应该是fgets( line, 256, stdin );

解释:

  1. fgets() 的第一个参数是char *,因此您可以使用指向char 的指针或char 的数组(在这种情况下,此数组名称将降级为char *)。

    当用作函数的参数时,数组名称降级为指针。

  2. 因为line 是一个指针,sizeof(line) 会给你一个指针的大小(在 32 位系统中通常为 4);但是如果line 是一个数组,比如char line[100]sizeof(line) 会给你数组的大小,在这个例子中是 100 * sizeof(char)。

    当用作sizeof 运算符的参数时,数组名称不会降级为指针。

【讨论】:

  • 感谢您的回答。那么对于 *line 的大小,我想用 strlen(line) 代替,对吗?如果我想要字符串数组的大小,那么我必须存储大小,因为 sizeof 不起作用?
  • @JohnT 您不能使用strlen(line) 来获取line 的长度,因为strlen() 需要一个字符串,在C 中,这意味着一个字符序列和一个\0,但是这个mallaced 记忆的内容是未知的,它可以是任何东西。
  • 你有点想在走路之前跑步,但 strlen() 会告诉你行的逻辑长度(这是你想要的),而不是物理长度。在 C 中,通过将最后一个字节设置为零来表示字符串的结尾。所以line[0] = 'a'; line[1] = 0; 将创建一个逻辑上只有一个字符长的字符串,而 strlen() 将返回 1。请注意,您实际上必须使用两个字节,一个用于“a”,一个用于 NULL(零)终止符。而 that 并没有说明行的物理大小,我们知道它是 256 字节!
  • @JohnT 通过'字符串数组的大小',你想得到哪个大小?此数组中的字符串数?这个数组中所有字符串的长度总和?还是别的什么?
  • 好的,我现在明白了。如果使用 fgets(),您需要数组的空间量 (256),因此 strlen() 在这种情况下不合适,但当您需要字符串的长度时。我想我大概知道这一点,只是忘记了。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-05-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多