コンテンツにスキップ

Q&A

ここでは講義中に出た質問に対する回答を載せていきます。随時更新です。

Week1

  • 確認なのですが、この講義は他学部履修での単位取得は可能でしょうか?

    • OKのはずです。便覧等はよく確認してください。
  • 以降の作業はローカル環境(vscodeとターミナル)で行っても問題ないでしょうか?

    • OKです。ここも参考にしてください。
  • すみません、左上のTerminalを押して出るターミナルと右上のボタンを押して出るターミナルの違いはありますか?

    • わからないです(知っている人がいたら教えてください)。たぶんどちらでも大丈夫です。講義では黒い画面が出るほうですすめます。参考までにその2つを下にはっておきます。どうやら二通りのターミナルが存在するようです。
  • ターミナルの最初の記号が$ではなく%と表示されているのはなぜでしょうか

    • シェル(ターミナルの裏側のプログラム)によってかわるようです。Google Cloud Shellのターミナルではbashという一般的なシェルが使われており、先頭は$です。一方、zshというシェルでは先頭は%のようです。macはcatalina以降zshがデフォルトになっているようですので、macの人は先頭が%になっているかもしれないです(bashに変えることも出来ますが推奨しません)
  • cd..と押すと-bash: cd..: command not foundと出てしまいます

    • cd..の間に半角スペースが必要です
  • 他のcdと違いcd -だけは移動だけではなく、今の位置も表示してくれるのは何か理由がありますか?

  • cdの後にtabキーを押しても自動補完されず、何も起こらないのですが、できるようにするにはどうしたらいいでしょうか。

    • ローカル環境だとしたら、環境によるかもしれません。連打したり、色々やってみてください
  • $ cat /usr/include/stdio.h においてusrの下の層にincludeがないのですがどうすれば見ることができますでしょうか?

    • macなど別環境だとパスが違うようです。こちらを確認してみてください:/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/
  • cd /usr/includeと打ったところ、too many argumentsと表示されたのですが、ファイルが多すぎて表示できないということでしょうか?

    • なにかタイピングをミスっていそうです。間違えて空白をいれてしまっていませんか? cd /usr /include のように。
  • cdと/の間にはスペースを開けるのがマナーなのですか?

    • cd のあとにはスペースを開ける必要があります。 cd/ こういうのはダメです
  • #includeの行とmain()の行は離したほうがいいのですか?単に見やすさのためですか?

    • 一行は空白をいれたほうがいいです。見やすさのためです。空白がなくてもコンパイルは通ります
  • main関数の型をvoidにした場合も“change return type to ‘int’“というnoteが出たのですが、ここではなぜintの方が適切なのでしょうか?

    • mainが返すものはintだと決まっているというだけです
  • 演習の時には int main() から始めて return 0; で終わるコードを使いましたが、何の違いでしょうか

    • そっちのほうが、全てちゃんと書いた丁寧な正しい表記です。今回書いたものはreturnとかを省略しています。
  • 64bitなのにアドレスには16進数で14桁までの数字しか表示されてませんが、これはなぜですか?

    • たとえば8bitの整数を考えるとき、もし0から1111までの下位4桁しか使わないことがわかっているなら、8桁表示しなくても毎回4桁表示するだけで十分ですよね。それと同じことが起きているだけです。アドレスは、64bitの整数で表現されますが、64bitで表現できるすべての値を使うわけではないです。例えばwindowsの場合は0x000'00000000から0x7fff'ffffffffを使うとのことです。参考:仮想アドレス領域
  • 先頭の0xっていうのは何進数かの表記でしょうか。

    • 16進数という意味です
  • コラムの32-18=4の部分は32-28=4ですか?

    • そうです。タイポです。。。。有難うございます。直しました。
  • ここでのメモリとは、個人のパソコンのものですか?それともクラウド上のものですか?

    • クラウド上のものです。これからの全ての話は、Google Cloud Shellを使う場合、クラウド環境のマシンの話です。ここも参考にしてください。
  • コード内で四則演算をするとき、文字と演算子の間に半角スペースを入れることが多い気がしますが、これは何らかの流儀によるものですか?

    • 流儀によるものです。参考文献の「【改訂版】組込みソフトウェア開発向け コーディング作法ガイド[C言語版]ESCR Ver.3.0, IPA, 2018.」を参考にしてください
    • pythonだと、引数入力の部分はむしろいれないのが流儀だったりします def hoge(a=123): のように。参考:PEP8
  • intでのアドレス差は4以上で、charでのアドレス差は1ということは何を意味するのでしょうか?

    • ここで言いたかったことは、教科書で勉強する「intは4byte」とか「charは1byte」ということを実感してほしかったとうことです。並べたときに(すくなくとも)intでは4byte以上離れますが、charは1byteしか使わないので連続になることもあり得ます。このへんを実際にアドレスの数字を表示して眺めてみてほしかったというのが意図です
  • 実行環境として Windows + MinGW でも問題ないでしょうか。

    • OKです。細かいところが違うと思いますのでそれは注意してください
  • 個人的な興味で char a[16] を作ってアドレスを見たら19個飛んでいたのですが、配列だとchar16個分よりも余計にメモリ領域が必要になると考えて良いのでしょうか?

    • 配列だということが理由で余計に確保されたわけではないと思います

Week2

  • 以下の「%」を「%%」にしないとダメではないですか? printf("a % b = %d\n", a % b);

    • その通りでした。gccだと%だけでもエラーは出ません。しかし、Mac (clang) だと%だけの場合エラーになります。gccでも-Wallオプションを付けて警告レベルをあげるとエラーになるようです。 ちなみに、%を重ねるのは\nなどのエスケープ文字とは違い、printfに直接書いたときだけ発動するという特殊なもののようですので、注意してください。次の例のように、不思議な挙動になります。

      printf("abc\n");    // abcと表示出力
      char s1[] = "abc\n";
      printf("%s", s1);   // 同様に、abcと表示
      
      printf("%%\n");     // %と表示
      char s2[] = "%%\n";
      printf("%s", s2);   // %%と表示(!)
      
  • for文の式3では++iとi++のどちらを使ってもいいのでしょうか?

    • iが単純なintなどの場合は、どちらでも同じです。Google c++ coding standardでは、イテレータなどに対しては前置(++i)にするほうが良いとありますので、私は癖で全て前置にしています。本講義で扱う、単純なスカラー値ではどちらでもいいです。
  • 2重ループにbreakが入っているときには二つのループ全部止まりますか、中のループだけ止まりますか。

    • 内側のループだけ止まります。
      for (int i = 0; i < 2; ++i) {
          for (int j = 0; j < 3; ++j) {
              if (j == 1) {
                  break;
              }
              printf("i=%d, j=%d\n", i, j);
          }
      }
      
      とすると結果は次のようになります。
      i=0, j=0
      i=1, j=0
      
  • 初期化していない配列にはどのような値が入っていますか?

    • C言語の取り決めでは、代入していない変数の値は「未定義」です。なのでどのような値のこともありえます。
  • 2次元配列の初期化時に,int a[][] = {{1, 2}, {3, 4}};のように書けないのは何故でしょうか?

    • そういう取り決めになってます(授業中に、あとからもう少し詳しく調べて追記すると言いましたが、良いリファレンスがありませんでした。。。。)
  • c言語ではtrueが1,falseが0なのに,main関数が正常終了で0を返すのは何故なのですか?

    • ごもっともな指摘ですね。。。この「正常終了が0」というのは、c言語の話ではなくて、linux (unix? bash?)のルールなのです。なので、c言語の真偽とは違った話です。以下のようなコードで確認できます。
      import subprocess
      
      ret = subprocess.run("ls")
      print(ret)
      
      これは、lsというコマンドを実行したときにどういう情報が返ってくるかを表示してくれるpythonコードです。これを実行してみます。
      matsui528@cloudshell:~$ python3 check.py
      check.py  README-cloudshell.txt
      CompletedProcess(args='ls', returncode=0)
      
      ここからわかることは、lsを通常通り実行した場合、0という値が返ってきているということです。ここからも、コマンドの正常な返り値が0というルールになっていることがわかります。
  • 基本的に、プロトタイプ宣言をせずに済むような順序で関数は定義したほうが良いのでしょうか

    • ソフ1の範囲では、mainの上に全部書けばOKです
  • 初歩的な質問で申し訳ないのですが、値渡しの項でのプログラムを正常に作動させる(きちんと1が足されるようにする)ためにはどのような改良をすれば良いのでしょうか?

    • ポインタをつかいます。week 4でやります
  • 宿題置き場とはどちらでしょうか?

  • READMEにも課題内容を入れていただけると助かるんですが可能ですか?

    • READMEの文中にリンクが張ってあります。それをクリックするとウェブの説明画面に飛びます。昨年は、READMEにミスがあってあとから修正するといったことが結構あったので、あとから修正しやすいようにこういう方式になっています。。。
  • week_2-2や2_3もmain.cにプログラムを書けばいいですか?

    • そうです
  • 宿題の提出で、コメントを書き忘れたため、コメントを追加しようとしたのですが、コメントを追加しても、commit changes のボタンが光らず、変更を加えることが出来ません。どうすればよろしいでしょうか?

    • ソースコードに全く変更がない場合は更新することができません。main.cのなかのどこか適当なところに空の行を追加すると更新できるようになり、コメントも書けます(ちなみにコメント忘れても別に大丈夫です)
  • math.hを使うと、自動採点システム上でコンパイルできない

    • math.hを使う場合は環境によってはgcc main.c -lmとする必要があります。Googleのほうでは必要ないようです。いずれにせよ、今回の課題ではmath.hを使わなくてもできるので、使わない方法を考えてみてください。

Week3

  • 文字とintegerの混ざった配列の場合、要素と配列のsizeofの商で,配列の長さを求めることはできない,という理解で正しいでしょうか?

    • char配列ということですよね?できます。sizeof(a) / sizeof(char) です。sizeof(char)は1なので、これはsizeof(a)になります。
  • strlen(a)のaもポインタなんですか?

    • ポインタなのです
  • strcmpの箇所のs4とs5の比較で表れる-100という数字は何を表しているのでしょうか?

    • これは処理系依存です。値自体に注目するべきではなく、正か負か0かのみが重要です。今回-100になるのは、おそらく下記のようなものに近い実装になっているからだと思います。すなわち、差異が生じたときのcharの値の差になっていると思います King, p306
      #include <stdio.h>
      #include <string.h>
      
      int mystrcmp(char s[], char t[]) {
        int i; 
        for(i= 0; s[i] == t[i]; i++) {
          if (s[i] == '\0') {
            return 0;
          }
        }
        return s[i] - t[i];
      }
      
      int main() {
        char a[] = "abc";
        char b[] = "abcd";
        printf("%d\n", strcmp(a, b));   // -100
        printf("%d\n", mystrcmp(a, b)); // -100
      }
      
  • Printf関数において型を間違えても警告が出るだけでちゃんと表示してくれることが多々ありますがこれはありがたい機能なのでしょうか?それとも何か危険性を孕んでいるのでしょうか?

    • 基本的にありがたい機能であり、従うとよいです。
  • pythonのように文字列同士の足し算は基本的に不可能ですか?

    • strcatを使うということになります(なので、pythonのように気軽・柔軟ではないです)
  • 数値を扱う場合、型ごとに関数を用意する必要がありますか?そうしなくても良い仕組みはありますか?

    • 基本的に型ごとに関数を用意する必要があります。マクロという機能で色々できる場合もあると思います。c++の場合は以下のようにテンプレートという機能で対応できたりします
      #include <iostream>
      
      template<typename T>
      T sum_array(T a[], int n) {
          T sum = 0;
          for (int i = 0; i < n; ++i) {
              sum += a[i];
          }
          return sum;
      }
      
      int main() {
          int arr[] = {1, 2, 3};
          double darr[] = {1.0, 2.0, 4.0};
          std::cout << sum_array<int>(arr, 3) << std::endl;
          std::cout << sum_array<double>(darr, 3) << std::endl;
      }
      
  • sizeof(配列名)はどのように配列の長さを取得しているのでしょうか?

    • コンパイラは知っているので、できます(あまり解答になっていませんが・・・)
  • strcpyについて、受けて側の容量が足りない場合リテラルで渡すとエラーが出たのですが、下記のように渡すとエラーも出ず通ってしまいました。

    char s1[] = abcd;
    char s2[strlen(s1) - 2];  // strlen(s1) - 2 = 2 となるはず
    // char s2[2];   // これだと通らない
    printf(%lu\n, strlen(s1) - 2);
    strcpy(s2, s1);  // 十分な容量でないはず
    printf(%s\n, s1);
    printf(%s\n, s2);
    printf(%lu\n, strlen(s2));
    printf(%lu\n, strlen(s1));
    
    何か見落としているのでしょうか、理由がわからないです。

    • 調べました。

      • s2の配列長に数字リテラルを使うかstrlenのようなものを経由するかによって、コンパイラが配列をメモリ上のどの位置にレイアウトするのかが変わる(この部分はコンパイラが決めるのでユーザは普通は関与しない)
      • 数字リテラルのときにはs2がちょうどs1の前の部分に配置される。strlen経由の場合は違う位置に配置される。
      • strcpyで無理やりコピー(未定義動作)するときに、s1とs2のメモリ上の位置関係の違いで結果が変わる。

    というのが結論です。これを見ていきましょう。まず今回の問題を整理すると以下になります。

    #include <stdio.h>
    #include <string.h>
    
    int main() {
        char s1[] = "abcd";
    
        // パターンA
        //char s2[strlen(s1) - 2];  // strlen(s1) - 2 = 2 となるはず
    
        // パターンB
        char s2[2];
    
        strcpy(s2, s1);  // s2が十分な容量でないのにコピーするので、未定義動作
        printf("s1: %s\n", s1);  // Aではabcdになる  Bではcdになる
        printf("s2: %s\n", s2);  // AでもBでもabcdになる
    }
    
    上記で、パターンAでコンパイルした場合と、パターンBでコンパイルした倍で結果が違うというのが問題です。ここで、以下のような調査関数をいれて調べてみます。
    #include <stdio.h>
    #include <string.h>
    
    
    void print_5chars(char s[]) {
        printf("\taddress: %p\n", s);
        printf("\tprint by char: ");
        for (int i = 0; i < 5; ++i) {
            printf("s[%d] = %c, ", i, s[i]);
        }
        printf("\n");
    }
    
    int main() {
        char s1[] = "abcd";
    
        // パターンA
        //char s2[strlen(s1) - 2];  // strlen(s1) - 2 = 2 となるはず
    
        // パターンB
        char s2[2];
    
        printf("initial s1:\n");
        print_5chars(s1);
        printf("initial s2:\n");
        print_5chars(s2);
    
        strcpy(s2, s1);  // s2が十分な容量でないのにコピーするので、未定義動作
    
        printf("after s1:\n");
        print_5chars(s1);
        printf("after s2:\n");
        print_5chars(s2);
    
        printf("s1: %s\n", s1);  // Aではabcdになる  Bではcdになる
        printf("s2: %s\n", s2);  // AでもBでもabcdになる
    
    }
    
    この結果は次のようになります。まず、パターンAの場合は以下です。
    initial s1:
            address: 0x7fff71a76f4b
            print by char: s[0] = a, s[1] = b, s[2] = c, s[3] = d, s[4] = ,
    initial s2:
            address: 0x7fff71a76f30
            print by char: s[0] = , s[1] = , s[2] = , s[3] = , s[4] = ,
    after s1:
            address: 0x7fff71a76f4b
            print by char: s[0] = a, s[1] = b, s[2] = c, s[3] = d, s[4] = ,
    after s2:
            address: 0x7fff71a76f30
            print by char: s[0] = a, s[1] = b, s[2] = c, s[3] = d, s[4] = ,
    s1: abcd
    s2: abcd
    
    ここでは、s1のアドレスは末尾が4bです。一方s230です。なので、s2s1よりずっと前の位置にあります。なので、s1を無理やりs2にコピーした結果、本来のs2の確保領域であるcharが2つ分を超えて、abcdおよびNULL文字の5文字分コピーしてしまいます。なので、s2"abcd"になります。さて、パターンBの場合はどうでしょうか。
    initial s1:
            address: 0x7fffcd02eb5b
            print by char: s[0] = a, s[1] = b, s[2] = c, s[3] = d, s[4] = ,
    initial s2:
            address: 0x7fffcd02eb59
            print by char: s[0] = , s[1] = , s[2] = a, s[3] = b, s[4] = c,
    after s1:
            address: 0x7fffcd02eb5b
            print by char: s[0] = c, s[1] = d, s[2] = , s[3] = d, s[4] = ,
    after s2:
            address: 0x7fffcd02eb59
            print by char: s[0] = a, s[1] = b, s[2] = c, s[3] = d, s[4] = ,
    s1: cd
    s2: abcd
    
    ここでは、s15bs259です。よって、s2s1の直前に配置されていることがわかります。図示すると次のような形です。

    • 0x7fffcd02eb59: s2[0] = ???
    • 0x7fffcd02eb5a: s2[1] = ???
    • 0x7fffcd02eb5b: s1[0] = 'a'
    • 0x7fffcd02eb5c: s1[1] = 'b'
    • 0x7fffcd02eb5d: s1[2] = 'c'
    • 0x7fffcd02eb5e: s1[3] = 'd'
    • 0x7fffcd02eb5f: s1[4] = '\0'

    s2の中身は未定義です。ここで、s1の中身を無理やりs2にコピーしようとするので、\0が出現するところまでコピーします。その結果が以下になります。

    • 0x7fffcd02eb59: s2[0] = 'a'
    • 0x7fffcd02eb5a: s2[1] = 'b'
    • 0x7fffcd02eb5b: s1[0] = 'c'
    • 0x7fffcd02eb5c: s1[1] = 'd'
    • 0x7fffcd02eb5d: s1[2] = '\0'
    • 0x7fffcd02eb5e: s1[3] = 'd'
    • 0x7fffcd02eb5f: s1[4] = '\0'

    上記のように、s2の境界を超えて次のメモリ領域であるs1の中身まで変えてしまいました。なので、これを表示するとs1"cd"になり、s2"abcd"になります。

  • 写経スラッシュプログラムで、最後にスラッシュがあるのはなぜ?つまり、「a/b/c/d」ではなく「a/b/c/d/」となるのはなぜか?

    • 最後に改行があるからです。'a' / 'b' / 'c' / 'd' / '\n' こうなっているので、「a/b/c/d/」に見えます (編集済み)
  • パイプをしながら順次計算結果を別ファイルに出力するということをしたいのですが、echo 123456789 | ./a.out > outrepeat1.dat | ./a.out < outrepeat1.dat > outrepeat2.dat | ./a.out < outrepeat2.dat > outrepeat3.dat だとうまくいきませんでした。(outrepeat2, 3に全く出力されない) どうすればよいでしょうか。

    • ./a.out > out.dat | ./a.out この時点で、一つ目のa.outの出力はout.datになっており、二つ目の./a.outに情報が入力されていないと思います
  • 宿題は点数を表示した後、getcharの入力を続けますか?それともプログラムを終了させますか?

    • どちらでもOKです。自動採点でOKになる限りなんでも大丈夫です。
  • 課題3についてですが、合っているisとaの部分は、どのような理由で得点に入れないのでしょうか?

    • 答えのiは先頭から3字目ですが、Tisの場合は2字目になっています。なのでダメ、という定義です
  • int n = c - ‘0’として数字そのものが得られるのは何故でしょうか?

    • 数字の「文字」の実態は、ただの数です。そして、連番になっています。すなわち、
      '0'は実は48です
      '1'は実は49です
      '2'は実は50です
      '3'は実は51です
      ...
      
      なので、次の
      int n = c - '0'
      
      を考えるということは、
      int n = c - 48
      
      を考えるということと同じです。なので、例えば c = '3' のときは、
      int n = '3' - 48
      
      であり、これは
      int n = 51 - 48
      
      と同じことです。なので、n には数字文字が表す実際の数字(ややこしい)である 3 が入ります。
  • 「Commit changes」の次の記入欄に書くコメントを書き間違えてしまったのですが、修正する方法はありますか?もう一度同じ画面で書いても、下の緑の「commit changes」が反応してくれません。

    • git cloneというコマンドでリポジトリをローカルにダウンロードしたあと、git commit 取り消しなどでググると出てくる内容を実行すれば修正できますが、変なことが起こる可能性も高いので今の段階ではあまりおすすめしません。コミットメッセージは別にあってもなくても大丈夫なので、気にせず新しいコミットをどんどん追記していけば大丈夫です
  • 文字について質問させていただきます。int型はchar型よりバイト数が大きいだけで、char型だけでなくint型も引数に文字を取ることができる(文字を数字として認識している)ということでよろしいでしょうか?

    • int型の引数は文字を受け取ることができる これはその通りです
    • 文字を数字として認識している ここは正確に言うと、「文字を数字として認識」といった概念はないです。文字「'a'」は、単に数字「97」と全く同じというだけです。なのでintに限らず数字を表す変数ならなんでも 97 であれば%cでprintすると 'a' になります
      unsigned char uc1 = 'a';
      unsigned char uc2 = 97;
      printf("%c %d\n", uc1, uc1);  // a 97
      printf("%c %d\n", uc2, uc2);  // a 97
      
      long l1 = 'a';
      long l2 = 97; 
      printf("%c %d\n", l1, l1);  // a 97
      printf("%c %d\n", l2, l2);  // a 97
      
  • int x[] = “This is a pen”;ではエラーが出て、char x[] = “This is a pen”;ではエラーが出ませんでした。intも配列を取れると思うのですが、この理由がわからないので教えていただきたいです。 -「文字列」という場合charの配列を指します。intの配列じゃダメです。intで文字を受け取れると言っていたのは、文字(=単なる整数)の部分の話であり、文字列(=charの配列)とは違います。

  • NUll文字についてなのですが、getchar関数のwhileループを抜ける時、c = getchar();と書いた後、

    if(c == '\n') {
        break;
    }
    
    と書くとループを抜けることができるのですが、
    if(c == '\0') {
        break;
    }
    
    だとループを抜けることができません。文字列の最後にはNULL文字がつくと思いますが、何故後者ではループを抜けられないのでしょうか?

    • キーボードから入力を受け付けているときは、たとえばa, b, c, enter, と打った場合、単にa, b, c, \n が入ってきます。これは文字列ではないです。なので、最後に暗黙的に\0が入ることもありません。

Week4

  • 32 bit PCだったころはどうやってポインタかどうか見極めていたのでしょうか

    • sizeofではわからないです。もし混乱するような状況なら普通はデバッガというのを使います
  • 前から疑問に思っていたことを質問させてください。各メモリにアドレスがあることはわかるのですが、それをアドレス演算子などで得る場合、どのように参照しているのでしょうか?1バイト毎に別にアドレス用の8バイトを確保しているというわけではないですよね?うまく言葉にできないのですが、図に灰色で書かれているようなアドレス(住所)はどのように保存されているのでしょうか?という意図です。

    • 「アドレスそのもの」は、「メモリ」のように領域が確保されているわけではないです。3年生向けの坂井先生のコンピュータアーキテクチャの講義第二回のスライドの12pに、実際にフィジカルな素子としてメモリがどのように構成されているかが載っています。これを見るとわかる通り、「アドレスを指定してメモリ領域を指定する」とは、イメージとしては回路に信号がやってきて回路がメモリ住所を決定し、そこにアクセスして中身を読む、というようなものです。ここでは「アドレスそのもの」が容量を占めているわけではありません。
    • アドレスをポインタに読み込むと、当然ながら、ポインタの値という形でメモリを消費します(64 bit)
  • 宣言の順番として

    int a = 10;
    int *p;
    
    と書いたのに,&a0x7ffee638d558,&p0x7ffee638d550と,pのアドレスのほうが小さな番号になっているのはなぜでしょうか。ちなみに,宣言の順番を変更しても,pのアドレスのほうがaのアドレスより小さな番号でした。

    • これはコンパイラが最適化して決めるので、そういうことはあります(変数を書いた順にメモリが確保されるというルールはないです)
  • ポインターp自体のアドレスも扱うことは多いですか

    • 次週やるような、多次元配列を関数にポインタとして渡すときとかは使います
  • %pで出力できるのはなんですか?

    • アドレスです
  • コンピュータのメモリを増設することで、プログラミングで使えるメモリの領域も増えるのでしょうか。また、32bit版のwindowsではメモリが4GBまでしか使えないと聞いたことがあるのですが、どのような理由による制限なのでしょうか。

    • 64btiコンピュータでは、64bitを超える範囲は無理です。ですが、それは広大なので、今のところは問題ないです。 32bitコンピュータでは、アドレスが2^32 = 4.2 * 10^9 ~ 4G個の住所しか扱えませんでした。1住所1byteなので、つまり仮に32bit全てを使ったとしても4GB程度しかメモリを表現できなかったのです。
  • int a = 10;
    int *p = &a;
    int *q = &p;
    printf(p: %p\n, p);
    printf(p: %p\n, *q);
    
    の両者が異なるのは何故でしょうか

    • int *q = &p; ここで、intのアドレスではなく、intポインタのアドレスをいれてしまっています
  • クイズの例で言えば配列を作る際に

    int length = 5;
    int array[length] = {7, 2, 3, 4, 5};
    int max_val, min_val;
    max_min(array, length, &max_val, &min_val);
    
    としたくなるのですが、コンパイルエラーが出ます。「可変な変数だとダメです」みたいなメッセージが出るのですが、これはどうしようもないのでしょうか。それとも、定数のようなものがあるのでしょうか?

    • 可変長配列では初期化の記法はダメだからです。week6でやります。去年の資料のこのあたりのコラムを参考。
  • %p%xではポインタに対して表示が変わりますが、これはどう理解したら良いのでしょうか? ポインタが保持している値もアドレスであるはずなので%x%pも同じにならないのが疑問です。また「cの絵本」では%xを使っていました。

    • %x%pで表示が違う理由は,%xがunsigned int(普通4バイト)用であり,ポインタの値が4バイトに入らないためにオーバーフローを起こしているからだと思われます。%xの代わりに%lxを使った場合には(頭の0xの有無を除いて)一致しました
  • pmsgにnullptrを入れたときメモリリークは起きないんですか

    • 起きないです
  • Strlen3()のところで引数にconst char *sを取っているのに関数内でsの値が変更できている(s++できている)のはなぜですか?

    • s自身は動かしていいんですが、sの指す値は変更できない、という意味です
  • char* p = "abc";
    p = "def";
    
    とするとエラーは起こりますか

    • 起きないです。ポインタのことを全て忘れると、単純に新しい文字列を割り振っただけです。文字列リテラルの処理はコンパイラがやってくれます
  • 自由にポインタの値(アドレス)を動かせるということは、走らせているプログラムとは関係ないアドレス(OSの重要なデータが保存されている部分や個人のファイルの部分など)に書き込んだりすることができてしまうのでしょうか。

    • 普通は大事なところは触れないようになっているのですが、頑張って悪さをすると触れることもあるかと思います
  • int strlen2(const char *s) {
      int n = 0;
      for (; *s; s++) {
          n++;
      }
      return n;
    }
    
    このコードでfor文中の *sはどう解釈されますか

    • *s != ‘\0’といっしょです。すなわち、*s != 0といっしょです。このstatementは*s0でないときは1, *s0ときは0なので、それは結局単に*sと書くのと同じことになります
  • 質問ではないのですが、答えのmy_strcpy3について警告が出ていました。以下のようにwhileの評価部分をさらに()でくくると消えました

    void my_strcpy3(char *s1, const char *s2) {
        while ((*s1++ = *s2++)) {
            ;
        }
    }
    

    • 出るかもしれないです
  • ポインターpそのもののアドレスが存在するということは例として int a = 4をするときメモリーがint分の4ビットのメモリだけではなく実際その2倍のメモリーを使うということでしょうか。ポインターpそのもののアドレスという概念が少し理解しにくいです

    • いえ、「メモリを消費」するのは、intの場合は4byte, pointerの場合は8byteだけです。それぞれに、位置を示す住所である「アドレス」があるというだけです。アドレスそのものは、位置を示す番地にすぎないので、「メモリを消費」ということはありません
  • 文字列の初期化でchar *pmsg = “abc”;としてありますが、ポイントには文字列ではなく、文字列のアドレスが入れられるのではないでしょうか?

    • そうです。
  • "Abc"は文字列自体ではないのでしょうか?

    • "Abc"そのものは、'A', 'b', 'c', \0がどこかに確保された配列の、先頭のアドレスを指します。
  • pmsgはポインタなので、文字列自体を出すにはprintf(“%s \n”, *pmsg);としなくて良いのでしょうか?

    • 大丈夫です。実は、%s は、アドレスを受け取り、\0までの部分を表示するというものなのです
  • week1の質問かもしれませんが,メモリのアドレス1つに対して1バイトしか保存しないのは理由があったりするんでしょうか?

    • これは逆で、現代のコンピュータは1バイトを一つの単位として扱っているので、なので1バイトごとにアドレスがある、ということです。
  • #include<stdio.h>
    
    void swap (double *a, double *b) {
        double *tmp = a;
        *a = *b;
        *b = *tmp;
    }
    
    int main() {
        double a[] = {1.0, 5.0, 3.0, 4.0, 2.0, 6.0};
        swap (a + 1, a + 4) ;
        for (int i = 0; i < 6; i++) {
            printf("a[%d]:%f, ",i,a[i]);
        }
        printf("\n");
    }
    
    クイズのswap関数についてなのですが、double tmp = a; b = tmp;と書くところを上記のように書くとa[4]が5.0にならず2.0になってしまいます。上記の書き方でもできるのではないかと思ったのですが、何故でしょうか?

    • これだと、double *tmp = aのときに、tmpの中身は、ポインタであるaが指しているものが入ります。すなわち、元の配列の前から2つめ(5)の「アドレス」です。次の*a = *bで、aが指しているもの、すなわち元の配列の前から2つめ(5)の値を、2に書き換えています。ここで、tmpもまた元の配列の前から2つ目を指していたので、自動的に、*tmpも2になります。
  • 最後のクイズについて質問なのですが、str1が{‘h’, ‘o’, ‘g’, ‘e’, ‘\0’, ‘a’, ‘a’, ‘a’, ‘a’, ‘a’, ‘\0’}となっていますが、‘\0’以降はprintされないということでしょうか?

    • そうです。char配列に対し、最初に\0が出てくるところまでが、validな文字列として処理されます。
  • また、while (*s1++ = *s2++)とありますが、後ろにインクリメントがついているから、++が適用される前のs1=s2も計算されているという認識であっていますでしょうか?

    • そうです。

Week5

  • 前回の課題なのですが、間違えてsucceededという文言が出るようにしたままにしてしまい、自動採点が落ちてしまっていました

    • これは一律で減点になってしまいます(全員にそうしています)
  • ポインタのポインタを使った関数にはどのような利点があるのですか?

    • ポインタ配列を扱うときは必須になります。また、「main側のポインタ」を「関数内から変更したい」ときは、ポインタのポインタが必要になります
  • char *hokuriku1[3]がポインタ配列で、その中身がs1,s2,s3なのはわかったのですが、例えばprintf(“%s\n”,hokuriku1[1])でIshikawaと表示されるのはなぜですか? なんで、*hokuriku1[1]じゃないのかがわかりません。

    • たとえば次を考えてみてください。int型の配列のint arr1[]の中身を表示する場合は、*arr1[1]ではなくarr1[1]としますよね。この議論を拡張して、char *型の配列であるchar *arr2[]の中身を見るときは*arr2[1]ではなくarr2[1]でOKです。
      int x = 10;
      int arr1[3] = {1, 2, 3};
      printf("single var    : %d\n", x);
      printf("second element: %d\n", arr1[1]);
      // arr1[1]を見るためには*arr1[1]ではない
      
      char* s = "aa";
      char* arr2[3] = {"bb", "cc", "dd"};
      printf("single var    : %s\n", s);
      printf("second element: %s\n", arr2[1]);
      // arr2[1]を見るためには*arr2[1]ではない
      
      また、printf%sは、文字列の先頭アドレスを受け取ります。
  • %sがアドレスを受け取って、そのアドレスのものを表示するという認識であってますか

    • はい、受け取ったアドレスから始めて、\0が出てくるところまでを表示する、という認識で大丈夫です。次を参考にしてください。
      char s1[] = "abcdef";
      printf("%s\n", &s1[2]); // "cdef"
      printf("%s\n", s1 + 2); // "cdef"
      
      char s2[] = {'x', 'y', 'z', '\0', 'a', 'b'};
      printf("%s\n", s2 + 1); // "yz"
      
  • 今日の授業の内容ではないですけれども最終評価はレポートですか試験ですか?

    • レポート(プログラミング課題)の予定です
  • week5_1で重複するものは一回だけ変換していいということですか?(bc >>  yy)の例でabcabcのbcが一回しか変わらないので質問します

    • そうです
  • 宿題について、Cloud Shell上で出力してみたら正しい答えが得られていると思っているのですが、自動採点でこの問題だけ最後に異物がくっついて不正解になってしまいます。

    • これはプログラミングを間違えている可能性が高いです。例えば、配列中に適切にNULL文字が設定されていないため、自動採点側では最後に変なことが起きているかもしれません。cloud shellでは偶然配列初期化時にゼロが詰められていてうまく表示されているだけの可能性があります。
    • 例えば配列を確保時に全要素を a にしてみてください。それで最後までやってみると、おそらく変な挙動であることが確認できます。多くの場合、NULL文字の付け忘れだろうと思います。配列を初期化しないで使う場合、その初期値は未定義なので、初期値が何であれうまくいくプログラムを書く必要があります。
    • 配列を全部0に初期化すると自動的にうまくいく可能性がありますが、それが本当に自分が意図しているかどうかよくチェックしてみてください。つまり、本来は明示的にNULL文字をつけるべきなのにそれを忘れていて、初期化の0のおかげで偶然助かっているだけかもしれません(その挙動が意図するものなのであればOKです)
    • 以下のようなコードで確認できます。
      #include <stdio.h>
      #define MAX_LEN 5
      
      void fill(char s[], int c) {
          // sをcで埋める
          for (int i = 0; i < MAX_LEN; ++i) {
              s[i] = c;
          } 
      }
      
      void sanity_check(char s[]) {
          // 明示的にsの中身を確認
          for (int i = 0; i < MAX_LEN; ++i) {
              printf("[%c/%d], ", s[i], s[i]); 
          }
          printf("\n");
      }
      
      int main(int argc, char *argv[]) {
          char s[MAX_LEN];
          sanity_check(s);   // [/0], [/0], [/0], [/0], [/0],   偶然0詰めされた場合、左のようになる
          printf("%s\n", s);  // 偶然0詰めされているので、printfすると何も表示されない(一文字目がNULL文字という扱いになる)
      
          fill(s, 'a');     // !!! あとでこの行をコメントアウトして再度実行すると、全部うまく表示されてしまう。fillされていてもうまくいくようなアルゴリズムにするべき。 !!!
      
          sanity_check(s);   // [a/97], [a/97], [a/97], [a/97], [a/97],  左のようになる
          printf("%s\n", s);  // NULL文字が無いので、最後がおかしなことになる   aaaaaP��U
      
          // ここからアルゴリズムを書き始めて、正しく動くかチェック。必要に応じてsanity_check
      
          // 例えばsを文字列として適切に扱うためには、ちゃんとNULL文字を足す必要がある。
          s[0] = 'x';
          s[1] = 'y';
          s[2] = 'z';
          sanity_check(s);   // [x/120], [y/121], [z/122], [a/97], [a/97],
          printf("%s\n", s);  // これはダメ。NULL文字がないので。 xyzaap"�V
      
          s[3] = '\0';
          sanity_check(s);   // [x/120], [y/121], [z/122], [/0], [a/97],
          printf("%s\n", s);  // これでOK。xyz
      }
      

Week6

Week7

  • 暗号系の実装など、本質的に密行列になってしまう行列(例: 乱数の行列)でも疎行列として扱う手法などは存在するのでしょうか?

    • わからないです。ですが「巨大な密行列は大変」は常に真です
  • 今回クローン時にユーザ名やアクセストークンの入力を求められなかったのですが、今までに入力したことがあるからということなのでしょうか。

    • 色々なパターンがあると思うのですが、過去にやった設定が保存されていると、入力しなくてもいいかもしれません
  • パスワードを入力、コピペができません、原因が分かりません

    • 普通にCTRL + V で出来ませんか?ちょっとわかりにくいです
  • パスワードは入力しても空欄のままですか?

    • そうです。「CTRL + V」 からの 「エンターキー」です
  • Support for password authentication was removed on August 13, 2021. Please use a personal access token instead.なる文が出るのですがパーソナルトークンを再びコピーするにはどうすればよいのでしょうか

    • パーソナルトークンをちゃんとコピペして保存しましたか?その内容をコピペすればできるはずなのですが・・
  • passwordを打った後remote: Repository not found.と出てしまったのですが。。。

    • 「先ほどのremote: Repository not found.と出てしまった件ですが、tokenを作ったときにrepoにチェックを立てるのを忘れていたのが原因でした」 了解です
  • Codespacesについて:Githubがホスティングしている、レポジトリ単位の仮想環境が数コマンドで作れるということでしょうか!?

    • そうです
  • Git logから抜けるにはどうすれば良いでしょうか?

    • 「q」を押します
  • git hubにローカル(VScode)からpushして変更した時に、git hub上での変更履歴の表示が自分のgithubアカウント名になっていないことに気づいたのですが、問題なく提出されているのでしょうか。課題1〜7も課題8もそのようになっていました。

    • ローカルマシンのgitのメールアドレス設定に、github登録アドレスとちがうアドレスが登録されているとそういう形になります。~/.gitconfig の中身を確認・修正してみてください。課題8以外は、自分自身しか宿題リポにアクセスできないので、そもそも問題はないです。課題8では、もし自分の名前が表示されていなければ誰がコミットしたのかわからないので、自分の名前が表示されるようにしておいてください。参考リンク