C 소스 파일을 연결하지 않는 이유는 무엇입니까?
이 질문에 이미 답변이 있습니다.
나는 스크립팅 배경에서 왔고 C의 전처리 기는 항상 나에게 못 생겼습니다. 나는 작은 C 프로그램을 작성하는 법을 배우면서 그것을 받아 들였다. 나는 내 자신의 기능을 위해 표준 라이브러리와 헤더 파일을 포함하기 위해 전처리기를 사용하고 있습니다.
내 질문은 왜 C 프로그래머가 모든 포함을 건너 뛰고 있고 C 소스 파일을 연결하는 것입니까? 모든 소스 파일이 아닌 한 번만 필요한 것이 정의됩니다.
여기에 제가 설명하는 예가 있습니다. 여기에 세 개의 파일이 있습니다.
// includes.c
#include <stdio.h>
// main.c
int main() {
foo();
printf("world\n");
return 0;
}
// foo.c
void foo() {
printf("Hello ");
}
cat *.c > to_compile.c && gcc -o myprogram to_compile.c
Makefile에서 와 같은 작업을 수행 하면 작성하는 코드의 양을 수 있습니다.
(이미 메인 소스 파일에 있기 때문에) 표준 라이브러리를 포함 할 필요가 없음을 의미합니다. 이것은 나에게 좋은 생각 인 것 가변!
그러나 나는 C가 매우 유사한 프로그래밍 언어라는 것을 알고 있고, 나보다 훨씬 더 똑똑한 누군가가 이미이 아이디어를 가지고 그것을 사용하지 않기 때문에 결정하고 상상하고 있습니다. 왜 안돼?
일부 소프트웨어는 그렇게 구축됩니다.
전형적인 예는 SQLite 입니다. 때때로 통합으로 컴파일됩니다 (많은 소스 파일에서 빌드시 수행됨).
그러나 접근 방식에는 장단점이 있습니다.
즉, 실질적인 검증 날 것입니다. 거의없는 경우에만 실용적입니다.
아마도 컴파일러는 좀 더 최적화 할 수 있습니다. 그러나 그러나 시간 최적화 (예 : 최근 GCC를 사용하는 경우 링크 및 링크 gcc -flto -O2
)를 사용하면 동일한 효과를 얻을 수 있습니다 (물론 빌드 시간이 늘어나는 대신).
각 함수에 헤더 헤더가 없습니다.
(함수 당 하나의 헤더 파일을 잘못된 접근 방식입니다. 1 인 프로젝트 (10 만 줄의 코드, 일명 KLOC = kilo line of code )의 경우, 최소한 소규모 프로젝트의 경우 단일 공통 헤더 파일 을위한 것이 합리적입니다 . 컴파일 사용하는 경우 GCC를 모든 공공 기능 현관과 유형, 아마의 선언을 포함하는) 정의 의 static inline
기능 현관 (작은 충분히으로부터 그 이익을 충분히 자주라는 인라인 ). 예를 들어, sash
셸 은 그런 방식으로 구성 됩니다 (52 KLOC 사용를 하는 lout
포맷 ).
또한 헤더 파일이 몇 개있을 수 있으며 모든 파일 #include
(그리고 미리 할 수있는) 인 단일 "그룹화"헤더가 있을 수 있습니다. 를 참조하십시오 예 얀손 (는 하나가 실제로 공공 헤더 파일)와 GTK (가 많은 내부 헤더를하지만, 사용하는 대부분 그것을의 응용 프로그램 이 단 한 #include <gtk/gtk.h>
차례에 모든 내부 헤더를 포함). 반대쪽에 POSIX 는 많은 헤더 파일을 가지고 어떤 파일이 어떤 순서로 포함되어야하는지 문서화합니다.
어떤 사람들은 많은 헤더 파일을 선호합니다 (어떤 사람들은 자체 선호 헤더 하나를 선언 할 것을 선호합니다). 나는 (개인 프로젝트 또는 두세 사람 만 코드를 작성하는 소규모 프로젝트의 경우)하지만 취향 의 문제입니다. BTW, 프로젝트가 많이 커지면 헤더 파일 (및 번역 단위) 세트가 크게 변경되는 경우가 자주 발생합니다. REDIS 도 찾아 내 ( .h
헤더 파일 139 개와 파일 214 .c
개, 즉 총 126 KLOC의 번역 단위).
하나 또는 여러 개의 번역 단위 를 사용합니다 (편의성, 습관, 관습). 내가 선호하는 것은 너무 작지 않고 일반적으로 수천 줄의 소스 파일 (즉, 번역 단위)을 가지고 있고, 종종 (60 KLOC 표준의 작은 프로젝트의 경우) 공통 단일 헤더 파일을 것입니다. GNU 메이크 와 같은 빌드 자동화 도구 를 사용하는 것을 잊지 마십시오 (종종 병렬 빌드를 사용 하며 동시에 여러 컴파일 프로세스를 실행하게됩니다). 성능이 우수한 소스 파일 구성의 장점은 상당히 빠르다는 것입니다. BTW, 경우에 따라 메타 프로그래밍make -j
접근 방식은 가치가 있습니다. 일부 (내부 헤더 또는 번역 단위) C "소스"파일은 다른 문제에 의해 생성 될 수 있습니다 (예 : AWK의 일부 펼쳐 , 들소 또는의 것 같은 특수 C 프로그램 ).
C는 1970 년대에 설계되었는데, 가장 선호하는 노트북보다 작고 느린 컴퓨터를 위해 설계 되었음 (일반적으로 모든 메모리는 최대 1 메가 바이트 또는 백만 킬로바이트였으며 컴퓨터는 천배 이상 느입니다.
소스 코드 를 연구하고 기존 무료 소프트웨어 프로젝트 (예 : GitHub , SourceForge 또는 좋아하는 Linux 배포판)를 빌드 할 것을 강력히 제안합니다 . 그들이 다른 접근 방식이라는 것을 알게 될 것입니다. 그 기억 C의에서 규칙 과 습관은 실제로 많은 문제가 , 그래서 거기에 다른 에서 프로젝트를 구성하는 방법 .c
과 .h
파일 . C 전처리기에 대해 읽어 읽어 보시기 바랍니다 .
또한 내가 만드는 각 파일에 표준 라이브러리를 포함 할 필요가 없음을 의미합니다.
라이브러리가 아닌 헤더 파일을 포함합니다 (하지만 라이브러리 를 연결 해야 함 ). 그것들을 각 그러나 .c
파일에 포함 시키 거나 (많은 프로젝트에서 그렇게하고 있음), 하나의 단일 헤더에 포함하고 해당 헤더를 미리 컴파일하거나 , 수십 개의 헤더를 가지고 각 컴파일의 시스템 헤더 뒤에 포함 할 수 있습니다 . 단위. YMMV. 각어의 컴퓨터에서는 전처리 시간이 빠르다는 점에 유의하십시오 (적어도 최적화는 구문 분석 및 전처리보다 시간이 더 걸리 컴파일러에게 최적화를 할 때).
일부 #include
-d 파일에있는 것은 내용 관습 적 이며 C 사양에 의해 정의되지 않습니다. 일부 프로그램은 이러한 파일에 일부 코드 를 포함합니다 ( "헤더"라고 부르지 않고 "포함 된 파일"이라고 부르면 안됩니다. 그런 다음 .h
접미사 가 없어야합니다 .inc
. 예를 들어 XPM 파일을 살펴보십시오 . 다른 극단에서, 당신은 원칙적 으로 자신의 헤더 파일의 (당신은 여전히 같은 구현에서 헤더 파일이 필요 없을 수도 있습니다. <stdio.h>
또는 <dlfcn.h>
귀하의 POSIX 시스템에서)를 복사하여 추가 된 코드를 .c
줄이 -eg 파일마다 int foo(void);
에 .c
파일이지만, 그것은 매우 나쁜 습관입니다 눈살을 찌푸립니다. 일부 프로그램은 생성 일부 공통 콘텐츠를 공유하는 C 파일.
BTW, C 또는 C ++ 14에는 OCaml과 같은 모듈이 없습니다. 즉, C에서 모듈은 대부분 관습 입니다.
(통지 많은 수천 개의 아주 작은 .h
및 각 .c
극적으로 빌드 시간이 느려질 수 있습니다 만 가지고 라인의 파일을,. 각각의 빌드 시간의 관점에서 더 합리적인 몇 백 라인의 수백 개의 파일을)
C로 1 인 프로젝트 작업을 시작한다면 하나의 헤더 파일 (그리고 미리 준비)과 여러 .c
번역 단위를 사용 하는 것이 좋습니다. 실제로 .c
파일을 파일보다 훨씬 자주 변경 합니다 .h
. 10 개 이상의 KLOC가 있으면 헤더 파일로 리팩터링 할 수 있습니다. 리팩토링은 설계하기 까다 롭지 만 수행하기 (많은 코드 복사 및 가져 오기). 다른 사람들은 다른 제안과 힌트를 가지고있을 것입니다 (괜찮습니다!). 그러나 컴파일 할 때 모든 경고 및 디버그 정보를 활성화하는 것을 잊지 마십시오 (따라서 에서 gcc -Wall -g
설정 하여 컴파일 CFLAGS= -Wall -g
하십시오 Makefile
). gdb
디버거 (및 Valgrind의 ...)를 사용하십시오 . 최적화 요청 ( -O2
) 이미 풍부한 프로그램을 벤치마킹 할 때. 또한 Git 과 같은 버전 제어 시스템을 사용하십시오 .
반대로, 여러 사람 이 작업 할 더 큰 프로젝트를 설계하는 경우 여러 파일 (여러 헤더 파일 포함)을 갖는 것이 더 나을 수 있습니다 (직관적으로 각 파일에는 주로 책임이 있는 한 사람이 있고 다른 사람은 해당 파일에 대한 기여).
댓글에 다음을 추가합니다.
많은 다른 파일에 내 코드를 작성하는 것에 대해 이야기하고 Makefile을 사용하여 연결합니다.
왜 그것이 유용한 지 모르겠습니다 (매우 이상한 경우를 제외하고). 각 번역 단위 (예 : 각 .c
파일)를 수업 파일 ( Linux 의 .o
ELF 파일 )로 만들고 나중에 링크 하는 것이 훨씬 낫습니다 (매우 일반적이고 일반적인 관행) . 이 용이합니다 make
(실제로, 당신은 단지 하나를 변경할 수 있습니다 때 .c
파일이 컴파일됩니다 것만 버그를 해결하기 위해 파일 예 및 증분 빌드는 정말 빠르다), 그리고 당신이 오브젝트 파일을 컴파일을 요청할 수 있습니다 병렬 사용 make -j
(및 그러면 빌드가 멀티 코어 프로세서에서 정말 빨리 진행됩니다.)
그렇게 할 수 는 주로 다음과 같은 M 프로그램을 별도의 번역 단위 로 분리하는 것이 좋습니다 .
빌드 속도를 높입니다. 파일 만 다시 변경된 빌드하면 되고 다른 컴파일 된 파일과 -link 되어 최종 프로그램을 구성 할 수 있습니다 .
C 표준 라이브러리는 미리 구성된 구성 요소로 구성됩니다. 정말 모든 것을 다시 준비하고 싶습니까?
코드베이스가 다른 파일로 분할되어 있으면 다른 프로그래머와 협업 할 수 있습니다.
- 모듈화를 사용하면 코드를 공유하지 라이브러리를 공유 할 수 있습니다.
- 단일 파일을 변경하면 전체 프로젝트를 수행하게됩니다.
- 큰 프로젝트를 더 쉽게 메모리가 부족할 수 있습니다.
- 모듈에 순환이있을 수있는 모듈 성은이를 유지하는 데 도움이됩니다.
접근 방식에 약간의 이득이있을 가능성이 있습니다.
.c 파일을 연결하는 방법은 완전히 깨졌습니다.
명령
cat *.c > to_compile.c
이 모든 함수를 단일 파일에 넣 습니다 . 처음 사용하기 전에 각 함수를 선언해야합니다.즉, 특정 순서를 강제하는 .c 파일간에 있습니다. 연결 명령이 순서를 정렬하지 않고 정렬 할 수 없습니다.
또한 서로를 재 사용하는 두 개의 함수가있는 경우 둘 중 하나에 선언을 사용할 방법이 없습니다. 사람들이 더 이상 기대하는 헤더 파일에 선언 할 수도 있습니다.
모든 것을 단일 파일로 연결하면 프로젝트의 한 줄이 변경 될 때마다 강제로 전체 재 빌드를 수행합니다.
고전적인 .c / .h 분할 방식 방식을 사용하면 함수를 변경하면 하나의 파일을 재 실행해야하고 헤더를 변경하면 실제로 헤더를 포함하는 파일을 재 구현해야합니다. 이렇게하면 .c 파일 수에 따라 100 배 이상 작은 변경 후 재 구축 속도를 쉽게 사용할 수 있습니다.
모든 것을 단일 파일로 연결하면 모든 기능 이 있습니다.
하이퍼 스레딩이 활성화 된 대용량 12 코어 프로세서가 있습니까? 유감입니다. 많은 소스 파일이 단일에 구현됩니다. 당신은 20보다 큰 팩터의 속도 향상을 잃었습니다 ... 좋아요, 이것은 극단적 인 예입니다.하지만 저는
make -j16
이미 소프트웨어를 빌드했습니다 . 그리고 저는 이것이 큰 차이를 만들 수 있습니다.time-은 일반적 컴파일으로 선형 이 아닙니다 .
일반적으로 컴파일러에는 2 차 실행 동작이있는 알고리즘이 있고 있고 있습니다. 더 느린 임계 값이 있습니다.
분명히이 임계 값의 정확한 위치는 컴파일러와 전달하는 최적화 플래그에 따라 다르지만 컴파일러가 하나의 거대한 소스 파일에서 30 분 이상이있는 것을 보았습니다. 변경-컴파일-테스트 루프에서 장애물을 테스트합니다.
실수하지 마십시오 : 이러한 모든 문제가 발생하더라도 실제로 .c 파일 연결을 사용하는 사람들이 있으며 일부 C ++ 프로그래머는 모든 것을 템플릿으로 이동하여 거의 동일한 지점에 도달합니다 (구현은 .hpp 파일이고 연관된 .cpp 파일이 없음), 전처리 기가 연결을 수행하도록합니다. 나는 그들이 어떻게 이러한 문제를 무시할 수 있는지 보지 못하지만 그들은 그렇게한다.
또한 이러한 문제의 대부분은 프로젝트 규모가 클 때만 분명해집니다. 프로젝트가 5000 줄 미만인 경우에도 컴파일 방법은 상대적으로 관련이 없습니다. 그러나 50000 줄 이상의 코드가있는 경우 증분 및 병렬 빌드를 지원하는 빌드 시스템이 필요합니다. 그렇지 않으면 근무 시간을 낭비하게됩니다.
분할하는 것이 좋은 프로그램 디자인이기 때문입니다. 좋은 프로그램 설계는 모듈화, 자율 코드 모듈 및 코드 재사용에 관한 것입니다. 결과적으로, 프로그램 설계를 할 때 상식이 매우 멀리 떨어질 것입니다. 함께 속하지 않는 것은 함께 배치해서는 안됩니다.
관련없는 코드를 다른 번역 단위에 배치하면 변수 및 함수의 범위를 가능한 한 많이 지역화 할 수 있습니다.
함께 일을 병합 만들어 꽉 커플 링을 정말 심지어 서로의 존재에 대해 알고 필요가 없습니다 코드 파일 사이의 어색한 종속성을 의미합니다. 이것이 프로젝트의 모든 포함을 포함하는 "global.h"가 나쁜 이유입니다. 전체 프로젝트에서 관련이없는 모든 파일간에 긴밀한 결합을 생성하기 때문입니다.
자동차를 제어하기 위해 펌웨어를 작성한다고 가정합니다. 프로그램의 한 모듈은 자동차 FM 라디오를 제어합니다. 그런 다음 다른 프로젝트에서 라디오 코드를 재사용하여 스마트 폰에서 FM 라디오를 제어합니다. 그리고 브레이크, 바퀴, 기어 등을 찾을 수 없기 때문에 라디오 코드가 컴파일되지 않습니다. 스마트 폰은 물론이고 FM 라디오에 대해 조금도 이해가되지 않는 것들.
더 나쁜 점은 긴밀한 결합이있는 경우 버그가있는 모듈에 로컬로 머무르는 대신 전체 프로그램에서 버그가 확대된다는 것입니다. 이것은 버그 결과를 훨씬 더 심각하게 만듭니다. FM 라디오 코드에 버그를 작성하고 갑자기 자동차 브레이크가 작동을 멈 춥니 다. 버그가 포함 된 업데이트로 브레이크 코드를 건드리지 않았더라도.
한 모듈의 버그가 관련이없는 것을 완전히 망가 뜨리는 경우, 이는 잘못된 프로그램 설계 때문입니다. 그리고 열악한 프로그램 디자인을 달성하는 특정 방법은 프로젝트의 모든 것을 하나의 큰 Blob으로 병합하는 것입니다.
헤더 파일은 인터페이스를 정의해야합니다. 이는 바람직한 규칙입니다. 해당 .c
파일 또는 .c
파일 그룹 에있는 모든 것을 선언하기위한 것이 아닙니다 . 대신 .c
사용자가 사용할 수 있는 파일의 모든 기능을 선언합니다 . 잘 설계된 .h
파일은 .c
주석이 하나도 없더라도 파일 의 코드에 의해 노출되는 인터페이스의 기본 문서로 구성 됩니다. C 모듈의 디자인에 접근하는 한 가지 방법은 먼저 헤더 파일을 작성한 다음 하나 이상의 .c
파일 에 구현하는 것입니다 .
결과 : .c
파일 구현 내부의 함수 및 데이터 구조 는 일반적으로 헤더 파일에 속하지 않습니다. 포워드 선언이 필요할 수 있지만 이러한 선언은 로컬이어야하며 따라서 선언되고 정의 된 모든 변수와 함수는 다음과 같아야합니다 static
. 인터페이스의 일부가 아닌 경우 링커는이를 볼 수 없습니다.
주된 이유는 컴파일 시간입니다. 작은 파일 하나를 변경할 때 컴파일하는 데 시간이 오래 걸릴 수 있습니다. 그러나 한 줄을 변경할 때마다 전체 프로젝트를 컴파일하려면 컴파일해야합니다. 예를 들어 매번 10,000 개의 파일을 컴파일하면 훨씬 더 오래 걸릴 수 있습니다.
위의 예에서와 같이 10,000 개의 소스 파일이 있고 하나를 컴파일하는 데 10ms가 걸린다면이 변경된 파일 만 컴파일하면 전체 프로젝트가 (단일 파일을 변경 한 후) 점진적으로 빌드됩니다 (10ms + 링크 시간). (10ms * 10000 + 짧은 연결 시간) 모든 것을 하나의 연결된 Blob으로 컴파일하는 경우.
여전히 모듈 방식으로 프로그램을 작성하고 단일 번역 단위로 빌드 할 수 있지만 C가 모듈성을 강화하기 위해 제공하는 모든 메커니즘 을 놓칠 수 있습니다 . 여러 번역 단위를 사용하면 eg extern
및 static
키워드 를 사용하여 모듈의 인터페이스를 세밀하게 제어 할 수 있습니다.
코드를 단일 번역 단위로 병합하면 컴파일러가 경고하지 않기 때문에 발생할 수있는 모듈성 문제를 놓칠 수 있습니다. 큰 프로젝트에서 이것은 결국 의도하지 않은 종속성이 확산되는 결과를 초래합니다. 결국 다른 모듈에서 글로벌 부작용을 만들지 않고 모듈을 변경하는 데 어려움이 있습니다.
모든 include를 한 곳에 넣으면 모든 소스 파일이 아닌 한 번만 필요한 것을 정의하면됩니다.
이것이 .h
파일 의 목적 이므로 필요한 것을 한 번 정의하고 모든 곳에 포함 할 수 있습니다. 일부 프로젝트 everything.h
에는 모든 개별 .h
파일 을 포함 하는 헤더 도 있습니다 . 따라서 별도의 파일로도 전문가 를 얻을 수 있습니다 .c
.
이것은 내가 생성하는 각 함수에 대해 헤더 파일을 작성할 필요가 없다는 것을 의미한다 ...]
어쨌든 모든 함수에 대해 하나의 헤더 파일을 작성해서는 안됩니다. 관련 함수 세트에 대해 하나의 헤더 파일이 있어야합니다. 그래서 당신의 죄수 도 유효하지 않습니다.
이것은 내가 생성하는 각 함수에 대해 헤더 파일을 작성할 필요가 없음을 의미하며 (이미 메인 소스 파일에 있기 때문에) 또한 내가 생성하는 각 파일에 표준 라이브러리를 포함 할 필요가 없음을 의미합니다. 이것은 나에게 좋은 생각 인 것 같습니다!
당신이 알아 차린 전문가들은 실제로 이것이 때때로 소규모로 수행되는 이유입니다.
대규모 프로그램의 경우 비실용적입니다. 언급 된 다른 좋은 답변과 마찬가지로 이것은 빌드 시간을 크게 늘릴 수 있습니다.
그러나 번역 단위를 더 작은 비트로 나누는 데 사용할 수 있으며, 이는 Java의 패키지 접근성을 연상시키는 방식으로 함수에 대한 액세스를 공유합니다.
위의 방법은 전 처리기의 일부 규율과 도움을 포함합니다.
예를 들어 번역 단위를 두 개의 파일로 나눌 수 있습니다.
// a.c
static void utility() {
}
static void a_func() {
utility();
}
// b.c
static void b_func() {
utility();
}
이제 번역 단위에 대한 파일을 추가합니다.
// ab.c
static void utility();
#include "a.c"
#include "b.c"
그리고 빌드 시스템 중 하나를 구축하지 않습니다 a.c
또는 b.c
, 대신 전용 빌드 ab.o
에서 ab.c
.
무엇을 ab.c
성취합니까?
여기에는 단일 번역 단위를 생성하는 두 파일이 모두 포함되어 있으며 유틸리티에 대한 프로토 타입을 제공합니다. 모두의 코드가되도록 a.c
하고 b.c
그것을 볼 수, 순서에 관계없이하는 그들은 포함하고, 될 수있는 기능을 요구하지 않고있다 extern
.
참고 URL : https://stackoverflow.com/questions/42135503/why-not-concatenate-c-source-files-before-compilation
'ProgramingTip' 카테고리의 다른 글
JS 객체에 키가 있는지 확인 (0) | 2020.10.20 |
---|---|
Android Studio의 JNI 및 Gradle (0) | 2020.10.20 |
페이지를 새로 고침 할 때 선택이 선택 상태로 유지되는 이유는 무엇입니까? (0) | 2020.10.20 |
최대 로깅 모드 로깅 콘솔 출력 (0) | 2020.10.20 |
string.xml 파일에 "-"를 넣는 방법 (0) | 2020.10.20 |