Eat Study Love

먹고 공부하고 사랑하라

SW 만학도/C++ & Algorithm

Review 3 - Functions and Memory Management in C++

eatplaylove 2024. 7. 19. 16:35

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++이 이름때문에 프로그래밍 언어 중에 제일 끝판왕 격으로 어려운 줄 알았으나, C가

eglife.tistory.com

기초적인 C++의 Container들이 무엇이 있는지 배웠으니,

 

이제 그것들을 이용해서 각종 Fucntion을 어떻게 만들고, 그 때마다 Memory 할당은 어떻게 되는지 알아볼 시간!

 

1. Functions

 

#include <iostream>

using namespace std;
int divide(int a, int b = 2){
    return a/b;
}
int main(){

    cout<<divide(12)<<endl;
    //6
    cout<<divide(12,3)<<endl;
    //4
    return 0;
}

 

가볍게 시작하자, 함수에는 이렇게 default 값을 Parameter로 선언해줄 수 있다.

물론 당연히, default를 짓누르고 내가 원하는 값을 넣기도 가능

C를 배울때도 다뤘던 개념인데,

Call by value / Call by reference는 구분이 필요하다. Swap 함수처럼 변수 값을 직접 바꿀 때는 변수의 value가 아닌 address를 받아서 계산하는 call by reference작업을 해야한다.

 

void swap(int*a, int*b){
    int temp = *a;
    *a = *b;
    *b = temp;
}

int main(){

    int a = 10;
    int b = 20;

    cout<<"Before: " << a <<"\t" <<b<<endl;
    swap(&a,&b);
    cout<<"After: " << a <<"\t" <<b<<endl;
// Before: 10      20
// After: 20       10
    return 0;
}

 

C++에선 이렇게 Pointer를 이용해서 Call by reference를 쓰기도 하지만(just like C)

 

아래와 같이 Reference Parameter를 쓰기도 한다.

 

void swap(int &a, int &b){
    int temp = a;
    a = b;
    b = temp;
}

int main(){

    int a = 10;
    int b = 20;

    cout<<"Before: " << a <<"\t" <<b<<endl;
    swap(a,b);
    cout<<"After: " << a <<"\t" <<b<<endl;
// Before: 10      20
// After: 20       10
    return 0;
}

Reference를 사용한다는 건 alias(나의 분신)를 쓰는 것이다.

Reference의 동작이 헷갈린다면, 사실상 그냥 Pointer동작과 같다고 봐도 무방하다.

 

swap(int *a, int *b) -> swap(int &a, int &b) 이렇게 기호만 바뀌었을 뿐이다.

 

근데 이렇게 Reference를 가지고 놀다보면 의도치 않게 parameter값을 바꿔버릴 수도 있는데 이를 방지하고자 "const"를 써주기도 한다.

void swap(const int &a, int &b){
    int temp = a;
    a = b;
    // function.cpp:10:9: error: assignment of read-only reference 'a'
    //  a = b;
    b = temp;
}

const가 붙어 있는데, variable을 re-assign 하려고 하면 위와 같이 Error가 발생한다.

 

int& middleElement(map<string,int>&m){
//output이 reference이다.
    int idx = m.size()/2;
    auto it = m.begin();

    for(int i = 0 ; i<idx; i++){
        it++;
    }
    return it->second;

}

int main(){
    map<string,int> m2 = {
        {"a",3},
        {"b",6},
        {"c",9}
    };

    middleElement(m2) = 10; //referemce를 반환하는 거라 변경 가능

    for(auto p : m2){
        cout << p.first << " " << p.second << endl;
    }
// a 3
// b 10
// c 9

다소 헷갈리는 reference 사용.. 그치만 최대한 Pointer와 비슷하다는 개념으로 이해하고 잘 넘어가보자..

 

이 Reference사용은 Parameter들의 복사를 막고, direct하게 사용하는 것이라 large data-set을 다룰 때 memory management 측면에서 매우 효과적이다.

 

우리가 별 생각 없이 쓰고 있던 "cin" function도 사실은 reference parameter를 반환받는 것이라 chain rule이 가능한 것이다.

 

 

 

 

2. Function Overloading

 

C++에선 같은 함수이름을 사용해도 Parameter를 다르게 받으면 다른 동작을 할 수 있도록 설정이 가능하다.

(C와의 차이점)

C++에선 함수이름이 모두 동 ㅡ 일

C++ compiler가 함수이름을 받을 때, Parameter Type까지 한 번에 기억하기에 가능한 현상!

 

BUT! 사실 C++에서 저렇게 동일한 Method를 data type 다르다고 3번 써주는 것도 비효율적이다.

 

그래서 나온 해결책이 바로 아래 단계!

 

 

3. Function Templates

template <typename T>
void mySwap(T &x, T &y){
    T temp = x;
    x = y;
    y = temp;
}

이렇게 해주면 여러 Data type을 한 번에 통일이 가능하니까 훨씬 간결하다.

 

"T"의 경우 임의로 변경해도 상관이 없다.

 

int main(){

    int c = 100;
    int d = 2;

    mySwap(c,d); // mySwap<int>(c,d) 로 해주는 것이 좀 더 정확
    cout << "c:" << c << " d:" << d << endl;
// c:2 d:100

 

template <typename T1, typename T2>
void pr(T1 &x, T2 &y){

    cout << x << " " << y << endl;

}

int main(){
    int a = 1234;
    bool b = true;
    pr(a,b);
// 1234 1

 

위와 Function 안에 template을 여러 개 쓰는 것도 가능하다.

 

Function에 Template을 쓰면 좋은 점 , 안 좋은 점 정리!

 

 

 

 

4. Memory Management

기본적인 Runtime-Stack / Heap Memory 비교

C와 C++에서 Heap 할당 차이점을 보여주는 것이다.

 

기본적으로 C는 malloc 을 사용, C++은 new를 사용한다.

    int *ptr = new int;
    int *arr = new int[4];
    delete ptr;
    delete[] arr;

 

Single object, Array 일 때 initialize, delete하는 방법이 다르다. 귀찮지만 어차피 new 쓰는 게 2개 case밖에 없으니까 잘 숙지하고 있으면 되겠다.

 

#include <vector>
int main(){

    vector<int>* myVector = new vector<int>(); // () 소괄호를 붙쳐준다

    myVector -> push_back(123);
    myVector -> push_back(456);

    cout << myVector->front() << " "<< (*myVector)[1] << endl;
// 123 456

 

pointer는 C와 마찬가지로 *ptr. 또는 -> 를 이용해서 method를 사용한다.

 

vector 와 같은 container를 포인팅하는 포인터는 memory 할당해줄 때 소괄호를 사용해 initialize 진행한다.

    list<int>* mylist = new list<int>();

    mylist -> push_back(111);
    mylist -> push_back(333);

    for(int i : *mylist){
        cout << i << endl;
    }

    for(std::list<int>::iterator itt=mylist->begin();itt!=mylist->end();itt++){
        cout << *itt << endl;
    }

 

Pointer 를 이용해서 print하는 방법도 C와 크게 다르지 않다.

 

new로 Dynamic allocation 했으면 다 쓰고 variable을 delete( or delete[ ] ) 해주는 것만 잊지 말자.

unique_ptr, shared_ptr 처럼 pointer가 본인이 선언된 scope를 벗어나면 자동적으로 released 되는 smart pointer도 있지만, unique_ptr은 C++14 ver, shared_ptr은 C++11ver부터 지원한다.

 

 

5. Poiner ( C내용 복습 )

Pointer는 선언해줄 때 type이 중요하다.

ex : int -> 4bytes, char -> 1byte 라서 int, char을 가리키고 있는 *ptr은 각각 4, 1 byte만큼을 read한다.

Pointer 선언 시, 기본적인 Memory구조는 위와 같다.

 

5라는 int value 가 new에 의해 heap에 저장되면 그놈의 주소를 aPtr이 받는다.

 

그래서 aPtr 자체는 5가 저장된 heap 주소를 갖고 있고, dereference(*aPtr)을 하면 그 값으로 가서 5를 가져오는 것이다.

 

그리고 aPtr자체도 하나의 variable이라 고유의 memory address를 갖는데, 얘는 Dynamic 할당이 아닌 local variable이라서 runtime에 Stack에 저장되어있는 주소다.

array의 경우 * = [ ] 이다. 대괄호 한 번 쓰면 dereference를 한 번 한 것이고, *를 사용하면 마찬가지로 dereference를 한 번 한 것이라고 생각하면 된다.

 

arr은 지금 int value를 받고 있고, int value는 크기가 4byte이다.

 

그래서 arr+1을 했을 때 arr 주소가 +1 이 되는 것이 아니고, int 1칸의 크기를 4byte로 계산하기에 주소값 +4가 된다.

 

만약 char arr = new char[3] 이런 식이었으면 주소가 +1이 된다. char은 크기가 1byte이기에..

 

arr이라는 pointer variable은 역시나 Stack에 할당, 이 놈이 가리키는 동적할당된 arr[0]~[2] 이 놈들은 heap에 할당된다. NEW를 써서 만들어졌으니까 말이다!!

 

이 그림만 잘 이해하고 있어도 거진 Pointer는 정복했다고 보면 된다.

일단 double pointer 구조이다.

 

제일 중요한 것은, **ptr이 *ptr을 가리키는데, 포인터의 메모리 크기는 어떤 종류의 포인터건 무조건 8byte이다.

 

이 점을 잘 숙지하고 위 그림의 output을 함 봐보자.

 

일단 arr+1 -> arr이 가리키는 건 arr[0]의 주소. 근데 arr[0]도 arr[0][0]을 가리키는 포인터이다. 즉 arr은 어떠한 포인터를 가리키고 있다. 그래서 arr+1은 arr이 가리키는 주소에 +8byte를 한다.

 

*(arr+1)은 dereference 한 번 들어간다. 즉 e28(끝자리 기준)이 뭘 가리키는 지 한 단계 타고 가면 arr[1] 값을 이르면 된다 => fd0

 

*(arr+1) +2 는 deference한 번 타고 들어간 다음 주소값을 2만큼 더한다. 근데 derefer 한 번 타고 들어가면 arr[1] 얘네는 int value를 가리키기때문에 4byte단위다. 따라서 fd0 + 2*4bytes => fd8

 

*(*(arr+1)+2)를 하면 위 주소를 dereference 하라는 것이다. 그러면 해당 주소의 값이 읽힌다 => F

*은 [ ] 와 같다고 했다. arr[1][2] 는 그 위치에 있는 놈을 **와 같이 dereference 한다 => F

 

 

Stack 구조는 아래로 갈수록 memory 값이 커지고, int value는 4byte단위로 memory가 할당되는 걸 재확인할 수 있다.

 

메모리 값은 통상 16진수다. 0~F까지 사용한다!

 

그리고 여기 글에서 쓰는 메모리구조는 기본적으로 xFFFF ~ x0000까지는 한 칸당 0~F 가 16 = 2^4 라서 4bit 표현이 가능하다. 이런 놈이 4칸이니까 4bit*4 = 16bits = 2bytes 표현이 가능한 표를 쓰고있다.

 

 

- E. O. D -