C Primer Plus 第11章(字符串和字符串函数)

1. 表示字符串和字符串 I/O

  • 字符串是以空字符(\0)结尾的 char 类型数组
char a[5] = "abc";
char* b = "ABC";

puts("123");
puts(a);
puts(b);
  • 和 printf() 函数一样,puts() 函数也属于 stdin.h 系列的输入/输出函数
  • 与 printf() 函数不同之处,puts() 函数只显示字符串,并且自动在显示的字符串末尾加上换行符

1.1 在程序中定义字符串

  • 程序应该确保有足够的空间存储字符串

1.1.1 字符串字面量(字符串常量)

  • 用双引号括起来的内容称为字符串字面量(string literal),也叫作字符串常量(string constant)

  • 双引号中的字符和编译器自动加入末尾的 \0 字符,都作为字符串存储在内存中

  • 如果字符串字面量之间没有间隔,或者用空白字符分隔,C会将其视为串联起来的字符串字面量

    char c[5] = "a""b" "c"
        		"d";
    // 等效于
    char c[5] = "abcd";
    
  • 字符串常量属于静态存储类别(static storage class),如果在函数中使用字符串常量,该字符串只会被存储一次,在整个程序的生命期内存在,其实函数被调用多次

  • 用双引号括起来的内容被视为指向该字符串存储位置的指针;类似于把数组名作为指向该数组位置的指针

    char c[5] = "abc";
    
    printf("%s, %p, %c\n", c, c, *c);
    // abc, 008FFA70, a
    

1.1.2 字符串数组和初始化

  • 定义字符串数组时,必须让编译器知道需要多少空间

  • 第一种方法为:用足够空间的数组存储字符串

    char c[5] = "abc";
    
    • 这种形式的初始化比标准的数组初始化形式简单很多
    char c[5] = { 'a', 'b', 'c', '\0' };
    
    • 注意最后的空字符

    • 结尾没有空字符,这就不是一个字符串,而是一个字符数组

    • 在指定数组大小时,要确保数组的元素个数至少比字符串长度多 1(为了容纳空字符)

    • 所有未被使用的元素都被自动初始化为 0

  • 第二种方法:让编译器确定数组的大小

    char c[] = "abc";
    
    • 处理字符串的函数通常都不知道数组的大小,这些函数通过查找字符串末尾的空字符确定字符串在何处

    • 让编译器计算数组大小只能用在初始化数组时

    • 如果创建一个稍后再填充的数组,就必须在声明时指定大小

  • 第三种方法:使用指针表示法创建字符串

    char* c = "abc";
    
    c == &c[0];
    *c == 'a';
    *(c + 1) == 'b';
    

1.1.3 数组和指针

  • 数组形式和指针形式的不同

    • 数组形式
      • 在计算机的内存中分配为一个内含元素的数组(每个元素对应一个字符,还加上一个末尾的空字符 ‘\0’ ),每个元素被初始化为字符串字面量对应的字符
      • 字符串作为可执行文件的一部分存储在数据段中,当把程序载入内存时,也载入了程序中的字符串
      • 字符串存储在静态存储区(static memory)
      • 程序在开始运行时才会为该数组分配内存,此时,才将字符串拷贝到数组中
      • 此时,字符串有两个副本
        • 一个在静态内存中的字符串字面量
        • 另一个是存储在数组中的字符串
      • 此后,编译器把数组名识别为该数组首元素地址的别名
      • 在数组形式中,数组名是地址常量
      • 不能更改数组名,如果改变数组名,则意味着改变了数组的存储位置(地址)
    • 指针形式
      • 同样使得编译器为字符串在静态存储区预留同等数量的空间
      • 另外,一旦开始执行程序,它会为指针变量留出一个存储位置,并把字符串的地址存储在指针变量中
      • 该变量最初指向该字符串的首字符,但是它的值可以改变
    char a[5] = "abc";
    char* b = "abc";
    
    printf("%#x\n", a);
    // 0x8ff9c4
    
    printf("%#x\n", b);
    // 0xac7ce4
    
    printf("%#x\n", "abc");
    // 0xac7ce4
    

1.1.4 数组和指针的区别

char a[5] = "abc";
char* b = "abc";

// a: 常量
// b: 变量
  • 两者的主要区别

    • 数组名是常量
    • 指针名是变量
  • 实际使用

    • 都可以使用数组表示法

      a[0] == 'a';
      b[0] == 'a';
      
    • 都能进行指针的加法操作

      *(a + 1) = 'b';
      *(b + 1) = 'b';
      
    • 只有指针表示法可以进行递增操作

      *(++b) = 'b';
      
    • 改变数组中元素的信息

      • 数组的元素是变量(除非用 const 声明)
      • 数组名是常量
      • 不会修改字符串常量
      a[0] = 'A';
      
    • 改变指针指向字符串的信息

      • 会修改字符串常量
      b[0] = 'A';
      

1.1.5 字符串数组

char* a[2] = { "abc", "ABC" };
char b[2][5] = { "abc", "ABC" };
  • 二者在某些方面非常相似

    • 使用一个下标时都分别表示一个字符串

      a[0] == "abc";
      b[0] == "abc";
      
    • 使用两个下标时都分别表示一个字符

      a[0][0] == 'a';
      b[0][0] == 'a';
      
    • 初始化方式相同

  • 二者的区别

    • 指针表示:
      • 是一个内含指针的数组
      • 指针指向初始化时所用的字符串字面量的位置,这些字符串字面量被存储在静态内存中
    • 数组表示:
      • 是一个内含数组的数组
      • 数组存储着字符串字面量的副本,所以每个字符串都被存储了两次
      • 分配内存的使用率低
      • 每个元素的大小必须相同,而且必须时能存储最长字符串的大小

1.2 指针和字符串

  • 字符串的绝大多数操作都是通过指针完成
char* a = "abc";
char* b;

b = a;

printf("a = %s, &a = %#x, value = %#x\n", a, &a, a);
// a = abc, &a = 0xaff7b0, value = 0xcd7ce4

printf("b = %s, &b = %#x, value = %#x\n", b, &b, b);
// b = abc, & b = 0xaff7a4, value = 0xcd7ce4
  • 通常,程序要完成某项操作只需要知道地址就可以

2. 字符串输入

  • 如果想把一个字符串读入程序,首先必须预留存储该字符串的空间,然后用输入函数获取该字符串

2.1 分配空间

  • 计算机在读取字符串时不会计算它的长度,然后再分配空间

    char* c;
    scanf("%s", c);
    // 编译器给出警告
    
    • scanf() 需要将数据拷贝至参数指定的地址上,而此时的参数是个未初始化的指针,参数可能会指向任何地方
  • 第一种方法:在声明时指明数组的大小

    char c[5];
    
  • 第二种方法:使用 C 库函数来分配内存(12章补充)

  • 为字符串分配内存后,便可以读入字符串

  • C 库提供了许多读取字符串的函数:scanf() 、gets() 和 fgets()

2.2 不幸的 gets() 函数

  • gets() 函数读取整行输入,直至遇到换行符,然后丢弃换行符,存储其余字符,并在这些字符的末尾添加一个空字符使其成为一个 C 字符串
  • 它经常和 puts() 函数配对使用,该函数用于显示字符串,并在末尾添加换行符
char c[5];

puts("Enter str:");
gets(c);
puts(c);
  • 有的编译器在使用 gets() 函数,在编译时会给出警告

  • get() 函数的问题

    • get() 函数唯一的参数是数组地址,其无法检查数组是否装得下输入行
    • get() 函数只知道数组的开始处,并不知道数组中有多少个元素
  • 如果输入的字符串过长,会导致缓冲区溢出(buffer overflow),即多余的字符超出了指定的目标空间

  • 如果多余的字符仅占用了尚未使用的内存,就不会立即出现问题

  • 如果它们擦写掉程序中的其他数据,会导致程序异常中止或其他安全问题

2.3 gets() 的替代品

  • 过去用 fgets() 替代 gets(),fgets() 函数稍微复杂一些,在处理输入方面与 gets() 略有不同
  • C11 新增的 gets_s() 函数也可以替代 gets()
    • 该函数与 gets() 函数更接近,而且可以替换现有的代码中给的 gets()
    • 但是,它是 stdio.h I/O 函数系列中的可选扩展,所以支持 C11 的编译器也不一定支持它

2.3.1 fgets() 函数(和 fputs() )

  • fgets() 函数通过第 2 个参数限制读入的字符数来解决溢出问题

  • 该函数专门设计用于处理文件输入,所以一般情况下可能不太好用

  • fgets() 和 gets() 的区别

    • fgets() 函数的第 2 个参数指明了读入字符的最大数量
      • 如果参数为 n ,那么 fgets() 将读入 n-1 个字符,或者读到遇到的第一个换行符为止
    • 如果 fgets() 读到一个换行符,会把它存储在字符串中
      • 这点与 gets() 不同,gets() 会丢弃换行符
    • fgets() 函数的第 3 个参数指明要读入的文件
      • 如果读入从键盘输入的数据,则以 stdin(标准输入)作为参数,该标识符定义在 stdio.h 中
  • fgets() 函数通常要与 fputs() 函数配对使用,除非该函数不在字符串末尾添加换行符

  • fputs() 函数的第 2 个参数指明它要写入的文件

  • 如果要显示在计算机显示器上,应使用 stdout(标准输出)作为该参数

char c[5];

fputs("Enter str:", stdout);
fgets(c, 5, stdin);
fputs("str:", stdout);
fputs(c, stdout);
  • fputs() 函数返回指向 char 的指针
  • 如果正常执行,该函数返回的地址与传入的第1个参数相同
  • 但是如果函数读到文件结尾,它将返回一个特殊的指针:空指针(null pointer)
  • 该指针保证不会指向有效的数据,所以可用于标识这种特殊情况
  • 在代码中,可以用数字 0 来代替,不过在 C 语言中用宏 NULL 来代替更常见
char c[5];

fputs("Enter str:", stdout);

while (fgets(c, 5, stdin) != NULL && c[0] != '\n')
{
    fputs(c, stdout);
}
  • 循环获取字符串(与 Java I/O流自定义缓冲区进行循环获取相同)

  • 系统使用缓冲的 I/O

  • 这意味着用户在按下 Return 键之前,输入都被存储在临时存储区(缓冲区)中

  • 按下 Return 键就在输入中增加了一个换行符,并把整行输入发送给 fgets()

  • 对于输出,fputs() 把字符发送给另一个缓冲区,当发送换行符时,缓冲区中的内容被发送至屏幕上

  • fputs() 存储换行符的好处和坏处

    • 好处:对于存储的字符串而言,检查末尾是否有换行符可以判断是否读取了一整行
    • 坏处:用户可能并不想把换行符存储在字符串中,这样的换行符会带来一些麻烦
空字符和空指针
  • 空字符(’\0’)
    • 用于标记 C 字符串末尾的字符,其对应字符编码是0
    • 由于其他字符的编码不可能是0,所以不可能是字符串的一部分
  • 空指针(NULL)
    • 有一个值,该值不会与任何数据的有效地址对应
    • 通常,函数使用它返回一个有效地址表示某些特殊情况发生
  • 空字符是整数类型(占1字节),而空指针是指针类型(占4字节)
  • 二者容易混淆的原因
    • 它们都可以用数值0来表示
    • 从概念上来看,二者是不同类型的0

2.3.2 gets_s() 函数

  • gets_s() 函数和 fgets() 类似,用一个参数限制读入的字符数

  • gets_s() 与 fgets() 的区别

    • gets_s() 只从标准输入中读取数据
    • 如果 gets_s() 读到换行符,会丢弃它
    • 如果 gets_s() 读到最大字符数都没有读到换行符,会执行以下步骤
      • 把目标数组中的首字符设置为空字符,读取并丢弃随后的输入直至读到换行符或文件结尾
      • 返回空指针
      • 调用依赖实现的”处理函数“,可能会中止或退出程序
  • gets() 、fgets() 、gets_s() 的适用性

    • 目标存储区可以容纳
      • 三个函数使用正常
      • 但是,fgets() 会保留输入末尾的换行符作为字符串的一部分,要编写额外的代码将其替换成空字符
    • 目标存储区无法容纳
      • gets() 不安全,它会擦写现有数据,存在安全隐患
      • gets_s() 很安全,但是,如果并不希望程序中止或瑞出,就需要使用”处理函数“
        • 另外,如果打算让程序继续运行,gets_s() 会丢弃该输入行的其余字符
      • fgets() 最容易使用,而且可以选择不同的处理方式

2.4 scanf() 函数

  • scanf() 和 gets() 或 fgets() 的区别在于它们如何确定字符串的末尾

    • scanf() 有两种方法确定输入结束
      • 使用 %s 转换说明,以下一个空白字符(空行、空格、制表符或换行符)作为字符串的结束(字符串不包括空白字符)
      • 如果指定宽度,将读取指定宽度个字符或第1个空白字符停止
    • gets() 和 fgets() 会读取第1个换行符之前所有的字符
  • 根据输入数据的性质,用 fgets() 读取从键盘输入的数据更合适

  • scanf() 的典型用法是读取并转换混合数据类型为某种标准形式

  • scanf() 和 gets() 类似,也存在一些潜在缺点

    • 如果输入行的内容过长,scanf() 也会导致数据溢出
    • 在 %s 转换说明中使用字段宽度可以防止溢出

3. 字符串输出

  • C 有3个标准库函数用于打印字符串:puts() 、fputs() 和 printf()

3.1 puts() 函数

  • puts() 函数很容易使用,只需把字符串的地址作为参数传递给它即可
puts("abc");
  • puts() 停止的时机:在遇到空字符是就停止输出

3.2 fputs() 函数

  • fputs() 和 puts() 的区别
    • fputs() 函数的第 2 个参数指明要写入数据的文件
      • 如果要打印在显示器上,可以用定义在 stdio.h 中的 stdout(标准输出)作为该参数
    • 与 puts() 不同,fputs() 不会再输出的末尾添加换行符
  • gets() 丢弃输入中的换行符,puts() 在输出中添加换行符
  • fgets() 保留输入中的换行符,fputs() 不在输出中添加换行符

3.3 printf() 函数

  • 和 puts() 一样,printf() 也把字符串的地址作为参数
  • printf() 可以格式化不同的数据类型
  • 与 puts() 不同之处,printf() 不会自动在每个字符串末尾加上一个换行符

4. 字符串函数

  • ANSI C 把这些处理字符串的函数的原型放在 string.h 头文件中
  • 最常用的函数有 strlen() 、strcat() 、strcmp() 、strncmp() 、strcpy() 、strncpy() 和 sprintf()(额外)

4.1 strlen() 函数

  • 统计字符串的长度
char a[5] = "abc";
printf("%d\n", strlen(a));

4.2 strcat() 函数

  • 用于拼接字符串,接收 2 个字符串作为参数
  • 把第 2 个字符串的备份附加在第 1 个字符串末尾,并把拼接后形成的新字符串作为第 1 个字符串,第 2 个字符串不变
  • strcat() 函数的类型是 char* (指向 char 的指针)
  • strcat() 函数返回第 1 个参数,即拼接第 2 个字符串后的第 1 个字符串的地址
char a[5] = "abc";
char b[5] = "ABC";
strcat(a, b);
puts(a);
puts(b);

4.3 strncat() 函数

  • strcat() 函数无法检查第 1 个数组是否能容纳第 2 个字符串
  • 如果分配给第 1 个数组的空间不够大,多出来的字符溢出到相邻存储单元时就会出现问题
  • strncat() 函数的第 3 个参数指定了最大添加字符数
char a[5] = "abc";
char b[5] = "ABC";
strncat(a, b, 1);
puts(a);
puts(b);

4.4 strcmp() 函数

  • 判断两个字符串的内容是否相等
  • 如果两个字符串参数相同,该函数就返回 0 ,否则返回非零值
char a[5] = "abc";
char b[5] = "ABC";
printf("%d\n", strcmp(a, b));

4.4.1 strcmp() 的返回值

  • 在一些系统中,返回的是两个字符串的前后关系
strcmp("a", "a") == 0;
strcmp("a", "c") == -1;
strcmp("c", "a") == 1;
strcmp("ab", "a") == 1;
  • 在另一些系统中,返回的是两个字符串的 ANSII 码的差值
strcmp("a", "a") == 0;
strcmp("a", "c") == -2;
strcmp("c", "a") == 2;
strcmp("ab", "a") == 98;

4.4.2 strncmp() 函数

  • 可以限定比较的长度
strcmp("abc", "abd") == -1;
strncmp("abc", "abd", 2) == 0;

4.5 strcpy() 和 strncpy() 函数

  • 拷贝整个字符串
  • 拷贝出来的字符串被称为目标字符串
char a[5] = "abc";
char b[5] = "ABC";
strcpy(a, b);
printf("%s\n", a);
// ABC

4.5.1 strcpy() 的其他属性

  • 重要属性
    • strcpy() 的返回类型是 char* ,该函数返回的是第 1 个参数的值,即第一个字符的地址
    • 第 1 个参数不必指向数组的开始,这个属性可用于拷贝数组的一部分
char a[5] = "abc";
char b[5] = "ABC";
strcpy(a + 1, b);
printf("%s\n", a);
// aABC

4.5.2 更谨慎的选择:strncpy()

  • strcpy() 不能检查目标空间是否能容纳源字符串的副本
  • strncpy() 更安全,该函数的第 3 个参数指明可拷贝的最大字符数
char a[5] = "abc";
char b[5] = "ABC";
strncpy(a, b, 1);
printf("%s\n", a);
// Abc

4.6 sprintf() 函数

  • sprintf() 函数声明在 stdio.h 中
  • 该函数是把数据写入字符串,可以把多个元素组合成一个字符串
  • sprintf() 的第 1 个参数是目标字符串的地址
char a[5] = "abc";
char b[5] = "ABC";
sprintf(a, "%.1s%s", a, b);
printf("%s\n", a);
// aABC

4.7 其他字符串函数

  • char* strchr(char* str, char c);
    
    • 返回指向字符串中首次出现的字符的指针
    • 如果在字符串中未找到字符则返回空指针
  • char* strbrk(char* str1, char* str2);
    
    • 如果 s1 中包含 s2 的任意字符,则返回指向 s1 字符串首位置的指针
    • 如果在 s1 字符串中未找到 s2 字符串中的字符,则返回空指针
  • char* strrchr(char* str, char c);
    
    • 返回指向字符串中最后一次出现的字符的指针
    • 如果在字符串中未找到字符则返回空指针
  • char* strstr(char* str1, char* str2);
    
    • 返回指向 s1 字符串中 s2 字符串出现的首位置
    • 如果在 s1 字符串中未找到 s2 字符串,则返回空指针

5. ctype.h 字符函数和字符串

  • 字符函数无法处理字符串,但是可以处理字符串中的字符
char a[5] = "abc";
char* b;

b = a;

while (*b)
{
    *b = toupper(*b);
    b++;
}

printf("%s", a);
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值