[C] 제네릭 프로그래밍 / 함수형 포인터, void 포인터

fluideun·2022년 7월 18일
0

C

목록 보기
1/1

제네릭 프로그래밍

제네릭 프로그래밍(generic programming)이란, 데이터 형식에 의존하지 않고, 하나의 값이 여러 다른 데이터 타입들을 가질 수 있도록 하는 프로그래밍 방식이다.

즉, 프로그래머가 어떤 자료형을 사용할 지 미리 알 수 없기 때문에 어떤 자료형이든 상관없이 실행할 수 있도록 프로그래밍한 것이다.

이 글에서는 C언어로 제네릭 프로그래밍을 직접 해 볼 것이지만, 이해하기 쉽도록 미리 예시를 들어보자면 C++ STL 중 vector를 생성할 때 vector<int>, vector<double>< > 안에 원하는 자료형을 적어 사용하는 것을 볼 수 있다.

그리고 C++의 템플릿은 제네릭 프로그래밍으로 되어있다.

함수형 포인터

제네릭 프로그래밍을 설명하기 전에, 함수형 포인터를 먼저 설명하겠다.
함수형 포인터란, 말 그대로 함수를 포인터로 사용하는 것인데 예시를 먼저 보자.

#include<stdio.h>
int maxv(int a, int b) {
	return a > b ? a : b;
}

int main() {
	int(* c)(int, int) = maxv;	// 함수형 포인터
	printf("%d", c(5,3));
	return 0;
}

먼저, 큰 값을 반환하는 maxv 함수를 만들었다.
그리고 int(* c)(int, int)라고 쓴 부분이 함수형 포인터를 만든 것이다.

반환값 자료형(ex. int), 함수형 포인터 이름(ex. c), 괄호 안에는 매개변수 자료형(ex. (int, int))을 순서대로 써주면 된다.
그리고 코드에 쓴 것처럼 int(* c)(int, int) = maxv;라고 쓰면 함수형 포인터 c에는 함수 maxv의 주소가 들어가는 것이다.

그 후에는 함수형 포인터 c를 함수처럼 사용할 수 있다.
(ex. c(5, 3)maxv(5, 3)은 같다.)

void 포인터

void 포인터에 대해서도 간단하게 설명하겠다.
제네릭 프로그래밍을 하면서 void*는 계속 쓰이기 때문에 꼭 알아야 한다.

먼저 포인터는 주소값을 저장해준다.
int*를 쓰면 저장된 시작 주소로부터 4byte의 데이터(int형 데이터)를 읽어낼 것이고,
double*를 쓰면 저장된 시작 주소로부터 8byte의 데이터(double형 데이터)를 읽어낼 것이다.

그런데 우리는 저장하려는 주소에 int형 데이터가 들어갈지 double형 데이터가 들어갈지 모르기 때문에 임시로 void*를 쓴다.

void*는 주소가 가리키는 데이터의 자료형은 모르겠지만 일단 주소만 저장되어있다.
그렇기 때문에 사용할 때 사용하려는 데이터의 자료형을 가리키는 포인터로 바꿔야한다.

제네릭 프로그래밍 예제

그러면, 제네릭 프로그래밍으로 배열에 원하는 숫자가 있는지 찾아서 위치를 반환하는 함수 linear_search를 만들어보겠다.

이 예제는 배열에 1~100의 숫자가 들어가 있고 (자료형은 찾는 값에 따라 정해짐),
찾는 값이 int, double 등 숫자 자료형으로 이루어져 있지만 자료형이 무엇인지 정해져 있지 않기 때문에 제네릭 프로그래밍을 해야한다.

linear_search 함수 코드

#include<stdio.h>
#include<math.h>
#include<stdlib.h>
int linear_search(void* _arr, int size, int esize, void* _key) {
	char* arr = (char*)_arr;
	char* key = (char*)_key;
	for (int i = 0; i < size; i++) {
		int cnt = 0;
		for (int j = 0; j < esize; j++) {	// 자료형 크기만큼 반복하며 숫자 하나를 비교
			if (*(arr + (esize * i) + j) == *(key + j)) {
				cnt++;
			}
		}
		if (cnt == esize) {
			return i;
		}
	}
	return -1;
}

linear_search의 매개변수는 자료형이 void*인 배열, 자료형이 int인 배열의 크기, 자료형이 int인 찾는 값 key의 자료형 크기, 자료형이 void*인 찾는 값으로 이루어져 있다.
배열과 찾는 값의 자료형이 void* 인 이유는 위에서도 말했듯이 자료형에 상관없이 찾을 수 있도록 하기 위해서이다.

그 다음 줄을 보면 배열과 찾는 값을 char*로 바꿔주는데, 그 이유는 1bytechar*어떤 자료형이든 비교할 수 있기 때문이다.

자세히 설명하자면, 위에서 void 포인터는 사용할 때 사용하려는 데이터의 자료형을 가리키는 포인터로 바꿔야한다고 했는데
우리는 자료형을 모르기 때문에 사용하려는 데이터의 자료형으로 바꾸는 코드를 함수 안에 작성할 수 없다.

따라서 byte를 하나씩 비교하며 찾아줄 것인데, char*은 1byte의 데이터를 가리키기 때문에
int형은 1byte를 4번, double형은 1byte를 8번 반복하여 같은지 비교할 수 있다.

그 다음 줄에 나오는 for문이 비교하는 과정을 나타낸다.
배열을 차례대로 돌면서 비교하다가 찾는 값과 같은 값을 발견하면 인덱스를 반환한다.
찾지 못하면 -1을 반환한다.

main 코드 (double형 데이터 찾기)

int main() {
	void* ptr = malloc(sizeof(double) * 100);	// 동적할당 (double형 배열)
	double* a = ptr;	// double형 배열
	for (int i = 0; i < 100; i++) {		// 배열 초기화
		a[i] = i;
	}
	for (int i = 0; i < 100; i++) {		// 배열 출력
		printf("%f\n", *(a+i));
	}
	double key = 24;	// 찾는 값
	int ans = linear_search(a, 100, sizeof(double), &key);
	printf("%d", ans);
	return 0;
}

linear_search 함수의 매개변수 자료형을 보면 원래 void*를 넣어야 하는 자리에 그냥 double*를 넣은 것을 볼 수 있다.
void*에 넣을 때는 굳이 void*로 바꿔주지 않고 넣어도 된다. 하지만 헷갈린다면 형변환을 해줘도 좋다.

문제점

int형 데이터를 찾을 때는 문제가 없을 수 있지만, double형 데이터를 찾을 때 배열을 초기화하는 과정을 다르게 하면 부동소수점으로 인해 찾지 못하는 경우가 생긴다.

main 코드 배열 초기화 수정

int main() {
	void* ptr = malloc(sizeof(double) * 100);	// 동적할당 (double형 배열)
	double* a = ptr;	// double형 배열
	for (int i = 0; i < 100; i++) {		// 배열 초기화 다른 방법
		a[i] = sqrt(i) * sqrt(i);
	}
	for (int i = 0; i < 100; i++) {		// 배열 출력
		printf("%f\n", *(a+i));
	}
	double key = 24;	// 찾는 값
	int ans = linear_search(a, 100, sizeof(double), &key);
	printf("%d", ans);
	return 0;
}

배열 초기화를 할 때 넣으려는 값 i루트를 씌우고, 다시 제곱을 해줬다.
이렇게 초기화를 하고 배열을 출력해 보면 값이 똑같이 출력되는 것을 볼 수 있다.

하지만 컴퓨터는 찾는 값 24를 찾지 못한다. 그 이유는 우리가 byte를 하나씩 비교해가며 같은 값인지 찾았기 때문이다.

사람이 직접 계산하기로는 루트를 씌우고 다시 제곱을 하는 것은 루트가 그냥 없어진다고 생각할 수 있다. 하지만 컴퓨터는 루트를 계산하고, 계산한 값을 다시 제곱한다.
24\sqrt{24}는 무리수이다. 그렇기 때문에 컴퓨터는 정확한 값을 다 적어낼 수가 없다. 정확하지 않은 값을 제곱하여 나타내니 아주 작은 오차가 생기게 된다.

배열을 출력할 때는 아주 작은 오차가 보이지 않았지만, byte를 비교하니까 다른 점이 생긴 것이다.

문제점 해결

문제점을 해결하기 위해 int형일 때와 double형일 때의 숫자 비교 방법을 나누어 함수를 만들어 줄 것이다.
그리고 그 함수를 linear_search 매개변수에 넣어줄 것인데, 이 때 함수형 포인터가 사용이 된다.

새로운 linear_search 함수 코드

#include<stdio.h>
#include<stdlib.h>
#include<math.h>
int int_compare(const void* a, const void* b) {
	int* aa = (int*)a;
	int* bb = (int*)b;
	return (* bb > *aa) ? -1 : (*aa > *bb);
}
int double_compare(const void* a, const void* b) {
	double* aa = (double*)a;
	double* bb = (double*)b;
	if (fabs(*bb - *aa) < 0.0001) {
		return 0;
	}
	if (*aa > *bb) {
		return 1;
	}
	else {
		return -1;
	}
}
int linear_search(void* _arr, int size, int esize, void* _key, int (*compare_function)(const void*, const void*)) {
	char* arr = (char*)_arr;
	char* key = (char*)_key;
	for (int i = 0; i < size; i++) {
		if (compare_function(arr + esize * i, key) == 0) {
			return i;
		}
	}
	return -1;
}

int_compare 함수와 double_compare 함수를 설명하기 전에, 새로운 linear_search 함수를 먼저 설명하겠다.
위에 있던 linear_search 함수와 달라진 점은
매개변수에 int (*compare_function)(const void*, const void*)가 추가된 것이다.
이 코드에 함수형 포인터와 void 포인터가 모두 쓰였다.

함수형 포인터를 쓴 이유는 int_compare 함수와 double_compare 함수 중 어떤 함수를 사용할 지 모르기 때문이고, 매개변수에 void 포인터를 쓴 이유는 마찬가지로 자료형이 int형인지 double형인지 모르기 때문이다.

그리고 숫자를 비교하는 부분을 간단하게 함수로 나타내주었다.
두 숫자를 비교했을 때 같은 숫자가 나오면 0을 출력하도록 함수를 만들 것이다.

이제 int_compare 함수와 double_compare 함수를 설명하겠다.
원래는 자료형을 모르기 때문에 byte를 하나하나 비교했다면, int_compare 함수와 double_compare 함수를 사용하게 되면서 int형인지 double형인지 함수는 알게 된다.
그렇기 때문에 int_compare 함수에서 void*로 받은 매개변수를 int*형변환하여 숫자를 바로 비교한다.

double_compare 함수는 다르다. 위에서 설명했던 문제점 때문에 우리는 아주 작은 오차는 무시하고 비교하도록 코드를 짜야 한다.
마찬가지로 먼저 void*로 받은 매개변수를 double*로 형변환하고, 두 숫자의 차의 절댓값을 계산한다. 이 차이가 자신이 설정한 아주 작은 오차보다 더 작게 나온다면 같은 숫자라고 판단하는 것이다.

main 코드 (int형 데이터 찾기)

int main() {
	void* ptr = malloc(sizeof(int) * 100);	// 동적할당 (int형 배열)
	int* a = ptr;	// int형 배열
	for (int i = 0; i < 100; i++) {		// 배열 초기화
		a[i] = (int)(sqrt(i) * sqrt(i) + 0.5);
	}
	for (int i = 0; i < 100; i++) {		// 배열 출력
		printf("%d\n", *(a+i));
	}
	int key = 24;	// 찾는 값
	int ans = linear_search(a, 100, sizeof(int), &key, int_compare);
	printf("%d", ans);
	return 0;
}

main 코드 (double형 데이터 찾기)

int main() {
	void* ptr = malloc(sizeof(double) * 100);	// 동적할당 (double형 배열)
	double* a = ptr;	// double형 배열
	for (int i = 0; i < 100; i++) {		// 배열 초기화
		a[i] = sqrt(i) * sqrt(i);
	}
	for (int i = 0; i < 100; i++) {		// 배열 출력
		printf("%f\n", *(a+i));
	}
	double key = 24;	// 찾는 값
	int ans = linear_search(a, 100, sizeof(double), &key, double_compare);
	printf("%d", ans);
	return 0;
}

만약 int형 데이터를 찾으려면 배열 초기화를 바꾸는 것이 좋다.
double형에서의 문제점을 찾기 위해 일부러 루트를 씌우고, 제곱을 했기 때문에 int형으로 바꾸기 위해 번거롭게 반올림을 해주었다.
double형에서의 문제점을 초점으로 한 코드이기 때문에 double형 코드를 중점으로 보길 바란다.

배열 초기화를 잘 하면 int형이든 double형이든 원하는 값의 인덱스를 잘 찾아낼 것이다.

0개의 댓글