【C言語】
整数表現で int より狭いchar/short型を多用すると
符号拡張の事故にあう

warning: conversion to ‘size_t’ from ‘short’ may change the sign of the result

警告:shortからsize_tに変換すると結果の符号が変わる可能性がある
[-Wsign-conversion]
[-Wconversion]

■この記事の概要

この記事では、C言語でintより狭い型(charshort)を使用する際の問題点を解説しています。符号拡張や型変換によるバグのリスク、メモリ節約の誤解を指摘し、整数型は基本的にintを使用することを推奨。具体例を挙げて、可読性と安全性を確保するプログラミングの重要性を述べています。


■昔メモリの1byteは血の一滴

昔メモリの1byteは血の一滴とする時代があって、
整数を表現する際もint型ではなくて狭い型のchar/short型を多用していました。

今でもこの文化を継承しているプロジェクトは多々ありますが、
桁あふれや符号拡張等の面倒くさい事故にあうリスクが増えるので
メモリをケチるのは止めましょう

というか、狭い型のchar/short型を多用してもメモリ節約にならない事を理解しましょう。


■short型の符号拡張事故例

#include    <string.h>
char    buf[0x8000] ;
int main(void){
    short   size = 0x8000; 
    memset(buf,'X',size); //NG:異常終了する
}

short型で0x8000を表現できず0x8000を代入すると負値になります。

memset()の第三引数はsize_t型にもかかわらずshort型を使用したため
型変換(符号拡張)が発生し
0x8000 ではなくて
0xFFFF8000がmemset()に渡ります。

memset()に0xFFFF8000という非常に大きな値が渡ったため領域外破壊が発生し多くの場合プログラムは異常終了します。

注意:
gcc -Wall -Wextra だけでは警告は出ません。
[-Wsign-conversion]
[-Wconversion]を追加指定して下さい。


■char型の無限ループ事故例

#include    <stdio.h>
void f(int max) {
    for(char i = 0 ; i < max;i++){
        printf("%d\n",i);
    }
}
int main(void) {
    f(256);
}

このコードは
gcc -Wallでも
clang -Weverythingでも警告が出ませんが
char型では256を表現できないので
無限ループします。


■扱う数値で型を色々使うのは避けよう

#include    <stdio.h>
int main(void)  {
    char    c;  
    scanf("%d",(int *)&c);
    printf("コーヒーはコンビニで%d円で買える\n",c);
    short   s;
    scanf("%d",(int *)&s);
    printf("一か月の小遣いは%d円\n",s);  
    int i;
    scanf("%d",(int *)&i);
    printf("サラリーマンの生涯収入%d円",i);
}

コーヒーはコンビニで100円なのでchar型で表現可能。
月の小遣いは3万円以下なのでshort型で表現可能。
と、
最大値に応じて型をchar/short/intと様々な型を使うのは
止めましょう。

整数を扱うならば基本は int 型です。
最大値に応じて型を分けるのは無意味に几帳面でハイリスクです。
上記のコードでは”%d”書式に対応する変数はint型(4byte)必要ですがcharは1byte、shortは2byteしかありません。
1byteや2byteしかない領域に4byte書き込むのでメモリ破壊が発生してプログラムが異常終了する場合があります。


現代の32/64bitのコンパイラでは int 型は 2147483647≒21億まで表現できます。

「最大値をchar/shortで表現可能だからchar/shortを使う」と言った
貧乏くさい発想は止めましょう。

整数を扱うならば 基本は int 型を使用して
符号拡張、データ欠損、メモリ破壊といった事故を回避しましょう。


■整数を扱うならば基本は int 型を使う

#include    <stdio.h>
int main(void)  {
    int    c;  
    scanf("%d",&c);
    printf("コーヒーはコンビニで%d円で買える\n",c);
    int   s;
    scanf("%d",&s);
    printf("一か月の小遣いは%d円\n",s);  
    int i;
    scanf("%d",&i);
    printf("サラリーマンの生涯収入%d円",i);
}

整数はint型が基本,無駄なキャストも削除

■除夜の鐘を数える型を検討してください

#include <stdio.h>
#include <stdint.h>
#include <stdint.h>
#if     TYPE==11
//メモリの1バイトは血の一滴    
//除夜の鐘は108より大きくならないのでchar型で足りる
#define TYPE_X          char 
#elif   TYPE==12
//符号付きを明示する
#define TYPE_X          signed char
#elif   TYPE==13
//符号無しを明示する
#define TYPE_X          unsigned char
#elif   TYPE==14
#define TYPE_X          int8_t
#elif   TYPE==15
#define TYPE_X          uint8_t
#elif   TYPE==16
//基本型はtypedefする(MISRA-C 4.6)
typedef unsigned char   UB;
#define TYPE_X          UB
#elif   TYPE==41
//除夜の鐘は負値にならない
#define TYPE_X          unsigned int
#elif   TYPE==42
#define TYPE_X          int32_t
#elif   TYPE==43
#define TYPE_X          uint32_t
#elif   TYPE==44
#define TYPE_X          size_t
#else
//整数を扱うのに最も自然な型を使う
#define TYPE_X          int
#endif
int main(void){
    
    for(TYPE_X i = 108;i >= 0 ;i--){
        printf("除夜の鐘残り%d突き\n",i);
    }
}

符号無し系は無限ループして面倒な事になるし、
int より狭い型は符号拡張して面倒な事になるので
筆者は最も自然な int 型を推奨します。


参考:

C-FAQ 1.1: どの整数型を使えばよいか、どうやって決めればよいか。

http://www.kouno.jp/home/c_faq/c1.html#1

int よりも狭いビット幅の型の多用を避ける。

https://www.ipa.go.jp/security/awareness/vendor/programmingv2/contents/c907.html

INT02-C. 整数変換のルールを理解する

https://www.jpcert.or.jp/sc-rules/c-int02-c.html

■アンケート

2
除夜の鐘を数えるのに適切な変数の型は?