K3の住民

最近はレア社のゲームについては書いていませんが、一応レア社のゲームが一番好きな人です。『雪圀』で"圀"は"国"とほぼ同義ですが、国ではありませんって当たり前か(笑)。

【Cプログラミング】Cで数字を整数として扱える文字列比較を作る

2019/02/18 目的の食い違いにより、タイトルの変更

皆さん、こんにちは。雪圀です。

今回は数字を整数として比較出来る文字列比較をCで作ってみたので配布しようと
思います。
何番煎じか分かりませんが・・・


先ず、Cの文字列比較で数字を比較する際の欠点についてですね。

strcmp()関数

Cには文字列を比較する関数があります。それがstrcmp()関数です。この関数は文字列
の大小を比較する関数なのですが、この関数には大きな欠点があります。

それは文字数の多さ関係無く、1文字の大小だけで比較しているという点です。

どういうことでしょうか?
では、どのような構造になっているのか見てみましょう。

f:id:ryoryoau24:20190216154736p:plain
文字列比較の大体の構造

先ず最初に書いておきますが、文字列同士の比較は不可能です。しかし、文字の比較
は可能です。
つまり、strcmp()関数でやっていることは1文字1文字の比較を繰り返しているだけで、
比較した文字が全て同じ(str1=str2)だった場合、0が戻り値となります。
そうで無かった場合、str1>str2であれば正の値、str2>str1であれば負の値が戻り値と
なります。

ここまで書けば分かると思いますが、この中に文字数に関する記述はありません。
では文字数が異なる場合、どうなるのでしょうか?文字列は最後に'\0'(NULL文字)
が入る決まりがある(但し、文字数と格納する文字を指定すれば無しにすることも
可能)ので、文字数以外は全て同じ文字列なのであれば、そのNULL文字と比較する
ことになります。
NULL文字の文字コードは0なので、符号付で無い限りNULL文字で無い方が大きい値と
なります。
異なる文字列の場合、文字数が少ない文字列に文字数が多い文字列よりも大きい
文字コードが見つかった場合は、文字数が少ない文字列が大きい文字列となります。
つまり何が言いたいのかというと、文字数は文字列の大小にはあまり関係が無い
ということです。

非数字の場合はその比較方法が採用されているのでそのままでも良いのですが、
数字の場合はどうなのでしょうか。
数字は'0'~'9'の順に文字コードの値が大きいです。では、"99"と"100"、どちらが
大きい文字列となるでしょうか。

まぁ、答えを言っちゃうと、"99"の方が大きい文字列になるんですけどね。何故か
というと、最初に比較されるのは'1'と'9'であり、'9'の方が文字コードとしては
大きい値となるので"99">"100"となるのです。
これが最近のOSだと、ちゃんと"100">"99"になっています。Windows10で試して
みましょう。

f:id:ryoryoau24:20190216155607p:plain
Windows10による"100"と"99"の比較

ほら、ちゃんと"100"は"99"よりも大きい文字列になってるでしょう?
つまり、非数字同士と数字同士は異なる方法で比較されている、ということになり
ます。

今回、数字をちゃんと整数として扱えるような文字列比較関数を作ってみます。

Cで再現するにあたって

標準ライブラリ関数は使いません。つまり、ヘッダファイルは使いません。
理由は標準ライブラリ関数があれば楽に実現出来てしまう為です。

非数字比較と数字比較の分別

先ず、非数字比較と数字比較は以下のように定義します。

  • 非数字比較
    • 数字で無い文字を比較する
    • 比較する文字のうち一つでも非数字であったならば比較が行われる
      • 但し、数字比較で桁数が異なった場合は無視される
    • strcmp()関数と同じ働きをする
  • 数字比較
    • 数字を比較する
    • 比較する文字同士が数字であるならば比較を開始する
    • 数字の桁数を比較し、桁数が多い方が大きい値となる
    • 桁数が同じ場合はstrcmp()関数と同じ働きをする

この定義をもとにorg_strcmp()関数を作っていきます。

org_strcmp()関数の考え方

org_strcmp()関数で重要な考え方として、範囲条件があります。
Cでは範囲条件は「a < x < b」のように表すことが出来ず、AND演算もしくはOR演算
で表す必要があります。
例えば、「a < x < b」の場合、xはaよりも大きいかつb未満であることが条件なので、
「a < x」と「x < b」のANDで範囲条件を指定することが出来ます。

これを文字列比較に置き換えてみます。
非数字比較を行う場合、文字列は'0'未満であるまたは'9'より大きければ良いわけです
から

int main(void)
{
	char c;					// 文字格納用
	
	/* あらゆる方法で文字を格納する */
	
	if( (c < '0') || (c > '9') ) {		// c < '0'、'9' < c
		/* 処理 */
	}

以上の書き方で書けばいいわけです。
この逆の場合、ド・モルガンの法則より(!(a || b) = !a && !b)、文字列は'0'以上かつ
'9'以下であれば良いので

int main(void)
{
	char c;					// 文字格納用
	
	/* あらゆる方法で文字を格納する */
	
	if( (c >= '0') && (c <= '9') ) {	// '0' ≦ c ≦ '9'
		/* 処理 */
	}

以上のように書くことで範囲条件を指定することが出来ます。

あとは、2つの文字もしくは2つの数字の桁数が異なる場合に比較を行わせるだけ
です。

これらをまとめてorg_strcmp()関数を作ると大体以下のような形になります。

int org_strcmp(/* 文字列1 */, /* 文字列2 */)
{
	/* 変数宣言 */
	
	/* 文字列比較 */
	/* - 非数字((文字 < '0') || (文字 > '9'))比較 */
	/* -- どちらかが非数字である(文字1 || 文字2)場合、行われる */
	/* --- 通常の文字列比較と同じ */
	/* --- 但し、数字比較で桁数が異なった場合、行われない */
	/* - 数字((文字 >= '0') && (文字 <= '9'))比較 */
	/* -- どちらも数字である(文字1 && 文字2)場合、開始される */
	/* --- 桁数が異なる場合、多い方が大きい文字列となる */
	/* --- 桁数が同じ場合、通常の文字列比較と同じ */
	
	return 0;	// 全く同じ文字列の場合、戻り値は0
}

ヘッダファイルを作ろう

この関数は何回も使うことが予想されますので、その度にこの関数を作るのは少々
面倒です。
というわけで、いつでも呼び出せるようにヘッダファイルを作ってみましょう。

その前に、ヘッダファイルとは何か、についてですね。例えば標準出力するときに
printf()関数が使われるわけですが、それを使うときに冒頭にこのような記述を書く
と思います。

#include <stdio.h>

これは「stdio.h」をincludeしており、このファイルに記述されているprintf()関数
を呼び出しているのです。
この関数を呼び出すためにincludeされるファイルをヘッダファイルと呼びます
このヘッダファイルですが、ある程度の知識があれば、自分で作ることも出来ます。
但し、通常の場合、ヘッダファイルに関数をそのまま宣言するのでは無く、ソース
ファイルに宣言をして、ヘッダファイルではプロトタイプ宣言だけをします。
そのまま宣言するのはあまり推奨されない書き方です。

では、ヘッダファイルの基本的な書き方をorg_string.hを作りながら説明します。
先ず、ヘッダファイルの最初と最後には、多重includeを防止するために、少し
特殊(?)な記述します。これをincludeガードと呼びます。
includeガードは以下のように記述します。

#ifndef ORG_STRING_H	// includeガード開始
#define ORG_STRING_H

/* org_string.hの中身 */

#endif			// includeガード終了

includeガード開始直後にマクロ定義がされていると思いますが、これにより
org_string.hはincludeされました、という印をつけることが出来ます。この印を付ける
ことで既にincludeしていた場合に中身を全て飛ばしていきます。

次に、変数や関数はCのものである為、このままだとC++では使えません(何故
使えないかについてはC++の仕様の話になってくるので、省略)。そこで、C++でも
使えるようにする為に、以下のような記述をしてやります。

#ifdef __cplusplus	// C++のコンパイル時に使われるマクロ
extern "C" {		// Cとしてのコード開始
#endif

/* org_string.hの中身 */

#ifdef __cplusplus
}			// Cとしてのコード終了
#endif

__cplusplusは予約済み識別子です。C++のコードをコンパイルするときにこのマクロ
は定義されます。要は、「C++コンパイル時にだけ読むよ」ってことを記述して
います。
extern "C"はC++で存在する修飾子で、「コンパイルするときにCとしてコードを読んで
ね」ってことを記述しています。
Cでは存在しない修飾子なので、Cで書いちゃうとエラーを吐いてしまいます。だから
そういう意味でも、__cplusplusは必要なのです。

以上をまとめると、このようになります。

// org_string.h

#ifndef ORG_STRING_H	// includeガード開始
#define ORG_STRING_H

#ifdef __cplusplus	// C++のみの記述開始
extern "C" {		// Cとしてのコード開始
#endif			// C++のみの記述終了

int org_strcmp(/* 文字列1 */, /* 文字列2 */);

#ifdef __cplusplus	// C++のみの記述開始
}			// Cとしてのコード終了
#endif			// C++のみの記述終了

#endif			// includeガード終了

最後はorg_string.cに関数を宣言してorg_string.hをincludeして、終わりです!

// org_string.c

#include "org_string.h"

int org_strcmp(/* 文字列1 */, /* 文字列2 */)
{
	/* 変数宣言 */
	
	/* 文字列比較 */
	/* - 非数字((文字 < '0') || (文字 > '9'))比較 */
	/* -- どちらかが非数字である(文字1 || 文字2)場合、行われる */
	/* --- 通常の文字列比較と同じ */
	/* --- 但し、数字比較で桁数が異なった場合、行われない */
	/* - 数字((文字 >= '0') && (文字 <= '9'))比較 */
	/* -- どちらも数字である(文字1 && 文字2)場合、開始される */
	/* --- 桁数が異なる場合、多い方が大きい文字列となる */
	/* --- 桁数が同じ場合、通常の文字列比較と同じ */
	
	return 0;	// 全く同じ文字列の場合、戻り値は0
}

あとはヘッダファイルをincludeするときに標準ライブラリ以外のものを使うわけ
なので、<>では無く""で囲むってことに気を付ければいいだけですね。

コンパイル時の注意点

コンパイルするとき、ソース内のincludeで指定する場所に注意してください。今回
作ったヘッダファイルは標準では無いので、gccコマンドであるオプションを使わない
限りパス指定が必要となります。

例えば今のディレクトリ(windowsで言うフォルダのこと)より下の階層にあった
場合、「#include "./dir/org_string.h"」と言った感じで書く必要があります。
階層がそこまで深くなければ、絶対パス*1で指定することが望ましいです。

また、gccコマンドでコンパイルする場合、以下のように書く必要があります。

gcc -o (実行ファイル名) (メインのソース) (org_string.c)

このときもorg_string.cが違うディレクトリにある場合は、そこに指定します。

因みに「-I」オプションを使いincludeするヘッダファイルのパスを指定することで、
ソース内のincludeのパス指定を省略し、ファイル名を書くだけでコンパイルを成功
させるようにすることが出来ます。

いちいちコンパイルするのが面倒くせえって方は、Makefileを作ったらいいん
じゃないですか?(丸投げ)

とりあえず配布

org_strcmp()関数が入ったソースファイルとヘッダファイルを配布しておきます。
煮るなり焼くなり修正するなり好きにしてください。

ダウンロードページはこちら

ソースファイルとかヘッダファイルをアップするならGitHub使えよ、と思われる
かもしれないですが、趣味の範囲で使うのはどうだろう、と思ったのでやめて
おきました。
コメントは標準で入っているヘッダファイルを意識して作ったので敢えて書いて
いませんが、要望があれば、コメントありにして再配布します。


今回はこれくらいにしときます。

*1:「/(ルートディレクトリ)」から始まるパスのこと