7. Глава 7: Массивы и Строки — Работа с Коллекциями Данных и Текстом

Массивы и строки — это фундаментальные конструкции языка C, которые позволяют эффективно работать с упорядоченными наборами данных и текстом. Понимание того, как они функционируют на низком уровне, является ключом к эффективному программированию на C.

В этой главе мы подробно рассмотрим:

  • Одномерные массивы: Как хранить коллекции однотипных данных.

  • Строки (`char[]`): Особенность строк в C как массивов символов, завершающихся нулевым байтом.

  • Основные функции для работы со строками из библиотеки <string.h>.

  • Важные советы по безопасному и эффективному использованию массивов и строк.

7.1. Одномерные массивы

Массив — это коллекция элементов одного и того же типа, хранящихся в непрерывной области памяти. Каждый элемент массива доступен по своему индексу.

Объявление массива:

При объявлении массива необходимо указать тип его элементов и желаемый размер (количество элементов).

int numbers[5];   // Объявляет массив 'numbers', который может хранить 5 целых чисел.
char letters[10]; // Объявляет массив 'letters', который может хранить 10 символов.

Важно: Элементы массива индексируются от `0` до `n - 1`, где n — это размер массива. То есть, первый элемент находится по индексу 0, а последний — по индексу n-1.

Инициализация массива:

Массивы можно инициализировать при их объявлении, присваивая значения его элементам.

// Инициализация массива 'a' с явным указанием размера и всех элементов.
int a[3] = {1, 2, 3};
// Инициализация массива 'vowels' символьными литералами.
char vowels[5] = {'a', 'e', 'i', 'o', 'u'};

Если вы не указываете размер массива при инициализации, компилятор автоматически определяет его по количеству предоставленных элементов:

// Размер массива 'nums' будет автоматически установлен в 3, так как предоставлено 3 элемента.
int nums[] = {10, 20, 30};

### Пример 7.1: Вывод Элементов Массива

Этот пример демонстрирует, как объявить, инициализировать и затем итерировать по элементам массива, выводя их значения.

// array_example.c
#include <stdio.h> // Подключаем стандартную библиотеку ввода-вывода для функции printf.

int main() {
    // Объявление и инициализация одномерного целочисленного массива 'data'.
    // Массив имеет размер 5 элементов.
    // Элементы инициализируются значениями {10, 20, 30, 40, 50}.
    // Память под эти 5 целых чисел выделяется непрерывно.
    int data[5] = {10, 20, 30, 40, 50};

    // Объявляем переменную-счетчик 'i' для использования в цикле.
    int i;

    printf("Элементы массива:\n"); // Выводим заголовок. \n переносит на новую строку.

    // Цикл for для итерации по элементам массива.
    // Массивы в C индексируются с 0.
    // 'i' начинается с 0, идет до 4 (пока i < 5).
    for (i = 0; i < 5; i++) {
        // Выводим индекс элемента (data[i]) и его значение (data[i]).
        // %d - спецификатор формата для вывода целых чисел.
        // data[i] - обращение к элементу массива по индексу 'i'.
        printf("data[%d] = %d\n", i, data[i]);
    }

    // Возвращаем 0, чтобы сигнализировать операционной системе,
    // что программа завершилась успешно.
    return 0;
}

Вывод при компиляции и запуске array_example.c:

Элементы массива:
data[0] = 10
data[1] = 20
data[2] = 30
data[3] = 40
data[4] = 50

7.2. Строки в стиле C (char[])

В языке C строка — это особый вид массива символов (`char[]`), который обязательно завершается специальным нулевым символом (0). Этот нулевой символ служит маркером конца строки, позволяя функциям, работающим со строками, знать, где заканчивается текст.

Пример объявления и инициализации строки:

При использовании строкового литерала (текста в двойных кавычках) компилятор автоматически добавляет 0 в конец массива.

char greeting[] = "Привет"; // Этот массив будет иметь размер 7 символов:
                            // 'П', 'р', 'и', 'в', 'е', 'т', '\0'

Важно: Ошибка инициализации без `0`:

Если вы инициализируете char массив вручную символами, вы должны явно добавить 0, иначе это не будет считаться корректной строкой в C.

char word[5] = {'H', 'e', 'l', 'l', 'o'}; // НЕ является строкой в C, так как отсутствует '\0'.
                                        // Попытка использовать ее со строковыми функциями
                                        // приведет к неопределенному поведению.

Правильный ввод строк с клавиатуры:

Функция scanf(«%s», …) опасна для чтения строк, так как она останавливается на первом пробеле и не проверяет размер буфера, что может привести к переполнению буфера.

char name[20];
printf("Введите ваше имя: ");
scanf("%s", name); // ОПАСНО! Останавливается на пробеле и не проверяет границы массива.
printf("Привет, %s!\n", name);

Для безопасного ввода строк с пробелами используйте `fgets()`:

fgets() является более безопасной функцией, так как она позволяет указать максимальное количество символов для чтения, предотвращая переполнение буфера.

// fgets(буфер, размер_буфера, источник)
fgets(name, sizeof(name), stdin);
// Важно: fgets() читает символ новой строки '\n' (если он есть) и включает его в буфер.
// Его часто нужно удалять вручную, как показано в Примере 7.2.

7.3. Работа со строками (<string.h>)

Для эффективной работы со строками в C используется стандартная библиотека <string.h>. Чтобы использовать её функции, необходимо подключить заголовочный файл:

#include <string.h>

Часто используемые функции из <string.h>:

Sure thing! My apologies for the oversight. Here’s that updated table in reStructuredText (RST) format, with the improved descriptions for strncpy and strncat as discussed:

Функции для работы со строками в C (<string.h>)

Функция

Описание

strlen(const char *s)

Возвращает длину строки s (количество символов без завершающего нулевого символа \0).

strcpy(char *dest, const char *src)

Копирует строку src (включая \0) в dest. Внимание: dest должен быть достаточно большим, чтобы вместить всю строку src, иначе возможно переполнение буфера. Используйте strncpy для безопасной копии.

strncpy(char *dest, const char *src, size_t n)

Безопасная копия: Копирует не более n символов из src в dest. Если src короче n, dest дополняется нулями. Важно: Если src длиннее или равен n, strncpy не гарантирует добавление завершающего ``0``. Часто нужно добавлять его вручную: dest[n-1] = '\0';.

strcmp(const char *s1, const char *s2)

Сравнивает строки s1 и s2 лексикографически (по символам). Возвращает 0, если строки идентичны. Возвращает отрицательное значение, если s1 «меньше» s2; положительное, если s1 «больше» s2.

strcat(char *dest, const char *src)

Конкатенирует (добавляет) строку src (включая \0) в конец строки dest. Внимание: dest должен иметь достаточно места для размещения обеих строк, иначе возможно переполнение буфера. Используйте strncat для безопасной конкатенации.

strncat(char *dest, const char *src, size_t n)

Безопасная конкатенация: Добавляет не более n символов из src в конец dest. После добавления символов и \0 (если n не превышает длину src), dest всегда будет корректно завершен \0.

strcspn(const char *s, const char *reject)

Возвращает длину начального сегмента строки s, который не содержит ни одного символа из строки reject. Очень часто используется для удаления символа новой строки (``n``) от fgets(): string[strcspn(string, "\\n")] = '\\0';.

Let me know if you need anything else!

### Пример 7.2: Расширенная Работа со Строками

Этот пример демонстрирует чтение строк с помощью fgets() с последующей очисткой от n, а также использование strlen(), strcpy(), strcmp() и strcat().

// string_manipulation.c
#include <stdio.h>  // Для функций ввода-вывода, таких как printf и fgets.
#include <string.h> // Для строковых функций, таких как strlen и strcspn.

int main() {
    // Объявляем символьный массив 'name' размером 50 символов.
    // Этого достаточно для хранения строки до 49 символов плюс завершающий нулевой символ '\0'.
    char name[50];

    printf("Введите ваше имя: "); // Запрос ввода у пользователя.

    // fgets() - безопасная функция для чтения строки.
    // Она считывает строку из стандартного ввода (stdin)
    // до символа новой строки '\n' или до (sizeof(name) - 1) символов,
    // записывая их в массив 'name'.
    // Важно: fgets() включает символ '\n' (если он был введен) в строку.
    fgets(name, sizeof(name), stdin);

    // Удаляем символ новой строки ('\n'), который мог быть прочитан fgets().
    // strcspn(name, "\n") находит индекс первого вхождения символа '\n' в строке 'name'.
    // Если '\n' найден, мы заменяем его на нулевой символ '\0',
    // что корректно завершает строку в этом месте.
    // Это критически важно для корректной работы других строковых функций и вывода.
    name[strcspn(name, "\n")] = 0;

    // Выводим приветствие с введенным именем.
    // %s - спецификатор формата для вывода строки.
    printf("Привет, %s!\n", name);

    // strlen(name) - функция из <string.h>, которая возвращает длину строки,
    // исключая завершающий нулевой символ '\0'.
    // %lu - спецификатор формата для вывода значения типа size_t,
    // который возвращает strlen.
    printf("Длина строки: %lu\n", strlen(name));

    // --- Демонстрация других строковых функций (дополнительно) ---

    // strcpy - копирование строки.
    char copied_name[50];
    // strcpy(назначение, источник)
    // Копирует содержимое 'name' в 'copied_name', включая '\0'.
    strcpy(copied_name, name);
    printf("Скопированное имя: %s\n", copied_name);

    // strcmp - сравнение строк.
    char another_name[50];
    strcpy(another_name, "Alice"); // Задаем другую строку для сравнения

    // strcmp возвращает 0, если строки идентичны.
    // Отрицательное значение, если s1 < s2.
    // Положительное значение, если s1 > s2.
    if (strcmp(name, another_name) == 0) {
        printf("Имена одинаковые.\n");
    } else {
        printf("Имена разные.\n");
    }

    // strcat - конкатенация (объединение) строк.
    char full_message[100];
    strcpy(full_message, "Hello, "); // Инициализируем начальную часть
    // strcat(назначение, источник)
    // Добавляет содержимое 'name' в конец 'full_message'.
    strcat(full_message, name);
    strcat(full_message, "!"); // Добавляем восклицательный знак
    printf("Полное сообщение: %s\n", full_message);

    // Возвращаем 0 для успешного завершения.
    return 0;
}

7.4. Строковые литералы и указатели

В C можно объявлять строковые литералы, используя указатели:

char *msg = "Hello, world!";

Важно: Строковый литерал (текст в двойных кавычках) часто хранится в памяти, предназначенной только для чтения. Присвоение его указателю (char *) означает, что msg указывает на эту область памяти.

Попытка изменить содержимое такой строки через указатель (`msg[0] = „X“;`) приведет к неопределенному поведению или, что чаще, к краху программы (segmentation fault), так как вы пытаетесь записать в область памяти, защищенную от записи. Если вам нужна изменяемая строка, всегда используйте объявление массива char[].

7.5. Практический Пример: Подсчёт гласных

Этот пример демонстрирует более сложную работу со строками: итерацию по символам и условную логику для анализа содержимого.

### Пример 7.3: Подсчитать количество гласных в строке

// vowel_counter.c
#include <stdio.h>  // Для функций ввода-вывода (printf, fgets).
#include <string.h> // Для строковых функций (strcspn).
#include <ctype.h>  // Для функции tolower() - для упрощения проверки регистра.

int main() {
    // Объявляем символьный массив 'str' для хранения введенной строки.
    char str[100];
    // Объявляем целочисленную переменную для подсчета гласных, инициализируем нулем.
    int count = 0;

    printf("Введите строку: "); // Запрос ввода у пользователя.

    // Безопасное чтение строки с использованием fgets().
    fgets(str, sizeof(str), stdin);

    // Удаляем символ новой строки '\n', если он был прочитан fgets().
    str[strcspn(str, "\n")] = 0;

    // Цикл for для итерации по каждому символу строки.
    // Условие str[i] != '\0' гарантирует, что цикл продолжается до конца строки.
    for (int i = 0; str[i] != '\0'; i++) {
        // Получаем текущий символ строки.
        char c = str[i];

        // Преобразование символа в нижний регистр для упрощения проверки.
        // tolower() из <ctype.h> преобразует букву в нижний регистр;
        // если это не буква, символ возвращается без изменений.
        c = tolower(c);

        // Проверяем, является ли текущий символ гласной (a, e, i, o, u).
        if (c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u') {
            count++; // Если символ является гласной, увеличиваем счетчик.
        }
    }

    // Выводим общее количество найденных гласных.
    printf("Количество гласных: %d\n", count);

    // Возвращаем 0 для успешного завершения программы.
    return 0;
}

7.6. Полезные советы и распространённые ошибки

  • Индексирование с 0: Всегда помните, что массивы в C начинаются с индекса 0.

  • Завершающий `0`: Убедитесь, что все ваши строки в C корректно завершаются нулевым символом (0). Без него строковые функции будут читать за пределами выделенной памяти, что приведёт к ошибкам.

  • Используйте `fgets()` вместо `scanf(«%s», …)`: fgets() гораздо безопаснее, так как позволяет указать размер буфера и предотвращает переполнение.

  • Избегайте выхода за границы массива: Попытка доступа к элементу массива по индексу, который находится за его объявленными границами (например, data[5] в массиве data[5]), приведёт к неопределенному поведению (Undefined Behavior), что может выражаться в крахе программы, некорректных данных или уязвимостях безопасности. Это одна из самых частых и сложных для отладки ошибок в C.

  • Помните о `const` для строковых литералов: Если вы инициализируете char * строковым литералом («Hello»), помните, что это данные только для чтения.

7.7. Компиляция и Запуск Примеров

Для компиляции и запуска этих программ вам понадобится компилятор C (например, GCC).

В терминале, находясь в директории с исходными файлами:

# Компиляция Примера 7.1
gcc array_example.c -o array_example

# Компиляция Примера 7.2
gcc string_manipulation.c -o string_manipulation

# Компиляция Примера 7.3
gcc vowel_counter.c -o vowel_counter

Запуск исполняемых файлов:

./array_example
./string_manipulation  # Потребуется ввод имени
./vowel_counter        # Потребуется ввод строки

7.8. Дополнительные Ресурсы

  • man strlen, man strcpy, man strcmp, man strcat — Используйте эти команды в терминале Linux/macOS для получения подробной справки по функциям из <string.h>.

  • man fgets, man scanf — Справка по функциям ввода.

  • man tolower — Справка по функции из <ctype.h>.

  • C Arrays (W3Schools)

  • C Strings (TutorialsPoint)

  • C String Functions (GeeksforGeeks)