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 stores 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 (including 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, a 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:
- The 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 cast 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 08: Kirchhoff's Voltage Law and Kirchhoff's Current Law.
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 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 constants by providing the ASCII value of it.
'A', 'w'
Above are 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 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 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 the 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.
Valid identifiers
The following is a list of valid identifiers:
num45
AREA
area_under_the_curve
i
J5x7
x_yt3
_sysflag
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 a 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 the compiler deduce the type.
auto address; // (C++) error. The compiler cannot deduce a type without an initializing value.
age = 12; // error. The 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 a 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, has 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 ->*).
Why is scope important?
Scope is important for several reasons:
- It helps to prevent errors. If a variable is only visible in a certain part of the program, then it cannot be accidentally modified from another part of the program. This can help to prevent bugs.
- It makes code easier to understand. When you know the scope of a variable, you know exactly where it can be used and modified. This can make code easier to read and understand.
- It helps to improve performance. When a variable is only visible in a certain part of the program, the compiler can optimize the code for that variable. This can improve the performance of the program.
Scope rules
There are a few basic rules about scope in C/C++:
- The scope of a variable begins at the point of declaration and ends at the end of the block in which it is declared.
- A variable can only be accessed from inside its own scope and from any nested scopes.
- If there are two variables with the same name in different scopes, then the variable in the inner scope will be used if it is referenced from inside the inner scope.
Scope and lifetime
The scope of a variable is also related to its lifetime. The lifetime of a variable is the length of time that it exists in memory.
- A global variable has a lifetime that lasts for the entire program.
- A local variable has a lifetime that lasts for the duration of the block in which it is declared.
Scope is an important concept in C/C++. It helps to prevent errors, makes code easier to understand, and improves performance. By understanding the scope of a variable, you can write better code and avoid common mistakes.
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. The lifetime of Their lifetime begins when the function or block is entered and ends when it is exited. Local variables are typically used to store temporary values that are specific to the function or block in which they are declared. begins when the function or block is entered and ends when it is exited. Local variables are typically used to store temporary values that are specific to 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 that it occupies.
Disadvantages of using local variables
- Common data is 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 an 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 the initial value to i, checks 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 the 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
Global variables are declared outside of any function or block and have global scope, meaning they are accessible from anywhere in the program. Their lifetime extends throughout the entire execution of the program. Global variables are typically used to store shared data that needs to be accessed by multiple functions.
- 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 are only required to declare a global variable single time outside the modules.
- It is ideally used for storing "constants" as it helps you keep consistency.
- A Global variable is useful when multiple functions are accessing the same data.
Disadvantages of using Global Variables
- Too many variables are declared as global, and then they remain in the memory till program execution is completed. This can cause out-of-memory issues.
- 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 are equally important while writing a program in any language. However, a large number of the global variables may occupy a huge memory. An undesirable change to global variables becomes 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 |
Advantages of Local Variables
- Encapsulation: Local variables promote encapsulation by restricting their access to the specific function or block in which they are declared.
- Reduced Namespace Pollution: Local variables help avoid namespace pollution by limiting the number of variables with the same name.
- Improved Code Readability: Local variables enhance code readability by making it clear where a variable is defined and used.
Disadvantages of Local Variables
- Limited Scope: Local variables can be accessed only within their scope, which can sometimes restrict their usage.
- Memory Overhead: Local variables require memory allocation and deallocation each time the function or block is entered and exited.
Advantages of Global Variables
- Shared Data Access: Global variables facilitate data sharing across multiple functions, simplifying data access.
- Global State Management: Global variables can be used to maintain global state information that needs to be accessible throughout the program.
Disadvantages of Global Variables
- Namespace Pollution: Global variables can lead to namespace pollution, increasing the risk of naming conflicts.
- Reduced Encapsulation: Global variables break encapsulation by allowing unrestricted access from anywhere in the program.
- Code Maintainability Issues: Global variables can make code difficult to maintain and debug due to their potential for uncontrolled modifications.
General Guidelines for Variable Usage
- Use local variables whenever possible: Local variables promote encapsulation, reduce namespace pollution, and improve code readability.
- Use global variables sparingly: Use global variables only when essential for sharing data across multiple functions.
- Name global variables carefully: Choose meaningful names for global variables to avoid confusion and potential conflicts.
- Document global variable usage clearly: Add comments to explain the purpose and usage of global variables to enhance code maintainability.
By understanding the differences and considerations between local and global variables, programmers can make informed decisions about variable usage, leading to more maintainable, efficient, and bug-free code.
Exercises
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 also be 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
Conversion using Cast Operator (C++ only)
A Cast operator is a unary operator, which forces one data type to be converted into another data type.
C++ supports four types of casting:
- Static Cast
- Dynamic Cast
- Const Cast
- Reinterpret Cast
Static Cast
The static cast operator is the most commonly used casting operator in C++. It is used to perform explicit conversions between compatible data types. Static casts are checked by the compiler at compile time, ensuring that the conversion is valid and safe.
Example of Static Cast
int intValue = 10;
double doubleValue = static_cast<double>(intValue); // Explicit conversion from int to double
std::cout << doubleValue << std::endl; // Output: 10
In this example, the variable intValue is an integer with the value 10. The static cast operator is used to explicitly convert intValue to a double value. This conversion is valid and safe because double can represent integers without loss of precision.
Dynamic Cast
The dynamic cast operator is used to perform runtime type checks and casts. It is typically used for casting between polymorphic types, such as classes derived from a common base class. Dynamic casts are checked at runtime, and if the cast is not valid, a bad_cast exception is thrown.
Example of Dynamic Cast
class Shape {
public:
virtual double getArea() const = 0;
};
class Circle : public Shape {
public:
Circle(double radius) : radius(radius) {}
double getArea() const override {
return M_PI * radius * radius;
}
private:
double radius;
};
int main() {
Shape* shape = new Circle(5); // Polymorphic object of type Circle
Circle* circle = dynamic_cast<Circle*>(shape); // Dynamic cast to Circle
if (circle != nullptr) {
std::cout << "Circle area: " << circle->getArea() << std::endl;
} else {
std::cout << "Invalid dynamic cast to Circle" << std::endl;
}
return 0;
}
In this example, the Shape* pointer shape points to a Circle object. The dynamic cast operator is used to cast shape to a Circle* pointer circle. If the dynamic cast is successful, the value of circle will be a non-null pointer to the Circle object. Otherwise, a bad_cast exception will be thrown.
Const Cast
The const cast operator is used to remove or add the const qualifier to a variable. It does not change the underlying data type or value of the variable.
Example of Const Cast
const int constValue = 10; // Constant variable
int intValue = const_cast(constValue); // Remove const qualifier
intValue = 5; // Modify the value of intValue
std::cout << constValue << std::endl; // Output: 10 (Value remains unchanged)
std::cout << intValue << std::endl; // Output: 5 (Modified value)
In this example, the variable constValue is declared as a constant integer. The const cast operator is used to remove the const qualifier, allowing the variable intValue to be modified. However, the original constValue remains unchanged.
Reinterpret Cast
The reinterpret cast operator is used to perform low-level pointer conversions. It allows casting between incompatible pointer types, such as casting a pointer to a data type to a pointer to an integer type or vice versa. Reinterpret casts should be used with caution as they can lead to undefined behavior if not used correctly.
Example of Reinterpret Cast
int* data = reinterpret_cast<int*>(dataPointer); // Reinterpret cast to int pointer</int*>
In this example, the pointer dataPointer is cast to an integer pointer data. This cast is valid because the memory location pointed to by dataPointer is also accessible as an integer value. However, it is important to ensure that the memory location is indeed valid for integer access before using the reinterpret cast operator.
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 ). Data type modifiers are keywords in C and C++ that are used to modify the behavior of fundamental data types, such as int, char, and double. They allow programmers to control the size, range, and signedness of variables.
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
What is the static modifier?
The static modifier is a keyword in C and C++ that can be applied to variables, functions, and classes. It has different effects depending on the context in which it is used.
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.
Static Functions
When applied to a function, the static modifier restricts the function's visibility and lifetime. A static function has the following properties:
- Visibility: Static functions have internal linkage and are only visible within the file in which they are declared. They cannot be accessed from other files.
- Lifetime: Static functions exist throughout the program's execution, similar to static variables. They are not destroyed when their enclosing block or function exits.
Example of Static Functions:
static void printMessage() {
std::cout << "Static function message" << std::endl;
}
int main() {
printMessage(); // Calling static function from main()
return 0;
}
In this example, the printMessage() function is declared as static, making it a static function with internal linkage. It can only be called from within the file in which it is declared and remains accessible throughout the program's execution.
Static Members of Classes
When applied to a member of a class, the static modifier controls the member's accessibility and storage duration. A static class member has the following properties:
Accessibility: Static class members are shared among all objects of the class and can be accessed using the class name directly, not through individual objects.
Storage Duration: Static class members are initialized only once when the class is first loaded into memory and persist throughout the program's execution. They are not associated with individual objects.
Example of Static Class Members:
class Counter {
public:
static int count; // Static class member variable
void increment() {
count++;
}
};
int Counter::count = 0; // Initialization outside class declaration
int main() {
Counter::count++; // Accessing static class member using class name
std::cout << Counter::count << std::endl;
return 0;
}
In this example, the count member of the Counter class is declared as static, making it a static class member. It is shared among all objects of the class and can be accessed using the class name directly. It is initialized only once when the class is first loaded into memory and persists throughout the program's execution.
Benefits of Using Static Modifiers
Static modifiers provide several benefits for programmers:
- Data Persistence: Static variables retain their value throughout the program's execution, allowing them to store shared data that persists across function calls.
- Controlled Visibility: Static functions and class members are only visible within their respective files or classes, preventing unintended access and promoting encapsulation.
- Efficient Memory Usage: Static variables are initialized only once, reducing memory overhead compared to repeatedly initializing local variables.
Considerations when Using Static Modifiers
When using static modifiers, it is important to consider the following points:
- Overuse: Avoid overusing static modifiers, as they can make code less modular and difficult to maintain.
- Thread Safety: Static variables shared among multiple threads require synchronization mechanisms to prevent data races, where multiple threads attempt to access and modify the same variable simultaneously, leading to unpredictable or corrupted data. Mutexes, semaphores, and other synchronization primitives are essential for ensuring thread-safe access to static variables.
- Initial Values: Static variables must be initialized explicitly to establish their starting values. Default initialization based on data types may not always align with the expected behavior, leading to potential issues. Explicit initialization ensures consistent and predictable variable states.
- Global vs. Local Static Variables: Global static variables have external linkage, allowing access from anywhere in the program, while local static variables have internal linkage, limiting access to within the file where they are declared. Global static variables are useful for shared data across modules, while local static variables are preferred for maintaining state within a function or block.
- Static Functions and Namespace Pollution: Static functions have internal linkage and cannot be overloaded, affecting their visibility and redefinition capabilities. Excessive use of static functions outside of classes can contribute to namespace pollution, making it challenging to identify and distinguish specific functions.
- Static Class Members and Object Independence: Static class members are shared among all objects of the class, providing a central repository for shared data or utility functions. This promotes encapsulation and reduces code redundancy.
- Memory Management: Static variables, especially static class members, require careful memory management to prevent memory leaks. If not properly deallocated, they can lead to memory consumption issues.
- Performance: Static variables can enhance performance when used for caching frequently accessed data or reducing repeated memory allocations. However, overreliance can lead to less efficient code and hinder optimization opportunities.
- Testing and Debugging: Static variables can make it more difficult to test and debug code, as they can interfere with the expected behavior of functions and classes. It is important to consider the impact of static variables on testing and debugging when designing and implementing code.
- Documentation and Communication: Static modifiers can make code less self-documenting, as they can affect the visibility and linkage of variables and functions. It is important to provide clear and concise documentation to explain the purpose and usage of static members when sharing code with others.
In summary, static modifiers are powerful tools that can be used to manage variable storage duration, function visibility, and class member accessibility in C/C++ programming. However, it is important to use them judiciously, considering their potential benefits and drawbacks to writing maintainable, efficient, and secure code.
The static modifier makes:
- Local variables have the same lifetime as the global variable.
- Global variables have the same scope as the local variables.
volatile
volatile: Ensuring Data Integrity in Multithreaded and Hardware-Influenced Environments
The volatile modifier plays a crucial role in C/C++ programming, particularly in multithreaded environments and when dealing with hardware interactions. It informs the compiler that a variable's value can be altered by external factors beyond the program's direct control, such as hardware devices, concurrent threads, or interrupt handlers.
When a variable is declared as volatile, the compiler is instructed to refrain from optimizing its usage. This means that the compiler cannot assume the variable's value to remain constant between access points. Instead, it must always retrieve the latest value from memory whenever the variable is referenced.
This precaution is essential because external factors might modify the variable's value without the program's explicit knowledge. In a multithreaded setting, multiple threads may simultaneously access and alter shared data, potentially leading to inconsistencies if the compiler optimizes variable access.
Similarly, when interacting with hardware devices, their registers or memory locations can be updated by external processes or interrupts. Without the volatile modifier, the compiler might use a cached value, leading to erroneous behavior.
By employing the volatile modifier, programmers ensure that the code always reflects the current value of the variable, preventing potential conflicts and ensuring data integrity in multithreaded and hardware-influenced environments.
A variable should be declared volatile whenever its value could change unexpectedly. And use volatile to avoid the compiler optimizing 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 the 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 the 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 occurs, the CPU will jump to the interrupt service routine and change the Global_variable to 1. This change will affect the code in the main() function when reading the Global_variable value.
Global Variables using in Multitask / Multithread
When a global variable is accessed in multitask, it must be declared as volatile.
int volatile var = 0;
int task1()
{
while (var == 0) { // do sth }
}
int task2()
{
var++;
}
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 the 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 are 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 is 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 that are most frequently used in a C program can be put in registers using the 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.
Considerations when using data type modifiers
When using data type modifiers, it is important to consider the following points:
- Data range and overflow: Ensure that the chosen modifier can accommodate the expected range of values for the variable. Overflow can lead to unexpected behavior or program crashes.
- Performance implications: Modifiers like short and long may affect memory access speed and instruction execution time. Consider performance trade-offs when selecting modifiers.
- Compatibility and portability: Different platforms may have varying integer sizes and representations. Use type modifiers with caution when targeting multiple architectures.
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 variable's name 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?
|
|
|
|
|
|
|
|
|
|
|
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 an 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, and 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 a 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 an array (size 5) of a pointer to a function returning a pointer to pointer to an array (size 7) of an 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 an 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 constant |
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, and 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 () ) ();