반응형

 

※ 연산자 오버로딩 기본 개념 참고

 

C++ 연산자 오버로딩

※ firend의 개념을 모른다면 다음 포스팅을 먼저 숙지할 것. C++ friend friend 선언 - 접근지시자에 관계없이 모든 멤버의 접근을 허용하는 선언이다. ※ 그렇기 때문에, 클래스의 friend 선언은 객체

srdeveloper.tistory.com


 

 

대입 연산자

- 정의하지 않으면 디폴트 대입 연산자가 삽입된다.

- 복사 생성자와 마찬가지로 멤버 대 멤버의 복사가 일어난다.

- 디폴트 대입 연산자는 얕은 복사이기 때문에 깊은 복사가 필요하다면 직접 정의해야한다.

#include <iostream>
using namespace std;

class SomeClass
{
private:
    int n1;
    
public:
    SomeClass(int n1 = 0) : n1(n1) { }
    void ShowData()
    {
        cout << n1 << endl;
    }
    SomeClass& operator=(const SomeClass& ref)
    {
        cout << "SomeClass operator=()" << endl;
        this->n1 = ref.n1;
        return *this;
    }
};

void main()
{
    SomeClass sc1(10);
    SomeClass sc2;
    sc2 = sc1;
    sc2.ShowData();
    
    //결과
    //SomeClass operator=()
    //10
}
  • sc2 선언시 바로 대입 연산자를 사용하면 대입 연산자가 호출 되는것이 아닌 복사 생성자가 호출이 된다.

 

 

 

디폴트 대입 연산자의 문제점

- 복사 생성자에서 생기는 문제와 유사하다. (얕은 복사의 문제)

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <cstring>
using namespace std;

class Name
{
private:
    char* name;
    
public:
    Name(const char* name) 
    {
        int len = strlen(name) + 1;
        this->name = new char[len];
        strcpy(this->name, name);
    }
    void ShowData()
    {
        cout << name << endl;
    }
    Name& operator=(const Name& ref)
    {
        delete[] name;
        int len = strlen(ref.name) + 1;
        this->name = new char[len];
        strcpy(this->name, ref.name);
        return *this;
    }
    ~Name()
    {
        delete[] name;
    }
};

void main()
{
    Name n1("Lee");
    Name n2("Yoon");
    n2 = n1;
    n2.ShowData();
    
    //결과 : Lee
}
  • 대입 연산자를 통한 얕은 복사에서 일어나는 메모리 누수를 막기위해 name 멤버 변수에 할당된 기존 데이터를 할당 해제하고 새롭게 할당하여 깊은 복사를 진행한다.

 

 

 

상속 구조에서의 대입 연산자

- 자식 클래스에서 대입 연산자를 오버로딩 하는 경우, 부모 클래스의 대입 연산자를 명시해 주지 않으면 부모 클래스의 대입 연산자는 호출이 되지 않는다.

- 따라서, 디폴트 대입 연산자의 경우 부모 클래스의 대입 연산자까지 호출되는 것을 확인 할 수 있다.

#include <iostream>
using namespace std;

class Parent
{
private:
    int n1, n2;
    
public:
    Parent(int n1 = 0, int n2 = 0) : n1(n1), n2(n2) { }
    void ShowData()
    {
        cout << n1 << " / " << n2 << endl;
    }
    Parent& operator=(const Parent& ref)
    {
        cout << "Parent operator=()" << endl;
        this->n1 = ref.n1;
        this->n2 = ref.n2;
        return *this;
    }
};

class Child : public Parent
{
private:
    int n1, n2;
    
public:
    Child(int n1, int n2, int n3, int n4) : Parent(n1, n2), n1(n3), n2(n4) { }
    void ShowData()
    {
        Parent::ShowData();
        cout << n1 << " / " << n2 << endl;
    }
    Child& operator=(const Child& ref)
    {
        cout << "Child operator=()" << endl;
        //부모 클래스 대입 연산자 호출
        Parent::operator=(ref);
        this->n1 = ref.n1;
        this->n2 = ref.n2;
        return *this;
    }
};

void main()
{
    Child c1(1, 2, 3, 4);
    Child c2(0, 0, 0, 0);
    c2 = c1;
    c2.ShowData();
    
    //결과
    //Child operator=()
    //Parent operator=()
    //1 / 2
    //3 / 4
}
  • Child 클래스에서 대입 연산자 오버로딩에 Parent::operator(ref)를 주석처리하고 실행해보면 부모 클래스의 멤버는 대입 연산자가 작용하지 않은것을 확인할 수 있다.

 

 

 

인덱스 연산자

- C, C++의 배열은 '경계검사를 하지 않는다'는 단점을 가지고 있다.

- 즉 배열의 범위에 벗어난 인덱스 값을 넣어도 실행하는데 무리없이 진행이 되기 때문에, 안정성을 고려해서 배열 클래스를 만든다.

#include <iostream>
using namespace std;

class IntArray
{
private:
    int* arr;
    int arrLen;

public:
    IntArray(int len) : arrLen(len)
    {
        arr = new int[len];
    }
    int& operator[](int index)
    {
        if (index < 0 || index >= arrLen)
        {
            cout << "Arr index out of range" << endl;
            exit(1);
        }
        return arr[index];
    }
    ~IntArray()
    {
        delete[] arr;
    }
};

void main()
{
    IntArray ia(3);
    for(int i = 0; i <= 3; i++)
    {
        ia[i] = i + 1;
    }
    
    //결과 : Arr index out of range
}
  • 결과와 같이 배열에 대한 잘못된 접근을 차단함으로써 안정성을 보장받을 수 있다.

 

 

 

※ 배열 클래스의 복사에 대해서..

- 배열은 저장소의 일종이고, 저장소에 저장된 데이터는 '유일성'이 보장되어야 하기 때문에, 저장소의 복사는 잘못된 일로 간주된다.

- 따라서, 복사 생성자와 대입 연산자는 private 멤버로 지정하여 복사를 원칙적으로 막는것이 좋은 선택이 되기도 한다.

//IntArray 클래스의 private에 복사 생성자와 대입 연산자를 멤버로 둔다.
IntArray(const IntArray& ref) { }
IntArray& operator=(const IntArray& ref) { }

 

 

 

 

const를 통한 배열 안정성 보장하기

- 어떠한 함수를 정의할 때, 매개변수를 const로 지정하면 해당 함수 내에서는 값의 변경이 불가능하므로 안정성을 보장받을 수 있지만, 위와 같은 코드에서는 인덱스 연산자에서 const 선언이 되어있지 않기 때문에 const로 지정된 매개변수로 받으면 컴파일 에러가 난다.

//전역함수
void ShowData(const IntArray& ref)
{
    for(int i = 0; i < 3; i++)
    {
        cout << ref[i] << endl;	//컴파일 에러 발생
    }
}

 

- 해당 컴파일 에러를 해결하려면 const함수를 오버로딩하면 해결이 된다.

#include <iostream>
using namespace std;

class IntArray
{
private:
    int* arr;
    int arrLen;
    
    IntArray(const IntArray& ref) { }
    IntArray& operator=(const IntArray& ref) { }

public:
    IntArray(int len) : arrLen(len)
    {
        arr = new int[len];
    }
    //배열 안에 값을 넣을 때 호출됨.
    int& operator[](int index)
    {
        if (index < 0 || index >= arrLen)
        {
            cout << "Arr index out of range" << endl;
            exit(1);
        }
        return arr[index];
    }
    //const 매개변수로 선언된 배열 안의 값을 찾을 때 호출된
    int operator[](int index) const
    {
        if (index < 0 || index >= arrLen)
        {
            cout << "Arr index out of range" << endl;
            exit(1);
        }
        return arr[index];
    }
    ~IntArray()
    {
        delete[] arr;
    }
};

void ShowData(const IntArray& ref)
{
    for(int i = 0; i < 3; i++)
    {
        cout << ref[i] << endl;
    }
}

void main()
{
    IntArray ia(3);
    for(int i = 0; i < 3; i++)
    {
        ia[i] = i + 1;
    }
    ShowData(ia);
    
    //결과
    //1
    //2
    //3
}
  • 기존의 인덱스 연산자에서 const를 붙이는게 아닌 오버로딩을 한 이유는, 배열 멤버에 대한 저장이 불가능해지기 때문이다.
  • 따라서 const 선언을 이용해 인덱스 연산자를 오버로딩하여 해결한다.

 

 

 

반응형

'Stack > C++' 카테고리의 다른 글

C++ 네임스페이스(namespace), using  (0) 2021.12.12
C++ 연산자 오버로딩  (0) 2021.12.05
C++ friend  (0) 2021.12.05
C++ 다중상속  (0) 2021.11.22
C++ 멤버함수, 가상함수 동작원리  (0) 2021.11.17

+ Recent posts