Read this first: Tests for Programming Interview: Part 1.
In that first posting I talked about the success rate of people asked to perform a simple coding test of the sort I give to job candidates. The test is intended to be simple so it doesn't take long and can be done under stress, and it's not the end point, it's the starting point. It's the start of a conversation about coding in general.
However,
before
showing
and
talking
about
the
code
I
received,
here's
the
code
as
I
would
derive
it.
And
I'm
not
just
going
to
present
you
with
code,
I'm
going
to
write
it
as
you
watch,
with
a
commentary
as
I
go.
This isn't the only way to write this code, it's the way I write this code. Firstly, here at right is what we're given as a specification:
|
|
void condense_by_removing( char *z_terminated , char char_to_remove ) { char *p_read = z_terminated; char *p_rite = z_terminated; } |
They
will
both
start
at
the
beginning
of
the
given
string,
so
I
can
initialise
them
now:
|
I'll
carry
on
incrementing
the
read
pointer
down
my
string
until
I
reach
the
end,
which
is
a
0
char:
void condense_by_removing( char *z_terminated , char char_to_remove ) { char *p_read = z_terminated; char *p_rite = z_terminated; while (*p_read) { // Do something p_read++; } } The while test could be coded as (*p_read!='\0') but it's natural and idiomatic in C simply to test as I've written it. In other languages it's more natural to have the specific test rather than the implicit coercion.
|
|
Now
for
each
character
I
look
at,
I'll
decide
if
it
should
be
copied
to
the
destination:
There's a duplication there in the dereferencing of p_read, but a sensible compiler will keep it in a register anyway. I could explicitly copy it to a variable, but I won't. |
void condense_by_removing( char *z_terminated , char char_to_remove ) { char *p_read = z_terminated; char *p_rite = z_terminated; while (*p_read) { if (*p_read != char_to_remove) { // Do the copy } p_read++; } } |
void condense_by_removing( char *z_terminated , char char_to_remove ) { char *p_read = z_terminated; char *p_rite = z_terminated; while (*p_read) { if (*p_read != char_to_remove) { *p_rite++ = *p_read; } p_read++; } } |
If
I
don't
want
to
copy
the
character
in
question
I
don't
have
to
do
anything,
but
if
I
do,
then
I
copy
it
to
the
destination.
|
Finally,
when
I'm
done,
I
need
to
terminate
the
destination.
The
terminating
'\0'
wasn't
copied
because
that's
when
my
while
loop
bailed
out.
By the method I've used to derive the code I'm pretty sure it's right - the derivation itself is effectively a proof (of sorts) that it's doing what it needs to do, and satisfies the requirements. I have:
|
void condense_by_removing( char *z_terminated , char char_to_remove ) { char *p_read = z_terminated; char *p_rite = z_terminated; while (*p_read) { if (*p_read != char_to_remove) { *p_rite++ = *p_read; } p_read++; } *p_rite = '\0'; } |
void condense_by_removing( char *z_terminated , char char_to_remove ) { char *p_read = z_terminated; char *p_rite = z_terminated; for (;*p_read;p_read++) { if (*p_read != char_to_remove) { *p_rite++ = *p_read; } } *p_rite = '\0'; } |
So
far
so
good.
At
least
at
this
stage
I
hope
I've
proved
that
I
can
write
code,
and
reason
about
it
effectively.
Now I can start to mutate the code. Each of the following mutations is unnecessary, but valid for consideration.
Firstly, I notice that every time through the while loop p_read gets incremented. That means we could convert the while into a for and consider myself as looping over the elements of the input string.
|
We
could
also
put
the
declaration
and
initialisation
of
p_read
into
the
for
loop:
Note that in older flavors of C the declaration inside the for loop wasn't possible.
|
void condense_by_removing( char *z_terminated , char char_to_remove ) { char *p_rite = z_terminated; for (char *p_read = z_terminated;*p_read;p_read++) { if (*p_read != char_to_remove) { *p_rite++ = *p_read; } } *p_rite = '\0'; } |
void condense_by_removing( char *z_terminated , char char_to_remove ) { for ( char *p_read = z_terminated; *p_read; p_read++) { if (*p_read != char_to_remove) { *z_terminated++ = *p_read; } } *z_terminated = '\0'; } |
Now
I
can
observe
that
p_rite
gets
initialised
to
z_terminated
and
then
z_terminated
never
gets
referenced
again,
so
we
could
just
use
z_terminated
instead.
(I'm not going to get into arguments about the specifics of the formatting. Everyone has their own opinions, and there is rarely any common ground. It usually turns out to be less important than people think, and most people can adapt to a style that's at least acceptable to others, even if not preferred.)
|
As
a
final
tweak,
the
braces
are
mostly
unnecessary,
and
could
be
removed,
and
the
names
z_terminated,
char_to_remove
and
p_read
can
be
replaced
with
something
more
succinct:
Each of these changes is unnecessary and in some cases damage readability. Further, a good compiler will get most, if not all, of the efficiency for you without making these changes. Having said that, this final version of the for loop is quite idiomatic and natural for the experienced C coder. Will all your future maintainers be experienced C coders?
|
void condense_by_removing( char *str , char remove ) { for (char *rd = str;*rd;rd++) if (*rd != remove) *str++ = *rd; *str = '\0'; } |
It is also worth noting, though, that this final version can be read quite simply as:
Plans for the next articles are: