0% found this document useful (0 votes)
28 views8 pages

Lecture L08 C IO Notes

C Unix Notes

Uploaded by

zouwrightnze23
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
28 views8 pages

Lecture L08 C IO Notes

C Unix Notes

Uploaded by

zouwrightnze23
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 8

Lecture 08: C, Input/Output

Functions
As a second-year subject, we assume that you are at least somewhat familiar with functions. Thus,
most of this should be revision.

Arguments
Functions take arguments. It is likely at this stage that you have a basic understanding on how to use
functions including arguments. So why am I repeating myself. Well, this is a good opportunity to do
some revision with a little bit more depth.

So lets begin with an example:


int sum(int i1, int i2)
{
int local = i1 + i2;
return local;
}

Sum is a function (well duh) which takes two integers, applies some logic, and gives the caller back
another integer. In this case, the name of the function and its return value match well (sum the
integers given). In terms of arguments, the above is an example of pass by copy. What does this mean?

Pass by Copy
Consider the following ‘troll’ function and its corresponding ‘main’ call.
int sum_troll(int i1, int i2)
{
i1 = -7;
int local = i1 + i2;
return local;
}

int main()
{
int a = 1;
int b = 2;
int c = sum_troll(a, b);
// Print everything
}

As we haven’t learned how to print in C, let’s just assume there is some code at the end of main which
prints the values of ‘a’, ‘b’ and ‘c’. Now sum_troll is an awful function. Instead of doing something
useful, it returns somethings fairly useless (i2 – 7). In our call we might expect that ‘c’ should now
contain (b – 7) which in this case is ‘-5’. All good. What about ‘a’ and ‘b’. Given we have not really
touched ‘b’, we’d expect it to remain as ‘2’. However, with ‘a’, it is a little bit more contentious. There
is a correct answer but only if we make some assumptions about how the code works because given
what we just learned in Bash, our intuitions might be completely wrong.
Above is a crude representation of what the memory might look like (the above is wrong by the way
so do not get wedded to it). Here we have ‘a’ and ‘b’ with ‘1’ and ‘2’ in them. Then we have ‘c’ which
we know evaluates to ‘-2’. Then we have a separate location for ‘i1’, ‘i2’ and ‘local’ with their
corresponding values. If we look at the above, it is clear that the value of ‘a’ never changes even though
the value ‘i1’ is set to ‘-7’ in the sum_troll function. ‘a’ and ‘i1’ simply occupy different locations in
memory (this much is true). So what is wrong with the above.

Well… ‘c’ never exists at the same time as ‘i1’, ‘i2’ or ‘local’. Also, it is not really representative to say
that ‘i1’, ‘i2’ and ‘local’ exist in the same universe as ‘a’, ‘b’ and ‘c’. At no point in the code would it be
legal to add the line:
int new_thing = a + i1;

A better way to visualise it would be the following:

This still falls short a bit. Again, ‘c’ does not exist until ‘i1’, ‘i2’ and ‘local’ have come into existence and
then disappeared back into non-existence. However, it gives a better view of scope especially the next
concept: Pass by Pointer.

Before you ask… there is no Pass by Reference… (something I find most students don’t understand
well).

Pass by Pointer
Let us revisit our sum_troll function, but this time if it were implemented in pointer-land.
int sum_troll2(int * i1, int * i2)
{
*i1 = -7;
int local = *i1 + *i2;
return local;
}

int main()
{
int a = 1;
int b = 2;
int * a_ptr = &a;
int * b_ptr = &b;
int c = sum_troll2(a_ptr, b_ptr);
// Print everything
}

Let us skip words and go straight to a picture.

To understand the picture, we need to recall that pointers contain addresses of things. I have only
added addresses to the two bits of memory we care about (‘a’ and ‘b’). *a contains the address of ‘a’
and *b contains the address of ‘b’. When we copy the value in the pointer *a into the pointer *i1 it
will contain the same address x00. It then makes a lot of sense that when I set the value in *i1 to ‘-7’
that what I am doing is updating the value in the address x00 (i.e. ‘a’) thus meaning that ‘a’ changes
value to.

One little bit on names. It is a bit silly that we have the term ‘Pass by Copy’ and ‘Pass by Pointer’. It
makes it sound like the ‘Pass by Pointer’ is not copying anything. But it is. Just like in the copy example
above *a and *i1 aren’t the same thing. We could easily point them at different things. They are their
own pointer. So instead of copying a value, we have instead copied a pointer. Which is different… but
really we should call it ‘Pass by Pointer Copy’ </rant>.

Static Variables
Some of you may have skipped over the previous section thinking you already understand it, which is
good. Some may have already read it, which is better (revision is always good). The reason I somewhat
laboured the above revision is that the next topic ‘Static Variables’ was completely new to me (having
finished my Engineering degree… 10 years beforehand).

Let us see the example (I usually find this helps).


int weird_count()
{
static int not_local = 0;
not_local++;
return not_local;
}

int main()
{
int a = weird_count();
int b = weird_count();
// Print everything
}

Now, using my intuition, I would read this as… ‘a’ is set to the value of ‘not_local’. ‘not_local’ starts as
zero and is incremented to ‘1’. Then… ‘b’ would be the same right? WRONG! ‘b’ will be ‘2’.

By adding the static keyword, the variable not_local is not stored in the stack frame. It is instead stored
in the data section of memory. This is where you would find global variables. Given that global
variables should be treated with suspicion at all times, I am inherently suspicious of static variables
within functions.

A good way of thinking about it is that the line ‘static int not_local = 0’ is only really executed once.
When the function is called the second time, it is ignored.

One useful application of this is to count how many times a function has been called without having a
global variable somewhere else.

Static Functions
Adding static to a function (rather than a variable) is conceptually quite different. A static function can
only be called within the file in which it appears. This is useful for data-encapsulation and scope control.
You can have your own special little function hidden in your own file without worrying that you are
corrupting the global scope.

As an example, imagine you have two files:


• Duck.c
• Dog.c

If each file has a static function called ‘make_noise’, you could have separate code in each function.
When one calls ‘make_noise’ from the Dog.c file, it calls one function. When you call ‘make_noise’
from the Duck.c file, it calls the other function. This is a crude example of how you might implement
some Object-Oriented ideas into ‘C’. Of course, you would be better off using C++ than trying to make
C something that it is not.

A Word on Functions
One last concept I want to discuss before we leave functions entirely is the idea of what a function
actually is. A function should always evaluate something. Thus, a function in C or C++ that returns void
is not exactly a function in the strictest sense. Likewise, Bash functions aren’t really functions in the
strictest sense either. This is also somewhat true in C where we often use the return value simply to
return whether the function was successful or not.

However, for convenience, C and C++ void functions operate very similarly to functions by simply
returning a useless return value (void).

Input and Output


Input and Output in C are different from C++. However, the differences for most purposes are pretty
small. Instead of using cout and cin, you will find yourself using printf and scanf. These two C functions
are not quite as easy to use, but once you get the hang of them, they end up straightforward enough
that you should be able to easily convert C++ I/O into C I/O without too much trouble.

Let us begin:
printf(“Hello World”);

That was not too painful. This seems easy. What if I want to print a string with a number in it?
Something akin to:
cout << “a is “ << a << endl; // C++ code

Well, in C it is a bit more finicky. Let us first look at the function signature for printf for clues.
int printf(const char * format, ...);

Well, now I am more confused. Is that ‘…’ actually a thing? Well, it turns out it is. It is C’s version of a
variable number of arguments. This means that printf can take as many arguments as you want (it is
a bit like a bash function…). The next intuition is that you could just add things to print in the brackets…
nope. OK… how does this work then…

Let us take a little aside into Variable Parameters.

Variable Parameters
To have a function that takes variable parameters, one must first include <stdarg.h>. Easy enough.
Next, the function signature must conform to the following:
int my_vararg_function(int i, …)

‘i' is supposed to contain the number of parameters and then the other parameters go afterwards.
This makes a lot of sense because otherwise how would the program know to continue looking for
parameters. In classic C/C++ style it would have no idea otherwise and would either always just have
1 or continue looking through random bits of memory until it segfaulted. For reference, the internal
building of a variable argument function looks something like the following. Here we establish a list,
then start counting arguments (we need to know how many). Each call to va_arg gives us another
argument and then eventually we finish counting arguments and presumably do whatever the
function is supposed to do.
#include <stdarg.h>
void example(int num, …) {
va_list valist; // This is an inbuilt type
va_start(valist, num)
va_arg(valist, int); // Indicates what type
// something is only grab one
va_arg(valist, float); // #2 arg
va_end(valist);
}

Back to printf
So far, so good. Except, printf apparently takes a char*. This seems like a violation of my trust… well,
you can interpret an integer as a char* easily enough. So the way printf resolves this apparent
contradiction is by hiding the number of parameters in the string you pass.
fprintf(“Hello World %d\n’, world_num);

The ‘%d’ is basically a flag to say “this is where I want to print my number” and ‘world_num’ is the
number you want to print. Also note that ‘\n’ is just a new-line. This gives many options as to what
kind of thing you want to print. Here are some other options:

• %d: Base10 signed int


• %f: Base10 float
• %o: Base8 signed int
• %u: Base10 unsigned int
• %c: char
• %4: Print at least 4 chars
• %.2: Print only 2 decimal places

One last thought is… why does printf return an int? Conveniently, the number returned is the number
of characters printed, or a negative number if something messed up.

scanf
As you can imagine, scanf is pretty similar to printf (like cout and cin are similar). However, reading is
a bit more nuanced than writing. When you print, you just want to spit some stuff out and have some
terminal or file contain the characters. When you read… normally you want to link what your read to
some variables in your program. Let us look at the signature again.
int scanf(const char * format, ...)

It is basically the same! Except, what goes in the ‘…’ part. It would make sense that it would be some
variables. That way we can read from the string (first argument) and place the values we read in the
‘…’ arguments. Of course, if we just blindly put in variables like ‘a’ and ‘b’ we would run into the
problems we had above with pass by copy. Simply put, they would not stick. Consider the following
call:
int a = 0;
int b = 0;
scanf(<a_mystery>, a, b)

If we call any other function and pass ‘a’ and ‘b’, then we would expect the values of ‘a’ and ‘b’ to be
completely unaffected by anything which happens in the function. scanf is no different. This means
we need to give it references instead.
scanf(<a_mystery>, &a, &b)

So what goes in the “<a_mystery>” box? Well because we are not printing anything, the only thing
that goes there are the flags we care about. A typical call might look like:
scanf(“%d%d”, &a, &b)

This would read two decimal integers into ‘a’ and ‘b’.

Like printf, scanf will return a negative number when it fails and the number of characters read on a
success.

File I/O
Reading and writing to files is quite similar to standard I/O, just with a few tweaks. Actually reading
and writing from anything in Unix is remarkably similar due to the ‘everything is a file’ design principle.

Firstly we need to open the file. Some code:


FILE * fopen( const char * filename, const char * mode);

The filename is pretty self-explanatory. The mode is a bit more nuanced. There are six options:

• r: Read
• w: Write (can create a file)
• a: Append (if you don’t want to delete what is there)
• r+: Read/Write
• w+: Read/Write (length changed to zero)
• a+: Read from start, Write from end

Now we have a few options for reading/writing to the file. We can either just ask for a single character:
int fgetc(FILE * fp )

Alternatively we can get a bunch of characters:


char * fgets(char * buf, int n, FILE * fp)

Conveniently, if we hit a ‘\n’ before we have read our ‘n’ characters, the function is smart enough to
ignore whatever comes after the newline.

Now, to write to a file is largely the same. Again, we can write one character:
int fputc(int c, FILE * fp )
Or multiple characters:
fputs(const char * c, FILE * fp)

Or even a suspiciously familiar:

fprintf(FILE * fp, const char * c, ...)

The above works like printf for files.

Having opened a file, then read and written to it, we need to finish the operation by closing the file:
int fclose(FILE *fp);

fclose is a curious function. It also returns an integer, so what does that represent? Well, on a success
(the file closed) it returns zero (sounds like Bash to me). On a failure… it returns EOF. Turns out that is
also a thing.

EOF
Quickly, EOF stands for End-of-File. It is a character which is sent when a file has been completely
finished (no characters left). It is in literal terms a ‘-1’. Interestingly, you can’t actually have a character
which is ‘-1’ because when you read a character is it is read in as an unsigned int (which are all positive).
The only way to get the ‘-1’ is through another non-character means. This may answer a question you
had which is how do you tell the console you have run out of things to type with a scanf? The answer
is “Ctrl-D” which sends the EOF character to the console and will bump you out of a scanf as it will
register that as the console running out of characters to read (which is otherwise impossible).

You might also like