Brise

[C++ 기본2] 5. OOP(Stack example) 본문

프로그램/C,C++

[C++ 기본2] 5. OOP(Stack example)

naudhizb 2022. 2. 24. 00:15
반응형

절차적 프로그래밍과 OOP의 차이를 알아보기 위하여 Stack 예제를 다루어 보자.

// 5_Stak1 - 60 page ~
#include <iostream>

// 스택을 만들어 봅시다.
// 버전 1. C언어
// 단점 : 스택이 2개 이상 필요하다면....
int buff[10];
int idx = 0;

void push(int a) { buff[idx++] = a;}
int  pop()       { return buff[--idx]; }

int main()
{
    push(10);
    push(20);
    std::cout << pop() << std::endl;
}

C에서 간단하게 Stack을 구현한다면 위와 같이 스택을 구성하게 될 것이다.
이 경우 사용이 간단하지만, 만약 스택을 여러개 써야 한다면 각각 변수와 함수를 따로 구현하여야 한다.

// 5_Stak1 - 60 page ~
#include <iostream>

// 스택을 만들어 봅시다.
// 버전 2. C언어
//          관련된 데이타를 묶어서 "Stack"타입을 설계하자

struct Stack
{
    int buff[10];
    int idx;
};
void push(Stack* s, int a) { s->buff[(s->idx)++] = a; }
int  pop(Stack* s)         { return s->buff[--(s->idx)]; }
int main()
{
    Stack s1; s1.idx = 0;
    Stack s2; s2.idx = 0;

    push(&s1, 10);
    push(&s1, 20);
    std::cout << pop(&s1) << std::endl;
}

이를 헷지하기 위하여 데이터와 컨텍스트를 묶어 구조체로 설계하고 함수가 컨텍스트를 가진 구조체 포인터를 인자로 받도록 변경하면 여러 개의 스택을 사용 할 수 있다.
하지만, push와 pop이라는 함수 이름이 전역적으로 예약 되기 때문에 사용 상의 혼동이 발생할 수 있어 오류의 가능성이 높아진다.

#include <iostream>

// 버전 3. C++언어
// 캡슐화 : 상태를 나타내는 데이타와 상태를 조작하는 함수를 
//            하나의 구조체 안에 묶어 놓는다.
struct Stack
{
    // 멤버 데이타
    int buff[10];
    int idx;

    // 멤버 함수
    void push(int a)      // void push(Stack* this, int a)
    {
        buff[idx++] = a; //  this-> buff[this->idx++] = a
    }
    int  pop()       { return buff[--idx]; }
};
int main()
{
    Stack s1; s1.idx = 0;
    Stack s2; s2.idx = 0;

    // push(&s1, 10);
    s1.push(10);// push( &s1, 10)
    s1.push(20);
    std::cout << s1.pop() << std::endl;
}

이를 위하여 C++언어에서는 특정 구조 안에 함수를 묶을 수 있다.
스택 구조체 안에 멤버함수를 넣음으로써, 혼동 없이 스택 구조체에 대한 연산을 수행 할 수 있다.
이에 더하여 멤버함수를 선언하는 경우 일전의 예제에서 스택 포인터를 인자로 선언한 것과 달리 묵시적으로 인자를 넣은 것과 같은 효과를 가지게 된다.
중요한 점은 이 예제에서 멤버 함수와 외부에서 구조체의 데이터에 자유롭게 접근 할 수 있다는 사실이다.

#include <iostream>

// 버전 4. 정보 은닉 - "잘못 사용하기 어렵게 설계해라"
//            외부의 잘못된 사용으로 객체의 상태가 불안해
//            지는 것을 막는다.

// private, public : 접근 지정자

//struct Stack  // 접근 지정자 생략시 디폴트가 public
class Stack     // 접근 지정자 생략시 디폴트가 private
{
private:            // 멤버 함수에서만 접근 가능
    int buff[10];    // 멤버가 아닌 함수는 접근 안됨
    int idx;

public:                // 모든 함수에서 접근가능
    void init()      { idx = 0; }
    void push(int a) { buff[idx++] = a;     }
    int  pop()       { return buff[--idx]; }
};
int main()
{
    Stack s1; 
    //s1.idx = 0; // error
    s1.init();
    s1.push(10);
    s1.push(20);

    //s1.idx = 100;

    std::cout << s1.pop() << std::endl;
}

위의 예제에서는 외부 프로그램이 데이터를 조작하지 못하도록 private 키워드를 사용하여 멤버 변수를 접근하지 못하도록 하였다.
그리고 이 때문에 초기값을 설정하지 못하게 되었으므로, 초기값을 설정할 수 있도록 init함수를 만들게 되었다.
이 경우 만약 유저가 깜빡 잊고 init함수를 호출하지 않는다면 런타임에서 예측하지 못한 동작을 수행하게 된다.

#include <iostream>

// 버전 5. 객체 초기화를 자동으로 - 생성자 문법

class Stack 
{
    int buff[10];
    int idx;
public:            
    // 생성자 : 클래스 이름과 동일한 함수.
    //           반환타입을 표기 하지 않는다.
    //            객체를 생성하면 자동으로 호출된다.
    Stack() { idx = 0; }

    void push(int a) { buff[idx++] = a; }
    int  pop() { return buff[--idx]; }
};
int main()
{
    Stack s1;
    //s1.init();
    s1.push(10);
    s1.push(20);

    std::cout << s1.pop() << std::endl;
}

휴먼 에러를 방지하기 위하여 객체 생성 시에 생성자를 이용하여 자동으로 함수가 불리도록 설정할 수 있다.

#include <iostream>

// 버전 6. 자료구조의 변경 - 동적 메모리 할당 사용
//         소멸자 도입. 소멸자에서 객체가 사용하던 자원 해지
//            소멸자는 꼭 만들어야 하는 것이 아니라 필요한 경우만
//            만들면 된다.

class Stack
{
    int* buff;
    int idx;
public:
    Stack(int size = 10) 
    { 
        buff = new int[size];
        idx = 0; 
    }

    // 소멸자 : 클래스 이름앞에 ~ 가 붙는 함수
    //           객체 파괴시 자동으로 호출된다.
    ~Stack() { delete[] buff; }


    void push(int a) { buff[idx++] = a; }
    int  pop() { return buff[--idx]; }
};
int main()
{
    //Stack s1; // 디폴트값 10 사용

    Stack s1(100); // 객체를 만들면 생성자 호출
                    // 생성자에 인자로 100 전달

    s1.push(10);
    s1.push(20);

    std::cout << s1.pop() << std::endl;
}

스택의 사이즈를 동적으로 조절하기 위해서는 인자로 사이즈를 받아 스택을 동적으로 할당하는 방법도 있다. 그리고 객체의 사용이 끝나면 delete를 이용 소멸자를 호출하여 자원을 정리한다.

#include <iostream>

// 버전 7. 선언과 구현의 분리. 파일 분할
// Stack.h
/*
class Stack
{
    int* buff;
    int idx;
public:
    // 클래스 안에는 멤버함수의 선언만 제공.
    Stack(int size = 10);
    ~Stack();
    void push(int a);
    int  pop();
};
// Stack.cpp
// 멤버 함수의 외부 구현
Stack::Stack(int size )
{
    buff = new int[size];
    idx = 0;
}
Stack::~Stack() { delete[] buff; }

void Stack::push(int a) { buff[idx++] = a; }
int  Stack::pop() { return buff[--idx]; }

*/

// 클래스 사용자는 헤더만 포함하면 됩니다.
#include "Stack.h"

int main()
{
    Stack s1(100); 

    s1.push(10);
    s1.push(20);

    std::cout << s1.pop() << std::endl;
}

일반적으로는 위와 같이 Stack.h에 선언을, Stack.cpp에 구현을 하여 파일을 분할하여 사용한다.
개인적으로는 이 부분이 C언어와 큰 차이를 가진다고 생각했는데, 그 이유는 C에서는 대부분 헤더 없이도 내용의 추론이 가능한 반면 C++에서는 헤더와 소스 둘을 모두 보지 않으면 전체 동작 흐름이 예상이 안되는 경우가 많았던 것 같다.
실제 내용을 본다고 하더라도, 대부분은 소스를 보는 것보다 설계 문서를 보는 것이 파악이 더 쉬웠던 듯 하다.

#include <iostream>

// 버전 8. Stack.h 참고

// 버전 9. 템플릿 도입

/*
// StackT.h  에 복사해 놓으세요
template<typename T>
class Stack
{
    T* buff;
    int idx;
public:
    Stack(int size = 10)
    {
        buff = new T[size];
        idx = 0;
    }
    ~Stack() { delete[] buff; }

    void push(const T& a) { buff[idx++] = a; }
    T  pop() { return buff[--idx]; }
};
*/
#include "StackT.h"

int main()
{
//    square<int>(10);
//    square(10);

    Stack<int> s1(100);
    s1.push(10);
    s1.push(20);
    std::cout << s1.pop() << std::endl;
}

앞의 예제에서 좀 더 확장하여 다른 타입을 쓰고 싶은 경우에는 위와 같이 템플릿을 이용할 수도 있다. 템플릿의 사용법에 대해서는 일전의 글을 참고해보자.

#include <iostream>
//#include "StackT.h"
#include <stack>  // C++ 표준 스택

int main()
{
    std::stack<int> s;
    s.push(10);
    s.push(20);
    std::cout << s.top() << std::endl; //꺼내기만
    s.pop(); // 제거만, 반환은 안함(void)
    std::cout << s.top() << std::endl;
}

// 구글에서   "tensorflow github"

// tensorflow/core/platform/

C++의 강력한 점 중의 하나는 C보다 월등하게 많은 부분들이 기본 라이브러리로 구현되어 있다는 것이다. 대부분의 자료구조나 알고리즘 책에서 보아왔던 구조체는 대부분 있을 것이다. 이 때문에 개발자는 상세한 자료구조를 처음부터 만들기보다 기존의 있던 부분을 활용하여 실 응용 개발에 많은 시간을 할애할 수 있다.

반응형
Comments