Как стать автором
Обновить

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

Программирование *C++ *C *


Определение


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


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


По умолчанию, в классе (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”, либо непосредственно в теле самой функции. Поскольку эти методы не обходят инкапсуляцию а отменяют её, они выходят за рамки этой статьи.


Заключение


Инкапсуляция существует за пределами ООП языков. Современные ООП языки делают использование инкапсуляции удобным и естественным. Существует множество способов обойти инкапсуляцию и избежание сомнительных практик поможет ее сохранить как в Си, так и в Си++.

Теги:
Хабы:
Всего голосов 20: ↑10 и ↓10 0
Просмотры 45K
Комментарии 10
Комментарии Комментарии 10

Публикации

Истории

Работа

Программист C++
121 вакансия
QT разработчик
8 вакансий
Программист С
53 вакансии