2.1 클래스란,
2.1-1 클래스란,
Note) 클래스의 아이디어를 C 기초문법 포스팅에서 다룸.
어떤 개체의 "정의"를 뜻함. DNA 같은 것.
Note) 구조체와 클래스의 차이
기본 접근 제어자가 다름.
구조체는 public, 클래스는 private.
Note) 컴퓨터는 구조체와 클래스를 구분할 수 있을까?
구분할 수 없음. 심지어 구조체 조차도
그저 변수가 여러 개 있는 것.
Ex02010101)
<hide/>
// MyVector.h
#pragma once
class MyVector // 클래스명은 항상 대문자로 시작.
{
int mX; // 변수명은 멤버를 뜻하는 m으로 시작.
int mY;
};
<hide/>
// MyVector.cpp
#include "MyVector.h"
<hide/>
// main.cpp
#include <iostream>
#include "MyVector.h"
using namespace std;
int main()
{
MyVector v1;
return 0;
}
Note) C++에서는 구조체를 클래스처럼 쓸 수 있다.
그러나 절대 그러지 말자. 할 수 있다고 다 해야하는건 아님.
구조체는 항상 C 스타일로 쓰는 것이 올바른 방법.
Note) 구조체를 올바르게 사용하는 코딩 표준.
1. 구조체는 순수한 멤버로만 정의되어야 함.
이를 Plain Old Data(POD)
즉, primitive type으로만 이루어져 있어야 함.
ex. 멤버로 Animal*를 가져도 될까? 왜 안될까?
primitive type이 아니므로, shallow copy 위험성이 있음.
2. 접근 제어자는 작성하지 말자.
그냥 public으로 두고 사용하자.
2.1-2 접근 제어자
Note) 이전 C 기초문법 포스팅에서 언급하였듯,
클래스의 기본 접근 제어자는 private임.
따라서, main() 함수에서는 MyVector 클래스의
멤버에 접근할 수 없음.
Def) 접근 제어자(Access Modifier)
public: 어디서나 접근 가능함.
protected: 자식 클래스까지 접근 가능.
private: 해당 클래스에서만 접근 가능.
Ex02010201)
<hide/>
// MyVector.h
#pragma once
class MyVector
{
// default access modifier == private
int mX;
public:
int mY;
protected:
int mZ;
private:
int mW;
};
<hide/>
// MyVector.cpp
#include "MyVector.h"
<hide/>
// main.cpp
#include <iostream>
#include "MyVector.h"
using namespace std;
int main()
{
MyVector v1;
// v1.mX = 100;
v1.mY = 200;
// v1.mZ = 200;
// v1.mW = 200;
return 0;
}
2.1-3 멤버와 메서드
Def) 멤버
클래스 내에 정의된 변수를 뜻함.
멤버 변수라고도 부름.
Def) 메서드
클래스 내에 정의된 함수를 뜻함.
멤버 함수라고도 부름.
Ex02010301)
<hide/>
// MyVector.h
#pragma once
class MyVector
{
public:
int GetX() { return mX; }
int GetY() { return mY; }
void SetX(int x) { mX = x; }
void SetY(int y) { mY = y; }
private:
int mX;
int mY;
};
<hide/>
// MyVector.cpp
#include "MyVector.h"
<hide/>
// main.cpp
#include <iostream>
#include "MyVector.h"
using namespace std;
int main()
{
MyVector v;
cout << v.GetX() << ", " << v.GetY() << endl;
v.SetX(10);
v.SetY(20);
cout << v.GetX() << ", " << v.GetY() << endl;
return 0;
}
Def) const 메서드
해당 클래스의 멤버 변수를 바꾸지 못하는 함수.
바꾸려고 시도한다면 compile error.
Note) 올바른 함수 시그니처 작성법
1. 시그니처만 보고도 무슨 동작을 하는지 이해되게끔 작성.
2. const를 기본적으로 적고, 필요할 때만 제거하는 식으로 작성.
Ex02010302)
<hide/>
// MyVector.h
#pragma once
class MyVector
{
public:
int GetX() const { return mX; } // getter는 읽기 전용 함수이므로, const 메서드로 작성.
int GetY() const { return mY; }
void SetX(int x) { mX = x; } // setter는 멤버를 변경해야 하므로, const 메서드로 작성하면 안됨.
void SetY(int y) { mY = y; }
private:
int mX;
int mY;
};
<hide/>
// MyVector.cpp
#include "MyVector.h"
<hide/>
// main.cpp
#include <iostream>
#include "MyVector.h"
using namespace std;
int main()
{
MyVector v;
cout << v.GetX() << ", " << v.GetY() << endl;
v.SetX(10);
v.SetY(20);
cout << v.GetX() << ", " << v.GetY() << endl;
return 0;
}
2.1-4 개체와 메모리
Note) 방금전 우리가 변수를 선언한 방식은
스택 메모리에 생성되게끔 하는 방식.
그렇다면 힙 메모리에 동적 할당되게끔 하려면?
MyVector* v2 = new MyVector(); // 생성자에 대해서는 바로 뒤에 나옴.
Note) 그러나, 위와 같이 힙 메모리에 생성된 개체는
반드시 해제해 주어야함. 마치 C에서 free() 먼저 하던것 처럼.
그래야만 메모리 누수가 발생하지 않음.
하는 방법은 delete 키워드를 사용하는 것.
MyVector* v2 = new MyVector();
...
delete v2;
v2 = nullptr;
Note) 이제부터 "변수를 선언한다."는 C 스타일 이야기.
대신에 "개체를 생성한다."라고 C++처럼 말하고자 함.
즉, 개체를 스택 메모리에 생성하냐 Vs.
개체를 힙 메모리에 생성하냐에 대한 이야기.
Note) 스택 메모리는 예약된 로컬 메모리 공간으로,
함수 호출과 반환이 이 메모리에서 일어나게 됨.
단순히 스택 포인터를 옮기기만함으로서,
메모리 할당 및 해제의 개념이 없음. 따라서 비교적 빠름.
스택 메모리에 할당된 공간은 함수의 스코프를 벗어나게 되면
더이상 접근할 수 없게됨. 즉, 나중에 알아서 덮어쓰기 당함.
Note) 하지만 스택 메모리에 큰 개체를 많이 넣으면,
스택 오버플로(Overflow)가 발생할 수 있음.
또한 OS 관련 내용이지만, 1MB의 스택 메모리를 요청해도
1MB 스택 메모리가 모두 다 생기진 않음.
swap-by로 어느정도는 HDD에 생기게됨.
그래서 언제든지 빠른건 아님. 오히려 느려질 수도.
Ex02010401)
<hide/>
// MyVector.h
#pragma once
class MyVector
{
public:
int mX;
int mY;
void add(const MyVector& other);
};
<hide/>
// MyVector.cpp
#include "MyVector.h"
void MyVector::add(const MyVector& other)
{
mX += other.mX;
mY += other.mY;
}
<hide/>
// main.cpp
#include <iostream>
#include "MyVector.h"
using namespace std;
int main()
{
MyVector v1;
MyVector v2;
v1.mX = 100;
v1.mY = 200;
v2.mX = 300;
v2.mY = 400;
v1.add(v2);
cout << v1.mX << ", " << v2.mY << endl;
return 0;
}
Note) 힙 메모리는 전역 메모리 공간으로
스택 메모리와 비교했을 때 상당히 큼. 몇 GB 단위.
연속적이면서도 사용되지 않은 메모리 블록을
찾아야하기 때문에 스택 메모리보다 느림.
다만 엄청 큰 데이터의 경우에는 힙 메모리에 넣고,
주소인 4 byte만 스택에 넣어서 연산하는 것이 더 나음.
작은 데이터의 경우에는 스택 메모리가 더 올바른 방법.
Note) 다만, 프로그래머가 직접 메모리를 할당 및 해제 해야함.
그렇지 않으면 메모리 누수가 발생함.
즉, C++도 언매니지드 언어임.
2.2 생성자와 소멸자
2.2-1 생성자란,
Def) 생성자(Constructor)
개체가 생성될 때, 자동으로 호출되어지는 메서드.
Ex02020101)
<hide/>
// MyVector.h
#pragma once
class MyVector
{
public:
int mX;
int mY;
MyVector(); // 생성자는 클래스명과 동일한 메서드명을 가짐.
void add(const MyVector& other);
};
<hide/>
// MyVector.cpp
#include <iostream>
#include "MyVector.h"
using namespace std;
MyVector::MyVector()
{
cout << "[Constructor]MyVector() has been called." << endl;
}
void MyVector::add(const MyVector& other)
{
mX += other.mX;
mY += other.mY;
}
<hide/>
// main.cpp
#include <iostream>
#include "MyVector.h"
using namespace std;
int main()
{
MyVector v1;
MyVector v2;
v1.mX = 200;
v1.mY = 500;
v2.mX = 500;
v2.mY = 200;
v1.add(v2);
cout << v1.mX << ", " << v2.mY << endl;
return 0;
}
Note) 생성자는 어떻게 활용되는걸까?
대부분 초기화에 사용되어짐.
그러나 아래와 같은 코드는 엄밀한 의미의 초기화는 아님.
...
MyVector::MyVector()
{
cout << "[Constructor]MyVector() has been called." << endl;
mX = 0;
mY = 0;
}
void MyVector::add(const MyVector& other)
{
...
}
2.2-2 초기화 리스트
Note) 위와 같은 "초기화"는 마치 콜라 캔이 생성되고
공장을 이미 빠져나온 뒤에 콜라가 채워지는 격임.
공장에서 캔이 생성된 직후 바로 콜라가 채워지는
엄밀한 의미의 초기화는 어떻게 할까?
이를 초기화 리스트라고 함.
Ex02020201)
<hide/>
// MyVector.h
#pragma once
class MyVector
{
public:
int mX;
int mY;
MyVector();
void add(const MyVector& other);
};
<hide/>
// MyVector.cpp
#include <iostream>
#include "MyVector.h"
using namespace std;
MyVector::MyVector()
: mX(0) // 초기화 리스트 작성방법.
, mY(0)
{
cout << "[Constructor]MyVector() has been called." << endl;
}
void MyVector::add(const MyVector& other)
{
mX += other.mX;
mY += other.mY;
}
<hide/>
// main.cpp
#include <iostream>
#include "MyVector.h"
using namespace std;
int main()
{
MyVector v1;
MyVector v2;
v1.add(v2);
cout << v1.mX << ", " << v2.mY << endl;
return 0;
}
Note) 초기화 리스트 작성의 차이점.
개체가 생각된 후에 대입 Vs. 개체가 생성되면서 초기화
Ex02020202)
<hide/>
// Human.h
#pragma once
enum { LENGTH = 16 };
class Human
{
public:
char mName[LENGTH];
float mHeight;
float mWeight;
Human(const char* name, const float& height, const float& weight);
void SayHi();
};
<hide/>
// Human.cpp
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <string.h>
#include "Human.h"
using namespace std;
Human::Human(const char* name, const float& height, const float& weight)
: mName{0,}
, mHeight(height)
, mWeight(weight)
{
strcpy(mName, name);
}
void Human::SayHi()
{
cout << "My name is " << mName << endl;
cout << "My height is " << mHeight << endl;
cout << "My weight is " << mWeight << endl;
}
<hide/>
// main.cpp
#include <iostream>
#include "Human.h"
using namespace std;
int main()
{
Human me = Human("Parkthy", 173.f, 65.f);
me.SayHi();
return 0;
}
Note) 만약 멤버가 const 변수 혹은 참조 변수라면
선언과 동시에 초기화 되어야 하므로,
초기화 리스트를 써야만 함.
면접때 초기화 리스트를 작성하지 않는다면 감점 요인임.
2.2-3 기본 생성자
Def) 기본 생성자(Default Constructor)
클래스에 생성자가 없으면 컴파일러가
자동으로 만들어주는 생성자를 기본 생성자라 함.
MyVector() {} // 대략 이런식.
Note) 기본 생성자는 멤버를 초기화하지 않음.
이는 Java와 다른점임. 인자를 받지도 않음.
Note) 다만, 멤버로 다른 클래스의 개체를 갖고 있는 경우
해당 개체의 생성자는 자동으로 호출 해줌.
Note) 만약 생성자가 구현되어 있는 경우
기본 생성자는 만들어지지 않음.
따라서 아래 코드는 컴파일 되지 않을 수도 있음.
왜 그런지 생각해 보자.
매개변수를 받는 생성자가 오버로딩 되어 있다고 해보자.
MyVector a;
2.2-4 생성자 오버로딩(Constructor Overloading)
Note) 여러 버전의 생성자를 만들 수도 있음.
즉, 같은 이름이면서 매개변수의 개수나 자료형이 다름.
같은 시그니처의 함수가 중복되어서는 안됨.
overwriting과는 다른 개념임에 주의.
오버라이딩은 같은 시그니처의 함수임.
Ex02020401)
<hide/>
// MyVector.h
#pragma once
class MyVector
{
public:
int mX;
int mY;
MyVector();
MyVector(int x, int y); // 연산자 오버로딩
void add(const MyVector& other);
};
<hide/>
// MyVector.cpp
#include <iostream>
#include "MyVector.h"
using namespace std;
MyVector::MyVector()
: mX(0)
, mY(0)
{
cout << "[Constructor]MyVector() has been called." << endl;
}
MyVector::MyVector(int x, int y)
: mX(x)
, mY(y)
{
cout << "[Constructor]MyVector(int, int) has been called." << endl;
}
void MyVector::add(const MyVector& other)
{
mX += other.mX;
mY += other.mY;
}
<hide/>
// main.cpp
#include <iostream>
#include "MyVector.h"
using namespace std;
int main()
{
MyVector v1 = MyVector(2, 7);
MyVector v2;
v1.add(v2);
cout << v1.mX << ", " << v1.mY << endl;
return 0;
}
2.2-5 복사 생성자
Def) 복사 생성자(Copy Constructor)
같은 클래스에 속한 다른 개체를 이용하여
새로운 개체를 초기화함.
Note) 왜 복사 "생성자"일까?
복사는 자주 일어나는 작업이므로, C++에서는
생성자 안에서 해결하고자 했음.
생성자인데, 매개변수의 자료형에 따라서
복사 생성자인지 아닌지를 구별함.
만약 매개변수에 클래스명이 적혀 있다면 복사 생성자.
Note) 생성자의 매개변수 자료형이 클래스?
개체 그 자신은 아직 생성되지 않았으므로 전달 받을 일이 없음.
당연히 똑같은 클래스의 다른 개체가 인자로 전달되어서
개체가 복사됨을 의미함.
Ex02020501)
<hide/>
// MyVector.h
#pragma once
class MyVector
{
public:
MyVector();
MyVector(int x, int y);
MyVector(const MyVector& other);
~MyVector();
int GetX() const { return mX; }
int GetY() const { return mY; }
void SetX(int x) { mX = x; }
void SetY(int y) { mY = y; }
private:
int mX;
int mY;
};
<hide/>
// MyVector.cpp
#include "MyVector.h"
MyVector::MyVector()
: mX(0)
, mY(0)
{
}
MyVector::MyVector(int x, int y)
: mX(x)
, mY(y)
{
}
MyVector::MyVector(const MyVector& other)
: mX(other.mX) // other는 다른 개체이지만, 같은 MyVector 클래스이기에 private 멤버 접근 가능.
, mY(other.mY)
{
}
MyVector::~MyVector()
{
}
<hide/>
// main.cpp
#include <iostream>
#include "MyVector.h"
using namespace std;
int main()
{
MyVector v = MyVector(2, 5); // 복사 생성자가 호출됨.
MyVector clonedV = MyVector(v);
cout << clonedV.GetX() << ", " << clonedV.GetY() << endl;
return 0;
}
2.2-6 암시적 복사 생성자
Def) 암시적 복사 생성자(Implicit Copy Constructor)
클래스에 복사 생성자가 없는 경우,
컴파일러가 암시적으로 복사 생성자를 자동 생성.
이 복사 생성자를 암시적 복사 생성자라고 함.
Note) 암시적 복사 생성자의 시그니처는 단 하나.
그렇기 때문에, 컴파일러가 자동으로 만들어 줄 수 있음.
그냥 모든 멤버별 복사를 함. 멤버 중 어떤 멤버는
복사하고 어떤 멤버는 복사 안한다면 그건 복사 생성자가 아님.
Note) 암시적 복사 생성자는 얕은 복사(Shallow Copy)를 수행함.
primitive type은 값 복사가 이뤄지지만,
reference type은 주소 복사가 이뤄짐 -> 얕은 복사.
만약 개체라면, 해당 클래스의 복사 생성자가 호출됨.
Note) 만약 개체라면 해당 클래스의 복사 생성자 호출?
Person 클래스의 멤버로 MyVector 클래스의 변수가 있다면
MyVector 클래스의 복사 생성자가 자동으로 호출됨.
Ex02020601)
<hide/>
// Record.h
#pragma once
class Record
{
public:
Record(int* scores, const size_t& count);
Record(const Record& other);
~Record();
int GetRecord(const size_t& index) const;
void SetRecord(const size_t& index, const int& record);
void PrintRecords();
private:
size_t mCount;
int* mScores;
};
<hide/>
// Record.cpp
#include <iostream>
#include "Record.h"
using namespace std;
Record::Record(int* scores, const size_t& count)
: mScores(scores)
, mCount(count)
{
}
Record::Record(const Record& other)
: mScores(other.mScores)
, mCount(other.mCount)
{
}
Record::~Record()
{
delete[] mScores;
mScores = nullptr;
}
int Record::GetRecord(const size_t& index) const
{
return *(mScores + index);
}
void Record::SetRecord(const size_t& index, const int& record)
{
*(mScores + index) = record;
}
void Record::PrintRecords()
{
for (size_t i = 0; i < mCount; ++i) { cout << *(mScores + i) << ", "; }
cout << endl;
}
<hide/>
// main.cpp
#include <iostream>
#include "Record.h"
using namespace std;
enum { COUNT = 5 };
int main()
{
int scores[COUNT] = { 99, 70, 89, 73, 65 };
Record classRecord = Record(scores, COUNT);
classRecord.PrintRecords();
Record* classRecordBackup = new Record(classRecord);
classRecordBackup->PrintRecords();
classRecordBackup->SetRecord(2, 10);
classRecord.PrintRecords();
classRecordBackup->PrintRecords();
return 0;
}
Note) Ex02020601은 왜 에러가 났을까?
classRecordBackup에 얕은 복사 되었던 주소가 소멸됨.
Note) 클래스 안에서 되도록이면 동적할당을 자제하자.
동적 메모리 할당을 하면 얕은 복사의 위험이 매우 높음.
즉, 이때 암시적 복사 생성자를 쓰면 안됨.
직접 복사 생성자를 만들어서, 깊은 복사를 하는 것이 올바름.
다시 말해, 포인터가 가리키는 실제 데이터까지도 복사하는 것.
Ex02020602)
<hide/>
// Record.h
#pragma once
class Record
{
public:
Record(const int* scores, const size_t& count);
Record(const Record& other);
~Record();
int GetRecord(const size_t& index) const;
void SetRecord(const size_t& index, const int& record);
void PrintRecords();
private:
size_t mCount;
int* mScores;
};
<hide/>
// Record.cpp
#include <iostream>
#include "Record.h"
using namespace std;
Record::Record(const int* scores, const size_t& count)
: mCount(count)
{
mScores = new int[mCount];
memcpy(mScores, scores, mCount * sizeof(int));
}
Record::Record(const Record& other)
: mCount(other.mCount)
{
mScores = new int[mCount];
memcpy(mScores, other.mScores, mCount * sizeof(int));
}
Record::~Record()
{
delete[] mScores;
mScores = nullptr;
}
int Record::GetRecord(const size_t& index) const
{
return *(mScores + index);
}
void Record::SetRecord(const size_t& index, const int& record)
{
*(mScores + index) = record;
}
void Record::PrintRecords()
{
for (size_t i = 0; i < mCount; ++i) { cout << *(mScores + i) << ", "; }
cout << endl;
}
<hide/>
// main.cpp
#include "Record.h"
enum { COUNT = 5 };
int main()
{
int scores[COUNT] = { 99, 70, 89, 73, 65 };
Record classRecord = Record(scores, COUNT);
classRecord.PrintRecords();
Record* classRecordBackup = new Record(classRecord);
classRecordBackup->PrintRecords();
classRecordBackup->SetRecord(2, 10);
classRecord.PrintRecords();
classRecordBackup->PrintRecords();
return 0;
}
Ex02020603)
<hide/>
// MyString.h
#pragma once
class MyString
{
public:
MyString(void);
MyString(const char* string, const size_t& capacity);
MyString(const MyString& other);
~MyString(void);
private:
char* mString;
size_t mCapacity;
size_t mLength;
};
<hide/>
// MyString.h
#include <iostream>
#include "MyString.h"
using namespace std;
MyString::MyString(void)
: mString(nullptr)
, mCapacity(15)
, mLength(0)
{
mString = new char[mCapacity + 1];
}
MyString::MyString(const char* string, const size_t& capacity)
: mString(nullptr)
, mLength(strlen(string))
, mCapacity(capacity)
{
mString = new char[mCapacity + 1];
memcpy(mString, string, (mCapacity + 1) * sizeof(char));
}
MyString::MyString(const MyString& other)
: mString(nullptr)
, mLength(strlen(other.mString))
, mCapacity(other.mCapacity)
{
mString = new char[mCapacity + 1];
memcpy(mString, other.mString, (mCapacity + 1) * sizeof(char));
}
MyString::~MyString(void)
{
delete[] mString;
mString = nullptr;
mCapacity = 0;
mLength = 0;
}
<hide/>
// main.cpp
#include "MyString.h"
enum { LENGTH = 15 };
int main()
{
MyString firstName = MyString("Park", LENGTH);
MyString fullName = MyString(firstName);
return 0;
}
2.2-7 소멸자(Destructor)
Def) 소멸자(Destructor)
생성자와 반대로, 개체가 지워질 때
즉, delete 사용 혹은 스택 메모리가 반환될 때
자동으로 호출되는 메서드.
클래스명에 ~ 문자를 붙히면 소멸자 메서드 완성.
Note) C++ RAII에 따라, 해당 스코프에서
동적으로 메모리를 할당 했다면 필히
해당 스코프에서 메모리를 직접 해제 해줘야 함.
그러려면 개체가 지워지는 시기를 알아야 함.
그 시기를 알기 위해 있는 것이 소멸자.
즉, 언제든 해당 개체가 사라질 때
소멸자가 호출 될 것이기 때문에,
소멸자에서 메모리를 직접 해제해주면 됨.
Note) 자동으로 호출되는 것이지
내가 호출 하는 것이 아니므로 매개변수는 따로 없음.
즉, 오버로딩도 불가능함.
Ex02020501)
<hide/>
// MyVector.h
#pragma once
class MyVector
{
public:
int mX;
int mY;
MyVector();
MyVector(int x, int y);
~MyVector();
void add(const MyVector& other);
};
<hide/>
// MyVector.cpp
#include <iostream>
#include "MyVector.h"
using namespace std;
MyVector::MyVector()
: mX(0)
, mY(0)
{
cout << "[Constructor]MyVector() has been called." << endl;
}
MyVector::MyVector(int x, int y)
: mX(x)
, mY(y)
{
cout << "[Constructor]MyVector(int, int) has been called." << endl;
}
MyVector::~MyVector()
{
cout << "[Destructor]~MyVector() has been called." << endl;
}
void MyVector::add(const MyVector& other)
{
mX += other.mX;
mY += other.mY;
}
<hide/>
// main.cpp
#include <iostream>
#include "MyVector.h"
using namespace std;
int main()
{
MyVector v1 = MyVector(2, 7);
MyVector v2;
v1.add(v2);
cout << v1.mX << ", " << v1.mY << endl;
return 0;
}
Ex02020502)
<hide/>
// MyString.h
#pragma once
class MyString
{
public:
MyString(void);
~MyString(void);
private:
const char* mString;
size_t mCapacity;
size_t mLength;
};
<hide/>
// MyString.h
#include <iostream>
#include "MyString.h"
using namespace std;
MyString::MyString(void)
: mString(nullptr)
, mCapacity(15)
, mLength(0)
{
cout << "Constructor has been called." << endl;
mString = new char[mCapacity + 1];
}
MyString::~MyString(void)
{
cout << "Destructor has been called." << endl;
delete[] mString;
mString = nullptr;
mCapacity = 0;
mLength = 0;
}
<hide/>
// main.cpp
#include <iostream>
#include "MyString.h"
using namespace std;
int main()
{
MyString str;
return 0;
}
2.3 함수 오버로딩
2.3-1 함수 오버로딩
Note) 반환 자료형은 달라도 상관 없음.
함수명은 같음. 매개변수 목록은 유일해야 함.
이를 함수 오버로딩이라고 함.
Note) 다음 중 컴파일 에러가 나는 함수 오버로딩 코드는?
void Print(int score); // 1번
void Print(const char* name); // 2번
void Print(float gpa, const char* name); // 3번
int Print(int score); // 4번
int Print(float gpa); // 5번
2.3-2 함수 오버로딩 매칭
Note) 오버로딩된 함수 중에 어떤 함수를 호출하는지 판단 과정
1. 가장 적합한 함수를 찾은 경우 -> 정상 작동
2. 매칭되는 함수를 여러 개 찾은 경우 -> 컴파일 에러
3. 매칭되는 함수를 찾을 수 없는 경우 -> 컴파일 에러
Note) 다음 중 링크 에러가 나는 코드는?
void Print(int score);
void Print(const char* name);
void Print(float gpa, const char* name);
Print(100);
Print("Parkthy");
Print(3.14f, "Parkthy");
Print(3.14f);
Print("Parkthy", 3.14f);
Note) 아래와 같은 경우에도 문제가 생길 수 있음.
int Max(int, int);
int Max(double, double);
Max(1, 3.14); // 1번과 매칭하면 1개 정확한 매치, 1개는 표준 변환(Standard Conversion)
// 2번을 매칭해도 1개는 표준 변환, 1개는 정확한 매치.
// 따라서, 모호한 호출이므로 컴파일 에러.
Ex02030201)
<hide/>
// MyVector.h
#pragma once
class MyVector
{
public:
MyVector();
MyVector(int x, int y);
MyVector(const MyVector& other);
~MyVector();
int GetX() const { return mX; }
int GetY() const { return mY; }
void SetX(const int& x) { mX = x; }
void SetY(const int& y) { mY = y; }
bool IsEqual(const MyVector& other) const { return (mX == other.mX && mY == other.mY); };
MyVector Multiply(const int& scalar);
MyVector Multiply(const MyVector& other);
void scale(const int& scalar);
void scale(const MyVector& other);
private:
int mX;
int mY;
};
<hide/>
// MyVector.cpp
#include "MyVector.h"
MyVector::MyVector()
: mX(0)
, mY(0)
{
}
MyVector::MyVector(int x, int y)
: mX(x)
, mY(y)
{
}
MyVector::MyVector(const MyVector& other)
: mX(other.mX)
, mY(other.mY)
{
}
MyVector::~MyVector()
{
}
MyVector MyVector::Multiply(const int& scalar)
{
MyVector temp = MyVector();
temp.mX = mX * scalar;
temp.mY = mY * scalar;
return temp;
}
MyVector MyVector::Multiply(const MyVector& other)
{
MyVector temp = MyVector();
temp.mX = mX * other.mX;
temp.mY = mY * other.mY;
return temp;
}
void MyVector::scale(const int& scalar)
{
mX *= scalar;
mY *= scalar;
}
void MyVector::scale(const MyVector& other)
{
mX *= other.mX;
mY *= other.mY;
}
<hide/>
// main.cpp
#include <iostream>
#include "MyVector.h"
using namespace std;
int main()
{
MyVector a = MyVector(3, 4);
MyVector b = MyVector(7, 8);
MyVector res = a.Multiply(b);
cout << res.GetX() << " : " << res.GetY() << endl;
res.Multiply(3);
cout << res.GetX() << " : " << res.GetY() << endl;
res.scale(a);
cout << res.GetX() << " : " << res.GetY() << endl;
res.scale(2);
cout << res.GetX() << " : " << res.GetY() << endl;
return 0;
}
Note) 더 자세한 함수 오버로딩 규칙
만약 컴파일러 설계에 관심이 있다면,
구글에 "Function Overload Resolution"이라고 검색.
2.4 연산자 오버로딩
2.4-1 연산자의 종류
Note) 연산자도 함수이다.
입력이 있고, 출력이 있다는 면에서 함수라 할 수 있음.
Note) 단항 연산자(Unary Operator)
단항 연산자 | |
! | ~ |
& | * |
+ | - |
++ | -- |
형변환 연산자(Type-cast operator) | |
그외 다수 |
Note) 다만, MyVector& vRef;에서 &는 연산자가 아님.
마찬가지로 MyVector* vPtr;에서 *도 연산자가 아님.
Note) 이항 연산자(Binary Operator)
대부분의 연산자가 이항 연산자에 속함.
-> 연산자 또한 이항 연산자
Note) 기타 연산자
1. 함수 호출 연산자 ()
ex. Max(1, 3.14f);
2. 첨자 연산자 []
(Subscript Operator)
ex. int score = scores[10];
3. new
ex. MyVector* vectors = new MyVector[10];
4. delete
ex. delete[] vectors;
2.4-2 연산자 오버로딩
Note) 우리가 만든 MyVector 클래스의 개체간에
덧셈은 어떻게 할까? 컴퓨터에게 시키면 할 수 있을까?
절대 할 수 없음. 즉, 우리가 덧셈을 정의시켜줘야 함.
Note) 벡터의 덧셈 연산자를 오버로딩해보자.
반환 자료형은? MyVector.
왜? resV = v1 + v2;와 같은 상황 때문에.
매개변수 자료형은? const MyVector&.
왜? 1. 불필요한 값복사 방지->& 2. 읽기 전용->const
Ex02040201)
<hide/>
// MyVector.h
#pragma once
class MyVector
{
public:
MyVector();
MyVector(int x, int y);
MyVector(const MyVector& other);
~MyVector();
int GetX() const { return mX; }
int GetY() const { return mY; }
void SetX(const int& x) { mX = x; }
void SetY(const int& y) { mY = y; }
MyVector operator+(const MyVector& other);
private:
int mX;
int mY;
};
<hide/>
// MyVector.cpp
#include <iostream>
#include "MyVector.h"
using namespace std;
MyVector::MyVector()
: mX(0)
, mY(0)
{
}
MyVector::MyVector(int x, int y)
: mX(x)
, mY(y)
{
}
MyVector::MyVector(const MyVector& other)
: mX(other.mX)
, mY(other.mY)
{
}
MyVector::~MyVector()
{
}
MyVector MyVector::operator+(const MyVector& other)
{
cout << "MyVector::operator+() has been called." << endl;
MyVector temp = MyVector();
temp.mX = mX + other.mX;
temp.mY = mY + other.mY;
return temp;
}
<hide/>
// main.cpp
#include <iostream>
#include "MyVector.h"
using namespace std;
int main()
{
MyVector a = MyVector(3, 4);
MyVector b = MyVector(7, 8);
MyVector res = a + b;
cout << res.GetX() << " : " << res.GetY() << endl;
return 0;
}
Note) 이렇듯, 기호는 같지만 여러가지 연산이 가능함.
정수 간의 덧셈은 물론이고, 문자열간의 덧셈도 가능함.
Note) 두 가지 연산자 오버로딩 방법
1. 멤버 함수의 오버로딩
2. 멤버 함수가 아닌 함수의 오버로딩
2.4-3 멤버 함수의 오버로딩
Note) Ex02040201의 operator+()가 대표적.
Note) 특정 연산자들은 멤버 함수의 오버로딩만 가능.
ex. =, (), [], ->
Note) 멤버 연산자 작성 방법
반환자료형 클래스명::operator연산자(매개변수목록) { ... }
Vector Vector::operator-(const Vector& rhs) const;
Vector Vector::operator*(const Vector& rhs) const;
Vector Vector::operator/(const Vector& rhs) const;
2.4-4 멤버 함수가 아닌 함수의 오버로딩
Note) 그렇다면 아래와 같은 연산자 오버로딩도 가능할까?
안된다면 왜 안될까? mX와 mY가 private 멤버 변수기 때문.
cout << v1.mX << ", " << v1.mY << endl;
Note) 그렇다면 아래와 같은 코드는 어떨까?
모든 멤버 변수에 getter를 작성해야 가능하고, 함수 호출까지 해야함.
cout << v1.GetX() << ", " << v1.GetY() << endl;
Note) 우리의 목표는 아래와 같음.
cout << v1;
Note) 아래와 같은 것을 만들면 됨.
std::cout.operator<<(v1);
cout 클래스의 멤버 연산자를 만들려면,
cout 클래스에 추가로 구현해 주어야 함.
Note) cout은 iostream header file에 있음.
그럼 iostream header file에 아래 코드를 추가하면 될까?
void cout::operator<<(const MyVector& rhs) const { ... }
Note) iostream은 남이 만든 헤더파일이므로
그걸 수정한다는거 자체가 말이 안됨. cpp file을 볼수도 없음.
Note) 그럼 어떻게 해야 할까?
어떤 클래스의 멤버 함수로 못 만들기 때문에
전역 함수로 만들어야 함.
Note) 전역 함수 operator<<() 연산자를 만들어보자.
void operator<<(std::ostream& os, const MyVector& rhs) // 전역 함수기때문에, 범위지정자가 없음.
{
os << rhs.mX << ", " << rhs.mY;
}
Note) 위 코드에는 두 가지 문제가 있음.
1. 위 전역 함수를 어디에 선언하고 구현할까?
지금까지는 클래스별로 header file과 cpp file을 구성했음.
어디에 구현해야할까
2. 위 전역 함수가 MyVector 클래스의 private 멤버를 어떻게 읽을까?
이 문제를 해결하기 위한 키워드가 바로 friend 키워드.
2.4-5 friend 키워드
Def) friend 키워드
다른 클래스나 전역 함수가 나의 private 또는
protected 멤버에 접근할 수 있게 허용해줌.
Note) 아래 코드가 정상적으로 작동할까? 왜 안될까?
모르는 사람이 와서 나랑 친구라 주장하면, 내 번호를 줘도 되나?
class MyVector {
private:
int mX;
int mY;
};
class X {
friend class MyVector;
public:
void printMyVectorMemver() { cout << mX << ", " << endl; }
};
Ex02040501)
<hide/>
// X.h
#pragma once
class X
{
friend class Y;
public:
X();
private:
int mPrivateMember;
};
<hide/>
// X.cpp
#include "X.h"
X::X()
: mPrivateMember(0)
{
}
<hide/>
// Y.h
#pragma once
class X; // 전방 선언. 어딘가에 클래스 X가 정의되어 있으니, 컴파일 단계에선 그냥 넘어가달란 뜻.
class Y
{
public:
void foo(X& x);
};
<hide/>
// Y.cpp
#include <iostream>
#include "Y.h"
#include "X.h"
using namespace std;
void Y::foo(X& x)
{
x.mPrivateMember += 10;
cout << x.mPrivateMember << endl;
}
<hide/>
// main.cpp
#include <iostream>
#include "X.h"
#include "Y.h"
using namespace std;
int main()
{
X x;
Y y;
y.foo(x);
return 0;
}
Note) 다만, friend 키워드가 OOP 진영에서는 안티 패턴으로 불림.
OOP 진영에선 각 클래스마다 캡슐화가 되어있어서,
외부에선 쓰지 못하게 해야하는데, 그걸 깨버리기 때문.
Note) 근데 클래스를 만들다보면 실제 서로 관계가 있어서
더 큰 클래스로 묶어서 만들어져야 할때가 있음.
하지만, 이러면 너무 커져서 복잡해지니까 나누게됨.
근데 또 각자 private 멤버들을 접근할 수 없어짐.
그래서 getter를 호출하게됨. 무한 굴레.
결국 이런 경우에는 friend 키워드가 해결해 줄 수 있음.
다만 너무 남용하면 코드의 스파게티화가 진행됨.
Note) 이번에는 friend 함수를 구현 해 보자.
Ex02040502)
<hide/>
// X.h
#pragma once
class X
{
friend void foo(X& x);
public:
X();
private:
int mPrivateMember;
};
<hide/>
// X.cpp
#include "X.h"
X::X()
: mPrivateMember(0)
{
}
<hide/>
// main.cpp
#include <iostream>
#include "X.h"
using namespace std;
void foo(X& x);
int main()
{
X x;
foo(x);
return 0;
}
void foo(X& x)
{
x.mPrivateMember += 10;
cout << x.mPrivateMember << endl;
}
Note) 연산자 오버로딩에 필요한 friend 함수
friend 함수는 멤버 함수가 아님에 주의. 전역 함수임.
그저 클래스에 해당 전역 함수가 접근할 수 있도록 함.
Ex02040503)
<hide/>
// MyVector.h
#pragma once
class MyVector
{
friend void operator<<(std::ostream& os, const MyVector& rhs);
public:
MyVector();
MyVector(int x, int y);
MyVector(const MyVector& other);
~MyVector();
int GetX() const { return mX; }
int GetY() const { return mY; }
void SetX(const int& x) { mX = x; }
void SetY(const int& y) { mY = y; }
MyVector operator+(const MyVector& other);
private:
int mX;
int mY;
};
<hide/>
// MyVector.cpp
#include <iostream>
#include "MyVector.h"
using namespace std;
MyVector::MyVector()
: mX(0)
, mY(0)
{
}
MyVector::MyVector(int x, int y)
: mX(x)
, mY(y)
{
}
MyVector::MyVector(const MyVector& other)
: mX(other.mX)
, mY(other.mY)
{
}
MyVector::~MyVector()
{
}
MyVector MyVector::operator+(const MyVector& other)
{
cout << "MyVector::operator+() has been called." << endl;
MyVector temp = MyVector();
temp.mX = mX + other.mX;
temp.mY = mY + other.mY;
return temp;
}
void operator<<(std::ostream& os, const MyVector& rhs)
{
cout << '(' << rhs.mX << ", " << rhs.mY << ')' << endl;
}
<hide/>
// main.cpp
#include <iostream>
#include "MyVector.h"
using namespace std;
int main()
{
MyVector v;
v.SetX(2);
v.SetY(5);
cout << v;
return 0;
}
Note) 아래와 같은 코드를 작성하면 발생하는 문제점은?
cout << v1 << endl;는 void << endl;로 평가되어서 컴파일 에러.
cout << v1 << endl;
Ex02040504)
<hide/>
// MyVector.h
#pragma once
class MyVector
{
friend std::ostream & operator<<(std::ostream & os, const MyVector & rhs);
public:
MyVector();
MyVector(int x, int y);
MyVector(const MyVector& other);
~MyVector();
int GetX() const { return mX; }
int GetY() const { return mY; }
void SetX(const int& x) { mX = x; }
void SetY(const int& y) { mY = y; }
MyVector operator+(const MyVector& other);
private:
int mX;
int mY;
};
<hide/>
// MyVector.cpp
#include <iostream>
#include "MyVector.h"
using namespace std;
MyVector::MyVector()
: mX(0)
, mY(0)
{
}
MyVector::MyVector(int x, int y)
: mX(x)
, mY(y)
{
}
MyVector::MyVector(const MyVector& other)
: mX(other.mX)
, mY(other.mY)
{
}
MyVector::~MyVector()
{
}
MyVector MyVector::operator+(const MyVector& other)
{
cout << "MyVector::operator+() has been called." << endl;
MyVector temp = MyVector();
temp.mX = mX + other.mX;
temp.mY = mY + other.mY;
return temp;
}
std::ostream& operator<<(std::ostream& os, const MyVector& rhs)
{
os << '(' << rhs.mX << ", " << rhs.mY << ')';
return os;
}
<hide/>
// main.cpp
#include <iostream>
#include "MyVector.h"
using namespace std;
int main()
{
MyVector v1;
MyVector v2;
v1.SetX(2);
v2.SetX(2);
v1.SetY(5);
v2.SetY(5);
cout << v1 << v2 << endl;
return 0;
}
Note) 연산자 오버로딩 제한사항
1. 오버로딩된 연산자는 최소한 하나의
사용자 정의 자료형을 매개변수 자료형으로 가져야함.
ex. MyVector operator+(const MyVector& rhs) const;
2. 오버로딩된 연산자는 피연산자 수를 동일하게 유지해야함.
다만, +/- 부호 연산자는 단항 연산자로도 오버로딩 가능.
3. 새로운 연산자 부호를 만들 순 없음.
ex. MyVector operator@(const MyVector& rhs) const;
4. 오버로딩 할 수 없는 연산자가 존재함.
ex. . / .* / :: / ?: 등등
Ex02040505)
<hide/>
// MyVector.h
#pragma once
class MyVector
{
friend std::ostream & operator<<(std::ostream & os, const MyVector & rhs);
public:
MyVector();
MyVector(int x, int y);
MyVector(const MyVector& other);
~MyVector();
int GetX() const { return mX; }
int GetY() const { return mY; }
void SetX(const int& x) { mX = x; }
void SetY(const int& y) { mY = y; }
bool operator==(const MyVector& rhs) const;
MyVector operator+(const MyVector& other) const;
MyVector operator*(const MyVector& rhs) const;
MyVector operator*(int multiplier) const;
friend MyVector operator*(int multiplier, const MyVector& v);
MyVector& operator*=(const MyVector& rhs);
MyVector& operator*=(int multiplier);
friend std::ostream& operator<<(std::ostream& os, const MyVector& rhs);
private:
int mX;
int mY;
};
<hide/>
// MyVector.cpp
#include <iostream>
#include "MyVector.h"
using namespace std;
MyVector::MyVector()
: mX(0)
, mY(0)
{
}
MyVector::MyVector(int x, int y)
: mX(x)
, mY(y)
{
}
MyVector::MyVector(const MyVector& other)
: mX(other.mX)
, mY(other.mY)
{
}
MyVector::~MyVector()
{
}
bool MyVector::operator==(const MyVector& rhs) const
{
return (mX == rhs.mX && mY == rhs.mY);
}
MyVector MyVector::operator*(const MyVector& rhs) const // 곱셈은 호출한 자신을 바꾸는 연산자가 아니므로, 지역변수가 필요 -> 따라서 반환자료형으로 참조형은 안됨.
{
MyVector temp;
temp.mX = mX * rhs.mX;
temp.mY = mY * rhs.mY;
return temp;
}
MyVector MyVector::operator*(int multiplier) const
{
MyVector temp;
temp.mX = mX * multiplier;
temp.mY = mY * multiplier;
return temp;
}
MyVector& MyVector::operator*=(const MyVector& rhs) // 복합대입연산자는 자기 자신에게 대입될 필요가 있어서 참조형을 통해 복사 비용을 줄임. 즉, chaining이 필요하다면 참조형.
{
mX *= rhs.mX;
mY *= rhs.mY;
return *this;
}
MyVector& MyVector::operator*=(int multiplier)
{
mX *= multiplier;
mY *= multiplier;
return *this;
}
MyVector MyVector::operator+(const MyVector& other) const
{
cout << "MyVector::operator+() has been called." << endl;
MyVector temp = MyVector();
temp.mX = mX + other.mX;
temp.mY = mY + other.mY;
return temp;
}
MyVector operator*(int multiplier, const MyVector& v) // 상수 * 벡터의 경우. 벡터 * 상수가아님. 교환 법칙을 위해서.
{
MyVector temp;
temp.mX = v.mX * multiplier;
temp.mY = v.mY * multiplier;
return temp;
}
std::ostream& operator<<(std::ostream& os, const MyVector& rhs)
{
os << '(' << rhs.mX << ", " << rhs.mY << ')';
return os;
}
<hide/>
// main.cpp
#include <iostream>
#include "MyVector.h"
using namespace std;
int main()
{
MyVector v = MyVector(2, 5);
cout << v << endl;
return 0;
}
Note) 그러나, 연산자 오버로딩을 남용하진 말 것.
할 수 있는게 많아서 자유로울수록 실수가 많아짐.
ex. MyVector v = v1 << v2; // 대체 뭘하는 연산자인지?
Note) 연산자 오버로딩 대신 함수를 만들자.
시그니처를 잘 지으면 남들이 그 의도를 단박에 파악 가능.
2.5 암시적 함수 제거
2.5-1 대입 연산자
Note) 클래스에 딸려오는 암시적 함수들
- 매개변수 없는 생성자
- 복사 생성자
- 소멸자
- 대입 연산자(=)
Note) 대입 연산자도 엄청 자주 쓰기 때문에
암시적으로 컴파일러가 자동 구현해줌.
Def) 대입 연산자(Assignment Operator)
복사 생성자와 하는 일이 거의 동일.
그러나, 대입 연산자는 메모리 해제해줄 필요가 있을지도.
Note) 대입 연산자 Vs. 복사 생성자
오래된 개체(대입) Vs. 따끈한 새개체(복사)
만약 MyString 클래스가 있고, 멤버로 const char[]를 갖는다 해보자.
B 개체를 A개체에 대입해주려 한다면, 무작정 대입하면 안됨.
A 개체가 들고 있던 const char[]를 지워버리고(메모리 해제)
B 개체가 들고 있는 데이터에 맞게 새로운 메모리 만들고,
memcpy() 해야 안전한 대입이 가능함.
Note) 복사 생성자를 구현 했다면, 하는 일이 비슷한
대입 생성자도 구현 하는 것이 올바름.
정리하면, 복사 생성자 -> 소멸자 -> 대입 생성자
Note) 복사 생성자 호출 시기와 대입 생성자 호출 시기
개체가 새로 생성되어서 대입할 때는 복사 생성자가 호출됨.
그러나, 개체 두 개가 이미 생성 및 초기화가 이뤄지고
대입 연산자를 실행하면 대입 연산자가 호출됨.
여기 부분 필수로 다음에 내 포스팅에다가 정리하기..
Note) 암시적 operator=() 연산자는 깊은 복사일까, 얕은 복사일까?
2.5-2 암시적 함수들을 제거하는 방법
Note) 기본 생성자를 지우는 방법
1. 매개변수가 있는 생성자 오버로딩
class MyVector {
public:
MyVector(const int& x, const int& y);
...
};
2. private 접근 제어자로 매개변수 없는 생성자 선언
class MyVector {
private:
MyVector() {} // 정의는 안하고 선언만해도 컴파일러가 지우는 효과를 내줌.
...
};
Note) 방법 1은 엄밀하게 지운다는 느낌은 아님.
오버로딩을 함으로써 생기는 부가적인 효과.
방법 2가 비교적 지우는 느낌이 남.
아주 엄밀하게 지우는 방법은 아닌거 같음.
Note) 생성자와 마찬가지로, 복사생성자/소멸자/대입연산자도
접근 제어자로 private을 작성하면 지울 수 있음.
Note) 다만, 소멸자의 경우에는 주의가 필요함.
개체를 지역 변수로 선언하고, 스택 프레임이 반환될 때
자동으로 소멸자가 호출 될텐데 이러면 컴파일 에러.
그래서 절대 안지울 개체의 경우,
즉 프로그램 종료 때까지 들고 있으려는 개체의 경우에
사용하는 방법임.
'C++ > 문법 정리' 카테고리의 다른 글
Chapter 06. C++11/14/17 (0) | 2022.05.08 |
---|---|
Chapter 05. 템플릿과 파일 시스템 (0) | 2022.05.07 |
Chapter 04. 캐스팅과 인라인 (0) | 2022.05.06 |
Chapter 03. 상속과 다형성 (0) | 2022.05.05 |
Chapter 01. 입출력 기초 (0) | 2022.04.28 |
댓글