One of the hallmarks of C++ is that you can define a type
that resembles any built-in type. Thus, if you need to define a type
that supports arbitrary-sized integers—call it bigint
—you can do so, and programmers will be
able to use bigint
objects the same
way they use int
objects.
You can define a brand new type by defining a class (see Chapter 6) or an enumeration (see Section 2.5.2 later in this
chapter). In addition to declaring and defining new types, you can
declare a typedef
, which is a synonym
for an existing type. Note that while the name typedef
seems to be a shorthand for "type
definition," it is actually a type declaration. (See Section 2.5.4 later in this
chapter.)
This section lists the fundamental type specifiers that are built into the C++
language. For types that require multiple keywords (e.g., unsigned
long
int
), you can mix the keywords in any order,
but the order shown in the following list is the conventional order.
If a type specifier requires multiple words, one of which is int
, the int
can be omitted. If a type is signed
, the signed
keyword can be omitted (except in the
case of signed
char
).
bool
Represents a Boolean or logical value. It has two possible
values: true
and false
.
char
Represents a narrow character. Narrow character literals
usually have type char
. (If a
narrow character literal contains multiple characters, the type
is int
.) Unlike the other
integral types, a plain char
is not necessarily equivalent to signed
char
. Instead, char
can be equivalent to signed
char
or unsigned
char
, depending on the implementation.
Regardless of the equivalence, the plain char
type is distinct and separate
from signed
char
and unsigned
char
.
All the narrow character types (char
, signed
char
, and unsigned
char
) share a common size and
representation. By definition, char
is the smallest fundamental
type—that is, sizeof(char)
is
1
.
double
Represents a double-precision, floating-point number. The
range and precision are at least as much as those of float
. A floating-point literal has
type double
unless you use
the F
or L
suffix.
float
Represents a single-precision, floating-point number.
long double
Represents an extended-precision, floating-point number.
The range and precision are at least as much as those of
double
.
signed char
Represents a signed byte.
signed int
Represents an integer in a size and format that is natural for the host environment.
signed long int
Represents an integer whose range is at least as large as
that of int
.
signed short int
Represents an integer such that the range of an int
is at least as large as the range
of a short
.
unsigned char
Represents an unsigned byte. Some functions, especially in
the C library, require characters and character strings to be
cast to unsigned
char
instead of plain char
. (See Chapter 13 for details.)
unsigned long int
Represents an unsigned long integer.
unsigned short int
Represents an unsigned short integer.
void
Represents the absence of any values. You cannot declare
an object of type void
, but
you can declare a function that "returns" void
(that is, does not return a
value), or declare a pointer to void
, which can be used as a generic
pointer. (See static_cast<>
under Section 3.5.2 for
information about casting to and from a pointer to void
.)
wchar_t
Represents a wide character. Its representation must match
one of the fundamental integer types. Wide character literals
have type wchar_t
.
The representations of the fundamental types are
implementation-defined. The integral types (bool
, char
, wchar_t
, int
, etc.) each require a binary
representation: signed-magnitude, ones' complement, or two's
complement. Some types have alignment restrictions, which are also
implementation-defined. (Note that new
expressions always return pointers that
are aligned for any type.)
The unsigned
types always use
arithmetic modulo 2n, in which n
is the number of bits in the type.
Unsigned types take up the same amount of space and have the same
alignment requirements as their signed companion types. Nonnegative
signed values are always a subset of the values supported by the
equivalent unsigned type and must have the same bit representations as
their corresponding unsigned values.
Although the size and range of the fundamental types is
implementation-defined, the C++ standard mandates minimum
requirements for these types. These requirements are specified in
<climits>
(for the integral
types) and <cfloat>
(for
the floating-point types). See also <limits>
, which declares templates
for obtaining the numerical properties of each fundamental
type.
An enumerated type declares an optional type name (the enumeration ) and a set of zero or more identifiers (enumerators ) that can be used as values of the type. Each enumerator is a constant whose type is the enumeration. For example:
enum logical { no, maybe, yes }; logical is_permitted = maybe; enum color { red=1, green, blue=4 }; const color yellow = static_cast<color>(red | green); enum zeroes { a, b = 0, c = 0 };
You can optionally specify the integral value of an enumerator
after an equal sign (=
). The value
must be a constant of integral or enumerated type. The default value
of the first enumerator is 0
. The
default value for any other enumerator is one more than the value of
its predecessor (regardless of whether that value was explicitly
specified). In a single enumeration declaration, you can have more
than one enumerator with the same value.
The name of an enumeration is optional, but without a name you cannot declare use the enumeration in other declarations. Such a declaration is sometimes used to declare integer constants such as the following:
enum { init_size = 100 }; std::vector<int> data(init_size);
An enumerated type is a unique type. Each enumerated value has a
corresponding integer value, and the enumerated value can be promoted
automatically to its integer equivalent, but integers cannot be
implicitly converted to an enumerated type. Instead, you can use
static_cast<>
to cast an
integer to an enumeration or cast a value from one enumeration to a
different enumeration. (See Chapter
3 for details.)
The range of values for an enumeration is defined by the
smallest and largest bitfields that can hold all of its enumerators.
In more precise terms, let the largest and smallest values of the
enumerated type be vmin
and vmax . The largest
enumerator is emax and
the smallest is emin .
Using two's complement representation (the most common integer
format), vmax is the
smallest 2 n - 1,
such that vmax ≥
max(abs( e
min ) - 1, abs( e
max
)). If e
min is not negative, v
min
= 0; otherwise, v
min = -( v
max
+ 1).
In other words, the range of values for an enumerated type can be larger than the range of enumerator values, but the exact range depends on the representation of integers on the host platform, so it is implementation-defined. All values between the largest and smallest enumerators are always valid, even if they do not have corresponding enumerators.
In the following example, the enumeration sign
has the range (in two's complement) -2
to 1. Your program might not assign any meaning to static_cast<sign>(-2)
, but it is
semantically valid in a program:
enum sign { neg=-1, zero=0, pos=1 };
Each enumeration has an underlying integral type that can store all of the enumerator values. The actual underlying type is implementation-defined, so the size of an enumerated type is likewise implementation-defined.
The standard library has a type called iostate
, which might be implemented as an
enumeration. (Other implementations are also possible; see <ios>
in Chapter 13 for details.) The
enumeration has four enumerators, which can be used as bits in a
bitmask and combined using bitwise operators:
enum iostate { goodbit=0, failbit=1, eofbit=2, badbit=4 };
The iostate
enumeration can
clearly fit in a char
because the
range of values is 0
to 7
, but the compiler is free to use int
, short
, char
, or the unsigned
flavors of these types as the
underlying type.
Because enumerations can be promoted to integers, any arithmetic
operation can be performed on enumerated values, but the result is
always an integer. If you want to permit certain operations that
produce enumeration results, you must overload the operators for your
enumerated type. For example, you might want to overload the bitwise
operators, but not the arithmetic operators, for the iostate
type in the preceding example. The
sign
type does not need any
additional operators; the comparison operators work just fine by
implicitly converting sign
values
to integers. Other enumerations might call for overloading ++
and --
operators (similar to the succ
and pred
functions in Pascal).
Example 2-10 shows how
operators can be overloaded for enumerations.
Example 2-10. Overloading operators for enumerations
// Explicitly cast to int, to avoid infinite recursion. inline iostate operator|(iostate a, iostate b) { return iostate(int(a) | int(b)); } inline iostate& operator|=(iostate& a, iostate b) { a = a | b; return a; } // Repeat for &, ^, and ~. int main( ) { iostate err = goodbit; if (error( )) err |= badbit; }
POD is short for Plain Old Data. The fundamental types and
enumerated types are POD types, as are pointers and arrays of POD types. You
can declare a POD class, which is a class
or struct
that uses only POD types for its
nonstatic data members. A POD union is a union
of POD types.
POD types are special in several ways:
A POD object can be copied byte for byte and retain its
value. In particular, a POD object can be safely copied to an
array of char
or unsigned
char
, and when copied back, it retains
its original value. A POD object can also be copied by calling
memcpy
; the copy has the same
value as the original.
A local POD object without an initializer is uninitialized,
that is, its value is undefined. Similarly, a POD type in a
new
expression without an
initializer is uninitialized. (A non-POD class is initialized in
these situations by calling its default constructor.) When
initialized with an empty initializer, a POD object is initialized
to 0
, false
, or a null pointer.
A goto
statement can
safely branch across declarations of uninitialized POD objects.
(See Chapter 4 for more
information.)
A POD object can be initialized using an aggregate initializer. (See Section 2.6.3 later in this chapter for details.)
See Chapter 6 for more information about POD types, especially POD classes.
A typedef
declares a synonym for an existing type. Syntactically,
a typedef
is like declaring a
variable, with the type name taking the place of the variable name,
and with a typedef
keyword out in
front. More precisely, typedef
is a
specifier in a declaration, and it must be combined with type
specifiers and optional const
and
volatile
qualifiers (called
cv-qualifiers), but no storage class specifiers.
A list of declarators follow the specifiers. (See the next section,
Section 2.6, for more
information about cv qualifiers, storage class
specifiers, and declarators.)
The declarator of a typedef
declaration is similar to that for an object declaration, except you
cannot have an initializer. The following are some examples of
typedef
declarations:
typedef unsigned intuint
; typedef long int *long_ptr
; typedef double matrix[3][3]; typedef void (*thunk
)( ); typedef signed charschar
;
By convention, the typedef
keyword appears before the type specifiers. Syntactically, typedef
behaves as a storage class specifier
(see Section 2.6.1 later
in this chapter for more information about storage class specifiers)
and can be mixed in any order with other type specifiers. For example,
the following typedef
s are
identical and valid:
typedef unsigned longulong
; // Conventional long typedef int unsignedulong
; // Valid, but strange
A typedef
is especially
helpful with complicated declarations, such as function pointer
declarations and template instantiations. They help the author who
must concoct the declarations, and they help the reader who must later
tease apart the morass of parentheses, asterisks, and angle brackets.
The standard library uses them frequently. (See also Example 2-11.)
typedef std::basic_string<char, std::char_traits<char> >string
;
A typedef
does not create a
new type the way class
and enum
do. It simply declares a new name for
an existing type. Therefore, function declarations for which the
parameters differ only as typedef
s
are not actually different declarations. The two function declarations
in the following example are really two declarations of the same
function:
typedef unsigned int uint; uint func(uint); // Two declarations of the unsigned func(unsigned); // same function
Similarly, because you cannot overload an operator on
fundamental types, you cannot overload an operator on typedef
synonyms for fundamental types. For
example, both the following attempts to overload +
result in an error:
int operator+(int, int); // Error typedef int integer; integer operator+(integer, integer); // Error
C programmers are accustomed to declaring typedef
s for struct
, union
, and enum
declarations, but such typedef
s are not necessary in C++. In C, the
struct
, union
, and enum
names are separate from the type names,
but in C++, the declaration of a struct
, union
, class
, or enum
adds the type to the type names.
Nonetheless, such a typedef
is
harmless. The following example shows typedef
being used to create the synonym
point
for the struct point
:
struct point { int x, y; } typedef struct point point; // Not needed in C++, but harmless point pt;
An elaborated type specifier begins with an introductory keyword: class
, enum
, struct
, typename
, or union
. The
keyword is followed by a (possibly) qualified name of a suitable type.
That is, enum
is followed by the
name of an enumeration, class
is
followed by a class
name, struct
by a struct
name, and union
by a union
name. The typename
keyword is used only in templates
(see Chapter 7) and is followed
by a name that must be a type name in a template instantiation.
A typename
-elaborated type
specifier is often needed in template definitions. The other
elaborated type specifiers tend to be used in headers that must be
compatible with C or in type names that have been hidden by other
names. For example:
enum color { black, red }; color x; // No need for elaborated name enum color color( ); // Function hides color enum color c = color( ); // Elaborated name is needed here