データ型

基本的なデータ型

データ型とは、変数や関数の戻り値などで使用する、プログラムで実際に扱う「値」の形式のことです。
いままで登場したint型は整数を表す型、double型は小数を表す型です。

C言語には以下のようなデータ型が存在します。

char
1バイト符号付整数。
-128~127の値を表現できる。
主に英数字を一字を表現する用途に使用する。
short
2バイト符号付整数。
-32,768~32,767の値を表現できる。
int
4バイト符号付整数。
-2,147,483,648~2,147,483,647の値を表現できる。
(約-21億~21億)
64ビット環境では8バイトとなる環境もある。
(ちなみに16ビット環境では2バイト整数だった)
long
4バイト符号付整数。
64ビット環境では8バイトとなる環境もある。
(UNIX系OSなど)
long long
8バイト符号付整数。
-9,223,372,036,854,775,808~9,223,372,036,854,775,807の値を表現できる。
(約-922京~922京)
float
4バイト単精度浮動小数点実数。
double
8バイト倍精度浮動小数点実数。
floatよりも高い精度の小数を表現できる。

これらはC言語の基本的なデータ型で、プリミティブ型といいます。

バイトとは、コンピューターの情報量(記憶容量)の単位のひとつです。
1バイトで英数文字を一文字分記憶できます。
(1バイト=8ビット=2の8乗=256通りの情報を記憶できる)

整数のデータ型は、記憶できる数値の幅が違うだけで使い方は同じです。
大きい桁数の数値を使いたい場合には、より大きなバイト数を持つデータ型を使用します。

小数を含む値を使用する場合は、float型かdouble型のどちらかを使用します。
これらのデータ型が実際に記憶できる数値の幅はちょっと複雑なため、省略します。

整数型や小数型をまとめて算術型といいます。

64ビット版のWindowsでは、int型、long型は4バイト(32ビット)です。
64ビット版のUnix系OS(多くのLinux、MacOSなど)はint型は4バイト(32ビット)、long型は8バイト(64ビット)です。

符号なし整数

上記に加えて、整数のデータ型には以下の分類が存在します。

signed
符号付き整数。
+-記号を付けられる整数。
通常は省略される。
unsigned
符号なし整数。
+記号のみを付けられる整数。
正の数(0以上の数)しか表現できない。
その代わり、正方向への表現できる数が倍になる。
(unsigned charならば0~255の範囲を表現できる)

これらは各データ型の名称の前に記述します。


int main()
{
    //signedは付けてもつけなくても同じ
    int a = 1;
    signed int b = 1;
	short c = 1;
	signed short d = 1;
    
    //unsigned型はマイナス値を持てない
    unsigned char e = 1;
	unsigned short f = 1;
	unsigned int g = 1;
	unsigned long h = 1;
	unsigned long long i = 1;
}

signedもunsignedも記述しない場合、signedを指定したものとみなされます。
なので、通常はsignedの記述は省略されます。

unsigned型整数はマイナスの値を保存することができません。
そのかわり、プラス側には符号あり型の変数の倍の大きさの値を保存できます。
例えば「signed char」型なら「-128~127」の範囲の値を表現できますが、「unsigned char」型の場合は「0~255」の範囲の値を表現できます。

小数型には符号なし型はありません。

なお「signed」のみを記述した場合は「signed int」とみなされます。
「unsigned」のみを記述すると「unsigned int」とみなされます。


int main()
{
	//どちらも符号ありint型
	signed a = 0;
	signed int b = 0;

	//どちらも符号なしint型
	unsigned c = 0;
	unsigned int d = 0;
}

「short」「long」「long long」は、それぞれ「short int」「long int」「long long int」の省略形です。
しかし冗長になるだけなので、普通は省略形で記述します。


int main()
{
	//一緒
	short a = 0;
	short int b = 0;

	//一緒
	long c = 0;
	long int d = 0;

	//一緒
	long long e = 0;
	long long int f = 0;
}

文字型としてのcharと数値型としてのchar

char型は主に一文字(半角英数記号、および制御文字)を格納する用途に使用されますが、内部的には他の整数型と同じように数値で管理されています。
コード中に単に「char」と書いた場合、それが符号あり整数なのか符号なし整数なのかは実は決められていません。
charは、signed charunsigned charとは異なる型として定義されています。

char型の符号の有無は環境により異なるため、char型を文字型ではなく整数型として使用する場合は注意が必要です。
サイズの小さな整数型としてchar型を使用する場合は明示的に符号を付けることで、別の環境でも期待通りに動作するコードになります。


//char型は符号あり整数ではないかもしれないので
//環境によっては意図しない値となる可能性がある
char c = -1;

//char型を整数として使用する場合は
//符号の有無を明示しておくと安心
signed char sc = -1;
unsigned char uc = 1;

ちなみに、char型はほとんどの場合で256通りの値が使用できる「8ビット」のサイズで扱われますが、まれに7ビットで扱う環境も存在します。
(7ビットは128通りの値を扱える)
char型、signed char型、unsigned char型のサイズは全て同じです。

リテラル

コード中に直接記述される、数値や文字列などの値をリテラルといいます。


int num = 10;
char moji = 'A';
printf("あいうえお");

このコード中の「10」「'A'」「"あいうえお"」がリテラルです。

文字リテラル

シングルクォーテーションで囲われた、コード中に直接記述される文字を文字リテラルと言います。

文字列リテラル

ダブルクォーテーションで囲われた、コード内に直接記述される文字列を文字列リテラルと言います。

変数

文字列リテラルはchar型の配列、またはchar*型(char型のポインタ)変数に格納できます。
文字列の扱い方
ポインタと文字列


char str1[] = "ABC";
char *str2 = "ABC";

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

NULL文字

文字列リテラルの末尾には自動的にNULL文字が追加されます。


char str1[] = "ABC";
//↑と↓は同じ
char str2[] = { 'A', 'B', 'C', '\0' };

「\0」がNULL文字です。
これは文字列の終端を表す特殊な文字です。
char型配列であっても終端にNULL文字がない場合、それはただの「文字の配列」であって、C言語では文字列として扱うことはできません。

文字列リテラルの改行

文字列リテラルの途中に円記号(バックスラッシュ)を書くと、コード中の文字列を途中で改行することができます。
実際に出力される文字列は改行されません。


printf("いろはにほへと ちりぬるを \
わかよたれそ つねならむ");
いろはにほへと ちりぬるを わかよたれそ つねならむ

円記号は環境によって日本円の記号(¥)またはバックスラッシュ(\)で表示されます。
ここではバックスラッシュは全角文字で表示していますが、本来は半角文字です。

文字列リテラルの連結

文字列リテラル同士を連続で書くと、文字列は結合しているものとみなされます。
途中の空白やタブ文字、改行は無視されます。


printf("ABC" "DEF");
//↑↓同じ
printf("ABC""DEF");
//↑↓同じ
printf("ABCDEF");

これを利用してコード中の文字列を改行することもできます。


printf("いろはにほへと ちりぬるを"
    " わかよたれそ つねならむ");

数値リテラル

コード中にそのまま記述される数値を数値リテラルといいます。


//「10」が数値リテラル
int num = 10;

小数値の整数部が0の場合、0の記述は省略できます。


double d;

//どちらも同じ
d = 0.5;
d = .5;

サフィックス

数値リテラルは、整数値をそのまま書くとそれはint型の整数とみなされます。
小数値をそのまま書くとdouble型とみなされます。

これらの末尾に特殊な文字を加えることで、そのデータ型を変更することができます。
この特殊文字をサフィックスといいます。


int _int				= 10;
double _double			= 10.0;
unsigned int _uint		= 10U;
long _long				= 10L;
unsigned long _ulong	= 10UL;
long long _longlong		= 10LL;
unsigned long long _ull = 10ULL;
float _float			= 10.0F;

サフィックスには以下があります。

U,u
unsigned int
L,l
long
UL, ul
unsigned long
LL, ll
long long
ULL, ull
unsigned long long
F, f
float

大文字、小文字はどちらでも構いませんが、Lの小文字は数字の1と紛らわしいため、大文字で統一したほうがいいでしょう。

なお、サフィックスを付けずに整数値をそのまま記述した場合でも、int型で表現できる範囲外の大きな値である場合はunsigned int型やlong long型など、その値が表現できるうちの最小のデータ型であるとみなされます。
unsigned long long型で表現できる値以上を記述した場合はエラーとなります。

プレフィックス(プリフィックス)

数値リテラルの先頭に特定の文字を追加することで8進数や16進数を表現することができます。
これをプレフィックス(プリフィックス)といいます。


#include <stdio.h>

int main()
{
    int oct = 010;	//8進数
    int hex = 0x10;	//16進数

    printf("%d\n", oct);
    printf("%d", hex);
    getchar();
}
8
16
0
8進数を表す
0x
16進数を表す

8進数は桁ひとつで「0~7」を表しますから、2桁目を「1」にすると「8」を表します。
16進数は桁ひとつで「0~15」を表しますから、2桁目を「1」にすると「16」を表します。

16進数は数字だけでは数が足りないので、10以降はA~Fのアルファベットを使用します。
大文字小文字は区別されません。


#include <stdio.h>

int main()
{
	int hex9 = 0x9;
	int hex10 = 0xA;
	int hex11 = 0xB;
	int hex15 = 0xF;
	int hex16 = 0x10;

	printf("%d\n", hex9);
	printf("%d\n", hex10);
	printf("%d\n", hex11);
	printf("%d\n", hex15);
	printf("%d\n", hex16);
    getchar();
}
9
10
11
15
16

8進数、16進数をそのまま画面に表示する場合は、printf関数の変換指定子を以下のように書き換えます。


#include <stdio.h>

int main()
{
    int oct = 010;
    int hex = 0x10;

    //8進数表示
    printf("%o", oct);

    printf("\n");

    //16進数表示
    printf("%x", hex);

    getchar();
}

このコードはどちらも「10」と表示されます。

コンパイラによっては「0b」で2進数を表すことができます。


int bin = 0b1101;
printf("%d", bin);
13

桁あふれ(オーバーフロー)

ラップアラウンド

それぞれのデータ型には、保存できる値の幅に制限があります。
ではその幅を超えた値を変数に代入するとどうなるでしょうか。


#include <stdio.h>

int main()
{
    unsigned char uc = 255;
    uc = _uchucar + 1;
    printf("%d", uc); //0
    getchar();
}

unsigned char型には0~255の値を保存できます。
そのため5行目の時点では問題はありません。

次の行で255に1を足すと256となり、unsigned charの限界を超えてしまいます。
これを桁あふれ(オーバーフロー)といいます。

このコードを実行すると、結果は「0」と表示されます。
255から一巡してまた0に戻ってきてしまったのです。

反対に、0から1を引くと255になります。

厳密に言えば、符号なし整数のオーバーフローはラップアラウンドといいます。
ラップアラウンドはC言語の規格通りの動作で、それ自体はエラー等にはなりません。

符号あり整数の場合

上記は符号なし整数(unsigned)で説明しましたが、符号あり整数の場合はこのような動作になる保証はありません。


#include <stdio.h>

int main()
{
    signed char sc = 127;
    sc = sc + 1;
    printf("%d", sc); //-128...?
    getchar();
}

char型の変数には-128~127の整数が保存できます。
すでに127が保存されている変数に1を足すとどうなるでしょうか。

実際にやってみると、おそらく「-128」と表示されると思います。
なんだ結局一巡するんじゃないか、と思われるかもしれませんが、この動作はただの偶然です。

符号付き整数でオーバーフローが発生した場合、どういった動作になるかはC言語では決められていません。
つまり一巡して最低値に戻るとは限らず、無茶苦茶な値になったりプログラムがクラッシュしたり…という可能性もゼロではないのです。
(Visual Studioではたぶん一巡してくれますが、C言語の仕様上は符号付き整数のオーバーフローは禁則)

符号付き整数でも符号なし整数でも、オーバーフローが発生すると意図しない値になることは変わらないので、オーバーフローはできるだけ発生させないようにプログラミングする必要があります。
(ただし、符号なし整数をあえてオーバーフローさせることは可能です)

未定義の動作

C言語などのプログラミング言語には「規格」があり、各コンパイラ(Visual Studioやgccなど)はその規格に沿って作られています。
しかしすべての場合においての事細かな取り決めがあるわけではなく、規格に定義されていない動作も存在します。
符号付き整数のオーバーフローがその代表的な例で、こういったものを未定義の動作といいます。
(符号なし整数のラップアラウンドは定義通りの動作です)

未定義の動作の実行については、C言語の規格としてはどのような動作をしてもかまわない、とされています。
つまり同じコードでもコンパイラによって動作が異なるので、あるコンパイラでは(一見して)エラーに見えないように処理されたり、あるコンパイラではプログラムが異常終了したり、ということになり得ます。
同じコンパイラでもバージョンアップによって動作が変更される可能性もあり得るので、こういったコードは可搬性(移植のしやすさ)がなく避けるべきです。