갓똥
나는야 프로그래머
갓똥
전체 방문자
오늘
어제
  • 분류 전체보기 (186)
    • 프로그래밍 (146)
      • 자바 (9)
      • 안드로이드 (2)
      • 유니티 (20)
      • C++ (38)
      • C# (56)
      • HTML (2)
      • 파이썬 (3)
      • 자료구조 (2)
      • 알고리즘 (0)
      • 문제풀이 (4)
      • 디자인 패턴 (7)
      • 카카오톡 봇 (1)
      • 엑셀 (1)
      • 기타 (1)
    • 게임 (21)
      • 테일즈위버 (0)
      • 카이로소프트 (1)
      • 순위 (19)
      • 기타 (1)
    • 일상 (13)
      • 카페 (1)
      • 방탈출 (12)
    • 기타 (6)
      • 웃긴자료 (5)

블로그 메뉴

  • 홈
  • 방명록

공지사항

인기 글

태그

  • 전세계 게임 매출
  • C# boxing
  • c# delegate
  • 유니티 그래프
  • 글로벌게임매출
  • c# 코루틴
  • 모바일 게임 순위
  • 유니티 그래프 그리기
  • C# 예외 처리
  • 롤 골드그래프
  • Unity Graph
  • c# unboxing
  • C++ 소멸자
  • C++ 상속
  • c# Thread
  • 게임매출순위
  • 자바
  • 전세계게임매출순위
  • 강남 방탈출
  • 알고리즘
  • 유니티 골드그래프
  • 게임 매출 순위
  • C++
  • 2020년 게임 매출
  • pc 게임 순위
  • C++ virtual
  • 게임 디자인 패턴
  • c# coroutine
  • c# collection
  • pc게임 순위

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
갓똥

나는야 프로그래머

[C++] 객체지향 프로그래밍의 개념(1)
프로그래밍/C++

[C++] 객체지향 프로그래밍의 개념(1)

2019. 10. 29. 00:00
728x90
반응형

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 클래스를 코드를 컴파일러가 생성하는 것

  - 주의 사항

    => 클래스를 템플릿으로 만들 시 함수의 구현부도 반드시 헤더 파일에 있어야 한다.

    => 멤버함수를 클래스 외부에 구현할 수 있지만 외부구현 자체도 헤더에 놓아야 한다.

728x90
반응형

'프로그래밍 > 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
    '프로그래밍/C++' 카테고리의 다른 글
    • [C++] 접근지정자
    • [C++] 객체지향 프로그래밍의 개념(2)
    • [C++] 동적 메모리 할당, nullptr
    • [C++] Explicit Casting
    갓똥
    갓똥
    공부하며 알아가는 내용을 정리해 봅니다.

    티스토리툴바