[C] C lang 콘솔 입력, 파일 입출력, 커맨드 라인 인자

입력

  • 출력의 반대

  • 외부의 데이터를 읽어와서 프로그램에서 사용
  • 어떤 데이터가 올 지 몰라서 괴상한 데이터가 종종 들어옴.
    • 사용자가 잘못된 데이터를 키보드에서 입력
    • 예전에 저장해 둔 파일을 누가 잘 못 바꿨거나 일부 데이터가 유실

입력은 출력보다 까다롭다.

  • 출력에 비해 조심해야 할 일들이 많음
  • 데이터 읽기에 실패했는데, 제대로 처리 안하면 펑펑 터짐
  • 정말 많이 실수한다.
  • 그래서 모든 입력 값에는 반환값이 존재함. 따라서 어떤 함수가 어떤 값을 반환하는 지, 문서에서 확실히 읽고 코드에서 검사할 것.

  • 대부분의 입력 처리 코드의 문제는 반환값이 뭔지 문서를 제대로 안 봐서 발생. 🤔



입력의 출처는 어디?

  • 입력은 어디에서 읽어올까?
  • 어딘가에 출력을 했다면 거기서 읽어올 수 있다고 생각하면 편함
  • 스트림
    • 콘솔 창에 출력했으니, 콘솔(키보드)로 부터 받아옴.
    • 파일에 출력(저장)했으니 파일로부터 입력받아 옴.
    • 등등
  • 문자열
    • 문자열에 출력(저장) 했으니, 문자열로 부터 , 입력받아 옴.
  • 어디로 출력하던 거의 비슷한 함수들이 몇 있었다.
  • 입력도 마찬가지



입력 처리 전략

  • 크게 4가지 전략이 있음.

1 . 한 글자씩 읽기 2 . 한 줄씩 읽기 3 . 한 데이터씩 읽기. 4 . 한 블록 씩 읽기(이진 데이터)



한 글자씩 읽기, 한 글자씩 읽는 알고리듬 1

1 . 한 글자(char)를 읽어온다. 2 . 그 글자를 필요한 곳에 사용한다. 3 . 1번 단계로 되돌아간다.


#include<stdio.h>

int c;

while(TRUE){    //define TRUE(); 
    c = getchar();
    putchar(c);
}

20221024_232251

20221024_232516



getchar() 반쪽짜리 설명 ver

int getchar(void)
int fgetc(FILE * stream)    //여기에 stdin 넣어주면 getchar와 동일함.
//getchar는 기본적으로 stdin에서 읽어오는 거, stdin이라 하면 키보드임.
  • 키보드 (stdin)으로 부터 문자 하나를 읽어서 int형으로 반환
    • 왜 int형?
    • 사실 굉장히 많은 입출력 함수들이 문자를 읽고 쓸 때, char 대신 int 씀
  • fgetc(stdin) 하고 같음



getchar()와 EOF 키

#include<stdio.h>

int c;

while(TRUE){    //define TRUE(); 
    c = getchar();
    putchar(c);
}

20221024_233853

위 코드에도 문제가 있다. 무한 루프 돌음.



언제 멈춰야 하는가?

1 . 특별한 키를 입력 받았을 경우

  • 예) ‘x’는 프로그램 종료
  • 메뉴 입력받은 거 받을 때는 가능
  • 그러나 타자 프로그램 등에서 모든 키보드 문자를 허용할 때는 불가능.

20221024_234024



  1. 어디에서도 사용 가능한 “입력 끝~” 을 나타내는 무언가가 있으면 좋겠다.
  • 그게 뭔가?
  • 혹시 반환값이 int형인게 뭔가 이유가 있어서가 아닐까?
    int getchar(void)
    
  • getchar()에 대한 설명에서 실마리를 얻어보자.(레퍼런스 참조)



getchar()의 반환 값

20221024_235603

  • 성공하면 문자를, 실패하면 EOF(End Of File)를 반환
  • EOF 바로 얘다.



입력의 끝을 나타내는 값, ‘EOF’

  • C 표준에 의하면 EOF는 음수라고 함.
  • 그런데 표준에 따르면 char는 부호가 있을 수도 있고 없을 수도 있음.
  • 따라서 char에 언제나 이 음수값을 담는게 불가
  • 그리고 char는 unsigned인지 char인지 표준이 정해두지 않음.
  • 이게 getchar()가 int를 반환하는 이유



완전한 getchar() 설명

int getchar(void);
int fgetc(FILE * stream);
  • 키보드 (stdin)로 부터 문자를 하나씩 읽음
  • 반환값
    • 성공시, 읽은 문자의 아스키 코드를 반환
    • 실패시 EOF를 반환
  • fgetc(stdin)하고 같음.



한 글자씩 읽기 알고리듬 2. EOF 키는 어디에 있는지

1 . 한 글자를 읽어온다. 2 . 글자를 읽어오는 데 실패했으면, (EOF)프로그램을 종료 3 . 아니라면 그 글자를 필요한 곳에서 사용한다. 4 . 1번 단계로 되돌아간다.


한 글자씩 읽는 코드:

#include<stdio.h>

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

근데 위 처럼 작성하면 EOF가 없어서 무한으로 입력받게 된다.



EOF키는 ctrl키와 섞어서 사용

  • 윈도우 계열 : ctrl+z
  • 유닉스 같은 시스템들 : ctrl+d



getchar()가 좀 거슬리는데

20221025_090650


1줄로 줄일수는 있다.

int c;

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



c=getchar()를 괄호로 안 감쌀 경우?

int c;

while(c = getchar() != EOF)
{
    putchar(c);
}
  • != 연산자가 = 연산자 보다는 연산자 우선순위가 높다.
    • != 연산자는 7위, = 연산자는 14위
  • 따라서 c= (getchar()!=EOF) 이렇게 되어버림.
    • 이 경우 c는 0 또는 1이 됨.
    • EOF면 0 이 되고 아니면 1이 됨
  • 한 줄로 줄이는 건 실수하기 쉬움.
  • 그래서 요즘 언어들은 잘 쓰지 않는다.



한글자 씩 읽는게 가장 유용한 경우?

  • 가장 간단한 입력방법
  • 입력이 문자/문자열일때 매우 좋음.
  • 쓸 데 없이 메모리에 입력값 저장 안해둬도 됨.
    • 글자 하나 읽어와서 하나 처리하고 바로 버린다.
    • 무슨 배열에 쌓아놓고 나중에 같이 쓰고 이러지 말자는 뜻.
    • 용량 절약
    • 실수 줄임
    • 막 대문자로 글자 바꾸기 이런거 할 때도 매우 유용.


O(N)!

  • for문 딱 한번만 도는 알고리즘에 적합한 경우가 많다.
  • 다음 두 방법이 뭐가 다른지 보자

    • 입력값을 모두 읽어 배열에 저장해두고, for문 돌려 처리
    • 키보드로부터 한 글자씩 읽어 곧바로 처리
  • 그러나 다른 데이터형으로 쓰기는 좀 어려움
    • 예: 정수형 1004를 읽기 ‘1’‘0’‘0’‘4’ 이렇게 4번 읽어서 정수 변환하기엔 좀..



공백과 줄 수세는 예제

20221025_093600

#include <stdio.h>

#include <ctype.h>
#include "whitespace_counter.h"

void print_whitespace_stat(void)
{
    int c;
    size_t = num_whitespace = 0u;
    size_t = num_newlines = 0u;
    
    c = getchar()
    while( c != EOF)
    {
        if(isspace(c)){
            ++num_whitespace;

            if(c =='\n')
            {
                ++num_newlines;
            }
        }
        c= getchar();
    }

    printf(# whitespaces = %5d\n", num_whitespaces);
    printf(# num_newlines = %5d\n", num_newlines);
    
}

main.c(별거 없음)

#include "whitespace_counter.h"

int main(void)
{
    print_whitespace_stat();
    return 0;
}



한 줄씩 읽기, gets()

생각보다 매우 많이 쓰고 중요한 방법

한 줄씩 읽는 알고리즘

1 . 한 줄을 읽어온다 2 . 한 줄을 읽어오는데 실패하면 프로그램을 종료 3 . 성공했다면, 한줄 읽어온 데이터를 필요에 따라 사용 4 . 1번으로 되돌아간다.


한 줄을 어떻게 읽나?

  • 한 줄을 읽어오면 어디에 저장해야 하는가
  • 한 줄 읽어오는 함수가 뿅 하고 새로운 문자열을 반환해 주는가?
  • 어쨌든 한줄 읽는 게 문자열 읽어온다는 개념. 줄이라는 거 자체가 문장, 문자열, char string이런거니까
    아무튼 배열에 저장을 하고싶다는 의미

20221025_140021

  • 그러면 새로 만들어줬으면, 내가 다 쓰고 난 뒤 프로그래머가 다 쓰고 난 다음 지워줘야함.
  • 근데 안 지워주면 그 메모리는 어딘가에 존재하는데 쓰지 못하는 그런 상황.
    • 메모리 누수
    • 이미 strstr에서 말함



  • C는 절대 그렇게 돌지 않는다. -> 이제 익숙해져야함
  • 따라서 프로그래머가 미리 만든 배열을 함수에 전달
  • 함수는 그 배열에 한 줄을 읽어옴.

20221025_141027

근데 여기서 한줄 읽어오는 함수는 gets().

  • 근데 너무 위험해서 퇴출당함. 그래도 일단 알아는 두자.



gets()

char* gets(char * str)
  • stdin에서 새줄 문자(‘\n’) 또는 EOF를 만날 때까지 계속 문자들을 읽어서 str배열에 저장.
    • 그래서 str이 const가 아님
  • 마지막 문자 바로 다음에 널 문자(‘\0’)도 넣어줌.
    • (그럼 뭐함 퇴출인걸)
  • stdin에서 새 줄문자를 제거하지만 버퍼에 저장하지는 않음.



gets()의 반환값

char * gets(char * str)
  • 반환되는 것은?
    • 성공시, str(String)
    • 실패시, NULL(널 포인터다, 널 문자 아님)
한 줄 읽기
#include <stdio.h>
#define LINE_LENGTH(64) //배열 64개 잡아주고

char line[LINE_LENGTH];

while(gets(line)!=NULL) //get 함수 호출할게요. 널 아닐떄까지 계속 받아옵시다 하는 것.
{
    puts(line)  //그래서 이걸 읽어와서 화면에 출력하는 것. 한줄 입력하고 엔터치고 a입력하고 엔터치고 이런거
}
  • 메우메우 위험한 함수
  • C11은 아예 제거해버림
  • 그래서 최신 헤더파일에선 gets가 아예 존재하지 않음.
    • 굳이 쓰려면 함수 원형을 전방 선언해야함.



왜 위험한가?

  • 만약 64자 이상 입력하면? -> 버퍼 오버플로
  • 늘리면 되겠지 1024자로
    #define LINE_LENGTH (1024)
    
  • 근데 1024 이상 입력시 버퍼 오버플로 발생 -> 또 늘리면?
    #define LINE_LENGTH (2048)
    
  • 그럼 이번엔 2048자 이상 입력
  • 무한 굴레에 빠짐.
  • 한마디로 통제 불가.



버퍼 오버플로우가 발생

#include <stdio.h>
#define LINE_LENGTH(10)

void print_my_input(void)
{
    char line[LINE_LENGTH];
    gets(line);
    puts(line);
}

int main(void)
{
    print_my_input();
    printf("Well done\n");

    return 0;
}

결과:

20221025_144614

위처럼 열심히 입력하고 출력해보면 ente까지만 출력이 됨.

중요한건 아무리 봐도 10개인데, 문제가 있을거 같은데 (왜냐면 훨씬 더 넣었기 떄문, 위 사진은 10개 이후로 계속 나옴)

20221025_144747

즉, 읽다가 널 문자 만나서 멈춘 거. 우연하게, 결과적으로 버퍼 넘어서 쓰고 읽고 다 한거.



왜 오버플로우가 발생하는가?

  • 올바르지 않은 주소에 키보드로 입력받은 값을 써버림.

20221025_145912

함수 만들어서 매개변수 호출하고 이랬다고 가정해보자. 이 매개변수 안에 그리고 그 안에 뭔가 버퍼를 잡아줬다 가정.

그럼 버퍼가 실제 함수 실행되는 스택 프레임이였고, 여기서 부터 쭉 적은거 쭉 적다보니 여기저기로 가고 돌아갈 베이스 포인터가 있었다. 그 돌아갈 주소 그것도 덮어 쓸 수 있고, 별에 별 것들 다 덮어 쓸 수 있다.

버퍼 오버플로 어택 이 위 방식을 사용해서 공격 하는 것.

이런 것들 떄문에 fgets() 잘못 쓰는 순간 굉장히 문제가 많은 코드들이 많이 나왔었고 아예 표준에서 삭제해버림.

그래서 절대 *100 gets()를 써선 안 됨.
C11부터 사라진게 천만 다행.



fgets()로 안전하게 한 줄 읽기

char * fgets(char *str, int count, FILE * stream);
  • stdio.h에 있음
  • 최대 count -1개의 문자열을 읽어서 str에 저장
  • 즉, 새 줄을 만나지 않아도, 이 함수가 반환될 수 있음.
  • str에 새 줄까지 넣어줌.



왜 새 줄 문자(‘\n’)가 들어가는지?

  • 새 줄을 만나서, 끝났을 떄와 아닐 때를 구분해야 하기 때문
    • 이게 gets() 동작 방식과 다른 이유

20221025_151056



fgets()의 매개변수

char * fgets(char *str, int count, FILE * stream)
  • str : gets()와 마찬가지로 입력받은 한 줄을 저장할 char 배열
    • 역시 const가 아님.
  • count : 한번에 str에 쓰는 최대 문자 수 1개
    • 널 문자를 포함하기 떄문에 실제로 읽어오는 문자수는 count -1개
  • Stream : 데이터를 읽어올 스트림. 키보드 입력을 읽어오고 싶다면, stdin을 넣어주면 됨.
FILE 자료형
  • 출력함수 할 떄도 나옴.
  • 스트림을 제어하기 위해 필요한 정보들을 담은 자료형(모든 * 정보들을 담았다고 일반적으로 봄)
    • 그럼 그떄 필요한 자료들이 뭐냐, 파일 현재 위치가 어디냐. 이런것들.(3바이트 읽었으면 3바이트 위치에 있고 10바이트 읽었으면 10바이트 위치에 있고 이런거)
  • 스트림을 제어하기 위해 필요한 정보는?

1 . 파일 위치 표시자
2 . 스트림이 사용하는 버퍼의 포인터
3 . 읽기/쓰기 중 발생한 오류를 기록하는 오류 표시자
4 . 파일 끝에 도달했음을 알리는 EOF지시자.

  • 플랫폼 마다 이 자료형을 구현하는 방식은 다를 수가 있음.
  • 입력 및 출력 스트림은 오직 FILE 포인터로만 접근 및 조작 가능.
  • 이름이 FILE이라 파일만 될것 같지만 다른 스트림들도 모두 표현 가능.
  • 나중에 실제 파일을 열어서 거기에 쓰거나 읽을 떄도 이 자료형을 사용할 것임.



fgets()의 반환값

char *fgets(char * str, int count, FILE * stream)


  • str : gets() 와 마찬가지로 입력받은 한 줄 저장할 char 배열.
    • 역시 const가 아님.
  • count : 아까와 마찬가지로? 한 번 str에 쓰는 최대 문자 수
    • 널 문자를 포함하기 때문에 실제로는 읽어오는 문자수는 count -1 개
  • stream : 데이터를 읽어 올 스트림. 키보드 입력을 읽어오고 싶다면 stdin을 넣어주면 됨.



근데 stream의 자료형이 좀 이상하다.

char * fgets(char * str, int count, FILE * stream);
  • 출력 함수들 볼 때도 봤다.
  • 스트림을 제어하기 위해 필요한 정보를 담고 있는 자료형.
  • 스트림을 제어하기 필요한 정보

1 . 파일 위치 지시자 2 . 스트림이 사용하는 버퍼의 포인터 3 . 읽기/쓰기 중에 발생하는 오류를 기록하는 오류 표시자. 4 . 파일의 끝에 도달했음을 알리는 기록하는 EOF 지시자.


  • 플랫폼마다 이 자료형을 구현하는 방식은 다를 수 있음.
  • 입력 및 출력 스트림 오직 FILE 포인터로만 접근 및 조작 가능.
  • 이름이 FILE이라 파일만 될 것 같지만 다른 스트림도 모두 표현 가능.
  • 나중에 실제 파일을 열어서 거기에 쓰거나 읽을 떄도 이 자료형을 사용할 것.😀

fgets() 의 반환값

char * fgets(char * str, int count, FILE *stream)
  • gets() 와 동일

  • 성공시, str 를 반환
  • 실패시, NULL 을 반환
#include <stdio.h>

#define LINE_LENGTH(10)

char line[LINE_LENGTH];

while(fgets(line, LINE_LENGTH, stdin)!=NULL)
{
    printf("%s", line);
}

20221026_003136

여기 입력 스트림에서 LINE_LENGTH-1 개 만큼 읽음. line의 마지막 요소에는 ‘\0’를 넣어줌. 1~9까지 읽어 마지막 당연히 널 문자를 넣어줌.

20221026_003202

아직 입력 스트림에 남아있는 문자들을 읽음. 읽는 중간에 새 줄 문자(‘\n’)를 만남. 새 줄 문자까지의 문자열을 line에 넣음.

언제나 배열의 크기 충분히 크게 잡을 것


성공적으로 읽었으므로 NULL 이 아님.

20221026_003843



fgets()에 쓸 버퍼는 초기화 없음

20221026_003936

fgets()가 읽지도 않음. 그래서 얘를 무조건 처음부터 채워줌. 그런 쓰레기값 저장되어 있지만 처음부터 읽어주고 널 문자도 이쁘게 알아서 잘 해줌.

char line[LINE_LENGTH];

while(fgets(line, LINE_LENGTH, stdin)!= NULL)
{
    printf("%s", line);
}
  • puts() 대신 printf()쓴 이유?

    • gets()로 읽어온 것엔 puts() 써도 말이 됨
    • fgets() 에 말이 안 됨
      • puts() 문서 읽어보면 힌트가 있다.



한 줄씩 읽는 방법이 유용한 경우

  • 일단 단어 하나씩 읽는 것 보다는 한 줄씩 읽는게 빠름
  • CPU를 벗어나 외부 구성 요소로부터 뭔가를 읽어올 때는 한번에 많이 읽어오는 게 빠르기 때문
  • 따라서 버퍼 크기는 충분히 큰게 좋다

    • 하지만 당연히 버퍼 오버플로는 없어야 한다.

근데 이 자체로 의미가 있다기 보다 뒤에서 배울 ‘한 데이터씩 읽어올 때’ 쓸 일이 더 많음(안전하게 쓰기 위해)



한 데이터씩 읽기, scanf()

출력에는 이쁘게 포맷해주는 함수가 있다.

  • 출력에서는 여러 자료형을 이쁘게 출력해주는 아이가 있음.
  • print formatted-> printf()
  • 읽는것도 그게 가능함. -> scanf()

3가지 버전 존재

1 . scanf(): stdin으로 부터 읽음.

int scanf(const char* format, ...)

2 . fscanf(): 파일 스트림으로 부터 읽음.

int fscanf(FILE* stream, const char* format, ...)

3 .sscanf(): C 스타일의 문자열로 부터 읽음

int sscanf(const char* buffer, const char* format, ...)

printf 와 비슷하다

stdin 에서 정수 읽기

int main(void)
{
    int num;
    printf("enter a number");
    scanf("%d", num);
    printf("num = %d", num);
}

return 0;


  • scanf()
int scanf(const char* format, ...)
  • stdio.h 에 있음
  • 키보드(stdin)로 부터 입력 받아 변수에 저장


왜 저장할 변수의 주소를 전달하나?

  • 참조에 의한 전달 흉내중
  • 그냥 num을 넣으면 복사된 매개변수. 함수속에서 바꿔봐야 반환시 사라짐.
//정확한 코드는 아니지만 이런식이 되어버림
int scanf(const char * format, int num, ...)
{
    //10은 입력 스트림에서 얻어옴
    num =10;
}


———-

그냥 scanf()에서 반환하면 안되나?

//정확한 코드는 아니지만 이런식이 되어버림
int scanf(const char * format, ...)
{
    //10은 입력 스트림에서 얻어옴
    num =10;
}
  • 불가능
    • 읽는 타입에 따라 함수 이름이 달라져야 함
      int scanf_int()
      float scanf_float()..
    

scanf의 반환값

  • scanf는 뭘 반환하나?
  • 몇개 데이터 읽었는지 반환
  • 첫 데이터 읽기 전 실패하면 EOF를 반환



scanf()의 일반적인 서식 문자열 형식

%[*][너비][길이] 서식지정자
  • 일반적으로 % 뒤에 최대 4개의 지정자를 지정할 수 있음. 1 . * 선택 2 . 너비(선택) 3 . 길이 수정자 (선택) 4 . 서식 지정자(필수)

  • 반드시 순서 지켜서 작성해야 함.

scanf의 형식 지정자

20221101_133646

scanf()의 서식자

20221103_004224

  • 반드시 넣어야 함
  • 모든 데이터는 한 단어씩(공백문자로 구분)또는 가능할 떄 까지 읽음
  • 공백문자는 버림(예외: %c)



대입 생략 문자 *

  • 영어로는 assignment-suppressing character 라고 함
  • 이 문자를 쓸 경우 키보드로부터 받은 입력을 변수에 저장하지 않음
int num;
printf("enter:");
scanf("%*d%d", &num);

printf("num: %d",num);

너비

출력 뿐 아니라 입력시에도 너비를 지정할 수 있다.

20221103_010252

  • %s의 경우 너비를 지정 안하면 버퍼 오버플로 발생



너비 지정시 주의할 점

  • 너비 지정 후 여러 데이터 한 번에 읽을 시 문제 발생

20221103_011356

3개씩 끊어서 올떄 애매하게 오게 될수 있는데?


길이 수정자

20221103_011646



문자를 읽을 때 scanf()의 문제점과 해결책, clearerr()

문자열이 또…

  • 막강함
  • 근데 문제 있어 보이는데
  • %s 쓸 떄 배열크기보다 큰 문자열이 들어오면 버퍼 오버플로

20221103_015445

숫자만 읽어야 하는데 문자를 읽으면..

  • 그래서 scanf()는 문자열 읽을때 스면 별로임
  • 근데 그 뿐 아니라 다른 자료형 읽을 떄도 무한루프에 빠질 위험이 큼.

무한루프 안 빠지고 읽는 올바른 예

20221103_015745

fgets와 sscanf써서 하는게 맞다.


버퍼 오버플로 문제 없이 문자열 읽기

word와 line 맞춰주고(4096)

  • 버퍼 오버플로는 발생하지 않음
  • 단, 4096보다 긴 문자열 들어오면 잘리는게 전부



근데 clearerr()는 뭔가

void clearerr(FILE* stream);
  • clear error
  • 스트림을 읽거나 쓸떄 EOF를 만나면 그 스트림의 EOF표시자(indicator)가 세팅됨
  • 그 외 이유로 실패시 오류 표시자를 세팅
  • 그게 안 지워져서 다음에 읽거나 쓸 때 계속 실패 가능
  • 그래서 그 오류를 지워주는 것
  • 참고로 저 표시자의 세팅여부를 보고싶다면 feof씀
  • 크게 중요하진 않음


한 데이터 씩 읽는게 유용한 경우?

  • 텍스트를 다른 자료형으로 곧바로 읽어오는 가장 간단한 방법
  • 안 그러면 문자열로 적힌 정수를 어떻게 읽어서 변환하나?
    • 넘 귀찮
  • 사용자 입력 받을시 (여러 데이터가 혼용 된 텍스트 파일 읽을시 ) 가장 많이 쓰는 법



한 블록씩 읽기

이진파일은 어떻게 읽나

  • 여태 입력방법은 텍스트로 저장된 데이터를 읽는 것
  • 한 블럭씩 읽는 법

한 블록씩 읽기

size_t fread(void * buffer, size_t size, size_t count , FILE * stream)
  • size 바이트 데이터를 총 count 개수 만큼 읽음
  • 그래서 buffer에 저장


20221105_185506



한 블록 읽기

size_t fread (void * buffer, size_size, size_t count, FILE * stream);
  • EOF 만나면 당연히 멈춤
  • 그러면 count 보다 적은 수를 읽을 수 있다는 얘기
  • 그래서 실제 읽는 개수를 반환
  • 참고로 읽는 게 있으면 당연히 쓰는 것도 있음.
size_t fwrite(const void* buffer, size_t size, size_t count, FILE * stream)



실제로 블록을 읽어보면

  • 이 함수를 실제로 사용해보려 했는데 .. 어디에 쓰지?
  • stdin, stdout은 텍스트가 들어오는데
  • 이진 데이터 어떻게 보여주나



파일 스트림이 있으면 작동하는 코드

int nums[64]
size_t num_read;
FILE * fstream; //곧 배울 파일 스트림

num_read = fread(nums, sizeof(nums[0], 64, fstream));
fwrite(nums, sizeof(nums[0]), 64, fsteram);
  • int 블록을 저장
    • 총 몇개? 64개
    • 총 몇 바이트? 64 * sizeof(int)
  • fstream은 파일 스트림

한 블록씩 읽는게 유용한 경우

  • 가장 중요한 건 데이터 읽기 위해
  • 이진 데이터를 하나씩 읽을 수도 있지만, 한꺼번에 읽으면 성능 향상
    • 앞서 말한것과 마찬가지의 이유


한 블록씩 읽을 때 주의할 점

  • 기본 데이터형의 크기는 시스템마다 다르다.
  • 따라서 이런게 안 될수가 없다.
    • A 시스템 용으로 빌드한 실행 파일을 실행해서 파일을 저장
    • B 시스템 용으로 빌드한 실행 파일을 실행해서 그 파일을 읽음
    • 그러나 바이트 크기가 틀려서 엉뚱한 데이터가 읽힘.
  • 따라서 이러한 일들을 하려면 정확히 파일에 저장할 데이터를 저장해두는게 좋음

파일 입출력

  • C에서 파일 다루기는 되게 힘들다.
    • C#처럼 파일 내용을 한번에 읽어서 문자열 배열로 반환하는 등의 함수가 없다.
    • 심지어는 파일 복사 함수도 없다.
      • 각 운영체제에서 제공하는 함수 쓰거나
      • 그 기능을 직접 스트림 읽기/쓰기 함수를 사용해서 구현해야 한다.



따라서 C 파일 관련은 다 이렇다.

1 . 파일을 열어서 파일 스트림을 가져온다. 2 . 그 파일 스트림을 사용해서 하고 싶은 걸 한다. 3 . 그 파일을 닫아준다.

20221105_215451

FILE * fopen(const char * filename, const char * mode)
  • filename으로 지정된 파일을 연다(받을 떄 이진파일인지 텍스트 파일인지 이런거 판단)

  • 열 떄 사용하는 모드 (읽기 전용, 이진 파일 등은) 모드로 지정

  • 반환값은 파일 스트림 포인터



파일 열기 모드

20221105_220352

  • b를 붙이면 이진 모드 파일로 엶
  • 이진 모드란
    • 사실 유닉스 계열에선 별 차이가 없다.
      • rb나 r이나 별 차이 없음
    • 윈도우 새 줄 문자 처리하는 것만으로도 달라진다.
      • 그외 문자는 그대로

20221105_222709


20221105_222747



파일에 쓰기/읽기 예, fflush, 파일에 이어 쓰기 예

20221105_223130


20221105_223211

fwrite는 \n을 \n으로 인식 못한다.

size_t fwrite(const void* buffer, size_t size, size_t count, FILE* stream)
  • 첫 인자로 받는 buffer가 char* 도 아니고, int * 도 아니고 float* 도 아니고 그냥 void* 임

  • 즉, fwrite() 입장에서는 그냥 비트 패턴이 쭉 들어옴
    • char * 로 들어오면, 1바이트 단위로 읽어서 아스키 코드로 인식하지만,
    • void* 이기 때문에 숫자에 의미가 없어진다.
  • 0x0A가 fwrite() 입장에서 \n을 의미하는 건지, 정수 10을 의미하는 건지 아니면 부동소수점의 일부인지 알 수 없다.

  • 따라서 fflush()가 유일한 해결법



파일 닫기, 파일 오류처리, stderr, strerror(), perror()

20221105_230148

1 . 파일 열어서 스트림 가져옴 2 . 그 파일 스트림을 이용해서 하고 싶은거 함 3 . 그 파일 다 읽음

근데 위에서 3번이 없다.

열어둔 파일을 닫지 않으면 문제가 된다.



내가 열었으면 내가 닫아야 함

  • 파일은 운영체제가 열어주는 것
  • 운영 체제는 우리가 언제 파일을 다 써서 필요 없는 지 모름.
  • 따라서 직접 말 안해주면 알아서 닫지 않는다.
  • 계속 파일을 열기만 하면 어느 순간 운영체제가 더이상은 파일 열수 없다면 뻗을 수가 있다.
  • C# 같은건 알아서 해줬다.
    • 그건 언어가 알아서 해줘서..
    • C는 그런거 없다. (^오^)



파일 닫기

int fclose(FILE* stream)
  • 파일을 닫음
  • 성공하면 0 , 실패하면 EOF를 반환
  • 버퍼링 중 스트림은 이렇게 작동
    • 출력 스트림: 버퍼에 남아있는 데이터는 파일로 보냄
    • 입력 스트림: 무시하고 바이바이
  • 와.. 끝?



하지만 C는 예외처리가 없다.

  • 그럼 fopen함수는 실패하면 뭘 반환하나?
FILE * fopen(const char* filename, filename, const char* mode);

-> 널포인터

stderr이란?

  • 프로그램 실행시 자동으로 3개 스트림 만들어준다.
  • stdout, stdin
  • stderr도 stdout과 비슷
  • 다만 stderr은 오류 관련 메시지를 출력하는 전용 스트림
    • 반드시 규칙은 아니고 관습.. 관례?
    • 참고로 이 아이는 보통 버퍼링을 안 씀(오류 보여줘야 하므로)
  • 콘솔창에서 실행 할 때, stdout과 stderr 모두 까만 화면에 같이 섞여나옴-> 이미 봤다.
    • 이거 분리하는 법은 입출력 리디렉션떄 봄.



파일 열기에 실패하는 경우

20221105_233407

근데 위에서 != 보다는 ==null 이런 방식이 더 좋다고는 함(명시적이므로)


오류 코드를 보여주는 코드

20221106_000918

숫자로 어쩌란거지..

근데 이게 숫자인데 무슨 의미인지 어떻게 알지?

  • 알 방법 없음.
  • 거기다 각 컴파일러마다 구현을 다르게 함.



오류를 말로 해주는 코드가 있긴 하다

char * strerror(int errnum)
  • string.h 에 정의 됨.
  • errno 넣으면 문자열로 된 친절한 설명을 돌려줌.

생생한 오류 보여주는 코드

20221106_001229

참고로 C에서의 오류 처리는

1 . 함수가 곧바로 오류코드를 반환 2 . 내부적으로 오류코드를 전역변수로 들고 있다가 검사.

  • 당연히 까먹기 쉬움
  • 문서 안 보면 어떻게 세팅 되어있는지도 잘 모름.



입출력 리디렉션

  • 손가락이 아파서 못해먹겠다
    • stdin 맨날 받을 때 마다 타이핑 해야하고 EOF 넣기도 귀찮고
  • 눈 아파서 더 못해먹겠다
    • stdin 뒤에 stdout 출력하면 뒤죽박죽 나오고
    • stdout/stderr 출력 했는데 섞여 나오고

리디렉션을 전적으로 믿어야 한다

  • 위 문제를 해결할 방법이 리디렉션
  • 영어로는 IO redirection

리디렉션이 뭔가 그래서

  • 방향을 틀어준다.
  • 즉, 입력이나 출력의 방향을 다른데로 돌려줌.
  • 입력 리디렉션(코드 변경 없음)
    • 개념: 텍스트 파일을 열어 stdin 대신 타이핑 해주는 기능
  • 출력 리디렉션
    • 개념: stdout 에 출력 되는 것들을 화면에 보여주는 대신 텍스트 파일에 저장
    • 마찬가지로 stderr에 출력되는 것도 별도로 텍스트 파일에 저장해줌.

입출력 리디렉션은 C의 기능이 아님

  • C의 기능이 아닌 커맨드 라인 또는 shell의 기능. 어떤 프로그램에 써도 가능.
사용방법
  • stdin: <를 사용
  • stdout : >를 사용
  • stderr: 2>를 사용(두 번쨰 출력이라 생각)

20221106_031526

당연히 셋중 필요한 거만 골라 사용 가능



  • 손가락이 안 아파짐
    • 여태껏 다양한 테스트 할 떄 콘솔에서 타이핑 한거 전부 input.txt에 저장
    • 그러고 a < input.txt 하면 끝
  • 출력과 오류 메시지 확실히 구분
    • 출력만 파일에 저장, 오류 메시지는 여전히 화면에
    • 아니면 출력을 화면에, 오류 메시지는 파일에
    • 아니면 둘다 별도의 파일에

파일을 다 읽는 순간 EOF가 자동으로 추가

  • 기존 키보드에서 직접 입력할 떄는 EOF+엔터 를 쳐줘야 했다
    • 프로그램이 키보드 입력 끝을 알 수 없으니 사용자가 직접 넣어줌
    • 그러나 EOF뒤에 새 줄 문자가 따라옴
    • 다시 읽는 함수 호출하면 문제 될 수도
    • 하지만 파일로 입력 리디렉션 대신 할 떄 자동으로 EOF가 입력됨
  • 이게 End of file인 이유

콘솔창에는 아무것도 없다.

20221106_032700

  • 타이핑 안 치고 파일로 stdin으로 전달 가능
  • 이렇게 stderr하고 stdout이 분리 가능

fopen() 과는 다르다

  • 리디렉션 하면 파일을 열거나 파일 열라 하면 리디렉션 하는 경우가 있다.
  • 리디렉션은 fopen이 아니다!



커맨드 라인 인자

  • 커맨드 라인에서 프로그램 실행시 인자를 넣어주는 방법
  • 예: > filecopy.exe a txt b txt
  • 저렇게 들어온 인자들 main()함수의 매개변수에서 읽어올 수 있음.
int main(int argc, const char* argv[])
{

}
  • 여기서 argc는 들어온 인자의 수
    • 이 수는 실행한 파일의 이름까지 포함
      • filecopy.exe atxt b2.txt
    • 위의 경우 argc는 3이다
      • 첫 인자: 실행한 파일의 경로
      • 나머지 인자: 그 후 따라온 2개의 인자.
  • argv[]는 char포인터 배열
    • argv[ argc+1 ]로 생성됨
    • argv[ 0 ]: 첫번쨰 요소에슨 실행파일 이름이
    • argv[ 1 ]~argv[ argc-1 ]: 커맨드 라인 인자들이 순차적으로 들어옴
    • argv[ argc ]: null

20221106_115700

(const) char * argv[] 는 포인터 배열의 배열

  • 각 포인터는 그냥 C스타일 문자열 1 . 커맨드 라인에 들어온 값을 프로그램 실행 할 떄 만든 프로세스 메모리 어딘가에 저장하고, 2 . 그 주소들을 모아 argv[]배열에 넣어 보내주는 것
  • 물론 정확히 어떤 메모리에 들어가는 지는 OS따라 다르다.

const char* argv[]의 내부

20221106_121911



커맨드 라인 인자 메모리 뷰

20221106_123909

인자 하나씩 가져와서 메모리 복사

실제 메모리

20221106_124821

근데 이건 입출력 리디렉션과는 다르다

20221106_131233



중요하게 봐야 할 것

1 . 입력 2 . 파일 입출력 3 . 입출력 리디렉션 4 . 커맨드 라인 인자





© 2021.03. by yacho

Powered by github