Определение
Инкапсуляция это набор инструментов для управления доступом к данным или методам которые управляют этими данными. С детальным определением термина “инкапсуляция” можно ознакомиться в моей предыдущей публикации на Хабре по этой ссылке. Эта статья сфокусирована на примерах инкапсуляции в Си++ и Си.
Инкапсуляция в Си++
По умолчанию, в классе (class
) данные и методы приватные (private
); они могут быть прочитаны и изменены только классом к которому принадлежат. Уровень доступа может быть изменен при помощи соответствующих ключевых слов которые предоставляет Си++.
В Си++ доступно несколько спецификаторов, и они изменяют доступ к данным следующим образом:
- публичные (
public
) данные?—?доступны всем; - защищенные (
protected
)?—?доступны только классу и дочерним классам; - приватные (
private
) —доступны только классу которому они принадлежат.
Для краткости, только два уровня (приватный и публичный) будут освещены в примерах.
Пример инкапсуляции
В классе Contact
, публичные переменные и методы доступны из основной программы (main
). Приватные переменные и методы могут прочитаны, вызваны или изменены только самим классом.
#include <iostream>
using namespace std;
class Contact
{
private:
int mobile_number; // private variable
int home_number; // private variable
public:
Contact() // constructor
{
mobile_number = 12345678;
home_number = 87654321;
}
void print_numbers()
{
cout << "Mobile number: " << mobile_number;
cout << ", home number: " << home_number << endl;
}
};
int main()
{
Contact Tony;
Tony.print_numbers();
// cout << Tony.mobile_number << endl;
// will cause compile time error
return 0;
}
Попытка напечатать или изменить приватную переменную mobile_number
из основной программы (main
) вызовет ошибку при компиляции потому как доступ к приватным данным в классе ограничен.
Нарушение инкапсуляции с Друзьями (Хорошая практика)
В Си++ присутствует ключевое слово “друг” (friend
) которое позволяет добавить исключения в общие правила доступа к данным. Если функция или класс названы другом (friend
) класса Contact?
—?они получают свободный доступ к защищенным или приватным данным.
Существует два основных правила дружбы?—?дружба не наследуется и не взаимна. Также, наличие “друзей” не изменяет уровень защищенности данных?—?приватные данные остаются приватными с исключением в виде “друга”.
#include <iostream>
using namespace std;
class Contact
{
private:
int mobile_number; // private variable
int home_number; // private variable
public:
Contact() // constructor
{
mobile_number = 12345678;
home_number = 87654321;
}
// Declaring a global 'friend' function
friend void print_numbers( Contact some_contact );
};
void print_numbers( Contact some_contact )
{
cout << "Mobile number: " << some_contact.mobile_number;
cout << ", home number: " << some_contact.home_number << endl;
}
int main()
{
Contact Tony;
print_numbers(Tony);
return 0;
}
В этом примере, функция print_numbers()
?—?обычная функция, не метод класса Contact
. Объявление функции print_numbers()
“другом” класса Contact
?—?единственная причина по которой функция print_numbers()
имеет доступ к приватным данным. Если убрать строку с определением друга?—?код не скомпилируется.
Примечание: друзьями лучше не злоупотреблять. Добавление друга стоит рассматривать как исключение, не как общую практику.
Нарушение инкапсуляции с Преобразованием типов и Указателями (Плохая практика)
Прежде всего, стоит заметить что использовать указатели и преобразование типов таким способом?—?плохая идея. Этот способ не гарантирует получения нужных данных. Он плохо читается и плохо поддерживается. Невзирая на это, он существует.
Си++ получил в наследство от Си множество инструментов, один из которых?—?преобразование типов (typecasting
). По умолчанию, все переменные и методы в классе приватные. В то же время, стандартный уровень доступа к данным в структуре (struct
)?—?публичный. Возможно создать структуру или полностью публичный класс в котором данные будут расположены идентично данным в классе Contact
и используя преобразование типов получить доступ к приватным данным.
#include <iostream>
using namespace std;
class Contact
{
private:
int mobile_number; // private variable
int home_number; // private variable
public:
Contact() // constructor
{
mobile_number = 12345678;
home_number = 87654321;
}
void print_numbers()
{
cout << "Mobile number: " << mobile_number;
cout << ", home number: " << home_number << endl;
}
};
struct Contact_struct
{
int mobile_number;
int home_number;
};
int main()
{
Contact Tony;
Contact_struct * structured_Tony;
Tony.print_numbers();
structured_Tony = (Contact_struct *) & Tony;
structured_Tony->mobile_number = 20;
structured_Tony->home_number = 30;
Tony.print_numbers();
return 0;
}
Приватные данные были прочитаны и изменены благодаря преобразованию типов
Инкапсуляция в Си
Традиционно считается что инкапсуляция?—?один из ключевых ООП принципов. Тем не менее, это не лимитирует использование этого принципа в процедурно-ориентированных языках. В Си, инкапсуляция используется давно, невзирая на отсутствие ключевых слов “приватный” и “публичный”.
Приватные переменные
В контексте инкапсуляции, все данные в Си могут быть рассмотрены как публичные по умолчанию. Уровень доступа к переменным в структурах (struct
) может быть изменен на приватный если изолировать их определение от основной программы. Нужный эффект может быть достигнут при использовании отдельных заголовочных (header, .h) и исходных (source, .c) файлов.
В данном примере, структура была определена в отдельном исходном файле “private_var.c”. Поскольку инициализация структуры в Си требует выделения и освобождения памяти, несколько вспомогательных функций были добавлены.
#include "private_var.h"
#include <stdio.h>
#include <stdlib.h>
struct Contact
{
int mobile_number;
int home_number;
};
struct Contact * create_contact()
{
struct Contact * some_contact;
some_contact = malloc(sizeof(struct Contact));
some_contact->mobile_number = 12345678;
some_contact->home_number = 87654321;
return( some_contact );
}
void delete_contact( struct Contact * some_contact )
{
free(some_contact);
}
В соответствующем заголовочном файле "private_var.h", структура Contact
была объявлена, но ее содержание осталось скрытым для основной программы.
#ifndef PRIVATE_VAR
#define PRIVATE_VAR
struct Contact;
struct Contact * create_contact();
void delete_contact( struct Contact * some_contact );
#endif /* PRIVATE_VAR */
Таким образом, для “main.c” содержание структуры неизвестно и попытки прочитать или изменить приватные данные вызовут ошибку при компиляции.
#include "private_var.h"
#include <stdio.h>
int main()
{
struct Contact * Tony;
Tony = create_contact();
// printf( "Mobile number: %d\n", Tony->mobile_number);
// will cause compile time error
delete_contact( Tony );
return 0;
}
Получение доступа к приватным переменным с Указателями
Преобразование типов может быть использовано для преодоления инкапсуляции в Си также как и в Си++, но данный подход уже был описан. Зная, что в структуре данные расположены в порядке их декларации, указатели и арифметика указателей подойдет для достижения цели.
Доступ к переменным в структуре ограничен. Тем не менее, спрятаны только переменные, не память в которой хранятся данные. Указатели можно рассматривать как ссылку на адрес памяти, и если эта память доступна программе?—?данные сохраненные в этой памяти можно прочитать и изменить. Если указатель назначен на память в которой структура хранит свои данные?—?их можно прочитать. Используя то же определение структуры (те же “.c” и “.h” файлы) и модифицированный “main.c” файл, ограничение доступа было преодолено.
#include "private_var.h"
#include <stdio.h>
int main()
{
struct Contact * Tony;
Tony = create_contact();
int * mobile_number_is_here = (int *)Tony;
printf("Mobile number: %d\n", *mobile_number_is_here);
int * home_number_is_here = mobile_number_is_here + 1;
*home_number_is_here = 1;
printf("Modified home number: %d\n", *home_number_is_here);
delete_contact( Tony );
return 0;
}
Данные в структуре были прочитаны и модифицированы
Приватные функции
Функции, будучи внешними (extern
) по умолчанию, видимы во всей так называемой единице трансляции (translation unit
). Другими словами, если несколько файлов скомпилированы вместе в один объектный файл, любой из этих файлов сможет получить доступ к любой функции из любого другого файла. Использование ключевого слова “статический” (static
) при создании функции ограничит ее видимость до файла в котором она была определена.Следовательно, для обеспечения приватности функции необходимо выполнить несколько шагов:
- функция должна быть объявлена статической (
static
) либо в исходном файле (.c), либо в соответствующем заголовочном файле (.h); - определение функции должно находиться в отдельном исходном файле.
В данном примере, в файле “private_funct.c”, была определена статическая функция print_numbers()
. К слову, функция delete_contact()
успешно вызывает print_numbers()
поскольку они находятся в одном файле.
#include "private_funct.h"
#include <stdio.h>
#include <stdlib.h>
struct Contact
{
int mobile_number;
int home_number;
};
struct Contact * create_contact()
{
struct Contact * some_contact;
some_contact = malloc(sizeof(struct Contact));
some_contact->mobile_number = 12345678;
some_contact->home_number = 87654321;
return( some_contact );
}
static void print_numbers( struct Contact * some_contact )
{
printf("Mobile number: %d, ", some_contact->mobile_number);
printf("home number = %d\n", some_contact->home_number);
}
void delete_contact( struct Contact * some_contact )
{
print_numbers(some_contact);
free(some_contact);
}
В соответствующем заголовочном файле "private_funct.h", print_numbers()
была декларирована как статическая функция.
#ifndef PRIVATE_FUNCT_H
#define PRIVATE_FUNCT_H
struct Contact;
struct Contact * create_contact();
static void print_numbers( struct Contact * some_contact );
void delete_contact( struct Contact * my_points );
#endif /* PRIVATE_FUNCT_H */
Основная программа, “main.c”, успешно вызывает print_numbers()
опосредовательно через delete_contact()
, поскольку обе функции находятся в одном документе. Тем не менее, попытка вызвать print_numbers()
из основной программы вызовет ошибку.
#include "private_funct.h"
#include <stdio.h>
int main()
{
struct Contact * Tony;
Tony = create_contact();
// print_numbers( Tony );
// will cause compile time error
delete_contact( Tony );
return 0;
}
Получение доступа к приватным функциям
Вызвать функцию print_numbers()
из основной программы возможно. Для этого можно использовать ключевое слово goto
или передавать в main
указатель на приватную функцию. Оба способа требуют изменений либо в исходном файле “private_funct.c”, либо непосредственно в теле самой функции. Поскольку эти методы не обходят инкапсуляцию а отменяют её, они выходят за рамки этой статьи.
Заключение
Инкапсуляция существует за пределами ООП языков. Современные ООП языки делают использование инкапсуляции удобным и естественным. Существует множество способов обойти инкапсуляцию и избежание сомнительных практик поможет ее сохранить как в Си, так и в Си++.
Комментарии (9)
playermet
22.03.2019 06:55поскольку Си++ совместим с Си, примеры использованные в Си могут применены в обоих языках.
Нет, не могут, если только под «применить» не подразумевалось «скомпилировать в буквальном виде». Нельзя просто взять и перенести объявление полей класса в cpp файл, потому что плюсам нужно знать реальный размер класса для его аллокации, а для этого они должны быть в заголовочном файле. Чтобы скрыть поля в C++ схожим образом нужно использовать технику PIMPL.KaterynaBondarenko Автор
22.03.2019 21:53да, я абсолютно с вами согласна. изменю текст статьи, спасибо
Fedorkov
Раз уж заговорили об ООП, в GCC можно реализовать прозрачное наследование:
Работает только в GCC с параметром -fms-extensions.
u_235
x.a = 42;
Вы про это место говорите?
Fedorkov
Да, и про обращение через базовый указатель чуть ниже.
u_235
С обращением через указатель ничего необычного нет.
Habra_nik
Это работает где угодно, начиная с C99 (или 98?). «Указатель на структуру равен указателю на первый элемент этой структуры».
KanuTaH
Нет, не работает. Без "-fms-extensions" строка
внутри struct derived будет воспринята как forward declaration некоей локальной по отношению к derived структуры base (компилятор в этом месте выдаст предупреждение, что «declaration does not declare anything»), и вот в этом месте:
компилятор ругнется, что «struct derived has no member named a». А с "-fms-extensions" все соберется и будет работать.
KanuTaH
Ну не совсем «прозрачное», все-таки придется следить за тем, чтобы в структурах-наследниках не появлялись члены с именами, которые уже есть в структурах-предках, иначе ничего не соберется. Но в целом да, забавное расширение.