Essential C and C++
Preprocessor directives
Directives direct the compiler to do something. Preprocessor directives specifically direct preprocessor actions before compilation to object code.
// insert the input-output stream header (definitions)
#include <iostream>
Within brackets, the preprocessor searches for a header file (with the name given) along a predefined path (a system file defined as an environment variable). Within double quotes, the preprocessor looks for the header file by filename first (in the same folder of the file that the directive is called), before trying the predefined path. The latter therefore tends to be used for non-standard libraries.
// insert the input-output stream header (definitions)
#include <iostream>
#include "myLibraryHeader.h"
Fundamental types
| Type | Size (byte) | Range of values | Types with literals |
|---|---|---|---|
| bool (C++; C99 or above, use _Bool) | 1 | true or false | bool isRed = true; |
| char | 1 | Equivalent to signed char or unsigned char (compiler dependent) | char letA = 'A'; or char letA = 65; |
| signed char | 1 | -128 to 127 | signed char letA = 'A'; |
| unsigned char | 1 | 0 to 255 | unsigned char letA = 'A'; |
| wchar_t (C++) | 2 | Wide char, 0 to 65,535 | wchar_t wideChar = L'Z'; see note below |
| short | 2 | -32,768 to 32,767 | short someNo = 5; |
| unsigned short | 2 | 0 to 65,535 | unsigned short nothing = 0u; |
| int | 4 | -2,147,483,648 to 2,147,483,647 | int bigValue = 456354; |
| unsigned int | 4 | 0 to 4,294,967,295 | unsigned int evenBigger = 12546865U; |
| long | 4 | same as int, above | long longer = 24l; |
| unsigned long | 4 | same as unsigned int, above | unsigned long twoFour = 24lu; or unsigned long twoFour = 24ul; |
| float | 4 | ±3.4x10±38 precision of 6-7 d.p. | float someFloat = 110.0f; or float someExp = 1.88E10f; |
| double | 8 | ±1.7x10±308 precision of 15 d.p. | double someDouble = 3.14; or float someDouble = 3.14; see below |
| long double | 8 | ±1.7x10±308 precision of 19 d.p. | long double someLongDouble = 3.13E-24l; |
The comma operator
It is possible to assignment multiple variables with the comma operator:
long alpha = 0, beta = 1, gamma = 2;
When a variable is assigned a value with a comma operator, then it takes the rightmost value:
// unknown = 2; all other variables as stated
long unknown = (alpha = 0, beta = 1, gamma = 2);
Characters
Take note of integer assignments to characters.
// ASCII decimal 65, therefore 'A'
char letA = 65;
// ASCII octal prefixed with 0. 065 is decimal 53, therefore '5'
char letB = 065;
// ASCII hexadecimal prefixed with 0x (or 0X). 41 is decimal 65, therefore 'A'
char letC = 0x41;
char letD = 0X41;
Wide characters: this can be confusing if applied to different environments. On Windows, wide characters are typically UTF16 (2 bytes) while on other machines are UTF32 (4 bytes).
Below are example escape sequences:
| Escape sequence | Description |
|---|---|
\a | beeping sound |
\n | newline |
\' | single quote |
\\ | backslash |
\b | backspace |
\t | tab |
\" | double quote |
\? | question mark |
Unsigned and float postfixes
Unsigned variables must be appended with U or u. Long variables must be appended with l or L.
Floating-point number must contain at least a decimal point or an exponent E. When appended with the letter f, floating-points are treated as type float, otherwise they are treated as type double.
Custom (derived) data types
Typedefs
Typedefs are synonyms of existing types, including pointers and classes, and mainly used to make code easier to read.
typedef long int BigInt;
BigInt someNumber = 10L;
// equivalent to
// long int someNumber = 10L;
Enumerations and static casting
Enumerations are collections of related constants. They indexed by integers by default but can be indexed with characters instead.
int main(){
// of type int by default, MondayN = 0 etc.
enum WeekDayNum {
MondayN,
TuesdayN,
WednesdayN,
ThursdayN,
FridayN
};
// of type char, Monday = M
enum WeekDay : char {
Monday = 'M',
Tuesday = 'T',
Wednesday = 'W',
Thursday = 'T',
Friday = 'F'
};
WeekDay someWeekDay = Monday;
WeekDayNum someWeekDayNum = MondayN;
// << is known as a stream operator; use a more modern form of type-casting
cout << "The current weekday set is: " << static_cast<char>(someWeekDay) << endl;
cout << "The current weekday (num) set is: " << someWeekDayNum << endl;
cout << "Pick a number between 0 and 4: " << endl;
int customDay;
cin >> customDay;
while (customDay < 0 || customDay > 4){
cout << "Please enter a value between 0 and 4, inclusive: " << endl;
cin >> customDay;
}
if (someWeekDayNum == customDay){
cout << "You picked the same day" << endl;
} else
cout << "You picked a different day" << endl;
return 0;
}
The above static_cast<destinationType>(originType) operator does not check the validity of the cast, so it is up to the developer to make sure that the cast is valid. If the cast is invalid at runtime then the program will terminate. To get the compiler to check the validity of the cast, and then set the output as null (as opposed to terminate the program) use dynamic_cast<destinationType> (orginType).
Structures
Structures are collections of data types under one name and remain pervasive, particularly in solutions which are/were written primarily in C. Each structure is defined by its members or fields.
struct Rectangle{
int length;
int breadth;
};
void main(){
Rectangle r; //places r in the stack
r = {10, 5}; //initialises r
r.length = 10;
r.breadth = 20;
printf('Area of rectangle is %d/n', r.length*r.breadth);
//declaring an array of structs
Rectangle rectangles[5];
rectangles[0].length = 30; //etc...
}
Structure members (or fields) can be of any type except for the structure being defined. To use the same structure definition in the structure, use a pointer to the structure instead:
struct Node {
int level;
struct* Node node;
struct AnotherStruct another;
// would not compile
// struct Node node;
};
This idea is useful for linked lists.
C structures are generally superseded by C++ classes.
Unions
Unions are similar to C/C++ structures in that they collect fields but differ in that only one field can be assigned at a given time. These tend to be useful when data can take on different types but logically represent the same construct. For example, IDs can be of type int in some cases but then be of type char in others.
The memory reserved for unions is the same as that required for the largest fundamental data type involved.
union shreadID
{
int intId;
char charId;
}
// define an instance of a union
sharedID unionInstance;
// set intId; override (clear) charId
unionInstance.intId = 12;
// change of circumstance, set charId; override (clear) intId
unionInstance.charId = 'D';
Namespaces (C++)
Variables and functions with the same name (e.g. value below) in one library can often be used and therefore conflict with other variables and functions in another. To get around this, C++ introduces the idea of namespaces which act as a container, setting a prefix to each variable or function listed within the namespace.
namespace someNamespace
{
int value = 0;
}
When referring to said variables and functions outside of the library, developers can use a fully qualified identifier with the scope resolution operator (::) as follows:
#include <iostream>
namespace someNamespace
{
int value = 0;
int value2 = 2;
}
// perfectly legal; value here is a different variable (with value 1) compared to value in the namespace
int value = 1;
int main()
{
std::cout << "Coming at you from the standard library namespace, std\n";
std::cout << "someNamespace: " << someNamespace::value << '\n'; // prints 0
std::cout << "Global namespace scope: " << value << '\n'; // prints 1
return 0;
}
Using fully qualified names can get verbose, so instead one can apply the using directive which indicate all variables and functions of a given namespace that can be referred to more concisely:
#include <iostream>
namespace someNamespace
{
int value = 0;
int value2 = 2;
}
// using directives
using namespace std;
using namespace someNamespace;
// won't build anymore, as reference to value is ambiguous with using directive
int value = 1;
int main()
{
cout << "Coming at you from the standard library namespace, std\n";
cout << "someNamespace: " << value << '\n'; // prints 0
cout << "Global namespace scope: " << value << '\n'; // prints 1
return 0;
}
Note however that using directives (to other libraries) allows developers to use all variables and functions in the namespace, and thus limits what can be declared in the current project, thus defeating the purpose of namespaces.
Instead, a more selective and mostly preferred approach is to use using declarations instead of using directives.
#include <iostream>
namespace someNamespace
{
int value = 0;
int value2 = 2;
}
// using directives
using namespace std;
using someNamespace::value;
// perfectly legal, since we can still refer to value2 from someNamespace if we fully qualify
int value2 = 1;
int main()
{
cout << "Coming at you from the standard library namespace, std\n";
cout << "someNamespace value: " << value << '\n'; // prints 0
cout << "Global namespace scope: " << value2 << '\n'; // prints 1
cout << "someNamespace value2: " << someNamespace::value2 << '\n'; // prints 2
return 0;
}
It is possible to define multiple namespaces in a given file.
Arrays and strings
Arrays in C and C++ are zero-based.
// array declaration
long numbers[7];
// array initialisation; the final and third element would be initialised with zero
long moreNumber[3] = {34, 12};
std::cout << "moreNumber second element: " << moreNumber[1]; // prints 12
Arrays are always blocked in sequence in memory from the first to the last element.
The literal moreNumber[1] is actually a pointer to the beginning of the array and then 1 block after. Since the compiler knows the data type of the array and therefore the size of the type, it can automatically navigate to the location of the second element. Furthermore, the array itself is stored dynamically, and is located on the heap (or free store) in memory. Pointers and the heap are outlined later.
Strings, via raw arrays, are arrays of characters. A list of strings can be handled with a multidimensional (two-dimensional) array of characters.
Examples of string features are given here for completeness, though most developers will find the standard library String class (#include<string>) much easier to use.
#include <iostream>
using std::cin;
using std::cout;
using std::endl;
using std::fill;
using std::copy;
int main()
{
const int MAX = 80;
char buffer[MAX];
const int MAX_STRINGS = 2;
char stringList[MAX_STRINGS][MAX];
int count = 0;
for (int z = 1; z < MAX_STRINGS + 1; z++)
{
cout << "Enter string " << z << " with a max of "
<< MAX << " characters:" << endl;
// read a string until a new line
cin.getline(buffer, MAX, '\n');
copy(buffer, buffer + MAX, stringList[z-1]);
cout << "Filled array with string " << z << endl;
// no direct way to clear an array, so we use std::fill
fill(buffer, buffer + MAX, 0);
}
cout << endl;
for (int j = 0; j < MAX_STRINGS; j++)
{
cout << "String " << j + 1 << ": " << stringList[j];
cout << endl;
}
return 0;
}
Arrays are discussed in more detail later, in regard to general features and operations.
Bitwise operations
Bitwise operations work on integers and chars only (up to 4 bytes or 32 bits in size), and tend to useful when setting the state of devices (usually hardware) as a collection of bits.
In terminology, bitwise operators operate on bits, whereas logical operators (e.g. && || !) operate on high-level scalar types. Unary operators require one operand, binary operators require two operands.
| C and C++ operator | Operation | Examples |
|---|---|---|
& | binary, AND | 1 & 1 = 1, 0 + 0 = 1, 0 + 1 = 0 |
\| | binary, inclusive OR | 1 | 1 = 1, 0 | 0 = 0, 0 | 1 = 1 |
\| | binary, exclusive OR | 1 ^ 1 = 0, 0 ^ 0 = 0, 0 ^ 1 = 1 |
~ | unary, NOT | inverts bits, so ~1 becomes 0, or ~0 becomes 1 |
<< | binary, left-shift | shifts all bits to the left by n-bits |
>> | binary, right-shift | shifts all bits to the right by n-bits |
Developers can use more concise representation of the bitwise operators:
letter = letter & 0x0F; // equivalent to letter &= 0xOF;
number = number | otherNumber; // equivalent to number |= otherNumber;
figure = figure ^ otherFigure; // equivalent to figure ^= otherFigure;
The next subsections explore more examples.
Bitwise AND
This generates a new value following evaluation of all bits e.g. int 5 & 3 = 1:
0000 0101 (decimal 5)
AND 0000 0011 (decimal 3)
= 0000 0001 (decimal 1)
The bitwise AND operation can be useful in masking a specific group of bits, to yield a bit sequence which ignores the masked bits and preserves the other (unmasked) bits.
The bits to be masked are ADDed to zero, which always produces 0. The unmasked bits are ADDed to one, which preserves their original state.
0110 0101
AND 0000 1111
= 0000 0101
In the above example, the last five bits are masked with zeros (are eliminated), and the first four bits are preserved.
Bitwise inclusive OR
This generates a new value following evaluation of all bits e.g. int 5 | 3 = 7:
0000 0101 (decimal 5)
OR 0000 0011 (decimal 3)
= 0000 0111 (decimal 7)
This operation is useful when switching individual bits “on” (e.g. bit 6 below), ORing with ones, leaving all others in their present state.
0000 0101
OR 0010 0000
= 0010 0101
Bitwise exclusive OR: XOR
This generates a new value following evaluation of all bits e.g. int 5 ^ 3 = 6:
0000 0101 (decimal 5)
XOR 0000 0011 (decimal 3)
= 0000 0110 (decimal 6)
The XOR operation can be used to swap bit sequences between two variables e.g. lvalue is 0100 0001 and rvalue is 0101 1010, if the following is carried out:
lvalue = lvalue ^ rvalue; // or lvalue ^= rvalue; lvalue becomes 0001 1011
rvalue = rvalue ^ lvalue; // or rvalue ^= lvalue; rvalue becomes 0100 0001
lvalue = lvalue ^ rvalue // lvalue becomes 0101 1010
The final result (following all three operations) is lvalue is 0101 1010 and rvalue is 0001 1011.
Bitwise left and right shift
These operations shift each bit n-places to the left (or right). Bits that are ejected at the terminus are lost and new bits introduced are padded with zero.
For example, bitwise shift left by two bits e.g.
// decimal 23
unsigned int someNumber = 23u;
// decimal 92
someNumber <<= 2;
0001 0111 (decimal +23) left-shift by two bits
= 0101 1100 (decimal +92)
In some contexts, it may be necessary to clarify when a stream operator is required versus a bitwise shift operator by including parentheses:
std::cout (number << 2);