cpp
C++
Contents
- C++ basics: objects, variables, initialization, input, and output
- Functions, namespaces, preprocessing, and multiple files
- Fundamental data types, literals, constants, and strings
- Operators and bit manipulation
- Scope, duration, linkage, and storage classes
- Control flow
- Type conversion
- Function overloading and templates
- References, pointers, passing arguments, dynamic memory, and arrays
- Structs, classes, constructors, access levels, and friends
1. C++ basics
Structure of a C++ program
The main function is the entry point of a C++ program. A program starts running from main().
#include <iostream>
int main()
{
std::cout << "Hi\n";
return 0;
}
Objects and variables
In C++ we can access memory via objects. An object represents a region of storage, such as memory or a CPU register.
We can initialize a variable in three ways:
int a = 5; // copy initialization
int b(5); // direct initialization
int c{ 5 }; // list initialization
List initialization is generally better because it does not allow narrowing conversions, so it can catch data loss and some mistakes at compile time. We can also do multiple initializations in one statement when needed.
For an unused variable, we can use [[maybe_unused]] before the declaration. This is useful when we want to avoid an unused-variable compiler warning.
[[maybe_unused]] int unusedValue{};
An uninitialized local variable can contain an indeterminate value. We should initialize variables when we declare them.
Input and output: iostream, cout, cin, and endl
The <iostream> header is needed for input and output.
std::coutmeans character output.std::cinmeans character input.<<is the insertion operator when used withstd::cout.>>is the extraction operator when used withstd::cin.
#include <iostream>
int main()
{
int x{};
std::cout << "Hi, this is value of x: " << x << '\n';
std::cin >> x;
std::cout << "Now x is: " << x << '\n';
return 0;
}
std::cout is buffered. This means output is often stored in a buffer first and then sent to the console later.
std::endl writes a newline and flushes the output buffer, so it is slower than '\n'. We normally use '\n' and use std::endl only when we actually need a flush.
std::cin is also buffered, and we can use multiple extractions:
int x{};
int y{};
std::cin >> x >> y;
If we enter 5 7, the first extraction reads 5 and the second reads 7. A space, tab, or newline can separate them.
cin >> behavior — quick notes
std::cin >>reads one whitespace-delimited token and skips leading whitespace such as spaces, tabs, and newlines.- It does not wait for a fresh Enter when data is already in the input buffer.
- So
1 2typed on one line, or1and2typed on separate lines, work the same way for twostd::cin >> intreads. - A leftover newline is mainly a problem when
std::getlinefollowsstd::cin >>. Beforegetline, we can write:
#include <limits>
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
- A failed extraction, for example entering
abcfor anint, sets the fail state and leaves the bad characters in the input buffer. Future reads fail until we fix it:
std::cin.clear();
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
- Check
std::cin.fail()after input that can be invalid. - One line to remember: >> stops at whitespace, consumes only what it reads, and never auto-clears errors.
Literals, parameters, and arguments
Literals are fixed values inserted directly into the source code. For example, 5, 1.2, and "Hello world!" are literals.
A parameter is a variable used in a function definition. An argument is the value passed by the caller.
int add(int a, int b) // a and b are parameters
{
return a + b;
}
int result{ add(2, 3) }; // 2 and 3 are arguments
Forward declarations and naming conflicts
We can write a function declaration at the beginning of the program, then define the function after main().
int add(int a, int b); // forward declaration
int main()
{
return add(2, 3);
}
int add(int a, int b)
{
return a + b;
}
If two identifiers have the same name in the same scope, it produces a naming conflict. We can avoid such collisions by using local variables, different scopes, or namespaces. A common naming convention for a global variable is to start its name with g_, such as g_count.
2. Functions, namespaces, preprocessing, and multiple files
Namespaces and the scope resolution operator
A namespace is like creating a scope so that we can avoid name collisions. We use the scope resolution operator :: to access names inside a namespace.
namespace Rishi
{
int age{ 5 };
void bedtime()
{
}
}
int main()
{
int age{ 10 };
return Rishi::age;
}
We can define a namespace in global scope or inside another namespace. A namespace cannot be defined inside a function.
The preprocessor
The preprocessor runs before compilation and produces a translation unit, which is then compiled by the compiler. Linking happens after compilation: the linker combines object files and libraries into the final program. Together, preprocessing, compilation, and linking are normally called the build process.
Some important work of the preprocessor is:
- Remove comments.
- Resolve
#include. - Expand macros made with
#define.
Things that start with # are usually called preprocessor directives. They are handled during preprocessing, and the preprocessor does not understand normal C++ syntax.
#include <iostream>
#define JOE
int main()
{
#ifdef JOE
std::cout << 1 << '\n';
#endif
return 0;
}
In the case of a macro, the identifier is replaced by its replacement text.
Header files and header guards
When making multiple files, for example add.cpp and main.cpp, we can put a function declaration in a header file. Then every file that needs the declaration can include the header.
The header file consists of a function declaration and a header guard.
// add.h
#ifndef ADD_H
#define ADD_H
int add(int a, int b);
#endif
Header guards stop the same header from being included more than once in one translation unit.
3. Fundamental data types, literals, constants, and strings
Fundamental data types
There are many fundamental data types. My notes divide them into these categories:
void- Null pointer type:
std::nullptr_t - Floating-point types
- Integer types
bool- Character types
Integer types
Integer types store whole numbers.
short intintlong intlong long int
Any data type can hold 2n values, where n is the number of bits.
We can create an unsigned data type by writing unsigned.
unsigned int count{};
If we assign a negative value to an unsigned value, it wraps around modulo 2n. For example, converting -1 to an unsigned integer gives the highest value that type can store. This is unsigned wraparound, so we should avoid unsigned integers for normal arithmetic unless we really need their behaviour.
For signed integers, overflowing the valid range is undefined behavior.
Fixed-width integers and size_t
C++ only promises minimum ranges for types such as int, and the exact size can vary by system. Fixed-width types are useful when we need an exact size and want code that is less platform-dependent.
We use them through <cstdint>:
#include <cstdint>
std::int8_t smallNumber{};
std::uint8_t smallUnsignedNumber{};
std::int16_t mediumNumber{};
std::uint16_t mediumUnsignedNumber{};
std::int32_t number{};
std::uint32_t unsignedNumber{};
std::int64_t bigNumber{};
std::uint64_t bigUnsignedNumber{};
std::int8_t and std::uint8_t may behave like character types when printed. std::size_t is the unsigned type generally used for sizes and indexes.
Floating-point types
The floating-point types are:
floatdoublelong double
For scientific notation, we can write something like:
double x{ 8.0e10 };
By default, std::cout prints floating-point values with a precision of 6 digits. We can change that with std::setprecision(), which needs <iomanip>.
#include <iomanip>
#include <iostream>
std::cout << std::setprecision(10) << 1.23456789 << '\n';
When a floating-point literal has no suffix, it is double. A suffix f makes it float.
double d{ 200.5 };
float f{ 200.5f };
Floating-point values have a sign, exponent, and significand (also called mantissa). A finite number cannot store every possible real number, so rounding error happens. Floating-point also has special values such as infinity, NaN, and positive and negative zero.
Boolean values
We can do:
bool a{ true };
bool b{ false };
bool c{ !true };
bool d{ !false };
bool e{ 1 };
bool f{ 0 };
// bool g{ 2 }; // not allowed: narrowing conversion in list initialization
bool g = 2; // allowed with copy initialization and evaluates to true
When printing bool values:
std::cout << a << std::endl; // prints 1 if a is true and 0 if false
If we want to print true or false, we use:
std::cout << std::boolalpha; // manipulates cout; it does not output text itself
std::cout << a << '\n';
std::cout << std::noboolalpha; // reset back as before
Input of boolean values
We can use normal boolean input:
bool b{};
std::cin >> b;
// 0 becomes false and non-zero numeric input becomes true
If we want to take true and false as input, then we apply boolalpha to std::cin.
std::cin >> std::boolalpha;
std::cin >> b; // enter lowercase true or false; another word makes extraction fail
std::cin >> std::noboolalpha;
Characters and the null pointer type
Character types include:
charwchar_tchar8_tchar16_tchar32_t
These are integral types, so they are stored as numbers in memory.
nullptr is the modern null pointer literal. Its type is std::nullptr_t.
Literals, suffixes, and numeral systems
We have suffixes we can add to literals:
#include <iostream>
int main()
{
std::cout << 5 << '\n'; // 5 without a suffix is int by default
std::cout << 5L << '\n'; // long
std::cout << 5u << '\n'; // unsigned int
return 0;
}
Numeral systems:
int hexVar{ 0x1A }; // hexadecimal: 1A is 26 in decimal
int octVar{ 055 }; // octal: 055 is 45 in decimal
int binVar{ 0b11010 }; // binary: 11010 is 26 in decimal, available since C++14
We can use ' to separate digits:
int mask{ 0b1011'0010 };
By default the data printed by std::cout is decimal, but we can use output manipulators:
#include <iostream>
int main()
{
int x{ 12 };
std::cout << x << '\n'; // decimal by default
std::cout << std::hex << x << '\n'; // hexadecimal
std::cout << x << '\n'; // still hexadecimal
std::cout << std::oct << x << '\n'; // octal
std::cout << std::dec << x << '\n'; // return to decimal
std::cout << x << '\n'; // decimal
return 0;
}
To output binary, there is a standard library class called std::bitset:
#include <bitset>
#include <iostream>
int main()
{
std::bitset<8> bits{ 0b0010'0010 };
std::cout << bits << '\n';
return 0;
}
Constants, the as-if rule, and compile-time programming
The as-if rule says the compiler can modify code in any way to optimize it as long as there is no observable change in the program's behavior.
It can do this through:
- Constant folding.
- Constant propagation.
- Dead code elimination.
Compile-time programming is a technique that shifts some execution of logic to build time, which can make the program faster at runtime.
Many compile-time features use constant expressions. A constant expression is an expression that can be evaluated at compile time. Expressions that must wait for runtime input are runtime expressions.
#include <iostream>
int getNumber()
{
std::cout << "Enter a number: ";
int y{};
std::cin >> y; // can only execute at runtime
return y;
}
int five()
{
return 5; // the function itself is not constexpr, so a call to five() is a runtime expression
}
int main()
{
5; // constant expression
1.2; // constant expression
"Hello world!"; // constant expression
5 + 6; // constant expression
1.2 * 3.4; // constant expression
8 - 5.6; // constant expression
sizeof(int) + 1; // constant expression
getNumber(); // runtime expression
five(); // runtime expression
std::cout << 5; // runtime expression
return 0;
}
const means that a variable cannot be modified after initialization. constexpr means the value is usable in a constant expression and requires compile-time evaluation when the context needs it. We use constexpr when we want the compiler to evaluate a value at compile time.
String
C-style strings are difficult to work with. Modern C++ gives us std::string and std::string_view. They are class types, not fundamental types.
We use std::string from the <string> header:
#include <iostream>
#include <string>
int main()
{
std::string name{ "Rishi" };
std::cout << name << '\n';
name = "another text of any length";
std::cout << name << '\n';
return 0;
}
Behind the scene, std::string manages its own dynamic memory when needed. We can use it much like a normal variable.
Getting input can be a little problem because normal std::cin stops at whitespace. To get a whole sentence, we use std::getline.
#include <iostream>
#include <string>
int main()
{
std::string text{};
std::getline(std::cin >> std::ws, text);
std::cout << text << '\n';
return 0;
}
std::ws ignores leading whitespace before getline starts reading. It is an input manipulator, similar to how std::setprecision() is an output manipulator.
We can get the length of a string using stringVar.length() or stringVar.size(). It returns an unsigned size type and does not include the terminating null character used by c_str().
Normal double-quoted text is a string literal, which has C-style character-array behavior. We can make a std::string literal using the s suffix:
#include <string>
using namespace std::string_literals;
std::string name{ "Rishi"s };
Passing a std::string by value can copy its data, so for read-only function parameters we usually use std::string_view or const std::string&, depending on the situation.
4. Operators and bit manipulation
Side effects and the conditional operator
In C++, a side effect is any action that modifies the state of the execution environment or causes observable changes beyond computing and returning a value. Common examples include modifying a variable, writing to a file, taking user input, or printing to the console.
For example, in x = 5;, calculating 5 is the expression's value computation, while storing 5 in x is the side effect.
Understanding side effects is important when dealing with evaluation order. Avoid modifying and reading the same variable in one complicated expression.
a ? x : y is called the conditional operator, or arithmetic if. If a is true, the expression evaluates to x; otherwise, it evaluates to y.
Bit operations
#include <bitset>
#include <iostream>
int main()
{
std::bitset<8> bits{ 0b0000'0101 }; // 0000 0101
bits.set(3); // 0000 1101
bits.flip(4); // 0001 1101
bits.reset(4); // 0000 1101
std::cout << "All the bits: " << bits << '\n';
std::cout << "Bit 3 has value: " << bits.test(3) << '\n';
std::cout << "Bit 4 has value: " << bits.test(4) << '\n';
return 0;
}
5. Scope, duration, linkage, and storage classes
Compound statements, scope, and shadowing
A compound statement is a block surrounded by braces:
{
int x{};
}
Scope means where an identifier can be accessed in the source code.
There are three main types of scope in these notes:
- Global scope.
- Local scope.
- Block scope.
Variable shadowing means a higher-scope variable is hidden by another variable with the same name in a lower scope.
#include <iostream>
int g_foo{ 5 };
int main()
{
std::cout << g_foo << '\n'; // prints 5
int g_foo{ 10 }; // shadows the global variable
std::cout << g_foo << '\n'; // prints 10
return 0;
}
This happens when we reinitialize or redeclare a variable in a smaller scope.
Duration and linkage
Linkage tells us whether declarations of an identifier refer to the same entity across scopes or files.
- Local variables have no linkage.
- Global variables and functions can have internal or external linkage.
- A global variable with internal linkage is also called an internal variable.
Storage duration means when the variable is created and destroyed:
- Automatic duration.
- Static duration.
- Dynamic duration, which comes from dynamic memory allocation.
Storage class specifiers are keywords used in variable and function declarations. They can affect storage duration and linkage.
Global variables
// Non-constant global variables
int g_x; // zero-initialized by default
int g_value{}; // explicitly value-initialized
int g_count{ 1 }; // explicitly initialized
// Const global variables
// const int g_y; // error: const variables must be initialized
const int g_y{ 2 };
// Constexpr global variables
// constexpr int g_z; // error: constexpr variables must be initialized
constexpr int g_z{ 3 };
By default, non-constant global variables have external linkage. To make one internal, we can use static:
static int g_foo{ 5 };
Global const and constexpr variables have internal linkage by default. Functions have external linkage by default.
// add.cpp
[[maybe_unused]] static int add(int x, int y)
{
return x + y;
}
// main.cpp
#include <iostream>
int add(int x, int y); // a forward declaration does not make the static function accessible
int main()
{
std::cout << add(3, 4) << '\n';
return 0;
}
This program will not link because add has internal linkage in add.cpp.
We can use extern for an external variable declaration, but we must be careful: an initialized non-const global with extern is a definition, not just a declaration.
Static local variables
When we create a local variable, it has automatic duration by default. If we use static with a local variable, its duration becomes static. It is created once, keeps its value after leaving its block, and is destroyed at program end.
#include <iostream>
void incrementAndPrint()
{
static int s_value{ 1 }; // initializer runs only once
++s_value;
std::cout << s_value << '\n';
} // s_value becomes inaccessible here but is not destroyed here
int main()
{
incrementAndPrint();
incrementAndPrint();
incrementAndPrint();
return 0;
}
Inline functions and variables
Calling a function normally has some call overhead. An inline function gives the compiler permission to replace a call with the function body when it is useful. It does not force the compiler to do so.
inline is also useful for allowing identical function or variable definitions in more than one translation unit.
Although we can make a constexpr variable externally linked, it must be initialized at its definition, so it cannot be separately forward declared as constexpr.
Old storage-class notes
The old types written in my copy are static, extern, register, and auto.
autois now mainly used for type deduction in modern C++.registeris no longer useful in modern C++ and is deprecated.statichas several meanings depending on where it is used.externis used for external linkage declarations.
The picture below is a quick old-style storage-class reference. In modern C++, use the scope, duration, and linkage notes above as the main rule.

6. Control flow
Control statements
Control statements decide which statements execute. A basic switch looks like this:
switch (x)
{
case 1:
return 1;
case 2:
return 2;
default:
return 0;
}
std::exit() stops the program immediately. We can give a function a name and call it when we need it.
7. Type conversion
Type conversion
There are implicit type conversions and explicit type conversions. When the compiler converts the type for us, it is implicit. When we ask for the conversion, it is explicit.
The static_cast operator is used for a normal explicit type cast:
static_cast<int>(5.5); // gives 5
Syntax:
static_cast<new_type>(expression)
The older C-style cast also exists:
int number{ (int)5.5 };
But in C++, we prefer static_cast<int>(5.5) because it clearly shows the intended conversion.
8. Function overloading and templates
Function overloading
Sometimes we might want to create two functions with the same name. For that, we need function overloading.
We can differentiate overloaded functions through the number of parameters and the types of parameters. Return type is not used to differentiate functions during overload resolution.
int add(int x, int y)
{
return x + y;
}
int add(int x, int y, int z)
{
return x + y + z;
}
The function signature is the parts of the function header used to differentiate functions. In C++, this includes the function name, number of parameters, parameter types, and function-level qualifiers. It does not include the return type.
The process of matching a function call to one overloaded function is called overload resolution. A simplified order is:
- Exact match.
- Numeric promotion.
- Numeric conversion.
- User-defined conversion.
- Give up with an error.
Resolving ambiguous matches
Explicitly cast ambiguous argument(s) to match the type of the function we want to call.
Sometimes we want to delete overloads:
#include <iostream>
void printInt(int x)
{
std::cout << x << '\n';
}
void printInt(char) = delete; // calls halt compilation
void printInt(bool) = delete; // calls halt compilation
int main()
{
printInt(5); // okay
// printInt('a'); // error
// printInt(true); // error
return 0;
}
If there are many non-matching types, we can use a deleted template:
template <typename T>
void printInt(T) = delete;
We can also define default parameters in a function. The important rule is that after one parameter has a default argument, every parameter to its right must also have a default argument.
Function templates
When doing function overloading, there are times when we need almost identical functions that only differ by type. In that case, we create a template. The compiler generates overloads from it, so we maintain only one template.
The initial function template is the primary template. Functions generated from it are instantiated functions.
template <typename T>
T add(T x, T y)
{
return x + y;
}
When creating a primary function template, we use typename and a type called a template type parameter or template type.
To create a template function, add the template keyword:
template <typename T>
T add(T x, T y)
{
return x + y;
}
The compiler can deduce template arguments from a call:
#include <iostream>
int main()
{
std::cout << add(1, 2) << '\n'; // T is deduced as int
}
We can also specify the type:
std::cout << add<int>(1, 2) << '\n';

For a simple template such as max:
template <typename T> // T is a placeholder
T max(T x, T y)
{
return (x < y) ? y : x;
}
After using max(1, 2), the compiler can generate an int version:
// Conceptually, the generated function is like this:
int max(int x, int y)
{
return (x < y) ? y : x;
}
This generated function is an instantiation. Implicit instantiation means it is instantiated when a function call needs it.
We can have multiple template types:
template <typename R, typename T1, typename T2>
R convert(T1 a, T2 b)
{
return static_cast<R>(a + b); // calculation using a and b
}
// Called explicitly like: convert<int>(5.5f, 2.1f);
template <typename T1, typename T2>
auto add(T1 a, T2 b)
{
return a + b; // return type is automatically deduced
}
Template argument deduction means that most of the time we do not need to write a template argument. For example, add(arg1, arg2) can let the function template deduce the parameter types itself.
We can mix normal values and template variables if the template parameter can be deduced. We can also have multiple template variables.
Overloading function templates
#include <iostream>
template <typename T>
auto add(T x, T y)
{
return x + y;
}
template <typename T, typename U>
auto add(T x, U y)
{
return x + y;
}
template <typename T, typename U, typename V>
auto add(T x, U y, V z)
{
return x + y + z;
}
int main()
{
std::cout << add(1.2, 3.4) << '\n';
std::cout << add(5.6, 7) << '\n';
std::cout << add(8, 9, 10) << '\n';
return 0;
}
9. References, pointers, passing arguments, dynamic memory, and arrays
Pointers
A pointer is declared using . A pointer stores an address. When we access the data stored at the address in a pointer using , we call it dereferencing.
The * token can also be multiplication, so its meaning depends on the context. A token can have a defined meaning depending on whether it is a keyword, identifier, constant, literal, operator, or special symbol.
int value{ 5 };
int* ptr{ &value };
*ptr = 10; // dereferencing: value becomes 10
Passing by pointer
Passing by pointer means we pass an address from the calling side. It can change the original variable.
void work(int* a)
{
*a = 11; // dereferencing
}
int main()
{
int value{ 8 };
work(&value); // passing address
}
Passing into a function
There are three main ways to pass a value into a function:
- By value.
- By reference.
- By pointer.
Pass by value
Pass by value is the default in C++. The data is copied into the parameter. Changes inside the function do not go outside the function.
void work(int a)
{
a = 10;
}
Pass by reference
With pass by reference, changes inside the function are reflected outside. No copy is made.
void work(int& a)
{
a = 10;
}
For a read-only value that should not be copied, we often pass by const reference:
class Person;
void print(const Person& person);
Pass by pointer
With pass by pointer, the pointer is an input and the function dereferences it to change the original object.
void work(int* a)
{
*a = 11;
}
int main()
{
int value{ 8 };
work(&value);
}
For a class object, pass by pointer, reference, and value each have different syntax, but they all let us pass the object to a function or method of another class.
Dynamic memory allocation (DMA)
Dynamic memory allocation is the process of allocating memory for a variable during runtime instead of normal automatic allocation. DMA is usually done using the new and delete operators.
Normal local variables are generally allocated with automatic storage duration and are destroyed when they go out of scope. Dynamically allocated objects are allocated on the free store, often called the heap.
int* ptr{ new int }; // allocates an int and returns its address
int* ptr1{ new int{ 2 } };
int* ptr2{ new int(2) };
After we are done using the memory, we can delete it:
delete ptr;
ptr = nullptr; // set pointer to null after delete
A pointer that points to deallocated memory is a dangling pointer.
A memory leak happens when the program loses the memory address and there is no way to free the dynamically allocated memory. The memory can remain allocated until the program closes.
If there is no available space, new normally throws std::bad_alloc.
In modern C++, raw new and delete are usually avoided when a container or smart pointer can manage the memory safely. The raw examples are here because they are part of my notes.
Dynamically allocating arrays
We can dynamically allocate arrays like a normal scalar, but use new[] and delete[].
int* arr{ new int[5] };
delete[] arr;
int* numbers{ new int[5]{ 1, 2, 3, 4, 5 } };
delete[] numbers;
The array variable is a pointer to the first element, so normal indexing works:
arr[0] = 5;
arr[4] = 3;
There is no built-in way to resize a raw dynamic array after it is initialized. This is one reason we use std::vector instead when we need resizing.
10. Structs, classes, constructors, access levels, and friends
Structs, classes, and objects
A class is a user-defined type.
Student mike; // Student is the data type, mike is an instance of Student
An object is an instance of a type.
Classes can do information hiding with:
publicprivateprotected
They also give us the ability to create instances of a class.
Access specifiers
Each element of a class has an access level that determines who can access the member. C++ has three levels:
publicprivateprotected
Members of a struct are public by default. Members of a class are private by default.
Access functions are manually implemented methods that let us access or manipulate private data in a controlled way.

Classes are not always aggregates. An aggregate is a type that allows direct member initialization using brace-list initialization.
For a class to be an aggregate, the important rules from my notes are:
- All data members are public.
- No user-declared constructors.
- No virtual base classes or virtual functions.
- No private or protected base classes.
Constructors and destructors
Constructors and destructors are special functions in a class.
A constructor is automatically called when an object is made. A destructor is automatically called when an object is destroyed, either by delete or when the object goes out of scope.
A constructor:
- has the same name as the class;
- has no return type;
- is automatically called;
- can be overloaded;
- is used to initialize an object.
Types of constructor:
- Default constructor.
- Parameterized constructor.
- Copy constructor.
A default constructor takes no arguments and is usually used to initialize data members with default values.
class Person
{
int height{};
public:
Person() // default constructor
{
}
};
A parameterized constructor accepts arguments from outside and uses them to initialize data members. It needs to be public when objects should be created from outside the class.
A copy constructor is a special constructor that creates a new object by copying the data members of an existing object of the same class.
Member functions and member variables
Functions included in a class are called member functions. Functions that are not members of a class are called free functions.
Member functions can be invoked and created in any order, but data members are initialized in declaration order.
Member functions can also be overloaded, but the overloads should be in that class.
#include <iostream>
struct Date
{
int year{};
int month{};
int day{};
void print()
{
std::cout << year << '/' << month << '/' << day;
}
};
int main()
{
Date today{ 2020, 10, 14 };
today.day = 16; // member variables use the member selection operator .
today.print(); // member functions also use .
return 0;
}
Memory for a class is allocated when an object is created, not when the class itself is declared. Each normal data member has separate memory in each object.
Static data members are shared across all objects of a class. Normal data members get their own copy per object, but a static data member has one shared copy.
Const class objects and const member functions
We can create a const object of a class:
const Person person{};
If an object is const, we cannot modify its data members or call a non-const member function on it.
#include <iostream>
struct Date
{
int year{};
int month{};
int day{};
void print() const
{
std::cout << year << '/' << month << '/' << day;
}
};
void doSomething(const Date& date)
{
date.print(); // works because print is const
}
We can have a const and non-const overload:
#include <iostream>
struct Something
{
void print()
{
std::cout << "non-const\n";
}
void print() const
{
std::cout << "const\n";
}
};
Defining member functions outside the class
Member functions can be declared inside a class and defined outside it. The declaration needs to be inside the class, and the definition uses the scope resolution operator.
class Person
{
public:
void will();
};
void Person::will()
{
}
The scope resolution operator should not be used when calling the member function through an object. We call it with the object:
Person person{};
person.will();
Friend functions and friend classes
Friend functions can access public, private, and protected data members of a class.
Friend functions are declared inside the class using the friend keyword. They can be member functions or non-member functions, and they are defined with the normal rules for the function.
#include <iostream>
class Accumulator
{
private:
int m_data{ 5 };
friend void print(const Accumulator& accumulator);
};
void print(const Accumulator& accumulator)
{
std::cout << accumulator.m_data << '\n';
}
In this case, print is not a member function, but it can access the private data of Accumulator.
Friend classes and friend functions are not inherited.
A friend class can also be forward declared, declared as a friend inside another class, and then defined later.
A member function as a friend
#include <iostream>
class Storage;
class Display
{
private:
bool m_displayIntFirst{};
public:
Display(bool displayIntFirst)
: m_displayIntFirst{ displayIntFirst }
{
}
void displayStorage(const Storage& storage);
};
class Storage
{
private:
int m_nValue{};
double m_dValue{};
public:
Storage(int nValue, double dValue)
: m_nValue{ nValue }, m_dValue{ dValue }
{
}
friend void Display::displayStorage(const Storage& storage);
};
void Display::displayStorage(const Storage& storage)
{
if (m_displayIntFirst)
std::cout << storage.m_nValue << ' ' << storage.m_dValue << '\n';
else
std::cout << storage.m_dValue << ' ' << storage.m_nValue << '\n';
}
int main()
{
Storage storage{ 5, 6.7 };
Display display{ false };
display.displayStorage(storage);
return 0;
}