목차
- C++ STL(Standard Template Library)에 대해 배워보자
- Chap02 : 함수 포인터
- Chap03 : 함수 객체
- Chap04 : 템플릿
- Chap05 : STL 소개
- Chap06 : 시퀀스 컨테이너
- Chap07 : 연관 컨테이너
- Chap08 : 알고리즘
- Chap09 : STL 함수 객체
- Chap10 : 반복자
- Chap11 : 컨테이너 어댑터
- Chap12 : String 컨테이너
- 참고하면 좋은 내용들
해당 글은
공동환 저자뇌를 자극하는 C++ STL도서 기반으로 작성되었습니다.
Github 코드
추가로 긴 글을 일일히 읽기 어려우신 분들은 깃허브 코드만 보시는걸 추천합니다.
물론 파일 하나하나 설명되어 있는 md 파일은 없지만(이건 곧 추가 하도록 하겠습니다 ㅎㅎ)
코드에 있는 주석에 설명에 관한 요약된 내용이 있습니다.
해당 코드는 다음과 같은 환경에서 실행되었습니다.
개발 환경 : M2 Macbook Pro 14,Sonoma 14.0
IDE :CLion
Language Standard Version :C++17
들어가며
드디어 STL에 대한 첫 포스팅입니다! 아직 서투른게 많지만, 배운 내용 하나하나 차곡차곡 정리하며 포스팅하려 합니다. Chap01 부터 Chap04 까지 STL을 배우기에 앞서 STL에 꼭 필요한 문법에 대해서 배울 예정입니다. Chap05부터 본격적으로 STL에 대해 배워볼건데요. 만약 이 내용들을 다 알고계신다면 Skip 하셔도 무방합니다. 그럼 이번 챕터에서는 연산자 오버로딩과 그 종류에 대해 알아보도록 하겠습니다. 레츠고!! 😊😊
중요 포인트는요!
지금과 같이
노란색 박스에는주의사항이초록색 박스에는팁 / 참고사항이 있으니
꼭 읽어보시길 권합니다!중요한 내용이 담겨 있습니다!!
01. 연산자 오버로딩이란
연산자 오버로딩(다중 정의) 은 C++에서 제공하는 기본 타입이 아닌 클래스 타입, 즉 사용자 정의 타입에도 연산자를 사용할 수 있게 하는 문법입니다.
예를 들어
1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
using namespace std;
int main()
{
int n1(10);
int n2(20);
cout << n1 + n2 << endl;
return 0;
}
위 코드는 기본적으로 컴파일러에 정수의 + 연산이 정의되어 있기 때문에 가능한 연산입니다.
하지만, 아래와 같이 사용자가 정의한 Point 클래스의 연산은 불가능합니다. 컴파일러 내부에 Point 객체에 대한 연산이 정의되어 있지 않기 때문이죠.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>
using namespace std;
class Point
{
int x, y;
public:
Point(int _x = 0, int _y = 0)
: x(_x), y(_y)
{
}
void Print() const
{
cout << x << ',' << y << endl;
}
};
int main()
{
Point p1(2, 3);
Point p2(5, 5);
p1 + p2; // 컴파일러 내부에 정의되지 않은 타입의 연산이 있음. Error!!
return 0;
}
만약 위 코드를 실행하게 되면 다음과 같은 오류가 나옵니다.
error: invalid operands to binary expression (‘Point’ and ‘Point’)
Point 객체 p1과 p2 를 더하는것이 이진 식에 대한 잘못된 피연산자 즉, 정의되지 않은 연산이므로 오류가 발생합니다.
이럴 때 사용할 수 있는것이 바로 연산자 오버로딩 입니다.
우리가 클래스를 만들때, 지원 가능한 연산자들을 만들어서 넣어준다면 사용자는 훨씬 더 간결한 방법으로 코드를 작성할 수 있게되고, 코드의 확장성 또한 높아집니다.
하지만, 연산자 오버로딩을 사용할 시 명심해야할 사실은
연산자 함수는 절대 오류가 발생하면 안됩니다.
- 함수는 반환값을 보고 문제를 확인할 수 있지만, 연산자는 결과를 확인하는 일이 없습니다. 가령 3 + 4라는 연산이 실패할 가능성을 생각한다거나 7이 되지 않는 경우를 고려하지 않는다는 것입니다.
- 또한, 사용자 코드를 너무 간결하게 작성할수 있도록 해준 덕분에 사용자 코드의 문제점을 찾기가 훨씬 더 어려울 수 있습니다.
절대로 논리 연산자들을 다중 정의해서는 안됩니다.
- 심각한 논리적 오류를 발생시킬 수 있습니다.
"모호성"을 항상 고려해야 합니다.
- 연산자 함수 뿐만아니라 모든 함수의 오버로딩(다중 정의)을 사용할때에는 항상 “꼭!”
"모호성"에 대해 고려하여 코드를 짜야 합니다.- 함수가 오버로딩(다중 정의) 되는 것은 문법적으로 문제가 되진 않습니다. 즉, 함수 제작자는 오류를 경험하지 않겠지만, 사용자 입장에선 오류를 경험할 수 있게 되는 것입니다.
02. 연산자 오버로딩 정의 및 사용하기
연산자 오버로딩의 핵심은 클래스 타입 즉, 사용자 정의 타입의 객체에 연산자를 사용하면 컴파일러가 정의된 연산자 함수를 호출하는 데 있습니다.
위에서 얘기한 Point 객체에 대한 연산을 예제로 들어보겠습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <iostream>
using namespace std;
class Point
{
int x, y;
public:
Point(int _x = 0, int _y = 0)
: x(_x), y(_y)
{
}
void Print() const
{
cout << x << ',' << y << endl;
}
void operator+(Point arg)
{
cout << "operator+() 함수 호출" << endl;
}
};
int main()
{
Point p1(2, 3), p2(5, 5);
p1 + p2; // p1.operator+(p2) 와 같다. operator+() 호출!
p1 * p2; // p1.operator*(p2) 와 같다. 정의되지 않은 연산 Error!
p1 = p2; // p1.operator=(p2) 와 같다. 정의되지 않은 연산 Error!
return 0;
}
위 예제의 코드를 컴파일러는 주석에 쓴 것처럼 해석합니다.
p1 + p2 는 p1.operator+(p2) 라고 호출하므로 p1 객체를 기준으로 멤버 함수 operator+()를 호출하고, 함수의 인자로 p2 를 전달합니다. 즉, Point 클래스에 멤버 함수 operator+()가 정의되어 있으면 호출이 가능하고, 없으면 컴파일러 에러가 발생합니다.
Point 객체 p1 과 p2를 더한다는 것은 p1의 x와 p2의 x를 더하고, p1의 y와 p2의 y를 더한다는 의미이고, 연산자 함수가 두 객체를 더한 객체를 반환해야 하므로 반환 타입을 사용하여 다음과 같이 구현할 수 있습니다.
다음은 Point 객체의 + 연산 오버로딩 예제입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// https://github.com/orion-gz/CPP-STL-Programming/blob/main/Chap01/Operator1.cpp
// 연산자 오버로딩
// 기본 타입이 아닌 클래스 타입, 즉 사용자 정의 타입에도 연산자를 사용할 수 있게 하는 문법.
// 연산자 오버로딩을 사용하면 컴파일러 내부에 정의되지 않은 타입의 연산이 가능하여 코드의 '직관성'과 '가독성'을 좋게 할 수 있음.
// 핵심은 클래스 타입의 객체에 연산자를 사용하면 컴파일러가 정의된 함수를 호출하는 데 있다.
#include <iostream>
using namespace std;
class Point
{
int x, y;
public:
Point(int _x = 0, int _y = 0)
: x(_x), y(_y)
{
}
void Print() const
{
cout << x << ',' << y << endl;
}
// 값을 반환해야 하므로 상수형 객체를 반환함 (const 선언으로 반환되는 값이 수정하는 것을 방지함)
// 함수 내에서 멤버 변수를 변경하지 않으므로 const 상수형 메서드 선언
const Point operator+(const Point& arg) const
{
Point pt;
pt.x = this->x + arg.x;
pt.y = this->y + arg.y;
return pt;
}
};
int main()
{
Point p1(2, 3), p2(5, 5);
Point p3;
p3 = p1 + p2; // 컴파일러가 p1.operator+(p2)로 해석해서 호출
p3.Print();
p3 = p1.operator+(p2); // 직접 호출
p3.Print();
return 0;
}
[출력 결과]
7, 8
7, 8
위 코드에서 우리는 Point 객체의 연산을 해야합니다. 앞서 말했듯이 p1 + p2 를 컴파일러는 p1.operator+(p2) 로 해석합니다.
즉, p1 객체의 멤버 함수인 operator+() 함수를 호출하고, p2를 인자로 전달하게 됩니다.
연산자 함수 내부에선 p1 객체 자신인 this 의 x, y 값인 this->x, this->y 와 매개변수(p2)인 arg 의 x, y 값인 arg.x, arg.y 의 값을 각각 더해주어 새로운 객체 pt 에 저장하게 됩니다.
값이 더해진 pt 가 반환되고 나면 비로소 p1 + p2 의 연산이 완료되는 것입니다.
참고 - 상수형 객체, 메서드
위 코드에서 상수형 메서드를 사용했는데요. C++에서 객체, 메서드의 상수화는 매우 중요한 부분이기도하고 글이 길어지기 때문에 따로 짚고 넘어가겠습니다.꼭 한번씩은 읽어보시길 바랍니다.
03. 단항 연산자 오버로딩
단항 연산자 중 오버로딩이 가능한 연산자는 다음과 같습니다.
| 연산자 기호 | 연산자 종류 |
|---|---|
| ! | 논리 연산자 |
| & | 주소 연산자 |
| ~ | 비트(보수) 연산자 |
| * | 간접 참조 연산자 |
| +, - | 부호 연산자 |
| ++, -- | 증감 연산자 |
| type | 형변환 연산자 |
증감(++, –) 연산자 오버로딩
단항 연산자 중에서 간단하게 증감 연산자의 오버로딩만 다루어보겠습니다.
++ 연산자는 전위 ++ 연산자와 후위 ++ 연산자가 있으며, – 연산자 역시 전위 -- 연산자와 후위 -- 연산자로 나뉘어 집니다.
증감 연산자는 전위, 후위 연산자를 구분하기 위해 후위 연산자는 operator++() 함수 호출시 의미 없는 dummy 정수형 인자 0을 전달합니다.
- operator++() 전위 ++ 연산자
- operator++(int) 후위 ++ 연산자
- operator--() 전위 -- 연산자
- operator--(int) 후위 -- 연산자
참고 - 증가 및 감소 연산자 오버로드
증분 또는 감소 연산자의 후위 형식에 대해 오버로드된 연산자를 지정하는 경우 추가 인수는형식 int이어야 합니다.다른 형식을 지정하면 오류가 발생합니다.
다음은 증감 연산자의 오버로딩 예제입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
// https://github.com/orion-gz/CPP-STL-Programming/blob/main/Chap01/Operator2.cpp
// 단항 연산자 오버로딩
// ++ 연산자 오버로딩
// 전위 ++ 연산자 (operator++())
// 후위 ++ 연산자 (operator++(int))
// 전위 ++ 연산자와 후위 ++ 연산자의 구분을 위해 후위 ++ 연산자는 operator 함수 호출시 dummy 정수형 인자 0을 전달함.
// -- 연산자 오버로딩도 ++ 연산자 오버로딩과 방법이 같음.
#include <iostream>
using namespace std;
class Point
{
int x, y;
public:
Point(int _x = 0, int _y = 0)
: x(_x), y(_y)
{
}
void Print() const
{
cout << x << ',' << y << endl;
}
const Point& operator++() // 전위 ++
{
++x;
++y;
return *this;
}
const Point operator++(int) // 후위 ++
{
Point pt(x, y);
++x; // 내부 구현이므로 멤버 변수는 전위 ++ 연산을 사용해도 무방하다.
++y; // 내부 구현이므로 멤버 변수는 전위 ++ 연산을 사용해도 무방하다.
return pt;
}
const Point& operator--() // 전위 --
{
--x;
--y;
return *this;
}
const Point& operator--(int) // 후위 --
{
Point pt(x, y);
--x; // 내부 구현이므로 멤버 변수는 전위 ++ 연산을 사용해도 무방하다.
--y; // 내부 구현이므로 멤버 변수는 전위 ++ 연산을 사용해도 무방하다.
return pt;
}
};
int main()
{
Point p1(2, 3), p2(2, 3);
Point result;
result = ++p1; // p1.operator++() 와 같다.
p1.Print();
result.Print();
result = p2++; // p1.operator++(0) 와 같다.
p2.Print();
result.Print();
result = --p1; // p1.operator--() 와 같다.
p1.Print();
result.Print();
result = p2--; // p1.operator--(0) 와 같다.
p2.Print();
result.Print();
}
[출력 결과]
3,4
3,4
3,4
2,3
2,3
2,3
2,3
3,4
앞서 구현했던 Point 객체의 + 연산과 비슷하지만, 연산자의 종류가 단항 연산자 라는 것에서 조금 다릅니다.
단항 연산자의 경우 하나의 피연산자로 연산이 진행되기 때문에 연산자 함수 호출시 전달되는 인자(여기서는 객체)가 없습니다. 단, 증감 연산자의 경우 전위, 후위를 구분하기 위해 후위의 경우 dummy 정수형 인자 0을 전달합니다.
또한, 전위 연산자인지, 후위 연산자인지에 따라 연산자 함수 구현 방법도 다릅니다.
전위 연산자 함수의 경우 단순히 멤버 변수의 전위 연산을 진행하고 객체 자신을 반환하는 반면,
후위 연산자 함수의 경우 Point 객체 pt 를 이용해 먼저 증감 되기 전의 값을 반환하고 증감 연산을 진행합니다.
04. 이항 연산자 오버로딩
이항 연산자 중 오버로딩이 가능한 연산자는 다음과 같습니다.
| 연산자 기호 | 연산자 종류 |
|---|---|
| +, -, *, / | 산술 연산자 |
| ==, !=, <, <=, >, >= | 비교 연산자 |
| =, +=, -=, *=, /=, %=, &=, |=, ^= | 대입 또는 복합 대입 연산자 |
| &&, ||, ^ | 논리 연산자 |
| &, | | 비트 연산자 |
| », »=, «, «= | 쉬프트 연산자 |
참고 - 이항 연산자 오버로드
이항 연산자의 반환 형식에 대한 제한은 없지만 대부분의 사용자 정의 이항 연산자는 클래스 형식이나 클래스 형식에 대한 참조를 반환합니다.
비교(==, !=) 연산자 오버로딩
== 연산자도 위에서 배웠던 + 연산자와 비슷하게 오버로딩 합니다.
== 연산자는 비교 연산자로 true 혹은 false로 결과가 반환되는 bool 타입 입니다.
!= 연산자는 == 연산자의 부정 즉, !(==) 이므로 == 연산자를 이용해 쉽게 구할 수 있습니다.
다음은 == 연산자와 != 연산자의 오버로딩 예제입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// https://github.com/orion-gz/CPP-STL-Programming/blob/main/Chap01/Operator3.cpp
// 이항 연산자 오버로딩
// == 연산자 오버로딩
// 비교연산으로 true 혹은 false 결과가 반환되는 bool 타입이다.
// != 연산자 오버로딩
// == 연산자의 부정으로 == 연산의 결과에 not 연산을 하면 된다.
#include <iostream>
using namespace std;
class Point
{
int x, y;
public:
Point(int _x = 0, int _y = 0)
: x(_x), y(_y)
{
}
void Print() const
{
cout << x << ',' << y << endl;
}
bool operator==(const Point& arg) const
{
return x == arg.x && y == arg.y ? true : false;
}
bool operator!=(const Point& arg) const
{
return !(*this == arg);
}
};
int main()
{
Point p1(2, 3), p2(5, 5), p3(2, 3);
if (p1 != p2) // p1.operator!=(p2) 와 같다.
cout << "p1 != p2" << endl;
else if (p1 == p2) // p1.operator==(p2) 와 같다.
cout << "p1 == p2" << endl;
if (p1 != p3) // p1.operator!=(p3) 와 같다.
cout << "p1 != p3" << endl;
else if (p1 == p3) // p1.operator==(p3) 와 같다.
cout << "p1 == p3" << endl;
return 0;
}
[출력 결과]
p1 != p2
p1 == p3
비교 연산자 역시 앞서 다뤘던 내용인 + 연산 동일하게 이항 연산자로 비슷한 방식으로 구현할 수 있습니다.
05. 전역 함수를 이용한 연산자 오버로딩
연산자 오버로딩에는 다음 두 가지가 있습니다.
멤버 함수를 이용한 연산자 오버로딩전역 함수를 이용한 연산자 오버로딩
일반적으로 멤버 함수를 이용한 연산자 오버로딩(위에서 배웠던 내용입니다)을 사용하지만, 멤버 함수를 이용한 연산자 오버로딩을 사용할 수 없을 때는 전역 함수 연산자 오버로딩을 사용합니다.
그럼 멤버 함수를 이용한 연산자 오버로딩을 사용할 수 없을 경우에 대해 알아보겠습니다.
멤버 함수를 이용한 연산자 오버로딩을 사용할 수 없는 경우
이항 연산의 왼쪽 객체를 기준으로 연산자 오버로딩 멤버 함수를 호출하기 때문에 왼쪽 항이 연산자 오버로딩 객체가 "아니면" 멤버 함수를 이용한 연산자 오버로딩을 이용할 수 없습니다.
예를 들어
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Point
{
const Point operator+(const Point& arg) const
{
...
}
};
int main()
{
Point p1(1, 1), p2(3, 5);
p1 + p2; // p1.operator+(p2);
return 0;
}
위 코드는 p1을 this 객체로 멤버 함수를 p1.operator+(p2) 와 같이 호출합니다.
즉, 왼쪽 항 p1이 + 연산이 오버로딩된 객체이므로 오버로딩된 연산자 함수 호출이 가능 합니다.
또 다른 예를 들어보겠습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Point
{
const Point operator+(const Point& arg) const
{
...
}
};
int main()
{
Point p1(1, 2);
k + p1; // k 는 연산자 오버로딩 객체가 아니므로 k.operator+(p1) 처럼 호출 불가
// 전역 함수를 이용한 연산자 오버로딩을 사용해야 함.
return 0;
}
왼쪽 항 k는 + 연산이 오버로딩되지 않은 객체이므로 오버로딩된 연산자 함수 호출이 불가능 합니다.
연산자 오버로딩은 컴파일러가 p1 == p2 와 같은 코드를 두 가지로 해석합니다.
멤버 함수로p1.operator==(p2)처럼 해석하며, 이는p1의 operator==() 멤버 함수를 호출해p2 를 인자로 전달합니다.
전역 함수로operator==(p1, p2)처럼 해석하며, 이는전역 함수 operator==()의인자로 p1과 p2 객체를 각각 전달합니다.
전역 함수를 이용한 연산자 오버로딩
전역 함수를 이용한 연산자 오버로딩은 다음과 같이 나타낼 수 있습니다.
[return_type] operator [op] (arg1, arg2) { }
return_type 은 반환 형식이고, op는 연산자 중 하나이며 arg1 arg2는 함수 매개변수입니다.
다음은 전역 함수를 이용한 연산자 오버로딩에 대한 예제입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// https://github.com/orion-gz/CPP-STL-Programming/blob/main/Chap01/Operator4.cpp
// 멤버 함수를 이용한 연산자 오버로딩
// p1.operator==(p2);로 해석하며, p1의 operator==() 멤버 함수를 호출해 p2를 인자로 전달함.
// 전역 함수를 이용한 연산자 오버로딩
// 이항 연산의 왼쪽 항이 연산자 오버로딩 객체가 아니면 멤버 함수를 이용한 연산자 오버로딩을 이용할 수 없다. (왼쪽 객체 기준으로 오버로딩 함수 호출하기 때문)
// 전역 함수 연산자 오버로딩을 사용해야 함.
// operator==(p1, p2);로 해석하며, 전역 함수 operator==()의 인자로 p1 와 p2 객체를 각각 전달함.
#include <iostream>
using namespace std;
class Point
{
int x, y;
public:
Point(int _x = 0, int _y = 0)
: x(_x), y(_y)
{
}
void Print() const
{
cout << x << ',' << y << endl;
}
int GetX() const
{
return x;
}
int GetY() const
{
return y;
}
// 멤버 함수를 이용한 연산자 오버로딩
// 매개변수로 p2의 전달 인자를 받음.
// const Point operator-(const Point& arg) const
// {
// return Point(x - arg.x, y - arg.y);
// }
};
// 전역 함수를 이용한 연산자 오버로딩
// 매개변수로 p1, p2의 전달 인자를 받음.
// 전역 함수를 이용하면, 클래스의 Private 멤버인 x, y에 접근 불가능하므로 getter를 이용하거나 프렌드 함수를 사용함.
const Point operator-(const Point& argL, const Point& argR)
{
return Point(argL.GetX() - argR.GetX(), argL.GetY() - argR.GetY());
}
int main()
{
Point p1(2, 3), p2(5, 5);
Point p3;
p3 = p1 - p2;
p3.Print();
return 0;
}
[출력 결과]
-3,-2
여기서 주의해야 할 점은
Point 클래스에 주석처리 되어 있는 부분은 멤버 함수를 이용한 연산자 오버로딩입니다. 주석처리를 해제한다면 전역 함수를 이용한 연산자 오버로딩과의
"모호성"이 발생하게 됩니다. 즉, 컴파일러는p1 - p2를p1.operator-(p2)와operator(p1, p2)중 어떤 함수를 호출하는 것인지 판단 할 수 없기 때문에 컴파일 오류가 발생하게 됩니다.
거듭 강조하지만, "모호성"은 항상 신경써야 하는 부분입니다.
위 코드에서 p1 - p2 는 전역함수 operator-(p1, p2)를 호출합니다. Point 객체 p1과 p2는 각각 인자로 전달됩니다.
멤버 함수 operator-(p2) 와 비슷하지만, 전역함수이므로 멤버 변수 접근이 불가능해 getter로 값을 받아와 연산을 진행합니다.
06. STL에 필요한 주요 연산자 오버로딩
함수 호출 연산자 오버로딩(() 연산자)
함수 호출 연산자 오버로딩은 객체를 함수처럼 동작하게 하는 연산자입니다. Print(10) 이라는 함수 호출 문장은 다음 세 가지로 해석할 수 있습니다.
함수 호출 : Print가 함수 이름인 경우함수 포인터 : Print가 함수 포인터인 경우함수 객체 : Print가 함수 객체인 경우
여기서 함수 호출 연산자를 정의한 객체를 함수 객체 라고 합니다.
함수 포인터와 함수 객체는 위 링크를 참고해 주세요. 😊
다음은 위 세가지 해석을 코드로 나타낸 예제입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// https://github.com/orion-gz/CPP-STL-Programming/blob/main/Chap01/Operator5.cpp
#include <iostream>
using namespace std;
struct FuncObject
{
public:
// () 연산자 오버로딩
void operator()(int arg) const
{
cout << "정수 : " << arg << endl;
}
};
void Print1(int arg)
{
cout << "정수 : " << arg << endl;
}
int main()
{
void (*Print2)(int) = Print1; // 함수 포인터 선언 및 정의
FuncObject Print3; // 함수 객체 선언
Print1(10); // 첫째, '함수'를 사용한 정수 출력
Print2(10); // 둘째, '함수 포인터'를 사용한 정수 출력
Print3(10); // 셋째, '함수 객체'를 사용한 정수 출력
// Print3.operator(10) 와 같음
return 0;
}
[출력 결과]
정수 : 10
정수 : 10
정수 : 10
참고 - 클래스 및 구조체
C++의 struct 키워드는 class 키워드와 같습니다. 유일하게 다른 점은 멤버 접근 한정자를 지정하지 않으면struct는 public이 기본 속성이 되고,class는 private가 기본 속성이 됩니다.
배열 인덱스 연산자 오버로딩([] 연산자)
배열 인덱스 연산자 오버로딩을 사용하면 배열에 사용하는 [] 연산자를 객체에도 사용할 수 있습니다. [] 연산자 오버로딩은 일반적으로 많은 객체를 저장하고 관리하는 객체에 사용됩니다.
다음은 배열 인덱스 연산자 오버로딩의 예제입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// https://github.com/orion-gz/CPP-STL-Programming/blob/main/Chap01/Operator7.cpp
// 배열 인덱스 연산자 오버로딩
// 일반적으로 많은 객체를 저장하고 관리하는 객체(컨테이너 객체)에 사용된다.
#include <iostream>
using namespace std;
class Array
{
int *arr;
int size;
int capacity;
// 복사생성자, 복사대입연산자 생략
public:
Array(int cap = 100)
: arr(0), size(0), capacity(cap)
{
arr = new int[capacity];
}
~Array()
{
delete [] arr;
}
void Add(int data)
{
if (size < capacity)
arr[size++] = data;
}
int Size() const
{
return size;
}
// operator[] 함수는 쓰기 연산도 제공해야하므로 아래와 같이 const 메서드, 비 const 메서드 모두 제공해야함.
// const 메서드
int operator[](int idx) const
{
return arr[idx];
}
// 비 const 메서드
int& operator[](int idx)
{
return arr[idx];
}
};
int main()
{
Array ar(10);
ar.Add(10);
ar.Add(20);
ar.Add(30);
cout << ar[0] << endl; // ar.operator[](int) 호출
cout << endl;
const Array& ar2 = ar; // 상수형 객체에 복사 생성
cout << ar2[0] << endl; // ar2.operator[](int) const 호출
cout << endl;
ar[0] = 100; // ar.operator[](int) 호출
// 상수 객체를 반환하므로 값을 대입할 수 없음.
// ar2[0] = 100;
return 0;
}
[출력 결과]
1010
위 코드에선 Array 객체 ar의 생성자를 호출하면서 크기가 10인 배열을 생성하게 됩니다. 이후, Add() 함수를 통해 값을 저장하고,Operator[]()를 통해 인덱스에 접근합니다.
operator 함수는 읽기, 쓰기 연산이 모두 가능한 비 const 객체(ar)에 사용되며,
operator const 함수는 읽기 연산만 가능한 const 객체(ar2)에 사용됩니다.
메모리 접근, 클래스 멤버 접근 연산자 오버로딩(*, -> 연산자)
*, -> 연산자는 스마트 포인터나 반복자 등의 특수한 객체에 사용됩니다. 반복자가 STL의 핵심 구성 요소이므로 *, -> 연산자 오버로딩이 매우 중요합니다.
다음은 *, -> 연산자의 오버로딩에 대한 예제입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
// https://github.com/orion-gz/CPP-STL-Programming/blob/main/Chap01/Operator8.cpp
// 메모리 접근, 클래스 멤버 접근 연산자 오버로딩
// *, -> 연산자는 스마트 포인터나 반복자 등의 특수한 개체에 사용되는데, 반복자가 STL의 핵심 구성 요소이므로 *, -> 연산자 오버로딩이 아주 중요하다.
#include <iostream>
using namespace std;
class Point
{
int x;
int y;
public:
Point(int _x = 0, int _y = 0)
: x(_x), y(_y)
{
}
void Print() const
{
cout << x << ',' << y << endl;
}
};
class PointPtr
{
Point *ptr;
public:
// PointPtr 클래스 객체 생성시 초기화 리스트를 통해 Point 객체 초기화
// Point 클래스의 생성자가 호출됨
PointPtr(Point *p)
: ptr(p)
{
}
~PointPtr()
{
delete ptr;
}
// 클래스 멤버 접근 연산자 오버로딩
// 객체의 내부에 보관된 실제 포인터인 'ptr'을 반환한다.
Point* operator->() const
{
return ptr;
}
// 메모리 접근 연산자 오버로딩
// 포인터가 가리키는 객체 자체를 반환
Point& operator*() const
{
return *ptr;
}
};
int main()
{
Point* p1 = new Point(2, 3); // 일반 포인터
PointPtr p2 = new Point(5, 5); // 스마트 포인터
p1->Print(); // 멤버 접근 연산자 '->'로 함수 호출
// Point 클래스의 멤버에 접근할 수 있도록 '->' 연산자 오버로딩
// p2.operator->()->Print() 와 같음
// 반환받은 포인터를 이용해 Print() 함수 호출
p2->Print();
cout << endl;
(*p1).Print(); // (*p1).Print() 호충
(*p2).Print(); // p2.operator*().Print() 와 같음
// 반환받은 객체 자체로 Print() 함수 호출
delete p1; // 일반 포인터인 p1 객체는 메모리 해제를 해줘야함
// 스마트 포인터인 p2 객체는 소멸자를 통해 자동으로 메모리에서 제거된다.
return 0;
}
p1은 일반 포인터로 *p1 연산이 객체 자체이므로 (*p1).Print()와 같이 멤버 함수를 호출합니다.
p2는 스마트 포인터로 *p2 연산이 객체를 반환하게 p2.operator*()를 호출하고, 객체 참조를 받아 p1.operator*().Print()처럼 함수를 호출합니다.
07. 타입 변환 연산자 오버로딩
사용자가 직접 정의해서 사용할 수 있는 타입 변환은 두 가지가 있습니다.
생성자를 이용한 타입 변환타입 변환 연산자 오버로딩을 이용한 타입 변환
먼저 생성자를 이용한 타입 변환에 대해서 알아보겠습니다.
생성자를 이용한 타입 변환
특정 타입을 인자로 받는 생성자가 있다면, 생성자 호출을 통한 타입 변환(객체 생성 후 대입)이 가능합니다. 생성자를 이용해 다른 타입을 자신의 타입으로 변환할 수 있습니다.
- 모든 생성자와 마찬가지로
변환 생성자는 반환 형식을 지정하지 않습니다.선언에 반환 형식을 지정하는 것은 오류입니다.- 변환 대상 형식은 생성 중인 사용자 정의 형식입니다.
- 일반적으로 변환 생성자에는
단 하나의 인수만 사용됩니다. 하지만각각의 추가 매개 변수에 기본값이 있는 경우에는 변환생성자가 추가 매개 변수를 지정할 수 있습니다. 소스 형식에는 첫 번째 매개 변수의 형식이 유지됩니다.- 변환 생성자는 명시적일 수 있습니다.
다음은 생성자 타입을 설명하기 위한 예제입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// https://github.com/orion-gz/CPP-STL-Programming/blob/main/Chap01/Operator9.cpp
// 생성자를 이용한 타입 변환
// 특정 타입을 인자로 받는 생성자가 있다면, 생성자 호출을 통한 타입 변환이 가능하다.
// 단, 생성자를 이용한 형변환을 의도하지 않는다면 인자를 갖는 생성자는 명시적 호출만 가능하도록 모두 explicit 키워드를 지정하는게 좋다.
#include <iostream>
using namespace std;
class A
{
};
class B
{
public:
B()
{
cout << "B() 생성자" << endl;
}
B(A& a)
{
cout << "B(A _a) 생성자" << endl;
}
B(int n)
{
cout << "B(int n) 생성자" << endl;
}
B(double d)
{
cout << "B(double d) 생성자" << endl;
}
};
int main()
{
A a;
int n = 10;
double d = 5.5;
B b; // B() 생성자 호출
b = a; // b = B(a) 암시적 생성자 호출 후 대입
b = n; // b = B(n) 암시적 생성자 호출 후 대입
b = d; // b = B(d) 암시적 생성자 호출 후 대입
return 0;
}
[출력 결과]
B() 생성자
B(A _a) 생성자
B(int n) 생성자
B(double d) 생성자
문장 b = a에서 컴파일러는 A 타입의 객체를 B 타입으로 변환하기 위해 생성자를 확인합니다. A 타입의 객체(a)를 인자로 받는 생성자가 있으므로 이 생성자를 호출해 B 타입의 객체를 생성합니다.
참고 - explicit 키워드 및 암시적 변환 문제
생성자를 이용한 형변환을 의도하지 않는다면 생성자는 명시적 호출만 가능하도록 explicit 키워드를 지정합니다.
암시적인 생성자 형변환을 의도하지 않는 한 인자를 갖는 생성자는 모두 explicit로 만드는게 좋습니다.
타입 변환 연산자 오버로딩을 이용한 타입 변환
타입 변환 연산자 오버로딩을 사용하면 자신의 타입을 다른 타입으로 변환할 수 있습니다.
- 변환 대상 형식은 변환 함수 선언 전에 선언되어야 합니다. 클래스, 구조체, 열거형 및 typedef는 변환 함수 선언 내에서 선언할 수 없습니다.
변환 함수는 인수를 사용하지 않습니다.선언에 매개 변수를 지정하는 것은 오류입니다.- 변환 함수에는 변환 함수의 이름(변환 대상 형식의 이름이기도 함)에 의해 지정되는 반환 유형이 있습니다.
선언에 반환 형식을 지정하는 것은 오류입니다.- 변환 함수는 가상일 수 있습니다.
- 변환 함수는 명시적일 수 있습니다.
다음은 타입 변환 연산자 오버로딩의 간단한 예제입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// https://github.com/orion-gz/CPP-STL-Programming/blob/main/Chap01/Operator10.cpp
// 타입 변환 연산자 오버로딩을 이용한 타입 변환
// 타입 변환 연산자는 생성자나 소멸자처럼 반환 타입을 지정하지 않는다.
#include <iostream>
using namespace std;
class A
{
};
class B
{
public:
operator A()
{
cout << "operator A() 호출" << endl;
return A();
}
operator int()
{
cout << "operator int() 호출" << endl;
return 10;
}
operator double()
{
cout << "operator double() 호출" << endl;
return 5.5;
}
};
int main()
{
A a;
int n;
double d;
B b;
a = b; // b.operator A() 암시적 호출
n = b; // b.operator int() 암시적 호출
d = b; // b.operator double() 암시적 호출
cout << endl;
// 명시적 호출
a = b.operator A();
n = b.operator int();
d = b.operator double();
return 0;
}
[출력 결과]
operator A() 호출
operator int() 호출
operator double() 호출operator A() 호출
operator int() 호출
operator double() 호출
여기서 주의해야 할 점은
a = b와 같이 암시적인 호출을 한 경우에 지금같은 경우에는 컴파일러가 알아서 실행시켜주지만, 만약 class A에operator=() (대입 연산자 오버로딩)이 정의되어 있다면, 컴파일러는a = b와 같은 암시적인 호출이a.operator=(b)의 호출을 의미하는지a = b.operator A()를 의미하는지 전혀 알 턱이 없습니다. 즉, 코드의"모호성"이 생기게 됩니다.
거듭 강조하지만 오버로딩(다중 정의)시에는 꼭 "모호성"을 고려해 코드를 짜시길 바랍니다.
위 출력 결과를 보면, 각 형 변환에 맞게 operator가 호출되는 것을 확인할 수 있습니다. 물론 객체의 값 역시 출력해보면 각 operator의 리턴값임을 확인할 수 있겠죠.
그렇다면 Point 클래스 타입을 정수 타입으로 변환하려면 어떻게 해야 될까요?
바로 다음과 같이 operator int() 를 선언하면 됩니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Point
{
...
operator int() const
{
return x;
}
};
int main()
{
int n = 10;
Point pt(2, 3);
n = pt; // pt.operator int() const 암시적 호출
cout << n << endl;
cout << pt << endl; // pt.operator int() const 암시적 호출
return 0;
}
[출력 결과]
2
2
마무리 하며..
이번 챕터에서는 STL을 배우기 전에 꼭 알아야할 연산자 오버로딩에 대해서 배워보았습니다. 중요한 내용도 정말 많지만, 이번 장에 다 못담는게 너무 아쉽습니다. 최대한 디테일을 살려서 중요한 내용을 많이 넣어봤는데 너무 길었나요? ㅎㅎ.. 코드가 절반이라 해도 벌써 markdown 파일이 1000줄이 넘어가네요. md에 아직 익숙치도 않고 양도 많아서 작성하는데에만 대략 6~7시간정도 들인거 같습니다..! 😭😭 줄이려고 노력하고 노력했지만서도 설명하는게 처음이기도 하고 아직은 내공이 많이 부족한가 봅니다.
잘 모르는 부분이나, 이해 안가는 부분, 또는 질문 있으시다면 남겨주시기 바랍니다. 아는 한에서 최대한 답변해드릴 수 있도록 노력하겠습니다!!
본문 내용에 잘못된 내용이나 오탈자가 있다면 댓글 부탁드립니다!!
