Starting from:

$25

CPSC 1021 –Lab 3 - Solved

 

 Fun with Structs

 
 
 

Lab Objective
●       Practice building multiple file programs

●       Build comfort with pointers of structs

●       Practice checking for bad user input

●       Build complex makefiles

●       Continue to build comfort with pointers and dynamic memory allocation.  

 

Introduction
As you’ve been working on lab invariably you’ve typed a command in incorrectly or misspelled a filename. As your projects grow, we want to move away from manually typing in the gcc/g++ compile commands and begin using automated build tools.  

For Linux-like environments, Make is a very common build tool which allows you to compile and link C/C++ programs in a more structured way. Today, we’ll begin with simple Makefiles and build to a general-purpose script which will be applicable to many future projects.

This lab consist of 2 parts; 1. build familiarity with makefiles in C; 2) work with structures. Understanding the core idea of structures will help your transition to Object Oriented Programming (OOP) when we move to C++. Todays lab will use an array of structures in the form of a pointer. Your program will be a basic collection of students with various information collected about the student.  

You must implement the given functions in the .h file.  

Part 1 

Resources
For a step by step tutorial on Makefiles, check the link here:  

https://www.tutorialspoint.com/makefile/  Also, some information of bash scripting https://www.tutorialspoint.com/unix/unix-what-is-shell.htm 

As projects get larger and include more source files, it often makes sense to logically separate them into sub directories to make them easier to manage. Even if you don’t split them up, compiling multiple source files into an executable becomes more involved and error prone process. Especially when multiple files have compiler errors.  

This is where the make command is very useful. make allows us to define a script called the Makefile which contains instructions on compiling our programs. In the root directory (Lab3) of the code listed above, create a file named Makefile by doing the command below:

touch makefile 
Note that the file MUST be named Makefile or makefile for the make command to identify it.  

Makefile Targets and Dependencies  
Makefiles are a form of bash script, which is like a programming language for controlling your Linux terminal.

It allows us to compile source files and run additional programs like echo, grep, or tar. Consider the following:

MESSAGE=HELLO WORLD! 

 all: build 

 @echo "All Done" build: 

   @echo "${MESSAGE}" 
The first thing to notice is that we defined a bash variable at the top of the script. Bash variables are simply string substitutions. Using the $() or ${} operator, we can replace the variable with the defined string. Use ${} with curly braces if you are in between double quotes. This is a pretty straightforward concept, but it’s very powerful. In this instance, we use a variable to define a message we print to the screen.  

****************************************************************************

HUGELY IMPORTANT NOTE:

Any code that belongs to a given target must be indented at least one tab character. There is a tab before both of the @echo commands above.

**************************************************************************** YOU SHOULD TYPE THE ABOVE IN THE MAKEFILE YOU CREATED WITH TOUCH

Now we move on to our first target, the default target all:   

A target is simply a named chunk of code our Makefile will try and execute. By default, Make will execute the all: target if it exists. Otherwise, it executes the first target it finds. You can also specify a target from the command line, which means that the following commands are equivalent:  

make  make all  
Next to the target name there is a white-space separated list of dependencies. When make encounters a target to execute, it will read the dependency list and perform the following actions:  

1.      If the dependency is another target, attempt to execute that target  

2.      If the dependency is a file that matches a rule, execute that rule

 

This means we can chain multiple targets together.  

Run the make command from the terminal and see what is printed to the screen:

make HELLO WORLD! 

All Done! 
Notice that the “HELLO WORLD!” is printed first… that’s because the all: target is dependent on the build: target and so it is run first.

For those wondering, echo is a command that just prints its arguments to the terminal. Including the @ symbol in front of the command prevents make from printing the command. Play around with the above file to get a feel for what we are doing. See what happens if you remove the @ symbol.

Making C Programs
The power of the makefile is in this dependency list/rule execution loop. What we want is for our build target to run our gcc compile command. This target should be dependent on our C source files. A powerful feature of make dependencies is that a target will only be executed if its dependencies have changed since the last time you called make. Again:  

A target will only be executed if its dependencies have changed since the last time you called make!  

 

Let’s just skip to an example!

# Config, just variables 

CC=gcc 

CFLAGS=-Wall -g  

LFLAGS=-lm  

TARGET=lab3  

  

# Generate source and object lists, also just string variables 

C_SRCS := src/main.c src/functions.c 

HDRS := src/functions.h 

OBJS := src/main.o src/functions.o 

 

# default target all: build 

                  @echo “All Done!” 

  

# Link all built objects  build: $(OBJS)  

                  $(CC) $(OBJS) -o $(TARGET) $(LFLAGS) 

 

# special build rule 

%.o: %.c $(HDRS)  

                  $(CC) $(CFLAGS) -c $< -o $@  
 

 

The output of running this makefile looks like this:

 

make gcc -Wall -g -c lab3.c -o lab3.o gcc -Wall -g -c src/main.c src/functions.c -o src/main.o src/functions.o gcc lab5.o src/main.o src/functions.o -o out -lm All Done
 

Take a deep breath… this is a lot so let’s go over it! First, we define some new variables using an alternative assignment operator “:=”. Nothing special here, just an assignment and string substitution. The C_SRCS and HDRS variables should make sense, but this might be the first time you’ve seen the .o files defined in the OBJS variable.   

A .o file is called an object file, and is basically one step away from machine runnable code. Open one in a text editor and look at it yourself. We use the -c gcc flag to generate these files, and they are useful as an intermediate step before producing our executable. Using this in between step, it is possible to compile source files individually to isolate compiler errors before linking.

Note that our build target depends on these object files. This means that make will look for these files in our directory, and try to create them if they are missing.

To create these object files, we define a rule or recipe:

%.o: %.cpp $(HDRS)             $(CC) $(CFLAGS) -c $< -o $@  
This rule applies to any file that ends in .o. So, when build tries to resolve the $(OBJS) dependencies, it will run the rule once for every file in our list. The % sign is a wildcard in this case, replaced by the path to the file we are processing.   

Note that this rule to make an object file also has dependencies. In this case, the object file depends on the source file of the same name and all the headers in the program. This means that if the C source or any header is changed between makes, we will then recompile the object. The compile command is straightforward except for the $< and $@ variables:  

•      $< evaluates to the first dependency (so, %.c)  

•      $@ evaluate to the name of the rule (so, %.o)  

 

This allows us to write one rule that covers all object files in our OBJS list. For every object file we need, we will run an individual compile command. Note that this rule will run if the .o file does not exist or if the dependencies have changed since last make. Basically, Make checks the “last altered” time of the C source file and the object file to see if they are different.

It might not be easy to see, but all this means is that as we work on larger projects and need to recompile, only object files which need to be updated will be recompiled. This saves us time and reduces the number of unneeded recompiles.   

Once all the object files are up to date and created, we go back to the build target and link them all together to produce our executable TARGET.   

It’s not a big deal if this doesn’t make complete sense right now, but play around with the makefile above and it should come together. At the end of the day, it just has to work!

Advanced Makefile Commands and Rules  
All this is great, but really, we just moved typing out file lists from the terminal to a file. What would be useful is if the Makefile could find our source files by itself. Linux has many useful functions to help manipulate files in our project, and we can access these tools from our Makefile. You can include conditionals as well as filters to really customize your build process. For our purposes, let’s just use the simple wildcard function:  

C_SRCS := \   $(wildcard *.c) \   $(wildcard src/*.c) \   $(wildcard src/**/*.c)  

 

HDRS := \   $(wildcard *.h) \   $(wildcard src/*.h) \   $(wildcard src/**/*.h)  
Here we are executing the wildcard command to match all files with the .c and .h endings. This includes all files in the “src” folder and all its subdirectories. The \ is used to break a command over more than 1 line.

Let’s add a new target called “which” to see what these wildcard commands do:  

which:  

 

 
 

 
@echo "FOUND SOURCES: ${C_SRCS}"  

@echo "FOUND HEADERS: ${HDRS}"  
You can run this target with “make which” and it produces output:  

make which FOUND SOURCES: lab3.c src/main.c src/functions.c FOUND HEADERS: src/functions.h
Now that we’ve successfully collected our headers and source, we need to take the list of sources and generate an appropriate list of object files. Things are about to get a little tricky:  

OBJS := $(patsubst %.c, bin/%.o, $(wildcard *.c))  OBJS += $(filter %.o, $(patsubst src/%.c, bin/%.o, $(C_SRCS)))  
Don’t panic. The first line uses the patsubst command to generate a list of strings where we replace the .c

ending with a .o ending. patsubst stands for pattern substring and replaces a pattern found in source files (such as the extension!). In this case, we use the wildcard function again so that we only process files in the root directory. Note another tweak: we’ve also tacked on a “bin/” in the front of all these object files when we did the substitution. More on that later.   

Next, we get the list of object files needed from the src directory and its children. Here we are using the filter function to exclude any results which don’t end in .o. This is necessary if we have source files in both the root directory and source directory because root directory files get processed twice.   

Don’t worry too much if this step is confusing, just know that it allows us to collect all our source, header, and object files with only 4 lines of code!   

 

The last step is to tweak our rules used in compiling object files. Remember that “bin/” we added to the beginning of all our objects? We did this so that the compiled objects are saved to their own bin/ directory instead of cluttering up our src folder.   

Erase the old object file rule and use these two rules to correctly compile our objects:  

# Catch root directory src files  bin/%.o: %.c $(HDRS)  

                  @mkdir -p $(dir $@)  

                  $(CC) $(CFLAGS) -c $< -o $@  

  

# Catch all nested directory files  bin/%.o: src/%.c $(HDRS)  

                  @mkdir -p $(dir $@)  

                  $(CC) $(CFLAGS) -c $< -o $@  
  

We simply add a bin/ to the front of the rule to match our new object file names. We also create a duplicate rule which looks for src/%.c files instead of simply %.c files. This is needed because we are no longer saving our .o files next to our source files, and the paths don’t match by default.   

There’s also a call to mkdir in these rules, which will create the bin/ directory and its subfolders if necessary. Here we use the dir command to get the directory prefix of our object file (remember the file name is the same as the rule name $@).  

Now if you run “make” you will be able to compile all your source files into objects and store those object in the bin/ directory which will be created by make. Since our original build target depends on the OBJS list, it will automagically know to use this bin/ directory to link your program together.   

The only thing we have left to do is to add a clean target and a run target to allow for fresh builds and quick execution respectively. Add these to the bottom of your Makefile:  

clean:  

       

       
rm -f $(TARGET)  rm -rf bin  
  run: build  

       
./$(TARGET)  
Again, run these targets using “make clean” or “make run”. Notice that run depends on our build target, so the program will be compiled if needed before we try and run the executable.   

Test out the makefile and run the ppm code if you wish. This Makefile is generic and will work on most C projects. For very large projects with modules and outside libraries, it is possible to generate dependency lists, include other makefiles, have conditional rules, and all manner of other witchcraft.   

  

Make is powerful, if you code in C or C++ it is one of the most important tools to learn. Other compiled languages like Java or TypeScript have similar dependency tools like Maven, Gradle, and yarn. Out in the industry they are must haves for efficient development, so keep a lookout and be ready to learn new tools to make your programming life easier!   

Tips and Tricks
Here is the completed Makefile you wrote today:

# Config 

CC=gcc 

CFLAGS=-Wall -g 

LFLAGS=-lm 

TARGET=out 

 

# Generate source and object lists 

C_SRCS := \ 

  $(wildcard *.c) \ 

  $(wildcard src/*.c) \ 

  $(wildcard src/**/*.c) 

 

HDRS := \ 

  $(wildcard *.h) \ 

  $(wildcard src/*.h) \ 

 $(wildcard src/**/*.h) 

 

OBJS := $(patsubst %.c, bin/%.o, $(wildcard *.c)) 

OBJS += $(filter %.o, $(patsubst src/%.c, bin/%.o, $(C_SRCS))) 

 all: build 

                     @echo "All Done" 

 



 

More products