Source: https://apprize.info/c/principles/27.html
Timestamp: 2019-04-20 20:36:04+00:00

Document:
This chapter is a brief overview of the C programming language and its standard library from the point of view of someone who knows C++. It lists the C++ features missing from C and gives examples of how a C programmer can cope without those. C/C++ incompatibilities are presented, and C/C++ interoperability is discussed. Examples of I/O, list manipulation, memory management, and string manipulation are included as illustration.
The C programming language was designed and implemented by Dennis Ritchie at Bell Labs and popularized by the book The C Programming Language by Brian Kernighan and Dennis Ritchie (colloquially known as “K&R”), which is arguably still the best introduction to C and one of the great books on programming (§22.2.5). The text of the original definition of C++ was an edit of the text of the 1980 definition of C, supplied by Dennis Ritchie. After this initial branch, both languages evolved further. Like C++, C is now defined by an ISO standard.
• Describe where C isn’t a subset of C++.
• Describe which C++ features are missing in C and which facilities and techniques can be used to compensate.
The version of C that is used today is still mostly C89 (as described in the second edition of K&R), and that’s what we are describing here. There is still some Classic C in use and some C99, but that should not cause you any problems when you know C++ and C89.
There are no performance differences between equivalent C and C++ programs.
Like C++, C is very widely used. Taken together, the C/C++ community is the largest software development community on earth.
It is not uncommon to hear references to “C/C++.” However, there is no such language, and the use of “C/C++” is typically a sign of ignorance. We use “C/C++” only in the context of C/C++ compatibility issues and when talking about the large shared C/C++ technical community.
Most incompatibilities relate to C++’s stricter type checking.
The type of a character literal, such as 'a', is int in C and char in C++. However, for a char variable ch we have sizeof(ch)==1 in both languages.
Information related to compatibility and language differences is not exactly exciting. There are no new neat programming techniques to learn. You might like printf() (§27.6), but with that possible exception (and some feeble attempts at geek humor), this chapter is bone dry. Its purpose is simple: to allow you to read and write C if you need to. This includes pointing out the hazards that are obvious to experienced C programmers, but typically unexpected by C++ programmers. We hope you can learn to avoid those hazards with minimal grief.
Most C++ programmers will have to deal with C code at some point or another, just as most C programmers will have to deal with C++ code. Much of what we describe in this chapter will be familiar to most C programmers, but some will be considered “expert level.” The reason for that is simple: not everyone agrees about what is “expert level” and we just describe what is common in real-world code. Maybe understanding compatibility issues can be a cheap way of gaining an unfair reputation as a “C expert.” But do remember: real expertise is in the use of a language (in this case C), rather than in understanding esoteric language rules (as are exposed by considering compatibility issues).
ISO/IEC 9899:1999. Programming Languages — C. This defines C99; most implementations implement C89 (often with a few extensions).
ISO/IEC 9899:2011. Programming Languages — C. This defines C11.
ISO/IEC 14882:2011. Programming Languages — C++.
Kernighan, Brian W., and Dennis M. Ritchie. The C Programming Language. Prentice Hall, 1988. ISBN 0131103628.
Stroustrup, Bjarne. “Learning Standard C++ as a New Language.” C/C++ Users Journal, May 1999.
Stroustrup, Bjarne. “C and C++: Siblings”; “C and C++: A Case for Compatibility”; and “C and C++: Case Studies in Compatibility.” The C/C++ Users Journal, July, Aug., and Sept. 2002.
The papers by Stroustrup are most easily found on my publications home page.
• Use struct and global functions.
• Use structs, global functions, and pointers to functions (§27.2.3).
• Use error codes, error return values, etc.
• Give each function a distinct name.
• Use malloc()/free() and separate initialization/cleanup code.
• Use C-style casts, e.g., (int)a rather than static<int>(a).
Lots of useful code is written in C, so this list should remind us that no one language feature is absolutely necessary. Most language features — even most C language features — are there for the convenience (only) of the programmer. After all, given sufficient time, cleverness, and patience, every program can be written in assembler. Note that because C and C++ share a machine model that is very close to the real machine, they are well suited to emulate varieties of programming styles.
• Emulate the programming techniques that the C++ features were designed to support with the facilities provided by C.
• When writing C, write in the C subset of C++.
• Use compiler warning levels that ensure function argument checking.
• Use lint for large programs (see §27.2.2).
• The compiler will remind you when you are using a C++ feature that is not in C.
• If you follow the rules above, you are unlikely to encounter anything that means something different in C from what it means in C++.
We give examples of a few such uses in this chapter.
I introduced the // comments into C++ from C’s ancestor BCPL when I got really fed up with typing /* . . . */ comments. The // comments are accepted by most C dialects including C99 and C11, so it is probably safe just to use them. Here, we will use /* . . . */ exclusively in examples meant to be C. C99 and C11 introduced a few more C++ features (as well as a few features that are incompatible with C++), but here we will stick to C89, because that’s far more widely used.
For a complete description, see a good C textbook, such as K&R. All of these libraries (and header files) are also available in C++.
• There can be only one function of a given name.
• Function argument type checking is optional.
• There are no references (and therefore no pass-by-reference).
• There are no member functions.
• There are no inline functions (except in C99).
• There is an alternative function definition syntax.
Apart from that, things are much as you are used to in C++. Let us explore what that means.
This is occasionally claimed to be a virtue: now you can’t accidentally use the wrong function to print an int! Clearly we don’t buy that argument, and the lack of overloaded functions does make generic programming ideas awkward to implement because generic programming depends on semantically similar functions having the same name.
A C compiler will accept this: you don’t have to declare a function before you call it (though you can and should). There may be a definition of f() somewhere. That f() could be in another translation unit, but if it isn’t, the linker will complain.
The linker will not report that error. You will get a run-time error or some random result.
How do we manage problems like that? Consistent use of header files is a practical answer. If every function you call or define is declared in a header that is consistently #included whenever needed, we get checking. However, in large programs that can be hard to achieve. Consequently, most C compilers have options that give warnings for calls of undeclared functions: use them. Also, from the earliest days of C, there have been programs that can be used to check for all kinds of consistency problems. They are usually called lint. Use a lint for every nontrivial C program. You will find that lint pushes you toward a style of C usage that is rather similar to using a subset of C++. One of the observations that led to the design of C++ was that the compiler could easily check much (but not all) of what lint checked.
The declaration of h() specifies no argument type. This does not mean that h() doesn’t accept arguments; it means “Accept any set of arguments and hope they are correct for the called function.” Again, a good compiler warns and lint will catch the problem.
There is a special set of rules for converting arguments where no function prototype is in scope. For example, chars and shorts are converted to ints, and floats are converted to doubles. If you need to know, say, what happens to a long, look it up in a good C textbook. Our recommendation is simple: don’t call functions without prototypes.
I soon regretted that, though, since that looks odd and is completely redundant when argument type checking is uniformly applied. Worse, Dennis Ritchie (the father of C) and Doug McIlroy (the ultimate arbiter of taste in the Bell Labs Computer Science Research Center; see §22.2.5) both called it “an abomination.” Unfortunately, that abomination became very popular in the C community. Don’t use it in C++, though, where it is not only ugly, but also logically redundant.
The compiler should accept these calls (but would warn, we hope, for the first and third).
• Use function prototypes consistently (use header files).
• Set compiler warning levels so that argument type errors are caught.
The result will be code that’s also C++.
You can link files compiled with a C compiler together with files compiled with a C++ compiler provided the two compilers were designed for that. For example, you can link object files generated from C and C++ using your GNU C and C++ compiler (GCC) together. You can also link object files generated from C and C++ using your Microsoft C and C++ compiler (MSC++) together. This is common and useful because it allows you to use a larger set of libraries than would be available in just one of those two languages.
Basically extern "C" tells the compiler to use C linker conventions. Apart from that, all is normal from a C++ point of view. In fact, the C++ standard sqrt(double) usually is the C standard library sqrt(double). Nothing is required from the C program to make a function callable from C++ in this way. C++ simply adapts to the C linkage convention.
No mention of C++ is needed (or possible) in C for this to work.
The benefit of this interoperability is obvious: code can be written in a mix of C and C++. In particular, a C++ program can use libraries written in C, and C programs can use libraries written in C++. Furthermore, most languages (notably Fortran) have an interface for calling to/from C.
The rules for layout in any language can be complex, and the rules for layout among languages can even be hard to specify. However, you can pass built-in types between C and C++ and also classes (structs) without virtual functions. If a class has virtual functions, you should just pass pointers to its objects and leave the actual manipulation to C++ code. The call_f() was an example of this: f() might be virtual and then that example would illustrate how to call a virtual function from C.
Apart from sticking to the built-in types, the simplest and safest sharing of types is a struct defined in a common C/C++ header file. However, that strategy seriously limits how C++ can be used, so we don’t restrict ourselves to it.
• For each “pseudo-virtual” function (such as draw()), we have to write a new switch-statement.
• Each time we add a new shape, we have to modify every “pseudo-virtual” function (such as draw()) by adding a case to the switch-statement.
This Shape2 can be used just like Shape1.
With a little extra work, an object need not hold one pointer to a function for each pseudo-virtual function. Instead, it can hold a pointer to an array of pointers to functions (much as virtual functions are implemented in C++). The main problem with using such schemes in real-world programs is to get the initialization of all those pointers to functions right.
This section gives examples of minor C/C++ differences that could trip you up if you have never heard of them. Few seriously impact programming in that the differences have obvious work-arounds.
Amazingly enough, thanks to a devious compatibility hack, this also works in C++. Having a variable (or a function) with the same name as a struct is a fairly common C idiom, though not one we recommend.
In general, you’ll find typedefs more common and more useful in C, where you don’t have the option of defining new types with associated operations.
Whenever possible, don’t nest structs in C: their scope rules differ from what most people naively (and reasonably) expect.
Don’t use these names as identifiers in C, or your code will not be portable to C++. If you use one of these names in a header file, that header won’t be useful from C++.
In C, they are defined in <iso646.h> and <stdbool.h> (bool, true, false). Don’t take advantage of the fact that they are macros in C.
Better still, avoid the global variable.
This does not give the type checking done by reinterpret_cast and const_cast, but it does make these inherently ugly operations visible and the programmer’s intent explicit.
We used the C-style cast (§27.3.4) so that it would be legal in both C and C++.
Here we can’t even be sure what memory is overwritten. Maybe j and part of p? Maybe some memory used to manage the call of f() (f’s stack frame)? Whatever data is being overwritten here, a call of f() is bad news.
Note that (the opposite) conversion of a T* to a void* is perfectly safe — you can’t construct nasty examples like the one above for that — and those are allowed in both C and C++.
Unfortunately, implicit void*-to-T* conversions are common in C and possibly the major C/C++ compatibility problem in real code (see §27.4).
“Falling off the end” of the enumerators may or may not have been what we wanted.
This technique is so popular that it is usually a bad idea to use one- or two-letter prefixes.
The typedef size_t is an unsigned type also defined in <stdlib.h>.
This might work, but it is not portable code. Furthermore, for objects with constructors or destructors, mixing C-style and C++-style free-store management is a recipe for disaster.
For an explanation of the C input operations, see §27.6.2 and §B.11.2.
The realloc() function may or may not move the old allocation into newly allocated memory. Don’t even think of using realloc() on memory allocated by new.
Refer to the paper “Learning Standard C++ as a New Language” (see the reference list in §27.1) for a more thorough discussion of input and allocation strategies.
This is not the full set, but these are the most useful and most used functions. We will briefly illustrate their use.
The value of the pointer comparison s1==s2 is not guaranteed to be 0 (false). An implementation may decide to use the same memory to hold all copies of a character literal, so we would get the answer 1 (true). Usually, strcmp() is the right choice for comparing C-style strings.
Note that strlen() counts characters excluding the terminating 0. In this case, strlen(s1)==4 and it takes 5 bytes to store "asdf". This little difference is the source of many off-by-one errors.
It is your job to be sure that the target string (array) has enough space to hold the characters from the source.
The strncpy(), strncat(), and strncmp() functions are versions of strcpy(), strcat(), and strcmp() that will consider a maximum of n characters, where n is their third argument. Note that if there are more than n characters in the source string, strncpy() will not copy a terminating 0, so that the result will not be a valid C-style string.
The strchr() and strstr() functions find their second argument in the string that is their first argument and return a pointer to the first character of the match. Like find(), they search from left to right in the string.
Did we get that right? Who will free() the string returned from cat()?
Test cat(). Why 2? We left a beginner’s performance error in cat(); find it and remove it. We “forgot” to comment our code. Add comments suitable for someone who can be assumed to know the standard C-string functions.
This recommendation applies to both C and C++.
Again, this is illegal in C and C++, but C compilers can’t catch it. Sometimes this is referred to as transmutation: it turns consts into non-consts, violating reasonable assumptions about code.
Don’t use these functions in C++. In particular, memset() typically interferes with the guarantees offered by constructors.
We leave to you the explanation of why this actually copies the C-style string q into p. Post-increment is described in §A.5: The value of p++ is the value of p before increment.
Is this implementation of strcpy() correct? Explain why.
If you can’t explain why, we won’t consider you a C programmer (however competent you are at programming in other languages). Every language has its own idioms, and this is one of C’s.
There are no iostreams in C, so we use the C standard I/O defined in <stdio.h> and commonly referred to as stdio. The stdio equivalents to cin and cout are stdin and stdout. Stdio and iostream use can be mixed in a single program (for the same I/O streams), but we don’t recommend that. If you feel the need to mix, read up on stdio and iostreams (especially ios_base::sync_with_stdio()) in an expert-level textbook. See also §B.11.
Here, %g means “Print a floating-point number using the general format,” %s means “Print a C-style string,” %d means “Print an integer using decimal digits,” and %c means “Print a character.” Each such format specifier picks the next so-far-unused argument, so %g prints d, %s prints s,%d prints i, and %c prints ch. You can find the full list of printf() formats in §B.11.2.
The effect of the last printf() is interesting: it prints every byte in memory following a until it encounters a 0. That could be a lot of characters.
This lack of type safety is one reason we prefer iostreams over stdio even though stdio works identically in C and C++. The other reason is that the stdio functions are not extensible: you cannot extend printf() to print values of your own types, the way you can using iostreams. For example, there is no way you can define your own %Y to print some struct Y.
File handles are described in §27.6.3.
Never do that! Consider gets() poisoned. Together with its close cousin scanf("%s"), gets() used to be the root cause of about a quarter of all successful hacking attempts. It is still a major security problem. In the trivial example above, how would you know that at most 11 characters would be input before a newline? You can’t know that. Thus, gets() almost certainly leads to memory corruption (of the bytes after the buffer), and memory corruption is a major tool of crackers. Don’t think that you can guess a maximum buffer size that is “large enough for all uses.” Maybe the “person” at the other end of the input stream is a program that does not meet your criteria for reasonableness.
Like printf(), scanf() is not type safe. The format characters and the arguments (all pointers) must match exactly, or strange things will happen at run time. Note also that the %s read into s may lead to an overflow. Don’t ever use gets() or scanf("%s")!
We need space for a terminating 0 (supplied by scanf()), so 19 is the maximum number of characters we can read into buf. However, that leaves us with the problem of what to do if someone does type more than 19 characters. The “extra” characters will be left in the input stream to be “found” by later input operations.
EOF is a stdio macro meaning “end of file”; see also §27.4.
Consider this: there are no exceptions in C, so how do we make sure that the files are closed whichever error happens?
The name of the macro MAX is replaced by the characters 30, which is the value of the macro; that is, the number of elements of a1 is 30 and the value in the second case label is 30. We use all capital letters for the MAX macro, as is conventional. This naming convention helps minimize errors caused by macros.
Beware of macros: in C there are no really effective ways of avoiding macros, but their use has serious side effects because they don’t obey the usual C (or C++) scope and type rules. Macros are a form of text substitution. See also §A.17.2.
How do we try to protect ourselves from the potential problems of macros apart from (relying on C++ alternatives and) minimizing their use?
• Give all macros we define ALL_CAPS names.
• Don’t give anything that isn’t a macro an ALL_CAPS name.
• Never give a macro a short or “cute” name, such as max or min.
• Hope that everybody else follows this simple and common convention.
In addition, there is a wide variety of less common uses.
We consider macros seriously overused, but there are no reasonable and complete alternatives to the use of macros in C programs. It can even be hard to avoid them in C++ programs (especially if you need to write programs that have to be portable to very old compilers or to platforms with unusual constraints).
Apologies to people who consider the techniques described below “dirty tricks” and believe such are best not mentioned in polite company. However, we believe that programming is to be done in the real world and that these (very mild) examples of uses and misuses of macros can save hours of grief for the novice programmer. Ignorance about macros is not bliss.
That is, cc could easily have gotten a different value from what you would reasonably expect looking at the definition of cc. When you define a macro, remember to put every use of an argument as an expression in parentheses.
On the other hand, not all the parentheses in the world could save the second expansion. The macro parameter x was given the value aa++, and since x is used twice in MAX, a can get incremented twice. Don’t pass an argument with a side effect to a macro.
Fortunately, that macro was not all that important. However, there are tens of thousands of macros in popular header files; you can’t undefine them all without causing havoc.
The lines ending with \ are not a typesetting problem; it is the way you break a macro definition across lines. When writing C++, we prefer to use new.
• Many people don’t share their idea of what is a better syntax.
• The “improved” syntax is nonstandard and surprising; others get confused.
• There are uses of the “improved” syntax that cause obscure compile-time errors.
• What you see is not what the compiler sees, and the compiler reports errors in the vocabulary it knows (and sees in source code), not in yours.
Don’t write syntactic macros to “improve” the look of code. You and your best friends might find it really nice, but experience shows that you’ll be a tiny minority in the larger community, so that someone will have to rewrite your code (assuming it survives).
The #ifdef WINDOWS test doesn’t care what WINDOWS is defined to be; it just tests that it is defined.
The #ifndef test checks that something is not defined; i.e., #ifndef is the opposite of #ifdef. Logically, these macros used for source file control are very different from the macros we use for modifying source code. They just happen to use the same underlying mechanisms to do their job.
The C++ standard library containers, such as vector and map, are non-intrusive; that is, they require no data in the types used as elements. That is how they generalize nicely to essentially all types (built-in or user-defined) as long as those types can be copied. There is another kind of container, an intrusive container, that is popular in both C and C++. We will use a non-intrusive list to illustrate C-style use of structs, pointers, and free store.
It is not our aim to demonstrate clever representation techniques or clever algorithms, so there are none of those here. However, do note that there is no mention of any data held by the Links (the elements of a List). Looking back at the functions provided, we note that we are doing something very similar to defining a pair of abstract classes Link and List. The data for Links will be supplied later. Link* and List* are sometimes called handles to opaque types; that is, giving Link*s and List*s to our functions allows us to manipulate elements of a List without knowing anything about the internal structure of a Link or a List.
C doesn’t have namespaces, so we need not worry about using declarations or using directives. On the other hand, we should probably worry that we have grabbed some very common short names (Link, insert, init, etc.), so this set of functions cannot be used “as is” outside a toy program.
We decided not to deal with error handling for bad pointers to lists at run time. By using assert(), we simply give a (run-time) system error if a list pointer is null. The “system error” will give the file name and line number of the failed assert(); assert() is a macro defined in <assert.h> and the checking is enabled only during debugging. In the absence of exceptions, it is not easy to know what to do with bad pointers.
Note the way we traverse using the suc member of Link. We can’t safely access a member of a struct object after that object has been free()d, so we introduce the variable next to hold our position in the List while we free() a Link.
If we didn’t allocate all of our Links on the free store, we had better not call clear(), or clear() will create havoc.
Note that we are making no provisions for calling a cleanup function (destructor) for the elements represented by Links. This design is not a completely faithful imitation of C++ techniques or generality — it couldn’t and probably shouldn’t be.
However, we would never have gotten it right without drawing a few boxes and arrows on our doodle pad. Note that we “forgot” to consider the case where the argument p was null. Pass 0 instead of a pointer to a Link and this code will fail miserably. This is not inherently bad code, but it isnot industrial strength. Its purpose is to illustrate common and useful techniques (and, in this case, also a common weakness/bug).
So we “cheated.” We used a cast to treat a Name* as a Link*. In that way, the user knows about the “library-type” Link. However, the “library” doesn’t know about the “application-type” Name. Is that allowed? Yes, it is: in C (and C++), you can treat a pointer to a struct as a pointer to its first element and vice versa.
Obviously, this List example is also C++ exactly as written.
A common refrain among C++ programmers talking with C programmers is, “Everything you can do, I can do better!” So, rewrite the intrusive List example in C++, showing how to make it shorter and easier to use without making the code slower or the objects bigger.
1. Write a “Hello, World!” program in C, compile it, and run it.
2. Define two variables holding “Hello” and “World!” respectively; concatenate them with a space in between; and output them as Hello World!.
3. Define a C function that takes a char* parameter p and an int parameter x and print out their values in this format: p is "foo" and x is 7. Call it with a few argument pairs.
In the following, assume that by C we mean ISO standard C89.
1. Is C++ a subset of C?
3. Name a highly regarded C textbook.
4. In what organization were C and C++ invented?
5. Why is C++ (almost) compatible with C?
6. Why is C++ only almost compatible with C?
7. List a dozen C++ features not present in C.
8. What organization “owns” C and C++?
9. List six C++ standard library components that cannot be used in C.
10. Which C standard library components can be used in C++?
11. How do you achieve function argument type checking in C?
12. What C++ features related to functions are missing in C? List at least three. Give examples.
13. How do you call a C function from C++?
14. How do you call a C++ function from C?
15. Which types are layout compatible between C and C++? (Just) give examples.
16. What is a structure tag?
17. List 20 C++ keywords that are not keywords in C.
18. Is int x; a definition in C++? In C?
19. What is a C-style cast and why is it dangerous?
20. What is void* and how does it differ in C and C++?
21. How do enumerations differ in C and C++?
22. What do you do in C to avoid linkage problems from popular names?
23. What are the three most common C functions from free-store use?
24. What is the definition of a C-style string?
25. How do == and strcmp() differ for C-style strings?
26. How do you copy C-style strings?
27. How do you find the length of a C-style string?
28. How would you copy a large array of ints?
29. What’s nice about printf()? What are its problems/limitations?
30. Why should you never use gets()? What can you use instead?
31. How do you open a file for reading in C?
32. What is the difference between const in C and const in C++?
33. Why don’t we like macros?
34. What are common uses of macros?
35. What is an include guard?
For these exercises it may be a good idea to compile all programs with both a C and a C++ compiler. If you use only a C++ compiler, you may accidentally use non-C features. If you use only a C compiler, type errors may remain undetected.
1. Implement versions of strlen(), strcmp(), and strcpy().
2. Complete the intrusive List example in §27.9 and test it using every function.
3. “Pretty up” the intrusive List example in §27.9 as best you can to make it convenient to use. Do catch/handle as many errors as you can. It is fair game to change the details of the struct definitions, to use macros, whatever.
4. If you didn’t already, write a C++ version of the intrusive List example in §27.9 and test it using every function.
5. Compare the results of exercises 3 and 4.
6. Change the representation of Link and List from §27.9 without changing the user interface provided by the functions. Allocate Links in an array of links and have the members first, last, pre, and suc be ints (indices into the array).
7. What are the advantages and disadvantages of intrusive containers compared to C++ standard (non-intrusive) containers? Make lists of pros and cons.
8. What is the lexicographical order on your machine? Write out every character on your keyboard together with its integer value; then, write the characters out in the order determined by their integer value.
9. Using only C facilities, including the C standard library, read a sequence of words from stdin and write them to stdout in lexicographical order. Hint: The C sort function is called qsort(); look it up somewhere. Alternatively, insert the words into an ordered list as you read them. There is no C standard library list.
10. Make a list of C language features adopted from C++ or C with Classes (§27.1).
11. Make a list of C language features not adopted by C++.
12. Implement a (C-style string, int) lookup table with operations such as find(struct table*, const char*), insert(struct table*, const char*, int), and remove(struct table*, const char*). The representation of the table could be an array of a struct pair or a pair of arrays (const char* andint*); you choose. Also choose return types for your functions. Document your design decisions.
13. Write a program that does the equivalent of string s; cin>>s; in C; that is, define an input operation that reads an arbitrarily long sequence of whitespace-terminated characters into a zero-terminated array of chars.
14. Write a function that takes an array of ints as its input and finds the smallest and the largest elements. It should also compute the median and mean. Use a struct holding the results as the return value.
15. Simulate single inheritance in C. Let each “base class” contain a pointer to an array of pointers to functions (to simulate virtual functions as freestanding functions taking a pointer to a “base class” object as their first argument); see §27.2.3. Implement “derivation” by making the “base class” the type of the first member of the derived class. For each class, initialize the array of “virtual functions” appropriately. To test the ideas, implement a version of “the old Shape example” with the base and derived draw() just printing out the name of their class. Use only language features and library facilities available in standard C.
16. Use macros to obscure (simplify the notation for) the implementation in the previous exercise.
We did mention that compatibility issues are not all that exciting. However, there is a lot of C code “out there” (billions of lines of code), and if you have to read or write it, this chapter prepares you to do so. Personally, we prefer C++, and the information in this chapter gives part of the reason for that. And please don’t underestimate that “intrusive List” example — both “intrusive Lists” and opaque types are important and powerful techniques (in both C and C++).

References: §27
 §22
 §27
 §27
 §27
 §27
 §27
 §27
 §27
 §27
 §27
 §27