ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [C++] 대입연산자 (assignment operator)
    C++ 2023. 1. 9. 16:15

    우선 복사생성자와 대입연산자를 헷갈리면 안됨

    두 개가 호출되는 시점 차이가 있음 ㅇㅇ

     

    대입연산자는

    이미 생성된 두 객체 간 대입연산이 진행될때,

    기존에 생성된 두 객체 간의 대입연산 시에 호출되는거임

    (복사생성자와 호출시점 비교)

     

    operator=(Point& ref)

     

    이 형식으로 호출되는 거임.


    연산자 오버로딩의 한 종류로

    오버로딩 중에서 가장 중요한 놈임!

     

    클래스의 대입연산자는 반드시 정의해주어야한다!

     

    대입연산자는 복사생성자와 동일한 구석이 있다.

     


    [대입연산자와 복사생성자의 공통점]

     

    1) 정의하지 않으면 디폴트 가 자동으로 삽입된다

    2) 디폴트 는 얕은 복사를 한다 ( 깊은 복사 X )

    3) 연산자 내에서 동적할당을 한다면, 깊은 복사가 필요하다면, 직접 정의해야 한다!


    이와 같이 대입연산자와 복사생성자는 99% 비슷하지만 호출시점에 차이가 있다.

     


    [호출 시점의 차이 (이게 개중요한거임)]


    복사생성자

    객체 생성과 동시에 기존 객체로 초기화 할때 호출됨

     

    Point p1 = p2;
    Point p1(p2);


    대입연산자

    기존 객체를 기존 객체로 초기화할때 호출됨

     

    p1 = p2;

     


    [default 대입연산자]

    (대입연산자를 정의하지 않았을때)

     

    자식 클래스에 대입연산자를 명시 하지 않으면 default 대입연산자가 자동으로 삽입된다.

    그리고 이때 삽입되는 default 대입연산자는 얕은복사를 한다

    또한 default 대입연산자에는 부모클래스 대입연산자를 호출하는 코드가 있다

     


    [대입연산자는 생성자가 아니다!]

     

    대입연산자를 직접 정의했을때

    부모클래스의 대입연산자가 명시되어 있지 않으면 진짜로 안부른다

     

    즉, 컴파일러가 자동으로 추가해주지 않는다!

     

    class First {
    private:
    	int num1, num2;
    public:
    	First(int n1 = 0, int n2 = 0):num1(n1), num2(n2) {
    		// empty
    	}
    
    	First& operator=(First& ref) {
    		num1 = ref.num1;
    		num2 = ref.num2;
    		return *this;
    	}
    };
    class Second : public First {
    private:
    	int num3, num4;
    public:
    	Second(int n1=0, int n2=0,int n3 = 0, int n4 = 0) : First(n1,n2), num3(n3), num4(n4) {
    		// emtpy
    	}
    	
        
        // Second 대입연산자 직접 정의!
    	Second& operator=(Second& ref) {
    		num3 = ref.num3;
    		num4 = ref.num4;
    		return *this;
    	}
    };

     

    이렇게 First 클래스를 상속하는 Second 클래스가 있을때

     

    Second& operator=(Second& ref) {
    	num3 = ref.num3;
    	num4 = ref.num4;
    	return *this;
    }

     

    Second 클래스의 대입연산자에서 First 클래스의 대입연산자를 명시해놓지 않으면 진짜로 안부름 ㅇㅇ...

    그래서 아래와 같이 직접 불러줘야함ㅋㅋ

     

    Second& operator=(Second& ref) {
        // 대입연산자 직접 정의해줬으므로
        // 부모클래스 대입연산자도 직접 명시해주어야함!
        First::operator=(ref);
        
        num3 = ref.num3;
    	num4 = ref.num4;
    	return *this;
    }

     

    생성자와 이 부분이 다른거임

     

    우리가 자식클래스의 생성자를 직접 정의했을때,

    부모클래스 생성자를 명시해놓지 않아도

    알아서 컴파일러는 부모클래스의 기초생성자(void형) 을 추가하여

    호출시켜주지만 대입연산자는 아니다

     

    따라서 자식클래스에 대입연산자를 직접 정의했으면

    꼭 무조건 명시적으로 부모클래스 대입연산자를 적어줘야한다!

     

    여기서 한가지 짚고 넘어가야할게 있다면,

     

    First::operator=(ref) 일때 ref는 Second& 형이고

    First의 operator= 매개변수형은 First& 형인데 어케 인자 전달이 가능할까

     

    다형성 ㅇㅇ 이건 포인터뿐만 아니라 참조자에도 적용되므로

    작은놈 큰놈 참조 ㄱㄴ하니까 ㄱㄴ한거임

     


    [멤버이니셜라이저 성능평가]

     

    이제 생성자에서 이니셜라이저 쓰는게 왜 성능이 더 좋은지 알 수 있다

     

    class AAA {
    private:
    	int num;
    public:
    	// 기본생성자
    	AAA(int n = 0) : num(n) {
    		//empty
    	}
    
    	// 복사생성자
    	AAA(const AAA& ref) {
    		num = ref.num;
    	}
    
    	// 대입연산자
    	AAA& operator=(const AAA& ref) {
    		num = ref.num;
    		return *this;
    	}
    };

     

    클래스 BBB와 CCC에서 사용될 AAA 클래스

     

     

    class BBB {
    private:
    	AAA mem;
    public:
    	// 복사생성자
    	BBB(const AAA& ref) : mem(ref) {
    		// empty
    	}
    };

     

    BBB 클래스는 복사생성자에 멤버이니셜라이저를 사용했다.

    따라서 복사생성자 호출 시 mem(ref) 복사생성자 하나만 호출된다

     

    class CCC {
    private:
    	AAA mem;
    public:
    	CCC(const AAA& ref) {
    		mem = ref;
    	}
    };

    CCC 클래스는 복사생성자에 멤버이니셜라이저를 사용하지 않았다.

     

    그럼 mem = ref 라는 코드는 선언과 초기화를 각각 별도의 문장에서 진행하게 된다

    // 1. 기본생성자로 객체생성
    AAA mem;  
    
    // 2. 대입연산자 호출하여 초기화!
    mem.operator=(ref)

     

    바로 이렇게 말이다

     

    따라서 멤버이니셜라이저를 사용하면 작업횟수가 한 번으로 줄어들어 더 성능이 좋아지는 것이다.

    멤버이니셜라이저를 최대한 사용하도록 노력하자 ㅇㅇ