본문 바로가기
  • 쓸쓸한 개발자의 공부방
C++

[C++] 객체 지향 프로그래밍 OOP

by 심찬 2021. 8. 16.

 

 

객체 지향 프로그래밍 OOP 개념

 

OOP : Objected Oriented Programming

 

복소수를 표현하기 위해 double형 변수 2개를 사용

날짜를 표현하기 위해 int형 변수 3개 를 사용

사람을 표현하기 위해 이름, 나이 등의 변수들을 사용

 

아래와 같이 복소수 계산을 위해 input 6개를 넣어주는 함수를 만드는데 복잡하고 번거롭다.

객체 지향 프로그래밍 관점에서 Complex, Date, Person 와 같은 객체를 만들어 표현하고 싶다.

 

// 복소수 2개를 더하고 싶다.
void add( double ar,  double ai, double br, double bi, // 계산에 필요한 변수 4개 (in parameter)
          double* sr, double* si ) // 결과 값을 담을 변수 2개 (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 );
    
    // sr, si 결과가 있다.
}

 

필요한 데이타 타입을 설계한다.

 

// Complex라는 데이타 타입을 설계해 좀 더 편하게 코딩이 가능해진다.
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 ret = add(c1, c2);    
}

 

현실세계에 존재 하는 사물은 상태와 동작이 있다.

타입을 설계할 때, 상태는 변수로 동작은 함수로 표현한다.

 

C에서의 구조체는 데이타만 포함할 수 있다.

C++에서의 구조체는 데이타와 함수를 포함할 수 있다.

 

객체 지향 프로그래밍 예제

stack을 예제로 객체 지향 프로그래밍을 살펴보면,

 

아래와 같이 stack을 만들고 push와 pop을 사용하고 싶다.

 

int main()
{
    push(10);
    push(20);
    push(30);
    
    int n1 = pop(); // 30
    int n2 = pop(); // 20

}

 

0단계 : 간단한 구현.

 

구현을 해보면 push와 pop을 만들어 볼 수 있다.

그런데 여기서 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
}

 

1단계 : 너무 불편한데...

 

2개의 stack이 필요하다면? 변수를 2개를 설정해 만들어 볼 수 있다.

그런데... 뭔가 좀 복잡해진다. 연관된 데이타를 묶어서 Stack 타입을 만들고 싶어진다.!!!

 

#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];   // 2번째 버퍼
    int idx2 = 0;   // 2번째 인덱스
    push(buf1, &idx1, 10);
    push(buf1, &idx1, 20);
    push(buf2, &idx2, 30);
    
    std::cout << pop(buf1, &idx1) << std::endl;  // 30
    std::cout << pop(buf1, &idx1) << std::endl;  // 20
}

 

2단계 : 뭔가 좀 한 참 모자라...

 

구조체를 활용해서 Stack 을 설계해보는데,,,

조금 간편해 졌긴 하지만, 아직은 조금 불편하다.

첫번째 인자로 Stack* s 를 건네줘야 한다. push, pop 이외에도 직접 idx, buf에 접근이 가능하다.

 

#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);
    s1.idx = 0;
    push(&s1, 30);
    
    std::cout << pop(&s1) << std::endl;  // 30
    std::cout << pop(&s1) << std::endl;  // 20
}

 

3단계 : 아직 조금 모자라~~

 

구조체 안에 함수를 포함한다.

멤버 함수와 멤버 데이타

멤버 함수에서는 멤버 데이터에 접근할 수 있다.

단점 : idx, buf를 누구나 접근이 가능하다.

 

#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);
    
    s1.idx = 0;  // 외부에서 idx 접근이 가능... (문제 발생 가능성!)
    
    std::cout << s1.pop() << std::endl;  // 30
    std::cout << s1.pop() << std::endl;  // 20
}

 

4단계 : 가장 이상적인 OOP의 예제!!!

 

class로 정의하게 되면, private와 public으로 함수를 구분하여 idx, buf 접근을 막고

init, push, pop에 접근할 수 있도록 한다. 캡슐화라고 말한다.

 

#include <iostream>

class Stack 
{
private:  // 접근지정자 생략시 class는 접근지정자가 private가 된다.
          // 접근지정자 생략시 struct는 접근지정자가 public이 된다.
    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);
    
    std::cout << s1.pop() << std::endl;  // 30
    std::cout << s1.pop() << std::endl;  // 20
}

 

 

생성자

 

생성자를 통해 자동으로 초기화를 해준다. 리턴 타입이 없다.

 

#include <iostream>

class Stack
{
private:
    int buf[10];
    int idx;
    
public:
    Stack()       { idx = 0; }     // 생성자 : 자동으로 초기화, 리턴 타입이 없음
    void push( int a) { buf[idx++] = a; }    
    int pop()         { return buf[--idx]; }
};

int main()
{
    Stack s1;
    //s1.init();
    s1.push(10);          
    std::cout << s1.pop() << std::endl;  
    
}

 

 

생성자와 파괴자 + 메모리 할당 및 해제

 

생성자에 input을 받아 버퍼 크기를 바꿀 수 있도록 한다.

생성자에 size를 받아서 new int[size] 메모리 동적 할당을 해준다.

파괴자를 통해 buf를 delete[]를 통해 메모리 해제를 해준다.

 

#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 a) { buf[idx++] = a; }    
    int pop()         { return buf[--idx]; }
};

int main()
{
    Stack s1(30);
    
    s1.push(10);          
    std::cout << s1.pop() << std::endl;  
}

 

 

클래스 파일 분할

실무에서는 대부분 파일이 분할되어 있기에 기본적으로 이 구조를 많이 보게 된다.

 

/* ============= stack.h ============== */

class Stack
{
public:
    Stack(int size = 10);
    ~Stack();
    void push( int a);
    int pop();
    
private:                 // 코딩 관례상 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 a) { buf[idx++] = a; }    
int  Stack::pop()         { return buf[--idx]; }

 


// ====================== main ==========================

#include <iostream>
#include "stack.h"

int main()
{
    Stack s1(30);
    
    s1.push(10);          
    std::cout << s1.pop() << std::endl;  
    
}

 

Template Class (템플릿 클래스)

 

buf 타입을 int만이 아닌 다양한 type을 처리해주는 코딩을 하고 싶다.

적절하게 template 코드를 넣어준다.

 

#include <iostream>

template<typename T>     // template 사용하는 클래스로 만들기!
class Stack
{
private:
    T* buf;          // 버퍼를 template로 !
    int idx;
    
public:
    Stack(int size = 10) 
    {
         idx = 0; 
         buf = new T[size];         // 메모리 생성시 T타입
    }
    ~Stack() { delete[] buf;}
 
    void push( T a) { buf[idx++] = a; }      // input도 T로 넣어준다.
    T pop()         { return buf[--idx]; }    // 반환값 T
};


int main()
{
    Stack<int> s1(30);             // input 타입이 필요
    Stack<double> s2(30);
    
    s1.push(10);          
    std::cout << s1.pop() << std::endl;  
    
}

 

댓글