컴퓨터공학과, 프로그래머에 있어 면접을 볼 때면 거의 필수로 등장하는 아이가 포인터라는 아이인데요.
많은 분들이 이 포인터를 배우는 단계에서부터 큰 좌절감을 느낍니다. 저도 그랬구요. 아직까지도 제가 포인터를 완벽히 이해한 것인가? 싶을 정도로 프로그래밍을 하다보면 종종 오류를 맞이하곤 합니다. 그래서 포인터에 어려움을 겪고 있는 분들을 위해 이 포스팅을 빌어 말끔히 그 고민을 해소해 보고자 합니다.
본격적으로 포인터에 대해 이야기하기 앞서 우리는 컴퓨터의 데이터 처리 원리에 대해 먼저 알아야할 필요가 있습니다.
기본적으로 C/C++ 언어에서 변수를 선언하면 해당 변수는 컴퓨터 메모리에 저장됩니다.
#include <stdio.h>
int main(void) {
int num1 = 10; // 정수형 변수 num1 선언 및 초기화
float float1 = 28.8; // 실수형 변수 float1 선언 및 초기화
char char1 = 'K'; // 문자형 변수 char1 선언 및 초기화
printf("=== 변수들의 크기 ===\n");
printf("num1의 크기:%d\n", sizeof(num1)); // num1의 크기 출력
printf("float1의 크기:%d\n", sizeof(float1)); // float1의 크기 출력
printf("char1의 크기:%d\n\n", sizeof(char1)); // char1의 크기 출력
printf("=== 변수들의 주소 ===\n");
printf("num1의 주소:%u\n", &num1); // num1의 주소 출력
printf("float1의 주소:%u\n", &float1); // float1의 주소 출력
printf("char1의 주소:%u\n", &char1); // num2의 주소 출력
return 0;
}
여기서 메모리의 단위는 바이트이며, 각각의 자료형의 크기만큼 할당이 됩니다. 저장된 메모리의 주소는 주소 연산자 &(Ampersand, 앰퍼샌드)를 이용하여 위와 같이 출력할 수 있습니다. 변수의 메모리 주소 할당은 프로그램 실행할 때마다 실행되므로 해당 코드를 실행할 때마다 주소가 바뀌는 것을 보실 수 있습니다. 그리고 출력되는 메모리 주소는 해당 변수가 차지하는 메모리 공간의 첫 메모리 주소입니다. 여기서 말하는 메모리가 무엇이냐? 쉽게 말해 우리가 컴퓨터를 살 때 중요하게 생각하는 부분 중 하나인 RAM 메모리을 생각하시면 됩니다(이것은 쉽게 말했을 때이고, 더 자세히 말하면 분류가 더 있습니다)
| 포인터(Pointer)란?
포인터는 앞서 설명한 변수의 메모리 주소를 가리키는 아이이다. 포인터의 값도 고정되어 있는 것이 아닌 변할 수 있으므로 변수의 일종이다. 따라서, 포인터 변수라 부르기도 한다.
변수이기 때문에 사용을 위해서는 선언을 먼저 해주어야 한다.
여기서 우리는 곱셈에서 사용했던 *(Asterisk, 애스터리스크)를 사용해서 포인터 변수를 선언해줄 수 있습니다.
변수의 메모리 주소를 간접적으로 가리키도록 도와주기 때문에 여기서 *는 간접 참조 연산자라고 부릅니다.
| 포인터 변수 선언과 주소 저장
#include <stdio.h>
int main(void) {
int num1 = 10; // 정수형 변수 num1 선언 및 초기화
int* p = NULL; // 정수 포인터 변수 선언 및 NULL로 초기화
p = &num1; // 포인터 변수 p에 num1의 주소를 저장
printf("=== 변수들의 크기 ===\n");
printf("num1의 크기:%d\n", sizeof(num1)); // num1의 크기 출력
printf("p의 크기:%d\n", sizeof(p)); // p의 크기 출력
printf("*p의 크기:%d\n\n", sizeof(*p)); // *p의 크기 출력
printf("=== 변수들의 주소 ===\n");
printf("num1의 주소:%u\n", &num1); // num1의 주소 출력
printf("p의 주소:%u\n", &p); // p의 주소 출력
printf("*p의 주소:%u\n", &*p); // *p의 주소 출력
printf("=== 변수의 값 출력 ===\n");
printf("num1의 값:%d\n", num1); // num1의 값 출력
printf("p의 값:%d\n", p); // p의 값 출력
printf("*p의 값:%d\n\n", *p); // *p의 값 출력
return 0;
}
크기는 모두 int 정수형이므로 4바이트를 나타내는 것을 볼 수 있다. 그리고 p = &num1로 포인터 변수가 num1의 주소값을 가리키도록 초기화했으므로 p의 값을 출력했을 때에는 num1의 첫 메모리 주소를 출력하고, *p의 값을 출력했을 때는 가리키는 메모리 주소의 값을 출력하는 것을 확인할 수 있다.
결론, *p는 num1의 값을 가리키고 포인터 변수 p는 num1의 첫 메모리 주소를 가리킨다.
*p = num1 = 10, p = &num1
| 포인터 그런데 왜 쓰는거죠?_포인터의 용도
굳이 포인터를 사용할 이유가 있을까? 라는 의문이 드실 겁니다. 개념도 어렵고, 이걸 도대체 왜 쓰는 걸까? 궁금증을 해결해드리기 위해 포인터의 용도에 대해 말씀드리겠습니다.
(1) 참조에 의한 호출(reference-by-value)을 가능하게 해준다.
기본적으로 우리가 배운 함수는 값에 의한 호출(call-by-value)입니다. 이게 무엇이냐? 즉, 우리가 선언한 함수로 값을 전달하는데 이게 진짜 값이 아니라 복사본을 넘겨준다는 말입니다. 그래서 함수에서 아무리 별 쌩쇼를 다해서 값을 바꾼다고 한들 그것은 복사본이기 때문에 실제 원본값은 변경되지 않지요.
그런데, 경우에 따라 우리는 함수에서 바로 원본값을 변경하고 싶을 때가 있단 말이지요. 예를 들어 여러 개의 값을 반환하고 싶을 때(함수는 return으로 반환할 수 있는 값이 하나 뿐이에요!!) 등...이 때 사용하는 것이 지금 배운 포인터입니다. 포인터를 이용해 매개변수를 포인터 변수로 정의해서 전달인자를 넘겨주면 우리는 함수에서도 원본값을 수정할 수 있게 됩니다.
(2) 메모리의 크기를 줄여준다.
기본적으로 포인터 변수는 메모리의 첫번째 주소를 가리키기 때문에 어떠한 자료형이든 32bit 환경에서는 4바이트, 64bit 환경에서는 8바이트 밖에 차지하지 않습니다. 따라서 연산할 때 메모리 크기를 줄여준다는 장점이 있습니다.
| 배열의 포인터
기본적으로 배열의 주소를 출력하면 배열의 첫번째 인덱스의 메모리 주소를 가리킨다는 걸 확인할 수 있습니다. 따라서, 배열의 변수는 포인터 상수라고 할 수 있으며, 아래와 같이 int arr[]을 int * arr로 변경해도 오류가 안난다는 것을 확인할 수 있습니다.
#include <stdio.h>
#define SIZE 5 // 배열의 크기 정의
void ArraySum(int arr[]); // 반환값이 없는 배열 합계 함수 선언
int main(void) {
int arr[] = { 1, 2, 3, 4, 5 }; // 배열 선언 및 초기화
ArraySum(arr);
return 0;
}
void ArraySum(int arr[]) // 반환값이 없는 배열 합계 함수 정의
{
int total = 0;
for (int i = 0; i < SIZE; i++) { // 반복문으로 배열의 합계 구하기
total += arr[i];
}
printf("배열의 합계: %d\n", total); // 배열의 합계 출력
}
#include <stdio.h>
#define SIZE 5 // 배열의 크기 정의
void ArraySum(int *arr); // 반환값이 없는 배열 합계 함수 선언
int main(void) {
int arr[] = { 1, 2, 3, 4, 5 }; // 배열 선언 및 초기화
ArraySum(arr);
return 0;
}
void ArraySum(int *arr) // 반환값이 없는 배열 합계 함수 정의
{
int total = 0;
for (int i = 0; i < SIZE; i++) { // 반복문으로 배열의 합계 구하기
// total += arr[i];
total += (*arr++); // 위 아래 모두 사용가능
}
printf("배열의 합계: %d\n", total); // 배열의 합계 출력
}
이를 통해 배열 매개변수는 일반 변수들처럼 값을 넘기는 것이 아니라 포인터처럼 주소를 전달하는 것을 알 수 있다.
그리고 배열 매개변수를 포인터 매개변수로 변경하게 되면 arr[i] 대신 (*arr++)처럼 사용도 가능해진다. 이는 배열이나 포인터나 가리키는 값의 첫번째 메모리 주소를 가리킨다는 특징 때문에 가능하다.
| 포인터 매개변수
매개변수로 포인터를 사용하게 되면 일반 변수의 경우 주소 연산자 &(Ampersand)를 사용하여 주소값을, 배열은 위에 설명한 바와 같이 본래 포인터처럼 주소값을 전달하므로 상수를 그대로 전달해도 무관하다.
#include <stdio.h>
#define SIZE 5 // 배열의 크기 정의
void ArraySum(int *arr, int *arr_size); // 반환값이 없는 배열 합계 함수 선언
int main(void) {
int arr[] = { 1, 2, 3, 4, 5 }; // 배열 선언 및 초기화
int arr_size = sizeof(arr) / sizeof(int); // 배열의 크기를 구해 초기화
ArraySum(arr, &arr_size);
return 0;
}
void ArraySum(int *arr, int *arr_size) // 반환값이 없는 배열 합계 함수 정의
{
int total = 0;
for (int i = 0; i < SIZE; i++) { // 반복문으로 배열의 합계 구하기
// total += arr[i];
total += (*arr++); // 위 아래 둘다 사용가능
}
printf("배열의 합계: %d\n", total); // 배열의 합계 출력
printf("배열의 크기: %d\n", *arr_size); // 배열의 크기 출력
}
| 2차원 배열 포인터 매개변수
포인터 매개변수와는 조금 다르게 2차원 배열을 포인터를 매개변수로 넘기는 방법은 두가지가 있습니다.
둘의 데이터를 출력하는 방법이 조금 다르니 확인해두시길 바랍니다.
첫번째, 배열과 비슷한 형태로 넘기는 경우
#include <stdio.h>
#define SIZE 4
void MatrixPrint(int(*arr)[4]);
int main(void) {
int arr[SIZE][SIZE] = { {11, 12, 13, 14},
{15, 16, 17, 18},
{19, 20, 21, 22},
{23, 24, 25, 26} };
MatrixPrint(arr);
return 0;
}
void MatrixPrint(int(*arr)[4])
{
for (int i = 0; i < SIZE; i++) {
for (int j = 0; j < SIZE; j++) {
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
두번째, 2차원 배열 전체를 포인터로 넘기는 경우
#include <stdio.h>
#define SIZE 4
void MatrixPrint(int *arr);
int main(void) {
int arr[SIZE][SIZE] = { {11, 12, 13, 14},
{15, 16, 17, 18},
{19, 20, 21, 22},
{23, 24, 25, 26} };
MatrixPrint(arr);
return 0;
}
void MatrixPrint(int *arr)
{
for (int i = 0; i < SIZE; i++) {
for (int j = 0; j < SIZE; j++) {
printf("%d ", *(arr + i * SIZE + j));
}
printf("\n");
}
}
이상으로 포인터에 대해 알아보았구요. 추가로 알게된 개념이 있다면 추후 업데이트를 통해 계속 정리하도록 하겠습니다. 감사합니다. ^^
'코딩 | 개념 정리 > Common Concept' 카테고리의 다른 글
프로그래밍 공통 개념 뽀개기 5탄_주석 (0) | 2022.05.08 |
---|---|
프로그래밍 공통 개념 뽀개기 4탄_자료형(Data Type) (0) | 2021.09.21 |
프로그래밍 공통 개념 뽀개기 3탄_식별자(Identifier) (0) | 2021.09.20 |
프로그래밍 공통 개념 뽀개기 1탄_함수의 매개변수와 전달인자란? (0) | 2021.09.18 |