Конструкции и литье в C - PullRequest
       17

Конструкции и литье в C

10 голосов
/ 02 октября 2010

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

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

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 *?

Ответы [ 8 ]

11 голосов
/ 02 октября 2010

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

struct Base {
  int foo;
};

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

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

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

7 голосов
/ 02 октября 2010

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

#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 требовалось скомпилировать какесли два аргумента могут указывать на один и тот же объект - я больше не уверен в своем собственном понимании стандарта, чтобы сказать, действительно ли что гарантированно продолжит работать.

1 голос
/ 02 октября 2010

Поскольку вы, похоже, стремитесь к объектно-ориентированному программированию в CI, можно предложить вам взглянуть на следующую ссылку:

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

В ней подробно рассматриваются способы обработкиООП принцип в ANSI C.

1 голос
/ 02 октября 2010

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

1 голос
/ 02 октября 2010

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

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

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

0 голосов
/ 06 сентября 2015

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

Во-первых, этот актерский состав:

(struct Base *)ptr

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

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

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

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

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

Что вы могли бы сделать вместо этого:

*((int *)ptr) = 1;

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

0 голосов
/ 02 октября 2010

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

#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 для хранения только «идентификатора класса» или хранения указателя. В любом случае это оказалось в какой-то момент мне пригодится общая обработка типов событий. Надеюсь, я не буду слишком не в теме с этим

0 голосов
/ 02 октября 2010

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...