【※ 当記事は2020年7月26日時点の情報です】
ペイヴメント(@pavement1234)です。
C言語の基本的な文法をざっくり網羅的に知りたい。
バージョン情報
Windows10 Home(64bit)1903
VMware Player 15上で動くUbuntu18.04
後編で出来ること
C言語の基本的な文法をざっくり紹介します。
極力さらっと読める感じにしていますが、
たまにマニアックな話題(中・上級者向けのネタ)に触れています。
後編では
⑬標準入出力とエスケープシーケンス、
⑭文字列操作、
⑮ファイルI/O、
⑯ビット演算、
⑰コマンドライン引数、
⑱ファイル分割、
⑲読みやすいプログラム(可読性向上)、
⑳その他を説明します。
⑬標準入出力とエスケープシーケンス、
⑭文字列操作、
⑮ファイルI/O、
⑯ビット演算、
⑰コマンドライン引数、
⑱読みやすいプログラム(可読性向上)
⑬標準入出力とエスケープシーケンス
標準入出力
標準入出力は、
標準入力(stdin)というキーボードからの文字入力と、
標準出力(stdout)という画面への文字表示のことです。
<stdio.h>をインクルードすることで使えるようになります。
※標準エラー出力(stderr)は今回割愛します。
標準出力に文字を出すにはprintf()を使えばよいです。前編・中編でよく登場するので説明は割愛します。
では標準入力はどうすれば良いのでしょうか?
いろいろありますがfgets()を使うことをお勧めします。
※gets(), scanf()は推奨しません。
サンプルプログラム:標準入出力
1 2 3 4 5 6 7 8 9 |
#include <stdio.h> int main( int argc, char** argv ) { char buf[ 32 ]; fgets( buf, 32, stdin ); printf( "%s\n", buf ); return 0; } |
解説します
5行目:32バイトの文字列を保存するバッファを確保します。
6行目:stdin(標準入力)からfgetsで文字列を32文字取り込みます。
正確には31文字+\0(終端)が読み込まれます。
fgetsをstdinを指定して呼び出すと
キーボード入力待ちになりますので文字を入力してください。
7行目:バッファをprintfで表示します。
これを実行すると、こうなります。
$ gcc -g -o test test.c
$./test
(キーボード入力待ちになります)
1111111111222222222233333333334444444444(あえて40文字入力します)
1111111111222222222233333333334(31文字表示されました)
gets()、scanf()という関数でも
標準入力を取り込むことができますが推奨されていません。
理由はバッファオーバフローを考慮しない(できない)ためです。
例えば、
文字を入力するために10バイトの領域を確保してgets()、scanf()を呼び出し、
11文字以上入力するといくらでも読み込もうとするので大変危険。
この脆弱性を悪用して大量の文字を入力させると
最悪の場合システムが落ちてしまいます。
入力文字数を指定できるfgetsをstdinで呼び出すのが安全なので、
覚えておいてください。
エスケープシーケンス
エスケープシーケンスとは特殊文字のことです。
printf()で文字列を表示するときに
末尾に付けているオマジナイ\n(改行)もエスケープシーケンスの1つです。
エスケープシーケンス | 意味 |
\a | Bell(Beep音) |
\b | Backspace(バックスペース) |
\f | Formfeed(改ページ) |
\n | Newline(改行) |
\r | Return(復帰:先頭に戻る) |
\t | Tab(タブ) |
\\ | Backslash(バックスラッシュ、円マーク) |
\’ もしくは ’’ | Single Quate(シングルクォート) |
\” もしくは ”” | Double Quate(ダブルクォート) |
\0 | Null(NULL文字) |
\x[00-FF] |
Hexadecimal representation(16進数表現) |
【ノウハウ】ASCIIコード表
赤字は制御文字のため印字できません。
printfのフォーマット(書式)
printfのfはフォーマット(書式)のf。
何気なくつかっているprintf()の%d、%sみたいなやつを
フォーマット指定子といいます。
指定子 | 型 | 説明 | 例 |
%c | char | 1文字出力 | “%c” |
%s | char* | 文字列出力 | “%s” |
%d | short, int | 符号あり整数を10進出力 | “%d” |
%ld |
long | 符号あり長整数を10進出力 | “%ld” |
%o | short, int, unsigned short, unsigned int | 整数を8進出力 | “%03o” |
%x | short, int , unsigned short, unsigned int | 整数を16進出力 | “%04x” |
%f | float | 短精度実数を出力 | “%4.2f |
%e | float | 短精度実数を指数表示で出力 | “%4.2e” |
%g | float | 短精度実数を最適な表示で出力 | “%g” |
%u | unsigned short, unsigned int | 符号なし整数を10進出力 | “%ld” |
%lu | unsigned long | 符号なし長整数を10進出力 | “%lu” |
%lo | long, unsingned long | 長整数を8進出力 | “%lo” |
%lx | long, unsigned long | 長整数を16進出力 | “%lx” |
%lf | double | 倍精度実数を出力 | “%lf” |
【ノウハウ】表示桁数の指定
実数の場合
<全体の幅>.<小数点以下の桁数>で指定する。
文字列の場合
.<小数点以下の桁数>で最大文字数を指定する。
デフォルトは右詰め。
例 | 出力 |
printf( “[%8.3f”], 12345.6789 ); | [ 12345.67 ] |
printf( “[%8.3e]“, 12345.6789 ); | [ 1.234e+4 ] |
printf( “[%10s]“, “ABCDEFG” ); | [ ABCDEFG ] |
printf( “[%.3s]“, “ABCDEFG” ); | [ ABC ] |
【ノウハウ】ゼロ詰め
数値の場合、
桁数指定の前に0を付加するとゼロ詰めを指定できる。
例 | 出力 |
printf( “[%05d”], 1 ); | 00001 |
【ノウハウ】右詰め左詰め
デフォルトは右詰め。
桁数指定の前に-(マイナス)を付加すると左詰めを指定できる。
例 | 出力 |
printf( “[%-5d”], 1 ); | [ 1 ] |
【ノウハウ】符号の指定
デフォルトは正の数に+符号が付かない。
桁数指定の前に+(プラス)を付けることで符号をONにできる。
例 | 出力 |
printf( “[%+5d”], +123 ); | [ +123] |
printf( “[%+5d”], -123 ); | [ -123] |
⑭文字列操作
文字列とはchar型の配列に複数の文字を並べたものです。
1 2 |
char string[] = { "ABCDE" }; printf( "ABCDE" ); |
文字列を操作するための便利関数が<string.h>に定義されています。
コピー
文字列コピーはstrcpy( Dst, Src );という構文です。
SrcサイズよりDstサイズが小さい場合、
Dstの後ろの領域を破壊するので推奨しません。
strcpyではなく、strncpyを使いましょう。
サンプルプログラム:strcpy(危険)
1 2 3 4 5 6 7 8 9 10 11 |
#include <stdio.h> #include <string.h> int main( int argc, char** argv ) { char src[] = { "ABCDE\0" }; char dst[ 32 ]; strcpy( dst, src ); printf( "%s", dst ); return 0; } |
解説します。
2行目:<string.h>をインクルードしました。これでstrcpy()が使えます。
6行目:コピー元(src)の文字列を定義(実体確保)。
7行目:コピー先(dst)のバッファを確保。
8行目:コピー元(src)をコピー先(dst)にstrcpy()でコピー。
9行目:コピー先(dst)をprintf()で表示。
文字列コピー(文字数指定)はstrncpy( Dst, Src, 文字数 );という構文です。
コピーする文字数を指定できるので安全です。
サンプルプログラム:strncpy(安全)
1 2 3 4 5 6 7 8 9 10 11 |
#include <stdio.h> #include <string.h> int main( int argc, char** argv ) { char src[] = { "ABCDE\0" }; char dst[ 32 ]; strncpy( dst, src, 6 ); printf( "%s", dst ); return 0; } |
解説します。
8行目:コピー元(src)をコピー先(dst)にstrcnpy()で
”ABCDE”+”\0”の合計6文字分コピー。
※他はstrcpy()と全く同じなので説明しません。
【ノウハウ】”\0”をコピーするのが重要です
コピー先(dst)が0に初期化されていれば良いのですが、
未初期化だとスタックメモリはたいてい不定値(ゴミ)が入ってます。
コピー元(src)の末尾が\0(終端)になってるので、
6文字コピーするとコピー先(dst)の末尾に\0(終端)が付くので
printf()で”ABCDE”と表示させることが可能。
コピー元(src)の末尾\0(終端)になってるが、
5文字しかコピーしないとコピー先(dst)の末尾に\0(終端)が付かない。
printf()で表示すると”ABCDE・・・・・”みたいに
不定値エリアも表示しようとして文字化けする
(奇跡的に”\0″が見つかればそこでSTOPするが期待できない)。
比較
文字列比較は strcmp( 文字列1, 文字列2 ); という構文です。
strcmpは文字列の大小(ASCIIコードの大きい方が大)で
以下の戻り値を返します。
文字列1 > 文字列2 正の数
文字列1 < 文字列2 負の数
文字列1 = 文字列2 0(ゼロ)
サンプルプログラム:strcmp
1 2 3 4 5 6 7 8 9 10 11 |
#include <stdio.h> #include <string.h> int main( int argc, char** argv ) { char str1[] = { "ABCDE" }; char str2[] = { "AAAAA" }; int ret = strcmp( str1, str2 ); printf( "%d", ret ); return 0; } |
解説します。
2行目:<string.h>をインクルードしました。
これでstrcmp()が使えます。
3行目:文字列1(str1)に”ABCDE”を設定。
4行目:文字列2(str2)に”AAAAA”を設定。
5行目:文字列1”ABCDE”と文字列2”AAAAA”をstrcmp()で比較。
文字列1の2文字目:B(0x42)、
字列2の2文字目:A(0x41)をASCIIコードで比較して
B>Aになるので正の数が返ります。
【ノウハウ】文字列比較(文字数指定)は
strncmp( 文字列1, 文字列2, 文字数 );という構文です。
先頭から3文字だけ比較したい、みたいな用途で使えます。
連結
文字列連結はstrcat( Dst, Src );という構文です。
サンプルプログラム:strcat
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#include <stdio.h> #include <string.h> int main( int argc, char** argv ) { char src1[] = { "ABC\0" }; char src2[] = { "DEF\0" }; char dst[ 32 ]; strncpy( dst, src1, 4 ); strcat( dst, src2 ); printf( "%s", dst ); return 0; } |
解説します。
2行目:<string.h>をインクルードしました。
これでstrcat()が使えます。
3行目:文字列1(str1)に”ABC\0″を設定。
4行目:文字列2(str2)に”DEF\0″を設定。
5行目:連結文字列(dst)を定義。
6行目:連結文字列(dst)に文字列1(src1)を4文字分コピー。※”\0”を含む。
7行目:連結文字列(dst)に文字列2(src2)を連結。
プログラムを実行すると”ABC”と”DEF”が連結されて
”ABCDEF”という文字列が表示されます。
図にするとこんな感じ。
⑮ファイルI/O
例えばC言語のプログラムを記述している「test.c」をファイルと呼びます。
ファイルI/O(読み書き)は以下の手順で処理します。
ファイル読み出し
「test.c」を1行ずつ読み込んでprintf()するプログラムを書きました。
サンプルプログラム:ファイル読み出し
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
#include <stdio.h> int main( int argc, char** argv ) { // ファイルポインタ定義 FILE *fp; // ファイルオープン fp = fopen( "./test.c", "r" ); if( fp == NULL ){ // エラー終了 printf("file open failed.\n"); return -1; } // ファイル読み込み char str[ 256 ]; while( NULL != fgets( str, 256, fp ) ){ printf("%s", str ); } // ファイルクローズ fclose( fp ); return 0; } |
解説します。
1行目:<stdio.h>をインクルード。ファイルI/Oが使えるようになります。
6行目:ファイルポインタ(FILE *fp)を定義。
9行目:ファイル”./test.c”を読み出し専用モード”r”でオープン。
10行目:ファイルポインタ(fp)がNULLの場合、
オープン失敗なので12行目でエラーメッセージを出して
13行目でmain関数から抜ける。
17行目:ファイルから文字列を読み出すためのバッファを作成。
18行目:fgets()で1行読み出し。
EOF(ファイル終端)になるとNULLを返すので、
そこまで1行読み出しが続く。
19行目:fgets()で1行読み出したデータをprintf()する。
23行目:ファイルをクローズする。
ファイル書き込み
「test.c」を1行ずつ読み込んで
「test2.c」にfputs()するプログラムを書きました。
サンプルプログラム:ファイル書き込み
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
#include <stdio.h> int main( int argc, char** argv ) { // ファイルポインタ定義 FILE *fin; FILE *fout; // 入力ファイルオープン fin = fopen( "./test.c", "r" ); if( fin == NULL ){ // エラー終了 printf("file open failed.\n"); return -1; } // 出力ファイルオープン fout = fopen( "./test2.c", "w" ); if( fout == NULL ){ // エラー終了 printf("file open failed.\n"); return -1; } // ファイル読み込み&書き込み char str[ 256 ]; while( NULL != fgets( str, 256, fin ) ){ fputs( str, fout ); } // ファイルクローズ fclose( fin ); fclose( fout ); return 0; } |
解説します。
17行目:ファイル”./test2.c”を書き込み専用モード”w”でオープン。
27行目:fgets()で1行読み出したデータをfputs()する。
※1つ前の例と重複する説明は割愛しました。
【ノウハウ】ファイルオープンのモード
モード | 動作 | ファイルがある | ファイルがない |
“r” | 読み出し専用 | 正常 | エラー(NULL) |
”w” | 書き込み専用 | サイズ0のファイルで上書き | 新規作成 |
”a” | 追記専用 | 末尾に追記 | 新規作成 |
“r+” | 読み書き | 正常 | エラー(NULL) |
”w+” | 書き読み | サイズ0のファイルで上書き | 新規作成 |
”a+” | 読み込みと追記 | 末尾に追記 | 新規作成 |
【ノウハウ】バイナリモード
バイナリモード | 動作 |
デフォルト |
テキストモード |
”b” |
バイナリモード |
※バイナリファイルの読み書きはfread()、fwrite()で行います。
⑯ビット演算
ビット演算は
組込CPU(マイコンとかアプリケーションプロセッサ)の
レジスタ制御を行うときによく使います。
サンプルプログラム:シフト演算
1 2 3 4 5 6 7 8 9 |
#include <stdio.h> int main( int argc, char** argv ) { unsigned int a = 0x00000001; a <<= 4; // 4bit左シフト printf("0x%08x\n", a ); return 0; } |
解説します。
5行目:unsigned int型の変数aを定義し0x00000001を代入。
6行目:4ビットシフトします。
7行目:変数aをHEX(16進)表示します。
要はこういうことですね。
コンパイル&実行の結果は以下の通りです。
$ gcc -g -o test test.c
$./test
0x00000010
【ノウハウ】ビットフィールド
構造体の各メンバにビット幅を指定することで、
ビット単位の変数を管理できます。
サンプルプログラム:ビットフィールド
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
#include <stdio.h> typedef struct _reg{ unsigned char a0 : 1; unsigned char a1 : 1; unsigned char a2 : 1; unsigned char a3 : 1; unsigned char a4 : 1; unsigned char a5 : 1; unsigned char a6 : 1; unsigned char a7 : 1; }REG; int main( int argc, char** argv ) { REG reg; reg.a0 = 1; reg.a1 = 0; reg.a2 = 1; reg.a3 = 0; reg.a4 = 1; reg.a5 = 0; reg.a6 = 1; reg.a7 = 0; printf( "%d%d%d%d%d%d%d%db", reg.a7, reg.a6, reg.a5, reg.a4, reg.a3, reg.a2, reg.a1, reg.a0 ); return 0; } |
コンパイル&実行の結果は以下の通りです。
$ gcc -g -o test test.c
$./test
01010101b
⑰コマンドライン引数
main関数の引数argcとargvの使い方を説明します。
入力パラメタ数+1がargcに入り、
入力パラメタの文字列がargvに入ります。
サンプルプログラム:コマンドライン引数
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#include <stdio.h> int main( int argc, char** argv ) { if( argc < 2 ){ printf( "no parameter." ); return 1; } int i; for( i = 0; i < argc; i++ ){ printf( "param[%s]\n", argv[ i ] );<br /> } return 0; } |
解説します。
5~8行目:argcが2より小さい場合、
コマンド名以外のパラメタは入力されていないので
”no parameter.”と表示して抜けます。
10~12行目:argc数分だけループを回してargvを表示します。
argv[ 0 ]はプログラム名が入り
argv[ 1 ]以降が実際のパラメタになります。
コンパイル&実行の結果は以下の通りです。
$ gcc -g -o test test.c
$./test
no parameter.
$./test 1
param[test12.exe]
param[1]
$./test A B C
param[test12.exe]
param[A]
param[B]
param[C]
⑱読みやすいプログラム(可読性向上)
インデント
インデントとは字下げのことです
字下げをすることでプログラムの読みやすさが向上します。
まずはインデントが無いプログラムを見てみます。
かなり読みにくいと思いますが、いかがでしょうか?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#include <stdio.h> int main( int argc, char** argv ) { switch( argc ){ case 0: break; case 1: break; case 2: break; default: break; } printf("argc = %d\n", argc ); return 0; } |
次にインデントが有るプログラムを見てみます。
だいぶ見やすくなりました。
C言語では4文字の字下げが一般的。
HTMLは2文字の字下げが良いと聞いたことがあります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#include <stdio.h> int main( int argc, char** argv ) { switch( argc ){ case 0: break; case 1: break; case 2: break; case 3: break; default: break; } return 0; } |
コメント
コーディングしながらコメントを入れておくことをオススメします。
プログラムをコーディングして1週間もすると
何を書いたか忘れてしまうことが多いため、
備忘録として残しておくと良いです。
例えばプログラムを他の人に引き継ぐときにコメントを入れておくと
イチイチ説明しなくても、そこそこ伝わることもあるので。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
/** test.c 2020/07/26 */ #include <stdio.h> // 標準入出力 /** メイン関数 @param argc [in] argument counter @param argv [in] argument vector @return 0:正常、0以外:エラー @note なし */ int main( int argc, char** argv ) { // HelloWorldを表示する printf( "HelloWorld\n" ); return 0; } |
まとめ
おわりです。そのうち応用編書きます。