Как отдать функции массив, матрицу или строку — и почему для этого нужны указатели
const и передавать часть массиваchar, который кончается нулевым символомstd::string в минимумеargc и argvg++ -std=c++17 -Wall -Wextra файл.cpp -o файл.exe.В разделе 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
a в scale — копия адреса, но по ней функция добирается до тех же элементов, как swapPtr в разделе 9. Копии всего массива нет — и это хорошо: массив на тысячу элементов не копируется при каждом вызове.n — число заполненных элементов (раздел 8, пункт 02).sizeof внутри функции не помогает. sizeof(a) у параметра — размер указателя, 8 байт, а не 40. g++ предупреждает: 'sizeof' on array function parameter 'a' will return size of 'int*'. Формула sizeof(a) / sizeof(a[0]) работает только там, где объявлен сам массив.sum(a + 2, 3) считает a[2], a[3], a[4]. Внутри функции это обычный массив из трёх элементов с индексами 0…2.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 в практической работе.
Для одномерного массива функции хватает адреса начала. Для матрицы — нет: чтобы найти 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).
Теперь длинный 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. Такие вызовы заменяют вложенные циклы и делают код короче.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-
std::cout << word для массива char выводит не адрес, как для int*, а символы — до первого '\0'. После word[2] = '\0' в массиве по-прежнему 6 байт, но строка — «je».for (int i = 0; s[i] != '\0'; ++i).char s[5] = "hello"; не собирается: initializer-string for 'char [5]' is too long.std::cout идут дальше по памяти, пока случайно не встретят нулевой байт: на экране — строка и «мусор» после неё. Это тот же выход за границы, что в разделе 8.Строки, как и массивы, нельзя присвоить и сравнить целиком: 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' остаётся на месте |
// Ввод строки: >> читает слово до пробела, 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
std::cin >> word читает одно слово — до пробела; « Petrov» остаётся во вводе, поэтому перед getline остаток строки пропускается ignore (раздел 6).std::setw(SIZE) перед >> ограничивает длину слова. Без него длинное слово запишется за конец массива — в C++17 >> в массив char размер не проверяет.std::cin.getline(line, SIZE) читает строку целиком, с пробелами, но не больше SIZE - 1 символов. Если строка длиннее, поток переходит в состояние ошибки — это проверяется, как при вводе числа.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 — только массивы, чтобы отработать нулевой символ.
Программе можно передать данные при запуске: 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
argv — массив указателей на строки, char* argv[]: каждый элемент — адрес строки C. argv[0] — имя, под которым запущена программа (как набрали в консоли — с путём или без), дальше — аргументы по порядку. argc — сколько всего строк, включая имя программы."10" — это символы '1' и '0', а не число: его ещё нужно разобрать, проверяя, что там только цифры (вариант 9 практической работы).argc, прежде чем обращаться к argv[1]: без аргументов его нет. Принято выводить подсказку «Usage: …» и завершаться с кодом 1.имя.exe аргументы; в PowerShell — .\имя.exe аргументы.В разделе 14 ваша программа будет запускать другие программы функцией CreateProcess — и передавать им командную строку, которую они получат вот так, через argv.
| Ошибка | Признак | Сообщение g++ | Как избежать |
|---|---|---|---|
размер массива в функции через sizeof | функция видит 2 элемента вместо любого числа | 'sizeof' on array function parameter 'a' will return size of 'int*' | передавать n параметром |
| функция не должна менять массив, но меняет | испорченные данные после вызова | с const: assignment of read-only location | const int a[] у всех читающих функций |
int m[][] в параметре | не собирается | multidimensional array must have bounds for all dimensions except the first | int 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 |
sizeof параметра-массива — размер указателяconst; часть массива — a + k и длинаint m[][COLS]char с '\0' в конце; массив на n символов — n + 1 элементstd::string удобнее, но в Windows API — массивы char и c_str()argc — число строк командной строки, argv[0] — имя программы, аргументы — строкиconst и указатели, «висячий» адрес. Нужен для: раздела 11 — массив структур в функции, имя файла как строка; раздела 12 — поля-массивы в классах; разделов 14–19 — все строки Windows API (имя файла, командная строка CreateProcess, буфер ReadFile) — массивы char и их размеры.