배움 저장소

[홍정모의 따라하며 배우는 C++] 9.연산자 오버로딩 본문

Programming Language/C++

[홍정모의 따라하며 배우는 C++] 9.연산자 오버로딩

시옷지읏 2021. 12. 25. 20:15

9.1 산술 연산자 오버로딩 하기


add 함수를 구현해보자. Parameter가 늘어날 때마다 오버로딩을 해주어야 한다

class Cents
{
private:
    int m_cents;
public:
    Cents(int cents=0){ m_cents = cents; }
    int getCents() const { return m_cents; }
};

Cents add(const Cents& c1, const Cents& c2){
    return Cents(c1.getCents() + c2.getCents());
}

int main(){
    Cents wallet1(5);
    Cents wallet2(13);
    
    cout << add(wallet1, wallet2).getCents() << endl;
}

 

연산자 오버로딩으로 구현하면 간편하다

- 위 코드에서 add 대신에 operator +를 입력하면 '+' 연산자를 사용할 수 있다.

Cents operator + (const Cents& c1, const Cents& c2){
    return Cents(c1.getCents() + c2.getCents());
}

int main(){
    Cents wallet1(5);
    Cents wallet2(13);
    
    cout << (wallet1 + wallet2 + Cents(2)).getCents() << endl;
}

 

연산자 오버로딩을 멤버함수로 구현해도 된다

- " + " 연산자는 이항연산자로 연산자의 왼쪽과 오른쪽에 피연산자를 가진다. 이 때 멤버변수는 왼쪽 피연산자를 이미 가지게 되므로 오른쪽 피연산자만 있으면 된다. 그래서 Parameter를 하나 추가하면 에러가 발생한다.

class Cents
{
private:
    int m_cents;
public:
    Cents(int cents=0){ m_cents = cents; }
    int getCents() const { return m_cents; }

    Cents operator + (const Cents& c) {
        return Cents(this->m_cents + c.m_cents);
    }
};

int main(){
    cout << (Cents(5) + Cents(13) + Cents(2)).getCents() << endl;
}

이 때 연산자 오버로딩의 매개변수에 const가 붙음을 확인하자. const 자료형은 인자로 R-value와 L-value 모두 가질 수 있다. const를 뺀 일반 자료형은 L-value만 가질 수 있다. 그래서 위 예제에서 const를 빼버리면 임시 객체는 사용할 수 없고 모두 L-vale만 사용해야 한다.

Cents wallet1(5);
Cents wallet2(13);

cout << (wallet1 + wallet2 ).getCents() << endl;

 

연산자 오버로딩 정보

꼭 외울 필요는 없다. 컴파일러가 경고,에러를 발생시키면 그때 구글링하자

(1) 다음 연산자 오버로딩은 반드시 멤버함수로 구현되어야 한다

    assignment operator " = "

    subscript " [ index ] "

    function call " ( ) "

    member selection " -> "

 

(2) " +, -, *, / " 모두 연산자 오버로딩이 가능하나 연산자의 우선순위는 바꿀 수 없다

(3) 아래 5개 연산자는 오버로딩이 불가능하다

    sizeof operator(sizeof), Trenary conditional operator(?), Scope resolution operator(::)

    member access operator(.), Pointer to member operator(.* or ->*), 

(4) Bitwise XOR operator(^)는 연산자 우선순위가 낮아 괄호를 사용해주어야 한다. 이 연산자는 오버로딩하지 말자

 

9.2 입출력 연산자 오버로딩 하기


멤버함수로 입출력 연산자를 구현할 수 없다

- 입출력 연산자 << 와 >>는 두 개의 피연산자를 가진다. 멤버 함수는 왼쪽의 피연산자가 항상 this이다. 입출력 연산자를 오버로딩하려면 ostream객체 내부에서 해당 객체를 위한 멤버변수를 새로 만들어야한다. ostream 객체 내부를 수정하기는 어렵다.

 

입출력 연산자의 overloading은 friend로 처리하자

- 이 때 i/o stream의 reference를 반환해주자. 연쇄호출이 가능하다

class Point
{
private:
    double m_x, m_y, m_z;
public:
    Point(double x = 0.0, double y = 0.0, double z = 0.0):m_x(x), m_y(y), m_z(z){}
    friend std::ostream& operator << (std::ostream& out, const Point& point){
        out << point.m_x << " " << point.m_y << " " << point.m_z << endl;
        return out; // for chaining member function
    }

    friend std::istream& operator >> (std::istream& in, Point& point) {
        in >> point.m_x >> point.m_y >> point.m_z;
        return in; // for chaining member function
    }
};

int main(){
    cout << Point(0.2, 0.1, 0.3) << " and " << Point(1.1, 2.2, 3.3) << endl;
    
    Point p1, p2;
    cin >> p1 >> p2;
    cout << p1 << p2 << endl;
}

 

출력 연산자 overloading을 활용하여 파일 출력을 구현할 수 있다

#include <fstream>
int main(){
    // Write File
    ofstream output("out.txt");
    output << Point(0.2, 0.1, 0.3) << Point(1.1, 2.2, 3.3) << endl;
    output.close();

    Point p1, p2;

    // Read File
    ifstream input("out.txt");
    input >> p1 >> p2;
    cout << p1 << p2;
    input.close();
}

생성된 파일 : out.txt

0.2 0.1 0.3
1.1 2.2 3.3

출력값

0.2 0.1 0.3
1.1 2.2 3.3

 

 

9.3 단항 연산자 오버로딩 하기


class Cents
{
private:
    int m_cents;
public:
    Cents(int cents): m_cents(cents){}

    friend std::ostream& operator << (std::ostream& out, const Cents& cents){
        out << cents.m_cents;
        return out; // for chaining member function
    }

    Cents operator - () const{ return Cents(-1 * m_cents); }
    bool operator !() const { return m_cents == 0; }
};

int main(){
    cout << -Cents(1) << endl;  // >> -1
    cout << -Cents(-1) << endl; // >>  1

    cout << boolalpha;
    cout << !Cents(1) << endl;  // >> false
    cout << !Cents(0) << endl;  // >> true
}

 

9.4 비교 연산자 오버로딩 하기


"==", "!=" 모두 구현가능하다

- class 내부에 friend keyword가 입력된 함수를 선언해주자

bool operator !=(const Cents& left, const Cents& right){
    return left.m_cents != right.m_cents;
}

int main(){
    cout << boolalpha;
    cout << (Cents(0) != Cents(1)) << endl;
    cout << (Cents(1) != Cents(1)) << endl;
}

 

"<" 구현하기

- <algorithm>에 구현된 sort ( ) 함수를 사용하려면 less than operator "<"가 구현되어야 한다

#include <iostream>
#include <random>
#include <vector>

using namespace std;

class Cents
{
private:
    int m_cents;
public:
    Cents(int cents=0): m_cents(cents){}
    int& getRef(){ return m_cents; }

    friend std::ostream& operator << (std::ostream& out, const Cents& cents){
        out << cents.m_cents;
        return out; // for chaining member function
    }

    friend bool operator < (const Cents& left, const Cents& right){
        return left.m_cents < right.m_cents;
    }
};

int main(){
    // init
    vector<Cents> v(10);
    for(int i=0; i<v.size(); ++i) { v[i].getRef() = i; }

    // shuffle
    std::random_device rd;
    std::mt19937 engine(rd());
    std::shuffle(v.begin(), v.end(), engine);

    // before sort, print
    for(Cents&c:v){ cout << c.getRef() << " "; }
    cout << endl;

    // sort
    std::sort(v.begin(), v.end());

    // after sort, print
    for (Cents& c : v) { cout << c.getRef() << " "; }
    cout << endl;
}

 

9.5 증감 연산자 오버로딩 하기


Prefix와 Postfix는 Parameter로 구분한다

- Prefix는 Parameter가 없다. Postfix는 dummy Parameter를 넣어준다

class Digit{
private:
    int m_digit;
public:
    Digit(int digit): m_digit(digit){}
    
    friend std::ostream& operator <<(std::ostream& out, const Digit& digit){
        out << digit.m_digit;
        return out;
    }

    // Prefix
    Digit& operator ++ () {
        ++this->m_digit;
        return *this;
    }

    // Postfix
    Digit operator ++ (int) { // must add dummy para
        Digit temp = *this;
        this ->m_digit++;
        return temp;
    }
};

int main(){
    Digit ten(10);
    cout << ++ten << endl;
    cout << ten << endl;

    Digit five(5);
    cout << five++ << endl;
    cout << five << endl;
}

 

9.6 첨자 연산자 오버로딩 하기

[ ] subscript operator


첨자 연산자 없이 멤버변수 배열에 접근해보자 : 불편하다

class IntList {
private:
    int m_list[10];
public:
    int* getList(){ return m_list; }
};

int main(){
    IntList ilist;
    for(int i=0; i<10; ++i){ ilist.getList()[i] = i; }

    for (int i = 0; i < 10; ++i){ cout << ilist.getList()[i] << " "; }
}

 

subscript operator [ ] 첨자 연산자 구현하기

- 다음은 첨자 연산자에 정수형을 사용하는 예제이다. map을 사용할 때는 문자열로 구현한다.

- Instance에 const가 붙어있으면 별도의 함수를 호출해야 한다. 멤버함수를 const로 오버로딩 해주자.

class IntList {
private:
    int m_list[10] = { 0, };
public:
    //int& operator [](const int &index){ return *(m_list + index); }
    int& operator [](const int &index) {
        assert(0 <= index); assert(index < 10);
        return m_list[index];
    }
    const int operator [] (const int &index) const {
        assert(0 <= index); assert(index < 10);
        return m_list[index];
    }
};

int main() {
    IntList ilist;
    //for (int i = 0; i < 10; ++i) { ilist[i] = i; }
    for (int i = 0; i < 10; ++i) { cout << ilist[i] << " "; }
    cout << endl;

    const IntList const_ilist;
    for (int i = 0; i < 10; ++i) { cout << const_ilist[i] << " "; }
}

- assert는 inline으로 구현되어있다. if( expression ) 보다 훨씬 빠르다.

- Release 모드에서도 작동하지 않으므로 따로 코드를 수정하지 않아도 된다.

 

객체 포인터를 사용할 때 객체 배열과 subscription operator를 혼용하지 말자

IntList *ptr = new IntList;
ptr[1];    // access class array's next element
(*ptr)[1]; // use subscription operator, access to member variable

 

subscription operator로 map 구현하기

struct StudentGrade{
	std::string name{};
	char grade{};
};

class GradeMap{
private:
	std::vector<StudentGrade> m_map{};

public:
	char& operator[](const std::string& name);
};

char& GradeMap::operator[](const std::string& name)
{
	auto found{ std::find_if(m_map.begin(), m_map.end(),
				[&](const auto& student){ // this is a lambda
  					return (student.name == name);
				}) };

	if (found != m_map.end())
		return found->grade;

	// otherwise create a new StudentGrade for this student and add
	// it to the end of our vector.
	m_map.push_back({ name });

	// and return the element
	return m_map.back().grade;
}

 

9.7 괄호 연산자 오버로딩과 함수 객체

parenthesis and Function object(Functor)


클래스 이름을 함수처럼 활용할 수 있다

class Functor {
private:
    int m_value;
public:
    Functor(int value=0): m_value(value){}
    int operator () (int added) { return (m_value += added); }
};

int main(){
    Functor func(0);
    cout << func(5) << endl; // >> 5
    cout << func(5) << endl; // >> 10
}

 

9.8 형변환을 오버로딩 하기


클래스를 형변환 할 때 호출되는 함수를 구현해보자

- 아래 형변환 오버로딩 멤버함수는 해당 객체를 정수형으로 변환할 때 호출된다

class Cents {
private:
    int m_cents;
public:
    Cents(int cents=0):m_cents(cents){}
    operator int() { 
        cout << "casting!" << endl;
        return m_cents; 
    }
};

아래를 실행하면

Cents cent(5);
int value = int(cent);
(int)cent;

다음이 출력된다

casting!
casting!

 

달러 클래스를 센트 클래스로 형변환하는 오버로딩 멤버함수를 구현해보자

- main에서 Cents Instance가 출력되는 이유는 int로 형변환되기 때문이다.

- 함수명이 자료형을 알려주기 때문에 형변환 오버로딩에 반환 자료형이 없다.

class Cents {
private:
    int m_cents;
public:
    Cents(int cents=0):m_cents(cents){}
    operator int() { 
        cout << "casting!" << endl;
        return m_cents; 
    }
};

class Dollar{
private:
public:
    int m_dollars = 0;
public:
    Dollar(const int& dollars) : m_dollars(dollars) {}

    // Type Conversion for class Cents
    operator Cents() { return Cents(m_dollars * 100); }
};

int main(){
    Cents wallet = Dollar(1);
    cout << wallet << endl;
}

- 위에서 Cent 인스턴스를 바로 출력할수도 있고 함수를 구현하여 출력할 수도 있다. 함수를 따로 구현한다면 매개변수의 자료형은 const int&여야 한다. Cents의 형변환 오버로딩의 반환값이 R-Value이기 때문이다. const 자료형은 L-Value, R-Value 모두 복사할 수 있다.

void printCent(const int& value){ cout << value << endl; }

int main() {
    Cents wallet;
    printCent(wallet);
}

 

9.9 복사 생성자, 복사 초기화, 반환값 최적화

                                                              Return Value Optimization


Copy Constructor 복사 생성자를 사용한 예제이다

- 보안을 위해 복사생성자를 private sector에 넣을 수 있다. 그러면 외부에서 복사생성자를 사용할 수 없다. 

class Fraction {
private:
    int m_numerator;
    int m_denominator;
public:
    Fraction(int num = 0, int den = 1) : m_numerator(num), m_denominator(den) 
    { assert(den != 0); }

    // Copy Constructor
    Fraction(const Fraction& fraction):m_numerator(fraction.m_numerator), m_denominator(fraction.m_denominator)
    { cout << "Copy Initialization is Called!" << endl; }

    friend std::ostream& operator << (std::ostream& out, const Fraction &fraction) {
        out << fraction.m_numerator << " / " << fraction.m_denominator;
        return out;
    }
};

int main(){
    Fraction f1(3,5);
    Fraction copied(f1);
    cout << copied << endl;   // >> Copy Initialization is Called

    Fraction copied_2 = f1;
    cout << copied_2 << endl; // >> Copy Initialization is Called
}

 

Return Value Optimization(반환값 최적화)

- 아래 예제를 debug 모드에서 실행하면 복사생성자가 호출된다. release 모드에서 실행하면 복사생성자가 호출되지 않는다. 컴파일러가 최적화를 해주었다. 복사생성자 호출은 컴파일러가 생략할 수 있다

Fraction ReturnValueOptimization(){
    Fraction temp(1,2);
    cout << &temp << endl;
    return temp;
}

int main(){
    Fraction result = ReturnValueOptimization();
    cout << &result << endl;
    cout << result << endl;
}

Output

-Debug-                          -Release Mode-
 00CFF900                         00D5FAE4
 Copy Initialization is Called!   00D5FAE4
 00CFF9F0                         1 / 2
 1 / 2

 

9.10 변환 생성자, explicit, delete

converting constructor                     


변환 생성자는 생성자를 변환시켜 준다

 

explicit keyword는 변환 생성자를 금지한다

 

delete는 특정 생성자를 지운다. 이때 동적할당에서 사용한 메모리 해제와 다른 의미이다

 

Converting Constructor(변환생성자)의 활용

- 클래스가 매개변수로 사용되었을 때, 매개변수의 값이 클래스의 생성자 매개변수 포맷과 일치한다면 자동으로 생성자를 호출해준다. 이를 변환생성자라 한다.

void ConversionPrint(Fraction frac){
    cout << frac << endl;
}

int main(){
    ConversionPrint(7); // >> 7 / 1
}

 

객체의 생성자에 explicit keyword를 추가하면 변환생성자를 사용할 수 없다

class Fraction {
//...
public:
    explicit Fraction(int num = 0, int den = 1) : m_numerator(num), m_denominator(den) 
    { assert(den != 0); }
//...
}

다음과 같은 에러가 발생한다. no suitable constructor exists to convert from "int" to "Fraction

int main(){
    ConversionPrint(7); // Error!
}

 

생성자 지우기, delete keyword 활용하기

(1) 업데이트 된 프로그램에서 특정 생성자를 금지할 때 delete keyword를 사용한다.

(2) 클래스 내부에서 의도하지 않은 conversion constructor가 실행되서 원하지 않는 결과값을 얻을 수 있다. 이를 방지하기 위하여 해당 conversion constructor를 지워버릴 수 있다.

 

의도하지 않은 변환생성자가 호출되는 사례를 확인해보자

(1) 아래 예제에서 int 매개변수로 string 자료형의 길이를 정한다. 문자 매개변수가 들어오면 자동으로 int 자료형으로 변환되어 사용한다.

(2) 문자를 넣어 해당 인스턴스를 초기화 시도할 수 있다. 원하지 않는 결과가 나타난다.

(3) 따라서 문자형 인수를 사용했을 때 Error를 발생시켜야 한다.

- explicit keyword를 사용하면 conversion constructor는 실행되지 않는다.

- delete keyword를 사용하면 함수 내부에서도 conversion constructor를 사용할 수 없다.

class MyString {
private:
    std::string m_string;
public:
    //MyString(char) = delete;
    //explicit MyString(int x) { m_string.resize(x); }

    MyString(int x) { m_string.resize(x); }// allocate string of size x

    MyString(const char* string) { m_string = string; } // allocate string to hold string value
    friend std::ostream& operator<<(std::ostream& out, const MyString& s);
};

std::ostream& operator<<(std::ostream& out, const MyString& s) {
    out << s.m_string;
    return out;
}

void printString(const MyString& s) { std::cout << s; }

int main() {
    MyString mine = 'x';
    std::cout << mine;
    printString('x');
}

 

9.11 대입 연산자 오버로딩, 깊은 복사, 얕은 복사


멤버변수가 동적할당메모리를 사용하고 있을 때 대입연산자는 깊은 복사와 얕은 복사를 할 수 있다

 

다음은 얕은 복사 예제이다. 기본 copy constructor(복사연산자)를 사용하면 얕은 복사가 실행된다

- 얕은 복사 이후 복사본은 원본과 동적할당메모리를 공유한다.

- 복사본이 소멸자를 호출하여 동적할당 메모리를 해제시키면 원본은 더이상 동적할당메모리를 사용할 수 없다.

- 아래 예제를 실행시켜보자. 해제된 동적할당 메모리를 다시 해제하여 에러가 발생한다.

#include <iostream>
#include <cassert>
using namespace std;

class MyString {
private:
    char* m_data = nullptr;
    int m_length = 0;

public:
    MyString(const char* source = "") {
        assert(source);
        m_length = std::strlen(source) + 1;// for adding null character
        m_data = new char[m_length];
        for (int i = 0; i < m_length; ++i) { m_data[i] = source[i]; }
        m_data[m_length - 1] = '\0';
    }

    ~MyString() {
        delete[] m_data;
    }
    char* getData() { return m_data; }
};

int main() {
    MyString hello("Hello");
    cout << (int*)hello.getData() << endl;
    cout << hello.getData() << endl;

    {
        MyString copied = hello; // call default copy constructor
        //copied = hello // call assignment operator
        
        cout << (int*)copied.getData() << endl;
        cout << copied.getData() << endl;
    }
    cout << hello.getData() << endl;
}
0083E178
Hello
0083E178
Hello
硼硼硼硼硼

 

깊은 복사이다. deep copy constructor(복사연산자)를 구현했다. 동적할당메모리를 새로 요청하여 값을 복사한다

class MyString {
private:
    char* m_data = nullptr;
    int m_length = 0;

public:
    MyString(const char* source = "") {
        assert(source);
        m_length = std::strlen(source) + 1;// for adding null character
        m_data = new char[m_length];
        for (int i = 0; i < m_length; ++i) { m_data[i] = source[i]; }
        m_data[m_length - 1] = '\0';
    }

    ~MyString() {
        delete[] m_data;
    }
    char* getData() { return m_data; }

    // Deep Copy Constructor
    MyString(const MyString& source) {
        cout << "Copy constructor " << endl;
        m_length = source.m_length;
        if (source.m_data != nullptr) {
            m_data = new char[m_length];
            for(int i=0; i<m_length; ++i)
                m_data[i] = source.m_data[i];
        } else {
            m_data = nullptr;
        }
    }
};
00A9C898
Hello
Copy constructor
00A9C668
Hello
Hello

 

Copy Constructor VS Assignmnet Operator

- 초기화 때 사용하는 assignment operator는 복사 생성자를 호출한다.

- 인스턴스에 다른 인스턴스 값을 넣을 때는 오버로딩된 연산자가 호출된다.

헷갈린다면 객체를 초기화할 때 = 연산자를 피하고 { } 혹은 ( )를 사용하자

MyString hello("Hello");
MyString Copied = hello; // Copy constructor

MyString Assigned;
Assigned = hello; // Assignmnet Operator

 Assignmnet Operator를 사용하려면 객체 멤버함수에 다음을 추가해주자

MyString& operator = (const MyString& source) {
    cout << "Assignmnet Operator " << endl;

    if (this == &source) return *this; // Instance = Instance Makes Error
                                       // Prevent self-assignmnet 
    delete[] m_data; // free used allocated memory.
    m_length = source.m_length;

    if (source.m_data != nullptr) {
        m_data = new char[m_length];
        for (int i = 0; i < m_length; ++i)
            m_data[i] = source.m_data[i];
    }
    else {
        m_data = nullptr;
    }
    return *this;
}

 

기본으로 구현되는 얕은 복사생성자를 delete keyword로 금지할 수 있다

- 얕은 복사생성자를 허용하면 동적할당 메모리 관리가 어려워질 수 있다. 복사를 금지해보자.

class MyString {
public:
// ...
    MyString(const MyString& source) = delete;
// ...
};

 

9.12 이니셜라이져 리스트 initializer list


initializer list는 배열 자료형을 한꺼번에 초기화해준다. 클래스에도 사용해보자

  <initializer_list> 라이브러리를 포함시켜주자

#include <iostream>
#include <initializer_list>
#include <cassert>
using namespace std;

class IntArray {
private:
    unsigned m_length = 0;
    int *m_data = nullptr;
public:
    IntArray(unsigned length) :m_length(length){
        m_data = new int[length];
    }
    ~IntArray() {
        delete[] this->m_data;
    }
    
    // take Initializer list parameters for it
    IntArray(const std::initializer_list<int>& list) : IntArray(list.size()) {
        int i = 0;                                 // call another constructor
        for (auto& e : list) { // only iterator is available
            m_data[i] = e; 
            ++i;
        }

        //for(unsigned i = 0; i<list.size(); ++i)
        //    m_data[i] = list[i]; // Error! initzer list didn't support []
    }

    friend ostream& operator << (ostream& out, IntArray& arr) {
        for(unsigned i=0; i< arr.m_length; ++i)
            out << arr.m_data[i] << " ";
        return out;
    }
};

int main() {
    int arr[5] = { 1, 2, 3, 4, 5 };// initializer list
    int *dy_arr = new int[5] { 1, 2, 3, 4, 5 };// initializer list

    auto il = { 10, 20, 30 }; // auto is initializer_list<int>

    IntArray int_array { 1, 2, 3, 4, 5 };
    IntArray int_array2 = { 6, 7, 8, 9, 10 };

    cout << int_array << endl;
}

 

initializer list를 대입연산자에 구현하기: 멤버함수로 구현해보자

IntArray& operator = (const std::initializer_list<int>& list) {
    cout << "Assignment operator is executed" << endl;
    delete[] m_data;
    m_length = list.size();

    m_data = new int[m_length];
    int i = 0;
    for (auto& e : list) {
        this->m_data[i] = e;
        i++;
    }
    return *this;
}
int main() {
    IntArray int_array{ 1, 2, 3, 4, 5 };
    IntArray int_array2(5);
    int_array2 = {11, 12, 13};

    cout << int_array2 << endl;
}
Assignment operator is executed
11 12 13
Comments