파일 입출력에 앞서 컴퓨터의 입출력 경로들을 알아보고 가자.
1. 콘솔 입출력 : 말 그대로 콘솔(검은 화면에 하얀 글씨가 기본이다. 어떤것인지 모르겠다면 시작버튼을 누르고 cmd라고 치면 나오는 명령 프롬프트를 켜보자. 이게 콘솔이다.)을 통한 입력과 출력이다. 여태 우리는 cin과 cout으로 이러한 입출력방식을 사용하였다.
2. 파일 입출력: 또 말 그대로 파일을 통한 입력과 출력이다. 각종 게임의 세이브,로드 등은 보통 파일을 통한 입출력이고 이 파일은 이미지,한글,텍스트 등 모든 파일을 의미한다.
3. 소켓 입출력: 소위 tcp/ip를 이용한 네트워크 통신 방식이다. 나는 이것을 배우지 않을것이며 이 블로그에 올라올 일 없다.
입출력 스트림
스트림이란 일반적으로 흐름이라는 뜻을 가지고 있다. 하지만 이어지는 설명이 "줄을 지어 이어지다" 라는 뜻 또한 있는데 컴퓨터에서는 이쪽으로 생각하여 연결짓다라는 뜻을 가지고 있다. 물론 이는 직접적인 연결은 아니다.
예시를 들어보자. 최근엔 스트리머라는 직업이 그렇게 생소하진 않을것이다. 인터넷 방송인을 스트리머라고 부르는데 기존의 방송과는 달리 인터넷 방송(소위 스트리밍)의 경우 댓글을 이용하여 시청자와의 준 동시적 소통이 가능하게 된다. 이것 또한 방송인과 시청자가 연결되어있다는 뜻으로 스트리밍,스트리머라는 단어를 사용하는 것이다.
그렇다면 컴퓨터에서의 연결(스트림)은 무엇일까?? 단순히 지금 보고있는 컴퓨터 또한 마우스,키보드와 연결되어있는것이다. 이는 물리적 연결을 의미하는것이 아닌, 마우스를 움직이면 모니터의 마우스커서가 움직이는 등의 그런 연결을 말한다.
그렇다고 마우스가 움직인다고 모니터의 커서가 바로 움직이는것이 아니고, cpu를 통해 마우스의 움직임을 입력받고(입력 스트림), 모니터의 커서를 움직이는것(출력 스트림)이다. 하지만 이 연산과정이 워낙에 빠르기 때문에 우리는 마우스와 마우스 커서를 하나로 인식할정도로 "연결"되어있는것이다.
이것을 표준 입출력 스트림이라고 부른다.
stdin 기본 입력 스트림 - 기본 키보드 대상
stdout 기본 출력 스트림 -기본 모니터 대상
stderr 기본 오류 스트림 - 기본 모니터 대상이지만 보통의 경우 볼 수 없다.
여기서 잠시 의문이 들 수 있다. 아까 예시를 든건 마우스면서 왜 stdin의 예시에 마우스는 없냐?!?
맞다. 마우스도 기본 입력 스트림이다. 하지만 마우스는 비교적 후세대에 나온 장치이다. 그리고 그 작동 방식은 키보드의 입력이다. 이해가 되지 않는다면 시작버튼을 누르고 "마우스 키 켜기 끄기"를 찾아 체크하게 된다면 이제 당신은 숫자 키패드를 이용하여 마우스 조작이 가능하다. 4는 왼쪽 6은 오른쪽같은 경우이다. 요즘같은 경우 컴퓨터는 마우스가 없으면 사용할 수 없지만, 과거엔 마우스가 없는것이 당연했었다. 그래서 기본 입력 스트림은 키보드대상이라고 한 것이다. 그렇다고 또한 거기에 추가하여 마우스라고 할 수 있는것이다.
좋다. 이제 스트림은 넘어가고 버퍼를 보자.
컴파일러를 열고 메인함수에 아무런 변수를 선언 후 cin으로 그 변수의 값을 받아보자. 출력하지 않아도 좋다.
그렇게 된다면 콘솔 창이 열리며 하얀 줄이 깜빡이게 된다. 이것을 버퍼라고 한다.
버퍼의 존재 이유는 아무래도 사람이 키보드로 값을 입력하는 속도보다 컴퓨터의 연산처리속도가 압도적으로 빠르다 보니 둘 사이의 시간적 차이를 없애기 위해 컴퓨터가 기다려 주는 것이다. 임시적으로 메모리를 할당하여 값을 버퍼에 받아내고 엔터를 치는 순간 입력이 진행되고 연산을 한 후에 출력을 하던, 변수에 값을 넣던 하게 되는것이다.
만일 버퍼가 없다면 우리는 매 키보드를 칠때마다 값이 입력될것이고, 오타가 나더라도 수정할 수 없게된다.
스트림= 컴퓨터 소프트웨어(cpu)와 하드웨어(마우스, 키보드, 모니터 등)의 연결(혹은 하드웨어를 사용하는 사람과의 연결)
버퍼= 사람의 입력과 컴퓨터의 연산처리속도를 맞추기 위해 메모리를 사용하여 잠시 그 값을 저장해 놓는 공간
이렇게 정리할 수 있겠다.
놀랍다. 파일 입출력이라며 아직 파일은 꺼내지도 못했다.
걱정마라. 곧 할꺼다. 그리고 걱정해라. 파일열기 시작하면 더 많은 내용이 기다릴테니
일단 파일은 여러가지 종류가 있다. 이미지도 있고 텍스트도 있고 기타 등등 너무나도 많다. 하지만 컴퓨터의 모든것은 비트와 바이트로 이루어져있으니 무서울것이 없다. 먼저 우리 편하게 텍스트 파일부터 열도록 하겠다.
파일을 입력하는 함수는 다음과 같다.
fopen_s(입력받을 포인터변수,"파일의 경로","읽는 방식");
설명할것이 굉장히 많다. 차근차근 설명하겠다.
먼저 이 함수에는 FILE*(보시다시피 전부 대문자이다.)인 변수가 필요하다. 특별할 거 없다. 포인터(주소값)인데 그것의 읽는 방식이 파일일 뿐이다.
fopen_s를 하기전에 미리 파일을 받을 파일포인터를 선언 후에 그 파일포인터 변수를 입력받을 포인터변수에 넣으면 된다.
그럼 자연히 그 파일에 대한 데이터가 파일포인터변수에 대입된다.
그다음 입력받을 주소가 있다.여기에는 포인터값을 넣을 수 있는 포인터 변수가 들어가게 된다.
그다음 파일의 경로가 있다.
파일의 경로 형식에는 절대경로와 상대경로가 있는데.
먼저 절대경로는 드라이브명부터 파일의 이름.확장자까지 모든 경로이다. 즉 "c:/폴더명/폴더명/파일.확장자" 형태를 띄게 된다. 이거 모르겠는데요!! 한다면 열고자하는 파일에 마우스 우클릭을하고 속성을 보자. 위치 항목에 친절하게 절대경로로 쓰여있다. 그러나 이것을 복붙해서 fopen함수에 입력하려면 오류가 발생한다. 속성의 경로에서는 /가 \로(혹은 특수문자 원 표시) 쓰여있다. 하지만 컴파일러는 이를 이스케이프 문자(프로그램 내에서 특수문자를 쓰기 위한 1바이트 문자)로 인식하여 폴더명이 이상하게 된다.
만약 내가 c:/tistory/file.txt라는 파일을 적었다면 이를 c:(특수문자)istory(특수문자)ile.txt로 받아들이는 것이다. 이를 수정하기 위해서는 \를 /로 바꿔주던가, \\로 바꿔주어야 컴퓨터가 온전히 읽어낼 수 있게 된다.
상대 경로는 지금 작성중인 코드의 파일의 위치를 시작점으로 잡아 그 뒤 "/폴더명/파일.확장자" 가 된다.
또한 지금 폴더를 벗어나 이전으로 가고 싶다면 ../해주면 폴더를 벗어날 수 있게 된다.
만일 자신이 적고있는 cpp파일이 어디 저장되는지 모른다면 컴파일러에서 아마 오른쪽에 있을 솔루션에 프로젝트 명에 우클릭하여 파일탐색기에서 폴더 열기를 누른다면 그 프로젝트폴더가 열린다. 그곳에 지금 적고있는 소스가 있을 가능성이 높다.
그러니 상대경로를 이용하자면 그 소스를 기준으로 폴더명과 파일.확장자 까지 적게 된다면 그 폴더에 접근할 수 있게 된다.
이번에는 읽는 방식이 있다. 이것은 읽을것이냐 쓸것이냐를 물어보는것인데 두글자로 이루어져있다. 종류는 다음과 같다.
r :read 읽다.
w :write 쓰다.
a :add 뒤에 덧붙여쓰다.
========================
b :binary 이진수형으로. (바이너리 모드)
t :text 글자 형으로. (텍스트 모드)
앞뒤의 글자들을 조합하여 내가 읽는 방식을 정할 수 있다. 즉 "rt"하면 읽겠다. 글자형으로!가 된다.
또한 이 함수는 반환값이 errno_t인데 단순히 int이다. 과거에 bool형 자료형이 없는경우 사용했던 방식이 아직까지 남아있는것이다. 파일을 열기에 성공했을때 0을 반환하고, 실패하면(즉 파일이 없거나 읽을 수 없는 상태일때)1혹은 -1을 반환한다.
그리고 이렇게 파일을 열었을 경우 파일 스트림이라고 한다. 파일과 코드를 "연결"해놓았다는 뜻이다.
예를 들어보겠다. 예시를 위해 아까 열었던 현재 적고있는 프로젝트의 경로보다 한칸 바깥의 폴더에 Data폴더를 만들고 그 안에 Test.txt파일을 적어보겠다. 텍스트의 내용물은
10
20
30
40
100
이라고 다섯개의 숫자를 적어두겠다.
그런 뒤 그 파일을 여는 코드는 다음과 같다.
FILE* fTest = nullptr;
errno_t err = fopen_s(&fTest, "../Data/Test.txt", "rt");
if (!err) {
//행동
}
조금 생소할 수 있다. 첫줄부터 설명하겠다.
먼저 FILE포인터를 받을 수 있는 fTest변수를 선언하였고 그값을 널포인터로 지정하여 쓰기 불가능하게 만들어두었다.
다음 errno_t 를 받는(사실 없어도 된다. 그냥 숫자이다.)
err변수에 fopen_s의 반환값을 넣었고, 매개변수는 순서대로 fTest의 포인터(즉 FILE포인터의 포인터이니 이중포인터이다.),파일경로,텍스트 읽기방식의 형태로 열었다.
err로 받은 이유는 fopen_s가 오류가 발생했을 때 파일을 열면 안되는 상황이 된다. 그것을 확인하기 위해 변수로 받았다.
따라서 if(!err)는 err가 0이면 0이 아닌 값으로 바뀌어 참이 된다.즉, 정상적으로 파일이 열렸을 때! 행동을 하겠다는 의미이다.
이렇게 파일을 열 수 있었다. 하지만 설명이 부족한 부분이 있었다. 바이너리 모드와 텍스트모드인데 일단 둘 다 파일을 읽는것 자체는 크게 다르지 않다. 버퍼에 들어가고, 그것을 읽어내는것이다. 하지만 두가지는 읽는 방식의 차이가 있는데.
그것을 보기 위해 실험용으로 만들어둔 Test.txt를 각각 텍스트모드와 바이너리 모드로 읽고 둘의 차이를 보자. 읽는 코드는 다음과 같다.
FILE* fTest = nullptr;
errno_t err = fopen_s(&fTest, "../Data/Test.txt", "rt"/*혹은 "rb"*/);
if (!err) {
while (1) {
int iTemp= fgetc(fTest);
if (iTemp == EOF) {
break;
}
cout << iTemp << endl;
}
}
위의 틀은 다르지 않으니 설명하지 않겠다.
파일이 정상적으로 열렸을 때, 바로 while문의 반복을 시작했다. 그리고 그 안에 int iTemp를 선언하여 fgetc()함수를 이용하여 파일을 한칸만을 읽어내고 (아직 설명하지 않은 부분이다. 다음 포스팅에 할것이니 일단 넘어가자.)
읽어낸 값이 EOF(파일의 끝을 의미하는 코드이다. 이역시 다음에 설명한다.)가 아닐경우 즉, 끝나지 않았을 경우 그 값을 숫자형태로 출력하였다.
그렇다면 결과값은 각각 다음과 같다.
바이너리 모드 텍스트 모드
49 49
48 48
13 10
10
50 50
48 48
13 10
10
51 51
48 48
13 10
10
52 52
48 48
13 10
10
53 53
48 48
13 10
10
49 49
48 48
48 48
일단 우선 우리가 원한 10,20같은 값은 아니다. 당연히 우리는 그 파일을 int형식으로 읽어냈기 때문에 이렇다. 이것은 각각의 아스키 코드값이다. 즉 첫줄인 49와 그다음 48을 아스키코드 변환하면 각각 1과 0이다.
또한 텍스트 모드의 값은 내가 직접 서로 비교하기 편하기위해 줄을 맞춰놓은 상태이며, 원래는 빈칸 없이 줄줄히 입력되어있었다.
바이너리와 텍스트모드를 각각 비교해보면 텍스트 모드에는 없는 13이 있을것이다.
이것은 아스키코드표를 참고하면 '\r'이다. 이것은 커서를 왼쪽 끝으로 정렬하는 역할을 한다. 설명하겠다
우리는 글을쓸때, 줄바꿈을하면 자연히 그 줄의 가장 왼쪽으로 가서 다음 줄을 적기 시작한다. 하지만 컴퓨터는 밑의 줄로 가라 라는 의미의 '\n'을 하게되면 그 바로 밑줄로 가지만, 왼쪽 끝으로 가라는 명령은 받은적이 없기때문에 바로 그 위치에 가서 적게된다.
그렇기에 텍스트모드와 바이너리 모드로 "abc\ndef\nghy"를 입력하게 된다면 다음과같이 표현될 것이다.
텍스트모드
abc
def
ghy
바이너리 모드
abc
def
ghy
그래서 우리가 원래 생각하는 줄을 바꿔라 라는 명령어를 하기 위해서는 '\r' 과 '\n' 을 같이 적어주어야 왼쪽 끝으로 이동한 후 줄을 바꾸게 될것이다.
하지만 사람이 적은 텍스트를 바이너리모드로 읽게 될 경우 줄을 바꿀때마다 \r과 \n을 적게되면 바이트가 낭비되므로 자동으로 두가지 기능을 같이 하는 텍스트 모드로 읽는것이 적지만 바이트 수의 차이가 나게 된다.
이상으로 파일을 여는법을 마치겠다.
그럼 이제 파일을 저장하는 법 이다.
방식은 이전과 동일하다. fopen 중 모드를 r이 아닌 w나 a모드로 사용하면 된다.
주의할 점은 파일을 저장할 위치에 동일한 파일명이 존재할경우 그 파일은 덮어쓰기 되므로 기존의 데이터가 모두 날아가게 된다. 또한 덮어쓰기방식의 삭제의 경우 어떤 방법으로도 복구가 불가능하기 때문에 이런 일은 절대로 있어서는 안될것이다. 혹시모르니 코드로 읽고 쓰는것을 보여주겠다.
FILE* fTestRead = nullptr;
FILE* fTestWrite = nullptr;
errno_t errRead = fopen_s(&fTestRead, "../Data/Test.txt", "rt");
errno_t errWrite = fopen_s(&fTestWrite, "../Data/Test2.txt", "wt");
if (!errRead && !errWrite) {
//동작
}
파일을 구분하기 위해 각각의 포인터명에 Read와 Write를 적어준것과 "rt","wt"가 변한것 외에 크게 차이는 없다.
다만 파일명은 덮어쓰기 방지를 위하여 Test2.txt로 저장하고자 했고 연 파일이 두개이기 때문에 조건문의 형태 또한 바뀌었다.
이 코드를 적게 된다면 큰 문제가 없다면 Data폴더에 Test2.txt가 생겼을 것이다.
보통 쓰기의 경우 에러를 반환할 일이 없다. 기존에 동일한 파일이 있다면 덮어쓰면 그만이고 없으면 만들면 그만이라서 그렇다. 다만 "읽기전용" 드라이브의 경우에는 아마 오류를 반환할것이라 생각한다.(안해봤다, 추측이다.)
또한 여태 말하지 않았지만 이전 동적할당처럼 열려있는 파일이 있다면 꼭 닫아주어야한다. (동적할당과 같이 요즘은 필수는 아니지만 그렇다고 열여둘 이유는 없다.)
열린 파일을 닫는 함수는 다음과 같다.
fclose(파일포인터)
단순하다. 열린 파일을 닫겠다는것이다. 이 역시 파일이 정상적으로 열렸을 경우에만 닫을 수 있기 때문에 if(!err) 안에서 동작해야 할 것이다.
그리고 이 과정을 파일 스트림을 해제한다 라고 표현한다. 연결을 해제했다는 뜻이다.
이것으로 파일 입출력을 마치고, 다음 포스팅은 파일 읽기 및 작성이다.
'c언어' 카테고리의 다른 글
매크로와 레퍼런스 (0) | 2023.02.09 |
---|---|
파일 입출력 -파일 읽기 및 쓰기 (0) | 2023.02.09 |
동적 할당(신버전) + 메모리 관련 함수들 (0) | 2023.02.07 |
동적 할당(구버전) (0) | 2023.02.06 |
사용자 정의형 자료형 - 공용체와 열거체 (0) | 2023.02.06 |