4. 识别字符串与字符字面量
任务目标
在词法分析器中增加对字符字面量(单引号)与字符串字面量(双引号)的识别,并正确处理常见转义序列(如 \n、\t、\\、\'、\")。输入一段源码文本,输出对应的 流,每个 包含类型、字面量值和在源码中的位置。
实现方案
采用手工递归下降方式实现一个最小词法分析器。核心逻辑集中在 next_() 函数中:当遇到单引号或双引号时,调用对应的扫描函数,逐字符读取直到匹配的结束引号,期间处理反斜杠开头的转义序列。字符字面量只允许一个字符(转义序列展开后算一个字符),字符串字面量允许任意长度。扫描完成后返回 ,并记录起始行号和列号。
完整代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
typedef enum {
_CHAR,
_STRING,
_EOF,
_ERROR
} TokenType;
typedef struct {
TokenType type;
char *value; // 字符或字符串的实际内容(转义已展开)
int line;
int col;
} ;
static const char *src;
static int pos = 0;
static int line = 1;
static int col = 1;
static void advance() {
if (src[pos] == '\n') { line++; col = 1; }
else col++;
pos++;
}
static char peek() { return src[pos]; }
static char peek_next() { return src[pos + 1]; }
static char decode_escape(char esc) {
switch (esc) {
case 'n': return '\n';
case 't': return '\t';
case 'r': return '\r';
case '0': return '\0';
case '\\': return '\\';
case '\'': return '\'';
case '\"': return '\"';
default: return esc; // 未知转义按原字符处理
}
}
static make_(TokenType type, char *value, int start_line, int start_col) {
t;
t.type = type;
t.value = value;
t.line = start_line;
t.col = start_col;
return t;
}
static scan_char() {
int start_line = line, start_col = col;
advance(); // 跳过开头的单引号
if (peek() == '\'' || peek() == '\0') {
return make_(_ERROR, strdup("unterminated char literal"), start_line, start_col);
}
char ch;
if (peek() == '\\') {
advance();
if (peek() == '\0') {
return make_(_ERROR, strdup("unterminated escape"), start_line, start_col);
}
ch = decode_escape(peek());
advance();
} else {
ch = peek();
advance();
}
if (peek() != '\'') {
return make_(_ERROR, strdup("char literal must contain exactly one character"), start_line, start_col);
}
advance(); // 跳过结尾的单引号
char *buf = malloc(2);
buf[0] = ch;
buf[1] = '\0';
return make_(_CHAR, buf, start_line, start_col);
}
static scan_string() {
int start_line = line, start_col = col;
advance(); // 跳过开头的双引号
size_t cap = 16, len = 0;
char *buf = malloc(cap);
while (peek() != '\0' && peek() != '"') {
if (peek() == '\\') {
advance();
if (peek() == '\0') break;
char c = decode_escape(peek());
if (len + 1 >= cap) { cap *= 2; buf = realloc(buf, cap); }
buf[len++] = c;
advance();
} else {
if (len + 1 >= cap) { cap *= 2; buf = realloc(buf, cap); }
buf[len++] = peek();
advance();
}
}
if (peek() != '"') {
free(buf);
return make_(_ERROR, strdup("unterminated string literal"), start_line, start_col);
}
advance(); // 跳过结尾的双引号
buf[len] = '\0';
return make_(_STRING, buf, start_line, start_col);
}
next_() {
while (isspace(peek())) advance();
if (peek() == '\0') {
return make_(_EOF, strdup(""), line, col);
}
if (peek() == '\'') return scan_char();
if (peek() == '"') return scan_string();
// 其他字符(如标识符、运算符)不在本案例范围内,直接跳过并报错
char *err = malloc(16);
snprintf(err, 16, "unexpected char: %c", peek());
advance();
return make_(_ERROR, err, line, col - 1);
}
int main() {
// 测试输入:包含字符、字符串和转义序列
const char *test_src = "'a' \"hello\\nworld\" '\\'' \"say \\\"hi\\\"\"";
src = test_src;
pos = 0; line = 1; col = 1;
t;
do {
t = next_();
if (t.type == _CHAR) {
printf("CHAR '%s' @%d:%d\n", t.value, t.line, t.col);
} else if (t.type == _STRING) {
printf("STRING \"%s\" @%d:%d\n", t.value, t.line, t.col);
} else if (t.type == _ERROR) {
printf("ERROR %s @%d:%d\n", t.value, t.line, t.col);
}
free(t.value);
} while (t.type != _EOF && t.type != _ERROR);
return 0;
}
编译与运行
gcc -o lexer lexer.c
./lexer
验证方法
程序内置了测试输入 'a' "hello\nworld" '\'' "say \"hi\"",包含四种典型情况:普通字符、含换行转义的字符串、转义单引号字符、含转义双引号的字符串。运行后观察输出:
- 第1个 应为
CHAR 'a',位置@1:1; - 第2个 应为
STRING "hello\nworld"(其中\n已展开为真实换行符,终端显示为换行),位置@1:5; - 第3个 应为
CHAR '''(单引号字符),位置@1:22; - 第4个 应为
STRING "say \"hi\""(含双引号字符),位置@1:27; - 最后输出
EOF。
若输出与上述一致,说明字符/字符串识别与转义处理均正确。可修改 test_src 中的内容验证更多边界情况(如未闭合引号、空字符串 "" 等),观察错误提示是否符合预期。