Saturday, April 5, 2008

C Language Technical Interview Questions - 16

Declarations and Initializations:

1. How do you decide which integer type to use?

A: If you might need large values (above 32,767 or below -32,767), use long. Otherwise, if space is very important (i.e. if there are large arrays or many structures), use short. Otherwise, use int. If well-defined overflow characteristics are important and negative values are not, or if you want to steer clear of sign- extension problems when manipulating bits or bytes, use one of the corresponding unsigned types. (Beware when mixing signed and unsigned values in expressions, though.) Although character types (especially unsigned char) can be used as "tiny" integers, doing so is sometimes more trouble than it's worth, due to unpredictable sign extension and increased code size.

A similar space/time tradeoff applies when deciding between float and double. None of the above rules apply if the address of a variable is taken and must have a particular type. If for some reason you need to declare something with an *exact* size (usually the only good reason for doing so is when attempting to conform to some externally-imposed storage layout, but see question 20.5), be sure to encapsulate the choice behind an appropriate typedef.

2. What should the 64-bit type on a machine that can support it?

A: The forthcoming revision to the C Standard (C9X) specifies type long long as effectively being at least 64 bits, and this type has been implemented by a number of compilers for some time. (Others have implemented extensions such as __longlong.) On the other hand, there's no theoretical reason why a compiler couldn't implement type short int as 16, int as 32, and long int as 64 bits, and some compilers do indeed choose this arrangement.

3. What's the best way to declare and define global variables and functions?

A: First, though there can be many "declarations" (and in many translation units) of a single "global" (strictly speaking, "external") variable or function, there must be exactly one "definition". (The definition is the declaration that actually allocates space, and provides an initialization value, if any.) The best arrangement is to place each definition in some relevant .c file, with an external declaration in a header (".h") file, which is #included wherever the declaration is needed. The .c file containing the definition should also #include the same header file, so that the compiler can check that the definition matches the declarations. This rule promotes a high degree of portability: it is consistent with the requirements of the ANSI C Standard, and is also consistent with most pre-ANSI compilers and linkers. (Unix compilers and linkers typically use a "common model" which allows multiple definitions, as long as at most one is initialized; this behavior is mentioned as a "common extension" by the ANSI Standard, no pun intended. A few very odd systems may require an explicit initializer to distinguish a definition from an external declaration.) It is possible to use preprocessor tricks to arrange that a line like DEFINE(int, i); need only be entered once in one header file, and turned into a definition or a declaration depending on the setting of some macro, but it's not clear if this is worth the trouble. It's especially important to put global declarations in header files if you want the compiler to catch inconsistent declarations for you. In particular, never place a prototype for an external function in a .c file: it wouldn't generally be checked for consistency with the definition, and an incompatible prototype is worse than useless.

4: What does extern mean in a function declaration?

A: It can be used as a stylistic hint to indicate that the function's definition is probably in another source file, but there is no formal difference between extern int f(); and int f();

5: What's the auto keyword good for?

A: Nothing; it's archaic.

6. Define a linked list and tried typedef struct { char *item; NODEPTR next; } *NODEPTR; but the compiler give to error messages. Can't a structure in C contain a pointer to itself?

A: Structures in C can certainly contain pointers to themselves; the discussion and example in section 6.5 of K&R make this clear. The problem with the NODEPTR example is that the typedef has not been defined at the point where the "next" field is declared. To fix this code, first give the structure a tag ("struct node"). Then, declare the "next" field as a simple "struct node *", or disentangle the typedef declaration from the structure definition, or both. One corrected version would be struct node { char *item; struct node *next; }; typedef struct node *NODEPTR; and there are at least three other equivalently correct ways of arranging it. A similar problem, with a similar solution, can arise when attempting to declare a pair of typedef'ed mutually referential structures.

7: How do you declare an array of N pointers to functions returning pointers to functions returning pointers to characters?

A: The first part of this question can be answered in at least three ways:

1. char *(*(*a[N])())(); 2. Build the declaration up incrementally, using typedefs: typedef char *pc; /* pointer to char */ typedef pc fpc(); /* function returning pointer to char */ typedef fpc *pfpc; /* pointer to above */ typedef pfpc fpfpc(); /* function returning... */ typedef fpfpc *pfpfpc; /* pointer to... */ pfpfpc a[N]; /* array of... */ 3. Use the cdecl program, which turns English into C and vice versa: cdecl> declare a as array of pointer to function returning pointer to function returning pointer to char char *(*(*a[])())() cdecl can also explain complicated declarations, help with casts, and indicate which set of parentheses the arguments go in (for complicated function definitions, like the one above). Any good book on C should explain how to read these complicated C declarations "inside out" to understand them ("declaration mimics use"). The pointer-to-function declarations in the examples above have not included parameter type information. When the parameters have complicated types, declarations can *really* get messy.

8: How you declare a function that can return a pointer to a function of the same type? you will building a state machine with one function for each state, each of which returns a pointer to the function for the next state. But you can't find a way to declare the functions.

A: We can't quite do it directly. Either have the function return a generic function pointer, with some judicious casts to adjust the types as the pointers are passed around; or have it return a structure containing only a pointer to a function returning that structure.

9. Some compiler is complaining about an invalid redeclaration of a function, but you only define it once and call it once.

A: Functions which are called without a declaration in scope (perhaps because the first call precedes the function's definition) are assumed to be declared as returning int (and without any argument type information), leading to discrepancies if the function is later declared or defined otherwise. Non-int functions must be declared before they are called. Another possible source of this problem is that the function has the same name as another one declared in some header file.

10. What's the right declaration for main()? Is void main() correct?

A: But no, it's not correct

11. What are you allowed to assume about the initial values of variables which are not explicitly initialized? If global variables start out as "zero", is that good enough for null pointers and floating-point zeroes?

A: Uninitialized variables with "static" duration (that is, those declared outside of functions, and those declared with the storage class static), are guaranteed to start out as zero, as if the programmer had typed "= 0". Therefore, such variables are implicitly initialized to the null pointer (of the correct type; see also section 5) if they are pointers, and to 0.0 if they are floating-point. Variables with "automatic" duration (i.e. local variables without the static storage class) start out containing garbage, unless they are explicitly initialized. (Nothing useful can be predicted about the garbage.) Dynamically-allocated memory obtained with malloc() and realloc() is also likely to contain garbage, and must be initialized by the calling program, as appropriate. Memory obtained with calloc() is all-bits-0, but this is not necessarily useful for pointer or floating-point values

12. This code, straight out of a book, isn't compiling:

int f() { char a[] = "Hello, world!"; }

A: Perhaps you have a pre-ANSI compiler, which doesn't allow initialization of "automatic aggregates" (i.e. non-static local arrays, structures, and unions). (As a workaround, and depending on how the variable a is used, you may be able to make it global or static, or replace it with a pointer, or initialize it by hand with strcpy() when f() is called.)

13. What's wrong with this initialization? char *p = malloc(10); your compiler is complaining about an "invalid initializer", or something.

A: Is the declaration of a static or non-local variable? Function calls are allowed only in initializers for automatic variables (that is, for local, non-static variables).

14. What is the difference between these initializations? char a[] = "string literal"; char *p = "string literal"; My program crashes if I try to assign a new value to p[i].

A: A string literal can be used in two slightly different ways. As an array initializer (as in the declaration of char a[]), it specifies the initial values of the characters in that array. Anywhere else, it turns into an unnamed, static array of characters, which may be stored in read-only memory, which is why you can't safely modify it. In an expression context, the array is converted at once to a pointer, as usual (see section 6), so the second declaration initializes p to point to the unnamed array's first element. (For compiling old code, some compilers have a switch controlling whether strings are writable or not.)

15. I finally figured out the syntax for declaring pointers to functions, but now how do I initialize one?

A: Use something like extern int func(); int (*fp)() = func; When the name of a function appears in an expression like this, it "decays" into a pointer (that is, it has its address implicitly taken), much as an array name does. An explicit declaration for the function is normally needed, since implicit external function declaration does not happen in this case (because the function name in the initialization is not part of a function call).

No comments: