CS/C++

C++ 공부 섹션18 입출력 : 홍정모의 따배씨쁠쁠

샤아이인 2022. 1. 17.

내돈내고 내가 공부한것을 올리며, 중요한 단원은 저 자신도 곱씹어 볼겸 상세히 기록하고 얕은부분들은 가겹게 포스팅 하겠습니다.

1) 섹션18

이번시간에는 입출력에 관하여 집중적으로 배웠다. 버퍼같은 개념이 간단한 것 같으면서도, 처음에 만나면 좀 당황스럽다.

18-1 istream으로 입력받기

이번에는 istream에 대하여 조금더 자세히 배웠다. 기본적으로 stream은 buffer에 임시적으로 저장이 되었다가 일부분씩 꺼내오는 방식이다. 우선 다음의 오류가 나는 간단한 코드를 확인해 보자.

buf의 사이즈가 10인대 입력문자가 10개가 넘어가 runtime에러가 발생하고 있다.

항상 이런점을 고려하면서 불편하게 사용해야 하는 것 일까? 아니다!

iomanip (input/output manipulators)를 header에 include 해준다음 사용하면 다음과 같이 사전에 방지할 수 있다.

위의 코드는 setw(_)를 사용하여 5글자 까지만 받아오고 있다.

 

내부적으로 cin 즉, input stream이 buffer를 갖고있다. 따라서 buffer로 abc~lmn이 들어가고 그중에서 5개만 갖어오는 것 인데 char배열의 경우 마지막에 null문자가 존제하기 때문에 4자리만 있어서 문자 4개만 나온다.

즉, 버퍼에서 5글자를 갖어왔는데 char배열에 4글자만 담았으니 나머지 한글자인 e는 아직 버퍼에 남아있다.

다음코드를 통해 확인 해 보자.

buf는 길이가 5인 배열이지만 마지막 한글자는 null이 담겨있으니 처음 출력은 abcd가 나온다.

이후 아직 버퍼에 담겨있는 efghi 중에서 다시 4글자만 갖어와 efgh를 출력한다. i는 버퍼에 남게된다.

 

다음 코드는 while문을 통하여 끊임없이 입력을 받는다.

다 좋은데 공백이 없어져서 문자열이 하나로 합쳐저 버렸다. cin으로부터 그냥 buffer에서 갖어올때는 빈칸은 무시하고 갖어온다.

빈칸까지 포함하면서 갖어오고 싶다면 cin.get(ch)을 사용하면 된다. 다음 사용 예를 확인해보자.

또한 배열에서도 사용이 가능하다.

추가적으로 gcount 라는 함수에 대하여 알게되었다. 최대 몇글자를 읽어왔는지를 알려주는 함수이다.

4글자씩 갖어오고 있음을 알 수 있었다.

 

그다음으로는 getline()에 대하여 배웠다.

getline은 한줄을 한번에 읽어들이지만, 제한된 범위까지만 읽어온다. 또한 두번째 getline을 읽어들이지 못했다.

왜냐하면 첫번째 getline이 통체로 읽어들이기 때문에 buffer가 비어있는 것 이다.

또한 getline은 줄바꿈 문자('\n') 까지 같이 읽어들인다.

 

또한 string에서도 사용이 가능하다.

위의 코드를 보면 cin.gcount()가 0을 출력했다. 당연한것이 getline함수를 사용한 것 이지, cin.gcount() 를 사용하지 않았다.

 

다음으로는 내가 요즘 햇갈리고 있었던 cin.ignore()에 대하여 배울 수 있었다.

ignore를 사용하면 stream의 첫 한글자를 버리는 기능을 한다. 만약 ignore(2) 처럼 사용한다면 2글자를 버리게 된다.

 

cin.peek()에 대해서 배웠는데 이는 처음보는 함수였다.

보통은 읽어들이면 버퍼에서 한글자가 빠지기 때문에 3번째 줄에서 ello 만 출력되지만, peek은 버퍼를 살짝 들여다 보기만 할뿐, 꺼내오지는 않는다.

다음 문자가 무엇일지 살짝 보는 것 이다. 또한 반대로 마지막에 읽어온 것을 다시 버퍼로 넘기는 unget() 이라는 함수도 있었다. 또한 역으로 버퍼에 전달하는 putback() 도 있다.

 

18-2 ostream으로 출력하기

cout.setf (set flag)를 사용하여 출력옵션을 바꿀 수 있다. 다음과 같이 말이다.

#include <iostream>
#include <iomanip>
using namespace std;

int main(void)
{
	cout.setf(std::ios::showpos);
	cout << 108 << endl;

	return 0;
}
 

결과로는 +108 이 나온다. 앞에 +기호가 출력이 된다. 또한 unsetf 도 있다.

+기호가 다시 사라졌다. 이를 활용하여 16진수를 출력하는 방식을 살펴보자.

 

16진수로 출력하기 위해서는 먼저 10진수를 unsetf 해줘야 한다.

하지만 이러한 방식은 불편하다. 다음과 같이 사용하는것이 편하다.

int main(void)
{
	cout.setf(std::ios::hex, std::ios::basefield);
	cout << 108 << endl;

	return 0;
}
 

base 상태에서 바로 hex로 출력하는 것 이다. field가 여러가지가 있는데, 그중 basefield의 flag를 set해주겠다는 의미가 된다.

마지막으로 setf 쓰는것 자체가 귀찮다면 다음과 같이 하면된다.

int main(void)
{
	cout << std::hex;
	cout << 108 << endl;

	return 0;
}
 

결과는 6C 로 원하는 16진수 방식으로 출력되고 있다. 다시 10진수로 돌아가고 싶으면 std::dec; 를 해주면 된다.

추가적으로 지금 16진수가 소문자로 출력되고있는데, 만약 대문자로 출력하고 싶다면 다음과 같이 바꿔보자.

대문자로 출력되고 있다.

 

bool값을 출력할때 또한 문자로 true, false를 확인하고 싶다면 cout << std::boolalpha; 를 추가해주면 된다.

boolalpha가 없었다면 1 또는 0으로 출력된다.

 

다음으로는 정밀도 조정에 대하여 알아보자.

정밀도 4의 결과를 보면 123.4가 아니라 123.5가 되있다. 이는 5번째 숫자인 6을 올림을 하여서 123.5가 된 것 이다.

추가로 소수점 아래의 숫자들을 고정하고 싶다면 std::fixed를 추가해주면 된다.

과학적 표기법을 사용하고 싶다면 cout << std::scientfic을 추가하자.

 

출력의 칸수를 정해줄 수도 있다.

 

18-3 문자열 스트림 (String stream)

문자열 스트림에 대하여 알아보자.

<< 을 통하여 stream으로 데이터를 보낼수 있고, >> 를 통하여 stream에서 추출해 올 수 있다.

하지만 결과를 보면 뒷 부분이 잘려나갔다. 이러한 현상은 앞에서도 본적이 있다. 빈칸이 있으면 잘라버리는 것 이다.

 

잘려나가는 문제를 해결하기 위해 다음과 같이 os.str()을 사용할 수 있다. string을 통으로 가져오고 있다.

 

str함수를 사용하여 입력하면 buffer를 통체로 Hello, World로 채운다. 이전의 Hello, World! 는 싹 지운 후 바꿔버리는 것 이다.

 

또한 string str을 사용하는 것 이 아닌, 다음과 같이 직접 사용할수도 있다.

 

이번에는 숫자를 문자열로써 입력 받아보자.

2개의 string을 사용하여 입력 받아오는것을 알 수 있다. 참고로 중간에 공백 빈칸을 기준으로 str1과 2로 나누어 진다.

 

다음으로는 string stream을 비우는 법에 대하여 알아보자.

int main(void)
{
	stringstream os;
	
	os << "12345 67.89";

	os.str(""); // 버퍼를 빈칸으로 비움

	cout << os.str() << endl;

	return 0;
}
 

원하는대로 아무것도 출력되지 않는다. 주의 깊게 봐야할점이, 똑같은 str함수인데 매개변수가 있을때는 입력받은것으로 buffer를 덮어씌우고 없을때는 return해주고 있다. 오버로딩 된 것 이다.

 

os.clear()는 error flag 를 초기화 해준다. stream의 state와 error flag에 대하여 다음쳅터에서 알아보자.

 

18-4 흐름 상태와 입력 유효성 검증

우리가 stream을 통해 입력을 받거나, file로부터 데이터를 읽어드릴때 항상 의도한대로 입력이 들어온다는 보장이 없다.

매번 string의 상태가 어떤지?, 입력받은 데이터가 우리가 의도한것과 같은지? 검증하는 과정이 필요하다.

 

std::ios를 통해 인자를 받는 printStates함수는 file stream과 콘솔의 IO_stream을 다 받아들일 수 있다.

void printStates(const std::ios& stream) //file, io stream과 같이 공통적으로 사용가능
{
	cout << boolalpha;
	cout << "good()=" << stream.good() << endl; // 상태가 좋으면 true
	cout << "eof()=" << stream.eof() << endl; // 파일을 다 읽엇는지
	cout << "fail()=" << stream.fail() << endl; // good의 반대
	cout << "bad()=" << stream.bad() << endl; // 데이터를 읽고쓸때 문제발생시 true
}
 

이방식 말고 bit mask를 사용하는 법도 배웠는데 기록은 생략하겠다. 위의 방식이 modern cpp의 방향이라 생각한다고 하셨다.

#include <iostream>
#include <cctype>
#include <string>
#include <bitset>
using namespace std;

void printStates(const std::ios& stream) //file, io stream과 같이 공통적으로 사용가능
{
	cout << boolalpha;
	cout << "good()=" << stream.good() << endl; // 상태가 좋으면 true
	cout << "eof()=" << stream.eof() << endl; // 파일을 다 읽엇는지
	cout << "fail()=" << stream.fail() << endl; // good의 반대
	cout << "bad()=" << stream.bad() << endl; // 데이터를 읽고쓸때 문제발생시 true
}

int main(void)
{
	while (true)
	{
		int i;
		cin >> i; // 입력이 정수가 아니면 문제발생

		printStates(cin);

		cout << i << endl;
		cin.clear();
		cin.ignore(1024, '\n'); // 청소작업
		cout << endl;
	}

	return 0;
}
 

위의 코드를 실행시켜보자.

정수를 입력받아야 하기 때문에 123은 정상적으로 받아서 출력해주고 있다. 문자를 잘못 입력하면 flase가 되었으며, double형을 입력해준 경우 소수점 자리가 절삭되고 있다.

 

state말고 내가 뭔하는 문자인지를 확인하는 함수도 만들 수 있다.

#include <iostream>
#include <cctype>
#include <string>
#include <bitset>
using namespace std;

void printStates(const std::ios& stream) //file, io stream과 같이 공통적으로 사용가능
{
	cout << boolalpha;
	cout << "good()=" << stream.good() << endl; // 상태가 좋으면 true
	cout << "eof()=" << stream.eof() << endl; // 파일을 다 읽엇는지
	cout << "fail()=" << stream.fail() << endl; // good의 반대
	cout << "bad()=" << stream.bad() << endl; // 데이터를 읽고쓸때 문제발생시 true
}

void printCharacterClassification(const int& i)
{
	cout << boolalpha;
	cout << "isalnum" << bool(std::isalnum(i)) << endl; // 알파벳 또는 숫자냐?
	cout << "isblank" << bool(std::isblank(i)) << endl; // 빈칸이냐?
	cout << "isdigit" << bool(std::isdigit(i)) << endl; // 10진수냐?
	cout << "islower" << bool(std::islower(i)) << endl; // 소문자냐?
	cout << "isupper" << bool(std::isupper(i)) << endl; // 대문자냐?
}

int main(void)
{
	while (true)
	{
		char i;
		cin >> i; // 입력이 정수가 아니면 문제발생

		printStates(cin);

		//cout << i << endl;

		printCharacterClassification(i);

		cin.clear();
		cin.ignore(1024, '\n'); // 청소작업
		cout << endl;
	}

	return 0;
}
 

함수 하나가더 추가되었다. 실행 결과를 확인해 보자.

위와 같이 string의 상태를 확인할 수 있다.

 

또한 원하는 형식인지 검증하는 함수도 만들어 사용할 수 있다. 다음 함수는 flag를 이용하여 10진수가 아니라면 flag값을 false로 바꾼다.

bool isAllDigit(const string& str)
{
	bool ok_flag = true;

	for(auto e : str) // 한글자씩 비교해봄
		if (!std::isdigit(e))
		{
			ok_flag = false;
			break;
		}

	return ok_flag;
}

int main(void)
{
	cout << boolalpha;
	cout << isAllDigit("1234") << endl;
	cout << isAllDigit("a1234") << endl;

	return 0;
}
 

출력결과로는 true false가 나온다.

 

18-5 정규 표현식 소개

어떤 문자열이 우리가 원하는 형식에 맞춰저 있는지 확인하는것은 입출력 데이터를 다룰때 중요한 문제이다.

예를들어 전화번호 형식에 맞는지 안맞는지 확인하는 정규표현식에 대하여 배웠다.

#include <iostream>
#include <regex>

using namespace std;

int main(void)
{
	regex re("\\d"); // 정수냐?
	regex re("[ab]"); // a 또는 b 한글자
	regex re("[[:digit:]]{3}"); // 정수 3개
	regex re("[A-Z]+"); // A부터 Z까지 여러개
	regex re("[A-Z]{1,5}"); // A부터 Z까지 1개부터 5개 까지
	regex re("((0-9){1})([-]?)([0-9]{1,4})"); // 0부터 9까지중 숫자 하나, -는 있어도되고 없어도되고, 0부터9까지중 1개부터 4개까지

	return 0;
} 
 

위의 식에서도 함수가 추가적으로 필요하지만, 이 단원의 자세한 설명은 생략하겠습니다.

 

18-6 기본적인 파일 입출력

실무에서는 파일을 다루는 일 또한 중요한 일이다. 기본적인 파일 입출력에 대하여 알아보자.

#include <iostream>
#include <fstream>
#include <string>
#include <cstdlib> // exit()
#include <sstream>

using namespace std;

int main(void)
{
	// writing
	if (true)
	{
		ofstream ofs("my_first_file.dat"); //생성자의 인자로 파일이름 넘기기
		// ofs.open("my_first_file.dat");

		if (!ofs) // 파일을 열 수 없다면
		{
			cerr << "Couldn't open file " << endl;
		}

		ofs << "Line 1" << endl;
		ofs << "Line 2" << endl;

		// ofs.close(); 수동으로 파일 닫기, 추가 안해도 범위를 벗어나면서 소멸자가 닫음
	}

	return 0;
}
 

실행시 아무것도 변화가 없는것 처럼 보인다.

해당 폴더로 이동해보니 dat 파일이 생성된것을 확인할 수 있었다.

이렇게 << 연산자 를 사용하면 text모드로 파일에 저장이 된다. 아스키 포멧에 맞추어서 생성된다.

이 말은 메모장으로 열어볼 수 있다는 것 이다. 다음과 같이 말이다.

ofs에 전달한 문자열들이 저장되있는 것 을 확인할수있었다.

 

다음으로는 파일을 읽어보자.

int main(void)
{
	// reading
	if (true)
	{
		ifstream ifs("my_first_file.dat");

		if (!ifs)
		{
			cerr << "Cannot open file" << endl;
			exit(1);
		}

		while (ifs)
		{
			std::string str;
			getline(ifs, str);

			std::cout << str << endl;
		}
	}
	return 0;
}
 

위의 코드를 실행시 직전에 만든 파일을 정상적으로 읽어오는 것 을 확인할수 있다. 결과로는 Line 1 Line 2이 출력된다.

만약 파일이름을 잘못 입력했다면 파일을 찾을 수 없다고 나온다. 다음은 찾을 파일의 이름에 실수라는 단어를 추가한 예이다.

이번에는 binary로 저장하는 방법에 대하여 알아보자. 아스키 형식으로 데이터를 전부 저장하면 엄청 느리기 때문에 사용한다.

실무에서는 거의 binary로 저장한다고 알려주셨다.

 

binary로 저장을 할때는 데이터가 어디가 끝인지 알수가 없다. 따라서 어떤 데이터가 얼마만큼 저장이 될지를 미리 약속하여 알고있어야 한다.

#include <iostream>
#include <fstream>
#include <string>
#include <cstdlib> // exit()
#include <sstream>

using namespace std;

int main(void)
{
	// writing
	if (true)
	{
		ofstream ofs("my_first_file.dat"); //생성자의 인자로 파일이름 넘기기
		// ofs.open("my_first_file.dat");

		if (!ofs) // 파일을 열 수 없다면
		{
			cerr << "Couldn't open file " << endl;
		}

		const unsigned num_data = 10;
		ofs.write((char*)&num_data, sizeof(num_data));

		for (int i = 0; i < num_data; i++)
			ofs.write((char*)&i, sizeof(i));

		// ofs.close(); 수동으로 파일 닫기, 추가 안해도 범위를 벗어나면서 소멸자가 닫음
	}

	// reading
	if (true)
	{
		ifstream ifs("my_first_file실수.dat");

		if (!ifs)
		{
			cerr << "Cannot open file" << endl;
			exit(1);
		}

		unsigned num_data = 0;
		ifs.read((char*)&num_data, sizeof(num_data));

		for (unsigned i = 0; i < num_data; i++)
		{
			int num;
			ifs.read((char*)&num, sizeof(num));

			cout << num << endl;
		}

	}
	return 0;
}
 

위의 코드를 실행시키면 0부터 9까지 적힌 파일이 만들어진 후, 다시 읽어들여 정상 출력되는 것 을 확인할수있다.

 

18-7 파일의 임의 위치 접근하기

파일 입출력을 할때 항상 순차적으로만 작업할수는 없다. 이미 존제하는 파일의 중간에 필요한 부분을 찾아 일부를 수정할수도 있고,

전체 파일에서 중간에서 필요한 부분만 읽어올수도 있다. 임의 위치 접근방법에 대하여 알아보자.

 

다음 예제는 a부터 z까지 write한 파일을 만든후 읽어온다.

#include <iostream>
#include <fstream>
#include <string>
#include <cstdlib> // exit()
#include <sstream>

using namespace std;

int main(void)
{
	const string filename = "my_file.txt";

	// make a file
	{
		ofstream ofs(filename);

		for (char i = 'a'; i <= 'z'; i++)
			ofs << i;
		ofs << endl;
	}

	// read the file
	{
		ifstream ifs(filename);

		ifs.seekg(5); // 파일 처음로부터 5바이트 이동
		cout << (char)ifs.get() << endl;

		ifs.seekg(5, ios::cur); // 현 위치로부터 다시 5바이트 이동
		cout << (char)ifs.get() << endl;
	}

	return 0;
}
 

결과는 다음과 같다.

처음 위치에서 5칸 이동하니 f가 나왔고, f를 읽은 후(6칸) 다시 5칸 이동하니 l(11칸)이 읽혔다.

또한 총 문자가 몇게인지 궁금하면 다음과 같이 추가해줄수도 있다.

ifs.seekg(0, ios::end); // 마지막 문자의 위치
cout << ifs.tellg() << endl;
 

이번에는 파일을 한번 열어서 읽기도 하고, 쓰기도 하는 방법에 대하여 살펴보자. fstream으로 파일을 초기화 하면 된다.

int main(void)
{
	const string filename = "my_file.txt";

	{
		fstream iofs(filename);

		iofs.seekg(5);
		cout << (char)iofs.get() << endl; // read

		iofs.seekg(5);
		iofs.put('A'); // write
	}
	

	return 0;
}
 

결과는 다음과 같이 f를 출력후 A로 교체하고있다.

우리가 현제 파일을 읽고있음과 동시에, 능동적으로 데이터를 수정해야 하는경우 사용한다.


2) 나의 현황

● 난 이상하게 file I/O가 C언어 때부터 어려웠다. 난 입출력이 포인터보다 어려웠었다. 이번에도 이해는 잘 갔는데 뭐랄까 뇌리에 팍 박히는 느낌이 없달까? 원리를 정확히 알고싶다면 OS과목을 들어봐야할것 같다.

이글의 모든 사진과 내용의 출처는 홍정모 교수님께 있습니다.

홍정모 교수님 블로그:

 

홍정모 연구소 : 네이버 블로그

안녕하세요! 홍정모 블로그에 오신 것을 환영합니다. 주로 프로그래밍 관련 메모 용도로 사용합니다. 강의 수강하시는 분들은 홍정모 연구소 카페로 오세요.

blog.naver.com

댓글