Добавление переменных
Сохраняйте значения и обращайтесь к ним.
«Добавление переменных» — бесплатный урок C Academy на CoddyKit. Это урок 4 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения C Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс C Academy содержит 4 уроков всего.
За пределами арифметики
Чтобы поддержать x = 5, а затем x + 1, интерпретатору нужны идентификаторы и место для хранения их значений. Это место называется окружением.
Мы расширим лексер, парсер и вычислитель, чтобы они обрабатывали имена и присваивание.
Лексический разбор идентификаторов
Идентификатор начинается с буквы или символа подчёркивания, а затем может содержать буквы, цифры и символы подчёркивания. Мы копируем имя в токен.
Буфер фиксированного размера упрощает пример; настоящие лексеры помещают имена в таблицу интернирования.
#include <ctype.h>
#include <string.h>
typedef struct { TokKind kind; int value; char name[32]; } Token;
static Token lex_ident(void) {
Token t; t.kind = TOK_IDENT; int i = 0;
while (isalnum((unsigned char)peek()) || peek() == '_')
if (i < 31) t.name[i++] = advance(); else advance();
t.name[i] = '\0';
return t;
}Узел переменной
AST получает два вида узлов: ссылку на переменную и присваивание. Каждый из них хранит имя переменной.
Узел присваивания также хранит выражение, значение которого он сохраняет.
typedef struct Node {
enum { N_NUM, N_BINOP, N_VAR, N_ASSIGN } kind;
int value; char op; char name[32];
struct Node *left, *right; /* assign uses right as value expr */
} Node;Окружение
Окружение сопоставляет имена со значениями. Для небольшого интерпретатора достаточно простого линейного массива пар «имя — значение».
В более крупных языках для поиска за O(1) и вложенных областей видимости используют хеш-таблицы.
#define MAX_VARS 64
typedef struct {
char names[MAX_VARS][32];
int values[MAX_VARS];
int count;
} Env;
static Env env; /* global for this tiny example */Присваивание переменной
При присваивании либо обновляется существующая ячейка, либо добавляется новая. Линейный поиск позволяет сделать код короче.
Возврат сохранённого значения позволяет использовать присваивания как выражения, например y = (x = 3).
#include <string.h>
static int env_set(const char *name, int v) {
for (int i = 0; i < env.count; i++)
if (strcmp(env.names[i], name) == 0) {
env.values[i] = v; return v;
}
strcpy(env.names[env.count], name);
env.values[env.count] = v;
env.count++;
return v;
}Чтение переменной
Поиск просматривает имена и возвращает значение найденного имени. Обращение к неопределённой переменной является ошибкой времени выполнения.
Проверка здесь позволяет вывести понятное сообщение вместо мусорного значения.
#include <stdio.h>
#include <stdlib.h>
static int env_get(const char *name) {
for (int i = 0; i < env.count; i++)
if (strcmp(env.names[i], name) == 0)
return env.values[i];
fprintf(stderr, "undefined variable: %s\n", name);
exit(1);
}Разбор присваивания
Присваивание имеет самый низкий приоритет и правую ассоциативность. Мы разбираем множитель; если это отдельный идентификатор, за которым следует =, то создаём узел assign.
В противном случае идентификатор — это просто чтение переменной.
static Node *parse_assign(void) {
if (cur().kind == TOK_IDENT) {
char saved[32]; strcpy(saved, cur().name);
bump();
if (cur().kind == TOK_ASSIGN) {
bump();
Node *val = parse_assign();
return assign_node(saved, val);
}
return var_node(saved); /* not an assignment */
}
return parse_expr();
}Вычисление переменных
Вычислитель получает два новых случая. Узел переменной ищет её имя в окружении, а узел присваивания вычисляет выражение значения и сохраняет результат.
Оба случая повторно используют вспомогательные функции env_get и env_set.
int eval(Node *n) {
switch (n->kind) {
case N_NUM: return n->value;
case N_VAR: return env_get(n->name);
case N_ASSIGN: return env_set(n->name, eval(n->right));
case N_BINOP: {
int l = eval(n->left), r = eval(n->right);
switch (n->op) {
case '+': return l + r; case '-': return l - r;
case '*': return l * r; case '/': return l / r;
}
}
}
return 0;
}Рабочий шаг REPL
Эта программа сохраняет переменную, а затем читает её значение в выражении — всё через окружение. Она моделирует одну строку настоящего интерпретатора.
Запустите программу, чтобы увидеть совместную работу присваивания и поиска.
#include <stdio.h>
#include <string.h>
#define MAX_VARS 64
static char names[MAX_VARS][32];
static int values[MAX_VARS];
static int count;
static int env_set(const char *n,int v){
for(int i=0;i<count;i++) if(!strcmp(names[i],n)){values[i]=v;return v;}
strcpy(names[count],n); values[count]=v; count++; return v;
}
static int env_get(const char *n){
for(int i=0;i<count;i++) if(!strcmp(names[i],n)) return values[i];
return 0;
}
int main(void){
env_set("x", 5); /* x = 5 */
int r = env_get("x") + 1; /* x + 1 */
printf("x = %d\n", env_get("x"));
printf("x + 1 = %d\n", r);
return 0;
}Области видимости и затенение
Одно плоское окружение является глобальным. В настоящих языках добавляют вложенные области видимости, чтобы локальные переменные функции не затрагивали внешние имена.
Это реализуется цепочкой окружений: поиск идёт от самой внутренней области видимости наружу.
typedef struct Env {
char names[MAX_VARS][32];
int values[MAX_VARS];
int count;
struct Env *parent; /* enclosing scope */
} Env;Куда двигаться дальше
Когда переменные готовы, можно добавить инструкции, условные конструкции и функции. Каждый из них — новый вид узла и соответствующий случай вычисления.
С этого момента та же цепочка «лексер — парсер — вычислитель» масштабируется до настоящего небольшого языка.
Быстрая проверка
Подумайте, за что отвечает окружение.
Итоги
Вы добавили идентификаторы в лексер, узлы var и assign в AST, окружение с операциями установки и получения значения, а также случаи вычисления для чтения и присваивания.
Теперь интерпретатор сохраняет состояние — это основа для инструкций и функций.
Часто задаваемые вопросы
Урок «Добавление переменных» бесплатный?
Да — полный текст урока «Добавление переменных» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс C Academy, подпишись на CoddyKit PRO. Курс C Academy содержит 4 уроков всего.
Чему я научусь в уроке «Добавление переменных»?
Сохраняйте значения и обращайтесь к ним. Ты практикуешь C Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать C Academy?
Предыдущий опыт не требуется. C Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 4 из 4.
Сколько времени занимает урок «Добавление переменных»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке C Academy?
Да. Каждый урок C Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Токенизация входных данных
- Разбор выражений
- Вычисление дерева
- Добавление переменных