Chapter 22. Input/Output

지환·2022년 3월 4일
0

I/O library는 C에서 가장 크고 가장 중요한 표준 라이브러리이다.

이 챕터에선 <stdio.h>의 8개 빼고 모든 함수를 다룬다.
안 다루는 함수 중 하나인 perror 함수는 <errno.h>헤더와 연관돼있어서 Section 24.2에서 다루고,
나머지 7개 함수(vfprintf,vprintf,vsprintf,vsnprintf,vfscanf,vscanf,vsscanf)는 va_list type과 연관돼있어서 Section 26.1에서 그것과 같이 다루겠다.

C89에선 모든 I/O 함수가 <stdio.h>에 있었지만, C99에선 그렇지 않다.
<wchar.h>에도 I/O함수가 있는데,
<stdio.h>의 함수를 byte input/output functions라고 하고,
<wchar.h>의 함수를 wide-character input/output functions라고 한다.


22.1 Streams

C에서 stream이라 함은, any source of input or any destination of output 이다.

예를들어 stream에는 keyboard, CD, DVD, flash memory, network port, printer 등이 있을 수 있다.
(여기선 대표적으로 file로 다룰건데, 다른 stream으로도 잘 작동한다.)

File Pointers

C에서 stream에 접근하기 위해선 File Pointer를 통해야 한다.(보통 file에만 접근하긴하지만 아래 standard stream처럼 file pointer 이용해서 keyboard나 screen에 접근하기도하네)
FILE *
(FILE type은 <stdio.h>에 정의돼있음)

특정 stream은 standard name을 가진 file pointer로 미리 표현돼있지만(아래 표 참고),
필요하다면 우리가 위 type을 이용해 추가적으로 file pointer를 만들어 stream에 접근할 수 있다.
FILE *fp1, *fp2;
주로 OS에서 한번에 열 수 있는 streams 수를 제한하긴하지만, FILE * variables은 얼마든지 만들 수 있다.

Standard Streams and Redirection

standard stream은 <stdio.h>에 정의돼 사용하기위한 준비가 다 돼있다. 그래서 선언하거나, 열거나 닫을 필요가 없다.

File PointerStreamDefault Meaning
stdinStandard inputKeyboard
stdoutStandard outputScreen
stderrStandard errorScreen

앞서 사용한 printf, scanf 같은 함수는 이런 standard streams으로부터 값을 입력받거나 출력했다.

Redirection
많은 OS에선 Standard stream의 Default meaning을 redirection을 통해 수정할 수 있게 해준다. (즉, stdout이 screen이 아니라 다른 곳으로 출력하게 만듦)
demo <in.dat >out.dat : 이렇게하면 input과 output의 default meaning이 해당 파일로 바뀜.

stderr
근데 이렇게 바꿔버리면, error 내용 또한 파일에 적어버릴 수도 있다.
그럼 file을 열어보기 전까진 error가 떴는지 알 수 없다.
stderr이 따로 필요한 이유이다. 에러가 뜨면, 얘의 default meaning은 screen이기 때문에 화면에 에러를 띄운다.

Text Files versus Binary Files

<stdio.h>는 두가지 종류의 file을 지원한다.
1. text file
2. binary file
이다.

  1. text file
    말 그대로 text로 나타내진 file이다. 이 파일의 byte들은 character를 나타낸다.
    따라서 사람이 접근해서 수정하거나 읽을 수 있다.
    ASCII 기반이라면 ASCII code표에 따라 data를 해당 code표에 맞게 convert해서 저장했다고 보면 된다.
    예시로는 C source code, 메모장에서 작성한 파일 등이 있다.

  2. binary file
    binary 즉 이진수를 날것 그대로 형식에 맞게 저장한 파일이다. 여기 byte들은 꼭 character일 필요가 없다.
    그냥 해당 데이터의 이진수 표현법에 맞게 그대로 적었다고 보면 된다. int면 int 모양대로, flaot이면 float 모양대로.. (text file도 ASCII 표현법에 맞게 적긴 한거임)
    예시로는 C 실행파일, 이미지파일 등이 있다.
    따라서 얘는 해당 데이터를 취급하는 특정 프로그램으로만 판독 가능하다.(이미지 파일이 binary로 저장된거를 text 편집기로 불러봐야 이상하게만 보임, 해당 data 유형에 맞게 봐야된다.)

Text File은 두가지 특징을 가진다.

  1. line으로 나뉘어져 있다.
    end-of-line marker(라인의 끝)는 OS마다 다르게 처리한다.
    Window는 carriage-return character('\x0d')와 line-feed character('\x0a')가 연달아 나오고,
    UNIX와 MAC OS는 하나의 line-feed character('\x0a')이다.
  2. "end-of-file" marker를 포함할 수도 있다.
    몇 OS는 text file의 끝을 나타내는 marker를 포함할 수도 있다.
    예를들어 Window는 '\x1a'(Ctrl-Z)가 end-of-file marker이다.(꼭 필요하진 않음)
    UNIX를 포함한 대부분의 OS는 특별한 end-of-file character가 없다.

Binary File은 line으로 구분돼 있지도 않고, end-of-line이나 end-of-file marker가 없다. 모든 bits가 동등하게 대해진다.
(text file은 사람이 보기 좋게 하려는게 있지만, 얜 있는 그대~로 저장돼서 그런듯)

파일을 읽거나 쓸때 그게 txt인지 binary인지 고려해야 한다.
file copying program은 text file이라고 가정할 수 없다. 왜냐하면 end-of-file character를 포함하는 binary file은 제대로 copy가 안될거기 때문이다.(그 이후로 copy안됨)
정확히 말하기 어려우면 그냥 binary라고 생각하는게 안전하다.

Q. Chapter 7 Q&A에 보면 C에선 '\n'를 무조건 line-feed로만 본다고 했었는데?    

C에서 그렇게 보이게 처리하는 것 뿐이다.
OS에 따라 어떻게 저장되든지 간에, carriage-return이 나오든 line-feed가 나오든 둘 다나오든,
C에선 그냥 하나의 line-feed character로 해석하고 처리한다.
file에서 읽을때도 마찬가지고, 반대로 파일에 쓸때도 해당 OS에 맞는 형태로 변환해서 쓴다.
이렇게 함으로써 우리는 end-of-line이 어떻게 생겼나 신경 안쓰고도 portability를 유지할 수 있다.
binary file은 carriage-return이든, line-feed든 신경 안쓰고 그대로 저장하니까 상관없다.

.

Q. text와 binary 둘의 차이가 뭘까? 왜 <stdio.h>는 둘을 구분할까?
둘 다 이진수로 저장되고, 해당 파일을 보려면 파일 형식에 맞는 프로그램을 써야되는건 똑같지않나?
text는 그냥 character로 변환되는게 다를 뿐이지,
이미지나 음성 파일도 결국 해당 형식에 맞게 변환(?)돼서 저장되는거 아닌가?
날 것 그대로 저장됐다고는 하는데 결국 읽어올때는 text가 ASCII 이용하듯,
mp3면 해당 프로그램 이용하고, jpg면 또 그거에 맞는 프로그램 이용해서 변환하는거 아닌가?

위에도 적혀있지만,
차이라고는 text file은 line을 나누는 것 말곤 없다.(거기에 EOF marker도 생각해주면 됨)
근데 그 line을 나누는 방법이 OS마다 다르기 때문에, text file이라고 따로 구분해줘야
end-of-line 처리를 자동으로 할 수 있다.
text file이라고 구분없이 그냥 character 형식에 맞게 변환만 해서 저장하면
end-of-line 때문에 OS마다 다르게 프로그램을 작성해야되고(누구는 LF, 누구는 CR..),
portability를 잃게 된다.

참고)
https://codedragon.tistory.com/5103
https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=tipsware&logNo=221353023593
https://stackoverflow.com/questions/51754913/why-binary-file-is-not-a-text-file-and-all-text-files-are-binary-files
https://stackoverflow.com/questions/6039050/difference-between-text-file-and-binary-file


22.2 File Operations

redirection만 사용하기엔 기능이 제한적이다.
따라서 File operation이 필요하다.

Opening a File

file을 stream으로 사용하려면 fopen함수를 이용해 열어야한다.
(위에서 말했듯이, stream에 접근하려면 FILE *이 필요함. 이건 stream 중 하나인 file에 접근하는 방법이다.)

fopen

FILE *fopen(const char * restrict filename,
			const char * restrict mode);

Parameter
첫번째 인자는 open하려는 file name이다.(file's location이 같이 있어도 OK)
두번째 인자는 그 파일에 수행하려는 operation을 알려주는 "mode string"이다.

둘 다 char * type이므로 string 이어야 한다는 거에 주의하자.
그리고 restrict가 쓰였으므로 두 인자는 다른 memory location을 가리켜야한다.

첫번째 인자에 path 넣을 때 `\` character는 주의해서 넣어야 된다.
include 할때와는 다르게, 얘는 string으로 인식하기 때문에 `\`는 escape sequence로 인식할 수 있다.
그래서 `\\`로 쓰거나, window의 경우 `\` 하나로 쓰면 된다.

Retrun
반환 type은 file pointer(FILE*)이다.
fp = fopen("in.dat", "r"); -> 주로 이렇게 사용한다.
파일을 열 수 없다면 null pointer를 반환하므로 이를 잘 체크해야 한다.
(파일이 없거나, path를 잘못 입력했거나, 권한이 없을때)

Modes
file open 할때 mode는
(1)어떤 operation을 할지
(2)어떤 file 종류를 열지(text/binary)
에 따라 달라진다.

TEXT FILE

StringMeaning
"r"Open for reading
"w"Open for writing (file need not exist)
"a"Open for appending (file need not exist)
"r+"Open for reading and writing, starting at beginning
"w+"Open for reading and writing (truncate if file exists)
"a+"Open for reading and writing (append if file exists)

BINARY FILE

StringMeaning
"rb"Open for reading
"wb"Open for writing (file need not exist)
"ab"Open for appending (file need not exist)
"r+b" or "rb+"Open for reading and writing, starting at beginning
"w+b" or "wb+"Open for reading and writing (truncate if file exists)
"a+b" or "ab+"Open for reading and writing (append if file exists)
writing은 overwrites하기 때문에 기존 내용이 사라짐.
appending은 뒤에 덧붙여 쓰기 때문에 기존 내용 보존됨.
"file need not exist"라는게, 보통은 그 파일이 없어도 그냥 그 파일을 만들어 버리는 듯

+를 이용해서 reading and writing 모드로 열면 특별한 규칙이 적용된다.
reading에서 writing으로 바꾸고 싶으면,
file-positioning function을 호출하거나,
reading operation이 end-of-file을 만나야한다.
writing에서 reading으로 바꾸고 싶으면,
file-positioning function을 호출하거나,
fflush를 호출해야 한다.

Q. text mode로 여는거랑 binary mode로 여는거랑 정확히 무슨 차이가 있지?
실제로 code 짜보면 binary mode로 열어도 일반 string들 잘 변환돼서 들어감.

위에서 말했듯이 둘의 차이는 end-of-line 처리하는게 대부분인 것 같다.
binary mode로 열어도, 뭐 'a' 이걸 어떻게 저장하겠어, 어쩔 수 없이 ASCII로 변환해서 저장하겠지
근데 이제 line 끝 처리가 binary mode면 그대로 저장되고,
text mode면 OS에 맞게 변환돼서 저장된다 는 차이가 있을 것이다.
밑에 링크타고 보면 UNIX나 LINUX는 사실상 binary mode란게 의미가 없음
(C처럼 Line Feed만 인정해서 그런듯)

위 질문 참고) https://stackoverflow.com/questions/43777913/the-difference-in-file-access-mode-w-and-wb
(두번째 답변까지 읽어보면 좋음. POSIX 따르는 system에선 딱히 의미도 없네..)

Closing a File

fclose

int fclose(FILE *stream);

Parameter
인자는 fopen이나 freopen에서 반환받은 file pointer여야 한다.

Return
성공적으로 closed되면 0을 return하고,
실패하면, EOF를 return한다.(<stdio.h>에 정의된 macro)

Attaching a File to an Open Stream

freopen

FILE *freopen(const char * restrict filename,
			  const char * restrict mode,
              FILE * restrict stream);

이미 열려있는 stream에 다른 파일을 붙이는 것이다.
즉, 세번째 인자로 들어온 file pointer가 다른 stream(파일)을 가리키게 바꾼다. (해당 file pointer가 가리키던 stream은 close)

주로 standard streams을 바꾸는데 사용한다.
(마치 redirection처럼) stdouta.txt가 되게 만들면, 그냥 printf로 작성해도 해당 파일에 작성된다.

Return
성공하면, 세번째 인자를 return한다.
새로운 파일을 여는데 실패하면 null pointer를 반환한다.
(이전 파일을 닫는건 실패해도 무시한다.)

filenamenull pointer면?
그러면 명시된 mode로 mode를 바꾸려고 시도한다.
Implementation에 따라 꼭 지원할 필요는 없고, 지원한다면 모드를 바꿀 수 있는 상황이 따로 있을 것이다.

Obtaining File Names from the Command Line

flexibility를 위해 파일 이름을 사용자로부터 입력받고 싶다. code내에 적으니 좀 그렇다.
근데 이게 프로그램 실행시키고 입력받자니 string 길이도 그렇고 좀 이상하다.

그래서 Section 13.7에서 다뤘던 command-line argument를 이용하는게 BEST다.
(-D option으로 macro 정의하는 것도 괜찮지않나..(chapter 15))

예시
demo names.dat dates.dat
이라는 명령어가 들어왔으면
argv[1], argv[2]를 이용해 file name 추출할 수 있음.

Temporary Files

실제 프로그램에선 temporary file(프로그램 실행 중에만 잠깐 존재하는 임시 파일)이 자주 필요하다.
예를들어 compiler도 source code를 object code로 변환하는 중간단계에선 이런 임시 파일을 이용해 잠깐 저장해둔다.

tempfile

FILE *tempfile(void);

파일이 닫히거나, 프로그램이 끝날때까지만 존재하는 임시 파일을 만든다.("wb+" mode로 연다.)

Return
file에 접근 할 수 있는 file pointer를 반환한다.
파일을 만드는데 실패하면 null pointer를 반환한다.

tempfile의 단점이 있는데, (1)파일 이름을 모른다는 것, (2)나중에 그 파일을 저장하고 싶어도 방법이 없다는 것이다.
따라서 대안으로 fopen을 이용할 수 있다.(fopen mode 몇개는 "file need not exist"라고 그냥 만들어 버리는 듯)
근데 이전에 존재하던 파일과 이름이 같아선 안되므로 tmpnam함수가 필요하다.

tmpnam

char *tmpnam(char *s);

Parameter
(1) NULL pointer

char *filename;
filename = tmpnam(NULL);
//null pointer가 parameter로 오면 static 변수에 file이름을 저장해서 반환한다.

(2) character array

char filename[L_tmpnam];
tmpnam(filename);
//이 경우 file 이름을 넘겨받은 character array에 copy한다.
//이 경우 반환값은 first character의 pointer이다.

L_tmpnam macro<stdio.h>에 정의돼있다. 임시 파일 이름이 얼마나 길지 정의해둔 macro이다. 따라서 tmpnam에 argument를 넘겨줄때 array가 최소 L_tmpnam만큼의 길이는 돼야한다.

TMP_MAX macro<stdio.h>에 정의돼있는데, 이는 프로그램 실행중에 tmpnam에 의해 만들어질 수 있는 최대 임시파일 이름 개수이다. 그래서 tmpnam도 너무많이 부르는건 삼가야된다.

Return
file name을 만드는데 실패하면 NULL pointer를 반환한다.

File Buffering

data를 disk drive에 읽고/쓰고 하는건 상대적으로 slow operation이다. 그래서 매번 프로그램이 disk file에 접근하는건 딱히 실현가능하지않다.

해결 방법은 바로 buffering을 이용하는 것이다.
stream에 작성되는 데이터는 사실 memory의 buffer라는 공간에 저장된다.
(1)buffer가 꽉차거나 (2)stream이 닫히면, "flushed"된다.(== 실제 output device에 작성된다.)

input에서도 비슷한 과정으로 진행된다. 이는 매우 효율적이다. 실제 disk에 byte단위로 조금씩 읽고 쓰는 것보단 buffer에 읽고 쓰는게 훨씬 빠르고, 실제 disk에 접근하더라도 buffer가 블럭단위로 크게크게 읽고 쓰기 때문이다.

<stdio.h>가 이익이 된다고 판단하면 buffering을 알아서 진행하기 때문에, 대게 우리가 신경쓸 필요는 없다.
특수한 경우에서만 신경써주면 된다.

fflush

프로그램이 output을 file에 작성할때, buffer에 저장됐다가 자동으로 flushed된다.(꽉차거나 file이 닫히면)
하지만 fflush 호출을 통해서 필요할때 flush할 수 있다.

int fflush(FILE *stream);

Parameter
일반적인 output stream이 오면 그냥 해당 output stream의 buffer를 flush하고,
NULL(null pointr)가 오면 모든 output stream을 flush한다.

Return
성공시 0, 실패시 EOF 반환

말했듯이, 얘는 output에 대한 함수다.
따라서 많이들 실수하는 fflush(stdin);은 UB이다.

여기서 말하는 output은,
(a) output을 위해 열렸거나 or (b) update를 위해 열렸는데 마지막 operation이 read가 아닌 경우를 말한다.
즉 'NULL pointer`를 인자로 넣으면 (a)와 (b) 모두 flush.

setvbuf

int setvbuf(FILE * restrict stream,
			 char * restrict buf,
             int mode, size_t size);

특정 stream의 버퍼의 위치, 크기, mode, size를 조정하는 함수이다.

Parameter
첫번째 인자는 당연히 어떤 stream의 버퍼를 변경하고싶은지, 그 stream을 넘겨준다. 주의할 점은 해당 stream이 열려있어야하고, 그 stream에 다른 operation이 실행되기 전이어야한다.
두번째 인자는 원하는 buffer의 address이다. automatic이든 static이든 dynamically allocated memory이든 상관없다. (automatic이면 block이 끝날때 buffer 사라짐.) dynamically allocated memory는 다 쓰고 해제해줘야된다.
이게 NULL pointer면 명시된 size의 buffer를 만든다.
세번째 인자는 buffering의 종류를 정하는 인자다. 다음 세 macro중에 하나를 넣어야한다.
(1) _IOFBF(full buffering) : buffer가 비었을때 read하고, 꽉찼을때 write한다.
(2) _IOLBF(line buffering) : read/write를 한 line을 기준으로 한다.
(3) _IONBF(no buffering) : buffer 없이 바로 read/write
interactive device와 연결되지 않은 경우 "full buffering"이 default다.
네번째 인자는 buffer의 byte 수이다.

Return
성공시 0, 실패시 (혹은 mode argument가 invalid면) non zero 반환

setbuf

void setbuf(FILE * restrict stream,
			char * restrict buf);

한물간 함수다.
(void) setvbuf(stream, buf, _IOFBF, BUFSIZ);
와 효과가 같다.
여기서 BUFSIZ<stdio.h>에 정의된 macro이다.

두번째 인자가 NULL이라면,
(void) setvbuf(stream, NULL, _IOFBF, 0);
과 효과가 같다.

setvbuf든 setbuf든, steram을 닫고 buffer를 닫아야 한다.
만약 buffer가 automatic storage duration이면 함수 종료전에 stream 닫아야 한다.

Miscellaneous File Operations

아래 두 함수는 다른 함수와 다르게 File pointer가 아니라 File name을 parameter로 받는다.
둘 다 return값은 성공시 0, 실패시 nonzero를 반환한다.

remove

int remove(const char *filename);

parameter로 넘겨받은 이름의 파일을 삭제하는 함수이다.
tmpfile이 아닌 fopen으로 임시 파일을 만들었을 경우 이 함수로 삭제하면 유용하다.
삭제 전에 해당 파일을 close해야한다. (열려있는 파일에 적용하면 Implementation-defined)

rename

int rename(const char *old, const char *new);

fopen으로 만든 임시 파일을 계속 저장하려고 결정한 뒤, 파일 이름 바꿀때 쓰면 유용하다.(처음에는 대게 tmpnam으로 이름 받아서 쓰니까 그런듯)
new name이 이미 있으면 Implementation-defined
(얘도 파일 close 후에 rename 해야한다.)


22.3 Formatted I/O

"Format string"을 사용하는 library function에 대해 알아보겠다.
이 함수들은 output일때 numeric form의 data를 character form으로, input일때 character form의 data를 numeric form로 바꾸는 능력이 있다.
다른 I/O function은 그런 능력이 없다.

Q. 바로 위 마지막 줄 뭔 소리지?
다른 I/O 함수들도 'a' 들어오면 97로 바꿔서 저장하는건 같지 않나?

format string과 conversion specificion을 보면 뭔 소린지 알 수 있다.
형태 있는 그대로 바꾸는걸 의미한다.
printf("%d", a); //int a = 5;
a는 character가 아닌 int, 즉 숫자이지만, 결국 "5" 문자로 바뀐다.
즉, "있는 그대로" 바꾼다.

scanf("%d", &a);
여기 '5'를 입력하면 이는 사실상 문자지만, a에는 5라는 숫자가 저장된다.
이런 능력은 format string과 conversion specification이 있는 함수에만 있다.

vfprintf,vprintf,vsprintf,vsnprintf,vfscanf,vscanf,vsscanf 는 Section 26.1에서 다룬다.

The ...printf Functions

int fprintf(FILE * restrict stream,
			const char * restrict format, ...);

int printf(const char * restrict format, ...);

output의 appearance를 "format string" 을 이용해 조정한다.
fprintf는 output stream을 지정할 수 있지만,
printf는 무조건 stdout이 output stream이다.
fprintfstderr에 error message 작성할때 주로 사용한다.(user가 stdout redirect해도 screen에 뜨도록 보장해준다.)

Paremeter
...으로 표현된건 ellipsis이다.
(자세한건 26.1, 앞에 macro 함수 ellipsis는 나온적 있음)

Return
성공 : number of characters written 반환
error : negative value 반환

...printf Conversion Specifications

Format string 내의
(1) ordinary characters는 그냥 그대로 print,
(2) conversion specifications은 남은 arguments를 어떻게 character form을 바꿀지 알려준다.

Conversion Specification
%<flag><minimum field width><precision><length modifier><conversion specifier>

Chapter 3와 Chapter 7에 나온 관련 내용들만 일단 익혀두고,
저 안에 자세한게 궁금하거나 찾아봐야될 것 같으면 책 찾아 보는걸로 하자.
여기다 다 옮겨적기도 좀 그릏네 양이 참
https://ziegler.tistory.com/88
여기 잘 정리돼있네 이거봐도 되고ㅎㅎ

알아둘만한 것:
(1) C99부턴 a, e, f, g (A, E, F, G)가 "infinity"와 "NaN"을 출력할 수 있다.
IEEE754에 따르면 inifinity, negative infinity, NaN("Not a Number")이 결과로 나올 수 있다.
(1.0/0.0 == infinity, -1.0/0.0 == negative infinity, 0.0/0.0 == NaN)

display되는 형태에는 inf, infinity, -inf, -infinity, nan, -nan이 있다.(A, E, F, G면 대문자로 나옴)

(2) * character를 사용하는 방법
printf("%*.4d", 6, i);
라고 하면 6*자리에 들어간다.
주로 macro를 이용해서 width나 precision을 명시할때 사용한다.

(3) 가장 안쓰이는 두개 %p, %n
%p는 pointer의 value를 print하는데 standard에는 어떤 형식으로 표시할지 명시 안돼있다. 8진수나 16진수일 것.
그리고 얘는 void * value를 printable form으로 바꾸기 때문에 (void *) casting 해주자.

%nprintf("%d%n\n", 123, &len); 처럼 쓴다.
이러면 len3이 저장되고, %n은 출력은 안된다.
즉, %n 앞에까지 출력된 문자 수를 저장하는 것이다.

(4) s conversion specifier은 명시된 precision이 꽉차거나 null character를 만나면 writing stop.

The ...scanf Functions

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

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

input의 layout을 format string을 이용해 나타낸다.
output의 appearance를 "format string" 을 이용해 조정한다.
input item들은 conversion specification에 의해 변환되어 object에 저장된다.

fscanf는 input stream을 지정할 수 있지만,
scanf는 무조건 stdin이 input stream이다.

Return
아무 data item도 읽지 않았는데 input이 실패하면, EOF 반환

아래 3가지 경우면 미리 반환
1. input failure : 더 이상 input character를 읽을 수 없을때
2. matching failure : format string과 input character가 맞지 않을때
3. endcoding error : multibyte character를 읽으려고 시도했으나 valid multibyte character가 아닐때
이 경우 함수는 진행되다가 return 한다.
반환값은 읽혀서 assgin된 data item의 개수이다.
(끝까지 읽는데 성공해도 위 값 반환)

malloc 반환값 확인하듯이 ...scanf도 반환값 확인하며 제대로 입력됐는지 확인해줘야된다.
https://stackoverflow.com/questions/33071080/is-it-possible-there-will-be-an-encoding-error-when-reading-a-character

...scanf Format Strings

"format string"이 pattern matching의 역할을 한다.
맞지 않는 character는 다음 reading을 위해 push back된다.

...scanf의 format string은 아래 3가지 요소로 구성된다.

  1. Conversion specifications
    : %[, %c, %n을 제외한 모든 conversion specifications은 input item 처음에 나오는 white-space character들을 skip한다.
    trailing white-space character는 skip하지 않는다.

  2. White-space characters
    : format string에서 하나 이상의 white-space character는 input stream의 0개 이상의 white-space character와 match된다.

  3. Non-white-space characters
    : non-white-space character는 input stream의 같은 character와 match된다.

...scanf Conversion Specifications

...printf보단 좀 더 간단하다.

conversion specification
%<*><Maximum field width><Length modifier><Conversion specifier>

얘도 Chapter 3랑 Chapter 7에 있는 내용들만 알고있고 나머진 책 찾아보자.

알아둘만한 것 :
(1) conversion specification에 처음 붙는 *assignment suppression이다. input item이 읽히지만, object에 assign은 안된다. 따라서 return 값에도 얘는 포함되지 않는다.

(2) c conversion specifier는 Length modifier와 함께 쓰여서 %5c 이런식으로도 쓰일 수 있다. 이러면 대신 argument가 pointer to character array거나 해야한다.

(3) %s는 sequence of non-white space character와 매치되고, 마지막에 null character를 추가한다.

(4) %[는 nonempty sequence of characters from "scanset"과 매치되고, 마지막에 null character를 추가한다.
scanset은 %[set]의 형태나 %[^set]의 형태가 가능하다.

(5) ...scanf의 conversion specifier은 <stdlib.h>의 numeric conversion function과 매우 연관성이 높다.
예를들어 <stdlib.h>strtol 함수는 d specifier처럼 문자 "297"을 숫자 297로 바꾼다.
(해당 함수와 conversion specifier 관계는 p.562 표에 정리돼있음)

(6) infinityNaN을 읽을 수 있다. 대신, ...printf 함수가 출력하는 것과 같은 형태로 입력해야 한다.(case는 상관없다.)

(7) wide character를 지원한다. multibyte character를 읽어서 storage에 wide character로 변환해 저장한다.
(ex. %lc, %ls, %l[set])

Dectecting End-of-File and Error Conditions

...scanf의 return값이 우리가 예상한 값보다 작다면 세가지 가능성이 있다.(아예 안읽혀서EOF return된 경우 제외)

  1. End-of-file : format string이 다 match되기전에 end-of-file을 만난 경우 (input failure)
  2. Read error : stream으로부터 character를 읽어올 수 없는 경우 (encoding failure)
  3. Matching failure : data item이 wrong format인 경우 (matching failure)

모든 stream은 두가지 indicators를 갖는다.
(1)error indicator와 (2)end-of-file indicator이다. (각 indicators는 stream이 열릴때 clear된다.)

end-of-file indicatorEnd-of-file을 만났을 때 set되고,
error indiicatorread error가 일어났을 때 set된다.
matching failure는 아무 변화가 없다.

clearerr

void clearerr(FILE *stream);

해당 stream의 indicator를 모두 clear한다.(End-of-file, error)

feof & ferror

int feof(FILE *stream);

int ferror(FILE *stream);

Retrun
end-of-file indicator가 set돼있다면 feof의 값은 nonzero value이다.
아니라면, zero이다.

error indicator가 set돼있다면 ferror의 값은 nonzero value이다.
아니라면, zero이다.

둘 다 zero를 return한다면 matching failure이 발생했을 것이다.(물론 ...scanf return값이 예상값보다 작은 경우)

이를 활용한 프로그램이 궁금하면 p.565

fscanf(fp, "%*[^\n]"); 로 다음 \n까지 남은 line을 모두 skip할 수 있다.

feof는 end of file을 탐지하는 것이 아니라, 그것이 일어난 후 확인하는 것이다. 따라서 end-of-file까지 읽어야 feof를 통해 확인할 수 있다.

22.4 Character I/O

이 Section의 함수들은 text stream이든 binary stream이든 잘 작동한다.
그리고 여기 함수들은 character를 char가 아니라 int로 본다.
(이유 중 하나는 negative integer인 EOF 처리 때문에 그렇다.)

Q. text stream에서 character 단위(1byte)로 읽어오는 이런 함수를 써도 end of line처리가 잘 되나?

yes. window에서 test해본 결과 알아서 잘 변환한다. 즉 binary mode로 "\r\n"이라고 써둔 뒤,
text mode로 character 하나만 읽어와도 "\n"으로 잘 변환돼서 읽힌다. OS에서 뭐 어떻게 변환해주는건가..

Output Functions (Character I/O)

putchar & fputc & putc

int putchar(int c);

int fputc(int c, FILE *stream);

int putc(int c, FILE *stream);

putcharstdout stream에 character 하나 작성
#define putchar(c) putc((c), stdout) 주로 이렇게 구현됨.

fputcputc는 지정된 stream에 character 하나 작성

Return
함수 모두 다 write error가 발생하면 error indicator를 set하고, EOF를 return한다.
아니면, 작성된 character를 return한다.

fputcputc의 차이
둘의 차이는, putc는 주로 함수와 Macro로 implement된다는 것이다.
fputc는 주로 함수로만 implement된다.
보통 빠른 속도때문에 putc를 선호하지만, fputc가 대안이 될 수 있다.
(macro는 argument가 한번 이상 계산될 수 있다는 문제가 있음.)

Input Functions (Character I/O)

getchar & fgetc & getc

int getchar(void);

int fgetc(FILE *stream);

int getc(FILE *stream);

getcharstdin stream으로부터 character 하나 읽어온다.
#define putchar(c) putc((c), stdout) 주로 이렇게 구현됨.

fgetcgetc는 지정된 stream으로부터 character 하나 읽어온다.

Return
함수 모두 다 read error가 발생하면 error indicator를 set하고, EOF를 return한다.
end-of-file에선 end-of-file indicator를 set하고, EOF를 return한다.
("At end-of-file" 이라고만 돼있으니 EOF marker가 없어도 끝이면 되지 싶다.)
아니면, 읽히는 문자를 return한다.

fgetcgetc의 차이
둘의 차이는, getc는 주로 함수와 Macro로 implement된다는 것이다.
fgetc는 주로 함수로만 implement된다.
보통 빠른 속도때문에 getc를 선호하지만, fgetc가 대안이 될 수 있다.
(macro는 argument가 한번 이상 계산될 수 있다는 문제가 있음.)

idom

while ((ch = getc(fp)) != EOF) {  //ch는 int여야 한다.
	~
}

ungetc

int ungetc(int c, FILE *stream);

stream에서 읽은 character를 다시 (buffer에) 집어넣는다. (다른 character를 다시 넣어도 됨)
그래서 그걸 다시 읽어오면 push back한 character가 나온다.
그리고 end-of-file indicator를 초기화한다.

첫번째 call은 성공이 보장되지만, 연속적인 호출은 성공여부가 implementation과 stream 종류에 따라 다르다.

file-positioning function을 호출하면 pushed back한 character는 모두 사라진다.

Return
push back한 character를 return한다.
implementation이 허용하는 연속 call 횟수보다 더 많이 call하거나 EOF를 push back 하려고 할 경우 EOF를 retrun한다.

Copying a File

fcopy f1.c f2.c -> 이렇게 command 입력하면 s1이 s2에 복사되는 프로그램

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
	FILE *source_fp, *dest_fp;
    int ch;
    
    if (argc != 3) {
    	fpirntf(stderr, "usage: fcopy source dest\n");
        exit(EXIT_FAILURE);
    }
    
    if ((source_fp = fopen(argv[1], "rb")) == NULL) {
    	fprintf(stderr, "Can't open %s\n", argv[1]);
        fclose(source_fp);
        exit(EXIT_FAILURE);
    }
    
    if ((dest_fp = fopen(argv[2], "wb")) == NULL) {
    	fprintf(stderr, "Can't open %s\n", argv[2]);
        fclose(dest_fp);
        exit(EXIT_FAILURE);
    }
    
    while ((ch = getc(source_fp)) != EOF)
    	putc(ch, dest_fp);
        
    fclose(source_fp);
    fclose(dest_fp);
    return 0;
}

"rb" 모드와 "wb"모드를 사용해서 text와 binary 중 어떤게 와도 복사 되도록 한다.
(text mode로 열어버리면 end-of-file marker가 중간에 오는 binary file은 복사가 제대로 안됨.)


22.5 Line I/O

lines을 읽고 쓰는 함수
binary stream에서도 legal이긴 하지만, 주로 text stream에서 사용한다.

함수들을 보면 output 함수는 끝에 new line character 추가하기도 하고, input 함수는 null character 추가하기도 하는데
이걸 binary stream에서 쓰기 좀 그릏긴하지

Output Functions (Line I/O)

puts & fputs

int puts(const char *s);

int fputs(const char * restrict s,
		  FILE * restrict stream);

putsstdout에 write하고, 항상 new-line character를 끝에 추가한다.

fputs는 지정된 stream에 write하고, new-line character는 적힌게 없으면 추가하지 않는다.

Return
error 발생하면 EOF를 반환한다.
아니면, nonnegative number를 반환한다.

Input Functions (Line I/O)

gets & fgets

char *gets(char *s);

char *fgets(char * restrict s, int n,
			FILE * restrict stream);

getsstdin으로부터 new-line character를 만날때까지 읽고 저장한다. 마지막에 읽는 new-line character는 버린다. 그리고 마지막에 \0을 저장한다.
(%s랑 다르게 white space는 상관없다.)

fgets는 지정된 stream으로부터 (1)new-line character를 만날때까지 or (2)n-1개의 character만큼 저장될때까지 읽고 저장한다. 그리고 마지막에 \0을 저장한다.
(gets보다 좀 더 안전, 특별한 경우 아니면 쟤는 쓰지마라)

Return
성공시 둘 다 첫번째 argument를 반환한다.
read error가 발생하거나, end of input stream에 도달하면 NULL pointer를 반환하고, 각 indicator를 set한다.


22.6 Block I/O

한번에 많은 block of data를 읽거나 쓸 수 있게하는 함수이다.
주로 binary stream에서 쓰인다. text stream에서도 주의해서 다루면 사용 가능하다.
(text로 읽어오면 newline이 변환될 수 있어서 크기가 달라질 수 있다. data의 track을 잃을 수 있다.)

fread

size_t fread(void * restrict ptr,
			 size_t size, size_t nmemb,
             FILE * restrict stream);

Parameter
첫번째 인자는 array's address
두번째 인자는 각 array element의 size(in bytes)
세번째 인자는 읽어올 element의 수
네번째 인자는 읽어올 file pointer
ex) n = fread(a, sizeof(a[0]), sizeof(a) / sizeof(a[0]), fp);

Return
실제로 읽어온 array elements의 개수 (bytes 아님)
성공했다면 세번째 argument와 같은 수가 나오겠지만,
중간에 파일 끝까지 가거나 read error가 발생하면 더 작은 수가 나온다.
그리고 각 indicator를 set한다.

fwrite

size_t fwrite(const void * restrict ptr,
			  size_t size, size_t nmemb,
              FILE * restrict stream);

Parameter
첫번째 인자는 array's address
두번째 인자는 각 array element의 size(in bytes)
세번째 인자는 작성할 element의 수
네번째 인자는 작성할 file pointer
ex) fwrite(a, sizeof(a[0]), sizeof(a) / sizeof(a[0]), fp);

Return
실제로 작성된 array elements의 개수 (bytes 아님)
성공했다면 세번째 argument와 같은 수가 나오겠지만,
중간에 write error 발생하면 더 작은 수가 나온다.

두 함수 다 꼭 data가 array가 아니어도 사용할 수 있다. (예를들어 structure)
대신 fwrite로 pointer value가 들어있는 structure를 적을때 조심해야된다.
다시 읽을때 그 값이 valid할지 보장할 수 없다.
(주소 달라졌을수도 있으니까 하는 말인듯)

22.7 File Positioning

모든 stream은 각각의 file position을 가지고 있다.
file이 열릴때 (append mode가 아니라면) 초기 file position은 파일 시작점이다. read or write operation이 수행되면 file position은 자동으로 나아간다.

비록 이런 자동화가 잘 이루어져있긴하지만, file position을 수동으로 바꿔야 할 때가 있다. 예를들어 특정 기록으로 넘어가고 싶을때..
그래서 아래 5개 함수가 이를 가능하게 한다.

text stream에서도 가능하긴하지만, 주로 binary stream에서 쓰인다. text stream에서 사용한다면 OS가 다를 수 있으므로 주의해서 사용해야 한다.

fseek

file position 위치를 변경해주는 함수이다.

int fseek(FILE *stream, long int offset, int whence);

end-of-file indicator clear한다.(Q&A)

Parameter
1번째 인자는 file position을 바꿀 stream을 지정해준다.
2번째 인자는, 3번째 인자를 기준으로 얼마만큼 이동할지를 byte 단위로 기술한다.(long int type인 점을 생각해주면 좋음)
3번째 인자는, 어떤 곳을 기준으로 2번째 인자가 계산할지를 정한다.
아래 3개 macro 중에 고르면 된다.
SEEK_SET : Beginning of file
SEEK_CUR : Current file position
SEEK_END : End of file

ex) fseek(fp, -10L, SEEK_CUR); : 현재 지점으로부터 10 bytes 뒤로 이동

Return
성공시 : 0 반환
실패시(ex. 찾는 위치가 없음) : nonzero 반환

text stream 에서 쓸때 주의할 점)
(1)offset(2번째 인자)이 0여야 "하거나"
(2)2번째인자가 ftell함수에서 얻은 값이고, 3번째 인자가 SEEK_SET이어야 한다.
즉, text stream에선 (1)처음, (1)끝, (2)이전에 지나온 곳 으로 밖에 이동을 못한다.

binary stream에서 쓸때 주의할 점)
3번째 인자가 SEEK_END인 건 지원하지 않을 수도 있다.

ftell

long int ftell(FILE *stream);

현재 file position을 long int 값으로 반환해주는 함수이다.
저장해뒀다가 예전 file position으로 돌아갈 수 있다.

Return
성공하면 당연히 해당 file 위치 반환하고,
실패하면 -1L을 반환하고, errno에 error code를 저장한다.

주의할 점)
binary stream에서 반환값은 byte count된 값이지만,
text stream에선 아닐수도 있다.(end of line 때문에 애매하긴하네)
그래서 ftell의 반환값에 arithmetic operation을 하는건 그닥 추천하지 않는다.
(예를들어 두값을 빼서 file position 얼마나 떨어졌나 본다던가.. 하는건 비추천)

rewind

void rewind(FILE *stream);

file position을 처음으로 세팅한다.
fseek(fp, 0L,SEEK_SET);과 비슷하다.
차이는, rewind는 return값이 없고, 인자로 받은 stream의 두 indicator 모두 clear한다.(Q&A)

fgetpos & fsetpos

int fgetpos(FILE * restrict stream, fpos_t * restrict pos);

int fsetpos(FILE *stream, const fpos_t *pos);

fseekftell의 단점은, long int로 표현될 수 있는 position에서만 가능하단 것이다.
그래서 더 큰 파일을 다루기 위해, 위 두 함수는 fpos_t type을 사용한다.(해당 type은 int 일수도 있고, structure일 수도 있음)
fsetposend-of-file indicator clear한다.(Q&A)

Parameter
둘 다 두번째 인자가 주소값임에 주의하자.
fsetpos의 두번째 인자는 무조건 fgetpos로부터 받아온 값이어야한다.

Return
성공시 : 0
실패시 : nonzero, errno에 error code 저장


22.8 String I/O

여기 함수들은 stream과 file과는 관계가 없다.
string을 마치 stream인 것처럼 사용해서 데이터를 읽고 쓴다.
즉, stream이 아닌 string에 data를 작성하고, 읽어온다.
string을 대상으로 printf의 formatting 능력과 scanf의 pattern matching 능력을 사용할 수 있다.

Output Functions (String I/O)

sprintf

int sprintf(char * restrict s, const char * restrict format, ...);

첫번째 argument가 가리키는 character array에 write한다는 점 말고는 printffprintf와 비슷하다.
두번째 argument는 format string이다.
마지막에 \0을 추가한다.

Return
저장된 characters 개수를 반환한다.(\0 제외)
encoding error가 발생하면, negative number를 반환한다.(wide character가 유효한 multibyte character로 바뀌지 않을 수 있다.)

snprintf

int snprintf(char * restrict s, size_t n, const char * restrict format, ...);

최대 n-1개의 character만 쓰일 수 있다.(마지막엔 \0)(format string 내의 character도 포함해서 센다.)

차이는 이 함수의 경우, Return 값으로 길이 제한이 없었으면 작성됐을 길이를 반환한다.(\0 제외)
(encoding error시에 negative number 반환하는건 같다.)

Input functions (String I/O)

int sscanf(const char * restrict s, const char * restrict format, ...);

첫번째 argument가 가리키는 character array에 read한다는 점 말고는 scanffscanf와 비슷하다.
두번째 argument는 format string이다.
다른 애들과 달리 input을 필요한만큼 여러번 test해볼 수 있다는 장점이 있다.

Return
성공적으로 읽혀서 저장된 data 개수 반환 (끝까지 못읽어도 이 값 반환됨, like scanf)
아무 item도 읽지 않았는데 end of string(\0) 도달시 EOF반환


Q&A

redirection 하면, command에 있는 file name도 command-line arguments에 포함되나?
No, demo foo <in_file bar >out_file baz 라고하면, demo, foo, bar, baz만 남는다.

end-of-line marker가 OS마다 다르다고?
다르다. 하지만 C library 함수가 하나의 new line character처럼 보이게 해준다.
getc도 마찬가지다. 어떤게 end-of-line marker여도 잘 변환해준다.(반대도 마찬가지)
하지만, binary stream이면 변환같은거 없이 그냥 그대로 취급한다.

다른 프로그램에서 불러오려고 data 저장하려는데, binary가 낫나 text가 낫나?
text로 시작하면 그게 그거지만, 숫자가 포함되면 장단점이 있다.
binary의 장점으로는 숫자가 포함됐을때 더 빨리 읽고 쓸 수 있다, 그리고 공간도 절약된다.
단점으로는 사람이 읽기 어려워 debugging하기 곤란하다.
그리고 OS마다 data 저장하는 방식이 달라 대게 portable하지 않다.
(누구는 int가 2byte, 누구는 4byte.. 누구는 big-endian, 누구는 little-endian..)

UNIX에선 파일열때 b를 안쓰던데?
UNIX에선 text file과 binary file이 정확히 같은 format을 가진다.
그래서 딱히 사용할 필요가 없지만, 다른 OS와의 호환을 위해 필요하기도하다.

fclose쓰란거지? 어차피 프로그램 종료될때 다 닫히잖아?
abort 함수로 종료하면 아니다.
그게 아니더라도 써야할 이유가 있는데,
(1) open files의 개수를 줄여준다.
<stdio.h>FOPEN_MAX macro는 implementation이 동시에 열 수 있도록 보장하는 최소 file 개수가 명시돼있다.
즉, 한번에 열 수 있는 파일 최대치가 있다.
(2) 프로그램을 이해하고 수정하기 쉽게 해준다.
(3) safety를 위해 그렇다. close했단건 file이 잘 update됐단 뜻이다.
만약 나중에 program에 crash가 생겨도 file엔 영향이 없다.

file name 입력받고 싶은데 길이는 얼마나 해야되지?
FILENAME_MAX macro(in <stdio.h>)를 이용하면 된다.
이는 implementation이 열 수 있도록 보장하는 최대 filename 길이이다.

...printf...scanf format string이 변수여도되나?
당연!

while (--argc > 0)
	printf((argc > 1) ? "%s " : "%s", *++argv);

심지어 이렇게 해도됨.(argv print하는 loop)

왜 character input 함수는 저렇게 많지? putcgetc에 이미 함수버전과 macro 버전 둘 다 있는데 뭐하러 fputcfgetc가 필요한거지?
Historical reasons때문이다.
표준화 전에는 parameterized macro를 true function으로 back up해야한단 rule이 없어서,
원래는
putcgetc는 parameterized macro로만 구현됐었고,
fputcfgetc는 function으로만 구현됐었다.

input 남은거 skip해버리고 싶은데?
1) 다음 new line까지 skip (하고 newline character는 버림)

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

2) 다음 new line까지 skip (하고 newline character는 unread 상태로 놔둠)

scanf("%*[^\n]");

fflush(stdin);은 쓰면 안된다. output stream에 대한 함수이므로 UB

fseek, ftell ??
얘네가 먼저 있었는데, long int 표현범위를 넘어가는 byte 수를 가진 file은 처리할 수가 없어서 fsetposfgetpos가 추가되었다.
implementation이 64bits long int를 지원한다면, fseek, ftell 로도 대부분 충분하다.

cursor 움직이고, character 색 변경하는 screen control은 왜 다루지 않나?
screnn control에 대한 표준 함수는 없다.

0개의 댓글