ポインタ変数

今回はポインタ変数の入門として,その概念を簡単に紹介する.
ポインタ変数は,C言語の学習の中でも躓きやすいところとされているが,コンピュータのハードウエアを少し理解できれば,そんなに難しい概念ではない.

目次

アドレス

メモリとディスク

コンピュータ内において,文字や数値などの情報を記憶しておくハードウエアを,記憶装置と呼ぶ.
特に,CPUでの計算や演算中にデータを保管するための領域を主記憶装置=メモリと呼ぶ.
これに対して,補助記憶装置とは,SSD, HDD,USBメモリ,SDメモリなど,いわゆるディスク類のことを指す.
近年ではSDメモリやUSBメモリのように,名前はメモリだが補助記憶装置に該当するものがあり,一概に呼び名だけで区別することはできないが,主記憶装置と補助記憶装置の役割分担は明確である.

主記憶(メモリ)内の情報はコンピュータの電源を切ると消去されるが,読み書きが非常に高速なので,計算処理中の一時保存場所として利用される.(揮発性メモリという)
一方,補助記憶装置(ディスク類)は,電源OFF後もデータを保持でき(不揮発性メモリという),その容量も大きく,メディア自体を取り外して持ち運びできる. その代わり,読み書きの速度は主記憶(メモリ)に対して遅い.

コンピュータでの処理においては,(文字も含めて)数値情報はメモリやディスクに読み書きすることで記憶・保管される. そのためには,保管場所を正確に指定・管理する必要があり,コンピュータのハードウエアや,Windows・Mac OSなどのOSには,その仕組みが用意されている.

Memory module
Desktop PCに内蔵されているメモリモジュール

変数とアドレス

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 に変えてもよい.

ポインタ変数を理解するためには,下図のように「変数の値」と,その「変数のアドレス」の概念をしっかり区別して理解しよう.

Memory
メモリ(箱)に連番で番号が付与されている.
プログラム中では識別子 i などの変数名を使用してアクセスする.
&iと書くと,iのアドレス(ここでは0x0018FF0C)が取り出せる.

アドレスとポインタ変数

アドレスは変数などが置かれている場所であることが理解できたとして,次に,その使い方を解説しよう.

アドレスは符号なし整数であるので,unsigned int に格納して利用すれば良さそうに思われるが,以下の理由からポインタ変数という仕組みが用意されている.

ポインタ変数には別の変数のアドレス値が格納されている. このことから,ポインタ変数は他の変数を指すとか,ポインタ変数が指す変数という表現をする.
これにより,ポインタを使って別の変数の参照・代入などの操作ができる.

図の矢印のように,ポインタ変数 p は変数は i指している,という言い方をする.

Pointer
ポインタ変数と,それが指す別の変数.
ポインタ変数には指す相手の変数のアドレスが格納される.

ポインタ変数の定義

一般の変数と同様,ポインタ変数も使用前に定義する必要がある.
ポインタ変数では,以下の通り定義時に変数の前にアスタリスク * をつける.

型名 *変数名;

または

型名* 変数名;

ここで,ポインタの型名は,ポインタが指す相手の変数の型であり,ポインタ変数自身の値ではない.
これは,ポインタが指す相手の変数のサイズが何バイト分かを管理する必要があるためである.
(指す相手が何型であろうと,ポインタ変数自身の値は整数である.)

型名と変数名の間に置かれる * の前後のスペースはどちらに入れても良く,以下はそれぞれ同じ意味である.

// 整数型変数を指すポインタ
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 と nullptr

注意:ヌルポインタ,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言語の文法では*記号は

  1. ポインタ変数の定義
  2. ポインタの指す値を取り出すポインタ演算子
  3. 乗算の演算子

のように,同じ記号が異なる意味で使用される
したがって,その意味は文脈で判断する必要がある.

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 

ポインタ変数に整数を加減算(++, --など)すると,アドレス値が変更される. これは,配列をポインタで操作する際に非常に有用である.

ポインタの定義に指す相手の型名が必要である理由が,ここにある.
即ち,指す相手のサイズに応じて,ポインタ変数が何バイト分を管理しているのかを決めるためである.

array0 array1
ポインタを配列の先頭に設定したのち,ポインタをインクリメント(++ で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;
}

練習問題

上記プログラムを実行し,

  1. ポインタをインクリメント,即ち1を足すと (p++ の部分) ,アドレスの値がどのように増加するか確認しよう.
    16進表記がわかりにくい場合は,%p%dに変えてもよい.
  2. (重要!)配列の型およびポインタの型をそれぞれ,charshort, float, double に変更し,同様にアドレス値がどのように増加するか確かめよう.

このように,ポインタのインクリメントでは,指している相手のサイズに応じて自動的にアドレス加算が行われる.
例えば,char*型ポインタの指す相手は 1 byte分だけを考えれば良い.
同様に,intは4 byte ,doubleは 8 byte である.

chararr intarr
(左)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;
}

練習問題

上記プログラムを実行してみよう.仮引数としてのポインタと配列の違い(違わない?)を確認しよう.

ポインタ配列

整数型変数の配列=整数配列,実数型変数の配列=実数配列,文字型変数の配列=文字列,と同様の考え方で,ポインタの配列=ポインタ配列を作ることができる.
これは,配列の各要素が,ポインタ変数であるものをいう.
ポインタ配列と,配列を指すポインタとは異なるので注意.

array of pointer
ポインタ配列.配列の各要素がポインタ変数であり,配列の各要素がそれぞれ別の変数や配列を指している.

#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

注:argv[]は,あくまで文字列であるので,数値として扱いたい場合は,atoi(), atof()などの文字列→数値変換ライブラリ関数を使用する必要がある.
例えば,argv[1]を整数に変換する場合は,int n = atoi(argv[1]);とする.

argv
ポインタ配列の各要素が,文字列の先頭を指している.

よくある間違い

ポインタ変数は,他の変数などを指している状態で使うのが原則なので,指す相手(=アドレス値)が定まっていないポインタを参照したり,代入などをしてはいけない.
もちろん,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種類あることは関数の回に述べたが,再度確認してみよう.

引数の渡し方

1.値渡し(call by value)
通常の変数の渡し方であり,呼び出される関数側の仮引数に,実引数の値がコピーされて渡される.
したがって,仮引数を変更しても元の実引数には影響を与えない.
関数に渡した変数の値を変更されたくない場合に使用.

2.アドレス渡し
値のコピーではなく,値のある場所(アドレス)を渡す方法である. すなわち,ポインタ変数を使用する渡し方である.
関数に引数を変更させたい場合に使用.
(細かいことを言えば,同じアドレスを指すポインタ変数が複製され,結果的に呼び出し元の変数を操作できる.)
C言語では,配列・文字列を関数に渡す場合はアドレス渡しとなる.

3.参照渡し(call by reference)
C++ の新しい機能で,呼び出し側の元のデータの「参照」が渡される.
本質はアドレス渡しであるが,ポインタ演算子*, &の記述を減らせるメリットがある.
一方,デメリットとしては,渡す側から見て関数で引数が変更されるか否か一目ではわからない.
(アドレス渡しであれば,& を明示して渡すので,変更される可能性があるとわかる.)

値渡し アドレス渡し 参照渡し(C++のみ)
#include <stdio.h>

void clear(int x)
{
    x = 0;
}

int main(void)
{
    int a = 100;

    printf("a=%d\n",a);

    clear(a);

    printf("a=%d\n",a);

    return 0;
}
Call by value
値渡しでは仮引数(別の変数)に値がコピーされる.
#include <stdio.h>

void clear(int *px) // ポインタで受け取る
{
    *px = 0;    // ポインタ変数には*をつける
}

int main(void)
{
    int a = 100;

    printf("a=%d\n",a);

    clear(&a);     // アドレスを渡す

    printf("a=%d\n",a);

    return 0;
}

Call by address
アドレス渡しでは仮引数が実引数を指すポインタとなり,実引数を操作できる.

#include <stdio.h>

void clear(int& x)   // 参照で受け取る
{
    x = 0;
}

int main(void)
{
    int a = 100;

    printf("a=%d\n",a);

    clear(a);  // 値渡しと同じ見た目

    printf("a=%d\n",a);

    return 0;
}

ポインタ演算子の優先順位

ポインタ変数にその都度,* がつくため,慣れないと判読が少々面倒である.

float *a = ...;
float *b = ...;

float x = *a + *b;   // floatの値同士の加算
float x = *a * *b;   // floatの値同士の乗算.*が続く・・・!

特に,乗算の場合は,ポインタ演算子と乗算が同じ記号であるため,やや読みにくい.
このような場合は,明示的に ( ) で括って,

float x = (*a) + (*b);
float x = (*a) * (*b);

などとするとわかりやすい.

C言語の各種演算子には優先順位が決められており,そのなかで,ポインタ演算子 * は順位がかなり高いため,この例では()は不要である.
しかし,演算子の優先順位を明示的するために ( ) を適宜使用すると良い.

練習問題(授業時間内に実習)

  1. 下記の実行例のように,ある文字 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...
    
  2. 自然数 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.
    
  3. 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;
    }
  4. 引数として受け取った文字列の順序を反転する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
    
  5. 要素数N個の整数配列の,ある一部分を並べ替える「部分ソーティング」を行う関数 partial_sort_bubb()を作成しよう.
    並べ替えは昇順(=小さい順)として,添え字の範囲はキーボードから入力すること.

    ソーティングにはいろいろな方法がある. ここではバブルソートを実装してみよう.
    バブルソートでは,先頭から末尾まで順に常に隣同士を比較して,所望の大小関係(ここでは小→大)となるよう入れ替える. この比較・入替を配列全体に行い,入替回数がゼロにならない間は,繰り返す.

    バブルソートのアルゴリズム:

    1. 配列の先頭の要素から開始して,先頭の値と,次の値とを比較,大→小の順になっていたら入れ替え(swap)て,小→大の順にする(swapの回数を記録).
    2. 同様に,次の値と,その次の値を比較し,大→小の順になっていたら入れ替える(swap).
    3. これを配列の終端まで行う.
    4. 上記1-3の操作(先頭から終端までを一巡とする)を,入替が起こらなくなる(即ち,swapの回数==0 となる)まで繰り返す.

    可能であれば,配列の範囲指定を(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!