プログラミングの基礎からやり直そうという気持ちで始めたC言語ですが、普段あまり使わないので、いざという時に参考になるように基本中の基本をまとめておきたいと思います。あくまで個人的なメモ書きという意味合いが強いので、各サンプルの詳細については割愛させて頂きます。

このポストも随時更新していく予定です。

Hello World

最初は、コンソール画面に「Hello World」を出力するサンプルです。

#include <stdio.h>

int main()
{
  printf("Hello, world!¥n");

  return 0;
}
Hello, world!

整数と実数の出力

数値には色々な型が存在しています。それぞれ表現できる最大最小値も違いますが、それを出力する方法もそれぞれです。次はは複数の数値型をprintf関数を使って同時に出力するサンプルです。

#include <stdio.h>

int main()
{
  unsigned short num1 = 65536;
  unsigned int num2 = 4294967296;
  char num3 = 128;
  float num4 = 1.800000f;
  double num5 = 2.900000;
  long double num6 = 3.7l;
  
  // 型によって書式文字列の書き方も変わってくる
  printf("%u %u %d %f %f %Lf\n",
    num1, num2, num3, num4, num5, num6);
  // sizeof演算子は型や変数の大きさを教えてくれる
  printf("%d %d\n", sizeof(num5), sizeof(num6));
  
  return 0;
}
0 0 -128 1.800000 2.900000 3.700000
8 16

変数num1からnum3は整数型で、型の最大値を超えているため、「オーバーフロー」していることが分かります。実数型は後ろにflが付くなど、特殊であるため注意が必要です。

ビット演算子

+-などの演算子は人間が扱う10進数の数値に対して演算を行いますが、ビット演算子は0011のようなパソコンが扱う2進数の数値に対して演算を行います。ビット演算子には以下の種類があります。

  • AND(&) 両方のビットが1の場合1になる
  • OR(|) どちらかのビットが1の場合1になる
  • XOR(^) 両方のビットが異なる場合1になる
  • NOT(~) ビットが反転する
#include <stdio.h>

int main()
{
  unsigned int num1 = 1; // 0000 0001
  unsigned int num2 = 2; // 0000 0010
    
  printf("%u\n", num1 ^ num2);
  printf("%u\n", num1 | num2);
  printf("%u\n", num1 & num2);
  printf("%u\n", ~num1);
    
  return 0;
}
3
3
0
4294967294

シフト演算子

実はシフト演算子も上記のビット演算子の種類ですが、少し考え方が変わりますので別途に分けました。次はシフト演算子を応用したサンプルになります。

#include <stdio.h>

int main()
{
  unsigned char flag1 = 1 << 7; // 1000 0000
  unsigned char flag2 = 1 << 3; // 0000 1000
    
  // 1000 0000 OR 0000 0100
  flag1 |= 1 << 2;
  // 1000 0100 AND 0111 1111
  flag1 &= ~(1 << 7);
  // 0000 1000 XOR 0000 1000
  flag2 ^= 1 << 3;

  printf("%u %u\n", flag1, flag2);

  return 0;
}
4 0

シフト演算子は<<>>の二種類があり、ビットを左右にシフトしてくれます。1シフトする度に、<<の時は数値が2倍に、>>の時は1/2倍になる特徴があります。

ループ処理

条件分岐のifと同じようにループ処理と言えば、誰もがforwhileを思い浮かべるでしょう。次は、ランダムを使った簡単なサイコロです。forで3回サイコロを振りますが、do whileで「6」がでたら、再度サイコロを振るシンプルなサンプルです。

#include <stdlib.h>
#include <time.h>

int main()
{
  srand(time(NULL));
    
  int dice = 0;
    
  for (int i = 0; i < 3; i++)
  {
      do
      {
          dice = rand() % 6 + 1;
          printf("Game %d : %d\n", i, dice);
      // サイコロが6の場合、再度doを実行する
      } while (dice == 6);
  }
    
  return 0;
}
Game 0 : 1
Game 1 : 6
Game 1 : 1
Game 2 : 5

ポインタ

Cの難関とも言われるポインタですが、コンピュータのメモリを理解していれば、難しくはありません。単純に変数のアドレスを入れる変数と考えてください。次は、ポインタのポインタとメモリの動的メモリを扱うサンプルです。

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

int main()
{
  int *ptr1; // ポインタを宣言
  int **ptr2; // ポインタのポインタを宣言
  int *ptr3;
  int num = 10;
  
  ptr1 = &num; // 変数numのアドレスを代入
  ptr2 = &ptr1; // ポインタptr1のアドレスを代入
  ptr3 = malloc(sizeof(int)); // 動的メモリの確保
  
  *ptr3 = 20; // ポインタを逆参照して値を代入
  
  printf("%d\n", **ptr2); // ポインタのポインタを逆参照
  printf("%d\n", *ptr3); // ポインタを逆参照
  
  free(ptr3); // 動的メモリを解放

  return 0;
}
10
20

int *ptrと宣言をした状態には、ptrが指すアドレスにゴミのデータが格納されています。このポインタ(アドレス)に、ゴミではない値があるアドレス(&num等)の代入や、動的なメモリの確保(malloc)及び初期化(memset)によって、メモリを活用することができます。

配列

配列は同じ型のデータを並べたものです。連続性を持っているため、大量のデータでも繰り返し処理を使って簡単に扱うことができます。配列の中に配列を入れた多次元配列もありますが、まずは1次元配列のサンプルです。

#include <stdio.h>

int main()
{
  int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; // 配列の宣言と初期化
  int sum = 0;
  int *ptr = arr; // int型ポインタに配列1番目のアドレスを代入

  for (int i = 0; i < sizeof(arr) / sizeof(int); i++) // 配列のサイズ / 型のサイズ = 要素数
  {
    sum += arr[i];
  }

  printf("%d\n", sum); // 要素の合計
  printf("%d\n", *ptr); // 1番目要素
  printf("%d\n", *arr); // 1番目要素
  printf("%d\n", ptr[7]); // 8番目要素

  return 0;
}
55
1
1
8

int arr[10]のように配列を宣言し{}の中にデータを入れます。データを扱う際は、arr[0]のように要素のインデックスを指定します。また、arrのようにインデックスを外すと、1番目要素のポインタとしても扱うことができます。

動的配列

前項より配列をポインタとして扱えることを確認しました。同じく、ポインタにメモリを動的に割り当てることで、動的な配列を作ることが可能です。次は二重ポインタを使い、2次元配列を作るサンプルです。

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

int main()
{
  int **m = malloc(sizeof(int *) * 3); // 二重ポインタにintポインタ3個分を割当
  for (int i = 0; i < 3; i++)
  {
    m[i] = malloc(sizeof(int) * 4); // ポインタにintポインタ4個分を割当
  }

  m[0][1] = 1; // 縦「0」横「1」のインデックスに値を割当
  m[2][3] = 5; // 縦「2」横「3」のインデックスに値を割当

  printf("%d\n", m[0][1]);
  printf("%d\n", m[2][3]);

  for (int i = 0; i < 3; i++)
  {
    free(m[i]); // 横のポインタのメモリを全て解放
  }

  free(m); // 縦のポインタのメモリを解放

  return 0;
}
1
5

2次元配列はarr[0][1]のように表現し、最初のインデックスが「縦」次が「横」のサイズになります。1次元配列では、要素のアドレスに「値」が存在していましたが、2次元配列では、更に1次元配列を指す「ポインタ」があることを意識してください。

文字列

文字列とは文字(char)が複数続いた状態を意味します。Javaのような言語では文字列を格納できる資料型がありますが、Cにはないので、次のようにcharのポインタで文字列を扱います。

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

int main()
{
  char *str1 = " World"; // str1に文字列をアドレスを格納
  char *str2 = malloc(sizeof(char) * 20); // str2にメモリ領域を確保

  strcpy(str2, "Hello"); // str2に文字列"Hello"をコピー

  strcat(str2, str1); // str2にstr1の文字列を結合

  printf("%s\n", str2);

  free(str2);

  return 0;
}
Hello World

基本はcharのポインタに文字列を格納する必要があります。文字列は配列のように格納されているので、str[1]といった方法で扱うこともできます。しかし、こうした方法では文字列の編集ができないため、動的メモリに対して文字列を扱う必要があります。

char str2[20];のように宣言だけの状態は書き込みが可能です。

構造体

膨大なデータを扱うと変数や配列だけではデータを管理するのが難しくなります。C言語ではデータを管理しやすくするための「構造体」が用意されています。struct 構造体名 { 資料型 メンバー名; };のように定義をし、struct 構造体名 変数名のように変数を宣言します。構造体のメンバーへの値の割当や出力はサンプルをご確認ください。

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

// 匿名構造体の定義
typedef struct {
  char name[20];
  int age;
} Person;

int main()
{
  Person chino; // 匿名構造体の宣言
  
  strcpy(chino.name, "香風智乃"); // 文字列データの割当
  chino.age = 14; // 数値データの割当
  
  // 匿名構造体ポインタの宣言と、メモリの割当
  Person *cocoa = malloc(sizeof(Person));
  
  strcpy(cocoa->name, "保登心愛"); // 文字列データの割当
  cocoa->age = 15; // 数値データの割当
  
  printf("名前:%s\n", chino.name);
  printf("年齢:%d\n", chino.age);
  
  printf("名前:%s\n", cocoa->name);
  printf("年齢:%d\n", cocoa->age);
    
  free(cocoa); // メモリの解放
  
  return 0;
}
名前:香風智乃
年齢:14
名前:保登心愛
年齢:15

上記ではtypedefを使った匿名構造体及び、構造体ポインタを使って構造体の宣言を簡略化していますが、これを普通の使い方だと認識しても問題はありません。

共用体

宣言方法や使い方は構造体とあまり変わりませんが、すべてのメンバーが同じメモリ領域を共有する特徴があります。つまり、メンバーが多くても共用体の大きさは、一番大きいサイズのメンバーのサイズに固定されます。次は、リトルエンディアンの概念を応用したサンプルになります。

#include <stdio.h>

// 匿名共用体の定義
typedef union {
  char c1;
  short num1;
  int num2;
} Data;

int main()
{
  Data data; // 匿名共用体の宣言
  
  // リトルエンディアンの場合、78 56 34 12と格納される
  data.num2 = 0x12345678;
  
  // 16進数で出力
  printf("0x%x\n", data.num2);
  printf("0x%x\n", data.num1);
  printf("0x%x\n", data.c1);
  
  // 共用体のサイズ
  printf("%d\n", (int)sizeof(data));
    
  return 0;
}
0x12345678
0x5678
0x78
4

共用体の宣言やメンバーの扱い方は構造体と変わりません。ただ、先述した通り、メンバーが同じメモリ領域を共有するため、共用体のサイズはメンバーの中で一番大きい、int型のサイズ(4バイト)となります。もし、int型メンバーに値を格納して、short型メンバーに値を格納した場合、前に格納した値は上書きされます。

上記のサンプルはメモリの構造を見やすくするために16進数で4バイトの値を格納しています。リトルエンディアンの場合、先頭の1バイトが、メモリ領域の一番後ろに格納されます。共用体の特徴も合わせて、ご確認ください。

構造体と共用体の応用

構造体と共用体、そしてリトルエンディアンの復習も兼ねて簡単なおもしろサンプルを作成してみました。このサンプルで使っている特定の言葉については、あくまで「サンプルデータ」ですので、特定企業に対する個人的な感情などは一切ありません。

#include <stdio.h>

struct AmazonOK {
  union {
    unsigned long long hex;
    struct {
      unsigned char string[8];
    };
  };
};

int main()
{
  struct AmazonOK amz;
  
  amz.hex = 0x616d617a6f6e6f6b;
  
  for (int i; i < sizeof(unsigned long long); i++)
  {
    printf("%c", amz.string[i]);
  }
  printf("\n");
  
  return 0;
}
konozama

long long型の変数hexに16進数で表現している文字列amazonokを代入しています。共用体の中で定義をしているので、charの配列stringからも同じメモリへアクセスができます。しかし、hexに代入した8バイトのデータはリトルエンディアンとして保持されているため、先頭から順番に読み込むと逆さになるわけです。

ポインタ演算

ポインタとはアドレスを入れる変数ですが、+-の演算子を使って演算をすることも出来ます。もちろん、実際の値ではなくアドレスの値が変化します。例えばintのポインタに1を足したらアドレスがintの大きさである4だけ増えます。もしint arr[2] = { 1, 2 };のような配列があれば、*(arr + 1)arr[1]と同じことになります。次は構造体とvoidポインタを使ったポインタ演算のサンプルです。

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

struct Person {
  char name[20];
  int age;
};

int main()
{
  void *ptr = malloc(sizeof(struct Person) * 2);
  struct Person p[2];

  strcpy(((struct Person *)ptr)->name, "香風智乃");
  ((struct Person *)ptr)->age = 14;

  strcpy(((struct Person *)ptr + 1)->name, "保登心愛");
  ((struct Person *)ptr + 1)->age = 15;

  memcpy(p, ptr, sizeof(struct Person) * 2); // 構造体の配列にメモリ領域をコピー

  printf("%s %d\n", p[0].name, p[0].age);
  printf("%s %d\n", ((struct Person *)ptr + 1)->name, ((struct Person *)ptr + 1)->age);

  free(ptr);

  return 0;
}
香風智乃 14
保登心愛 15

voidポインタは型を持たないポインタのため、(struct Person *)ptrのように型変換をしないとポインタ演算は出来ません。voidポインタの型変換とポインタ演算を除けば特別な内容はありませんが、想定外のメモリ領域に触れないよう注意が必要です。

関数

繰り返して使いたい処理や、汎用的に使う処理などは毎回記述するよりも、関数として必要な時に呼び出すのが効率的でコードも見やすくなります。今まではmain関数で全ての処理を記述しましたが、今回はステータス管理などに使う列挙型を含めて、関数の作成と呼び出しのサンプルを用意しました。

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

enum SHOP {
  RABBIT_HOUSE = 0,
  AMAUSAAN,
  FLEUR_DU_LAPIN
};

struct Employee {
  char name[20];
  enum SHOP shop;
};


void newEmp(struct Employee *emp, char name[20], enum SHOP shop)
{
  strcpy(emp->name, name);
  emp->shop = shop;
}

void printEmp(struct Employee *emp)
{
  printf("%s\n", emp->name);
  switch (emp->shop) {
    case 0:
      printf("%s\n", "ラビットハウス");
      break;
    case 1:
      printf("%s\n", "甘兎庵");
      break;
    case 2:
      printf("%s\n", "フルール・ド・ラパン");
      break;
  }
}

int main()
{
  struct Employee *chino = malloc(sizeof(struct Employee));
  newEmp(chino, "香風智乃", RABBIT_HOUSE);
  printEmp(chino);

  return 0;
}
香風智乃
ラビットハウス

処理は単純に構造体に中にデータを入れて、そのデータを出力するたけのプログラムですが、データ登録とデータ出力を関数にしています。上記の関数は戻り値がないため、void 関数名(引数1、引数2…){処理}のように定義しています。main関数より従業員を表す構造体のポインタにメモリを割り当てて、newEmp関数に名前とお店を指す列挙定数を渡します。newEmp関数の呼び出しにより構造体のメモリにデータが登録され、その構造体のポインタをprintEmp関数に渡すことで、データを出力します。

もし、関数をmain関数より下に定義したい場合は、main関数の上に追加した関数のプロトタイプを宣言する必要があります。