質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.47%
C

C言語は、1972年にAT&Tベル研究所の、デニス・リッチーが主体となって作成したプログラミング言語です。 B言語の後継言語として開発されたことからC言語と命名。そのため、表記法などはB言語やALGOLに近いとされています。 Cの拡張版であるC++言語とともに、現在世界中でもっとも普及されているプログラミング言語です。

Q&A

解決済

5回答

2871閲覧

C言語の二次元配列で二重ポインタを使ってもうまくいきません。

Cpro

総合スコア15

C

C言語は、1972年にAT&Tベル研究所の、デニス・リッチーが主体となって作成したプログラミング言語です。 B言語の後継言語として開発されたことからC言語と命名。そのため、表記法などはB言語やALGOLに近いとされています。 Cの拡張版であるC++言語とともに、現在世界中でもっとも普及されているプログラミング言語です。

0グッド

0クリップ

投稿2019/06/26 13:49

編集2019/06/26 23:19

前提・実現したいこと

C言語で年内の経過日数を求めるプログラムを作っています。
二次元配列のことで上手くコンパイラできないので質問をさせていただきます。

発生している問題・エラーメッセージ

例外がスローされました:読み取りアクセス違反。 days が 0x1110131 でした。

該当のソースコード

c

1 2 3//年を入力させて、○月○日は年内で何日経過しているかを導いてくれるプログラムを作成する 4 5#include<stdio.h> 6 7//使う関数の定義 8int which_year(int y); 9int day_of_month(int which, int month, int day, int **days); 10 11//うるう年か平年かを示す関数 12int which_year(int y) 13{ 14 return y % 4 == 0 && y % 100 != 0 || y % 400 == 0; 15} 16 17 18//一か月の日にちの配列を組み立てる関数 19int day_of_month(int which, int month,int day,int **days) 20{ 21 int i; 22 int sumday = 0; 23 24 25 for (i = 0; i < month - 1; i++) 26 sumday += days[which][i]; //←←ここで「例外がスローされました」と表示されて失敗 27 28 return sumday+day; //日数を返す 29 30} 31 32 33//main関数 34int main(void) 35{ 36 //days[0][12]:平年 days[1][12]:うるう年 37 int days[2][12] = { { 31,28,31,30,31,30,31,31,30,31,30,31 },{ 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 } }; 38 39 int year; 40 int month; 41 int day; 42 int pathday; 43 int which; //うるう年:1 平年:0を返す 44 int remainday; 45 46 //年月日を入力させる 47 printf("年:"); scanf_s("%d", &year); 48 printf("月:"); scanf_s("%d", &month); 49 printf("日:"); scanf_s("%d", &day); 50 51 //平年(0) or うるう年(1) 52 which = which_year(year); 53 54 55 //年内で何日目?? 56 pathday = day_of_month(which, month, day,**days); //実際に年内で経過した日数 57 printf("年内で%d日目です。\n", pathday); 58 59 60 return 0; 61 62} 63

試したこと

day_of_month関数の
int day_of_month(int which, int month,int day,int **days) を
int day_of_month(int which, int month,int day,int days[2][12]) に、

main関数の
pathday = day_of_month(which, month, day,**days); を、
pathday = day_of_month(which, month, day,days);  に変更したら成功します。

ですが、なぜ**daysを使うと上手くいかないのかが納得できません。
どうしてなのか、どなたか説明していただけないでしょうか?

###最後に

この度初めて質問させていただきます。
C言語を学び始めて2か月ほどの初心者で拙い部分もあるかと思います。
「ココの部分をもっとこうしたほうが良い」といった質問以外の所で訂正の箇所がありましたら併わせてご指摘いただけますと大変嬉しく思います。

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

退会済みユーザー

退会済みユーザー

2019/06/26 22:03

C, C++ の話で C# は関係ないのでは? であれば C# のタグははずしてください。
Cpro

2019/06/26 23:21

訂正しました。 ご指摘ありがとうございます。
guest

回答5

0

私なら関数(及びプロトタイプ宣言も)を、こう書きます。

C

1int day_of_month(int which, int month, int day, 2 int arr[][12]) // ←ここ 3{ 4 ... 5 for (i = 0; i < month - 1; i++) 6 sumday += arr[which][i];

このスタイルは間違いや迷いが少ないと思います(が、説明が要るかもしれないな)。
なお、普通は仮引数の変数名は days で問題ありませんが、仮引数と実引数を区別するために、仮引数の変数名を「arr」に書き換えました。

関数呼び出しはこうです。

C

1 pathday = day_of_month(which, month, day, days);

以上、質問者の書き方と見比べてください。

さて、わかってしまえば簡単なことだと思って説明を書き始めたものの、簡潔な説明が難しくて挫けてしまったので(苦笑)、要点を示します。いくつか問題が重なっていると思いました。

  • 二次元配列とダブルポインタは違うものです
  • 仮引数と実引数が区別できているか
  • 特に、アドレス(ポインタ)を引数で渡す際の注意

まず、二次元配列とダブルポインタを混同しないこと。
そのためには、コード上の字面で理解しようとするのではなく、メモリ上の配置を図に描いて理解することをお勧めします(後半に図を追加しました)。
逆にお聞きしたいのだけど、どこに「二次元配列とダブルポインタは同じ」と書いてありましたか?

次に、関数の引数には仮引数と実引数があります。ご存じでしたか?

  • 関数定義側とプロトタイプ宣言に書くのが仮引数
  • 関数を呼び出す側に書くのが実引数

この2つは同じもののようでいながら(だから誤解するのだけど)、しばしば表記の恰好は明確に異なる形をとらなければなりません。今回のように、配列やポインタが絡む場合がそうです。私の例では、実引数が「days」なのに対し、仮引数は「arr[][12]」と、格好が違います。

この背景には、変数定義と、実行時の処理では、文法が違うことがあります。仮引数は変数定義の一つであり、実引数を渡す箇所は実行時の処理です。

ここでもう一つ確認すべき事は、関数呼び出しの際、実引数は仮引数に代入されて関数に値が渡ることです。私の書き方なら、さしずめ
arr = days; という代入が行われるという事。
days は二次元配列の先頭アドレスです。配列の先頭アドレスが関数に渡るから、sumday += arr[which][i]; という計算ができるのです。

質問者のコードは、関数定義も、呼出しも、

int day_of_month(..., int **days); // 関数定義 pathday = day_of_month(..., **days); // 関数呼出し

と、どちらも同じ格好の **days なのがオカシイのです(この書き方自体が誤りですが)。質問者の呼び出し方だとさしずめ
arr = **days; のような代入になるでしょう。arr には常に days[0][0] の値が代入されます。つまり arr = 31; という代入です。それは質問者が期待した動作でしょうか?この辺り、もう少し簡単な一次元配列で確認してみると良いと思います(この段落、少し修正しました)。

とりあえず、そんな事です。


メモリ上の配置を図に描いて理解するについて、拙い図を描きました。
二次元配列として。同じものを、2つの描き方をしました。

  • (A)二次元配列のイメージ
  • (B)実際のメモリ上の配置

メモリ上では days[0][11] の次が days[1][0] である=連続していることを示しています。
次に、関数の引数として int **dptr がある場合、

  • (C)配置の基本

仮引数としてダブルポインタ dptr があるということは、dptr が指すメモリにもうひとつのポインタ変数が存在し、そのポインタ変数が int 型の値をポイントしている、これが基本の形です。ダブルポインタがあるということは、このような構造があることを意味します。

実際は、この基本形だけではなく、

  • ポインタが配列になっている
  • int 型のデータが配列になっている

場合もあります。なぜなら、ポインタが指す場所のメモリは連続しているので、配列とみなせるからです。そんな場合を示したのが次の図です。

  • (D)配列をポイントする場合

この場合、図の右側に { 31, 28, ... 31 } と、{ 31, 29, ... 31 } という2つの配列が描かれています(描いたつもりですw)が、この2つの配列はメモリ上で連続している保障がありません。二次元配列では days[0][11] の次が days[1][0] だったような保障は無い、ということ。

以上、配列として確保したメモリは(A)もしくは(B)の姿をしていて、そのなかにはポインタとなっているメモリは存在しないということ、
一方、ダブルポインタの場合は、ダブルポインタが指すメモリにもうひとつのポインタ変数(もしくはポインタ変数の配列)が存在していますので、メモリ上の姿は二次元配列と異なっている、よって同じものではないのです。この違いを理解しないと何時までも疑問が解消しないと思います。

メモリ配置の様子

投稿2019/06/27 06:27

編集2019/06/28 08:32
rubato6809

総合スコア1380

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

Cpro

2019/06/27 13:41

回答ありがとうございました。 一つ質問なのですが、関数呼び出しの時にary[][12]を定義すると同時にary=daysの代入が行われるという解釈でよろしいのでしょうか?
rubato6809

2019/06/27 13:54 編集

day_of_month() の仮引数を int ary[][12] と定義した場合、 day_of_month(which, month, day, days); という関数呼出し時に ary = days という代入が行われる、です。 念のため補足:上記、仮引数の ary はポインタ変数です。
Cpro

2019/06/28 12:18

そういうことなのですね!とても参考になりました。 わざわざ図まで作っていただき本当にありがとうございました。
guest

0

これ↓ならイケるんちゃう?

C

1//年を入力させて、○月○日は年内で何日経過しているかを導いてくれるプログラムを作成する 2 3#include<stdio.h> 4 5//使う関数の定義 6int which_year(int y); 7int day_of_month(int which, int month, int day, int **days); 8 9//うるう年か平年かを示す関数 10int which_year(int y) 11{ 12 return y % 4 == 0 && y % 100 != 0 || y % 400 == 0; 13} 14 15 16//一か月の日にちの配列を組み立てる関数 17int day_of_month(int which, int month,int day,int **days) 18{ 19 int i; 20 int sumday = 0; 21 22 23 for (i = 0; i < month - 1; i++) 24 sumday += days[which][i]; //←←ここで「例外がスローされました」と表示されて失敗 25 26 return sumday+day; //日数を返す 27 28} 29 30 31//main関数 32int main(void) 33{ 34 //days[0][12]:平年 days[1][12]:うるう年 35 int normal[12] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; 36 int leap[12] = { 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; 37 int* days[2] = { normal, leap }; 38 39 int year; 40 int month; 41 int day; 42 int pathday; 43 int which; //うるう年:1 平年:0を返す 44 int remainday; 45 46 //年月日を入力させる 47 printf("年:"); scanf_s("%d", &year); 48 printf("月:"); scanf_s("%d", &month); 49 printf("日:"); scanf_s("%d", &day); 50 51 //平年(0) or うるう年(1) 52 which = which_year(year); 53 54 55 //年内で何日目?? 56 pathday = day_of_month(which, month, day, days); //実際に年内で経過した日数 57 printf("年内で%d日目です。\n", pathday); 58 59 60 return 0; 61 62}

投稿2019/06/26 23:21

episteme

総合スコア16614

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

0

C

1 int d[2][12] = { 2 {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}, 3 {31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31} 4 }; 5 int *days[2] = { d[0], d[1] }; 6 ... 7 pathday = day_of_month(which, month, day, days);

このようにすると、day_of_month の宣言の第4引数は int **day のままでよくなります。
d の型は int [2][12]。
d[0] の型は int [12] だけど、式の中では int * に変換されます。

投稿2019/06/26 23:16

kazuma-s

総合スコア8224

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

0

Cはポインタの型の検査がいい加減なのでコンパイルを通っちゃいますが、より厳しい検査をするC++だとそのプログラムはコンパイルを通りません。

二次元配列とダブルポインタは全く別の構造になります。ダブルポインタでも二次元のデータ構造を表すことはできますが、メモリ配置などが異なります。混同してはいけません。
int days[2][12];
では、データがメモリ上に
days[0][0],days[0][1],days[0][2],...,days[0][11],days[1][0],days[1][1],days[1][2],...,days[1][11]
が密にならんで確保されます。
一方、
int **pday; //混乱しないように名前変えます
は、
intへのポインタへのポインタpdayですから、多分
intへのポインタが配列になっているならば
pday[0],pday[1],pday[2],...
が連続して配置され
pday[0]で指されたアドレス(pとしましょうか)に
p[0], p[1], p[2],...
が並んでいます。が、pday[0][11]とpday[1][0]が並んでいる保証はありません(たまたま並んでいることはあるかも知れません。あるいは並ぶように意図して並べることはできます)。

int days[2][12];
を引数で受け取りたいなら、
void func( int (*days)[12]);
とでもして下さい。int(*)[12]は、[intが12要素ある配列]へのポインタです。

[追記]
で、関数の呼び出し方法は
func(days);
となります。この場合には*やら&やら[]は不要です。

ついでに

pathday = day_of_month(which, month, day,days);  に変更したら成功します。

具体的にいくつかの日付を入力してみて確認してみましょう。本当に成功していますか?

投稿2019/06/26 15:37

編集2019/06/27 02:47
thkana

総合スコア7652

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

0

ベストアンサー

pathday = day_of_month(which, month, day,**days);

この場合、**daysはdays[0][0]と等価になりますので、31という値を引数に渡していることになります。
対して、関数day_of_monthのdaysにはポインタのポインタを渡すと定義されています。
そうすると、渡された31という値がアドレスとして扱われてしまうので不正な領域にアクセスしようとしたことになり例外がスローされます。

pathday = day_of_month(which, month, day,days);

この場合、days[0][0]の先頭アドレスが引数に渡されることになりますので期待した動作になっています。

変数とポインタの関係の例をコードに示します。
参考になれば幸いです。

C

1#include <stdio.h> 2 3int main() 4{ 5 int x = 123456; 6 int *xp; 7 int **xpp; 8 9 xp = &x; // ポインタ変数xpに変数xのアドレスを代入する 10 xpp = &xp; // ポインタポインタ変数xppにポインタ変数xpのアドレスを代入する 11 12 printf("x = %d\n", x ); // x 13 printf("xp = %d\n", *xp ); // xp ⇒ x 14 printf("xpp = %d\n", **xpp ); // xpp ⇒ xp ⇒ x 15 16 return 0; 17}

実行結果
x = 123456
xp = 123456
xpp = 123456

投稿2019/06/26 14:28

TaroToyotomi

総合スコア1430

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

thkana

2019/06/26 15:37

> **daysはdays[0][0]と等価になりますので なりません。
kazuma-s

2019/06/26 22:59

添字演算子[] の定義により、a[i] = *(a + i) だから、 days[0][0] = *(days[0] + 0) = *(days[0]) = *(*(days + 0)) = *(*(days)) = **days となります。
bsdfan

2019/06/27 01:20 編集

int **p; /* pがポインタのポインタ */ ならば **p と p[0][0] は等価ですが(ちゃんとした値がはいっている前提)、 int a[2][12]; /* aは2次元配列 */ ならば **a と a[0][0] は等価ではないです。 *aは配列であってアドレスではないため、**aは使えません。 -- 追記 上のように思ってましたが、いま試してみたらwarningもなく使えました。同じになりました。 思い違いをしていたようです。
thkana

2019/06/27 02:45

あぁ、確かに。アクセス違反発生のメカニズムはTaroToyotomiさんの回答の通りで、 int days[2][12];において days[0][0]と**daysは等価になります。 また、 int **days;において days[0][0]と**daysは等価になります。 (int [2][12]型のdaysとint**型のdaysで混乱しました。失礼しました)
Cpro

2019/06/27 13:27

とても参考になりました!本当にありがとうございました。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.47%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問