Определение


Инкапсуляция это набор инструментов для управления доступом к данным или методам которые управляют этими данными. С детальным определением термина “инкапсуляция” можно ознакомиться в моей предыдущей публикации на Хабре по этой ссылке. Эта статья сфокусирована на примерах инкапсуляции в Си++ и Си.


Инкапсуляция в Си++


По умолчанию, в классе (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)


  1. Fedorkov
    21.03.2019 09:56
    +1

    Раз уж заговорили об ООП, в GCC можно реализовать прозрачное наследование:

    struct base
    {
       int a;
    };
    
    struct derived
    {
       struct base;
       int b;
    };
    
    int main()
    {
        struct derived x;
        x.a = 42;
        struct base* y = &x;
        printf("%d", y->a);
    }

    Работает только в GCC с параметром -fms-extensions.


    1. u_235
      21.03.2019 12:15

      x.a = 42;
      Вы про это место говорите?


      1. Fedorkov
        21.03.2019 12:33

        Да, и про обращение через базовый указатель чуть ниже.


        1. u_235
          21.03.2019 13:35

          С обращением через указатель ничего необычного нет.


    1. Habra_nik
      21.03.2019 19:30

      Это работает где угодно, начиная с C99 (или 98?). «Указатель на структуру равен указателю на первый элемент этой структуры».


      1. KanuTaH
        22.03.2019 00:58

        Нет, не работает. Без "-fms-extensions" строка

        struct base;

        внутри struct derived будет воспринята как forward declaration некоей локальной по отношению к derived структуры base (компилятор в этом месте выдаст предупреждение, что «declaration does not declare anything»), и вот в этом месте:

        x.a = 42;

        компилятор ругнется, что «struct derived has no member named a». А с "-fms-extensions" все соберется и будет работать.


    1. KanuTaH
      22.03.2019 01:04

      Ну не совсем «прозрачное», все-таки придется следить за тем, чтобы в структурах-наследниках не появлялись члены с именами, которые уже есть в структурах-предках, иначе ничего не соберется. Но в целом да, забавное расширение.


  1. playermet
    22.03.2019 06:55

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


    1. KaterynaBondarenko Автор
      22.03.2019 21:53

      да, я абсолютно с вами согласна. изменю текст статьи, спасибо