스택과 계산기

이대로 괜찮을까?

5.1) 이대로 괜찮을까?

필자가 작성한 코드 중에 아래와 같은문자열에서 수를 읽어내는 코드가 있다.

07_conversion.cpp

#include <iostream>

// 공용 정의

const int MAX_EXPR_LEN = 256;

typedef std::string Exception;

// 공용 함수

inline void clear_input_buffer() { // 입력받기 전에 입력 버퍼를 비운다

std::cin.clear();

std::cin.ignore(std::cin.rdbuf()->in_avail());

}

inline bool is_digit(char ch) { return ('0' <= ch && ch <= '9'); }

inline bool is_space(char ch) { return (ch == ' ' || ch == '\t' || ch == '\n'); }

// 정수와 연산자 사이에 사이띄개를 넣어 출력하는 프로그램

int main(void) {

try {

char expression[MAX_EXPR_LEN] = "";

std::cin >> expression;

char ch;

char *expr = expression;

// 문자열의 모든 문자 탐색

for (ch = *expr; (ch = *expr) != '\0'; ++expr) {

if (is_digit(ch)) { // 수가 발견된다면 정수 획득

int digit = 0; // 자리수를 임시로 저장할 변수

int value = 0; // 최종적으로 획득할 정수를 저장할 변수

while (is_digit(*expr)) { // 숫자를 획득하는 동안 value 갱신

digit = *expr++ - '0';

value = 10 * value + digit;

}

--expr; // 반복문 재실행 시 ++expr이 수행되므로 한 칸 되돌린다

std::cout << value << ' '; // 획득한 정수를 출력하고 뒤에 공백을 추가

}

else { // 수가 아닌 문자의 경우 출력하고 공백으로 띄운다

std::cout << ch << ' ';

}

}

std::cout << std::endl;

return 0;

}

catch (Exception &ex) {

std::cerr << ex.c_str() << std::endl;

return 1;

}

}

문제없이 잘 동작한다이대로라면 나쁘지 않은 것 같은데그렇게 생각한다면 문제를 내겠다아래에 수의 제곱,두 수의 곱부피를 구하는 함수가 비어있다이를 구현해보라.

08_conversion_skeleton.cpp

#include <iostream>

// 공용 정의

const int MAX_EXPR_LEN = 256;

typedef std::string Exception;

// 공용 함수

inline void clear_input_buffer() { // 입력받기 전에 입력 버퍼를 비운다

std::cin.clear();

std::cin.ignore(std::cin.rdbuf()->in_avail());

}

inline bool is_digit(char ch) { return ('0' <= ch && ch <= '9'); }

inline bool is_space(char ch) { return (ch == ' ' || ch == '\t' || ch == '\n'); }

// 사용 함수

int get_square(const char *ns); // 문자열을 정수로 변환하고 그 제곱을 반환

// 두 문자열을 정수로 변환하고 그 곱을 반환

int get_mul(const char *ns1, const char *ns2);

int get_volume(const char *x, const char *y, const char *z); // 부피 계산 후 반환

int main(void) {

try {

char input1[MAX_EXPR_LEN], input2[MAX_EXPR_LEN], input3[MAX_EXPR_LEN];

std::cout << "Enter number: ";

std::cin >> input1;

std::cout << "Enter number: ";

std::cin >> input2;

std::cout << "Enter number: ";

std::cin >> input3;

std::cout << get_square(input1) << std::endl;

std::cout << get_mul(input1, input2) << std::endl;

std::cout << get_volume(input1, input2, input3) << std::endl;

return 0;

}

catch (Exception &ex) {

std::cerr << ex.c_str() << std::endl;

return 1;

}

}

// 구현

int get_square(const char *ns) { // 문자열을 정수로 변환하고 그 제곱을 반환

/* implement it */

}

// 두 문자열을 정수로 변환하고 그 곱을 반환

int get_mul(const char *ns1, const char *ns2) {

/* implement it */

}

int get_volume(const char *xs, const char *ys, const char *zs) { // 부피 계산 후 반환

/* implement it */

}

필자의 구현은 다음과 같다.

09_conversion_implementation.cpp

int get_square(const char *ns) { // 문자열을 정수로 변환하고 그 제곱을 반환

int digit = 0;

int value = 0;

char ch;

for (ch = *ns; (ch = *ns) != '\0'; ++ns) {

if (is_digit(ch) == false) {

throw Exception("Invalid number");

}

digit = ch - '0';

value = 10 * value + digit;

}

return value * value;

}

int get_mul(const char *ns1, const char *ns2) { // 두 문자열을 정수로 변환하고 그 곱을 반환

int digit = 0;

int left = 0;

char ch;

for (ch = *ns1; (ch = *ns1) != '\0'; ++ns1) {

if (is_digit(ch) == false) {

throw Exception("Invalid number");

}

digit = ch - '0';

left = 10 * left + digit;

}

int right = 0;

for (ch = *ns2; (ch = *ns2) != '\0'; ++ns2) {

if (is_digit(ch) == false) {

throw Exception("Invalid number");

}

digit = ch - '0';

right = 10 * right + digit;

}

return left * right;

}

int get_volume(const char *xs, const char *ys, const char *zs) { // 부피 계산 후 반환

int digit = 0;

int x = 0;

char ch;

for (ch = *xs; (ch = *xs) != '\0'; ++xs) {

if (is_digit(ch) == false) {

throw Exception("Invalid number");

}

digit = ch - '0';

x = 10 * x + digit;

}

int y = 0;

for (ch = *ys; (ch = *ys) != '\0'; ++ys) {

if (is_digit(ch) == false) {

throw Exception("Invalid number");

}

digit = ch - '0';

y = 10 * y + digit;

}

int z = 0;

for (ch = *zs; (ch = *zs) != '\0'; ++zs) {

if (is_digit(ch) == false) {

throw Exception("Invalid number");

}

digit = ch - '0';

z = 10 * z + digit;

}

return x * y * z;

}

 

무엇이 문제인지 알겠는가바로 문자열을 정수로 변환하는 코드가 지나치게 중복적이라는 것이다알고 있겠지만이렇게 코드를 작성하면 가독성이 떨어지고코드 하나가 잘못되었거나 후에 업데이트해야 하는 경우이렇게 작성한 모든 코드를 전부 찾아내 수정해야 하므로 유지 및 보수에 애로사항이 꽃피게 된다어떻게 해결해야할까?

여기서 우리는 atoi 함수처럼문자열에서 정수를 긁어내는 함수를 작성해볼 수 있다.

10_atoi.cpp

int ascii_to_int(const char *ns) { // 문자열을 정수로 변환하고 그 값을 반환

// 중요정수가 아닌 문자가 나타날 때까지 분석을 진행한다

int digit = 0;

int value = 0;

char ch;

for (ch = *ns; (ch = *ns) != '\0'; ++ns) {

if (is_digit(ch) == false) {

break;

}

digit = ch - '0';

value = 10 * value + digit;

}

return value;

}

int get_square(const char *ns) { // 문자열을 정수로 변환하고 그 제곱을 반환

int value = ascii_to_int(ns);

return value * value;

}

int get_mul(const char *ns1, const char *ns2) { // 두 문자열을 정수로 변환하고 그 곱을 반환

int left = ascii_to_int(ns1), right = ascii_to_int(ns2);

return left * right;

}

int get_volume(const char *xs, const char *ys, const char *zs) { // 부피 계산 후 반환

return ascii_to_int(xs) * ascii_to_int(ys) * ascii_to_int(zs);

}

세 함수의 코드 길이가 크게 줄었음을 볼 수 있다또한 함수 이름이 함수가 하는 내용을 암시하고 있으므로함수 내부를 들여다보았을 때 내부적으로 어떻게 동작하는지를 이해하기도 쉽다이렇듯 문자열로부터 정수를 긁어내는 작업은 코드 길이가 길고 반복적으로 사용되기 때문에 반드시 뜯어내야 한다.

정수를 획득하는 코드를 뜯어내야 한다는 사실을 알았으니이것이 적용되지 않은 이전의 예제들도 수정해야 한다는 생각이 든다옳은 판단이고 지금 바로 예제를 수정해보려 한다초반에 작성했던 사칙 연산 계산기 예제부터 수정해볼까다음은 atoi를 이용하여 사칙 연산을 개선한 예제다.

11_basic4_atoi.cpp

int calculate(const char *expr) { // 넘겨받은 식을 계산하여 값을 반환한다

if (is_digit(*expr) == false) { // 입력의 처음이 숫자가 아니라면 예외 처리

throw Exception("타당하지 않은 입력입니다.");

}

int left = ascii_to_int(expr); // 왼쪽에 나타나는 수 획득

while (is_digit(*expr)) { ++expr; } // 정수가 아닐 때까지 expr를 이동한다

if (*expr == '\0') { // 입력이 끝났다면 획득한 정수를 반환하고 종료

return left;

}

// 연산자 획득사칙 연산에 대해서만 다루므로 연산자 길이는 반드시 1

char op = *expr++; // 문자열 포인터가 가리키는 연산자를 획득 후 포인터 이동

int right = ascii_to_int(expr); // 오른쪽에 나타나는 수 획득

while (is_digit(*expr)) { ++expr; } // 정수가 아닐 때까지 expr를 이동한다

if (*expr != '\0') { // 입력이 아직 끝나지 않았다면 예외 발생

throw Exception("두 개의 피연산자로만 연산할 수 있습니다.");

}

// 획득한 값과 연산자를 이용하여 연산

int retVal = 0;

switch (op) {

case '+': retVal = left + right; break;

case '-': retVal = left - right; break;

case '*': retVal = left * right; break;

case '/': retVal = left / right; break;

default: throw Exception("올바른 연산자가 아닙니다.");

}

return retVal;

}

예제를 유심히 보면 이상한 부분이 있다. ascii_to_int 함수는 넘긴 문자열로부터 정수를 읽는 함수다즉 이미 정수를 읽었는데도 불구하고 문자열 포인터에서 다시 정수를 확인하며 지나기고 있다왜 같은 행위를 두 번이나 하는가우리가 이전에 작성했던 예제에서는 정수를 읽고 나면 포인터가 이동해있었는데왜 이 예제에서는 그렇지 않고 우리가 다시 포인터를 옮겨주어야 하는가?

그 이유는 아주 당연한데함수 호출 시에 인자를 넘길 때는 언제나 넘기려는 값의 사본이 넘어가기 때문이다포인터를 넘긴다면 포인터가 저장하는 값의 사본이참조를 넘긴다면 참조의 위치 값의 사본이 넘어간다위 예제에서는 expr 포인터를 넘겼으니 expr 포인터가 가리키는 문자 배열의 주소 값을 사본으로 전달했지만이것이 expr포인터 변수의 주소는 아니라는 것이다. ns는 atoi 함수에서 정의된 지역 변수이고, expr은 calculate 함수에서 정의된 지역 변수이다즉 두 변수에 대해 (ns == expr)은 성립하지만 (&ns == &expr)는 성립하지 않는다. expr의 값을 수정하려면 &expr 또한 함수의 인자로 넘겨야만 한다.

이 문제를 해결하려면 atoi의 인자로 expr의 주소도 같이 넘기던가아니면 atoi의 인자를 expr의 참조로 하던가 해야 한다결국 다음과 같은 식인데 어느 쪽도 그렇게 깔끔하지 않다.

12_atoi_alt.cpp

int ascii_to_int_adr(const char **ns_src) { // 문자열 포인터의 주소를 넘긴다

const char *ns = *ns_src; // 문자열 포인터가 가리키는 값을 획득한다

int digit = 0;

int value = 0;

char ch;

for (ch = *ns; (ch = *ns) != '\0'; ++ns) {

if (is_digit(ch) == false) {

break;

}

digit = ch - '0';

value = 10 * value + digit;

}

*ns_src = ns; // 분석이 끝나면 문자열 포인터 변수에 값을 대입한다

return value;

}

int ascii_to_int_ref(const char *&ns) { // 문자열 포인터의 참조를 넘긴다

int digit = 0;

int value = 0;

char ch;

for (ch = *ns; (ch = *ns) != '\0'; ++ns) {

if (is_digit(ch) == false) {

break;

}

digit = ch - '0';

value = 10 * value + digit;

}

return value;

}

int calculate(const char *expr) { // 넘겨받은 식을 계산하여 값을 반환한다

if (is_digit(*expr) == false) { // 입력의 처음이 숫자가 아니라면 예외 처리

throw Exception("타당하지 않은 입력입니다.");

}

int left = ascii_to_int_adr(&expr); // 왼쪽에 나타나는 수 획득 (주소)

if (*expr == '\0') { // 입력이 끝났다면 획득한 정수를 반환하고 종료

return left;

}

// 연산자 획득사칙 연산에 대해서만 다루므로 연산자 길이는 반드시 1

char op = *expr++; // 문자열 포인터가 가리키는 연산자를 획득 후 포인터 이동

int right = ascii_to_int_ref(expr); // 오른쪽에 나타나는 수 획득 (참조)

if (*expr != '\0') { // 입력이 아직 끝나지 않았다면 예외 발생

throw Exception("두 개의 피연산자로만 연산할 수 있습니다.");

}

// 획득한 값과 연산자를 이용하여 연산

int retVal = 0;

switch (op) {

case '+': retVal = left + right; break;

case '-': retVal = left - right; break;

case '*': retVal = left * right; break;

case '/': retVal = left / right; break;

default: throw Exception("올바른 연산자가 아닙니다.");

}

return retVal;

}

필자는 이에 대한 대안으로 문자열 버퍼 클래스 StringBuffer를 제안한다.

댓글

댓글 본문
작성자
비밀번호
버전 관리
한도영
현재 버전
선택 버전
graphittie 자세히 보기