C Programming Language Quiz
This quiz is about quirks of the programming language C and intended for fun and educational purpose. The one or the other question is more academic, i.e., it should make you think ;-)
Unless otherwise stated, the questions and corresponding answers are independent of a specific version of the C standard. Thus the answers are the same considering C89 up to and including C17 and probably future releases.
-
If two pointers
p
andq
of the same type point to the same address, thenp == q
must evaluate to true.YesNoShort answer: Comparing two pointers which are derived from two different objects which are not part of the same aggregate or union object invokes undefined behavior.
Have a look at this post for a detailed discussion.
-
Consider the following code snippet where an object of type
int
is accessed through lvalues of typeshort
andunsigned char
:(A)
and(B)
invoke undefined behavior(A)
invokes undefined behavior but(B)
is legal(B)
invokes undefined behavior but(A)
is legal(A)
and(B)
are legalThe rule of thumb is: accessing an object of type
T
through an lvalue of typeU
whereT
andU
are not compatible (modulo few exceptions) invokes undefined behavior—according to the strict aliasing rules. That means, in the example we accessed an object of typeint
through an lvalue of typeshort
which leads to undefined behavior. One exception to the rule is that a character pointer may alias any other pointer, i.e., any object may be accessed through a character pointer.Note, only type
unsigned char
is guaranteed to have no padding bits and therefore has no trap representation which could invoke undefined behavior (since C11 alsosigned char
is guaranteed to have no padding bits).Thus
(A)
invokes undefined behavior whereas(B)
is legal.Have a look at this post for a detailed discussion.
-
It may make a difference whether an integer constant is given in decimal or hexadecimal format. In other words the meaning may be different.
YesNoIn a nutshell the type of an unsuffixed decimal constant is always signed whereas of a hexadecimal constant the type may be signed or unsigned.
C17 § 6.4.4.1 Integer constants ¶ 5
The type of an integer constant is the first of the corresponding list in which its value can be represented.
Suffix Decimal Constant Octal or Hexadecimal Constant none int
long int
long long int
int
unsigned int
long int
unsigned long int
long long int
unsigned long long int
Thus for constants between
INT_MAX+1
andUINT_MAX
the type differs depending on whether the constant is given in decimal or hexadecimal format. In certain cases this might lead to unexpected effects. One example are functions with a variable number of arguments.Depending on the ABI and width of integer types of a platform, the function calls may differ. For example, according to the ABI for the Arm 32-bit architecture an
int
andlong
are 32-bit wide and passed in one register whereas along long
is 64-bit wide and passed in two registers. Thus the call sides differ.Of course, there are plenty examples about arithmetic expressions which suffer from this nuance as e.g.
where
x
equals zero andy
equals 232 assuming thatint
is 32-bit wide.We may not only run into different behavior on the same platform but also across which results in portability problems. Arithmetic expressions may lead to different results on different platforms. For example, expression
-1 < 0x8000
evaluates totrue
on a platform whereint
is 32-bit wide and tofalse
whereint
is 16-bit wide.A further example where the type of a constant makes a difference are generic selections:
While speaking of generic selections the whole story may also lead to subtle situations in C++ where we have overloaded functions:
Another, probably more contrived example, is
sizeof(0x80000000) == sizeof(2147483648)
which evaluates tofalse
on a platform whereint
is 32-bit wide. -
The following two function declarations can be used interchangeably, i.e., they mean exactly the same:
YesNoThe short answer is that the former declares a function with an unknown number and types of arguments while the latter declares a function without any argument, i.e., it is a nullary function.
Have a look at this post for a detailed discussion.
-
The following function declaration in conjunction with the function definition is legal.
YesNoThat is a valid function declaration and definition. The declaration only introduces the function name
foo
without defining the number and types of arguments.Have a look at this post for a detailed discussion.
-
The following function declaration in conjunction with the function definition is legal.
YesNoThis is not a valid function declaration in conjunction with the definition. The short answer is that whenever a function is called where no prototype is available, then the default argument promotions are applied. For example, a
float
is promoted to adouble
. In this case, the function type is not compatible with the function type after the default argument promotions.Have a look at this post for a detailed discussion.
-
Consider function
foo
which does not return a value although the return type isint
.Now consider two statements
(A)
and(B)
which call functionfoo
, respectively.(A)
and(B)
invoke undefined behavior(A)
invokes undefined behavior but(B)
is legal(B)
invokes undefined behavior but(A)
is legal(A)
and(B)
are legalThis is an artifact from the old days. In K&R C there exists no type
void
. Furthermore, in case no type was given, default typeint
was assumed. Thus, the following functions are equivalent in K&R C and both do not return a value:The implicit
int
rule was abandoned in C99 (see discussion N661 or C99 rationale). However, for the sake of downward compatibility, it is still legal to return no value. Though, using the value of the call invokes undefined behavior.C17 § 6.9.1 Function definitions ¶ 12
If the
}
that terminates a function is reached, and the value of the function call is used by the caller, the behavior is undefined.Fun fact: C++ differs in the sense that value-returning functions must return a value—independent whether the value of a call is used or not.
C++98 § 6.6.3 The
return
statement ¶ 2[…] Flowing off the end of a function is equivalent to areturn
with no value; this results in undefined behavior in a value-returning function.Note, what may seem counterintuitive at first glance is that a C++ compiler may in general only emit diagnostics in such cases and not treat them as errors. The following example illustrates why this is the case.
First of all there is no way to express a value of type
T
which could be returned in case theelse
branch is taken. Furthermore, in general it is not possible to prove whether function callabort_program()
terminates or not. Thus, throwing an error would reject possibly valid C++ programs which is the reason why only a diagnostics is emitted, if at all. -
Consider the following two declarations of function
foo
.Which linkage does function
foo
finally have?Internal linkageExternal linkageCode invokes undefined behaviorThis is defined in the following paragraph:
C17 § 6.2.2 Linkages of identifiers ¶ 4
For an identifier declared with the storage-class specifier
extern
in a scope in which a prior declaration of that identifier is visible,31) if the prior declaration specifies internal or external linkage, the linkage of the identifier at the later declaration is the same as the linkage specified at the prior declaration. If no prior declaration is visible, or if the prior declaration specifies no linkage, then the identifier has external linkage.31) As specified in 6.2.1, the later declaration might hide the prior declaration.
Fun fact: the other way around
invokes undefined behavior which is caught by GCC as well as Clang:
-
Function parameter
x
isconst
-qualified in the function declaration but not in the function definition. Furthermore, parameterx
is even written to in the function body. Is this legal?YesNoType qualifiers are not considered while determining if two function parameters are compatible.
C11 § 6.7.6.3 Function declarators (including prototypes) ¶ 15
[…] In the determination of type compatibility and of a composite type, […] each parameter declared with qualified type is taken as having the unqualified version of its declared type. […]This was also asked in DR040.
-
The return type for function
foo
isconst
-qualified in the function definition but not in the function declaration. Is this legal?YesNoThe answer to this question is neither right nor wrong. The overall consensus is that qualifiers for rvalues should be ignored. Though, strictly speaking the wording of the C standard up to and including C11 do not address this.
In C17 it was made explicit that type qualifiers should be ignored for rvalues, i.e., in particular for casts (C17 § 6.5.4 ¶ 5), lvalue conversion (C17 § 6.5.1.1 ¶ 2), and for function declarators:
C17 § 6.7.6.3 Function declarators (including prototypes) ¶ 5
If, in the declaration “
T D1
”,D1
has the formor
and the type specified for ident in the declaration “
T D
” is “derived-declarator-type-listT
”, then the type specified for ident is “derived-declarator-type-list function returning the unqualified version ofT
”.The words “the unqualified version of” were added in C17 which have a greater impact than one might first suspect. Consider the following code:
Both assignments are legal, although the return types of the function types are differently
const
-qualified. -
Consider the following translation unit consisting of a global variable declaration:
At the point of the declaration of variable
x
the structure typefoo
is incomplete. Thus the size is unknown. Only later on in the translation unit the type is completed. Is this translation unit legal?YesNoThis is allowed in certain circumstances as e.g. for global variables. A similar argument holds for arrays with an incomplete type.
This is also mentioned in DR016.
-
Declaring a variable of type
void
with internal linkage is not legal.Is it legal to declare a variable of type
void
with external linkage?YesNoAccording to the grammar this is legal. Furthermore, it is nowhere explicitly stated in the C11 standard that it is not allowed. However, I cannot think of any real use case and therefore assume that this is only allowed by accident. Note, type
void
is incomplete and cannot be completed.C11 § 6.2.5 Types ¶ 19
The
void
type comprises an empty set of values; it is an incomplete object type that cannot be completed.Thus the only operation which comes to my mind is to take the address of variable
foo
. However, it turns out that expressionfoo
is not a valid lvalue:C11 § 6.3.2.1 Lvalues, arrays, and function designators ¶ 1
An lvalue is an expression (with an object type other thanvoid
) that potentially designates an object […]It is unclear to me whether taking the address of such objects was legal in the past or not, since the definition of lvalues changed between standard revisions. At least for C11 I cannot think of any meaningful and conforming operation on external objects of type
void
.The whole story is also discussed in DR012. There it has been pointed out that it is legal to take the address of object
foo
if the type is changed fromvoid
toconst void
. Again this looks more like an oversight to me than something which was intended. -
The bit representation of the null pointer must have all bits zero.
YesNoThe null pointer constant is defined by the C standard. However, the representation of the null pointer at run-time is not. Actually, the representation of pointers in general is not defined by the C standard. For example, the Symbolics Lisp Machine 3600 does not make use of numerical pointers at all but of tuples of the form
<array-object, index>
. The representation of the null pointer is then<nil, 0>
. Have a look at clc FAQ 5.17 for further examples. -
Constant
0
evaluates to an integer or the null pointer—depending on the context.YesNo -
The expression
(void *)0
evaluates to the null pointer.YesNo -
If the expression
e
evaluates to0
, then it is guaranteed that(void *)e
evaluates to the null pointer.YesNoOnly the null pointer constant converted to pointer type is guaranteed to equal the null pointer.
-
If the expression
e
evaluates to the null pointer, then it is guaranteed thate+0
evaluates to the null pointer, too.YesNoNull pointer arithmetic is undefined behavior in C.
-
Is it possible that function
foo
is called?YesNoShort answer: Yes, it is possible because variable
x
is uninitialized when read.Long answer: the wording of the C standard has changed over time w. r. t. uninitialized variables, indeterminate values, and so on. Therefore, let us consider C11 for the moment which received the following update:
C11 § 6.3.2.1 Lvalues, arrays, and function designators ¶ 2
[…] If the lvalue designates an object of automatic storage duration that could have been declared with theregister
storage class (never had its address taken), and that object is uninitialized (not declared with an initializer and no assignment to it has been performed prior to use), the behavior is undefined. […]Thus according to the paragraph the code snippet invokes undefined behavior and therefore anything may happen including a function call to
foo
.Why the special treatment of objects which may have
register
storage class? This constraint can be traced back to the Intel Itanium architecture (see DR338) which has general integer registers with 64-bit plus one trap bit. The trap bit indicates whether the register is initialized or not and is referred to asNaT
, “not-a-thing”. Reading a register where theNaT
bit is set may raise an exception. -
Now consider a slightly modified version of the previous question. Is it possible that function
foo
is called?YesNoIn the previous example it is possible that function
foo
is called because the code invokes undefined behavior according to C11 section 6.3.2.1 paragraph 2. This time the address of variablex
is taken and therefore the paragraph does not apply anymore.The value of variable
x
is indeterminate, i.e., it is either a trap representation or an unspecified value. In the former case, reading a trap representation invokes undefined behavior (C11 § 6.2.6.1 ¶ 5) and therefore anything may happen including a function call tofoo
. In the latter case, if the value ofx
is unspecified, the expressionx != x
evaluates to an unspecified value, too. Thus the expression may evaluate totrue
orfalse
which means that functionfoo
may be called.Have a look at DR260 or DR451 for further discussions of indeterminate and unspecified values. The prevailing opinion is that the value of an expression is indeterminate (unspecified) if a subexpression evaluates to an indeterminate (unspecified) value. For example, if
x
of typeint
is unspecified, thenx *= 0
is unspecified, too. In particular this means it is not guaranteed thatx
equals zero after the assignment. Furthermore, unspecified values may be “unstable” or sometimes also referred to as “wobbly”.In the example it is not guaranteed that the same value is printed twice. Thus, the value of
x
may change between the function calls. For further discussions have a look at N1793, N1818, N2012, N2013, N2221. -
Again, consider a slightly modified version of the previous question. Is it possible that function
foo
is called?YesNoThe type
unsigned char
is guaranteed to have no trap representations (C11 § 6.2.6.1 ¶ 3). Thus the initial value ofx
is unspecified.According to an answer from a C committee member on StackOverflow the value of
x
should be specified after the call to the standard library functionmemcpy
. Thus the expressionx != x
should evaluate tofalse
. I'm saying should because I cannot find any evidence for that in the C standard. Having a look at DR451 the committees response islibrary functions will exhibit undefined behavior when used on indeterminate values
which contradicts with the answer on StackOverflow. Therefore, I leave this question open.Have a look at Uninitialized Reads for further discussions.
-
Let
T
be a (derived) object type. The assignment ofcp
is legal. Is the assignment ofcpp
legal, too?YesNoFor this question I do not have a short answer. Have a look at this post for a detailed discussion.
-
The expression
sizeof(int) > -1
evaluates totrue
false
The short answer is that operator
sizeof
returns an unsigned integer of typesize_t
. According to the usual arithmetic conversions (C11 § 6.3.1.8) the operand which is signed and has a lower rank than the unsigned operand, is converted to an unsigned integer type of the same rank as the unsigned operand. Any signed integer which is equivalent to-1
has all bits set. Such a sequence of bits interpreted as an unsigned integer equals the maximal unsigned integer of each corresponding integer rank. In other words(unsigned int)-1
equalsUINT_MAX
. The same holds true for all other integersshort int
,long int
, andlong long int
. Thus, the left operand of the relational operator evaluates to the maximal unsigned integer representable by an unsigned integer whose rank is equivalent to the rank ofsize_t
. Since there exists no unsigned integer with a same rank which is strictly greater, the expression always evaluates tofalse
. -
The following two statements are equivalent.
YesNoThe first assignment initializes an array with automatic or static storage duration (depends on whether
x
is declared in file or function scope) which is modifiable. Whereas the second assignment initializes a pointer to an array with static storage duration which is not necessarily modifiable. Or in other words arrays are not pointers.Have a look at this post for a detailed discussion.
-
Let
int a[42];
be an array, then all three expressionsa
and&a
and&a[0]
evaluate to a pointer to the first element of the array, i.e., the following equalities hold:Does this mean that all three expressions mean exactly the same and therefore can be used interchangeably?
YesNoAll three expressions evaluate to a pointer to the first element of the array. However, each expression has a different type. Have a look at this post for a detailed discussion.
-
Assume that the variables
a
,b
, andc
are initialized before read.The values for the variables
x
andz
may differ, i.e.,x == z
may evaluate tofalse
.are always the same, i.e.,x == z
always evaluates totrue
.Short answer: The integer promotions require that the value of each variable is promoted to size
int
, then the addition and division is performed. The resulting value is truncated and stored in the corresponding variable of each assignment, respectively. Provided that the addition may overflow the values forx
andz
may not equal.Long answer: The modulo operation is not distributive over division. Lets illustrate the problem by an example. Assume
a=255;
,b=1;
, andc=2;
, then for the first assignment we have that the value stored inx
equals the expression((255 + 1) / 2) % 256
which equals(256 / 2) % 256
which equals128 % 256
which finally equals128
. For the second assignment we have that the value stored iny
equals the expression(255 + 1) % 256
which equals256 % 256
which finally equals0
. N.B. an overflow occurred which is defined behavior for unsigned integers. For the last assignment we then have that the value stored inz
equals the expression(0 / 2) % 256
which equals0 % 256
which finally equals0
. Thereforex == z
is not always the case since128 != 0
.Fun fact 1: The modulo operation is distributive over addition, i.e.,
(a + b) % x == [(a % x) + (b % x)] % x
for alla,b,x
. Thus, if we change the division by an addition, then the values for the variablesx
andz
are always the same.Fun fact 2: We could change the first assignment to
uint8_t x = ((uint8_t)(a + b)) / c;
. Then the values for the variablesx
andz
are always the same, too. -
Is the size of type
char
the same as the size of any character constant? That means, the expressionsizeof(char) == sizeof('x')
for any character constant'x'
evaluates totrue
false
Character constants have type
int
(C11 § 6.4.4.4 ¶ 10). Thus onlysizeof(int) == sizeof('x')
is guaranteed to evaluate totrue
.N.B. in terms of the C11 standard a character constant is either an integer character constant or a wide character constant. The former is a sequence of one or more multibyte characters. Thus 'abc' is a valid integer character constant where the representation is implementation-defined. If an integer character constant contains a single character, then its value equals the integer representation of an object of type
char
which represents the very same single character. -
Consider the following code snippet:
The expression
x+++y
leads to a compile error since operator+++
is not definedinvokes undefined behavioris equivalent to(x++) + y
is equivalent tox + (++y)
In C you cannot define new operators like in C++. Thus such wired locking operators are a combination of existing operators. For example,
--*--
is not a new operator but a valid combination of operators in the following code snippet:Expression
--*--p
is equivalent to--(*(--p))
which evaluates to-1
and as a side-effect assigns-1
tox[0]
. -
Consider the following code snippet:
Code invokes undefined behavior.Code is legal and the value of variabley
equalsOperator precedence is well defined. However, the order of evaluation of arithmetic operands is undefined. Thus, the expression
(x=1) + (x=2)
invokes undefined behavior, i.e., it is undefined whether variablex
should equal1
or2
after the assignments. This renders the whole code as undefined.Fun fact: GCC 8.2.1 evaluates the expression to
4
and Clang 7.0.0 to3
while using options -std=c11 -O2. -
Consider the following code snippet:
The value of variable
y
equalsanything because the code invokes undefined behaviortrue
false
Operator precedence is well defined and for the logical operators
&&
and||
the order of evaluation of operands is well defined, too. With the words of the C standard: between the evaluation of the first and second operand there exists a sequence point. Thus in the example we have that firstx=1
is evaluated which equalstrue
and thenx=2
is evaluated which also equalstrue
which renders the whole expressiontrue
. -
Which of the following two compilation units compile?
Both compilation units compile.Compilation unitA
compiles but notB
.Compilation unitB
compiles but notA
.Both compilation units do not compile.Short answer: compilation unit A does not compile because global array
x
is a variable length array which is not allowed at file scope. However, a variable length array is allowed at block scope and therefore compilation unit B compiles.For the long answer we first look up the definition of an integer constant expression:
C11 § 6.6 Constant expressions ¶ 6
An integer constant expression117) shall have integer type and shall only have operands that are integer constants, enumeration constants, character constants,
sizeof
expressions whose results are integer constants,_Alignof
expressions, and floating constants that are the immediate operands of casts. Cast operators in an integer constant expression shall only convert arithmetic types to integer types, except as part of an operand to thesizeof
or_Alignof
operator.117) An integer constant expression is required in a number of contexts such as the size of a bit-field member of a structure, the value of an enumeration constant, and the size of a non-variable length array. Further constraints that apply to the integer constant expressions used in conditional-inclusion preprocessing directives are discussed in 6.10.1.
Thus, the expressions
n
andm
which are used while declaring arraysx
andy
, respectively, are not integer constant expressions—althoughn
andm
are both variables which areconst
-qualified. If the size expression of an array declaration is not an integer constant expression we have the following:C11 § 6.7.6.2 Array declarators ¶ 4
[…] If the size is an integer constant expression and the element type has a known constant size, the array type is not a variable length array type; otherwise, the array type is a variable length array type. […]Thus both arrays
x
andy
are of type variable length array for which we have:C11 § 6.7.6.2 Array declarators ¶ 2
If an identifier is declared as having a variably modified type, it shall be an ordinary identifier (as defined in 6.2.3), have no linkage, and have either block scope or function prototype scope. If an identifier is declared to be an object with static or thread storage duration, it shall not have a variable length array type.
Strictly speaking variable length arrays are a feature that implementations need not support and therefore there might exist a compiler which does not compile unit B, too. However, the takeaway message is that you cannot make use of variables in constant expressions although they are
const
-qualified. You might want to replace a variable with a preprocessor macro, but then you lose all type information, which has some drawbacks, too.Fun fact: In C++ both compilation units compile fine. Though, C++ does not have the concept of variable length arrays and therefore
y
is compiled into an ordinary array consisting of 42 elements. -
Consider the following function which includes a
switch
statement:Code does not compileCode compiles but invokes undefined behaviorCode is legal and after three consecutive function callsfoo(0); foo(1); foo(2);
the console output equals02313223The body of a switch statement may be an arbitrary statement which renders the given code legal. Before we go on let us have a look at some oddities.
Although the controlling expression of the
if
statement always evaluates tofalse
thecase
label inside the body of thetrue
branch renders it live, i.e., the statementprintf("1");
is not dead code.Another interesting point is that the clause-1 of the loop as well as the controlling expression must not be executed, if
case 2
is jumped to. Thus, it is crucial that variablei
is initialized upfront.A further oddity is that although
case 1
has nobreak
statement and therefore falls through, it “jumps” overcase 2
and continues atcase 3
becausecase 1
is contained in thetrue
branch andcase 2
is contained in thefalse
branch of theif
statement.Thus we have the following console output produced by the respective function calls:
A famous real world example of a mixed loop and switch statement is termed Duff's device.
-
Consider the following function where the type of parameter
x
as well as of local variabley
is an array of 42 integers.The return value of function
foo
equalstrue
false
Any parameter of type “array of
T
” is adjusted to a “pointer toT
”. Thus the example is equivalent to the following:For
sizeof(y)
we have that it equals42 * sizeof(int)
. In general, the size of an object pointer does not equal 42 times the size of an integer. Thus,sizeof(x) == sizeof(y)
evaluates tofalse
in the general case.Have a look at this post for a detailed discussion.
-
Assume that a compilation unit only consists of the definition of function
foo
and nothing else.Inside of function
foo
another functionbar
is called for which no declaration is given.Code does not compileCode compiles and is legal ifbar
is a unary function and the type of the parameter is ashort
Code compiles and is legal ifbar
is a unary function and the type of the parameter is aint
Code compiles and is legal ifbar
is a unary function and the type of the parameter is along
The code compiles since in C implicit functions are allowed—in contrast to C++ where this is a compile-time error.
Although integer
42
is guaranteed to be representable by all integer types—including all character types—the only legal type for the parameter isint
. This is the case since no function declaration forbar
is given and therefore the integer argument promotions are applied (see C11 § 6.5.2.2 Function calls ¶ 6) which means that number42
is represented by anint
. Thus if type of functionbar
is not compatible withT (*)(int)
for some return typeT
, then the example invokes undefined behavior. -
Consider the following code snippet:
According to one of the following three standards the code invokes undefined behavior. Which one?
C90C99C11The lifetime of certain objects has been reduced in C11. In the example, the object returned by the function call is only alive until the right-hand side is evaluated in C11 whereas in C99 it is alive until the enclosing block ends. Referring to an object outside its lifetime invokes undefined behavior.
An example which invokes undefined behavior in C99, too, is given in the following:
The lifetime of an object with automatic storage duration is bounded to its nearest enclosing block in C99. Thus in line 4 we refer to an object which is not live anymore resulting in undefined behavior.
C11 § 6.2.4 Storage durations of objects ¶ 2
[…] If an object is referred to outside of its lifetime, the behavior is undefined. […]The following paragraph is new and is the reason why the example invokes undefined behavior in C11.
C11 § 6.2.4 Storage durations of objects ¶ 8
[…] Its lifetime ends when the evaluation of the containing full expression or full declarator ends. […]
For further discussion have a look at N1285 from which the code snippet was taken.
Score
You answered 0 out of 0 questions correctly!