C和指针:第九,十章
第9章 字符串,字符和字节
1. strlen() 返回值是size_t类型,它是个无符号整形。当无符号参与表达式运算时,可能产生不可预期的结果。如:
if(strlen (x) - strlen(y) >= 0) // 这个条件永远都为真,即使strlen(x)的值小于strlen(y)。因为无符号的值是不可能为负的
同样,当有符号数与无符号数一起运算时,也会产生上面同样的问题,如:
if (strlen(x) – 10 >= 0) // 即使strlen(x) 值小于10,条件也永远为真
解决办法是,强制将strlen的返回值转换为int
2. 字符串的复制
使用strcpy函数可对字符串进行拷贝,函数原型为:
char *strcpy(char *dst, char const *src);
1.) 如果参数中src和dst的内存位置出现重叠,结果是未定义的
2.) 目标参数的以前内容都将被覆盖并丢失,即使新的字符串比原来的字符串短
3.) 目标字符数组空间必须足以容纳要复制的字符串
4.) 函数返回第一个参数的值
3. 字符串的连接
使用strcat函数可以将两个字符串连接在一起,函数原型为:
char *strcat(char *dst, char const *src);
1.) 如果参数中src和dst的内存位置出现重叠,结果是未定义的
2.) 目标字符数组空间必须足以容纳要复制的字符串(必须考虑原来目标字符串中字符数量)
3.) 函数返回第一个参数的值
4. 字符串的比较
使用strcmp函数可以对两个字符串进行比较,函数原型为:
char *strcmp(char *constr *s1, char const *s2);
1.) 要比较的字符串必须以NUL结尾,否则比较的结果将没有意义
2.) 字符串之间的比较是按逐个字符进行比较的,字符之间的比较依照ASCII码进行比较
3.) 函数返回int值,如果s1 等于 s2,返回0;如果s1 大于 s2,返回大于0的值(但不一定是1);如果s1 小于 s2,返回小于0的值(但不一定是-1)。
5. strncpy, strncat 和strncmp函数,原型如下:
char *strncpy(char *dst, char const *src, size_t len);
char *strncat(char *dst, char const *src, size_t len);
char *strncmp(char *s1, char const *s2, size_t len);
1.) 如果参数中源参数和目标参数的内存位置出现重叠,结果是未定义的
2.) strncpy从源字符串中复制len个字符到目标字符串中。如果strlen(src)小于len,des数组就用NUL填充剩余的字节;如果strlen(src)大于或等于len,那么只有len个字符被复制到dst数组中。并且它的结果不会以NUL结尾。如果在需要使用字符串的地方使用不以NUL结尾的字符序列,结果是未定义的。
3.) strncat从源字符串中复制len个字符到目标数组的后面,总在结果字符串后面添加一个NUL字节。strncat总是向目标数组追加len个字符,即使目标字符串数组空间不够。
4.) strncmp比较两个字符串的前len个字符,返回结果与strcmp一样。
6. 字符串的查找
1.) 使用strchr,strrchr查找一个字符,函数原型为:
char *strchr(char const *str, int ch); // 查找str中字符ch第一次出现的位置,找到后返回指向该位置的指针,未找到返回NULL指针
char *strrchr(char const *str, int ch); // 查找str中字符ch最后一次(最右边)出现的位置,找到后返回指向该位置的指针,未找到返回NULL指针
如:
char string[20] = “Hello, there, honey.”;
char *ans;
ans = strchr(string, ‘h’); // ans指向string + 7
ans = strrchr(string, ‘h’); // ans指向string + 14
2.) 使用strpbrk查找任何几个字符,函数原型为:
char *strpbrk(char const *str, char const *gruop); // 返回指向str中第1个匹配gruop中任何一个字符的字符位置,未找到返回NULL指针
如:
char stirng[20] = “Hello, there honey.”;
char *ans;
ans = strpbrk(string, “aeiou”); // ans指向string + 1,因为string字符数组中的第二个字符‘e’在”aeiou”中是第一个被包含的字符
3.) 使用strstr查找一个子串,函数原型为:
char *strstr(char const *s1, char const *s2); // 查找s1中查找完全匹配s2的字符,找到则返回指向s1中第一个匹配s2的字符指针,未找到返回NULL
如:
char string[20] = “Hello, there honey.”;
char test[10] = “ere”;
char *p;
p = strstr(s1, s2); // p指向string + 9
7. 使用strspn,strcspn对任意连续字符串查找,函数原型如下:
size_t strspn(char const *str, char const *group); // 查找str字符串中的第一个字符是否与group中的字符匹配,如果匹配,则继续匹配第二个,直到碰到与group字符串不匹配的或结束符\0,并返回匹配的字符数。如果str的第一个字符与gruop中的字符不匹配,则返回0。
如:
int len1, len2;
char buffer[] = “25, 142, 330, Smith, J, 239-4124”;
len1 = strspn(buffer, “0123456789”); // 返回2,因为buffer数组中的前面两个字符2和5都能与”0123456789”匹配,而逗号则不行,此时终止查找 len2 = strspn(buffer, “,0123456789”); // 返回11,因为buffer数组从2到Smith的前一个字符都能与”0123456789”匹配,Smith前一共有11个字符
size_t strcspn(char const *str, char const *group); // 与strspn的作用相反,它是查找str字符串中的第一个字符是否与group中的字符匹配,如果匹配,则返回0,如果不匹配,则继续匹配下一个字符,直到与group匹配的字符或结束符\0。strcspn中c代表complement(补充物)。
如:
int len1, len2;
char buffer[] = “25, 142, 330, Smith, J, 239-4124”;
len1 = strcspn(buffer, “0123456789”); // 返回0,因为buff的第一个字符2就与”0123456789”中能匹配
char str[] = “xyz012345”
len2 = strcspn(str, “0123456789”); // 返回3,因为str的前三个字符xyz在”0123456789”中都不匹配,但遇到第4个字符0时,匹配,终止查找
8. 字符标记查找(分割字符串)
使用strtok函数将str按sep字符集合分隔,并将包含sep的字符集用Null(\0)来替换,直到str结束。函数原型:
char *strtok(char *str, char const *sep); // sep是个字符串,定义了用作分隔符的字符集
如:
char buffer[] = “Hello world. test”;
char seps[] = “ ”; // 以空格做为分隔字符集合
char *p;
p = strtok(buffer, seps);
while (p != NULL)
{
printf("%s\n", p);
p = strtok(NULL, seps);
}
以上结果为:
Hello
world.
test
如果第一个参数中含有多个标记,需要多次调用strtok才能将字符串分解为标记,这时就要用到循环。第一次调用strtok包含两个参数,即要标记化的字符串和包含用来分隔标记的字符的字符串。后面再调用strtok时,第一个参数为NULL,继续将sep做为标记字符集合。NULL参数表示调用strtok继续从string中上次调用 strtok时保存的位置开始标记化。如果调用strtok时已经没有标记,则strtok返回NULL。
另外,如果你愿意,可以在每次调用strtok函数时使用不同的分隔符集合。当一个字符串的不同部分由不由的字符集合分隔的时候,这个技巧将很凑效。
9. 其它字符串操作函数
1.) iscntrl 判定当前字符是否在0x00-0x1F之间或等于0x7F(DEL)时,是,返回非零值,否则返回零。
2.) isspace 判定当前字符是否为空白字符:空格,换页,换行,回车,制表符
3.) isdigit 判定当前字符是否为十进制数字0-9
4.) isxdigit 判定当前字符是否为十六进制数字0-9, a-f或0-9, A-F
5.) islower 判定当前字符是否为小写字母a-z
6.) isupper 判定当前字符是否为大写字母A-Z
7.) isalpha 判定当前字符是否为字母,a-z或A-Z
8.) isalnum 判定当前字符是否为数字或字母,0-9, a-z或A-Z
9.) ispunct 判定当前字符是否为标点符号:任何不属于数字或字母的可打印符号
10.) isgraph 判定当前字符是否为任何图形字符
11.) isprint 判定当前字符是否为任何可打印字符,包括图形字符和空白字符
12.) tolower 将字母转换为小写
13.) toupper 转字母转换为大写
另外,直接测试或操作字符将会降低程序的可移植性,如:
if (ch >= ‘A’ && ch <= ‘z’)
上面语句在ASCII字符集下能够运行,如果在EBCDIC字符集下将会失败,但如果使用:
if (isupper(ch))
将在任何字符集下都能顺利运行
第10章 结构和联合
1. C提供了两种聚合数据类型(aggregate data type, 能够同时存储超过一个的单独数据):数组和结构
2. 结构的声明
struct tag {
member-list
} variable-list;
标签(tag)和variable-list是可先的,但不能同时都省略,允许两者同时出现。
标签允许为成员列表提供一个名字,这样可在后续的声明中使用。variable-list做为结构的实例列表,可以是多个。
如:
struct simple {
int a;
char b;
float c;
} y[10], *p;
struct x;
当然, 还可以借助typedef来创建一个新的类型:
typedef struct {
int a;
char b;
float c;
} simple;
simple x; // simple做为一种新的类型
如果想在多个源文件中使用同一种类型结构,可以把结构声明放在一个头文件中,当源文件需要这个结构时,可以使用#include 把文件包含进来。
结构中的成员类型可以是任何C语言数据类型,包含普通变量,数组,指针和结构。如:
struct COMPLEX{
float f;
int a[20]; // 此处的数组a不会与simple 中数组a发冲突
long *lp;
struct simple s;
struct simple sa[10];
struct simple *sp;
};
3. 结构成员的初始化
初始化结构变量时,同时初始化结构成员,如:
struct simple {
int a;
char b;
float c;
} x = {2, ‘b’, 3.2};
或
struct simple x = {2, ‘b’, 3.2};
以下形式的初始化是非法的:
struct simple x;
x = {2, ‘b’, 3.2}; // 非法
如果,初始化结构变量后,没有同时初始化成员值,只能对结构的每个成员单独赋值,如下:
struct simple x;
x.a = 2;
x.b = ‘b’;
x.c = 3.2;
初始化部分值,未初始化的成员,默认值为0,如:
struct simple x = {0}; // 默认x.b = \0, x.c = 0.0
4. 结构成员的访问
结构成员的访问有直接访问和间接访问两种。
1.) 直接访问是通过点操作符(.)访问,点操作符的左边是结构变量名 ,右边是需要访问的成员名。以上面的结构COMPLEX为例,如:
struct COMPLEX comp;
要访问成员f,可以是:comp.f
要访问成员数组a的第三个元素,可以是:comp.a[2]
要访问成员结构s中的成员b,可以是:comp.s.b
要访问成员结构数组sa的第四个元素里的c,可以是:comp.sa[3].c
2.) 间接访问
如果你拥有一个指向结构的指针,如:
struct COMPLEX *cp;
要访问指针指向的结构成员,先对指针执行间接访问,然后用点操作符来访问结构中的成员,如访问指向结构中的成员f:
(*cp).f // 点操作符优先级高于解释引用操作,所以这里必须要用括号括起来
上面的表达式看起来有些复杂,C提供箭头操作符来简化指针指向结构的成员访问“->”,上面可改为:
cp->f
5. 结构的自引用
strcut self_ref1 {
int a;
struct self_ref1 b;
int c;
};
以上代码是非法的,成员b是一个完全的成员变量,是包含了它自己在内的完整结构,而第2个成员又是另一个完整的结构,这样就像永远不会终止的递归。但下面的声明是合法的:
struct self_ref1 {
int a;
struct self_ref2 *b;
int c;
};
原因在于,b现在是一个指针而不是一个结构,它指向的是同一类型的不同结构。在链表和树中,都会使用到这一技巧来指向链表的下一个元素或树的下一个分枝。
但下面的声明也是非法的:
typedef strcut {
int a;
self_ref3 *b;
int c;
} self_ref3;
原因是,类型名直到声明的未尾才定义,此时的结构声明还未定义,解决办法是在struct 后紧跟一个结构标签名,如下:
typedef struct self_ref3_tag {
int a;
struct self_ref3_tag *b;
int c;
} self_ref3;
6. 结构、指针和成员
有如下结构:
typedef struct {
int a;
short b[2];
} Ex2;
typedef struct EX {
int a;
char b[3];
Ex2 c;
struct Ex *d;
} Ex;
Ex x = {10, “Hi”, {5, {-1, 25}}, 0};
Ex *px = &x;
1.) px做右值是,所指向结构地址;做左值时,是个代表指针变量存储空间;
2.) *px 做右值时,是px所指向的整个结构值;做左值时指px指针变量的存储空间;
3.) px->a 做右值时,访问Ex结构中的成员a,相当于x.a;做左值时,代表结构成员a的存储空间;
4.) *px->a 做右值和左值时,表达式都非法;
5.) px->b 做右值时,Ex结构中成员数组b的第一个元素地址;不能做左值;
6.) *px->b 做右值时,访问Ex结构中成员数组b的第一个元素的值;做左值时代表Ex结构成员数组b的第一个元素存储空间;
7.) px->c.a 做右值时,访问Ex结构中的Ex2结构中a的值,相当于x.c.a;做左值时代表Ex结构中Ex2结构中a的存储空间;
8.) *px->c.a 做右值和左值时,表达式都非法;
9.) px->d 做右值时,访问Ex结构中d的值;做左值时,代表Ex结构成员中d的存储空间;
10.) *px->d 做右值和左值时,表达式都非法;
7. 结构的存储分配
struct ALING {
char a;
int b;
char c;
};
系统禁止编译器在一个结构的起始位置跳过若干字节来满足边界的对应要求。上面结构中,成员a占1个字节,成员b占4个字节,成员c占1个字节,按照对齐规则,上面这个结构占用12字节,但实际只用到其中的6个。如果将成员位置调整下:
struct ALING1 {
int b;
char a;
char c;
};
上面这个结构占8个字节,节省了33%的内存空间。
当程序创建成百上千个结构时, 我们有必须对结构成员的字节对齐的浪费做出改善。而由此带来的可读性可增加注释来弥补。
8. 作为函数参数的结构
typedef struct {
char product[PROUCT_SIZE];
int quantity;
float unit_price;
float total_amount;
} Transaction;
void print_receipt (Transaction *trans) // 或 void print_receipt(¤t_trans); current_trans是一个transaction结构
{
printf(“%s\n”, trans->product);
printf(“%d @ %.2f total %.2f\n”, trans->quantity, trans->unit_price, trans->total_amount);
}
上面的void print_receipt(Transaction *trans)使用结构指针作为参数要比void print_receipt(Transaction trans)的效果要高得多,而且结构越大,所提高的效率越大。用结构指针做参数传递给函数也存在一个缺陷,函数可以对结构变量中的成员进行修改。但可以使用const关键字来防止修改:
void print_receipt(Transaction const *trans);
绝大多数情况下,传递指针效率都会更高。如果希望函数修改结构的任何成员,也应该使用指针传递方案。
9. 位段
struct CHAR {
unsigned ch : 7;
unsigned font : 6;
unsigned size : 19;
};
struct CHAR ch1;
位段的的声明和结构成员相同,但有两点例外:一是,位段成员必须为整型;二是,成员名后面是一个冒号和一个整数,这个整数指定位段所占用的位的数目。
在可移植性高的程序中,应避免使用位段,因为位域在不同的系统中可能会有不同的结果:
1. int位段被当作有符号还是无符号数;
2. 位段中位的最大数目范围,32位机器上的位段放到16位机上可能会无法运行;
3. 位段中的成员在内存中是从左到右分配的还是从右到左分配的(大端法(Big Endian),小端法(Little Endian))
4. 当一个声明指定了两个位域,第2个位域比较大,无法容纳第1个位域剩余的位时,编译器有可能把第2个位段放在内存的下一个字节,也可能直接放在第1个位段后面,从而在两个内存位置的边界上形成重叠。
具体还参考位结构体和位域(http://www.cnblogs.com/jeff_nie/archive/2010/10/09/1846221.html)
10. 联合(union)
union {
float f;
int i;
char c;
} fi;
如果在若干种数据数据中,任意使用其中一种,可以使用将这些数据类型定义为联合。联合的以数据类型中最大的作为存储空间大小。上面联合占用4个字节。
初始化联合时,只能对联合的第一个成员初始化值,如:
union {
int a;
float b;
char c;
} x = {5};
如果给出的初始化值是任何其他类型,它会尽可能的转换为整形。