본문 바로가기

컴퓨터공학/C, C++

C,C++> 연산자 오버로딩

연산자 함수

기본형 연산자

덧셈 연산자는 피연산자 타입이 달라도 정확하게 연산한다. 

정수형과 실수형은 비트구조가 다르지만 덧셈이 가능하다.

왜냐하면 덧셈 연산자가 피연산자 타입에 따라 오버로딩되어 있기 때문이다. 

int +(int, int);

double +(double, double);

아마 이런 식으로 오버로딩되어 있을 것이다.

그러나 연산자 중복 정의는 기본 타입에서 적용되지만 직접 만든 클래스는 이런 규칙이 적용되지 않는다.

따라서 컴파일러에게 방법을 알려줘야 하는데 이것을 연산자 오버로딩이라고 한다. 

아마  Complex +(Complex, Complex); 이렇게 새로 만들어질 것이다. 

 

연산자 함수 

클래스에 멤버 함수를 정의하여 덧셈을 할 수 있겠지만 직관적이지 않다. 

연산자는 모양이 특이한 함수라고 볼 수 있다. 인수가 필요한 것과 연산 결과를 리턴한다는 점에서 함수와 같다. 

연산자 함수의 이름은 operator 키워드 다음에 연산자 기호를 쓴다. 연산자 기호를 명칭으로 쓸 수 없으므로 operator 키워드를 앞에 두는 것이다. 

덧셈 연산자 함수 이름은 operator +가 되는데 operator+라고 붙여 써도 상관없다. 

const Time AddTime(const Time &T) const

const Time operator +(const Time &T) const

바뀌는 건 이름만 바뀌는 것뿐이다. 

다시 복습하면 const TIme은 상수 객체이고 인수로 const Time &T 객체에서 쓸 수 있는 건 상수 멤버 함수이다. 

 

 

Time A(1,1,1);
Time B(2,2,2);
Time C;
C = A + B;
C = A.operator +(B); //C=A+B;와 같다

동작이 동일하다. 

 

연산 함수 장점은 무엇일까? 

첫 번째 연산자 호출 방식 길이가 짧아서 오타 발생이 적다.

두 번째 연산문 형태라서 직관적이고 사용하기 쉽다. 

세 번째 괄호를 쓰지 않아도 연산 순서가 자동으로 적용되어 편하다. 

 

연산자 함수 형식

클래스 연산자 함수 정의법은 두 가지가 있다.

1. 클래스 멤버 함수로 작성한다. 

2. 전역 함수로 작성한다. 

 

멤버 연산자 함수를 작성하는 형식은 

리턴타입 Class::operator 연산자(인수 목록)

{

함수 본체;

}

 

예시

class Complex{
private:
	double real;
    double image;
    
public:
	Complex() { }
    Complex(double r, double i) : real(r), image(i) { }
    void Outcomplex const { printf("%.2f + %.2fi\n", real, image); }
    const Complex operator +(const Complex &T) const {
    	Complex R;
        R.image = image + T.image;
        R.real = real + T.real;
        return R;
    }
};

인라인 함수로 정의하면 함수명 앞 소속 클래스 표기가 빠지는데 외부에서 정의하면 소속 클래스 이름도 밝혀야 한다. 

 

const 키워드가 가지는 의미, 레퍼런스를 넘기는 이유, 값을 리턴하는 이유 등 알아보자

 

인수 타입

객체는 인수 앞에 &기호를 빼고 값으로 넘겨도 동작에는 별 이상없다.

다만 기본형보다 덩치가 커서 값을 넘기면 비효율적이다. 

그래서 레퍼런스로 넘기는게 유리하다. 

포인터로 수정은 할 수 있다. 

Complex operator +(const Complex *T) const {
	Complex R;
    R.image = image + T->image;
    R.real = real + T->real;
    return R;
}

연산자 함수가 포인터를 받으면 이 함수를 호출할 때 피연산자 주소를 넘겨야 한다.

호출부 모양이 C3=C1.operator + (&C2);이 되고 연산식은 C3 = C1 + &C2;가 된다.

 

연산자 오버로딩의 목적은 객체 연산문을 기본형과 같은 방법으로 표현해서 가독성을 높이고 클래스의 직관적인 활용성을 향상시키는 것이다. 근데 이런 식으로 매번 & 연산자를 사용하면 장점이 무색해진다. 

값은 효율이 좋지 못하고 포인터는 직관성이 떨어진다.

레퍼런스로 넘기면 효율과 직관성 둘 다 잡을 수 있다.

C++ 이 레퍼런스 타입을 지원하는 이유가 여기에 있다.

바로 객체 연산식의 직관적인 표현을 위해서다. 

받는 객체를 그대로 표현하되 포인터처럼 참조하고 싶을 경우 인수에 레퍼런스를 쓴다.

 

인수 상수성

피연산자로 전달된 인수는 읽기만 한다. 인수를 상수 취급한다. 

객체 레퍼런스를 전달할 때 함수가 객체 상태를 함부로 변경하지 못하게 const 지정자를 붙이는 게 안전하다.

const를 안 붙이면 다음 연산도 불가능하다.

const Complex C2(1.0, 2.0);
C3 = C1 + C2;

상수 객체도 피연산자로 사용할 수 있어야 하는데 인수가 상수가 아니라면 에러로 처리된다. 

정수 연산에서 a=b+3; 도 허용되므로 상수 객체를 피연산자로 쓸 수 있어야 한다. 

 

함수 인수에 const가 붙을 때와 안 붙을 때 이해가 안 가서 테스트했다. 

인수가 Complex일 때 왜 상수 객체인 const Complex를 못 받는 걸까?

Complex 변수는 상수 객체 const Complex와 일반 객체 Complex 둘 다 대입받을 수 있는데?

결론은 레퍼런스였다.

  Complex 대입 const Complex 대입
const Complex &T가 인수일 때 작동 작동
Complex &T가 인수일 때 작동 에러
const Complex T가 인수일 때 작동 작동
Complex T가 인수일 때 작동 작동
Complex C3가 변수일 때 작동 작동
const Complex C3가 변수일 때 작동 작동

 

오직 한 가지 상황에서만 에러가 생긴다.

binding of reference to a value of type drops qualifiers이라고 뜨는데

비상수 레퍼런스가 상수 객체를 다룰려고 할 때 문제가 생긴다. 

이거는 포인터도 문제가 생긴다. 에러 내용은 다르게 나온다. 

 invalid conversion from ‘const Complex*’ to ‘Complex*’

 

레퍼런스나 포인터를 통해 상수 객체를 수정하는 걸 허용하면 constant correctness를 깨게 되어 에러를 뱉는다.

c++ - binding of reference to a value of type drops qualifiers - Stack Overflow

 

 

함수의 상수성

멤버 연산자 함수가 호출 객체의 상태를 바꾸지 않을 경우 원칙에 따라 const 함수로 지정하는게 좋다. 

호출 객체를 변경하는 사고를 막을 수 있다. 

리턴타입을 상수로 취급하겠다는 뜻인데 함수 내부에 A+B 결과가 저장되고 리턴할 때 상수 취급하여 C에 대입되고 조작을 허용하지 않는다. 

통상적인 이항 연산자들은 객체 값을 읽기만 할 뿐 객체를 변경하지 않는다. 

반면 객체 값을 직접 변경하는 연산자는 const로 지정하면 안된다. 

주로 증감 연산자, 대입 연산자, 복합 대입 연산자에 해당된다. 

 

리턴 타입 

임의의 타입 T에 대한 덧셈 결과는 역시 T형이 되는 것이 합리적이다. 그래야 연산 결과를 제3 객체에게 대입할 수 있다. 

연산자 함수가 리턴할 때 레퍼런스를 리턴할 것인가 값을 리턴할 것인가는 연산자에 따라 다르다. 

operator +의 경우 임시 객체로 연산 결과를 리턴하기 때문에 레퍼런스형은 안된다.

Complex &operator +(const Complex &T) const { 

...

임시 객체는 함수 호출이 종료되면 사라지며 함수 리턴 직후 다른 객체로 대입할 수 있는 값으로 넘겨야 한다. 

C3 = C1 + C2는 정상 실행이 되는데 왜냐하면 + 연산 바로 다음 연산이 대입 연산이고 대입 연산은 함수 호출이 아닌 멤버별 복사 코드 실행이기 때문에 스택에 있는 임시 변수가 대입하는 시점까지 계속 유지하기 때문이다. 

그러나 C4 = C1 + C2 + C3 는 제대로 동작하지 않는다. 연산 순위에 따라서 C1 + C2 가 먼저 호출되고 연산 결과 R의 레퍼런스가 리턴되며 R + C3가 작동하는데 이 시점에서 스택에 있는 호출 객체인 R이 없기 때문이다. 

따라서 C4는 제대로 된 값을 대입받을 수 없다. 

바로 직전 함수가 만든 지역변수는 다음 함수가 호출되면 완전히 사라진다. 

스택은 매 함수 호출마다 새로 재구성되는 임시 기억 장소이기 때문이다. 

반면 값으로 리턴하는 경우 값은 리턴될 때 새로 만들어지는 사본이기 때문에 다른 함수 호출에 영향없다. 

그래서 Complex 레퍼런스가 아닌 Complex 값을 리턴하는 게 정확하다. 

 

리턴 타입 상수성

객체 타입을 리턴하는 함수는 보통 상수 객체를 리턴해야 한다. 

예를 들어 보면 

int i=3, j=4, k;
k=i+j;

이 연산 결과 i+j는 7이라는 정수상수이지 정수형 변수가 아니다.

즉 좌변값이 아니라 우변값이다.

만약 i+j가 정수형 변수를 리턴한다면 i+j=5;라는 연산식도 허용되어야 할 것이다. 

 

함수 원형을 보면 const가 세 번 사용되는데 각각 의미는 다르다. 

const Complex operator +(const Complex &T) const

첫 번째는 리턴되는 값이 읽기 전용.

두 번째는 피연산자, 우변이 상수.

세 번째는 좌변이 상수라는 뜻이다. 

 

전역 연산자 함수

전역 연산자 함수

이전은 멤버 연산자 함수로 만드는 방법이다. 전역 함수로 만드는 방법을 알아보자.

전역 연산자 함수는 클래스 외부에 존재하되 인수로 클래스 객체를 받아들인다. 

연산자 함수를 정의하지 않은 대신 operator + 전역 함수를 friend로 지정하여 자신 모든 멤버를 자유롭게 액세스할 수 있도록 허락한다. 

 

A+B의 멤버 연산자 함수는 A.operator +(B)

전역 연산자 함수는 operator +(A,B) 

내부에 있는가 아니면 외부에 있되 프렌드로 지정되어 있는가.

연산 논리나 호출 방법은 동일하지만 함수 원형은 다르다. 

 

멤버 연산자 함수는 인수가 하나이다.

호출 객체인 *this가 좌변인데 암시적으로 받고 우변은 인수로 전달받는다. 

반면 전역 연산자 함수는 두 개 인수를 취한다. 암시적으로 전달받는 this가 없어서 좌우변 모두 인수로 받는다. 

어떤 방법을 사용하든 상관없지만 캡슐화 원칙에 부합할 수 있는 멤버 연산자 함수가 깔끔하다. 

하지만 전역으로만 만들어야 하는 경우가 있고 =, ( ), [ ], -> 연산자들은 반드시 멤버 연산자 함수로만 만들어야 한다.

결국 두 가지 방법 모두 필요하다. 

 

객체와 기본형 연산

예를 들어 덧셈 교환 법칙이 성립하기 위해서 전역 연산자가 필요하다.

멤버 연산자를 만들어  A = A + 5를 수행할 수 있지만 

5+A도 작동하기 위해서는 전역 연산자 함수가 필요하다. 

5+A가 있으면 컴파일러는 두 함수 중 하나를 찾는다. 

const Time int::operator +(Time);

const Time operator +(int, Time);

int 클래스에서 함수를 직접 만드는 건 불가능하다. int 형은 시스템 내장 타입이라서 사용자가 이 클래스를 마음대로 확장할 수 없다. 아래 함수는 전역 연산자 함수인데 int와 Time 둘 다 인수로 취한다. 

전역 연산자 함수를 만들고 Time 클래스에서 프렌드 설정을 하여 Time의 멤버 변수를 접근할 수 있도록 설정하면 된다. 

 

오버로딩 규칙

1. 연산자 오버로딩은 이미 존재하는 연산자 기능을 조금 바꾸는 것이지 새로 연산자를 만드는 건 아니다.

오버로딩이란 이미 존재하는 걸 중복 정의하는 것이지 없는걸 아예 새로 만드는 건 아니기 때문이다.

2. 이미 존재하는 연산자 중 오버로딩 대상자가 아닌 것들이 있다. 

3. 기존 연산자 기능을 바꾸더라도 피연산자 개수와 우선순위를 변경할 수 없다. 

4. 한 클래스가 하나의 연산자를 여러 가지 피연산자 타입에 오버로딩할 수 있다. 

5. 오버로딩된 연산자의 피연산자 중 적어도 하나는 사용자 정의형이어야 한다. 

6. 연산자 논리적 의미는 유지하는 게 좋다.

 

 

오버로딩 연산자 예

관계 연산자 

bool operator ==(const Time &T) const {

...

 

증감 연산자

감소 연산자도 있지만 어차피 오버로딩 방법은 동일하므로 ++를 보자

Time &operator ++() {
	...
    return *this;
}

const Time operator ++(int) {
	Time R = *this;
    ++*this;
    return R;
}

++ 연산자 피연산자는 자기 자신이다. 따라서 함수 다음 const 지정이 없고 const Time &이 아니라 Time &이어야 한다. 

이항 연산자는 읽기만 하는데 단항 연산자는 직접 변경하기 때문이다. 

리턴 타입을 void으로 쓸 수 없는 이유는 다른 객체에 대입해야 하기 때문이다. 

 

++ 연산자는 별도 인수가 필요 없지만 전위형과 후위형을 구분하기 위해서 인수를 받는다. 

전위형은 인수를 받지 않고 후위형은 인수를 받는다. 

쓰지 않을 인수이지만 오버로딩이 성립되고 함수를 컴파일러가 구분할 수 있다.

일종의 약속이라서 외우고 규칙대로 ++연산자를 오버로딩해야 한다.

 

전위형은 레퍼런스를 리턴하고 후위형은 상수 객체를 리턴한다. 

왜냐하면 후위형은 이전 값을 보존하고 내보내야하기 때문에 상수 객체를 반환한다. 

전위형은 호출 객체 값을 증가하고 내보내기 때문에 레퍼런스를 반환한다.

 

이전에 레퍼런스 리턴을 하면 안된다고 했는데 그건 이항 연산자일 경우이다. 

지역 변수로 사라져서 가리킬 대상이 없어져서 그런건데 이거는 값을 변경한 호출 클래스 객체 레퍼런스를 리턴하는 거라서 파괴되지 않는다. 그래서 레퍼런스 리턴이 가능하다. 

 

전위형은 단순 산술식이지만 후위형은 임시 객체 생성, 초기화, 전위형 ++ 호출 등 여러 가지 추가 처리가 필요하다.

그래서 증가 동작만 본다면 전위형이 훨씬 빠르고 효율이 좋아서 가급적이면 ++ 연산자를 쓰는게 유리하다. 

 

그러나 수식 내에서는 두 형태의 효과가 다르므로 적합한 형식을 사용해야 한다. 

기본형일 경우 전위형이 더 유리하지만 컴파일러는 단독으로 사용되는 증가 연산자는 전위형으로 바꿔서 호출한다.

따라서 i++나 ++i나 동일하다. 단 기본형이라도 수식 내에서 사용될 때 이런 최적화를 하지 않는다. 

 

대입 연산자 

대입 연산자는 자신과 같은 타입의 다른 객체를 대입받을 때 사용하는 연산자이다. 

객체 자체와 연관이 있어서 클래스 멤버 함수로만 정의할 수 있고 전역 함수로는 정의할 수 없다.

정적 함수로는 만들 수 없고 반드시 일반 함수로 만들어야 한다. 

 

Person A = B;

객체 선언과 동시에 초기화를 하면 복사 생성자가 호출된다.

Person A, B;

A=B;

그러나 선언한 다음 대입을 받게 되면 대입 연산자가 호출된다.

대입은 이미 생성된 객체에 적용, 실행 중 언제든지 여러 번 대입될 수 있다는 점에서 초기화와 다르다. 

객체 끼리 대입 연산을 하면 어떤 일이 생길까?

 

깊은 복사를 하는 대입

대입 연산자를 별도로 정의하지 않으면 컴파일러는 디폴트 대입 연산자를 만든다. 

이 연산자는 디폴트 복사 생성자처럼 단순한 멤버별 대입한다.

우변 객체 모든 멤버 내용을 좌변 객체 대응되는 멤버로 대입한다. 

그래서 두 객체의 멤버가 같은 주소를 가리키는 상황이 생길 수 있다. 

생성과 동시에 초기화할 때처럼 대입을 받을 때 깊은 복사를 하게 해야 한다.

 

class Person
{
	...
    Person &operator =(const Person &Other) {
    	if(this != Other) {
        	delete [] Name;
            Name = new char[strlen(Other.Name)+1];
            strcpy(Name,Other.Name);
            Age=Other.Age;
        }
        return *this;
    }
};

Name 멤버를 할당하기 전에 이전 사용하던 메모리를 먼저 해제한다.

복사 생성은 새로 만들어지는 거라서 할당되어 있지 않지만 대입은 사용 중인 객체에 일어나는 연산이다.

헌 것을 지우고 새 것을 넣어야 한다. 

 

대입 후 리턴값

대입 연산자 리턴 타입이 레퍼런스인 이유는 A=B=C  같은 연쇄 대입이 가능해야 하기 때문이다. 

리턴 객체가 상수일 필요는 없다. 대입후 리턴 객체를 바로 사용할 수 있고 변경할 수 있기 때문이다.

(Young=Boy.OutPerson(); 이런 식으로 대입하고 리턴되는 객체를 바로 멤버 함수 호출할 수 있다.

이 멤버 함수가 객체 상태를 변경하는 비상수 함수라도 사용할 수 있다.

 

올바른 디폴트 생성자

디폴트 생성자는 받아들이는 인수가 없으므로 메버를 초기화하여 쓰레기 값을 치우는 것이 임무이다. 

동적 할당을 하는 클래스는 포인터를 Null로 초기화하면 안된다.

Person() { Name = null; Age= 0;}

Person Boy;
Person Young = Boy;

 

이런 식이 있다면 디폴트 생성자가 쓰레기를 치우고 있으므로 인수없이 객체 생성이 가능하다.

그러나 문제가 생긴다. 

본체에서 strlen 함수로 Other.Name의 길이를 구하는데 Null값을 읽으려고 하니까 다운된다.

따라서 예외 코드를 더 작성해야 한다. 

Person(const Person &Other) {
	if (Other.Name == NULL) {
    	Name = NULL;
    } else {
    	Name = new char[strlen(Other.Name)+1];
        strcpy(Name, Other.Name);
    }
    Age = Other.Age;
}

NULL인 경우 일일이 예외 처리해야 하는데 귀찮고 비효율적이라서 디폴트 생성자가 포인터를 초기화할 땐 1바이트라도 할당해서 NULL이 되지 않도록 해야 한다. 

 

동적 할당 클래스 조건 

대입이 초기화보다 훨씬 더 복잡하고 비용도 많이 든다. 그래서 컴파일러는 복사 생성자와 대입 연산자를 구분해서 호출한다. 따라서 우리는 둘 다 만들어야 한다. 

값만 있는 클래스는 디폴트 복사 생성자, 디폴트 대입 연산자만 있어도 잘 동작한다. 

그러나 동적 할당 메모리가 있으면 관리할 것이 많다. 

 

복합 대입 연산자

클래스에 operator +를 정의한다고 operator += 까지 같이 정의되는 건 아니다. 

+와 차이점은 호출 객체를 직접 변경하기 때문에 const가 아니라는 점,

그리고 자기 자신이 피연산자라서 임시 객체가 필요하지 않다는 점이다. 

 

복사 생성 및 대입 금지

경우에 따라서 동작을 금지할 필요가 있다. 

복사 생성자와 대입 연산자를 선언하되 둘 다 private 영역에 둔다. 

정의하지 않으면 컴파일러가 디폴트를 만드므로 반드시 private 영역에 직접 선언한다. 

예를 들어 대입문이 실행하려고 할 때 컴파일러는 복사 생성자나 대입 연산자를 호출하려고 한다.

객체가 선언하는 곳은 객체 외부이므로 private 멤버를 호출할 수 없고 컴파일 중에 허가되지 않는다. 

 

두 함수를 private 영역에 둘 때 본체 내용은 안 쓰는 것이 좋다. 

왜냐하면 외부 함수 호출은 컴파일러가 막아주지만  클래스 내부 멤버함수나 프렌드 함수에서 이 함수를 호출할 수 있기 때문이다. 함수 선언만 하고 본체를 정의하지 않았을 때 함수가 호출되기 전까진 링커가 본체를 찾지 않으므로 아무 이상 없다. 

그러나 만약 정의하지 않은 함수를 호출하려고 하면 컴파일은 되지만 링크할 때 에러처리된다. 

요약하면 외부에서 불가능한 동작을 시도하면 컴파일러가 막아주고 내부에서 동작을 시도하려고 하면 링커가 막아준다.

 

<< 연산자 

cout 객체 소속 클래스인 ostream에 여러가지 원형의 << 멤버 연산자 함수가 오버로딩되어 있다. 

C++ 컴파일러는 << 다음 피연산자 타입을 보고 적절한 << 멤버 연산자 함수를 호출하므로 대부분 기본 타입을 문제없이 출력할 수 있다. 

만약 새로운 클래스를 피연산자로 받아들이고 싶다면 새로운 객체 연산자 함수를 하나 더 오버로딩하면 된다. 

operator << 연산자 함수를 cout 소속 클래스인 ostream 멤버함수로 추가하거나 전역 함수로 만들어야 한다.

표준 라이브러리 함수를 뜯어 고칠 수는 없으니 반드시 프렌드 전역 연산자 함수로 정의해야 한다. 

자신의 모든 멤버를 읽을 수 있도록 권한을 준다. 

 

[ ] 연산자 

반드시 멤버 함수로만 정의할 수 있으며 전역 함수로는 정의할 수 없다. 

class DArray
{
	...
    ELETYPE &operator [](int idx) {
        return ar[idx];
    }
};

 

배열 첨자를 인수로 전달하면 인수가 지정하는 순서의 배열 요소를 레퍼런스로 리턴한다. 

ar[3]으로 네 번째 요소를 읽을 수 있고 레퍼런스를 리턴하므로 좌변에 놓을 수 있다.

[ ] 연산자가 특이한 것은 좌변과 우변 모두 쓸 수 있다는 점이다. 

그러나 상수 객체에 쓸 수 있는 const 버전의 [ ] 연산자도 중복 정의해야 한다. 

const DArray car;
car[0] = 3; //에러
printf("%d\0",car[0]); //에러

상수 객체로 선언되면 값을 변경할 수 없어서 좌변에 둘 수 없다. 

근데 문제는 읽는 것도 안된다는 것이다. 

컴파일러는 상수 객체에 const 지정이 없으면 객체 내용을 바꿀 수 있다고 생각해서 막아버린다. 

그래서 상수 객체에서도 값을 읽을 수 있게 const 처리해야 한다. 

 

const ELETYPE &operator [](int idx) const {
	return ar[idx];
}

 

 

[ ] 연산자를 오버로딩한 객체를 만든다면, 배열에서 객체를 선택하는 [ ] 연산자와 오버로딩된 [ ] 연산자는 어떻게 구분할 수 있을까? 다음 예시를 보자.

DArray ar[3];
ar[2][1] = 5;

 ar[2]는 ar배열에서 2번째 요소를 선택한다. 

뒤 쪽[1]은 ar[2]이 DArray 타입 객체이므로 이 객체에서 오버로딩된 연산자이다.

 

멤버 참조 연산자

클래스나 구조체 멤버를 참조하는 연산자는 .와 -> 두 가지이다.. 

이중 . 연산자는 너무나도 기본 연산자라서 오버로딩할 수 없다. 

객체 포인터로 멤버를 읽는 -> 연산자는 오버로딩 대상자이다. 

이 연산자는 독특한 오버로딩 규칙이 적용된다. 

원래 이항 연산자이지만 오버로딩하면 단항 연산자가 되며 전역 함수로는 정의할 수 없고 클래스 멤버함수로만 정의할 수 있다. 멤버 함수이면서 단항이므로 결국 인수를 취하지 않는다. 

 

이 연산자 리턴 타입은 클래스나 구조체 포인터로 고정되어 있다. 

보통 클래스에 포함된 다른 클래스 객체나 구조체 번지를 반환하여 포함된 객체의 멤버를 읽는다. 

이 연산자는 포함 객체 멤버를 자신 멤버처럼 액세스할 수 있다. 

 

struct Author {
	char Name[32];
    char Tel[24];
    int Age;
}

class Book {
private:
	char Title[32];
    Author Writer;

public:
	Author *operator->() { return &Writer; }
    const char *GetTitle() { return Title; }
};

위 코드를 바탕으로 Book 클래스 인스턴스인 Hyc가 있다고 가정하자.

Hyc의 멤버 Author 구조체 Name를 읽기 위해 ->를 사용한다.

그러면 Hyc->Name 가 된다.  이 표현식은 다음처럼 해석한다.

Hyc.operator->()->Name

원래 ->연산자 좌변에는 포인터만 올 수 있다. 

하지만 오버로딩되었기 때문에 객체가 와도 상관없다. 

Hyc.Writer.Name 이런 식으로 . 연산자를 두 번 사용하는 건 허가되지 않는다. 

왜냐하면 Writer가 private 접근제한 속성을 갖고 있기 때문이다. 

-> 연산자는 숨겨진 멤버 포인터를 읽어주고 이 멤버에 속한 멤버를 바로 액세스할 수 있게 한다. 

 

() 연산자

이 연산자 좌변은 항상 호출 객체이므로 전역 함수로는 정의할 수 없으며 멤버 함수로만 정의해야 한다. 

 

new, delete 

new 연산자는 두 가지 동작을 한다. 운영체제 힙 관리 함수를 호출하여 요청한 만큼 메모리 할당한다. 

그리고 이 할당 메모리에 관해서 객체 생성자를 호출하여 초기화한다. 

new가 생성자를 호출하는 건 언어 고유 기능이므로 생성자 호출을 금지할 수 없지만 메모리 할당 방식은 원하는 대로 바꿀 수 있다. 연산자 자체는 오버로딩 대상자가 아니지만 함수 내부적으로 호출하는 operator new는 오버로딩 대상이다. 

 

delete 함수도 두 가지 동작을 한다. 파괴자를 호출하여 객체를 정리하고 다음으로 operator delete를 호출하여 객체가 사용하던 메모리를 해제한다. operator new를 오버로딩하여 할당 방식을 바꾸면 operator delete도 오버로딩해서 해제하는 방식도 할당 동작에 맞게 바꿔야 한다. 예를 들어 가상 메모리를 직접 다루고 싶다거나 미리 할당해 놓은 메모리 풀을 조금씩 돌려가며 사용하고 싶을 때가 이런 경우이다. 

 

문자열 클래스 

 Str 클래스

C는 문자열을 기본 타입으로 제공하지 않고 문자형 배열로 표현한다.

그래서 대입, 연결, 비교, 추가 등 모든 연산을 함수로만 해야 한다.

연산자를 쓸 수 없어 불편하고 배열 경계를 넘어설 수 있어서 위험하다.

그래서 C++에서는 보통 문자열을 클래스로 작성한다. 

다양한 연산자로 기본 타입과 똑같은 방법으로 문자열을 다룰 수 있다.

 


C/C++ 학습 의미

지금은 문법을 공부하고 있지만 현실은 문법 이후 고급 기술에 관심이 많을 것이다. 사실 C언어 외에도 훌륭하고 재밌는 언어들은 얼마든지 있다. 그렇다면 C/C++ 공부는 어떤 의미가 있을까?

입문자가 C/C++을 추천하는 이유는 이 언어로 문법을 정리하면 실력이 늘어나거나 쉽게 공부할 수 있는 과목들이 아주 많기 때문이다. 자바나 C#은 같은 계열이라서 거의 동일하고, 웹스크립트 언어나 파이썬같이 별 상관 없어 보이는 것들도 C++를 잘 하면 더 쉽게 그리고 확실하게 공부할 수 있다. 이 외 HTML이나 SQL처럼 연관 없는 과목도 있지만 논리력과 사고력을 키우는데 이만한 언어가 없다. 

한 때 웹 개발자에게도 C/C++ 공부가 필요한지 논쟁이 있었다. 당장 필요는 없지만 알아 두면 좋다는 정도로 잠정적인 결론이 났다. 웹 페이지만 만들면 필요없지만 어느 수준에 이르면 C 유사 언어를 피할 수 없기 때문에 결국 공부하는 것이 유리하다. 

C/C++ 언어만이 개발자 기본 과목으로서 가치가 있는 것은 아니다. 자바나 C#처럼 효율있는 언어가 많기 때문이다. 다만 중요한 것은 어떤 언어를 선택하느냐가 아니라 얼마나 꾸준히 깊이있게 연구하는가에 있다. 철새처럼 어떤 언어가 좋으면 방향을 틀어 공부했다가 다시 새로 나온 개발툴이 좋다는 소리를 들으면 금방 언어를 바꾸는 사람이 있다. 많이 배울 수 있겠지만 깊이가 없어진다. 선택한 언어나 개발툴이 무엇이든 끈기있게 물고 늘어져야 오묘한 맛을 알 수 있다. 

공부를 잘하는 친구와 못 하는 친구를 잘 보면 차이점을 알 수 있다. 공부 못 하는 친구는 오만가지 책을 다 구해 열심히 읽지만 실력이 잘 늘지 않는다. 반면에 잘하는 친구는 책 한권을 열 번이고 스무 번이고 계속 읽는다. 이것저것 많이 집적거려 봤자 넓게만 볼 수 있을 뿐 한 번도 깊게 보지 못하므로 실력이 늘지 않으며 늘어나도 한계가 있다. 언어도 마찬가지이다. 한 언어를 다 파헤쳐 보고 개념을 확실히 익힌 다음 유사 언어를 공부해야 쉽게 익힐 수 있다. 

그렇다면 하나를 무엇으로 정해야 할까? 여기에는 정답이 없다. 무슨 언어든 하나를 정해 충분히 기간을 두고 반복 학습과 실습을 하면 된다. C/C++를 선택한 이상 당분간 이 언어에 모든 것을 투자해야 한다. 다른 언어에 비해 C/C++은 실용성이 높아 가치가 충분하고 자료가 많아 깊이 있는 연구를 하기에 적합하다. 물론 오래된 언어라 최신 언어에 비해 부족한 것이 있는건 당연하다. 그래도 지금 C/C++를 공부하고 있다면 당분간 다른 언어에 현혹되지 말고 당장 하고 있는 공부에 매진하는 것이 좋다. 충분히 그럴만한 가치가 있는 언어이다. 

 

'컴퓨터공학 > C, C++' 카테고리의 다른 글

C,C++> 다형성  (0) 2022.08.22
C,C++> 상속  (0) 2022.08.19
C,C++> 캡슐화  (0) 2022.08.09
C,C++> 생성자  (0) 2022.08.08
C,C++> 클래스  (0) 2022.08.04