함수 포인터를 이용한 구현
일반적으로 C언어나 C++ 에서 사용하는 함수의 경우, 인자(매개변수) 혹은 파라미터로 변수를 받아갑니다. 이 값들을 가지고 정의된 기능을 수행하게 되죠. 하지만 프로그램을 만들다 보면 함수 자체를 다른 함수의 매개변수로 하는 방식이 유용할 때가 있습니다. 함수의 포인터를 다른 함수의 인자로 설정함으로써 이를 구현할 수 있으며, 이 때 함수의 파라미터 및 리턴값을 정확하게 명시해야 합니다.
Hello World! 코드를 약간 바꿔서 이를 사용해 봅시다.
#include<stdio.h>
void let_us_say(void (*func_message)());
void greetings();
void goodbye();
int main(int argc, char *argv[]) {
// function pointer
void (*ptr_func)();
fprintf(stdout, "\n");
// the pointer is now to greetings()
ptr_func = &greetings;
let_us_say(ptr_func);
// the pointer is now to goodbye()
ptr_func = &goodbye;
let_us_say(ptr_func);
return 0;
}
void let_us_say(void (*func_message)()) {
fprintf(stdout, "Let's say : ");
(*func_message)();
fprintf(stdout, "\n");
}
void greetings() {
fprintf(stdout, "Hello World!\n");
}
void goodbye() {
fprintf(stdout, "Bye Bye ~\n");
}
위의 프로그램에서 main 함수는 let_us_say 라는 함수를 호출하고 있는데요. 이 함수는 파라미터와 리턴값이 없는 함수를 매개변수로 받고 있습니다. main 함수 위에 위치한 프로토타입에 따르면 let_us_say 라는 함수 자체도 리턴값이 없죠. main 함수 바로 밑에는 let_us_say 함수에 대한 정의가 있습니다.
주의할 점은 func_message 는 함수의 포인터이므로, 실제 함수를 호출하기 위해서는 메모리 주소에 연동된 값을 불러오는 * 연산자를 앞에 붙여줘야 한다는 것입니다. 추가적으로 let_us_say 함수의 매개변수로 들어갈 greetings 와 goodbye 함수를 정의해 줍니다. 이 코드를 실행시켜 보면 다음과 같은 결과를 얻을 수 있습니다.
정적분이나 미분방정식 등의 수치적인 해를 구하는 프로그램 등에 함수의 포인터를 유용하게 활용할 수 있습니다. 구체적인 예시들은 다음 포스팅들에 소개되어 있습니다.
Runge-Kutta 방법을 이용하여 초기조건이 주어진 연립 상미분 방정식을 풀 수 있습니다.
반복-이완 계산법을 통해 경계조건이 주어진 연립 상미분 방정식을 풀 수 있습니다.
GSL 라이브러리를 이용한 정적분에도 함수의 포인터가 들어갑니다.
예시
급수와 오일러 상수
첫번째 예시로서, 급수 계산 프로그램을 소개할까 합니다. 수열의 각 항을 리턴하는 함수를 정의하고, 이 함수의 포인터를 인자로 받아서 유한급수를 계산하는 프로그램을 만들어볼텐데요. 여기서는 자연지수 함수의 밑이 되는 오일러 상수 (Euler's number)를 한번 계산해 보겠습니다. 자연지수 함수가 어떻게 정의되는지, 그리고 이로부터 오일러 상수를 어떻게 계산할 수 있는지에 대해서는 다음 포스팅에 소개되어 있습니다.
C언어 코드입니다.
#include<stdio.h>
#include<stdlib.h>
#include<math.h>
/* n_min 에서 n_max 까지의 유한급수를
* 계산하는 함수입니다.
* 더하고자 하는 수열의 각 항을 정의하는
* 함수의 포인터도 인자로 받고 있습니다. */
double sum_series(long int n_min,
long int n_max,
double (*ptr_term)(long int));
/* 오일러 상수를 구하기 위한 무한급수의
* 각 항을 정의하는 함수입니다. */
double get_term_exp(long int n);
// 메인 함수
int main(int argc, char *argv[]) {
/* 함수 포인터 변수를 선언하고
* 이 포인터는 get_term_exp 함수를
* 가리킵니다. */
double (*ptr_func)(long int);
ptr_func = &get_term_exp;
long int n_min = 0;
long int n_max = 20;
// 유한급수로 오일러 상수의 근사값 계산
double e_euler =
sum_series(n_min, n_max, ptr_func);
/* 오일러 상수의 근사값을 출력하고
* C 수학 함수 라이브러리의 값과 비교 */
fprintf(stdout,
"e from finite series\n");
fprintf(stdout,
" > e = %.16f\n", e_euler);
fprintf(stdout,
"e from C math library\n");
fprintf(stdout,
" > e = %.16f\n", M_E);
return 0;
}
double sum_series(long int n_min,
long int n_max,
double (*ptr_term)(long int)) {
double sum = 0.;
long int i;
for (i = n_min; i <= n_max; i++) {
sum += (*ptr_term)(i);
}
return sum;
}
double get_term_exp(long int n) {
if (n < 0) {
return 0.;
}
double term = 1.;
long int i;
for (i = 1; i <= n; i++) {
double factor_now = 1. / (double)i;
term = term * factor_now;
}
return term;
}
이 프로그램의 핵심 요소는 유한급수의 값을 계산하는 sum_series 함수입니다. 이 함수는 더하고자 하는 수열의 범위와 함께, 수열의 형태를 정의한 함수의 포인터를 매개변수로 받고 있습니다. 수열의 각 항을 정의하는 함수는 정수형 변수를 인자로 받아서 실수형 변수를 리턴하는 방식을 상정하고 있으므로, main 함수에서 함수 포인터 변수 ptr_func 를 선언할 때도 매개변수와 리턴값의 자료형들을 맞춰야 하겠죠.
여기서는 오일러 상수를 계산하고자 하기 때문에, 그에 해당하는 수열을 정의한 함수 get_term_exp 가 있습니다. 유한급수를 구하는 sum_series 함수가 get_term_exp 함수의 포인터를 인자로 받아가면, 오일러 상수의 근사값을 계산할 수 있게 되겠습니다. 프로그램을 실행하면 다음과 같은 결과를 볼 수 있습니다.
푸리에 변환
지금까지는 설명을 위해 간단한 코드를 예로 들었습니다만, 이제는 좀 더 실용적인 걸 해 봅시다. 저번 포스팅에서 고속 푸리에 변환 라이브러리인 FFTW에 대해 간략하게 소개를 했었습니다. 그리고 가우스함수의 푸리에 변환을 위한 예제 코드도 같이 첨부를 했죠.
위 포스팅에서는 모든 함수를 하나의 소스 파일 안에 넣고, 푸리에 변환을 main 함수에서 진행했었는데요. 이번에는 새로운 객체를 도입하고 변환대상이 되는 함수를 인자로 전달하는 방식으로 코드를 재구성해 볼 예정입니다. 이렇게 하면 사용하기에도 더 편리하고, 코드의 가독성도 높아지는 효과가 있으리라 기대해 봅니다.
FFTransformer.h [다운로드]
#ifndef FFTRANSFORMER_H
#define FFTRANSFORMER_H
#include<stdlib.h>
#include<string.h>
#include<math.h>
#include<vector>
#include<fftw3.h>
typedef struct PtrFuncFFT {
double (* re)(double, void *);
double (* im)(double, void *);
void *param;
} PtrFuncFFT;
class FFTransformer {
private :
int nbin_q_;
double q_min_;
double q_max_;
double delta_q_;
double *qbin_;
double **func_q_;
int nbin_x_;
double delta_x_;
double x_min_;
double x_max_;
double *xbin_;
double **func_x_;
bool initialized_;
bool direction_fft_;
/* the input array
* containing a function to be tranformed */
fftw_complex *array_fft_ini_;
/* the output array
* containing the outcome */
fftw_complex *array_fft_fin_;
// declare and initialize FFT transformer
fftw_plan fft_plan_;
public :
FFTransformer() {
initialized_ = false;
};
~FFTransformer() {
free_array();
};
void free_array() {
if (!initialized_) {
return;
}
// deallocate the FFTW stuffs
fftw_destroy_plan(fft_plan_);
fftw_free(array_fft_ini_);
fftw_free(array_fft_fin_);
for (int iq = 0; iq <= nbin_q_; iq++) {
delete [] func_q_[iq];
}
delete [] func_q_;
delete [] qbin_;
for (int ix = 0; ix <= nbin_x_; ix++) {
delete [] func_x_[ix];
}
delete [] func_x_;
delete [] xbin_;
initialized_ = false;
}
bool init(double q_min_in, double q_max_in,
double delta_q_in, bool direction_fft_in);
bool next(PtrFuncFFT ptr_func_in);
void get_array_ini(std::vector<double> &q_out,
std::vector<double> &func_re_out,
std::vector<double> &func_im_out);
void get_array_fin(std::vector<double> &x_out,
std::vector<double> &func_re_out,
std::vector<double> &func_im_out);
void write(const char *filename);
};
#endif
헤더 파일에서는 FFTransformer 클래스의 각종 변수와 함수들의 프로토타입이 선언되어 있습니다. 변환대상 및 변환된 함수들에 대한 정보를 저장하기 위한 배열 및 FFTW 실행을 위한 클래스 변수들을 포함하고 있죠. init 함수를 이용해서 함수들의 정의역과 격자 간격을 입력하고, 푸리에 변환이 정방향인지 역방향인지를 지정한다음 객체를 초기화할 수 있습니다.
함수와 파라미터의 포인터들로 이루어진 구조체 PtrFuncFFT 가 새롭게 정의되어 있는데요. 이는 복소수함수의 실수(re) 및 허수(im)부분의 포인터를 멤버로 가지고 있습니다. 또한 파라미터의 포인터 param 도 있는데, 함수의 특징을 결정짓는 각종 파라미터 변수들의 포인터입니다. 여기서 예시로 든 가우스 함수의 경우 평균값 및 폭을 구조체로 정의한 다음, 그 구조체의 포인터로 설정하는 게 가능합니다.
그다음으로 눈여겨봐야 할 것은 next 라는 멤버함수의 프로토타입입니다. 변환대상이 되는 복소수함수의 포인터 구조체 (PtrFuncFFT)를 매개변수로 받은 다음 푸리에 변환을 실행하는 함수죠. 마지막으로 결과물을 파일에 출력하기 위한 write 함수가 있습니다.
FFTransformer.cpp [다운로드]
#include"FFTransformer.h"
bool FFTransformer::init(double q_min_in, double q_max_in,
double delta_q_in, bool direction_fft_in) {
free_array();
q_min_ = q_min_in;
q_max_ = q_max_in;
delta_q_ = delta_q_in;
direction_fft_ = direction_fft_in;
nbin_q_ = (int)floor((q_max_ - q_min_) / delta_q_ + 1e-8);
if (nbin_q_ % 2 == 1) {
nbin_q_ += 1;
}
delta_q_ = (q_max_ - q_min_) / (double)nbin_q_;
qbin_ = new double [nbin_q_ + 1];
func_q_ = new double *[nbin_q_ + 1];
for (int iq = 0; iq <= nbin_q_; iq++) {
qbin_[iq] = q_min_ + delta_q_ * (double)iq;
func_q_[iq] = new double [2];
func_q_[iq][0] = 0.; // real part
func_q_[iq][1] = 0.; // imaginary part
}
nbin_x_ = nbin_q_;
/* lattice spacing in the x-space,
* which is depending on one in the q-space */
delta_x_ = 2. * M_PI / (delta_q_ * (double)nbin_q_);
x_min_ = -0.5 * delta_x_ * (double)nbin_x_;
x_max_ = 0.5 * delta_x_ * (double)nbin_x_;
xbin_ = new double [nbin_x_ + 1];
func_x_ = new double *[nbin_x_ + 1];
for (int ix = 0; ix <= nbin_x_; ix++) {
xbin_[ix] = x_min_ + delta_x_ * (double)ix;
func_x_[ix] = new double [2];
func_x_[ix][0] = 0.; // real part
func_x_[ix][1] = 0.; // imaginary part
}
array_fft_ini_ =
(fftw_complex *)fftw_malloc(nbin_q_ * sizeof(fftw_complex));
array_fft_fin_ =
(fftw_complex *)fftw_malloc(nbin_x_ * sizeof(fftw_complex));
if (direction_fft_) {
fft_plan_ =
fftw_plan_dft_1d(nbin_q_,
array_fft_ini_, array_fft_fin_,
FFTW_FORWARD, FFTW_MEASURE);
} else {
fft_plan_ =
fftw_plan_dft_1d(nbin_q_,
array_fft_ini_, array_fft_fin_,
FFTW_BACKWARD, FFTW_MEASURE);
}
initialized_ = true;
return true;
}
bool FFTransformer::next(PtrFuncFFT ptr_func_in) {
if (!initialized_) {
return false;
}
void *ptr_param = ptr_func_in.param;
for (int iq = 0; iq <= nbin_q_; iq++) {
// real part
func_q_[iq][0] =
(*ptr_func_in.re)(qbin_[iq], ptr_param);
// imaginary part
func_q_[iq][1] =
(*ptr_func_in.im)(qbin_[iq], ptr_param);
}
// define the input array
for (int iq = 0; iq < nbin_q_; iq++) {
double factor_in[2];
factor_in[0] = cos(x_min_ * delta_q_ * (double)iq);
if (direction_fft_) {
factor_in[1] = -sin(x_min_ * delta_q_ * (double)iq);
} else {
factor_in[1] = sin(x_min_ * delta_q_ * (double)iq);
}
array_fft_ini_[iq][0] =
func_q_[iq][0] * factor_in[0] -
func_q_[iq][1] * factor_in[1]; // real part
array_fft_ini_[iq][1] =
func_q_[iq][1] * factor_in[0] +
func_q_[iq][0] * factor_in[1]; // imaginary part
}
/* perform FFTW to take the input array
* and compute the output array */
fftw_execute(fft_plan_);
double factor_measure;
if (direction_fft_) {
factor_measure = delta_q_;
} else {
factor_measure = delta_q_ / (2. * M_PI);
}
// collect the reslut from FFTW arrays
for (int ix = 0; ix < nbin_x_; ix++) {
double factor_out[2];
factor_out[0] =
cos(q_min_ * xbin_[ix]) * factor_measure;
if (direction_fft_) {
factor_out[1] =
-sin(q_min_ * xbin_[ix]) * factor_measure;
} else {
factor_out[1] =
sin(q_min_ * xbin_[ix]) * factor_measure;
}
func_x_[ix][0] =
array_fft_fin_[ix][0] * factor_out[0] -
array_fft_fin_[ix][1] * factor_out[1]; // real part
func_x_[ix][1] =
array_fft_fin_[ix][1] * factor_out[0] +
array_fft_fin_[ix][0] * factor_out[1]; // imaginary part
}
func_x_[nbin_x_][0] = func_x_[0][0];
func_x_[nbin_x_][1] = func_x_[0][1];
return true;
}
void FFTransformer::get_array_ini(std::vector<double> &q_out,
std::vector<double> &func_re_out,
std::vector<double> &func_im_out) {
if (!initialized_) {
return;
}
q_out.clear();
func_re_out.clear();
func_im_out.clear();
for (int iq = 0; iq <= nbin_q_; iq++) {
q_out.push_back(qbin_[iq]);
func_re_out.push_back(func_q_[iq][0]);
func_im_out.push_back(func_q_[iq][1]);
}
}
void FFTransformer::get_array_fin(std::vector<double> &x_out,
std::vector<double> &func_re_out,
std::vector<double> &func_im_out) {
if (!initialized_) {
return;
}
x_out.clear();
func_re_out.clear();
func_im_out.clear();
for (int ix = 0; ix <= nbin_x_; ix++) {
x_out.push_back(xbin_[ix]);
func_re_out.push_back(func_x_[ix][0]);
func_im_out.push_back(func_x_[ix][1]);
}
}
void FFTransformer::write(const char *filename) {
if (!initialized_) {
return;
}
FILE *fout;
fout = fopen(filename, "w");
fprintf(fout,
"# q phi(q).re phi(q).im x psi(x).re psi(x).im\n");
for (int iq = 0; iq <= nbin_q_; iq++) {
int ix = iq;
fprintf(fout, "%e %e %e %e %e %e\n",
qbin_[iq], func_q_[iq][0], func_q_[iq][1],
xbin_[ix], func_x_[ix][0], func_x_[ix][1]);
}
fclose(fout);
}
이 소스 파일에서는 FFTransformer 클래스의 멤버 함수들을 정의하고 있습니다. 수학적으로는 이전 포스팅에 나온 것과 다르지 않습니다만, FFTransformer::next 함수에서는 푸리에 변환의 대상이 되는 함수의 포인터 구조체 (PtrFuncFFT)를 매개변수로 받아서 func_q_ 배열의 값을 정하게 되죠. 이걸 가지고 FFTW를 실행한 뒤에, 변환된 함수에 대한 정보를 func_x_ 배열에 저장합니다.
한 가지 더 짚고 넘어갈 점이라면, 변환대상이 되는 복소수함수가 정의역에 해당하는 double 형 변수값 뿐 아니라 파라미터를 위한 void 형 포인터 역시 인자로 받고 있다는 점입니다. 여기에는 PtrFuncFFT 구조체의 멤버 포인터 param 을 가져다 넣는 방식을 취하고 있습니다.
이렇게 푸리에 변환을 한 뒤에는 write 함수를 호출해서 파일에 데이터 값들을 기록할 수 있습니다.
test2_fftw.cpp [다운로드]
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<math.h>
#include"FFTransformer.h"
typedef struct InfoGauss {
// mean and width of a Gaussian function
double mean;
double sigma;
} InfoGauss;
double func_gaussian(double x, double mean_x, double sigma_x);
double func_fft_re(double q, void *ptr_param);
double func_fft_im(double q, void *ptr_param);
int main(int argc, char *argv[]) {
// Determine the actual values for mean and width.
double mean_q = 1.;
double sigma_q = 0.5;
// range in the q-space
double q_min = mean_q - 10. * sigma_q;
double q_max = mean_q + 10. * sigma_q;
// lattice spacing in the q-space
double delta_q = 0.02;
FFTransformer fft_transform;
// Initialize the Fourier transformation class
bool fft_init = fft_transform.init(q_min, q_max, delta_q, false);
if (!fft_init) {
fprintf(stderr,
"ERROR : FFTransformer is not properly initialized.\n");
exit(1);
}
InfoGauss info_gauss;
info_gauss.mean = mean_q;
info_gauss.sigma = sigma_q;
// function pointer
PtrFuncFFT ptr_func_in;
ptr_func_in.re = &func_fft_re; // real part
ptr_func_in.im = &func_fft_im; // imaginary part
ptr_func_in.param = &info_gauss; // parameter
// Perform fast Fourier transformation
bool fft_next = fft_transform.next(ptr_func_in);
if (!fft_next) {
fprintf(stderr,
"ERROR : FFTransformer failed to perform.\n");
exit(1);
}
char filename_output[200];
if (argc > 1) {
strcpy(filename_output, argv[1]);
} else {
strcpy(filename_output, "func2_gaussian_fft.dat");
}
fft_transform.write(filename_output);
return 0;
}
double func_gaussian(double x, double mean_x, double sigma_x) {
double dx_scaled = (x - mean_x) / sigma_x;
double func =
exp(-0.5 * fabs(dx_scaled * dx_scaled)) /
(sigma_x * sqrt(2. * M_PI));
return func;
}
// We are doing Fourier transform of a real Gaussian function.
double func_fft_re(double q, void *ptr_param) {
InfoGauss *ptr_info = (InfoGauss *)ptr_param;
return func_gaussian(q, ptr_info->mean, ptr_info->sigma);
}
// So the imaginary part would be zero.
double func_fft_im(double q, void *ptr_param) {
return 0.;
}
마지막으로 main 함수가 포함된 소스 파일입니다. 여기서 할 일은 변환대상이 되는 복소수함수의 실수와 허수부분을 정의하고 이를 FFTranformer 객체에 넘겨주는 것인데요. 이 함수들은 각각 func_fft_re, func_fft_im 이라는 이름으로 정의되어 있습니다. 이들은 void 형 포인터를 두번째 인자로 받고 있는데, 여기서는 가우스 함수의 평균과 폭을 정의한 InfoGauss 라는 구조체의 포인터를 넘겨주고 있습니다. 물론 이때 InfoGauss 형 포인터에서 void 형 포인터로의 형 변환이 필요하겠죠.
마지막으로 푸리에 변환 객체에 들어갈 PtrFuncFFT 구조체의 멤버 포인터 re 와 im 은 각각 func_fft_re 및 func_fft_im 함수들의 포인터로 설정됩니다. 파라미터를 위한 포인터 param 역시 이번에는 InfoGauss 구조체를 가리키게 됩니다. 이전 포스팅과 마찬가지로 실수부분은 가우스함수, 허수부분은 0인 함수를 가지고 푸리에 변환을 해보면 동일한 결과를 얻을 수 있습니다. 그리고 이 main 함수는 출력할 파일의 이름을 명령행 인자로 받고 있습니다.
리눅스나 유닉스 환경에서 실행하는 경우 먼저 소스파일들을 컴파일해야 합니다. GNU C++ 컴파일러를 사용할 경우 다음과 같이 할 수 있습니다.
g++ FFTransformer.cpp -c
g++ test2_fftw.cpp -c
그리고 오브젝트 파일들을 FFTW 라이브러리와 함께 링크해서 실행파일을 만듭니다.
g++ test2_fftw.o FFTransformer.o -lfftw3 -lm -o test2_fftw.exec
이렇게 하면 test2_fftw.exec 라는 실행파일이 생성될텐데요. 출력되는 파일의 이름을 명령행 인자로 지정해 줄수 있습니다. 예를들어 func_output.dat 라는 이름으로 저장하고 싶다면 다음과 같이 실행하면 되겠습니다.
./test2_fftw.exec func_output.dat
위 예시에서 푸리에 변환을 수행하기 위한 FFTransformer 클래스를 정의할 때, new 및 delete 키워드들이 사용되고 있는 것을 보셨을 것입니다. 이들을 이용하면 필요할 때 필요한 만큼만 메모리 공간을 할당받아서 사용할 수 있기 때문에, 효율성이 좋아지게 되죠. 이 동적 메모리 관리에 대한 자세한 사항이 궁금하시다면, 다음 포스팅이 큰 도움이 되리라 생각합니다.
추가로 main 함수가 argc 및 argv라는 매개변수를 받고 있고, 이들이 출력파일의 이름을 지정하는데 사용된다는 걸 볼 수 있는데요. 이들을 명령행 인자라고 부르며, 프로그램을 실행할 때 커맨드라인에서 그 값을 지정해 줄 수 있습니다. 더 자세한 사항에 대해서는 다음 포스팅을 참고하면 좋습니다.
같이 알고 있으면 좋은 C/C++ 팁들
라이브러리 제작
포트란과의 하이브리드
C언어 동적 메모리 관리