Power vs. Adventure - PL/I and C |
---|
This paper was presented at the G.U.I.D.E.&SHARE Europe Joint Conference (10-13 October 1994, Vienna, Austria).
Eberhard Sturm Universitätsrechenzentrum Einsteinstr. 60 D-48149 Münster Germany
Text marked up in cooperation with:
Dave Jones Velocity Software
Please use at least WebExplorer Version 1.03 or NetScape Version 1.1 as WWW browser.
The "old and honest" programming language PL/I seems to grow new blossoms; in OS/2 -- and soon AIX -- PL/I offers capabilities comparable to C. This presentation shows the differences in philosophy of both languages: PL/I allows problem-oriented programming without knowledge of low-level constructs, whereas C requires, for example, pointer programming even when reading a file. You should consider using PL/I instead of C if you prefer a programming style where the compiler does the tedious work for you.The author is member of the scientific staff for application programming at the Computing Center of the University of Münster, Germany.
This presentation is biased -- neither accidentally nor intentionally but necessarily. The reason is that programmers have their favorite programming language. Mine is PL/I but I studied C because there is a widespread operating system called Unix which I had to write programs for. Additionally I had many discussions with colleagues in my computing center comparing the two languages. Of course, I will always refer to OS/2 PL/I and ANSI-C because they are the modern versions!
First I want to present 5 theses: Too many things in C...
Let's start with the outward appearance of a C and a PL/I program:
C | PL/I |
---|---|
/* compilation unit: file */ #include |
/* compilation unit: package */ Program: package exports (Main); declare Something float external, Anything float static; Main: procedure options (main); ... call Subroutine (...); ... end; Subroutine: procedure (...); ... put (Function(...)); ... end; Function: procedure (...) returns(float); ... return (...); end; end; |
As you can see, at least comments are the same in both languages. The first difference is the treatment of letters. C is case-sensitive, PL/I doesn't care. For example, don't write the word main in C in capital letters; you will get an error message by the linker! The main procedure in C always has to have the name main! PL/I handles the specification of a main procedure in the usual way: it introduces keywords: options (main). Here we come to the next difference: Keywords in C are reserved words, in PL/I it's clear from the syntax what is a keyword and what is not. In PL/I you can truthfully say "What you don't know can't hurt you!"
Both in C and in PL/I the unit of compilation is a file. But in PL/I there must be at least one complete procedure contained in the file; in the general case a file will contain a package as shown here. The advantage compared to C is that the compiler is able to recognize a corrupted file, perhaps caused by a file transfer.
The scope of functions and variables defined outside functions in C is global by default! The PL/I rule -- names are local to the package or procedure -- isn't as dangerous: You have to mention procedures in the export-option of the package statement and to declare variables external if you want them to be used by other routines. The method to reduce the scope in C is to define it as static. This treatment is a candidate for thesis 1! Static storage is storage that lives during the lifetime of a program. And that is true whether you specify static or not. static in this place means "local to the file"! Even the scope of a function can be reduced to the file by declaring it "static".
If you wonder why there is a #include-line in the C program: input and output is done by library functions, and you have to include the function declarations in order to be able to do I/O.
The two lines following in the C example have no counterpart in PL/I. They declare the functions coming afterwards in the file, for the purpose of correct parameter passing. This is neither necessary nor possible in PL/I because the compiler "knows" all procedures in the package, only procedures in other files have to be declared in an analogous manner. C differentiates between declaration and definition: the first is an association of attributes and the second an association of storage. In PL/I there are only declarations, a PL/I programmer doesn't even understand the problem! The extern-declaration doesn't reserve storage. Why it's called extern is mysterious: The definition may come in another file or in the same file later on!
Declarations and definitions are order-dependent in C and block-dependent in PL/I. This means that in C you cannot use something you have not at least declared previously in the file. In PL/I declare-statements may be in any order, relevant is only the block they are contained in. This makes life easier for PL/I programmers!
Obviously there are two kinds of subprograms in PL/I and only one in C. The subprogram subroutine is called by a call statement in PL/I and only by referencing the name in C. C programmers praise this fact as a clean solution. But referencing thesis 4 I would reply that a subprogram should only serve one of two purposes: either computing a value or providing an effect! This can be checked by a compiler and is, of course, checked by the OS/2-PL/I compiler: there must be either a returns-attribute and a return-statement with value or a return-statement without a value (or return by an end-statement). The methods of parameter passing will be explained later.
At first glance declarations (or definitions) look similar in C and PL/I:
C | PL/I |
---|---|
const volatile float f = 1; |
dcl F float init(1) nonasgn abnormal; dcl F float nonasgn abnormal init(1); |
But there could be no greater error! First you have to obey a
certain order of keywords in C whereas PL/I only demands that the first
keyword specifies the statement type. The attributes following the name
of the variable may be in any order. Of course the initial value 1 has
to follow immediately the keyword init
! The keyword
const
in C means in fact nonassignable
as it it
called in PL/I!
Recall Thesis 1! The following statements declare fixed-point datatypes:
C | PL/I |
---|---|
char I; signed char J; unsigned char K; short int L; int M; long int N; /* no counterpart */ /* no counterpart */ |
dcl I char; /* ? */ dcl J fixed binary (7); dcl K fixed bin (8) unsigned; dcl L fixed bin (15); dcl M fixed bin; dcl N fixed bin (31); dcl O fixed bin (31,16); dcl S fixed dec (7,2); |
If you specify only char
, it depends on which C compiler
you're using whether it's signed or unsigned. What adventurous
consequences this has will be discussed in the operations part of this
presentation. If you have never heard of signed characters you needn't
worry! It's up to thesis 1: char
means in fact small
integer, as you see when you regard their PL/I counterparts! You feel
the power of PL/I when you look at the various types on the right:
Binary fractionals (you can find a long-winded C-style definition of
them in OS/2 Presentation Manager manuals) and decimal numbers even
with fractional parts. In PL/I you can declare in a problem-oriented
fashion what precision you want, as it is for floating-point numbers:
C | PL/I |
---|---|
float A; double B; long double C; /* no counterpart */ |
dcl A float; dcl A float decimal (6); dcl A float binary (21); dcl B float dec (16); dcl B float bin (53); dcl C float dec (33); dcl C float bin(109); dcl D float dec (12); |
Additionally there are non-computational datatypes in PL/I:
area, entry, file, format, handle, label, offset, pointer,
and task
! Because of the little space in this presentation I
will only comment on two PL/I counterparts to the C datatype pointer.
First a typical usage in both languages:
C | PL/I |
---|---|
char x, *p = &x; *p = 'Z'; |
dcl X char, P ptr init(addr(X)), C char based (P); C = 'Z'; |
The C definition means that x
and *p
each have
one byte of storage.
*
in this context is the indirection operator: *p
is the storage where p
points to. In other words:
p
is a pointer to char
. The syntax is slightly
chaotic; on the one hand *p
is of type char
, on
the other hand p
is initialized to the address of
x
(by using the address operator &
).
It's unclear whether to group char *p
or p = &x
!
The assignment means in fact that x
gets the value
'Z'
.
In the PL/I example the pointer p
is "untyped". This is a
point where using PL/I can get adventurous, but the advantage is that
pointers in PL/I often only appear in declare statements as it is in
this case.
You have to define a character object C
which is located
where the pointer P
points to.
In the assignment no pointer is used, only the name of a based object.
The other example shows the usage of variable subprograms in the two languages:
C | PL/I |
---|---|
float (*f) (float); f = func; printf ("%f", f(7.3)); |
dcl F entry (float) variable returns (float); F = Func; put (F(7.3)); |
The C philosophy is that f
is a pointer to a function
with parameter float
which returns a float
value.
In PL/I there is no pointer needed, only the program control data type
entry
. The keyword variable
is necessary because
names of procedures are entry constants. You shouldn't confuse the C
definition with the following: float *f (float);
which means
that f
is a function which returns a pointer to
float
! By the way, what do you think is the result of
quotient
in the following example:
C |
float *pdenominator; ... quotient = nominator/*pdenominator /* ? */; |
---|
You think that nominator
is divided by the value
pdenominator
points to? No, that's wrong (surprise,
surprise)! The characters "/*" after nominator
start a
comment! And thinking of thesis 5: does your compiler complain?
In order to introduce character strings we have first to present
arrays. Although there is a rudimentary concept of arrays in C, no
"good" C programmer uses the rudimentary array notation directly
(with the exception of character arrays). Instead they use pointers
which are closely related to arrays in C. For example you want to copy
array A
to array B
:
C | PL/I |
---|---|
int i; float a [10], b [10]; for (i = 1; i < 10; i += 1) a[i] = b[i]; |
dcl I fixed bin; dcl (A, B init ((5)0,(5)1)) dim (0:9) float; do I = lbound(A) to hbound(A); A(I) = B(I); end; |
The C philosophy is that array bounds start with 0 and extend to N-1, if N is the declared number of elements. In PL/I you can specify that you mean the pumpkin harvest of the years 1989 to 1993!
PL/I |
dcl Pumpkin_harvest (1989:1993) fixed bin (31); |
---|
Of course this supports thesis 3 because no human starts counting from 0! And what do "real" programmers in C and PL/I do to improve the examples above?
C | PL/I |
---|---|
float *pa, *pb; float a [10], b [10]; for (pa=a, pb=b; p < a+10; *pa++, *pb++); |
A = B; |
Instead of arrays only pointers are used, with pointer arithmetic
used exclusively! What advantage are C programmers hoping for?
The PL/I compiler can do a better job of optimization than any C
programmer!
Here thesis 1 and 4 apply! An array is treated as a pointer and no
subscript value can be checked by the compiler. Using pointers is not
a natural way of programming arrays.
They obscure inherently clear things! The most extreme example is the C
definition that a[i]
is the same as
*(a+i)
. And this is the same as *(i+a)
. As you
can easily conclude, they are all the same as i[a]
!
Let A
and B
declared as above, then the
following is possible in PL/I:
PL/I |
dcl C float dim (0:9); C = A + 2*B - 4; |
---|
Arrays are full fledged objects and not pointers in PL/I!
Now to the translations of char to PL/I counterparts:
C | PL/I |
---|---|
char c = ' '; char s [10] = {'a','b','c'}; char t [4] = "abc"; t = s; /* illegal */ t = "abc"; /* illegal */ strcpy (t, s); |
dcl C char init ('Ä'); dcl S char (3) init ('abc'); dcl T char (3) var; T = S; T = 'abc'; T = T || 'xy'; |
The C paradigms know about character arrays like s
in the
example above and strings which extend up to a null byte like
t
. Here t
effectively uses 4 bytes of storage,
the length is maximal 3, and the subscript of the last character is 2!
As arrays are in fact constant pointers you cannot assign s
to t
because you
cannot assign a value to a constant!.
The string constant "abc"
may be assigned
to t
in the definition but not by a
assignment statement! Indeed, the library function strcpy
brings some limited help, but it will blindly move many bytes of
s
to t
until it finds the null byte. There is no
check possible whether there is enough space in t
! In PL/I
there is even a concatenation operator (||
), a task which
has to be done in C again by using a library function!
In PL/I, if you want to declare varying length strings ending with a
null byte like in C, you can use the varyingz
attribute. If
you want to use varying strings of arbitrary contents (even ones
containing null bytes in the midst of the string) PL/I gives you the
opportunity to do so by specifying the varying
attribute.
There is no distinction between characters and strings.
Both are scalars in PL/I. Of course, you can declare arrays of strings!
In C there are three concepts regarding bits. The first one treats
int
as a sequence of bits. In PL/I you have to use the
pseudovariable and builtin function unspec
if you want to
treat anything as bit if it is not:
C | PL/I |
---|---|
short int k; k = k & 0x00FF; |
dcl K fixed bin (15); unspec(K) = unspec(K) & '00FF'x; |
This is, of course, not really equivalent because unspec
means bit representation whereas a hexadecimal constant in C represents
an int
!
Of course, use of unspec
may inhibit portability! In PL/I
there is an extra datatype bit
which can be used in an
analogous manner to character strings, complete with builtin functions
like substr
, which can be modelled in C only in an arcane
manner:
C | PL/I |
---|---|
short int x; k = (x>>(m+1-n))& ˜(˜0<<n); |
dcl X bit (16); K = substr(X,M,N); |
The second concept in C uses structures; this, of course, is possible in PL/I, too:
C | PL/I |
---|---|
struct {int a: 4; signed int b: 1; int c: 3;} byte; byte.c = 2; |
dcl 1 Byte unaligned, 2 A bit (4), 2 B bit, 2 C bit (3); C = '010'b; |
Really strange is the treatment of bits as integers: The variable
b
can have only one of two possible
values: 0 and -1!
The third point is that C uses int
values for storing
logical values: 0 means false and all other values mean true. In PL/I a
bit string of length one is used to hold logical values. Thus there is
only one set of logical operators in PL/I whereas C has two:
C | PL/I |
---|---|
if (x > 0 && j == 2) ... b = k < 0 & j == 1; |
if K > 0 & J = 2 then ... B = K > 0 & J = 2; |
As you can see, C needs three concepts where PL/I has only one!
As static and automatic are very similar in C and PL/I, there is one
storage class you can find only in C: register
. It expresses
your wish to hold a variable in a register.
There are two storage classes which can be found only in PL/I:
controlled
and based
. Let's first consider an
example of controlled
:
PL/I |
declare (K, L) fixed bin, A dim (K) char (L) ctl; K = 10; L = 10; allocate A; ... K = 15; L = 29; allocate A; ... put (A); free A; put (A); free A; |
---|
The controlled
(abbrev. ctl
) means that the
variable initially has no storage allocated to it. Only after executing
an allocate
statement can you make a legal reference to it.
After executing a corresponding free
statement, the
controlled
variable is no longer available. A powerful use
of this type of storage class is to make multiple allocations of a
variable before freeing it.
PL/I treats this as building a stack.
Older generations of the variable are hidden in the meantime, and only
the most recent allocation of the variable is available to the program.
Even more
remarkable is that array bounds and string sizes may be different in
different generations.
The PL/I attribute based
can best be explained when
defining chained (or "linked") structures. Look at the following
example:
C | PL/I |
---|---|
struct atype {struct btype *a1; float a2;}; struct btype {long int b1; float b2 [10];}; struct atype *a; a = (struct atype *) malloc (sizeof(struct atype)); a->a1 = (struct btype *) malloc (sizeof(struct btype)); printf ("%f", a->a1>b2[3]); |
dcl 1 A based (P); 2 A1 ptr, 2 A2 float; dcl 1 B based (A1), 2 B1 fixed bin (31), 2 B2 float dim (10); allocate A; allocate B; put (B2(3)); |
In PL/I you find complexity only in the declaration: A
is
where P
points to, B
is where A.A1
points to. After this you can forget the pointers and use only the
object names A
and B
.
In C, however, you have to use the pointers everywhere. Pointers are
indeed typed in C, but even for the purpose of allocation you have to
cast them to byte pointers!
If you compare the following tables, what do you think? Is it really necessary to have 45 operators instead of 20? Is it really necessary to have 15 priority groups instead of 7?
C | ||
---|---|---|
priority | direction | operator |
15
|
<
|
() [] . ->
|
14
|
>
|
++ -- ˜ - + ! & *
(typename) sizeof()
|
13
|
>
|
* / %
|
12
|
>
|
+ -
|
11
|
>
|
<< >>
|
10
|
>
|
< > <= >=
|
9
|
>
|
== !=
|
8
|
>
|
&
|
7
|
>
|
^
|
6
|
>
|
|
|
5
|
>
|
&.&.
|
4
|
>
|
||
|
3
|
<
|
?:
|
2
|
<
|
= += -= *= /= <<=
%= &= ^= |= >>=
|
1
|
>
|
,
|
PL/I | ||
---|---|---|
priority | direction | Operator |
7
|
<
|
+ - ^ **
|
6
|
>
|
* /
|
5
|
>
|
+ -
|
4
|
>
|
= ^= < > <= >= ^< ^>
|
3
|
>
|
||
|
2
|
>
|
&
|
1
|
>
|
| ^
|
Even ==
and >=
are in different precedence
groups! **
and ||
of PL/I
cannot to be found in C, and C's shift operators (the <<
and
>>
operators)
are handled by builtin functions in PL/I.
Many operations in C are done after the so-called usual conversions. So, what is done in the following C example:
C |
signed char c = 0xFF; if (c == 0xFF) ... |
---|
c
is converted to int
giving -1,
0xFF
is converted to int
giving 255! In the first case
sign bits are added, in the second case zeroes.
If sizeof(int)
is less than sizeof(long)
the
following comparisons are both true: -1L < 1U
and -1L >
1UL
! This is because conversion of signed long
to
unsigned long
preserves the bit pattern,
-1L
is converted to the largest possible integer!
Let's consider the division of signed integers in C: -7/4
can yield -1 or -2, depending not on the language definition, but on
the hardware the C program is running on.
Unsigned int
operations are carried out (silently!) in
modulo arithmetic: 1 - 2
yields the largest possible integer!
You will not be astonished to hear that during the >>
operation the sign bit or zeroes may be filled in, dependent not on the
standard but on the hardware!
There is no operator in PL/I which can alter its operands, but in C they abound! Because assignment is an operator you can write:
C |
if (a = b) ... |
---|
a == b
. The resulting value of
the assignment in C is the left operand after having done the side
effect of assignment. The increment and decrement operators only
establish a side effect. Thus, the following examples yield
undefined results:
C |
b = a++ + a; a[i] = i++; |
---|
In both C and PL/I the order of evaluating an expression is undefined. But whereas this is important in PL/I only if you write functions that have side effects, this is possible in C by specifying operators. You may enter such strange situations as
C |
if (a < b || c < d++) ... |
---|
d
after evaluation depends on the
values of a
and b
because in C the evaluation of
a logical expression stops if the final result is already known. In
this case d
is not incremented!
The if
statements in C and PL/I are very similar. Let's
concentrate therefore on the comparison of the switch
and
the select
statements:
C | PL/I |
---|---|
switch (k) { int i = 1; /* ? */ case 1: case 2: ... break; default: ... break; case 0: ... break; } |
select (K); when (1, 2) do; ... end; when (0) ... otherwise do; ... end; end; |
The switch
statement is only a goto
statement
in disguise: dependent on the value of the control variable a jump is
done to that case
which matches the value. If you forget the
break
statement the next case
is also executed.
This is even known by German TV script writers. I watched a thriller
last year where hackers broke into the computer at a bank. In the final
take one of the hackers pointed to the screen and said to the
detective:
"Here, a break
is missing in the switch
statement, thus we fall into the next case
and get unlimited
authorization!"
By the way, the initialization isn't done in a switch
block!
When regarding loop statements we first encounter unsigned problems again:
C | PL/I |
---|---|
unsigned int k = 10; while (k-- >= 0) { ... } |
dcl K unsigned fixed bin init (10); do while (K >= 0); (size): K = K - 1; ... end; |
The loop is intended to execute while k >= 0
.
Unfortunately k
is decremented at 0 nevertheless. In C this
doesn't yield -1 but the largest possible
integer, in effect making this an infinite loop. In PL/I you have the
same effect unless you allow the compiler to check whether the result
of an assignment yields a value beyond the declared size. You do this
by specifying the appropriate condition prefix. PL/I will issue an
error message at execution time. This is typical for C and PL/I: C
keeps quiet when something is going wrong, PL/I raises a condition.
If you want to enter a loop first and check it at the end, both languages offer a similar construct. But more interesting is a loop with a control variable:
C | PL/I |
---|---|
for (i = 1; i <= 10; i++) { ... } |
do I = 1 to 10; ... end; |
In C you have to write down the name of the control variable three
times, thus allowing more chances for
mistyping. In PL/I you say what you want.
Confusing in C is that you can do almost anything between the
parentheses of the for
statement.
Loop control is intended but not forced to be the only purpose. If you
exploit the powerful possibilities of the PL/I do
construct,
it can be confusing, too:
PL/I |
do K = 1 to 10, 9 to 1 by -1, 2 repeat 2*I while (I <= 1024); ... end; |
---|
K
takes the values 1 to 10, 9 down
through 1, and then all powers of 2 up to and including 1024!
In the beginning of this presentation we already saw that C has only
functions. If you specify void
as the return type it's in
fact a subroutine
in the PL/I sense. If you specify void
as the parameter
there is no parameter
in fact. But let's now concentrate on the question of
parameter passing. At first glance there are simple concepts in both
languages:
The C paradigm brings problems when more than one value is to be returned. The solution is to pass pointers to values. The pointers cannot be altered but the values can! In the following example you can discover that arrays in C are only pointers in disguise:
C | PL/I |
---|---|
int a [2] = {1,2}, k, *m; void sub (int k, int *m, int a[]); a++; /* illegal */ ... sub (k, &m, a); printf ("%d %d %d %d", k, *m, a[0], a[1]); ... void sub (int k, int *m, int a[]) { k++; (*m)++; a++; /* legal */ a[0] += 1; /* ??? */ } |
dcl (A dim (0:1) init (1, 2), K, M) fixed bin; ... call Sub ((K), M, A); put (K,M,A); ... Sub: procedure (K, M, A); dcl (K, M, A dim(*)) fixed bin; K += 1; M += 1; A(0) += 1; end; |
The value of argument k
is passed to sub
,
incrementing by 1 doesn't alter the contents of the argument. The
integer value m
points to can be altered because only the
pointer is passed. But what about array a
?
It's declared as an array also in sub
, but it's allowed to
be incremented because it's in fact a pointer, too. Really adventurous
is then the incrementing of a[0]
because it's in
fact incrementing a[1]
!
In PL/I there is only a small problem: if the argument is an
expression or has different attributes than the corresponding parameter,
a copy is passed because a parameter cannot be identical to an
expression. By enclosing K
in parentheses you can therefore
force passing by value.
Arrays are also passed by address. They keep their object nature, and
you can even inquire their bounds by using builtin functions.
There again is an area where PL/I has only one concept to
be learnt, but
C, however, has four! The PL/I builtin function size
is in C
the operator sizeof
! The PL/I builtin function
abs
is in C realized as two library functions:
abs
and labs
(you yourself have to differentiate
according to the type of the argument)!
The PL/I builtin function huge
returns the greatest possible
value of a floating point variable; this is accomplished in C by three
preprocessor values FLT_MAX, DBL_MAX,
and
LDBL_MAX,
according to the type of the argument.
The fourth case is casting: where C uses a type in parentheses in front
of a variable, PL/I again has builtin functions. A PL/I programmer
would find two types of casting in C:
C | PL/I |
---|---|
x = (unsigned) h; y = (float) k; |
unspec(X) = unspec(H); y = float(K); |
The first case preserves the bit pattern which in PL/I can only be
done by using the unspec
pseudovariable and builtin
function. The second case is identical in both languages: the value is
preserved and converted appropriately.
The C library isn't indeed mandatory in the ANSI standard but if it exists it must conform to the standard. I want to show only a few examples but they are symptomatic of the whole library. First, there are character functions which are insufficient even for citizens of Great Britain. Translating the word manœuver to upper case will fail because you will get
C | PL/I |
---|---|
toupper('œ') == EOF |
translate(S, 'ä', 'Ä') |
You've seen it correctly, translating a character which doesn't
belong to the normal ASCII set will yield "End of File" in C! In PL/I
you can specify what you want to be translated, also German Umlauts!
There are also other functions in C which have imprecise return values.
For instance, if malloc
fails to allocate the requested
amount of storage, it returns a null pointer. This is handled in PL/I
in the usual manner: the storage
condition is raised and you
have the chance to take an appropriate action. There are many library
functions in C regarding string manipulation. This is copying the
string s
to string t
in C and PL/I:
C | PL/I |
---|---|
strcpy(t, s); |
(stringsize): T = S; |
In C characters are copied until a null byte is found, and no check
is made whether there is enough space in the target variable
t
to hold all of the characters. In PL/I there is no chance
of destroying your data. If the source string is longer than the
target, it is truncated. If you want to consider this an error, allow
the compiler to check the stringsize
condition. Another
example is strncpy
: I don't want to explain here exactly why
the C library function has a bad design: sometimes it adds a null
character, sometimes not!
But here is the absolute implementation overkill. Depending on the
arguments the function realloc
either behaves as
allocate
, or as free
.
Additionally it may increase or decrease the storage allocated
previously (and not tell you about it!).
C |
realloc (NULL, size) realloc (&a, greater_size) realloc (&a, less_size) realloc (&a, 0) |
---|
There is no argument validation possible! Reading the program yourself doesn't show what the function does. You have to watch the execution!
All input and output is done by library functions in C. What is to be done if you want to read a character?
C | PL/I |
---|---|
if ((c = getchar()) != EOF) ... |
get edit (C) (a(1)); if endfile() then ... |
The C function getchar
returns int
, of course!
If c
in the above example is char
you will never
get an endfile indication because EOF is a negative int
constant! In PL/I there are language elements to do I/O, the
get
statement and the endfile
builtin function in
this case.
A big difference lies in the processing of formats:
C | PL/I |
---|---|
printf ("%*ld %13.6Lf", j, li, ld); scanf ("10lf", &x); |
put edit (K, D) (f(J), f(13,6)); get edit (X) (f(10)); |
Formats in C are interpreted at runtime. The compiler cannot do any
error checking. Additionally you have to specify in the format not
only how to output a value but also what type the variable has. Take
notice that scanf
needs pointers to be able to return
values! All this is completely harmless in PL/I. Formats are
interpreted and checked at compile time. Formats only specify the
outward appearance of a variable, not its type. If you look at the
printf
example there are two formats and three variables.
The first variable, j
, specifies the field length the second
variable is to have. This is done in a clearer way in PL/I: you only
have to specify in the f-format a variable instead of a constant!
The naming of the C functions is standardized in a chaotic manner!
Don't expect analogies: fputc
, putc
, and
fputs
all write to the file specified, whereas
puts
and putchar
both write to stdout
!
Another example is the order of arguments of putc
and
fprintf
: whereas putc
has the file specification
at the end of the argument list, fprintf
demands it as the
first argument!
A powerful example of I/O in PL/I is the so-called record-I/O. The statement
PL/I |
read file (F) into (R) key ('Smith'); |
---|
'Smith'
is at a specified
position within the record. Such a dataset can be considered a small
database!
We have sometimes mentioned error handling methods in C and PL/I.
C has poor or no error indication, compared to PL/I: there are no
floating point errors defined. There are no errors in unsigned
arithmetic by definition! The library uses different error codes:
putchar
returns EOF, setvbuf
a value not equal 0,
and malloc
a null pointer in case of an error. In PL/I every
error has a number, and errors are divided into groups:
anycondition, area, attention, condition, conversion, endfile,
endpage, error, finish, fixedoverflow, invalidop, key, name, overflow,
record, size, storage, stringrange, subscriptrange, transmit,
undefinedfile, underflow,
and zerodivide
. Here is an
example how to intelligently
survive a division by zero:
PL/I |
on zerodivide, underflow begin; put (oncode()); X = 0; goto Continue; end; ... Z = 0; X = Y / Z; Continue:; |
---|
You provide an on-unit which is entered in case of one of the
conditions having been raised. You can, for example, output the error
code, set X
to a value you wish and continue with the
statement following the
statement that produced the error. This is the only case in PL/I
where you need a goto
statement!
Now we come to the preprocessor. The main difference is clear to see: the C preprocessor uses a completely different language -- line-oriented, with special rules and operators whereas preprocessor PL/I is a subset of full PL/I with fewer statements and datatypes available. The capabilities of the C and the PL/I preprocessor are similar regarding simple text replacement activities. Macro facilities are very different. Here is the C definition of a macro computing the square root in C (pay attention to the fact that there is no space between the function name and the left parenthesis!):
C |
#define square( x) (x) * (x) ... square(z+1) ... |
---|
This would be possible in PL/I, too. But, moreover, you can define a
completely new statement, for example the REXX parse
statement. If you want to be able to say:
parse value (Filename) with (Name, '.', Ext);
PL/I |
%Parse: procedure (Upper, Value, With) statement returns (char); dcl (Upper, Value, With, R) char; if ^parmset(Value) | ^parmset(With) then note ('VALUE and WITH are required!', 12); ... R = 'do;'; R = R || ... /* generate normal PL/I statements */ return (R || 'end;'); %end; |
---|
A preprocessor procedure takes the parameters (the keywords of the statement) and computes a return value, consisting of normal PL/I statements using builtin functions, for instance. This return value is then used as replacement text.
We have seen that many things in C are defined in a more dangerous way than in PL/I. First I want to present a list of hints to programmers who occasionally need to program in C:
++
and no decrement --
operators!
Nevertheless we must state that in the definition of C many of the experiences gained by other languages have been ignored. In my opinion, a good language is one that ...
If you would have to rate C and PL/I, to what language would you give more points?