如果希望拷贝整个字符串, 要使用strcpy()函数。
只有在输入以q开头的单词后才会递增计数器i, 而且该程序通过比较字符进行判断
strcpy()第2个参数(temp) 指向的字符串被拷贝至第1个参数(qword[i]) 指向的数组中。 拷贝出来的字符串被称为目标字符串, 最初的字符串被称为源字符串。 参考赋值表达式语句, 很容易记住strcpy()参数的顺序, 即第1个是目标字符串, 第2个是源字符串。
strcpy()接受两个字符串指针作为参数, 可以把指向源字符串的第2个指针声明为指针、 数组名或字符串常量; 而指向源字符串副本的第1个指针应指向一个数据对象(如, 数组) , 且该对象有足够的空间储存源字符串的副本。 记住, 声明数组将分配储存数据的空间, 而声明指针只分配储存一个地址的空间。
strcpy()的其他属性
strcpy()函数还有两个有用的属性。 第一, strcpy()的返回类型是 char *,该函数返回的是第 1个参数的值, 即一个字符的地址。 第二, 第 1 个参数不必指向数组的开始。 这个属性可用于拷贝数组的一部分。
strcpy()把源字符串中的空字符也拷贝在内。由于第1个参数是copy +7, 所以ps指向copy中的第8个元素(下标为7) 。 因此puts(ps)从该处开始打印字符串。
更谨慎的选择: strncpy()
strcpy()和 strcat()都有同样的问题, 它们都不能检查目标空间是否能容纳源字符串的副本。 拷贝字符串用 strncpy()更安全, 该函数的第 3 个参数指明可拷贝的最大字符数。
strncpy(target, source, n)把source中的n个字符或空字符之前的字符(先满足哪个条件就拷贝到何处) 拷贝至target中。 因此, 如果source中的字符数小于n, 则拷贝整个字符串, 包括空字符。 但是, strncpy()拷贝字符串的长度不会超过n, 如果拷贝到第n个字符时还未拷贝完整个源字符串, 就不会拷贝空字符。 所以, 拷贝的副本中不一定有空字符。
sprintf()函数
sprintf()函数声明在stdio.h中, 而不是在string.h中。该函数和printf()类似, 但是它是把数据写入字符串, 而不是打印在显示器上。 因此, 该函数可以把多个元素组合成一个字符串。 sprintf()的第1个参数是目标字符串的地址。 其余参数和printf()相同, 即格式字符串和待写入项的列表。
sprintf()函数获取输入, 并将其格式化为标准形式, 然后把格式化后的字符串储存在formal中。
其他字符串函数
char *strcpy(char * restrict s1, const char * restrict s2);
该函数把s2指向的字符串(包括空字符) 拷贝至s1指向的位置, 返回值是s1。
char *strncpy(char * restrict s1, const char * restrict s2, size_t n);
该函数把s2指向的字符串拷贝至s1指向的位置, 拷贝的字符数不超过n, 其返回值是s1。 该函数不会拷贝空字符后面的字符, 如果源字符串的字符少于n个, 目标字符串就以拷贝的空字符结尾; 如果源字符串有n个或超过n个字符, 就不拷贝空字符。
char *strncat(char * restrict s1, const char * restrict s2, size_t n);
该函数把s2字符串中的n个字符拷贝至s1字符串末尾。 s2字符串的第1个字符将覆盖s1字符串末尾的空字符。 不会拷贝s2字符串中空字符和其后的字符, 并在拷贝字符的末尾添加一个空字符。 该函数返回s1。
int strncmp(const char * s1, const char * s2, size_t n);
该函数的作用和strcmp()类似, 不同的是, 该函数在比较n个字符后或遇到第1个空字符时停止比较。
char *strchr(const char * s, int c);
如果s字符串中包含c字符, 该函数返回指向s字符串首位置的指针(末尾的空字符也是字符串的一部分, 所以在查找范围内) ; 如果在字符串s中未找到c字符, 该函数则返回空指针。
char *strpbrk(const char * s1, const char * s2);如果 s1 字符中包含 s2 字符串中的任意字符, 该函数返回指向 s1 字符串首位置的指针; 如果在s1字符串中未找到任何s2字符串中的字符, 则返回空字符。
char *strrchr(const char * s, int c);该函数返回s字符串中c字符的最后一次出现的位置(末尾的空字符也是字符串的一部分, 所以在查找范围内) 。 如果未找到c字符, 则返回空指针。
使用const关键字的函数原型表明, 函数不会更改字符串。
size_t类型是sizeof运算符返回的类型。 C规定sizeof运算符返回一个整数类型, 但是并未指定是哪种整数类型, 所以size_t在一个系统中可以是unsigned int, 而在另一个系统中可以是 unsigned long。 string.h头文件针对特定系统定义了 size_t, 或者参考其他有 size_t定义的头文件。
如果strchr()未找到换行符, fgets()在达到行末尾之前就达到了它能读取的最大字符数。
字符串示例:字符串排序
排列指针而非字符串
ptrst[0]被设置为input[0], ptrst[1]被设置为input[1], 以此类推。 这意味着指针ptrst[i]指向数组input[i]的首字符。 每个input[i]都是一个内含81个元素的数组, 每个ptrst[i]都是一个单独的变量。 排序过程把ptrst重新排列, 并未改变input。 例如, 如果按字母顺序input[1]在intput[0]前面, 程序便交换指向它们的指针(即ptrst[0]指向input[1]的开始, 而ptrst[1]指向input[0]的开始) 。 这样做比用strcpy()交换两个input字符串的内容简单得多, 而且还保留了input数组中的原始顺序。
选择排序算法
选择排序算法(selection sort algorithm) 来排序指针。利用for循环依次把每个元素与首元素比较。 如果待比较的元素在当前首元素的前面, 则交换两者。 循环结束时, 首元素包含的指针指向机器排序序列最靠前的字符串。 然后外层for循环重复这一过程, 这次从input的第2个元素开始。 当内层循环执行完毕时, ptrst中的第2个元素指向排在第2的字符串。 这一过程持续到所有元素都已排序完毕。
排序过程的伪代码:for n = 首元素至 n = 倒数第2个元素。
找出剩余元素中的最大值, 并将其放在第n个元素中 从n = 0开始, 遍历整个数组找出最大值元素, 那
该元素与第1个元素交换; 然后设置n = 1, 遍历除第1个元素以外的其他元
素, 在其余元素中找出最大值元素, 把该元素与第2个元素交换; 重复这一过程直至倒数第 2 个元素为止。 现在只剩下两个元素。 比较这两个元素, 把较大者放在倒数第2的位置。 这样, 数组中的最小元素就在最后的位置上。
在剩余项中查找最大值的方法是, 比较数组剩余元素的第1个元素和第2个元素。 如果第2个元素比第1个元素大, 交换两者。比较数组剩余元素的第1个元素和第3个元素, 如果第3个元素比较大, 交换两者。每次交换都把较大的元素移至顶部。 继续这一过程直到比较第 1 个元素和最后一个元素。
排列其他元素
for n - 第2个元素至最后一个元素
比较第n个元素与第1个元素, 如果第n个元素更大, 交换这两个元素的值
ctype.h字符函数和字符串
while (*str)循环处理str指向的字符串中的每个字符, 直至遇到空字符。此时*str的值为0(空字符的编码值为0) , 即循环条件为假, 循环结束。
ToUpper()函数利用toupper()处理字符串中的每个字符(由于C区分大小写, 所以这是两个不同的函数名) 。 根据ANSI C中的定义, toupper()函数只改变小写字符。
ctype.h中的函数通常作为宏(macro) 来实现。
程序使用 fgets()和 strchr()组合, 读取一行输入并把换行符替换成空字符。 这种方法与使用s_gets()的区别是: s_gets()会处理输入行剩余字符(如果有的话) , 为下一次输入做好准备。
命令参数
命令行(command line) 是在命令行环境中,用户为运行程序输入命令的行。
一个C程序可以读取并使用这些附加项
C编译器允许main()没有参数或者有两个参数(一些实现允许main()有更多参数, 属于对标准的扩展) 。 main()有两个参数时, 第1个参数是命令行中的字符串数量。
过去, 这个int类型的参数被称为argc (表示参数计数(argument count)) 。 系统用空格表示一个字符串的结束和下一个字符串的开始。repeat示例中包括命令名共有4个字符串, 其中后3个供repeat使用。 该程序把命令行字符串储存在内存中, 并把每个字符串的地址储存在指针数组中。 而该数组的地址则被储存在 main()的第 2 个参数中。这个指向指针的指针称为argv(表示参数值[argument value]) 。
int main(int argc, char **argv)
char **argv与char *argv[]等价。 也就是说, argv是一个指向指针的指针, 它所指向的指针指向 char。 因此, 即使在原始定义中, argv 也是指向指针(该指针指向 char) 的指针。 两种形式都可以使用, 但我们认为第1种形式更清楚地表明argv表示一系列字符串。
集成环境中的命令行参数
Windows集成环境(如Xcode、 Microsoft Visual C++和Embarcadero C++Builder) 都不用命令行运行程序。 有些环境中有项目对话框, 为特定项目指定命令行参数。 其他环境中, 可以在IDE中编译程序, 然后打开MS-DOS窗口在命令行模式中运行程序。
Macintosh中的命令行参数
如果使用Xcode 4.6(或类似的版本) , 可以在Product菜单中选择Scheme选项来提供命令行参数, 编辑Scheme, 运行。 然后选择Argument标签, 在Launch的Arguments Pass中输入参数。
或者进入Mac的Terminal模式和UNIX的命令行环境。 然后, 可以找到程
序可执行代码的目录(UNIX的文件夹) , 或者下载命令行工具, 使用gcc或clang编译程序。
把字符串转换为数字
数字既能以字符串形式储存, 也能以数值形式储存。 把数字储存为字符串就是储存数字字符。
C要求用数值形式进行数值运算(如, 加法和比较) 。 但是在屏幕上显示数字则要求字符串形式, 因为屏幕显示的是字符。 printf()和 sprintf()函数, 通过%d 和其他转换说明, 把数字从数值形式转换为字符串形式,scanf()可以把输入字符串转换为数值形式。 C 还有一些函数专门用于把字符串形式转换成数值形式。
$是UNIX和Linux的提示符(一些UNIX系统使用%) 。 命令行参数3被储存为字符串3\0。 atoi()函数把该字符串转换为整数值3, 然后该值被赋给times。 该值确定了执行for循环的次数。
运行该程序时没有提供命令行参数, 那么argc < 2为真, 程序给出一条提示信息后结束。 如果times 为 0 或负数, 情况也是如此。 C 语言逻辑运算符的求值顺序保证了如果 argc < 2, 就不会对atoi(argv[1])求值。
如果字符串仅以整数开头, atio()函数也能处理, 它只把开头的整数转换为字符。在我们所用的C实现中, 如果命令行参数不是数字, atoi()函数返回0。 然而C标准规定, 这种情况下的行为是未定义的。
程序中包含了stdlib.h头文件, 因为从ANSI C开始, 该头文件中包含了atoi()函数的原型。 除此之外, 还包含了 atof()和 atol()函数的原型。 atof()函数把字符串转换成 double 类型的值, atol()函数把字符串转换成long类型的值。 atof()和atol()的工作原理和atoi()类似, 因此它们分别返回double类型和long类型。
ANSI C还提供一套更智能的函数: strtol()把字符串转换成long类型的值, strtoul()把字符串转换成unsigned long类型的值, strtod()把字符串转换成double类型的值。这些函数的智能之处在于识别和报告字符串中的首字符是否是数字。 而且, strtol()和strtoul()还可以指定数字的进制。
当base分别为10和16时, 字符串"10"分别被转换成数字10和16。 还要注意, 如果end指向一个字符, *end就是一个字符。 因此, 第1次转换在读到空字符时结束, 此时end指向空字符。 打印end会显示一个空字符串, 以%d转换说明输出*end显示的是空字符的ASCII码。
对于第2个输入的字符串, 当base为10时, end的值是'a'字符的地址。 所以打印end显示的是字符串"atom", 打印*end显示的是'a'字符的ASCII码。当base为16时, 'a'字符被识别为一个有效的十六进制数, strtol()函数把十六进制数10a转换成十进制数266。
strtol()函数最多可以转换三十六进制, 'a'~'z'字符都可用作数字。strtoul()函数与该函数类似, 但是它把字符串转换成无符号值。 strtod()函数只以十进制转换, 因此它值需要两个参数。
关键概念
字符串, 无论是由字符数组、 指针还是字符串常量标识, 都储存为包含字符编码的一系列字节, 并以空字符串结尾。 C 提供库函数处理字符串, 查找字符串并分析它们。牢记, 应该使用 strcmp()来代替关系运算符,当比较字符串时, 应该使用strcpy()或strncpy()代替赋值运算符把字符串赋给字符数组。
本章小结
C字符串是一系列char类型的字符, 以空字符('\0') 结尾。 字符串可以储存在字符数组中。 字符串还可以用字符串常量来表示, 里面都是字符, 括在双引号中(空字符除外) 。 编译器提供空字符。 因此, "joy"被储存为4个字符j、 o、 y和\0。 strlen()函数可以统计字符串的长度, 空字符不计算在内。
字符串常量也叫作字符串——字面量, 可用于初始化字符数组。 为了容纳末尾的空字符, 数组大小应该至少比容纳的数组长度多1。 也可以用字符串常量初始化指向char的指针。
函数使用指向字符串首字符的指针来表示待处理的字符串。 通常, 对应的实际参数是数组名、 指针变量或用双引号括起来的字符串。 无论是哪种情况, 传递的都是首字符的地址。 一般而言, 没必要传递字符串的长度, 因为函数可以通过末尾的空字符确定字符串的结束。
fgets()函数获取一行输入, puts()和 fputs()函数显示一行输出。 它们都是stdio.h 头文件中的函数, 用于代替已被弃用的gets()。
C库中有多个字符串处理函数。 在ANSI C中, 这些函数都声明在string.h文件中。 C库中还有许多字符处理函数, 声明在ctype.h文件中。
给main()函数提供两个合适的形式参数, 可以让程序访问命令行参数。第1个参数通常是int类型的argc, 其值是命令行的单词数量。 第2个参数通常是一个指向数组的指针argv, 数组内含指向char的指针。 每个指向char的指针都指向一个命令行参数字符串, argv[0]指向命令名称, argv[1]指向第1个命令行参数, 以此类推。
atoi()、 atol()和atof()函数把字符串形式的数字分别转换成int、 long 和double类型的数字。 strtol()、 strtoul()和strtod()函数把字符串形式的数字分别转换成long、 unsigned long和double类型的数字。
存储类别
C提供了多种不同的模型或存储类别(storage class) 在内存中储存数据。
从硬件方面来看, 被储存的每个值都占用一定的物理内存, C 语言把这样的一块内存称为对象(object) 。 对象可以储存一个或多个值。 一个对象可能并未储存实际的值, 但是它在储存适当的值时一定具有相应的大小(面向对象编程中的对象指的是类对象, 其定义包括数据和允许对数据进行的操作, C不是面向对象编程语言) 。
从软件方面来看, 程序需要一种方法访问对象。
int entity = 3;
该声明创建了一个名为entity的标识符(identifier) 。 标识符是一个名称, 在这种情况下, 标识符可以用来指定(designate) 特定对象的内容。 标识符遵循变量的命名规则。
变量名不是指定对象的唯一途径。
int * pt = &entity;
int ranks[10];
第1行声明中, pt是一个标识符, 它指定了一个储存地址的对象。 但是, 表达式*pt不是标识符, 因为它不是一个名称。 然而, 它确实指定了一个对象, 在这种情况下, 它与 entity 指定的对象相同。 一般而言, 那些指定对象的表达式被称为左值entity既是标识符也是左值; *pt既是表达式也是左值。
ranks + 2 * entity既不是标识符(不是名称) , 也不是左值(它不指定内存位置上的内容) 。 但是表达式*(ranks + 2 * entity)是一个左值, 因为它的确指定了特定内存位置的值, 即ranks数组的第7个元素。
const char * pc = "Behold a string literal!";
程序根据该声明把相应的字符串字面量储存在内存中, 内含这些字符值的数组就是一个对象。 由于数组中的每个字符都能被单独访问, 所以每个字符也是一个对象。 该声明还创建了一个标识符为pc的对象, 储存着字符串的地址。 由于可以设置pc重新指向其他字符串, 所以标识符pc是一个可修改的左值。 const只能保证被pc指向的字符串内容不被修改, 但是无法保证pc不指向别的字符串。 由于*pc指定了储存'B'字符的数据对象, 所以*pc 是一个左值, 但不是一个可修改的左值。 与此类似, 因为字符串字面量本身指定了储存字符串的对象, 所以它也是一个左值, 但不是可修改的左值。
可以用存储期(storage duration) 描述对象, 所谓存储期是指对象在内存中保留了多长时间。 标识符用于访问对象, 可以用作用域(scope) 和链接(linkage) 描述标识符, 标识符的作用域和链接表明了程序的哪些部分可以使用它。 不同的存储类别具有不同的存储期、 作用域和链接。 标识符可以在源代码的多文件中共享、 可用于特定文件的任意函数中、 可仅限于特定函数中使用, 甚至只在函数中的某部分使用。 对象可存在于程序的执行期, 也可以仅存在于它所在函数的执行期。 对于并发编程, 对象可以在特定线程的执行期存在。 可以通过函数调用的方式显式分配和释放内存。
作用域
作用域描述程序中可访问标识符的区域。 一个C变量的作用域可以是块作用域、 函数作用域、 函数原型作用域或文件作用域。
示例中使用的变量几乎都具有块作用域。 块是用一对花括号括起来的代码区域。 例如, 整个函数体是一个块, 函数中的任意复合语句也是一个块。 定义在块中的变量具有块作用域(block scope) , 块作用域变量的可见范围是从定义处到包含该定义的块的末尾。 另外, 虽然函数的形式参数声明在函数的左花括号之前, 但是它们也具有块作用域, 属于函数体这个块。
具有块作用域的变量都必须声明在块的开头。 C99 标准放宽了这一限制, 允许在块中的任意位置声明变量。
for (int i = 0; i < 10; i++)
printf("A C99 feature: i = %d", i);
C99把块的概念扩展到包括for循环、 while循环、do while循环和if语句所控制的代码, 即使这些代码没有用花括号括起来,也算是块的一部分。 所以, 上面for循环中的变量i被视为for循环块的一部分, 它的作用域仅限于for循环。 一旦程序离开for循环, 就不能再访问i。
函数作用域(function scope) 仅用于goto语句的标签。 这意味着即使一个标签首次出现在函数的内层块中, 它的作用域也延伸至整个函数。 如果在两个块中使用相同的标签会很混乱, 标签的函数作用域防止了这样的事情发生。
函数原型作用域的范围是从形参定义处到原型声明结束。 这意味着, 编译器在处理函数原型中的形参时只关心它的类型, 而形参名( 如果有的话)通常无关紧要。 而且, 即使有形参名, 也不必与函数定义中的形参名相匹配。 只有在变长数组中, 形参名才有用。
变量的定义在函数的外面, 具有文件作用域( file scope) 。 具有文件作用域的变量, 从它的定义处到该定义所在文件的末尾均可见。
通常在源代码(.c扩展名) 中包含一个或多个头文件(.h 扩展名) 。 头文件会依次包含其他头文件, 所以会包含多个单独的物理文件。 但是, C预处理实际上是用包含的头文件内容替换#include指令。 所以, 编译器源代码文件和所有的头文件都看成是一个包含信息的单独文件。 这个文件被称为翻译单元(translation unit) 。 描述一个具有文件作用域的变量时, 它的实际可见范围是整个翻译单元。 如果程序由多个源代码文件组成, 那么该程序也将由多个翻译单元组成。 每个翻译单元均对应一个源代码文件和它所包含的文件。
链接
C 变量有 3 种链接属性: 外部链接、 内部链接或无链接。 具有块作用域、 函数作用域或函数原型作用域的变量都是无链接变量。 这意味着这些变量属于定义它们的块、 函数或原型私有。 具有文件作用域的变量可以是外部链接或内部链接。 外部链接变量可以在多文件程序中使用, 内部链接变量只能在一个翻译单元中使用。
C 标准用“内部链接的文件作用域”描述仅限于一个翻译单元(即一个源代码文件和它所包含的头文件) 的作用域, 用“外部链接的文件作用域”描述可延伸至其他翻译单元的作用域。 但是, 对程序员而言这些术语太长了。 一些程序员把“内部链接的文件作用域”简称为“文件作用域”, 把“外部链接的文件作用域”简称为“全局作用域”或“程序作用域”。
查看外部定义中是否使用了存储类别说明符static,
int giants = 5; // 文件作用域, 外部链接
static int dodgers = 3; // 文件作用域, 内部链接
int main()
{
...
} ...
该文件和同一程序的其他文件都可以使用变量giants。 而变量dodgers属文件私有, 该文件中的任意函数都可使用它。