연산자 오버로딩
- 연산자 오버로딩
C++언어에서는 연산자의 오버로딩을 통해서 기존에 존재하던 연산자의 기본 기능 이외에 다른 기능을 추가할 수 있다.
먼저 15번 라인을 통해서 operator+ 라는 함수를 제공한다. 이 함수를 정의함으로써 연산자를 오버로딩한다. 29번 라인을 보면 operator+ 멤버 함수를 호출함으로써 a1의 멤버 변수 value의 값과 a2의 멤버 변수 value의 값을 더한 결과를 a3로 객체 복사를 통해서 전달하고 있음을 확인할 수 있다. 그런데 이러한 과정이 30번 라인에서도 똑같이 일어나고 있음을 알 수 있다. 즉, 정리해보자면 다음과 같음을 알 수 있다.
이는 객체 또한 기본 자료형 변수처럼 취급하고 각종 연산이 가능하도록 하려는 C++의 기본 철학이다. operator 키워드와 연산자를 묶어서 함수를 정의하면 함수의 이름(operator’연산자’)을 이용한 함수 호출은 물론 연산자만을 이용해도 operator’연산자’ 함수 호출로 바꾸어 준다.
- 연산자 오버로딩의 두 가지 방법
멤버 함수에 의한 연산자 오버로딩
위에서 제시한 예의 경우이다.
전역 함수에 의한 연산자 오버로딩
다음의 예를 통해 설명.
20번 라인에서 private 영역에 접근을 위해서 전역 함수를 friend 선언을 해 주었다. 그리고 22번 라인에서 + 연산자에 대해서 전역 함수 형태로 오버로딩 하였다. 이 함수를 이용해서 30번 라인에서 a1+a2는 operator+(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은 개행 기능을 하는 함수이다.
- <<, >> 연산자의 오버로딩 ( 특별 자료형에 대한 cout과 cin의 구현 )
cout과 cin에 대해서 <<, >> 연산자 오버로딩을 하기 위해서는 전역 함수를 통한 오버로딩을 해야 한다. 왜냐하면 멤버 함수를 통하여 오버로딩을 하기 위해서는 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 연산자를 오버로딩 한다.
new와 delete 연산자 오버로딩의 예
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번 라인에서 먼저 num1의 int 형 변환 발생이 일어나고 그 결과 값을 3과 더한 뒤, 그 정수 값을 다시 num2에 대입하는 과정에서 num2의 생성자가 호출되어 임시 객체를 통한 대입 연산자가 호출되고 대입이 이루어진다.
'컴퓨터 언어 정리 > 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 |