コンテンツにスキップ

Week 1 (2021/10/7)

今日やること

  • Google Cloud Shell Editorを用いた環境構築
  • ターミナルおよびコマンドの解説
  • コンパイルの実行
  • 基本のデータ型
  • GitHubの準備

プログラミングとは

本講義では、C言語を題材に、プログラミングの基本の基本を勉強します。 WindowsやMacといったOS、FirefoxやEdgeといったブラウザ、ポケモンといったゲームなど、 世の中の全てのソフトウェアは、プログラミングという作業によって作られています。 プログラミングを習得することで、皆さんはコンピュータ上で自由にデータを処理したり、 計算を行ったり、検索エンジンを作ったり、ゲームを作ったりすることが出来るようになります。

プログラミングは、現実の問題を解決するために必須の技術です。 皆さんはこれから先、研究という活動を通じて現実の問題を解くことになります。 例えば、もしあなたが電子回路の研究をするなら、 ある回路素子の電圧値を継続的に測定し、そのデータから数学的な関係を導出したいかもしれません。 その際、データをまとめて関数をフィッティングする作業は、プログラミングによって実現されます。 もしあなたが情報系の研究をするなら、カメラで撮った画像をまとめて、物体の三次元形状を復元するかもしれません。 そのために線形方程式を解くのなら、それはプログラミングによって行います。 このように、現実の問題を解く上で、プログラミングは必ず必要になります。 それは研究だけに限らず、この先みなさんが直面するありとあらゆる問題について、プログラミングは重要な役割を果たします。 特に本講義では電気系の最初の情報系講義なので、どの分野でも必要となる基本的な内容を教えます。 なるべくわかりやすく解説しますので、難しければいつでも質問してください。

C言語

世の中には様々なプログラミング言語がありますが、本講義ではC言語を扱います。 皆さんは教養の講義で既にPythonという言語に触れたと思います。PythonはCと比較すると高レベルな言語です。 逆に、CはPythonに比べると低レベルな言語です。ここで高いとか低いと言っているのは、どちらが簡単だとかいうわけではありません。 より低レベル(低水準、定レイヤ)であるとは、よりコンピュータのハードウェアに近いレベルを容易にコントロールすることが出来る、という意味です。 具体的に言えば、コンピュータのメモリなどに直接アクセスできるのがC言語です。 よって、メモリのレイアウトを意識して高速なコードを書いたりすることができます。 逆にPythonでは、そういうローレベルな作業は隠蔽されており、通常はユーザは意識しません。 Pythonに比べると、Cは融通が利かず、コード分量が多くなり、また難しいという印象を持つと思います。 しかし、Cを理解することは、 コンピュータの内部の動作の本質を理解することにつながります。 また、同じ処理を行うとき、ローレベルな言語のほうが性能が出る場合が多いです(高速、低メモリ消費)。

C言語を習得すれば、ほかの言語を学ぶことは簡単です。 特に、本講義の最大の目的であるポインタという概念を理解すれば、他の言語を学ぶことは怖くありません。 本講義では、全員がポインタを理解することを目的とします。 世の中の言語には流行廃りがありますが、 C言語を取得することの重要性は松井が学生のころから今まで全く変わっていません。

Google Cloud Shell Editorによるオンライン環境構築

それでは早速始めましょう!まずは環境を構築します。例年であれば皆さんは情報教育棟で備え付けのコンピュータでプログラミングをしてもらうのですが、 今年はコロナウイルスの影響で情報教育棟が使えません。 よって、各自の環境をそろえることが出来ないため、ブラウザベースでプログラミング環境を構築します。

  1. まずはECCSアカウントあるいは個人gmailにログインしてください(googleのサービスを使います)
  2. Google Cloud Shell Editor というサービスを利用します。このリンクをクリックしてください。
  3. 1分ぐらい待ちます。すると画面が立ち上がります。
    • もし画面の字が赤くなり、処理が止まっているように見える場合は、リロードしてください。気軽にリロードして大丈夫です。
  4. この状態ではまだどのフォルダも開いていない初期状態です。まずは、開始位置のフォルダを開きます。左上の紙が二枚重なっているアイコンをクリックし、「Open Folder」を選択し、自分のユーザ名のフォルダを選択してください。参考:プログラム演習の長谷川先生の資料
  5. すると、以下のような画面になります。今後、この画面でプログラミングを行っていきます。

上のような画面に辿り着くはずです。

  • ここで、左側は現在開いているフォルダの中身を表示しています。windowsでいうエクスプローラ、MacでいうFinderです。 例えばここで右クリックして新しくファイルを作れます。ここでは、「main.c」というファイルを一つ作ってみましょう。
  • 右側の画面はエディタです。windowsでいうメモ帳です。左側で「main.c」をクリックすると、この右側のエディタ画面ではそのmain.cの中身が表示されています。 このエディタ画面でファイルを編集していきます。
  • 下側はターミナルというものです。ここにコマンドを打ち込み、様々な処理を行います。

コラム

さて、上記のGoogle Cloud Shell Editorは一体何を行っているのでしょうか? ここでは、イメージとしては、Google社がどこかに用意しているサーバ上に、仮想的にコンピュータを立ち上げています。 そして、その仮想コンピュータに、ブラウザからアクセスしていると思ってください。 詳しくは、オンライン環境構築のページを見てください。

やってみよう(3分)

Google Cloud Shell Editorにアクセスし、上記の画面に辿り着きましょう。上記の指示に従い、main.cというファイルを作り、適当にファイルを編集してみましょう。

ターミナルとコマンド

次に、ターミナルとコマンドについて勉強しましょう。 ターミナルとコマンドは、プログラミングを行ううえで避けては通れない概念です。 ターミナルというのは処理を受け付けたり結果を表示する画面です。そこにコマンドという命令を書き込むことで、処理を実行します。 普段みなさんがコンピュータ上でファイルを操作したりアプリケーションを扱うときは、 マウスを使ってポインタを動かしたり、アイコンをクリックすると思います。このように、視覚的に物事を操作する仕組みをGraphical User Interface (GUI)といいます。 一方で、マウスを用いず、キーボードによるテキスト入力のみで処理を行う方式を、Command-line Interface (CLI) あるいはCharacter-based User Interface (CUI)と言います。 CLIでは、ターミナルにコマンドを打ち込むことで様々な処理を行います。

世の中には非常に多くのコマンドがあります。今日は基本となる以下のコマンドを勉強します。これらは基本の基本なので、何も考えなくても入力できるように覚えてしまうとよいです。

  • ls: List. ファイル一覧の表示
  • pwd: Print working directory. 現在の位置を表示
  • cd: Change directory. ディレクトリの移動
  • cat: Concatenate. ファイルの中身の表示
  • touch: ファイルの生成
  • mkdir: Make directory.ディレクトリの作成
  • mv: Move. ファイルの移動
  • cp: Copy. ファイルのコピー
  • rm: Remove. ファイルの削除

さて、まずターミナルを起動してみましょう。次のようなものが表示されていると思います。

matsui528@cloudshell:~$
ここで、

  • matsui528@cloudshellの部分は、matsui528というユーザ名でcloudshellというマシンにログインしていることを意味します。皆さんは自分のユーザ名になっているはずです。
  • ~と言うのは、そのユーザの「ホーム」の位置にいるということを意味します。後で説明します。
  • $というのが、ターミナルの先頭をあらわすサインのようなものです。

このターミナルに色々入力していくわけですが、今後は簡単のため上記を

$
とだけ表記します。

ls

lsとは、現在のフォルダの中にあるファイルを表示するコマンドです。「list」の略です。 ターミナルに以下を入力してください。

$ ls
全角ではなく、半角英数字で入力してください。そして、エンターキーを押してください。すると、以下のようなものが表示されます
main.c  README-cloudshell.txt
さて、左側のウインドウを見るとわかる通り、現在注目している位置のフォルダの中にはmain.cREADME-cloudshell.txtしかないため、その2つが表示されます。 ちなみに、現在注目しているフォルダのことを「カレントディレクトリ」と呼びます。 「ディレクトリ」というのはwindowsでいう「フォルダ」と同じ意味です。「現在のフォルダ」という、そのままの意味ですね。

pwd

次に、カレントディレクトリの位置を表示するコマンド、pwdを実行してみましょう。Print working directory の略だそうです。

$ pwd
すると以下のようなものが表示されます(ここの表示内容は、各自の環境によって違います)
/home/matsui528
ここで、一番最初の/(スラッシュ)は、このコンピュータの最も「上」の位置を表します。そこにhomeというディレクトリがあります。 その下にmatsui528というディレクトリがあり、自分は現在そのmatsui528の中にいます。 なので、現在のカレントディレクトリに配置されているmain.cというファイルの位置は、正確に書くと

  • /home/matsui528/main.c

と書けます。このような指定の方式を絶対パス指定と呼びます。これは、/日本/東京/文京区/本郷/IZASAのように地名を正確に指定していることに相当します。 一方で、カレントディレクトリは.(ピリオド一個)で表すことが出来ます。これを用いると、上記のmain.cの位置は次のようにも書けます

  • ./main.c (カレントディレクトリの下のmain.cという意味)

この表記を相対パス指定と呼びます。この表記は一意には定まらない(カレントディレクトリに依存する)のですが、簡単にファイルの位置を表現できます。 今いる場所/IZASA という意味です。本郷のラーメン屋の議論をするとわかっている際は、/日本/東京/... などと書く必要がないので、相対パスのほうがわかりやすい、ということです。

cd

次に、ディレクトリ間を移動するコマンド、cdをやってみましょう。Change directoryの略です。 cd 移動先 で移動出来ます。 ここで、..(ピリオド二つ)は、「カレントディレクトリから一個上のディレクトリ」を 表します(../でもいいです)。そこに移動してみましょう。

$ cd ..
lsをを打ってカレントディレクトリのファイルを確認してみましょう
$ ls
lost+found  matsui528
一個上にあがったので、先ほどまでカレントディレクトリだったmatsui528が見えますね。また、lost+foundという、ゴミ箱を管理するディレクトリもあるようです。 また、現在位置も確認してみましょう。
$ pwd
/home
一個上にあがったことがわかりますね。

コラム

ちなみに、ここでターミナルの最初の部分が以下のようになっていると思います。

matsui528@cloudshell:/home$
ここで/homeの部分は、今自分がいる位置、すなわちpwdの結果と同じになっていると思います。ここはターミナルが気を利かせて現在位置を表示してくれています。

ここから、今度は一段下って、最初の位置に戻りましょう

$ cd matsui528
こうして再度lspwdをすると、最初の位置にもどってきたことがわかります。 ちなみに、上のような長いコマンドは全て入力する必要はありません。 ある程度入力したあとにTABキー(Tab)を入力することで、自動補完されます。 すなわちここでは、c, d, スペース、m, a, 程度を打ったあとにTABキーを押すことで、残りが自動的に入力されます (C+D+Space+M+A+Tab)。あるいは、c, dを押したあとにTABを二度押すと候補がサジェストされるので、その後mを打ってTAB・・・という風にもできます (C+D+Space+Tab+Tab)。 このTab補完は非常に便利なだけではなくタイプミスを減らすので、覚えておいてください。

ここで、左側のファイルビューアの画面は、自分の現在位置(pwdで表示される位置)に関わらず、一番最初に開いたフォルダがずっと表示されています。

やってみよう(5分)

上記のコマンドを実行し、ls, cd, pwdを使ってディレクトリを色々移動してみましょう。

また、以下はよく使う特殊な表記になります。これらも実行してみましょう。また、これらは覚えておきましょう。

  • $ cd .. 一個上に移動
  • $ cd . 自分が現在いる位置(カレントディレクトリ)に移動。つまりどこにも移動しない。(これを使うことは無いが、意味は知っておく)
  • $ cd / コンピュータの一番上の位置に移動
  • $ cd ~ 「ホーム位置」に戻る。すなわち、松井の環境だと$ cd /home/matsui528と同じこと。
  • $ cd 何もつけないと、$ cd ~と同じこと。すなわち「ホーム位置」に戻る。
  • $ cd - 「直前の位置」に戻る。例えば、$ cd /etc/emacsしたあとに$ cd /opt/mavenしてから$ cd -すると、/etc/emacsに戻る。

cat

次に、ファイルの操作に関するコマンドを見ていきましょう。まずは、ファイルの中身を表示するコマンド、catを見ていきましょう。 これはconcatenateの略だそうです。cat 対象ファイル名 で、対象ファイルの中身を表示します。

まず、左側のファイルビューアからmain.cをクリックして、右側のエディタで適当に文字列を入力してください。例えば次のようにするとしましょう。

xxxx
yyyy

そのうえで、cd ~として初期のカレントディレクトリに移動した上で、以下のコマンドを実行します

$ cat main.c
すると以下のような出力になります。
xxxx
yyyy
これは、main.cの中身をターミナル上で表示した、ということです。このようにして、エディタ(メモ帳)を開かずとも、ターミナル上でファイルを確認することができます。

同様にして、全然別の場所にあるファイルも見てみましょう。

$ cd /usr/include
こうすると、最上位階層である/からはじまり、usrというディレクトリの中の、includeというディレクトリに移動します。ここでlsをするとたくさんのファイルがあることがわかります。
$ ls
aio.h         bzlib.h      cursslk.h   expat_external.h  gdbm.h          iconv.h     libmount  mntent.h       neteconet   paths.h             pthread.h   rpc          sound          sudo_plugin.h  threads.h   values.h
aliases.h     c++          dbus-1.0    expat.h           getopt.h        ifaddrs.h   limits.h  monetary.h     netinet     pcrecpparg.h        pty.h       rpcsvc       spawn.h        syscall.h      tic.h       video
alloca.h      clang        dirent.h    fcntl.h           gio-unix-2.0    inttypes.h  link.h    mqueue.h       netipx      pcrecpp.h           pulse       ruby-2.5.0   sqlite3ext.h   sysexits.h     time.h      wait.h
argp.h        complex.h    dlfcn.h     features.h        git2            iproute2    linux     mtd            netiucv     pcre.h              pwd.h       sched.h      sqlite3.h      syslog.h       ttyent.h    wchar.h
argz.h        cpio.h       elf.h       fenv.h            git2.h          jerror.h    locale.h  nc_tparm.h     netpacket   pcreposix.h         python2.7   scsi         stab.h         szlib.h        uchar.h     wctype.h
ar.h          crypt.h      endian.h    fmtmsg.h          glib-2.0        jmorecfg.h  lzma      ncurses_dll.h  netrom      pcre_scanner.h      python3.7   search.h     stdc-predef.h  tar.h          ucontext.h  wordexp.h
arpa          ctype.h      envz.h      fnmatch.h         glob.h          jpegint.h   lzma.h    ncurses.h      netrose     pcre_stringpiece.h  python3.7m  selinux      stdint.h       termcap.h      ulimit.h    X11
asm-generic   cursesapp.h  err.h       form.h            gmpxx.h         jpeglib.h   malloc.h  ncursesw       nfs         php                 rdma        semaphore.h  stdio_ext.h    term_entry.h   unctrl.h    x86_64-linux-gnu
assert.h      cursesf.h    errno.h     fstab.h           gnumake.h       langinfo.h  math.h    net            nl_types.h  poll.h              readline    sepol        stdio.h        term.h         unistd.h    xen
avahi-client  curses.h     error.h     fts.h             gnu-versions.h  lastlog.h   mcheck.h  netash         nss.h       postgresql          re_comp.h   setjmp.h     stdlib.h       termio.h       utime.h     yaml.h
avahi-common  cursesm.h    eti.h       ftw.h             grp.h           libaec.h    memory.h  netatalk       obstack.h   printf.h            regex.h     sgtty.h      string.h       termios.h      utmp.h      zconf.h
blkid         cursesp.h    etip.h      gconv.h           gshadow.h       libgen.h    menu.h    netax25        openssl     proc_service.h      regexp.h    shadow.h     strings.h      tgmath.h       utmpx.h     zlib.h
byteswap.h    cursesw.h    execinfo.h  gdb               hdf5            libintl.h   misc      netdb.h        panel.h     protocols           resolv.h    signal.h     stropts.h      thread_db.h    uuid
ここで、stab.hというファイルの中身を見てみましょう。
$ cat stab.h
すると、次に示す、stab.hの中身が表示されるかと思います。
#ifndef __GNU_STAB__

/* Indicate the GNU stab.h is in use.  */

#define __GNU_STAB__

#define __define_stab(NAME, CODE, STRING) NAME=CODE,

enum __stab_debug_code
{
#include <bits/stab.def>
LAST_UNUSED_STAB_CODE
};

#undef __define_stab

#endif /* __GNU_STAB_ */
このように、catコマンドを用いてターミナルからファイルの中身を表示することが出来ます。

ちなみに、先ほどは「ディレクトリを移動してから表示」を行いましたが、これはファイルを絶対パスで指定するのも同じことです。 また、現在位置から相対位置で指定することもできます。 以下の3つは同じファイル内容を表示します。

  • cd /usr/include してから cat stab.h (ディレクトリを移動してから実行)
  • 任意の位置からcat /usr/include/stab.h (絶対パスで指定して実行)
  • 例えばcd ~とした上で、cat ../../usr/include/stab.h(相対パスで指定して実行)

ここでは「二階層上」を示す方式として../../を使っています。例えばcd ../../ とすると二個上にあがること意味します。

コラム

ちなみに、ターミナルに表示された情報が増えすぎて上のほうに流れていってしまい見えなくなってしまった場合、ターミナル上でマウスのホイールをスクロールすることで以前表示した情報を確認できます。 あるいは、ターミナル画面の上部左にある歯車マークを押して「ターミナルの設定」から「スクロールバーを表示」とするとスクロールバーが出てきて便利です。

touch

次に、ファイルを生成するコマンド、touchを使ってみましょう。 cd ~として最初の位置に戻ったうえで、 以下のコマンドで、hoge.cというファイルを作りましょう

$ touch hoge.c
これによりファイルが生成されました。確認してみましょう。
$ ls
hoge.c  main.c  README-cloudshell.txt
出来ていますね。ここでは、左側のファイルビューアでもhoge.cが生成されていることが確認できます。それをクリックすると、 真ん中のエディタ画面に、その中身が表示されます。現在、何も書かれていないですね。適当に何かを書いてみましょう。 何か書いたあとに、ターミナルでcat hoge.cとして、その中身が表示されることを再度確認しましょう。

コラム

Google Cloud Shell Editorは自動セーブ機能があるので、「上書き保存」しないでも自動的にファイルの変更が保存されます。 この機能をオフにするには、メニューの「File」から「Auto Save」のところのチェックを外します。そうすると、「File」から「Save」を押すか、 あるいはCtrl+Sを押したときのみ保存されるようになります。

mkdir

次に、mkdirコマンドでディレクトリを作ってみましょう。Make directoryの略です。

$ mkdir sample_directory
これにより、sample_directoryというディレクトリが出来ます。左側のビューアからも確認できますし、lsと打つことでも確認できます。

mv

次は、mv (move)コマンドを試しましょう。これは、ファイルを別の場所に移動するコマンドですが、それを利用してファイルの名前を変えることもできます。 mv 元のファイル 移動先のファイル名 という使い方をします。

$ mv hoge.c fuga.c
このようにすることで、hoge.cfuga.cにリネームされました。lscatやあるいは左側のファイルビューアからクリックすることで、fuga.cの中身を見てみましょう。 次に、このhuga.cをsample_directoryの中にもっていきましょう。
$ mv fuga.c sample_directory/fuga.c
この表記で、ファイルを移動することが出来ます。左側のビューアから、実際にファイルが移動していることを確認してみてください。

cp

次にcp によるコピーを行ってみましょう。使い方はcp コピー元 コピー先です。

$ cp main.c main2.c
これにより、main.cと同じ名前のmain2.cが出来ているはずです。

rm

次に、今作ったデータとディレクトリを削除してみましょう。これにはrmコマンドを使います。removeの略です。

$ rm main2.c
これで、先ほど作ったmain2.cが消えました。rmコマンドで消したものはゴミ箱に保存などされず、完全に消えてしまいます。 なので、うっかり間違えて重要なファイルを消さないように注意しましょう。 次に、ディレクトリも消してみます。
$ rm sample_directory
こうすると、以下のようなエラーが表示されます。
rm: cannot remove 'sample_directory/': Is a directory
これの意味するところは、rmコマンドではディレクトリを消せないということです。実はディレクトリを消すときは、 -rというオプション(recursiveの略です)をつける必要があります。再帰的に、ディレクトリを消し、その中身も消していく、という意味です。 ここでオプションとは、コマンドのあとに記入するもので、コマンドに追加の指示を与えます。試してみましょう。
$ rm -r sample_directory
これにより、先ほど作ったディレクトリが、その中身ごと消えました。

上記のようなファイル・ディレクトリ操作の処理は、左側のビューアからファイルを右クリックしたりドラッグアンドドロップしたりして 行うこともできます。windowsではエクスプローラで行うこともできます。ですが、コマンドによる処理を覚えておくことは重要です。 なぜなら、

  • コマンド処理を組み合わせることで、より複雑な処理を自動化して実行することができます。
  • 将来的にGUIが無い状態でプログラミングをすることがあり得ます(サーバ上でプログラミングするなど)。その場合、コマンド操作は必須です。

コラム

何かコマンドを実行したときにエラーが表示されたときは、まずそのエラーメッセージを読んで意味を考えましょう。そのうえで、そのエラーメッセージでググると、多くの場合解決策がわかります。

やってみよう(15分)

  • 上記の通り、cat, touch, mkdir, mv, cp, rmを試してみましょう。
  • カレントディレクトリ以下に、以下のような構造のディレクトリおよびファイルを作ってみましょう:./aaa/bbb.py, ./aaa/ccc.py, ./aaa/ddd/fff.py
  • lsに対して、隠しファイルも含めた全てを表示するオプションが-a、詳細にプリントするオプションが-lです。これらを組み合わせてls -alとすると、現在のディレクトリ下のファイルの詳細一覧が表示されます。やってみましょう。
  • man というコマンドを使ってみましょう。マニュアルという意味です。$ man コマンドとすることで、そのコマンドの説明が表示されます。コマンドの説明はもちろんググれば出てくるのですが、manコマンドのものは公式で最新なのでより信頼がおけます。説明画面では上下キーでスクロール、Qを押すと終了します。試しに $ man ls などとして結果を眺めてみましょう。

クイズ

/xxx/yyy.txt./xxx/yyy.txtがそれぞれ何を表すか説明してください。

答え
  • /xxx/yyy.txt: コンピュータの一番上の位置から、xxxというディレクトリがあり、その下にyyy.txtとうファイルがあり、それへの絶対パス。
  • ./xxx/yyy/.txt: カレントディレクトリ以下にxxxというディレクトリがあり、その下にyyy.txtというファイルがあり、それへの相対パス。この絶対パスは何かわからない。たとえば次のようなものかもしれない:/home/matsui528/xxx/yyy.txt.

Hello Worldプログラムのコンパイルと実行

プログラミングの準備はここまでで終わりです。早速プログラミングを行いましょう! プログラミングを書き、それを実行するという作業は、簡略化して言えば以下の3つのステップに分解されます。

  1. ソースコードを書く:人間が読んで理解できるテキストファイルを書きます。これは、極端に言えば、windowsのメモ帳で文章を書く行為と同じです。
  2. ソースコードをコンパイルし、実行可能ファイルを作る:人間が理解できるソースコードを、コンパイルという作業によって、機械が理解できる形式に変更します。Pythonのようなインタプリタ言語は、このステップがありません。
  3. 実行可能ファイルを実行する:得られた実行可能ファイルを、実際に実行します。

このそれぞれのステップについて、順番に見ていきましょう。

ソースコードを書く

まず最も有名な「hello, world」コードを書いてみましょう。これは、「hello, world」という文字列を出力するだけのプログラムです。 ある言語を勉強するときは、最初にそのようなコードを書くという文化があります。 cd ~として最初の位置に戻ると、main.cというファイルがあります。これを左側のビューアからクリックし、真ん中のエディタを使って、 以下を写経してください。コピペしてはダメです。ちなみに、下記のコードは教科書の「カーニハン&リッチー, プログラミング言語C」の7pに書いてあります。 今後はそれを省略して右のように表記します:(K&R, 7p)

#include <stdio.h>

main()
{
    printf("hello, world\n");
}

ソースコードをコンパイルする

次に、ソースコードをコンパイルします。次のコマンドを実行してみましょう。

$ gcc main.c
ここで、gccはコンパイラと呼ばれるプログラムです。これはソースコードを受け取り、それをコンピュータが解釈できる実行可能ファイル に変換します。ここでは、実行可能ファイルであるa.outが生成されているはずです。

実行可能ファイルを実行する

この実行可能ファイルを実行してみましょう。以下のようにします

$ ./a.out
これは現在のローカルディレクトリにある実行可能ファイルa.outを実行しろ、という意味です。 次のような出力が得られるはずです。
hello, world
これが皆さんがコンパイルして実行した初めてのプログラムです!

やってみよう(15分)

  • 上記の通り、hello, worldプログラムを書き、コンパイルし、実行してみましょう
  • うまくいかないかもしれません。よくある理由としては以下です
    • 全角で入力してはダメです。#Include のように書いてはいけません。#includeとしましょう
    • カッコのつけ忘れ、閉じ忘れ
    • 行末のセミコロンのうち忘れ

コラム

以下のような警告が出るかもしれませんが、気にしないでください。これは、本当はもっとちゃんと書かないといけない部分を省略しているので、コンパイラが気を利かせてそれを教えてくれています。

main.c:3:1: warning: return type defaults to ‘int’ [-Wimplicit-int]
 main()
 ^~~~

解説

それでは、先ほどのコードを解説していきましょう。

#include <stdio.h>
#includeという表記は、別のファイルをこの部分によみこむ、 という意味です。ここでは、/usr/include/stdio.hという ファイルをここに読み込んで展開しています。 stdioはstndard input/outputの略です。 printfを使うためにはこれが必要なので、本講義の範囲ではいつでも includeすることになります。

main() {      
    // 本体
}
「main関数」の部分は、プログラムの本体を書く部分です。 この波括弧で囲まれた内容が、./a.outのときに実行されます。

ここではmainの内側では左側に半角スペース4個ぶんの空白をいれてあります(インデントをいれる、と表現します)。 ここは、空白がなくてもいいですし、たくさんあっても大丈夫です。 ですが、コードの可読性を上げるために、常に決まったルールで空白を入れるといいです。 広く使われる慣習として、波括弧の内側では4つ空白を入れるようにしましょう。 実際は、そのようなルールはエディタが自動的に機械的にそろえてくれます。

コラム

Google Cloud Shellでは、前の行がちゃんと半角スペース4個分の空白を入れてある場合、Enterを押して改行すると、次の行も自動的にちょうどいいインデント(半角スペース4個分)になると思います。手動でインデントを足すときは、Spaceを4回押すのでは無く、Tabを一度だけ押すと、自動的に空白が4個追加されます。Shift+Tabとすると4個が消えます。

これらの字下げの統一は可読性のために非常に重要なので、意識的にしっかりやっていくようにしましょう。

printf("hello, world\n");
printf関数です。関数というものは、数学の関数のように、カッコの内側に 引数(入力情報)が与えられ、何らかの処理を行います。 printf関数は、printf(引数) としたときに、引数の内容をターミナルに表示します。

ここで引数とされているのは "hello, world\n"です。 ここで、"あいうえお"のように二重引用符で囲まれている任意個の文字の列を文字列と呼びます。 これはその名の通り、文字を表します。ここでは日本語を使っても構いません。 最後の\nは改行を表す特殊な記号です。

また、C言語では、行の最後にはセミコロンを書きます。これを忘れるとエラーになりますので、注意してください。

やってみよう(20分)

  • $ cat /usr/include/stdio.hstdio.hの中身を眺めてみましょう。
  • printf("あああ")のように、printfの中身をいろいろと変更してコンパイルして実行してみましょう。
  • "hello, world"のように、最後の改行記号を取り除くとどうなるか確認しましょう。
  • "hello, world\n\n"のように、最後に改行記号を二度入力するとどうなる確認しましょう。
  • 改行のほかにも、様々な特殊記号があります。\tはタブを表します。これは出力の位置ぞろえに役立ちます。"abc\tabc\n"をプリントしてみましょう。
  • 二重引用符そのもの(")のプリントは\"とします。また、バックスラッシュそのもの(\)は\\とします。次を実行してみましょうprintf("double quotation is \" back slash is \\ Check your console.\n");

クイズ

次の文字列を出力したいとします:

back slash + double quotation (\") is used to print a double quotation
ここで、次のように書くとうまくいきません。それは何故でしょうか?そして、どう書き換えればいいでしょうか?
printf("back slash + double quotation (\") is used to print a double quotation\n");

答え

\"も、文字列中で使いたいときは特殊文字にしなければならないため。正解:

printf("back slash + double quotation (\\\") is used to print a double quotation\n");

基本データ型

それでは次に、基本となるデータ型について勉強しましょう。 あらためてdata_type.cというファイルを作りましょう。$ touch data_type.cとしてもいいですし、左側から右クリックで作ってもいいです。 その中に以下を写経してください。

#include <stdio.h>

main()
{
    int height;
    int width;
    height = 10;
    width = 5;
    int area;
    area = height * width;
    printf("Height: %d  Width: %d  Area: %d\n", height, width, area);

    char c1, c2, c3;
    c1 = 12;
    c2 = 24;
    c3 = 140;
    printf("c1: %d, c2: %d, c3: %d, c1+c2: %d\n", c1, c2, c3, c1 + c2);

    float f = 123.5;
    printf("f=%f, f/3=%f\n", f, f / 3);

    printf("address of height is %p. address of width is %p\n", &height, &width);
}

写経が終ったら、コンパイルしましょう。

$ gcc data_type.c

そして実行してみましょう。以下のような内容になるはずです(addressの値は違うものになります)

$ ./a.out 
Height: 10  Width: 5  Area: 50
c1: 12, c2: 24, c3: -116, c1+c2: 36
f=123.500000, f/3=41.166668
address of height is 0x7fff138be160. address of width is 0x7fff138be164

ちなみに、コンパイル後の実行可能ファイルの名前はデフォルトでa.outですが、次のように-o 名前 というオプション をつけることで変更して出力できますので、覚えておきましょう。

$ gcc data_type.c -o data_type  # a.outではなくdata_typeという名前のファイルが出来る
$ ./data_type  # 実行

やってみよう(10分)

上のコードを写経し、実行してみましょう

変数

それではコードを解説していきます。

int height;
int width;
height = 10;
width = 5;
ここで、int heightという表記は、height という名前の変数 を一つ準備するという意味です。 変数とは、よく「箱」だと例えられます。 箱の名前が変数名であり、それがheightです。 そして、箱のintです。 型は、箱の大きさと中に入れるものの種類を決定します。 今回は32 bitの大きさの整数を表すint型です。 よって、このheightは整数一つを表す変数です。

その箱の中に、を保持します。ここでは、height = 10という表記で、10という値をheightを入れています。 値を入れることを代入と言います。 ここでのイコールは「左辺に右辺を代入する」という意味です。「左辺と右辺は等しい」という意味である数学のイコールとは違うので注意してください。 10 = heightという書き方はできません。

変数名としては英字、数字、アンダースコアが使えます。大文字と小文字は区別されます。 また、c言語の文法として用いられるfor, int, など(予約語)は変数名として使うことはできません。

int _height;    // OK。アンダースコアは使える
int Height;     // OK。大文字も使える。Heightとheightは別物。
int height123;  // OK。先頭以外で数字は使える
int 123height;  // ダメ。先頭に数字は使えない
int for;        // ダメ。Cの予約語。

ところで、変数をまず作ることを宣言といいます。複数の変数はまとめて宣言することもできます。つまり、 上で見たheightwidthの宣言は、下記のようにまとめても書けます。

int height, width;
宣言と同時に値を代入することもできます。これを初期化と言います。 また、変数は後から別の値を代入して値を上書きすることができます。
int height = 5;  // 宣言と同時に代入(初期化)
height = 12;  // 値の上書き
ちなみに、変数を宣言して初期化していない段階では、その変数の値は未定義なので注意してください。
int height;
// この段階ではheightの値は未定
height = 5;

さて、それではコードの次の部分を見ていきましょう。

int area;
area = height * width;
printf("Height: %d  Width: %d  Area: %d\n", height, width, area);
ここでは、areaというint型変数を新たに作り、そこにheightwidthを掛け合わせたものを代入しています。 変数は、掛け算を表す*といった演算を適用することかできます。これについては来週詳しく説明します。

そして、printfを使ってその中身を表示します。ここで、printfには4つの引数が渡されています。 最初の"Height: %d Width: %d Area: %d\n"の部分は、Hello worldの例で見た通り、表示する内容です。 ここで、%dという部分は、「何らかのデータをここに表示する」というサインになっています。 2つ目の引数がheightです。これが、最初の%dの部分に表示されます。 この処理は何度も行うことが出来ます。次の%dには、3つ目の引数であるwidthが表示されます。 最終的に、以下の表示になります。

Height: 10  Width: 5  Area: 50

やってみよう(5分)

  • 上で述べた、禁止されている変数名(123heightfor)を使って変数を作り、コンパイルしてみましょう。どういうエラーが返ってくるか見ましょう。

次に、型について勉強しましょう。Cで使われる整数型には以下のようなものがあります。これらは全て整数を表します。

型名 サイズ (bit) 値の範囲
char (よく使う) 8 (\(-2^{7}\)から\(2^{7}-1\)), すなわち, (-128 から 127)
short 普通は16 (\(-2^{15}\)から\(2^{15}-1\)), すなわち, (-32,768 から 32,767)
int (よく使う) 普通は32 (\(-2^{31}\)から\(2^{31}-1\)), すなわち, (-2,147,483,648 から 2,147,483,647)
long 32か64 (\(-2^{31}\)から\(2^{31}-1\)), あるいは, (\(-2^{63}\)から\(2^{63}-1\))
unsigned char 8 (\(0\)から\(2^{8}-1\)), すなわち, (0 から 255)
unsigned short 普通は16 (\(0\)から\(2^{16}-1\)), すなわち, (0 から 65,535)
unsigned int 普通は32 (\(0\)から\(2^{32}-1\)), すなわち, (0 から 4,294,967,295)
unsigned long 32か64 (\(0\)から\(2^{32}-1\)), あるいは, (\(0\)から\(2^{64}-1\))

変数のサイズは、一つの変数を表すために使われるビットの数です。たとえば8ビットを使用するchar型は、8つの{0, 1}で表されます。 00000010のようなものです。8ビットなので、\(2^8=256\)個の値を表現できます。 char型は負の数も表現するため、1つのビットを符号の表現に用います。そのため、正の数が7ビット(\(2^7=128\))分、負の数も同様に7ビット分 表現できるたけ、最終的に-128から127までの整数を表現できます。 ビット数が少ないとメモリ消費量も少なくなり良いですが、そのぶん表現できる数の範囲も小さくなります。 整数型は、後述する実数型とは違い、整数を厳密に表現します。

このcharに対応して、負の数を表現しない型をunsigned charと呼びます。こちらは8ビット分全てを整数の表現に使うので、 0から255までの値を表現できます。このように、整数の各型に対応してunsignedのバージョンがあります。

最も標準的に使われる整数型はintです。これは大体プラスマイナス20億の値を表現できるため、普段の計算ではこの長さで十分です (もちろん足りなくなる場合もありますが)。本講義の範囲では普通に整数を扱う場合はintを使えばOKです。

サイズに「普通は」と書いているのはどういうことでしょうか? C言語のルールとして、たとえばintは「最低16ビット使う」というような取り決めとなっており、 具体的に何ビットを使うかは処理系に依存ということになっています。 なので、組み込みのようなメモリ消費がシビアな条件では上記と違うビット数の処理系もあるでしょう。 K&R本では以下のような記述があります「intは16ビットあるいは32ビットなのが普通である」(K&R, p44)。 K&Rが書かれたころは、intが16ビットである処理系も多かったのでしょう。 本講義で扱う範囲(一般的な64 bitコンピュータ)では、上記の値だと思って大丈夫です。

次に、実数型を見てみましょう。これらは整数に限らず、小数を表現できます。これらの表現形式を浮動小数点数と呼びます。

型名 サイズ (bit) 値の範囲
float (よく使う):単精度浮動小数点数 普通は32 \(3.4\times10^{-38}\) から 約\(3.4\times10^{38}\)
double (よく使う):倍精度浮動小数点数 普通は64 \(1.7\times10^{-308}\) から 約\(1.7\times10^{308}\)

実数型は整数型と違い、小数を近似的に表現します。たとえば、\(1/3 = 0.3333...\)は、浮動小数点では正確に表現できません。 ビット数が多いほうが、表現できる値の範囲が大きくなり、また精度よく数を表現できます。

floatdoubleどちらを使うべきでしょうか?これは問題に依存します。 数値計算など、高い精度が求められる場合はdoubleが用いられるでしょう。 精度よりもメモリ消費のほうが重要な場合はfloatのほうが好まれます。

コラム

機械学習などの分野では、さらに精度を犠牲にしてでもメモリ効率をよくしたいという要望があります。 そのため、近年は16 bitの浮動小数(半精度浮動小数点数)が注目を浴びています。 16 bitというのはかなり少ないビット数なので、その精度で大丈夫なのか?と言いたいところなのですが、 機械学習では計算は相当おおざっぱでいいのでメモリ問題のほうが重要ということのようです。 数値の型などという極めて基礎的な部分でも進化が起きる、情報科学という分野のダイナミックさが見て取れます。

それでは、もとのコードの次の部分を見ていきましょう。

char c1, c2, c3;
c1 = 12;
c2 = 24;
c3 = 140;
printf("c1: %d, c2: %d, c3: %d, c1+c2: %d\n", c1, c2, c3, c1 + c2);
その結果は以下でした。

c1: 12, c2: 24, c3: -116, c1+c2: 36

ここでは、3つのchar型変数を作り、値を代入し、表示しています。 printfの最後の部分のように、%dに代入するものはその場でc1 + c2のように計算してもよいです。

ここで、c3の値がおかしいことに気付きましたでしょうか?c3には140を代入したはずなのに、 printしてみると-116になっていますね。これは、上で述べた通り、charが表現できる限界である127を超えているからです。 この値がなぜ-116になるかについては、「型変換」の章で解説します。

float f = 123.5;
printf("f=%f, f/3=%f\n", f, f / 3);
実数型についても、整数型と同様に用いることができます。 ここで、実数型を表示するときは%dではなく%fを用いることを覚えておきましょう。

やってみよう(5分)

整数型を%fフォーマットで、また実数型を%dフォーマットで表示したときどうなるかやってみましょう。

クイズ

\(x^2 + 2x + 3\) を計算するコードを書いてみましょう。float x としたうえで、数式を記述しましょう。xを色々変えて出力してみてください。

答え

例えば以下のように書けます。xの値を色々変化させてみましょう。

float x = 0.01;
float result = x * x + 2 * x + 3;
printf("x: %f, result: %f\n", x, result);

メモリ上での変数のふるまい

それではコード中の最後の行を見てみましょう。ここでは、変数がコンピュータ上で実際にどのように表現されているかを見てみましょう。

printf("address of height is %p. address of width is %p\n", &height, &width);
この結果は例えば次のようになります。
address of height is 0x7fff138be160. address of width is 0x7fff138be164
ここでは、変数がコンピュータのメモリ上のどこに位置しているかが表示されています。 これを理解するために、変数がどのように表されるかを下図に示します。

これは、コンピュータのメモリを表現しています。メモリとは、プログラムが情報を保存するために利用できる領域のことです。 上の図のように抽象化されます。一つのブロックは、{0, 1}が8個並んだブロックになっています(8 bit = 1 byte)。 そのようなブロックがずらっと並んでいます。これがメモリです。ここに情報を書き込んだり読み込んだりします。

そして、各ブロックには自分の住所であるアドレスが割り振られています。 このアドレスを指定することで、所望のブロックを一意に指定できます。 アドレスはここでは16進数で表現されています。このアドレスは、実際には(64 bitマシンの場合は)64ビットの整数です。 その整数が順番に並んでいると思ってください。注意として、ここでは16進数なので、値が9の次はaになります。

ここで、変数heightを宣言するということは、このメモリ中のブロック4つがheightであるとまさに宣言するということです。 int型は32bitなので、4つのブロックを確保します。そして、その中に値を書き込みます。ここでは10なので、二進数表記で00000000|00000000|00000000|00001010となります。 ここでは、次のwidthはそのすぐ次に宣言されています。4ブロックあとなので、先頭アドレスが4増えていることがわかります。

ここで、変数の頭に&(アンド)を付けると、その変数の先頭アドレスを取得することができます。よって、ここでは、 &height0x7fff138be160となります。&)はアドレスと覚える人もいます。 この内容はprintfにおいて%pを用いることで表示できます。それが、先ほどのコードの意味です。

このようなことを考えなくてもプログラミングは出来るのですが、上記のようなメンタルモデルを忘れないようにしていてください。 のちにポインタを勉強するときに、このモデルの理解が必須になります。

コラム

上の図では変数を宣言した順番通り、heightのほうがwidthよりも「前」に位置していますが、これは必ずしもそうなるとは限りません。例えば以下のようになるかもしれません。

address of height is 0x7ffc2c951620. address of width is 0x7ffc2c95161c
ここで下の二桁に注目すると、heightの番地「20」は、widthの番地「1c」よりも、16進数で4だけ「後ろ」になります(16進数においては\(20 - 1c = 4\)。これを10進数で表現すると\(32 - 18 = 4\)【2021/10/7追記】\(32 - 28 = 4\)のタイポでした。。。

やってみよう(10分)

  • intを2つ並べると、その先頭アドレスの差の絶対値は(少なくとも)4以上になります。ここでcharを並べた場合にどうなるかやってみましょう。

ローカル環境でのプログラミングについて

意欲のある学生は、ローカル環境でのプログラミングを推奨します。

宿題

本日の講義はここまでです。 次回までに、以下を行ってください。

  • 宿題の提出にはGitHubおよびGitHub Classroomを使います。そのため、GitHubのアカウントを作っておいてください。これは普段使っている個人用があればそれでもいいですし、学科用に新しく作ってもいいです。
  • ITC-LMSのアンケートの「アカウント申告」に答えて、GitHubアカウント名を松井まで教えてください。履修登録していないけど講義を受けて課題も解きたいという人は、松井まで個別にGitHubアカウントを教えてください。
  • Githubアカウントを登録してもらわないと、成績がつきませんので注意してください。

ちなみに、Git/GitHubとは以下のようなものになります。

  • バージョン管理(ソースコードの履歴を保存する仕組み)としてgitというソフトウェアがあります。gitは単に自分の手元でソースコードを管理する仕組みなのですが、そのgit管理されたソースコードを保存・公開するウェブサービスがGitHubです。
  • GitHubを使うことで、人々は簡単に共同でコードを編集することが出来ます。また、コードを公開したい場合、GitHub上で公開すればを使いたい人が簡単に見ることができます。例えばPythonそのもののコードは以下になります
  • GitおよびGitHubを知っていれば、これまでに開発されたコードをチェックすることができます。いわゆる「巨人の肩の上に立つ」ということです。
  • 本講義ではGit/GitHubの使い方の基礎を第七回に勉強します。それに先立ち、宿題の提出はGitHub経由で行うため、まずアカウントを作ってもらいます。
  • また、GitHubはSNS的な側面もあります。アカウントを持つと自分のページが公開され、自分が公開したいコードをそこで表示することができます。他の人のコードに質問をしたり、修正パッチを送るなどもできます。ちなみに松井のGitHubアカウントは以下になります。
  • 逆に、公開しないプライベートなコードを置いておくこともできます。これは安全なバックアップになります。
  • GitHubの類似サービスとしてBitbucketGitLabがあります。