C언어

[C언어 #2][포인터 #1]

Sim 2024. 3. 11. 02:24
반응형

 

· 목차

 - 포인터의 기본 개념

    1. 포인터의 선언

    2. 포인터 변수와 &연산자

    3. 포인터 타입

    4. 잘못된 포인터의 사용과 널 포인터

 - 포인터와 배열

    1. 배열 그리고 포인터의 연산

    2. Call by value, Call by reference


 

 

 

포인터를 알기 위해서 변수와 메모리 주소에 대해서 먼저 이해가 필요할 것 같습니다.

 

int main(void) {
	char ch1 = 'A', ch2 = 'Q';
    int num = 7;
}

 

변수를 할당하게 되면 임의로 정한 주소 값인 '메모리의 주소값'이 1바이트의 메모리 공간을 단위로 할당됩니다.

위의 코드를 예시로 하자면 A : 1바이트, Q : 1바이트, 7 : 4바이트인 총 6바이트가 할당되게 됩니다.

 

 

int형인 7의 주소를 물어본다면 전체의 주소에 대해서 이야기할 것입니다.

하지만 C언어는 시작주소만을 알려준다는 것을 알고 있어야 합니다. 계산은 간단하실 것입니다.

주소는 시작주소로부터 하나씩 증가해서 총 4바이트의 크기만큼을 가늠할 수 있기 때문입니다.

 

결국 앞서 말한 것과 같이 변수를 할당하게 되면 정수형 주소를 갖게 되는데, 

정수 형태의 주소 값을 저장하는 목적으로 선언되는 것이 '포인터 변수'입니다.

포인터 변수의 크기는 시스템의 주소 값 크기에 따라서 다릅니다.

 

16비트 시스템 → 주소 값 크기 16비트 → 포인터 변수의 크기 16비트

32비트 시스템 → 주소 값 크기 32비트 → 포인터 변수의 크기 32비트

 

 

 

포인터의 선언

 

 

 

int *ptr; // ptr는 포인터로 선언된 것이며 integer값을 리턴합니다.
char *cptr; // cptr는 포인터로 선언된 것이며 character값을 리턴합니다.

 

 

포인터의 선언은 위와 같으며, 포인터의 타입은 포인터가 가리키는 변수의 타입과 일치해야 합니다.

포인터는 변수의 주소를 할당하여 초기화되게 되는데, 이는 주소연산자(&)를 통해서 가능합니다.

 

int main () {
	int num1 = 5;
    double * pnum1 = &num1; // 타입이 일치하지 않음
    
    double num2 = 5;
    int * pnum2 = &num2; // 타입이 일치하지 않음
    
}

 

 

 

포인터 변수와 &연산자

 

 

 

&와 *의 연사자를 포인터 연산자라고 합니다. 

 

  • 주소연산자(&)

 

 

 

주소연산자(&)는 피연산자의 주소 값을 반환하는 연산자입니다.

int main () {
int num = 5;
int * pnum = # // num의 주소 값을 반환해서 포인터 변수 pnum을 초기화
}

 

포인터에서 변수의 자료형에 맞지 않는 포인터 변수의 선언은 할 수 없습니다.

컴파일러에러는 발생하지 않지만 경고메시지는 발생하지만 포인터 관련 *연산 시 문제가 발생합니다.

 

 

 

  • 메모리 참조 연산자 *

 

 

 

*연산자는 포인터가 가리키는 메모리 공간에 접근할 때 사용하는 연산자입니다.

int main () {
	int num 10; // 포인터 변수 pnum이 변수 num을 가리키게 하는 문장
    int *pnum = # // pnum이 가리키는 변수에 20을 저장
	printf("%d", *pnum); // pnum이 가리키는 변수를 부호 있는 정수로 출력
}​

 

위의 코드 블록을 보면 pnum이 가리키는 변수에 20을 저장하는 것을 보실 수 있습니다.

*pnum이 의미하는 메모리에 접근을 해서 20을 저장하는 것입니다.

 

위에서 지금까지 포인터 변수라고 말했던 것을 아실 수 있습니다. 그렇다면 참조의 대상이 바뀔 수 있지 않을까요?

참조의 대상을 변경할 수 있을지에 대해서 이야기하기 전에 변수와 상수에 대해서 간단하게 설명해 보자면

변수는 변할 수 있는 값, 상수는 고정되어 변할 수 없는 값이라고 생각하시면 더 편하실 것이라고 생각됩니다.

 

 

본론으로 돌아와서 "참조하는 대상을 변경할 수 있는가?"에 대해서 답을 하자면 변경할 수 있습니다.

 

int main () {
	int x = 10;
    int y = 10;
    int *ptr;
    
    ptr = &x; // x를 참조
    ptr = &y; // y를 참조
}

 

 

 

포인터 타입

 

 

 

주소연사자에서 포인트의 타입이 다르면 안 된다고 했던 것을 보실 수 있습니다.

예를 들어서, *ptr의 값을 반환한다고 생각을 해면 각 타입에 따라서 주소로부터 몇 바이트를

읽어 들여야 하는지 그리고 실수 또는 정수중에 어느 것으로 해석해야 하는지에 대해서 알아야 합니다.

 

위에 대한 예시로, int타입이라면 지정된 주소로 부터 4바이트를 읽어 들이고, 정수로 해석해야 합니다.

결국 포인터의 타입은 메모리 공간을 참조하는 기준이 되기 때문에 *연산자가 메모리 공간에 어떻게

접근해야 하지는지에 대한 기준을 마련해 주고 후에 출력이 의미 없는 값이 되지 않도록 하게 됩니다.

 

 

 

잘못된 포인터의 사용과 널 포인터

 

 

 

// Case1

int main () {
	int *ptr;
    *ptr = 10;
}

int main () {
	int * ptr = 125;
    *ptr = 10;
}

 

 

Case1의 경우, 각각 10 또는 125가 어디인지도 모르는 상태로 초기화한 것과 같은 것입니다.

 

 

// Case2

int main () {
	int * ptr = NULL;
}

 

Case2는 '널 포인터'에 대한 것입니다. 널 포인터는 의도적으로 아무 곳도 가르키지 않는 상태로 만든 것 입니다.

포인터 변수를 우선 선언해 놓은 이후에 유효한 주소 값을 채워 넣을 예정이라면 널 포인터를 사용해서 초기화해놓는 것이 좋습니다.

 


 

 

포인터와 배열은 밀접한 관계를 가지고 있습니다. 일단 기본적인 키워드를 보자면,

배열과 포인터 산술, 포인터를 통해 배열 요소에 액세스 하기, 배열 매개변수 감소,

고정된 관계(고정 기본 주소), 메모리 할당 등에 대한 키워드를 나열할 수 있습니다.

 

지금은 아직 기본만 배운 상태이기 때문에 후에 차근차근 알아보도록 하기로 하면서

기본적인 개념을 위주로 우선 공부해 보도록 하겠습니다.

 

배열의 이름도 포인터입니다. 하지만 배열의 이름은 값을 바꿀 수 없는 상수 형태의 포인터 입니다.

int형 배열요소간 주소값의 차이

 

또한 int형 배열요소 간 주소값의 차이는 4바이트입니다. 그렇다면 double은 8 바이트겠죠?

 

C언어에서는 이런 이유들로 인해서 배열의 이름을 '상수 형태의 포인터'라고도 부릅니다.

배열 또한 *연산자를 배열의 이름과 함께 사용할 수 있습니다.

 

 

 

배열 그리고 포인터의 연산

 

 

 

int main () {
	int arr[5] = {1, 2, 3, 4, 5};
    int *p = arr; // int *p = &arr[0]과 같습니다.
    printf("%d \n", *p) // 1이 나옵니다.
}


int main () {
	int arr1[3] = {1, 2, 3};
    printf("%d \n", *arr1);
    *arr1 += 100;
    printf(%d \n, arr1[0]);
    return 0;
}
// 결과 값으로 1, 101이 나옵니다.

 

 

위처럼의 예시를 통해서 알 수 있듯이, 둘 다 포인터이기 때문에 포인터 변수로 할 수 있는 연산은

배열의 이름으로도 할 수 있고, 배열의 이름으로 할 수 있는 연산은 포인터 변수로도 할 수 있습니다.

 

int main () {
	int arr[3] = {1, 2, 3};
    int *ptr = &arr[0]; // int *ptr = arr;과 동일
    printf("%d %d \n", *ptr, *arr);
	printf("%d %d \n", ptr[0], arr[0]);
    printf("%d %d \n", ptr[1], arr[1]);
    
    return 0;
}
// 결과값
// 1, 1
// 1, 1
// 2, 2

 

포인터를 대상으로 *연산 이외에 증가 및 감소연산도 가능합니다.

 

int main() {
    int arr[] = {10, 20, 30, 40, 50};
    int *ptr = arr; // ptr은 arr의 첫 번째 원소를 가리킵니다.

    printf("현재 가리키는 값: %d\n", *ptr); // 10 출력

    ptr++; // 포인터를 증가시켜 다음 원소를 가리킵니다.
    printf("증가 연산 후 가리키는 값: %d\n", *ptr); // 20 출력

    ptr--; // 포인터를 감소시켜 이전 원소를 가리킵니다.
    printf("감소 연산 후 가리키는 값: %d\n", *ptr); // 다시 10 출력

    return 0;
}


int main () {
    int c[4] = {1, 2, 3, 4}; char d[4] = {1,2,3,4};
    int *a; char * b;
    a = &c[2]; b = &d[1];

    printf("%lu %lu \n", sizeof(c), sizeof(d));
    printf("%d %d %s %hhd\n", *a, *a, b, *b);
    printf("%d %d %s %hhd\n", *(a + 1), *(a + 1), b + 1, *(b + 1));
} 

// 결과값
// 16 4
// 3 3 2
// 4 4 3

 

 

 

포인터의 증가연산에 따른 이동을 그려보면 아래와 같이 이해할 수 있습니다.

 

 

여기까지 살펴보면 배열과 포인터는 배열의 이름과 포인터 변수는 상수냐 변수냐의 차이만 있는 것입니다.

위의 코드 예시를 보면 c와 포인터 a의 주소값이 같기 때문에 사실상 모두 같은 것입니다.

 

이제 마지막으로 Call by Value와 Call by reference에 대해서 배우고 끝내도록 하겠습니다.

 

 

 

Call by value, Call by reference

 

 

 

  • Call by value

Call by value는 별도의 메모리 위치에서 인수 값을 받습니다. 함수 내의 매개변수에 대한

변경사항은 함수 호출에 사용되는 인수에 영향을 주지 않습니다.

//Case1

Void increment(int x) {
	x = x + 1;
    printf("Increment: \n", x);
}

int main () {

	int a = 5;
    increment(a);
    printf("In main: \n", a);
    
    return 0;
}

// 결과값
// Increment: 6
// In main: 5



// Case2

void CallByValue(int *ptr) {
    int b = 10;
    ptr = &b;
}

int main() {
    int a = 5;
    int *ptr = &a;
	CallByValue(ptr);
}

// 여기서 포인터는 여전히 a를 가르키고 있습니다.

 

위의 예로 살펴보자면 increment 내부의 'x'를 변경해도 'main'의 'a'는 변경되지 않습니다.

그 이유는 해당 함수가 'a'의 복사본으로 작동하기 때문입니다.

 

 

  • Call by reference

함수가 실제 값이 아닌 인수에 대한 참조(또는 주소)를 이용한다는 의미입니다.

쉽게 말해서 모두 동일한 메모리 위치를 가리키기 때문에 값이 변경되면 인수에 영향을 줍니다.

 

// Case1

void increment(int *x) {
    *x = *x + 1;
    printf("Increment: %d\n", *x);
}

int main() {
    int a = 5;
    increment(&a);
    printf("In main: %d\n", a);
    return 0;
}

// 결과값
// Increment: 6
// In main: 6



// Case2

void CallByReference(int **ptr) {
    int b = 10;
    *ptr = &b;
}

int main() {
    int a = 5;
    int *ptr = &a;
	CallByReference(&ptr);
}

// ptr은 b를 가르킵니다.

 

 

 

포인터가 가리키는 주소를 실제로 변경하려면 포인터 자체의 주소를 전달해야 합니다.

 

 


 

 

여기까지 포인터에 대한 기본을 배웠습니다. 아직 배우지 못한 포인터에 대한 내용이 더 있습니다.

한 번에 너무 많은 내용을, 그것도 어려운 내용이라면 더욱 배우기 힘든 것 같습니다.

오늘은 여기까지 하고 저 또한 더 공부해서 다시 포인터에 대해서 글을 작성해 보도록 하겠습니다.

 

C언어를 배우다 보면 포인터에 의해서 많은 사람들이 배우기를 포기한다고 들었던 것 같습니다.

솔직히 말해서 저 또한 정리하고 공부했지만 누가 물어보면 아직까지는 바로 답변할 자신이 없네요.

포인터의 경우 한번 깊게 공부하고 머릿속에 각인을 시켜놓으면 이보다 쉬운 건 없다고 하니

저 같은 감자는 그 말을 믿고 우직히 걸어가 보려고 합니다.

 

감사합니다.

 

 

반응형

'C언어' 카테고리의 다른 글

[C언어 #1][기본 자료형]  (0) 2024.03.08