Структуры и литье в C

Мне было интересно:

Если у меня есть определения структуры, например, вот так:

struct Base {
  int foo;
};

struct Derived {
  int foo; // int foo is common for both definitions
  char *bar;
};

Можно ли сделать что-то вроде этого?

void foobar(void *ptr) {
  ((struct Base *)ptr)->foo = 1;
}

struct Derived s;

foobar(&s);

е. г. поместите указатель void на Base *, чтобы получить доступ к его foo, когда его тип на самом деле Derived *?

Ответ 1

Многие реальные C-программы предполагают, что конструкция, которую вы показываете, является безопасной, и существует интерпретация стандарта C (в частности, правила "общей исходной последовательности", C99 §6.5.2.3 p5), в соответствии с которым оно соответствует, К сожалению, за пять лет, с тех пор как я изначально ответил на этот вопрос, все компиляторы, с которыми я легко справляюсь (например, GCC и Clang), сходились на другой, более узкой интерпретации общего правила начальной последовательности, при котором конструкция, которую вы показываете, провоцирует undefined. Конкретно, экспериментируйте с этой программой:

#include <stdio.h>
#include <string.h>

typedef struct A { int x; int y; }          A;
typedef struct B { int x; int y; float z; } B;
typedef struct C { A a;          float z; } C;

int testAB(A *a, B *b)
{
  b->x = 1;
  a->x = 2;
  return b->x;
}

int testAC(A *a, C *c)
{
  c->a.x = 1;
  a->x = 2;
  return c->a.x;
}

int main(void)
{
  B bee;
  C cee;
  int r;

  memset(&bee, 0, sizeof bee);
  memset(&cee, 0, sizeof cee);

  r = testAB((A *)&bee, &bee);
  printf("testAB: r=%d bee.x=%d\n", r, bee.x);

  r = testAC(&cee.a, &cee);
  printf("testAC: r=%d cee.x=%d\n", r, cee.a.x);

  return 0;
}

При компиляции с включенной оптимизацией (и без -fno-strict-aliasing) оба GCC и Clang предполагают, что два аргумента указателя на testAB не могут указывать на один и тот же объект, поэтому я получаю вывод, например

testAB: r=1 bee.x=2
testAC: r=2 cee.x=2

Они не делают этого предположения для testAC, но - ранее считалось, что testAB требуется скомпилировать, как если бы его два аргумента могли указывать на один и тот же объект - я уже недостаточно уверен в мое собственное понимание стандарта, чтобы сказать, гарантированно ли оно продолжать работать.

Ответ 2

Вы должны сделать

struct Base {
  int foo;
};

struct Derived {
  struct Base base;
  char *bar;
};

чтобы не нарушать строгое сглаживание; это распространенное заблуждение, что C допускает произвольные отбрасывания типов указателей: хотя он будет работать как ожидалось в большинстве реализаций, он нестандартен.

Это также позволяет избежать любой несовместимости выравнивания из-за использования директив прагмы.

Ответ 3

В особых случаях это может работать, но в целом - нет, из-за выравнивания структуры.

Вы можете использовать разные #pragma, чтобы сделать (фактически, пытаться) выравнивание идентичным - и тогда, да, это сработает.

Если вы используете визуальную студию Microsoft, вы можете найти эту статью.

Ответ 4

Это будет работать в этом конкретном случае. Поле foo в первом члене обеих структур и ударе имеет тот же тип. Однако это не так в общем случае полей внутри структуры (которые не являются первыми членами). Такие предметы, как выравнивание и упаковка, могут сделать этот разрыв тонким путем.

Ответ 5

Как вы, кажется, нацелены на объектно-ориентированное программирование на C, я могу предложить вам посмотреть следующую ссылку:

http://www.planetpdf.com/codecuts/pdfs/ooc.pdf

В нем подробно рассказывается о способах использования принципов oop в ANSI C.

Ответ 6

Есть еще одна мелочь, которая может быть полезной или связана с тем, что вы делаете.

#define SHARED_DATA int id;

typedef union base_t {
    SHARED_DATA;
    window_t win;
    list_t   list;
    button_t button;         
}

typedef struct window_t {
    SHARED_DATA;
    int something;
    void* blah;
}

typedef struct window_t {
    SHARED_DATA;
    int size;
 }

typedef struct button_t {
    SHARED_DATA;
    int clicked;
 }

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

Ответ 7

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

Во-первых, это приведение:

(struct Base *)ptr

... разрешен, но только если выполняются требования к выравниванию. Во многих компиляторах ваши две структуры будут иметь одинаковые требования к выравниванию, и их легко проверить в любом случае. Если вы преодолеете это препятствие, то следующий результат заключается в том, что результат отливки в основном не указан - то есть нет требования в стандарте C, что указатель, который когда-то был отброшен, все же относится к одному и тому же объекту (только после его возврата к оригиналу тип обязательно это сделает).

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

(Присвоения указателей рассматриваются в разделе 6.3.2.3 как стандарта C99, так и более позднего стандарта C11. По-моему, правила в обоих случаях одинаковы.)

Наконец, у вас есть так называемые правила "строгого сглаживания", с которыми можно бороться (C99/C11 6.5, пункт 7); в принципе, вам не разрешается обращаться к объекту одного типа с помощью указателя другого типа (с некоторыми исключениями, которые не применяются в вашем примере). См. Что такое правило сглаживания? или для очень углубленного обсуждения, прочитайте мой в блоге по этому вопросу.

В заключение, то, что вы пытаетесь выполнить в своем коде, не гарантируется. Можно гарантировать, что всегда будет работать с определенными компиляторами (и с некоторыми параметрами компилятора), и это может работать случайно со многими компиляторами, но оно, безусловно, вызывает поведение undefined в соответствии со стандартом языка C.

Вместо этого вы можете сделать следующее:

*((int *)ptr) = 1;

... I.e. так как вы знаете, что первый член структуры является int, вы просто указываете непосредственно на int, который обходит проблему сглаживания, так как оба типа структуры действительно содержат int по этому адресу. Вы полагаетесь на знание макета структуры, которое будет использовать компилятор, и вы все еще полагаетесь на нестандартную семантику кастинг-указателя, но на практике это значительно менее вероятно, что вы даете вам проблемы.

Ответ 8

Великая/плохая вещь о C заключается в том, что вы можете бросить практически что угодно - проблема в том, что она может не работать.:) Однако в вашем случае это будет *, поскольку у вас есть две структуры, чьи первые члены имеют один тип; см. эту программу для примера. Теперь, если struct derived имел свой новый тип в качестве своего первого элемента - например, char *bar - тогда нет, вы получите странное поведение.

* Я должен убедиться, что с "почти всегда", я полагаю; там много разных компиляторов C, поэтому у некоторых может быть другое поведение. Однако я знаю, что он будет работать в GCC.