반응형
728x90
반응형

C++의 새로운 형 변환

  • 기존 C언어 스타일의 형 변환 연산자

C언어 스타일의 형 변환 연산자( (자료형) 변수 및 상수 ) 형태는 C언어의 호환성을 위해서 C++에서도 제공을 한다. 그런데 문제는 C언어 스타일의 형 변환 연산자는 변환하지 못하는 대상이 없는 아주 강력한 연산자이다. 다음의 예제에서 이를 나타낸다.

39번 라인에서 Base 클래스 포인터 형을 Derived 클래스 포인터 형으로 형 변환하는 것은 일반적인 연산이 아니다. Derived 클래스 형이 필요했다면 애초에 Base 클래스 포인터 형으로 Derived 객체를 가리키게 생성할 필요가 없었다. 그리고 43번 라인은 확실하게 형 변환의 문제가 된다. 클래스 포인터 변수 pb2는 실재하지 않는 멤버를 호출한다. 왜냐하면 실제 생성된 객체는 A 클래스의 객체인데 B의 멤버 함수를 호출하기 때문이다. 이렇게 문제가 있음에도 불구하고 C 언어의 형 변환 연산자는 오류 하나 내주지 않는다.

C++ 언어에서는 이러한 문제를 보완한 4개의 연산자를 추가로 제공해준다.

static_cast / const_cast / dynamic_cast / reinterpret_cast

 

  • dynamic_cast 연산자 : 상속 관계에서의 안전한 형 변환

<> 사이에 변환하고자 하는 객체의 포인터 또는 참조형으로 구성된 자료형의 이름을 둔다.
( ) 
사이에 변환하고자 하는 대상의 이름을 둔다.

이 연산자를 통한 형 변환은 다음과 같은 형 변환 요구가 따라온다.

상속 관계에 놓여 있는 두 클래스 사이에 Derived 클래스의 포인터 및 참조형 데이터를 Base 클래스의 포인터 및 참조형 데이터로 형 변환하는 경우(Derived 클래스 포인터/참조형 -> base 클래스 포인터/참조형)

위의 요구에 부합하는 형 변환 이외의 변환이(형 변환의 방향이 반대인 경우) 해당 연산자를 통해 이루어지는 경우 컴파일러 차원의 에러가 발생한다. 아래는 그에 해당하는 예제와 실행 결과이다.

그러나 39행의 경우는 크게 문제가 되지 않고, 오히려 필요한 경우도 존재할 수 있는데 이러한 형 변환을 의도적으로 진행하는 것을 의도적으로 명시하기 위한 형 변환 연산자가 제공된다.

 

, Derived 클래스 포인터/참조형 -> base 클래스 포인터/참조형으로 변환이 가능한 경우 또한 존재한다. 다음과 같은 가정 하에.

Base 클래스가 Polymorphic 클래스(하나 이상의 가상함수를 포함하는 클래스) 일 때!

상속 관계에 놓여 있는 두 클래스 사이에서 기초 클래스에 가상함수가 하나 이상 존재하면, dynamic_cast 연산자를 이용하여 Base 클래스 포인터/참조형 -> Derived 클래스 포인터/참조형으로의 변환이 가능하다! 다음은 클래스가 수정된 해당 예이다.

47번 라인은 원래 허용되지 않았던 연산이지만 가상 함수가 존재하는 이유로 허용되는 것을 볼 수 있다. 그러나 50번 라인은 pa2가 가리키는 대상을 pb2가 가리킬 수 없는 상황이다. 변환은 가능하지만 Segmentation fault 에러가 런타임에 나오는 것을 볼 수 있는데, 해당의 경우 반환하게 되면 NULL 값을 반환한다.

이렇게 dynamic_cast 형 변환 연산자는 안정적인 형 변환을 보장해주는데 컴파일 시간이 아닌 실행 시간에 안정성을 검사하도록 컴파일러가 바이너리 코드를 생성한다.

 

  • static_cast 연산자 : A 타입에서 B 타입으로

위의 연산자는 dynamic_cast 연산자와 동일한 형태를 갖는다. static_cast 연산자는 Derived 클래스 포인터/참조형 -> base 클래스 포인터/참조형으로의 변경 뿐만 아니라, base 클래스 포인터/참조형 -> Derived 클래스 포인터/참조형으로의 변경 또한 허용한다. 그러나 그에 따른 책임은 프로그래머가 지도록 되어있다. 다음은 그 예이다.

두 변환 모두 허용은 하지만 특히, 43번과 같은 변환에 대한 결과는 예측할 수 없는 책임질 수 없는 결과가 나온다. static_cast는 프로그래머의 의도적인 변환임을, 그래서 책임 또한 지겠다는 의미가 내포되어 있다.

그리고 static_cast 연산자는 기본 자료형에 대해서도 형 변환이 가능하다.

그렇다면 static_cast 연산자는 그에 따른 결과는 연산 결과를 프로그래머가 책임지면 기존의 C 언어에서의 형 변환 연산자랑 무엇이 다를까?

기존 C 언어에서의 형 변환 연산자는 위와 같은 말도 안되는 경우를 허용한다. 그러나 static_cast로 형 변환을 진행하는 경우에는 다음과 같은 결과를 확인할 수 있다.

 

  • const_cast 연산자 : const 성향을 삭제!

위의 연산자를 이용하여 const로 선언된 참조자 혹은 포인터의 const 성격을 제거하는 형 변환을 이룰 수 있다. 이와 같은 연산은 const의 가치를 떨어뜨릴 수 있지만 다음의 예와 같이 의미를 둘 수 있다.

함수의 인자 전달 시 const 선언으로 형의 불일치가 일어날 경우 요긴하게 쓰일 수 있는 연산자이다. 그러나 const에 대한 의미가 옅어지도록 하는 요소이므로 필요할 때만 사용한다. 그런데 const_cast 연산자로 변환된 형태에 대해서 값의 변경을 시도하면 런타임 에러가 발생하기는 한다.

  • reinterpret_cast 연산자 : 상관없는 자료형으로의 형 변환

위와 같이 이전의 형 변환 연산자들과 동일한 형태를 띤다. reinterpret_cast 연산자는 전혀 상관없는 자료형으로의 형 변환을 행할 때 사용되는 연산자이다. 포인터를 대상으로 하는, 그리고 포인터와 관련이 있는 모든 유형의 형 변환을 허용한다.

위와 같은 형 변환 연산이 허용된다. 클래스 C와 클래스 D는 상속 관계가 아닌 전혀 다른 타입이기 때문이다.

위의 예제와 결과는 포인터를 대상으로 하는 포인터와 관련이 있는 모든 유형의 형 변환을 허용한다는 내용에 대한 예제인데 허용이 되지 않는 것 같다(보류)

  • 형 변환 시 발생하는 예외

위와 같은 경우는 dynamic_cast 연산자의 두 번째 경우와 같은 경우인데(NULL 반환 경우), 참조자를 대상으로는 NULL 값을 반환할 수 없기 때문에 bad_cast 예외가 발생함을 참고한다.

728x90
반응형

'컴퓨터 언어 정리 > C++ 언어' 카테고리의 다른 글

24 예외 처리  (0) 2020.10.15
23 템플릿  (0) 2020.10.13
22 string 클래스 - 직접 일부 구현  (0) 2020.10.12
21 연산자 오버로딩  (0) 2020.09.23
20 다중 상속  (0) 2020.09.22
728x90
반응형

예외처리

  • 예외

프로그램 실행 도중 발생하는 예외적인 상황을 이야기 한다. 컴파일 시 발생하는 에러는 예외와는 관련이 없다. 0으로 나눗셈 등이 예외이다.

 

기존 방식의 if문을 통한 예외 처리

기존 방식의 if문을 통한 예외 처리이다. 이 경우 먼저 중요한 것은 예외가 발생하는 위치와 예외가 발견되는 위치는 다르다는 것을 알아야한다. 그리고 위와 같은 기존 방식의 if문을 통한 예외 처리의 경우 프로그램의 흐름을 구성하는 코드와 예외 처리를 위한 코드를 한 눈에 쉽게 구분하기 힘듦.

 

C++의 예외 처리 메커니즘

먼저, 위와 같이 try의 구역은 예외 발생이 예상 되는 지역을 이야기 한다. try 지역을 선정하는 것은 예외가 발생할 만한 영역 만을 선정하는 것이 아니라 이 예외와 관련된 영역 모두를 구역으로 묶고 선정해야 한다. 예외가 발생함으로써 실행 되서는 안 될 (예외 지역부터 순차적으로 실행되어 왔어야 할) 영역을 묶지 않게 된다면 문제가 되기 때문이다. , 트랜잭션의 개념으로 try 구역을 묶고 선정해야 한다.

다음은 throw 키워드를 이용하여 발생한 예외 상황에 대해서 알릴 수 있고, 알림으로써 처리를 위한 루틴(catch)으로 예외를 전달할 수 있다. 예외를 던졌다고 표현한다. throw를 통해 예외가 알려지면 해당 catch 영역 내의 해당 throw 밑의 영역은 실행하지 않고 예외가 처리되는 루틴으로 실행 흐름이 넘어간다.

마지막으로 catch 영역은 예외를 처리하는 영역이다. throw에 의해서 알려져 전달된 예외를 이 catch 블록에서 처리를 하는데 이 때, 전달된 예외의 자료형과 catch의 매개 변수의 자료형은 일치해야 전달과 처리까지 진행된다. (자료형 일치를 통해서 던져진 예외는 같은 자료형의 매개변수를 지닌 catch로 전달된다.) 이러한 catch 영역은 try에 연이어 구성되어야 하며, 하나의 try구문 당 여러 catch 구문이 구성될 수 있다. 왜냐하면 처리해야 할 예외의 자료형이 여럿일 수 있기 때문이다.

예외가 발생하지 않을 경우 try문을 실행하고, 이어서 등장하는 catch문은 뛰어 넘고 catch문 직후를 실행한다. 그러나 예외가 발생할 경우 throw에 의해 발생된 예외가 던져지고 던져진 이후의 try 문장은 실행하지 않고, 던져진 예외는 해당 예외를 감싸는 try문에 의해 감지가 되고, 이어서 등장하는 catch 블록에 전달되고 처리된다. 그렇게 처리된 후 다시 try문으로 돌아가 나머지 문장을 실행하는 것이 아니라, catch 블록 이후의 라인이 실행된다.

 

  • Throw를 통한 예외의 전달

예외가 throw에 의해 던져진 지역에서 처리가 되지 않는 경우는 함수 내에서 예외가 발생했지만 그에 따른 처리가 함수 내에서 이루어지지 않는 경우와 일맥상통하다. 위의 예제를 보면 divFunc 함수 내에서 throw에 의해 예외가 던져졌지만, 그에 상응하는 예외 처리가 divFunc 함수 지역 내에서 이루어지지 않았는데 이렇게 해당 지역에서 예외 처리가 이루어지지 않을 경우에는 예외가 발생한 함수를 호출한 영역으로 예외와 예외처리에 대한 책임이 전달된다. 위의 예에서 12번 라인에 의해 예외가 던져지면 13, 14번 라인은 진행되지 않고(함수가 종료됨) 흐름은 27번 라인으로 시작된다.

이러한 현상을 스택 풀기라고 한다. 함수 내의 또 다른 함수 호출이 이루어지는 상황에서 예외가 던져지면 함수가 호출된 순서의 반대 순서로 리턴 하듯이 종료하면서 예외가 전달된다.

 

  • 전달되는 예외의 명시

예외가 전달되는 경우가 존재하는 함수에 대해서 전달될 수 있는 예외의 종류를 명시할 수 있다. 이렇게 명시되는 예외의 종류 또한 함수의 특징이 될 수 있는데 이렇게 전달되는 예외의 종류를 명시해야만 하는 이유는 예외가 함수 내에서 처리되지 않고 전달되어 오게 되면 해당 함수 호출 부분 아래에 catch 문을 구성해야 하는데 이 catch문을 어떤 타입의 예외들로 구성을 해 놓아야 하는지를 알 수 있기 때문이다.

위와 같이 예외를 명시할 수 있다.

 

  • 예외 객체/클래스와 상속 관계에서의 catch 순서

  

위의 예제는 클래스 객체를 통해서 예외 클래스를 만들고 활용하는 예제이다. 여기서 중요한 것은 예외 객체의 상속 관계에 있는 예외들을 전달하게 되는데 받을 때 catch문의 구성이 중요하다. 먼저 왼쪽 main 함수 내의 catch 문 구성은 Base 클래스부터 catch 문을 구성하였고 아래로 내려갈수록 Derived 클래스 순으로 구성하는 것을 볼 수 있다. 오른쪽 main 함수 내의 catch 문 구성은 최하위 Derived 클래스부터 아래로 내려갈수록 Base 클래스 순으로 구성을 하였는데 이에 대한 결과는 다음과 같다.

왼쪽 main 함수에서는 전달되어 온 예외들은 모두 첫 번째 catch ExceptionA 클래스가 모두 받는다. 반면에 오른쪽 main 함수에서 전달되어 온 예외들은 정상적으로 각 예외를 적절한 catch문이 받는 것을 확인할 수 있다. 왜냐하면 Base 클래스가 Derived 클래스 형태로 전달되어 온 예외 객체를 모두 받기 때문이다.

  • new 연산자에 의해 발생되는 예외

bad_alloc 예외 클래스는 new 헤더에 만들어져 있고 메모리 공간의 할당이 실패했음을 알리는 의도로 정의된 예외 클래스이다.

  • 모든 예외를 처리하는 catch 블록

위와 같이 마지막 catch“…” 을 매개변수로 넣은 catch 문을 확인할 수 있는데 이 때, “…”의 의미는 위에서 거르지 못하고 전달되어 온 예외들을 모두 받아드리겠다는 선언이다. , 위와 같은 선언을 할 시에는 발생하는 예외에 대해서 어떤 정보도 받을 수 없고, 예외의 종류 또한 구분할 수 없다. 

  • 전달되어 온 예외, 다시 던지기!

예외를 이중으로 다시 던지는 것이 가능함을 보여주는 예이다. 하지만 간결한 구조를 위해 굳이 예외를 거듭해서 던질 필요 없다.

728x90
반응형

'컴퓨터 언어 정리 > C++ 언어' 카테고리의 다른 글

25 형 변환  (0) 2020.10.17
23 템플릿  (0) 2020.10.13
22 string 클래스 - 직접 일부 구현  (0) 2020.10.12
21 연산자 오버로딩  (0) 2020.09.23
20 다중 상속  (0) 2020.09.22
728x90
반응형

템플릿

  • 함수 템플릿

모형자에 비유하여 함수 템플릿을 설명

모형자는 모형을 만들어 낸다. 모형의 틀은 결정되어 있지만, 모형의 색은 결정되어 있지 않아서 결정해야함.
함수 템플릿은 함수를 만들어 낸다. 함수의 기능은 결정되어 있지만, 자료형은 결정되어 있지 않아서 결정해야함.

, 함수 템플릿은 함수를 만드는 도구가 될 수 있다. 함수의 기능은 정해져 있으나 그 기능 수행에 사용되어지는 요소들의 자료형이 결정되어 있지 않기 때문에 다양한 자료형의 함수를 만들어 낼 수 있다.

위의 예는 기본 함수를 나타낸 것이다. 정수형에 대한 곱셈 연산을 진행하는 함수이다. 이를 템플릿으로 정의하게 되면 다음과 같다.

곱셈 연산을 진행하는 함수이지만 연산의 대상에 대한 자료형은 결정되어 있지 않고, 나중에 T를 대신해서 실제 자료형을 결정하겠다는 뜻이다. 여기서 T는 자료형을 결정짓지 않겠다는 의미이고, 함수를 만들어 내는 템플릿을 정의하기 위해 사용된 것이다. 위의 예를 완성시키면 다음과 같다.

14번 라인, 15번 라인에서 <자료형> 방식을 통해 함수 템플릿의 T 를 결정 짓고 호출한다는 뜻이다. 컴파일러는 컴파일 할 때, 각 자료형에 따른 함수를 하나씩 만들고, 호출하게 된다. 컴파일 속도는 떨어지지만 실행 속도가 지연되지는 않으므로 문제가 없다.

다음과 같이 함수를 호출할 때, <자료형> 형식으로 호출하지 않고, 일반 함수 호출 방법도 허용한다. 다만, 컴파일러에 의해서 전달되는 인자로 호출될 자료형의 함수를 판단한다.

17, 18번 라인과 같이 호출 가능.

 

  • 함수 템플릿과 템플릿 함수

 

  • 두 개의 이상의 자료형을 이용하기

8번 라인에서 매개 변수의 타입은 이미 정해졌기 때문에 호출 시 18~ 20번 라인과 같이 템플릿 T1, T2의 자료형을 명시해줘야 한다. 그리고 12번 라인에서 형 변환의 두 가지 방법이 나오는데 모두 같은 효과를 지닌다.

  • 함수 템플릿의 특수화

함수 템플릿의 특수화란 함수 템플릿을 통해 템플릿 함수를 구현할 때, 구성 방법에 예외를 둘 필요가 있을 때 사용하는 방법이다. 위의 예제에서 6번 라인에서의 compare 함수는 두 인자에 대한 비교 후 큰 값을 출력하도록 되어 있는데 18번 라인과 같은 경우는 의미가 없는 비교가 된다. 왜냐하면 사전식 비교나 문자열 길이 비교가 아닌 주소 값의 비교이기 때문이다. 이러한 경우, compare 템플릿 함수의 구성에서 문자열에 대한 템플릿 함수는 의미가 없는 구성이기 때문에 다음과 같이 예외로 두고 새롭게 정의해야 한다.

12, 18번 라인과 같이 적용하면 함수 템플릿의 특수화가 적용이 된다. , 예외가 적용되는 것이다. 12, 18번 라인들의 경우 각각 char*, const char* 형의 템플릿 함수는 예외로 두고 직접 제시할 테니 해당 함수들이 호출될 경우 템플릿 함수로 구성하지 말고 가져다 쓰라는 뜻이다. 위의 예에서는 문자열에 대한 비교에서 길이 비교, 사전식 비교를 진행하는 예외 두 가지를 두었다

  • 클래스 템플릿

클래스 또한 클래스 템플릿을 정의하고 이를 기반으로 컴파일러에 의해 템플릿 클래스를 만들 수 있다.

위의 예에서 볼 수 있듯이 클래스 템플릿을 정의하고 이를 통해 템플릿 클래스의 객체를 생성할 수 있는데 25~27번 라인처럼 T에 대한 자료형을 명시한 채로 객체를 생성해야 한다. 함수처럼 생략은 불가능하다.

클래스 내의 템플릿 함수의 정의와 선언을 분리하는 방법이다.

  • 특정 템플릿 클래스의 객체를 인자로 받는 일반 함수 정의 및 friend 선언

위와 같이 일반 함수를 대상, 매개 변수로 템플릿 클래스를 받을 수 있으며 템플릿 클래스를 상대로 friend 선언 또한 가능하다.

  • 클래스 템플릿 특수화

함수 템플릿의 특수화를 하는 이유 : 특정 자료형에 대해서 구분이 되는 다른 기능을 보이기 위함.
클래스 템플릿의 특수화를 하는 이유 : 특정 자료형을 기반으로 생성된 객체에 대해서 구분이 되는 다른 행동 양식을 적용하기 위함.

19, 20번 라인과 같이 특수화시킬 수 있다.

  • 클래스 템플릿의 부분 특수화와 우선 순위

34번 라인과 같은 경우를 부분 특수화라고 한다. 부분적으로만 자료형을 지정하지 않았기 때문이다. 그렇다면 전체 특수화와 부분 특수화 모두 지정한 경우 50번 라인과 같이 겹치는 자료형에 의한 객체 생성이 발생한다면 어떻게 될까? 그 결과는 전체 특수화된 클래스를 대상으로 객체가 생성된다. , 전체 특수화가 부분 특수화보다 우선시 된다.

  • 템플릿 매개 변수와 템플릿 인자

템플릿 정의할 시에 결정되지 않은 자료형을 의미하는 T, T1 따위의 문자를 템플릿 매개 변수라고 함. 이러한 템플릿 매개 변수에 전달되는 자료형 정보를 템플릿 인자라고 한다.

이러한 템플릿 매개 변수에는 변수의 선언이 올 수 있다.

6번 라인과 같이 템플릿 매개 변수에 일반 변수의 선언이 허용되고, 템플릿 인자로 값을 받을 수 있다. 이 때 받아온 템플릿 인자를 통해서 10번 라인과 같이 멤버 변수에 대한 배열 길이를 정의 한다. 배열 길이의 정의는 생성자로도 충분히 할 수 있지만 생성자를 통해서 동적 할당을 통한 배열 구현의 경우 24번 라인에 대해서 컴파일 에러가 발생하지 않지만, 템플릿 인자를 통한 배열 구현의 경우 24번 라인에 대해서 컴파일 에러가 발생한다. 왜냐하면 <int, 5><int, 3>은 서로 다른 자료형으로 취급하기 때문이다.

  • 템플릿 매개변수의 디폴트 값

템플릿 매개변수 또한 디폴트 값을 가질 수 있다.

  • 템플릿과 static

Static 멤버는 클래스 변수로 객체 별로 생성되는 변수가 아닌 클래스 별로 생성되는 변수로서 객체가 생성되지 않은 상황에서도 이미 생성되어 있는 변수이다. 템플릿을 적용한 템플릿 클래스에서 또한 템플릿 클래스 별로 static 멤버 변수를 유지한다. , 위의 예에서 Data<int> 클래스에 하나, Data<short> 클래스에 하나씩 유지한다. 그렇기 때문에 d1d2(or d3d4)는 서로 다른 객체이지만 static 멤버가 공유된 결과를 볼 수 있다.

 

  • template<typename T …> / template <>

template<typename T …> / template <>

위 키워드의 쓰임은 함수 템플릿 및 클래스 템플릿을 정의 혹은 그 외의 템플릿 관련 정의에 대해서 위와 같은 선을 두고 템플릿의 일부 또는 전부를 정의하고 있다는 사실을 컴파일러에게 알린다. , T가 무엇인지 그리고 정의 및 특수화, 부분화 정의를 시키고 있다 라는 것을 알리기 위함이다. 전자의 경우는 템플릿 매개변수가 사용된 정의에 대해서 관련 템플릿 문자를 맞춰서 선언하고, 후자는 템플릿 문자를 사용하지 않았을 때 선언한다.

  • 템플릿 static 멤버 변수 초기화의 특수화

위 예시의 한 부분인데 위와 같이 static 멤버를 초기화하면 모든 템플릿 클래스에 대해서 0으로 초기화 된다.( Data<char>, Data<int>, Data<long>, Data<short> ) 하나의 자료형에 대해서만 다른 값으로 초기화해야 하는 경우는 다음예의 22, 23번 라인과 같이 특수화 하면 된다.

728x90
반응형

'컴퓨터 언어 정리 > C++ 언어' 카테고리의 다른 글

25 형 변환  (0) 2020.10.17
24 예외 처리  (0) 2020.10.15
22 string 클래스 - 직접 일부 구현  (0) 2020.10.12
21 연산자 오버로딩  (0) 2020.09.23
20 다중 상속  (0) 2020.09.22
728x90
반응형

-

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
#include <iostream>
#include <string>
#include <cstring>
 
namespace mystd
{
    using std::cout;
    using std::cin;
    using std::endl;
    using std::printf;
    using std::ostream;
    using std::istream;
 
    class string
    {
    private:
        char * str;
        int str_len;
    public:
        string();
        string(const string& r_string);
        string(const char * str);
        ~string();
 
        string& operator=(const string& r_string);
        string& operator=(const char * str);
        const string operator+(const char * str);
        const string operator+(const string& r_string);
        string& operator+=(const char * str);
        string& operator+=(const string& r_string);
        bool operator== (const string& r_string);
        bool operator== (const char * str);
 
        friend ostream& operator<<(ostream& os, const string& r_string);
        friend const string operator+(const char * str, const string& r_string);
        friend char * operator+=(char * str, const string& r_string);
        friend bool operator==(const char * str, const string& r_string);
    };
    istream& operator>> (istream& is, string& r_string);
    ostream& operator<< (ostream& os, const string& r_string);
    const string operator+(const char * str, const string& r_string);
    char * operator+=(char * str, const string& r_string);
    bool operator==(const char * str, const string& r_string);
};
 
using namespace mystd;
 
int main(void)
{
    string str1 = "I like "// 복사 생성자
    string str2 = "string class"// 복사 생성자
    string str3 = str1 + str2; // 복사 생성자, + 연산자 오버로딩
 
    cout<<"------ str1 ------"<<endl;
    cout<<str1<<endl// << 연산자 오버로딩 전역
    cout<<"------ str2 ------"<<endl;
    cout<<str2<<endl// << 연산자 오버로딩 전역
    cout<<"------ str3 ------"<<endl;
    cout<<str3<<endl// << 연산자 오버로딩 전역
 
    string str4;
    str4 = "HELLO";
    cout<<"------ str4 ------"<<endl;
    cout<<str4<<endl;
 
    string str5;
    str5 = str3 + str4 ;
    cout<<"------ str5 ------"<<endl;
    cout<<str5<<endl;
 
    string str6;
    str6 = str4 + " AAA" ;
    cout<<"------ str6 ------"<<endl;
    cout<<str6<<endl;
 
    string str7;
    str7 =  "AAA " + str4 ;
    cout<<"------ str7 ------"<<endl;
    cout<<str7<<endl;
 
    string str8 = "asdf";
    string str9 = "fdsa";
 
    str8 += str9;
    str9 += "asdf";
    cout<<"------ str8 ------"<<endl;
    cout<<str8<<endl;
    cout<<"------ str9 ------"<<endl;
    cout<<str9<<endl;
 
    cout<<"------ str11 ------"<<endl;
    string str10="222222222";
    char str11[30= "fdjskalfjklsaf";
    str11+=str10;
    printf("%s\n",str11+=str10);
 
    string str12 = "ZXC1";
    string str13 = "ZXC";
    if(str12 == str13)
        cout<<"동일한 문자열이다1"<<endl;
    else
        cout<<"동일하지 않은 문자열이다1"<<endl;
 
    char str14[10= "ZXC";
    if(str12 == str14)
        cout<<"동일한 문자열이다2"<<endl;
    else
        cout<<"동일하지 않은 문자열이다2"<<endl;
 
    if(str14 == str12)
        cout<<"동일한 문자열이다3"<<endl;
    else
        cout<<"동일하지 않은 문자열이다3"<<endl;
 
    cout<<"문자열 입력 : ";
    cin>>str4; // >> 연산자 오버로딩 전역
    cout<<"입력한 문자열 : "<<str4<<endl// << 연산자 오버로딩 전역
 
    return 0;
}
 
 
mystd::string::string() : str(NULL), str_len(0) { }
mystd::string::string(const string& r_string) : str_len(r_string.str_len)
{
    this->str = new char[r_string.str_len+1];
    strcpy(this->str, r_string.str);
}
mystd::string::string(const char * str) : str_len(strlen(str))
{
    this->str = new char[strlen(str)+1];
    strcpy(this->str, str);
}
mystd::string::~string()
{
    delete []this->str;
}
 
string& mystd::string::operator=(const string& r_string)
{
    this->str_len = r_string.str_len;
 
    if(this->str == NULL)
    {
        this->str = new char[this->str_len+1];
    }
    else
    {
        delete []this->str;
        this->str = new char[this->str_len+1];
    }
    strcpy(this->str, r_string.str);
    return *this;
}
 
string& mystd::string::operator=(const char * str)
{
    this->str_len = strlen(str);
 
    if(this->str == NULL)
    {
        this->str = new char[this->str_len+1];
    }
    else
    {
        delete []this->str;
        this->str = new char[this->str_len+1];
    }
 
    strcpy(this->str, str);
    return *this;
}
const string mystd::string::operator+(const char * str)
{
    char * tmp_str = new char[this->str_len + strlen(str) + 1];
    string tmp;
 
    strcpy(tmp_str, this->str);
    strcpy(tmp_str + this->str_len, str);
 
    tmp = tmp_str; // 대입 연산자 오버로딩
    delete []tmp_str;
    return tmp;
}
const string mystd::string::operator+(const string& r_string)
{
    char * tmp_str = new char[this->str_len + r_string.str_len + 1];
    string tmp;
 
    strcpy(tmp_str, this->str);
    strcpy(tmp_str + this->str_len, r_string.str);
 
    tmp = tmp_str; // 대입 연산자 오버로딩
    delete []tmp_str;
    return tmp;
}
string& mystd::string::operator+=(const char * str)
{
    int tmp_strlen = this->str_len + strlen(str);
    char * tmp_str = new char[tmp_strlen + 1];
 
    strcpy(tmp_str, this->str);
    strcpy(tmp_str+this->str_len, str);
 
    if(this->str == NULL)
        this->str = new char[tmp_strlen + 1];
    else
    {
        delete []this->str;
        this->str = new char[tmp_strlen + 1];
    }
    strcpy(this->str, tmp_str);
    this->str_len = tmp_strlen;
 
    delete []tmp_str;
    return *this;
}
string& mystd::string::operator+=(const string& r_string)
{
    int tmp_strlen = this->str_len + r_string.str_len;
    char * tmp_str = new char[tmp_strlen + 1];
 
    strcpy(tmp_str, this->str);
    strcpy(tmp_str+this->str_len, r_string.str);
 
    if(this->str == NULL)
        this->str = new char[tmp_strlen + 1];
    else
    {
        delete []this->str;
        this->str = new char[tmp_strlen + 1];
    }
    strcpy(this->str, tmp_str);
    this->str_len = tmp_strlen;
 
    delete []tmp_str;
    return *this;
}
bool mystd::string::operator== (const string& r_string)
{
    if(!strcmp(this->str, r_string.str))
        return true;
    else
        return false;
}
bool mystd::string::operator== (const char * str)
{
    if(!strcmp(this->str, str))
        return true;
    else
        return false;
}
 
istream& mystd::operator>> (istream& is, string& r_string)
{
    char str[100];
    is>>str;
    r_string = string(str);
    return is;
}
ostream& mystd::operator<< (ostream& os, const string& r_string)
{
    os<<r_string.str;
    return os;
}
const string mystd::operator+(const char * str, const string& r_string)
{
    char * tmp_str = new char[r_string.str_len + strlen(str) + 1];
    string tmp;
 
    strcpy(tmp_str, str);
    strcpy(tmp_str + strlen(str), r_string.str);
 
    tmp = tmp_str; // 대입 연산자 오버로딩
    delete []tmp_str;
    return tmp;
}
char * mystd::operator+=(char * str, const string& r_string)
{
    int i = 0;
    int tmp_strlen = strlen(str) + r_string.str_len;
    char * tmp_str = new char[tmp_strlen + 1];
 
    strcpy(tmp_str, str);
    strcpy(tmp_str + strlen(str), r_string.str);
 
    while(str[i++]!=0); // str의 사이즈를 구한다.(i-2)
    strncpy(str,tmp_str,i-2); // str의 기존 사이즈만큼 복사한다. 짤리겠지.
    delete []tmp_str;
    return str;
}
bool mystd::operator==(const char * str, const string& r_string)
{
    if(!strcmp(str, r_string.str))
        return true;
    else
        return false;
}
cs

-

 

직접 구현해보았으나 설명 및 풀이를 시간이 나면 진행을 해보겠다.

728x90
반응형

'컴퓨터 언어 정리 > C++ 언어' 카테고리의 다른 글

24 예외 처리  (0) 2020.10.15
23 템플릿  (0) 2020.10.13
21 연산자 오버로딩  (0) 2020.09.23
20 다중 상속  (0) 2020.09.22
19 멤버함수 가상함수의 동작 원리  (0) 2020.09.15
728x90
반응형

연산자 오버로딩

  • 연산자 오버로딩

C++언어에서는 연산자의 오버로딩을 통해서 기존에 존재하던 연산자의 기본 기능 이외에 다른 기능을 추가할 수 있다.

먼저 15번 라인을 통해서 operator+ 라는 함수를 제공한다. 이 함수를 정의함으로써 연산자를 오버로딩한다. 29번 라인을 보면 operator+ 멤버 함수를 호출함으로써 a1의 멤버 변수 value의 값과 a2의 멤버 변수 value의 값을 더한 결과를 a3로 객체 복사를 통해서 전달하고 있음을 확인할 수 있다. 그런데 이러한 과정이 30번 라인에서도 똑같이 일어나고 있음을 알 수 있다. , 정리해보자면 다음과 같음을 알 수 있다.

이는 객체 또한 기본 자료형 변수처럼 취급하고 각종 연산이 가능하도록 하려는 C++의 기본 철학이다. operator 키워드와 연산자를 묶어서 함수를 정의하면 함수의 이름(operator’연산자’)을 이용한 함수 호출은 물론 연산자만을 이용해도 operator’연산자함수 호출로 바꾸어 준다.

  • 연산자 오버로딩의 두 가지 방법

멤버 함수에 의한 연산자 오버로딩

위에서 제시한 예의 경우이다.

전역 함수에 의한 연산자 오버로딩

다음의 예를 통해 설명.

20번 라인에서 private 영역에 접근을 위해서 전역 함수를 friend 선언을 해 주었다. 그리고 22번 라인에서 + 연산자에 대해서 전역 함수 형태로 오버로딩 하였다. 이 함수를 이용해서 30번 라인에서 a1+a2operator+(a1,a2) 형태로 해석이 된다.

 

l  오버로딩이 가능한 / 불가능한 연산자의 종류

오버로딩 불가능 연산자

오버로딩 가능 연산자

 

  • 연산자 오버로딩 주의사항

1.     본래의 의도를 벗어난 형태의 연산자 오버로딩은 좋지 않다.

2.     연산자의 우선순위와 결합성은 바뀌지 않는다.

3.     매개변수의 디폴트 값 설정이 불가능하다

4.     연산자의 순수 기능까지 빼앗을 수 없다.

 

  • 단항 증감 연산자(선 증감 후 연산)의 오버로딩

operator++( ) 처럼 인자에 아무 것도 명시하지 않은 경우에는 선 증감 후 연산의 증감 연산자가 오버로딩 된다.

  • 단항 증감 연산자(선 연산 후 증감)의 오버로딩

operator++(int)처럼 인자에 int 키워드를 넣어 명시하는 경우에는 선 연산 후 증감의 증감 연산자가 오버로딩 된다. 인자 전달의 int 의미는 아니라는 것. 이 때 int 키워드는 매개 변수의 가장 뒤에 넣는다.

21번 라인이나 30번 라인과 같이 Const를 이용하여 객체를 반환한 이유는 단순히 const A 타입은 returnObj 반환하기 때문에 const를 기재한 것이 아니라, 반환함으로써 생기는 임시 객체를 const 객체로 생성한다는 의미에서 const를 기재한 것임.

선 연산 후 증감에 대한 연산자의 경우 해당 연산이 존재하는 라인에서는 증가되지 않은 값을 출력해야 한다. 그리고 또한 그 값을 변경할 수도 없기 때문에 참조 값을 반환하지 않을 뿐 더러, 임시 객체를 const로 반환한다.

 

(a1++)++ 이 불가능한 이유

(a1++)++ -> (a1.operator++( ))++ -> (A const 임시객체)++ -> (A const 임시객체).operator++( )

이 과정에서 마지막 operator++( ) 호출이 불가능하다. 왜냐하면 const로 선언된 함수 내에서는 const 함수의 호출만을 허용하기 때문이다.

 

  • 교환 법칙이 필요한 연산자의 오버로딩 ( * )

멤버 함수의 * 연산자 오버로딩에서, 이와 같은 경우는 멤버 함수의 연산자 오버로딩으로 설명이 가능하다.

a1 * 3; -> a1.operator*(3);

그러나, 아래와 같은 경우는 멤버 함수의 연산자 오버로딩으로 설명이 불가능하다

3 * a1; -> 3.operator*(a1);   -   ?

이러한 경우에는 전역 함수의 형태로 연산자 오버로딩을 진행해야 한다.

a1 * 3; -> operator*(a1, 3);
3 * a1; -> operator*(3, a1);

교환 법칙을 성립한 연산자 오버로딩의 예제이다.

  • cout, cin, endl의 정체 ( >> 연산자의 오버로딩 )

정체는 위의 예와 같다. cout은 생성된 객체의 이름이었고, endl은 개행 기능을 하는 함수이다.

  •  <<, >> 연산자의 오버로딩 ( 특별 자료형에 대한 coutcin의 구현 )

coutcin에 대해서 <<, >> 연산자 오버로딩을 하기 위해서는 전역 함수를 통한 오버로딩을 해야 한다. 왜냐하면 멤버 함수를 통하여 오버로딩을 하기 위해서는 ostream(객체 cout의 클래스) 클래스와 istream(객체 cin의 클래스) 클래스를 수정해야 하는데 그것은 불가능하기 때문이다.

  • 대입 연산자의 오버로딩

대입 연산자 오버로딩은 복사 생성자와 그 성격이 매우 유사하다.

디폴트 대입 연산자 : 객체 간의 대입 연산

다음은 복사 생성자와 대입 연산자의 공통점이다.

다음은 복사 생성자와 대입 연산자의 유일한 차이점이다.

그러므로 객체 간의 대입 연산을 진행할 시에는 복사 연산자의 경우처럼 깊은 복사 문제에 대해서도 고려를 해야 한다

상속 관계에서의 대입 연산자 오버로딩

Derived 클래스를 통하여 디폴트 대입 연산이 진행되면 base 클래스의 대입 연산자까지 호출한다. 그러나 derived 클래스에서 대입 연산자를 정의해주면 명시적으로 base 클래스의 대입 연산자의 호출문을 삽입하지 않으면, base 클래스의 대입 연산자는 호출되지 않아서 base 클래스의 멤버 대 멤버 복사는 이루어지지 않는다.

 

대입 연산자 호출과 이니셜라이저의 효과

38번 라인처럼 이니셜라이저를 사용하면 선언과 동시에 초기화가 이뤄지는 형태로 바이너리 코드가 생성되기 때문에(A aaa=ref와 같은 효과) 58번 라인에서는 복사 생성자만 호출된다. 그러나 생성자의 몸체 부분에서 대입 연산을 통한 초기화를 진행하면, 선언과 초기화를 각각 별도의 문장에서 진행하는 형태로 바이너리 코드가 생성된다. 그렇기 때문에 50번 라인처럼 초기화를 진행하는 경우이기 때문에 60번 라인은 생성자 이외에 대입 연산자자 호출되는 것이다.

  • 배열의 인덱스 연산자 오버로딩

C언어 및 C++언어의 배열은 배열 인덱스에 대한 검사를 하지 않는다. 그렇기 때문에 배열이름[범위를 넘어선 인덱스]의 경우를 허용한다. 이런 허용을 막는 배열 클래스를 만들어 볼 것이다. 배열의 역할을 하는 클래스.

위와 같이 만든 배열 클래스를 이용하여 배열 간의 대입을 이루어지게 하면 얕은 복사가 이루어지므로 깊은 복사가 이루어지도록 복사 생성자 및 대입 연산자를 수정할 수 있다. 그러나 배열은 저장소의 일종이고, 저장소에 저장된 데이터는 유일성이 보장되어야 하기 때문에 저장소의 복사는 불필요하거나 잘못된 일로 간주된다.

그러므로 다음과 같이 복사 생성자나 대입 연산자를 private 멤버로 둠으로써 복사와 대입을 차단한다.

 

  • Const 함수의 오버로딩

위의 예제에서 문제는 showAllData 함수의 매개 변수로 const Array& ref 형태로 Array 객체를 받아들이는데 41번 라인에서 const화 되지 않은 함수 operator[] 가 호출된다. 그렇기 때문에 에러가 발생하는데 그렇다고 해서 operator[] 함수를 const화 할 수도 없는 노릇이다. 왜냐하면 배열에 저장이 불가능해지기 때문이다. 그렇기 때문에 const 선언 유무에 따른 함수 오버로딩을 통해 위의 문제를 해결해야 한다.

 

  • new & delete 연산자의 오버로딩

new 연산자 오버로딩

위와 같은 문장에서 new 연산자가 오버로딩되어 있는 상태라면 어떻게 해석해야 하는가? 해석할 수 없다. 왜냐하면 오버로딩 된 new 연산자는 기본적으로 제공하는 new 연산자를 완벽 대체가 불가능하다.

기본 new 연산자의 기능

1.     메모리 공간의 할당

2.     생성자의 호출

3.     할당하고자 하는 자료형에 맞게 반환된 주소 값의 형 변환

기본 new 연산자의 기능 중 2, 3번은 여전히 컴파일러가 맡게 되고, 1번의 기능인 메모리 공간의 할당만 오버로딩 할 수 있다.

위와 같은 형태로 오버로딩 된다. 출력 타입은 무조건 void * 형태이고, 매개 변수로 사이즈를 받을 수 있다.

오버로딩 된 new 연산자를 이용하여 위와 같이 할당을 하더라도 new 연산자가 반환하는 값은 operator new 함수가 반환하는 값이 아니고, 컴파일러에 의해서 적절히 형 변환이 된 값이다. 또한 생성자의 호출정보는 operator new 함수와 아무런 상관이 없다.

 

delete 연산자 오버로딩

위와 같은 형태로 delete 연산자를 오버로딩 한다.

 

newdelete 연산자 오버로딩의 예

18번 라인과 24번 라인에서 static을 넣은 이유는 객체가 생성이 되기 전에 호출되어야 하기 때문이다.

new, delete 연산자를 이용하여 배열 할당 및 소멸을 할 경우에는 다음의 operator 함수가 호출된다.

 

  • 포인터 연산자 오버로딩

아래의 예는 위의 포인터 관련 연산자를 구현한 예제이다.

40번 라인의 경우에는 객체의 참조 값에 대입이 이루어지고 있으니 대입 연산자가 호출되어 number 멤버 변수에 값이 저장되었고, 41번 라인에서는 Number*형 주소 값을 이용하여 -> 연산자(추가)를 이용하여 멤버 함수에 간접 접근을 통해 호출한다. 그리고 42번 라인은 객체의 참조 값을 이용하여 .연산자를 이용하여 멤버 함수에 직접 접근을 통해 호출한다. 위의 예처럼 main 함수에서 private 멤버의 값을 변경하는 연산은 좋지 못함.

 

  • 스마트 포인터

스마트 포인터란 포인터의 역할을 하는 객체를 뜻한다.     

객체에 대해서 포인터 역할을 하는 객체이면서도 가리키는 객체에 대해서 delete 연산이 자동으로 이뤄졌다는 점에서 스마트 포인터이다. 위의 예는 아주 작은 기능의 스마트 포인터를 구현해 본 예이다.

 

  • ( ) 연산자의 오버로딩

( ) 연산자를 오버로딩 하면 객체를 함수처럼 사용할 수 있다. 함수처럼 동작하는 클래스를 가리켜 펑터(Functor)라고 한다.

 

  • 임시 객체로의 자동 형 변환과 형 변환 연산자

 

위의 예제에서 39번 라인에서 서로 다른 자료형 임에도 불구하고 대입 연산이 이루어 졌음을 확인할 수 있다.

num=30;

num = Number(30);
임시 객체의 생성

num.operator=(Number(30));
임시 객체를 대상으로 하는 대입 연산자 호출

위와 같은 단계로 대입이 이루어짐. 이는 정리를 해보자면,

A형 객체가 와야 할 위치에 B형 데이터(또는 객체)가 왔을 경우, B형 데이터를 인자로 전달받는 A형 클래스의 생성자 호출을 통해서 A형 임시 객체를 생성한다.

, Number 객체가 와야 할 위치에 정수형 데이터가 왔기 때문에 정수형 데이터를 인자로 전달받는 Number 형 클래스의 생성자 호출을 통해 해당 정수형 데이터가 대입되어 있는 Number형 임시 객체를 생성한다.

그렇다면 반대로 Number형 객체를 정수형 데이터 타입으로 변경할 수 있는지 다음의 예제로 확인한다

23번 라인에서 int 형 변환 연산자 오버로딩이 이루어지고 있다. 형 변환 연산자는 반환 형을 명시하지 않는다. 그러나 return 문을 통해서 값의 반환은 가능하다.

46번 라인에서 먼저 num1int 형 변환 발생이 일어나고 그 결과 값을 3과 더한 뒤, 그 정수 값을 다시 num2에 대입하는 과정에서 num2의 생성자가 호출되어 임시 객체를 통한 대입 연산자가 호출되고 대입이 이루어진다.

728x90
반응형

'컴퓨터 언어 정리 > C++ 언어' 카테고리의 다른 글

23 템플릿  (0) 2020.10.13
22 string 클래스 - 직접 일부 구현  (0) 2020.10.12
20 다중 상속  (0) 2020.09.22
19 멤버함수 가상함수의 동작 원리  (0) 2020.09.15
18 상속과 다형성  (0) 2020.09.15
728x90
반응형

다중 상속

  • 다중 상속

위와 같은 상황을 다중 상속된 상황이다. 이러한 다중상속의 상황에서는 멤버 함수의 이름을 명확하게 구분 짓지 않으면 모호성이 발생하게 된다. 이런 경우에는 범위를 지정하여 멤버 함수를 호출하는 방법이 있다.

 

DeepDerived 클래스에서 DerivedLeft::func(); DerivedRight::func(); 또한, DeepDerived 클래스에서 Base 클래스의 멤버 함수를 호출할 때 DerivedLeft에 상속된 멤버를 호출해야 할지, DerivedRight에 상속된 멤버를 호출해야 할지 알 수 없는 경우가 발생한다. 이런 경우에는 가상 상속 개념을 이용한다.

위의 상황에서는 Base 클래스가 두 번 상속되게끔 되어 있다. 그러나 이 두 번 상속되는 다중 상속의 상황을 한 번 만 상속되도록 하는 문법이 가상 상속 개념이다. virtual 키워드를 이용하여 가상 상속을 적용하자.

위와 같은 방법을 적용하면 DeepDerived 클래스에서 Base 클래스의 멤버 함수를 호출할 때 DerivedLeft에 상속된 멤버를 호출해야 할지, DerivedRight에 상속된 멤버를 호출해야 할 지에 대한 문제는 자동으로 해결된다. 왜냐하면 Base 클래스가 딱 한 번 상속되기 때문이다. 그렇기 때문에 Base 클래스의 생성자도 한 번 실행되고, Base 클래스의 함수 또한 하나씩만 존재하게 된다.( 두 클래스에 걸쳐 있는 형태로 )

728x90
반응형

'컴퓨터 언어 정리 > C++ 언어' 카테고리의 다른 글

22 string 클래스 - 직접 일부 구현  (0) 2020.10.12
21 연산자 오버로딩  (0) 2020.09.23
19 멤버함수 가상함수의 동작 원리  (0) 2020.09.15
18 상속과 다형성  (0) 2020.09.15
17 상속  (0) 2020.09.14
728x90
반응형

멤버함수 가상함수의 동작 원리

  • 가상함수

객체 포인터가 참조를 할 때, 포인터 연산의 기준으로 두는 것은 실제 가리키는 객체의 자료형이 아니라, 선언된 포인터의 자료형을 기준으로 한다. 그로 인해 포인터 변수의 자료형에 따라서 호출되는 함수의 종류가 달라진다.

이와 같은 상황을 발생하지 않도록 가상함수 라는 문법을 제공.

위 소스에서 포인터 변수의 자료형을 기준으로 판단하여 호출되는 함수를 결정하는 상황을 발생시킬 수 있는 오버라이딩 된 함수들이다. 이러한 오버라이딩 된 함수들을 "가상 함수"로 만듦으로써, 실제 가리키는 객체의 자료형을 기준으로 판단하여 호출 함수를 결정하게 만들 수 있다.

가상함수로 만드는 것은 위의 소스처럼 해당 함수 앞에 "virtual" 키워드를 붙임으로써 완성한다.

함수를 가상함수로 만들게 되면 이를 "오버라이딩 하는" 함수들 또한 virtual 키워드를 붙이지 않더라도 자동으로 가상 함수화 된다.

  • 순수 가상함수, 추상 클래스

클래스 중에는 객체 생성을 목적으로 하지 않는 클래스가 존재한다. 상속을 위한 Base 클래스로서 존재하는 클래스들이 존재하는데, 이를 객체로 생성하게 되는 것은 개발자의 실수이다.

이러한 실수를 방지하기 위해 가상함수를 순수 가상함수로 선언하여 객체의 생성을 방지한다.

순수 가상함수란? 함수의 몸체가 정의되지 않은 함수를 말한다. 이를 포함한 클래스는 불완전한 클래스이기 때문에 객체를 생성하게 되면 컴파일 에러가 발생한다. 이러한 순수 가상함수가 멤버 함수로 포함된 불완전한 클래스를 추상 클래스라고 한다.

 

  • 가상 소멸자

소멸자 또한 가상함수 선언이 된다. Base 클래스를 Derived 클래스가 상속하였을 때, Derived 클래스의 생성자를 호출하고 바디를 실행하기 이전에 Base 클래스 생성자를 호출한다. 이 두 생성자는 멤버 변수의 동적 할당을 수행한다고 가정한다! 이 때, 객체 포인터를 이용하여 객체를 할당하였다면 소멸자 또한 객체 포인터의 자료형을 기준으로 판단하여 경우에 따라서는 Derived 클래스의 소멸자가 실행되지 않을 수 있다.

이를 방지하기 위해서 소멸자에 virtual 키워드를 붙임으로써 해결한다.

  • 다형성(polymorphism)

문장은 같은데 결과는 다른 것을 다형성이라고 한다. ( 동질이상, 열혈 C++ 참고 )
오버라이딩, 순수 가상함수 개념이 다형성에 해당한다. 이는 참조자(레퍼런스) 에서도 똑같이 적용된다.

  • 멤버함수의 진실

멤버 변수는 객체 내에 존재한다. 그러나 사실, 멤버 함수는 객체 안에 존재하지 않는다. 멤버 함수는 사실, 메모리 한 공간에 위치하고, 해당 클래스로 선언되어 생성된 객체들이 이 한 공간에 위치한 멤버 함수들을 공유하는 형태로 존재한다.

  • V-Table

위와 같은 두 클래스는 V Table이라는 가상 함수 테이블을 구성하게 된다. 객체 생성이 되지 않더라도 컴파일러에 의해 가상함수 테이블을 만든다.

위의 예제에 해당하는 가상 함수 테이블은 2개가 다음과 같은 형태로 구성이 된다.

유심히 살펴봐야 할 부분은 Derived 클래스의 테이블인데, 이 테이블에서는 상속을 받은 입장인대도 불구하고, Base::f1의 정보가 존재하지 않는다. 이는 오버라이딩 된 가상 함수의 정보는 존재하지 않고, 유도 클래스의 오버라이딩 한 가상 함수만이 존재한다.

728x90
반응형

'컴퓨터 언어 정리 > C++ 언어' 카테고리의 다른 글

21 연산자 오버로딩  (0) 2020.09.23
20 다중 상속  (0) 2020.09.22
18 상속과 다형성  (0) 2020.09.15
17 상속  (0) 2020.09.14
16 friend, static, const, mutable  (0) 2020.09.14
728x90
반응형

상속과 다형성

  • 객체 포인터를 통한 참조

위의 소스에서는 Base 클래스가 Derived 클래스를, DeepDerived 클래스가 Derived 클래스를 상속하고 있는 2단 상속이 이루어지고 있다. 위의 상황에서 다음과 같은 소스 코드는 에러 없이 실행이 가능하다.

정리를 해보자면, C++ 에서는 Base 클래스 형 포인터 변수는 Base 클래스의 객체 또는 Base 클래스를 직접적(Base 를 직접 상속) 혹은 간접적(Base를 상속한 Derived 클래스를 상속)으로 상속하는 모든 객체를 가리킬 수 있다.

왼쪽 위의 예제는 오른쪽 그림의 소스 코드에서 주석과 같이 실행이 된다.

위 소스와 결과에서 주목해야 할 여러 가지가 있다.

1. 상속 관계에서 Base 클래스 그리고 Derived 클래스들에 완전히 동일한 함수 (이름, 반환타입, 매개변수) 를 정의할 수 있다.
>>
이를 "Overriding" 이라고 하고, 오버라이딩 된 함수들 중 Base 혹은 이전 Derived 클래스의 함수들은 가장 마지막 Derived 클래스의 함수에 의해 가려진다.

2. 객체 포인터를 이용하여 멤버 함수 등을 호출할 때에는 실제 참조하고 있는 객체를 기준하는 것이 아니라, 객체 포인터의 자료형을 기준으로 한다.
>> 5
, 9번 줄에서 보았을 때, 5번 줄에서는 Base * 자료형이지만 실제로는 DeepDerived 객체를 가리킨다. 그러나 9번 줄에서 오버라이딩 된 함수 print()를 호출하였을 때, Base 클래스에 있는 함수를 호출한다. 이것이 증거이다.

728x90
반응형

'컴퓨터 언어 정리 > C++ 언어' 카테고리의 다른 글

20 다중 상속  (0) 2020.09.22
19 멤버함수 가상함수의 동작 원리  (0) 2020.09.15
17 상속  (0) 2020.09.14
16 friend, static, const, mutable  (0) 2020.09.14
15 복사 생성자  (0) 2020.09.13
728x90
반응형

상속

  • 상속 들어가기 전에 앞서

상속이란? (열혈 C++, 윤성우) "기존에 정의해 놓은 클래스를 재활용을 하기 위한 목적으로 만들어진 문법" 이라고 한다.

그러나, 이는 과거에 상속을 바라봤던 관점이라고 한다. 그렇다면 상속을 어떤 관점으로 봐야 하고, 왜 필요한지 이해할 필요가 있다고 생각한다.

 

  • 상속이 필요한 이유

객체지향 언어에서는 데이터적인 성격이 강한 데이터 클래스와 그 데이터 클래스를 이용하여 어떤 여러 기능들의 처리를 담당하는 컨트롤 클래스(혹은 핸들러 클래스)가 필요하다.

하나의 주제 및 테마에 대한 데이터 클래스 + 해당 데이터 클래스에 맞는 컨트롤 클래스

그런데 상속이라는 어떤 개념을 사용하지 않은 상태에서 비슷한 주제에 대한 다른 데이터 클래스를 추가하게 된다면 그 데이터 클래스에 대한 기능을 수행할 수 있도록 기존 컨트롤 클래스 모두를 갱신해야 한다. 이는 대공사가 아닐 수가 없다.

 -> 시시각각 변하는 여러 요구로 인한 변경에 대응하는 프로그램의 유연성과 기능을 추가함으로써 프로그램이 확장되는 확장성이 부족하다고 표현한다.

 

"탈 것"을 주제로 예를 들어 보았을 때,

1) 상속을 사용하지 않을 경우

기차 클래스  / 기차 기능을 수행하는 컨트롤 클래스

이렇게 구성하고 기차를 운영하는 프로그램을 작성했는데, 어느 날 비행기를 추가한다고 한다. 이런 경우에는

 

기차 클래스 / 비행기 클래스

기차 기능 수행 컨트롤 클래스 / 비행기 기능 수행 컨트롤 클래스

혹은

기차 클래스 / 비행기 클래스

기차 기능, 비행기 기능 수행 컨트롤 클래스

이렇게 대대적인 공사가 필요하다.

2) 상속을 사용하는 경우

 

비행기, 기차 등의 최소한의 공통점을 이용한 부모 클래스 / 컨트롤 클래스

(
상속)

비행기, 기차, 자동차 등..

기차 클래스 외에 다른 클래스가 추가되는 경우에는 추가한 후 상속하고, 간단한 수정만 하게 되면 컨트롤 클래스까지 추가 수정하는 대대적인 공사를 막을 수 있다.

  • 상속

간단하게 자식이 부모로부터 재산을 물려받는 행위를 상속이라고 하는데 객체지향 프로그래밍 언어에서는 재산 뿐만 아니라 행동 및 성격, 사상 등의 모든 것이 상속의 대상이 된다!

부모 ------ (상속) ------> 자식
, 재산, 집 문서(데이터적인 성격)
하는 일, 습관적인 행동(기능적인 성격)

 

위 소스에서는 child 클래스가 parent 클래스를 상속받고 부모가 물려준 money 값을 출력하는 예제 소스이다. child 클래스 내에서 50번 줄의 함수 호출은 parent 클래스의 함수를 호출하고 있다. 이는 child parent 를 상속받았기 때문에 가능한 호출이다. 상속은 34번 줄과 같이 " : 접근제한 상속_대상_클래스 " 형식으로 이루어진다.

그리고 57번 줄과 같이 child 클래스만 이용하여 선언했지만 parents 클래스를 상속받기 때문에 parents 클래스에 해당하는 생성자 호출 및 초기화 또한 child 클래스가 담당해야 한다. 관련 부분이 40번 줄, 이니셜라이저를 통하여 부모 클래스의 생성자가 호출되고 있음을 알 수 있다. 이니셜라이저를 통한 초기화이기 때문에 parent 클래스의 생성자는 child 생성자 보다도 앞서 본체까지 모두 실행되고 그 뒤에 child 생성자의 본체가 실행된다.

-> 이는 정말 당연하게도 부모가 존재해야 자식에게 상속을 할 수 있는 것처럼 부모가 먼저 객체화 되어야 자식 또한 상속받은 상태로 객체화 할 수 있기 때문이다.

 

  • 상속 시 부모 클래스의 private 영역 접근

위의 예제에서 child 클래스가 parent 클래스를 상속받은 경우, child 클래스 내에서 parent 클래스의(부모 클래스) private 영역을 직접 접근이 가능한가가 문제이다.

위의 경우는 불가능하다. 아무리 상속을 받았다고 하더라도 부모 클래스의 private 영역은 자식 클래스에서도 직접 접근이 불가능하다. 이렇게 상속 관계에서 또한 정보 은닉 개념이 지켜진다.

private 영역은 부모의 사생활 영역이라고 생각하면 된다. 부모의 뇌라고 생각하고, 자식이 부모의 뇌까지 상속받아서 무슨 생각을 하는지 알 수 있는 것은 아니지 않은가!

  • 상속 설정된 객체를 생성 시, 생성자 / 소멸자 호출 순서

생성자 호출 순서 : 부모 클래스 생성자 body 호출 -> 자식 클래스 생성자 body 호출
소멸자 호출 순서 : 자식 클래스 소멸자 호출 -> 부모 클래스 소멸자 호출

이러한 호출 순서는 당연한 것이다. 왜냐하면 객체 생성 시 부모 클래스 객체가 먼저 생성이 되어야 다음으로 생성되는 자식 객체에게 상속될 수 있기 때문이다.

소멸자의 호출은 생성자의 호출 순서에 반대이다!

지역변수의 호출 순서와는 무관하다. 지역 변수는 기본적으로 스택 구조에 담기기 때문에 LIFO 순서로 메모리 공간에서 해체되므로 이에 맞는 생성자/소멸자 호출이 이뤄진다.

  • protected 접근 제어 지시자

접근 제어 지시자 정리

private   : 외부에서 이 영역에는 접근 불가하며, 상속 관계에서의 derived 클래스에서도 또한 접근이 불가능하다. 오직 해당 클래스의 멤버 함수를 통한 접근(내부 접근)만 허용
protected :
외부에서 이 영역에는 접근 불가하나 private와 다른 점은 상속 관계에서 derived 클래스에서 이 영역에 직접 접근이 가능하다.
public
  : 외부, 내부, 상속 관계에서의 모든 접근을 허용함.

  • 세 가지 상속 형태

상속하는 방법에는 3가지 형태가 존재한다. public, private, protected 상속이 존재한다. 하지만 이러한 세 가지 상속들은 Base 클래스의 접근 제어 지시자와 함께 관련이 있기 때문에 함께 보아야한다.

Base 클래스의 접근 제어 지시자 + 세 가지 중 하나에 해당하는 상속 형태

이것이 결정하는 것은 바로!! 상속을 완료하였을 때, Base 클래스가 Derived 클래스에서 어떤 접근 권한으로 갱신되는지를 결정한다.

위의 표는 일반적으로 설명에 쓰이는 표. 이 포스팅에서는 조금 다른 방법으로 정리해보았다.

1.     먼저 상속의 형태 쪽을 훌라후프와 같은 원이라고 생각하자! 범위가 넓을 수록 큰 원이 된다.

public 상속은 제일 큰 원  / protected 상속은 중간 크기 원 / private 상속은 가장 작은 원

2.     그리고 Base 클래스 또한 원이라고 생각하는데 이 원을 1번에서 생각한 원에 떨어트려본다.

3.     크기가 같은 경우는 쏙 빠져 나온다고 생각하자,

EX)

- public 상속의 원에 public 멤버를 넣게 되면 public 접근 제어 권한으로 갱신되어 상속!

- private 상속의 원에 public 멤버를 넣게 되면 private 원 크기만큼만 잘려서 나오므로 private 접근 제어 권한으로 갱신되어 상속!

- Base 클래스에서 private 멤버인 경우는 부모의 사생활이기 때문에 무슨 짓을 해도 외부 접근 / 상속 관계의 derived 클래스에서의 접근 모두 허용하지 않고 내부 접근만 허용하므로 접근 불가.

 

이 두 상속 관계의 클래스가 상속이 완료된 모습을 보게 된다면 (정확한 형태는 본인도 잘 모르겠으나 핵심만 확인하기)

이러한 식으로 권한이 재 결정된다. 이는 외부 혹은 Derived 클래스를 다시 상속받으려고 하는 클래스의 입장에서 바라보았을 때의 권한이 된다. Base 클래스 만을 바라보면 Base 클래스 권한 그대로 유지하고 봐야 한다.

  • 상속을 고민할 때, 따져 봐야 할 조건

IS-A 조건 과 HAS-A 조건

IS-A 조건 :
a
b의 일종이다 라는 말이 성립할 경우, b Base 클래스로, a Derived 클래스로 정의.

HAS-A 조건 :
a
b 를 소유한다 라는 말이 성립할 경우, b Base 클래스로, a Derived 클래스로 정의
->
그러나 HAS-A 조건은 상속이 아닌 소유의 관계로 풀면 된다. a의 멤버로 b를 선언하여 표현.

728x90
반응형

'컴퓨터 언어 정리 > C++ 언어' 카테고리의 다른 글

19 멤버함수 가상함수의 동작 원리  (0) 2020.09.15
18 상속과 다형성  (0) 2020.09.15
16 friend, static, const, mutable  (0) 2020.09.14
15 복사 생성자  (0) 2020.09.13
14 클래스와 배열 및 this 포인터  (0) 2020.09.13
728x90
반응형

const, friend, static 키워드

  • const 객체

위와 같이 객체에 const 키워드를 선언하게 되면 이 객체를 대상으로는 const 멤버 함수만을 호출할 수 있다. 왜냐하면 const 키워드는 기본적으로 데이터 변경을 허용하지 않음을 의미하기 때문이다.

  • 함수 오버로딩의 조건 : const

함수 오버로딩이 성립하기 위해서는 함수의 이름과 매개 변수 정보(자료형, ) 등이 필요했는데 거기에 더해서 const 여부 또한 오버로딩의 조건이 될 수 있다.

  • 클래스와 friend 선언

aBoy, bGirl이라는 두 클래스가 있을 때, aBoy 클래스가 bGirl 클래스를 friend 선언하게 되면 bGirl 클래스는 aBoy 클래스의 private 영역까지 직접 접근이 가능하다. 그러나 aBoy 클래스는 bGirl 클래스의 private 영역에 접근이 불가능하다 왜냐하면 aBoy 클래스 측에서만 bGirl 클래스를 friend로 인정했지만 bGirl 클래스는 aBoy 클래스를 friend로 인정하지 않았기 때문이다.

다음은 위의 상황을 나타내는 예제 코드이다.

전역 함수와 클래스 멤버 함수를 대상으로도 friend 선언이 가능하다. 함수를 friend 선언하게 되면 friend 선언된 함수들은 해당 클래스의 private 영역에 접근이 가능하다.

위의 예에서 특이한 점은 8번 라인처럼 B가 클래스 이름임을 선언해야 먼저 나오는 클래스 A에서 B가 무엇인지 식별을 할 수 있다. 그리고 friend로 선언된 함수들은 선언과 정의를 분리해줘야 하는 것 같다. (확인필요)

  • static 키워드

C언어 에서의 static 키워드

c언어에서의 static 키워드는 다음과 같은 의미를 지님.

1.     전역 변수 필드에 선언되었을 때
-> 선언된 파일 내에서만 참조를 허용

2.     함수 내에 선언되었을 때 
-> 메모리의 데이터 영역에 딱 한 번 생성과 동시에 초기화 되어 저장되며, 함수 내에서 그대로 지역적인 성격을 가지고 있지만함수가 종료되어도 소멸되지 않는다

C++ 에서의 static 키워드

static 멤버 변수 ( = 클래스 변수 )

c++ 에서도 지니는 성질은 같다. static 멤버 변수로 활용을 하는데, 클래스 변수라고 한다.

위와 같이 5번 줄에서 클래스 내부에서 static 선언을 이용하여 생성한 변수를 클래스 변수 및 static 멤버 변수라고 하는데, 이는 일반 클래스의 멤버 변수와 같이 객체가 생성될 때마다 생성되는 것이 아니다.

해당 클래스의 객체가 생성되든지 안되든지 생성 여부와 상관없이 프로그램이 시작되면 메모리 공간에 독립적으로 딱 하나만 할당되어서 공유되는 변수이다! (생성과 소멸의 시기가 전역 변수와 동일!)

객체가 생성되지 않았을 때에도 메모리 공간에 할당되어 있다는 이야기는 static 멤버는 객체에 종속되어 있는 변수가 아니라 외부에 존재하는 변수라는 이야기다.

, 클래스 내부에 선언하여 멤버처럼 사용할 수 있는 이유는 객체에게 멤버 변수처럼 접근할 수 있는 권한만 부여되었기 때문임.

private: / public : 영역으로 나뉘는 것은 static 멤버를 해당 객체 내애서만 쓸 것인지, 외부에서도 클래스 이름 및 객체 이름을 통해 접근 가능하게 할 것인지를 나타낸다.

10번 라인처럼 초기화 방법이 따로 존재하는데, C++에서는 자바처럼 클래스 내에서 초기화 하는 방식을 지원하지 않기 때문에 생성자 및 멤버함수를 통해서 초기화 시키는 방법이 있다. 그러나 이 생성자를 이용하여 초기화하는 방법을 사용할 경우, 객체가 생성되어 생성자가 실행될 때마다 static 멤버 변수가 초기화 되기 때문에 이 방법을 사용해서는 안 된다. 10번 라인과 같이 초기화를 시켜준다.

 

static 멤버 함수

static 멤버 함수 또한 static 멤버 변수와 같이 특성이 그대로 적용된다. 다만, 객체의 멤버로 존재하지 않기 때문에 다음과 같은 코드는 에러가 발생한다.

9번 줄에서 에러가 발생하는 이유는 static 멤버 함수는 실제로 객체의 멤버가 아니다. 소속 객체가 없기 때문에 9번 줄에서 지정된 number 는 어느 객체의 number 인지 알 수 있는 길이 없다. 그렇기 때문에 에러가 난다

-> 결국 static 멤버 함수 내에서는 static 멤버 변수, static 멤버 함수만 호출 가능하다.

c언어에서처럼 전역 변수를 이용한 카운팅 등을 static 멤버들을 활용하여 대체할 수 있다.

const static 멤버

const static 멤버는 static 멤버의 특성을 유지한 채 const 화 되어 값을 항상 일정하게 유지하기 때문에 다음과 같은 코드가 허용이 된다.

  • mutable 키워드

mutable 키워드를 사용하여 멤버 변수에 선언하게 되면 const화된 함수 내에서 멤버에 대한 값의 변경을 예외적으로 허용하겠다 라는 의미를 지닌다!

728x90
반응형

'컴퓨터 언어 정리 > C++ 언어' 카테고리의 다른 글

18 상속과 다형성  (0) 2020.09.15
17 상속  (0) 2020.09.14
15 복사 생성자  (0) 2020.09.13
14 클래스와 배열 및 this 포인터  (0) 2020.09.13
13 생성자와 소멸자  (0) 2020.09.12

+ Recent posts