Прошлой теме было рассмотрено управление состоянием модуля на клиенте. В частности, в заголовочном файле account.h было определено следующее API модуля:
#define STR_SIZE 16 struct Account { char username[STR_SIZE]; char password[STR_SIZE]; }; struct Account* createAccount(char* username, char* pass); void printName(struct Account* acc); void deleteAccount(struct Account* acc);
Здесь определено состояние определено в виде структуры Account, которая представляет условную учетную запись и определяет два поля - для хранения соответственно имени пользователя и пароля.
Также API определяет три функции. Функция createAccount принимает логин и пароль пользователя и возвращает указатель на структуру Account. Функция printName получает указатель на структуру Account и выполняет с ней некоторые действия (выводит имя пользователя на консоль). И функция deleteAccount удаляет структуру Account.
В файле account.c была определена реализация API модуля:
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <assert.h> #include "account.h" struct Account* createAccount(char* username, char* pass) { struct Account* acc = malloc(sizeof(struct Account)); memcpy(acc->username, username, STR_SIZE); memcpy(acc->password, pass, STR_SIZE); return acc; } void printName(struct Account* acc) { if(acc) printf("User Name: %s\n", acc->username); } void deleteAccount(struct Account* acc) { if(acc) free(acc); acc = 0; }
В данном случае в функции createAccount выделяем память под структуру Account, а в функции deleteAccount освобождаем память. Функция printName имитирует некоторую работу с состоянием - вывод поля структуры Account на консоль.
В файле app.c был определен клиент, который будет хранить объект состояния и обращаться к API модуля account:
#include "account.h" int main(void) { struct Account* acc = createAccount("tom smith", "qwerty"); // создаем состояние printName(acc); // операции с состоянием deleteAccount(acc); // удаляем состояние }
Хотя этот пример прекрасно работает. Но что, если мы обратимся на клиенте к полям структуры:
#include <stdio.h> #include "account.h" int main(void) { struct Account* acc = createAccount("tom smith", "qwerty"); printf("%s\n", acc->password); // обращаемся к полю password deleteAccount(acc); }
Этот код прекрасно работает. При данной организации клиент может напрямую обращаться к полям структуры состояния. Однако это может быть нежелательно - клиент может нежелательным образом изменить состояние. Кроме того, это девальвирует API модуля. Поскольку если мы можем обращаться напрямую к полям состояния, зачем нам API?
В ряде языков программирования есть приватные переменные в классах. В Си мы также можем эмулировать подобную возможность, чтобы из внешнего кода нельзя было обращаться к полям структуры. И тут есть два варианта. Можно просто в заголовочном файле просто объявить структуру без реализации, либо объявить в заголовочном файле указатель на структуру. Рассмотрим оба случая. И сначла пропишем реализацию структуры Account непосредственно в файле модуля account.c:
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <assert.h> #include "account.h" #define STR_SIZE 16 struct Account { char username[STR_SIZE]; char password[STR_SIZE]; }; struct Account* createAccount(char* username, char* pass) { struct Account* acc = malloc(sizeof(struct Account)); memcpy(acc->username, username, STR_SIZE); memcpy(acc->password, pass, STR_SIZE); return acc; } void printName(struct Account* acc) { if(acc) printf("User Name: %s\n", acc->username); } void deleteAccount(struct Account* acc) { if(acc) free(acc); acc = 0; }
Первый способ инкапсуляции полей структуры представляет объявление структуры без ее определения в API модуля. Так, изменим API в файле account.h
struct Account; // только объявление структуры struct Account* createAccount(char* username, char* pass); void printName(struct Account* acc); void deleteAccount(struct Account* acc);
Здесь указано только объявлена структура без реализации. Теперь из кода клиента мы не сможем напрямую обращаться к полям структуры:
#include <stdio.h> #include "account.h" int main(void) { struct Account* acc = createAccount("tom smith", "qwerty"); printf("%s\n", acc->password); // ! Ошибка deleteAccount(acc); }
При попытке обратиться к полям структуры на клиенте мы получим ошибку, поскольку клиент не знает реализацию структуры Account.
Второй способ скрытия реализации структуру в принипе аналогичен и подразумевает определения указателя на структуру. Так, изменим API в файле account.h следующим образом:
typedef struct Account* UserAccount; // указатель на структуру struct Account* createAccount(char* username, char* pass); void printName(struct Account* acc); void deleteAccount(struct Account* acc);
Здесь только определен указатель на структуру, в качестве псевдонима которого выступает тип UserAccount. Но из этого определения клиент опять же не узнает о содержимом структуры. На клиенте в файле app.c мы могли бы ссылаться на указатель структуры как на тип UserAccount
#include <stdio.h> #include "account.h" int main(void) { UserAccount acc = createAccount("Tom Smith", "qwerty"); printf("%s\n", acc->password); // ! Ошибка deleteAccount(acc); }
Но при попытке обратиться к отдельным полям структуры мы опять же столкнемся ошибкой. И таким образом, взаимодействовать со структурой мы можем только через API модуля.