CS 50 - 16진법과 포인터, Call by reference

2021. 7. 18. 10:36컴퓨터 과학

현재 카테고리의 첫 글에서 binary와 컴퓨터가 값을 표현하는 방법에 대해 설명했다.

컴퓨터는 8비트(1바이트)를 기준으로 8개의 0과 1로 표현을 하게 되는데, 이를 사람이 어느 정도 보기 편하게 바꿔놓은 것이 16진수이다.

 

16진수는 대부분 알 것 같으니 패스하고, 10진수는 0과 1로 이루어진 코드를 직관적으로 보기 힘들지만, 2^4 = 16이라는 사실로 1바이트를 16진수 숫자 2자리로 표기하는 것이다.

 

그런데 10진법 10과 16진법 10은 서로 "10"이지만 나타내는 값은 10과 16이다. 이런 모호함을 방지하기 위해 사람들은 16진수 수의 앞에는 0x를 붙이도록 약속했음.

예를 들어 1111 1111은 0xFF로, 0010 1100은 0x2C 이런 식으로..

이 정도 설명하면 16진수는 끝

 

메모리

메모리는 컴퓨터가 정보를 저장하는 공간임은 아마 다 알 테고, 컴퓨터를 하며 동작하는 모든 "값"은 고유의 메모리 주소를 가지고 있다.

0x0 0x1 0x2 0x3 0x4 0x5 0x6 0x7
0x8 0x9 0xa 0xb 0xc 0xd 0xe 0xf
...              
               

이런 식으로 각각의 주소에는 각각의 트랜지스터가 있고 여기서 비트를 표시한다.

컴퓨터는 이런 식의 주소 체계에서 비어있는 부분에 정보를 입력하고 이를 가져다 쓴다.

예를 들어

int a = 2147483647;

printf("%p", &a);

이런 코드가 있다면 화면에 0x~~~~~~~와 같은 값이 출력될 것이다.

이 0x~~~은 값의 주소를 뜻하며, 해당 주소 값부터 int의 크기인 4바이트만큼 a에 저장해 둔 값이 들어가 있다.

즉, 0x12345678가 나왔다면, 메모리의 0x12345678부터 7F FF FF FF가 저장되어있다는 뜻이다.

물론 이는 운영체제가 x86(32비트), x64인지 메모리가 빅 엔디안 방식, 혹은 리틀 엔디안 방식으로 저장되었는지 따라 조금씩 차이가 있지만, 다음을 기약하자.

 

다시 위와 같이 &a에서 '&'는 앰퍼샌드로 발음하며 a의 주소를 나타내는 연산자이다. 비트 마스킹과는 다르며, 반대의 역할을 하는 연산자로는 '*' 연산자가 있다. *연산자는 해당 주소로 이동하여 값을 참조하는 "역참조" 연산자이다.

만약 *(&a)라는 표시가 있다면 a와 같은 값을 출력할 것이다. (a의 주소를 물은 다음 해당 주소를 참조)

 

이런 *은 문맥에 따라서 "포인터"라는 의미로 쓰일 수 있는데, 주소를 저장하는 "포인터 변수"를 선언할 때에는 변수명 앞에 *를 붙여 포인터 변수임을 표시한다.

int a = 50;
int *b = &a;	//int *는 해당 주소에 들어있는 값이 int임을 뜻한다.
int c = *b;

위와 같은 식은 a의 주소에 50이라는 수를 입력한 후,

포인터 변수(주소를 저장하는 변수) b에 a의 주소를 입력하고,

정수를 담을 변수인 c에 b에 들어있는 a의 주소로 이동해 값을 역참조하여 50을 넣는다.

 

이해하기 쉽게 추상화를 하자면,

포인터 변수 b는 어떤 그림 a가 있는 갤러리 주소를 가지고 있고, c는 b를 보고 해당 갤러리로 찾아가서 그림을 똑같이 그려서 자기 갤러리에 둔다... 고 설명하면 댐?

 

아마 안될 거 같은데;

좀,,, 됨,,?

위 사진을 보면 주소를 담는 포인터 변수 또한 주소를 가짐을 알 수 있다.

이는 *b의 주소를 담는 **d라는 변수를 만들 수 있고, **d를 통해 d를 역참조하여 가져온 b의 주소를 역참조하여 a를 가져올 수 있음을 뜻한다.

 

포인터가 처음이라면 약간 복잡할 수 있지만, 뭐 어쩌겠음 다 하다 보면 익숙해짐

 

자 그럼 이제 방금 사용한 포인터를 이용해 값을 바꾸는 함수를! 써보도록 하겠읍니다. ㅎㅎ

#include <stdio.h>

void swap1(int, int);
void swap2(int*, int*);
int main(void)
{
	int a, b;
    a = 10;
    b = 20;
    
    /*	//주석만 지우고 써보세여
    	// case 1
        int tmp = a; //임시 변수를 만들어서 a의 값을 잠시 담아둠
        a = b;
        b = tmp;
    */
    
    /*
    	//case 2
        swap1(a, b);
    */
    
    /*
    	//case 3
        swap2(&a, &b);
    */
    
    /*
    	//bonus
        a ^= b;
        b ^= a;
        a ^= b;
    */
    
    printf("a : %d b : %d\n", a, b);
    
	return (0);
}

void swap1(int a, int b)
{
	int tmp = a;
    a = b;
    b = tmp;
}

void swap2(int *a, int *b)
{
	int tmp = *a;
    *a = *b;
    *b = tmp;
}

총 4개의 케이스를 넣어놓았구 마지막 부분은 이해하기 어려울 수 있습니다. 그래서 보너스에여

 

첫 번째 케이스의 주석을 지우고 실행시켜보면 a : 20, b : 10으로 값이 잘 바뀐 것을 볼 수 있습니다.

다시 첫 번째 케이스에 주석을 넣고 두 번째 케이스를 돌려보면 같은 알고리즘에 함수만 사용했는데, 더 이상 바뀌지 않음!!

 

call by reference VS call by value

왜 그런고 하니, 첫 케이스의 a, b는 각각 10과 20이라는 값을 가지고 있고 "main"이라는 함수에서 선언이 되었습니다.

그렇다는 것은 이 a와 b 두 변수들은 main 함수가 종료될 때까지 값을 유지하고 있습니다. 프로그램이 끝날 때까지는 변수들이 남아있겠죵?

그런데 두 번째 케이스에서는 이 해당 값들을 "복사"해서 swap1 함수로 보냈습니다.

복사된 함수는 main과는 비록 다른 주소 값을 가졌지만, 같은 값을 가지고 있었습니다,,,,

swap 함수는 메인에서와 같은 동작을 했고, 물론 swap1 함수 내부의 복사해온 값 역시 잘 바뀌었습니다. 

swap1 함수가 끝날 때까지만요.

swap1 함수에 printf("a : %d, b : %d\n", a, b);를 넣어서 확인해보시면 분명히 20 10으로 바뀐 채로 출력됩니다.

 

근데 아까 "메인"함수에서 선언한 변수는 "메인 함수가 종료될 때 까지" 값이 유지된다고 했는데,

메인에서 값을 "복사"해온 a와 b 역시도 swap1 함수가 종료될 때까지 값을 유지합니다.

사라져 버릴 모래알 같은 변수들을 가지고,,,

값을 잘 바꿨으니 할 일을 끝낸 swap1은 종료됩니다. 그럼 그 복사된 값은 사라져버리죵 ,,, 

그렇다면 어떻게 해야 "메인"에서 선언한 a와 b의 값을 바꿀 수 있을까요?

 

바로 main에서 선언한 a와 b의 주소를 "직접" 주는 것입니다.

그럼 함수는 주소의 값을 받아 *를 붙여 값이 저장된 위치로 찾아가서 swap2 연산을 합니다.

자 swap2에서 값을 잘 바꿨으니 할 일을 끝낸 swap2 역시 종료됩니다.

 

아까와 다른 점이 있다면, swap2의 a와 b는 main 함수에서 선언한 변수이기 때문에, swap2 함수가 종료되어도 남아있습니다.

이 값은 메인에서 swap 연산을 한 것처럼 main 함수가 종료될 때까지 남아있게 됩니다.

 

이 차이가 call by reference와 call by value 방식의 차이입니다.

 

call by value 방식의 swap1 함수가 메인 함수의 변수 a, b의 '값'을 복사해서 함수 내에서 사용했다면,

call by reference 방식의 swap2 함수는 메인 함수의 변수 a, b의 '주소'를 복사한 후 직접 역참조를 해서 사용하게 합니다.

이러한 방식으로 더 상위 함수에서 선언한 값이 해당 함수가 끝날 때까지 남아있게 해 줍니다.

 

보너스

10과 20은 각각 0과 1로 나타내면

a : 0000 1010 => 8 + 2

b : 0001 0100 => 16 + 4로 나타낼 수 있습니다.

 

^연산은 두 비트 간 '하나'만 있는 비트는 1로, 둘 다 0이거나 둘 다 1인 경우에는 0으로 필터링(정확히는 마스킹이라고 표현합니다)해줌

xor 연산이라고도 하는데, exclusive or(상호 배타적인 'or' 연산)이라는 뜻이다.

여기에 =을 붙인 ^= 연산자는 +=, -= 연산자 등과 같이 a ^= b => a = a ^ b라는 뜻을 가지고 있다.

 

위의 3 단계를 거치면 변수 선언 없이 값을 바꿀 수 있는데 동작 구조는 다음과 같다.

 

0 0 0 0  1 0 1 0 => 10

0 0 0 1   0 1 0 0 => 20

0 0 0 1   1 1 1 0 => a = 30

 

다시 b ^= a

 

0 0 0 1  0 1 0 0 => 20

0 0 0 1  1  1 1 0 => 30

0 0 0 0  1 0 1 0 => b = 10

 

b가 바뀌었다!

 

다시 a ^= b

 

0 0 0 1  1 1 1 0 => 30

0 0 0 0 1  0 1 0 => 10

0 0 0 1  0 1 0 0 => a = 20

 

잘 바뀐 걸 볼 수 있다.

 

요 로직은 더하기로도 할 수 있는데,

a = a + b => 10 + 20 = 30

b = a - b => 30 - 20 = 10

a = a - b => 30 - 10 = 20

과 같이 나타낼 수도 있다 ㅎㅎ

 

비트 연산은 한 번 더 보면서 원리를 생각해주셈

반응형