stack을 만들면서 객체지향에 대해 알아보자 😁
안녕하세요! 두두코딩 널두 🥸 입니다 ✋
오늘은 객체지향 프로그램을 만들어보겠습니다!
🖇 소스코드에 마우스를 올리고 copy 버튼을 누를 경우 더 쉽게 복사할 수 있습니다!
궁금한 점, 보안점 남겨주시면 성실히 답변하겠습니다. 😁
+ 감상평 댓글로 남겨주시면 힘이됩니다. 🙇
Intro.
이번 포스팅에서는 C++ 객체지향 개념을 알아보기 위한 Stack with OOP #1을 알아보겠습니다. C++언어의 특징을 활용해 Stack을 만들어볼 것이며, 포스팅이 길어질 수 있어 나눠 기록하고자 합니다.
새롭게 등장하는 keyword는 뒷 포스팅에서 자세하게 다룰 것이라, Stack을 만들어보는 포스팅에서는 “아! 이런것도 있구나..” 하는 마음가짐으로 접근하면 좋을 것 같습니다!
Stack 프로그램 만들기 [1]
우선, Stack이라는 개념을 알아야한다.
간단히 설명하면, Stack은 LIFO (Last in First Out) 마지막에 넣은 값을 가장 처음 꺼내줄 수 있는 자료구조를 말한다. 해당 문장만으로 Stack을 이해하기 어려운 분들은 구글링해 찾아보면 좋을 것 같다.
#include <iostream>
// stack이라는 데이터 자료구조에 데이터를 넣을 수 있는 push(),
// 출력할 수 있는 pop()가 필요함.
int main()
{
push(10);
push(20);
push(30);
// 가장 마지막 값인 30이 출려되어야한다.
std::cout << pop() << std::endl;
}
가장 쉽게 생각해 Stack을 만들 수 있는 방법은 무엇일까?
바로 배열을 활용해 구현하는 것이다. 전역에 배열을 하나 만들고, index 값을 두어 Stack 자료구조를 만들어보자.
#include <iostream>
int buf[10];
int idx = 0;
// 넣고 idx를 증가시켜야되기 때문에 후위 연산
void push(int n) { buf[idx++] = n; }
// idx는 값을 가리키고 있는 것이아니라, 넣어야될 부분을 가리키고 있기 때문에
// idx 포인터를 내리고, 출력할 값을 return해야함. 즉, 전위 연산 필요.
int pop() { return buf[--idx]; }
int main()
{
push(10);
push(20);
push(30);
std::cout << pop() << std::endl;
}
위의 코드와 같이 전역에 배열을 만들고, Stack에 필요한 index를 활용해 관리하게 될 경우 Stack에 데이터 입/출력을 쉽게 할 수 있다.
하지만 여기서 생각해보자.
Stack을 2개 만들고자 한다면 어떻게 될까?
지금은 전역영역에 Stack을 하나 만들어 관리했다. 2개가 될 경우 하나 더 만들어 사용하면 된다. 예를들어 10개의 Stack을 사용할경우에는…
특히, push()
, pop()
에게 어떤 Stack인지와 index가 누구것인지를 같이 전달해야된다. 아래와 같이 인자 값이 늘어나 더 복잡해질 것이다.
// 아래와 같이 push와 pop이 전역의 stack이 누구것인지 알아야하기때문에 복잡해짐.
void push(int* s, int* idx, int n) { s->buf[*idx++] = n; }
int pop(int* s) { return s->buf[--(*idx)]; }
위와 같이 전역으로 모든걸 해결하려고 할 경우 복잡해지고, 코드가 길어지는 문제가 있다. 이를 좀 더 개선해보자.
Stack 프로그램 만들기 [2]
우리는 이전 포스팅을 통해 OOP의 가장 먼저 할 일은 “필요한 타입을 먼저 설정하자” 라는 것을 배웠다. 앞서 배운 예시는 2개의 int 타입을 묶어 복소수를 표현하는 struct Complex
새로운 타입을 만들었다.
Stack도 배열과 인덱싱 포인트를 한데 묶어 자료구조화하면 관리가 더 편리하다.
#include <iostream>
struck Stack{
int buf[10];
int idx = 0;
};
void push(Stack* s, int n) { s->buf[s->idx++] = n; }
int pop(Stack* s) { return s->buf[--(s->idx)]; }
int main() {
Stack s1, s2;
s1.idx = 0;
s2.idx = 0;
push(&s1,10);
push(&s1,20);
push(&s1,30);
// 30 출력 (가장 마지막 데이터)
std::cout << pop(&s1) << std::endl;
}
위의 코드와 같이, struct Stack
을 활용하면 다수의 Stack을 만들 때 좀 더 관리가 편하다.
혹시, 위 코드에서 push
와 pop
의 내용이 좀 복잡하다고 느껴지지 않는가?
우선, 인자로 Stack의 정보를 받아야하고, 해당 Stack 내 데이터에 접근하기 위해 ->
연산을 활용해야된다는 점이 복잡하기도 하고, 코드상 보기도 별로이다.
복잡한 코드가 나온 원인을 먼저 분석 해보자.
Stack에 대한 정보를 넘겨야되는 이유는 우리가 struct
구조체를 만들 때 오직 데이터타입만을 묶어 만들었기 때문이다. 구조체 내 데이터에 접근해서 변경하는 동작같은 경우 데이터 값에 매우 의존적이다. 따라서, 묶어서 구조체를 만들 경우, 고유의 데이터에 접근할 때 추가적인 정보가 필요없게 된다.
“아.. 잘 모르겠고, 좀 더 단순화 해보자.”
우리는 단순화를 위해 C++의 Struct 특징을 활용해보고자 한다.
C의 구조체 struct
같은경우 오직 데이터만 묶어서 하나의 타입으로 만들 수 있었다. 하지만 C++의 구조체 struct
같은 경우 데이터 뿐만아니라 데이터와 관련된 함수도 묶어서 하나의 구조체를 만들 수 있다.
Stack 프로그램 만들기 [3]
C++ 구조체를 활용해, 데이터와 함수를 묶어 하나의 구조체로 만들어보자.
#include <iostream>
struct Stack{
// 새로운 타입 내에 데이터가 있다고 해서 => 멤버 데이터
int buf[10];
int idx = 0;
// 새로운 타입 내에 함수가 있다고 해서 => 멤버 함수
// 데이터에 접근이 가능하기 때문에, Stack에 대한 정보를 받을 필요가 없음.
// 함수가 훨씬 간결해짐.
void push(int n) { buf[idx++] = n; }
int pop() { return buf[--idx]; }
};
// 함수를 따로 관리할 땐 복잡함..
// void push(Stack* s, int n) { s->buf[s->idx++] = n; }
// int pop(Stack* s) { return s->buf[--(s->idx)]; }
int main() {
Stack s1, s2;
s1.idx = 0;
s2.idx = 0;
// 아래와 같이 Stack정보 넘길 필요 없음.
// push(&s1,10);
// s1 Stack에 20 넣어줘~ 좀 더 고차원적 의미.
s1.push(20);
// 인간 친화적으로 쓰여졌지만, 결국 컴파일러가 아래와 같이 변경함.
// push(&s2,10); <- 컴파일러는 아래의 문장을 이렇게 변경함.
s2.push(30);
// 20 출력, s1의 마지막에 입력된 값.
std::cout << s1.pop() << std::endl;
}
struct
를 활용해 데이터와 함수를 묶어 사용하다보니, 함수 사용자체가 좀 더 인간 친화적(?)이 됐고, 간결해졌다.
위의 주석에도 적어뒀지만, 결국 컴파일러는 push(&s1,20)
와 같은 형태로 변경해서 넘긴다는 것을 기억해두자.
추가로, 멤버함수에서는 멤버데이터에 접근을 마음대로 할 수 있다는 점을 꼭 기억하자!
우리는 [1],[2],[3] 의 방법을 통해 Stack을 좀 더 간결하고 편하게 사용하도록 변경해왔다.
[3] 코드가 완벽한 Stack일까?
그렇지 않다. 어떤 문제가 있을까? 아래의 코드를 보자.
int main() {
Stack s3;
// idx는 0부터 시작해야되는데.. 배열에는 -1이 없어ㅠ..
s3.idx = -1;
}
위의 코드와 같이 만약 사용자가 idx
값에 잘못된 값을 입력한다면 해당 프로그램은 어떻게 될까?
실행하다 죽는다 물론 사용자가 프로그램을 잘 쓰면 된다. 하지만 개발자의 입장에서 잘못 사용하기 어렵게 만들 순 없을까?를 고민하는게 맞다. 좀 더 개선된 프로그램을 통해 사용자가 잘못사용하지 못하도록 만들어보자.
Stack 프로그램 만들기 [4]
프로그램은 잘못 사용하지 않도록 만드는 것이 좋다 위의 코드와 달리, 사용자가 Stack을 잘못 사용하기 어렵게 만들어보자.
C++에는 접근지정자 개념이 존재한다.
private
접근지정자를 사용하게 될 경우, 외부에서는 접근할 수 없고, struct
or class
내부에서 접근이 가능하도록 한다.
public
접근지정자를 사용하게 될 경우, 외부에서 접근이 가능하다. 즉, 누구나 접근이 가능하다.
private
같은 경우 멤버데이터 public
같은 경우 멤버함수를 지정할 때 많이 사용한다.
접근지정자를 활용해 멤버데이터를 사용자가 직접 접근하지 못하도록 만들어보자.
#include <iostream>
struct Stack{
private:
int buf[10];
int idx = 0;
public:
// 초기화하는 함수.
void init() { idx = 0; }
void push(int n) { buf[idx++] = n; }
int pop() { return buf[--idx]; }
};
int main() {
Stack s1, s2;
// 데이터에 직접 접근하는 것이 아닌, 멤버함수로 접근해 데이터를 변경하도록함.
s1.init();
s2.init();
s1.push(20);
s2.push(30);
std::cout << s1.pop() << std::endl;
}
위의 코드와 같이 init
함수를 사용해 멤버데이터에 접근하는걸 막을 경우 외부에서는 멤버데이터 변경이 불가능하다. 멤버함수 내 예외처리를 두어 사용자가 잘못접근하는 것도 막을 수 있다는 장점이있다.
우리가 OOP를 할 때 가장 많이 듣는 가장 중요한 특징이 바로 캡슐화 💊 개념이다. “멤버 데이터를 직접적으로 접근하는 것은 불가능, 오직 멤버함수로만 접근해야됨.” 즉, 내부로 들어오려면 외부를 통해서 들어오도록 캡슐처럼 꽁꽁 싸맸다 라는 의미이다.
기억하자 캡슐화 (Encapsulation) 개념을..!!
추가로, class
개념을 설명하면 좋을 것 같다.
현재 코드에서 C++의 struct를 활용하고 있는데, 우리가 OOP를 하면 class 개념을 많이 듣곤한다. C++의 struct는 class 와 한가지만 제외하고 기능이 동일하다.
둘의 차이점은 default 접근 지정자의 차이다.
🌱 strcut - default 접근지정자는 public이다.
🌱 class - defautl 접근지정자는 class이다.
struct는 일단 모든 것을 열어두고 필요한 것만 막자 라는 개념이고, OOP에서 탄생한 class 개념은 캡슐화를 엄청 중요하게 생각해서 일단 다 막고 필요한 것만 열자의 개념이다.
이 코드에서는 수정할 부분이 없을까?
만약, 사용자가 초기화를 깜빡하고 init()
를 호출하지 않고 Stack을 사용하고자 한다면 어떻게 될까?
초기화가 되지 않아 사용이 불가능할 것이다.
자동으로 초기화하면 좀 더 편하지 않을까? 개선된 프로그램을 통해 자동 초기화하는 방법을 배워보자.
Stack 프로그램 만들기 [5]
자동으로 초기화 하는 방법 생성자를 활용하는 방법이다.
사용방법은 아래의 코드와 같다.
#include <iostream>
struct Stack
{
private:
int buf[10];
int idx;
public:
// 클래스 이름과 동일한 함수 : 생성자.
// 만드는 사람보다.. 사용자에게 편하게 해줌.
Stack() { idx = 0; }
void push(int n) { buf[idx++] = n; }
int pop() { return buf[--idx]; }
};
int main()
{
Stack s1;
// 만드는 순간 초기화.
// s1.init();
s1.push(10);
s1.push(20);
std::cout << s1.pop() << std::endl;
}
struct
또는 class
를 활용한 사용자 타입의 생성을 변수 생성이 아닌 객체 생성이라 부른다.
객체를 생성할 때, 생성자가 불린다. 생성자는 자동적으로 불리도록 되어져있기 때문에 해당 영역에서 초기화를 하면 자동으로 초기화를 누릴 수 있다.
생성자의 몇 가지 특징
- 클래스 이름과 동일한 이름을 가지는 함수
- 객체를 만들면 자동으로 호출됨
- 리턴타입을 표기하지 않아도 됨
- 인자가 있어도 되고 없어도 됨
생성자를 활용하면 만드는 사람보다, 사용자를 좀 더 편리하게 만들어주는 장점이 있다.
Stack을 개선하면서 열심히 만들었는데, 이게 끝은 아니다. 내용이 더 있으며 너무 길어진 것 같아 다음 포스팅을 통해 추가로 개선하는 과정을 설명한다.
Outro.
해당 포스팅은 Ecourse의 C++ Basic 강의를 참고해 작성되었습니다.
강의를 참고하실 분은 여기를 클릭해 확인해주세요!