如果想修改字符串,就复制它
如果想要修改字符串的内容,就需要对它的副本进行操作。如果在存储器的非只读区域创建了字符串的副本,就可以修改它的字母了。
怎么创建副本?用字符串创建一个新数组就行了。
为什么这个方法行得通呢?一切字符串皆为数组,但是在旧代码中,
cards 只是一个指针,而在新代码中,它是一个数组。假如你声明了一个名为 cards 的数组,然后把它设置成字符串字面值,cards 数组就成为了一个全新副本。cards 不再只是一个指向字符串字面值的指针,而是一个崭新的数组,里面保存了字符串字面值的最新副本。
为了弄明白它实际是怎么工作的,需要看看存储器中发生了什么。
百宝箱
cards[]还是*cards?如果看到这样的声明,会觉得它是什么意思?char cards[]
这取决于在什么地方看到它,如果是普通的变量声明,cards 就是一个数组,而且必须立即赋值:但如果 cards 以函数参数的形式声明,那么 cards 就是一个指针:
存储器中的 char cards[]="JQK";
我们已经见过了那段有问题的代码在运行时计算机做了哪些事。那么新代码呢?我们来瞧瞧。
- 计算机载入字符串字面值。
和刚刚一样,当计算机把程序载入存储器时,会把常量值(如字符串“JQK”)保存到只读存储器。
- 程序在栈上新建了一个数组。
我们声明了数组,所以程序会创建一个足够大的数组来保存字符串“JQK”,在这个例子中 4 个字符足矣。
- 程序初始化数组。
除了为数组分配空间,程序还会把字符串字面值“JQK”的内容复制到栈上。
区别是原来的代码使用了指向只读字符串字面值的指针;而在新的代码中,你用字符串字面值初始化了一个数组,从而得到了这些字母的副本,这样就可以随意修改它们了。
试驾
在代码中构造一个新数组,看看会发生什么。
代码正确工作了!
cards 变量现在指向存储器中非只读区域中的字符串,所以我们可以自由地修改它的内容。
百宝箱
为了从此避免这个错误,可以不再将 char 指针设置为字符串字面值,像这样:char *s = "Some string";
但是把指针设为字符串字面值又没错,问题出在你试图修改字符串字面值。如果你想把指针设成字符串字面值,必须确保使用了 const 关键字:const char *s = "some string";
这样一来,如果编译器发现有代码试图修改字符串,就会提示编译错误:s[0] = 'S';monte.c:7: error: assignment of read-only location
神奇子弹案件
他正在向音乐管理软件中导入“枪炮玫瑰”乐队的全集,突然听到有人敲门。一个女人走了进来,她身高 1 米 68,金色的头发,挎了一个别致的笔记本包,穿了一双便宜的鞋。他有很强烈的预感,她是个女程序员。“求你帮帮我……你一定要洗刷他的清白,我可以保证,Jimmy 是无辜的!”他递给她一张纸巾让她擦掉眼泪,请她就坐。
和所有老掉牙的故事一样,故事的开头是:她遇到了一个男人。Jimmy Blomstein 是当地一家星巴克的服务生,他的业余爱好是骑自行车与收集动物标本,他梦想着有一天能制作一头大象标本。可惜他交友不慎。那天早上,蒙面劫匪 Masked Raider 去喝咖啡时遇到了 Jimmy,那时他们还活着:char masked_raider[] = "Alive";char *jimmy = masked_raider; printf("Masked raider is %s, Jimmy is %s\n", masked_raider,jimmy);
那天下午,蒙面劫匪动身去抢劫酒吧。过去的一百次行动他都未曾失手,但这一次,当他一脚踹开 Head First 酒吧地下室的门时,却发现联邦调查局的人正在玩“三猜一”欢度周末。枪声响起,一声尖叫,歹徒倒在人行道上,人群一阵骚乱。masked_raider[0] = 'D';masked_raider[1] = 'E';masked_raider[2] = 'A';masked_raider[3] = 'D';masked_raider[4] = '!';
奇怪的是,当这个女人去咖啡店找他男朋友时,却被告知昨晚他调的那杯橙色摩卡冰乐已成了绝唱。printf("Masked raider is %s, Jimmy is %s\n", masked_raider, jimmy);
到底发生了什么?为什么一颗神奇的子弹同时杀死了 Jimmy 和蒙面劫匪?你认为发生了什么?
神奇子弹案件
为什么一颗神奇的子弹同时杀死了 Jimmy 和蒙面劫匪?
在大恶魔蒙面劫匪被击毙的一瞬间,Jimmy,这位温文尔雅的咖啡师也被离奇击中:#include <stdio.h>int main(){ char masked_raider[] = "Alive"; char *jimmy = masked_raider; printf("Masked raider is %s, Jimmy is %s\n", masked_raider, jimmy); masked_raider[0] = 'D'; masked_raider[1] = 'E'; masked_raider[2] = 'A'; masked_raider[3] = 'D'; masked_raider[4] = '!'; printf("Masked raider is %s, Jimmy is %s\n", masked_raider, jimmy); return 0;}
“恐怕我要告诉你一个坏消息,Jimmy 和蒙面劫匪……是同一个人。”
“怎么可能!”
她猛吸一口气,用手捂住了嘴。“抱歉,女士,但我必须告诉你我是怎么看出来的。来看看存储器的使用情况。”他画了一个图。
“jimmy 和 masked_raider 是同一个存储器地址的两个别名,它们都指向了同一个地方。当 masked_raider 被子弹击中时,Jimmy 也无法幸免。这里还有一张发票,抬头是旧金山大象保护中心,这里还有一张 15 吨包装材料的订单,铁证如山,我想真相已经大白。”
要点
如果在变量声明中看到
* ,说明变量是指针。
字符串字面值保存在只读存储器中。
如果想要修改字符串,需要在新的数组中创建副本。
可以将 char 指针声明成为 const char * ,以防代码用它修改字符串。
这里没有蠢问题问:为什么编译器不直接告诉我“不能修改字符串”?答:我们只是把 cards 声明成 char * ,编译并不知道这个变量指向字符串字面值。问:为什么字符串字面值要保存在只读存储器中?答:因为它们专门用来表示常量。如果你写了一个打印“Hello World”的函数,一定不想让程序的其他部分修改“Hello World”这个字符串字面值。问:只读变量在所有操作系统中都不能够修改吗?答:大部分操作系统都有这个规定,一些 Cygwin 的 gcc 版本允许修改字符串字面值,不会报错,但这样做常常是错的。问:const 到底是什么意思?它能让字符串变成只读吗?答:加不加 const ,字符串字面值都是只读的,const 修饰符表示,一旦你试图用 const 修饰过的变量去修改数组,编译器就会报错。问:在存储器中,不同的存储器段总是以相同的顺序出现吗?答:在同一种操作系统中它们出现的顺序相同,不同操作系统之间略有差异,例如 Windows 的代码段就不在地址的低位。问:我还是不理解,为什么数组变量不保存在存储器中?既然它存在,就应该在某个地方,不是吗?答:程序在编译期间,会把所有对数组变量的引用替换成数组的地址。也就是说在最后的可执行文件中,数组变量并不存在。既然数组变量从来不需要指向其他地方,有和没有其实都一样。问:每当我把一个新数组设为字符串字面值,程序实际上会复制字符串字面值的内容吗?答:这取决于编译器,最后的机器代码既有可能把整个字符串字面值的内容复制到数组,也有可能程序会根据声明设置每个字符的值。问:你老是在说“声明”,它是什么意思?答:声明是一段代码,它声称某样东西(变量或函数)存在;而定义说明它是什么东西。如果在声明了变量的同时将其设为某个值(例如 int x = 4; ),这段代码既是声明又是定义。问:为什么 scanf() 要被称为 scanf() ?答:scanf() 其实表示“scan formatted”,它用来扫描带格式的输入。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论