cでポインタをダブルポインタにキャストするのはなぜですか?

Aug 21 2020

私はCでOOPを読んでいて、次のヘッダーがあります。

#ifndef _NEW_H
#define _NEW_H

#include <stdarg.h>
#include <stddef.h>
#include <assert.h>

void *new (const void *type, ...);
void delete (void *item);

typedef struct
{
    size_t size;
    void *(*ctor)(void *self, va_list *app);
    void *(*dtor)(void *self);
    void *(*clone)(const void *self);
    int (*differ)(const void *self, const void *x);
} class_t;

inline void *new (const void *_class, ...)
{
    const class_t *class = _class;
    void *p = calloc(1, class->size);

    assert(p);
    *(const class_t **)p = class; //why would you cast to double pointer, when you immediately dereference it?

    if (class->ctor)
    {
        va_list args;
        va_start(args, _class);
        p = class->ctor(p, &args);
        va_end(args);
    }
    return p;
}

#endif //_NEW_H

今、私はこの表現を理解していません:

*(const class_t **)p = class;

const class_t **タイプとはどういう意味ですか?それは配列の配列のようなものですか?しかし、カスタムクラス(つまり、構造体へのポインタだけclass_tでなく、より多くの「パブリック」メソッド)が必要な場合、クラス全体は型の配列ではありませんclass_t。では、なぜvoidポインターをdoubleポインターにキャストし、すぐに逆参照するのでしょうか。どのように理解すればよいですか?

その声明についての本から: * (const struct Class **) p = class;

pは、オブジェクトの新しいメモリ領域の先頭を指します。オブジェクトの先頭をstructclass_tへのポインターとして扱うpの変換を強制し、引数クラスをこのポインターの値として設定します。

回答

4 RemyLebeau Aug 21 2020 at 06:12

toの呼び出しcalloc()は、指定されていない「クラス」タイプの1つの要素の配列を割り当てるために使用されます。このタイプの最初のメンバーはclass_t*ポインターであることが期待されます(したがってclass->size、少なくともsizeof(class_t*)、以上である必要があります)。calloc()malloc()表される追加のデータメンバーclass->sizeがゼロで初期化されるようにするためだけでなく、が使用される可能性memset()があります。そうでない場合は、明示的に必要になります。

奇妙なキャスト+逆参照は、コードが入力classポインタclass_t*をその割り当てられたオブジェクトの最初のメンバーに直接格納できるようにするためのものです。

配列には、ダブルポインタを使用してアクセスできます。このようなポインタを逆参照すると、配列の最初の要素のアドレスが得られます。この場合、これもclass_t*メンバーと同じアドレスになります。

OOP用語では、メモリ内のオブジェクトのレイアウトは通常、クラスの「仮想」メソッドへの関数ポインタのリストを含むオブジェクトクラスのvtableへのポインタから始まります。クラスが「派生」する場合、子孫は、オブジェクトのvtableポインターを関数ポインターの新しいリストに設定するだけで、仮想メソッドを「オーバーライド」します。このOOPの概念はCには実際には存在しませんが、C ++の基本です。Cでは、手動で実装する必要があります。これは、このコードが実行していることです。

基本的に、コードは割り当てられたオブジェクトにこのメモリレイアウトを割り当てています。

           ------------ --------------------
void * p-> | class_t * | -> | size_tサイズ|
           ------------ --------------------
           | ... | | void(* ctor)()|
           ------------ --------------------
                           | void(* dtor)()|
                           --------------------
                           | void(* clone)()|
                           --------------------
                           | void(* different)()|
                           --------------------

同じ割り当てを実行する別の方法は、アクセスを容易にするために「クラス」タイプにtypedefを使用することです。たとえば、元のコードは次のようになります。

typedef struct
{
    class_t *vtable;
    // other data members, if class->size > sizeof(class_t*) ...
} class_info_t;

inline void *new (const void *_class, ...)
{
    const class_t *class = _class;
    class_info_t *p = (class_info_t*) calloc(1, class->size);

    assert(p);
    p->vtable = class;
    // other data members are implicitly zeroed by calloc() ...

    ...
}

typedefを使用したり、キャストしたりすることなくmemcpy()、同じことを実現するために使用できます。例:

inline void *new (const void *_class, ...)
{
    const class_t *class = _class;
    void *p = calloc(1, class->size);

    assert(p);
    memcpy(p, &class, sizeof(class));

    ...
}
1 KonradZapałowicz Aug 21 2020 at 06:52

どういう意味ですか const class_t ** type?

ポインタへのポインタです。それが指しているものはすべて、実際には別のポインターであることを表します。そして、その2番目は、タイプの具体的なオブジェクト、この場合は、class_t

[type] ---> [2nd pointer] ---> [class_t object]

なぜ*(const class_t **)p = class;ですか?

この奇妙な構造はclass、「class_tオブジェクト」がある場所に配置することです。さて、このnew関数がどのように使用されるかを見ると、それほど奇妙ではありません。

これnewは、任意のカスタム型(struct)のジェネリックコンストラクターです。ただし、タイプには、構造体に最初のメンバーとしてclass_tへのポインターが含まれている必要があるという要件があります。これは、ポリモーフィズムを有効にするためであり、基本的には、Vテーブルへのポインターを使用してC ++が内部で行うことを可能にします。

ここで、カスタムタイプを定義する場合は、次のようにします。

struct Foo {
    void *class_t;
    // whatever members I need for my type
};

どうやって使うのですか?

さて、タイプが定義されたら、もう1つやるべきことがあります。new引数としての関数は、作成inline void *new (const void *_class, ...)されるものの基礎として使用されるポインターを受け取ることに注意してくださいconst class_t *class = _class;。それは、何かを作成するときに何かを渡す必要があることを意味します-それで、ポイントは何ですか?

さて、このタイプのオブジェクトを構築するために使用できるヘッダーにタイプへのconstポインターを定義するトリックがあります。

fooヘッダー内:

struct Foo {
    void *class_t;
    // whatever members I need for my type
};

static const struct class_t _Foo = { 
sizeof (struct Foo), 
// and other functions such ad ctor, dtro, etc...
}; 

const void *Foo = &_Foo;

上記は、このタイプのすべてのオブジェクトを作成するために、マトリックスとともにカスタムタイプを定義しています。それはタイプでclass_tあり、のサイズFooが記録されていることに注意することが重要です。

最後に、Fooの新しいオブジェクトが次のように作成されます。

void *f = new(Foo);

Fooしかし型であるclass_tが、我々はそれになりたいですFooFoo構造体の最初の要素はへのポインタclass_tであるため、この関係を作成するには、ダブルポインタを内部で使用する必要があります。new