C Function questions and Answer

Posted by Unknown at 14:14

Interview Questions

1. What is a local block?
A local block is any portion of a C program that is enclosed by the left brace ({) and the right brace (}). A C function contains left and right braces, and therefore anything between the two braces is contained in a local block. An if statement or a switch statement can also contain braces, so the portion of code between these two braces would be considered a local block.

Additionally, you might want to create your own local block without the aid of a C function or keyword construct. This is perfectly legal. Variables can be declared within local blocks, but they must be declared only at the beginning of a local block. Variables declared in this manner are visible only within the local block. Duplicate variable names declared within a local block take precedence over variables with the same name declared outside the local block. Here is an example of a program that uses local blocks:


#include <stdio.h>
void main(void);
void main()
{
/* Begin local block for function main() */
int test_var = 10;
printf("Test variable before the if statement: %d\n", test_var);
if (test_var > 5)
{
/* Begin local block for "if" statement */
int test_var = 5;
printf("Test variable within the if statement: %d\n",
test_var);
{
/* Begin independent local block (not tied to any function or keyword) */
int test_var = 0;
printf( "Test variable within the independent local block:%d\n", test_var);
}
/* End independent local block */
}
/* End local block for "if" statement */
printf("Test variable after the if statement: %d\n", test_var);
}
/* End local block for function main() */
This example program produces the following output:
Test variable before the if statement: 10
Test variable within the if statement: 5
Test variable within the independent local block: 0
Test variable after the if statement: 10


Notice that as each test_var was defined, it took precedence over the previously defined test_var. Also notice that when the if statement local block had ended, the program had reentered the scope of the originaltest_var, and its value was 10.

2. Should variables be stored in local blocks?

The use of local blocks for storing variables is unusual and therefore should be avoided, with only rare exceptions. One of these exceptions would be for debugging purposes, when you might want to declare a local instance of a global variable to test within your function. You also might want to use a local block when you want to make your program more readable in the current context.


Sometimes having the variable declared closer to where it is used makes your program more readable. However, well-written programs usually do not have to resort to declaring variables in this manner, and you should avoid using local blocks.


3. When is a switch statement better than multiple if statements?
A switch statement is generally best to use when you have more than two conditional expressions based on a single variable of numeric type. For instance, rather than the code


if (x == 1)
printf("x is equal to one.\n");
else if (x == 2)
printf("x is equal to two.\n");
else if (x == 3)
printf("x is equal to three.\n");
else
printf("x is not equal to one, two, or three.\n");
the following code is easier to read and maintain:
switch (x)
{
case 1: printf("x is equal to one.\n");
break;
case 2: printf("x is equal to two.\n");
break;
case 3: printf("x is equal to three.\n");
break;
default: printf("x is not equal to one, two, or three.\n");
break;
}


Notice that for this method to work, the conditional expression must be based on a variable of numeric type in order to use the switch statement. Also, the conditional expression must be based on a single variable. For instance, even though the following if statement contains more than two conditions, it is not a candidate for using a switch statement because it is based on string comparisons and not numeric comparisons:


char* name = "Lupto";
if (!stricmp(name, "Isaac"))
printf("Your name means 'Laughter'.\n");
else if (!stricmp(name, "Amy"))
printf("Your name means 'Beloved'.\n ");
else if (!stricmp(name, "Lloyd"))
printf("Your name means 'Mysterious'.\n ");
else
printf("I haven't a clue as to what your name means.\n");


4. Is a default case necessary in a switch statement?

No, but it is not a bad idea to put default statements in switch statements for error- or logic-checking purposes. For instance, the following switch statement is perfectly normal:
switch (char_code)
{
case 'Y':
case 'y': printf("You answered YES!\n");
break;
case 'N':
case 'n': printf("You answered NO!\n");
break;
}
Consider, however, what would happen if an unknown character code were passed to this switch statement. The program would not print anything. It would be a good idea, therefore, to insert a default case where this condition would be taken care of:
...
default: printf("Unknown response: %d\n", char_code);
break;
...
Additionally, default cases come in handy for logic checking. For instance, if your switch statement handled a fixed number of conditions and you considered any value outside those conditions to be a logic error, you could insert a default case which would flag that condition. Consider the following example:


void move_cursor(int direction)
{
switch (direction)
{
case UP: cursor_up();
break;
case DOWN: cursor_down();
break;
case LEFT: cursor_left();
break;
case RIGHT: cursor_right();
break;
default: printf("Logic error on line number %ld!!!\n", __LINE__);
break;
}
}

5. Can the last case of a switch statement skip including the break?
Even though the last case of a switch statement does not require a break statement at the end, you should addbreak statements to all cases of the switch statement, including the last case. You should do so primarily because your program has a strong chance of being maintained by someone other than you who might add cases but neglect to notice that the last case has no break statement.


This oversight would cause what would formerly be the last case statement to "fall through" to the new statements added to the bottom of the switch statement. Putting a break after each case statement would prevent this possible mishap and make your program more "bulletproof." Besides, most of today's optimizing compilers will optimize out the last break, so there will be no performance degradation if you add it.

6. Other than in a for statement, when is the comma operator used?
The comma operator is commonly used to separate variable declarations, function arguments, and expressions, as well as the elements of a for statement. Look closely at the following program, which shows some of the many ways a comma can be used:
#include <stdio.h>
#include <stdlib.h>
void main(void);
void main()
{
/* Here, the comma operator is used to separate
three variable declarations. */
int i, j, k;
/* Notice how you can use the comma operator to perform
multiple initializations on the same line. */
i = 0, j = 1, k = 2;
printf("i = %d, j = %d, k = %d\n", i, j, k);
/* Here, the comma operator is used to execute three expressions
in one line: assign k to i, increment j, and increment k.
The value that i receives is always the rightmost expression. */
i = (j++, k++);
printf("i = %d, j = %d, k = %d\n", i, j, k);
/* Here, the while statement uses the comma operator to
assign the value of i as well as test it. */
while (i = (rand() % 100), i != 50)
printf("i is %d, trying again...\n", i);
printf("\nGuess what? i is 50!\n");
}

Notice the line that reads
i = (j++, k++);
This line actually performs three actions at once. These are the three actions, in order:


1. Assigns the value of k to i. This happens because the left value (lvalue) always evaluates to the rightmost argument. In this case, it evaluates to k. Notice that it does not evaluate to k++, because k++ is a postfix incremental expression, and k is not incremented until the assignment of k to i is made. If the expression had read ++k, the value of ++k would be assigned to i because it is a prefix incremental expression, and it is incremented before the assignment is made.
2. Increments j.
3. Increments k.
Also, notice the strange-looking while statement:
while (i = (rand() % 100), i != 50)
printf("i is %d, trying again...\n");

Here, the comma operator separates two expressions, each of which is evaluated for each iteration of the whilestatement. The first expression, to the left of the comma, assigns i to a random number from 0 to 99.
The second expression, which is more commonly found in a while statement, is a conditional expression that tests to see whether i is not equal to 50. For each iteration of the while statement, i is assigned a new random number, and the value of i is checked to see that it is not 50. Eventually, i is randomly assigned the value 50, and the while statement terminates.

7. How can you tell whether a loop ended prematurely?


Generally, loops are dependent on one or more variables. Your program can check those variables outside the loop to ensure that the loop executed properly. For instance, consider the following example:
#define REQUESTED_BLOCKS 512
int x;
char* cp[REQUESTED_BLOCKS];
/* Attempt (in vain, I must add...) to
allocate 512 10KB blocks in memory. */
for (x=0; x< REQUESTED_BLOCKS; x++)
{
cp[x] = (char*) malloc(10000, 1);
if (cp[x] == (char*) NULL)
break;
}
/* If x is less than REQUESTED_BLOCKS,
the loop has ended prematurely. */
if (x < REQUESTED_BLOCKS)
printf("Bummer! My loop ended prematurely!\n");

Notice that for the loop to execute successfully, it would have had to iterate through 512 times. Immediately following the loop, this condition is tested to see whether the loop ended prematurely. If the variable x is anything less than 512, some error has occurred.

8. What is the difference between goto and long jmp( ) and setjmp()?

A goto statement implements a local jump of program execution, and the longjmp() and setjmp() functions implement a nonlocal, or far, jump of program execution. Generally, a jump in execution of any kind should be avoided because it is not considered good programming practice to use such statements as goto and longjmpin your program.

A goto statement simply bypasses code in your program and jumps to a predefined position. To use the gotostatement, you give it a labeled position to jump to. This predefined position must be within the same function. You cannot implement gotos between functions. Here is an example of a goto statement:
void bad_programmers_function(void)
{
int x;
printf("Excuse me while I count to 5000...\n");
x = 1;
while (1)
{
printf("%d\n", x);
if (x == 5000)
goto all_done;
else
x = x + 1;
}
all_done:
printf("Whew! That wasn't so bad, was it?\n");
}

This example could have been written much better, avoiding the use of a goto statement. Here is an example of an improved implementation:
void better_function(void)
{
int x;
printf("Excuse me while I count to 5000...\n");
for (x=1; x<=5000; x++)
printf("%d\n", x);
printf("Whew! That wasn't so bad, was it?\n");
}

As previously mentioned, the longjmp() and setjmp() functions implement a nonlocal goto. When your program calls setjmp(), the current state of your program is saved in a structure of type jmp_buf. Later, your program can call the longjmp() function to restore the program's state as it was when you called setjmp(). Unlike the goto statement, the longjmp() and setjmp() functions do not need to be implemented in the same function.


However, there is a major drawback to using these functions: your program, when restored to its previously saved state, will lose its references to any dynamically allocated memory between the longjmp() and thesetjmp(). This means you will waste memory for every malloc() or calloc() you have implemented between your longjmp() and setjmp(), and your program will be horribly inefficient. It is highly recommended that you avoid using functions such as longjmp() and setjmp() because they, like the goto statement, are quite often an indication of poor programming practice.

Here is an example of the longjmp() and setjmp() functions:
#include <stdio.h>
#include <setjmp.h>
jmp_buf saved_state;
void main(void);
void call_longjmp(void);
void main(void)
{
int ret_code;
printf("The current state of the program is being saved...\n");
ret_code = setjmp(saved_state);
if (ret_code == 1)
{
printf("The longjmp function has been called.\n");
printf("The program's previous state has been restored.\n");
exit(0);
}
printf("I am about to call longjmp and\n");
printf("return to the previous program state...\n");
call_longjmp();
}
void call_longjmp(void)
{
longjmp(saved_state, 1);
}

9. What is an lvalue?
An lvalue is an expression to which a value can be assigned. The lvalue expression is located on the left side of an assignment statement, whereas an rvalue is located on the right side of an assignment statement. Each assignment statement must have an lvalue and an rvalue. The lvalue expression must reference a storable variable in memory. It cannot be a constant. For instance, the following lines show a few examples of lvalues:


int x;
int* p_int;
x = 1;
*p_int = 5;
The variable x is an integer, which is a storable location in memory. Therefore, the statement x = 1 qualifies xto be an lvalue. Notice the second assignment statement, *p_int = 5. By using the * modifier to reference the area of memory that p_int points to, *p_int is qualified as an lvalue. In contrast, here are a few examples of what would not be considered lvalues:
#define CONST_VAL 10
int x;
/* example 1 */
1 = x;
/* example 2 */
CONST_VAL = 5;
In both statements, the left side of the statement evaluates to a constant value that cannot be changed because constants do not represent storable locations in memory. Therefore, these two assignment statements do not contain lvalues and will be flagged by your compiler as errors.

10. Can an array be an lvalue?

Is an array an expression to which we can assign a value? The answer to this question is no, because an array is composed of several separate array elements that cannot be treated as a whole for assignment purposes. The following statement is therefore illegal:

int x[5], y[5];
x = y;
You could, however, use a for loop to iterate through each element of the array and assign values individually, such as in this example:
int i;
int x[5];
int y[5];
for (i=0; i<5; i++)
x[i] = y[i]
Additionally, you might want to copy the whole array all at once. You can do so using a library function such as the memcpy() function, which is shown here:
memcpy(x, y, sizeof(y));

It should be noted here that unlike arrays, structures can be treated as lvalues. Thus, you can assign one structure variable to another structure variable of the same type, such as this:

typedef struct t_name
{
char last_name[25];
char first_name[15];
char middle_init[2];
} NAME;
...

NAME my_name, your_name;
...
your_name = my_name;

...

In the preceding example, the entire contents of the my_name structure were copied into the your_namestructure. This is essentially the same as the following line:
memcpy(your_name, my_name, sizeof(your_name));

11. What is an rvalue?

rvalue can be defined as an expression that can be assigned to an lvalue. The rvalue appears on the right side of an assignment statement. Unlike an lvalue, an rvalue can be a constant or an expression, as shown here:
int x, y;
x = 1; /* 1 is an rvalue; x is an lvalue */
y = (x + 1); /* (x + 1) is an rvalue; y is an lvalue */
An assignment statement must have both an lvalue and an rvalue. Therefore, the following statement would not compile because it is missing an rvalue:
int x; x = void_function_call() /* the function void_function_call() returns nothing */ 
If the function had returned an integer, it would be considered an rvalue because it evaluates into something that the lvalue, x, can store.

12. Is left-to-right or right-to-left order guaranteed for operator precedence?
The simple answer to this question is neither. The C language does not always evaluate left-to-right or right-to-left. Generally, function calls are evaluated first, followed by complex expressions and then simple expressions.

Additionally, most of today's popular C compilers often rearrange the order in which the expression is evaluated in order to get better optimized code. You therefore should always implicitly define your operator precedence by using parentheses.
For example, consider the following expression:
a = b + c/d / function_call() * 5
The way this expression is to be evaluated is totally ambiguous, and you probably will not get the results you want. Instead, try writing it by using implicit operator precedence:
a = b + (((c/d) / function_call()) * 5)

Using this method, you can be assured that your expression will be evaluated properly and that the compiler will not rearrange operators for optimization purposes.

13. What is the difference between ++var and var++?

The ++ operator is called the increment operator. When the operator is placed before the variable (++var), the variable is incremented by 1 before it is used in the expression. When the operator is placed after the variable (var++), the expression is evaluated, and then the variable is incremented by 1.

The same holds true for the decrement operator (--). When the operator is placed before the variable, you are said to have a prefix operation. When the operator is placed after the variable, you are said to have a postfix operation.
For instance, consider the following example of postfix incrementation:


int x, y;
x = 1;
y = (x++ * 5);

In this example, postfix incrementation is used, and x is not incremented until after the evaluation of the expression is done. Therefore, y evaluates to 1 times 5, or 5. After the evaluation, x is incremented to 2.

Now look at an example using prefix incrementation:


int x, y;
x = 1;
y = (++x * 5);

This example is the same as the first one, except that this example uses prefix incrementation rather than postfix. Therefore, x is incremented before the expression is evaluated, making it 2. Hence, y evaluates to 2 times 5, or 10.

14. What does the modulus operator do?
The modulus operator (%) gives the remainder of two divided numbers. For instance, consider the following portion of code:
x = 15/7
If x were an integer, the resulting value of x would be 2. However, consider what would happen if you were to apply the modulus operator to the same equation:
x = 15%7
The result of this expression would be the remainder of 15 divided by 7, or 1. This is to say that 15 divided by 7 is 2 with a remainder of 1. The modulus operator is commonly used to determine whether one number is evenly divisible into another. For instance, if you wanted to print every third letter of the alphabet, you would use the following code:
int x;
for (x=1; x<=26; x++)
if ((x%3) == 0)
printf("%c", x+64);

The preceding example would output the string "cfilorux", which represents every third letter in the alphabet.


Variables and data storage

1. Where in memory are my variables stored?
Variables can be stored in several places in memory, depending on their lifetime. Variables that are defined outside any function (whether of global or file static scope), and variables that are defined inside a function as static variables, exist for the lifetime of the program's execution. These variables are stored in the "data segment." The data segment is a fixed-size area in memory set aside for these variables. The data segment is subdivided into two parts, one for initialized variables and another for uninitialized variables.

Variables that are defined inside a function as auto variables (that are not defined with the keyword static) come into existence when the program begins executing the block of code (delimited by curly braces {}) containing them, and they cease to exist when the program leaves that block of code.

Variables that are the arguments to functions exist only during the call to that function. These variables are stored on the "stack". The stack is an area of memory that starts out small and grows automatically up to some predefined limit. In DOS and other systems without virtual memory, the limit is set either when the program is compiled or when it begins executing. In UNIX and other systems with virtual memory, the limit is set by the system, and it is usually so large that it can be ignored by the programmer.

The third and final area doesn't actually store variables but can be used to store data pointed to by variables. Pointer variables that are assigned to the result of a call to the malloc() function contain the address of a dynamically allocated area of memory. This memory is in an area called the "heap." The heap is another area that starts out small and grows, but it grows only when the programmer explicitly calls malloc() or other memory allocation functions, such as calloc(). The heap can share a memory segment with either the data segment or the stack, or it can have its own segment. It all depends on the compiler options and operating system. The heap, like the stack, has a limit on how much it can grow, and the same rules apply as to how that limit is determined.

2. Do variables need to be initialized?


No. All variables should be given a value before they are used, and a good compiler will help you find variables that are used before they are set to a value. Variables need not be initialized, however. Variables defined outside a function or defined inside a function with the static keyword are already initialized to 0 for you if you do not explicitly initialize them.

Automatic variables are variables defined inside a function or block of code without the static keyword. These variables have undefined values if you don't explicitly initialize them. If you don't initialize an automatic variable, you must make sure you assign to it before using the value.

Space on the heap allocated by calling malloc() contains undefined data as well and must be set to a known value before being used. Space allocated by calling calloc() is set to 0 for you when it is allocated.

3. What is page thrashing?

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"). Hope you're still with me, because here's the tricky part. 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.


4. What is a const pointer?


The access modifier keyword const is a promise the programmer makes to the compiler that the value of a variable will not be changed after it is initialized. The compiler will enforce that promise as best it can by not enabling the programmer to write code which modifies a variable that has been declared const.


A "const pointer," or more correctly, a "pointer to const," is a pointer which points to data that is const(constant, or unchanging). A pointer to const is declared by putting the word const at the beginning of the pointer declaration. This declares a pointer which points to data that can't be modified. The pointer itself can be modified. The following example illustrates some legal and illegal uses of a const pointer:
const char *str = "hello";
char c = *str /* legal */
str++; /* legal */
*str = 'a'; /* illegal */
str[1] = 'b'; /* illegal */



The first two statements here are legal because they do not modify the data that str points to. The next two statements are illegal because they modify the data pointed to by str.


Pointers to const are most often used in declaring function parameters. For instance, a function that counted the number of characters in a string would not need to change the contents of the string, and it might be written this way:
my_strlen(const char *str)
{
int count = 0;
while (*str++)
{
count++;
}
return count;
}



Note that non-const pointers are implicitly converted to const pointers when needed, but const pointers are not converted to non-const pointers. This means that my_strlen() could be called with either a const or a non-const character pointer.


5. When should the register modifier be used? Does it really help?


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 and 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.


In the rare event that a program is too slow, and you know that the problem is due to a variable being stored in memory, you might try adding the register modifier as a last resort, but don't be surprised if this action doesn't change the speed of the program.


6. When should the volatile modifier be used?


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:
time_t time_addition(volatile const struct timer *t, int a)
{
int n;
int x;
time_t then;
x = 0;
then = t->value;
for (n = 0; n < 1000; n++)
{
x = x + a;
}
return t->value - then;
}



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.


7. Can a variable be both const and volatile?


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, the timer structure was accessed through a volatile const pointer. The function itself did not change the value of the timer, so it was declaredconst. 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.


8. When should the const modifier be used?


There are several reasons to use const pointers. First, it allows the compiler to catch errors in which code accidentally changes the value of a variable, as in
while (*str = 0) /* programmer meant to write *str != 0 */
{
/* some code here */
str++;
}



in which the = sign is a typographical error. Without the const in the declaration of str, the program would compile but not run properly.


Another reason is efficiency. The compiler might be able to make certain optimizations to the code generated if it knows that a variable will not be changed.


Any function parameter which points to data that is not modified by the function or by any function it calls should declare the pointer a pointer to const. Function parameters that are passed by value (rather than through a pointer) can be declared const if neither the function nor any function it calls modifies the data.


In practice, however, such parameters are usually declared const only if it might be more efficient for the compiler to access the data through a pointer than by copying it.


9. How reliable are floating-point comparisons?


Floating-point numbers are the "black art" of computer programming. One reason why this is so is that there is no optimal way to represent an arbitrary number. The Institute of Electrical and Electronic Engineers (IEEE) has developed a standard for the representation of floating-point numbers, but you cannot guarantee that every machine you use will conform to the standard.


Even if your machine does conform to the standard, there are deeper issues. It can be shown mathematically that there are an infinite number of "real" numbers between any two numbers. For the computer to distinguish between two numbers, the bits that represent them must differ. To represent an infinite number of different bit patterns would take an infinite number of bits. Because the computer must represent a large range of numbers in a small number of bits (usually 32 to 64 bits), it has to make approximate representations of most numbers.


Because floating-point numbers are so tricky to deal with, it's generally bad practice to compare a floating- point number for equality with anything. Inequalities are much safer. If, for instance, you want to step through a range of numbers in small increments, you might write this:


#include <stdio.h>
const float first = 0.0;
const float last = 70.0;
const float small = 0.007;
main()
{
float f;
for (f = first; f != last && f < last + 1.0; f += small);
printf("f is now %g\n", f);
}
However, rounding errors and small differences in the representation of the variable small might cause f to never be equal to last (it might go from being just under it to being just over it). Thus, the loop would go past the value last. The inequality f < last + 1.0 has been added to prevent the program from running on for a very long time if this happens. If you run this program and the value printed for f is 71 or more, this is what has happened.


A safer way to write this loop is to use the inequality f < last to test for the loop ending, as in this example:
float f;
for (f = first; f < last; f += small);

You could even precompute the number of times the loop should be executed and use an integer to count iterations of the loop, as in this example:
float f;
int count = (last - first) / small;
for (f = first; count-- > 0; f += small)


10. How can you determine the maximum value that a numeric variable can hold?

The easiest way to find out how large or small a number that a particular type can hold is to use the values defined in the ANSI standard header file limits.h. This file contains many useful constants defining the values that can be held by various types, including these:

Value         Description 

CHAR_BIT     Number of bits in a char 

CHAR_MAX    Maximum decimal integer value of a char 

CHAR_MIN     Minimum decimal integer value of a char 
MB_LEN_MAX -   Maximum number of bytes in a multibyte character 
INT_MAX        -   Maximum decimal value of an int 
INT_MIN         -  Minimum decimal value of an int 
LONG_MAX     -   Maximum decimal value of a long 
LONG_MIN      -  Minimum decimal value of a long 


For integral types, on a machine that uses two's complement arithmetic (which is just about any machine you're likely to use), a signed type can hold numbers from -2(number of bits - 1) to +2(number of bits - 1) - 1.

An unsigned type can hold values from 0 to +2(number of bits)- 1. For instance, a 16-bit signed integer can hold numbers from -215(-32768) to +215 - 1 (32767).

11. Are there any problems with performing mathematical operations on different variable types?

C has three categories of built-in data types: pointer types, integral types, and floating-point types. Pointer types are the most restrictive in terms of the operations that can be performed on them. They are limited to
- subtraction of two pointers, valid only when both pointers point to elements in the same array. The result is the same as subtracting the integer subscripts corresponding to the two pointers.
+ addition of a pointer and an integral type. The result is a pointer that points to the element which would be selected by that integer.

Floating-point types consist of the built-in types float, double, and long double. Integral types consist of char, unsigned char, short, unsigned short, int, unsigned int, long, and unsigned long. All of these types can have the following arithmetic operations performed on them:
+ Addition
- Subtraction
* Multiplication
/ Division


Integral types also can have those four operations performed on them, as well as the following operations: %Modulo or remainder of division
<< Shift left
>> Shift right
& Bitwise AND operation
| Bitwise OR operation
^ Bitwise exclusive OR operation
! Logical negative operation
~ Bitwise "one's complement" operation

Although C permits "mixed mode" expressions (an arithmetic expression involving different types), it actually converts the types to be the same type before performing the operations (except for the case of pointer arithmetic described previously). The process of automatic type conversion is called "operator promotion."

12. What is operator promotion?

If an operation is specified with operands of two different types, they are converted to the smallest type that can hold both values. The result has the same type as the two operands wind up having. To interpret the rules, read the following table from the top down, and stop at the first rule that applies.
The following example code illustrates some cases of operator promotion. The variable f1 is set to 3/4. Because both 3 and 4 are integers, integer division is performed, and the result is the integer 0. The variable f2 is set to 3/4.0. Because 4.0 is a float, the number 3 is converted to a float as well, and the result is the float 0.75.
#include <stdio.h>
main()
{
float f1 = 3 / 4;
float f2 = 3 / 4.0;
printf("3 / 4 == %g or %g depending on the type used.\n", f1, f2);
}

13. When should a type cast be used?

There are two situations in which to use a type cast. The first use is to change the type of an operand to an arithmetic operation so that the operation will be performed properly. The variable f1 is set to the result of dividing the integer i by the integer j. The result is 0, because integer division is used. The variable f2 is set to the result of dividing i by j as well. However, the (float) type cast causes i to be converted to a float. That in turn causes floating-point division to be used and gives the result 0.75.
#include <stdio.h>
main()
{
int i = 3;
int j = 4;
float f1 = i / j;
float f2 = (float) i / j;
printf("3 / 4 == %g or %g depending on the type used.\n", f1, f2);
}


The second case is to cast pointer types to and from void * in order to interface with functions that expect or return void pointers. For example, the following line type casts the return value of the call to malloc() to be a pointer to a foo structure.
struct foo *p = (struct foo *) malloc(sizeof(struct foo));

14. When should a type cast not be used?
A type cast should not be used to override a const or volatile declaration. Overriding these type modifiers can cause the program to fail to run correctly.
A type cast should not be used to turn a pointer to one type of structure or data type into another. In the rare events in which this action is beneficial, using a union to hold the values makes the programmer's intentions clearer.


15. Is it acceptable to declare/define a variable in a C header?

A global variable that must be accessed from more than one file can and should be declared in a header file. In addition, such a variable must be defined in one source file. Variables should not be defined in header files, because the header file can be included in multiple source files, which would cause multiple definitions of the variable.

The ANSI C standard will allow multiple external definitions, provided that there is only one initialization. But because there's really no advantage to using this feature, it's probably best to avoid it and maintain a higher level of portability.

"Global" variables that do not have to be accessed from more than one file should be declared static and should not appear in a header file.

16. What is the difference between declaring a variable and defining a variable?

Declaring a variable means describing its type to the compiler but not allocating any space for it. Defining a variable means declaring it and also allocating space to hold the variable. You can also initialize a variable at the time it is defined. Here is a declaration of a variable and a structure, and two variable definitions, one with initialization:
extern int decl1; /* this is a declaration */
struct decl2 
{
int member;
}; /* this just declares the type--no variable mentioned */
int def1 = 8; /* this is a definition */
int def2; /* this is a definition */

To put it another way, a declaration says to the compiler, "Somewhere in my program will be a variable with this name, and this is what type it is." A definition says, "Right here is this variable with this name and this type."

A variable can be declared many times, but it must be defined exactly once. For this reason, definitions do not belong in header files, where they might get #included into more than one place in your program.

17. Can static variables be declared in a header file?


You can't declare a static variable without defining it as well (this is because the storage class modifiersstatic 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.

18. What is the benefit of using const for declaring constants?

The benefit of using the const keyword is that the compiler might be able to make optimizations based on the knowledge that the value of the variable will not change. In addition, the compiler will try to ensure that the values won't be changed inadvertently.
Of course, the same benefits apply to #defined constants. The reason to use const rather than #define to define a constant is that a const variable can be of any type (such as a struct, which can't be represented by a #defined constant). Also, because a const variable is a real variable, it has an address that can be used, if needed, and it resides in only one place in memory.

Bits and bytes


1. What is the most efficient way to store flag values?
A flag is a value used to make a decision between two or more options in the execution of a program. For instance, the /w flag on the MS-DOS dir command causes the command to display filenames in several columns across the screen instead of displaying them one per line. In which a flag is used to indicate which of two possible types is held in a union. Because a flag has a small number of values (often only two), it is tempting to save memory space by not storing each flag in its own int or char.
Efficiency in this case is a tradeoff between size and speed. The most memory-space efficient way to store a flag value is as single bits or groups of bits just large enough to hold all the possible values. This is because most computers cannot address individual bits in memory, so the bit or bits of interest must be extracted from the bytes that contain it.
The most time-efficient way to store flag values is to keep each in its own integer variable. Unfortunately, this method can waste up to 31 bits of a 32-bit variable, which can lead to very inefficient use of memory. If there are only a few flags, it doesn't matter how they are stored. If there are many flags, it might be advantageous to store them packed in an array of characters or integers. They must then be extracted by a process called bit masking, in which unwanted bits are removed from the ones of interest.
Sometimes it is possible to combine a flag with another value to save space. It might be possible to use high- order bits of integers that have values smaller than what an integer can hold. Another possibility is that some data is always a multiple of 2 or 4, so the low-order bits can be used to store a flag.


2. What is meant by "bit masking"?
Bit masking means selecting only certain bits from byte(s) that might have many bits set. To examine some bits of a byte, the byte is bitwise "ANDed" with a mask that is a number consisting of only those bits of interest. For instance, to look at the one's digit (rightmost digit) of the variable flags, you bitwise AND it with a mask of one (the bitwise AND operator in C is &):
flags & 1;
To set the bits of interest, the number is bitwise "ORed" with the bit mask (the bitwise OR operator in C is |). For instance, you could set the one's digit of flags like so:
flags = flags | 1;
Or, equivalently, you could set it like this:
flags |= 1;
To clear the bits of interest, the number is bitwise ANDed with the one's complement of the bit mask. The "one's complement" of a number is the number with all its one bits changed to zeros and all its zero bits changed to ones. The one's complement operator in C is ~. For instance, you could clear the one's digit of flags like so:

flags = flags & ~1;
Or, equivalently, you could clear it like this:
flags &= ~1;
Sometimes it is easier to use macros to manipulate flag values.
Example Program : Macros that make manipulating flags easier.
/* Bit Masking */
/* Bit masking can be used to switch a character
between lowercase and uppercase */
#define BIT_POS(N) ( 1U << (N) )
#define SET_FLAG(N, F) ( (N) |= (F) )
#define CLR_FLAG(N, F) ( (N) &= -(F) )
#define TST_FLAG(N, F) ( (N) & (F) )
#define BIT_RANGE(N, M) ( BIT_POS((M)+1 - (N))-1 << (N) )
#define BIT_SHIFTL(B, N) ( (unsigned)(B) << (N) )
#define BIT_SHIFTR(B, N) ( (unsigned)(B) >> (N) )
#define SET_MFLAG(N, F, V) ( CLR_FLAG(N, F), SET_FLAG(N, V) )
#define CLR_MFLAG(N, F) ( (N) &= ~(F) )
#define GET_MFLAG(N, F) ( (N) & (F) )
#include <stdio.h>
void main()
{
unsigned char ascii_char = 'A'; /* char = 8 bits only */
int test_nbr = 10;
printf("Starting character = %c\n", ascii_char);
/* The 5th bit position determines if the character is
uppercase or lowercase.
5th bit = 0 - Uppercase
5th bit = 1 - Lowercase */
printf("\nTurn 5th bit on = %c\n", SET_FLAG(ascii_char, BIT_POS(5)) );
printf("Turn 5th bit off = %c\n\n", CLR_FLAG(ascii_char, BIT_POS(5)) );
printf("Look at shifting bits\n");
printf("=====================\n");
printf("Current value = %d\n", test_nbr);
printf("Shifting one position left = %d\n",
test_nbr = BIT_SHIFTL(test_nbr, 1) );
printf("Shifting two positions right = %d\n",
BIT_SHIFTR(test_nbr, 2) );
}

BIT_POS(N) takes an integer N and returns a bit mask corresponding to that single bit position (BIT_POS(0)returns a bit mask for the one's digit, BIT_POS(1) returns a bit mask for the two's digit, and so on). So instead of writing

#define A_FLAG 4096
#define B_FLAG 8192
you can write
#define A_FLAG BIT_POS(12)
#define B_FLAG BIT_POS(13)

which is less prone to errors.


The SET_FLAG(N, F) macro sets the bit at position F of variable N. Its opposite is CLR_FLAG(N, F), which clears the bit at position F of variable N. Finally, TST_FLAG(N, F) can be used to test the value of the bit at position F of variable N, as in
if (TST_FLAG(flags, A_FLAG))
/* do something */;

The macro BIT_RANGE(N, M) produces a bit mask corresponding to bit positions N through M, inclusive. With this macro, instead of writing
#define FIRST_OCTAL_DIGIT 7 /* 111 */
#define SECOND_OCTAL_DIGIT 56 /* 111000 */


you can write
#define FIRST_OCTAL_DIGIT BIT_RANGE(0, 2) /* 111 */
#define SECOND_OCTAL_DIGIT BIT_RANGE(3, 5) /* 111000 */

which more clearly indicates which bits are meant.
The macro BIT_SHIFT(B, N) can be used to shift value B into the proper bit range (starting with bit N). For instance, if you had a flag called C that could take on one of five possible colors, the colors might be defined like this:
#define C_FLAG BIT_RANGE(8, 10) /* 11100000000 */
/* here are all the values the C flag can take on */
#define C_BLACK BIT_SHIFTL(0, 8) /* 00000000000 */
#define C_RED BIT_SHIFTL(1, 8) /* 00100000000 */
#define C_GREEN BIT_SHIFTL(2, 8) /* 01000000000 */
#define C_BLUE BIT_SHIFTL(3, 8) /* 01100000000 */
#define C_WHITE BIT_SHIFTL(4, 8) /* 10000000000 */
#define C_ZERO C_BLACK
#define C_LARGEST C_WHITE
/* A truly paranoid programmer might do this */
#if C_LARGEST > C_FLAG
Cause an error message. The flag C_FLAG is not
big enough to hold all its possible values.
#endif /* C_LARGEST > C_FLAG */

The macro SET_MFLAG(N, F, V) sets flag F in variable N to the value V. The macro CLR_MFLAG(N, F) is identical to CLR_FLAG(N, F), except the name is changed so that all the operations on multibit flags have a similar naming convention. The macro GET_MFLAG(N, F) gets the value of flag F in variable N, so it can be tested, as in
if (GET_MFLAG(flags, C_FLAG) == C_BLUE)
/* do something */;

3. Are bit fields portable?

Bit fields are not portable. Because bit fields cannot span machine words, and because the number of bits in a machine word is different on different machines, a particular program using bit fields might not even compile on a particular machine.

Assuming that your program does compile, the order in which bits are assigned to bit fields is not defined. Therefore, different compilers, or even different versions of the same compiler, could produce code that would not work properly on data generated by compiled older code. Stay away from using bit fields, except in cases in which the machine can directly address bits in memory and the compiler can generate code to take advantage of it and the increase in speed to be gained would be essential to the operation of the program.


4. Is it better to bitshift a value than to multiply by 2?

Any decent optimizing compiler will generate the same code no matter which way you write it. Use whichever form is more readable in the context in which it appears. The following program's assembler code can be viewed with a tool such as CODEVIEW on DOS/Windows or the disassembler (usually called "dis") on UNIX machines:

Example: Multiplying by 2 and shifting left by 1 are often the same.
void main()
{
unsigned int test_nbr = 300;
test_nbr *= 2;
test_nbr = 300;
test_nbr <<= 1;
}

5. What is meant by high-order and low-order bytes?

We generally write numbers from left to right, with the most significant digit first. To understand what is meant by the "significance" of a digit, think of how much happier you would be if the first digit of your paycheck was increased by one compared to the last digit being increased by one.

The bits in a byte of computer memory can be considered digits of a number written in base 2. That means the least significant bit represents one, the next bit represents 2´1, or 2, the next bit represents 2´2´1, or 4, and so on. If you consider two bytes of memory as representing a single 16-bit number, one byte will hold the least significant 8 bits, and the other will hold the most significant 8 bits. Figure shows the bits arranged into two bytes. The byte holding the least significant 8 bits is called the least significant byte, or low-order byte. The byte containing the most significant 8 bits is the most significant byte, or high- order byte.

6. How are 16- and 32-bit numbers stored?

A 16-bit number takes two bytes of storage, a most significant byte and a least significant byte. If you write the 16-bit number on paper, you would start with the most significant byte and end with the least significant byte. There is no convention for which order to store them in memory, however.

Let's call the most significant byte M and the least significant byte L. There are two possible ways to store these bytes in memory. You could store M first, followed by L, or L first, followed by M. Storing byte M first in memory is called "forward" or "big-endian" byte ordering. The term big endian comes from the fact that the "big end" of the number comes first, and it is also a reference to the book Gulliver's Travels, in which the term refers to people who eat their boiled eggs with the big end on top.

Storing byte L first is called "reverse" or "little-endian" byte ordering. Most machines store data in a big- endian format. Intel CPUs store data in a little-endian format, however, which can be confusing when someone is trying to connect an Intel microprocessor-based machine to anything else.

A 32-bit number takes four bytes of storage. Let's call them Mm, Ml, Lm, and Ll in decreasing order of significance. There are 4! (4 factorial, or 24) different ways in which these bytes can be ordered. Over the years, computer designers have used just about all 24 ways. The most popular two ways in use today, however, are (Mm, Ml, Lm, Ll), which is big-endian, and (Ll, Lm, Ml, Mm), which is little-endian. As with 16-bit numbers, most machines store 32-bit numbers in a big-endian format, but Intel machines store 32-bit numbers in a little-endian format.







click the Below link download this file 


If you enjoyed this post and wish to be informed whenever a new post is published, then make sure you subscribe to my regular Email Updates. Subscribe Now!


Kindly Bookmark and Share it:

YOUR ADSENSE CODE GOES HERE

0 comments:

Have any question? Feel Free To Post Below:

 

© 2011. All Rights Reserved | Interview Questions | Template by Blogger Widgets

Home | About | Top