fstream, filesystem

하루공부·2024년 1월 23일
0

C++

목록 보기
21/25
post-thumbnail

C++ 아이콘 제작자: Darius Dan - Flaticon


fstream

  • 파일로부터 프로그램에 입력할 수 있도록 도와주는 클래스
#include<iostream>
#include<fstream>
#include<string>
#include<vector>

int main() {
	std::vector<std::string> words = { "aaa\n", "bbb\n", "ccc" };

	std::ofstream writefile("test.txt");
	if (writefile.is_open())
	{
		for (int i = 0; i < static_cast<int>(words.size()); i++)
		{
			writefile << words[i];
		}
	}

	writefile.close();

	std::ifstream readfile("test.txt");

	if (readfile.is_open())
	{
		while (!readfile.eof())
		{
			std::string str;
			readfile >> str;
			std::cout << str << std::endl;
		}
	}
}

  • 파일 생성

    • std::ofstream writefile("test.txt"); ==> 파일에 쓰기용으로 파일을 생성
    • std::ifstream readfile("test.txt"); ==> 파일을 읽기용으로 파일을 생성

      위 처럼 파일을 선언할 때 파일 이름을 전달해 파일을 open하는 생성자 사용하면 편하다.

  • 파일 열기

    • 파일을 열기위해 open()함수를 사용해야한다.
    • 함수원형

      void open (const char* fileName, ios_base::openmode mode);
      void open (const string& fileName, ios_base::openmode mode);

      파일을 열 때 모드를 설정할 수 있다.
      보통의 경우에는 ifstream은 in, ofstream은 out으로 default 설정되어 있음.

      ios_base::in - 파일을 read할 목적으로 open할 것이다.
      ios_base::out - 파일에 write할 목적으로 open할 것이다.
      ios_base::binary - 파일을 바이너리 형태로 open할 것이다.
      ios_base::app - 파일 내용에 이어서 추가됨(ofstream객체 옵션)
      ios_base::ate : 자동으로 파일 끝에서 부터 읽기와 쓰기를 실시, 기존 내용 보존 x
      ios_base::trunc : 파일을 열면 기존에 내용들이 모두 지워진다.

  • 파일이 열렸나???

    파일이 열려있는지 확인 ==> bool is_open() const; 멤버 함수

  • 파일 닫기

    void close(); 멤버 함수

    사실 c++에서 fstream객체의 소멸자가 자동으로 close해준다.
    하지만 새로운 파일에서 무언가를 할 때는 이전 파일을 닫고 작업을 해야함.

  • 파일 내용 추가

    • <<으로 string을 파일에 추가했는데
    • 이외에 write함수가 있다. ostream& write(const char* str, streamsize n);

      str 문자열을 n만큼 파일에 추가한다.
      string일 경우 const char* 로 변환해주는 string.c_str() 사용

      c언어 배열로 나타내는 문자열은 문자열 끝에 '\0'이 들어가 있기 때문에 배열의 "총 길이-1"을 write의 두번째 인자로 넣어야해서 귀찮다. 

    getline함수 (string에 있는 함수, 위는 파일 헤더에 있는 함수)
    ==> std::getline(filename, 저장될 string 객체) 위 보다 더 간단하지만 <<이 젤 편한 것 같다.

  • 파일 내용 읽기

    • >> 내용 추가와 마찬가지로 >>를 사용해 읽기도 가능하다.
    • 이외에 get() 함수 ==> istream& get (char& c); 파일에서 1문자 씩 c에 저장한다.
    • getline() 함수 ==> istream& getline(char* str, streamsize len); 파일에서 1줄 씩 읽어 str에 저장

      여기서 1줄의 기준은 '\n' 개행문자 또는 EOF 만날 때 까지다.

      위 함수는 char을 받는다. c_str을 사용해도 const char을 반환한다.

  • 끝은 어디인가??

    bool eof() const; 끝임을 확인하는 함수, 파일 끝이면 true 아니면 false

    파일에는 위치 지정자가 있는데 (그냥 커서라고 편히 생각) 이를 사용해 파일의 위치를 확인해서
    읽고 쓰기를 한다.
    이것이 파일의 끝에 도착했는지 확인하는 것임

  • binary로 파일을 열 때

 int x;
readfile.read((char*)(&x), 4);
  1. 첫 번째 인자에 문자를 저장할 장소인 버퍼를 전달해야 한다.
    (에제의 경우 int 변수를 마치 4 바이트 짜리 char 배열을 전달..)
  2. 두 번째 인자로 반드시 몇 바이트를 읽을 지 전달해야 한다.



filesystem

  • C++ 17 에 도입된 파일 시스템 라이브러리 fstream과 다르다.

    fstream은 파일의 데이터를 건들이지 파일에 관한 정보(파일위치 등등)는 만질 수 없다.
    filesystem은 파일 데이터에 접근을 도와주며 파일을 읽는 것은 못한다.
    파일 시스템 라이브러리의 경우 모든 클래스와 함수들이 std::filesystem 이름 공간 안에 정의되어 있다. std::filesystem 를 fs으로 정의해서 편하게 사용하기

path

  • 컴퓨터 상의 모든 파일에는 해당 파일의 위치를 나타내는 고유의 주소가 있는데 이를 경로(path)라고 부른다.

    컴퓨터에서 해당 파일을 참조할 때 가장 맨 첫 번째 디렉토리 부터 순차적으로 찾아간다.
    예를 들어서 /a/b/c 라는 경로를 따라가기 위해서는 맨 처음에 /a 디렉토리를 찾은 다음
    해당 디렉토리 안에 b 라는 디렉토리를 찾고 맨 마지막으로 b 안에 c 라는 파일을 찾는다.

    이 때 경로를 지정하는 방식에는 두 가지가 있는데, 바로 절대 경로(absolute path) 와 상대 경로(relative path) 가 있다

    • 절대 경로는 가장 최상위 디렉토리 (root 디렉토리) 에서 내가 원하는 파일까지의 경로를 의미한다.
    • 윈도우의 경우 root 디렉토리는 C:\나 D:\와 같은 것들이 된다.
      즉, 경로의 맨 앞에 C:\이면 절대 경로라 생각하자

    • 상대 경로의 경우 반대로 현재 프로그램이 실행되고 있는 위치에서 해당 파일을 찾아가는 경로 입니다.
    • 예를 들어서 경로를 그냥 a/b 라고 했다면 이는 현재 프로그램의 실행 위치에서 a 라는 디렉토리를 찾고 그 안에 b 라는 파일을 찾는 식이지요.
    • 따라서 만약에 현재 프로그램의 실행 절대 경로가 /test/game 라면 b 의 절대 경로는 /test/gamea/b가 된다.

std::filesystem::path p("./some_file"); 여기서 ./some_file는 상대경로인 것이다.
.은 현재 디렉토리를 의미하는 문자로 위 경로는 현재 프로그램이 실행되는 위치의 some_file을 나타낸다.

filesystem의 모든 함수들은 파일을 나타내기 위해서 path 객체를 인자로 받는다.
그래서 경로를 나타내는 path객체를 정의하고 해당 객체로 파일(디렉토리) 정보를 다룬다

여기서 path객체는 파일의 경로만 나타내지 파일을 가리키는게 아니다.

exists

  • 해당 경로에 파일이 있는지 확인하는 함수
    ex) std::filesystem::exists(path 객체) 해당 경로에 파일이 있으면 1 아니면 0을 반환한다.

파일? 디렉토리?

  • 파일인지 확인하는 함수 ==> is_regular_file(path 객체)
  • 디렉토리인지 확인하는 함수 ==> is_directory(path 객체)

current_path() : 프로그램이 실행되는 경로를 리턴
path 객체.relative_path() : 상대경로 표시
absolute(path 객체) : 해당 경로를 절대 경로로 변경
canonical(path 객체) : absolute의 경로에서 . 이나 .. 같은 불피요한 요소를 제거 - 공식적인 절대 경로 => ??

위의 모든 함수에서 해당 경로에 파일이 존재하지 않으면 모두 예외를 반환한다.
따라서 위 함수들을 호출하기전 exists로 확인하자

디렉토리 안에 있는 파일, 디렉토리 살펴보기

  • filesystem 라이브러리에서는 directory_iterator 라는 반복자를 제공한다.
  • 사용하기 위해서 객체를 정의시 생성자에 탐색할 경로를 넣어준다.
    ex) fs::directory_iterator itr(fs::current_path() / "a");

    /는 path의 연산자로 디렉토리 경로를 추가하는 연산이다
    위 예시는 현재 프로그램 실행 경로에 /a를 추가한거다.

    filesysytem의 end함수에 반복자를 전달하면 해당 반복자의 끝을 얻을 수 있다.
    ex) fs::end(itr)


  • 반복자들은 디렉토리에 정의되어 있는 개개의 파일을 나타내는 directory_entry를 가르키고 있다.

    directory_entry에는 여러 정보들이 저장되어 있다 ... 파일 이름, 크기 등등...

fs::directory_iterator itr(fs::current_path() / "a"); // 현재 프로그램 디렉토리/a 안에 있는 파일을 탐색함
while (itr != fs::end(itr)) {
  const fs::directory_entry& entry = *itr;
  std::cout << entry.path() << std::endl; // => 대충 /현재 프로그램 디렉토리/a/b
  itr++;  }

const fs::directory_entry& entry = *itr;
entry.path()로 해당 파일의 경로를 추출할 수 있다.

1가지 단점은 해당 디렉토리 안에 있는 디렉토리까지는 살펴보지 않는다.
위 예시에서 b디렉토리 안에 여러가지 파일이 있지만 그 것을 살펴보지는 안는다.
( 예시로 b디렉토리 안에 c디렉토리도 있지만 출력되지 않음 )

만약 디렉토리안의 디렉토리까지 모두 순회하고 싶다면 recursive_directory_iterator 반복자를 사용하면 된다.




디렉토리 생성하기

  • create_directory함수로 디렉토리를 생성한다. 인자로 path객체를 전달하여 사용
    ex) fs::create_directory("./a/c"); ==> ./a경로에 /c 디렉토리 생성

    주의할 점은 생성하는 디렉토리의 부모 디렉토리는 반드시 존재해야 한다
    위의 경우 ./a 디렉토리가 존재하기에 해당 작업은 잘 작동한다.
    그렇지 않으면 예외가 발생한다

    부모 디렉토리까지 1번에 같이 만드는 함수도 있다.
    create_directories(path 객체) 함수 사용하면 된다.




파일/디렉토리 복사하기

  • copy 함수로 디렉토리와 디렉토리 안에 있는 모든 파일을 복사할 수 있다.
    fs::copy(복사할 대상들이 들어있는 path 객체, 해당 파일 들을 저장할 path 객체, 옵션)
$ tree
├── a
│   ├── a.txt
│   └── b
│       └── c.txt
├── c

==> 현재 디렉토리와 파일 상태라고 생각하자

fs::path from("./a");
fs::path to("./c");
fs::copy(from, to, fs::copy_options::recursive);
|── a
│   ├── a.txt
│   └── b
│       └── c.txt
├── c
│   ├── a.txt
│   └── b
│       └── c.txt

해당 path의 경로 밑에 있는 것들을 원하는 path경로 밑에 복사한다.
위 예시에서 옵션을 recursive 주었는데
==> 이는 복사할 대상에 존재하는 모든 디렉토리와 파일들을 복사하는 옵션

만약 복사할 대상이 이미 존재한다면 예외를 던진다
이 때 filesystem에서 제공하는 3가지 선택지를 준다. ==> 옵션으로 전달

  • skip_existing : 이미 존재하는 파일은 무시 (예외 안던지고)
  • overwrite_existing : 이미 존재하는 파일은 덮어 씌운다.
  • update_existing : 이미 존재하는 파일이 더 오래되었을 경우 덮어 씌운다.



파일/디렉토리 삭제

  • 파일 삭제는 remove 함수를 통해서 간단히 경로만 전달. fs::remove(path 객체)
fs::path p("./a/b.txt");  
fs::remove(p); // ./a/b.txt 삭제

1가지 주의사항은 해다 디렉토리는 반드시 빈 디렉토리여야 한다.
만약 다른 파일 또는 디렉토리가 들어있는 디렉토리를 삭제하고 싶다면 remove_all을 사용하면된다.


directory_iterator 사용시 주의할 점

  • 예를 들어서 해당 디렉토리 안에 확장자가 txt 인 파일을 모두 삭제하는 프로그램을 만들고 싶다고 가정
#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;
int main() {
	fs::path p("./a");
	for (const auto& entry : fs::directory_iterator("./a")) {
		const std::string ext = entry.path().extension();
		if (ext == ".txt")  fs::remove(entry.path());
	}
}

우선 extension은 파일 구성 요소 확장자를 반환한다

위 예제에서 문제점은 디렉토리에서 1개씩 무언갈 삭제할 때 디렉토리의 구조가 바뀐다
==> 반복자는 디렉토리의 구조가 바뀔 때 마다 무효화가 된다.
==> 그래서 1번 삭제하면 기존 반복자를 사용할 수 없다.

어쩔 수 없이 파일을 삭제할 때 마다 반복자를 초기화 해줘야한다

int main() {
    fs::path p("./a");
    while (true) {
        bool is_modified = false;
        for (const auto& entry : fs::directory_iterator("./a"))
        {
            const std::string ext = entry.path().extension();
            if (ext == ".txt")
            {
                fs::remove(entry.path());
                is_modified = true;
                break;
            }
        }
        if (!is_modified) break;
    } 
}

참조
filesystem
path::extension
공부한 내용 복습

개인 공부 기록용 블로그입니다.
틀린 부분 있으다면 지적해주시면 감사하겠습니다!!

0개의 댓글