본문 바로가기

Studying/Computer Programs

C언어와 C++를 조합한 프로그래밍

프로그램을 만들다 보면 C언어로 작성된 함수를 C++ 프로그램에서 사용하거나, 그 반대의 경우가 생길 때가 있습니다. 뿐만 아니라 C언어로 만들어진 라이브러리를 C++ 프로그램에서도 사용할 수 있게 설계해 두면 상당히 유용하겠죠. extern "C" 코드블록 및 __cplusplus 식별자를 이용해서 언어의 장벽을 뛰어넘을 수 있습니다.

 

반응형

 

extern "C" 코드블록

C언어와 C++ 사이의 호환성을 확보하기 위한 방법 중의 하나는 앞에 언급한 대로 extern "C" 코드블록을 사용하는 것입니다. 함수의 본체가 C언어로 정의되어 있는 경우, 그 프로토타입을 C++ 소스코드 내에서 선언하기 위해서는 extern "C" 코드블록이 필요하죠. 이러한 조치를 취하지 않으면 오브젝트 파일들을 링크할 때 C++ 컴파일러는 C언어로 작성된 함수를 인지하지 못합니다.

C++ 에서는 함수의 오버로딩, 즉 동일한 이름의 함수가 서로 다른 갯수와 종류의 인자를 받는 것이 가능한데요. 이를 위해서 소스와 헤더 파일 내에서 같은 이름을 가진 함수라도, 오브젝트 파일 및 링크 단계에서는 서로 다른 이름을 가지게 되는 name mangling 이 일어납니다. C언어에는 이러한 기능이 없기 때문에, 링크 단계에서 함수의 이름이 맞지 않는 상황이 발생하는 것이죠. extern "C" 코드블록은 C++ 컴파일러가 name mangling 을 수행하지 않도록 함으로써, 함수가 정의된 C언어 소스코드와 그 프로토타입을 포함한 C++ 소스코드 사이에 매칭이 이루어지게 합니다.

 

__cplusplus 를 이용한 C/C++ 범용 헤더파일

C언어로 작성된 라이브러리 다수가 C++ 프로그램에서도 사용이 가능한데요. 이는 프로그램을 무슨 언어로 작성하느냐에 따라 헤더파일이 소스 파일에 포함되는 방식이 다르기 때문에 가능합니다. 대표적인 예시로 GSL 라이브러리가 있으며, 별도의 포스팅에 간략하게 소개되어 있습니다.

 

 

GSL : C/C++ 수치해석 라이브러리

라이브러리 소개 이번 포스팅에서는 이공계 분야의 수치해석에서 널리 쓰이는 GSL라이브러리에 대해 써 볼까 합니다. GSL은 GNU Scientific Library의 약자로서, 과학 및 공학 분야의 수치해석에 유용한

swstar.tistory.com

 

GSL 라이브러리의 헤더 파일 중 하나인 gsl_sys.h 를 살펴보면, 다음과 같은 구조를 가지고 있습니다.

 

#ifndef __GSL_SYS_H__
#define __GSL_SYS_H__

#undef __BEGIN_DECLS
#undef __END_DECLS
#ifdef __cplusplus
# define __BEGIN_DECLS extern "C" {
# define __END_DECLS }
#else
# define __BEGIN_DECLS /* empty */
# define __END_DECLS /* empty */
#endif

__BEGIN_DECLS

// ... 헤더파일의 내용 ...

__END_DECLS

#endif /* __GSL_SYS_H__ */

 

맨 위의 두 줄 및 맨 아랫줄의 전처리문들은 식별자 __GSL_SYS_H__ 에 관한 것으로서, 헤더파일이 중복으로 포함되는 것을 방지하는 역할을 합니다. 그 다음으로 C언어와 C++ 간의 호환성과 관련해서 중요한 식별자인 __BEGIN_DECLS__END_DECLS 가 등장하는데요.

 

  1. 먼저 #undef 전처리기를 통해 정의된 값들을 해제합니다.
  2. 그 다음 __cplusplus 식별자가 정의되어 있는지를 체크하는 부분이 있는데, 이는 헤더 파일을 포함한 게 C++ 소스파일인지 아니면 C언어 소스파일인지를 확인하는 과정이라 할 수 있죠.
  3. 만일 이 헤더파일이 C++ 소스코드에 포함되어 있다면, __cplusplus 식별자가 정의되고 __BEGIN_DECLS 및 __END_DECLS 는 각각 extern "C" { 및 } 로 대체됩니다. 반면에 C언어 소스코드에서 사용된다면, 주석 처리된 /* empty */ 문구가 들어가겠죠.

 

결과적으로 C++ 소스코드에 포함되어 컴파일이 되는 경우, 전체 헤더파일의 내용이 extern "C" 코드블록에 들어가게 됩니다. 이러한 장치 덕분에, GSL 라이브러리 자체는 C언어로 작성되었지만 C++ 프로그램에서도 문제없이 사용이 가능합니다. 필요한 헤더 파일들을 포함시키고, 라이브러리를 링크하면 C언어 프로그램에서 사용할 때와 비슷한 방식으로 쓸 수 있는 것이죠.

 

출처 : stackoverflow

 

Combining C++ and C - how does #ifdef __cplusplus work?

I'm working on a project that has a lot of legacy C code. We've started writing in C++, with the intent to eventually convert the legacy code, as well. I'm a little confused about how the C and C++

stackoverflow.com

 

여기에 소개된 방법을 활용한다면 C언어 라이브러리를 직접 제작하고, 이를 C언어 및 C++ 프로그램에서 활용하는 것도 가능합니다. C언어 라이브러리를 만드는 방법은 다음 포스팅에 소개되어 있습니다.

 

 

C언어 라이브러리 만들기

자주 쓰는 함수들을 라이브러리 형태로 만들어 두면, 프로그램을 짤 때 편리합니다. 예를 들어서 함수 Function이 mylib.c 라는 소스코드에 정의되어 있고, 프로토타입이 mylib.h 라는 헤더 파일에 선

swstar.tistory.com

 

C 프로그램에서 C++ 소스코드 사용하기

위에서는 C언어로 정의된 함수들을 C/C++ 범용 헤더 파일을 통해 C++ 프로그램에서 사용하는 법을 다루었습니다. 이번에는 반대로 C++로 작성된 소스코드들을 C언어 main 프로그램에서 사용하는 법을 알아봅시다. 이 때는 C++의 클래스 등의 개념을 C언어 프로그램에 바로 적용할 수가 없으므로, 경우의 수와 해결 방법도 다양합니다. 이 포스팅에서는 로지스틱 방정식을 수치적으로 푸는 프로그램으로 이를 구현한 예시를 보여줄까 합니다.

 

 

C/C++ Runge-Kutta 방법으로 알아보는 인구역학

목차 로지스틱 방정식 확장된 가설 : 어른과 어린이 확장된 가설 : 인간과 삼림 인터넷을 돌아다니다가 우연히 인구역학 (population dynamics) 및 이를 위한 수학적 모형에 대한 설명을 위키 페이지에

swstar.tistory.com

 

위 포스팅에 제시된 소스코드에는 초기조건이 주어진 상미분 방정식을 수치적으로 풀기위한 ODESolveRK 클래스가 도입되어 있는데요. 여기서는 객체를 그대로 사용하면서도 main 함수를 C언어로 작성하는 식으로 코드를 재구성해 보겠습니다. ODESolveRK 클래스의 헤더 및 소스 파일들은 위 포스팅과 동일합니다.

ODESolveRK.h [다운로드]

ODESolveRK.cpp [다운로드]

main 함수를 C언어로 작성하면 이 클래스를 그대로 사용할 수가 없죠. 그래서 앞에 언급된 범용 헤더파일과 추가적인 C++ 소스파일을 작성하여 이를 중재해 줄 필요가 있습니다.

 

wrap1_population_RK4.h [다운로드]

 

더보기
#ifndef WRAP1_POPULATION_RK_H
#define WRAP1_POPULATION_RK_H

#undef __WRAP_CXX_INI
#undef __WRAP_CXX_FIN
#ifdef __cplusplus
  #define __WRAP_CXX_INI extern "C" {
  #define __WRAP_CXX_FIN } 
#else
  #define __WRAP_CXX_INI /* empty */
  #define __WRAP_CXX_FIN /* empty */
#endif

__WRAP_CXX_INI

extern int initialized_oderk_;
extern int evolved_oderk_;

extern int nbin_t_;
extern double t_ini_;
extern double t_fin_;
extern double delta_t_;

extern double n_ini_;

void setup(double (*ptr_in_func_derivative)(double, double *));

void evolve();

void write(char *filename_prefix, char *filename_extend);

void finalize();

/* analytic solution
 * to the logistic equation */
double func_sol_logistic(double t);

__WRAP_CXX_FIN

#endif

 

main 함수가 정의된 C언어 소스파일 및 중개를 위한 C++ 소스파일 모두에 포함되는 헤더파일입니다. 앞서 언급한대로 extern "C" 코드블록 및 __cplusplus 식별자를 활용한 덕분에 C언어와 C++ 소스파일 모두에서 사용가능하죠. 초기조건과 Runge-Kutta 방법에서의 시간간격 등이 전역변수로 선언되어 있습니다. 추가로 다음 함수들의 프로토타입들이 선언되어 있는데요.

 

  • setup
    수치적인 해를 저장하기 위한 배열과 ODESolveRK 객체를 초기화하는 함수입니다. ODESolveRK 클래스의 멤버 함수 중 하나인 init 을 호출합니다.
  • evolve
    로지스틱 방정식의 수치적인 해를 구하고 이를 배열에 저장하는 기능을 가진 함수입니다. ODESolveRK 클래스의 멤버 함수인 next_RK4get_system_current 를 호출합니다.
  • write
    수치적인 해를 해석적인 해와 함께 파일에 출력하는 함수입니다. 출력파일의 이름과 확장자를 매개변수로 받고 있습니다.
  • finalize
    main 함수에서 맨 마지막에 호출되는 함수로서, 동적으로 할당된 메모리들을 해제하는 역할을 합니다.
  • func_sol_logistic
    로지스틱 방정식의 해석적인 해입니다.

 

이제 이 함수들을 별도의 C++ 소스파일에서 구체적으로 정의해 줘야 합니다.

 

wrap1_population_RK4.cpp [다운로드]

 

더보기
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<math.h>
#include<vector>
#include"wrap1_population_RK4.h"
#include"ODESolveRK.h"

int initialized_oderk_;
int evolved_oderk_;

int nbin_t_;
double t_ini_;
double t_fin_;
double delta_t_;

double n_ini_;

/* arrays to store the evolution history
 * of population */
double *t_history;
std::vector<double> *n_history;
/* array of function pointer for time derivatives
 * of population */
func_oderk_deriv *ptr_in_func;

ODESolveRK *ptr_oderk_solver_;

void setup(double (*ptr_in_func_derivative)(double, double *)) {
    ptr_in_func =
        (func_oderk_deriv *)malloc(sizeof(func_oderk_deriv) * 1);
    ptr_in_func[0] = ptr_in_func_derivative;

    nbin_t_ = (int)floor(fabs(t_fin_ - t_ini_) / delta_t_ + 1e-8);
    t_history = new double [nbin_t_ + 1];
    n_history = new std::vector<double> [nbin_t_ + 1];

    t_history[0] = t_ini_;

    n_history[0].clear();
    n_history[0].push_back(n_ini_);

    ptr_oderk_solver_ = new ODESolveRK();
    // initialize the ODE Runge-Kutta solver
    bool init_oderk =
        ptr_oderk_solver_->init(1,
            t_history[0], n_history[0], ptr_in_func);
    if (init_oderk) {
        initialized_oderk_ = 1;
    } else {
        initialized_oderk_ = 0;
    }

    evolved_oderk_ = 0;
}

void evolve() {
    if (initialized_oderk_ == 0) {
        return;
    }

    for (int it = 1; it <= nbin_t_; it++) {
        bool go_next = ptr_oderk_solver_->next_RK4(delta_t_);
        if (!go_next) {
            break;
        }

        ptr_oderk_solver_->get_system_current(t_history[it], n_history[it]);
        fprintf(stderr, "  %e  %e  %e\n", t_history[it],
            n_history[it].at(0), func_sol_logistic(t_history[it]));
    }

    evolved_oderk_ = 1;
}

void write(char *filename_prefix, char *filename_extend) {
    if (initialized_oderk_ == 0 || evolved_oderk_ == 0) {
        return;
    }

    char buffer[10];
    sprintf(buffer, "%f", n_ini_);

    FILE *fout;
    char filename_out[200];
    strcpy(filename_out, filename_prefix);
    strcat(filename_out, "_n0_");
    strcat(filename_out, buffer);
    strcat(filename_out, ".");
    strcat(filename_out, filename_extend);
    fout = fopen(filename_out, "w");
    fprintf(fout, "# n0/K = %e\n", n_ini_);
    fprintf(fout, "# [Gamma t]  [N/K numerical]  [N/K analytic]\n");

    for (int it = 0; it <= nbin_t_; it++) {
        fprintf(fout, "  %e  %e  %e\n", t_history[it],
            n_history[it].at(0), func_sol_logistic(t_history[it]));
    }

    fclose(fout);
}

void finalize() {
    if (!initialized_oderk_) {
        return;
    }

    delete ptr_oderk_solver_;

    delete [] t_history;
    delete [] n_history;
    free(ptr_in_func);

    evolved_oderk_ = 0;
    initialized_oderk_ = 0;
}

double func_sol_logistic(double t) {
    return n_ini_ / (n_ini_ + (1. - n_ini_) * exp(t_ini_ - t));
}

 

ODESolveRK 객체의 포인터와 수치적인 해를 저장하기 위한 배열 등을 전역변수로 가지고 있는데요. main 함수에서는 이들을 참조하지 않고, 현재 소스파일에서만 사용되므로 헤더 파일에는 포함되어 있지 않습니다. 위에 언급된 헤더 파일에 선언된 함수들의 본체가 여기에 정의되어 있습니다. 한 가지 눈여겨볼 것은 함수의 본체는 벡터와 객체 등을 사용하고 있지만, 함수의 매개변수 및 리턴 변수형은 C언어에도 있는 것들이라는 점입니다. 그렇기 때문에 C언어로 작성된 소스코드에서도 이 함수들의 프로토타입을 선언하는데 문제가 없죠.

마지막으로 필요한 것은 main 함수가 포함된 C언어 소스파일입니다.

 

test1_population_RK4.c [다운로드]

 

더보기
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include"wrap1_population_RK4.h"

/* time is multiplied by the natural growth rate Gamma,
 * while population is divided by the carrying capacity K.
 * By doing that,
 * we can deal with dimensionless quantities. */

// time derivative of population
double dn_dt(double t, double *n);

int main(int argc, char *argv[]) {
    initialized_oderk_ = 0;
    evolved_oderk_ = 0;

    t_ini_ = 0.;  // initial time
    t_fin_ = 5.;  // final time
    delta_t_ = 0.02;  // size of timestep in RK

    n_ini_ = 0.4;  // initial population (N at t_ini)

    /* declare and set the function pointer to be used
     * in the ODE solver */
    double (*ptr_in_func)(double, double *);
    ptr_in_func = &dn_dt;

    // initialize ODE solver
    setup(ptr_in_func);

    // solve ODE by Runge-Kutta method
    evolve();

    char filename_prefix[200];
    char filename_extend[200];

    if (argc >= 2) {
        strcpy(filename_prefix, argv[2]);
    } else {
        strcpy(filename_prefix, "population_logistic");
    }

    if (argc >= 3) {
        strcpy(filename_extend, argv[2]);
    } else {
        strcpy(filename_extend, "dat");
    }

    // write output file
    write(filename_prefix, filename_extend);

    finalize();

    return 0;
}

double dn_dt(double t, double *n) {
    return n[0] * (1. - n[0]);
}

 

main 함수에서는 전역변수들을 통해 초기조건을 설정한 다음, 함수들을 호출하여 미분방정식의 수치적인 해를 구하고 이를 파일에 출력합니다. GNU 컴파일러를 사용하는 경우 다음과 같이 컴파일 및 링크하여 실행파일을 얻을 수 있습니다.
  g++ ODESolveRK.cpp -c
  g++ wrap1_population_RK4.cpp -c
  gcc test1_population_RK4.c -c
  gcc test1_population_RK4.o wrap1_population_RK4.o ODESolveRK.o -lm -lstdc++ -o [실행파일 이름]

프로그램을 실행해보면, 앞에 링크된 인구역학 관련 포스팅과 동일한 결과를 얻을 수 있습니다.

 


 

같이 알고 있으면 좋은 C/C++ 팁들

 

포트란과의 하이브리드

 

C/C++와 Fortran을 조합한 프로그래밍

목차 공통사항 C 메인 프로그램에서 Fortran subroutine 사용하기 Fortran 프로그램에서 C언어 함수 사용하기 수치계산을 위한 프로그램을 짜다 보면, 포트란 함수를 C 혹은 C++ 에서 쓰거나, 그 반대의

swstar.tistory.com

 

함수를 인자로 사용하기

 

C/C++ 에서 함수를 매개변수로 사용하기

함수 포인터를 이용한 구현 일반적으로 C언어나 C++ 에서 사용하는 함수의 경우, 인자(매개변수) 혹은 파라미터로 변수를 받아갑니다. 이 값들을 가지고 정의된 기능을 수행하게 되죠. 하지만 프

swstar.tistory.com

 

명령행 인자

 

Command-line arguments (C/C++ 명령행 인자)

C++를 배우기 위해 책을 보는데, command-line arguments 즉 명령행 인자에 대한 내용이 있었습니다. 메인함수를 특별한 방법으로 정의해서, 프로그램을 실행시킬때 커맨드 라인에서 옵션을 지정해줄

swstar.tistory.com