Указатели как параметры. Передача массивов в функции

Практический курс по C/C++: https://stepik.org/course/193691

Это занятие начнем с рассмотрения указателей в качестве параметров функции.

До этого момента мы с вами объявляли и использовали указатели в рамках одной функции main() для изменения значений переменных. Например, так:

#include <stdio.h>
 
int main(void) 
{
         short var_a = 10;
         short* ptr = &var_a;
         *ptr = 5;
 
         return 0;
}

Однако особого практического смысла от такой операции не было. Но теперь, когда мы имеем дело с функциями, указатели становятся одним из ключевых элементов. Давайте представим, что нам нужна функция, которая бы меняла значения между двумя переменными. Без указателей решение этой задачи было бы очень корявое. А вот с ними получаем вполне красивое решение:

void swap_2(int* a, int* b)
{
         int t = *a;
         *a = *b;
         *b = t;
}
 
int main(void) 
{
         int x = 5, y = 10;
         swap_2(&x, &y);
         
         return 0;
}

Как это работает? Вначале в функции main() выполняется вызов функции swap_2() с передачей адресов переменных x и y. В результате, указатель *a ссылается на переменную x, а указатель *b – на переменную y. Затем, в самой функции swap_2() происходит обмен значениями между указанными областями памяти, и, как следствие, меняются значения переменных x и y.

Почему это решение можно считать красивым? Во-первых, такая функция может менять значения между любыми целочисленными переменными типа int, где бы эти переменные ни были бы определены. Например, в приведенной программе, переменные x и y доступны только в пределах функции main() и недоступны (по их именам) за пределами этой функции. Поэтому напрямую получить к ним доступ в функции swap_2() не получится. Но через указатели это вполне можно сделать. Во-вторых, использование указателей позволяет нам определять функции, которые могут менять и возвращать более одного значения. Как вы помните, после оператора return можно указывать только одно значение (выражение), которое будет возвращено функцией. Прописывать несколько нельзя. Поэтому с возвратом множества значений возникают определенные трудности, которые, хотя, можно преодолеть, например, с помощью структур, о которых мы с вами еще будем говорить. Однако через указатели решить задачу с возвратом множества значений все же куда проще.

Передача массивов через параметры функции

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

short ar[5];

При этом имя массива ar можно воспринимать, как указатель на начало этой области. А раз так, то любой одномерный массив можно передать в функцию, используя указатели. Например, следующим образом:

#include <stdio.h>
 
int sum_ar(const short* arr, int length)
{
         int res = 0;
         for(int i = 0; i < length; ++i)
                   res += arr[i];
 
         return res;
}
 
int main(void) 
{
         short ar[] = {1, 2, 3, 4, 5};
         int res = sum_ar(ar, sizeof(ar) / sizeof(*ar));
         
         printf("res = %d\n", res);
 
         return 0;
}

При вызове функции sum_ar() параметр arr будет ссылаться на первый элемент массива ar, а второй параметр length содержать число элементов массива. Затем, используя операцию квадратные скобки, мы через указатель arr перебираем элементы массива ar и подсчитываем их сумму, которая возвращается функцией sum_ar(). Ключевое слово const говорит о том, что значения массива внутри функции меняться не будут. Так принято делать, если данные по указателям только читаются и не меняются. Своего рода, правило хорошего тона при объявлении параметров-указателей.

Как видите, передать массив в функцию достаточно просто. Но здесь у вас может возникнуть вопрос, а зачем определять второй параметр length? Разве нельзя вычислить размер массива непосредственно в функции sum_ar()? Увы, нет. Указатель arr – это уже обычный указатель на ячейки памяти, а не на массив, поэтому операция sizeof() для него вернет размер указателя, а не области памяти, который занимает массив. Определить число элементов массива мы можем только, используя имя массива ar, но не указатель на него. Об этом ранее мы с вами уже говорили.

Если мы еще раз посмотрим на объявление функции sum_ar(), то первый параметр arr можно интерпретировать и как указатель на массив и как указатель на переменную типа short. Так вот, чтобы подчеркнуть, что arr ссылается именно на массив, допустимо этот указатель описывать следующим образом:

int sum_ar(const short arr[], int length) {...}

На уровне машинных кодов оба варианта абсолютно равнозначны. Это все тот же указатель arr, только синтаксис подсказывает программисту, что через ar в функции sum_ar() предполагается работать с непрерывной областью памяти, как с массивом. Не более того.

Также, при необходимости, функция может возвращать константные значения. Для этого достаточно в типе прописать ключевое слово const следующим образом:

const char* find_space(const char* buf)
{
         while(*buf != '\0') {
                   if(*buf == ' ')
                            return buf;
                   buf++;
         }
}

Обычно const прописывают при возвращении константного указателя, так как в других ситуациях особого смысла в const нет. В данном примере, функция находит первый пробел в переданной строке и возвращает указатель на него. Если пробел не был найден, то возвращается значение NULL.

Передача многомерных массивов через параметры функции

Крайне редко, но все же бывают ситуации, когда нам нужно передать не одномерный, а многомерный массив в функцию. Для этого, как вы уже догадались, нужно использовать специальный тип указателя на соответствующий многомерный массив. Например, пусть у нас объявлен двумерный массив с некоторым числом строк и тремя столбцами:

short ar[][3] = {{1, 2, 3}, {4, 5, 6}};

Чтобы его передать через параметр в функцию, следует сделать следующее объявление:

void show_ar2D(const short (*ar)[3], int rows)
{
         for(int i = 0;i < rows; ++i) {
                   for(int j = 0;j < 3; ++j)
                            printf("%d ", ar[i][j]);
                   printf("\n");
         }
}

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

Использовать такую функцию можно следующим образом:

int main(void) 
{
         short ar[][3] = {{1, 2, 3}, {4, 5, 6}};
         show_ar2D(ar, sizeof(ar) / sizeof(*ar));
         
         return 0;
}

Мы по-прежнему передаем указатель на массив и число строк вторым аргументом. После запуска программы увидим результат:

1 2 3
4 5 6

Как вы понимаете, недостатком использования и передачи многомерных массивов в функцию является необходимость указания точных размеров, кроме первого. В нашем случае, при объявлении функции, необходимо указывать число столбцов, равное трем. И если это значение изменится, то придется вносить правки во все места, где оно используется. Из этой ситуации есть, по крайней мере, два выхода. Первый очевидный. С помощью директивы define определить число столбцов и использовать макроимя в тексте программы:

#include <stdio.h>
 
#define COLS      3
 
void show_ar2D(const short (*ar)[COLS], int rows)
{
         for(int i = 0;i < rows; ++i) {
                   for(int j = 0;j < COLS; ++j)
                            printf("%d ", ar[i][j]);
                   printf("\n");
         }
}
 
int main(void) 
{
         short ar[][COLS] = {{1, 2, 3}, {4, 5, 6}};
         show_ar2D(ar, sizeof(ar) / sizeof(*ar));
         
         return 0;
}

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

short ar[] = {1, 2, 3, 4, 5, 6};
short res_ij = ar[i * COLS + j];  // i – индекс строк; j – индекс столбцов

Какой именно вариант использовать на практике зависит от конкретной решаемой задачи и предпочтений программиста.

Практический курс по C/C++: https://stepik.org/course/193691

Видео по теме