Home C언어 (14) 함수와 포인터
Post
Cancel

C언어 (14) 함수와 포인터

C언어 (14) 함수와 포인터

여태까지 배운 것을 합쳐서 함수에서 어떻게 포인터를 활용하는지 알아보자.

call by value vs call by reference

값에 의한 호출(call by value)은 변수의 값을 복사해서 함수를 호출하는 방식이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
int func(int n);
int main() {
    int a = 10;
    int result = 0;
    result = func(a);
    printf("%d\n", a);
    printf("%d", result);
    return 0;
}
int func(int n) {
    n += 1;
    return n;
}

이 코드의 흐름을 보면, 값이 10인 a로 func(int n) 함수를 호출한다.
int n에는 a의 값인 10이 복사되고, 변수 n은 a와는 다른 메모리 공간을 가지고 10인 값을 가진다.
그 다음 n는 1 증가하고, n의 값이 반환되어 result에 들어간다.
그렇기에 a는 10, result는 11의 값을 가진다. 이렇게 값을 이용해 함수를 호출하는 것이다.

그렇다면 주소에 의한 호출(call by reference)은 어떻게 함수를 호출할까?
포인터를 이용해 주소를 참조해서 함수를 호출한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
int func(int* p);
int main() {
    int a = 10;
    int result = 0;
    result = func(&a);
    printf("%d\n", a);
    printf("%d", result);
    return 0;
}
int func(int* p) {
    *p = *p + 1;
    return *p;
}

이전과 비슷하게 생긴 코드지만 결과는 11, 11이 출력된다.
흐름을 보면 a의 주소를 통해 func를 호출하고, int* p에는 a의 주소가 들어간다.
그러면 *p는 a의 메모리 공간을 의미하고, 거기에 1을 더하니 a에 1을 더한 것과 같다.
이렇게 포인터 변수를 통해 다른 함수에서 지역 변수 a에 저장한 값을 참조하거나 변경할 수 있다.

매개 변수로 배열 사용하기

주소를 매개 변수로 함수를 호출할 수 있다는 것은 배열도 매개체가 될 수 있다는 것이다.
int ar[100]같은 큰 데이터를 함수로 보내야 하는데 값을 전부 보내려면 시간과 메모리의 소비가 클 것이다.
하지만 배열의 주소로 함수를 호출하면 쉽고 빠르게 값들을 참조할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
void func(int* p);
int main() {
    int ar[10] = { 1,2,3,4,5,6,7,8,9,10 };
    func(ar);
    return 0;
}
void func(int* p) {
    for (int i = 0; i < 10; i++) {
        printf("%d ", p[i]);
    }
}

이렇게 int *p에 ar배열의 주소를 넣고 반복문을 통해서 배열의 값을 바로 참조할 수 있다.
함수의 인자로는 int* p가 들어가있는 것을 확인할 수 있다.

마찬가지로 이차원 배열을 매개 변수로 보내려면 어떻게 해야 할까?
배열 포인터를 사용하면 똑같이 할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
void func(int (*p)[4]);
int main() {
    int ar[2][4] = { 
        {1,2,3,4},
        {5,6,7,8}
    };
    func(ar);
    return 0;
}
void func(int(*p)[4]) {
    for (int i = 0; i < 2; i++) {
        for (int j = 0; j < 4; j++) {
            printf("%d ", p[i][j]);
        }printf("\n");
    }
}

포인터를 사용하면 대량의 데이터를 보내야할 때 값을 전부 복사할 필요 없이 주소로만 쉽게 보낼 수 있다!

주소를 반환하는 함수

그렇다면 대량의 데이터를 반환해야 한다면 어떻게 해야 할까?
이번에는 반환하는 자료형을 가지는 부분에 포인터형이 들어갈 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
int* func(void);
int main() {
    int* p;
    p = func();
    for (int i = 0; i < 5; i++) {
        printf("%d ", p[i]);
    }
    return 0;
}
int* func(void) {
    int arr[5] = { 1,2,3,4,5 };
    return arr;
}

이렇게 func에서 생성된 배열의 주소를 return해서 main의 *p로 보낸다.
아무런 문제 없이 배열을 반환한 것처럼 보이고, 결과도 잘 나올 것이다.
하지만 이 코드에는 문제가 있는데 func의 int arr[5]는 func() 지역에 선언된 배열이라는 것이다.
func() 함수가 return하면 지역 변수(배열) arr[5]의 메모리 공간도 같이 소멸된다.
그러면 int *p에 있는 arr의 주소는 유효하지 않은 메모리를 가리킬 수도 있는 것이다.

이 문제점을 잘 해결할 수 있는 방법을 저번 글에서 봤다. 바로 정적 변수이다.
정적 변수는 함수가 종료된 후에도 메모리 공간이 소멸되지 않기에 위의 문제를 해결할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
int* func(void);
int main() {
    int* p;
    p = func();
    for (int i = 0; i < 5; i++) {
        printf("%d ", p[i]);
    }
    return 0;
}
int* func(void) {
    static int arr[5] = { 1,2,3,4,5 };
    return arr;
}

방법도 간단하게 배열 앞에 static을 붙이면 된다. 그러면 안전하게 배열의 주소를 반환할 수 있다.

함수 포인터

image

이 그림을 보면 함수도 code 영역이라는 메모리에 저장된다는 것을 볼 수 있다.
함수를 작성해서 컴파일하면 함수의 정의는 기계어로 변환되고, 링크 과정을 거쳐 실행 파일의 일부가 된다.
함수를 실행하기 위해서는 함수가 있는 영역의 주소를 알아야하고 함수의 시작 주소값 역시 있다.

배열의 이름이 배열의 시작 주소였던 것처럼 함수의 이름도 함수의 시작 주소이다.
그러면 함수의 주소를 저장하는 포인터 변수도 만들 수 있을 것이고 그것을 함수 포인터라고 부른다.

1
2
3
4
5
6
#include <stdio.h>
int main() {
    printf("%p\n", main);
    return 0;
}

이렇게 main 함수의 주소를 출력해볼 수 있다.

함수의 포인터는 저장할 함수의 원형과 거의 비슷하게 생겼다.

1
int (*pointer) (int, int)

반환 자료형, *(함수 포인터 이름), 매개 변수로 이루어져 있다. 예제로 확인해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
void add(int n, int m) {
    int result;
    result = n + m;
    printf("%d", result);
}
int main() {
    int x = 3, y = 4;
    void (*pointer)(int, int);
    pointer = add;
    printf("add의 주소 %p\n", add);
    printf("add의 주소 %p\n", &add);
    printf("포인터의 주소 %p\n", pointer);

    pointer(x, y);
    return 0;
}
1
2
3
4
add의 주소 00007FF7B34C1070
add의 주소 00007FF7B34C1070
포인터의 주소 00007FF7B34C1070
7

이렇게 add와 &add, pointer가 같은 주소를 가리킨다는 것을 볼 수 있고, 이 주소는 add 함수의 주소다.
add와 pointer는 같은 주소를 가리키기 때문에 pointer(x,y)는 add(x,y)와 같고 x+y의 결과가 출력된다.
당연하지만 함수 포인터도 포인터 변수이기에 32bit면 4바이트, 64bit면 8바이트의 크기를 가진다.

이 함수 포인터를 어떻게 유용하게 쓸 수 있을까?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
void add(int n, int m);
void minus(int n, int m);
void multiple(int n, int m);
void divide(int n, int m);

int main() {
    int choice;
    int x, y;
    void (*pointer)(int, int);
    while (1) {
        printf("계산할 번호를 입력하세요:\n");
        printf("1. Add\n");
        printf("2. Minus\n");
        printf("3. Multiply\n");
        printf("4. Divide\n");
        printf("Enter choice: ");
        scanf("%d", &choice);
        printf("계산할 두 수를 입력하세요 : ");
        scanf("%d%d", &x, &y);

        switch (choice) {
        case 1:
            pointer = add;
            break;
        case 2:
            pointer = minus;
            break;
        case 3:
            pointer = multiple;
            break;
        case 4:
            pointer = divide;
            break;
        default:
            continue;
        }
        pointer(x, y);
    }

    return 0;
}
void add(int n, int m) {
    printf("%d + %d = %d\n", n, m, n + m);
}
void minus(int n, int m) {
    printf("%d - %d = %d\n", n, m, n - m);
}
void multiple(int n, int m) {
    printf("%d * %d = %d\n", n, m, n * m);
}
void divide(int n, int m) {
    if (m != 0) {
        printf("%d / %d = %.2f\n", n, m, (double)n / m);
    }
    else {
        printf("0으로 나눌 수 없습니다.\n");
    }
}

이렇게 선택에 따라 계산하는 프로그램을 만들어 볼 수 있다.
입력에 따라 pointer에 다른 함수의 주소를 넣어서 계산하는 것이다.
또한 함수 포인터에도 배열을 만들 수 있는데 위의 코드를 더 명료하게 나타낼 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
void add(int n, int m);
void minus(int n, int m);
void multiple(int n, int m);
void divide(int n, int m);

int main() {
    int x, y;
    void (*operations[4])(int, int) = { add, minus, multiple, divide };
    int choice;

    while (1) {
        printf("계산할 번호를 입력하세요:\n");
        printf("1. Add\n");
        printf("2. Minus\n");
        printf("3. Multiply\n");
        printf("4. Divide\n");
        printf("5. Exit\n");
        printf("Enter choice: ");
        scanf("%d", &choice);
        printf("계산할 두 수를 입력하세요 : ");
        scanf("%d%d", &x, &y);;

        if (choice == 5) {
            break;
        }
        if (choice >= 1 && choice <= 4) {
            operations[choice - 1](x, y);
        }
    }
    return 0;
}
void add(int n, int m) {
    printf("%d + %d = %d\n", n, m, n + m);
}
void minus(int n, int m) {
    printf("%d - %d = %d\n", n, m, n - m);
}
void multiple(int n, int m) {
    printf("%d * %d = %d\n", n, m, n * m);
}
void divide(int n, int m) {
    if (m != 0) {
        printf("%d / %d = %.2f\n", n, m, (double)n / m);
    }
    else {
        printf("0으로 나눌 수 없습니다.\n");
    }
}
1
void (*operations[4])(int, int) = { add, minus, multiple, divide };

위 코드는 *operations[4]배열에 add, minus, multiple, divide 함수의 주소를 저장한 것이다.
그 다음 사용자의 선택에 따라 함수를 실행할 수 있다. 굉장히 깔끔한 코드라고 생각한다.

이 글은 함수 포인터를 정리하며 생긴 궁금증을 찾아보고 알아봤다. 궁금하면 한번 봐보는 것도..
함수는 어떻게 저장되는가?


이제 C언어의 기본 기능은 정말 끝낸 것 같고, 구조체, 문자열, 파일 입출력, 동적 할당 정도가 남았다.
하나씩 잘 알아가보자.

This post is written by PRO.