C 언어에서 포인터는 다음과 같이 정의될 수 있다.
포인터는 '메모리 상에 위치한 특정한 데이터의 주소값을 보관하는 변수'이다.
CPU는 주소값을 통해 램에 어디에 접근할지 명령한다.
프로그램을 실행하게 되면, 컴퓨터의 운영체제가 CPU에게 램에 위치해 있는 프로그램의 시작점을 알려주게 되고, 그 이후 CPU는 해당 위치부터 명령어를 실행한다.
중요한 점은 CPU가 현재 램의 어디에서 명령어를 읽어야 할지 알아야 한다는 점이다. 이는 CPU 안에 지금 읽어들일 명령어의 위치(instruction pointer) 만을 보관하는 특별한 레지스터 덕분에 가능하다.
포인터는 특정한 데이터의 주소값을 저장한다. 이 때 포인터는 주소값을 보관하는 데이터의 형에 *을 붙임으로써 정의하고, & 연산자로 특정한 데이터의 메모리 상의 주소값을 알아낼 수 있다.
단항 & 연산자는 피연산자의 주소값을 불러온다.
&/* 주소값을 계산할 데이터 */
단항 * 연산자는 주소값에서 해당 주소값에 대응되는 데이터를 가져온다.
(포인터에 주소값이 저장되는 데이터의 형) *(포인터의 이름);
/* & 연산자 */
#include <stdio.h>
int main() {
int a;
a = 2;
printf("변수 a의 값: %d \n", a);
printf("변수 a의 주소값: %p \n", &a);
return 0;
}
/* 포인터의 시작 */
#include <stdio.h>
int main() {
int *p;
int a = 3;
p = &a;
printf("포인터 p에 저장된 값: %p \n", p);
printf("int 변수 a가 저장된 주소: %p \n", &a);
printf("%d", *&a);
return 0;
}
포인터는 특정한 데이터의 주소값을 보관한다. 이 때 포인터는 주소값을 보관하는 데이터의 형에 *르 붙임으로써 정의하고, & 연산자로 특정한 데이터의 메모리 상의 주소값을 알아알 수 있다.
/* * 연산자 */
#include <stdio.h>
int main() {
int *p;
int a;
p = &a;
*p = 3;
printf("a의 값: %d \n", a);
printf("*p의 값: %d \n", *p);
return 0;
}
/* 포인터도 변수다 */
#include <stdio.h>
int main() {
int a;
int b;
int *p;
p = &a;
*p = 2;
p = &b;
*p = 4;
printf("a: %d \n", a);
printf("b: %d \n", b);
return 0;
}
/* 상수 포인터 */
#include <stdio.h>
int main() {
int a;
int b;
const int *pa = &a;
// *pa = 3;
pa = &b;
return 0;
}
/* 포인터의 덧셈 */
#include <stdio.h>
int main() {
int a;
int *pa;
pa = &a;
printf("pa의 값: %p \n", pa);
printf("(pa + 1)의 값: %p \n", pa + 1);
return 0;
}
/* 포인터의 뺄셈 */
#include <stdio.h>
int main() {
int a;
int *pa = &a;
printf("pa의 값: %p \n", pa);
printf("(pa - 1)의 값: %p \n", pa - 1);
return 0;
}
/* 포인터의 대입 */
#include <stdio.h>
int main() {
int a;
int *pa = &a;
int *pb;
*pa = 3;
pb = pa;
printf("pa에 저장된 값: %d \n", *pa);
printf("pb에 저장된 값: %d \n", *pb);
}
/* 배열과 포인터 1 */
#include <stdio.h>
int main() {
int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int i;
for (i = 0; i < 10; i++) {
printf("arr[%d]의 주소값: %p \n", i, &arr[i]);
}
return 0;
}
/* 배열과 포인터 2 */
#include <stdio.h>
int main() {
int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int *parr;
int i;
parr = &arr[0];
for (i = 0; i < 10; i++) {
printf("arr[%d]의 주소값: %p ", i, &arr[i]);
printf("(parr + %d)의 값: %p ", i, (parr + i));
if (&arr[i] == (parr + i)) {
/* 만일 (parr + i)가 성공적으로 arr[i]를 가리킨다면 */
printf("일치 \n");
} else {
printf("불일치 \n");
}
}
}
&arr[i] == (parr + i): 배열 arr의 i번째 원소의 주소값은 (parr + i)와 같다.
/* 배열 이름의 비밀 */
#include <stdio.h>
int main() {
int arr[3] = {1, 2, 3};
printf("arr의 정체: %p \n", arr);
printf("arr[0]의 주소값: %p \n", &arr[0]);
return 0;
}
arr == &arr[0]: 배열의 주소값은 배열의 첫번째 원소의 주소값과 같다.
#include <stdio.h>
int main() {
int arr[6] = {1, 2, 3, 4, 5, 6};
int *parr = arr;
printf("sizeof(arr): %d \n", sizeof(arr));
printf("sizeof(parr): %d \n", sizeof(parr));
return 0;
}
배열 자체의 크기가 아니라 포인터의 크기를 반환한다.
/* 1차원 배열의 sizeof */
#include <stdio.h>
int main() {
int arr[3] = {1, 2, 3};
printf("arr의 정체: %p \n", arr);
printf("arr[0]의 주소값: %p \n", &arr[0]);
return 0;
}
C 언어 상에서 배열의 이름이 sizeof 연산자나 주소값 연산자와 사용될 때를 제외하고
배열의 이름을 사용시 암묵적으로 첫번째 원소를 가리키는 포인터로 타입 변환된다.
#include <stdio.h>
int main() {
int arr[5] = {1, 2, 3, 4, 5};
printf("a[3]: %d \n", arr[3]);
printf("*(a + 3): %d \n", *(arr + 3));
return 0;
}
arr[3] = *(arr + 3): [] 연산자를 사용하면 자동으로 변환하여 처리된다.
/* 신기한 [] 사용 */
#include <stdio.h>
int main() {
int arr[5] = {1, 2, 3, 4, 5};
printf("3[arr]: %d \n", 3[arr]);
printf("*(3 + a): %d \n", *(arr + 3));
return 0;
}
[] 연산자를 사용하여 3[arr]을 *(3 + arr)로 변환하였다.
/* 1차원 배열 가리키기 */
#include <stdio.h>
int main() {
int arr[3] = {1, 2, 3};
int *parr;
parr = arr;
// parr = &arr[0];도 동일하다
printf("arr[1]: %d \n", arr[1]);
printf("parr[1]: %d \n", parr[1]);
return 0;
}
두 가지 경우를 제외하고 arr은 arr[0]을 가리키는 포인터로 형 변환한다.
/* 포인터의 포인터 */
#include <stdio.h>
int main() {
// 정수형 변수 a 선언
int a;
// a의 포인터 pa 선언
int *pa;
// 포인터의 포인터 ppa 선언
int **ppa;
// pa는 변수 a의 주소값을 가지는 포인터
pa = &a;
// ppa는 포인터 변수 pa의 주소값을 가지는 포인터
ppa = &pa;
// 정수형 변수 a에 3을 저장
a = 3;
// *pa는 주소값의 * 연산자를 이용해 저장된 값을 반환 3 / **ppa: *ppa는 pa와 같으므로 주소값이고, *을 이용해 3을 가리킨다
printf("a: %d // *pa: %d // **ppa: %d \n", a, *pa, **ppa);
// &a: a의 주소값 / pa: a의 주소값 / *ppa: *ppa는 pa와 같으므로 주소값
printf("&a: %p, pa: %p // *ppa: %p \n", &a, pa, *ppa);
// &pa: &a의 주소값 / ppa: &a의 주소값
printf("&pa: %p // ppa: %p \n", &pa, ppa);
return 0;
}
/* 배열 이름의 주소값 */
#include <stdio.h>
int main() {
// 원소가 3개인 배열 arr 선언
int arr[3] = {1, 2, 3};
// parr은 배열 arr의 주소값
int (*parr)[3] = &arr;
// arr[1]은 배열의 1번 인덱스 원소
printf("arr[1]: %d \n", arr[1]);
// 배열 arr의 1번 인덱스 주소값에 저장된 값을 반환
printf("parr[1]: %d \n", (*parr)[1]);
return 0;
}
arr과 parr은 모두 배열의 첫번째 원소의 주소값을 출력한다.
/* 2차원 배열의 [] 연산자 */
#include <stdio.h>
int main() {
// 2 * 3 형태의 2차원 배열 arr 선언
int arr[2][3];
// arr[0]은 배열의 0번 인덱스 행
printf("arr[0]: %p \n", arr[0]);
// &arr[0][0]은 0행 0열 인덱스의 주소값
printf("&arr[0][0]: %p \n", &arr[0][0]);
// arr[1]은 arr의 1번 인덱스 행
printf("arr[1]: %p \n", arr[1]);
// &arr[1][0]은 1행 0열 인덱스의 주소값
printf("&arr[1][0]: %p \n", &arr[1][0]);
return 0;
}
arr[0]은 배열 0행 0열 인덱스의 주소값과 같다.
arr[1]은 배열 1행 0열 인덱스의 주소값과 같다.
arr[0] 의 값이 arr[0][0] 의 주소값과 같고, arr[1] 의 값이 arr[1][0] 의 주소값과 같습니다. 이것을 통해 알 수 있는 사실은 기존의 1 차원 배열과 마찬가지로 sizeof 나 주소값 연산자와 사용되지 않을 경우, arr[0] 은 arr[0][0] 을 가리키는 포인터로 암묵적으로 타입 변환되고, arr[1] 은 arr[1][0] 을 가리키는 포인터로 타입 변환된다라는 뜻이 됩니다.
/* 2차원 배열의 sizeof */
#include <stdio.h>
int main() {
int arr[2][3] = {{1, 2, 3}, {4, 5, 6}};
printf("전체 크기: %d \n", sizeof(arr));
printf("총 행의 개수: %d \n", sizeof(arr) / sizeof(arr[0]));
printf("총 열의 개수: %d \n", sizeof(arr[0]) / sizeof(arr[0][0]));
}
sizeof 연산자는 포인터로 타입 변환을 시키지 않는다.
전체 배열에 sizeof를 사용하면 배열 전체의 크기를 반환
총 행의 개수는 전체 크기를 열의 크기로 나눈 것
sizeof(arr[0])은 0번째 행의 길이(총 열의 개수)
sizeof(arr[0][0])은 int의 크기인 4를 반환
/* 포인터의 형을 결정짓는 두 가지 요소 */
#include <stdio.h>
int main() {
int arr[2][3] = {{1, 2, 3}, {4, 5, 6}};
// **parr로 하면 오류 발생
int (*parr)[3];
// parr이 배열 arr을 가리키도록 함
parr = arr;
printf("arr[1][1]: %d \n", arr[1][1]);
printf("parr[1][1]: %d \n", parr[1][1]);
printf("arr[1][2]: %d, parr[1][2]: %d \n", arr[1][2], parr[1][2]);
return 0;
}
/* 배열의 포인터 */
#include <stdio.h>
int main() {
int arr[2][3];
int brr[10][3];
int crr[2][5];
int (*parr)[3];
parr = arr;
parr = brr;
// parr = crr;
return 0;
}
#include <stdio.h>
int main() {
int arr[2][3] = {{1, 2, 3}, {4, 5, 6}};
int **parr;
parr = arr;
printf("parr[1][1]: %d \n", parr[1][1]);
return 0;
}
parr은 배열 arr의 주소값이 저장된 포인터이고 parr[1][1]은 ((parr + 1) + 1)과 동일하다.
parr + 1을 계산하면 실제 주소값이 8 증가하므로 arr 배열의 세번째 원소의 주소값을 가진다.
*(parr + 1) + 1은 7이므로 주소값 7에 있는 값을 반환한다.
/* 포인터 배열 */
#include <stdio.h>
int main() {
// 원소가 3개인 배열 arr의 포인터
int *arr[3];
int a = 1, b = 2, c = 3;
// arr의 0, 1, 2번 인덱스는 a, b, c의 주소값
arr[0] = &a;
arr[1] = &b;
arr[2] = &c;
// arr[0]은 a의 주소값이므로 * 연산자를 사용하여 a의 값을 반환
printf("a: %d, *arr[0]: %d \n", a, *arr[0]);
// arr[1]은 b의 주소값이므로 * 연산자를 사용하여 b의 값을 반환
printf("b: %d, *arr[1]: %d \n", b, *arr[1]);
// arr[2]은 c의 주소값이므로 * 연산자를 사용하여 c의 값을 반환
printf("c: %d, *arr[2]: %d \n", a, *arr[2]);
return 0;
}