Eat Study Love

먹고 공부하고 사랑하라

SW 만학도/C++ & Algorithm

Review 4 - Class,Overloading,Special Members in C++

eatplaylove 2024. 7. 19. 23:08

https://eglife.tistory.com/87

 

Review 2 - Functions and Memory Management in C++

https://eglife.tistory.com/85 Review 2 - Container, Iteration in C++https://eglife.tistory.com/83 Review 1 - Basic Standard Library in C++(Cin/out,file I/O, String)이번엔 C++의 기초 복습이다. 사실 C++이 이름때문에 프로그래밍 언

eglife.tistory.com

 

금번엔 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의 StructC++ 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 object의 memory 모양

자세히 보면 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를 만들 수 있다.

SimpleVector() 이렇게 input 인자가 없는 Constructor가 있어야 아무것도 안 받는 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 이 이에 해당한다.

Protected 영역에 derived 같은 개념도 나중 복습내용에 나온다.

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이 어떻게 되는지 체크가 필요하다.

PRE/POSTFIX가 잘 보면 output 반환 type이 다르다. 하나는 value 하나는 reference

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-