Objective-C Variables and Types: Object Pointers, id, Class, SEL, BOOL, and Cocoa's Portable Numeric Types
Understand how every C primitive type remains fully usable in Objective-C, that every object variable is a pointer, and learn the meaning and purpose of Objective-C's distinctive types: id, Class, SEL, BOOL, NSInteger/NSUInteger, and CGFloat — including why architecture-dependent types exist at all.
This lesson is dense by design — it's a reference you'll come back to repeatedly as you read real Objective-C and Cocoa code, because these type names appear in virtually every method signature, property declaration, and variable you'll write from here forward. The good news is that nothing here replaces what you already know about C types; Objective-C simply adds a new layer of types on top, purpose-built for working with objects and for writing code that behaves correctly whether it's compiled for a 32-bit or 64-bit processor. Understanding what each of these types actually is, and why it exists, will make the rest of this course — and every piece of Apple's own documentation you read afterward — far easier to follow.
C's Primitive Types Still Work Exactly as Before
Because Objective-C is a strict superset of C, every primitive type you learned in Module 1 — int, float, double, char, long, unsigned int, _Bool/bool, and every variant of them — remains completely valid, unchanged Objective-C. int age = 25; is exactly as legal, and behaves exactly the same way, in an Objective-C .m file as it does in a plain C .c file. Objective-C does not replace or reinterpret any of C's primitive numeric types; it only adds new types on top for working with objects and with the object-messaging system covered in the previous lesson.
This matters because it means you're never starting over: everything about declaration, initialization, scope, and storage duration from Module 1 carries forward directly. What's new in Objective-C is a second category of types entirely — types that exist specifically to represent objects, classes, and method names, none of which have any real equivalent in plain C. The rest of this lesson works through that second category, type by type.
• Every C primitive type (int, float, double, char, long, unsigned variants, _Bool/bool) remains fully valid, unchanged Objective-C. • Objective-C does not reinterpret C's primitives — it adds new types on top, specifically for objects and messaging. • Everything from Module 1 about declaration, scope, and storage duration continues to apply exactly as before.
Object Pointers: Every Object Variable Is a Pointer
In Objective-C, you never hold an object directly in a variable the way you hold an int directly — you always hold a pointer to it, and this is not optional or occasional; it is how every single object variable works, without exception. A variable that refers to an NSObject (the base class most Objective-C objects ultimately descend from, previewed here and covered fully in the Foundation module) is declared as NSObject *someObject; — note the asterisk, exactly the pointer syntax you'd use for a pointer to any C type, except here it's mandatory for objects rather than optional.
Why pointers, always? Objects in Objective-C are allocated dynamically (on the heap, a concept covered in the Memory module later), and their size and internal layout can vary, grow, or even change identity (through subclassing) in ways a fixed-size stack variable cannot accommodate. A pointer is a fixed-size value (an address) regardless of how large or complex the object it points to actually is, which is exactly what makes it possible to pass objects around, store them in collections, and manage their lifetime uniformly, no matter which specific class they belong to. You'll see this pattern in literally every object type this course introduces from here forward: NSString *, NSArray *, NSDictionary *, and your own custom classes — always followed by an asterisk, always a pointer, never the bare class name alone.
This is worth stating explicitly because it trips up beginners coming from languages where object variables don't visibly show pointer syntax: in Objective-C, writing NSObject someObject; (without the asterisk) is not just unconventional — it does not mean what a beginner might expect, and in practice is essentially never what you want, because it's asking for a variable of the object's exact structural type rather than a pointer to a heap-allocated instance of it. The asterisk is not decoration; it's required, load-bearing syntax.
• Every object variable in Objective-C is a pointer: NSObject *someObject;, NSString *name;, and so on — always with an asterisk. • Objects are heap-allocated with variable size/layout, which is exactly why a fixed-size pointer (an address) is the uniform way to refer to any of them. • This pattern is universal across every object class you'll meet in this course, not just NSObject. • Omitting the asterisk on an object variable is essentially always a mistake, not a valid stylistic alternative.
id: the Generic, Dynamically-Typed Object Pointer
id is one of Objective-C's most distinctive types: a generic pointer that can point to an object of any class whatsoever, without declaring up front which specific class it is. id thing = someObject; compiles regardless of what class someObject actually is, because id deliberately erases that specific-class information at the type level — as far as the compiler is concerned, a variable typed id could be pointing to an NSString, an NSArray, a Person you wrote yourself, or anything else.
This is genuinely different from C, where every pointer's type (int *, char *) tells the compiler exactly what it points to and lets it type-check your usage of that pointer thoroughly. With id, the compiler defers almost all of that checking — you can send id thing essentially any message, and the compiler will generally let it through, trusting that the actual object at runtime will know how to respond (or produce a runtime error if it doesn't). This is precisely why id is described as dynamically typed: the real type is determined by what object the variable actually holds while the program runs, not by anything fixed at compile time.
id is genuinely useful — for collections that can hold objects of mixed classes, for APIs (like many delegate and callback patterns) that intentionally don't want to commit to one specific class, and as the natural return type for a factory-style method that might hand back different subclasses. But it also gives up compile-time safety: a typo'd selector sent to an id-typed variable, one that no object actually implements, often won't be caught until the program is running and that exact line executes, unlike a statically-typed pointer where the compiler can check selector validity against the specific declared class ahead of time.
• id — a generic pointer that can point to any object of any class, with no asterisk needed (it's already a pointer type). • id thing = someObject; compiles no matter what class someObject actually is — the compiler defers type checking. • Called dynamically typed because the real type is only known by looking at the actual object at runtime. • Trade-off: flexible for mixed-type collections and generic APIs, but gives up much of the compiler's compile-time selector checking.
Class: A Pointer to the Class Object Itself
Class is a distinct type from id, and it's easy to confuse the two at first: while id points to an instance of some class, Class points to the class itself — the runtime's representation of, say, NSString as a thing in its own right, separate from any particular string instance. Class personClass = [Person class]; retrieves the class object for Person and stores it in a Class-typed variable.
This matters because Objective-C's runtime treats classes as real, inspectable objects, not just as compile-time labels the way C treats a struct tag. Having an actual Class value lets you do runtime introspection — asking, while the program is running, 'what class is this object actually an instance of?', or 'does this class respond to this particular selector?', or even creating a new instance of a class you only know about by its Class value rather than by writing its name directly in your source. These capabilities are previewed here and covered in full depth in the Runtime module, where you'll see exactly how much dynamic, self-inspecting behavior this type of design enables.
For now, the key distinction to keep straight is: id = 'a pointer to some object instance, class unknown at compile time'; Class = 'a pointer to a class itself, used to work with or reason about the class as a thing in its own right, separately from any one instance of it.'
• Class — a pointer to a class object itself (not an instance), e.g. Class personClass = [Person class];. • Classes are real, runtime-inspectable objects in Objective-C, not just compile-time labels. • Class values enable runtime introspection and dynamic instance creation, covered fully in the Runtime module. • Keep id (an instance, unknown class) and Class (the class itself) clearly distinct in your mind.
SEL: An Opaque Identifier for a Method Name
The previous lesson introduced the term selector — the name identifying which method a message is asking for, like addNumber: or setName:age:. SEL is the actual C-level type used to store and pass around a selector as a value in its own right, separately from actually sending a message with it. SEL mySelector = @selector(sayHello); captures the selector for the sayHello method as a value you can store in a variable, compare, or pass to other code.
SEL is described as opaque because you're not meant to peer inside it or rely on its internal representation — it's simply a unique identifier the runtime uses to look up the right method implementation when a message with that selector is sent to some receiver. Having selectors as first-class values (rather than only ever writing them directly inside a message send's brackets) is what makes possible things like dynamically choosing which message to send based on some runtime condition, or asking an object 'do you actually respond to this selector?' before sending it — both patterns the Messaging module covers in full depth later in this course.
For now, simply recognize SEL on sight as 'this variable holds a selector — a method name as a value,' distinct from id (an object instance) and Class (a class itself). All three exist because Objective-C treats objects, classes, and method names as inspectable, storable, passable things, not just fixed syntax baked into your source code.
• SEL — an opaque type representing a selector (a method name) as a storable, passable value. • SEL mySelector = @selector(sayHello); — captures a selector without sending a message. • Opaque: you're not meant to inspect its internal representation, only use it as an identifier. • Forward-reference: enables dynamic message dispatch and 'does this object respond to this selector?' checks, covered fully in the Messaging module.
BOOL: Objective-C's Boolean, Historically a Signed char
Objective-C provides its own boolean type, BOOL, distinct from C's _Bool/bool covered in Module 1. Historically, BOOL was defined as a signed char — an 8-bit type — with two special constant values, YES (equivalent to 1) and NO (equivalent to 0), rather than C99's _Bool. BOOL isActive = YES; declares and initializes a boolean using Objective-C's own convention.
The reason to know this history, rather than just the surface syntax, is that it explains real behavioral quirks you'll encounter in existing Objective-C code: because BOOL was historically just a signed char under the hood, it could in principle hold values other than exactly 0 or 1 (any byte value), which occasionally produced surprising bugs when code assumed BOOL behaved with the strict two-value guarantee that C99's true _Bool provides. Modern Objective-C on 64-bit Apple platforms actually defines BOOL as C's real bool in many contexts today, but you will still constantly see YES/NO used as the idiomatic literals, and signed char-based BOOL remains extremely common in existing code, documentation, and 32-bit-era material you'll encounter.
The practical takeaway: always use YES and NO (not 1/0, and not C's true/false) when working with BOOL in Objective-C code, both because it's the idiomatic convention every Objective-C developer expects, and because it sidesteps any ambiguity about exactly how BOOL is defined on a given platform or SDK version.
• BOOL — Objective-C's own boolean type, historically defined as signed char, with constants YES (1) and NO (0). • Distinct from C's _Bool/bool, though modern platforms sometimes define BOOL in terms of real bool under the hood. • Because it was historically a signed char, BOOL could technically hold values other than exactly 0/1 — a known historical quirk. • Always use YES/NO (not 1/0 or true/false) with BOOL, for idiomatic clarity and platform-independence.
NSInteger and NSUInteger: Architecture-Portable Whole Numbers
NSInteger (signed) and NSUInteger (unsigned) are typedefs — alternate names for an existing type, not brand-new types — that resolve to different underlying C types depending on the architecture the code is compiled for: on 64-bit platforms, NSInteger is a long (8 bytes); on 32-bit platforms, it's an int (4 bytes). NSUInteger follows the same pattern with unsigned long versus unsigned int. NSInteger count = 42; looks and behaves like an ordinary whole-number variable, but its actual byte width silently adapts to whichever architecture compiles it.
Why do types like this exist at all, rather than everyone just using plain int everywhere? The answer is portability across 32-bit and 64-bit builds without silent truncation bugs. Consider code that stores something like an array index or a byte count as a plain int: on a 32-bit build that's a 4-byte value, matching the platform's natural pointer/register size, but on a 64-bit build, memory addresses and natural machine word sizes have grown to 8 bytes, and a plain 4-byte int can silently truncate a legitimately larger 64-bit value (like a very large collection count, or an offset into a huge block of data) — producing a wrong number rather than a compiler error, because the truncation is a perfectly legal (if lossy) conversion in C. NSInteger, by growing to match the platform's natural word size, sidesteps this class of bug: code written once, using NSInteger consistently, automatically gets the right-sized integer on whichever architecture it's compiled for, with no source-code change needed.
This is exactly why Cocoa APIs use NSInteger/NSUInteger pervasively instead of raw int — method return types, property types, parameter types for things like array counts and indices are declared with these portable typedefs throughout Apple's frameworks, and idiomatic Objective-C code follows the same convention for its own APIs, specifically so that code remains correct and consistent whether it's built for a 32-bit or 64-bit target, rather than requiring careful manual auditing of every raw int for possible truncation on a different architecture.
• NSInteger — typedef resolving to long on 64-bit, int on 32-bit; NSUInteger — same pattern, unsigned. • Exists specifically for portability: matches the platform's natural word size instead of a fixed 4-byte int. • Prevents silent truncation bugs where a legitimately large 64-bit value gets cut down by a fixed 4-byte int with no compiler error. • Cocoa APIs use NSInteger/NSUInteger pervasively (counts, indices, many properties) instead of raw int for exactly this reason. • Write NSInteger/NSUInteger consistently in your own Objective-C code rather than raw int, to get the same portability.
CGFloat: Architecture-Portable Floating Point
CGFloat, originating from Apple's Core Graphics framework, follows the exact same architecture-dependent pattern as NSInteger, but for floating-point numbers: on 64-bit platforms it's a double (8 bytes), and on 32-bit platforms it's a float (4 bytes). CGFloat width = 320.0; looks like an ordinary floating-point variable, but again, its actual precision and size silently adapt to the target architecture.
The motivation mirrors NSInteger's exactly: graphics and geometry APIs (screen coordinates, sizes, distances) need a floating-point type that naturally matches the platform's preferred floating-point width, so that geometry math remains both efficient and precise on whichever architecture the code runs on, without requiring separate 32-bit and 64-bit versions of every graphics-related API. CGFloat is used pervasively throughout Core Graphics and UIKit/AppKit — screen coordinates, view frame dimensions, colors — for exactly this reason.
Together, NSInteger/NSUInteger and CGFloat represent a consistent design philosophy running through Cocoa's APIs: rather than picking one fixed-width C type and hoping it stays appropriate forever, Apple's frameworks define architecture-relative typedefs so the same source code compiles correctly and efficiently regardless of the target's word size — a pattern you'll now recognize instantly anywhere you see these type names in a method signature or property declaration.
• CGFloat — typedef resolving to double on 64-bit, float on 32-bit; from Core Graphics. • Same motivation as NSInteger: match the platform's natural floating-point width for correctness and efficiency. • Used pervasively for coordinates, sizes, and distances throughout Core Graphics, UIKit, and AppKit. • NSInteger/NSUInteger and CGFloat together reflect Cocoa's consistent architecture-portable-typedef design philosophy.
A Preview of Object Types: NSString, NSNumber, NSArray, NSDictionary
You will see four object pointer types constantly from this point forward, well before the Foundation module covers each of them in full depth: NSString * (text), NSNumber * (a number wrapped as an object, useful anywhere a collection needs to hold numeric values as objects rather than raw primitives), NSArray * (an ordered collection of objects), and NSDictionary * (a collection of key-value pairs). All four follow exactly the object-pointer rule already covered in this lesson: always declared with a trailing asterisk, always a pointer to a heap-allocated object, never held directly.
It's worth noting now, as a preview, why NSNumber exists at all given that C already has perfectly good primitive numeric types: an NSArray or NSDictionary can only hold objects (pointers), not raw primitives like a plain int or double — so when you need to put a number into a collection, you wrap it in an NSNumber object first. This wrapping/unwrapping pattern, and the full API for working with all four of these classes, is covered thoroughly in the Foundation module; for now, simply recognize these four names on sight as object pointer types, following the same asterisk convention as every other Objective-C object variable.
• NSString *, NSNumber *, NSArray *, NSDictionary * — common object pointer types, previewed here, covered fully in the Foundation module. • All four follow the universal object-pointer rule: declared with an asterisk, always a pointer to a heap object. • NSNumber exists specifically to let numeric values be stored as objects, e.g. inside an NSArray or NSDictionary, which can only hold objects.
Code Examples
rawCount and width/count show that C primitives and Cocoa's portable typedefs coexist freely. someObject shows the mandatory asterisk on every object pointer. id shows a generic object pointer whose real class is only known at runtime. Class shows a pointer to the class itself, distinct from any instance. SEL shows a method name captured as a storable value via @selector(...).
On a 64-bit build, [hugeCollection count] can legitimately return a value larger than a 4-byte int can hold; storing it in a plain int silently truncates it with no compiler error. NSInteger grows to match the platform's natural word size (long on 64-bit), so the same source code is correct on both 32-bit and 64-bit builds without modification -- exactly why Cocoa APIs return NSInteger/NSUInteger rather than raw int.
NSObject someObject; // missing the asterisk NSObject *someObject; // a pointer, as every object variable must be
Every object variable in Objective-C must be a pointer. Omitting the asterisk does not declare a valid 'direct' object variable the way it might in some other languages -- it is essentially always a mistake and does not mean what a beginner typically expects.
BOOL isReady = 1; if (isReady == true) {... } BOOL isReady = YES; if (isReady) {... }
Idiomatic Objective-C uses YES/NO (not raw 1/0 or C's true/false) with BOOL, and idiomatic style tests a BOOL directly in a condition (if (isReady)) rather than comparing it explicitly, both for clarity and because BOOL's historical signed-char representation means an exact equality comparison against true or a bare 1 can be unreliable.
int itemCount = [hugeCollection count]; // fixed 4 bytes even on 64-bit
NSInteger itemCount = [hugeCollection count]; // matches platform word size
Storing a Cocoa API's NSInteger-returning value into a plain int risks silent truncation on architectures where NSInteger is wider than int (64-bit, where it's a long). Match the API's own portable type rather than substituting a fixed-width primitive.
// Confusing id and Class: id personClass = [Person class]; [personClass sayHello]; // sayHello is an instance method, not sent to the class
Class personClass = [Person class]; id personInstance = [[Person alloc] init]; [personInstance sayHello];
id refers to an instance of some class; Class refers to the class object itself. Sending an instance method's selector to a Class value (the class itself, not an instance) is a mismatch -- keep the two concepts distinct, as covered in this lesson.
Why must every Objective-C object variable be declared as a pointer (with an asterisk)? A. It's purely a stylistic convention with no real technical reason B. Objects are heap-allocated with variable size/layout, and a fixed-size pointer (an address) is the uniform way to refer to any of them regardless of class C. Only NSObject itself requires a pointer; its subclasses do not D. Pointers are required only for objects stored in collections
B. Objects are heap-allocated with variable size/layout, and a fixed-size pointer (an address) is the uniform way to refer to any of them regardless of class — Objects are dynamically allocated on the heap and can vary in size and layout; a pointer is a fixed-size address that works uniformly to refer to any object regardless of its actual size or class, which is why every object variable is a pointer, without exception.
What makes id different from a normally-typed object pointer like NSString *? A. id can only point to NSString objects B. id is not actually a pointer at all C. id can point to an object of any class, deferring most type checking to runtime rather than compile time D. id is exactly identical to NSString * in every respect
C. id can point to an object of any class, deferring most type checking to runtime rather than compile time — id is a generic object pointer that can refer to an instance of any class; the compiler defers most checking of messages sent to an id-typed variable until runtime, unlike a specifically-typed pointer such as NSString *.
What is the key difference between id and Class? A. There is no difference; they are interchangeable B. id points to an instance of some class; Class points to the class object itself, separate from any instance C. Class can only be used with NSObject, while id works with any class D. id is used only in 32-bit code, while Class is used only in 64-bit code
B. id points to an instance of some class; Class points to the class object itself, separate from any instance — id refers to an object instance (with its specific class unknown at compile time), while Class refers to the class object itself as a distinct runtime entity, separate from any particular instance of that class.
Why do NSInteger and NSUInteger exist instead of everyone just using plain int and unsigned int? A. They run faster than int in every case B. They are required by the Objective-C language grammar itself C. They resolve to the platform's natural word size (long/unsigned long on 64-bit, int/unsigned int on 32-bit), preventing silent truncation bugs when moving between architectures D. int and unsigned int are deprecated in Objective-C and no longer usable
C. They resolve to the platform's natural word size (long/unsigned long on 64-bit, int/unsigned int on 32-bit), preventing silent truncation bugs when moving between architectures — NSInteger/NSUInteger are typedefs that adapt to the compiling architecture's natural word size specifically to avoid silent truncation when a value that fits in a 64-bit long is stored in a fixed 4-byte int, which is exactly why Cocoa APIs use them pervasively.
What does SEL represent in Objective-C? A. A pointer to an object instance B. A pointer to a class object C. An opaque identifier representing a selector (a method name) as a storable, passable value D. A boolean value, either YES or NO
C. An opaque identifier representing a selector (a method name) as a storable, passable value — SEL is the type used to hold a selector as a first-class value -- for example via @selector(methodName) -- distinct from id (an instance) and Class (a class object), enabling patterns like dynamic message dispatch covered later in the Messaging module.
int rawCount = 10; // ordinary C primitive, still fully valid
NSInteger count = 10; // portable whole number, long on 64-bit
CGFloat width = 320.0; // portable float, double on 64-bit
BOOL isActive = YES; // Objective-C boolean, YES/NO convention
NSObject *someObject = nil; // an object pointer -- always has *
id thing = someObject; // generic pointer, class unknown at compile time
Class personClass = [NSObject class]; // a pointer to the class object itself
SEL mySelector = @selector(description); // a selector captured as a value// Fragile: a raw int is always 4 bytes, even on 64-bit,
// which can silently truncate a legitimately larger count.
int itemCount = [hugeCollection count];
// Portable: NSInteger matches the platform's natural word size
// (long on 64-bit, int on 32-bit), avoiding silent truncation.
NSInteger itemCountSafe = [hugeCollection count];