POCU C언어 정주행 8회차 - 스트림, 입출력 함수, 버퍼, 입력 알고리즘

2022. 12. 14. 20:28C언어 복습

1. 스트림 (Stream)

stream이라는 단어는 흐름이라는 뜻을 가지고 있으며 물이 흘러 내리는 것처럼 한 방향으로의 흐름을 뜻한다. C에서 stream이라고 하면 데이터가 흐르게 해주는 다리의 역할을 하는 매개체이다. 스트림에도 여러 종류가 있을 수 있는데 이 글에서 다룰 내용은 콘솔 입출력 스트림이며 키보드와 모니터를 뜻한다. 이제 전체 구조를 보자.

입출력 스트림

사실 원래는 스트림을 사용하려면 운영체제에 스트림의 생성을 요청해야 한다. 근데 우리가 지금까지 콘솔로 프로그램을 작성하면서 그런 코드를 작성한 적이 없던 이유는 콘솔 입출력을 위한 스트림은 프로그램이 실행과 종료에 따라 자동으로 생성과 소멸을 알아서 시켜주기 때문이다. 이 스트림을 표준 입출력 스트림이라고도 하며 다음과 같은 것들이 있다.

  • stdin 표준 입력 스트림 (키보드 대상)
  • stdout 표준 출력 스트림 (모니터 대상, 라인 버퍼링 사용)
  • stderr 표준 출력 스트림 (모니터 대상, stdout과 동일하나 용도가 다름)

데이터가 스트림을 통과할 때, 버퍼라는 곳을 반드시 지나가야 한다. 스트림에 존재하는 버퍼의 역할은 데이터를 임시로 저장하기 위함인데 데이터 전송의 효율성 때문에 데이터를 임시로 저장한다. 우리가 어떤 물건을 나를 때, 하나씩 손으로 나르기 보다 어딘가에 가득 담아서 나르는 것이 더 효율적인 것과 비슷하다고 보면 된다.

 

 

2. 입출력 함수

C는 다양한 문자열 입출력 함수를 제공하고 있으며 이 글에서는 총 8개의 함수를 알아볼 것이다. 

 

매개 변수: 출력할 문자의 아스키 코드 값

반환값: 출력한 문자의 아스키 코드 값 (실패시 EOF 반환)

문자 하나를 stdout으로 표현되는 표준 출력 스트림으로 전송한다.

int putchar(int c);

 

매개 변수: 출력할 문자의 아스키 코드, 지정할 출력 스트림

반환값: 출력한 문자의 아스키 코드 값 (실패시 EOF 반환)

출력 스트림을 지정하는 것 외에 putchar와 동일하다.

int fgetc(int c, FILE* stream);

 

매개 변수: 없음

반환값: 입력받은 문자의 아스키 코드 값 (실패시 EOF 반환)

문자 하나를 stdin으로 표현되는 표준 입력 스트림으로부터 받아오는 함수이다.

int getchar(void);

 

매개 변수: 지정할 입력 스트림

반환값: 입력받은 문자의 아스키 코드 값 (실패시 EOF 반환)

입력 스트림을 지정하는 것 외에 getchar와 동일하다.

int fgetc(FILE* stream);

 

매개 변수: 출력시킬 문자열

반환값: 음수가 아닌 정수 (실패시 EOF 반환)

stdout으로 표현되는 표준 출력 스트림으로 문자열을 전송하는 함수이다.

/* 개행 해줌 */
int puts(const char* str);

 

매개 변수: 출력시킬 문자열, 지정할 출력 스트림

반환값: 음수가 아닌 정수 (실패시 EOF 반환)
출력 스트림을 지정하는 것 외에 puts와 동일하다.

/* 개행 안해줌 */
int fputs(const char* str, FILE* stream);

 

매개 변수: 입력받을 문자열

반환값: 입력받은 문자열의 첫 주소 (실패시 NULL 반환)

stdin으로 표현되는 입력 스트림으로부터 매개 변수에 문자열을 입력받는 함수이다.

/* 개행 문자를 넣어주지 않음. 버퍼 오버플로우에 위험하니 쓰지 말자. */
char* gets(char* str);

 

매개 변수: 입력받을 문자열, 입력받을 문자열의 길이, 지정할 입력 스트림

반환값: 입력받은 문자열의 첫 주소 (실패시 NULL 반환)

문자열의 길이, 입력 스트림을 지정하는 것 외에 gets와 동일하다.

/* 개행 문자 넣어줌. gets쓰지 말고 이거 쓰자. */
char* fgets(const char* str, int n, FILE* stream);

 

주목할 부분은 int getchar(void); 같은 함수처럼 입력받은 문자를 반환할 때 char로 반환하지 않고 굳이 int로 반환하는 것이다. 왜 이런 것인지를 알기 위해서는 EOF라는 것을 알아야 한다. EOF는 End Of File의 약자로 -1이라는 int형 정수로 정의되어 있으며 이 수가 반환되면 파일의 끝에 도달해서 데이터를 읽을 수 없다는 의미가 된다. 그래서 getchar함수는 읽을 데이터가 없으면 EOF를 반환하는데 char로 반환을 하면 -1이라는 값으로 반환이 된다는 보장이 없다.

 

아래의 링크를 참고하면 왜 char로 -1을 반환시키기 꺼려하는지 알 수 있다. (signed vs unsigned)

https://dafher-diary.tistory.com/3

 

POCU정주행 1회차 - 매개변수 빈칸 vs void / 주석 / signed vs unsigned

1. 함수 선언에서 매개변수 리스트를 작성할 때, 빈 칸 vs void 함수 2개를 아래와 같이 선언했다고 가정하자. int FuncA(); int FuncB(void); 필자는 이 2개의 선언이 같은 뜻이라고 생각했다.... 착각이었다

dafher-diary.tistory.com

여담으로 scanf라는 입력 함수도 있는데 이 함수로 %s라는 서식 문자를 사용해서 문자열을 입력받을 경우에는 한 단어씩 읽으며 공백은 전부 버린다. 단, %c를 사용해서 문자를 읽을 때는 예외적으로 공백을 읽는다. 반환값은 scanf가 몇 개의 데이터를 읽었는지를 반환하며 첫 번째 데이터를 읽기 전에 읽기에 실패할 경우 EOF를 반환한다.

 

 

3. 버퍼

위에서 스트림에 대해 정리할 때, 버퍼에 대한 것을 간략하게 설명했었다. 그리고 위에서 소개했던 문자열 입출력 함수들은 사실 모두 입출력 스트림을 통해 입출력 버퍼를 거쳐서 키보드, 모니터 등으로 입출력이 가능했던 것이다. 문자열 입출력 함수를 사용할 때마다 입출력 스트림을 지나서 입출력 버퍼에 데이터가 쌓이기 때문이다. 그런데 C언어는 이렇게 쌓인 데이터를 비울 수 있는 함수를 제공한다.

int fflush(FILE* stream);

fflush함수는 출력 버퍼를 위해 만들어진 함수이며 실제로 stdout이나 다른 출력 스트림을 매개 변수로 넣고 fflush함수를 호출하면 출력 버퍼가 비워지면서 그것을 대상으로 데이터가 이동한다. 그리고 출력 버퍼를 비운다는 것은 출력 스트림이 목적지로 하는 곳에 데이터를 이동시킨다는 뜻이기도 하다.

 

다만 입력 버퍼를 비우기 위해서 fflush(stdin); 과 같은 코드를 짠다면 매우 위험해질 수 있다. fflush는 출력 버퍼를 비우기 위해서 만들어진 함수여서 C 표준에서는 이런 코드의 결과를 정의하고 있지 않고 있기 때문이다. 컴파일러마다 입력 버퍼를 정상적으로 비워주는 경우도 있지만 위험하니까 하지 말자. 참고로 여기서 입력 버퍼가 비워진다는 것은 입력 버퍼에 있는 데이터가 소멸한다는 것을 의미한다.

 

그럼 입력 버퍼를 비우기 위해서는 어떻게 해야할까? 아래와 같이 입력 버퍼를 비우는 방법이 있다.

while (getchar() != '\n');

이러면 개행 문자를 읽을 때까지 입력 버퍼를 전부 읽어버려서 입력 버퍼를 비우는 효과가 있다.

 

 

4. 입력 알고리즘

문자열 함수를 적절하게 사용해서 한 문자, 한 줄, 한 데이터, 한 블록씩 읽는 방법에 대해서 배우고 코드로 짜보았다. 이걸 잘 배워두면 나중에 파일 입출력에서 사용한다고 한다. 한 블록씩 읽는 방법에 대한 코드는 다음 글에서 할 것 같다. 또한 강의에서 제시한 예제 문제를 보고 그것도 풀어봤다. 배운 것을 토대로 최선을 다해서 짜보았고 저번 포스팅과 마찬가지로 이번 역시 피드백을 주시면 정말 감사할 것 같다.

 

read_algorithm.h

#ifndef __READ_ALGORITHM_H__
#define __READ_ALGORITHM_H__

#define LINE_LENGTH 20

void read_character(void);

void read_line(void);

void read_number(void);

#endif

 

read_algorithm.c

#include "read_algorithm.h"

#include <stdio.h>

void read_character(void)
{
    int c;
    
    while ((c = getchar()) != EOF)
    {
        putchar(c);
    }
}

void read_line(void)
{
    char str[LINE_LENGTH];
    
    while (fgets(str, LINE_LENGTH, stdin) != NULL)
    {
        fputs(str, stdout);
    }
}

void read_number(void)
{
    char str[LINE_LENGTH];
    int num;
    
    while (fgets(str, LINE_LENGTH, stdin) != NULL)
    {
        if (sscanf(str, "%d", &num) > 0)
        {
            printf("%d\n", num);
        }
    }
}

 

pocu_stream.h

#ifndef __POCU_STREAM_H__
#define __POCU_STREAM_H__

#include "stddef.h"

#define MATCH_COUNT (5)
#define NUM_CHAMPS (5)

#define BUFFER_LENGTH (4096)

void wirte_match_history(char* buffer,
    const char* names[], const float kills[],
    const float deaths[], const float assists[],
    const int wins[], const int losses[], const size_t count);
    
void read_match_history(char* buffer);

void input_match_history(void);

#endif

 

pocu_stream.c

#include "pocu_stream.h"
#include "string.h"

#include <stdio.h>

void wirte_match_history(char* buffer,
    const char* names[], const float kills[],
    const float deaths[], const float assists[],
    const int wins[], const int losses[], const size_t count)
{
    char line[BUFFER_LENGTH];
    float KDA;
    float winning_rate;
    size_t i;
    
    for (i = 0; i < count; i++)
    {
        KDA = (*kills + *assists) / *deaths;
        winning_rate = (float)*wins / (*wins + *losses) * 100.0f;
        
        sprintf(line, "%s %f %f %f %f %d %d %f\n",
            *names++, *kills++, *deaths++, *assists++,
            KDA, *wins++, *losses++, winning_rate);
            
        concatenate_string(buffer, line);
    }
}

void read_match_history(char* buffer)
{
    char names[BUFFER_LENGTH];
    float kills;
    float deaths;
    float assists;
    float KDA;
    int wins;
    int losses;
    float winning_rates;
    
    char* token = NULL;
    const char delims[] = "\n";
    
    printf("%8s %7s %7s %7s %7s %6s %6s %9s\n",
        "Champs", "Kills", "Deaths", "Assists",
        "KDA", "Wins", "Losses", "Rate");
        
    token = get_string_token(buffer, delims);
    
    while (token != NULL)
    {
        sscanf(token, "%s %f %f %f %f %d %d %f",
            names, &kills, &deaths, &assists,
            &KDA, &wins, &losses, &winning_rates);
            
        printf("%8s %7.2f %7.2f %7.2f %7.2f %6d %6d %8.2f\n",
            names, kills, deaths, assists,
            KDA, wins, losses, winning_rates);
            
        token = get_string_token(NULL, delims);
    }
}

void input_match_history(void)
{
    const char* names[NUM_CHAMPS] = {
        "Akali", "Sylas", "Yasuo", "Leblanc", "Aatrox"
    };
    
    const float average_kills[NUM_CHAMPS] = {
        6.11f, 6.62f, 4.81f, 5.95f, 5.19f
    };
    
    const float average_deatchs[NUM_CHAMPS] = {
        3.65f, 3.87f, 3.97f, 3.05f, 3.23f
    };
    
    const float average_assists[NUM_CHAMPS] = {
        4.63f, 6.68f, 4.47f, 5.25f, 6.02f
    };
    
    const int wins[NUM_CHAMPS] = {
        52, 55, 28, 34, 32
    };
    
    const int losses[NUM_CHAMPS] = {
        62, 38, 31, 21, 21
    };
    
    char buffer[BUFFER_LENGTH];
    
    write_match_history(buffer, names,
        average_kills, average_deaths,
        average_assists, wins, losses, NUM_CHAMPS);
        
    read_match_history(buffer);
}

여담으로 이건 내가 실수를 해서 새벽 시간내내 고생했던 부분인데... 솔직히 이건 내가 생각해도 알고나니까 내가 너무 멍청했다는 생각이 들었다. read_match_history부분을 보면 buffer로부터 데이터를 받기 위해 names, kills, deaths... 등 메모리를 선언한 부분을 볼 수 있을 것이다. 근데 사실 저것들은 원래 변수가 아니라 배열이었다. names는 char 배열이 아니라 char* 배열이었고 kills도 flaot kills가 아니라 flaot kills[BUFFER_LENGTH] 였다. 원래는 이 상태로 밑의 반복문에서 i값을 증가시키면서 인덱스 연산을 시키려하고 있었던 것이다.

 

근데 문제가 터졌다. 당연한 것이지만 char* names[BUFFER_LENGTH]에 names[i]로 문자열 데이터를 받아오려고 했어 에러가 발생했던 것이다... 포인터만 선언해놓고 그 포인터가 가리키는 곳에는 유효한 메모리가 없는 상태에서 &names[i]같은 곳에 데이터를 받으려고 했으니 당연히 문제가 생긴 것이다... 사실 이 문제를 해결하게 된 과정은 도저히 문제가 해결이 안되서 강의를 다시 봤는데 거기서는 배열로 하지 않고 하나의 변수를 선언해놓고 거기서 반복문마다 그 변수의 값을 새로운 값으로 다시 받아오는 것이다.

 

혹시나 해서 나도 같은 방식으로 해봤는데 작동이 잘 되는 것이다... 당연히 될 수 밖에 없는 것이었지만 그 때는 왜 그렇게 되는지 고민을 해보고 나서야 왜 기존에는 되지 않았다가 해결이 되었는지 알 수 있었다... 좀 쪽팔려서 적기 싫기는 한데 그래도 적어야지 하면서 적고 있다.