C언어의 꽃,,,? 이라고 할 수 있는 포인터에 대해 공부해 보도록 하겠습니다.
메모리 (주기억장치)
프로그램 도중에 값을 저장해 두었다가 추후에 읽을 수 있게 하는 장치입니다.
1byte(8bit)마다 주소가 세겨져 있습니다.
포인터
포인터는 메모리(주기억장치) 공간의 주소입니다.
주소는 결국 숫자지만, 이를 int 타입에 담아서는 안됩니다.
포인터 변수
포인터 변수는 메모리의 주소만 담는 변수입니다.
포인터 변수를 줄여서 포인터라고 부릅니다. (사실 포인터는 정확히 말해서는 '주소' 그 자체일 뿐이지만, 대부분 포인터 변수를 포인터라 부르기 때문에, 앞으로 등장하는 모든 포인터 변수를 포인터라 하겠습니다.)
포인터 변수의 타입은 *를 붙여서 표시합니다. ( 예: * ptr)
그러나 주소 값만 가지고는 할 수 있는게 없습니다.
주소부터 시작해서 값이 몇 바이트만큼의 크기를 가지고 저장되어 있는지 등을 알 수 있는 방법이 없기 때문입니다.
따라서 포인터는 이를 위해 추가적인 정보를 필요로 합니다.
그리고 그 추가적인 정보가 바로 포인터가 가리키는 변수의 자료형(타입)입니다.
포인터 변수는 쓰레기 값으로 초기화됩니다.
포인터 변수의 메모리 크기
포인터 변수의 크기는 어떠한 자료를 가리키고 있는가에 상관없이 8바이트입니다.
(32비트 운영체제에서는 4바이트입니다.)
즉 다음 코드의 결과는 모두 8입니다.
printf("%lu\n", sizeof(int *));
printf("%lu\n", sizeof(char *));
printf("%lu\n", sizeof(char ******));
printf("%lu\n", sizeof(double *));
주소 연산자 (&)
포인터 변수는 주소의 값을 저장하는 변수라고 하였습니다.
그렇다면 변수의 주소는 어떻게 알아올 수 있을까요?
포인터 변수에 직접 숫자를 대입하는 것을 불가능합니다. (가능은 하지만 절대 해서는 안되며, 또한 이를 사용하려고 하면 오류를 발생시킵니다.)
#include <stdio.h>
int main(){
int * ptr = 1024;
printf("%d\n", *ptr);
}
위 코드를 컴파일 하면 다음과 같은 경고 메세지가 출력되고, 실행하면 segmentation fault 오류가 발생합니다.
따라서 변수의 주소를 가져오기 위한 다른 방법이 필요하고,
이때 주소 연산자(&)를 사용할 수 있습니다.
& 연산자는 이미 선언된 변수의 주소를 가져옵니다.
사용은 다음과 같이 사용합니다.
#include <stdio.h>
int main(){
int n = 1024;
int * ptr = &n;
//int * ptr2 = &1024; expression must be an lvalue or a function designator
printf("d로 출력 : %d\n", ptr);
printf("p로 출력 : %p\n", ptr);
printf("ptr 이 가리키는 주소에 저장된 값 : %d\n", *ptr);
}
위 코드의 결과는 다음과 같습니다.
그렇다면 int형 포인터에 double형 변수의 주소를 저장하면 어떻게 될까요?
값은 들어가지만, 컴파일러가 경고 메세지를 출력하며, 값도 이상한 값이 나옵니다.
#include <stdio.h>
int main(){
double n = 1024.123;
int * ptr = &n;
//int * ptr2 = &1024; expression must be an lvalue or a function designator
printf("d로 출력 : %d\n", ptr);
printf("p로 출력 : %p\n", ptr);
printf("ptr 이 가리키는 주소에 저장된 값 : %d\n", *ptr);
}
이것은 double은 8바이트이지만, int형은 4바이트이므로, 8바이트 변수에 담겨있는 값을 4바이트만 읽어와 출력하여 이렇게 된 것입니다.
* 연산자 (역참조 연산자)
(포인터 자료형을 선언할 때 사용하는 *가 아닙니다.)
바로 위에서 & 연산자에 대해 알아보았습니다.
& 연산자는 변수를 주면, 주소를 반환하는 연산자입니다.
그렇다면 & 연산자와는 반대로 주소를 주면 변수를 반환하는 연산자는 없을까요?
아쉽게도 존재하지 않습니다.
그러나 주소(l-value)를 주면, 변수가 가진 값(r-value)을 꺼내주는 연산자는 있습니다.
그러한 연산자가 바로 * 연산자입니다.
사실 해당 연산자는 위의 예시들에서 계속해서 사용했었습니다.
#include <stdio.h>
int main(){
int n = 1024;
int * ptr = &n;
printf("%p\n", &n);
printf("%d\n", *(&n));
int pn = *ptr;
printf("%d\n", pn);
printf("%p\n", &pn);
printf("%d\n", *(&pn));
}
위 코드의 결과는 다음과 같습니다.
참고로 연산자 *&를 사용하면 자기 자신을 출력합니다, 즉 상쇄됩니다.
그러나 &*를 사용하는 것은 불가능합니다.
그 이유는 *연산자를 하면 상수값이 반환되는데, 해당 값에 & 연산자를 취한 것이므로, 상수값의 주소를 취하려는 시도를 한 것이기 때문입니다.
*와 포인터 변수를 사용하여 일반 변수처럼 사용하기
* 연산자와 포인터 변수를 함께 써서 일반 변수처럼 사용할 수 있습니다
#include <stdio.h>
int main(){
int num1 = 100, num2 = 200;
int * pnum ;
pnum = & num1;
(*pnum) += 30;
pnum = & num2;
(*pnum) -= 30;
printf("num1:%d, num2:%d\n", num1, num2);
}
위 코드의 실행 결과는 다음과 같습니다.
배열과 포인터의 관계
C언어에서 배열의 이름은 포인터입니다.
배열의 이름은 배열의 첫 번째 요소의 주소를 나타냅니다. 즉 배열 이름이 arr이라면, arr과 &arr[0]은 같습니다.
주의해야 할 점은 배열의 이름에 대입 연산을 하는 것은 불가능하다는 것입니다.
#include <stdio.h>
int main(){
int arr[3] = {1,2,3};
printf("배열 이름: %p \n", arr);//(1)
printf("첫 번째 요소 주소(%%p): %p \n", &arr[0]);//(1)과 같다.
printf("두 번째 요소 주소(%%p): %p \n", &arr[1]);
printf("세 번째 요소 주소(%%p): %p \n", &arr[2]);
// arr = &arr[1]; 오류
return 0;
}
결과는 다음과 같습니다.
배열 이름을 포인터처럼 사용하기
배열 이름에 일반 포인터처럼 * 연산을 해서 값을 가져올 수 있으며,
arr이 배열이면 *arr은 arr[0]과 동일하게 사용됩니다.
#include <stdio.h>
int main(){
int arr[3] = {1,2,3};
printf("%d \n", *arr);
printf("%d \n", arr[1]);//(1)
printf("%d \n", *(&arr[1]));//(1)과 같다
*arr += 100;
printf("%d \n", arr[0]);//(2)
printf("%d \n", *arr);//(2)와 같다
return 0;
}
결과는 아래와 같습니다.
포인터 변수를 배열처럼 사용하기
배열 이름을 포인터 변수처럼 사용할 수 있었던 것 처럼,
포인터 변수도 배열처럼 사용할 수 있습니다.
#include <stdio.h>
int main(){
int arr[3] = {1,2,3};
int * ptr = &arr[0];//int * ptr = arr; 과 동일
int* ptr2 = &arr[0];
int *ptr3 = &arr[0];
printf("%d %d\n", ptr[0], arr[0]);
printf("%d %d\n", ptr[1], arr[1]);
printf("%d %d\n", ptr[2], arr[2]);
printf("%d %d\n", *ptr, *arr);
return 0;
}
대신 반드시 포인터를 배열처럼 사용할 때는, 배열의 시작주소를 할당받은 포인터에서만 사용하여야 합니다.
꼭 배열의 시작주소를 할당받지 않고, 단일 변수의 주소를 할당받은 포인터 변수도 배열처럼 사용할 수 있지만, 예상하지 못한 값들이 출력되기 때문에 반드시 피해주셔야 합니다.
포인터의 증감 연산
주소 1024 담고 있는 포인터 변수에 1을 더하면 어떻게 될까요?
1025를 가리킬 거 같지만, 확인해보면 그렇지 않다는 것을 알 수 있습니다.
1을 더하게 되면 (sizeof(자료형) * 1)만큼의 값이 더해지게 됩니다.
(더하기 빼기는 가능하지만, 곱셈은 불가능합니다.)
이를 통해 다음과 같은 코드를 작성할 수 있습니다.
#include <stdio.h>
int main(){
int arr[3] = {11,22,33};
int * ptr = arr;
printf("%d %d %d \n", *ptr, *(ptr+1), *(ptr+2));
printf("%d %d %d \n", *ptr, *ptr+1, *ptr+2);
printf("%d ", *ptr);
ptr++;
printf("%d ", *ptr);
ptr++;
printf("%d ", *ptr);
ptr--;
printf("%d ", *ptr);
ptr--;
printf("%d \n", *ptr);
return 0;
}
결과는 다음과 같습니다.
*(arr + i) 는 arr[i]와 똑같다.
배열 이름도 포인터이기 때문에 포인터 변수를 이용한 배열의 접근방식을 배열의 이름에도 사용할 수 있습니다.
그리고 배열의 이름을 이용한 접근방식도 포인터 변수를 대상으로 사용할 수 있습니다.
즉 arr이 포인터 변수의 이름이건 배열의 이름이건 변수의 값에 접근할 때에는 arr[i]와 *(arr+i)는 같습니다.
#include <stdio.h>
int main(){
int arr[3] = {11,22,33};
int * ptr = arr;
printf("%d %d %d \n", *ptr, *(ptr+1), *(ptr+2));
printf("%d %d %d \n", *(ptr+0), *(ptr+1), *(ptr+2));
printf("%d %d %d \n\n\n", ptr[0], ptr[1],ptr[2]);
printf("%d %d %d \n", *arr, *(arr+1), *(arr+2));
printf("%d %d %d \n", *(arr+0), *(arr+1), *(arr+2));
printf("%d %d %d \n", arr[0], arr[1],arr[2]);
return 0;
}
위 코드의 결과는 아래와 같습니다.
#include <stdio.h>
int main(){
int arr[3] = {11,22,33};
int * ptr = arr;
printf("기존 ptr \n");
printf("%d %d %d \n", *ptr, *(ptr+1), *(ptr+2));
printf("%d %d %d \n", *(ptr+0), *(ptr+1), *(ptr+2));
printf("%d %d %d \n", ptr[0], ptr[1],ptr[2]);
printf("기존 ptr 끝 \n\n");
printf("기존 arr \n");
printf("%d %d %d \n", *arr, *(arr+1), *(arr+2));
printf("%d %d %d \n", *(arr+0), *(arr+1), *(arr+2));
printf("%d %d %d \n", arr[0], arr[1],arr[2]);
printf("기존 arr 끝 \n\n");
int temp[3] = {100, 200, 300};
//arr = {100, 200, 300}; Error
//arr = temp; ERROR
arr[0] = 100;//OK
*(arr + 1) = 200;//OK
//ptr = {100, 200, 300};// Error
ptr = temp; //OK!!!!!
ptr[0] = 1000;//OK
*(ptr + 1) = 2000;//OK
printf("바뀐 ptr \n");
printf("%d %d %d \n", *ptr, *(ptr+1), *(ptr+2));
printf("%d %d %d \n", *(ptr+0), *(ptr+1), *(ptr+2));
printf("%d %d %d \n", ptr[0], ptr[1],ptr[2]);
printf("바뀐 ptr 끝 \n\n");
printf("바뀐 arr \n");
printf("%d %d %d \n", *arr, *(arr+1), *(arr+2));
printf("%d %d %d \n", *(arr+0), *(arr+1), *(arr+2));
printf("%d %d %d \n", arr[0], arr[1],arr[2]);
printf("바뀐 arr 끝 \n\n");
return 0;
}
결과는 다음과 같으며, 주석에 주의하셔야 합니다.
문자열과 포인터
문자열은 char의 배열을 사용하여 표현한다고 배웠습니다.
그런데 배열 또한 포인터 변수로 사용될 수 있었습니다.
그렇다면 문자열의 경우는 어떨까요??
문자열의 두 가지 정의 방법
지금까지는 char 배열을 통해서만 문자열을 정의하였는데 사실 한가지 방법이 더 존재합니다.
바로 포인터를 사용하는 것인데 한번 확인해 보도록 하겠습니다.
#include <stdio.h>
int main(){
char * s1 = "hi there";
s1 = "hi there!!!!!!";//OK
//s1[0] = "b"; (X) 컴파일은 되지만 런타임 에러
printf("%s\n", s1);
char s2[] = "hi there";
//s2 = "hi there!!!!!!"; (X)
s2[0] = 'b';
printf("%s\n", s2);
return 0;
}
결과는 다음과 같습니다
이때 주의할 점은
배열로 정의된 문자열은 재할당이 불가능하지만,
포인터로 정의된 문자열을 재할당이 가능하다는 것입니다.
또한
배열로 정의된 문자열의 각 자리를 바꾸는 것은 가능하지만,
포인터로 정의된 문자열의 각 자리를 바꾸는 것은 불가능합니다.
자동 할당 문자열
배열로 문자열을 정의할 때 초기화 값으로 쓰이는 경우가 아닌
그 외의 모든 큰 따음표로 둘러싸인 문자열은 자동 할당 문자열이 됩니다.
자동 할당 문자열은 문자열이 먼저 메모리에 할당된 이후, 반환되는 메모리의 주소 값을 저장하는 방식입니다.
char * str = "HI";
-> 문자열 저장 후 주소 값 반환
char * str = 0x1234;
printf("HI")
-> 문자열 저장 후 주소 값 반환
printf(0x1234);
포인터의 배열
배열의 원소가 포인터인 배열에 대해 알아보겠습니다.
#include <stdio.h>
int main(){
int n1 = 10, n2 = 20, n3 = 30;
int arr[10] = {n1, n2, n3}; //요소의 타입이 int
int * arr1[10] = {&n1, &n2, &n3}; //요소의 타입이 * (즉, 주소)
printf("%d ", *arr1[0]);
printf("%d ", *arr1[1]);
printf("%d ", *arr1[2]);
return 0;
}
결과는 다음과 같습니다.
그러면 문자열을 배열에 저장하려면 어떻게 해야 할까요?
다음과 같이 할 수 있습니다.
#include <stdio.h>
int main(){
char * strArr[3] = {"Hi", "My", "Name"};
printf("%s ", strArr[0]);
printf("%s ", *(strArr + 1));
printf("%s ", strArr[2]);
return 0;
}
또한 다음과 같이 포인터 배열이 아닌 문자 배열의 배열을 사용할 수도 있습니다.
#include <stdio.h>
int main(){
char strArr[3][30] = {"Hi", "My", "Name"};
printf("%s ", strArr[0]);
printf("%s ", *(strArr+1));
printf("%s ", strArr[2]);
return 0;
}
포인터와 함수
함수가 호출될 때 인자로 넘기는 변수에 대해서, 실제 변수가 전달되는 것이 아니라 값만 복사됩니다.
그렇다면 배열 혹은 포인터의 경우에는 어떻게 될까요?
바로 주소값이 복사되어 전달됩니다.
따라서 다음과 같이 사용할 수 있습니다.
#include <stdio.h>
void printArr(int * ptr){
printf("%d %d", ptr[0], ptr[1] );
printf("쓰레기 값 출력 %d", ptr[1000] );
}
int main(){
int arr[3] = {1,2,3};
int z;
printArr(arr);
return
}
그런데 위의 예시를 보면 문제점 하나를 찾을 수 있습니다.
바로 배열의 길이을 알 수 없다는 것인데요, sizeof를 통해 크기를 구하면 배열의 길이가 아닌 포인터 변수의 바이트 크기인 8(32bit 운영체제인 경우 4)이 나옵니다.
따라서 배열을 인자로 넘길 때에는 배열 길이도 함께 넘겨주어야 합니다.
(단, 문자열의 끝은 '\0'을 통해 나타내기 때문에, 끝을 알 수 있는 방법이 이미 존재하여 굳이 길이를 함께 전달할 필요 없습니다.)
#include <stdio.h>
//void printArr(int * ptr, int len){
void printArr(int ptr[], int len){
int i=0;
for (i = 0; i < len; i++)
{
printf("%d ", ptr[i]);
}
printf("\n");
for (i = 0; i < len; i++)
{
printf("%d ", *(ptr+ i));
}
}
int main(){
int arr[3] = {1,2,3};
printArr(arr, sizeof(arr)/ sizeof(arr[0]));
//다음도 가능 : printArr(arr, sizeof(arr)/ sizeof(*arr));
return 0;
}
결과는 다음과 같습니다.
참고
https://storyofsol.tistory.com/157
https://ideadummy.tistory.com/58
'c언어' 카테고리의 다른 글
[C언어] - Const (0) | 2022.04.20 |
---|---|
[C언어] - Call By Value (0) | 2022.04.20 |
[C언어] - 문자열 (0) | 2022.04.19 |
[C언어] - 배열 (0) | 2022.04.19 |
[C언어] - 재귀함수 (0) | 2022.04.19 |