配列について

フォーラム(掲示板)ルール
フォーラム(掲示板)ルールはこちら  ※コードを貼り付ける場合は [code][/code] で囲って下さい。詳しくはこちら
moku

配列について

#1

投稿記事 by moku » 7年前

C言語の2次元配列について質問です。
以下のmainでは初期化をm[0][0]からm[2][2]まで行っています。
しかし、初期化を行っていないはずのm[0][3]、m[1][4]にも-1が入っているようです。
何が原因でしょうか?

コード:

#include <stdio.h>
int main(){
  int m[3][3];

  for(int i=0; i<3; i++){
    for(int j=0; j<3; j++){
      m[i][j] = -1;
    }
  }

  printf("%d\n",m[0][3]);
  printf("%d\n",m[1][3]);
  printf("%d\n",m[1][4]);

  return 0;
}

アバター
みけCAT
記事: 6734
登録日時: 13年前
住所: 千葉県
連絡を取る:

Re: 配列について

#2

投稿記事 by みけCAT » 7年前

m[0][2]の後に続けてm[1][0], m[1][1], ... と配置する実装であり、たまたま初期化した場所を読み込んだからでしょう。
確保した配列の範囲外の読み書きをすると未定義動作となり、何が起こってもおかしくなくなるので、やってはいけません。

N1570 J.2 Undefined behaviorより引用
1 The behavior is undefined in the following circumstances:

(中略)

- An array subscript is out of range, even if an object is apparently accessible with the
given subscript (as in the lvalue expression a[1][7] given the declaration int
a[4][5]) (6.5.6).
複雑な問題?マシンの性能を上げてOpenMPで殴ればいい!(死亡フラグ)

かずま

Re: 配列について

#3

投稿記事 by かずま » 7年前

コード:

#include <stdio.h>

int main()
{
    int a[3][3], k = 0, m = -3, n = -6;

    for (int i = 0; i < 3; i++)
        for (int j = 0; j < 3; j++)
            a[i][j] = 900 + i * 10 + j;

    for (int i = 0; i < 3; i++)
        for (int j = 0; j < 3; j++) {
            printf(" a[%d][%d]:%d  a[0][%d]:%d  a[1][%2d]:%d  a[2][%2d]:%d\n",
              i, j, a[i][j], k, a[0][k], m, a[1][m], n, a[2][n]);
            k++, m++, n++;
        }

    return 0;
}
実行結果

コード:

 a[0][0]:900  a[0][0]:900  a[1][-3]:900  a[2][-6]:900
 a[0][1]:901  a[0][1]:901  a[1][-2]:901  a[2][-5]:901
 a[0][2]:902  a[0][2]:902  a[1][-1]:902  a[2][-4]:902
 a[1][0]:910  a[0][3]:910  a[1][ 0]:910  a[2][-3]:910
 a[1][1]:911  a[0][4]:911  a[1][ 1]:911  a[2][-2]:911
 a[1][2]:912  a[0][5]:912  a[1][ 2]:912  a[2][-1]:912
 a[2][0]:920  a[0][6]:920  a[1][ 3]:920  a[2][ 0]:920
 a[2][1]:921  a[0][7]:921  a[1][ 4]:921  a[2][ 1]:921
 a[2][2]:922  a[0][8]:922  a[1][ 5]:922  a[2][ 2]:922
やってはいけないと言われても、やれば通常はこうなります。
ならない処理系があったら、教えてほしい。

TKS

Re: 配列について

#4

投稿記事 by TKS » 7年前

みけCAT さんが書きました:m[0][2]の後に続けてm[1][0], m[1][1], ... と配置する実装であり、たまたま初期化した場所を読み込んだからでしょう。
確保した配列の範囲外の読み書きをすると未定義動作となり、何が起こってもおかしくなくなるので、やってはいけません。

N1570 J.2 Undefined behaviorより引用
1 The behavior is undefined in the following circumstances:

(中略)

- An array subscript is out of range, even if an object is apparently accessible with the
given subscript (as in the lvalue expression a[1][7] given the declaration int
a[4][5]) (6.5.6).
横からの質問になってしまい、申し訳ありません。
二次元配列として確保した場合、連続する領域に確保される事は保証された動作だと思っていたのですが、そうならない場合はあるのでしょうか?

Math

Re: 配列について

#5

投稿記事 by Math » 7年前

>C言語の2次元配列について質問です。
>以下のmainでは初期化をm[0][0]からm[2][2]まで行っています。
>しかし、初期化を行っていないはずのm[0][3]、m[1][4]にも-1が入っているようです。
>何が原因でしょうか?

C言語は”プログラマーは全知全能である”という理念のもとに設計されています。

UNIXはアセンブラーで開発されていましたがUNIXの作者Ken Tompsonはアセンブラーが嫌になって”B"という言語を開発しました。Bは遅すぎたため使われませんでした。1971年Tompsonの同僚Dennis Ritchie は"NB"(New B)を作りました。これがのち”C"と呼ばれるようになりました。
Cはその後UNIXのニーズに合わせかなり行き当たりばったりに機能拡張を繰り返して来ました。
・現場の人間が目前の問題を解決するために作成した言語であり
・非常に便利ではあるけれども
・見た目あちこち不格好で
・よくわかっていない人がうっかり使うと危険な目に合ったりする
つまり”自分の必要に応じて作成したので”実用性は高いが人間工学には問題がありです。コンパイラの警告レベルはなるべく上げることです。

>確保した配列の範囲外の読み書きをすると未定義動作となり、何が起こってもおかしくなくなるので、やってはいけません。
このように、こういう事は論外でやってはいけません。

参考
2次元の配列の初期化

コード:

#include <stdio.h>
int main() {
	int m[][3] = {
		{ -1,-2,-3 },
		{ -4,-5,-6 },
		{ -7,-8,-9 },
	};

	for (int i = 0; i<3; i++) {
		for (int j = 0; j<3; j++) {
			printf("%d\n", m[i][j]);
		}
		printf("\n");
	}

	return 0;
}

コード:

1>------ すべてのリビルド開始: プロジェクト:ConsoleApplication6, 構成: Debug Win32 ------
1>c1.c
1>ConsoleApplication6.vcxproj -> D:\z17\c\ConsoleApplication6\Debug\ConsoleApplication6.exe
1>ConsoleApplication6.vcxproj -> D:\z17\c\ConsoleApplication6\Debug\ConsoleApplication6.pdb (Partial PDB)
========== すべてリビルド: 1 正常終了、0 失敗、0 スキップ ==========

コード:

-1
-2
-3

-4
-5
-6

-7
-8
-9

続行するには何かキーを押してください . . .
[/size]

Math

Re: 配列について

#6

投稿記事 by Math » 7年前


>やってはいけないと言われても、やれば通常はこうなります。
>ならない処理系があったら、教えてほしい。

[]はシンタックスシュガーなので a[1][-3]:900 a[2][-6]:900 は a[0][0]:900 であり 正しい使い方ですよ。
http://dixq.net/forum/viewtopic.php?f=3&t=19227参照

Math

Re: 配列について

#7

投稿記事 by Math » 7年前

>横からの質問になってしまい、申し訳ありません。
>二次元配列として確保した場合、連続する領域に確保される事は保証された動作だと思っていたのですが、
>そうならない場合はあるのでしょうか?
Cの規格は言語仕様を規定するものであり実装方法について規定するものではありません。
いまのPCのOSなら仮想アドレスを実現しているとおもいますがそのうえで普通はそうなるでしょう(^^;

moku

Re: 配列について

#8

投稿記事 by moku » 7年前

回答ありがとうございます。
ミケCAT さんが書きました:m[0][2]の後に続けてm[1][0], m[1][1], ... と配置する実装であり、たまたま初期化した場所を読み込んだからでしょう。
確保した配列の範囲外の読み書きをすると未定義動作となり、何が起こってもおかしくなくなるので、やってはいけません。
つまり、m[0][0]からm[2][2]まで初期化しましたが、範囲外もたまたま初期化されたということでしょうか?

かずま

Re: 配列について

#9

投稿記事 by かずま » 7年前

コード:

#include <stdio.h>
 
int main(void)
{
    int m[3][3];
 
    for (int i = 0; i < 3; i++)
        for (int j = 0; j < 3; j++)
            m[i][j] = 900 + i * 10 + j;
    for (int i = 0; i < 3; i++)
        for (int j = 0; j < 3; j++)
            printf(" %p: m[%d][%d]=%d\n", &m[i][j], i, j, m[i][j]);
	printf("\n");
	printf(" %p: m[0][3]=%d\n", &m[0][3], m[0][3]);
	printf(" %p: m[1][3]=%d\n", &m[1][3], m[1][3]);
	printf(" %p: m[1][4]=%d\n", &m[1][4], m[1][4]);
    return 0;
}
実行結果

コード:

 00FFF984: m[0][0]=900
 00FFF988: m[0][1]=901
 00FFF98C: m[0][2]=902
 00FFF990: m[1][0]=910
 00FFF994: m[1][1]=911
 00FFF998: m[1][2]=912
 00FFF99C: m[2][0]=920
 00FFF9A0: m[2][1]=921
 00FFF9A4: m[2][2]=922

 00FFF990: m[0][3]=910
 00FFF99C: m[1][3]=920
 00FFF9A0: m[1][4]=921
m[0][3] は m[1][0] と同じ場所だということ。
m[1][3] は m[2][0] と同じ場所だということ。
m[1][4] は m[2][1] と同じ場所だということ。

m[0][-1] や m[2][3] ならメモリが割り付けれていないから、
他の変数を壊したり、関数からのリターンアドレスなどを
壊したりして、わけの分からない動作をするということです。

ISLe
記事: 2650
登録日時: 13年前
連絡を取る:

Re: 配列について

#10

投稿記事 by ISLe » 7年前

C99より前は、構造体の最後にサイズ1の配列を配置して、バイナリのヘッダ構造に続くデータを読み書きする方法を慣例として認める記述がありましたが、規格としては特に規定はありませんでした。

C99では、構造体の最後のメンバに限り、サイズ指定のない配列の宣言が規格として取り入れられました。

それに合わせて、配列に対しては、厳格となりました。

みけCATさんの指摘は、よく見てもらったら分かると思いますが、6.5.6に対する注釈であり、aに対しては未定義動作となるという記述ですが、pに対しては言及されていません。

ISLe
記事: 2650
登録日時: 13年前
連絡を取る:

Re: 配列について

#11

投稿記事 by ISLe » 7年前

オフトピック
No.1のコードは下記のようにするとC99でも(言及がないという意味で)許容範囲になるのかな。

コード:

#include <stdio.h>
int main(){
  int m[3][3];
  int (*p)[3] = m;
 
  for(int i=0; i<3; i++){
    for(int j=0; j<3; j++){
      p[i][j] = -1;
    }
  }
 
  printf("%d\n",p[0][3]);
  printf("%d\n",p[1][3]);
  printf("%d\n",p[1][4]);
 
  return 0;
}
配列名を使って配列にアクセスする部分に対して、未定義の動作とするのは、処理系が負う責任の中身を明確にする意味があるのではないだろうか。

Math

Re: 配列について

#12

投稿記事 by Math » 6年前

”式の中では配列はポインターに読み替えられる”ので
void f( int hoge[3][2] );とか
void f( int hoge[][2] ); は

void f( int (*hoge)[2] ); のシンタックスシュガーでありまったく同じ意味になります。

[参考]
(配列に関する演算子:
後置演算子[]は添字演算子と呼び ポインターと整数をオペランドとして取ります。
p は *(p + i) のシンタックスシュガーでありそれ以外の意味はありません。)
とのことです。

Math

Re: 配列について

#13

投稿記事 by Math » 6年前

Cには実際には多次元配列は存在しないのであって多次元配列のように見えるものは「配列の配列」です。
型Tの配列を引数としてわたすには「Tヘのポインター」を渡せば良いので
「配列の配列」を引数としてわたすには「配列ヘのポインター」を渡せということになります。

コード:

#include <stdio.h>

void f(int(*m)[3])
{
	int i, j;
	for (i = 0; i < 3; i++) {
		for (j = 0; j < 3; j++) {
			printf("%d\n", m[i][j]);
		}
		printf("\n");
	}
}

int main() {
	int m[][3] = {
		{ -1,-2,-3 },
		{ -4,-5,-6 },
		{ -7,-8,-9 },
	};

	f(m);

	return 0;
}

コード:

1>------ すべてのリビルド開始: プロジェクト:ConsoleApplication9, 構成: Debug Win32 ------
1>c1.c
1>ConsoleApplication9.vcxproj -> D:\z17\c\ConsoleApplication9\Debug\ConsoleApplication9.exe
1>ConsoleApplication9.vcxproj -> D:\z17\c\ConsoleApplication9\Debug\ConsoleApplication9.pdb (Partial PDB)
========== すべてリビルド: 1 正常終了、0 失敗、0 スキップ ==========
[code]
-1
-2
-3

-4
-5
-6

-7
-8
-9

続行するには何かキーを押してください . . .
[/size]

moku

Re: 配列について

#14

投稿記事 by moku » 6年前

理解できた気がします。
ありがとうございました!

返信

“C言語何でも質問掲示板” へ戻る