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.cppDefine the usual
mainfunction and necessary#includes, but instead ofint main(), define yourmainfunction 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
mainwhen you want to use command line arguments.Add a
coutstatement to print out the number of arguments, then aforloop 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 worldshould print:
3 ./a.out hello world
Part 2: Reading and writing files
Create a new file named
input.txtand 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 useifstreamandofstreamobjects. Then, declare anifstreamobject and pass the name of your first argument to its constructor. For example:ifstream in(argv[1]);Add a
whileloop to read in each line of the file using thegetlinefunction 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
ofstreamobject and pass the name of your second argument to its constructor as in step 2. Modify yourwhileloop 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.cppand 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
utilfunction frommain, then compile the usual way (g++ main.cpp).The previous step shouldn’t have worked, because the compiler doesn’t know that
utilexists. Add a forward declaration tomainby 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.cppagain.Okay, so that still didn’t work, but now it’s saying there’s a linker error. We need to compile both
main.cppandutil.cppinto objects, then link them together. Use the following commands (note the-cafterg++!):$ g++ -c util.cpp # creates util.o $ g++ -c main.cpp # creates main.o $ g++ main.o util.o # creates a.outThe last step does the linking and creates the final program, so here’s where you can define a name for your program with the
-ooption 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.hand add the following content:#ifndef UTIL_H #define UTIL_H void util(); #endif // UTIL_HThen, in
util.cppandmain.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
.hfile is not compiled directly, it is just#includeed from the.cppfiles.Define a new function named
copyinutil.cppwith the following declaration inutil.h:void copy(istream &in, ostream &out);This function should read from
inand write toout. Move your file reading and writing code frommaintocopy, then callcopyfrommain. Recompile your program and verify that it still works.Note:
istreamandostreamare the base classes forifstreamandofstream, respectively. This means thatcopycan accept either anifstreamorcinas its first argument, and either anofstreamorcoutas 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
makefileand 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.cppThe rules of a
makefileare defined as:target: dependency-list command command ... commandwhere 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
makeand press Enter. This will likely do nothing other than inform you thattestProgis up to date. To make a change, openmain.cppin Emacs and add a space somewhere, then save the file (or justtouch main.cppto update the timestamp). Runmakeagain, and this time it should list the commands it runs. Notice that it does not make the targetutil.o, asutil.cpphasn’t been changed.Add compiler flags: The
makeutility 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; coutyour uninitialized variable- Compile with
g++and verify that you get a warning. - Delete
main.o, then runmakeand 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
-ansiflag 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++ -clines 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.cppAs demonstrated in class yesterday, you may need to type this manually rather than copy pasting, as the
-character may be rendered differently. Additionally,makefiles 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
makefileand use them in your commands. For example, you could define a variableCXXFLAGSto 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.cppAt 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.has a dependency formain.oandutil.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.oandutil.oare recompiled ifutil.hchanges.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
cleantarget: 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.ofiles and the final program. You can do this manually, but while we’re automating, why not add acleantarget to yourmakefile?- Add the following to the end of your
makefile:This creates a new target namedclean: rm -f *.o $(TARGET)cleanthat deletes all object files and the final program. The-fflag tellsrmto ignore errors, so it won’t complain if the files don’t exist. - Run
make cleanand 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 cleanfollowed bymake. This is a bit annoying, so let’s add analltarget to ourmakefile:all: clean $(TARGET)This tells
makethat thealltarget depends onclean, then$(TARGET)being completed. There’s no additional commands here.To run your new automagic rebuild, type
make alland 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
.