오버로딩은 매개변수의 타입에 따라 구분되지만 전위/후위 연산자는 매개변수를 받지 않는 문제가 있음

문제를 해결하기 위해 후위 연산자는 int 타입의 인자를 받도록 하고, 컴파일러는 후위 증감 연산자가 호출될 때 0을 전달

이름이 있는 매개변수를 사용하지 않으면 경고 메시지를 표시하기 때문에 이름을 제거

class UPInt
{
public:
    UPInt&& operator++(); // 전위 ++
    const UPInt operator++(int); // 후위 ++

    UPInt& operator--(); // 전위 --
    const UPInt operator--(int); // 후위 --
};

 

전위 연산자는 증감시키고 값을 사용하는 연산자이고, 후위 연산자는 값을 사용하고 증감시키는 연산자

UPInt& UPInt::operator++()
{
    *this += 1;
    return *this;
}

const UPInt UPInt::operator++(int)
{
    const UPInt oldValue = *this;
    ++(*this);
    
    return oldValue;
}

 

후위 연산자가 const 객체를 반환하는 이유는 연산자를 두 번 쓰는 경우 한 번만 적용되는데 이는 코드의 의미와 같지 않으므로 직관적이지 않고 헷갈리는 것을 방지하기 때문

따라서 operator++가 const 객체를 반환하여 처음 operator++에 의해 반환된 const 객체의 operator++를 호출하는데, 두 번째 operator++는 const가 아닌 멤버 함수이므로 호출할 수 없어 에러가 발생하도록 구현

UPInt i;
i++++; // i.operator++(0).operator++(0)이고, 실질적 의미는 i++와 같음

 

전위 연산자는 참조자 타입을 반환하고 후위 연산자는 const 객체 타입을 반환

후위 연산자는 전위 연산자에 비해 반환값을 위한 임시 객체 생성의 비용이 추가로 발생

 

후위 연산자는 반환 타입을 제외하면 전위 연산자와 하는 일이 같기 때문에 코드 중복을 피하는 목적으로 전위 연산자를 호출

C++은 컴파일러가 스스로의 추론에 의한 암시적인 타입 변환을 할 수 있도록 함

따라서 char->int, short->double 뿐만 아니라 데이터가 손상될 수 있는 int->short, double->char 타입 변환까지 가능

 

기본 데이터 타입들(int, short, char, double 등)은 언어 안에 내장되어 있어 암시적 변환을 제어할 수 없지만 사용자 정의 타입(직접 만든 타입)은 암시적 타입 변환을 수행하기 위해 컴파일러가 사용하는 함수를 제어할 수 있음

컴파일러가 사용할 수 있는 암시적 타입 변환은 단일 인자 함수, 암시적 타입 변환 연산자

 

단일 인자 생성자

인자 하나만 받도록 선언되거나 1개를 제외한 나머지가 모두 기본값을 갖도록 선언돼있는 생성자

class Name
{
public:
    Name(const string& s);
}

class Rational
{
public:
    Rational(int numerator = 0, int denominator = 1);
};

 

암시적 타입 변환 연산자

반환값에 대해 타입을 지정해 줄 수 없고, 반환값의 타입이 함수의 이름

class Rational
{
public:
    operator double() const;
};

Ration r(1, 2);
double d = 0.5 * r; // r을 double로 암시적 변환

 

타입 변환 함수들의 문제점

프로그래머의 의도와 상관 없이 호출될 수 있으므로 문제

디버깅하기에도 찾기 어려움

 

단일 인자 생성자의 문제와 해결 방법

template<class T>
class Array
{
public:
    Array(int lowBound, int highBound);
    Array(int size); // 단일 인자 생성자
    
    T& operator[](int index);
};

bool operator==(const Array<int>& lhs, const Array<int>& rhs);
Array<int> a(10);
Array<int> b(10);

for(int i = 0; i < 10; ++i)
{
    if(a == b[i]) // a에 들어 있는 요소를 b에 들어 있는 요소와 비교하려고 했는데 실수로 배열 인덱스 연산자를 붙이지 않은 상태
}

위 코드를 실행하면 컴파일러는 Array<int> 타입과 int 타입을 받는 operator== 연산자 함수가 호출되는 것으로 판단하고 int를 단일 인자 생성자에 전달하여 Array 타입으로 암시적 변환을 함

원하는 동작이 아닐 뿐더러 매번 Array를 생성하고 소멸하는 비용이 추가되기도 함

 

단일 인자 생성자에 explicit 키워드를 사용하여 암시적 타입 변환을 막아줌

(명시적 타입 변환은 허용됨)

template<class T>
class Array
{
public:
    Array(int lowBound, int highBound);
    explicit Array(int size); // 단일 인자 생성자
    
    T& operator[](int index);
};

 

암시적 타입 변환 연산자의 문제와 해결 방법

// Rational 클래스에 operator<< 함수를 선언하지 않았다고 가정
Rational r(1, 2);
std::cout << r;

컴파일러가 Rational::operator<<를 호출해야 하는 시점에서 함수를 찾지 못하지만 어떻게든 함수 호출을 성공시키기 위해 암시적 타입 변환 함수들을 찾아봄

위 코드에서는 Rational::operator double()을 찾아내어 double로 암시적 타입 변환하여 double::operator<<를 실행

따라서 프로그래머의 의도와 달리 함수가 잘못 호출되므로 위험한 코드

 

이러한 문제를 해결하기 위해 암시적 타입 변환 함수가 아닌 원하는 타입의 객체를 반환하는 함수를 선언해야 함

타입 변환 함수를 직접 호출하는 것이 불편하지만, 의도하지 않았던 잘못된 함수 호출을 방지할 수 있음

예를 들어, 표준 라이브러리에서 string을 char*로 변환하는 암시적 변환 연산자가 없고 대신에 직접 호출할 수 있는 c_str이 있음

class Rational
{
public:
    double asDouble() const;
};

Rational r(1, 2);
std::cout << r; // error
std::cout << r.asDouble(); // 정상

 

기본 생성자는 아무런 인자도 받지 않고 호출될 수 있는 생성자

생성자는 객체를 초기화하는 역할을 맡기 때문에, 기본 생성자는 객체 관련 정보를 갖지 않고도 객체를 초기화

 

다른 생성자를 선언했다면 컴파일러가 기본 생성자를 자동으로 선언하지 않기 때문에 기본 생성자 방법으로 사용할 경우 컴파일 에러 발생

class EquipmentPiece
{
public:
    EquipmentPiece(int IDNumber);
};

vector<EquipmentPiece> bestPieces(5); // error

vector<EquipmentPiece> bestPieces{EquipmentPiece(5)}; // 정상
vector<EquipmentPiece> bestPieces{5, 6}; // 정상

 

class EquipmentPiece
{
public:
    EquipmentPiece(int IDNumber = UNSPECIFIED);
    
private:
    static const int UNSPECIFIED = 0;
};

필요 없는 기본 생성자가 있는 클래스는 멤버 함수에서 데이터가 제대로 초기화되었는지 검사해야 하니 런타임 속도가 떨어지고, 코드 크기 증가, 검사 실패했을 때 처리하는 코드를 생각해야 하므로 코드 품질이 좋지 않음

상속의 장점은 기본 클래스 객체의 포인터나 참조자를 통해 파생 클래스 객체를 조작할 수 있는 점

 

class BST { ... };
class BalancedBST : public BST { ... };

void printBSTArray(ostream& s, const BST array[], int numElements)
{
    for(int index = 0; index < numElements; ++index)
    {
        s << array[index]; // BST 클래스에서 operator<<이 정의되어 있다고 가정
    }
}

BST BSTArray[10];
printBSTArray(cout, BSTArray, 10); // 정상

BalancedBST bBSTArray[10];
printBSTArray(cout, bSTArray, 10); // 미정의 동작

array[index]의 표현식은 *(array + index)이고, array+index가 가리키는 메모리 위치는 array + index * sizeof(BST)

따라서 컴파일러는 배열 내의 요소 위치를 정확히 지정하는 코드를 생성하기 위해 배열의 요소 객체 크기를 결정할 수 있어야 함

 

하지만 BalancedBST의 배열을 printBSTArray 함수에 전달하면 컴파일러는 배열의 요소 객체 크기를 BST라고 가정하지만 실제로는 BalancedBST의 크기를 갖고 있기 때문에 정확한 메모리 위치를 지정할 수 없어 미정의 동작 발생

 

결론적으로, 배열은 포인터의 산술 연산을 같이 할 뿐이므로 다형성을 갖고 있다는 생각은 하지 말자

C 스타일 캐스팅의 문제점

어떤 타입을 다른 타입으로 조건 없이 변경하는 것이 가능하기 때문에 버그가 발생할 수 있음

문법적으로 식별자를 괄호로 감쌌기 때문에 한 눈에 알아보기 어려움

int i = 1;
double d = (double)(i);

 

C++ 캐스트 연산자

C 스타일의 캐스팅의 문제점을 보완하기 위해 4개의 캐스팅을 도입

컴파일러가 캐스팅 에러를 찾아낼 수 있고, 한 눈에 알아보기도 쉬움

아래와 같은 문법으로 사용함

static_cast<타입>(표현식)

 

static_cast

C 스타일 캐스팅과 똑같은 캐스팅인 기본적인 캐스트 연산자

C 스타일 캐스팅과 똑같은 제약을 받아 struct를 int로 바꾸는 등의 작업을 하지 못함 

 

const_cast

상수성이나 휘발성(volatileness)를 없애는 데 사용

상수성이나 휘발성을 제거하는 것 이외의 용도로 const_cast를 사용하면 컴파일러 에러 발생

 

dynamic_cast

상속 계층 관계에 관련된 클래스 타입으로 안전하게 캐스팅할 때 사용

캐스팅의 실패는 nullptr이나 예외를 보고 판별할 수 있음

가상 함수가 없는 타입에는 적용할 수 없음

 

reinterpret_cast

포인터/정수 간의 변환 등 저수준 타입 재해석에 사용

거의 모든 포인터 변환을 허용하므로 매우 위험할 수 있음

 

dynamic_cast는 런타임에 실행되고 나머지는 컴파일 타임에 실행됨

포인터와 참조자는 다른 객체를 간접적으로 참조할 수 있게 하는 것

 

참조자는 널 참조자란 것이 없기 때문에 항상 메모리 공간을 차지한 객체를 참조하고 있어야 함

포인터의 값을 nullptr로 설정할 수 있기 때문에 객체를 참조하지 않을 경우도 있다면 포인터를 사용

 

char* pc = nullptr;
char& rc = *pc; // 널 포인터를 역참조한 것을 참조자를 통해 참조

컴파일이 되지만 미정의 동작 발생하므로 나쁜 코드


참조자는 반드시 객체를 참조하고 있어야 하기 때문에 선언될 때 반드시 초기화해야 함

string& rs; // 에러
string* ps; // 정상

string s("x");
string& rs = s; // 정상

 

참조자는 반드시 객체를 참조하고 있어야 하므로 사용하기 전에 유효성을 검사할 필요가 없어 포인터보다 더 효율적

하지만 포인터는 nullptr를 가리킬 수 있으므로 유효한 객체를 가리키고 있는지 검사해야 함

void printDouble(const double& rd)
{
    cout << rd;
}

void printDouble(const double* pd)
{
    if(pd)
    {
        cout << *pd;
    }
}

 

포인터는 다른 객체를 가리키도록 주소값을 변경할 수 있지만 참조자는 참조하는 객체를 변경하는 것이 불가능하여 초기화될 때 참조했던 객체만 참조

stirng s1("N");
string s2("C");

string& rs = s1;
string* ps = &s1;

rs = s2; // rs는 s1을 가리키지만 s1의 값이 s2의 값인 "C"로 변경됨
ps = &s2 // ps가 s2를 가리킴

 

연산자 함수의 반환값을 참조자로 반드시 사용해야 함

vector<int> v(10);
v[5] = 10;

 

만약 연산자 함수의 반환값을 포인터로 반환하면 코드가 포인터의 벡터인 것처럼 보이는 단점이 있음

*v[5] = 10;

+ Recent posts

목차