Pointers and Memory Addresses in C

Learn how a C program lays out its variables in memory and how pointers let you work with memory addresses directly. Take addresses with &, read and change values through pointers, and pass variables to functions by reference. Walk through arrays and strings with pointer arithmetic, use pointers to pointers for command-line arguments and for functions that move a caller's pointer, and call functions through function pointers, including comparison callbacks for the standard qsort.

IntermediateLesson 4 of 8 in Programming in C Course

What you will learn

  1. Picture how a C program's variables occupy bytes in memory, store addresses in pointer variables, and use those pointers to read and change the variables they point to.
    • Picture how variables are laid out in memory
      Describe memory as numbered bytes, find the size and address of variables and array elements, and explain how long each kind of variable exists.
    • Declare pointers and take addresses with &
      Declare pointer variables of the right type, store a variable's address with the address-of operator, and print, redirect or reset a pointer safely.
    • Read and change values through pointers
      Use the dereference operator to read and write the object a pointer points to, trace code with memory diagrams and avoid dereferencing invalid pointers.
  2. Write functions that change their caller's variables through pointer parameters, protect read-only data with const and return pointers that stay valid.
    • Pass variables by reference with pointer parameters
      Explain why C functions receive copies of their arguments, and use pointer parameters to change a caller's variables, swap values, return several results and read input with scanf.
    • Protect data with const and return pointers safely
      Mark read-only data with const, tell a pointer to a constant from a constant pointer, and return pointers that remain valid after the function ends.
  3. Move pointers through arrays and strings, explain how array names relate to pointers, and keep every pointer inside the array it belongs to.
    • Do arithmetic with pointers
      Add integers to pointers, subtract and compare pointers into the same array, and know which results are valid.
    • Explain how arrays and pointers are related
      Use the conversion of an array name to a pointer, the identity between a[i] and *(a + i), and the real differences between an array and a pointer.
    • Walk through arrays and strings with pointers
      Write loops that traverse arrays and null-terminated strings with pointers, use idioms such as *p++ correctly and stay within bounds.
  4. Follow several levels of indirection, let functions update a caller's pointer, and use arrays of pointers such as the command-line arguments.
    • Use pointers to pointers
      Declare and dereference a pointer to a pointer, trace it in a memory diagram and use it to let a function change a caller's pointer.
    • Use arrays of pointers and command-line arguments
      Build arrays of string pointers, process argc and argv as pointers, and tell an array of pointers from a two-dimensional array.
  5. Store and call functions through pointers, pass functions to other functions, and sort data of any type with the standard qsort and a comparison callback.
    • Declare, assign and call function pointers
      Write and read function pointer declarations, call functions through them and build tables of functions that choose behavior while the program runs.
    • Write callbacks and sort with qsort
      Pass functions as arguments, use void pointers to handle data of any type, and write correct comparison functions for qsort.

Pointers in C study guide

5 chapters

1

Memory, addresses, and pointed-to values

A variable has a value, occupies storage, and has a lifetime. A pointer lets you reach that variable through its address, so reading code now means tracking both the address held in a pointer and the value found there. Start with byte layout, then follow the effects of taking an address, redirecting a pointer, and writing through it. Small memory diagrams make these different operations visible before they appear inside functions and loops.

1.1 Bytes, sizes, and lifetimes

Memory can be pictured as a sequence of numbered bytes. Each number is an address. An object such as an int occupies consecutive bytes, and its address identifies its first byte. The name you use in source code lets the compiler find that object; the language does not require a name label to be stored beside its value.

Measure storage with sizeof

sizeof reports bytes and produces a size_t value. Use %zu to print it:

int scores[4] = {12, 18, 15, 21};
printf("%zu %zu\n", sizeof scores, sizeof scores[0]);

With 4-byte ints, this prints 16 4. The first result measures all four elements; the second measures one. sizeof(char) is always 1. A common 64-bit desktop layout uses 4-byte ints, 8-byte doubles, and 8-byte object pointers. Other targets can differ. Even among 64-bit systems, long commonly occupies 8 bytes on Linux and macOS but 4 on Windows.

Storage size and text length answer different questions. For char word[20] = "hi";, sizeof word is 20, while strlen(word) is 2. The array still contains 20 elements even though its string uses only two letters and a terminator. Except for variable-length arrays, these object sizes are fixed at compile time.

Follow an array's addresses

Hexadecimal addresses use digits 0 through 9, then a through f. The prefix 0x marks base 16. For an illustrative array beginning at 0x1000, four 4-byte elements start at:

ElementFirst byteLast byte
scores[0]0x10000x1003
scores[1]0x10040x1007
scores[2]0x10080x100B
scores[3]0x100C0x100F

0x100C is decimal 4108, twelve bytes beyond 0x1000. Array elements have no gaps. Separately declared variables have no guaranteed order or spacing; real printed addresses can also change between runs. A compiler can keep a value in a processor register when no addressable storage is needed.

Alignment and byte order

Alignment restricts where a type's objects can begin. A common layout places 4-byte ints at multiples of 4 and 8-byte doubles at multiples of 8. The compiler supplies suitable alignment for declared objects.

Endianness is the order of bytes within a multibyte value. For a 4-byte unsigned int containing 0x12345678, little-endian storage has bytes 78 56 34 12 from low to high addresses; big-endian storage has 12 34 56 78. An unsigned char * can inspect an object's representation one byte at a time. Byte order does not change the array's index order.

Keep the object alive

An ordinary local variable has automatic storage duration: its lifetime lasts through its block's execution. Typical implementations keep such objects in a call's stack frame and reuse that storage afterward. Globals, static variables, and string literals last throughout the program. Saving an address does not extend the lifetime of its object.

Key Takeaways

  • sizeof measures bytes; strlen counts characters before a string terminator.
  • Array elements occupy consecutive storage in index order.
  • Separate variables have no guaranteed relative addresses.
  • Alignment controls suitable starting addresses; endianness controls byte order inside a value.
  • A saved address remains usable only while its object exists.

1.2 Store and display an address

A pointer stores an address and has a type describing the object it can access. Declaring an int * gives you storage for a pointer; it does not create an int for that pointer to use. Give the pointer a known value when you declare it.

Take an object's address
int count = 42;
int *p = &count;

The unary address-of operator & obtains the address of count. If count begins at the illustrative address 0x1000, p holds 0x1000, while count holds 42. The pointer is also an object: &p is the address of the separate storage holding that pointer.

You can take the address of an array element, such as &scores[2]. A numeric constant or calculation result is not a stored object you can address this way: &42 and &(count + 1) are invalid. The binary expression a & b uses the same symbol for bitwise AND; the number of operands tells you which operation is present.

Read each declarator
DeclarationMeaning
int *p, q;One pointer and one ordinary int
int *p, *q;Two pointers to int
double *r;One pointer to double

Whitespace changes none of these types. int* p and int *p are equivalent. Declaring one pointer per line makes its star harder to miss. On common 64-bit desktops an object pointer occupies 8 bytes, but its own size does not tell you the size of the object it points to.

The target type matters. For double price = 9.5;, use double *r = &price;. Initializing an int * with &price produces an incompatible-pointer diagnostic. A cast that hides the diagnostic does not make an int access to that double valid: the type controls how the stored bytes are accessed.

Redirect a pointer
int a = 5, b = 8;
int *p = &a;
p = &b;

The final assignment replaces the address in p. It does not move either integer or copy one integer's value to the other. Now p points to b; a is still 5 and b is still 8. Several pointer variables can also store the same address.

Use a known empty state

An uninitialized local pointer has an indeterminate value, sometimes called a wild pointer. Do not inspect or dereference it. Initialize it with a valid object's address or with NULL, available from headers including <stddef.h> and <stdio.h>:

int *pending = NULL;
if (pending == NULL) {
    pending = &count;
}

A null pointer designates no object. !pending is another null test. These tests make sense only for initialized pointers; they do not validate an arbitrary address or a pointer whose object has died.

Print an object pointer
printf("%p\n", (void *)p);

%p expects void *, so supply the cast when using a typed object pointer. Its printed representation is chosen by the implementation and is commonly hexadecimal. %d and %x expect integer arguments of particular types and cannot be used as substitutes for %p.

Key Takeaways

  • Initialize a pointer with a valid address or NULL.
  • &count is an address; count is the stored integer.
  • Each pointer name needs its own * in a declaration.
  • Pointer types must suit the objects accessed through them.
  • p = &b changes the pointer, leaving the integers alone.
  • Print an object pointer with %p and a void * argument.

1.3 Follow a pointer to read or write

The unary dereference operator * follows a pointer to its object. If p points to x, *p designates x itself. Reading *p reads that object; assigning to *p changes it.

Trace a write through an address
int x = 10;
int *p = &x;
*p = *p + 5;
int y = *p * 2;
printf("%d %d %d\n", x, *p, y);

First p stores the address of x. On the assignment's right side, *p reads 10. Adding 5 gives 15, and the left-side *p stores 15 in x. The next line multiplies the updated value by 2, so y becomes 30. Output is 15 15 30.

Context distinguishes the stars: int *p declares a pointer, unary *p accesses an object, and *p * 2 also contains a binary multiplication. For a valid object x, *&x designates x; for a pointer to a valid object, &*p gives back the pointer value.

Separate addresses from contents

Start each trace with two rows of information: what address each pointer holds and what value each integer holds. Suppose p points to a, containing 3, and q points to b, containing 8.

Statement considered from that starting stateEffect
*p = *q;Copy 8 into a; pointers keep their targets
p = q;Make p point to b; integers keep their values

A memory diagram uses a box for each object. Label a box with its name and address, put its current value inside, and draw an arrow from a pointer to its target. An address assignment moves an arrow; a pointed-to assignment changes a target box's contents.

After p = q, the pointers are aliases for b. If you then execute *p = 20;, reading *q or b gives 20. Copying an address creates another route to the same object, not another integer.

Check that a dereference is valid

A pointer used for access must designate a live object of a suitable type, and writing also requires a modifiable object. Null pointers designate no object. Uninitialized pointers have no usable known target. A pointer becomes dangling when its target's lifetime ends.

Access through an invalid pointer is undefined behavior: C imposes no requirements on the outcome. The program may stop with a segmentation fault, corrupt data, or appear to run normally. An apparently successful run cannot establish that the access was valid.

For a pointer that can legitimately be null, test before accessing it:

if (p != NULL) {
    *p = 0;
}

Assigning NULL to a pointer you no longer need can prevent later accidental use of that variable. It does not reset other aliases. Keep track of the object's lifetime separately.

Put parentheses around the value you mean

Postfix ++ binds more tightly than unary *. Thus *p++ means *(p++): it uses the old pointer to reach a value and advances the pointer. (*p)++ increments the pointed-to value and leaves the pointer in place. *p + 1 reads the value and adds 1 without storing the result. These are three different actions.

Key Takeaways

  • *p designates the object reached through p.
  • Copying an address creates aliases; copying *p copies a value.
  • A dereference requires a live object and a suitable access type.
  • Undefined behavior has no promised result, including no promised crash.
  • *p++ advances the pointer; (*p)++ increments its target.
2

Let functions work on caller-owned data

A function receives a copy of each argument, including a pointer argument. That copied address can still reach an object owned by the caller, which makes swaps, input functions, and multiple outputs possible. The function interface should also show whether it changes the data. Follow the address into the call, distinguish local changes from writes through it, and check which objects will still exist when a returned pointer is used.

2.1 Change caller variables through parameters

C passes every argument by value. A parameter is a local variable initialized from the argument. Passing an address does not change that rule: the function receives a copy of the address, and that copy can reach the caller's object.

Compare two reset functions
void reset_copy(int n) {
    n = 0;
}

void reset(int *n) {
    *n = 0;
}

For int score = 12;, reset_copy(score) changes only its local n. score stays 12. The call reset(&score) copies score's address into pointer parameter n; assigning to *n then changes score to 0. This technique is often called pass by reference, although the language's actual argument passing remains by value.

The address in the call and the dereference in the body do different jobs. &score tells the function where the object is. *n = 0 tells it what to store there. Calling reset(score) supplies an integer where a pointer is required and triggers a compiler diagnostic.

Work through a swap
void swap(int *a, int *b) {
    int t = *a;
    *a = *b;
    *b = t;
}

/* Inside a calling function: */
int x = 3, y = 8;
swap(&x, &y);
  1. a points to x and b points to y.
  2. t saves the original x, which is 3.
  3. *a = *b writes 8 into x.
  4. *b = t writes the saved 3 into y.

Both caller variables change. A version taking plain int a, int b exchanges only copies. A pointer version that does int *t = a; a = b; b = t; also fails to exchange the integers: it redirects only the local pointer parameters.

Deliver more than one result

A return statement supplies one value, but pointer parameters can carry additional outputs:

void divide(int total, int size, int *q, int *r) {
    *q = total / size;
    *r = total % size;
}

/* size must be positive for these example inputs. */
int q, r;
divide(17, 5, &q, &r);

Integer division gives q equal to 3 and r equal to 2. The caller supplies two distinct writable objects. Another useful interface returns a success status and writes an output only on success; callers check that status before using the output.

Recognize the same pattern in library calls

scanf("%d", &age) needs age's address because it stores a parsed integer there. Passing age supplies a value instead of the required int *, producing undefined behavior. Check that scanf returns 1 before using the newly read value.

For text, char name[20]; scanf("%19s", name); passes a pointer to the array's first character. The width allows at most 19 input characters, leaving room for '\0'. The array expression already provides the required character pointer.

An array parameter likewise receives an address, not a copied block of elements. In void zero(int *a, size_t n), assignments to a[i] affect the caller's array. Passing the address copies only a pointer, which also lets read-only functions inspect large arrays without copying every element.

Key Takeaways

  • C copies every argument, including addresses.
  • A write through a pointer parameter can change the caller’s object.
  • A working integer swap exchanges *a and *b using a saved value.
  • Output parameters need valid writable objects supplied by the caller.
  • scanf receives addresses and reports how many conversions succeeded.
  • Array parameters share access to caller-owned elements.

2.2 Const access and safe return values

A pointer interface should make two things clear: whether the function may write through it, and how long the pointed-to storage remains valid. const answers the first question. The object's lifetime answers the second.

Locate the const restriction
DeclarationMay redirect p?May assign through *p?
const int *p = &x;YesNo
int *const p = &x;NoYes
const int *const p = &x;NoNo

A pointer to const limits this access path. If x was defined as ordinary int x = 5;, x = 6; is still allowed even while a const int * points to it. If the object itself was defined const, attempting to modify it through any pointer is undefined behavior.

A const pointer keeps one address. It must be initialized, and the address cannot later be replaced. The object at that address can still change when its type permits it. Read outward from the name: int *const p makes p constant; const int *p protects the int reached through p.

Use a read-only parameter such as const char *s when a function only examines a string. An ordinary char * can be passed to that parameter without a cast. The function can advance its local s, but assigning through *s is rejected. Converting back to a writable type requires more than removing a compiler warning: the underlying object must actually be modifiable.

Return a position in caller-owned storage
int *find(int *a, size_t n, int target) {
    for (size_t i = 0; i < n; i++) {
        if (a[i] == target) return &a[i];
    }
    return NULL;
}

For int v[] = {4, 9, 2, 9};, find(v, 4, 9) returns &v[1]. The function ends, but v belongs to the caller and remains alive there. After checking the result against NULL, the caller can compute found - v to obtain index 1 or assign *found = 0 to update the element. A search for 7 returns NULL, which must not be dereferenced. strchr and strstr use the same null-for-not-found convention.

Reject addresses of expired locals
int *bad(void) {
    int result = 42;
    return &result;
}

result has automatic storage duration. Its lifetime ends when the call returns, leaving the caller with a dangling pointer. The compiler may warn, and any later access through that pointer has undefined behavior. The old bytes being visible on one run does not keep the object alive.

The same problem can occur at an inner block's closing brace. An outer pointer cannot extend the lifetime of an inner variable whose address it saved.

Choose storage that lasts long enough

Return an int value when the caller needs only an integer result. For several results, let the caller supply storage to fill. A returned pointer can also refer to the caller's array, a string literal exposed through const char *, or a static object.

A static local is initialized once and lasts throughout the program, but all calls share it. If a later call stores a new value there, every earlier returned pointer sees that change. Static storage solves the lifetime problem without giving each call its own independent result object.

Key Takeaways

  • const int * restricts writes through that pointer, while int *const fixes the address.
  • An object defined const cannot legally be modified.
  • Pointers into the caller’s live array can remain valid after a function returns.
  • Check a possibly null search result before access or subtraction.
  • Returning an automatic local’s address leaves a dangling pointer.
  • A static result object is shared by all calls.
3

Move through arrays and strings

Pointer arithmetic connects an element’s address to its position in an array. The pointed-to type supplies the step size, while the array supplies the legal range. Use that model to read subscripts, pass a slice to a function, and build loops that stop before an invalid access. Strings add a second stopping rule: their terminating null character marks the end of the text, even when the storage contains more bytes.

3.1 Element steps and array bounds

Adding an integer to a pointer moves by elements of the pointed-to type. If p points to array element i, p + n points to element i + n, provided the resulting position is within the array or one past it.

Scale the address once

At an illustrative address 0x1000, adding 3 gives these results:

Pointer typeSize of one elementAddress after p + 3
int *4 bytes0x100C
double *8 bytes0x1018
char *1 byte0x1003

The compiler performs the scaling. Writing p + 3 * sizeof *p scales twice and generally goes to the wrong element. Use unsigned char * when the intended step really is one byte of an object's representation.

For int a[6] = {10, 20, 30, 40, 50, 60}; and int *p = &a[1];, *(p + 2) reads a[3], which is 40. The assignment p += 3; moves the pointer itself to a[4], containing 50. From there *(p - 1) reads 40. p++, p--, p += n, and p -= n all use element steps.

Subtract positions to count elements

&a[5] - &a[1] is 4, and reversing the operands gives -4. The result already counts elements; do not divide it by sizeof(int). Its signed type is ptrdiff_t, declared in <stddef.h>, and %td prints it.

Subtract only pointers into the same array, including its one-past position, with a result representable in ptrdiff_t. This operation describes positions within that array. Subtracting addresses of unrelated variables is undefined behavior even if the printed numbers happen to be close.

Use the one-past position as a boundary

For six elements, a + 6 is a valid one-past-the-end pointer. It can serve as a bound and participate in valid same-array subtraction, but it cannot be dereferenced. a + 7 and a - 1 already have undefined behavior when formed; avoiding a later dereference does not repair them.

for (int *p = a; p < a + 6; p++) {
    printf("%d\n", *p);
}

The body runs for indices 0 through 5. After the final increment, p equals a + 6, and the next test stops the loop. Using <= would allow the body to access that non-element.

For arithmetic rules, a single object acts like a one-element array. You may form &x + 1; you may not use it to read a supposed next variable.

Compare only meaningful positions

Ordering operators such as < compare positions within one array. Do not order pointers to unrelated objects. Equality and inequality can compare compatible pointer types and can test against a null pointer.

You cannot add two pointers, multiply pointers, or do standard C arithmetic on a void * or a function pointer. To find the midpoint of a same-array range [p, q), use p + (q - p) / 2: subtract positions to obtain a count, divide that count, then advance from p.

Key Takeaways

  • Integer addition moves a pointer by elements of its target type.
  • A same-array pointer difference already counts elements and has type ptrdiff_t.
  • One-past pointers may mark a boundary but cannot be dereferenced.
  • Forming a pointer before the array or beyond one-past is invalid.
  • Use same-array positions for ordering comparisons.
  • Subtract positions before computing a midpoint.

3.2 Array expressions and pointer parameters

An array is a block of elements. A pointer is a separate object holding an address. Their close relationship comes from an automatic conversion that happens in most expressions, not from the two objects being the same kind of thing.

Convert an array expression
int a[5] = {3, 1, 4, 1, 5};
int *p = a;

Here a converts to a pointer to a[0]. This array-to-pointer conversion, often called decay, makes the initializer equivalent to &a[0]. Both *p and a[0] read 3.

C defines subscripting through arithmetic: a[i] means *(a + i). Therefore p[3] works too and reads 1. &a[i] and a + i identify the same element. Even 3[a] has the same defined meaning as a[3], although the usual spelling is much clearer.

Know when the whole array stays an array

The conversion does not occur for operands of sizeof or unary &. It also does not occur for C23 typeof and typeof_unqual, or when a string literal initializes a character array.

Expression, with 4-byte ints and an 8-byte pointerResult
sizeof a20 bytes for all five elements
sizeof p8 bytes for the pointer object
sizeof a / sizeof a[0]5 elements

You cannot assign a new address to an array or increment its name: a = p; and a++; are invalid. You can redirect p, such as with p = a + 2;. Copy array contents with an element loop or an appropriate memcpy call.

Follow the adjusted parameter type
int sum(const int *values, size_t n) {
    int total = 0;
    for (size_t i = 0; i < n; i++) total += values[i];
    return total;
}

The caller supplies both an address and a count. For the array above, sum(a + 2, 3) visits original indices 2, 3, and 4, adding 4, 1, and 5 to return 10. Inside the function, index 0 is relative to the supplied starting pointer.

In a function parameter list, int a[], int a[10], and int *a all declare a pointer parameter. The plain [10] spelling supplies no checked bound and transfers no length. Inside such a function, sizeof a measures a pointer. The expression sizeof a / sizeof a[0] would give 2 on an 8-byte-pointer, 4-byte-int system, regardless of the caller's real array length.

Distinguish an element pointer from an array pointer

a converts to int *, while &a has type int (*)[5], a pointer to the whole five-element array. Both identify the beginning of the same storage, but arithmetic follows their different types. a + 1 advances one int, or 4 bytes in this layout. &a + 1 advances one entire array, or 20 bytes, to its one-past position.

A printed starting address cannot reveal that type difference. Read the declaration and keep the pointed-to type beside the pointer when tracing an expression.

Key Takeaways

  • Most array expressions convert to a pointer to the first element.
  • a[i] is defined as *(a + i).
  • sizeof a measures the whole array only where a really is an array.
  • An array parameter is adjusted to a pointer; pass its usable length separately.
  • &a points to an entire array, so its arithmetic has a larger step.

3.3 Pointer loops over numbers and text

A pointer loop needs a current position and a stopping rule. A counted array uses an end pointer; a string uses its terminating '\0'. Choose the form that makes the visited elements easiest to see.

Traverse a counted range
int a[] = {3, 1, 4, 1, 5};
int total = 0;
for (const int *p = a; p < a + 5; p++) {
    total += *p;
}

The running totals are 3, 4, 8, 9, and 14. An indexed loop adding a[i] for indices below 5 gives the same result. Modern optimizing compilers often generate similar code for these forms; pointer syntax alone promises no speed advantage.

Read before or after moving

With p at the 3 in {3, 1, 4}, int v = *p++; sets v to 3 and leaves p at the 1. Postfix increment supplies the old pointer value to * and updates the pointer. *++p first moves to the next element and then reads it. (*p)++ changes the current element and does not move the pointer.

Stop on a string terminator
size_t my_strlen(const char *s) {
    const char *p = s;
    while (*p != '\0') p++;
    return (size_t)(p - s);
}

For "pointer", p stops on the terminator, seven positions from the start. The result excludes that terminator. A character count uses the same walk: scanning "mississippi" and incrementing a counter whenever *p == 's' counts four matches among eleven letters.

A pointer into the middle of a string gives access to its suffix. With const char *msg = "hello world";, puts(msg + 6) prints "world". The earlier characters stay where they were.

Copy the terminator too
char destination[4];
const char *src = "cat";
char *dst = destination;
while ((*dst++ = *src++) != '\0') {
}

Each iteration assigns one character, tests the assigned value, and advances both pointers. The final iteration copies '\0', so the destination is a complete string. Both pointers then stand just beyond their terminators. The storage needs the text length plus one byte; omitting that extra byte causes a buffer overflow. This copy pattern also requires nonoverlapping source and destination ranges.

Reverse from both ends

For a writable string, first obtain its length. If the length is zero, stop before forming a last-character pointer. Otherwise set left = s and right = s + length - 1. While left < right, exchange the characters, increment left, and decrement right. For "stressed", four swaps produce "desserts"; the terminator remains at the original end.

The empty-string check matters because subtracting 1 from its starting pointer would form a pointer before the array.

Choose writable character storage

char word[] = "hello"; creates a modifiable six-byte array, including the terminator. const char *word = "hello"; points to a string literal with static storage duration. Modifying a string literal is undefined behavior, even if a declaration uses char * without const. Use const char * to make accidental writes a compiler error.

An array loop must stop before its one-past position, and a string walk must actually encounter a terminator within accessible storage. A pointer carries neither a length nor a guarantee that the bytes form a string.

Key Takeaways

  • A counted range ends at a one-past pointer; a string ends at its null character.
  • *p++ uses the old position and advances the pointer.
  • String copying includes the terminator and needs length plus one bytes.
  • Check an empty string before forming its last-character pointer.
  • Character arrays initialized from text are writable; string literals are not.
  • A pointer alone carries no length or termination guarantee.
4

Follow and update another pointer

A pointer can itself be the object reached through an address. Following two levels lets a function replace the caller’s pointer and lets a program walk an array of string addresses. Keep each level’s type visible: a pointer to an integer, a pointer to that pointer, and a pointer to a whole row represent different layouts. Trace which object each dereference reaches before deciding what an assignment will change.

4.1 Two levels of indirection

A pointer is a stored object, so it has an address that another pointer can hold. A pointer to a pointer adds one more step between the expression and the final data.

Follow the arrows one at a time
int x = 7;
int *p = &x;
int **pp = &p;

Use illustrative addresses: x at 0x2000 holds 7; p at 0x3000 holds 0x2000; pp at 0x4000 holds 0x3000. Evaluating *pp reaches p, whose value has type int *. Evaluating **pp follows that pointer onward to x, an int.

ExpressionTypeObject or value reached
ppint **Address of p
*ppint *The pointer stored in p
**ppintThe integer stored in x

&p has one more pointer level than p; each dereference removes one level. Initializing int **pp with &x is a mismatch because &x has type int *.

Choose the assignment level

With another live integer y, **pp = 9; changes x. The assignment *pp = &y; changes the pointer p, so future uses of **pp reach y. It leaves x at its current value. If q is another int *, pp = &q; changes only the outer pointer, making its first arrow reach q instead of p.

Do not count stars without also checking the object at each level. Before evaluating **pp, both the route to the pointer object and that object's route to the integer must be valid.

Move a caller's string pointer
void skip_spaces(const char **s) {
    while (**s == ' ') {
        (*s)++;
    }
}

/* In the caller: */
const char *text = "   hi";
skip_spaces(&text);
puts(text);

The caller passes the address of its pointer variable. *s therefore designates text; **s reads the current character. Each (*s)++ advances text by one character. After three iterations it points to 'h', and puts prints "hi". The function requires a valid pointer to a pointer into a null-terminated string.

A version taking const char *s and incrementing s changes only a local copy. The parentheses in (*s)++ are also essential: *s++ increments the outer local pointer instead of the caller's character pointer. In const char **s, the characters are read-only through this access path, but the caller's pointer may move.

Read a library output pointer

strtol from <stdlib.h> uses an output parameter of type char **:

char *end;
long number = strtol("42abc", &end, 10);

It returns 42 and stores a pointer to the first unused character in end, which now reaches "abc". If the end pointer equals the starting pointer, no digits were converted. A fully checked numeric parser also handles range errors; the pointer's job here is to report the stopping position.

Key Takeaways

  • int **pp can hold the address of an int * object.
  • Each dereference follows one pointer level.
  • **pp changes the final integer; *pp can change the intermediate pointer.
  • Updating a caller’s pointer requires that pointer’s address.
  • strtol uses char ** to return the first unused character’s position.

4.2 String-pointer arrays and rectangular arrays

An array can store pointers as its elements. A two-dimensional array instead stores complete rows as its elements. Both support two subscripts, but their storage and converted pointer types differ.

Store addresses of separate strings
const char *days[] = {"Mon", "Tue", "Wed"};

days holds three character pointers. Each points to a separate string literal. days[1] is the pointer to "Tue", and days[1][0] reads 'T'. With 8-byte object pointers, sizeof days is 24, regardless of the strings' lengths. The strings themselves occupy storage elsewhere.

The lengths can differ, producing a ragged collection. Reordering the list can exchange pointer elements without copying the text. Assigning days[0] = "Thursday"; is allowed: the pointer element can change even though characters reached through it are read-only.

In most expressions, days converts to a pointer to its first element. Because an element is a const char *, the converted type is const char **:

for (const char **d = days; d < days + 3; d++) {
    puts(*d);
}

The loop advances by one pointer element per iteration. *d retrieves the character pointer that puts needs.

Read command-line strings

int main(int argc, char *argv[]) receives the same parameter type as char **argv. argc is nonnegative and counts the available strings. When it is positive, argv[0] represents the program name or an empty string if that name is unavailable. User arguments occupy indices 1 through argc - 1, and argv[argc] is a null pointer.

For a typical invocation ./greet Ada 42, argc is 3, argv[1] points to "Ada", and argv[2] points to "42". The last argument is text until a conversion such as strtol reads it. argv[1][0] is 'A'. Check argc > 2 before using argv[2].

A loop that prints user arguments can use for (int i = 1; i < argc; i++) puts(argv[i]);. To walk from argv + 1 until a null pointer instead, first check argc > 0; if argc is zero, there is no user-argument range to start that way. Argument strings are modifiable, unlike string literals.

Follow complete rows

int grid[3][4] contains twelve integers in row-major order: all of row 0, then row 1, then row 2. With 4-byte ints, each row occupies 16 bytes. grid[2][1] lies 36 bytes after grid[0][0]: two complete rows contribute 32 bytes and the column contributes 4.

Declaration or expressionMeaning
int (*rows)[4]Pointer to a row of four integers
int *ptrs[4]Array of four integer pointers
Converted gridint (*)[4]

grid + 1 advances one row. A function accepting it can use int g[][4] or int (*g)[4], with a separate row count. It cannot use int **: the grid has no stored row pointers to dereference. A cast cannot manufacture those missing pointers.

Similarly, char names[3][6] owns eighteen character slots in fixed-size rows. const char *names[3] owns three pointer slots that can refer to strings of different lengths.

Key Takeaways

  • An array of string pointers stores addresses, with the text elsewhere.
  • An array of pointers converts to a pointer to its first pointer element.
  • argv[argc] is null; check the argument count before reading a particular string.
  • A two-dimensional array stores contiguous rows and contains no row-pointer objects.
  • int (*)[4] and int ** describe different storage layouts.
  • Two subscripts alone do not establish that two objects have the same type.
5

Choose functions and supply callbacks

A function pointer lets a program choose an operation while it runs. Its type records the call that is allowed, and a callback parameter lets another function request that operation when needed. Use those ideas first in a small table of arithmetic functions, then in qsort, where the callback must interpret element addresses correctly and return a consistent ordering. Sorting string pointers brings together both function pointers and two levels of object pointers.

5.1 Function pointer declarations and calls

A function pointer stores a reference to a function that can be called later. Its type includes the return type and parameter types. Those types must match the function used through the pointer.

Put parentheses around the pointer name
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }

int (*op)(int, int) = add;

Read from op: it is a pointer, the pointed-to thing is a function taking two ints, and that function returns an int. Without the parentheses, int *op(int, int); declares a function returning an integer pointer. The parentheses change the declaration's meaning.

A function name converts to a function pointer in this initializer. op = &add; is equivalent to op = add;. Both op(6, 3) and (*op)(6, 3) call add and return 9. Assigning op = sub; makes the same call return 3. Nothing about the call expression has changed except the function stored in the pointer.

Keep the signature compatible

A pointer of type int (*)(int, int) cannot safely call a function taking and returning double. For example, assigning sqrt to it produces an incompatible-type diagnostic. Casting the function pointer to silence a diagnostic does not make a mismatched call valid; the behavior is undefined.

Standard C treats object pointers and function pointers separately. void * is the generic object-pointer type, not a portable container for a function pointer. Function pointers support equality and inequality tests, but no pointer arithmetic.

A null function pointer means there is no callable function. When an interface permits that state, check it:

if (op != NULL) {
    int result = op(6, 3);
    printf("%d\n", result);
}

Calling through a null function pointer is undefined behavior.

Name the type once
typedef int (*binop)(int, int);
binop ops[] = {add, sub, mul};

The typedef names a pointer type. binop selected = mul; declares one pointer; binop pick(char symbol); declares a function returning one. Without the typedef, that return declaration would be int (*pick(char symbol))(int, int);.

An array of function pointers is a dispatch table. ops[2](6, 3) selects mul and returns 18. For a signed selection index i, check i >= 0 && i < 3 before evaluating ops[i]. Indexing outside the table is invalid even if every stored function is otherwise callable.

Read larger declarations outward

Start with the name and resolve parentheses and postfix parts before moving farther outward. In int (*table[3])(int, int), table[3] makes an array of three elements, * makes each element a pointer, and (int, int) makes its target a function returning int.

In char *(*f)(const char *), f is a pointer to a function taking a const char *; that function returns a char *. Keep the returned pointer's lifetime requirements separate from the function pointer's compatible-call requirements.

Key Takeaways

  • Parentheses distinguish a function pointer from a function returning a pointer.
  • A function pointer records the return and parameter types of valid calls.
  • A function name can initialize a compatible function pointer without &.
  • A typedef can make function pointer tables and return types easier to read.
  • Validate the table index and any optional null pointer before calling.

5.2 Callbacks and correct qsort comparisons

A callback is a function supplied to another function, which calls it as part of its work. This separates the general traversal or sorting operation from the particular action or ordering you choose.

Pass an operation without calling it
void twice(int *x) { *x *= 2; }

void for_each(int *a, size_t n, void (*fn)(int *)) {
    for (size_t i = 0; i < n; i++) fn(&a[i]);
}

For int v[] = {1, 2, 3};, for_each(v, 3, twice); produces {2, 4, 6}. The function name supplies a pointer. Writing twice() would try to call it immediately, with no required argument, instead of supplying an operation for later calls.

Recover a type from a generic object pointer

A void * can carry an object pointer and convert back to the original type. It cannot be dereferenced or used in standard C pointer arithmetic because it supplies no element type. Preserve qualifiers: read-only data travels through const void *, then through an appropriate pointer to const.

qsort from <stdlib.h> takes a base address, an element count, an element size in bytes, and a comparison callback. The callback has type int (*)(const void *, const void *). Each argument points to an array element, and the returned sign says where the first element belongs:

Return signOrdering
NegativeFirst before second
ZeroEquivalent for this ordering
PositiveFirst after second
Sort integers without overflow
int cmp_int(const void *a, const void *b) {
    int x = *(const int *)a;
    int y = *(const int *)b;
    return (x > y) - (x < y);
}

/* In the caller: */
int v[] = {42, -7, 19, 0, 3};
qsort(v, 5, sizeof v[0], cmp_int);

The array becomes {-7, 0, 3, 19, 42}. Comparisons produce 0 or 1, so the return expression produces only -1, 0, or 1. Reversing the ordering expression to (y > x) - (y < x) sorts in descending order.

Do not use return x - y; for arbitrary signed integers. For 32-bit int, subtracting -2000000000 from 2000000000 exceeds INT_MAX. Signed overflow has undefined behavior, so there is no promised wrapped result or reliable comparison sign.

Sort pointers to strings
int cmp_str(const void *a, const void *b) {
    return strcmp(*(const char *const *)a,
                  *(const char *const *)b);
}

const char *names[] = {"Lin", "Ada", "Grace"};
qsort(names, 3, sizeof names[0], cmp_str);

Include <string.h> for strcmp. An array element here is itself a pointer. The callback receives an address of that pointer element, so dereferencing once retrieves the string address. The added inner const prevents changing the element through this access path. After sorting, the order is "Ada", "Grace", "Lin".

strcmp(a, b) would read the bytes of the pointer slots as text. Also use sizeof names[0], the size of a pointer element, not sizeof(char) or the length of a name.

Comparators must leave elements unchanged and answer consistently, including transitive ordering. qsort does not promise to preserve the order of elements that compare equal. bsearch accepts the same comparator shape to search an array already sorted according to that ordering.

Key Takeaways

  • Pass a function name to supply a callback; parentheses call a function immediately.
  • Recover the element type from each generic comparison argument before access.
  • qsort needs a count, one element’s size, and a compatible comparator.
  • Use comparison signs without potentially overflowing subtraction.
  • String-pointer comparators receive addresses of pointer slots.
  • Equal comparison results do not guarantee stable order after qsort.

Pointers in C examples

5 Walkthroughs

1

Track an address and the value it reaches

13 steps
Build a memory picture, redirect pointers, and predict which object each assignment changes.
  1. 1
    IntroductionKeep three things separate

    A stored integer has a value, a location, and a lifetime. A pointer adds another object whose value is an address. Follow a small program by recording both kinds of values: integers such as 10, and addresses such as 0x1000.

    The useful question at each line is concrete: does this statement change an address, change the object reached through that address, or simply read a value?

  2. 2
    ExplanationLay out four integers

    Imagine int scores[4] beginning at 0x1000 on a system with 4-byte integers. The first element uses bytes 0x1000 through 0x1003. The next starts at 0x1004, followed by 0x1008 and 0x100C.

    The elements have consecutive storage. This layout rule belongs to the array; it gives no promised order for two separately declared integers.

  3. 3
    ExampleMeasure the array, then its contents

    For char word[20] = "hi";, two measurements give different results:

    • sizeof word is 20 bytes of array storage.
    • strlen(word) is 2 characters before '\0'.

    The terminator occupies another array element, and the remaining elements still belong to the array. sizeof returns size_t, printed with %zu; it does not count how many elements the program currently uses.

  4. 4
    ConnectionSeparate layout from lifetime

    Alignment controls suitable starting addresses, while endianness controls the order of bytes inside a value. Neither tells you how long an object exists.

    An ordinary local lasts through its block’s execution. A static local or global lasts throughout the program. Saving an address records a route to storage; it cannot keep an automatic local alive after the block ends.

  5. 5
    ChallengeFind an element’s address

    A six-element integer array starts at 0x1800, with 4 bytes per element. Where does element 3 begin, and what is the whole array’s size?

    ---reveal---

    Element 3 starts twelve bytes after the beginning, at 0x180C. Six 4-byte elements occupy 24 bytes. The index gives an element offset; multiplying by the element size gives the byte offset.

  6. 6
    ExplanationGive the pointer a target

    Consider int count = 42; int *p = &count;. The & operator obtains the address of count, and p stores that address. If count is at illustrative address 0x2000, the pointer’s value is 0x2000; the integer’s value stays 42.

    The pointer also occupies storage of its own. Its address is &p, which is different information from the address held in p.

  7. 7
    TipRead the star beside each name

    int *p, q; declares one pointer and one ordinary integer. To declare two pointers, use int *p, *q;, or put the declarations on separate lines.

    Initialize a pointer with a suitable object address or NULL. A null pointer is a known empty state; an uninitialized local pointer has an indeterminate value and cannot safely be tested as though it were already null.

  8. 8
    ChallengeRedirect without changing data

    Start with int a = 9, b = 16; int *p = &a;. Then execute p = &b;. Which integer values change, and what does p now hold?

    ---reveal---

    Neither integer changes: a remains 9 and b remains 16. Only p changes, storing the address of b. Pointer assignment replaces an address; it does not copy the contents of the old or new target.

  9. 9
    ExampleRead, add, and write through p

    Continue with int x = 10; int *p = &x; and execute:

    *p = *p + 5;
    

    The right-side *p reads 10 from x. Adding 5 gives 15. The left-side *p designates the same object, so the assignment writes 15 into x. The address in p has not changed. Next, int y = *p * 2; reads 15 and creates a separate integer y containing 30.

  10. 10
    Key InsightAn address copy creates another route

    For int n = 6; int *p = &n; int *q = p;, both pointers reach n. The assignment copied an address, not the integer object.

    After *q = 14;, reading n, *p, or *q gives 14. Draw one integer box and two arrows to it. Creating two integer boxes would incorrectly suggest independent data.

  11. 11
    TipCheck the target before access

    Access needs a pointer to a live object of a suitable type. A null pointer designates no object, and a dangling pointer has lost its live target. A null test handles an initialized pointer’s optional empty state; it does not establish that a non-null target remains alive.

    Invalid access has undefined behavior. C promises neither a particular value nor a crash, even when one run appears normal.

  12. 12
    ChallengeFollow a copied address

    What are the final values of a and b?

    int a = 6, b = 19;
    int *p = &a, *q = p;
    p = &b;
    *q = *p + 1;
    

    ---reveal---

    a becomes 20 and b stays 19. Redirecting p leaves q pointing to a. The final line reads b through p and writes the sum into a through q.

  13. 13
    RecapTrace the object named by the left side

    Read the left side before predicting a change. Assigning to p changes an address; assigning to *p changes its target. Keep copied pointers as separate arrows, check the target’s lifetime, and use the pointed-to type to interpret each access.

2

Make a function change caller-owned data

11 steps
Trace a swap, use output parameters, and decide whether returned pointers remain valid.

Save this lesson to follow every Walkthrough step by step.

  1. 1
    IntroductionFollow the address into a call
  2. 2
    ExplanationA copied address still reaches the caller
  3. 3
    ExampleSave the value before replacing it
  4. 4
    ExampleWrite the second value into the first slot
  5. 5
    ExampleFinish the exchange
  6. 6
    ChallengeDeliver two outputs
  7. 7
    ExplanationPut const at the intended level
  8. 8
    ExampleReturn a live position
  9. 9
    ChallengeExplain the lifetime failure
  10. 10
    ChallengeChoose a writable search interface
  11. 11
    RecapCheck the write and the lifetime
3

Walk an array without crossing its boundary

15 steps
Connect arithmetic, array conversion, and string loops by tracking each visited position.

Save this lesson to follow every Walkthrough step by step.

  1. 1
    IntroductionFind the start, step, and stop
  2. 2
    ExplanationMove by elements
  3. 3
    ExampleSubtract two positions
  4. 4
    Key InsightUse a boundary that is not an element
  5. 5
    ChallengeLocate the midpoint
  6. 6
    ExplanationAn array expression supplies a starting pointer
  7. 7
    ExampleKeep sizeof at the correct boundary
  8. 8
    ChallengePass a smaller range
  9. 9
    ExampleRead then advance, or advance then read
  10. 10
    ExplanationFind a string’s end
  11. 11
    ExampleCopy one character at a time
  12. 12
    TipReserve the terminator’s slot
  13. 13
    ChallengeFix the final loop visit
  14. 14
    ChallengeChoose a safe reversal setup
  15. 15
    RecapMake every visited position explicit
4

Change a pointer and read a pointer array

12 steps
Follow two levels of indirection, then distinguish string pointer slots from complete array rows.

Save this lesson to follow every Walkthrough step by step.

  1. 1
    IntroductionMake the pointer itself a target
  2. 2
    ExampleDraw two arrows
  3. 3
    ExplanationChoose which level changes
  4. 4
    ExamplePass the caller’s position by address
  5. 5
    ChallengePredict a pointer-only change
  6. 6
    ConnectionRead a conversion’s stopping position
  7. 7
    ExplanationAn array can own pointer slots
  8. 8
    ExampleAdvance through addresses
  9. 9
    TipCheck argument counts before string access
  10. 10
    ChallengeRead a whole row type
  11. 11
    ChallengeMatch a function to the supplied storage
  12. 12
    RecapName the object at every level
5

Choose an operation and sort through a callback

15 steps
Read function pointer types, select an operation from a table, and build integer and string-pointer comparators.

Save this lesson to follow every Walkthrough step by step.

  1. 1
    IntroductionSeparate a general job from its chosen operation
  2. 2
    ExplanationRead the callable type
  3. 3
    ExampleSelect a different function
  4. 4
    TipName a pointer type for a table
  5. 5
    ChallengeRead a related declaration
  6. 6
    ExplanationSupply an action callback
  7. 7
    ExampleSupply the four sort arguments
  8. 8
    ExplanationRecover the integer element type
  9. 9
    ExampleReturn the sign of the ordering
  10. 10
    ChallengeAvoid an overflowing shortcut
  11. 11
    ConnectionSort string pointers, not character rows
  12. 12
    ChallengeChoose the correct sorting size
  13. 13
    Key InsightEquality does not promise stability
  14. 14
    ChallengeTrace a selected comparison callback
  15. 15
    RecapMatch the call, layout, and ordering

Pointers in C practice questions

40 Practice Questions in 4 sets

Set 1: Trace addresses and function calls10 Practice Questions

Question 1 of 10

An int occupies 4 bytes. An array int levels[7] starts at illustrative address 0x2400. At what address does levels[4] begin?
  1. 0x2404
  2. 0x2410
  3. 0x2414
  4. 0x241C
Show answerHide answer
Answer: B
Index 4 is four elements after the start. Each occupies 4 bytes, so the offset is 16 bytes, written 0x10, and the address is 0x2410. Adding only 4 to the address would treat the index as a byte count and reach the second element instead.

Question 2 of 10

Given int items = 16;, what declaration creates a pointer to items?
  1. int *p = items;
  2. int p = &items;
  3. int *p = *items;
  4. int *p = &items;
Show answerHide answer
Answer: D
&items obtains the address of the integer, and int *p declares the matching pointer type. Initializing the pointer with items would supply the stored integer 16 instead of an address of an integer object. Taking an address leaves the value of items unchanged.

Question 3 of 10

What value does n hold after this code?

int n = 14;
int *p = &n;
*p = 23;
  1. 23
  2. 14
  3. 37
  4. 0
Show answerHide answer
Answer: A
p holds the address of n, so *p designates that same integer. Assigning 23 through *p replaces the value in n. Keeping 14 would confuse a write through the pointer with a change to the pointer’s own stored address.

Question 4 of 10

What is score after clear(score) returns?

void clear(int value) { value = 0; }
/* In the caller: */
int score = 27;
clear(score);
  1. 0
  2. 1
  3. 27
  4. Undefined behavior
Show answerHide answer
Answer: C
The function receives a local copy of the integer argument. Setting value to zero changes that parameter, so the caller’s score stays 27. Expecting zero would require a function that receives &score through an integer pointer and writes to the pointed-to object.

Question 5 of 10

On a system with 8-byte object pointers, what does this print?

char label[12] = "oak";
char *p = label;
printf("%zu %zu
", sizeof label, sizeof p);
  1. 4 8
  2. 12 8
  3. 12 12
  4. 3 8
Show answerHide answer
Answer: B
label is an array of twelve characters, so sizeof label is 12 even though the stored text has only three letters. p is a separate pointer object and occupies 8 bytes on this system. Counting the terminator gives the string’s occupied prefix, not the declared array size.

Question 6 of 10

What are left and right after the call?

void exchange(int *a, int *b) {
    int *t = a;
    a = b;
    b = t;
}
/* In the caller: */
int left = 11, right = 29;
exchange(&left, &right);
  1. 29, 11
  2. 29, 29
  3. 11, 11
  4. 11, 29
Show answerHide answer
Answer: D
The function exchanges its local pointer copies without assigning to *a or *b. No integer object is written, so left stays 11 and right stays 29. Expecting the values to swap confuses moving local address values with copying the integers reached through those addresses.

Question 7 of 10

A function is declared int read_count(int *out);. It returns 1 on success and writes the count only on success. What caller code safely prints a newly read count?
  1. int n; if (read_count(&n)) printf("%d", n);
  2. int n; read_count(&n); printf("%d", n);
  3. int n; if (read_count(n)) printf("%d", n);
  4. int *n; if (read_count(n)) printf("%d", *n);
Show answerHide answer
Answer: A
The caller supplies an integer object by address and uses its value only after the success status is true. An unconditional print could read an uninitialized n when the function fails without writing. An uninitialized pointer supplies no valid output object for the function to fill.

Question 8 of 10

Given int x = 6, y = 17; const int *p = &x;, what assignment is allowed?
  1. *p = y;
  2. (*p)++;
  3. p = &y;
  4. *p += 1;
Show answerHide answer
Answer: C
p is a pointer to const integer data, so it may be redirected to y. Assigning through *p is rejected because that access path is read-only. The restriction is on access through this pointer; x itself was defined without const and can still be changed by its own name.

Question 9 of 10

What are the final values of a and b?

int a = 4, b = 13;
int *p = &a, *q = p;
*p += 3;
p = &b;
*q = *p - 2;
*p = *q + 5;
  1. 7, 12
  2. 11, 13
  3. 7, 18
  4. 11, 16
Show answerHide answer
Answer: D
The first write makes a equal 7, and redirecting p leaves q pointing to a. The next assignment sets a to 11 using b’s value 13, then the final write sets b to 16. Keeping a at 7 incorrectly moves q when only p was redirected.

Question 10 of 10

What pair does the final line print?

int *next_value(void) {
    static int n = 20;
    n += 3;
    return &n;
}
/* In the caller: */
int *first = next_value();
int saved = *first;
int *second = next_value();
printf("%d %d
", saved, *first + *second);
  1. 23 49
  2. 23 52
  3. 26 52
  4. 23 46
Show answerHide answer
Answer: B
The first call stores 23, which is copied into saved. The second call changes the same static integer to 26, and both returned pointers reach it, so their values sum to 52. The result 49 treats the first pointer as a preserved snapshot, but only saved holds a separate copied integer.
Set 2: Reason about array ranges and strings10 Practice Questions

Save this lesson to see every answer and practice every set.

Question 1 of 10

For double values[8];, what is &values[6] - &values[2]?
  1. 32
  2. 8
  3. 4
  4. 2
Show answerHide answer

Save this lesson to see this answer.

Question 2 of 10

What does puts(message + 5) print for const char *message = "blue sky";?
  1. sky
  2. sky
  3. blue
  4. ky
Show answerHide answer

Save this lesson to see this answer.

Question 3 of 10

What is the value of *(p - 2) after these statements?

int bins[] = {6, 12, 18, 24, 30, 36, 42};
int *p = bins + 1;
p += 4;
  1. 18
  2. 36
  3. 12
  4. 24
Show answerHide answer

Save this lesson to see this answer.

Question 4 of 10

What does total(data + 1, 3) return?

int total(const int *p, size_t n) {
    int s = 0;
    for (size_t i = 0; i < n; i++) s += p[i];
    return s;
}
/* In the caller: */
int data[] = {9, 4, 7, 2, 8};
  1. 20
  2. 13
  3. 17
  4. 21
Show answerHide answer

Save this lesson to see this answer.

Question 5 of 10

On a system with 8-byte pointers and 4-byte ints, what does count(values) return?

size_t count(int a[9]) {
    return sizeof a / sizeof a[0];
}
/* In the caller: */
int values[9] = {0};
  1. 2
  2. 9
  3. 8
  4. 4
Show answerHide answer

Save this lesson to see this answer.

Question 6 of 10

What are taken and a[0] afterward?

int a[] = {8, 15, 22};
int *p = a;
int taken = *p++;
(*p)++;
  1. 15, 9
  2. 8, 9
  3. 8, 8
  4. 9, 8
Show answerHide answer

Save this lesson to see this answer.

Question 7 of 10

After this code, what is the greatest nonnegative integer k for which p + k can be formed within the permitted pointer range?

int a[8];
int *p = a + 1, *end = a + 8;
p += (end - p) / 2;
end -= 2;
p += end - p;
  1. 3
  2. 2
  3. 1
  4. 4
Show answerHide answer

Save this lesson to see this answer.

Question 8 of 10

A writable null-terminated string is reversed with this setup. What input exposes an invalid pointer calculation before the loop begins?

size_t n = strlen(s);
char *left = s;
char *right = s + n - 1;
while (left < right) {
    char t = *left; *left++ = *right; *right-- = t;
}
  1. A two-character string
  2. A one-character string
  3. An odd-length string
  4. An empty string
Show answerHide answer

Save this lesson to see this answer.

Question 9 of 10

An array int a[6] begins at illustrative address 0x3000, and each integer occupies 4 bytes. What addresses do a + 1 and &a + 1 represent?
  1. 0x3004 and 0x3018
  2. 0x3004 and 0x3004
  3. 0x3018 and 0x3018
  4. 0x3001 and 0x3006
Show answerHide answer

Save this lesson to see this answer.

Question 10 of 10

What are count and p - text after this loop?

char text[] = "abaca";
char *p = text;
int count = 0;
while (*p != '\0') {
    if (*p++ == 'a') count++;
}
  1. 3, 6
  2. 2, 5
  3. 3, 5
  4. 2, 6
Show answerHide answer

Save this lesson to see this answer.

Set 3: Follow pointer levels and storage layouts10 Practice Questions

Save this lesson to see every answer and practice every set.

Question 1 of 10

With 8-byte object pointers, what is sizeof words?

const char *words[] = {"red", "violet", "gold", "teal"};
  1. 20
  2. 32
  3. 24
  4. 8
Show answerHide answer

Save this lesson to see this answer.

Question 2 of 10

What is *p afterward?

int a = 18, b = 25;
int *p = &a;
int **pp = &p;
*pp = &b;
**pp -= 4;
  1. 21
  2. 14
  3. 25
  4. 18
Show answerHide answer

Save this lesson to see this answer.

Question 3 of 10

After int n = 31; int *p = &n; int **pp = &p;, what is the type of *pp?
  1. int
  2. int **
  3. int ***
  4. int *
Show answerHide answer

Save this lesson to see this answer.

Question 4 of 10

For char *end; long n = strtol("57kg", &end, 10);, what does end point to after the conversion?
  1. The first character of "57kg"
  2. The character 7 in "57kg"
  3. The character k in "57kg"
  4. The terminator after "57kg"
Show answerHide answer

Save this lesson to see this answer.

Question 5 of 10

A program receives the command line ./paint amber 6 in the usual hosted layout. What are argc and argv[2][0]?
  1. 2 and 6
  2. 3 and 6
  3. 3 and '6'
  4. 2 and '6'
Show answerHide answer

Save this lesson to see this answer.

Question 6 of 10

What parameter type matches a caller’s int board[2][5] after its usual array conversion?
  1. int **board
  2. int (*board)[5]
  3. int *board[5]
  4. int (*board)[2]
Show answerHide answer

Save this lesson to see this answer.

Question 7 of 10

What are a, b, and *p after the call?

void shift(int **slot, int *next) {
    **slot += 2;
    *slot = next;
    **slot += 3;
}
/* In the caller: */
int a = 5, b = 12;
int *p = &a;
shift(&p, &b);
  1. 7, 15, 15
  2. 10, 12, 12
  3. 7, 15, 7
  4. 5, 15, 15
Show answerHide answer

Save this lesson to see this answer.

Question 8 of 10

What is the byte offset of map[2][3] from map[0][0], and what is the type of map + 1, for int map[4][6] with 4-byte integers?
  1. 36 bytes and int **
  2. 60 bytes and int **
  3. 36 bytes and int (*)[6]
  4. 60 bytes and int (*)[6]
Show answerHide answer

Save this lesson to see this answer.

Question 9 of 10

What are word and the index of p afterward?

char word[] = "abcd";
char *p = word;
char saved = *p++;
*p = saved;
p += 2;
*p = 'z';
  1. "aacz", index 3
  2. "bbcz", index 3
  3. "aazd", index 2
  4. "aacd", index 4
Show answerHide answer

Save this lesson to see this answer.

Question 10 of 10

A helper must return a pointer to the largest element of the caller’s nonempty array, allowing the caller to modify it. What design meets both requirements?
  1. Copy the largest value into a local int and return its address.
  2. Store the largest value in a static int and return its address.
  3. Track the largest element’s index and return &a[index].
  4. Track the largest value and return it through a const int *.
Show answerHide answer

Save this lesson to see this answer.

Set 4: Choose operations and build sorting callbacks10 Practice Questions

Save this lesson to see every answer and practice every set.

Question 1 of 10

What does int (*filter)(int); declare?
  1. A function taking int and returning int *.
  2. A pointer to a function taking int and returning int.
  3. A function taking int * and returning int.
  4. A pointer to a function taking int * and returning int.
Show answerHide answer

Save this lesson to see this answer.

Question 2 of 10

A function has signature void visit(int *a, size_t n, void (*action)(int *));, and void mark(int *x); is available. How does a caller supply mark for a five-element array data?
  1. visit(data, 5, mark());
  2. visit(data, 5, *mark(data));
  3. visit(data, 5, mark(data));
  4. visit(data, 5, mark);
Show answerHide answer

Save this lesson to see this answer.

Question 3 of 10

What value is assigned to result?

int difference(int a, int b) { return a - b; }
int product(int a, int b) { return a * b; }
int (*op)(int, int) = difference;
op = product;
int result = (*op)(7, 4);
  1. 28
  2. 3
  3. 11
  4. -3
Show answerHide answer

Save this lesson to see this answer.

Question 4 of 10

For int readings[8];, what call sorts all elements using a compatible compare callback?
  1. qsort(readings, sizeof readings, 8, compare);
  2. qsort(readings, 8, sizeof readings, compare);
  3. qsort(readings, 8, sizeof readings[0], compare);
  4. qsort(readings, 8, sizeof readings[0], compare());
Show answerHide answer

Save this lesson to see this answer.

Question 5 of 10

What are result and calls after this code?

int calls = 0;
int add_one(int x) { calls++; return x + 1; }
int twice(int x) { calls++; return x * 2; }
/* In a function: */
int (*ops[])(int) = {twice, add_one};
int result = ops[1](ops[0](5));
  1. 12, 2
  2. 11, 1
  3. 12, 1
  4. 11, 2
Show answerHide answer

Save this lesson to see this answer.

Question 6 of 10

An integer comparator must work for every representable int in ascending order. Which return expression meets that requirement after reading the elements into x and y?
  1. (x > y) - (x < y)
  2. (x < y) - (x > y)
  3. x - y
  4. x > y
Show answerHide answer

Save this lesson to see this answer.

Question 7 of 10

For an array const char *labels[], what retrieves the string passed as comparator parameter const void *a before calling strcmp?
  1. (const char *)a
  2. (const char *const *)a
  3. *(const char *const *)a
  4. **(const char *const *)a
Show answerHide answer

Save this lesson to see this answer.

Question 8 of 10

A table int (*ops[3])(int, int) has valid functions in slots 0 and 2, and NULL in slot 1. For a signed integer choice, what guard safely permits ops[choice](8, 2)?
  1. choice < 3 && ops[choice] != NULL
  2. choice >= 0 && choice < 3 && ops[choice] != NULL
  3. ops[choice] != NULL && choice >= 0 && choice < 3
  4. choice >= 0 && choice < 3
Show answerHide answer

Save this lesson to see this answer.

Question 9 of 10

A comparator sorts integers by absolute value, returning zero for opposite values of equal magnitude. It sorts {4, -4, 2, -2} with qsort. What result is guaranteed?
  1. {-2, 2, -4, 4} in exactly that order.
  2. {2, -2, 4, -4} in exactly that order.
  3. {-4, 4, -2, 2} in exactly that order.
  4. The two magnitude-2 values precede the magnitude-4 values.
Show answerHide answer

Save this lesson to see this answer.

Question 10 of 10

A list is sorted by increasing string length using this callback body. What is the defect?

const char *x = *(const char *const *)a;
const char *y = *(const char *const *)b;
size_t nx = strlen(x), ny = strlen(y);
return nx < ny;
  1. It returns positive for shorter text and zero for longer text.
  2. It reads the string lengths from pointer-slot bytes.
  3. It changes the strings while measuring their lengths.
  4. It compares character addresses instead of character counts.
Show answerHide answer

Save this lesson to see this answer.

Stuck on your own question? Homework Help

Key Terms

60 Key Terms, each with its definition

A

Address-of operator
The unary address-of operator & obtains the address of an addressable object or a function, producing a matching pointer type.
For an integer array, &values[2] supplies the address of the third element without changing its contents.
Aliasing
Aliasing occurs when different expressions or pointers designate the same object, so a write by one route is visible by another.
After int *q = p;, both pointers reach the same integer until a later assignment redirects one of them.
Alignment
Alignment is a type’s restriction on suitable starting addresses for its objects, handled automatically for ordinary declarations.
A common four-byte-aligned integer layout places successive array elements at addresses divisible by four.
argc
argc is the nonnegative argument count passed to hosted main, counting the strings supplied through argv, including a program name when present.
Before reading argv[2], test that argc > 2 so the requested argument string actually exists.
argv
argv is the character-pointer list passed to hosted main, with a null pointer at index argc and user argument strings starting at index 1.
For a usual ./sum 8 13 invocation, argv[2] reaches "13"; the argument is text before numeric conversion.
Array bounds
Array bounds determine which element indices exist and where a pointer may move, with only one additional position allowed for an end marker.
Forming a - 1 for a pointer at an array’s start is invalid even if the program never reads through it.
Array of pointers
An array of pointers stores separate address values as its elements, with the pointed-to objects kept in other storage locations.
A list of four string pointers has four address slots even when the strings have very different lengths.
Array parameter adjustment
Array parameter adjustment changes an array declaration in a function parameter list into a pointer parameter with no copied array length.
A parameter written int a[10] is a pointer inside the function, so sizeof a measures that pointer.
Array slice
An array slice can be supplied in C as a starting pointer plus a count describing a selected consecutive portion of an existing array.
Passing data + 2 with a count of 3 selects original indices 2 through 4 without copying them.
Array-to-pointer conversion
Array-to-pointer conversion turns an array expression into a pointer to its first element in most expression contexts without copying its elements.
int *p = values; initializes a pointer to values[0]; the original array still owns all its elements.
Automatic storage duration
Automatic storage duration makes an ordinary local object exist during its block’s execution, with storage ending when that execution leaves.
A helper’s ordinary local result dies on return even if the helper has copied its address into a pointer.

B

Buffer overflow
A buffer overflow writes beyond a destination object’s bounds, such as when a string copy lacks room for its final null character.
A three-letter word requires four character slots; a three-byte destination cannot hold its terminator.
Byte
A byte is the storage unit measured by sizeof; a C char always occupies one byte, though other type sizes depend on the system.
On a target with four-byte integers, an array of eight integers occupies 32 bytes before any pointer is declared.

C

Callback
A callback is a function supplied to another function for it to invoke when its general work requires a chosen action or decision.
A traversal accepts an action pointer and calls it with each element’s address to update that element.
Comparator
A comparator orders a pair of elements using a negative, zero, or positive result for first-before, equivalent, or first-after.
Returning (x > y) - (x < y) orders integers in ascending order without subtracting the values themselves.
Const pointer
A const pointer is a pointer object whose stored address cannot be changed after initialization, even when its target remains writable.
With int *const p = &x;, *p = 3; is allowed, but p = &y; cannot redirect the pointer.

D

Dangling pointer
A dangling pointer is a pointer whose former target’s lifetime has ended, making it unusable for later access to that expired object.
Returning an automatic local’s address leaves a dangling pointer when the function call ends.
Dereferencing
Dereferencing applies unary * to a pointer to designate the pointed-to object, allowing a suitable read or write through that route.
After int *p = &score;, assigning *p = 28; changes the same integer later read through score.
Dispatch table
A dispatch table is an array of function pointers that selects an operation by an index chosen while the program runs.
An operation menu checks its selection index before using that index to retrieve and call a table entry.

E

End pointer
An end pointer records where a text conversion stopped, letting the caller inspect the unconsumed suffix independently of the numeric result.
Converting "73ms" with strtol can return 73 and set the caller’s end pointer to the m.
Endianness
Endianness is the order of bytes within a multibyte value, with little-endian systems storing the least significant byte first.
A four-byte 0x12345678 has bytes 78 56 34 12 in low-to-high address order on a little-endian target.

F

Function pointer
A function pointer designates a callable function and records the compatible return and parameter types required when calling through it.
int (*op)(int, int) = add; stores an addition function that can later be called as op(4, 7).
Function signature
A function signature describes the return and parameter types that must be compatible with the pointer type used to call the function.
A pointer for two integer parameters cannot safely call a function expecting one double, even through a cast.

G

Generic object pointer
A generic object pointer uses void *, or a qualified form, to carry an object address that must regain a suitable type before access.
An integer comparator converts const void * to const int * before reading the addressed array element.

I

Indeterminate pointer value
An indeterminate pointer value is the uninitialized state of an ordinary local pointer; it supplies no known usable target.
The declaration int *p; alone does not let the program safely test, follow, or write through that pointer.
Indirection level
An indirection level is one pointer step in a chain of addresses, followed by a dereference and added by taking an object’s address.
For int **pp, the first dereference gives an int *, and the second reaches an integer object.

M

Memory address
A memory address identifies a byte of storage; an object’s address identifies the first byte of the storage it occupies.
If a four-byte integer begins at illustrative address 0x2000, its remaining bytes follow through 0x2003.

N

Null character
The null character is the character with value zero, written '\0', that terminates a C string within its accessible character storage.
A length loop stops on the terminator, while a complete copy loop must also write that final character.
Null pointer
A null pointer is a pointer value that designates no object or function and can represent an optional empty result or absent callback.
An element search returns NULL when no match exists, so its caller tests the result before dereferencing it.

O

One-past-the-end pointer
A one-past-the-end pointer marks the position immediately after an array’s final element and may serve as a bound but not be dereferenced.
For int a[5], a traversal can stop when its current pointer equals a + 5, before accessing another element.
Operator precedence
Operator precedence determines how operators group when parentheses do not specify the grouping, before the resulting expression is evaluated.
Postfix ++ binds more tightly than unary *, so *p++ groups as *(p++), not as (*p)++.
Output parameter
An output parameter is a pointer parameter used to store a result in caller-supplied writable storage in addition to any return value.
A division helper can fill separate quotient and remainder integers while returning a success status.

P

Pass by reference
Pass by reference in C commonly names the technique of passing an address and modifying its target while the address itself is copied.
A swap called with &x and &y changes caller variables by writing through the two local pointer parameters.
Pass by value
Pass by value gives a function parameter a copy of its argument’s value, including when that value is a pointer to caller-owned storage.
Assigning to a local int parameter leaves the caller’s integer unchanged; writing through a copied address can change it.
Pointer
A pointer is a typed value that can designate an object or function; an object pointer can also hold a null or one-past position.
In int *p = &count;, the pointer variable stores an address while the integer retains its own value.
Pointer arithmetic
Pointer arithmetic moves a pointer by counts of its element type, with results restricted to its array and that array’s one-past boundary.
Adding 2 to an integer pointer moves two integer positions; it does not ask the caller to scale the offset by sizeof.
Pointer declarator
A pointer declarator uses * with an individual declared name to say that the named object has a pointer type.
In int *left, right;, only left is a pointer; declaring right as a pointer needs another star.
Pointer difference
A pointer difference is the signed element count obtained by subtracting two positions in one array, including a valid one-past position.
&a[5] - &a[2] gives 3 elements regardless of how many bytes one element occupies on that system.
Pointer parameter
A pointer parameter is a function’s local pointer initialized from an argument, giving the function access to an object by address.
A reset helper receives &score through int *out and writes zero into the caller’s integer using *out.
Pointer to a pointer
A pointer to a pointer designates a pointer object, adding a level of indirection before access reaches the final pointed-to data.
For int **pp = &p;, assigning *pp = &y; redirects p, while assigning **pp = 8; changes its target.
Pointer to an array
A pointer to an array designates a whole array as its element type, so adding 1 advances by the storage size of that complete array.
For int a[4], &a has type int (*)[4]; its arithmetic step differs from that of &a[0].
Pointer to const
A pointer to const permits reading its target but rejects writes through that pointer; the pointer itself may still be redirected.
const int *p = &x; allows p = &y;, while an assignment such as *p = 3; is rejected.
Postfix increment
Postfix increment supplies an object’s old value to its surrounding expression and increments that object, as in the pointer step in *p++.
int x = *p++; reads the element at the old position and leaves p pointing to the next position.
ptrdiff_t
ptrdiff_t is the signed integer type used for pointer subtraction; a defined subtraction must have a result representable in that type.
Include <stddef.h> for ptrdiff_t and use %td to print a difference between valid same-array positions.

Q

qsort
qsort is the standard library function that rearranges an array using its element count, element size, and a supplied comparison callback.
Sorting string-pointer elements requires sizeof names[0] for the slot size, not a string’s character count.

R

Ragged array
A ragged array is a collection whose rows may have different lengths, commonly represented in C using pointers to separately stored rows.
String pointers can refer to "red" and "violet" without reserving equal-sized character rows for both words.
Row-major order
Row-major order stores every element of one row before the next row in a rectangular array, with the fixed row width determining row steps.
For int a[3][4], adding 1 to converted a advances a complete four-integer row, not one integer.

S

Segmentation fault
A segmentation fault is a system-reported invalid memory access that often stops a program, though invalid C access need not produce one.
A null dereference may stop with a segmentation fault; a different invalid pointer write may corrupt data silently.
Signed integer overflow
Signed integer overflow occurs when a signed arithmetic result is outside its type’s representable range, giving undefined behavior in C.
An integer comparator using x - y can overflow even when each input separately fits in an int.
sizeof
The sizeof operator reports a type’s or object’s storage size in bytes and returns a value of the unsigned integer type size_t.
For char code[10] = "go";, sizeof code reports 10 even though the text has only two characters.
Stable sort
A stable sort preserves the original relative order of elements that its comparator treats as equal; qsort provides no such guarantee.
Sorting names only by length may reorder two equally long names even though their comparison result is zero.
Stack frame
A stack frame is the storage a typical implementation uses for one function call’s working data, including many automatic local objects.
Returning from a function allows its old frame storage to be reused; a saved local address cannot preserve it.
Static storage duration
Static storage duration makes an object’s storage last throughout the program, with a static local retaining its value between calls.
Two pointers returned to one static result object see the same updated value after another call writes it.
Storage duration
Storage duration describes how long an object’s storage lasts, such as one block execution for an ordinary automatic local variable.
A pointer saved from an inner block cannot keep that block’s ordinary local integer alive afterward.
String literal
A string literal is quoted text in C that supplies an array with static storage duration whose characters must not be modified.
const char *p = "oak"; exposes the literal through a read-only path, while char a[] = "oak"; creates a writable copy.
strtol
strtol is the standard library function that converts leading numeric text to long and can report its stopping position through char **.
Passing &end lets strtol replace the caller’s character pointer with the first unused character’s address.
Subscript operator
The subscript operator accesses an element by defining a[i] as *(a + i), using the pointer’s element type to scale the position.
If p points to original element 2, then p[1] accesses original element 3 of the same array.

T

Two-dimensional array
A two-dimensional array is an array whose elements are fixed-size arrays, storing complete contiguous rows rather than separate row pointers.
int grid[2][5] converts to int (*)[5]; passing it as int ** would describe a different layout.
typedef
A typedef declaration introduces a name for an existing type, making a complex function-pointer type easier to reuse in declarations.
After typedef int (*operation)(int);, operation chosen; declares a function pointer, not a function body.

U

Undefined behavior
Undefined behavior means the C standard imposes no requirements on an operation’s outcome, including whether the program stops or continues.
Dereferencing a null pointer has undefined behavior even if a particular machine reports a segmentation fault.

About this lesson

By StudeliReport a mistake

Make your own Study Materials

Turn your notes, slides and documents into Study Guides, Practice Questions, Flashcards and 6 more tools.

Get Started