1. 객체지향 프로그래밍(OOP : Object Oriented Programming)
// 복소수 2개를 더하고 싶다.
? add(double ar, double ai, double br, double bi) {
double sr = ar + br;
double si = ai + bi;
return ?
}
int main() {
double xr = 1, xi = 1; // 1 + 1i
double yr = 2, yi = 2; // 2 + 2i
add(xr, xi, yr, yi);
}
위의 코드는 복소수(실수+허수의 형태)를 더하는 코드이다.
위에서 add함수로 인자를 4개 넣고 결과 값을 받고 싶은데, 함수의 타입과 리턴은 어떻게 해야 할까?
// 복소수 2개를 더하고 싶다.
void add(double ar, double ai, double br, double bi // in parameter
double* sr, double* si) { // out parameter
*sr = ar + br;
*si = ai + bi;
}
int main() {
double xr = 1, xi = 1; // 1 + 1i
double yr = 2, yi = 2; // 2 + 2i
double sr, si;
add(xr, xi, yr, yi, &sr, &si);
}
double 타입의 변수 2개의 주소를 인자로 보내고 포인터로 결과값을 주소에 저장하게 해서 처리했다.
이 때, 함수 내부에서 처리되는 인자를 in parameter / 외부에 영향을 미치는 인자를 out parameter라고 한다.
- 현실 세계에 존재 하는 것들을 프로그래밍 한다고 생각해보면 다음과 같이 정리 할 수 있을 것 같다.
- 위의 3가지를 프로그래밍 하기 위해 Complex / Date / Person 이라는 변수의 타입이 있다면 편리하지 않을까?
=> C언어의 구조체를 사용하면 새로운 타입을 만들 순 있다.
그렇다면, 구조체를 이용해서 Complex라는 타입을 설계하고 add함수를 만들어보자
struct Complex {
double real;
double image;
};
Complex add(const Complex& c1, const Complex& c2) {
Complex temp;
temp.real = c1.real + c2.real;
temp.image = c1.image + c2.image;
return temp;
}
int main() {
Complex c1 = {1, 1}; // 1 + 1i
Complex c2 = {2, 2}; // 2 + 2i
Complex result = add(c1, c2);
}
- 딱 봐도 코드의 가독성이 좋아지고 간단해졌다.
- 필요한 데이터 타입을 먼저 설계한다면 위와같이 코딩이 간단해진다.
객체 지향의 핵심 개념
- 프로그램에서 필요한 타입을 먼저 설계한다.
- 현실세계에 존재하는 사물은 상태와 동작이 있다.
상태 | 동작 | |
자동차 | 색상, 속도, 종류 등 | 달린다, 멈춘다 등 |
사람 | 이름, 나이, 몸무게 등 | 웃는다, 운다, 먹는다 등 |
게임 캐릭터 | 직업, 성별, 종족 등 | 싸운다, 춤춘다 등 |
복소수 | 실수부, 허수부 | 더한다, 절대값을 구한다 등 |
- 타입을 설계할 때
=> 상태와 동작을 표현할 수 있어야 한다.
=> 상태는 변수로, 동작은 함수로 표현한다.
- C의 구조체와 C++의 구조체
=> C : 데이터만 포함 할 수 있다.
=> C++ : 데이터 뿐 아니라 함수도 포함 할 수 있다.
2. Stack 만들기로 배우는 객체지향 프로그래밍
#include <iostream>
int buf[10];
int idx = 0;
void push(int value) {
buf[idx++] = value;
}
int pop() {
return buf[--idx];
}
int main() {
push(10);
push(20);
push(30);
std::cout << pop() << std::endl; // 30
std::cout << pop() << std::endl; // 20
}
Stack 만들기 1단계
- 전역변수를 사용해서 버퍼와 인덱스를 관리
위의 코드로 stack을 구현했다.
이 때, 2개 이상의 스택이 필요하다면 어떻게 해야 할까?
#include <iostream>
void push(int* buf, int* idx, int value) {
buf[++(*idx)] = value;
}
int pop(int* buf, int* idx) {
return buf[(*idx)--];
}
int main() {
int buf1[10];
int idx1 = 0;
int buf2[10];
int idx2 = 0;
push(buf1, &idx1, 10);
push(buf1, &idx1, 20);
push(buf2, &idx2, 30);
std::cout << pop(buf1, &idx1) << std::endl; // 20
std::cout << pop(buf2, &idx2) << std::endl; // 30
}
=> push, pop 함수의 인자로 버퍼와 인덱스를 전달한다.
=> 2개 이상의 스택에 대응될 수 있게 코드를 짰다. 하지만 2개인데도 너무나 복잡해 보인다.
- 위의 코드에서 연관된 데이터를 묶어서 Stack 타입을 만들어 보자
=> 구조체로 만들어 보자
Stack 만들기 2단계
#include <iostream>
struct Stack {
int buf[10];
int idx;
};
void push(Stack* s, int value) {
s->buf[s->idx++] = value;
}
int pop(Stack* s) {
return s->buf[--(s->idx)];
}
int main() {
Stack s1;
Stack s2;
s1.idx = 0;
s2.idx = 0;
push(&s1, 10);
push(&s1, 20);
push(&s2, 30);
std::cout << pop(&s1) << std::endl; // 20
std::cout << pop(&s2) << std::endl; // 30
}
- 구조체를 사용해서 Stack 타입을 설계
- 코드가 이 위의 코드보다 간단해졌다.
- 하지만 단점이 있다.
=> Stack의 상태를 나타내는 데이터와 상태를 조작하는 함수가 분리되어 있다.
=> push와 pop함수가 1번째 인자로 Stack을 전달받아야 한다.
=> push, pop함수 이외의 함수에서도 idx와 buf에 접근할 수 있다.
그렇다면 상태를 나타내는 데이터와 상태를 조작하는 함수를 묶으면 어떨까?
Stack 만들기 3단계
#include <iostream>
struct Stack {
int buf[10];
int idx;
void push(int value) {
buf[idx++] = value;
}
int pop() {
return buf[--idx];
}
};
int main() {
Stack s1;
Stack s2;
s1.idx = 0;
s2.idx = 0;
s1.push(10);
s1.push(20);
s2.push(30);
std::cout << s1.pop() << std::endl; // 20
std::cout << s2.pop() << std::endl; // 30
}
- 상태를 나타내는 데이터와 상태를 조작하는 함수를 묶는다.
=> C++언어는 구조체가 함수를 포함할 수 있다.
=> 멤버 함수와 멤버 데이터 개념
=> 멤버 함수에서는 멤버 데이터에 접근할 수 있다.
- push 함수의 모양
- 문제점
=> push, pop뿐 아니라 모든 함수에서 idx에 접근할 수 있다.
=> 사용자가 idx에 잘못된 값을 넣으면 문제가 발생하게 된다.
idx를 잘못 사용하기 어렵게 만들 수 없을까?
Stack 만들기 4단계
#include <iostream>
struct Stack {
private:
int buf[10];
int idx;
public:
void init() { idx = 0; }
void push(int value) {
buf[idx++] = value;
}
int pop() {
return buf[--idx];
}
};
int main() {
Stack s1;
Stack s2;
s1.init();
s2.init();
s1.push(10);
s1.push(20);
s2.push(30);
std::cout << s1.pop() << std::endl; // 20
std::cout << s2.pop() << std::endl; // 30
}
- 정보 은닉이 필요 -> 구조체 내에 private: 지정자를 통해 접근하지 못하도록 한다.
- 접근 지정자
=> private : 멤버 함수에서만 접근 할 수 있다.
=> public : 멤버 함수가 아닌 함수에서도 접근할 수 있다.
- 정보 은닉(information hiding)
=> 멤버 변수를 외부에서 직접 접근할 수 없게 하고, 멤버 함수를 통해서만 멤버 변수에 접근하게 한다.
=> 외부의 잘못된 사용으로 객체의 상태가 불안정 해지는 것을 막는다.
=> 사용자는 Stack의 내부 구조인 buf와 idx를 알 필요가 없다. push/pop/init 함수만 알면 된다.
- struct vs class
=> struct : 접근 지정자 생략시 디폴트가 public
=> class : 접근지정자 생략시 디폴트가 private
이로써 완성된 것 같지만 또 문제가 있다. Stack을 쓰려면 객체를 선언하고 꼭 init함수를 호출해야 한다.
매번 초기화를 하는건 불편한데 자동으로 초기화 할 수 없을까?
Stack 만들기 5단계
#include <iostream>
class Stack {
private:
int buf[10];
int idx;
public:
// void init() { idx = 0; }
Stack() { idx = 0; }
void push(int value) { buf[idx++] = value; }
int pop() { return buf[--idx]; }
};
int main() {
Stack s1;
Stack s2;
// s1.init();
// s2.init();
s1.push(10);
s2.push(30);
std::cout << s1.pop() << std::endl; // 10
std::cout << s2.pop() << std::endl; // 30
}
- 생성자
=> 클래스 이름과 동일한 이름을 가지는 함수
=> 리턴 타입은 표기하지 않는다.
=> 변수(객체)를 생성하면 자동으로 생성자가 호출된다.
- Stack타입의 객체를 생성하면 필요한 초기화가 자동으로 이루어진다.
이제 크기에 대한 문제이다.
현재 buf의 크기는 10으로 10개 이상을 push하면 오버플로우가 발생한다.
스택의 크기를 사용자가 결정하게 더 좋아보인다. 어떻게 하면 될까?
Stack 만들기 6단계
#include <iostream>
class Stack {
private:
int* buf;
int idx;
public:
Stack(int size = 10) {
idx = 0;
buf = new int[size];
}
~Stack() { delete[] buf; }
void push(int value) { buf[idx++] = value; }
int pop() { return buf[--idx]; }
};
int main() {
Stack s1(30);
Stack s2(20);
s1.push(10);
s2.push(30);
std::cout << s1.pop() << std::endl; // 10
std::cout << s2.pop() << std::endl; // 30
}
- 사용자가 스택의 버퍼 크기를 변경할 수 있도록
=> 배열이 아닌 동적 메모리 할당 사용
=> new로 할당된 메모리는 사용 후 반드시 delete 해야 한다.
- 소멸자(destructor)
=> ~클래스 이름() 의 모양의 함수
=> 리턴 타입을 표기하지 않으며 인자도 가질 수 없다.
=> 객체가 파괴 될 때 자동으로 호출 된다.
=> 객체가 생성자에서 자원을 할당한 경우 소멸자에서 자원을 반납한다.
또, 문제가 있다. 현재는 멤버 데이터와 멤버 함수의 구현까지 다 클래스 내부에 있다.
코딩을 하다보면 클래스 내부에 멤버함수가 매우 많아질 수 있다.
그래서 보통 클래스 내부에는 함수의 선언부만 만들고, 외부에 구현부를 만든다.
Stack 만들기 7단계
#include <iostream>
class Stack {
private:
int* buf;
int idx;
public:
Stack(int size = 10);
~Stack();
void push(int value);
int pop();
};
Stack::Stack(int size) {
idx = 0;
buf = new int[size];
}
Stack::~Stack() { delete[] buf; }
void Stack::push(int value) { buf[idx++] = value; }
int Stack::pop() { return buf[--idx]; }
int main() {
Stack s1(30);
Stack s2(20);
s1.push(10);
s2.push(30);
std::cout << s1.pop() << std::endl; // 10
std::cout << s2.pop() << std::endl; // 30
}
- 위와 같이 선언부와 구현부를 나눈다.
- 또, default parameter는 구현부에서 쓸 수 없으므로 지운다.
- 보통 함수의 선언은 헤더 파일로 제공하고 구현부는 다른 파일로 제공하므로 나눈다.
// stack.h
class Stack {
public:
Stack(int size = 10);
~Stack();
void push(int value);
int pop();
private:
int* buf;
int idx;
};
// stack.cpp
#include "stack.h"
Stack::Stack(int size) {
idx = 0;
buf = new int[size];
}
Stack::~Stack() { delete[] buf; }
void Stack::push(int value) { buf[idx++] = value; }
int Stack::pop() { return buf[--idx]; }
// main.cpp
#include <iostream>
#include "stack.h"
int main() {
Stack s1(30);
Stack s2(20);
s1.push(10);
s2.push(30);
std::cout << s1.pop() << std::endl; // 10
std::cout << s2.pop() << std::endl; // 30
}
- 클래스를 만드는 일반적인 방법
=> 클래스의 선언 안에는 멤버 데이터와 멤버 함수의 선언만 포함 한 후 헤더 파일에 넣는다.
=> 멤버 함수의 구현부는 별도의 소스파일로 제공한다.
- 클래스 사용자
=> 헤더 파일을 포함 한 후 사용
- 코딩 관례
=> 클래스 선언부에서 멤버 함수를 위쪽에 놓고, 멤버 데이터를 아래쪽에 놓는 경우가 많다.
=> 왜냐하면 갖다 쓰는 사용자 입장에서 멤버 데이터는 알 필요 없이, 함수만 알면 되므로...
마지막 단계다.
현재 스택은 int타입만을 저장할 수 있다. 다른 타입을 저장하려면 똑같은 코드를 타입만 다르게 다시 만들어야 한다.
이럴 때 클래스 템플릿을 사용하여 해결할 수 있다.
아래는 코드를 보기 편하게 헤더파일과 구현파일을 나누기 전 코드이다.
Stack 만들기 8단계
#include <iostream>
template<typename T>
class Stack {
private:
T* buf;
int idx;
public:
Stack(int size = 10) {
idx = 0;
buf = new T[size];
}
~Stack() { delete[] buf; }
void push(T value) { buf[idx++] = value; }
T pop() { return buf[--idx]; }
};
int main() {
Stack<int> s1(30);
Stack<double> s2(20);
s1.push(10);
s2.push(1.2);
std::cout << s1.pop() << std::endl; // 10
std::cout << s2.pop() << std::endl; // 1.2
}
- int외에 다른 타입 버전도 쓰고 싶다.
=> 클래스 템플릿
- 하나의 스택에 여러 가지 타입이 보관되는 것이 아니라, 각각의 타입을 저장하는 별도의 Stack 클래스를 코드를 컴파일러가 생성하는 것
- 주의 사항
=> 클래스를 템플릿으로 만들 시 함수의 구현부도 반드시 헤더 파일에 있어야 한다.
=> 멤버함수를 클래스 외부에 구현할 수 있지만 외부구현 자체도 헤더에 놓아야 한다.
'프로그래밍 > C++' 카테고리의 다른 글
[C++] 접근지정자 (0) | 2019.10.29 |
---|---|
[C++] 객체지향 프로그래밍의 개념(2) (0) | 2019.10.29 |
[C++] 동적 메모리 할당, nullptr (0) | 2019.10.27 |
[C++] Explicit Casting (0) | 2019.10.25 |
[C++] 레퍼런스(reference) (0) | 2019.10.22 |