0Pricing
C Academy · Урок

Добавление переменных

Сохраняйте значения и обращайтесь к ним.

«Добавление переменных» — бесплатный урок 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 — локальная установка не требуется.

Все уроки этого курса

  1. Токенизация входных данных
  2. Разбор выражений
  3. Вычисление дерева
  4. Добавление переменных
← Назад к C Academy