TransWikia.com

What should be included in a best practices C course?

Computer Science Educators Asked by Git on August 21, 2021

At our University, we have in the first semester a very difficult C Introductory Course, that consists of presenting a shortened version of the language specification: What are for/while loops, if clause, what is a pointer and so on.

But they never show how good C code in a more complicated application should look. What version control is, what Valgrind is (a gnu tool for detecting memory leaks) and so on.
For the many new (about 60%) to programming or C, the group projects are quite a knockout (about 40%).

Some motivated students and I decided to offer a “best practices” session before the first assignment is handed out and after the lecture has finished.
For the students to have a better chance of finishing this course successfully.

Our selected Chapters are these:

  • how to compile (clang and gcc and their flags and warnings)
  • valgrind
  • make
  • coding style
    • best practices -> how to allocate memory, typedefs, file operations
    • basic program flow, some simple patterns
  • git
  • pitfalls specific to this course and C in general
  • how to ask the right question (and in turn how to google their problems)
  • how to use the manpage

Are there points missing? Is there something that we shouldn’t do?
In short, how to improve the content. We are planning on 3 Sessions with around 3 hours each.

12 Answers

Too much, too fast

"First semester". If I parse correctly, you say 60% students are new to programming.

You say "The following points knock them out: they don't know how to test, they don't know how to check for memory leaks, they have no concept of encapsulating". And you say "group assignments".

Taken together, it looks like this course is "introductory" only in this sense it will introduce part of students to the idea that this university doesn't want to teach them (programming or even teach them how to learn programming), but it wants to filter them out.

You cannot change that fact. You cannot help everyone, you can help some.

Either you want:

  • to help programmers to become better
  • to help some non-programmers to become programmers (will work 10% of the time in your setting)
  • to help all non-programmers to learn a bit, but probably not enough to let them pass

Presently your mini-course is a big mish-mash. Come on: someone needs to be informed how to google stuff? Really? And the next minute you talk about git? To help who, in what way...? Don't waste everyone's time, narrow it down:

  • to help programmers to become better

    • valgrind
    • coding style
      • best practices -> how to allocate memory, typedefs, file operations
      • basic program flow, some simple patterns
    • git
    • pitfalls in general
  • to help some non-programmers to become programmers (will work 10% of the time in your setting)

    • you want to give them ready-made stuff; their heads are bound to explode, the more boilerplate you give them, the less the explosion
      • make
        • boilerplate gcc flags included, etc
      • pitfalls specific to this course
      • the rest will come later (i.e. someday they will decide they need to improve on memory handling - but not today for sure, someday they will decide to migrate version control out of their Facebook - not today)
  • to help all non-programmers to learn a bit, but probably not enough to let them pass
    • "how to google their problems"
    • "how to use the manpage"

Correct answer by kubanczyk on August 21, 2021

Besides all of these good answers, I want to add one thing. Make them familiar with the world of Competitive Programming and Problem Solving where they would learn by playing. They would learn how to approach and solve a problem. Some of the most common competitive programming and problem solving platforms are:

  • URI OJ - There are a lot of problems here for the beginner from adding two numbers to much more.
  • UVa OJ - Here, they would learn how computer program works for checking two files! Statistics of this site is better seen here in uHunt.
  • Codeforces - This is a good site for competitive programming, as well as problem solving. New problems come in almost every week. The most important part here is, one could see other's code after a contest. Editorials for problems are available. This is a Russian platform.
  • At Coder - Similar platform like Codeforces, but this is a Japanese platform. Don't worry, in both of the platforms, English version is available.
  • CodeChef - This is another similar platform. This is an Indian site.
  • Hacker Rank - Besides basic programming, they also have tracks for Data Structures, Algorithms, Databases, AI etc. Though I do not recommend those tracks for a beginner.
  • Project Euler - There are so many interesting problems here. Here, only answer of one scenario should be submitted instead of submitting code.
  • Virtual Judge - This is a Chinese platform. Contests could be arranged here taking problems from different online Judges.
  • Toph - A Bangladeshi platform for both competitive programming and problem solving.
  • SourceCode (Temporary Name) - Our students made this platform, though it is in a very early stage. The name is not yet finalized. We take programming exams through it. There are several problems for problem solving.

There are a lot of interesting things on the internet. We should encourage and entertain our new students with these tools, rather than compelling him to know complex things.

(My answer seemed not relevant to the question, but it is relevant as you could submit answers in C/C++.)

Answered by Enamul Hassan on August 21, 2021

That's really a nice question and I appreciate that you consider the noise around programing like version control (git) and coding style.

View things important to me are:

Flowcharts: or the art of coding on a piece of paper. When I was a student, our teacher insisted on designing the logic on paper before typing it into the machine. Maybe this sounds old school but for larger projects I still take the pencil first. This helps to understand and focus on logic.

Algorithms: or defining the problem and finding a solution to it. Mostly C programs are likely command line programs and no full blown apps. So the best use and challenge for C is to learn concepts of Algorithms and Computing It's always fun to me to draw concepts on a whiteboard, this helps to get some imagination and creativity for the topic.

Debugging: or how to get to know what your machine is doing right now. Programming is like manipulating bits and bytes in the Memory (tag: Von Neumann architecture). To visualize what's going helps to understand what the machine is doing and much more help to find and understand your mistakes (semantic failures). With the debugger you can walk through your code and see variable and memory change. Especially when it comes to different types, arrays, pointers, and memory location (stack, heap, registers, etc.) it is good to know how to look that up.

If you like to teach "how to google their problem" it would be an advice to teach how to ask the right question

Answered by chris.stckhmr on August 21, 2021

If you want to teach the students some valuable real life skills, helping them in writing programs without bugs, teach them about sequence points and integer promotion.

If a C programmer does not understand those concepts, he or she is bound to make some serious mistakes sooner or later.

#include <stdlib.h>
#include <stdio.h>

static int n = 100;

static int f1(void) {
    n++;
    return n;
}

static int f2(void) {
    n++;
    return n;
}

static int f3(int a, int b, int c, int d)
{
    printf("Undefined behaviour allows the compiler to do anything!n");
    printf("n = %d, a = %d, b = %d, c = %d, d = %dn", n, a, b, c, d);
    return n + a + b + c + d;
}

int main(int argc, char *argv[])
{
    unsigned int a = 10;
    signed int b = -10;

    if (a < b) {
        printf("This is executed despite that (%u < %d) is not truen", a, b);
    }

    n = f3(n, f1(), f2(), n++);

    return EXIT_SUCCESS;
}

Compiled with gcc:

This is executed despite that (10 < -10) is not true
Undefined behaviour allows the compiler to do anything!
n = 103, a = 103, b = 103, c = 102, d = 100

Compiled with clang:

This is executed despite that (10 < -10) is not true
Undefined behaviour allows the compiler to do anything!
n = 103, a = 100, b = 101, c = 102, d = 102

Even if the students do not fully grasp those two concepts, being aware of them is important.

Answered by hlovdal on August 21, 2021

Note:

I was under the impression that this question was by a faculty asking for advice on best practices for teaching a course on C. Instead it's a question by a proactive student that is disappointed with the department's approach and wants to help educate his peers on best practices.

I believe that my answer is wrongly targeted but I'm leaving it here as I feel that it might hold some value to others.

--

When I went to school for programming our department had a very interesting approach which, in my opinion, helped to mitigate a lot of the confusion surrounding a first introduction.

Provide a very rigid style guide

  • We had department-wide coding guidelines which were adhered to for all courses (much like when working for a company) so not only was code between students fairly consistent but also consistent between assignments for the same student. This is often overlooked and is in my opinion one of the most important things when teaching what literally amounts to be a foreign language.

Provide some form of static analysis for both style and logic

  • Every assignment was passed through a "style checker" written by the department that verified a combination of style conformity and basic linting for static error checking. This further reinforced adhering to a single style of coding and the linting was helpful and helped us to not be afraid of output warnings and errors.

Provide unit tests so that students can focus on the implementation rather than the formatting

  • Every assignment to be submitted was run through unit tests that verified input/output. This was tremendously helpful as it allowed us to spend less time on the math and more on the implementation and practical application of the code we were writing to solve a problem.

Focus on a standard and cross-platform tool-chain and leverage it to teach principals that are applicable to other environments; teach them the language not an IDE

  • Compilers and the aforementioned tools were available only from the department "Linux lab" computers. We were tasked with learning the command line and text editor of our choice (nano/emacs/vi) and compilation was done directly at the command line with g++. We were also taught how to ssh into the Linux lab so that we could work off-campus. This provided a simple environment without all of the extraneous features of IDEs and a basic introduction to both a Unix-like environment and command line compiling.

Put blinders on your students and choose to use a subset of the language to force students to think about solving problems with the language itself rather than through a library

  • All classes for teaching programming principals stripped everything to the bare minimum so that we were learning not only the core of the language (C++ in this case) but eschewed even the STL; for example, we programmed all of our data structures by hand. We learned the tools and the language which allowed us to later understand what a given library was doing under the hood rather than it being a black box.

Tell them where to go for resources

  • It might seem like second nature to us but new students don't know where to go for resources and may not think beyond their textbook. Websites that provide an overview of the language, its keywords and headers, such as http://en.cppreference.com/w/c/language that are easily browse-able on an internet device are excellent. Man pages are also useful but remember that this is a foreign concept so remember to demonstrate how to use it frequently during class. A small library of books as references can also provide additional insight on how to tackle a problem outside of a course textbook.

For those who are new to programming, this rigid conformity is essential to block out so much of the background noise and flip-flopping on coding style that is so prevalent for new students.

There was no question for us about where top put braces, semi-colons, white-space, and comments. There was no question about basic linting. There was no question about whether a program worked or not since it was run through unit tests.

I cannot stress enough how effective this approach was for me as a student. It gave us blinders that shielded us from the unimportant details and allowed us to focus on the most important things in each lesson and made learning to program a very satisfying and extremely rewarding experience.

Answered by Zhro on August 21, 2021

It's important to teach that the name C refers to two diverging languages--a low-level language which is useful for systems programming because many operations which different platforms may handle differently are handled in a documented fashion characteristic of the actual execution environment, and a more recent high-level-only language which uses the same syntax as the low-level language, but which allows compilers to behave in arbitrary fashion if code attempts certain operations whose behaviors would be defined in the former language.

Given a piece of code like:

unsigned mul_mod_65536(unsigned short x, unsigned short y)
{
  return (x*y) & 0xFFFF;
}

compilers for the former language would multiply the values of x and y using whatever semantics the platform uses for integer multiplication, take the bottom 16 bits of that result, and return them. On platforms where integer overflow would yield a result whose bottom 16 bits is correct, the above would yield the correct mod-65536 sum for all combinations of x and y. The published rationale for C89 indicates that the authors of the Standard would have expected such behavior from most current (and IMHO presumably future) compilers.

On compiler processing the latter language, however, the above code may sometimes malfunction in totally nonsensical ways if the value of x*y would exceed 2147483647. If, for example, code were to call mul_mod_65536(i,65535) some "modern" compilers would use the multiplication to infer that i cannot be greater than 32768, and thus "optimize out" code elsewhere in the program that would only be relevant if it were. Thus, unlike some languages which specify that an overflow will wrap cleanly (like Java), or specify that it will trap (like C#, within checked contexts), "modern C" requires that programmers absolutely positively prevent overflows from occurring under any circumstances even in cases where one of the above behaviors would suffice (or even in circumstances where either would be equally useful).

Programmers need to be aware that there exists a lot of code which is written for the former kind of C, and also that some compilers like gcc and clang will be incompatible with such code unless explicitly forbidden from applying aggressive "optimizations".

Answered by supercat on August 21, 2021

This might be controversial, but I would make a point to explain that goto is not always considered harmful (and explain that the context of Dijkstra's "Go To Statement Considered Harmful" was about using available control structures). In C, there aren't very good control structures for releasing resources; in the absence of them, goto works well, and people should not be afraid to use it for that purpose.

People new to C inevitably have trouble doing manual resource management and end up with memory (or other resource) leaks. Dealing with it is hard. People then think C is harder than it is. Trying to follow a single-entry, single-exit (SESE) pattern can reduce the cognitive burden and make code easier to maintain in the future.

For example:

char* foo(const char* directory)
{
    char* path = make_full_path(directory, CONSTANT_FILENAME);
    if (path == NULL)
    {
        return NULL;
    }

    FILE* fp = fopen(path, "r");
    if (fp == NULL)
    {
        free(path);
        return NULL;
    }

    char line[1024];
    fgets(line, sizeof line, fp);

    char* copy = malloc(strlen(line) + 1);
    if (copy == NULL)
    {
        fclose(fp);
        free(path);
        return NULL;
    }

    strcpy(copy, line);

    fclose(fp);
    free(path);

    return copy;
}

Simpler:

char* foo(const char* directory)
{
    char* path = NULL;
    FILE* fp = NULL;
    char* copy = NULL;

    path = make_full_path(directory, CONSTANT_FILENAME);
    if (path == NULL)
    {
        goto exit;
    }

    fp = fopen(path, "r");
    if (fp == NULL)
    {
        goto exit;
    }

    char line[1024];
    fgets(line, sizeof line, fp);

    copy = malloc(strlen(line) + 1);
    if (copy == NULL)
    {
        goto exit;
    }

    strcpy(copy, line);

exit:
    if (fp != NULL)
    {
        fclose(fp);
    }
    free(path);

    return copy;
}

And imagine that we need to introduce some new, temporary allocation to the code. In the first version, that would require considering where the new allocation occurs and inspecting the various exit points to make sure that each exit point cleans up the new allocation if necessary. In the second version, it requires only adding the variable to the beginning, initializing it to a sentinel value (e.g. NULL), and then unconditionally freeing it at the end.

Answered by jamesdlin on August 21, 2021

I think an important aspect to any best-practices list is the rationale behind it. It is entirely too common for a programmer to, for example, insist that gotos and global variables are evil and then proceed to use exceptions and singletons to create the exact same problems that got those features proscribed in the first place.

So I suggest that when introducing a rule of thumb, you don't just give examples of code that follows the rule, but rather examples of the sort of awful code that led to the rule being created. Let them understand why the rule exists both so they can avoid making similar mistakes and also recognize when the rule isn't applicable. (Of course, no examples will be as helpful as allowing them to write terrible code and then try to modify it, but you have limited time.)

A related recommendation that is somewhat tangential to the best practices lecture, but related to the question of how to help them complete the course successfully: Don't assume that the more abstract explanations of topics are necessarily easier to understand than the ones that get into the gritty details.

I've had a number of students who were completely confused when we tried to explain pointers using diagrams of boxes and arrows, but as soon as I sketched out a table of memory (with addresses as indices), explained that a pointer is just an integer that is used to "index memory", and then walked through a block of code, updating the table as I went, they understood it almost immediately. (We put the abstractions back in place when it was time to work with higher level data structures, after they understood the fundamentals.)

Answered by Ray on August 21, 2021

Two important concepts that seem absent from the list, and that I often find are barely developed in even fairly advanced students are testing and debugging. I would suggest some brief introduction along the following lines:

Unit test and integration test; test scaffolding; test-first strategy; exhaustive test vs. (targeted) random test vs. testing manually determined corner cases.

Use of assertions; logging, possibly at various debug levels; insertion of printf() calls for adhoc debugging; use of a debugger: break points, single-stepping, observing variables, watchpoints; code simplification to create an MCVE.

Answered by njuffa on August 21, 2021

if they are new to programming start with flip-flops. I usually refer to Lombardi's "This year, we are going to start from the beginning. This, gentleman, is a football". It makes it funnier when they are British.

You can delete this answer as I don't have comment permissions.

debian
eclipse-cdt
svn/git
cmake
jenkins
unit test

250,000 lines of code is more reasonable than 800. Also, if you need to add a comment to code it means that the code is unintelligible.

Edit:

As requested in the comment. I think starting with a brief introduction to electrical engineering and discreet mathematics is appropriate in an introductory programming course. The list above is not complete or accurate, but it's something that can be setup in a day and increases the productivity of other programming tasks that are part of your lesson.

Also, implementing strcpy is a good task for anyone from an arts background.

Answered by Abdul Ahad on August 21, 2021

Lint

I would not program in C without a lint tool e.g. gcc -Wall or pclint/flex-lint, unfortunately the latter two are proprietary.

However you need to show that error/warning messages are your friend. There are not an accident. Someone spent time writing them, to help you. Read them, and fix the underlying problem. Often I have seen people finding ways to get the error/warning to go away, but changing the code in a convoluted way, that makes it worse, but has no error.

Brackets {} are not an option.

I don't care what the standard says, always use brackets.

Comments

Well written code with good names, is better than code with bad names and comments. Use procedure/function names, and variable names to comment your code.

Bad

i++; /*Increment i*/
i++; /*Increment index*/

Better

index++;

Local consistency

Style must be locally consistent, and preferably globally consistent. Global consistency may suffer if there are more than one person on the team. But local consistency must never suffer.

Nouns, verbs, adjectives

Variable name should be nouns or for booleans adjectives. Procedures should be verbs. Functions should be as for variables.

Full command/query separation is not easy in C as you would have to program Object Oriented.

Answered by ctrl-alt-delor on August 21, 2021

Your list is a bit narrow in one sense. I assume it is well matched to your specific course, but probably doesn't represent "best practice" in general. For example, valgrind is limited to linux, which suits you better than me. But the idea of including memory testing, for example, is a good idea no matter the specific tool. Similarly for git. There are alternatives, but code management and version control is the big idea.

But one item I find missing here, but also essential is some sort of tool for unit testing. There are many available and building good clean code requires pre testing everything to arrive at a good result painlessly.

Another suggestion I'd make, though this may be your intent already, is that you show many of your ideas in the context of a large and complicated program. You state that dealing with such projects/programs is an issue in the course at the beginning of your post and that may actually be the biggest issue. So don't present your tool set using only "toy" programs.

If the projects are done in teams you may also want to include something specific about how to be successful in a team environment. Many of your students may not have experienced that. There is a book, in fact, named Teamwork is an Individual Skill that has valuable hints for any professional.

Answered by Buffy on August 21, 2021

Add your own answers!

Ask a Question

Get help from others!

© 2024 TransWikia.com. All rights reserved. Sites we Love: PCI Database, UKBizDB, Menu Kuliner, Sharing RPP