C › C언어의 포인터(Week4_Day4)
오늘은 어제에 이어서 c언어 공부를 했다.
포인터
사전지식
먼저 짚고 갈 점은 모든 데이터들은 메모리 상에 특정 공간에 저장 되어 있다는 것이다.
메모리의 각각의 공간은 1바이트를 가지고, 예를 들어 int형 변수를 정의한다면 4칸을 차지하게 된다.
어떤 공간의 정보를 사용해야 할 지 구분하기 위해서 각 공간에 고유 주소(address)를 붙이고,
1
int a = 123;
16진수로 표현된 메모리 주소(0x152839)로 부터 4바이트의 공간을 차지 하면서 123 이라는 값이 저장되게 되는 것이다.
만약
1
a = 10;
라고 a에 10을 대입하게 되면, 메모리주소(0x152839)로 부터 4바이트의 공간에 있는 데이터를 10으로 바꾸게 되는 것이다.
포인터란
말그대로 가리키다 를 의미한다.
포인터 또한 앞서 나왔던 int 형 변수, float 형 변수 등과 함께 하나의 변수(포인터 변수)이다.
int형이 정수, float형이 실수를 저장하는 변수였다면 포인터는 메모리 주소를 저장하는 변수이다.
그 중에서도 메모리 상에 위치한 특정 데이터의 시작 주소값을 저장하는 변수이다.
1
2
3
int* p;
int *p;
int *p, *q, *r;
포인터는 위와 같은 방식으로 정의 할 수 있고, 둘 다 같은 뜻이다.
코드에서 볼 수 있듯이, 포인터에도 자료형이 존재한다.
위 코드에서는 포인터가 메모리 상의 int형 데이터의 주소값을 저장한다는 것이 된다. 저렇게 한 번에 여러개를 선언 할 수도 있다.
& (참조)연산자
&연산자를 AND 연산자와 혼동할 수 있는데, &연산자는 피연산자가 두 개가 필요한 이항 연산자이며, 사용할 때는 && 로 사용을 한다.
&연산자의 경우 피연산자 하나에만 적용되는 단항연산자이며, 주소값을 불러오기 위해 사용한다.
1
&a
사용법은 위와 같이 단순하다.
코드 분석
1
2
3
4
5
6
7
8
#include <stdio.h>
int main() {
int a = 2;
printf("%p \n", &a);
return 0;
}
위 코드를 실행하면 int값 2를 저장하는 변수 a의 메모리 주소를 16진수의 형태로 출력하게 된다.
그러나 결과를 보면 0x7ffe6e7ea3e4로 8바이트가 아님을 알 수 있는데, 이는 단순히 앞의 0들이 잘려나간 것이니 참고하면 되겠다.
결과적으로 int형 변수 a는 메모리주소(0x7ffe6e7ea3e4 )를 시작으로 4바이트의 공간에 2를 저장하고 있었다. 라고 정리 할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
int main() {
int *p;
int a;
p = &a;
printf("포인터 p 에 들어 있는 값 : %p \n", p);
printf("int 변수 a 가 저장된 주소 : %p \n", &a);
return 0;
}
이제 &연산자를 통해 얻은 메모리 주소값을 p라는 int형 포인터 변수에 저장하는 코드이다.
결과는 당연히 둘 다 0x7ffe6e7ea3e4를 출력하게 된다.
한 번 정의된 변수의 주소값은 변하지 않지만, 위 코드를 여러번 실행 해보면 매번 다른 값이 나오는 걸 알 수 있다.
이는 ASLR(Address Space Layout Randomization)이라는 기법 때문인데, 프로그램이 실행 될 때마다 메모리 주소 공간을 무작위화 하여 배치하는 보안 강화 기술이다.
그렇기 때문에 코드를 실행할 때마다는 바뀌지만 한 코드 내에서는 변수의 정의된 메모리 주소는 불변이다.
* (역참조)연산자
* 연산자는 곱셈 연산자와 혼동될 수 있으나, 곱셈 연산자는 이항연산자이다.
앞서 & 연산자가 value의 주소값을 가져오는 연산자였다면, 주소값에서 해당 주소값이 가리키고 있는 곳에 저장 된 value를 가져오는 연산자가 * 연산자 이다.
즉 포인터를 포인터에 저장된 주소값에 위치한 value로 취급하는 것이다.
코드 분석
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
int main(){
int* p;
int a;
p = &a;
a = 2;
printf("a의 값 : %d \n", a);
printf("*p의 값 : %d \n", *p);
return 0;
}
해당 코드에서는 포인터 변수 p에 변수 a의 주소값을 저장했고, 변수 a 에 값 2를 저장했다.
출력으로는 a변수에 저장 된 값, p포인터에 저장 된 주소값에 저장 된 값을 출력했다. 포인터 변수 p는 변수 a의 메모리 주소 값을 가지고 있기 때문에 결과는 같은 값이 출력된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
int main() {
int* p;
int a = 2;
p = &a;
*p = 3;
printf("a의 값 : %d \n", a);
printf("*p의 값 : %d \n", *p);
return 0;
}
해당 코드에서는 포인터 변수 p에 변수 a의 주소값을 저장하고, 역참조를 통해서 포인터 p에 저장된 주소값의 값을 3으로 저장했다.
출력값을 보면 처음 a를 선언할 때 2로 초기화를 했지만, 역참조를 통해 a의 값을 3으로 저장했기 때문에 둘 다 3이 출력 됨을 알 수 있다.
메모리 공간을 살펴보면 이런 식으로 포인터 또한 변수이기 때문에 공간을 차지하고있고, 포인터 p가 변수 a를 가리킨다라고 표현한다.
포인터의 타입
포인터는 주소값을 저장하는 곳이라고 계속 얘기해왔다.
근데 주소값만 저장할 거면 32비트 환경에선 4바이트, 64비트 환경에서는 8바이트 일 텐데 왜 굳이 타입이 필요한 걸까??
이유는 포인터에 저장되는 주소값이 변수가 저장된 공간의 시작 주소값이라는 점에 있다.
* (포인터)로 값에 접근을 했을 때 컴퓨터는 메모리에서 시작 할 곳은 알지만 거기서 부터 얼마나 읽어야 하는지를 알 수 없다.
그렇기 때문에 포인터가 int타입이라면 아 int데이터를 가리키겠구나 하고 4바이트를 읽어 값을 가져오는 것이다.
상수 포인터
앞서 상수를 선언할 때 const를 사용한다고 했었다. 이렇듯 const는 불변해야 할 값을 선언 할 때 사용한다.
그럼 const를 포인터에서 사용하면 어떻게 될까?
1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
int main() {
int a;
int b;
const int* pa = &a;
*pa = 3; // ...1
pa = &b; // ...2
a = 5; // ...3
return 0;
}
이 때 const int*의 의미는 const int 형 변수를 가리킨다는 것이 아니라, int 형 변수를 가리키는데 pa 가 가리키는 그 변수의 값을 절대로 바꾸지 마라는 의미가 된다.
그렇기 때문에 코드를 살펴보면 1번 라인은 오류를 발생, 그 외 2, 3번 라인은 정상적인 코드 인 것이다.
1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
int main() {
int a;
int b;
int* const pa = &a;
*pa = 3; // .. 1
pa = &b; // ...2
return 0;
}
그럼 여기선 어떨까. 이번에는 const가 pa 앞에 붙어있다. 이는 의미의 큰 차이를 불러오게 된다.
이번에는 pa를 절대 바꾸지 마라는 의미이기 때문에 pa에 처음 저장된 a의 주소값 외에 다른 값을 가질 수 없는 것이다.
그렇기 때문에 1번 라인은 정상적인 코드, 2번 라인은 오류가 발생하게 된다.
포인터의 덧셈
1
2
3
4
5
6
7
8
9
10
11
#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;
}
해당 코드를 실행해보면 결과로 나온 두 수의 차이는 4임을 알 수 있다.
왜 주소값 + 1을 했는데 +4를 한 값이 출력이 되었을까? 이는 포인터의 형이 int 형이였기 때문이다.
int는 4바이트 이기 때문에 4만큼 더한 값이 나온 것이다. 때문에 char은 +1, double은 +8이 될 것이다.
하지만 포인터끼리의 덧셈은 불가능하다. 이유는 의미가 없기 때문이다. 더해봤자 메모리 공간 속 전혀 연관없는 공간을 가리키게 때문에 이는 막혀있다.
배열과 포인터
앞서 공부했듯이 배열은 메모리 상에서 연속으로 놓이게 된다.
사진은 int형 배열을 나타낸 것으로 배열의 원소가 연속으로 놓이고, 각 원소는 int 형 변수이기 때문에 4바이트를 차지하게 된다.
그렇다면 포인터를 통해서 배열에 쉽게 접근할 수 있겠다! 가 된다.
앞서 포인터의 덧셈에서 보았듯이 +1을 하면 자료형의 크기를 곱한 값을 더하기 때문에 다음 원소를 가리키게 할 수가 있는 것이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#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)) {
printf(" --> 일치 \n");
}else{
printf("--> 불일치\n");
}
}
return 0;
}
해당 코드를 보면 index로 접근했을때의 각 원소의 주소값이 포인터의 덧셈을 통해 접근한 주소값과 일치함을 확인 할 수 있다.
주의점
1
2
3
4
5
6
7
8
9
#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;
}
해당 코드의 출력값을 보면 동일함을 알 수 있다.
????
배열에서의 배열의 이름은 첫 번째 원소의 주소값을 나타낸다는 뜻이다.
그렇다면 배열의 이름은 첫 번째 원소를 가리키는 포인터인가?? 이건 또 아니다.
다음 코드를 보면 알 수 있다.
1
2
3
4
5
6
7
8
#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));
}
이 때의 출력값은 24와 8이 나온다.
배열의 이름의 size는 6개의 int 원소이므로 24가 나왔고, 64비트 환경에서의 포인터의 크기는 8이 출력이 된 것이다.
이를 통해 배열의 이름과 배열의 첫 번째 원소의 주소값은 엄연히 다른 것임을 알 수 있다.
그렇다면 왜 아까는 같은 값이 나왔을까?
그 이유는 c언어에서 sizeof 연산자나 &연산자와 사용될 때를 제외하고는 배열의 이름을 사용할 때 암묵적으로 첫 번째 원소를 가리키는 포인터로 타입 변환이 되기 때문이다!
[] 연산
우리가 익숙하게 사용하던 []도 연산자였다!
1
2
3
4
5
6
7
8
#include <stdio.h>
int main() {
int arr[5] = {1, 2, 3, 4, 5};
printf("a[3] : %d \n", arr[3]); // ...1
printf("*(a+3) : %d \n", *(arr + 3)); // ...2
return 0;
}
한 번도 어떻게 []로 배열의 인덱싱이 가능한지 생각해보지 않았었는데, 출력결과를 보면 위 두 개가 같은 결과를 보인다.
즉 자동으로 2번의 형태로 바뀌어서 포인터 덧셈으로 처리가 된다는 것이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
int main()
{
int arr[10] = {100, 98, 97, 95, 89, 75, 24, 2, 0};
int *parr = arr;
int sum = 0;
while (parr - arr <= 8)
{
sum += (*parr);
parr++;
}
printf("내 시험 점수 평균 : %d \n", sum / 10);
return 0;
}
해당 코드에서는 포인터 parr에다가 arr로 배열의 첫 번째 원소의 주소값을 저장했고, parr이 원소의 마지막 원소를 벗어나지 않는 동안 1씩 더해야며 최종적으로 모든 원소의 합을 구하는 과정이다.
여기서 생각했던게 그렇다면 굳이 parr이라는 새로운 포인터를 선언하지 않고, arr++를 해주면 되는 거 아닌가? 였는데, 따져보면 (메모리 주소) ++ 라는 말도 안되는 형태이기 때문에 안되는게 당연했다.
출처 : 씹어먹는 C언어