본문 바로가기

프로그래밍/C, C++

printf, fprintf, sprintf는 어떤 차이일까?


C/C++ 에서는 여러가지 출력 방법이 있다. C언어의 printf부터 C++의 cin까지 다양한 함수, 객체와 메소드가 있지만 그 중 서식화된 출력에 유용하게 사용할 수 있었던 여러가지 함수를 소개해 보려 한다.


1. printf()

int printf(const char *format-string, argument-list);

일반적으로 printf()를 사용할 때는 매개변수로 쌍따옴표(")로 싸인 문자열과 문자열에서 %로 지정된 서식 매개변수를 같이 전달하여 출력한다. 또한 줄바꿈, 커서 이동 등을 위해 이스케이프 시퀀스가 사용된다.


이는 간단한 서식을 고려한 출력에 사용되며 가장 많이 접하고 학습 초기에 익숙해져야 할 핵심적인 함수라고 할 수 있다.


#include <stdio.h>

int main()
{
    printf("Hello World!")
}


C언어 관련 도서를 한번만이라도 펼쳐보았다면 위와 같은 Hello World예제가 기억날 것이다.

이렇게만 보면 간단한 기능을 가진 함수로 보이지만, 아래의 출처 링크만 들어가 보아도 엄청난 양의 매뉴얼을 볼 수 있다.

즉 굉장히 복잡한 함수라는 것이다.


간략히 정리하고 들어가자면 이 printf()가 하는 것은 서식화된 문자열을 표준 출력(Standard Output, stdout)에 보내는 것이다.

이런 stdout, stdin, stderr등은 표준 스트림이라고 불리는데 이는 유닉스 운영체제의 유산으로 컴퓨터와 사용자 사이에서 물리적으로 진행되는 입출력을 추상적으로 표현한 것이라 생각하면 된다. 사용자가 사용을 위해 따로 작업을 할 필요는 없으며 <stdio.h> 헤더 파일에 포함되어 있다. 자세한 내용은 이곳을 참고하자. 


아무튼 이런 표준 출력에 문자열을 보낸다는 것은 컴퓨터가 데이터를 출력장치를 통해 우리에게 보여주는 일련의 과정이라고 생각하면 된다. 여기서는 콘솔 프로그램이므로 윈도우의 명령 프롬프트, 리눅스의 터미널 등에 나타나는 것이다.



하지만 중요한 것은 printf() 의 서식 기능을 어떻게 사용하는지, 그를 어떻게 응용할 수 있는지가 될 것이다.

함수의 원형에서 볼 수 있듯이 printf()는 const char* 형 문자열(format-string)과 매개변수 목록(argument-list)를 받는데 전자의 경우 const char* 형 문자열은 코딩 단계에서부터 큰따옴표(")로 묶인 문자열 상수를 의미한다.


우리가 printf()안에 "Hello World" 등을 써 넣는 것도 문자열 상수를 선언, 매개변수로 전달한 것이다.

그렇다면 뒤의 매개변수 목록에는 당연히 이 문자열 상수에서 지정된 형식의 매개변수들이 순서에 맞게 들어갈 것이다.

그렇다면 이 '형식'이란 것은 무엇일까?


대부분의 C언어 교재에는 이 printf()에 사용되는 형식 지정자들의 목록이 나열되어 있다.


specifierOutputExample
d or iSigned decimal integer392
uUnsigned decimal integer7235
oUnsigned octal610
xUnsigned hexadecimal integer7fa
XUnsigned hexadecimal integer (uppercase)7FA
fDecimal floating point, lowercase392.65
FDecimal floating point, uppercase392.65
eScientific notation (mantissa/exponent), lowercase3.9265e+2
EScientific notation (mantissa/exponent), uppercase3.9265E+2
gUse the shortest representation: %e or %f392.65
GUse the shortest representation: %E or %F392.65
aHexadecimal floating point, lowercase-0xc.90fep-2
AHexadecimal floating point, uppercase-0XC.90FEP-2
cCharactera
sString of characterssample
pPointer addressb8000000
nNothing printed.
The corresponding argument must be a pointer to a signed int.
The number of characters written so far is stored in the pointed location.
%% followed by another % character will write a single % to the stream.%


퍼센트 기호(%)와 그 뒤에 붙는 문자로 구성되는 형식 지정자는 여러 종류가 있지만 자주 쓰이는 것들을 몇 개 추려보자면

  • %d : 정수(int) 출력
  • %f : 소수(float) 출력
  • %lf : 소수(double) 출력
  • %c : 문자(char) 출력
  • %s : 문자열(char[], char*) 출력
  • %p : 포인터 주소(1a2b3c4d같은 16진수) 출력

등으로 간추릴 수 있다. 이들은 형식 지정자와 이에 상응하는 매개변수의 자료형이 일치하지 않으면 대부분 원하는 결과를 출력하지 않는데 의도적이든 비의도적이든 간에 자료형은 일치시키는 것이 좋다. 자료형에 따라 다른 값으로 출력되거나 데이터가 소실되는 경우가 있기 때문인데 대표적인 예로 문자 'a'를 정수로 출력시킨다면 ASCII 값이 나오거나 소수(36.5)를 정수로 출력시킨다면 36으로 소수부가 잘리는 등의 문제가 있다.[각주:1]


또한 이런 형식 지정자들은 단순히 퍼센트 기호와 문자로만 이루어진 것이 아니다.

이는 몇가지 서식으로 이루어져 있는데 그는 다음과 같다.


flags 

width 

precision 

prefix 

 type


  • flags : 부호, 공백, 소수점, 8진 및 16진 접두부(0, 0x)의 출력과 정렬 서식 등을 결정한다. '-'는 왼쪽 정렬, '+'는 부호 출력 등의 기능을 가지고 있으며 '0'의 경우 빈 자릿수를 빈칸이 아닌 0으로 출력하는 등의 특징을 가지고 있다. 이곳 참고
  • width : 출력될 문자의 최소 갯수를 지정한다. 이곳 참고
  • precision : 문자열, 유효 자릿수 또는 부동 소수점 값의 소수점 뒷자리수가 출력되는 최대 자릿수 또는 정수 값이 출력되는 최소 자릿수를 지정하는 선택적 10진수 숫자다. 
  • prefix : 인수의 크기를 지정한다. 부동소수를 나타내는 float이 '%f'인것에 비해 double은 '%lf'인 것을 예로 들 수 있다.
  • type : 위에서 언급한 자료형에 따른 문자가 위치한다. 정수면 'd', 문자면 'c' 같은 식이다.
각 부분의 이름과 기능은 위와 같으며 자세한 내용은 아래의 출처나 레퍼런스를 참고하자.
이 중에서 자주 사용하는 몇 가지를 추려보자면 flags의 '-', '0' 이 서식 출력에 유용할 것이다.

#include 

int main()
{
    int num1 = 100;
    int num2 = 200;

    printf("%05d \n", num1);
    printf("%5d \n", num2);
    printf("%-05d \n", num2);

    return 0;
}

위의 코드를 실행하면 아래와 같은 결과가 나온다.


먼저 num1의 경우 "%05d" 형식 출력을 통해 5글자 공간에서 빈 공간은 0으로 채우도록 출력시켰다.(flags: 0, width: 5)
그 다음 num2는 "5d"를 통해 오른쪽 정렬을, 그 다음에는 "-5d"를 통해 일반 출력과 왼쪽 정렬 출력을 구분하였다.

이는 초보적인 단계에서 자주 쓰일 예시를 들은 것 뿐이고 여러 플래그의 조합을 통해 데이터를 다양하게 출력할 수 있다.
추가적인 정보는 레퍼런스와 MSDN을 참고하도록 하자. 특히 아래 출처의 첫번째 링크인 IBM Knowledge Center에는 예제가 제공되므로 관심이 있다면 들어가 보는 것을 추천한다.


출처:

https://www.ibm.com/support/knowledgecenter/ko/ssw_ibm_i_73/rtref/printf.htm

https://ko.wikipedia.org/wiki/%ED%91%9C%EC%A4%80_%EC%8A%A4%ED%8A%B8%EB%A6%BC

http://www.cplusplus.com/reference/cstdio/printf/#compatibility

2. fprintf()

int fprintf(FILE *stream, const char *format-string, argument-list);


fprintf()는 printf()와 유사하지만 앞에 f라는 글자가 붙어 있는데 이는 file을 뜻한다고 봐도 무방하다.

즉 파일 스트림에 서식화된 문자열을 출력하는 함수다.


그런데 첫 번째 매개변수를 보면 FILE *stream 이라는 이름을 가지고 있다. 이는 형식 문자열이 출력될 파일 포인터를 의미하지만 추가로 위에서 stdout과 관련해서 언급한 표준 스트림을 전달할 수도 있다. 즉 fprintf(stdout, "Hello World!")는 printf("Hello World")와 동일한 동작을 수행한다.[각주:2]


대부분의 경우 파일에 서식화된 문자열 출력을 위해 사용된다. 문자열 형식와 그에 맞는 매개변수를 전달하는 것은 기존의 printf()의 사용 방식과 동일하기 때문에 사용에 큰 어려움은 없다. 이는 비정상적으로 동작하였을 경우 정상적일 때 반환하는 출력된 바이트의 개수가 아닌 음수를 반환하므로 이를 조건문으로 검사하여 파일에 제대로 출력이 되었는지 검증하는 것도 좋은 방법일 것이다.


하지만 일반적인 출력으로 stdout에 출력하는 게 아닌 이상, 파일 포인터가 제대로 열렸는지 확인하는 것이 중요할 것이다.

함수 원형에 맞게 제대로 사용하였을 경우에도 파일에 제대로 출력이 되지 않는다면 파일 포인터가 적절한 모드로 열리고 닫혔는지, 문자열 서식의 플래그는 목적에 맞게 사용되었는지를 먼저 확인하도록 하자.


#include 

int main()
{
    int num1;
    char name[20];
    FILE *fp;
    
    fp = fopen("test.txt", "w");
    
    printf("TYPE NAME : ");
    scanf("%s", name);
    
    printf("TYPE AGE : ");
    scanf("%d", &num1);
    
    fprintf(fp, "Hello World. My name is %s and I\'m %d years old.", name, num1);
    
    fclose(fp);
    return 0;
}


위의 예제를 실행하면 아래와 같은 데이터 입력 과정을 거치게 된다.



이로 인해 출력되는 파일의 내용은 아래와 같다.




    fprintf(fp, "Hello World. My name is %s and I\'m %d years old.", name, num1);

이 구문에서 텍스트 파일에 문자열을 입력하였다는 것을 명심하자.



출처:

https://modoocode.com/64

https://www.ibm.com/support/knowledgecenter/ko/ssw_ibm_i_73/rtref/fprintf.htm

https://kr.mathworks.com/help/matlab/ref/fprintf.html




3. sprintf()


int sprintf(char *buffer, const char *format-string, argument-list);


이 역시 printf()와 동일한 문자열 서식 및 매개변수를 사용한다. 반환값도 출력한 바이트의 개수를 반환하는 등 비슷하지만 이번에는 첫번째 매개변수가 FILE* 형 변수였던 fprintf()와는 달리 char* 형 변수라는 것이다. 이는 매개변수의 이름을 보면 감이 오겠지만 배열로 이루어진 버퍼에 일련의 서식 문자열을 입력하는 함수다. 즉 파일이나 화면에 출력하는 것이 아니라 변수(버퍼)에 문자열을 출력한다.


중요한 것은 여기서 버퍼는 배열로 이루어져 있다는 것이다.

익숙한 사람들은 잘 알겠지만 String이라는 문자열 자료형이 따로 있는 C++과는 달리 C언어에서는 char 자료형을 통하여 모든 문자열의 처리가 이루어진다. char* 형을 이용하여 문자열 상수를 선언하거나 char[] 형을 이용하여 수정 가능한 문자열을 다루며 대부분 <string.h> 헤더를 통해 문자열 처리를 수행한다. 이때 문자열 상수는 한번 선언되면 새로 할당하는 것 말고는 내용을 변경할 수 없으므로 배열로 선언된 char 자료형 스타일의 문자열을 주로 사용하는데 이 문자열에 서식화된 문자열을 할당할 수 있는 함수라 할 수 있다.


<string.h>에서도 strcpy등을 통해 배열에 문자열을 할당할 수 있지만 서식화되어 있지 않다는 특징이 있었는데 이 sprintf()를 사용하면 printf()를 사용하여 화면에 출력하듯이 배열에 서식화된 문자열을 할당할 수 있다.


#include 

int main()
{
    int num1;
    char name[20];
    char string[200];
    
    printf("TYPE NAME : ");
    scanf("%s", name);
    
    printf("TYPE AGE : ");
    scanf("%d", &num1);
    
    sprintf(string, "Hello World. My name is %s and I\'m %d years old.", name, num1);
    printf("%s", string);

    return 0;
}


위와 같은 코드를 실행하면 아래처럼 동작한다.



아래 코드를 통해 string 문자열 배열에 서식화된 문자열을 할당하였기에 위처럼 동작할 수 있다는 것을 기억하자.

    sprintf(string, "Hello World. My name is %s and I\'m %d years old.", name, num1);


출처:

https://www.ibm.com/support/knowledgecenter/ko/ssw_ibm_i_73/rtref/sprintf.htm


  1. 자바(Java)에서는 이를 아예 허용하지 않음으로써 강건성을 향상시키는 방향을 채택하고 있다. [본문으로]
  2. stdout 뿐 아니라 stderr에 출력하여 디버깅을 위한 에러 메시지를 출력하는 기법도 있다. 이는 기존의 stdout은 입력 버퍼를 통하여 출력되기에 프로그램 오류 시 출력되지 않을 수 있는 것과 달리 stderr은 바로 출력되기에 확인이 용이하다는 점이 있다. [본문으로]