ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

CS50 x 2024 Notes Memory - 08

2026/9/1 6:57:24 拓冰建站 浏览量
CS50 x 2024 Notes Memory - 08 ⑴So what about these other locations in memory ?Well, it turns out that, indeed, the stack, as weve described it, grows up, and up, and up. And recall that stack here in this sense is kind of like the stack of trays and the cafeteria or any of the dinning halls. Theres one tray, another tray, another tray, another tray. But then you start removing them from top down. So theres an ordering to them that well actually revisit next week.But this is not a good design, in general, you shouldnt be doing things like two trains on the tracks barreling together toward each other in this way. But honestly, its kind of the only way, because if youve only got a finite amount of memory, ok, sure, you can have them both grow in the same direction. But theyre still going to hit some impass eventually. Youre still going to run out of space. So the way computers were designed years ago is they use memory in this way, even though bad things can happen, if you use too much stack space or too much heap space. So what do I mean by that ? Our example a moment ago just had us call main and then swap and that was it. So its like two frames no big deal.But if you call many functions again, and again, and again, if you do something recursively, where you call yourself, youre going to pile, pile, pile stack frames potentially. So you could start to hit the, so called heap area.Meanwhile, if you call malloc too many times, you might be growing down, down, down, down and then overrun some of the stack memory, as well.So bad thing can happen when you overrun either of these. And those of you maybe with prior programming experience might have heard at least one of the these terms, heap overflow, or more popularly, stack overflow. Super popular website for questions and answers about programming. The etymology thereof is exactly this idea of overflowing the stack and touching memory that you should not,whether its memory down here, or even worse, memory over here as by something called heap overflow.And these are specific examples of what well start calling buffer overflows. Buffer is just a chunk of memory. And buffer overflows means overflowing, using too much of that memory. And buffers are everywhere. In fact, if youve used YouTube recently, and maybe its just kind of paused and spinning, and spinning, and spinning, maybe youre on a really bad connection. Theres no more bytes in your buffer. Theres no more video footage in the buffer because maybe you have such a bad connection. But if Google were to make mistakes and try to download too much bytes at a time, they too could overflow a buffer. And if YouTube or similar apps have ever crashed, it could be because theyre trying to use more memory than they actually should be.So these things are sort of everywhere. Now, as for these training wheels, we sort of took away the mystery of what a string is. But what about all of these other functions. Weve been taking for granted now for a few weeks ? You can and should still use them to solve some problems because, frankly, C does not make it easy to get user input safely, like, period, full stop. It is very non-trivial to get user input without running like risk of overflowing a buffer. Why ? Well, youre the programmer. How do you possible know in advance how big of a string a human might type in tomorrow or the next week, or the next day ? You could try to be safe and allocate a million bytes all at once. But what if they type in 1000001 characters, or use copy paste so much that they similarly overflow ? So getting user input is a hard problem. So lets introduce you to what the alternative would be and given an appreciation for what libraries like cs50s and others like it are actually doing for you. Let me go ahead and create our own version of get_int and get_string without using the cs50 library but using a standard C function called scanf.And to do that, let me go over to VS Code, let me create a new file called, for instance, get.c. And then in get.c, lets make a very simple program first that just gets an integer, but again, without using the cs50 library. So let me go ahean and include the stdio.h. Let me go ahead and declare main as int main void. And then inside of main, let me go ahead and declare an integer n, so that we have some place to put the integer that were getting from the user. Then let me go ahead and just prompt the user for a value for n, so n colon space, for instance. Because again, Im not using get_int, so I cant just call it to present the user with a prompt. So Im going to use printf to create my own prompt. And now, let me use this function scanf as follows. Im going to call scanf, and then Im going to pass to scanf, similar in spirit to printf, a format code, like %i, effectively telling scanf that what I want it to scan, so to speak, from the users keyboard is in fact a single integer. Now Im going to close quotes, and I dont need a new line because Im not trying to print anything. Im trying to get something from the user using scanf. But I do need to tell scanf where to put this integer. Now, if I want to put this integer in the variable n, its not quite as simple as just passing n in, because recall how variables are passed. This variable n is going to be passed by value. Effectively, a copy is going to go into scanf, and so scanf is not going to have the ability to change that value. But if you think back to how we swapped two values and passed two values into that swap function in C, well, if we pass those two values in by their addresses, so passing by reference, so to speak, then the function swap in that case, scanf in this case, can actually go to that address and change the value. So to summarize, Im going to pass the scanf one argument, which is a format code, and a second argument, which is the address of an integer into which to put the users value. After that Im just going to go ahead and print out whats happened. So Im going to go ahead and print out the value of n followed by a colon, followed by an actual placeholder, %i backslash n. And Im going to pass in now to printf the value n. So to be clear, Im still passing n into printf, just like weve been doing since week one, but Im passing to scanf the address of n, so that scanf can actually go to that address and change the value of n. So I think, this is actually going to work, even though Ive not used get_int or any of the cs50 library.Let me go into my terminal, run make get, seems to compile ok. Let me do ./get, and let me type in a value like 50 for n. And indeed, I should see spit back at me that the value I got was 50. So it turns out that getting an integer from users is relatively straightforward just using scanf. But, of course, to use scanf, you need to know a little something about pointers, or addresses, more generally. That was not knowledge we had in week one. And so we do, indeed, use those training wheels of the cs50 library for the past few weeks. So that we can get integers more easily. And it turns out, if the user types more than a simple integer, or doesnt even type in an integer, scanf isnt necessarily going to behave as user friendly as get_int might. So in the cs50 library, we do a bit more error handing for you, as well. But lets consider now an implementation, not of getting an integer, but getting a string instead.Let me clear my terminal window, and let me go ahead and erase all of this code and instead focus this time on getting a string.Well, we know we cant use string anymore, at least if were not using the cs50 library. But not a problem, because we know that strings are now char stars. So if I want to get a string from the user, thats like getting, I think, a char star. So let me just call this string s by default. Let me go ahead therefore and declare a variable s thats going to store my string. Let me go ahead next, as before, and prompt the user for the value of that variable, just by prompting them with printf. So nothing fancy there. And let me try again using scanf to scan this time, a string from the users keyboard. Im going to type scanf. Im going to do %s instead of %i because I, indeed, want to scan a string in this case. And then Im going to go ahead and pass in just s. And here, at first glance, seems to be an inconsistency because, previously, I did ampersand n. But thats because n was an integer, not the address thereof. But in the word of strings as we now know, a string is just the address of its first byte. And so if we declare s to be a char star, a.k.a, string, well, s is already in address. So I can just pass in s in this case to scanf without actually using an ampersand. After that, lets go ahead and print out the result. So lets just use printf. Lets print out a prefix like s colon again, %s as my placeholder and now backslash n because Im formatting it on the screen. And then lets go ahead and pass in s as always to printf. So s is just a string, so I just pass it into printf like that.Well, let me go ahead now, and Im going to go ahead and compile this an old fashioned way because we actually protect you from doing something like this. But Im going to go ahead and ignore the warnings you would otherwise see us make. And Im going to go ahead and compile this with Clang directly. So clang -o get, because thats the name of the program I want to output. But Im also going to specify clash capital W, no, uninitialized, which is simply another command line argument thats going to tell clang not to warn us about variables that are not initialized.Because case in point on line five, as some of you might have noticed. I didnt actually initialize s to anything, even null. But thats ok because I want to forge ahead blindly just to make a point as to whats going on here.And in fact, lets go ahead and compile this code as follows. It does seem to compile, even though make would have warned us that somethings awry. Let me go ahead now and run ./get, and this time, not type in 50. But let me type in something like our familiar HI! , and hit enter. I immediately get a segmentation fault, which means something has gone wrong related to memory. A segment of memory has been touched that I shouldnt have. Why, in fact, is this ? Well, lets consider what it is weve been doing.If this here is my computers memory, and in the first case. I was just trying to get an integer, that was actually pretty straightforward, because even if this memory is filled with a whole bunch of garbage values, as personified here by Oscar the Grouch, when I declared n to be an integer before, I just needed, on this machine, four bytes, which is the typical size for an int.And I put the number 50 there. So it doesnt matter that there were these garbage values. I just went to those four bytes after declaring a variable called n and overwrote those bits, with some pattern of bits representing the number 50. But strings we now know are sort of fundamentally different.If I go back to the same chunk of memory and declare s as a pointer, that is, a char star, then recall that on modern systems pointers are typically eight bytes. So its like taking eight bytes out of memory and calling them s. But the key point is, if I dont initialize s to a valid location by calling malloc, then there will still be garbage values there. That is, those bit patterns that may have been there all along, from some previous function that got called, or some other lines of code if the program were actually bigger. So its just some garbage value is filling that variable s.The problem, though, is that in my code now, when I call scanf to scan a string from the user and to put it at that location s, well, what is that location s ?Its literally a garbage value. Its the equivalent of a foam finger pointing there, there, there. We just dont know because its not a valid address.And so I get that segmentation fault here in my terminal window, because Ive not initialized s to be some known value, I get a segmentation fault because, effectively, Iv accidentally touched memory that I should not, in fact, have done so. So how do we fix this ? Well, clearly, I need s to point at some valid chunk of memory and I could do that using malloc. But frankly, in this case, I could do it even more simply by just declaring s to be an array of characters, as we might have in week two.So let me go ahead and clear my terminal window here. Let me go into get.c, and lets simply change whats s is. Instead of a char star, which we know is what a string technically is, we can still implement strings as arrays of characters. Thats certainly still true. So let me go ahead and do that, declare s to be an array of, say, four characters. And in this case, I should have enough room for the H, the I, the exclamation point, and even that null character, the trailing backslash zero.So now, let me go ahead and build this. Make get. And because Im not, not initializing something this time, I can use make as usual without getting yelled at because Im not yet doing anything wrong. Now let me go ahead and do ./get, enter. And in this case, its ready to receive my HI! , and all actually seems well. Why ?Because in this case, I actually had enough space for s, because if I go back to my memory here, Ive now redeclared s as an actual array of four characters, thats like asking the operating system, for instance, for these four chars here. And certainly, I can fit HI! and the null character into those four bytes. So theres not a problem.But there might be a problem if the I, or the user, more generally, types in too many characters. So let me go ahead and run ./get again. Let me type HI! . But just to get a little aggressive, let me highlight that and paste it again, again, agian, and again and really type, very excitedly, a pretty long string that is surely longer than four bytes.Well, unfortunately, Ive only asked the operating system for an array of four bytes. So whats going to happen with all of those extra his, his ? Theyre just going to, by default remain contiguous from left to right, top to bottom in the computers memory. But theyre going to end up some of those characters at location, I didnt ask the operating system for in this array.So if I go back to VS Code here, Ive typed in a very long string, certainly longer than four bytes in total. Let me hit enter. And darn it. There is another segmentation fault. So in short, you are going to see these segmentation faults any time you touch segments of memory, so to speak, that do not belong to you, that you didnt allocate space for, as via an array, or even via malloc. And this is going to be a fundamental problem with getting strings because I dont know in advance how long the string is going to be that the humans going to type in. Maybe its four. Maybe its fewer characters. Maybe its even more. So whats the alternative ?Well, I could go in here maybe and allocate. I dont know, like 4000 characters for s. But what if you type in an even longer string thats 4001 characters or more ? I might still have these memory related errors, these segmentation faults. So one of the reason then, too, that we provide you with the cs50 library and in turn, functions conservatively walks through these users input, byte, by byte, by byte, one character at a time. And what the cs50 library is doing underneath the hood is, as soon as it realizes, oh, the user gave us another byte, another byte, we in the cs50 library are constantly allocating and reallocating more and more memory using malloc for you, and effectively managing the memory required for that string. So even though scanf exists, its dangerous to use with strings. And even with integers, it turns out it lacks some of the error handing that the cs50 library has thus far provided. How do we actually go about solving this ?The way get_string actually works in the cs50 library is it kind of tiptoes. It waits. It gets one character from you and then checks if theres another one coming. Then it allocates more space for a second. If theres still a third, it allocates more space, more space, more space. So essentially, what get_string does is it uses malloc again, and again, and again, and it kind of lays the tracks down as youre typing in the keystrokes and hitting enter, so that we never assume how many characters youre going to type in. We dynamically allocate just enough bytes for you, plus one extra for the null character. And this is sort of a hoop thats just not fun to jump through when, at the end of the day, all you want to do is get input from the user. So even with the training wheels officially off. Its going to be annoying to get strings from users in C. But it is easy with ints, with floats, with other data types. And frankly, well soon, in two weeks, pivot to Python, which takes care of all of these problems for us and manages our memory.