字符指针和数组的区别

考虑一下:

char amessage[] = "now is the time";
char *pmessage = "now is the time";

我从 C 程序设计语言第2版中读到,上述两个语句并不做同样的事情。

我一直认为数组是一种方便的方式来操作指针来存储一些数据,但事实并非如此... ... 在 C 语言中,数组和指针之间的“非平凡”差异是什么?

62155 次浏览

没错,但这是一个细微的差别,从本质上说,是前者:

char amessage[] = "now is the time";

定义一个数组,其成员位于当前范围的堆栈空间中,而:

char *pmessage = "now is the time";

定义一个指针,该指针位于当前作用域的堆栈空间中,但是它引用其他地方的内存(在本例中,“ now is the time”存储在内存的其他地方,通常是一个字符串表)。

另外,请注意,由于属于第二个定义(显式指针)的数据没有存储在当前作用域的堆栈空间中,因此没有确切指定它将存储在哪里,不应该修改它。

编辑: 正如 Mark、 GMan 和 Pavel 所指出的,在这些变量上使用 address-of 操作符也是有区别的。例如,& pmessage 返回一个 char * * 类型的指针,或者一个指向指向 char 的指针,而 & ammessage 返回一个 char (*)[16]类型的指针,或者一个指向16个字符数组的指针(像 char * * 一样,在 litb 指出时需要解引用两次)。

数组包含元素。指针指向它们。

第一种是一种简短的说法

char amessage[16];
amessage[0] = 'n';
amessage[1] = 'o';
...
amessage[15] = '\0';

也就是说,它是一个包含所有字符的数组。特殊初始化将为您初始化它,并自动确定它的大小。数组元素是可修改的——您可以覆盖其中的字符。

第二种形式是一个指针,它只指向字符。它不直接存储字符。由于数组是字符串文字,因此不能使用指针并将其写入指向的位置

char *pmessage = "now is the time";
*pmessage = 'p'; /* undefined behavior! */

这段代码可能会在你的机器上崩溃,但是它可以做任何它喜欢的事情,因为它的行为是未定义的。

第二个函数在 ELF 的某个只读区域中分配字符串。 试试以下方法:

#include <stdio.h>


int main(char argc, char** argv) {
char amessage[] = "now is the time";
char *pmessage = "now is the time";


amessage[3] = 'S';
printf("%s\n",amessage);


pmessage[3] = 'S';
printf("%s\n",pmessage);
}

并且在第二次赋值时会得到一个 Segfault (pmessage [3] = ‘ S’)。

除了将字符串“ now is the time”的内存分配到两个不同的位置之外,还应该记住,数组名充当指针 价值,而 pmessage 是指针 变量。主要区别在于指针变量可以修改为指向其他地方,而数组不能。

char arr[] = "now is the time";
char *pchar = "later is the time";


char arr2[] = "Another String";


pchar = arr2; //Ok, pchar now points at "Another String"


arr = arr2; //Compiler Error! The array name can be used as a pointer VALUE
//not a pointer VARIABLE

数组是一个常量指针。您不能更新它的值并使它指向其他任何位置。 而对于一个指针你可以做。

我不能对其他答案进行有用的补充,但是我要指出,在 深 C 的秘密中,Peter van der Linden 详细介绍了这个例子。如果你问这些问题,我想你会喜欢这本书的。


附言。可以为 pmessage赋一个新值。不能为 amessage赋新值; 它是 永恒不变

如果定义了一个数组,使其大小在声明时可用,则 sizeof(p)/sizeof(type-of-array)将返回数组中的元素数。

第一个表单(amessage)定义了一个变量(一个数组) ,它包含字符串 "now is the time"的一个副本。

第二种形式(pmessage)定义了一个变量(指针) ,它与字符串 "now is the time"的任何副本位于不同的位置。

试试这个程序:

#include <inttypes.h>
#include <stdio.h>


int main (int argc, char *argv [])
{
char  amessage [] = "now is the time";
char *pmessage    = "now is the time";


printf("&amessage   : %#016"PRIxPTR"\n", (uintptr_t)&amessage);
printf("&amessage[0]: %#016"PRIxPTR"\n", (uintptr_t)&amessage[0]);
printf("&pmessage   : %#016"PRIxPTR"\n", (uintptr_t)&pmessage);
printf("&pmessage[0]: %#016"PRIxPTR"\n", (uintptr_t)&pmessage[0]);


printf("&\"now is the time\": %#016"PRIxPTR"\n",
(uintptr_t)&"now is the time");


return 0;
}

您将看到,虽然 &amessage等于 &amessage[0],但是对于 &pmessage&pmessage[0]却不是这样。实际上,您将看到存储在 amessage中的字符串位于堆栈上,而 pmessage指向的字符串位于其他位置。

最后一个 printf 显示字符串文字的地址。如果您的编译器执行“ string pool”,那么将只有字符串的一个副本“ now is the time”——您将看到它的地址与 amessage的地址不同。这是因为在字符串初始化时,amessage获取字符串的 收到

最后,要点是 amessage将字符串存储在它自己的内存中(在本例中是在堆栈上) ,而 pmessage指向存储在其他地方的字符串。

这句话是: Char amessage [] = “现在是时候了”;

编译器将使用 ammessage 作为一个指针,指向包含字符“ now is the time”的数组的开始。编译器为“ now is The time”分配内存,并用字符串“ now is The time”初始化它。您知道该消息存储在哪里,因为 message 始终指向该消息的开始。Ammessage 可能不会被赋予一个新的值——它不是一个变量,而是字符串的名称“ now is the time”。

这句话: Char * pmessage = “现在是时候了”;

声明一个变量 pmessage,它是字符串“ now is the time”的起始地址的 已初始化(给定一个初始值)。与 ammessage 不同,pmessage 可以被赋予一个新值。在这种情况下,与前一种情况一样,编译器也在内存的其他地方存储“ now is the time”。 例如,这将导致 pmessage 指向以“ is the time”开头的‘ i’。 Pmessage = pmessage + 4;

下面是一个假设的内存映射,显示了两个声明的结果:

                0x00  0x01  0x02  0x03  0x04  0x05  0x06  0x07
0x00008000:  'n'   'o'   'w'   ' '   'i'   's'   ' '   't'
0x00008008:  'h'   'e'   ' '   't'   'i'   'm'   'e'  '\0'
...
amessage:
0x00500000:  'n'   'o'   'w'   ' '   'i'   's'   ' '   't'
0x00500008:  'h'   'e'   ' '   't'   'i'   'm'   'e'  '\0'
pmessage:
0x00500010:  0x00  0x00  0x80  0x00

字符串文字“ now is The time”存储为16个元素的 char 数组,内存地址为0x00008000。这个内存可能不是可写的; 最好假设它不是。永远不要试图修改字符串文字的内容。

宣言

char amessage[] = "now is the time";

在内存地址0x00500000处分配一个由16个元素组成的 char 数组,并将字符串文字的 内容复制到该数组。这段记忆是可写的,你可以根据自己的内心改变信息的内容:

strcpy(amessage, "the time is now");

宣言

char *pmessage = "now is the time";

在内存地址0x00500010处分配一个指向 char 的指针,并将字符串文字的 地址复制到该指针。

由于 pmessage 指向字符串文字,因此不应将其用作需要修改字符串内容的函数的参数:

strcpy(amessage, pmessage); /* OKAY */
strcpy(pmessage, amessage); /* NOT OKAY */
strtok(amessage, " ");      /* OKAY */
strtok(pmessage, " ");      /* NOT OKAY */
scanf("%15s", amessage);      /* OKAY */
scanf("%15s", pmessage);      /* NOT OKAY */

如果你把 pmessage 改成了 ammessage:

pmessage = amessage;

然后它可以使用任何地方消息可以使用。

下面是我对数组和指针之间主要差异的总结,这是我为自己做的:

//ATTENTION:
//Pointer depth 1
int    marr[]  =  {1,13,25,37,45,56};      // array is implemented as a Pointer TO THE FIRST ARRAY ELEMENT
int*   pmarr   =  marr;                    // don't use & for assignment, because same pointer depth. Assigning Pointer = Pointer makes them equal. So pmarr points to the first ArrayElement.


int*   point   = (marr + 1);               // ATTENTION: moves the array-pointer in memory, but by sizeof(TYPE) and not by 1 byte. The steps are equal to the type of the array-elements (here sizeof(int))


//Pointer depth 2
int**  ppmarr  = &pmarr;                   // use & because going one level deeper. So use the address of the pointer.


//TYPES
//array and pointer are different, which can be seen by checking their types
std::cout << "type of  marr is: "       << typeid(marr).name()          << std::endl;   // int*         so marr  gives a pointer to the first array element
std::cout << "type of &marr is: "       << typeid(&marr).name()         << std::endl;   // int (*)[6]   so &marr gives a pointer to the whole array


std::cout << "type of  pmarr is: "      << typeid(pmarr).name()         << std::endl;   // int*         so pmarr  gives a pointer to the first array element
std::cout << "type of &pmarr is: "      << typeid(&pmarr).name()        << std::endl;   // int**        so &pmarr gives a pointer to to pointer to the first array elelemt. Because & gets us one level deeper.

指针只是一个保存内存地址的变量。请注意,您使用的是 playinf 和“ string 文字”,这是另一个问题。基本上:

#include <stdio.h>


int main ()
{


char amessage[] = "now is the time"; /* Attention you have created a "string literal" */


char *pmessage = "now is the time";  /* You are REUSING the string literal */




/* About arrays and pointers */


pmessage = NULL; /* All right */
amessage = NULL; /* Compilation ERROR!! */


printf ("%d\n", sizeof (amessage)); /* Size of the string literal*/
printf ("%d\n", sizeof (pmessage)); /* Size of pmessage is platform dependent - size of memory bus (1,2,4,8 bytes)*/


printf ("%p, %p\n", pmessage, &pmessage);  /* These values are different !! */
printf ("%p, %p\n", amessage, &amessage);  /* These values are THE SAME!!. There is no sense in retrieving "&amessage" */




/* About string literals */


if (pmessage == amessage)
{
printf ("A string literal is defined only once. You are sharing space");


/* Demostration */
"now is the time"[0] = 'W';
printf ("You have modified both!! %s == %s \n", amessage, pmessage);
}




/* Hope it was useful*/
return 0;
}

以上的答案一定已经回答了你的问题。但我想建议你阅读由丹尼斯 · 里奇爵士撰写的 C 语言的发展中的“胚胎 C”段落。

字符指针和数组之间的区别

C99 N1256汇票

字符串字面值有两种不同的用法:

  1. 初始化 char[]:

    char c[] = "abc";
    

    这是“更神奇的”,在6.7.8/14“初始化”中有描述:

    字符类型的数组可以通过字符串文字(可选)初始化 字符串字面值的连续字符(包括 如果有空间或数组大小未知,则终止空字符)初始化 数组的元素。

    所以这只是一个捷径:

    char c[] = {'a', 'b', 'c', '\0'};
    

    像任何其他常规数组一样,可以修改 c

  2. 其他地方: 它产生一个:

    • 无名氏
    • 字符数组
    • 静态存储
    • 如果修改,就会产生 UB

    所以当你写:

    char *c = "abc";
    

    这类似于:

    /* __unnamed is magic because modifying it gives UB. */
    static char __unnamed[] = "abc";
    char *c = __unnamed;
    

    注意从 char[]char *的隐式转换,这总是合法的。

    然后,如果你修改 c[0],你也修改 __unnamed,这是 UB。

    这在6.4.5“字符串文字”中有记载:

    在翻译阶段7,每个多字节后面附加一个字节或值为零的代码 由字符串文字或文字产生的字符序列。多字节字符 序列然后用于初始化静态存储持续时间和长度的数组 对于字符串文字,数组元素具有 类型 char,并使用多字节字符的单个字节进行初始化 序列[ ... ]

    6如果这些数组的元素具有 如果程序试图修改这样的数组,则行为为 未定义。

6.7.8/32“初始化”给出了一个直接的例子:

声明

char s[] = "abc", t[3] = "abc";

定义“普通”字符数组对象 st,它们的元素用字符串文字初始化。

此声明与

char s[] = { 'a', 'b', 'c', '\0' },
t[] = { 'a', 'b', 'c' };

数组的内容是可修改的

char *p = "abc";

定义 p的类型为“指向 char 的指针”,并将其初始化为指向长度为4的类型为“ array of char”的对象,该对象的元素用字符串文字初始化。如果尝试使用 p修改数组的内容,则该行为是未定义的。

GCC 4.8 x86-64 ELF 实现

程序:

#include <stdio.h>


int main(void) {
char *s = "abc";
printf("%s\n", s);
return 0;
}

编译和反编译:

gcc -ggdb -std=c99 -c main.c
objdump -Sr main.o

输出包括:

 char *s = "abc";
8:  48 c7 45 f8 00 00 00    movq   $0x0,-0x8(%rbp)
f:  00
c: R_X86_64_32S .rodata

结论: GCC 将 char*储存在 .rodata区,而非 .text区。

如果我们对 char[]也这样做:

 char s[] = "abc";

我们得到:

17:   c7 45 f0 61 62 63 00    movl   $0x636261,-0x10(%rbp)

因此它被存储在堆栈中(相对于 %rbp)。

但是请注意,默认的链接器脚本将 .rodata.text放在同一段中,该段具有执行权限,但没有写权限。这可以通过以下方式观察到:

readelf -l a.out

其中包括:

 Section to Segment mapping:
Segment Sections...
02     .text .rodata