programmers
CS-50 का चैप्टर दो ---- How arrays store data in back-to-back portions of memory

One fish, two fish, red fish, blue fish.
Congratulations! Today is your day. You're off to great places, you're off and away.
It was a bright, cold day in April, and the clocks were striking thirteen.
Winston Smith, his chin nuzzled into his breast in an effort to escape the vile wind,
slipped quickly through the glass doors of victory mansions,
though not quickly enough to prevent a swirl of gritty dust from entering along with him.
All right, this is CS50, and this is week two.
And if we could, after this dramatic reading, a round of applause for our volunteers.
So we can now take for granted from week one that we now have a new way to express some of the ideas
that we first explored in week zero, like functions and conditionals and variables and the like.
And now we're doing in C what we used to do in Scratch.
Today what we're going to start to focus on is some real-world problems
so that we can take for granted that we have that expressiveness.
We have some tools in our toolkit and actually start to solve some real-world problems if representative thereof.
In particular, the real-world problem that we're going to start today and this week with is that of reading levels.
Odds are when growing up, you read at a certain level based on the age at which you were at.
Maybe it was first grade level or fifth grade level or tenth grade level or the like.
And that was a function of just how comfortable you were with the words in the book or words on the screen that you were reading.
What you've just heard, thanks to our volunteers, are three different reading levels that each of these three volunteers reads at.
And in fact, why don't we go ahead and hear them again and be a little more thoughtful this time
as to assess at what reading level your classmate is reading.
So let's start with Leah if you'd like to introduce yourself first.
Hi, I'm Leah. I'm a first year in Hoardy and here is my little thing.
One fish, two fish, red fish, blue fish.
So at what reading level would you say Leah reads based on her recitation thereof?
Yeah, in the front.
Kindergarten.
Kindergarten? OK, so a fairly young age.
And what makes you say kindergarten?
He is speaking in very short phrases without much complexity.
OK, very short phrases without much complexity.
And indeed, according to one scientific measure that we'll explore in this week's problem set,
indeed, we would say that Leah reads before grade one.
So kindergarten would indeed be apt.
But welcome to the stage here.
Let's move on now to Maria if you'd like to introduce yourself.
Yeah, hi, I'm Maria. I'm in Stouton thinking of applied math.
Congratulations. Today is your day.
You're off to great places. You're off and away.
Another familiar phrase perhaps at what reading level would you say Maria is?
Yeah, over here.
And what makes you say second or third grade?
OK.
So now we're starting to introduce complexities like rhyming and a bit more substance to the quote.
And indeed, based on that same measure that I described earlier,
which will involve a mathematical function that somehow analyzes what it is Maria just said,
indeed, we would conclude that she read at a third grade level or grade three.
Finally, Omar, if you'd like to introduce yourself and read once more yours.
OK.
So hi, everyone. I'm Omar.
I'm a freshman at Hurlbutt thinking of doing comps high.
And this is my reading.
It was a bright cold day in April and the clocks were striking 13.
Winston Smith, his chin nuzzled into his breast and in effort to escape the vile wind,
slipped quickly through the glass doors of victory mansions,
though not quickly enough to prevent the swirl of gritty dust from entering along with him.
All right.
So we've escalated quickly.
What reading level is Omar at, would you say?
Someone else.
What might you say or estimate?
Yes.
Right here in the front.
OK.
Eighth grade.
And what made you say that?
More complex sentences, more complex words.
And indeed, according to that same measure, this full paragraph of text now,
which indeed has even more grammar when you see it there on the screen,
would be said to be at grade 10 because of that added complexity.
So with that said, we're going to need to be able to somehow crunch these numbers
to determine given a body of text at what reading level someone is.
But in order to do that and apply any metrics to a body of text,
we're going to need to represent that text in memory using something like strings from last week.
But last week with strings, we could really just print them out
or display them wholesale on the screen.
But I think we're going to need to break down these various texts and others like it
at a finer grade level.
And indeed, among the goals for today is to explore exactly that
and also to take the proverbial hood off of the car to take a look underneath
and how the computer is actually working, how these things like strings are actually functioning.
So if you could join me one last time in a round of applause for our volunteers.
Thank you so much for helping out.
Thank you guys.
Thank you.
Thank you to Maria as well.
So among the goals for today, beyond exploring a representative problem
like this of reading levels, is going to be another one, which is even more important
and more omnipresent than reading levels, namely cryptography,
the art of scrambling information or specifically encrypting it
so you can send secure communications.
Now, you sort of take this for granted increasingly nowadays that when you send a text message
or perhaps an email or check out online with a credit card
that somehow or other your information is secure.
And for the coming weeks, we're going to explore to what extent that is actually true
and why or why not.
Now with cryptography, similarly to if we want to be able to send messages securely
such that if I want to send a message to you,
I don't want anyone else in the room to be able to figure out what it is I have said
even if they physically intercept that message, which is all too possible in a digital world,
we're going to need to come up with metrics and mechanisms for actually scrambling information
in a reversible way so that I can write my message, somehow scramble it.
You can receive that message even if after it's passed through many other hands,
and you can de-scramble or decrypt that same message.
So for instance, here on the screen is a message, a fairly simplistic one,
that has somehow been encrypted.
And we'll see by the end of today and by the end of this week
that this encrypted message, and there's a bit of a tell on the end there,
actually will be said to decrypt to this is CS50.
But why is going to be the underlying question?
And what additional tools do we need on our toolkit in order to do that?
Another word on tools.
So up until now, you've probably experienced some bugs,
whether it was in Scratch or ever more so in C.
In fact, don't feel too bad if the very first program you wrote in C didn't even work.
You couldn't even make it or compile it until you went back and fixed some of the code that you had written.
Well, it turns out that bugs, mistakes in programs, are ever so commonplace.
And even though we've already provided you with tools like the virtual rubber duck at CS50.ai,
also embedded into VS Code at CS50.dev, of whom you can ask questions along the way
among the goals today are to give you some lifelong tools at how you can actually debug software yourself
when you don't have a duck nearby, when you don't have a TA nearby,
let alone any humans at all.
So with debugging, there's going to be a number of techniques that we can use
all toward an end of finding and removing bugs or mistakes from our software.
And perhaps the person best known for having popularized this term of bugs
is that of Dr. Grace Hopper, pictured here, who was a rear admiral in the Navy
and was one of the original programmers of the so-called Harvard Mark I,
a very early mainframe computer that if you wander across the Charles River
over to the Science and Engineering Complex here at Harvard,
you can actually see part of this on display still in the lobby.
It was succeeded by the Harvard Mark II.
And on the Harvard Mark II, Dr. Hopper and her team
were known for having put this note in their log book
after having done some number crunching on the system there.
And if we zoom in, they have found a problem with the computer this one day
whereby there was literally a bug, a moth, inside of the circuitry of the computer.
And as was written here, first actual case of bug being found.
And ever since then, do we say ever more so, the phrase bug
and debugging when it comes to finding and eliminating problems in our code?
So let's start with just that.
In fact, let me go over to VS Code and let's deliberately make some mistakes together
that might very well be reminiscent of some of the mistakes you've accidentally made thus far.
But along the way, give you all the more tools for solving those problems
as opposed to sort of having to ask someone else, be it virtual or physical,
for help and actually find these mistakes in your own code.
Let me go ahead and consciously, in VS Code, create a program known to be buggy
called buggy.c.
And in this program, let's go ahead and do some fairly familiar code initially.
I'm going to go ahead and start just like we did last week with int main void,
more on that today before long.
Inside of my curly braces, I'm going to say printf, hello, comma, world.
That's it.
Now I'm going to go back to my terminal window here.
I'm going to go ahead and do make buggy to make a program from that source code.
But before I do, odds are, even after just a week of this stuff,
you can probably spot a few mistakes I've made, a few bugs.
What do you see wrong already?
Yeah?
I didn't include standard.io.h, that so-called header file,
which is important because it tells the compiler
that I plan to use functions therein, like printf, which clearly I'm doing.
So let me go in and include standard.io.h.
What else seems to be wrong here?
Yeah?
I'm missing a semicolon at the end of line five.
So I'm going to go ahead and add that in.
And this is subtle and arguably not a bug, but maybe an aesthetic detail.
What else have I done arguably wrong?
Yeah?
And back?
Yeah, I forgot my backslash and the new line character
just to move the cursor to the next line so that when I get a new prompt,
it's on a fresh line of its own.
Again, more of an aesthetic, but certainly a pretty reasonable thing to do.
So let me go ahead now.
And actually, in my terminal window, run make buggy, and it indeed compiled.
But up until then, had I not fixed those mistakes,
I would have triggered a whole bunch of bugs, a whole bunch of error messages
as a result.
In fact, let's rewind in time and undo the fixes I just made
and go back to the original form here and try running again, make buggy,
enter.
And we'll see some scary-looking messages up here.
Let me scroll up to the top of the output
here where we see buggy.c colon three, which means line three.
That's where the problem is right now.
Error.
Call to undeclared library function printf with type,
and then it starts to get a little more complicated.
But I do see clearly that it's calling my attention to printf.
So hopefully, at some point, if not last week, hopefully this week onward,
your instinct will be, oh, all right.
I mean, I forgot the header file in which printf is actually declared.
It's not a huge deal.
It's going to come with practice.
So that's how I might know in more intuitively what, in fact, the solution here might be.
Now, here's another common mistake that I've just gone in and fixed.
But I did do something wrong.
And hopefully, none of you actually did this because it's an annual FAQ.
What did I just do accidentally wrong?
So it's not studio.h.
It's standard.io.h.
So do kind of ingrain that one for standard input output.
The next, though, bug that I haven't yet fixed is that semicolon.
So let me clear my screen and rerun make buggy.
I should no longer see that first error message anymore.
But I now do see another error message on line 5, expected semicolon after expression.
All right, that one's pretty explicit.
So I'm going to go ahead and fix this.
But notice that up until now, my code wouldn't have been able to compile
because of those two error messages.
It stopped showing me by showing me these errors.
But at this point, if I run make buggy enter, it did, in fact, compile.
And yet, it's arguably still buggy.
Because when I run dot slash buggy, I get my prompt on the wrong line.
So this is a distinction now between a syntax error, something that or a programming error
that outright stops my program from compiling.
It's sort of a deal breaker versus something that's maybe more of a logical error.
I actually meant to move the cursor to the next line.
And so there's different types of errors in the world as we're seeing here.
Of course, if I rerun make buggy again, dot slash buggy, now we're back in business,
hopefully with the intention of having this display exactly that.
All right, well, let's modify it to look a little more like something else from last week.
Recall that last week, I started to get someone's name more dynamically.
So I said something like name equals get string.
And that was a function we introduced.
And I might have said something like this.
What's your name?
Question mark with a space just to move the cursor over.
I know now I definitely need to end my thought with a semicolon.
I could try and compile this.
Make buggy now.
And I'm seeing a different error message altogether that you might not have seen yet.
So on buggy.cline5, error, use of undeclared identifier, name.
What now is the mistake that I've made?
Why does it not know?
Yeah, I forgot to declare the type of this variable, which for those of you with the prior programming experience,
is not something you have to do in some languages, like Python, for instance.
But in languages like C, C++, Java, and others,
you do in fact need to explicitly tell the compiler that you want to instantiate a variable,
create a variable in the computer's memory by telling it its type.
And it's not going to be an int, because I don't want an integer, of course, in this case.
I want text, which we now know to be called string instead.
All right, I think this fixes that bug.
So let me do make buggy again.
And hopefully, huh, a fatal error this time.
Again, indicating that my code did not recompile.
On line five still, I have an error.
But this time, it says, use of undeclared identifier string, did I mean standard in?
So this is a bit of a red herring.
The compiler's trying to be helpful in saying, did I mean standard in?
But I don't think I actually do.
That just is the most similar-looking word in the compiler's own memory.
What's the actual mistake that I've made here?
Yeah?
Yeah, I didn't include the CS50 header file, because string, recall,
is a feature of the CS50 library, as is get string and get int and others.
So the solution here is indeed to go up here.
And just to be nitpicky, I tend to alphabetize my header files.
It's not strictly required technically, but stylistically.
I find it nice to be able to skim the header files alphabetically to see if something is there or not.
I can include CS50.h in addition to standard io.h.
And it's in that file, CS50.h, that not only is get string declared,
so that the compiler knows that it exists, it turns out so is the word string.
So this is a bit of a white lie.
And this is something we do in the early weeks of the class.
We dug up these old training wheels from a bicycle,
the whole idea being to sort of keep you up and avoid you having to do too much complexity early on.
The point of these training wheels in the form of the CS50 library
is to let us kind of ignore what a string really is for just another week or two,
after which we will then peel back that layer, take off those training wheels,
and reveal to you what is actually going on.
So for now, strings exist, but they exist because of the CS50 library.
In a couple of weeks, they're still going to exist,
but we're going to call them by a different name, as we'll eventually see.
But everyone in the real world, every software developer, uses the phrase string.
So this is a concept that exists.
It is not CS50 specific at all.
It's just that in C, the word string doesn't typically exist unless you make it so as we have.
All right, so I think now, if I clear my terminal window and rerun, make buggy,
now it should, in fact, compile.
And if I run dot slash buggy, enter, I should be able to type in my name.
And now, voila, hello.
So this is now not a syntax error, because I didn't screw up my code per se,
like it compiled.
Everything is grammatically correct, so to speak.
But logically, intellectually, this is not what I wanted, right?
I wanted it presumably to say hello comma David.
Let's fix one final bug here.
How do I fix this?
On what line?
How do I get it to say, yeah, hello, David?
Yeah, on line 7, I need to do the string placeholder, the format code, so to speak,
percent s, and then one more thing, someone else.
What do I do after this?
Yeah, in back?
Yeah, a comma, and then add the variable name that contains the value I want to substitute
in there, which is indeed name, though I could have called it anything I want.
All right, so now make buggy enter seems to have compiled again, dot slash buggy.
Now I type in my name once more, and now we're back in business.
So over the course of these few exercises, clearly I meant
to make most of all of these bugs, these mistakes.
But they demonstrate not only syntax errors, which
are just going to stop the compiler in its tracks.
Like you won't even be able to compile your code until you fix those things.
But even after that, there could be these latent bugs that seem to not be there until you actually provide input
and see what's actually happening at so-called run time when you're running the actual code.
And so here's where.
It's no longer as easy as just reading the error message and figuring out what it means,
because there is no error message that appeared on the screen when it said, hello, comma, world.
We had a user on human intellect and realized, OK, that's clearly not what I wanted.
Had you run CS50's own Check50 program on something like that,
we could have told you that that's not correct by automatically assessing the correctness of it.
But the compiler has no idea what you are trying to achieve logically.
It only knows about the language C itself and the requisite syntax for actually writing and compiling code.
So how could we go about solving logical problems in code?
So I would propose that we start to consider this here list.
Whereby, when you want to find a logical problem in your code
and better understand what's going on or really what's going wrong, printf is going to be your friend.
Up until now, we've used printf to literally print on the screen.
Hello, David.
Hello, Kelly or anything else on the screen.
But you can certainly use printf temporarily to just print stuff out inside of your program
that you might want to better understand.
And then once you understand it and once you've solved some problem, fine.
Then you can delete those temporary lines of code, recompile, and move on.
So let's use printf as a debugging tool in that sense.
Let me go back over to VS Code here and let me in this same program, buggy.c,
sort of delete everything and start over with a different sort of bug.
I'm going to include standard.io.h at the top.
I'm going to do int main void after that.
And inside main, I'm going to do a simple for loop that just prints out like a stack of three bricks.
Like we saw in the world of Mario, when Mario needed to, we claimed sort of jump over a stack of bricks.
We want to print out just three of those at the moment.
So I'm going to go ahead and say for int i equals zero.
i is less than or equal to three because I want three of these, i plus plus.
Then inside of this for loop, I'm going to go ahead and quite simply do printf hash symbol to represent the brick,
followed by a new line to move the cursor to the next line, semicolon to complete the thought.
Now, I've deliberately made a stupid mistake here.
But in the context of a simple enough program that we can focus on the debugging technique,
I'm not on the obscurity of the bug in question.
Hopefully you'll spot the bug in just a moment if not already.
When I do make buggy now and do dot slash buggy, I don't get three bricks.
I of course get one, two, three, four total.
So there's a logical bug in this program.
And odds are you can already spot what it is.
But let me propose that this program is representative of a type of problem
that you can solve a little more diagnostically by poking around and really asking the computer via printf to show you what's really going on.
And I would propose that one of the most helpful techniques in a situation like this,
if you're trying to wrap your mind around why are there four bricks instead of three,
well, clearly this is related to the loop somehow.
So let's look a little more thoughtfully at what the value of i is before we print out each of those bricks.
And I might literally do something like this temporarily.
Printf, quote unquote, i is percent i backslash n, close quote.
And then I could just print right here and now the value of i just so that I can actually see it.
Let me now go down into my terminal window, make buggy again, dot slash buggy.
And now, and I'll full screen my terminal, I'll get some diagnostic information at the same time.
So when i is one, I get a brick.
When i is zero, I get a brick.
When i is one, I get another brick.
When i is two, I get another brick.
When i is three, I get a fourth brick.
So now I can kind of see that, OK, my loop is working, but I'm going too far.
I'm going too long.
Now I can do this even more succinctly for what it's worth.
I don't need a whole new printf statement.
I could just go into my existing printf, put my percent i there, and then maybe just space,
just to scooch things over, and then print out i in that same line.
If I now do make buggy dot slash buggy, OK.
Now I'm seeing that I'm printing a hash, a brick,
for each value of i from i equals zero, one, two, and also three.
So the solution, of course, is that I shouldn't be starting at zero
and iterating less than or equal to three.
The solution is like, oh, I'm an idiot.
I should have said less than three.
Or if I prefer to count starting at one, like a normal person,
I could have said i equal to one, and then go up two and through three.
But as I claimed last week, the canonical way, the most common way to do this,
is start counting at zero and go up two, but not through the total value that you have in mind.
But there's going to be another technique that's worth knowing here.
Let me go ahead and sort of abstract this away
by whipping up a slightly better variant of this as follows.
Let me go ahead and delete this for loop.
Let me assume for the moment that inside of main,
I'm going to ask the user now for the height of a pyramid.
And I'm going to do something like this, int h equals get int.
And let's prompt the user for the height value of this pyramid or this wall.
And then let's go ahead and assume there exists a function called print column
who takes as input a number h, which is how many bricks you want to print.
Now, this function does not exist yet.
Print column.
Get int does exist, but I don't have access to it.
So let me not make the same mistake twice.
What do I need to add at the top of this file?
Yeah.
I need the cs50 header file because I'm using the get int function now, which
again comes from our library, not c.
So let me go ahead and include cs50.h.
But now print column, I can invent this function myself.
So let me go ahead and say void print column int height in parentheses,
more on that in just a moment.
And then I'm going to recreate the loop from before.
For int i equals 0, i is less than or equal to the height.
So I'm going to deliberately for now make that same mistake as before.
i plus plus.
And then inside of this for loop, I'm going to go ahead and print out
a single hash and a new line to represent that their brick.
So now, main can use a function called print column.
It's going to pass in the value of h.
And then this for loop in the print column function
is going to take care of printing this thing for me.
So let me do this again.
Make buggy enter so far so good dot slash buggy.
Let's put it in the height.
I'm going to say manually height of three.
And I should see three bricks.
But of course, I'm still seeing four.
Now, before we move on, let me hide my terminal
and propose that this is just kind of stylistically bad to put anything
other than your main function at the top.
But recall that if I move my helper function, print column,
and it's a helper function insofar as I made it to help me solve another problem,
I can't recompile and run my code now.
Why?
The compiler won't let me.
Yeah.
Exactly.
When the compiler gets to line seven of my code,
it's going to abort compilation because it doesn't know what print column is.
Why?
Because I don't tell it what it is until line 10.
And this was the only time I proposed that copy paste is reasonable
is to highlight and copy the very first line of that function,
paste it above main with a semicolon, and that's a so-called function prototype.
It specifies what the name of it is, what its inputs are, if any,
and what its output is, if any.
And more on these inputs and outputs later on.
But now, this is just a more complicated but more modularized version
of the same program.
Let me do make buggy, still compiles, dot slash buggy, type in three.
And I still have that same bug.
But the catch now is that my code has gotten more complicated.
And the point of my having abstracted away this idea of printing a column
into a new function is that there's just more code now to debug.
I could certainly go in there and start adding printfs.
But at some point, printf is going to be a very primitive tool.
And you're going to waste more time adding printfs,
recompiling your code, running your code, changing the printf,
recompiling your code, running your code.
It's going to get very tedious quickly when you have lots of lines of code on the screen.
So can I actually step through my code line by line,
maybe like your TA would in a section or a small class,
line by line walking through the code?
You can.
Because another tool that you have access to is that called debug50.
So this is a CS50 command that will start an industry standard debugger.
And a debugger is a piece of software that is used in the real world
that literally lets you do that.
Debug your code by letting you slow down or even pause execution
and walk through execution of your code line by line.
The only reason we call it debug50 is because in VS Code,
it's a little annoying to start the debugger.
And so we automated the process of starting the debugger.
But everything thereafter has nothing to do with CS50
and everything to do with real world software engineering techniques.
So how do we use this?
Let me go back to VS Code here and let me propose that I want to step through this code
line by line, just like we might at a whiteboard in a smaller class,
to figure out why I'm getting four instead of three hashes.
Well, in my terminal window, what I'm going to go ahead and do is this.
Debug50 space dot slash buggy.
So debug50 is the command.
It needs to know what program I want to debug.
So I'm specifying dot slash buggy, which is the name of the program I just compiled.
I'm going to get an error, though, with the first time I run this.
As will you if you make the same mistake.
I'm about to see this message here.
Looks like you haven't set any breakpoints,
set at least one breakpoint by clicking to the left of a line number,
and then rerun debug50.
So what is this really telling me?
Well, the debugger has no idea when and where I want to pause execution so as
to start walking through my code line by line.
It wants me to tell it where to break.
That is where to pause by clicking on a line number.
So let me hide my terminal for just a moment.
And you've probably never done this intentionally.
But if you hover over the space to the left of your program's line numbers,
you'll see a little red dot, a little stop sign of sorts.
If you actually click on a line number, that red dot will stay there.
And you can see the hover here saying click to add breakpoint.
What I'm going to go ahead and do is say click to add a breakpoint at main.
Main is the entry point to my program.
It's the default function that gets called.
Let's break right away so I can step through this code line by line.
All right, let me reopen my terminal window and clear it.
And then run debug 50 again with dot slash buggy enter.
And now a whole bunch of stuff is going to happen quickly on the screen.
And then it's going to clean itself up because once the debugger is running
and ready to go, it's going to allow me to start stepping through my code line by line.
So what is going on?
Well, notice nothing has happened in the terminal yet.
Why?
Because my code has been paused inside of main.
In particular, it's been paused in the first real line of code.
So the curly brace is uninteresting.
The first line is just the function's name, essentially.
So line 8 is the first juicy line of code that could possibly do anything useful.
It's been highlighted here in yellow.
And the fact that this cursor is here means that we have broken execution on this line,
but we have not yet executed this line, which is why in the terminal, I don't see
anything yet.
I definitely don't see height followed by colon.
Notice what else has happened here.
All of a sudden in the left-hand side of the screen where your file explorer typically
is or where the CS50 duck typically is, we see mention of variables.
You can actually see inside of the debugger what the value of any variable
in the computer's memory happens to be.
Now, I don't quite understand this right now.
We'll come back to this over time.
But weirdly, before line 8 even executes, it
seems that h has a default value of 32,764, which seems to have come from nowhere.
As an aside, this is going to be what's called a garbage value.
And this is actually why we have Oscar so omnipresently here.
A garbage value tends to be a default value inside of a variable that's
the result of that memory having been used previously for something else.
Inside of your computer, you've got all of this memory, random access memory,
or RAM, more on that today.
And it stands to reason that my computer, or whatever cloud server we're using,
has been running for some time.
So the bits that h is going to use might already have some random switches
on and off, some random pattern of bits that happens to give me 32,764.
But the moment this line of code executes,
that value is going to get changed to what I actually wanted to be,
which is what the human is going to type in.
Meanwhile, at the bottom here, you'll see a so-called call stack,
more on this too in the weeks to come.
But you'll see that we've paused on the function called main in the file
called buggy.c.
So how do I do something useful?
Well, at the very top of the debugger, you'll
see a whole bunch of color-coded icons.
One looks like a play button.
And if I click that, it's just going to continue execution of my code
as though I don't want to step through it anymore.
So I'm not going to click that just yet.
The second arrow, which is a little curved arrow over a dot,
is the so-called step over line, which will mean step over this line
and execute it, but only one line at a time.
Let's go ahead and do exactly that.
So I'm going to click the step over icon, the second one, which
is the curved arrow with the dot under it.
Click.
Now I see in my terminal window height being prompted.
Let's go ahead and type in 3, just like I did before, and hit Enter.
Now notice what happens.
Execution has paused on line 9 instead of 8.
And you'll see that my variable, a so-called local variable,
has the value of 3 as intended.
All right, so far, this isn't all that enlightening other
than demonstrative of the fact that I can pause execution of my program
any time I want.
So let's now click that step over button again.
And again, so that we actually print this column.
Click.
And there we have it, four hashes at the bottom of the screen.
Now execution has paused at the end of the function.
This is just my opportunity to either stop or restart or continue.
I'm just going to go ahead and click the Play button and let it finish executing.
Unfortunately, that wasn't really at all enlightening, except to confirm for me
that I typed in 3, and 3 is what is in the computer's memory.
Not that interesting, though, yet.
So let's do this.
Leave the breakpoint on line 6 as before.
Let's rerun the debugger by running debug50, space, dot slash buggy.
Let's let it do its startup thing, which looks a little messy at first,
but now we've highlighted line 8 again.
I'm going to go ahead and step over this line, because I do want to get an int.
I'm going to type in 3 again, Enter.
But this time, instead of stepping over line 9 and just letting print column
happen, this is where the debugger gets powerful.
Let me step into line 9 and walk through the print column function
itself line by line.
So let me go ahead and click not this button, which is the curved arrow
over the dot, but the next one, which is the step into button.
Click.
And now you'll see that execution has jumped inside of print column
and paused on line 14, at which point I can see at top left what
the default value of i is.
And this is some crazy garbage value, because whatever bits are being used
to store i's value have some random garbage from some previous use of that memory.
But as soon as line 14 executes once, I bet i is going to take on a value of 0.
So let's do that.
I'm going to go ahead and click step over, because I don't need to step into this,
because there's no other functions there, step over it.
And immediately at top left, i is now 0.
Now line 16 is highlighted.
Let's step over this.
And notice in the terminal window, what do you see?
The first of our hashes.
Let's step over, step over, second hash.
And i is now 1.
Step over, step over.
Now we see a third hash and i is now 2.
Step over, step over.
OK, there's the symptom of the bug.
Four hashes.
And yet i is 3.
But wait a minute, this is going to draw my attention now to line 14
before I continue on.
Wait a minute.
3 is, of course, less than or equal to 3,
which is why I got that fourth hash on the screen.
So at the end of the day, you still need to exercise some of your own human intellect
to figure out and understand what's going on.
But the value of this here debugger is that you can pause and work through things
at your own pace and poke around inside of your own code
and better understand what's happening as opposed to compiling the program,
running it, and just now having to infer from the symptoms alone
what the source of the problem might be.
So that was a lot.
Let me go ahead here and just let it continue to the end,
because I know what the problem is now.
I need to change the less than or equal to sign to a simple less than instead.
Questions, though, on debug 5, if you are any of these steps.
Yeah.
Sure.
Do you go over what's the break point?
And then my second question is around the garbage value.
The second time you ran it, it still gave that same garbage value
that you had assigned you to H.
Correct.
So in order of your questions, what again are these break points?
The break point or the little red stop sign here just tells the debugger
where to pause execution.
So frankly, I didn't have to break pause execution at main.
If I really care about debugging print column,
I could have clicked down here instead.
And then it would have just run main automatically and only paused once
print column gets called.
So a break point is where your code will break, the point at which it will break.
And as for the garbage values, I'm oversimplifying exactly what's going on
inside of the computer's memory.
And it's not necessarily using exactly the same memory as before,
but the operating system will govern exactly how the memory is laid out.
This is actually a significant problem, long story short,
in a lot of today's systems, because it's not that interesting to me
to know that there was 32,000, whatever that number is, or the negative number.
But suppose that that revealed the password of someone, another program
or function that had some information there.
It seems all too easy with the debugger, let alone
C, to actually poke around the computer's memory.
And we're going to come back to that in a couple of weeks.
But for now, it's a garbage value insofar as you didn't put the value there.
It somehow got there on its own for now.
Other questions?
So the question is about the order of operations for a for loop.
So the first time you go through a for loop,
the initialization happens, the stuff before the first semicolon.
And the condition is actually checked, the Boolean expression.
Then everything inside of the curly braces is executed.
Then the incrementation or update happens, which in this case is i++.
And then the condition is again checked, the Boolean expression.
The code is executed.
The update happens.
The condition again.
The code is updated.
And so it starts to loop like this.
The debugger's graphics are fairly simplistic,
and it just highlights the whole line without making super clear what's happening.
But that's just the definition of a for loop.
Good question.
Others about debug 50 or print def?
All right.
Yeah?
Can you change the location of i++ and i height?
Can you change the position of i++ and height?
Short answer, no.
The first thing is the initialization, the variable you want to create and initialize.
The second thing is the actual condition, the so-called Boolean expression.
The third thing is always the update.
So it must come in this order.
What you're not seeing is that you can actually have multiple Boolean expressions.
You can have multiple initializations.
You can have multiple updates.
But we're keeping it simple for now.
And this is canonical.
All right.
So to make clear, assuming that either print def or debug 50
helped me figure out where the logic was in my thoughts,
I now know that the fix here is to just go and change the less than or equal to
to a simple less than.
And if I run the program again, of course,
it's going to give me the three bricks that I always wanted instead.
But there's other techniques we can use too.
So besides print def and debug, you might wonder why we have a seven foot duck behind me here,
all of these little rubber ducks on the floor.
So rubber duck debugging per week zero is actually a thing.
This was popularized in a book some years ago.
And the idea is that when you are facing some bugs, some mistake in your program,
or you're just confused on some concept, there is anecdotal evidence to suggest
that just talking out the problem with an inanimate object like a rubber duck on your desk
is enough often for that proverbial light bulb to go off over your head
because you hear in your own words what confusion you're having,
what illogical thoughts you're having.
And you don't even need another human or TA or AI in the room to answer the problem for you.
So in fact, on the way out today at the end of class,
we've got hundreds of ducks and enough for everyone to take home with you
if you'd like to use that as another debugging technique,
whether in CS50 or something else.
But of course, now in the age of AI, you also have the AI powered virtual duck at CS50.ai
and also in VS Code at CS50.dev, which really is a mechanism for asking questions
that you don't think you can solve on your own.
So it might be reasonable to ask the duck, what does this error message mean
if you're having trouble wrapping your mind around it?
But it's less reasonable to say, copy paste your code into the duck and say,
what's wrong with my code?
You should really be meeting the AI halfway.
After all, what's the point of actually doing this or any other class
is to develop that muscle memory, develop those mental models,
get some practical skills.
So try hard to walk that line between asking the duck too much versus deploying
some of these same tools yourself, printfdbug50, even a physical rubber duck
on your desk before you resort to sort of escalating it to human-like or duck help.
All right, so with those tools added to one's toolkit,
let's actually consider and reveal what's been going on underneath the hood since last week.
So this was the mental model we proposed for last week,
whereby when you write source code in a language like C,
it's not something that the computer itself understands natively,
because computers we saw only understand 0s and 1s, a.k.a. machine code.
So the compiler is the program that we use to convert your source code
to the machine's code, from C to 0s and 1 in this case.
More generally, a compiler is just a program that translates one language to another.
And in this case, we're going from source code to machine code.
So let's consider what's really happening.
Indeed, this is among the goals of this week is to take a look at a lower level
so that when you encounter more interesting, more challenging problems,
you'll understand from so-called first principles what the computer is actually
doing and supposed to do so you can deductively figure things out for yourself
and generally not view computers as magic, or I don't know how this works.
You'll have a fairly bottom-up sense of how everything works by term's end
inside of any computer, laptop, desktop phone, or the like these days.
So here's the simplest of programs that we wrote last week, even though there's
a lot of syntactic complexity, as we've seen.
The goal is to get it to machine code.
These here are 0s and 1s.
So how has that been happening when you just run make since last week?
Well, these are the two commands that we've typically
run after creating a file like hello.c.
We then compile with make hello, and then we run it with dot slash hello.
So let's give ourselves this starting point real quick,
just so that we have an example in mind of exactly what it is we're compiling.
So let me go back to VS Code here, close out buggy.c,
and let's create a new file just like last week called hello.c, inside of which
is our old friend, standard io.h, int main void,
and then inside of this we'll keep it simple,
just printing out hello comma world, which again is my source code in C.
How do I now actually compile that?
Well, of course, I can go down to my terminal window, make hello, dot slash hello,
and we're off and running.
So it was a bit of a white lie for me to let you think, though, that last week
the compiler itself is called make.
Make is a command that literally makes your program.
It makes it by compiling, but make is not technically the compiler.
If we really want to get nitpicky, the compiler you've been using
is actually called clang for C language.
And this is a very popular compiler, freely available, open source, so to speak.
You can even look at the code other humans wrote to create the compiler online.
And what make is really doing for us is essentially automating this command.
So all this time I could have just run clang space hello.c,
but the default file name from clang, the compiler,
weirdly and for historical reasons, is not going to be hello, as you would hope,
it's going to be a.out for assembler output.
And we don't do this in week one of the class,
because this just makes things unnecessarily complex
that we're adding some random name that you just have to know to type.
However, we can do this now as follows.
Let me go back to VS Code here.
Let me clear my terminal and type ls, and we'll see everything we've created thus far.
buggy.c, which when I compiled it, I got buggy.
And hello.c, which I just wrote, and when I compiled it, I got hello.
Let's do this command now manually, though.
Let's use clang on hello.c and hit Enter.
That too seems to work.
But if I now type ls, you'll see a third program specifically called a.out,
which happens to be the same as hello.
It just is using the default name instead of my custom name hello.
But if I do dot slash a.out, indeed, that too will work.
But the reason we don't do that, certainly in the first week of the course,
is that things get a little annoying or sort of escalate quickly thereafter.
So let me go ahead and change this program as we've done a few times already.
Let me include cs50.h so that we get access to get string.
Let me do string name equals get string, quote unquote,
what's your name, question mark, close quote.
And then down here, just like before, let me add my percent s and add in my name.
So I did that super quickly.
But it's the same program we wrote a few minutes ago,
and it's the same one we wrote last week.
What happens now, though, is as follows.
If I now try to do clang hello.c enter, I actually get an error message.
This one perhaps more cryptic than most.
Somehow or other, I have this error.
Linker command failed with exit code 1 because of an undefined reference to get string.
Now, in the past, when we've seen undefined or really undeclared mentions
of get string, the problem was just with missing this line.
This line is clearly here.
But the catch is I'm getting this error message now
because when I run clang of hello.c, I'm just assuming
that clang knows where to find the cs50 version of get string.
And that is not the case.
Technically, if I want the compiler to compile this code for me,
what I'm actually going to have to do is this.
Let me go back to my terminal window here.
And I'm going to say clang hello.c.
But I'm then going to specify dash lcs50, which is cryptic at first glance.
But this is telling the compiler to link in the cs50 library
so that it knows what the 0s and 1s are that belong to the get string function.
Long story short, if I hit Enter now, the error message has gone away.
If I type ls, I've still got it a.out, but it's a new version thereof.
And if I do dot slash a.out, now I see the new behavior where I can type in my name
and see hello, David.
Now, this is getting a little stupid that I keep using a.out.
We can change that as well.
In fact, these commands, as we're starting to see,
support what are called command line arguments.
And a lot of the programs we've run already take command line arguments.
When we run code, space, hello.c, the so-called command line argument
to code is hello.c.
When I run make hello, the command line argument to make is hello.
In other words, the command line arguments to a program
are all of the words you're typing in your terminal after the name of the program
itself, whether it's make or whether it's code or anything else.
So this is to say, what I just ran, clang of hello.c-lcs50,
I was passing in two command line arguments, hello.c, which is the code
I want to compile, and dash lcs50, which means use the cs50 library please.
But I can add another to the mix.
I can actually do something like this, whereby I do clang dash oh hello,
then I can do hello.c and then dash lcs50 enter.
Now that too seems to work.
And if I type ls, well, I've got all the same programs as before.
So let's go ahead and get rid of those to make clear what's going on.
I'm going to remove a.out.
I'm going to remove hello.
And just for good measure, I'll remove buggy as well,
so that all I have left in this folder is source code.
So if I type ls, there's my two files.
Let's do this again.
Clang dash oh hello, hello.c-lcs50 enter.
Now if I type ls, I don't see a.out anymore,
because apparently, according to the documentation for clang, the actual compiler,
if you pass dash oh as a command line argument, followed by another word of your choice,
you can name the program anything you want without having to resort to mv
or clicking on it and typing a new name in manually.
So if I now do dot slash hello, I see the exact same version where it's just
asking me for my name and then printing it out.
But long story short, the whole point of this exercise
is that running commands like this quickly gets very tedious.
You have to remember the order in which to do it, what the command line argument.
I mean, this is just a stupid waste of time.
Typically, certainly in week one of the course,
you have to memorize these kinds of magical commands to get things working.
But for now, no, that when you run make, it's
essentially automating all of that for you and making it as simple semantically
as make hello or make buggy.
But what's really happening is the make command,
because of the way we've configured, cs50.dev for you,
is doing all of this behind the scenes.
It's not that magical.
This just means change the file name to hello when you compile it.
This just means compile this code.
And this just means use the cs50 library.
Like that's all.
But that message about linking something in,
there's something juicy going on there such that make is in fact
helping us solve a whole bunch of problems when we compile.
And in fact, let me propose that if we take a step back
and look at some of the actual code that we're compiling,
let's consider what we actually mean by compiling.
Yes, it's the case that to compile your code means
to go from source code to machine code.
But technically, there's a few more steps involved.
Technically, when you compile your code, that sort of
become the industry term of art that really is referring to four separate processes,
all of which are happening in succession automatically,
but each of which is doing a different thing.
So just once, let's walk through with these several steps.
So what is this pre-processing step?
So consider this program here, which we wrote in brief last week.
We've got includestandardio.h, which is there because we want to be able to use
printf ultimately.
We've then got a prototype for this meow function.
And the meow function does this.
All it does is print out quote unquote meow followed by a new line,
takes no input, returns no return values.
The main function now has a for loop, iterates three times,
each time calling the meow function.
And we saw this already earlier today.
This line of code here, the so-called prototype, is necessary
because we need to tell the compiler that meow exists before we actually use it here,
especially if I don't get around to implementing it until later.
So this copy-paste of that first line of code, a so-called prototype,
solved that problem.
This is what the header files are essentially doing for us.
Before I use printf down here, the compiler needs
to know what it is, what its inputs are, what its outputs are.
Turns out the prototype for printf is going to be in standard io.h.
And that's what that line of code has been doing for us all this time.
In fact, let's take a simpler example that we keep using here,
whereby I'm including cs50.h and standard io.h.
And I'm using the cs50 getString function to get someone's name
and put it in a variable called name.
And then I'm printing out hello comma such and such.
What's going on now when I pre-process this file
by running make, which in turn runs clang?
Well, the compiler finds on the server's hard drive
the file called cs50.h, goes inside and essentially copies
and pastes its contents into my own code.
Meanwhile, such that we get the prototype there for getString.
And we haven't seen this yet, but it stands to reason
that all this time using printf, we've been passing in a prompt, like what's
your name, and we've been getting back a string.
What's inside the parentheses recall is the input.
What's before the function name is the output, the so-called return value.
What about standard io.h?
It's in that file that printf's prototype is.
So essentially what the compiler does when pre-processing this file
is it finds standard io.h somewhere on the server's hard drive, goes inside
and copy and pastes those relevant lines of code into my code as well.
It's to avoid me having to do all of that myself, find the file, copy,
paste it, or manually type out the prototype.
These pre-processor directives just automate all of that tedium.
So what this effectively has at the top of my code after the file's been
pre-processed is all of those hash symbols, followed by include,
are changed to contain the actual contents of those header files.
Now the compiler knows what getString is all about and what printf is all about.
That then is the pre-processing step.
What is compiling technically mean?
Compiling means taking that pre-processed code, which again looks a little something
like this, and convert it into something called assembly code.
And we won't spend much time in this class on assembly code,
but this is how programmers used to write code.
Before there was C, before there was Python and Java,
and all of these other modern languages, programmers were writing code like this.
Before this existed, they were programming zeros and ones into the earliest
of mainframe computers using punch cards and other technologies,
like literally sheets of paper with holes in them.
Not very fun, very tedious, so the world invented this.
Not that much fun, so the world invented Python and so forth,
that we continue to evolve as a species with code.
But the compiler technically takes your pre-processed source code
and converts it into something that looks like this, cryptic.
And that's to be expected, but there are some familiar phrases.
There's mention of main, there's mention of getString,
there's mention of printf.
And there's a bunch of other things.
Move and push and xor and call and these other commands here.
These are the assembly instructions.
Those are the lowest level instructions that the CPU inside of a computer understands.
CPU is the central processing unit.
The thing by Intel or AMD or Apple or other companies,
those are the lowest level commands that the actual hardware inside of the computer understand.
It's just nice to be able to write words like main and for and printf,
then it would be to run these much more arcane commands
that you'd have to look up in a manual.
So compiling just takes C code and makes it a lower level type of code called assembly.
When I said a.out means assembler output,
that's why inside of that file is essentially the output of an assembler.
All right, we're almost there.
What does it mean to assemble a program,
which is step three of the compilation process?
That means converting assembly code to the actual zeros and ones we keep talking about.
So if the file is called hello.c,
when that file is assembled,
the assembly code becomes the zeros and ones for your code in hello.c.
But your code is not everything that composes your final program.
Your code from hello.c has to be combined with code from CS50's library,
from the standard IO library that other humans wrote.
I and the team wrote the CS50 code.
Other humans in the world wrote the printf code and standard IO.
So essentially, the fourth and final step
is to link all of those zeros and ones together.
Somewhere on the server, there is not just the header file,
CS50.h and standard IO.h, but your code, hello.c,
our code, CS50.c, and the code that contains printf's own implementation.
Bit of a white line.
It's technically not called standard IO.c,
but the point remains ultimately the same.
So these files have already been piled for you in advance.
This is your code.
What the assembly process does is it combines all of that into zeros and ones,
and then all three chunks of zeros and ones are linked together.
So if you think back to when I tried compiling the code without dash LCS50,
there was some mention of linker.
Linking just means the computer did not know
how to link your code with CS50's code, because we were missing dash LCS50,
which tells the compiler to go find it somewhere on the hard drive.
And the final step, then, of linking is to combine all of those zeros and ones
into one bigger blob of zeros and ones, and that's
what's inside your hello program that you can execute.
So long story short, these four steps are what's been happening ever
since the start of last week.
Pre-processing, compiling, assembly, and linking.
But thankfully, the world of programmers generally
just treats all four of these steps as what we know now as compiling.
It's just a lot of users say compile and not
worry about those lower level details.
But that might reveal better to you what all of these error messages mean
when you see hints of this kind of terminology.
Questions on any and all of that?
From here on out, we're going to go higher level than lower.
Yeah.
I don't get the part with the, like, when we're talking about,
I think it's the assembly process.
When you look at it, it's zeros and ones.
It doesn't, like, across the multiple, like, two different ones.
So if there's zeros and ones in the five different things,
and one is in the five text, and the other is in the five something else,
how does the computer know, like, what part, what eight
corresponds to which part?
Really good question.
How does the computer know which of those zeros and ones corresponds to data,
like numbers or strings of text or actual commands?
We're going to come back to that in week four of the class.
But long story short, what we just saw on the screen
is a big blob of zeros and ones actually follow some pattern,
where the bits up top represent a certain functionality.
The bits on the bottom represent something else,
and they're organized into patterns.
So long story short, we'll come back to that, but they follow conventions.
It's not just a hot mess of, like, zeros and ones.
Other questions?
Correct.
The preprocessing step goes into the header file
and essentially copies and paste the contents of it into your own code
so you don't have to waste time doing that manually yourself.
Other questions?
No.
So when you compile your code, you're going from the assembly code to the zeros and ones.
Sorry.
When you compile, let me pull up the chart again.
When you compile your code, you're going from the C code to the assembly code.
And the patterns you get when you see the assembly code
are specific to a certain CPU.
So long story short, if you're designing software for iPhones
or for Android devices or Macs or PCs, you're going to necessarily use a different compiler.
Because given the same C code, you will get different assembly instructions in the output.
And this is why you can't just take, back in the day,
like a CD containing a program from a Mac and run it on a PC or vice versa,
because it's the wrong patterns of instructions.
But the reason why we have all of these annoying layers of complexity
is because, one, four different people can now implement the notion of compiling.
Someone can implement the preprocessor.
Someone can implement the compiler, the assembler, the linker.
And you can actually collaborate by breaking things down into these quantized steps.
But also, you can do this step, this step.
And then two different people can write compilers to actually write to output assembly code
for like iPhones over here and Android devices over here.
But all of us can still enjoy using the same language up here.
So there's a lot of reasons for this complexity.
Just understanding it is useful, but you're not going to need to use this sort of knowledge day to day.
But it's what enables so much of today's complexity nonetheless.
All right.
So a bit of a flourish now as to what we've been doing with compiling.
Well, compiling is going ultimately from source code to machine code.
Couldn't you just kind of reverse the process?
Right?
If someone wrote really interesting software, like Microsoft Word or Excel or something like that,
well, when I buy it or download it, like I literally have a copy of all of those zeros and ones,
couldn't I just kind of reverse this process and reverse engineer someone else's code
by decompiling it?
And this is genuinely a threat.
And this comes up in matters of law and intellectual property,
because the zeros and ones have to be accessible to you and to your computer.
So it's not a great feeling if someone with enough time and enough savvy could sort of reinvent Microsoft Word
by just figuring out what all of those zeros and ones mean.
However, it's sort of easier said than done to reverse engineer code from these zeros and ones.
For instance, this pattern of bits on the screen here did what?
Did we say last week?
Silly question.
No normal person should be able to answer this.
But I did say it before.
These zeros and ones print what?
It just prints out hello world.
And I cannot glance at that and figure it out, like, off the top of my head.
But if I know what architecture, what CPU this code has been compiled into,
and I pay attention in week four and know what the various layout of the zeros and ones are,
I could painstakingly figure out what each of those patterns of zeros and one means
by breaking them into chunks of 8 or 16 or 32 or 64,
which are common units of measure that I alluded to last week.
Now, that's going to take a crazy amount of time.
And the sort of presumption is that if you are smart enough and capable enough
and have enough free time to do that, it would probably take you less time
to just implement Microsoft Word the normal way and just rebuild the software.
It's going to take you more time to go in reverse than it would in the so-called forward direction.
But there's other subtleties as well.
Inside of this code is not only commands like printf, functions like printf,
but suppose that it contained a loop, for instance, to print meow, meow, meow.
Well, we know already that you can use a for loop sometimes,
or you can use a while loop, but they're functionally equivalent.
It's sort of a stylistic decision, which one you use, whichever one you're more comfortable with,
or maybe feels a little better designed.
But you can't figure out from the zeros and ones whether or not it was a while loop
or a for loop because it just results in the same pattern of zeros and ones.
It's just a programmer's choice, which is to say, you can't even perfectly reverse engineer everything
because it's not going to be obvious from the zeros and ones what the source code originally looked like.
But again, the bigger deal breaker is if you have that much time and energy and savvy,
just like re-implement Microsoft Word itself, don't try to reverse the whole process,
which is going to be much more painstaking and time consuming instead.
Now, this is not true for all languages.
And just as a teaser in a few weeks' time, when we talk about web programming
and another language called JavaScript,
it turns out that JavaScript source code is actually sent from web servers to web browsers.
And you can look at the source code of any website on the internet, harvard.edu, facebook.com, gmail.com.
It's going to be there.
So not all languages, it turns out, are even compiled typically.
Sometimes the source code is just executed by the underlying computer.
So we're just scratching the surface of some of the implications of all this.
In a little bit time, let's take a look further under the hood at the actual memory,
solve some other problems.
But I think it's now time for cheese it.
So let's go ahead and take a 10-minute break.
Snacks are now served.
See you in 10.
All right.
We are back.
And up until now, when we've been writing code,
recall that we have to specify what type of value you want to put in a variable.
That's why I had to go in and add string before the word name in my first bug today.
But it turns out C, as we've kind of seen already, has a whole bunch of these data types.
I rattled these off last week, bool, int, long, float, double, char, string.
But we'll consider for a moment just how much space each of these things takes up
and see if we can't help you see what the debugger was seeing earlier.
That is what is where in memory.
So a bool, it turns out, actually takes up one byte, which is kind of stupid
because technically a bool, true or false, really only needs one bit.
It just turns out that it's more efficient and easier to just use a whole byte, eight bits,
even though seven of them are effectively unused.
So a bool will take up one byte even though it's just true and false.
An int recall uses four bytes.
So if you want to count really high with an int,
the highest you can go is roughly 4 billion, we've claimed.
Unless you want to represent negative numbers, in which case the highest is like 2 billion,
because if you want to be able to count all the way down to negative 2 billion,
you've got to kind of split the difference.
Along, meanwhile, is twice that.
It uses 8 bytes, which is roughly 9 quadrillion possibilities,
which is quite a few more than 4 billion.
That is, if you want to include negative numbers as well.
Then we had floats, which were real numbers with decimal points,
which speak to just how precise you can be with significant digits.
A float is four bytes by default.
But a double gives you twice as many bits to play with,
which lets you be more precise even though at the end of the day,
whether using floats or doubles, floating point in precision, as we've seen,
is a fundamental problem for scientific, financial, and other types of computing
where precision is ever so important.
A char, meanwhile, at least as we've seen it,
is a single byte using ASCII characters specifically.
And then string, I'll put as a question mark,
because a string totally depends on its length.
If you're storing high, that's like 1, 2 bytes.
If storing hello, that's like 5 bytes and so forth.
So strings depend on how many characters you actually want to store inside of them.
So where does this go?
Well, here is a picture of a stick of memory, a dim, so to speak,
whereby on this stick of memory, which is slid into your computer,
your laptop, your desktop, or some other device,
there's all these little black chips that essentially contain lots of room for zeros and ones.
It's somehow electronic, but inside of there are all of the zeros and ones that we can store data in.
So if we kind of zoom in on this, it stands to reason that for the sake of discussion,
if this one chip represents like 1 gigabyte, 1 billion bytes,
it stands to reason that we could slap some addresses on these bytes,
whereby we could say this is the first byte and this is the last byte.
Or more precisely, this is byte 0, 1, 2, 3, dot, dot, dot, byte 1 billion.
And it doesn't matter if it's top, down, left, right, or any other order,
we're just talking about this conceptually at the moment.
So in fact, let's go ahead and draw this really as a grid of memory,
a sort of canvas that we can just use to store types of data, like bools,
and ints, and charrs, and floats, and everything else.
If we are going to use one byte to store like a char, well,
you might use just these eight bits up here, one byte up here.
If you want to store an int, well, that's four.
You might use all four of these bytes, necessarily contiguous.
You can't just choose random bits all over the place.
When you have a four byte value like an int, they're all going to be contiguous,
back to back to back in memory like this.
But if you've got a long or a double, you might use eight bytes instead.
So truly, when you store a value in memory, whether it's a little number or a big number,
all you're doing is using some of the zeros and ones physically in the computer's hardware
somewhere and letting it permute them, turn them on and off to represent that value
you're trying to store.
All right, so let's go ahead and abstract away from the hardware, though.
And let's just start to think of this grid of memory sort of in zoomed-in form.
And consider, at a lower level, what is actually being stored inside of here?
For instance, suppose that we've got some code like this,
containing three scores on like problem sets.
You've got a 72 on one of them, a 73 on another, and a 33 on the third.
We've deliberately chosen our old friends, 72, 73, 33, which recall spell high.
Or together, in the context of colors, it's like a shade of yellow,
just so that we're not adding some new random numbers to the mix.
These are our old friends, three integers.
Well, let's use these in a program.
Let me go over to BS code here.
And let me create with code a program called scores.c.
That's just going to let me quickly calculate my average score on my problem sets.
I'm going to go ahead and include, as we often do, standard io.h at the top.
I'm going to do int main void after that.
And then inside of my curly braces, I'm going to do exactly those sample lines of code.
My first score was, let's say, a 72.
My second score was 73.
And my third score was 33.
So I've declared three variables, one for each of my problem set scores.
Now let's calculate the average.
So printf, quote unquote, average colon, just so I know what I'm printing.
And now I'm going to go ahead and use maybe percent i backslash n.
And then what I'm going to pass in is a bit of math.
So to compute an average is just score 1 plus score 2 plus score 3 divided by 3.
And I put the scores, the numerator and parentheses just like in grade school,
like I need to do that operation first before doing the division.
So just like math class.
Semi-colon at the end to finish my thought.
Let's see how this goes.
Make scores, enter, dot slash scores.
And it would seem that my average across these three problem sets is 72, which is great.
But I don't think that's actually what I want here.
What have I done wrong?
It's unintentional.
Yeah.
Yeah, I'm kind of being a little generous with myself here.
I didn't really factor in my worst score.
So that was accidental.
So now let me do this correctly.
Make scores, dot slash scores.
And now, OK, my average is 59.
But I beg to differ.
I'd like to quibble.
My score, technically, I think mathematically should really be 59 and a third.
I'm kind of being cheated, that third of a point.
So what's going on here?
Why am I only seeing 59 and not my full grade?
Perfect.
Because I'm using integers.
When I divide by 3, it's going to truncate everything after the decimal point,
which we touched on at the very end of week 1,
which is an issue with just truncation in general.
So one approach to fix this.
I could change my percent i to percent f, which is the format code.
It turns out for a float.
And that is what I want to print.
So let's see if that fix alone is enough.
Make scores.
Oops, it's not.
I got ahead of myself there.
And let me scroll up to the error.
Format specifies double, but the argument has type int.
Turns out you can use percent f for doubles as well.
So that's why I'm saying double, even though I intended to float in this case.
So there's a problem here.
The argument has type int, even though I'm passing in percent f.
You're seeing mention of percent d here, which is an alternative to percent i.
We typically encourage you to use percent i because i for integer.
But that is not the solution to this problem,
because I want my third of a point back.
So how could I go about fixing this?
Well, the fundamental problem here is that I'm
trying to format an integer as a float, or even as a double.
Well, I need to convert these scores to floats instead.
So I could go in and change this to float, this to float, this to float.
And heck, just to be super precise, I could add a 0.0 on the end of each of them
just to make super clear these are floats.
But there's another way.
I could, for instance, simply convert my denominator to 3.0.
Because it turns out, so long as you involve like one float in your math,
the whole thing is going to get promoted, so to speak, to floating point values
instead of integers.
I don't have to convert all of them.
So I think now, if I do make scores dot slash scores now,
ah, there's my third of a percent, the third of a point back.
There's another way to do this, just as an aside.
And we'll see this again down the line.
If you really want to stick with 3, because it's a little weird,
just semantically, to divide by 3.0.
That's an implementation detail.
But you're truly computing an average of three things.
You can technically cast the three to a float.
In parentheses, you can specify the data type that you want to convert another data type to.
And this too should make the compiler happy.
Ah-ha, dot slash scores, I get roughly the same answer.
We're seeing some floating point in precision, though, nonetheless.
But that too would achieve the goal here.
But in short, that's all just a function of floating point arithmetic there.
So what's going on now, actually, in the computer's memory?
Let me revert back to the simpler one with just 0.0 there.
And let me propose that we consider where these three things are in memory.
Well, if we treat this as my grid or canvas of memory, who knows where they're going to end up.
But for the sake of discussion, let's assume that 72 ended up in the top left of my computer's memory.
I've drawn it to scale, so to speak, and that this score one variable is clearly taking up four bytes of memory.
And it's an int.
And that's typically how many bytes are used on systems.
Technically, it depends on the exact system you're using.
But nowadays, it's pretty reasonable to assume that an integer will be 32 bits on most modern systems.
Score two is probably over there.
Score three is probably over there.
So I'm using 12 bytes total, four bytes for each of these values.
All right, so that's really all that's going on underneath the hood.
I don't have to worry about this.
The compiler essentially figured out for me where to put all of these things in memory.
But what really is in memory, well, technically, each of these variables,
if it's composed of 32 bits, is really just a pattern of literally 32 zeros and ones.
And I figured out the pattern here.
I crammed them all into the space there.
But you see here three patterns of 32 bits, which collectively compose those numbers there.
But let's consider design now.
In terms of my code, this gets the job done.
It's not that bad or big of a deal for just calculating the average of three scores.
But this should also start to rub you the wrong way this week onward when it comes to design.
Like, this is correct, especially now that I clamored back my third of a point.
But this is bad design using the variables in this way.
Why might you think?
Yeah?
Yeah, I'm going to have to type in each score manually with each passing week
when I get the fourth problem set in the fifth.
I mean, surely people who came before us came up with a better way to solve this problem
than manually create 10 variables, 20 variables, whatever it is by the end of the semester.
It just feels a little sloppy.
And indeed, that's often the way to think about the quality of something that's designed.
Think about the extreme.
If you don't have three scores, but 30 or 300,
is this really going to be the best way to do it?
And if you feel like, no, no, there's got to be a better way, odds are there are.
Certainly if the language itself is well-designed.
So let's consider how else we might go about solving this.
Well, it turns out we can treat our canvas of memory, that grid of bytes,
into chunks of memory known as arrays.
An array is a chunk of contiguous memory, back to back to back,
whereby if you want to store three things,
you ask the computer for a chunk of memory for three things.
If you want 30, you ask for one chunk of size 30.
If you want even more, you ask for a chunk of size 300.
Chunk is not a term of art.
I'm just using it to colloquially explain what an array actually is.
It's a chunk or a block of memory that is back to back to back to back.
So what does this mean in practice?
Well, it means that we can introduce a little bit of new syntax in C.
If I want to create one variable instead of three,
and certainly one variable instead of 30, I can use syntax like this.
Hey compiler, give me a variable called scores, plural.
Give me room for three integers therein.
So it's a little bit of a weird syntax,
but you specify the type of all of the values in the array.
You specify the name of the array, scores in this case,
and I pluralized it just semantically because it makes more sense than calling it score now.
And then in square brackets, so to speak,
you specify how many integers you want to put into that chunk of memory.
So this one line of code now will essentially give me 12 bytes automatically.
But they'll all be referable by the name scores, plural.
So let's go ahead and weave this into some code as follows.
Let me go back to VS code here, clear my terminal.
And now let's just whip up the same kind of program,
but get rid of these three independent variables.
And instead, let's go ahead and just say int, scores, plural, bracket three.
Now I need a way to initialize the three values, but this I can do too.
It turns out that if I want to put three values in this,
I just need slightly new syntax.
I can say scores, bracket zero equals 72.
Scores, bracket one equals 73.
Scores, bracket two equals 33.
So it's not all that different from having three variables,
but now I technically have one variable,
and I am indexing into it at different locations, location zero, one, and two.
And it's zero because we always, in computing, start counting from zero.
So I do scores, bracket zero is going to be my 72 problem set.
Scores, bracket one is my 73 problem set.
And scores, bracket two was my weakest, my 33 p set.
Now my syntax down here has to change because there are no more score.
One score, two score, three variables.
But there are scores, brackets zero plus scores, bracket one plus.
And notice what VS code is trying to do for me.
It's saving me some keystrokes.
As I type in scores and type one single bracket,
notice it finishes my thought for me and magically puts the cursor where I want it
so I can put the two right there and generally save on keystrokes.
But that has nothing to do with C.
It just has to do with VS code trying to be now helpful.
So I think now, if I go down here and do make scores dot slash scores,
we get the same answer.
But it's arguably better designed because I now have one variable instead of three,
let alone many more.
And in fact, if I wanted to change the total number of scores,
I can just change what's in that initial square bracket.
So if we consider what's going on now, if we look at the computer's memory,
it's the same exact layout.
But there's no more three variable names.
There's one, scores, brackets zero, scores, bracket one, and scores, bracket two.
And notice here, ever more important, in arrays values are indeed contiguous.
Back to back to back.
Now, the screen is only so wide, so they kind of wrap around to the next row of bytes.
But the computer has no notion of up, down, left, right.
I mean, it's just a piece of hardware that's got lots of bytes available
that can be addressed from the first byte all the way down to the last byte.
The wrapping is just a visual artifact on this here screen.
All right, so if I've done this now, maybe we
can make this program a little more dynamic than just hard coding in my scores.
Let me go in and add the CS50 header library so that we could also use, for instance,
like getInt and start getting these scores dynamically.
So I could do getInt and I could prompt the user for a score.
I could use getInt again.
And I can prompt the user for another pset score.
I can use getInt a third time and prompt the user for third such score.
And then pretty much the rest of my code can stay the same.
Let's do makeScores again.
Dot slash scores, 72, 73, 33.
And now my program's a little more interactive.
Like, this doesn't work for just my three scores.
It could work for anyone's scores in the class.
Now, this too hints of bad design.
I like my introduction of the array because I now have one variable instead of three.
But what now might rub you the wrong way among lines 7, 8, and 9?
Yeah.
It's repetitive.
I mean, I typed it manually, but I might as well have just copied and pasted,
like, literally the same thing.
So what's a candidate for fixing this?
Like, what programming construct might clean this up?
Yeah.
Yeah, we could use a for loop or a while loop or whatever.
But a for loop would get the job done.
And that's often my go to.
So let's do that instead.
Let's go under my declaration of the array and do for int i equals 0, i less than 3,
i plus plus, which we keep seeing again and again.
Now, how do I index into the array at the right location?
Well, here's where the square brackets are kind of powerful.
I can just say my scores array at the location i should get an int from the user as follows.
So now I'm using get int once inside of a loop.
But because i keeps getting incremented, as we've done many a time now for meowing
and other goals, I'm putting the first one at location 0.
Why?
Because i is initialized to 0.
I'm putting the second one at location 1.
Why?
Because I'm going to plus plus or increment i on the next iteration than the next iteration.
So this has the ultimate effect of putting these three scores at location 0, 1, and 2,
instead of me having to type all of that out manually.
Now, I don't love how I've done this still.
If we really want a nitpick, this solves the problem correctly.
But it's kind of got a poor design decision still.
It's got a magic number, as people say.
What is the magic number here?
And why is it bad?
Yeah, over here.
Yeah, it was a little soft.
But I think the number three is hard-coded in two places.
We've got it on line six, which is the size of the array.
And then again, on line seven, which is how many times I want to iterate.
But those are the exact same concepts.
But it's on the honor system that I typed the number three correctly both times.
So I think we can fix this a little better.
I could do something like int n equals three.
And then I could use n here.
And then I could use n here so that now I only change it in one place.
If your eyes are wandering to the bottom of the program,
there's still a problem here because I've still hard-coded zero, one, and two.
But we'll come back to that.
But this is arguably a little better.
But let's talk a little bit about style.
Typically, when you've got a variable that should not change its value,
we saw last week that we should declare it as constant.
And the trick there is to literally just write
const for short in front of the type of the variable.
And now it should not be changeable by you, by a colleague, a collaborator,
or the like.
But typically, too, by convention, stylistically,
to make visually clear to another programmer that this is a constant,
it's convention also to capitalize constant.
So to actually use a capital n here in all places,
just to make clear visually that there's something interesting about this variable.
And indeed, it is a constant that cannot be changed.
All right, with that refinement, I don't think
we've really improved the program fundamentally.
I think we're going to need to do a bit more work to do this really well.
So I'm going to do this a little quickly, but mostly to make the point
that we can make this indeed more dynamic.
So let me hide my terminal window there.
Let me go ahead now and get the scores as I already am as follows here.
And let me go ahead and assume, for the sake of time,
that we have a function that exists already called average.
And I simply want to pass in to that average function, the scores,
whose average I want to calculate.
So average does not exist off the shelf.
Like I can't just use an existing library for it.
I'm going to have to implement this thing myself.
But how?
All right, well, let's go ahead and do this.
At the top of my file, I'm going to go ahead and compute or define a function
called average that takes in what?
An array of numbers.
So this syntax is going to be a bit new.
But the way I do this is int, say array bracket 0.
Or array sounds a little too generic.
Let's just call it numbers, for instance, here.
So that says my average function is going to take as an argument an array of numbers.
This average function, though, should return a value, too.
And it should return what type of value from what we've seen thus far?
A number of float, specifically.
It could be int, but then I'm going to get short-changed,
my third of a point, potentially.
So I think I want it to return a float.
Or if you really want precision, you could return a double, just to be really nitpicky.
But that seems excessive here.
All right, well, now inside of my average function, how can I calculate the average?
Well, this is just kind of like a math thing.
So I could declare a variable called sum and set it equal to 0.
I could then have a for loop inside of this function for int i gets 0 i less than, huh.
I'm going to come back to this, the numbers in the array.
And then I'm going to do i plus plus.
And then on each iteration, I'm going to do sum equals whatever the current sum is
plus whatever is in the numbers array at that location.
So I'm going a little quickly.
But again, I'm just applying the same lesson learned.
Numbers is my array.
Numbers bracket i means go to the ith location in there.
But if my loop starts at 0, that means go to location 0, and then 1, and then 2.
And heck, if there's more scores in this array,
it's just going to keep going on up from there because of the plus plus.
But I hesitated here for a couple of reasons.
So I put it to do here, which is not a thing.
That's a note to self.
How far do I iterate?
Well, if you've come into CS50 with programming before,
you can usually just ask an array, a.k.a. a vector what its length is in Java
and in Python and the like.
You can't do that in C.
So if I want to know what the length is of this array,
I've got to have the function tell me.
So I'm going to additionally propose that this average function can't just take the array.
It's also going to have to take another argument, a second input,
for instance, called length, that tells me how long it is.
And then down here, which is where we started the story,
when I used this so-called average function,
I'm going to have to tell the average function by passing in n
how many numbers are in that array, just because.
This is annoying that you have to pass in not only the array,
but also its size separately.
That's the way it's done in C.
More recent languages have improved upon this,
so you can just figure out what the length of the array is,
as we'll see in a few weeks in Python.
All right, back to the average function at hand.
I think we're almost there.
This is a little unnecessarily verbose.
Recall that we can tighten this up by just doing plus equals,
whatever is in numbers bracket i, that's just tightening it up.
It's syntactic sugar, so to speak.
And then the last thing I'm going to do in my average function is what?
Actually calculate the average.
So what is the average?
Just the numerator, like the sum of all of the scores,
divided by the total number of all of the scores.
Well, I've got the sum, so I think I just want
to do sum divided by what to get the actual average now?
Yeah.
Exactly.
Sum divided by length will give me the average,
because the sum is the numerator effectively,
all of the scores added together, and the denominator is the length.
How many numbers were there actually?
Now, I can't just write this math expression here.
If this is going to be my function's return value,
and we've done this once or twice before,
I literally say in my average function, return this value.
So it hands back the work.
I could use printf and just print it on the screen,
but I don't want that visual side effect.
I want to hand it back so that on line 23,
I can simply calculate the average of those n scores
and let printf use it as the value of that format code, percent f.
All right.
I think we are in reasonably good shape.
Let me cross my fingers now, and hope I didn't screw this up.
Make scores.
OK.
Dot slash scores.
How many do we want to do?
So we'll do 72, 73, 33.
Enter.
And there is so close average.
I've had a regression.
I've made the same mistake again, just in a different way.
I think I saw your hand go up.
Why am I getting 59?
And I'm not getting my third of a point.
I think that in the function, I can use
to do it in the return line past the line.
Yeah.
In this return line on line 11 right now,
I'm again stupidly doing integer divided by integer.
That will make us suffer from integer truncation.
Because if you're returning an integer, there's
no room for the decimal point or any numbers thereafter.
So how do we fix this?
Well, I could change the sum to float.
Like that would be reasonable.
So then I do a float divided by the length.
I could do my casting trick, like convert the length to a float,
just for the sake of floating point arithmetic.
There's a bunch of ways to solve this.
But I think I'll go with this one now.
Let me now do make scores again.
Dot slash score, 72, 73, 33.
And now I've got, albeit with some imprecision,
I think enough precision certainly for a college grade in this case, 59.333 and so forth.
OK.
So what are the things to actually care about here?
So there's a decent amount of code here.
Most of it is sort of stuff we've seen before.
But the interesting parts I would propose are this.
When you create your own function that takes an array as input,
you have to take as input the length of the array.
You're not going to be able to figure it out correctly as in modern newer languages.
You also need, of course, to pass in the array itself.
How do you pass in an array?
Well, when you're defining the function, you specify the type of values in the array,
whatever you want to name the array inside of this function,
and then you use empty square brackets like this.
You don't have to put in or some other number there.
All you need to tell the compiler is that my average function
is going to take some array of values, specifically this many.
You don't put it inside the square brackets there.
Then when I use it, now it's just the now familiar syntax.
When you want to index into your array, that is go to location 0 or 1 or 2,
you just use square bracket notation here.
But the array itself, recall, was actually created in main when I did this line of code here,
where I said give me an array called scores, each of whose values is going to be an int,
and I want this many of them.
And so maybe the final flourish that I'll add here, just to be sort of nitpicky,
is I keep saying that main should really go at the top.
Fine, no big deal.
Let me highlight my average function, move it to the bottom of my file, just because.
And then, and only then, I'll copy and paste that first line, the so-called prototype,
so that clang doesn't freak out by not knowing what the average function is.
So in short, there's seemingly a bunch of complexity here,
but all we're the only thing that's really new in this one example
is this is how you pass to a function in array that already exists elsewhere.
Not by its name, but by the square brackets there.
OK.
Questions on arrays or any of this new syntax?
Yeah?
I'm just going to slow, but back when you did the more average thing,
you said that we could store it as a float.
And instead of saying 3.0 was a float, you just said because 3.0 is a float.
How does it know it's not a double?
Oh, how does it know it's not a double?
So by default, if you just type a number like 3.0 into your code,
it will be assumed to be a double, just because.
Raw values, literal numbers with a decimal point
will be treated by the compiler as doubles and be allocated 64 bits.
How come you still do the percentage app?
Just because.
Like, the world did not need to create a new format code.
Like, percent d is not double.
Percent d is decimal integer, but don't worry about that.
We tend not to talk about it too much in class.
Percent i is integer, percent f is float, but percent f is also double.
And this is not consistent, because what's a long?
Percent lli, what did I say last week?
Percent lli gives you a long integer.
It's just a mess.
There's no good reason for this other than historical baggage.
Sure, I'm not sure if that's reassuring, but all right.
So, OK, let's use this knowledge for something useful now,
and actually tease apart how we can use these skills for good.
And to better understand what's going on inside of the computer as follows.
Let me go over to our grid of memory.
And this time, let's not store some numbers,
but let's store these three lines of code, these three variables.
So three chars, even though you know where this is going.
This is not good design, because I got three stupidly named variables,
but let's make a point first.
The first variable's value is quote unquote h.
Second is i.
Third is exclamation point.
Why, though, am I using single quotes suddenly instead of double quotes?
It's a character.
Chars are single quotes.
Strings are double quotes.
And we'll see the distinction why in a moment.
So for instance, if this is my grid of memory,
and this program contains just three variables, each of them a char,
odds are they'll end up like this in memory.
C1, C2, C3, hi, exclamation point.
Assuming there's nothing else going on in my program,
they're just going to end up being back to back to back in this way,
even though it might not in this way.
So what does this really mean is going on?
Well, let's go ahead and poke around.
Let me go back to VS Code here.
Let's close scores.c, reopen my terminal,
and let's create a new program called hi.c, and just do something playful.
So let me include standard io.h at the top.
Let me do int main void after that.
And inside of my curly braces, let's just repeat this.
C1 equals h in caps, char C2 equals i in caps, and then char C3 equals
exclamation point in exclamation point.
That's all.
Now, let's actually poke around and see what's inside the computer's memory.
So I could do something like this.
I could print f, for instance, %c, %c, %c, backslash n, and %c.
This turns out means character.
So what do I want to plug in?
C1, C2, and C3, semicolon.
So let's go ahead and do this.
Make, hi, enter, dot slash hi, and voila.
There's my hi exclamation point.
There's no magic here.
I'm literally just printing out three char variables.
I don't need the spaces.
If I want to get rid of those spaces between the word, I can remake this.
Make hi, dot slash hi, and now we're back in business, hi exclamation point.
But here's where an understanding of types can give you a bit of power
and sort of satiate some curiosity.
What if I change my %c to %i, %i, %i.
So int, int, int.
Well, it turns out that a char is really just a number,
because it's an ASCII value from 0 to 255.
So there's nothing stopping me from telling the compiler, don't print these as char.
Print them as integers.
So let's do make hi, dot slash hi, enter.
And that's a little cryptic.
It looks like it's saying 727,333, but no.
Let me add those spaces back in between each of those placeholders.
Make hi again, dot slash hi.
There are our old friends, 72, 73, 33.
It is not necessary in this case to say int, int, int,
because the compiler is smart enough and printf is smart enough that if you hand it
a value that happens to be a char, it knows already it's going to be an integer,
so you don't even need to bother explicitly casting it this way.
We're essentially implicitly casting it to an integer
by using those format codes as such.
All right, so that just proves that what I've claimed is the case,
that there is this equivalence between characters and numbers,
is actually the case inside of the computer's memory.
So even though you're storing hi exclamation point,
technically you're storing three patterns of eight bits each
that give you these decimal numbers, 72, 73, and 33,
or specifically these patterns here.
All right, then what is a string?
And this is where things get a little more interesting.
A string, as we've used it, is like a whole word or a phrase
or when we started class today, like a whole paragraph of text.
So that's multiple values.
Now, why is that interesting for us potentially?
Well, let's go ahead and write one line of code as a string.
So here, for instance, is one line of code with a string.
Let's go ahead and put that into my program.
So I'm going to go back to VS Code here and clear my terminal.
And I'm going to go ahead and delete all of this code here for a moment.
And I'm going to do something like this.
String s equals quote unquote high with double quotes now.
And now, just like in week one, I'm going to print out percent s backslash n
and print out the value of s.
Per earlier, because string is technically one of our training wheels
for just a few weeks, I'm going to additionally include
cs50.h at the top so that the compiler knows about what this word is, string.
All right, let's go into the terminal, make high, dot slash high, enter.
And we're back in business, printing that out now as an entire string.
Well, what's going on inside of the computer's memory this time?
Well, I still have hi exclamation point, but it's a string now.
Well, it turns out the way that's going to be laid out in the computer's memory
is exactly like before.
There's no mention of c1, c2, c3, because those variables don't exist.
There's just one variable, s, but it's referring to three bytes of memory.
It would seem hi exclamation point.
And you can kind of see where this is going.
Like a string as a spoiler turns out is actually just what?
It's just going to be an array of characters.
Hence the dots we're trying to connect today.
So at the moment, though, this is a single variable, s a string,
the value of which is hi exclamation point.
But you know what, if it is, in fact, an array,
I bet we can start playing around with our new square bracket notation
and see as much in our actual code.
So in fact, let me go ahead and do this.
In VS Code now, let's not use percent s.
Let's use percent c, percent c, and percent c three times.
Then instead of just s, let's print it out like it is an array,
s bracket 0, s bracket 1, s bracket 2.
Let's go back to my terminal in VS Code.
Make hi dot slash hi, and nothing has changed.
But I'm printing it out now one character at a time
because I understand what's going on underneath the hood in this case.
I can actually see these values now.
Let's go ahead and change the percent c to percent i,
and add a space just so it's easier to read.
Percent i space, percent i space.
I don't need my casts in parentheses because printf is smart enough to do this for me.
Make hi again dot slash hi.
There again is my 72, 73, 33.
However, that came from the mere fact that I put in double quotes hi exclamation point.
So what's really happening here is it seems that a string is indeed just an array of characters.
But how does the computer know when doing percent s know what to actually print?
In other words, it stands to reason that eventually,
if I've got more variables, more code, there's
going to be other stuff in the computer's memory.
Why does printf know when using percent s to stop here and not just keep printing characters that are over here,
especially if I did have more variables and more stuff in memory?
Well, let's take a look at what's just past the end of this array.
Let's go back to VS Code, and now let's get a little crazy and add in a fourth percent i.
And even though this shouldn't exist, let's do s bracket 3, which even though it's the number 3,
it's the fourth location, but hi exclamation point is only three values.
So let's look one location past the end of this array.
Make hi dot slash hi interesting.
It seems, and maybe it's just luck, good or bad,
that the fourth byte in the computer's memory seems to be a zero.
Well, that's actually very much by design.
It turns out if we look a little further by convention,
what the compiler will do for us automatically is terminate, that is,
end any string we put in double quotes with a pattern of eight zero bits.
More succinctly, it's just the number zero, because if you do out the math,
where you've got eight zeros, it gives you zero and decimal.
Or more technically, the way it's typically written is this,
because it's not like the number zero that we want to see on the screen.
Backslash zero, similar to backslash n, is sort of a special escape character.
This just means literally eight zero bits, not the number zero
that you might see in a phone number or something like that.
So even though we said string s equals quote unquote hi with an exclamation point,
seemingly three characters, how many bytes does a string of length three
actually seem to take up in memory?
It's actually going to be four.
And this happens automatically.
That's what the double quotes are doing for you.
They're telling the compiler, this is not just a single character.
This is a sequence of characters.
Please be sure to terminate it for me automatically with a special pattern of eight zero bits.
And that special pattern of eight zero bits actually has a name.
It's the so-called null character, or N-U-L for short.
The null character is just a byte of zero bits, and it represents the end of a string.
You've actually seen it before, if super briefly, two weeks ago.
Here was our ASCII chart, and we focused mostly on this column here and this column here.
And then we looked at the exclamation point over here.
But all this time over here, ASCII character zero is null, N-U-L, which just means,
that's how you pronounce all eight zero bits.
It's been there this whole time.
So why is it done this way?
Well, how is the computer actually printing something out in memory?
Well, it needs to know where to stop.
Printf is pretty stupid.
Odds are inside of printf.
There's just a loop that starts printing the first character, the next character,
the next character, and it's looking for the end of the string.
Why?
Well, consider what might happen.
Suppose you've got a program that has not just one string, but two.
For instance, two strings like this.
So in fact, let me go back to VS code here, clear my terminal,
and let's just make this program a little more interesting for a moment.
String t equals quote unquote by, for instance.
And then down here, let's do two printf's.
Percent s, backslash n, and printout s.
Printf, percent s, backslash n, printout t.
Now to be clear, percent s means string placeholder.
t and s are just also the names of the variables.
There's no percent t that we want to use here.
All right.
Let me go down to my terminal, make high dot slash high,
and voila, I get high and by, just like you would have expected last week.
But what's going on inside of the computer's memory?
Well, insofar as I've asked it to create two variables,
s and t like this, odds are what's happening in the computer's memory
is high is ending up here, a.k.a. s.
t, because there's nothing else in this program,
is probably going to end up here, b.y.e. exclamation point.
But it wraps on this particular screen.
t is taking up 1, 2, 3, 4, 5 bytes total.
Just as high is taking up 4 bytes total.
Because the compiler is automatically adding for me
the backslash 0, the null character, to make clear to other functions
where this string ends.
So what does this mean in real terms?
And why is it 0?
Why is it 0?
Like just because, like at the end of the day, all we have is bits.
We've got eight bits to work with for chars.
You've got to pick some pattern.
We could have chosen all ones.
We could have chosen all zeros.
We could have chosen something arbitrary.
A bunch of humans in a room years ago decided eight zeros will mean the null character.
That's the special character we will use to terminate strings in this way.
Well, what does that mean with our new syntax?
Well, it means we could poke around with strings as well.
So even though that first variable is s and that second one is t,
you could technically poke around and access s bracket 0 and 1 and 2 and 3,
t bracket 0, 1, 2, 3, and 4, and so forth.
So in fact, if I wanted to dive in deeply there and actually see that,
well, let me go ahead and do this.
Back in VS Code here.
Let me make a refinement here.
I've now got my two strings here.
I could go and, for instance, down here just like before, percent c, percent c,
percent c, percent c, percent c, percent c, percent c.
And if I then do s bracket 0, s bracket 1, s bracket 2, whoops, 2,
and then down here, t bracket 0, t bracket 1, t bracket 2,
t bracket 3.
And I'm doing that only because the word buy is longer than the word high.
If I do make high dot slash high, same principles work even in this context here.
But let's add an interesting twist just because if I have these values in memory
here as follows, well, if I've got two words in memory,
I could use them in an array too.
Instead of having like s and t or word 1 and word 2,
I can actually put strings in an array too.
So let's go ahead and do this.
Let me go back to VS Code.
And just for fun now, let's go ahead and do this.
Give me an array called words that's going to fit two strings.
Then in the first words, words bracket 0, put high.
Then in words bracket 1, put buy.
The only thing new here is that I'm making an array of strings now
instead of an array of ints.
But all of the syntax is exactly the same.
How can I go about printing these things?
Well, just as before, I can do printf percent s backslash n
and print out words bracket 0.
Then I can do printf quote unquote s backslash n words bracket 1.
Again, I'm just sort of applying the same simple syntax that we saw before,
dot slash high.
Again, of the sixth version of this program, right?
I'm just sort of jumping through syntactically
to demonstrate that these are just different lenses through which
to look at the exact same idea.
And while a normal person would not do this,
we could think about what's really going on in memory with arrays of words
when those words themselves are arrays of characters
because a word is just a string.
So this code here gives us something like this in memory.
In that program a moment ago, this is words bracket 0.
This is words bracket 1.
The only thing that's different is I'm not calling them s and t.
I've given them one name with two locations, 0 and 1.
Well, if each of these values is itself a string,
well, you said earlier that a string is just an array.
So we can actually think of these two strings,
even though the syntax is getting a little crazy,
using two sets of square bracket notation,
where I can index into my array of words
and then index into the individual letters of that word
by just using more square brackets.
And again, this is just to demonstrate a point,
because a normal person would do this.
But if I go back to VS Code, instead of printing out these two strings,
why don't I do something like this?
Printf quote unquote percent c, percent c, percent c, backslash n.
Then let's print out the first word, but the first character therein.
Let's print out the first word, but the second character therein.
The first word, but the third character therein.
And even though I'm saying third and second and first,
we're going to print out the second word, percent c, percent c, percent c,
percent c, backslash n.
Then words bracket.
How do I get to the second word in this array?
Words bracket one, the first character therein.
Words bracket one, the second character therein.
Words bracket one, the third character therein.
Words bracket one, the last character therein.
And again, this is just to demonstrate a point,
but if I do make high now, dot slash high,
we have full control over everything that's going on.
If you now do agree and understand that an array can be indexed into
with square bracket notation as can a string, because a string is itself just an array.
Strings are arrays for today's purposes then.
Questions on any and all of these tricks?
No?
All right.
Yeah, in front.
This is a establishable string.
It was in an array in the first part of the board.
OK.
It was like a full average in this last number.
Is that establishing an array?
Like how do you establish an array?
How do you establish or create an array?
Well, in the context of this program, if I go back to VS Code, line six here
gives me an array of size two, an array of two strings, if you will.
The previous example we were playing with, which was my scores.
Whoops, wrong program.
Wrong file.
If I open up scores.c as before, this line here, line nine,
gives me an array of n integers.
So that is what establishes or creates the array in memory.
You specify a name, the size, and the type.
That's all.
And the only thing that's new today, again, is the square bracket notation,
which in this context creates an array of that size.
But once it exists, you can then access that chunk of memory
by using square brackets as well.
Other questions on arrays?
Yeah, in front.
Are you able to top all of the values in the array as a behavior?
Or do you need to go in index by index to declare an app?
Good question.
Do you need to go index by index to put things inside of an array?
Short answer, no.
So let me open up, again, scores.c from before.
And what I could have done in an earlier version of my program
would be something like this.
I could have done 72, 73, 33.
And I deliberately didn't show this because I didn't want to add too much complexity.
But you can use curly braces in this new way
and initialize the array in one line.
And in that case, you don't even need to specify the size
because the compiler is not an idiot.
It can figure out that if you've got three numbers on the right,
it knows that it needs three elements on the left to put them into.
But let me undo that and leave it just as I did.
But short answer, yes.
You can statically initialize an array if you know all of the values upfront
and not when using getInt in that case.
All right.
So if you're on board with the idea that all a string is, is an array
and that array is always null terminated,
we can now use that knowledge to solve some simple problems
and problems that others have already solved before us.
So let me go ahead and close that file in VS Code.
Let me go ahead and open up another program here called length.c.
And let's just play around with the length of strings as follows.
Let me include the CS50 library at the top.
Let me include standard IO after that.
Let me do int main void after that.
And then inside of main, let's prompt the user for their name
by using getString and just say name colon today.
And then after that, let's go ahead and figure out the length of the person's name.
Like DAVID, I should get the answer of five.
And KELLY, we should get the answer of five.
And hopefully for a longer or shorter name,
we'll get the correct answer as well.
So how can I go about counting the number of characters in a string?
Well, the string is just an array and that array ends with the null character.
There's a bunch of ways we can do this, but let me go ahead and do this.
Let me create a variable called n, which eventually will contain the length of the name.
And I'm going to set it equal to zero because I don't know anything yet about the length.
Then I can do this with a for loop, but I prefer this time to use a while loop.
I'm going to say the following.
While the person's name at that location does not equal backslash zero,
go ahead and add one to the value of n.
And then after all of this, go ahead and print out with percent i backslash n the value of n.
So what's going on here?
This is easier said when you know already where you want to go with it,
but with practice you too can bang this out pretty quickly.
n is going to contain the length of my string.
I have in my loop here a Boolean expression that's just asking the question,
does name at the current value of n not equal the null character?
In other words, you're asking yourself, is this character null?
Is this character null?
Is this character null?
Is this character null?
Is this character null?
And if not, you keep going.
You keep going.
And this is kind of a clever trick because I'm using n and incrementing it inside the loop.
So when I look at d, that's not equal to backslash zero.
So I increment n.
Now n is one.
So I look at name bracket one.
What's it name bracket one?
If it's my name?
a.
a does not equal backslash zero.
So it increments n.
What's it location two in d, a, v, id?
v.
v does not equal backslash n.
So we repeat with i.
We repeat with d.
And then we get to the end of my name, which is the null character,
because the getString function and c put it there automatically for me.
The null character does equal backslash zero and does not get incremented any more time.
So at this point in the story on line 13, n is still five, because I have not counted
the null character.
So I hope I will see five on the screen.
This is just kind of a very mechanical way of checking, checking, checking,
checking, trying to figure out through inference how long the string is,
because it's as long as it takes to get to that backslash zero, the null character.
So let's do make length enter dot slash length.
Type in my name, David, and I indeed get five.
Let's go ahead and do dot slash length.
Kelly, I indeed get five.
And hopefully for shorter and longer names,
I'm going to get the exact same thing too.
In fact, we can try a corner case, dot slash length enter.
Let's not give it a name at all.
If I just hit enter here, what should the length of the person's name be?
Zero, which is not incorrect.
It's literally true.
But that's because we're going to get back essentially quote unquote.
But even though it's quote unquote in the computer's memory,
it's still going to take up one byte, because the get string function will
still put null at the end of the string, even if it's got no characters therein.
So it turns out this is not something you need to do frequently,
like initializing a variable, using a loop like this.
It turns out there are better solutions to this problem.
You do not need to reinvent this wheel yourself,
because it turns out, in addition to standard io.h and cs50.h,
and as you probably saw in problems at one, math.h and perhaps others,
there are other libraries out there, namely the string library itself.
In fact, if you go into the cs50 manual, you can look up the documentation
for a header file called string.h, which contains declarations for,
that is prototypes for, a whole bunch of helpful functions.
In fact, the manual pages for it are at this URL here.
The most important function, and the one we're going to use so often
for the next few weeks, is wonderfully called stirlang for string length.
Someone else literally decades ago wrote the code that essentially looks quite
like this, but packaged it up in a function that you and I can use,
so we don't have to jump through these stupid hoops just to count the length
of a string.
We can just ask the stirring length function what the length of a string is.
But odds are, if we looked at the C code that someone wrote decades ago,
it would look indeed quite like this.
So how can I simplify this program?
Well, I can get rid of all of this code here.
I can include string.h at the top of my file.
And then I quite simply could do something like this.
int length equals stirlang of name.
That's going to put in the variable length.
Actually, let's keep it consistent.
int n equals stir length of name.
And then on line nine, let's print it out.
Let's try this.
Make length dot slash length.
David.
OK.
Kelly.
OK.
And no one.
And zero.
It seems to now be working.
So this is a wheel we do not need to reinvent.
And frankly, now in a matter of design, I don't really need the variable n anymore.
Recall that we can nest our functions just like we did with average before.
So let me get rid of that line and just say,
stirlang of name is actually perfectly reasonable here.
All right.
So what more can we do with this?
Well, let's consider some other matters of design.
Let me close out length.c.
And let's create another program of our own called string.c,
in which we'll play around now with this library and others.
Let me go ahead and include cs50.h.
Let me go ahead and include standard io.h.
Let me go ahead and include also string.h.
All right.
What do I want to now do?
Well, int main void and inside of main, let's go ahead and write a program that
prints a string character by character just to demonstrate these mechanics.
So string s equals getString.
And I'm going to ask the user for some input,
because I just want to play around with any old string.
I'm going to go ahead and proactively say output here.
And I'm going to go ahead and not use a new line character there deliberately.
Below this now, I'm going to have a for loop, though I could use a while loop,
that says int i equals 0 i is less than stirlang of s, the string I just got from the human,
and increment i on each iteration.
And on each iteration, print out just one character in that string, specifically at s location i.
And then at the very bottom of this program, let's just print a single backslash n
to move the character onto a new line.
Long story short, what have I done?
I wrote a stupid little program that prompts the user for a string,
prints the word output thereafter, and then it prints the word that they typed in character by character by character by character
until it reaches the end of the string based on the length returned by stirlang.
So let's go ahead and run this.
In my terminal window, I'm going to do make string dot slash string,
and I'll type in my own name and before.
This was a subtlety.
I deliberately wrote two spaces here, because to be nitpicky,
I wanted input and output to line up perfectly.
So you can see what's happening.
Indeed, if I do enter here, now I see input is David, the output is David as well.
So that was just a formatting trick that I foresaw.
Why is this program correct but not arguably well designed?
It's pretty good in that it's using the stirlang function.
I didn't reinvent a wheel unnecessarily, but there's an inefficiency that's kind of subtle.
And it relates to how a for loop works.
Any thoughts?
This program, I claim, is doing unnecessary work somewhere.
Why do you have to output a character by character?
OK.
That's definitely stupid.
You don't have to output a character by character.
That's just my pedagogical decision here.
So correct, but not the question we're fishing for.
There's a second stupid thing.
Yeah?
Every time through this loop, and this isn't so much my conscious choice,
but my mistake, I'm checking the length of s again and again.
Why?
Because recall how a for loop works.
The initialization happens once at the very beginning.
Then you check the Boolean expression.
Then if it's true, you do the code.
Then you do the update.
Then you check the Boolean expression.
Then you do the code.
Update, Boolean expression, you do the code.
But every time you evaluate this Boolean expression, you're asking,
is i less than the stir length of s?
But this is a function call.
Like you are literally using stirlang again and again and again.
And like a crazy person, you're asking the computer, what's the length of s?
What's the length of s?
What's the length of s?
What's the length of s?
It's not going to change.
It's going to be the same no matter what.
So how can we fix this?
Well, I could solve this in a couple of ways.
Like I could, for instance, down here, do int n equals stirlang of s
and store it in a variable n and just do that.
I think that eliminates the inefficiency because now I
calculate the length of s once.
It's not going to change, nor is my variable.
So I can now use and reuse that variable.
It's just saving me a little bit of time, microseconds maybe.
But when you're writing bigger programs and you're doing things in loops,
if that loop is running not three times or five, but a million times,
millions of times, all of those microseconds, milliseconds,
might very well add up.
But it turns out there's some syntactic tricks we can do too.
I alluded to this earlier.
If you want to initialize not one variable but two,
you can actually do it all before the first semicolon, like that.
So now on line 9, I'm declaring a variable called i and setting it equal to 0.
And I'm declaring a second variable called n, also the same type, int,
and setting it equal to the length of s.
And now I can use that again and again.
Now as an aside, this is a little bit of a white lie
because smart compilers nowadays are so advanced
that they will notice that you're calling sterling again and again inside of a loop
and they will just fix this for you, unbeknownst to you.
But it's representative of a class of problems
that you should be able to spot with your own human eyes and avoid altogether
so that you don't waste more time and more compute and more money in some sense
than you might otherwise need to in this case.
Any questions on that there?
Optimization, yeah.
You do not say int again.
The constraint is that you have to use the same data type for all of your initialization.
So you better hope that you only want ints in this case.
Otherwise, you've got to pull it out and do what I did earlier.
Good question.
Others on this?
Yeah?
When does it account for spaces?
A space is just a character, an ASCII character number 32.
So there's nothing special about it.
It's sort of invisible, but it is there.
It is treated like any other character.
There's no special accounting whatsoever.
The null character, which is also invisible, is special because printf and sterling
know to look for the end of that variable, the end of that value as such.
All right, let's try one other demonstration of some of these ideas here.
Let me go into another file that we'll create called how about uppercase.c.
Let's write a super simple program that uppercase is a string that the human types in
and see how we can do this sort of good, better, and best.
So I'm going to call this file uppercase.c inside of this file.
Let's use our now friends, include cs50.h.
Let's do include standard IO.h.
Let's then include lastly how about string.h.
And the goal here inside of main is going to be to get a string from the user.
So string s equals get string.
And we're going to ask the user for a before string, representing what it is they typed
before we uppercase everything.
Then I'm going to go ahead after that and print out just as a placeholder after
and two spaces just to be nitpicky so that the text lines up vertically on the screen.
Now I'm going to do the following for int i equals 0, n equals sterling of s,
semicolon i less than n, just like before, i plus plus.
So I'm just kicking off a loop that's going to iterate over the string the human typed in.
Now, if my goal in life is to change the user's input from lower case, if indeed in lower case
to uppercase, let's just express that literally.
If the current character in the string, so s bracket i, is greater than or equal to quote
unquote a, and s bracket i is less than or equal to quote unquote z using single quotes,
this is arguably a very clever way of expressing the question, is it lower case?
We know from our ASCII chart from week 0 that the ASCII chart has not only numbers
representing all the uppercase letters, but also numbers representing all the lower case letters.
Lower case a, for instance, is 97, and they're all contiguous thereafter.
So we can actually treat just like we did before, chars as ints and ints as chars
and sort of ask mathematical questions about these chars and say,
is s bracket i between a and z inclusive?
So if it is lower case, and I'll add a comment here for clarity,
if s bracket i is lower case, what do we want to do?
We want to force it to uppercase.
So this is a little trick I can do as follows.
Print f, the current character, but let's do some math on it.
Let's change s bracket i by subtracting some value.
Well, what might that value be?
Well, recall from week 0, our ASCII chart here.
And let's focus, for instance, on the lower case letters here
and the uppercase letters here.
What's the distance between all upper and lower case letters?
It's 32, right?
And the lower case letters are bigger.
So it stands to reason, if I just subtract 32 from the lower case letter,
it's going to immediately get me to the uppercase version thereof.
So this is kind of cool.
So I can actually go back to VS Code, and I can literally subtract the number 32
in this case, because ASCII is a standard, it's not going to change.
Else, if the letter is not lower case, I'm just going to go ahead
and print it out unchanged without doing any mathematics at all to it.
And I'll make clear with a comment, else if not lower case,
makes clear what's going on there.
All right, let me go ahead and make uppercase in my terminal window,
dot slash uppercase, let's type in my name all lower case,
and I get back, David, oh, minor bug, couple bugs, actually.
Let me fix my spacing.
I think I want another space after the word after.
And at the very bottom of my program, I think I want a backslash n.
Now, let's rerun, make an uppercase, dot slash uppercase, enter, d-a-v-i-d.
And now, it's forcing it all to uppercase.
Meanwhile, if I do it once more and type in name capitalized,
it's still going to force everything else to uppercase.
Questions?
Oh, I'm an idiot.
OK, thank you.
Yes, I misspelled after, otherwise my alignment would have worked.
So let's do this again, make uppercase, if only so that we can prove it's the same,
d-a-v-i-d in all lower case, and there we go.
That was, thank you, the intent.
All right, so it's kind of a little trick, but this is kind of tedious, right?
Like Microsoft Word, Google Docs all have the ability to toggle case
from uppercase to lowercase or lowercase to uppercase.
It's kind of annoying they have to write this much code to achieve something so simple, seemingly,
and so commonplace.
Well, it turns out there's a better approach here, too.
In addition to there being the string library, there's also the ctype library.
In ctype.h, another header file, there's a whole bunch of other functions that are useful
that relate to characters in ASCII.
So for instance, if we go ahead and use this as follows,
I'm going to go ahead at the top of my file here and include now ctype.h.
It turns out there's going to be functions via which I can actually ask these questions myself.
For instance, in this next version of the program, I don't need to do any of this clever but pretty verbose math.
I can just say if the is lower function, which comes from the ctype library,
passing in sbracket i returns true will then convert the letter to lower uppercase by subtracting 32.
But I don't even need to do this mental math or math and code.
I can also, from the ctype library, use a function called toUpper, which takes as input a character like sbracket i
and let someone else's function do the work for me.
So let me go back down to my terminal window here.
Let me make uppercase now dot slash uppercase enter before david.
This now works too.
But if I really dig into the documentation for the ctype library,
you'll see that you can just use the is lower function on any character,
and it will very intelligently only uppercase it if it is actually lowercase.
So someone else years ago wrote the conditional code that checks if it's between little a and little z.
So knowing this, and you would see that indeed in the documentation, I don't even need this else.
I can instead just get rid of this whole conditional, tighten my code up significantly here,
and simply say printf using percent c, the two upper version of that same letter.
And let the function itself realize if it's uppercase, pass it through unchanged.
If it's lowercase, change it first and then return it.
So now if I open my terminal window again and clear it, make uppercase dot slash uppercase enter david.
And we're back in business.
So again, demonstrative of how if you find that coding is becoming tedious
or you're solving a problem that surely someone else has solved, odds are there is in fact a library function for it,
whether it's from CS50 or from the standard library that you yourselves can use.
And unlike the CS50 library, which is indeed CS50 specific,
which is why Clang needed to know about dash L CS50, many of these libraries just automatically work.
You don't need to link in the C type library.
You don't need to link in other libraries.
But non-standard libraries like CS50's training wheels for the first few weeks, we do need to do that.
But make is configured to do all of that automatically for you.
All right.
In our final minutes together, let's go ahead now and reveal some of the details we've been sweeping under the rug about main.
I asked on week one that you just sort of take on faith that you've got to do the void.
You've got to do the int.
You've got to do the void and all of that.
Well, let's see why that actually is.
So main is special.
Insofar as in C, it is the function that will be called automatically after you've compiled and then run your code,
just because not all languages standardize the name of the function, but C and C++ and Java and certain other ones do in this case.
Here is the most canonical, simple form of main.
We know that including standard io.h just gives us access to the prototypes for functions like printf.
But what's going on with int and what's going on with void?
Well, void in parentheses here just means that main, and in turn, all of the programs we've written up until this moment, do not take command line arguments.
Literally every program we've written, dot slash a dot out, dot slash hello, dot slash scores, dot slash everything else,
I have never once typed another word after the name of our programs that we've written in class.
That is because every program has void inside of these parentheses telling the computer this program does not take command line arguments,
words after the program's name.
That is different from make and code and CD and other commands that you've typed with words after their names at the prompt.
But it turns out the other supported syntax for the main function in C can look like this too,
which at a glance looks like kind of a mouthful, but it just means that main can take zero arguments or it can take two.
If it takes two, the first is an integer and the second is an array of strings.
By convention, those inputs are called argc and argv.
Argc is the count of arguments that are typed after the program's name.
Argv is the argument vector, aka array of actual words.
In other words, now that we have the ability to use arrays,
we can get zero or one or two or three or more words from users at the prompt when they run our own programs.
So what do I mean by this?
We can now write programs that actually have command line arguments as follows.
Let me go into VS Code here and close our old program uppercase.
Let's write a new simpler program here in my terminal called greet.c and just greet the user in a couple of different ways.
So I'm going to include initially cs50.h and then I'm going to include standard io.h here.
Then I'm going to say int main void without introducing anything new just yet.
I'm going to ask the user like we did last week for return value from get string,
asking them what's your name as we've done so many times.
Then I'm going to say printf hello comma percent s backslash n spitting out their answer as follows.
Same program as last week again.
I'm going to make greet.
I'm going to say dot slash greet and I'm prompted now for my name.
I hit enter.
Notice that I did not take any command line arguments.
The only command I ran was dot slash greet no other words.
Let's now use this new trick and actually let the user type their name when they're running my program rather than waste their time by using get string and prompting them.
Let me go into my editor here.
Let's get rid of the cs50 library.
Let's get rid of my use of get string and let's simply change void to int argc then string argv open print open bracket close bracket.
That's all down here.
Let's simply print out argv bracket one for reasons we'll soon see the only change that I'm making really is changing the prototype for main from the first version,
which we've been using for like a week and a bit now to the second version, which is the only other version supported.
I'm going to go back to my terminal window now.
Make greet and darn it.
I shouldn't so close.
Why did I make?
How do I fix the mistake I accidentally made?
Yeah, back.
Oh, no, in front.
Yes, I should have kept the cs50 library because it's in the cs50 library that string is defined.
So include cs50.h in week four, we will delete that line for real and actually show you what string actually is.
I promised at the start of class that string is a term of art, but it's not a keyword in C.
But it will see what it means in a couple of weeks time.
OK, let me fix this.
Make greet dot slash greet.
But now I'm going to type before I even hit enter my actual name.
And when I hit enter now, I see hello comma David.
If I instead do dot slash greet kelly, enter.
Now I see hello comma kelly.
If I do nothing like greet, enter, I just see hello null, which is not the same null as before, nul.
This is nul for reasons we'll come back to before long.
But clearly printf knows something's going on.
There's no actual word there.
Why, though, did I do argv bracket one?
Well, it turns out that just as a feature of C, if I recompile this program and do dot slash greet,
and type in nothing else, I'm going to see something kind of curious, hello dot slash greet.
Because automatically, the zero location in the argv variable
will automatically contain the program's own name.
Why is this useful?
If you ever want to do something self-referential, like thanks for running my program,
or you want to show documentation for your program in the name of your program,
that it depends on whatever the file itself is called,
you can use argv bracket zero, which will always contain the program's name,
no matter what the file has been named or renamed to.
But we can fix that null issue now in a couple of ways.
So argc is the other input that I said now can exist, which is the count of arguments at the prompt.
So if I want to check if the user actually typed their name,
I could say something like, if argc equals equals two, well, then, and only then,
go ahead and print out their name.
Else, let's just do some clever default, like printf, quote unquote, hello world, or heck, nothing at all.
This version of the program now is a little smarter,
because when I run make greet and do dot slash greet of my name, works exactly as intended.
But if I forget and only do dot slash greet, it's going to say hello world.
Moreover, if I don't quite cooperate and I say, David Maylan, enter,
it similarly just ignores me because arg count is not two anymore.
It's now three.
So argc contains the total numbers of words at the prompt,
but the first one is always the program's name.
Question?
Sorry, can you say that once a little louder?
Why is it information that we just have, or?
Oh, so the short answer is just because.
Like the definition of C, if you look at the documentation for C,
you can either define main as taking no arguments with the word void,
or you can specify that main can take two arguments.
And the compiler and the operating system will just ensure that if you provide two,
those two variables, argc and argv, will be filled with those two values automatically.
Someone else decided that, though.
That's just the way it works.
You can't put three there.
You can't put four there.
You can change the names of those variables, but not the types.
Because of this convention.
So there's one last feature of main, then.
It's the actual value with returns.
Up until now, every program I've written starts with int main something.
Int main something.
What is that int?
We have yet to use it.
Technically, the value that main returns is going to be called a so-called exit status,
which is a numeric status that indicates success or failure.
Numbers are everywhere in the world of computing.
So for instance, here's a screenshot from Zoom whereby if something goes wrong with Zoom,
like you have bad internet connectivity or something like that,
you might see an error code like 1132.
That means nothing to normal people unless you Google it, look up the documentation.
But it means something very much to the software engineers who wrote this code
because they know, oh, shoot, 1132 means this error.
And they probably have a spreadsheet or a cheat sheet
somewhere that converts those codes to actually useful error messages.
And frankly, in a better world, they would just tell you what the problem is
rather than just say report the problem and mention this number.
That said, on the web, odds are you're familiar with this number, 404,
which is also a weird thing for so many normal people to know.
But this generally means file not found.
It's a numeric code that signifies that something has gone wrong.
Exit status isn't quite this, but it's similar in spirit.
In main, you can return a value like 0 or 1 or 2 or something else
to indicate whether something was successful or not.
By convention, a function like main returns 0 on success, if all is well.
And that leaves you then with several hundred possible things that can go wrong
because you could return one to signify one thing, two to return another,
three to signify another.
And so long as you have a spreadsheet or a cheat sheet or something,
you can just keep track as the programmer as to what error means what.
So what does this mean in real terms?
Well, if I go over to VS Code here, let me implement a relatively simple program,
our last, called status.c.
So in status.c, I'm going to go ahead and use the CS50 library at the top,
the standard IO library at the top.
And then inside of int main and with our new format int argc string argv
square brackets inside of main, I'm going to now do the following.
If argc does not equal 2, then I'm going to go ahead and print out this time a warning.
I'm not going to have some silly default like hello world.
Let's tell the user that they didn't use my program correct.
And I'm going to say printf missing command line argument.
And we'll assume they know what that means.
Then to signify an error, I'm going to say return 1.
It could be 2.
It could be 3.
But this is the first possible error, so I'm going to start simple with 1.
Otherwise, if argc does equal 2 and I get to this part of my code,
I'm going to say hello comma percent s backslash n and pass in argv bracket 1
just like before.
And just to be super specific, I'm going to return 0 to tell the computer,
the operating system, that this is success.
0 signifies success.
Any other value signifies error.
Let's make status now.
Let's do dot slash status.
And this is a little magical.
But let me go ahead and cooperate initially.
I'm going to type in my name David.
And I'm going to see hello comma David.
Most people wouldn't know this, but among the commands
you can type at your terminal are this one here.
And the TFs and I, the TAs and I would do something like this.
We, after running your code, can do echo, space, dollar sign, question mark.
And we can see secretly the return value that your program returned, 0 in this case.
Meanwhile, if we do this again, dot slash status and let me not type my name this time.
When I do this, I see missing command line argument.
What value should the code have returned then?
1.
So let's see.
Echo, dollar sign, question mark.
There's the 1.
So even after just one week of CS50, if you've ever wondered how check50
knows if your code was correct or not, among the ways we check for that
is by checking this semi-secret status code, this exit status, which isn't really a secret.
It's just not displayed to normal people because it's not all that enlightening
unless you're the software developer who wrote the code in question.
But this means we could return 1 in some cases or 2 in other cases
or 3 or 4 in yet others.
And these command line arguments are sort of everywhere.
And in fact, a program I skipped over a moment ago was going to be this.
There is no academic value to what you're about to see.
But another program that takes command line arguments is known as COSE.
And this is sort of very famous in computing circles
because it's been on systems for many years.
COSE is a program that allows you to type in a word after the prompt like moo.
And it will print out what's called ASCII art.
An adorable little cow with a speech bubble that says moo.
So kind of evocative of like scratch.
But it takes other command line arguments, not just the words
that you want to come out of its mouth, but even the appearance that you want it to have.
So for instance, I can say dash f duck and run it again, enter.
And now I have a little cute duck saying moo, which is a bit of a bug.
So let me change that to quack, for instance, instead.
Again, no academic value here.
It's just fun to now play with the various options.
But if we really want to have fun with this, we can do another one.
So COSE dash f dragon.
And we can say something like roar.
And now we have this crazy dragon appearing on the screen, which is to say, again, no value here.
It's just fun to play with command line arguments sometimes.
And how is COSE doing this?
Well, someone wrote code maybe in C or some other language using argc and argv
and poking around at their values and maybe a conditional that says if the dash f value is dragon,
then print this graphic.
Else if the value is duck, then print this other one.
It all boils down to the same fundamentals of week 0 of functions
and conditionals and loops and Boolean expressions and the like.
It's just being composed into more and more interesting things.
And indeed, in closing, among the other interesting things we'll play with this week
to come full circle is that of cryptography, the art of scrambling information
so as to have secure communication.
So important nowadays with passwords and credit card
numbers and personal messages that you might want to send.
And we'll have you explore through code some of the algorithms via which you yourselves
can encrypt information.
And there's a number of ways we can do this form of encryption.
And they all boil down to this mental model.
You've got some input like the message you want to send.
And you want to encipher it somehow, encrypt it somehow
so that no one knows what message you've sent.
So you want your plain text, which is the human readable version in English
or any other language, to become cipher text ultimately.
So the code you'll be writing this week is inside of this black box some kind of cipher
an algorithm that encrypts information so that you can do exactly this.
Now the catch is that you can't just give it plain text
and run it through an algorithm and get cipher text
because you need to somehow have a secret typically for encryption to work.
Like if I'm going to send a message to someone in back,
well, I could just randomize the letters that I'm writing down.
But how would they know how to reverse that process?
Probably what we need to do is agree in advance that, you know what?
I'm going to change every A to a B and every B to a C and a C to a D
and a Z to an A. I'll wrap back around at the end of the alphabet.
It's not very sophisticated, but who know, middle school teacher,
if they intercept two kids passing notes in class,
are going to waste time trying to figure out this cipher.
But it does presuppose that there's a secret between them, the number one in that case,
because I'm changing every letter by one place.
So how might this work?
Well, if I want to encrypt the word high, hi, exclamation point,
and my secret key with someone that I've come up with in advance is one,
I should send the cipher text ij exclamation point.
Now, this is a simple cipher, so I'm not really encrypting the punctuation,
which may or may not be a good thing.
But I am encrypting at least the alphabetical letters.
But what does the recipient then have to do to decrypt this message?
When they see on paper ij exclamation point, how do they know what I said?
Well, they use that same key, but subtract.
So B becomes A, C becomes B, A becomes Z, and so forth,
essentially inverting the key from positive one to negative one.
Of course, slightly more secure than a cipher of one, a key of one would be 13.
And in fact, in computing circles, 13 has special significance.
ROT 13, R-O-T 13 is an algorithm that's been used for many years online.
Just to sort of avoid spoilers, like Reddit might do this or other websites
where they want you to have to do some effort to see what the message says.
But it's not all that hard.
You just have to click a button or write the code that actually does this.
But if you use 13 instead, you wouldn't get ij, you'd get uv.
Because u and v are 13 places away from h and i, respectively.
But again, we're not touching the punctuation.
Or we could send something more personal, like I love you,
and the message comes out like that.
Slightly more secure than that would be ROT 26, no?
No.
Why?
Because it's the same thing.
It literally rotates all the way around.
A becomes A, B becomes B. So there's a limit to this.
But more seriously, that speaks to just how strong this encryption is or is not.
Because if you think about this now from an adversary's perspective,
like the teacher in the room intercepting the slip of paper,
how much work do they need to do?
Well, they just try all possibilities.
Key of one, key of two, key of three, dot, dot, dot, key of 25.
And at some point, they will see clearly that they guessed the key, which
means that cipher is not very secure.
Nonetheless, what we're talking about is historically known as the Caesar cipher.
Because back in the day, when Caesar was communicating by legends with his generals,
if you're the first human on earth to come up with encryption or come up
with this specific cipher, it doesn't really matter how not complex it is
if no one else knows what's going on.
Nowadays, it's not hard at all to write some C code or any other language that
could just brute force their way through this.
So there are much more sophisticated algorithms nowadays than simple rotations
of letters of the alphabet as we'll soon see.
But when it comes to decryption, it really is just a matter of reversing that process.
So this message here, if we rotate all the letters in the opposite direction
by subtracting one, will be our final flourish for today.
There's a bit of a hint there, which will reveal that this message
and our final words for us as the clock strikes 4.15 is going to be the u becomes t
and the i becomes h.
I'm the only one this is amusing.
h i s w a s c s 50.
And this was CS50.
We'll see you next time.
We'll see you next time.
