Указатели на аннулированные указатели в C - могу ли я использовать void ** для рудиментарного полиморфизма? - PullRequest
4 голосов
/ 08 февраля 2010

Я могу понять, как void** может выглядеть в памяти, но мне интересно, правильно ли я его использую. Есть ли фундаментальные недостатки в том, что я опишу ниже? Например, хотя я могу сказать «это работает для меня», я каким-то образом создаю плохой / непортативный код?

Итак, у меня есть клон астероидов. Есть три сущности, которые могут стрелять пулями, игроки (SHIP *player_1, SHIP *player_2) и НЛО (UFO *ufo). Когда стреляет пуля, важно знать, кто выстрелил; если это был игрок, когда он что-то поражает, их счет должен быть увеличен. Таким образом, пуля будет хранить, к какому типу сущности она принадлежит (owner_type), а также указатель непосредственно на владельца (owner):

enum ShipType
{
    SHIP_PLAYER,
    SHIP_UFO
};

typedef struct Bullet
{ 
    // ...other properties
    enum ShipType owner_type;
    void **owner;
} BULLET;

Затем, когда игрок нажимает кнопку или НЛО видит цель, будет вызвана одна из этих функций:

void ship_fire(SHIP **shipp)
{
    BULLET *bullet = calloc(1, sizeof(BULLET));
    bullet->owner_type = SHIP_PLAYER;
    bullet->owner = (void**)shipp;
    // do other things
}

void ufo_fire(UFO **ufop)
{
    BULLET *bullet = calloc(1, sizeof(BULLET));
    bullet->owner_type = SHIP_UFO;
    bullet->owner = (void**)ufop;
    // do other things
}

... их можно назвать, например, так:

ship_fire(&player_1);

Наконец, когда пуля попадает в цель (например, астероид), мы разыменовываем владельца. Если это корабль, мы можем тут же увеличить счет.

void hit_asteroid(ASTEROID *ast, BULLET *bullet)
{
    SHIP *ship_owner;
    if (bullet->owner_type == SHIP_PLAYER && *bullet->owner != NULL)
    {
        ship_owner = (SHIP*)*bullet->owner;
        ship_owner->score += 1000;
    }
}

Это кажется разумным подходом? Как я уже сказал, это работает для меня, но у меня есть только несколько месяцев опыта работы с C.

Последнее замечание: почему я не использую void* вместо void**? Потому что я хочу избежать висящих указателей. Другими словами, скажите, что player_1 умирает и свободен, но их пуля продолжает двигаться и попадает в астероид. Если у меня есть только void*, функция hit_asteroid не может знать, что bullet->owner указывает на нераспределенную память. Но с void** я могу достоверно проверить, имеет ли он значение NULL; если player_1 равно NULL, то *bullet->owner также будет равно NULL.

РЕДАКТИРОВАТЬ: Все респонденты до сих пор согласны с тем, что использование void **, вероятно, не является необходимым здесь, потому что я могу избежать проблемы висячих указателей (например, просто статическое размещение базового объекта). Они верны, и я сделаю рефакторинг. Но мне все еще интересно узнать, использовал ли я void ** таким образом, что мог бы что-то сломать, например. с точки зрения выделения памяти / приведения. Но я думаю, что если никто не поднял руки в воздух и не заявил, что это неисправно, то это, по крайней мере, похоже на то, что технически сработает.

Спасибо!

Ответы [ 6 ]

6 голосов
/ 09 февраля 2010

Даже если вы хотите продолжать делать то же самое, что и раньше, вам не нужно использовать void ** (и не следует).

Хотя void * является универсальным типом указателя, void ** - это , а не универсальный тип указателя на указатель - он всегда должен указывать на подлинный объект void *.Ваш код разыменовывает указатель SHIP ** или UFO ** через lvalue типа void ** - технически это не гарантирует работу.(Это происходит, когда вы делаете (SHIP*)*bullet->owner).

Тем не менее, хорошая новость заключается в том, что вы можете продолжать использовать метод двойного указателя, используя простой void * для выполнения этой работы.void * может с радостью хранить указатель на указатель (потому что это, в конце концов, просто еще один вид указателя).Если вы измените owner на void *, то в ship_fire вы сделаете это:

bullet->owner = shipp;

и в hit_asteroid вы сделаете это:

ship_owner = *(SHIP **)bullet->owner;

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

4 голосов
/ 08 февраля 2010

Ядро Linux делает это интересным образом.Это будет что-то вроде

/**
 * container_of - cast a member of a structure out to the containing structure
 * @ptr:        the pointer to the member.
 * @type:       the type of the container struct this is embedded in.
 * @member:     the name of the member within the struct.
 *
 */
#define container_of(ptr, type, member) ({                      \
        const typeof( ((type *)0)->member ) *__mptr = (ptr);    \
        (type *)( (char *)__mptr - offsetof(type,member) );})


typedef struct Ship {
     void (*fire)(struct Ship * shipp);
     /* ...other methods...*/
} SHIP;

#define playership_of(shipp) container_of(shipp, PLAYERSHIP, ship)
#define ufoship_of(shipp) container_of(shipp, UFOSHIP, ship)

typedef struct PlayerShip {
    /* PlayerShip specific stuff ...*/
    SHIP ship;
    /*...*/
} PLAYERSHIP;

typedef struct UFOShip {
    /*...UFO stuff...*/
    SHIP ship;
    /*...*/
} UFOSHIP;

void ship_fire(SHIP * shipp)
{
     shipp->fire(shipp);
}

void player_fire(SHIP *shipp)
{
    PLAYERSHIP * ps = playership_of(shipp);
    BULLET *bullet = calloc(1, sizeof(BULLET));
    bullet->owner = shipp;
    // do other things
}

void ufo_fire(SHIP * shipp)
{
    UFOSHIP * ufos = ufoship_of(shipp);
    BULLET *bullet = calloc(1, sizeof(BULLET));
    bullet->owner = ufop;
    // do other things
}

UFOSHIP ufoship = { /*...*/ .ship = { .fire = ufo_fire } /* ... */ };
PLAYERSHIP playership = { /*...*/ .ship = { .fire = player_fire } /*...*/ };


/* ... */
ship_fire(&playership.ship);

Прочитайте исходный код ядра Linux для множества примеров этой техники.

2 голосов
/ 08 февраля 2010

Прежде всего, проверьте объединенную конструкцию , предложенную mipadi; это очень удобочитаемый и эффективный способ борьбы с полиморфизмом .

Ближе к вашему фрагменту / вопросу, на первый взгляд, я не вижу необходимости / использования для двойной косвенности, вводимой указателями на указатели. Вся логика работала бы одинаково, если бы аргументы методов xxxx_fire () были [прямыми] указателями на объекты xxxx (и если бы типизация и т. Д. В остальной логике следовали соответствующим образом.

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

Редактировать : (при использовании двойного косвенного обращения для управления «парками» объектов, которые могут быть освобождены .
Отвечая на ваш комментарий, сделайте , а не рефакторинг, чтобы объекты не были перераспределены (из памяти), когда их убили / уничтожили (как часть игры). Вместо , посмотрите на что-то вроде следующего, так как это действительно пример, где конструкция указатель на указатель очень помогает. Вот как это может работать:

  • При инициализации игры (или уровня) выделите массив указателей , достаточно большой, чтобы содержать столько указателей, сколько общее количество объектов, которые может выделить игра, со временем. Инициализируйте все его значения в NULL.
  • Ввести индекс значения int, который указывает расположение следующего доступного (= неиспользованного) указателя в этом массиве.
  • Когда создается новый объект (НЛО, Корабль или что у вас есть), происходят четыре вещи:
    • новая память выделена для объекта как такового
    • адрес этой новой памяти хранится в массиве указателей объектов (в месте, указанном индексом)
    • индекс увеличивается
    • "мир" знает этот объект только по двойной косвенности
  • когда объект разрушается, происходят две вещи
    • память освобождена
    • указатель в массиве установлен в нуль
  • при доступе к любым объектам программа делает три вещи
    • первая разыменование (один раз) указатель на указатель
    • проверьте, имеет ли это значение null (если это так, это указывает на то, что объект больше не существует, логика может решить удалить эту ссылку из того места, где она хранилась, чтобы не пытаться снова, но это, конечно, необязательно).
    • доступ к реальному объекту путем разыменования промежуточного указателя (если он не равен NULL)

В понимании, короткий фрагмент на языке C, возможно, был более явным; извините, я описал это словами ...

2 голосов
/ 08 февраля 2010

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

typedef struct Bullet {
    enum ShipType owner_type;
    union {
        SHIP *ship;
        UFO *ufo;
    } owner;
} BULLET;

/* and then... */

void hit_asteroid(ASTEROID *ast, BULLET *bullet)
{
    SHIP *ship_owner;
    if (bullet->owner_type == SHIP_PLAYER && bullet->owner.ship != NULL) {
        ship_owner = bullet->owner.ship;
        ship_owner->score += 1000;
    }
}

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

1 голос
/ 08 февраля 2010

Если ваши владельцы пуль часто меняются (например, освобождаются), подход «указатель-указатель» подходит. Решение профсоюза не решает эту проблему напрямую; как представлено, он не поддерживает освобождение кораблей, не касаясь указателя на каждой из пуль этого корабля. Конечно, это может быть практическим решением в некоторых реализациях, например если вам нужно найти все маркеры данного игрока, вы можете сохранить их связанный список: указатель «next_bullet» для каждой пули и указатель «last_bullet» на заголовок списка для каждого игрока.

И вместо того, чтобы выделять каждую пулю отдельно, я бы также следовал предложению mjv предварительно выделить некоторое количество из них и выбрать следующий доступный. В реализации связанного списка вы можете использовать те же указатели «next_bullet», чтобы поддерживать один список предварительно распределенных маркеров, которые в настоящее время не используются. Преимущество этого подхода состоит в том, что вы можете легко распределять больше, если у вас закончились ресурсы, вместо того, чтобы поддерживать массив, т.е. если список доступных маркеров пуст, просто добавьте их в список по требованию. Аналогично, поместите «просроченные» (взорвавшиеся?) Маркеры обратно в список доступных, и выделенная сумма автоматически адаптируется к тому, сколько потребуется.

Еще одна вещь, которая приходит на ум, это то, что вам, возможно, не нужно знать, какой именно НЛО (или другой враг) владеет данной пулей; просто используйте один указатель (например, SHIP **) для игрока-владельца и установите его в NULL для всех неигровых пуль. Если это не подходит, вы можете также рассмотреть возможность сохранения типа каждого владельца в начале самой структуры структуры владельца, например ::10000

enum EntityType { TYPE_PLAYER_SHIP, TYPE_UFO_SHIP, TYPE_BULLET, };

struct GameEntity {
    enum EntityType type;
    // This struct is not actually used, it just defines the beginning
};

struct Ship {
    enum EntityType type; // Set to TYPE_PLAYER_SHIP when allocating!
    …
};

struct UFO {
    enum EntityType type; // Set to TYPE_UFO_SHIP when allocating!
    …
};

struct Bullet {
    enum EntityType type;  // Set to TYPE_BULLET when allocating!
    struct GameEntity *owner;
    …
};

struct Bullet *ship_fire (struct Ship *ship) {
    Bullet *b = get_next_available_bullet();
    b->owner = (struct GameEntity *) ship;
    return b;
}

void hit_asteroid (struct Asteroid *ast, struct Bullet *bullet) {
    if (bullet->owner && bullet->owner->type == TYPE_PLAYER_SHIP) {
        …
    }
}

Обратите внимание, что этот прием основан на том, что указатели на различные типы структур взаимозаменяемы, а одно перечисление хранится с одинаковым смещением в каждом типе структуры. На практике это не является необоснованным предположением, но я не уверен, что это поведение строго гарантировано в стандарте C (однако, например, struct sockaddr использует один и тот же прием, и он используется различными сетевыми функциями POSIX, такими как bind).

1 голос
/ 08 февраля 2010

Я бы сделал так:

enum _ShipType
    {
    SHIPT_PLAYER,
    SHIPT_UFO, //trailing , is good if you need to add types later
    };

typedef struct _Bullet
    { 
    // ...other properties
    struct _Bullet_Owner
        {
        enum _ShipType type;
        void* ship;
        }owner;
    } Bullet;

void ship_fire(Player* p)
    {
    Bullet* b = malloc(sizeof(Bullet));
    // ...other init
    b->owner.type = SHIPT_PLAYER;
    b->owner.ship = p;
    }

Если в игре только игроков, вам лучше иметь флаг dead для каждого из них и устанавливать после смерти. (И статически распределить их.)

#define PLF_DEAD 0x1
//more stuff

struct _Player
    {
    long flags;
    //other data;
    }player_1,player_2;

Или вы можете иметь массив, или ...

Редактировать: Непостоянные игроки, ужасно переоцененное решение:

typedef struct _PShip
    {
    long nweakrefs;
    void** weakrefs;
    //etc...
    }PShip;

PShip* PShip_new(/* args or void */)
    {
    PShip t;
    t = malloc(sizeof(PShip));
    t->nweakrefs = 1;
    t->weakrefs = malloc(sizeof(void*)*t->nweakrefs);
    //other stuff
    }
void PShip_regref(PShip** ref)
    {
    void** temp;
    temp = realloc((*ref)->weakrefs,(*ref)->nweakrefs);
    if(!temp){/* handle error somehow */}
    (*ref)->weakrefs = temp;
    (*ref)->weakrefs[(*ref)->nweakrefs++] = ref;
    }
void PShip_free(PShip* ship)
    {
    long i;
    for(i=0;i<ship->nweakrefs;i++)
        {
        if(ship->weakrefs[i]){*(ship->weakrefs[i]) = 0;}
        }
    //other stuff
    }

В качестве альтернативы счетчик ссылок может работать хорошо, без памяти O (n).

typedef struct _PShip
    {
    long refs;
    //etc...
    }PShip;

void Bullet_free(Bullet* bullet)
    {
    //other stuff
    if(bullet->owner.type == SHIPT_PLAYER)
        {
        if(--(((PShip*)(bullet->owner.ship))->refs) <= 0)
            {PShip_free(bullet->owner.ship);}
        }
    }

Кроме того, ни один из них не является потокобезопасным.

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