CS50 x 2024 Notes Memory - 06

📅 2026/8/24 2:20:59
CS50 x 2024 Notes Memory - 06
⑴What is null ? It is just an address, and its literally the address zero. So theres this theme N-U-L, recall was the terminating symbol, which just means the string ends here. N-U-L-L, which is not greatly named, but its what humans went with years ago, just means that this is the address zero. And what your computer does is, even though Ive been playfully saying that, Oh, in the top left is address zero, and then one, and then two and then three, the address zero is hands off. Its kind of a wasted byte that your computer should never use because the computer uses zero as a special sentinel value, null, to signify error. So were spending one byte out of billions nowadays just to make sure that theres a special symbol thats coming back that can indicate when something has gone wrong.⑵So let me give you a tool with which to make some of this stuff easier, so that when you make mistake or have bugs, as you invariably will, you can chase them down without having to raise your hand. You actually have more technical tools with which to diagnose the problem yourself. And theres this new tool that well introduce today called valgrind.And valgrinds purpose in life is to check your usage of memory for you. Admittedly, its an older program. Its pretty arcane in terms of its interface, and theres just going to be a mess of output on the screen. But theres going to be certain patterns of mistakes that youll notice, and Ill demonstrate a couple of them now, so you can see where and how you might go wrong.So Im going to go over to VS Code here. Im going to create a program called memory.c that is deliberately buggy, but its not going to be obviously buggy at first. So by that I mean this let me do include stdio.h. Let me also include proactively stdlib.h, so I can use malloc. Let me declare main with no command line arguments and let me do something very simple. Instead of just declaring an int called x, let me be a little crazy and manually allocate this memory myself. So int x just gives me an integer and it has since week one. But now that I have malloc, I can kind of take control over this process. So let me declare, not an int, but an int star called x. So if it give me the address of an integer and let me store there the return value of malloc by asking malloc for, lets say, four bytes. So I know that ints are four bytes. If I want four bytes, I just tell malloc, give me four bytes. Now, frankly, this is a little stupid I shouldnt just assume that the int is always going to be four bytes on everyones computer.So theres this function you can start using called sizeof, or this operator, technically where you can say sizeof int. And even if youre on an older computer, for instance, really old at this point, sizeof int will return the correct value, no matter what. You dont have to assume that its, in fact, four. But you know what ?Id actually like more than this number of ints. Let me actually treat x as an array of integers. So actually, if I want an array of integers, I could do this. Give me three integers.But no, no. Let me not do week two syntax. Let me do this myslef as follows. Let me treat this as three time the size of an int. So thats technically going to give me 12 bytes. But this makes x effectively an array. And this is kind of deliberate now because if an array is just contiguous memory, and malloc returns to you a chunk of contiguous memory, you can treat what comes back from malloc as an array. And indeed, thats waht were doing as strings. Were treating chunks of memory as arrays of chars.So let me do something arbitrary here. Let me go to x bracket one and set it equal to 72, x bracket two, set it equal to 73, x bracket three, set it equal to 33. And we did this a couple of weeks ago. Thats HI! but in ASCII Code.Let me go ahead and make memory, and it seems to work fine. Let me do ./memory and no problem. Theres no error messages from the compiler. Theres no runtime errors when I actually run the code. But does anyone see any of the bugs thus far ?It doesnt seem to know when the array ends. Or more specifically, Im not respecting when the array ends because Im sort of stupidly starting at one, then two, then three. But technically, if I asked for three of these things, I should have done bracket zero, bracket one, bracket two. And theres a second more subtle bug that you would only know from today. Ok, I dont necessarily know when one integer ends and the next one begins. Thats actually not a problem, because on a given system, integers are always the same size. So the computer can be smart enough to go from here, four bytes this way, four bytes this way, four bytes this way. Thats ok. Strings are problematic because who knows how big the sentence was that the human typed in. But theres a more subtle bug. What have I not done ? I didnt call free. So I didnt practice what I just preached. Anytime I malloc, I call free. But again, per my terminal window, neither of these bugs seem obvious. You might submit this code, or deploy it to your software, and be none the wiser. But a tool like valgrind can actually help you find these things.Let me run this command valgrind on my program. So ./memory is how I ran it a moment ago. Just like debug50 you type before the name of your program. Valgrind, you type before the name of your program.And the output is going to look crazy, but this is useful. Why ? So notice at the very top of this, were just seeing what version of valgrind, were using and what command we ran.But this starts to get juicy, and Ill highlight this here. Invalid write of size 4. So writing means changing information, like setting a value or assigning it a value and this is useful here. The problem is in memory.c at the line nine. So colon nine means line nine.So let me go back to my code, look at line nine, and, do invalid write of size four. So its cryptic, but size four I know is the size of an integer. So Im probably doing something stupid on line nine involving changing an integer. And sure enough, even though its not super obvious, x bracket three, this doesnt exist.So I have change the problem. One and two were ok, even though its logically the wrong thing. Now I think this will get rid of this error.Let me recompile my code because I made a change. Let me rerun valgrind of ./memory. And now, that error went away.But this is interesting here now. 12 bytes in one blocks are definitely lost in loss record one of one. So unnecessarily verbose, but the hint here is that I somehow lost some bytes, otherwise known as a memory leak. So earlier, when I described an imaginary bad programmer who kept calling malloc, malloc, malloc, and never freeing, thats whats called a memory leak, where youre sort of losing track of your memory and never freeing it again.So Ive definitely lost 12 bytes in one block, whatever a block is in this case. This is a little less obvious.Its up to us to notice that, ok, wait a minute, memory.c line six is somehow germane.Let me go back to, oh, this is where I called malloc. And valgrind doesnt necessarily know when I should free the memory.Thats up to me, but I should probably free it at the end of my function when Im definitely done with it, because once you free your memory you should not touch that variable again, unless you actually change what its value is. So now, as Ive done this, and this program to be clear does nothing useful. This is just an intellectual exercise, not anything productive.Let me do make memory one last time. Lets do valgrind ./memory. And even though its still kind of output, its still kind of cryptic, at least it says no leaks are possible. So now this is my own sort of teaching assistant telling me before I submit the code, or before I deploy it to production in real software, that at least there seem to be no memory related errors. So valgrind is not for logical bugs. Its not for syntax errors. Its for memory related bugs, as of today.⑶It turns out that garbage values are a thing. And recall that, if you declare a variable but dont give it a value with an equal sign, and you just blindly start using it, like printing it out, or doing math on it, you might be manipulating a garbage value, which is some number thats essentially remnants of your computer having been on for a while. Because if youre using this canvas and reusing it again and again, surely theres going to be patterns of zeros and ones there that you didnt put there yourself, at least in the moment. They might be remnants of the past.So garbage values are values of variable that you did not proactively set yourself as intended.So we can actually see this. Let me go ahead and open garbage.c. And in here, Ill do include stdio.h and then int main void. And then inside of the curly braces, lets give me a really big array of scores, like 1024 scores, like if its a really busy semester. And then let me go ahead and just blindly iterate from i equals zero, on up to i is less than 1024. And Im not going to bother with constants. Im just going to play around with these numbers for a moment. So in my loop here, Im just going to do something stupid, like print out all of the values in the scores array using %i, even though I did not put anything in this array. So on line five, Im obviously declaring an array of size 1024, for that many ints, but Im never actually putting values in there myself, or with get_int or any other function. So theres garbage values there. Theres presumably 1024 garbage values there, and we can now actually see them.Let me make garbage, no pun intended, ./garbage. And theres going to be way more than even fit on the screen. But who cares ? We just need to see a few.There are some of the garbage values in the array. So make super clear that when you create variables of your own, you do not give them values or your own. Who knows what may be there ? In some cases, it gets automatically initialized for you to all zeros, but that is not always the case. And in general, distrust the variable unless you yourself have put a value there. How now might we think about potential problems ?Well, consider this code here, which this program, too, is more for discussion than actual utility, where at the top of it, I declare a variable called x and a variable called y, both of type pointer. So x and y are supposed to be the addresses of two integers, malloc, the size of an int, and stored in x. So Im giving myself space for x, even though, obviously, I could have done this weeks ago by just not using the star and just say give me an int x. Now, Im doing it the low level way, mallocing the x for myself. Im then saying go to x, go to that address in memory, and put the number 42 there. Im then saying go to y and put the unlucky number 13 there. But whats worrisome about this line here ?After this line, this line, this line, somethings bad. Yeah, I never allocated memory for y. So specifically, I never assigned y a value, which means its a garbage value, which is still a number. Maybe its zero. Maybe its a big number. Maybe its a negative number. And if its a positive number, it could be an actual address somewhere in the computers memory. But star y means go there. Who knows what memory Im touching ? Thats how computers crash if you touch memory that youre not supposed to.So let me pretend that I didnt at least do this and let me just forge ahead and set y equal to x, so theyre the same. And I think what that would mean is now, if I do star y and go to the address, thats the same thing as going to the address in x. And I think this will have the effect of changing the 42 to 13. So this code is correct, so long as I dont blindly dereference y by using star y notation. So this gets a little abstract, even though this is just an exercise here.This code allocates two pointers, which can point to integers.Well, I see the two pointers, but they dont seem to be pointing to anything. Thats right, initially, pointers dont point to anything. The things they point to are called pointees, and setting them up is a separate step. The pointees are separate. So how do you allocate a pointee ?Well, this code allocates a new integer pointee, and this part sets x to point to it.Hey, that looks better. So make it do something.Ok, Ill dereference the pointer x to store the number 42 into its pointee.This is what the code looks like. So doing a dereference on x follows the arrow to access its pointee, in this case, to store 42 in there.Try using it to store the number 13 through the other pointer y.Oh, that didnt work. I dont think dereferencing y is a good idea because seting up the pointee is a separate step. We allocated the pointer y, bu we never set it to point to a pointee. Can you fix it so that y points the same pointee as x ?This doesnt touch the pointees,it just changes one pointer to point to the same thing as another. Now y points to the same place as x. Now y is fixed.It has a pointee, so you can try the wand of dereferencing again to send the 13 over.Now dereferencing works on y. Because the pointers are sharing that one pointee, they both see the 13.