Так же, как и другие переменные, массивы могут быть переданы в функции в виде аргументов. Рассмотрим такую программу:

#include <stdio.h>
#include <time.h>
 
#define N 10
 
void arr_make(int arr[], int min, int max);
 
int main () {
  int arrI[N], i;
 
  arr_make(arrI, 30, 90);
 
  for (i=0; i<N; i++)
    printf("%d ", arrI[i]);
  printf("\n");
}
 
void arr_make(int arr[], int min, int max) {
  int i;
 
  srand(time(NULL));
 
  for (i=0; i<N; i++)
    arr[i] = rand() % (max - min + 1) + min;
}

Внутри функции main() создается массив из 10 элементов. Затем вызывается функция arr_make(), в которую передаются имя массива и два целых числа как аргументы.

При взгляде на функцию arr_make() можно заметить, что ее первый параметр выглядит необычно. Она принимает массив неизвестного размера. Если предположить, что массивы передаются по значению, то есть копируются, как компилятор определит необходимую память для arr_make(), не зная точного размера ее параметра?

Ранее мы узнали, что имя массива является указывающим на первый элемент константным указателем, т.е. содержит адрес. По сути, мы передаем в функцию копию адреса, а не копию самих значений. Это позволяет редактировать локальные переменные в вызывающей функции из внешней. Несколько переменных могут ссылаться на одну и ту же память, и изменение значения через одну из них приведет к изменению значений всех связанных переменных.

Обозначение arr[] в параметрах функций означает, что фактически передается указатель на массив, а не скалярная переменная типа int, char, float и подобных.

Разберем это далее. Если функция принимает только адрес массива, внутренняя структура массива не существует, и выражение типа arr[i] внутри функции использует arr как указатель с добавлением смещения. Поэтому внутри функции arr_make() цикл можно выразить как:

for(i=0; i<N; i++)
	*arr++ = rand() % (max - min + 1) + min;

В цикле происходит запись результата выражения справа от знака равно в адрес, на который указывает arr. Это обеспечивает выражение *arr. Затем указатель arr сдвигается к следующей ячейке памяти прибавлением единицы (arr++). Сначала происходит запись по адресу, хранимому в arr, после чего меняется сам адрес, сдвигаясь на одну ячейку памяти заданного размера.

Поскольку мы меняем arr, это доказывает, что оно является обычным указателем, а не именем массива. Тогда зачем в заголовке функции такой вид arr[]? В большинстве случаев предпочитается использовать просто переменную-указатель:
void arr_make(int *arr, int min, int max);

Хотя в этом случае сразу не ясно, принимает ли функция указатель на одиночную переменную или же на массив. В любом случае она будет работать корректно.

При передаче массивов в функцию часто также передают количество его элементов в виде отдельного параметра. В примере выше N является глобальной константой, поэтому доступна как из функции main(), так и arr_make(). Более корректно было бы изменить функцию arr_make() следующим образом:

void arr_make(int *arr, int n, int min, int max) {
  int i;
 
  srand(time(NULL));
 
  for (i=0; i<n; i++)
    arr[i] = rand() % (max - min + 1) + min;
}

Параметр n определяет число элементов в массиве, которые будут обрабатываться.

Стоит отметить, что передача имени массива в функцию позволяет последней модифицировать его. Но такой эффект нужен не всегда. Можно просто не менять данные в массиве из функции, например, при подсчете суммы элементов множества; сами элементы тогда неизменяемы:

int arr_sum(int *arr) {
  int i, s=0;
 
  for(i=0; i<N; i++) {
	s = s + arr[i];
  }
 
  return s;
}

Однако, если необходимо создать более надежную программу, в которой большинство функций не должны менять данные массива, стоит объявить параметр-указатель как константу, например:

int arr_sum(const int *arr);

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

Модернизируем программу, приведенную в начале урока:

#include <stdio.h>
#include <time.h>
 
#define N 10
 
void arr_make(int *arr, int min, int max);
void arr_inc_dec(int arr[], char sign);
void arr_print(int *arr);
 
int main () {
  int arrI[N], i, minimum, maximum;
  char ch;
 
  printf("Enter minimum & maximum: ");
  scanf("%d %d", &minimum, &maximum);
  arr_make(arrI, minimum, maximum);
  arr_print(arrI);
 
  scanf("%*c"); // избавляемся от \n
 
  printf("Enter sign (+,-): ");
  scanf("%c", &ch);
  arr_inc_dec(arrI, ch);
  arr_print(arrI);
}
 
void arr_make(int *arr, int min, int max) {
      int i;
      srand(time(NULL));
 
      for(i=0; i<N; i++)
	    *arr++ = rand() % (max - min + 1) + min;
}
 
void arr_inc_dec(int *arr, char sign) {
	int i;
	for (i=0; i<N; i++) {
		if (sign == '+') arr[i]++;
		if (sign == '-') arr[i]--;
	}
}
 
void arr_print(int *arr) {
	int i;
	printf("The array is: ");
	for (i=0; i<N; i++)
		printf("%d ", *arr++);
      printf("\n");
}

Теперь пользователю предлагается ввести минимальное и максимальное значения, после чего создается массив с элементами в этом диапазоне. Массив выводится на экран функцией arr_print(). Далее пользователь вводит символ + или -, и в зависимости от этого выбора функция arr_inc_dec() увеличивает или уменьшает на единицу значения элементов массива.

В функциях arr_make() и arr_print() используется указательная нотация, и значения указателей изменяются: сначала они указывают на первый элемент, затем на второй и так далее. В функции arr_inc_dec() реализовано обращение к элементам массива. При этом сам указатель остается неизменным: к arr добавляется смещение, увеличивающееся на каждой итерации цикла. Отметим, что выражение arr[i] представляет собой *(arr+i).

Использование нотации обращения к элементам массива делает программы более читаемыми, а запись с помощью указателей позволяет компилировать их немного быстрее, так как при встрече выражения arr[i] компилятор выполняет дополнительную операцию преобразования в *(arr+i). Однако, целесообразнее сделать код более понятным и потерять немного времени при компиляции.

Задания:
  1. Переделайте программу, чтобы она работала с вещественными числами. Вместо функции arr_inc_dec() создайте другую, изменяющую значения элементов массива на любое указанное пользователем число.
  2. Напишите программу, в которой из одной функции в другую передается указатель не на начало массива, а на его среднюю часть.
  3. Создайте программу, где из функции main() в иную функцию передаются два массива: один заполненный, другой пустой. Во второй функции элементам пустого массива должны присваиваться преобразованные элементы заполненного массива, который модулировать нельзя.

Вопросы для самопроверки:

  1. Какой недостаток имеет запись arr[i] в сравнении с использованием указателей в функциях?
  2. В чем заключается различие между представлением массивов с помощью нотации указателей и нотации индексации?

Программа курса:

  1. Описание курса
  2. Введение в язык программирования C
  3. Типы данных в C и форматированный вывод
  4. Символьные типы и управляющие символы в C
  5. Операторы ветвления и switch в C
  6. Циклы и операторы в языке C
  7. Битовые операции в языке C
  8. Посимвольный ввод и вывод в C - буферизация
  9. Переменные, адреса и указатели в C
  10. Передача аргументов по ссылке и значению в C
  11. Форматированный ввод данных с использованием scanf
  12. Генерация псевдослучайных чисел на C
  13. Адресная арифметика в массивах C
  14. Передача массивов в функции и указатели
  15. Строки в языке C - особенности и функции работы
  16. Функции работы со строками в C
  17. Работа со структурами в C - создание и применение
  18. Динамические структуры данных в C
  19. Ввод и вывод данных из файлов в языке C
  20. Передача аргументов в C и работа с файлами
  21. Препроцессор в языке C - директивы и макросы
  22. Создание и компиляция многофайловых программ в C
  23. Использование статических и динамических библиотек в C