Pointer targets and shared objects
Follow the target before changing a value
A copied integer keeps its own value. A copied pointer value can leave two pointer variables identifying the same integer object. To explain a later write, you need two records: the numbers stored in the integer objects, and the current targets of the pointers.
This lesson introduces simple pointers to int, address-of &, and dereference *. You will distinguish a pointer variable from its stored pointer value and its target, trace shared targets and retargeting, and recognise null-pointer and lifetime boundaries.
Prerequisites: initialized integer variables, sequential assignments, copied values, if, and short circuit. Array knowledge from the previous lessons is useful context, but every target here is an ordinary scalar integer. We do not yet use pointer arithmetic, array-to-pointer conversion, pointer parameters, allocation or machine addresses.
C11 remains our explicit teaching convention, not a version specified by the GATE syllabus. All arithmetic in the defined complete programs stays within −32767 through 32767. Each program starts afresh. The two unsafe practice fragments are for classification only; do not compile or run them to guess an output.
A pointer variable holds a pointer value
Consider the declarations int score = 7; and int *p = &score; inside main.
scoreis an integer object whose current value is 7pis a separate pointer variable, with type “pointer toint”- The value stored in
pidentifiesscore. We write its target symbolically asp → score
A pointer variable is itself an object, but it is not the integer object it points to. Pointer types are scalar types in C: p holds one pointer value. That does not make the pointer value an ordinary integer used in our arithmetic.
The & in &score obtains a pointer to score; it does not read the integer 7 and turn 7 into an address. No invented address such as 1000 is needed. Our arrow records identity only, not a byte count, physical location, relative placement or machine layout. The arrow is a drawing convention, not C syntax.
The * in the declaration int *p = &score; is part of declaring p as a pointer. In a later expression, *p follows the current pointer value and designates its target. With p → score, reading *p reads the current integer in score. An assignment such as *p = 12; writes 12 into score. It does not change the target of p.
By contrast, p = &other;, with another live integer other, replaces the pointer value in p. It does not write into either integer. Before the next dereference, update the arrow.
| Item to record | In this starting example | Question it answers |
|---|---|---|
| Integer object and value | score = 7 | What number is stored? |
| Pointer variable and value | p contains the pointer obtained from &score | What does this separate variable hold? |
| Current target | p → score | Which integer will *p select? |
Use a separate declaration for each pointer while learning. A declaration alone is not a promise that a target exists. An automatic pointer declared without an initializer, such as int *p;, does not acquire a usable target or a guaranteed null value. Give it a known value before using it.
Copying a pointer value can preserve a shared target
After int *q = p;, both arrows end at score: p → score ← q. The pointer variables p and q remain separate. The initialization copies the pointer value, not the integer object, and it does not make q point to the variable p.
We call this a shared target, or aliasing: the expressions *p and *q currently access the same integer object. A write through either expression changes that one object's value, so a later read through the other expression sees the updated value.
Compare int saved = *p;. Here the initializer reads an integer and initializes a separate integer variable. saved gets a number, not a continuing connection to the target. This follows the copied-value model from the foundation lessons. What matters is which value was copied: an integer or a pointer.
For each statement:
- Read the current arrows before resolving any
*por*q - Check that each accessed target is a live, suitable object and that every integer read has a known value
- Compute from the current integer values
- Update the one destination: an integer value for
*p = ..., or an arrow forp = ... - Carry forward every other number and arrow
Worked example 1 Two routes to one integer
Predict the output. Will q still read 7 after the first store through p? Will saved follow the later updates?
#include <stdio.h>
int main(void)
{
int score = 7;
int *p = &score;
int *q = p;
int saved = *p;
*p = *p + 5;
*q = saved + *q;
printf("%d %d %d\n", score, *q, saved);
return 0;
}| After this step | score | saved | Target of p | Target of q |
|---|---|---|---|---|
| All declarations | 7 | 7 | score | score |
*p = *p + 5 | 12 | 7 | score | score |
*q = saved + *q | 19 | 7 | score | score |
pis initialized from&score;qcopies that pointer value. The diagram isp → score ← qsavedreads 7 throughp, then retains that integer copy- The first store reads the current 7, adds 5 and writes 12 into
score. Neither arrow moves - The second store reads
saved = 7and the current*q = 12, then writes 19 into the samescore - The final target diagram is still
p → score ← q, now withscore = 19. Output:19 19 7, followed by a newline
Predicting 12 14 7 gives q a private copy of the old integer, even though it copied a pointer value. Predicting 24 24 24 makes saved follow target updates instead of keeping its copied integer. Both errors disappear when pointer values and saved integer values have different entries in the trace. The shared object is still just one integer; writing through two different pointer variables does not create two versions of it.
Worked example 2 Retarget one pointer and keep the other
Now there are two integer objects. Reassigning p must not silently move q.
#include <stdio.h>
int main(void)
{
int left = 3;
int right = 11;
int *p = &left;
int *q = p;
p = &right;
*p = *q + 2;
*q = *p + 4;
printf("%d %d %d %d\n", left, right, *p, *q);
return 0;
}| After this step | left | right | Target of p | Target of q |
|---|---|---|---|---|
| All declarations | 3 | 11 | left | left |
p = &right | 3 | 11 | right | left |
*p = *q + 2 | 3 | 5 | right | left |
*q = *p + 4 | 9 | 5 | right | left |
Initially p → left ← q, with right separate. After retargeting, the diagram is p → right and q → left. No integer has changed yet.
The first store reads 3 through q, adds 2 and writes 5 through p into right. The next store reads the new 5 through p, adds 4 and writes 9 through q into left. Output: 9 5 5 9, followed by a newline.
If you moved both arrows when executing p = &right, you treated q = p as a permanent link between the pointer variables. It was a one-time value copy. If you changed left to 11 at retargeting, you confused the pointer assignment with *p = right. Those are different destinations. The current targets, rather than the pointer variable names, decide which later store reaches which integer.
Null is an explicit absence of a target
Include <stddef.h> when using NULL in our examples. int *p = NULL; initializes a pointer to a known null value. We draw p → no target. The pointer variable exists, but this value supplies no integer object that may be read or written through *p.
A null pointer is different from a pointer to an integer containing zero. After int value = 0; and int *p = &value;, the diagram is p → value with value = 0. Reading *p is valid and yields 0. With p = NULL, reading or assigning through *p is invalid, regardless of the number you wanted to read or store. Do not interpret NULL as a promise about an all-zero byte pattern or a physical address.
When p is known to be either null or a pointer to a suitable live integer, if (p != NULL) can guard a dereference in its body. With null, the body is skipped; with a valid target, the body may use that target. Skipping a write does not create a target or achieve a required update.
The same ordering matters in p != NULL && *p > 0: under that same known-value assumption, the left comparison prevents the right operand from being evaluated when p is null. Reversing those operands would attempt the read before checking for null. A null comparison is not a general test that an arbitrary pointer is safe. In particular, it does not repair an uninitialized pointer or a pointer whose target has stopped existing.
A target has to remain alive
A plain local int in these examples has automatic storage duration. If it belongs to a nested block, its lifetime ends when execution leaves that block. A pointer variable in an enclosing block can remain alive for longer than the integer it once identified. Keeping the pointer variable does not keep the target object alive.
In a target trace, mark the inner object as ended at the closing brace. Any pointer still holding a pointer to that object has an indeterminate value at that boundary under C11. It does not automatically become null. Do not continue drawing it as a usable arrow, dereference it, or try to validate its old value by comparing it with NULL.
A safe plan is to finish the target's work and copy any needed integer result into a longer-lived variable before leaving the block. If the longer-lived pointer is no longer needed, assign NULL to it while its target is still alive. Clearing one pointer does not clear other pointers to that object; each pointer has its own stored value. Another possible plan is to put the target in a block that lasts through all its required uses. The exercise below uses the first plan.
Lifetime and current number are separate facts. An integer copy can remain valid after the source object ends; a copied pointer does not preserve that object. We need no assumption about “old bytes still being there” to make this distinction.
Practice before opening the solutions
These four original transfer exercises are unscored. For the defined programs, give the output, intermediate integer states and target diagrams. For exercises 3 and 4, classify the unsafe fragment without compiling or executing it, then solve the specified repair.
Practice 1 Equal numbers and a new shared target
Initially, do p and q identify the same object? Show what changes at q = p, then give the output.
#include <stdio.h>
int main(void)
{
int first = 6;
int second = 6;
int *p = &first;
int *q = &second;
*p = 9;
q = p;
*q = *q - 2;
printf("%d %d %d %d\n", first, second, *p, *q);
return 0;
}Practice 2 An integer copy and two pointer changes
Trace each destination. Explain why q = p does not copy the number 10 and why the following retargeting of p does not move q.
#include <stdio.h>
int main(void)
{
int red = 4;
int blue = 10;
int *p = &red;
int *q = &blue;
int saved = *q;
*p = *q;
q = p;
p = &blue;
*q = *q - 3;
*p = saved + *q;
printf("%d %d %d %d %d\n", red, blue, saved, *p, *q);
return 0;
}Practice 3 Supply the missing target
The intention is to increase total from 8 by 3, using a store through p. Assume <stddef.h> is included and this fragment occurs inside main. It is deliberately unsafe; read it only.
int total = 8; int *p = NULL; *p = total + 3;
Classify the store. Insert one pointer assignment between the declaration of p and the store so that the stated update is valid. Explain why merely guarding the original store with if (p != NULL) would not achieve the intended update.
Practice 4 Keep the result after the inner object ends
Assume the usual headers and an enclosing main. This fragment is deliberately unsafe; do not compile or run it.
int saved = 0;
int *p = NULL;
{
int local = 9;
p = &local;
*p = *p + 2;
saved = *p;
}
printf("%d %d\n", saved, *p);Trace the valid work inside the block. At what boundary does p cease to hold a usable pointer to local? Does C11 prescribe 11 11 as the output? Repair the fragment while keeping local and its update inside the inner block: retain the final integer in saved, clear p before leaving that block, and print only saved afterward.
Full solutions and wrong-turn feedback
Practice 1 solution
| After this step | first | second | Target of p | Target of q |
|---|---|---|---|---|
| All declarations | 6 | 6 | first | second |
*p = 9 | 9 | 6 | first | second |
q = p | 9 | 6 | first | first |
*q = *q - 2 | 7 | 6 | first | first |
The initial diagram is p → first and q → second. Equal stored numbers do not merge two objects. The first store changes only first. Then q = p changes only q's target: the diagram becomes p → first ← q, and second still contains 6. The last store reads 9 through q, subtracts 2 and writes 7 into first. Output: 7 6 7 7, followed by a newline.
7 7 7 7 wrongly joins the two integer objects because their initial values match. 9 4 9 4 ignores the pointer assignment and continues using second as q's target. A copied pointer value must be recorded at the moment it is copied.
Practice 2 solution
| After this step | red | blue | saved | Target of p | Target of q |
|---|---|---|---|---|---|
| All declarations | 4 | 10 | 10 | red | blue |
*p = *q | 10 | 10 | 10 | red | blue |
q = p | 10 | 10 | 10 | red | red |
p = &blue | 10 | 10 | 10 | blue | red |
*q = *q - 3 | 7 | 10 | 10 | blue | red |
*p = saved + *q | 7 | 17 | 10 | blue | red |
The first store copies the integer 10 from blue to red; the diagram stays p → red and q → blue. Then q = p copies a pointer value, leaving p → red ← q. It does not read *p or *q. Retargeting only p produces p → blue and q → red.
The subtraction therefore writes 10 - 3 = 7 into red. The last store reads the saved integer 10 and the current red = 7, then writes 17 into blue. Output: 7 17 10 17 7, followed by a newline.
Making saved become 17 treats an integer copy as an alias. Making the subtraction change blue makes q follow a later assignment to p. Treating *p = *q as an arrow change confuses copying the targets' contents with copying a pointer value. The left side's * is decisive.
Practice 3 solution
Initially total = 8 and p → no target. The right-hand arithmetic 8 + 3 is within range, but that does not supply a destination for the store. Dereferencing the null pointer for *p = ... has undefined behavior. C11 does not promise an update, a particular output or a crash.
Insert p = &total; before the store. The complete repaired program is:
#include <stddef.h>
#include <stdio.h>
int main(void)
{
int total = 8;
int *p = NULL;
p = &total;
*p = total + 3;
printf("%d %d\n", total, *p);
return 0;
}- After declarations:
total = 8,p → no target - After
p = &total:total = 8,p → total - The store reads 8, computes 11 and writes it into
total. Nowtotal = 11,p → total - Both printed expressions read the same live integer. Output:
11 11, followed by a newline
p = NULL again would still provide no target. *p = 0 cannot initialize the pointer; it would try another invalid target write. Guarding the original store with if (p != NULL) safely skips it in this known-null case, but leaves total = 8, so it does not satisfy the requested update. Assigning p = &total first supplies the target rather than merely avoiding the operation.
Practice 4 solution
Follow the valid prefix without assigning a numeric output to the whole unsafe fragment:
| Checkpoint | saved | Inner local | State of p |
|---|---|---|---|
| Before inner block | 0 | Not yet alive | Known null |
After local initialization | 0 | 9 | Known null |
After p = &local | 0 | 9 | p → local |
After *p = *p + 2 | 0 | 11 | p → local |
After saved = *p | 11 | 11 | p → local |
| After leaving inner block | 11 | Lifetime ended | Indeterminate; no usable old target |
The inner read and write are valid. The closing brace ends local's lifetime, even though saved and the pointer variable p remain alive in the enclosing block. The later *p cannot read that ended object. The fragment has undefined behavior, so C11 does not prescribe 11 11, or even a guaranteed first printed 11. A correct trace of the valid prefix is not a guarantee about the output of an execution containing undefined behavior.
The requested repair is:
#include <stddef.h>
#include <stdio.h>
int main(void)
{
int saved = 0;
int *p = NULL;
{
int local = 9;
p = &local;
*p = *p + 2;
saved = *p;
p = NULL;
}
printf("%d\n", saved);
return 0;
}The repair follows the same valid states through saved = 11. Then, while local is still alive with value 11, p = NULL replaces the pointer value: p → no target. It does not change local or saved. After the inner block ends, p remains known null and saved remains 11. The print reads only the longer-lived integer copy. Output: 11, followed by a newline.
Adding a null test to the original print is not a repair: the original p is indeterminate after the lifetime boundary, not a known choice between null and a live target. Copying p into a second pointer before the block ends would leave that second pointer with the same lifetime problem. Keeping the integer result is different from keeping another pointer to the ended object.
Before moving on
Explain the different destinations in p = q and *p = *q. Draw the two arrows after copying p into q and then retargeting only p. Finally, explain why saving an integer can preserve a result across a block boundary while saving a pointer to that block's local integer cannot preserve the target. Next we will connect these target records to function calls while keeping the copied-argument model.
Source note
The examples, diagrams, traces, exercises and explanatory wording are original. Semantic facts were checked against the WG14 N1570 C11 committee draft: pointer and scalar types §§6.2.5p20–21; address-of and dereference §§6.5.3.2p3–4; pointer declarations §6.7.6.1p1; scalar initialization §§6.7.9p10–11; assignment §6.5.16.1p2; null pointers §6.3.2.3p3 and NULL §7.19p3; pointer/null comparison §§6.5.9p2, p5–6; short circuit §6.5.13p4; automatic lifetime §§6.2.4p2, p5–6.
C11 is a teaching convention, not a claimed GATE requirement. This lesson covers a bounded introduction to scalar pointer targets, not all pointers, C or GATE CS.
Quick reference
int *p = &x;creates a pointer variable and initializes it to identify the live integerx- Record integer values and pointer targets separately; arrows are symbolic, not machine addresses
*pselects the current target. Reading it gets the target's integer; assigning through it changes that integerp = &yretargetspwithout changing either integerq = pcopies the current pointer value. It can create a shared target, but does not makeqfollow future changes topint saved = *p;copies an integer; later target updates do not changesaved- A known null pointer has no dereferenceable target. A pointer to an integer containing zero is different
- A null guard only excludes null under a known-valid-value assumption; it does not establish general pointer validity
- An automatic target must outlive every access to it. Save needed integer results before its lifetime ends
- Invalid dereferences are classification questions; an observed output cannot make them valid
Notes for this lesson
Sign in to keep your progress. Sign in