Variables

1. Variables

Variables are scope stored values with a name and a type, in the memory. The type of the variable, just as in the table of fundamental types (Fundamental Types - Figure 1.1), determine the size, range of values, layout of the variable in memory and the set of applicable operations on the variable. Generally the terms variable and object are used synonymously.

1.1. Defining Variables

  • In it’s simplest form a variable definition is as:
    type name
    (int someInt)

  • It is possible to initialize the variable to an initial value like.
    type name = valueopt
    (int someInt = 5).

  • It is also possible to define multiple variables of the same type on a single line. All of the variables will have the same type and any of them can be initialized with a value but it’s again, optional.
    type name , name , name = valueopt , name = valueopt
    (double someDouble, anotherDouble = 2.1, yetAnotherD = 5.67)

1.2. Defining vs Declaring

Variable Declaration introduces a variable with a specific type and a name to a scope.
Variable Definition is a declaration but it also allocates storage for the variable in the memory. It may also provide an initial value.

The reason for differentiating between definition and declaration is to support separate compilation. It’s the C++ way to make the variables known to the code in other files.

Separate compilation, simply put is the compilation of a program that consists of multiple source files (translation units).

To declare a variable that is not also defined the keyword extern is used. Variables declared with the extern can be initialized explicitly unless they are in a function. But doing so renders the keyword extern ineffective. Most compilers will issue a warning stating this.
(extern int num; // Correct, num is only declared)
(extern int num = 5; // Warning, declared extern and initialized, extern loses it's meaning)
(void main() { extern int num = 5; } // Error, num has both extern and initializer)

Variables can be declared multiple times but defined only once. To use a variable in multiple files it must be defined only once but should be declared in all the files we intend to use it in. If we define a variable in a new file scope, the newly defined variable will hide the declared variable in the other file we want to use.

1.3. Identifiers

There are some rules and limitations for choosing identifiers. (or more simply, name of a user defined entity like variables, functions, classes etc)

  • Identifiers are case-sensitive
  • An identifier must start with a letter. _ is considered a letter too. (int _Test, test, TEST; All distinct and valid names)
  • An identifier can’t contain operators. (int test-1, test=, test++, b|c; would not compile) One exception to this rule is the special operators like sizeof or delete. (int sizeofTest, deleteMe; would work)
  • There is no limit on how long an identifier can be. But good practice is to keep them as short as can be without sacrificing comprehension.
  • An identifier can’t be any of the reserved keywords in the Figure 1.1 These keywords are used by the language itself.
alignas co_await module struct
alignof co_return mutable switch
and co_yield namespace synchronized
and_eq decltype new template
asm default noexcept this
atomic_cancel delete not thread_local
atomic_commit do not_eq throw
atomic_noexcept double nullptr transaction_safe
auto dynamic_cast operator transaction_safe_dynamic
bitand else or true
bitor enum or_eq try
bool explicit private typedef
break export protected typeid
case extern public typename
catch false register union
char final relfexpr unsigned
char16_t float reinterpret_cast using
char32_t for requires virtual
class friend return void
compl goto short volatile
concept if signed wchar_t
const import sizeof while
constexpr inline static xor
const_cast int static_assert xor_eq
continue long static_cast

Figure 1.1. Table of reserved C++ keywords

1.4. Initializers

If a variable gets a value when it’s created, it’s initialized. A variable can be explicitly initialized using a literal, a previously initialized variable of compatible type or an expression.

int a; // Implicitly initialized with the value of 0, default initialization
int b = 5; // Explicitly initialized
int c = b; // Explicitly initialized using a previously initialized variable
int d = 5, e = d; // This also works
int f = (b + c) * d; // Initialized with an expression

int mul(int a, int b) {
	return a * b;
}

int g = mul(4, 5); // Initialized by the return value of a function

// One can also initialize in the form below
int h(5); // Same as int h = 5

Assignment and initialization are different operations in C++ even though they use the same = symbol.

1.4.1. List Initialization

List initialization uses braces in the following form and it has some properties important properties that separates it from the usual initialization.

int i{5};
double d = {3.14};
  • When built-in type variables are initialized with List Initialization, the compiler will not let us use values that would not fit the type of the variable contrary to the truncation of normal initialization. Such a case is called a Narrowing Conversion.
    The C++ iso standard draft lists the cases of narrowing conversion as below:
  1. A narrowing conversion is an implicit conversion
  2. from a floating-point type to an integer type, or
  3. from long double to double or float, or from double to float, except where the source is a constant expression and the actual value after conversion is within the range of values that can be represented (even if it cannot be represented exactly), or
  4. from an integer type or unscoped enumeration type to a floating-point type, except where the source is a constant expression and the actual value after conversion will fit into the target type and will produce the original value when converted back to the original type, or
  5. from an integer type or unscoped enumeration type to an integer type that cannot represent all the values of the original type, except where the source is a constant expression whose value after integral promotions will fit into the target type.

Source: dcl.init.list/ยง7 (Iso C++ Standard Draft) (Emphasis added by me)

long double ld = 3.1415926536;

int a{ld};	// Error, narrowing conversion not allowed
int b = {ld};	// Error, narrowing conversion not allowed

int c = ld;	// OK, ld will be truncated to the value "3"
int e(ld);	// OK, same as above

A constant expression is an expression with a value that is determined during compilation. The value can be evaluated at runtime but cannot be changed. For more info check out 1.7. Constant Variables

1.4.2. Default Initialization

Default initialization is when the compiler implicitly initializes a defined but uninitialized variable to its “default” value.

  • Variables with built-in types defined outside a function/class/struct body are default initialized to value 0.
  • Variables with built-in types defined inside a function/struct/class body are not default initialized. Thus the value of such a variable is undefined.
  • Default initialization of class types are determined by their constructors or the brace-or-equal-initializer’s in the class body. For example the standard library string class type std::string is default initialized to an empty string.
class AClass {
private:
	int j{10};	// brace-
	int i = 5;	// or-equal-initializer

	// The default values above will be implicitly used in any constructor for this class unless overriden by the constructor

public:
	// Default constructor without any form of initialization, the variable i j will get the values 5 and 10 respectively
	AClass() = default;

	// Constructor with an initializer list, the variables will be what val1 and val2 is, effectively overriding the default values assigned above
	AClass(int val1, int val2) : i{val1}, j{val2} {};
};

brace-or-equal-initializer is a term used in the C++ standard. It simply means both ways of initialization, both the braces form and the = form. int a{5}, b = 6;

1.5. Scope

Scope is a region of the program source code where a specific name in the program is accessible. The following list contains the scope types in C++.

  • Global scope
  • Class scope
  • Namespace scope
  • Block scope
  • Function Parameter scope
  • Function scope
  • Enumeration scope
  • Template Parameter scope

A block is an area of program source code delimited by braces {...}

/* Global Scope
 * Accessible from anywhere in the program
 * Unary Scope Resolution Operator can be used for more readability or for avoiding naming clashes
 */

int someInt = 5;

void PrintSomeInt() {
	int someInt = 10;
	std::cout << someInt << std::endl;	// Prints the local someInt with the value 10
	std::cout << ::someInt << std::endl;// Prints the global someInt with the value 5, also easier to understand we're accessing the global
}

/* Class Scope
 * Public members can be accessed inside the class and anywhere an instance of the class is accessible
 * Protected members can be accessed inside the class and also inside the derived classes
 * Private members can only be accessed inside the class
 */

class SomeClass {
public:
	int GetClassInt() {
		return classInt;
	}

protected:
	float protectedFloat {0.5f};

private:
	int classInt {0};
};

/* Namespace Scope
 * Accessible within the namespace and from anywhere that's `using` this namespace or via the Scope Resolution Operator
 */

namespace NamespaceScope {
	int namespaceInt;
}

/* Block Scope
 * Accessible within the block and the nested inner blocks within the block
 */

void BlockScopeExample() {
	{
        int outerScope = 5;
        
        {
            int innerScope = 10;
            outerScope = 50; // OK, a is declared in the outer scope
        }

        outerScope = 10; // OK, same scope
        innerScope = 20; // Error: 'innerScope' was not declared in this scope, it's lifetime is over when the program finished executing the inner scope
    }
	

	if(true) {
		int i = 5; // Also block scope
	}
}

/* Function Parameter Scope
 * Parameters only accessible within the function
 */

void SomeFunction(int functionParameterInt) {}

/* Function Scope
 * Variables local to the function, only accessible within the function
 */

void SomeOtherFunction() {
	int functionInt;
}

/* Scoped Enumeration Scope (C++11)
 * Accessible only with the enum type qualification
 */

enum class Colors {
    Blue,
    Red
};

Colors Red() {
    return Colors::Red;
}

/* Template Parameter Scope
 * Accessible in the template function parameters and body
 */

template <class templateParameter>
templateParameter Max(templateParameter a, templateParameter b) {
	return(a > b) ? a : b;
}

1.5.1.Nested Scopes

Scopes can contain other scopes. The contained/nested scope is called the inner scope while the containing is called the outer scope.

The outer scope names are accessible from all the inner scopes.
The inner scope names are not accessible from the outer scopes.
The outer scope names can be re-defined in the inner scope, this however hides the outer scope name and renders it inaccessable for the inner scopes. In other words, newly redefined name takes the place of the outer scope name for the scope it’s defined in and it’s nested scopes.

int a = 5;

int main() {
	a = 10; // OK, accessing the global scope
	int b = 5; // A variable defined for the function scope

	if(true) {
		a = 20; // OK, accessing the global scope
		
		int a = 50;	// OK, but hides the global 'a'
						// Also not a great idea, global 'a' is not accessible now and it's also frustrating
		
		a = 25;	// OK, but doesn't change the value of global/outer 'a', it's the local 'a' now
				

		b = 100; // OK, accessing the outer scope (function scope)

		int c = 20; // Local variable defined in the inner scope
	}

	b = 30; // Error: b is not defined in this scope (can't access the local variable from an outer scope)
}

Rest of this article will be added in a future update

1.5.2 Scope Resolution Operator

1.6. Compound Types

1.6.1. References

1.6.2. Pointers

1.7. Constant Variables

1.8. Type Aliases

1.9. Structure Types

1.10. Type Checking

C++ is a statically typed language. The types of all the variables must be known at compile time. The set of operations applied to a variable of a type are validated at compile time and if an invalid operation is detected the program won’t compile.