仕様?

アバター
BEAT
記事: 4
登録日時: 14年前
住所: 兵庫県S市杜王町
連絡を取る:

仕様?

投稿記事 by BEAT » 13年前

先日C言語の授業でこういうプログラムの存在を知りました。

CODE:

	char *p[3] = {"car","bus","ship"};
こう記述するとポインタ配列の各要素には各文字列の先頭アドレスが入り。
メモリを隙間なく使用することができますよね?

ちょっとした疑問を持ってこういうプログラムを作ってみました。

CODE:

#include 
main()
{
	int i;
	char *p[3] = {"car","bus","ship"};

	for(i = 0; i < 3; i++){
	printf("%d\n",p[i]);//先頭アドレスの表示
	printf("%s\n",p[i]);//文字列の表示
        }	
}
実行結果は
-------------------
18657284
car
18657292
bus
18657304
ship
-------------------
となりました。

これおかしくないですかね?carとbusのメモリの差は
carと¥0で4バイトとなるはずですよね?なのになぜか8バイト
busをshipの差も4バイトのはずが12バイト・・・
試しに

CODE:

	for(i = 0; i < 24; i++){
	printf("%c\n",*p[0]++);//1文字表示して改行
        }	
というプログラムを実行して見ると
-------------------------
c
a
r

b
u
s

b
u
s

s
h
i
p




s
h
i
p
-------------------------
となりました。

どういうことなんでしょうね。
p[0]とp[1]の差が8バイトなのは、なぜかp[0]内に「bus¥0」入ってるからでしょうね。
後ろの「ship¥0¥0¥0¥0」も謎です。

もしかしてポインタの仕様か何かなんでしょうか?

アバター
Dixq (管理人)
管理人
記事: 1662
登録日時: 15年前

Re: 仕様?

投稿記事 by Dixq (管理人) » 13年前

それは二次元配列とポインタ配列を混合していることから生じる疑問ですね。

char *p[3] = {"car","bus","ship"};

こう書いた時

> メモリを隙間なく使用することができますよね?

は間違いで、これは文字列の先頭アドレスを3つ持っているに過ぎません。
"car" と書けば、メモリの書き込み禁止領域のある所に4バイト領域が確保されます。
"bus" と書けば、メモリの書き込み禁止領域のある所に4バイト領域が確保されます。
この2つは連続しているとは限りません。メモリアロケータが都合が良いと思った所に配置されます。
どこか分からない場所に確保されたアドレスを、持っただけですね。

但し、
char [2][4];
であれば、全てのアドレスは連続しています。
また、ヒープ領域に領域が確保されるので、代入後も、変数の中身を書きかえることが出来ます。

for(i = 0; i < 24; i++)
はエラー落ちしてもおかしくないコードで、0だったのはたまたまでしょう。
連続した領域を使いたければ二次元配列にするか、大きな一次元配列を作って自分で切り刻んで管理するかでしょうか。

ISLe
記事: 2650
登録日時: 15年前

Re: 仕様?

投稿記事 by ISLe » 13年前

面白いので調べてみました。
Visual C++は文字列リテラルをオブジェクト内に4バイトアラインメントで二つずつ持つ仕様らしいです。

char a[3][10] = {"car","bus","ship"};
というコードで1番目のリテラルが初期化に使われます。
char *p[3] = {"car","bus","ship"};
というコードで2番目のリテラルが初期化に使われます。

2つ必要なのは理由があります。考えてみてください。

どちらかの使い方しかしていなかったらもう一方は無駄ですけどね。

どっちにしても初期化用のリテラルは4バイト境界に調整されるのでメモリを隙間なく使用することはできませんね。

ちなみにgccだと配列を初期化するリテラルはインラインで埋め込まれてました。
最後に編集したユーザー ISLe on 2011年10月26日(水) 02:31 [ 編集 2 回目 ]

アバター
BEAT
記事: 4
登録日時: 14年前
住所: 兵庫県S市杜王町
連絡を取る:

Re: 仕様?

投稿記事 by BEAT » 13年前

これは文字列の先頭アドレスを3つ持っているに過ぎません。
>この2つは連続しているとは限りません。メモリアロケータが都合が良いと思った所に配置されます。
>どこか分からない場所に確保されたアドレスを、持っただけですね。

そうだったんですか。中身の書き換えも行うなら2次元配列を使うほうがいいですね。

Visual C++は文字列リテラルをオブジェクト内に4バイトアラインメントで二つずつ持つ仕様らしいです。
なるほど、だから8バイト差や12バイト差が生まれたわけですね。

>2つ必要なのは理由があります。考えてみてください。
考えてみましたが、わからなかったです。気になるのでヒントをもらえませか?

ISLe
記事: 2650
登録日時: 15年前

Re: 仕様?

投稿記事 by ISLe » 13年前

BEAT さんが書きました:>2つ必要なのは理由があります。考えてみてください。
考えてみましたが、わからなかったです。気になるのでヒントをもらえませか?
char a[3][10] = {"car","bus","ship"};
こちらは配列の初期値として文字列リテラルが使われています。
一時変数は宣言が現れるたびにメモリ領域が確保され初期化されます。

char *p[3] = {"car","bus","ship"};
こちらは文字列リテラルの先頭アドレスが初期値です。
本来、文字列リテラルはconst char[]で書き換えることができないのですが、ウィンドウズでは過去の悪習を引きずっているため書き換えることができてしまいます。

aとpのそれぞれからアクセスできる文字列を書き換えた場合、結果、というか影響範囲、が異なります。
ということで、この2つは明確に使い分ける必要があります。

ヒントというかほとんど解答ですね。
文字列を書き換えた場合の影響範囲を考えてみてください。

アバター
BEAT
記事: 4
登録日時: 14年前
住所: 兵庫県S市杜王町
連絡を取る:

Re: 仕様?

投稿記事 by BEAT » 13年前

aは10文字以上の文字列は入らないけど
pは10文字以上の文字列まで入ってしまう可能性があるということでしょうか?

ISLe
記事: 2650
登録日時: 15年前

Re: 仕様?

投稿記事 by ISLe » 13年前

このコードを動かしてみると違いが出ます。

CODE:

#include 
void foo(void)
{
	char a[3][10] = {"car","bus","ship"};
	char *p[3] = {"car","bus","ship"};
	printf("%s\n",a[0]);
	printf("%s\n",p[0]);
	a[0][1]='x';
	p[0][1]='x';
}
int main(void)
{
	foo();
	foo();
	return 0;
}
(追記1)
処理系依存なので必ずそうなるとは限らないですけど。
(追記2)
最適化オプションによって結果が変わりました(VC++2010で確認)。
最後に編集したユーザー ISLe on 2011年10月26日(水) 19:05 [ 編集 2 回目 ]

アバター
BEAT
記事: 4
登録日時: 14年前
住所: 兵庫県S市杜王町
連絡を取る:

Re: 仕様?

投稿記事 by BEAT » 13年前

なるほど、中身が描き変わったままになってしまっていますね