본문 바로가기
P.L

C/C++ call by value, 포인터(call by reference), 메모리 접근 동작 방식

by ahsung 2021. 10. 5.

C/C++의 call by value, call by reference

포인터 =  call by reference, 그 외 타입은 call by value 라고 그냥 외우는 경우들도 많은데,

이런 암기로는 C/C++ 언어의 특성과 동작 방식을 제대로 이해하기 힘들다고 생각합니다.

 

먼저 C언어의 변수가 가지고 있는 정보는 변수에 할당된 메모리의 첫번째 주소, 그리고 타입(type)이며

항상 모든 타입 변수에 대해 기본적으로 call by value로 동작하고 있습니다.

그리고 call by reference가 가능하도록 연산자를 제공할 뿐입니다.

 

C언어의 call by value

변수가 의미하는 메모리 영역의 값을 그대로 가져옵니다.

예로 아래 예시의 var 변수는 첫주소 100000000와 long long 타입(8바이트)를 통해서

100000000~ 100000007(8바이트) 영역에 저장되어 있는 0x100000004를 가져옵니다.

 

 

C언어의 call by reference를 위한 연산자와 포인터 타입

C언어는 call by reference를 위해 &와 * 연산자를 제공합니다

.

변수 앞에 &를 붙이면 해당 변수의 첫번째 주소를 반환해줍니다.

변수 앞에 *를 붙이면 해당 변수의 메모리안의 값을 "또다른 메모리 주소"로서 인식하여

인식한 메모리 주소를통해 알아낸 메모리 영역의 값을 가져오게 됩니다.

 

이때  call by value의 경우, 첫번째 메모리 주소와 타입을 통해 메모리 영역을 알수 있었는데,

위의 설명만으로는 * 연산자의 경우, 첫번째 메모리 주소는 알지라도 얼만큼의 영역을 가져올지 알 수 가 없습니다.

 

이를 위해 C언어에는 포인터라는 타입이 존재합니다.

일단 포인터는 int *, char *, 혹은 특정 구조체 *,  포인터를 통틀어서 모두 8바이트 타입입니다.

다만 이들의 차이점이라면 *, 포인터 연산자를 통해 내부 값을 주소로서 인식하여 인식한 주소로 접근할때

얼만큼의 메모리 영역을 가져올지 구분하게됩니다.

int *이라면 점프한 곳에서 4바이트, char * 라면 1바이트...

 

 

실습 예시

// 변수 앞에 오는 & 연산자는 변수의 첫번째 메모리 주소를 나타낸다.
long long var = (long long)&var + 4 // var변수의 첫번째 주소 + 4

위 변수를 컴파일 할 때, 컴파일러가 var의 첫번째 주소를 0x100000000부터 할당하였다고 가정하면

(변수를 메모리의 어떤 위치에 할당할지는 매번 컴파일 할때마다 다르며, 정확하게는 링킹시마다 다릅니다.)

변수의 값은 0x100000004이며 대부분의 OS에서는 주소가 낮은쪽부터 할당하기 때문에 

사람이 보기에 뒤집어서 값이 들어간 것처럼 보이기에 메모리는 아래 상태와 같을 것입니다.  

(메모리는 1바이트마다 주소를 가집니다.)

메모리 주소
(16진수) 
100000000 100000001 100000002 100000003 100000004 100000005 100000006 100000007

(16진수 )
40 00 00 00 10 00 00 00

 

#include<iostream>
using namespace std;


int main(){
    long long var = (long long)&var + 4;
    cout << hex;

    cout << &var << endl;   // var 주소 출력
    cout <<  var  << endl;  // var 값 출력

    cout <<  *(long long *)&var <<endl;
    cout << *(unsigned int *)&var  << endl;

    cout <<  (unsigned int *)var  << endl;
    cout << *(unsigned int *)var  << endl;
}


-----------------------------출력-----------------------------
# 편의상 모두 16진법으로 출력

var 주소 출력                                 : 0x0000000100000000 // 8바이트
var 값 출력                                  : 0x0000000100000004 // 8바이트

long long 타입 call by reference 출력         : 0x0000000100000004 // 8바이트
unsigned int 타입 call by reference 출력       : 0x00000004 // 4바이트

var 값 출력(포인터 타입)                        : 0x0000000100000004 // 8바이트
var 값을 call by reference 출력..             : 0x00000001 // 4바이트

line 9 : var 변수의 첫번째 주소

line 10 : var 변수 메모리안의 값

 

line 12 : (long long *) 타입으로 &var값을 call by reference

&var 값은 0x100000000이고, (long long *)는 총 8바이트를 읽게 하므로

0x100000000 ~ 0x100000007 영역의 데이터를 읽어옴  ==> 0x100000004 (위에 표로 계산할 수 있음, var 값이랑 그냥 같음)

 

line 13 : (unsigned int *) 타입으로 &var값을 call by reference

&var 값은 0x100000000이고, (unsigned int *)는 총 4바이트를 읽게 하므로

0x100000000 ~ 0x100000003 영역의 데이터를 읽어옴 ==> 0x4 (위에 표로 계산할 수 있음, 메모리 절반이 날라갔음)

 

line 15 : (unsigned int *) 타입으로 형변환한 var값을 출력 

형변환은 변환하는 타입의 메모리 크기만큼 첫주소부터 자를뿐 메모리 내부 값의 변화는 없다.

포인터 타입도 8바이트, long long 타입도 8바이트이기 때문에 값에 변화 없음

 

line 16 : (unsigned int *) 타입으로 var값을 call by reference

var 값은 0x100000004이고 (unsigned int *)는 총 4바이트를 읽게 하므로

0x100000004 ~ 0x100000007 영역의 데이터를 읽어옴 ==> 0x1 (위에 표로 계산할 수 있음, 메모리 절반이 날라갔음)

 

 

물론 위 예시는 이해를 돕기위해 억지로 만들어 내었고 실제로 저렇게 사용할 일은 없지요..

주소라면 정확하게 포인터 타입만 사용하는게 좋습니다.

댓글