Перейти к содержанию

Работа со строками в C: Полное руководство

ПO для чпу
1 1 9 1
  • В C строки — это просто массивы символов (char), заканчивающиеся нулевым байтом (\0😞

    char str[] = "Hello";
    // В памяти: H e l l o \0
    // Индексы: 0 1 2 3 4 5
    

    Нулевой байт (\0) — это терминатор, он сигнализирует окончание строки. Без него функции вроде printf() не будут знать, где остановиться, и напечатают мусор из памяти.

    Это ключевое отличие от других языков, где строки — это отдельный тип данных. В C это просто договоренность: строка = массив char + нулевой терминатор.


    Объявление строк

    Способ 1: Массив с инициализацией

    char str[] = "Hello";  // Автоматический размер (6 байт: H e l l o \0)
    

    Компилятор сам считает, сколько нужно места, и выделит 6 байт (5 символов + терминатор).

    Способ 2: Массив с явным размером

    char str[50] = "Hello";  // Массив размером 50 байт
    

    Первые 6 байт: H e l l o \0, остальные 44 — неинициализированы (мусор).

    Способ 3: Указатель на строковый литерал

    char *str = "Hello";  // Указатель на неизменяемую строку
    

    Эта строка хранится в read-only памяти (в сегменте кода). Вы можете читать её, но НЕ можете менять:

    char *str = "Hello";
    str[0] = 'J';  // КРАХ! Segmentation fault — попытка написать в read-only память
    

    Способ 4: Массив для изменяемой строки

    char str[] = "Hello";  // Копия в стеке, можно менять
    str[0] = 'J';  // OK! Теперь str = "Jello"
    

    Таблица различий:

    Объявление Размер Изменяемо? Хранилище
    char str[] = "..." Автоматический Да Стек
    char str[50] = "..." Фиксированный Да Стек
    char *str = "..." N/A Нет Readonly память
    char *str = malloc(...) Динамический Да Heap

    Основные функции для строк

    Все они находятся в <string.h>:

    #include <string.h>
    

    strlen() — длина строки

    Возвращает количество символов БЕЗ терминатора:

    #include <stdio.h>
    #include <string.h>
    
    int main() {
        char str[] = "Hello";
        size_t len = strlen(str);
        printf("Длина: %zu\n", len);  // 5
        return 0;
    }
    

    Важно: strlen() идёт по памяти, пока не найдёт \0. Если его нет, программа зависнет или упадёт.

    char str[5];  // Ошибка: нет терминатора!
    strlen(str);  // Неопределённое поведение
    

    strcpy() — копирование строки

    char src[] = "Hello";
    char dest[50];
    strcpy(dest, src);
    printf("%s\n", dest);  // Hello
    

    ОПАСНОСТЬ: strcpy() не проверяет размер буфера — это классическая уязвимость:

    char dest[5];           // 5 байт
    strcpy(dest, "Hello, World!");  // 13 символов!
    // БУФЕР ПЕРЕПОЛНЕН! Пишем за границы массива
    

    Исправление: используйте strncpy():

    char dest[50];
    strncpy(dest, src, 49);  // Максимум 49 символов
    dest[49] = '\0';          // Гарантируем терминатор
    

    Или ещё лучше — используйте snprintf():

    char dest[50];
    snprintf(dest, sizeof(dest), "%s", src);  // Безопасно и удобно
    

    strcat() — конкатенация (склеивание)

    char str1[50] = "Hello";
    char str2[] = " World";
    strcat(str1, str2);
    printf("%s\n", str1);  // Hello World
    

    Требование: str1 должен быть выделен достаточно большой, чтобы вместить результат.

    ОПАСНОСТЬ: как и strcpy(), strcat() не проверяет границы:

    char str1[6] = "Hello";  // Только 6 байт
    strcat(str1, " World");  // ПЕРЕПОЛНЕНИЕ
    

    Исправление: strncat():

    char str1[50] = "Hello";
    char str2[] = " World";
    strncat(str1, str2, 49 - strlen(str1) - 1);  // Добавить не более N символов
    

    strcmp() — сравнение строк

    #include <string.h>
    
    int main() {
        char str1[] = "Hello";
        char str2[] = "Hello";
        char str3[] = "World";
        
        printf("%d\n", strcmp(str1, str2));  // 0 (одинаковые)
        printf("%d\n", strcmp(str1, str3));  // < 0 (str1 < str3 в ASCII)
        printf("%d\n", strcmp(str3, str1));  // > 0 (str3 > str1 в ASCII)
        
        return 0;
    }
    

    Возвращаемые значения:

    • 0 — строки одинаковые
    • < 0 — первая строка лексикографически меньше
    • > 0 — первая строка лексикографически больше

    Почему не использовать ==?

    char *str1 = "Hello";
    char *str2 = "Hello";
    if (str1 == str2) { }  // НЕПРАВИЛЬНО! Сравнивает адреса, не значения
    if (strcmp(str1, str2) == 0) { }  // ПРАВИЛЬНО
    

    strchr() — поиск символа

    char str[] = "Hello World";
    char *pos = strchr(str, 'o');
    if (pos != NULL) {
        printf("Найден 'o' на позиции %ld\n", pos - str);  // 4
    }
    

    strchr() возвращает указатель на первое вхождение символа или NULL, если не найден.

    Вычисляем позицию как pos - str (арифметика указателей).

    strstr() — поиск подстроки

    char str[] = "Hello World";
    char *pos = strstr(str, "World");
    if (pos != NULL) {
        printf("Найдена подстрока на позиции %ld\n", pos - str);  // 6
    }
    

    Аналогично strchr(), но ищет не один символ, а всю подстроку.

    strdup() — дублирование строки

    char str[] = "Hello";
    char *copy = strdup(str);  // Динамическое выделение + копирование
    printf("%s\n", copy);
    free(copy);  // Не забыть!
    

    strdup() эквивалентен:

    char *copy = malloc(strlen(str) + 1);
    strcpy(copy, str);
    

    Важно: strdup() выделяет память, которую вы должны освободить.

    strtok() — разбор строки на токены

    Разделяет строку по разделителям:

    char str[] = "apple,banana,orange";
    char *token = strtok(str, ",");
    while (token != NULL) {
        printf("%s\n", token);
        token = strtok(NULL, ",");  // NULL = продолжить с предыдущей строки
    }
    

    Вывод:

    apple
    banana
    orange
    

    Внимание: strtok() модифицирует исходную строку (вставляет \0 на месте разделителей). Если вам нужна исходная строка, сделайте копию:

    char str[] = "apple,banana,orange";
    char *copy = strdup(str);
    char *token = strtok(copy, ",");
    // ... использование ...
    free(copy);
    

    Работа с динамическими строками

    Чтение строки от пользователя

    НЕПРАВИЛЬНО (уязвиво):

    char name[10];
    scanf("%s", name);  // Если пользователь введёт "Alexander", ПЕРЕПОЛНЕНИЕ!
    

    ПРАВИЛЬНО:

    char name[50];
    fgets(name, sizeof(name), stdin);  // Максимум 49 символов
    // Удалить символ новой строки
    if (name[strlen(name) - 1] == '\n') {
        name[strlen(name) - 1] = '\0';
    }
    

    ЛУЧШЕ (динамическое):

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    char *read_string(void) {
        char buffer[256];
        if (fgets(buffer, sizeof(buffer), stdin) == NULL) {
            return NULL;
        }
        
        // Удалить \n
        buffer[strcspn(buffer, "\n")] = 0;
        
        // Скопировать в динамическую память
        return strdup(buffer);
    }
    
    int main() {
        printf("Введите имя: ");
        char *name = read_string();
        
        if (name != NULL) {
            printf("Привет, %s!\n", name);
            free(name);
        }
        return 0;
    }
    

    Конкатенация без переполнения

    #include <stdio.h>
    #include <string.h>
    
    int main() {
        char result[100] = "";
        
        snprintf(result, sizeof(result), "%s %s %d",
                 "Hello", "World", 2025);
        
        printf("%s\n", result);  // Hello World 2025
        return 0;
    }
    

    snprintf() — это самый безопасный способ форматирования и конкатенации строк.


    Строки и указатели

    Массив строк

    char *names[] = {
        "Alice",
        "Bob",
        "Charlie"
    };
    
    for (int i = 0; i < 3; i++) {
        printf("%s\n", names[i]);
    }
    

    Это массив указателей, каждый указывает на строковый литерал.

    Нельзя менять эти строки, так как они в read-only памяти:

    names[0][0] = 'X';  // КРАХ
    

    Если нужно менять: используйте массив массивов:

    char names[][20] = {
        "Alice",
        "Bob",
        "Charlie"
    };
    
    names[0][0] = 'X';  // OK, теперь names[0] = "Xlice"
    

    Обход строки по указателю

    char str[] = "Hello";
    char *ptr = str;
    
    while (*ptr != '\0') {
        printf("%c ", *ptr);
        ptr++;
    }
    // Вывод: H e l l o
    

    Это эквивалентно:

    for (char *ptr = str; *ptr; ptr++) {
        printf("%c ", *ptr);
    }
    

    Преобразования типов

    atoi() — строка в целое число

    char str[] = "123";
    int num = atoi(str);
    printf("%d\n", num);  // 123
    

    atof() — строка в float

    char str[] = "3.14";
    double num = atof(str);
    printf("%.2f\n", num);  // 3.14
    

    strtol() и strtof() — с проверкой ошибок

    #include <stdlib.h>
    
    char str[] = "123abc";
    char *endptr;
    long num = strtol(str, &endptr, 10);
    
    printf("Число: %ld\n", num);        // 123
    printf("Остаток: %s\n", endptr);    // abc
    

    strtol() правильнее, так как возвращает информацию об ошибке.


    Практический пример: парсер CSV

    #include <stdio.h>
    #include <string.h>
    #include <stdlib.h>
    
    #define MAX_LINE 1000
    #define MAX_FIELDS 10
    
    int parse_csv_line(char *line, char *fields[], int max_fields) {
        int count = 0;
        char *copy = strdup(line);
        char *token = strtok(copy, ",");
        
        while (token != NULL && count < max_fields) {
            // Удалить пробелы
            while (*token == ' ') token++;
            fields[count++] = strdup(token);
            token = strtok(NULL, ",");
        }
        
        free(copy);
        return count;
    }
    
    int main() {
        char line[] = "Alice, 25, New York, Engineer";
        char *fields[MAX_FIELDS];
        
        int count = parse_csv_line(line, fields, MAX_FIELDS);
        
        for (int i = 0; i < count; i++) {
            printf("Field %d: [%s]\n", i, fields[i]);
            free(fields[i]);
        }
        
        return 0;
    }
    

    Вывод:

    Field 0: [Alice]
    Field 1: [25]
    Field 2: [New York]
    Field 3: [Engineer]
    

    Частые ошибки

    Ошибка 1: забыть место для терминатора

    char str[5] = "Hello";  // Нужно 6 байт (5 символов + \0), выделили 5
    // БУФЕРНОЕ ПЕРЕПОЛНЕНИЕ
    

    Исправление:

    char str[6] = "Hello";  // Правильно
    

    Ошибка 2: использовать strcpy без проверки размера

    char dest[10];
    char src[] = "This is a very long string";
    strcpy(dest, src);  // ПЕРЕПОЛНЕНИЕ
    

    Исправление:

    strncpy(dest, src, sizeof(dest) - 1);
    dest[sizeof(dest) - 1] = '\0';
    

    Ошибка 3: сравнивать строки с ==

    char *str1 = "Hello";
    char *str2 = "Hello";
    if (str1 == str2) { }  // Может быть неправильным!
    if (strcmp(str1, str2) == 0) { }  // Правильно
    

    Ошибка 4: забыть free для strdup

    char *str = strdup("Hello");
    // Использование
    free(str);  // ОБЯЗАТЕЛЬНО!
    

    Ошибка 5: стоковый буфер для возврата

    char *bad_function() {
        char str[] = "Hello";  // Локальная переменная
        return str;  // Возвращаем адрес стека — НЕПРАВИЛЬНО
    }
    
    int main() {
        char *str = bad_function();
        printf("%s\n", str);  // Мусор или крах
    }
    

    Исправление:

    char *good_function() {
        char *str = malloc(50);
        strcpy(str, "Hello");
        return str;  // Вызывающий должен free
    }
    

    Лучшие практики

    1. Всегда проверяйте размер буфера:

      strncpy(dest, src, sizeof(dest) - 1);
      dest[sizeof(dest) - 1] = '\0';
      
    2. Используйте snprintf() вместо sprintf():

      snprintf(buffer, sizeof(buffer), "%s: %d", name, age);
      
    3. Используйте fgets() вместо gets() или scanf("%s"):

      fgets(buffer, sizeof(buffer), stdin);
      
    4. Проверяйте возвращаемые значения:

      char *token = strtok(str, " ");
      if (token != NULL) { }
      
    5. Освобождайте динамические строки:

      char *str = strdup("Hello");
      free(str);
      str = NULL;
      
    6. Документируйте правила памяти:

      // Возвращает выделенную динамически строку (вызывающий должен free)
      char *create_greeting(const char *name) { }
      
    7. Для сложных манипуляций используйте вспомогательные функции:

      // Вместо прямого strtok, инкапсулируйте логику
      char **split_string(const char *str, const char *delim, int *count) { }
      

    Заключение

    Работа со строками в C требует дисциплины и внимания к деталям. Главное правило: всегда знайте размер вашего буфера и проверяйте границы.

    Основные инструменты:

    • strlen() — длина
    • strcpy() / strncpy() — копирование (используйте strncpy())
    • strcat() / strncat() — конкатенация (используйте strncat())
    • strcmp() — сравнение
    • strchr() / strstr() — поиск
    • strtok() — разбор на токены
    • snprintf() — безопасное форматирование

    Овладев этими функциями и избегая ошибок, вы сможете писать надёжный код без утечек памяти и переполнений буферов.

  • Работа с файлами в C: от открытия до закрытия

    ПO для чпу
    1
    0 Голоса
    1 Сообщения
    8 Просмотры
    Нет ответов
  • Указатели в C и работа с памятью: Полное руководство

    ПO для чпу
    1
    1 Голоса
    1 Сообщения
    13 Просмотры
    Нет ответов
  • Программирование на C для станков с ЧПУ и промышленного оборудования

    ПO для чпу
    6
    0 Голоса
    6 Сообщения
    34 Просмотры
    kirilljsxK
    Практические советы для начинающих Начинайте с малого Не пытайтесь сразу написать сложную систему управления. Сначала освойте базовые конструкции языка: переменные, циклы, условия, функции. Каждая новая программа должна решать одну небольшую задачу. Изучайте чужой код Открытые проекты на GitHub — отличный источник для обучения. Например, проект GCodeWorkShop показывает, как создаются редакторы программ для станков с ЧПУ. ​ Практикуйтесь регулярно Программирование требует постоянной практики. Пишите код каждый день, даже если это простые упражнения. Решайте задачи на специализированных платформах, пробуйте модифицировать существующие примеры. ​ Объединяйте теорию с практикой Если есть доступ к учебному станку или микроконтроллеру, используйте его для практических экспериментов. Реальное железо дает понимание того, как код превращается в физические действия машин. ​ Общайтесь с сообществом Форумы, Telegram-группы и специализированные ресурсы помогут быстрее разобраться в сложных вопросах. Не стесняйтесь задавать вопросы — сообщество программистов обычно готово помогать начинающим. ​ Дальнейшее развитие После освоения базового C стоит изучить: C++ для объектно-ориентированного программирования и работы с библиотеками MFC, Qt Python для быстрого прототипирования и автоматизации задач Assembler для максимально низкоуровневой работы с процессором Стандарты промышленной автоматизации (IEC 61131-3, OPC UA) Архитектуру микроконтроллеров (ARM Cortex, AVR, PIC) Путь программиста в промышленности требует времени и усилий, но результат того стоит. Вы получаете полный контроль над оборудованием, можете решать уникальные задачи автоматизации и создавать собственные инструменты для производства. ​ Начните с малого, двигайтесь пошагово, и уже через несколько месяцев сможете писать программы для реальных промышленных систем. Удачи в освоении C!
  • МЭК 61131-3: как программировать ПЛК по стандарту

    ПO для чпу мэк
    1
    2
    0 Голоса
    1 Сообщения
    13 Просмотры
    Нет ответов
  • EtherCAT и Modbus TCP: в чём разница и как выбрать для автоматизации

    ПO для чпу
    1
    1
    1 Голоса
    1 Сообщения
    8 Просмотры
    Нет ответов
  • Бесплатные альтернативы дорогому CAM-ПО: FreeCAD, Carbide Create, Estlcam

    ПO для чпу чпу cnc
    1
    0 Голоса
    1 Сообщения
    11 Просмотры
    Нет ответов
  • Как сделать еврозапил на чпу станке

    ПO для чпу
    1
    3 Голоса
    1 Сообщения
    211 Просмотры
    Нет ответов
  • Использование симуляторов для оптимизации процессов металлообработки

    ПO для чпу
    1
    1 Голоса
    1 Сообщения
    132 Просмотры
    Нет ответов