Search

CPP Module 01

Created
2021/10/07
tag
42서울
42Seoul
CPP Module
Class
Member Function
Standard I/O Stream

Subjects

1. ex00 (BraiiiiiiinnnzzzZ)

Static Member Function

자유 공간으로부터 할당 받는 newZombie와 오토매틱으로 두면서 스택 프레임이 제거될 때 함께 해제되는 randomChump의 함수를 선언하고 정의해야 한다. 각 함수는 정해진 .cpp 파일에 위치해야 하는데, 개인적으로는 두 함수를 모두 static 멤버 함수로 선언하는 것이 더 적절하다고 생각하여 Zombie 클래스 안으로 넣었다. 이를 통해 Zombie 클래스와 각 함수 간의 연관성을 더 높일 수 있었고, Zombie 객체를 만들지 않아도 newZombierandomChumpZombie 클래스를 통해 호출할 수 있게 만들었다.

Random Name

std::string Zombie::randomName(void) { static std::string characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; std::string temp(10, '\0'); std::string::iterator iter = temp.begin(); std::string::iterator end = temp.end(); while (iter != end) *iter++ = characters.at(static_cast<int>(Random::randr(0.0, 61.0))); return (temp); }
C++
복사
추가적인 코드 작성에 제약이 없기 때문에 Random.cppRandom.hpp를 작성하여 Zombie생성자를 호출 할 때 무작위 이름을 가질 수 있도록 구현했다. a-z, A-Z, 0-9의 문자들을 무작위로 10개를 선정하여 이름을 만들었고, Random 자체는 선형 합동 알고리즘에 기반하여 작성했다. <random>C++부터 이용할 수 있기 때문에 이와 같은 방법을 이용하였다. 이에 대해선 아래 링크의 Random Function Implementation 항목에 자세하게 작성해두었다.

2. ex01 (Moar brainz!)

Initialization

이전 항목과 큰 차이가 없으나 자유 공간에서 할당 받는 Zombie 객체가 단일이 아니라는 점에 유의해야 한다. new 연산자를 이용하여 한 번에 여러 공간을 할당 받고, 반복문을 이용하여 이름을 정할 수 있도록 만들어야 한다. 생성자 안에서 announce 함수를 호출하도록 만든 경우에는 이를 다른 곳으로 적절하게 옮길 필요가 있다.

3. ex02 (HI THIS IS BRAIN)

원시 포인터 자체를 다뤄야 하는 상황이 아니라면 참조자를 사용하는 것이 더 좋다. 참조자를 이용하면 (스택 프레임이 달라지지 않는 한) 컴파일러의 Aliasing Rule에 기반하기 때문에 메모리를 추가적으로 소모하지도 않고, 읽는데 불필요한 기호들이 없어 가독성을 높이는데 충분하다.
다만 참조자의 동작이 포인터와 비슷한 참조로 이뤄진다고 해서, 포인터참조자가 동일한 것은 아니므로, 참조자를 잘 활용하기 위해선 정확한 개념이 뒷받침 되어야 한다. 참조자는 아래 링크에 자세히 정리해두었다.

4. ex03 (Unnecessary violence)

Pointer ? Reference ?

구현 자체는 이전 문제들보다 오히려 쉽거나 비슷하다. 다만 문제에서 요구하는 바는 HumanA 클래스와 HumanB 클래스를 정의할 때, Weapon을 어느 클래스에 참조자 혹은 포인터로 정의하는 것이 바람직한 것인지 고민하는 것이다.
문제에 따르면 HumanA 객체는 생성 시에 Weapon을 갖고 있는 채로 생성이 되어야하고, HumanB 객체는 Weapon을 소지할 수는 있지만 생성 시에 갖고 있지는 않는다는 것을 알 수 있다. 주어진 main 함수의 각 객체의 생성자 호출을 통해 각 생성자의 인자들을 확인할 수 있는데, 위에서 제시된 참조자에 대한 글을 읽었다면 참조자는 생성 초기에만 정의될 수 있다는 사실을 통해 답을 유추할 수 있다.
HumanB 객체처럼 생성자Weapon을 인자로 받지 않을 때 Weapon이라는 멤버 변수가 참조자라면, 해당 멤버 변수를 정의하는 것이 불가능하여 컴파일 조차 되지 않는다. 따라서 HumanB 클래스의 Weapon포인터가 적절하고, 자연스럽게 HumanA 클래스는 포인터 혹은 참조자 중 어느 것을 사용해도 되므로 참조자가 더 적절한 것을 알 수 있다.

5. ex04 (Sed is for losers)

Constructor of std::ifstream & std::ofstream

std::ifstreamstd::ofstream생성자 인자로 파일 이름을 할당하는 것이 가능하고, 이 덕분에 멤버 함수 open이라는 함수를 아낄 수 있다. 다만 파일에 대한 스트림이 잘 열렸는지 확인을 위해 good 멤버 함수를 호출하는 것이 필요하고, 스트림을 이용한 작업이 끝났다면 close 멤버 함수를 호출하는 것도 잊지 말아야 한다.
std::ifstreamstd::ofstream으로 파일을 열 때는 ios_base::openmode 타입의 Flag 값을 설정할 수 있는데, std::ifstreamstd::ifstream::in 그리고 std::ofstreamstd::ofstream::out이 기본 값으로 설정되어 있다. 만일 바이너리 모드로 읽어야 하거나, 데이터를 모두 날리고 작성해야 하거나, 기존 데이터를 유지하고 이어서 작성을 해야 한다면 별도의 Flag들을 명시해야 한다.

rdbuf & std::stringstream

std::ifstream의 데이터를 갖고 오는 방법은 여러가지가 있다. 스트림 상의 데이터를 바이트 단위로 읽어서 Whitespace 단위로 끊은 뒤에 원하는 타입으로 읽어올 수도 있고, 바이너리 단위로 읽어서 문자 타입으로 읽을 수도 있다. 전자의 경우는 기본적으로 텍스트 모드로 동작하는 스트림을 istream_iterator<T>를 이용하여 데이터를 읽어오고, 후자의 경우는 바이너리 모드로 동작하는 스트림을 istreambuf_iterator<T>를 이용하여 데이터를 읽어온다.
만일 입력받은 데이터를 정수, 실수, 혹은 사용자가 원하는 타입으로 읽어오거나 이를 적절히 변환하고 싶을 떄는 텍스트 모드의 istream_iterator<T>>> 라는 추출 연산자를 이용하면 정말 편리하게 읽어올 수 있다. 다만, 이 방법은 데이터의 변환에 취약하다. 스트림으로부터 되찾은 데이터들을 구분지으려면 직접 Whitespace를 데이터에 붙여야 하며, Whitespace 단위로 데이터를 구분짓기 때문에 여러 개의 Whitespace가 있었다면 이를 원래 모습으로 붙여내기는 어렵다. 따라서 서브젝트에서 원하는 replace 작업은 정말 쉽게 할 수 있지만, Whitespace에 대한 고려가 되지 않으므로 istream_iterator<T>를 이용하는 방식은 그리 적절하지 않다.
바이너리 모드로 읽어올 수 있는 istreambuf_iterator<T>는 스트림의 파일 버퍼에 직접 접근할 수 있고, 텍스트 모드와 달리 바이트 단위의 변환이 없기 때문에 istream_iterator<T>보다 빠르게 작업을 처리할 수 있다. 더군다나 Endian만 동일하다면 버퍼에서 읽어오는 데이터의 변환에 대해서도 걱정을 할 필요가 없다. 다만istream_iterator<T>에서는 어느 타입으로든 데이터 반환이 가능했다면, istreambuf_iterator<T>는 문자 타입으로만 반환이 가능하다는 것인데 여기서는 별 문제가 되지 않으니 istreambuf_iterator<T>를 쓰는 것이 더 적절해 보인다.
기본적으로 std::ifstream을 읽어오는데 사용되는 두 iterator는 파일 탐색을 기반으로 한다. 파일은 디스크에 위치하고 있기 때문에 해당 버퍼를 반복적으로 불러와야 하는 상황에서는 꽤나 큰 단점으로 작용할 수 있다. 따라서 이런 경우에는 파일에 대한 스트림이 갖고 있는 버퍼의 내용을 모두 메모리 상의 버퍼로 옮겨서 작업을 하는 것이 훨씬 적절하다. 이를 가능하게 해주는 것이 std::stringstream이다. std::ifstream의 데이터를 std::stringstream으로 갖고 올 때는 제시된 2가지 iterator를 활용할 수 있지만, 스트림 간의 데이터 이동은 rdbuf라는 멤버 함수를 이용하면 한 번에 데이터를 옮기는 것이 가능하다. rdbufbasic_filebuf라는 객체의 주소를 반환하는데 이를 << 라는 삽입 연산자와 함께 사용하면 한 번에 입력할 수 있다.
물론 이번 서브젝트에서는 1회성으로 파일을 읽어오면 되므로 지금 소개된 버퍼를 옮기는 작업을 하지 않고 istreambuf_iterator<T>를 이용해도 무방하다.
std::ifstreamrdbuf 멤버 함수를 통해 얻어낸 basic_filbuf 객체의 주소를 <<std::stringstream에 삽입했다면, std::ifstream의 모든 데이터가 메모리 버퍼로 이동되었을 것이다. 이 때 std::stringstreamstr 멤버 함수를 이용하면 std::string으로 변환이 가능하므로 이를 std::string 객체에 저장하여 replace를 진행하면 된다.

replace

위 과정에서 파일 내용을 std::stringstream에 담은 후, std::stringstream 내의 데이터를 str 멤버 함수를 통해 std::string으로 불러왔다면 replace 자체는 std::string의 멤버 함수를 이용하면 되므로 큰 문제가 되지 않는다. 방법은 여럿 있겠지만 std::stringfindsubstr이라는 멤버 함수 2개를 이용하여 마칠 수 있다. 참고로 std::string의 멤버 함수들은 대체적으로 iterator 형식이 아닌 index 값과 관련있다.
초기 값 0을 갖고 있는 indexfind 멤버 함수에 지속적으로 이용한다. find 멤버 함수를 통해 s1이 위치한 index를 찾아내고, 반환된 값을 index에 할당하여 그 값이 std::string::npos와 동일한지 확인한다. 만일 동일하지 않다면 s1을 찾은 것이므로, index를 이용하여 파일 내용에서 s1 이전 부분과 s1 이후 부분을 substr 멤버 함수로 만들어 s2 양 옆에 붙인다. 이후 반복되는 로직에서는 찾아 놓은 indexfind 멤버 함수에 지속해서 개입하기 때문에 해당 지점부터 이어서 작업을 할 수 있게 되어, 반복 회차마다 처음부터 찾을 필요가 없다.
substr 멤버 함수가 O(n)O(n)으로 생각보다 비싼 것이 아쉬운 점이다. 다만 Modern C++에서는 std::string_view라는 참조형 읽기 전용 클래스가 있다. std::string과 거의 동일한데, 참조형 읽기 전용이라 substr 멤버 함수가 굉장히 싸다. 해당 클래스의 substr은 문자열을 새롭게 만들지 않고, 참조형 읽기 전용 클래스인 std::string_view를 주기 때문에 O(1)O(1)로 처리할 수 있다. 여기서 얻은 std::string_viewdata 멤버 함수를 통해 std::string을 얻을 수 있다.

File Out

파일을 읽어올 때 std::ifstream을 이용했다면, 파일을 쓸 때는 std::ofstream을 이용하면 된다. 어차피 새로운 파일에 저장을 할 것이므로, std::ofstream::trunc는 선택 사항이 된다. std::ifstream을 이용했을 때처럼 파일이 잘 열렸는지 good 멤버 함수로 잘 확인한 후에 << 라는 삽입 연산자를 활용하여 데이터를 파일에 쓸 수 있도록 하자.

6. ex05 (Karen 2.0)

Array of Member Function Pointer

Logging Level에는 4가지가 고정적으로 존재하며, 문제에서 제시한 것처럼 if / else if / else 의 배치보다는 멤버 함수 포인터를 이용하는 것이 요구된다. 이는 아예 if 문을 사용하지 말라는 것이 아니라 각 Logging Level의 비교 시 if 문을 마구잡이로 나열하지 말라는 것으로 해석하면 된다. 따라서 기존에는 4개의 Logging Level에서 if / else if / else if / else가 요구되었다면, 멤버 함수 포인터를 담는 배열과 std::string을 담는 배열을 활용 하면 loop / if 만으로 Logging Level에 맞는 멤버 함수를 호출하는 것이 가능하다.

7. ex06 (Karen-filter)

Finding Level

이전 문제에서 크게 벗어날 필요 없이 멤버 함수 포인터를 담은 배열과 std::string을 담은 배열에 Filtering을 위한 함수와 요소를 각각 추가하면 된다. 지정된 Level 이상의 Logging들을 출력해야 하므로, 현재 Level을 찾은 뒤에 Filtering만 아니라면 loop를 통해 찾은 Level 이상의 Logging들을 간단히 출력할 수 있다.