В этой статье мы поговорим о самой сложной для начинающих теме — защите классов.
1. Различие в области видимости С и C++
В языке Си при разработке больших проектов используется модульное проектирование.
Фрагменты кода распределяются по модулям и закрываются от внешнего доступа. Любые структуры данных или функции доступны только внутри модуля. Если необходимо дать внешний доступ, то в заголовочном файле объявляются доступные извне элементы. Это механизм прост и обеспечивается компилятором, то есть очень надежен.
Поэтому программист Си может спокойно работать в команде. Он знает, что ни один чужой модуль или библиотека не повлияет на его код. Только если сам специально дал доступ, но тогда сам и виноват. И это легко отследить.
Теперь рассмотрим язык C++.
С точки зрения программиста C++ весь код программы делится на следующие фрагменты:
- Базовый класс — это основа функционала, на котором дальше выстраивается иерархия классов.
- Наследуемые классы — это иерархические выстроенные классы с постепенным добавлением функционала.
- Клиентский код — это любой другой код в программе за пределами данного класса.
Если мы берем команду разработчиков, то каждый программист может играть разную роль. Например:
- Иванов — разработчик базового класса.
- Петров — наследует базовый класс, разработанный Ивановым.
- Сидоров — наследует класс Петрова, который, в свою очередь, наследует класс Иванова.
- Кузнецов — пишет свой клиентский код и просто вызывает класс Сидорова.
И тут получается такая картина. Даже если разбить программу на модули, то хотя область видимости и похожа на C, но наследование делает защиту на уровне модуля бесполезной.
В этом примере Иванов присвоил правильное значение свойству класса. Сидоров хотел воспользоваться этим свойством, но программа рухнула. При этом ни у Иванова, ни у Сидорова ошибки в программе нет. Но Петров по ошибке испортил жизнь Сидорову. То есть наследование преодолевает модульную защиту. Вся иерархия классов рушится как карточный домик.
Поэтому в языке C++ реализована сложная система защиты классов, которая позволяет программисту управлять доступом к свойствам и методам класса. Но при этом защиту должен писать сам программист.
Поэтому тему защиты класса нужно знать очень хорошо.
2. Модификаторы доступа: public, private, protected
Для защиты классов в C++ используются модификаторы доступа к свойствам и методам:
Модификатор public (общий) указывает на то, что свойства и методы доступны без ограничений.
Модификатор private (закрытый) указывает на то, что свойства и методы доступны только внутри класса.
Модификатор protected (защищенный) указывает на то, что свойства и методы доступны только наследникам класса.
Модификаторы доступа используются на уровне описания класса. То есть, когда вы проектируете класс, вы должны знать, какие участки кода вам нужно защитить.
Посмотрим на примере
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
#include <iostream> using namespace std; // Программист Иванов class Summa { public: // Доступно всем Summa() { cin >> x >> y; } int Sum() { return x + y; } private: // Закрыто для всех int x; protected: // Доступно наследникам int y; }; // Программист Петров class Summa2 : public Summa { public: int Sum2() { return Sum() * 2; } }; // Программист Сидоров class Summa3 : public Summa2 { public: int Sum3() { return Sum() / y; } }; int main() { Summa3 s3; cout << "(x + y)/y = " << s3.Sum3(); return 0; } |
Разберем этот пример с точки зрения программиста Сидорова. Так как он унаследовал базовый класс Summa, то ему доступны метод Sum и свойство y. А конструктор базового класса доступен уже потому, что срабатывает в момент создания экземпляра класса.
Для клиентского кода доступны только открытые свойства и методы, поэтому для программиста Кузнецова класс закрыт также, как если бы он был закрыт на уровне модуля в языке C.
С помощью модификаторов доступа класс уже лучше защищен, но пока мы еще не решили проблему из первой части и Петров по-прежнему может подставить ножку Сидорову, изменив свойство класса. Поэтому рассмотрим следующий уровень защиты класса.
3. Геттеры и сеттеры
Если нам нужно строго ограничить доступ к свойствам класса, то используется такой прием. Свойства полностью закрываются от внешнего доступа, то есть помещаются в защищенный раздел (private), а обращение к свойству идет через функции:
- Метод-геттер — возвращает значение.
- Метод-сеттер — устанавливает значение.
То есть для каждого свойства можно задать геттер и сеттер, в которых реализовать любые ограничения, которые нам нужны.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class Summa { public: // Доступно всем void SetX() { cin >> x; } int GetX() { return x; } private: // Закрыто для всех int x; |
Вот теперь мы можем полностью защитить класс.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
#include <iostream> using namespace std; class Summa { public: // Доступно всем Summa() { SetX(); SetY(); } void SetX() { cin >> x; } int GetX() { return x; } void SetY() { cin >> y; } int GetY() { return y; } int Sum() { return x + y; } private: // Закрыто для всех int x; int y; }; // Программист Петров class Summa2 : public Summa { public: int Sum2() { return Sum() * 2; } }; // Программист Сидоров class Summa3 : public Summa2 { public: int Sum3() { return Sum() / GetY(); } }; int main() { Summa3 s3; cout << "(x + y)/y = " << s3.Sum3(); return 0; } |
Так как обращение к свойству x теперь идет через геттер, то Петров, как бы он ни старался, обрушить программу не сможет. Вот теперь наш класс полностью защищен.
Конечно же, необходимость защиты классов значительно усложняет программирование. Программист не может сосредоточиться на функционале программы, а должен следить еще и за защитой класса. К тому же необходимость писать защиту снижает производительность работы. Тут простая арифметика, если программисту на С достаточно написать одну строчку с заданием переменной, то программисту C++ нужно написать три строчки (описание переменной, геттер, сеттер). То есть производительность работы снижается втрое.
Ну и так как человеку свойственно ошибаться, то программы на C++ отличаются слабой надежностью. В тех областях, где ошибки в ПО имеют катастрофические последствия, язык C++ не применяется. Как выразился один программист: «Я бы не хотел лететь на самолете, где софт написан на C++».