공부/C || C++

C/C++ 파일 입출력

sudo 2021. 8. 14. 02:31

C 스타일 파일 입출력

errno_t fopen_s(
   FILE** pFile,
   const char *filename,
   const char *mode
);

return : 성공 시 0을 리턴하고, 에러 발생 시 errno_t타입의 0이 아닌 값을 리턴

pFile : FILE타입(파일 스트림)의 이중 포인터

filename : 파일 이름. 파일 이름만 쓰고, visual studio에서 빌드 하면 프로젝트 파일이 있는 곳에 파일을 만든다

mode 

  • r : 파일 읽어오기
  • w : 파일 쓰기(만들기)
  • a : 파일에 접근하여 이어 쓰기
  • r+ : 파일이 존재하면 해당 파일을 읽고 쓰기 가능하게 연다. 파일이 없으면 에러를 반환
  • w+ : 파일이 존재하면 해당 파일에 읽고 쓰기 가능하게 연다. 파일이 없으면 새로 만든다
  • a+ : 파일이 존재하면 해당 파일의 끝부터 읽고 쓰기가 둘다 가능하게 연다. 파일이 없으면 새로 만든다
  • rt : txt 파일을 읽어온다
  • rb : binary 파일을 읽어온다
  • wt : txt 파일에 쓴다
  • wb : binary 파일에 쓴다
  • r+b 또는 rb+ : 읽고 쓰기 가능하게 binary 파일을 연다. 파일이 없으면 에러를 반환
  • w+b 또는 wb+ : 읽고 쓰기 위해 비어 있는 binary 파일을 쓴다. 없으면 파일을 새로 만든다.
  • a+b 또는 ab+ : 파일의 끝에 추가해서 쓰기 위해 binary 파일을 연

반드시 fopen이나 fopen_s로 파일을 열었으면 fclose로 닫아주자!

 

쓰기 함수들은 fputc, fputs, fprintf 등이 있다.

#include <stdio.h>
int fputc(int c, FILE *stream);	// 파일에 문자 하나 쓰기
int fputs(const char *string, FILE *stream); // 파일에 문자열 쓰기
int fprintf(FILE *stream, const char *format-string, argument-list); // 파일에 문자열 쓰기

fprintf의 특징은 문자열 포맷팅을 사용해서 숫자도 써줄 수 있다는 것이다

int main()
{
	FILE* FileStream = nullptr;

	fopen_s(&FileStream, "Test.txt", "wt");

	int	Number1 = 200;
	int	Number2 = 300;

	fprintf(FileStream, "Number1 %d Number2 %d입니다.\n", Number1, Number2);
    
    fclose(FileStream);
    
	return 0;
}

 

파일을 읽는 함수는 쓰는 함수와 대응되도록 fgetc, fgets가 있다.

#include <stdio.h>
int fgetc(FILE *stream); // 문자 하나를 읽어오는 함수
char *fgets (char *string, int n, FILE *stream); // 문자열을 읽어오는 함수

fgets의 특이한 점은 개행 문자를 만나면 거기까지만 읽어오는 것이다.

int main()
{
	FILE* FileStream = nullptr;

	fopen_s(&FileStream, "Test.txt", "wt");

	if (FileStream != nullptr)
	{
		fputs("문자 첫번째줄입니다.\n", FileStream);
		fputs("문자 두번째줄입니다\n", FileStream);

		fclose(FileStream);
	}

	fopen_s(&FileStream, "Test.txt", "rt");

	if (FileStream)
	{
		char Line[128] = {};
		fgets(Line, 127, FileStream);

		std::cout << Line << std::endl;
        
		fclose(FileStream);
	}
    
    return 0;
}

첫 번째 줄을 출력하고 개행 문자를 만나서 그 줄만 읽고 있는 것을 확인할 수 있다. 

 

#include <stdio.h>
int fseek(FILE *stream, long int offset, int origin); // 파일 커서 위치 변경
long int ftell(FILE *stream); // 파일 커서 위치 반환
  • offset : 인자 origin으로부터 몇 바이트를 이동할 것인지
  • origin : 기준점
  • 기준점의 종류 
    1. SEEK_CUR : 현재 파일 포인터의 위치이다.
    2. SEEK_END : 파일의 가장 끝 위치로 이동한다.
    3. SEEK_SET : 파일의 시작 위치로 이동한다.

파일을 읽고 나면 파일 내 커서가 읽은 만큼 이동한다. 따라서 나중에 읽을 때는 커서 위치부터 읽게 되므로, 읽지 않은 부분부터 읽기 시작한다. 이 커서 위치는 fseek 함수를 이용해서 바꿀 수 있으며 ftell 함수로 현재 커서 위치를 얻을 수 있다. 이 두 함수로 파일의 크기를 알 수 있다. 파일을 열면 커서 위치가 맨 처음으로 설정되어 있을 것이고 파일 커서 위치를 fseek 함수를 이용해서 맨 끝으로 설정하고, ftell 함수로 커서 위치를 반환하면 그 차이가 파일 사이즈이다.

int main()
{
    	
    fopen_s(&FileStream, "Test.txt", "rt");

	if (FileStream)
	{
		fseek(FileStream, 0, SEEK_END);

		// ftell : 현재 파일커서의 위치를 얻어온다.
		int	FileSize = ftell(FileStream);
        
		fclose(FileStream);
	}

	return 0;
}

 

binary 파일은 게임에서 save/load를 할 때 자주 사용되는데, 예를 들어 플레이어의 정보를 담고 있는 구조체를 binary 파일에 저장해뒀다가 나중에 읽어올 수 있다. 확장자명은 아무렇게나 해도 된다.

enum class EJob
{
	None,
	Knight,
	Archer,
	Magicion
};

struct Player
{
	char	Name[32];
	EJob	Job;
	int		Attack;
	int		Armor;
};

int main()
{
	Player	player;
	strcpy_s(player.Name, "마스터이");
	player.Job = EJob::Knight;
	player.Attack = 10;
	player.Armor = 5;

	FILE* FileStream;

	fopen_s(&FileStream, "Test1.lol", "wb");

	if (FileStream)
	{
    	fwrite(&player, sizeof(Player), 1, FileStream);
        fclose(FileStream);
	}
    
	Player	player1 = {};
    
	fopen_s(&FileStream, "Test1.lol", "rb");

	if (FileStream)
	{
		fread(&player1, sizeof(Player), 1, FileStream);
		std::cout << "이름 : " << player1.Name << std::endl;
		std::cout << "직업 : ";
		switch (player1.Job)
		{
		case EJob::Knight:
			std::cout << "기사" << std::endl;
			break;
		case EJob::Archer:
			std::cout << "궁수" << std::endl;
			break;
		case EJob::Magicion:
			std::cout << "마법사" << std::endl;
			break;
		}

		fclose(FileStream);
	}

	return 0;
}

코드를 실행하면 다음과 같이 쓰려고 했던 파일이 잘 써진 것을 확인할 수 있으며 읽어오는 것도 잘 되고 있는 것을 확인할 수 있다.

C++ 스타일 파일 입출력

C++ 스타일 파일 입출력은 읽어올 때는 ifstream 클래스, 쓸 때는 ofstream 클래스를 이용한다. std안에 존재하며, fstream 헤더에 포함되어 있다. C언어 방식과 비교했을 때 독특한 것은 클래스라는 것이며, 그 의미는 읽고 쓰는 함수는 멤버 함수로 제공된다는 의미이다.

 

대표적으로 쓰이는 함수들은 아래와 같다.

void open (const char* fileName, ios_base::openmode mode = ios_base::in);
bool is_open() const;
void close();
istream& get (char& c); // 문자 하나씩 읽어오는 함수
istream& getline(char* str, streamsize len); // 한 줄을 읽어오는 함수
ostream& write(const char* str, streamsize n); // 인자 str을 파일에 써주는 함수

쓰임새는 함수 이름에서도 직관적으로 알 수가 있다. 예시 코드로 아래와 같이 작성해보았다.

#include <iostream>
#include <fstream>


int main()
{
	std::ofstream write;
	write.open("Test.txt");
	
	if (write.is_open())
	{
		const char str[128] = "가나다라\nABCD";

		write.write(str, strlen(str));
	}
	else
	{
		std::cout << "file open error" << std::endl;
		write.close();
		return 0;
	}

	write.close();
	
	std::ifstream read;
	read.open("Test.txt");

	if (read.is_open())
	{
		while (!read.eof())
		{
			char buf[128] = {};
			read.getline(buf, 128);

			std::cout << buf << std::endl;
		}
		read.close();
	}
	else
	{
		std::cout << "file open error" << std::endl;
		read.close();
		return 0;
	}

	return 0;
}

 실제로 getline은 개행 문자를 만나면 거기까지만 읽기 때문에 while문이 두 번 돌면서 읽어오는 모습을 확인할 수 있다.