friend 키워드는 클래스 내부에서 다른 클래스나 함수들을 frient 로 정의할 수 있는데, friend 로 정의 된 클래스나 함수들은 원래 클래스의 private 로 정의된 변수나 함수들에 접근할 수 있다.
아래 코드에서 클래스 B 와 void func() 는 A 의 friend 라고 선언하고 있다. 이렇게 friend 라고 선언하게 되면, B 와 func 안에서는 A 의 모든 private 멤버 함수들과 멤버 변수들에 대한 접근 권한을 부여하게 된다.
-> 정말 친한 친구 사이라고 보면 된다.
한가지 재미있는 점은 짝사랑과 같다는 점이다.
-> 아래 코드에서 B 는 A 의 모든 private 들을 볼 수 있지만, B 안에서 A 를 friend 로 지정하지 않는 이상, A 는 B의 private 개체들에 접근할 수 없다.
class A {
private:
void private_func() {}
int private_num;
// B 는 A 의 친구!
friend class B;
// func 은 A 의 친구!
friend void func();
};
class B {
public:
void b() {
A a;
// 비록 private 함수의 필드들이지만 친구이기 때문에 접근 가능하다.
a.private_func();
a.private_num = 2;
}
};
void func() {
A a;
// 비록 private 함수의 필드들이지만 위와 마찬가지로 친구이기 때문에 접근
// 가능하다.
a.private_func();
a.private_num = 2;
}
int main() {}
이항 연산자 오버로딩
아래 이항 연산자 오버로딩에는 다음과 같은 문제점이 있다.
1 은 정상적으로 컴파일 되지만,
2 는 컴파일되지 않는다.
왜냐면 1 은 a.operator+("i3.923"); 로 변환될 수 있지만, 2 는 그렇지 못하기 때문이다.
a = a + "i3.923"; // 1
a = "i3.923" + a; // 2
하지만 개발자 입장에서는 원칙적으로 클래스를 사용하는 입장에서 1 이 된다면 2 도 되어야 한다.
다행스럽게, 컴파일러는 이항 연산자를 다음과 같은 두 개의 방식으로 해석한다.
a.operator+(b);
operator+(a, b);
컴파일러는 둘 중 가능한 케이스를 택해서 처리한다.
a.operator+(b) 에서의 operator+ 는 a 의 클래스 멤버 함수로써 사용되는 것이고,
operator+(a, b) 에서의 operator+ 는 클래스 외부에 정의되어 있는 일반적인 함수를 의미하게 된다.
이를 처리하기 위해서 함수를 정의하면 다음과 같다.
-> 두 개의 const Complex& 타입의 인자 a,b 를 받는 함수를 구현했다.
Complex operator+(const Complex& a, const Complex& b) {
...
}
앞에서 언급했던 것과 같이 컴파일러는 정확히 타입이 일치하지 않는 경우, 가장 가까운 가능한 오버로딩 되는 함수를 찾게 되는데, 마침 우리에게는 Complex(const char * ) 타입의 생성자가 있다면 아래와 같이 타입 변환이 가능하게 된다.
"i3.923" + a; // before
operator+(Complex("i3.923"), a); // after
구현된 코드는 다음과 같다.
-> Complex 클래스 내부에 friend Complex operator+(const Complex& a, const Complex& b); 선언을 한다.
#include <iostream>
#include <cstring>
class Complex {
private:
double real, img;
double get_number(const char* str, int from, int to) const;
public:
Complex(double real, double img) : real(real), img(img) {}
Complex(const Complex& c) { real = c.real, img = c.img; }
Complex(const char* str);
Complex operator+(const Complex& c) const;
Complex& operator=(const Complex& c);
// 나머지 연산자 함수들은 생략 :)
void println() {
std::cout << "( " << real << " , " << img << " ) " << std::endl;
}
// 이제 이 함수는 Complex 의 private 멤버 변수들에 접근할 수 있습니다.
friend Complex operator+(const Complex& a, const Complex& b);
};
Complex operator+(const Complex& a, const Complex& b) {
Complex temp(a.real + b.real, a.img + b.img);
return temp;
}
Complex::Complex(const char* str) {
// 입력 받은 문자열을 분석하여 real 부분과 img 부분을 찾아야 한다.
// 문자열의 꼴은 다음과 같습니다 "[부호](실수부)(부호)i(허수부)"
// 이 때 맨 앞의 부호는 생략 가능합니다. (생략시 + 라 가정)
int begin = 0, end = strlen(str);
img = 0.0;
real = 0.0;
// 먼저 가장 기준이 되는 'i' 의 위치를 찾는다.
int pos_i = -1;
for (int i = 0; i != end; i++) {
if (str[i] == 'i') {
pos_i = i;
break;
}
}
// 만일 'i' 가 없다면 이 수는 실수 뿐이다.
if (pos_i == -1) {
real = get_number(str, begin, end - 1);
return;
}
// 만일 'i' 가 있다면, 실수부와 허수부를 나누어서 처리하면 된다.
real = get_number(str, begin, pos_i - 1);
img = get_number(str, pos_i + 1, end - 1);
if (pos_i >= 1 && str[pos_i - 1] == '-') img *= -1.0;
}
double Complex::get_number(const char* str, int from, int to) const {
bool minus = false;
if (from > to) return 0;
if (str[from] == '-') minus = true;
if (str[from] == '-' || str[from] == '+') from++;
double num = 0.0;
double decimal = 1.0;
bool integer_part = true;
for (int i = from; i <= to; i++) {
if (isdigit(str[i]) && integer_part) {
num *= 10.0;
num += (str[i] - '0');
} else if (str[i] == '.')
integer_part = false;
else if (isdigit(str[i]) && !integer_part) {
decimal /= 10.0;
num += ((str[i] - '0') * decimal);
} else
break; // 그 이외의 이상한 문자들이 올 경우
}
if (minus) num *= -1.0;
return num;
}
Complex Complex::operator+(const Complex& c) const {
Complex temp(real + c.real, img + c.img);
return temp;
}
Complex& Complex::operator=(const Complex& c) {
real = c.real;
img = c.img;
return *this;
}
int main() {
Complex a(0, 0);
a = "-1.1 + i3.923" + a;
a.println();
}
위 코드에서 모든 문제가 해결된 것은 아니다.
아래 실행 코드에서는, 다음과 같이 두가지 연산자 중 어떤 연산자를 사용할 지 모르겠다는 오류가 발생한다.
int main() {
Complex a(0, 0);
a = "-1.1 + i3.923" + a;
a = a + a;
a.println();
}
앞서 컴파일러는 a + a 를 해석할 때 아래와 같이 두가지 형태 중 하나를 선택하려 한다는 것이다.
a.operator+(a);
operator+(a, a);
이를 해결하기 위해서는 둘 중 하나의 함수를 없애야 한다.
기본적으로, 자기 자신을 리턴하지 않는 이항 연산자들, 예를 들어 +, -, *, / 들은 모두 외부 함수로 선언하는 것 이 원칙이다.
반대로, 자기 자신을 리턴하는 이항 연산자들, 예를 들어 +=, -= 들은 모두 멤버 함수로 선언하는 것이 원칙이다.
위 원칙을 적용한 코드는 다음과 같다.
#include <cstring>
#include <iostream>
class Complex {
private:
double real, img;
double get_number(const char* str, int from, int to) const;
public:
Complex(double real, double img) : real(real), img(img) {}
Complex(const Complex& c) { real = c.real, img = c.img; }
Complex(const char* str);
Complex& operator=(const Complex& c);
// 나머지 연산자 함수들은 생략 :)
void println() {
std::cout << "( " << real << " , " << img << " ) " << std::endl;
}
friend Complex operator+(const Complex& a, const Complex& b);
};
Complex operator+(const Complex& a, const Complex& b) {
Complex temp(a.real + b.real, a.img + b.img);
return temp;
}
Complex::Complex(const char* str) {
// 입력 받은 문자열을 분석하여 real 부분과 img 부분을 찾아야 한다.
// 문자열의 꼴은 다음과 같습니다 "[부호](실수부)(부호)i(허수부)"
// 이 때 맨 앞의 부호는 생략 가능합니다. (생략시 + 라 가정)
int begin = 0, end = strlen(str);
img = 0.0;
real = 0.0;
// 먼저 가장 기준이 되는 'i' 의 위치를 찾는다.
int pos_i = -1;
for (int i = 0; i != end; i++) {
if (str[i] == 'i') {
pos_i = i;
break;
}
}
// 만일 'i' 가 없다면 이 수는 실수 뿐이다.
if (pos_i == -1) {
real = get_number(str, begin, end - 1);
return;
}
// 만일 'i' 가 있다면, 실수부와 허수부를 나누어서 처리하면 된다.
real = get_number(str, begin, pos_i - 1);
img = get_number(str, pos_i + 1, end - 1);
if (pos_i >= 1 && str[pos_i - 1] == '-') img *= -1.0;
}
double Complex::get_number(const char* str, int from, int to) const {
bool minus = false;
if (from > to) return 0;
if (str[from] == '-') minus = true;
if (str[from] == '-' || str[from] == '+') from++;
double num = 0.0;
double decimal = 1.0;
bool integer_part = true;
for (int i = from; i <= to; i++) {
if (isdigit(str[i]) && integer_part) {
num *= 10.0;
num += (str[i] - '0');
} else if (str[i] == '.')
integer_part = false;
else if (isdigit(str[i]) && !integer_part) {
decimal /= 10.0;
num += ((str[i] - '0') * decimal);
} else
break; // 그 이외의 이상한 문자들이 올 경우
}
if (minus) num *= -1.0;
return num;
}
Complex& Complex::operator=(const Complex& c) {
real = c.real;
img = c.img;
return *this;
}
int main() {
Complex a(0, 0);
a = "-1.1 + i3.923" + a;
a = a + a;
Complex b(1, 2);
b = a + b;
b.println();
}
입출력 연산자 오버로딩
아래 코드는 std::cout.operator<<(a) 와 같은 의미이다.
std::cout << a;
실제로 우리가 include 하는 iostream 의 헤더파일을 살펴보면 ostream 클래스에 다음과 같이 엄청난 수의 operator<< 가 정의되어 있는 것을 알 수 있다.
하지만 위 operator<< 는 문제가 있는데, 해당 operator<< 에서 c.real, c.img 에 접근할 수 없다는 점이다.
따라서 위 operator<< 를 friend 로 지정하면 된다.
완성된 코드는 다음과 같다.
무분별한 friend 키워드는 지양하자
물론 무분별하게 friend 키워드를 남발하는 것은 썩 권장하지 않는다.
왜냐하면 friend 키워드는 해당 함수나 클래스에게 자기 자신의 모든 private 멤버 함수와 변수들을 공개하기 때문이다.
따라서 구현 디테일은 최대한 숨기라 는 원칙을 지키기가 힘들어진다.
물론 절대 friend 를 쓰지 말라는 것은 아니고, 테스트 코드 작성과 같이 friend 를 유용하게 사용할 수 있는 경우가 종종 있다.
#include <iostream>
#include <cstring>
class Complex {
private:
double real, img;
double get_number(const char* str, int from, int to) const;
public:
Complex(double real, double img) : real(real), img(img) {}
Complex(const Complex& c) { real = c.real, img = c.img; }
Complex(const char* str);
Complex& operator+=(const Complex& c);
Complex& operator=(const Complex& c);
// 나머지 연산자 함수들은 생략 :)
friend std::ostream& operator<<(std::ostream& os, const Complex& c);
friend Complex operator+(const Complex& a, const Complex& b);
};
std::ostream& operator<<(std::ostream& os, const Complex& c) {
os << "( " << c.real << " , " << c.img << " ) ";
return os;
}
Complex operator+(const Complex& a, const Complex& b) {
Complex temp(a.real + b.real, a.img + b.img);
return temp;
}
Complex::Complex(const char* str) {
// 입력 받은 문자열을 분석하여 real 부분과 img 부분을 찾아야 한다.
// 문자열의 꼴은 다음과 같습니다 "[부호](실수부)(부호)i(허수부)"
// 이 때 맨 앞의 부호는 생략 가능합니다. (생략시 + 라 가정)
int begin = 0, end = strlen(str);
img = 0.0;
real = 0.0;
// 먼저 가장 기준이 되는 'i' 의 위치를 찾는다.
int pos_i = -1;
for (int i = 0; i != end; i++) {
if (str[i] == 'i') {
pos_i = i;
break;
}
}
// 만일 'i' 가 없다면 이 수는 실수 뿐이다.
if (pos_i == -1) {
real = get_number(str, begin, end - 1);
return;
}
// 만일 'i' 가 있다면, 실수부와 허수부를 나누어서 처리하면 된다.
real = get_number(str, begin, pos_i - 1);
img = get_number(str, pos_i + 1, end - 1);
if (pos_i >= 1 && str[pos_i - 1] == '-') img *= -1.0;
}
double Complex::get_number(const char* str, int from, int to) const {
bool minus = false;
if (from > to) return 0;
if (str[from] == '-') minus = true;
if (str[from] == '-' || str[from] == '+') from++;
double num = 0.0;
double decimal = 1.0;
bool integer_part = true;
for (int i = from; i <= to; i++) {
if (isdigit(str[i]) && integer_part) {
num *= 10.0;
num += (str[i] - '0');
} else if (str[i] == '.')
integer_part = false;
else if (isdigit(str[i]) && !integer_part) {
decimal /= 10.0;
num += ((str[i] - '0') * decimal);
} else
break; // 그 이외의 이상한 문자들이 올 경우
}
if (minus) num *= -1.0;
return num;
}
Complex& Complex::operator+=(const Complex& c) {
(*this) = *this + c;
return *this;
}
Complex& Complex::operator=(const Complex& c) {
real = c.real;
img = c.img;
return *this;
}
int main() {
Complex a(0, 0);
a = "-1.1 + i3.923" + a;
std::cout << "a 의 값은 : " << a << " 이다. " << std::endl;
}
타입변환 연산자 오버로딩 - Wrapper 클래스
Wrapper 클래스는 기본 자료형들을 클래스로 포장해 각각의 자료형을 개체로 사용하는 것을 Wrapper 클래스라고 부른다.
int 자료형을 감싸는 int Wrapper 클래스 Int 는 다음과 같이 구성할 수 있다.
class Int
{
int data;
public:
Int(int data) : data(data) {}
Int(const Int& i) : data(i.data) {}
}
위 Wrapper 클래스에서 아래 코드가 작성하기 위해서 int 자료형에서 사용되는 모든 연산자 함수를 만들어주면 된다고 생각할 수 있다.
-> 하지만 수많은 연산자 오버로딩을 하는 것은 매우 괴롭다..
Int x = 3;
int a = x + 4;
그렇다면 그냥 이 Wrapper 클래스 개체를 마치 int 형 변수라고 컴파일러가 생각하게 하는 방법이 있을까?
물론 있다!
-> 왜냐면 우리에게는 타입 변환 연산자가 있기 때문이다.
타입 변환 연산자는 다음과 같이 정의한다.
예를 들어서 우리의 Wrapper 클래스의 경우 다음과 같은 타입 변환 연산자를 정의할 수 있다.
operator int( return data;)
한가지 주의할 점은, 함수의 리턴 타입을 사용하면 안된다!
-> 이는 C++ 에서 변환 연산자를 정의하기 위한 규칙이다.
아래와 같이 코드를 완성할 수 있다.
-> 컴파일러 입장에서 적절한 변환 연산자로 operator int 를 찾아서 결국 int 로 변환해서 처리하게 된다.
#include <iostream>
class Int {
int data;
// some other data
public:
Int(int data) : data(data) {}
Int(const Int& i) : data(i.data) {}
operator int() { return data; }
};
int main() {
Int x = 3;
int a = x + 4;
x = a * 2 + x + 4;
std::cout << x << std::endl;
}
증감 연산자 오버로딩
증감 연산자는 전위와 후위로 나누어지는데, 연산자 오버로딩 시 어떻게 구분할까?
전위 증감 연산자 오버로딩
operator++();
operator--();
후위 증감 연산자 오버로딩
여기서 매개변수 x 는 아무런 의미가 없다. 단순히 컴파일러가 전위와 후위를 구별하기 위해서 int 인자를 넣어주는 것 뿐이다.
operator++(int x);
operator--(int x);
주의할 점
여기서 주의할 점은 전위 증감 연산의 경우 값이 바뀐 자신을 리턴해야 하고,
후위 증감 연산의 경우 값이 바뀌기 이전의 개체를 리턴해야 한다.
증감 연산자 오버로딩의 예제는 다음과 같다.
#include <iostream>
class Test {
int x;
public:
Test(int x) : x(x) {}
Test(const Test& t) : x(t.x) {}
Test& operator++() {
x++;
std::cout << "전위 증감 연산자" << std::endl;
return *this;
}
// 전위 증감과 후위 증감에 차이를 두기 위해 후위 증감의 경우 인자로 int 를
// 받지만 실제로는 아무것도 전달되지 않는다.
Test operator++(int) {
Test temp(*this);
x++;
std::cout << "후위 증감 연산자" << std::endl;
return temp;
}
int get_x() const {
return x;
}
};
void func(const Test& t) {
std::cout << "x : " << t.get_x() << std::endl;
}
int main() {
Test t(3);
func(++t); // 4
func(t++); // 4 가 출력됨
std::cout << "x : " << t.get_x() << std::endl;
}