본문 바로가기
언어/C언어

[C 언어] 11. Input/Output Stream, String

by 천무지 2024. 4. 14.
반응형

Input/Output Stream

스트림이란 데이터를 송수신하기 위한 통로의 개념입니다.

가까운 예시로는 OTT스트리밍이나, 음악 스트리밍이 있습니다.

스트림은 데이터가 연속으로, 순서대로, 시리즈로 전송, 저장되는 것을 말합니다.

스트림에는 2가지가 존재하는데 하나는 binary stream으로 영상물이 여기에 해당하고, 다른 하나는 text stream으로 소스코드가 여기에 해당합니다.

 

C언어에서 스트림의 데이터 타입은 FILE*로 file pointer로 지칭합니다.

스트림을 읽고 쓸 정보가 필요합니다. 

이러한 정보를 FILE은 스트림 연결에 관한 내부의 상태정보를 저장합니다.

 

정보는 3가지를 저장합니다.

첫 번째로 file position indicator입니다. 말 그대로 파일의 위치를 지정하는 포인터입니다.

 

두 번째로 버퍼입니다. 버퍼는 데이터를 저장하기 위한 임시 저장 공간입니다. 

데이터를 바로바로 저장하는 것이 아닌 임시 공간에 저장하고 그다음에 저장합니다.

송신을 하든 수신을 하든 버퍼는 무조건 거치게 되어있습니다.

비동기적으로 어느 정도 버퍼에 모이면 한 번에 보냅니다.

일정한 양이 차면 애플리케이션으로 데이터를 송신하거나 시스템으로 데이터를 수신합니다.

 

세 번째로 상태 지시자입니다.

과정 중 에러가 발생하면 에러 정보를 담고 있다가 바로 알려줍니다. 이 에러는 버퍼를 거치지 않습니다.

버퍼를 거친다면 한 번에 모인 상태에서 전송되기 때문에 에러가 발생한 위치를 찾을 수 없기 때문입니다.

 

C에서 main() 함수를 실행하면 지정된 세 개의 스트림이 open 됩니다.

FILE* stdin : standard input stream  //keyborad 

FILE* stdout : standard output stream  //display 

FILE* stderr : standard error stream  //display

문자 입출력

문자 출력을 알아보겠습니다.

int putchar(int c); //stdout
int fputc(int c, FILE* stream);
int putc(int c, FILE* stream);

표현이 다를 뿐 사실 다 같습니다. 

여기서 반환형이 모두 정수형인데 큰 의미를 담고 있다기보단 에러 검출용입니다.

첫 번째 줄의 코드는 스트림을 인자로 받지 않고 두 번째, 세 번째 줄의 코드는 스트림을 인자로 받고 있습니다.

첫 번째 줄의 코드는 기본으로 스트림을 stdout으로 받습니다. 따라서 display를 통해 출력됩니다.

두 번째, 세 번째 줄의 코드는 스트림을 display 외에도 파일과 같이 스트림을 지정해 줄 수 있습니다. 

 

문자를 입력을 알아보겠습니다.

int getchar(void); //stdin
int fgetc(FILE* stream); //returns the character read as an unsigned char cast to an int or EOF
int getc(FILE* stream);

먼저 반환형을 정수형으로 했는데 읽을 데이터는 char입니다. 데이터 타입이 서로 다릅니다.

여기서 의문을 품을 수 있습니다.

받은 데이터뿐만 아니라 에러 정보도 표시해야 하기 때문입니다. 그래서 1바이트 문자형이 아닌 4바이트 정수형을 사용합니다. 

에러 표시는 EOF로 -1이기 때문입니다. -1은 11111111 11111111 11111111 11111111이기 때문에 정수형을 사용해야 합니다.

 

문자의 입출력 예시를 보겠습니다.

#include <stdio.h>
void main(void)
{
    char c = 0;
    int count = 0;
    while (c != 'q') {
        c = getchar();
        putchar(c);
        count += 1;
        printf("%d", count);
    }
}

저는 abc를 입력하고 엔터를 누른 뒤 q를 입력하고 엔터를 눌렀습니다.

그리고 입력한 문자를 입력하고 count라는 변수로 몇 번째 출력인지 출력한 문자 뒤에 숫자를 붙였습니다.

여기서 이상한 점은 제가 3개의 문자를 입력했는데 a1b2c3까지 잘 나오다가 줄이 바뀐 뒤에 4가 나온 것입니다.

문자는 3개인데 줄이 바뀌고 4까지 나왔다는 것은 줄 바꿈 문자 \n 도 문자로 취급되었기 때문입니다.

줄바꿈 문자 \n은 두 가지 의미를 가집니다,

첫 번째로 라인을 바꾼다.

두 번째로 버퍼에 전달해라.

현재 두 가지 의미가 동시에 작동되어서 이러한 문제가 발생한 것입니다.

enter를 누르기 전까지 버퍼에 존재하다가 enter를 누르면 \n도 버퍼에 들어간 다음 애플리케이션으로 넘어갑니다. 터미널에서는 \n이 나왔을 때 버퍼 안에 데이터가 어플리케이션으로 넘 거 갑니다.

만약 이런 출력을 원하지 않는다면 따로 코드를 직접 작성해서 \n을 제외시켜주어야 합니다.

 

다음은 EOF(End of File)에 대해서 알아보겠습니다.

파일의 끝에 도달하거나 오류가 발생했을 때 표현하기 위해 사용하는 신호입니다.

<stdio.h> 헤더파일에 정의되어있고, int형으로 -1 dlqslek.

콘솔에서 Ctrl+z를 누르면 EOF를 반환합니다.

 

예시를 보겠습니다.

#include <stdio.h>

void main(void)
{
    char c = 0;
    while (c != EOF) { //-1
        c = getchar();
        putchar(c);
    }
    printf ("bye\n");
}

처음 입력할 때에는 제가 a를 입력했더니 종료되지 않고 a가 출력되었습니다.

두 번째 입력에서 Ctrl+z를 입력했더니 프로그램이 종료되었습니다.

한 가지 알아두어야 할 젊음 abc를 입력하고 Ctrl+z를 입력하고 엔터를 누르면 프로그램은 종료되지 않습니다.

위 코드에서 반복문을 빠져나오려면 Ctrl+z를 단독으로 입력해 주어야 합니다.

이유는 Ctrl+z의 아스키코드가 버퍼 중간에 있을 때와 단독으로 있을 때 다르기 때문입니다.

버퍼 중간에 존재하면 아스키코드는 26 혼자 있다면 -1입니다.

EOF는 -1로 Ctrl+z가 혼자 있어야 두 값이 같아집니다.


문자열 입출력

문자열 출력부터 알아보겠습니다.

int puts(const char *s); //stdout
int fputs(const char *s, FILE* stream);

사용 방법은 문자 출력과 같습니다. 다만 여기서는 문자가 아닌 문자열이 들어갔다는 점만 다릅니다.

다만 여기서 두 출력은 서로 다른 식으로 데이터를 전송합니다.

첫 번째 줄의 코드는 문자열 s에 \n을 붙여서 stdio에 전달합니다.

하지만 두 번째 줄의 코드는 문자열 s에 \n을 붙이지 않고 문자열 s만 stream에 출력합니다.

에러가 발생한 경우 EOF(-1)을 반환하고 나머지 경우에 대해서는 양수 값을 반환합니다.

 

문자열 입력을 보겠습니다.

char* s gets_s(char* s, int n); //stdin
char* s fgets(char* s, int n, FILE *stream);

gets() 함수가 아닌 gets_s()로 쓰인 이유는 gets() 함수는 배열의 크기를 지정하지 않아 보안상 심각한 문제를 발생시킬 수 있어서 사용이 불가능합니다.

int n은 읽을 문자의 개수입니다.

첫 번째 줄의 코드는 \n까지 또는 EOF까지 stdio에서 읽어옵니다.

자동으로 NULL 문자를 추가하기 때문에 최대 n-1 문자까지 읽을 수 있습니다.

입력이 만약 n-1을 초과한다면 buffer flush를 해버리고 runtime error를 리턴합니다.

buffer flush는 입력되어 있는 버퍼를 싹 비워버리는 것입니다.

\n문자는 입력 문자열 s에 포함되지 않고 버립니다.

두 번째 줄의 코드는 \n 또는 EOF까지 stream에서 읽어옵니다.

입력에 \n이 있다면 \n을 포함하고 NULL을 추가하여 입력 문자열 s에 저장합니다.

입력이 n-1을 초과하면 n-1까지는 입력 문자열 s에 저장하고, 스트림의 파일 포인터를 n으로 옮깁니다.

파일 포인터를 n으로 옮기면 파일 포인터 안에 있는 position indicator를 n 변재 인덱스에 가져다 놓는 것입니다.

 

예시를 보겠습니다.

#include <stdio.h>
void main(){
    char s[10]; //몇 개의 문자가 가능?
    fputs("Input string: ", stdout);
    fgets(s, sizeof(s), stdin);
    //gets_s(s, sizeof(s));
    fputs("Your String: ", stdout);
    fputs(s, stdout);
}

첫 번째 출력과 두 번째 출력은 그대로 출력된 걸 볼 수 있습니다.

세 번째 출력에서는 입력을 fgets로 받았기 때문에 n-1까지 읽어서 문자열 s에 저장하였고 남은 문자들을 버퍼에 남아있을 것입니다.

n-1까지의 문자열이 fputs에 의해 잘 출력된 것을 볼 수 있습니다.


스트림 버퍼링

스트림 입출력은 문자 단위로 즉시 전달되지 않고 여러 문자를 블록으로 모아서 비동기적으로 전달합니다.

스트림 버퍼링을 통해 시스템 오버헤드를 줄일 수 있습니다.

 

버퍼링 방식은 3가지가 있습니다.

  •  언버퍼링(unbuffered): 문자 단위로 즉시 전송합니다. 예시로 stderr은 오류 정보를 display에 나타내는데, 오류를 버퍼링 하면 엉뚱한 곳에서 출력될 수 있기 때문에 오류는 버퍼링 하지 않고 발생한 즉시 출력합니다.
  • 라인버퍼링(line buffered): new line 문자 \n이 나오면 블록을 전송합니다. 지금까지 계속 살펴봤던 것입니다.
  • 풀버퍼링(fully buffered): 임의의 사이즈를 가지는 블록을 전송할 수 있습니다. 사용자가 크기를 지정하면 그 크기만큼 버퍼링 하는 것입니다.

스트림 버퍼링 방식은 파일이나 장치에 따라 다릅니다.

일반적으로 파일은 풀 버퍼링을 지원합니다.

터미널 같은 상호작용이 되는 장치에 연결된 스트림은 라인 버퍼링 방식으로 동작합니다.

만약 상호작용이 되는 장치에서 즉시 출력이 필요하다면 fflush() 함수를 호출하여 강제로 버퍼 내용을 파일로 전송하고 버퍼를 비웁니다. (buffer flush)

 

버퍼를 비우는 방법들에 대해서 알아보겠습니다.

int fflush(FILE* stream);

출력 스트림에서는 버퍼의 내용을 모두 출력 스트림으로 전송합니다. 한마디로 애플리케이션의 내용을 모두 시스템으로 전송하는 것입니다.

입력스트림에서는 입력 스트림에 대한 fflush는 표준으로 정의되어있지 않습니다. 따라서 사용하지 않습니다.

 

stdin의 버퍼를 비우는 방법은 2가지가 있습니다.

  • while (getchar()!= '\n');
  • rewind(stdin);  //void rewind(FILE* stream); //스트임의 파일포인터를 파일 시작 부분으로 이동합니다.

예시를 보겠습니다.

#include <stdio.h>
#include <string.h>

void clear_buffer()
{
	int ch;
	while ((ch = getchar()) != '\n' && ch != EOF);
}

void main() {
	char ID[8];
	char name[10];
	fputs("학번 6자리를 입력하세요: ", stdout);
	fgets(ID, sizeof(ID), stdin);

	//입력에'\n'이 포함된 경우 '\0'으로 바꾼다.
	for (int i = 0; i < sizeof(ID) / sizeof(char); i++)
	{
		if (ID[i] == '\n')
			ID[i] = '\0';
	}
	//입력 overflow 발생을 고려하여 (ID[]에 \n이 포함되지 않은 경우 overflow) 버퍼를 비운다.
	if (strlen(ID) + 1 == sizeof(ID) / sizeof(char))
		clear_buffer();

	fputs("이름을 입력 하세요: ", stdout);
	fgets(name, sizeof(name), stdin);

	//입력에'\n'이 포함된 경우 '\0'으로 바꾼다.
	for (int i = 0; i < sizeof(name) / sizeof(char); i++)
	{
		if (name[i] == '\n')
			name[i] = '\0';
	}
	//입력 overflow 발생을 고려하여 (ID[]에 \n이 포함되지 않은 경우 overflow) 버퍼를 비운다.
	if (strlen(name) + 1 == sizeof(name) / sizeof(char))
		clear_buffer();

	printf("학번: %s\n", ID);
	printf("이름: %s\n", name);
}

위 코드는 먼저 입력에서 \n문자가 있고 이를 처리하지 않는다면 의도치 않게 줄이 바뀔 수 있으므로 줄 바꿈 문자가 있다면 이를 NULL문자로 교체해 주었습니다.

입력이 배열의 크기를 넘어가게 될 경우 필요한 만큼의 정보만을 받고 버퍼에 남아있는 정보들은 미리 정의해 둔 clear_buffer함수를 이용해서 버퍼를 초기화하고 다음 입력을 받을 수 있도록 하였습니다.

 

반응형

'언어 > C언어' 카테고리의 다른 글

[C 언어] 13. 구조체  (0) 2024.04.14
[C 언어] 12. File  (0) 2024.04.14
[C 언어] 10.포인터  (0) 2024.04.14
[C 언어] 9. 함수  (0) 2024.04.14
[C 언어] 8. 배열  (0) 2024.04.14