Last Updated:

About data types | variables and C++ operators

Before moving on to the more complex C++ tools, it makes sense to learn more about some of the data types and operators. In addition to the data types we've already discussed, C++ defines others. Some of them consist of modifiers added to the types you already know. Others include enumerations, and still others use the typedef keyword. C++ also supports a number of operators that greatly expand the scope of the language and allow you to solve programming problems in a very wide range. We are talking about bit operators, shift operators, as well as the operators "?" and sizeof. In addition, this chapter discusses special operators such as new and delete. They are designed to support the C++ dynamic memory allocation system.


Specifiers of type const and volatile


Specifiers of type const and volatile control access to a variable.
C++ defines two type specifiers that influence how variables can be accessed or modified. These are const and volatile specifiers. Officially, they are called cv-specifiers and must precede the base type when declaring a variable.

Const


Type Specifier Variables declared using the const specifier cannot change their values at run time. However, you can assign some initial value to any const variable. 

const double version = 3.2;


a double-variable version is created that contains the value 3.2, and the program can no longer change this value. However, you can use this variable in other expressions. Any const variable receives a value either during explicit initialization or when using hardware-dependent tools. Applying a const specifier to declaring a variable ensures that it is not modified by other parts of your program.


The const specifier prevents the variable from being modified during program execution.
The const specifier has a number of important uses. It may be most commonly used to create const parameters of type pointer. Such a pointer parameter protects the object it references from modification by the function. In other words, if a pointer parameter is preceded by the const keyword, no statement of that function can modify the variable addressed by that parameter. For example, the code function () in the next short program shifts each letter in the message by one alphabetical position (i.e., the letter 'B' is placed instead of the letter 'A', etc.), thus displaying the message in an encoded form. Using the const specifier in a parameter declaration prevents function code from modifying the object that the parameter points to.

 

#include <iostream>

using namespace std;

void code (const char *str);

int main ()

{

code ("This is a test.");

return 0;

}

/* Using the const specifier ensures that str cannot change the argument it points to.

*/

void code (const char *str)

{

while (*str) {

cout << (char) (*str+1);

str++;

}

}


Because the str parameter is declared as a const pointer, the code() function has no way of making any changes to the string addressed by the str parameter. However, if you try to write the code() function as shown in the following example, you are sure to get an error message and the program will not compile.

This code is incorrect.

 

void code (const char *str)

{

while (*str) {

*str = *str + 1; Error, the argument cannot be modified.

cout << (char) *str;

str++;

}

}

 


Because the str parameter is a const pointer, it cannot be used to modify the object it references.


You can also use the const specifier for reference parameters to prevent the function from modifying the variables referenced by those parameters. For example, the following program is incorrect because the function f () attempts to modify the variable referenced by the i parameter.

Const links cannot be modified.

 

#include <iostream>

using namespace std;

void f (const int &i);

int main ()

{

int to = 10;

f (j);

return 0;

}

Use the const reference parameter.

void f (const int &i)

{

i = 100; Error, you cannot modify the const link.

cout << i;

}


You can still use the const specifier to confirm that your program does not change the value of a variable. Recall that a variable of type const can be modified by external devices, i.e. its value can be set by some hardware device (for example, a sensor). By declaring a variable using the const specifier, you can prove that any changes to that variable are caused solely by external events.


Finally, the const specifier is used to create named constants. Often, programs repeatedly use the same value for different purposes. For example, you would declare several different arrays so that they all have the same size. When you need to use such a "magic number", it makes sense to implement it as a const variable. You can then use the name of that variable instead of the actual value, and if you have to change that value later, you'll change it in only one place in the program. The following program allows you to try this kind of application of the const specifier "to taste".

 

#include <iostream>

using namespace std;

const int size = 10;

int main ()

{

int A1[size], A2[size], A3[size];

// . . .

}


In this example, if you want to use the new size for the arrays, you only need to change the declaration of the size variable and recompile the program. As a result, all three arrays will automatically receive a new size.


The volatile


specifier informs the compiler that a given variable can be changed by factors external to the program.


The volatile specifier informs the compiler that the value of the corresponding variable can be changed implicitly in the program. For example, the address of some global variable can be passed to an interrupt-driven clock routine that updates that variable with the arrival of each pulse of the time signal. In such a situation, the contents of the variable are changed without using explicitly specified program instructions. There are good reasons to inform the compiler about the external factors of changing a variable. The fact is that the C++ compiler is allowed to automatically optimize certain expressions on the assumption that the contents of a particular variable remain unchanged if it is not on the left side of the assignment instruction. However, if some factors (external to the program) change the value of this field, this assumption will be incorrect, resulting in problems.


For example, in the following code snippet, assume that the clock variable is updated every millisecond by the computer's clockwork. But, because the clock variable is not declared using the volatile specifier, this code snippet can sometimes not work properly. (Pay special attention to the lines marked with the letters "A" and "B".)

 

int clock, timer;

// ...

timer = clock; line A

// ... Some action.

cout << "Elapsed time" << clock-timer;


In this fragment, the clock variable gets its value when it is assigned to the timer variable in line A. But because the clock variable is not declared using the volatile specifier, the compiler is free to optimize this code, and in such a way that the value of the clock variable may not be queried in the cout statement (line B) if there is no intermediate assignment of the value of the clock variable between lines A and B. (In other words, in line B, the compiler can simply reuse the value that the clock variable received in line A.) But if between the moments of execution of lines A and B there are regular pulses of the time signal, then the value of the clock variable will necessarily change, and the line B in this case will not reflect the correct result.


To resolve this issue, declare a clock variable with the volatile keyword.

volatile int clock;


Now the value of the clock variable will be polled each time it is used.
And although at first glance it may seem strange, the specifiers const and volatile can be used together. For example, the following declaration is absolutely valid. It creates a const pointer to the volatile object.

const volatile unsigned char *port = (const volatile char *) 0×2112;
In this example, a type conversion operation is required to convert the integer literal 0×2112 to a const pointer to a volatile character.


C++ Memory


Class Specifiers supports five memory class specifiers:

  • auto
  • extern
  • register
  • static
  • mutable


Memory class specifiers define how a variable should be stored.


With these keywords, the compiler gets information about how the variable should be stored. The specifier of memory classes must be specified at the beginning of the variable declaration.


The mutable specifier applies only to the objects of the classes in question. We will consider the rest of the specifiers in this section.


Auto


Memory Class Specifier The rarely used auto specifier declares a local variable.
The auto specifier declares a local variable. But it's rarely used (you may never get to use it) because local variables are "automatic" by default. It is unlikely that you will come across this keyword in other people's programs.


Extern


Memory Class Specifier All the programs we've covered so far have had a fairly modest size. Real computer programs are much more. As the size of the file containing the program increases, the compilation time sometimes becomes annoyingly long. In this case, you should split the program into several separate files. After that, small changes made to one file will not require recompilation of the entire program. When developing large projects, this multi-file approach can save significant time. The extern keyword allows you to implement this approach.


In programs that consist of two or more files, each file must "know" the names and types of global variables used by the program as a whole. However, you cannot simply declare copies of global variables in each file. The fact is that in C++ a program can include only one copy of each global variable. Therefore, if you try to declare the necessary global variables in each file, problems will arise. When the linker tries to compose these files, it will detect duplicate global variables and the program will not be linked. To get out of this predicament, it is enough to declare all the global variables in one file, and in others use extern declarations, as shown in Fig. 9.1


The extern specifier declares a variable, but does not allocate memory space for it.
In the F1 file, the variables x, y, and ch are declared and defined. The F2 file uses a list of global variables copied from the F1 file, to the declaration of which the extern keyword is added. The extern specifier makes the variable known to the module, but does not actually create it. In other words, the extern keyword provides the compiler with information about the type and name of global variables without re-allocating memory for them. During the layout of these two modules, all references to these external variables will be defined.


Until now, we haven't clarified what the difference is between declaring and defining a variable, but it's very important here. When a variable is declared, a name and type are assigned, and memory is allocated to the variable through a definition. In most cases, variable declarations are also definitions. By prejudging the name of a variable with the extern specifier, you can declare the variable without defining it.


There is another use for the extern keyword that is not associated with multi-file projects. It's no secret that a lot of time is spent declaring global variables that are like a rule.Lo, given at the beginning of the program, but this is not always necessary. If a function uses a global variable that is defined below (in the same file), you can specify it as external in the function body (using the extern keyword). If it detects a definition of this variable, the compiler will calculate the appropriate references to it.


Consider the following example. Note that the global variables first and last are declared not before, but after the main () function.

 

#include <iostream>

using namespace std;

int main ()

{

extern int first, last; Use global variables.

cout << first << " " << last << «\n»;

return 0;

}

Global definition of first and last variables.

int first = 10, last = 20;


When you run this program, the numbers 10 and 20 will be displayed because the global variables first and last used in the cout statement are initialized with these values. Because the extern declaration in the main () function tells the compiler that the variables first and last are declared somewhere else (in this case, below, but in the same file), the program can be compiled without error, even though the variables first and last are used before they are defined.


It is important to understand that the extern declarations of variables shown in the previous program are necessary here only because the variables first and last were not defined before they were used in the main () function. If the compiler had detected their definitions before defining the main() function, there would be no need for an extern instruction. Remember, if the compiler encounters a variable that has not been declared in the current block, it checks to see if it matches any of the variables declared inside the other enabling blocks. If not, the compiler looks at the previously declared global variables. If their names match, the compiler assumes that the reference was to this global variable. The extern specifier is only necessary if you want to use a variable that is declared either lower in the same file or in another.


And one more thing. Although the extern specifier declares but does not define a variable, there is one exception to this rule. If a variable is initialized in an extern declaration, the extern declaration becomes a definition. This is a very important point, because any object can have several declarations, but only one definition.


Static variables


Of type static are variables of "long-term" storage, i.e. they store their values within the limits of their function or file. They differ from global ones in that they are unknown beyond the scope of their function or file. Since the static specifier determines the "fate" of local and global variables differently, we will consider them separately.


Local static variables


The local static variable maintains its value between function calls.


If a static modifier is applied to a local variable, a constant memory space is allocated to it in much the same way as a global variable. This allows the static variable to maintain its value between function calls. (In other words, unlike a regular local variable, the value of a static variable is not lost when you exit a function.) The key difference between a static local variable and a global variable is that a static local variable is known only to the block in which it is declared. Thus, a static local variable can to some extent be called a global variable that has a limited scope.


To declare a static variable, simply prefix its type with the static keyword. For example, when you execute this statement, the variable count is declared static.

static int count;


You can assign some initial value to a static variable. For example, this statement sets the variable count to an initial value of 200:

static int count = 200;


Local static variables are initialized only once, at the beginning of program execution, not each time they are entered into the function in which they are declared.
The ability to use static local variables is important for creating independent functions because there are types of functions that must store their values between calls. If static variables were not provided in C++, you would have to use global variables instead, which would open the way for all sorts of side effects.


Let's consider an example of using a static variable. It is used to store the current average of the numbers entered by the user.

 

/* Calculate the current average of the numbers entered by the user.

*/

#include <iostream>

using namespace std;

int r_avg (int i);

int main ()

{

int num;

do {

cout << "Enter numbers (-1 means output): ";

cin >> num;

if (num != -1)

cout << "The current average is: " << r_avg (num);

cout << '\n';

}while (num > -1);

return 0;

}

Calculate the current average.

int r_avg (int i)

{

static int sum=0, count=0;

sum = sum + i;

count++;

return sum / count;

}


Here, both the local variables sum and count are declared static and initialized with a value of 0. Remember that static variables are initialized only once (the first time the function is executed), not each time the function is entered. In this program, the r_avg function () is used to calculate the current average of the numbers entered by the user. Because both the sum and count variables are static, they maintain their values between calls to the r_avg() function, which allows us to get the correct result of the calculations. To make sure that you need the static modifier, try removing it from the program. After that, the program will not work correctly, since the intermediate amount will be lost each time you exit the r_avg function ().


Global static variables


A global static variable is known only for the file in which it is declared.
If the static modifier is applied to a global variable, the compiler will create a global variable that is known only to the file in which it is declared. This means that although this variable is global, other functions in other files have "no idea" about it and cannot change its contents. Therefore, it cannot become a "victim" of unauthorized changes. Therefore, for special situations where local static is powerless, you can create a small file that contains only functions that use global static variables, compile this file separately and work with it without fear of harm from the side effects of "universal globality".


Consider an example, which is a revised version of a program (from the previous section) that calculates the current average. This version consists of two files and uses global static variables to store intermediate sum values and an input count.



---------------------First file---------------------

#include <iostream>

using namespace std;

int r_avg (int i);

void reset ();

int main ()

{

int num;

do {

cout <<"Enter numbers (-1 to exit, -2 to reset): ";

cin >> num;

if (num==-2) {

reset ();

continue;

}

if (num != -1)

cout << "The average is: " << r_avg (num);

cout << '\n';

}while (num != -1);

return 0;

}

---------------------The second file---------------------

 

#include <iostream>

static int sum=0, count=0;

int r_avg (int i)

{

sum = sum + i;

count++;

return sum / count;

}

void reset ()

{

sum = 0;

count = 0;

}


In this version of the program, the sum and count variables are globally static, i.e. their globality is limited to the second file. So, they are used by the r_avg () and reset () functions, both of which are located in the second file. This version of the program allows you to reset the accumulated amount (by setting the sum and count variables to the initial position) so that you can average another set of numbers. But none of the functions located outside the second file can access these variables. Working with this program, you can reset the previous accumulations by entering the number -2. In this case, the reset() function will be called. Check it out. Also, try to access any of the sum or count variables from the first file. (You'll get an error message.)


So, the name of the local static variable is known only to the function or the block of code in which it is declared, and the name of the global static variable is known only to the file in which it "lives". Essentially, the static modifier allows variables to exist in such a way that only the functions that use them know about them, thereby "keeping in check" and limiting the possibility of negative side effects. Static variables allow a programmer to "hide" some parts of his program from other parts. It can just be super worthy when you have to develop a very large and complex program.


Importantly! Although global static variables are still valid and widely used in C++ code, the C++ standard objects to their use. To control access to global variables, another method is to use namespaces. This method is described later in this book.


Register variables


The register class specifier may be most commonly used. For the compiler, the register modifier means that the corresponding variable is stored in such a way that it can be accessed as quickly as possible. Typically, the variable in this case will be stored either in the register of the central processing unit (CPU) or in the cache (high-speed buffer memory of small capacity). You probably know that access to CPU registers (or cache memory) is fundamentally faster than access to a computer's main memory. Thus, a variable stored in the register will be served much faster than a variable stored, for example, in random access memory (RAM). Since the rate at which variables can be accessed determines, in fact, the speed at which your program runs, it is important to use the register specifier wisely to obtain satisfactory programming results.


The register specifier in a variable declaration means that you want to optimize your code to get the fastest possible access rate.


Formally, the register specifier is only a query that the compiler has the right to ignore. This is easy to explain: after all, the number of registers (or memory devices with a short sampling time) is limited, and for different environments it may be different. Therefore, if the compiler runs out of fast access memory, it will store register variables in the usual way. In general, an unsatisfied register request does no harm, but, of course, does not give any advantages of storage in registered memory.


Since in reality only a limited number of variables can be accessed quickly, it is important to carefully accesschoose which one to apply the register modifier to. (Only the right choice can improve the performance of the program.) In general, the more often a variable needs to be accessed, the greater the benefit of optimizing your code using the register specifier. Therefore, it makes sense to declare loop control variables or variables that are accessed in the body of the loop to be registered. The following function shows how a register variable of type int is used to control a loop. This function calculates the result of the expression me for integer values while retaining the sign of the original number (that is, if m = -2 and e = 2, the result will be -4).

 

int signed_pwr (register int m, register int e)

{

register int temp;

int sign;

if (m < 0) sign = -1;

else sign = 1;

temp = 1;

for ( ; e; e--) temp = temp * m;

return temp * sign;

}


In this example, the variables m, e, and temp are declared registered because they are all used in the body of the loop and are therefore often accessed. However, the sign variable is declared without the register specifier because it is not part of a loop and is used less frequently.


The register


modifier was first defined in C. Initially, it was applied only to variables of type int and char or to pointers and forced variables of this type to be stored in the CPU register, and not in RAM, where ordinary variables are stored. This meant that operations on register variables could be performed much faster than operations on other (stored in memory) because no memory access was required to interrogate or modify their values.


After standardizing the C language, it was decided to expand the definition of the register specifier. According to the ANSI C standard, the register modifier can be applied to any data type. Its use began to mean for the compiler the requirement to make access to a variable of type register as fast as possible. For situations involving characters and integer values, this still means putting them in CPU registers, so the traditional definition is still valid. Because C++ is built on the C ANSI standard, it also supports an extended definition of the register specifier.


As mentioned above, the exact number of register variables that will actually be optimized in any one function is determined by both the type of processor and the specific C++ implementation you are using. In general, you can count on at least two. However, don't worry that you might have declared too many register variables, because C++ will automatically convert register variables to non-register variables when their limit is reached. (This ensures that C++ code is portable across a wide range of processors.)
To illustrate the impact of register variables on program performance, the following example measures the execution time of two for loops, which differ from each other only in the type of control variables. The program uses the standard library C++ function clock (), which returns the number of pulses of the system clock time signal calculated from the beginning of the execution of this program. The program must include the title <ctime>.

 

/* This program demonstrates the impact that using the register variable can have on the speed of program execution.

*/

#include <iostream>

#include <ctime>

using namespace std;

unsigned int i; not register-variable

unsigned int delay;

int main ()

{

register unsigned int j;

long start, end;

start = clock ();

for (delay=0; delay<50; delay++)

for (i=0; i<64000000; i++);

end = clock ();

cout << "Number of ticks for non-register cycle: ";

cout << end-start << ' \n';

start = clock ();

for (delay=0; delay<50; delay++)

for (j=0; j<64000000; j++);

end = clock ();

cout << "Number of ticks for the register cycle: ";

cout << end-start << '\n';

return 0;

}


When you run this program, you will see that the loop with "register" control is approximately twice as fast as the cycle with "non-register" control. If you don't see the expected difference, it could mean that your compiler is optimizing all the variables. Just "play" the program until the difference becomes apparent.


Just a note. This book was written using Visual C++, which ignores the register keyword. Visual C++ applies optimization "as it sees fit." Therefore, you may not notice the effect of the register specifier on the execution of the previous program. However, the register keyword is still accepted by the compiler without an error message. It just doesn't have any impact.


Enumerations


In C++, you can define a list of named integer constants. Such a list is called an enumeration. These constants can then be used wherever integer values are valid (for example, in integer terms). Enumerations are defined using the enum keyword, and their format is as follows:

enum type_name { enumeration_list } variable_list;


Enumeration_list is a comma-separated list of names that represent enumeration values. Variable_List is optional because you can declare variables later by using the name of the enumeration type. The following example defines an apple enumeration and two variables of type apple named red and yellow.

enum apple {Jonathan, Golden_Del, Red_Del, Winesap, Cortland, McIntosh} red, yellow;


Once you have defined an enumeration, you can declare other variables of that type by using the enumeration name. For example, the following statement declares a single fruit variable of the apple enumeration.

apple fruit;


You can write this instruction down like this.

enum apple fruit;
The enum keyword declares an enumeration.
However, the use of the enum keyword is redundant here. In the C language (which also supports enumerations), the second form was required, so in some programs you may find a similar entry.
Based on the previous declarations, the following instruction types are perfectly acceptable.

 

fruit = Winesap;

if (fruit==Red_Del) cout << «Red Delicious\n»;


It is important to understand that each character in the enumeration list means an integer, and each subsequent number (represented by the identifier) is one more than the previous one. by default the value of the first character of the enumeration is zero therefore the value of the second character is one and so on.

cout << Jonathan << ' ' << Cortland;


the numbers 0 4 will be displayed.


Although enumerable constants are automatically converted to integer constants, the reverse conversion is not automatically performed. For example, the following statement is incorrect.

fruit =1; error


This statement will cause a compile-time error because there is no automatic conversion of integer values to apple values. You can correct the previous statement by using a type conversion operation.

fruit = (apple) 1; It's okay now, but the style isn't perfect.


The fruit variable will now contain the value Golden_Del because this apple constant is associated with the value 1. As noted in the commentary, despite the fact that this instruction has become correct, its style leaves much to be desired, which is forgivable only in special circumstances.


Using the initializer, you can specify the value of one or more enumerated constants. This is done as follows: after the corresponding item in the enumeration list, an equal sign and the desired integer are placed. When you use an initializer, the next (after initialized) list item is assigned a value one greater than the previous initializer value. For example, when you execute the following statement, the Winesap constant is set to 10.

enum apple {Jonathan, Golden_Del, Red_Del, Winesap=10, Cortland, McIntosh};
It is often mistakenly assumed that enumeration characters can be entered and displayed as strings for enumerations. For example, the following code snippet will not be executed.

The word "McIntosh" will not hit the screen in this way.

fruit = McIntosh;

cout << fruit;


Keep in mind that the McIntosh character is just a name for some integer value, not a string. Therefore, when you run the previous code, the screen displays the numeric value of the McIntosh constant instead of the string "McIntosh". Of course, you can generate input and output code for enumeration characters as strings, but it comes out somewhat cumbersome. Here, for example, is how you can display on the screen the names of apple varieties associated with the fruit variable.

 

switch (fruit) {

case Jonathan: cout << «Jonathan»;

break;

case Golden_Del: cout << «Golden Delicious»;

break;

case Red_Del: cout << «Red Delicious»;

break;

case Winesap: cout << «Winesap»;

break;

case Cortland: cout << «Cortland»;

break;

case McIntosh: cout << «McIntosh»;

break;

}


Sometimes you can declare an array of strings and use the enumeration value as an index to translate an enumeration value into the appropriate string. For example, the following program displays the names of three varieties of apples.

#include <iostream>

using namespace std;

enum apple {Jonathan, Golden_Del, Red_Del, Winesap, Cortland, McIntosh};

An array of strings associated with the apple enumeration.

char name[][20] = {

«Jonathan»,

«Golden Delicious»,

«Red Delicious»,

«Winesap»,

«Cortland»,

«McIntosh»,

};

int main ()

{

apple fruit;

fruit = Jonathan;

cout << name[fruit] << '\n';

fruit = Winesap;

cout << name[fruit] << '\n';

fruit = McIntosh;

cout << name[fruit] << '\n';

return 0;

}

 


The results of the programme are as follows.

Jonathan

Winesap

McIntosh


The method used in this program to convert an enumeration value to a string can be applied to any type of enumeration as long as it contains no initializers. For an array of strings to be properly indexed, the enumerated constants must start at zero, be strictly ordered in ascending order, and each successive constant must be exactly one larger than the previous one.


Because enumeration values must be manually converted to human-readable strings, they are mostly used where such conversion is not required. For example, consider the enumeration used to define the compiler symbol table.


The typedef


keyword allows you to create a new name for an existing data type.
C++ allows you to define new data type names using the typedef keyword. When you use a typedef name, you do not create a new data type, but only define a new name for an existing type. With typedef names, you can make machine-dependent programs more portable by sometimes changing the typedef instructions. This tool also helps you improve the readability of your code because you can use descriptive names for standard data types. The general format for writing a typedef statement is as follows:

typedef type new_name;


Here, the type element stands for any valid data type, and the new_name element is the new name for that type. Note that you define the new name as an addition to the existing type name, not to replace it.
for example you can use the following statement to create a new name for the float type

typedef float balance;


This instruction instructs the compiler to recognize the balance identifier as another name for the float type. After this statement, you can create float variables using the name balance.

balance over_due;


A floating-point variable is declared here over_due of type balance, which is a standard float type but has a different name.

More about operators Earlier in this book, you've already seen most operators that are not unique to C++. But, unlike other programming languages, C++ provides other special operators that significantly expand the capabilities of the language and increase its flexibility. The rest of this chapter is devoted to these operators.


Bitwise operators


Bitwise operators handle individual bits.
Because C++ aims to allow full access to a computer's hardware, it is important that it be able to directly affect individual bits within a byte or machine word. That is why C++ contains bitwise operators. Bitwise operators are designed to test, set, or shift real bits in bytes or words that correspond to character or integer C++ types. Bitwise operators are not used for operands such as bool, float, double, long double, void, or other even more complex data types. Bitwise operators (they are listed in Table 9.1) are very often used to solve a wide range of system-level programming problems, for example, when interviewing information about the state of the device or its formation. Now let's look at each operator of this group separately.


Bitwise operators AND, OR, excluding OR and NOT


Bitwise operators AND, OR, excluding OR and NOT (denoted by the characters &, |, ^ and ~ respectively) perform the same operations as their logical equivalents (i.e. they act according to the same truth table). The only difference is that bitwise operations work on a bitwise basis. The following table shows the result of each bitwise operation for all possible combinations of operands (zeros and ones).


As can be seen from the table, the result of using the XOR operator (excluding OR) will be equal to TRUE (1) only if only one of the operands is true (equal to value 1); otherwise, the result is FALSE (0).


The bit and operator AND can be thought of as a way to suppress bit information. This means that 0 in any operand will ensure that the corresponding result bit is set to 0. Here's an example.


1101 0011
& 1010 1010
1000 0010


The following program reads characters from the keyboard and converts any lowercase character to its uppercase equivalent by setting the sixth bit to 0. The ASCII character set is defined so that lowercase letters have almost the same code as uppercase letters, except that the code of the former differs from the code of the latter by exactly 32[only for the Latin alphabet]. Therefore, as shown in this program, in order to make a lowercase letter from a lowercase letter, it is enough to reset its sixth bit.

Obtaining uppercase letters.

 

#include <iostream>

using namespace std;

int main ()

{

char ch;

do {

cin >> ch;

This instruction resets the 6th bit.

ch = ch & 223; The variable ch now has an uppercase letter.

cout << ch;

}while (ch! = 'Q');

return 0;

}

The value 223 used in the bit I instruction is the decimal representation of the binary number 1101 1111. Therefore, this AND operation leaves all the bits in the ch variable intact except for the sixth (it is reset to zero).
The AND operator is also useful if you want to determine whether the bit you are interested in is set (i.e., whether it is equal to the value 1) or not. for example if you run the following statement you will know whether the 4th bit in the status variable is set

if (status & 8) cout << "Bit 4 installed";


To understand why the number 8 is used to test the fourth bit, remember that in the binary number system, the number 8 is represented as 0000 1000, i.e. only the fourth digit is set to the number 8. Therefore, the conditional expression of the if statement will yield TRUE only if the fourth bit of the status variable is also set (equal to 1). An interesting use of this method is shown in the example of the disp_binary function (). It displays in binary format the bit configuration of its argument. We will use the disp_binary function later in this chapter to explore the possibilities of other bit-to-bit operations.

Displays the bit configuration in a byte.

 

void disp_binary (unsigned u)

{

register int t;

for (t=128; t>0; t=t/2)

if (u & t) cout << «1»;

else cout << "0 ";

cout << «\n»;

}


The function disp_binary (), using the bit and operator, sequentially tests each bit of the low byte of the variable u to determine whether it is set or reset. If it is set, the number 1 is displayed, otherwise the digit 0 is displayed. For the sake of interest, try extending this function so that it displays all the bits of the variable u, not just its low-order byte.
The bit or operator, as opposed to the bit AND, is convenient to use to set the desired bits per unit. When performing an OR operation, the presence of a bit equal to 1 in any operand means that as a result, the corresponding bit will also be equal to one. Here's an example.


1101 0011
| 1010 1010
1111 1011


You can use the OR operator to convert the above program (which converts lowercase characters into their uppercase equivalents) into its "opposite", i.e. now, as shown below, it will convert uppercase letters to lowercase letters.

Obtain lowercase letters.

 

#include <iostream>

using namespace std;

int main ()

{

char ch;

do {

cin >> ch;

/* This instruction makes the letter lowercase by setting it to the 6th bit.*/

ch = ch | 32;

cout << ch;

}while (ch != 'q');

return 0;

}


Setting the sixth bit turns an uppercase letter into its lowercase equivalent. Bitwise exclusive OR (XOR) sets the result bit per unit only if the corresponding operand bits differ from one another, i.e. are not equal. Here's an example:


0111 1111
^1011 1001
1100 0110


A UNARY OPERATOR (or an addition operator to 1) inverts the state of all bits of its operand. For example, if the integer value (stored in the variable A) is a binary code of 1001 0110, then the result of the operation ~A is the binary code 0110 1001.
The following program demonstrates how to use the NOT operator by displaying a number and adding it to 1 in binary code using the disp_binary () function above.

 

#include <iostream>

using namespace std;

void disp_binary (unsigned u);

int main ()

{

unsigned u;

cout << "Enter a number between 0 and 255: ";

cin >> u;

cout << "Source number in binary: ";

disp_binary (u);

cout << "Its addition to one: ";

disp_binary (~u);

return 0;

}

Displays the bits that make up bytes.

void disp_binary (unsigned u)

{

register int t;

for (t=128; t>0; t=t/2)

if (u & t) cout << «1»;

else cout << «0»;

cout << «\n»;

}

 


Here's what the results of this program look like.

Enter a number between 0 and 255: 99

Source number in binary: 01100011

Its addition to one: 10011100


And more. Do not confuse logical and bitwise operators. They perform various actions. Operators &, | and ~ apply directly to each bit of the value individually. Equivalent Logical operators treat TRUE/FALSE (not zero/zero) values as operands. Therefore, bitwise operators cannot be used in place of their boolean equivalents in conditional expressions. For example, if the value x is 7, then the expression x && 8 is TRUE, while the expression x & 8 gives the value FALSE.


A nodule for memory. A relationship operator or logical operator always generates a result that is TRUE or FALSE, while a similar bitwise operator generates a value that is derived from the truth table of a particular operation.


Shift Operators


Shift operators, >>, and << shift all bits in a value to the right or left.
The general format for using the right shift operator is as follows.

the value >> num_bits A the left shift operator is used as follows
.

value << num_bits


Shift operators are designed to shift bits within an integer value.


Here, the num_bits element indicates how many positions the value should be shifted by. With each shift to the left, all the bits that make up the value are shifted to the left by one position, and zero is written to the lower digit. With each shift to the right, all the bits are shifted, respectively, to the right. If an unsigned value is shifted to the right, zero is written to the high digit. If a value with a sign is shifted to the right, the value of the sign digit is preserved. As you may recall, negative integers are represented by setting the highest digit of the number to one. Thus, if the offset value is negative, one is written for each shift to the right to the higher digit, and if positive, zero. Do not forget, the shift performed by shift operators is not cyclical, i.e. when shifting both to the right and to the left, the extreme bits are lost, and the contents of the lost bit cannot be known.


Shift operators work only with integer type values, such as symbols, integers, and long integers. They do not apply to floating-point values.


Bitwise shift operations can be very useful for decoding input information received from external devices (e.g., digital-to-analog converters) and processing device state information. Bitwise shift operators can also be used to perform accelerated multiplication and division operations. With the help of shifting to the left, you can effectively multiply by two, shifting to the right allows you to divide by two no less efficiently.


The following program illustrates the result of using shear operators.

 

Demonstration of bitwise shift execution.

 

#include <iostream>

using namespace std;

void disp_binary (unsigned u);

int main ()

{

int i=1, t;

for (t=0; t<8; t++) {

disp_binary (i);

i = i << 1;

}

cout << «\n»;

for (t=0; t<8; t++) {

i = i >> 1;

disp_binary (i);

}

return 0;

}

Displays the bits that make up bytes.

void disp_binary (unsigned u)

{

register int t;

for (t=128; t>0; t=t/2)

if (u & t) cout << «1»;

else cout << "0 ";

cout << «\n»;

}


The results of the programme are as follows.

0 0 0 0 0 0 0 1

0 0 0 0 0 0 1 0

0 0 0 0 0 1 0 0

0 0 0 0 1 0 0 0

0 0 0 1 0 0 0 0

0 0 1 0 0 0 0 0

0 1 0 0 0 0 0 0

1 0 0 0 0 0 0 0

1 0 0 0 0 0 0 0

0 1 0 0 0 0 0 0

0 0 1 0 0 0 0 0

0 0 0 1 0 0 0 0

0 0 0 0 1 0 0 0

0 0 0 0 0 1 0 0

0 0 0 0 0 0 1 0

0 0 0 0 0 0 0 1


Question Mark operator


One of the most remarkable C++ operators is the "?". The "?" operator can be used as a replacement for if-else statements used in the following common format.

if (condition)

variable = expression 1;

else

variable = expression 2;


Here, the value assigned to a variable depends on the result of evaluating the condition element that controls the if statement.


The operator "?" is called ternary because it works with three operands. Here's its general recording format:

Expression1? Expression2 : Expression3;


All elements here are expressions. Note the use and location of the colon.
The value of the ? expression is defined as follows: Expression1 is evaluated. If it turns out to be true, Expression2 is evaluated, and the result of its calculation becomes the value of the entire ?-expression. If the result of evaluating The Expression1 element is false, the value of the entire ?-expression is the result of the evaluation of the Expression3 element. Consider the following example.

 

while (something) {

x = count > 0 ? 0 : 1;

// ...

}


Here, the variable x will be set to 0 until the value of the count variable is less than or equal to zero. Similar code (but using an if-else statement) would look like this.

while (something) {

if (count >0) x = 0;

else x = 1;

// ...

}


And here is another example of the practical application of the operator ?. The following program divides two numbers, but does not allow division by zero.

 

/* This program uses the ? to prevent division by zero.

*/

#include <iostream>

using namespace std;

int div_zero ();

int main ()

{

int i, j, result;

cout << "Enter divisible and divisor: ";

cin >> i >> j;

This statement will prevent a division by zero error from occurring.

result = j ? i/j : div_zero ();

cout << "Result: " << result;

return 0;

}

int div_zero ()

{

cout << "Cannot be divided by zero. \n»;

return 0;

}


Here, if the value of the variable j is not zero, the value of the variable i is divided by the value of the variable j, and the result is assigned to the variable result. Otherwise, the divide by zero error handler is called div_ze go(), and the result variable is set to zero.