Раздел 10 — Массивы и строки в функциях

Как отдать функции массив, матрицу или строку — и почему для этого нужны указатели

Прогресс курса Раздел 10 из 20

Что вы освоите в этом разделе

2 академических часа теории и 4 часа практики: практическая работа №14 «Обработка массивов с использованием функций» — на портале. В ней вы перепишете свою практическую работу №10 на функциях. Все программы страницы — полные; собирайте их командой g++ -std=c++17 -Wall -Wextra файл.cpp -o файл.exe.
01

Массив-параметр — это адрес

В разделе 8 массив обрабатывался в main: передать его в функцию было нечем. Теперь известно главное: имя массива — адрес первого элемента (раздел 9, пункт 06). Когда массив передают в функцию, копируется только адрес, а не элементы. Параметр int a[] — другая запись для int* a, компилятор их не различает.

// Массив в функции: передаётся адрес первого элемента, поэтому функция может изменить массив
#include <iostream>

constexpr int N = 10;

void printArray(const int a[], int n);
int sum(const int a[], int n);
int maxIndex(const int a[], int n);
void scale(int a[], int n, int factor);

int main()
{
    int a[N] = {3, -1, 4, 1, 5};
    int n = 5;                                  // заполнено: a[0] .. a[n - 1]
    printArray(a, n);
    std::cout << "sum " << sum(a, n) << ", max at " << maxIndex(a, n) << '\n';
    std::cout << "sum of a[2..4] " << sum(a + 2, 3) << '\n';   // часть массива: адрес начала и длина
    scale(a, n, 10);
    printArray(a, n);
    std::cout << "sizeof(a) in main: " << sizeof(a) << '\n';
    return 0;
}

// Выводит n элементов через пробел
void printArray(const int a[], int n)
{
    for (int i = 0; i < n; ++i) {
        std::cout << a[i] << ' ';
    }
    std::cout << '\n';
}

// Сумма n элементов
int sum(const int a[], int n)
{
    int total = 0;
    for (int i = 0; i < n; ++i) {
        total += a[i];
    }
    return total;
}

// Индекс первого наибольшего; n >= 1
int maxIndex(const int a[], int n)
{
    int best = 0;
    for (int i = 1; i < n; ++i) {
        if (a[i] > a[best]) {
            best = i;
        }
    }
    return best;
}

// Умножает каждый элемент на factor — меняется массив вызывающего
void scale(int a[], int n, int factor)
{
    for (int i = 0; i < n; ++i) {
        a[i] *= factor;
    }
}
3 -1 4 1 5
sum 12, max at 4
sum of a[2..4] 10
30 -10 40 10 50
sizeof(a) in main: 40
02

const и роли параметров

По заголовку функции должно быть видно, что она сделает с массивом. Для этого — const (раздел 9, пункт 05):

ЗаголовокЧто делает с массивомПример
f(const int a[], int n)только читаетsum, maxIndex, printArray, поиск
f(int a[], int n)изменяет элементы, n то жеscale, сортировка, разворот
int f(int a[], int n)изменяет и возвращает новое число элементовудаление: n = removeAll(a, n, x);
bool f(int a[], int& n)заполняет массив и число элементов; false — не удалосьввод массива
void f(const int a[], int n, int result[])читает один массив, пишет результат в другойпрефиксные суммы, слияние

Если функция с const int a[] попытается изменить элемент, программа не соберётся: assignment of read-only location. Это защита от случайной записи: const в заголовке — обещание, которое проверяет компилятор. Правило курса: массив, который функция не меняет, передаётся как const.

Вернуть массив из функции через return нельзя. return a; вернул бы адрес — а если это локальный массив функции, то «висячий» адрес (раздел 9, пункт 03). Результат-массив функция пишет в массив, который ей передал вызывающий: так устроены rowSums в пункте 03 и prefixSums в практической работе.

03

Матрица в функции

Для одномерного массива функции хватает адреса начала. Для матрицы — нет: чтобы найти m[i][j], нужно отступить i строк, а для этого надо знать длину строки. Поэтому число столбцов — часть типа параметра, а число строк передаётся отдельно:

// Матрица в функции: число столбцов — в типе параметра, число строк — отдельным параметром
#include <iostream>
#include <iomanip>

constexpr int COLS = 3;

void printMatrix(const int m[][COLS], int rows);
int diagonalSum(const int m[][COLS], int n);
void rowSums(const int m[][COLS], int rows, int sums[]);

int main()
{
    int m[3][COLS] = {
        {1, 2, 3},
        {4, 5, 6},
        {7, 8, 9}
    };
    printMatrix(m, 3);
    std::cout << "diagonal " << diagonalSum(m, 3) << '\n';
    int sums[3];
    rowSums(m, 3, sums);
    std::cout << "row sums: " << sums[0] << ' ' << sums[1] << ' ' << sums[2] << '\n';
    return 0;
}

// Выводит rows строк матрицы, каждое число в поле ширины 4
void printMatrix(const int m[][COLS], int rows)
{
    for (int i = 0; i < rows; ++i) {
        for (int j = 0; j < COLS; ++j) {
            std::cout << std::setw(4) << m[i][j];
        }
        std::cout << '\n';
    }
}

// Сумма главной диагонали квадратной матрицы n × n, n <= COLS
int diagonalSum(const int m[][COLS], int n)
{
    int total = 0;
    for (int i = 0; i < n; ++i) {
        total += m[i][i];
    }
    return total;
}

// sums[i] — сумма строки i; результат — в массив вызывающего
void rowSums(const int m[][COLS], int rows, int sums[])
{
    for (int i = 0; i < rows; ++i) {
        sums[i] = 0;
        for (int j = 0; j < COLS; ++j) {
            sums[i] += m[i][j];
        }
    }
}
   1   2   3
   4   5   6
   7   8   9
diagonal 15
row sums: 6 15 24

Запись int m[][] не собирается: declaration of 'm' as multidimensional array must have bounds for all dimensions except the first. Второй размер в параметре должен совпадать с объявленным у матрицы — поэтому он задаётся одной константой COLS для всей программы. Фактическое число столбцов меньше COLS передают ещё одним параметром cols, как n у одномерного массива. Одна строка матрицы — одномерный массив: printArray(m[i], cols).

04

Программа из функций над массивом

Теперь длинный main из практических работ №10–12 разбирается на части, как программа из раздела 7. Типовой набор:

ФункцияЗаголовок
ввод массива с проверкойbool readArray(const char sizeName[], const char itemsName[], int low, int high, int a[], int& n) — приглашения, n от 1 до N и n элементов через readNumber
выводvoid printArray(const char title[], const int a[], int n)
поискint find(const int a[], int n, int x) — индекс или −1
условиеbool isSorted(const int a[], int n)
изменениеvoid reverseRange(int a[], int first, int last), int removeAll(int a[], int n, int x)

Функция, которая считает, не печатает — это правило из раздела 7 остаётся. main после разбиения — ввод, вызовы и вывод результатов, без вложенных циклов. Функции-«кирпичики» переходят из программы в программу без изменений: readArray и printArray из практической работы №14 пригодятся до конца курса.

Функцию можно вызвать для части того же массива. contains(a, i, a[i]) — «есть ли a[i] среди элементов левее» — это поиск в начале того же массива длиной i. Такие вызовы заменяют вложенные циклы и делают код короче.
05

Строка C — массив char с нулём в конце

Текст в C и в Windows API хранится в массиве char: по символу в элементе, а за последним символом — нулевой символ '\0' (код 0). Он — единственный признак конца строки: длина нигде не записана, её находят, считая символы до нуля. Строковый литерал "hello" занимает 6 байт — пять букв и ноль.

// Строка C — массив char с нулевым символом '\0' в конце
#include <iostream>

int length(const char s[]);

int main()
{
    char word[] = "hello";                      // 6 байт: h e l l o \0
    char name[20] = "Ann";                      // 20 байт, заняты 4
    std::cout << word << ' ' << sizeof(word) << ' ' << length(word) << '\n';
    std::cout << name << ' ' << sizeof(name) << ' ' << length(name) << '\n';
    word[0] = 'j';
    std::cout << word << '\n';
    word[2] = '\0';                             // строка кончается на первом '\0'
    std::cout << word << ' ' << length(word) << '\n';
    for (int i = 0; name[i] != '\0'; ++i) {
        std::cout << name[i] << '-';
    }
    std::cout << '\n';
    return 0;
}

// Длина строки — число символов до '\0'
int length(const char s[])
{
    int n = 0;
    while (s[n] != '\0') {
        ++n;
    }
    return n;
}
hello 6 5
Ann 20 3
jello
je 2
A-n-n-
06

Функции над строками

Строки, как и массивы, нельзя присвоить и сравнить целиком: s = "abc" не собирается, а answer == "yes" сравнивает адреса и почти всегда ложно — g++ предупреждает: comparison with string literal results in unspecified behavior. Копирование и сравнение — функциями:

// Копирует src в dest вместе с '\0'; false — не помещается в size символов (dest не трогается)
bool copyString(char dest[], int size, const char src[])
{
    const int n = length(src);
    if (n + 1 > size) {
        return false;
    }
    for (int i = 0; i <= n; ++i) {                  // <= n — копируется и '\0'
        dest[i] = src[i];
    }
    return true;
}

// Совпадают ли строки посимвольно
bool equal(const char a[], const char b[])
{
    int i = 0;
    while (a[i] != '\0' && a[i] == b[i]) {
        ++i;
    }
    return a[i] == b[i];                          // обе кончились одновременно — равны
}

Функция, которая пишет в строку, получает размер массива-приёмника — иначе она не сможет проверить, помещается ли результат. У функции, которая только читает строку, размера нет: конец виден по нулю.

Готовые функции — в заголовке <cstring>: strlen — длина, strcpy — копирование, strcmp — сравнение (0 — равны), strcat — дописать. strcpy и strcat размер приёмника не проверяют: переполнение буфера в них — классическая уязвимость (раздел 8, пункт 04). В практической работе №14 строковые функции пишутся своими циклами — чтобы было понятно, как устроены готовые.

ЗадачаПриём
символ — цифра?c >= '0' && c <= '9'; значение цифры — c - '0'
строчная латинская буква → заглавнаяstatic_cast<char>(c - 'a' + 'A') при c от 'a' до 'z'
номер буквы в алфавитеc - 'a' — от 0 до 25; счётчики букв — массив int count[26]
удаление символовдва индекса, как в разделе 8, и s[kept] = '\0' в конце — новая строка короче
разворотобмены с двух концов до середины; '\0' остаётся на месте
07

Ввод строк

// Ввод строки: >> читает слово до пробела, getline — всю строку; оба — не больше размера массива
#include <iostream>
#include <iomanip>
#include <limits>

constexpr int SIZE = 16;

int main()
{
    char word[SIZE];
    char line[SIZE];
    std::cout << "Name and surname: ";
    std::cin >> std::setw(SIZE) >> word;        // не больше SIZE - 1 символов и '\0'
    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
    std::cout << "Address: ";
    std::cin.getline(line, SIZE);
    if (std::cin.fail()) {
        std::cout << "Error: address is longer than " << SIZE - 1 << " characters\n";
        return 1;
    }
    std::cout << "First word: " << word << '\n';
    std::cout << "Address: " << line << '\n';
    return 0;
}
Name and surname: Ivan Petrov
Address: Tula, Lenina 5
First word: Ivan
Address: Tula, Lenina 5

Name and surname: Ivan Petrov
Address: Saint Petersburg, Nevsky 1
Error: address is longer than 15 characters
08

std::string в минимуме

В стандартной библиотеке C++ есть класс std::string (заголовок <string>): строка, которая сама хранит свою длину и сама увеличивает память. С ней нет ни нулевого символа, ни переполнения:

// std::string: длину и память строка хранит сама, склейка — через +, сравнение — через ==
#include <iostream>
#include <string>

int main()
{
    std::string name = "Ann";
    std::string greeting = "Hello, " + name + '!';
    std::cout << greeting << ' ' << greeting.size() << '\n';
    greeting[0] = 'J';
    std::cout << greeting << '\n';
    std::cout << (name == "Ann") << ' ' << (name < "Bob") << '\n';

    std::string line;
    std::cout << "Line: ";
    std::getline(std::cin, line);               // любая длина — строка растёт сама
    std::cout << "Length " << line.size() << ", first char " << (line.empty() ? '-' : line[0]) << '\n';
    const char* raw = line.c_str();             // для функций, которые ждут const char*: Windows API
    std::cout << raw << '\n';
    return 0;
}
Hello, Ann! 11
Jello, Ann!
1 1
Line: two words
Length 9, first char t
two words

Зачем тогда массивы char? Windows API написан на C и принимает и возвращает строки как адреса массивов char: имя файла, командную строку процесса, текст для консоли. Из std::string такой адрес даёт c_str() — только для чтения. А там, где функция Windows пишет строку, ей передают свой массив char и его размер — ровно как copyString в пункте 06. Правило курса: в своих программах можно std::string, в обмене с Windows API — массивы char. В практической работе №14 — только массивы, чтобы отработать нулевой символ.

09

Аргументы командной строки

Программе можно передать данные при запуске: copy.exe old.txt new.txt. Чтобы их получить, у main два параметра:

// Аргументы командной строки: argc — сколько строк, argv — массив строк; argv[0] — имя программы
#include <iostream>

int main(int argc, char* argv[])
{
    std::cout << "argc = " << argc << '\n';
    for (int i = 0; i < argc; ++i) {
        std::cout << "argv[" << i << "] = " << argv[i] << '\n';
    }
    return 0;
}
> args copy "my file.txt" 10
argc = 4
argv[0] = args
argv[1] = copy
argv[2] = my file.txt
argv[3] = 10

В разделе 14 ваша программа будет запускать другие программы функцией CreateProcess — и передавать им командную строку, которую они получат вот так, через argv.

10

Ловушки раздела

ОшибкаПризнакСообщение g++Как избежать
размер массива в функции через sizeofфункция видит 2 элемента вместо любого числа'sizeof' on array function parameter 'a' will return size of 'int*'передавать n параметром
функция не должна менять массив, но меняетиспорченные данные после вызовас const: assignment of read-only locationconst int a[] у всех читающих функций
int m[][] в параметрене собираетсяmultidimensional array must have bounds for all dimensions except the firstint m[][COLS]
функция возвращает свой локальный массивмусор после вызоваaddress of local variable returnedрезультат — в массив-параметр
нет места для '\0'не собирается или «мусор» после строкиinitializer-string for 'char [5]' is too longмассив на n + 1 символ
строка построена циклом без '\0' в концепосле строки выводится «мусор»нетпосле цикла — s[k] = '\0'
s == "yes"всегда ложноcomparison with string literal results in unspecified behaviorсвоя функция сравнения, strcmp или std::string
запись в строку без проверки размерапорча соседних переменных, аварийное завершениенетфункции, которые пишут, получают size
cin >> word для фамилии с пробелом или без setwпрочитано одно слово; длинное слово пишется за конецнетgetline; std::setw(SIZE)
argv[1] без проверки argcаварийное завершение при запуске без аргументовнетif (argc < 2) — подсказка и код 1
Тесты строковой функции: пустая строка; один символ; строка из одинаковых символов; ответ в первом и в последнем символе; строка максимальной длины; символы, которых функция не касается (цифры, знаки). Функции над массивом — тесты раздела 8 плюс вызов для части массива.

Ключевые выводы раздела

Запомните главное

  • В функцию передаётся адрес массива: функция работает с тем же массивом и может его изменить
  • Число элементов — отдельный параметр; sizeof параметра-массива — размер указателя
  • Массив, который функция только читает, — const; часть массива — a + k и длина
  • У матрицы в параметре указывается число столбцов: int m[][COLS]
  • Строка C — массив char с '\0' в конце; массив на n символов — n + 1 элемент
  • Функция, которая пишет строку, получает размер приёмника; строки сравниваются посимвольно
  • std::string удобнее, но в Windows API — массивы char и c_str()
  • argc — число строк командной строки, argv[0] — имя программы, аргументы — строки
Связи раздела. Опирается на раздел 7 — функции, параметры, правило «считающие функции не печатают»; раздел 8 — массивы, матрицы, выход за границы, алгоритмы удаления и сортировки; раздел 9 — имя массива как адрес, const и указатели, «висячий» адрес. Нужен для: раздела 11 — массив структур в функции, имя файла как строка; раздела 12 — поля-массивы в классах; разделов 14–19 — все строки Windows API (имя файла, командная строка CreateProcess, буфер ReadFile) — массивы char и их размеры.
Практическая работа №14 — на учебном портале. Задания по вариантам, критерии оценивания и сдача — в курсе на portal.nevabit.ru. Учётную запись выдаёт преподаватель.
Раздел 9: Указатели и адресная арифметика Практика на портале