feature image

2020年5月19日 | ブログ記事

printfを飼いならす(上級編)【新歓ブログリレー72日目】

この記事は新歓ブログリレー72日目の記事です。

sea314です。

前回に引き続きprintfに関する記事を書きます。こちらの記事はかなり発展的な内容になるので、printfについて基礎的な知識がない人は前日の記事から読むことを推奨します。

printfファミリー

printf関数の他に、出力先、引数のとり方などが違うだけで使い方が同じ関数がいくつも存在します。中には一部環境では存在しない、関数名が異なる場合があります。

関数名 出力先 引数 その他違い 備考
sprintf 文字列 可変引数
snprintf 文字列 可変引数 出力バイト数制限できる
fprintf ファイル 可変引数
vprintf 標準出力 va_list
vsprintf 文字列 va_list
vsnprintf 文字列 va_list
vfprintf ファイル va_list
_scprintf 無し 可変引数 出力せずにバイト数を返す VisualStudio専用
_vscprintf 無し va_list 出力せずにバイト数を返す VisualStudio専用
asprintf 文字列 可変引数 asprintf内で文字列を確保する GCC拡張機能
vasprintf 文字列 va_list vasprintf内で文字列を確保する GCC拡張機能

見ていれば気づくと思いますが、sは文字列出力、fはファイル出力、nは出力文字数制限あり、vはva_listになっています。

va_listとは?

可変引数を持った関数を自作する際に使う型です。可変引数の関数は各引数に名前を付けることが出来ないので、va_listという型で一括管理します。

以下のmyprintf関数はprintfの出力にmessage:という文字を先頭に付け加える関数です。printfの戻り値は出力バイト数なので、printf("message:")とvprintf(fmt, args)の値を足したものを戻り値にしています。

#include <stdio.h>
#include <stdarg.h>

int myprintf(const char* fmt, ...){
    va_list args;
    va_start(args, fmt);
    int ret = printf("message:");
    ret += vprintf(fmt, args);
    va_end(args);
    return ret;
}

int main(){
    double a=3.141592;
    myprintf("%.3f", a);
    return 0;
}

出力

message:3.142

このように、printfのようなフォーマット機能をもつ関数を自作する際にはv付き関数群を使うことになります。

asprintf、vasprintfについて

この2つの関数は次のように宣言されています。

int asprintf(char **strp, const char *fmt, ...);
int vasprintf(char **strp, const char *fmt, va_list ap);

fmtを文字列に直し、必要なバイト数を確保して書き込んだあと、strpに返します。

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

int main(){
    char *str;
    asprintf(&str, "%.3f", 3.14);
    puts(str);
    free(str);
    return 0;
}

このように使います。

文字列に出力する際に配列サイズやバッファオーバーフローを気にしなくて良くなります。

_scprintf、_vscprintfについて

この2つの関数は次のように宣言されています。

int _scprintf(const char *fmt, ... );
int _vscprintf(const char *fmt, va_list ap);

fmtを文字列に直し、必要なバイト数を計算して返します。出力はしません。

この関数に限らず、printfファミリーの関数は戻り値が出力バイト数なので、出力しないこと以外変わった点はありません。しかし、printfの戻り値をわざわざ受け取ることは普通しないので使われ方は全く違います。

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

int main(){
    char *str = (char *)malloc(_scprintf("%.3", 3.14)+1);
    sprintf(str, "%.3f", 3.14);
    puts(str);
    free(str);
    return 0;
}

このように使います。

見た目asprintf、vasprintfよりも使い勝手が悪そうですが、必要バイト数を返してくれるおかげでsprintf後に更に文字操作する際にバッファを大きめに取れたりと融通が効きます。

asprintf、vasprintfを実装する

VisualStudioを使っている人はasprintf、vasprintfを使うことが出来ませんが、_vscprintfを使うことで実装できます。

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

int vasprintf(char** strp, const char* fmt, va_list ap) {
    char* buf = (char*)malloc(_vscprintf(fmt, ap) + 1);
    if (buf == NULL) {
        if (strp != NULL) {
            *strp = NULL;
        }
        return -1;
    }
    *strp = buf;
    return vsprintf(buf, fmt, ap);
}


int asprintf(char** strp, const char* fmt, ...) {
    va_list args;
    va_start(args, fmt);
    int ret = vasprintf(strp, fmt, args);
    va_end(args);
    return ret;
}

既存関数を書式対応化する

Visual Studio独自関数の_vscprintf、_vscwprintf (_vscprintfのワイド文字版)、_vswprintf (vsprintfのワイド文字版)を使うので、他のコンパイラではエラーになる可能性が高いです。

MessageBox

いろんなライブラリにMessageBox関数はあると思いますが、WinAPIのMessageBox関数を書式対応化してみます。

MessageBoxは次のように宣言されています。

#ifdef UNICODE
#define MessageBox  MessageBoxW
#else
#define MessageBox  MessageBoxA
#endif // !UNICODE
int MessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType);
int MessageBoxW(HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uType);

MessageBoxのような文字列を引数にとる関数は歴史的経緯でUNICODEマクロが定義されているかどうかで実際に呼び出される関数がA版かW版か切り替わるようになっています。これに合わせてMyMessageBoxAとMyMessageBoxWの2つの関数を作ります。

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

#ifdef UNICODE
#define MyMessageBox  MyMessageBoxW
#else
#define MyMessageBox  MyMessageBoxA
#endif // !UNICODE

int MyMessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType, ...) {
	va_list arg;
	va_start(arg, uType);
	int b = 0;
	if (lpText == NULL) {
		b = MessageBoxA(hWnd, NULL, lpCaption, uType);
	}
	else {
		LPSTR buf = (LPSTR)malloc(sizeof(char) * _vscprintf(lpText, arg) + 1);
		vsprintf(buf, lpText, arg);
		b = MessageBoxA(hWnd, buf, lpCaption, uType);
		free(buf);
	}
	va_end(arg);
	return b;
}


int MyMessageBoxW(HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uType, ...) {
	va_list arg;
	va_start(arg, uType);
	int b = 0;
	if (lpText == NULL) {
		b = MessageBoxW(hWnd, NULL, lpCaption, uType);
	}
	else {
		LPWSTR buf = (LPWSTR)malloc(sizeof(WCHAR) * _vscwprintf(lpText, arg) + 1);
		_vswprintf(buf, lpText, arg);
		b = MessageBoxW(hWnd, buf, lpCaption, uType);
		free(buf);
	}
	va_end(arg);
	return b;
}

MyMessageBoxA(NULL, "%012.3f\n%*.*f\n%12.3e\n", "", MB_OK, -12.3456, 12, 3, -12.3456, -12.3456);

表示

OutputDebugString

この関数はVisualStudioのデバッグウィンドウに文字を出力する関数です。

#ifdef UNICODE
#define OutputDebugString  OutputDebugStrinW
#else
#define OutputDebugString  OutputDebugStringA
#endif // !UNICODE
void OutputDebugStringA(LPCSTR lpOutputString);
void OutputDebugStringW(LPCWSTR lpOutputString);

この関数も書式対応させます。

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

#ifdef UNICODE
#define MyOutputDebugString  MyOutputDebugStringW
#else
#define MyOutputDebugString  MyOutputDebugStringA
#endif // !UNICODE

void MyOutputDebugStringA(LPCSTR lpOutputString, ...) {
	va_list arg;
	va_start(arg, lpOutputString);
	if (lpOutputString == NULL) {
		MyOutputDebugStringA(lpOutputString);
	}
	else {
		LPSTR buf = (LPSTR)malloc(sizeof(char) * _vscprintf(lpOutputString, arg) + 1);
		vsprintf(buf, lpOutputString, arg);
		OutputDebugStringA(buf);
		free(buf);
	}
	va_end(arg);
}


void MyOutputDebugStringW(LPCWSTR lpOutputString, ...) {
	va_list arg;
	va_start(arg, lpOutputString);
	if (lpOutputString == NULL) {
		MyOutputDebugStringW(lpOutputString);
	}
	else {
		LPWSTR buf = (LPWSTR)malloc(sizeof(WCHAR) * _vscwprintf(lpOutputString, arg) + 1);
		_vswprintf(buf, lpOutputString, arg);
		OutputDebugStringW(buf);
		free(buf);
	}
	va_end(arg);
}

MyOutputDebugString(TEXT("%+.*d\n"), 7, 12345);
MyOutputDebugString(TEXT("%7d\n"), -12345);
MyOutputDebugStringA("%07d\n", -12345);
MyOutputDebugStringW(L"%#x\n", 12345);

表示

その他

Windowsプログラミングでprintfを使う

通常、Windowsプログラミングではprintf関数は使えませんが、簡易デバッグ用にprintfが使いたいことがあると思います。

プログラム開始時に以下を呼び出すことでコンソール画面を作成し、printfをそのコンソールに出力させることが出来ます。

AllocConsole();
freopen("CONOUT$", "w", stdout);

コンソールを使い終わったら以下を呼び出してコンソールを破棄します。

FreeConsole();

このような感じになります。

最後に

今日の記事ではprintfを使った応用について書きましたが、一部の環境でしか動かないと思うので気をつけて下さい。

明日はmazreanさんの記事です。お楽しみに!

sea314 icon
この記事を書いた人
sea314

Monsterのエディタ&ゲーム本体のファイル入出力系を担当しています。 C/C++を主に使っています。 「しーさんいちよん」と読みます

この記事をシェア

このエントリーをはてなブックマークに追加
共有
記事一覧 タグ一覧 Google アナリティクスについて