09-C语言格式化字符串操作
一、格式化字符串概述
在前面的学习中已经接触过了字符串格式化的相关操作,如:格式化输出printf("我的名字是:%s,今年%02d岁了\n", name, age)
,这个格式化输出是默认输出到终端设备上的。其实也可以将字符串格式化输出到指定的字符数组,或者文件中去,同时也可以对一段字符串按照格式拆解成一个个字串。格式化字符串在后续学习中有较为广泛的运用,包括在网络通信部分,涉及到通信数据的拆包、组包操作等。
二、字符串组包
1.sprintf函数
sprintf
函数用于将格式化字符串数据输出到指定的字符数组,用于数据的组包操作。
- 代码演示
int main() {
// 创建一个空字符数组
char str[64] = "";
// 定义三个日期变量
int year = 2024;
int month = 12;
int day = 5;
// 将上面的日期按照****年**月**日格式组包到str数组,不足位补0
int len = sprintf(str, "%04d年%02d月%02d日", year, month, day);
printf("%s\n", str);
printf("%d", len);
return 0;
}
- 运行结果
2024年12月05日
14
- 注意:
- 由上面的代码可以看出,
sprintf
函数对目的字符数组有写操作,因此这里的字符数组必须允许读写操作,而不能定义成char *
指针指向文字常量区; sprintf
函数的返回值为组包的字符串的长度。
- 由上面的代码可以看出,
2.fprintf函数
fprintf
函数和sprintf
差不多,都是用于数据的组包操作,只不过这里的函数是将格式化字符串写入到文件里面。
- 代码演示:文件操作后面学习,这里知道
fprintf
函数用于将组包数据写入文件即可
int main() {
// 以写入模式打开一个文件
FILE *fd = fopen("a.txt", "w");
// 定义三个日期变量
int year = 2024;
int month = 12;
int day = 5;
// 将上面的日期按照****年**月**日格式组包到a.txt文件,不足位补0
fprintf(fd, "%04d年%02d月%02d日\n", year, month, day);
printf("%s\n", fd);
fclose(fd);
return 0;
}
- 运行结果:生成了一个 a.txt 文件,并且往文件里写入了组好的数据
三、字符串解包
1.sscanf函数
sscanf
函数用于按照指定的格式从字符串中提取想要的数据,用于数据的解包操作。
1.1sscanf字符串提取
字符串提取:sscanf
与%s
结合提取字符串。
- 代码演示
int main() {
// 创建字符数组
char str1[32] = "hello world!";
char *str2 = "hel\0lo world!";
// 创建字符数组用于保存解包数据
char str3[32] = "";
sscanf(str1, "%s", str3);
char str4[32] = "";
sscanf(str2, "%s", str4);
printf("%s\n", str3);
printf("%s\n", str4);
return 0;
}
- 运行结果
hello
hel
- 根据上面代码的运行结果可知:
- 函数格式:
sscanf(被解包的字符串,解包格式,存放解包结果的内存起始地址)
; sscanf
按照%s
解包操作提取字符串的时候,遇到空格、'\0’和回车结束;- 被解包的字符串只是读操作,可以存放在文字常量区,但用于存放解包结果的字符数组要允许读写操作。
- 函数格式:
1.2sscanf数值提取
数值提取:sscanf
与%d %ld %hd %u %lu %hu
结合提取数值。
- 代码演示
int main() {
// 创建字符数组
char str[64] = "2358dece8679";
int num = 0;
// %d提取数值
sscanf(str, "%d", &num);
printf("%d\n", num);
return 0;
}
- 运行结果
2358
- 说明:
- 数值提取时,遇到非数值字符结束提取;
- 因为要将提取的数值通过一个
int
变量来接收,即将提取数值写入数值变量,sscanf
第三个元素为普通变量的时候要取地址。
1.3sscanf字符提取
字符提取:sscanf
与`%c结合提取一个字符。
- 代码演示
int main() {
// 创建字符数组
char str[64] = "2358dece8679";
char ch = '0';
// %c提取字符
sscanf(str, "%c", &ch);
printf("%c\n", ch);
return 0;
}
- 运行结果
2
- 说明:不管是提取字符串、数值还是字符,都应该遵循提取什么类型的数据就按对应格式提取。因为提取的结果是通过一个新的变量来接收,是写操作,因此用于接收的变量作为
sscanf
函数参数的时候要地址传递。
2.sscanf的高级用法
2.1提取指定个数的字符或数字
提取指定个数的字符或数字,按照 %3s %3d
来提取,3代表要提取的字符或数字的个数,也可以是其它值。
- 代码演示:%ns
int main() {
// 创建字符数组
char str[64] = "hello world";
char str1[64] = "hello\0world";
// 提取指定长度字符串
char str2[64] = "";
sscanf(str, "%3s", str2);
printf("%s\n", str2);
sscanf(str, "%10s", str2);
printf("%s\n", str2);
sscanf(str1, "%10s", str2);
printf("%s\n", str2);
return 0;
}
- 运行结果
hel
hello
hello
-
结论:
%ns
提取指定个数字符的时候,如果还没有取到n个字符就遇到了空格,则提前结束;%ns
提取指定个数字符的时候,如果还没有取到n个字符就遇到了’\0’,则提前结束。
-
代码演示:%nd
int main() {
// 创建字符数组
char str[64] = "1234cewc89";
// 提取数值
int num = 0;
sscanf(str, "%3d", &num);
printf("num = %d\n", num);
printf("num = %d\n", ++num); // 124,可以进行数值运算,证明是数值
sscanf(str, "%6d", &num);
printf("num = %d\n", num);
return 0
}
- 运行结果
num = 123
num = 124
num = 1234
- 结论:
%nd
提取指定个数值的时候,如果还没有取到n个数值就遇到了非数值,则提前结束。
2.2跳过提取到的字符或数值
%*c
跳过提取到的单个字符,%*s
跳过提取到的1个字符,%*d
跳过提取到的1数值,%*3s
跳过提取到的3个字符, %*3d
跳过提取到的3个数值。
- 代码演示
int main() {
// 创建字符数组
char str[64] = "hello world!";
char str1[64] = "123456789";
// 提取字符
char str2[64] = "";
sscanf(str, "%*6s%s",str2);
printf("%s\n", str2);
// 提取数值
int num = 0;
sscanf(str1, "%*3d%3d", &num);
printf("%d\n", num);
return 0;
}
- 运行结果
world!
456
- 结论:虽然
sscanf
通过%s
提取字符串的时候遇到空格’和\0’会提前结束,但是%*s
屏蔽的时候,会将空格屏蔽,'\0’无法屏蔽。
3.3匹配指定范围的字符
如:%[a-z]
表示匹配 a 到 z 中任意字符。
- 代码演示
int main() {
// 创建字符数组
char str[64] = "hello12345worldABCDE";
char str1[64] = "";
sscanf(str, "%[a-z | 1-9]", str1);
printf("%s\n", str1);
return 0;
}
- 运行结果
hello12345world
- 匹配指定范围的的字符,支持与或操作,遇到非范围内的字符就结束提取。
3.4匹配指定多个字符中的字符
如:%[heA]
匹配heA
中的字符,遇到非heA
的字符结束提取
- 代码演示
int main() {
// 创建字符数组
char str[64] = "hello12345worldABCDE";
char str1[64] = "";
sscanf(str, "%[heA]", str1);
printf("%s\n", str1);
return 0;
}
- 运行结果
he
3.5匹配非指定范围内的字符
如:%[^0-9]
,匹配非0-9的字符,遇到0-9中任意一个字符就结束;%[^l35]
,匹配非l35的字符,遇到任意l35字符中的一个就结束。
- 代码演示
int main() {
// 创建字符数组
char str[64] = "hello12345worldABCDE";
char str1[64] = "";
sscanf(str, "%[^0-9]", str1);
printf("%s\n", str1);
sscanf(str, "%[^l35]", str1);
printf("%s\n", str1);
return 0;
}
- 运行结果
hello
he
- 案例:
int main() {
// 创建字符数组
char str[64] = "(hello:world!)";
char str1[64] = "";
// 提取hello
sscanf(str, "%*c%[^:]", str1);
printf("%s\n", str1);
// 提取world
sscanf(str, "%*[^:]%*c%[^!]", str1);
printf("%s\n", str1);
return 0;
}
- 运行结果
hello
world
3.6案例歌词提取
- 提取如下字符串的内容,
"[02:16.33][04:11.44]我想大声宣布对你依依不舍"
,打印在第多少秒输出歌词
int main() {
// 创建字符数组
char str[128] = "[02:16.33][04:11.44]我想大声宣布对你依依不舍";
// 定义数组用于存放歌词
char str1[128] = "";
// 用于存放分钟
int min1 = 0;
// 用于存放秒数
int sec1 = 0;
int min2 = 0;
int sec2 = 0;
sscanf(str, "%*[^]]%*c%*[^]]%*c%s", str1);
sscanf(str,"%*c%d", &min1);
sscanf(str,"%*[^:]%*c%d", &sec1);
printf("第%ds播放歌词:%s\n", min1 * 60 +sec1, str1);
sscanf(str,"%*c%*[^[]%*c%d", &min2);
sscanf(str,"%*[^:]%*c%*[^:]%*c%d", &sec2);
printf("第%ds播放歌词:%s\n", min2 * 60 +sec2, str1);
return 0;
}
- 运行结果
第136s播放歌词:我想大声宣布对你依依不舍
第251s播放歌词:我想大声宣布对你依依不舍
虽然能够得到结果,但代码存在大量冗余,且代码不具备通用性,假设现在时间增加一个或多个,上面的代码就无法实现需求了。
- 较通用版本:观察发现时间的格式都是一样的,可以利用这点
int main() {
// 创建字符数组
char str[128] = "[02:16.33][04:11.44][05:11.44]我想大声宣布对你依依不舍";
// 定义一个指针指向歌词位置
char *lrc = str;
while (*lrc == '[')
{
lrc += 10;
}
// 定位不同时间
char *time = str;
while(*time == '[')
{
int min = 0;
int sec = 0;
sscanf(time, "%*c%d", &min);
sscanf(time, "%*[^:]%*c%d", &sec);
printf("第%ds播放歌词:%s\n", min * 60 +sec, lrc);
time += 10;
}
return 0;
}
- 运行结果
第136s播放歌词:我想大声宣布对你依依不舍
第251s播放歌词:我想大声宣布对你依依不舍
第311s播放歌词:我想大声宣布对你依依不舍
四、const修饰的变量
被const
修饰的变量就不能再通过被修饰的变量改变相应的值了。
1.const修饰普通变量
- 代码演示
int main() {
const int num = 10;
printf("num = %d\n", num);
// 不能通过num去修改num的值了
// num = 20; // error: assignment of read-only variable 'num'
// 通过一个指针指向num的值
int *p = (int *)#
*p = 20;
printf("num = %d\n", num); // 修改成功
return 0;
}
- 运行结果
num = 10
num = 20
- 说明:
- 被
const
修饰的变量可以对其初始化赋值; - 除了初始化赋值,后面不能再通过被
const
修饰的变量修改相应的值; - 这里仅仅是不能通过被修饰的变量名修改其值,但是找到数据存储的内存地址依旧能改变其值,因为这里仅仅是修饰变量名而已,而不是修饰存储数据的空间。
- 被
2.const修饰指针变量
2.1const在*
的左边
const
在*
的左边:const int *p
和int const *p
是一个意思,这种情况下,const修饰的是*
,即不能通过*p
进行写操作。
- 代码演示
int main() {
int num = 10;
printf("num = %d\n", num);
const int *p = #
// 可以通过*p进行读操作,不能进行写操作
printf("%d\n", *p); // 10
// *p = 20; // error: assignment of read-only location '*p'
// p可读可写
int num2 = 20;
p = &num2;
printf("%d\n", *p); // 20
return 0;
}
- 运行结果
num = 10
10
20
- 说明:
- 由上面的代码可以看出,
const
在*
的左边时,*p
可读不可写,p可读可写。 - 实际应用中这种写法通常运用于函数的参数,我们将一个外部变量作为函数参数时,实参太大,所以就地址传递,但是又不希望函数内部修改外部的变量,因此就对
*p
在*
左边加const
修饰。
- 由上面的代码可以看出,
2.2const在*
的右边
cons
t在*
的右边;即int * const p
,此时const
修饰的是p。
- 代码演示
int main() {
int num = 10;
printf("num = %d\n", num);
int *const p = #
// 可以通过*p修改num的值
*p = 20;
printf("%d\n", num); // 20
// 不能修改p的指向
int num2 = 40;
// p = &num2; // error: assignment of read-only variable 'p'
return 0;
}
- 运行结果
num = 10
20
- 说明:
- 由上面的代码可以看出,
const
在*
的右边时,*p
可读可写,p可读不可写; - 实际应用中,这种
const
修饰方式用于不希望修改指针指向的情况。
- 由上面的代码可以看出,
2.3const既在*
左又在*
右
const
即在*
左又在*
右:即const int *const p
- 代码演示
int main() {
int num = 10;
printf("num = %d\n", num);
const int *const p = #
// 不可以通过*p修改num的值
// *p = 20; // error: assignment of read-only location '*(const int *)p'
printf("%d\n", num);
// 不能修改p的指向
int num2 = 40;
// p = &num2; // error: assignment of read-only variable 'p'
return 0;
}
- 说明:
- 由上面的代码可以看出,
const
在*
的左右两边时,*p
可读不可写,p可读不可写; - 实际应用中,这种
const
修饰*
左右两边的方式,是不希望修改指针指向的内容同时不希望修改指针的指向。
- 由上面的代码可以看出,
五、别名
typedef
给已有类型取别名,当我们需要重复定义某种类型的数据的时候,如果类型名过长,定义起来比价麻烦,就需要为其取一个较短的别名。
1.别名的定义步骤
- 代码演示
int main() {
typedef int I;
I num1 = 50;
printf("%d\n", num1); // 50
return 0;
}
- 别名定义步骤:
- 先定义想要定义的类型的变量:
int num
; - 然后在其前面假设typedef关键字:
typedef int num
; - 最后通过别名替换变量名:
typedef int I
。
- 先定义想要定义的类型的变量:
2.创建的typedef定义形式
2.1指针类型起别名
- 代码演示
int main() {
int num = 0;
typedef int *P_INT;
P_INT p = #
*p = 10;
printf("%d\n", num); // 10
return 0;
}
- 案例:下面代码p1,p2和p3,p4有什么区别
#define P_TYPE1 int*
typedef int *P_TYPE2;
int main() {
// 宏替换相当于:int *p1, p2;
P_TYPE1 p1, p2;
// 起别名,将int * 看成一个整体,相当于:int *p3, *p4
P_TYPE2 p3, p4;
return 0;
}
- 答案:p1是指针变量,类型为
int *
;p2是普通变量,类型为int
;p3,p4都是指针变量,类型为int *
。
2.2数组类型起别名
- 代码演示
int main() {
typedef int INTNUM_5[5];
INTNUM_5 nums = {1, 2, 3, 4, 5};
// 遍历数组
int i = 0;
for (i = 0; i < 5; i++)
{
printf("%d ", nums[i]);
}
printf("\n");
return 0;
}
- 运行结果
1 2 3 4 5
2.3函数指针类型
- 代码演示
int main() {
typedef int (*MY_FUNC)(int, int);
MY_FUNC p = my_add;
MY_FUNC p1 = my_mul;
printf("%d\n", p(3, 5));
printf("%d\n", p1(3, 5));
return 0;
}
- 运行结果
8
15
- 在实际写代码的过程中,可能很多的函数的形参都是别名,可以在一下高级编译软件下编程时按住ctrl,然后鼠标左键点击能查看其本来面目。