1

C で関数を作成する場合、関数から結果が返される方法を左右する考慮事項は何ですか? 私の質問は、二重リンクリストにノードを作成し、この本から来た次のコードを読むことから生じます:

typedef struct DLLIST
{
    uint16 tag;                 /* This node's object type tag */
    struct DLLIST *next;
    struct DLLIST *previous;
    void *object;
    uint32 size;                /* This node's object size */
} DLLIST;

DLLIST *dlCreate(uint16 tag, void *object, uint32 size)
{
    DLLIST *newNode;

    newNode = malloc(sizeof(*newNode));

    if (newNode != NULL)
    {
        newNode->previous = NULL;
        newNode->next = NULL;
        newNode->tag = tag;
        newNode->size = size;
        newNode->object = malloc(size);

        if (newNode->object != NULL)
        {
            memcpy(newNode->object, object, size);
        }
        else
        {
            free(newNode);
            newNode = NULL;
        }
    }

    return newNode;
}

実際、この関数には、初心者として当惑する多くの側面があります。

  1. 参照ポインターとして渡されて変更されるのではなく、DLLIST ポインターがこのように返されるのはなぜですか? これを行うと、戻り値を解放して、関数の成功/失敗を報告するステータス バイトとして使用できます。
  2. ポインターが NULL でないことを確認するために 2 番目の引数がテストされないのはなぜですか? 確かに memcpy に NULL ポインターを渡すのは非常に悪いことでしょうか?

さらに、モジュール内のすべての関数に同じ呼び出し規約を使用するよう強制するのは一般的な方法ですか? たとえば、上記の関数の取得元である双方向リスト モジュール内のすべての関数は、以下に準拠する必要がありstatus byte = function(argument list)ますか? さらに、値が正しくない可能性があるすべての関数引数を検証するのは一般的な方法ですか? たとえば、すべてのポインター値をチェックして、null でないことを確認しますか?

4

3 に答える 3

2

それはすべてかなり主観的です、私は言わなければなりません。

1) 参照ポインターとして渡されて変更されるのではなく、DLLIST ポインターがこのように返されるのはなぜですか? これを行うと、戻り値を解放して、関数の成功/失敗を報告するステータス バイトとして使用できます。

これはやや好みの問題です。動的に割り当てられたポインターを返すことも有効ですが、呼び出し元がメモリを管理できるように、関数にポインターを渡すことを好みます。API アーキテクチャによっては、より適切でシンプルな場合もあります。NULLどちらの方法でも、戻り値は成功/失敗のチェックに使用できます。ポインターを返すと失敗が示されるためです。

2) ポインターが NULL でないことを確認するために 2 番目の引数がテストされないのはなぜですか? memcpy に NULL ポインターを渡すことは、非常に悪いことでしょうか?

これは、多くの C プログラマー (とにかく、私が知っている人たち) が持っている考え方であり、言語がどのように構築されているかを反映しています。ほとんどの C ライブラリでは、各関数にコントラクトがあり、それを尊重しないと未定義の動作が発生します。NULL関数に渡される引数をわざわざチェックすることはめったにありません。私がそうするとき、それはNULLポインターが関数の動作を変更するためであり、呼び出し元が契約を尊重していることを確認しているからではありません。あちこちでいくつかの命令を節約できるため、パフォーマンスやサイズの点で優れていると主張する人もいるかもしれません (特に、C で知られている限られたハードウェアではそうです)。・あちこちチェック。適切に文書化されている限り、どちらでもかまいません。

さらに、モジュール内のすべての関数に同じ呼び出し規約を使用するよう強制するのは一般的な方法ですか?

私はそれに同意しなければなりません。他の人を代弁することはできませんが、一般的には良い習慣として受け入れられていると思います。同じ API 内の関数間で呼び出し規則を切り替えると、プログラマーが混乱し、ドキュメントを何度も確認する時間が無駄になる可能性があります。

さらに、値が正しくない可能性があるすべての関数引数を検証するのは一般的な方法ですか?

それは一般的ですが、おそらくその逆も同様に一般的です (上記の #2 を参照)。

于 2013-01-08T19:47:43.670 に答える
1

DLLISTポインターが、参照ポインターとして渡されて変更されるのではなく、この方法で返されるのはなぜですか?これを行うと、関数の成功/失敗を報告するステータスバイトとして使用するために戻り値を解放できます。

外部から渡されたパラメータを介して結果を返すことは、かなりエレガントで面倒な方法であり、他に方法がない場合にのみ使用する必要があります。呼び出し元は受信者オブジェクトを定義する必要がありますが、それ自体はすでに不良です。しかし、それに加えて、追加の宣言が必要なため、純粋な式で関数を使用することはできません。

ほとんどの場合、純粋関数を介して機能を実装できる場合は、純粋関数を介して実装する必要があります。つまり、関数パラメータは入力用であり、関数の戻り値は出力用です。これはまさにこの場合に見られるものです。

ポインタがNULLでないことを確認するために、2番目の引数がテストされないのはなぜですか?確かに、NULLポインタをmemcpyに渡すことは非常に悪いことでしょうか?

まあ、議論はありません、そのようなテストを実行することは常に良い考えです。ただし、テストの正確な方法は、プログラム階層内の関数の「レベル」(「低レベル」から「高レベル」まで)によって異なります。デバッグのみのアサーションにする必要がありますか?それは永続的なアサーション(つまり、エラーログと「多分電話してください」メッセージを伴う制御された中止)である必要がありますか?意味のある回復戦略を暗示する実行時テストである必要がありますか?この質問に答えるための「1つの真のルール」はありません。それはコード設計の問題です。

テストが失敗した場合に実行する必要がある、この関数の意味のあるフェイルセーフ戦略はありますか?そうでない場合は、低レベル関数での実行時テストはまったく意味がありません。それは、ノイズと無駄なパフォーマンスでコードを乱雑にするだけです。これが実際に「低レベル」関数である場合、ここでの実行時テストは実際にifは適切ではありません。より適切なのは、デバッグのみのアサーションですassert(object != NULL && size > 0)

object != NULL特にテストを適切にする詳細dlCreateは、object値がライブラリ関数に渡されることです。ライブラリ関数は、nullポインタ入力の場合に未定義の動作memcpyを生成することが知られています。コードの代わりに独自の関数を使用した場合、の有効性を主張する適切な場所はの内部になります。関数自体は、値の有効性を実際には気にしません。memcpymy_copy_dataobjectmy_copy_datadlCreateobject

于 2013-01-08T20:01:07.077 に答える
1

質問 1 への回答: 戻り値には 3 つの一般的な規則があり、アプリケーションに最適なものを選択できます。

  1. ステータス値 (int) を返します。通常、負の値はエラーを示し、>=0 成功を示します。たとえば、POSIX の「open()」関数です。オブジェクトを渡す必要がある場合は、そのオブジェクトへのポインター (「IN」パラメーターの場合)、またはそのポインターへのポインター (「OUT」パラメーターの場合) を渡します。
  2. 「オブジェクト指向」関数の場合、特に何かを割り当てる関数の場合、オブジェクトへのポインターを返します(二重リンクリストの例のように)。失敗した場合は NULL を返します。POSIX の "fopen()" を例に考えてみましょう。
  3. 絶対値関数。これらは、値が成功または失敗を示すものではなく、何らかの計算の実際の結果です ("sin()" を考えてください)。

これらのスタイルを自分の好みに合わせて組み合わせることができます。多くのライブラリには混合物があります。API のユーザーがそれを使用するためにゆがめなければならないようにしてください!

たとえば、上記のコードを使用して、API を

int dlCreate(uint16 tag, void *object, uint32 size, DLLIST **list)

現在、ユーザーは、ファイルが開かれたかどうかを判断する 2 つの方法 (終了ステータスを確認する方法と、入力されたリスト ポインターを確認する方法) を持っており、何をすべきかを判断するために多くの余分な作業を行う必要があります。エラーのため。だから書く代わりに

DLLIST *list;
if ((list = dlCreate(tag, object, sz) == NULL) {
  // handle out of memeory error
  ...

ユーザーが言わなければならない

DLList *list = NULL // must initializein this case
if (dlCreate(tag, object, sz, &list) == ERROR || list == NULL)  {
  if (list != NULL) {
    free(list);
  }
  // and other error handling as above

そうは言っても、ライブラリに一貫したスタイルを使用することは、多くの場合良い習慣です。

質問 2 への回答: これはずさんなプログラミングであり、その通りです。優れたプログラマーは、呼び出し元が「正しいことを行う」と決して信頼すべきではありません。引数を検証するコードを必ず追加してください。ここで不適切なポインターを渡すと、クラッシュします。

于 2013-01-08T20:05:10.403 に答える