Now that you know how to create and edit files, you can generate new programs. The most commonly used languages in the CS Department at the moment are C++, C, and Java.
The most popular C++ and C compilers are g++
and
gcc
.[35]
Although not really a Unix-specific topic, it's hard to discuss how to compile code under any operating system without a basic understanding how programs are put together.
The source code for a C++ (or C) program is contained in a number of text files called source files. Very simple programs might be contained within a single source file, but as our programs grow larger and more complicated, programmers try to keep things manageable by splitting the code into multiple source files, no one of which should be terribly long.
There are two different kinds of source files: header
files and non-header files. Header
files are generally given names ending in
“
.h
”. Non-header files are generally
given names ending in “
.cpp
” for C++
code and “
.c
” for C code.[36]
Header and non-header files are treated differently when we build programs. Each non-header file is compiled separately from the others (Figure 7.1, “Building 1 program from many files”). This helps keep the compilation times reasonable, particularly when we are fixing bugs in a program and may have changed only one or two non-header files. Only those changed files need to be recompiled.
Header files are not compiled directly, Instead, header files are
included into other source files via
#include
. In fact, when you invoke a C/C++ compiler, before
the “real” compiler starts, it runs a
pre-processor whose job is to handle the special
instructions that begin with #
. In the case of
#include
statements, the pre-processor simply grabs the
relevant header file and sticks its content into the program right at
the spot of the #include
.
This can result in a dramatic increase in the amount of code that
actually gets processed. The code shown here, for example, is pretty
basic. But the #include
statements bring in an entire
library of I/O and string-related declarations from the C++ standard
library. Here, for
example, is the output of the pre-processor for one compiler. (If you
look at the very end, you can recognize the main code for this
program.)
A header file can be #include
d from any number of
other header and non-header files. That is, in fact, the whole point of
having header files. Header files should contain
declarations of things that need to be shared by
multiple other source files. Non-header files should declare only things
that do not need to be shared.
As we go through all the compilation steps required to build a program, anything that appears in a non-header file will be processed exactly once by the compiler. Anything that appears in a header file may be processed multiple times by the compiler.
The short answer is that a header file contains shared declarations, a non-header file contains definitions and local (non-shared) declarations.
Pretty much everything that has a “name” in C++ must be declared before you can use it. Many of these things must also be defined, but that can generally be done at a much later time.
You declare a name by saying what kind of thing it is:
1 const int MaxSize; // declares a constant extern int v; // declares a variable void foo (int formalParam); // declares a function (and a formal parameter) class Bar{...}; // declares a class 5 typedef Bar* BarPointer; // declares a type name
In most cases, once you have declared a name, you can write code that uses it. Furthermore, a program may declare the same thing any number of times, as long as it does so consistently. That's why a single header file can be included by several different non-header files that make up a program - header files contain only declarations.
You define constants, variables, and functions as follows:
1 const int MaxSize = 1000; // defines a constant
2 int v; // defines a variable
3 void foo (int formalParam) {++formalParam;} // defines a function
4
A definition must be seen by the compiler once and only once in all the compilations that get linked together to form the final program. A definition is itself also a declaration (i.e., if you define something that hasn't been declared yet, that's OK. The definition will serve double duty as declaration and definition.).
When a non-header file is compiled, we get an
object-code file, usually ending in
“
.o
”. These are binary files that
are “almost” executable - for some variables and
functions, instead of the actual address of that variable/function,
they still have its name. This happens when the variable or function
is declared but not defined in that non-header file (after expansion
of #include
s by the pre-processor). That name will be
assigned an address only when a file containing a definition of that
name is compiled. And that address will only be recorded in the object
code file corresponding to the non-header source file where the name
was defined.
The complete executable program is then produced by linking all the object code files together. The job of the linker is to find, for each name appearing in the object code, the address that was eventually assigned to that name, make the substitution, and produce a true binary executable in which all names have been replaced by addresses.
Understanding this difference and how the entire compilation/build process works (Figure 7.1, “Building 1 program from many files”) can help to explain some common but confusingly similar error messages:
If the compiler says that a function is undeclared, it means that you tried to use it before presenting its declaration, or forgot to declare it at all.
The compiler never complains about definitions, because an apparently missing definition might just be in some other non-header file you are going to compile later. But when you try to produce the executable program by linking all the compiled object code files produced by the compiler, the linker may complain that a symbol is
undefined (none of the compiled files provided a definition) or is
multiply defined (you provided two definitions for one name, or somehow compiled the same definition into more than one object-code file).
For example, if you forget a function body, the linker will eventually complain that the function is undefined. If you put a variable or function definition in a .h file and include that file from more than one place, the linker will complain that the name is multiply defined.
The simplest case for each compiler involves compiling a
single-file program (or a program in which all files are combined by
#include
statements).
Example 7.1. Try This
Use an editor (e.g., emacs) to prepare the following files:
hello.cpp
#include <iostream> using namespace std; int main () { cout << "Hello from C++ !" << endl; return 0; }
hello.c
#include <stdio.h> int main () { printf ("Hello from C!\n"); return 0; }
To compile and run these, give the commands:
g++ -g hello.cpp
ls
Notice that a file a.out
has been
created.
./a.out
gcc -g hello.c
./a.out
The compiler generates an executable program called
a.out
. If you don't like that name, you can use the
mv
command to rename it.
Alternatively, use a -o
option to specify the
name you would like for the compiled program:
g++ -g -o hello1 hello.cpp
./hello1
gcc -g -o hello2 hello.c
./hello2
In the example above, we placed "./" in front of the file name of our compiled program to run it. In general, running programs is no different from running ordinary Unix commands. You just type
pathToProgramOrCommand
parameters
In fact, almost all of the "commands" that we have used in this course are actually programs that were compiled as part of the installation of the Unix operating system.
As we have noted earlier, we
don't usually give the command/program name as a lengthy file path. We
say, for example, "ls
" instead of
"/bin/ls
". That works because certain directories,
such as /bin
, are automatically searched for a program of
the appropriate name. This set of directories is referred to as your
execution path. Your account was set up so that
the directories holding the most commonly used Unix commands and
programs are already in the execution path.[37] You can see your path by giving the command
echo $PATH
One thing that you will likely find is that your
$PATH
probably does not include ".
",
your current directory. Placing the current directory into the $PATH is
considered a (minor) security risk, but that means that, if we had
simply typed "a.out
" or
"hello
", those programs would not have been found
because the current directory is not in the search path. Hence, we gave
the explicit path to the program files, "./a.out
"
and "./hello
".
A typical program will consist of many .cpp
files.
(See Figure 7.1, “Building 1 program from many files”) Usually, each class or group of
utility functions will have their definitions in a separate
.cpp
file that defines everything declared in the
corresponding .h
file. The .h
file can then be
#include
d by many different parts of the program that use
those classes or functions, and the .cpp
file can be
separately compiled once, then the resulting object code file is
linked together with the object code from other
.cpp
files to form the complete program.
Splitting the program into pieces like this helps, among other things, divide the responsibility for who can change what and reduces the amount of compilation that must take place after a change to a function body.
When you have a program consisting of multiple files to be
compiled separately, add a -c
option to each compilation.
This will cause the compiler to generate a .o
object code
file instead of an executable. Then invoke the compiler on all the
.o
files together without the -c
to link them
together and produce an executable:
g++ -g -c file1.cpp g++ -g -c file2.cpp g++ -g -c file3.cpp g++ -g -o programName file1.o file2.o file3.o
(If there are no other .o files in that directory, the last command
can often be abbreviated to “
g++ -o programName -g
*.o
”.) The same procedure works for the gcc
compiler as well.
Actually, you don't have to type separate compilation commands for each file. You can do the whole thing in one step:
g++ -g -o programName file1.cpp file2.cpp file3.cpp
But the step-by-step procedure is a good habit to get into. As you
begin debugging your code, you are likely to make changes to only one
file at a time. If, for example, you find and fix a bug in
file2.cpp
, you need to only recompile that file and relink:
g++ -g -c file2.cpp g++ -g -o programName file1.o file2.o file3.o
Example 7.2. Try This
Use an editor (e.g., emacs) to prepare the following files:
hellomain.cpp
#include <iostream> #include "sayhello.h" using namespace std; int main () { sayHello(); return 0; }
sayhello.h
#ifndef SAYHELLO_H #define SAYHELLO_H void sayHello(); #endif
sayhello.cpp
#include <iostream>; #include "sayhello.h" using namespace std; void sayHello() { cout << "Hello in 2 parts!" << endl; }
To compile and run these, give the commands:
g++ -g -c sayhello.cpp
g++ -g -c hellomain.cpp
ls
g++ -g -o hello1 sayhello.o hellomain.o
ls
./hello1
Note, when you do the first ls
, that the first two
g++
invocations created some .o
files.
Alternatively, you can compile these in one step. Give the command
rm hello1 *.o
ls
just to clean up after the previous steps, then try compiling this way:
g++ -g -o hello2 hellomain.cpp sayhello.cpp
ls
./hello2
An even better way to manage multiple source files is to use the
make
command.
Another useful option in these compilers is -D
. If
you add an option
-D
,
then all occurrences of the identifier name in the
program will be replaced by value. This can be
useful as a way of customizing programs without editing them. If you use
this option without a value,
name
=value
-D
, then the compiler still
notes that name has been “defined”.
This is useful in conjunction with compiler directive
name
#ifdef
, which causes certain code to be compiled only if a
particular name is defined. For example, many programmers will insert
debugging output into their code this way:
⋮ x = f(x, y, z); #ifdef DEBUG cerr << "the value of X is: " << x << endl; #endif y = g(z,x); ⋮
The output statement in this code will be ignored by the
compiler unless the option -DDEBUG
is included in the
command line when the compiler is run.[38]
Sometimes your program may need functions from a
previously-compiled library. For example, the sqrt
and
other mathematical functions are kept in the
“
m
” library (the filename is actually
libm.a
). To add functions from this library to your
program, you would use the “
-lm
” option. (The
“m” in “
-lm
” is the library
name.) This is a linkage option, so it goes at the end of the command:
g++ -g -c file1.cpp g++ -g -c file2.cpp g++ -g -c file3.cpp g++ -g -o programName file1.o file2.o file3.o -lm
The general form of gcc/g++ commands is
g++ compilation-option
files
linker-options
Here is a summary of the most commonly used options[39] for gcc/g++:
Compilation Flags | |
---|---|
-c | compile only, do not link |
-o filename | Use filename as the name of the compiled program |
-Dsymbol=value | Define symbol during compilation. |
-g | Include debugging information in compiled code (required if you want to be able to run the gdb debugger). |
-O | Optimize the compiled code (produces smaller, faster programs but takes longer to compile) |
-I directory | Add directory to the list
of places searched when a ``system'' include
(#include … ) is encountered.
|
Linkage Flags | |
-L directory | Add directory to the list of places searched for pre-compiled libraries. |
-llibname | Link with the precompiled library lib
|
Unfortunately, once you start writing your own code, you will almost certainly make some mistakes and get some error messages from the compiler.
This is likely to lead to two problems: reading the messages, and understanding the messages.
Unless you are a far better programmer than I, you will not only get error messages, you will get so many that they overflow your telnet/xterm window.
How you handle this problem depends upon what command shell you are running, but there are two general approaches. You can use redirection and pipes to send the error messages somewhere more convenient, or you can use programs that try to capture all output from a running program (i.e., the compiler).
We've talked before about how many Unix commands are “filters”, working from a single input stream and producing a single output stream. Actually, there are 3 standard streams in most operating systems: standard input, standard output, and standard error. These generally default to the keyboard for standard input and the screen for the other two, unless either the program or the person running the program redirects one or more of these streams to a file or pipes the stream to/from another program.
Pipes and redirection
We introduced pipes and redirection earlier. The complicating factor here is that what you want to pipe or redirect is not the standard output stream, but the standard error stream. So, for example, doing something like
g++ myprogram.cpp > compilation.log
or
g++ myprogram.cpp | more
won't work, because these commands are only redirecting the standard output stream. The error messages will continue to blow on by.
How you pipe or redirect the standard error stream depends on the shell you are running:
The >
and |
symbols can
be modified to affect the standard error stream by
appending a '&' character. So these commands do
work:
g++ myprogram.cpp >& compilation.log g++ myprogram.cpp |& more
A useful program in this regard is tee
,
which copies its standard input both into the standard
output and into a named file:
g++ myprogram.cpp |& tee compilation.log
The sequence "2>&1
" in a command
means "force the standard error to go wherever the
standard output is going". So we can do any of the
following:
g++ myprogram.cpp 2>&1 > compilation.log g++ myprogram.cpp 2>&1 | more
and we can still use tee
:
g++ myprogram.cpp 2>&1 | tee compilation.log
Capturing compiler output
There are other ways to capture the output of a compiler (or
of any running program). You can run the compiler within the emacs
editor, which then gives you a simple command to move from
error message to error message, automatically bringing up the
source code file on the line cited in the error message. This
works with both the Unix and the MS Windows ports of emacs, and is
the technique I use myself. vim
, the “vi
improved” editor will do the same.
Finally, in Unix there is a command called
"script
" that causes all output to your screen to be
captured in a file. Just say
script log.txt
and all output to your screen will be copied into log.txt until you say
exit
script
output can be kind of ugly, because it includes
all the control characters that you type or that your programs use
to control formatting on the screen, but it's still
useful.[40]
Cascading One thing to keep in mind is that errors, especially errors in declarations, can cascade, with one “misunderstanding” by the compiler leading to a whole host of later messages. For example, if you meant to write
string s;
but instead wrote
strng s;
you will certainly get an error message for the unknown symbol
strng
. However, there's also the factor that the compiler
really doesn't know what type s
is supposed to be. Often
the compiler will assume that any symbol of unknown type is supposed
to be an int
. So every time you subsequently use
s
in a “string-like” manner, e.g.,
s = s + "abcdef";
or
string t = s.substring(k, m);
the compiler will probably issue further complaints. Sometimes, therefore, it's best to stop after fixing a few declaration errors and recompile to see how many of the other messages need to be taken seriously.
Backtracking A compiler can only report where it detected a problem. Where you actually committed a mistake may be someplace entirely different.
The vast majority of error messages that C++ programmers will see are
syntax errors (missing brackets, semi-colons, etc.)
undeclared symbols
undefined symbols
type errors (usually “cannot find a matching function” complaints)
const errors
Let's look at these from the point of view of the compiler.
Assume that the compiler has read part, but not all, of your program. The part that has just been read contains a syntax error. For the sake of example, let's say you wrote:
x = y + 2 * x // missing semi-colon
Now, when the compiler has read only the first line, it can't tell that anything is wrong. That's because it is still possible, as far as the compiler knows, that the next line of source code will start with a “;” or some other valid expression. So the compiler will never complain about this line.
If the compiler reads another line, and discovers that you had written:
x = y + 2 * x // missing semi-colon
++i;
it still won't conclude that there's a missing semi-colon. For all it knows, the “real” mistake might be that you meant to type “+” instead of “++”.
Now, things can be much worse. Suppose that inside a file
foo.h
you write
class Foo { Foo(); int f(); // missing };
and inside another file, bar.cpp
, you write
#include "foo.h"
int g() {...}
void h(Foo) {...}
int main() {...}
Where will the error be reported? Probably on the very last
line of bar.cpp
! Why? Because until then, it's
still possible, as far as the compiler knows, for the missing
“
\
;}” to come, in which case
g
, h
, and main
would just
be additional member functions of the class
Foo
.
So, with syntax errors, you know only that the real
mistake occurred on the line reported or
earlier, possibly in an
earlier-#include
'd file.
When you forget to declare or define a type, variable, function, or other symbol, the compiler doesn't discover that anything is wrong until you try to use the unknown symbol. That, of course, may be far removed from the pace where you should have declared it.
When you use the wrong object in an expression or try to apply the wrong operator/function to an object, the compiler may detect this as a type mismatch between the function and the expression supplied as the parameter to that function. These messages seem to cause students the most grief, and yet the compiler is usually able to give very precise descriptions of what is going wrong. The line numbers are usually correct, and the compiler will often tell you exactly what is going wrong. That explanation, however, may be quite lengthy, for three reasons:
Type names, especially when templates are involved, can be very long and messy-looking.
Because C++ allows overloading, there may be many functions with the same name. The compiler will have to look at each of these to see if any one matches the parameter types you supplied. Some compilers report on each function tried, explaining why it didn't match the parameters in the faulty call.
If the function call was itself produced by a template instantiation or an inline function, then the problem is detected at the function call (often inside a C++ standard library routine) but the actual problem lies at the place where the template was used/instantiated. So most compilers will list both the line where the error was detected and all the lines where templates were instantiated that led to the creation of the faulty call.
So, to deal with these, look at the error message on the faulty function call. Note what function/operator name is being complained about. Then look at the line where the faulty call occurred. If it's inside a template or inline function that is not your own code, look back through the “instantiated from” or “called from” lines until you get back into your own code. That's probably where the problem lies.
Here's an example taken from a student's code:
g++ -g -MMD -c testapq.cpp
/usr/local/lib/gcc-lib/sparc-sun-solaris2.7/2.95.2/../../../../include/g++-3/
stl_relops.h: In function `bool operator ><_Rb_tree_iterator<pair<const
PrioritizedNames,int>,pair<const PrioritizedNames,int> &,pair<const
PrioritizedNames,int> *> >(const _Rb_tree_iterator<pair<const
PrioritizedNames,int>,pair<const PrioritizedNames,int> &,pair<const
PrioritizedNames,int> *> &, const _Rb_tree_iterator<pair<const
PrioritizedNames,int>,pair<const PrioritizedNames,int> &,pair<const
PrioritizedNames,int> *> &)':
adjpq.h:234: instantiated from `adjustable_priority_queue<
PrioritizedNames,map<PrioritizedNames,int,CompareNames,allocator<int> >,
ComparePriorities>::percolateDown(unsigned int)'
adjpq.h:177: instantiated from `adjustable_priority_queue<PrioritizedNames,
map<PrioritizedNames,int,CompareNames,allocator<int> >,
ComparePriorities>::makeHeap()'
adjpq.h:84: instantiated from here
/usr/local/lib/gcc-lib/sparc-sun-solaris2.7/2.95.2/../../../../include/
g++-3/stl_relops.h:43: no match for `const _Rb_tree_iterator<pair<const
PrioritizedNames,int>,pair<const PrioritizedNames,int> &,pair<const
PrioritizedNames,int> *> & < const _Rb_tree_iterator<pair<const
PrioritizedNames,int>,pair<const PrioritizedNames,int> &,pair<const
PrioritizedNames,int> *> &'
Now, that may look intimidating, but that's mainly because of the long type names (due to template use) and the long path names to files from the C++ standard library. Let's strip that down to the essentials:
g++ -g -MMD -c testapq.cpp
stl_relops.h: In function `bool operator >:
adjpq.h:234: instantiated from `percolateDown(unsigned int)'
adjpq.h:177: instantiated from `makeHeap()'
adjpq.h:84: instantiated from here
stl_relops.h:43 no match for ... < ...
(This one is actually worse than most error messages, because it's
easy to miss the “
<
” operator
amidst all the <…>
template markers.)
The problem is a “no match for” a less-than
operator call in line 43 of a template within the standard
library file stl_relops.h
. But that template is
instantiated from the student's own code (adjpq.h) and so the
thing to do is to look at those three lines (234, 177, and 84)
for a data type that is supposed to support a less-than
operator, but doesn't.
Technically, “const”-ness is part of a type, so while sometimes these get special messages of their own, often they masquerade as ordinary type errors and must be interpreted in the same way.
Java programs get compiled into object code for an imaginary CPU called the “Java Virtual Machine” (JVM). Consequently, you can't execute compiled Java code directly. You must run a program that simulates a JVM and let that simulated computer execute the Java code.
That may seem a little convoluted, but the JVM simulator is easier to write than a “true” compiler. Consequently, JVM simulators can be built into other programs (such as web browsers), allowing Java code compiled on one machine to be executed on almost any other machine. By contrast, a true native-code compiler (e.g., g++) produces executables that can only be run on a single kind of computer.
The command to compile Java code is
“
javac
” (“c” for compiler) and the
command to execute compiled Java code is “
java
”.
So a typical sequence to compile and execute a single-file Java program
would be
javac -g MyProgram.java
java MyProgram
Unlike most programming languages, Java includes some important restrictions on the file names used to store source code.
Java source code is stored in files ending with the extension
“
.java
”.
Each Java source code file must contain exactly one public class declaration.
The base name of the file (the part before the extension) must be the same (including upper/lower case characters) as the name of the public class it contains.
So the command
javac -g MyProgram.java
compiles a file that must contain the code:
public class MyProgram ...
The output of this compilation will be a file named
MyProgram.class
(and possibly some other .class
files as well).
If we have a program that consists of multiple files, we can simply compile each file in turn:
javac -g MyProgram.java
javac -g MyADT.java
but this might not be necessary. If one Java file
imports another, then the imported file will be automatically compiled if
no .class
file for it exists.
So, if the file MyProgram.java
looked like this
import MyADT; public class MyProgram ...
then compiling MyProgram.java
would also
compile MyADT.java
.[41]
Java programs come in two forms: applets and applications. Applets are placed on web servers and can be launched from HTML web pages so that the Java code runs whenever someone browses that page. Applications, on the other hand, are more like traditional programs that get invoked directly by the user.
To run a Java application, we use java
:
java MyProgram
which looks for a file named
MyProgram.class
. Within that file, it looks for a compiled
version of a function Main
that must have been declared this
way:
public static void main (java.lang.String[] args) { ⋮ }
and executes that function.
As Java programs get larger, programmers usually begin to group their classes into packages. Packages can also be grouped (nested) inside other packages. Again, Java has rules that cause the program's modular structure to be reflected in the file structure used to store it. If a class is declared to be inside one or more levels of nested packages, then each package name is used as a directory name when storing the source code file.
For example, if we had source code like this:
package Project.Utilities; class MyADT { ⋮
and
package Project;; import Project.Utilities.MyADT; class MyProgram { ⋮ public static void main (java.lang.String[] args) { ⋮ } }
then we would need a directory/file structure as shown in
Figure 7.2, “Sample Java Package Structure”. Inside the Project
directory
would be the MyProgram.java
file and another directory named
“Utilities”, and the MyADT.java
function goes
inside that Utilities
directory.
Now, here's the part that trips up many a Java programmer:
When you compile and execute code in Java packages, you must always do so from the directory at the top of the package structure.
The javac
compilation command takes the name of
the source code file.
The java
execution command takes the name of the
class containing the main
function.
So the compilation and execution commands would be
javac -g Project/Utilities/MyADT.java
javac -g Project/MyProgram.java
java Project.MyProgram
For projects with many packages, the compiled code is often packaged up into a single file, called a jar, with a file extension of ".jar". This makes it easy to distribute an entire program as a single file. A jar file is actually a conventional "zip" compressed archive file with a little bit of extra directory information written into a special file (called the manifest) included in the archive.
A jar file can contain multiple programs. Sometimes there is a single preferred program specified in the manifest. If so, you can execute this program by simply saying
java -jar pathToTheJarFile
If there is no preferred program or you want to execute
a program other than the preferred one, you use the normal
java command to name the class containing the desired
main
function, but use the -cp
option (described
below) to tell the VM to look inside the jar file, e.g.,
java -cp myLargeProgram.jar Project.MyProgram
As with g++, there are several options that you may choose to employ when compiling and executing Java code. Here is a summary of the most commonly used ones:
Compilation Flags | |
---|---|
-cp pathlist | Add the directories and jar files named in the pathlist
(multiple items may be separated by ':') to the list of places
searched when
import
ing other
Java source code.
|
-g | Include debugging information in compiled code (required if you want to be able to run the gdb debugger. |
-depend | Check each imported class to see if its source code has been changed since it was last compiled. If so, automatically recompile it. |
-deprecation | Check the code for features that used to be legal in Java, but are expected to become illegal in the near future. |
-O | Optimize the compiled code (produces smaller, faster programs but takes longer to compile) |
Execution Flags | |
-cp pathlist | Add the directories and jar files named in the pathlist
(multiple items may be separated by ':') to the list of places
searched when import ing other compiled Java .class
files.
|
[35] Actually, gcc
and g++
are the same
compiler being invoked with slightly different options.
[36] There are variations, particularly for C++. Other less common
endings accepted by some C++ compilers for non-header files include
“
.C
”
“
.cc
” and
“
.cxx
”.
[37] You can modify your execution path, if desired, to add additional directories.
[38] Zeil's 1st Rule of Debugging: Never remove debugging output. Just make it conditional. If you remove it, you're bound to want it again later.
Zeil's 2nd Rule of Debugging: Never leave your debugging code active when you submit your programs for grading. If the grader is using an automatic program to check the correctness of the output, unexpected output will make your program fail the tests. On the other hand, if the grader is reading the output to check its correctness, wading through extra output really ticks the grader off!
[39] A note for CygWin users: By default, CygWin gcc and g++
produce Windows “console” applications --- applications
designed to run from within a shell (bash
or the
Windows command line tool). They can, however, produce GUI
applications with windows, menus, etc., by using -l
to
link in the windowing libraries. The list of libraries involved is
rather long, so a shortcut option is provided:
-mwindows
.
Programming windowing code is a fairly involved process. I suggest getting a library that simplifies this process for beginners. The V library is a good choice, and has the additional advantage that code written for use with V can be compiled to produce either Microsoft Windows or Unix X windows programs.
[40] In particular, if you ever want to capture both the
output of some program AND the stuff you typed to produce that
output (e.g., so you can send it in an e-mail to someone
saying "what am I doing wrong here?"), then
script
is the way to go.
[41] Beware, however, after the first compilation. javac
by default only checks to see if an appropriately named
.class
file exists. If you subsequently make changes to
MyADT.java
and then recompile
MyProgram.java
, the compiler will not realize that
MyADT.java
needs to be recompiled as well.