F.R.I.D.A.Y.

[TIPS 19TH] 07 : 2018.07.16. (월) 본문

외부활동/TIPS 19th

[TIPS 19TH] 07 : 2018.07.16. (월)

F.R.I.D.A.Y. 2018. 7. 17. 08:19
반응형

1. 프로그램? 프로세스?

 어디서는 프로그램, 어디서는 프로세스라고 하는 경우가 있다. 컴퓨터에 큰 관심이 없는 사람들은 두 단어를 같은 개념으로 생각할 수 있지만, 실제로 두 단어는 다른 의미를 가진다.


 프로그램

>> 실행 파일, 확장자가 [ .exe ]로 이뤄진 파일을 말한다.

 프로세스

>> 실행 파일, 즉 위에서 프로그램이라고 했던 파일을 운영체제의 로더가 메모리에 불러올 때, CPU가 해당 파일에 있는 명령어를 실행할 수 있도록 재배치 해 적재된 것.


 즉, 프로그램은 간단히 명령어의 집합을 파일로 구성해놓은 것이고, 프로세스는 이러한 파일을 실제 사용할 수 있도록 재구성한 것이라고 생각하면 된다.


 운영체제의 로더가 파일을 불러오게 되면, 해당 파일은 메모리상에 몇가지 구조로 나뉘어 적재가 된다. 아래 그림은 옛날에 프로그램이 메모리에 적재된 때의 구조이고, 현재는 이보다 복잡하게 구성된다고 한다.


※ 단순 참고용으로만 바라볼 것 ※


 실제로 프로그램, 즉 실행파일에는 위 모든 영역이 들어있지 않고 기계어 명령어와 문자열 상수목록만 들어있다. 나머지[각주:1]는 운영체제의 로더가 불러들일 때 메모리에 재구성하면서 생성된다.


 먼저 스택(stack)은 함수에서 지역변수가 들어가는 영역이다. 7장 이전에서 배운 모든 변수 선언 방식은 이 스택이라는 영역에 적재되는 것이라고 생각하면 된다.

 스택을 사용할 때는 주의해야할 것이 있다. 스택에는 지역변수의 값이 저장되는 공간이므로 다르게 말하면 지역변수를 사용할 때의 주의할 점이다. 스택은 일반적으로 각 프로세스당 1 MB[각주:2]를 넘지 못한다. 다르게 말하면 프로그램이 사용하는 모든 정적변수들 크기의 합이 1 MB가 넘게되면 프로그램이 죽어버린다는 소리다.

>> 단일 변수 크기가 1 MB가 넘으면 컴파일 오류가 발생한다.

>> 코드 내 사용중인 모든 정적 변수 크기의 합이 1 MB를 넘게되면 런타임 오류가 발생한다.


 정적 메모리 특징

>> 지역변수를 사용하면 해당 변수의 현재 주소를 알아야 함.

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

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



2. 스택 :: 자료구조

일단 위에서 설명한 엑스트라 세그먼트의 스택과는 다르다. 정확히는 엑스트라 세그먼트의 스택 영역이 자료구조의 스택의 특성을 이용해서 해당 영역이 스택이라는 이름이 붙게 되었다. 이번 단에서 말하는 스택은 저기 위의 엑스트라 세그먼트의 스택[각주:3]이 아니라, 자료구조로서의 스택을 말한다. 이번 단에서 엑스트라 세그먼트의 스택을 의미할 때는 "스택세그먼트"라고 하겠다.


 스택은 밑이 막힌 병과 같은 구조를 가지고 있다. 그래서 LIFO(Last In First Out) 구조를 가지고 있다. 따라서 스택 중간에 저장된 값을 사용하기 위해서는 찾고자 하는 값 이후에 넣은 값들을 모두 꺼내고 사용후에 다시 스택에 넣어야 한다.


 스택을 사용하기 전에, 먼저 특징을 설명하면 다음과 같다.

>> 밑이 막힌 구조를 가지고 있으며 맨 아랫 단을 bp(Base Point)라고 한다.

>> 가장 최근에 들어간 부분을 sp(Stack Point)라고 한다.

>> 최소한의 크기로 이론상 무한정의 크기를 관리할 수 있다.

>> 효율적인 자료구조는 아니다.


 컴퓨터 스택과 같이 사용한다. 비효율적인데 왜 사용하냐고? 간단하다. 3번처럼 최소한의 크기로 이론상 무한정의 크기를 관리할 수 있기 때문이다.


 자료구조의 스택과 컴퓨터의 스택은 약간 다르게 작용한다.

명령어 

자료구조의 스택 

컴퓨터의 스택 

PUSH 

스택 포인터 주소 증가 

SP 주소 감소 

POP 

스택 포인터 주소 감소 

SP 주소 증가 

 

일반적으로 자료구조에서는 PUSH를 하면 값이 쌓이기 때문에 증가하고, POP은 값을 빼기 때문에 감소한다고 생각하는데 컴퓨터는 그와 반대로 작용하므로 주의해야한다.


참고

>> C언어가 스택 세그먼트에 자기만의 자료구조를 만들어 넣은 것을 스택 프레임이라고 한다.

>> 필요한 값이 중간에 있다고 pop, pop, ...., 사용, push, ...., push 하는것은 굉장히 비효율적이기 때문에 메모리에서는 실제 스택처럼만 구성을 하고 사용은 다르게 한다. 즉, int 3개 크기의 메모리가 필요할 때는 [ sub SP, 12 ]를 하고 필요가 없어지면 [ add SP, 12 ]를 해서 지워버린다.

>> END Point[각주:4]가 없으면 END Point를 찾는 매커니즘을 또 생성해야하므로 END Point를 만들었다.



3. 동적 메모리 할당

 스택은 정적 메모리 할당 위한 공간이라고 한다면, 힙은 동적 메모리 할당을 위한 공간이라고 보면 된다.

 힙(Heap)은 스택과는 다른 특징을 가지고 있는데, 스택은 관리주체가 컴파일러라면, 힙은 관리주체가 프로그래머라는 것이다. 말인 즉, 우리가 일반적인 변수를 선언하게 되면 해당 변수는 스택이라는 공간에 쌓이게 된다. 쌓인 상태에서 사용을 하다가 변수가 선언된 함수가 종료되면 스택에 쌓인 변수공간은 자연스레 사라지게 된다. 컴파일러가 이러한 일[각주:5]을 자동을 맡아서 처리해주기 때문이다. 그러나, 힙을 사용할 때는 선언과 해제를 프로그래머가 명시적으로 작성해주어야한다.

 아래는 같은 행위를 하지만, 메모리 할당을 정적 메모리 할당으로 한 것과 동적 메모리 할당으로 한것으로 나눈 코드이다.


void A() { // 정적 메모리 할당
	int arr[5];
	for (int i = 0; i < 5; i++) {
		arr[i] = i + 1;
		printf("%d ", arr[i]);

	}
	printf("\n");

}

void A() { // 동적 메모리 할당
	int *arr;
	arr = (int *)malloc(20);
	for (int i = 0; i < 5; i++) {
		*(arr + i) = i + 1; // arr[i] = i + 1;
		printf("%d ", *(arr + 1)); // printf("%d ", arr[i]);

	}
	printf("\n");

	free(arr);

}


위 두 코드는 길이 5인 배열을 만들고 각 인덱스에 1 부터 5를 넣은 다음, 차례로 값을 출력하는 함수이다. 정적 메모리 할당을 했을 때보다 동적 메모리 할당을 했을 때, 좀더 복잡하고 작성해야 할 코드도 좀 더 많아짐을 알 수 있다.

 이렇게 보기만 하면 정적 할당이 더 편하고 이익이 많을것 같지만, 오히려 대부분의 경우에서 동적할당이 더 많은 장점을 가진다. 그래서 쓸텐데 뭘 물어


 먼저, 동적 메모리 할당을 사용할 때의 장점이다.

>> 스택에 비해 큰 크기를 할당 받을 수 있다. 동적 메모리 할당은 힙영역에서 이뤄지며 힙은 최대 2 GB까지 할당받을 수 있다.

: 윈도우 기준 스택은 1 MB까지만 할당받아 사용할 수 있지만, 힙은 최대 2GB까지 할당받을 수 있다.

>> 메모리의 할당, 해제 시점을 직접 정할 수 있다.

: 스택에 적재된 변수는 할당과 해제 시점이 함수, 혹은 프로그램의 시작과 끝일 뿐이지만 동적 할당은 프로그래머가 직접 정할 수 있다.

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

: 길이 5인 배열을 사용하다가 길이 10인 배열이 필요할 때 소스코드를 재 컴파일 할 필요가 없다. 즉 가변 길이의 배열을 만들어낼 수 있다.[각주:6]


 단점은 다음과 같다.

>> 코드가 정적 메모리 할당을 사용할 때보다 복잡해진다.

: 위 코드를 보면 확실히 그렇다는 것을 느낄 것이다.

>> 작은 자료형의 변수를 생성할 때는 비효율적이다.

: 메모리를 할당받을 때, char 타입이나 short타입을 하나 할당받는다고 하면 할당받은 해당 메모리 주소를 기억하기 위해 되려 4 Byte공간이 추가로 들어 배보다 배꼽이 더 큰 상황이 되어버린다.


 동적 메모리 할당을 하면서 개인적으로 한가지 유의해야할 것이 있다고 생각하는데, 동적 메모리 할당이 힙에 올라간다고 해서 위에서 동적 메모리 할당 코드 중, [ int *arr ] 또한 힙에 올라가는 것이 아니다. 원론적으로 보면 [ int *arr ]은 우리가 이전에 배웠던 지역변수로 int 포인터 변수를 할당한 것이기 때문에 변수 [ arr ]은 힙이 아니라 스택 영역에 존재한다. 즉, 동적 메모리 할당을 할 때는 스택 영역 또한 사용된다고 보면 된다.



4. malloc & free

 앞에서 동적 메모리 할당에서 [ malloc ] 함수와 [ free ]함수를 사용했다. 각 함수의 사용 이유와 사용법은 다음과 같다.

함수 이름 

의미 

실 사용 예 

 malloc[각주:7]( size );

 메모리 할당

 int *p;

 p = (int *)malloc(4);

 free( varName );

 malloc로 할당된 메모리 해제 

 free( p ); 


 여기서 [ malloc ]함수는 반환형이 [ void * ]이기 때문에 대입하기 전에 해당 타입으로 캐스팅을 해준 후 대입해줘야한다. 해주지 않더라도 대입은 정상적으로 이뤄지지만 잠재적인 문제들이 모여서 버그를 일으키기 때문에 선택이 아닌 필수로 생각하고 해주자.


 [ malloc ]과 [ free ] 함수는 < malloc.h >헤더 파일에 선언되어 있다. 따라서 이 두 함수를 사용하고자 할 때는 < malloc.h >헤더 파일을 인클루드 해야한다. 단, < stdlib.h >헤더 파일을 인클루드 했다면 굳이 할 필요는 없다. < stdlib.h >헤더 파일 안에서 < malloc.h >헤더 파일을 인클루드 하기 때문이다.


 알아둘 것이 있는데, [ malloc ] 함수는 변수를 새로 생성해주는 것이 아니라 프로세스가 메모리를 사용할 수 있도록 일정량의 메모리를 할당받아오는 것이다. 즉, 메모리를 할당 받았더라도 그것은 단지 값을 넣을 수 있는 메모리이지 코드상에서 자료형을 지정해 값을 넣는 그런 변수가 아니다.


 [ malloc ]의 매개변수는 하나로, 할당받을 메모리의 크기를 Byte 단위로 입력하면 된다. 그런데 이전에 썼던 것처럼 길이 5인 int형 배열로 할당받으려고 [ malloc(20) ] 등과 같이 그냥 숫자를 넣으면 한눈에 할당받는 메모리가 어떤 것을 의미하는지 파악하기가 힘들다. 따라서 길이 n인 int형 배열처럼 할당받을 땐 아래처럼 작성하자.


int *p;
p = (int *)malloc(sizeof(int) * n );
// 길이 n인 동적 배열을 생성함.

 [ malloc ] 함수는 항상 성공하는 것이 아니다. 힙 영역에 할당하는데 힙 영역을 벗어나는 크기를 할당하게 되면 함수가 실패하면서 NULL을 반환한다.


 [ free ] 함수도 주의해서 사용해야 할 점이 있다. 다음과 같은 상황에서는 제어 코드를 추가로 생성해 작성해주어야한다.

>> 할당되지 않은 메모리를 해제하려고 할 때.

>> 이미 해제된 메모리를 다시 해제하려고 할 때.



5. 다차원 포인터

 다차원 포인터는 기계어 명령어로 1:1 대응되지 않는다. 즉, 기계어의 자체 기능이 아니라 컴파일러에서 제공하는 기능이라는 것이다.


int data = 0;
int temp = (int)&data;

int *p = (int *)temp;
*p = 5;

위 코드는 뭔가 잘못 되었다. 일반 변수 [ temp ]에 [ data ]변수의 주소가 들어갔기 때문이다. 일반 변수에 변수의 주소값이 들어갔기 때문에 프로그램에서는 [ temp ]에 저장된 값을 통해 [ data ]변수에 접근이 불가능하다. 그래서 포인터 [ p ]를 추가로 해주어서 [ temp ]변수를 캐스팅해 [ p ]에 넣어줬다.


int data = 0;
int temp = (int)&data;

int **p = (int **)&temp;
**p = 5;

 위 코드는 바로 직전 코드의 변수 [ p ]의 자료형을 [ int * ]에서 [ int ** ]로 고치고 [ p ]에 [ temp ]의 주소를 넣어준 것이다. 이렇게 해도 정상적으로 프로그램이 실행된다.


 차원 포인터는 포인터 또한 일반 변수처럼 동적으로 늘이고 줄일 수 있도록 만들기 위해 생성된 것이라고 보면 된다. 차원 포인터는 사이에 있는 변수가 포인터인지는 관심이 없다. 단지 자신이 가리키는 변수의 값이 주소값으로 작용해 접근할 수 있도록 해주는 것이다. 따라서 단순 메모리 할당만 해주는 [ malloc ]함수로 메모리 할당을 받아 사용하더라도 큰 문제가 없다.


 처음 다차원 포인터를 설명할 때, 다차원 포인터는 컴파일러가 제공하는 기능이라고 말한 것처럼 컴파일이 되어 어셈블리가 되면 일차원 포인터가 반복의 형식으로 가리킨다고 보면 된다.


 이차원 포인터는 다음과 같이 작성할 수 있다.

1. data[a][b];

2. *(*(data + a) + b);

3. *(data[a] + b);

4. (*(data + a))[b];


 여기서 주의해야할 것은 4번째 [ (*(data + a))[b] ]이다. 연산자 우선 순위에 의해 [ *(data + a)[b] ]가 되는데, 이걸 혼합형이 아니라 3번처럼 작성하면 [ *(*(data + a + b)) ]가 된다. 즉 data[a + b][0]이 된다. 꼭 연산자 우선순위를 작성해주어야한다.


예시 코드 : 이차원 포인터로 row 4, col 5인 배열 생성하기


#include <stdlib.h>

int main(void) {
	int **p;
	p = (int **)malloc(sizeof(int *) * 4);
	for (int i = 0; i < 4; i++) {
		*(p + i) = (int *)malloc(sizeof(int) * 5);

	}

	for (int i = 0; i < 4; i++) {
		free(*(p + i));
	}

	free(p);

	return 0;
}




다차원 포인터 VS 다차원 배열


상황 

다차원 포인터 

다차원 배열 

기계어 번역 

반복 구조로 해서 일차원 포인터로 변환 

컴파일러에서 다차원 배열을 일차원 배열로 재해석 

사용되는 메모리 

다차원 배열보다 많음 

각 차원에서 소비하는 크기만큼[각주:8] 

인덱스 자유도 

동류 차원의 인덱스를 제각각 지정 가능 

동류 차원의 크기가 모두 같음 




Etc.

자료구조를 포함해 무엇이든 배울 때는 왜 만들어졌고 왜 사용하는지를 배우는게 좋다.


스택프레임도 컴파일러가 만들어주므로 나중엔 스택 프레임도 직접 구현해 컴파일러에서 벗어나는 것이 좋겠다.


내가 편하면 프로그램은 융통성이라쓰고싸가지라고읽는다.이 없어진다.


주절주절주저리 : 너무 글만 적은것같아.. 짧고 굵게 핵심만 찍어서 쓰려고 했는데...

  1. 전역변수, static 전역변수, 힙, 스택 등 [본문으로]
  2. 여기서 1 MB는 Windows 계열에서의 이야기이다. 리눅스계열은 가용 메모리가 증가함에 따라 2 MB로 스택의 크기를 증가시켰다고 한다. [본문으로]
  3. 스택 자료구조로 관리되는 메모리 공간이기 때문에 줄여서 스택이라고 불리게 되었다. [본문으로]
  4. sp(stack point)다. [본문으로]
  5. 메모리에 적재된 변수를 해제해주는 행위 [본문으로]
  6. 주의해야할 점은 GCC기반의 컴파일러는 정적배열 선언부에 상수가 아닌 변수를 넣을 수 있다. Visual Studio에서는 가변배열 기능을 지원하지 않으므로 동적 메모리 할당을 통해 해결해야한다. [본문으로]
  7. Memory ALLOCation [본문으로]
  8. [A][B][C] 이면 A * B * C * 배열의 자료형 크기 [본문으로]
728x90
반응형

'외부활동 > TIPS 19th' 카테고리의 다른 글

[TIPS 19TH] 09 : 2018.07.24. (월)  (2) 2018.07.24
[TIPS 19TH] 08 : 2018.07.19. (목)  (2) 2018.07.21
[TIPS 19TH] 06 : 2018.07.13. (목)  (2) 2018.07.13
[TIPS 19TH] 05 : 2018.07.09. (월)  (2) 2018.07.10
[TIPS 19TH] 04 : 2018.07.05. (목)  (2) 2018.07.06
Comments