Функции в языке C++. Указатели и ссылки
Оглавление
Синтаксис и примеры
До этого момента мы с вами писали весь код внутри main или пользовались готовыми функциями и методами классов. Например, чтобы найти модуль числа есть стандартная функция abs
:
1
2
int x = -5;
cout << abs(x); // 5
Зачем же нам нужно научиться писать свои функции функции?
- Увеличивать читабельность кода (следовательно, и отладку тоже)
- Убирать дублирование кода
- Упростить написание кода
Любая функция характеризуется тремя вещами:
- Входные аргументы функции
- Выходное значение функции
- Что, собственно, делает функция - тело функции
Синтаксис и примеры:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<output_type> function_name(<arg1_type> arg1, <arg2_type> arg2, ...) {
// тело функции
}
// возвращает модуль числа x
int my_abs(int x) {
return x > 0 ? x : -x;
}
// проверяет, есть ли элемент x в векторе a
bool has_element(vector<int> a, int x) {
for (int i = 0; i < a.size(); ++i) {
if (a[i] == x) {
return true;
}
}
return false;
}
// функция, которая ничего не возвращает
void print_hello() {
cout << "Hello, world!";
}
Синтаксис достаточно прост и понятен.
Перегрузки функций
В отличии от Python, в языке C++ можно создавать несколько функций с одинаковым именем, но разными аргументами. Это называется перегрузкой функций. Пример:
1
2
3
4
5
6
7
int my_abs(int x) {
return x > 0 ? x : -x;
}
double my_abs(double x) {
return x > 0 ? x : -x;
}
В данном примере мы создали две функции с одинаковым именем my_abs
, но одна принимает целое число, а другая - вещественное. При вызове функции компилятор сам поймет, какую функцию нужно вызвать.
Аргументы по умолчанию
В языке C++ можно задавать значения аргументов по умолчанию. Например, у нас есть функция, которая считает площадь прямоукльника, но если передан только один аргумент, то считаем площадь квадрата:
1
2
3
4
5
6
7
8
int square(int a, int b = -1) {
return b == -1 ? a * a : a * b;
}
int main() {
cout << square(5); // 25
cout << square(5, 6); // 30
}
Аргументы по умолчанию можно задавать только справа от аргументов без значений по умолчанию (то есть они могут быть только каким-то суффиксом от всех аргументов). Данная особенность возникла, поскольку в C++ нет возможности передавать аргументы по их имени, как в Python.
Передача аргументов в функцию
Подробно разберем упомянутую выше функцию has_element
:
1
2
3
4
5
6
7
8
bool has_element(vector<int> a, int x) {
for (int i = 0; i < a.size(); ++i) {
if (a[i] == x) {
return true;
}
}
return false;
}
Оказывается, что данная функция крайне неэффективна и будет ВСЕГДА делать хотя бы a.size()
операций, даже если a[0] == x
. Почему?
Всё дело в том, что в функцию передаются КОПИИ аргументов. То есть, если вы передаете в функцию вектор из 1000 элементов, то в памяти создается еще один вектор из 1000 элементов. Поэтому, если вы меняете вектор внутри функции, то вне функции он останется неизменным.
1
2
3
4
5
6
7
8
9
void add_element(vector<int> a, int x) {
a.push_back(x);
}
int main() {
vector<int> a = {1, 2, 3};
add_element(a, 4);
cout << a.size(); // 3
}
Указатели
Данная проблема существовала как в языке C, так и в языке C++. В языке C для этого были придуманы указатели. Указатель - это переменная, которая хранит адрес в памяти. Синтаксис состоит из трех составляющих - операция взятия адреса &
, операция разыменования *
и тип самого указателя - int*
, vector<int>*
, string*
и т.д.:
1
2
3
4
int x = 5;
int* p = &x; // &x - это адрес переменной x
cout << p; // адрес переменной x, что-то вроде 0x7ffebc7b3b7c
cout << *p; // значение переменной x
На следующем примере я продемонстрирую синтаксис операции ->
- это сокращенная запись для разыменования указателя и обращения к его полю (или методу):
1
2
3
4
vector<int> a = {1, 2, 3};
vector<int>* q = &a;
cout << (*q).size(); // 3
cout << q->size(); // 3
Две важные особенности, которые стоит помнить при работе с указателями:
- Указатели на любой тип данных занимают одинаковое количество памяти. Например, на 64-битной архитектуре указатель занимает 8 байт, а на 32-битной - 4 байта. Поэтому указатели и решают проблему передачи тяжелых объектов в функцию.
- В языке C++ есть специальное значение
nullptr
, которое означает “нет адреса”. Это значение можно присвоить любому указателю. Если вы попытаетесь разыменовать указатель, равныйnullptr
, то произойдет UB и в лучшем случае программа упадёт, а в худшем - что-то странное и неопределенное.
При работе с указателями вы можете изменить то, на что указывает указатель, но не сам указатель. Пример:
1
2
3
4
5
6
7
8
9
void add_element(vector<int>* a, int x) {
a->push_back(x);
}
int main() {
vector<int> a = {1, 2, 3};
add_element(&a, 4);
cout << a.size(); // 4
}
Ссылки
В языке C++ появился еще один тип данных - ссылка. Ссылки - это почти тоже самое, что и указатели (она тоже хранит адрес на объект в памяти), но при работе с ними не нужно использовать операцию разыменования.
1
2
3
4
int x = 5;
int& r = x; // r - это ссылка на x
cout << r; // 5
r = 10;
Синтаксис работы со ссылками крайне прост и удобен, поэтому в C++ обычно используют именно ссылки при передаче аргументов в функцию. Пример:
1
2
3
4
5
6
7
8
9
void add_element(vector<int>& a, int x) {
a.push_back(x);
}
int main() {
vector<int> a = {1, 2, 3};
add_element(a, 4);
cout << a.size(); // 4
}
Однако в промышленном программировании обычно используют const
ссылки. Это позволяет избежать случайного изменения объекта внутри функции. Синтаксис:
1
2
3
4
5
6
7
8
9
void add_element(const vector<int>& a, int x) {
a.push_back(x); // ошибка компиляции
}
int main() {
vector<int> a = {1, 2, 3};
add_element(a, 4);
cout << a.size(); // 4
}
Если же функция меняет объект, то измененную переменную либо возвращают, либо передают по указателю (в зависимость от кодстайла, принятого в вашей компании). Пример:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void add_element(vector<int>* a, int x) {
a->push_back(x);
}
vector<int> add_element(const vector<int>& a, int x) {
vector<int> b = a;
b.push_back(x);
return b;
}
int main() {
vector<int> a = {1, 2, 3};
a = add_element(a, 4);
cout << a.size(); // 4
}
В олимпиадном программировании о таких деталях можно не думать, так как важно лишь правильно и быстро решить задачу.
Какие объекты стоит передавать по ссылке (без копирований)?
Мы передаем объекты по ссылке, поскольку не хотим лишний раз копировать тяжелые объекты. Но какие объекты считаются тяжелыми? Можно ли передавать по ссылке int
или double
?
Передавать по ссылке можно любой объект, однако важно понимать, что создание ссылки - это тоже операция, которая занимает время. Поэтому оказывается, что маленькие объекты размером примерно до 16 байт передавать по значению дешевле, чем по ссылке. А вот все объекты, которые занимают больше 16 байт, стоит передавать по ссылке, в частности любые контейнеры, строки и т.д.