You’re reading Ry’s Objective-C Tutorial → Data Types |
C Primitives
The vast majority of Objective-C’s primitive data types are adopted from C, although it does define a few of its own to facilitate its object-oriented capabilities. The first part of this module provides a practical introduction to C’s data types, and the second part covers three more primitives that are specific to Objective-C.
The examples in this module use NSLog()
to inspect variables.
In order for them to display correctly, you need to use the correct format
specifier in NSLog()
’s first argument. The various
specifiers for C primitives are presented alongside the data types themselves,
and all Objective-C objects can be displayed with the %@
specifier.
The void Type
The void
type is C’s empty data type. Its most common use
case is to specify the return type for functions that don’t return anything. For
example:
void
sayHello
()
{
NSLog
(
@"This function doesn't return anything"
);
}
The void
type is not to be confused with the void pointer. The former
indicates the absence of a value, while the latter represents
any value (well, any pointer, at least).
Integer Types
Integer data types are characterized by their size and whether they are
signed or unsigned. The char
type is always 1 byte, but it’s
very important to understand that the exact size of the other integer types is
implementation-dependent. Instead of being defined as an absolute
number of bytes, they are defined relative to each other. The only guarantee
is that short <= int <= long <= long long
; however it is
possible to determine their exact sizes
at runtime.
C was designed to work closely with the underlying architecture, and
different systems support different variable sizes. A relative definition
provides the flexibility to, for example, define short
,
int
, and long
as the same number of bytes when the
target chipset can’t differentiate between them.
BOOL
isBool
=
YES
;
NSLog
(
@"%d"
,
isBool
)
;
NSLog
(
@"%@"
,
isBool
?
@"YES"
:
@"NO"
)
;
char
aChar
=
'a'
;
unsigned
char
anUnsignedChar
=
255
;
NSLog
(
@"The letter %c is ASCII number %hhd"
,
aChar
,
aChar
)
;
NSLog
(
@"%hhu"
,
anUnsignedChar
)
;
short
aShort
=
-
32768
;
unsigned
short
anUnsignedShort
=
65535
;
NSLog
(
@"%hd"
,
aShort
)
;
NSLog
(
@"%hu"
,
anUnsignedShort
)
;
int
anInt
=
-
2147483648
;
unsigned
int
anUnsignedInt
=
4294967295
;
NSLog
(
@"%d"
,
anInt
)
;
NSLog
(
@"%u"
,
anUnsignedInt
)
;
long
aLong
=
-
9223372036854775808
;
unsigned
long
anUnsignedLong
=
18446744073709551615
;
NSLog
(
@"%ld"
,
aLong
)
;
NSLog
(
@"%lu"
,
anUnsignedLong
)
;
long
long
aLongLong
=
-
9223372036854775808
;
unsigned
long
long
anUnsignedLongLong
=
18446744073709551615
;
NSLog
(
@"%lld"
,
aLongLong
)
;
NSLog
(
@"%llu"
,
anUnsignedLongLong
)
;
The %d
and %u
characters are the core specifiers
for displaying signed and unsigned integers, respectively. The hh
,
h
, l
and ll
characters are modifiers
that tell NSLog()
to treat the associated integer as a
char
, short
, long
, or long
long
, respectively.
It’s also worth noting that the BOOL
type is actually
part of Objective-C, not C. Objective-C uses YES
and
NO
for its Boolean values instead of the true
and
false
macros used by C.
Fixed-Width Integers
While the basic types presented above are satisfactory for most purposes, it
is sometimes necessary to declare a variable that stores a specific number of
bytes. This is particularly relevant for algorithms like arc4random()
that operate on a fixed-width integer.
The int<n>_t
data types allow you to represent signed and
unsigned integers that are exactly 1, 2, 4, or 8 bytes, and the
int_least<n>_t
variants let you constrain the
minimum size of a variable without specifying an exact number of
bytes. In addition, intmax_t
is an alias for the largest integer
type that the system can handle.
// Exact integer types
int8_t
aOneByteInt
=
127
;
uint8_t
aOneByteUnsignedInt
=
255
;
int16_t
aTwoByteInt
=
32767
;
uint16_t
aTwoByteUnsignedInt
=
65535
;
int32_t
aFourByteInt
=
2147483647
;
uint32_t
aFourByteUnsignedInt
=
4294967295
;
int64_t
anEightByteInt
=
9223372036854775807
;
uint64_t
anEightByteUnsignedInt
=
18446744073709551615
;
// Minimum integer types
int_least8_t
aTinyInt
=
127
;
uint_least8_t
aTinyUnsignedInt
=
255
;
int_least16_t
aMediumInt
=
32767
;
uint_least16_t
aMediumUnsignedInt
=
65535
;
int_least32_t
aNormalInt
=
2147483647
;
uint_least32_t
aNormalUnsignedInt
=
4294967295
;
int_least64_t
aBigInt
=
9223372036854775807
;
uint_least64_t
aBigUnsignedInt
=
18446744073709551615
;
// The largest supported integer type
intmax_t
theBiggestInt
=
9223372036854775807
;
uintmax_t
theBiggestUnsignedInt
=
18446744073709551615
;
Floating-Point Types
C provides three floating-point types. Like the integer data types, they are
defined as relative sizes, where float <= double <= long
double
. Literal decimal values are represented as doubles—floats
must be explicitly marked with a trailing f
, and long doubles must
be marked with an L
, as shown below.
// Single precision floating-point
float
aFloat
=
-
21.09f
;
NSLog
(
@"%f"
,
aFloat
)
;
NSLog
(
@"%8.2f"
,
aFloat
)
;
// Double precision floating-point
double
aDouble
=
-
21.09
;
NSLog
(
@"%8.2f"
,
aDouble
)
;
NSLog
(
@"%e"
,
aDouble
)
;
// Extended precision floating-point
long
double
aLongDouble
=
-
21.09e8L
;
NSLog
(
@"%Lf"
,
aLongDouble
)
;
NSLog
(
@"%Le"
,
aLongDouble
)
;
The %f
format specifier is used to display floats and doubles
as decimal values, and the %8.2f
syntax determines the padding and
the number of points after the decimal. In this case, we pad the output to fill
8 digits and display 2 decimal places. Alternatively, you can format the value
as scientific notation with the %e
specifier. Long doubles require
the L
modifier (similar to hh
, l
,
etc).
Determining Type Sizes
It’s possible to determine the exact size of any data type by passing
it to the sizeof()
function, which returns the number of bytes
used to represent the specified type. Running the following snippet is an easy
way to see the size of the basic data types on any given architecture.
NSLog
(
@"Size of char: %zu"
,
sizeof
(
char
))
;
// This will always be 1
NSLog
(
@"Size of short: %zu"
,
sizeof
(
short
))
;
NSLog
(
@"Size of int: %zu"
,
sizeof
(
int
))
;
NSLog
(
@"Size of long: %zu"
,
sizeof
(
long
))
;
NSLog
(
@"Size of long long: %zu"
,
sizeof
(
long
long
))
;
NSLog
(
@"Size of float: %zu"
,
sizeof
(
float
))
;
NSLog
(
@"Size of double: %zu"
,
sizeof
(
double
))
;
NSLog
(
@"Size of size_t: %zu"
,
sizeof
(
size_t
))
;
Note that sizeof()
can also be used with an array, in which
case it returns the number of bytes used by the array. This presents a new
problem: the programmer has no idea which data type is required to store the
maximum size of an array. Instead of forcing you to guess, the
sizeof()
function returns a special data type called
size_t
. This is why we used the %zu
format specifier
in the above example.
The size_t
type is dedicated solely to representing
memory-related values, and it is guaranteed to be able to store the maximum
size of an array. Aside from being the return type for sizeof()
and other memory utilities, this makes size_t
an appropriate data
type for storing the indices of very large arrays. As with any other type, you
can pass it to sizeof()
to get its exact size at runtime, as shown
in the above example.
If your Objective-C programs interact with a lot of C libraries,
you’re likely to encounter the following application of
sizeof()
:
size_t
numberOfElements
=
sizeof
(
anArray
)
/
sizeof
(
anArray
[
0
]);
This is the canonical way to determine the number of elements in a
primitive C array. It simply divides the size of the array,
sizeof(anArray)
, by the size of each
element, sizeof(anArray[0])
.
Limit Macros
While it’s trivial to determine the potential range of an integer type once you know how how many bytes it is, C implementations provide convenient macros for accessing the minimum and maximum values that each type can represent:
NSLog
(
@"Smallest signed char: %d"
,
SCHAR_MIN
)
;
NSLog
(
@"Largest signed char: %d"
,
SCHAR_MAX
)
;
NSLog
(
@"Largest unsigned char: %u"
,
UCHAR_MAX
)
;
NSLog
(
@"Smallest signed short: %d"
,
SHRT_MIN
)
;
NSLog
(
@"Largest signed short: %d"
,
SHRT_MAX
)
;
NSLog
(
@"Largest unsigned short: %u"
,
USHRT_MAX
)
;
NSLog
(
@"Smallest signed int: %d"
,
INT_MIN
)
;
NSLog
(
@"Largest signed int: %d"
,
INT_MAX
)
;
NSLog
(
@"Largest unsigned int: %u"
,
UINT_MAX
)
;
NSLog
(
@"Smallest signed long: %ld"
,
LONG_MIN
)
;
NSLog
(
@"Largest signed long: %ld"
,
LONG_MAX
)
;
NSLog
(
@"Largest unsigned long: %lu"
,
ULONG_MAX
)
;
NSLog
(
@"Smallest signed long long: %lld"
,
LLONG_MIN
)
;
NSLog
(
@"Largest signed long long: %lld"
,
LLONG_MAX
)
;
NSLog
(
@"Largest unsigned long long: %llu"
,
ULLONG_MAX
)
;
NSLog
(
@"Smallest float: %e"
,
FLT_MIN
)
;
NSLog
(
@"Largest float: %e"
,
FLT_MAX
)
;
NSLog
(
@"Smallest double: %e"
,
DBL_MIN
)
;
NSLog
(
@"Largest double: %e"
,
DBL_MAX
)
;
NSLog
(
@"Largest possible array index: %llu"
,
SIZE_MAX
)
;
The SIZE_MAX
macro defines the maximum value that can be stored
in a size_t
variable.
Working With C Primitives
This section takes a look at some common “gotchas“ when working with C’s primitive data types. Keep in mind that these are merely brief overviews of computational topics that often involve a great deal of subtlety.
Choosing an Integer Type
The variety of integer types offered by C can make it hard to know which one
to use in any given situation, but the answer is quite simple:
use int
’s unless you have a compelling reason not to.
Traditionally, an int
is defined to be the native word size of
the underlying architecture, so it’s (generally) the most efficient
integer type. The only reason to use a short
is when you want to
reduce the memory footprint of very large arrays (e.g., an OpenGL index
buffer). The long
types should only be used when you need to store
values that don’t fit into an int
.
Integer Division
Like most programming languages, C differentiates between integer and floating-point operations. If both operands are integers, the calculation uses integer arithmetic, but if at least one of them is a floating-point type, it uses floating-point arithmetic. This is important to keep in mind for division:
int
integerResult
=
5
/
4
;
NSLog
(
@"Integer division: %d"
,
integerResult
)
;
// 1
double
doubleResult
=
5.0
/
4
;
NSLog
(
@"Floating-point division: %f"
,
doubleResult
)
;
// 1.25
Note that the decimal will always be truncated when dividing two
integers, so be sure to use (or cast to) a float
or a
double
if you need the remainder.
Floating-Point Equality
Floating-point numbers are inherently not precise, and certain
values simply cannot be represented as a floating-point value. For example, we
can inspect the imprecision of the number 0.1
by displaying
several decimal places:
NSLog
(
@"%.17f"
,
.1
)
;
// 0.10000000000000001
As you can see, 0.1
is not actually represented as
0.1
by your computer. That extra 1
in the 17th digit
occurs because converting 1/10
to binary results in a repeating
decimal. Of course, a computer cannot store the infinitely many digits required
for the exact value, so this introduces a rounding error. The error gets
magnified when you start doing calculations with the value, resulting in
counterintuitive situations like the following:
NSLog
(
@"%.17f"
,
4.2
-
4.1
)
;
// 0.10000000000000053
if
(
4.2
-
4.1
==
.1
)
{
NSLog
(
@"This math is perfect!"
);
}
else
{
// You'll see this message
NSLog
(
@"This math is just a tiny bit off..."
);
}
The lesson here is: don’t try to check if two floating-point values
are exactly equal, and definitely don’t use a floating-point type to
store precision-sensitive data (e.g., monetary values). To represent exact
quantities, you should use the fixed-point NSDecimalNumber
class.
For a comprehensive discussion of the issues surrounding floating-point math, please see What Every Computer Scientist Should Know About Floating-Point Arithmetic by David Goldberg.
Objective-C Primitives
In addition to the data types discussed above, Objective-C defines three of
its own primitive types: id
, Class
, and
SEL
. These are the basis for Objective-C’s dynamic typing
capabilities. This section also introduces the anomalous NSInteger
and NSUInteger
types.
The id Type
The id
type is the generic type for all Objective-C objects.
You can think of it as the object-oriented version of C’s void pointer.
And, like a void pointer, it can store a reference to any type of
object. The following example uses the same id
variable to hold a
string and a dictionary.
id
mysteryObject
=
@"An NSString object"
;
NSLog
(
@"%@"
,
[
mysteryObject
description
])
;
mysteryObject
=
@{
@"model"
:
@"Ford"
,
@"year"
:
@1967
};
NSLog
(
@"%@"
,
[
mysteryObject
description
])
;
Recall that all Objective-C objects are referenced as pointers, so when they
are statically typed, they must be declared with pointer notation:
NSString *mysteryObject
. However, the id
type
automatically implies that the variable is a pointer, so this is not necessary:
id mysteryObject
(without the asterisk).
The Class Type
Objective-C classes are represented as objects themselves, using a special
data type called Class
. This lets you, for example, dynamically
check an object’s type at runtime:
Class
targetClass
=
[
NSString
class
];
id
mysteryObject
=
@"An NSString object"
;
if
([
mysteryObject
isKindOfClass:
targetClass
])
{
NSLog
(
@"Yup! That's an instance of the target class"
);
}
All classes implement a class-level method called class
that
returns its associated class object (apologies for the redundant terminology).
This object can be used for introspection, which we see with the
isKindOfClass:
method above.
The SEL Type
The SEL
data type is used to store selectors, which are
Objective-C’s internal representation of a method name. For example, the
following snippet stores a method called sayHello
in the
someMethod
variable. This variable could be used to dynamically
call a method at runtime.
SEL
someMethod
=
@selector
(
sayHello
);
Please refer to the Methods module for a thorough discussion of Objective-C’s selectors.
NSInteger and NSUInteger
While they aren’t technically native Objective-C types, this is a good
time to discuss the Foundation
Framework’s custom integers, NSInteger
and
NSUInteger
. On 32-bit systems, these are defined to be 32-bit
signed/unsigned integers, respectively, and on 64-bit systems, they are 64-bit
integers. In other words, they are guaranteed to be the natural word size on
any given architecture.
The original purpose of these Apple-specific types was to facilitate the
transition from 32-bit architectures to 64-bit, but it’s up to you
whether or not you want to use NSInteger
over the basic types
(int
, long
, long long
) and the
int<n>_t
variants. A sensible convention is to use
NSInteger
and NSUInteger
when interacting with
Apple’s APIs and use the standard C types for everything else.
Either way, it’s still important to understand what
NSInteger
and NSUInteger
represent, as they are used
extensively in Foundation, UIKit,
and several other frameworks.
Be sure to check out Ry’s Cocoa Tutorial. This brand new guide is a complete walkthrough of Mac App development, and it leverages all of the Objective-C skills that we just discussed. Learn more › |
Mailing List
Sign up for my low-volume mailing list to find out when new content is released. Next up is a comprehensive Swift tutorial planned for late January.
You’ll only receive emails when new tutorials are released, and your contact information will never be shared with third parties. Click here to unsubscribe.