"Copy, Cast, or Alias?"

By FreePare Team · Wed Jul 29 2026 · 11 min read

"Copy, Cast, or Alias?"

How values move through C23, Java SE 25, and Python 3.14

Most bugs in this area begin with one innocent sentence:

"I passed the variable to the function."

That sentence hides several questions. Was the value copied? Was an address copied? Can the function mutate the original object? Did a numeric conversion discard information? How long does the object remain alive?

Variables are less like labelled boxes and more like backstage passes. The pass may contain a value, identify an object, or grant access to memory that has already left the building.

Let us check every pass.

What this article covers

1. Conversion is not transportation

A conversion does not move a value between physical containers. It asks the language to represent a value using a different type.

Sometimes the value survives exactly. Sometimes it is rounded, truncated, wrapped, clamped, or rejected.

A practical conversion matrix

ConversionC23Java SE 25Python 3.14
Small integer to wider integerValue preserved when representableValue preservedAlready arbitrary-size
Large integer to float/doubleMay roundMay roundfloat() may round or raise OverflowError
Floating value to integerFraction discarded; out-of-range standard binary value can be undefinedRound toward zero, then clamp to int/long; NaN becomes 0int() truncates toward zero; infinity raises OverflowError; NaN raises ValueError
Signed integer to smaller signed integerImplementation-defined if unrepresentableLow-order bits retainedNo narrowing without a target library/type
Signed integer to unsigned C typeReduced modulo target rangeNo unsigned int primitiveNot applicable to built-in int

The correct question is not "Is this widening or narrowing?" It is:

What exact rule does this source-target pair use?

2. Precision can disappear during a "widening" conversion

Java calls int to float a widening primitive conversion, but widening does not always mean exact:

public class PrecisionLab {
    public static void main(String[] args) {
        int exactInteger = 16_777_217;
        float approximate = exactInteger;

        System.out.println(exactInteger); // 16777217
        System.out.println(approximate);  // 1.6777216E7
        System.out.println((int) approximate); // 16777216
    }
}

A binary32 float has 24 bits of significand precision, including the implicit leading bit. It cannot represent every 32-bit integer.

The same basic precision issue appears in C when an integer is converted to float.

Python's built-in float normally uses the platform C double representation:

x = 2**53 + 1
y = float(x)

print(x)       # 9007199254740993
print(y)       # 9007199254740992.0
print(int(y))  # 9007199254740992

The Python integer is exact. The floating-point representation is the nearest representable value.

3. Narrowing: three languages, three sets of paperwork

Narrowing is only defined against a range, so the range is worth having in front of you: Integer Boundaries: C23 vs Java SE 25 vs Python 3.14 sets out what each type holds and what each language does when a result leaves it.

C floating-point to integer

For a finite standard floating value, C discards the fractional part. If the remaining integral value cannot be represented by the destination integer type, the behavior is undefined.


double price = 19.95;
int units = (int)price; /* 19 */

Do not execute an out-of-range cast and then inspect the result as if the standard promised one. Check first:

#include <limits.h>
#include <math.h>
#include <stdbool.h>

bool double_to_int(double value, int *out) {
    if (!isfinite(value) || value < INT_MIN || value > INT_MAX) {
        return false;
    }

*out = (int)value;
return true;
}

This straightforward check fits mainstream implementations where every int boundary is exactly representable as double, such as a 32-bit int with an IEEE binary64 double. A fully generic numeric library must also account for implementations whose int precision exceeds the precision of double.

Java floating-point to integer

Java specifies the result:

public class NarrowingLab {
    public static void main(String[] args) {
        System.out.println((int) 3.9); // 3
        System.out.println((int) -3.9); // -3
        System.out.println((int) 1e20); // 2147483647
        System.out.println((int) -1e20); // -2147483648
        System.out.println((int) Double.NaN); // 0
        System.out.println((int) Double.POSITIVE_INFINITY);
        // 2147483647
    }
}

The value is rounded toward zero. An out-of-range result is clamped to the nearest int boundary. NaN becomes zero.

That is different from narrowing one integer type to another:

int value = 0x1234_5678;
short lowerBits = (short) value;

System.out.printf("0x%04X%n", lowerBits & 0xFFFF);
// 0x5678

Python conversion

print(int(3.9))   # 3
print(int(-3.9))  # -3

for value in (math.inf, math.nan):

try:

try:
    print(int(value))
except (OverflowError, ValueError) as error:
    print(type(error).__name__)

Python refuses to create an integer from infinity or NaN. A third-party fixed-width type, such as a NumPy integer, has its own documented conversion rules. Do not attribute those rules to Python's built-in int.

4. Division: truncation and flooring are different elevators

C and Java integer division truncate toward zero:

-3 / 2 = -1

Python's // operator performs floor division:

print(-3 // 2)  # -2
print(-3 / 2)   # -1.5

For positive values, truncation and flooring often agree. Negative values reveal the difference.

When porting bucket calculations, pagination, coordinate transforms, or hash partitioning, add negative test cases.

5. C passes everything by value

C function parameters receive values. A pointer parameter receives a copied pointer value:

#include <stdio.h>

void replace_pointer(int *pointer) {
    static int replacement = 99;
    pointer = &replacement; /* Only the local copy changes. */
}

void replace_value(int *pointer) {
    *pointer = 99; /* The pointed-to object changes. */
}

int main(void) {
    int value = 10;
    int *pointer = &value;

    replace_pointer(pointer);
    printf("%d\n", value); /* 10 */

    replace_value(pointer);
    printf("%d\n", value); /* 99 */
}

To change the caller's pointer object, pass its address:

void replace_pointer_for_real(int **pointer) {
    static int replacement = 99;
    *pointer = &replacement;
}

Aliasing

Two pointers alias when they can access the same object:

int value = 5;
int *a = &value;
int *b = &value;

Writing through a changes what b observes.

The restrict qualifier allows optimization by promising a restricted access relationship for an object's lifetime within a block. It is not a runtime check:

void add_arrays(
    size_t count,
    int *restrict output,
    const int *restrict left,
    const int *restrict right
) {
for (size_t i = 0; i < count; ++i) {
    output[i] = left[i] + right[i];
}
}

Calling this function with overlapping arrays in a way that violates the restrict association makes the behavior undefined. restrict means "the programmer guarantees this access pattern," not "the compiler will detect aliases."

6. Java passes values, including reference values

public class ReferenceLab {
    static void mutate(StringBuilder builder) {
        builder.append("!");
    }

static void rebind(StringBuilder builder) {
    builder = new StringBuilder("new");
}

public static void main(String[] args) {
    StringBuilder message = new StringBuilder("hello");

    mutate(message);
    System.out.println(message); // hello!

    rebind(message);
    System.out.println(message); // hello!
}
}

message contains a reference value. The method receives a copy of that reference:

Java is not pass-by-reference. It is pass-by-value, and some values happen to be references.

Identity versus equality

String first = new String("byte");
String second = new String("byte");

System.out.println(first == second); // false
System.out.println(first.equals(second)); // true

String interning may make some == comparisons appear to work. That is not permission to use identity as text equality.

7. Python binds names to objects

Python function calls are commonly described as call-by-sharing. The function receives a new local name bound to the same object:

def mutate(items: list[int]) -> None:
    items.append(4)


def rebind(items: list[int]) -> None:
    items = [99]


numbers = [1, 2, 3]

mutate(numbers)
print(numbers)  # [1, 2, 3, 4]

rebind(numbers)
print(numbers)  # [1, 2, 3, 4]

The list is shared. The local name is not.

is versus ==

first = ["byte"]
second = ["byte"]

print(first is second)  # False
print(first == second)  # True

Use is for identity, most importantly:

if result is None:
    ...

Do not rely on implementation caching of small integers or strings. Identity is not value equality.

Mutable defaults

Default arguments are evaluated when the function is defined:

def unsafe_append(value: int, items: list[int] = []) -> list[int]:
    items.append(value)
    return items


print(unsafe_append(1))  # [1]
print(unsafe_append(2))  # [1, 2]

Create the object during the call instead:

def append_value(
    value: int,
    items: list[int] | None = None,
) -> list[int]:
    if items is None:
        items = []

    items.append(value)
    return items

8. Scope and lifetime are related, but not identical

Lifetime is the same question as ownership, one level down. Bytes, Bounds, and Unicode Gremlins follows it into allocation, realloc, and APIs that have to make ownership visible.

Scope answers:

Where can this name be used?

Lifetime answers:

During which part of execution does this object exist?

C's four storage durations

C defines:

1. Static storage duration - the lifetime is the entire program execution.

2. Thread storage duration - a distinct object exists for each thread.

3. Automatic storage duration - normally associated with entering and leaving a block.

4. Allocated storage duration - controlled through allocation and deallocation functions.

#include <stdlib.h>

int global_counter; /* Static storage duration. */
thread_local int thread_counter; /* Thread storage duration in C23. */

void example(void) {
    int local = 0; /* Automatic storage duration. */
    static int calls = 0; /* Static storage duration. */
    int *dynamic = malloc(sizeof *dynamic); /* Allocated storage. */

    free(dynamic);
}

Objects with static storage duration are initialized once before program startup. A block-scoped static in C is not C++'s lazily initialized local static.

Reading an uninitialized automatic object can produce an indeterminate value and, depending on its type and use, undefined behavior. Enable warnings and initialize intentionally.

Java

Java requires definite assignment for local variables:

int value;
// System.out.println(value); // Compile-time error.

Instance and static fields receive default values. Object lifetime is based on reachability, and garbage-collection timing is not a resource-management contract.

Use try-with-resources for files, sockets, and database resources:

try (var reader = Files.newBufferedReader(path)) {
    System.out.println(reader.readLine());
}

Python

Python name lookup follows Local, Enclosing, Global, Built-in order:

counter = 0


def outer() -> None:
    total = 0

    def add() -> None:
        nonlocal total
        total += 1

    add()
    print(total)  # 1


outer()

Use global only when assignment must target a module-level name.

if, for, and while statements do not create ordinary block scopes, but Python 3 comprehensions execute in their own implicit scope:

values = [number * 2 for number in range(3)]
# number is not available here.

CPython uses reference counting plus a cyclic garbage collector. Other Python implementations may manage memory differently. Use context managers instead of depending on object-finalization timing:

with open("data.txt", encoding="utf-8") as file:
    text = file.read()

9. A practical debugging checklist

When a value changes unexpectedly, ask:

1. What is its static type?

2. What is its runtime value?

3. Which conversion occurs before the operation?

4. Is information rounded, truncated, clamped, wrapped, or rejected?

5. Does the name contain a value or a reference-like value?

6. Can another name or pointer access the same object?

7. Is the operation mutation or rebinding?

8. Is equality being confused with identity?

9. Is the object still alive?

10. Is a runtime implementation detail being treated as a language promise?

Try it yourself

1. Convert 2**24 + 1 to Java float and 2**53 + 1 to Python float.

2. Test Java casts for NaN, positive infinity, and negative infinity.

3. Modify the C pointer example so the function changes the caller's pointer.

4. Write a Java method that tries and fails to swap two caller variables.

5. Create a Python function that safely accumulates values without a mutable default.

Exit test

You understand conversions and aliasing when you can predict all four separately:

Final reference

A copied reference is still a copy. A shared object is still shared. A "wider" numeric type can still lose precision.

Once those three sentences feel ordinary, this entire class of bug becomes much less ghostly.

Tags: coding-fundamentals, computer-science-fundamentals, programming-fundamentals, how-to-prepare-for-placements, student-learning-guide, exam-preparation-tips, smart-study-tips, effective-study-techniques