포인터
포인터: 주소를 저장하는 변수
사실 포인터는 아주 간단하다. int a;를 살펴보자
정수형 변수 a가 선언되고 4바이트 메모리 방이 할당된다는 걸 이미 알고 있다.
여기 메모리 방에는 정수 값이 저장될 수 있다.
메모리 방에 주목하고 간단한 질문에 대한 대답을 생각해보자
우리집에도 있고 친구 집 등 모든 집에 있는 것은 무엇일까?
정답은 그 집에 해당하는 주소이다.
메모리도 마찬가지이다.
우리가 원하는 정보를 저장하는 메모리 방에도 해당하는 메모리 방의 주소가 있다.
다시 int a;를 생각해보자
int a;가 수행되는 순간 컴퓨터는 정수를 저장할 수 있는 메모리 방을 잡는다.
컴퓨터 안에는 사용할 수 있는 메모리 방이 수없이 많다.
그 중에서 4바이트 크기의 정보를 저장할 수 있는 메모리 방을 골라 임의로 변수a라는 이름으로 잡는 것이다.
물론 그 메모리 방에는 해당하는 주소가 있다.
우리가 호텔에서 방을 잡으면 '303호입니다.'라고 통보를 받듯이, 정수형 변수 메모리를 할당 받으면 방 주소에 해당하는 7339124같은 형태의 메모리 주소가 할당된다.
정수를 저장하려면 정수형 변수 int를 사용하여 메모리 방을 할당 받아서 저장했다.
정수를 저장하는 메모리 방이 있다면 주소를 저장하는 메모리 방도 따로 있는 법이다.
결론적으로 이야기하면 주소를 저장하는 변수가 바로 포인터이다.
숫자 3을 저장할 때 정수형 변수가 필요한 것처럼, 어떤 변수의 메모리 주소를 저장하려면 포인터 변수가 필요하다.
주소를 저장하기 위해 특별한 변수를 사용하는데 이것은 포인터 변수라고 한다.
정수형 변수a의 주소를 저장하는 포인터 변수 b를 선언하고 변수a의 주소를 포인터 변수 b에 저장할 수 있다.
ex) 아파트 경비원 아저씨가 어떤 집을 찾아주시거나 설명하려면
1. a아파트 102동 302호로 가시면 되요. ===> a변수
2. a아파트 가을이네 집 주소로 가면 되요. ====> b변수
C언어도 마찬가지다.
int a;처럼 변수 a를 선언하면 컴퓨터에서 임의로 메모리를 할당받는다.
그 뒤에 해당하는 주소를 '7339124번지 or 정수형 a의 주소'라고 말하면된다.
정수형 a의 주소를 표기하고자 할때는 &기호를 사용해서 나타내고 이것을 '주소 연산자'라고 한다.
즉, 변수a의 주소는 '7339124' or '&a'이 두가지로 표현할 수 있다.
포인터도 변수!!
변수 a의 주소, 즉 &a를 저장하는 변수를 만들어 보자
&a는 주소이기 때문에 저장하려면 포인터 변수를 선언해야 한다.
또한 a는 정수형 변수이므로 &a는 정수형 변수의 주소이다.
정수형 변수의 주소를 저장하는 포인터 변수 b를 선언하자면 아래와 같다.
int *b;
int: 정수형
*: 포인터 변수를 의미
b: 변수 이름
이제 포인터 변수 b는 정수형 변수의 주소를 저장할 수 있다.
double형 변수 x를 만들고 x의 주소 값을 저장하는 포인터 변수 y를 선언과 동시에 초기화하자
double x; //'x라는 집'라고 하고
double *y = &x; //주소변수를 담는 바구니를 *y이고 &x는 주소이다. 즉, *y는 x의 주소를 가르킨다라고 해석할 수 있다.
'포인터 변수가 주소를 저장하고 있다.'는 무슨 의미일까?
int a;
int *b = &a;
포인터 변수 b가 주소 값 104번지를 가지고 있다.
즉, 104번지의 변수a를 가르키고 있다는 의미이다.
'가르키고 있다.'라고 하여 포인터이다.
정리하자면, 포인터 변수가 '어떤 메모리의 주소를 저장한다.'는 의미는 '저장한 주소에 해당하는 메모리를 포인터 변수가 가리킨다.'는 말을 포함한다.
포인터의 필요성
포인터가 가리키는 대상 변수(변수a)를 참조할 수 있다.
참조할 수 있다는 말은 가르키는 대상 변수에 접근할 수 있다는 의미로,
가르키는 변수의 값을 포인터 변수를 통해 초기화하거나 변경할 수 있다는 뜻이다.
이게 무슨 말이냐 즉, 역참조라고 하는데
포인터 변수를 통해 가리키는 변수의 값을 설정할 수 있다는 의미이다.
포인터 변수 b를 통해 간접적으로 접근하여 변수a의 값을 15로 변경할 수 있다.
기존에는 이렇게 변형 시켰다면 포인터 변수를 활용하면 아래 코드와 같다.
즉, *b=15는 a=15와 같은 의미이다.
포인터 변수 *를 사용하는 경우는 무조건 두가지 중 하나이다.
1. 포인터 변수를 선언하고자 할 때 *를 사용
2. 역참조를 할 때 *를 사용
1번째의 경우는 *b=15는 포인터 변수를 선언하는 문장이므로 *를 사용한 것이다.
2번째의 경우는 *b=15는 포인터를 선언하지 않는데 *를 사용한 것이므로 여기서 *은 역참조를 하고 있는 것이다.
b=&a;는 뭘까? 포인터 변수 b에 *를 사용하지 않았는지에 대한 답은 쉽다.
포인터 변수b를 선언한 것도 아니고 역참조를 하는 것도 아니다.
이 두가지에 해당하지 않는다면, 그냥 포인터 변수를 초기화하는 것이다.
포인터 변수를 선언과 동시에 초기화하고 싶다면 int *b = &a;와 같이 작성하면 된다.
변수 a의 주소 값은 수행하는 컴퓨터 환경에 따라 다르다.
또한 코드를 실행할 때마다 새롭게 변수를 정의하므로 실행 결과로 나오는 주소 값은 매번 달라질 수 있다.
주소 값은 왜 매번 달라 질까?
1. 주소 값의 다양성: 컴퓨터의 메모리 구조는 각 시스템마다 다를 수 있다.
따라서 변수가 메모리 상에서 할당되는 주소 값은 컴퓨터의 운영체제, 컴파일러 등에 따라 달라진다.
이러한 이유로, 같은 c코드를 다른 컴퓨터에서 실행하면 주소 값이 다르게 나타날 수 있다.
2.새로운 변수 할당: 포인터 변수를 선언하면, 해당 포인터 변수는 메모리 주소를 저장하기 위한 공간을 확보한다.
코드를 실행할 때마다 함수 호출, 반복문 등으로 인해 새로운 변수가 할당되는 경우, 해당 변수의 주소 값도 달라진다.
따라서 반복문 내에서 동일한 코드가 반복 수행되면서 변수를 새롭게 할당하게 되므로 주소 값이 매번 달라진다.
하지만, 주소값의 변화는 프로그램의 정확성에 큰영향을 미치지 않는다.
포인터를 통해 변수를 간접적으로 참조하므로 프로그래머가 실제 주소 값을 알 필요는 없다.
대신 포인터를 통해 해당 주소의 값을 읽거나 변경할 수 있다.
주소 값이 달라진다고 해도 프로그램의 동작은 동일하게 유지 될 수 있다.
실수를 저장하고 싶다면 float, double자료형으로 선언
float 4바이트 크기의 방이므로 4바이트에 해당하는 크기만큼 실수로 저장할 수 있다.
더 큰 바이트를 저장하려면 두배 더 큰 메모리 즉, 8바이트 메모리 크기를 가지는 double을 선언하여 사용해도 된다.
하지만 포인터 변수는 자료형에 상관없이 메모리 크기가 언제나 같다.
float형 포인터와 double형 포인터 변수의 메모리 크기가 같다는 말이다.
하물며 배열, 조체 등 어떤 자료형의 포인터 변수도 크기는 모두 같다.
쉽게 설명하자면, 집의 크기가 크든 작든 똑같이 갖고 있는 것은 '주소'이다.
큰 집이거나 작은 집이거나 상관없이 집 주소 형식은 동일하다.
아무리 큰 집이여도 'abc로 123길'일 것이고, 반대로 작은 집 주소는 'xyz로 254길'과 같이 형식이 똑같다.
단 컴파일러 종류에 따라 포인터 변수의 크기가 8바이트가 아닌 4바이트일 때도 있다.
만약 컴파일러 설정이 32비트일 경우 포인터 변수의 크기는 4바이트이고
64비트로 설정할 경우에는 포인터 변수의 크기가 8바이트가 된다.
포인터의 존재여부
포인터는 왜 존재할까? 그 이유를 알기 전에 우선 아래 문제를 풀어보자
x=1, y=2로 선언하고 출력할 때 서로 바꿔서 출력하시오.
기존에 배웠던대로 하면 이러한 형식으로 코드를 구현해볼 수 있다.
main()함수에서 값을 변경하는 것이 아니라 값을 변경하는 함수를 작성하여 호출하는 것이 바람직하다.
함수와 함수 사이에 값을 주고 받을 때 포인터를 사용하여 주소 값을 전달하면 복사가 아니라 변수의 메모리 주소가 전달된다. 따라서 주소를 전달받은 함수 안에서 전달된 가리키는 값, 즉 함수를 호출한 곳의 변수 값을 변경할 수 있다.
함수와 함수 사이에 값을 주고 받을 때 복사를 상용하지 않는 이유는 메모리 및 성능 효율성 때문이다.
1. 메모리 효율성: 변수의 크기가 큰 경우, 값에 의한 복사를 사용하면 해당 변수의 복사본이 생성된다. 이렇게 되면 메모리 사용량이 증가하고, 데이터 복사에 시간이 소요된다. 포인터를 사용하여 변수의 주소만 전달하면 원래 변수를 복사하지 않고 해당 주소를 통해 변수에 접근할 수 있기 때문에, 메모리를 더 효율적으로 사용할 수 있다.
2. 성능 효율성: 값에 의한 복사를 사용하면 변수가 크거나 복잡한 데이터 구조를 다룰 때 함수 호출에 상당한 시간이 소요될 수 있다.
3. 값 변경 가능성: 포인터를 사용하여 주소 값을 전달하면, 함수 내부에서 해당 주소를 참조하여 변수의 값을 변경할 수 있다. 이는 값에 의한 복사를 사용할 때는 불가능한 일이다.
4. 동적 메모리 할당: 포잍너를 사용하여 메모리를 동적으로 할당할 수 있다. 이렇게 할당한 메모리는 함수 호출이 끝난 후에도 유지되므로, 함수 외부에서도 사용할 수 있다.
포인터 배열
int a[5] = {1,2,3,4,5};
정수 다섯개를 저장할 수 있는 메모리 방이 연속해서 다섯 개 생성된다.
즉, 정수 다섯개를 저장하는데 필요한 20바이트(5개의 정수 x 4바이트) 메모리를 확보할 수 있는 공간을 찾을 것이다.
사용가능한 20바이트 메모리 어딘가에 할당 받고,
다섯개의 방은 각각 a[0], a[1], a[2], a[3], a[4]라는 이름으로 할당한다.
여기서 '어딘가'는 우리가 신경 쓰지 않아도 되고 알필요도 없다.
컴파일러가 알아서 사용가능한 메모리 방을 할당한다.
이처럼 어딘가에 잡은 방이라도 주소는 있다.
각 메모리 방의 주소는 숫자로 표현될 수 있고, 배열의 이름으로도 표현될 수 있다.
여기서 각 주소에 해당하는 실질적인 숫자를 물리적 주소라고 하고,
각 주소에 해당하는 변수를 통해 표현하는 것을 상징적 주소라고 한다.
물리적 주소 104번지, 108번지, 112번지, 116번지, 120번지
상징적 주소 &a[0], &a[1], &a[2], &a[3], &a[4]
컴퓨터의 컴파일러가 5개의 정수를 저장하고서 104번지~120번지까지 사용가능한 메모리 방을 할당받았다.
여기서 a[0]의 주소는 104번지라는 숫자로 표현 할 수 있고, 주소 연산자 &를 사용하여 &a[0]라고도 표현할 수 있다.
각방은 4바이트 크기로 하나의 정수를 저장하므로 처음 방이 104번지였다면, 두 번째 방은 4바이트 더해진 108번지가 되고 마지막 방은 120번지가 된다.
물리적 주소가 4바이트씩 커지는 것을 확인할 수 있다.
컴파일러가 필요에 따라 그때 그때 메모리를 할당 받으므로
컴퓨터의 환경과 수행 시점에 따라 주소값은 언제든 달라질 수 있다.
따라서 다음 코드를 수행할 때는 28308744에서 시작하지 않을 수 있다.
코드에 경고가 발생하는 이유는 주소를 출력하는 데 printf() 함수의 형식 지정자 %d를 사용했기 때문이다.
변수의 주소를 출력할 때 %p형식 지정자를 사용하면 경고가 없어진다.
%p를 사용하면 16진수로 주소를 표현한다.
하지만 10진수 숫자로 알아보기 쉽게 하고자 %d로 출력했다.
포인터도 변수이다.
하나의 포인터 변수로 주소 값을 연속적인 메모리 공간에 저장할 수 있다.
즉, 포인터도 배열로 구성할 수 있다.
int *a[2];
a[0]와 a[1]은 모두 포인터 변수이며, 어떤 정수형 변수의 주소를 가질 수 있다.
포인터 배열 변수 a[0]에는 정수형 변수 b의 주소를 넣었으므로
104가 저장되고 a[1]에는 정수형 변수 c의 주소를 넣었으므로 208이 된다.
문제
아래 그림과 같이 정수형 변수 a, b,c를 선언하고 포인터 배열 x를 사용하여 정수형 변수 a와b의 합을 c에 저장하는 프로그램을 작성하시오.
포인터 배열에 차례대로 정수형 변수 a, b, c의 주소를 넣어 각 변수를 가르키게 하고 포인터 배열의 각 요소를 역참조하여 a, b, c변수에 접근이 가능하게 했다.
이를 통해 a와 b의 합을, 포인터 배열 요소 x[2]를 역참조하여 정수형 변수c에 저장했다.
'C언어' 카테고리의 다른 글
C언어 <전치행렬> (0) | 2023.08.05 |
---|---|
C언어 <문자열> <함수 포인터> (0) | 2023.07.26 |
C언어 <배열> (0) | 2023.07.24 |
C언어 <함수> (0) | 2023.07.21 |
C언어 <반복문> 13~19 문제 풀이 (0) | 2023.07.21 |