-
Notifications
You must be signed in to change notification settings - Fork 0
T1 04
We will move quickly through this next step, but will show you how to download the Git repository with the answers.
Open your Shell and navigate to a good location to store the code and run the following:
$ git clone https://github.com/betsalel-williamson/Programming-Tutorial.git
$ cd Programming-Tutorial
$ ls
You should see the folders T1
and T2
with all of the code files and answers. You can now follow along and use the tutorials folder as a good location to store your code.
This step has the following parts:
Create a file named main.c
in the directory tutorials/step-3-hello-world/1-Hello-World
and copy the following code to it.
int main ( ) {
write( 1, "Hello World", 12 );
return 0;
}
Open up a shell and change directories so that when you list the files you see the file main.c
. Type the following command to compile your code:
$ gcc -w main.c -o main
The -w
option will ignore all warnings, which for now is ok. The -o
option tells gcc that the output should be named main
. On windows you will see a file main.exe
on *nix you will see a file main
. They are equivalent files.
On Windows you can run the following:
$ main.exe
On *nix and Windows systems with Cygwin you can run the following:
$ ./main
There is a lot more going on here than can be explained in shorthand, and there are much better resources and classes to go through all of the details. We leave you to the references and the Internet if you would like to learn more about what just happened to go from code to a program.
Some good Google search terms that may help you along your way:
- Keywords reserved words
- Main function
- Macros
- Variables
- Types
- Compile warning and compile errors
- Linking and compiling
- Seg faults
- Addresses and pointers
We will first write our tests:
-
Given that we provide a single text input to our program;
When we run it;
Then the program will print the text to the Shell and exits with the value 0
-
Given that we don't provide a single text input to our program;
When we run it;
Then the program exits without printing the input and has the value 1
To execute these tests we create a file test.tcl
. This file must be in the same directory as our main.c
file. We will want to remove the old program first. We already know that our program is going to compile with gcc -w main.c -o main
.
We can run these using the exec
function in TCL and the programs rm
and gcc
.
Next is the code to run our program main
. For our first test we will spawn
our program with the argument "Hello World"
. We can use the expect
command like in the first step to see that we output the text "Hello World". Finally, we get the exit code of our program with the following code:
}
expect eof
catch wait result
if {[lindex $result 3] == 0} {
puts "Exited successfully.\n"
For our second test we only need to test that the exit code is 1.
Once your tests are complete, see that when you run them that they both fail. If test one doesn't fail, it may be because your program is already printing "Hello world". Write another test to see that when you run the main program with "Another test" that the test fails.
Here is a version of the test.tcl
file you can base your code off of:
# delete the executable file
if { [file exists main] == 1 } {
exec rm main
}
# compile the file
exec gcc -w main-fixed.c -o main
### Test 1 ###
#
# Given that we provide a single text input to our program,
# When we run it,
# Then the program will print the text to the Shell
# and exits with the value 0
#
spawn ./main "Hello World"
expect {
-nocase "hello world" {
puts "\nFound output of: Hello World\n"
}
default {
puts "\nTest failed. Expected Hello world\n"
}
}
expect eof
catch wait result
if {[lindex $result 3] == 0} {
puts "Exited successfully.\n"
} else {
puts "Test failed with code [lindex $result 3].\n"
}
### Test 2 ###
#
# Given that we don't provide a single text input to our
# program,
# When we run it,
# Then the program exits without printing the input and
# has the value 1
#
spawn ./main
expect eof
catch wait result
if {[lindex $result 3] == 1} {
puts "Exited successfully.\n"
Now that we have failing tests, we will now modify the main function to accept what is called an input argument and to output this to the Shell.
Modify your main.c
file with the following:
int main ( int argc, const char* argv[] ) {
int exit_value = -1;
if ( argc == 2 ) {
while( argv[1][i] != '\0' ) {
i = i + 1;
}
exit_value = 0
} else {
exit_value = 1;
}
return exit_value;
}
We've left an error in the code for you to fix. Once it is correct, you should see that your tests now all pass!
How does this all work? We will go through this program line by line.
We change the main function from int main()
to int main ( int argc, const char* argv[] )
so that it will accept input arguments or parameters. The variable argc
is the number of length of the array argv
. The const char* argv[]
is an array of the arguments. In C, we call char*
C strings, and the square brackets let us know that this variable is what we call an array. An array is a group of items that we can access. In C, arrays are accessed using the set of numbers that we call index numbers. Index numbers start at 0 and go until the number of items minus one. If we have a group of 5 numbers and want to access the first one we use the first index value of 0. This is a topic that you should work out on pen and paper. So wrapping up the function arguments, the variable argv
is an array of strings.
With int exit_value = -1;
we next declare a variable to hold our exit code and initialize it with a value of -1
so we can tell if our program exited unexpectedly.
Next, we check with if ( argc == 2 )
that we only have 2 arguments. We'll come back to this point of 2 arguments in a bit.
If we have two arguments, then we find the length of the text passed in with the following lines. First we create a variable to hold our place, then while the current character in the string passed in isn't the null character or \0
(this is how C lets you know that you've reached the end of your string) we increment i
:
while( argv[1][i] != '\0' ) {
i = i + 1;
}
Next we can output the string in argv
to the Shell with write(1, argv[1], i);
.
We then set the exit_value
variable which holds our exit code to 0 now that we've finished outputting the information to the Shell.
In the else
block we set the exit code to 1 because any time the program doesn't perform the print there is an assumption of error.
Finally, we return from our main function, or end our program and return the exit value with return exit_value;
.
What is in the first index location of argv
? What would happen if you set the while loop to access argv[0][i]
instead?
In the previous parts we added the -w
flag in our compile step and even ran the program from within our test file. In this part we will go through the warnings and error system for gcc, and introduce makefiles. In the following tutorial we will show an example project with our code set so we can have a version of it that will output a lot of information for development purposes and a version that will hide this information for a production ready copy of our program.
Before we get into this, we need to have a talk about why it is important to have a development and production or release version of your code. Say that you're handing in a copy of your assignment in your class and the professor asks that you output something very specific. During your process, you would like to output many different things when the program is running like the state of a certain variable, or the inclusion of a specific mode of your program. It is very important to be able to quickly remove this information and by default not include it in your compiled code. You can comment the lines of code out manually, but doing so runs the risk of forgetting a line or worse having to spend your time manually commenting and then uncommenting the lines of code back in when you want to look at it later. Why should the default mode should always be the production mode? This avoids the accident of including too much information for people that do not need it.
Welcome to GNU make or the makefile! There is a scripting language called makefile
which keeps track of all of the information that you want to tell gcc and other compiler programs to be able to convert your code or compile it into an executable. We will gloss over the details of compile and linking steps and leave it to you to learn more about this process on the Internet. For now, just copy this code.
CC = gcc
SRCS = main-fixed.c
OBJS = $(subst .c,.o,$(SRCS))
CFLAGS = -Wall -Werror -Wextra
RM = rm -f
all: main
debug: $(eval CFLAGS := -g -DDBUG)
debug: main
main: $(OBJS)
$(CC) -o main $(OBJS) $(CFLAGS)
main.o: main-fixed.c
# We don't glob because this doesn't scale for
# large projects
# %.o: %.c $(DEPS)
# $(CC) -g -c -o $@ $< $(CFLAGS)
clean:
$(RM) $(OBJS) main
Notice that we are now failing the build process as is shown:
$ make
gcc -g -DDBUG -c -o main-fixed.o main-fixed.c
gcc -o main main-fixed.o -g -DDBUG
Lets walk through this step by step:
-
$ make
is the command we typed in the Shell -
gcc -g -DDBUG -c -o main-fixed.o main-fixed.c
is the command that make will run to compile our program -
The following is a chunk that tells us there's something up with the
main
function.gcc -o main main-fixed.o -g -DDBUG
The file that we need to look at is
main.c
, we then need to look atthat means the file `main.c`, line
, column ``.implicit declaration of function 'write'
means that it found a function that we didn't explicitly tell the compiler where to find it. System functions likeprintf
,write
, are assumed to be available if you don't list them with aninclude
directive. Lastly the part[-Werror,-Wimplicit-function-declaration]
is the computer code for the type of error. -
The next few lines are the pretty version of what is at line
, column
.
We now need to add the information in our code for where the compiler should find the write function. In the next part we will get into more detail about C functions. For now, we can tell gcc where to explicitly find the write
function with an include statement at the head of our main.c
file. The include statement will tell the compiler that there are additional code files that we want to include when we make our executable.
#include <unistd.h>
Congrats on finishing this last step and for completing the first tutorial! You've now successfully written a tested program in C. Thank you for sticking with it. In the next tutorial we will look at a more complete project in C and make sure we cover all of the basic keywords and functionality of the language.
- Introduction to C - https://www.tutorialspoint.com/cprogramming/index.htm
- Another introduction to C - https://www.w3schools.in/c-tutorial/
- An explanation of the usual "Hello World!" using the printf function - http://osteras.info/personal/2013/10/11/hello-world-analysis.html
- About the write function - https://linux.die.net/man/2/write
- StackOverflow about makefiles - https://stackoverflow.com/questions/2481269/how-to-make-a-simple-c-makefile#2481326
- StackOverflow about makefiles 2 - https://stackoverflow.com/questions/1079832/how-can-i-configure-my-makefile-for-debug-and-release-builds#1080180
- Answers - https://github.com/betsalel-williamson/Programming-Tutorial/tree/main/src/T1/step-4-hello-world
- Jan 1, 2021 - Generated from m4 template
Authored by Betsalel (Saul) Williamson [email protected].
For more information about SERC visit sercpitt.weebly.com