第六章:指针
6.1 内存和地址
高级语言提供的特性之一就是通过名字而不是地址来访问内存的位置。
这些名字就是我们所称的变量,名字和内存位置之间的关联并不是由硬件提供的,它是由编译器为我们实现的。
所有这些变量给了我们一种更为方便的方法来记住地址--硬件仍然通过地址访问内存位置。
6.2 值的类型并非值本身所固有的一种特性,而是取决于他的使用方式。
6.5 未初始化和非法的指针
int *a;
...
*a = 12; 段错误
指针变量和其他变量并无区别,静态,则初始化为0,自动则不会被初始化。
6.6 NULL 指针
对所有的指针变量进行显式的初始化是一个好做法,如果你已经知道指针将被初始化为什么地址,就把他初始化为该地址,否则初始化为NULL。
如果函数失败是因为粗心大意的调用者懒得检查参数的有效性而引起的,那是他活该如此。
越界指针和指向未知值的指针是两个常见的错误根源。
第七章:函数
7.2 函数声明
使用函数原型最方便也是最安全的方法是把原型置于一个单独文件,#include
参数名字并非必须,但在函数原型中加入描述性的参数名是明智的。
缺省认定:当程序调用一个无法见到原型的函数时,编译器便认为该函数返回一个整型值。对于那些并非返回整型值的函数,可能会引起错误。
7.3 函数的参数
传值调用,函数将获得参数值的一份拷贝。
7.4 ADT 和黑盒
static
7.5 递归
C 通过运行时堆栈支持递归函数的实现。
使用字符常量而不是整型常量可以提高程序的移植型。
'0' + 0 = '0'
'0' + 1 = '1'
递归函数调用将涉及一些运行时开销--参数必须压入堆栈中,为局部变量分配内存空间,寄存器的值必须保存等。当递归函数每次返回时,上述操作
必须还原。
尾部递归可以很方便地转换成一个简单循环。
7.6 可变参数列表
stdarg 宏 可变参数列表是通过宏来实现的, stdarg.h
声明了一个类型 va_list和三个宏--va_start, va_arg和va_end
#include <stdarg.h>
float
average(int n_values, ...)
{
...
}
第八章:数组
8.1 一维数组
int a;
int b[10];
在C中,在几乎所有使用数组名的表达式中,数组名的值是一个指针常量,也就是数组第一个元素的地址。
请不要根据这个事实得出数组和指针是相同的结论。数组具有一些和指针完全不同的特征。例如,数组具有确定的元素,而指针只是一个标量值。
编译器用数组名来记住这些属性。只有当数组名在表达式中使用时,编译器才为它产生一个指针标量。
只有在两种场合下,数组名并不用指针常量来表示--就是当数组名作为sizeof操作符或单目操作符&的操作数时。
sizeof返回整个数组的长度,而不是指向数组指针的长度。取一个数组名的地址所产生的是一个指向数组的指针,而不是指向某个指针常量值的指针。
array[subscript]等价于
*(array + (subscript))
2[array] 等价于 array[2]
指针和下标:假定这两种方法都是正确的,下标绝不会比指针更有效率,但指针有时候会比下标更有效率。
指针的效率:
#define SIZE 50
int x[SIZE];
int y[SIZE];
int i;
int *p1, *p2;
计数器+下标:
void
try1()
{
for(i=0; i<SIZE; i++)
x[i] = y[i];
}
指针:
void
try2()
{
for(p1=x, p2=y; p1-x<SIZE;)
*p1++ = *p2++;
}
指针+计数器:
void
try3()
{
for(i=0,p1=x,p2=y; i<SZIE; i++)
*p1++ =*p2++;
}
寄存器指针+计数器:
void
try4()
{
register int *p1, *p2;
register int i;
for(i=0,p1=x,p2=y; i<SIZE; i++)
*p1++ = *p2++;
}
寄存器指针(best):
void
try5()
{
register *p1, *p2;
for(p1=x,p2=y; p1<&x[SIZE]; )
*p1++ = *p2++;
}
结论:
1. 当你根据某个固定数目的增量在某个数组中移动时,使用指针变量将比使用下标产生效率更高的代码。
当这个增量是1并且机器具有地址自动增量模型时,这点表现得更为突出。
2. 声明为寄存器变量的指针通常比位于静态内存和堆栈中的指针效率更高。
3. 如果你可以通过测试一些已经初始化并经过调整的内容来判断循环是否应该终止,那么你就不需要使用一个单独的寄存器。
4. 那些必须在运行时求值的表达式较之于诸如&array[SIZE]或array+SIZE这样的常量表达式往往代价更高。
作为函数参数的数组名:
字符串赋值:
/*
** 把第二个参数中的字符串复制到第一个参数指定的缓冲区。
*/
void
strcpy(char *buffer, char const *string)
{
/* 重复复制字符,直到遇见NUL字节。 */
while((*buffer++ = *string++) != '\0')
;
}
声明数组参数:
int strlen(char *string);
int strlen(char string[]);
你可以使用任何一种声明,但是那个“更加准确呢”,答案是指针。
因为实参就是一个指针,而不是一个数组,同样,表达式sizeof sring的值是指向字符指针的长度,而不是数组的长度。
另一方面,这样的实现使得函数无法直到数组的长度。如果函数需要知道数组的长度,它必须作为一个显示的参数传递给函数。
初始化:
静态和自动初始化
储存于静态内存的数组只初始化一次,也就是在程序开始执行之前。程序不需要执行指令把这些值放到合适的位置,他们一开始就在那里了。
这个魔术是由连接器完成的。
自动变量位于运行时堆栈中,缺省情况下是未初始化的。对于那些非常庞大的数组,它的初始化时间可能非常可观。
字符数组的初始化
char message[] = {'H', 'e', 'l', 'l', 'o', 0};
char *message = "Hello";
尽管它看上去像是一个字符串常量,实际上 并不是,它只是前例的初始化列表的另一种写法。
char *message2 = "Hello";
多维数组
int matrix[3][10];
警告:matrix[3,4] == matrix[4];
指向数组的指针
int matrix[3][10];
声明一个指向整型数组的指针:int (*p)[10];
int (*p)[10] = matrix;
p是一个指向拥有10个整型元素的数组的指针。
int *pi = &matrix[0][0];
int *pi = matrix[0];
指向matrix的第一个整型元素。
警告:避免这种类型声明--int (*p)[] = matrix;
作为函数参数的多维数组
int vector[10];
void fun1(int *vec);
void fun1(int vec[]);
int matrix[3][10];
void fun2(int (*mat)[10]);
void fun2(int mat[][10]);
把fun2写成下面这样的原型是不正确的:
void fun2(int **mat);
这个例子把mat声明为一个指向整型指针的指针。
指针数组
int *api[10];
存于矩阵
char const keyword[][9] = {
"do",
"for",
"if",
"register",
"return",
"switch",
"while"
};
指针数组
char const *keyword[] = {
"do",
"for",
"if",
"register",
"return",
"switch",
"while",
NULL
};
第九章:字符串、字符和指针
C 语言没有显式的字符串数据类型,因为字符串以字符串常量的形式出现或者储存于字符数组中。
9.1 字符串基础
string.h
9.2 字符串长度
size_t strlen(char const *string);
size_t 这个类型是在头文件 stddef.h 中定义的,它是一个无符号整型。
if(strlen(x) >= strlen(y))... right
if(strlen(x)-strlen(y) >= 0)... wrong uint - uint = uint always >= 0;
事实上,这两种方式是不相等的。
/*
** 计算字符串参数的长度。
*/
#include <stddef.h>
size_t
strlen(char const *string)
{
int length;
for(length = 0; *string++ != '\0';)
length++;
return length;
}
警告:
表达式中如果同时包含了有符号数和无符号数,可能会产生奇怪的结果。和前面一样,下面两条语句并不相同,
原因相同
if(strlen(x) >= 10) ... right
if(strlen(x) - 10 >= 0) ... wrong
uint - int(寻常算数转换) = uint always >= 0;
将strlen的返回值强制转换为int, 就可以消除这个问题。
提示:复用已经存在的软件比重新开发一个效率更高。
9.3 不受限制的字符串函数
复制字符串
char *strcpy(char *dst, char const *src);
警告:
程序员必须保证目标字符数组的空间足以容纳需要复制的字符串。如果字符串比数组长,多余的字符能被复制,
他们将覆盖原先储存于数组后面的内存空间的值。
连接字符串
char *strcat(char *dst, char *const *src);
函数的返回值
strcpy 和strcat 返回他们第一个参数的一份拷贝。
事实上,在这些函数的绝大多数调用中,他们的返回值只是被简单的忽略。
字符串比较
int strcmp(char const *s1, char const s2);
s1小于s2, 返回小于零,s1大于s2, 返回大于零,s1等于s2, 返回零。
9.4 长度受限的字符串函数
char *strncpy(char *dst, char const *src, size_t len);
char *strncat(char *dst, char const *src, size_t len);
int strncmp(char const *s1, char const *s2, size_t len);
和strcpy一样,strncpy把源字符串的字符复制到目标数组。然而,它总是正好向dst写入len个字符。
如果strlen(src)的值小于len, dst数组就用额外的NUL 字节填充到len长度。如果strlen(src)的值大于或等于len,那么只有len个字符被复制到dst中。
注意!它的结果将不会以NUL字节结尾。
警告:
strncpy 调用的结果可能不是一个字符串,因此字符串必须以NUL字节结尾,如果在一个需要字符串的地方(例如strlen函数的参数)使用了一个
不是以NUL字节结尾的字符序列,会发生什么呢?strlen函数无法知道NUL字节是没有的,所以它将继续进行查找,一个字符接一个字符,直到它发现
一个NUL字节为止,或许它找了几百个字符都没有找到,而strlen函数返回值从本质上说是一个随机数。
或者,如果函数试图访问系统分配给这个程序以外的内存范围,程序就会崩溃。
警告:
这个问题只有当你使用strncpy函数创建字符串,然后或者对他们使用str开头的库函数,或者在printf中使用%s格式码打印它们时才会发生。
在使用不受限制的函数之前,你首先必须确定字符串实际上是以NUL字节结尾的。例如如下代码段:
char buffer[BSIZE];
...
strncpy(buffer, name, BSIZE);
buffer[BSIZE-1] = '\0';
尽管strncat也是一个长度受限的函数,但他和strncpy存在不同之处,它从src中最多复制len个字符到目标数组的后面。但是,strncat总是在结果字符串
后面添加一个NUL字节,而且它不会像strncpy那样对目标数组用NUL字节进行填充。
strncat最多向目标数组复制len个字符(再加一个结尾的NUL字节),他才不管目标参数除去原先存在的字符串之后留下的空间够不够。
9.5 字符串查找基础
查找一个字符
char *strchr(char const *str, int ch);
char *strrchr(char const *str, int ch);
例子:
char string[20] = "Hello there, honey.";
char *ans;
ans = strchr(string, 'h'); ans 指向位置是 string+7;
查找几个字符
char *strpbrk(char const *str, char const *group);
例子:
char string[20] = "Hello there, honey.";
char ans;
ans = strpbrk(string, "aeiou"); ans 指向位置是 string+1;
查找一个子串
char *strstr(char const *s1, char const *s2);
9.6 高级字符串查找
查找一个字符串前缀
size_t strspn(char const *str, char const *group);
size_t strcspn(char const *str, char const *group);
例子:
int len1, len2;
char buffer[] = "25,142,330,Smith,J,239-4123";
len1 = strspn(buffer, "0123456789"); len1 == 2;
len2 = strspn(buffer, ",0123456789"); len2 == 11;
下面代码将计算一个指向字符串中第一个非空白字符的指针。
ptr = buffer + strspn(buffer, "\n\r\f\t\v");
strcspn函数和strspn函数正好相反。
查找标记
char *strtok(char *str, char const *sep);
sep参数是个字符串,定义了用作分隔符的字符集合,第一个参数指定一个字符串,strtok 找到str的下一个标记,并将其用NUL结尾,
然后返回一个指向这个标记的指针。
警告:
当strtok函数执行任务时,它将会修改它所处理的字符串。如果源字符串不能被修改,那就复制一份,将这份拷贝传递给strtok函数。
在典型情况下,在第一次调用strtok时,向它传递一个指向字符串的指针,然后,这个函数被重复利用,(第一个参数为NULL),直到
它返回NULL为止。
例子:
。。。
警告:
由于strtok函数保存它所处理的函数的局部状态信息,所以你不能用它同时解析两个字符串。
9.7 错误信息
当你调用一些函数,请求操作系统执行一些功能如打开文件时,如果出现错误,操作系统是通过设置一个外部的整型变量errno进行错误代码报告的。
strerror函数把其中一个错误代码作为参数并返回一个指向用于描述错误的字符串的指针。
char *strerror(int error_number);
9.8 字符操作 原型位于 ctype.h
字符分类
iscntrl, isspace, isdigit, isxdigit, islower, isupper, isalpha, isalnum, ispunct, isgraph, isprint
字符转换
int tolower(int ch);
int toupper(int ch);
提示:
直接测试或操纵字符将会降低程序可移植性。如,if(ch >= 'a' && ch <= 'z')
这条语句在使用ASCII字符集的机器上能够运行,但在使用EBCDIC字符集的机器上将会失败,另一方面,下面这条语句,
if(isupper(ch))
无论机器使用哪个字符集,他都能顺利运行。
9.9 内存操作
根据定义,字符串由一个NUL字节结尾,所以字符串内部不能含有任何NUL字符,但是,非字符串数据内部包含零值的情况并不罕见。
你无法使用字符串函数来处理这类型的数据,因为当他们遇到第一个NUL字节时将停止工作。
不过,我们可以使用另外一组函数
void *memcpy(void *dst, void const *src, size_t length);
void *menmove(void *dst, void const *src, size_t length);
void *memcmp(void *dst, void const *src, size_t length);
void *menchr(void const *a, int ch, size_t length);
void *memset(void *a, int ch, size_t length);
memcpy 从src 的起始位置复制length个字节到dst的内存起始位置。你可以使用这种方法复制任何类型的值。第三个参数指定复制值的长度(以字节记)。
如果src和dst 以任何形式出现了重叠,它的结果是未定义的。
例如
char temp[SIZE], values[SIZE];
...
memcpy(temp, values, SIZE);
但是如果两个数组都是整型数组该怎么办呢?下面的语句可以完成这项任务。
memcpy(temp, values, sizeof(values));
复制部分内容
memcpy(saved_answers, answers, count*sizeof(answer[0]));
你也可以使用这种方式复制结构或结构数组。
memmove 函数的行为和memcpy差不多,只是它的源和目的操作数可以重叠。
如果源和目的参数真的可能存在重叠,就应该使用memmove, 如下
/*
** Shift the values in the x array left one position.
*/
memmove(x, x+1, (count-1)*sizeof(x[0]));
6.1 内存和地址
高级语言提供的特性之一就是通过名字而不是地址来访问内存的位置。
这些名字就是我们所称的变量,名字和内存位置之间的关联并不是由硬件提供的,它是由编译器为我们实现的。
所有这些变量给了我们一种更为方便的方法来记住地址--硬件仍然通过地址访问内存位置。
6.2 值的类型并非值本身所固有的一种特性,而是取决于他的使用方式。
6.5 未初始化和非法的指针
int *a;
...
*a = 12; 段错误
指针变量和其他变量并无区别,静态,则初始化为0,自动则不会被初始化。
6.6 NULL 指针
对所有的指针变量进行显式的初始化是一个好做法,如果你已经知道指针将被初始化为什么地址,就把他初始化为该地址,否则初始化为NULL。
如果函数失败是因为粗心大意的调用者懒得检查参数的有效性而引起的,那是他活该如此。
越界指针和指向未知值的指针是两个常见的错误根源。
第七章:函数
7.2 函数声明
使用函数原型最方便也是最安全的方法是把原型置于一个单独文件,#include
参数名字并非必须,但在函数原型中加入描述性的参数名是明智的。
缺省认定:当程序调用一个无法见到原型的函数时,编译器便认为该函数返回一个整型值。对于那些并非返回整型值的函数,可能会引起错误。
7.3 函数的参数
传值调用,函数将获得参数值的一份拷贝。
7.4 ADT 和黑盒
static
7.5 递归
C 通过运行时堆栈支持递归函数的实现。
使用字符常量而不是整型常量可以提高程序的移植型。
'0' + 0 = '0'
'0' + 1 = '1'
递归函数调用将涉及一些运行时开销--参数必须压入堆栈中,为局部变量分配内存空间,寄存器的值必须保存等。当递归函数每次返回时,上述操作
必须还原。
尾部递归可以很方便地转换成一个简单循环。
7.6 可变参数列表
stdarg 宏 可变参数列表是通过宏来实现的, stdarg.h
声明了一个类型 va_list和三个宏--va_start, va_arg和va_end
#include <stdarg.h>
float
average(int n_values, ...)
{
...
}
第八章:数组
8.1 一维数组
int a;
int b[10];
在C中,在几乎所有使用数组名的表达式中,数组名的值是一个指针常量,也就是数组第一个元素的地址。
请不要根据这个事实得出数组和指针是相同的结论。数组具有一些和指针完全不同的特征。例如,数组具有确定的元素,而指针只是一个标量值。
编译器用数组名来记住这些属性。只有当数组名在表达式中使用时,编译器才为它产生一个指针标量。
只有在两种场合下,数组名并不用指针常量来表示--就是当数组名作为sizeof操作符或单目操作符&的操作数时。
sizeof返回整个数组的长度,而不是指向数组指针的长度。取一个数组名的地址所产生的是一个指向数组的指针,而不是指向某个指针常量值的指针。
array[subscript]等价于
*(array + (subscript))
2[array] 等价于 array[2]
指针和下标:假定这两种方法都是正确的,下标绝不会比指针更有效率,但指针有时候会比下标更有效率。
指针的效率:
#define SIZE 50
int x[SIZE];
int y[SIZE];
int i;
int *p1, *p2;
计数器+下标:
void
try1()
{
for(i=0; i<SIZE; i++)
x[i] = y[i];
}
指针:
void
try2()
{
for(p1=x, p2=y; p1-x<SIZE;)
*p1++ = *p2++;
}
指针+计数器:
void
try3()
{
for(i=0,p1=x,p2=y; i<SZIE; i++)
*p1++ =*p2++;
}
寄存器指针+计数器:
void
try4()
{
register int *p1, *p2;
register int i;
for(i=0,p1=x,p2=y; i<SIZE; i++)
*p1++ = *p2++;
}
寄存器指针(best):
void
try5()
{
register *p1, *p2;
for(p1=x,p2=y; p1<&x[SIZE]; )
*p1++ = *p2++;
}
结论:
1. 当你根据某个固定数目的增量在某个数组中移动时,使用指针变量将比使用下标产生效率更高的代码。
当这个增量是1并且机器具有地址自动增量模型时,这点表现得更为突出。
2. 声明为寄存器变量的指针通常比位于静态内存和堆栈中的指针效率更高。
3. 如果你可以通过测试一些已经初始化并经过调整的内容来判断循环是否应该终止,那么你就不需要使用一个单独的寄存器。
4. 那些必须在运行时求值的表达式较之于诸如&array[SIZE]或array+SIZE这样的常量表达式往往代价更高。
作为函数参数的数组名:
字符串赋值:
/*
** 把第二个参数中的字符串复制到第一个参数指定的缓冲区。
*/
void
strcpy(char *buffer, char const *string)
{
/* 重复复制字符,直到遇见NUL字节。 */
while((*buffer++ = *string++) != '\0')
;
}
声明数组参数:
int strlen(char *string);
int strlen(char string[]);
你可以使用任何一种声明,但是那个“更加准确呢”,答案是指针。
因为实参就是一个指针,而不是一个数组,同样,表达式sizeof sring的值是指向字符指针的长度,而不是数组的长度。
另一方面,这样的实现使得函数无法直到数组的长度。如果函数需要知道数组的长度,它必须作为一个显示的参数传递给函数。
初始化:
静态和自动初始化
储存于静态内存的数组只初始化一次,也就是在程序开始执行之前。程序不需要执行指令把这些值放到合适的位置,他们一开始就在那里了。
这个魔术是由连接器完成的。
自动变量位于运行时堆栈中,缺省情况下是未初始化的。对于那些非常庞大的数组,它的初始化时间可能非常可观。
字符数组的初始化
char message[] = {'H', 'e', 'l', 'l', 'o', 0};
char *message = "Hello";
尽管它看上去像是一个字符串常量,实际上 并不是,它只是前例的初始化列表的另一种写法。
char *message2 = "Hello";
多维数组
int matrix[3][10];
警告:matrix[3,4] == matrix[4];
指向数组的指针
int matrix[3][10];
声明一个指向整型数组的指针:int (*p)[10];
int (*p)[10] = matrix;
p是一个指向拥有10个整型元素的数组的指针。
int *pi = &matrix[0][0];
int *pi = matrix[0];
指向matrix的第一个整型元素。
警告:避免这种类型声明--int (*p)[] = matrix;
作为函数参数的多维数组
int vector[10];
void fun1(int *vec);
void fun1(int vec[]);
int matrix[3][10];
void fun2(int (*mat)[10]);
void fun2(int mat[][10]);
把fun2写成下面这样的原型是不正确的:
void fun2(int **mat);
这个例子把mat声明为一个指向整型指针的指针。
指针数组
int *api[10];
存于矩阵
char const keyword[][9] = {
"do",
"for",
"if",
"register",
"return",
"switch",
"while"
};
指针数组
char const *keyword[] = {
"do",
"for",
"if",
"register",
"return",
"switch",
"while",
NULL
};
第九章:字符串、字符和指针
C 语言没有显式的字符串数据类型,因为字符串以字符串常量的形式出现或者储存于字符数组中。
9.1 字符串基础
string.h
9.2 字符串长度
size_t strlen(char const *string);
size_t 这个类型是在头文件 stddef.h 中定义的,它是一个无符号整型。
if(strlen(x) >= strlen(y))... right
if(strlen(x)-strlen(y) >= 0)... wrong uint - uint = uint always >= 0;
事实上,这两种方式是不相等的。
/*
** 计算字符串参数的长度。
*/
#include <stddef.h>
size_t
strlen(char const *string)
{
int length;
for(length = 0; *string++ != '\0';)
length++;
return length;
}
警告:
表达式中如果同时包含了有符号数和无符号数,可能会产生奇怪的结果。和前面一样,下面两条语句并不相同,
原因相同
if(strlen(x) >= 10) ... right
if(strlen(x) - 10 >= 0) ... wrong
uint - int(寻常算数转换) = uint always >= 0;
将strlen的返回值强制转换为int, 就可以消除这个问题。
提示:复用已经存在的软件比重新开发一个效率更高。
9.3 不受限制的字符串函数
复制字符串
char *strcpy(char *dst, char const *src);
警告:
程序员必须保证目标字符数组的空间足以容纳需要复制的字符串。如果字符串比数组长,多余的字符能被复制,
他们将覆盖原先储存于数组后面的内存空间的值。
连接字符串
char *strcat(char *dst, char *const *src);
函数的返回值
strcpy 和strcat 返回他们第一个参数的一份拷贝。
事实上,在这些函数的绝大多数调用中,他们的返回值只是被简单的忽略。
字符串比较
int strcmp(char const *s1, char const s2);
s1小于s2, 返回小于零,s1大于s2, 返回大于零,s1等于s2, 返回零。
9.4 长度受限的字符串函数
char *strncpy(char *dst, char const *src, size_t len);
char *strncat(char *dst, char const *src, size_t len);
int strncmp(char const *s1, char const *s2, size_t len);
和strcpy一样,strncpy把源字符串的字符复制到目标数组。然而,它总是正好向dst写入len个字符。
如果strlen(src)的值小于len, dst数组就用额外的NUL 字节填充到len长度。如果strlen(src)的值大于或等于len,那么只有len个字符被复制到dst中。
注意!它的结果将不会以NUL字节结尾。
警告:
strncpy 调用的结果可能不是一个字符串,因此字符串必须以NUL字节结尾,如果在一个需要字符串的地方(例如strlen函数的参数)使用了一个
不是以NUL字节结尾的字符序列,会发生什么呢?strlen函数无法知道NUL字节是没有的,所以它将继续进行查找,一个字符接一个字符,直到它发现
一个NUL字节为止,或许它找了几百个字符都没有找到,而strlen函数返回值从本质上说是一个随机数。
或者,如果函数试图访问系统分配给这个程序以外的内存范围,程序就会崩溃。
警告:
这个问题只有当你使用strncpy函数创建字符串,然后或者对他们使用str开头的库函数,或者在printf中使用%s格式码打印它们时才会发生。
在使用不受限制的函数之前,你首先必须确定字符串实际上是以NUL字节结尾的。例如如下代码段:
char buffer[BSIZE];
...
strncpy(buffer, name, BSIZE);
buffer[BSIZE-1] = '\0';
尽管strncat也是一个长度受限的函数,但他和strncpy存在不同之处,它从src中最多复制len个字符到目标数组的后面。但是,strncat总是在结果字符串
后面添加一个NUL字节,而且它不会像strncpy那样对目标数组用NUL字节进行填充。
strncat最多向目标数组复制len个字符(再加一个结尾的NUL字节),他才不管目标参数除去原先存在的字符串之后留下的空间够不够。
9.5 字符串查找基础
查找一个字符
char *strchr(char const *str, int ch);
char *strrchr(char const *str, int ch);
例子:
char string[20] = "Hello there, honey.";
char *ans;
ans = strchr(string, 'h'); ans 指向位置是 string+7;
查找几个字符
char *strpbrk(char const *str, char const *group);
例子:
char string[20] = "Hello there, honey.";
char ans;
ans = strpbrk(string, "aeiou"); ans 指向位置是 string+1;
查找一个子串
char *strstr(char const *s1, char const *s2);
9.6 高级字符串查找
查找一个字符串前缀
size_t strspn(char const *str, char const *group);
size_t strcspn(char const *str, char const *group);
例子:
int len1, len2;
char buffer[] = "25,142,330,Smith,J,239-4123";
len1 = strspn(buffer, "0123456789"); len1 == 2;
len2 = strspn(buffer, ",0123456789"); len2 == 11;
下面代码将计算一个指向字符串中第一个非空白字符的指针。
ptr = buffer + strspn(buffer, "\n\r\f\t\v");
strcspn函数和strspn函数正好相反。
查找标记
char *strtok(char *str, char const *sep);
sep参数是个字符串,定义了用作分隔符的字符集合,第一个参数指定一个字符串,strtok 找到str的下一个标记,并将其用NUL结尾,
然后返回一个指向这个标记的指针。
警告:
当strtok函数执行任务时,它将会修改它所处理的字符串。如果源字符串不能被修改,那就复制一份,将这份拷贝传递给strtok函数。
在典型情况下,在第一次调用strtok时,向它传递一个指向字符串的指针,然后,这个函数被重复利用,(第一个参数为NULL),直到
它返回NULL为止。
例子:
。。。
警告:
由于strtok函数保存它所处理的函数的局部状态信息,所以你不能用它同时解析两个字符串。
9.7 错误信息
当你调用一些函数,请求操作系统执行一些功能如打开文件时,如果出现错误,操作系统是通过设置一个外部的整型变量errno进行错误代码报告的。
strerror函数把其中一个错误代码作为参数并返回一个指向用于描述错误的字符串的指针。
char *strerror(int error_number);
9.8 字符操作 原型位于 ctype.h
字符分类
iscntrl, isspace, isdigit, isxdigit, islower, isupper, isalpha, isalnum, ispunct, isgraph, isprint
字符转换
int tolower(int ch);
int toupper(int ch);
提示:
直接测试或操纵字符将会降低程序可移植性。如,if(ch >= 'a' && ch <= 'z')
这条语句在使用ASCII字符集的机器上能够运行,但在使用EBCDIC字符集的机器上将会失败,另一方面,下面这条语句,
if(isupper(ch))
无论机器使用哪个字符集,他都能顺利运行。
9.9 内存操作
根据定义,字符串由一个NUL字节结尾,所以字符串内部不能含有任何NUL字符,但是,非字符串数据内部包含零值的情况并不罕见。
你无法使用字符串函数来处理这类型的数据,因为当他们遇到第一个NUL字节时将停止工作。
不过,我们可以使用另外一组函数
void *memcpy(void *dst, void const *src, size_t length);
void *menmove(void *dst, void const *src, size_t length);
void *memcmp(void *dst, void const *src, size_t length);
void *menchr(void const *a, int ch, size_t length);
void *memset(void *a, int ch, size_t length);
memcpy 从src 的起始位置复制length个字节到dst的内存起始位置。你可以使用这种方法复制任何类型的值。第三个参数指定复制值的长度(以字节记)。
如果src和dst 以任何形式出现了重叠,它的结果是未定义的。
例如
char temp[SIZE], values[SIZE];
...
memcpy(temp, values, SIZE);
但是如果两个数组都是整型数组该怎么办呢?下面的语句可以完成这项任务。
memcpy(temp, values, sizeof(values));
复制部分内容
memcpy(saved_answers, answers, count*sizeof(answer[0]));
你也可以使用这种方式复制结构或结构数组。
memmove 函数的行为和memcpy差不多,只是它的源和目的操作数可以重叠。
如果源和目的参数真的可能存在重叠,就应该使用memmove, 如下
/*
** Shift the values in the x array left one position.
*/
memmove(x, x+1, (count-1)*sizeof(x[0]));