본문으로 바로가기

Day-9 메모리 할당, 다차원 포인터

category Programming/TIPS 17기 2017. 7. 28. 11:52

 

 

 

프로그램과 프로세스

 

프로그램(Program) : 프로그래머가 만든 프로그램 실행 파일이다.

프로세스(Process) :

- CPU가 실행 파일에 있는 명령들을 실행할 수 있도록 운영체제가 실행 파일의 명령들을 읽어서 메모리에 재구성 한것이다.

- '실행 중인 프로그램'이라고도 한다.

- 세그먼트의 집합으로 구성되어 있다.

세그먼트 :

- 여러 가지 정보나 사용자가 입력한 데이터를 기억하는 메모리 공간이다.

- 코드 세그먼트, 데이터 세그먼트, 스택 세그먼트로 이루어져 있다.

 

힙 세그먼트는 낮은 주소에 높은주소로 메모리가 할당 된다. 또한 크기가 고정되어 있지 않고 유동적이다.

스택 세그먼트는 높은 주소에서 낮은주소로 메모리가 할당 된다. 크기가 고정적이다.

 

메모리 할당

 

데이터를 저장할 공간을 적정하게 나누는 작업이다.

 

정적 메모리 할당 (Static Memory Allocation)

 

컴파일러가 소스코드를 기계어로 번역하는 시점에 변수의 크기에 맞게 메모리를 할당하는 것을 의미한다.

프로그램이 실행될 때 메모리의 위치가 결정된다.

프로그램 실행 중에 할당된 메모리의 크기는 변경이 불가능하다.

 

변수가 메모리에서 유지되는 시간

 

전역 변수

 

프로그램이 시작해서 종료할 때까지 유지된다.

프로그램 실행 중에 전역 변수를 추가 혹은 삭제가 불가능하다

프로세스의 데이터 세그먼트에 저장된다.

 

지역 변수

 

함수가 호출될 때마다 메모리에 할당되었다가 함수가 끝나면 사라진다.

프로세스의 스택 세그먼트에 저장된다.

 

정적으로 할당된 메모리를 관리하는 법

 

지역 변수를 사용하려면 그 변수의 현재 주소를 알아야 한다.

각 지역 변수의 현재 주소를 기억하려면 지역 변수의 개수만큼 메모리가 더 필요하다.

같은 함수에 선언한 지역 변수들을 하나의 메모리 그룹으로 관리가 가능하다.

지역 변수를 시작 위치(Start) 포인터와 끝 위치(End) 포인터를 사용하여 관리 가능하다.

아래 그림과 같이 변수가 여러개처럼 보이지만 기계어로 번역 되면 a,b,c 각각 주소값을 바뀌는 것이 아닌 [a의 주소값 + ]형태이다.

 

END 포인터가 필요한 이유가 무엇일까?

 

Start 주소값에서 + 를 통해 주소를 더해 나가면 END 포인터의 필요성을 느끼지 못 할 수 있다.

하지만 아래와 같은 경우를 보자.

즉, Test 함수가 또 다른 함수를 호출할 경우 메모리 위에 A함수를 위한 공간을 할당해야 하기 때문이다.

 

 

 

스택에 대하여

 

스택(Stack)은 자료 구조(Data Structure)의 한 종류이다.

두 개의 포인터로 많은 양의 데이터를 효과적으로 관리하는 이론이다.

베이스 포인터(Base Pointer, BP)를 기준으로 데이터를 추가할 때마다 순서대로 데이터를 쌓아 올린다.

스택 포인터(Stack Pointer, SP)로 새로운 데이터가 추가될 위치를 가리킨다.

 

기계어로 번역되면 Start, End 포인터가 아니라 BP, SP로 사용된다. 데이터가 추가 되면 sp가 위로 올라간다.

만약 SP, BP가 같다면 데이터가 없다는 것이다.

 

PUSH : stack에 데이터를 추가한다.

POP : statck에 데이터를 제거한다.

 

컴퓨터가 관리하는 메모리양은 굉장히 많다. 변수가 네 개 있는데 변수의 메모리 저장을 위해 변수의 주소값을 저장할 메모리를 할당하면 비효율적이다. 따라서 stack이라는 공간을 만들 때 가장 효율성을 생각해서 만든것 이다. stack 공간만 PUSH, POP을 이용한다.

 

자료구조(이론) vs 컴퓨터 시스템(실제)

 

컴파일러가 지역 변수를 저장할 메모리 공간을 확보하는 방법

 

 

 

 

 

sub 명령과 add 명령을 사용하는 방법

 

 

 

 

우선 C언어를 어셈블리어로 번역한다.

int a,b,c --> stack에 a, b, c가 들어갈 세 개의 공간을 만들어야 한다. 따라서 PUSH, POP을 세번 씩 해야한다. 변수가 많아질수록 비효율적이다.

 

C언어는 C언어 구조상 크기 예측이 가능하다.

sub SP, 12 --> SP-12    //  PUSH와 같은 기능을 한다.

add SP, 12 --> SP+12    //  POP과 같은 기능을 한다.

PUSH, POP을 이용하면 변수의 개수가 많을수록 호출 속도가 느리겠지만 sub, add를 사용하면 변수의 개수에 속도가 영향을 받지 않는다.

 

컴파일러가 스택에 할당된 지역 변수를 사용하는 원리

 

C 언어 컴파일러는 지역 변수가 선언된 순서대로 메모리를 할당한다.

최근에 저장된 스택 포인터(SP) 바로 아래의 데이터부터 차례대로 꺼낼 수 있다.

 

위와 같은 소스 코드에서 변수 b의 값을 5로 변경할 때 컴파일러가 수행하는 과정을 알아보자.

 

 

위와 같은 그림처럼 원래 자료구조 개념에서 c를 빼고 b를 빼고 b의 값을 바꾸고 b를 집어 넣고 c를 집어 넣는 방식이다.

하지만 이 방싱은 변수가 많을수록 비효율적이다. 따라서 간접주소방식(포인터)를 이용해 위처럼 사용하지 않고 값을 대입할 수 있다.

 

베이스 포인터를 사용하여 스택에 할당된 지역 변수 사용하기

 

스택 메모리도 간접 주소 지정 방식(포인터) 개념을 이용하여 값을 읽거나 저장할 수 있다.

정적 메모리 할당은 stac이라는 구조를 사용한다. PUSH, POP은 비효율적인 부분이 많기 때문에 BP, SP를 이용한다.

BP는 주로 고정되어 있고 SP는 계속 변한다.

 

함수를 호출할 때 스택 메모리가 변화하는 과정

 

함수의 실행

 

 

함수의 종료

 

 

 

main 함수에서 Test 함수를 호출하면 지금까지 자기 위치를 기록한다. 기계어가 수행된 위치를 말하는 지점을 ip라고 한다.

그 후 PUSH BP를 한다. 왜냐하면 main 함수에서 Test 함수로 제어권이 넘어가 Test 내에 있는 변수를 사용해야 한다.

Test 함수가 실행되면서 변수가 선언될 때마다 SP는 계속 올라가고 Show함수를 호출할때 마찬가지로 PUSH IP, PUSH BP를 한다.

 

명령어도 메모리 먼지에 올라간다. [번지 push ax] 이러한 형식으로 사용한다. 여기서 번지는 ip이다.

어셈블리어에서 ret 위에 한 줄을 사용할 수 있는데 이것이 리턴값으로 쓰인다.

스택 프레임은 운영체제가 제공하는 것이 아니라 C언어가 제공하는 것이다.

 

정적 메모리 할당의 한계

 

프로세스 안에서 지역 변수가 저장되는 기본 스택(Stack) 메모리 크기는 1Mbyte이다.

단일 변수의 크기가 1MByte를 넘는 경우에 오류 메시지를 출력한다.

함수 내의 모든 변수의 크기를 합산하여 1MByte를 넘는 경우에는 프로그램 실행 시 오류가 발생한다.

 

동적 메모리 할당(Dynamic Memory Allocation)이란?

 

원하는 시점에 원하는 크기만큼 힙(Heap)에 메모리 할당 가능하다.

메모리 사용이 끝나면 언제든지 할당한 메모리 해제 가능하다.

heap 메모리는 스택 프레임 하고 상관없다. 메모리 관리 규칙(스텍 프레임)이 따로 존재하지 않는다.

Giga Byte 단위까지 할당 가능하다.

 

malloc 함수로 동적 메모리 할당하기

 

C표준 함수인 malloc 함수를 사용해서 메모리 할당 가능하다.

함수 원형 : void *malloc(size_t size);    //  size_t는 unsinged in와 같은 자료형이다.

함수 사용 형식 : void *p= malloc(100);    //  100 바이트의 메모리를 할당하여 포인터 p에 저장한다.

 

위와 같이 포인터 하나만 있으면 모든 메모리를 관리 할 수 있다.

 

void * 형식으로 주소를 반환한다.

 

할당된 메모리 주소를 받는 시점에 미리 형 변환(Casting)하여 사용한다.

프로그래머 입장에서 데이터 타입을 미리 알수 없기 때문에 void * 형을 사용한다.

 

free 함수로 할당된 메모리 해제하기

 

힙(Heap)에 할당된 메모리는 프로그램이 끝날 때까지 자동 해제되지 않는다.

free 함수를 이용하여 명시적으로 메모리가 해제된다.

malloc, free 함수가 선언된 malloc.h 파일을 포함해야 한다.

free(p);    //  p가 가지고 있는 주소에 할당된 메모리를 해제한다.

 

지역변수는 stack frame으로 관리되어서 자동으로 메모리가 해제된다. 그러나 동적 메모리는 free하지 않으면 계속 사용중이다.

malloc 함수는 실패 가능한 함수이다. 실패하면 null 값을 반환한다. 단, 프로그램이 종료되면 heap은 사라지기 때문에 해제 된다.

그러나 프로그램이 오랫도안 종료되지 않는 경우도 있기 때문에 free를 해주는 것이 좋다.

 

프로그램의 속도는 동적 할당으로 인해 많이 저하되기도 한다.

 

힙은 스택처럼 순차적으로 관리하지 않는다.

 

 

2바이트를 할당 하려면 앞부터 체크한다. 2바이트가 할당될 공간이 없기 때문에 맨 뒤에 할당된다. 위와 같은 상황을 메모리가 조각났다라고 한다. 프로그램에서 동적할당을 하면 조각이 많아 지기 시작한다. 조각이 많아 지면 처음부터 체크를 계속 해야 하기 때문에 프로그램이 느려진다.

 

malloc(상수, 변수 가능)

 

변수가 가능하다는 것은 scanf로 사용자가 원하는 값을 받아올 수 있다는 것이다.

 

CPU 레지스터

 

2+3을 하고 싶으면 ax=2 대입, bx=3 대입을 하고 add ax, bx라고 어셈블리어를 사용하면 된다.

만약 변수를 10개가 되면 레지스터가 제한적이기 때문에 외부 메모리 RAM을 사용한다.

CPU 레지스터 ax, bx, cx, dx를 사용하면 속도가 매우 빠르지만 외부 메모리를 이용하면 속도가 느려진다.

 

stack에는 지역변수가 할당된다.

char *p =(char *)malloc(100);

p는 스택에 들어간다. 동적 할당 된 100Byte는 heap 영역에 들어간다. p가 힙의 주소값을 가지고 있다.

 

malloc 함수를 사용할 때 주의할 점

 

함수 내에서 동적 할당된 메모리를 해제하지 않으면 함수가 호출될 때 마다 메모리가 할당되어 힙 영역에 누적된다.

동적 할당된 메모리의 주소를 잃어버리면 메모리 손실이 발생한다.

 

할당되지 않은 메모리를 해제하는 경우

 

할당되지 않은 메모리를 할당하면 실행시 오류가 발생한다.

 

정적으로 할당된 메모리를 해제하는 경우

 

동적으로 할당된 주소를 두번 해제하면 프로그램 실행 시 오류가 발생한다.

 

 

 

동적 메모리 할당의 장단점

 

장점

  스택에 비해 큰 크기의 메모리 할당 가능하다.

  메모리를 할당하고 해제하는 시점을 지정 가능하다.

  할당되는 메모리의 크기를 프로그램 실행 중에 변경 가능하다.

단점

  코드가 복잡해진다.

  작은 메모리를 사용할 때에는 비효율적이다.

 

 

배열과 비슷한 형식으로 동적 메모리 사용하기

 

동적 할당된 메모리를 배열처럼 그룹으로 묶어서 주로 사용한다.

 

 

동적 메모리를 할당하는 또 다른 방법

 

int *p = (int *)malloc(sizeof(int)*3);                        //  12바이트 동적 할당

short *p = (short *)malloc(sizeof(short)*6);          //  12바이트 동적 할당

sizeof 함수를 사용해도 속도의 저하가 없다. sizeof 함수는 반환값이 항상 상수이다.

12와 sizeof(int)*3은 기계어로 바꾸면 같다.

 

malloc 함수는 메모리 할당 크기를 변수로 지정할 수 있다.

 

메모리를 동적 할당하면 사용할 데이터의 개수를 제한할 필요가 없다.

반면 배열의 요소 개수는 상수로만 명시 가능하다.

 

다차원 포인터란?

 

기계어(간접 주소 지정 박식)은 1차원 밖에 없다. 포인터는 다차원 포인터의 개념을 제공한다.

다차원 포인터 : 간접으로 여러 번 대상을 가리키는 포인터

차원은 '자신이 가리키는 대상'의 개수만큼 증가

 

다차원 포인터를 사용하는 이유

 

기계어는 4Byte만 있으면 무조건 포인터로 사용이 가능하다. 하지만 C언어는 변수 앞에 *키워드를 붙어야 한다.

 

int data;

char *p;

int *p = (int *)malloc(4);

내가 원하는 크기만큼 자료형으로 쓸 수 있다.

동적 메모리의 최대 단점 : 일차원 포인터만 존재한다면 포인터를 동적 메모리를 사용해서 사용할 수 없다.

구조체를 제외하면 char *p 처럼 포인터 형을 동적으로 할당할 문법이 없다.

 

다차원 포인터 생성의 이유와 목표

 

포인터를 무한히 생성하기 위해서 만들어 졌다. 동적 메모리 할당을 기본 4바이트이면 변수로만 사용할 수 있지만 2차원 포인터를 사용하면 동적 메모리를 포인터처럼 사용할 수 있다. 일반 변수도 포인터화 할 수 있다는 것이다. 즉, 포인터가 아닌데 포인터처럼 사용할 수 있다는 것이다.

 

다차원 포인터 정의하기

 

포인터 변수 선언 시 * 키워드를 추가할 때마다 차원이 하나씩 증가한다.

* 키워드를 두 개 이상 사용한 포인터가 '다차원 포인터'이다.

* 키워드는 최대 7개까지 사용 가능하다.

포인터 변수 선언 시 사용한 * 키워드 개수만큼 포인터를 사용할 때 * 연산자를 사용가는하다.

 

char *p1;       //  1차원 포인터 : p1, *p1

char **p2;     //  2차원 포인터 : p2, *p2, **p2

char ***p3;   //  3차원 포인터: p3, *p3, **p3, ***p3

 

일반 변수의 한계와 다차원 포인터

 

4Byte 크기의 변수라면 포인터 변수가 아니더라도 주소 저장 가능하다.

 

short data = 0;

int my_ptr = (int)&data;  //  data 변수의 주소를 my_ptr에 저장하기 위해 형 변환, 4바이트이기 때문에 주소가 저장된다.

*my_ptr = 3;                  //  오류 발생 : 포인터 변수가 아니라 * 연산자 사용이 불가능하다.

 

2차원 포인터의 선언과 사용

 

* 연산자를 최대 2개까지 사용 가능하다.

2차원 포인터는 주소 이동을 두 번 할 수 있다.

 

2차원 포인터는 대상들이 4바이트의 메모리이고 주소만 탑재 되어 있으면 포인터가 아닌 변수라도 된다.

즉, 자신의 징검다리로 쓰이는 대상은 반드시 포인터일 필요는 없다.

 

2차원 포인터의 구성

 

2차원 포인터는 1차원 포인터의 주소 값을 저장한다.

 

short **p;

int data = 3;

pp = (short **)&data;  //  data의 주소가 2차원 포인터 변수 pp에 저장된다.

data 변수의 저장된 일반 숫자 값을 주소로 사용해서 5번지로 이동하여 값을 읽거나 저장하려고 하면 오류가 발생한다.

2차원 포인터가 가리키는 대상이 일반 변수인 경우

 

2차원 포인터 변수가 가진 주소 값의 형식에 상관없이 4바이트 크기를 가지면 그 주소에 있는 변수의 값을 변경 가능하다.

 

short **p, data=3;

int my_ptr=(int)&data;

short **p = (short **)&my_ptr;

**p=5;  //  data 변수의 값이 3에서 5로 변경된다.

 

2차원 포인터가 가리키는 대상을 동적으로 할당하기

 

2차원 포인터는 1차원 포인터의 주소 값을 저장한다.

 

short **p, data=3;

pp = (short **)malloc(4);    //  pp = (short **)malloc(sizeof(short *))와 같은 뜻이다.

*pp=&data;    //  data 변수의 주소 값을 두 번째 상자에 저장한다.

**p=5;           //  data 변수의 값이 3에서 5로 변경된다

 

2차원 포인터가 가리키는 대상을 동적으로 할당하면 좋은점

 

한 번에 여러 개의 포인터 할당 가능하다. 

short **pp = (short **)malloc( 3 * sizeof(short *));   //  12바이트 (3 * 4)크기로 메모리가 할당된다.