Week 1 (2023/10/5)¶
今日やること
- 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によるオンライン環境構築¶
それでは早速始めましょう!まずは環境を構築します。 week2(来週)からは皆さんには情報教育棟で備え付けのマックを使ってもらいます。 week1(今日)はzoomなので、情報教育棟が使えません。 なので今日は、ブラウザベースでプログラミング環境を構築します。 week2以降は、マックでローカル環境でプログラミングしてもいいですし、 week1同様にブラウザベース環境を引き続き使っていってもいいです(マック上でもブラウザベース環境を実行できます)
- まずはECCSアカウントあるいは個人gmailにログインしてください(googleのサービスを使います)
- Google Cloud Shell Editor というサービスを利用します。このリンクをクリックしてください。
- 1分ぐらい待ちます。すると画面が立ち上がります。
- もし画面の字が赤くなり、処理が止まっているように見える場合は、リロードしてください。気軽にリロードして大丈夫です。
- この状態ではまだどのフォルダも開いていない初期状態です。まずは、開始位置のフォルダを開きます。左上の「File」メニューから「Open」を選択し、自分のユーザ名のフォルダを選択してください。参考:プログラミング基礎演習の長谷川先生の資料
- すると、以下のような画面になります。今後、この画面でプログラミングを行っていきます。
上のような画面に辿り着くはずです。
- ここで、左側は現在開いているフォルダの中身を表示しています。windowsでいうエクスプローラ、MacでいうFinderです。初期状態では「README-cloudshell.txt」というファイルのみがあると思います。 例えばここで右クリックして新しくファイルを作れます。ここでは、「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.c
とREADME-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
ls
やpwd
をすると、最初の位置にもどってきたことがわかります。
ちなみに、上のような長いコマンドは全て入力する必要はありません。
ある程度入力したあとに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.c
がfuga.c
にリネームされました。ls
やcat
やあるいは左側のファイルビューアからクリックすることで、fuga.c
の中身を見てみましょう。
次に、このfuga.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つのステップに分解されます。
- ソースコードを書く:人間が読んで理解できるテキストファイルを書きます。これは、極端に言えば、windowsのメモ帳で文章を書く行為と同じです。
- ソースコードをコンパイルし、実行可能ファイルを作る:人間が理解できるソースコードを、コンパイルという作業によって、機械が理解できる形式に変更します。Pythonのようなインタプリタ言語は、このステップがありません。
- 実行可能ファイルを実行する:得られた実行可能ファイルを、実際に実行します。
このそれぞれのステップについて、順番に見ていきましょう。
ソースコードを書く¶
まず最も有名な「hello, world」コードを書いてみましょう。これは、「hello, world」という文字列を出力するだけのプログラムです。
ある言語を勉強するときは、最初にそのようなコードを書くという文化があります。
cd ~
として最初の位置に戻ると、main.c
というファイルがあります。これを左側のビューアからクリックし、真ん中のエディタを使って、
以下を写経してください。コピペしてはダメです。ちなみに、下記のコードは教科書の「カーニハン&リッチー, プログラミング言語C」の7pに書いてあります。
今後はそれを省略して右のように表記します:(K&R, 7p)
#include <stdio.h>
main()
{
printf("hello, world\n");
}
コラム
マックの場合、バックスラッシュ(\
)を打つには、「option」キーを押しながら「¥」キーを押すといいようです。
ソースコードをコンパイルする¶
次に、ソースコードをコンパイルします。次のコマンドを実行してみましょう。
$ 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]
3 | main() {
| ^~~~
上記のようにwarning
(警告)の場合はとりあえずはコンパイルが成功します。一方、error
(エラー)の場合はコンパイルが失敗し、a.out
が生成されません。たとえばセミコロンを忘れると以下のようなエラーが出ます。
main.c: In function ‘main’:
main.c:4:30: error: expected ‘;’ before ‘}’ token
4 | printf("hello, world!\n")
| ^
| ;
5 | }
| ~
解説¶
それでは、先ほどのコードを解説していきましょう。
#include <stdio.h>
#include
という表記は、別のファイルをこの部分によみこむ、
という意味です。ここでは、/usr/include/stdio.h
という
ファイルをここに読み込んで展開しています。
stdioはstandard input/outputの略です。
printf
を使うためにはこれが必要なので、本講義の範囲ではいつでも
includeすることになります。
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.h
でstdio.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 v = 123.5;
printf("v=%f, v/3=%f\n", v, v / 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
v=123.500000, v/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の予約語。
ところで、変数をまず作ることを宣言といいます。複数の変数はまとめて宣言することもできます。つまり、
上で見たheight
とwidth
の宣言は、下記のようにまとめても書けます。
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
型変数を新たに作り、そこにheight
とwidth
を掛け合わせたものを代入しています。
変数は、掛け算を表す*
といった演算を適用することかできます。これについては来週詳しく説明します。
そして、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分)
- 上で述べた、禁止されている変数名(
123height
やfor
)を使って変数を作り、コンパイルしてみましょう。どういうエラーが返ってくるか見ましょう。
型¶
次に、型について勉強しましょう。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...\)は、浮動小数点では正確に表現できません。 ビット数が多いほうが、表現できる値の範囲が大きくなり、また精度よく数を表現できます。
float
とdouble
どちらを使うべきでしょうか?これは問題に依存します。
数値計算など、高い精度が求められる場合はdouble
が用いられるでしょう。
精度よりもメモリ消費のほうが重要な場合はfloat
のほうが好まれます。
コラム
機械学習などの分野では、さらに精度を犠牲にしてでもメモリ効率をよくしたいという要望があります。 そのため、近年は16 bitの浮動小数(半精度浮動小数点数)が注目を浴びています。 16 bitというのはかなり少ないビット数なので、その精度で大丈夫なのか?と言いたいところなのですが、 機械学習では計算は相当おおざっぱでいいのでメモリ問題のほうが重要ということのようです。 数値の型などという極めて基礎的な部分でも進化が起きる、情報科学という分野のダイナミックさが見て取れます。
コラム
実は厳密には、char
と書いたときにそれが符号付きか符号なしかは処理系依存だそうです。特に、ARMというCPUを使っている場合はchar
は符号無しの場合があります。本講義の範囲内では、char
は符号有りとして扱います。
それでは、もとのコードの次の部分を見ていきましょう。
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 v = 123.5;
printf("v=%f, v/3=%f\n", v, v / 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増えていることがわかります。
ここで、変数の頭に&
(アンド)を付けると、その変数の先頭アドレスを取得することができます。よって、ここでは、
&height
は0x7fff138be160
となります。&
(アンド)はアドレスと覚える人もいます。
この内容は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 - 28 = 4\))
やってみよう(10分)
int
を2つ並べると、その先頭アドレスの差の絶対値は(少なくとも)4以上になります。ここでchar
を並べた場合にどうなるかやってみましょう。
宿題¶
本日の講義はここまでです。 次回までに、以下を行ってください。
- 「教育用計算機システム(ECCS)」を利用可能な状態になってください。すなわち、情報教育棟にてマックを利用可能な状態になっておいてください。もしこれまでにまだECCSを使ったことが無い場合は、この手順を参考に登録してください。
- 宿題の提出にはGitHubおよびGitHub Classroomを使います。そのため、GitHubのアカウントを作っておいてください。これは普段使っている個人用があればそれでもいいですし、学科用に新しく作ってもいいです。
- ITC-LMSのアンケートの「アカウント申告」に答えて、GitHubアカウント名を松井まで教えてください。履修登録していないけど講義を受けて課題も解きたいという人は、松井まで個別にGitHubアカウントを教えてください。
- Githubアカウントを登録してもらわないと、成績がつきませんので注意してください。
- Q&A(随時更新)がありますので、そちらも参考にしてください。
コラム
ちなみに、Git/GitHubとは以下のようなものになります。
- バージョン管理(ソースコードの履歴を保存する仕組み)として
git
というソフトウェアがあります。git
は単に自分の手元でソースコードを管理する仕組みなのですが、そのgit管理されたソースコードを保存・公開するウェブサービスがGitHubです。 - GitHubを使うことで、人々は簡単に共同でコードを編集することが出来ます。また、コードを公開したい場合、GitHub上で公開すればを使いたい人が簡単に見ることができます。例えばPythonそのもののコードは以下になります
- GitおよびGitHubを知っていれば、これまでに開発されたコードをチェックすることができます。いわゆる「巨人の肩の上に立つ」ということです。
- 本講義ではGit/GitHubの使い方の基礎を第七回に勉強します。それに先立ち、宿題の提出はGitHub経由で行うため、まずアカウントを作ってもらいます。
- また、GitHubはSNS的な側面もあります。アカウントを持つと自分のページが公開され、自分が公開したいコードをそこで表示することができます。他の人のコードに質問をしたり、修正パッチを送るなどもできます。ちなみに松井のGitHubアカウントは以下になります。
- 逆に、公開しないプライベートなコードを置いておくこともできます。これは安全なバックアップになります。
- GitHubの類似サービスとしてBitbucketやGitLabがあります。
コラム
このコラムは発展的なので、気にならない人はスルーでも大丈夫です。
実行可能ファイルを実行するとき、なぜ./a.out
のように./
をつける必要があるのでしょうか?実際、これを取ってa.out
とすると実行できません。これを説明します。
まずターミナルでコマンドを実行する際のルールについて説明し、その後a.out
ではなぜダメかを説明し、そして./a.out
ではなぜOKなのか説明します。ちなみにこの解説が詳しいです。
- ターミナルでコマンドを実行する際のルール
- ターミナルでコマンドを実行する際、何が起こっているのでしょうか。例えば
ls
と打つとファイル一覧が見れるわけですが、これはコンピュータのどこかにある「ls
プログラム」を実行しているからです。ではそれはどこにあるのでしょうか? - これは、
which ls
のようにwhichコマンドで確認できます。例えば私の環境ですと次のようになります。つまり、$ which ls /usr/bin/ls
ls
プログラムは/usr/bin/
にあります。 - それでは、なぜターミナルで
ls
と打つと/usr/bin/
の中のプログラムを実行してくれるのでしょうか?これは、「ターミナルでコマンドを実行しようとするときに、そのコマンドを探す場所一覧」が指定されているからです。これは$PATH
という環境変数の中に記載されています。これを見てみましょう。ここではコロン区切りでディレクトリの一覧が記載されています。一番最初に$ echo $PATH /home/matsui/usr/bin:/home/matsui/.vscode-server/bin/74b1f979648cc44d385a2286793c226e611... (以下、続く)... :/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/s ... (以下、続く)...
/home/matsui/usr/bin
とあります。また、途中には/usr/local/sbin
や/usr/bin
もあります。(ちなみに、環境変数というのは上記のように$XXX
と言う形で指定されるものであり、環境に応じて別のものが入ります。) - よって、
ls
のようなコマンドを実行しようとする場合、上記の$PATH
の中のディレクトリたちの中にls
プログラムを見つけることができたなら、それを実行します。
- ターミナルでコマンドを実行する際、何が起こっているのでしょうか。例えば
- なぜ
a.out
はダメか?- 上記のルールを元に考えると、
a.out
を実行しようとすると次のようになります。まず$PATH
で指定されるディレクトリたちの中にa.out
がないかどうか探します。 - ですが、実は現在のワーキングディレクトリは
$PATH
で指定されていません。 - なので、
a.out
プログラムを発見できず、実行することが出来ません。 - ちなみに、これはセキュリティ的に安全ではない処理ですが、
$PATH
に無理やり現在のディレクトリを追加すると実行できるようになります。たとえば上記をやったあとは、いったんターミナルを終了してください。そうると$ a.out <- a.outだけだと普通はエラー a.out: command not found $ export PATH="$PATH:./" <- 一次的に$PATHに現在位置である「./」を追加するコマンド $ echo $PATH <- 確認すると、最後に「./」が追加されている /home/matsui/usr/bin:/ ..... :/snap/bin:./ $ a.out <- 実行できる! Hello World!
$PATH
への追加も消えます。
- 上記のルールを元に考えると、
- なぜ
./a.out
はOKなのか?- 次に、それではなぜ
./a.out
ではOKなのかを説明します。 ./a.out
のように、「/
」を含んだコマンドは、自動的に「$PATH
は探さず、それ自身を実行する」というルールになっています- そのため、
./a.out
の場合は、$PATH
に関係なくそれ自身を実行します。 - ちなみに、
.
が現在位置を示すので、./
は現在位置の直下すなわち現在位置そのものを指し、./a.out
は現在位置に存在するa.out
を一意に特定できます。 - スラッシュ入りであれば別の状況でも大丈夫なので、例えば以下のように別の位置から実行することもできます。
$ ls a.out $ ./a.out Hello World! $ mkdir xxx $ cd xxx $ ../a.out Hello World! $ mkdir yyy $ cd yyy $ ../../a.out Hello World!
- 次に、それではなぜ