如何在C语言中从字符串数组中访问单个字符?

11

我只是试图理解如何在一个字符串数组中寻找单个字符。同时,这当然也能让我理解指向指针的下标操作。

如果有char **a,我想要访问第二个字符串的第三个字符,这个表达式是否正确:**((a+1)+2)?看起来应该是可以的...

6个回答

21

几乎正确,但不完全正确。 正确答案是:

*((*(a+1))+2)

因为你需要首先对实际字符串指针之一进行解引用,然后将所选的字符串指针向下解引用到所需字符。 (请注意,我添加了额外的括号以便清楚地表示操作顺序)。

或者,这个表达式:

a[1][2]

这种写法也可以运行!或许更受欢迎,因为你所尝试做的意图更加自明,而且符号本身更加简洁。对于刚接触该语言的人来说,这种形式可能不是立即显而易见的,但要理解数组符号之所以有效是因为在C语言中,数组索引操作实际上只是等价指针操作的简写。即:*(a+x)和a[x]是相同的。因此,通过将这种逻辑扩展到原始问题,有两个单独的指针解引用操作级联在一起,表达式a[x][y]等同于*((*(a+x))+y)的一般形式。


那么指向指针的指针可以作为数组使用,而无需任何其他声明吗? - sdellysse
幕后,a[b] 被翻译为 *(a+b)。 - Greg Rogers
@sdellysse - 是的!- 实际上,不仅可以使用指向指针的指针,使用数组符号也可以对任何指针进行索引和取消引用。我已经在我的答案中添加了细节来涵盖您和@Binary Worrier的观点。 - Tall Jeff

5

您不必使用指针。

int main(int argc, char **argv){

printf("argv[1]的第三个字符是[%c]。\n", argv[1][2]);

}

然后:

$ ./main hello argv[1]的第三个字符是[l]。

那是一个数字1和字母l。

如果您想要,可以使用指针...

*(argv[1] +2)

或者甚至是

*((*(a+1))+2)

就像上面有人指出的那样。

这是因为数组名称是指针。


是的,我刚刚改了它。这就是我试图快速发布而没有检查我的下标得到的结果。 - Steve Klabnik

3
在Jon Erickson的《黑客攻防艺术》第二版中有一份关于指针、字符串和变量操作的精彩C编程解释,仅此就值得一提。https://leaksource.files.wordpress.com/2014/08/hacking-the-art-of-exploitation.pdf
虽然这个问题已经有人回答过了,但其他想要了解更多的人可能会发现以下是Erickson书中的亮点,有助于理解问题背后的一些结构。
头文件
你可能会用到的变量操作头文件示例:
stdio.h - http://www.cplusplus.com/reference/cstdio/
stdlib.h - http://www.cplusplus.com/reference/cstdlib/
string.h - http://www.cplusplus.com/reference/cstring/ limits.h - http://www.cplusplus.com/reference/climits/
函数
你可能会用到的常用函数示例:
malloc() - http://www.cplusplus.com/reference/cstdlib/malloc/
calloc() - http://www.cplusplus.com/reference/cstdlib/calloc/
strcpy() - http://www.cplusplus.com/reference/cstring/strcpy/
内存
"编译后的程序内存被划分为五个段:文本段、数据段、bss段、堆和栈。每个段表示专门用于特定目的的一部分内存。文本段有时也称为代码段,其中包含程序的汇编机器语言指令。"
"由于高级控制结构和函数编译成汇编语言中的分支、跳转和调用指令,因此在该段中执行指令是非线性的。当程序执行时,EIP被设置为文本段中的第一条指令。然后处理器遵循执行循环,执行以下操作:"
"1. 读取EIP指向的指令"
"2. 将指令的字节长度添加到EIP中"
"3. 执行步骤1中读取的指令"
"4. 返回步骤1"
"有时候指令是跳转或调用指令,会改变EIP到内存中的不同地址。处理器并不关心这种变化,因为它期望执行是非线性的。如果在第3步中改变了EIP,处理器将返回第1步,并读取在EIP所更改到的地址上找到的指令。"
"写入权限在文本段中被禁用,因为它不用于存储变量,只用于代码。这可以防止人们实际修改程序代码;任何尝试写入此内存段的操作都会导致程序向用户发出警告,程序将被终止。此段只读内存的另一个优点是,它可以在程序的不同副本之间共享,允许同时多次执行程序而没有问题。还应注意,该内存段具有固定大小,因为其中的内容永远不会改变。"
"data"和"bss"段用于存储全局和静态程序变量。数据段填充了初始化的全局和静态变量,而bss段填充了它们未初始化的对应项。尽管这些段是可写的,但它们也有固定的大小。请记住,全局变量是持久的,即使在函数上下文中(例如前面示例中的变量j)。由于它们存储在自己的内存段中,因此全局和静态变量都能够持久存在。
堆段是程序员可以直接控制的内存段。该段中的内存块可以分配并用于程序员可能需要的任何内容。关于堆段的一个值得注意的点是它不是固定大小的,因此它可以根据需要变大或变小。
堆中的所有内存都由分配器和解除分配器算法管理,它们分别为堆中的使用保留一段内存区域,并删除保留以允许稍后重新使用该内存部分的保留。堆将根据为使用保留的内存量而增长和缩小。这意味着使用堆分配函数的程序员可以即时保留和释放内存。堆的增长向高内存地址方向移动。
"The stack segment是可变大小的,用作临时刮擦板来存储本地函数变量和上下文,在函数调用期间使用。这就是GDB的backtrace命令所查看的内容。当程序调用函数时,该函数将具有其自己的传递变量集,并且函数的代码将位于文本(或代码)段中的不同内存位置。由于在调用函数时必须更改上下文和EIP,因此堆栈用于记住所有传递的变量,EIP应在函数完成后返回的位置以及该函数使用的所有本地变量。所有这些信息都在堆栈上一起存储,称为堆栈帧。堆栈包含许多堆栈帧。"
"从计算机科学的角度来看,堆栈是经常使用的抽象数据结构。它具有先进后出(FILO)排序,这意味着放入堆栈的第一个项是最后一个出来的项。可以将其视为在一端打了个结的绳子上放珠子 - 直到将所有其他珠子移除之前,无法取出第一个珠子。将项目放入堆栈中称为推送,从堆栈中删除项目称为弹出。"
"顾名思义,内存的堆栈段实际上是一个堆栈数据结构,其中包含堆栈帧。ESP寄存器用于跟踪堆栈末尾的地址,随着项目被推入和弹出,该地址不断变化。由于这是非常动态的行为,因此堆栈的大小也不固定。相反,与堆的动态增长相反,堆栈在内存的可视列表中向上增长,朝向较低的内存地址。"
"The FILO(先进后出)nature of a stack might seem odd, but since the stack is used to store context, it’s very useful. When a function is called, several things are pushed to the stack together in a stack frame. The EBP register—sometimes called the frame pointer (FP) or local base (LB) pointer—is used to reference local function variables in the current stack frame. Each stack frame contains the parameters to the function, its local variables, and two pointers that are necessary to put things back the way they were: the saved frame pointer (SFP) and the return address. The SFP is used to restore EBP to its previous value, and the return address is used to restore EIP to the next instruction found after the function call. This restores the functional context of the previous stack frame."
"Strings(字符串)"
"In C, an array is simply a list of n elements of a specific data type. A 20-character array is simply 20 adjacent characters located in memory. Arrays are also referred to as buffers."
#include <stdio.h>

int main()
{
    char str_a[20];
    str_a[0] = 'H';
    str_a[1] = 'e';
    str_a[2] = 'l';
    str_a[3] = 'l';
    str_a[4] = 'o';
    str_a[5] = ',';
    str_a[6] = ' ';
    str_a[7] = 'w';
    str_a[8] = 'o';
    str_a[9] = 'r';
    str_a[10] = 'l';
    str_a[11] = 'd';
    str_a[12] = '!';
    str_a[13] = '\n';
    str_a[14] = 0;
    printf(str_a);
}

在前面的程序中,定义了一个20个元素的字符数组str_a,并且逐个写入了数组的每个元素。请注意,数字从0开始,而不是1。还要注意,最后一个字符是0(也称为空字节)。字符数组被定义,因此为其分配了20个字节,但实际上只使用了其中的12个字节。在末尾使用的空字节编程是一个分隔符字符,告诉任何处理该字符串的函数在那里停止操作。其余的额外字节只是垃圾,将被忽略。如果在字符数组的第五个元素中插入一个空字节,则printf()函数只会打印出Hello这几个字符。由于设置字符数组中的每个字符都很麻烦,而且字符串经常被使用,因此创建了一组用于字符串操作的标准函数。例如,strcpy()函数将从源复制字符串到目标,迭代源字符串并将每个字节复制到目标(并在复制空终止字节后停止)。
"函数参数的顺序类似于Intel汇编语法,首先是目标,然后是源。char_array.c程序可以使用strcpy()重写,以使用字符串库实现相同的功能。下面显示的char_array程序的下一个版本包括string.h,因为它使用了一个字符串函数。"
#include <stdio.h>
#include <string.h>

int main() 
{
    char str_a[20];
    strcpy(str_a, "Hello, world!\n");
    printf(str_a);
}

查找有关C字符串的更多信息

http://www.cs.uic.edu/~jbell/CourseNotes/C_Programming/CharacterStrings.html

http://www.tutorialspoint.com/cprogramming/c_strings.htm

指针

"EIP寄存器是一个指针,通过包含其内存地址,在程序执行期间“指向”当前指令。指针的概念也被用于C语言中。由于物理内存实际上无法移动,其中的信息必须被复制。将大块内存复制以供不同函数或不同位置使用可能非常计算密集。从内存角度来看,这也很昂贵,因为必须在源代码复制之前保存或分配新目标复制的空间。指针是解决此问题的一种方法。与复制大块内存不同,传递该块内存开头的地址更加简单。"。

"C语言中的指针可以像任何其他变量类型一样定义和使用。由于x86架构上的内存使用32位寻址,因此指针的大小也为32位(4字节)。通过在变量名前面添加星号(*),可以定义指针。不需要定义该类型的变量,而是定义一个指向该类型数据的指针。pointer.c程序是使用char数据类型的指针的示例,该类型大小仅为1字节"。

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

int main() 
{
    char str_a[20]; // A 20-element character array
    char *pointer; // A pointer, meant for a character array
    char *pointer2; // And yet another one
    strcpy(str_a, "Hello, world!\n");
    pointer = str_a; // Set the first pointer to the start of the array.
    printf(pointer);
    pointer2 = pointer + 2; // Set the second one 2 bytes further in.
    printf(pointer2); // Print it.
    strcpy(pointer2, "y you guys!\n"); // Copy into that spot.
    printf(pointer); // Print again.
}

"正如代码中的注释所示,第一个指针设置在字符数组的开头。当像这样引用字符数组时,它实际上是一个指针本身。这就是为什么此缓冲区之前被作为指针传递给printf()和strcpy()函数的原因。第二个指针设置为第一个指针地址加上两个位置,然后打印一些内容(在下面的输出中显示)。"
reader@hacking:~/booksrc $ gcc -o pointer pointer.c
reader@hacking:~/booksrc $ ./pointer
Hello, world!
llo, world!
Hey you guys!
reader@hacking:~/booksrc $

"取地址运算符常与指针一起使用,因为指针包含内存地址。addressof.c程序演示了如何使用取地址运算符将整数变量的地址放入指针中。下面的粗体行显示了这条语句".
#include <stdio.h>

int main() 
{
    int int_var = 5;
    int *int_ptr;
    int_ptr = &int_var; // put the address of int_var into int_ptr
}

"还有一种名为解引用运算符的一元运算符可用于指针。该运算符将返回指针指向的地址中找到的数据,而不是地址本身。它采用与指针声明类似的变量名称前面的星号形式。再次说明,解引用运算符在GDB和C中都存在。"
"对addressof.c代码(显示在addressof2.c中)进行了一些添加,以演示所有这些概念。添加的printf()函数使用格式参数,我将在下一节中解释这些参数。现在,只需关注程序的输出即可。"
#include <stdio.h>

int main() 
{
    int int_var = 5;
    int *int_ptr;
    int_ptr = &int_var; // Put the address of int_var into int_ptr.
    printf("int_ptr = 0x%08x\n", int_ptr);
    printf("&int_ptr = 0x%08x\n", &int_ptr);
    printf("*int_ptr = 0x%08x\n\n", *int_ptr);
    printf("int_var is located at 0x%08x and contains %d\n", &int_var, int_var);
    printf("int_ptr is located at 0x%08x, contains 0x%08x, and points to %d\n\n", &int_ptr, int_ptr, *int_ptr);
}

“当一元运算符与指针一起使用时,取地址运算符可以被视为向后移动,而解引用运算符则向指针所指向的方向向前移动。”
了解更多关于指针和内存分配的内容。
加利福尼亚大学计算机科学系教授丹·赫什伯格有关计算机内存的文章: https://www.ics.uci.edu/~dan/class/165/notes/memory.html

http://cslibrary.stanford.edu/106/

http://www.programiz.com/c-programming/c-dynamic-memory-allocation

数组

这里有一篇由名叫Alex Allain的人撰写的关于多维数组的简单教程,链接在这里:http://www.cprogramming.com/tutorial/c/lesson8.html

这里有一篇由名叫Todd A Gibson的人撰写的关于数组的信息,链接在这里:http://www.augustcouncil.com/~tgibson/tutorial/arr.html

遍历一个数组

#include <stdio.h>

int main() 
{

    int i;
    char char_array[5] = {'a', 'b', 'c', 'd', 'e'};
    int int_array[5] = {1, 2, 3, 4, 5};
    char *char_pointer;
    int *int_pointer;
    char_pointer = char_array;
    int_pointer = int_array;

    for(i=0; i < 5; i++) { // Iterate through the int array with the int_pointer.
        printf("[integer pointer] points to %p, which contains the integer %d\n", int_pointer, *int_pointer);
        int_pointer = int_pointer + 1;
    }

    for(i=0; i < 5; i++) { // Iterate through the char array with the char_pointer.
        printf("[char pointer] points to %p, which contains the char '%c'\n", char_pointer, *char_pointer);
        char_pointer = char_pointer + 1;
    }

}

链表 vs 数组

数组并不是唯一的选择,这里提供关于链表的信息。

http://www.eternallyconfuzzled.com/tuts/datastructures/jsw_tut_linklist.aspx

结论

这篇文章仅仅是为了传递我在研究这个主题时阅读到的一些信息,希望能对他人有所帮助。


2
据我所知,字符串实际上是字符数组,因此这应该可以工作:
```

据我所知,字符串实际上是字符数组,因此这应该可以工作:

```
a[1][2]

2
来自维基百科 C指针文章 的引用 -
在C语言中,数组索引是以指针算术的形式正式定义的;也就是说,语言规范要求array [i]等价于*(array + i)。因此,在C语言中,可以将数组视为指向连续内存区域的指针(没有间隙),并且用于访问数组的语法与用于取消引用指针的语法相同。例如,可以按以下方式声明和使用数组:
int array[5];      /* Declares 5 contiguous (per Plauger Standard C 1992) integers */
int *ptr = array;  /* Arrays can be used as pointers */
ptr[0] = 1;        /* Pointers can be indexed with array syntax */
*(array + 1) = 2;  /* Arrays can be dereferenced with pointer syntax */

因此,针对您的问题 - 是的,指向指针的指针可以作为数组使用,而无需任何其他声明!

1

尝试使用a[1][2]或者*(*(a+1)+2)

基本上,数组引用是指针解引用的语法糖。a[2]与a+2相同,也与2[a]相同(如果你真的想要不可读的代码)。字符串数组与双指针相同。因此,您可以使用a[1]或*(a+1)提取第二个字符串。然后,您可以使用b[2]或*(b + 2)在该字符串中找到第三个字符(现在称其为'b')。将原始的第二个字符串替换为“b”,我们最终得到了a[1][2]或*(*(a+1)+2)


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