Структуры. Режимы доступа. Сеттеры и геттеры

Практический курс по C/C++: https://stepik.org/course/193691

Продолжаем тему структур языка С++. На прошлом занятии мы с вами увидели, что в структурах можно объявлять методы и вызывать их через объект-структуру (переменные структуры):

#include <iostream>
#include <math.h>
 
using std::cout;
using std::endl;
 
struct point {
    int x, y;
 
    double length() { return sqrt(x*x + y*y); }
};
 
int main()
{
    struct point pt {1, 2};
    cout << pt.length() << endl;
    return 0;
}

Причем, значения полей x и y мы можем задавать, как при инициализации, так и через переменную pt, ровно так, как это было и в языке Си:

    pt.x = 10;
    pt.y = 20;

Однако это не всегда желаемое поведение. Например, у нас может быть ограничение, что точка типа point принимает значения координат из строго заданного диапазона. Для примера возьмем его [-100; 100]. Тогда непосредственное изменение состояния объекта pt через переменные x и y легко может привести к нарушению этого требования. Например:

pt.x = 153;  // за пределами [-100; 100]

Как же быть? Для этого в структурах и классах языка С++ дополнительно можно определять режимы доступа к тем или иным полям структуры (класса). На данный момент мы рассмотрим два таких режима:

  • public – публичный доступ к переменным и методам;
  • private – частный (закрытый) доступ к переменным и методам.

Например, если нам нужно закрыть прямой доступ извне для каких-либо полей структуры, то достаточно объявить их как private следующим образом:

struct point {
private:
    int x, y;
public:
    double length() { return sqrt(x*x + y*y); }
};

То есть, пишется ключевое слово private и ставится двоеточие. Все, что следует ниже, попадает в раздел private до тех пор, пока не встретится какой-либо другой режим доступа. Так как мы хотим, чтобы метод length() был общедоступным, то необходимо явно прописать режим public после private.

Давайте посмотрим, к чему это приведет. Теперь мы не можем выполнять инициализацию объекта pt, так как инициализируемые поля x и y скрыты от внешнего доступа. Также мы не можем изменять значения этих полей, обращаясь к ним напрямую через объект pt:

pt.x = 153;  // ошибка

А вот вызывать метод length() по-прежнему можно:

double res = pt.length(); // ok

Причем, внутри метода length() обращение к приватным переменным x и y разрешено. То есть, режим доступа private запрещает работать с переменными x и y напрямую извне структуры point, но не запрещает делать это изнутри. А режим public позволяет прямое обращение как извне структуры, так и внутри нее. По умолчанию все поля структуры помечаются как public. Именно поэтому на прошлом занятии мы даже не подозревали о существовании каких-то режимов доступа.

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

И еще одно важное замечание. Защита private определяется на уровне типа данных – структуры целиком, а не на уровне отдельных объектов. О чем здесь речь? Смотрите, если в нашей структуре point объявить еще один метод, например, sum() для сложения одного вектора с другим:

struct point {
private:
    int x, y;
public:
    double length() { return sqrt(x*x + y*y); }
    void sum(const point& pt)
    {
        this->x += pt.x;
        this->y += pt.y;
    }
};

То внутри метода sum мы можем совершенно спокойно обращаться к переменным x и y через переданный объект pt. И все благодаря тому, что функция-член sum() принадлежит типу данных point. Она находится, как бы, внутри него. А раз, так, то автоматически получает доступ ко всем приватным полям этой структуры, даже если они берутся из другого объекта. Поэтому и говорят, что защита действует на уровне типа данных, а не на уровне объектов.

Конечно, полученная структура point, на данный момент не пригодна для практического использования. Мы не можем задавать нужные нам координаты и читать их. Самый очевидный шаг, как это можно было бы поправить – это определить соответствующие публичные методы. Например, следующим образом:

struct point {
private:
    int x, y;
public:
    double length() { return sqrt(x*x + y*y); }
    void sum(const point& pt)
    {
        this->x += pt.x;
        this->y += pt.y;
    }
 
    void set_coords(int x, int y) 
    { 
        if(x < -100 || x > 100 || y < -100 || y > 100)
            return;
 
        this->x = x; 
        this->y = y; 
    }
 
    void get_coords(int& x, int& y) {x = this->x; y = this->y; }
    int get_x() { return this->x; }
    int get_y() { return this->y; }
};

Обратите внимание, что в методах set_coords() и get_coords() для обращения к переменным x и y текущего объекта необходимо использовать неявный указатель this. Если бы мы прописали просто x, то это соответствовало бы локальному параметру x функции, а не переменной объекта. Поэтому в таких случаях, когда локальные переменные внутри метода совпадают по именам с полями структуры, для обращения к полям необходимо использовать указатель this на текущий объект.

Теперь мы можем передавать координаты и читать их, используя публичные методы:

int main()
{
    struct point pt;
    
    pt.set_coords(1, 2);
    cout << pt.get_x() << " " << pt.get_y() << endl;
 
    double res = pt.length();
    cout << res << endl;
    
    return 0;
}

В ООП методы, которые служат для установки значений переменных объекта, получили название сеттеры (от префикса set, который часто записывается вначале таких методов), а методы, с помощью которых читаются значения переменных объекта – геттерами (от префикса get).

Однако и в такой реализации структура point все еще остается недоработанной. Это связано с тем, что когда мы создаем объект pt, то его начальное состояние оказывается неопределенным из-за неизвестных значений координат x и y. Но следующие улучшения мы будем делать уже на следующем занятии.

Практический курс по C/C++: https://stepik.org/course/193691

Видео по теме