teratail header banner
teratail header banner
質問するログイン新規登録
C

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

Q&A

解決済

3回答

580閲覧

C言語 ポインタ渡しでreallocしたのに、関数呼び出し前後でポインタが指すアドレスが変わらない

qaz_sd

総合スコア4

C

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

0グッド

0クリップ

投稿2025/06/17 02:00

0

0

前提

1つ前の質問の続きです。
↑この質問において、プログラムの記述が間違っている指摘を受けたため、
自分なりに調べて修正しようとしたのですが、どうやってもうまくいきませんでした。
(free()を1回しか実行していないのに、なぜか二重解放したことになって怒られるfree(): double free detected in tcache 2)

1日かけていろいろ実験したところ、問題を再現する簡単なプログラムAを作成出来たため、それについて質問させていただきます。

発生している問題

c

1#include <stdio.h> 2#include <stdlib.h> 3 4typedef struct { 5 int num;//数字を保存 6 char c;//アルファベットを保存 7} Kouzo; 8 9void func(Kouzo *ptr); 10 11int main(void){ 12 Kouzo *ptr = malloc(sizeof(Kouzo)); //要素1つ分の構造体を作成 13 14 ptr[0].num = 10; 15 ptr[0].c = 'A'; 16 17 printf("addr in main bef:%p\n",ptr); 18 19 func(ptr); 20 21 printf("addr in main aft:%p\n",ptr); 22 23 free(ptr); 24} 25 26void func(Kouzo *ptr){ 27 int loop = 1; 28 while(loop<10){ 29 ptr = realloc(ptr, (loop+1) * sizeof(Kouzo)); 30 ptr[loop].num = 10; 31 ptr[loop].c = 'A'; 32 loop++; 33 printf("addr in func:%p, loop:%d\n",ptr,loop); 34 } 35 36}

text

1//出力結果 2addr in main bef:0x2079260 3addr in func:0x2079260, loop:2 4addr in func:0x2079260, loop:3 5addr in func:0x2079690, loop:4 6addr in func:0x2079690, loop:5 7addr in func:0x2079690, loop:6 8addr in func:0x2079690, loop:7 9addr in func:0x2079690, loop:8 10addr in func:0x2079690, loop:9 11addr in func:0x2079690, loop:10 12addr in main aft:0x2079260 13free(): double free detected in tcache 2 14中止 (コアダンプ)

この出力より、なぜ二重解放のエラーが出たのかは理解出来ました。

  • a=realloc(b,size)に成功すると、bが指す領域は解放(free())されて、aが新しく確保された領域を指すようになる。
  • この出力によると、loop4でアドレス0x2079260が解放されて、新しく0x2079690を確保している。
  • func関数から戻ってきたとき、ポインタptrが指しているアドレスは0x2079260である。
  • ここでfree(ptr)をすると、0x2079260を再度解放することになり、 二重解放となってエラーが発生する。

しかし、 「func(ptr)の実行前と実行後でポインタptrが指すアドレスが変わらない」という点が理解できません。

↓このようなプログラムBを考えます

c

1#include <stdio.h> 2 3void funcval(int a); 4void funcptr(int *a); 5 6int main(void){ 7 int a = 0; 8 9 printf("in main, a = %d\n",a); 10 funcval(a); 11 printf("after funcval, a = %d\n",a); 12 13 funcptr(&a); 14 printf("after funcptr, a = %d\n",a); 15 return 0; 16} 17 18void funcval(int a){ 19 a = 10; 20 printf("in funcval, a = %d\n",a); 21} 22 23void funcptr(int *a){ 24 *a = 100; 25 printf("in funcptr, a = %d\n",*a); 26} 27

text

1//出力結果 2in main, a = 0 3in funcval, a = 10 4after funcval, a = 0 5in funcptr, a = 100 6after funcptr, a = 100

このプログラムBのfuncvalは値渡しですから、funcval内でaを変更しても、呼び出し元のaは変更されません。
それに対して、funcptrはポインタ渡しなので、funcptrでaを変更すると、呼び出し元のaも変更されます。
以上より、関数の引数にポインタを置き、その関数でポインタを変更すると、呼び出し元は必ず変化するはずですが、プログラムAではそれが発生していません。
これは何故なのでしょうか。

補足情報

Red Hat 8.3.1
gcc 8.3.1

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

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

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

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

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

jimbe

2025/06/17 02:13 編集

(回答へ移動)
dodox86

2025/06/17 02:48

> しかし、 「func(ptr)の実行前と実行後でポインタptrが指すアドレスが変わらない」という点が理解できません。 既に回答をいただいているのでコメントのみですが、実引数と仮引数について理解しましょう。回答中のご指摘のように勘違いからきていると思いますが、実引数はfuncを呼び出すmainのptr、仮引数はfunc関数の中でのptrで、func関数の中でptrの値(アドレス自体)を変更しても呼び出し側のmainのptrの方へは作用しません。(ptrで指し示すアドレスの先の内容へは作用できる)
guest

回答3

0

ベストアンサー

このプログラムBのfuncvalは値渡しですから、funcval内でaを変更しても、呼び出し元のaは変更されません。
それに対して、funcptrはポインタ渡しなので、funcptrでaを変更すると、呼び出し元のaも変更されます。

この部分で重要な勘違いがあります。
見た目で値(*が付いていない)だからとかアドレス(*が付いている)だからは関係ありません。

B が動くのは、パラメータとしてアドレスを渡した時、呼び出された側がそのアドレスの示す先の値を変更すれば、呼び出し側で変更した値を得られるからです。
変数 a の値を変更する為に a のアドレスを渡す のです。

実行イメージ

A の func は "『アドレス』という値" をもつ ptr を変更して main に返したいのですから、パラメータとしては 変数 ptr の値を変更する為に ptr のアドレスを渡さなければなりません

実行イメージ

値渡しとかポインタ渡しとか(時には参照渡しとか)いう言葉に惑わされないでください。
ポインタは単に、"アドレス"という値の入った変数です。その変数を(関数のパラメータとして渡して)更新するのなら、他と同じようにそのアドレスを渡す必要があります。


c

1#include <stdio.h> 2#include <stdlib.h> 3 4typedef struct { 5 int num; //数字を保存 6 char c; //アルファベットを保存 7} Kouzo; 8 9void func(Kouzo **ptr){ 10 for(int loop=1; loop<10; loop++) { 11 Kouzo *p = realloc(*ptr, (loop+1) * sizeof(Kouzo)); //realloc は失敗する可能性があるので直接 *ptr を更新してはいけない 12 if(p == NULL) { 13 printf("realloc failure\n"); 14 return; 15 } 16 *ptr = p; 17 p[loop].num = 10; 18 p[loop].c = 'A'; 19 printf("addr in func:%p, loop:%d\n", *ptr, loop+1); 20 } 21} 22 23int main(void){ 24 Kouzo *ptr = malloc(sizeof(Kouzo)); //要素1つ分の構造体を作成 25 26 ptr->num = 10; 27 ptr->c = 'A'; 28 29 printf("addr in main bef:%p\n", ptr); 30 31 func(&ptr); 32 33 printf("addr in main aft:%p\n", ptr); 34 35 free(ptr); 36}

実行結果(paiza.io)

addr in main bef:0x1b7f42a0 addr in func:0x1b7f42a0, loop:2 addr in func:0x1b7f42a0, loop:3 addr in func:0x1b7f52d0, loop:4 addr in func:0x1b7f52d0, loop:5 addr in func:0x1b7f52d0, loop:6 addr in func:0x1b7f52d0, loop:7 addr in func:0x1b7f52d0, loop:8 addr in func:0x1b7f52d0, loop:9 addr in func:0x1b7f52d0, loop:10 addr in main aft:0x1b7f52d0

投稿2025/06/17 02:13

編集2025/06/17 16:31
jimbe

総合スコア13357

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

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

qaz_sd

2025/06/18 01:07

回答ありがとうございます。 ようやく理解出来ました。ポインタはアドレスを保持しているだけの変数であって、 その保持されているアドレス(値)を変更するには、「ポインタそのもののアドレス」が必要というわけですね。(だから、そのポインタを指すポインタ{ダブルポインタ}が必要になる)
jimbe

2025/06/18 05:14

現在の主流であるノイマン型コンピュータはプログラムもデータもメモリに置くことになりますので、全てにアドレスがあります。 アドレスを扱えば変数だけでなくプログラム自体もデータとして扱えます。(今はそれをすると大抵はハッキングとして扱われるので基本的によりハードウェアに近いレベルでガードがありますが、言語としては今も可能です。) 今後は関数のアドレスというのも扱う機会があるかもしれませんね。(書き方が結構面倒ですけども…。) 質問からもコメントからもほぼアドレス(やポインタ)を理解されているように感じますので、先に進めるのではないでしょうか。 期待しております。
guest

0

int a;

これはポインタではありませんね
一方で

Kouzo *ptr;

これはポインタです
ポインタの正体はアドレスです
アドレスとはポインタにキャストされた符号なし整数です
つまりポインタptr

ptr=(Kouzo *)1234567;

のような整数を持つと解釈できます
そして、整数を持っているという点ではint aと本質的には同じです
では、これを引数に渡したらどうなるでしょうか?

整数がコピーされるのだから、当然アドレスもコピーされます
何故ならアドレスも元々は整数だからです
では、このptr自体のアドレスを渡したい時は?
そんな時は以下のようにします

Kouzo **ptr_pointer=&ptr;

これがダブルポインタです

投稿2025/06/17 06:04

Manabu

総合スコア120

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

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

qaz_sd

2025/06/18 01:11

回答ありがとうございます。 「ポインタに代入されたアドレス(ポインタが指しているアドレス)」と「ポインタが保存されている領域のアドレス」が同じものである(どっちも同じように変更できる)と勘違いしていたようです。
guest

0

こんにちは。

引数が「int」だったとき、関数の中で引数である「int」を書き換えても外では変わらないことは分かっていると思います。
引数が「int のポインタ」だったとき、中で int を書き換えることはできますが、引数である「int のポインタ」は書き換えたとしても、外では変わっていません。
引数が「int のポインタのポインタ」だったとき、中で int を書き換えたり、int のポインタを書き換えることはできますが、引数である「int のポインタのポインタ」を書き換えたとしても、外では変わっていません。

要するにこれらは、「関数の引数を書き換える」行為には意味がない、という表現に単純化できます。

投稿2025/06/17 04:00

tamoto

総合スコア4346

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

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

qaz_sd

2025/06/18 01:13

ありがとうございます。引数のポインタが指す先を編集することはできるが、引数ポインタそのもの(ポインタが指している場所)を編集することはできないということですね。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.30%

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

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

質問する

関連した質問