今回はポインタ変数の入門として,その概念を簡単に紹介する.
ポインタ変数は,C言語の学習の中でも躓きやすいところとされているが,コンピュータのハードウエアを少し理解できれば,そんなに難しい概念ではない.
コンピュータ内において,文字や数値などの情報を記憶しておくハードウエアを,記憶装置と呼ぶ.
特に,CPUでの計算や演算中にデータを保管するための領域を主記憶装置=メモリと呼ぶ.
これに対して,補助記憶装置とは,SSD, HDD,USBメモリ,SDメモリなど,いわゆるディスク類のことを指す.
近年ではSDメモリやUSBメモリのように,名前はメモリだが補助記憶装置に該当するものがあり,一概に呼び名だけで区別することはできないが,主記憶装置と補助記憶装置の役割分担は明確である.
主記憶(メモリ)内の情報はコンピュータの電源を切ると消去されるが,読み書きが非常に高速なので,計算処理中の一時保存場所として利用される.(揮発性メモリという)
一方,補助記憶装置(ディスク類)は,電源OFF後もデータを保持でき(不揮発性メモリという),その容量も大きく,メディア自体を取り外して持ち運びできる.
その代わり,読み書きの速度は主記憶(メモリ)に対して遅い.
コンピュータでの処理においては,(文字も含めて)数値情報はメモリやディスクに読み書きすることで記憶・保管される. そのためには,保管場所を正確に指定・管理する必要があり,コンピュータのハードウエアや,Windows・Mac OSなどのOSには,その仕組みが用意されている.
C言語のプログラム中において
int i;
char moji[100];
float data[10];
のような変数定義をすると,OSにより主記憶装置(メモリ)中に記憶領域が自動的に確保される.
そして,プログラム中からは,i, moji, data[0]などの識別子=名前を使って,変数への代入や,変数の参照などのアクセスができる.
通常の計算処理ではこれで充分であるが,時として変数がメモリ内のどの場所に置かれているかを知る必要がある.
このデータの置かれているメモリ内の場所を,アドレス, addressと呼ぶ.
コンピュータにおけるアドレス値は,実世界のアドレス(=住所・番地)とは異なり,実体は符号なし整数である.
例えば,16進数で 0x000000001E223FF4DE352BC9 のような値である.
「アドレス」の概念は C言語固有では無く,一般にコンピュータすべてに共通するものであり,C言語ではアドレスを直接扱う方法が言語仕様に組み込まれている.
逆に.他のプログラミング言語では,アドレスの扱いを極力ユーザーに見えないようにしているものもある.
C言語では,このメモリ内の場所を表すアドレス値を格納するための専用の変数が用意されており,
これをポインタ変数 と呼ぶ.
ポインタ変数は,単に「ポインタ」と呼ばれることもある.
プログラム中で,ある変数のアドレスを知りたいときは,その変数名の前に & 演算子をつける.
(そうです!,scanf() 関数で,変数の前につけろ,と言われたアレです.)
#include <stdio.h>
int main(void)
{
int i = 100;
printf("value = %d \n", i ); // 変数の値を表示
printf("address = %p \n", &i ); // 変数のアドレスを表示
return 0;
}
実行例1:
value = 100
address = 000000000061FE1C
実行例2:
value = 100
address = 0000004827BFFE3C
address値は,この値になるとは限らない.また,処理系によっては先頭の 0x が自動的に付く場合がある.
上記プログラムを実行し,アドレス値を確認してみよう.
ここに表示された address の値が,変数 i のアドレスである.
16進数表記が分かりにくければ,printf() 中の %p の箇所を %d に変えてもよい.
ポインタ変数を理解するためには,下図のように「変数の値」と,その「変数のアドレス」の概念をしっかり区別して理解しよう.
プログラム中では識別子 i などの変数名を使用してアクセスする.
&iと書くと,iのアドレス(ここでは0x0018FF0C)が取り出せる.
アドレスは変数などが置かれている場所であることが理解できたとして,次に,その使い方を解説しよう.
アドレスは符号なし整数であるので,unsigned int に格納して利用すれば良さそうに思われるが,以下の理由からポインタ変数という仕組みが用意されている.
ポインタ変数には別の変数のアドレス値が格納されている.
このことから,ポインタ変数は他の変数を指すとか,ポインタ変数が指す変数という表現をする.
これにより,ポインタを使って別の変数の参照・代入などの操作ができる.
図の矢印のように,ポインタ変数 p は変数は i を指している,という言い方をする.
ポインタ変数には指す相手の変数のアドレスが格納される.
一般の変数と同様,ポインタ変数も使用前に定義する必要がある.
ポインタ変数では,以下の通り定義時に変数の前にアスタリスク * をつける.
型名 *変数名;または型名* 変数名;
ここで,ポインタの型名は,ポインタが指す相手の変数の型であり,ポインタ変数自身の値ではない.
これは,ポインタが指す相手の変数のサイズが何バイト分かを管理する必要があるためである.
(指す相手が何型であろうと,ポインタ変数自身の値は整数である.)
型名と変数名の間に置かれる * の前後のスペースはどちらに入れても良く,以下はそれぞれ同じ意味である.
// 整数型変数を指すポインタ
int *p;
int* p;
// 浮動小数点型変数を指すポインタ
double *p;
double* p;
// 文字型変数を指すポインタ
char *p;
char* p;
ただし,複数のポインタ変数を1行で定義する場合は,* 付け方に注意を要する.
// (1) int *p1; int p2; と等価.
// p1はポインタ,p2は普通の変数.
int* p1, p2;
// (2) このように書けば, p1 も p2 もポインタ変数となる.
int *p1, *p2;
// (3) 1行に1つ書くスタイル.明瞭かつ変更しやすいのでおすすめ
int *p1;
int *p2;
C言語において,変数は定義直後,中身が不定ときまっているので,ポインタが予期しない場所を指している可能性がある.
そこで,ポインタ変数では特別にどの変数も指していない状態を設定することができる.
int *p = NULL;
char *p2 = NULL;
double *p3 = NULL;
大文字表記の NULL は,ヌルポインタ (NULL pointer) と呼ばれ,どの変数も指していない無効なポインタという意味である.
ナルポインタ,と呼ぶ場合もある.
ヌルポインタ NULL の値を調べてみよう.
ヒント:NULL の値を printf の書式指定 %p,または整数 %d などで表示してみよう.
注意:ヌルポインタ,NULL は文字列で学んだヌル文字, '\0' とは異なる.
(ただし,結果的に双方とも数値の「ゼロ」が割り当てられている場合が多い.)
このような曖昧さを回避するため,最近のC++では NULL ではなく nullptr を使うのが推奨されている.
この授業では,互換性を考慮して NULL を使用する.
*, &
ポインタ変数には,アドレス値(符号なし整数)を直接代入することが可能である.
(ただし,最近のコンパイラでは,キャストしないと警告・エラーとなる場合が多い.)
int *p;
p = 0x1122ffee;
しかし,このような定数の直接的な代入は,通常では行わない.
(例外として,マイコンなどの入出力などで直接アドレス指定がされている場合がある.
マイコンや組み込み機器などのハードウエアに近い処理では,今でもC言語が使われている由縁である.)
ではどのようにするかと言うと,ポインタ変数が指したい他の変数のアドレスを & 演算子で取り出し,ポインタ変数に代入する.
逆に,ポインタ変数に* 演算子をつけると,ポインタの指す相手の変数を操作できる.
int i=100; // int 型のふつうの変数.
int *p; // int 型変数を指すポインタ p の定義.不定な状態
p = &i; // p に,i のアドレスを代入 → ここではじめて p は i を指す状態となる
このような指定をしておくと,変数 i への代入などを,ポインタ p を使って操作できる.
*p = 20; // p が指す変数,即ち i=20; と等価
ポインタ操作に関して重要な演算子は,アスタリスク * と,アンパサンド(アンド) & である.
& 演算子は,一般の変数の「アドレス」を取り出す.* 演算子は,ポインタ変数の指す変数に「格納された値」を取り出す.間接参照演算子と呼ぶ#include <stdio.h>
int main(void)
{
double r=0.5; // これはdouble型の変数.
double *p; // これはdouble型変数を指すポインタ p の定義.
p = &r; // r のアドレス &r を,ポインタ p に代入する.
printf(" r = %lf , &r = %p \n", r, &r ); // &r は「変数 r が置かれているアドレス」== ???
printf(" *p = %lf , p = %p \n", *p, p ); // *p は「ポインタ p の指すアドレスにある値」== 5
return 0;
}
上記プログラムを実行し,変数の「値」と「アドレス」の関係を理解しよう.
また,ポインタ演算子を使用した「変数のアドレスを取り出して,ポインタに代入」と,「ポインタの指すアドレスにある値を操作」を理解しよう.
ポインタの指す相手は,プログラムの実行中に自在に変更できる. ポインタがどの変数を指しているのかをよく考えながらプログラムを書く必要がある.
#include <stdio.h>
int main(void)
{
int a = 1, b = 20;
int *p = NULL; // ポインタ変数の定義.NULL で初期化しておく.
printf("Initial values.\n");
printf(" a = %3d, b = %3d\n", a, b); // a,b の値を表示
printf("&a = %p, &b = %p\n", &a, &b); // a,b のアドレスを表示
printf(" p = %p \n", p ); // pの値を表示
printf("--------------------\n");
p = &a; // ポインタの指す相手を a に設定
*p = 55; // *p の値を変更
printf("a=%3d, b=%3d, p=%p, *p=%3d \n", a, b, p, *p ); // a,b の値と,p,*pを表示
printf("--------------------\n");
p = &b; // ポインタの指す相手を b に変更.
*p = 77; // *p の値を変更.
printf("a=%3d, b=%3d, p=%p, *p=%3d \n", a, b, p, *p ); // a,b の値と,p,*pを表示
return 0;
}
上記プログラムを実行し,a の値,b の値,およびポインタ p の値がどう変わるか確かめてみよう.
ここでは「ポインタの値」とは,「ポインタの指す変数 a,b の値」ではなく,「ポインタ自身の値(=アドレス)」のことである.
* 演算子の使い分けに注意
文字・記号の限られたソースコードにはよくあるが,C言語の文法では*記号は
のように,同じ記号が異なる意味で使用される.
したがって,その意味は文脈で判断する必要がある.
int n = 10;
int *p = &n; // 1.「pは,ポインタ変数です」というポインタの定義
*p += 20; // 2.ポインタ p の指す別の変数の値を取り出す「ポインタ演算子」
n = n * 2; // 3.「掛け算」の記号
「配列」と「ポインタ」とは非常に親和性が高く,実は [ ] を付けない配列名には,配列の先頭アドレスが格納されている.
次の例で配列のアドレスを確認してみよう.
(アドレス表示の16進数がわかりにくい場合は,%p を %d または %u に変えてみよう.)
#include <stdio.h>
int main(void)
{
int a[] = {10, 20, 30}; // 配列の定義と初期化
// 配列の各要素のアドレスを表示
printf("Address of array : %p %p %p \n", &a[0], &a[1], &a[2]);
// 配列の先頭アドレスを示すようにポインタを初期化. p = &a[0]; と等価.
int *p = a;
// p と p の指す相手の値(*p)を表示
printf("initial : p = %p, *p = %d \n", p, *p);
return 0;
}
実行例:
Address of array : 000000000061FE0C 000000000061FE10 000000000061FE14
initial : p = 000000000061FE0C, *p = 10
(%p を %d に変更して実行)
Address of array : 6422028 6422032 6422036
initial : p = 6422028, *p = 10
ポインタ変数に整数を加減算(++, --など)すると,アドレス値が変更される.
これは,配列をポインタで操作する際に非常に有用である.
ポインタの定義に指す相手の型名が必要である理由が,ここにある.
即ち,指す相手のサイズに応じて,ポインタ変数が何バイト分を管理しているのかを決めるためである.
++ で1を足す)ポインタ変数のインクリメントで,アドレス値が実際にどうに変化するかを確認してみよう.
#include <stdio.h>
int main(void)
{
int a[] = {10, 20, 30}; // 配列の定義と初期化
int *p = a;
printf("Address of array : %p %p %p \n", &a[0], &a[1], &a[2]);
printf("initial : p = %p, *p = %d \n", p, *p); // p と p の指す相手の値(*p)を表示
p++; // p に 1 を加える
printf("p++ : p = %p, *p = %d \n", p, *p);
p++; // p に 1 を加える
printf("p++ again : p = %p, *p = %d \n", p, *p);
return 0;
}
上記プログラムを実行し,
p++ の部分) ,アドレスの値がどのように増加するか確認しよう.%pを%dに変えてもよい.
char や short, float, double に変更し,同様にアドレス値がどのように増加するか確かめよう.
このように,ポインタのインクリメントでは,指している相手のサイズに応じて自動的にアドレス加算が行われる.
例えば,char*型ポインタの指す相手は 1 byte分だけを考えれば良い.
同様に,intは4 byte ,doubleは 8 byte である.
char配列の場合,1要素は 1byte. (右)int配列の場合.サイズが 2 や 8 となる処理系もある.
配列を指すポインタに整数を加減算するとアドレス値が変更されるので,文字列操作に非常に有用である.
#include <stdio.h>
int main(void)
{
char s[] = "She has a pen and a book."; // 文字列
printf("Address of array : %p %p %p \n", &s[0], &s[1], &s[2]); // 最初の3要素のアドレスを表示
char *p = &s[0]; // 配列の先頭を示すよう,ポインタを初期化
printf("initial : p = %p, *p = %c \n", p, *p); // p および p の指す相手の値(*p)を表示
p++; // p に 1 を加える?
printf("p++ : p = %p, *p = %c \n", p, *p);
p++; // *p に 1 を加える?
printf("*p++ : p = %p, *p = %c \n", p, *p);
return 0;
}
上記プログラムを実行し,p自身の値と,pの指す相手の値の変化をよく理解しよう.
関数の引数に配列を渡す場合,配列本体と,要素数を一緒に渡す.
(∵C言語では,配列はアドレス渡しとなる仕様なので,関数には配列の先頭の場所しか渡されない.=配列サイズを渡さないと終端が判別できない.)
具体的には,以下のように記述する.
#include <stdio.h>
// 配列本体 a と,要素数 n を一緒に引数に取れば,任意の要素数の配列に対応できる.
void disp_array(int data[], int n) // [] のみでOK.
{
int i;
for(i=0; i<n; i++) {
printf("data[%d] = %d \n", i, data[i] );
}
}
int main(void)
{
const int N = 5; // 配列の要素数
int a[N] = {2, 5, 13, 14, 15}; // 配列
disp_array( a, N ); // 関数呼び出し,配列を渡す
return 0;
}
配列を関数に渡す場合はアドレス渡しとなるので,ポインタ変数で受け取ることもできる.
void disp_array(int *p, int n) // pは配列の先頭アドレス
{
int i;
for(i=0; i<n; i++) {
printf("data[%d] = %d \n", i, *(p+i) ); // アドレスが自動で計算できる
}
}
注意:C言語の仕様では,関数に渡す配列はアドレス渡しとなり,呼び出した関数内で配列の内容を変更すると,呼び出し元の配列の内容も変更される!
* と,配列演算子[] C言語の文法においてポインタと配列は異なる概念であるが,いずれもアドレスであるので,その使い方は非常によく似ている.
下記のように,配列を指すポインタに配列演算子[ ] を使用することができる.
逆に,配列にたいして整数を加算してポインタのようにアドレスの計算を行うこともできる.
この様に,ポインタと配列は相互に互換性があるような使い方が可能である.
違いは,配列名は++,--して,そのアドレスを変更することはできない.
以下のソースコードで確かめてみよう.
#include <stdio.h>
int main(void)
{
// Array
char s[] = "Meiji University";
s[0] = 'A'; // OK
s++; // NG! s は,変更不可
// Pointer
char *p = s;
p[0] = 'A'; // OK
p++; // OK
return 0;
}
コンパイル結果:
test.cpp:7:6: error: lvalue required as increment operand
s++;
ただし,これには例外があり,以下のように,配列が関数の仮引数である場合は,ポインタと同じようにアドレスの増減ができてしまう.
つまり,関数の仮引数として配列を渡す場合,配列としても,ポインタとしても,区別なく使うことができる.
#include <stdio.h>
void func(char s[]) // 配列 s は関数の仮引数
{
s[0] = 'A'; // OK
s++; // この場合はOK
char *p = s;
p[0] = 'A'; // OK
p++; // OK
}
int main(void)
{
char s[] = "Meiji University";
func(s);
return 0;
}
#include <stdio.h>
void f1(int a[], int N) // 配列で受ける
{
for(int i=0; i<N; i++) {
printf("%d ", *(a+i)); // ポインタの如く利用
}
printf("\n");
}
void f2(int *p, int N) // ポインタで受ける
{
for(int i=0; i<N; i++) {
printf("%d ", p[i] ); // 配列の如く利用
}
printf("\n");
}
int main(void)
{
const int N = 5;
int a[N] = {10,20,30,40,50};
f1(a,N);
f2(a,N);
return 0;
}
上記プログラムを実行してみよう.仮引数としてのポインタと配列の違い(違わない?)を確認しよう.
整数型変数の配列=整数配列,実数型変数の配列=実数配列,文字型変数の配列=文字列,と同様の考え方で,ポインタの配列=ポインタ配列を作ることができる.
これは,配列の各要素が,ポインタ変数であるものをいう.
ポインタ配列と,配列を指すポインタとは異なるので注意.
#include <stdio.h>
int main(void)
{
char* p[3]; // 文字型を指すポインタ変数 x 3個 の配列.
char h[] = "Hello";
char w[] = " World";
char e[] = "!.";
// 2つの文字列 h,w の先頭アドレスを,ポインタ配列pに代入
// アドレスを代入しているだけなので,strcpy() は不要.
p[0] = h; // p[0] はポインタ p[0]=&h[0]; でもOK
p[1] = w; // p[1] もポインタ
p[2] = e; // p[2] もポインタ
printf("%s%s%s\n", p[0], p[1], p[2]);
return 0;
}
main()関数の引数
main()関数の引数は,正式には int main(int argc, char* argv[])と書く.
これは,実行ファイル (a.exe) を実行させたとき,これに続く文字列をプログラムに渡す際に用いられ,非常に重要な機能である.
例えば,ソースファイルをコンパイルする際に,c++ test.cpp のように,引数としてファイル名を渡す方法である.
main() の最初の引数 argc は引数の数,次の argv は文字列を指すポインタの配列である.
#include <stdio.h>
int main(int argc, char* argv[])
// argc : 引数の個数
// argv : 引数(文字列)の先頭を指すポインタ配列
{
printf("argc = %d\n", argc);
int i;
for(i=0; i<argc; i++) {
printf("argv[%d] = %s\n", i, argv[i]); // argv[i] は,文字列の先頭を指すポインタ
}
return 0;
}
上記のプログラムを実行する際,実行ファイル名に続いて以下のようにスペース区切りで単語を入力してみよう.
.\a.exe aaa bbb 123 456
以下のような出力になるはずである.
argc = 5 argv[0] = a.exe argv[1] = aaa argv[2] = bbb argv[3] = 123 argv[4] = 456
argcには引数の個数,argv[]には文字列の先頭を指す,ポインタが格納される.argv[0]には,常に実行ファイルの名前が格納される.注:argv[]は,あくまで文字列であるので,数値として扱いたい場合は,atoi(), atof()などの文字列→数値変換ライブラリ関数を使用する必要がある.
例えば,argv[1]を整数に変換する場合は,int n = atoi(argv[1]);とする.
ポインタ変数は,他の変数などを指している状態で使うのが原則なので,指す相手(=アドレス値)が定まっていないポインタを参照したり,代入などをしてはいけない.
もちろん,NULLポインタに代入することも禁止である.
#include <stdio.h>
int main(void)
{
int *p; // これだけでは何もさしていない!
// int *p = NULL; // NULLポインタ
*p = 100; // OMG !! どこに代入される?
return 0;
}
タチの悪いことに,このコードは多くの場合コンパイルエラーが出ず,実行しても問題が起きない場合も多い.
このような「迷子のポインタ」を使うことはプログラムの深刻なバグになるので,ポインタ変数は慎重に扱う必要がある.
(意図的にポインタでエラーを起こす場面は通常は無いが,うっかり無効なポインタ参照をしてしまうことがよくある.)
#include <stdio.h>
int main(void)
{
int data;
int *p = &data;
*p = 100; // pはdataを指しているので,OK
return 0;
}
上記の二つのプログラムを実行して,どのようになるか(コンパイルエラー,実行時エラーなど)を確かめよ.
関数に引数を渡す方には,3種類あることは関数の回に述べたが,再度確認してみよう.
引数の渡し方
*, &の記述を減らせるメリットがある.& を明示して渡すので,変更される可能性があるとわかる.)
ポインタ変数にその都度,* がつくため,慣れないと判読が少々面倒である.
float *a = ...;
float *b = ...;
float x = *a + *b; // floatの値同士の加算
float x = *a * *b; // floatの値同士の乗算.*が続く・・・!
特に,乗算の場合は,ポインタ演算子と乗算が同じ記号であるため,やや読みにくい.
このような場合は,明示的に ( ) で括って,
float x = (*a) + (*b);
float x = (*a) * (*b);
などとするとわかりやすい.
C言語の各種演算子には優先順位が決められており,そのなかで,ポインタ演算子 * は順位がかなり高いため,この例では()は不要である.
しかし,演算子の優先順位を明示的するために ( ) を適宜使用すると良い.
下記の実行例のように,ある文字 c と,続く単語 word を入力したとき, word 中に文字 c が存在すれば 1 を,存在しなければ 0 を返す関数hantei()を作成して,文字の存在を判定せよ.
#include <stdio.h>
const int MAX_CHAR = 128; // 入力文字数の最大値
int hantei(char c, char* str)
{
...
}
int main(void)
{
char word[MAX_CHAR];
char c;
printf("Input: ");
scanf("%c", &c); // 1文字を入力
scanf("%[^\n]%*c", word); // 単語を入力
if(hantei(...) == ??) {
printf("Found!");
} else {
printf("Not found...");
}
return 0;
}
【実行例】
Input: a apple
Found!
Input:b apple
Not found...
自然数 n(>0) と,続く文章 string を1行で入力したとき, string 中の文字を n 個おきに表示する.
#include <stdio.h>
const int MAX_CHAR = 128; // 入力文字数の最大値
int main(void)
{
int n;
char word[MAX_CHAR];
scanf(...);
char *p = &word[0];
// p を用いて処理
return 0;
}
【実行例】
入力:2 Hello!
出力:Hlo
入力:3 M3Eed!issj*(iZl.#Q
出力:Meiji.
8桁の2進数を,10進数に変換してみよう.
2進数は8文字の文字列で与えられるものとし,以下のような変換する関数を作成しよう.
(数値の後につく()内の数値は,何進数かを表す.)
ヒント:
00000001(2) = 1(10)
00000010(2) = 2(10)
00000100(2) = 4(10)
00001000(2) = 8(10)
00010000(2) = 16(10)
00100000(2) = 32(10)
01000000(2) = 64(10)
10000000(2) = 128(10)
#include <stdio.h>
int binary2dec(char str[]) // 引数は char *p でもOK
{
...
}
int main(void)
{
char data[9] = "11011011"; // 8文字+ヌル文字=9個.値を変えても正しく動作すること.
printf("%s(2) = %d(10)\n", data, binary2dec(data));
return 0;
}
引数として受け取った文字列の順序を反転するinvert_str()関数を作成せよ.
処理のヒント:invert_str()関数内で
方法1:文字列バッファ(仮の変数)を用意し,受け取った仮引数の文字列をいったんすべてバッファにコピー,元の配列に逆順で上書き.
方法2:引数の文字列にたいして,先頭と末尾の文字をswap, 2番目と末尾-1番目の文字をswap, ... を,(文字列長さ)/2 まで繰り返す.
swap()関数を作成しておくとよい.
表示はmain()関数で行い,invert_str()では表示をせず,文字列を逆順に入れ替えるのみとすること.
#include <stdio.h>
const int MAX_CHAR = 128; // 入力文字数の最大値
void invert_str(char* str)
{
char tmp[MAX_CHAR]; // 一時保管用の文字列.
// ここでは画面表示を行わないこと
}
int main(void)
{
char words[MAX_CHAR];
printf("Type words: ");
scanf("%[^\n]%*c", words); // %[^\n] は,改行コード以外を格納.%*c は,最後の改行コードを読み飛ばす処理.
invert_str(words);
printf("%s", words);
return 0;
}
【実行例】
Type words: Hello, world 2024!
-> !4202 dlrow ,olleH
要素数N個の整数配列の,ある一部分を並べ替える「部分ソーティング」を行う関数 partial_sort_bubb()を作成しよう.
並べ替えは昇順(=小さい順)として,添え字の範囲はキーボードから入力すること.
ソーティングにはいろいろな方法がある.
ここではバブルソートを実装してみよう.
バブルソートでは,先頭から末尾まで順に常に隣同士を比較して,所望の大小関係(ここでは小→大)となるよう入れ替える.
この比較・入替を配列全体に行い,入替回数がゼロにならない間は,繰り返す.
バブルソートのアルゴリズム:
可能であれば,配列の範囲指定を(scanf() ではなく)コマンドラインから直接引数として与えるようにしてみよう.
また,入力された整数の範囲が,配列の添え字の範囲を超えていないかどうかチェックすること.もちろん大小関係も要チェック.
#include <stdio.h>
void swap(...)
{
// おなじみの入れ替え関数
}
void partial_sort_bubb(int *p_beg, int *p_end)
// 並べ替えを行う部分配列の先頭・終端アドレスを受け取る
{
int swapcount; // 1巡するうちに,入れ替えが発生した回数
do {
swapcount=???; // 毎ループごとに初期化
// 先頭から順に隣接値を比較
// 大小関係を比較して条件によってswap
int *p;
for( p = p_beg ; ??? ; p++) { // ポインタ変数でループ?
if(???) {
...
swapcount++; // 入れ替え発生! カウンタをインクリメント
}
}
} while(???); // 入替が発生しているうちは繰り返す.
}
int main(int argc, char* argv[])
{
const int N = 20;
int data[N] = {3, 9, 8, 4, 5, 1, 10, 3, 4, 7, 2, 8, 4, 5, 8, 2, 7, 4, 9, 0};
// 並べ替え範囲を入力
// できれば argv[1], argv[2] を数値に変換して使用する
int beg, end;
scanf(...);
// 入力されたbeg, end が,配列の範囲を超えていないかチェック.
if(???) {
printf("Out of range!\n");
return 0;
}
// 入力されたbeg, end の順序をチェック
if(???) {
printf("Wrong order of input numbers\n");
return 0;
}
int i;
printf("before:");
for(i=0; i<N; i++) {
printf("%d ", data[i]);
}
printf("\n");
partial_sort_bubb(data+beg, data+end); // 並べ替え区間の始めと終わりを,アドレスで渡す
printf("after :");
for(i=0; i<N; i++) {
printf("%d ", data[i]);
}
printf("\n");
return 0;
}
実行例 1 : scanf()で範囲を入力させる例.
5 10 <-添え字の範囲を入力
before:3 9 8 4 5 1 10 3 4 7 2 8 4 5 8 2 7 4 9 0
after :3 9 8 4 5 1 2 3 4 7 10 8 4 5 8 2 7 4 9 0
実行例 2 : a.exeに続いて,argvに範囲を入力.
a.exe 5 10 <- a.exeに続いて添え字の範囲を入力
before:3 9 8 4 5 1 10 3 4 7 2 8 4 5 8 2 7 4 9 0
after :3 9 8 4 5 1 2 3 4 7 10 8 4 5 8 2 7 4 9 0
実行例 3 : 範囲エラーの例.
a.exe 5 50 <- a.exeに続いて添え字の範囲を入力
Out of range!