내돈내고 내가 공부한것을 올리며, 시간을 들여 배운과정을 복습하기보다는 간결히 공부한 흔적은 남긴다 하고 생각하고 써갈 예정입니다. 모든 내용을 이곳에 올릴수는 없으며, 그중 기억남은 몇가지 내용 위주만 올리겠다.
1) 섹션9
이번시간에는 연산자 오버로딩에 대하여 배웠다. 함수오버로딩과 마찬가지로, 같은 연산자를 다르게 사용하는 방식이다.
원래 블로그에 자세한 정리글을 안올리는대 이번단원이 잘 이해가지 않아 하나하나 다시 뜯어가면서 설명하듯 글을 작성하였다.
이번단원이 강좌들의 시간은 짧았음에도 불구하고 은근 어려운 부분이 많은 단원 이였다.
◆ 9-1 산술 연산자 오버로딩
산술 연산자의 오버로딩에서는 총 4가지 방식을 통해 오버로딩이 어느 순간에 필요한지를 점차 알려주셨다.
방법 1) class 밖에 원하는 기능을 수행하는 함수를 만든다. 이는 그냥 함수를 이용하는 일반적인 방식이다.
방법 2) class 밖에 원하는 기능을 수행하는 연산자를 오버로딩한다. <over_loading>
방법 3) class안으로 friend를 사용하여 연산자를 오버로딩한다. <over_loading>
방법 4) class안에서 member function으로 오버로딩함수를 만든다. <over_loading>
#include <iostream>
using namespace std;
class Cents {
int m_cents;
public:
Cents(int cents) { m_cents = cents; }
int getCents() const { return m_cents; }
friend Cents operator + (const Cents& cents1, const Cents& cents2); // 방법 3)
Cents operator + (const Cents& cents) { // 방법 4)
return Cents(this->m_cents + cents.getCents());
}
};
Cents add(const Cents& cents1, const Cents& cents2) { // 방법 1)
return Cents(cents1.getCents() + cents1.getCents());
}
Cents operator + (const Cents& cents1, const Cents& cents2) { // 방법 2)
return Cents(cents1.getCents() + cents2.getCents());;
}
int main()
{
Cents cents1(8), cents2(2);
cout << add(cents1, cents2).getCents() << endl; // 방법 1)
cout << (cents1 + cents2 + Cents(5)).getCents() << endl; // 방법 2, 3, 4)
}
방법 4) 의 경우 인자가 1개인대 이는 this 포인터 때문이다.
member_function에서만 오버로딩 가능한 연산자 들로는 =, [], (), -> 가 있다.
오버로딩 불가능한 연산자 conditional (?:), sizeof, scope (::), member selector (.), member pointer selector (.*), typeid
◆ 9-2 입출력 연산자 오버로딩
입출력 stream 또한 오버로딩을 통하여 편하게 사용할 수 있었다.
대표적으로 << Overloading이 있다. << Overloading은 + Overloading과 마찬가지로 binary operators이다.
이러한 입출력 오버로딩은 왜 사용하는 것 일까? 다음 코드를 먼저 같이 살펴보자.
#include <iostream>
using namespace std;
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)
{}
double getX() { return m_x; }
double getY() { return m_y; }
double getZ() { return m_z; }
// 기존 함수를 만들어 출력하던 방식
/*void print(){ cout << m_x << " " << m_y << " " << m_z << endl };*/
// 출력연산자 오버로딩 방식
// std::cout is actually an object of type std::ostream.
friend ostream& operator << (ostream& out, const Point& p) { // return type이 ostream&
out << p.m_x << " " << p.m_y << " " << p.m_z << endl; // out이 마치 cout 처럼
return out;
}
};
int main()
{
Point p1(0.0, 0.1, 0.2), p2(3.4, 1.5, 2.0);
// cout << p1.print() << " " << p2.print() << endl; 기존방식
cout << p1 << " " << p2 << endl; // 출력연산자 오버로딩 방식
return 0;
}
위의 코드에서 기존의 방식인 print함수를 만들어 사용하는 방식이 편하다 생각할수도 있다.
하지만 다음과 같은 상황에서는 중간에point.print()문을 삽입하기가 귀찮을 것 이다.
int main()
{
const Point point{1.0, 2.0, 3.0};
std::cout << "My point is: ";
point.print();
std::cout << " in Cartesian space.\n";
}
이럴때 << 오버로딩을 통하여 한줄로 편하게 사용할수가 있는 것 이다.
int main()
{
Point p1(0.0, 0.1, 0.2);
cout << "My point is: " << p1 << " in Cartesian space." << endl; // 출력연산자 오버로딩 방식
return 0;
}
<중요!>
return type이 왜 ostream& (참조)형인 이유는 std::ostream 특별히 복사가되지 않기 때문이다.
따라서 ostream과 같은 일반적인 반환은 사용할수가 없다.
또한 이로인해 chaining function(ex. cout << p1 << endl)기능을 사용할수 있게된 것이다.
◆ 9-3 단항 연산자 오버로딩
단항연산자 또한 이전과 비슷한 사용법을 갖는다. 다만 인자의 수에 주의해야한다.
#include <iostream>
using namespace std;
class Cents {
int m_cents;
public:
Cents(int cents = 0) { m_cents = cents; }
int getCents() const { return m_cents; }
// operator-()에 인자가 없음을 주의! (it operates on the *this object)
Cents operator-() const {
return Cents(-m_cents);
}
// 논리연산도 가능
bool operator ! () const {
return (m_cents == 0) ? true : false;
}
friend ostream& operator << (ostream& out, const Cents ¢s) {
out << cents.m_cents;
return out;
}
};
int main()
{
Cents cents1(3); Cents cents2(7);
cout << cents1 << endl; // 3
cout << -cents1 << endl; // -3
cout << -Cents(-10) << endl; // 10
cout << !cents1 << " " << !cents2 << endl; // 0 0
return 0;
}
◆ 9-4 비교 연산자 오버로딩
vector는 그냥 sort를 바로 적용할수가 없다. 이는 vector안에서 크기 비교를 할수 없기 때문이다.
따라서 vector를 sort하려면 연산자 오버로딩이 필요하다. sort를 할때는 < 연산자를 오버로딩 해야한다.
> 연산자는 sort가 되지 않는다.
#include <iostream>
#include <vector>
#include <algorithm>
#include <random>
using namespace std;
class Cents {
int m_cents;
public:
Cents(int cents = 0) { m_cents = cents; }
int getCents() const { return m_cents; }
int& getCents() { return m_cents; }
friend bool operator == (const Cents& c1, const Cents& c2){
return c1.m_cents == c2.m_cents;
}
friend bool operator < (const Cents& c1, const Cents& c2) { // Sort 사용시 < 연산자 사용
return c1.m_cents < c2.m_cents;
}
friend ostream& operator << (ostream& out, const Cents ¢s) {
out << cents.m_cents;
return out;
}
};
int main()
{
Cents cents1(3); Cents cents2(7);
vector<Cents> arr(20);
random_device randomDevice;
mt19937_64 makeRandom(randomDevice());
for (int i(0); i < 20; i++)
arr[i].getCents() = i;
shuffle(begin(arr), end(arr), makeRandom); // C++ 17 방식
for (auto& itr : arr)
cout << itr << " ";
sort(begin(arr), end(arr)); // 그냥사용 못함 arr안의 Cents는 크키비교를 못한다
// -> 따라서 연산자 오버로딩 필요
cout << "\n";
for (auto& itr : arr)
cout << itr << " ";
return 0;
}
◆ 9-5 증감 연산자 오버로딩
전위연산과 후위연산의 방식을 알아볼 수 있었다. postfix와 prefix모두 ++ 연산자를 사용하고 있기 때문에 오버로딩시 문제가 생긴다.
따라서 dummy를 주어 차이를 둔다. operator ++(int) 에서 int가 dummy parameter 이다.
#include <iostream>
using namespace std;
class Digit {
int m_digit;
public:
Digit(int digit = 0): m_digit(digit) {}
// prefix
Digit& operator ++ () {
++m_digit; // 먼저 증가후
return*this; // instance 자기자신 반환
}
// postfix
Digit operator ++(int) { // dummy값
Digit temp(m_digit); // temp에 값 일시저장
++(*this); // 값증가
return temp; // 값 증가 이전의 temp를 반환
}
friend ostream& operator << (ostream& out, const Digit& d) {
out << d.m_digit;
return out;
}
};
int main()
{
Digit d(5);
cout << ++d << endl; // 6
cout << d << endl; // 6
cout << d++ << endl; // 6
cout << d << endl; // 7
return 0;
}
◆ 9-6 첨자 연산자 오버로딩
#include <iostream>
using namespace std;
class IntList
{
private:
int m_list[10];
public:
void setItem(int index, int value) { // index에 value 저장
m_list[index] = value;
}
int getItem(int index){ // index에 해당하는 값을 꺼낸다
return m_list[index];
}
int* getList(){ // 포인터를 반환.
return m_list; // array 포인터를 반환한다
}
};
int main()
{
IntList my_list;
my_list.setItem(3, 1);
cout << my_list.getItem(3) << endl;
my_list.getList()[3] = 1; //포인터로 array를 꺼낸뒤에 값을 바꿔준다.
cout << my_list.getList()[3] << endl;
return 0;
}
위의 코드를 보면 main에서 my_list.getList()[3] = 1; 와 같이 값을 수정할때 ()와 []를 같이 사용해서 접근해야 한다는 불편함이 있다.
다음 subscript 오버로딩한 코드를 보자.
class IntList
{
private:
int m_list[10];
public:
int & operator [] (const int index){ // 입력받는 정보의 타입은 자유롭다.
return m_list[index];
}
};
int main()
{
IntList my_list;
my_list[3] = 10; // l-value 여야 하니까 참조형으로 반환
cout << my_list[3] << endl;
return 0;
}
참조형을 반환하고 있다. 이는 원본의 값을 수정, 대입하고 또 원하는 정보를 갖어오기 위해 원본을 조작하는 것 이다.
혹은 my_list[3] = 10; 에서 my_list[3]이 l-value여야 값의 변경이 가능하다.
따라서 이를 위해 참조형을 반환한다 생각해도 된다.
◆ 9-7 괄호 연산자 오버로딩
괄호 연산자 오버로딩을 하면 마치 객체가 함수인것 처럼, 마치 함수를 호출하여 더하기를 하는것 처럼 보이는데 이를 functor 라고 한다.
#include <iostream>
using namespace std;
class Accumulator
{
int m_counter = 0;
public:
int operator()(int i) { return (m_counter += i); } //m_counter+i를 return
};
int main()
{
Accumulator acc;
cout << acc(10) << endl; // 10
cout << acc(20) << endl; // 30
return 0;
}
◆ 9-8 형 변환 오버로딩
#include <iostream>
using namespace std;
class Cents
{
int m_cents;
public:
Cents(int cents = 0){
m_cents = cents;
}
int getCents() { return m_cents; }
void setCents(int cents) { m_cents = cents; }
operator int(){
cout << "cast here" << endl; // 사용확인용
return m_cents;
}
};
void printInt(const int &value){
cout << value << endl;
}
int main()
{
Cents cents(7);
int value = (int)cents;
value = int(cents);
value = static_cast<int>(cents);
printInt(cents); // 이줄부터 위로 4줄 모두 같은 int() 오버로딩 사용
return 0;
}
결과는 다음과 같다.
◆ 9-9 복사 생성자, 복사 초기화 반환값 최적화
복사 생성자는 자기와 똑같은 type의 instance가 들어오면 그것을 복사한다.
#include <iostream>
#include <cassert>
using namespace std;
class Fraction
{
int m_numerator; // 분자
int m_denominator; // 분모
public:
Fraction(int num = 0, int den = 1)
: m_numerator(num), m_denominator(den)
{
assert(den != 0); // 분모가 0이되면 안됨
}
Fraction(const Fraction &fraction) //copy constructor
:m_numerator(fraction.m_numerator), m_denominator(fraction.m_denominator)
{ // 멤버 변수를 복사
cout << "Copy constructor called" << endl; // 복사생성자 호출 확인용
}
friend std::ostream & operator << (std::ostream & out, const Fraction & f){
out << f.m_numerator << " / " << f.m_denominator << endl;
return out;
}
};
int main()
{
Fraction frac(3, 5);
Fraction fr_copy1(frac); // Copy constructor called
Fraction fr_copy2 = frac; // Copy constructor called
cout << frac << " " << fr_copy << endl;
return 0;
}
만약 복사를 못하게 막고싶다면, Copy constructor를 private로 옮겨주면 된다. 그럼 다음의 경우에도 복사 생성자가 호출될까?
int main()
{
Fraction fr_copy1(Fraction(3, 10));
cout << frac << " " << fr_copy << endl;
return 0;
}
놀랍게도 복사생성자가 호출되지 않는다. 그냥 보기에는 Fraction(3, 10)을 fr_copy1에 복사해서 넘겨주는것 처럼 보이지만 컴파일러는 이를 자동으로 fr_copy(3, 10)으로 변경해 버렸다.
◆ 9-10 변환 생성자, explicit
#include <iostream>
#include <cassert>
using namespace std;
class Fraction
{
int m_numerator;
int m_denominator;
public:
Fraction(int num = 0, int den = 1) // parameter가 둘중 하나만 들어와도 생성
: m_numerator(num), m_denominator(den)
{
assert(den != 0); // 분모가 0이되면 안됨
}
Fraction(const Fraction &fraction) //copy constructor
:m_numerator(fraction.m_numerator), m_denominator(fraction.m_denominator)
{ //
cout << "Copy constructor called" << endl; //몇번 호출하는지 검토하려고
}
friend std::ostream & operator << (std::ostream & out, const Fraction & f){
out << f.m_numerator << " / " << f.m_denominator << endl;
return out;
}
};
Fraction doSomething(Fraction frac){
cout << frac << endl;
}
int main()
{
Fraction frac(7);
doSomething(7); // 이부분에서 변환 생성자 호출
return 0;
}
위의 코드에서 main을 보면 Franction frac(7);은 정상적인 방법으로 작동한다.
하지만 두번째의 doSomething(7); 을 보면 인자로 Fraction의 인스턴스가 아닌 7을 넣어주고 있다.
이렇게 7을 넣어도 자동으로 바꿔주는 것 을 변환 생성자 라고 부른다.
7이 doSomething의 인자로 들어갔는대, doSomething은 Fraction밖에 받을수가 없으니 자동으로 생성자 처럼 바꾸어주는 것 이다.
public:
explicit Fraction(int num = 0, int den = 1)
: m_numerator(num), m_denominator(den)
{
assert(den != 0); // 분모가 0이면 무한대로 발산
}
만약 위의 코드처럼 explicit을 지정해주면 위의 자동 생성자는 작동하지 않는다.
◆ 9-11 깊은복사와 얕은 복사 부분은 따로 글을 작성해볼 생각이다.
2) 나의 현황
이번단원은 거의 하나하나 다시 보면서 내가 글을 다시 작성한 느낌이다. 한번 이렇게 복습하고 나니 좀더 쉽게 받아들여지는 것 같아 좋다. 이제 LearnCPP만 조금더 읽어보면 될 것 같다.
이글의 모든 사진과 내용의 출처는 홍정모 교수님께 있습니다.
홍정모 교수님 블로그:
'CS > C++' 카테고리의 다른 글
C++ 공부 섹션11 상속 : 홍정모의 따배씨쁠쁠 (0) | 2022.01.16 |
---|---|
C++ 공부 섹션10 객체관계 : 홍정모의 따배씨쁠쁠 (0) | 2022.01.16 |
C++ 공부 섹션8 : 홍정모의 따배씨쁠쁠 (0) | 2022.01.16 |
C++ 공부 섹션7 : 홍정모의 따배씨쁠쁠 (0) | 2022.01.16 |
C++ 공부 섹션6 : 홍정모의 따배씨쁠쁠 (0) | 2022.01.16 |
댓글