「2016年にCで書く方法」という記事に対する批判



翻訳者から:



この出版物は、 Inoventica Servicesブログで記事「How to C in 2016」翻訳が出版された後に自発的に発生したシリーズの3番目の最終記事です。 ここで、原文で述べられた論文のいくつかが批判され、最初の出版物の著者が提起した質問に関する意見の最終的な「写真」とC.でのコードの記述方法が最終的に形成されます。 私が思うに、 CodeRushユーザーの多くの馴染みのあるユーザーによって与えられたテキストへのヒントである2番目の出版物はここにあります



マット(少なくとも私の知る限り、著者の姓が示されていないWebサイト)は、「2016年のCでのプログラミング」という記事を公開しました。これは後にRedditとHacker Newsに掲載されました。



はい、Cプログラミングを際限なく「議論する」ことができますが、私は明らかに反対する側面があります。 この重要な記事は建設的な議論で書かれています。 場合によっては、マットが正しい可能性があり、私は間違っています。



Mattの出版物全体を引用するわけではありません。 特に、私が同意するいくつかの点を省略することにしました。 始めましょう。



Cでのプログラミングの最初のルールは、他のツールで問題がなければ使用しないことです。


私はこの声明に同意しませんが、これは議論するには広すぎるトピックです。



Cでプログラミングする場合、 lang



はデフォルトでC99を参照するため、追加のオプションは必要ありません。


clang



のバージョンによって異なりますclang 3.6



デフォルトでC99で動作し、 clang 3.6



はC11で動作します。 箱から出して使用するときにどれほど厳しいものかわかりません。



gccまたはclangに特定の標準を使用する必要がある場合は、複雑にしないで、std = cNN -pedanticを使用してください。



デフォルトでは、 gcc-5



-std=gnu11



要求しますが、実際にはGNUなしでc99またはc11を指定する必要があります。


原則として、これらの目的に非常に適している特定のgcc拡張機能を使用したくない場合を除きます。



新しいコードでchar



int



short



long



またはunsigned



ようなものを見つけた場合、ここにエラーがあります。


もちろんすみませんが、これはナンセンスです。 特に、intは現在のプラットフォームで最も受け入れられる整数データ型です。 少なくとも16ビットの高速符号なし整数について話している場合、intを使用しても何も問題はありません(または、 int_least16_t



オプションを参照できます。これは、同じタイプの関数で問題int_least16_t



ますが、 int_least16_t



の方が価値があります)。



最新のプログラムでは、 #include <stdint.h>



を指定してから、標準のデータ型を選択する必要があります。


int



スペルが«std»



はないという事実は、非標準的なものを扱っているという意味ではありません。 int



long



などのタイプはC言語に組み込まれ、 <stdint.h>



修正されたtypedefは追加情報として後で表示されます。 これは、組み込みの型よりも「標準」ではありませんが、いくつかの点では後者より劣っています。



float



-32ビット浮動小数点標準

double



-64ビット浮動小数点標準


float



およびdouble



は、32ビットおよび64ビット浮動小数点標準の非常に一般的なIEEEタイプです。特に、最新のシステムでは、Cでプログラミングするときにこれにこだわることはありません。私は、floatが64ビットで使用されるシステムで作業しました。



注意してください:これ以上のchar.



通常、Cプログラミング言語では、 char



コマンドは呼び出されるだけでなく、誤って使用されます。


残念ながら、Cでプログラミングするときにパラメーターとバイトをマージすることは避けられません。 char型は安定して1バイトと見なされます。「バイト」は少なくとも8ビットです。



ソフトウェア開発者は、符号なしのバイト操作が実行される場合でも、charコマンドを使用して「バイト」を意味し続けます。 個々の符号なしバイト/オクテット値にuint8_t



を指定し、符号なしバイト/オクテット値のシーケンスにuint8_t *



を選択する方がはるかに正確です。


バイトが含まれている場合は、 unsigned char



使用します。 オクテットの場合、 uint8_t



選択します。 CHAR_BIT > 8



場合、 uint8_t



を作成できません。つまり、機能せず、コードをコンパイルできません(おそらくこれがまさに必要なものです)。 8ビット以上のオブジェクトをuint_least8_t



場合は、 uint_least8_t



使用しuint_least8_t



。 バイトがオクテットを意味する場合、次のようなコードをコードに追加します。



 #include <limits.h> #if CHAR_BIT != 8 #error "This program assumes 8-bit bytes" #endif
      
      





注:POSIXはCHAR_BIT == 8



要求します。



Cプログラミング言語では、文字列リテラル("hello")



char *



ように見えます。


いいえ、文字列リテラルはchar []に設定されます。 特に、「hello」の場合、char [6]です。 配列はポインターではありません。



unsigned



を使用してコードを記述しようとしないでください。 これで、コンテンツを読みにくくするだけでなく、完成品を使用する効率性に疑問を投げかける多数のデータ型を使用して、厄介なCの規則なしにまともなコードを記述する方法がわかりました。


Cの多くの型には、いくつかの単語で構成される名前が付けられます。 それに問題はありません。 余計な文字を入力するのが面倒な場合、これはあらゆる種類の略語をコードに詰め込む必要があるという意味ではありません。



単純なuint64_t



制限できる場合、誰がunsigned long long intを入力しますか?


一方では、intを意味するunsigned long longを使用できます。 同時に、これらは異なるものであり、 unsigned long long



型は少なくとも64ビットであり、インデントが存在する場合と存在しない場合があります。 uint64_t



、インデントビットなしで、正確に64ビット用に設計されています。 このタイプは、必ずしもいずれかのコードに登録されているわけではありません。



unsigned long long



Cの組み込み型です。このプログラミング言語を使用する専門家は、この言語に精通しています。



または、 uint_least64_t



試してuint_least64_t



。これは、 unsigned long long



と同一または異なる場合があります。



タイプ<stdint.h>



、より具体的かつ正確な意味を持ち、作者の意図をよりよく伝え、コンパクトであり、操作と読みやすさの両方にとって重要です。


もちろん、 intN_t



型とuintN_t



uintN_t



より具体的です。 しかし、すべてのコードが主なものではありません。 あなたにとって重要ではないものを指定しないでください。 uint64_t



は、実際に正確に64ビットが必要な場合にのみ選択してuint64_t







特定の形式に適応する必要がある場合など、正確な長さの型が必要になることがあります(バイト順、要素の配置などに重点が置かれることがあります。Cの<stdint.h>は、特定のパラメーターを記述する機能を提供しません)。 ほとんどの場合、組み込み型[u] int_leastN_tまたは[u] int_leastN_tが適している特定の範囲の値を指定するだけで十分です。



この場合のポインターの正しいタイプはuintptr_t



で、ファイル<stdint.h>



によって設定されます。


なんというひどい間違い。



小さなエラーから始めましょう: uintptr_t



<stddef.h>



ではなく<stddef.h>



<stdint.h>



に設定されます。



これは、詳細についての話です。 データを失うことなくvoid*



を別の整数型に変換できないコマンドを呼び出すことは、 uintptr_t



によって決定されることはほとんどありません(そのような場合は、非常にまれです)。



代わりに:



 long diff = (long)ptrOld - (long)ptrNew;
      
      







はい、そうではありません。



使用:



 ptrdiff_t diff = (uintptr_t)ptrOld - (uintptr_t)ptrNew;
      
      







しかし、このオプションは優れています。



型の違いを強調したい場合は、次のように書きます。



 ptrdiff_t diff = ptrOld - ptrNew;
      
      





バイトに集中する必要がある場合は、次のようなものを選択します。



 ptrdiff_t diff = (char*)ptrOld - (char*)ptrNew;
      
      





ptrOld



およびptrNew



が必要なパラメーターを示さない場合、または単にオブジェクトの最後からジャンプする場合、ポインターがデータ減算コマンドを呼び出す方法をトレースすることは困難です。 uintptr_t



切り替えると、少なくとも相対的な結果が保証されますが、非常に有用とは言えません。 ポインターを使用した比較またはその他の算術は、高レベルシステム用のコードを記述する場合にのみ許可されます。そうでない場合は、調査中のポインターが特定のオブジェクトの末尾を参照するか、そこからスキップすることが重要です(例外:==および!異なるオブジェクト)。



そのような状況では、intptr_t(プラットフォーム上の単語に等しい値に対応する整数データ型)を参照するのが合理的です。


しかし、ありません。 「単語と等しい」という概念は非常に抽象的なものです。 intptr_t



データ損失なしでvoid*



intptr_t



、またはその逆に正常に変換intptr_t



符号付き整数型です。 さらに、これはvoid*



を超える値になる場合があります。



32ビットプラットフォームでintptr_t



intptr_t



int32_t



変換されます。


それは起こりますが、常にではありません。



64ビットプラットフォームでは、 intptr_t



int64_t



ます。


繰り返しになりますが、必要ではありません。



実際、 size_t



は「巨大な配列インデックスを格納できる整数」のようなものです。


いや



そのため、作成中のプログラムの偏りの印象的な指標を修正できます。


はい、このタイプのデータを使用すると、プログラムの起動に関係する最大のオブジェクトのサイズに関する情報を保存できます(これもオプションであるという意見もありますが、実際には、これがまさに起こると仮定することができます)。 すべてのオフセットが同じオブジェクト内で作成される場合、メインメモリオフセットを修正できます。



いずれの場合でも、最新のプラットフォームでは、 size_t



uintptr_t



と実質的に同じ特性を持つため、32ビットバージョンでsize_t



size_t



uint32_t



、64ビットuint64_t



uint64_t



ます。


最も可能性が高いが、必要ではない。



より具体的には、 size_t



は個々のオブジェクトのサイズを保存するために使用できますが、 uintptr_t



はポインター値を設定し、それに応じてさまざまなオブジェクトのバイトのアドレスを混同しなくなります。 最新のシステムのほとんどは、分割できないアドレス行で動作するため、理論的には、オブジェクトの最大サイズは合計メモリサイズに等しくなります。 Cプログラミング標準では、この要件を厳守する必要があります。 そのため、たとえば、64ビットシステム上でオブジェクトが32ビットを超えない場合があります。



「モダン」という言葉を強調し、古い選択肢(x86など、ニアポインターとファーポインターを使用したセグメント化されたアドレス指定を使用)を自動的に省略し、C標準との互換性を提供する可能性のある将来の製品については触れていませんが、定義を超えています「モダン」



操作中はデータ型を参照しないでください。 常に適切なタイプのポインターを使用してください。


これは選択肢の1つですが、成功する唯一のソリューションではありません(そして、確かに、「%p」にvoid *を記述する必要があることに同意するでしょう)。



ポインターの初期値は%pです(最新のコンパイラーでは、16進システムで表示されます。最初はポインターをvoid *



送信します)


素晴らしいヒント-出力形式のみが起動オプションによって設定されます。 これは通常16進値ですが、他に何も指定されていないとは思わないでください。



  printf("Local number: %" PRIdPTR "\n\n", someIntPtr);
      
      





someIntPtr



という名前はint*



型を意味し、実際にはintptr_t



型を指定します。



トピックにはさまざまなバリエーションがあります。つまり、マクロ名の無限の組み合わせを覚える必要はありません。



 some_signed_type n; some_unsigned_type u; printf("n = %jd, u = %ju\n", (intmax_t)n, (uintmax_t)u);
      
      





intmax_t



uintmax_t



は通常64ビットです。 それらの変換は、物理的なI / Oよりもはるかに経済的です。



注:%はフォーマット文字列のリテラル本体に入りますが、タイプポインターはその外側に留まります。


これらはすべてフォーマット文字列の一部です。 マクロは、隣接する文字列リテラルと組み合わせた文字列リテラルとして定義されます。



最新のコンパイラーは#pragma once



サポートし#pragma once





しかし、このディレクティブを使用する必要があると言う人はいません。 プロセッサの命令でさえ、そのような推奨事項を公開していません。 また、「一度だけの見出し」セクションでは、#pragma onceについての言葉ではありません。 ただし、 #ifndef



説明されています。 次のセクションでは、「#ifndefパッカーの代替案」が#pragmaを1回点滅させましたが、この場合、これは移植性のあるオプションではないことに注意してください。



この関数は、すべてのコンパイラおよび異なるプラットフォームでサポートされており、ヘッダーセキュリティコードを手動で入力するよりもはるかに効率的なメカニズムです。


そして、誰がそのような勧告をしますか? #ifndef



ディレクティブは理想的ではないかもしれませんが、信頼性と移植性があります。



重要:構造に内部インデントがある場合、{0}メソッドはこの目的のために追加のバイトをゼロにしません。 そのため、たとえば、構造が1ワード単位で埋められるため、構造物のcounter



後(64ビットプラットフォーム上)に4バイトのインデントがある場合に発生します。 未使用のインデントバイトを含む構造全体を無効にする必要がある場合は、 memset(&localThing, 0, sizeof(localThing))



指定しますmemset(&localThing, 0, sizeof(localThing))



sizeof(localThing) == 16 bytes



、8 + 4 = 12バイトしか使用できません。


タスクは複雑になっています。 通常、インデントバイトに特別な注意を払う理由はありません。 貴重な時間を彼らに捧げたい場合は、 memset



を使用してリセットしてください。 memset



を使用して構造体をクリーニングすると、要素全体に実際にゼロの値が割り当てられていることを考慮しても、浮動小数点型またはポインターに対して同じ効果が保証されないことに注意してください-それぞれ0.0およびNULL



である必要がありNULL



(ただしほとんどのシステムでは、関数は正常に動作します)。



可変長の配列はC99で登場しました


いいえ、C99はVLA(可変長配列)の初期化子を提供しません。 しかし、実際、MattはVLAイニシャライザーについては書いておらず、VLA自体についてのみ言及しています。



可変長の配列は矛盾した現象です。 mallocとは異なり、リソース割り当てにエラー検出は含まれません。 したがって、データのバイト数Nを割り当てる必要がある場合、以下が必要になります。



 { unsigned char *buf = malloc(N); if (buf == NULL) { /* allocation failed */ } /* ... */ free(buf); }
      
      





少なくとも、概して、次の場合よりも安全です。



 { unsigned char buf[N]; /* ... */ }
      
      





はい、VLAの使用時のエラーには深刻な問題が伴います。 しかし、実際には、どのプログラミング言語の各機能についても同じことが言えます。



そして、固定長の古い配列では、同様の疑問が生じました。 配列を作成する前にサイズを確認する限り、変数Nを持つVLAは同じサイズの固定長配列と同じくらい無害です。 原則として、固定長の配列を記述するには、実際のデータを格納するためにその一部が必要なので、予想される要素の数を超える値が選択されます。 VLAを使用すると、コンポーネントが必要とするスペースを正確に割り当てることができます。 そして、ここで私はマットの推奨に同意します。



1つの側面に加えて、C11では、必要に応じてVLAを選択できます。 実際、ほとんどのC11コンパイラーは、小さな組み込みシステムの場合を除き、可変長配列をオプションとして認識しているとは思いません。 ただし、最もポータブルなコードを作成する場合は、この機能を覚えておく必要があります。



関数が*任意**ソースデータおよび特定の長さで機能する場合、このパラメーターのタイプを制限しないでください。 *



知って間違い:



 void processAddBytesOverflow(uint8_t *bytes, uint32_t len) { for (uint32_t i = 0; i < len; i++) { bytes[0] += bytes[i]; } }
      
      





代わりに使用します:



 void processAddBytesOverflow(void *input, uint32_t len) { uint8_t *bytes = input; for (uint32_t i = 0; i < len; i++) { bytes[0] += bytes[i]; } }
      
      





void*



、任意のメモリのパラメーターを修正するための理想的な型であることに同意します。 少なくとも標準ライブラリのmem*



関数を使用します(ただし、lenはuint32_t



ではなくsize_t



にする必要がありuint32_t



)。



ソースデータ型をvoid *として宣言し、関数本体で直接必要な実際のデータ型を再割り当てまたは再度参照することにより、ライブラリで何が起こっているかを考える必要がないため、ユーザーを保護します。


小さなメモ:これは、Mattの機能では詳しく説明されていません。 ここでは、 void*



からuint8_t*



への暗黙的な変換を確認します。



この例では、一部の読者がアライメントの問題に直面しています。


そして、彼らは間違っていました。 バイトシーケンスのように、特定のメモリを使用する場合、常に安全です。



C99は、関数<stdbool.h>



セット全体を提供します。ここで、 true



は1、 false - 0



です。


はい。ただし、 _Bool



組み込み型のエイリアスとして使用されるbool



を指定することもできます。



成功/失敗の戻り値の場合、関数はtrue



またはfalse



を返す必要があり、戻り値の型int32_t



なく、1と0の手動入力を必要とします(さらに悪いことに1と-1。それをどうやって調べるか:0- success



、および1- failure?



または0- success



、および-1- failure?



))。


特に、Unixなどのシステムでは、成功した場合に関数が0を返し、失敗した場合にゼロ以外の値(多くの場合-1)を返す広範なアルゴリズムがあります。 多くの場合、ゼロ以外の変数の結果はさまざまな種類のエラーを示します。 既製のインターフェースに新しい関数を追加する場合、前述の標準に従うことが重要です(一般に、関数が効果的に機能するためのオプションは1つだけなので、0は成功に相当しますが、多くのエラーが発生する可能性があります)。



特定の条件を分析するために作成された関数は、 true



またはfalse



を返す必要がありfalse



。 それらを成功/失敗したコード実行結果と混同しないでください。



bool



関数には、アサーションの形式で名前を付ける必要があります。 英語では、はい/いいえの質問に答える文言になります。 たとえば、 is_foo()



およびhas_widget()



特定のアクション用に設計された関数で、実行の成功度を知ることが重要な場合、おそらく別のステートメントによって設定されます。 一部の言語では、例外の追加/減算に頼ることが合理的です。 Cでは、関数の肯定的な結果にゼロ値を設定するなど、特定の暗黙のルールに従う必要があります。



2016年にCで開発された製品をフォーマットできる唯一の製品はclang-formatです。 ネイティブのclang形式の設定は、他の自動Cコードフォーマッタよりも桁違いに高くなっています。


私自身はclang形式を使用していません。 私は彼に会う必要があります。



しかし、Cコードのフォーマットに関するいくつかの基本的なポイントを表明したいと思います。





私はめったに自動書式設定ツールに頼りません。 たぶん無駄ですか?



malloc



使用しない

calloc





もう1つあります。 割り当てられたメモリのすべてのビットをリセットしようとすると、非常にarbitrary意的なプロセスになります。原則として、これは良い考えではありません。 コードが正しく記述されている場合、対応する値を最初に割り当てることなく、このオブジェクトまたはそのオブジェクトを呼び出すことはできません。 calloc



を使用すると、コード内のバグがゼロに等しくなるという事実に遭遇します。これは、システムエラーを不要なデータと混同しやすいことを意味します。 それはコードの改善のように聞こえますか?



メモリをゼロ化すると、多くの場合、プログラムコードのエラーが順次アルゴリズムを実行します。 定義により、これを正しい起動コースと呼ぶことはできません。 ただし、順次エラーの追跡ははるかに困難です。



はい、コードがエラーなしで作成された場合。 ただし、コードを作成するときに防御戦略に従う場合は、 無効なカテゴリから特定の値を割り当てられたメモリに割り当てる価値があります。



一方、すべてのビットをゼロにすることで設定されたタスクが解決される場合は、 calloc



を使用してみてください。






PS

また、来週読者をガイド付きツアーでクラウドデータセンターに招待します。 Habréのイベントのお知らせはこちら




All Articles