C의 void 포인터에 대한 포인터 - 기본적인 다형성에 void**를 사용할 수 있나요?
-
19-09-2019 - |
문제
나는 어떻게 이해할 수 있습니다 void**
메모리를 보면 알겠지만 제가 제대로 사용하고 있는지 궁금합니다.아래에 설명하는 내용에 근본적인 결함이 있습니까?예를 들어, "저에게 적합합니다"라고 말할 수 있지만 어떤 방식으로든 나쁘거나 이식할 수 없는 코드를 만들고 있는 걸까요?
그래서 나는 소행성 클론을 가지고 있습니다.총알을 발사할 수 있는 엔터티는 세 가지가 있습니다. 플레이어(SHIP *player_1
, SHIP *player_2
)와 UFO(UFO *ufo
).총알이 발사되면 누가 총알을 쐈는지 아는 것이 중요합니다.플레이어인 경우 무언가에 부딪히면 점수가 증가해야 합니다.따라서 글머리 기호는 자신이 속한 엔터티의 종류를 저장합니다(owner_type
) 및 소유자를 직접 가리키는 포인터(owner
):
enum ShipType
{
SHIP_PLAYER,
SHIP_UFO
};
typedef struct Bullet
{
// ...other properties
enum ShipType owner_type;
void **owner;
} BULLET;
그런 다음 플레이어가 버튼을 누르거나 UFO가 목표물을 발견하면 다음 함수 중 하나가 호출됩니다.
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**를 사용한 적이 있는지 알고 싶습니다.메모리 할당/캐스팅 측면에서.그러나 아무도 손을 높이 들고 결함이 있다고 선언하지 않았다면 적어도 기술적으로 작동하는 것과 비슷할 것입니다.
감사해요!
해결책
당신이 그대로 계속하고 싶었더라도 사용할 필요가 없습니다. void **
(그리고해서는 안됩니다).
하지만 void *
일반적인 포인터 유형이며 void **
~이다 ~ 아니다 일반적인 포인터 투 포인터 유형-항상 진정한 것을 가리 려야합니다. void *
물체. 귀하의 코드는 a SHIP **
또는 UFO **
유형의 LValue를 통한 포인터 void **
- 기술적으로 작동하지 않습니다. (이것은 당신이 할 때 발생합니다 (SHIP*)*bullet->owner
).
그러나 좋은 소식은 평원을 사용하여 더블 포인터 방법을 계속 사용할 수 있다는 것입니다. void *
일을하기 위해. void *
행복하게 포인터-포인터를 저장할 수 있습니다 (결국 또 다른 종류의 포인터이기 때문에). 당신이 변하면 owner
에게 void *
, 그런 다음 ship_fire
당신은 이것을 할 것입니다 :
bullet->owner = shipp;
그리고에서 hit_asteroid
당신은 이것을 할 것입니다 :
ship_owner = *(SHIP **)bullet->owner;
일반적으로 포인터 캐스트로 작업하는 규칙은 다음과 같습니다. 먼저 포인터를 실제로 알고있는 포인터 유형으로 다시 캐스팅합니다. 그 다음에 불평.
다른 팁
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);
이 tecnique의 많은 예에 대해서는 Linux 커널 소스 코드를 읽으십시오.
당신은 두 가지 유형 만 가지고 있기 때문에, 나는 이런 종류의 일에 노조를 사용할 것입니다.
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;
}
}
사용한 포인터 투 포인터 체계를 사용하지 않았습니다. 나는 실제로 그것의 필요성을 확신하지 못하고, 내가 제안한 코드에는 그러한 기술이 필요하지 않습니다.
먼저, 확인해 보세요. 조합 구성 mipadi가 제안한;그것은 매우 읽기 쉽고 효율적인 방법입니다. 다형성 다루기.
스니펫/질문에 더 가까이 다가가면 포인터 간 포인터에 의해 도입된 이중 간접 참조의 필요성/사용이 한눈에 보이지 않습니다.xxxx_fire() 메서드에 대한 인수가 xxxx 개체에 대한 [직접] 포인터인 경우(그리고 유형 변환 등이 있는 경우) 전체 논리는 동일하게 작동합니다.나머지 논리에서는 그에 따라 따라야했습니다.
포인터에 대한 포인터는 중간 포인터의 값이 어떤 지점에서 변경될 수 있을 때 유용합니다..예를 들어 기본 객체가 이동되거나 완전히 다른 객체로 대체되는 경우(예: 게임의 새 레벨에서 더 나은 장비를 갖춘 선박 부분 등...)
편집하다:(에 할당 해제될 수 있는 객체의 "플릿"을 관리하기 위해 이중 간접 사용.
귀하의 의견에 응답하여 ~ 아니다 객체가 다음과 같이 리팩토링됩니다. ~ 아니다 (게임의 일부로) 죽거나 삭제되면 할당이 해제됩니다(메모리에서). 대신에, 다음과 같은 내용을 살펴보십시오. 이는 실제로 포인터 간 구조가 많은 도움이 되는 예이기 때문입니다.작동 방법은 다음과 같습니다.
- 게임(또는 레벨) 초기화 시 배열을 할당합니다. 포인터의 시간이 지남에 따라 게임이 할당할 수 있는 총 개체 수만큼 많은 포인터를 포함할 수 있을 만큼 충분히 큽니다.모든 값을 NULL로 초기화합니다.
- 이 배열에서 다음 사용 가능한(=지금까지 사용되지 않은) 포인터의 위치를 나타내는 int 값 인덱스를 도입합니다.
- 새로운 개체(UFO, 선박 또는 무엇을 가지고 있는지)가 생성되면 네 가지 일이 발생합니다.
- 객체 자체에 새로운 메모리가 할당됩니다.
- 이 새 메모리의 주소는 객체 포인터 배열(인덱스로 표시된 위치)에 저장됩니다.
- 인덱스가 증가합니다
- "세계"는 이중 간접 참조를 통해서만 이 객체를 알고 있습니다.
- 객체가 파괴되면 두 가지 일이 발생합니다
- 메모리가 해제되었습니다
- 배열의 포인터가 null로 설정되었습니다.
- 어떤 객체에 접근할 때 프로그램은 세 가지 일을 합니다.
- 첫 번째 역참조(한 번) 포인터 간 포인터
- 이것이 null인지 확인합니다(그렇다면 객체가 더 이상 존재하지 않는다는 것을 나타내는 경우 논리는 다시 시도하지 않기 위해 이 참조를 저장한 위치에서 제거하기로 결정할 수 있지만 물론 선택 사항입니다).
- 중간 포인터를 역참조하여 실제 객체에 액세스합니다(NULL이 아닌 경우).
통찰력에 있어서 C 언어의 짧은 조각이 더 명확했을 수도 있습니다.미안 이걸 말로 설명했어...
만약에 총알 소유자는 자주 변경됩니다 (예 : 거래). 포인터 투 포인터 접근 방식이 적합합니다. Union 솔루션은이 문제를 직접 해결하지 못합니다. 제시된 바와 같이, 그것은 각 배의 총알에 포인터를 만지지 않으면 서 거래선을 지원하지 않습니다. 물론, 그것은 일부 구현에서 실제로 실용적인 솔루션 일 수 있습니다. 예를 들어 주어진 플레이어의 모든 총알을 찾을 필요가 있다면 링크 된 목록을 유지할 수 있습니다. 각 총알에 대한 "Next_Bullet"포인터 및 "Last_Bullet" ”각 플레이어의 목록 헤드에 대한 포인터.
그리고 각 총알을 개별적으로 할당하는 대신, 나는 또한 일부를 사전 할당하고 다음에 사용 가능한 다음을 선택하겠다는 MJV의 제안을 따를 것입니다. 링크 된 목록 구현에서 동일한 "Next_Bullet"포인터를 사용하여 현재 사용되지 않은 사전 배치 총알 목록을 유지할 수 있습니다. 이 접근법의 장점은 배열을 유지하는 대신, 가용 총알 목록이 비어있는 경우에 배열을 유지하는 대신 더 많은 것을 쉽게 할당 할 수 있다는 것입니다. 마찬가지로,“만료 된”(폭발?) 총알을 사용 가능한 것 목록에 다시 넣으면 할당 된 양이 자동으로 적응하지만 많은 사람들이 필요합니다.
떠오르는 또 다른 것은 특정 UFO (또는 다른 적)가 주어진 총알을 소유 한 것을 알 필요가 없다는 것입니다. 단일 포인터 만 가지고 있습니다 (예 : SHIP **
) 소유 플레이어의 경우 모든 비 플레이어 총알에 대해 NULL로 설정하십시오. 이것이 적합하지 않은 경우, 소유자 구조 자체의 시작 부분에서 각 소유자의 유형을 저장하는 것을 고려할 수도 있습니다.
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
).
나는 이것을 좋아할 것이다 :
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;
}
만 있다면u003Cconstant> 플레이어, 당신은 a 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);}
}
}
또한 이들 중 어느 것도 나사산이 아닙니다.