C Interview Questions and Answers asked in Actual Interviews

When writing your C program, you can include
files in two ways. The first way is to surround the file you
want to include with the angled brackets < and >. This method of
inclusion tells the preprocessor to look for the file in the predefined default
location. This predefined default location is often an INCLUDE environment
variable that denotes the path to your include files. For instance, given the
INCLUDE variable

using the #include <file> version of file inclusion, the compiler first
checks the C:\COMPILER\INCLUDE
directory for the specified file. If the file is not found there, the compiler
then checks the
S:\SOURCE\HEADERS directory. If the file is still not found, the preprocessor checks
the current directory.

The second way to include files is to surround the file you want to include
with double quotation marks. This method of inclusion tells the preprocessor to
look for the file in the current directory first, then look for it in the
predefined locations you have set up. Using the #include “file” version of file
inclusion and applying it to the preceding example, the preprocessor first
checks the current directory for the specified file. If the file is not found
in the current directory, the C:COMPILERINCLUDE directory is searched. If the
file is still not found, the preprocessor checks the S:SOURCEHEADERS directory.
The #include <file> method of file inclusion is often used to include
standard headers such as stdio.h or
stdlib.h. This is because these headers are rarely (if ever) modified, and they
should always be read from your compiler’s standard include file directory.

The #include “file” method of file inclusion is often used to include
nonstandard header files that you have created for use in your program. This is
because these headers are often modified in the current directory, and you will
want the preprocessor to use your newly modified version of the header rather
than the older, unmodified version.

The use of an enumeration
constant (enum) has many advantages over using the traditional symbolic
constant style of #define. These advantages include a lower maintenance
requirement, improved program readability, and better debugging capability.

1) The first advantage is that enumerated constants are generated automatically
by the compiler. Conversely,
symbolic constants must be manually assigned values by the programmer.

For instance, if you had an enumerated constant type for error codes that could
occur in your program, your enum definition could look something like this:

In the preceding example, OUT_OF_MEMORY is automatically assigned the value of
0 (zero) by the compiler because it appears first in the definition. The
compiler then continues to automatically assign numbers to the enumerated
constants, making INSUFFICIENT_DISK_SPACE equal to 1, LOGIC_ERROR equal to 2,
and FILE_NOT_FOUND equal to 3, so on.

If you were to approach the same example by using symbolic constants, your code
would look something like this:

values by the programmer. Each of the two methods arrives at the same result:
four constants assigned numeric values to represent error codes. Consider the
maintenance required, however, if you were to add two constants to represent
the error codes DRIVE_NOT_READY and CORRUPT_FILE. Using the enumeration
constant method, you simply would put these two constants anywhere in the enum
definition. The compiler would generate two unique values for these constants.
Using the symbolic constant method, you would have to manually assign two new
numbers to these constants. Additionally, you would want to ensure that the
numbers you assign to these constants are unique.

2) Another advantage of using the enumeration constant method is that your
programs are more readable and thus can be understood better by others who
might have to update your program later.

3) A third advantage to using enumeration constants is that some symbolic
debuggers can print the value of an enumeration constant. Conversely, most
symbolic debuggers cannot print the value of a symbolic constant. This can be
an enormous help in debugging your program, because if your program is stopped
at a line that uses an enum, you can simply inspect that constant and instantly
know its value. On the other hand, because most debuggers cannot print #define
values, you would most likely have to search for that value by manually looking
it up in a header file.

Your C compiler library
contains a low-level file function called sopen() that can be used to open a
file in shared mode. Beginning with DOS 3.0, files could be opened in shared
mode by loading a special program named SHARE.EXE. Shared mode, as the name
implies, allows a file to be shared with other programs as well as your own.

Using this function, you can allow other programs that are running to update
the same file you are updating.

The sopen() function takes four parameters: a pointer to the filename you want
to open, the operational
mode you want to open the file in, the file sharing mode to
use, and, if you are creating a file, the mode to create the file in. The
second parameter of the sopen() function, usually referred to as the “operation
flag”parameter, can have the following values assigned to it:

Constant Description O_APPEND Appends all writes to the end of the file

O_BINARY Opens the file in binary (untranslated) mode
O_CREAT If the file does not exist, it is created
O_EXCL If the O_CREAT flag is used and the file exists, returns an error
O_RDONLY Opens the file in read-only mode
O_RDWR Opens the file for reading and writing
O_TEXT Opens the file in text (translated) mode
O_TRUNC Opens an existing file and writes over its contents
O_WRONLY Opens the file in write-only mode

The third parameter of the sopen() function, usually referred to as the
“sharing flag,” can have the following values assigned to it:

Constant Description
SH_COMPAT No other program can access the file
SH_DENYRW No other program can read from or write to the file
SH_DENYWR No other program can write to the file
SH_DENYRD No other program can read from the file
SH_DENYNO Any program can read from or write to the file

If the sopen() function is successful, it returns a non-negative number that is
the file’s handle. If an error occurs, –1 is returned, and the global variable
errno is set to one of the following values:

To hash means to grind up, and that’s essentially
what hashing is all about. The heart of a hashing algorithm is a hash function
that takes your nice, neat data and grinds it into some random-looking integer.

The idea behind hashing is that some data either has no inherent ordering (such
as images) or is expensive to compare (such as images). If the data has no
inherent ordering, you can’t perform comparison searches.

If the data is expensive to compare, the number of comparisons used even by a
binary search might be too many. So instead of looking at the data themselves,
you’ll condense (hash) the data to an integer (its hash value) and keep all the
data with the same hash value in the same place. This task is carried out by
using the hash value as an index into an array.

To search for an item, you simply hash it and look at all the data whose hash
values match that of the data you’re looking for. This technique greatly
lessens the number of items you have to look at. If the parameters are set up
with care and enough storage is available for the hash table, the number of
comparisons needed to find an item can be made arbitrarily close to one.

One aspect that affects the efficiency of a hashing implementation is the hash
function itself. It should ideally distribute data randomly throughout the
entire hash table, to reduce the likelihood of collisions. Collisions occur
when two different keys have the same hash value. There are two ways to resolve
this problem. In “open addressing,” the collision is resolved by the choosing
of another position in the hash table for the element inserted later. When the
hash table is searched, if the entry is not found at its
hashed position in the table, the search continues checking until either the
element is found or an empty position in the table is found

The second method of resolving a hash collision is called “chaining.” In this
method, a “bucket” or linked list holds all the elements whose keys hash to the
same value.

The answer depends on what you mean by quickest. For
most sorting problems, it just doesn’t matter how quick the sort is because it
is done infrequently or other operations take significantly more time anyway.
Even in cases in which sorting speed is of the essence, there is no one answer.
It depends on not only the size and nature of the data, but also the likely
order. No algorithm is best in all cases.

There are three sorting methods in this author’s “toolbox” that are all very
fast and that are useful in different situations. Those methods are quick sort,
merge sort, and radix sort.

The Quick Sort
The quick sort algorithm is of the “divide and conquer” type. That means it
works by reducing a sorting
problem into several easier sorting problems and solving each of them. A
“dividing” value is chosen from the input data, and the data is partitioned
into three sets: elements that belong before the dividing value, the value
itself, and elements that come after the dividing value. The partitioning is
performed by exchanging elements that are in the first set but belong in the
third with elements that are in the third set but belong in the first Elements
that are equal to the dividing element can be put in any of the three sets—the
algorithm will still work properly.

The Merge Sort
The merge sort is a “divide and conquer” sort as well. It works by considering
the data to be sorted as a
sequence of already-sorted lists (in the worst case, each list is one element
long). Adjacent sorted lists are merged into larger sorted lists until there is
a single sorted list containing all the elements. The merge sort is good at
sorting lists and other data structures that are not in arrays, and it can be
used to sort things that don’t fit into memory. It also can be implemented as a
stable sort.

The Radix Sort
The radix sort takes a list of integers and puts each element on a smaller
list, depending on the value of its least significant byte. Then the small
lists are concatenated, and the process is repeated for each more significant
byte until the list is sorted. The radix sort is simpler to implement on
fixed-length data such as ints.

The volatile modifier is a directive to the
compiler’s optimizer that operations involving this variable should not be
optimized in certain ways. There are two special cases in which use of the
volatile modifier is desirable. The first case involves memory-mapped hardware
(a device such as a graphics adaptor that appears to the computer’s hardware as
if it were part of the computer’s memory), and the second involves shared
memory (memory used by two or more programs running simultaneously).

Most computers have a set of registers that can be accessed faster than the
computer’s main memory. A good compiler will perform a kind of optimization
called “redundant load and store removal.” The compiler looks for places in the
code where it can either remove an instruction to load data from memory because
the value is already in a register, or remove an instruction to store data to
memory because the value can stay in a register until it is changed again
anyway.

If a variable is a pointer to something other than normal memory, such as
memory-mapped ports on a
peripheral, redundant load and store optimizations might be detrimental. For
instance, here’s a piece of code that might be used to time some operation:

In this code, the variable t->value is actually a hardware counter that is
being incremented as time passes. The function adds the value of a to x 1000
times, and it returns the amount the timer was incremented by while the 1000
additions were being performed. Without the volatile modifier, a clever
optimizer might assume that the value of t does not change during the execution
of the function, because there is no statement that explicitly changes it. In
that case, there’s no need to read it from memory a second time and subtract
it, because the answer will always be 0. The compiler might therefore
“optimize” the function by making it always return 0.

If a variable points to data in shared memory, you also don’t want the compiler
to perform redundant load and store optimizations. Shared memory is normally
used to enable two programs to communicate with each other by having one
program store data in the shared portion of memory and the other program read
the same portion of memory. If the compiler optimizes away a load or store of
shared memory, communication between the two programs will be affected.

The register modifier hints to the compiler that the
variable will be heavily used and should be kept in the CPU’s registers, if
possible, so that it can be accessed faster.

There are several restrictions on the use of the register modifier.

First, the variable must be of a type that can be held in the CPU’s register.
This usually means a single value of a size less than or equal to the size of
an integer. Some machines have registers that can hold floating-point numbers
as well.

Second, because the variable might not be stored in memory, its address cannot
be taken with the unary & operator. An attempt to do so is flagged as an
error by the compiler. Some additional rules affect how useful the register modifier
is. Because the number of registers is limited, and because some registers can
hold only certain types of data (such as pointers or floating-point numbers),
the number and types of register modifiers that will actually have any effect
are dependent on what machine the
program will run on. Any additional register modifiers are silently ignored by
the compiler.

Also, in some cases, it might actually be slower to keep a variable in a
register because that register
then becomes unavailable for other purposes or because the variable isn’t used
enough to justify the overhead of loading and storing it.

So when should the register modifier be used? The answer is never, with most
modern compilers. Early C compilers did not keep any variables in registers unless
directed to do so, and the register modifier was a valuable addition to the
language. C compiler design has advanced to the point, however, where the
compiler will usually make better decisions than the programmer about which
variables should be stored in registers.

In fact, many compilers actually ignore the register modifier, which is
perfectly legal, because it is only a hint and not a directive.

Some operating systems (such
as UNIX or Windows in enhanced mode) use virtual memory. Virtual
memory is a technique for making a machine behave as if it had more memory than
it really has, by using disk space
to simulate RAM (random-access memory). In the 80386 and higher Intel CPU chips, and in
most other modern microprocessors (such as the Motorola 68030, Sparc, and Power PC), exists a
piece of hardware called the Memory Management Unit, or MMU.

The MMU treats memory as if it were composed of a series of “pages.” A page of
memory is a block of
contiguous bytes of a certain size, usually 4096 or 8192 bytes. The operating system sets up
and maintains a table for each running program called the Process Memory Map,
or PMM. This is a table of all the pages of memory that program can access and
where each is really located.

Every time your program accesses any portion of memory, the address (called a “virtual address”) is
processed by the MMU. The MMU looks in the PMM to find out where the memory is
really located (called the “physical address”). The physical address can be any
location in memory or on disk that the operating system has assigned for it. If
the location the program wants to access is on disk, the page containing it
must be read from disk into memory, and the PMM must be updated to reflect this
action (this is called a “page fault”).

Because accessing the disk is so much slower than accessing RAM, the operating
system tries to keep as much of the virtual memory as possible in RAM. If
you’re running a large enough program (or several small programs at once),
there might not be enough RAM to hold all the memory used by the programs, so
some of it must be moved out of RAM and onto disk (this action is called
“paging out”).
The operating system tries to guess which areas of memory aren’t likely to be
used for a while (usually based on how the memory has been used in the past).
If it guesses wrong, or if your programs are accessing lots of memory in lots
of places, many page faults will occur in order to read in the pages that were
paged out. Because all of RAM is being used, for each page read in to be
accessed, another page must be paged out. This can lead to more page faults,
because now a different page of memory has been moved to disk.

The problem of many page faults occurring in a short time, called “page
thrashing,” can drastically cut the performance of a system. Programs that
frequently access many widely separated locations in memory are more likely to
cause page thrashing on a system. So is running many small programs that all
continue to run even when you are not actively using them. To reduce page
thrashing, you can run fewer programs simultaneously. Or you can try changing
the way a large program works to maximize the capability of the operating
system to guess which pages won’t be needed. You can achieve this effect by
caching values or changing lookup algorithms in large data structures, or
sometimes by changing to a memory allocation library which provides an
implementation of malloc() that allocates memory more efficiently. Finally, you
might consider adding more RAM to the system to reduce the need to page out.

You can’t, really. free() can
, but there’s no way for your program to know the trick free() uses. Even if
you disassemble the library and discover the trick, there’s no guarantee the
trick won’t change with the next release of the compiler.

You can’t declare a static variable without defining
it as well (this is because the storage class modifiers
static and extern are mutually exclusive). A static variable can be defined in
a header file, but this would cause each source file that included the header
file to have its own private copy of the variable, which is probably not what
was intended.

Yes. This can be done by
using the #if, #else, and #endif preprocessor directives. For example, certain
compilers use different names for header files. One such case is between
Borland C++, which uses the header file alloc.h, and Microsoft C++, which
uses the header file malloc.h. Both of these headers serve the same purpose,
and each contains roughly the same definitions. If, however, you are writing a
program that is to support Borland C++ and Microsoft C++, you must define which
header to include at compile time. The following example shows how this can be
done:

Yes. The const modifier means
that this code cannot change the value of the variable, but that does not mean
that the value cannot be changed by means outside this code. For instance, in
the example in
FAQ 8, the timer structure was accessed through a volatile const pointer. The
function itself did not change the value of the timer, so it was declared
const. However, the value was changed by hardware on the computer, so it was
declared volatile. If a variable is both const and volatile, the two modifiers
can appear in either order.

Yes. Include files can be
nested any number of times. As long as you use precautionary measures , you can
avoid including the same file twice. In the past, nesting header files was seen
as bad programming practice, because it complicates the dependency tracking
function of the MAKE program and thus slows down compilation. Many of today’s
popular compilers make up for
this difficulty by implementing a concept called precompiled headers, in which
all headers and associated dependencies are stored in
a precompiled state.

Many programmers like to create a custom header file that has #include
statements for every header needed for each module. This is perfectly
acceptable and can help avoid potential problems relating to #include files,
such as accidentally omitting an #include file in a module.

Using the #define method of
declaring a constant enables you to declare a constant in one place and use it
throughout your program. This helps make your programs more maintainable,
because you need to maintain only the #define statement and not several instances
of individual constants throughout your program.

For instance, if your program used the value of pi (approximately 3.14159)
several times, you might want to declare a constant for pi as follows:

#define PI 3.14159

Using the #define method of declaring a constant is probably the most familiar
way of declaring constants to traditional C programmers. Besides being the most
common method of declaring constants, it also takes up the least memory.
Constants defined in this manner are simply placed directly into your source code, with no
variable space allocated in memory. Unfortunately, this is one reason why most
debuggers cannot inspect constants created using the #define method.

Unfortunately, the only way to search a linked list
is with a linear search, because the only way a linked list’s members can be
accessed is sequentially. Sometimes it is quicker to take the data from a
linked list and store it in a different data structure so that searches can be
more efficient.

This is paranoia based on
long experience. After a pointer has been freed, you can no longer use the
pointed-to data. The pointer is said to “dangle”; it doesn’t point at anything
useful. If you “NULL out” or “zero out” a pointer immediately after freeing it,
your program can no longer get in trouble by using that pointer. True, you
might go indirect on the null pointer instead, but that’s something your
debugger might be able to help you with immediately. Also, there still might be
copies of the pointer that refer
to the memory that has been deallocated; that’s the nature of C. Zeroing out
pointers after freeing them won’t solve all problems;

Comments

There are three sorting methods in this author’s “toolbox” that are all very fast and that are useful in different situations. Those methods are quick sort, merge sort, and radix sort.Uk superiorpapers com