CPP Module 00
“CPP Module 00에 대하여”
이제 C로 짠 코드는 읽을 수 없는 몸이 되어버렸어요…
Namespaces, classes, member functions, stdio streams, initialization lists, static, const, and some other basic stuf
이름 공간, 클래스들, 멤버 변수들, 표준 스트림들, 초기화자 리스트, 정적(변수), 상수, 그리고 몇 가지 기초적인 것들 … C++의 시작이다. 앞으로 천천히 알아가보는 시간을 가져보도록 하자.
ex00: Megaphone
My Code
#include <iostream>
#include <string>
#include <cctype>
int main(int argc, char **argv)
{
if (argc == 1)
std::cout << "* LOUD AND UNBEARABLE FEEDBACK NOISE *";
else
{
for (int i = 1; i < argc; i++)
{
std::string str = argv[i];
// C++ Manner ?
for (int j = 0; j < static_cast<int>(str.length()); j++)
{
std::cout << static_cast<char>(std::toupper(str[j]));
}
}
}
std::cout << std::endl;
return (0);
}
// ... C Manner ?
for (int j = 0; j < (int) (strlen(str)); j++)
{
printf("%c", (char) (toupper(str[j])));
}
// ...
printf("\n");
** Solve the exercises in C++ Manner **
이야깃거리
진짜 별거 없는데, 사실 메가폰에서 가장 눈에 띄는 문구가 저거다. C++ 매너로 문제 풀라는거.
그런데 내 생각에 C++ Manner라는 말은 너무 모호하다. 그냥 str의 length 메서드 가져다 쓰고 static cast 좀 하고 toupper에 std 붙이고 이런 것들만이 C++ Manner가 아니라는거다. C++에서도 절차 지향 코드 짜고, C 문법 가져다가 쓸 수 있다. 멀티 패러다임 언어니까…
굳이 이런 코멘트를 남기는 이유는, 평가할 때 이것 때문에 소모적인 이야기가 오가는 생각이 들어서다. 차라리 strlen(), toupper() 혹은 명시적 형변환 같은 C Style 코딩을 아예 금지시키는 문구를 써두었으면 더 좋지 않았을까?
ex01: My Awesome PhoneBook
뉴비 분쇄기다.
코드 양이 좀 많아서 내가 이야기하고 싶은 것 위주로 적으면서 필요하다면 코드도 남기도록 하겠다. 앞으로의 모듈 대부분을 이런식으로 진행할 것 같다. 파일이 한두개가 아니다.
위에 메가폰도 C에 가깝다고 생각하면, 사실상 C++로 진행하는 첫번째 Exercise인데 그런 것 치고는 알아야 할 내용이 꽤 많다.
(1) std::string의 기본 생성자는 빈 문자열 ““이다.
과제를 보면 PhoneBook과 Contact의 두 클래스를 만들어야 한다. 정황상 PhoneBook은 전화책이고, Contact는 전화책에 올라간 개개인의 정보 하나를 의미한다.
PhoneBook은 Contact 클래스의 배열을 멤버 변수로 가지고, 최대 8개의 정보를 가질 수 있다. 8번째 이후부터는 배열의 가장 오래된 정보를 대체하게 된다.
Contact 클래스는 first name, last name, nickname, phone number, darkest secret의 5가지 field를 가진다. Field에는 빈 문자열이 존재할 수 없다.
생각해보면 대략 아래와 같은 모양을 가질 것 같고 …
#pragma once
class Contact
{
private:
std::string info[5]; // 5가지 field를 변수로 가지는 Contact 클래스
public:
...
};
class PhoneBook
{
private:
...
Contact contacts[8]; // Contact 클래스의 배열
public:
....
};
#endif
- 과제가 시작하면 일단 아래처럼 개시를 할텐데,
#include "PhoneBook.hpp"
int main(void)
{
PhoneBook phonebook;
// ... implementation
return 0;
}
std::string의 기본 생성자가 빈 문자열 ““이라는 말은, phonebook 안에 있는 contact 배열 각각에 있는 5개의 field들이 빈 문자열로 초기화된다는 말이 되겠다. std::string 또한 일종의 클래스이기 때문에 생성자를 가지고, 빈 문자열로 초기화되도록 설계되어 있다.
int에도 기본 생성자가 있을까? 예를 들어 0으로 초기화될 수 있다고 생각할 수도 있을 것 같다. 그러나 int는 기본 생성자같은 것을 가지고 있지 않다. int는 C, C++의 내장된 기본 자료형이기 때문에 클래스가 아니며 생성자와 소멸자를 가지지 않는다. 따라서 선언 시 값이 초기화되어 있지 않기 때문에, 예상하지 못한 동작을 방지하기 위해서는 초기화 작업이 따로 필요할 것이다.
(2) std::cin과 std::getline
ADD, SEARCH 같은 명령어를 받거나, contact를 채울 정보를 받기 위해서 표준 입력을 받아야 한다. 표준 입력을 받기 위한 수단으로 크게 std::cin과 std::getline을 생각할 수 있는데, 둘의 주요한 차이는 구분자에 있다.
cin은 tab, \n, 공백을 기준으로 입력을 받는다. getline은
std::string name;
std::getline(std::cin, name, '\n');
// std::getline(std::cin, name);
이런 식으로 사용할 수 있는데, 첫 번째 매개변수인 std::cin은 입력을 받을 스트림을 의미하고, 두 번째 매개변수는 입력받은 값을 저장할 변수, 세 번째는 구분자를 의미한다. 구분자를 비워놓으면 기본적으로 ‘\n’을 구분자로 받는다.
- cin의 가장 큰 문제점은 공백을 기준으로 입력받는다는 것에 있다(물론 tab도 문제다). 이것은 그냥 공백이 들어간 이름을 못 받는 것보다는 훨씬 큰 문제라 볼 수 있다. 아마 contact에 들어갈 정보를 채워넣기 위해 연속으로 표준 입력을 받게 될텐데, 이때 문제가 생긴다. 아래 코드를 보자.
#include <iostream>
#include <string>
int main()
{
std::string info[5];
std::string input[5] = {"First Name : ", "Last Name : ",
"Nickname : ", "Phone Number : ", "Darkest Secret : "};
for (int i = 0; i < 5; i++)
{
info[i] = "";
std::cout << input[i];
while (info[i] == "")
{
std::cin >> info[i];
}
}
// 출력
for (int i = 0; i < 5; i++)
std::cout << info[i] << " ";
std::cout << std::endl;
}
❯ ./a.out
First Name : a
Last Name : b
Nickname : c
Phone Number : d
Darkest Secret : e
a b c d e
- 5회에 걸쳐 표준 입력을 정보 배열에 받는 코드이다. a, b, c, d, e를 차례대로 입력했다. 여기까지는 잘 동작하는 것 같은데…?
❯ ./a.out
First Name : a b c
Last Name : Nickname : Phone Number : d
Darkest Secret : e
a b c d e
- 공백을 넣은 채 입력하면 이렇게 버퍼가 밀려서 공백의 갯수만큼 표준 입력이 스킵된다. a를 info[0]에 넣고, 공백을 만나면 다음 표준 입력으로 넘어가고, b를 info[1]에 넣고, 또 c를 info[2]에 넣고, 뒤의 개행까지 공백이 2번 있었기 때문에 표준 입력이 2번 스킵되었다. 작동은 위에서 a ~ e를 하나씩 입력한 것과 동일하다. 이런 식의 동작은 정상적이지 않다. 사람 이름이 “a b c”일수도 있는 거 아닌가? 따라서 우리는 구분자를 개행만으로 제한할 필요가 있고, 그래서 std::getline을 사용해야 한다.
#include <iostream>
#include <string>
int main()
{
std::string info[5];
std::string input[5] = {"First Name : ", "Last Name : ",
"Nickname : ", "Phone Number : ", "Darkest Secret : "};
std::string tmp;
for (int i = 0; i < 5; i++)
{
info[i] = "";
std::cout << input[i];
while (info[i] == "")
{
std::getline(std::cin, tmp);
info[i] = tmp;
}
}
for (int i = 0; i < 5; i++)
std::cout << info[i] << " ";
std::cout << std::endl;
}
❯ ./a.out
First Name : a b c
Last Name : d e f
Nickname : alpha
Phone Number : beta
Darkest Secret : charlie
a b c d e f alpha beta charlie
- 깔끔하게 잘 받아진다.
(3) EOF 처리하기
- 저 상태에서 ctrl d를 눌러 EOF을 알려보자.
❯ ./a.out
First Name : ^D
(block 되어 작동하지 않는 터미널 ... )
- 이 상태에서 꿈쩍도 안할 것이다. 저기에서 엔터 누르면 더 이상 표준 입력도 받지 않는다. 그러면 시그널 보내서 끄는 수 밖에 없다. ctrl d를 눌러서 EOF을 알렸을 때의 처리를 해주지 않아서 그렇다.
goodbit failbit badbit eofbit
좋은놈, 나쁜놈, 이상한 놈
#include <iostream>
#include <string>
int main()
{
std::string tmp = "";
while (1)
{
std::cout << "Howdy : " <<;
std::getline(std::cin, tmp);
std::cout << std::cin.goodbit << ' ';
std::cout << std::cin.badbit << ' ';
std::cout << std::cin.eofbit << ' ';
std::cout << std::cin.failbit << std::endl;
std::cout << std::cin.good() << ' ';
std::cout << std::cin.bad() << ' ';
std::cout << std::cin.eof() << ' ';
std::cout << std::cin.fail() << std::endl;
std::cout << "input : " << tmp << std::endl;
}
}
❯ ./a.out
Howdy : hi
0 1 2 4
1 0 0 0
input : hi
Howdy :
이게 뭔가 싶을 것인다. 요 4인방은 std::ios_base의 멤버 변수로 스트림의 상태를 나타낸다. 더 자세한 입출력의 정수를 알고 싶다면 C++ IOstream (입출력) 라이브러리를 참고하자.
주목해야 할 부분은 std::cin.good(), bad(), eof(), fail()의 4가지 함수이다. 각각 goodbit, badbit, eofbit, failbit가 켜져있는지 검사한다. 정상적인 입출력이 이루어지고 있는 경우 goodbit가 켜져 있다. 위 예시의 출력에서는 goodbit가 켜져있다. 여기서 ctrl d를 쏴보자.
Howdy : 0 1 2 4
0 0 1 1
input :
Howdy : 0 1 2 4
0 0 1 1
input :
Howdy : 0 1 2 4
0 0 1 1
input :
Howdy : 0 1 2 4
0 0 1 1
input :
(무한히 반복됨)
...
- 난리도 아닐 것이다. goodbit 이외의 다른 bit들은 일반적으로는 관측되지 않는다.
… failbit은 보통 입력 작업 시 내부적인 논리 오류로 인해 발생되는 오류, 예컨대 입력 받기를 기대하였던 값이 오지 않을 때 (파일에 접근할 수 없다던지..) 설정되므로, failbit 이 설정되더라도 스트림의 다른 입출력 작업들은 가능하다. 반면에 badbit 의 경우 스트림의 완전성(integrity)이 깨질 때, 쉽게 말하면 하나의 스트림에 동시의 두 개의 다른 작업이 진행될 때 발생되는 것이므로 badbit 이 설정되면 다른 입출력 작업들은 할 수 없게 된다. badbit 은 bad 함수를 통해 상태를 독립적으로 확인할 수 있다 … (출처 : C++ 레퍼런스 - ios::fail 함수)
- ctrl d를 받게 되면 eofbit와 failbit가 둘 다 켜지고 그 상태로 계속해서 표준 입력을 받는다. while 1이라면 시그널을 보내서 끌때까지, for 문을 다섯 번 돌려서 표준 입력을 받는 코드를 짰으면 5번, 하여튼 코드가 끝날 때까지 계속해서 받는다. 각각의 함수들(good(), bad() …)은 비트를 검출하는 기능만 하지 버퍼에 무언가 처리를 하지 않기 때문에 failbit, eofbit가 켜진 채로 계속 입력을 받기만 한다. 따라서 우리가 수동으로 처리해야한다. EOF니까 프로그램을 종료하면 되겠다.
#include <iostream>
#include <string>
int main()
{
std::string info[5];
std::string input[5] = {"First Name : ", "Last Name : ",
"Nickname : ", "Phone Number : ", "Darkest Secret : "};
std::string tmp;
for (int i = 0; i < 5; i++)
{
info[i] = "";
std::cout << input[i];
while (info[i] == "")
{
std::getline(std::cin, tmp);
if (std::cin.eof())
{
exit(0);
}
info[i] = tmp;
}
}
for (int i = 0; i < 5; i++)
std::cout << info[i] << " ";
std::cout << std::endl;
}
❯ ./a.out
First Name : %
- 잘 꺼진다.
(4) 선 / 후행 공백 버리기
사람 이름 중간에 공백이 있을 수 있어도, 선후행에 공백이 있을 수는 없는 법이다. 그래서 나는 입력에 들어온 선후행 공백을 전부 제거했다.
커맨드도 마찬가지다. 그래서 “ ADD”와 “ADD “, “ ADD “ 모두 “ADD”로 처리되도록 했다.
선행 공백 제거, std::ws
- 아래와 같이 스트림 내에서 호출한다. 스트림에서 입력값을 받은 후, 선행으로 들어온 공백(man isspace(3))을 제거한 후 문자열에 저장한다. 이른바 스트림 변환 메서드이다.
...
std::getline(std::cin >> std::ws, tmp);
...
후행 공백 제거, std::substr
- 우리가 알고 있는 그 substr 맞다. 나는 후행 공백을 제거하기 위해, 비트 체크 이후에 대략 다음과 같은 함수를 만들어 썼다.
std::string ridrspace(std::string tmp)
{
int len = tmp.length();
while (tmp[len - 1] == ' ' || tmp[len - 1] == '\t')
len--;
return (tmp.substr(0, len));
}
- 이 둘을 적용시킨 코드는 아래와 같다.
#include <iostream>
#include <string>
int main()
{
std::string info[5];
std::string input[5] = {"First Name : ", "Last Name : ",
"Nickname : ", "Phone Number : ", "Darkest Secret : "};
std::string tmp;
for (int i = 0; i < 5; i++)
{
info[i] = "";
std::cout << input[i];
while (info[i] == "")
{
std::getline(std::cin >> std::ws, tmp); // 선행 공백 제거
if (std::cin.eof())
{
exit(0);
}
std::string new_tmp = ridrspace(tmp); // 후행 공백 제거
info[i] = new_tmp;
}
}
for (int i = 0; i < 5; i++)
std::cout << info[i] << " ";
std::cout << std::endl;
}
(5) TAB 제한
- tab은 아스키 상에서는 ‘\t’로 한 글자지만, 화면 상에서는 4칸을 차지하게 된다. 저것 때문에 나는 Contact를 받을 때 tab을 입력하면 프로그램을 끄도록 했다.

- tab을 받아버리면 어떻게 되느냐. 예를 들어 아래처럼 입력을 받았다고 해보자.
❯ ./a.out
First Name : tab tab
Last Name : lastname
Nickname : nickname
Phone Number : none
Darkest Secret : none
- 그럼 아마 아래처럼 나올 것이다.
index|first name| last name| nickname
1| tab tab| s| d
- 저기 “tab tab”은 10글자가 맞다. 좌클릭 꾹하고 긁어봐도 저 tab 사이의 공백은 한칸이다. 한 칸이 10칸이어야 한다는 제약 사항 자체에는 위배되지 않았는데 미관상 안예뻐보이는 건 사실이다. 나는 tab이 입력되는 것을 막기 위해 후행 공백 제거 이후, 문자열을 전부 검사하여 ‘\t’가 있으면 프로그램을 꺼버렸다. 지금 와서 생각해보면 바로 끄지 말고, 그 부분만 표준 입력을 다시 받도록 하는게 더 예뻤을 것 같다고 생각한다.