苦学楽学塾 ホームページ
最終更新日:2025年1月2日
1.構造体
Python,JavaScript,C言語の上位規格であるC++には、クラスという考え方があり、オブジェクト指向言語と言われますが、C言語にはオブジェクト指向としての性格はありません。その代わりをするものが構造体です。クラスは、C言語の構造体を発展させ、継承という概念を加えたものということができます。オブジェクト指向のクラスと同様、C言語の構造体も、種々のデータ、関数(メソッドという言い方はしませんが)を持つことができます。
例えば、あるスーパーで、商品を扱う処理を構造体を用いて行うとき、商品名、種類、メーカー、価格、当日販売数、累積販売数、在庫数、仕入れ価格、仕入れ日、展示位置、値引き率と言ったデータと、販売数から在庫数を修正し、在庫不足の場合に警告する関数sales(),仕入れ日からの日数で累積販売量を割った数値から値引き率を計算する関数discount(),商品名・価格から値札を作成する関数pricetag()と言った関数を有する構造体を用意するのであれば、
struct good {
char *name; // 商品名
int type; // 商品タイプ
char *maker; // メーカー名
int price; // 価格
int salescount_day; // 当日販売数
int salescount_cum; // 累積販売数
int stock; // 在庫数
int purchaseprice; // 仕入れ価格
time_t purchaseday; // 仕入れ日
int location; // 商品展示位置
float discountrate; // 値引き率
unsigned char *priceimg; // 値札画像へのポインタ
int (*sales)(struct good *); // 在庫チェック
float (*discount)(); // 値引き率を計算する
int (*pricetag)(); // 値札画像を制作する
};
とします。構造体内の各データを構造体のメンバーと言います。構造体を定義するときは、structに続けて構造体名(上記ではgood)を書き、'{'と'}'の間に構造体のメンバーの定義を書きます。メンバーには関数を入れることもできます。
まず、在庫チェック関数として、以下のようなfunc1()を用意します。
int func1(struct good *pg1) {
pg1->salescount_cum += pg1->salescount_day;
pg1->stock -= pg1->salescount_day;
if (pg1->stock < 10) {
printf("%sの仕入れが必要です。現在庫数:%d\n", pg1->name, pg1->stock);
return -1;
}
return 0;
}
構造体goodを使って、菓子部門(商品タイプ3791とします)に、令和製菓の「まろやかチョコレート」という商品を、価格230円、在庫数100個、仕入れ価格159円で2024年5月21日に仕入れたとして、この商品のデータを登録する際には、以下のように構造体の実体(オブジェクト指向で言うところのインスタンスのようなもの)を設定します。事前にgood構造体が宣言されており、構造体goodの実体g1と、構造体goodの配列goodsを定義し、さらに、在庫チェック関数が上記のfunc1(),値引き率計算関数がfunc2(),値札画像制作関数がfunc3()だとします。あらかじめ、
int goodnum = 0; // 商品番号
time_t t1;
struct tm tm1;
struct good g1;
struct good goods[30000];
と変数を宣言しておき、
tm1.tm_year = 2024;
tm1.tm_mon = 4; // 月-1に注意
tm1.tm_mday = 21;
t1 = mktime(&tm1);
・・・・・・
g1.name = "まろやかチョコレート";
g1.type = 3791;
g1.maker = "令和製菓";
g1.price = 230;
g1.stock = 100;
g1.purchaseprice = 159;
g1.purchaseday = t1;
g1.sales = func1;
g1.discount = func2;
g1.pricetag = func3;
goods[goodnum++] = g1;
として、構造体の実体のメンバーにまろやかチョコレートのデータをセットします。
ここでは、good構造体の配列goodsが要素数30000として確保され、30000種の商品のデータを格納できます。最後にgoods[goodnum++] = g1とすれば、構造体g1の全メンバーの値が、配列要素にまとめて代入されます。goodnumは商品番号のカウンタで、最初0で、商品を登録するたびに、goodnum++として1ずつ増えていきます。g1のメンバーは、メンバーnameであれば、'.'を用いて、g1.nameとしてアクセスします。また、構造体のデータが位置するアドレスを値とするポインタpg1(=&g1)を使って、メンバーnameにアクセスするときは、'->'を用いて、pg1->nameとします。
g1.name = "まろやかチョコレート"として、g1.nameに商品名が設定されますが、g1.nameに入るのは、文字列"まろやかチョコレート"の先頭アドレスです。文字列本体が構造体実体に入るわけではありません(文字列本体を構造体実体の中に入れてしまうこともできないわけではありません)。上記のfunc1()の定義は、int func1(struct good *pm1)となっていますが、構造体内に関数への参照を入れる場合には、メンバー名の整数値を返す関数へのポインタ(こうするのは、関数名が保持するのは、メモリ上に位置する関数のコードの先頭アドレスだからです)のsalesのみ書き、引数名は省略し、
int (*sales)(struct good *);
と記述します。構造体goodのメンバーsalesに関数func1()を設定する場合は、
g1.sales = func1;
のようにして、関数へのポインタを保持するg1.salesに代入する形で、関数名func1をセットします。
構造体のメンバーである関数を呼び出す場合は以下のようにします。
g1.salescount_day = 7;
printf("7個売れました。\n");
g1.sales(&g1);
g1.salescount_day = 85;
printf("85個売れました。\n");
g1.sales(&g1);
画面には、
7個売れました。
85個売れました。
まろやかチョコレートの仕入れが必要です。現在庫数:8
と表示されます。g1.sales(&g1)で、引数が構造体g1そのものでなく、g1へのポインタ&g1になっているのは、構造体のサイズが96バイトもあり大きいからです。g1へのポインタであれば8バイトで済みます。g1.salesにセットされている関数func1()の本体では、int func1(struct good *pg1)としていて、引数をgood構造体へのポインタpg1としていて、ここに、g1の位置するアドレスが入っています。g1自体ではなく、g1へのポインタpg1を用いて、g1のメンバーにアクセスするためには、'->'演算子を用います。メンバーnameであれば、pg1->nameとします。
関数func1()では、累積販売数pg1->salescount_cumに、当日販売数pg1->salescount_dayを加え、在庫数pg1->stockから当日販売数pg1->salescount_dayを引き、在庫数が10未満になると、「仕入れが必要です」というメッセージを出すようになっています。
また、上記でtm構造体が出てきますが、この構造体は、tm_year(年を表します),tm_mon(月の数値から1引いた値を表します。5月なら4です),tm_mday(日の数値を表します)というメンバーを持っており、これらを設定して、構造体へのポインタをmktime()関数に渡すと、time_t型(CYGWINでは、巡り巡ってsys/_types.hというファイルでlong型にdefineされています)の時刻データが返ってきます。これらの機能を使うためには、<time.h>をincludeしてください。
2.共用体
HTMLなどで文字色などを指定する場合に、crimsonという色を指定する場合、RGBで#dc143cと設定しますが、赤の強さがdc,緑の強さが14,青の強さが3cです。色コードから、それぞれの色の強さを求めたい、というような場合に共用体が使えます。共用体の宣言は構造体と同様に行いますが、構造体とは違い、共用体のメンバーは、全て共用体の先頭アドレスからメモリ上に配置されます。
共用体は、以下のように定義します。
union rgb {
int code;
unsigned char c[4];
}
整数型変数codeと4バイトの配列c[4]は、メモリ上の同じ位置に重ねて配置されます。変数color1を
union rgb color1;
と宣言します。共用体のメンバへのアクセスも構造体と同様に行います。
color1.code = 0xdc143c;
printf("rgb:%06x, r:%02x, g:%02x, b:%02x\n", color1.code, color1.c[2], color1.c[1], color1.c[0]);
color.c[2] = 0x37;
color.c[1] = 0xc2;
color.c[0] = 0x5e;
printf("rgb:%06x, r:%02x, g:%02x, b:%02x\n", color1.code, color1.c[2], color1.c[1], color1.c[0]);
とすると、画面に、
rgb:dc143c, r:dc, g:14, b:3c
rgb:37c25e, r:37, g:c2, b:5e
と表示されます。
3.例外処理
C言語にも、Pythonのtry~exceptの処理と似たような仕組みがあります。Pythonのtry~exceptに似ている、というよりもJavaScriptのaddEventListnerメソッドに近い働きをする関数として、signal()関数があります。プログラム実行中にCtrl-Cを押すと、プログラムが強制的に実行停止しますが、強制停止させずに、実行を継続させることができます。Ctrl-Cキー押下を検出すると、SIGINT例外が発生しますが、signal()関数により、この例外を捕捉することができます。事前に、handler1()関数を用意しておき、sleep()関数を用いて10秒休ませるようにして、
signal(SIGINT, handler1);
sleep(10);
を実行すると、PCがお休みになりますが、10秒以内にCtrl-Cを押すと、SIGINT例外が発生して、処理が関数handler1()に移ります。handler1()実行後は、例外を起こした位置の次の行に処理が進みます。
他に、alarm()関数によってSIGALRM例外を起こして、指定時間後に確認作業を行うといったことも可能です。sleep(10)と同様のことをalarm()関数によって実現させる例を以下に掲げます。まず、2つのグローバル変数を用意します。
int tenseconds = 0;
int handler1on = 0;
tensecondsは、alarm()関数によりSIGALRM例外を発生させて10秒後を検出すると1になります。handler1onは、handler1()実行中に1にしておき、handler1()実行中にSIGALRM例外が重複して発生しても10秒後検出tensecondsが1にならないようにします(但し、実はこの程度の例外重複監視では甘いのです。handler1()が起動していてhandler1on = 1が実行される前に、SIGALRM例外が発生してhandler2()が起動してしまう、など、微妙なタイミングで2つの例外が重なると深刻な状況に陥ることがあります。例外の重複が銀行のシステム障害などの原因になることもあります。鳥取支店のATMから入金のデータが送られてきて、その処理をやろうとしたまさにその瞬間に秋田支店から口座引き出しのデータが送られてきた、という不幸な重複があると、トラブルになりかねません)。handler1(),handler2()は以下のようにします。
void handler1() {
handler1on = 1;
printf("Ctrl-Cを検出しました。\n");
signal(SIGINT, handler1);
handler1on = 0;
}
void handler2() {
if (handler1on) {/* handler1実行中はもう1秒伸ばす */
signal(SIGALRM, handler2);
alarm(1);
}
else tenseconds = 1;
}
上記では、handler1()実行中にhandler2()が起動してしまった場合、さらに1秒後にSIGALRM例外を発生させます。main()関数では、
signal(SIGINT, handler1);
signal(SIGALRM, handler2);
printf("10秒間休憩します。Ctrl-Cで起こしてください。\n");
alarm(10);
while(1) {
if (tenseconds) {
printf("10秒経過したので起きます。\n");
break;
}
exit(0);
}
とすると、signal()関数により、SIGINT例外発生時にhandler1()を起動させ、SIGALRM例外発生時にhandler2()を起動させるようにします。その後、while(1)として無限ループを作り、handler2()により、tensecondsが1になると、無限ループを打ち切り終了します。実行させると、画面に、
10秒間休憩します。Ctrl-Cで起こしてください。
と表示しますが、10秒以内にCtrl-Cを押下すると、押下するたびに、
Ctrl-Cを検出しました。
と表示し、10秒後に、
10秒経過したので起きます。
と表示します。
alarm()関数、signal()関数では、JavaScriptのsetTimeoutメソッドほどの細かい制御は行えないので、アニメーション表示などにはとても使えませんが、時間のかかる作業の監視などに利用できます。なお、signal()関数を利用するためには<signal.h>を、sleep()関数、alarm()関数を利用するためには<unistd.h>を、includeしてください。
例外には、その他に、ユーザ定義のSIGUSR1,SIGUSR2という例外があり、raise()関数で、raise(SIGUSR1),raise(SIGUSR2)とすることにより、SIGUSR1例外、SIGUSR2例外を発生させることができます。signal(SIGUSR1, handler)として、SIGUSR1例外発生時に、handler()関数に制御を移すことができます。プログラム内のエラー対応に利用できます。nullポインタの先に書き込むと発生するSIGSEGV(記憶保護例外),abort()関数が発生するSIGABRTなどもあります。
4.プリ・プロセッサ
プリ・プロセッサは、C言語コンパイラのパーサ(構文解析)がソース・プログラムの解析を始める前に、構文解析の流れを制御できるように、ソース・プログラムを変化させるツールです。プリ・プロセッサの命令は、ディレクティブと呼ばれます。ディレクティブの先頭には'#'をつけて、ソース・プログラム本体と区別できるようにします。ディレクティブには、#include,#define,#if,#endif,#elif,#else,#ifdef,#ifndefなどがあります。C言語の文ではないので、末尾に';'を付けないことに注意してください。
#include
ヘッダ・ファイルの読み込みに使います。C言語が提供しているヘッダ・ファイル(決められた場所に置かれています)を読み込む場合は、ファイル名を'<','>'でくくります。自作のヘッダ・ファイル名は、"~"でくくります。C言語のソース・プログラムと同じ場所に置くのが原則です。そうでない場合には、どこに存在するのかパスを指定する必要があります。
#include <stdio.h>
#include "test.h"
#include "../../mydir/header/test.h"
のように指定します。
#define
定数、定数名、変数名、構造体名を言い換えるのに使います。長い変数名の短縮形を作る場合、同一の数値を複数回参照する場合などに便利です。ソース・プログラム中で、言い換えであることを明確にするためにアルファベット大文字で記述して言い換えることが多いのですが、小文字で記述してはいけない、といった制約はありません。
#define ARRAY_MAX 1024
#define TRUE 0xff
#define FALSE 0
#define BOOL unsigned char
#define good struct good
としておくと、以降、プリ・プロセッサは、ソース・プログラム内のint array[ARRAY_MAX]をint array[1024]に読み替え、TRUEを0xffに読み替え、FALSEを0に読み替え、BOOLをunsigned charに読み替え、goodをstruct goodに読み替えます。struct good *pg1とするのを、good *pg1と省略記述することができます。うっかり、
#define ARRAY_MAX 1024;
と末尾に';'を付けると、int array[1024;]と読み替えられてエラーになるので注意してください。
#if,#elif,#else,#endif
ソース・プログラム中に、printfを散りばめて、変数の動きをモニタしたい場合、デバグをどこまで詳しくやるか、あるいはデバグ完了後には、printfを除去したい、というような場合に使います。
#define LEVEL 0
#if LEVEL > 2
printf("x1:%d\n", x1);
#endif
#if LEVEL > 1
printf("x2:%d\n", x2);
#endif
#if LEVEL > 0
printf("x3:%d\n", x3);
#endif
となっている場合、コンパイルして実行すると何も表示されません。ですが、最初の#define LEVEL 0を#define LEVEL 1としてコンパイル、実行すると、x3が17であれば、
x3:17
と表示されます。#define LEVEL 2としてコンパイル、実行すると、x2が293であれば、
x2:293
x3:17
と表示されます。#define LEVEL 3としてコンパイル、実行すると、x1が7538であれば、
x1:7538
x2:293
x3:17
と表示されます。場合分けが複雑な場合、#elif,#elseも使えます。MODEがCHILDであるか、YOUNGであるか、OLDであるかによって、出力メッセージの異なる実行ファイルを作成したい場合、
#if MODE = CHILD
message = "がんばろうね";
#elif MODE = YOUNG
message = "がんばれ";
#else
message = "がんばってくださいね";
#endif
として、MODEの設定により、CHILD向け実行ファイル、YOUNG向け実行ファイル、OLD向け実行ファイルを別々に作成することができます。
#ifdef
単純に、デバグであるかないか、というだけなら、#ifdefを使って、
#define DEBUG
#ifdef DEBUG
printf("success!\n");
#endif
としてコンパイル、実行すると、DEBUGがdefineされているので、success!と表示する実行ファイルができますが、デバグを終了して、#define DEBUGを消すか、#define NODEBUGにするか、'/*'~'*/'で囲んでコメントにしてしまうと、success!が表示されない実行ファイルが作られます。
#ifndef
#ifdefとは逆に、defineされていない場合に実行します。
#define PRODUCT
#ifndef PRODUCT
printf("success!\n");
#endif
としてコンパイル、実行すると、PRODUCTがdefineされているので、success!が表示されない実行ファイルができます。
【広告】 ここから広告です。ご覧の皆さまのご支援ご理解を賜りたく、よろしくお願いいたします。
【広告】 広告はここまでです。