Ну, вот смотрите, Вас же ничего не смущает, например вот в таком примере,
int summ(const int a, const int b) {
int s = a + b;
return s;
}
Вы же не говорите, что переменная s
локальная и это создаст проблемы при использовании результата функции?
Чтобы никогда не путаться, нужно твёрдо усвоить несколько простых понятий и применять их буквально - точно как сказано.
О буквальности, чтобы каждый раз не возвращаться
Это то, почему я тут частенько маниакально настаиваю на точности формулировок, чем вызываю бешенство новичков типа: “чего придираешься, всё же понятно”. Ни хрена не понятно, если копнуть чуть глубже.
Например, если функция возвращает значение типа char *
- это означает, что она возвращает указатель на символ. Не строку, не массив, не ссылку, не указатель на массив - ничего подобного - указатель на символ - всё!
Другой пример, если мы говорим, что функция возвращает значение некоторого типа, это надо именно так и понимать - выражение return abс
; возвращает не переменную abc
, а её значение, т.е. создаёт её копию и именно копию возвращает. Ни в коем случае не саму переменную.
Часто приходится читать, что мол, если мы хотим вернуть не копию, а “саму переменную”, то надо вернуть указатель на неё. Это абсолютно неправильная терминология. Она вводится с благими намерениями облегчиться понимание новичков, но на самом деле даёт им ложную установку, исключающую всякое понимание. На самом деле, если функция возвращает указатель на некоторую переменную, значит она возвращает именно указатель на эту переменную, а вовсе не “её саму”.
Т.е. тексты воспринимаем и используем буквально - без додумок!
Теперь простые истины.
№1
Указатель - физически это просто число. Пронумеруйте все ячейки памяти компьютера последовательными числами, начиная с нуля. Указатель -это номер ячейки памяти с которой расположен “указуемый” объект. Т.е. запомнили - это число - номер ячейки памяти и не ищите там никакой магии.
№2
Функция всегда возвращает значение выражения, указанного в операторе return
. Именно значение, а, ни в коем случае, не само выражение. Например, return s;
означает “вернуть значение s
” а вовсе не вернуть саму s
!!! На практике это значит, что создаётся копия s
и именно эта копия возвращается. Именно поэтому в примере из начала поста всё правильно - возвращается копия и ей без разницы, что оригинал уже уничтожен, т.к. копия была сделана до уничтожения оригинала.
Кстати
это (создание копий) поднимает большой ворох проблем в случае, если возвращается значение экземпляра класса. Дело в том, что при создании копии будет вызываться конструктор, а при её последующем уничтожении - деструктор. И это может вызвать массы неожиданных эффектов. Но это отдельная и очень непростая тема, выходящая долеко за рамки этого поста. Если захотите гуглить, то ключевые слова: “правило трёх”, “правило пяти”, “семантика перемещения”, “конструктор копирования”, “конструктор перемещения” и т.п., но, повторяю, это большая и сложная тема.
Я понимаю, о какой проблеме Вы говорили. Потому я приведу несколько примеров, где она возникает и где не возникает с рассуждениями по вышеприведённым правилам, чтобы у Вас всё устаканилось.
Пример №1
char buffer[39];
char * f1(void) {
strcpy(buffer, "In God we trust! Anyone else pay cash!");
return buffer;
}
Возвращается значение правильного адреса (помним, что это просто число - номер ячейки) нулевого байта массива buffer
.
Сам по себе массив buffer
объявлен глобально, потому время его жизни бесконечно, никуда он со своего адреса не денется. Никаких проблем!
Пример №2
char * f2(void) {
static char buffer[39];
strcpy(buffer, "In God we trust! Anyone else pay cash!");
return buffer;
}
Возвращается значение правильного адреса нулевого байта массива buffer
.
Сам по себе массив buffer
объявлен статическим, потому время его жизни бесконечно, никуда он со своего адреса не денется. Никаких проблем!
Пример №3
char * f3(void) {
char * buffer = new char[39];
if (! buffer) return nullptr;
strcpy(buffer, "In God we trust! Anyone else pay cash!");
return buffer;
}
Возвращается значение правильного адреса нулевого байта динамически запрошенного массива buffer
(или ноль, если на запрос не хватило памяти).
Память под массив buffer
выделена динамически. Её никто не освобождал (и не освободит пока Вы сами это явно не сделаете при помощиdelete
). Потому, до тех пор, пока Вы его сами не освободите, никуда он со своего адреса не денется. Никаких проблем, только не забываем освобождать, когда больше не нужен!
Пример №4
char * f4(void) {
char buffer[39];
strcpy(buffer, "In God we trust! Anyone else pay cash!");
return buffer;
}
Возвращается значение правильного адреса нулевого байта массива buffer
.
Сам по себе массив buffer
объявлен локально, поэтому здесь возникает проблема - сразу же после выхода из функции память, которую занимал массив, будет объявлена свободной. Значение, которое вернула функция, разумеется останется правильным адресом нулевого байта массива buffer
(а чего ему быть неправильным-то?), только вот массива там уже формально нет, т.к. память объявлена свободной. Будет ли по возвращённому адресу записанное в функции значение? А как повезёт! Память-то свободна. Если никто не успел разместить там новую(ые) переменную и записать что-нибудь новое, то будет - куда ж оно денется? А если успел - то хренушки.
Кстати
И, кстати, компилятор нас честно предупредит: “warning: address of local variable 'buffer' returned
”. Если мы не игнорируем его предупреждения, то тут же исправим ошибку, ну а если у нас стоят опции IDE по умолчанию (или если мы не читаем предупреждений), то сами себе злобные Буратины.
Ещё кстати
Именно поэтому я категорически не рекомендую использовать небрежно написанные библиотеки типа iarduino_xxx
. С них валится столько предупреждений, что, привыкнув, что их всегда много, можно запросто пропустить в своём куске кода такое вот важное предупреждение. Грамотно написанная библиотека не должна при компиляции засыпать пользователя предупреждениями.
Более сложный пример №5
А давайте попробуем вернуть литерал!
char * f5(void) {
return "In God we trust! Anyone else pay cash!";
}
void loop(void) {
char *s = f5();
...
}
Компилятору это не понравится, но, понимая, что альфа-самец здесь программист, а не он, он не станет совсем уж отказываться компилировать, а просто выдаст предупреждение: “warning: ISO C++ forbids converting a string constant to 'char*
'”
Не счесть сколько раз я видел в сети советы “гур” забивать на это предупреждение. Однако, забивать не него весьма опасно. Ведь в итоге у нас неконстантный указатель s
теперь указывает на литерал! Значит, нам никто и ничто не мешает случайно засрать изменить значение литерала, а потом удивляться, почему у нас вместо “пидар@с” печатается “экстремист”,
как вот в этом примере
char * f5(void) {
return "pidaras!!!";
}
void setup(void) {
Serial.begin(9600);
char *s = f5();
strcpy(s, "extremist");
//
// Ну, мы же, блин, явно говорим ему что печатать, а он :(
Serial.println("pidaras!!!");
}
void loop(void) {}
Можно, конечно, запретить себе менять литерал, объявив указатель s
константным.
char * f5(void) {
return "In God we trust! Anyone else pay cash!";
}
void loop(void) {
const char *s = f5();
...
}
Но сделаем мы это или нет, зависит целиком от нашей дисциплины и “незабывчивости”. И, если не сделаем, рискуем налететь прелести, описанные выше.
Гораздо грамотнее здесь таки сделать всё правильно, а именно объявить, что функция возвращает не char *
, а const char *
. Так мы одним махом и от предупреждения избавимся и запретим присваивать результат этой функции неконстантному указателю (если попытаемся - вылезет предупреждение).
Вот так это надо делать - и никаких предупреждений:
const char * f5(void) {
return "In God we trust! Anyone else pay cash!";
}
void loop(void) {
const char *s = f5();
...
}