我试图在C中创建一个UNIX shell.如果是在Java中,它将是一块蛋糕,但我在C中没那么有经验.C中的数组让我感到困惑.我不知道如何声明或访问某些数据结构.
我想创建一个字符串来读取每一行.足够简单:只是一个字符数组.我会按如下方式初始化它:
char line[256]; //Maximum size of each line is 255 characters
Run Code Online (Sandbox Code Playgroud)
要访问此数组的元素,我将执行以下操作:
line[0] = 'a'; //Sets element 0 to 'a'
fgets( line, sizeof line, stdin ); //Gets a line from stdin and places it in line
Run Code Online (Sandbox Code Playgroud)
如何以这种方式声明和使用字符串与将其声明为指针有何不同?根据我的理解,C中的数组衰减到指针.那么,以下内容是否相同?
char *line = (char*) malloc( sizeof(char) * 256 );
line[0] = 'a';
fgets( *line, sizeof(line), stdin );
Run Code Online (Sandbox Code Playgroud)
你什么时候使用指针字符'*',什么时候不使用?在上面的例子中,包含'*'在fgets中是必要的,还是正确的?
现在,我想创建一个字符串数组,或者更确切地说,是一个指向字符串的指针数组.我会这样做吗?
char *arr[20]; // Declares an array of strings with 20 elements
Run Code Online (Sandbox Code Playgroud)
我将如何访问它?
arr[0] = "hello" // Sets element zero of arr to "hello"
Run Code Online (Sandbox Code Playgroud)
它是否正确?
我如何将此数组传递给函数?
execvp("ls", arr); // Executes ls with argument vector arr
Run Code Online (Sandbox Code Playgroud)
这是正确的,还是我会使用指针*arr?如果是这样,为什么?
现在更糟糕的是,我想要一个字符串数组(例如,如果我想要保存多个参数向量,以便在管道序列中执行多个命令).它会被声明如下吗?
char **vector_arr[20]; // An array of arrays of strings
Run Code Online (Sandbox Code Playgroud)
我将如何访问此数组的元素?
execvp("ls", vector_arr[0]); // Executes ls with first element of vector_arr as argument vector
Run Code Online (Sandbox Code Playgroud)
我认为我掌握了对指针是什么的理解,甚至数组如何与指针相关,但是我似乎无法将其与实际代码相关联.我想在处理指针时,我不知道何时引用*var,var或&var.
让我们讨论一下与 C 中的数组相关的表达式和类型。
数组
当你声明一个数组时
char line[256];
Run Code Online (Sandbox Code Playgroud)
表达式的 line类型为“256 元素数组char”;除非该表达式是 thesizeof或一元运算符的操作数&,否则它将被转换(“衰减”)为“指向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)
Run Code Online (Sandbox Code Playgroud)
请注意,表达式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) );
Run Code Online (Sandbox Code Playgroud)
每行都会得到不同的输出;将给出下一个以下linep + 1的地址,而将给出下一个256 元素数组的地址。 charlinelinearrp + 1charline
该表达式line不是可修改的左值;你不能分配给它,所以像
char temp[256];
...
line = temp;
Run Code Online (Sandbox Code Playgroud)
将是非法的。line没有为与line[0]through分开的变量预留存储空间line[256];没有什么可分配给 的。
因此,当您将数组表达式传递给函数时,函数接收到的是指针值,而不是数组。在函数参数声明的上下文中,T a[N]和T a[]被解释为T *a; 所有三个都声明a为指向 的指针T。参数的“数组性”在调用过程中已丢失。
所有数组访问都是通过指针算术完成的;表达式的a[i]计算结果为*(a + i)。首先根据上述规则将数组表达式a转换为指针类型的表达式,然后我们从该地址偏移i 元素并取消引用结果。
与 Java 不同,C 不会为指向数组的指针与数组元素本身分开进行存储:所保留的内容如下:
+---+
| | line[0]
+---+
| | line[1]
+---+
...
+---+
| | line[255]
+---+
Run Code Online (Sandbox Code Playgroud)
C 也不从堆中为数组分配内存(无论堆的定义如何)。如果声明了数组auto(即,块的本地数组并且没有关键字static),则将从实现为本地变量获取内存的任何位置(我们大多数人称之为堆栈)分配内存。如果数组是在文件范围或使用static关键字声明的,则将从不同的内存段分配内存,并且它将在程序启动时分配并保留到程序终止。
与 Java 不同的是,C 数组不包含有关其长度的元数据;C 假设您在分配数组时知道该数组有多大,因此您可以自己跟踪该信息。
指针
当你声明一个指针时
char *line;
Run Code Online (Sandbox Code Playgroud)
该表达式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)
Run Code Online (Sandbox Code Playgroud)
在这种情况下,line和&line确实给了我们不同的值,以及不同的类型;line是一个简单的标量对象,因此&line给我们该对象的地址。同样,数组访问是根据指针算术完成的,因此line[i]无论将 line 声明为数组还是指针,其工作原理都是相同的。
所以当你写的时候
char *line = malloc( sizeof *line * 256 ); // note no cast, sizeof expression
Run Code Online (Sandbox Code Playgroud)
这是像 Java 一样工作的情况;您有一个单独的指针变量,它引用从堆分配的存储,如下所示:
+---+
| | line -------+
+---+ |
... |
+---+ |
| | line[0] <---+
+---+
| | line[1]
+---+
...
+---+
| | line[255]
+---+
Run Code Online (Sandbox Code Playgroud)
与 Java 不同,当没有更多引用时,C 不会自动回收该内存。当您使用库函数完成它时,您必须显式地释放它free:
free( line );
Run Code Online (Sandbox Code Playgroud)
至于你的具体问题:
Run Code Online (Sandbox Code Playgroud)fgets( *line, sizeof(line), stdin );
什么时候使用指针字符“*”,什么时候不使用?在上面的示例中,在 fgets 中包含“*”是必要的还是正确的?
这是不正确的;fgets期望第一个参数的类型为“pointer to char”;表达式的 *line类型为char。声明如下:
char *line;
Run Code Online (Sandbox Code Playgroud)
其次,sizeof(line)只给出指针的大小,而不给出指针所指向的内容的大小;除非您想精确读取sizeof (char *)字节,否则必须使用不同的表达式来指定要读取的字符数:
fgets( line, 256, stdin );
Run Code Online (Sandbox Code Playgroud)
现在,我想创建一个字符串数组,或者更确切地说,创建一个指向字符串的指针数组。我会这样做吗?Run Code Online (Sandbox Code Playgroud)char *arr[20]; // Declares an array of strings with 20 elements
C 不像 C++ 或 Java 那样有单独的“字符串”数据类型;在 C 中,字符串只是以 0 结尾的字符值序列。它们存储为 的数组char。请注意,上面声明的只是一个 20 个元素的指针数组char;这些指针可以指向不是字符串的东西。
如果所有字符串都具有相同的最大长度,则可以声明一个二维数组,char如下所示:
char arr[NUM_STRINGS][MAX_STRING_LENGTH + 1]; // +1 for 0 terminator
Run Code Online (Sandbox Code Playgroud)
然后你将每个字符串分配为
strcpy( arr[i], "some string" );
strcpy( arr[j], some_other_variable );
strncpy( arr[k], MAX_STRING_LENGTH, another_string_variable );
Run Code Online (Sandbox Code Playgroud)
尽管要提防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.
Run Code Online (Sandbox Code Playgroud)
注意, 的每个元素arr都是一个指针值;这些指针是否指向字符串(以 0 结尾的序列char)取决于您。
现在更糟糕的是,我想要一个字符串数组的数组(例如,如果我想保存多个参数向量,以便按管道顺序执行多个命令)。是否会声明如下?Run Code Online (Sandbox Code Playgroud)char **vector_arr[20]; // An array of arrays of strings
您声明的是一个指向 char 指针的指针数组;请注意,如果您不知道char需要在每个元素中存储多少个指向的指针,则这是完全有效的。但是,如果您知道每个元素的最大参数数,那么编写可能会更清楚
char *vector_arr[20][N];
Run Code Online (Sandbox Code Playgroud)
否则,您必须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 );
}
}
}
Run Code Online (Sandbox Code Playgroud)
因此, 的每个元素vector_arr都是指向 的 M 元素数组的指针的 N 元素数组char。
你确实走在正确的道路上。
在您使用的第二个示例中,将像这样调用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 的指针),如果您查看文件中char *的声明,您将看到它期望的第一个参数:fgets<stdio.h>
char *fgets(char * restrict str, int size, FILE * restrict stream);
...即一个char *. 因此,您可以通过line声明它的任何方式(作为指针或数组)传递。
关于您的其他问题:
char *arr[20];声明 20 个未初始化的指针char *。要使用此数组,您需要对 的元素进行 20 次迭代arr,并为每个元素分配 的一些结果malloc():
arr[0] = (char *) malloc( sizeof(char*) * 256 );
arr[1] = (char *) malloc( sizeof(char*) * 256 );
...
arr[19] = (char *) malloc( sizeof(char*) * 256 );
Run Code Online (Sandbox Code Playgroud)
然后您就可以使用这 20 个字符串中的每一个。要将第二个参数传递给fgets需要 achar *作为其第一个参数的 ,您可以这样做:
fgets( arr[1], ... );
然后fgets得到char *它所期望的。
当然请注意,您必须malloc()在尝试此操作之前调用,否则arr[1]将无法初始化。
您使用 execvp() 的示例是正确的(假设您使用malloc()first.is vector_arr[0]a char **分配了所有这些字符串,这execvp()是期望的。[还记住 execvp() 期望向量数组的最后一个指针具有值NULL,请参阅 man页面进行澄清]。
请注意,它execvp()是这样声明的(请参阅<unistd.h>)
int execvp(const char *file, char *const argv[]);
为了清楚起见删除该const属性,它也可以这样声明:
int execvp( const char *file, char **argv );
char **array功能上等同于 的声明char *array[]。
还要记住,在我们使用的每个示例中malloc(),您都必须在某些时候使用相应的free(),否则会泄漏内存。
我还要指出的是,一般来说,虽然您可以执行向量数组(以及向量数组的数组等),但当您在维度上扩展数组时,您会发现代码变得越来越难来理解和维护。当然,您应该了解这一切是如何工作的并进行练习,直到您完全理解它为止,但是如果在设计代码的过程中您发现自己认为需要数组的数组的数组,那么您可能会使事情变得过于复杂。