meideru blog

家電メーカーで働いているmeideruのブログです。主に技術系・ガジェット系の話を書いています。

【C言語】配列とポインタを使って文字列を扱い方の注意点のまとめ

 

一昨日に続いて、今日もC言語の文字列について記事を書きたいと思います。

【関連記事】
C言語の文字列操作は苦手ですorz

C言語で配列とポインタで文字列を扱うときに間違いやすいことをまとめたので、この記事に書きたいと思います。

目次

配列とポインタでの文字列の扱い方の違い

サンプルプログラムを用いて説明します。

// 文字列のサンプルプログラム

#include <stdio.h>

int main(void)
{
	char str1[]  = "ABC";
	char *str2 = "ABC";

	printf("str1 = %s\n", str1);
	printf("str2 = %s\n", str2);

	return 0;
}

上のサンプルプログラムでは2つの方法を用いて、文字列を表示させています。1つは配列で表現される文字列で、2つ目はポインタ変数で表現される文字列です。出力される結果は両方同じで、ABCと表示されます。

2つの結果は同じですが、処理系によって行われる手続きは全く異なります。

 

配列で表現される文字列(char str1[] = “ABC”)は、それぞれの配列の要素に文字コードとして文字が保存されています。

具体的に説明すると、str1[0]、str1[1]、str1[2]、str1[3]にそれぞれ、’A’、’B’、’C’、’\0’が文字コードとして直接保存されています。文字コードとは、AやBなどの文字に割り当てられている、0x41や0x42といったコードのことです。詳しくはググってみてください。

末尾に保存されている\0はヌル文字と言って、文字列の終端を表すコードです。

以下の様な方法でも宣言することが可能です。

// 文字列のサンプルプログラム

#include <stdio.h>

int main(void)
{
	char str1[] = { 'A', 'B', 'C', '\0'};

	printf("str1 = %s\n", str1);
	
	return 0;
}

上のプログラムでも全く同じように動作します。

つまり、char str1[] = “ABC”と宣言するのは、char str1[] = { ‘A’, ‘B’, ‘C’, ‘\0’}と宣言するのと全く同じ意味です。

 

一方のポインタで表現される文字列(char *str2 = “ABC”)は、メモリ上に連続して存在する’A’、’B’、’C’、’\0’の先頭のアドレスをポインタ変数str2に保存しています。

“ABC”といった書き方は文字列リテラルと言い、メモリ上のどこかに連続して保存されています。これらの管理はコンパイラが適切に行なっています。

“ABC”単体では、’A’が保存されているアドレスを表します。つまり、char *str2 = “ABC”というのは、str2に’A’が保存されているアドレスを保存しています。

 

以上より、配列で表現する文字列と、ポインタで表現する文字列は全く別物なのです。

ポインタでは文字列を再定義できるが、配列ではできない

// 文字列のサンプルプログラム

#include <stdio.h>

int main(void)
{
	char str1[] = "ABC";
	char *str2 = "ABC";

	str1 = "DEF";	// エラー
	str2 = "DEF";	// 正しい

	printf("str1 = %s\n", str1);
	printf("str2 = %s\n", str2);

	return 0;
}

上のプログラムでは、文字列”ABC”を表すstr1[]と*str2を書き換えて、”DEF”にしようとしています。

しかし、ポインタでは上手くいくのに、配列では上手く行きません。この理由について説明します。

 

まず、配列で表現する文字列について。char str1[] = “ABC”宣言してからstr1 = “DEF”と書けるような構文はC言語にはありません。これが理由です。

一方のポインタで表現する文字列について。str2 = “DEF”というのは、ポインタ変数str2に保存されている’A’のアドレスを、’D’が保存されているアドレスに書き換えるだけなので、正しい構文です。

最初、char *str2 = “ABC”という宣言によって、’A’が保存されているアドレスがstr2に保存されています。次に行うstr2 = “DEF”によって、’D’が保存されているアドレスに書き換えられただけなので、これは正しいのです。

初期化されていないポインタ変数にscanfを適用することはできない

// 文字列のサンプルプログラム

#include <stdio.h>

int main(void)
{
	char str1[256];
	char *str2;

	scanf("%s", str1);  // 正しい
	scanf("%s", str2);  // エラー

	printf("str1 = %s\n", str1);
	printf("str2 = %s\n", str2);

	return 0;
}

上のプログラムは、scanfを使って文字列を保存しようと指定ます。

この場合、配列はOKでも、ポインタはエラーを起こします。理由を説明します。

 

配列で表現される文字列について。[]を付けずに単体で表されるstr1はそのアドレスを表します。よって、scanf(“%s”, str1)というのは正しい構文です。

一方のポインタで表現される文字列について。ポインタ変数はアドレスを保存する変数です。上のプログラムだと、char *str2と宣言されただけでアドレスが保存されていません。よって、scanf(“%s”, str2)としたところで、str2はどこのアドレスも指していないポインタ変数なので、無効な構文です。

 

文字列リテラルは書き換えてはいけない

// 文字列のサンプルプログラム

#include <stdio.h>

int main(void)
{
	char *str2 = "ABC";

	scanf("%s", str2);	// やってはいけない構文

	printf("str1 = %s\n", str2);

	return 0;
}

さっき説明したサンプルプログラムでは、ポインタ変数str2にアドレスが保存されていませんでした。上のプログラムだと、char *str2 = “ABC”と宣言されていて、’A’が保存されているメモリの先頭のアドレスが保存されています。次の文scanf(“%s”, str2)によって、これから読み取る文字列でstr2が指すアドレスの内容を書き換えようとしています。

これはOKのように見えますが、絶対にやっては構文です。理由を端的に説明すると、メモリ上の文字列リテラルをプログラマが書き換えることは許されていない行為なのです。

“ABC”のような文字列リテラルは、メモリ上のどこかに適切にコンパイラが保存していると言いました。ポインタ変数str2の宣言と初期化によって、str2は文字列リテラル”ABC”の先頭の文字’A’のアドレスが保存されています。

次のscanf(“%s”, str2)という構文は、str2が指す文字列”ABC”を、これから読み取る文字列で書き換えることを意味しています。

文字列リテラルを書き換える行為はやってはいけない行為です。

だからエラーを起こします。

初期化された配列にscanfを使うときは注意

// 文字列のサンプルプログラム

#include <stdio.h>

int main(void)
{
	char str[] = "ABC";
	
	printf("str = %s\n", str);  // エラーを起こすかも

	printf("str:");
	scanf("%s", str);

	printf("str = %s\n", str);

	return 0;
}

配列strの宣言と初期化が同時に行われています。それに、新たにscanfで文字をstrに保存しようとしているプログラムです。

これは注意が必要です。

char str[] = “ABC”という宣言では、strはstr[0]、str[1]、str[2]の3つの要素しか作れません。

つまり、strには3バイトしか保存できないということです。これ以上の大きさをscanfで読み取ってしまうとエラーになります。

 

以上です。

私も勉強したばっかりなので、わからないことも沢山あります。何か間違いがあったら教えてください(^_^;)

また、なにか質問があれば、容赦なくコメント欄にください。大歓迎です(^O^)/

 - 技術系