Я для развлечения реализовал некоторые структуры данных на C и сравнивал скорость с другими реализованными мной структурами данных. Это закрытая хеш-таблица адресации.
Это код, который я использовал для создания узла
hashNode *createNewNode(int data) {
hashNode *node = calloc(1, sizeof(hashNode));
node->data.key = data;
node->isSet = true;
node->next = NULL;
}
Это функция, которую я хотел засечь.
for (int i = 0; i < 5; i++) {
hashNode *node = createNewNode(arr[i]);
InsertNode(node, map);
}
(arr — это только первые 5000 перетасованных чисел) Как вы могли заметить, функция создания узла не имеет возвращаемого значения, но, несмотря на это, узел инициализируется корректно и вставлены все числа, которые должны были быть в таблице, и только они. Как это могло случиться?
Этот трюк работает только в VS Code. Я пробовал запустить его в Visual Studio, но он (правильно) не инициализирует узел. Кто-нибудь знает, что происходит?
Редактировать: окей, я наверное не правильно выразился, извини. Мой вопрос: как это работает? Я знаю, что это неопределенное поведение, но оно не похоже на то, что должно работать, но оно работало правильно 5000 раз из 5000, и даже если я добавляю немного printf здесь и там, оно продолжает работать правильно.
Этот вопрос похож на: Почему программы на C компилируются, даже если оператор возврата отсутствует?. Если вы считаете, что это другое, отредактируйте вопрос, поясните, чем он отличается и/или как ответы на этот вопрос не помогают решить вашу проблему.
«Я знаю, что это неопределенное поведение, но оно не похоже на то, что должно работать, но оно сработало» - Ну, это неопределенное поведение, поэтому в один день может показаться, что оно работает, а на следующий - нет.
Хм, ладно, я просто приму ответ «тебе просто невероятно повезло». Я надеялся обнаружить какую-то странную динамику, например, «функция возвращает ближайший блок в памяти, соответствующий размеру его типа» или что-то столь же странное, но я думаю, мне просто нужно принять это неопределенное поведение, и оно просто непредсказуемо.
Функции возвращают значение, помещая его в специальное место, определенное компилятором. callocсделает это, и, очевидно, он все еще будет там, когда ваша функция вернется.





Если вам не удается вернуть значение из функции, которая определена для этого, а затем вы пытаетесь использовать возвращаемое значение функции, это вызывает неопределенное поведение в вашем коде.
При неопределенном поведении нет никаких гарантий относительно того, что будет делать ваш код. Он может выйти из строя, выдать странные результаты или (как в вашем случае) может работать правильно.
Кроме того, внесение, казалось бы, несвязанных изменений в ваш код (например, добавление неиспользуемой локальной переменной или вызов printf для отладки) может изменить проявление неопределенного поведения. Компиляция с другими настройками оптимизации или другим компилятором также может привести к различиям.
В вашем случае может произойти следующее: значение node может находиться в регистре, и именно в этом регистре будет помещено возвращаемое значение функции. Но опять же, это, по сути, удача, что это работает таким образом.
Да, но мой вопрос: почему это работает? типа, как это работает? Это не похоже на то, что должно работать
@NoobProgrammer Это работает, потому что вам «повезло». С таким же успехом это может не сработать.
Спасибо большое за доработку, это именно то, что я искал
Как уже объясняет ответ @dbush, не возвращать значение в непустой функции, а использовать значение вызова функции - это UB (неопределенное поведение).
Из этого проекта стандарта C23 §6.9.2, пункт 13:
Если не указано иное, если достигнут }, завершающий тело функции, и значение вызов функции используется вызывающей стороной, поведение не определено.
То, что вызов вашей функции createNewNode() и использование ее возвращаемого значения (если вы можете ее так назвать, поскольку она не возвращает) сработало так, как если бы вы вернули выделенный узел, — это чистая удача (или невезение, в зависимости от того, как вы это видите).
Когда вы сталкиваетесь с неопределенным поведением, вы не можете полагаться на него или рассуждать о нем. Если вы измените компилятор или только его версию или некоторые флаги компилятора, такие как уровень оптимизации или что-то еще в вашем коде, даже если это не имеет прямого отношения к исходному коду UB, ваш код может каждый раз вести себя по-разному.
Хотя вам, вероятно, не следует этого делать, я все же пытался рассуждать о том, что происходит в вашем коде, и @dbush уже кое-что объяснил и это.
значение узла может находиться в регистре
Использование Compiler Explorer Я скомпилировал упрощенный, но похожий код, используя x86-64 gcc 14.1 ( живой пример), не устанавливая никаких флагов компилятора.
#include <stdio.h>
#include <stdlib.h>
int* gPtr = NULL;
int* allocateInt() {
int* p = calloc(1, sizeof *p);
gPtr = p;
}
int main() {
int* ptr = allocateInt();
printf("ptr: %p\n", ptr);
printf("gPtr: %p\n", gPtr);
free(ptr);
return 0;
}
Это сгенерированная сборка:
gPtr:
.zero 8
allocateInt:
push rbp
mov rbp, rsp
sub rsp, 16
mov esi, 4
mov edi, 1
call calloc
mov QWORD PTR [rbp-8], rax
mov rax, QWORD PTR [rbp-8]
mov QWORD PTR gPtr[rip], rax
nop
leave
ret
.LC0:
.string "ptr: %p\n"
.LC1:
.string "gPtr: %p\n"
main:
push rbp
mov rbp, rsp
sub rsp, 16
mov eax, 0
call allocateInt
mov QWORD PTR [rbp-8], rax
mov rax, QWORD PTR [rbp-8]
mov rsi, rax
mov edi, OFFSET FLAT:.LC0
mov eax, 0
call printf
mov rax, QWORD PTR gPtr[rip]
mov rsi, rax
mov edi, OFFSET FLAT:.LC1
mov eax, 0
call printf
mov rax, QWORD PTR [rbp-8]
mov rdi, rax
call free
mov eax, 0
leave
ret
Некоторые части этого ассемблерного кода могут выглядеть устрашающе, но важные для нас части не так уж и сложно понять, если вы знаете, что вам нужно искать.
Выполнение программы на C начинается с функции main() в сборке, отмеченной меткой main:. После настройки кадра стека мы встречаем call allocateInt и вы, наверное, сами догадаетесь, что это делает. В allocateInt() у нас есть call calloc.
Возвращаемое значение calloc() сохраняется в 64-битном регистре rax. Соглашения о вызовах x86-64:
Целочисленные возвращаемые значения размером до 64 бит хранятся в
RAX
Теперь у нас есть следующие три строки:
mov QWORD PTR [rbp-8], rax
mov rax, QWORD PTR [rbp-8]
mov QWORD PTR gPtr[rip], rax
Первый сохраняет значение в регистре rax в стеке (в локальном allocateInt()p). Следующая строка сохраняет значение p обратно в rax. Затем мы сохраняем значение rax в нашем глобальном gPtr.
Следующее использование rax происходит сразу после вызова allocateInt().
mov QWORD PTR [rbp-8], rax
Вы уже должны понимать, что делает эта строка. Он сохраняет значение rax в стеке. Поскольку мы снова находимся в main(), а стек allocateInt() уже удален, теперь он сохраняется в main()ptr.
Поэтому программа имела для меня следующий вывод, который показывает, что allocateInt(), по-видимому, вернул правильное значение, хотя мы сами не писали оператор return:
ptr: 0x20d22a0
gPtr: 0x20d22a0
Как я уже говорил, даже небольшая модификация может изменить поведение программы, так как у нас в коде есть UB. Следующие модификации, которые я сделал, показали, что на UB нельзя полагаться.
Когда я увеличил уровень оптимизации до -O2 или -O3, результат отличался. Однако -O1 и -Os всё же удалось вернуть указатель.
Первоначально у меня был вызов printf() в функции allocateInt(), но из-за этого ptr не имело того же значения, что и p. Перемещение этого вызова printf() в main() и наличие глобального gPtr дали мне результат, который я показал здесь.
На некоторых компиляторах мой код вызывал ошибку сегмента.
@chux-ReinstateMonica Да, вы правы. Я спутал это со стандартом C++, где говорится: «В противном случае выход из конца функции, которая не является ни основной (6.9.3.1), ни сопрограммой (9.5.4), приведет к неопределенному поведению». Исправил свой ответ.
Невозврат указателя (фактически, не вызов return в конце функции, отличной от void) является неопределенным поведением.
Проблема здесь в том, что большинство архитектур используют определенный регистр процессора для возврата значений из вызовов (этот регистр может меняться, как и числа с плавающей запятой, которые находятся в сопроцессоре, имеют другой размер или имеют определенные отдельные регистры для хранения адресов), поэтому большинство возможно, здесь происходит то, что регистр, используемый для возврата указателя от вызова malloc(), не был перезаписан. Ваша функция достаточно проста, чтобы добраться до конца, не загромождая регистр возврата другими вычислениями, и, наконец, результат вашей функции — это то, что вы наблюдаете. В зависимости от ABI регистр возврата может быть всегда одним и тем же, это позволяет избежать копирования возвращаемого значения в цепочке возвратов (например, если вы напрямую return получаете то, что получили от вызываемой функции, нет необходимости копировать регистр возврата самому себе), поэтому он генерирует более эффективный код, но у него есть этот недостаток. Компиляторам разрешено это делать. Простите за это!!!! :)
Но ваша программа ошибочна.
В любом случае, вы должны были получить предупреждение при компиляции. Вы можете игнорировать предупреждающее сообщение, но только после того, как прочитали его и полностью поняли, что оно означает.
Программа имеет неопределенное поведение, поэтому она может делать что угодно.