C언어 Pointer
- C 언어에서 포인터(pointer)는 매우 중요한 개념으로, 메모리 주소를 가리키는 변수입니다. 포인터를 이용하면 동적 메모리 할당, 함수 매개변수 전달, 데이터 구조의 효율적인 관리 등이 가능합니다. 아래에서 포인터에 대해 좀 더 자세히 설명드릴게요.
- 포인터는 데이터의 실제 값이 저장된 메모리 주소를 저장하는 변수로 이를 통해 메모리에 직접 접근할 수 있습니다.
- 포인터는 *(dereference) 연산과 &(주소 연산)을 사용해 데이터를 간접적으로 참조하거나, 수정할 수 있습니다.
C언어의 포인터
1. 포인터의 기본 개념
포인터는 변수의 주소를 저장하는 변수입니다. 일반적인 변수는 특정 타입의 데이터를 저장하지만, 포인터는 메모리 상의 주소를 저장합니다. C에서는 메모리 주소를 다루는 것이 필수적이므로 포인터가 필수적으로 사용됩니다.
포인터는 다음과 같은 형태로 정의됩니다:
int a = 10; // 정수형 변수
int *p = &a; // 포인터 p는 변수 a의 주소를 저장
위 코드에서:
int a = 10;
은 정수형 변수a
를 선언하고 값10
을 할당합니다.int *p = &a;
는 정수형 포인터p
를 선언하고,a
의 주소를&a
로 가져와p
에 저장합니다.
여기서 *
은 포인터 선언 시 포인터 타입을 나타내는 역할을 하며, 변수 앞에 사용할 경우 해당 포인터가 가리키는 메모리 위치의 값을 참조하는 연산자(역참조)입니다.
2. 포인터의 연산
포인터는 다양한 연산을 할 수 있습니다. 주요 연산은 다음과 같습니다:
- 참조 연산 (
*
):``포인터가 가리키는 주소의 값을 가져옵니다.
int value = *p; // p가 가리키는 메모리 주소의 값을 value에 저장
- 주소 연산 (
&
): 변수의 주소를 가져옵니다.
int *p = &a; // 변수 a의 주소를 p에 저장
- 포인터 산술 연산: 포인터는 배열과 밀접한 관계가 있어 산술 연산을 지원합니다.
- 포인터에 1을 더하면 해당 포인터의 타입 크기만큼 메모리에서 다음 위치를 가리키게 됩니다.
int arr[3] = {1, 2, 3};
int *p = arr; // 배열의 첫 번째 요소를 가리킴
p++; // p는 arr[1]을 가리킴
3. 포인터와 배열
포인터는 배열과 밀접한 관계를 가지고 있습니다. 배열의 이름은 배열의 첫 번째 요소에 대한 포인터와 같은 역할을 합니다.
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr; // arr은 배열의 첫 번째 요소의 주소를 의미함
여기서 p
는 arr
과 같은 주소를 가지며, 배열의 요소에 접근할 수 있습니다:
printf("%d\n", *(p + 2)); // 출력: 30
포인터와 배열은 메모리의 연속된 공간을 가리킨다는 점에서 비슷하나, 차이점은 있습니다.
- 배열은 고정된 크기의 메모리 공간을 할당하며, 첫번째 요소의 주소를 가르킨다.
- 포인터는 다양한 메모리 주소를 가르킬 수 있으며, 동적으로 메모리 할당이 가능합니다.
4. 포인터와 함수
포인터는 함수와 함께 사용될 때 매우 유용합니다. 특히 다음과 같은 경우에 사용됩니다:
- Call by Reference: 포인터를 사용해 함수를 호출하면 함수가 원래 변수의 값을 변경할 수 있습니다.
void increment(int *n) {
(*n)++;
}
int value = 5;
increment(&value); // value의 주소를 전달
// value는 6으로 변경됨
- 동적 메모리 할당: C에서는
malloc()
,calloc()
,free()
등의 함수로 동적으로 메모리를 할당하고 해제할 수 있으며, 이때 포인터가 필요합니다.
void* malloc(size_t size)
void* calloc(size_t size, sizeo_t elsize) // 배열 요소 개수, 배열 요소 사이즈
void* realloc(void *ptr, size_t size) // 이미 할당받은 메모리에 추가 메모리 할당, 이전 메모리 주소 없어짐
void free(void *ptr) // 할당 메모리 해제
int *p = (int *)malloc(sizeof(int) * 5); // 정수형 5개를 위한 메모리 할당
if (p != NULL) {
p[0] = 1; // 동적 메모리의 첫 번째 요소에 값 할당
free(p); // 메모리 해제
}
5. 이중 포인터
이중 포인터는 포인터를 가리키는 포인터입니다. 즉, 포인터의 주소를 저장하는 포인터라고 할 수 있습니다.
int a = 10;
int *p = &a; // p는 a의 주소를 가리킴
int **pp = &p; // pp는 p의 주소를 가리킴
이중 포인터는 주로 다차원 배열을 처리하거나, 포인터 매개변수를 함수에서 수정하고자 할 때 사용됩니다.
int main() {
int rows = 3;
int cols = 4;
// 이중 포인터를 사용하여 2차원 배열 할당
int **array = (int **)malloc(rows * sizeof(int *));
for (int i = 0; i < rows; i++) {
array[i] = (int *)malloc(cols * sizeof(int));
}
// 배열에 값 할당
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
array[i][j] = i * cols + j;
}
}
// 배열 출력
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
printf("%d ", array[i][j]);
}
printf("\n");
}
// 메모리 해제
for (int i = 0; i < rows; i++) {
free(array[i]);
}
free(array);
return 0;
}
6. 포인터 사용 시 주의 사항
- 잘못된 참조: 초기화되지 않은 포인터를 참조하거나, 이미 해제된 메모리를 참조하면 예기치 않은 동작이 발생할 수 있습니다.
- NULL 포인터: 초기화되지 않은 포인터는 항상
NULL
로 초기화하는 것이 좋습니다. #include <stdio.h> void checkPointer(int *ptr) { if (ptr == NULL) { printf("Pointer is NULL.\n"); } else { printf("Pointer is not NULL and points to value: %d\n", *ptr); } } int main() { int *p1 = NULL; // 포인터를 NULL로 초기화 int value = 10; int *p2 = &value; // Check if p1 is NULL checkPointer(p1); // Check if p2 is NULL checkPointer(p2); return 0; }
- 메모리 누수: 동적 할당된 메모리는 반드시 사용 후
free()
로 해제해야 합니다.
7. 포인터를 사용하는 이유는?
- 포인터를 사용하면 메모리 관리를 효율적으로 할 수 있습니다.
- 동적 메모리 할당이 가능함
- 함수 인수로 배열을 절단할 때 효율성을 높일 수 있는데, 대용량 데이터를 함수에 복사하지 않고, 주소만 전달하여 성능을 개선할 수 있음
- 포인터는 복잡한 데이터 구조를 구현하는 데 필수적임
8. 포인터의 크기는 항상 같은가?
- 포인터의 크기는 하드웨어 아키텍처에 따라 달라집니다.
- 1 bytes = 8 bits.
- 32비트 시스템에서는 포인터의 크기는 4 byte입니다.
- 64비트 시스템에서는 포인터 크기는 8 byte입니다.
- 하지만 모든 데이터 타입에 대한 포인터의 크기는 각 시스템 마다 동일합니다.
9. 상수 포인터와 포인터의 상수의 차이는?
- 상수 포인터(const pointer)는 포인터가 가리키는 값을 수정할 수 없다는 의미이고, 포인터 상수(pointer to constant)는 포인터 자체의 주소를 수정할 수 없다는 의미입니다. 즉, 상수 포인터는
int *const ptr
과 같이 선언되고, 포인터 상수는const int *ptr
과 같이 선언됩니다.
#include <stdio.h>
int main() {
int value1 = 10;
int value2 = 20;
// 상수 포인터 (pointer to constant): 포인터가 가리키는 값을 변경할 수 없음
const int *ptr_to_const = &value1;
printf("Value via ptr_to_const: %d\n", *ptr_to_const);
// *ptr_to_const = 15; // 오류: 포인터가 가리키는 값 변경 불가
ptr_to_const = &value2; // 포인터가 다른 주소를 가리키는 것은 가능
// 포인터의 상수 (constant pointer): 포인터 자체가 다른 주소를 가리킬 수 없음
int *const const_ptr = &value1;
printf("Value via const_ptr: %d\n", *const_ptr);
*const_ptr = 15; // 포인터가 가리키는 값 변경은 가능
// const_ptr = &value2; // 오류: 포인터가 다른 주소를 가리킬 수 없음
// 상수 포인터의 상수 (constant pointer to constant): 포인터도 다른 주소를 가리킬 수 없고, 값도 변경 불가
const int *const const_ptr_to_const = &value1;
printf("Value via const_ptr_to_const: %d\n", *const_ptr_to_const);
// *const_ptr_to_const = 25; // 오류: 포인터가 가리키는 값 변경 불가
// const_ptr_to_const = &value2; // 오류: 포인터가 다른 주소를 가리킬 수 없음
return 0;
}
10. Dangling Pointer란 무엇인가요?
- Dangling Pointer는 더 이상 유효한 메모리 위치를 가리키지 않는 포인터를 의미합니다. 예를 들어, 메모리를 해제한 후에도 해당 메모리를 가리키는 포인터를 사용하면 Dangling Pointer가 됩니다. 이러한 포인터를 사용하면 예측할 수 없는 동작이 발생할 수 있으므로 주의가 필요합니다.
#include <stdio.h>
#include <stdlib.h>
int main() {
int *dangling_ptr = NULL;
{
int temp = 42;
dangling_ptr = &temp; // temp는 여기서 유효함
} // temp의 유효 범위가 끝남, dangling_ptr은 이제 댕글링 포인터
// 댕글링 포인터 사용 (위험)
printf("Dangling pointer value: %d\n", *dangling_ptr); // 정의되지 않은 동작
// 동적 메모리 할당 후 해제한 경우의 댕글링 포인터 예시
int *dynamic_ptr = (int *)malloc(sizeof(int));
*dynamic_ptr = 100;
free(dynamic_ptr); // 메모리 해제, dynamic_ptr은 이제 댕글링 포인터
// 댕글링 포인터 사용 (위험)
printf("Dangling pointer value after free: %d\n", *dynamic_ptr); // 정의되지 않은 동작
return 0;
}
11. 함수 포인터란?
- 함수 포인터는 함수의 주소를 가리키는 포인터, 함수를 매개변수로 전달하거나, 런타임에 호출할 함수를 결정할 수 있다.
- 함수이름에 포인터를 붙이고 괄호로 묶는다.
- 콜백 함수나, 동적함수 호출에 유용하다.
void hello()
{
printf("Hello, World!\n");
}
int add(int a, int b)
{
return a+b;
}
int main()
{
void (*fp)();
fp = hello;
fp();
int (*fp)(int, int);
fp = add;
fp(10, 20);
}
콜백함수 예시
// A가 배포함 라이브러리 함수, 콜백을 구현할 수 있도록 만듦
int sum(int a, int b, void (*funcp)(int*,int*)) {
if (funcp != NULL) {
funcp(&a, &b);
}
return a + b;
}
// B가 만든 사용자 정의 함수 콜백용으로 만든 함수
void userdefine(int* a, int* b) {
if (*a < 0) *a = *a * -1; // 절대값을 취함
if (*b < 0) *b = *b * -1;
}
int main() {
int result = 0;
result = sum(2, -2, NULL);
printf("콜백 함수 없이 호출 한 결과 : %d \n", result);
result = sum(2, -2, userdefine);
printf("콜백 함수를 정의하여 호출 한 결과 : %d\n", result);
return 0;
}
요약
포인터는 C 언어에서 매우 강력하고 중요한 도구로, 메모리 관리와 데이터 구조에 큰 유연성을 제공합니다. 포인터의 개념을 이해하고 잘 활용하면 C 프로그램의 성능과 기능을 극대화할 수 있습니다. 그러나 포인터의 잘못된 사용은 심각한 버그를 일으킬 수 있으므로 주의해서 사용해야 합니다.
'Interview > C' 카테고리의 다른 글
C언어 Shift 연산자 (0) | 2024.10.17 |
---|---|
C언어 Bit & 연산자 (0) | 2024.10.17 |