Chapter 4: Using Variables and Assignments

Programs manipulate data values. Whether a program performs a single calculation, such as, say, converting a temperature value from Fahrenheit into Celsius, reads data only to display it, or performs much more complex calculations and interactions, the values a program manipulates must be both accessible and assignable. Accessible means that a value must reside somewhere in computer memory and should be retrievable. Assignable means that a given value, or the result of a calculation, must be stored somewhere in computer memory to be retrieved and/or changed later. Each value that can be accessed and assigned has a data type and a named location where it is stored. These can either be variables or constants.

Variables, or non-constant variables, hold values that are accessible and assignable. Their values may change during the execution of the program. Constant variables are variables that don't change their value once they are given a value. Variables, whether constant or variable, receive their values via assignment. Assignment is a simple expression. Literal values, or literals, are values that are encoded in the program and can never change.

The following topics will be covered in this chapter:

  • Understanding data types and values
  • Introducing variables
  • Exploring constants
  • Using types and assignments

Understanding variables and constants is essential to learning how to manipulate values. We will see that as values are manipulated, changed, and stored in variables, we can compose both simple and complex manipulations on values via variables. This makes variables become the doorway to meaningful calculations.

Technical requirements

For this chapter, you will need the following:

  • A plaintext editor of your choice
  • A console, terminal, or command-line window (depending on your OS)
  • A compiler – either GCC or clang – for your particular OS

The source code for this chapter can be found at https://github.com/PacktPublishing/Learn-C-Programming-Second-Edition/tree/main/Chapter04

Understanding data types and values

Every value in a computer program has an associated type. The type of a value can be inferred by how it is expressed in the program code and how it is coded. Alternatively, the type of a value can be explicitly determined by you. A value in C always has a type. So, a value can have either an inferred or implicit type or an explicit type.

There are also inferred types from literal values. A literal value is a sequence of digits in the program code whose value is implicitly determined by the compiler at compile time, which is when the program is compiled. The value of a literal can never change; it is baked into the program code. 

When a value is given an explicit type, the compiler assigns a type to that value. A value of one type can also be converted into another type, either implicitly by how it is used or explicitly with typecasting.

So, we should always think of the value/type pair. They are closely intertwined. The type determines not only how the value is interpreted but also what possible valid ranges of values it can have.

If we have a value, then we should always ask what is its type? If we have a type, then we should always ask what values can it have? and what value is it now? This kind of thinking will be critical when we look at looping and arrays.

Introducing variables

variable is a location in memory that holds a value of a specified type that can vary over the life of the variable, identified by its name. When the variable is defined with both a type and an identifier, its life begins. It can hold the same value throughout its life, or it can be modified or overwritten with a new value of that type. The variable's life ends – that is, the memory it identifies is deallocated – when the block that it was declared in ends. We'll talk more about variable lifetimes in Chapter 25Understanding Scope.

So, a variable is a memory location with an identifier (name) associated with a type that contains a value. The following three components are essential: 

  • A unique identifier or name
  • A type
  • A value

A variable is created by declaring it; this is also called defining a variable. When we declare a variable, we specify the data type to be stored in the variable and the name, or identifier, of the variable. This identifier is the location of the value in memory. When we declare a variable with just a data type and identifier, it does not have a known value yet.

The variable should always have some known starting value, even if it is 0. The first value that's given to a variable is called initialization. Initialization can occur when the value is created or any time before we access the variable for use. If we don't give the variable an initial value, we can never be sure what value it might have from one access to the next. When function blocks and programs are deallocated, the values that are occupied by their memory are left behind. So, it is up to us to ensure that we initialize the memory we use to known good values.

A variable is initialized and overwritten using an assignment operation, where a value is assigned to the memory location identified by the variable. Once a constant variable is given a value, that value can never change.

Before we explore values and assignment, we need to understand explicit typing when we create variables.

Naming variables

Every variable has an identifier or a name. A variable name is an identifier; function names are identifiers. We will encounter other kinds of identifiers that are used in many different contexts. 

An identifier, or name, in C is a sequence of capital letters (A..Z) and small letters (a..z), digits (0..9), and the underscore (_) character. An identifier may not begin with a digit. Upper and lowercase letters are different from each other, so acharaCharAChar, and ACHAR would identify different variables. An identifier may not have the same spelling as a C keyword. A list of C keywords can be found in the Appendix section of this book.

As with function identifiers, relying on the casing of letters to differentiate variables is not good programming practice. The most essential guideline is that variable names should closely match the kinds of values they hold. Use variables names that reflect their purpose – for example, inch, foot, yard, and mile.

There are many conventions for naming variables. Two common methods for making variable names descriptive yet easy to read are camel case and underscore-separated, also known as snake case. Camel case names have the beginning characters of words within the name capitalized. In underscore-separated names, _ is used between words:

  • All lowercaseinchesperminutefeetpersecond, and milesperhour
  • Camel caseinchesPerMinutefeetPerSecond, and milesPerHour
  • Snake case (or underscore-separated): inches_per_minutefeet_per_second, and miles_per_hour

As you can see, all lowercase names are somewhat difficult to read. However, these are not nearly as difficult to read as all-uppercase names. The other two ways are quite a bit easier to read. Therefore, we prefer to use either of the last two. We will use camel case identifiers throughout this book.

If you choose one identifier naming convention, stick to it throughout your program. Do not mix different identifier naming schemes as this makes remembering the exact name of a thing, function identifiers, and other identifiers much more difficult and error-prone.

We can now explore explicit types of variables.

Introducing explicit types of variables

The format of a variable declaration is as follows:

type identifier; 

Alternatively, it can be as follows:

 type identifier1, identifiers, ... ;.

Here, type is one of the data types that we encountered earlier and identifier is the name of the variable we are declaring. In the first example, a single variable was declared. In the second example, multiple variables were declared, each having the same type, separated by commas. Note that each is a C statement because it concludes with ;. Consider the following variable declarations:

/* stdbool.h allows us to use: bool, true, false */
#include <stdbool.h> 
int       aNumber;
long      aBigNumber;
long long aReallyBigNumber;
float     inches;
float     feed;
float     yards;
double    length, width, height;
bool      isItRaining;

Each variable is declared with a data type and an identifier. None of them have any known values yet.

In each of these declarations, we use spacing to make the type and name of each variable easier to read. Unfortunately, these declarations are not necessarily the best we could use. The values of the variables do not have any value yet, only a data type and identifier. They have not been initialized yet. Let's learn how to declare and initialize variables in one statement.

Using explicit typing with initialization

A better format for a variable declaration is one where the variable is initialized or given a starting value when it is declared, such as the following:

type  identifier1 = value1; 
type  identifier2 = value2;

The following is an alternative:

type identifier1 = value1 , identifier2 = value2 , ... ;.

Here, value is a literal constant or an already-declared variable. Note that in the second form of variable declaration, each variable must have an initializing value. These are often accidentally omitted. Therefore, the first form is preferred. Consider the following declarations with initialization:

#include <stdbool.h>   /* So we can use: bool, true, false */
int       aNumber          = 10;
long      aBigNumber       = 3211145;
long long aReallyBigNumber = 425632238789;
float     inches           = 33.0;
float     feet             = 2.5;
float     yards            = 1780;
double    length = 1 , width = 2 , height = 10;
bool      isItRaining      = false;

Here, arbitrary spacing is used to vertically align variable names, as well as to align their initial values. This is only done for readability. This sort of practice is not required but is generally considered good practice.

Initialization is a form of assignment; we are assigning a starting value to each variable. This is indicated by the = sign.  

Note that because the type of the variable is explicit in its declaration, the assigned values – in this case, literal constants – are converted values of that type. So, while the following is correct, it may mislead an already tired or overworked programmer reading it:

double length = 1 , width = 2 , height = 10;

Note how a comma (,) is used to separate variable identifiers and the initialization. This declaration is still a single statement since it ends with a semi-colon (;). We will explore this further in Chapter 5Exploring Operators and Expressions.

It is almost always better, although not required, to be explicit. A slightly better version of the preceding code would be as follows:

double length = 1.0;
double width  = 2.0;
double height = 10.0;

Here, each type, identifier, and initial value is on a single line:

int myCounter = 0;
int aDifferentCounter = myCounter;

In these two declarations, myCounter has been initialized to 0 and aDifferentCounter has been initialized to the current value of myCounter.

Having examined variables whose values can change, we can now turn our attention to constant variables (yes, that sounds odd to me as well) and literal constants.

Exploring constants

We use variables to hold values that are computed or can change over their lifetime, such as counters. However, we often have values that we don't ever want to change during their lifetimes. These are constants and can be defined in several ways for different uses.

Literal constants

Consider the following literal character sequences:

      65
     'A'
     8.0f
131072.0

Each of these has an internal byte stream of 0100 0001. However, because of the punctuation surrounding these values, the compiler can infer what types they have from their context:

      65 --> int
      'A' --> unsigned char
      8.0f --> float
 131072.0 --> double

These values are typed into our source code and their types are determined by how they are written, or by how punctuation around them specifies the context for their data type.

The internal value for each is constant; it is that same bit pattern. The literal 65 value will always be interpreted as an integer with that value. The literal 'A' value will always be interpreted as a single character. The literal 8.0 value may be interpreted as a float; if it is interpreted as a double, it will have a slightly different internal bit pattern. When we append f to 8.0, we are telling the compiler that this literal should be treated as a float. The literal 131072.0 value may also be interpreted as a float or a double. When it is interpreted as a double, it will also have the same bit pattern as the others.

To prove the similarity of bit patterns, the following output was produced by the bitpattern_0x41.c program:

Figure 4.1 – Possible interpretations of bitpattern_0x41.c

Figure 4.1 – Possible interpretations of bitpattern_0x41.c

In the first column, the data type is shown; in the second column, the natural representation of that value is shown; and in the third column, the hexadecimal value 0x41 is shown. As you can see, all of these data types have either 0x00 or 0x41 somewhere in their sequence. You may want to return to this chapter to study how this program works after Chapter 19, Exploring Formatted Output; at that time, you will have explored all of the concepts that are being used in this program.

Here are some examples to expand on each of these explanations:

  • Integer constants651024-17 , 163758 , 0, and -1
  • Double constants3.5-0.71748.37530.0-0.000000715e4, and -58.1e-4
  • Character constants'A''a''6''0''>''.', and ' '

Notice that while the 0 value is present in each case, because they are all typed differently, they will have different internal bitstreams. Integer 0 and double 0.0 are patterns of all zeros. However, the '0' character will have a different value, which we will see in Chapter 15Working with Strings.

You may have also noticed the strange notation for the 15e4 and -58.1e-4 doubles. These values are expressed in scientific notation, where the first value evaluates to 15 x 10^4, or 150,000, and the second evaluates to -58.1 x 10-^4, or -0.000581. This is a shorthand notation for values that are either too large or too small to easily express with common decimal notation.

When a constant integer value is too large to fit into the valid ranges of the default type, the type is altered to hold the larger value. Therefore, an int value may become long int or long long int, depending on the value given and the machine architecture. This is implicit typecasting. The compiler does not want to lose any part of a value by stuffing it into a type with a smaller range, so it picks a type that will fit the value. 

There is the same issue regarding implicit conversion of floats and doubles – when a literal value is interpreted, the compiler will pick the most appropriately sized data type, depending on both the value and how that literal value is used.

The preceding literal constants are the most common ones and are evaluated using the base-10 or decimal numbering system. In base-10, there are 10 symbols, 0 through 9, to represent 10 values. Every value in a base-10 number system can be represented as a power of 10. The value 2,573 is really 2,000 + 500 + 70 + 3, or, using exponents of base-10, it is 2*10^3 + 5*10^2 + 7*10^1 + 3*10^0

Sometimes, we may want to express a constant value in base-8, known as octal, or base-16, known as hexadecimal. In C, octal constants begin with the digit 0 and subsequent digits must be in the range of valid octal digits, 0 through 7. Hexadecimal constants begin with ox or 0X (0 followed by the letter x) and the following characters may be the valid hexadecimal digits, 0 through 9 and a through f or A through F.

Without going into greater detail about octal and hexadecimal, here are some examples:

  • Octal integers07011, and 036104 
  • Unsigned octal07u011u, and 036104u
  • Long octal07L011L, and 036104L
  • Hexadecimal integers0x40Xaf0x106a2, and ox7Ca6d
  • Unsigned hexadecimal0x4u0Xafu0x106a2u, and ox7Ca6du
  • Long hexadecimal0x4L0XafL0x106a2L, and ox7Ca6dL

Just remember that base-10, base-8, and base-16 are just different ways to represent values.

Additionally, if you want to guarantee that a decimal number will be a smaller float type than a double, you can follow it with f or F. If you want it to be a much larger long double, you can follow it with l or LL is preferred over using l so as not to confuse the number 1 with the letter l. Here are some examples:

  • Float literals0.5f-12E5f, and 3.45e-5F
  • Double literals0.5-12E5, and 3.45e-5
  • Long-double literals0.5L-12E5fL, and 3.45e-5FL

By using a suffix, you can explicitly type a literal value instead of letting the compiler implicitly pick a type.

Literal constants are interpreted by the compiler and embedded into our program. 

Typically, we use these kinds of constants when we initialize our variables, as we saw in the previous section, or when we want to perform some known calculation on a variable, such as the following:

feet  = inches / 12.0; 
yards = feet / 3.0;

In these calculations, both denominators are decimal numbers. Therefore, they determine the contexts of the resultant calculation. Regardless of what type inches is, the result will be a decimal number. Then, that result will be implicitly converted into whatever type feet is upon assignment. Likewise, the result of feet / 3.0 will be a decimal number (float or double); upon assignment, that result will be converted into the yards type.

Defined values

Another way to define constants is to use the #define preprocessor directive. This takes the form of #define symbol text, where symbol is an identifier and text is a literal constant or a previously defined symbol. Symbol names are typically all in uppercase and underscores are used to distinguish them from variable names.

An example would be to define the number of inches in a foot or the number of feet in a yard:

#define INCHES_PER_FOOT 12
#define FEET_PER_YARD    3
feet  = inches / INCHES_PER_FOOT; 
yards = feet / FEET_PER_YARD;

When the preprocessing phase of compilation encounters a definition such as this, it carries out a textual substitution. It simply replaces the symbol with text. There is no type associated with the symbol and there is no way to verify that the actual use of a symbol matches its intended use. For this reason, the use of these kinds of constants is discouraged. We only included them here for completeness since many older C programs may make extensive use of this mechanism.

Because #define enables textual substitution, there are many other ways it can be and is used. This feature is so powerful that it must be used with extreme caution; that is, if it is used at all. Many of the original reasons for relying on the preprocessor are no longer valid. The best place to explore the preprocessor would be in a much more advanced programming course.

Explicitly typed constants

C provides a safer means of declaring named constants, other than by using the preprocessor. This is done by adding the const keyword to a variable declaration. This sort of declaration must be of the const type identifier = value; form, where typeidentifier, and value are the same as in our preceding variable declaration form – except here, the initializing value is not optional. The constant variable loses its ability to change after the statement is evaluated. If we don't give it a value when we declare it, we cannot do so later. Such a declaration without an initializing value is, therefore, useless.

When we declare a constant in this manner, it is named; it has a type, and it has a value that may not be changed. So, our previous example becomes as follows:

const float kInchesPerFoot = 12.0;
const float kFeetPerYard   =  3.0;    
feet  = inches / kInchesPerFoot; 
yards = feet / kFeetPerYard;

This is considered safer because the constant's type is known and any incorrect use of this type or invalid conversion from this type to some other type will be flagged by the compiler.

It is an arbitrary convention to begin constant names with theletter k;it is not mandatory to do so. We could have also named these constants inchesPerFootConst and feetPerYardConst, or, simply, inchesPerFoot and feetPerYard. Any attempt to change their values would result in a compiler error.

Naming constant variables

C makes no distinction between a variable identifier and a constant variable identifier. However, it is often useful to know whether the identifier you are using is a constant.

As with functions and variables, several conventions are commonly used for naming constants. The following conventions are relatively common to arbitrarily differentiate constants from variables:

  • Prefix a constant name with k or k_ – for example, kInchesPerFoot or k_inches_per_foot.
  • Suffix a name with const or _const – for example, inchesPerFootConst or inches_per_foot_const.
  • Use snake case with all capitals – for example, THIS_IS_A_CONSTANT. All uppercase is quite unreadable. This is typically used for the #define symbols to show that they are not just a constant – for example, INCHES_PER_FOOT
  • None. C does not distinguish between constants – for example, int inchesPerFoot versus const int inchesPerFoot. It should be obvious that the number of inches per foot never changes. Therefore, there is no real need to distinguish its name as a constant.

As with other naming conventions, any convention for constant variables, if one exists, should be clearly defined and consistently used throughout a program or set of program files. I tend to use either the first or the last conventions in my programs.

Using types and assignments

So, we have variables to hold values of a specified type that we can retrieve and manipulate by their identifiers. What can we do with them? Essentially, believe it or not, we can just copy them from one place to another. Values in variables or constants can only be changed through assignment. When we use them, their value is copied as part of the evaluation, but the value remains unchanged. A variable's value can be used in many ways over its lifetime, but that value will not change except for when a new value is copied over it. We will now explore the various ways that variables are copied:

  • Explicit assignment using the = operator
  • Function parameter assignment
  • Function return assignment
  • Implicit assignment (this will be covered when we look at expressions in the next chapter)

Let's look at the first three ways of copying variables in the subsequent sections.

Using explicit assignment – the simplest statement

We have already seen explicit assignment being used when we initialized variables and constants. Once we have declared a variable, we can change it by using the = assignment operator. An assignment statement is of the form identifier = value;, where identifier is our already-declared variable and the value can be a constant, another variable, the result of a calculation, or the returned value from a function. Later, we will see how all of these expressions are evaluated and provide a result.

Here is an example of assignment statements:

float feet;
float yards;
feet = 24.75;

The 24.75 literal constant is evaluated as a value of float, in this case, and is assigned to the feet variable:

feet = yards/3.0 ;

The value of yards is obtained and then divided by the 3.0 literal constant. The result of the evaluation is assigned to the feet variable. The value of yards is unchanged.

Assigning values by passing function parameters

When we declare a function prototype that takes parameters, we also declare those parameters as formal parameters. Formal parameters have no value – only a type and, optionally, a name. However, when we call the function, we supply the actual parameters, which are the values that are copied into those placeholder parameters.

Consider the printDistance() function declaration and definition in the following program:

#include <stdio.h>
void printDistance( double );
int main( void )  
{
  double feet = 5280.0;
   printf( "feet = %5.4g

" , feet );
 printDistance( feet );
  return 0;
}
  // Given feet, print the distance in feet and yards.
void printDistance( double f )  
{
  printf( "The distance in feet is %5.3g
" , f );
  f = f / 3.0 ;
  printf( "The distance in yards is %5.3g
" , f );
}

In this function, we focus on the assignment that occurs between the function call and the execution of the function (at its definition).

The function prototype says that the function takes a single parameter of the double type. Its name is not given since it is optional. Even if we did give it a name, it is not required to match the parameter name given in the function definition. In the function definition, the parameter is named f. For the function, the f variable is created as a double with the value given in the function call. It is as if we had assigned the value of the feet variable to the f function variable. This is exactly how we do that. We can manipulate f because it is assigned a copy of the value of feet at the time the function is called. What this example shows is that the value of the f variable is divided by 3.0 and assigned back to the f variable, and then printed to the console. The f variable has a lifetime of the block of the function and the changed value in f does not change the value in the feet variable.

Because we want to use good coding practices that make our intentions clear rather than obtuse, a better version of this function would be as follows:

  // Given feet, print the distance in feet and yards.
void printDistance( double feet )  {
  double yards = feet / 3.0 ;
  printf( "The distance in feet is %5.3g
" , feet );
  printf( "The distance in yards is %5.3g
" , yards );
}

This is clearer because of the following:

  • We declare the actual parameter, feet, which tells the reader what is being passed into the function.
  • We are explicitly declaring the yards variable and assigning it a value that is the number of feet, or feet, divided by 3.0.

The lifetime of the feet actual parameter and the yards declared variable begins with the start of the execution of the function block and ends with the end of the function block, the return statement, or a closing bracket, }.

Assignment by the function return value

function is a statement that can return the result of its execution to its caller. When the function has a data type that is not void, the function is replaced with its returned value, which can then be assigned to a variable of a compatible type.

The returned value may be explicitly assigned to a variable, or it may be implicitly used within a statement and discarded once the statement has been completed.

Consider thinchesToFeet() function declaration and definition:

#include <stdio.h>
double inchesToFeet( double );
int main( void )  {
  double inches = 1024.0;
  double feet = 0.0;
  feet = inchesToFeet( inches );
  printf( "%12.3g inches is equal to %12.3g feet
" , inches , feet );
  return 0;
}
  // Given inches, convert this to feet
double inchesToFeet( double someInches )  {
  double someFeet = someInches / 12.0;
  return someFeet;
}

In this function, we are focusing on the assignment that occurs at the return statement in the function block and the caller.

In this function, inches – known to the function as the i variable – is converted via a simple calculation into feet and that value is assigned to the f variable in the function. The function returns the value of f to the caller. Essentially, the function call is replaced with the value that it returns. That value is then assigned (copied) to the feet variable in the main() function.

First, we have the following two lines:

  double feet = 0.0;
  feet = inchesToFeet( inches );

Those two lines could be replaced with the following single line:

  double feet = inchesToFeet( inches );

This statement declares feet as a double and initializes it via the return value from the function call.

Summary

Variables are how we store values and their associated types. Variables are identified by a given name. Variable declarations allocate memory for the lifetime of the variable. This depends on where the variable is declared. Variables that are declared within a block, between { and }, only exist while that block is executing. There are variables whose values can change while the program executes, constant variables whose values do not change once they are given a value, and literal values that never change.

Variables are declared with explicit types. However, the type of a value can be implicitly inferred by how it is used. Literal values are constants whose type is inferred both by how they appear and how they are used.

The only way to change the value of a variable is by assigning a value to it. Initialization is the means of giving a variable or constant a value when it is declared. Otherwise, the values of the variables can only change through direct assignment, which is done as the result of a function return value. Functions receive values via their function parameters when the function is called; these values are copies of the values when called and are discarded when the function returns.

Now, you might be wondering, how do we manipulate variables, not just copy them? Variables are changed by assigning the resulting value from an evaluation. C has a rich set of operators to perform evaluations; we will explore these in the next chapter.

Questions

Answer the following questions to test your knowledge of this chapter:

  1. What are the three components of a variable?
  2. What are the various types of constant values?
  3. Why do we initialize variables?
  4. How do we give a variable a value?
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.109.201