0) Introduction This article will first introduce why you want to separate your C source code into several reasonable independent files, when needed, how can you be able to divide? Then tell you how GNU Make automates your compilation and connection steps. For users of other Make tools, although appropriate adjustments should be made when using other similar tools, this paper is still very useful. If you have doubts about your own programming tool, you can try it in actually, but please read the user manual. 1) Multi-file item 1.1 why use them? First, there is the benefit of multi-file projects? They seem to make things more complicated. Also wants the header file, but also the extern declaration, and if you need to find a file, you have to search in more files. But in fact, we have a strong reason to support us to break up a project into small pieces. When you change a line of code, the compiler needs to be re-compiled to generate a new executable. But if your project is separated in a few small files, when you change one of the files, the object file of the other source files already exists, so there is no reason to recompile them. What you need to do is just that reproduces the file that is changed, and then reconnects all the target files. In a large project, this means that it is a simple adjustment from a long (minutes from a few hours). As long as the basic planning, a project is broken down into multiple small files to make it easier to find a code. Very simple, you decompose your code into a different file according to the role of the code. When you want to see a code, you can know accurately in that file to look for it. Generating a package from a lot of target files (library) is much better than from a single big target file. Of course, this is actually a advantage is determined by the system you use. However, when a package is connected to a program using GCC / LD (a GNU C Compilation / Connector), in the process of the connection, it will try to not use the part that is not used. But each time you can only remove a complete target file from the package. So if you refer to any of the symbols in a single target file, then this target file will be connected in. If a package is very fully decomposed, then after the connection, the obtained executable is much smaller than the files connected to the package consisting of a large target file. Because your program is very modular, the sharing part between files is minimized, then there are many benefits - it can be easily tracked into bugs, which often can be used in other projects, At the same time, others can easily understand what your code is doing. Of course, there are many other benefits ... 1.2 When you decompose your project, it is unreasonable to decompose anything is unreasonable. Simple programs like "World, you are good" are not divided, because there is nothing to be able to divide. Decompose the decomposition of small procedures for testing is also meaningless. But in general, when a decomposition project helps layout, development and readability, I will take it. In most cases, this is suitable. (The so-called "world, you are good", both 'Hello World', just a sample program that introduces a programming language, which will display a line 'Hello World' on the screen.) If you need Develop a considerable project, before the start, consider how you will implement it and generate a few files (with the appropriate name) to put your code.
Of course, in the process of your project development, you can build new files, but if you do this, you may change the original idea, you should think about whether it needs to be adjusted for the overall structure. For medium-sized projects, you can certainly use the above skills, but you can also start entering your code, when your code is more difficult to manage, decompose them into different files. But in my experience, I will form a probably scheme in my mind, and try to follow it, or during development, as needed, it will make development easier. 1.3 How to Decompose the project first instructions, this is totally my personal opinion, you can (maybe you really will?) Do something else. This will touch the problem of coding style, and everyone has never stopped the debate on this issue. Here I just give me my favorite approach (while giving this reason): i) Do not point to multiple source files (exception: the header file of the package). The way to define a source code file is more efficient and easier to find. Otherwise, changing the structure of a source file (and its header file) must recompile several files. II) If you can, you can use more than one header file to point to the same source code file. Sometimes the function prototype, type definition, and so on, which cannot be invoked, and it is very useful from their C source file files. Using a Header file to install public symbols, use another private symbol that if you change the internal structure of this source file, you can just recompile it without having to recompile other source files that use its public header file. . III) Do not repeat the definition information in multiple header files. If necessary, in one of the header files #include one, but do not repeat the same Header information twice. The reason is that if you change this information later, you only need to change it once, no search and change another repetitive information. IV) In each source file, #include states all Header files that declare the symbols in the source file file. In this way, the contradictions you make in some functions in the source file and the Header file can be discovered by the compiler. 1.4 Reviews for common errors a) Conversation (Identifier) In the source file: In C, the default state of variables and functions is public. Therefore, any C source file file can reference universal (GLOBAL) functions and general variables existing in other source code archives, making this file without the declaration or prototype of the variable or function. So you must ensure that you can't use the same symbol name in different files, otherwise there will be an incorrect connection or a warning when compiling. A method of avoiding such an error is to add a prefix with its source file before the public symbol. For example: All functions in GFX.c are plus prefix "GFX_". If you are very careful to decompose your program, use meaningful function names, and not excessive common variables, of course, this is not a problem. To prevent a symbol from being seen outside of its defined source file, you can add keyword "static" before it definition. This is useful to use only simple functions that other files use only in one file. b) Multi-defined symbols: The Header file will be replaced with the location of the #include in your source file.
Therefore, if the Header file is included in #include to more than one source file, all definitions in this header file will appear in each related source code file. This will cause the symbols in which they are defined over, thus connecting the connection error (see). Workaround: Do not define variables in the HEADER file. You only need to declare them in the Header file and then define them in the appropriate C source file (should be included in the Header file). For beginners, definitions and statements are easily confused. The role of the statement is to tell the compiler that the symbols they have declared should exist and have the type specified. However, it does not make the compiler allocates storage space. The definition is required to require the compiler to allocate storage space. When making a statement instead of being defined, put a keyword "extern" before the declaration. For example, we have a variable called "counter". If we want it to become public, we define it in a source code (in one inside): "int counter;", then in the relevant header file declaration It: "Extern Int Counter;". The function prototype is implied with Extern, so you don't need to worry about this problem. c) Repeat definition, repeat declaration, contradictory type: Please consider if there are two files in the #include in a C source file, and the AH is also #include BH file (the reason is that the BH file defines some ah needs type) What happens? At this time, the C source code file #include has b.h twice. So each of the #define in B.H has occurred twice, and each declaration has occurred twice, and so on. In theory, because they are exactly the same copy, there should be no problems, but in practical applications, this is not conforming to C's grammar, which may occur when compiling, or at least warning. The solution is to determine that every Header file is only included in any source file file. We are generally used to achieve this with the pretreatment. When we enter every Header file, we have a giant command for this Header file #define. We really use the body file of the Header file only if this giant intention is not defined. In practical applications, we only need to put the following one code in the start of each Header file: #ifndef filename_h #define filename_h then put the following code in the last: #ENDIF with the Header file file (capital) instead FileName_h, use the bottom line instead of the point in the file. Some people like to make comments in #ENDIF to remind them that this #ndif refers to what. For example: #ENDIF / * #ifndef filename_h * / I personally there is no such habit, because this is actually very obvious. Of course, this is only different in the style of each person, and there is no harm. You only need to add this skill in the header file with compilation errors, but there is no loss in all HEADER files, which is a good habit. 1.5 Re-compiling a multi-file project clear distinct compilation and connection is important. The compiler uses the source code file to generate some form of destination file (Object Files). During this process, the external symbol reference is not explained or replaced.
Then we use the connector to connect these object files and some standard package plus your specified package, and finally connect to an executable program. At this stage, the reference to the symbols in other files is interpreted in a target file, and the reference that cannot be interpreted is generally reported in the form of error information. Basic steps should be, put your source code file into a form of a target file, and finally connect all the target files to an executable file. How to do it is determined by your compiler. Here I only give the relevant commands of the GCC (GNU C compiler), which may also apply to your non-GCC compiler. GCC is a multi-objective tool. It calls other components (pre-processing programs, compilers, combination programs, connector) when needed. Which components of specific components are called depends on the type of input file and the switch you pass to it. In general, if you only give it C source file file, it will preprocess, compile, combine all files, and then connect the resulting target file into an executable file (generally generated file is named A.out). Of course you can do this, but this will destroy a lot of benefits we have decomposed into multiple files. If you give it a -C switch, GCC only compiles its file into a target file, named the file name of the source file but the suffix is ".c" or ".cc" becomes ".o". If you give it a list of target files, GCC will connect them into executables, the default file name is A.out. You can change the name of the default name and use the file name you specified after you. Therefore, after you change a source code file, you need to recompile it: 'gcc -c filename.c' then reconnect your item: 'gcc -o exec_filename * .o'. If you change a Header file, you need to recompile all #include's source files for this file, you can use 'gcc -c file1.c file2.c file3.c' and then connect it as above. Of course, this is very cumbersome, fortunately, we have some tools to make this step simple. The second part of this article is to introduce one of the tools: GNU Make tool. 2) GNU Make Tools 2.1 Basic Makefile Structure GNU Make's main job is to read a text file, makefile. This document is mainly related to which files ('target' destination file) is from which other files ('Dependencies' relying on files), what commands are used to make this process. With this information, Make will check the file on the disk, if the timestamp of the destination file (the time when this file is generated or changed), Make performs the corresponding command to update Purpose file. (The destination file is not necessarily the final executable, which can be any file.) Makefile is typically called "makefile" or "makefile". Of course you can tell the file names in the Make command line. If you don't specify, it will find "makefile" or "makefile", so use these two names is the simplest.
A makefile mainly contains a series of rules, as follows: For example, consider the following makefile: === makefile start === myprog: foo.o bar.o gcc foo.o bar.o -o myprog foo.O: foo. C foo.h bar.h gcc -c foo.c -o foo.o bar.o: bar.c bar.h gcc -c bar.c -o bar.o === makefile end === this is one Very basic makefile - make from the top, put the top first destination, 'myprog', as its main goal (one it needs to guarantee the latest ultimate goal). The rules given instructions As long as the file 'myprog' is more old than any of the file 'foo.o' or 'bar.o', the next line will be executed. However, before checking the timestamp of the file foo.o and bar.o, it will look down the rules that make foo.o or bar.o as the target file. It found about foo.o rules that rely on files foo.c, foo.h and bar.h. It can't find a rule that generates these relying on files from below, it starts to check the timestamp of these relying on files on the disk. If any of the timestamps in these files are more than foo.o, the command 'gcc -o foo.o foo.c' will be executed, so that the file foo.o is updated. Next, a similar inspection is made to the file bar., and the file is here is the file bar.c and bar.h. Now, make back to 'myprog' rules. If any one of the two rules is executed, myProg needs to be rebuilt (because one of them. "New than 'myprog', the connection command will be executed. I hope this, you can see the benefits of using the make tool to create a program - all cumbersome check steps in the previous chapter are made by Make: Check the timestamp. A simple change in your source file will cause that file being recompiled (because the .o file relies on .c files), the executable file is reconnected (because the .o file has been changed). In fact, the real benefits are when you change a Header file - you no longer need to remember that source file rely on it, because all the information is in makefile. Make will easily recompile all the source code files that have changed because of this Header file, if necessary, reconnect. Of course, you have to make sure the rules you write in makefile are correct, only listing the Header files of #include in the source file ...
2.2 Write the Make Rule (Rules) The method of writing rules is a way to view the source code file, make their target file as a purpose, while the C source file and the Header file of #include by it. Rely on files. But you also have to list other Header files of these Header files #include as relying on files, as well as files included in the files included ... Then you will find more and more files, then Your hair begins to fall off, your temper begins to get bad, your face turns into a dish, you start to collide with the electric wire on the road, and finally you smash your computer monitor, stop programming. Is there any way to have some way? Of course! To the compiler! When compiling each source file, it should be found to include what kinder files should be included. When using GCC, use the -m switch, which will output a rule for each of your C file, make the target file as a purpose, and this C file and all Header files whose #include should be used as a file. . Note that this rule will join all Header files, including the files surrounded by cornence brackets (`<',`>') and dual quotation marks (`"). In fact, we can quite certainly determine the system Header file (such as stdio.h, stdlib .h, etc.) Will not be changed by us, if you use -mm instead to pass to GCC, the Header file surrounded by the corner parentheses will not be included. (This saves some compile time) by GCC output The rule does not contain a command section; you can write your command or not write, let Make use its implicit rules (refer to the following 2.4). 2.3 Makefile variables mentioned in Makefiles mainly included some Rules. The other things they contain are variable definitions. The variables in the makefile are like an environment variable. In fact, environmental variables are interpreted as Make's variables in the Make process. These variables are sensitive, Usually use uppercase letters. They can be referenced almost anywhere, or they can be used to do a lot of things, such as: i), store a file name list. In the above example, the rules that generate executables include some target file names It is dependent. In this rule, the same files are delivered to the GCC as a command parameter. If you use a variable to store all the target file names, add a new target file to become simple and easier Error .ii) Store the executable file name. If your project is used in a non-GCC system, or if you want to use a different compiler, you must change all the compiler to use new compilation The name of the name. But if you use a variable instead of the compiler name, you only need to change a place, the command names of all other places have changed. III) Store the compiler flag. Suppose you want to give you all your compile commands Passing a set of identical options (case -wall -o -g); if you put this set of options into a variable, you can put this variable on all call compilers. And when you want to change the option You only need to change this variable in one place. To set a variable, you only need to write the name of this variable in a line, followed by one = number, followed by the value of this variable you want to set. In the future, you have to reference this variable, write a $ symbol, followed by the variable name surrounded in parentheses.
For example, in the following, we use the previous makefile to rewrite the variables again: === Makefile start === Objs = foo.o bar.o cc = GCC cflags = -wall -o -g myprog: $ (OBJS) $ ( CC) $ (objs) -o myprog foo.o: foo.c foo.h bar.h $ (cc) $ (cflags) -c foo.c -o foo.o bar.o: bar.c bar.h $ (Cc) $ (cflags) -c bar.c -o bar.o === makefile ends === There are also some set internal variables, which are defined according to each rule content. Three more useful variables are $ @, $ A concrete command of a CC will be: $ (cc) $ (cflags) $ (CPPFLAGS) $ (target_arch) -c $ <-o $ @ Of course you can define these variables in your own needs. That's why the code that uses GCC-M or -MM switch output can be used directly in a Makefile. 2.5 PHONY TARGETS, assuming that one of your projects will eventually generate two executable files. Your main goal is to generate two executable files, but these two files are independent of each other - if a file needs to be rebuilt, does not affect the other. You can use the "Imaginary Purpose" to achieve this effect. A illusion is almost the same as a normal purpose, but this destination file does not exist. Therefore, Make always assume that it needs to be generated. When it is updated, it will execute the command line in its rule. If you start at our makefile: All: Exec1 exec2 where Exec1 and Exec2 are two executables we are as a purpose. Make put this 'all' as its main purpose, try to update 'all' every time you execute. However, since this line does not have a command to act in a actual file called 'all' (in fact ALL does not actually be generated on the disk), this rule does not really change the status of 'all'. Since this file does not exist, Make will try to update the all rule, so check it to rely on Exec1, if EXEC2 needs to be updated, if necessary, reach our purpose. Illustrative purposes can also be used to describe a set of non-preset actions. For example, you want to delete all files generated by makefile, you can set up such rules in Makefile: VeryProg Prerequisite is that there is no other rule depending on this 'Veryclean' purpose, it will never be carried out. However, if your clear use of the command 'make verycleclean', Make will use this purpose as its primary goal, perform those RM commands. What happens if there is a virtual file on your disk? At this time, this destination file must be the latest in this rule (all relying on files are already the latest), so they don't make the user clearly command Make to re-generate it. things happen. The solution is to mark all of the ingredients (with .phony), telling make not to check if they exist on the disk, nor to find any implicit rules, directly assume that the specified purpose needs to be updated. Add the following line to the Makefile to include the rules of the above rules: ..phony: VeryClean is OK. Note that this is a special Make rule, make know .phony is a special purpose, of course, you can join any illusion you want to use in its relying on it, and Make knows that they are ingredients. 2.6 Functions Makefile The function is similar to its variables - when you use, you use a $ symbol with the bracket, function name, and the space is followed by a comma-separated parameter, and finally the end of the bracket. For example, there is a function called 'Wildcard' in GNU Make, which has a parameter, which is to expand into a list of file names that are described by its parameters, and the file is spaced apart. You can use this command below: Sources = $ (Wildcard * .c) This line generates a list of all files ending with '.c', and then stores into the variable Sources. Of course, you don't need to store the results into a variable. Another useful function is the PATSUBST (Patten Substitude, the abbreviation of matching replacement) functions. It requires three parameters - the first is a model that needs to match, the second means to replace it, the third is a word that needs to be processed by the space separated by the space. For example, dealing with variables after the above definition, OBJS = $ (PatSubst% .c,%. O, $ (Sources)) This line will process all words in the Sources list (a column file name) if it ends Is '.c', use '.o' to replace '.c'. Note that the% symbols here will match one or more characters, and it is called a 'handle' each time the string is called. In the second parameter, the% is interpreted as the handle that matches the first parameter. 2.7 A more effective makefile takes us now, we can build a quite effective Makefile. This makefile can do most of the rely on how we need it, you can use it directly in most projects without doing too much change. First we need a basic Makefile to build our program. We can let it search for the current directory, find the source file, and assume that they belong to our item, put it into a variable called Sources. Here, if all * .cc files are also included, it may be more insurance because the source code file may be a C code. Sources = $ (Wildcard * .c * .cc) Using PATSUBST, we can generate the target file name by the source file name, we need to compile these target files. If our source code file has both .c files, there are also .cc files, we need to use the embedded PATSUBST function call: OBJS = $ (PatSubst% .c,%. O, $ (PatSubst% .cc,%. O, $ (SOURCES))) The most inside of the PATSUBST called the .cc file suffixed replacement, the resulting result is replaced by the outer PATSUBST call processing, which is replaced with the .c file suffix. Now we can set up a rule to build an executable: MyProg: $ (objs) gcc -o myprog $ (objs) further rules do not necessarily need, GCC already knows how to generate the target file (Object Files). Below we can set rules that have relying on information: Depends: $ (SOURCES) GCC -M $ (Sources)> Depends here If a file called 'Depends' does not exist, or any source file is longer than an existing Depends This file is new, then a Depends file will be generated. The Depends file will contain rules for the source code file generated by GCC (Note -M Switch). Now let's let Make make these rules as part of the Makefile file. The techniques used here are like the #include system in the C language - we ask Make to include this file inClude to makefile, as follows: Include Depends gnu make, check whether the 'Depends' is updated, if not, it is used We re-produced the Depends file. It will then contain this set (new) rule into in and continue to handle the final target 'myprog'. When you see rules about MyProg, it checks if all target files are updated - using the rules in the Depends file, of course these rules are now updated. This system is actually very efficient, because whenever a source code file is changed, all source code files must be preliminary to produce a new 'Depends' file. And it is not 100% security, because when a Header file is changed, relying on information is not updated. But on the basic work, it is also quite useful. 2.8 A better makefile This is a Makefile I have designed for my most items. It should be able to do not need to modify it in most items. I mainly use it on DJGPP, which is a DOS version of the GCC compiler. So you can see the executed command name, 'alleg' package, and RM -F variables reflect this. === Makefile start === ####################### # # # gener makefile # by kpld # email: kpld@hotmail.com # # Copyright (c) 2004 Titan Kpld # all rights reserved. # 保 all copyright # # no warranty, no liability; # you.com.. # No insurance , Not responsible # 你用 用 这个 你 风 # # distribute this without giving # credit to the original author. # You can change and send this file # without need to give the author honor. # (How is your meaning?) # ################################################################################################################################################################################################################################################################################################ user setting # # Adjust the following if necessary; EXECUTABLE is the target # executable's filename, and LIBS is a list of libraries to link in # (eg alleg, stdcx, iostr, etc) You can override these on make's # command line. The following things are adjusted if needed, if needed. Executable is the target's executable file name, libs # is a list of packages that require a connection (such as Alleg, Stdcx, iostr, etc.). Of course, you can overwrite them on the command line of make, you are willing to have no problem. # Executable: = Mushroom.exe Libs: = alleg # now alter any implicit rules' variables if you like, eg: # # Now change the variables in the implicit rules you want to change, such as cflags: = -g -wall -O3 -m486 cxxflags: = $ (cflags) # The next bit checks to seehther rm is in your djgpp bin # Directory; if not it Uses del instead, but this can cause (harmless) # `File Not find 'Error Messages If you are not usning dos at all, # set the variable to something # files. # # Let's check if there is any rm command in your DJGPP command directory, if not, we use the del command instead, but It is possible to give us the 'File Not Found' this error message, this is not # 什么 大. If you are not using DOS, set it into a command that deletes files and unused. (In fact, this step is excessive in the UNIX class, but it is convenient for DOS users. Unix users can delete these 5 lines of commands.) IFNEQ ($ (Wildcard $ (djdir) /bin/rm.exe),) RM-F : = RM -F ELSE RM-F: = Del Endif # You Shouldn't Need To Change Anything Below this point. # # From here, you should not need to change anything. (I am not trustful, too NB!) Source: = $ (Wildcard * .c) $ (Wildcard * .cc) Objs: = $ (PatSubst% .c,%. O, $ (PatSubst% .cc, % .o, $ (Source))) DEPS: = $ (PatSubst% .o,%. D, $ (OBJS)) missing_deps: = $ (Filter-Out $ (Wildcard $ (DEPS)), $ (DEPS) Missing_Deps_sources: = $ (Wildcard $ (PatSubst% .D,%. C, $ (missing_deps)) / $ (patsubst% .d,%. CC, $ (missing_deps))) cppflags = -md ..phony: Everything DEPUILD EVERYTHING: $ (Executable) DEPS: $ (DEPS) OBJS: $ (OBJS) Clean: @ $ (rm-f) * .o @ $ (rm-f) * .d verycleclean: clean @ $ (RM-F) $ (Executable) Rebuild: Veryclean Everything IFNEQ ($ (Missing_Deps),) $ (Missing_Deps): @ $ (r r) $ (PATSUBST% .D,%. O, $ @) Endif - Include $ (DEPS) $ (Executable): $ (OBJS) GCC -O $ (Executable) $ (OBJS) $ (Addprefix -l, $ (LIBS)) === Makefile End === There are several places worth explaining One. First, when I define most of the variables, I use: = instead of = symbol. Its role is to immediately expand the functions and variables referred to in the definition. If you use =, the function and variable reference will remain in that, that is to say that the value of changing a variable can cause the value of other variables to be changed. For example: a = foo b = $ (a) # now B is $ (a), and $ (a) is 'foo'. A = BAR # now B is still $ (a), but its value has become 'bar'. B: = $ (a) # The value of B is 'bar'. The value of a = foo # b is still 'bar'. Make will ignore all the text behind the # symbol until the end of the line. Ifneg ... Else ... Endif System is a failure / effective tool that makes a certain part of the code in the makefile. IFEQ uses two parameters if they are the same, it is added until Else (or ENDIF, if there is no else), if the ELSE is added; if different, add a piece of ELSE to ENDIF to makefile (if there is ELSE). The use of IFNEQ is just the opposite. The 'filter-out' function uses two lists separated by spaces, which deletes the items existing in the second list in the first list. I use it to handle the DEPS list, delete all existing projects, but only those missing. I have said before, CPPFlags has some flags for passing to the pre-processor for implicit rules.