☰
CS50 x 2024 Notes Memory - 08
2026/10/18 8:17:22 网站建设 项目流程

⑴

So what about these other locations in memory ?

Well, it turns out that, indeed, the stack, as we've 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. There's one tray, another tray, another tray, another tray. But then you start removing them from top down. So there's an ordering to them that we'll actually revisit next week.

But this is not a good design, in general, you shouldn't be doing things like two trains on the tracks barreling together toward each other in this way. But honestly, it's kind of the only way, because if you've only got a finite amount of memory, ok, sure, you can have them both grow in the same direction. But they're still going to hit some impass eventually. You're 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 it's 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, you're 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 it's memory down here, or even worse, memory over here as by something called heap overflow.

And these are specific examples of what we'll 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 you've used YouTube recently, and maybe it's just kind of paused and spinning, and spinning, and spinning, maybe you're on a really bad connection. There's no more bytes in your buffer. There's 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 they're 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. We've 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, you're 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 let's introduce you to what the alternative would be and given an appreciation for what libraries like cs50's and other's 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, let's 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 we're 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, I'm not using get_int, so I can't just call it to present the user with a prompt. So I'm going to use printf to create my own prompt. And now, let me use this function scanf as follows. I'm going to call scanf, and then I'm 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 user's keyboard is in fact a single integer. Now I'm going to close quotes, and I don't need a new line because I'm not trying to print anything. I'm 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, it's 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, I'm 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 user's value. After that I'm just going to go ahead and print out what's happened. So I'm going to go ahead and print out the value of n followed by a colon, followed by an actual placeholder, %i backslash n. And I'm going to pass in now to printf the value n. So to be clear, I'm still passing n into printf, just like we've been doing since week one, but I'm 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 I've 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 doesn't even type in an integer, scanf isn't 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 let's 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 can't use string anymore, at least if we're 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, that's 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 user's keyboard. I'm going to type scanf. I'm going to do %s instead of %i because I, indeed, want to scan a string in this case. And then I'm 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 that's 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, let's go ahead and print out the result. So let's just use printf. Let's print out a prefix like s colon again, %s as my placeholder and now backslash n because I'm formatting it on the screen. And then let's 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 I'm going to go ahead and compile this an old fashioned way because we actually protect you from doing something like this. But I'm going to go ahead and ignore the warnings you would otherwise see us make. And I'm going to go ahead and compile this with Clang directly. So clang -o get, because that's the name of the program I want to output. But I'm also going to specify clash capital W, no, uninitialized, which is simply another command line argument that's 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 didn't actually initialize s to anything, even null. But that's ok because I want to forge ahead blindly just to make a point as to what's going on here.

And in fact, let's go ahead and compile this code as follows. It does seem to compile, even though make would have warned us that something's 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 shouldn't have. Why, in fact, is this ? Well, let's consider what it is we've been doing.

If this here is my computer's 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 doesn't 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 it's like taking eight bytes out of memory and calling them s. But the key point is, if I don't 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 it's 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 ?

It's literally a garbage value. It's the equivalent of a foam finger pointing there, there, there. We just don't know because it's not a valid address.

And so I get that segmentation fault here in my terminal window, because I've not initialized s to be some known value, I get a segmentation fault because, effectively, I'v 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 let's simply change what's 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. That's 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 I'm not, not initializing something this time, I can use make as usual without getting yelled at because I'm not yet doing anything wrong. Now let me go ahead and do ./get, enter. And in this case, it's 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, I've now redeclared s as an actual array of four characters, that's 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 there's 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, I've only asked the operating system for an array of four bytes. So what's going to happen with all of those extra hi's, hi's ? They're just going to, by default remain contiguous from left to right, top to bottom in the computer's memory. But they're going to end up some of those characters at location, I didn't ask the operating system for in this array.

So if I go back to VS Code here, I've 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 didn't 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 don't know in advance how long the string is going to be that the human's going to type in. Maybe it's four. Maybe it's fewer characters. Maybe it's even more. So what's the alternative ?

Well, I could go in here maybe and allocate. I don't know, like 4000 characters for s. But what if you type in an even longer string that's 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 user's 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, it's 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 there's another one coming. Then it allocates more space for a second. If there's 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 you're typing in the keystrokes and hitting enter, so that we never assume how many characters you're 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 that's 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. It's 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, we'll soon, in two weeks, pivot to Python, which takes care of all of these problems for us and manages our memory.

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询