Lecture 4
Last Time
-
We talked last time about arrays, contiguous locations in memory where we can store the same type of data for indexed access. There are other data structures we can build, too.
-
We also looked at how we might search and sort arrays, with algorithms like:
-
linear search
-
binary search
-
bubble sort
-
selection sort
-
insertion sort
-
merge sort
-
-
We started using basic terms to describe running time (in units of steps taken), like:
-
n2
-
n log n
-
n
-
log n
-
1
-
…
-
-
And the notation for theoretical running time includes:
-
O, worst-case running time, or upper bound
-
Ω, best-case running time, or lower bound
-
Θ, if both of those are the same
-
Swapping
-
In the C programming language,
string
is actually not a real type. Instead, the keywork is provided by the CS50 Library. -
But first, let’s think about how we might swap two liquids in two cups. We would need a third cup to hold one of the liquids. Similarly, in programming, to swap the values of two variables, we need some kind of temporary storage.
-
Let’s take a look at the
swap
function:void swap(int a, int b) { int tmp = a; a = b; b = tmp; }
-
First, we put the value of
a
into a temporary variable calledtmp
, then seta
to the value ofb
, and finally setb
to the value oftmp
, which was the original value ofa
.
-
-
Now let’s look at this program:
#include <stdio.h> void swap(int a, int b); int main(void) { int x = 1; int y = 2; printf("x is %i, y is %i\n", x, y); swap(x, y); printf("x is %i, y is %i\n", x, y); } void swap(int a, int b) { int tmp = a; a = b; b = tmp; }
-
Our
main
function here will call theswap
function, the same as what we looked at just now, and print out the values ofx
andy
before and after the swap.
-
-
But when we run this program, it doesn’t swap the values of
x
andy
inmain
. -
It turns out that, while our algorithm for swapping is correct, our implementation in code doesn’t do what we want for a subtle reason.
Strings
-
Recall that bytes in memory can be visualized as a grid, with each location having some numerical address indicating its position:
-
Our computer, or more precisely our programs written in C, have some structure to how that memory is used:
-
We’ll discuss the other areas later, but for now notice we have an area labeled
heap
at the top andstack
at the bottom. -
Before we get any further, let’s see what we can find out about addresses with
compare0.c
:#include <cs50.h> #include <stdio.h> int main(void) { // get two strings string s = get_string("s: "); string t = get_string("t: "); // compare strings' addresses if (s == t) { printf("same\n"); } else { printf("different\n"); } }
-
We get two strings from the user, but no matter what we type in, our program only prints out
different
.
-
-
Let’s try
copy.c
:#include <cs50.h> #include <ctype.h> #include <stdio.h> #include <string.h> int main(void) { // get a string string s = get_string("s: "); // copy string's address string t = s; // capitalize first letter in string if (strlen(t) > 0) { t[0] = toupper(t[0]); } // print string twice printf("s: %s\n", s); printf("t: %s\n", t); }
-
We get a string from the user,
s
, and copy it tot
. Then, only if the string is long enough, we capitalize the first letter of the string. -
Hmm,
s
andt
are printed out the same, too, with both of them capitalized even though we tried to capitalize justt
.
-
-
It turns out,
string
is just a synonym forchar *
. -
What does this mean? Well, let’s look at
compare1.c
:#include <cs50.h> #include <stdio.h> #include <string.h> int main(void) { // get two strings char *s = get_string("s: "); char *t = get_string("t: "); // compare strings for equality if (strcmp(s, t) == 0) { printf("same\n"); } else { printf("different\n"); } }
-
We’ve removed the training wheels of using
string
, and we now use a library function,strcmp
, to compare the strings, for our program to work as intended.
-
-
But that doesn’t quite explain why we can’t just compare
s
andt
. In the past, when we wrote a line likestring s = get_string("s: ");
, we were actually creating a variable in memory calleds
:s [ ]
-
Then, whatever the user typed in was stored in some bytes in memory elsewhere:
| S | t | e | l | i | o | s | \0 |
-
And since we know bytes in memory has a location, or addresses, we can return the location of the first character in the array of characters we just created:
| S | t | e | l | i | o | s | \0 | 100 101 102 103 104 105 106 107
-
Assuming that these bytes are numbered something like the above,
s
will contain the value100
, essentially pointing to the first character. And recall that we know where the string ends, thanks to the use of our NUL character,\0
. -
Now, we understand why comparing
s
andt
will always show that they’re different, since they’re two different addresses. Each time we callget_string
, it stores the input from the user in a different location in memory. Sos
might have a value like100
, whilet
has a value like300
, or wherever the second string was stored. -
So, to come full circle,
s
is not actually astring
, but achar *
, the address of a specific character. -
And in C, we call variables that store addresses of other variables pointers. (The
*
symbol indicates that a variable is a pointer to some other variable type, so we could haveint *
in addition tochar *
and others.) -
strcmp
, we can now infer, must be comparing strings character by character, by going to the addresses thats
andt
point to. -
And in
copy0
, when we created our variablet
and set it to whats
was, we were just creating another pointer that pointed to the same string in memory. So when we tried to capitalizet
, we were capitalizing the one string that boths
andt
pointed to.
Memory
-
Let’s look at a program that actually copies strings,
copy1.c
:#include <cs50.h> #include <ctype.h> #include <stdio.h> #include <string.h> int main(void) { // get a string char *s = get_string("s: "); if (!s) { return 1; } // allocate memory for another string char *t = malloc((strlen(s) + 1) * sizeof(char)); if (!t) { return 1; } // copy string into memory for (int i = 0, n = strlen(s); i <= n; i++) { t[i] = s[i]; } // capitalize first letter in copy if (strlen(t) > 0) { t[0] = toupper(t[0]); } // print strings printf("s: %s\n", s); printf("t: %s\n", t); // free memory free(t); return 0; }
-
We get a string,
s
, and make sure thats
is actually a valid string withif (!s)
. Ifget_string
failed for some reason, perhaps because the computer ran out of memory to store a really long string, then it returns a special value,NULL
(not to be confused with NUL), indicating that there is no actual location in memory thats
can point to. We can also writeif (s == NULL)
, but sinceNULL
is equal to0
, we can just writeif (!s)
. Finally,main
itself also returns anint
, to indicate whether the program as a whole worked or failed. In the event of success,0
is implicitly or explicitly returned, and in the event of failure, some non-zero number can be returned to indicate that. -
Now for
t
, we call a functionmalloc
, (short for memory allocation), which finds some amount of memory that we can use and returns an address to the beginning of a chunk of memory, that is of the size we pass in. When we get that address back, the values stored inside that newly allocated chunk of memory are garbage values, or values we didn’t set and don’t know the meaning of, since some other program might have just used it for something else before it didn’t need it anymore. -
And the amount of memory we want to allocate in this case is
(strlen(s) + 1) * sizeof(char)
, which is the number of characters ins
(plus 1 for the NUL terminator), times the size of a character, to get the total number of bytes that we want. We use thesizeof
function to get the size of a type of variable. -
We check that
t
was notNULL
, sincemalloc
could also fail and not find as much memory as we asked for. -
Now we can copy the string ourselves, one character at a time, with a familiar
for
loop. Notice that, if we usei ⇐ n
, withn = strlen(s)
, then the NUL character at the end of the string will also be copied. -
Finally, we’ll only be capitalizing
t
, and print out two different strings as we wanted.
-
-
Let’s look at
string0.c
:#include <cs50.h> #include <stdio.h> #include <string.h> int main(void) { // get a string char *s = get_string("string: "); if (!s) { return 1; } // print string, one character per line for (int i = 0, n = strlen(s); i < n; i++) { printf("%c\n", s[i]); } return 0; }
-
We get a string, check that
s
is notNULL
, and print it one character at a time withs[i]
, to get the character at each indexi
.
-
-
We can replace the loop with
printf
to read:... for (int i = 0, n = strlen(s); i < n; i++) { printf("%c\n", *(s + i)); } ...
-
Here, at each index
i
, we are adding that number tos
, to create an address with a higher value thans
, so we can get to each character in the string directly with those values. And we have to use the*
notation around that address to get the value stored at that address. -
(
*
is also used, confusingly, when declaring a variable that the variable should be a pointer. But in this case, and other cases, it is used to go to some address and read the value there.)
-
-
We can now start to slowly take away our training wheels of
get_int
by writing something like this:#include <stdio.h> int main(void) { int x; printf("x: "); scanf("%i", &x); printf("x: %i\n", x); }
-
scanf
is a function in C’s standard I/O library, that reads from the user’s keyboard. The arguments it takes are likeprintf
's, but instead of printing to the screen it stores values to variables. Here, we are telling it to look for something that matches a%i
, integer, and to store it in&x
.x
is anint
we initialized in our program, and&
gets us the address of a variable. So we are passing in the address ofx
toscanf
, so it can store the value a user types, intox
:int x [ ] 500
-
-
We need to pass in the address of
x
, which we imagined to be something like500
in the above example. -
Going back to our friend
noswap.c
, we can add lines to ourswap
function to show that it is indeed working within the function:... void swap(int a, int b) { eprinf("a is %i, b is %i\n", a, b); int tmp = a; a = b; b = tmp; eprinf("a is %i, b is %i\n", a, b); } ...
-
Let’s think back to the closeup of how memory is organized for our program:
-
The heap, at top, is where memory for
malloc
comes from. -
The stack, in the bottom, is used for functions. In fact, for our C programs, the very bottom of the stack contains a chunk of memory for our
main
function, such as any local variables or arguments. -
Then, on top of that, the next function called, such as
swap
, will have its own chunk of memory, called a stack frame: -
x
was copied intoa
, andy
was copied intob
, soswap
was working with its own copy of the variables. And onceswap
returns, that entire frame of memory is marked as free to be used again.
-
-
We now know enough to solve our problem with
swap.c
:#include <stdio.h> void swap(int *a, int *b); int main(void) { int x = 1; int y = 2; printf("x is %i, y is %i\n", x, y); swap(&x, &y); printf("x is %i, y is %i\n", x, y); } void swap(int *a, int *b) { int tmp = *a; *a = *b; *b = tmp; }
-
Now we’re passing in pointers to our
main
function’sx
andy
, and swapping their values directly.swap
takes in two addresses toint
s, and uses the*a
and*b
syntax to access and change the values at those addresses:
-
-
We can try to get a string, too:
#include <stdio.h> int main(void) { char *s; printf("s: "); scanf("%s", s); printf("s: %s\n", s); }
-
But we never set
s
to anything, so it’s an address with some random value, which means we are trying to store a string at some random place in memory which might have other important things in it!
-
-
We can create an array of 5 characters for
scanf
to use:#include <stdio.h> int main(void) { char s[5]; printf("s: "); scanf("%s", s); printf("s: %s\n", s); }
-
But this is also bad, because a longer string that is passed in will start overwriting memory after our array, that we haven’t allocated, which might be storing other things too!
-
We watch an animated video that tries to explain pointers, Pointer Fun with Binky.
-
Remember that David, when he was first learning about pointers, didn’t understand them for a while until he was in office hours and a TF walked him through a few times.
Images
-
When we zoom in on an image, we see something like this:
-
Each square is a pixel, or one solid color that’s the base unit in an image.
-
-
A black and white smiley face might be represented in binary like this:
-
With the bit
1
to represent black and0
for white, we can create an image with a grid of bits.
-
-
With many dots, and many more bits to represent different colors, we can store entire images.
-
A JPEG file is a particular type of image file, based on a standard the world once agreed upon, that stores images in a particular format. Every JPEG file starts with the same three bytes to identify its format, the values
255 216 255
. -
Those values are stored in binary, but we can also easily represent them in hexadecimal, a numbering system which uses 16 symbols instead of 10. In addition to the symbols
0-9
, we usea
,b
,c
,d
,e
, andf
, for the higher values of 10, 11, 12, 13, 14, and 15. -
The number
255
, in binary, is1111 1111
; and216
is1101 1000
. Each of those four bits, since they can hold 16 values, map perfectly to hexadecimal.1111
is 15 in decimal, orf
in hexadecimal,1101
is 13, ord
, and1000
is 8, also8
. So255
maps toff
, and216
tod8
. And it’s convention to write hexadecimal as0xff
and0xd8
. -
So the first three bytes of a JPEG file are
0xff 0xd8 0xff
. -
Our problem set this week will involve recovering images from a file of binary data, so knowing that those bytes start a JPEG file will come in useful.
-
Bitmap files, with the extension BMP, maps bits directly to pixels.
-
The headers at the beginning of bitmap files are more complicated, and look like this:
-
Files are just a sequence of bytes, and if we think of each byte as having some offset from the beginning, we can specify exactly what should be in a file for it to be valid. To write a program that reads certain types of files, we need to find documentation on those standardized file types.
-
Once we get past the fields at the beginning, we notice a repeating sequence at the end, an
RGBTRIPLE
comprised of three bytes that each represent the colors red, green, and blue. With those three colors in various amounts, we can display millions of different colors. And with oneRGBTRIPLE
per pixel, we can create images, as we’ll see in our problem set this week.
-
-
We need one more new keyword to easily represent this, a
struct
. With astruct
, we can create a more complicated data type:typedef struct { string name; string dorm; } student;
-
To represent a
student
, we might want to include two pieces of information,string name
andstring dorm
.
-
-
With this syntax, we can group any number of other data types together, and work with them, reading and writing them to disk as well.
-
In this week’s problem set, we’ll get to work with images and structs. See you there!