Cにおける文字列の配列 - charの二次元配列と、charのポインターのポインターの違い -



charの二次元配列と、charのポインターのポインターは、どちらも文字列の配列として利用できる。
しかし、両者は同じではない。
違いがわからなかったため、以下に整理した。

charの配列と、charのポインター

先に、より簡単な、charの配列と、charのポインターについて整理した。
charの配列も、charのポインターも、文字列として扱える。

#include <stdio.h>

int main() {
 // charの配列
 char name_a[] = "john lennon";
 printf("%s\n", name_a);
 // charのポインター
 char *name_p = "paul mccartney";
 printf("%s\n", name_p);
 return 0;
}

実行結果
john lennon
paul mccartney

(下記コードのname_p = name_aのように)配列をindex指定なしで使うと、配列の先頭要素のアドレスとして扱われる。 
このため、アドレスを格納するための型であるcharのポインターに代入できる。
しかし、配列の先頭要素のアドレスは変更できない仕様のため、charのポインターを配列に(先頭要素のアドレスとして)代入することはできない。

#include <stdio.h>

int main() {
 char name_a[] = "john lennon";
 char *name_p = "paul mccartney";

 /*
   * index指定なしだと、配列は、配列の先頭要素のアドレスとして扱われる
  * よって、下記の2つは同じ値を出力する
  */
 printf("%p\n", &name_a[0]); /* &は、変数のアドレスを取得する演算子 */
 printf("%p\n", name_a);

 /*
  * 配列の先頭要素の「アドレス」なので、charのポインターに代入できる
  */
 name_p = name_a;
 printf("%s\n", name_p);
 // &name_a[0]やname_aと同じアドレスを出力
 printf("%p\n", name_p);

 // 配列の先頭要素のアドレスは変更不可のため、下記はコンパイルエラー
 // name_a = name_p;
 return 0;
}

実行結果
0x7ffee50e25ac
0x7ffee50e25ac
john lennon
0x7ffee50e25ac

文字列をダブルクオーテーションで囲んだ右辺の式(文字列リテラル)は、必要な大きさのcharの配列を作成(メモリーを確保)し、そのcharの配列の先頭要素のアドレスを返す仕様と思われる。
このため、name_pにも代入できる仕様のようだ。 
であれば、charの配列の初期化時、つまりname_a[]にname_pを代入できそうなものだが、配列の初期化は{'j', 'h', 'o', 'n'}のような形または文字列リテラルのみ受け付ける仕様のようなので、コンパイルできない。

charの二次元配列と、charのポインターのポインター

本題に入る。

charの二次元配列も、charのポインターのポインターも、下記のように文字列の配列として扱うことができる。

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

int main() {
 // 15は、"paul mccartney"の文字数(14) + \0の文字数(1)
 char names_a[2][15] = {"john lennon", "paul mccartney"};
 for (int i = 0; i < 2; i++) {
  printf("%s\n", names_a[i]);
 }
 
 // charのポインターの配列を表現するため、charのポインター(char *)のサイズ 2個分のメモリーを確保
 char **names_p = malloc(sizeof(char *) * 2);
 char *name1 = "john lennon";
 char *name2 = "paul mccartney";
 names_p[0] = name1;
 names_p[1] = name2;
 for (int i = 0; i < 2; i++) {
  printf("%s\n", names_p[i]);
 }
 // 確保したメモリーの開放
 free(names_p);

 return 0;
}

実行結果
john lennon
paul mccartney
john lennon
paul mccartney

上記のように、どちらもindexでアクセスして文字列として扱えるため、一見、charの二次元配列と、charのポインターのポインターは、同じに見える。
しかし、下記のように、charのポインターにcharの配列を代入したノリで、charのポインターのポインターに、charの2次元配列を代入しても、動かない。
charの二次元配列と、charのポインターのポインターは、別物である。

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

int main() {
 char names_a[2][15] = {"john lennon", "paul mccartney"};
 char **names_p = malloc(sizeof(char *) * 2);

 // これは動かない
 names_p = (char **)names_a;
 for (int i = 0; i < 2; i++) {
  printf("%s\n", names_p[i]);
 }

 free(names_p);

 return 0;
}

下記のように、charの2次元配列の要素を1つ1つ代入していけば、動作する。

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

int main() {
 char names_a[2][15] = {"john lennon", "paul mccartney"};
 char **names_p = malloc(sizeof(char *) * 2);

 // これは動く
 names_p[0] = names_a[0];
 names_p[1] = names_a[1];

 for (int i = 0; i < 2; i++) {
  printf("%s\n", names_p[i]);
 }

 free(names_p);

 return 0;
}

実行結果
john lennon
paul mccartney

これは、考えると当たり前で、charの2次元配列の要素はcharの配列(上の例では、大きさ15のメモリー領域)であるのに対し、charのポインターのポインターの要素はcharのポインター(アドレスを格納するメモリー領域)だからである。 
実際に、そもそもnames_a[2][15]と、malloc(sizeof(char *) * 2)では、確保されるメモリーの大きさが異なる。
従って、names_p = (char **)names_a;は不適切である。
対して、names_p[0] = names_a[0];は、names_a[0]の値がcharの配列であるため、(charの配列をindexなしで使った時と同様)charの配列の最初のアドレスを返すことから、charのポインターであるnames_p[0]に代入できる。

より詳しい確認のため、names_pの各要素が「保持する」アドレスと、names_pの各要素「そのもの」のアドレスを見てみた。

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

int main() {
 char names_a[2][15] = {"john lennon", "paul mccartney"};
 char **names_p = malloc(sizeof(char *) * 2);
 names_p[0] = names_a[0];
 names_p[1] = names_a[1];

 /*
  * names_pの各要素が「保持する」アドレス
  */
 // 下記3つは同じアドレスを出力
 printf("%p\n", names_a);
 printf("%p\n", names_a[0]);
 printf("%p\n", names_p[0]);
 // 下記2つは同じアドレスを出力
 printf("%p\n", names_a[1]);
 printf("%p\n", names_p[1]);

 /*
   * names_pの各要素「そのもの」のアドレス
  */
 // 下記の2つは同じアドレスを出力
 printf("%p\n", names_p);
 printf("%p\n", &names_p[0]);
 // 下記は&names_p[0] + sizeof(char *)の値(筆者環境では8)のアドレスを出力
 printf("%p\n", &names_p[1]);
 // charのポインターのメモリー上の大きさ
 printf("%d\n", (int)sizeof(char *));

 free(names_p);

 return 0;
}

実行結果
0x7ffee3fb3550
0x7ffee3fb3550
0x7ffee3fb3550
0x7ffee3fb355f
0x7ffee3fb355f
0x7f80084006a0
0x7f80084006a0
0x7f80084006a8
8

names_p[0]が保持するアドレスはnames_aの最初の要素のアドレス(names_aまたはnames_a[0])、names_p[1]が保持するアドレスはnames_aの2つ目の要素のアドレス(names_p[1])と同じであることがわかる。 
また、アドレスの差は、0x7ffee3fb3550と0x7ffee3fb355fから確認できるように、文字列(charの配列)の大きさ、15であることがわかる。

names_pの各要素「そのもの」のアドレスは、&names_p[0]が0x7f80084006a0、&names_p[1]が0x7f80084006a8と、その差はcharのポインターの大きさ(sizeof(char *)の値、8)であることがわかる。
また、charのポインターのポインターであるnames_pが格納する値は、charのポインターであるnames_p[0]のアドレス(&names_p[0])と同じであることもわかる。
なお、アドレスの数値から明らかだが、文字列そのものが格納された位置とは関係のない、異なるメモリー上の場所に位置している。

もう少し言うと、0x7f80084006a0のcharのポインターが、最初の要素のアドレスが0x7ffee3fb3550のcharの配列(文字列)、0x7f80084006a8のcharのポインターが、最初の要素のアドレスが0x7ffee3fb355fのcharの配列(文字列)を参照していることになる。

メモリー上のデータのイメージは、下記のようになる。

charの二次元配列(計30 bytes)
john lennon\0   |paul mccartney\0

charのポインターのポインター(計16 bytes)
john lennonのjのアドレス(8 bytes)|paul mccartneyのpのアドレス(8 bytes)

なお、ポインターのポインターをindexで配列のように扱えるのは仕様。 
indexが1つ増えるごとに、intのポインターではintに必要なメモリー分、longのポインターではlongに必要なメモリー分、charのポインターではcharに必要なメモリー分と、ポインターが参照する型に応じて、アドレスが増える。
故に、char*のポインターのポインターでは、charのポインターに必要なメモリー(8 bytes)だけ、アドレスが増えることになる。 
indexを増やすのと同様、ポインターをインクリメントしても同じ挙動を示す。
下記は、これらの仕様を明らかにするコード。

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

int main() {
 // int, long, charそれぞれの型のポインターに対し、適当なアドレスを代入
 int _numbers = 1;
 long _numbers_l = 1;
 char _names = 'a';
 int *numbers = &_numbers;
 long *numbers_l = &_numbers_l;
 char *names = &_names;

 // それぞれの型に必要なメモリーの大きさを表示
 printf("int: %d\n", (int)sizeof(int));
 printf("long: %d\n", (int)sizeof(long));
 printf("char: %d\n", (int)sizeof(char));

 // それぞれの型について、ポインターのアドレスと、
 // ポインターに対し[1]でアクセスした場合のアドレスを比較
 printf("int: %p -> %p\n", numbers, &numbers[1]);
 printf("long: %p -> %p\n", numbers_l, &numbers_l[1]);
 printf("char: %p -> %p\n", names, &names[1]);

 // ++でも同じ結果になることを確認
 // コンパイル時に警告が出るが無視
 printf("int: %p -> %p\n", numbers, ++numbers);
 printf("long: %p -> %p\n", numbers_l, ++numbers_l);
 printf("char: %p -> %p\n", names, ++names);

 return 0;
}

実行結果
int: 4
long: 8
char: 1
int: 0x7ffeef515578 -> 0x7ffeef51557c
long: 0x7ffeef515570 -> 0x7ffeef515578
char: 0x7ffeef51556f -> 0x7ffeef515570
int: 0x7ffeef515578 -> 0x7ffeef51557c
long: 0x7ffeef515570 -> 0x7ffeef515578
char: 0x7ffeef51556f -> 0x7ffeef515570

実行結果の最初の3行で、int、long、char、それぞれの型で必要なメモリーの大きさは、4 bytes、8 bytes、1 byteであることがわかる。
4行目以降で、それぞれの型のポインターについて、indexを1つ進めたり、インクリメントしたりすると、アドレスは、intのポインターでは0x7ffeef515578 -> 0x7ffeef51557cで4 bytes、longのポインターでは0x7ffeef515570 -> 0x7ffeef515578で8 bytes、charのポインターでは0x7ffeef51556f -> 0x7ffeef515570で1 byte進んでいることがわかる。

charのポインターの配列

もう一つ似たものとして、charのポインターの配列がある。
charのポインターの配列も、文字列の配列として利用できる。
また、要素がcharのポインターであるため、charのポインターのポインターに代入しても動作する。

#include <stdio.h>

int main() {
 // charのポインターの配列
 char *names[] = {"john", "paul"};
 int size = sizeof(names) / sizeof(char *);
 for (int i = 0; i < size; i++) {
  printf("%s\n", names[i]);
 }

 // charのポインターの配列の要素はcharのポインターなので、
 // charのポインターのポインターに代入できる
 char **names_pp = names;
 for (int i = 0; i < size; i++) {
  printf("%s\n", names_pp[i]);
 }

 return 0;
}

実行結果
john
paul
john
paul

文字列の配列の動的確保

以上を理解したら、文字列の大きさや数が事前に把握できない場合に、動的に文字列の配列を作成することが容易となった。

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

int main() {
 /*
  * 下記の2、15は事前に把握できないと仮定する
  */
 char source_names[2][15] = {"john lennon", "paul mccartney"};
 int num = 2;

 /*
  * 動的な文字列の配列を作成
  */
 // charのポインターのサイズのメモリーを、文字列の数分確保する
 char **dest_names = malloc(num * sizeof(char *));
 for (int i = 0; i < num; i++) {
  // 文字列を格納するために必要な大きさのメモリーを確保し、
  // そのアドレスを動的配列の要素(charのポインター)として代入
  dest_names[i] = malloc(strlen(source_names[i]) + 1);
  // 確保したメモリーに、文字列をコピー
  strcpy(dest_names[i], source_names[i]);
 }

 /*
  * 動的に確保した文字列の配列の使用
  */
 for (int i = 0; i < num; i++) {
  printf("%s\n", dest_names[i]);
 }

 /*
  * 確保したメモリーの開放
  */
 for (int i = 0; i < 2; i++) {
  free(dest_names[i]);
 }
 free(dest_names);

 return 0;
}

コメント

このブログの人気の投稿

PowerShell 6で、Shift_JISのCSVをImport-Csvで読み込んだら文字化けした

Windowsで、特定のユーザーに特定のサービスの再起動を許可する

PowerShellでイベントログを取得する時、「指定した選択条件に一致するイベントが見つかりませんでした。」が煩わしいのでcatchする