Скрытие информации структуры C (непрозрачный указатель)

В настоящее время я немного запутался в концепции сокрытия информации в C-структурах.

Фоном этого вопроса является встроенный проект c с почти нулевым знанием ООП.

До сих пор я всегда объявлял свои структуры typedef внутри файла заголовка соответствующего модуля. Таким образом, каждый модуль, который хочет использовать эту структуру, знает тип структуры.

Но после проверки MISRA-C я обнаружил предупреждение средней степени серьезности: MISRAC2012-Dir-4.8 - Реализация структуры без необходимости подвергается воздействию единицы перевода.

После небольшого исследования я обнаружил концепцию сокрытия информации в C-структурах, ограничивая видимый доступ членов структуры частной областью видимости.

Я быстро попробовал простой пример, который звучит так:

struct_test.h

//struct _structName;

typedef struct _structName structType_t;

struct_test.c

#include "struct_test.h"

typedef struct _structName
{
    int varA;
    int varB;
    char varC;
}structType_t;

main.c

#include "struct_test.h"

structType_t myTest;

myTest.varA = 0;
myTest.varB = 1;
myTest.varC = 'c';

Это приводит к ошибке компилятора, что для main.c размер myTest неизвестен. И, конечно же, main.c знает только о существовании структуры типа structType_t и ничего больше.

Итак, я продолжил свои исследования и наткнулся на концепцию непрозрачных указателей.

Итак, я попробовал вторую попытку:

struct_test.h

typedef struct _structName *myStruct_t;

struct_test.c

#include "struct_test.h"

typedef struct _structName
{
    int varA;
    int varB;
    char varC;
}structType_t;

main.c

#include "struct_test.h"

myStruct_t myTest;

myTest->varA = 1;

И я получаю ошибку компилятора: разыменование указателя на неполный тип struct _structName

Очевидно, я не понял основной концепции этой техники. Моя основная путаница заключается в том, где будут данные объекта структуры?

До сих пор я понимал, что указатель обычно указывает на «физическое» представление типа данных и читает / записывает содержимое по соответствующему адресу.

Но с помощью описанного выше метода я объявляю указатель myTest, но никогда не устанавливаю адрес, на который он должен указывать.

Я взял идею из этого сообщения: Что такое непрозрачный указатель в C?

В сообщении упоминается, что доступ осуществляется с помощью методов интерфейса set / get, поэтому я попытался добавить что-то подобное:

void setVarA ( _structName *ptr, int valueA )
{
  ptr->varA = valueA;
}

Но это также не работает, потому что теперь он говорит мне, что _structName неизвестно ... Итак, я могу получить доступ к структуре только с помощью дополнительных методов интерфейса, и, если да, как я могу добиться этого в моем простом примере?

И мой более важный вопрос все еще остается, где объект моей структуры находится в памяти. Я знаю только концепцию указателя:

varA - Адрес: 10 - Значение: 1

ptrA - Адрес: 22 - Значение: 10

Но в этом примере у меня есть только

myTest - Адрес: xy - Значение: ??

Мне сложно понять, где находится "физическое" представление соответствующего указателя myTest?

Более того, я не вижу преимуществ этого в относительно небольших встраиваемых проектах, где я являюсь производителем и потребителем модулей.

Может ли кто-нибудь объяснить мне, действительно ли этот метод разумен для малых и средних встроенных проектов с 1-2 разработчиками, работающими с кодом? В настоящее время кажется, что приложить больше усилий для создания всех этих методов указателя интерфейса, чем просто объявить структуру в моем заголовочном файле.

заранее спасибо


person Evox402    schedule 31.01.2020    source источник
comment
Непрозрачная версия указателя имеет тип void *, который при необходимости приводится к типу реального указателя.   -  person Ctx    schedule 31.01.2020
comment
Вот полный пример.. Для встроенных систем (и MISRA-C) вы не можете использовать malloc, но вам нужно реализовать свой собственный пул статической памяти, пример .   -  person Lundin    schedule 31.01.2020
comment
Более того, я не вижу преимуществ этого в относительно небольших встраиваемых проектах, где я являюсь производителем и потребителем модулей. Это имеет смысл, когда вы реализуете несколько сложных ADT, особенно если они являются частью HAL, с учетом соображений переносимости. Например, я использую непрозрачный шрифт, когда делаю кроссплатформенные HAL для CAN-шины библиотечного качества. Затем непрозрачная структура будет содержать данные о конкретных аппаратных периферийных устройствах и размещена в конкретном драйвере на более низком уровне, чем HAL. - ›   -  person Lundin    schedule 31.01.2020
comment
Для более простых вещей, таких как SPI и UART для конкретного проекта и т. Д., Я обычно не беспокоюсь о HAL и непрозрачных типах, а просто жестко кодирую все это с нулевой переносимостью и повторным использованием кода. Чтобы решить, когда его использовать, а когда нет, приходит с опытом проектирования системы, и как таковой, это довольно субъективно. Что касается MISRA-C, имеющего директиву для этого, основная цель - обучить и информировать вас. Обратите внимание, что Dir 4.8 носит рекомендательный характер, поэтому вы можете применять его в каждом конкретном случае.   -  person Lundin    schedule 31.01.2020
comment
Спасибо за ответы, так как мой проект не такой уж большой и нам не нужен такой масштаб независимости, я посмотрю, сможем ли мы сделать исключение из этого правила, как намекнул Лундин :)   -  person Evox402    schedule 03.02.2020


Ответы (3)


Как вы уже поняли, при использовании непрозрачного типа, такого как этот, основной исходный файл не может получить доступ к членам структуры и фактически не знает, насколько велика структура. Из-за этого вам нужны не только функции доступа для чтения / записи полей структуры, но вам также нужна функция для выделения памяти для структуры, поскольку только источник библиотеки знает определение и размер структуры.

Итак, ваш заголовочный файл будет содержать следующее:

typedef struct _structName structType_t;

structType_t *init();
void setVarA(structType_t *ptr, int valueA );
int getVarA(structType_t *ptr);
void cleanup(structType_t *ptr);

Этот интерфейс позволяет пользователю создавать экземпляр структуры, получать и устанавливать значения и очищать их. Исходный код библиотеки будет выглядеть так:

#include "struct_test.h"

struct _structName
{
    int varA;
    int varB;
    char varC;
};

structType_t *init()
{
    return malloc(sizeof(structType_t ));
}

void setVarA(structType_t *ptr, int valueA )
{
    ptr->varA = valueA;
}

int getVarA(structType_t *ptr)
{
    return ptr->varA;
}

void cleanup(structType_t *ptr)
{
    free(ptr);
}

Обратите внимание, что вам нужно определить typedef только один раз. Это и определяет псевдоним типа, и вперед объявляет структуру. Затем в исходном файле появляется фактическое определение структуры без typedef.

Функция init используется вызывающей стороной, чтобы выделить место для структуры и вернуть на нее указатель. Затем этот указатель можно передать функциям получения / установки.

Итак, теперь ваш основной код может использовать этот интерфейс следующим образом:

#include "struct_test.h"

int main()
{
    structType_t *s = init();
    setVarA(s, 5);
    printf("s->a=%d\n", getVarA(s));
    cleanup(s);l
}
person dbush    schedule 31.01.2020
comment
Спасибо, это помогло мне лучше понять концепцию :) - person Evox402; 03.02.2020
comment
Я использовал тот же пример для запуска кода, но gcc test.c (основной файл) дает мне неопределенные ссылочные ошибки для всех функций. Что я сделал не так? - person trail99; 18.05.2020
comment
@ trail99 Вам необходимо связать несколько исходных файлов вместе, т.е. gcc -o myprog test.c struct_test.c - person dbush; 18.05.2020
comment
Обратите внимание, что предлагаемый подход использует динамическое распределение памяти, что нарушает Директиву 4.12 MISRA-C. Насколько мне известно, это вопрос выбора своего яда, когда дело касается сокрытия членов объекта. - person Jonathon S.; 03.12.2020

Моя основная путаница заключается в том, где будут данные объекта структуры?

Дело в том, что вы не используете представление struct (то есть его размер, поля, макет и т. Д.) В других единицах перевода, а скорее вызываете функции, которые делают эту работу за вас. Для этого вам нужно использовать непрозрачный указатель, да.

как я могу добиться этого на моем простом примере?

Вы должны поместить все функции, которые используют поля структуры (настоящая структура) в один файл (реализация). Затем в заголовке предоставьте только интерфейс (функции, которые вы хотите, чтобы пользователи вызывали, и которые принимают непрозрачный указатель). Наконец, пользователи будут использовать заголовок для вызова только этих функций. Они не смогут вызывать какие-либо другие функции, и они не смогут узнать, что находится внутри структуры, поэтому код, пытающийся сделать это, не будет компилироваться (в этом суть!).

Более того, я не вижу преимуществ этого в относительно небольших встраиваемых проектах, где я являюсь производителем и потребителем модулей.

Это способ заставить модули быть независимыми друг от друга. Иногда его используют, чтобы скрыть реализации от клиентов или гарантировать стабильность ABI.

Но да, для внутреннего использования это обычно является обузой (и мешает оптимизации, поскольку все становится черным ящиком для компилятора, кроме случаев, когда вы используете LTO и т. Д.). Для этого лучше подходит синтаксический подход, такой как _2 _ / _ 3_ в других языках, например C ++.

Однако, если вы обязаны следовать MISRA в такой степени (т.е.если ваш проект должен следовать этому правилу, даже если он носит рекомендательный характер), вы мало что можете сделать.

Может ли кто-нибудь объяснить мне, действительно ли этот метод разумен для малых и средних встроенных проектов с 1-2 разработчиками, работающими с кодом?

Это зависит от вас. Есть очень большие проекты, которые не следуют этому совету и приносят успех. Обычно достаточно комментария для закрытых полей или соглашения об именах.

person Acorn    schedule 31.01.2020
comment
На самом деле не обязательно, чтобы все функции, использующие поля структуры, в одном файле, который может стать довольно большим в сложных случаях. В качестве альтернативы вы можете иметь определение структуры во внутреннем заголовке, который не публикуется. - person Ingo Leonhardt; 31.01.2020
comment
Однако, если вы обязаны следовать MISRA, вы мало что можете сделать. Директива относительно непрозрачного типа носит рекомендательный характер - основная цель - обучить программистов. Вы можете быть совместимы с MISRA без использования непрозрачного шрифта. - person Lundin; 31.01.2020
comment
@Lundin Действительно, я поясню, что имел в виду. - person Acorn; 31.01.2020
comment
@IngoLeonhardt Конечно, но я здесь упрощаю, так как OP не понимает модели компиляции, а его / ее простой пример - это всего лишь несколько функций, как я полагаю. - person Acorn; 31.01.2020
comment
Спасибо за ответы :) Теперь я понимаю смысл этой техники. Но я еще раз проверю, нужна ли мне 100% жалоба или я могу сделать исключение из этого правила и дважды проверить, насколько чист наш текущий метод. - person Evox402; 03.02.2020

В сообщении упоминается, что доступ осуществляется с помощью методов интерфейса set / get, поэтому я попытался добавить что-то подобное:

void setVarA ( _structName *ptr, int valueA )
{
  ptr->varA = valueA;
}

But this also doesn't work because now he tells me that _structName is unknown...

Тип не _structName, а struct _structName или (как определено) structType_t.

И мой более важный вопрос все еще остается, где объект моей структуры находится в памяти.

С помощью этого метода будет метод, который возвращает адрес такого непрозрачного объекта. Он может быть размещен статически или динамически. Конечно, также должен быть способ освободить объект.

Более того, я не вижу преимуществ этого в относительно небольших встраиваемых проектах, где я являюсь производителем и потребителем модулей.

Я согласен с тобой.

person Armali    schedule 31.01.2020