[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인 포인터 배열