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

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

ただいまの
回答率

89.07%

構造体とポインタと配列がごっちゃになって困っています。

解決済

回答 4

投稿 編集

  • 評価
  • クリップ 1
  • VIEW 4,929

teityura

score 67

下記のプログラムのどこがどうダメなのか教えてください。

1.関数の引数に通常の変数を渡すと値渡しとなり、
呼び出し元の変数に変更を加えることができませんが、
関数の引数に配列を渡すと参照渡しとなり、
呼び出し元の関数でも変更を加えることができると認識しています。

2.&data[0]やdataは配列の先頭のアドレスを指しており、
呼び出し先の関数でdata[i]->nameとすれば、
student型data変数の先頭のアドレスから
i個離れたアドレスのname要素にアクセスすると認識しています。

また、*(構造体ポインタ変数名).要素名 と 
構造体ポインタ変数名->要素名 の 使い分けもいまいち理解できていません。
(scanf("%s", *(data[i]).name); としてもエラーが出ました)

#include <stdio.h>
#include <string.h>

typedef struct student_tag {
    char name[64];
    int year;
    char sex;
} student;

void student_input(student *data/* あるいは data[] ? */, int count);
void student_print(student *data/* あるいは data[] ? */, int count);

int main(void) {
    student data[3];

    student_input(&data[0]/* あるいは data ? */, 3);
    student_print(&data[0]/* あるいは data ? */, 3);

    return 0;
}

void student_input(student *data, int count) {
    int i;
    char txt;
    for (i=0; i<count; i++){
        scanf("%s", data[i]->name);
        scanf("%d", data[i]->year);
        scanf("%s", data[i]->sex);
    }

    return;
}

void student_output(student data[], int count) {
    int i;
    for (i=0; i<count; i++){
        printf("[名前]\t%s\n", data[i]->name);
        printf("[年齢]\t%d\n", data[i]->year);
        printf("[性別]\t%s\n", data[i]->sex);
    }

    return;
}

下記追記(2018_0316_1907)

ドット演算子を使うべき場面で、アロー演算子を使っていること。
ドット演算子を使うべき場面が分からなくなってきました...

頭の整理のために下記のようなものを書いてみましたが、コメントの通り混乱しております。Wandbox

1.warning: format '%p' expects argument が出ているのは
pdata[0],pdata[1],pdata[2]にアドレスが入ってないからでしょうか。

2.*pdataというポインタ変数を宣言していますが、配列ではなく
pdataという配列を宣言していないのに、pdata[1]など、要素にアクセスしようとするとエラーになるのでしょうか。
pdata[1]はpdataに格納されたアドレスから1個分離れたアドレスではないのでしょうか。
pdata[0]がpdataに格納されたと同じにならないのはなぜでしょうか。

このあたりの解釈について、上手くまとまっているサイトなどはないでしょうか。

#include <stdio.h>
#include <string.h>

typedef struct student_tag {
    char name[64];
    int year;
    char sex[64];
} student;

int main(void) {
    student data[3];
    student *pdata;
    pdata = data;
    printf("_data___ = %p\n", data);        //先頭のアドレス
    printf("pdata___ = %p\n", pdata);        //先頭のアドレス
    printf("&data[0] = %p\n", &data[0]);    //先頭のアドレス
    printf("pdata[0] = %p\n", pdata[0]);    //先頭のアドレス+0個離れてるアドレスのはずなのに先頭のアドレス+0と違う?
    printf("_data[0] = %p\n\n", data[0]);    //pdata[0]と同じアドレス? そもそも何者?

    printf("&data[1] = %p\n", &data[1]);    //先頭のアドレス+1個離れたアドレス
    printf("pdata[1] = %p\n", pdata[1]);    //先頭のアドレス+1個離れてるアドレスのはずなのに先頭のアドレス+1と違う?
    printf("_data[1] = %p\n\n", data[1]);    //謎

    printf("&data[2] = %p\n", &data[2]);    //先頭のアドレスから2個離れたアドレス
    printf("pdata[2] = %p\n", pdata[2]);    //先頭のアドレス+2個離れてるアドレスのはずなのに先頭のアドレス+2と違う?
    printf("_data[2] = %p\n\n", data[2]);    //謎

    //strcpy(data->name, "Mario");                        //いける
    //strcpy(data[0]->name, "MARIO");                    //構造体ポインタ変数名じゃないからいけない
    //strcpy(&data[0]->name, "MARIO");                    //構造体ポインタ変数名じゃないからいけない
    //strcpy(pdata[0]->name, "MARIO");                    //構造体ポインタ変数名だけど、要素がついたらいけない? そういう決まり?
    strcpy(pdata->name, "MARIO");                        //いける
    //printf("data[0]->name = %s\n",data[0]->name);        //構造体ポインタ変数名じゃないからいけない
    //printf("pdata[0]->name = %s\n",pdata[0]->name);    //構造体ポインタ変数名なのになぜかいけない?
    printf("data[0].name = %s\n",data[0].name);            //いける
    printf("pdata[0].name = %s\n",pdata[0].name);        //いける

    return 0;
}
  • 気になる質問をクリップする

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

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

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 4

checkベストアンサー

+3

上から順に行きます。既に皆さんの回答とかぶる部分もありますが。

  1. 関数の引数に配列を渡すと...
    Cでは、言葉通りの「配列を渡す」という動作は出来ません。配列の先頭要素へのアドレスを渡します。機能としては参照渡しを実現することにはなりますが、やっていることはアドレスの値渡しです。ので、これを参照渡しと言ってしまうことも多いです。
    また、引数として配列のような記述をすることは出来ますが、解釈はポインタ型として行われ、要素数を書いてあったとしても無視されます。
    void func( char str[128]) --等価--> void func( char *str )

2.&data[0]やdataは、配列の先頭要素へのアドレスを指しています。&dataとすると配列へのアドレスということになるので、厳密に区別する必要がある場合(ほとんどない)は区別します。
注意すべきは...&data[0]やdataは、その通り、配列の先頭要素へのアドレスです。ですが、data[i]は配列のi番目の要素そのものです。なので、その構造体メンバーへのアクセスを記述するならドット演算子.で繋ぐことになります。
&data[i]や(data+i)であればdata変数の先頭のアドレスからstudent型変数i個分離れたアドレスということになり、アロー演算子で繋げば構造体メンバーへのアクセスを行えます。

ポインタと配列の関係(?)をもしかして把握してないかな?
T型の配列arrayとT型へのポインタptrがあったとして、ptr=arrayであってまたNが整数であれば array[N]と*(ptr+N)は等価、というのがCの規則です。
もう一つ、配列が単独で記述されると、いくつかの例外を除き、配列の先頭要素へのポインタとして解釈されるというのもポインタと配列の話をするときに重要な規則。
(ここからN[array]も有効な記法でarray[N]と等価だ、なんていう有名なヘンな記述が導かれるわけですが)

ココで余計なことを。ぜひ。ポインタにおいてはxxポインタとかxxのポインタとかいう言い方でなく、xx'型への'ポインタ、と呼ぶようにしてみて下さい。ちょっとだけですが、これを意識することで何かが変わってくると思います。'型'は面倒なら略してもいいですけど'への'は必須。

話を戻して。
.の結合力は*より強いので、(*構造体へのポインタ).メンバー名、としないといけません。カッコの付け所が違っています。*(構造体へのポインタ).メンバー名だと、*(構造体へのポインタ.メンバー名)という解釈になり、ドットの使いどころではないのでエラーになります。
で、参照演算子*を付けたらポインタが指している実体そのものになり、構造体そのものなのでメンバーへのアクセスは.を挟んで行うことになります。(* ).と->が同じ役割、と言ってもいいでしょうか。

(scanf("%s", *(data[i]).name); としてもエラーが出ました)

繰返しになりますが、data[i]はもはやポインタではない(だって、*(data+i)と等価で、*が付いてるでしょ)ので、
data[i].name
(data+i)->name
とかでないといけません。

ということで...

/* 全般に、仮引数の student *dataとstudent data[]は等価です。 */
/* だからといって記法を混ぜるのは礼儀(?)としてやめましょう。 */
/* 配列として扱うことを強く意識するなら、student data[]の方が */
/* 適切かと私は思います。そのために配列形式の記法が許されているのです */
void student_input(student data[], int count);
void student_print(student data[], int count);

int main(void) {
    student data[3];

    student_input(data, 3); 
/* ここも、「配列を渡す」という意識の現れとして&data[0]よりは */
/* dataの方が適切でしょう */
    student_print(data, 3);

    return 0;
}

void student_input(student data[], int count) { /* プロトタイプに合わせる */
    int i;
    /* char txt; 未使用です */
    for (i=0; i<count; i++){
        scanf("%s", data[i].name);
        scanf("%d", &data[i].year);/* scanfには変数のアドレスを渡しましょう */
/* 前述のように、.の結合力は強いので、敢えてカッコをつけると*/
/* &data[i].yearは(&data[i]).yearではなく*/
/* &(data[i].year)と解釈されます */
        scanf("%c", &data[i].sex);/* 既に指摘ありますね。ここは%c */
    }

    return;
}

void student_print(student data[], int count) {/* 関数名どっちが本当? */
    int i;
    for (i=0; i<count; i++){
        printf("[名前]\t%s\n", data[i].name);
        printf("[年齢]\t%d\n", data[i].year);
        printf("[性別]\t%c\n", data[i].sex);
    }

    return;
}


さて、追記パート

ドットとアローの使い分けはもう上記の通り、としかいいようがないです。構造体の実体にはドット、構造体型へのポインタにはアロー。

1.%pにはアドレスを与えなければいけません。pdataやpdata+1,pdata+2ならアドレスですが、pdata[0]はもはやアドレスではありません。入っている/入ってない、ではなく、アドレスなのか、そうでないのか、です。

2.これも前述の通り。ポインタと配列の関係から、pdataというポインタはまた配列として扱えて、pdata[1]は*(pdata+1)と等価です。pdata[1]は、pdataに格納されたアドレスからstudent1個分はなれたところにある構造体の実体、pdata[0]が同じなのは*pdataです。
printf("pdata[0] = %p\n", pdata[0]);も
printf("_data[0] = %p\n\n", data[0]);も、
本来は%pで表示すべきデータではありませんが、現実としてはdata[0]に格納されているデータそのものを表示しているのでしょう。手元のVisual Cでは、data[0].nameに"1234"を入れておいたら34333231が表示されました。'4'が0x34,'3'が0x33, '2'が0x32, '1'が0x31 ですね。

data[0]も、pdata[0]も、構造体の実体を表している、ここをなにか誤解しているのが混乱のもとのような気がします。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2018/03/19 01:02

    混乱していた部分が少しスッキリした気がします。
    1.関数の引数として配列のような記述をするとポインタ型として解釈され、要素数を書いても無視される。
    void func( char str[128]) --等価--> void func( char *str )

    2.&data[i]や(data+i)はdata変数の先頭のアドレスからstudent型変数i個分離れたアドレスを指す。

    3.data[0]も、pdata[0]も、構造体の実体を表しているので、*(data[i]).name)はエラーになり、
    data[i].name や (data+i)->name としなければならない。

    まだ、たまに迷ったときにはまりそうですが、頭の整理のためにも
    xx'型への'ポインタ と呼ぶように意識してみます。ありがとうございました。

    キャンセル

+2

1.関数の引数に通常の変数を渡すと値渡しとなり、
呼び出し元の変数に変更を加えることができませんが、
関数の引数に配列を渡すと参照渡しとなり、
呼び出し元の関数でも変更を加えることができると認識しています。

表面的には、『配列を渡すと要素を変更できる』という認識は正しいです。

ただし... C言語に参照渡しはありません。どちらも値渡しです。
ポインタを値渡しすれば、同じ領域にアクセスすることが出来るだけです。


2.&data[0]やdataは配列の先頭のアドレスを指しており、

概ね正しいです。 (厳密にはdataが配列そのものを示す文脈もあります。)

呼び出し先の関数でdata[i]->nameとすれば、

data[i]の型はstudentであって、student*ではありません。
非ポインタの構造体変数 の要素を参照するときには、ドット演算子を使わねばなりません。

student型data変数の先頭のアドレスから
i個離れたアドレスのname要素にアクセスすると認識しています。

この認識自体は正しいです。


また、*(構造体ポインタ変数名).要素名 と 
構造体ポインタ変数名->要素名 の 使い分けもいまいち理解できていません。
(scanf("%s", *(data[i]).name); としてもエラーが出ました)

どちらも同じです。ただアロー演算子を使った方が素直でしょう。
訂正: 間接参照演算子*よりドット演算子.の方が優先順位が高いため、等価ではありません。

追記:
構造体ポインタのメンバにアクセスする、等価な方法は次の二つです。

  • (*構造体ポインタ変数名).要素名
  • 構造体ポインタ変数名->要素名

追記終わり

void student_input(student *data/* あるいは data[] ? */, int count);

student *dataでもstudent data[]でも同じです。
しかし個人的には、後者の方が配列の存在を感じられる分好みです。

(ポインタが配列を指さないことも充分あり得る。)

結局何が問題か?

ドット演算子を使うべき場面で、アロー演算子を使っていること。

それ以外の問題

入力周りに問題があります。

scanf("%s", data[i].name);
scanf("%d", data[i].year);
scanf("%s", data[i].sex);
  1. scanfの第二引数以降は、格納先のアドレスです。
    data[i].nameはポインタを返すので問題ないですが、yearとsexは非ポインタです。
  2. 文字は%cで受け取る必要があります。
  3. 必要に応じてバッファのゴミを回収せねばなりません

上記の問題をクリアすると次のようになります。

scanf("%s", data[i].name);
scanf("%d", &data[i].year); 

// %cはそのまま使うと改行文字を読んでしまう
while(getchar() != '\n');
scanf("%c", &data[i].sex);

あるいは、一気に読んでしまうのもアリです。

scanf("%s\n%d\n%c\n", data[i].name, &data[i].year, &data[i].sex);

本当はエラーチェックもすべきところですが、ここでは省きます。(c.f. サンプル)

その他

  • student_output と student_print の表記揺れ。
  • 無駄な変数 txt の存在。 (エラーではなく警告だけど)

サンプル

『とりあえず動く』ようにしたコードです。Wandbox

#include <stdio.h>
#include <string.h>

typedef struct student_tag {
    char name[64];
    int year;
    char sex;
} student;

void student_input(student *data/* あるいは data[] ? */, int count);
void student_print(student *data/* あるいは data[] ? */, int count);

int main(void) {
    student data[3];

    student_input(&data[0]/* あるいは data ? */, 3);
    student_print(&data[0]/* あるいは data ? */, 3);

    return 0;
}

void student_input(student *data, int count) {
    int i;
    for (i=0; i<count; i++){
        scanf("%s", data[i].name);
        scanf("%d", &data[i].year); 

        // %cはそのまま使うと改行文字を読んでしまう
        while(getchar() != '\n');
        scanf("%c", &data[i].sex);
    }

    return;
}

void student_print(student data[], int count) {
    int i;
    for (i=0; i<count; i++){
        printf("[名前]\t%s\n", data[i].name);
        printf("[年齢]\t%d\n", data[i].year);
        printf("[性別]\t%c\n", data[i].sex);
    }

    return;
}

気が向いたので改造もしてみました。Wandbox

#include <stdio.h>
#include <stdlib.h>
#define ARRAY_LEN(arr) (sizeof(arr) / sizeof(*arr))

typedef struct {
    char name[64];
    int  year;
    char sex;   // is_man にする余地あり
} Student;

int scan_student(Student *dst);
int print_student(const Student *src);

int main(void) {
    Student data[3];
    for(size_t i = 0; i < ARRAY_LEN(data); ++i) {
        scan_student(&data[i]);
        print_student(&data[i]);
    }

    return 0;
}

int scan_student_without_check(Student *dst) {
    return scanf(
        "%s\n%d\n%c\n",
        dst->name, &dst->year, &dst->sex
    );
}
int scan_student(Student *dst) {
    int ret = scan_student_without_check(dst);
    if(ret != 3) {
        fprintf(stderr, "Cannot read student data.\n");
        exit(1);
    }

    return ret;
}

int print_student(const Student *src) {
    return printf(
        "[名前]\t%s\n"
        "[年齢]\t%d\n"
        "[性別]\t%c\n",
        src->name, src->year, src->sex
    );
}

まだまだ改良の余地があるとは思いますが。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2018/03/16 10:06 編集

    「*(構造体ポインタ変数名).要素名」は構文エラーではないでしょうか。この書き方をするときは
    「*(構造体変数名).ポインタ型メンバ変数」という場合だと思われます。この場合は変数名を括弧でくくる必要はありませんが。

    「構造体ポインタ変数名->要素名」と等価なのは「(*構造体ポインタ変数名).要素名」になると思います。

    キャンセル

  • 2018/03/16 10:28

    おっしゃるとおりです。訂正しておきます。
    ご指摘ありがとうございます。

    キャンセル

  • 2018/03/19 01:06

    > ドット演算子を使うべき場面で、アロー演算子を使っていること。
    に気を付けます!
    inputの問題やサンプルまで丁寧に説明いただき、ありがとうございました。

    キャンセル

+1

C言語の特例ですが、ポインタは配列と同等に扱うことができます
(ここらへんがややこしいところですが)

1.warning: format '%p' expects argument が出ているのは

pdata[0] はアドレスではなく、student 型です
同様に、pdata[1] と *(pdata+1) は同じ student型変数を指してます

2.*pdataというポインタ変数を宣言していますが、配列ではなく

上記の特例から、pdata[1]やpdata[2] という表現は正当なものです

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

0

student_input(&data[0]/* あるいは data ? */, 3);

この書き方だと、data[0] のアドレスをstudent_input に渡しています
この関数の内容からすると、data という(配列)変数のアドレスを渡すべきですから、

student_input(&data, 3);

とするのが正解でしょうね。

#ちなみに、上記2通りの書き方では全く同じ値がstudent_input に渡るんで同じ結果になりますが


void student_input(student *data, int count) {
    int i;
    char txt;
    for (i=0; i<count; i++){
        scanf("%s", data[i]->name);
        scanf("%d", data[i]->year);
        scanf("%s", data[i]->sex);
    }

    return;
}


この関数の引数、dataは、student配列のアドレスですから、
data[i] で、student配列のi番目の要素を表します
なので、

data[i].name

という記述でないとダメですね。


で、ホンマは、
dataは、student配列のアドレスですから、

void student_input(student *(data[]), int count) {

と定義するのが正解なんですねー

#まーどっちでも同じ結果になるんでええっちゃーえーはなしですが

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2018/03/16 22:21

    >dataは、student配列のアドレスですから、

    配列は3つの例外を除いて常にポインタに読み替えられることを考えると違和感のある理解方法かなぁと

    キャンセル

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

  • ただいまの回答率 89.07%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る