C语言字符串中【数组形式】和【指针形式】不同之处

先来看一个错误代码:

#include<stdio.h>


int main()
{
	char arr1[] = "hello world";
	char* p1 = arr1;
	*p1 = 'A';
	printf("%s\n", arr1);

	char* arr2 = "hello world";
	char* p2 = arr2;
	*p2 = 'B';   // 引发了异常: 写入访问权限冲突。p2 是 0x1C7C34。
	printf("%s\n", arr2);

	system("pause");
	return 0;
}

一、字符串两种声明方式

  • 使用指针表示法创建字符串:
const char *pt1 = "Something is pointing at me.";
  • 使用数组形式声明字符串:
const char ar1[] = "Something is pointing at me.";

以上两个声明标明:pt1和ar1都是该字符串的地址。在这两种情况下,带双引号的字符串本身决定了预留给字符串的存储空间。尽管如此,这两种形式并不完全相同。

二、数组和指针

数组形式指针形式有何不同?

先说数组:两个副本

以上面的声明为例,数组形式(ar1[])在计算机的内存中分配为一个内含29个元素的数组(每个元素对应一个字符,还加上一个末尾的空字符'\0'),每个元素被初始化为字符串字面量对应的字符。通常,字符串都作为可执行文件的一部分储存在数据段中。当把程序载入内存时,也载入了程序中的字符串。字符串储存在静态存储区(static memory)中。但是,程序在开始运行时才会为该数组分配内存。此时,才将字符串 拷贝 到数组中。

注意,此时字符串有两个副本。一个是在静态内存中的字符串字面量,另一个是储存在ar1数组中的字符串

此后,编译器便把数组名ar1识别为该数组首元素地址(&ar1[0])的别名。这里关键要理解,在数组形式中,ar1是地址常量

不能更改ar1,如果改变了ar1,则意味着改变了数组的存储位置(即地址)

可以进行类似ar1+1这样的操作,标识数组的下一个元素。但是不允许进行++ar1这样的操作。递增运算符只能用于变量名前(或概括地说,只能用于可修改的左值),不能用于常量。

再来看指针形式:

指针形式(*pt1)也使得编译器为字符串在静态存储区预留29个元素的空间。另外,一旦开始执行程序,它会为指针变量pt1留出一个储存位置并把字符串的地址储存在指针变量中。该变量最初指向该字符串的首字符,但是它的值可以改变。因此,可以使用递增运算符。例如,++pt1将指向第 2 个字符(o)。

字符串字面量被视为const数据。(字符串常量)由于pt1指向这个const数据,所以应该把pt1声明为指向const数据的指针。这意味着不能用pt1改变它所指向的数据,但是仍然可以改变pt1的值(即,pt1指向的位置)。

如果把一个字符串字面量拷贝给一个数组就可以随意改变数据,除非把数组声明为const。

总之,初始化数组把静态存储区的字符串 拷贝 到数组中,而初始化指针只把字符串的 地址拷贝 给指针。

实例代码观察上述:

#include <stdio.h>
#define MSG "I'm special"


int main()
{
	char ar[] = MSG;
	const char* pt = MSG;
	printf("Address of \"I'm special\":	%p \n", "I'm special");
	printf("		address ar:	%p \n", ar);
	printf("		address pt:	%p \n", pt);
	printf("		address of MSG:	%p \n", MSG);
	printf("address of \"I'm special\":	%p  \n", "I'm special");

	system("pause");
	return 0;
}

输出:

Address of "I'm special":       00DC7B30
                address ar:     003DF89C
                address pt:     00DC7B30
                address of MSG: 00DC7B30
address of "I'm special":       00DC7B30

该程序的输出说明了什么?

  • 第一,pt和MSG的地址相同,而ar的地址不同,这与我们前面讨论的内容一致。
  • 第二,虽然字符串字面量"I'm special"在程序的两个 printf()函数中出现了两次,但是编译器只使用了一个存储位置,而且与MSG的地址相同。编译器可以把多次使用的相同字面量储存在一处或多处。另一个编译器可能在不同的位置储存3个"I'm special"。
  • 第三,静态数据使用的内存与ar使用的动态内存不同。不仅值不同,特定编译器甚至使用不同的位数表示两种内存。

结论:

  • 初始化数组把静态存储区的字符串 拷贝 到数组中。这个拷贝生成一份新的数据,新的地址。
  • 初始化指针只把字符串的 地址 拷贝 给指针。


三、数组和指针的区别

初始化字符数组来储存字符串和初始化指针来指向字符串有何区别(“指向字符串”的意思是指向字符串的首字符)?例如,假设有下面两个声明:

char heart[] = "I love Tillie!";
const char *head = "I love Millie!";

两者主要的区别是:数组名heart是常量,而指针名head是变量。那么,实际使用有什么区别?

 首先,两者都可以使用数组表示法:

其次,两者都能进行指针加法操作:

void test02()
{
	char heart[] = "I love Tillie!";
	const char* head = "I love Millie!";
    // 首先,两者都可以使用数组表示法:
	for (size_t i = 0; i < 6; i++)
	{
		putchar(heart[i]);
	}
	putchar('\n');

	for (size_t i = 0; i < 6; i++)
	{
		putchar(head[i]);
	}
	putchar('\n');
	// 两者都输出:
	// I love
	// I love

    // 其次,两者都能进行指针加法操作:
	for (size_t i = 0; i < 6; i++)
	{
		putchar(*(heart + i));
	}
	putchar('\n');
	for (size_t i = 0; i < 6; i++)
	{
		putchar(*(head + 1));
	}
	putchar('\n');
}

但是,只有指针表示法可以进行递增操作:

while (*(head) != '\0')  /* 在字符串末尾处停止*/

    putchar(*(head++));  /* 打印字符,指针指向下一个位置 */

假设想让head和heart统一,可以这样做:

head = heart;   /* head现在指向数组heart */

这使得head指针指向heart数组的首元素。

但是,不能这样做:

heart = head;   /* 非法构造,不能这样写 */

这类似于x = 3;和3 = x;的情况。赋值运算符的左侧必须是变量(或概括地说是可修改的左值),如*pt_int。顺带一提,head = heart;不会导致head指向的字符串消失,这样做只是改变了储存在head中的地址。除非已经保存了"I love Millie!"的地址,否则当head指向别处时,就无法再访问该字符串。

数组的元素是变量(除非数组被声明为const),但是数组名不是变量。是常量。

因此,建议在把指针初始化为字符串字面量时使用const限定符:

const char * pl = "Klingon";  // 推荐用法

然而,把非const数组初始化为字符串字面量却不会导致类似的问题。因为数组获得的是原始字符串的副本。

总之,如果不修改字符串,不要用指针指向字符串字面量。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值