Иногда необходимо предоставить доступ к данным извне таким образом, чтобы они не могли быть изменены и были доступны только для чтения.
Стандартным решением в этом случае будет определение данных в статической памяти. Данные могут определяться как на этапе компиляции, так и на этапе запуска приложения, и после этого они не изменяются. А для доступа к данным определяется функция, которая возвращает указатель на эти данные. Причем возвращается указатель на константу, чтобы данные не могли быть изменены извне. В общем случае это выглядит следующим образом. На одной стороне - стороне источника данных определяются собственно данные и функция для доступа к ним:
// структура, которая представляет неизменяемые данные struct Immutable { int x; int y; }; // данные, которые храняться в статической памяти static struct Immutable data = {11, 22}; // функция, которая возвращает ссылку на данные const struct Immutable* getData() { return &data; }
На другой стороне - стороне приемника данных вызываем выше определенную функцию и получаем данные в указатель на константу:
const struct Immutable* immut_data; // чтобы данные не изменились, определяем указатель на константу immut_data = getData(); // получем данные из источника данных
Подобный паттерн иногда называют Immutable Object или Immutable Instance. Рассмотрим простейший вариант применения. Пусть у нас есть файл user.h, где определяется структура используемых данных:
struct User{ char* name; unsigned age; char* email; };
Структура User имеет три поля. Это те данные, которые будут представлять неизменяемые данные.
Определим файл user.c, который определяет неизменяемые данные и API для доступа к ним:
#include "user.h" static struct User data = {"Tom", 39, "tom@smail.ru"}; const struct User* getUser(void) { return &data; }
Здесь данные представляют объект data, который представляет структуру User и который размещен в статической памяти. Обратите внимание, что это не константный объект. Он может быть установлен и в процессе работе программы и в принципе даже может быть изменен. Однако он определен с помощью ключевого слова static, поэтому напрямую (без применения указателей) из вне файла user.c не виден.
Для доступа к этому объекту определена функция getUser, которая возвращает указатель на константу - адрес объекта data.
И также определим основной файл приложения app.c, в котором получаем этот объект:
#include <stdio.h> #include "user.h" extern const struct User* getUser(void); int main(void) { const struct User* user; // получаемый объект представляет указатель на константу user = getUser(); // получаем объект в константу printf("name=%s\n", user->name); printf("email=%s\n", user->email); printf("age=%d\n", user->age); }
Здесь вызываем функцию getUser из файла "user.c" и получаем данные в указатель на константу.
Скомпилируем оба файла в приложение и запустим его:
c:\C>gcc -Wall -pedantic app.c user.c -o app c:\C>app name=Tom emai=tom@smail.ru age=39 c:\C>
Таким образом, компонент-потребитель данных (код в файле app.c) не должен заботиться об управлении памяти, посольку данные определены в статической памяти. Данный подход может успешно использоваться в многопоточных приложениях, поскольку потоки не изменяют данные, а только читают, и для всех обращающих потоков данные остаются теми же.
Однако здесь также есть недостатки. Например, что если код в файле app.c будет получать указатель на неконстантый объект:
struct User* user = getUser(); user->name = "Bob";
Хотя при компиляции компиляторы могут выдать предупреждение, но код скомпилируется, нормально отработает, а значения по полученному указателю изменятся.
Кроме того, данные могут измениться на стороне компонента user.c. Соответственно в данном случае мы имеем дело с относительной неизменяемостью данных.
Стоит отметить, что мы могли бы пойти дальше в сторону инкапсуляции и определить объект в user.c в самой возвращаеющей его функции:
#include <stdio.h> #include "user.h" const struct User* getUser(void) { static struct User data = {"Tom", 39, "tom@smail.ru"}; return &data; } void printUser(void){ const struct User* user; user = getUser(); printf("name=%s\n", user->name); printf("emai=%s\n", user->email); printf("age=%d\n", user->age); }
И также внутри "user.c" для получения объекта вызываем функцию getUser и получаем указатель на константу.