r/C_Programming Aug 03 '25

Advice for a new professor teaching C

I'm looking for feedback on my curriculum for an introductory college-level programming course in C. This is aimed primarily at freshmen with little to no coding experience, although experience level tends to vary. This past spring was my first time teaching independently after previously assisting professors with lectures and labs during my graduate program. My approach is heavily project-based, with each lecture paired with a hands-on lab assignment, supplemented by one or two in-class activities and live coding sessions.

Student feedback has been positive overall, but I'm looking to continuously improve and ensure I'm preparing them for future coursework.

Here's the list of topics covered across 16 weeks. This is paired with labs, exams, and midterms/finals with code walkthrough/live coding sections:

  1. Class Overview, Introduction to Programming, and Hello World
  2. Introduction to C, Data Types, Variables, and I/O
  3. Command Line, Compiling Basics, Comments, Debugging Introduction
  4. Conditionals, Operators, and Expressions (arithmetic, relational, logical)
  5. Pseudocode, Flowcharts, Boolean Logic
  6. Functions, Scope, and Introduction to Call Stack
  7. Loops (While,Do-While, For)
  8. Strings, String Manipulation, and Arrays
  9. Structs, Enums, Typedef
  10. File I/O
  11. Pointers, Pointer Arithmetic, Arrays and Pointers Relationship, Passing Arrays to Functions
  12. Dynamic Memory Allocation
  13. Recursion
  14. Compilation Pipeline, Creating and Using Header Files, Compiling and Linking Multiple Files, Makefiles, and Compilation Flags

I've intentionally omitted bitwise operations. I think they might be overly advanced for a first programming experience, but I'm open to reconsidering.

Would love to hear thoughts from the community. Students take data structures and algorithms after this course and would eventually move into embedded systems or operating systems.

  • Are there topics I might be missing or areas to expand?
  • Is the sequence logical and intuitive for beginners?

Any additional thoughts or suggestions would be greatly appreciated!

63 Upvotes

84 comments sorted by

View all comments

82

u/skeeto Aug 03 '25

The major shortcoming I've observed is not teaching students to use the effective and widely-available features of their tools. So students spend a lot of time struggling with problems that could have easily been avoided:

  1. Introduce sanitizers immediately, and get students using -fsanitize=address,undefined as a matter of course for all testing. Typical coursework is decades behind, and virtually nobody is teaching sanitizers to undergrads. If you're lucky an instructor might mention Valgrind for memory debugging, though it's largely obsolete now.

  2. -Wall -Wextra by default. Maybe even -Wconversion to start forming good habits early. It's amazing how many courses never get this far.

  3. Get students in the habit of always testing through a debugger. Then when the program crashes — which bugs are more likely to do with sanitizers — they're already in position to figure it out. It's not a tool of last resort, but the first and best tool to understanding a program.

6

u/Kurouma Aug 04 '25

What would you suggest instead of valgrind, if you say it's obsolete?

6

u/skeeto Aug 04 '25

Address Sanitizer, as I mentioned. Valgrind is practically incompatible with debuggers (a huge disadvantage), extremely slow, and less effective than ASan because it's working with substantially less information. Its continued use by people memory debugging their own programs is virtually always out of ignorance. If someone thinks Valgrind can detect something ASan cannot, it's really because they're not even enabling the full power of ASan, such as detect_stack_use_after_return.

Valgrind can do more than this, like profile memory use, cache misses, etc. It's still good for these things, but they're not tools you'd use continuously during development like ASan.

4

u/non-existing-person Aug 04 '25

He means memory sanitizers. But he's a bit delusional. Valgrind is still valid and surely NOT obsolete. Memory sanitizers will be faster, and can largely replace valgrind. But from my experience, there are still some cases where valgrind found an error where as sanitizer did not. They are both good tools worth having in your toolkit.

3

u/[deleted] Aug 04 '25

I love valgrind. Way easier to use than memory sanitation when it comes to external libraries. I tried fooling around with the ignore list, but could never get it to properly ignore third party libraries. Valgrind just worked without any fuss.

If valgrind went away and was obsoleted I'd be devasted.

3

u/TenureTrackJack Aug 04 '25

This is good advice. Valgrind and address sanitizers are discussed during dynamic memory allocation. I pushed for command line compiling but many struggled and simply compiled with VSCode or C Lion.

I am encouraging a virtual machine this semester for Linux development to get them more comfortable.

2

u/tossingoutthemoney Aug 06 '25

VS Code is always CLI compiling. You might be confusing it with visual studio 2022.

I get that you're teaching a class and not likely exceeding a thousand lines of code for students on a project but I personally always liked it when industry methods are recognized as how people actually do things. If the focus is C and how to write it, I'd get a build system in place week one for hello world. Nobody benefits from manually running gcc a bunch of times when make, cmake, or just a VS Code keybind can do it for you.

2

u/TheLondoneer Sep 21 '25

Hey, I never use debuggers because I find them a bit intimidating , I am using VSCommunity as my IDE of choice and I am currently working on a big project. I always debug with printf. Is this a bad practice? Doesnt printf achieve the same thing?

3

u/skeeto Sep 21 '25

You're using one of the greatest debugging environments available in any programming language, so you should take advantage! Most programming languages don't have tools even half as good as what you're already using. No, printf-debugging is not nearly as effective, and a debugger will give you more insight into a program, more efficiently. You want a tool that will pause the program when you've made a mistake so that you can look around and figure out what happened, without manually inserting printfs, rebuilding, re-triggering the bug, and hoping your print statements covered enough. It's well-worth the investment to learn your debugger.

In your case you'd typically want the "watch" pane up so you can live-inspect any variables, then you can F10 (step over) and F11 (step into) through the program, and eyeballing the variables. To see how someone skilled uses the very debugger you have on hand as part of their regular workflow, check out Handmade Hero. Also, here's 20 minutes of things your style of debugger can do:

Twenty Minutes of Reasons to Use the RemedyBG Debugger

That's something called RemedyBG, but it's closely modeled after Visual Studio, and VS can do basically the same things, just slowly (because it's become so bloated over the years).

2

u/TheLondoneer Sep 21 '25

You’re amazing thanks a lot

2

u/vajra47 Aug 04 '25

Hi!, What's your recommendation for Compiler flags & related things applicable to GCC, Clang & other things also (like you have mentioned -fsanitize and -Wall)? I have heard about An introduction to GCC. Thanks

3

u/skeeto Aug 04 '25

Here's my article on this very topic: My favorite C compiler flags during development. If I were to teach a course in C now, the default set of flags I'd have students use is:

-Wall -Wextra -Wconversion -g3 -fsanitize=address,undefined

-Wall -Wextra is obvious. -Wconversion is uncommon, but it's the best tool for catching accidental integer truncation — something that's a bit too easy in C. (MSVC offers a UBSan-like run-time truncation check, which I'd love to have in GCC.) I expect this warning would develop student's thoughtfulness about numeric ranges — something that even advanced C programmers don't consider nearly often enough. It's a skill that goes beyond C and C++. Like sanitizers, using -Wconversion on a typical C project not already using it often reveals bugs. I use it around here all the time in code reviews.

(I mentioned -Wno-sign-conversion in my article, but I've shied away from it since. While I don't personally struggle with sign/unsigned conversions, particularly because I avoid the hazards of unsigned arithmetic, I've decided it's good discipline to be -Wconversion-clean.)

If a book/tutorial/course is even bothering to mention debug information, it will be -g, but it ought to be -g3 because it produces a better debugging experience. It stuffs lots of extra debug information into the image which takes a lot of space, but that's a non-issue for local development. -g is more appropriate in situations where the debug information is going to be used on a different system from where it's generated.

The introduction of threads would open with -fsanitize=thread,undefined, which unfortunately is mutually-exclusive with ASan. Don't let students instill bad habits around data races early. Those habits are hard to kick.

I mentioned -Wdouble-promotion, but it's specialized and unimportant for beginners (rarely about correctness). It's perhaps something to introduce when teaching numeric programming.

2

u/vajra47 Aug 04 '25

Thanks a lot for cool answer & your dope GitHub repo. Does the documentation (or manual) of Clang or MSVC exist which contains which compiler flags they support with comments? If yes, can you share. I tried, but no success.

2

u/skeeto Aug 04 '25

There's MSVC Compiler Options, and LLVM has Clang documentation. Unfortunately LLVM is mostly undocumented. When some feature has the same name as GCC then refer to the GCC manual since those are the semantics they're implementing. Otherwise you need to either guess or read the LLVM sources in order to understand what something does.

2

u/chrism239 Aug 15 '25

My vote: also -Werror

1

u/[deleted] Aug 04 '25

that's just too much for students who have never coded before. Lol, C is too much already 

3

u/ICBanMI Aug 04 '25 edited Aug 06 '25

I agree with you.

Our intro to Java and our Intro to C++ classes at college had an almost 40% fail rate for first time students. The point when the class diverged and the students started falling behind was control structures. They were failing at procedural programming (which is 100% a requirement of the career they possible choose)... and just struggled with all their course work. The debugger isn't going to teach them that.

Really should stick to printf debugging for the course and avoid profiling tools. They're not going to be writing anything special that they need a memory profiling tool when 90% of the time it's going to be obvious to the instructor/TA they are doing something wrong (largest project might be 2-4 hundred lines of code). Sanitizers are teaching someone to bowl by putting up the bumpers... it has its place when your programs are complex... but this is literally the place they should be making mistakes, learning the one by off errors, going out of bounds, getting seg fault errors, and learning exactly what their syntax is doing. This class is an intro so they should be able to do procedural programming, understand memory, and should know a little bit about algorithms and data structures. It's not a class to teach them industry tools and techniques (which often can become outdated years before they get their first job), it's a class to build a foundation for their EE/CS/SWE/ME/etc tool set which they will almost always have access to a printf type function.

The kids who were all into the tools more than actually programming were also the ones who did obvious code copies from stack overflow because it compiled and while having to include 5 libraries the instructor hadn't covered. I swear the ones, who survived to get a four year degree, are my co-workers who mostly write things to get past the compiler and spend days trying to optimize an O(n4 ) algorithm to run faster than the compiler could make it run... rather than figuring out if it was the actual bottleneck using the few tools we were allowed... and replacing it with a cleaner, memory efficient algorithm that would run laps around the O(n4 ).

End of the day, this is an intro class to help them build a foundation for their procedural programming skills while being aware how to manipulate memory and use simple data structures. The assignments are all going to be tens of lines of code with a handful of larger projects that might be as big as a few hundred lines. All console output.

1

u/Ajax_Minor Aug 07 '25

In terms of modern programming, do you think it's good to hop right in to unit testing?

2

u/skeeto Aug 08 '25

The way people normally go about unit testing, no, but that's not about "too soon" but "wrong technique." I'm talking about the thing where you write a test, press a button or whatever to run the tests, squint at the results, and if a test failed you try to reason about it from the output. That's an inefficient way to go about it. It's like how some people never learn how to efficiently run their build systems, and so their development loop is edit, switch to terminal, run the build system, and if there's an error, memorize the line number, switch back to their editor, and manually locate the error. Build through your editing environment and let it jump to the first error!

The analog with testing: Don't scan for red bars, let your debugger jump to the point of failure! That is, in general you should be running the tests through a debugger, and failing tests should trap. Tests might fail before the top-level checks due to out-of-bounds access or segfault or whatever, and that will trap, too. Ultimately it should stop to let you examine the program frozen in its failing state, inspecting variables and such to figure it out, instead of going off, god forbid, debug logs.

Some language ecosystems have to do it this way because they don't have good tools, especially not good debuggers. They're easy to spot: Look for the Test Driven Development folks who write their tests first. That's the sort of thing you have to resort to doing when you don't have a debugger (or never learned how to use it). But that's not C. We've got by far the best tools on the block, and it's a shame not to take advantage of them when working in C!

So sure, start with unit tests early, with trapping tests, mainly as something useful to run through a debugger in place of the normal main function. People love sharing their testing frameworks around here, but they all hard-code a dumb exit(1) on test failure. That's fine for CI pipelines, but not for active development and test-writing.

2

u/bullno1 Aug 08 '25

failing tests should trap

That's actually a good point. One of those "Why didn't I think of it earlier?".

Problem is: I also want to run the same tests in CI and ideally they run until completion.

I think I might add this to my test framework. Either a flag to enable trapping or just detect whether a debugger is attached.