본문 바로가기
XR개발/C언어

C언어 포인터

by 오머리쿠_OmaryKoo 2025. 3. 1.

1. 포인터(Pointer)란?

프로그래밍을 하다 보면 변수를 사용하여 데이터를 저장하는 것이 일반적이다. 하지만 변수가 어디에 저장되는지를 알게 되면 더욱 다양한 기능을 구현할 수 있다. 이를 가능하게 해주는 것이 바로 포인터(Pointer) 이다.

포인터는 변수가 저장된 메모리 주소를 저장하는 변수이다. 즉, 일반 변수는 데이터를 저장하지만, 포인터는 해당 데이터가 저장된 메모리 주소를 저장한다.


A. 변수와 메모리 주소

컴퓨터에서 변수는 메모리의 특정 주소에 저장된다. 변수를 선언하면 운영체제가 해당 변수를 위한 메모리 공간을 할당하고 고유한 주소(address) 를 부여한다.

예를 들어, 아래와 같은 코드가 있다고 가정해보자.

int a = 10;

위 코드에서 a라는 변수는 메모리의 특정 공간에 저장되며, 값 10을 가진다. 하지만 이 변수가 어느 주소에 저장되었는지는 우리가 직접 확인할 수 없다. 이를 확인하기 위해 주소 연산자(&) 를 사용한다.

printf("변수 a의 주소: %p\n", &a);

이제 a가 메모리의 어느 위치에 저장되었는지를 확인할 수 있다. 하지만 이 주소값은 실행할 때마다 달라질 수 있다.


B. 포인터 선언과 사용

포인터를 선언할 때는 어떤 타입의 변수를 가리킬 것인지를 지정해야 한다. 예를 들어, int 타입 변수를 가리키는 포인터는 다음과 같이 선언한다.

int *p;

위 코드에서 *p는 "p는 정수형 변수를 가리키는 포인터"라는 의미이다.

 

포인터에 주소 저장

포인터를 선언한 후에는 반드시 변수의 주소를 저장해야 사용할 수 있다.

int a = 10;
int *p;
p = &a;

이제 pa의 주소를 저장하며, p를 통해 a의 값을 간접적으로 접근할 수 있다.


3. 간접 참조 연산자(*)

포인터가 주소를 저장하지만, 그 주소에 저장된 실제 값을 사용하려면 간접 참조 연산자(*) 를 사용해야 한다.

printf("p가 가리키는 값: %d\n", *p);

위 코드에서 *pp가 가리키는 주소에 저장된 값을 의미한다. 따라서 *pa의 값인 10을 출력한다.


A. 포인터의 초기화와 NULL 포인터

포인터는 반드시 초기화 후 사용해야 한다. 초기화되지 않은 포인터는 임의의 주소를 가리킬 위험이 있어, 프로그램이 비정상적으로 동작할 수 있다.

 

NULL 포인터

포인터를 초기화하지 않은 상태에서 사용하는 것은 위험하다. 이를 방지하기 위해 NULL 포인터를 활용할 수 있다.

int *p = NULL;

NULL은 포인터가 어떤 유효한 메모리도 가리키지 않음을 의미하며, 초기화되지 않은 포인터를 사용할 때보다 안전하다.


B. 포인터의 활용

포인터는 단순히 변수를 가리키는 기능 외에도 다양한 활용이 가능하다.

 

1) 포인터를 사용한 값 변경

포인터를 이용하면 변수를 직접 수정하지 않고도 값을 변경할 수 있다.

int a = 10;
int *p = &a;
*p = 20;

위 코드를 실행하면 a의 값이 20으로 변경된다.

 

2) 포인터를 활용한 값 교환 (Call by Reference)

포인터를 사용하면 함수에서 변수의 값을 직접 변경할 수 있다.

void swap(int *x, int *y) {
    int temp = *x;
    *x = *y;
    *y = temp;
}

int main() {
    int a = 5, b = 10;
    swap(&a, &b);
    printf("a = %d, b = %d\n", a, b);
    return 0;
}

이처럼 포인터를 활용하면 함수에서 직접 변수 값을 수정할 수 있다.


4. 포인터 사용 시 주의할 점

포인터는 강력한 기능을 제공하는 도구지만, 잘못 사용하면 치명적인 오류를 유발할 수 있다. 


A. 초기화되지 않은 포인터 사용 금지

포인터를 선언만 하고 초기화하지 않으면 쓰레기 값을 가질 수 있으며, 이를 사용하면 예기치 않은 동작이나 프로그램 충돌을 초래할 수 있다.

int *p;  // 초기화되지 않은 포인터
*p = 100;  // 위험한 코드: 메모리 어디에 저장될지 모름

이런 문제를 방지하기 위해, NULL 포인터를 사용하여 명시적으로 초기화하는 것이 좋다.

int *p = NULL;

포인터가 유효한 메모리를 가리키기 전에 이를 확인하는 것도 중요하다.

if (p != NULL) {
    *p = 100;  // NULL이 아닐 때만 실행
}

B. 잘못된 메모리 접근

포인터가 유효하지 않은 메모리를 가리키면 Segmentation Fault(메모리 접근 오류)가 발생할 수 있다. 대표적인 예로, 해제된 메모리를 참조하는 경우가 있다.

int *p = (int *)malloc(sizeof(int));
*p = 10;
free(p);
*p = 20;  // 해제된 메모리에 접근 -> 위험한 코드

 

해결 방법:

  • free() 이후에는 포인터를 NULL로 설정하여 잘못된 접근을 방지한다.
free(p);
p = NULL;

C. 포인터 타입 일치

포인터는 반드시 올바른 타입의 데이터를 가리켜야 한다. 다른 타입의 변수를 잘못 가리키면 데이터 크기 불일치로 인해 예상치 못한 동작이 발생할 수 있다.

double *pd;
int a = 10;
pd = &a;  // 잘못된 코드: int를 double 포인터에 저장

이런 문제를 방지하려면 항상 올바른 타입의 포인터를 사용해야 한다.

int *p;
int a = 10;
p = &a;  // 올바른 코드

5. 포인터 연산

포인터는 단순히 주소를 저장하는 변수일 뿐만 아니라, 연산을 수행할 수도 있다. 하지만 포인터 연산은 일반적인 정수 연산과 다르므로 주의해야 한다.


A. 포인터의 덧셈과 뺄셈

포인터에 정수를 더하거나 빼는 연산이 가능하지만, 바이트 단위가 아니라 가리키는 자료형 크기 단위로 증가 또는 감소한다.

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

printf("p = %p\n", p);
p++;
printf("p = %p\n", p);  // int 크기만큼(4바이트) 증가

위 코드에서 p++을 실행하면 p는 4바이트 증가하여 다음 배열 요소를 가리킨다.


B. 포인터 간의 차이 계산

포인터 간의 차이를 구하면 두 메모리 주소가 몇 개의 요소만큼 떨어져 있는지 계산할 수 있다.

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

printf("p2 - p1 = %ld\n", p2 - p1);  // 3 (int형 3개 차이)

위 코드에서 p2 - p1의 결과는 3이 나오며, 이는 두 포인터가 3개의 int형 요소만큼 떨어져 있음을 의미한다.


C. 포인터의 증가 연산과 간접 참조 연산

포인터 연산에서는 증가 연산(++)과 간접 참조(*)를 조합할 때 주의해야 한다.

int a = 10;
int *p = &a;
(*p)++;  // a 값을 증가 (올바른 코드)
p++;  // 포인터를 증가 (다른 메모리를 가리킬 위험)

위 코드에서 (*p)++a의 값을 증가시키지만, p++p가 다른 메모리를 가리키도록 한다. 따라서 포인터를 증가할 때는 현재 가리키는 데이터인지, 다음 주소로 이동하는 것인지 명확하게 구분해야 한다.


6. 포인터와 함수

A. 함수 호출 시 인수 전달 방식

C 언어에서 함수가 데이터를 전달받는 방식에는 두 가지가 있다.

  • 값에 의한 호출(Call by Value): 변수의 값을 복사하여 전달하는 방식
  • 참조에 의한 호출(Call by Reference): 변수의 주소를 전달하는 방식

값에 의한 호출은 함수 내부에서 값을 변경하더라도 원래 변수에 영향을 미치지 않는다. 반면, 참조에 의한 호출을 사용하면 함수가 직접 원래 변수를 수정할 수 있다.


 

 

C언어 함수

1. 함수란 무엇인가?함수(Function)는 특정 작업을 수행하는 코드 블록에 이름을 부여한 것이다. 함수를 사용하면 반복되는 코드의 중복을 줄이고, 프로그램의 구조를 더 명확하게 만들 수 있다. 특

omarykoo.tistory.com


B. 값에 의한 호출 예제

#include <stdio.h>
void swap(int x, int y) {
    int temp = x;
    x = y;
    y = temp;
}

int main(void) {
    int a = 100, b = 200;
    swap(a, b);
    printf("a = %d, b = %d\n", a, b); // 값 변경되지 않음
    return 0;
}

위 예제에서는 swap 함수가 값을 복사하여 사용하기 때문에 ab는 변경되지 않는다.


C. 참조에 의한 호출 예제 (포인터 활용)

#include <stdio.h>
void swap(int *px, int *py) {
    int temp = *px;
    *px = *py;
    *py = temp;
}

int main(void) {
    int a = 100, b = 200;
    swap(&a, &b);
    printf("a = %d, b = %d\n", a, b); // 값이 변경됨
    return 0;
}

포인터를 사용하여 swap을 구현하면 함수 내에서 변수의 값을 직접 변경할 수 있다.


D. scanf 함수와 포인터

scanf 함수는 사용자가 입력한 값을 변수에 저장하는 역할을 한다. 이는 변수의 주소를 scanf에 전달하기 때문에 가능하다.

int x;
scanf("%d", &x); // x의 주소를 전달하여 입력값 저장

만약 &x를 전달하지 않고 x만 전달하면 메모리 접근 오류가 발생할 수 있다.


E. 여러 값을 반환하는 방법

C 언어에서는 하나의 함수가 여러 값을 반환하는 기능이 없다. 하지만 포인터를 사용하면 여러 값을 전달할 수 있다.

#include <stdio.h>
int get_line_parameter(int x1, int y1, int x2, int y2, float *slope, float *y_intercept) {
    if (x1 == x2) return -1;
    *slope = (float)(y2 - y1) / (x2 - x1);
    *y_intercept = y1 - (*slope) * x1;
    return 0;
}

위 함수는 두 점의 기울기와 y절편을 계산하여 포인터를 통해 값을 반환한다.


7. 포인터와 배열

A. 배열의 이름과 포인터

배열의 이름은 배열의 첫 번째 요소의 주소를 의미한다.

int a[3] = {10, 20, 30};
printf("a = %p\n", a);
printf("&a[0] = %p\n", &a[0]);

위 코드에서 a&a[0]는 같은 주소를 출력한다.


 

 

C언어 배열

1. 배열이란?배열(array)은 같은 데이터 타입의 여러 개의 값을 하나의 변수로 관리할 수 있는 자료구조이다. 기존 변수는 하나의 값만 저장할 수 있지만, 배열을 사용하면 여러 개의 데이터를 연

omarykoo.tistory.com


B. 포인터를 이용한 배열 접근

배열의 각 요소는 포인터를 사용하여 접근할 수도 있다.

int a[3] = {10, 20, 30};
int *p = a; // 배열의 첫 번째 요소를 가리킴
printf("%d\n", *(p + 1)); // 두 번째 요소 값 (20)

포인터를 이용하면 a[i]와 같은 방식으로 배열 요소에 접근할 수 있다.


C. 포인터를 이용한 배열 값 변경

배열을 포인터로 가리키면 배열 요소의 값을 변경할 수도 있다.

int a[3] = {10, 20, 30};
int *p = a;
*p = 50; // a[0] = 50
*(p + 1) = 60; // a[1] = 60

D. 함수에서 배열을 전달하는 방법

함수에 배열을 전달하면 배열 전체가 복사되는 것이 아니라 배열의 주소만 전달된다.

void modify_array(int *b, int size) {
    b[0] = 4;
    b[1] = 5;
    b[2] = 6;
}

int main(void) {
    int a[3] = {1, 2, 3};
    modify_array(a, 3);
    printf("%d %d %d\n", a[0], a[1], a[2]); // 4 5 6
    return 0;
}

함수 modify_array에서 배열을 전달받아 내부 값을 변경하면, 원래 배열도 함께 변경된다.


E. 포인터 연산을 이용한 배열 탐색

배열 요소를 탐색할 때 포인터를 활용하면 코드가 간결해진다.

int get_sum(int *a, int n) {
    int sum = 0;
    for (int i = 0; i < n; i++) sum += *(a + i);
    return sum;
}

포인터를 이용하여 배열 요소를 직접 접근하는 방식은 효율적이며, 특히 임베디드 시스템에서 자주 활용된다.


8. 포인터 사용의 장점

프로그래밍에서 **포인터(pointer)**는 단순히 변수의 주소를 저장하는 도구가 아니라 다양한 기능을 수행할 수 있도록 도와주는 강력한 개념이다. 포인터를 활용하면 일반적인 변수만으로는 구현하기 어려운 기능들을 효과적으로 구현할 수 있다.


A. 연결 리스트 및 트리 같은 동적 자료 구조 구현

배열은 요소들이 메모리에서 연속된 공간을 차지하는 반면, 포인터를 활용하면 연결 리스트(Linked List), 트리(Tree) 등과 같은 동적 자료 구조를 구현할 수 있다.

  • 배열의 한계: 배열은 크기가 고정되어 있어 동적으로 요소를 추가하거나 삭제하는 것이 어렵다.
  • 연결 리스트의 유용성: 연결 리스트는 포인터를 이용하여 메모리의 여러 위치를 동적으로 연결할 수 있어, 삽입과 삭제 연산이 효율적이다.
  • 트리 구조: 이진 트리(Binary Tree)나 그래프(Graph)와 같은 복잡한 구조도 포인터를 사용하면 효율적으로 표현할 수 있다.

연결 리스트 예시

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

struct Node {
    int data;
    struct Node* next;
};

int main() {
    struct Node* head = (struct Node*)malloc(sizeof(struct Node));
    head->data = 10;
    head->next = (struct Node*)malloc(sizeof(struct Node));
    head->next->data = 20;
    head->next->next = NULL;
    
    printf("첫 번째 노드: %d\n", head->data);
    printf("두 번째 노드: %d\n", head->next->data);
    return 0;
}

위 코드에서는 포인터를 활용하여 연결 리스트를 구현하고 있다. 이러한 구조는 데이터를 유동적으로 추가하고 제거하는 데 매우 유용하다.


B. 메모리 매핑 하드웨어 제어

포인터는 단순한 변수 조작을 넘어 하드웨어를 제어하는 데도 활용된다. **메모리 매핑(memory-mapped hardware)**을 통해 하드웨어 장치의 특정 주소에 직접 접근하여 제어할 수 있다.

  • 임베디드 시스템: 마이크로컨트롤러에서 포인터를 활용하여 LED나 센서 같은 하드웨어를 조작할 수 있다.
  • 운영체제 개발: 커널 프로그래밍에서 메모리 주소를 직접 조작할 때 포인터가 필수적이다.

예제 (메모리 매핑)

volatile int *hw_address = (volatile int *)0x7FFF;
*hw_address = 0x0001; // 하드웨어 장치에 값 쓰기

위 코드에서 특정 메모리 주소(0x7FFF)에 접근하여 값을 쓰는 것을 볼 수 있다. 이는 하드웨어를 직접 조작할 때 매우 중요한 기능이다.


C. 참조에 의한 함수 호출(Call by Reference)

C 언어는 기본적으로 **값에 의한 호출(Call by Value)**를 사용하지만, 포인터를 활용하면 **참조에 의한 호출(Call by Reference)**이 가능해진다.

  • 값에 의한 호출의 한계: 기본적으로 함수에 값을 전달하면 해당 값이 복사되므로, 함수 내에서 값을 변경해도 원본 변수에는 영향을 주지 않는다.
  • 참조에 의한 호출의 장점: 포인터를 이용하여 함수에서 변수의 주소를 전달하면, 함수 내부에서 직접 원본 값을 변경할 수 있다.

예제 (Call by Reference 활용)

#include <stdio.h>

void swap(int *x, int *y) {
    int temp = *x;
    *x = *y;
    *y = temp;
}

int main() {
    int a = 10, b = 20;
    swap(&a, &b);
    printf("a = %d, b = %d\n", a, b); // a = 20, b = 10
    return 0;
}

위 코드에서는 swap 함수가 포인터를 사용하여 원본 변수의 값을 직접 변경하고 있다.


D. 동적 메모리 할당

배열은 크기가 고정되어 있어 실행 중 크기를 변경할 수 없지만, **동적 메모리 할당(dynamic memory allocation)**을 이용하면 실행 중에도 메모리를 유연하게 관리할 수 있다.

  • malloc(), free() 함수 사용: 프로그램 실행 중 필요한 만큼 메모리를 할당하고, 사용이 끝나면 해제할 수 있다.
  • 메모리 낭비 방지: 필요한 만큼만 할당하여 불필요한 메모리 사용을 줄일 수 있다.

동적 메모리 할당 예제

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

int main() {
    int *arr = (int *)malloc(5 * sizeof(int));
    for (int i = 0; i < 5; i++) {
        arr[i] = i * 10;
    }
    for (int i = 0; i < 5; i++) {
        printf("%d ", arr[i]);
    }
    free(arr);
    return 0;
}

위 코드에서는 malloc()을 사용하여 크기가 5인 정수 배열을 동적으로 생성하고, free()를 사용하여 메모리를 해제하고 있다.

728x90

'XR개발 > C언어' 카테고리의 다른 글

C오답노트01_출력과 입력  (0) 2025.03.19
C언어 문자와 문자열  (1) 2025.03.02
C언어 배열  (2) 2025.02.28
10_Q&A 함수원형에 대한 물음.zip  (0) 2025.02.16
09_Q&A 알고리즘에 대한 물음.zip  (0) 2025.02.15