web-dev-qa-db-ja.com

Cで構造体の一部のフィールドを非表示にするにはどうすればよいですか?

私は構造体の人を実装しようとしています、そしていくつかのフィールドを隠すかそれらを一定にする必要があります。 プライベートフィールドを作成するためのトリック。

ヘッダ:

#pragma once

#define NAME_MAX_LEN 20

typedef struct _person {
    float wage;
    int groupid;
} Person;

const char const *getName (Person *p);
int getId (Person *p);

/// OTHER FUNCTIONS

ソース

#include "person.h"


struct _person
{
    int id;

    float wage;
    int groupid;

    char name[NAME_MAX_LEN];
};

/// FUNCTIONS

GCCはperson.c:7:8: error: redefinition a 'struct _person' struct _person

これをヘッダーに書き込むことはできますが、その後は構造体のフィールドを使用できません。

typedef struct _person Person;
18
Wootiae

Cには、構造型の個々のメンバーを非表示にするメカニズムがありません。ただし、そのような型に対してpointersの観点からのみ操作し、定義を提供しないことにより、型全体を不透明にすることができます。ユーザーはインスタンスを操作するために、提供された関数を使用する必要があります。これは時々行われることです。

ある程度、隠されたコンテキストで説明するようなことを達成できる場合があります。たとえば、次のことを考慮してください。

header.h

typedef struct _person {
    float wage;
    int groupid;
} Person;

implementation.c

struct _person_real {
    Person person;  // must be first, and is a structure, not a pointer.
    int id;
    char name[NAME_MAX_LEN];
};

これでこれを行うことができます:

Person *create_person(char name[]) {
    struct _person_real *pr = malloc(sizeof(*pr));

    if (pr) {
        pr->person.wage = DEFAULT_WAGE;
        pr->person.groupid = DEFAULT_GROUPID;
        pr->id = generate_id();
        strncpy(pr->name, name, sizeof(pr->name));
        pr->name[sizeof(pr->name) - 1] = '\0';

        return &pr->person;  // <-- NOTE WELL
    } else {
        return NULL;
    }
}

構造体の最初のメンバーへのポインターは常に構造体全体も指すため、クライアントがその関数から取得したポインターをユーザーに渡す場合、次のことができます。

struct _person_real *pr = (struct _person_real *) Person_pointer;

より大きなコンテキストからメンバーに取り組みます。

ただし、そのようなスキームには危険が伴うことを十分に認識してください。ユーザーがPersonwithoutより大きなコンテキストを作成し、コンテキストオブジェクトが存在することを期待する関数にポインターを渡すことを妨げるものはありません。他にも問題があります。

概して、C APIは一般に、不透明な構造のアプローチを取るか、クライアントがアクセスできるデータを使用して何が許可されているかを注意深く文書化するか、またはすべてがどのように機能するかを文書化して、ユーザーが自分で選択できるようにします。これら、特に後者は、Cの全体的なアプローチやイディオムとうまく整合しています。Cは手を握ったり、害を与えたりしないようにしています。自分が何をしているかを知り、自分がやろうとしていることだけを実行することを信頼します。

18
John Bollinger

構造体は、複数の矛盾する定義を持つことはできません。そのため、一部のフィールドを非表示にする構造体を作成することはできません。

canを実行しても、構造体が定義されずにヘッダーに存在することが宣言されます。その後、呼び出し元は、構造体へのポインタのみを使用し、実装で関数を使用して構造体を変更するように制限されます。

たとえば、次のようにヘッダーを定義できます。

typedef struct _person Person;

Person *init(const char *name, int id, float wage, int groupid);

const char *getName (const Person *p);
int getId (const Person *p);
float getWage (const Person *p);
int getGroupid (const Person *p);

そしてあなたの実装は以下を含みます:

#include "person.h"

struct _person
{
    int id;

    float wage;
    int groupid;

    char name[NAME_MAX_LEN];
};

Person *init(const char *name, int id, float wage, int groupid)
{
    Person *p = malloc(sizeof *p);
    strcpy(p->name, name);
    p->id = id;
    p->wage= wage;
    p->groupid= groupid;
    return p;
}

...
24
dbush

ミックスインスタイルを使用できます。例えばヘッダーに書き込む:

_struct person {
    float wage;
    int groupid;
};

struct person *person_new(void);
char const *getName (struct person const *p);
int getId (struct person const *p);
_

とソースで

_struct person_impl {
    struct person   p;
    char            name[NAME_MAX_LEN];
    int             id;
}

struct person *person_new(void)
{
    struct person_impl *p;

    p = malloc(sizeof *p);
    ...
    return &p->p;
}

chra const *getName(struct person const *p_)
{
    struct person_impl *p =
           container_of(p_, struct person_impl, p);

    return p->name;
}
_

たとえば、 https://en.wikipedia.org/wiki/Offsetofcontainer_of()の詳細。

3
ensc

ジョンボリンジャーの回答の補遺:

IMHO、アクセサ関数(init/get/set/destroy)を持つ不透明なポインタ型が最も安全なアプローチですが、ユーザーがスタックにオブジェクトを配置できるようにする別のオプションがあります。

structの一部として単一の「タイプなし」メモリチャンクを割り当て、追加のタイプを使用する代わりにそのメモリを明示的に(ビットごと/バイトごとに)使用することが可能です。

例:

// public
typedef struct {
    float wage;
    int groupid;
    /* explanation: 1 for ID and NAME_MAX_LEN + 1 bytes for name... */
    unsigned long private__[1 + ((NAME_MAX_LEN + 1 + (sizeof(long) - 1)) / sizeof(long))];
} person_s;

// in .c file (private)
#define PERSON_ID(p) ((p)->private__[0])
#define PERSON_NAME(p) ((char*)((p)->private__ + 1))

これは、private__メンバーのデータへのアクセスを回避する必要があることを示す非常に強力なインジケーターです。実装ファイルにアクセスできない開発者は、そこに何があるのか​​さえ知りません。

そうは言っても、pthread_t AP​​I(POSIX)を使用しているときに遭遇する可能性があるので、最善のアプローチは不透明なタイプです。

typedef struct person_s person_s;
person_s * person_new(const char * name, size_t len);
const char * person_name(const person_s * person);
float person_wage_get(const person_s * person);
void person_wage_set(person_s * person, float wage);
// ...
void person_free(person_s * person);

メモ

  1. ポインタでtypedefを回避します。開発者を混乱させるだけです。

    すべての開発者は、使用している型が動的に割り当てられていることを知ることができるように、ポインターを明示的に保つことをお勧めします。

    編集:また、ポインタ型を「typedefing」することを回避することで、APIは将来の/代替実装でもAPIでポインタを使用することを約束し、開発者がこの動作を信頼して信頼できるようにします(コメントを参照)。

  2. 不透明なタイプを使用する場合、NAME_MAX_LENを回避して、任意の長さの名前を許可できます(名前の変更には新しいオブジェクトが必要であると想定)。これは、不透明なポインターアプローチを好む追加のインセンティブです。

  3. 可能であれば、_を識別子の先頭に配置しないでください(つまり、_name)。 _で始まる名前は特別な意味を持つものと見なされ、一部は予約されています。 _tで終わる型についても同様です(POSIXで予約されています)。

    _sを使用して型を構造体としてマークする方法に注意してください。_t(予約済み)は使用していません。

  4. Cはより頻繁にsnake_caseです(少なくとも歴史的には)。最もよく知られているAPIとほとんどのC標準はsnake_caseです(C++からインポートされたものを除く)。

    また、一貫性がある方が良いです。場合によってはCamelCase(またはsmallCamelCase)を使用し、他の目的でsnake_caseを使用すると、開発者がAPIを覚えようとするときに混乱する可能性があります。

2
Myst

John Bollinger が書いたものは、構造体とメモリがどのように機能するかを活用する優れた方法ですが、segfaultを取得する簡単な方法でもあります(Personの配列を割り当て、最後に最後の要素を ' idまたはその名前にアクセスするメソッド)、またはデータを破壊します(Personの配列で、次のPersonが前のPersonの「プライベート」変数を上書きしています)。 Personの配列ではなく、Personへのポインターの配列を作成する必要があることを覚えておく必要があります(何かを最適化して、構造体を初期化関数よりも効率的に割り当てて初期化できると考えるまでは、かなり明白に聞こえます)。

誤解しないでください。これは問題を解決する優れた方法ですが、使用する際には注意が必要です。 (Personごとに4/8バイト多くのメモリを使用していますが).cファイルでのみ定義され、プライベートデータを保持する別の構造体へのポインタを持つ構造体Personを作成することをお勧めします。そうすれば、どこかで間違いを犯すことが難しくなります(そして、それがより大きなプロジェクトである場合は、私を信頼してください-遅かれ早かれそれを行うでしょう)。

.hファイル:

_#pragma once

#define NAME_MAX_LEN 20

typedef struct _person {
    float wage;
    int groupid;

    _personPriv *const priv;
} Person;

void personInit(Person *p, const char *name);
Person* personNew(const char *name);

const char const *getName (Person *p);
int getId (Person *p);
_

.cファイル:

_typedef struct {
    int id;
    char name[NAME_MAX_LEN];
} _personPriv;

const char const *getName (Person *p) {
    return p->priv->name;
}

int getId (Person *p) {
    return p->priv->id;
}

_personPriv* _personPrivNew(const char *name) {
    _personPriv *ret = memcpy(
        malloc(sizeof(*ret->priv)),
        &(_personPriv) {
            .id = generateId();
        },
        sizeof(*ret->priv)
    );

    // if(strlen(name) >= NAME_MAX_LEN) {
    //     raise an error or something?
    //     return NULL;
    // }

    strncpy(ret->name, name, strlen(name));

    return ret;
}

void personInit(Person *p, const char *name) {
    if(p == NULL)
        return;

    p->priv = memcpy(
        malloc(sizeof(*p->priv)),
        &(_personPriv) {
            .id = generateId();
        },
        sizeof(*p->priv)
    );

    ret->priv = _personPrivNew(name);
    if(ret->priv == NULL) {
        // raise an error or something
    }
}

Person* personNew(const char *name) {
    Person *ret = malloc(sizeof(*ret));

    ret->priv = _personPrivNew(name);
    if(ret->priv == NULL) {
        free(ret);
        return NULL;
    }
    return ret;
}
_

補足:このバージョンは、ローカリティを改善するために構造体の「パブリック」部分の直後/前にプライベートブロックが割り当てられるように実装できます。 sizeof(Person) + sizeof(_personPriv)を割り当てて、1つの部分をPersonとして、2番目の部分を__personPriv_として初期化します。

1
Grabusz