Является ли законным иметь прототип функции, немного отличающийся от её определения?

Вопрос или проблема

//my_struct.h

typedef struct my_struct_t *my_handle;

void f1(my_handle handle);

void f2(my_handle handle);
//my_struct.c
#include "my_struct.h"

typedef struct
{
    int a;
    int b;
} my_struct_t;

//Является ли это определение законным?
void f1(my_struct_t *handle)
{
    //bla bla
}

//Является ли это определение законным?
void f2(const my_struct_t *handle)
{
    //bla bla
}
//main.c
#include "my_struct.h"

void g1(my_handle handle)
{
    //Является ли этот вызов законным?
    f1(handle);
    
    //Является ли этот вызов законным?
    f2(handle);
}

В данном конкретном случае my_handle является псевдонимом для struct my_struct_t *, так что я не вижу причин, почему указанный код должен быть незаконным.

GCC не дает мне скомпилировать этот код, так что я полагаю, что он незаконен.

Что говорит стандарт ISO c99 об этом?

Правка

Это ошибки, которые вызывает GCC:
https://onlinegdb.com/6hg40rTsV

my_struct.c:11:6: error: conflicting types for ‘f1’; have ‘void(my_struct_t *)’
   11 | void f1(my_struct_t *handle)
      |      ^~
In file included from my_struct.c:1:
my_struct.h:5:6: note: previous declaration of ‘f1’ with type ‘void(struct my_struct_t *)’
    5 | void f1(my_handle handle);
      |      ^~
my_struct.c:17:6: error: conflicting types for ‘f2’; have ‘void(const my_struct_t *)’
   17 | void f2(const my_struct_t *handle)
      |      ^~
In file included from my_struct.c:1:
my_struct.h:7:6: note: previous declaration of ‘f2’ with type ‘void(struct my_struct_t *)’
    7 | void f2(my_handle handle);
      |      ^~

Проблема в том, что

  • typedef struct my_struct_t *my_handle; и
  • typedef struct { ... } my_struct_t;

не ссылаются на один и тот же тип.

Первый объявляет псевдоним my_handle, который является предварительным объявлением указателя на struct my_struct_t, структуры с тегом – допустимо само по себе. Второй объявляет псевдоним my_struct_t, который является псевдонимом для другой структуры без тега.

Нигде в коде вы не определяете struct my_struct_t. Теги структур находятся в собственном пространстве имен, называемом пространством имен тегов, в то время как typedef находятся в обычном пространстве имен. (C и C++ отличаются в этом.)

Исправление довольно простое: просто добавьте то же имя тега, что использовалось в предварительном объявлении, в определение структуры:

typedef struct my_struct_t
{
    int a;
    int b;
} my_struct_t;

Однако гораздо лучшее решение – никогда не скрывать указатели за помощью typedef, потому что это всего лишь запутывает программиста, заставляя думать, что он передает что-то по значению, когда это не так.

И еще лучше, не используйте 2 разные имена для одной и той же вещи, потому что единственное, чего вы достигаете, это также запутывает программиста. Почему у вас есть что-то под названием my_handle в заголовке, называемом my_struct? Это не имеет смысла.

// my_struct.h
typedef struct my_struct_t my_struct_t;

// my_struct.c
struct my_struct_t
{
    int a;
    int b;
};

void f1(my_struct_t* handle);

void f1(my_struct_t* handle)
{
}

И теперь, как бывает, это паттерн проектирования, известный как “непрозрачный тип” – способ сделать приватную инкапсуляцию в C.

f1 не является допустимым и не должен даже компилироваться. my_struct_t является именем псевдонима, а не именем структуры. По той же простой причине f2 также не является допустимым.

Если вы измените определение структуры в исходном коде на struct my_struct_t { ... }; и измените определения функций (не декларации), чтобы они принимали struct my_struct_t, то f1 будет компилироваться и работать нормально, но f2 все равно не скомпилируется, потому что определение не будет соответствовать декларации.

Если вы не позволите исходному файлу видеть заголовок, то, вероятно, не будет ошибок компиляции или линковки, но поведение кода будет неопределенным, так как произойдет нарушение правила единственного определения между декларациями f2 (одна видима для main.c, другая для my_struct.c).

Ответ или решение

Есть ли законность в различии прототипов функции и их определений?

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

Ситуация в вашем коде

Рассмотрим предоставленный вами код:

// my_struct.h
typedef struct my_struct_t *my_handle;

void f1(my_handle handle);
void f2(my_handle handle);

Теперь заглянем в реализацию функцій в файле my_struct.c:

// my_struct.c
#include "my_struct.h"

typedef struct {
    int a;
    int b;
} my_struct_t;

void f1(my_struct_t *handle) {
    // bla bla
}

void f2(const my_struct_t *handle) {
    // bla bla
}

И, наконец, основные вызовы в main.c:

// main.c
#include "my_struct.h"

void g1(my_handle handle) {
    f1(handle); // юридичность вызова?
    f2(handle); // юридичность вызова?
}

В данном случае my_handle является псевдонимом для указателя на struct my_struct_t. Однако в вашем коде определение my_struct_t не совпадает с тем, что ожидает компилятор.

Причина проблемы

Ваша структура определена как typedef struct { ... } my_struct_t;, и в этом контексте my_struct_t не является указателем на struct my_struct_t. Следовательно, компилятор не может сопоставить аргументы функций f1 и f2 с теми, которые они ожидают на основе прототипов, объявленных в my_struct.h. Это приводит к следующим ошибкам компиляции:

error: conflicting types for ‘f1’; have ‘void(my_struct_t *)’
error: conflicting types for ‘f2’; have ‘void(const my_struct_t *)’

Соответствие стандарту ISO C99

Согласно стандарту ISO C99, все объявления функций должны согласовываться с их определениями. В противном случае, это будет рассматриваться как нарушение, и компилятор не позволит вам успешно скомпилировать код.

Функции f1 и f2 имеют типы, которые не соответствуют тем, что объявлены с использованием my_handle. Это приводит к несоответствию, и, как следствие, к ошибкам компиляции, которые вы наблюдаете.

Решение проблемы

Чтобы исправить данную ситуацию и сделать код корректным, можно изменить файл my_struct.h следующим образом:

typedef struct my_struct_t {
    int a;
    int b;
} my_struct_t;

typedef my_struct_t* my_handle; // теперь это корректно

Согласовав определение структуры и псевдонима, можно гарантировать, что все ссылки на структуры будут работать корректно.

Заключение

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

Оцените материал
Добавить комментарий

Капча загружается...