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

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

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

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

Q&A

解決済

3回答

1463閲覧

n個の要素へのポインタの有効範囲について

m0m0

総合スコア14

C

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

0グッド

3クリップ

投稿2019/08/02 13:29

【参考書】新・明解C言語 入門編
【項目】関節演算子と添え字演算 (p277)

C言語の勉強をしておりますが、学習を進めるにあたって「n個の要素へのポインタの有効範囲について」分からないことがあります。
上記の参考書に以下の説明がありました。


(※新・明解C言語 入門編 p277の説明より抜粋)

配列aの要素数がnであれば、配列aを構成する要素はa[0]からa[n-1]までの"n個"です。
ところが
①要素へのポインタとしては&a[0]からa[n]までのn+1個が正しい値として有効という規則があります。

たとえば、配列aはa[0]からa[4]までの5個の要素で構成されるのですが、各要素のポインタ&a[0]...&a[4]に加えて&a[5]も正しいポインタとして有効です。(全部で6個です。)
このような仕様になっているのは配列要素の走査における終了条件(末尾に到達したかどうか)の判定の際に末尾要素の1個後方の要素へのポインタが利用できると便利だからです。

なお、
②&a[6]、&a[7]...がa[4]の2,3..個後方の要素に相当する領域を正しくさせるという保証はありません。

教えていただきたいことは

①要素へのポインタとしては&a[0]からa[n]までのn+1個が正しい値として有効とありますが、n個の要素の1つ後方のポインタを指したとして、それはどういった使い方があるのでしょうか。
ポインタはアドレスを指すので、「n個の要素の1つ後方のポインタ」とはアドレスを指し示しているのだという認識です。
ただ、配列の領域外のアドレスを指したところでそれがどういった条件判定に使用できるのか理解できません。
番兵法というものがあることは理解しています。ただ番兵法とは「探索する値」と全く同じ値を、配列の一番後ろに置く探索の方法のことで、あくまで配列の最後の要素の中の値のことを言っているのであり、アドレスを指しているのではないという認識です。
このほかに、n個の要素の1つ後方のポインタを指すことで得られる方法があるのでしょうか。

②「&a[6]、&a[7]...がa[4]の2,3..個後方の要素に相当する領域を正しくさせるという保証はありません。」とありますが、正しくさせないかもしれないとはどういうことでしょうか。
配列の領域外でも、例えばインクリメントしていけば配列の型の大きさ分、アドレスが進んでいくように思うのですが、認識が誤っているのでしょうか。(上記説明の内容では、n個の配列のn+1個分の後方は正しくインクリメントされない可能性もあると読めてしまいます。)

①と②の疑問について、私一人だけだと限界がありますので、ご存知の方いらっしゃいましたら何卒ご教示いただけないでしょうか。
よろしくお願いいたします。

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

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

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

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

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

guest

回答3

0

Cの規格書(C99なのでちょっと古い)JISX3010をひっくり返してみました。

これかなぁ

6.5.6加減演算子

<略>
ポインタオペランドが配列オブジェクトの要素を指し,配列が十分に大きい場合,その結果は,その配列の要素を指し,演算結果の要素と元の配列要素の添字の差は,整数式の値に等しい。すなわち,式Pが配列オブジェクトのi番目の要素を指している場合,式(P)+N(N+(P)と等しい)及び(P)-N(N は値nをもつと仮定する。)は,それらが存在するのであれば,それぞれ配列オブジェクトのi+n番目及びi−n番目の要素を指す。さらに,式 P が配列オブジェクトの最後の要素を指す場合,式(P)+1 はその配列オブジェクトの最後の要素を一つ越えたところを指し,式 Q が配列オブジェクトの最後の要素を一つ越えたところを指す場合,式(Q)-1 はその配列オブジェクトの最後の要素を指す。ポインタオペランド及びその結果の両方が同じ配列オブジェクトの要素,又は配列オブジェクトの最後の要素を一つ越えたところを指している場合,演算によって,オーバフローを生じてはならない。それ以外の場合,動作は未定義とする。結果が配列オブジェクトの最後の要素を一つ越えたところを指す場合,評価される単項*演算子のオペランドとしてはならない。
(減算についても同様の記述)

規格書には必ずしも「何故」は書いてないので、目的は考えてみるしかないですが、配列の要素にポインタのインクリメントでアクセスしていると、その最後の条件に使えて便利でしょ、とか配列の後ろからn要素などという計算をするときに配列の最後+1のポインタから減算で求められると便利でしょ、というくらいしか思いつきません。

②については、「未定義」という言葉の定義(ややこしい)からいうと、何が起こるか決まっていない、としか言えないので、めちゃくちゃな値になることがあるかも、ということになります。

さらに、加減算演算子のところには

配列の要素でないオブジェクトへのポインタは,要素型としてそのオブジェクトの型をもつ長さ 1 の配列の最初の要素へのポインタと同じ動作をする。

という記述もあります。長さ1の配列の最初の要素って...ありゃあ。

C

1int *p; 2p=(int*)malloc(sizeof(int)*100);

右辺は配列オブジェクトではありませんから、長さ1の配列と同じ...つまり、p[1]は許されるけどp[2]は未定義? それは困るなぁ。

もう少し仕様書をひっくり返してみないだめみたいです。


もう少しひっくり返してみました。

7.20.3 記憶域管理関数 calloc 関数,malloc 関数及び realloc 関数の<略>割付けが成功したときに返されるポインタは,いかなる型のオブジェクトへのポインタに代入してもよいように,また(領域が明示的に解放されるまで)その割り付けられた領域のオブジェクト又はオブジェクトの配列へのアクセスに使用してもよいように,適切に境界調整されているものとする。

ここで配列同様のアクセスが出来ることが保証されていました。あぁよかった。

投稿2019/08/02 16:00

編集2019/08/02 22:37
thkana

総合スコア7639

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

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

m0m0

2019/08/02 16:33

ご教示ありがとうございます! > 規格書には必ずしも「何故」は書いてないので なるほど...難儀ですね。 考えられる理由としては利便性くらいなものだと...ふむふむ。
m0m0

2019/08/02 23:39

わかりやすくひっくり返していただきありがとうございます!
guest

0

ベストアンサー

単にそこまでポインタを進めても大丈夫、というだけのことでしかありません。
それから外れると、インクリメントしたときに何が起きても文句が言えないってことですね

#まあ、たまたま+1した値が代入されるのかもしれない

投稿2019/08/02 13:43

y_waiwai

総合スコア87774

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

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

m0m0

2019/08/02 13:49

回答していただきありがとうございます。 > 単にそこまでポインタを進めても大丈夫 大丈夫ということは、n個の配列のn+1個分までのアドレスなら指しても動作は(メモリ破壊など)保証されているということでしょうか。
y_waiwai

2019/08/02 13:58

n+1個めの領域はアクセス違反となりますんでアクセスできません、が、アドレス計算としてそこまで勧めた結果で計算しても正しいことが保証されるってことなんでしょうね。 #ましかし、n+1までOKというのがホンマに正しいのかどうかはよーわかりませんが またとえば、 for(char* p= array;p<(array+sizeof(array));p++){なんやかや} とした場合には、pはarray+sizeof(array)まで進むんですから、そこでの比較が不定だとされると困りますわな
cateye

2019/08/02 14:11

文字列などの処理で、while(*ptr++){〜}などと書くと終端を行き過ぎる事が有るかもです。(処理系依存だと思うが)
m0m0

2019/08/02 14:12

教えていただきありがとうございます。 なるほど。アドレス計算を使用した条件判定はそのような使い方があるんですね。 勉強になります。 > ましかし、n+1までOKというのがホンマに正しいのかどうかはよーわかりませんが 領域へはアクセス違反になりますが、アドレス計算としては正しく計算できるということなんですね。 わかりやすく教えていただきありがとうございます。 自分が完全に理解できたかというと少し怪しいですが、もやもやっとしてたものはだいぶ取れました。 ありがとうございます。
y_waiwai

2019/08/02 14:17

まー、組み込みを生業としてると、n+1までしか有効でないとされたら、こんなもんやってられるかーーとちゃぶ台を返したくなりますがw #今まで幾多のCPU/コンパイラ扱ってきましたが、範囲から外れたらわやになるという環境には出会ってませんね
m0m0

2019/08/02 14:18

ご教示ありがとうございます。 > 文字列などの処理で、while(*ptr++){〜}などと書くと終端を行き過ぎる事が有るかもです。(処理系依存だと思うが) 終端を行き過ぎてしまうとメモリ破壊などの問題があるんですよね? そのような事態にならないように使えるテクが「n個の配列のn+1個分のアドレス」ということなんでしょうかね。 「n個の配列のn+1個分のアドレス」というのが存在しないアドレスの場合はどうなっちゃうんでしょうか。(動作が保証されているとのことなので大丈夫なんですかね。) まだちょっともやっとしたものが残ってる感じです。
m0m0

2019/08/02 14:19

> まー、組み込みを生業としてると、n+1までしか有効でないとされたら、こんなもんやってられるかーーとちゃぶ台を返したくなりますがw 大抵の場合はn+1より後方も指そうと思えば指せるんですね。 勉強になります。
SaitoAtsushi

2019/08/02 14:31

C99 の文面だと 6.5.6 の第 8 段落で明記されています。 - ポインタの演算の結果が配列の最後の要素の次までなら指すのは OK - それ以外のときの動作は未定義 - ポインタが配列を超えているときに単項 * を適用してはならない http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1256.pdf#page=95
m0m0

2019/08/02 15:00 編集

回答ありがとうございます。 リンク先が英文で自分のレベルでは和訳することは難しいので、和訳していただいて助かります。 ありがとうございます。 > - ポインタが配列を超えているときに単項 * を適用してはならない これは配列の領域外の値を参照してもどういったものが入っているかわからないからですよね(たぶん...) > - ポインタの演算の結果が配列の最後の要素の次までなら指すのは OK これは例えば配列の最後の要素が使用できるアドレスのきわっきわまで使用していて、それ以上先のアドレスは割り振られていないって時も、ポインタの演算にしようする分には問題はないのでしょうか。 (う~ん、自分の勉強不足が露見していってお恥ずかしい)
y_waiwai

2019/08/02 14:53

その規定というのはおそらく仮想記憶とかキャッシュ、投機的実行とかを備えたイマドキのCPUを想定してるんだろう、というきはします。 時々の状況によって配置されるメモリは変わり、そのアドレスも連続的ではない、+1したそのアドレスが存在しているのかどうかわからないという今の当たり前の状況を、Cの世界にその環境を定義するもの(縛り付けるもの?)なんでしょう。
m0m0

2019/08/02 15:00

ご教示ありがとうございます。 仮想記憶などで動作を保証しているのだろうということですね。 なるほど、なるほど。 だんだんと疑問がクリアになってきました。 ありがとうございます。
SaitoAtsushi

2019/08/02 15:58

仕様の文面における「してはならない」は「した場合の動作は未定義」と同じであることが 4 に書かれています。 そして「動作は未定義」は「出鱈目な値が入っているかもしれない」ではなく、有体に言えば「コンパイラはどんなコードを生成してもよい」です。 素朴なコンパイラならばおおよそ想像通りに動くでしょうが、強力な最適化能力を持ったコンパイラは書かれているプログラムが正しい (仕様に沿っている) ことをあてにして最適化することがあるので、普通の人間には予測不可能なアクロバティックなコードを生成することがありますし、そうなるとプログラム上のどこの間違いが奇妙な動作を引き起こすのか追跡することが困難です。 奇妙な未定義動作の代表格としては「タイムトラベル」が知られています。 仕様に沿わないことを書いてしまったとき、その間違い箇所を通過するよりも前に奇妙な動作として現れることがあります。 https://cpplover.blogspot.com/2014/06/old-new-thing.html この例は C++ で書かれているので具体的な内容まで理解する必要はありませんが、いくつかの最適化処理が組み合わさった処理を人間が予測するのが難しいことはわかるかと思います。 もちろん処理系によってその動作内容は異なりますし、同じ処理系でもバージョンアップで挙動が変わることもあるでしょう。 未定義動作に踏み込むとあらゆる理不尽が許されてしまうので「どうしてこうなるのか」というのがわからず得るものがないのです。 デバイスドライバや OS、あるいはファームウェアを書くときは処理系ごとのクセをあてにして言語仕様で未定義動作としていることも書かなければならないことはあるでしょうが、言語を学習中の段階ではどのようにコンパイルされるかというのを想像してプログラムを書くのはあまりおすすめできることではないと私は思います。 あくまでも私の意見ですが。 いずれにしても未定義動作というのはそれくらいわけわからんものだということには留意した方がよいでしょう。 ちなみに C99 は JISX3010 として日本語で書かれた規格にもなっていて無料で見ることが出来ます。 JISC のサイトで JISX3010 を検索してみてください。 https://www.jisc.go.jp/index.html C11 や C17 での改定は JIS に反映されていないのですが、基礎的な部分には大きな変更は無いので学習のためには充分だと思います。
m0m0

2019/08/02 16:06

詳しくご説明ありがとうございます。 また、C99の日本語規格の情報を教えていただき重ねてありがとうございます。 環境次第でクセがあることを勉強できました。 クセがあるんだなというだけで、中の動作まで踏み入ったことはまだ理解できていませんが、 このことを頭にとどめ、勉強していきたいと思います。 ご教示していただきありがとうございました。
guest

0

①は配列の終端(ヌルポインタ)を表していると思われます。
②は文章の意味が理解できませんでした。

投稿2019/08/02 14:15

meg_

総合スコア10580

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

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

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.48%

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

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

質問する

関連した質問