금번엔 C++의 꽃!
Class에 대해서 관련된 것들을 심도있고 빠르게 파보겠다.
요놈부터가 사실상 C++의 Main Content라고해도 과언이 아니다.
위 질문에 대한 답변이..
1. Class
이다.
Class는 data와 function의 encapsulated 형태이고, data management가 간단히 될 수 있도록 도와준다.
Python OOP에서도 간단히 봤듯이, Class는 Object들의 Blueprint(일종의 설계도)이고, 여기는
1. Attribute( Data fields )
2. Method (Functions )
이 두 가지가 보통 정의되어 있다.
Object는 Class의 산물로, Runtime Instnace다. Class Object들은 유저가 뭐 설정하기도 전에 이미 Memory에 올라와 있는 녀석들이다.
이와 유사한 C의 Struct와 C++ Class의 차이는 아래와 같다.
일단, C는 struct 안에 method를 선언하지 못 한다. 그리고 typedef가 없다면 object 만들 때 "struct Triangle" 를 모두 타이핑해줘야 한다.
Class의 구성은 위와 같다.
각종 Pointer , Method( Function ) , Attribute( Variable ) 모두가 Class 안에서 정의되어 있다.
이 SimpleVector라는 Class를 직접 만들고, Object까지 생성해보자.
#include <iostream>
using namespace std;
class SimpleVector{
public:
// Attribute
int* arr;
int size;
int capa;
SimpleVector(int initialCapa):size(0),capa(initialCapa){
arr = new int[capa];
} // Constructor -> 추후 설명
~SimpleVector(){
delete[] arr;
} // Destructor -> 추후 설명
// Method
void addElement(int e){
if(size==capa){
resize();
}
arr[size] = e;
size++;
}
void resize(){
capa *= 2;
int *newArr = new int[capa];
for(int i=0;i<size;i++){
newArr[i] = arr[i];
}
delete[] arr;
arr = newArr;
}
int getSize(){
return size;
}
};
int main(){
SimpleVector vec(10); // initial Capa : 10
vec.addElement(5);
cout << vec.getSize() <<endl;
//1
return 0;
}
아으~ 이제부터 코드가 꽤나 길어진다.
구찮다 구찮아~ 다뤄야 할 게 너---무 많은 #CLASS !
자세히 보면 class 내에 attribute 선언 순서대로 object 내에 메모리가 형성되었다.
젤 먼저 선언된 arr의 경우 int* 포인터 타입이라 총 8byte만큼 크기를 차지하고 이놈의 첫 번째 부분 주소가 object 이름인 vec 그 자체가 가지고 있는 주소값과 동일하다.
그리고 size, capa가 각 4byte만큼의 크기를 이어서 contiguous 하게 가지고 있는 것을 볼 수 있다.
Attibute와 Method에는 어떻게 ACCESS할까?
Class 안 에서는 this라는 친구 + ->로 access가능하고, class 밖에서는 object와 dot operator로 접근이 가능하다.
2. Constructor & Destructor
Python의 __init__() 함수와 비슷한 개념이다.
Object의 Initial State를 설정해주는 것이라고 보면 되고, 보통 Class 이름과 같은 이름을 사용하여 만든다.
Object 만들 때, 자동으로 호출되는 Method라고 보면 된다.
Construct할 때 특정 값들을 std::initializer_list 로 받아서 초기화 하기도 한다.
이렇게 하면 한번에 object에 element를 여러개 넣어서 초기화 시킬 수 있다.
Construct Method를 만들어 주면, 그 틀에 맞게 object를 선언해야 한다.
Object가 괄호 안에 무언가를 받기로 해놓고, 아무 것도 없이 Object를 선언하면 Error가 난다.
Error를 없애려면 1. Class의 Constructor를 만들 때 아예 정의를 하지 않거나 2. Input이 없는 Constructor를 만들어 줘야 한다.
Constructor를 어떻게 만들었냐에 따라 총, 4가지 방식으로 Object를 만들 수 있다.
- Destructor
문자그대로 무언 가를 없애는 놈이다. Object의 활용이 끝났을 때 그 놈을 자동적으로 없애주며 한 클래스에 하나만 정의될 수 있다.
Destructor는 다음과 같은 case에 발동된다.
1. 자동으로 생긴 on-stack Object가 Scope를 벗어날 때(ex : function의 return 값)
2. Dynamic 하게 heap에 할당된 object의 메모리가 delete로 해제될 때!
etc.
※ 추가로 class 안에서도 특별히 별하지 않는 Varialbe with Method에 대해서는 const를 써주는 것이 권고된다.
ex : int getSize() const { return size; } 이렇게 하면 해당 Method를 통해선 Attribute 수정이 없다는 뜻이라 Compiler 작업에 효율화를 가져올 수 있다.
3. Access Specifiers
Class 안에 선언하는 Public, Protected, Private 이 이에 해당한다.
Private영역의 경우 더 이상 수정이 필요 없는 것들을 모아놔서 IF를 Simple하게 한다.(for ABSTRACTION!!)
4. Class Templates ( 중요! )
이 전 시간 Function에서도 특정 data type에서만 function을 정의하지 않고, template <typename T>를 써서 여러 data type에 대해 parameter를 받을 수 있게 했었는데, Class에서도 동일한 작용이 적용된다.
대신에 이렇게 template을 이용해 Class를 정의한 경우, Object를 만들 때 data type을 명시해줘야 한다.
int main(){
SimpleVector<int> vec1(10);
SimpleVector<double> vec2(10);
SimpleVector<string> vec3(10);
- Class Template을 썼을 때 장점?
1. Reusability : 많은 data type에 골고루 쓸 수 있으므로
2. Performance : 각 data type에 최적화 될 수 있게 알아서 type 별로 compile 된다.
3. Maintainability : 수정 필요하면 한 class만 고치면 된다.
4. Type Safety : Void Pointer 같은 pointer 사용이 줄게 되어 Error가 줄어든다.
5. Out - Of - Class Definition
#include <iostream>
using namespace std;
template <typename T>
class SimpleVector{
private:
T* arr;
int size;
int capa;
void resize(); // out of Def
public:
SimpleVector(int c);
SimpleVector(initializer_list<int> element);
~SimpleVector(){
delete[] arr;
}
void addElement(T element);
int getSize() const;
}; //요기까지 CLASS 선언하고, 안쪽 Method는 class 밖에다가 정의
template<typename T>
SimpleVector<T>::SimpleVector(int c):size(0),capa(c){
arr = new T[capa];
}
template <typename T>
SimpleVector<T>::SimpleVector(initializer_list<int> e):size(0),
capa(e.size()){
arr = new T[capa];
for(auto x : e){
addElement(x);
}
}
template <typename T>
SimpleVector<T>::~SimpleVector(){
delete[] arr;
}
template <typename T>
void SimpleVector<T>::addElement(T element){
if(size == capa){
resize();
}
arr[size] = element;
size++;
}
template <typename T>
void SimpleVector<T>::resize(){
capa *= 2;
T* newArr = new T[capa];
for(int i=0;i<size;i++){
newArr[i] = arr[i];
}
delete[] arr;
arr = newArr;
}
template <typename T>
int SimpleVector<T>::getSize()const {
return size;
}
int main(){
SimpleVector<int> vec = {1,2,3,4,5};
cout << vec.getSize() << endl;
// for(auto x : vec){
// }
return 0;
}
골자를 말하면, Class 안에 Method 들은 Declare만 해주고, method implementation은 class 밖에다가 적어주는 것이다. method implementation을 할 때마다 template을 다 적어줘야 하고, SimpleVector<T> Scope를 다 일일이 적어줘야 해서 보통 귀찮은 일이 아닐 수 없다.
다만 class 밖에 정의를 했음에도 class 내부의 attribute는 그 놈들이 private 영역에 있어도 맘대로 접근할 수가 있다.
어쨌건, 이 귀찮은 걸 도대체 왜 할까?
1. Class InterFace를 깔끔하게 만들 수 있다.
2. Class의 definition만 header file에 적고, implementation은 따로 source file에 빼서 head와 linking만 건 뒤에 활용가능(예제에선 한 file에 몰아서 만들긴 했음..ㅠ)
3. 2번의 이유로, method implementation의 변경이 필요할 때, source file만 수정하면 된다. 즉, User가 header file로만 code를 돌린다면 굳이 recompile 할 필요가 없다는 것이다.
하지만 귀찮다는 큰 단점이 있긴 하다 ㅎ;;;
6. Class Pointer
뭐 딴건 아니고,
그냥 Class를 이용해서 만든 Object를 Pointing 하는 Pointer를 뜻한다.
특이하게 new를 써도 new int[10] 이렇게 대괄호로 크기를 정해서 heap에 할당하는 게 아니고
new SimpleVector<int>(10) 이렇게 그냥 new 뒤에 object를 만들어 버린다.
희한하넹..
SimpleVector<int> *ptr1 = new SimpleVector<int>{1,2,3,4,5};
SimpleVector<int> *ptr2 = new SimpleVector<int>(5);
SimpleVector<int> vec{3,6,9};
SimpleVector<int> *ptr3 = &vec;
SimpleVector<int> *ptr4 = nullptr;
cout << ptr1->getSize() << endl;
cout << ptr2->getSize() << endl;
cout << ptr3->getSize() << endl;
cout << ptr4->getSize() << endl;
// 5
// 0
// 3
// Segmentation fault
Default nullptr로 선언하면 getSize method도 이용하지 못하는 無의 영역인가보다.
initialize list로 선언한 object를 포인팅하는 경우엔, list의 첫 element 주소를 Pointing 하는 pointer가 반환된다.
얘네도 역시 다 썼으면 main 내에서도 delete ptr;로 삭제해줘야 한다.
그리고 이 ptr을 main에서 가지고 놀 때에는, Class에서 Public에 정의된 Attribute, method만 call할 수 있다.
7. Operator Overloading ( 얘도 중요개념!! )
각 Data type 마다(Class 마다) Operator(+,-,* 등등)의 역할이 다 정의가 되어 있다.
우리가 임의로 만든 Class에도 이런 Operator들을 써서 특정 기능을 수행하게 하는 것이 Operator Overloading이다.
여러 가지 Operator Overloading Code를 한 번 짜보자..!
template <typename T>
T& SimpleVector<T>::operator[](int idx){
return arr[idx];
}
template <typename T>
SimpleVector<T>::operator bool() const{
return size >0;
}
int main(){
SimpleVector<int> *ptr1 = new SimpleVector<int>{1,2,3,4,5};
SimpleVector<int> *ptr2 = new SimpleVector<int>(5);
SimpleVector<int> vec{3,6,9};
SimpleVector<int> *ptr3 = &vec;
SimpleVector<int> *ptr4 = nullptr;
if(vec) cout << "TRUE"<<endl;
else cout << "FALSE";
cout << vec[1] <<endl;
//TRUE
//6
이런식으로 Operator를 만들어주기 전에, 일단 원본 Class에 Declaration은 해놔야 한다.
여기서는 생략되어 있는 것처럼 보이는데, 이게 빠지면 어차피 ERROR가 난다.
기타 등등 많은 operator들이 있는데, 각 oeprator의 in/output type이 어떻게 되는지 체크가 필요하다.
post++의 경우 일단 임시 arr temp를 복제해서 내보내는데, 함수 내부적으로 나 자신(this arr!)을 하나씩 더해주는 작업이 들어간다.
그래서 반환을 임시로 복사 떠놓은 나를 일단 내보내는데,
이 ++ operator가 끝나고 나면 나 자신은 1씩 증가해 있는 것이다.
결론적으로 operator 안에서 지지고 복고 해서 '나'가 return 될 때에는 output type이 SimpleVector<T>& 이다.
( with Return *this )
그냥 SimpleVector<T>를 반환하면 '나'의 복사본이 반환되는 것이라 Mermoy 주소가 꼬일 수 있다.
Operator 우변에 받는 Operand가 변수라면, const 를 안 써도 그만인데, 어떤 숫자를 집어 넣으려면 const가 있어야 한다. 솔직히 이유는 모르겠네;;
얘는 Class 안에서 declare 해 줄 때도 output type 따로 없이 적어준다.
우리가 흔히 Container에서 쓰던 begin(), end() 를 가리키는 Pointer도 직접 overloading으로 만들어 주는 것이다.
T* begin(){
return arr;
};
T* end(){
return arr+size;
}
이렇게만 class 내부에 정의해줘도, 우리가 흔히 쓰는 for문을 main에 썼을 때 얘네들이 begin()이 뭐고 end()가 뭔지 알아서 참고한다.
※ 참고로, reference의 경우는 C++에만 있는 특수병사다. pointer는 C/C++ 공통적으로 동적할당에 쓰이고 memory 주소 값을 갖고 있는 녀석이라면, C++의 Reference는 그냥 기존 object의 memory를 참조만 하고 있는 녀석이다. 얘를 다시 어디에 재할당 하는 것이 불가하고, 얘는 Pointer가 아니라 얘로 new/malloc을 써서 동적할당을 할 수 없다.
Referece &는 한 번 initialize 되면 평생 그 object만 참조하고 있는 C++만의 녀석이라고 보면 된다. 오로지 call by reference를 위해서 만들어진 놈! 이해하기가 좀 어려운데.. 받아드려야 한다 ㅠㅠ
그냥 reference는 initial object를 접근할 수 있는 하나의 alias라고 보면 된다. 얘는 포인터처럼 하나의 메모리공간을 차지하는 것이 아니고, C++ Compiler가 특수하게 원본 object를 수정할 수 있는 code를 부여한 것이다. 그냥 다른 이름으로 원본 object를 접근할 수 있는 녀석 그 이상/이하도 아니라고 보면 된다.
#include <iostream>
using namespace std;
int main() {
int a = 10;
int& ref = a; // ref is a reference to a
cout << "a = " << a << endl; // Outputs: a = 10
cout << "ref = " << ref << endl; // Outputs: ref = 10
ref = 20; // Modify the value of a through the reference
cout << "a = " << a << endl; // Outputs: a = 20
cout << "ref = " << ref << endl; // Outputs: ref = 20
return 0;
C++에서 reference는 따로 data type을 가지고 있지는 않다.
어쨌든, 다시 operator overloading으로 돌아오면..
우리는 지금까지 SimpleVector Class 안에 있는 member function에 대해서만 overloading을 했는데, 원한다면 global scope에서 overloading 할 수 있다.
그러면 죄다 이렇게 global scope에서 operator overloading 해주면 class 내부에 declaration도 필요 없어서 편할텐데.. 왜 굳이 class member function에 대해서만 overloading 했는지 싶다..
이렇게 global scope에 overloading을 하면 좋은 점은
1. class 수정 없이 operation을 추가할 수 있다는데... 솔직히 뭔 소린지 모르겠다. 아래를 참고하자 ㅠㅠ
template <typename T>
SimpleVector<T>& operator<<(SimpleVector<T>& l,SimpleVector<T>& r){
for(int i=0;i<r.getSize();i++){
l.addElement(r[i]);
}
return l;
}
template <typename T>
SimpleVector<T>& SimpleVector<T>::operator>>(SimpleVector<T>& r){
for(int i=0;i<r.getSize();i++){
this->addElement(r[i]);
}
return *this;
}
이게 구분이 있나 싶다..
위에 코드처럼 그냥 non-member 안 해도 상관은 없는데,, 뭔 차이가 있는지 모르겠다.
-E. O. D-
'SW 만학도 > C++ & Algorithm' 카테고리의 다른 글
[Main course!] Review 7 - Inheritance in C++ (0) | 2024.07.21 |
---|---|
Review 5 - Special Members in C++ (0) | 2024.07.20 |
Review 3 - Functions and Memory Management in C++ (1) | 2024.07.19 |
Review 2 - Container, Iteration in C++ (0) | 2024.07.18 |
Review 1 - Basic Standard Library in C++(Cin/out,file I/O, String) (0) | 2024.07.16 |