본문 바로가기

# Lang/C++

1.3 변수와 메모리 모델

 C++ 프로그램 메모리 모델

변수는 올바른 주소가 할당된 뒤에야 사용할 수 있다.

그런데 그 주소는 언제 할당될까?  아무 주소나 할당해도 될까?


이 궁금증을 해소하려면,  프로그램이 RAM에 어떻게 적재되는지 이해해야 한다.

아래의 그림은 RAM에 프로그램이 적재되는 모양에 관한 레이아웃이다.



프로그램이 RAM에 적재되면 주소의 최소치(low)와 최대치(high)이 주어지는데.

모든 데이터는 low와 high 안에서 저장되어야 한다.



만약 외부영역을 건들면 (=다른 프로그램의 영역을 건들게 되면,)

운영체제에 의해 강제로 종료되는것에 주의하자.


정리하자면,  프로그램 내의 모든 변수는 low와 high내에서 주소를 할당받고.

변수의 종류에 따라 할당되는 영역이 달라지게 된다.





 리터럴은 .text 영역에 저장된다.


리터럴은 Read-Only 변수이므로 .text의 not-excutable 영역에 저장된다.

.text 영역에 저장된 데이터는 수정할 수 없는것에 주의.


#include <iostream>

int main() {
    // "Hello, World!"는 문자 리터럴.
    // .text 영역에 저장된다.
    char* word = (char*) "Hello, World!";
    
    
    // char* 포인터로 값을 수정하려고 시도했지만.
    // .text 영역은 수정할 수 없으므로, 비정상 종료된다.
    word[0] = 'h';
    
    
}





 전역변수는 .data 또는 .bss 영역에 저장된다.


먼저 전역변수의 성질을 가지고 있는 변수들을 살펴보자.

각 경우마다 주의해야할 사항이 있기 때문이다.

  • global 
  • static global 
  • static local 
  • static class member 
1 >> 최상위에서 선언된 일반변수
2 >> 최상위에서 선언된 static 변수
3 >> 함수 안에서 선언된 static 변수
4 >> 클래스 안에서 선언된 static 멤버 변수


(공통점)

먼저 전역변수는 .data 또는 .bss 영역에 저장되는데,

초기화 여부에 따라 저장되는 영역이 다르다. (BSS : Block Started by Symbol.)


.data 영역과 .bss 영역은 static section이기 때문에,

이 영역은 실행파일에 포함되어 있으므로, 파일크기를 증가시키는 요인이 된다.


하지만 .bss 영역은 실행파일의 크기를 증가시키지 않는데,

uninitialized만 저장하므로, "몇 바이트만 쓸거야ㅡ"라는 정보만 파일에 적기 때문이다.


만약 .bss 영역이 100 byte를 사용해야 한다면,  실행파일에는 숫자 100만 적는 식이다.

프로그램이 적재될 때 100을 읽고,  100 byte를 할당한 뒤,  0으로 초기화 한다.



(차이점)

각 변수마다 아래 항목별 성질이 다르다.

  • 가시범위

변수를 이용할 수 있는 범위(Scope).
해당 범위를 벗어나면 변수를 참조할 수 없다.
  • 메모리 할당 조건 (정적영역, 파일안에 저장됨)

선언만 되어있는 전역 클래스 멤버는 컴파일 과정에서 무시된다.
외부에서 해당 이름으로 동일하게 중복선언해야 파일에 적힌다.
  • 변수 초기화 시점

static local을 제외한 변수는 메인함수 이전에 초기화된다.
static local은 처음으로 변수가 사용될 때 초기화된다.
  • 중복선언시 반응

서로다른 소스코드의 static 변수는 동일한 것으로 취급한다.
global 변수는 중복선언으로 인한 컴파일 에러가 발생한다.

(보충설명)

static local의 가시범위가 "구체화된 함수 안"이며,

템플릿<T>로 만들어진 함수는 T의 자료형마다 별도로 구체화된다는 것에 주의하자.

즉, <int>와 <double>에서 만들어진 static local 변수는 각각 다른 값을 가진다는 것 이다.



static local을 제외한 전역변수는 "메인함수 이전"에 초기화되지만,

어떤 변수가 먼저 초기화될지는 알 수 없다.





 지역변수는 .stack 영역에 쌓이고, 파기된다.


중괄호를 만날때마다 stack 영역에는 새로운 스택이 생성되고,

중괄호가 끝날때마다 스택이 파기된다.


중간중간에 만나는 지역변수나 매개변수는 스택에 쌓이고,

중괄호(또는 함수)가 끝날때마다 데이터채로 스택이 파기되는데,


이 과정에서 범위(scope)와 생명주기(lifetime)가 만들어지게 된다.

#include <iostream>

void foo(int a, int b);

int32_t main(){
    // 스택생성
    // stack : [ ]

    int a, b;
    // 지역변수 a, b를 스택에 삽입.
    // stack : [a, b]

    foo(a, b);
    // 함수 foo로 이동.

    std::cout << c << std::endl;
    // error
    // foo 안에 있는 c는 스택과 함께 파기되었음.

}
// 스택파기
// stack :


void foo(int a, int b){
    // 스택생성 및 매개변수 a, b를 스택에 삽입.
    // stack : [a, b] [a, b]

    int c;
    // 지역변수 c를 스택에 삽입.
    // stack : [a, b] [a, b, c]

}
// 스택파기
// stack : [a, b]








 동적할당은  .heap 영역에서 빌려쓰고, 반납한다.



new 키워드를 사용하면 heap 영역에서 메모리를 할당받는다.

할당받은 메모리를 반납하려면 delete 키워드를 사용.


#include <iostream>

int main() {
    // p는 heap 영역의 메모리를 가르키게 된다.
    int32_t *p = new int32_t;


    // delete 키워드로 메모리 반납.
    delete p;
    
}




 여유공간이 부족하면?


heap영역과 stack영역은 같은 여유공간을 공유한다.

그런데 만약 여유공간이 부족하면 어떻게 될까?


폭발(= 비정상 종료)한다!



stack 영역이 heap 영역을 침범하는 것을,  Stack-Overflow.

heap 영역이 stack 영역을 침범하는 것을,  Heap-Overflow. 라고 한다.



  • Stack-Overflow
#include <iostream>

void recursion(){
    // 무한히 스택을 잡아먹음.
    recursion();
}

int main() {
    // Stack-Overflow.
    recursion();
}


  • Heap-Overflow
#include <iostream>

int main() {

    // Heap-Overflow.
    while(true){

        // 동적할당이 무한히 이루어지지만,
        // 해제되지는 않음.
        new int32_t[1024];
    }
}




 heap 영역 VS Stack 영역

위의 내용을 정리하면,  동적할당은 heap에 할당되고,  지역변수는 stack에 할당된다.

그렇다면 데이터를 어느 영역에 저장하는 것이 더 좋을까?


답은, 케바케(Case By Case).

상황에 따라 다르다.



Stack 영역의 특징

  • 메모리 할당이 엄청 빠르다.

  • 스택파괴에 의한 데이터 생명주기가 존재한다.

  • 한번에 많은 메모리를 할당할 수 없다.


Heap 영역의 특징

  • 메모리 할당이 무겁고, 느리다. 

  • 메모리를 반납할때 까지 데이터가 살아있다.  (데이터 누수, Heap-Overflow의 위험)

  • 많은 메모리를 할당할 수 있다.





 생각해보기 (1) :  이게 왜 에러야?

아래 코드가 메모리 에러를 뿜는 이유는 무엇일까?

#include <iostream>

int main(){
    char* a = (char*) "Hello, World!";
    a[0] = 'H';
}




 생각해보기 (2) :  이게 왜 에러야?

아래 코드가 메모리 에러를 뿜는 이유는 무엇일까?
#include <iostream>

class Block {
public:
    explicit Block(int _val) :val(_val) {};
    int val=0;
};


Block* getBlock(){
    Block newBlock(5);
    return &newBlock;
}


int main(){
    Block* a = getBlock();
    std::cout << a->val << std::endl;
}








'# Lang > C++' 카테고리의 다른 글

2.2 자료형과 데이터 해석  (0) 2019.03.27
2.1 자료형  (0) 2019.03.26
1.2 변수의 생성  (0) 2019.03.21
1.1 변수의 정의  (0) 2019.03.20
모던 C++14(17) 학습목차  (0) 2019.03.20