1042라는 십진수를 102(십진수)라는 주소에 대입하라.
1042를 2진수로 보면, 이 값은 8비트 하나에 모두 저장할 수 없다. 따라서 두 개의 8비트에 나눠서 들어가게 된다.
메모리에 저장된 데이터를 표시할 때 16진수로 하는데, 이는 2진수에 가장 가까운 표현법이다. 1바이트는 8비트, 혹은 4비트 2개라고 할 수 있으며 이러한 4비트는 2의 4승으로 0부터 15까지, 16진수 1개를 나타낼 수 있다.즉 16진수 1개가 나타낼 수 있는 '한 자리수'와 같다. 이때 0x는 그저 10진수와 헷갈리는 것을 방지하기 위해 붙이는 것이다.
따라서 그림과 같이 0x04(10진수로 4), 0x12(10진수로 18)로 표현된다. 주소 또한 메모리에 할당될 수 있기 때문에 16진수로 표현한다.
0x0412값을 0x00000066 주소에 2바이트 크기로 대입하라를 어셈블리로 보면
mov word ptr[00000066h], 0412h
어셈블리에서는 16진수의 0x를 뒤의 h(hexadecimal)로 표현한다. 여기서 word는 2바이트 단위로 처리하겠다는 것이다.
short birthday; // birthday가 메모리 0x00000066에 위치한다고 가정.
birthday = 0x0412; // mov word ptr[00000066h], 0412h로 번역(컴파일)된다.
어셈블리와는 다르게 c언어가 메모리 주소 대신 변수라는 개념을 사용하는 이유는, 코드를 더 쉽게 이해할 수 있기 때문이다. 이는 c언어, 혹은 프로그래밍 언어(사람에 가까운 언어)가 개발된 이유라고 볼 수 있을 듯 하다.
tips는 main이라는 함수에서 지역 변수로 사용되기 때문에, Test라는 함수 내에서 사용할 수 없다. 메모리에서의 주소가 다름에도 불구하고 참조를 할 수 없는 것이다.
간접 주소 지정 방식
메모리에 주소값이 대입되므로 메모리를 더 사용하게 된다. 직접 주소 지정 방식은 c언어에서 변수를 이용하고, 간접 주소 지정 방식은 포인터를 이용하고 있다.
int addr = 0x0000006C;
일반 변수도 주소를 저장할 수 있으나, 저장된 주소의 메모리에 가서 값을 읽거나 저장할 수 있는 기능은 없다.
short birthday; // 2byte 크기의(short형) 변수 birthday 선언
short *ptr; // 2byte 크기의 대상을 가르킬 수 있는 포인터 ptr 선언
ptr = &birthday; // 변수 birthday 주소를 계산해서 ptr에 알려달라.
// 컴파일하면 birthday 값이 아니라 birthday의 주소가 ptr에 들어가게 된다.
// 즉 0x0000006C라고 직접 적지 않는다. 주소가 바뀌면 문제가 될 수 있기 때문이다.
#include <stdio.h>
// 포인터는 메모리를 효율적으로 이용하는 도구이다.
void main()
{
short birthday;
short *ptr;
ptr = &birthday;
// %p 형식은 메모리 주소를 16진수 형태로 출력한다.
printf("birthday 변수의 주소는 %p입니다.\n", ptr);
}
ptr에 직접 0x0000006C라고 적어도 되지만, 프로그램이 실행될 때마다 주소는 바뀐다. 따라서 프로그램 실행 시 우연히 0x0000006C에 어떤 값이 저장되어 있을 수 있다. 이러면 문제가 생긴다. 따라서 주소를 직접적으로 적는 것은 좋지 않다.
#include <stdio.h>
void main() {
short birthday; // short형 변수 birthday 선언
short *ptr; // 포인터가 가리키는 대상의 크기가 2byte인 포인터 변수 선언
ptr = &birthday; // birthday 변수의 '주소'를 자기 자신인 ptr 에 대입
*ptr = 1042; // ptr에 저장된 주소(ptr이 가리키는 대상) 에 가서 값 1042를 대입, 즉 birthday = 1042
printf("%d", birthday);
}
ptr 자기 자신한테 값을 대입할 때는 * 없이, ptr이 가리키는 대상에 값을 저장할 때는 *을 붙인다. 이렇듯 포인터는 *를 붙이고, 혹은 붙이지 않고 사용할 수 있다.
#include <stdio.h>
void main()
{
short birthday;
short *ptr;
ptr = &birthday;
*ptr = 0x0412;
printf("birthday = %d\n", birthday);
}
birthday에 직접적으로 값을 입력하지 않았음에도 불구하고, birthday에 1042이라는 10진수가 제대로 들어간 것을 확인할 수 있다.
모든 변수가 같은 함수에 선언되어 있는 것이 아니기 때문이다. c언어는 함수와 함수가 단절되어 있다. 서로의 안에 선언된 일반 변수를 서로 참조할 수 없다. 허나 포인터 변수는 다른 함수에 선언된 변수의 값을 읽거나 변경이 가능하다.
end에 start를 대입하는 순간, end와 start 모두 96이 되어버린다. 따라서 start에 5를 대입할 수 없게 된다.
이를 보완하기 위해서는 temp라는 변수(공간)이 더 필요하다.
#include <stdio.h>
void Swap(int a, int b)
{
int temp = a;
a = b;
b = temp;
}
void main()
{
int start = 96, end = 5;
printf("before : start = %d, end = %d\n", start, end);
if (start > end) {
Swap(start, end);
}
printf("after: start = %d, end %d\n", start, end);
}
하지만 프로그램 실행 시 여전히 after에서 start와 end 값이 변하지 않은 것을 확인할 수 있다. 이는 Swap 함수 안에서만 서로 교환 되었을 뿐이기 때문이다. main 함수에서는 바뀐 start와 end를 참조하지 못하는 것이다.
#include <stdio.h>
void Swap(int *pa, int *pb)
{
int temp = *pa; // *pa(start) = 96, *pb(end) = 5
*pa = *pb; // *pa(start) = 5, *pb(end) = 5
*pb = temp; // *pa(start) = 5, *pb(end) = 96
}
void main()
{
int start = 96, end = 5;
printf("before : start = %d, end = %d\n", start, end);
if (start > end) {
Swap(&start, &end);
}
printf("after : start = %d, end = %d\n", start, end);
}
위와 같이 포인터를 활용하여 swap 기능을 제대로 실현할 수 있다.
pa = pb;
*pa = pb;
위의 경우 데이터 타입이 다르기 때문에(포인터 or int형) 컴파일 에러가 난다.
int temp = *pa; // *pa(start) = 96, *pb(end) = 5
pa = pb; // pb에 저장된 주소가 pa에 복사되어 pa도 end를 가리킴
*pb = temp; // *pa(end) = 96, *pb(end) = 96
위의 경우 pa, pb 모두 end를 가르키게 된다. 컴파일 에러가 나지 않기 때문에 코드가 복잡해지면 에러를 찾아내기 어렵다.
이러한 실수를 막기 위해 const 키워드를 사용하기도 한다.
void Swap(int * const pa, int * const pb)
{
int temp = *pa;
pa = pb; // pa는 const 변수이기 때문에 변경이 불가능하여 오류 발생
*pb = temp;
}
pa라는 포인터의 값, 즉 주소는 const로 인해 바뀔 수 없다.
const int * const pa;
위의 경우는 pa(pa의 값으로 주소)와 *pa(pa가 가리키는 대상) 모두 변경될 수 없다.
const int *pa;
*pa(pa가 가리키는 대상)은 변경될 수 없다.
이 모든 경우, 당연히 처음 초기화(선언)될 때에는 오류가 나지 않는다.
int data = 5, temp = 0;
int * const pa = &data; // 처음 초기화(선언)되는 부분으로 오류가 발생하지 않는다.
*pa = 3;
pa = &temp; // 오류 발생
포인터 변수의 주소 연산
시작 주소와 끝 주소를 기억하는 것과 시작 주소와 사용할 크기를 기억하는 것이 있다.
시작 주소와 끝 주소 2개를 기억하려면 4바이트씩 총 8바이트가 필요하다. 따라서 시작 주소와 사용할 크기를 이거하는 방법으로 포인터를 사용하고 있다.
int data = 0;
int *p = &data; // 시작 주소인 &data에서 4바이트 크기만큼 사용하겠다.
*p = 5;
short data = 0;
short *p = &data;
p = p + 1;
위의 경우, data 주소에 0이라는 값이 대입된다. 그리고 포인터 p가 선언되고 이 p는 data의 시작 주소를 가르키게 된다. 그리고 p가 가르키는 대상에 가서 작업을 할 때 2byte 크기만큼만 쓰게 된다. 즉 p에는 주소값이 대입되고, *p는 data의 시작 주소를 가르키게 된다.
여기서 +1을 하면 산술 연산으로 1이 증가하는 것이 아니라, 포인터가 가르키는 데이터의 다음 데이터를 가르키게 되는 주소 연산을 수행하게 된다.
주소에 +1을 하면 100 주소에서 101 주소로 가는 것이 아니라 102 주소로 가는 것이다. +2를 하면 104 주소로 가게 된다.
만약 다음과 같이
int *p = &data;
int 형이었다면, +1 주소 연산 시 100 주소에서 102 주소가 아니라 104 주소로 가게 된다.
#include <stdio.h>
int main()
{
// p1, p2, p3, p4에 강제로 100이라는 주소를 저장
char *p1 = (char *)100;
short *p2 = (short *)100;
int *p3 = (int *)100;
double *p4 = (double *)100;
p1++; // 101
p2++; // 102
p3++; // 104
p4++; // 108
printf("%p %p %p %p", p1, p2, p3, p4);
}
추가로, 포인터 변수가 가르키는 대상의 자료형과, 포인터 변수 선언 시 적는 자료형을 동일하게 지정하는 것이 일반적이다.
#include <stdio.h>
void main()
{
int data = 0x12345678, i;
char *p = (char*)&data;
// 4바이트 데이터를 바이트 단위로 값 출력 4번 반복
for (i = 0; i < 4; i++) {
printf("%x, ", *p); // p가 가진 주소부터 1바이트 크기만큼 사용
p++; // 1바이트 만큼 증가
}
}
int data = 0;
void *p = &data; // data의 시작 주소를 저장한다. 캐스팅 없이, 경고 없이 다 받을 수 있다.
*p = 5; // 이 때는 오류가 난다. 대상 메모리의 크기가 정해져 있지 않기 때문
자기가 얼마만큼의 크기로 작업해야 될지 모르기 때문에(끝 주소를 모르는 상황이기 때문에) 오류가 난다.
따라서
int data = 0;
void *p = &data;
*(int *)p = 5; // 대상의 크기를 4바이트로 정함.
받을 때는 편하나 쓸 때는 캐스팅으로 타입 변환을 해줘야 한다. 받을 때는 편하지만 쓸 때는 불편하다. 해당 프로그램을 쓰는 사람에게는 편리함을 제공한다.