weemiee
@weemiee (weemiee)

Are you sure you want to delete the question?

If your question is resolved, you may close it.

Leaving a resolved question undeleted may help others!

We hope you find it useful!

スタック・pop関数の引数について

Q&A

以下のサイト(ここではサイトAとします)で、まずはスタックについてのプログラムを学習しました。

しかし、他にも色々調べていたら、pop関数の書き方で次のようなものもありました。

void pop(STACK_T *stack, int *x){
	if (stack->tail <= 0) {
		printf("スタックが空です\n");
		return;
	}
	*x = stack->data[stack->tail - 1];
	stack->tail --;
}

※構造体や変数名はなるべくサイトAに合わせています。ただ、個人的な利便性からプログラムに修正を入れ、初期化時はstack->tail=0としています。

このプログラムでは、個人的に*xの動きが特に気になっています。pop関数内で、引数のxが何故ポインタ付きのint型として定義されたのか、ご教授頂きたいです。push関数内での xに該当する変数inputはポインタの付かないint型だったので、なおさら気になりました。

0

7Answer

x がpopしたい値だと思いますが、それを呼び出し元(サイトAであればmain関数)へ戻したいために、intポインタとしているのかと思います。
(イメージは、main関数側から&outputのような形でアドレス渡しで呼んでoutputにpopしたい値を入れてもらう感じでしょうか)
push関数は入力した値を呼び出し元へ戻す必要はないので、ポインタではなかったのかと。

void pop(STACK_T *stack, int *x){
	if (stack->tail <= 0) {
		printf("スタックが空です\n");
		return;
	}
	*x = stack->data[stack->tail - 1];
	stack->tail --;
}

int main(void){
    int output;
    pop(&stack, &output);
0Like

@marumenさん
ご回答ありがとうございます。
結論から申しますと、まだうまく思い浮かべられていない感じです。push関数とpop関数は、表面上ではスタックを「入れるか」「出すか」だけの違いにしか見えず、どちらか一方が何か別の影響を受けているとも思えないので、関数中のコードの書き方も矢印の向き以外ほぼ一緒で、ポインタの出番も同じではないのかと、どうしても考えてしまいます。
ただ、回答頂いた内容からプログラム処理の大雑把な流れは把握出来たような感じがするので、僕の方でも調べてみようと思います。

ちなみに、@marumenさんが仰った

(イメージは、main関数側から&outputのような形でアドレス渡しで呼んでoutputにpopしたい値を入れてもらう感じでしょうか)

のうち「アドレス渡し」をキーワードに含めてネット検索したら、次のサイト

#include <stdio.h>

struct Animal {
    int age;
    double weight;
};

void・・・・

で始まる 見出しタイトル「構造体のメンバを設定する関数」の部分で

関数の引数には構造体のポインタも使うことができます。
構造体のポインタを使えば、関数内で構造体のメンバにアクセスできます。

という風に述べられていました。これはつまり、「関数内で引数を使って構造体のメンバにアクセスする際は、関数の引数は定義時にポインタ型にする必要がある」といった意味で、定石のようなものなのでしょうか?

0Like

「関数内で引数を使って構造体のメンバにアクセスする際は、関数の引数は定義時にポインタ型にする必要がある」といった意味で、定石のようなものなのでしょうか?

アクセスの方法によりますが、基本的には次のように考えると分かりやすいかなと思います。

  1. メンバに値を設定する(書き換えたい)場合はポインタ型が必要
  2. メンバの値を読みだす場合はポインタ型じゃなくていい

例えば参考にされていたサイトのソースを少しいじったものを以下に載せると、

  • set_animal_paramsは上記の1に該当し、メンバに値を設定するので引数がポインタ型
  • get_animal_paramsは上記の2に該当し、メンバの値を読みだすので引数はポインタ型でなくていい

と説明できます。set_fail_animal_paramsは関数内では値を設定できますが、設定した値をmain関数の変数animalには反映させることができません。

説明がややこしい感じになってしまいましたが、ご理解の助けになるようであれば幸いです。

#include <stdio.h>

struct Animal {
    int age;
    double weight;
};

void set_animal_params(struct Animal *animal, int age, double weight) {
    animal->age = age;
    animal->weight = weight;
}

// 関数追加
void set_fail_animal_params(struct Animal animal, int age, double weight) {
    animal.age = age;
    animal.weight = weight;
    printf("set_fail_animal print %d %f\n", animal.age, animal.weight);  // 40 64.200000
}

// 関数追加
void get_animal_params(struct Animal animal) {
    printf("get_animal print %d %f\n", animal.age, animal.weight);  // 20 32.100000
}

int main(void) {
    struct Animal animal;

    // animalをアドレス渡ししてage,weightの値を設定
    set_animal_params(&animal, 20, 32.1);

    // アドレス渡ししないと設定したage,weightの値はmainのanimalには反映されない
    set_fail_animal_params(animal, 40, 64.2);

    // set_animal_paramsで設定した値がgetされる
    get_animal_params(animal);

    printf("main print %d %f\n", animal.age, animal.weight);  // 20 32.100000

    return 0;
}
実行結果
$ ./animal 
set_fail_animal print 40 64.200000
get_animal print 20 32.100000
main print 20 32.100000
0Like

@marumenさん
なるほどですね、アドバイスも詳しくて分かりやすいです!

つまりこんな感じでしょうか?以下、@marumenさんのアドバイスをもとに調べて分かった点について書いてみました。間違った箇所等あれば是非教えてください。

♢♢♢

プログラムにおける作成した関数までの部分

#include <stdio.h>

struct Animal {
	int age;
	double weight;
};

void set_animal_params(struct Animal *animal, int age, double weight) {
	// (*animal).age は animal->age 、(*animal).weight は animal->weight と同じ
	animal->age = age;
	animal->weight = weight;
	printf("set_animal print %d %f\n", animal->age, animal->weight);
}

void set_fail_animal_params(struct Animal animal, int age, double weight) {
	animal.age = age;
	animal.weight = weight;
	printf("set_fail_animal print %d %f\n", animal.age, animal.weight);
}

void get_animal_params(struct Animal animal) {
	printf("get_animal print %d %f\n", animal.age, animal.weight);
}

であるとして、

set_animal_paramsは上記の1に該当し、メンバに値を設定するので引数がポインタ型

set_animal_params関数について。main関数にて&animalという形でアドレス渡しをしているので、構造体Animalのメンバage,weightそれぞれに値を設定しても、正常にコンパイルされる。故に、main関数が以下のような時、エラーなく1と12.3を表示出来る。

int main(void) {
	struct Animal animal;
	set_animal_params(&animal, 1, 12.3);
	return 0;
}

get_animal_paramsは上記の2に該当し、メンバの値を読みだすので引数はポインタ型でなくていい

get_animal_params関数について。ここでは、最初にset_animal_params関数で 構造体Animalのメンバage,weightそれぞれに値を設定する事で、get_animal_params関数も含めて正常にコンパイルされている。set_animal_params関数なしに(構造体へのアドレス渡しをせずに)、メンバの値を読み出すだけで構造体へのアドレス渡しをしていないget_animal_params関数は使用しない(そうしなければ、プログラムはコンパイルエラーつきで実行される)。main関数が以下のような時に、エラーなく1と12.3を表示出来る。

int main(void) {
	struct Animal animal;
	set_animal_params(&animal, 1, 12.3);
	get_animal_params(animal);
	return 0;
}

set_fail_animal_paramsは関数内では値を設定できますが、設定した値をmain関数の変数animalには反映させることができません。

set_fail_animal_params関数は、get_animal_params関数と同じく、set_animal_params関数による構造体へのアドレス渡しなしには正常に実行出来ない。
また、これもget_animal_params関数と同じく、以下のようにset_fail_animal_paramsとset_animal_paramsを入れ替えてもコンパイルエラーとなる。

//コンパイルエラーになる
int main(void) {
	struct Animal animal;
	set_fail_animal_params(animal, 2, 45.6);
	set_animal_params(&animal, 1, 12.3);
	return 0;
}

気づいた点:アドレス渡しは、先に、必ず行う必要があるらしい。

0Like


また、これもget_animal_params関数と同じく、以下のようにset_fail_animal_paramsとset_animal_paramsを入れ替えてもコンパイルエラーとなる。

こちらですが、コンパイルオプションによってはエラーになるかもしれないですが、以下のように問題なくコンパイル通って、正常に実行もできます。
get_animal_paramsの説明のせいで分かりづらくなってしまった気がしますね・・。

int main(void) {
    struct Animal animal;

    // アドレス渡ししないと設定したage,weightの値はmainのanimalには反映されない
    set_fail_animal_params(animal, 40, 64.2);

    // animalをアドレス渡ししてage,weightの値を設定
    set_animal_params(&animal, 20, 32.1);

    printf("main print %d %f\n", animal.age, animal.weight);  // 20 32.100000

    return 0;
}
# コンパイル
$ gcc -o animal_check animal_check.c

# 実行結果
$ ./animal_check 
set_fail_animal print 40 64.200000
main print 20 32.100000

ポインタについての理解は、こちらの「値渡し」と「参照渡し」を見てもらうと理解しやすいかなと思います。
https://qiita.com/RuthTaro/items/f35c3a26779c0ca1a41a

今回のソースコードを上記のURLを参考に説明すると、

  • set_animal_paramsは「参照渡し」(=アドレス渡し)になります。
    • 参照渡し(=アドレス渡し)により、原本(main関数のanimal)を書き換えることができます
  • set_fail_animal_paramsget_animal_paramsは「値渡し」になります。
    • get_animal_paramsだと説明しづらいので、set_fail_animal_paramsの方で説明すると、set_fail_animal_paramsは複製(set_fail_animal_paramsのanimal)を書き換えてるだけなので、main関数のanimalには影響しません

他にも調べたい場合は、ポインタ 値渡し 参照渡し などで検索してもらうと色々出てくるかなと思います。

0Like

@marumenさん
ご指摘ありがとうございます。
ここまで読ませて頂いた時点だと、やはり@marumenさんが冒頭で仰っていた内容が気になります。というのも、@marumenさんが直近の回答でご教授くださったmain関数のプログラムが、paiza.ioというオンライン実行サイトでは何故かコンパイルエラーとともに実行されてしまうのです。
その原因について、自分でも調べてみているところです。

0Like

@marumenさんが直近の回答でご教授くださったmain関数のプログラムが、paiza.ioというオンライン実行サイトでは何故かコンパイルエラーとともに実行されてしまうのです。

なるほど、そうだったんですね。
もしかしたらコンパイルの時にwarningが出ることはあるかもしれないです。
(warningはあくまで警告なので、コンパイル自体には成功してプログラムは実行できる)

すでに調べられているかもしれないですが、警告 エラー コンパイルなどで検索して警告とエラーの意味合いの違いを調べられるといいかもしれません。

0Like

Your answer might help someone💌