언어 자료구조 알고리즘/Escort C++

[C++] 대입 연산자 중복 정의

언제나휴일 2016. 4. 15. 15:07
반응형

7.2 연산자 중복 정의 예

 

 C++ 언어에서 연산자 중복 정의를 할 때 연산 종류에 따라 좀 더 세심한 주의를 해야 하는 연산들이 있습니다. 여기에서는 이러한 연산 중에 대입 연산, [] 연산, 묵시적 형 변환 연산, 증감 연산에 대해 살펴봅시다.

 

7.2.1 대입 연산자 중복 정의

 

 C++에서 클래스를 정의할 때 사용자가 정의하지 않아도 컴파일러에 의해 기본적으로 제공되는 디폴트 멤버들이 있습니다. 이러한 멤버에는 this, 디폴트 기본 생성자, 디폴트 소멸자, 디폴트 복사 생성자 및 디폴트 대입 연산자가 있습니다.

 

 여기서는 디폴트 대입 연산자에 대해 알아보기로 하겠습니다.

 

 C++에서 클래스를 정의할 때 대입 연산자를 중복정의를 하지 않으면 컴파일러에 의해 기본적으로 디폴트 대입 연산자를 정의해 줍니다. 이는 변수 선언문 이외의 구문에서 = 연산자를 사용할 경우에 수행되며 할당된 메모리를 덤핑하도록 작성되어 있습니다. 이 경우에 = 연산의 우항에 있는 멤버 필드와 동일한 상태의 값으로 개체의 멤버 필드가 바뀌게 됩니다. 만약, 특정한 멤버 필드만 복사하기를 원하거나 특정 멤버 필드에 동적으로 생성하여 보관해야 한다면 중복 정의하여 직접 행위를 구현하면 됩니다. 그리고 디폴트 대입 연산자의 경우는 = 우항에 오는 형식이 자기 자신과 동일한 경우에 대해 정의되어 있는 것이기 때문에 다른 형식을 대입하는 것에 대해서도 가능하게 하려고 한다면 개발자가 연산자 중복 정의를 해 주어야 합니다.

 

 이제 대입 연산자 중복 정의에 대해 하나씩 확인해 보도록 합시다. 제일 먼저 디폴트 대입 연산자가 가능하다는 것을 확인해 보도록 하겠습니다.

 

Musician.h

#pragma once

#include <iostream>

#include <string>

using namespace std;

class Musician

{

    int num;

    string name;

public:

    Musician();

    Musician(int _num,string _name);

    virtual ~Musician(void);

    void View();

};

 

 

Musician.cpp

#include "Musician.h"

Musician::Musician(int _num,string _name)

{

    num = _num;

    name = _name;

}

Musician::Musician()

{

    num = 0;

    name = "";

}

Musician::~Musician(void){}

void Musician::View()

{

    cout<<"번호:"<<num<<" 이름:"<<name<<endl;

}

 

 현재 Musician 클래스에는 대입 연산자를 중복 정의를 하지 않았습니다. 이 경우에 사용하는 곳에서 대입 연산자를 사용하면 어떻게 될까요? [그림 7.3]을 보시면 다른 연산자를 사용하면 해당 연산자에 대해 중복 정의를 하지 않았다는 오류가 발생하는 것을 알 수 있습니다. 하지만 [그림 7.4]와 같이 대입 연산자에 대해서는 오류 없이 잘 동작합니다. 이를 통해 디폴트 대입 연산자가 존재함을 알 수 있습니다.


[그림 7.3]


디폴트 대입 연산자

[그림 7.4]

 

 이번에는 변수 선언 시에 초기화 구문이 아닌 곳에서 대입 연산자를 사용할 때만 대입 연산자가 호출된다는 것에 대해 살펴보겠습니다. 변수 선언 시에 =를 사용할 경우에는 복사 생성자가 호출됩니다. 그리고 그 외에는 대입 연산자가 호출이 됩니다. 이를 확인하기 위해 다음과 같이 Musician 클래스에 대입 연산자를 중복 정의를 하고 테스트를 해 보겠습니다. 먼저, 대입 연산자의 입력 매개 변수는 const Musician &형식으로 디폴트 대입 연산자와 같은 형식으로 하겠습니다. 이를 정의하면 디폴트 대입 연산자는 만들어지지 않습니다. 그리고 대입 연산자는 i=j=k; 와 같이 연쇄 작업이 가능해야 하기 때문에 반환 형식을 Musician &로 하겠습니다.

 

Musician.h

#pragma once

#include <iostream>

#include <string>

using namespace std;

class Musician

{

    int num;

    string name;

public:

    Musician();

    Musician(int _num,string _name);

    virtual ~Musician(void);

    Musician &operator=(const Musician &mu);

    void View();

};

 

 

 대입 연산자를 중복 정의하는 함수에서는 테스트 목적상 화면에 대입 연산자가 호출되었음을 출력하도록 할게요.

 

Musician.cpp

#include "Musician.h"

... 이전 예와 동일하므로 생략...

Musician &Musician::operator=(const Musician &mu)

{

    cout<<"대입 연산자가 호출되었음"<<endl;

    num = mu.num;

    name = mu.name;

    return (*this);

}

 

 이처럼 구현하면 [그림 7.5]와 같이 개체 생성과 동시에 =를 통해 초기화할 경우에 대입 연산자가 호출되지 않고 선언문 외에서 =을 사용할 때에만 대입 연산자가 호출됨을 알 수 있습니다. 선언문에서 =를 사용하면 대입 연산자가 수행되는 것이 아니고 복사 생성자가 호출됩니다.


대입 연산자와 복사 생성자 호출 시점

[그림 7.5]

  

 이번에는 대입 연산자를 반드시 중복 정의를 하는 것을 권장하는 경우에 대해 얘기해 보도록 할께요. 개체 내부에서 동적으로 다른 개체를 생성하여 관리하는 경우에 대입 연산자를 사용하면 내부의 개체는 같은 개체를 참조하게 됩니다. 이 경우에 각각의 소유 개체가 피 소유 개체를 독립적으로 유지되어야 하도록 프로그래밍해야 할 것입니다. 우리가 대입 연산자를 정의하지 않고 디폴트 대입 연산자를 사용하게 되면 두 개의 소유 개체가 같은 피 소유 개체를 소유하게 됩니다. 이 경우 피 소유 개체를 다른 소유 개체에서 소멸하였을 때 이미 소멸한 개체를 소유하게 되는 문제가 발생합니다.

 

 예를 들기 위해 Musicain 클래스에 악기를 소유하는 형태를 만들어 보도록 하겠습니다. 먼저, 악기에 대한 클래스로 Instrument를 간략하게 정의 및 구현해 봅시다.

 

Instrument.h

#pragma once

#include <iostream>

#include <string>

using namespace std;

 

class Instrument

{

    string name;

public:

    Instrument(string _name);

    string GetName()const;

};

 

 

Instrument.cpp

#include "Instrument.h"

 

Instrument::Instrument(string _name)

{

    name = _name;

}

string Instrument::GetName()const

{

    return name;

}

 

 음악가 클래스인 Musician에는 멤버 필드로 Instrument를 관리하는 필드를 추가하고 대입 연산자 중복 정의에서는 기존 음악가의 악기를 복사 생성하여 대입하는 구문을 작성해 주어야 음악가별로 별도의 악기를 소유할 수 있습니다.

 

Musician.h

#pragma once

#include "Instrument.h"

class Musician

{

    int num;

    string name;

    Instrument *instrument;

public:   

    Musician();

    Musician(int _num,string _name,Instrument* _instrument);

    virtual ~Musician(void);

    Musician &operator=(const Musician &mu);

    void View();

};

 

 

 음악가의 소멸자에서는 내부에서 동적으로 생성한 악기가 있는지를 확인하여 존재하는 경우에 소멸에 관한 책임을 표현해 주어야 합니다. 컴파일러에서는 이에 관한 책임을 개발자가 다하지 않는다고 하더라도 컴파일 오류를 발생하지 않을뿐더러 프로그램 동작 중에도 이 때문에 프로그램이 잘못 동작함을 발견하기 어렵습니다. 하지만 프로그램에서는 이미 필요없으며 관리되지 않는 개체에 대한 메모리가 계속 존재함으로써 서버 프로그램 같은 경우에 메모리 누수때문에 해당 프로세스가 점진적으로 쓸데없는 개체를 위해 할당된 메모리로 인해 무거워지고 느려질 수 있습니다.

 

 대입 연산자에서는 입력 인자로 전달받은 음악가의 악기를 복사 생성하여 대입해 주는 구문을 작성해 줌으로써 악기를 각각의 음악가가 독립적으로 소유하게 할 수 있을 것입니다.

 

Musician.h

#include "Musician.h"

Musician::Musician(int _num,string _name,Instrument* _instrument)

{

    num = _num;

    name = _name;

    instrument = _instrument;

}

Musician::Musician()

{

    num = 0;

    name = "";

    instrument = 0;

}

Musician::~Musician(void)

{

    if(instrument)

    {

        delete instrument;

    }

}

void Musician::View()

{

    cout<<"번호:"<<num<<" 이름:"<<name<<endl;

    cout<<instrument->GetName()<<"를 연주합니다."<<endl;

}

 

Musician &Musician::operator=(const Musician &mu)

{

    num = mu.num;

    name = mu.name;

    instrument = new Instrument(*(mu.instrument));

    return (*this);

}


[그림 7.6]

 

 [그림 7.6]을 보시면 정상적으로 동작함을 알 수 있습니다. 만약, 대입 연산자를 중복 정의하지 않고 디폴트 대입 연산자에 의해 동작하면 어떻게 될까요? [그림 7.7]과 같이 음악가가 소멸하면서 내부에 악기를 소멸하는 곳에서 버그가 발생하게 됩니다.


디폴트 대입 연산에서의 버그

[그림 7.7]


7장 산자 의 Part1

7장 산자 의 Part2

(모든 동영상 강의는 무료입니다.)

반응형