Функция в языке С может возвращать некоторый результат, и для этого применяется оператор 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); }