패킷 Log 를 찍는 Util 을 만들다가

 

컴파일 의존성

 

문제로 골머리를 앓고 있다...

 

우선 Mutiple definition of  '~~~' 부터 해결 해보자. 

 

그 전에 참고할게 있다.

 

중요한 내용을 다시 공부하고 가보자.

( 길어서 1편, 2편으로 나눴다. 문제 해결은 2편에서 다뤄본다 )

 

그리고 컴파일 과정에 대하여 기억나지 않는 다면 https://forward-movement.tistory.com/268?category=1004265 이 글을 보고오자.


cpp프로젝트의 build process는 **cpp 파일 별로** object file을 만드는 것입니다. ( 단위가 cpp 파일이다. )

 

이때 컴파일 하고자 하는 파일 밖의 다른 정보를 include하고 싶은경우 cpp나 object파일이 아닌 header 파일만 가지고있으면 cpp프로젝트는 컴파일이 가능합니다. ( 링킹을 통해 연결 작업이 필요하지만 )

 

cpp 파일 각자 독립적으로 빌드가 가능한 것이죠. ( 이것도 중요 ) 

 

아래 그림을 보자.

 

 

zoo.cpp 파일에서 Dog 나 Cat object의 함수들을 사용하려면 Cat.h 와 dog.h 를 #include 를 통해 추가하면 된다.

 

1. 전처리기(preprosessor)가 cat.h , dog.h 헤더파일만 가져와서 zoo.cpp와 하나의 translation unit 을 만들고

2. 컴파일 해주면 zoo.o( 목적파일) 이 만들어진다.

( 이 과정을 독립적인 Build Process 라고 한다. )

 

3. 이후 다른 obj 파일들과 라이브러리들을 링킹 해준다.

* 그래서 zoo.o 파일은 바이너리로 된 코드인데 여기에는 cat.h 에 대한 헤더파일의 코드만 있을 뿐,

cat.cpp에 있는 구현부가 없다.

그래서 링킹 해주지 않으면 사용하지 못한다.


 


main.cpp 를 아래와 같이 만들고 출력해보자.

 

 

아무 문제 없이 0을 출력해준다. 

 

이제 foo.cpp 를 만들고 ( foo.cpp 스크린샷은 생략.. )  아래의 코드만 친 후 main.cpp와 함께 빌드해보자.

어떻게 될까? ( 꼭 먼저 생각해보자!)

 

 

 

main.cpp가 컴파일러에 의해 컴파일되어 목적 파일이 생기고 ( main.o )

foo.cpp 또한 독립적으로 목적파일이 생길것이다.( foo.o )

 

그리고 링커는 두 개의 목적파일을 링킹해주어 하나의 실행파일이 생길것이다.

( ~~.exe ) 

 

문제 없어 보인다.

 

 

 

 

 

그런데 빌드해보면

아래와 같은 오류가 뜬다.  

 

컴파일을 하게 되면 main.o , foo.o 를 아무런 문제 없이 만들어주지만 링킹을 통해 두 목적 파일을 하나의 실행파일로 만들어주는데 a가 중복이 되는것을 보고 어떤 a를 사용해야할지 몰라 error를 던져주는것이다. 

 

이걸 방지하기 위한 여러가지 방법이 있는데 

 

 

 


첫번째는 extern 이라는 키워드이다.

 

main.cpp 에서 int a 에 값을 정의하지 말고, extern 키워드를 붙혀주면서 선언 해주면 정상적으로 빌드가 되고 실행이 된다. 

 

extern를 붙혀줌으로서 int a 의 정의는 바깥쪽 어딘가에 있어~ 라고 알려주는 키워드이다.

 

즉, 컴파일 될때 a가 바깥쪽 어딘가에서 정의된다는 정보를 가지고 있다. 

 

그리고 exe 파일을 만드는 링커는 main 쪽에서는 a의 정보가 바깥쪽에 있다는것을 확인하고,

foo 에서는 원래 들어있는 a = 100 을 이용해서 빌드를 할 수 있던것이다. 

 

 

 


 

두번째는 Static 이라는 키워드이다.

 

main 에서는 int a= 0 을 넣고, foo.cpp 에서는 static in a =100; 을 넣고 빌드해보면

빌드가 아무런 문제 없이 되고, 결과는 0 이 나온다.

 

main.cpp
foo.cpp

 

Static의 의미는 컴파일을 통해 ( 엄밀히 따지자면 컴파일 + 어셈블리 과정 을 통해)  .o 파일(목적파일) 을 만들때, a 라는 변수를 바깥쪽에서 찾을 수 없도록 만들어주는 키워드이다. 

 

하나의 translation unit 혹은 .o 파일 안에서만 접근이 가능하게 해준다. 

 

Build 프로세스를 따라가다 보면 링커가 foo.o 의  a에 접근 할 수 없기 때문에 main 에 있는 a를 가져와서 사용하게 되는것이다. 

 

# 22.07.04 추가 Start  -------------------------------------------------------------------------

 

아직 이해하지 못하고 있는 부분이 있어서 추가한다.

( Cat.cpp 와 Cat.h 파일 두개이다. )

발단은 아래의 코드이다.

cat.cpp
cat.h

빌드 해보면 어떻게 될까?

복습 할 때 스크롤 먼저 내리지 말고 결과가 어떻게 될지 먼저 생각 해보자!

 

 

 

 

아래와 같은 오류가 뜬다.

왜??? ( 당연하다...)

 

Cat.cpp가 컴파일 될 때 이미 Cat.h 에서 static int b로 초기화 되지 않는 data 영역(bss)에 ( 메모리에 ) 올라가 있는데

Cat.cpp 에서 다시 static int b = 101; 이라고 재정의 했기 때문이다.

 

같은 translation unit  끼리는 해당 static 변수에 접근 할 수 있다.

 

 

 

그렇다면

 

main.cpp 가 아래와 같고

 

foo.cpp 가 아래와 같을 때 빌드를 해주면 어떻게 될까?

 

항상 먼저 생각부터 해보자!!

 

main.cpp 컴파일 되서 obj 파일 생기고, foo.cpp 파일 컴파일 되서 obj 파일 생기고

여기까지 okay 일 것이다.

 

근데 이제 링킹 과정에서 어떤 변수가 우선 순위? 인지는 모르겠지만

main.cpp 의 a 변수가 우선?이라고 한다면

 

foo.obj 파일에서 LNK2005 에러가 나면서 "int a" 가 이미 main.obj에 정의 되어 있습니다.

하면서 링킹 에러가 날것이다. 

 

 

# 추가  End -------------------------------------------------------------------------

 

# 22.08.31 추가 --- start

다시 보는데 헷갈리는 내용이 있어서 추가한다.

 

그렇다면 main.cpp 가 아래와 같고, ( int a 가 전역 변수가 아니라 main 안에 지역 변수로 있다면 )

foo.cpp 가 아래와 같으면 어떻게 될까?

우선 둘다 컴파일 과정을 거쳐 목적파일이 생길것이다.

 

그리고 링킹 하는 과정에서 이중 정의로 링킹 에러가 날까?

 

아니다. 에러가 안난다.

 

전역 변수나 , static 변수가 아닌 변수들은 링킹 과정에서 링킹을 해주는 대상이 아니다?( 이 정도 표현이 맞는거 같다.)

 

다시 말해 아래 그림 같이 되는것 같다.!!

 

# 22.08.31 추가 --- end

 

 

 

 

 

함수도 마찬가지이다.

 

main.cpp foo.cpp 두 개를 만들어 보자.

빌드 하면 어떻게 될까?

무조건 생각 먼저 해보자!

 

main.cpp 를 컴파일해서 main.obj가 생기고 foo.cpp를 컴파일해서 foo.obj가 생길텐데

두 목적 파일을 링킹하는 과정에서 링커는 foo() 함수를 정의해줘야한다.

원래 같았으면 main.cpp의 foo() 함수에 대한 구현부를 알 수 없기 때문에 링킹 에러가 날것 같지만

 

컴파일 해서 실행해보면 아무런 문제가 없다.

 

main 에서 사용한 foo() 에 대한 함수 정의는 링커가 다른 .o 파일( foo.o)에서 찾아서 실행파일을 만들어냈다.

이건 마치 변수에 extern을 사용한것이랑 다를게 없다.

 

실제로 extern을 붙혀주어도 결과는 똑같다. 

 

 

이유는 함수에 대해서는 default 값으로 extern 이 붙어있기 때문이다. 

 

 

그럼 static 키워드를 함수에 붙히면 어떻게 될까?

 

반드시 먼저 생각해보자!!

 

역시 예상대로 main.cpp에서 foo() 함수를 실행할 수 없다고 나온다.

링킹 에러가 아닌거는 잘 모르겠다.. 

 

static이 붙으면 링킹 하기 전에 컴파일 타임에 정의를 찾나보다.! 라고 생각하면서 찾아보니

맞는거 같다! static 한 영역의 메모리 할당을 컴파일 과정에서 수행한다고하니!

 

 

다시 돌아와서 그럼 아래의 경우는 어떻게 될 까?

꼭 생각 먼저 해보자!

 

foo.cpp 와 main.cpp 이다. 

 

 

우선 두 .cpp 모두 컴파일되서 obj 파일이 나올테고

 

링커에 의해 링킹 될 때 , main.cpp 파일의 foo() 함수와 bar() 함수는 default 로 extern 이 붙은 거니깐

외부에서 찾아 주려고 할 것이다.

 

근데

 

static 키워드는 해당 .o (목적파일) 에 대해서만 링크를 제공해주기 때문에 바깥에서 부르면 bar() 함수에 대해 아래와 같은 오류가 난다.

 

당연한 소리지만, 같은 translation unit 혹은 .o 파일에서 불러주게 되면 아무런 문제 없이 실행된다.

 

아래 foo() 함수 내에서 bar() 함수를 불러줬다. 

 

결론적으로 함수 혹은 변수가 하나의 .o 파일 혹은 translation unit 바깥으로 링크를 주지 않는다는 확신이 있으면 static을 붙혀줌으로써 더 안전한 build 프로세스를 가져갈 수 있다. 

 

예를 들면

이런 Cat 클래스가 있다고 하면

bye() 라는 함수는 어떠한 member variable 에도 접근을 하지 않고 있다. 

 

이럴때 bye() 함수를 static 을 붙혀 free func 으로 만들어주면 cat.o ( 목적파일) 혹은 translation unit 에서만 접근이 가능하게 된다.

 

 

 

 


추가)

 

main.cpp 에서 Cat.cpp 에 있는 bye 함수를 사용하려고 한다면 어떻게 할까? ( 바로 위의 코드를 이용해서 ) 

 

Cat.h 를 include 한다고 될까?

 

main.cpp

 

Cat.h 에는 bye() 함수가 없으니깐 당연히 안 될 것이다. 

 

그렇다면 Cat.h 를 아래처럼 바꿔주면 되지 않을까?

Cat.h                                                                                                                     Cat.cpp

일단 빨간줄이 생기지 않는 걸로 봐서는 bye 함수를 cat.h 를 통해 아는것 같다. 

하지만, 실행해보면 아래 오류가 나온다. 

 

자~~ 생각할 시간이다.

왜 틀렸는지 먼저 생각부터 해보자. 

 

결론부터 말하자면 "헤더파일을 통해 선언은 확인 했으나 Cat.cpp 파일의 bye 함수가 static이라 접근을 하지 못한것이다."

 

Cat.cpp / main.cpp 둘 다 독립적으로 obj 파일이 생길 텐데

둘다 #include "Cat.h"를 해주고 있다.

 

 

build 과정을 통해 말하자면 Cat.cpp는  #include "Cat.h"  를 통해 Cat.h 까지 같은 .o 파일로 변하는 translation unit 이기 때문에  bye() 함수를 알 수 있으나, 정작 main.cpp 를 main.o 로 컴파일 하는 과정에서 bye 함수의 선언은 알 수 있는데 정의는 모르는 것이다. 정의는 bye 함수가 static 함수이기 때문에 링킹 과정을 통해 가져 올 수 없다. 즉 알려면 해당  translation unit  안에 정의가 있어야 되는것이다. ( static 변수나, static 함수는 컴파일 타임에 정의되어 있어야한다. )

* MSDN : 함수는 static 파일 범위 내에서 정의해야 합니다. 함수가 다른 파일에 정의되면 선언 extern되어야 합니다.

 

주의할점 : 위 에러는 컴파일 에러이다. 조금 더 위에 있는 static 함수 관련 에러는 링크 에러이다.

둘이 왜 에러가 다른지 구분 할 수 있어야 한다. ( 더 위에는 컴파일까지는 okay 인데 extern이 붙은 bar() 함수를 링킹하려고 찾아보니깐 static 이라 못읽는거고(링킹 오류), 이거는 애시당초 static 함수라 translation unit  안에서만 아는데 main.cpp 를 main.o 로 컴파일 할 때 bye()  함수의  정의가 없는 컴파일 에러이다.   만약에 main.cpp 에서 #include "Cat.h"를 없애고 아래 처럼 한다면 이제는 링킹 에러가 날것이다. 

함수의 default 는 extern 이기 때문에 컴파일은 넘어가는데 링킹 과정에서 찾아올래도 정의를 찾을 수 없으니 말이다. ( Cat.h 에 있는 bye 함수는 static으로 되어 있기 때문에 해당 translation unit 에서만 쓰인다. )

)

 

 

즉, bye 함수가 static이 아니였다면 

 

main 함수에서 bye() 함수가 잘 실행되는데 ( extern 처리가 되기 때문에 링킹 과정에서 찾을 수 있다.)

 

이는 앞서 살펴본것 처럼 extern 키워드가 생략되었기 때문에 컴파일 과정에서 "bye() 는 외부 어딘가에 정의되어 있어요" 라고  extern 정보를 같이 .o 파일로 만들고 링커는 그것을 토대로 cat.cpp 를 컴파일한 cat.o 에서 bye 의 정의를 링킹해주기 때문이다.

 

추가 2) 그러면 zoo.h 를 새로 만들고 cat.h 에서 include "zoo.h" 를 해주고, zoo.h 안에  bye() 함수 정의를 만들면 가능하지 않을까?

cat.h

zoo.h

cat.cpp

 

 

실행해보면 잘 실행 된다. 

 

main.cpp 의 오브젝트 파일을 만들때 ( 컴파일) cat.h 에서 zoo.h 를 include 했으니깐 zoo.h 까지 translation unit 이 되어 main.cpp 에서 bye() 함수의 정의를 볼 수 있기 때문이다. 

 


정리하자면 그냥 함수나 전역변수를 선언하면 자동으로 extern 키워드가 붙어 외부에서 가져다 쓸 수 있도록 동작한다.

물론 명시적으로 extern을 써도 된다.

 

static 키워드를 붙이면 외부에서 가져다 쓰지 못하고 해당 파일 내부에서만 쓸 수 있도록 접근 범위를 제한한다.

 

만약 내가 지금 만드는.o 파일 안에서만 쓸 함수나 전역변수를 선언하고자 한다면 앞에 static 키워드를 붙여주면 여러가지를 생각하면서 만드는 코딩을 할 수 있다.

 


+ Recent posts