본문 바로가기

Software

Secure Cordng C - 포인터의 개념과 이해 #1

개발환경 : V$ 2017, linux +gcc, mingw, eclipse

gcc 경로 설정 : C:\MinGW\bin

path의 환경변수 추가



1-1포인터의 개념과 이해


포인터쪽을 공부하다보면 다음과 같은 3개 정도의 단어를 접하게 된다.

1) call by reference
2) call by value
3) call by address

call by value나 call by reference는 알겠는데, call by address는 무엇인가? 의문을 갖게 된다.

우선  C언어는 Referece 라는 개념 자체가 없다. 따라서 call by reference는 C에서는 없는 개념이며,
call by reference는 C++에서 등장한 별명을 붙인다는 참조의 개념이다.

C언어는 call by value와 call by address가 있는데, 엄밀히 따져보면 call by address는 주소가 들어가는
포인터 변수를 만들어, 그 방안에 넘겨주고자 하는 원본의 주소값을 복사하는 call by value의 형태이다.

또한 call by address의 결과를 보면 주소를 참조하여 원본 데이터의 값을 바꾸는 것이 가능하다.
그래서 결론적으로 보면 call by reference 라고도 볼 수 있다.

하지만 call by address와 call by reference는 값을 변경했을 때 원본 데이터가 바뀌는 결과는 같지만,
이 둘 의 동작 매커니즘을 보면 구분지어 주는 것이 당연하다.

지금부터 call by value, call by address, call by reference의 특징들을 설명해보겠다.

우선 call by value는 말 그대로 값의 의한 전달이다.
그렇기 때문에 함수로 값을 전달하면, 그 값이 함수의 매개변수에 복사가 되며,
함수 내에서 값을 아무리 바꾸어도 원본의 값은 바뀌지 않는다.

call by address를 살펴보면
함수의 매개변수로 포인터형 변수를 선언(포인터는 4byte size의 공간이 메모리에 할당됨)하고,
그 포인터 변수의 공간에 원본의 주소값을 복사한다. 그 후 그 주소를 참조하여 가리키는 곳의
값을 변경하면 원본의 데이터가 변경된다.

call by reference의 경우
함수가 호출되는 시기에 매개변수인 레퍼런스 변수가 받아오는 변수의 별명으로서 초기화가 이루어진다.
즉 이 레퍼런스 변수는 메모리 공간을 할당 받는것이 아닌 원본 변수의 별명으로써 존재하는 것이다.
레퍼런스 변수는  그 변수 자체를 참조하여 값을 변경하는것이 가능하기 때문에 함수내에서 값을 변경
하여도 원본 변수의 값 역시 바뀌는 특징을 가진다.

call by address와 call by reference 두가지 모두 결과적으로는 원본 변수의 값을 변경한다는 특징을 가지고 있다.

하지만 포인터 변수는 주소값을 가지는 변수로서 메모리공간 4byte를 차지한다는 것, 레퍼런스 변수는 메모리 공간을
차지하지 않고 원본 변수의 별명으로 서 동작되어 진다는 것이다.

이를 확인해 볼 수 있는 예제로 sizeof() 함수를 사용해보면 명확히 알 수 있다.
아래의 예제 소스를 통해 확인해 보도록 하자.

void CallByAddress(char *add);
void CallByReference(char &ref);

int main(){

char buf;

CallByAddress(&buf);    // 주소값을 포인터변수에 복사
CallByReference(buf);    // 값을 전달하여 그 값에 레퍼런스 변수를 추가

}

void CallByAddress(char *add){
    cout << sizeof(add) << endl;    //  sizeof() 이용하여 add의 크기를 확인하면 포인터변수의 크기 4byte를 할당받은 것을 확인할 수 있다.
}

void CallByReference(char &ref){
    cout << sizeof(ref) << endl;    // ref의 크기는 char형 변수 1byte를 확인가능
}


위와 같이 call by reference와 call by address는 분명히 차이점이 존재하다. 따라서 해당 개념을 구분하여 인지하는 것이 중요하다.



1-2 포인터와 상수


1. 상수 지시 포인터(Pointer to Constant)

  • 상수 지시 포인터란 포인터가 가리키는 주소에 존재하는 값을 변경할 수 없는 포인터를 의미
  • 상수 지시 포인터를 선언하는 방법은 다음과 같으며, 아래의 선언은 모두 같은 의미임
void main()
{
int age = 10;

const int *pAge1 = &age;    // const 대상_객체의_타입 * 변수명
*pAge1 = 0;    // ERROR
pAge1 = 0;    // OK

int const *pAge2 = &age;    // 대상_객체의_타입 const * 변수명
*pAge1 = 0;    // ERROR
pAge1 = 0;    // OK
}


2. 상수 포인터(Const Pointer to)

  • 상수 포인터란 포인터 자체의 값을 변경할 수 없는 포인터를 의미
  • 상수 포인터를 선언하는 방법은 다음과 같다

void main()
{
int age = 10;
int * const pAge = &age;    // 대상_객체의_타입 * const 변수명;
*pAge = 0;    // OK
pAge = 0;    // ERROR
}


3. 상수를 가리키는 상수 포인터(Constant Pointer to Constant)

  • 상수를 가리키는 상수 포인터란 포인터가 가리키는 객체와 포인터 자체의 값을 변경할 수 없는 포인터를 의미
  • 상수를 가리키는 포인터의 선언은 다음과 같다
int main()
{
int age = 10;

const int * const pAge = &age;    // const 대상_객체의_타입 * const 변수명;
*pAge = 0;    // ERROR
pAge = 0;    // ERROR
}



4. const 키워드가 사용된 포인터의 해석

  • * 기호를 중심으로 const가 왼쪽에 있는 경우, 상수를 가리키는 포인터
  • * 기호를 중심으로 const가 오른쪽에 있는 경우, 상수 포인터


1-3 포인터의 타입


1. 포인터 타입

  • 포인터 선언 시, 반드시 포인터가 가리키는 객체의 타입과 동일하게 선언해야 함

2. 포인터 타입의 의미
  • 포인터 선언 시, 포인터가 가리키는 객체의 타입을 명시하는 이유는
    컴파일러에게 대생 객체의 크기(offset) 정보를 알려주기 위함
  • C 언어에서는 객체의 위치 또는 주소 만으로는 대상 객체의 크기를 알 수 있는 방법은 없다.


1-4 배열과 포인터


1. 배열(Array)

  • 배열이란 동일한 타입의 객체(원소 또는 요소)가 연속된 메모리공간을 의미
  • 배열을 선언하는 방법 : 원소의_타입 배열명[크기];
  • 배열의 각 원소 또는 요소에 접근할 수 있도록 0으로 시작되는 번호(첨자 또는 인덱스)가 할당되어 있음
  • 배열의 길이를 N이라고 했을 때, 마지막 원소의 첨자는 N-1
  • 단일 배열 길이를 사용하여 선언된 배열을 1차원 배열이라고 함


2. 배열의 유효 범위
  • 배열의 첨자 또는 인덱스는 0부터 시작하여 배열의 선언된 크기보다 하나 작은 값 까지만 사용할 수 있다.
  • C 언어에서는 배열의 범위를 강제하지 않으며, 배열의 범위를 넘어서는 행위는 미정의 동작이다.
#include <stdio.h>

void main()
{
int arr[5] = {0, };
int i;
for(i = 0; i < 6; i++)
printf("arr[%d] = %d\n", i, arr[i]);
}


3. 배열의 길이 계산

  • 배열에는 배열의 길이 정보가 없음
  • 배열의 길이를 계산하기 위해 일반적으로 다음과 같은 매크로 함수를 정의하여 사용

#include <stdio.h>
#define ARR_LENGTH(arr)    (sizeof((arr)) / sizeof(arr[0]))

void main()
{
    int arr[5] = {0, };
    int i;
    for(i = 0; i < ARR_LENGTH(arr); i++)
        printf("arr[%d] = %d\n", i, arr[i]);
}



4. 함수와 배열

  • 배열은 일반 변수와 마찬가지로 함수의 실제 인자로 전달될 수 있음
int sum_arr(int arr[])
{
int sum, i = 0;
for(i = 0; i < 10; i++)
sum += arr[i];

return sum;
}

void main()
{
int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
printf("%d\n", sum_arr(arr));    //55
}
  • 다음과 같이 사용할 수는 없음
void main()
{
int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int i;

int ar[] = arr;    // ERROR
for(i = 0; i < 10; i++)
printf("%d\n", ar[i]);
}


5. 배열을 사용한 함수 호출

  • C 언어에서 실제 인자를 사용한 호출은 모두 값에 의한 호출(call by value)로 동작하나
    배열은 예외적으로 '주소에 의한 호출(call by address)'로 동작함
  • 배열의 이름이 배열의 첫 번째 원소의 시작 주소로 해석됨
#include <stdio.h>

void init_int(int i /* = 10; */)
{
i = 0;
}

void init_arr(int arr[] /* = 0x12FF60 */)
{
arr[0] = 0;    // = *arr = 0;
}

void main()
{
int i = 10;
init_int(i);    //init_int(10)
printf("i = %d\n", i);

int arr[] = { 10 };
init_arr(arr);    //init_int(0x12FF60);
printf("arr[0] = %\n", arr[0])
}


6. 배열에 대한 포인터 선언

  • 배열에 대한 포인터를 선언하는 방법 : 첫번째_원소의_타입 * 변수명;
  • 배열의 이름이 곧 배열의 시작 주소를 의미하므로 주소 연산자를 사용하지 않음

#include <stdio.h>

void main()
{
    int arr[5] = { 1, 2, 3, 4, 5 };
    int i;
    for(i = 0; i < 5; i++)
        printf("arr[%d]= %d\n", i, arr[i]);
    printf("\n");
    
    int *pArr = arr;
    for(i = 0; i < 5; i++)
        printf("pArr[%d] = %d\n", i, pArr[i]);
}


반응형

'Software' 카테고리의 다른 글

Secure Cordng C - 포인터의 개념과 이해 #3  (0) 2018.07.18
Secure Cordng C - 포인터의 개념과 이해 #2  (0) 2018.07.18
DSP란 무엇인가?  (1) 2016.11.21
ROM BIOS란?  (0) 2016.05.18
명령어(Command)  (0) 2016.05.10