스토리텔링 개발자

[More Effective C++] 34. C++, C 혼용하기 본문

개발/More Effective C++

[More Effective C++] 34. C++, C 혼용하기

김디트 2024. 10. 21. 11:54
728x90

항목 34. 한 프로그램에서 C++와 C를 함께 사용하는 방법을 이해하자.

 

 

 

네임 맹글링(Name Mangling)
  • C++ 컴파일러가 내부 호출용 이름을 만들어 함수마다 붙이는 동작.
  • 이 동작은 C에서는 발생하지 않는다.
    • C에서는 함수 오버로딩이 발생하지 않기 때문이다.
    • 즉, 이름이 같은 함수란 존재하지 않는다.
  • 하지만 C++에서는 이름이 같은 함수를 부지기수로 볼 수 있다.
  • 사실, 오버로딩된 함수는 링커를 통과할 수 없다.
    • 같은 이름의 여러 함수를 구분할 수 없기 때문이다.
    • 그렇기 때문에 네임 맹글링을 사용한다.
  • C로 제작한 라이브러리 함수를 C++에서 사용한다면?
  • 아래와 같은 C 라이브러리가 있다면..
void drawLine(int x1, int y1, int x2, int y2);
// C++ 컴파일러가 네임 맹글링으로 내부 호출용 이름으로 바꿔버린다!!
// 예컨대 xyzzy 라는 이름으로 네임 맹글링을 해버렸다면..

drawLine(a, b, c, d); // 맹글링되지 않은 함수 이름으로 호출
// 이는 컴파일러에 의해 아래처럼 바뀐다.
// xyzzy(a, b, c, d); // 맹글링된 함수 이름으로 호출

// 하지만 C 라이브러리 내부에선 맹글링 되지 않은 이름(drawLine)으로 호출하는데
// (C 라이브러리이기 때문에 맹글링되지 않는다.)
// 이로 인해 링크 에러가 발생하게 된다.
  • 즉, C++ 컴파일러가 생성하는 목적 파일에는 맹글링되지 않은 이름의 C 함수를 호출하도록 해야 한다는 뜻이다.
// drawLine란 이름의 함수를 선언하되,
// 이 이름은 맹글링되지 않는다.
extern "C"
void drawLine(int x1, int y1, int x2, int y2);
  • extern "C"의 의미
    • 이 함수가 C로 작성되었다는 주장이 아니다.
    • 이 함수가 C로 작성된 함수처럼 호출되어야 한다는 뜻이다.
    • 아무튼 확실한 건 네임 맹글링을 막아준다는 것.
  • 어셈블러로 만들더라도 네임 맹글링은 되면 안되므로 extern "C"를 붙여주자.
// 이 함수는 어셈블러로 만들어졌다.
// 맹글링되지 않도록 한다.
extern "C" void twiddleBits(unsigned char bits);
  • C++ 함수 역시 extern "C"로 선언이 가능하다.
    • 왜 필요하냐면, 다른 언어에서 해당 라이브러리를 쓰고 싶을 수 있기 때문.
// C++ 이외의 프로그래밍 언어에서 할 수 있도록, 맹글링을 막는다.
extern "C" void simulate(int iterations);
  • extern "C"는 중괄호로 묶어줄 수도 있다.
extern "C"
{
    void drawLine(int x1, int y1, int x2, int y2);
    void twiddleBits(unsigned char bits);
    void simulate(int iterations);
    ...
}
  • C++와 C를 동시에 사용하는 경우
    • C++ 컴파일러로 컴파일할 때는 extern "C"가 작동되고
    • C 컴파일러로 컴파일할 때는 extern "C"가 드러나지 않도록 하기.
    • __cplusplus 전처리자를 사용하여 처리한다.
#ifdef __cplusplus
extern "C"
{
#endif

void drawLine(int x1, int y1, int x2, int y2);
void twiddleBits(unsigned char bits);
void simulate(int iterations);
...

#ifdev __cplusplus
}
#endif
  • 주의 사항
    • C++ 컴파일러의 "표준" 네임 맹글링 알고리즘 같은 것은 없다.
    • 즉, 호환되지 않는 C++ 컴파일러로 생성한 목적 코드는 섞어 사용하면 링크 에러가 발생한다.

 

 

 

정적 데이터 초기화(Initialization of Statics)
  • 정적 데이터 초기화란?
    • main 함수가 실행되기 전에
      • 정적 클래스 객체
      • 전역 객체
      • 네임 스페이스와 파일 범위에 있는 객체
    • 의 생성자가 실행되는 것을 말한다.
  • 개발자들은 main이 가장 먼저 실행된다고 가정하기 때문에 C++ 컴파일러는..
    • main의 시작 부분에 정적 데이터 초기화를 수행하는 전용 함수 호출 코드를 삽입한다.
    • main의 마지막 부분에는 정적 데이터 소멸을 수행하는 함수를 삽입한다.
int main(int argc, char* argv[])
{
    performStaticInitialization(); // 컴파일러가 생성한 코드
    
    main에 개발자가 써넣는 코드들...
    
    performStaticDestruction(); // 컴파일러가 생성한 코드
}
  • 이는 즉, main이 C++로 작성되지 않으면 정적 객체의 초기화 및 소멸이 이루어지지 않는다는 뜻이다.
    • 그러므로 C++를 사용한다면 main은 C++로 작성하는 것이 좋다.
    • 프로그램의 대부분이 C여서 main을 C로 작성해야 합당할 것 같을 때라도 그렇게 하자. 정적 객체는 언제 생길지 모른다.
      • C로 만든 main에 새 이름을 할당해주고
      • C++로 만든 main에서 해당 함수를 호출해주는 식으로 변경하자.
      • extern "C"
        int realMain(int argc, char* argv[]); // C로 구현한 main
        
        int main(int argc, char* argv[]) // C++로 구현한 main
        {
            return realMain(argc, argv);
        }

 

 

 

동적 메모리 할당(Dynamic Memory Allocation)
  • C++에서 만든 객체는 new / delete를 사용하고(항목 8 참조)
  • C에서 만든 객체는 malloc(calloc) / free를 사용하자.
  • 각 페어를 맞게 사용하는 한 문제는 없다.
    • 하지만 짝을 잘못 맞춰 사용하는 경우 문제가 발생한다.
  • 이 룰을 실천하기 힘든 상황이 있다.
// ps가 가리키고 있는 문자열을 별도의 힙 메모리에 복사하고
// 그 포인터를 반환한다.
char* strdup(const char* ps);
// 리턴값의 해제는 사용자 측에서 해줘야 하는데..
// delete를 써야 할까, free를 써야 할까?

 

 

 

자료구조의 호환성(Data Structure Compatibility)
  • C++와 C 프로그램 사이에 데이터를 주고받을 수 있을까?
  • C는 C++의 기능을 이해할 수 없는 것이 당연하므로, C 언어가 이해 가능한 개념만 넘길 수 있다.
    • 즉, 멤버 함수 포인터나 객체를 C에 넘기는 건 불가능하다.
    • 하지만, raw 포인터나 구조체, 기본 제공 타입은 넘길 수 있다.
  • 구조체
    • C와 C++은 메모리 배열 구조가 동일하므로 안전하게 오갈 수 있다.
    • 함수가 들어 있더라도 가상 함수만 아니면 메모리 배열 구조는 변하지 않는다.
      • 즉, 비가상 함수만 가지고 있는 객체는 멤버 함수가 지원되지 않는  C에서도 사용할 수 있다.
      • 하지만 가상 함수가 포함되면 가상 함수 테이블 때문에 C++ 구조체의 메모리 배열이 C와 달라진다.(항목 24 참조)
    • 또한 상속 구조를 가진 구조체도 메모리 구조가 변경되므로 C에서 사용하기 힘들다.
  • C++와 C에서 동시에 컴파일 되는 자료구조는 호환성이 있다고 할 수 있다.
728x90
Comments