[C언어] 12. 포인터(pointer) 기초

이번에는 C언어의 포인터(pointer)기초에 대해 알아보자.





포인터 변수

메모리 주소와 연산자&

1) 주소란

  • 메모리 공간 : 1byte마다 고유한 주소(address)가 있음
  • 메모리 주소
    • 0부터 1byte마다 1씩 증가
    • 저장장소인 변수이름과 함께 기억(저장)장소를 참조하는 또 다른 방법
    • 주소 정보를 이용해 주소가 가리키는 변수의 값 참조 가능
    • 주소 정보의 이웃한(이전 또는 이후) 저장공간의 값 참조 가능


2) 주소연산자 &

  • &(ampersand)
    • 피연산자인 변수의 메모리 주소를 반환하는 주소연산자
    • 사용 : &variable_name, 변수에만 사용 가능(상수나 표현식에는 사용할 수 없음)
    • 형식제어문자 %p : 변수의 주소값 출력 시 사용


3) 윈도우 10 64비트 시스템에서 변수의 주소값

  • 변수의 주소값 : 8바이트(64비트), 16진수의 16개 자릿수로 출력
int input = 100;

printf("입력 값 : %d\n", input);				//100
printf("주소 값 : %p(16진수)\n", &input);			//16진수 값
printf("주소 값 : %llu(10진수)\n", (uintptr_t)&input);	//10진수 값
printf("주소 값 크기 : %zu\n", sizeof(&input));		//8(바이트), %zd 가능



포인터 변수

1) 포인터 변수란

  • 변수의 주소값을 저장하는 변수(반드시 주소값은 포인터 변수에 저장!)
  • 일반 변수와 구별되며 선언 방법이 다름
  • 포인터변수 = 포인터
  • 64비트 윈도우 10에서 포인터 변수는 모두 8바이트


2) 포인터 변수 선언
👉 연산자 *(asterisk)를 자료형과 변수이름 사이에 삽입해 선언

//자료형과 변수명 사이에 포인터(*)가 위치하면 됨
//읽기 : 자료형 포인터 변수명
//초기화하지 않으면 쓰레기(garbage) 값 저장
자료형 *변수이름;		
자료형*변수이름;
자료형 * 변수이름;
자료형* 변수이름;

//예시 - 모두 가능
int *ptrint;			
short*ptrshort;
char * ptrchar;
double* ptrdouble;


3) 주소값 대입

  • 초기화하지 않으면 쓰레기 값이 저장
  • 주소값 대입하기
//1. 선언 후 초기화
int data = 100;
int *str_data;
str_data = &data;	//포인터변수 str_data는 변수 data를 가리킨다(참조 reference)한다.

//2. 선언과 초기화 동시에
int data = 100;
int *str_data = &data;


4) 출력

  • 형식제어문자 %p 사용
int data = 100;
int *str_data = &data;

printf("data 값 : %d\n", data);
printf("data 주소값(%p) = 포인터 str_date 값 (%p)\n", &data, str_data);
printf("포인터 str_date 주소값 : %p\n", &str_data);



간접연산자 *와 포인터 연산

다양한 포인터 변수 선언

1) 여러 포인터 변수 선언

  • 여러개의 포인터 변수를 한 번에 선언하기
//콤마 이후에 변수마다 *기술
int *ptr1, *ptr2, *ptr2;
  • 특별한 초기값이 없는 경우 쓰레기값 저장 => 0번 주소값인 NULL로 초기값 지정
int *ptr = NULL;


2) NULL

  • 헤더파일 stdio.h에 정의되어 있는 포인터 상수
  • 0번지 주소값을 의미
#define NULL ((void *)0)
//(void*) : 아직 결정되지 않은 자료형의 주소
//=> NULL이 저장된 포인터 변수는 아무 변수도 가리키고 있지 않음을 의미
//=> 아직 유보된 포인터이므로 모든 유형의 포인터 값을 저장할 수 있는 포인터형


3) 간접연산자 *

  • 역참조 : 포인터 변수가 갖는 주소로 그 주소의 원래 변수를 참조
  • 간접연산자(indirection operator) * 사용
  • *포인터 : 해당 포인터가 가진 주소의 원래 변수, r-value와 l-value
  • &변수 : 주소값, r-value
int data = 100;
int *ptr = &data;

printf("역참조 : %d", *ptr);	//*ptr = data



포인터 변수의 연산

1) 주소연산

  • 포인터 변수는 간단한 더하기와 뺄셈 연산으로 이웃한 변수의 주소 연산 수행
  • 포인터가 가리키는 변수 자료형의 상대적인 위치에 대한 연산
  • 포인터에 저장된 주소값의 연산으로 이웃한 이전, 이후의 다른 변수 참조
p+1;
- p 가리키는 자료형의 다음 주소
- 실제 주소값 : p + [자료형크기(바이트)]

p+i;
- p 가리키는 자료형의 다음 i번째 주소
- 실제 주소값 : p + [자료형크기(바이트)] * i





다중포인터와 배열포인터

변수의 내부 저장표현과 포인터 변수의 형변환

1) 변수의 내부 저장 표현

  • 리틀 엔디안(little endian) : 시작 주소값에서 저장 값의 하위 바이트부터 저장하는 방식
  • 빅 엔디안(big endian) : 시작 주소값에서 저장 값의 상위 바이트부터 저장하는 방식


2) 명시적 형변환

  • 포인터변수는 동일한 자료형끼리만 대입이 가능
  • 자동 형변환이 불가능하므로, 명시적 형변환을 수행해 대입 가능
  • 동일한 메모리의 내용이 포인터의 자료형에 따라 참조 단위가 달라짐
int value = 0x44434241;
int *pi = &value;

//불가능
//=>pc가 가리키는 주소에서부터 1바이트 크기의 char형 자료를 참조하겠다
//char *pc = &value;	

//가능
char *pc = (char*)&value;


다중 포인터와 증감연산자의 활용

1) 다중포인터

  • 다중포인터: 포인터의 포인터
  • 이중포인터: 포인터 변수의 주소값을 갖는 변수
int i = 20;
int *pi = &i;		//pi => 포인터, *pi = i
int **dpi = π	//dpi => 이중포인터, **dpi = i


2) 간접연산자와 증감연산자 활용

  • *p++ = *(p++)(*p)++
  • ++*p = ++(*p)
  • *++p = *(++p)
int a[] = {10, 20};
int *p = &a[0];
연산식결과값*p연산 후 주소값 p이동연산 후 *p
*p++, *(p++)10*p : p의 간접참조 값p+1 : p 다음 주소20
(*p)++10*p : p의 간접참조 값p : 변화없음11
*++p, *(++p)20*(p+1) : (p+1) 간접참조 값p+1 : p 다음 주소20
++*p, ++(*p)11*p+1 : *p에 1 더하기p : 변화없음11


포인터 상수

1) 대입연산자의 l-value 사용 제한

int i = 10, j = 20;

//방법 1 - p가 가리키는 변수인 i를 상수로 선언
const int *p = &i;	//오류 : *p = 20;
int const *p = &i;	//오류 : *p = 20;

//방법 2 - p에 저장되는 초기 주소값을 상수로 선언
int* const p = &i;	//오류 : p = &j;





배열과 포인터

1차원 배열과 포인터

1) 배열 이름을 이용한 참조

int array[] = {10, 20, 30};
  • 배열의 주소값(배열 첫 번째 원소의 주소값) : array, &array[0]
  • 배열의 첫 번째 원소 저장 값 : *array, array[0]
  • 배열 (i + 1)번 째 원소 주소값 : (array + i), &array[i]
  • 배열 (i + 1)번 째 원소 저장 값: *(array + i), array[i]


2) 포인터 변수를 이용한 배열의 원소 참조

/* 포인터 변수를 이용해 주소값을 이동하면서 배열 원소를 순차적으로 참조하는 방법 */
int a[3] = {5, 10, 15};
int *p = a;	//a = &a[0]

//1. 포인터변수p를 사용해 배열 원소 값 참조
printf("%d %d %d\n", *(p), *(p+1), *(p+2));

//2. 포인터변수p를 배열처럼 사용
printf("%d %d %d\n", p[0], p[1], p[2]);



2차원 배열과 간접연산자, 배열 포인터를 활용한 다양한 접근

1) 배열이름과 간접연산자로 참조

  • 2차원 배열이름
    • 배열을 대표
    • array[0]을 카리키는 포인터 상수
  • 2차원 배열이름[0]
    • 배열의 첫 행을 대표 * 첫 번째 원소인 array[0][0]의 주소값 &array[0][0]
int array[][3] = {
			{1, 2, 3},
			{4, 5, 6}
		 };
배열배열원소참조배열원소참조
array[0][0]*(*(array + 0) + 0)*(*array+0)
array[0][1]*(*(array + 0) + 1)*(*array+1)
array[0][2]*(*(array + 0) + 2)*(*array+2)
array[1][0]*(*(array + 1) + 0)*(*array+3)
array[1][1]*(*(array + 1) + 1)*(*array+4)
array[1][2]*(*(array + 1) + 2)*(*array+5)



2차원 배열 포인터

1) 2차원 배열 포인터 선언

//1. 선언 후 초기화
원소자료형 (*변수이름)[배열크기];
변수이름 = 배열이름;

//2. 선언과 초기화 동시에
원소자료형 (*변수이름)[배열크기] = 배열이름;

int ary[][4] = {5, 7, 6, 2, 7, 8, 1, 3};
int (*p)[4] = ara;	//열이 4인 배열을 가리키는 포인터

int a[][4] = {
		{1, 2, 3, 4,},
		{10, 11, 12, 13},
		{20, 21, 22, 23}
	     };
int(*p)[4] = a;		//열이 4인 배열을 가리키는 포인터



포인터 배열

1) 포인터 배열(array of pointer)이란?

  • 포인터 배열 : 주소값을 저장하는 포인터를 배열원소로 하는 배열
  • 선언 : 자료형 *변수이름[배열크기];
int a = 5, b = 7, c = 9;
int *pa[3] = {&a, &b, &c};	//크기가 3인 포인터 배열