グレースケールと白黒画像
はじめに
今回はグレースケールと白黒画像について少しまとめていく
あまり画像処理に興味がない人は、どっちも対して変わらんと思うだろうけど
実は明確な差がある
白黒画像
googleで白黒画像と検索すると以下のような画像が出てくるが
これは正確には白黒画像ではない
コンピュータ画像処理において白黒画像とは白と黒の2値のみで表された画像なので灰色は含まれない
つまり、2値化した画像のことをいう
グレースケール
グレースケール画像というものは白黒の2値だけで表されていた画像に対して、灰色も含めて表す
具体的には白〜灰〜黒の連続した変化(濃度値)を8bitの0〜255の256レベルに量子化したもので
各濃度値に対応した濃淡の度合いを表示したもので、白黒画像に比べて豊かに画像を表現することができる
この表現方法にはいろいろあり
0を白、255を黒とする場合やその逆もある
対して白黒画像、つまり2値化した画像の場合は0を白、1を黒としたりする
OpenCVで画像の読み込みと表示、保存
画像の読み込みと表示、保存
OpenCVを使って画像処理をしようにも、まず対象の画像が読み込めないと何もできない
そして、いろいろやっていった結果の画像を見れるといいなというわけで
さらにいろいろやった結果を出力できたらなおいいというわけで、今回は全てやる
方法
まずcv2.imread("画像ファイル名")で画像を読み込んだものを変数に代入する
imreadの第二引数を0にするとグレースケールで読み込める
画像を表示するにはimshowを使い
cv2.imshow("ウィンドウ名",画像を読み込んだ変数)とする
#coding:utf-8 import numpy import cv2 img = cv2.imread("Lenna.png") img2 = cv2.imread("Lenna.png",0)#グレースケールで読み込み cv2.imshow("color",img) cv2.imshow("gray",img2) cv2.waitKey(0) cv2.destroyAllWindows()
waitKeyではキーボードからの入力待受で引数のミリ秒だけ入力を待ち受けてその後次の処理に移る
destroyAllWindows()では現在表示中のhighGUIの画像表示窓を全て破棄する
これで画像の表示と読み込みができるようになった
次に画像の保存
今回はカラー画像をグレースケールで読み込んでそれを出力する
画像の出力は簡単で
cv2.imwrite("出力画像ファイル名",画像ファイルを読み込んだ変数)
でいいので
#coding:utf-8 import numpy import cv2 img = cv2.imread("Lenna.png",0)#グレースケールで読み込み cv2.imwrite("LennaG.png",img)
これで画像の読み込み、表示、保存ができるようになった
PythonでOpenCVを使えるようにする
方法
まずは例にならって環境を最新にする
sudo apt-get update sudo apt-get upgrade
これで必要なパッケージをインストールするだけ
sudo apt-get install libopencv-dev sudo apt-get install python-opencv
numpyが必要なので入っていない場合はそれもインストール
それでちょっとテスト
#codeing:utf-8 import numpy as np import cv2 img = cv2.imread("ここにファイル名"); cv2.imshow("作成するウィンドウ名(なんでもいい)",img) cv2.waitKey()
ついでにヒストグラムの時に便利なライブラリもまとめていれておく
sudo apt-get install python python-dev sudo apt-get install python-numpy python-scipy python-matplotlib
ここでさらにヒストグラムを出してみる
#coding:utf-8 import cv2 import pylab as plt #グラフ可視化 im = cv2.imread("ファイル名") hist = cv2.calcHist([im],[0],None,[256],[0,256]) plt.plot(hist) plt.xlim([0,256]) plt.show()
C言語 関数とプログラム構造
4章 関数とプログラム構造
C言語で書かれたプログラムは大抵、大きな計算処理を小さな多くの関数に分割して作成される
その関数群は一つのソースファイルであるか、全体を通して言えば複数のソースファイルに分割されている
そしていくつかの大きな関数からプログラムができることはまずない
なぜこのような構造をとるかと言われると、それにはいくつかの理由がある
知る必要でない部分以外の操作に関する情報を隠す為
次にコードを細分化し変更を容易にするため
などといったものである
ここではそういった関数に関することやそれらからなるプログラムの構造について話していく
4.1 関数の基本事項
関数の基本事項を説明するために、有名なfizzbuzz問題を汚いコードで無駄に関数に分割して考える
今回そのコードは以下のようにfizzbuzzをとく
範囲は1~50までで、その範囲の数を全て格納した配列を用意する
用意した配列を先頭から値を取り出し
fizzbuzzを倍数によって考えそれぞれの結果でフラグとして値を返す
そのフラグをswitchで条件分岐にかけ結果を出力する
#include<stdio.h> #define MAX 50 #define START 1 #define END 50 #define FIZZBUZZ 1 #define FIZZ 2 #define BUZZ 3 void makeArray(int array[]); int calcFizzBuzz(int num); int main(){ int array[MAX]; int i; makeArray(array); i = 0; while(i < END){ switch(calcFizzBuzz(array[i++])){ case FIZZBUZZ: printf("FizzBuzz\n"); break; case FIZZ: printf("Fizz\n"); break; case BUZZ: printf("Buzz\n"); break; default: printf("%d\n",i); } } return 0; } void makeArray(int array[]){ int i; for(i = START; i <= END; i++){ array[i-1] = i; } } int calcFizzBuzz(int num){ if(num%15 == 0){ return FIZZBUZZ; }else if(num%5 == 0){ return BUZZ; }else if(num%3 == 0){ return FIZZ; } return -1; }
今回は無駄にフラグ関係にdefineを使ったりしてみた
そして、例としてあげたプログラムからわかるように関数は
戻り値の型 関数名(引数){ 処理 }
という形をとり、戻り値の型がvoidか存在しない時returnで値を返す必要があり
return 式;
という形をとり、これ以降に処理や計算を記述した場合、その処理は呼ばれることがないため
無限ループや処理の途中で抜けるようなbreakに近い動作をする
また戻り値が必要ない場合、関数内の処理が全て終わると呼出元にその制御を渡す
ある関数で値をあるところでは返し、またあるところでは返さないという様な構造を取る関数はまず作成するべきではなく
そのような関数はさまざまなエラーやバグの原因となるのでゴミになってしまう
4.2 非整数を返す関数
いままでは何も値を返さない、もしくは整数を返す関数を扱ってきた
それでは整数ではない値を返す関数はどうすべきだろうか
今回はひとつの例としてdoubleを返す関数を扱う
そして今回も4.1に続き説明の都合上、無意味に整数で高さと幅を受け取りその面積をdouble型で返す
dirty_s関数というゴミ同然の関数を作り説明する
#include<stdio.h> double dirty_s(int w,int h); int main(){ int w,h; int i; double sum; double dirty_s(int,int); w = h = 1; sum = 0; for(i = 0; i < 10; i++){ sum += dirty_s(w,h); } printf("%f\n",sum); return 0; } double dirty_s(int w,int h){ return (double)w*h; }
今回は今までにない書き方をしている部分がある
それは10行目の
double dirty_s(int,int);
である
これには非整数を返す場合特に必要はないがつけるのが望ましい
というのも、まだこれは短いプログラムでありソースファイルもひとつしかないが
大きなプログラムになると、外部から独自の関数を呼び出した時それが何型を返すのかわからないとすごく面倒なので
型 関数名(型,....);
のように宣言できる
配列の時は
型 関数名(型 [],....);
などとなる
これはコードの可読性を上げるという面でも覚えておくといい
4.3 外部変数
C言語で書かれたプログラムというのは主に変数と関数、それといくつかの外部オブジェクトによって形成されている
ここで紹介する外部変数というものはすべて関数の外で宣言され多数の関数で使用できる
またC言語では関数内に関数を定義することができないので関数はすべて外部的と言える
さらに、外部変数や関数は同じ名前で参照すれば別個コンパイルされた関数でもすべて同じものの参照となる外部リンクという性質をもっている
外部変数は広域的にアクセスすることができるから、引数や戻り値の代用として扱えたりもする
そして外部変数はその名前でアクセスする限り(基本的に)どの関数からでも参照できる
そうした性質を持つため、長い引数リストを使う関数なんかに外部変数を使用するのがよかったりする
便利そうに見えるこの外部変数というもの、しかしこれらの使用には注意しないといけなくて
プログラム構造が悪くなったり、関数間で多くのデータが結合しているプログラム(変更しにくい)になってしまう
ただ一般的に関数の内部で定義する変数はルーチンに入った時に生成されルーチンが終わると消滅するのに対して
外部変数は永久的であり、ひとつの関数の呼び出しで得た値を次の関数呼び出しても保持しているため
2つの関数間でデータを共有する必要があり、互いにもう一方の関数を呼び出すことをしないなら外部変数を使うべきと言える
これらを踏まえてすごく効率の悪いソートアルゴリズムを組んでみる
今回は少しわかりやすいようにスタックの構造をとってソートする
入力は以下のように仮定する(実際には改行で区切って入力される)
6 1 9 8 2 7 4 5 3 10
#include<stdio.h> #include<string.h> #define MAX 10 #define NEXIST -1//すでに探索したもの void init();//初期化処理 void push(int); int pop(void); int getMAX(int []); int res[MAX];//結果を格納する int base[MAX];//入力から与えられた数列を格納する int stack_top = 0; int main(void){ int i; int num,max; init(); for(i = 0; i < MAX; i++){ scanf("%d",&num); base[i] = num; } while(stack_top < MAX){ max = getMAX(base); push(max); } //結果出力 printf("---sort result---\n"); for(i = 0; i < MAX; i++){ printf("%d\n",pop()); } return 0; } void push(int number){ res[stack_top++] = number; } int pop(){ int num = res[--stack_top]; return num; } int getMAX(int array[]){ int max = 0; int position; int i; for(i = 0; i < MAX; i++){ if(max < array[i]){ max = array[i]; position = i; } } array[position] = NEXIST; return max; } void init(void){ memset(res,0,MAX); memset(base,0,MAX); }
今回外部変数との関係で注目すべきは15行目の
int stack_top = 0;
という外部変数と、それを使用している
void push(int);
と
int pop(void);
という関数
これらはstack_topという外部変数を使いつつ、互いに呼び出すことはないので
先ほど紹介した外部変数を使うに適した構造といえる
また外部変数は2つの関数で変化させているが49~52行のpop関数で正しく表示できるように
他の関数での変更が外部変数に適応されているため、データを共有できている
4.4 通用範囲に関する規則
C言語で書かれたプログラムは関数と外部変数が主体であり、すべてを同時にコンパイルする必要はない
プログラムのソースファイルを数個に分けて保存し、以前コンパイルしたライブラリからロードしてもいい
こうしたことを踏まえてここで話すのは通用範囲、つまりスコープに関することである
変数や関数の通用範囲はその名前を使うことのできるプログラムの部分である
つまり自動変数は他の関数で参照できないというのはこれに関係している
特に自動変数に商店を当てるなら、上の理由から別の関数に同じ名前の局所変数があったとしても関係はない
イメージでいうなら、関数はひとつの処理、データの集まりで関数の内部は関数として独立している
それに比べて関数、外部変数における通用範囲はコンパイルされるべきファイルで宣言された点からファイルの終わりまで続く
つまり同じファイルであれば単に名前を参照するだけで呼び出すことができる
しかし、外部変数が定義される前に参照されたり異なるソースファイルで定義されているとextern宣言が必要になる
4.5 ヘッダーファイル
実際にプログラムを組んでいくと、どのような構成にするかは人それぞれであるが
関数ごとにソースファイルに分割したりして可読性を確保する場合がある
そうしたことをしていくうちに、構成で迷うのが複数のソースファイルで共通する処理をどうするかである
処理と書いたが実際には宣言と定義である
これだけはなるべく集中化しておくのがいいだろう
なぜなら共有部分をまとめて宣言定義しておくことで、プログラムを書き換える場合に比較的少ない箇所を訂正するだけで済むから
そこで共通のデータや処理をヘッダーファイル(.h)にまとめておく
4.4のことも含めていうならヘッダーファイルに宣言、定義類を固めておいて
なおかつ、extern宣言しておくことでinclude先で使いやすくなる
4.6 静的変数
ここまで、外部変数や関数の通用範囲やそれらを超える他のファイルなどの外部変数、関数の通用範囲に応じた定義宣言の話をしてきたが
特に外部変数はそれより外の通用範囲に向けて使う使い方と、そのソースファイル内でしか使用しないものがある
宣言したソースファイルないでのみ使用する外部変数はその場所に使用を限定する場合staticをつけることで、オブジェクトの通用範囲をコンパイルされつつ
そのソースファイルの残りの部分にのみ適応できる
これは主に(というかほとんど)変数に使用されるが、関数に適応することも可能で関数に使用した場合その関数は外部から呼び出せなくなる
更にはstaticは内部変数に適応できて、これもその関数内に限定されるが他のものとは違い
関数が呼び出されるたびに生成されるのではなく、一度生成されると存在し続ける
つまり単一の関数においてその内輪で永久的なメモリが与えられる
4.7 レジスタ変数
register宣言をすることはコンパイラにその変数が頻繁に使われることを知らせるのによく使われる
その目的はregister変数をマシンのレジスタに置くことでプログラムを小さく速くできるからで、この指示はコンパイラが無視する場合がある
これらの宣言は自動変数と関数の仮引数に使える
しかしこれは実際のところハードウェアに左右され、また実際にレジスタに置かれるかどうかにかかわらずレジスタ変数のアドレスは求めることができない
4.8 ブロック構造
C言語では関数の中に関数を作るような構造にはできないが
変数においてはブロック構造のような構造をとることができる
具体的には以下のように宣言、定義できる
if(0 < number){ int i; for(i = 0; i < number; i++){ } }
この場合ではnumberが正であるときの分岐でブロック外にiがあったとしてもそれとは無関係になる
またブロック内で定義された自動変数はブロックに入るたびに初期化されるが
static変数であった場合はブロックに最初に入った時に初期化される
仮引数も含めて自動変数は同じ名前の外部変数や関数を隠す働きがあるため
なので以下の場合
int x,y; void function(double x){ double y; }
関数内でxを参照するとそれは仮引数のdouble型xであり
yについても関数内であれば同様のことがいえる
またスタイルの問題として、外側の通用範囲にある変数名を隠すような書き方は避けるべきで
そうでないと混乱やエラーの原因となってしまう
4.9 初期化
初期化については、これまでのコードの中で度々あったがここではもう少し初期化の規則についてまとめる
まず明示的な宣言がない場合は外部変数と静的変数は0に初期化されることが保証される
しかし自動変数とレジスタ変数は値が不定となるためメモリを無駄遣いしていると言える
スカラー変数においては名前のあとに等号と式を置くことで初期化できる
int x = 1;
外部変数と静的変数においては初期化は定数式でなければいけない
初期化は概念的にプログラムの実行時に一度だけ行われる
自動変数とレジスタ変数は初期化が関数やブロックに入るたびに行われる
また自動変数とレジスタ変数においては初期化が定数式とは限らない
前もって決まっていて正しいもの(関数の引数など)であればそれを含んだ任意の式でも構わない
事実として特に自動変数の初期化式は代入文の省略形でありどちらを好むかは趣味の問題である
一方配列では初期化式のリストをかっこで囲み、カンマで区切ったものをつけることによって初期化する
int p_num[] = {2,3,5,7,11,13};
配列のサイズが省略された場合カッコの中の要素数がその配列のサイズとなる
また指定したサイズより配列にたいする初期化式が少ないなら残り要素は0になる
ただしサイズを超えたものはエラーとして処理される
文字列の初期化についてはダブルクオーテーションで囲まれたものになるが
これは実は特殊で以下のようになっている
char hello[] = "hello"; char hello[] = {'h','e','l','l','o'};
4.10 再帰
C言語の関数は再帰が使える
再帰とは関数の処理がその関数自身を呼び出すことである
簡単な例としては、ユークリッドの互除法を再帰を使って実装できる
#include<stdio.h> int gcd(int,int); int main(){ int a = 5; int b = 25; printf("%d\n",gcd(a,b)); return 0; } int gcd(int a,int b){ if(b == 0){ return a; }else{ return gcd(b,a%b); } }
このようにreturnにつけて行うこともできるし、それがなくても再帰は行える
ただし再帰を使う場合、処理中の値のスタックを保持しないといけないためメモリの節約にならないこともある
さらに処理速度の向上もあまり見込めない
しかし再帰を使わないプログラムに比べてずっと書きやすく理解しやすくなることが多い
再帰は特にツリー構造のようなデータ構造に対して有効である
4.11 Cのプリプロセッサ
Cではプリプロセッサによりある程度の言語仕様が与えられる
これがよく使われるのはコンパイル中にファイルをロードするincludeとトークンを任意の文字列で置換するdefineだろう
4.11.1 ファイルの取り込み
ファイルの取り込みは#defineや宣言の集まりを取り扱うのを簡単にする
#include"ファイル名"
あるいは
#include<ファイル名>
はそのファイルの内容に置き換えられる
ファイル名にダブルクオーテーションがついていた場合呼び出しもとのファイルがある場所からそのファイルを探索する<>で囲まれいる場合は標準ライブラリか、後にインストールされたライブラリを参照する
またincludeされるファイルにincludeが存在してもいい
ソースファイルの先頭は共通のdefine文やextern宣言を取り込むため
またはstdio.hのようなヘッダに出ているライブラリ関数のプロトタイプ宣言にアクセスするためでもある
#include宣言は大きなプログラムで宣言を結びつけるのによく使われ、悪い書き方をしているものを防ぐ役目もある
ただ取り込むべきファイルの内容を変更した時はそれを使うすべてのファイルをコンパイルし直す必要がある
4.11.2 マクロの置換
ここで話すことはつまりはdefineである
#define name 置換テキスト
の形で宣言されるマクロ置換はnameを置換テキストで置き換えることを要求する
#define中の名前は変数名と同じ形を持ち、置換テキストは任意である。
通常、置き換えられるテキストは行の後に続き長い場合行末に/をつけることで次の行に続けることが可能
defineを用いて定義された名前の通用範囲はそのソースファイルの最後までである
また宣言中に以前の定義名を使用することもできるが、ダブルクォーテーションなど引用符に囲まれた中や定義名が含まれるものには置換が発生しない
少し前に記述したように置換テキストは任意であるため以下のような書き方ができる
#define forever for(;;)
これでforeverという名前の新しい無限ループが宣言される
また引数をつけた宣言も可能で
#define max(A,B) ((A) > (B) ? (A) : (B))
のように定義すると
int a,b,c; a = 2; b = 1; c = max(a,b);//c == 2
となる
ここでの仮引数は実引数と置換される
引数を首尾一貫して使う場合にはどんなデータ型でもよく
データ型が違ったとしても関数とは違い新たなmaxを宣言する必要はない
そして少し注意が必要な点がある
マクロ置換は置換テキストが展開されるためインクリメント演算子などを含む式が引数として渡されるの良くないことになる
defineだけでなく名前を定義するのであれば#undefも使用できる
undefはこれはそのルーチンがマクロではなく関数であることを保証するために使用する
#undef getchar int getchar(void){ }
引用符月の文字列の中では仮引数は置換されない
しかし置換テキストの中でパラメータ名の前に#がついているならその組み合わせはパラメータで実引数で置き換える形で引用符付き文字列のなかで展開される
これはデバッグ用のプリントマクロを次のように文字列の連結と組み合わせるのにつかえる
#define dprintf(expr) printf(#expr " = %g\n",expr)
一方プリプロセッサ用の演算子##はマクロ展開の最中に実引数を連結する一つの方法であり
以下のように使える
#define paste(front,back) front ## back
こうすることでpaste(hello,world)ならば
helloworldという文字記号を作れる
4.11.3 条件付き取り込み
defineでも条件分岐ができて
例えばhello.hというヘッダーを確実に一度だけ取り込むなら以下のように書ける
#if !defined(HELLO) #define HELLO /*hello.hの内容*/ #endif
本来はまずif行で取り込まれてそこから#endifまたは#elif、#elseまで取り込まれる
またif文中のdefinedはかっこないの名前が定義されているなら1それでなければ0になるというもの
またifが関係している文はこれ以外にもあり
名前が定義されているかというテストをする特殊形式の#ifndefと#ifdefがあり
これらを使うことでC言語にないtrue or falseを擬似的に実装できる
#ifndef Boolean #define Boolean int #endif #ifndef TRUE #define TRUE 1 #endif #ifndef FALSE #define FALSE 0 #endif
またさっきのdefinedのコードを書き直すこともできて
#ifndef HELLO #define HELLO //ここにhello.hの内容 #endif
C言語 制御の流れ
3章 制御の流れ
ここでは今まで普通に使ってきた制御に関することをもっと掘り下げていく
3.1 ブロックと文
これは凄く簡単なことで、C言語では式に;(セミコロン)を置くことで文になる
なので宣言や代入などそれら単体で文として独立する
また中カッコ{}は、それらで囲まれる部分を一つのブロックとして扱い、単一の文と同等の意味を持たせる
関数で関数がもつ処理を{}で囲んだりif~elseやfor,whileなどでブロックを使うことがその典型例である
3.2 if~else
判定を行うときはif~else文を用いる
else文は省略可能であり、正式にはこう書く
if (条件){
処理1
}else{
処理2
}
{}を省略することもできるが、それは時に曖昧さをもつ
以下の例をあげると
if(0 <= number) if(b < a) max = a; else max = b;
これは人間には解釈できるがコンパイラにとっては内側のifと同列にelseがあるように解釈される
特に以下の様な時に有害である
int array[10]; int i; memset(array,-1,sizeof(array)); array[0] = 1; if(0 <= array[0]) for(i = 1; i < sizeof(array); i++){ if(0 <= array[i]){ printf("...\n"); } } else printf("error");
これらの予期せぬ動作を防ぐ為には{}を用いてブロックを明示的に区別するのが得策といえる
3.3 else if
3.1ではif elseの様に
条件一つに対して、真か偽かとその判定に対する処理の多くて二つしか実装できなかったが
else ifを使うことで更に多くの条件判定を行える
if(条件1){
}else if(条件2){
}else{
}
上記に示す様にifでは条件1,else ifでは条件2、elseではそれ以外のときとなる
これは多分岐の条件判定を実装する一般的な方法で、最後のelseは省略できる
またこれら同列に並ぶif,else if,elseは上から順番に判定されどれかに当てはまった段階でその後の判定を全てスキップする
補足ではあるがif文に於て、elseはそれまで続く全ての条件に当てはまらない場合に実行されるのでエラー検出に用いることもできる
3.4 switch
これはif,else if,elseを用いた多分岐条件判断でよく使われる
以下の様な構文で用いられる
switch(式){
case 定数式:
文
default:
文
}
if文と同様にdefaultは省略可能で、caseは幾つでも定義できる
だが少し注意が必要である
#include<stdio.h> int main(){ int i; for(i = 1; i <= 10; i++){ switch(i){ case 1: case 2: case 3: case 4: case 5: printf("i <= 5\n"); break; default: printf("over 5\n"); } } return 0; }
上記のプログラムではcaseの後の文でbreakが宣言されていないため
1~5までの条件判断で処理を一つにまとめている
つまりbreak文が無いことで、次のcase文に移っていく
これはifで書くと
if(i <= 5)
もしくは
if(i == 1 || i == 2 || i == 3 || i == 4 || i == 5)
と解釈できる(switchと同じ意味合いであるなら後者)
3.5 ループ(forとwhile)
これまで説明こそしなかったものの、ループは問題なく使ってきた
まずはwhile文から
whileは以下の様な構造をとる
while(式){
文
}
while文ではまず式が評価され式が0とならないなら文に移り、式を評価して0になるまで繰り返される
次にfor文
forは以下のような構造をとる
for(式1;式2;式3){
文1
}
そして以下の様に書けば、whileとforは同じ意味合いをもつ
for(i = 0; i < 100; i++){}
これをwhileで書くと
i = 0; while(i < 100){ i++; }
これで同じ意味合いになる
またforにおいて、三つの要素は文法的に式であり、一般的には式1と式3が代入又は関数呼出で式2が関係式となる
この3つの要素はどれを省略してもいいが、セミコロンを省略することはできない。
式1、式3は省略された場合単に展開されないだけだが、式2の関係式が省略されると、それは永久に真であることを示す
つまり、以下の様にfor文を書くと無限ループになる
for(;;){
}
無限ループはwhileにもありwhileでは以下の様に書く
while(1){ }
また無限ループから脱出する際にはbreakかreturnを使うことになるだろう
ここのループの話ではreverse関数という文字列sを逆順にする関数を考えながらコンマ演算子について触れておく
コンマ演算子といえば、変数宣言や初期化やその他諸々の様々な場合で使用してきたものとは違い
for文に適応できて以下の様に書ける
void reverse(char s[]){ int c,i,j; for(i = 0, j = strlen(s)-1; i < j; i++,j--){ c = s[i]; s[i] = s[j]; s[j] = c; } }
ただこのコンマ演算子はあまり乱用するべきではない
使用してもいいのは、reverse関数のようにi,jが強く関連しているときなどである
3.6 ループ(do~while)
次に三番目のループ構文としてdo~while文を紹介する
これは基本的にはwhile文の様に式を評価してループするしくみなのだが、式に真か偽か関係なくdoのブロック内に含まれる処理を必ず一回は実行する
以下のような構文である
do{
文
}while(式);
もし、一回目の実行の後に式を評価し真であるならまた文を実行する
経験上for,while文ほど多用はしないが、たまに使いどころがくるので知っておくべき
3.7 breakとcontinue
breakとcontinueは主にループ内で多用し、breakにおいてはswitch文でも使用する
これらはループから早く抜ける為にあり、breakを使えばそれが含まれる最も内側のループから抜けることができるし
continueを使えば適切なタイミングでループをスキップし、次のループに移れる
3.8 gotoとラベル
C言語にはgotoという、ネストになったループから一発で抜けてしまうようなものがある
何度もifとbreakを書かなくていいという簡潔なメリットがあるが形式的にはgotoは不可欠なものでない
実際にそれらを使用しなくてもプログラムは書けるし、むしろgotoを使用するような構造のプログラムを書いてしまうことの方がまずいことである
gotoでは指定したラベルまで一気に飛べるため、以下のように使う
for(i = 0; i < n; i++){ for(j = 0; j < m; j++){ if(a[i] == b[j]){ goto found; } } } found://ラベル
また一度に多くの処理を飛ばしてしまうことがありgotoを使ったプログラムはgotoを使わないプログラムに比べて保守しにくく
バグの原因となるので、あまり使うべきではない
C言語 データ型、式、演算子
2章 データ型、式、演算子
前回の1章でざっくりとした入門を終えたので、今度はひとつひとつ掘り下げていきます。
はじめは、大抵どの入門書でもはじめに扱うデータ型、式、演算子というものです。
まず宣言と演算子を交えて…
宣言は変数を並べて、型を定義し、時には初期値を決める為に
演算子は変数に対して何を行うか、さらには式で変数や定数を結合し新しい値をもたせるのに使われる。
一方でそれらのオブジェクトが持つことが出来る値の集合と、どのような演算ができるかという事を示すのが型である
今回使用している、プログラミング言語CのANSI規格では基本的な型と式に変更と追加が加えられ
型の中でも、特にすべての整数型にはsigned、unsignedという型があり符号なし定数や16進文字定数の記法ができた
浮動小数点型では、単精度での演算も可能で拡張精度のためのlong double型も存在する。
文字配列、つまり文字列の中でも特に定数はコンパイル時に連結が可能となった
オブジェクトではconstを付加することで変更不可に出来たり、算術間の自動変換規則ではより豊富なデータ型を扱える様に強化されている。
2.1 変数名
これは誰もが知っていることかもしれないが一応解説。
まず変数名と記号定数名にはいくつかの規則がある。
それは名前の1文字目は英字でないといけないということで、下線は英字に含まれる
そして、伝統的に変数名は小文字で
定数名、特に記号定数は大文字で記述することになっている(そうしなくても良い)
それ以外に、内部変数名の長さを31文字以内にすることや、言語で定義されているキーワードを使用しない
といったものがある
なので、変数名はその変数自体が持つ意味合いやそれに関係する名前にするのがいいと言われている
特に局所変数、内部変数には短い名前を、外部変数にはそれなりに長い名前をつけるのがいいとされている
2.2 データ型とサイズ
C言語には次のような数の基本的なデータ型があるだけ
データ型 | 意味合い |
---|---|
char | 1バイト、ローカルな文字セット内に1文字保持する |
int | 整数、通常ホスト計算機の自然な整数サイズ |
float | 単精度浮動小数点数 |
double | 倍精度浮動小数点数 |
これらの基本型には修飾子をつけることが可能で整数にはshortとlongというものがあり次の様に適応できる
short int c1; long int c2;
こうした宣言ではintを省略できる。というか実際には省略されることがおおい
この意図は可能な限りshortとlongによって異なる長さの整数を扱える様にするべきというところにある
intは通常、特定の計算機にとって自然な大きさが取られていてshortは16bit、longは32bitであることが一般的である
具体的にいうとintは16bitか32bitのどちらかである。その点がハードによって違う
それらを考慮してshortはintより大きくてはいけない
またintはlongより長くてはいけないということがある
整数にはlong,shortがあるがcharにはsigned,unsignedがある
charが8bitの長さだとすると
signedは符号ありを示すもので-128から127の間の範囲である
unsignedは符号なし、つまり値が必ず0か正の数で0~255までの範囲である。これは印字可能な文字と関係があり
具体的には印字可能な文字を数値で表すと必ず正の整数なのでその点に関係する
一方、浮動小数点数にはlong doubleという拡張精度の浮動小数点数を表すものがある。
整数と同様にfloat,double,long doubleの長さは同じであることもあれば異なることもある
2.3 定数
1234のような整数定数はintとして扱われる。123456789Lのように末尾にLまたは小文字のlをつけるとそれはlongになる
それに加え大きすぎてintでは格納出来ない数字もlongとして扱われるがそのような使い方はおすすめしない
一方、符号なしのunsignedの場合定数には末尾にU,uをつけるため
unsigned long型だった場合、UL,ulを末尾につけるなどある
浮動小数点数は小数点(123.4)や指数部(1e-2)あるいはその両方を含むものであり
それらに接尾子が無い限りdoubleとして扱われf,Fを末尾につけるとfloatになる
さらにl,Lをつけた場合はlong doubleとして扱われる
整数では基本的に10進数を扱うことが多いが、8進数や16進数を標準で扱える
8進数の場合、先頭の数字が0
16進数の場合は先頭が0xもしくは0Xがつく
例として10進数で31を表現するなら
8進数で037
16進数で0x1fか0X1fと表現する
この時unsignedやlongを型にしているならulやULなどの接尾子を付加できる
次に文字定数について、文字定数は宣言するときにシングルクォーテーションで対象を囲む
例えば、Aという文字を文字定数として扱いたいなら'A'とする。
そして文字定数、文字とは数値であるため'A'を格納する定数は実質的に65という数値を格納している。
なのでC言語には数値型しかないと言われる。
またこの性質を使うと上の例で65を定義するときに'A'と書くとそれだけ特定の値と独立しコードが見やすくなる
ただむやみに、その性質を乱用するべきではない
そして然るべき時に使うことで、文字とは数値と同様の意味合いを持つ
この性質が一番よく使われるときは文字と文字の比較の時で、文字または文字列においてある種の文字は\nの様にエスケープ系列で表現される
これらは2文字のように見えるがそれが表すのは一文字である
文字を定数として定義するときはシングルクォートで囲んだが文字列を定数として定義するときはダブルクォーテーションで囲む
'A'//文字 "A"//文字列
ダブルクォーテーションで囲まれたものは文字列として扱われるため、例え1文字であっても文字列として扱われる
またこれら定数は数値と同様にdefineを用いて定数化することも出来る
定数式はコンパイル時に評価されるため以下の様に使用できる
#define MAX 100; char str[MAX+1];
ここまでは簡単、でもこれまで紹介したそれらより少しめんどくさい文字列
文字列は技術的に言うと、各要素が1文字の配列である
また文字列の最後にはヌル文字('\0')という文字列の終わりを示す文字が内部表現として追加されるので物理的な容量としてダブルクォーテーションで囲まれたものより1多い
これは文字列の長さに制限がないことを示すが、プログラム上でその長さを求めるためには最後までスキャンする必要がある
その例として、文字列の長さを返すstrlen関数を例に上げる
strlen関数はこのようになっている
int strlen(char s[]){ int i; while(s[i] != '\0'){ //null文字でないなら ++i; } return i; }
そもそもstrlen関数とはstring.hという標準ヘッダで制限されている関数である
そしてこの関数は内部表現として追加されたnull文字を除く文字列の長さを返している
この他に列挙定数というものがあり、enumを用いて以下のように宣言する
enum boolean{ YES,NO }
この場合、YES,NOの値を明示的に宣言していないので
YES = 0, NO = 1という値が暗黙的に宣言されている
それを利用して以下の様に書くことも出来る
enum months{ JAN = 1,FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC }
この場合先頭の定数が1という値を保持しているのでFEB = 2,MAR = 3というようになる
また列挙では名前が異なっている必要があるが、値は異なる必要は無い
列挙は名前と定数値を結びつけるのに便利で、defineの代用として使用もできるし
上のコードの様に、値が自動生成されるという点でも利点がある。
2.4 宣言
すべての変数は宣言を必要とする。
宣言は同じ型をまとめて1行で宣言したり、すべてバラバラに宣言したりできる
int i,max,min; char str[MAX]; int i; int max; int min; chsr str[MAX]; //上下で同じ意味
前者はコードを簡潔に出来る
後者はコメントなどが挿入しやすく、変更も容易である
また宣言と初期化を同時に行うことも可能
ただ初期は通例として一回行われるとき、式に定数を含むのが望ましい
さらに変数にconstという修飾子をつけることで変更不可な変数を生成することが出来る
const int i = 10; i = 11;//これはできない
変更不可というよりは初期化後に再代入できないため、関数の引数にconstをつけることもでき配列にも適応出来る
2.5 算術演算子
- ,-,*,/の二項演算子
%のモジュロ演算子がある
これらには実行優先度というものがあり+,-は算術演算子の中では同じ優先度であるがその他の算術演算子より優先度が低い
ざっくり言えば数学の四則演算と同じ優先度になっている
少し注意が必要なのが/,%の2つ
まずモジュロ演算子の%は浮動小数点に適応できない
さらに/は小数が切り捨てされるがそれらの方向は機種に依存する、負の演算についても同様
加えてオーバーフローやアンダーフローも機種に依存する
2.6 関係演算子と論理演算子
まず関係演算子として
すべて優先順位が等しい<,<= ,> ,>=があるこれは右辺左辺の大小を評価するものである
一つ下の優先順位には等値演算子と呼ばれる != ,==がある
さらに関係演算子は算術演算子よりも優先度が低い。これには注意が必要である
より興味深いのは論理演算子である&&,||である
これら論理演算子で結び付いている式は、左から右へ評価が行われその評価は対象の式が真か偽か判定出来た時点で終了する。
なので1章で示したgetline関数を例に示すと
for(i = 0; i < lim-1 && (c = getchar()) != '\n' && c != EOF; i++){ }
この処理ではまず i < lim -1が判定されなければならない。なぜならこの評価が偽であるなら先に進んで文字を入力しては行けないからだ
同様に!= EOFをgetcharの前に呼び出してはいけない
一方、&&の優先順位は||より高く、これらは関係演算子や等値演算子より低い
最後に単項の否定演算子!は0ではない真の被演算数の値を0にし
0つまり偽の被演算数の値を1にする
if(!valid)
というのは、つまりは
if(valid == 0)
とおなじであるということだ
2.7 型変換
異なる型の被演算数を一緒に計算すると、少数の規則によって共通の型に変換される
一般的には浮動小数点と整数を一緒に計算するときに、整数を浮動小数点に変換したり
それ以外で情報が欠損しない範囲でより広い型に変換されたりすることがある
ただし、情報の欠損があるとしても大きい型の値を狭い型へ変換や代入を行うことは警告が出るかもしれないが文法違反ではない
まずcharから
charは単に小さい整数だから算術式の中で自由に扱える
これは文字変換に対し、ある種の柔軟性を生む
例として文字を整数に変換するatoi関数の単純なものを紹介する
int atoi(char s[]){ int i,n; n=0; for(i = 0; s[i] >= '0' && s[i] <= '9';++i){ n = 10*n+(s[i] - '0'); } return n; }
この例でs[i] - '0'が計算可能なのは文字が数値、厳密には整数だからである
charからintへの変換例として、ASCII文字セットに対してのみ1文字を小文字に変換する関数lowerがある
int lower(int c){ if(c >= 'A' && c <= 'Z'){ return c+'a'-'A'; }else{ return c; } }
小文字は数値として一定の距離内にあり、かつ各文字は連続していて、A〜Zの間には文字しかなく
すべて数値として扱えるためASCII文字セットにのみ適応できる
ところでctype.hという標準ヘッダをしっているだろうか
これらに含まれる関数は文字セットとは独立した形で、検査変換などを行う関数の一群で
例えばtolower(c)はcが大文字であればcの小文字の値を返すからtolowerはlower関数の移植可能な代替版となる
またisdigit(c)という関数は、 c >= '0' && c <= '9'という検査の代替として使えるため今後はそれをつかっていく
次に文字から整数の変換で起こる微妙な点についてまとめてみる
それは文字を整数に変換した時にその数がsigned,unsignedであるか、つまり符号があるかないかという点である
これは計算機のアーキテクチャによって違いがでるもののC言語の定義では、すべて正の数であることが保証されている
しかし文字変数のビットパターンが正か負かという判断は計算機、ハードウェアに左右されるのでsigned,unsignedを明記するのがいいとされている
ここまで少し書いてきた型の暗黙的変換は大抵プログラマーの予想どおりの変換になる
式の中に、異なる型が混ざっているなら低い型は高い型に変換され、結果として式が返す結果は高い型となる
この変換にはまだ規則があるがそれは後述、unsignedの被演算数がなければ大まかな規則だけ知っていれば十分
基本的に暗黙的な型変換は
被演算数がlong doubleを含む場合は他方をlong doubleに変換し
それではなく被演算数がdoubleを含むなら他方をdoubleに
それではなく被演算数がfloatを含むならなら他方をfloatに
そうでないならcharとshortをintに変換する
そして、被演算数がlongを含むなら他方をlongに変換する
ここで注意すべきことはfloatが自動的にdoubleに変換されることはないこと
しかし、一般的にmath.hにあるような数学関数で倍精度が使われる
floatを使う理由は大きな配列の容量を減らすことであったり、計算時間を短くすることにある
ところでunsignedな被演算数が入ってくると変換の規則がもっと複雑になる。
問題はsigned,unsignedの比較が様々な整数の型に影響されるため機種に依存することである。
例として、
int->16bit
long->32bit
とすると
- 1L<1U
となる
何故ならintである1Uがsigned longに格上げされるためである
しかし
1UL<-1U
も成立していまう
これは、-1Lがunsigned long に格上げされるためである
変換は代入時にも行われ、右辺の値が左辺の型に変換され、それが結果の型となる
ここまでで述べてきた様に、文字は符号拡張されたり、されなかったりして整数に変換される
より長い整数は余分な上位ビットが捨てられて、より短い整数に変換される
つまり以下の例で
int i; char c; i = c; c = i;
cの値は不変になる
これは符号拡張の有無に関係なく言えることである。
しかし、代入の順序を逆にすると情報の欠損が起こる場合がある
ちなみにfloatからintへの変換では、小数部が切り捨てになり、doubleからfloatではまるめられるか切り捨てかは機種に依存する
最後にキャストについて
キャストとは明示的な型変換(強制的でもある)で、任意の式に適応できる
例えば、math.hに含まれるsqrt関数では引数にdoubleを仮定しているからnを整数とするなら
sqrt((double)n);
とすればdoubleに変換され引数として渡される
この時、元の変数や値が変わることはない
また実際はある程度の大きさまでは、暗黙的型変換が適応される。
2.8 インクリメントとデクリメントの演算子
C言語にはインクリメントとデクリメント演算子として、ここまではっきりとは紹介してこなかった演算子がある
まず、被演算数に1加える++のインクリメント演算子
そして、被演算数に-1するデクリメント演算子
実際には、既に使用していて
if(c == '\n'){ ++nl; }
の様な形で使用していた
見慣れない点はこれらの演算子が変数の前にある、前置演算子(++n,--n)の場合と変数の後ろにある後置演算子(n++,n--)の場合がある点で
例で言えば、どちらともnの値を±1できるが前置演算子ではnを先に±1し、後置演算子ではnを後で±1する
なのでこうなる
int x; int y; int n = 5; int m = 5; x = n++; y = ++m;
上の例で、xにはnの5が代入されてからnは6になる
またyには先にmが+1されて6が代入されmも6となる
2.9 ビットごとの論理演算子
C言語には6つのビット処理演算子を持っていて、これはunsigned,signedに関らず整数にのみ適応できる。
演算子 | 意味 |
---|---|
& | ビットごとのAND |
ビットごとのOR | |
^ | ビットごとの排他的OR |
<< | 左シフト |
>> | 右シフト |
~ | 1の補数(単項演算子) |
はてな記法上の問題でビット毎のORについてはかけなかったが「|」の記号を用いる
まず&演算子に付いてはビットをマスクするのによく使われ
n = n & 0117;
とあるなら、低位7ビット以外を0にする
|演算子はビットをオンにする役目を持ち
x = x | SET_ON;
とあるならSET_ON中で1になってるビットに対応するxのビットが1になる
ビットごとの排他的ORの^演算子では、対応するビットが異なる時にそのビットは1にされ
同じビットであるときは0にセットされる
ビットごとの演算子&,|は論理演算子&&,||と違うことに注意しなければならない
それは後者が左から右へ評価していくためである
なので x&yが0なら x&&yは1となる
シフト演算子は左被演算数と右被演算数で指定したビット数だけシフトするのに使われる
x << 2;
とあるならxを左へ2ビットシフトすることを意味し、シフトし出来た空白は0で埋められるため
この場合、左から2ビットが0になる
単項演算子~は1の補数を求めるのに使用される
これは各ビットが1なら0に0なら1に反転させるからである。
例えば
x = x ~ 0117;
とあるなら
xの下6ビットを0にする意味をもつ
2.10 代入演算子と式
次の様な式
i = i + 2;
では左辺が右辺に再び現れる
これは次の様にまとめて書くことが出来る
i += 2;
このような+=を代入演算子という
大抵の2項演算子はそれに対応する代入演算子をもつ
しかしこれには注意が必要である
以下の例で
x += y+1;
とするとこれはどう解釈できるだろうか
x = x + (y+1);
だろうか、それとも
x = x + y + 1;
正解は前者である
またこの代入演算子は様々な式に適応できて
例えば以下のようなfor文もかける
int i; for(i = 0; i < 100; i+=2){ }
こうあるならiを+1ではなく+2しながらループを回すことができる
2.11 条件式
以下のプログラムは最大値を代入するものである
if(b < a){ max = a; }else{ max = b; }
これは、3項演算子を用いて以下の様に書ける
x = (b < a)? a : b;
この二つの文は全く同じことをしている
またこれは、必ずしも代入する必要はなくprintfなど関数に値を渡す場合にもしようでき
//最大値を表示する printf("%d\n",(b < a)? a : b);
とも書ける