《C语言程序设计现代方法》note-9 C语言字符串详细介绍

助记提要

  1. 字面串中转义序列的注意;
  2. 字面串跨行的两种方式;
  3. 字面串、字符串、字符常量的区别;
  4. 字符串变量的初始化;
  5. 声明字符数组和声明字符指针的区别;
  6. puts和printf写字符串的特点;
  7. scanf和gets读字符串的特点;
  8. 字符串库函数strcpy、strcat、strlen、strcmp的使用;
  9. 编写strlen函数并改进;
  10. 如何存储字符串数组;
  11. 如何访问程序的命令行参数;

13章 字符串

13.1 字面串

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

字面串中使用转义序列

字面串仲可以包含转义序列。

注意 使用八进制和十六进制的转义序列时,要把转义序列和普通字符区分开。
八进制的转义序列在3个数字后结束,或者在第一个非八进制数的字符处结束。如"\1234"表示\1234两个字符,而\189表示\189三个字符。
十六进制的转义序列不限于3个数字,到第一个非十六进制数的字符处结束。大部分编译器会把十六进制转义序列的范围限制在\x0~\xff

字面串跨行延续

两种方式。

  1. 使用\结尾,下一行延续字面串;
    下一行的字面串必须从行的起点继续,会破坏程序的缩进结构。
  2. 多段字面串之间进使用空白字符分割时,编译器会将它们合并为一条字面串。
字面串存储

字面串是当做字符数组存储的。
编译器在遇到字面串时,会分配一个长度为n+1的内存空间。多一个位置用来存储标志字符串结尾的空字符\0

空字符是所有位都为0的字节。字符常量'0'的ASCII编码值是48。

字面串的操作

字面串是做为字符数组存储的,编译器将其看做char *类型的指针。
允许使用char *类型指针的地方都可以使用字面串。

char *p;
// p指向字符a的位置
p = "abc";

字面串也可以取下标。

char ch;
// 字面串取下标
ch = "abc"[1];

// 把数字转为16进制字符形式
char digit_to_hex_char(int digit)
{
    return "0123456789ABCDEF"[digit];
}

字面串虽然被当做数组表示,但是字面串不能改变。而字符数组是可以改变的。

char *p = "abc";
printf("%c\n", *p);  // *p 为字符a
printf("%s\n", p);   // p为整个字面串
// 错误!!!
*p = 'd';
字面串、字符串、字符常量

字面串属于源文件的一部分,是一串使用引号围起来的文本。
字面串经过编译后生成字符串,字符串是指位于系统存储器里面以空字符终止的字符序列。

只包含一个字符的字面串不同于字符常量。
字面串"a"是用指针表示的,而字符常量'a'是用整数表示的。

13.2 字符串变量

C语言规定每个字符串都必须以空字符结尾。
只要保证字符串是以空字符结尾的,任何一维字符数组都能用来存储字符串。

声明长度为n+1的字符数组并不是说它存储的字符串长度是n。字符串的长度取决于空字符的位置。

初始化字符串变量
// 数组形式初始化
char carr[5] = {'a', 'b', 'c', 'd', '\0'};
// 使用字面串初始化
char carr[5] = "abcd";

初始化器不能填满字符串变量时,编译器会在额外的空间填充空字符。

初始化器的长度刚好等于数组长度时,可以编译成功,但是编译器不会存储空字符,导致数组无法做为字符串使用。

初始化器超过数组的长度是非法的。有时会截取前面的一部分字符存到数组中,结尾没有空字符,也不能做为字符串使用。

声明字符串时可以省略长度。编译器会根据初始化器自动确定长度(n+1)。

char carr[] = "abcd";

不指定长度,不代表长度可变。编译结束后,数组的长度就是固定的了。

字符数组和字符指针

比较两种声明:

char carr[] = "abcd";
char *carr = "abcd";

第一个是字面量声明的数组,第二个是指向字面量的指针。由于数组和指针之间的关系,这两个声明都可以当做字符串使用。

未初始化的指针变量不能做为字符串。

它们的不同之处有两点:

  1. 声明为数组时,可以修改其中的元素。声明为指针时,指向字面量,不可以修改字面量。
  2. 声明为数组时,carr是数组名,不能作为左值。声明为指针时,carr是指针变量,可作为左值,所以可以指向其它的字符串。
char carr[5] = "abcd";
// 数组可修改元素
carr[2] = 'x';
char *carr = "abcd";
// 不可修改指向的字面量包含的元素
// carr[2] = 'x';
// 指针变量可以指向别的字面量
carr = "efgh";

13.3 读写字符串

puts函数写字符串

puts函数可以直接写指定字符串,写完后自动添加一个换行符:

char str[] = "Have fun!";
puts(str);
printf写字符串

printf需要使用转换说明符%s写字符串:

printf("%s\n", str);

printf函数会逐个写字符串中的字符,直到遇到空字符为止。
如果没有空字符,printf会越过字符串结尾继续写,直到遇到空字符为止。

格式%m.ps可以指定显示的字符数量为p个,显示长度为m个字符长度。

// 显示前5个字符
printf("%.5s\n", str);
// 前5个字符在大小为10的栏内右对齐显示
printf("%10.5s\n", str);
// 左对齐
printf("%-10.5s\n", str);

如果字符串长度超过m,不会截断,而是显示整个字符串。

scanf读字符串

scanf函数可以使用转换说明符%s把字符串读入字符数组:

scanf("%s", str);

str前不需要加&号,因为数组名会被当做指针。

scanf函数会跳过空白字符,然后读入字符并存到数组中,直到遇到空白字符为止。因此scanf函数读入的字符串不包括空字符。

scanf会自动在字符串末尾存储一个空字符。

输入字符串可能会比用来存储它的字符串变量长。
scanf函数无法检测数组何时被填满,存储时可能会越过数组边界。
可以使用%ns来指定可存储的最多字符数为n个。

gets函数

gets函数在历史上一直用于读入整行输入,但是由于安全问题(无法检测数组边界),在C11被移除。

gets函数不会跳过空白字符。
gets函数会持续读入直到遇到换行符为止。
gets会用空字符代替换行符。

逐个字符读字符串

通过逐个字符读入,能更大程度地控制读入操作。

自己编写的输入函数:
不跳过空白符,在第一个换行符处停止读取,忽略额外输入的字符。

// 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;
}

getchar读取的字符以int类型返回,因此ch的类型定义为int

13.4 访问字符串中的字符

既可以通过数组下标自增访问,也可以通过指针访问。

统计字符串中空格数的函数:

// const表示该函数不会改变数组
int count_spaces(const char s[])
{
    int count = 0, i;
    for (i = 0; s[i] != '\0'; i++)
        if (s[i] == ' ')
            count++;
    return count;
}

字符串可以通过空字符来确定其长度。如果s不是字符串,函数就需要第二个参数指明数组长度。

使用指针实现上述函数:

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

const表示不能修改s指向的数组。而传给函数的形参是指针变量,可以修改,不影响实际参数的指针。

13.5 C语言的字符串库

<string.h>函数库

C语言的运算符无法操作字符串。

字符串不可以通过赋值运算符复制到字符数组。

char str1[10], str2[10];

// str1 = "abc";
// str1 = str2;

可以使用关系运算符或判等运算符,但是比较的不是字符串,而是指针值,因此不会产生预期效果。

if (str1 == str2) ...

<string.h>头文件中定义了操作字符串的函数。
其中的函数的形式参数声明为char *类型,对应的实际参数可以是字符数组、char *类型的变量或字面串。
在函数声明中没有带const的字符串形参,调用函数时可能会发生改变。

strcpy复制字符串
// strcpy的原型
char *strcpy(char *s1, const char *s2);

函数strcpy把s2复制给s1。

// 字面串复制给str2
strcpy(str2, "abcd");
// str2复制给str1
strcpy(str1, str2);
// 字面串同时复制给str1和str2
strcpy(str1, strcpy(str2, "abcd"));

注意 strcpy函数无法检查str2指向的字符串大小是否适合str1指向的数组。strcpy会一直复制到第一个空字符为止,可能会越过str1的边界。

strncpy速度慢些,但是更安全。它有第三个参数用于限制复制的字符数。

// str2中最多复制str1长度的字符
// 如果str2过长,str1会没有终止的空字符
strncpy(str1, str2, sizeof(str1));
// 更安全的做法
strncpy(str1, str2, sizeof(str1) - 1);
str1[sizeof(str1)-1] = '\0';
strlen求字符串长度
// strlen的原型
size_t strlen(const char *s);

用于测量字符串的长度,而不是数组的总长度。

strcat字符串拼接
// strcmp的原型
char *strcmp(char *s1, const char *s2);

把字符串s2的内容追加到s1末尾,返回s1的指针。

// 把内容追加到字符串1
strcat(str1, "abc");
strcat(str1, str2);
// 使用返回值连续拼接
strcpy(str1, "abc");
strcpy(str2, "def")
strcat(str1, strcat(str2, "ghi"));

注意 如果str1的数组不足够容纳str2指向的字符串中的字符,会一直复制到第一个空字符为止,可能会越过str1的边界。

strncat函数速度慢些,但更安全。第三个参数为待复制的字符数。

// 注意第三个参数的计算
strncat(str1, str2, sizeof(str1) - strlen(str1) - 1)
strcmp字符串比较
// strcmp原型
int strcmp(const char *s1, const char *s2);

s1小于s2则返回小于0的值,等于则返回0,大于则返回大于0的值。
适用于关系运算符和判等运算符。

比较时使用字典顺序进行比较。如果前i位字符相同,s1的第i+1位字符在字符集中的数值小于s2的第i+1位字符,则s1小于s2。
前面字符都一致的两个字符串,短的字符串较小。

13.6 字符串惯用法

有时需要重写标准库提供的函数功能。
自己重写这些功能时,需要使用别的名字,即使不包含该函数所属的头。

搜索字符串的结尾

strlen函数初步实现

size_t strlen(const char *s)
{
    size_t n;
    // 遍历到结束,统计字符数
    for (n = 0; *s != '\0'; s++)
        n++;
    return n;
}

*s != '\0'*s != 0一样,因为空字符的整数值就是0。而条件测试*s != 0与直接测试*s是一样的,在*s不为0时才为真。
自增操作s++可以写到前面的条件测试表达式里,效果一样。
改进版1:

size_t strlen(const char *s)
{
    size_t n = 0;
    for (; *s++;)
        n++;
    return n;
}

这个for语句可以使用while语句代替。
空字符的地址减去第一个字符的地址即为字符串的长度。这样用一次计算代替每次循环中n的自增操作,可以加快运行速度。
改进版2:

size_t strlen(const char *s)
{
    const char *p = s;
    while (*s)
        s++;
    return s - p;
}

注意 指针变量p需要使用const声明,因为把s赋值给p会给s指向的字符串带来风险,编译器会警告。

注意 不可用while (*s++),这样会多加一次。

拼接字符串

strcat初步实现

char *strcat(char *s1, const char *s2)
{
    char *p = s1;
    // 确定s1末尾空字符位置
    while (*p != '\0')
        p++;
    // 把s2复制到p指向的位置
    while (*s2 != '\0'){
        *p = *s2;
        p++;
        s2++;
    }
    *p = '\0';
    return s1;
}

改进版:

char *strcat(char *s1, const char *s2)
{
    char *p = s1;
    while (*p)
        p++;
    while (*p++ = *s2++)
        ;
    return s1;
}

第二个循环测试的是赋值表达式的值。复制空字符后,赋值表达式值为假,循环结束。所以不需要另外加空字符。

13.7 字符串数组

字符串数组的存储

使用二维的字符数组,可以存储字符串数组:

char weekday[][4] = {"Mon", "Tues", "Wed", "Thur", "Fri", "Sat", "Sun"};

这种存储方式会浪费很多空间。因为二维数组的列数由存储的最长字符串而定,其它字符串行占不满的空间使用空字符填充。

为避免浪费空间,可以创建一个指向字符串的指针数组:

char *weekday[] = {"Mon", "Tues", "Wed", "Thur", "Fri", "Sat", "Sun"};

可以使用weekday[0]取到第一个字符串。

命令行参数

为了可以访问命令行参数,必须把main函数定义为含有两个参数的函数:

int main(int argc, char *argv[])
{ ... }

argc是命令行参数的数量,包括程序本身。
argv是指向命令行参数的指针数组,命令行参数以字符串的形式存储。其中argv[0]表示程序名,argv[1]argv[argc-1]指向剩下的命令行参数。
argv[argc]是一个空指针。

访问命令行参数的方式

for (int i = 1; i < argc; i++)
    printf("%s\n", argv[i]);
// 定义指向指针的指针
char **p;
// NULL表示空指针
for (p = &argv[1]; *p != NULL; p++)
    printf("%s\n", *p);

更多相关内容参考: 《C语言程序设计:现代方法》笔记

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值