본문 바로가기

프로그래밍/C, C++

[Tips 18기] C언어 여덟번째 강좌

1. 배열과 포인터

지난시간에 이어서 오늘도 배열과 포인터로 시작했다. 배열과 포인터는 서로 두가지 형태로 합체할 수 있는데 

배열을 기준으로 포인터와 합체하는 방법과 포인터를 기준으로 배열과 합체하는 방법 두가지이다. 


1.1 배열을 기준으로 포인터 사용

C언어에서 포인터는 참조하는 대상의 주소를 담는 "변수"이다. 따라서 일반 변수들과 같이 배열을 선언할 수 있고 포인터로 만들어진 배열을 포인터 배열이라고 한다.

포인터 배열은 다음과 같이 선언한다

int *p[5];


int *형식을 가진 배열p를 선언한다는 뜻이다. 


1.2 포인터를 기준으로 배열 사용

위에서는 포인터 배열을 선언하는 방법을 알아봤다, 그렇다면 이 코드는 어떨까?

int (*p) [5];


괄호를 하나 추가했을 뿐이지만 뜻은 완전히 다르다. 


배열을 기준으로 포인터를 사용할 때는  각 항목이 포인터인 배열을 선언한 것이지만 포인터를 기준으로 배열을 사용할 때는 int 5칸, 즉 20바이트를 가리키는 포인터라는 뜻이 된다. 


그렇다면 왜 이런 형식으로 포인터를 만들까?

C 언어의 기본 자료형은 1, 2, 4, 8바이트이다. 하지만 우리가 필요하다면 5바이트나 20바이트씩 묶어서 사용자 정의 자료형을 만들어야 할 때가 있다 이때 우리는 배열을 사용한다. 그런데 이런 배열을 참조하기 위한 포인터를 선언하기 위해서는 사용자 정의 자료형을 사용하는 포인터를 선언해야 한다. 이를 위해 포인터를 기준으로 배열을 사용하는 이런 포인터를 사용하는 것이다. 


다음의 코드를 보자


char arr[3][5]; // 5바이트씩 3칸의 배열

char (*p)[5] // 5바이트씩을 가르키는 포인터


p = arr; // arr의 시작 주소를 포인터에 저장


for (int i=0; i<3; i++){

        p[3] = 0; // arr의 i번째 요소의 3번째 요소를 0으로 만든다

        p++; // 주소를 5바이트 증가시켜 arr의 i +1 번째 주소를 가리킨다.
}


다음과 같이 범위를 사용자 지정한 포인터를 사용해 다차원 배열을 효율적으로 사용하는 등 다양한 곳에서 이러한 형식의 포인터를 사용할 수 있다.


2. 메모리 할당

우리가 만드는 코드를 컴파일 하면 프로그램이 된다. 프로그램은 기계어 명령어의 모음이다. 그리고 이 프로그램을 메모리에 올려 활성화 시키면 프로세스가 된다. 운영체제는 실행되는 프로그램들에 컴퓨터의 메모리를 분배해 준다. 이것을 메모리 할당 이라고 한다.

프로세스가 가진 메모리는 관리 효율과 보안을 위해 여러개의 메모리 세그먼트로 나뉘는데 우리는 그중 힙과 스택에 대해 알아볼 것이다.

스택과 힙은 프로그램이 사용하는 변수가 저장되는 메모리 세그먼트이다. 

메모리를 할당하는 방법은 정적 메모리 할당과 동적 메모리 할당 이 두가지로 나뉘는데 이 두가지 할당 방식에 대해 정리해 보자면 다음과 같다

정적 메모리 할당(일반적인 변수 선언)

 동적 메모리 할당(malloc을 통해 할당)

 컴파일 시점에 모든것이 결정된다

 런타임에 결정

 컴파일러의 관리 영역이다

 프로그래머 관리 영역(해제 필요)

 스택 프레임으로 관리

 C언어 자체 기능이 아니다 (외부 헤더파일 사용)

 최대 1~2MB

 32비트 기준 2GB 까지 사용가능

 스택 영역

 힙 영역


여기서 보듯이 스택과 힙은 많은 차이가 있다. 


하지만 결정적인 차이는 사용 가능한 공간의 크기일 것이다. 힙 공간을 사용하는 동적 메모리 할당은 스택에 비해 엄청나게 많은 양의 메모리를 활용 가능하다. 

또한 스택 영역을 사용하는 일반 변수는 컴파일 타임에 모든것이 결정되기 때문에 예상할 수 있는 최대치의 크기를 고려하여 코드를 만들고 변수 관련 문제가 생길 시 수정사항이 생기기 때문에 모두 재컴파일하여 재배포해야 하는 문제가 생기지만 동적 메모리 할당을 사용하면 런타임에 결정되기 때문에 동적으로 변수의 크기를 조정할수 있어 메모리를 좀 더 효율적으로 사용하게 된다.


하지만 장점만 있는 것은 아니다.


앞서 말했듯이 힙 영역은 컴파일러의 관리 영역이 아닌 프로그래머의 관리 영역이다. 따라서 힙 공간을 사용하는 것이 메모리 용량 면에서는 우월하지만 힙 공간을 사용하는 malloc(memory allocation) 함수를 사용하면 해제가 안될시 메모리가 점점 차기 때문에 꼭 free로 메모리 해제를 해야 하고 서로 다른 크기의 메모리를 malloc으로 할당하고 해제 순서가 뒤섞이면 메모리가 중간중간에 구멍이 나는 메모리 조각 현상이 생겨서 메모리 할당 속도가 현저하게 느려져 프로그램의 속도가 느려질수 있는 메모리 관리의 번거로움이 생기게 된다.


다음은 메모리를 동적 할당하고 해제하는 코드이다


char *hp = (char *)malloc(sizeof(char) * 4) // 힙 공간에 4 크기 배열 할당

char sp[4]; // 스택 공간에 4 크기 배열 할당


free(hp); // hp의 메모리 해제

3. 다차원 포인터


다차원 포인터란 무엇일까?
우리가 이때까지 쓴 포인터는 모두 1차원 포인터였다. 원본 대상이 있으면, 그 대상의 주소를 가진 한 변수의 친구 변수였다. 다차원 포인터는 두개 이상의 참조가 가능한 포인터이다. 다차원 포인터의 차원은 건널수 있는 만큼 늘어난다.


다음은 이차원 포인터의 사용 예시이다.


char **pp;

char *p;

char t = 'a';


p = &a;

pp = &a;


printf("%c", a);

printf("%c", *p);

printf("%c", **pp); // 셋다 'a' 출력


위와 같이 다차원 포인터는 n차원 포인터가 n-1차원 포인터를 가르키는 사슬 구조로 배우게 되지만 사실 다차원 포인터는 같은 차원을 가진 여러 포인터를 가르킬 수도 있다. 여기서 다시 생각해 보자면 다차원 포인터의 차원은 참조할 수 있는 변수의 수만큼 늘어난다고 볼수도 있다.


다음은 이차원 포인터가 일반 변수 두개를 참조하는 예시이다.


char **pp;


char t1 = 'a';

char t2 = 'b';


char *pp = &t1;

char **pp = &t2;



또한 다차원 포인터는 동적 메모리 할당에서 다차원 배열을 참조하는데 사용할 수도 있다.

다음은 삼차원 포인터를 사용하여 삼차원 배열을 동적 할당하는 예시 코드이다.


char ***ppp;


ppp = (char ***)malloc(sizeof(char *) * 2);


for (int i = 0; i < 2; i++) {

*(ppp + i) = (char **)malloc(sizeof(char **) * 3);

for (int j = 0; j < 3; j++) {

*(*(ppp + i) + j) = (char *)malloc(sizeof(char) * 4);

}

}



위의 예시 코드에서는 해제 코드를 적지 않았지만 실제로 작성해 볼 때는 해제 코드를 적어보도록 하자.