Clang

[크래프톤 정글 ] 포인터

하루이2222 2024. 10. 5. 10:37

C 언어에서 포인터는 변수의 메모리 주소를 저장하는 변수이다.

즉, 포인터는 다른 변수나 메모리 상의 데이터의 위치를 가리키는 역할을 한다. 이로 인해 직접적으로 메모리 주소를 다룰 수 있는 유연성과 강력함을 제공하지만, 그만큼 주의가 필요하다.

포인터는 메모리 관리, 함수 매개변수 전달, 동적 메모리 할당 등의 영역에서 중요한 역할을 한다.

1. 포인터 선언과 기본 사용법

포인터는 메모리 주소를 저장하는 변수이므로, 선언할 때는 데이터 타입 뒤에 *을 붙여 선언한다.

int *ptr; // int형 변수를 가리키는 포인터

이 선언은 ptrint형 데이터를 가리킬 수 있는 포인터임을 의미한다. 하지만 아직 ptr은 유효한 주소를 가리키고 있지 않으므로, 초기화되지 않은 상태에서 이를 사용하면 안 된다.

1.1 포인터의 초기화

포인터는 변수의 주소로 초기화할 수 있다. 주소를 얻기 위해서는 변수명 앞에 & 연산자를 사용한다.

int a = 10;
int *ptr = &a;  // ptr은 변수 a의 주소를 가리킨다

위 코드에서 ptr은 변수 a의 주소를 가리키며, 포인터를 통해 a에 접근할 수 있다.

1.2 포인터를 통한 값 접근 (역참조)

포인터가 가리키는 주소에 있는 값을 접근하거나 변경하려면 * 연산자를 사용한다. 이를 역참조(dereferencing)라고 한다.

int a = 10;
int *ptr = &a;

printf("%d\n", *ptr);  // 출력: 10 (포인터가 가리키는 값)
*ptr = 20;             // a의 값을 20으로 변경
printf("%d\n", a);     // 출력: 20

위 예에서 *ptra를 참조하여 a의 값을 출력하고, 이후 *ptr = 20;을 통해 a의 값을 변경할 수 있다.

2. 포인터의 종류

포인터는 다양한 용도로 사용되며, 포인터의 종류도 여러 가지가 있다.

2.1 NULL 포인터

초기화되지 않은 포인터를 사용하는 것은 위험하다. 이를 방지하기 위해 포인터를 명시적으로 NULL로 초기화할 수 있다. NULL 포인터는 아무런 유효한 주소도 가리키지 않는 포인터이다.

int *ptr = NULL;

NULL 포인터를 사용하면, 포인터가 유효한 주소를 가리키고 있는지 확인할 수 있다.

if (ptr != NULL) {
    // 포인터가 유효한 주소를 가리킬 때 실행되는 코드
}

2.2 배열과 포인터

C에서는 배열의 이름이 포인터와 같은 역할을 한다. 배열의 이름은 배열의 첫 번째 원소의 주소를 가리킨다.

int arr[5] = {1, 2, 3, 4, 5};
int *ptr = arr;  // arr는 배열의 첫 번째 원소의 주소를 가리킨다

for (int i = 0; i < 5; i++) {
    printf("%d ", *(ptr + i));  // 배열의 각 원소를 출력
}

여기서 ptr + i는 배열의 i번째 원소의 주소를 가리키며, *(ptr + i)는 그 값을 출력한다.

2.3 함수 포인터

C에서는 함수도 포인터로 다룰 수 있다. 함수의 주소를 저장하는 포인터를 함수 포인터라고 한다.

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

int (*func_ptr)(int, int) = &add;  // add 함수의 주소를 func_ptr에 저장
printf("%d\n", func_ptr(2, 3));    // 2와 3을 더한 결과 출력 (5)

위 코드에서 func_ptradd 함수를 가리키며, 함수 포인터를 사용해 함수를 호출할 수 있다.

2.4 포인터 배열

포인터 배열은 포인터의 배열이다. 즉, 여러 개의 포인터를 배열처럼 관리할 수 있다.

int a = 1, b = 2, c = 3;
int *arr[3] = {&a, &b, &c};

for (int i = 0; i < 3; i++) {
    printf("%d ", *arr[i]);  // 각 포인터가 가리키는 값 출력
}

포인터 배열을 통해 여러 변수의 주소를 저장하고 관리할 수 있다.

3. 동적 메모리 할당과 포인터

포인터는 동적 메모리 할당에도 중요한 역할을 한다. malloc, calloc, realloc 함수들은 메모리의 동적 할당을 위해 사용되며, 이 함수들은 메모리의 시작 주소를 반환한다.

int *ptr = (int *)malloc(5 * sizeof(int));  // int형 5개 크기의 메모리 할당

if (ptr == NULL) {
    printf("메모리 할당 실패\n");
    return 1;
}

for (int i = 0; i < 5; i++) {
    ptr[i] = i + 1;  // 할당된 메모리에 값 저장
}

for (int i = 0; i < 5; i++) {
    printf("%d ", ptr[i]);  // 저장된 값 출력
}

free(ptr);  // 동적 할당된 메모리 해제

위 코드에서 malloc을 사용해 메모리를 동적으로 할당하고, free를 통해 해제해주어야 한다.

4. 포인터 연산

포인터는 단순히 주소를 저장하는 것뿐만 아니라, 다음과 같은 연산이 가능하다.

4.1 포인터의 덧셈과 뺄셈

포인터에 정수를 더하거나 빼면, 가리키는 메모리 주소가 해당 자료형의 크기만큼 이동한다.

int arr[5] = {1, 2, 3, 4, 5};
int *ptr = arr;

ptr++;  // 다음 int형 변수의 주소로 이동 (4바이트 증가)
printf("%d\n", *ptr);  // 출력: 2

4.2 포인터끼리의 뺄셈

두 포인터를 뺄 수도 있다. 이는 두 포인터가 가리키는 요소 사이의 거리를 나타낸다.

int arr[5] = {1, 2, 3, 4, 5};
int *ptr1 = &arr[4];
int *ptr2 = &arr[0];

printf("%ld\n", ptr1 - ptr2);  // 출력: 4 (포인터 사이의 거리)

포인터끼리의 연산은 같은 배열이나 메모리 블록 내에서만 안전하다.

5. 포인터와 상수

5.1 상수 포인터

상수 포인터는 포인터 자체가 상수인 경우로, 한 번 특정 주소를 가리키면 그 이후에는 다른 주소를 가리킬 수 없다.

int a = 10;
int b = 20;
int *const ptr = &a;  // ptr은 상수 포인터

*ptr = 15;  // OK
// ptr = &b;  // 오류: ptr이 다른 주소를 가리킬 수 없음

5.2 상수를 가리키는 포인터

상수를 가리키는 포인터는 포인터가 가리키는 값을 변경할 수 없다.

int a = 10;
const int *ptr = &a;  // 상수를 가리키는 포인터

// *ptr = 20;  // 오류: 상수 값을 변경할 수 없음
ptr = &b;    // OK: 다른 주소를 가리키는 것은 가능

6. dot 과 allow 연산자

C에서 구조체를 다룰 때 .(dot)과 ->(arrow)는 구조체의 멤버에 접근하는 방법을 나타낸다. 이 두 연산자는 포인터와 구조체 변수를 다룰 때 각각 사용되며, 그 차이는 다음과 같다.

6.1. . (dot) 연산자

. 연산자는 구조체 변수에서 직접적으로 멤버에 접근할 때 사용된다. 구조체 변수가 직접적으로 정의되어 있을 때, 그 변수의 멤버에 접근하기 위해 사용된다.

예시:

#include <stdio.h>

typedef struct {
    int x;
    int y;
} Point;

int main() {
    Point p1;  // 구조체 변수 선언
    p1.x = 10; // . 연산자를 사용해 멤버에 접근
    p1.y = 20;

    printf("p1.x = %d, p1.y = %d\n", p1.x, p1.y);
    return 0;
}

설명:

  • Point p1;는 구조체 변수를 선언한 것이다.
  • p1.xp1.y. 연산자를 사용하여 p1 구조체의 멤버에 접근한다.

6.2. -> (arrow) 연산자

-> 연산자는 구조체 포인터에서 간접적으로 멤버에 접근할 때 사용된다. 구조체 포인터를 통해 멤버에 접근할 때, -> 연산자를 사용해 쉽게 멤버에 접근할 수 있다. 이는 구조체 포인터가 가리키는 메모리 위치에서 멤버를 참조하는 역할을 한다.

예시:

#include <stdio.h>
#include <stdlib.h>

typedef struct {
    int x;
    int y;
} Point;

int main() {
    Point *p1 = malloc(sizeof(Point));  // 구조체 포인터 선언 및 메모리 할당
    p1->x = 10;  // -> 연산자를 사용해 멤버에 접근
    p1->y = 20;

    printf("p1->x = %d, p1->y = %d\n", p1->x, p1->y);

    free(p1);  // 메모리 해제
    return 0;
}

설명:

  • Point *p1 = malloc(sizeof(Point));는 동적으로 구조체를 위한 메모리를 할당하고, 그 주소를 포인터 p1에 저장한다.
  • p1->xp1->y-> 연산자를 사용하여 포인터 p1이 가리키는 구조체의 멤버에 접근한다.

차이 요약:

  • . (dot) 연산자구조체 변수에서 멤버에 접근할 때 사용된다.
  • -> (arrow) 연산자구조체 포인터에서 멤버에 접근할 때 사용된다.

 

 

함수에 인자 를 전달할때도 포인터 개념은 중요하게 사용된다. 아래서 함수에 인자를 전달하는 두가지 방법에 대해 살펴보자 .

 

Call by Value vs Call by Reference

Call by Value

일반적으로 C에서 함수에 인자를 전달할 때 Call by Value 방식이 기본이다. 즉, 함수에 전달된 변수의 "값"을 복사하여 함수로 넘기기 때문에 함수 내에서 인자의 값을 변경하더라도 원래 변수에는 영향을 미치지 않는다.

#include <stdio.h>

void modifyValue(int x) {
    x = 20;
}

int main() {
    int a = 10;
    modifyValue(a);
    printf("a: %d\n", a);  // 출력: a: 10
    return 0;
}

위 코드에서 modifyValue 함수는 a의 복사본을 받아 x의 값을 수정하지만, 실제 a의 값에는 아무런 영향을 주지 않는다.

Call by Reference (포인터를 이용한 구현)

반면에 Call by Reference 방식은 변수의 "주소"를 전달함으로써 함수에서 원본 데이터를 수정할 수 있다. 이를 위해 포인터를 사용하여 변수의 주소를 함수에 전달하고, 함수 내에서 이 주소를 통해 원본 변수의 값을 변경한다.

#include <stdio.h>

void modifyValue(int *x) {
    *x = 20;  // x가 가리키는 메모리의 값을 변경
}

int main() {
    int a = 10;
    modifyValue(&a);  // a의 주소를 전달
    printf("a: %d\n", a);  // 출력: a: 20
    return 0;
}

2. 포인터를 이용한 Call by Reference 동작

위 코드에서 modifyValue 함수는 int형 변수를 가리키는 포인터 x를 매개변수로 받는다. 이를 통해 전달된 주소에 직접 접근하여, 그 주소가 가리키는 변수의 값을 변경한다.

void modifyValue(int *x) {
    *x = 20;  // 포인터 x가 가리키는 메모리의 값을 변경
}

*x는 포인터 x가 가리키는 메모리 주소의 값을 의미하며, *x = 20;은 그 주소에 있는 값을 20으로 변경하는 것이다.

3. Call by Reference와 포인터의 관계

C에서는 Call by Reference를 직접적으로 지원하지 않지만, 포인터를 이용해 Call by Reference와 유사한 기능을 구현할 수 있다. 함수에 인자의 주소를 넘기고, 그 주소를 사용하여 원본 데이터를 수정하는 방식은 Call by Reference의 본질과 동일하다.

#include <stdio.h>

// 두 수를 교환하는 함수
void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

int main() {
    int x = 10;
    int y = 20;

    printf("Before swap: x = %d, y = %d\n", x, y);
    swap(&x, &y);  // x와 y의 주소를 전달
    printf("After swap: x = %d, y = %d\n", x, y);

    return 0;
}

4. Call by Reference의 장점

  1. 메모리 효율성: 큰 크기의 데이터를 함수에 전달할 때 Call by Value 방식은 데이터를 복사하므로 메모리와 시간을 더 많이 사용한다. 반면에 Call by Reference는 변수의 주소만을 전달하므로 더 효율적이다.
  2. 변경 가능성: Call by Reference는 함수에서 전달받은 데이터를 직접 수정할 수 있어, 데이터를 반환하지 않고도 함수 내에서 수정된 값을 사용할 수 있다.