Skip to content
VLSI Mentor

Perl Part I · Module 5 · Arrays and Lists

Indexing, Negative Indexes and $#array

Perl gives you two ways to ask about an array's size and they are not the same number. One reports how many elements there are; the other reports the position of the last one, which is always one less. Mixing them up produces a loop that runs one iteration too many, reads an element that does not exist, and — because reading past the end is silent — carries on without complaint. This chapter settles which to use where, adds counting from the right-hand end, and measures two behaviours that sound more forgiving than they are: writing to a position beyond the end quietly inflates the array with empty slots, and shortening an array by setting its last position throws the removed values away for good.

Foundation14 min readPerlPart IFundamentalsArraysDebugging

Part I · Module 5 · Chapter 5.2 · Indexing, Negative Indexes and $#array

1. Positions, Both Ways

Azvya Education Pvt. Ltd.VLSI Mentor
positions.pl
use strict;
use warnings;

my @marks = (72, 65, 90, 88);

printf "scalar(\@marks) = %d   (how many)\n",     scalar(@marks);
printf "\$#marks        = %d   (last position)\n", $#marks;
printf "first          = %s\n", $marks[0];
printf "last via \$#    = %s\n", $marks[$#marks];
printf "last via -1    = %s\n", $marks[-1];
printf "second to last = %s\n", $marks[-2];
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
scalar(@marks) = 4   (how many)
$#marks        = 3   (last position)
first          = 72
last via $#    = 88
last via -1    = 88
second to last = 90

The array laid out both ways:

From the left0123
Value72659088
From the right-4-3-2-1
  • scalar(@marks) is 4 — the number of elements.
  • $#marks is 3 — the index of the last element.
  • $marks[-1] is the last element, and is the form to prefer over $marks[$#marks] because it is shorter and says what it means.

So $#array is always scalar(@array) - 1. That relationship is the entire content of §2.

Note there is no $# form for counting from the right — negative indexes do that job. And a negative index is a read convenience: -1 is the last element whatever the length, which is why it survives the array changing size underneath it.

2. The Off-By-One, Measured

Azvya Education Pvt. Ltd.VLSI Mentor
loop-bounds.pl
use strict;
use warnings;

my @marks = (72, 65, 90);

print "correct  0 .. \$#marks:\n";
for my $i (0 .. $#marks) {
    printf "  i=%d value=%s\n", $i, $marks[$i];
}

print "wrong    0 .. scalar(\@marks):\n";
for my $i (0 .. scalar(@marks)) {
    printf "  i=%d value=%s\n", $i, (defined $marks[$i] ? $marks[$i] : "undef");
}
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output — STDOUT
correct  0 .. $#marks:
  i=0 value=72
  i=1 value=65
  i=2 value=90
wrong    0 .. scalar(@marks):
  i=0 value=72
  i=1 value=65
  i=2 value=90
  i=3 value=undef
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet

Three elements, four iterations. The fourth read position 3, which does not exist, and got undef.

Nothing on STDERR. That follows from Chapter 5.1 §5: an out-of-range read is silent. The loop did not fail; it processed a phantom element. Had the script been counting failures, the count would be one too high; had it been building a report, there would be an extra blank row.

The two correct forms:

Azvya Education Pvt. Ltd.VLSI Mentor
Both of these are right
for my $i (0 .. $#marks)            { ... }
for my $i (0 .. scalar(@marks) - 1) { ... }

The first is idiomatic Perl and the one you will see. The second is clearer to a reader arriving from another language. Either is fine; mixing the two ideas is what produces the bug.

3. On an Empty Array, $# Is -1

Azvya Education Pvt. Ltd.VLSI Mentor
empty-bounds.pl
use strict;
use warnings;

my @none = ();

printf "scalar = %d   \$# = %d\n", scalar(@none), $#none;

print "loop 0 .. \$#none:\n";
for my $i (0 .. $#none) {
    print "  ran with i=$i\n";
}
print "  (no lines above means zero iterations)\n";
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
scalar = 0   $# = -1
loop 0 .. $#none:
  (no lines above means zero iterations)

$#none is -1, not 0. There is no last position, because there is no element.

And the loop ran zero times, which is correct — but it is worth knowing why, because the reason is a rule presented earlier as a pure hazard. 0 .. -1 is a backwards range, and Chapter 3.10 §2 measured that a backwards range is the empty list:

Azvya Education Pvt. Ltd.VLSI Mentor
Measured
my @r = (0 .. -1);     # 0 elements

So the empty-array case works out safely by accident of two rules lining up. That is the one place in this track where Chapter 3.10's backwards-range behaviour does something useful rather than something quietly wrong.

The scalar(@array) - 1 form behaves identically here — 0 .. -1 again — so both correct bounds are safe on an empty array. It is only the 0 .. scalar(@array) mistake that misbehaves, and on an empty array it runs once with $i of 0.

4. Writing Past the End Extends the Array

Reading past the end is silent and harmless. Writing past it is silent and changes the array.

Azvya Education Pvt. Ltd.VLSI Mentor
auto-extend.pl
use strict;
use warnings;

my @slots = ("a", "b", "c");

printf "before: count=%d  \$#=%d\n", scalar(@slots), $#slots;

$slots[6] = "g";

printf "after : count=%d  \$#=%d\n", scalar(@slots), $#slots;
printf "contents: [%s]\n", join("][", map { defined $_ ? $_ : "undef" } @slots);
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output — STDOUT
before: count=3  $#=2
after : count=7  $#=6
contents: [a][b][c][undef][undef][undef][g]
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet

One assignment took the array from three elements to seven, filling positions 3, 4 and 5 with undef. No warning.

That is occasionally what you want — reserving a slot at a known position — and it is more often a bug, because of what it does to the count. A script that writes $results[$test_number] = $status using a test number that happens to be large ends up with an array whose scalar() is the largest index plus one, most of it empty. Every count, every average and every "how many did we run" is then wrong, and the undef elements interpolate as nothing so the report looks merely sparse rather than broken.

The map in that last line is Chapter 5.10's; it is there to make the undef elements visible, since printing them directly would show empty gaps.

5. $#array Can Be Assigned, and Truncation Is Destructive

Azvya Education Pvt. Ltd.VLSI Mentor
truncate.pl
use strict;
use warnings;

my @a = (1, 2, 3, 4, 5);

$#a = 1;
printf "after \$#a = 1  : count=%d  [%s]\n", scalar(@a), join(" ", @a);

$#a = 4;
printf "after \$#a = 4  : count=%d  [%s]\n", scalar(@a),
    join(" ", map { defined $_ ? $_ : "undef" } @a);

my @b = (1, 2, 3);
$#b = -1;
printf "after \$#b = -1 : count=%d  [%s]\n", scalar(@b), join(" ", @b);
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
after $#a = 1  : count=2  [1 2]
after $#a = 4  : count=5  [1 2 undef undef undef]
after $#b = -1 : count=0  []
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output — continued
after $#b = -1 : count=0  []

Read the middle line. Setting $#a back to 4 restored the array's length and not its contents — positions 2, 3 and 4 came back as undef. The values 3, 4 and 5 are gone.

So "setting the length back" is not an undo. Shortening an array by assigning $# discards the elements beyond the new end, permanently.

$#b = -1 sets the array to empty, which works but is obscure. @b = (); is the readable way to clear an array and is what you should write — the explicit empty list from Chapter 5.1 §4.

Assigning $# at all is rare in modern Perl. It is worth recognising in inherited code, and splice from Chapter 5.5 is the right tool when you genuinely need to remove a span.

6. Why This Matters in Engineering Work

Walking a parsed row by position. Fixed-column data from a report comes out of Chapter 4.7 as an array, and the loop over it needs the right bound. One extra iteration appends a phantom field to whatever you are building.

Counts that feed a report. scalar(@failures) is the number in "7 tests failed", so anything that inflates the array — §4's stray write — corrupts a number somebody acts on. The inflation is invisible because the padding is undef.

The last element is a common thing to want. The newest log line, the final status, the most recent measurement. $array[-1] is the form; $array[$#array] is the same thing with more to read and more to get wrong.

And the empty-array case is the one to test. A clean run gives an empty @failures, and §3 is why a loop over it behaves correctly. But a count-based check needs thought: scalar(@failures) - 1 is -1 on a clean run, and if that number reaches arithmetic rather than a range it is nonsense. Chapter 5.7 gives the test that avoids the question entirely.

7. Common Mistakes

Looping to scalar(@array). One too far, and the extra read is silent. Use $#array or scalar(@array) - 1.

Expecting $#array to be the count. It is the last index — one less.

Expecting $# of an empty array to be 0. It is -1.

Writing to a far index by accident. The array inflates with undef and every count derived from it is wrong.

Expecting $#a = n to be reversible. Shortening discards the values; re-extending gives undef.

Using $#a = -1 to clear. Correct but obscure. Write @a = ();.

Using an index when you do not need one. for my $v (@array) has no bound to get wrong.

8. Exercise

Exercise 1 — both measurements. For a five-element array, print scalar(), $#, the first element, the last element two ways, and the third-from-last. Then write the position table from §1 for your array.

Exercise 2 — produce the off-by-one. Run the §2 script. Record both loops' output and the empty STDERR. Then add a line inside the wrong loop that interpolates the value into a sentence, and record what STDERR says now and which line it names.

Exercise 3 — the empty array. Declare an empty array and print scalar() and $#. Then loop 0 .. $#array and confirm zero iterations. Explain why, naming the chapter that established the behaviour of a backwards range.

Exercise 4 — the wrong bound on an empty array. Loop 0 .. scalar(@empty) over an empty array and count the iterations. Report the number and what $i was.

Exercise 5 — inflate an array. Start with three elements, assign to position 9, then print the count, $# and the contents with undef made visible. Say what a report reading scalar() would now claim.

Exercise 6 — truncation is not undo. Build a five-element array, set $# to 1, print it, then set $# back to 4 and print it again with undef visible. State in one sentence what happened to the original values.

Exercise 7 — drop the index. Rewrite a 0 .. $#array loop that only reads values as a for my $v (@array) loop. Then say which of the two you would use if the loop had to report the position of each failure.

9. Summary

  • scalar(@array) is the count; $#array is the last index. They differ by exactly one.
  • $marks[-1] is the last element and is preferable to $marks[$#marks].
  • Looping 0 .. scalar(@array) runs one iteration too many, reads undef, and warns about nothing.
  • Use 0 .. $#array or 0 .. scalar(@array) - 1 — or no index at all, with for my $v (@array).
  • On an empty array $# is -1, and the loop runs zero times because 0 .. -1 is the empty range — Chapter 3.10's backwards range being useful.
  • Writing past the end extends the array and pads with undef, silently inflating every count derived from it.
  • Assigning $#array truncates destructively — re-extending gives undef, not the old values. Clear with @a = ();.

Chapter 5.3 covers reading several positions at once, which turns "pick columns 1, 3 and 5 out of this row" into a single expression.

Where this fits

Part of the Perl curriculum.