
함수 포인터(Function Pointer)는 함수의 주소를 저장하는 포인터 변수이다.
일반적인 포인터가 변수의 메모리 주소를 저장하는 것처럼
함수 포인터는 함수가 저장된 메모리 주소를 저장하고 해당 함수를 가리킨다.
즉 함수 포인터를 사용하면 함수를 변수처럼 저장하고 호출할 수 있다.
함수 포인터는 다음과 같은 형태로 선언한다.
int (*fp)(int, int);
위 선언의 의미는 다음과 같다.
fp → 두 개의 int 값을 받아
int 값을 반환하는 함수를 가리키는 포인터
#include <stdio.h>
int add(int a, int b) { return a + b; }
int main(void)
{
int (*fp)(int, int);
fp = add;
printf("%d\n", fp(3, 4));
return 0;
}
실행 흐름은 다음과 같으며, 함수의 주소를 통해 함수를 호출하는 구조를 갖는다.
add 함수가 메모리에 저장됨
↓
fp가 add 함수의 주소 저장
↓
fp() 호출 시 add 함수 실행
일반적으로 함수는 직접 호출하면 되기 때문에 처음에는 함수 포인터가 왜 필요한지 이해하기 어려울 수 있다.
하지만 함수 포인터는 프로그램의 유연성과 확장성을 제공하는 중요한 개념이기에, 사용 목적을 알아보자.
구조체(struct)는 서로 다른 데이터 타입의 변수들을 하나의 묶음으로 관리하기 위한 사용자 정의 자료형이다.
기본 데이터 타입은 다음과 같이 하나의 값만 저장할 수 있다.
int → 정수
double → 실수
char → 문자
하지만 실제 프로그램에서는 서로 다른 성격의 데이터를 하나의 단위로 묶어 관리해야 하는 경우가 많다.
예를 들어 학생 정보를 생각해보면 다음과 같은 데이터가 필요하다.
학번 → 정수
이름 → 문자열
평균점수 → 실수
이러한 데이터를 하나의 단위로 묶기 위해 구조체를 사용한다.
구조체는 다음과 같이 정의한다.
struct Student
{
int id;
char name[20];
double score;
};
위 구조체는 다음과 같은 데이터 구조를 만든다.
Student
├ id
├ name
└ score
즉 여러 개의 서로 다른 타입의 데이터를 하나의 데이터 구조로 묶는 역할을 한다.
구조체를 선언한 후 다음과 같이 사용할 수 있다.
struct Student s1;
또는 아래와 같이 선언과 동시에 변수 생성도 가능하다.
struct Student
{
int id;
char name[20];
double score;
} s1;
구조체 변수는 각 멤버 변수들이 메모리 상에 순서대로 저장되는 구조를 가진다.
예를 들어 다음 구조체가 있다고 가정해보자.
struct Data
{
char a;
int b;
double c;
};
메모리 구조는 다음과 같이 구성된다.
a → 1byte
b → 4byte
c → 8byte
즉 구조체는 여러 개의 서로 다른 타입 데이터를 하나의 메모리 블록으로 묶어 관리하는 자료구조이다.
구조체는 다음과 같은 상황에서 사용된다.
예를 들어 상품 정보, 회원 정보와 같은 데이터들을 하나로 묶을 수 있다.
구조체는 현실 세계의 객체 정보를 프로그램 안에서 표현하기 위한 데이터 구조라고 볼 수 있다.
공용체(union)는 여러 개의 서로 다른 타입의 변수가 하나의 메모리 공간을 공유하는 사용자 정의 자료형이다.
구조체(struct)가 각 멤버 변수마다 개별적인 메모리 공간을 할당하는 것과 달리,
공용체는 모든 멤버가 하나의 메모리 공간을 함께 사용한다.
공용체는 union 키워드를 사용하여 선언한다.
union unTemp
{
char a;
int b;
double c;
};
구조체와 마찬가지로 다음과 같이 변수도 선언할 수 있다.
union unTemp un;
또는 선언과 동시에 변수 생성도 가능하다.
union unTemp
{
char a;
int b;
double c;
} un;
구조체와 공용체의 가장 큰 차이는 메모리 할당 방식이다.
공용체는 모든 멤버가 하나의 메모리 공간을 공유한다.
union unTemp
{
char a;
int b;
double c;
};
메모리 구조
a → 1byte
b → 4byte
c → 8byte
공용체는 가장 큰 데이터 타입의 크기만큼 메모리를 할당한다.
구조체는 멤버 변수마다 개별적인 메모리 공간이 할당된다.
struct stTemp
{
char a;
int b;
double c;
};
메모리 구조
a → 1byte
b → 4byte
c → 8byte
#include <stdio.h>
union Data
{
int i;
float f;
char c;
};
int main(void)
{
union Data data;
data.i = 10;
printf("i: %d\n", data.i);
data.f = 3.14;
printf("f: %f\n", data.f);
return 0;
}
여기서 중요한 점은 새로운 값을 저장하면 이전 값이 덮어써진다는 것이다.
공용체는 다음과 같은 상황에서 사용된다.
|:--|:--|
|구조체 (struct)|공용체 (union)|
|목적|데이터 묶기|메모리 공유|
|멤버 메모리 할당|멤버마다 메모리 할당|모든 멤버가 메모리 공유|
|값 저장|여러 값 동시에 저장 가능|한 번에 하나의 값만 의미|
|메모리 사용량|메모리 사용량 큼|메모리 사용량 작음
일반적인 변수 선언이나 배열 선언은 컴파일 타임에 메모리 크기가 결정된다.
예를 들어 다음 코드를 살펴보자.
int a;
double b;
또는 배열을 선언할 경우
int student[10];
프로그램이 실행되기 전에 메모리 크기가 미리 결정된다.
하지만 실제 프로그램에서는 데이터의 크기를 미리 알 수 없는 경우가 많다.
예를 들어 학교의 학생 수를 10크기의 배열로 저장했다고 가정해보자.
만약 학생 수가 늘어나면 아래와 같이 배열 크기를 변경해야 한다.
int student[100];
하지만 실제 상황에서는 학생 수가 얼마나 될지 미리 알 수 없다.
이렇듯 프로그램 실행 전에 메모리 크기를 확정하기 어려운 경우가 존재한다.
이러한 문제를 해결하기 위해 동적 메모리 할당(Dynamic Memory Allocation) 을 사용한다.
동적 메모리 할당은 프로그램 실행 중(Runtime)에 필요한 만큼의 메모리를 할당하는 방식이다.
C 언어에서는 malloc() 함수를 사용하여 메모리를 동적으로 할당한다.
함수 원형은 다음과 같다.
void* malloc(size_t size);
size: 할당할 메모리 크기(Byte 단위) 를 지정한다.malloc(4);
// 4byte 메모리 할당반환값: malloc() 함수는 할당된 메모리의 시작 주소를 반환한다.int *ptr = malloc(sizeof(int));
// ptr → 할당된 메모리 주소 저장만약 메모리를 할당할 수 없는 경우 NULL 포인터가 반환된다.
따라서 실제 프로그램에서는 NULL 체크가 필요하다.
malloc() 함수의 반환 타입은 void* 이다.
void*는 타입이 지정되지 않은 포인터를 의미한다.
"필요한 메모리를 할당해 줄 테니 어떤 타입으로 사용할지는 개발자가 결정하세요" 라는 의미이다.
따라서 실제 사용할 때는 원하는 타입의 포인터로 형 변환하여 사용한다.
int *ptr = (int*)malloc(sizeof(int));
다음 코드는 사용자로부터 학생 수를 입력 받아 배열을 생성하는 예시이다.
#include <stdio.h>
#include <stdlib.h>
int main()
{
int num;
printf("학생 수를 입력하세요 : ");
scanf("%d", &num);
int *student = (int*)malloc(sizeof(int) * num);
if(student == NULL)
{
printf("메모리 할당 실패\n");
return 1;
}
free(student);
return 0;
}
위 코드의 실행 흐름은 다음과 같다.
사용자 입력
↓
필요한 배열 크기 계산
↓
malloc으로 메모리 할당
↓
포인터에 주소 저장
동적으로 할당한 메모리는 반드시 free() 함수를 사용하여 해제해야 한다.
이 작업을 수행하지 않으면 메모리 누수(Memory Leak) 가 발생한다.
메모리 누수란 사용하지 않는 메모리가 계속 점유된 상태로 남아있는 현상을 의미한다.