C언어 포인터 (Pointer)

박지훈·2021년 6월 1일
2

코딩 도장 사이트 강의 내용과 다른 사람들의 블로그 내용을 참고하여 포스팅하였습니다.

C언어 포인터(Pointer)란?

  • 포인터란 자료가 저장되는 기억장치의 기억주소를 가리키는 지시자이다.

  • 포인터는 다른 기억장소의 자료를 참조하는데 사용되는 데이터이다.

포인터를 쓰는 이유
어떠한 변수이든지 Buffer를 할당받아서 사용하게 되는데, 모든 변수의 저장과 참조는 변수가 저장될 or 저장된 주소를 알아야 가능하다. 그래서 컴퓨터는 변수를 참조할 때 그 변수가 저장되어 있는 주소를 먼저 찾아내고, 그 주소가 가리키는 내용을 참조하게 된다. 이렇게 변수의 주소를 저장하거나 사용하기 위해 포인터를 사용하는 것이다.

[장점]
1. 간결하고 효율적인 표현과 처리가 가능
2. 더 빠른 기계어 코드를 생성할 수 있음 (값으로 접근하는 것이 아닌 주소로 빠르게 접근하기 때문이다.)
3. 복잡한 자료구조(배열, 구조체 등)와 함수의 쉬운 접근이 가능
4. 포인터를 사용하지 않았을 때 코드로 표현할 수 없는 경우가 발생함

예제를 직접 실행하면서 이해해보자.

// 변수의 메모리 주소 구하기 -> 실행할 때마다 달라짐!
#include <stdio.h>

int main()
{
    int num1 = 10;
    
    printf("%p\n", &num1);
    
    return 0;
}

// 포인터 문자열 변수를 이용하여 입출력
#include <stdio.h>
#include <stdlib.h>  

int main(void)
{
    char *s = malloc(sizeof(char) * 100);
    
    printf("문자열 입력: ");
    scanf("%[^\n]s", s); 
    
    printf("저장된 문자열 : %s \n", s);
    free(s);
    
    return 0;
}



포인터 선언

자료형 *포인터이름;
포인터 = &변수;

포인터 선언 시 *(Asterisk)의 위치에 따른 차이는 X

int* numPtr;     // 자료형 쪽에 *을 붙임
int * numPtr;    // 자료형과 변수 가운데 *를 넣음
int *numPtr;     // 변수 쪽에 *을 붙임
// 포인터 메모리 주소값 출력
#include <stdio.h>

int main()
{
    int *numPtr;	// 포인터 변수 선언
    int num1 = 10;
    
    numPtr = &num1;	// num1의 메모리 주소를 포인터 변수에 저장
    
    printf("%p\n", numPtr);	// 주소값 출력
    printf("%p\n", &num1);	// 주소값 출력 (위 출력과 동일)
    
    return 0;
}



역참조

포인터 변수에는 값(value)가 아닌 메모리 주소가 저장되어 있다. 이때 메모리 주소가 있는 곳으로 이동해서 값을 가져오고 싶다면 역참조(dereference) 연산자*을 사용하면 된다.

// 역참조 연산자를 이용하여 값(value) 접근
#include <stdio.h>

int main()
{
    int *numPtr;
    int num1 = 10;
    
    numPtr = &num1;
    
    printf("%d\n", *numPtr);	// num1의 값인 10이 출력
    // 역참조 연산자로 num1의 메모리 주소에 접근하여 값(value)을 가져옴
    
    return 0;
}

// 역참조 연산자를 이용하여 값(value)을 저장
#include <stdio.h>

int main()
{
    int *numPtr;
    int num1 = 10;
    printf("%d\n", num1);
    
    numPtr = &num1;
    *numPtr = 20;	// 역참조 연산자로 메모리 주소에 접근하여 20을 저장
    
    printf("%d\n", *numPtr);	// 역참조 연산자로 메모리 주소에 접근하여 값 가져옴
    printf("%d\n", num1);	// 실제 num1의 값(value)도 바뀜
    
    return 0;
}

포인터를 선언할 때 *"이 변수가 포인터이다!"라는 의미이고, 포인터에 사용할 때 *는 "포인터의 메모리 주소를 역참조하겠다!"라는 의미이다.

int *numPtr;                // 포인터. 포인터를 선언할 때 *
printf("%d\n", *numPtr);    // 역참조. 포인터에 사용할 때 *

추가적으로 포인터는 변수의 메모리 주소만을 가리킨다. 즉, 메모리 공간이 어디에 있는지 위치만 알고 있다.



포인터 자료형

다양한 자료형이 있듯이 포인터에도 다양한 포인터 자료형이 있다. 예제를 통해 알아보자.

#include <stdio.h>

int main()
{
    long long *numPtr1;	// long long형 포인터 선언
    float *numPtr2;
    char *cPtr1;
    
    long long num1 = 10;
    float num2 = 3.5f;
    char c1 = 'a';
    
    numPtr1 = &num1;
    numPtr2 = &num2;
    cPtr1 = &c1;
    
    printf("%lld\n", *numPtr1);
    printf("%f\n", *numPtr2);
	printf("%c\n", *cPtr1);
    
    return 0;
}

위처럼 C언어에서 사용할 수 있는 모든 자료형은 포인터로 만들 수 있다. 여기서 '그냥 포인터 자료형이라고 따로 만들면 되는데 왜 굳이 자료형마다 포인터를 선언할까?'라는 생각이 들 수 있다.

포인터에 저장되는 메모리 주소값은 정수형으로 동일하다. 하지만, 선언하는 자료형에 따라 메모리에 접근하는 방법이 달리지기 때문에 자료형마다 포인터를 선언하는 것이다. 아래의 그림을 보면 포인터를 역참조하면 선언한 자료형의 크기에 맞춰 값을 가져오거나 저장하게 된다.

모든 그림의 출처 : 링크

추가적으로 상수에도 포인터를 선언할 수 있다. 하지만, 상수에 포인터를 선언한 이후로 주소값과 주소값이 가리키는 값(역참조 연산자 이용)을 변경할 수 없다. -> 상수의 개념을 생각하면 된다.



void 포인터

void 포인터는 자료형이 정해지지 않은 포인터이다.

void *포인터이름;	// void 포인터 선언

// void 포인터
#include <stdio.h>

int main()
{
    int num1 = 10;
    char c1 = 'a';
    int *numPtr1 = &num1;
    char *cPtr1 = &c1;
    
    void *ptr;	// void 포인터 선언
    
    // 포인터 자료형이 달라도 오류 X
    ptr = numPtr1;	// void 포인터에 int 포인터 저장
    prt = cPtr1;	// void 포인터에 char 포인터 저장
    
    // 포인터 자료형이 달라도 컴파일 경고가 발생하지 않음
    numPtr1 = ptr;
    cPtr1 = ptr;
    
    return 0;
}

기본적으로 C언어에서는 자료형이 다른 포인터끼리 메모리 주소를 저장하면 오류(컴파일 경고)가 발생한다. 하지만, void 포인터는 자료형이 정해지지 않았으므로 어떠한 포인터 자료형이든 모두 저장할 수 있다. 또한, 반대인 경우 다양한 자료형으로 된 포인터에도 void 포인터를 저장할 수 있다. -> 이러한 특성으로 인해 void 포인터를 '범용 포인터'라고 한다.

추가적으로 void 포인터는 앞서 말했듯이 자료형이 정해지지 않았다. 따라서 값을 가져오거나 저장할 크기도 정해지지 않았기 때문에 void 포인터에는 역참조 연산자를 사용할 수 없다. (즉, 메모리 주소가 가리키는 값(value)을 가져올 수 없다.)



이중 포인터

포인터의 포인터이다. 포인터를 선언할 때 *을 2번 사용하여 선언한다.

자료형 **포인터이름;	// 이중 포인터 선언

// 이중 포인터
#include <stdio.h>

int main()
{
	int *numPtr1;	// 단일 포인터 선언
    int **numPtr2;	// 이중 포인터 선언
    int num1 = 10;
    
    numPtr1 = &num1;
    numPtr2 = &numPtr1;	// numPtr1의 메모리 주소 저장
    
    printf("%d\n", **numPtr2);
    
    return 0;
}

포인터의 메모리 주소는 일반 포인터에는 저장할 수 없다. 따라서 이중 포인터를 사용해야 하는데, int **numPtr2;처럼 이중 포인터에 저장하게 된다.

포인터를 선언할 때 *의 개수에 따라 삼중 포인터, 사중 포인터, 그 이상도 만들 수 있다. 같은 논리로 역참조 연산자를 사용할 때에도 *를 3번, 4번 또는 그 이상을 사용할 수 있다.


profile
Computer Science!!

0개의 댓글