9
6

More than 1 year has passed since last update.

printf のマイナーな機能の紹介

Last updated at Posted at 2018-12-20

C言語アドベントカレンダー の 12/20 が空いていたので、ちょっと書いてみた。

printf の書式文字列について、私が知らなかったことをメモする。

動作確認は、macOS Mojave 上の clang と gcc-8 で行った。

桁区切り文字を出力するフラグ文字

具体的にはこんな感じ:

c11
#include <stdio.h>
#include <locale.h>

int main(void)
{
  setlocale(LC_ALL, "sk_SK.ISO8859-2" ); // Slovak - SLOVAKIA
  printf("%'d\n", 12345678); //=> 12 345 678
  printf("%'f\n", 12345678.12345678); //=> 12 345 678,123457
  return 0;
}

ロケールによって出たり出なかったりする桁区切り文字。
スロバキアでは空白区切りらしい。

桁区切り文字とは関係ないけど、小数点がコンマになっているところにも注目。

なお。

デフォルトロケールだとシングルクオートなしと同じ結果になるので面白くない。

intmax_t 用の長さ修飾子

C99 には intmax_t, uintmax_t という、long long よりも長いかもしれない整数型がある。
というわけで、それ用の長さ修飾子がある。
こんな感じ。

c11
#include <stdio.h>
#include <stdint.h>

int main(void)
{
  uintmax_t u = UINTMAX_MAX;
  intmax_t i = INTMAX_MAX;

  printf("%ju\n", u ); // => 18446744073709551615
  printf("%jd\n", i ); // => 9223372036854775807
  return 0;
}

でも、手元の処理系では intmax_t, uintmax_t ともに 64bit なのであんまり面白くない。

char であることを示す長さ修飾子

char はどうせ int に昇格するので、コンパイラに警告を出させる以外にはあまり意味がない。

こんな感じ。

c11
#include <stdio.h>

int main(void)
{
  signed char sc = -128;
  unsigned char uc = 255;
  printf( "%hhd %d %hhu %u\n", sc, sc, uc, uc ); //=>-128 -128 255 255
  printf( "%hhd %d %hhu %u\n", -400, -400, 400, 400 ); //=> 112 -400 144 400
  return 0;
}

clang の場合、あとの方の printf は

warning: format specifies type 'char' but the argument has type 'int' [-Wformat]
warning: format specifies type 'unsigned char' but the argument has type 'int' [-Wformat]

という警告が出る。
gcc-8 は警告出してくれない。

※ ここから、2022年1月加筆

……と書いておいたら、ありがたいコメント が。

なるほど。

私としては負の値を 16 進数で出すことは稀なので気づきませんでした。

と思った上で man を見てみると

c11
printf( "foo %hhn", &x );

とすると、&x を char* だと思ってくれるという機能もあるらしい。不要。

※ ここまで、2022年1月加筆

ptrdiff_t 用の長さ修飾子

size_t 用の z は知っていたけど、ptrdiff_t 用の t は知らなかった。

c11
#include <stdio.h>

int a[1ULL<<44]; // 意外と行ける

int main(void)
{
  printf( "%td\n", a+sizeof(a)/sizeof(*a) - a ); // => 17592186044416
  return 0;
}

16進数の浮動小数点変換指定子

誤差なく浮動小数点を出力したい場合に必要になる、16進数浮動小数点。

c11
#include <stdio.h>

int main(void)
{
  printf( "%a %f\n", 0.1, 0.1 ); //=> 0x1.999999999999ap-4 0.100000
  return 0;
}

とはいえ、出てきた値を読むのが面倒なのであまり使いみちがない。

脆弱性の原因となる %n

昔からあるらしいけど、知らなかった。
C11 から入る printf_s には含まれないらしい。

c11
#include <stdio.h>

int main(void)
{
  int n0, n1, n2;
  printf( "foo %n bar %n baz\n%n", &n0, &n1, &n2 ); //=> foo  bar  baz
  printf( "%d %d %d\n", n0, n1, n2 ); //=> 4 9 14
  return 0;
}

引数に int へのポインタを置いておくと、%n までに出力した文字の数が入るらしい。
恐ろしい。
余計なことすんな、という感じ。

IPA にも フォーマット文字列攻撃対策 という記事が上がっている。こわいこわい。

というわけで

というわけで、printf のマイナーな機能の紹介はこれでおしまい。

いずれも C11/C99 準拠だと思う。

あとそういえば。
関数へのポインタを (void*) に安全にキャストすることはできないことになっているけど、関数へのポインタ用の 変換指定子 ってないんだね。必要だと思うんだけどなぁ。

9
6
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
6