介绍
编写一个 C 函数,扫描字符串并提取出位于其开头的数字
识别数字的标准参考下图

函数声明:
/// \brief
/// 提取出位于 `str` 开头的数字
/// \details
/// 尽可能多地扫描字符,直到不再构成合法的数字 \n
/// 支持整数、小数和科学计数法 \n
/// 整数部分不支持有多余的正号和前导零
/// \return
/// 返回该数字最末字符再往后一位字符的地址 \n
/// 若 `str` 开头没有数字则返回 `NULL`
char *extract_number(const char *str);
用法示例
#include <stdio.h>
int main(void) {
system("chcp 65001");
setbuf(stdout, NULL);
char str[64];
while (1) {
printf("\n请输入字符串:");
rewind(stdin);
scanf("%s", str);
char *num_end = extract_number(str); /* 提取数字 */
if (NULL == num_end)
printf("无法从字符串 \"%s\" 中提取出数字\n", str);
else {
printf("从字符串 \"%s\" 中提取出的数字是:", str);
*num_end = '\0'; // 截断字符串,只保留开头的数字部分
puts(str);
}
}
return 0;
}
请输入字符串:2.71828
从字符串 "2.71828" 中提取出的数字是:2.71828
请输入字符串:1024
从字符串 "1024" 中提取出的数字是:1024
请输入字符串:6.626070e-19[[
从字符串 "6.626070e-19[[" 中提取出的数字是:6.626070e-19
请输入字符串:1231__sf
从字符串 "1231__sf" 中提取出的数字是:1231
请输入字符串:78E6...
从字符串 "78E6..." 中提取出的数字是:78E6
请输入字符串:01
从字符串 "01" 中提取出的数字是:0 # 没有识别成 01(参照上述给出的识别标准)
请输入字符串:abcd
无法从字符串 "abcd" 中提取出数字
代码实现
可以用有穷状态自动机 DFA 来描述和解决这个问题
初态:S0
终态:I1
、I2
、I3
、F2
、E2
.
表示读入小数点-
表示读入负号+|-
表示读入正号或负号0
表示读入数字 01~9
表示读入 1 ~ 9 中的任一个数字0~9
表示读入 0 ~ 9 中的任一个数字e
表示读入字母 e 或 E
基于【goto】实现 DFA
char *extract_number(const char *str) {
const char *p = str;
const char *r = NULL;
if ('1' <= *p && *p <= '9') goto I1;
if (*p == '-') goto I0;
if (*p == '0') goto I3;
goto ED;
I0: ++p;
if ('1' <= *p && *p <= '9') goto I1;
if (*p == '0') goto I3;
goto ED;
I1: r = ++p;
if ('0' <= *p && *p <= '9') goto I2;
if ('.' == *p) goto F0;
if ('e' == *p || 'E' == *p) goto E0;
goto ED;
I2: r = ++p;
if ('0' <= *p && *p <= '9') goto I2;
if ('.' == *p) goto F0;
if ('e' == *p || 'E' == *p) goto E0;
goto ED;
I3: r = ++p;
if ('.' == *p) goto F0;
if ('e' == *p || 'E' == *p) goto E0;
goto ED;
F0: ++p;
if ('0' <= *p && *p <= '9') goto F1;
goto ED;
F1: r = ++p;
if ('0' <= *p && *p <= '9') goto F1;
if ('e' == *p) goto E0;
goto ED;
E0: ++p;
if ('0' <= *p && *p <= '9') goto E2;
if (*p == '-' || *p == '+') goto E1;
goto ED;
E1: ++p;
if ('0' <= *p && *p <= '9') goto E2;
goto ED;
E2: r = ++p;
if ('0' <= *p && *p <= '9') goto E2;
goto ED;
ED: return (char *)r;
}
基于【if】【while】实现 DFA
char *extract_number(const char *str) {
const char *p = str;
const char *r = NULL;
/* 整数部分 */
if (*p == '-')
++p;
if ('1' <= *p && *p <= '9') {
r = ++p;
while ('0' <= *p && *p <= '9')
r = ++p;
}
else if (*p == '0')
r = ++p;
else
goto end;
/* 小数部分 */
if (*p == '.') {
++p;
if ('0' <= *p && *p <= '9')
r = ++p;
else
goto end;
while ('0' <= *p && *p <= '9')
r = ++p;
}
/* 指数部分 */
if (*p == 'e' || *p == 'E') {
++p;
if (*p == '-' || *p == '+')
++p;
if ('0' <= *p && *p <= '9') {
r = ++p;
while ('0' <= *p && *p <= '9')
r = ++p;
}
}
end:
return (char *)r;
}
基于【状态转换表】实现 DFA
char *extract_number(const char *str) {
enum state {
// --------------------------------------------
// 状态集
// --------------------------------------------
S0, // start 开始
I0, I1, I2, I3, // integer 正在识别整数部分
F0, F1, // fraction 正在识别小数部分
E0, E1, E2, // exponent 正在识别指数部分
ED, // end 结束,退出状态机
// --------------------------------------------
// 初态:S0 终态:I1, I2, I3, F1, E2
};
enum symbol {
// --------------------------------
// 输入符号集
// --------------------------------
PLUS, // + | 正号
MINUS, // - | 负号
POINT, // . | 小数点
ZERO, // 0 | 数字 0
DIGIT, // 1~9 | 数字 1~9
EXP, // e E | 指数符号 e 或 E
OTHERS, // | 其他符号
// --------------------------------
SYMBOL_SET_SIZE
};
static enum state transition_table[] = {
// ------------------------------------
// 状态转换表
// ------------------------------------
// + - . 0 1~9 eE others
/* S0 */ ED, I0, ED, I3, I1, ED, ED,
/* I0 */ ED, ED, ED, I3, I1, ED, ED,
/* I1 */ ED, ED, F0, I2, I2, E0, ED,
/* I2 */ ED, ED, F0, I2, I2, E0, ED,
/* I3 */ ED, ED, F0, ED, ED, E0, ED,
/* F0 */ ED, ED, ED, F1, F1, ED, ED,
/* F1 */ ED, ED, ED, F1, F1, E0, ED,
/* E0 */ E1, E1, ED, E2, E2, ED, ED,
/* E1 */ ED, ED, ED, E2, E2, ED, ED,
/* E2 */ ED, ED, ED, E2, E2, ED, ED,
// ------------------------------------
};
const char *p = str;
const char *r = NULL;
/* DFA 开始 */
enum state state = S0;
for (;;) {
/* 读入字符,判断字符属于符号集中的哪个符号 */
enum symbol input;
switch (*p++) {
case '1': case '2': case '3': case '4':
case '5': case '6': case '7': case '8':
case '9': input = DIGIT; break;
case '0': input = ZERO; break;
case '+': input = PLUS; break;
case '-': input = MINUS; break;
case '.': input = POINT; break;
case 'e':
case 'E': input = EXP; break;
default: input = OTHERS; break;
}
/* 状态转移 */
state = transition_table[SYMBOL_SET_SIZE * state + input];
/* 每到一次终态则记录一次位置 */
switch (state) {
case I1:
case I2:
case I3:
case F1:
case E2: r = p;
default:;
}
/* 直到新读入的字符与已扫描的部分不再构成合法的数字 */
if (ED == state)
break;
}
return (char *)r;
}
在定义 enum symbol
时,往末尾添加一项 SYMBOL_SET_SIZE
,即可得到输入符号集的大小,也就是状态转换表的列数
状态转换表的行列索引的顺序要与 enum state
和 enum symbol
的顺序保持一致