얕은 복사와 깊은 복사

 

얕은 복사 (Shallow Copy)

멤버 데이터를 비트열 단위로 똑같이 복사함

(메모리 영역 값을 그대로 복사)

 

ex (a1 클래스를 얕은 복사, b 클래스는 동적 할당, a 클래스와 b 클래스는 생명 주기를 같이 함)

Stack : a1 [ q 0x1000 ] - Heap 0x1000 b [     ]

Stack : a2 [ q 0x1000 ]

Stack : a3 [ q 0x1000 ]

메모리 영역을 그대로 복사해오기 때문에 동일한 주소 값을 가지고 있다.

위와 같은 조건일 때 a1, 2, 3가 전부 동일한 Heap 영역의 b 클래스를 가리키고 있기 때문에 b 클래스는 생성자 호출을

한 번만 하게 되고 소멸자 호출은 세 번을 하게 되면서 에러가 발생한다.

 


 

깊은 복사 (Deep Copy)

멤버 데이터가 참조(주소) 값이라면 데이터를 새로 만들어준다.

(원본 객체가 참조하는 대상까지 새로 만들어서 복사)

 

ex

Stack : a1 [ q 0x1000 ] - Heap 0x1000 b [     ]

Stack : a2 [ q 0x2000 ] - Heap 0x2000 b [     ]

Stack : a3 [ q 0x3000 ] - Heap 0x3000 b [     ]

얕은 복사의 상황을 한 번에 해결할 수 있다. a 클래스들은 각각의 서로 다른 b 클래스 객체를 

가지게 된다. 

 

깊은 복사를 해주기 위해서는 복사 대입 연산자를 재정의해서 사용하면 된다.

 


 

복사 생성자 행동 절차

 

1. 암시적 복사 생성자

 - 부모 클래스가 있다면 부모 클래스의 복사 생성자 호출

 - 멤버 클래스(포인터가 아닌 일반적으로 클래스를 가지고 있는 상태를 말함)의 복사 생성자 호출

 - 두 경우가 모두 아니고 멤버의 타입이 기본 타입이라면 메모리 복사, 즉 얕은 복사를 하게 된다.

 

2. 명시적 복사 생성자

 - 부모 클래스의 기본 생성자 호출

 - 멤버 클래스의 기본 생성자 호출

 

중요한 포인트는 명시적으로 복사 생성자를 정의하게 되면 그 이후부터는 프로그래머가 모든

복사와 관련되서 핸들링해야 한다는 점이다. 암시적 때처럼 컴파일러가 자동적으로 복사 생성자를 호출해주지

않기 때문에 프로그래머는 따로 복사 생성자를 호출해야 한다. 만약 이 부분을 놓치게 되면 복사가 완전히

이루어지지 않고 짤리는 부분이 생기거나 할 수 있다.

 


 

복사 대입 연산자 행동 절차

 

1. 암시적 복사 대입 연산자

 - 부모 클래스의 복사 대입 연산자 호출

 - 멤버 클래스의 복사 대입 연산자 호출

 - 멤버가 기본 타입일 경우 메모리 복사(얕은 복사)

 

2. 명시적 복사 대입 연산자

 -  프로그래머가 모든 걸 알아서 핸들링해야 함

 

'프로그래밍 언어 공부 > C++' 카테고리의 다른 글

디버깅 연습문제  (0) 2022.07.02
객체지향 Text RPG 만들기  (0) 2022.06.28
면접 등에서 자주 나오는 질문 및 중요한 개념  (0) 2022.06.24
캐스팅 (타입 변환)  (0) 2022.06.22
동적 할당  (0) 2022.06.21

 

타입 변환

 

타입 변환은 크게 두 가지로 구분할 수 있다.

 

1. 값 타입 변환

의미를 유지하기 위해서 원본 객체와 다른 비트열로 재구성한다.

즉 최대한 원본 값의 의미를 최대한 유지하는 선에서 물리적으로 저장된 비트열을 재구성하는 것이다.

 

2. 참조 타입 변환

원본 데이터는 그대로 유지하고 우리가 바라보는 관점만 바꾼다.

즉 동일한 데이터를 다르게 해석해서 다른 값을 출력한다는 뜻이다.


안전도를 기준으로 한 분류

 

1. 안전한 변환

의미가 항상 완전히 일치하는 경우를 말한다.

어떤 상황에서 의미가 완전히 일치하는 변환이 일어날까? 

예를 들어 같은 타입이면서 크기만 더 큰 바구니로 이동했을 때가 있다.

 

2. 불안전한 변환

의미가 항상 완전히 일치한다고 보장하지 못하는 경우

예를 들어 타입을 바꾸는 경우, 같은 타입이지만 큰 바구니에서 작은 바구니로

변환하는 경우 등이 있다.


프로그래머 의도에 따른 분류

 

1. 암시적 변환

이미 알려진 타입 변환 규칙에 따라서 컴파일러가 자동으로 변환해주는 것을 말한다.

 

2. 명시적 변환

암시적과는 반대 개념이다. 사용자가 명시적으로 변환을 해주는 것을 말한다.


다음은 아무런 연관 관계가 없는 클래스 사이의 변환에 대해 알아보자.

 

1. 연관 없는 클래스 사이의 값 타입 변환

일반적으로 정상 작동하지 않는다. 

일반적이지 않게 특수한 방법을 사용해서 변환하면 되지 않을까?라고 생각이 드는데

결론은 가능하다.

 

a 클래스와 b 클래스가 존재하고 둘은 완전히 연관성이 없다고 가정해보자.

a 클래스를 값 타입 변환을 통해 b 클래스로 변환하려고 할 때 에러가 발생하지만 사용자가 의도적으로

b 클래스 내부에 타입 변환 생성자를 만들어서 a 클래스를 받아들이면 일단 정상적으로 컴파일은 된다.

물론 예외적인 상황이다. 타입 변환 생성자 말고도 하나 더 가능한 방법이 있는데 타입 변환 연산자가 그것이다.

operator를 이용해서 클래스 변환을 명시적으로 해주면 클래스 변환이 가능하다.

 

2. 연관 없는 클래스 사이의 참조 타입 변환

a 클래스를 b 클래스로 변환할 때

a xx;

b& x = (b&) xx;

다음과 같이 명시적으로 지정해주면 일단 정상적으로 통과되는 것을 확인할 수 있다.

하지만 b 클래스에만 존재하는 변수를 잘못 건드리면 원치 않게 메모리를 오염시킬 수 있기 때문에

조심해야 한다.


상속 관계에 있는 클래스 사이의 변환

 

1. 상속 관계 클래스의 값 타입 변환

자식 클래스를 부모 클래스로 변환하는 것 가능

부모 클래스를 자식 클래스로 변환하는 것은 불가능

 

2. 상속 관계 클래스의 참조 타입 변환

자식 클래스를 부모 클래스로 변환하는 것 가능

부모 클래스를 자식 클래스로 암시적 변환은 불가능 명시적 변환은 가능


연관성이 없는 클래스 사이의 포인터 변환

 

이번에는 포인터 변환이다. 

서로 연관성이 없는 클래스 a, 클래스 b가 있다고 가정하고 

a 클래스를 포인터로 선언하고 동적 메모리에 할당한다.

그리고 a 클래스를 명시적 변환을 통해 b 클래스 포인터에 집어넣을 수 있다.

위 상황처럼 했을 때 서로 다른 클래스이기 때문에 원치 않게 다른 메모리 영역을 오염시킬 수 있다.

메모리를 오염시켰을 때 바로 에러가 발생하면 다행이지만 며칠 뒤 혹은 몇 달 뒤에

갑작스레 문제가 될 수도 있다. 그렇기 때문에 타입 변환을 신중하게 해야 한다.

 

 

다음은 상속관계에 있는 클래스의 변환에 대해 알아보자

 

부모 -> 자식 변환

논리적으로 생각해봐도 부모에서 자식으로 변환하는 건 불가능하다.

하지만 명시적 변환을 통해 억지로 변환시킬 수 있는데 이렇게 진행하면 위와 마찬가지로 

의도치 않게 메모리 영역을 오염시킬 수 있다.

 

자식 -> 부모 변환

자식에서 부모 클래스로 변환하는 것은 암시적, 명시적 둘 다 가능하다.

부모에서 자식으로 변환하는 것과는 다르게 어느 정도 안정적이다.

 

실제 게임 제작을 하다 보면 암시적 및 명시적으로 포인터 타입 변환을 해주어야 하는 상황이

발생하기 때문에 실제 타입 변환을 해야 한다면 정말 주의를 요해야 한다.


중요한 개념

a라는 클래스가 최상위 부모 클래스라고 가정하고 a 클래스를 상속받는 자식 클래스들이 여럿 있다고

생각해보자. 동적 할당을 이용해서 메모리를 사용하고 이후에 메모리를 반환하려고 하는데 

최상위 부모 클래스만 delete 명령을 이용해서 반환을 하게 되면 정상적으로 메모리 반환이 이루어지지 않는다.

자식에서 부모로 타입 변환해서 사용했던 클래스들을 다시 부모에서 자식으로 변환을 하고 각각 메모리를 반환

해야 하는 번거로움이 존재한다. 

위의 상황을 한 번에 해결해줄 수 있는 게 있는데 바로 최상위 부모 클래스 내부의 소멸자를 가상 함수로

만들어주는 것이다. 가상 함수는 예전에도 공부했듯이 가상 함수 테이블에 의해 원본 객체의 함수를 찾아가기 때문에

최상위 클래스의 소멸자를 가상 함수로 만들어주면 해당 최상위 부모 클래스를 상속받고 타입 변환을 했던 자식

클래스들은 메모리를 반환할 때 가상 함수에 의해 자동으로 원본 객체의 소멸자를 호출하게 된다.

이렇게 되면 이용했던 모든 동적 할당 메모리를 정상적으로 반환할 수 있다.

(최상위 부모 클래스 소멸자에만 virtual을 붙여주어도 정상 작동하지만 웬만하면 부모 클래스의 소멸자에는

virtual을 붙이자)

 

개념 다시 한번 정리! 소멸자에 virtual을 붙여야 하는 이유! 아래 메모는 무조건 이해하고 외우자 상속 관계에 의해 함수를 재정의 해도 타입에 따라 실행이 되기 때문에 실제 객체, 즉 원본 자체를 찾아가고 싶다면 virtual을 붙이자 virtual에 의해 가상 함수 테이블이 만들어지고 해당 테이블을 참고해서 원본으로 이동해 원본의 함수를 실행할 수 있다.


C++에서 캐스팅(타입 변환)

 

C++에서는 아래 네 가지 방법으로 타입 변환 연산을 하게 된다.

 

1. static_cast : 타입 원칙에 비춰볼 때 상식적인 캐스팅만 허용해준다.

 

 1) int <-> float // 정수 타입과 부동소수점 타입 사이의 변환

  ex) int a = 100;

        int b = 200;

        float x = static_cast<float>(a) / b;

 

 2) 부모클래스* -> 자식클래스* (다운캐스팅) // 단, 안정성 보장 못함

  ex) A, B, C 클래스가 있다고 가정. 그 중에서 A가 부모 클래스 B, C가 A클래스를 상속받는 자식 클래스이다.

        A* a = new B(); // 원본 타입이 B 클래스, A 클래스가 부모이기 때문에 가능

        B* b = static_cast<B*>(a); // 원본이 B라는 걸 확신한 상태에서 B* 타입으로 다운 캐스팅. 해당 방법이 

                                                      안전한 방법이다. 

 

        A* a = new C(); // 원본 타입이 C 클래스, A 클래스가 부모이기 때문에 가능

        B* b = static_cast<B*>(a); // 원본이 C 클래스임을 제대로 확인하지 못하고 B 클래스로 변환 시도

                                                      변환 후 데이터 변경을 할 때 의도치 않은 이상한 메모리 영역의 값을 바꿀 수도

                                                      있기 때문에 아주 위험함.

 

 

2. dynamic_cast : 상속 관계에서의 안전한 형변환을 지원해준다.

 

 1) RTTI (RunTime Type Information) : 실시간으로 코드가 동작할 때 타입을 확인할 수 있는 기법이다. 다시 말하면

     다형성을 활용하는 방식이다. dynamic_cast를 사용하려면 이러한 다형성을 사용해야 하고 이러한 다형성을

     활용하는 방법은 virtual 함수를 사용하는 것이다. 다시 말하면 dynamic_cast를 사용하고 싶다면 최소 1개 이상의 

     virtual 함수를 사용해야 한다. virtual 함수를 사용하면 객체 메모리에 가상 함수 테이블 주소가 기입되고 그 상태에서 

     dynamic_cast를 사용해서 캐스팅을 할 때 맞는 타입인지 원본을 체크해서 캐스팅을 해준다. 만약 잘못된 타입으로 

     캐스팅을 했다면 nullptr을 반환해준다.

 

 2) static_cast와의 차이점 : 두 캐스팅 모두 상속 관계에서의 형변환을 지원해주지만 잘못된 타입으로 캐스팅을 한 경우에

     차이가 발생한다. static의 경우 잘못된 타입으로 캐스팅하면 별 다를 것 없이 캐스팅이 되지만 dynamic의 경우 잘못된

     타입으로 캐스팅한 경우 nullptr을 반환하기 때문에 올바른 타입으로 캐스팅했는지 확인하는 용도로 자주 사용된다.

     하지만 static은 dynamic에 비해 속도가 빠르기 때문에 원본 타입을 알고 있다면 static을 사용하는 것이 유리하다.

 

 

3. const_cast : const를 붙이거나 떼어내거나 할 때 사용된다.

 

 1) 다음과 같이 사용할 수 있다.

  ex) void PrintName(char* str) // 캐릭터형 포인터를 받아서 그대로 출력

         {

             cout << str << endl;

         }

 

        PrintName("Hello"); // 이렇게 사용할 시 에러가 발생한다. PrintName 함수는 캐릭터형 포인터를 받지만 매개값으로

                                           넣어주는 값은 const 캐릭터형 포인터이기 때문이다. 이런 경우 사용할 수 있는 것이

                                           const_cast이다.

 

       PrintName(const_cast<char*>("Hello")); // 대부분의 경우 이 const_cast를 사용하는 경우는 매우 매우 드물다. 

                                                                          일단 이러한 것이 존재한다는 것을 알아두자.

 

 

4. reinterpret_cast : 가장 위험하고 강력한 형태의 캐스팅이다. 포인터랑 전혀 관계없는 다른 타입으로

                                  변환 등에 사용된다.

 

 1) __int64 qqq = reinterpret_cast<__int64>(b); // 이런 식으로 사용할 수 있는데 여기서 b는

                                                                               B* b = static_cast<b*>(a); 이것을 의미한다. 즉 말 그대로

                                                                               전혀 다른 타입으로 캐스팅을 가능하게 해 준다.

 

 2) 포인터와 정수 사이에서도 변환이 가능하고 혹은 전혀 상관없는 클래스 포인터끼리의 변환도

     가능하다.

 

     re-interpret_cast를 그나마 사용하는 케이스를 소개하자면

     void* q = malloc(1000); // 1000바이트 크기만큼 동적 할당. 반환 타입은 보이드형 포인터이다. 

     (보이드형 포인터는 사용자가 어떤 타입으로 사용할지 모르니 알아서 변환해서 사용하라는 뜻이다.)

 

     A* a2 = q; // 맨 위에 A 클래스를 가지고 왔다. 보이드형 포인트인 q와 A*가 아무런 연관성이 없다 보니 

    기본적으로 실패 처리를 해주고 이러한 경우 사용하는 것이 reinterpret_cast이다.

 

    A* a2 = reinterpret_cast<A*>(q); // 이 reinterpret_cast 또한 아주 아주 드물게 사용된다.

 

 

동적 할당

 

동적 할당은 왜 필요한 것일까?

우선 다른 저장 영역의 특징을 간략하게 살펴보자

 

- 스택 영역

함수가 끝나면 같이 사라지는 불안정한 저장 공간

잠시 함수에 매개변수를 넘기는 등의 용도로 사용

 

- 메모리 영역

프로그램이 실행되는 동안은 무조건 사용되는 저장 공간

어떠한 상황이든 프로그램이 실행되는 동안 할당된 크기만큼의 저장 공간이 사용됨

 

스택과 메모리 영역으로도 많은 것을 만들 수 있지만 이 두 가지를 이용해서 만들기엔

불가능하거나 비효율적인 상황이 있다. 예를 들어 사용자가 필요한 만큼만 메모리를 사용하고

이후에 필요가 없어지면 언제든 반환할 수 있는 이런 시스템이 필요한 상황이 존재한다.

그래서 사용되는 것이 동적 할당(HEAP-힙 영역)이다. 

 

동적 할당은 스택과는 다르게 생성과 소멸 시점을 관리할 수 있다는 장점이 있다.

위에서 말했듯이 필요한 만큼만 사용하고, 다 사용했으면 반환할 수 있다.

 

C++에서는 기본적으로 CRT(C런타임 라이브러리)의 '힙 관리자'를 통해 힙 영역을 사용한다.

단, 정말 원한다면 사용자가 직접 API를 통해 힙을 생성하고 관리할 수도 있다.

(이런 상황은 보통 MMORPG 서버에서 메모리 풀링 같은 고급 기법을 이용해서 사용할 수 있다.)


동적 할당과 연관된 함수/연산자 종류 : malloc, free, new, delete, new[], delete[]

 

이 중에서 malloc과 free는 함수이다.

new와 delete는 연산자이다.

 

void* a = malloc(1000);

위와 같이 호출하였을 때 1000바이트만큼 메모리 할당 후 할당된 메모리의 시작 주소를 가리키는

포인터를 반환해준다. (메모리가 부족하면 NULL 포인터 반환)

 

free(a);

malloc과 짝꿍인 존재로 할당된 메모리 영역을 해제하는 역할을 한다.

 

동적 할당을 잘못 사용하면 큰 문제가 될 수 있다. 

메모리를 할당하고 free를 하지 않아서 메모리 누수가 발생한다던가

free를 해서 할당을 해제했지만 포인터 주소를 nullptr로 밀어주지 않아서 주소는 살아 있고 

나중에 해당 포인터 주소를 멋대로 사용해서 메모리 영역이 오염되는 상황이 발생할 수도 있다.

그렇기 때문에 꼼꼼하게 체크해가며 사용할 필요가 있다.

 

int* x = new int;

new의 사용법이다. 연산자이기 때문에 malloc과는 사용 방법에 차이가 있다. 하지만 실제 기능은

동일하다 malloc과 마찬가지로 동적 할당을 하며 필요한 만큼 사용한 후 delete를 이용해서 반환

하면 된다. 

 

delete x;

new와 한 세트이다. 동적 할당받은 메모리를 다 사용한 후 반환할 때 사용한다.

 

위와 같은 new 방식은 한 개의 int 값만을 만들기 때문에 여러모로 아쉬운 부분이 있다.

그래서 등장한 것이 배열을 이용한 방식이다. 

 

int* y = new int[5];

int형 5개를 만들어준다. 일반 new와 동일한 기능을 하며 첫 시작 주소를 반환한다.

 

int* yy = (y + 1);

변수 y는 동적 할당받은 메모리의 시작 주소를 가리키고 있기 때문에 다음 주소부분을 활용하기

위해서는 y 주소에서 +1을 해줘서 다음 주소를 yy 변수에 넣어 사용할 수 있다.

 

delete[] y;

배열 방식을 이용해서 동적 할당받은 메모리를 반환할 때 사용한다. 꼭 짝꿍을 맞춰야 한다.

new/delete, new[]/delete[] 짝꿍을 맞춰서 사용해야 에러가 발생하지 않는다.

 

 

malloc/free와 new/delete 비교

- 사용성이나 편의성은 new/delete가 좋다.

- 타입에 상관없이 특정한 크기의 메모리 영역을 할당받으려면 malloc/free가 좋다.

 

둘의 가장 중요하고 근본적인 차이는?

new/delete는 생성 타입이 클래스일 경우 생성자/소멸자를 호출해 준다.

(malloc/free는 생성자/소멸자를 호출하지 않는다.)

 

C++은 메모리 영역을 자유자재로 사용할 수 있다는 것이 장점이자 단점이다.

항상 메모리를 다루는 것은 긴장해야 하고 조심해야 한다는 것을 기억하자.

'프로그래밍 언어 공부 > C++' 카테고리의 다른 글

면접 등에서 자주 나오는 질문 및 중요한 개념  (0) 2022.06.24
캐스팅 (타입 변환)  (0) 2022.06.22
연산자 오버로딩  (0) 2022.06.18
초기화 리스트  (0) 2022.06.17
객체지향의 개념 정리  (0) 2022.06.16

연산자 오버로딩

 

연산자 오버로딩은 이름에서도 알 수 있듯이 연산자를 재정의해서 원하는 동작을 하도록

만드는 것을 말한다.

 

일반적인 산술 연산자로는 클래스 + 클래스를 진행할 수 없지만 

클래스 내부 멤버 함수에서 연산자를 오버로딩(재정의)해서 클래스끼리의 연산을 가능하게

만들 수 있다는 소리이다. 물론 클래스끼리가 아닌 클래스 + 정수도 가능하다.

하지만 주의할 점이 있는데 a op(연산자 재정의) b라고 했을 때 a를 기준 피연산자라고 하며 

a가 클래스여야 가능하다. 정수 + 클래스로 순서를 바꾸면 에러가 발생한다.

위에서 서술한 방식을 멤버 연산자 함수 방식이라고 한다.

 

멤버 연산자 함수 방식 말고도 다른 방식이 있는데 바로 전역 연산자 함수 방식이다.

a op b라고 했을 때 a, b 모두를 연산자 함수의 피연산자로 만든다.

전역 연산자 함수이기 때문에 클래스 내부가 아닌 외부에서 선언할 수 있다.

 

둘 다 상황에 따라 사용하기 때문에 둘 다 알아두는 것이 중요하다. 

 

 

'프로그래밍 언어 공부 > C++' 카테고리의 다른 글

캐스팅 (타입 변환)  (0) 2022.06.22
동적 할당  (0) 2022.06.21
초기화 리스트  (0) 2022.06.17
객체지향의 개념 정리  (0) 2022.06.16
알고리즘 연습문제  (0) 2022.06.11

 

초기화 리스트

 

초기화 리스트는 클래스 내의 멤버 변수 초기화에 쓰인다.

 

이미 알고 있는 얘기이지만 변수를 초기화해서 사용하지 않으면 당장이든 나중이든 스택 메모리에

남아있던 값 때문에 원하지 않는 로직이 실행되어 문제가 될 수 있다. 그래서 변수 선언 시

초기화를 해주는 것이 중요하다.

 

초기화 방법에는 생성자 내에서 초기화하는 방법, 초기화 리스트를 사용하는 방법

멤버 변수 선언과 동시에 초기화하는 방법이 있다. 이번에는 초기화 리스트에 대해 알아보자.

 

초기화 리스트는 클래스 내부의 선처리 영역에서 사용자의 명시적인 지시를 이용하는 것을 말한다.

초기화 리스트는 상속 관계에서 원하는 부모 생성자를 호출할 때 필요하다.

생성자 내에서 초기화하는 방법이랑 초기화 리스트를 사용하는 방법은 일반 변수에서는 차이가 없지만

멤버 타입이 클래스인 경우 차이가 난다.

 

예를 들어 멤버 타입이 a 클래스인 멤버 변수를 선언하고 생성자 내에서 해당 멤버 변수를 초기화를 한다고 했을 때

실행해보면 실질적으로 a 클래스의 기본 생성자와 초기화 값을 전달받는 생성자가 실행되는 걸 확인할 수 있다.

즉 원치 않게 생성자가 두 번 실행된다.

그 이유는 멤버 타입이 a 클래스인 멤버 변수를 b 클래스 내부에서 선언하면 기본적으로 b 클래스 선처리 영역에서

a 클래스의 기본 생성자가 실행된다. 그래서 b 클래스 생성자 내에서 초기화를 진행하면 결과적으로 a 클래스 생성자를

두 번 실행하는 것이 된다.

 

이러한 현상을 겪지 않기 위해서는 초기화 리스트를 사용해주면 된다.

b 클래스 생성자 내부에서 초기화를 해주는 것이 아닌 기본 생성자 옆에 ' : '을 이용해서 아래와 같이 명시적으로 초기화해주는 것이 좋다

 

ex

 

class b 

{

public:

       b : _hp(100), _xxx(1)

       {

       }

 

public:

        int _hp;

        a _xxx; // a라는 클래스가 따로 있다고 가정 (b 클래스가 a 클래스를 포함하고 있는 관계)

}

 

초기화 리스트는 참조 타입과 const 타입을 초기화 해줄 때도 유용하게 사용된다. 

참조 타입과 const 타입은 선언과 동시에 초기화를 해줄 필요성이 있는 형태이기 때문에 뒤늦게 생성자 내부에서 

초기화를 진행하려고 해도 에러를 뱉는다. 그렇기 때문이 초기화 리스트를 이용해서 만들어짐과 동시에 

초기화를 해주는 것이 좋다.

'프로그래밍 언어 공부 > C++' 카테고리의 다른 글

동적 할당  (0) 2022.06.21
연산자 오버로딩  (0) 2022.06.18
객체지향의 개념 정리  (0) 2022.06.16
알고리즘 연습문제  (0) 2022.06.11
자주 쓰이는 함수  (0) 2022.06.08

 

객체지향(OOP, Object Oriented Programming)

 

C++에서 객체는 class라는 문법으로 만들 수 있다. (class는 일종의 설계도라고 생각하면 된다.)

 

지금까지 Text RPG 코드 등과 같이 직접 만들었던 코드들은 절차 지향적 코드이다. 

이제부터는 본격적으로 게임 개발 등에서 많이 사용하는 객체지향을 이용할 것인데

객체지향은 말 그대로 각각의 객체들을 이용해서 원하는 로직을 만드는 것이다. 

즉 변수나 함수 등이 하나의 객체 안에 포함되어 있고 사용자는 그 객체를 불러와서

원하는 동작이나 로직 등을 쉽게 만들 수 있다.

 

class 내부에서 멤버 함수와 멤버 변수를 지정할 수 있으며 

class 안에서는 해당 멤버 변수를 어디에서든지 가져와서 사용할 수 있다.

보통 멤버 변수는 특정 객체 내의 멤버 변수라는 것을 나타내기 위해 

변수명 앞에 _(언더바) or m_ 등과 같이 써준다.

 

class를 이용해서 객체를 생성하면 무조건 생성자와 소멸자가 만들어진다. 

생성자는 여러개 존재할 수 있고 소멸자는 무조건 하나만 존재할 수 있다.

사용자가 직접 명시적으로 생성자를 선언할 수 있고 따로 명시적으로 생성자를 선언하지 않으면

컴파일러가 암시적으로 생성자를 선언한다.

하지만 이런 암시적인 생성자 선언은 원치 않는 상황이 발생할 수도 있으므로 

상황에 따라 명시적으로 선언하는 것이 좋다.

 

생성자의 종류는 크게 세 가지로 분류할 수 있다.

 

기본 생성자

타입 변환 생성자

기타 생성자

 

이 중에서 기본 생성자는 객체가 생성될 때 암시적이든 명시적이든 무조건 한 번은 호출되는

생성자이다. 사용자가 명시적으로 생성자를 선언하지 않으면 컴파일러가 암시적으로 생성하지만

사용자가 기본 생성자 포함 아무 생성자나 명시적으로 선언하면 컴파일러는 더 이상 기본 생성자를 

암시적으로 생성하지 않는다. 그렇기 때문에 다른 생성자를 직접 선언했다면 기본 생성자 또한 

명시적으로 선언해주어야 한다.

 

 

객체지향의 중요한 특성 세 가지는 다음과 같다.

상속성, 은닉성, 다형성

 

일단 상속성부터 정리해보자.

상속은 말 그대로 자식 객체가 부모 객체의 정보들을 그대로 물려받는 것을 의미한다.

즉 부모 객체의 멤버 함수나 멤버 변수들을 그대로 사용할 수 있다.

class a

{

public:

~~~~

}

 

class b : public a // 부모 a, 자식 b

{

 

}

 

위와 같이 상속을 선언할 수 있다. 부모 객체는 a이고 자식 객체는 b이다.

부모가 물려준 정보들을 그대로 사용할 수도 있고 아니면 자식 객체에서 해당 정보들을

재정의해서 사용할 수도 있다.

 

상속에서의 생성자와 소멸자의 개념도 조금 달라진다.

다른 함수나 변수 같은 경우 부모의 정보들을 그대로 받아와서 사용하지만 생성자의 경우는 

자식 객체에서 자식 생성자가 실행되기 직전(선처리 영역)에 부모의 생성자를 호출하고 그 이후에 

자식의 생성자를 실행한다. 소멸자 같은 경우 자식 소멸자가 먼저 실행되고 그 후에 부모의 소멸자를 호출한다.

 

두 번째로 은닉성에 대해 알아보자.

객체에서의 은닉성은 외부에 노출 시키면 안 되는 중요한 함수 등을 감추기 위해서 

사용한다. 예를 들어 객체 안에서만 해당 함수를 호출할 수 있다거나 자신 혹은 자식 객체에게만

함수 접근을 허용하는 경우가 있다. 이러한 은닉성은 아래와 같이 크게 세 가지로 구현할 수 있다.

 

public, protected, private

 

위 세가지는 영어 뜻과 기능이 흡사하다.

public은 어디에서든지 객체 내부의 함수를 호출할 수 있다. 누구나 접근이 가능하다. 

protected는 public과 private의 중간 역할을 하는 것으로 객체 자신 및 자식 객체에 한하여 

함수에 접근이 가능하다.

private는 객체 내부에서만 함수 호출이 가능하고 자식 및 외부에서는 접근이 불가능하다.

 

은닉성은 객체 내부에서만 사용하는 것이 아니라 상속을 할 때에도 사용된다.

부모 객체를 받아올 때 어떤 타입으로 받아오냐에 따라 자손들에게 부모 객체 정보 사용을 

허용할지 숨길지 결정할 수 있다. 

 

세번째로 다형성이 있다. 

다형성은 겉은 똑같지만 기능이 다르게 동작하는 것을 말한다.

다형성에는 오버로딩과 오버라이딩이 있다.

오버로딩은 함수의 이름을 재사용하는 것을 말하며

오버라이딩은 부모 클래스의 함수를 자식 클래스에서 재정의하는 것을 말한다.

 

또한 다형성에는 정적 바인딩, 동적 바인딩이라는 개념이 있고 

일반 함수가 정적, 가상 함수가 동적 바인딩을 사용한다.

객체 타입 변수를 여럿 생성하고 해당 변수들을 통합적으로 관리 및 사용할 때 사용되는데

가상 함수는 객체 타입에 따라 해당 객체의 주소를 따로 저장해놓고 호출할 때 해당 객체 타입에 맞는

객체로 이동해 내부에 있는 함수를 호출할 수 있다. 즉 부모 객체 내부의 함수가 호출되는 일반 함수와 달리

가상 함수는 객체 타입에 맞는 자식 객체 내부로 이동해서 자식 객체 내부에 오버라이딩된 함수를 호출할 수 있다는

소리이다. 

이와 관련해서는 차근 차근 실습을 해보면서 좀 더 공부해보자.

 

 

 

 

'프로그래밍 언어 공부 > C++' 카테고리의 다른 글

연산자 오버로딩  (0) 2022.06.18
초기화 리스트  (0) 2022.06.17
알고리즘 연습문제  (0) 2022.06.11
자주 쓰이는 함수  (0) 2022.06.08
Text RPG ver.2 (포인터, 참조 등 활용)  (0) 2022.06.08

+ Recent posts