Personal C++ notes
Compiled by Jeremy Neal Kelly
www.anthemion.org
February 19, 2010
These are my personal C++ notes, covering C++98. I have published them primarily for my own convenience, but others are welcome to distribute them, in whole or in part, so long as proper citation is made.
The notes do not provide an exhaustive description of the language; concepts that seem obvious to me are often not mentioned. Nor is everything here necessarily useful. The notes are not drawn from the standard, but from various secondary sources listed at the end. I am an experienced C++ developer, and I believe the notes are accurate; if you find a mistake, however, please let me know.
Character types are generally signed by default.
char specifies an integer representing one element from a single character set, usually but not necessarily ASCII; it is one byte in size.
wchar_t specifies a type representing one element out of all character sets in all locales supported by the implementation; it is usually two bytes in size. Wide character literals are prefixed with L:
wchar_t o = L'A';
Escape sequences allow special characters to be represented within character and string literals:
| Sequence | Character |
| \a | Bell |
| \b | Backspace |
| \f | Form feed |
| \n | Newline |
| \r | Carriage return |
| \t | Tab |
| \v | Vertical tab |
| \\ | Backslash |
| \' | Single quote |
| \" | Double quote |
| \? | Question mark |
Arbitrary characters can be specified with a single backslash followed by up to three octal digits:
char o = '\101';
or a backslash and an x followed by any number of hex digits:
o = '\x0041';
Integers are signed by default. Each type is less than or equal to the following type in size:
| Type | Common size |
| short | 2 bytes |
| int | 4 bytes |
| long | 4 bytes |
| long long | 8 bytes |
Integer literal modifiers:
| Modifier | Type | Meaning |
| 0 | Prefix | Octal |
| 0x | Prefix | Hex |
| U or u | Suffix | unsigned |
| L or l | Suffix | long |
| LL or ll | Suffix | long long |
Enumeration values are called enumerators. The size of an enumerator is normally that of an int, but may be larger if its value is outside the range of an int. Visual Studio 2005 truncates explicit enumerator values outside this range.
An enumerator can be used anywhere an int is used. An int cannot replace an enumerator.
Enumerations can be defined anonymously. They can also be defined with local scope:
void gOut() {
enum {
oReady = 10,
oDone = 20
};
...
Floats are always signed. Each type is less than or equal to the following type in size:
| Type | Common size | Common precision | Suffix |
| float | 4 bytes | 6 digits | F or f |
| double | 8 bytes | 15 digits | — |
| long double | 10 bytes | 19 digits | L or l |
Visual Studio 2005 treats long double as double.
Float literals can be specified with scientific notation, the exponent being marked with E or e:
const double oTol = 1E-3;
When an integer and a floating-point value are passed to an operator, the integer is converted to floating-point before the operation proceeds.
If it returns a non-const reference, a function's return value can be used as an l-value.
The type referenced by a pointer is the base type of that pointer. A void pointer can store the address of any object. It cannot be dereferenced, however, or participate in pointer arithmetic.
A pointer can be declared to reference a variable within a class type. Such a pointer does not reference a single variable instance, however, as other data pointers do; instead, it references one variable within any instance of the specified type. In this it resembles an object-relative offset rather than a memory address:
struct tEnv {
tEnv(int aWth, int aHgt): Wth(aWth), Hgt(aHgt) {}
int Wth;
int Hgt;
};
int tEnv::* opSize;
The declaration matches that of an ordinary data pointer, with the indirection operator prefixed by the class type. Typedefs are defined similarly, with the typedef name replacing the pointer name:
typedef int tEnv::* tpSize;
Since class data pointers do not reference specific class type instances, no instance is needed to set such a pointer, though the type is specified:
opSize = &tEnv::Hgt;
An instance is needed to dereference the pointer, however. The dereferenced pointer is applied to the instance and read or writtin as any class variable would be:
tEnv oEnvSm(6, 4); int oSize = oEnv.*opSize;
A function pointer can reference any function with a matching signature:
short gCt(int aID); short (* odCt)(int) = &gCt;
The pointer is declared like a function, with the indirection operator and pointer name replacing the function name. Parameter names can be specified or not. When setting the pointer, the 'address of' operator can be applied to the function or not:
odCt = gCt;
Function pointer typedefs are defined similarly, with the typedef name replacing the pointer name:
typedef short (* td)(int);
Once initialized, function pointers can be invoked directly:
short oCt = odCt(100);
or they can be dereferenced first:
oCt = (*odCt)(100);
Where function pointers are concerned, static class functions and non-class functions are identical; the same pointers can reference functions of either type, so long as the signatures match:
struct tCat {
static short sCt(int aID);
};
odCt = &tCat::sCt;
Pointers can also be made to reference non-static class functions. Much as class data pointers do, each such pointer references a specific class type as well as a function signature:
struct tIt {
short WgtMin(int aCt) const;
};
short (tIt::* odWgt)(int) const = &tIt::WgtMin;
As with non-class function pointers, the indirection operator and pointer name replace the function name in the declaration. As with class data pointers, the indirection operator is prefixed with the class type. Typedefs are defined analogously:
typdef short (tIt::* tdWgt)(int) const;
The class is specified when setting the pointer, but no instance is needed:
tdWgt odWgt = &tIt::WgtMin;
An instance is needed to dereference the pointer, however. The dereferenced pointer is applied to the instance and invoked as any class function would be:
tIt oIt; short oWgt = (oIt.*odWgt)(10); tIt* opIt = new tIt; oWgt = (opIt->*odWgt)(20);
Multidimensional arrays are implemented as arrays of arrays:
int oGrid[gXMax][gYMax];
Elements are accessed by dereferencing each array in turn:
int o = oGrid[oX][oY];
or the equivalent:
int o = *(*(oGrid + oX) + oY);
The rightmost dimension is the only one providing a continuous linear mapping of memory to array values.
When declaring arrays as function parameters, the size of all dimensions but the first must be specified:
void gScan(int aGrid[][gYMax]);
This allows offsets to be calculated when dereferencing the array.
Arrays can be initialized with initializer lists:
int oOff[] = { 5, 10, oX + 10 };
List elements need not be constant. Such lists can be used only during initialization.
If the array size is not specified, it will be inferred from the initializer list. If it is specified, and initializers are not provided for all elements, the remaining elements will be default-initialized.
When initializing multidimensional arrays, the innermost list levels correspond to the rightmost array dimensions:
int oGrid[2][3] = {
{ 11, 12, 13 },
{ 21, 22, 23 }
};
When initializing character arrays with string literals, the compiler adds and initializes one element for the zero-terminator, even when the string is empty:
char oText[] = "";
String literals separated by whitespace are joined into continuous arrays by the compiler:
char oText[] = "ABC" "DEF" "GHI";
All string literal data is allocated for the lifetime of the program.
Typedefs can be defined with local scope.
Class types include classes, structures, and unions.
Like variables and functions, namespaces, enumerations, typedefs, and other class types can be nested within class declarations. Such elements have class scope, plus the specified access level.
Class types can also be defined with local scope, as long as all functions are defined within the declaration:
void gExec() {
class tNode {
public:
int Cd;
void Send() { gSend(Cd); Cd = 0; }
};
...
Forward declaration allows types to be referenced before they are defined, so long as their members are not accessed and their sizes not calculated:
struct tNode;
class tGraph {
public:
void Add(const tNode& aNode);
...
Class elements are private by default. Structure and union elements are public by default.
The friends of a class gain access to all class elements accessible to the class itself, including protected elements inherited from parents. Because private parent elements are inaccessible to the class, they are inaccessible to friends.
The friends of a base class have access to the base class members of all derived classes, even when the derived classes do not. No other friend obligations are inherited by derived classes.
Class types and functions can be declared friends. Neither need be defined at the time of the friend declaration.
Classes, structures, and unions must be identified as such when declared as friends:
struct tBatch {
friend class tList;
friend struct tItr;
friend union thIdx;
...
Friend privileges are not inherited by types derived from friend types.
Function friends are declared with a complete function prototype:
class tSet {
friend void gSort(tList& arList);
friend void tStore::Write();
...
Non-static const class variables must be initialized with member initializers:
struct tLine {
tLine(unsigned int aCode): Code(aCode) {}
const unsigned int Code;
};
Class variables declared static are not associated with instances of the containing type; instead, a single variable instance is associated with the class as a whole. Such variables are initialized before main executes. Their relative initialization and deinitialization order is undefined.
Non-static class variable declarations serve as definitions. Static class variable declarations do not, however, so their variables must be defined elsewhere. Technically, even static const integral variables — which can be initialized where they are declared — require explicit definition:
struct tEl {
static const int sIdxMax = 255;
};
int tEl::sIdxMax;
Visual Studio 2005 does not require explicit definition in this case, however.
Fields or bit fields allow the memory used by an integral type to be split between several class variables. The bit size of each variable is specified after the variable name:
struct tMsg {
unsigned char X: 2;
unsigned char Y: 2;
char: 3;
bool Set: 1;
};
The size of all fields cannot exceed that of the type from which the fields derive. Only integral types and enumerations can be used as fields.
Omitting the variable name creates an anonymous field, typically used for padding. The implementation may also insert padding to preserve type boundaries.
Fields can be used in any class type. Like other such variables, fields declared within unions share memory with other variables. It is impossible to obtain the address of a field.
Static class functions are not associated with a particular instance of the class. As a result, this is not defined within static functions.
const functions are allowed to modify static class data. They cannot modify other variables in the same instance, except those declared mutable.
volatile class functions cannot invoke non-volatile functions on the same instance. They can modify non-volatile class data, however.
A constructor is called when storage is allocated. If no constructor is accessible at some point in the program, no instance can be allocated there, dynamically or otherwise.
Class variables are initialized by a constructor in the order of their declaration, not their order within the member initializer list.
Class types that do implement destructors cannot be automatically or statically allocated unless their destructors are accessible. They can be dynamically allocated in this situation, but not deleted.
Constructors with no non-default parameters qualify as default constructors. The function call operator is not used when invoking a default constructor:
tEl oElTemp; tEl* opEl = new tEl;
If no constructor is explicitly defined for some class type, the compiler will create a default constructor for it, as long as all the type's members can themselves be default-constructed. All constructors call the default constructors of member variables not explicitly constructed in their initializer lists.
The compiler does not implement destructors on its own, but it does allow destructors to be invoked on types that lack them. Such calls have no effect.
If an array is not completely initialized on definition, the base type of that array must provide an accessible default constructor, explicit or otherwise.
Copy constructors accept a reference to an instance of their own type. They can accept additional parameters if default values are provided for them. It is generally advisable, when implementing a copy constructor, to overload the assignment operator as well.
When a class type variable is initialized with an instance of the same type, the copy constructor is called, not operator=:
tIt oItTemp = oIt;
If no copy constructor is explicitly defined for some class type, the compiler will create one that calls the copy constructor of each variable in the type, as long as all variables have accessible copy constructors.
When a derived class is instantiated, the constructors of its least-derived base classes are invoked first. When it is destroyed, its destructor and those of its most-derived base classes are invoked first.
If a base class has a default constructor, it need not be explicitly initialized by the derived class. If it does not, a base class initializer must be included in the derived class member initializer list:
struct tCtl {
tCtl(int ahWin): hWin(ahWin) {}
int hWin;
};
struct tBtn: public tCtl {
tBtn(int ahWin): tCtl(ahWin) {}
};
There is no way for a derived class to initialize base class variables directly.
Declaring base class destructors virtual ensures that derived destructors are called even when instances are deleted through base class pointers.
Just as base class functions are hidden by derived class functions with the same name and signature, base class variables are hidden by derived class variables with the same name. Base members can be accessed from derived classes with the scope resolution operator:
struct tCont {
bool Avail;
void Sort();
};
struct tList: public tCont {
int Avail;
void Sort() { tCont::Sort(); }
};
Scope resolution can also be performed outside the class:
tList oList; bool oAvail = oList.tCont::Avail;
In both cases, the variable name is prefixed with the name of the containing class type.
Declaring a class function virtual allows derived classes to override it. Non-virtual functions can only be hidden by derived classes. Class types with at least one virtual function are said to be polymorphic.
Private base class functions can be overridden in derived classes, causing the derived implementations to be called when invoked through the base. The base implementations remain private.
Virtual functions are never inlined.
Classes declaring one or more pure virtual functions cannot be instantiated. Their descendents cannot be instantiated either, until all such functions have been overridden.
In all other respects, pure virtual functions resemble other class functions; they can even be defined and invoked:
struct tPerson {
virtual void vRep() = 0;
};
void tPerson::vRep() {
cout << "Person" << endl;
}
struct tCust: public tPerson {
void vRep() {}
};
void gExec() {
tCust o;
o.tPerson::vRep();
}
In fact, pure virtual destructors — used to render classes with no other functions abstract — must be defined if derived classes are to be instantiated.
Inheritance access levels 'cap' the accessibility of base class members, with protected inheritance rendering public base members protected in the inheriting class, private inheritance rendering all base members private, and public inheritance leaving all base members unchanged. The changes are enforced relative to the inheriting class, so private inheritance allows base members to be accessed by the inheriting class, but not by descendents of that class:
class tFig {
public:
int Wth;
};
class tSq: private tFig {
public:
tSq() { Wth = 10; }
};
To prevent non-public base classes from being accessed indirectly, derived classes cannot be upcast to protected or private base classes without an explicit cast:
tFig* opFig = (tFig*)&oSq;
When a class derives from multiple base classes, and two or more of the base classes share a common ancestor, multiple instances of the common ancestor variables and functions are defined in the derived class. To avoid ambiguity, it is necessary to specify an intermediate class when referencing duplicated variables and functions.
To avoid such duplication, the common ancestor class can be inherited as a virtual base class:
struct tVehi {
const string Model;
tSpanTime Age() const;
};
struct tBoat: virtual public tVehi {
};
struct tCar: virtual public tVehi {
};
struct tBoatCar: public tBoat, public tCar {
};
Unions cannot contain any variable that implements a constructor or destructor, or that overloads the assignment operator; doing so would cause ambiguity when the union itself was constructed, destroyed, or assigned. Unions cannot participate in inheritance or declare virtual functions.
Unions declared within other class types can be anonymous:
struct tVal {
union {
int uX;
int uY;
};
};
Variables within such unions are accessible as though they were members of the containing type. Unions with file scope can be made anonymous, but they must be declared static:
static union {
int uID;
int uIDEx;
};
Anonymous unions cannot define functions or non-public variables.
Like arrays, classes and structures without constructors or private or protected data are classified as aggregates, and can be initialized with initializer lists. The list values need not have the same type, and can include other classes or structures:
struct tOpt {
tOpt(int aLenMax): LenMax(aLenMax) {}
int LenMax;
};
struct tConn {
int ID;
string Name;
tOpt Opt;
};
tConn oConn = { 100, "Test", tOpt(10) };
The values must be specified in the order of their declaration. As with arrays, elements for which no value is provided are default-initialized. Initializer lists cannot be used except during initialization.
Conversions from user types to other types are defined with conversion operators. Conversions from other types are defined with constructors.
The conversion operator declaration is unlike that of other operators: no parameter is accepted, and no return type is specified, not even void. Instead, the conversion type is specified in place of the operator symbol:
struct tOrd {
operator int() const { return 100; }
};
Conversion operators allow user-defined type instances to be implicitly converted to other types:
tOrd oOrd; int oID = oOrd;
Conversion operators, when defined, are also used for explicit conversions:
cout << (int)oOrd;
Any constructor that can be invoked with a single parameter and is not declared explicit will be used by the compiler to perform implicit conversions from the parameter type to the containing type:
struct tCust {
tCust(int aID = 0, float aBal = 0.0);
};
tCust oCust = 1;
If the constructor is marked explicit, the same conversion can be performed only with a typecast.
Upcasting converts a derived class reference to a base reference. Downcasting converts a base class reference to a derived reference. Crosscasting converts a base class reference to another base reference, which is possible only when the instance derives from both.
dynamic_cast uses RTTI to convert pointers and references within a class family:
tCtl* opCtl = dynamic_cast<tCtl*>(opBtn); tCtl& orCtl = dynamic_cast<tCtl&>(orBtn);
When dynamic_cast succeeds, the converted pointer or reference is returned. When a pointer cast fails, or when the source pointer is zero, zero is returned. When a reference cast fails, bad_cast is thrown. Because they provide no type information, void pointers cannot be passed to dynamic_cast.
A given upcast will succeed if the destination is a unique, accessible base class of the instance type. If multiple matching base instances are found — which can result from multiple inheritance — the cast will fail. It will also fail if the destination is a private or protected base class.
A given downcast will succeed if the source pointer or reference type is polymorphic, the destination type is accessible, and the instance type matches or derives from the destination type.
A crosscast will succeed if the source pointer or reference type is polymorphic, the destination type is accessible, and the instance type derives from both the source and destination types. The destination type need not be polymorphic.
Any cast will succeed if the destination type matches the instance type.
static_cast does not use RTTI, never requires a polymorphic source type, and performs no run-time check, so it cannot return zero or throw. It is used to perform upcasts and downcasts. It also converts between 'related' types, like integers and floating-point types:
bool oVal = true; int oIdx = static_cast<int>(oVal);
const_cast is used to remove const or volatile qualifiers, which is safe only when the data thus modified was not originally declared with such qualifiers:
struct tCurs {
int Idx;
void Skip() const;
};
void tCurs::Skip() const {
++(const_cast<tCurs*>(this)->Idx);
}
reinterpret_cast converts 'unrelated' types. It generally produces a value with the same bit pattern as its argument. It converts between all pointer types, and between all pointer and integral types.
The traditional C-style cast can perform any conversion expressible with a combination of static_cast, reinterpret_cast, and const_cast.
The typeid operator returns a const reference to a type_info instance representing the specified type:
const type_info& oInfo = typeid(string);
The type_info class, defined in <typeinfo>, identifies types. It supports instance comparisons with operator== and operator!=, and implements the name function that returns a char array representing the type name.
The exact output of type_info::name is implementation-defined. The Visual Studio 2005 implementation returns 'class', 'struct', 'union', or 'enum', the names of any containing namespaces or classes, the type name, and the pointer indirection operator, if the type is a pointer type.
Declarations introduce identifiers to translation units. Some also define elements.
Identifiers can contain letters, numbers, and underscores, but cannot begin with numbers. Many identifiers beginning with underscores are reserved for use by the environment.
A variable declaration also serves as a definition if storage is allocated, which occurs for all but class-static variable declarations and extern declarations that lack initializers.
A function declaration serves as a definition if the function's body is inlined therein.
An identifier's scope determines the parts of a translation unit from which the identifier is usable.
Identifiers declared directly within a namespace have namespace scope. They are usable after their declaration.
The global namespace is unnamed, and is not explicitly defined; it contains all other namespaces. Identifiers in this namespace are said to have file scope.
Labels have function scope, making them usable throughout a function, even before they are declared. This allows goto to jump into or out of blocks. No other identifiers have this scope.
Non-label identifiers declared anywhere within a function have local scope. They are usable after their declaration in the block that declares them, including within blocks enclosed by that block.
Identifiers declared within class type declarations and outside function definitions have class scope. Such identifiers also have public, protected, or private access levels, but these are not scopes as such. Identifiers with class scope are usable anywhere their access levels permit.
Linkage determines whether distinct declarations naming the same identifier reference the same element, or different ones.
Elements referenced by identifiers with no linkage cannot be referenced from multiple scopes; such identifiers therefore reference different elements when used in different scopes. Local variables have no linkage.
Elements referenced by identifiers with internal linkage can be referenced from multiple scopes, but not from multiple translation units; such identifiers therefore reference different elements when used in different translation units. Variables and functions declared static with file scope have internal linkage.
Elements referenced by identifiers with external linkage can be referenced from multiple scopes or translation units; such identifiers therefore always reference the same elements. Variables and functions declared extern with file scope have external linkage, unless already given internal linkage in the same translation unit.
Storage classes and scopes combine to determine linkages and variable lifetimes.
const file scope variables always have internal linkage. For non-const variables, and those without file scope:
| Namespace | Local | Class | |
| (none) | External | External | External |
| static | Internal | None | External |
| extern | External | External | — |
| auto | — | None | — |
| register | — | None | — |
For functions:
| Namespace | Class | |
| (none) | External | External |
| static | Internal | External |
| extern | External | — |
Local variables declared static are initialized the first time they are encountered; in this they differ from other static variables, which lack deterministic initialization. Unlike other local variables, they are allocated on the heap, and persist for the lifetime of the program. Their relative deinitialization order is undefined.
Non-class functions have extern storage by default.
Local variables have auto storage by default.
register requests that the declared variable be stored in a register. This request may be ignored by the compiler.
Class variables declared mutable can be modified by functions declared const. Unlike other storage classes, mutable does not affect linkage or variable lifetimes.
The two qualifiers, const and volatile, are sometimes known as 'cv-qualifiers'. They can be specified simultaneously.
const can appear in two locations within a pointer declaration. Placing it before the type prevents the referenced instance from being modified through the pointer:
const int* op;
Placing it before the variable prevents the pointer itself from being modified:
int* const op = 0;
Placing it in both locations prevents either modification:
const int* const op = 0;
When applied to variables or parameters, volatile warns the compiler that their values can be read or written without its knowledge. The compiler is expected to avoid certain optimizations, therefore, such as placing the variables in registers.
Only volatile functions can be invoked by instances or functions declared volatile. Functions declared volatile can modify non-volatile variables, however. Declaring a class type instance volatile causes its data to be considered volatile as well.
Namespaces define distinct scopes, named or otherwise. They are defined at file scope or within other namespaces.
Declaring an element in a namespace block places it in that namespace. The element can be defined outside the block, but all namespace members must then be appropriately prefixed:
namespace nChain {
struct tLink;
tLink gHead();
}
struct nChain::tLink {
string ID;
};
nChain::tLink nChain::gHead() {
return nChain::tLink();
}
Distinct namespace blocks with the same name contribute to the same namespace:
namespace nStr {
string gUp(const string& aText);
}
namespace nStr {
string gLow(const string& aText);
}
Defining an unnamed namespace issues an implicit using directive. Because such a namespace cannot be referenced anywhere else, its content effectively has file scope:
namespace {
short gLenMax;
}
gLenMax = 0xFFFF;
using declarations bring individual identifiers into the current scope, allowing them to be referenced without specifying their namespaces:
using nStr::gUp;
A compilation error results if the specified identifier is already declared in the current scope. If the identifier is declared in an enclosing scope, the element in that scope is hidden.
using directives make entire namespaces accessible without bringing identifiers into the current scope:
using namespace nStr;
If the same identifier is declared locally and in an accessible namespace, the namespace declaration is hidden. If the same identifier is declared in an enclosing local scope and within an accessible namespace, the declaration in the enclosing scope is hidden. If the same identifier is declared in multiple accessible namespaces, including the global namespace, a namespace must be specified whenever the identifier is referenced.
The global namespace is always accessible.
break jumps out of the enclosing for, while, do while, or break statement. Only one such statement is terminated.
continue effectively jumps to the end of the enclosing for, while, or do while statement, not the beginning. The distinction is important for do while loops, as continue always causes the loop condition to be retested.
goto jumps to a label in the current function. Labels have function scope, not local scope, so goto can jump into blocks as well as out of them:
if (oDone) goto X;
do {
...
X:
cout << "Done" << endl;
} while (false);
Every label must be followed by a statement or another label.
The sizeof operator can be applied to instances or types. When applied to an array instance, the size of the entire allocation is returned, this being the size of the array type multiplied by the element count.
It is safe to delete null pointers.
To ensure the correct destructors are called, any pointer being deleted must have the same type as the referenced instance, or its type must be a base class of that instance with a virtual destructor.
Types allocated with new[] must provide accessible default constructors. Having been allocated with new[], arrays must be deallocated with delete[] to ensure the destructor is invoked for each element:
tEl* opEls = new tEl[4]; delete[] opEls;
The standard new operators throw bad_alloc if an allocation fails. The nothrow placement new overloads return zero instead:
char* opBuff = new(nothrow) char[256];
It is also possible to have a function called when allocation fails by passing a callback to set_new_handler.
The evaluation of bitwise operators is not 'short-circuited' as logical operator evaluation is.
Bitwise XOR is expressed with operator^. There is no logical XOR operator, but operator!= is equivalent if both operands are booleans.
Unary operators + and - promote their operands to int.
Both operands are evaluated by the sequence operator and the result of the right one is returned:
int oYOrig = ++oX, ++oY;
This operator has lower precedence than any other.
Applying the scope resolution operator without a name specifies the global namespace:
char gChEsc;
namespace n {
char oCh = ::gChEsc;
};
Applying the operator with a base class name allows base members to be accessed from derived instances:
struct tProd {
int Code;
}
struct tBook: public tProd {
int Code;
}
tBook oBook;
cout << oBook.tProd::Code << endl;
Function declarations are also known as prototypes.
Overloading allows the same name to be associated with different functions in the same scope. When an overloaded function is invoked, the compiler chooses the definition to be executed by comparing the provided arguments with available signatures. If no exact match is found, a near match is chosen by applying, in order:
| 1) | Promotions; |
| 2) | Standard conversions; |
| 3) | User-defined conversions; |
| 4) | Unspecified function parameters. |
If no match is found, a compiler error results.
The left operand becomes the host instance when a binary operator is defined as a class function.
Operator overloads in class types can be declared virtual. All overloads are inherited except those for the assignment operator.
If the assignment operator is not overloaded for a given class type, ordinary memberwise assignment is performed. Any assignment overload in the base class is called when that part of the derived class is assigned, however.
It is generally advisable, when overloading the assignment operator, to implement a copy constructor as well.
When overloading a postfix increment or decrement operator, it is necessary to introduce a dummy int parameter to distinguish its signature from that of its prefix counterpart. The parameter is not used:
struct ti {
// Prefix increment:
ti& operator++() { ++Idx; return *this; }
// Postfix increment:
ti operator++(int) { ti oi(*this); ++Idx; return oi; }
int Idx;
};
Postfix operators are also distinguished by the fact that they return instance copies, not references.
Like the binary arithmetic operators, new and delete can be overloaded as class or non-class functions. If they are overloaded as class functions, those overloads will be called when the operator is applied to the containing class in preference to any non-class overloads. The class function overloads are implicitly static.
Variations like array new and delete, placement new and delete, and nothrow new count as separate operators, with distinct overloads.
The size of the object being allocated is determined and passed automatically to operator new. The size of the entire array is passed to operator new[]. The pointer being deallocated is passed to the delete operator:
struct tMgr {
void* operator new(size_t aSize);
void operator delete(void* ap) {}
};
void* tMgr::operator new(size_t aSize) {
if (aSize > sizeof(tMgr)) terminate();
static tMgr osInst;
return &osInst;
}
Any number of parameters can be added to new or delete. Whether used to allocate at specific addresses or not, operators with extra parameters are known as placement new and delete:
struct tMgr {
tMgr() {}
tMgr(const string& aID): ID(aID) {}
string ID;
void* operator new(size_t aSize, int aIdx);
void operator delete(void* ap, int aIdx) {}
void operator delete(void* ap) {}
};
void* tMgr::operator new(size_t aSize, int aIdx) {
if ((aIdx < 0) || (aIdx >= 4)) terminate();
static tMgr osInsts[4];
return &osInsts[aIdx];
}
If an exception is thrown during initialization, the placement delete overload with the same additional parameters is called. If such an overload is not defined, no delete is called, and memory may leak. The placement delete overload is not called when memory is explicitly deleted.
To invoke placement new, pass the additional arguments to the new operator itself. The allocation size is passed automatically:
tMgr* opMgr = new(0) tMgr("A1");
Any type can be thrown. All standard exceptions derive from exception, defined in <exception>.
The first catch block to match some exception is executed, even if a succeeding block matches more closely. If no block in the call stack matches the exception, terminate is called. By default, terminate ends the program, but a custom handler can be set by passing a callback to set_terminate, also defined in <exception>.
Caught exceptions are re-thrown by invoking throw with no argument.
Exception specifications limit the types allowed to be thrown from a given function:
void gVeri() throw(bad_alloc, bad_typeid);
The same specification must be applied to the function's declaration and definition.
If an exception is thrown from a function with such a specification, and its type is neither specified nor derived from a specified type, unexpected is called. By default, unexpected ends the program, but a custom handler can be set by passing a callback to set_unexpected, also defined in <exception>. The specification is checked only when exceptions are thrown from the function; any types can be safely thrown and caught within the function.
When a function is overridden, it must specify the same exception specification as was declared in the base class, or a more restrictive one.
If no type is listed in the specification, no exception is allowed to be thrown from the function:
void gSort() throw();
The template keyword defines a template parameter list. Like a function parameter list, this construction introduces parameters to be used in the following definition:
template <typename xType, int xSize>
struct tArr {
tArr(int aVal);
xType Els[xSize];
};
In template classes, the class name is followed by a template argument list, which identifies the specialization to be defined. Often this reiterates the parameter list, but in partial or explicit specialization, some or all parameters are replaced with specific types:
template <typename xType, int xSize>
inline tArr<xType, xSize>::tArr(int aVal) {
for (int o = 0; o < xSize; ++o)
Els[o] = aVal;
}
Template functions are defined much as template classes are, but the template argument list is not needed for the general specialization:
template <typename xType>
void gExch(xType& arVal0, xType& arVal1) {
xType oVal(arVal0);
arVal0 = arVal1;
arVal1 = oVal;
}
Nor is the argument list often needed to invoke the function:
gExch(oX, oY);
It can be used to select particular specializations, however:
gOut<string>("Ready");
It is also used to specify non-type parameters:
gExec<256>(od, 1000);
Template functions can be defined within classes that are not themselves template classes:
struct tFile {
template <class xData>
void Write(xData& arData);
};
template <class xData>
inline void tFile::Write(xData& arData) {
eWrite(arData.Read());
}
Templates can be parameterized with type or non-type parameters. Default arguments can be specified for either:
template <typename xType = short, int xDim = 3>
struct tPt {
static const int sDim = xDim;
xType Disp[xDim];
};
As with function parameters, parameters following the first default argument also require defaults. An argument list must be specified when referencing a template class, even if all defaults are accepted:
tPt<> oPt;
Type parameters are preceded by typename or class. The two are equivalent in this context.
Class types with local scope cannot be used as type parameters.
Non-type parameters are defined with a type and a parameter name, much like function parameters, though data is not technically 'passed' during specialization. Template arguments must be constant at compile-time.
Non-type parameters can be:
String literals are not valid template arguments.
Explicit specialization allows distinct implementations to be defined for specific parameter combinations. The implementations can define different variables and functions, and need share nothing, in fact, but their names and template parameter signatures.
To distinguish an explicit specialization from other specializations, explicit or otherwise, the argument list is populated with specific types. Since all parameters are constrained, an empty parameter list is specified in the class definition:
template <>
struct tArr<bool, 1> {
tArr(int aVal);
bool El;
};
The same is done for non-class function specializations:
template <>
void gExch<int>(int& arVal0, int& arVal1) {
arVal0 ^= arVal1;
arVal1 ^= arVal0;
arVal0 ^= arVal1;
}
No parameter list whatsoever is needed for class function specializations:
inline tArr<bool, 1>::tArr(int aVal) {
El = (aVal != 0);
}
Partial specialization allows distinct implementations to be defined for certain parameter choices while leaving others open.
Open parameters remain in the parameter list, just as their names remain in the argument list. Constrained parameters are removed from the parameter list, and their entries in the argument list replaced with specific types:
template <int xSize>
struct tArr<bool, xSize> {
tArr(int aVal);
bool Els[xSize];
};
template <int xSize>
inline tArr<bool, xSize>::tArr(int aVal) {
for (int o = 0; o < xSize; ++o)
Els[o] = (aVal != 0);
}
Within template definitions, typename explicitly marks identifiers as types:
template <typename xType>
void gNext() {
typename xType::tData oData;
gExec(oData);
}
Because the compiler knows nothing of the parameter types until the template is instantiated, it cannot determine whether a member of such a type is itself a type, or something else. Without typename, it may be unable to follow the programmer's intent.
Visual Studio 2005 does not require typename in the above situation, however.
After preprocessing, source files are known as translation units or compilation units. The compiler transforms translation units into object files.
Generally, the pound sign in a preprocessor directives must be the first non-whitespace character in its line. Visual Studio 2005 does not enforce this, however.
Preprocessor directives can be made to span multiple lines by appending a backslash to the end of each continued line.
If no value follows the identifier in a #define directive, the identifier is defined, but is replaced with the empty string during preprocessing.
#undef cancels the effect of a #define directive.
#ifdef and #ifndef test whether a preprocessor identifier is defined. #if and #elif test whether a constant expression evaluates to true. Any of these can be followed by #else. All must be followed, at some point, by #endif.
Included files are always located in an implementation-specific manner. Generally, however, files with names enclosed in angle brackets are located by searching a user-defined path:
#include <iostream>
Filenames in double quotes are typically sought in the directory containing the file performing the include. Visual Studio 2005 then searches within directories containing files that include that file. Most implementations finally locate the file as though its name were enclosed in angle brackets:
#include "Test.h"
Compilation is aborted if the preprocessor encounters #error. The string following #error, if any, is displayed by the compiler.
#pragma provides access to implementation-specific compiler features.
#line is used to reset the preprocessor's record of the line number:
#line 1
or its record of the line number and file name:
#line 1 "Test.h"
The # operator wraps its operand with double quotes:
#define mOut(zText) cout << "~ " #zText << endl;
The ## operator concatenates its operands:
#define mJoin(z0, z1) z0##z1
These macros are defined by default:
| Identifier | Value |
| __LINE__ | The number of the line being preprocessed |
| __FILE__ | The name of the file being compiled |
| __DATE__ | The compile date |
| __TIME__ | The compile time |
| __cplusplus | Defined if the code is being compiled as C++ |
main accepts zero or two arguments:
int main() int main(int argc, char* argv[])
argv[0] stores the name of the executable, with or without its path. argv[argc] is set to zero.
The value returned from main becomes the return value of the executable. Instead of returning from main, programs can also be terminated with exit, which accepts a return value. If neither return nor exit is called, zero is returned.
C++ Pocket Reference
Kyle Louden
2003, O'Reilly Media
The C++ Programming Reference, Third Edition
Bjarne Stroustrup
1998, Addison-Wesley
Scope Regions in C and C++
Dan Saks
November 2008, www.embedded.com
Linkage in C and C++
Dan Saks
November 2008, www.embedded.com
volatile - Multithreaded Programmer's Best Friend
Andrei Alexandrescu
November 2008, www.ddj.com