Lesson 02: Data Types and Variables
C/C++ offers the programmer a rich assortment of built-in data types. Programmer-defined data types can be created to fit virtually any need. Variables can be created for any valid data type. Also, it is possible to specify constants of C/C++'s built-in types. In this lesson, various features relating to data types, variables, and constants are discussed.
2.1 Data Types
The data types are used to specify the type of data, and they are also defined as the data storage format, in which a variable can store data to perform a specific operation. The basic data types in C are:
- Character: A character is one-byte data that store the ASCII code of the character.
- Integer: Integers are whole numbers that can have both zero, positive and negative values but no decimal values. For example, 0, -10, 12.
- Floating-point: Float-pointing data types allow variables to store decimal values (include integer and fractional parts)
- Boolean: A Boolean data type has one of two possible values; 0 or 1 (usually denoted true and false), intended to represent the two truth values of logic and Boolean algebra.
- Valueless: A void is an incomplete type. It means "nothing" or "no type".
The C language provides the four basic arithmetic type specifiers char, int, float and double, and the modifiers signed, unsigned, short, and long.
Character
[signed | unsigned] char <variable_name>;
Character data types are used to store a single character value enclosed in single quotes. Use the type specifier char to define a character data type. Variables of type char are 1 byte in length.
A char can be signed, unsigned, or unspecified. By default, signed char is assumed.
Objects declared as characters (char) are large enough to store any member of the basic ASCII character set.
Integer
[signed | unsigned] [long long | long | short] int <variable_name>;
Use the int type specifier to define an integer data type. Variables of type int can be signed (default) or unsigned.
long can define a long integer (32-bit).
long long is introduced by ISO C99 and defines a 64-bit integer.
Exact-width integer types
The C99 expands integers to exact-width integer types. This allows programmers to write more portable code by specifying the size for integer types. The exact-width integers are defined in stdint.h (for C/C++) and cstdint (for C++). To use these integer types, the header files must be included in the source file.
#include <stdint.h> // for C/C++
#include <cstdint> // for C++
The exact-width integer types are of the form intN_t and uintN_t. Both types must be represented by exactly N bits with no padding bits. intN_t must be encoded as a two's complement signed integer and uintN_t as an unsigned integer.
datatype | Description | Data range |
---|---|---|
int8_t | 8-bit signed integer | -128 to 127 |
int16_t | 16-bit signed integer | -32,768 to 32,767 |
int32_t | 32-bit signed integer | -2.147.483.648 to 2,147,483,647 |
int64_t | 64-bit signed integer | −9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 |
datatype | Description | Data range |
---|---|---|
uint8_t | 8-bit unsigned integer | 0 to 255 |
uint16_t | 16-bit unsigned integer | 0 to 65,535 |
uint32_t | 32-bit unsigned integer | 0 to 4,294,967,295 |
uint64_t | 64-bit unsigned integer | 0 to 18,446,744,073,709,551,615 |
Floating-point
float <variable_name>;
Use the float type specifier to define an identifier to be a floating-point data type.
[long] double <variable_name>;
Use the double type specifier to define an identifier to be a floating-point data type. The optional modifier long extends the accuracy of the floating-point value.
Boolean
bool <variable_name>;
Use bool and the literals false and true to perform Boolean logic tests.
The bool keyword represents a type that can take only the value false or true. The keywords false and true are Boolean literals with predefined values. false is numerically zero and true is numerically one. These Boolean literals are r-values; you cannot make an assignment to them.
You can convert an rvalue that is of type bool to an rvalue that is int type. The numerical conversion sets false to zero and true becomes one.
A zero value, null pointer value, or null member pointer value is converted to false. Any other value is converted to true.
In C, you have to include the stdbool.h file to the source file.
#include <stdbool.h>
Valueless
Valueless
The void type specifies that valueless is available. It is used in the following situations:
- Function returns as void
There are various functions in C which do not return any value or you can say they return void. A function with no return value has the return type as void. For example:
void exit(int exit_code); - Function arguments as void
There are various functions in C which do not accept any parameter. A function with no parameter can accept a void. For example:
int rand(void); - Pointers to void
A pointer of type void * represents the address of an object, but not its type. For example, a memory allocation function
void *malloc(size_t size);
returns a pointer to void which can be casted to any data type.
You cannot create variables of void type.
void result; // illegal
But you can declare a point as void, meaning that it can point to any data type;
void *ptr; // legal
Compound Types
Compound Types
A compound type is a type that is defined in terms of another type. There are two compound types in C/C++ — pointers, and references.
Pointers (C/C++)
Pointers (C/C++)
A pointer is a variable whose value is the address of another variable. Like any variable or constant, you must declare a pointer before you can work with it. A variable can be declared as a pointer by putting * in the declaration. We will discuss this later in EE2440-Lesson 03: Minterms and Maxterms.
int x = 5; // normal integer int *ip = &x; // pointer to integer x double *dp; // pointer to a double float* fp; // pointer to a float char *pch; // pointer to a character
References (C++)
References (C++ only)
A reference variable is an alias, that is, another name for an already existing variable. A reference type "refers to" another type. A variable can be declared as a reference by putting & in the declaration. Once a reference is initialized with a variable, either the variable name or the reference name may be used to refer to the variable. We will discuss this later in C++ EE2049-Lab 10: Mesh and Nodal Analysis.
int x = 5; // normal integer int &y = x; // y is a reference to x int& z = y; // z is also reference to x
Constants
Constants
Constants are the fixed values that never change during the execution of a program. Following are the various types of constants:
Integer Constants
Integer constants
An integer constant is nothing but a value consisting of digits or numbers. These values never change during the execution of a program. Integer constants can be decimal, octal, hexadecimal, and binary.
Decimal constant contains digits from 0 ~ 9
253, 3860
Above are the valid decimal constants.
Octal constant contains digits from 0 ~ 7, and these types of constants are always preceded by 0.
016, 075
Above are the valid octal constants.
Hexadecimal constant contains digits from 0 ~ 9 as well as characters from A ~ F. Hexadecimal constants are always preceded by 0x or 0X.
0x3F, 0XBCD, 0xff
Above are valid hexadecimal constants.
Binary constant contains digits from 0 ~ 1. Binary constants are always preceded by 0b or 0B.
0b1101, 0b11110000
Above are valid binary constants.
Note: Not all compilers support binary constants.
Character Constants
Character Constants
A character constant contains only a single character enclosed within a single quote (''). We can also represent character constant by providing ASCII value of it.
'A', 'w'
Above are the examples of valid character constants.
Backslash Character Constants
Enclosing character constants in single quotes works for most printing characters, but a few, such as the carriage return, are impossible to enter into your program's source code from the keyboard. For this reason, C/C++ recognizes several backslash character constants, also called escape sequences. These constants are listed below:
Code | Meaning | ASCII Representation | ASCII Value (decimal) |
---|---|---|---|
\0 | Null space | NUL | 0 |
\a | Alert | BEL | 7 |
\b | Backspace | BS | 8 |
\f | Form feed | FF | 12 |
\n | Newline | NL (LF) | 10 |
\r | Carriage return | CR | 13 |
\t | Horizontal Tab | HT | 9 |
\v | Vertical Tab | VT | 11 |
\" | Double quote | " | 34 |
\' | Single quote | ' | 39 |
\? | Question mark | ? | 63 |
\\ | Backslash | \ | 92 |
\ooo | Octal constant | ||
\xhhh | Hexadecimal constant |
The backslash constants can be used anywhere a character can. For example, the following statement outputs a newline and a tab and then prints the string "This is a test"
printf("\n\tThis is a test"); // For C/C++, using standard I/O
cout << "\n\t" << "This is a test"; // For C++, using iostream
String Constants
String Constants
A string constant contains a sequence of characters enclosed within double quotes ("").
"Hello", "EE2450 Embedded Programming-I"
Above are the examples of valid string constants.
Float-point Constants
Float-point Constants
A floating-point constant contains a decimal point and a fractional value. The floating-point constants are also called as real constants. A floating-point constant can also be written with an exponent (e).
120.0, 3.141596, 1.4e2
These are the valid real constants.
For example, to declare a value that does not change like the classic circle constant PI, there are two ways to declare this constant:
- By using the const keyword in a variable declaration which will reserve a storage memory
const double PI = 3.14;
- By using the #define preprocessor directive which doesn't use memory for storage and without putting a semicolon character at the end of that statement
#define PI 3.14
#include <stdint.h> #include <stdbool.h> char ch = 'A'; // declare a character variable with initial character constant 'A' int items; // declare an integer variable float sum = 0.0; // declare a float variable with initial constant value 'zero' double average; // declare a double variable bool direction = false; // declare a boolean variable with initial value 'false' uint8_t msgType; // declare a 8-bit unsigned integer variable int16_t data1, data2; // declare two 16-bit signed integer variables char *pch; // declare a character pointer
Summary
- A constant is a value that doesn't change throughout the execution of a program.
- A variable is an identifier, which is used to store a value.
- There are five commonly used data types such as int, float, char, bool, and void.
- Each data type differs in size and range from one others.
2.2 Variables
Identifiers
Identifiers
Variable, function, and user-defined type names are all examples of identifiers. In C/C++, identifiers follow certain rules:
- Uses sequences of letters (a ~ z, A ~ Z), digits (0 ~ 9), and underscores (_) from one to several characters in length.
- A name can not begin with a digit.
- Identifiers may be of any length. The significant characters depend on the version of C and the compiler.
- No spaces are allowed in the name.
- It cannot be a C/C++ reserved word (keyword), and reserved words for the compiler.
- C/C++ is a "case sensitive" language, meaning that "AGE" and "age" are different identifiers.
Invalid identifiers
The following identifiers are not valid for the stated reasons
snum$ | $ is not a valid character. |
total price | Embedded spaces are not permitted. |
3D | Identifiers cannot start with a number. |
int | int is a reserved word. |
Last-Chance | - is not a valid character. |
#values | # is not a valid character. |
%done | % is not a valid character. |
lucky*** | * is not a valid character. |
Variables
Variables
A variable is an identifier that is pointed to a memory location to store value(s). Different types of variables require different amounts of memory and have some specific set of operations that can be applied to them.
Naming Conventions
Naming Conventions
C/C++ programmers generally agree on the following conventions for naming variables:
- Begin variable names with lowercase letters for the local variables.
Ex: sum; result; average; - Begin variable names with uppercase letters for the global variables.
Ex. Total; - Use all uppercase letters for constant variables or macro names, which are defined by the #define statement.
Ex. #define PI 3.14; const float TAX_RATE = 0.95; - Using meaningful identifiers
- Separate "words" within identifiers with underscores or mixed upper and lower case.
Ex: surface_area, surface_Area, surfaceArea.
Declaring Variables
A variable must be declared first before it is used somewhere inside the program. A typical variable declaration is of the form:
dataType variableName; // for signal variable
dataType variableName1, variableName2, variableName3; // for multiple variables
For example, to declare x to be a floating-point, y to be an integer, and ch to be a character, you would write
float x;
int y;
char ch;
You can declare more than one variable of a type by using a comma-separated list. For example, the following statement declares three integers.
int a, b, c;
Initializing Variables
A variable can be initialized by following its name with an equal sign and an initial value. For example, this declaration assigns count to an initial value of 100.
int count = 100;
An initializer can be any expression that is valid when the variable is declared. This includes other variables and function calls.
In C/C++, the global variables and static local variables must be initialized using only constant expressions.
Simple variable declarations
The following example shows some simple variable declarations with some descriptions for each. The example also shows how the compiler uses type information to allow or disallow certain subsequent operations on the variable.
int result = 0; // Declare and initialize an integer.
double coefficient = 10.8; // Declare and initialize a floating point value.
auto name = "Lady G."; // (C++) Declare a variable and let compiler deduce the type.
auto address; // (C++) error. The compiler cannot deduce a type without an initializing value.
age = 12; // error. Variable declaration must specify a type or use auto!
result = "Kenny G."; // error. Can't assign text to an int.
string result = "zero"; // (C++) error. Can't redefine a variable with new type.
int maxValue; // Not recommended! maxValue contains garbage bits until it is initialized.
Exercise
- Which, if any, of the following names are invalid?
- int double = 3.14;
- int _;
- int catch-22;
- int 1_or_2 = 1;
- double Double = 3.14;
- Explain the following definitions. For those that are illegal, explain what's wrong and how to correct it.
- int i = { 3.14} ;
- double salary = wage = 9999.99;
- int i = 3.14;
2.3 Scope of Variables
When you declare a program element such as a class, function, or variable, its name can only be "seen" and used in certain parts of your program. The context in which a name is visible is called its scope. For example, if you declare a variable x within a function, x is only visible within that function body. It has a local scope. You may have other variables by the same name in your program; as long as they are in different scopes, they do not violate the One Definition Rule and no error is raised.
For automatic non-static variables, the scope also determines when they are created and destroyed in program memory.
There are six kinds of scope in C/C++:
- Local scope: A name declared within a function or a lambda (C++ only), including the parameter names, have local scope. They are often referred to as "locals". They are only visible from their point of declaration to the end of the function or lambda body. Local scope is a kind of block scope.
- Global scope: A global name is one that is declared outside of any class, function, or namespace. However, in C/C++ even these names exist with an implicit global namespace. The scope of global names extends from the point of declaration to the end of the file in which they are declared. For global names, visibility is also governed by the rules of the linkage which determine whether the name is visible in other files in the program.
- Statement scope: Names declared in a for, if, while, or switch statement are visible until the end of the statement block.
- Function scope: A label has function scope, which means it is visible throughout a function body even before its point of declaration. Function scope makes it possible to write statements like
goto cleanup
before thecleanup
label is declared. - Namespace scope: (C++ Only) A name that is declared within a namespace, outside of any class or enum definition or function block, is visible from its point of declaration to the end of the namespace. A namespace may be defined in multiple blocks across different files.
- Class scope: (C++ Only) Names of class members have class scope, which extends throughout the class definition regardless of the point of declaration. Class member accessibility is further controlled by the public, private, and protected keywords. Public or protected members can be accessed only by using the member-selection operators (. or ->) or pointer-to-member operators (.* or ->*).
There are two types of variables based on the scope of variables in C/C++:
- Local Variables
- Global Variables
All variable names within the same scope must be unique. When a variable with a large scope has the same name as a variable with a small scope, the variable with a small scope will temporarily cover the variable with a large scope, called variable coverage.
Local Variables
Local Variables
Variables declared in functions or blocks are called local variables. They are also called auto variables and are only used inside the function or block in which they are declared.
- Local variables are declared inside the functions, blocks ({ }), or statements.
- The local variables exist until the block of the function is under execution. When the program exits the block, all the non-static local variables will be destroyed automatically.
- Local variables are stored in the stack segment of the memory, and the default values are unknown (random values). Therefore, you may need to set an initial value for the local variables.
- Local variable names begin with a lowercase letter.
Advantages of using local variables
- The use of local variables offers a guarantee that the values of variables will remain intact while the function is running.
- If several functions change a single global variable, the result may be unpredictable. But declaring it as a local variable solves this issue as each function will create its own instance of the local variable.
- You can give local variables the same name in different functions and blocks because they are only recognized by the function and block they are declared in.
- Local variables are deleted as soon as any function is over and release the memory space which it occupies.
Disadvantages of using local variables
- Common data required to pass repeatedly as data sharing is not possible between modules.
- They have a very limited scope.
Variables are declared inside the function
Following is the example using local variables:
#include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <stdbool.h> int main() { // local variables auto int a; // use 'auto' to define local variable int b = 20, c; // 'auto' can be ignored // actual initialization a = 10; c = a + b; printf("C = %d \n", c); return 0; }
In line 8, use auto modifier to define a local variable as having a local lifetime. This is the default for local variables and is rarely used.
Function Parameters
Although function parameters are not defined inside the function body, they act as local variables inside the function.
int max(int x, int y) // x and y enter scope here { // assign the greater of x or y to max int max; max = (x > y) ? x : y; // max enters scope here return max; } // x, y, and max leave scope, and they will be destroyed here
There are three local variables in the max() function: int x, int y, and int max.
Variables are placed inside the block
int i; for (i = 0; i < 5; ++i) { int n = 0; printf("%d ", ++n); // prints 1 1 1 1 1 - the previous value is lost }
A variable n is declared in the for-loop block and the initial value is set to 0. After setting the initial values, the for-loop will be executed in the following steps:
- Line 1: Declared an integer variable i.
- Line 2: The for-loop sets initial value to i, check the range, and then starts the loop.
- Line 3: The program creates variable n and assigns it to 0.
- Line 4: Print the value on the screen by adding one to n.
- Line 5: Loop ends: the local variable n will be destroyed. Jump to line 2 to increase i by 1 and check the condition to decide whether the next term can be looped (to line 3) or if the loop is finished.
The results will always be 1 because variable n is destroyed once the for-loop ends; when the for-loop starts again, variable n will be created again. Once the for-loop ends, variable n will be destroyed again.
Variables are declared in for-loop statement
When a variable with a large scope has the same name as a variable with a small scope, the variable with a small scope will temporarily cover the variable with a large scope. This is called variable coverage. For example:
int i = 20; // first i enters scope and is create here for (int i = 0; i < 3; i++) { // second i enters for-loop scope and is create here printf("i = %d \n", i); } // second i goes out of scope and is destroyed here printf("i = %d \n", i);
The output:
i = 0
i = 1
i = 2
i = 20
There are two variables named i, but they are in different scopes. The variable i that declared on line 3 is only available and accessible in the for-loop. When the loop ends, the variable will also end (it will be destroyed) and become unavailable. The variable in the printf() function is the i that is declared on line 1.
Variables are declared in nested blocks
Variables can be defined inside nested blocks.
int main() // Outer block { int x = 5; // first x enters scope and is create here { // nested block (second layer block) int x = 10; // second x enters second scope and is create here printf("x: %d \n", x); { // nested block (third layer block) int x = 15; // third x enters second scope and is create here printf("x: %d \n", x); } // third x goes out of scope and is destroyed here } // second x goes out of scope and is destroyed here printf("x: %d \n", x); } // first x goes out of scope and is destroyed here
The output screen:
x: 10
x: 15
x: 5
In the above example, variable x is defined inside nested blocks. Its scope is limited from its point of definition to the end of the nested block, and its lifetime is the same. Because the scope of variable x is limited to the inner block in which it is defined, it is not accessible anywhere in the outer block.
Global Variables
Global Variables
Variables declared outside the function are called global variables. They can be used throughout the program.
- Global variables are usually declared at the top of the program outside all functions and blocks.
- Global variables are available throughout the lift-time of a program.
- Global variables can be accessed from any portion of the program.
- Global variable names begin with an uppercase letter.
The reasons to use global variables are:
- The primary use of global variables is in programs in which many functions must access the value of the same variable.
- Although the use of global variables can reduce the number of arguments that need to be passed to a function.
Advantages of using Global variables
- You can access the global variable from all the functions or modules in a program.
- You only require to declare global variable single time outside the modules.
- It is ideally used for storing "constants" as it helps you keep the consistency.
- A Global variable is useful when multiple functions are accessing the same data.
Disadvantages of using Global Variables
- Too many variables declared as global, then they remain in the memory till program execution is completed. This can cause of Out of Memory issue.
- Data can be modified by any function. Any statement written in the program can change the value of the global variable. This may give unpredictable results in multi-tasking environments.
- If global variables are discontinued due to code refactoring, you will need to change all the modules where they are called.
Global variable initialization
Non-constant global variables can be optionally initialized:
int W; // No explicit initializer (zero-initialized by default) int X = 0; // zero initialized int Y = 1; // initialized with value;
When a local variable is defined, it is not initialized by the system, you must initialize it yourself. Global variables are initialized to 0 automatically by the compiler.
Same variable name in local and global
A program can have the same name for local and global variables but the value of the local variables inside a function will take preference. For example:
#include <stdio.h> // global variable declaration int x = 20; int main () { // local variable declaration int x = 10; printf ("x = %d \n", x); return 0; }
When the above code is executed, it produces the following result:
x = 10
Access global variable when there is a local and global conflict
What if we want to access the global variables when there is a local variable with the same name?
In C: we will need to declare a block variable with the extern modifier.
#include <stdio.h> int x = 100; // global variable with initialization value int main() { int x = 10; // local variable // access global variable x { extern int x; // declare an external variable x (pointed to the global variable x) printf("global x = %d \n", x); } printf("local x = %d", x); }
The result is shown below:
global x = 100
local x = 10
In C++: We will need to use the scope resolution operator (::). The below program explains how to do this with the help of a scope resolution operator.
#include <stdio.h> int x = 100; // global variable with initialization value int main() { int x = 10; // local variable printf("global x = %d \n", ::x); // access global variable x (C++ only) printf("local x = %d", x); // access local variable x }
Output:
global x = 100
local x = 10
The local and global variables equally important while writing a program in any language. However, a large number of the global variable may occupy a huge memory. An undesirable change to global variables is become tough to identify. Therefore, it is advisable to avoid declaring unwanted global variables.
The following table shows the features of the different types of variables:
Type of Variable | Storage (in memory) | Initial Value | Scope (Visibility) | Lifetime |
---|---|---|---|---|
local variable auto variable |
stack | Garbage (unknown) |
Within block | End of block |
global variable extern variable |
Data segment | Zero | Global Multiple files |
Till the end of the program |
static local variable | Data segment | Zero | Within block | Till the end of the program |
register variable | CPU Register |
Garbage (Unknown) |
Within block | End of block |
Exercises
- What is the value of j in the following program?
int i = 64;
int main()
{
int i = 128;
int j = i;
} - Is the following program legal? If so, what values are printed?
int i = 50, sum = 0;
for (int i = 0; i != 10; i++)
sum += i;
printf("i: %d, sum: %d \n", i, sum);
2.4 Data Type Conversions
Converting one data type into another is known as type casting or, type conversion. It is basically converting one type of data type to another type to perform some operation. There are two types of type conversion:
- Implicit type conversion
- Explicit type conversion
Implicit Type Conversion
Implicit Type Conversion
It is also known as automatic type conversion.
- Done by the compiler on its own, without any external trigger from the user.
- Generally takes place when in an expression more than one data type is present. In such conditions, type conversion takes place to avoid loss of data.
- All the data types of the variables are upgraded to the data type of the variable with the largest data type:
bool ⇒ char ⇒ short int ⇒ int ⇒ unsigned int ⇒ long ⇒ unsigned long ⇒ long long ⇒ float ⇒ double ⇒ long double
- It is possible for implicit conversions to lose information, signs can be lost (when signed is implicitly converted to unsigned), and overflow can occur (when long long is implicitly converted to float).
In the following situations, the compiler will perform automatic type conversion:
- A particular expression contains more than one data type. The compiler will follow the above rules to convert the type.
- When a value of one type is assigned to a variable of a different type. The compiler will convert the right-hand side type into the target data type (left-hand side).
- When a value is passed as an argument to a function with a different type. Or when a type is returned from a function.
For example:
#include <stdio.h> int main() { int i = 3; float f = 3.3; char ch = 'A'; // ASCII 'A' is 65 in decimal int x; x = (i * 2.5) + f + ch; printf("int x = %d \n", x ); }

The result of x is:
int x = 75
Explicit Type Conversion
Explicit Type Conversion
This process is also called type casting and it is user-defined. Here the user can typecast the result to make it of a particular data type. In C++, it can be done in two ways:
Converting by Assignment (C/C++)
Converting by Assignment
This is done by explicitly defining the required type in front of the expression in parenthesis. This can be also considered as forceful casting.
(dataType) expression
Where dataType is the standard C language data type and it indicates the data type to which the final result is converted. An expression can be a constant, a variable, or an actual expression.
#include <stdio.h> int main() { int y; y = (int) 3.5 * 2; printf("int y = %d \n", y ); return 0; }

The result of x is:
int y = 6
Important Points about Type Conversions
- Converting float to an int will truncate the fraction part hence losing the meaning of the value.
- Converting double to float will round up the digits.
- Converting long int to int will cause the dropping of excess high order bits.
In all the above cases, when we convert the data types, the value will lose its meaning. Generally, the loss of meaning of the value is warned by the compiler.
Exercises
- All the variables are integer, find the final results in each variable.
- w = 3.2 + 2;
- x = (int) 3.8 * 2;
- y = 3 / 2 + 1.5;
- z = 3 / 2.0 + 1.5;
- Find the following results:
int x = 375;
char ch = x;
float y = (int) (x / 6.0);- printf("ch = %d \n", ch);
- printf("y = %f \n", y);
2.5 Data Type Modifiers
Each variable has a storage class that defines the features of that variable. It tells the compiler about where to store the variable, its initial value, scope ( visibility level ), and lifetime ( global or local ).
const
const
Objects of type const cannot be changed by your program during execution. Also, an object pointed to by a const pointer cannot be modified. The compiler is free to place variables of this type into read-only memory (ROM). A const variable will receive its value either from an explicit initialization or by some hardware-dependent means. For example,
const int a = 10;
will create an integer called a with a value of 10 that may not be modified by your program.
const with pointers
- A const to the LEFT of * indicates that the object pointed by the pointer is a const object.
- A const to the RIGHT of * indicates that the pointer is a const pointer.
The following table summarizes what types of pointers you can create with const:
Declaration Syntax | Description | Can the pointer be reassigned? (ptr = &b;) | Can the pointee be modified? (*ptr = 10;) |
---|---|---|---|
const Type * ptr; | Pointer-to-const | Yes | No |
Type const * ptr; | Pointer-to-const | Yes | No |
Type * const ptr; | const pointer | No | Yes |
const Type * const ptr; | const pointer-to const | No | No |
Type const * const ptr; | const pointer-to-const | No | No |
static
static
The static modifier is used to change the lifetime or scope of the variable.
Static with Local Variables
Static with Local Variables
Without the static keyword, the local variables are automatically allocated when the function is called and released when the function exits (thus the name "automatic variable"). The static modifier instructs the compiler to keep a local variable in existence during the lifetime of the program instead of creating and destroying it each time it comes into and goes out of scope. Therefore, making local variables static allows them to remain in memory the whole time when the program is running.
Static with Global Variables
Static with Global Variables
The static modifier also can be applied to global variables. When this is done, it causes that variable's scope to be restricted to the file in which it is declared. This means that it will have internal linkage. Internal linkage means that an identifier is known only within its own file.
The static modifier makes:
- Local variables have the same lifetime as the global variable.
- Global variables have the same scope as the local variable.
volatile
volatile
The modifier volatile tells the compiler not to optimize the variable, in which their value may be changed in ways not explicitly specified by the program. Consequently, the compiler can make no assumptions about the value of the variable. In particular, the optimizer must be careful to reload the variable every time it's used instead of holding a copy in a register.
A variable should be declared volatile whenever its value could change unexpectedly. And use volatile to avoid compiler to optimize the code away if it seems that no useful operations are contained within it. The variable should be declared volatile in the following situations:
- Memory-mapped peripheral registers
- Accessing global variables in an interrupt service routine (ISR) or signal handler.
- Sharing global variables between multiple tasks within a multi-threaded application
- Software delay loop variables in a highly optimized function
- Variables that should not be optimized out if not used
When a volatile variable is not declared as volatile, the compiler assumes that its value cannot be modified externally to the implementation. Therefore, the compiler might perform unwanted optimizations. This can manifest itself in a number of ways:
- The code might become stuck in a loop while polling hardware.
- A multi-threaded code might exhibit strange behavior.
- Optimization might result in the removal of code that implements deliberate timing delays.
Global Variable using with ISR
Consider the following two examples:
If your program has a loop that tests the state of a variable that is never modified in the loop, the compiler thinks that it can remove the test from the loop (since the value cannot change within the loop).
A nonvolatile version of buffer loop
void do_something(int param1); int global_variable; vois IRQ_Handle(void) // Interrupt Service Routine { global_variable = 1; } int main() { global_variable = 0; while(1) { // Stay in this loop if( global_variable ) do_something(); } }
In the above code shown, the compiler can see that global_variable is set to zero outside of the loop and not modified within the loop. It assumes that it can execute the test once outside of the loop, and not need to perform it at every iteration.
In general, this is a safe assumption and can result in significant performance improvement for your code. If, on the other hand, global_variable was modified by an interrupt service routine outside of the scope of normal program flow, this assumption is incorrect.
A volatile version of buffer loop
void do_something(int param1); volatile int global_variable; vois IRQ_Handle(void) // Interrupt Service Routine { global_variable = 1; } int main() { global_variable = 0; while(1) { // Stay in this loop if( global_variable ) do_something(); } }
The volatile qualifier tells the compiler that it cannot make any assumptions about the variable and that it must perform the test every iteration of the loop.
The global_variable is declared as volatile, it will force the compiler to load/store variable every time it is used. Therefore, when an interrupt occurred, the CPU will jump to the interrupt service routine and change the global_variable to 1. This changed will affect the code in the main() function when reading the global_variable value.
Software Delay Function
A delay function is used to suspend the execution of a program for a particular time. The simplest way of doing this is by creating loops and doing nothing. The following code is an example code:
// Software Delay void Delay() { volatile int i; volatile int i; for (i = 0; i < 10000; i++) for (j = 0; j < 500; j++); }
Without the volatile modifiers, the compiler optimization may remove the empty loops.
extern
extern
You can share a variable in multiple C source files by using external variables. It is declared using extern keyword. The extern simply tells the compiler that the variable is defined elsewhere and not within the same block where it is used. The extern modifier is most commonly used when there ate two or more files sharing the same global variables.
// File1.c int GlobalVar = 0; // Declaration and Definition of the variable void IncGlobalVar() { GlobalVar ++; }
// main.c #include <stdio.h> extern int GlobalVar; // Declaration of the variable void IncGlobalVar(); // Declaration of the function int main() { IncGlobalVar(); printf("GlobalVar = %d \r\n\n", GlobalVar); system("pause"); // for Windows Console Application return 0; }
The extern modifier only can be used with global variables. An external variable must be defined exactly once in one of the source files of the program. To access the external variables, the external variables must be declared either outside or inside the function that wants to access.
auto
auto
auto tells the compiler that the local variable it precedes us created upon entry into a block and destroyed upon exit from a block. Since all variables defined inside a function or a block are auto by default, the auto keyword is seldom (if ever) used.
register
register
Registers are faster than memory to access, so the variables which are most frequently used in a C program can be put in registers using register keyword. The register modifier could be used only on local variables, and it caused the compiler to attempt to keep that variable in a register of the CPU instead of placing it in memory (RAM).
- If you use & operator with a register variable, then the compiler may give an error or warning (depending upon the compiler you are using), because when we say a variable is a register, it may be stored in a register instead of memory and accessing the address of a register is invalid.
#include <stdio.h>
int main()
{
register int n = 20;
int *ptr;
ptr = &n;
printf("address of n : %p \n", ptr);
return 0;
}When you compile the code, the compiler will show an error message as below:line 6: ptr = &n; [Error] address of register variable 'n' requested
- The register modifier can be used with pointer variables. Obviously, a register can have an address of a memory location.
#include <stdio.h>
int main()
{
int n = 20;
register int *ptr;
ptr = &n;
printf("address of n : %p \n", ptr);
return 0;
}Build the code and execute it:
address of n : 0x7ffc1320465c
- The register modifier can not be used with a static modifier.
#include <stdio.h>
int main()
{
register static int n = 20;
n++;
printf("address of n : %d \n", n);
return 0;
}When you compile the code, the compiler will show an error message as below:line 4: register static int n = 20; [Error] multiple storage classes in declaration specifiers
- The register can only be used within a block (local), it can not be used in the global scope (outside the function).
#include <stdio.h>
register int n = 20;
int main()
{
n++;
printf("address of n : %d \n", n);
return 0;
}When you compile the code, the compiler will show an error message as below:line 3: register int n = 20; [Error] Storage class 'register' is not allowed here
- The register is only a request. The compiler is free to ignore it.
2.6 Clockwise/Spiral Rule for C/C++ Declarations
[This was posted to comp.lang.c by its author, David Anderson, on 1994-05-06.]
The Spiral Rule is a completely regular rule for deciphering C declarations. It can also be useful in creating them.
There are four simple steps to apply the rule:
- Start from the name of the variable and move clockwise to the next pointer or type.
- When encountering the following symbols, replace them with the corresponding English statements:
[n] or [] array (size n ) of always on the right side * pointer to … always on the left side (type1, type2) function passing type1 and type2 returning … always on the right side - Repeat until the expression ends.
- Always resolve anything in parenthesis first.
Example: Simple Declaration
What is the str?
- str is an array (size 40) of …
- str is an array (size 40) of pointers to …
- str is an array (size 40) of pointers to char data
Example: Pointer to a Function Declaration
What is the fp?
- fp is a pointer to …
- fp is a pointer to a function passing (an int and a pointer to float ) returning …
- fp is a pointer to a function passing (an int and a pointer to float ) returning a pointer to …
- fp is a pointer to a function passing (an int and a pointer to float ) returning a pointer to an int data
Example: Ultimate
What is the signal?
- signal is a function passing (an int and a …
- fp is a pointer to …
- fp is a pointer to a function passing (an int ) returning …
- fp is a pointer to a function passing (an int ) returning nothing (void)
- signal is a function passing (an int and a (fp) pointer to a function passing (an int ) returning nothing (void) ) returning a pointer to …
- signal is a function passing (an int and a (fp) pointer to a function passing (an int ) returning nothing (void) ) returning a pointer to a function passing (an int ) returning …
- signal is a function passing (an int and a (fp) pointer to a function passing (an int ) returning nothing (void) ) returning a pointer to a function passing (an int ) returning nothing (void)
Example: Functiuon and Arrays
Some declarations look much more complicated than they are due to array size and argument lists in prototype form. If you see "[3]", that's read as "array (size 3) of …". If you see "(char *, int)" that's read as "function passing (char *, int) and returning …". Here's a fun one:
int (*(*func)(char *,double))[9][20];
It is:
ptr is a pointer to a function passing (a pointer to char and a double ) returning a pointer to array (size 9) of array (size 20) of int data
The most important thing here is to recognize the following recursive structure of all declarations (const, volatile, static, extern, inline, struct, union, typedef are removed from the statement for simplicity but can be added back easily):
dataType [ derived_part1: * | & ] identifier [ derived_part2: [] | () ]
Where
dataType | is one of the following type void [signed | unsigned] char [signed | unsigned] [short | long | long logn] int float [long] double bool |
identifier | the name of variable, function, or class |
* | & | denotes a pointer or reference, and can be repeated |
[] | in derived_part2, denotes bracketed array dimensions and can be repeated |
() | in derived_part2, denotes parenthesized function parameters delimited with commas (,) |
[…] | elsewhere denotes an optional part |
(…) | elsewhere denotes parentheses |
Once you've got all 4 parts parsed,
[identifier] is [derived-part2 (containing/returning)] [derived-part2 (pointer/reference to)] dataType.
If there's recursion, you find your object (if there's any) at the bottom of the recursion stack, it'll be the inner-most one and you'll get the full declaration by going back up and collecting and combining derived parts at each level of recursion.
While parsing you may move [identifier] to after [derived-part2] (if any). This will give you a linearized, easy to understand, declaration.
char * (**(*foo[3][5]) (void)) [7][9];
foo is an array (size 3) of array (size 5) of a pointer to a function returning a pointer to pointer to an array (size 7) of array (size 9) of pointers to a char.
int *(* func()) (); | func is a function returning a pointer to function returning a pointer to an int data |
int * a[][5]; | a is an array of array (size 5) of pointers to int data |
int (*(*func)())[][]; | ptr is a pointer to a function returning a pointer to array of array of int data |
Example: Legal and Illegal Combinations
It is quite possible to make illegal declarations using this rule, so some knowledge of what's legal in C is necessary. For instance, if the above had been:
int *((*func)())[][];
it would have been "func is a pointer to a function returning an array of array of pointers to int". Since a function cannot return an array, but only a pointer to an array, that declaration is illegal.
Illegal combinations include:
[]() | cannot have an array of functions |
()() | cannot have a function that returns a function |
()[] | cannot have a function that returns an array |
In all the above cases, you would need a set of parentheses to bind a * symbol on the left between these () and [] right-side symbols in order for the declaration to be legal.
Here are some more examples:
Legal
int i; | i is an int |
int *p; | p is an int pointer (ptr to an int) |
int a[]; | a is an array of ints |
int f(); | f is a function returning an int |
int **pp; | pp is a pointer to an int pointer (ptr to a pointer to an int) |
int (*pa)[]; | pa is a pointer to an array of ints |
int (*pf)(); | pf is a pointer to a function returning an int |
int *ap[]; | ap is an array of int pointers (array of pointer to ints) |
int aa[][]; | aa is an array of arrays of ints |
int *fp(); | fp is a function returning an int pointer |
int ***ppp; | ppp is a pointer to a pointer to an int pointer |
int (**ppa)[]; | ppa is a pointer to a pointer to an array of ints |
int (**ppf)(); | ppf is a pointer to a pointer to a function returning an int |
int *(*pap)[]; | pap is a pointer to an array of int pointers |
int (*paa)[][]; | paa is a pointer to an array of arrays of ints |
int *(*pfp)(); | pfp is a pointer to a function returning an int pointer |
int **app[]; | app is an array of pointers to int pointers |
int (*apa[])[]; | apa is an array of pointers to arrays of ints |
int (*apf[])(); | apf is an array of pointers to functions returning an int |
int *aap[][]; | aap is an array of arrays of int pointers |
int aaa[][][]; | aaa is an array of arrays of arrays of int |
int **fpp(); | fpp is a function returning a pointer to an int pointer |
int (*fpa())[]; | fpa is a function returning a pointer to an array of ints |
int (*fpf())(); | fpf is a function returning a pointer to a function returning an int |
llegal
int af[](); | af is an array of functions returning an int |
int fa()[]; | fa is a function returning an array of ints |
int ff()(); | ff is a function returning a function returning an int |
int (*pfa)()[]; | pfa is a pointer to a function returning an array of ints |
int aaf[][](); | aaf is an array of arrays of functions returning an int |
int (*paf)[](); | paf is a pointer to a an array of functions returning an int |
int (*pff)()(); | pff is a pointer to a function returning a function returning an int |
int *afp[](); | afp is an array of functions returning int pointers |
int afa[]()[]; | afa is an array of functions returning an array of ints |
int aff[]()(); | aff is an array of functions returning functions returning an int |
int *fap()[]; | fpa is a function returning an array of int pointers |
int faa()[][]; | faa is a function returning an array of arrays of ints |
int faf()[](); | faf is a function returning an array of functions returning an int |
int *ffp()(); | ffp is a function returning a function returning an int pointer |
Example: Pointer and Constant
int * ptr; | ptr is a pointer to an int data |
int const * ptr; | ptr is a pointer to a constant int data |
int * const ptr; | ptr is a constant pointer to an int data |
const int * ptr; | ptr is a pointer to an int that is constant data |
const int * const ptr; | ptr is a constant pointer to an int that is constant data |
int const * const ptr; | ptr is a constant pointer to a constant int data |
int ** ptr; | ptr is a pointer to pointer to an int data |
int ** const ptr; | ptr is a constant pointer to a pointer to an int data |
int const ** ptr; | ptr is a pointer to a pointer to a constant int data |
int * const * ptr; | ptr is a pointer to a constant pointer to an int data |
int * const * const ptr; | ptr is a constant pointer to a constant pointer to an int data |
- const int * ptr; == int const * ptr;
- const int * const ptr; == int const * const ptr;
Example: class (C++ only)
const ClassA * ptr; | ptr is a pointer to a ClassA that is constant |
ClassA const * ptr; | ptr is a pointer to a constant ClassA |
ClassA * const ptr; | ptr is a constant pointer to a ClassA |
const ClassA * const ptr; | ptr is a constant pointer to a ClassA that is cinstant |
const ClassA * prt; == ClassA const * ptr; as in ptr is a pointer to a ClassA that is constant.
Questions
- What does the "One Definition Rule (ODR)" mean?
- Declare 3 Integer Type & 3 float type Variables.
INT FLOAT - Examine the following C assignment statements, find the final value for each variable.
- int a = (32 / 5) % 4 + 2;
- int b = (17 - 11) / 3 + 3;
- int c = 5 / 2 - 5 % 2;
- float d = 3.5 + 3 / (2 + 3);
- int e = 3 * 2 / 10.0;
- What does the following declaration mean?
- int id [] [];
- int (* id []) ();
- int (* id () ) ();