Lesson 2 recap
Data types
- Union
- enum
- Structures
- C String (Null terminated string)
Statements
- Switch case statement
Theroy
- Stack & heap
Unions in C are variables that can be associated with several datatypes for the exact same memory location. The size of a union is the size of it's largest datatype
#include <iostream>
using namespace std;
union Pack {
char c;
long l;
};
int main()
{
cout << sizeof(Pack) << endl; // Size is 8 bytes which is the size of long
}
Unions are usually used with used with the company of a discriminator: a variable indicating which of the fields of the union is valid.
Usage
- Optimising memory consumption
- Accessing the same memory in multiple formats
#include <iostream>
using namespace std;
struct MyVariant_t {
int type; // This is a descriminator whose value can be 1 for char or 2 for long
union Pack {
char c;
long l;
} data;
};
int main()
{
MyVariant_t v;
/** do some code */
if (v.type == 1) { /* use v.data.c */ }
else if (v.type == 2) { /* use v.data.l */ }
}
Provide a logical name to an ordinal value.
enum EyeColor (Black, Brown, Blue };
int main() {
EyeColor myEyeColor = Brown; // Easy to read
}
The alternative would be to choose arbitrary values and use them
to identify the eye colour, but that is much less clear
int main() {
int myEyeColor = 1; // We decided that 1 is brown, not immediately obvious.
}
Structs in other languages are also known as records, the ability to aggregate data together and provide it with a meaningful name.
Example:
// Provide a data type for person containing the attributes we care about
class Person {
int age;
enum EyeColor {Blue, Brown, Black} eyeColor;
std::string name;
};
- Structs declaration must terminate with a semi-colon
- The size of a struct is at least the sum of the sizes of all it's constituents.
The different fields in the struct can be access via the dot operator.
Person p;
p.age = 56
p.name = "My Name"
p.eyeColor = Person::Brown;
Also known as NULL terminated string.
String is not a primitive type in C, but rather an array of characters that gets a bit of a differential treatment in some C functions, than other types of arrays.
Consider the following code
char str[10];
strcpy(str, "Hello");
Hereunder is the representation of the memory pointed by variable str.
The size of the array is 10 bytes, the first 5 contain the value we set it the 6th byte is NULL (0x00) and the rest of the bytes are unused (We don't care about them)
Given that C String is not a primitive type, there are no primitive operands that can manipulate Strings. We have to employ library functions for that
Here are a few most commonly used functions
Prior to the use of any of the functions, string.h header must be included.
#include <string.h>
For better readability and to avoid long condition chains, such as this
enum EyeColor (Blue, Brown, Black, Green);
EyeColor colour = /* some value */
if (colour == Blue) {
/* do something blue */
} else if (colour == Brown || colour == Green) {
/* Green & Brown have the same handling */
} else if (colour == Black) {
/* do something black */
} else {
/* Handle unexpected eye colour */
}
A switch case can make the code more readable and less verbose
enum EyeColor (Blue, Brown, Black, Green);
EyeColor colour = /* get some value */
switch (colour) {
case Blue: /* Handle Blue colour */
break;
case Brown:
case Green: /* Handle Blue & Green */
break;
case Blakc: /* Handle Black Color */
default:
/* Handle error, unexpected Eye colour */
};
without the break keyword processing will continue to the next case.
In C/C++ we have 2 distinct memory locations that we should be aware of
The stack whose memory management is entirely managed by the compiler, and whose workings we are supposed to understand only to only write more efficient code.
And the Heap which is synonymous to dynamic allocated memory. Memory that the developer is managing(Allocating and freeing on demand).
Also know as LIFO (Last In First Out), a stack has only 2 primitives push and pop, that grow and shrink the stack respectively.
In the diagram below, we see that first main's local variables are pushed into the stack, in step (b) main is preparing to call func1() prior to that it pushes the parameters required for func1() into the stack and than it calls func1() whose local variable are now pushed into the stack
In step (c) func1 finished processing and the stack is unwinding, meaning func1's locals and parameters are pop-ed.
Unlike the stack, the heap is not a sequential growing and shrinking according to usage, but rather it's being allocated and freed for us by services that are provided by the operating system.
namely. malloc(), calloc() and free()
However, in C++ we use safer primitives to request memory and free it.
# Allocating memory
int* pInt = new int(7); // Allocates one integer size memory and assigns 7 to it.
int* pIntArray = new int[10]; // Allocates 10 sequential integers.
# Freeing the memory
delete pInt; // Free the memory allocation to the 1 integer
delete [] pIntArray // Free the memory allocation to the 10 integer array
The new/delete constructs are type safe and are object oriented aware unlike their C malloc/calloc/free counterpart.