Структуры и литье в 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.