
Flow ControlAnother major benifit of C, is the ability to have flow control, that is close to what the machine is actually doing. These are pretty straightforward, so we will blaze right through them. Boolean expressions are important. You can use < > <= >= == or !=. They return 1 for true or 0 for false. (They case implicity to boolean values.) It is important to notice that the == boolean operator is NOT the = assignment operator. Once you have a boolean answer, you can combine them ising && (for and) or || (for or).If statementsIf statements allow you to decide wether or not to execute some lines of code. If the boolean returns true you execute the block or line of code, otherwise you execute the else or keep going if there is none. If there is only one line of code you do not need to surround the block with brackets.
if (a > b)
{
cout << "This worked!" << endl;
cout << "Isn't it neat?" << endl;
}
else cout << "No, it is not." << endl;
Looping constructsThere are two main looping constructs in C++, for and while. While runs the same section of code over and over, until the condition is no longer true. Example:
int x = 0
const int slope = 5;
cout << "linear series, slope of " << slope
<< ", to less than 100:" << endl;
while (x < 100)
{
cout << "x = " << x << endl;
x += slope;
}
The other looping construct is a bit more comples. The for loop has
three distinct parts where the condition should go. The first part
allows you to initalize a variable to iterate over. The second part
is the continuation condition. As long as that condition is true, the
code in the block will continue to run. The third part is the
iteration command. This is run once each time trough the loop, and
tells the for loop how to change the iteration variable. The three
parts are separated by semicolons. The following two loops do the
same thing:
int a = 10;
while (a > 0)
{
cout << "The next number is " << a;
a -= 1;
}
for (int b = 10; b > 0; b--)
{
cout << "The next number is " << a;
}
ArraysArrays are a very important part of C/C++. An array is just a chunck of memory that is large enough to hold a certain number of consecutive types. For instance, an array of 6 longs is 24 bytes. You declare the array by:int myArray[6];The int is the type of what is being stored in the array. The number tells you how many of the type are being stored in the array. This allows you to store a large number of a given type without needing to create a lot of variables. These are then stored on the stack in the current scope. Since it is being stored on the stack, the size of the array must be a constant size. (Note this is not what I said in class. I must now humbly beg for your forgiveness.) You can then access the array elements using the following syntax: myArray[3] = 5; // set the fourth element of myArray to 5. int x = myArray[3]; // set x to 5.The number in the brackets is the index into the array. The indicies start at 0 and go to one less than the size of the array. This means that to find the value of the fourth array element, you need to use the index 3. The size of the array can NOT be a run-time variable. The following example shows how to make and initialize an array of integers, whose size is a constant variable, if you want to mane an array of a size to be determined at run time, you must use memory allocation to put the array on the heap.
const int theArraySize = 10; // the size of the array must
// be a constant. (Known at compile-time.)
// One initalization method:
int theArray[theArraySize];
for (int i = 0; i < theArraySize; i++)
{ // note that i is only less than theArraySize, not <=.
theArray[i] = 3; // initalize all the array elements to 3.
}
// Another initalization method:
int theArray2[] = { 1, 4, 6, 2 };
The first initalization method is good for larger arrays. The second
method is good for small arrays with specific values. It also doesn't
need to have a size, since the compiler can figure it out. When using
the second method, it is better not to use a size since that will
assure that the correct size array is allocated. If you put the
constant in, you might put the wrong size array in.
When using arrays, you need to be very careful not to run off the end of your array. If you go past the top of the array, you will be accessing whatever is in memory right past the array. StringsStrings are just arrays of characters. They end in a null, so that you can operate on them without knowing how long the string is. This means that you can index into them like you would expect. You can declare them just like above:char myString1[] = "Hello World!"; char myString2[255] = "Hi world!";You can allocate more memory that you need (in case you want to change the string in the future) but you can not put less. Similarly with the second initalization type for the integer array above, you can allocate more space than you need:
int theArray[6] = {1, 2, 3};
But the you have non initalized data, which we have discussed before
as a bad idea. The rest of this lecture is just for kicks. If you
are interesed read about it. The bit on pointers for arrays will be
useful for the start of next times lecture when we talk about run-time
memory allocation.
Introduction to pointersLast time we talked about variables, which are just locations in memory where numbers are stored. Then you can just use the variable as if it were the value of the variable. Sometimes you may want to know the actual memory location of the value in memory. This is called finding the address of the variable. When you find the address of the variable, you store it in a special type of variable called a pointer. Since C++ is strongly typed, you need to declare what type the pointer is pointing to. Below is an example of creating a variable then creating a pointer to point to that location in memory.// Create a variable to point to. int x = 34; // Create a variable that will point to x. Note that to declare // a pointer to a given type you use the asterix after the type, // eg. [type] * [pointer variable name]; // To find the address of a variable you use the & operator. int * pointer_to_x = &x;Notice the pointer declaration (with the *) and finding the address of a variable (with the &). Now it is important to remember that the pointer is NOT the variable itself, but just the address of the variable in memory. In order to find out the value of the variable that the pointer is pointing to, you need to dereference the pointer. Dereferencing also uses the asterix operator.
// Pointer dereferencing example.
#include <iostream>
main()
{
int x = 57;
int * pointer_to_x = &x;
cout << "The address of x is: " << pointer_to_x << endl;
cout << "The value of x is: " << *pointer_to_x << endl;
}
Pointers are often thought of a difficult, but really they are quite
easy. Just think of them as a variable that contains the address of
another variable. One final note about pointers, since they are just
variables, you can take the address of them, and have a pointer to a
pointer:
// make a variable int x = 32; // point to it int * pointer_to_x = &x; // Just use another * to point to the pointer! int ** pointer_to_pointer_to_x = &pointer_to_x; // Now, x is the same as **pointer_to_pointer_to_x Breaking scope by passing pointersBy using pointers, we are able to break scope. This happens because of the way pointers work. A pointer is just a variable, so it can be passed as a parameter. When it is passed the value of the pointer is copied to the parameter. Now when you dereference the pointer, it is pointing to the actual data in the scope of the other function, not a copy. This means that if you change the value of what the pointer is pointing to, you change it in the scope of the calling function. Thus:
void caller() // The calling function
{
int x = 5;
int y = 6;
int * yPtr = &y;
callee(yPtr, x);
// y is now 7
// x is still 5
}
void callee(// The function that we call.
int * a, // the variable we can change
int b) // The variable we can't
{
b = 8; // Changes it locally but not in caller. Just like scheme
(*a) = 7;// changes the value that a is pointing to. y in caller.
}
You have to be very careful when you are doing this because it can be
confusing, but it can also be very useful. Especially when we get to
classes later.
ConstantsYou can declare any variable to be a constant by putting the keyword const in front of the declaration. This is a very useful technique and should be used whenever you make a variable that is not going to change. Const allows the compiler to make sure that you are not changing a variable that you didn't mean to. In general you should use const variables rather than numerical constants to enhance clarity and make your code easier to read. It also allows you to easily make global changes to your program. For example suppose you were making an array, you would want to declare the size as a constant rather than typing the actual size in a number of times:
const int theArraySize = 17;
char myArray[theArraySize];
// Initalize the array:
for (int i = 0; i < theArraySize)
myArray[i] = 0;
Now, if you want to change the size of the array, you only need to
change the 17 in one place rather than two. This can be vary useful
if your programs get large or you use the same number over and over
again.
Arrays AgainLast time we talked about how to statically allocate arrays. This is very useful, but sometimes you want the ability to dynamically allocate arrays. How is this possible? We will start with a quick recap of statically declared arrays, and then talk about how to dynamically allocate arrays.An array is a block of memory that is as large as the size of the type in the array times the number of items in the array. The following are all correct methods of declaring arrays (they all allocate the necessary memory):
int theIntArray[6]; // Allocates space for 6 ints.
long theLongArray[6] = // Allocates space for 6 longs ...
{ 2, 3, 6, 7, 2, 0 }; // ... and initalizes them.
long theLongArray2[] = // Allocates space for 6 longs ...
{ 2, 3, 6, 7, 2, 0 }; // ... and initalizes them.
char theString[] = // Allocates space for 13 chars ...
"Hello world!"; // ... the extra spot is for the teminal null.
By using these allocation methods you can only define arrays of a
static size. This is because when a function comes into scope, it has
to know how large the stack will grow by. We will discuss dynamic
allocation in a moment.
Then using the arrays is very simple, as long as you remember to index from zero to one less than the size of the array. Remember there is no array bounds checking, you need to watch out that you do not go outside of your array. Also remember that once an array is initialized, it can never point to a different block of memory. This is true for strings as well! Thus: long x = theLongArray[2]; // Set x to 6. theIntArray[0] = 4; // Set the first element to 4. theLongArray2[6] = 6; // Doing something very bad! theString = "Goodbye world!"; // Compile error.Remember what I said about arrays just being a pointer to the beginning of the block of memory. That is not quite true, an array is just a CONSTANT pointer to the beginning of the chunk of memory. This is why you get the last compile error from above. By using non-constant pointers, you can have your variable point to different chunks of memory. For instance:
long * theLongAray3 = theLongArray; // Now we have the long array
// referred to twice!
theLongArray3[4] = 4; // This works!
char * theString2 = theString; // This is ok.
char * theString2 = "Goodbye World."; // This works too.
Dynamic Memory AllocationSo now we know how to do static memory allocation, how do we do dynamic memory allocation? Since the stack has to behave in a manner that is well defined at compile time, we can not just put a runtime defined amount of memory on the stack. Fortunately C++ defines a free store of memory (the heap) which can be allocated at runtime. The way we allocate that memory in C++ is by using the new operator. The new operator returns a pointer, thus:int * myIntOnTheHeapPtr = new int;However when the function that made this call returns, the pointer is reclaimed, but the memory on the heap is not. Thus, before the pointer is reclaimed, we need to reclaim the memory on the heap. We do this with the delete operator: delete myIntOnTheHeapPtr;This reclaims the memory that myIntOnTheHeapPtr points to, not the pointer itself. That is reclaimed when the function returns. There that was easy, now for the fun stuff... Dynamic array allocationSo how do we declare an array dynamically (on the heap)? If you remember an array is just a pointer to the beginning of a chunk of memory. So the first thing we need is a local pointer. Then we use new to make the array, which returns a pointer to the beginning of the chunk of memory on the heap. The syntax looks like this:
int theArraySize = 0; // The size of the array is not constant!
int * myVariableSizeArray = 0; // A pointer to the array...
cout << "Input the array size: "; // ... points to nothing.
cin >> theArraySize; // Get the size of the array.
myVariableSizeArray = new int[theArraySize]; // Make the array
for (int i = 0; i < theArraySize; i++)
myVariableSizeArray[i] = 0; //initialize the array.
There, that wasn't so bad, was it? Now you just need to make sure
that when you index into the array, you don't go off the end. Also
when you are done with it, you need to delete it since it is on the
heap. (Don't forget to do this before the pointer goes out of scope.)
You should note that the pointer for an array of ints is the same as
the pointer for a single int. This means that when you are deleting
the chunk of memory, you need to let the compiler know that you are
deleting an array of ints rather than just a single int. You do this
as:
delete[] myVariableSizeArray;rather than just the normal delete. In general, for every new, there must be a corresponding delete, and every array that you allocate there must be a corresponding delete array. If you follow that rule of thumb you will not have any memory leaks. The opposite of memory leaks can also pose a problem, that is, you need to be sure to not delete something twice. |