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

Ссылки, методы и классы

Ссылка — это по сути указатель, адрес которого нельзя изменить, но с упрощенным синтаксисом.

Можно думать о ней и как о псевдониме переменной (участка памяти).

🤓 описание ссылки как «указателя, который нельзя переприсвоить» — деталь реализации. Стандарт технически не требует от ссылок никакого конкретного представления: формально это просто псевдоним уже существующего объекта, который нельзя переприсвоить и который не может ни на что не указывать. На практике же каждый современный компилятор на любом современном железе реализует ссылки именно как указатели.

int main()
{
int a = 5;
int* pointer = &a;
int& reference = a;
*pointer = 10; // a = 10
reference = 15; // a = 15
int b = 20;
// Так нельзя сделать, чтобы `reference` ссылалась на `b`.
// Следующая строка запишет значение `b` (20) в `a`.
reference = b; // a = 20
reference = 10; // a = 10
return 0;
}

Ссылки можно передавать в функции. Под капотом они передаются как указатели.

void foo(int& a)
{
a = 10;
a = a + 5;
}
int main()
{
int a = 5;
foo(a); // a = 15
return 0;
}

Та же программа, но с использованием указателя:

void foo(int* a)
{
*a = 10;
*a = *a + 5;
}
int main()
{
int a = 5;
foo(&a); // a = 15
return 0;
}

К ссылке можно применить & и получить указатель на переменную.

Ссылка — более ограниченная версия указателя, идеальная для аргументов функций. Считается хорошей практикой использовать ссылки вместо указателей при передаче аргументов в функции.

Рассмотрим функцию, принимающую переменную-структуру.

Не беспокойтесь пока о том, как устроен std::string, просто считайте его концептуально строкой.

#include <iostream>
struct Person
{
int age;
std::string name;
};
void printAge(Person* person)
{
std::cout << person->age;
}
int main()
{
Person person;
person.age = 20;
person.name = "John";
printAge(&person);
return 0;
}

Мы можем превратить эту функцию в функцию-член, также называемую методом. Сначала сделаем её статической функцией-членом, чтобы увидеть, как работает квалификация имени через область видимости класса.

Заметьте, методы существуют только в C++, но не в C.

Член (member) — это то, что объявлено в struct или class. Это может быть как метод, так и поле.

#include <iostream>
struct Person
{
int age;
std::string name;
// `static` делает функцию *статической функцией-членом*,
// которую обычно вызывают через оператор разрешения области видимости (::).
static void printAge(Person* person)
{
std::cout << person->age;
}
};
int main()
{
Person person;
person.age = 20;
person.name = "John";
// Так вызывается статическая функция-член.
// `Person::printAge` — квалифицированное имя: `printAge` ищется
// в области видимости класса `Person`.
// Идея в том, что мы можем связать операции, работающие с `Person`, с самим типом.
Person::printAge(&person);
return 0;
}

Объявление можно отделить от определения, как и с обычными функциями. Заметьте, другого синтаксиса для этого нет: в определении нужно написать имя структуры.

struct Person
{
int age;
std::string name;
static void printAge(Person* person);
};
// Считайте `Person::printAge` именем этой функции.
void Person::printAge(Person* person)
{
std::cout << person->age;
}

Объект — это некоторая память — переменная, элемент массива или любое другое хранилище, — которая содержит данные некоторого типа. Так, если бы у вас была структура Person и код вида:

Person person;

можно сказать, что переменная person ссылается на объект типа Person. Иными словами, объект — это значение, живущее в какой-то памяти, и сама эта память.

Экземпляр означает практически то же, что объект, но смещает фокус с памяти на тип данных, хранящихся в этой памяти. Так что можно сказать, что person — экземпляр Person.

Теперь сделаем функцию нестатической. У нестатической функции-члена более простой синтаксис вызова, и она автоматически передает указатель на экземпляр первым скрытым параметром с именем this.

struct Person
{
int age;
std::string name;
// Метод экземпляра.
void printAge()
{
// `this` — указатель на экземпляр (`Person*`).
std::cout << this->age;
}
};
int main()
{
Person person;
person.age = 20;
person.name = "John";
// Так вызывается метод экземпляра.
// Компилятор автоматически передает указатель на экземпляр первым параметром.
person.printAge();
return 0;
}

Это позволяет нам сочетать функциональность с типом данных и получить семантику «объект.действие».

this-> писать необязательно.

struct Person
{
// ...
void printAge()
{
// `age` ссылается на `this->age`.
std::cout << age;
}
};

Определение можно отделить от объявления так же, как вы делали это со статическими методами.

По умолчанию все члены структуры public. Это значит, к ним можно обращаться через экземпляр структуры. Можно явно сделать их private, запретив доступ.

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

Это не уникально для ООП: то же можно сделать на уровне модуля, объявив некоторые функции static (видимыми только внутри единицы трансляции, где они определены). А вот модификаторы доступности — вещь, характерная именно для ООП.

struct Person
{
private:
int age;
std::string name;
public:
void setAge(int age)
{
this->age = age;
}
void printAge()
{
std::cout << this->age;
}
void printName()
{
std::cout << this->name;
}
};
int main()
{
Person person;
person.age = 15; // ошибка времени компиляции: нельзя обратиться к закрытому полю.
person.name = "John"; // та же ошибка
person.setAge(15); // компилируется
person.printAge(); // компилируется
person.printName(); // компилируется
return 0;
}

Одни поля можно сделать private, а другие — public.

struct Person
{
private:
int age;
public:
std::string name;
};
int main()
{
Person person;
person.age = 10; // ошибка времени компиляции: нельзя обратиться к закрытому полю.
person.name = "John"; // работает
return 0;
}

class эквивалентен struct; единственная разница в том, что в его начале подразумевается private:. То есть все члены по умолчанию публичны в struct, но приватны в class.

class — ключевое слово, которое обычно используется в контексте ООП. Хорошим тоном считается использовать class вместо struct, если вы объявляете методы, которые инкапсулируют данные (поля), предоставляя к ним четко определенные схемы доступа.

struct Person
{
private:
int age;
std::string name;
};
// то же самое, что
class Person
{
int age;
std::string name;
};
// и наоборот ...
class Person
{
public:
int age;
std::string name;
};
// то же самое, что
struct Person
{
int age;
std::string name;
};