C Programming: A Modern Approach

第13章 字符串

2020-03-08  本文已影响0人  橡树人

英文原版:P277

本章将介绍处理字符串的便捷方法,包括字符串字面量和字符串变量,其中字符串变量会随着程序的执行而改变。

13.1节 字符串字面量

字符串字面量就是用双引号""括起来的字符序列。

理解字符串字面量

例1 字符串字面量

"When you come to a fork in the road, take it."

13.1.1节 在字符串字面量中的转义序列

C语言允许字符串字面量中像字符常量一样包含转义序列。

例2 在字符串字面量中出现转义序列

"Candy\nIs dandy\nBut liquor\n Is Quicker.\n --Ogden Nash\n"

注意:

13.1.2节 延续字符串常量

方法一:首行以反斜线\结尾,且字符串必须从下一行的起始处开始。

例3 使用反斜线来分隔较长的字符串字面量

printf("When you come to a fork in the road, take it.  \n
--Yogi Berra");

方法二:仅使用空格来分隔两个或者多个毗邻的字符串字面量

例4 使用空格来分隔多个字符串常量

printf("When you come to a fork in the road, take it."
        "Yogi Berra");

13.1.3节 如何存储字符串字面量

C语言将字符串字面量当做字符数组

C编译器认为字符串字面量的类型为char *

当C编译器遇到一个长度为n的字符串字面量时,它会为该字符串分配大小为n+1个字节的存储空间。

这块存储空间包含在字符串字面量里的所有字符,及一个额外的空字符null,用于标记该字符串字面量的结束。

空字符null是一个所有位都是0的字节,可用\0来表示。

注意
区分空字符null(\0)和0字符('0')

例5 理解printf调用时是如何传递字符串字面量的

// printf函数的首个参数值类型就是char *
// 调用printf时,是将字符串字面量"abc"的地址(即指向首字符'a'的内存地址的指针)传递给printf函数。
printf("abc");

13.1.4节 字符串字面量有哪些操作?

C语言允许:在任何可以使用char *的地方,都可以使用字符串字面量,比如

注意:
尽量避免去修改字符串字面量,因为尝试去修改字符串字面量会导致未定义的行为。

例1 赋值

char *p;
// 赋值并没有拷贝字符串"abc"中的任意字符;
//赋值只是使得p指向该字符串的首个字符
p = "abc";

例2 下标运算

char ch;
//ch的值是字符'b'
ch = "abc"[1];

例3 将0到15的数字转换成等价的十六进制字符

char digit_to_hex_char(int digit)
{
  return "0123456789abcdef"[digit];
}

例4 错误示例:修改字符串字面量

char *p = "abc";
//修改字符串字面量可能会导致程序崩溃或者出错
*p = 'd';

13.1.5节 字符串字面量 VS 字符常量

注意:

例5 错误示例

//这个是非法的
printf('\n');

13.2节 字符串变量

如何存储字符串变量?

缺点:

例1 存储一个最多有80个字符的字符串

#define STR_LEN 80

char str[STR_LEN+1];

解释:

13.2.1节 字符串变量初始化

例1 初始化式的长度+1等于字符数组长度

char date1[8] = "June 14";
初始化式的长度+1=字符数组长度.png

例2 初始化式的长度小于字符数组长度

char date2[9] = "June 14";
初始化式的长度<字符数组长度.png

例3 初始化式的长度大于字符数组长度

char date3[7] = "June 14";
初始化式的长度>字符数组长度.png

例4 省略字符串数组的长度

char date4[] = "June 14"

解释:C编译器会自动计算长度,为date4分配8个字符的空间,够存储在字符串"June 14"里的字符,加1个空字符null

13.3.2节 字符数组 VS 字符指针

例1 比较字符和数组和字符指针

char date[] = "June 14";

char *date = "June 14";

字符数组和字符指针的不同之处:

如果我们需要一个字符串可被修改,则有3种方法:
方法一:创建一个字符数组来存储该字符串。
方法二:声明一个字符指针p,并使p指向一个字符数组。比如:

char str[STR_LEN+1], *p;

p = str;

方法三:声明一个字符数组,使其之下宁一个动态分配字符串。

注意:
永远不要将一个未初始化的指针变量作为字符串使用。

例1 错误示例

char *p;
//由于指针p没有被初始化,所以如下对p的操作会到导致未定义的行为
p[0] = 'a';
p[1] = 'b';
p[2] = 'c';
p[3] = '\0';

13.3节 字符串的读写

写字符串

读字符串

13.3.1节 使用printfputs来写字符串

printf函数是如何输出字符串的?

puts函数是如何写字符串的?

转换说明%s:输出整个字符串;
%.ps:输出字符串的前p个字符;

%ms:在大小为m的域内显式字符串

%-ms:在大小为m的域内显式字符串

%m.ps:在大小为m的域中输出字符串的前p个字符

例1 使用%s来写字符串

#include <stdio.h>

int main(void)
{
    char str[] = "Are we having fun yet?";
    printf("printf函数效果展示\n");
    printf("%s\n", str);
    printf("%.6s\n", str);
    printf("%10s\n", str);
    printf("%24s\n", str);
    printf("%-24s\n", str);
    printf("%24.6s\n", str);
    printf("%-24.6s\n", str);

    puts("put函数效果展示");
    puts(str);

    return 0;
}

输出

printf函数效果展示
Are we having fun yet?
Are we
Are we having fun yet?
  Are we having fun yet?
Are we having fun yet?  
                  Are we
Are we                  
put函数效果展示
Are we having fun yet? 

13.3.2节 使用scanfgets函数来读字符串

转换说明%s允许scanf将字符串读入字符数组。

scanf是如何读字符串的?

gets函数是如何读字符串的?

gets函数和scanf函数有两点不同:

例2 比较scanf函数和gets函数

#include <stdio.h>

#define SENT_LEN 80

//To C, or not to C: that is the question
int main(void)
{
    char sentence[SENT_LEN+1];

    printf("Enter a sentence:\n");
    //scanf将sentence当做是一个指针,因为sentence是一个数组名
    scanf("%s", sentence);
    printf("%s\n", sentence);

    return 0;
}

输出

Enter a sentence:
  To C, or not to C: that is the question.
To

输入

#include <stdio.h>

#define SENT_LEN 80

//To C, or not to C: that is the question
int main(void)
{
    char sentence[SENT_LEN+1];

    printf("Enter a sentence:\n");
    gets(sentence);
    printf("%s\n", sentence);

    return 0;
}

输出

Enter a sentence:
warning: this program uses gets(), which is unsafe.
  To C, or not to C: that is the question.
  To C, or not to C: that is the question.

注意:

13.3.3节 逐个字符地读取字符串

标准库函数没有提供这个功能,需要C程序员手动编写输入函数来实现。

在设计逐个字符读入字符串时,需要考虑3个基本问题:

  1. 在开始读字符串前是否要跳过空白符?
  2. 哪些字符会导致读停止:换行符还是任何一个空白符?该字符是被存储在字符串中还是丢掉?
  3. 如果输入字符串过于长该怎么办:丢掉额外的字符还是留给下一次输入?

例1 实现read_line函数
目标:

函数原型

int read_line(char str[], int n);

函数定义

int read_line(char str[], int n)
{
  int ch, i=0;
  while((ch = getchar()) != '\n'){
    if (i<n){
      str[i++] = ch;
    }
  }
  str[i] = '\0';
  return i;
}

13.4节 如何访问字符串中的字符?

由于字符串是存储在字符数组中的,所以可以使用下标来访问字符串中的字符。

例1 统计在字符串中的空白符的个数
下标版本:

int count_spaces(const char s[])
{
  int count=0, i;
  
  for(i=0; s[i] != '\0';i++){
    if (s[i] == ' ') {
      count++;
    }
  }
    return count;
}

指针版本:

int count_spaces(char *s)
{
  int count=0;
  
  for(;*s != '\0';s++){
    if (*s == ' ') {
      count++;
    }
  }
    return count;
}

编写字符串函数时需要思考3个基本问题:

  1. 访问在字符串中的字符,是用数组操作更好,还是用指针操作更好?
    都可以。
    C程序员更倾向于使用指针来处理字符串。
  2. 字符串参数是该声明为数组还是指针?
    没有区别,因为编译器会将数组声明当做指针处理。
  3. 形式参数的格式(s[]或者*s)会对实际参数产生影响吗?
    不会,因为调用count_space函数时,可以传递数组名、指针变量、字符串字面量作为实际参数。

13.5节 如何使用C语言的字符串库?

例1 不能使用运算符来拷贝和比较字符串

//错误示例1
char str1[10], str2[10];
//数组名不能用作赋值运算符的左操作数
str1 = "abc";
str2 = str1;

// 来自于12.3节的错误示例2
//可以使用数组名作为指针,但不能给数组名赋新的值
while(*a != 0){
  a++;
}

// 来自于12.3节的正确示例1
//使用数组名作为指针,将a拷贝给一个指针变量p,然后对指针p进行修改
p = a;
while (*p != 0) {
  p++;
}


//正确示例2
//可在声明语句中使用等号来给数组初始化
char str1[10] = "abc";

13.5.1 字符串拷贝函数strcpystrncpy

函数strcpy原型:

char *strcpy(char *s1, const char *s2);

使用函数strcpy的注意事项:

例1 拷贝字符串

#include <stdio.h>
#include <string.h>

int main(void)
{
    char str1[10], str2[10];

    strcpy(str2, "abc");
    printf("%s\n", str2);

    strcpy(str1, str2);
    printf("%s\n", str1);

    return 0;
}

函数strncpy原型:

char * strncpy(char *s1, const char *s2, int n);

函数strncpy调用:

strncpy(str1, str2, sizeof(str1));

函数strncpy使用注意事项:

例2 拷贝字符串

#include <stdio.h>
#include <string.h>

int main(void)
{
    char str1[8], str2[12];

    strncpy(str2, "hello world", sizeof(str2)-1);
    str2[sizeof(str2) - 1] = '\0';
    printf("%s\n", str2);

    strncpy(str1, str2, sizeof(str1));
    str1[sizeof(str1)-1] = '\0';
    printf("%s\n", str1);

    return 0;
}

输出

hello world
hello w

13.5.2节 函数strlen

函数原型:

size_t strlen(const char *s);

解释:

#include <stdio.h>
#include <string.h>

int main(void)
{
    int len;
    char str1[10];

    len = strlen("abc");
    printf("%d\n", len);

    len = strlen("");
    printf("%d\n", len);

    strcpy(str1, "abc");
    len = strlen(str1);
    printf("%d\n", len);

    return 0;
}

输出

3
0
3

函数strcat

函数原型:

char *strcat(char *s1, char *s2);

功能:
函数strcat将字符串s2的内容添加到字符串s1的末尾。
返回值
函数strcat返回s1,是一个指向结果字符串的指针。

例1 字符串拼接

#include <stdio.h>
#include <string.h>

int main(void)
{
    char str1[10];
    char str2[10];

    //字符串字面量的拼接
    //strcpy(str1, "abc");
    //strcat(str1, "def");
    //printf("%s\n",str1);

    //字符串变量的拼接
    strcpy(str1, "abc");
    strcpy(str2, "def");
    strcat(str1, str2);
    printf("%s\n", str1);
    return 0;
}

使用strcat有几个注意事项:

例2 比较安全地使用字符串拼接:使用函数strncat

strncat(str1, str2, sizeof(str1) - strlen(str1) - 1);

解释:

函数strcmp

函数原型

int strcmp(const *s1, const *s2);

功能
函数strcmp比较的是字符串s1和s2的内容。
如果字符串s1小于字符串s2,则返回小于0的值;
如果字符串s1等于字符串s2,则返回0;
如果字符串s1大于字符串s2,则返回大于0的值;

函数strcmp比较字符串大小的规则:

有关ASCII字符集的相关规律:

示例程序:输出一个月的提醒列表

输出示例:

Enter day and reminder: 24 Susan's birthday
Enter day and reminder: 5 6:00 - Dinner with Marge and Russ
Enter day and reminder: 26 Movie - "Chinatown" 
Enter day and reminder: 7 10:30 - Dental appointment
Enter day and reminder: 12 Movie - "Dazed and Confused"
Enter day and reminder: 5 Saturday class
Enter day and reminder: 12 Saturday class
Enter day and reminder: 0
Day  Reminder
5  Saturday class
5  6:00 - Dinner with Marge and Russ
7  10:30 - Dental appointment
12  Saturday class
12  Movie - "Dazed and Confused"
24  Susan's birthday
26  Movie - "Chinatown"

源文件remind.c

#include <stdio.h>
#include <string.h>

#define MAX_REMIND 50
#define MSG_LEN 60

int read_line(char str[], int n);



// 输出一个月的提醒列表
// 输出示例:
// Enter day and reminder: 24 Susan's birthday
// Enter day and reminder: 5 6:00 - Dinner with Marge and Russ
// Enter day and reminder: 26 Movie - "Chinatown" 
// Enter day and reminder: 7 10:30 - Dental appointment
// Enter day and reminder: 12 Movie - "Dazed and Confused"
// Enter day and reminder: 5 Saturday class
// Enter day and reminder: 12 Saturday class
// Enter day and reminder: 0
// Day  Reminder
// 5  Saturday class
// 5  6:00 - Dinner with Marge and Russ
// 7  10:30 - Dental appointment
// 12  Saturday class
// 12  Movie - "Dazed and Confused"
// 24  Susan's birthday
// 26  Movie - "Chinatown"
int main(void)
{
    char reminders[MAX_REMIND][MSG_LEN+3];
    char day_str[3], msg_str[MSG_LEN+1];
    int day, i, j, num_remind = 0;

    for(;;){
        if (num_remind == MAX_REMIND) {
            printf("-- No space left --\n");
            break;
        }

        printf("Enter day and reminder: ");
        scanf("%2d", &day);
        if (day == 0) {
            break;
        }

        sprintf(day_str, "%2d", day);
        read_line(msg_str, MSG_LEN);

        //i表示要插入的提醒的位置
        //为新输入的提醒查找到合适的插入位置,将原位置的所有提醒统一向后移动一位
        for(i=0;i<num_remind;i++){
            if(strcmp(day_str, reminders[i]) < 0){
                break;
            }
        }
        for(j=num_remind;j>i;j--){
            strcpy(reminders[j], reminders[j-1]);
        }

        strcpy(reminders[i], day_str);
        strcat(reminders[i], msg_str);

        num_remind++;
    }

    printf("\nDay Reminder\n");

    for(i=0;i<num_remind;i++){
        printf(" %s\n", reminders[i]);
    }

    return 0;
}

int read_line(char str[], int n)
{
    int ch, i=0;

    while((ch = getchar()) != '\n'){
        if(i<n){
            str[i++] = ch;
        }
    }
    str[i] = '\0';
    return i;
}

13.6节 字符串惯用法

上一篇 下一篇

猜你喜欢

热点阅读