[VEDA] 4일차

나우히즈·2025년 3월 23일

VEDA

목록 보기
3/16

15장. 응용 포인터

포인터 배열

배열의 기본 원리는 포인터 배열에서도 동일하게 적용된다.
1. 동일한 자료형으로 구성된다.
2. 연속된 메모리 공간이 할당된다.

배열 선언 방식:

자료형 배열명[크기];

포인터 배열은 자료형 옆에 *를 붙여 포인터의 배열을 만들 수 있다.

자료형* 배열명[크기];

포인터 자료형에는 주소를 저장할 수 있으며, 배열처럼 인덱스를 통해 접근할 수 있다.

포인터 배열을 사용하는 이유

일반적인 다차원 배열을 사용할 경우, 메모리 공간이 비효율적으로 사용될 수 있다.

예제:

char names[5][20] = { "hi", "hello", "goodbye", "welcome", "thanks" };

위의 예제에서 names는 5개의 문자열을 저장할 수 있도록 100바이트(5×20)를 고정적으로 할당받는다. 그러나 실제로 사용되지 않는 부분이 많아 내부 단편화가 발생하고, 메모리 효율이 저하된다.

포인터 배열을 사용하면, 필요한 만큼만 메모리를 할당할 수 있다.

char *names[5] = { "hi", "hello", "goodbye", "welcome", "thanks" };

각각의 문자열은 메모리 어딘가에 저장되며, names 배열에는 문자열의 시작 주소만 저장된다. 따라서 불필요한 공간 낭비를 줄일 수 있다.

문자열 비교 시 주의할 점

문자열 리터럴("")은 실제로 문자열의 시작 주소를 의미한다. 따라서 if (name == "end") 같은 비교를 수행하면, 문자열 값이 아닌 메모리 주소를 비교(name이 포인팅하는 주소, end가 담긴 메모리의 시작주소)하게 되므로 올바르게 동작하지 않는다.

문자열 비교를 위해서는 strcmp() 함수를 사용해야 한다.

if (strcmp(name, "end") == 0) {
    // 문자열이 "end"와 같음
}

동적 할당과 포인터 배열

포인터 배열은 동적 할당과 함께 사용되며, 동적으로 할당된 메모리 주소들을 배열로 관리하는 방식으로 활용된다.

포인터 배열을 사용할 경우, 반드시 올바른 주소로 초기화해야 한다. 초기화되지 않은 포인터는 허용되지 않은 메모리 접근(Segmentation Fault, 세그폴트)을 유발할 수 있다.

printf와 포인터 배열

printf에서 %s 형식 지정자는 문자열을 출력하는데, 문자열의 주소를 넘겨야 한다.

printf("%s", names[0]);

위 코드에서 names[0]"hi" 문자열의 시작 주소를 가리키고 있으므로, printf는 올바르게 동작한다.

포인터 배열을 사용하면 문자열 길이에 구애받지 않고 유연한 문자열 관리가 가능하다.


다중 포인터

앞서 사용한 포인터는 *가 하나인 단일 포인터이다. 여기서 *을 추가하면 다중 포인터(이중, 삼중 포인터 등)를 만들 수 있다.

이중 포인터(**)

이중 포인터는 포인터를 가리키는 포인터이다.

포인터의 개념:

  • 단일 포인터: 값이 저장된 메모리 주소를 가리킨다.
  • 이중 포인터: 단일 포인터의 주소를 저장한다.

이중 포인터의 구조

int value = 10;
int *pt1 = &value;  // pt1은 value의 주소를 저장
int **pt2 = &pt1;   // pt2는 pt1의 주소를 저장

위 코드에서 pt2pt1을 가리키고, pt1value를 가리킨다.

메모리 관계:

   value  →  10
   pt1    →  &value (0x1000)
   pt2    →  &pt1   (0x2000)

이중 포인터 활용

이중 포인터는 포인터 변수를 함수에 전달할 때 주로 사용된다.

void allocateMemory(int **ptr) {
    *ptr = (int*)malloc(sizeof(int));
}

int main() {
    int *p = NULL;
    allocateMemory(&p);  // p의 주소를 전달하여 동적 메모리 할당
    *p = 100;            // 동적으로 할당된 메모리에 값 저장
    free(p);             // 메모리 해제
    return 0;
}

위 코드에서 allocateMemory 함수는 p의 주소를 받아 malloc을 통해 메모리를 할당하고, main 함수에서 해당 메모리를 사용할 수 있도록 한다.


함수 포인터

함수를 가리키는 포인터이다.

함수를 정의 및 선언한 후, 함수명을 호출하면 실행된다. 컴파일러는 코드를 컴파일할 때 함수명을 해당 함수의 메모리 주소로 변환하여 저장한다. 즉, 함수명은 함수 코드의 시작 주소를 의미한다.

함수의 메모리 구조

함수는 실행 파일로 번역될 때 코드 세그먼트(Code Segment)에 위치한다. 따라서 함수의 주소는 코드 세그먼트의 특정 위치를 가리키게 된다.

함수의 주소를 포인터 변수에 저장할 수 있으며, 이를 함수 포인터라고 한다.

함수 포인터 선언

배열 포인터를 선언하는 방식과 유사하게, 함수 포인터도 함수의 원형을 그대로 따른다.

반환형 (*함수포인터변수)(매개변수 목록);

예제:

int add(int a, int b) {
    return a + b;
}

int (*fp)(int, int);  // 함수 포인터 선언
fp = add;             // 함수의 주소를 함수 포인터에 저장

int result = fp(3, 4); // 함수 포인터를 사용하여 함수 호출
printf("%d", result);  // 7 출력

함수 포인터 사용 시 주의점

  • 함수 포인터를 사용하려면 반환 타입과 매개변수 타입이 동일해야 한다.
  • 함수 포인터를 이용하면 다양한 함수들을 동적으로 선택하여 실행할 수 있다.
  • 서로 다른 함수 원형을 가지는 경우, 같은 함수 포인터 변수로 사용할 수 없다.

함수 포인터 배열

함수 포인터도 배열로 저장할 수 있다. 동일한 반환형과 매개변수를 가지는 여러 함수를 배열 형태로 관리할 수 있다.

int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }

int (*operations[2])(int, int) = {add, sub};
int result = operations[0](5, 3); // add 함수 실행, 결과: 8

함수 포인터 배열을 사용하면 여러 개의 함수를 배열로 관리하여 유동적으로 선택할 수 있다.


void 포인터

void* 포인터는 특정 자료형을 정하지 않은 포인터이다.

void *p;

void 포인터의 특징

  • void* 포인터에는 어떤 자료형의 주소도 저장할 수 있다.
  • 그러나 void* 포인터는 연산이 불가능하다.
  • 자료형이 정해져 있지 않으므로, 값을 참조하려면 명시적 형 변환(casting)이 필요하다.
int num = 10;
void *ptr = #
printf("%d", *(int*)ptr); // void 포인터를 int*로 변환 후 값 참조

void 포인터의 활용

일반적인 데이터 처리를 위해 직접 사용할 일은 많지 않지만, 라이브러리 함수에서는 void 포인터를 적극 활용한다.

예를 들어, qsort(퀵 정렬) 함수는 void 포인터를 사용하여 어떤 자료형의 배열도 정렬할 수 있도록 범용성을 제공한다.

void qsort(void *base, size_t nitems, size_t size, int (*compar)(const void *, const void *));

이때 qsort는 데이터의 형식을 미리 알 수 없으므로, void*로 받아서 필요할 때 캐스팅하여 사용한다.

void 포인터 사용 시 주의점

  • void* 포인터는 자료형이 명확하지 않으므로, 연산이 불가능하다.
  • 메모리에 접근할 때는 반드시 명시적 형 변환(casting)을 사용해야 한다.
void *p;
char ch;
p = &ch;  // 가능

// *p로 직접 참조할 수 없음 → 명시적 형 변환 필요
(char*)p; // 형 변환하여 사용 가능

즉, void*는 모든 자료형의 주소를 저장할 수 있지만, 실제 값을 사용하려면 반드시 특정 자료형으로 변환해야 한다.

16장. 메모리 동적 할당

동적 할당 개념

실행 중에 필요한 메모리를 제공하는 기능을 동적 할당이라고 한다.

  • 필요한 만큼만, 필요할 때 메모리를 할당할 수 있다.
  • 실행 시점에서 메모리 크기를 결정해야 하므로 함수를 통해 메모리를 할당받는다.
  • 프로그램이 실행될 때 메모리를 할당하기 때문에 이를 동적 할당(Dynamic Allocation)이라고 한다.

malloc() 함수

메모리를 동적으로 할당하는 기본 함수이다.

void *malloc(size_t size);
  • size_t size: 할당받을 바이트 크기를 지정.
  • 반환값: 할당된 메모리의 시작 주소를 가리키는 포인터.
  • 메모리가 부족하면 NULL을 반환.

사용 예제

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

malloc()은 요청한 크기만큼의 연속된 메모리 공간을 할당하고, 시작 주소를 반환한다.

malloc()을 이용한 배열 할당

int *arr = (int *)malloc(10 * sizeof(int));

위 코드는 int 크기의 메모리를 10개 할당하여, arr를 배열처럼 사용할 수 있도록 한다.


realloc() 함수

이미 할당된 메모리의 크기를 변경할 수 있다.

void *realloc(void *ptr, size_t new_size);
  • 기존 메모리 크기를 변경하여 더 큰 메모리 공간을 할당.
  • 기존 공간에 연속적으로 추가 공간이 있다면 확장 가능.
  • 만약 기존 메모리 공간이 부족하면, 새로운 메모리 공간을 할당하고 기존 데이터를 복사한 후 이전 공간을 해제한다.

사용 예제

int *arr = (int *)malloc(5 * sizeof(int));
arr = (int *)realloc(arr, 10 * sizeof(int));

위 코드는 int 5개 크기의 메모리를 10개 크기로 확장한다.

주의점:

  • 메모리가 연속적으로 존재하지 않는 경우, 새로운 메모리를 할당한 후 기존 데이터를 복사하고, 원래 메모리는 해제된다.

free() 함수

동적 할당된 메모리를 해제하는 함수이다.

void free(void *ptr);
  • 동적 할당된 메모리는 커널이 자동으로 해제하지 않기 때문에, 개발자가 직접 해제해야 한다.
  • free() 함수는 동적 할당의 시작 주소를 전달해야 한다.

사용 예제

int *arr = (int *)malloc(10 * sizeof(int));
free(arr);

free()를 호출하지 않으면 메모리 누수(Memory Leak)가 발생할 수 있다.


calloc() 함수

동적 할당된 메모리를 0으로 초기화하는 함수이다.

void *calloc(size_t count, size_t size);
  • count: 할당할 요소 개수
  • size: 각 요소의 크기
  • calloc()은 할당된 메모리를 0으로 초기화하여 반환한다.

사용 예제

int *arr = (int *)calloc(10, sizeof(int));

위 코드는 int 크기의 메모리를 10개 할당하고, 모든 값을 0으로 초기화한다.

0개의 댓글