How can I hide the declaration of a struct in C?

Asked
Viewd14202

11

In the question Why should we typedef a struct so often in C?, unwind answered that:

In this latter case, you cannot return the Point by value, since its declaration is hidden from users of the header file. This is a technique used widely in GTK+, for instance.

How is declaration hiding accomplished? Why can't I return the Point by value?

ADD:

I understood why I can't return the struct by value, but, is still hard to see why i can't deference this point in my function. i.e. If my struct have member named y, why i can't do it?

 pointer_to_struct->y = some_value; 

Why should I use methods to do it? (Like Gtk+)

Thanks guys, and sorry for my bad english again.

14202

6 ответов

3

What that post means is: If you see the header

typedef struct _Point Point;

Point * point_new(int x, int y);

then you don't know the implementation details of Point.

2

Старый вопрос, ответ лучше:

В заголовочном файле:

 typedef struct _Point Point;
 

В файле C:

 struct _Point
{
   int X;
   int Y;
};
 
1

As an alternative to using opaque pointers (as others have mentioned), you can instead return an opaque bag of bytes if you want to avoid using heap memory:

// In public.h:
struct Point
{
    uint8_t data[SIZEOF_POINT];  // make sure this size is correct!
};
void MakePoint(struct Point *p);

// In private.h:
struct Point
{
    int x, y, z;
};

void MakePoint(struct Point *p);

// In private.c:
void MakePoint(struct Point *p)
{
    p->x = 1;
    p->y = 2;
    p->z = 3;
}

Then, you can create instances of the struct on the stack in client code, but the client doesn't know what's in it -- all it knows is that it's a blob of bytes with a given size. Of course, it can still access the data if it can guess the offsets and data types of the members, but then again you have the same problem with opaque pointers (though clients don't know the object size in that case).

For example, the various structs used in the pthreads library use structs of opaque bytes for types like pthread_t, pthread_cond_t, etc. -- you can still create instances of those on the stack (and you usually do), but you have no idea what's in them. Just take a peek into your /usr/include/pthreads.h and the various files it includes.

6

What he means is that you cannot return the struct by-value in the header, because for that, the struct must be completely declared. But that happens in the C file (the declaration that makes X a complete type is "hidden" in the C file, and not exposed into the header), in his example. The following declares only an incomplete type, if that's the first declaration of the struct

struct X;

Then, you can declare the function

struct X f(void);

But you cannot define the function, because you cannot create a variable of that type, and much less so return it (its size is not known).

struct X f(void) { // <- error here
  // ...
}

The error happens because "x" is still incomplete. Now, if you only include the header with the incomplete declaration in it, then you cannot call that function, because the expression of the function call would yield an incomplete type, which is forbidden to happen.

If you were to provide a declaration of the complete type struct X in between, it would be valid

struct X;
struct X f(void);

// ...
struct X { int data; };
struct X f(void) { // valid now: struct X is a complete type
  // ...
}

This would apply to the way using typedef too: They both name the same, (possibly incomplete) type. One time using an ordinary identifier X, and another time using a tag struct X.

5

В заголовочном файле:

 typedef struct _point * Point;
 

Когда компилятор это видит, он знает:

  • Существует номер struct под названием _point.
  • Существует указатель типа Point, который может ссылаться на struct _point.

Компилятор не знает:

  • Как выглядит struct _point.
  • Что содержит элементы struct _point.
  • Насколько велик struct _point?

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

Используя приведенный выше код, вы можете писать такие функции, как:

 Point f() {
   ....
}
 

потому что Point - это указатель, а struct указатель имеют одинаковый размер, и компилятору не нужно ничего о них знать. Но вы не можете написать функцию, возвращающую по значению:

 struct _point f() {
  ....
}
 

потому что компилятор ничего не знает о struct _point, в частности о его размере, который ему нужен для построения возвращаемого значения.

Таким образом, мы можем ссылаться на struct _point только через тип Point, который на самом деле является указателем. Вот почему в стандарте C есть типы вроде FILE, к которым можно получить доступ только через указатель - вы не можете создать экземпляр структуры FILE в своем коде.

33

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

В файле public.h :

 struct Point;

struct Point* getSomePoint();
 

В файле private.h :

 struct Point
{
    int x;
    int y;
}
 

В файле private.c :

 struct Point* getSomePoint()
{
    /* ... */
}
 

Если вы скомпилируете эти три файла в библиотеку, вы передадите потребителю библиотеки только public.h и файл объекта библиотеки.

getSomePoint должен возвращать указатель на Point, потому что public.h не определяет размер Point, только то, что является структурой и существует. Пользователи библиотеки могут использовать указатели на Point, но не могут получить доступ к элементам или скопировать их, потому что им неизвестен размер структуры.

Относительно вашего дальнейшего вопроса: Вы не можете разыменовать, потому что программа, использующая библиотеку, имеет только информацию из private.h , которая не содержит объявлений членов. Следовательно, он не может получить доступ к элементам точечной структуры.

Вы можете рассматривать это как функцию инкапсуляции C, точно так же, как вы объявляете члены данных класса C ++ как частные.