Skip to content

Latest commit

 

History

History
563 lines (412 loc) · 21.1 KB

c_study_day1.markdown

File metadata and controls

563 lines (412 loc) · 21.1 KB

WebエンジニアのためのC言語入門ハンズオン #1

C言語は、習得までに非常に時間がかかる言語である。

「Cの罠と落とし穴」とか「Cのパズルブック」とか、「Cとその秘密」といったタイトルの本は山のようにあるのに、 他の言語では、その類の本がまったくないことに気づいたことあるかい?その理由をここで明らかにしておこう!

Peter van der Linden (1996). エキスパートCプログラミング -知られざるCの深層 アスキー出版局 23.

本ハンズオンは、すでに他の言語にて基本的なプログラミングの素養がある方を対象に、C言語というものの概要と
それを独力で学び続けるために必要な、忘れにくい最低限のベース知識を習得することが目的です。

前提知識

Webエンジニアは大抵が下記いずれかの言語を習得していることを前提としています。

  • Ruby
  • PHP => 有利
  • Perl
  • Python
  • Java
  • Javascript

今日習得すること

  • 最低限の文法
  • メモリ領域とアドレス空間
  • ポインター
  • 実習

1. 最低限の文法

データ型

  • char
char moji = 'a';

char型は、シングルクォートで括ります。

  • int
int num = 5;
  • float
float num = 1.2f;
  • double
double num = 1.2345;

文字列型は無いです。文字列はcharの配列です。
文字列の終端はNull文字\00で区別されます。

文字配列

char str[10] = "ABCDEFG";
char str[] = "ABCDEFG";
char *str = "ABCDEFG";  // これは定数になります。読み取り専用なので書き換え出来ません。

(余談) Nullバイト攻撃は、C言語の文字列配列の終端文字を狙った攻撃です。 変数を宣言するときは、データ型を指定する必要があります。

ポインター関連の演算子

  • * 間接演算子
int *num;
  • & アドレス演算子
int a = 4;
int *b = &a;

ポインターについては、後でゆっくりやりますので、今は流して下さい。

文法

  • if else
  • for while
  • switch
  • サブルーチン(function)
  • break continue
  • gotoと名札

goto以外は、まあ大体の言語でもあると思います。 プログラムの行末にはセミコロンが必要です。

共有ライブラリ

C言語それ自体は、最低限の文法とデータ型を持つのみです。
そのままでは辛いので、共有ライブラリをincludeして、便利な関数群を使います。

有名なやつ

  • stdio.h
    標準入出力
  • stdlib.h
    メモリ確保とか
  • string.h
    文字列操作

演習問題1-1

(1) 次のプログラムを写経して下さい。
コピペしないで下さい。一文字一文字心を込めて打ち込んで下さい。

#include <stdio.h>

int main(){
    printf("Hello World\n");

    int a = 5;
    printf("a = %d\n", a);

    char ch = 'b';
    printf("ch = %c\n", ch);

    char str[10] = "abcdefghi";
    printf("str = %s\n", str);
}

ファイルは、hello.cという名前で保存してください。

注) printfは第一引数で出力文字列のフォーマットを指定します。出力する変数の型に合わせてフォーマットを変えます。

  • char型 => %c
  • char型配列 => %s
  • int型 => %d
  • float, double型 => %f

(2) コンパイル 次のコマンドでコンパイルを行って下さい。

gcc -o hello hello.c

コンパイル後は、helloという名の実行ファイルができています。実行して見てください。
終わった人は、下記のコマンドでもコンパイルを行って下さい。

gcc hello.c

実行可能ファイルは、何というファイル名で出来たでしょうか?

(3) 下記のコマンドを実行して下さい。

  • Linux
    ldd hello
  • Mac
    otool -L hello

表示された物は、動的リンク(実行時に読み込まれる)された共有ライブラリです。 本プログラムで利用している共有ライブラリは何でしょうか?

※stdio.hをincludeしたはずなのに、全然違う名前が出てきます。それでいいんです。

2. メモリ領域とアドレス空間

他の言語とC言語が異なる最大の物は、メモリ管理に対するエンジニアの関与が大きいということです。

下記の5つのメモリー空間を常に頭に思い浮かべる必要があります。
自分が書いているコードが、**アドレス空間のどこに影響するのか?**を常に意識して下さい。

スタック

main関数がスタックの1番目に入ります。
以降の関数は、main関数から呼び出されると、その上に積み上がっていくイメージです。
実行が終わった関数は、スタックから取り除かれます。
StackOverFlowとは、スタックが積み上がりすぎて用意したアドレス空間の範囲外まで達するエラーです。
再帰呼び出しで無限ループさせると、発生させることが出来ます。

ヒープ

ヒープは、スタックに関係なく、全ての関数からアクセス可能なメモリ空間です。
mallocという関数で確保されるのはヒープ領域です。 ヒープに確保した領域は、明示的に解放するか、プログラムが終了するまで、確保され続けます。 いわゆるメモリリークは、ここで発生します。

グローバル

関数外で宣言された変数は、ここに格納されます。グローバル空間です。
つまり、あまり不用意に使うと、大変なことになります。

定数

文字列リテラルや、constキーワードで宣言された読み取り専用の変数です。
ここに格納された変数は、書き換えが出来ません。

コード

ソースコードとかが読み込まれている空間です。

仮想アドレス空間の模式図↓です。

仮想アドレス空間

上記図の区切られたアドレス空間のことを、セグメントと呼びます。 そして、セグメントで区切ってメモリを管理することを、セグメント方式と呼びます。

セグメント方式では、それぞれのセグメントに対してアクセスのルールが決まっています。 たとえば、定数セグメントの領域は読み取り専用ですが、定数の内容を書き換えようとしたらエラーが発生します。

このように、セグメントのルールに違反して発生するエラーをSegmentation Faultと呼びます。
C言語を書く場合、ずっと付き合っていくエラーです。

演習問題2-1

ソースコード内で、様々な変数を宣言して、その変数のアドレスを出力します。 出力されたアドレスを仮想アドレス空間と見比べて、どのセグメントにあるのかを確認しましょう。

(1) プログラムを写経して下さい。

#include <stdio.h>
#include <stdlib.h>


int glob1 = 123;
int glob2;

int main(){

    static int st1 = 123;
    static int st2;
    int local = 0;

    static char static_array[1024];
    char local_array[1024];
    char * dynamic_array;

    dynamic_array = (char *)malloc(1024);

    printf("----------- VAL -----------\n");
    printf("glob1   =>  %p\n", &glob1);
    printf("glob2   =>  %p\n", &glob2);
    printf("st1     =>  %p\n", &st1);
    printf("st2     =>  %p\n", &st2);
    printf("local   =>  %p\n", &local);
    printf("----------- ARR -----------\n");
    printf("static  =>  %p\n", static_array);
    printf("local   =>  %p\n", local_array);
    printf("dynamic =>  %p\n", dynamic_array);
    printf("----------- FUNC ----------\n");
    printf("main    =>  %p\n", main);
    printf("printf  =>  %p\n", printf);
}

書き終えたら、address_check.cという名前で保存して下さい。 先と同じ要領で、address_checkと言う名前の実行可能ファイルを作成して下さい。

(2) プログラムを実行して下さい。

address_check関数を実行すると、それぞれの変数のメモリアドレスが出力されます。

  • glob1, glob2はグローバルセグメントです。
  • st1, st2, static配列は定数セグメントです。
  • local, local配列 及びmainはスタック領域です。
  • dynamic配列はヒープ領域です。
  • printfは共有ライブラリのコードです。

(3) セグメントを上記アドレス空間の図のようにプロットして図示してみましょう。

セグメントの位置関係はOSによって、少しずつ違うそうです。
実際にコードを動かして得られた内容が真です。実行しているOSのアドレス空間を確認しましょう。

思考練習

下記のPHPのコードで、同様に考えてみましょう。

<?php

class Animal {

    function createDog(){
        $a = 4;
        return new Dog($a);  //Dogクラスのインスタンスの参照を返す。
    }

}

function main(){
    $animal = new Animal();

    $dog = $animal->createDog();

    $dog->cry();
}

PHP, Ruby, Java等の最近の高級プログラミング言語は、関数がオブジェクトや文字列を参照で戻すことが出来ます。 つまり、上記でnewされたオブジェクトや、関数実行後に呼び元にセットされたインスタンスは、ヒープに確保された領域内のデータです。

しかし、ヒープの領域は明示的には解放されません。PHP, Ruby, Javaでは、C言語のfreeに相当する関数が提供されていません。 ヒープの領域が解放されないままで残ると、解放されないメモリが徐々に溜まっていきます。これをメモリリークと呼びます。

freeの代わりとして、これらの言語ではGarbage Collectionという機能が存在し、参照されなくなった変数やオブジェクトが含まれるヒープの領域を自動で解放します。

3. ポインタ

C言語において、ポインタとはメモリアドレスを表現するものです。

C言語では、全ての引数は値渡しです。
そのため、基本データ型(char,int,float,double)以外のデータをやり取りしたい場合は
型を指定したメモリアドレスを使って、関数間のデータの受け渡しを行います。

ポインタで活躍する演算子は、すでにご紹介した以下の2つです。

  1. 間接演算子 *
  2. アドレス演算子 &

演習問題3-1

間接演算子とアドレス演算子を使って、ポインタを使ってみましょう。

#include <stdio.h>

int main(){
   int * a;    //aという名前のint型のポインタ変数を宣言
   int b = 5;  //bという名前のint型の変数を宣言し、5で初期化した。
   
   a = &b;     //ポインタaにbのアドレスを代入した。
   
   printf("a => %d¥n", *a);    //aの中身を出力した。
}

ファイルは、pointer.cという名前で保存してください。
同様にコンパイルしてpointerという名前の実行ファイルを作って下さい。

間接演算子は、変数宣言や引数・戻り値の型指定の時と、値を参照する時で、意味合いが異なります。

変数宣言、引数・戻り値の型指定の時

int * a は「int型のポインター」である変数aという意味です。
つまり、意味合い的には int *型という捉え方がわかりやすいです。

値を参照する時の*

printf("a => %d¥n", *a) の時の*は、aというポインタ変数が指し示すを意味します。
今回の例でいうと、ポインタabのアドレスを持っているので、中身は5です。

つまり*a = 5 です。

ポインタ理解の鍵

  1. ポインタの概念
    すでに、高級言語で参照を使ったプログラムを書いているので、比較的すんなりと受け入れることができると思います。 ポインタとは、参照です。
  2. ポインタのソースコード上での表現 C言語の書き方の問題です。慣れの問題とも言えます。 先にもあげた通り、間接演算子は場合によって、意味合いが少しずつ違います。

ポインタの書き方のルール

間接演算子を左寄せ(データ型側にくっつける)のか、右寄せ(変数名側にくっつける)、中立(どちらにもくっつけない)は
コーディング規約に依ります。右寄せが一般的のようです。

プログラムとしては、左寄せint* a、右寄せint *a、中立int * aのいずれも同じです。

ポインタ宣言時の挙動

これは初心者キラーです。ポインタ変数の宣言時は、*がついていても間接参照になりません。

int *a = 4;  //一見出来そうだけど出来ない。
// 下記と同じ意味になってしまう。
// int *a;  
// a = 4;   => ポインタ変数にアドレスじゃない値を入れようとしている。

int b = 5;
int *a = &b;  //これは出来る
*a = 4;       //これは出来る

宣言時の代入は、変数a*がついていたとしても、値を代入することは出来ません。
あくまで、int型のポインタであるaなので、アドレスしか代入することは出来ません。

演習問題3-2

(1) 下記のソースコードはポインターを使った簡単な演算を行っています。
ちゃんと動作するプログラムです。写経してコンパイルして実行して下さい。

#include <stdio.h>

void add(int *a){
    *a = *a + 3;
}

int main(){
    int a = 1;
    add(&a);
    printf("a = %d\n", a);
}

書き終わったら、pointer2.cという名前で保存して下さい。 pointerという名前で実行可能ファイルを作成して、実行して下さい。4が出力されたら成功です。

(2) ソースコードの下記の記述は、それぞれ何を意味するでしょうか?

  1. add関数の引数int * a
  2. add関数内の *aの記述
  3. main関数において、add関数を呼び出す時の引数&aとはなんでしょうか?

大事なことなので、2度言います。
ポインターにおいて、最も重要なことは、概念だけでなく、書き方を正確に覚えるということです。

4. 配列ポインタ

ポインタを覚えたばかりなのに、配列ポインタって何よ?となりそうですが配列はポインタっぽいのです。

そもそもC言語の配列とは?

C言語における配列は、連続したメモリアドレス番地です。
メモリアドレス番地のそれぞれには値が格納されています。

配列宣言時に指定されたサイズで連続したメモリアドレスを確保しています。 配列サイズを後で柔軟に変更したり出来ないのは、そのためです。

演習問題4-1

実際に書いてみるのが一番です。まずは配列を添え字・アドレスで遊んで見ましょう。

#include <stdio.h>

int main(){
    char str[5] = "abcd";
    printf("str[0] => %c¥n", str[0]);
    printf("str[1] => %c¥n", str[1]);
    printf("str[2] => %c¥n", str[2]);
    printf("str[3] => %c¥n", str[3]);
    
    char * is = str; //strの先頭アドレスを取得
    printf("str[0] => %c¥n, *is);
    printf("str[1] => %c¥n, *(is+1));
    printf("str[2] => %c¥n, *(is+2));
    printf("str[3] => %c¥n, *(is+3));
    
}

ファイルは、array.cという名前で保存してください。
同様にコンパイルして、arrayという名前の実行ファイルを作って下さい。

前半は分かりやすいと思います。ほとんどの言語で同じような書き方が出来ると思います。

後半はまずchar * is = strstr配列の先頭アドレスを取得しています。
以降の行では、アドレスに数値を足した後に、間接演算子で中身を見ています。

C言語の配列は、連続したアドレス番地であることが分かって頂けたでしょうか?

演習問題4-2

配列がポインタのように振る舞うことを実感して下さい。

# include <stdio.h>

int main(){
    char str[5] = "abcd";
    printf("str => %s¥n", str);

ファイルは、array_pointer.cという名前で保存してください。 同様にコンパイルして、array_pointerという名前の実行ファイルを作って下さい。

  1. C言語では基本データ型以外を引数に指定する場合は、ポインタを使う。
  2. printfの第1引数はフォーマット文字列のポインタ、第2引数は、指定されたフォーマットのデータ型のポインタです。

この例が正常に動いているということはprintfは、第一引数に渡された文字列リテラルstrもポインタと判断してくれたということです。

配列はポインタ?

配列はポインタっぽいですが、ポインタではありません。
こんがらがって来ますよね?いいんです、こんがらがる所です。

以下の言葉を覚えて帰って下さい。

配列は、登場箇所によってはコンパイラがポインタとして扱ってくれる

配列がポインタとして扱われる登場箇所

  1. 式中の配列名はポインタ
  2. 関数の引数に指定された配列はポインタ

演習問題4-3

配列ポインタ演習のオマケとして、C言語で文字列をコピーする方法を覚えましょう。 (1) 下記のプログラムは、文字列のポインターを関数に渡して、値をセットして貰うプログラムです。

#include <stdio.h>

void setTitle(char * title){
    char title2[] = "C Study";
    title = title2;
}

int main(){
    char title[100];
    setTitle(title);
    
    printf("title is %s\n", title);
}

ファイルは、str_copy.cという名前で保存してください。 同様にコンパイルして、str_copyという名前の実行ファイルを作って下さい。

上手く動くと思ったら、動きません。 title2はmain関数から呼ばれたスタックです。 main関数がprintfを呼ぶときにはsetTitleのスタックは解放されています。 当然、setTitle関数内で定義された変数のメモリ領域も解放されてしまっています。

(2) プログラムを修正して、上手く動作するようにしましょう。

  1. <string.h>という共有ライブラリを追加でincludeする。
  2. 配列のポインタに、文字をコピーする時は、strcpy関数を使います。
    setTitleの中身を下記に書き換えてください。
strcpy(title, "C Study");

再度コンパイルして、実行して下さい。 title is C Studyと表示されたらOKです。

5. 実習問題

今日学んだ内容の理解を深めるための実習問題を用意しました。

実習1 ポインタ

main関数内で、2つのint型の変数x, yを宣言して下さい。初期値は0です。 引数に2つのintのポインタを受け取る関数、goNorthgoEastを作って下さい。

goNorthが呼ばれた場合は、yに1を足して下さい。 goEastが呼ばれた場合は、xに1を足して下さい。

goNorthを4回、goEastを3回実行した後、最後に変数x, yの内容を出力して下さい。

答え

x = 3;
y = 4;

実習2 配列ポインタ

文字列ABCDEを格納する文字列の配列を定義して下さい。 配列のポインタを利用して、先頭文字から順番に、全て小文字にして下さい。

最後に配列内容を出力して、abcdeになることを確認して下さい。

ヒント

  • char 'A' に +32すると小文字になります。
  • 配列の先頭文字のアドレスは配列変数そのものです。
  • 先頭文字のアドレスに+1すると、配列の2つ目のアドレスです。

実習3 完全なるオマケ

pwdコマンドを自作してみましょう。

#include <unistd.h>を、利用する共有ライブラリとして追記して下さい。 カレントディレクトリを取得する関数は、getcwd関数です。 getcwdのインターフェース

char * getcwd(char *buf, size_t size);

※size_tはint型だと思って下さい。 実行してカレントディレクトリが表示されたらpwdコマンドの完成です

参考図書

hanhan1978が学習に用いた本達です。良書厳選。

  • プログラミング言語C (K&R)

+ Head First C

+ 詳説Cポインタ

+ エキスパートCプログラミング―知られざるCの深層