C Language
共通の落とし穴
サーチ…
前書き
このセクションでは、Cプログラマが認識すべきであり、作成を避けるべきである一般的な間違いのいくつかについて説明します。予想外の問題とその原因の詳細については、 未定義の動作を参照してください。
算術演算での符号付き整数と符号なし整数の混合
通常、ミックスするのは良いアイデアではありませんsigned
とunsigned
算術演算で整数を。例えば、次の例の出力は?
#include <stdio.h>
int main(void)
{
unsigned int a = 1000;
signed int b = -1;
if (a > b) puts("a is more than b");
else puts("a is less or equal than b");
return 0;
}
1000が-1より大きいので、出力a is more than b
と予想されますが、そうではありません。
異なる整数型間の算術演算は、通常の算術変換と呼ばれる共通の型で実行されます(言語仕様6.3.1.8を参照)。
この場合、「共通型」はunsigned int
。 通常の算術変換で説明したように 、
そうでない場合、符号なし整数型を有するオペランドが他のオペランドの型のランク以上のランクを有する場合、符号付き整数型のオペランドは、符号なし整数型のオペランドの型に変換される。
つまり、 int
オペランドb
は比較の前にunsigned int
に変換されます。
-1に変換するとunsigned int
結果、最大の可能性であるunsigned int
ことを意味し、1000年よりも大きい値、 a > b
falseですが。
比較すると==の代わりに誤って書く=
=
演算子は代入に使用されます。
==
演算子は比較に使用されます。
1つは、2つを混ぜないように注意する必要があります。時には1つは間違って書き込みます
/* assign y to x */
if (x = y) {
/* logic */
}
本当に欲しかったのは次のときです:
/* compare if x is equal to y */
if (x == y) {
/* logic */
}
前者はyの値をxに代入し、比較を行う代わりにその値が非ゼロであるかどうかをチェックします。
if ((x = y) != 0) {
/* logic */
}
代入の結果をテストすることが意図され、一般的に使用されることがあります。なぜなら、コードを複製する必要がなく、初めて特別に扱わなければならないからです。比較
while ((c = getopt_long(argc, argv, short_options, long_options, &option_index)) != -1) {
switch (c) {
...
}
}
対
c = getopt_long(argc, argv, short_options, long_options, &option_index);
while (c != -1) {
switch (c) {
...
}
c = getopt_long(argc, argv, short_options, long_options, &option_index);
}
現代のコンパイラはこのパターンを認識し、上記のように括弧の中に代入があると警告しませんが、他の用途について警告することがあります。例えば:
if (x = y) /* warning */
if ((x = y)) /* no warning */
if ((x = y) != 0) /* no warning; explicit */
いくつかのプログラマーは、定数をオペレーターの左に置くという戦略を使用します(一般にYoda条件と呼ばれます )。定数はrvaluesなので、誤った演算子が使用された場合、コンパイラはエラーをスローします。
if (5 = y) /* Error */
if (5 == y) /* No error */
しかし、これはコードの可読性を著しく低下させ、プログラマーが優れたCコード体系に従うならば必要ではないと考えられ、2つの変数を比較する際には役に立たないので普遍的な解決策ではありません。さらに、現代のコンパイラの多くは、Yodaの条件でコードが記述されているときに警告を出すことがあります。
セミコロンの注意深い使用
セミコロンに注意してください。次の例
if (x > a);
a = x;
実際には:
if (x > a) {}
a = x;
これはどんな場合でもx
がa
に代入されることを意味しています。
セミコロンを紛失すると、目立たない問題が生じることがあります。
if (i < 0)
return
day = date[0];
hour = date[1];
minute = date[2];
戻り値のセミコロンは欠落しているので、day = date [0]が返されます。
これと同様の問題を回避する1つの手法は、複数行の条件文とループで常に中括弧を使用することです。例えば:
if (x > a) {
a = x;
}
\ 0に余分な1バイトを割り当てることを忘れる
文字列をmalloc
化されたバッファにコピーするときは、必ずstrlen
に1を加えてください。
char *dest = malloc(strlen(src)); /* WRONG */
char *dest = malloc(strlen(src) + 1); /* RIGHT */
strcpy(dest, src);
これは、 strlen
は末尾に\0
が含まれていないためです。 WRONG
(上記のように)のアプローチをとってstrcpy
を呼び出すと、あなたのプログラムは未定義の動作を呼び出します。
また、 stdin
や他のソースから既知の最大長の文字列を読み込んでいる場合にも適用されます。例えば
#define MAX_INPUT_LEN 42
char buffer[MAX_INPUT_LEN]; /* WRONG */
char buffer[MAX_INPUT_LEN + 1]; /* RIGHT */
scanf("%42s", buffer); /* Ensure that the buffer is not overflowed */
空きメモリを忘れる(メモリリーク)
プログラミングのベストプラクティスは、独自のコードで直接割り当てられたメモリを解放するか、 strdup()
などのライブラリAPIなどの内部関数または外部関数を暗黙的に呼び出すことです。メモリを解放できないとメモリリークが発生する可能性があります。このリークは、プログラム(またはシステム)で使用できない膨大な量のメモリを浪費し、クラッシュや未定義の動作につながる可能性があります。ループまたは再帰関数で繰り返しリークが発生すると、問題が発生する可能性が高くなります。漏れたプログラムが実行されるほど、プログラム障害のリスクは高くなります。ときどき問題が瞬時に現れることがあります。他の時間の問題は、何時間も何年もの一定の操作では見られません。メモリ枯渇の失敗は、状況によっては致命的なものになります。
次の無限ループは、新しいメモリを暗黙的に割り当てる関数であるgetline()
呼び出すことによって、メモリを解放せずに、最終的に使用可能なメモリリークをgetline()
リークの例です。
#include <stdlib.h>
#include <stdio.h>
int main(void)
{
char *line = NULL;
size_t size = 0;
/* The loop below leaks memory as fast as it can */
for(;;) {
getline(&line, &size, stdin); /* New memory implicitly allocated */
/* <do whatever> */
line = NULL;
}
return 0;
}
対照的に、以下のコードではgetline()
関数も使用していますが、今回は割り当てられたメモリが正しく解放され、リークが回避されます。
#include <stdlib.h>
#include <stdio.h>
int main(void)
{
char *line = NULL;
size_t size = 0;
for(;;) {
if (getline(&line, &size, stdin) < 0) {
free(line);
line = NULL;
/* Handle failure such as setting flag, breaking out of loop and/or exiting */
}
/* <do whatever> */
free(line);
line = NULL;
}
return 0;
}
メモリリークは、必ずしも具体的な結果をもたらすとは限らず、必ずしも機能的な問題ではない。 「ベストプラクティス」は、戦略的なポイントと条件で厳密にメモリを解放するよう指示しますが、メモリの使用量を削減し、メモリの消耗のリスクを低減するためには例外があります。例えば、プログラムが期間とスコープで制限されている場合、割り当ての失敗の危険性はあまりにも小さいと考えられます。その場合、明示的な割り当て解除をバイパスすることは容認可能と考えられます。例えば、最新のオペレーティングシステムのほとんどは、プログラムの失敗、 exit()
へのシステムコール、プロセスの終了、またはmain()
終了に起因して、プログラムが終了したときに消費されるすべてのメモリを自動的に解放します。差し迫ったプログラム終了の時点で明示的にメモリを解放すると、実際には重複したり、パフォーマンスの低下を招く可能性があります。
不十分なメモリが利用可能である場合、割り当ては失敗し、ハンドリング失敗はコールスタックの適切なレベルで考慮されるべきです。上記のgetline()
は、呼び出し元に解放されたメモリを解放するだけでなく、いくつかの理由で失敗する可能性のあるライブラリ関数であるため、興味深い使用例です。したがって、C APIを使用してドキュメント(マニュアルページ)を読んで、エラー状態とメモリ使用に特に注意を払い、返されたメモリを解放する負担がどのソフトウェア層にあるかを知ることが不可欠です。
もう1つの一般的なメモリ処理の方法は、メモリポインタを参照するメモリが解放された直後に、メモリポインタをNULLに一貫して設定することです。そのため、解放されたメモリにアクセスするため(NULL /ガベージ・データの取得(読み取り操作)やデータの破損(書き込み操作)やプログラム・クラッシュなどの深刻な問題を引き起こす可能性があります。現代のオペレーティングシステムでは、メモリの場所0( NULL
)を解放することは、C標準で要求されるようにNOPです(つまり、無害です)。ポインタをNULLに設定することで、ポインタfree()
渡されます。メモリを二重に解放すると、非常に時間がかかり、混乱し、障害を診断するのが困難になることに注意してください。
あまりにも多くのコピー
char buf[8]; /* tiny buffer, easy to overflow */
printf("What is your name?\n");
scanf("%s", buf); /* WRONG */
scanf("%7s", buf); /* RIGHT */
ユーザーが7文字より長い文字列を入力すると(null終端文字では-1)、バッファbuf
後ろのメモリは上書きされます。この結果、未定義の動作が発生します。悪意のあるハッカーは、リターンアドレスを上書きし、ハッカーの悪質なコードのアドレスに変更するために、これを悪用することがよくあります。
reallocの戻り値を一時的にコピーすることを忘れる
realloc
が失敗すると、 NULL
返しNULL
。 realloc
の戻り値に元のバッファの値を代入し、 NULL
返すと、元のバッファ(古いポインタ)が失われ、 メモリリークが発生します。解決策は、一時的なポインタにコピーすることで、一時的にそれがNULLでない場合は、 その後、実際のバッファにコピーします。
char *buf, *tmp;
buf = malloc(...);
...
/* WRONG */
if ((buf = realloc(buf, 16)) == NULL)
perror("realloc");
/* RIGHT */
if ((tmp = realloc(buf, 16)) != NULL)
buf = tmp;
else
perror("realloc");
浮動小数点数の比較
浮動小数点型( float
型、 double
型、およびlong double
)は、精度が有限でバイナリ形式の値を表すため、一部の数値を正確に表すことはできません。 1/3のような小数点以下10桁の小数点以下桁数を繰り返すのと同じように、2進でも有限では表現できない小数点もあります(1/3、さらに重要なのは1/10など)。浮動小数点値を直接比較しないでください。代わりにデルタを使用してください。
#include <float.h> // for DBL_EPSILON and FLT_EPSILON
#include <math.h> // for fabs()
int main(void)
{
double a = 0.1; // imprecise: (binary) 0.000110...
// may be false or true
if (a + a + a + a + a + a + a + a + a + a == 1.0) {
printf("10 * 0.1 is indeed 1.0. This is not guaranteed in the general case.\n");
}
// Using a small delta value.
if (fabs(a + a + a + a + a + a + a + a + a + a - 1.0) < 0.000001) {
// C99 5.2.4.2.2p8 guarantees at least 10 decimal digits
// of precision for the double type.
printf("10 * 0.1 is almost 1.0.\n");
}
return 0;
}
もう一つの例:
gcc -O3 -g -I./inc -std=c11 -Wall -Wextra -Werror -Wmissing-prototypes -Wstrict-prototypes -Wold-style-definition rd11.c -o rd11 -L./lib -lsoq
#include <stdio.h>
#include <math.h>
static inline double rel_diff(double a, double b)
{
return fabs(a - b) / fmax(fabs(a), fabs(b));
}
int main(void)
{
double d1 = 3.14159265358979;
double d2 = 355.0 / 113.0;
double epsilon = 1.0;
for (int i = 0; i < 10; i++)
{
if (rel_diff(d1, d2) < epsilon)
printf("%d:%.10f <=> %.10f within tolerance %.10f (rel diff %.4E)\n",
i, d1, d2, epsilon, rel_diff(d1, d2));
else
printf("%d:%.10f <=> %.10f out of tolerance %.10f (rel diff %.4E)\n",
i, d1, d2, epsilon, rel_diff(d1, d2));
epsilon /= 10.0;
}
return 0;
}
出力:
0:3.1415926536 <=> 3.1415929204 within tolerance 1.0000000000 (rel diff 8.4914E-08)
1:3.1415926536 <=> 3.1415929204 within tolerance 0.1000000000 (rel diff 8.4914E-08)
2:3.1415926536 <=> 3.1415929204 within tolerance 0.0100000000 (rel diff 8.4914E-08)
3:3.1415926536 <=> 3.1415929204 within tolerance 0.0010000000 (rel diff 8.4914E-08)
4:3.1415926536 <=> 3.1415929204 within tolerance 0.0001000000 (rel diff 8.4914E-08)
5:3.1415926536 <=> 3.1415929204 within tolerance 0.0000100000 (rel diff 8.4914E-08)
6:3.1415926536 <=> 3.1415929204 within tolerance 0.0000010000 (rel diff 8.4914E-08)
7:3.1415926536 <=> 3.1415929204 within tolerance 0.0000001000 (rel diff 8.4914E-08)
8:3.1415926536 <=> 3.1415929204 out of tolerance 0.0000000100 (rel diff 8.4914E-08)
9:3.1415926536 <=> 3.1415929204 out of tolerance 0.0000000010 (rel diff 8.4914E-08)
ポインタ演算で余分なスケーリングを行う
ポインタ演算では、ポインタに加算または減算される整数はアドレスの変更ではなく、移動する要素の数として解釈されます。
#include <stdio.h>
int main(void) {
int array[] = {1, 2, 3, 4, 5};
int *ptr = &array[0];
int *ptr2 = ptr + sizeof(int) * 2; /* wrong */
printf("%d %d\n", *ptr, *ptr2);
return 0;
}
このコードは、 ptr2
割り当てられたポインタを計算する際に余分なスケーリングを行います。現代の32ビット環境で典型的なsizeof(int)
が4の場合、式はarray[0]
8つの要素( array[0]
後ろ)を表し、 未定義の動作を呼び出します。
ptr2
array[0]
後に2つの要素を指すようにするには、単に2を追加するだけです。
#include <stdio.h>
int main(void) {
int array[] = {1, 2, 3, 4, 5};
int *ptr = &array[0];
int *ptr2 = ptr + 2;
printf("%d %d\n", *ptr, *ptr2); /* "1 3" will be printed */
return 0;
}
加算演算子を使用した明示的なポインタ算術は混乱する可能性があるため、配列の添字を使用するほうが良いかもしれません。
#include <stdio.h>
int main(void) {
int array[] = {1, 2, 3, 4, 5};
int *ptr = &array[0];
int *ptr2 = &ptr[2];
printf("%d %d\n", *ptr, *ptr2); /* "1 3" will be printed */
return 0;
}
E1[E2]
同一である(*((E1)+(E2)))
N1570 6.5.2.1、段落2)、及び&(E1[E2])
と等価である((E1)+(E2))
N1570 6.5.3.2、脚注102)。
あるいは、ポインタ演算が望ましい場合、ポインタをキャストして異なるデータ型にアドレス指定すると、バイトアドレス指定が可能になります。ただし、 エンディアンが問題になることがあります。また、「文字へのポインタ」以外の型にキャストすると厳密なエイリアシングの問題が発生します。
#include <stdio.h>
int main(void) {
int array[3] = {1,2,3}; // 4 bytes * 3 allocated
unsigned char *ptr = (unsigned char *) array; // unsigned chars only take 1 byte
/*
* Now any pointer arithmetic on ptr will match
* bytes in memory. ptr can be treated like it
* was declared as: unsigned char ptr[12];
*/
return 0;
}
マクロは単純な文字列の置換です
マクロは単純な文字列置換です。 (厳密には、任意の文字列ではなく、前処理トークンで動作します。)
#include <stdio.h>
#define SQUARE(x) x*x
int main(void) {
printf("%d\n", SQUARE(1+2));
return 0;
}
このコードでは9
( 3*3
)を印刷すると予想されますが、マクロは1+2*1+2
展開されるため実際には5
が印刷されます。
この問題を回避するには、引数と全体のマクロ式をカッコで囲む必要があります。
#include <stdio.h>
#define SQUARE(x) ((x)*(x))
int main(void) {
printf("%d\n", SQUARE(1+2));
return 0;
}
別の問題は、マクロの引数が一度評価されることが保証されていないことです。それらはまったく評価されないかもしれないし、複数回評価されるかもしれない。
#include <stdio.h>
#define MIN(x, y) ((x) <= (y) ? (x) : (y))
int main(void) {
int a = 0;
printf("%d\n", MIN(a++, 10));
printf("a = %d\n", a);
return 0;
}
このコードでは、マクロは((a++) <= (10) ? (a++) : (10))
ます。 a++
( 0
)は10
より小さいのでa++
は2回評価され、それはa
の値を作り、 MIN
から返されるものはあなたとは異なるかもしれません。
これは、関数を使用することで回避できますが、型は関数定義で固定されますが、マクロは型によって柔軟性があることに注意してください。
#include <stdio.h>
int min(int x, int y) {
return x <= y ? x : y;
}
int main(void) {
int a = 0;
printf("%d\n", min(a++, 10));
printf("a = %d\n", a);
return 0;
}
現在、二重評価の問題は修正されていますが、このmin
関数はdouble
データを切り捨てることなく処理することはできません。
マクロ・ディレクティブには、次の2つのタイプがあります。
#define OBJECT_LIKE_MACRO followed by a "replacement list" of preprocessor tokens
#define FUNCTION_LIKE_MACRO(with, arguments) followed by a replacement list
これらの2つのタイプのマクロを区別するものは、 #define
後の識別子に続く文字です: lparenの場合、それは関数のようなマクロです。そうでなければ、それはオブジェクトのようなマクロです。関数のようなマクロを書くことが意図されている場合は、マクロの名前の末尾に空白を入れてはいけません(
詳細な説明はこれを確認してください)。
C99以降では、 static inline int min(int x, int y) { … }
使用できます。
C11では、 min
に対して '型ジェネリック'式を書くことができます。
#include <stdio.h>
#define min(x, y) _Generic((x), \
long double: min_ld, \
unsigned long long: min_ull, \
default: min_i \
)(x, y)
#define gen_min(suffix, type) \
static inline type min_##suffix(type x, type y) { return (x < y) ? x : y; }
gen_min(ld, long double)
gen_min(ull, unsigned long long)
gen_min(i, int)
int main(void)
{
unsigned long long ull1 = 50ULL;
unsigned long long ull2 = 37ULL;
printf("min(%llu, %llu) = %llu\n", ull1, ull2, min(ull1, ull2));
long double ld1 = 3.141592653L;
long double ld2 = 3.141592652L;
printf("min(%.10Lf, %.10Lf) = %.10Lf\n", ld1, ld2, min(ld1, ld2));
int i1 = 3141653;
int i2 = 3141652;
printf("min(%d, %d) = %d\n", i1, i2, min(i1, i2));
return 0;
}
一般的な式は、 double
、 float
、 long long
、 unsigned long
、 long
、 unsigned
ようなより多くの型と適切なgen_min
マクロ呼び出しで記述することができます。
リンク時の未定義の参照エラー
コンパイル時の最も一般的なエラーの1つは、リンク段階で発生します。エラーは次のようになります。
$ gcc undefined_reference.c
/tmp/ccoXhwF0.o: In function `main':
undefined_reference.c:(.text+0x15): undefined reference to `foo'
collect2: error: ld returned 1 exit status
$
このエラーを生成したコードを見てみましょう:
int foo(void);
int main(int argc, char **argv)
{
int foo_val;
foo_val = foo();
return foo_val;
}
foo( int foo();
)の宣言はここにありますが、実際の関数の定義はありません。コンパイラに関数ヘッダを提供しましたが、どこにもそのような関数は定義されていなかったので、コンパイル段階は終わりますが、リンカはUndefined reference
エラーで終了します。
小さなプログラムでこのエラーを修正するには、fooの定義を追加するだけです。
/* Declaration of foo */
int foo(void);
/* Definition of foo */
int foo(void)
{
return 5;
}
int main(int argc, char **argv)
{
int foo_val;
foo_val = foo();
return foo_val;
}
今度はこのコードをコンパイルします。代わりに、 foo()
ソースが別のソースファイルfoo.c
にあり、 foo.c
とundefined_reference.c
両方に含まれるfoo()
を宣言するためのヘッダfoo.h
があります。次に、 foo.c
とundefined_reference.c
両方のオブジェクトファイルをリンクするか、両方のソースファイルをコンパイルします。
$ gcc -c undefined_reference.c
$ gcc -c foo.c
$ gcc -o working_program undefined_reference.o foo.o
$
または:
$ gcc -o working_program undefined_reference.c foo.c
$
もっと複雑なケースは、コードのようにライブラリが関わっている場合です:
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
int main(int argc, char **argv)
{
double first;
double second;
double power;
if (argc != 3)
{
fprintf(stderr, "Usage: %s <denom> <nom>\n", argv[0]);
return EXIT_FAILURE;
}
/* Translate user input to numbers, extra error checking
* should be done here. */
first = strtod(argv[1], NULL);
second = strtod(argv[2], NULL);
/* Use function pow() from libm - this will cause a linkage
* error unless this code is compiled against libm! */
power = pow(first, second);
printf("%f to the power of %f = %f\n", first, second, power);
return EXIT_SUCCESS;
}
コードは構文的に正しいですが、 #include <math.h>
からpow()
宣言が存在するため、コンパイルとリンクを試みますが、次のようなエラーが発生します。
$ gcc no_library_in_link.c -o no_library_in_link
/tmp/ccduQQqA.o: In function `main':
no_library_in_link.c:(.text+0x8b): undefined reference to `pow'
collect2: error: ld returned 1 exit status
$
これは、リンク段階でpow()
定義が見つからなかったために発生します。これを修正するには、 -lm
フラグを指定してlibm
という数学ライブラリにリンクしたいと指定する必要があります。 ( -lm
が必要ないmacOSなどのプラットフォームがあることに注意してください。しかし、未定義の参照を取得すると、ライブラリが必要になります)。
そこで、コンパイルの段階をもう一度実行します。今回は、ライブラリを指定します(ソースファイルまたはオブジェクトファイルの後ろ)。
$ gcc no_library_in_link.c -lm -o library_in_link_cmd
$ ./library_in_link_cmd 2 4
2.000000 to the power of 4.000000 = 16.000000
$
そしてそれは動作します!
アレイの崩壊の誤解
多次元配列、ポインタの配列などを使用するコードの一般的な問題は、 Type**
とType[M][N]
が基本的に異なる型であるという事実です。
#include <stdio.h>
void print_strings(char **strings, size_t n)
{
size_t i;
for (i = 0; i < n; i++)
puts(strings[i]);
}
int main(void)
{
char s[4][20] = {"Example 1", "Example 2", "Example 3", "Example 4"};
print_strings(s, 4);
return 0;
}
コンパイラの出力例:
file1.c: In function 'main':
file1.c:13:23: error: passing argument 1 of 'print_strings' from incompatible pointer type [-Wincompatible-pointer-types]
print_strings(strings, 4);
^
file1.c:3:10: note: expected 'char **' but argument is of type 'char (*)[20]'
void print_strings(char **strings, size_t n)
このエラーは、 main
関数s
配列がprint_strings
関数に渡されたことを示しています。また、 print_strings
が予期する型を表すprint_strings
と、 main
から渡された型も含まれます。
この問題はアレイ減衰と呼ばれるものが原因です。何が起こるs
その型とchar[4][20]
(20文字の4列のアレイ)が関数に渡されると、それはあなたが書いたかのように、その最初の要素へのポインタになりされる&s[0]
有し、型char (*)[20]
(20文字の1配列へのポインタ)。これは、ポインターの配列、配列の配列(3-D配列)の配列、および配列へのポインターの配列を含む、どの配列でも発生します。以下は、配列が崩壊したときの状況を示す表です。タイプ記述の変更は、何が起こるかを示すために強調表示されています。
崩壊前 | 崩壊後 | ||
---|---|---|---|
char [20] | 配列(20文字) | char * | ポインタ(1文字) |
char [4][20] | 配列( 20文字のうちの4つの配列 ) | char (*)[20] | ( 20文字のうちの1つの配列 ) へのポインタ |
char *[4] | 配列の( 1つの文字への4つのポインタ ) | char ** | 1つのcharへのポインタ(1 )へのポインタ |
char [3][4][20] | 配列( 20文字の4配列の3配列 ) | char (*)[4][20] | ポインタ( 20文字の4配列の1配列 ) |
char (*[4])[20] | 配列( 20文字の1配列に対する4つのポインタ ) | char (**)[20] | (1文字の20文字からなる1つの配列へのポインタ)へのポインタ |
配列がポインタに崩壊する可能性がある場合、ポインタは少なくとも1要素の配列と見なすことができます。例外はヌルポインタで、これは何も指さず結果的には配列ではありません。
アレイの減衰は一度だけ起こります。配列がポインタに崩壊した場合、配列ではなくポインタになります。配列へのポインタがあっても、ポインタが少なくとも1つの要素の配列とみなされる可能性があるので、配列の減衰は既に発生していることに注意してください。
つまり、配列( char (*)[20]
)へのポインタは決してポインタ( char **
)へのポインタになりません。 print_strings
関数を修正するには、単に正しい型を受け取れるようにしてください:
void print_strings(char (*strings)[20], size_t n)
/* OR */
void print_strings(char strings[][20], size_t n)
問題は、 print_strings
関数を任意の文字配列に対して汎用的にしたい場合に発生しますprint_strings
個ではなく30個の文字がある場合はどうなりますか?または50?答えは配列パラメータの前に別のパラメータを追加することです:
#include <stdio.h>
/*
* Note the rearranged parameters and the change in the parameter name
* from the previous definitions:
* n (number of strings)
* => scount (string count)
*
* Of course, you could also use one of the following highly recommended forms
* for the `strings` parameter instead:
*
* char strings[scount][ccount]
* char strings[][ccount]
*/
void print_strings(size_t scount, size_t ccount, char (*strings)[ccount])
{
size_t i;
for (i = 0; i < scount; i++)
puts(strings[i]);
}
int main(void)
{
char s[4][20] = {"Example 1", "Example 2", "Example 3", "Example 4"};
print_strings(4, 20, s);
return 0;
}
これをコンパイルすると、エラーは発生せず、予期した出力が得られます。
Example 1
Example 2
Example 3
Example 4
隣接しない配列を "実際の"多次元配列を期待する関数に渡す
malloc
、 calloc
、およびrealloc
で多次元配列を割り当てる場合、一般的なパターンは複数の呼び出しで内側の配列を割り当てることです(呼び出しが1回だけ出現してもループ内であっても)。
/* Could also be `int **` with malloc used to allocate outer array. */
int *array[4];
int i;
/* Allocate 4 arrays of 16 ints. */
for (i = 0; i < 4; i++)
array[i] = malloc(16 * sizeof(*array[i]));
内部配列の1つの最後の要素と次の内部配列の最初の要素との間のバイトの差は、「実際の」多次元配列(たとえば、 int array[4][16];
) :
/* 0x40003c, 0x402000 */
printf("%p, %p\n", (void *)(array[0] + 15), (void *)array[1]);
int
のサイズを考慮すると、8328バイト(8132-4)の差が得られます。これは2032のint
配列要素です。これは問題です。「実数」多次元配列には要素間にギャップがありません。
"実際の"多次元配列を期待する関数で動的に割り当てられた配列を使用する必要がある場合は、 int *
型のオブジェクトを割り当てて算術演算を使用して計算を行う必要があります。
void func(int M, int N, int *array);
...
/* Equivalent to declaring `int array[M][N] = {{0}};` and assigning to array4_16[i][j]. */
int *array;
int M = 4, N = 16;
array = calloc(M, N * sizeof(*array));
array[i * N + j] = 1;
func(M, N, array);
N
が変数ではなくマクロまたは整数リテラルである場合、コードは配列へのポインタを割り当てた後に、より自然な2次元配列表記法を使うことができます。
void func(int M, int N, int *array);
#define N 16
void func_N(int M, int (*array)[N]);
...
int M = 4;
int (*array)[N];
array = calloc(M, sizeof(*array));
array[i][j] = 1;
/* Cast to `int *` works here because `array` is a single block of M*N ints with no gaps,
just like `int array2[M * N];` and `int array3[M][N];` would be. */
func(M, N, (int *)array);
func_N(M, array);
N
がマクロまたは整数リテラルでない場合、 array
は可変長配列(VLA)を指します。これは、 int *
キャストすることによってfunc
とともに使用することができますint *
と新しい関数func_vla
はfunc_vla
を置き換えfunc_N
:
void func(int M, int N, int *array);
void func_vla(int M, int N, int array[M][N]);
...
int M = 4, N = 16;
int (*array)[N];
array = calloc(M, sizeof(*array));
array[i][j] = 1;
func(M, N, (int *)array);
func_vla(M, N, array);
注 :VLAはC11ではオプションです。実装がC11をサポートしていて、 __STDC_NO_VLA__
マクロを1に定義している場合は、C99より前の方法に固執しています。
文字列リテラルの代わりに文字定数を使用する(逆も同様)
Cでは、文字定数と文字列リテラルは異なるものです。
'a'
ように一重引用符で囲まれた文字は文字定数です。文字定数は、文字を表す文字コードの値を持つ整数です。 'abc'
ような複数の文字を持つ文字定数をどのように解釈するかは、実装定義です。
"abc"
ような二重引用符で囲まれたゼロ以上の文字は文字列リテラルです。文字列リテラルは、要素がchar
型の変更不可能な配列です。ダブルクォートとヌル文字を終える文字列は内容なので、 "abc"
は4つの要素( {'a', 'b', 'c', '\0'}
)があります。
この例では、文字定数を使用する文字定数が使用されています。この文字定数は、実装定義の方法でポインタに変換され、変換されたポインタが有効である可能性はほとんどないので、この例では未定義の動作が呼び出されます 。
#include <stdio.h>
int main(void) {
const char *hello = 'hello, world'; /* bad */
puts(hello);
return 0;
}
この例では、文字定数を使用する必要がある場合は文字列リテラルが使用されます。文字列リテラルから変換されたポインタは、実装定義の方法で整数に変換され、実装定義の方法でchar
に変換されます。 (変換する値を表現できない符号付きの型に整数を変換する方法は実装定義であり、 char
が署名されているかどうかは実装定義です。)出力は無意味なものになります。
#include <stdio.h>
int main(void) {
char c = "a"; /* bad */
printf("%c\n", c);
return 0;
}
ほとんどの場合、コンパイラはこれらの混乱について不平を言うでしょう。そうでない場合は、より多くのコンパイラ警告オプションを使用するか、より良いコンパイラを使用することをお勧めします。
ライブラリ関数の戻り値を無視する
C標準ライブラリのほとんどすべての関数は、成功したときに何かを返します。例えば、 malloc
は、成功したときに関数によって割り当てられたメモリブロックへのポインタと、要求されたメモリブロックの割り当てに失敗した場合にはヌルポインタを返します。したがって、デバッグを容易にするために常に戻り値をチェックする必要があります。
これは悪いです:
char* x = malloc(100000000000UL * sizeof *x);
/* more code */
scanf("%s", x); /* This might invoke undefined behaviour and if lucky causes a segmentation violation, unless your system has a lot of memory */
これはいい:
#include <stdlib.h>
#include <stdio.h>
int main(void)
{
char* x = malloc(100000000000UL * sizeof *x);
if (x == NULL) {
perror("malloc() failed");
exit(EXIT_FAILURE);
}
if (scanf("%s", x) != 1) {
fprintf(stderr, "could not read string\n");
free(x);
exit(EXIT_FAILURE);
}
/* Do stuff with x. */
/* Clean up. */
free(x);
return EXIT_SUCCESS;
}
この方法でエラーの原因をすぐに知ることができます。そうしないと、間違った場所でバグを探して時間を費やすことがあります。
改行文字は、通常のscanf()呼び出しでは消費されません
このプログラム
#include <stdio.h>
#include <string.h>
int main(void) {
int num = 0;
char str[128], *lf;
scanf("%d", &num);
fgets(str, sizeof(str), stdin);
if ((lf = strchr(str, '\n')) != NULL) *lf = '\0';
printf("%d \"%s\"\n", num, str);
return 0;
}
この入力で実行されます
42
life
出力は次のようになります42 ""
ではなく期待の42 "life"
。
これは、 42
後の改行文字はscanf()
呼び出しで消費されず、 life
読む前にfgets()
によって消費されるためです。それでfgets()
life
読む前に読書を止めます。
この問題を回避するには、例えば、オンライン審判システムの問題を解決するときに、lineの最大長が分かっているときに便利な方法の1つは、 scanf()
直接使用せず、 fgets()
介してすべての行を読むことです。 sscanf()
を使用すると、読み取られた行を解析できます。
#include <stdio.h>
#include <string.h>
int main(void) {
int num = 0;
char line_buffer[128] = "", str[128], *lf;
fgets(line_buffer, sizeof(line_buffer), stdin);
sscanf(line_buffer, "%d", &num);
fgets(str, sizeof(str), stdin);
if ((lf = strchr(str, '\n')) != NULL) *lf = '\0';
printf("%d \"%s\"\n", num, str);
return 0;
}
もう一つの方法は、 scanf()
を使った後、そしてfgets()
を使う前に改行文字を打つまで読むことです。
#include <stdio.h>
#include <string.h>
int main(void) {
int num = 0;
char str[128], *lf;
int c;
scanf("%d", &num);
while ((c = getchar()) != '\n' && c != EOF);
fgets(str, sizeof(str), stdin);
if ((lf = strchr(str, '\n')) != NULL) *lf = '\0';
printf("%d \"%s\"\n", num, str);
return 0;
}
#defineにセミコロンを追加する
Cプリプロセッサで混乱してCの一部として扱うのは簡単ですが、プリプロセッサは単なるテキスト置換メカニズムなので、間違いです。たとえば、次のように記述します。
/* WRONG */
#define MAX 100;
int arr[MAX];
コードは次のように展開されます。
int arr[100;];
これは構文エラーです。対処方法は、セミコロンを#define
行から削除することです。 #define
をセミコロンで終わらせるのは間違いなく間違いです。
複数行コメントは入れ子にできません
Cでは、複数行のコメント/ *と* /はネストしません。
このスタイルのコメントを使用してコードブロックまたは機能ブロックに注釈を付けると、次のようになります。
/*
* max(): Finds the largest integer in an array and returns it.
* If the array length is less than 1, the result is undefined.
* arr: The array of integers to search.
* num: The number of integers in arr.
*/
int max(int arr[], int num)
{
int max = arr[0];
for (int i = 0; i < num; i++)
if (arr[i] > max)
max = arr[i];
return max;
}
あなたはそれを簡単にコメントにすることはできません:
//Trying to comment out the block...
/*
/*
* max(): Finds the largest integer in an array and returns it.
* If the array length is less than 1, the result is undefined.
* arr: The array of integers to search.
* num: The number of integers in arr.
*/
int max(int arr[], int num)
{
int max = arr[0];
for (int i = 0; i < num; i++)
if (arr[i] > max)
max = arr[i];
return max;
}
//Causes an error on the line below...
*/
1つの解決策は、C99スタイルのコメントを使用することです。
// max(): Finds the largest integer in an array and returns it.
// If the array length is less than 1, the result is undefined.
// arr: The array of integers to search.
// num: The number of integers in arr.
int max(int arr[], int num)
{
int max = arr[0];
for (int i = 0; i < num; i++)
if (arr[i] > max)
max = arr[i];
return max;
}
これで、ブロック全体を簡単にコメントにすることができます。
/*
// max(): Finds the largest integer in an array and returns it.
// If the array length is less than 1, the result is undefined.
// arr: The array of integers to search.
// num: The number of integers in arr.
int max(int arr[], int num)
{
int max = arr[0];
for (int i = 0; i < num; i++)
if (arr[i] > max)
max = arr[i];
return max;
}
*/
別の解決策は、 #ifdef
または#ifndef
プリプロセッサディレクティブを代わりに使用して、コメント構文を使用してコードを無効にすることを避けることです。これらのディレクティブはネストして、好みのスタイルでコードにコメントすることができます。
#define DISABLE_MAX /* Remove or comment this line to enable max() code block */
#ifdef DISABLE_MAX
/*
* max(): Finds the largest integer in an array and returns it.
* If the array length is less than 1, the result is undefined.
* arr: The array of integers to search.
* num: The number of integers in arr.
*/
int max(int arr[], int num)
{
int max = arr[0];
for (int i = 0; i < num; i++)
if (arr[i] > max)
max = arr[i];
return max;
}
#endif
いくつかのガイドは、コードセクションに決してコメントをつけてはならないことを推奨しており、コードを一時的に無効にする場合は#if 0
ディレクティブを使用することができます。
コードセクションをブロックするには、 #if 0を参照してください。
配列境界のオーバーステッピング
配列は0から始まります。つまり、インデックスは常に0から始まり、インデックス配列の長さから1を引いた値で終了します。したがって、次のコードは配列の最初の要素を出力せず、印刷する最終値にガベージを出力します。
#include <stdio.h>
int main(void)
{
int x = 0;
int myArray[5] = {1, 2, 3, 4, 5}; //Declaring 5 elements
for(x = 1; x <= 5; x++) //Looping from 1 till 5.
printf("%d\t", myArray[x]);
printf("\n");
return 0;
}
出力: 2 3 4 5 GarbageValue
以下は、望ましい出力を得るための正しい方法を示しています。
#include <stdio.h>
int main(void)
{
int x = 0;
int myArray[5] = {1, 2, 3, 4, 5}; //Declaring 5 elements
for(x = 0; x < 5; x++) //Looping from 0 till 4.
printf("%d\t", myArray[x]);
printf("\n");
return 0;
}
出力: 1 2 3 4 5
アレイの長さを知ることが重要です。そうしないと、バッファが壊れたり、範囲外のメモリ位置にアクセスしてセグメンテーション違反が発生する可能性があります。
再帰関数 - 基本条件が抜けている
数値の階乗を計算することは、再帰関数の古典的な例です。
基本条件がありません:
#include <stdio.h>
int factorial(int n)
{
return n * factorial(n - 1);
}
int main()
{
printf("Factorial %d = %d\n", 3, factorial(3));
return 0;
}
典型的な出力: Segmentation fault: 11
この関数の問題は、ループが無限にループし、セグメント化エラーが発生することです。再帰を停止するには基本条件が必要です。
ベース条件宣言:
#include <stdio.h>
int factorial(int n)
{
if (n == 1) // Base Condition, very crucial in designing the recursive functions.
{
return 1;
}
else
{
return n * factorial(n - 1);
}
}
int main()
{
printf("Factorial %d = %d\n", 3, factorial(3));
return 0;
}
サンプル出力
Factorial 3 = 6
この関数は、条件n
が1になるとすぐに終了します( n
の初期値が十分小さい - int
が32ビット量の場合、上限は12
です)。
従うべき規則:
- アルゴリズムを初期化します。再帰的プログラムでは、しばしば始まるシード値が必要です。これは、関数に渡されるパラメーターを使用するか、非再帰的であるが再帰的計算のためのシード値を設定するゲートウェイ関数を提供することによって実行されます。
- 処理されている現在の値がベースケースと一致するかどうかを確認します。そうであれば、値を処理して返します。
- 小規模またはより単純なサブ問題またはサブ問題の観点から答えを再定義します。
- サブ問題に対してアルゴリズムを実行します。
- 答えを定式化する際に結果を結合する。
- 結果を返します。
ソース: 再帰関数
論理式の '真'のチェック
オリジナルのC標準には本来のブール型がなかったので、 bool
、 true
、 false
は固有の意味を持たず、しばしばプログラマによって定義されました。通常true
は1と定義され、 false
は0と定義されます。
C99には組み込み型_Bool
と、 bool
( _Bool
展開)、 false
およびtrue
を定義するヘッダー<stdbool.h>
が追加されていtrue
。また、 bool
、 true
およびfalse
を再定義することもできますが、これは廃止された機能であることに注意してください。
さらに重要なのは、論理式は、評価するものがすべて偽とみなし、0以外の評価を真とみなします。例えば:
/* Return 'true' if the most significant bit is set */
bool isUpperBitSet(uint8_t bitField)
{
if ((bitField & 0x80) == true) /* Comparison only succeeds if true is 0x80 and bitField has that bit set */
{
return true;
}
else
{
return false;
}
}
上記の例では、関数は上位ビットがセットされているかどうかをチェックし、 true
あればtrue
を返しtrue
。しかし、 true
明示的にチェックするtrue
により、 if
文は、 (bitfield & 0x80)
true
と定義されていると評価される場合にのみ成功します。通常は1
あり、ほとんど0x80
ありません。あなたが期待するケースに対して明示的にチェックするか、
/* Return 'true' if the most significant bit is set */
bool isUpperBitSet(uint8_t bitField)
{
if ((bitField & 0x80) == 0x80) /* Explicitly test for the case we expect */
{
return true;
}
else
{
return false;
}
}
または、ゼロ以外の値をtrueとして評価します。
/* Return 'true' if the most significant bit is set */
bool isUpperBitSet(uint8_t bitField)
{
/* If upper bit is set, result is 0x80 which the if will evaluate as true */
if (bitField & 0x80)
{
return true;
}
else
{
return false;
}
}
浮動小数点型リテラルはデフォルトでdouble型です
float
型の変数をリテラル値に初期化するときや、 0.1
などの通常の浮動小数点型リテラルがdouble
型であるため、それらをリテラル値と比較するときは注意が必要です。これは驚きにつながる可能性があります:
#include <stdio.h>
int main() {
float n;
n = 0.1;
if (n > 0.1) printf("Wierd\n");
return 0;
}
// Prints "Wierd" when n is float
ここで、 n
は初期化され、単精度に丸められ、その結果0.10000000149011612という値になります。次に、 n
は倍精度に変換されて0.1
リテラル(0.10000000000000001に等しい)と比較され、不一致が生じます。
丸め誤差に加えて、 float
変数をdouble
リテラルと混在さfloat
と、ハードウェアで倍精度をサポートしていないプラットフォームではパフォーマンスが低下します。