8 ms·
For "dark corners of C", when I was writing C code I had several serious concerns. Below I list eight such in roughly descending order on 'seriousness': First
by graycat 14y ago
For "dark corners of C", when I was writing C code I
had several serious concerns. Below I list eight
such in roughly descending order on 'seriousness':
First, what are malloc() and free() doing? That is,
what are the details, all the details and exactly
how they work?
It was easy enough to read K&R, see how malloc() and
free() were supposed to be used, and to use them,
but even if they worked perfectly I was unsure of
the correctness of my code, especially in
challenging situations, expected problems with
'memory management' very difficult to debug, and
wanted a lot of help on memory management. I would
have written my own 'help' for memory management if
I had known what C's memory management was actually
doing.
'Help' for memory management? Sure: Put in a lot
of checking and be able to get out a report on what
was allocated, when, by what part of the code, maybe
keep reference counters, etc. to provide some checks
to detect problems and some hints to help in
debugging.
That I didn't know the details was a bummer.
It was irritating that K&R, etc. kept saying that
malloc() allocated space in the 'heap' without
saying just what they meant by a 'heap' and which I
doubt was a 'heap' as in heap sort.
Second, the 'stack' and 'stack overflow' were always
looming as a threat of disaster, difficult to see
coming, and to be protected against only by mud
wrestling with obscure commands to the linkage
editor or whatever. So, I had no way to estimate
stack size when writing code or to track it during
execution.
Third, doing data conversions with a 'cast' commonly
sent me into outrage orbiting Jupiter.
Why? Data conversion is very important, but a
'cast' never meant anything. K&R just kept saying
'cast' as if they were saying something meaningful,
but they never were. In the end 'cast' was just
telling the type checking of the compiler that,
"Yes, I know, I'm asking for a type conversion, so
get me a special dispensation from the type checking
police.".
What was missing were the details, for each case, on
just how the conversion would be done. In strong
contrast, when I was working with PL/I, the
documentation went to great lengths to be clear on
the details of conversion for each case of
conversion. I knew when I was doing a conversion
and didn't need the 'discipline' of type checking in
the compiler to make me aware of where I was doing a
conversion.
Why did I want to know the details of how the
conversions were done? So that I could 'desk check'
my code and be more sure that some 'boundary case'
in the middle of the night two years in the future
wouldn't end up with a divide by zero, a square
root of a negative number, or some such.
So, too often I wrote some test code to be clear on
just what some of the conversions actually did.
Fourth, that the strings were terminated by the
character null usually sent me into outrage and
orbit around Pluto. Actually I saw that null
terminated strings were so hopeless as a good tool
that I made sure I never counted on the null
character being there (except maybe when reading the
command line). So, I ended up manipulating strings
without counting on the character null.
Why? Because commonly the data I was manipulating
as strings could contain any bytes at all, e.g., the
data could be from graphics, audio, some of the
contents of main memory, machine language
instructions, output of data logging, say, sonar
data recorded on a submarine at sea, etc. And, no
matter what the data was, no way did I want the
string manipulation software to get a tummy ache
just from finding a null.
Fifth, knowing so little about the details of memory
management, the stack, and exceptional condition
handling, I was very reluctant to consider trying to
make threading work.
Sixth, arrays were a constant frustration. The
worst part was that could write a subroutine to,
say, invert a 10 x 10 matrix but then couldn't use
it to invert a 20 x 20 matrix. Why? Because inside
the subroutine, the 'extents' of the dimensions of
the matrix had to be given as just integer constants
and, thus, could not be discovered by the subroutine
after it was called. So, basically in the
subroutine I had to do my own array indexing
arithmetic starting with data on the size of the
matrix passed via the argument list. Writing my own
code for the array indexing was likely significantly
slower during execution than in, say, Fortran or
PL/I, where the compiler writer knows when they are
doing array indexing and can take advantage of that
fact.
So, yes, no doubt as tens of thousands of other C
programmers, I wrote a collection of matrix
manipulation routines, and for each matrix used a C
struct to carry the data describing the matrix that
PL/I carried in what the IBM PL/I execution logic
manual called a 'dope vector'. The difference was,
both PL/I and C programmers pass dope vectors, but
the C programmers have to work out the dope vector
logic for themselves. With a well written compiler,
the approach of PL/I or Fortran should be faster.
It did occur to me that maybe other similar uses of
the C struct 'data type' were the inspiration for
Stroustrup's C++. For more, originally C++ was just
a preprocessor to C, and at that time and place,
Bell Labs, with Ratfor, preprocessors were popular.
Actually writing a compiler would have permitted a
nicer language.
Seventh, PL/I was in really good shape some years
before C was started and had subsets that were much
better than C and not much more difficult to
compile, etc. E.g., PL/I arrays and structures are
really nice, much better than C, and mostly are
surprisingly easy to implement and efficient at
execution. Indeed, PL/I structures are so nice that
they are in practice nearly as powerful as objects
and often easier and more intuitive to use. What
PL/I did with scope of names is also super nice to
have and would have helped C a lot.
Eight, the syntax of C, especially for pointers, was
'idiosyncratic' and obscure. The semantics in PL/I
were more powerful, but the syntax was much easier
to read and write. There is no good excuse for the
obscure parts of C syntax.
For a software 'platform' for my startup, I selected
Windows instead of some flavor of Unix. There I
wanted to build on the 'common language runtime'
(CLR) and the .NET Framework. So, for languages, I
could select from C#, Visual Basic .NET, F#, etc.
I selected Visual Basic .NET and generally have been
pleased with it. The syntax and memory management
are very nice; .NET is enormous; some of what is
there, e.g., for 'reflection', class instance
serialization, and some of what ASP.NET does with
Visual Basic .NET, is amazing. In places Visual
Basic borrows too much from C and would have done
better borrowing from PL/I.
- apaprocki 14y ago> Fourth, that the strings were terminated by the character null usually sent me into outrage and orbit around Pluto. Everything is about tradeoffs. Fortran uses space-padded strings with no null terminator. On the positive side, this forces everyone to explicitly pass the length they mean instead of relying on more work at runtime to figure out when to stop by looking for the null sentinel. Passing explicit lengths is good practice in C anyway because you usually avoid having to scan the contents multiple times / multiple calls to strlen at different levels in the stack. While everything should be better in the Fortran case, the class of bugs that persist are even more hard-to-find bugs because poorly written code mis-calculates the length, ignores it, etc., stomping over adjacent memory. This probably won't crash, and since other code has to use an explicit length when accessing the buffer, you usually won't notice the problem at the source of the issue. Contrast that with C, where you're more likely to see an issue immediately as soon as the string is used or passed to something else. tl;dr Poor programming is poor programming in any language.
- graycat 14y agoYup. With PL/I the maximum length of the string is set when the string is allocated, usually dynamically during execution. The length can be given as a constant in the source code or set from computations during execution. There is also a current length <= the maximum length. When passing that string to a subroutine, the subroutine has to work a little to discover the maximum string length, but, by in effect 'hiding' both the current and maximum length from the programmer of the subroutine, the frequency of some of the errors you mentioned should be reduced. In Visual Basic .NET, the maximum length of any string is the same, as I recall, 2 GB. Then having the strings be 'immutable' was a cute approach, slightly frustrating at times but otherwise quite nice and a good way to avoid the problems you mentioned. But, of course, the way I actually used strings in C was close to the way they were supported in Fortran. And, of course, likely 100,000+ C programmers wrote their own collection of string handling routines where use a struct to keep all the important data on the string, say, allocated or not, pointer to the allocated storage, maximum allocated length, current length, etc. (multi-byte character set anyone?) and then pass just a pointer to the struct instead of a pointer to the storage of the string; in this way, again should reduce the frequency of some of the errors you mentioned.