b97902HW 板


LINE

前言 上一篇文章我们简单地介绍了阵列的基本概念,我们可以把阵列想像 成一个表格,每一个表格可以有若干个储存格,每一个储存格有一个 索引(Index),可以存一个元素(Element)。 在这一篇我要来介绍阵列的进阶用法。在这一篇文章我会提到字串 (String)。 在 C 语言中,如果我们要输出、读入一串文字,我们都会用到字串。 即使是像 Hello World 这一种简单的程式我们也已经用到字串了, 所以字串的重要性可见一斑。我们在这一篇文章会讨论这一个问题。 printf("Hello world!\n"); 预备知识 1. 字元型别有二种,一种是 char,另一种是 wchar_t。前者主要用 於 ASCII、BIG5 环境,後者主要用於 Unicode 环境。不过因为 一些因素,本文将只讨论 char 的使用。 2. 字元字面常数(Character Literal Constant),一般而言我们会 用数字来代表字元,不过,因为并不是所有的环境都使用相同的 数字来表示同一个字元(例如: ASCII v.s. EBCDIC),所以为了可 移植性(Portability),我们会用 'A' 来代表「用以表示 A 这一 个字元的数字」,其余类推。 3. 脱逸(Escape),因为有些字元如果想用字元字面常数来表示,就 会破坏 C 语言的语法规则,所以我们不能直接用 '<字元>' 来表 示,而又有不少是很常见的字元,所以我们有其他的表示法。例 如换行字元,我们就用 \n 来代替;单引号字元,我们就用 \' 来代替。又因为 \ 被拿来当作脱逸符号,所以我们就用 \\ 来 代替反斜线。部分替换表如下: ┌───┬───┬───┬──────────┐ │字元脱逸ASCII说明 │ ├───┼───┼───┼──────────┤ │Null\00x00空字元(字串终止字元)│ ├───┼───┼───┼──────────┤ │Tab │\t │0x09 │ │ ├───┼───┼───┼──────────┤ │换行 │\n │0x0A │ │ ├───┼───┼───┼──────────┤ │" │\" │0x22 │在字串内的双引号 │ ├───┼───┼───┼──────────┤ │' │\' │0x27 │'\'' 单引号的代号 │ ├───┼───┼───┼──────────┤ │\ │\\ │0x5C │脱逸脱逸字元 │ └───┴───┴───┴──────────┘ 字串 在 C 语言中,字串的定义为:一个以字元型别为元素型别的阵列, 而且它必需以 空字元(\0) 结尾。另外,还有一种东西也可以算是字 串,那就是 字串字面(String Literal)。所谓的字串字面就是指以 双引号夹起来的东西,我们下面会再提到。 ┌────────────────────────────┐ │一段故事:如果你有 Windows 程式设计经验的话,你一定有看 │ │过「匈牙利命名法」。一种会在变数名称前面加上型别资讯的命│ │名方法。在匈牙利命名法中,有一个规则就是字串变数的名字会│ │加上 sz,例如 szName 之类的。其中,sz 指的就是 Zero-ter-│ │minated String 的意思。微软早期的工程师就是用这一种方法 │ │来让错误的程式码容易看得出来。不过,要不要用匈牙利命名法│ │呢?我想...,微软的程式品质...,嗯,总之师其意可,师其法│ │则万万不可。(因为有其他的缺点) │ └────────────────────────────┘ 根据上面的定义,我们知道,下面 array1 是字串,而 array2 和 array3 不是,为什麽? char array1[] = {'A', 'B', 'C', 'D', '\0'}; char array2[] = {'A', 'B', 'C', 'D', '\n'}; int array3[] = {'A', 'B', 'C', 'D', '\0'}; 很显然地,array1 满足我们字串的定义,它是一个以 char 为元素 型别的阵列而且以空字元(\0)结尾。不过,array2 就不是,因为它 不是以空字元(\0)结尾;而 array3 也不是,因为它的元素型别是 int 并不是字元型别。如果我们对 array2 做以下修改,我们也可以 称它为字串,因为有以空字元(\0)结尾,即便 \0 不是在阵列的最後 一个位置,也仍然是字串。 array2[2] = '\0'; /* After this assign operation, array2 is a string. */ 可是如果每一次要用字串的时候,我们都需要用上面 array1 的方式 来宣告,那写 C 语言程式的时候根本是一场梦魇。所以有一种叫作 字串字面的东西就被发明了。向上面的 array1 可以简写成像 array4 这样: char array4[] = "ABCD"; 我们可以直接用一对双引号括住若干个字元,它就会变成字串。当然 也因为如此,你的字串中如果有 ",你就必需用脱逸字元来处理。另 外,如果你很细心的话,你会发现,array4 没有用 \0 来终止字串, 你想必会质疑:为何它可以是字串? 事实上,字串字面会自动的帮我们在字串的後方加上一个空字元,所 以 "ABCD" 是 等价 {'A', 'B', 'C', 'D', '\0'} 的。也因为如此, 如果你写出下面的程式码,编译器会向你抱怨:阵列过小,因为事实 上 "ABCD" 有五个字元。 char array5[4] = "ABCD"; /* ERROR! */ 当然也因为 字串字面(String Literal) 会自动加上 \0,所以和字 元字面常数是不同的东西。简单来说 "A" 不等价 'A'。 字串函式 C 语言的标准函式库里面有若干个有用的函式,可以帮你简化字串处 理的繁锁工作。它的功能涵盖输入输出、字串比较、字串串接、等字 串处理工作。本文将其中一部分简述如下,不过限於篇幅,有些函式 被我省略掉了,如要更详细的资讯,大家可以自行去寻找「C Stand- ard Libray」的相关资料。 #include <stdio.h> stdio.h 这一个标头档,专门放输入输出的函式原型。所以如果要输 入抑或输出,你都必需引入 stdio.h 标头档。 scanf("%s", ...); 首先,字串不会总是写死在程式里,有时候我们也必需要让使用者 输入字串。在 C 语言中,我们可以用 scanf 来输入字串。用法和 我们读入数字的方法很像,不同点是我们用的不是 %d 而是 %s。 另外,我们不用使用取址(Address-of)运算子,直接把阵列名称传 入就好了(为什麽会这样呢?我们有机会说到指标再谈)。例如: char input[16]; scanf("%s", input); 这样我们就可以读入一个字串。假如我输入 xyz 之後按下 Enter, input 的前四个元素就会是 'x', 'y', 'z', '\0'。也就是说: input[0] == 'x'; input[1] == 'y'; input[2] == 'z'; input[3] == '\0'; scanf 会一直读入字元,写到阵列中,直到遇到「空白、换行、 Tab」等字元,它才会在阵列加上 \0,然後停止。不过,细心一点 的读者很快就会发现问题:我输入多少字,不论阵列够不够大, scanf 都会照单全收!进而产生缓充区溢位(Buffer Overflow)! 这是很多程式会有安全漏洞的原因。要怎麽避免呢?你可以在 s 的前面在上数字,表示字串(不含 \0)最多可以多长。例如: char input[16]; scanf("%15s", input); 就表示:最多可以输入 15 个字元,加上 1 个 \0 做为字串结尾。 如此一来就可以确保不会溢位。 printf("%s", ...); 当然我们常常会有输出字串的需求,我们要怎麽输出字串呢?当然 直接用 printf(字串) 也是一个方法。不过,他却不一定是一个好 方法。举例来说:如果字串是由使用者输入的,你本来不预期使用 者会输入 % 字元,可是使用者输入的话会发生什麽事呢?请自行 想像!XD 所以正确的方法是什麽呢?标准答案是 printf("%s", 字串); printf("%s", 字串); 使用 %s 来告诉 printf 函式:我们要印出一个字串,请把字串中 除了 \0 之外的所有字元都印到萤幕上。例如: char str[] = "String!"; printf("%s", str); printf("%s", "This is my string!"); fgets(str, n, stdin); 有时候,我们想要读入的字串含有空白,我们希望以换行字元为分 怎麽办呢?这时候 fgets 就可以上场了,fgets 可以帮我们读 入字元,直到遇上换行字元。第一个引数是字串,第二个引数是你 希望最多读入多少字元(含 \0),第三个引数大家先打 stdin 就可 以了。 char str[16]; fgets(str, 16, stdin); 以上会读入最多 15 个字元加上一个 \0。会以换行为分界。如果 输入的字元少於 15 个字元,在 \0 之前会有一个 \n。如果你不 想要他,你可以用下面的方式来处理。 if (strchr(str, '\n')) { *strchr(str, '\n') = '\0'; } #include <string.h> string.h 这一个标头档顾名思义,就是要用来处理字串用的。不过 除了处理字串的函式之外,还有一些是处理记忆体的,或者正确的说 是处理一段记忆体空间。 值得注意的是:以 str 开头的函式,在引数的部分会假定输入的字 串都是以真得字串,如果你传入的只是一个字元阵列,而且不以 \0 结尾,则你的程式执行起来会如何,没有人会知道。所以写程式的时 候要小心! strlen(str); 这一个函式以一个字串为引数,然後它会回传这一个字串的字数 (STRing LENgth)。所谓的字数,指得是「非 \0 的字数」,也就 是在碰到 \0 之前一共有多少个字。例如: char str1[] = "ABCD"; char str2[] = {'q', 'w', 'e', 'r', 't', 'y', '\0'}; size_t strlen1 = strlen(str1); /* == 4 */ size_t strlen2 = strlen(str2); /* == 6 */ strcmp(stra, strb); 这一个函式是用来比较二个字串的大小(STRing CoMPare)。如果二 个字串全等,则传会 0。如果二个字串之间第一个不相同的字元的 索引是 i,则回传值会和 stra[i] - strb[i] 同号。也就是说在 ASCII 环境下, strcmp("A", "a") < 0 strcmp("Ab", "AB") > 0 strcmp("A", "A") == 0 strcpy(dest, src); 这一个函式的用途是把一个字串复制到另一个字元阵列(STRing CoPY)。第一个引数是目标阵列,第二个引数是来源字串。这一个 函式会把 src 所有「在 \0 之前包含 \0 的字元」复制到 dest。 char receive[1024]; char source[] = "ABCDEFG"; strcpy(receive, source); printf("%s", receive); /* We get "ABCDEFG" as output */ 不过必需注意,如果 dest 的大小,不足以容纳 src,则你的程式 极可能发生执行阶段错误。这也是不少安全漏洞的来源,所以写程 式的时候请千万小心。 char buffer[1]; char source2[] = "This Is Going To Overflow!!!! HA! HA! HA!"; strcpy(buffer, source2); /* 错误!缓冲区溢位 */ strncpy(dest, src, n); 这一个函式也是要用来复制字串。不过和 strcpy 不同的是,它最 多只会复制 n 个字元。如果 strlen(src) < n,\0 会被复制,如 果 strlen(src) >= n,则 \0 就不会被复制。所以 strncpy 复制 出来的东西不一定是字串。 char buffer1[1024], buffer2[1024]; char buffer3[1024], buffer4[1024]; strncpy(buffer1, "ABCDE", 4); /* buffer1 不是字串 */ strncpy(buffer2, "ABCDE", 5); /* buffer2 也不是字串 */ strncpy(buffer3, "ABCDE", 6); /* buffer3 是字串 */ strncpy(buffer4, "ABCDE", 7); /* buffer4 是字串 */ 不过如果你手动补上 \0,buffer1、buffer2 也可以是字串。 buffer1[4] = '\0'; buffer2[5] = '\0'; 我们会有: strcmp(buffer1, "ABCD") == 0 /* 要补上 \0 才能满足条件 */ strcmp(buffer2, "ABCDE") == 0 /* 要补上 \0 才能满足条件 */ strcmp(buffer3, "ABCDE") == 0 strcmp(buffer4, "ABCDE") == 0 strcat(dest, src); 这一个函式的用途是把 src 字串接到 dest 字串之後(STRing conCATenate)。strcat 会先找到 dest 字串中的 \0 然後,从这 一个字元开始复制 src 的字元直到 src 结束为止。当然,src 的 \0 会被复制,所以执行完之後 dest 仍然会是一个字串。例如: char dest[1024] = "abc"; strcat(dest, "ABCDEFG"); printf("%s", dest); /* 输出 "abcABCDEFG" */ strcpy(dest, "12345"); strcat(dest, "HIJKL"); printf("%s", dest); /* 输出 "12345HIJKL" */ 不过和 strcpy 一样,strcat 不会在乎 dest 是否有足够的空间, 所以也会有溢位问题,你必需确定 dest 可以再加上 strlen(src) 个字元才能用这一个函式。请小心使用,我就不再赘述。 strncat(dest, src, n); 这一个函式和 strcat 一样是用来串接字串;和 strcat 不一样的 是有串接字数限制。和 strncpy 一样有字数限制,不同的是 \0 字元永远会被复制(事实上你可以想像成原本 dest 的 \0 被搬到 後面)。所以 dest 只要可以再加上 n 个字元就不会溢位。例如: char str1[10] = "ABCDE"; strncat(str1, "abcdefg", 4); /* OK! */ printf("%s", str1); /* 输出 "ABCDEabcd" */ memcpy(dest, src, n); 这一个函式可以把记忆体中的特定区块复制 n bytes 到另一处。 和 strcpy 不同,memcpy 会把从 src 算起一共 n bytes 搬到 dest 的位置。我们直接看范例: char str1[6]; char str2[] = "ABCDE"; memcpy(str1, str2, 6); printf("%s", str1); /* 输出 "ABCDE" */ char str3[] = "ABCDE"; char str4[] = "abcde"; memcpy(str3, str4, 2); printf("%s", str3); /* 输出 "abCDE" */ 有了基本概念之後,我们来看看变化型。我们在前一章有说到阵列 不可以 assign,我们要复制,只能一个元素一个元素地复制。不 过这样实在太麻烦了,有没有简单的方法?是有的。 int array1[] = {56, 58, 59}; int array2[3]; memcpy(array2, array1, sizeof(int) * 3); array2[0] == 56, array2[1] == 58, array2[2] == 59 稍微讲解一下,sizeof(int) 是指一个 int 要多少个 byte,我们 有空再谈。之後 memcpy 会帮我们复制 sizeof(int) * 3 个 bytes,所以 array2 的那一段记忆体空间会和 array1 的一样。 最後我们就会有和 array1 一样的 array2。 提醒一下: [dest, dest + n), [src, src + n) 最好不要重叠, 不然结果不被定义(EDIT: ckclark学长说,可以改用 memmove 函 式)。 memset(dest, ch, n); 这一个函式可以从 dest 这一个 byte 开始,把 n 个 bytes 全部 设为 ch 这一个字元。例如: char str[] = "ABCDEF"; memset(str, 'a', 3); printf("%s", str); /* 输出 "aaaDEF" */ 当然,只有这一种功能当然不够看。memset 最强大的功能就是可 以初始化阵列。不过我要先声明,这不是一个 Portable (可移植) 的方法 int array1[3]; memset(array1, 0, sizeof(int) * 3); /* 所有 array1 的元素是 0 */ int array2[3]; memset(array2, 0xFF, sizeof(int) * 3); /* 所有 array2 的元素是 -1 */ unsigned int array3[3]; memset(array3, 0xFF, sizeof(unsigned int) * 3); /* 所有 array3 的元素是 UINT_MAX */ 不过这一个方法,不是一个 Portable 的方法,它依赖平台的特性。 例如:如果有一个平台一个 9-bits 等於 1byte,array2 的元素 就不一定会是都是 -1。虽然,这一个方法很快速、简洁、好用, 不过,切记:跨平台的时候不要忘了! 备注:(在 C99, 也许 C89 也对) 如果要把阵列初始化为 0,大可 不用这种方法,还有更简单的方法,有空我们再谈。 结语 结语要说啥?我不知道....。打那麽多字,手好酸。总之,字串很重 要,看不懂的人,欢迎来信(MSN 的帐号,GMAIL结尾)。下集预 告:再看看.....(阵列III?)。 PS. 这就是 钢弹 吗?我可没有自信,第一次碰钢弹就可以重写 OS, 并干掉 Rusty.....。众人嘘:还不快滚去救生舱! PS2. 你的推文是我再写的原动力,你的嘘文是我自 d 的原动力。 --



※ 发信站: 批踢踢实业坊(ptt.cc)
◆ From: 140.112.241.166 ※ 编辑: LoganChien 来自: 140.112.241.166 (10/08 00:44) ※ 编辑: LoganChien 来自: 140.112.241.166 (10/08 00:44)
1F:推 ming1053:用gets就好了吧? 10/08 00:47
2F:推 ming1053:再推一个 这篇好文 10/08 00:51
3F:→ LoganChien:其实不说 gets 是故意的,连 strcpy, strcat 我都有一 10/08 00:54
4F:→ LoganChien:点犹豫,原因是... 不是很安全。 10/08 00:55
5F:推 iForests:彩色好文推 10/08 01:26
6F:推 drazi:科科 等到脱离scanf printf的时候 才会知道这些的珍贵... 10/08 02:03
7F:推 drazi:你要知道windows程式设计...要让东西出现在萤幕上 10/08 02:03
8F:推 drazi:跟读取Input是多困难的事情= = 噢还有...... 10/08 02:04
9F:推 drazi:匈牙利命名法有它存在的价值及必要......绝对有...... 10/08 02:04
10F:推 haoto:伤心TA推 目前AC的全都给我用array.....明明就不用...T_T 10/08 02:08
※ 编辑: LoganChien 来自: 140.112.241.166 (10/08 02:19)
11F:→ LoganChien:感谢 ckclark 助教/学长 的勘误。(204.) 10/08 02:22
12F:推 benck:太认真了 推一个 10/08 02:33
※ 编辑: LoganChien 来自: 140.112.241.166 (10/08 07:39)
13F:推 anfranion:不是很安全是指会不会用而造成的错误吗@@ 10/08 08:11
14F:→ anfranion:gets()比fgets基础很多,有f都是涉及开档读档了 10/08 08:12
15F:→ anfranion:虽然好像也是有人这样用啦囧 10/08 08:12
16F:推 sa072686:好文推XD 真是太详细了! 10/08 09:53
17F:推 godgunman:推好文XD 10/08 12:31
18F:→ hrs113355:fgets比gets安全... 10/08 12:52
19F:→ LoganChien:To drazi: 我觉得匈牙利命名法在那一个 type check 不 10/08 15:55
20F:→ LoganChien:是很好的年代,确实有它的好处。不过,我觉得现在的编 10/08 15:56
21F:→ LoganChien:译器都可以正确地检查型别,使用匈牙利命名法的理由有 10/08 15:57
22F:→ LoganChien:很大一部分被消灭了。除了像文中所提到的 sz 我觉得我 10/08 15:58
23F:→ LoganChien:不会想要用这一个命名法。因为太繁锁了,而且有一些现 10/08 16:00
24F:→ LoganChien:在来说不太必要(例如: lp 的 l 是指 long pointer)。 10/08 16:01
25F:→ LoganChien:除非要 Windows Programming 我现在都不会推荐别人用 10/08 16:02
26F:→ LoganChien:匈牙利命名法。不过话说回来,匈牙利命名法也已经是十 10/08 16:03
27F:→ LoganChien:多年的产物,以现今的角度看可能有失公允。 10/08 16:04
28F:→ LoganChien:To anfranion: 不是很安全指得是「你没有办法控制输入 10/08 16:06
29F:→ LoganChien:的大小」。如果你用 gets,然後,有心人士故意输入一个 10/08 16:07
30F:→ LoganChien:很长的字串,想要不溢位也难。如果溢位的是一般的字元 10/08 16:08
31F:→ LoganChien:还好,如果是精心设计,还可以执行你输入的字串(程式) 10/08 16:09
※ 编辑: LoganChien 来自: 140.112.241.166 (10/08 16:16)
32F:推 drazi:你说到重点了XD windows programming XD 10/08 18:05
33F:推 drazi:好啦我必须说有windows programming经验的就知道~ 没有的就. 10/08 18:05
34F:推 drazi:.....(摊手~) 10/08 18:05
35F:推 jimmycool:匈牙利命名法会让我想到这篇文章 http://0rz.tw/9e1op 10/08 19:03
36F:→ jimmycool:说得还满有道理的,可以参考看看:D 10/08 19:03
37F:推 drazi:大推XD 10/08 19:22
38F:推 dennis2030:楼主好文我顶 10/09 00:53
39F:→ LoganChien:To jimmycool: 那一篇我也看过,真得写得不错,不过他 10/09 00:59
40F:→ LoganChien:好像比较着重在型别之外加上抽象意义,而非把型别直接 10/09 01:04
41F:→ LoganChien:写上去。我觉得那一篇真得值得推荐! 10/09 01:05
※ 编辑: LoganChien 来自: 140.112.241.166 (10/09 01:06)
42F:推 anfranion:噢嗯原来是指这个方向的实作 楼主真强大XD 10/09 08:16
※ 编辑: LoganChien 来自: 140.112.241.166 (10/10 07:53)







like.gif 您可能会有兴趣的文章
icon.png[问题/行为] 猫晚上进房间会不会有憋尿问题
icon.pngRe: [闲聊] 选了错误的女孩成为魔法少女 XDDDDDDDDDD
icon.png[正妹] 瑞典 一张
icon.png[心得] EMS高领长版毛衣.墨小楼MC1002
icon.png[分享] 丹龙隔热纸GE55+33+22
icon.png[问题] 清洗洗衣机
icon.png[寻物] 窗台下的空间
icon.png[闲聊] 双极の女神1 木魔爵
icon.png[售车] 新竹 1997 march 1297cc 白色 四门
icon.png[讨论] 能从照片感受到摄影者心情吗
icon.png[狂贺] 贺贺贺贺 贺!岛村卯月!总选举NO.1
icon.png[难过] 羡慕白皮肤的女生
icon.png阅读文章
icon.png[黑特]
icon.png[问题] SBK S1安装於安全帽位置
icon.png[分享] 旧woo100绝版开箱!!
icon.pngRe: [无言] 关於小包卫生纸
icon.png[开箱] E5-2683V3 RX480Strix 快睿C1 简单测试
icon.png[心得] 苍の海贼龙 地狱 执行者16PT
icon.png[售车] 1999年Virage iO 1.8EXi
icon.png[心得] 挑战33 LV10 狮子座pt solo
icon.png[闲聊] 手把手教你不被桶之新手主购教学
icon.png[分享] Civic Type R 量产版官方照无预警流出
icon.png[售车] Golf 4 2.0 银色 自排
icon.png[出售] Graco提篮汽座(有底座)2000元诚可议
icon.png[问题] 请问补牙材质掉了还能再补吗?(台中半年内
icon.png[问题] 44th 单曲 生写竟然都给重复的啊啊!
icon.png[心得] 华南红卡/icash 核卡
icon.png[问题] 拔牙矫正这样正常吗
icon.png[赠送] 老莫高业 初业 102年版
icon.png[情报] 三大行动支付 本季掀战火
icon.png[宝宝] 博客来Amos水蜡笔5/1特价五折
icon.pngRe: [心得] 新鲜人一些面试分享
icon.png[心得] 苍の海贼龙 地狱 麒麟25PT
icon.pngRe: [闲聊] (君の名は。雷慎入) 君名二创漫画翻译
icon.pngRe: [闲聊] OGN中场影片:失踪人口局 (英文字幕)
icon.png[问题] 台湾大哥大4G讯号差
icon.png[出售] [全国]全新千寻侘草LED灯, 水草

请输入看板名称,例如:Soft_Job站内搜寻

TOP