2025. 4. 29. 18:02ㆍProgramming Language/C++
1. 선언과 정의의 분리
C++에서는 One Definition Rule(ODR, 단일 정의 규칙)이라고 해서 하나의 엔티티(함수, 변수, 클래스 등)는 프로그램 전체에서 단 하나의 정의만 가져야 한다라는 규칙이 존재하고 이는 C++의 컴파일 및 링크 과정에서 매우 중요한 요소가 된다.
ODR의 핵심개념
하나의 프로그램 전체에서 특정 이름(함수, 전역 변수, 클래스 등)은 오직 하나의 정의 만 존재해야 함
여러 번 선언은 가능하나 정의는 단 한 번 만 가능하고 만약이를 위배해서 여러 번 정의가 존재하면 링커 에러(linker error)가 발생함
우선 정의는 선언을 포함한 개념으로 정의만 하더라도 함수, 전역 변수, 클래스등의 사용은 문제가 없다.
함수의 경우는 프로토 타입이란것으로 함수의 선언을 따로 분리할 수 있었는데
이런 프로토 타입의 경우는 여러번 선언해도 문제가 되지 않는다.
특히 다른 파일에서 함수를 호출하기 위해서는 선언이 필요한 경우가 많다.
그러나 정의 자체를 다른함수에서 동일한 이름으로 하면
이렇게 링커 에러를 발생시킨다.
전역 변수의 경우는 이렇게 정의를 해두고
다른 파일에서 해당 전역 변수를 사용하고 싶다면 extren 키워드를 사용해서 정의를 따로 해줘야만 했었다
그런데 동일하게 다른 파일에서 전역 변수를 또 정의 한다면
이렇게 전역 변수의 정의가 여러번 되어 있다고 링커 에러를 발생시킨다.
그러면 클래스의 경우는 정의 한후에 다른 파일에서 사용하려면 어떻게 해야할까?
이렇게 정의된 Car 클래스가 있다고 했을때 해당 파일 내부에선
그냥 이렇게 선언해서 사용하면 됐었는데 이 클래스를 다른 파일에서 사용하려면
그냥 이렇게 복붙해서 사용해도
링커 에러가 발생하지 않는다.
클래스의 경우는 정의의 내용이 동일하다면 여러번 정의해도 하나의 정의라고 인식한다.
네임 스페이명을 using을 사용해서 생략하거나 생략을 안하는 정도의 작은 범위의 차이는 허용해주나 어쨋든 링커 에러를 넘어 간다고 하더라도 다른 점이 있다면 Undefined Behavior가 발생할 가능성이 높다.
어쨋든 이렇게 정의가 매 파일마다 되어 있어야 클래스를 사용할 수 있다면 여간 불편한게 아니다.
그래서 우리는 헤더파일이란걸 만들어서 사용하게 된다.
먼저 헤더파일을 하나 생성해보자.
헤더 파일 우클릭 > 추가 > 새항목 을 눌러주고
헤더파일을 클릭해준 후에 파일명을 작성해주고(여기선 클래스 명 그대로 작성함) 생성해주자
그리고 클래스의 정의 문을 가져와서 붙여 넣어준다.
그리고 이 클래스를 사용하려고 했던 파일의 상단에 #include 전처리 지시자를 사용해서 모듈을 include할때에는 꺽쇠<>를 사용해서 넣었는데 헤더 파일과 같이 다른 파일을 include하는 경우엔 쌍따옴표""를 사용해서 넣어주면 include 된다.
이렇게 넣어주면 각각의 파일에서 클래스를 사용할 수 있게 된다.
그리고
이렇게 다른 파일에서 선언되어 있는 함수를 사용할 때에도
이렇게 매번 선언을 따로 해줘야하는 부분은 번거로우니 이 또한 헤더 파일에 포함시켜보자.
그러면 이제
따로 선언을 안해줘도 해당 함수가 사용할 수 있게 된다.
그런데 여기서 이렇게 함수를 정의한 내용을 헤더 파일에 넣을때 생기는 단점은
이렇게 다른 파일에도 include 했을때 중복 정의로 처리가 되게 되는 점이다.
이러면 헤더 파일은 한번밖에 include 못하게 되는 것이다.
우리가 불편했던건 여기 저기에 함수를 사용하기 위해서는 여기 저기에 선언이 필요했기 때문이다
그렇기에 사실 우리가 필요한건 선언에 대한 것이지 정의가 아니기에 헤더파일에는 함수의 정의만 남겨두고
헤더파일과 동일한 명칭의 cpp파일을 하나 생성해주고 거기에 정의부를 넣어주고
이 내부에서 클래스 파일을 사용하는 경우가 많기에 동일한 헤더를 include 해준다.
이러고 실행해보면
문제 없이 실행이 된다.
추가로 클래스 내부의 함수의 선언도 분리해둘 수가 있다.
이 클래스 내부에 Car, call 함수를 프로토 타입으로만 남겨두고
cpp 파일에 넣어주는데
이렇게 그냥 사용할 수는 없고 해당 함수들이 어떤 클래스의 함수인지를 알리기 위해서 함수명 앞에 클래스 명을 :: 연산자를 사용해서 붙여주면 된다.
이렇게 하면 정의와 선언의 분리가 완료된것이다.
이 사용방법의 장점은 사용자는 이제 정의가 어떻게 되어 있는지는 상관없이 선언 부만 include해주면 사용이 가능하다는 점이다.
이로 인해서 사용자가 조금 더 사용하기에 편리하다는 이점이 있다.
이런 방식도 정보은닉, 캡슐화의 과정이다.
2. 전방 선언
전방 선언은 사실 이전에 함수의 선언에서 했던 프로토타입과 비슷한 개념이다.
우선 코드를 한번 보자면 먼저 Car를 사용하는 Company라는 클래스가 존재하고 이 클래스를 담을 헤더, cpp파일이 각각 존재한다고 해보자(각각 만들어 놓자)
만약 헤더 파일이 이렇게 선언만 되어 있는 곳이라면 클래스를
이렇게 include하면 되지만 전방선언을 하는것도 가능하다.
전방 선언이 아무곳에서나 되는 것은 아니고 위 처럼 정의부가 없이 선언부만 있는 경우 Car가 어떤 친구인지 알 필요가 없기 때문에 컴파일러가 이걸 궂이 걸고 넘어지지 않는다.
그러나나 이 헤더파일에 맞는 cpp파일의 경우는 함수에 대한 각각의 정의가 되어야 하기 때문에
이렇게 전방선언이 불가능하고 이 때는 헤더파일을 include해줘야만 한다.
이렇게 include를 사용하지 않고 전방선언을 사용하려는 이유는 Car.h파일을 Company.h에 넣는 순간 Car.h가 Company.h에 그냥 코드 자체가 들어간다고 보면 된다고 했었는데 이러면서 Car.h와 Company.h파일이 의존관계를 갖게 되는 것이다.
이러면 사실 Car.h를 변경하게 되면 Company.h에서도 변경이 이루어지는 것이고(그대로 갖다 넣는 것이기에) 동일하게 Car.h를 사용한 모든 곳에 변경점이 일어나게 되면서 모든 파일을 다시 컴파일해줘야 하는 상황이 생긴다.
우리 처럼 이렇게 간단하게 만드는 경우는 상관 없으나 프로젝트가 커지면서 이 컴파일 시간이 엄청 오래 걸리는 경우가 있는데 이런 부분들이 그 상황을 더 악화시킬 수 있다는 점을 알아두고 코드를 작성해줘야한다.
3. pragma once
헤더파일을 만들때
#pragma once라는 부분이 있는데 이건 C++에서 헤더 파일의 중복 포함을 방지하기 위한 전처리 지시문으로 한 번 포함된 헤더 파일이 동일 번역 단위(TU)에서 다시 포함되지 않도록 막아주는 장치이다.
만약 우리가 메인 파일에서
이렇게 Car과 Company를 둘다 넣었고 Company.h에서도 Car.h를 include 했다고 해보자
그러면 사실 메인에서는 Car.h를 include 하고 Company.h를 include 할때 Car.h파일을 또 include 해주게 되는 것이라서
#pragma once를 위와 같이 지워진 상태로 컴파일 해보면
이렇게 오류를 발생시킨다.
이걸 막아주는게 #pragma once이다.
그런데 사실 #pragma중 몆가지는 비표준이기에 사실 컴파일러 누구냐에 따라서 다르게 동작할 수 있는 전처리지시자이다.
그래서 컴파일러에 pragma가 갖고 있는 기능을 어떻게 구현했냐에 따라서 다르게 동작하게 된다.
그러나 사실 이 #pragma once는 대부분 사용이 가능하고 대부분 동일하게 구현이 되어 있어 사용하는데 문제가 없으나 오래된 프로젝트 혹은 보수적인 프로젝트나 오픈소스에서는 #ifndef 스타일의 include guard를 선호하는 경우도 있다.
#ifndef 스타일의 include guard
헤더 파일이 동일한 컴파일 단위(Translation Unit)에서 두 번 이상 include 되는 걸 막기 위한 구조로
// MyHeader.h
#ifndef MYHEADER_H // 이 매크로가 정의되어 있지 않다면
#define MYHEADER_H // 정의하고
class MyClass {
public:
void hello();
};
#endif // 여기까지 포함되도록 함
와 같이 구현하는 방식으로 처음 include 시에는 매크로가 정의되지 않았으므로 전체 내용이 들어가지만, 두 번째부터는 #ifndef가 false가 되어 내용이 무시된다.
우리의 프로그램에서 해보자면
이런식으로 만들어서 만약 이 헤더를 처음 include 할때 __CAR_H_가 메크로로 정의되어 있지 않다면(처음 들어 왔기에) #ifndef 문 내부로 들어와서 __CAR_H_를 정의하고 내부에 클래스 정의 파일을 include 하고 나간다.
두번째 동일한 헤더가 include 될때 이전에 __CAR_H_가 정의되었기에 #ifndef를 들어가지 않고 나가게 된다.
이렇게 include를 할때 동일한 헤더를 include 하는 것을 방지하기 위해서 사용되는 기법이다.
그런데 이 방법 또한 문제가 있을 수 있는데 먼저 저 메크로의 이름은 전역 메크로이기 때문에 중복 정의될 가능성이 존재한다.
물론 이건 휴먼 에러지만 사용자가 아무렇게나 지정할 수 있는 것이기 때문에 같은 파일이 다른 디렉터리에 존재할 경우 문제가 발생할 수 있다.
또한 pragma once는 에초에 열지를 않는데 이건 결국 include하기 위해서 파일을 열어서 메크로를 확인하기에 컴파일 속도가 느려질 수 도 있다는 단점이 있다.
그래서 사실은 __디렉토리명_파일명_ 과 같이 사용해서 충돌을 최소화하도록 하게 만들어 주는 방식이 존재하긴 한다.
강사는 pragma once를 쓰는게 좀 더 맞아보이는데 프로젝트에 따라서 맞춰 사용해야할것이라고 이야기 하긴 했으니 이는 상황에 맞게 선택해서 사용하는게 좋을듯 싶다.
'Programming Language > C++' 카테고리의 다른 글
Part2::Ch 01. 클래스 - 07. const (0) | 2025.04.29 |
---|---|
Part2::Ch 01. 클래스 - 06. this 포인터 (0) | 2025.04.29 |
Part2::Ch 01. 클래스 - 04. 파괴자(Destructor) (0) | 2025.04.29 |
Part2::Ch 01. 클래스 - 03. 생성자(Constructor) (0) | 2025.04.29 |
Part2::Ch 01. 클래스 - 02. 클래스와 객체 (0) | 2025.04.29 |