C++

More on Handling Basic Data Types

Have you ever wanted to learn how basic types of C++ variables interact in complex situations? Ivor Horton explains this, and also describes some interesting features of C++. This article is from chapter 3 of Ivor Horton's Beginning ANSC C++ The Complete Language (Apress, 2004; ISBN 1590592271).

Size of type char is 1Size of type short is 2Size of type int is 4Size of type long is 4Size of type float is 4Size of type double is 8Size of type long double is 8

======================================================

You could modify this example to use the sizeof operator to obtain the sizes of sample variables and expressions, too.

Finding the Limits

There are occasions when you may want to know more about a particular type than just its size. You might want to know what the upper and lower limits on the values it can hold are, for instance. The standard header called <limits>makes this kind of information available for all the standard data types. The information is provided through a class for each type, and because I haven’t discussed classes yet, the way this works won’t be obvious to you at this point. However, you’ll look at how you get the information using the facilities provided by the <limits>header, and I’ll leave the detail explanations until I cover classes specifically in Chapter 13.

Let’s look at an example. To display the maximum value you can store in a variable of type double, you could write this:

The expression std::numeric_limits ::max() produces the value you want. By putting different type names between the angled brackets, you can obtain the maximum values for other data types. You can also replace max() with min() to get the minimum value that can be stored, but the meaning of minimum is different for integer and floating-point types. For an integer type, min() results in the true minimum, which will be a negative number for a signed integer type. For a floating-point type, min() returns the minimum positive value that can be stored.

You can retrieve many other items of information about various types. The number of binary digits, for example, is returned by this expression:

std::numeric_limits<type_name>::digits

You just insert the type_name in which you’re interested between the angled brackets. For floating-point types, you’ll get the number of binary digits in the mantissa. For signed integer types, you’ll get the number of binary digits in the value—that is, excluding the sign bit.

To see this sort of thing in action, you can put together another little example to display the maximums and minimums for the numerical data types.

cout << endl<< "The range for type long is from "<< numeric_limits<long>::min()<< " to "<< numeric_limits<long>::max();cout << endl<< "The range for type float is from "<< numeric_limits<float>::min()<< " to "<< numeric_limits<float>::max();cout << endl<< "The range for type double is from "<< numeric_limits<double>::min()<< " to "<< numeric_limits<double>::max();cout << endl<< "The range for type long double is from "<< numeric_limits<long double>::min()<< " to "<< numeric_limits<long double>::max();cout << endl;return 0;}

On my computer, this program produces the following output:

======================================================

The range for type short is from -32768 to 32767The range for type int is from -2147483648 to 2147483647The range for type long is from -2147483648 to 2147483647The range for type float is from 1.17549e-038 to 3.40282e+038The range for type double is from 2.22507e-308 to 1.79769e+308The range for type long double is from 2.22507e-308 to1.79769e+308

=====================================================

HOW IT WORKS

This is a straightforward application of what you learned in the previous section. The values retrieved in the manner you’ve used in this example have a type and are therefore type-checked by the compiler when you use them.

Just so that you’re aware of them, the range limits for numeric data types are also defined by macros in headers that are inherited from C. The <cfloat>header defines symbols for floating-point limits and the <climits> header defines symbols relating to limits for integer types. For example, in the <climits>header, the symbol INT_MAX represents the maximum value of a value of type int, and SCHAR_MIN represents the minimum value of a signed char. There are type_MAX and type_MIN symbols for the other types too. However, these are just symbols that the compiler will replace in your code by the equivalent number as a literal. For this reason, it’s usually better to use the runtime_limits<> mechanism when you’re accessing limits for types at runtime.

Bitwise Operators

As their name suggests, the bitwise operators enable you to operate on an integer variable at the bit level. You can apply the bitwise operators to any type of integer, both signed and unsigned, including the type char. However, they’re usually applied to unsigned integer types.

A typical application for these operators is when you want to use individual bits in an integer variable to store information. An example of this would be flags, which is the term used to describe binary state indicators. You can use a single bit to store any value that has two states: on or off, male or female, true or false.

You can also use the bitwise operators to work with several items of information stored in a single variable. For instance, color values are often recorded as three 8-bit values for the intensities of the red, green, and blue components in the color. These are usually packed into 3 bytes of a 4-byte word. The fourth byte is not wasted either; it often contains a value that is a measure of the transparency of a color. Obviously, to work with individual color components, you need to be able to separate out the individual bytes from a word, and the bitwise operators are just the tool for this.

Let’s consider another example. Suppose that you need to record information about fonts. You might want to store information about the style and the size of each font, plus whether it’s bold or italic. You could pack all of this information into a 2-byte integer variable, as shown in Figure 3-1.

Figure 3-1.Packing font data into 2 bytes

You could use 1 bit to record whether the font is italic—a 1 value signifies italic and 0 signifies normal. In the same way, another bit could be used to specify whether the font is bold. You could use a byte to select one of up to 256 different styles. With another five bits, you could record the point size up to 32. Thus, in one 16-bit word you have four separate pieces of data recorded. The bitwise operators provide you with the means of accessing and modifying the individual bits and groups of bits from an integer very easily so they provide you with the means of assembling and disassembling the 16-bit word.

The Bitwise Shift Operators

The bitwise shift operators shift the contents of an integer variable by a specified number of bits to the left or right. These are used in combination with the other bitwise operators to achieve the kind of operations I described previously. The >> operator shifts bits to the right, and the << operator shifts bits to the left. Bits that fall off either end of the variable are lost.

All the bitwise operations work with integers of any type, but you’ll use 16-bit words in this chapter’s examples, which should keep the illustrative diagrams simple. You can declare and initialize a variable called number with the statement

unsigned short number = 16387U;

As you saw in the last chapter, you should write unsigned literals with a letter U or u appended to the number.

You can shift the contents of this variable and store the result with the statement

unsigned short result = number << 2; //Shift left two bit positions

The left operand of the shift operator is the value to be shifted and the right operand specifies the number of bit positions that the value is to be shifted by. Figure 3-2 shows the effect of the operation.

Figure 3-2.Shift operations

As you can see from Figure 3-2, shifting the value 16,387 two positions to the left produces the value 12. The rather drastic change in the value is the result of losing the high order bit.

To shift the value to the right, you can write

result = number >> 2; // Shift right two bit positions

This shifts the value 16,387 two positions to the right and produces the result 4,096. Shifting right two bits is effectively dividing the value by 4.

As long as bits aren’t lost, shifting nbits to the left is equivalent to multiplying the value by 2, n times. In other words, it’s equivalent to multiplying by 2n. Similarly, shifting right n bits is equivalent to dividing by 2n. But beware: As you saw with the left shift of the variable number, if significant bits are lost, the result is nothing like what you would expect. However, this is no different from the “real” multiply operation. If you multiplied the 2-byte number by 4 you would get the same result, so shifting left and multiplying are still equivalent. The incorrect answer arises because the value of the result of the multiplication is outside the range of a 2-byte integer.

When you want to modify the original value using a shift operation, you can do so by using an op= assignment operator. In this case, you would use the >>= or <<= operator, for eexample:

number >>= 2; // Shift contents of number two positions to the right

This is equivalent to

number = number >> 2; // Shift contents of number two positions to the right

You might imagine that confusion could arise between these shift operators and the insertion and extraction operators that you’ve been using for input and output. As far as the compiler is concerned, the meaning will generally be clear from the context. If it isn’t, in most cases the compiler will generate a message, but you do need to be careful. For example, if you want to output the result of shifting a variable number left by two bits, you could write

cout << (number << 2);

Here, the parentheses are essential. Without them, the compiler will interpret the shift operator as a stream insertion operator so you won’t get the result that you intended.

Shifting Signed Integers

You can apply the bitwise shift operators to both signed and unsigned integers. However, the effect of the right shift operator on signed integer types can vary between different systems, and it depends on your compiler’s implementation. In some cases, the right shift will introduce “0” bits at the left to fill the vacated bit positions. In other cases, the sign bit is propagated to the right, so “1” bits fill the vacated bit positions to the left.

The reason for propagating the sign bit, where this occurs, is to maintain consistency between a right shift and a divide operation. You can illustrate this with a variable of type char, just to show how it works. Suppose you define value to be of type signed char with the value –104 in decimal:

signed char value = -104;

Its binary value is 10011000. You can shift it two bits to the right with this operation:

value >>= 2; // Result 11100110

The binary result when the sign is propagated is shown in the comment. Two 0s are shifted out at the right end, and because the sign bit is 1, further 1s are inserted on the left. The decimal value of the result is –26, which is the same as if you had divided by 4, as you would expect. With operations on unsigned integer types, of course, the sign bit isn’t propagated and 0s are inserted on the left.

As I said, what actually happens when you right-shift negative integers is implementation defined, so you must not rely on it working one way or the other. Because for the most part you’ll be using these operators for operating at the bit level—where maintaining the integrity of the bit pattern is important—you should always use unsigned integers to ensure that you avoid the high-order bit being propagated.

Logical Operations on Bit Patterns

The four bitwise operators that you can use to modify bits in an integer value are shown in Table 3-1.

Table 3-1. Bitwise Operators

Operator

Description

~

This is the bitwise complement operator. This is a unary operator that will

invert the bits in its operand, so 1 becomes 0 and 0 becomes 1.

&

This is the bitwise AND operator, which will AND the corresponding bits in its

operands. If the corresponding bits are both 1, then the resulting bit is 1.

Otherwise, it’s 0.

^

This is the bitwise exclusive OR operator, which will exclusive-OR the

corresponding bits in its operands. If the corresponding bits are different (that

is, one is 1 and the other is 0), then the resulting bit is 1. If the corresponding

bits are the same, the resulting bit is 0.

|

This is the bitwise OR operator, which will OR the corresponding bits in its

operands. If either of the two corresponding bits is 1, then the result is 1. If both

bits are 0, then the result is 0.

The operators appear here in order of precedence, so the bitwise complement operator has the highest precedence in this set, and the bitwise OR operator the lowest. As you can see in the complete operator precedence table in Appendix D, the shift operators << and >> are of equal precedence, and they’re below the ~ operator but above the & operator.

If you haven’t come across operators like these before, you’re likely to be thinking, “Very interesting, but what are they for?” Let’s put them into some kind of context.

This article is excerpted from Beginning ANSI C++ The Complete Language by Ivor Horton (Apress, 2004; ISBN 1590592271). Check it out at your favorite bookstore today. Buy this book now.