본문 바로가기
C언어

C언어 포인터

by godfeeling 2020. 7. 5.

주소값의 이해

데이터의 주소값이란 해당 데이터가 저장된 메모리의 시작 주소를 의미합니다.

 

C언어에서는 이러한 주소값을 1바이트 크기의 메모리 공간으로 나누어 표현합니다.

 

예를 들어, int형 데이터는 4바이트의 크기를 가지지만, int형 데이터의 주소값은 시작 주소 1바이트만을 가리킵니다.

 

포인터란?

C언어에서 포인터(pointer)란 메모리의 주소값을 저장하는 변수이며, 포인터 변수라고도 부릅니다.

 

char형 변수가 문자를 저장하고, int형 변수가 정수를 저장하는 것처럼 포인터는 주소값을 저장합니다.

 

예제

int n = 100; // 변수의 선언

 

int *ptr = &n; // 포인터의 선언

 

포인터 연산자

C언어에서 포인터와 연관되어 사용되는 연산자는 다음과 같습니다.

 

 

 

1. 주소 연산자(&)

 

2. 참조 연산자(*)

 

주소 연산자(&)

주소 연산자는 변수의 이름 앞에 사용하여, 해당 변수의 주소값을 반환합니다.

 

'&'기호는 앰퍼샌드(ampersand)라고 읽으며, 번지 연산자라고도 불립니다.

 

참조 연산자(*)

참조 연산자는 포인터의 이름이나 주소 앞에 사용하여, 포인터에 가리키는 주소에 저장된 값을 반환합니다.

 

 

 

C언어에서 '*'기호는 사용하는 위치에 따라 다양한 용도로 사용됩니다.

 

이항 연산자로 사용하면 곱셈 연산으로 사용되며, 포인터의 선언 시나 메모리에 접근할 때도 사용됩니다.

 

포인터의 선언

C언어에서 포인터는 다음 문법에 따라 선언합니다.

 

문법

타입* 포인터이름;

 

 

 

타입이란 포인터가 가리키고자 하는 변수의 타입을 명시합니다.

 

포인터 이름은 포인터가 선언된 후에 포인터에 접근하기 위해 사용됩니다.

 

 

 

포인터를 선언한 후 참조 연산자(*)를 사용하기 전에 포인터는 반드시 먼저 초기화되어야 합니다.

 

그렇지 않으면 의도하지 않은 메모리의 값을 변경하게 되기 때문입니다.

 

따라서 C 컴파일러는 초기화하지 않은 포인터에 참조 연산자를 사용하면 오류를 발생시킵니다.

 

 

 

따라서 다음과 같이 포인터의 선언과 동시에 초기화를 함께 하는 것이 좋습니다.

 

문법

타입* 포인터이름 = &변수이름;

 

또는

 

타입* 포인터이름 = 주소값;

 

포인터의 참조

C언어에서 선언된 포인터는 참조 연산자(*)를 사용하여 참조할 수 있습니다.

 

 

 

다음 예제는 포인터의 주소값과 함께 포인터가 가리키고 있는 주소값의 데이터를 참조하는 예제입니다.

 

예제

int x = 7; // 변수의 선언

 

int *ptr = &x; // 포인터의 선언

 

int *pptr = &ptr; // 포인터의 참조

 

 

예제

int num01 = 1234;

 

double num02 = 3.14;

 

int* ptr_num01 = &num01;

 

double* ptr_num02 = &num02;

 

 

 

printf("포인터의 크기는 %d입니다.\n", sizeof(ptr_num01));

 

printf("포인터 ptr_num01이 가리키고 있는 주소값은 %#x입니다.\n", ptr_num01);

 

printf("포인터 ptr_num02가 가리키고 있는 주소값은 %#x입니다.\n", ptr_num02);

 

printf("포인터 ptr_num01이 가리키고 있는 주소에 저장된 값은 %d입니다.\n", *ptr_num01);

 

printf("포인터 ptr_num02가 가리키고 있는 주소에 저장된 값은 %f입니다.\n", *ptr_num02);

 

위 예제의 번 라인에서는 sizeof 연산자를 사용하여 포인터 변수의 크기를 구하고 있습니다.

 

 

 

포인터 변수는 메모리에서 변수의 위치를 나타내는 주소를 다루는 변수이므로, 그 크기는 일반적으로 CPU에 따라 결정됩니다.

 

따라서 32비트 CPU에서는 1워드(word)의 크기가 4바이트이므로, 포인터 변수의 크기 또한 4바이트가 될 것입니다.

 

 

 

하지만 이러한 포인터 변수의 크기는 컴파일러로 컴파일할 때 그 크기까지 직접 명시할 수 있습니다.

 

따라서 포인터 변수의 크기는 CPU의 종류와 컴파일할 때 사용된 컴파일러의 정책에 따라서 달라질 수 있습니다.

 

 

 

또한, 번과 번 라인에서처럼 포인터가 가리키는 변수의 타입에 따라 포인터의 타입도 같이 바꿔주고 있습니다.

 

포인터의 타입은 참조 연산자를 통해 값을 참조할 때, 참조할 메모리의 크기를 알려주는 역할을 하기 때문입니다.

 

워드(word)CPU가 한 번에 처리할 수 있는 데이터의 크기입니다.

1바이트는 8비트이므로 32비트 시스템에서는 32비트 / 8비트 = 4, 4바이트가 1워드(word)로 처리됩니다.

 

64비트 시스템에서는 64비트 / 8비트 = 8, 8바이트가 1워드(word)로 처리됩니다.

 

포인터 연산

포인터는 값을 증가시키거나 감소시키는 등의 제한된 연산만을 할 수 있습니다.

 

 

 

C언어의 포인터 연산에는 다음과 같은 규칙이 있습니다.

 

 

 

1. 포인터끼리의 덧셈, 곱셈, 나눗셈은 아무런 의미가 없습니다.

 

2. 포인터끼리의 뺄셈은 두 포인터 사이의 상대적 거리를 나타냅니다.

 

3. 포인터에 정수를 더하거나 뺄 수는 있지만, 실수와의 연산은 허용하지 않습니다.

 

4. 포인터끼리 대입하거나 비교할 수 있습니다.

 

 

 

다음 예제는 타입에 따른 포인터 연산의 증가값을 비교하는 예제입니다.

 

예제

char* ptr_char = 0;

 

int* ptr_int = NULL;

 

double* ptr_double = 0x00;

 

 

 

printf("포인터 ptr_char가 현재 가리키고 있는 주소값은 %#x입니다.\n", ptr_char);

 

printf("포인터 ptr_int가 현재 가리키고 있는 주소값은 %#x입니다.\n", ptr_int);

 

printf("포인터 ptr_double이 현재 가리키고 있는 주소값은 %#x입니다.\n", ptr_double);

 

 

 

printf("포인터 ptr_char1 증가 후에 가리키고 있는 주소값은 %#x입니다.\n", ++ptr_char);

 

printf("포인터 ptr_int1 증가 후에 가리키고 있는 주소값은 %#x입니다.\n", ++ptr_int);

 

printf("포인터 ptr_double1 증가 후에 가리키고 있는 주소값은 %#x입니다.\n", ++ptr_double);

 

위의 예제에서 모든 포인터에 저장된 초기 주소값은 0x00 입니다.

 

하지만 1을 증가시키는 포인터 연산 후 포인터가 가리키고 있는 주소는 각각의 포인터 타입에 따라 달라집니다.

 

그 증가 폭은 포인터가 가리키는 변수의 타입의 크기와 같게 됩니다.

 

예를 들어, int형 포인터의 증가폭은 int형 타입의 크기인 4바이트만큼 증가하게 됩니다.

 

이 법칙은 포인터의 뺄셈에서도 똑같이 적용됩니다.

 

 

 

다음 예제는 포인터끼리의 비교 연산과 대입 연산을 보여주는 예제입니다.

 

예제

int num01 = 10;

 

int num02 = 20;

 

int *ptr_num01 = &num01;

 

int *ptr_num02 = &num02;

 

 

 

if (ptr_num01 != ptr_num02) // 포인터끼리의 비교 연산

 

{

 

printf("포인터 ptr_num01이 가리키고 있는 주소에 저장된 값은 %d입니다.\n", *ptr_num01);

 

printf("포인터 ptr_num02가 가리키고 있는 주소에 저장된 값은 %d입니다.\n", *ptr_num02);

 

printf("포인터 ptr_num01ptr_num02는 현재 다른 주소를 가리키고 있습니다.\n\n");

 

ptr_num02 = ptr_num01; // 포인터끼리의 대입 연산

 

}

 

 

 

printf("포인터 ptr_num01이 가리키고 있는 주소에 저장된 값은 %d입니다.\n", *ptr_num01);

 

printf("포인터 ptr_num02가 가리키고 있는 주소에 저장된 값은 %d입니다.\n", *ptr_num02);

 

 

 

if (ptr_num01 == ptr_num02) // 포인터끼리의 비교 연산

 

{

 

printf("포인터 ptr_num01ptr_num02는 현재 같은 주소를 가리키고 있습니다.\n");

 

}

 

 

인수 전달 방법

함수를 호출할 때에는 함수에 필요한 데이터를 인수(argument)로 전달해 줄 수 있습니다.

 

이렇게 함수에 인수를 전달하는 방법에는 크게 다음과 같이 두 가지 방법이 있습니다.

 

 

 

1. 값에 의한 전달(call by value)

 

2. 참조에 의한 전달(call by reference)

 

값에 의한 전달(call by value)

값에 의한 전달 방법은 인수로 전달되는 변수가 가지고 있는 값을 함수 내의 매개변수에 복사하는 방식입니다.

 

이렇게 복사된 값으로 초기화된 매개변수는 인수로 전달된 변수와는 완전히 별개의 변수가 됩니다.

 

따라서 함수 내에서의 매개변수 조작은 인수로 전달되는 변수에 아무런 영향을 미치지 않습니다.

 

 

 

예제

#include <stdio.h>

 

void local(int);

 

 

 

int main(void)

 

{

 

int var = 10;

 

printf("변수 var의 초깃값은 %d입니다.\n", var);

 

 

 

local(var);

 

printf("local() 함수 호출 후 변수 var의 값은 %d입니다.\n", var);

 

return 0;

 

}

 

 

 

void local(int num)

 

{

 

num += 10;

 

}

 

위의 예제에서 local() 함수의 매개변수 num는 인수로 변수 var의 값을 전달받습니다.

 

따라서 함수 내에서 매개변수 num의 값을 아무리 변경하더라도 원래 인수로 전달된 변수 var의 값은 절대 변경되지 않습니다.

 

참조에 의한 전달(call by reference)

참조에 의한 전달 방법은 인수로 변수의 값을 전달하는 것이 아닌, 해당 변수의 주소값을 전달합니다.

 

즉 함수의 매개변수에 인수로 전달된 변수의 원래 주소값을 저장하는 것입니다.

 

이 방식을 사용하면 인수로 전달된 변수의 값을 함수 내에서 변경할 수 있게 됩니다.

 

 

 

예제

#include <stdio.h>

 

void local(int*);

 

 

 

int main(void)

 

{

 

int var = 10;

 

printf("변수 var의 초깃값은 %d입니다.\n", var);

 

 

 

local(&var);

 

printf("local() 함수 호출 후 변수 var의 값은 %d입니다.\n", var);

 

return 0;

 

}

 

 

 

void local(int* num)

 

{

 

*num += 10;

 

}

 

 

위의 예제에서 local() 함수의 매개변수 num는 인수로 변수 var의 주소값을 전달받습니다.

 

따라서 함수 내에서 매개변수 num의 값을 변경하면, 원래 인수인 변수 var의 값도 같이 변경됩니다.

 

포인터의 포인터

포인터의 포인터란 포인터 변수를 가리키는 포인터를 의미합니다.

 

참조 연산자(*)를 두 번 사용하여 표현하며, 이중 포인터라고도 부릅니다.

 

다음 예제는 포인터의 포인터를 선언하고, 포인터의 포인터를 이용한 접근 방법을 보여주고 있습니다.

 

예제

int num = 10; // 변수 선언

 

int* ptr_num = &num; // 포인터 선언

 

int** pptr_num = &ptr_num; // 포인터의 포인터 선언

 

 

 

printf("변수 num가 저장하고 있는 값은 %d입니다.\n", num);

 

printf("포인터 ptr_num가 가리키는 주소에 저장된 값은 %d입니다.\n", *ptr_num);

 

printf("포인터의 포인터 pptr_num가 가리키는 주소에 저장된 포인터가 가리키는 주소에 저장된 값은 %d입니다.\n",

 

**pptr_num);

 

void 포인터(void pointer)

void 포인터는 일반적인 포인트 변수와는 달리 대상이 되는 데이터의 타입을 명시하지 않은 포인터입니다.

 

따라서 변수, 함수, 포인터 등 어떠한 값도 가리킬 수 있지만, 포인터 연산이나 메모리 참조와 같은 작업은 할 수 없습니다.

 

 

 

void 포인터는 주소값을 저장하는 것 이외에는 아무것도 할 수 없는 포인터입니다.

 

또한, void 포인터를 사용할 때에는 반드시 먼저 사용하고자 하는 타입으로 명시적 타입 변환 작업을 거친 후에 사용해야 합니다.

 

 

 

다음 예제는 void 포인터의 선언 및 void 포인터를 이용한 접근 방법을 보여주고 있습니다.

 

예제

int num = 10; // 변수 선언

 

void* ptr_num = &num; // void 포인터 선언

 

 

 

printf("변수 num가 저장하고 있는 값은 %d입니다.\n", num);

 

printf("void 포인터 ptr_num가 가리키는 주소에 저장된 값은 %d입니다.\n", *(int*)ptr_num);

 

 

 

*(int*)ptr_num = 20; // void 포인터를 통한 메모리 접근

 

printf("void 포인터 ptr_num가 가리키는 주소에 저장된 값은 %d입니다.\n", *(int*)ptr_num);

 

 

위의 예제처럼 void 포인터는 사용할 때마다 명시적 타입 변환을 하고 난 뒤에 사용해야 합니다.

 

함수 포인터(function pointer)

프로그램에서 정의된 함수는 프로그램이 실행될 때 모두 메인 메모리에 올라가게 됩니다.

 

이때 함수의 이름은 메모리에 올라간 함수의 시작 주소를 가리키는 포인터 상수(constant pointer)가 됩니다.

 

이렇게 함수의 시작 주소를 가리키는 포인터 상수를 함수 포인터(function pointer)라고 부릅니다.

 

 

 

함수 포인터의 포인터 타입은 함수의 반환값과 매개변수에 의해 결정됩니다.

 

즉 함수의 원형을 알아야만 해당 함수에 맞는 함수 포인터를 만들 수 있습니다.

 

함수 원형

void func (int, int);

 

 

 

위와 같은 함수 원형을 가지는 함수에 대한 함수 포인터는 다음과 같습니다.

 

함수 포인터

void (*ptr_func) (int, int);

 

 

함수 포인터 사용시 연산자의 우선순위 때문에 반드시 *ptr_func 부분을 괄호(())로 둘러싸야 합니다.

 

 

함수 포인터는 다음 예제처럼 함수를 또 다른 함수의 인수로 전달할 때 유용하게 사용됩니다.

 

예제

double (*calc)(double, double) = NULL; // 함수 포인터 선언

 

double result = 0;

 

double num01 = 3, num02 = 5;

 

char oper = '*';

 

 

 

switch (oper)

 

{

 

case '+':

 

calc = add;

 

break;

 

case '-':

 

calc = sub;

 

break;

 

case '*':

 

calc = mul;

 

break;

 

case '/':

 

calc = div;

 

break;

 

default:

 

puts("사칙연산(+, -, *, /)만을 지원합니다.");

 

}

 

 

 

result = calculator(num01, num02, calc);

 

printf("사칙 연산의 결과는 %lf입니다.\n", result);

 

 

위의 예제는 함수 포인터를 사용하여 변수 oper의 값에 따라 4개의 사칙연산 함수 중 하나를 선택합니다.

 

이렇게 선택된 함수는 함수 포인터를 사용하여 calculator() 함수에 인수로 전달되게 됩니다.

 

널 포인터(null pointer)

0이나 NULL을 대입하여 초기화한 포인터를 널 포인터(null pointer)라고 합니다.

 

널 포인터는 아무것도 가리키지 않는 포인터라는 의미입니다.

 

 

 

'C언어' 카테고리의 다른 글

C언어 메모리  (0) 2020.07.05
C언어 포인터 & 배열  (0) 2020.07.05
C언어 배열  (0) 2020.07.05
C언어 함수  (0) 2020.07.04
C언어 제어문  (0) 2020.07.04

댓글