Setup
There is no starter code for this lab, but you may include it in your labs repo (just make sure to add and commit as usual or you won’t be able to pull changes in the future). You can also keep it separate if you prefer.
Create a new directory for your lab, then cd
into it.
Objectives
- Learn how to use command line arguments
- Learn how to read and write files
- Learn how to create a C++ program with multiple files from scratch
Part 1: Command line arguments
Create a new file named
main.cpp
Define the usual
main
function and necessary#include
s, but instead ofint main()
, define yourmain
function as follows:int main(int argc, char *argv[]) { // argc is the number of arguments // argv is an array of c-strings containing the arguments }
This is the standard way to define
main
when you want to use command line arguments.Add a
cout
statement to print out the number of arguments, then afor
loop to print out each argument. Compile and run your program with a few different arguments to see how it works. For example:$ g++ main.cpp $ ./a.out hello world
should print:
3 ./a.out hello world
Part 2: Reading and writing files
Create a new file named
input.txt
and add some text to it. This can be anything you like, but it should be at least a few lines long. For example:This is a file. It has multiple lines. It's not very interesting.
Back in
main.cpp
,#include <fstream>
to be able to declare and useifstream
andofstream
objects. Then, declare anifstream
object and pass the name of your first argument to its constructor. For example:ifstream in(argv[1]);
Add a
while
loop to read in each line of the file using thegetline
function and print it out tocout
. At this point, your program should display the contents of whatever file you pass as the first argument. For example:$ g++ main.cpp $ ./a.out input.txt This is a file. It has multiple lines. It's not very interesting.
Add a second argument to your program, then declare an
ofstream
object and pass the name of your second argument to its constructor as in step 2. Modify yourwhile
loop to write each line to the second file instead ofcout
. At this point, your program should copy the contents of the first file to the second file. For example:$ g++ main.cpp $ ./a.out input.txt output.txt $ cat output.txt This is a file. It has multiple lines. It's not very interesting.
Part 3: A multi-file program with manual compilation
Create a new file named
util.cpp
and add the following content:#include <iostream> using namespace std; void util() { cout << "Hello from util!" << endl; }
This file contains a simple function that prints a message to the console.
Try calling the
util
function frommain
, then compile the usual way (g++ main.cpp
).The previous step shouldn’t have worked, because the compiler doesn’t know that
util
exists. Add a forward declaration tomain
by adding just the function prototype forutil
, not the whole inplementation, i.e.:#include <iostream> using namespace std; void util(); // function prototype int main() {...}
Try compiling with
g++ main.cpp
again.Okay, so that still didn’t work, but now it’s saying there’s a linker error. We need to compile both
main.cpp
andutil.cpp
into objects, then link them together. Use the following commands (note the-c
afterg++
!):$ g++ -c util.cpp # creates util.o $ g++ -c main.cpp # creates main.o $ g++ main.o util.o # creates a.out
The last step does the linking and creates the final program, so here’s where you can define a name for your program with the
-o
option as usual.Looking at your two files, there’s a fair bit of redundancy. It’s normal to keep the
#include <iostream>
andusing namespace std;
in each file that needs it, but the function prototype should be moved to a header file. Create a new file namedutil.h
and add the following content:#ifndef UTIL_H #define UTIL_H void util(); #endif // UTIL_H
Then, in
util.cpp
andmain.cpp
, delete the function prototype and add#include "util.h"
. It feels like we just did a lot more work, but doing things this way allows you to write your prototypes in just one place instead of two, reducing room for error.Recompile the program using the same commands as above. Note that the
.h
file is not compiled directly, it is just#include
ed from the.cpp
files.Define a new function named
copy
inutil.cpp
with the following declaration inutil.h
:void copy(istream &in, ostream &out);
This function should read from
in
and write toout
. Move your file reading and writing code frommain
tocopy
, then callcopy
frommain
. Recompile your program and verify that it still works.Note:
istream
andostream
are the base classes forifstream
andofstream
, respectively. This means thatcopy
can accept either anifstream
orcin
as its first argument, and either anofstream
orcout
as its second argument.
Part 4: Automating the build process with make
The process to compile and link multiple modules is annoying and error-prone, so most projects automate this in some way. A popular tool is called make
, which processes a user-defined makefile
(or Makefile
if you prefer).
In your project directory, create a new file named
makefile
and open it in Emacs.Add the following contents (notice that Emacs is syntax-aware):
# This is a makefile. Comments start with # # The first instruction needs to be the main executable testProg: main.o util.o g++ main.o util.o –o testProg # Be very careful to use tab instead of spaces for indentation! # Emacs should help you out here as well. main.o: main.cpp g++ -c main.cpp util.o: util.cpp g++ -c util.cpp
The rules of a
makefile
are defined as:target: dependency-list command command ... command
where the target is the thing to build and the dependency list is the other targets necessary to build the current target.
Test out your new file! Type
make
and press Enter. This will likely do nothing other than inform you thattestProg
is up to date. To make a change, openmain.cpp
in Emacs and add a space somewhere, then save the file (or justtouch main.cpp
to update the timestamp). Runmake
again, and this time it should list the commands it runs. Notice that it does not make the targetutil.o
, asutil.cpp
hasn’t been changed.Add compiler flags: The
make
utility runs in its own shell, so your alias tog++
(to include the various warning options) doesn’t work. Don’t believe me? Try doing the following:- Declare (but don’t initialize) a variable, e.g.
int x;
cout
your uninitialized variable- Compile with
g++
and verify that you get a warning. - Delete
main.o
, then runmake
and verify that you don’t get a warning.
Compiler flags tell the compiler how to compile your code, and warnings can be very useful for identifying problems! Furthermore, the
-ansi
flag constrains your code to the C++98 specification - if you don’t test with-ansi
, you run the risk of your code not working when you submit an assignment and I compile it.To add compiler flags to your
makefile
, modify yourg++ -c
lines as follows:testProg: main.o util.o g++ main.o util.o –o testProg # Be very careful to use tab instead of spaces for indentation! # Emacs should help you out here as well. main.o: main.cpp g++ -ansi -pedantic-errors -Wall -Wconversion -c main.cpp util.o: util.cpp g++ -ansi -pedantic-errors -Wall -Wconversion -c util.cpp
As demonstrated in class yesterday, you may need to type this manually rather than copy pasting, as the
-
character may be rendered differently. Additionally,makefile
s need TAB as indentation.- Declare (but don’t initialize) a variable, e.g.
Getting fancy with make
: Optional extras
There’s a whole lot more that can be done with make
, and makefiles “in the wild” can get pretty intense. For future projects, you’ll probably want to copy a makefile
from a previous project and modify it as needed. Here’s a few more features that you might find useful.
Variables: You can define variables in a
makefile
and use them in your commands. For example, you could define a variableCXXFLAGS
to hold your compiler flags, then use it in your commands as follows:TARGET = testProg OBJECTS = main.o util.o CXXFLAGS = -ansi -pedantic-errors -Wall -Wconversion $(TARGET): $(OBJECTS) g++ $(OBJECTS) –o $(TARGET) main.o: main.cpp util.h g++ $(CXXFLAGS) -c main.cpp util.o: util.cpp util.h g++ $(CXXFLAGS) -c util.cpp
At first glance, this seems more complicated, but it’s actually more maintainable, as you can change your compiler flags, program name, or objects in just one place instead of multiple.
We’ve also added
util.h
as a dependency formain.o
andutil.o
. This file still isn’t part of the compile command (and never should be), but putting it in the dependency list here makes sure thatmain.o
andutil.o
are recompiled ifutil.h
changes.Note: variables are defined in the format
VARIABLE_NAME = value
, and are referenced in commands as$(VARIABLE_NAME)
. They don’t need to be in all caps, but it’s a convention.Add a
clean
target: If you really want to force everything to be recompiled (a good idea before submitting an assignment, for example), you need to delete all the.o
files and the final program. You can do this manually, but while we’re automating, why not add aclean
target to yourmakefile
?- Add the following to the end of your
makefile
:This creates a new target namedclean: rm -f *.o $(TARGET)
clean
that deletes all object files and the final program. The-f
flag tellsrm
to ignore errors, so it won’t complain if the files don’t exist. - Run
make clean
and verify that it deletes the files.
- Add the following to the end of your
Clean and rebuild: If you want to force everything to be recompiled, you can run
make clean
followed bymake
. This is a bit annoying, so let’s add anall
target to ourmakefile
:all: clean $(TARGET)
This tells
make
that theall
target depends onclean
, then$(TARGET)
being completed. There’s no additional commands here.To run your new automagic rebuild, type
make all
and verify that it deletes the files and rebuilds the program.
If you’d like to delve more into what make
can do, check out the GNU Make Manual
.