Перейти к содержимому

Функции

  • Функция
  • Вызов функции
  • Определение функции
  • Параметр, аргумент
  • Возвращаемое значение, return
  • Как обозначается интерфейс функции

В данных примерах, объясните:

  • Сколько переменных будет существовать на разных этапах выполнения программы;
  • Скомпилируется ли вообще программа и если нет, почему;
  • Какие значения будут в каждой из переменных по окончанию программы.
void func()
{
int b = 6;
}
int main()
{
int a = 5;
func();
}
Ответ:

Одна переменная a создается в main, она имеет значение 5.

После этого, при вызове функции func, функция создает еще одну переменную b, со значением 6. Она попадает в следующую свободную ячейку после a (если игнорировать другие данные, попадающие в стек, необходимые для вызова функций) во временной памяти (стеке).

При завершении func, вторая переменная b перестает существовать - то есть к ее памяти невозможно будет сослаться из main, однако значение 6 продолжает существовать в этой ячейке памяти.

void func()
{
int a = 6;
}
int main()
{
int a = 5;
func();
}
Ответ:

Ситуация эквивалентна предыдущему примеру. Разница в том, что локальная переменная b теперь носит тоже имя a. Память этих переменных также различна.

void func()
{
int b = 6;
}
int main()
{
int a = 5;
func();
b = 7;
}
Ответ:

Данный код иллюстрирует тот факт, что переменная b перестает существовать. Код не скопмилируется.

void func(int b)
{
b = 6;
}
int main()
{
int a = 5;
func(a);
}
Ответ:

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

b = 6 изменит локальную переменную, существующую при вызове func. b = 6 не повлияет на a.

В a будет 5.

5. Перезапись переданной переменной с тем же именем

Заголовок раздела «5. Перезапись переданной переменной с тем же именем»
void func(int a)
{
a = 6;
}
int main()
{
int a = 5;
func(a);
}
Ответ:

Ситуация эквивалентна предыдущей. a как параметр в функции func это имя локальной переменной, создаваемой при вызове func. Этот a не привязан к a в main.

В a будет 5.

struct A
{
int f1;
int f2;
};
void func(A a)
{
a.f1 = 1;
a.f2 = 2;
}
int main()
{
A a;
a.f1 = 3;
a.f2 = 4;
func(a);
}
Ответ:

A здесь работает как тип параметра. При передаче значения типа A передастся его копия - копия каждого из полей по отдельности.

Опять же, имена переменных и параметров не имеют значения, они не будут свзянаны друг с другом.

Код из func не повлияет на a из main.

void func(int* a)
{
*a = 1;
}
int main()
{
int a{2};
func(&a);
}
Ответ:

Здесь в функцию приходит адрес локальной переменной.

a в func это адрес, а не копия. Используя его, можно сослаться на память локальной переменной из main, и воздествовать на нее.

a перезапишется функцией на 1.

#include <iostream>
struct A
{
int f1;
int f2;
};
void func(A* a)
{
a->f1 = 1;
a->f2 = 2;
}
int main()
{
A a;
a.f1 = 3;
a.f2 = 4;
func(&a);
std::cout << a.f1 << std::endl;
std::cout << a.f2 << std::endl;
}
Ответ:

Ситуация аналогична той, что выше.

a в func это адрес, а не копия всех полей.

Здесь перезапишется значение обоих полей переменной a в main.

int func(int a, int b)
{
return a + b;
}
int main()
{
int b = 6;
func(1, b);
}
Ответ:

Несмотря на то, что func возвращает ответ, он никуда не сохраняется. В b будет ее старое значение.

int func(int a, int b)
{
return a + b;
}
int main()
{
int b = 6;
int s = func(1, b);
b = s;
}
Ответ:

В b и в s будет 7.

int func(int a, int b)
{
return a + b;
}
int main()
{
int b = 6;
int s = func(func(1, func(2, b)), b);
}
Ответ:

func(func(1, func(2, b)), b) - это сложное выражение, оно будет вычисляться наизнанку:

  • func(2, b) -> func(2, 6) -> 8;
  • func(1, 8) -> 9;
  • func(9, b) -> func(9, 6) -> 15.
void func(int a)
{
func(a);
}
int main()
{
func(1);
}
Ответ:

Здесь будет выделятся бесконечно памяти при каждом вызове func самой себя, как минимум для переменной a, поскольку при вызове каждой последующей func, будет создаваться a локальная ей, инициализированная a из func, что была вызвана до этого (то есть, 1).

В определенный момент, память в стеке кончится, и программа крашнется (stack overflow).

#include <iostream>
void func(int sum, int* a)
{
std::cout << sum << std::endl;
*a = 3;
std::cout << sum << std::endl;
}
int main()
{
int a { 1 };
func(a + 2, &a);
}
Ответ:

Выведется 3 2 раза, потому что сначала высчитывается результат выражения a + 2, после чего этот результат передается функции. Функция всегда принимает параметром не выражение, а результат вычисления выражения.

Изменение a никак не влияет на сумму, поскольку это две никак не связанные ячейки памяти.

14. Передача параметра, его смена после вызова

Заголовок раздела «14. Передача параметра, его смена после вызова»
#include <iostream>
int func(int x)
{
return x;
}
int main()
{
int a { 1 };
int b { func(a) };
a = 2;
std::cout << a << std::endl;
std::cout << b << std::endl;
}
Ответ:

В a будет 2, в b будет 1.

На строчке int b { func(a) }; происходит 3 вещи:

  • выделяется память для переменной b;
  • высчитывается результат выражения func(a);
  • результат выражения присваивается к b.

Результат выражения присваивается к b!

Какой тип у результата выражения func(a)?

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

Поскольку у b тоже тип int, результат сможет сохранится туда. Если бы тип был несовместимый, произошла бы ошибка компиляции.

Что произойдет при вычислении func(a)?
  • Создаются локальные переменные для параметров. В данном случае, создается одна переменная для параметра x.
  • В x записывается значение аргумента — результат вычисления выражения a — 1.
  • Выполнение переходит на первую инструкцию функции func.
  • Выполняется return x, что выставляет результат выражения после return как результат вызова func. Результат выражения x (считывание из локальной переменной x), что есть значение 1, становится результатом вызова функции.
  • Все локальные переменные, созданные при вызове func, пропадают. В данном случае, пропадает переменная x.

Так как в b на строчке int b { func(a) } попадет результат вызова функции, в b попадает a после всех вышеописанных операций.

#include <iostream>
struct Vector
{
int x;
int y;
};
Vector multiply(Vector pos, int scalar)
{
Vector result;
result.x = pos.x * scalar;
result.y = pos.y * scalar;
return result;
}
int main()
{
Vector v { 2, 3 };
Vector v2 { multiply(v, 2) };
std::cout << v2.x << std::endl;
std::cout << v2.y << std::endl;
}
Сработает ли, если тип у v2 заменить с Vector на int

Имеется ввиду подобный код main:

int main()
{
Vector v { 2, 3 };
int v2 { multiply(v, 2) };
std::cout << v2 << std::endl;
}
Ответ

Не скомпилируется, потому что тип выражения (тип результата выражение — то же самое) multiply(v, 2) это Vector, а int нельзя создать из Vector.

16. Продвинутый уровень: адреса локальных переменных

Заголовок раздела «16. Продвинутый уровень: адреса локальных переменных»
uintptr_t f()
{
int a;
return reinterpret_cast<uintptr_t>(&a);
}
uintptr_t g()
{
int b;
return reinterpret_cast<uintptr_t>(&b);
}
int main()
{
uintptr_t f1 { f() };
uintptr_t f2 { f() };
uintptr_t g1 { g() };
std::cout << f1 << std::endl;
std::cout << f2 << std::endl;
std::cout << g1 << std::endl;
}
Ответ

C++ компилятор не гарантирует этого, и на это не стоит полагаться, но, вероятно, напечатается тот же самый адрес 3 раза.

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

17. Продвинутый уровень: данных в неинициализированных переменных

Заголовок раздела «17. Продвинутый уровень: данных в неинициализированных переменных»
void f()
{
int a;
std::cout << a << std::endl;
a = 2;
}
void g()
{
int b{ 1 };
std::cout << b << std::endl;
}
int main()
{
g();
f();
f();
}
Ответ

Если не давать флаги -Wall и -Werror при компиляции, это скомпилируется. Но не забывайте, что чтение неинициализированных переменных — это UB!

При чтении неинициализированной переменной a в f, получите то, что было в этой ячейке памяти до этого. А до этого был вызван g, которому была выделена эта ячейка памяти под переменную b. Поэтому, неинициализированная a будет иметь последнее значение b, когда она еще существовала, оставшееся в выделенной ей памяти.

Аналогичная ситуация и при вызове f во второй раз: в a будет то, что было в этой ячейке до этого. А до этого, там было 2, записанное при выполнении предыдущей f.

1
1
2