C言語(応用編)~2日目~

配列とポインタ

配列はポインタを用いて操作することが可能です。本章では、配列をポインタで操作する手法について解説します。

配列の名前とポインタ

まずは、前回学習したポインタの特性を整理してみましょう。

  • ポインタはメモリアドレスを保持するための変数です。
  • ポインタ変数は他のアドレスに代入することが可能です。
  • ポインタは任意のメモリ位置を指し示すことができ、メモリを動的に操作することが可能です。

次に、配列の特徴を整理してみましょう。

  • 配列とは、同じデータ型の要素がメモリ上に連続して配置されるデータ構造のことです。
  • 配列のサイズは固定で、宣言時にその大きさが決まります。

そして、もう一つの特徴があります。配列の名前は、その配列の最初の要素のアドレスを示しています。つまり、配列名は最初の要素のアドレスを指し、それは先頭要素を指すポインタとして機能します。

例えば、`int array[5];` という配列を宣言すると、`array` は `&array[0]` と同じアドレスを指します。

プログラムコードを書いて確かめてみましょう。

int main(void){
	int array[5];
	
	printf("array=%p\n",array);
	for(int i=0;i<5;i++){
		printf("&array[%d]=%p\n",i,&array[i]);
	}
}

配列の各要素のアドレスを取得するには、変数の場合と同じように、先頭にアンパサンド(&)を付けます。
一方、配列名だけを指定すると、その配列の先頭要素のアドレスを取得することができます。
結果、arrayは&array[0]と同じアドレスという結果が得られました。

実行結果
array=000000f158fffcf0
&array[0]=000000f158fffcf0
&array[1]=000000f158fffcf4
&array[2]=000000f158fffcf8
&array[3]=000000f158fffcfc
&array[4]=000000f158fffd00

では、次のプログラムを実行してみましょう

int main(void){
	int array[5];
	
	for(int i=0;i<5;i++){
		printf("&array[%d]=%p\n",i,&array[i]);
		printf("array+%d  =%p\n",i,array+i);
		printf("*******************\n");
	}
}

配列arrayの先頭アドレスを1ずつ増やして、そのアドレスを表示しています。

【注意】アドレスを1増やすとは、単に1番地を増やすのではなく、その型のサイズ分だけアドレスを増やすということです。例えば、int型のポインタを1増やすと、通常4バイト分のアドレスが増加します。これは、sizeof(int)が4バイトであるためです。このようにして、配列の要素に順にアクセスすることができます。

実行結果
&array[0]=0000003dcfdffca0
array+0  =0000003dcfdffca0
*******************
&array[1]=0000003dcfdffca4
array+1  =0000003dcfdffca4
*******************
&array[2]=0000003dcfdffca8
array+2  =0000003dcfdffca8
*******************
&array[3]=0000003dcfdffcac
array+3  =0000003dcfdffcac
*******************
&array[4]=0000003dcfdffcb0
array+4  =0000003dcfdffcb0
*******************

array+nのアドレスは&array[n]の値と等しいことが理解できました。

前回の学習では、ポインタ変数の前に*(アスタリスク)を置くことで、そのポインタが指す値を取得できることを学びました。従って、*(array+i)と記述することにより、array[i]と同じように配列の添字を使わずに配列の要素にアクセスすることができるのです。

int main(void){
	int array[5]={1,2,3,4,5};
	
	for(int i=0;i<5;i++){
		printf("array[%d]=%d\n",i,array[i]);
		printf("array+%d=%d\n",i,*(array+i));
		printf("*******************\n");
	}
	return 0;
}
実行結果
array[0]=1
array+0=1
*******************
array[1]=2
array+1=2
*******************
array[2]=3
array+2=3
*******************
array[3]=4
array+3=4
*******************
array[4]=5
array+4=5
*******************

ポインタ変数を宣言し配列データを扱う

配列名は、配列の最初の要素を指すポインタとして機能します。従って、配列の最初の要素のアドレスをポインタ変数に割り当てたい場合、配列名をそのポインタ変数に直接代入すればよいのです。
例えば、int array[5]; で配列が宣言された場合、int *ptr = array; とすることで、ptrはarrayの最初の要素を指すポインタになります。

また、ポインタ変数を利用して配列の要素にアクセスすることも可能です。たとえば、*(ptr+1)はarray[1]と同じ値を指します。

#include <stdio.h>

int main(void){
	int array[5]={1,2,3,4,5};
	int *pt; 

  pt = array; //配列の先頭の要素のアドレスを代入
	//アドレスの確認
	for(int i=0;i<5;i++){
		printf("&array[%d]=%p\n",i,&array[i]);
		printf("pt+%d     =%p\n",i,pt+i);
		printf("*******************\n");
	}

	//要素の値の確認
	for(int i=0;i<5;i++){
		printf("array[%d]=%d\n",i,array[i]);
		printf("*(pt+%d)=%d\n",i,*(pt+i));
		printf("*******************\n");
	}
	return 0;
}

実行結果
&array[0]=0000001e571ffd70
pt+0     =0000001e571ffd70
*******************
&array[1]=0000001e571ffd74
pt+1     =0000001e571ffd74
*******************
&array[2]=0000001e571ffd78
pt+2     =0000001e571ffd78
*******************
&array[3]=0000001e571ffd7c
pt+3     =0000001e571ffd7c
*******************
&array[4]=0000001e571ffd80
pt+4     =0000001e571ffd80
*******************
array[0]=1
*(pt+0)=1
*******************
array[1]=2
*(pt+1)=2
*******************
array[2]=3
*(pt+2)=3
*******************
array[3]=4
*(pt+3)=4
*******************
array[4]=5
*(pt+4)=5
*******************

関数の引数に配列を渡す場合

関数に配列を引数として渡すことが可能です。この際、配列の値を直接渡すのではなく、配列の先頭のアドレスをポインタとして渡します。

■1次元配列の場合
以下は、配列の要素の合計を計算するサンプルプログラムです。配列を引数として受け取り、関数sum()で合計値を算出しています。

#include <stdio.h>

int sum(int p[],int size){
	int s = ;
	for(int i=0;i<size;i++){
		s = s +p[i];
	}
	return s;
}

int main(void){
	int array[5] = {1,2,3,4,5}
	int s;
	s = sum(array,5);
	printf("%d\n",s);
	
	return 0;
}

関数int sum(int p[], int size)におけるint p[]は、配列のポインタを引数として受け取ることを意味し、int sizeはその配列の要素数を指します。
配列の先頭ポインタのみが渡された場合、要素数が不明だと不便なことがあります。そのため、要素数も引数として渡しています。
sum(array, 5)という記述により、配列変数arrayの先頭アドレスと要素数5を実引数として、関数sum()が呼び出されます。

また、配列のアドレスを渡すため、関数内で配列の中身を変更することもできます。
以下は配列の各要素を二乗するサンプルプログラムです。array_pow()関数を使用して、引数として渡された配列の各要素の値を変更します。

#include <stdio.h>

int int_pow(int x){
	return x * x;
}

void array_pow(int a[],int size){
	for(int i=0;i<size;i++){
		a[i] = int_pow(a[i]);
	}
}

int main(void){
	int array[5]={1,2,3,4,5};
	
	printf("変更前\n");
	for(int i=0;i<5;i++){
		printf("array[%d]=%d\n",i,array[i]);
	}
	
	array_pow(array,5);
	
	printf("\n変更後\n");
	for(int i=0;i<5;i++){
		printf("array[%d]=%d\n",i,array[i]);
	}
	
	return 0;
}

実行結果
変更前
array[0]=1
array[1]=2
array[2]=3
array[3]=4
array[4]=5

変更後
array[0]=1
array[1]=4
array[2]=9
array[3]=16
array[4]=25

関数array_pow()において、仮引数a[]の値を変更すると、呼び出し元の配列の値も同様に変更されることが確認できます。

■2次元配列の場合
以下は、2次元配列の内容を表示するサンプルプログラムです。show1() 関数は二次元配列を受け取り、その内容を表示します。

#include <stdio.h>

void show1(int size,int a[][5]){
	for(int i=0;i<size;i++){
		for(int j=0;j<5;j++){
			printf("array[%d][%d]=%d\n",i,j,a[i][j]);
		}
	}
}

int main(void){
	int array[2][5]={
		{1,2,3,4,5},{6,7,8,9,0}
	};
	
	show1(2,array);

	return 0;

実行結果
array[0][0]=1
array[0][1]=2
array[0][2]=3
array[0][3]=4
array[0][4]=5
array[1][0]=6
array[1][1]=7
array[1][2]=8
array[1][3]=9
array[1][4]=0

この例において、2次元配列では角括弧内の最初の要素数を省略することができ、列数を渡しています。これは、2次元配列がメモリ上で1次元配列と同じように配置されているため、行がどこで区切られるかという情報が必要であるからです。

このサンプルでは、列数を固定の値で渡しているため、列が5の配列のときしか使用できません。そこで、以下のように記述することもできます。

#include <stdio.h>

void show2(int size1,int size2,int *a){
	for(int i=0;i<size1;i++){
		for(int j=0;j<size2;j++){
			int z = i * size2 + j;
			printf("array[%d][%d]=%d\n",i,j,*(a+z));
		}
	}
}
int main(void){
	int array[2][5]={
		{1,2,3,4,5},{6,7,8,9,0}
	};
	int *p = (int *)array;

	show2(2,5,p);
	return 0;
}

関数show2()には、配列の先頭アドレスを整数ポインタとして渡します。
そのため、`int *p = (int *)array;` では、配列を整数のポインタに型変換しています。
関数show2()では、引数で取得した行数、列数を使用し、int z = i * size2 + j;で対象となる領域を計算しています。

動的メモリの生成について

配列のサイズは固定で、宣言する際にそのサイズを指定する必要があります。
しかし、プログラムを作成する過程で、実行時まで必要なサイズが不明な場合もあります。
そのような場合は、動的にメモリを割り当てる方法を使用します。
動的メモリは、必要なメモリ領域をmalloc関数で確保し、ポインタを通じてアクセスするものです。
malloc関数の書式は以下のようになります。

void* malloc( size_t size )

malloc関数を使用する際には、ファイルの先頭で#include <stdlib.h>を定義する必要があります。
size_t size は、確保するメモリ領域のバイト数を指定するために使用されます。

メモリを確保後、不要になった場合は必ずfree()関数を実行して確保したメモリを破棄することが必要です。

整数型の配列のメモリを確保するプログラムを作成してみましょう。

#include <stdio.h>
#include <stdlib.h>

int main(void){
	int x = 3;
	int *p1 = NULL;
	
	p1 = (int *)malloc(sizeof(int) * x);
	if (p1 == NULL){
		printf("メモリ確保に失敗しました\n");
		return 1;
	}
	
	for(int i=0;i<x;i++){
		p1[i] = i;
	}
	
	for(int i=0;i<x;i++){
		printf("p1[%d] = %d\n",i,p1[i]);
	}

	free(p1);
	return 0;
	
  • int *p1 = NULL; このコードはメモリ領域のアドレスを格納するためのポインタ変数を用意しNULL(’\0’)で初期化しています。
  • p1 = (int *)malloc(sizeof(int) * x); このコードは、要素数xのint型配列に必要なメモリ領域を確保します。ここで、sizeof(int) * xはint型の要素がx個分のサイズを計算しています。
  • malloc()関数は、指定したサイズのメモリを確保できた場合、そのメモリの先頭アドレスを返却値として返します。そのため、このアドレスをポインタとして受け取ることになります。
  • malloc()関数の返却値はvoid型であり、任意の型のポインタに変換できることを意味します。ここでは、int型のポインタとして使用するために型変換を行っています。
  • メモリ確保が失敗した場合はNULL(’\0’)ポインタが返されます。NULL(’\0’)が返された場合は、プログラムをこれ以上実行することはできないため、メモリ確保失敗のメッセージを出力してプログラムを終了します。
  • p1[i] = i;は、確保されたメモリを配列のように扱っており、*(p1+i)とも記述できます。
  • free(p1); このコードで確保した領域を開放します。
実行結果
p1[0] = 0
p1[1] = 1
p1[2] = 2

動的メモリのサイズの変更について

malloc()関数で確保したメモリ領域を拡張するには、realloc()関数を使用します。
realloc()関数の書式は以下のようになります。

void *realloc(void *ptr, size_t size);

void *ptrは、malloc()関数で確保されたメモリ領域を指すポインタです。
size_t sizeは変更したい領域のバイト数を指定します。
※realloc()関数でメモリ領域のサイズを変更すると、返される先頭アドレスが異なることがあります。この場合、malloc()関数で確保された元のメモリ領域は自動的に解放され、新しく確保されたメモリ領域のアドレスが返されています。

では、実際のサンプルを見てみましょう

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    // int型の配列を10個分のメモリを確保する
    int *array = (int *)malloc(sizeof(int) * 10);
    if (array == NULL) {
        printf("メモリの確保に失敗しました。\n");
        return 1;
    }

    // 配列に値を代入する
    for (int i = 0; i < 10; i++) {
        array[i] = i + 1;
    }

    // 配列の内容を表示する
    printf("変更前の配列:\n");
    for (int i = 0; i < 10; i++) {
        printf("%d ", array[i]);
    }
    printf("\n");

    // 配列のサイズを20個分に変更する
    int *new_array = (int *)realloc(array, sizeof(int) * 20);
    if (new_array == NULL) {
        printf("メモリのサイズ変更に失敗しました。\n");
        free(array); // 確保したメモリを解放する
        return 1;
    }

    // 配列に値を追加する
    for (int i = 10; i < 20; i++) {
        new_array[i] = i + 1;
    }

    // 配列の内容を表示する
    printf("変更後の配列:\n");
    for (int i = 0; i < 20; i++) {
        printf("%d ", new_array[i]);
    }
    printf("\n");

    free(new_array); // 確保したメモリを解放する
    return 0;
}

実行結果
変更前の配列:
1 2 3 4 5 6 7 8 9 10
変更後の配列:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

realloc()関数から戻り値を受け取るために、new_arrayという新しいポインタを使用します。もしrealloc()関数がメモリの再確保に失敗してNULLを返した場合、malloc()関数で取得した元のポインタarrayで受け取るようにしていると、メモリ解放のためのアドレスがNULLで上書きされてしまいます。そのため、realloc()関数は必ずmalloc()関数で取得したポインタとは異なる新しいポインタで受け取るようにします。

コメント

この記事へのコメントはありません。

関連記事

Python 応用編 7日目

プロっぽいC言語の考え方

ポインタの極意 前編

PAGE TOP