C C++

[C | C++] cstdio, iostream, stdio.h 설명 및 차이점

Posted by DHniyeo Blog on September 16, 2023

🐿️ stdio.h - C

stdio.h는 C 프로그래밍 언어에서 사용되는 표준 라이브러리 헤더 파일입니다.

“stdio”는 “standard input/output”의 줄임말로, 이 헤더 파일은 표준 입력(stdin)과 표준 출력(stdout)을 다루는 데 필요한 함수와 매크로를 포함하고 있습니다.

이 헤더 파일은 사용자가 콘솔에서 데이터를 읽고 쓰는 등의 작업을 수행할 때 유용하게 활용됩니다.

주요 함수와 매크로 예시로는 다음과 같은 것들이 있습니다:

  • printf(): 포맷 지정된 출력을 수행하는 함수.
  • scanf(): 포맷 지정된 입력을 받는 함수.
  • getchar(): 문자 하나를 입력으로부터 가져오는 함수.
  • putchar(): 문자 하나를 출력으로 보내는 함수.
  • stdin: 표준 입력 스트림에 대한 파일 포인터 매크로.
  • stdout: 표준 출력 스트림에 대한 파일 포인터 매크로.

🐿️ cstdio - C++

  • cstdio는 “C STandarD Input and Output”의 약자로, C언어의 stdio.h와 같은 헤더 파일입니다. cstdio은 입/출력과 관련된 C 함수들을 포함하고 있습니다.

🐿️ stdio.h 와 cstdio 의 차이점?

stdio.h는 printf() 등의 표준 함수가 global namespace에 있다.

cstdio는 printf() 등의 표준 함수가std 이름 공간에 있다.

표준은 아니지만 대부분의 컴파일러는 global namespace에도 printf등의 표준 함수를 제공한다.

따라서 stdio.h를 사용할 경우 함수를 호출할 때 ::을 붙여 namespace를 명시해야 하지만, cstdio를 사용할 경우에는 std::를 사용하여 namespace를 명시합니다.

1
2
3
4
5
6
#include <stdio.h> // C / C++ 둘다 사용 가능

int main()
{
    printf("hello\n"); // <stdio.h>는 global namespace 이기 때문에 앞에 ::이 생략 되어있음. 
}
1
2
3
4
5
6
#include <cstdio> // C++ 에서만 사용 가능
int main()
{
	std::printf("hello\n"); // <cstdio>는 std namespace 안에 있기 때문에 std::을 이용하여 표준 함수를 사용한다. 
	// 참고로 대부분의 컴파일러는 global namespace에도 printf 등의 표준함수를 제공 하기 때문에 std::을 빼도 무관하지만 표준은 아니다. 
}

🐿️ iostream과의 차이점은 무엇인가요?

iostream은 입/출력을 수행하기 위한 모든 C++ 스트림을 포함한 헤더 파일입니다. iostream에는 cout, cin, cerr 등의 스트림 객체와 관련된 함수들이 포함되어 있습니다. 한편, cstdio는 C 언어의 입/출력 함수들을 포함하고 있습니다. 예를 들어 printf()cout보다 빠르기 때문에 특수한 상황에서 주로 사용되지만, 보통 C++에서는 cout을 사용합니다. cstdio와 iostream은 입/출력을 수행하는 기능이 동일하지만, 두 가지를 모두 사용할 수 있다면 보다 안전한 iostream을 사용하는 것이 좋습니다. 이유는 다음과 같습니다.

  • type-safe: iostream은 컴파일러가 I/O하는 객체의 타입을 정적으로 알 수 있기 때문에 type-safe한 입출력이 가능합니다. 반면, cstdio은 %를 사용하여 타입을 동적으로 알아내기 때문에 타입에 대한 검사가 필요합니다.
  • 에러: cstdio은 %를 사용하기 때문에 형식 지정자가 실제로 I/O할 객체와 일치해야 합니다. iostream은 % 토큰을 사용하지 않으므로 에러를 줄일 수 있습니다.
  • 확장성: iostream은 기존의 코드를 바꾸지 않고 사용자가 정의한 타입을 I/O할 수 있는 확장성을 제공합니다.
  • 상속성: iostream 메커니즘은 std::ostream이나 std:istream과 같은 실제 클래스로부터 만들어져서 상속이 가능합니다. 따라서 사용자가 직접 스트림을 정의하고 작동시킬 수 있습니다. 반면, cstdio의 FILE*은 상속이 불가능합니다. 또한, cstdio의 printf() 함수는 가변 인자 함수로서 인자의 타입을 검사하지 않고 다른 타입으로 overload될 수도 없습니다.

추가로, C의 모든 키워드를 C++에서도 사용할 수 있지만, 역은 항상 성립하지는 않습니다.

🐿️ 스트림(stream) 이란?

[참고] https://velog.io/@jinh2352/스트림stream

컴퓨터 기술에서 스트림은 연속적인 데이터의 흐름 혹은 데이터를 전송하는 소프트웨어 모듈을 일컫는다.

![0](/assets/img/2023-09-16-[C- -C++]-cstdio,-iostream,-stdio.h-설명-및-차이점.md/0.png)

C++ 표준에서는 오직 스트림을 통한 입출력만 지원한다.

이때 C++ 입출력 스트림의 중요한 특징은 스트림이 버퍼를 가진다는 점이다.

🐇 키 입력 스트림 버퍼

키보드에 연결된 cin 입력 스트림이 존재한다. 사용자가 입력한 ‘H’, ‘e’, ‘l’, ‘l’, ‘u’ 키를 순서대로 저장하고 프로그램에게는 전달하지 않는다. 아직 사용자의 키 입력이 끝났다고 볼 수 없기 때문이다.

![1](/assets/img/2023-09-16-[C- -C++]-cstdio,-iostream,-stdio.h-설명-및-차이점.md/1.png)

사용자가 <Backspace> 키를 입력하면 가장 최근에 입력된 문자인 ‘u’를 버퍼에서 지울 수 있다. <Backspace> 키는 버퍼에 저장되는 대신 버퍼를 제어하는 제어키의 역할을 한다. 그리고 <Enter> 키를 입력하면 비로소 ‘LOVE’ 문자들은 입력을 기다리고 있는 C++ 프로그램에게 전달된다.

🐇 스크린 출력 스트림 버퍼

스크린에 연결된 cout 출력 스트림이 존재한다. 출력 스트림은 보통 ‘\n’이 도착하거나 버퍼가 꽉 찰 때 스크린에 출력시킨다.

![2](/assets/img/2023-09-16-[C- -C++]-cstdio,-iostream,-stdio.h-설명-및-차이점.md/2.png)

하지만 cout.flush() 명령을 내리면 출력 스트림은 버퍼에 있는 내용을 모두 장치에 출력한다.

🐇 버퍼의 필요성

C++ 입출력 스트림은 운영체제 API(시스템 콜)를 호출하여 입출력 장치와 프로그램 사이에서 데이터를 전송한다.

파일 출력 스트림에서 만일 버퍼가 없다면, 프로그램이 몇 바이트씩 파일 쓰기를 실행할 때마다 시스템 콜을 호출하고, 그때마다 하드 디스크나 네트워크 장치가 자주 작동하게 되어 시스템의 효율이 나빠진다(비록 운영체제 또는 HW적으로 버퍼 기능이 있을 지라도 분명 오버헤드는 있다. 예를 들어 커널과의 문맥 교환이 있겠다).

(오버헤드(overhead)는 어떤 처리를 하기 위해 들어가는 간접적인 처리 시간 · 메모리 등을 말한다.)

버퍼가 있다면, 쓰기가 이루어진 데이터를 스트림 버퍼에 모아두었다가, 한 번에 운영체제에 요청하면 시스템 효율이 올라가게 된다.

또한 키 입력 스트림의 경우 입력된 키를 일단 버퍼에 저장하고, 프로그램에 전달하기 전 <Backspace> 와 같은 제어키를 통해 입력된 키들을 수정할 수 있다는 장점도 있다.

🐿️ C++ 입출력 라이브러리

과거 입출력 시스템은 영어와 같이 문자 하나를 한 바이트로 표현하는 언어의 문장만 입출력하도록 작성되었다. 문자 하나가 2바이트로 구성되는 한글 문자를 입력할 수 없었다.

![3](/assets/img/2023-09-16-[C- -C++]-cstdio,-iostream,-stdio.h-설명-및-차이점.md/3.png)
![4](/assets/img/2023-09-16-[C- -C++]-cstdio,-iostream,-stdio.h-설명-및-차이점.md/4.png)

즉 과거의 입출력 클래스들은 아래와 같으며 모두 문자를 한 바이트로만 다루는 클래스들이다.

=> ios, istream, ostream, ifstream, ofstream, fstream

현재의 표준 C++ 입출력 라이브러리는 한 문자를 여러 바이트로 표현하는 다국어의 입출력을 위해 템플릿(template)을 사용하여 C++ 입출력 라이브러리를 일반화시켰다.

=> basic_ios, basic_istream, basic_ostream, basic_ifstream, basic_ofstream, basic_fstream

새 표준을 사용하는 C++ 개발자들은 이들 템플릿 클래스에 구체적인 타입을 대입하여 입출력 클래스를 구체화하여 사용해야 한다.

표준을 변했다 하더라도 지금도 여전히 cin으로는 한글을 문자 단위로 읽을 수 없다.

![5](/assets/img/2023-09-16-[C- -C++]-cstdio,-iostream,-stdio.h-설명-및-차이점.md/5.png)

source: “명품 C++ 프로그래밍” / 황기태 / 생능출판

  • ios: 모든 입출력 스트림 클래스의 기본 클래스이다. 스트림 입출력에 필요한 공통 함수와 상수, 멤버 변수가 선언되었다.
  • istream: 문자 단위 입력 스트림
  • ostream: 문자 단위 출력 스트림
  • iostream: 문자 단위로 입출력을 동시에 할 수 있는 스트림 클래스
  • ifstream: 파일로부터 입력 스트림
  • ofstream: 파일로 출력 스트림
  • fstream: 파일로부터/로 입출력 스트림

🐿️ C++ 표준 입출력 스트림

<iostream> 헤더 파일을 인클루드한 C++ 프로그램이 실행되기 시작하면, cin, cout, cerr 등 표준 입출력 객체가 생성되며, 바로 이들을 사용할 수 있다.

  • cin

    키보드 장치와 연결된 istream 타입의 표준 입력 스트림 객체

  • cout

    스크린 장치와 연결된 ostream 타입의 표준 출력 스트림 객체

  • cerr과 clog

    cerr와 clog 객체는 둘다 표준 오류 출력 스트림 객체, clog는 버퍼를 거치지만 cerr는 버퍼를 거치지 않고 스크린에 오류 메시지 출력

🐇 istream의 ‘»’ 연산자 오버로딩

ifstream 클래스에서 비트 시프트 연산을 하는 >> 연산자를 오버로딩하였다. >> 연산자는 스트림 추출 연산자로 불리우며 왼쪽 피연산자인 스트림 객체로부터 데이터를 읽어 오른쪽 피연산자에 지정된 변수에 삽입한다.

사용자 입력이 일차적으로 cin의 스트림 버퍼에 저장되며, <Enter> 키가 입력되면 비로소 cin 입력 버퍼에서 키 값을 끌어내어 변수에 저장한다.

🐇 ofstream의 ‘«’ 연산자 오버로딩

<< 연산자는 출력 스트림에 데이터를 출력하는 연산자이고, 스트림 삽입 연산자라고 부른다. ostream 클래스에서 비트 단위로 시프트하는 << 연산자를 오버로딩하여 작성하였다. 정수, 문자, 문자열 등 기본 타입 또는 문자열에 대해서만 오버로딩되어 있기에 사용자 정의 클래스의 객체를 이 삽입 연산자의 피연산자로 두려면 해당 클래스에서 연산자 오버로딩을 하여야 한다.

1
cout << 'a' << 123 << endl;

위 문장은 객체, 입출력 스트림, 연산자 중복, 참조 매개변수, 참조 리턴이 절묘하게 연결되어 있는 문장이다.

🐇 리턴 타입의 의미

ostream 클래스 내에 중복 작성된 모든 << 연산자들은 다음과 같이 ostream&을 리턴한다.

1
ostream& operator << (char c);

« 연산자는 출력 스트림에 데이터를 삽입한 후, 출력 스트림(*this)를 리턴한다. 즉 스트림의 참조가 리턴된다.

함수 내에서 선언된 지역변수가 아닌 객체 자기 자신을 반환하는 것이다. 고로 참조로 반환이 가능하다.

🐇 구체적인 실행 과정

1
cout << 'a' << 123;

먼저 cout << 'a'에서 << 연산자를 호출한다. 컴파일러는 아래와 같이 변형하여 컴파일한다.

1
cout.<<('a')

이 코드는 cout 객체 내의 연산자 함수(operator<<(char c))하고 ‘a’를 매개변수에 넘겨준다.

1
2
3
4
5
ostream& operator<<(char c) {
    // .. 현재 스트림 버퍼에 c('a')를 삽입한다.
    // .. 버퍼가 차면 장치에 출력한다.
    return (*this);
}

실행 결과 cout의 버퍼에 ‘a’가 삽입되고, cout에 대한 참조가 리턴되었다. 그러므로 << 123은 `cout « 123’을 실행하는 것과 같고, ‘a’가 들어 있는 cout의 버퍼에 123을 출력하는 것이다.

1
2
3
4
5
ostream& operator<<(char c) {
    // .. 현재 스트림 버퍼에 n(123)를 삽입한다.
    // .. 버퍼가 차면 장치에 출력한다.
    return (*this);
}

결국 cout의 버퍼에는 “a123”으로 변경되며, 적절한 시점에 화면에 출력 된다.

만약 » 연산자 재정의 함수의 리턴 타임이 참조가 아닌 그냥 ostream이라면 어떻게 될까?

1
스트림의 복사본(*this)이 리턴되고, 그 다음에 실행되는 `<<` 연산은 복사된 스트림에 출력하게 되어, 연속되는 두 `<<` 연산이 서로 다른 출력 스트림에 출력하게 되므로 예상대로의 출력이 이루어지지 않을 수 있다.