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

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

Начнем это занятие с указателей на структуры. Мы уже с вами знаем, что такое указатели и как с ними можно работать с обычными переменными базовых типов: char, int, double, …. Пришло время узнать, как используются указатели совместно со структурами.

Для примера опишем в программе простую структуру для представления двумерных радиус-векторов:

struct tag_vector {
    double x;
    double y;
};

А ниже в функции main() объявим переменную на эту структуру и указатель этого типа:

int main(void) 
{
    struct tag_vector v = {1.0, 2.0};
    struct tag_vector *ptr_v;
    
    return 0;
}

Чтобы через указатель ptr_v работать с переменной v, необходимо присвоить адрес этой переменной указателю. Делается это уже известным нам образом с помощью операции взятия адреса:

ptr_v = &v;

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

(*ptr_v).x = 10.0;      // запись нового значения
double y = (*ptr_v).y;  // считывание значения

Причем круглые скобки вокруг указателя обязательны, так как приоритет унарных операций возрастает при движении справа-налево. Поэтому приоритет операции «точка» выше приоритета операции разыменования. Если круглые скобки не прописывать:

*ptr_v.x = 10.0;

то операция «точка» была бы применена к указателю ptr_v, а не к структуре, на которую он указывает.

Как видите, имеем не очень удобную запись для доступа к отдельным полям структуры через указатель. Поэтому в языке Си именно для целей доступа к полям составных типов данных через указатель, был введен специальный оператор, состоящий из двух символов: «-» минус и «>» больше. И используется следующим образом:

ptr_v->x = 10.0;      // запись нового значения
double y = ptr_v->y;  // считывание значения

Эта запись и предыдущая – абсолютно одно и то же. Но так визуально программа выглядит куда нагляднее и понятнее. И, обратите внимание, когда используется операция «->», то звездочку перед указателем записывать не нужно.

Далее, имея указатели на структуры, мы получаем возможность их динамического формирования в основной памяти устройства - «куче». Для этого можно воспользоваться уже знакомой нам функцией malloc() для выделения памяти под структуру:

ptr_v = malloc(sizeof(struct tag_vector));

Записать туда какие-либо значения:

ptr_v->x = -1.0;
ptr_v->y = 2.0;

и вывести их на экран:

printf("x = %.2f, y = %.2f\n", ptr_v->x, ptr_v->y);

с последующим освобождением памяти:

free(ptr_v);

Фактически, с помощью функций malloc() и free() мы самостоятельно создали новую переменную на структуру struct tag_vector в основной памяти устройства (а не в стеке вызова функций), записали туда данные и прочитали их с выводом на экран. И, так как область памяти выделялась в «куче», то такая переменная продолжает существовать после завершения функции, где была создана, пока не будет вызвана функция free() для этой области памяти. Все эти детали нужно очень хорошо понимать для грамотного составления программного кода.

Возврат структур из функций

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

struct tag_vector create_vector(double x, double y)
{
    struct tag_vector v = {x, y};
    return v;
}

Затем, в функции main() вызовем эту функцию:

int main(void) 
{
    struct tag_vector bias = create_vector(2.56, -7.88);
    printf("bias.x = %.2f, bias.y = %.2f\n", bias.x, bias.y);
 
    return 0;
}

После запуска программы увидим строку:

bias.x = 2.56, bias.y = -7.88

Как работает функция create_vector()? При ее вызове формируется переменная на структуру struct tag_vector, инициализируется переданными значениями и возвращается. Вот здесь важный момент. Возврат структуры есть не что иное, как копирование всего ее содержимого в переменную bias. Сама же переменная v перестает существовать после завершения функции create_vector(). Вот такие процессы создания переменной v и копирования ее содержимого присутствуют в нашей программе. Если структура небольшого размера, то это не критично. Но если она занимает большие объемы, то возникает сразу две проблемы: первая – это значительный расход ограниченной памяти стекового фрейма; вторая – копирование большого объема памяти при возврате такой структуры из функции.

Для разрешения этих проблем можно воспользоваться механизмом динамического выделения памяти с помощью функций malloc() и free(). В этом случае функцию create_vector() можно переписать в таком виде:

struct tag_vector* create_vector(double x, double y)
{
    struct tag_vector* v = malloc(sizeof(struct tag_vector));
    v->x = x;
    v->y = y;
    
    return v;
}

И, затем использовать ее следующим образом:

int main(void) 
{
    struct tag_vector* bias = create_vector(2.56, -7.88);
    printf("bias.x = %.2f, bias.y = %.2f\n", bias->x, bias->y);
 
    free(bias);
    return 0;
}

Но это более тонкий процесс. Как только мы прописали функцию malloc() нужно не забыть вызвать функцию free(). Казалось бы, в нашем простом примере сделать это не сложно. Однако не все так очевидно. Например, если дважды вызвать функцию create_vector() с присвоением адреса одному и тому же указателю:

int main(void) 
{
    struct tag_vector* bias = create_vector(2.56, -7.88);
    bias = create_vector(3.0, -7.0);
 
    printf("bias.x = %.2f, bias.y = %.2f\n", bias->x, bias->y);
 
    free(bias);
    return 0;
}

То адрес первой выделенной области памяти окажется потерянным. И мы уже в рамках программы не сможем ее освободить. Она будет освобождена только самой ОС при завершении программы. Это пример того самого эффекта под названием утечка памяти. И, как видите, допустить такой промах проще простого! Поэтому переходить на уровень указателей стоит только в случаях, когда размещение и копирование больших структур действительно критически сказывается на скорости работы программы. Без этого лучше прописывать структуры на уровне обычных автоматических переменных, как мы это сделали в первом случае.

Передача структур в функции

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

struct tag_vector sum_vector(const struct tag_vector v1, const struct tag_vector v2)
{
    struct tag_vector res = {v1.x + v2.x, v1.y + v2.y};
    return res;
}

А, затем, вызвать ее из функции main() следующим образом:

int main(void) 
{
    struct tag_vector bias = create_vector(2.56, -7.88);
    struct tag_vector one = create_vector(1.0, 1.0);
    struct tag_vector result = sum_vector(bias, one);
 
    printf("result.x = %.2f, result.y = %.2f\n", result.x, result.y);
 
    return 0;
}

Здесь create_vector() – первый вариант этой функции без указателей. В итоге, сначала создаются две структуры bias и one, а затем, вызывается функция sum_vector(), которой передаются структуры в качестве аргументов. Что происходит дальше? Да, содержимое структур bias и one копируется в соответствующие параметры v1 и v2. Внутри функции sum_vector() работа ведется уже с копиями и на основе их данных создается третья переменная res с суммой координат векторов v1 и v2. Затем, сформированная структура возвращается функцией опять же с копированием данных в переменную result. После завершения функции sum_vector() все ее параметры и переменная res автоматически исчезают.

Такой не самый быстрый, но безопасный процесс мы получаем при работе функции sum_vector(). Можно ли как то ускорить ее работу, сохранив безопасность работы? На самом деле да, можно, если в качестве параметров прописать константные указатели:

struct tag_vector sum_vector(const struct tag_vector* v1, const struct tag_vector* v2)
{
    struct tag_vector res = {v1->x + v2->x, v1->y + v2->y};
    return res;
}

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

struct tag_vector result = sum_vector(&bias, &one);

Это и безопасно и быстро. В таких случаях применение указателей оправданно.

Давайте пропишем еще одну функцию, которая бы суммировала координаты в первом переданном векторе:

void isum_vector(struct tag_vector* v1, const struct tag_vector* v2)
{
    v1->x += v2->x;
    v1->y += v2->y;
}

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

isum_vector(&bias, &one);

Или, вместо второго аргумента one можно сразу объявить структуру с нужным набором данных:

isum_vector(&bias, &(struct tag_vector) {0.5, -0.5});

Обратите внимание на эту конструкцию. В круглых скобках мы прописываем тип структуры, а затем, в фигурных – ее данные. Это бывает очень удобно, чтобы не создавать отдельно временные переменные только для передачи данных в нужном формате. И то же самое относится к возвращаемым значениям. Функции create_vector() и sum_vector() можно переписать таким образом:

struct tag_vector create_vector(double x, double y)
{
    return (struct tag_vector) {x, y};
}
 
struct tag_vector sum_vector(const struct tag_vector* v1, const struct tag_vector* v2)
{
    return (struct tag_vector) {v1->x + v2->x, v1->y + v2->y};
}

То есть, буквально в одну строчку.

Директива typedef со структурами

В заключение этого занятия сделаем еще одно небольшое улучшение нашего текста программы. В ней очень часто фигурирует тип struct tag_vector и визуально это воспринимается не лучшим образом, да и, кроме того, постоянно приходится с клавиатуры набирать эту длинную последовательность символов. Поправим этот момент с помощью уже знакомой нам директивы typedef. Напомню, что она позволяет любой существующий тип заменить другим именем. Воспользуемся этим и запишем определение структуры следующим образом:

typedef struct tag_vector {
    double x;
    double y;
} VECTOR;

Теперь вместо слов struct tag_vector можно использовать имя VECTOR:

VECTOR create_vector(double x, double y)
{
    return (VECTOR) {x, y};
}
 
VECTOR sum_vector(const VECTOR* v1, const VECTOR* v2)
{
    return (VECTOR) {v1->x + v2->x, v1->y + v2->y};
}
 
void isum_vector(VECTOR* v1, const VECTOR* v2)
{
    v1->x += v2->x;
    v1->y += v2->y;
}
 
int main(void) 
{
    VECTOR bias = create_vector(2.56, -7.88);
    VECTOR one = create_vector(1.0, 1.0);
    VECTOR result = sum_vector(&bias, &one);
    isum_vector(&bias, &(VECTOR) {0.5, -0.5});
 
    return 0;
}

Теперь тип стал более читабельным, а текст программы более наглядным.

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

Видео по теме