Brise
[C++ 기본2] 5. OOP(Stack example) 본문
절차적 프로그래밍과 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보다 월등하게 많은 부분들이 기본 라이브러리로 구현되어 있다는 것이다. 대부분의 자료구조나 알고리즘 책에서 보아왔던 구조체는 대부분 있을 것이다. 이 때문에 개발자는 상세한 자료구조를 처음부터 만들기보다 기존의 있던 부분을 활용하여 실 응용 개발에 많은 시간을 할애할 수 있다.
'프로그램 > C,C++' 카테고리의 다른 글
[C++ 기본 2] 8. 생성자 (0) | 2022.03.02 |
---|---|
[C++ 기본 2] 7. 접근지정자 (0) | 2022.03.02 |
[C++ 기본 2] 6. STL (0) | 2022.03.02 |
[C++ 기본 2] 4. OOP(Object Oriented Programming) (0) | 2022.02.23 |
[C++ 기본 2] 3. example(reference, template) (0) | 2022.01.29 |
[C++ 기본 2] 2. memory allocation (0) | 2022.01.29 |
[C++ 기본 2] 1. reference (0) | 2022.01.29 |