The same election problem in a language with no dictionary, no string equality operator and no bounds checking: arrays of structs, reading candidates from argv, comparing with strcmp, writing a search that returns a boolean, printing winners in two passes so ties need no special case, and reading a segmentation fault back to its cause.
Subject: IT Support & Networking · 62 slides · code lesson
Open the interactive version of this deck · Homework for this lesson
Title
IT · Programming
The same problem, in a language with no dictionary and no safety net
Objectives
You have solved this problem before in a language that gave you a dictionary, string comparison and bounds checking. C gives you none of the three, and that is the whole content of this session.
CS50x — Harvard's introduction to computer science, Problem Set 3 (plurality) Problem Set 3 — the assignment this deck is built around
Section
Section 1
Warm-up
Two minutes, from memory.
Discussion prompt
In the Python version, three language features did most of the work. Name them, and say what each one was for.
Hint: How did you look a candidate up, compare a name, and stay inside the list?
Answer:
A dictionary, which mapped a name to a count in one lookup.
The equality operator, which compared two strings by their contents.
Bounds checking, which turned a bad index into an error instead of a wrong answer.
C has none of them. Everything in this deck is you supplying one of the three by hand.
Picture it
Shorter bar means the language did it for you.
Figure (svg): Six bars comparing Python's built-in dictionary, string equality and bounds checking against the manual equivalents in C
None of this makes C worse. It makes C explicit, and the assignment is graded on whether you were explicit correctly.
Concept
A struct groups a name and a count into one value. An array of those structs is your tally.
typedef struct
{
string name;
int votes;
}
candidate;
candidate candidates[MAX];
int candidate_count;| Python | C | why the difference |
|---|---|---|
| tally = {} | candidate candidates[MAX] | C needs the size at compile time |
| tally['Alice'] | candidates[i].name | no lookup by key; you search |
| len(tally) | candidate_count | the array does not know how full it is |
| tally['Dave'] -> KeyError | no error at all | C does not check |
The last row is why the vote function has to return a value saying whether the name was found.
Kernighan and Ritchie, The C Programming Language, 2nd edition, Ch. 5 and 6 Ch. 6 — structures
Picture it
MAX slots exist from the start. Only the first candidate_count of them mean anything.
Figure (svg): Four array slots, three holding candidate names and vote counts and the fourth greyed out as unused
Forgetting that second fact is the source of most bugs in this program.
Prediction
MAX is 9, three candidates were given on the command line.
Predict first
What does candidates[3].votes hold?
Correct: 0, because globals are zero-initialised.
Why: A global array in C is zero-initialised before main runs, so the unused slots really are zero. That is a fact about globals, not about arrays: the same array declared inside a function would hold whatever was on the stack. Either way the slot is meaningless, and candidate_count is what tells you to ignore it.
Section
Section 2
Concept
The two parameters of main are the number of words on the command line and the words themselves.
int main(int argc, string argv[])
{
if (argc < 2)
{
printf("Usage: plurality [candidate ...]\n");
return 1;
}
candidate_count = argc - 1;
}| what you type | argc | argv[0] | argv[1] | candidate_count |
|---|---|---|---|---|
| ./plurality | 1 | ./plurality | - | usage error |
| ./plurality Alice | 2 | ./plurality | Alice | 1 |
| ./plurality Alice Bob Charlie | 4 | ./plurality | Alice | 3 |
CS50 Manual Pages — get_int, get_string and the cs50 library — the cs50 string type is a typedef for char pointer
Picture it
Four words on the command line, three of them candidates.
Figure (svg): The four elements of argv with argv zero marked as the program name and the remaining three as candidate names
Worked example
Take the command-line words and set up the tally.
Reject a run with no candidates
Why: argc of 1 means only the program name was given. Returning a non-zero value tells the shell it failed.
Guard against too many candidates
Why: The array is only MAX long. Writing past it is not an error in C, so this check is the only thing standing between you and memory corruption.
Copy, offsetting the index by one
Why: Candidate i comes from argv[i + 1], because argv[0] is the program.
candidate_count = argc - 1;
if (candidate_count > MAX)
{
printf("Maximum number of candidates is %i\n", MAX);
return 2;
}
for (int i = 0; i < candidate_count; i++)
{
candidates[i].name = argv[i + 1];
candidates[i].votes = 0;
}| i | argv[i + 1] | candidates[i].name | candidates[i].votes |
|---|---|---|---|
| 0 | Alice | Alice | 0 |
| 1 | Bob | Bob | 0 |
| 2 | Charlie | Charlie | 0 |
Verify: that the loop stops before candidate_count
Why: Three candidates means indices 0, 1 and 2. The condition uses a strict less-than, so index 3 is never touched.
Picture it
Three candidates, four boxes reached if the condition is wrong.
Figure (svg): Four array slots with the first three valid and the fourth marked in red as outside the array
In Python this is an IndexError and you find it immediately. In C it is a wrong answer today and a crash next Tuesday.
Trap
A loop that means to cover every candidate.
Annotate
Get a program that works until it does not
Why: Small inputs often survive. The grader's larger input does not.
Strict less-than, every time.
for (int i = 0; i < candidate_count; i++)
{
candidates[i].name = argv[i + 1];
candidates[i].votes = 0;
}| candidate_count | i values with < | i values with <= | valid indices |
|---|---|---|---|
| 1 | 0 | 0, 1 | 0 |
| 3 | 0, 1, 2 | 0, 1, 2, 3 | 0, 1, 2 |
| 9 | 0 to 8 | 0 to 9 | 0 to 8 |
Read the condition out loud as 'while i is a valid index'
Why: That phrasing makes the strict less-than obviously right, and it works for every array in every language.
Pattern
Four steps, always in this order, in every C program that takes arguments.
| step | what it prevents |
|---|---|
| argc check | reading an argument that does not exist |
| MAX check | writing past the end of the array |
| the plus one | the program name being entered as a candidate |
| the strict less-than | one write past the last valid slot |
Kernighan and Ritchie, The C Programming Language, 2nd edition, Ch. 5 and 6 Ch. 5 — command-line arguments
Check
Solve it on paper before you click.
Check your understanding
The command is ./plurality Ada Bob. What is candidate_count?
Answer: A
Why: argc counts every word including the program name, so argc is 3 and candidate_count is argc minus 1, which is 2. The candidates are argv[1] and argv[2].
Section
Section 3
Concept
A string in C is a pointer to the first character. Comparing two pointers with the equality operator asks whether they point at the same place in memory, not whether the characters match.
Two identical names typed at different times live at different addresses, so the comparison is false even though the text is the same.
Kernighan and Ritchie, The C Programming Language, 2nd edition, Ch. 5 and 6 Ch. 5 — pointers and arrays
Picture it
The left panel asks about addresses. The right panel asks about characters.
Figure (svg): Two panels contrasting comparing two string pointers with the equality operator against comparing their characters with strcmp
Concept
strcmp does not return true or false. It returns a negative number, zero, or a positive number, telling you the alphabetical order of the two strings.
Equality is the zero case. Since zero is also what C treats as false, the correct test reads oddly the first hundred times you write it.
C reference — strcmp — the exact return-value contract
Picture it
Three comparisons, three kinds of answer.
Figure (svg): Three boxes showing strcmp returning a negative value, zero, and a positive value for three pairs of names
This is also why strcmp is what you use to sort names, not just to compare them.
Worked example
Search the array for the name. If you find it, add a vote and report success. If you reach the end, report failure.
Return a boolean so the caller can react
Why: The caller prints the rejection message. Keeping the printing out of vote makes the function testable and matches the spec's separation.
Compare with strcmp and test against zero
Why: Not against true. Zero is the equal case.
Return immediately on a match
Why: One vote can only go to one candidate, and returning early also stops the loop.
Return false after the loop
Why: Reaching this line means every candidate was checked and none matched.
bool vote(string name)
{
for (int i = 0; i < candidate_count; i++)
{
if (strcmp(candidates[i].name, name) == 0)
{
candidates[i].votes++;
return true;
}
}
return false;
}| vote | i = 0 Alice | i = 1 Bob | i = 2 Charlie | returns |
|---|---|---|---|---|
| Alice | match | - | - | true |
| Bob | no | match | - | true |
| Dave | no | no | no | false |
| alice | no | no | no | false |
Verify: that the last row is what you want
Why: strcmp is case sensitive, so a lower-case name is rejected. If the spec wants case-insensitive matching, that is a different function and it should be a deliberate choice.
Invariant
Three candidates, one vote for Dave. Watch where control goes.
Step through it
Which line proves the array was not modified, and where would the increment have happened if the name had matched?
The return after the loop is not a fallback. It is a proof: the only way to reach it is to have compared against every candidate and failed.
Trap
The comparison written the way it reads in English.
Annotate
Watch every vote go to Alice
Why: Unless the voter typed Alice, in which case it goes to Bob.
Test against zero explicitly.
if (strcmp(candidates[i].name, name) == 0)
{
candidates[i].votes++;
return true;
}| names | strcmp returns | bare condition | == 0 condition |
|---|---|---|---|
| Alice vs Alice | 0 | false | true |
| Alice vs Bob | negative | true | false |
| Charlie vs Bob | positive | true | false |
Read the comparison out loud as 'the difference is zero'
Why: strcmp measures a difference. Zero difference is a match, and saying it that way makes the test obvious.
Elimination
Comparing a submitted vote against a candidate name.
Eliminate the wrong options
Which line belongs in vote?
Survives elimination: A
Why: Zero means the strings are identical. The other three either compare the wrong thing, invert the test, or rely on a value the standard does not promise.
Matching
Same operation, different amount of machinery.
Match the pairs
Why: Every row on the right is a loop or a function call where the left is an operator. That is not C being awkward; it is C declining to hide the work, which is also why it is fast.
Pattern
This loop shows up in every C assignment you will be set. Learn it as a shape.
Kernighan and Ritchie, The C Programming Language, 2nd edition, Ch. 5 and 6 Ch. 6
Check
Solve it on paper before you click.
Check your understanding
The candidates are Alice, Bob and Charlie. What does vote("Dave") return, and what changes in the array?
Answer: A
Why: The loop compares Dave against all three names, none matches, and control reaches the return after the loop. No increment happened, so the array is untouched.
Section
Section 4
Concept
Find the highest vote count first. Then print every candidate who has it. Two passes, and ties fall out with no extra code.
CS50x — Harvard's introduction to computer science, Problem Set 3 (plurality) Problem Set 3
Picture it
You cannot print the winners until you know the maximum, and you do not know the maximum until you have seen everyone.
Figure (svg): A four step flow: first loop finds the maximum, nothing printed yet, second loop prints everyone equal to it, and a tie prints two names
Worked example
Two loops over the same array, doing two different jobs.
Start the maximum at zero
Why: Vote counts are never negative, so zero is a safe floor. If a candidate could have a negative score you would start from the first element instead.
First loop: keep the largest count seen
Why: Only the number is remembered, not who had it, which is what makes the tie case work.
Second loop: print everyone matching
Why: Equality, not greater-than. Every candidate on the maximum is a winner.
void print_winner(void)
{
int max = 0;
for (int i = 0; i < candidate_count; i++)
{
if (candidates[i].votes > max)
{
max = candidates[i].votes;
}
}
for (int i = 0; i < candidate_count; i++)
{
if (candidates[i].votes == max)
{
printf("%s\n", candidates[i].name);
}
}
}| candidate | votes | after loop 1 | printed in loop 2? |
|---|---|---|---|
| Alice | 3 | max = 3 | yes |
| Bob | 3 | max still 3 | yes |
| Charlie | 1 | max still 3 | no |
Verify: against a clear win
Why: With Alice on 3, Bob on 2 and Charlie on 1, max is 3 and only Alice prints. The same code handles both cases with no branch anywhere.
Invariant
Four candidates, and the maximum only ever goes up.
Step through it
What would break if the first loop used greater-than-or-equal instead of greater-than?
Nothing, for the value of max. It would still end at 3. Using greater-than is a habit worth keeping anyway, because in the version that also records who is winning, the two differ.
Trap
A single-pass attempt at finding and printing the winner.
Annotate
Get output that is wrong in two opposite directions
Why: Too many names in one case and too few in the other.
Separate finding from printing.
int max = 0;
for (int i = 0; i < candidate_count; i++)
if (candidates[i].votes > max)
max = candidates[i].votes;
for (int i = 0; i < candidate_count; i++)
if (candidates[i].votes == max)
printf("%s\n", candidates[i].name);| tally | one-pass output | two-pass output |
|---|---|---|
| Alice 1, Bob 3 | Alice, Bob | Bob |
| Alice 3, Bob 3 | Alice | Alice, Bob |
| Alice 3, Bob 1 | Alice | Alice |
Notice the third row agrees
Why: Which is exactly why the bug survives testing: the obvious case works.
Prediction
You test with Alice 3, Bob 1, and the one-pass version prints Alice. Correct.
Predict first
Which input would expose the bug?
Correct: One where the eventual winner is not first in the array.
Why: The one-pass version prints every candidate who was ever ahead. When the winner is listed first, nobody else ever leads, so the output happens to be correct. Ordering the candidates so the winner comes last is the test that exposes it.
Section
Section 5
Concept
Under sixty lines, and every one of them is something this deck has covered.
#include <cs50.h>
#include <stdio.h>
#include <string.h>
#define MAX 9
typedef struct
{
string name;
int votes;
}
candidate;
candidate candidates[MAX];
int candidate_count;
bool vote(string name);
void print_winner(void);
int main(int argc, string argv[])
{
if (argc < 2)
{
printf("Usage: plurality [candidate ...]\n");
return 1;
}
candidate_count = argc - 1;
if (candidate_count > MAX)
{
printf("Maximum number of candidates is %i\n", MAX);
return 2;
}
for (int i = 0; i < candidate_count; i++)
{
candidates[i].name = argv[i + 1];
candidates[i].votes = 0;
}
int voter_count = get_int("Number of voters: ");
for (int i = 0; i < voter_count; i++)
{
string name = get_string("Vote: ");
if (!vote(name))
{
printf("Invalid vote.\n");
}
}
print_winner();
}| section of main | what it does | failure it guards against |
|---|---|---|
| the argc check | requires at least one candidate | a run with no ballot |
| the MAX check | refuses too many candidates | writing past the array |
| the copy loop | fills the array from argv | - |
| the voting loop | reads and counts each vote | - |
| the vote check | prints the rejection | an unknown name being counted |
The two function prototypes above main are what let main call functions defined below it. C reads the file top to bottom and will not use a name it has not seen.
CS50x — Harvard's introduction to computer science, Problem Set 3 (plurality) Problem Set 3
Picture it
Four votes, one of them invalid.
Figure (svg): A terminal compiling the program with make and running it with three candidate names, rejecting one vote and printing the winner
Note that make prints nothing on success. Silence is the good outcome.
Concept
A segmentation fault means the program touched memory it does not own. In a program this size it is nearly always an index outside an array, or a string pointer that was never set.
A wrong answer with no error at all is the other symptom, and in C it usually means the same thing: you read a slot that was never written.
$ ./plurality Alice Bob
Segmentation fault (core dumped)
$ debug50 ./plurality Alice Bob
# or:
$ valgrind ./plurality Alice Bob| symptom | usual cause here | tool that finds it |
|---|---|---|
| segmentation fault | an index past the end of an array | debug50 or gdb, then read the line number |
| silently wrong count | reading an uninitialised slot | valgrind, which reports the uninitialised read |
| every vote goes to one candidate | the strcmp test written without == 0 | reading the condition out loud |
| compiles but does nothing | the program was rebuilt without being re-run | look at the shell history |
Valgrind Quick Start Guide — the quick start guide is genuinely ten minutes
Matching
Five things that go wrong in this program, and what each one means.
Match the pairs
Why: Four of the five are things the compiler cannot detect, which is the real difference between C and a language with runtime checks. The fifth is a warning that people learn to ignore, and it should never be ignored.
Socratic
Worth two minutes, because the answer explains a lot of C.
Discussion prompt
Python raises IndexError. C returns whatever bytes are there. Why would a language be designed that way?
Hint: What would a check cost, and on what kind of machine was C designed to run?
Answer:
Because checking every index costs a comparison and a branch on every array access, and C was designed for systems where that cost was not acceptable.
The bargain is explicit: the language will not slow you down, and in exchange you guarantee your own indices are valid.
Which means a bounds bug in C is not the language failing. It is you not holding up your end, and the tools that catch it are external ones like valgrind.
Kernighan and Ritchie, The C Programming Language, 2nd edition, Ch. 5 and 6 Ch. 5
Trade off
Fill in what each column costs you.
Comparison matrix
| Python | C | |
|---|---|---|
| lines of code | about 25 | about 55 |
| a bad index | IndexError, immediately | silent corruption, or a crash much later |
| string comparison | == | strcmp, tested against zero |
| what you gain | speed of writing | speed of running, and control over memory |
Neither is the better language. They are different bargains, and the assignment is asking you to demonstrate that you understand which one you are in.
Ranking
When a C program misbehaves, in this order.
Put in order
Why: Warnings first because they are free and they name real bugs. Then the two mistakes this specific program invites. The tools come last, not because they are weak, but because in a fifty-line program the first three steps usually find it before you have finished typing the debugger command.
Section
Section 6
Concept
Graders do not test the case you had in mind. They test the boundaries, because that is where programs break.
CS50x — Harvard's introduction to computer science, Problem Set 3 (plurality) — check50 runs exactly this kind of input
Picture it
Run every one of these by hand. It takes about three minutes.
Figure (svg): Five boxes listing the edge cases: no candidates, too many candidates, zero voters, all votes invalid, and the winner listed last
The last one is the input that catches the one-pass print_winner bug, and it is the one nobody thinks of.
Worked example
The command line has three candidates and the user answers 0 at the prompt.
The setup loop runs normally
Why: Three candidates go into the array, each with zero votes.
The voting loop body never runs
Why: The condition is i less than zero, which is false immediately, so the loop is skipped entirely rather than running once.
print_winner still runs
Why: max starts at zero, no candidate exceeds it, so max stays zero.
Every candidate equals the maximum
Why: All three have zero votes, so all three print.
| candidate | votes | equals max of 0? | printed |
|---|---|---|---|
| Alice | 0 | yes | yes |
| Bob | 0 | yes | yes |
| Charlie | 0 | yes | yes |
Verify: that this is defensible rather than a crash
Why: With no votes cast, every candidate genuinely is tied for the lead. The program does not crash and its output is arguably correct. If the spec wants something else it has to say so.
Picture it
Three candidates, no votes, and a maximum of zero that every one of them reaches.
Figure (svg): Three bars all at zero votes, each marked as equal to the maximum and therefore printed
The program neither crashes nor invents a winner, which is exactly the behaviour to aim for when the spec is silent.
Missing information
The spec says: read the candidates, read the votes, print the winner.
Discussion prompt
Name three behaviours the grader will exercise that the spec never states.
Hint: All three are about inputs a careless user would produce by accident.
Answer:
What to print when two candidates tie. Almost every spec omits it and almost every grader tests it.
Whether the comparison is case sensitive, and therefore whether a lower-case name is a valid vote.
What to do with zero voters, where every candidate ties on zero.
When the spec is silent, choose the behaviour that cannot crash and leave a comment saying which choice you made.
Two truths and a lie
The argc check and the MAX check at the top of main.
Eliminate the wrong options
Which is true?
Survives elimination: A
Why: The check is not a formality; it is the only thing between the input and a buffer overrun. Placing it after the loop would be like locking the door on the way out.
Commit first
Answer, then rate your confidence honestly.
Predict first
./plurality Alice Bob Charlie, four voters, all four vote for Charlie. What prints?
Correct: Charlie.
Why: max becomes 4 after the first loop, and only Charlie equals it in the second, so one name prints once. The second loop prints each candidate at most once because it iterates the candidate array, not the votes.
Section
Section 7
Concept
Plurality stores one name per voter. Runoff stores an ordered list of preferences per voter, which in C means a rectangular array indexed by voter and by rank.
int preferences[MAX_VOTERS][MAX_CANDIDATES];
// voter j's rank-k choice, stored as a candidate index
preferences[j][k] = i;| stored | plurality | runoff |
|---|---|---|
| per voter | one name | an ordered list of candidate indices |
| array shape | one dimension over candidates | two dimensions, voters by ranks |
| passes over the data | one | one per elimination round |
| what is stored in a cell | not applicable | an index into candidates, not a name |
Storing the index rather than the name is the trick that makes elimination cheap: marking a candidate eliminated is one boolean, and no ballot has to be rewritten.
CS50x — Harvard's introduction to computer science, Problem Set 3 (plurality) Problem Set 3 — runoff follows plurality in the same problem set
Picture it
Plurality only ever reads the first column.
Figure (svg): A table of three voters and three ranked preferences each, with the first-choice column highlighted
Comparison
Fill in what runoff has to add.
Comparison matrix
| piece | plurality | runoff |
|---|---|---|
| candidate struct | name and votes | name, votes, and an eliminated flag |
| the tally loop | runs once | runs once per round, over non-eliminated candidates |
| winning condition | highest count | more than half of the remaining votes |
| what happens on no winner | not applicable | eliminate the lowest and count again |
Note how much carries over unchanged. Writing vote and print_winner as separate functions now is what makes runoff an extension rather than a rewrite.
Explain it
Two sentences, out loud.
Discussion prompt
Why does runoff store a candidate index in each cell rather than the candidate's name?
Hint: How would you check whether a ballot's current choice has been eliminated?
Answer:
Because an index is a number you can use to look the candidate up directly, including their eliminated flag, without any string comparison at all.
Storing names would mean running strcmp on every cell of the table in every round, which is both slower and one more place to get the comparison wrong.
Kernighan and Ritchie, The C Programming Language, 2nd edition, Ch. 5 and 6 Ch. 5
Check
Solve it on paper before you click.
Check your understanding
Which part of the plurality program carries over to runoff essentially unchanged?
Answer: A
Why: Reading candidates off the command line and guarding against too many of them is identical in both. That is why it is worth writing carefully once.
Pattern
Six rules, and the second and third between them account for most lost marks.
| do this | instead of | because |
|---|---|---|
| candidate_count = argc - 1 | argc | argv[0] is the program name |
| i < candidate_count | i <= candidate_count | n items means indices 0 to n minus 1 |
| strcmp(a, b) == 0 | a == b, or bare strcmp(a, b) | one compares addresses, the other inverts the test |
| return a bool from vote | printing inside vote | the caller owns the message |
| two loops in print_winner | one loop that prints as it goes | the maximum is not known until the end |
| check candidate_count against MAX | trusting the input | C will let you write past the array |
CS50x — Harvard's introduction to computer science, Problem Set 3 (plurality) Problem Set 3
Check
Solve it on paper before you click.
Check your understanding
Why must print_winner use two loops rather than one?
Answer: A
Why: Any candidate could turn out to have the highest count, including the last one. Printing during the first pass means printing before you know what the maximum is.
Exit ticket
One honest answer.
Predict first
Which of these would you least want to write from scratch right now?
Correct: Whichever you named is where the next session starts.
Why: The first three are each about fifteen minutes of drill. The fourth is a different kind of skill and worth a session of its own, because it transfers to every C program you will ever write rather than just this one.
Connect it up
Blank file, thirty minutes.
Draw it
From the pattern card alone, write main, vote and print_winner. Compile it. Then run it with the winner listed last on the command line, and with two candidates tied.
Those two inputs are the ones the grader will use. Anything that fails them is the agenda for next session.
Recap
Five sections, and the three C-specific traps in the middle are where the marks actually go.
| input | expected output |
|---|---|
| ./plurality with no candidates | usage message, exit code 1 |
| ten candidate names | maximum message, exit code 2 |
| Alice 3, Bob 2, Charlie 1 | Alice |
| Alice 3, Bob 3, Charlie 1 | Alice then Bob |
| a vote for Dave | Invalid vote. |
CS50x — Harvard's introduction to computer science, Problem Set 3 (plurality) — the specification, the test cases, and the style checker
Want this taught 1-on-1? Alexander tutors IT Support & Networking — $55/session, free consultation.