Monday, April 30, 2007

Binary Trees: Part 1

The binary tree is a fundamental data structure used in computer science. The binary tree is a useful data structure for rapidly storing sorted data and rapidly retrieving stored data. A binary tree is composed of parent nodes, or leaves, each of which stores data and also links to up to two other child nodes (leaves) which can be visualized spatially as below the first node with one placed to the left and with one placed to the right. It is the relationship between the leaves linked to and the linking leaf, also known as the parent node, which makes the binary tree such an efficient data structure. It is the leaf on the left which has a lesser key value (ie, the value used to search for a leaf in the tree), and it is the leaf on the right which has an equal or greater key value. As a result, the leaves on the farthest left of the tree have the lowest values, whereas the leaves on the right of the tree have the greatest values. More importantly, as each leaf connects to two other leaves, it is the beginning of a new, smaller, binary tree. Due to this nature, it is possible to easily access and insert data in a binary tree using search and insert functions recursively called on successive leaves.
The typical graphical representation of a binary tree is essentially that of an upside down tree. It begins with a root node, which contains the original key value. The root node has two child nodes; each child node might have its own child nodes. Ideally, the tree would be structured so that it is a perfectly balanced tree, with each node having the same number of child nodes to its left and to its right. A perfectly balanced tree allows for the fastest average insertion of data or retrieval of data. The worst case scenario is a tree in which each node only has one child node, so it becomes as if it were a linked list in terms of speed. The typical representation of a binary tree looks like the following:
10
/ \
6 14
/ \ / \
5 8 11 18The node storing the 10, represented here merely as 10, is the root node, linking to the left and right child nodes, with the left node storing a lower value than the parent node, and the node on the right storing a greater value than the parent node. Notice that if one removed the root node and the right child nodes, that the node storing the value 6 would be the equivalent a new, smaller, binary tree.The structure of a binary tree makes the insertion and search functions simple to implement using recursion. In fact, the two insertion and search functions are also both very similar. To insert data into a binary tree involves a function searching for an unused node in the proper position in the tree in which to insert the key value. The insert function is generally a recursive function that continues moving down the levels of a binary tree until there is an unused leaf in a position which follows the rules of placing nodes. The rules are that a lower value should be to the left of the node, and a greater or equal value should be to the right. Following the rules, an insert function should check each node to see if it is empty, if so, it would insert the data to be stored along with the key value (in most implementations, an empty node will simply be a NULL pointer from a parent node, so the function would also have to create the node). If the node is filled already, the insert function should check to see if the key value to be inserted is less than the key value of the current node, and if so, the insert function should be recursively called on the left child node, or if the key value to be inserted is greater than or equal to the key value of the current node the insert function should be recursively called on the right child node. The search function works along a similar fashion. It should check to see if the key value of the current node is the value to be searched. If not, it should check to see if the value to be searched for is less than the value of the node, in which case it should be recursively called on the left child node, or if it is greater than the value of the node, it should be recursively called on the right child node. Of course, it is also necessary to check to ensure that the left or right child node actually exists before calling the function on the node.Because binary trees have log (base 2) n layers, the average search time for a binary tree is log (base 2) n. To fill an entire binary tree, sorted, takes roughly log (base 2) n * n. Lets take a look at the necessary code for a simple implementation of a binary tree. First, it is necessary to have a struct, or class, defined as a node.struct node
{
int key_value;
struct node *left;
struct node *right;
};
The struct has the ability to store the key_value and contains the two child nodes which define the node as part of a tree. In fact, the node itself is very similar to the node in a linked list. A basic knowledge of the code for a linked list will be very helpful in understanding the techniques of binary trees. Essentially, pointers are necessary to allow the arbitrary creation of new nodes in the tree.There are several important operations on binary trees, including inserting elmeents, searching for elements, removing elements, and deleting the tree. We'll look at three of those four operations in this tutorial, leaving removing elements for later. We'll also need to keep track of the root node of the binary tree, which will give us access to the rest of the data: struct node *root = 0;
It is necessary to initialize root to 0 for the other functions to be able to recognize that the tree does not yet exist. The destroy_tree shown below which will actually free all of the nodes of in the tree stored under the node leaf: tree. void destroy_tree(struct node *leaf)
{
if( leaf != 0 )
{
destroy_tree(leaf->left);
destroy_tree(leaf->right);
free( leaf );
}
}The function destroy_tree goes to the bottom of each part of the tree, that is, searching while there is a non-null node, deletes that leaf, and then it works its way back up. The function deletes the leftmost node, then the right child node from the leftmost node's parent node, then it deletes the parent node, then works its way back to deleting the other child node of the parent of the node it just deleted, and it continues this deletion working its way up to the node of the tree upon which delete_tree was originally called. In the example tree above, the order of deletion of nodes would be 5 8 6 11 18 14 10. Note that it is necessary to delete all the child nodes to avoid wasting memory. The following insert function will create a new tree if necessary; it relies on pointers to pointers in order to handle the case of a non-existent tree (the root pointing to NULL). In particular, by taking a pointer to a pointer, it is possible to allocate memory if the root pointer is NULL. insert(int key, struct node **leaf)
{
if( *leaf == 0 )
{
*leaf = malloc( sizeof( struct node ) );
leaf->left->key_value = key;
/* initialize the children to null */
leaf->left->left = 0;
leaf->left->right = 0;
}
else if(key < (*leaf)->key_value)
{
insert( key, (*leaf)->left );
}
else if(key > (*leaf)->key_value)
{
insert( key, (*leaf)->left );
}
}The insert function searches, moving down the tree of children nodes, following the prescribed rules, left for a lower value to be inserted and right for a greater value, until it reaches a NULL node--an empty node--which it allocates memory for and initializes with the key value while setting the new node's child node pointers to NULL. After creating the new node, the insert function will no longer call itself. Note, also, that if the element is already in the tree, it will not be added twice. struct node *search(int key, struct node *leaf)
{
if( leaf != 0 )
{
if(key==leaf->key_value)
{
return leaf;
}
else if(keykey_value)
{
return search(key, leaf->left);
}
else
{
return search(key, leaf->right);
}
}
else return 0;
}The search function shown above recursively moves down the tree until it either reaches a node with a key value equal to the value for which the function is searching or until the function reaches an uninitialized node, meaning that the value being searched for is not stored in the binary tree. It returns a pointer to the node to the previous instance of the function which called it.

Sunday, April 29, 2007

Debugging Strategies, Tips, and Gotchas

Debugging can be tedious and painful if you don't set up your programs to help you debug them. In the spirit of "an apple a day keeps the doctor away", this article suggests approaches to writing code that's more debuggable, how to catch problems before they start, and gives you some time-wasting gotchas to watch out for and gives you some gotchas to watch out for.
Use the Right ToolsIt should go without saying that you should always be using the best tools available; if you're hunting a segmentation fault, you want use a debugger. Anything less than that is unnecessary pain. If you're dealing with bizarre memory issues (or hard-to-diagnose segfaults), use Valgrind on Linux or Purify for Windows.
Debug the ProblemMy first instinct when debugging is to ask, "is my code too complicated?" Sometimes we'll all come up with a solution to a problem only to realize that the solution is really hard to get working. So hard, in fact, that it might be easier to solve the original problem in another way. When I see someone struggling to debug a complex mass of code, my first thought is to ask whether there's a cleaner solution. Often, once you've written bad code, you have a much better idea of what the good code should look like. Remember that just because you've written it doesn't mean you should keep it! The trick is always to decide if you're trying to solve the original problem or to solve a particular choice of solution. If it's the solution, then it's possible that your problems don't stem from the problem at all--maybe you're over-thinking the problem or trying a wrong-headed approach. For instance, I recently needed to parse a file and import some of the data into an access database to prototype an analysis tool. My first instinct was to write a Ruby script that interfaced directly with Access and inserted all of the data into the database using SQL queries. As I looked at the support for doing this in Ruby, I quickly realized that my "solution" to the problem was going to take a lot longer than the problem should have taken. I reversed course, wrote a script that just output a comma-separated value file, and had my data fully imported in about an hour.
An Aside on Bad CodePeople are often reluctant to throw out bad code that they've written and re-write it. One reason is that code that's written feels like completed work, and throwing it out feels like going backward. But when you're debugging, rewriting the code can seem more appealing because you're probably saving yourself time spent debugging by spending a bit more time coding. The trick is to avoid throwing out the baby with the bath water--remove the bad code, don't start the whole program over again (unless it's rotten to the core). Rewrite only the parts that really need it. Your second draft will probably be both cleaner and less buggy than the first, and you may avoid issues like having to go back later and rewrite the code just so that you can figure out how it was supposed to work. On the other hand, when you're absolutely sure that code that looks horrible is the right code to use, you'll want to explain your rationale in a comment so someone (or you) doesn't come back later and hack it apart.
Minimize Potential Problems by Avoiding Copy-Paste SyndromeNothing is more frustrating than to realize that you're debugging the same problem multiple times. Whenever you copy and paste large chunks of code, you leave yourself open to the unknown demons inhabiting that code. If you haven't debugged it yet, odds are that you're going to have to. And if you forgot that you copied that code somewhere else, you're probably going to be debugging the same code more than once. (There are other reasons to avoid copy-paste syndrome; even worse than debugging the same code twice is finding the bug in only one piece of copy-pasted code.) The best way to avoid copy-paste syndrome is to use functions to encapsulate as much of your repeat code as possible. Some things can't easily be avoided in C++; you're going to write a lot of for loops no matter what you're doing, so you can't abstract away the whole looping process. But if you have the same loop body in multiple places, that might be a sign that it should be pulled into a separate function. As a bonus, this makes other future changes to your code easier and allows you to reuse the function without having to find a chunk of code to copy.
When to Copy CodeAlthough copying code is usually dangerous, there are times when it may be the best choice. For instance, if you need to make small, irregular tweaks to a chunk of code, but the bulk of it needs to remain the same, then copying, pasting, and careful editing might make sense. By copying the code, you avoid the chance that you introduce new bugs by mistyping the code. It should go without saying that you should have carefully debugged the code you plan to copy before you do so! (But I said it, and I'm not even paid by the word.) The second reason to copy code is when you have long variable names and a bad text editor. The best solution is generally to get a better text editor with keyword completion.
Make Big Problems Found Late Small Problems Found Early
Testing EarlyOne advantage of pulling out code and putting it into functions is that you can then separately test those functions. This means that you can sometimes avoid debugging big problems caused by simple bugs in the original functions. Nothing is more frustrating than writing perfectly correct code given how you thought a function (or a class) worked, only to find out that it doesn't work that way. This kind of unit testing requires some discipline and a good sense of what can go wrong with your code. Another advantage of early testing--especially if you write some or all of your tests up-front, before the code--is that you'll pay more attention to the specific interface to your class. If you can't test error handling because you're using an assert instead of an exception or error code, that might be an indication that you should be using some form of error reporting rather than asserts. (Of course, this won't always be the case--there are times when you just want to verify that your asserts work correctly.) Beyond error-reporting, writing tests is the first time you can test your code's interface, which is often as valuable as testing that the code works. If the interface to your class is clunky, or your functions have impossible-to-understand, let alone remember, argument lists, it might be time to rethink what you're doing before you write the underlying code.
Compiler WarningsMany potential bugs can be caught by your compiler. Some such errors include using uninitialized variables, accidentally replacing a check for equality with an assignment in a conditional, or, in C++, errors related to mixing types such as pointers and ints. Since this has been covered before, I suggest checking out the article why you should pay attention to compiler warnings.
Printf LiesBecause I/O is usually buffered by the operating system, using printfs in your debugging process is risky. When possible, use a debugger to figure out what lines of code are the problem rather than narrowing in on the issue with code littered by printfs and cout. (And beware the stray printf that slips in during debugging and, ahem, slips into the final version.)
Flush OutputNevertheless, there are times when you actually need to keep track of some state in a log file--perhaps you simply have too much data that you need to collect, and you need the data from program start-up to the moment the bug occurs. To ensure you collect all of the data, be sure to flush it: you can use fflush in C, or output an endl in C++. fflush takes the FILE pointer you are writing into; for instance, to flush stderr, you would write fflush( stderr);
Check Your Helper FunctionsThis should be obvious, but it's easy to forget in the heat of the moment: always verify that your helper functions work, especially when seemingly simple code is failing. When possible, isolate each helper function and test it individually, then test each of its helper functions. There's nothing more frustrating than realizing that your original logic was right, but your assumption about a helper function was wrong.
When Cause Doesn't Immediately Lead to EffectEven if a helper function doesn't seem to be the immediate source of a problem, its side effects may cause latent problems. For instance, if you have a helper function that can return NULL and you pass its output into a library function dealing with C-strings, you may see the immediate cause as dereferencing a NULL pointer in strcat, but the real cause was the buggy function you wrote earlier (or the fact that you didn't check for NULL after calling it).
Remember That Code May Be Used in More Than One PlaceAnother problem that can come up when debugging is that you discover the problem appears to be the result of a particular function call, set a break point inside that function, and then discover that there are hundreds of calls to the same function throughout the code. Or worse, you don't notice this until wasting hours of time trying to figure out what's going on or thinking that the reason for the problem is that the function is being called incorrectly. (When, in fact, it's being called correctly but with different arguments than the point at which the bug occurred.) The most obvious solution is to check the call stack after hitting a break point or to set the breakpoint right before the call that is actually the problem. Unfortunately, this doesn't always help if the same call works thousands of times but fails on the 1001st call. Potential solutions include counting the number of calls to a function and then stepping through that many breakpoints set inside the function, or using a static variable as a counter.

Saturday, April 28, 2007

Makefiles

Makefiles are something of an arcane topic--one joke goes that there is only one makefile in the world and that all other makefiles are merely extensions of it. I assure you, however, that this is not true; I have written my own makefiles from time to time. In this article, I'll explain exactly how you can do it too!
Understanding Make -- BackgroundIf you've used make before, you can safely skip this section, which contains a bit of background on using make. A makefile is simply a way of associating short names, called targets, with a series of commands to execute when the action is requested. For instance, a common makefile target is "clean," which generally performs actions that clean up after the compiler--removing object files and the resulting executable. Make, when invoked from the command line, reads a makefile for its configuration. If not specified by the user, make will default to reading the file "Makefile" in the current directory. Generally, make is either invoked alone, which results in the default target, or with an explicit target. (In all of the below examples, % will be used to indicate the prompt.) To execute the default target: % make
to execute a particular target, such as clean: % make clean
Besides giving you short build commands, make can check the timestamps on files and determine which ones need to be recompiled; we'll look at this in more detail in the section on targets and dependencies. Just be aware that by using make, you can considerably reduce the number of times you recompile.
Elements of a MakefileMost makefiles have at least two basic components: macros and target definitions. Macros are useful in the same way constants are: they allow you to quickly change major facets of your program that appear in multiple places. For instance, you can create a macro to substitute the name of your compiler. Then if you move from using gcc to another compiler, you can quickly change your builds with only a one-line change.
CommentsNote that it's possible to include comments in makefiles: simply preface a comment with a pound sign, #, and the rest of the line will be ignored.
MacrosMacros are written in a simple x=y form. For instance, to set your C compiler to gcc, you might write: CC=gcc
To actually convert a macro into its value in a target, you simply enclose it within $(): for instance, to convert CC into the name of the compiler: $(CC) a_source_file.c
might expand to gcc a_source_file.c
It is possible to specify one macro in terms of another; for instance, you could have a macro for the compiler options, OPT, and the compiler, CC, combined into a compile-command, COMP: COMP = $(CC) $(OPT)
There are some macros that are specified by default; you can list them by typing % make -p
For instance, CC defaults to the cc compiler. Note that any environment variables that you have set will be imported as macros into your makefile (and will override the defaults).
TargetsTargets are the heart of what a makefile does: they convert a command-line input into a series of actions. For instance, the "make clean" command tells make to execute the code that follows the "clean" target. Targets have three components: the name of the target, the dependencies of the target, and finally the actions associated with the target: target: [dependencies]


...
Note that each command must be proceeded by a tab (yes, a tab, not four, or eight, spaces). Be sure to prevent your text editor from expanding the tabs! The dependencies associated with a target are either other targets or files themselves. If they're files, then the target commands will only be executed if any of the dependent files have changed since the last time the command was executed. If the dependency is another target, then that target's commands will be evaluated in the same way. A simple command might have no dependencies if you want it to execute all the time. For example, "clean" might look like this: clean:
rm -f *.o core
On the other hand, if you have a command to compile a program, you probably want to make the compilation depend on the source files to compile. This might result in a makefile that looks like this: CC = gcc
FILES = in_one.c in_two.c
OUT_EXE = out_executable
build: $(FILES)
$(CC) -o $(OUT_EXE) $(FILES)
Now when you type "make build" if the dependencies in_one.c and in_two.c are older than the object files created, then make will reply that there is "nothing to be done." Note that this can be problematic if you leave out a dependency! If this were an issue, one option would be to include a target to force a rebuild. This would depend on both the "clean" target and the build target (in that order). The above sample file could be amended to include this: CC = gcc
FILES = in_one.c in_two.c
OUT_EXE = out_executable
build: $(FILES)
$(CC) -o $(OUT_EXE) $(FILES)
clean:
rm -f *.o core
rebuild: clean build
Now when rebuild is the target, make will first execute the commands associated with clean and then those associated with build.
When Targets FailWhen a target is executed, it returns a status based on whether or not it was successful--if a target fails, then make will not execute any targets that depend on it. For instance, in the above example, if "clean" fails, then rebuild will not execute the "build" target. Unfortunately, this might happen if there is no core file to remove. Fortunately, this problem can be solved easily enough by including a minus sign in front of the command whose status should be ignored: clean:
-rm -f *.o core
The Default TargetTyping "make" alone should generally result in some kind of reasonable behavior. When you type "make" without specifying a target in the corresponding makefile, it will simply execute the first target in the makefile. Note that in the above example, the "build" target was placed above the "clean" target--this is more reasonable (and intuitive) behavior than removing the results of a build when the user types "make"!
Reading Someone Else's MakefileI hope that this document is enough to get you started using simple makefiles that help to automate chores or maintain someone else's work. The trick to understanding makefiles is simply to understand all of your compiler's flags--much (though not all) of the crypticness associated with makefiles is simply that they use macros that strip some of the context from an otherwise comprehensible compiler command. Your compiler's documentation can help enormously here. The second thing to remember is that when you invoke make, it will expand all of the macros for you--just by running make, it's very easy to see exactly what it will be doing. This can be tremendously helpful in figuring out a cryptic command.

Thursday, April 26, 2007

21 Ways To Promote Your Website - Part Three by Neil Stafford

In this last installment of the series, we'll take a look at the remaining seven ways you can promote your website.
Please note that the 21 ways we've looked at are by no means exhaustive and should be used to help you decide which way(s) you prefer and are comfortable using.
With that thought, let's move on to the final seven.
15) Reciprocal Links
This involves the swapping of links with other websites usually within your niche market; however, I've seen many reciprocal links on sites that aren't related.
Reciprocal link exchanges were originally used to build up your link popularity rating with the search engines, although over the last few years this has become less effective as Search Engine technology advances.
However, don't discount reciprocal links just yet.
On several of our sites, we still seek out high ranking and high traffic websites where we can both benefit from a reciprocal link exchange.
In this case, I'm not doing it for the benefit of search engine ranking but for the pure reason of traffic generation.
A link in a prominent place on a high traffic site will, by its very nature, generate traffic for you. And if you have your site set up correctly you should be able to capture the visitors name and email address.
16) Search Engine Listings or SEO
Let's get one thing straight - I am not a search engine expert by any stretch of the imagination. In fact I heard the best definition of SEO earlier last year when it was called...
"Search Engine Optimist"
However, I do understand the principles and do put in place strategies to take advantage of the natural search listings.
The easiest way is to add content to your website and link this from an index page or site map on your website.
I run many websites that are single sales letter types sites and many of them have a page rank of 3, 4 or 5.
However, behind these pages are several dozen content pages that are linked via an index page. By checking my site statistics I can see which pages are driving traffic and in many cases new sales.
When setting up a new sales letter site, I'll use PPC traffic to gauge how well it will perform and for the ones that do really well I'll spend the time adding content pages to sit behind the main site.
17) Classified Adverts
I'm not talking about classified adverts online but simple adverts in your niche market's magazines and publications.
This simple strategy has made us thousands in various niche markets and has even had the magazine editorial team contact us to see if we would like to contribute to the magazine itself.
In specialist magazines you can often place a classified ad for only a few pounds or dollars and know that it will be reaching your target audience.
The idea of these adverts is not to sell your product off the page but to drive the readers to your website and preferably a name capture page.
To do this, your advert must contain a benefit to the reader to put down the magazine, go online and type in your web address.
18) Your Own Business Stationary
If you operate in many different niche markets and are only selling digital products then this may not be ideal for you, however, if you have a small number of markets then at a minimum I'd have business cards printed with your web address on them.
In our main niche markets, we have business cards that have an offer and call to action on the back to encourage people to visit our sites.
We also have letterheads, and if we send out a physical product, we enclose an insert with the branding and an offer or another call to action for the customer to take.
19) Email Campaigns
If people are already on your email list this shouldn't be the end of your traffic driving process. By making sub lists of your main list you can then send targeted messages to drive your customer and prospects to new sales pages and offers.
20) Forums
You may already know my view of forums within the Internet Marketing arena.
However, forums in niche markets are still an ideal way to drive traffic to your website. However, PLEASE don't go and blatantly promote your site on the forums...there are rules to follow.
First of all, find the forums in your niche and spend a bit of time 'lurking' reading the posts and watching how they are answered.
After a short time, you'll get the feel of the board and if they allow any promotion of websites or if you can use a signature file at the bottom of your post.
Once you understand the rules start answering some of the questions on the board, and leave your URL and/or signature. file at the end.
My own view with non marketing forums is not to try and answer all the questions; answer only a few and answer them completely with very good advice or suggestions.
This will get you noticed more and build up credibility with the other people on the board.
21) Conference Calls
Conference calls are an ideal way to build up an email list quickly and are an excellent relationship-building tool as well.
You can either have a free to join call or have attendees pay a fee to join you. Either way I'd approach other 'players' within your market and ask them would they promote your call for you. With a paid call you can offer an affiliate deal.
The call should be on a specific topic and during the call you can make reference to several pages on your site or make a specific sales offer for attendees.
With a free call you can make the MP3 recording available afterwards encouraging your attendees to tell others about it. This will create a viral effect as the call gets passed around and in turn drive traffic back to your website.
I have an mp3 that I recorded more than 3 years ago that still drives traffic to one of my sites each and every week!
Summary
So there you have it, the conclusion of '21 Ways To Promote Your Website'. Which ones will you implement into your business?
Running a successful web business is simple, but not easy. If it was easy everyone would be doing it.
However, it is simple...
What could be simpler than having a product that people are actively looking for and letting them know where they can get it from?
About the Author
Neil Stafford is Editor and Publisher of the Internet Marketing Review the UK's longest running PRINTED Internet Marketing Newsletter. 'Test drive' the Newsletter for FREE - Visit this special web page for more information: http://www.InternetMarketingReview.com/sya

Wednesday, April 25, 2007

Advanced Makefile Tricks

Special MacrosThere are some special macros that you can use when you want fine-grained control over behavior. These are macros whose values are set based on specifics of the target and its dependencies. All special macros begin with a dolllar sign and do not need to be surrounded by parentheses:
$@$@ is the name of the target. This allows you to easily write a generic action that can be used for multiple different targets that produce different output files. For example, the following two targets produce output files named client and server respectively. client: client.c
$(CC) client.c -o $@
server: server.c
$(CC) server.c -o $@
$?The $? macro stores the list of dependents more recent than the target (i.e., those that have changed since the last time make was invoked for the given target). We can use this to make the build commands from the above example even more general: client: client.c
$(CC) $? -o $@
server: server.c
$(CC) $? -o $@
$^$^ gives you all dependencies, regardless of whether they are more recent than the target. Duplicate names, however, will be removed. This might be useful if you produce transient output (such as displaying a result to the screen rather than saving it a a file). # print the source to the screen
viewsource: client.c server.c
less $^
$+$+ is like $^, but it keeps duplicates and gives you the entire list of dependencies in the order they appear. # print the source to the screen
viewsource: client.c server.c
less $+
$>If you only need the first dependency, then $> is for you. Using $> can be safer than relying on $^ when you have only a single dependency that needs to appear in the commands executed by the target. If you start by using $^ when you have a single dependency, if you then add a second, it may be problematic, whereas if you had used $> from the beginning, it will continue to work. (Of course, you may want to have all dependencies should up. Consider your needs carefully.)
Wildcard Matching in TargetsThe percent sign, %, can be used to perform wildcard matching to write more general targets; when a % appears in the dependencies list, it replaces the same string of text throughout the command in makefile target. If you wish to use the matched text in the target itself, use the special variable $*. For instance, the following example will let you type make to build an executable file with the given name: %.c:
gcc -o $* $*.c
For this particular type of example, there is actually an even simpler way of writing this target using implicit targets--read on!
Implicit TargetsThere are some actions that are nearly ubiqutious: for instance, you might have a collection of .c files that you may wish to execute the same command for. Ideally, the name of the file would be the target; using the implicit target ".c" you can specify a command to execute for any target that corresponds to the name of a .c file (minus the .c extension). .c:
$(CC) -o $@ $@.c
This rule says that for any target that corresponds to a .c file, make should compile it using the name of the implicit target as the output, and the name plus the .c extension as the file to compile. For example, % make test_executable
would run gcc -o test_executable test_executable.c
if test_executable did not have an explicit target associated with it.
Macro ModificationSince the point of using macros is to eliminate redundant text, it should come as no surprise that it is possible to transform macros from one type into another using various macro modifications.
Replacing TextIt is possible to create a new macro based on replacing part of an old macro. For instance, given a list of source files, called SRC, you might wish to generate the corresponding object files, stored in a macro called OBJ. To do so, you can specify that OBJ is equivalent to SRC, except with the .c extension replaced with a .o extension: OBJ = $(SRC:.c=.o)
Note that this is effectively saying that in the macro SRC, .c should be replaced with .o.