[C++] 메모리 영역, 변수

2024. 3. 2. 14:59Programming Language/C++

아래 링크 클릭 시 해당 본문으로 이동

메모리 영역

변수(Variable)

정적 변수와 외부 변수는 분할 구현에 대해 먼저 알고 공부하는 것이 좋다.
참고 링크
분할 구현

메모리 영역

프로그램이 실행되려면 먼저 프로그램이 메모리에 로드(load)되어야 하고, 프로그램에서 사용되는 변수들을 저장할 메모리 공간도 필요하다.

• 로드(load)
CPU에서 빠르게 처리할 수 있게 속도가 느린 저장소(ex. 하드 드라이브 등)에서 속도가 더 빠른 메모리(RAM)로 데이터를 이동하는 작업이다.

컴퓨터의 운영체제는 프로그램의 실행을 위해 다양한 메모리 공간을 제공하고 있다.

프로그램이 운영체제(OS)로부터 할당받는 대표적인 메모리 영역은 스택 영역, 데이터 영역, 힙 영역, 코드 영역이 있다.

 

메모리 영역들이 누구에 의해서 사용되고 어떤 특징을 지니고 있는지 알아보자.

메모리 영역에 대한 개념을 확실히 이해하고 어떤 식으로 돌아가는지 파악하는 부분은 C/C++의 난이도가 높은 이유이기도 하다.

▶ 메모리 영역

메모리 영역 해당 메모리 영역을 사용하는 것 특징
스택(stack) 영역 지역 변수,함수 매개변수, 반환 주소 함수가 사용하는 메모리 영역
• 함수 호출 시 할당되고, 함수 종료 시 해제된다.
데이터(data) 영역 전역 변수, 정적 변수, 외부 변수 프로그램 시작 시 생성되고, 종료 시 해제된다.
힙(heap) 영역 동적으로 할당된 변수 프로그래머의 요청에 따라 동적으로 할당 및 해제된다.
코드(code) 영역 실행 가능한 코드 프로그램 시작 시 정적으로 할당되며, 실행 중에는 읽기 전용이다.

출처 : tcp school


더보기

메모리와 코드를 동일시하면 안 된다.

내가 작성하는 코드가 그 메모리 영역이라고 생각하는 경우가 있는데, 이는 잘못된 생각이다.

코드는 명령어의 집합이고, 메모리는 명령에 맞춰 동작한다.

 

프로그래머가 코드를 입력하면 컴퓨터는 명령을 수행하면서 메모리 공간에 변수를 생성시키기도 하고, `main()`가 호출될 때 사용할 만큼의 메모리를 잡는 등의 일을 한다.

즉, 컴퓨터는 코드에 작성한 명령대로 메모리 영역을 조작 · 관리하고 값을 넣는다.


1. 스택(stack) 영역

지역 변수가 사용하는 메모리 영역

+) 함수 매개변수, 반환 주소

• 함수가 사용하는 메모리 영역 = 스택 영역

스택 영역 특징

• 함수가 호출되면 자동으로 할당되고, 함수가 종료되면 해당 함수의 스택 영역이 해제된다.

스택 영역 vs. 스택 프레임

  스택 영역(Stack Memory Area) 스택 프레임(Stack Frame)
정의 함수 호출을 관리하기 위해 사용되는 스택의 전체 메모리 영역 함수 호출할 때 할당된 스택 영역의 특정 부분
저장되는
데이터
함수 호출 정보, 지역 변수, 임시 데이터 등 지역 변수, 함수에 전달되는 매개 변수, 반환 주소(함수가 끝난 후에도 계속 실행될 수 있게) 등
크기 전체 프로그램(운영 체제 또는 컴파일러)에 따라 달라진다. 함수의 지역 변수, 매개 변수 등에 따라 달라진다.
예시 책장 전체 책장에 서랍 하나

• 함수 호출 시 ➜ 해당 함수의 스택 프레임이 생성되고 스택 영역에 push된다.

• 함수 반환 시 ➜ 해당 스택 프레임이 스택 영역에서 pop된다.


2. 데이터(data) 영역

전역 변수, 정적 변수, 외부 변수가 사용하는 메모리 영역

데이터 영역 특징

프로그램 시작 시 생성되고 종료 시 해체된다.

맨 처음에 프로그램이 시작될 때(`main()`가 호출될 때) 데이터 영역이 만들어지고 해당 변수의 공간이 생긴다.

∴ 데이터 영역은 프로그램이 실행되는 도중에는 계속 존재하다가 프로그램이 종료되면 없어진다.


스택 영역 vs. 데이터 영역

메모리 영역 프로그램 시작 시 함수 호출 시 함수 종료 시 프로그램 종료 시
스택 영역 - 생성 해체 -
데이터 영역 생성 존재 존재 해체

스택 영역은 함수가 호출되면 생성되고 종료하면 해체되는 것을 반복한다.

그것과 상관없이 데이터 영역에 있는 변수는 프로그램이 시작되고 종료될 때까지 계속 존재한다.

 

Q. 함수가 실행될 때마다 변숫값이 하나씩 증가하는 함수를 만들어서 함수를 3번 호출하면 두 경우 모두 변숫값이 3으로 증가할까?

A. 데이터 영역일 때만 변숫값이 3으로 증가한다.

▷ 예시

ⓐ 스택 영역

#include <iostream>

void Test()
{
	int a = 0;
    ++a;
    cout << "a = " << a;
}

int main()
{
	Test();
    Test();
    Test();
    
    return 0;
}

▽ 출력 결과

a = 1
a = 1
a = 1

`Test()` 스택이 생성되고 해체되는 것을 반복하기 때문에 함수가 호출될 때마다 출력되는 `a`값을 확인해 보면 각각 1이 나온다.

▷ 과정

① 첫 번째 `Test()`가 호출될 때 지역 변수 `a`의 공간이 생긴다.

`int a = 0;` : `a`의 초기값은 0이다.

② `++a` ➜ `a`가 1로 증가한다.

③ `Test()`의 `}` → `Test()`가 종료되면 해당 함수 스택도 없어진다.

스택이 없어졌으니 그 안에 있는 값도 없어진다.

두 번째, 세 번째 `Test()`도 마찬가지로 함수 스택이 만들어졌다가 없어진다.

a는 1

 

ⓑ 데이터 영역

#include <iostream>

int gInt = 0;

void Test()
{
    ++gInt;
}

int main()
{
	Test();
    Test();
    Test();
    
    cout << "gInt = " << gInt;
    
    return 0;
}

▽ 출력 결과

gInt = 3

`gInt`값은 3이므로 데이터 영역에 있는 변숫값은 함수가 종료되어도 존재한다는 것을 알 수 있다.

▷ 과정

전역 변수 `gInt`는 프로그램이 시작함과 동시에 생성된다.

`int gInt = 0;` : `gInt`의 초기값은 0이다.

① 첫 번째 `Test()` 호출

② `++gInt` ➜ gInt가 1로 증가

③ `Test()` 종료 ➜ 해당 함수는 종료되어 함수 스택은 없어졌지만 `gInt`는 계속 존재한다.

④ 두 번째 `Test()` 호출

⑤ `++gInt` ➜ gInt가 2로 증가

⑥ `Test()` 종료

⑦ 세 번째 `Test()` 호출

⑧ `++gInt` ➜ gInt가 3으로 증가


3. 힙(heap) 영역

동적 할당된 변수가 사용하는 메모리 영역

• `malloc()` 등의 함수에 의해 동적으로 할당되는 영역이다.

힙 영역 특징

• 런타임 중 동적 메모리 할당에 사용되며 사용자가 직접 관리해야 하는 영역이다.

즉, 프로그래머에 의해 메모리 공간이 동적으로 할당 및 해제된다.

• 메모리가 할당되는 방향은 메모리의 낮은 주소에서 높은 주소로 할당된다.


4. 코드(code) 영역

• 실행 가능한 코드가 사용하는 영역

• 텍스트 영역이라고도 한다.

코드 영역 특징

• CPU는 코드 영역에 저장된 명령어를 하나씩 가져가서 처리한다.

• 실행할 코드가 저장되어 있는 영역이므로 기계어 형태로 존재한다.

 프로그램 시작 시 정적으로 할당되며, 실행 중에는 읽기 전용이다.


변수(Variable)

데이터를 저장할 수 있는 메모리 공간

• 지역 변수와 전역 변수를 나누는 기준은 함수이다.

+) 변수명은 쓰고 싶은 대로 아무거나 쓰면 된다.


변수를 사용하려면 변수를 먼저 선언해야 된다. (ex. 값 대입할 때)

• 변수 선언하는 법 : 자료형 변수명;

변수 선언만 했을 때 해당 변수에는 아무 의미 없는 값들이 들어있다.

변수 선언 및 초기화하는 법 : 자료형 변수명 = 초기값;

특별한 이유(ex. 초기화하면 안 되는 경우)가 아니라면 변수 선언만 하기보다는 초기값을 줘서 초기화까지 하는 것이 좋다.

int main()
{
	int i;  // 변수 선언
    i = 50;  // 변수 초기화
    
    unsigned char c2 = 0;  // 변수 선언 및 초기화
    c2 = 255;
    
    return 0;
}

변수명 규칙

서울특별시에 같은 이름을 가진 구(ex. 마포구)가 없는 것처럼 변수명도 마찬가지다.

같은 지역(함수)에 같은 변수명을 쓸 수 없다.

int main()
{
	int abc = 0;
    int abc = 0;  // 오류
    
	return 0;
}

변수명 오류 - 재정의

 

서로 다른 지역에 같은 변수명은 가능하다.

int main()
{
	int abc = 0;  // main()
    int a = 5;
    
    if (a == 5)
    {
    	int abc = 3;  // if문
        abc;  // if문
    }
    
    abc;  // main()

	return 0;
}

▶ 변수

변수 사용하는 메모리 영역 설명
지역 변수 스택 영역  함수 안에 선언된 변수
• 해당 함수가 호출될 때 그 함수 스택 메모리 안에 존재한다.
함수가 종료되면 같이 없어진다.
전역 변수 데이터 영역  함수 밖에 선언된 변수
• 프로그램 시작될 때부터 종료될 때까지 메모리에 존재한다.
정적 변수 데이터 영역 • 키워드 : static
 선언된 위치에서만 동작한다.
• 프로그램 시작될 때부터 종료될 때까지 메모리에 존재한다.
외부 변수 데이터 영역 • 키워드 : `extern`
모든 파일에서 인식할 수 있다.

• 프로그램 시작될 때부터 종료될 때까지 메모리에 존재한다.

1. 지역 변수

지역(함수) 안에서만 사용할 수 있는 변수

• 사용하는 메모리 영역 : 스택 영역

지역 변수는 해당 함수가 호출될 때 그 함수 스택 메모리 안에 존재한다.

함수가 종료되면 같이 없어진다.


2. 전역 변수

• 전역에서 사용할 수 있는 변수

함수 밖에 선언된 변수

• 사용하는 메모리 영역 : 데이터 영역

• 프로그램 시작될 때부터 종료될 때까지 메모리에 존재한다.


정적 변수와 외부 변수는 특정 키워드와 함께 쓰인다.

3. 정적 변수

• 키워드 : `static`

static int a = 0;

• 사용하는 메모리 영역 : 데이터 영역

선언된 위치에서만 동작한다.

 

▶ 정적(static) ↔ 동적(dynamic)

동적은 움직인다는 의미이고, 반대로 정적은 움직이지 않는다는 의미이다.

Q. C++에서 움직이지 않는다는 것은 어떤 의미일까?

A. 선언된 위치에서 움직이지 않는다는 뜻이다.

변수 앞에 정적이라는 말이 붙은 정적 변수는 다른 파일에 선언된 정적 변수를 가져올 수 없다는 뜻이다.


Q. 정적 변수도 전역 변수처럼 데이터 영역을 사용하는데 두 영역의 차이점은 무엇일까?

A. 정적 변수는 함수 밖과 안에 선언되어 있을 때 각각 사용할 수 있는 위치가 다르다.

(전역 변수처럼) 정적 변수가 함수 밖에 선언되어 있을 때

• 정적 변수 선언 및 동작 위치 : 해당 cpp 파일

func.cpp

static int gStatic = 0;

int Add(int a, int b)
{
	gStatic;
	return a + b;
}

`func.cpp`의 `gStatic`은 `func.cpp` 파일에 선언된 것이다.

 

main.cpp

static int gStatic = 0;

int main()
{
	gStatic;
    
	return 0;
}

`main.cpp`의 `gStatic`은 `main.cpp` 파일에 선언된 것이다.

 

`gStatic`으로 변수명이 같아도 선언되어 있는 위치와 사용할 수 있는 위치는 각각의 변수가 속해있는 해당 파일이다.

각자 선언된 위치에서 움직이지 않고 틀어박혀있다고 생각하면 된다.

때문에 전역 변수에서 발생했던 문제가 정적 변수에서는 발생하지 않는다. (cf. 분할 구현)

  전역 변수 정적 변수
링크 단계에서 코드가 하나로 합쳐졌을 때
같은 이름이 여러 개인 경우
중복 문제 발생 문제없음.

정적 변수가 함수 안에 선언되어 있을 때

정적 변수 선언 및 동작 위치 : 정적 변수가 선언된 해당 함수 안

main.cpp

void Test()
{
	int a = 0;
	static int i = 0;
}

지역 변수 `a`는 `Test()`에 선언되어 있다.

`a`는 `Test()`가 호출되면 스택 메모리에 함수 스택이 만들어지고, `Test()` 안에서만 동작한다.

반면, 정적 변수 `i`는 호출 스택에 포함되어있지 않기 때문에 함수의 호출과 종료와 상관없이 계속 유지된다.

 

▷ 예시

main.cpp

int gInt = 0;  // 전역 변수

void Test()
{
	static int i = 0;  // 정적 변수
	i = 50;

	int a = 0;
}

int main()
{
	gInt = 10;
	i = 20;  // 오류
}

`Test()`에 선언된 정적 변수 `i`는 `Test()` 안에서만 사용할 수 있기 때문에 `main()`에 있는 `i`에 문법적으로 오류가 발생한다.

오류


전역 변수 `gInt`를 이용하여 프로그램이 진행되는 동안의 `Test()`를 몇 번 호출했는지 출력하는 코드

Q. 아래 코드에서 `Test()`의 호출 횟수는 몇 번일까?

int gInt = 0;

void Test()
{
	++gInt;
}

int main()
{
	Test();
	Test();
	Test();
	Test();
	Test();

	printf("Test() 함수 호출 횟수 : %d\n", gInt);  // Test() 함수 호출 횟수 : 5

	return 0;
}

A. 5번

코드 실행 결과 : 함수 호출 횟수 = 5

 

Q. `printf()` 윗줄에 전역 변수 `gInt`에 0을 넣으면 함수 호출 횟수는 얼마로 나올까?

int gInt = 0;

void Test()
{
	++gInt;
}

int main()
{
	Test();
	Test();
	Test();
	Test();
	Test();

	gInt = 0;  // gInt에 0을 넣는다.
    
	printf("Test() 함수 호출 횟수 : %d\n", gInt);  // Test() 함수 호출 횟수 : 0

	return 0;
}

A. 0

`printf()`가 실행되기 전에 `gInt`에 0을 넣었기 때문에 0으로 나온다.

코드 실행 결과 : 함수 호출 횟수 = 0

정적 변수의 기능 : 접근을 제한한다.

전역 변수는 다른 함수에서도 접근할 수 있다.

그래서 다른 함수에서 `gInt`의 값을 변경하지 않는다는 보장이 없다.

팀으로 같이 코드를 작성할 때 팀원이 다른 함수에서 `gInt`값을 변경하는 일이 발생할 수도 있지만, 코드가 점점 복잡해지면 내가 실수할 수도 있다.

 

이때 정적 변수를 사용하여 접근을 제한하면 실수를 줄일 수 있다.

함수 안에서 정적 변수를 사용하면 함수를 수정하지 않는 한 해당 변수를 접근할 방법이 없다.

정적 변수처럼 실수를 할 수 없게 만드는 것도 기능이다.

유지보수, 안정성, 방어적 코드를 위해 다른 곳에서 함부로 접근할 수 없고, 특정 상황에서만 수정 가능하게 하고 싶을 때 정적 변수를 사용하면 된다.


▷ 정적 변수 `i`를 이용하여 `Test()` 호출 횟수를 구하는 코드

#include <stdio.h>

int Test()
{
	static int i = 0;
    ++i;
    
    return i;
}

int main()
{
	Test();
    Test();
    Test();
    Test();
    int iCall = Test();  // 변수 i값(함수가 호출된 횟수)을 변수 iCall에 넣는다.
    
    printf("Test() 함수 호출 횟수 : %d\n", iCall);  // Test() 함수 호출 횟수 : 5
    
    return 0;
}

Q. 정적 변수를 함수 안에서 사용할 때 해당 함수 안에서만 존재한다고 했는데, `Test()`를 호출할 때마다 `i`는 계속 0으로 초기화될까?

A. 아니다.

컴파일이 처음 한 번만 초기화시키고 그다음부터는 건너뛰게 한다.

코드 실행해 보면 `Test()`가 호출될 때마다 `i`가 0으로 초기화되는 것이 아님을 알 수 있다.

코드 실행 결과 : 함수 호출 횟수 = 5


4. 외부 변수

• 키워드 : `extern`

extern int b = 0;

• 사용하는 메모리 영역 : 데이터 영역

모든 파일에서 인식할 수 있다.


헤더 파일의 변수와 cpp 파일의 변수

common.h

헤더 파일 : 모든 파일에서 접근할 수 있다.

#pragma once

static int gStatic = 0;  // 정적 변수

 

main.cpp

`gStatic`에 100을 넣는다.

#include "common.h"  // static int gStatic = 0;

int main()
{
	gStatic = 100; 
    Add(0, 0);  // 같은 변수인지만 확인하기 위해 추가한 코드 (값에 영향을 주지 않는다.)
    
    return 0;
}
`Add(0, 0);`에서 `Add`에 커서를 놓고 F12를 누르면 `Add()`가 선언된 위치로 이동한다.

 

func.cpp

`gStatic`의 값이 무엇인지 출력한다.

#include <stdio.h>
#include "common.h"  // static int gStatic = 0;

int Add(int a, int b)
{
	printf("gStatic의 값 : %d", gStatic);  // gStatic의 값 : 0
    return a + b;
}

 

Q. `main.cpp`에 있는 `gStatic`과 `func.cpp`에 있는 `gStatic`은 같은 변수일까?

만약 같다면 100이 나와야 한다.

A. 서로 다른 변수이다.

`main.cpp`에 있는 `gStatic`은 100이고, `func.cpp`에 있는 `gStatic`은 0이다.

main.cpp

gStatic에 100을 넣었을 때 = 100

 

func.cpp

Add() 함수 실행 = 0
코드 실행 결과


아래 강의 영상 캡처 사진을 보면 `gStatic`의 값에 100이 들어있다.

Add() 함수 안에 있는 gStatic에 100이 들어있음.
출처 : 유튜브 '어소트락 게임아카데미'

이는 인텔리센스(intellisense) 버그 때문이다. 디버깅을 도와주는 툴인 인텔리센스가 원래는 다른 것인데 같은 것으로 인식하는 비주얼 스튜디오의 버그이다.

내가 실행했을 때는 버그가 뜨지 않아서 강의 영상을 캡처했다. 강의 영상이 올라온 연도는 2021년이기 때문에 아마 해당 버그가 고쳐진 것이 아닐까 하는 게 나의 생각이다.


모든 파일에서 인식할 수 있으면서 데이터 영역에 있는 변수

정적 변수는 데이터 영역에 있지만 모든 파일에서 인식할 수는 없다.

함수 밖에 있을 때는 해당 cpp 파일에서만, 함수 안에 있을 때는 해당 함수 안에서만 사용 가능하기 때문이다.

외부 변수도 데이터 영역에 있는 변수인데, 모든 파일에서 인식하는 것도 가능할까?

 

common.h

#pragma once

extern int gExtern = 0;  // 오류

Q. 외부 변수를 초기화했을 뿐인데 오류가 발생하는 이유는?

오류

A. 헤더 파일에서 외부 변수를 초기화했기 때문이다.

헤더 파일에 있는 외부 변수에는  초기값을 줘서 초기화하면 안 된다.

 

Q. 초기화하면 안 되는 이유는?

A. 링킹(linking) 단계에서 파일이 다 합쳐질 때 해당 변수가 여러 번 정의되었다고 판단했기 때문이다.

링킹(linking) 단계 : 컴파일이 진행될 때 코드들을 연결(link)해 주는 단계로, 모든 파일들이 합쳐진다.

∴ 헤더 파일에서는 외부 변수를 선언해야 된다.

※ 주의할 점 : 선언했다고 확정 지은 것이 아니다.

이런 변수가 있다고 알려주는 정도이다.

 

Q. 선언해도 확정이 아니라 있다는 정도면 변수는 실제로 어디에 있는 것일까?

A. 헤더 파일에 선언했던 외부 변수를 초기화한 다른 파일

 

Q. 만약 헤더 파일에 선언만 하고 다른 파일에서 초기화하지 않으면 어떻게 될까?

A. 오류가 발생한다.

오류

실체가 없기 때문에 링킹 단계에서 오류가 발생한다.

파일들이 합쳐질 때 어딘가에 있기만 하면 돼서 딱 한 번만이라도 선언되어 있으면 된다. (초기화도 가능)


외부 변수가 '모든 파일에서 인식할 수 있으면서 데이터 영역에 있는 변수'라면 어느 파일이든 오류 발생 없이 사용 가능하고 함수가 종료돼도 값이 존재해야 된다.

common.h

#pragma once

static int gStatic = 0;  // 정적 변수

// gExtern이라는 변수가 있다.
extern int gExtern;  // 초기값을 주면 안 된다.

 

func.h

`Add()` 선언

#pragma once

int Add(int a, int b);

 

func.cpp

`gExtern`의 값이 얼마인지 출력한다.

#include <stdio.h>
#include "common.h"  // static int gStatic = 0;
					  // extern int gExtern;

int Add(int a, int b)
{
	printf("gStatic의 값 : %d", gStatic);  // gStatic의 값 : 0
	printf("gExtern의 값 : %d", gExtern);  // gExtern의 값 : 300
    return a + b;
}

 

main.cpp

`gExtern`에 300을 넣는다.

#include "common.h"  // static int gStatic = 0;
					  // extern int gExtern;
#include "func.h"

int main()
{
	gStatic = 100;
	gExtern = 300;
    Add(0, 0);
    
    return 0;
}

 

test.cpp

외부 변수가 실제로 존재하는 파일

int gExtern = 0;  // 외부 변수 초기화 (선언만 해도 된다.)

오류 발생 X

오류가 발생하지 않으므로 외부 변수는 '모든 파일에서 인식할 수 있으면서 데이터 영역에 있는 변수'이다.

 

출력 확인 결과 `gExtern`의 값이 잘 들어가 있다.

gStatic은 0, gExtern은 300


게임을 만들다가 변수 때문에 문제가 발생하는 경우
캐릭터 공격력을 1000으로 정했는데 실행하면 자꾸 0이 된다면 변수를 의심해 볼 필요가 있다.