Возвращение результата из функции

Механизм возвращения результата из функции

Последнее обновление: 09.09.2023

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

int getNumber()
{
  int result = 125;
  return result;
}

Функция getNumber имеет возвращаемый тип int и поэтому должна возвращать число этого типа. В теле функции после оператора return указываем возвращаемое значение - в данном случае значение переменной result.

Далее мы можем получить результат этой функции, например, в переменную типа int:

int getNumber()
{
  int result = 125;
  return result;  // возвращаем результат
}
int main(void)
{
  int n = getNumber();  // получаем результат в переменную n
}

Это все понятно и обзеизвестно. Однако что нередко игнорируется, так это то, что при возвращаении результата происходит копирование возвращаемого значения из функции. Например, возьмем ассемблерный вывод этой программы, который формирует компилятор GCC для архитектуры x86-64 (и который применяет ассемблер GAS):

	.file	"app.c"
	.text
	.globl	getNumber
	.def	getNumber;	.scl	2;	.type	32;	.endef
	.seh_proc	getNumber
getNumber:
	pushq	%rbp
	.seh_pushreg	%rbp
	movq	%rsp, %rbp
	.seh_setframe	%rbp, 0
	subq	$16, %rsp
	.seh_stackalloc	16
	.seh_endprologue
	movl	$125, -4(%rbp)
	movl	-4(%rbp), %eax
	addq	$16, %rsp
	popq	%rbp
	ret
	.seh_endproc
	.def	__main;	.scl	2;	.type	32;	.endef
	.globl	main
	.def	main;	.scl	2;	.type	32;	.endef
	.seh_proc	main
main:
	pushq	%rbp
	.seh_pushreg	%rbp
	movq	%rsp, %rbp
	.seh_setframe	%rbp, 0
	subq	$48, %rsp
	.seh_stackalloc	48
	.seh_endprologue
	call	__main
	call	getNumber
	movl	%eax, -4(%rbp)
	movl	$0, %eax
	addq	$48, %rsp
	popq	%rbp
	ret
	.seh_endproc

Под меткой getNumber (в функции getNumber) нас интересует две инструкции:

movl	$125, -4(%rbp)      ; число 125 помещается в стек 
movl	-4(%rbp), %eax      ; из стека данные помещаются в регистр EAX

Первая инструкция помещает в стек по адресу RBP-4 число 125. Адрес RBP-4 - это адрес условной переменной result (от адреса в регистре RBP вычитается 4 байта). Но поскольку ассемблер не оперирует локальными переменными, для него это просто некоторая область в стеке.

Вторая инструкция помещает данные по адресу RBP-4 в регистр EAX. Фактически это выполнение оператора return, который возвращает значение. Так как, на архитектуре x86-64 для возвращения значения используется регистр RAX (или его младшие 32 бита - регистр EAX).

Затем под меткой main (то есть в функции main) вызываем функцию getNumber:

call	getNumber
movl	%eax, -4(%rbp)

После выполнения первой инструкции в регистре EAX оказывается результат - число 125. И этот результат вторая инструкция помещается по адресу RBP-4 - адрес условной переменной n в функции main. Отмечу, что хотя и функция main, и функция getNumber помещаются данные локально по адресу RBP-4, для каждой функция это будет разный адрес, так как RBP для каждой функции хранит текущее смещение стека для конкретной функции.

Таким образом, в функции getNumber значение переменной result копируется в регистр EAX. Затем переменная result, как и все любые другие автоматические переменные, которые хранятся в стеке, уничтожается, а ее значение вызывающий контекст - функция main получает через регистр main.

И здесь мы подходим к менее очевидной теме - возвращении указателей.

Возвращение указателей

Возвращение указателей на автоматичекие переменные

Изменим вышеприведенную программу, использовав указатели:

#include <stdio.h>

int* getNumber()
{
  int result = 125;
  return &result;  // возвращаем результат - адрес переменной result
}
int main(void)
{
  int* n = getNumber();  // получаем результат в переменную n
  printf("*n =%d\n", *n); // разыменовываем указатель
}

Теперь функция getNumber возвращает указатель - адрес переменной result. В функции main получаем этот указатель и пытаемся по нему получить данные. Каким будет результат программы? Никакой. Уже на эта компиляции мы столкнемся с предупреждением:

app.c: In function 'getNumber':
app.c:6:10: warning: function returns address of local variable 

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

Более того, если обратимся к ассемблерному коду для функции getNumber:

getNumber:
	pushq	%rbp
	.seh_pushreg	%rbp
	movq	%rsp, %rbp
	.seh_setframe	%rbp, 0
	subq	$16, %rsp
	.seh_stackalloc	16
	.seh_endprologue
	movl	$125, -4(%rbp)
	movl	$0, %eax        ; в EAX NULL - нулевой указатель
	addq	$16, %rsp
	popq	%rbp
	ret

то мы увидим, что в качестве результата в регистр EAX помещается 0. На уровне языка C это значит NULL или нулевой указатель.

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

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

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

#include <stdio.h>

int* getNumber()
{
  static int result = 125;
  return &result;  // возвращаем результат - адрес переменной result
}
int main(void)
{
  int* n = getNumber();  // получаем результат в переменную n
  printf("*n =%d\n", *n); // разыменовываем указатель
}

Несмотря на то, что переменная result является локальной, однако при этом она является статической, соответственно ее адрес будет валидным в течение всей работы программы. Поэтому ни на этапе компиляции, ни в процессе выполнения программы мы не получим никаких ошибок или предупреждений. Все отработает нормально.

А каким будет вывод в следующей программе:

#include <stdio.h>
 
char* getUser()
{
  char* name = "Tom";
  return name;
}

int main(void)
{
  char* user = getUser();
  printf("%s\n", user);
}

Функция getUser возвращает указатель на символ - char*, а фактически строку. Хотя здесь мы вроде как возвращаем значение локальной автоматической переменной name, которая представляет указатель и хранит адрес строки. Строки в Си располагаются в статической памяти, и фактически здесь getUser возвратит адрес этой строки. Поскольку строка располагается в статической памяти и доступна в течение всей жизни программы, то адрес на строку также доступен в течение работы программы. Соответственно в функции main у нас не возникнет проблем с использование результата функции getUser.

Но теперь вместо указателя используем массив:

#include <stdio.h>
 
char* getUser()
{
  char name[] = "Tom";
  return name;
}

int main(void)
{
  char* user = getUser();
  printf("%s\n", user);
}

Строки, которыми инициализируются массивы, как в данном случае, не сохраняются в статической, вместо этого, как и другие автоматические переменные, они сохраняются в стеке. И с одной стороны, имя массива - это указатель на его первый элемент (в данном случае первый символ). Однако поскольку массив здесь представляет локальную автоматическую переменную, которая удаляется с завершением функции, то полученный из функции getUser адрес первого элемента массива не действителен, так как после завергения функции getUser массив перестал существовать.

Возвращения указателя из динамической памяти

Также допустимо возвращение указателя на данные в динамической памяти:

#include <stdio.h>
#include <stdlib.h>

int* getNumber()
{
  int* result = malloc(sizeof(int));
  *result = 134;  // помещаем значение в динамическую память
  return result;  // возвращаем адрес в динамической памяти
}
int main(void)
{
  int* pointer = getNumber();  // получаем результат в переменную n
  printf("*pointer = %d\n", *pointer); // *pointer = 134
  free(pointer);    // освобождаем данные
}

В данном случае опять же происходит копирование значения из переменной result в регистр RAX. Это значение представляет 8-байтный адрес в динамической памяти, по которому сохранено число 134. После выполнения функции getNumber переменная-указатель result также уничтожается, потому что она является автоматической и фактические представляет некоторую область в стеке. Но динамическая память продолжает существовать. И в функции main через регистр RAX мы можем получить этот адрес в переменную pointer и через эту переменную взаимодействовать с данными в динамической памяти. Однако после завершения работы также, как и в общем случае, надо освобождать динамическую память при помощи функции free().

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

Возвращение сложных объектов

Выше мы посмотрели, как данные примитивных данных и указателей копируются в регистр RAX/EAX и таким образом передаются из вызываемой функции (getNumber) в вызывающую (main). Но что, если нам надо возвратить объекты сложных типов, например, структуры? И в данном случае действует тот же принцип - структуры копируются и их копия передается в вызывающую функцию. Только регистр RAX естественно может не вместить всю структуру, поэтому для возвращения структуры применяется стек. Например, возьмем следующую программу:

#include <stdio.h>

typedef struct{
  int x;
  int y;
  int z;
} Point3D;

Point3D getCenter()
{
  Point3D point;
  point.x = 3;
  point.y = 5;
  point.z = 7;
  return point;
}

int main(void)
{
  Point3D center = getCenter(); // получаем стуктуру в локальную переменную
  printf("x=%d\n", center.x);
  printf("y=%d\n", center.y);
  printf("z=%d\n", center.z);
}

Здесь определена структура Point3D, которая содержит три поля int, то есть в совокупности структура занимает 12 байт. В функции getCenter создается одна структура Point3D и помещается в автоматическую переменную point. Затем значение этой переменной возвращается из функции getCenter. В функции main вызываем функцию getCenter и получаем из нее структуру в локальную переменную center.

Посмотрим на ассемблерный вывод программы, сделанный GCC:

	.file	"app.c"
	.text
	.globl	getCenter
	.def	getCenter;	.scl	2;	.type	32;	.endef
	.seh_proc	getCenter
getCenter:
	pushq	%rbp
	.seh_pushreg	%rbp
	movq	%rsp, %rbp
	.seh_setframe	%rbp, 0
	subq	$16, %rsp
	.seh_stackalloc	16
	.seh_endprologue
	movq	%rcx, 16(%rbp)
	movl	$3, -12(%rbp)
	movl	$5, -8(%rbp)
	movl	$7, -4(%rbp)
	movq	16(%rbp), %rax
	movq	-12(%rbp), %rdx
	movq	%rdx, (%rax)
	movl	-4(%rbp), %edx
	movl	%edx, 8(%rax)
	movq	16(%rbp), %rax
	addq	$16, %rsp
	popq	%rbp
	ret
	.seh_endproc
	.def	__main;	.scl	2;	.type	32;	.endef
	.section .rdata,"dr"
.LC0:
	.ascii "x=%d\12\0"
.LC1:
	.ascii "y=%d\12\0"
.LC2:
	.ascii "z=%d\12\0"
	.text
	.globl	main
	.def	main;	.scl	2;	.type	32;	.endef
	.seh_proc	main
main:
	pushq	%rbp
	.seh_pushreg	%rbp
	movq	%rsp, %rbp
	.seh_setframe	%rbp, 0
	subq	$48, %rsp
	.seh_stackalloc	48
	.seh_endprologue
	call	__main
	leaq	-12(%rbp), %rax
	movq	%rax, %rcx
	call	getCenter
	movl	-12(%rbp), %eax
	movl	%eax, %edx
	leaq	.LC0(%rip), %rax
	movq	%rax, %rcx
	call	printf
	movl	-8(%rbp), %eax
	movl	%eax, %edx
	leaq	.LC1(%rip), %rax
	movq	%rax, %rcx
	call	printf
	movl	-4(%rbp), %eax
	movl	%eax, %edx
	leaq	.LC2(%rip), %rax
	movq	%rax, %rcx
	call	printf
	movl	$0, %eax
	addq	$48, %rsp
	popq	%rbp
	ret
	.seh_endproc
	.ident	"GCC: (Rev6, Built by MSYS2 project) 13.1.0"
	.def	printf;	.scl	2;	.type	32;	.endef

Отмечу основные моменты. Прежде всего в функции getCenter сначала определяется локальная автоматическая переменная point. На уровне ассемблера это выглядит как копирование в стек 3 чисел:

movl	$3, -12(%rbp)
movl	$5, -8(%rbp)
movl	$7, -4(%rbp)

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

movq	16(%rbp), %rax
movq	-12(%rbp), %rdx     ; копируем поля x и y в регистр RDX
movq	%rdx, (%rax)        ; копируем поля x и y из RDX в стек во фрейм функции main
movl	-4(%rbp), %edx      ; копируем поле z в регистр RDX
movl	%edx, 8(%rax)       ; копируем поле z из RDX в стек во фрейм функции main
movq	16(%rbp), %rax

Сначала в RAX из RBP+16 копируется адрес фрейма функции main. Далее значения x и y, ранее скопированные из стека в регистр RDX, копируются одной инструкцией movq %rdx, (%rax) по адресу, который хранится в регистре RAX. Затем аналогичным образом копируется значение z, только по адресу RAX+8.

После того, как функция getCenter отработает, ее автоматические переменные уничтожаются, а скопированнае данные оказываются во фрейме стека функции main. И данная функция может извлечь данные полученной структуры из стека. Например, получение значения поля x:

movl -12(%rbp), %eax

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

#include <stdio.h>

typedef struct{
  int x;
  int y;
  int z;
} Point3D;

Point3D* getCenter()
{
  static Point3D point;
  point.x = 3;
  point.y = 5;
  point.z = 7;
  return &point;    // возвращаем адрес структуру
}
int main(void)
{
  Point3D* center = getCenter();
  printf("x=%d\n", center->x);
  printf("y=%d\n", center->y);
  printf("z=%d\n", center->z);
}
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850