LINKING BETWEEN C AND C++
Suppose we have a library of functions that have been compiled into object code and stored in the library code form. When we call a function from such a library in our program, the compiler with mark the name of the function as unresolved symbol in the object code of our program. To create an executable program, we have to use a linker and make sure that the linker searches the right library for the code of that function. If the linker finds the function’s object code in the library, it will combine that code with our program’s code to create an executable file.
Type-Safe linkage:
The key to linking C++ programs with functions lies in understanding how C++ resolves the names of functions. As we know, C++ lets us overload a function name, where by we can declare the same function with different sets of arguments. To help the linker, the C++ compiler uses an encoding scheme that creates a unique name for each overloaded function. The general idea of the encoding scheme is that is creates a unique name for each overloaded function. The general idea of the encoding algorithm is to combine the following component:
- The name of the function
- The name of the class in which the function is defined
- The list of argument types by the function.
To generate a unique signature for each function., we do not have to know the exact detail of the encoding algorithm, because it differs from one compiler to another. But knowing that a unique signature is generated for each function in a class should help us understand how the linker can figure out which of the many different version of a function to call.
Effect of function name encoding
How does function name encoding affect C++ program?
To see benefits of name encoding consider the following examples
#includevoid print (unsigned short x)
{
printf (“x=%u”, x);
}
void main (void)
{
short x= -1;
print (x);
}
The print function expects and unsigned short integer arguments, but main calls print with a signed short integer argument. You can see the result of this type mismatch from the output of the program:
X=65535
Even though print was called with a-1, the printed value is 65535. The explanation for this result is as follows: This program was run on a system that uses a 16-bit representation for short integers. The bit representation of the value –1 happens to be oxffff (all 16 bits are one) which when treated as an unsigned quantity, result in the value 65535
HOW TO MAKE A LIBRARY
While designing a class library, one has to decide the inheritance hierarchy of the classes; the way one class uses the facilities of others, a kind of operation that the classes will support.
ORGANIZING C++ CLASSES
If there were standard class library for C++, it would be easy to decide how to organize C++ classes. One can model the classes after the standard ones. Unfortunately, C++ does not have any classes yet. The (iostream) class library, which is part of AT&T’s C++ 2.0, may be the only standard class library that one can currently expect to find. But one needs much more than ’iostream’ class to develop complete application in C++. Additionally, even if there were many more standard classes for C++, one would still have to write classes that were customized for a particular application. From above discussion we can see that, sooner or later, you would face the task of the organization of a library of classes that will form the basis of application.
Inheritance hierarchy under single inheritance:
There two distinct ways two organize the inheritance tree of classes under single inheritance:
- A single class hierarchy in which all classes in the library are derived form a single root classes. The Smalltalk–80 programming language provides a class hierarchy of this type. Therefore, the organization of C++ classes is known as the ‘Smalltalk model’ or a ‘single tree model’. There the term root class refers to base class that is not drived from any other class.
- Multiple disjoint class hierarchies with more than one roote class. This has been referred to as the forest model, because there are multiple trees of class hierarchies in the library.
SINGLE CLASS HIERARCHY
As figure 1 shows, a library uses a single class hierarchy starting with a base class, usually named ‘object’, which declares a host of virtual function that apply to all other classes in the library. Each derived class defines precise versions of these functions, thus providing a standard interface to the library. Advantages of using single class hierarchy are given below:
- By defining a single root class, you can ensure that all classes provide the same standard set of public interface functions. This enhances the consistency & compatibility among classes.
- With this organization, it is easier to provide a capability such as ‘persistence’, which is the ability to store collections of objects in a disk file and retrieve them later. This is a consequence of having a single base class from which all classes are guaranteed to inherit. The single class hierarchy also makes it easy to provide a standard exception-handling mechanism.
- Because every class in the library is an ‘object’, it is easy to define polymorphic data structure. For example, the following array of pointers:
Object *Array of objptr
Looking at the other sides of the coin, many programmers find following disadvantages with a monolithic class hierarchy:
- The complier cannot provide strict type-checking, because many objects in the library are of type object, which can point to an instance of any class in the library. Such container class that is capable of storing instance of any class in the library.
- The root base class, object, typically includes a large number of virtual functions representing the union of all the virtual function implemented by the derived classes. This makes it burdensome to create derived class, because you have to provide the definition of all the virtual functions, even though many may not have any relevance to that derived class.
- Because of the large monolithic class hierarchy, compiling a single program may require processing a large number of header files. This can be a problem on MS-DOS systems that typically have a limited amount of memory.
Multiple class hierarchies:
In contrast to the monolithic, single class hierarchy of the Smalltalk80. Library, a C++ class library based on the forest model includes multiple class tree, with each tree providing a well – defined functionality. For instance figure 2 shows a number of class hierarchies in a C++ class library that provides the objects needed to build window-based user interface. Different types of windows are grouped together in the class hierarchy, with the “windows” class as the root. Each window has rectangle represented by the “rectangle” class, which, in turn, uses the “point” class to represent the corners of a window. The standalone ‘string’ class represents text strings that might appear in windows. One can model the physical display by a class hierarchy with a generic ‘display’ as the base class. From this generic display, we can further specialize to text or graphics display.
Here are the main advantages of the forest model of class library:
- Each class hierarchy is small enough so that you can understand and use the function easily.
- Virtual function declared in each root class is relevant to that class tree.
Thus, the primary disadvantages of the forest model are these:
- Because there is no common base class, it is difficult to design container class such as linked, stacks and queues that can store any type of object. One have to devise your own schemes for creating such data structure.
- Anything that requires a library-wide discipline becomes difficult to implement. For example, exception handing and persistence are harder to support under the forest than under a singletree model
Effects of multiple inheritances
The introduction of multiple inheritances in AT&T C++ release 2.0 changed the implication of organizing C++ class libraries according to one of the models just described. Because multiple inheritance allows a derived class to inherit from more than one base class, now one can combine and customize the capabilities of class in unique ways. The linked list is constructed by linking instance of a new class named ‘slink-string’, which is defined from the ‘string’ class and ‘single-link’ class as follows:
Class slink-string: public single-link, public string{
};
The ‘single link’ class is capable of holding a pointer to another instance of ‘single-link’. Here, multiple inheritances allow you to combine the capabilities of the two classes into a single class. Following are some ways of applying multiple inheritances to extend the capabilities of C++ libraries.
- You can derive a class from two or more classes in a library. You can do this even if the class library follows a singletree model of inheritance hierarchy. You can combine these two classes with multiple inheritance to define a ‘string ’ with a ‘link’.
- Even with a multiple-class hierarchy, you can add a standard capability such as persistence by defining a new base class and deriving other classes from it. You have to do extra work to create a whole new set of derived classes that incorporates the capability with multiple inheritances. With single inheritance, one lacks the opportunity to combine the behavior of two or more classes packages in a library.
CLIENT-SERVER RELATIONSHIP AMONG CLASSES
In additional to the inheritance relationship among classes, a class may also use the facilities of another class. This is referred to as the ‘client-server relationship’ among classes, because the client class calls the member functions of the server class to use its capabilities. In C++, the client class needs an instance of the server. The client class can get this instance in one of the following ways:
- One of the member functions of the client receives an instance of the server class as an argument.
- A member function of the client class calls a function that returns an instance of the server class.
- An instance of the server class is available as a global variable.
- The client class incorporates an instance of the server class as a member variable.
- A member variable of the client class is a pointer to an instance of the server class.
Of these, the last two cases are of interest because they constitute the most common ways of expressing a client-server relationship b/w incorporating an instance or a pointer to an instance. This is referred to as ‘composition’.
PUBLIC INTERFACE TO C++ CLASSES
The term public interface refers to the public member functions through which you access the capabilities of a class. The public interface to the classes in a library is as important as the relationship among the classes. Just as there is no standard C++ class library, there is also no standard interface to C++ classes. However, if one is designing a class library, it is good practice to provide a minimal set of member functions. Some member functions are needed to ensure proper operations, others to provide a standard interface to the library.
Default and copy constructors:
Each class in the library should have a default constructor that is a constructor that takes no argument. The default constructor should initialize any data members that the class contains. For example, here is the default constructor for the rectangle-shape class:
class rectangle_shape: public shape{
public:
rectangle_shape (): p1 (0), p2 (0){} /*This method of initialization is called initialization list.
//….
private:
point * p1, *_p2; };
This constructor sets the data members _p1 and _p2 to zero.
The default constructor is important because it is called when you are allocating arrays of class instances or when a class instance is defined without any initial value. Thus, the rectangle_shape: rectangle_shape () constructor is called to initialize the rectangle shape objects in the following:
rectangle_shape rect [16];rectangle_shape r;
Each class should also include a copy constructor of the form x (const x &) where x is the name of the class. The copy constructor is called in the following cases:
- Whenever an object is initialized with another of the same type, such as rectangle shape r2=r;
- When an object is passed by value to a function.
- When an object is returned by value from a function.
Destructors:
Defining a destructor is important for classes that include pointers as member variables. The destructor should reverse the effects of the constructor. Thus if the constructor allocates memory by calling ‘new’, the destructor should reallocate the memory by using the ‘delete ’ operator.
Assignment operator:
One should define the assignment operator for each class, because derived classes do not inherit the assignment operator from the base class. As, if we do not define the assignment operator, the C++ compiler provides a default one that simply copies each member of one class instance to the corresponding member of another.
Input and Output Functions
Each class should also define two functions for input-output. Output function prints a formatted text representation of each member of an object on an output stream.
These functions enable you to define the operators so that they can accept as arguments, instances of any class in the library. For examples, you might use the names printout and read-in for the input and output functions , respectively. Each of these functions should take a single argument i.e. the reference to the stream on which the input-output operation occurs. Then, for a class x, you would define the output operator <
# includeostream& operator<< (ostream& os, const x& X)
{
X.print_out (os);
return os;
}
FUNCTIONS
A function groups a number of program statements into a unit and gives it a name. This unit can then be invoked from other parts of program. Division of program into functions is known as MODULAR APPROACH. Another reason to use function is to reduce program files. Any sequence of code that comes very frequently in the program is a candidate of being made into a function. Use of function helps in saving memory space because function code is stored at only one place memory, even though the function is executed many times in the course of the program.
Function in C++ is similar to procedures and subroutines in other programming languages. Fig. 1 shows how same code of function is used for all calls to function
First example demonstrates a simple function, which is used to print a line of 45 asterisks.
This example program generates a table, and lines of asterisks are used to make the table more readable.
# includevoid startline ();
void main() /*function declaration */
{
starline (); /*call to function*/
cout << “data type range” <
starline ();
cout<< “char -128 to 127 0 ” <
<<”short -32,768 to 32,767” <
<<”int system dependent” <
<<”long -2,147,483,648, to 2,147,483,647” <
starline (); /*call to function*/
}
void startline () /*function declaration*/
{
for (int j=0.j<45;j++)
{
cout<<’+’,
}
cout<
}
In the above example we can see how to add a function other than “main ()” to the program. We have to use three components: the function “declaration”, the ‘call’ to the function, and the function “definition”
Function declaration
Just as you can’t use a variable without first telling the compiler what it is; you also can’t use a function without telling the compiler about it. There are two ways to do this. The approach we show here is to declare the function before the its called. (as shown in the previous example). In other approach we write the function body above the ‘main ()’ function .The declaration tells the compiler that at some later point we plan to present a function called ‘starline’.
void starline ();
The keyword ’void’ specifies that the function has no return value, and the empty parentheses indicate that it takes no arguments. Function declarations are also called prototypes, since they provide a modes or blueprint for the function.
Calling the function:
The function is “called” (or invoked) from the main () program by simply writing the name of function, followed by parentheses.
starline ();
The syntax for calling the function is very similar to that of the declaration, except that the return type is not used. A semicolon executing the call statement causes the function to execute terminate the call. Then, control is transferred to the function, the statements in the function definition are executed, and then control returns to the statement following the function call.
Function Definition: -
The “definition” contains the actual code for the function. Here’s the definition for starline( ):
void starline ( ){
for (int j=0; j<45; j++)
cout << ‘*’
cout << endl;
}
The definition consists of a line called declarator, followed by the function body. The function body is delimited by the braces.
The declarator must agree with the declaration: It must use the same function name, have the same argument types in the same order (if any arguments) and have the same return type. The only difference between declarator and declaration is that the declarator is not terminated by the semicolon.
PASSING ARGUMENTS TO FUNCTIONS
An ‘argument’ is a piece of data, passed from a program to the function to operate with different values, or even to do different things, depending on the requirements of the program calling.
Passing Constants: Let us suppose we decide that the starline( ) function in the last example is too rigid. Instead of a function that always print 45 asterisks, we want a function that will print any character any number of times.
# includevoid main( )
{
repchar (‘-‘, 43);
cout<< “Data type Range”<< endl;
repchar(‘=’, 23);
cout << “ char -128 to 127” << endl
<< “ Short -32768 to 32767” <
<< “ int system dependent” << endl
<< “ long -2,147, 483, 648 to 2,147, 483, 647”;
repchar (’-‘, 43);
}
void repchar (char ch, int n)
{
for ( int j=0; j
cout << ch;
cout << endl;
}
The new function is called repchar ( ). Its declaration looks like this:
void repchar (char, int); /* declaration specifies data types */
The items in the parentheses are the data types of the arguments that will be sent to repchar( ): char and int. In a function call, specific values – constants in this case, are inserted in the appropriate place in the parentheses.
repchar(‘-‘, 43); /* function call specifies actual values */
The declarator in the function definition specifies both the data types and the names of the parameters:
void repchar (char ch, int n) /* declarator specifies parameter names and data types */
The parameters names ‘ch’ and ‘n’, are used in the function as if they were normal variables. When the function is called, its parameters are automatically initialized to the values passed by the calling program.
Passing Variables: Variables, instead of constants, may be passed as arguments. This program, incorporates the same ‘repchar( )’ function, but lets the user specify the character and the number of times it should be repeated.
# includevoid repchar ( char, int); /* function declaration.*/
void main ( )
{
char chin;
int nin;
cout << “Enter a character:”;
cin >> chin;
cout << “Enter no. of times to repeat it:”;
cin >> nin;
repchar (chin, nin);
}
void repchar (char ch, int n)
{
for (int j=0; j
cout << ch;
}
Passing by value: -
In the above example, the particular values possessed by chin and nin when the function call is executed will be passed to the function. As it did when constants were passed to it, the function creates new variables to hold the values of these variable arguments. The function gives these new variables the names and data types of the parameters specified in the declarator: ch of type char and n of type int. Passing arguments in this way, where the function creates copies of the arguments passed to it, is called passing by value:
RETURNING VALUES FROM FUNCTIONS
When a function completes its execution, it can return a single value to the calling program. Usually, this return value consists of an answer to the problem the function has solved. The next example demonstrates a function that returns a weight in kilogram after being given a weight in pound.
# includefloat lbstokg (float); // declaration
void main ( )
{
float lbs, kgs;
cout << “In enter your weight in pounds:”;
cin >> lbs;
kgs = lbstokg (lbs);
cout <<”Your weight in kilogram is” <<
}
float lbstokg (float pounds)
{
float kilograms = 0.453592 * pounds;
return kilograms;
}
When a function returns a value, the data type of this value must be specified. The function declaration does this by placing the data type, float in this case, before the function name in the declarator and the definition
float lbstokg (float);
The first float specifies the return type. The float in parentheses specifies that an argument to be passed to lbstokg( ) is also of type float.
While many arguments may be sent to a function, only one value can be returned from it.
REFERENCE ARGUMENTS
When arguments are passed by value, the called function creates a new variable of the same type as the argument and copies the argument’s value into it. As we noted, the function cannot access the original variable in the calling program, only the copy it created. Passing arguments by value is useful when the function does not need to modify the original variable in the calling program.
Passing arguments by reference uses a different mechanism instead of a value being passed to the function, a ‘reference’ to the original variable, in the calling program, is passed. The function can access the actual variables in the calling program. Among other benefits, this provides a mechanism for passing more than one value from the function back to the calling program.
The next example shows a simple variable passed by reference.
# includevoid main ( )
{
void intfrac (float, float, float);
float number, intpart, fracpart; /*global variables */
do {
cout <<”In Enter a real number;”;
cin >> number;
intfrac (number, intpart, fracpart);
cout <<”Integer part is” << intpart
<<”,fraction part is” << fractpart<
} while (number ! = 0.0); /* exit loop on 0.0 */
}
void intfrac (float n, float intp, float fracp)
{
long temp = static_cast (n); /*convert to long*/
intp = static_cast (temp); /*back to float*/
fracp = n-intp;
}
The function declaration echoes the usage of the ampersand in the definition:
void intfrac (float, float &, float &); /* ampersands */
As in the definition, the ampersand follows those arguments that are passed by reference.
The ampersand is not used in the function call
inlfrac (number, intpart, fracpart); /* no ampersands */
While ‘intpart’ and ‘fracpart’ are passed by reference, the variable ‘number’ is passed by value. ‘intp’ and ‘intpart’ are different names for the same place in memory, as are ‘fracp’ and ‘fracpart’. On the other hand, since it is passed by value, the parameter ‘n’ in ‘intfrac( )’ is a separate variable into which the value of ‘number’ is copied. It can be passed by value because the ‘intfrac( )’ function does not need to modify ‘number’.
OVERLOADED FUNCTIONS
An overloaded function appears to perform different activities depending on the kind of data send to it. We can overload functions only when there is difference in their fields: number of arguments and type of arguments. It would be far more convenient to use the same name for all three functions, even though they each have different arguments. Here’s an example, that makes this possible:
# includevoid repchar ( );
void repchar (char);
void repchar (char, int);
void main( )
{
repchar ( );
repchar (‘=’);
repchar (‘+’, 30);
}
void repchar ( )
{
for (int j=0; j<45; j++) /* always loops 45 times */
cout << “*”; /* always prints asterisks */
cout << endl;
}
void repchar (char ch)
{
for (int j=0; j<45; j++) /* always loops 45 times */
cout << ch; /* prints specified character */
cout << endl;
}
void repchar (char ch, int n)
{
for (int j=0; j
cout << ch; /* prints specified character */
cout << endl1;
}
The program contains three functions with the same name. There are three declarations, three function calls, and three function definitions. What keeps the compiler from becoming hopelessly confused? It uses the number of arguments, and their data types, to distinguish one function from another. In other words, the declaration.
void repchar ( );
which takes no arguments, describes an entirely different function than the declaration :
void repchar (char);
Which takes one argument of type ‘char’, or the declaration
void repchar (char, int);
INLINE FUNCTIONS
One of the objectives of using functions in a program is to save some memory space, which becomes appreciable when a function is likely to be called many times. However, every time a function is called, it takes a lot of extra time in executing a series of instructions for tasks such as jumping to the function, saving registers, pushing arguments into the stack and returning to the calling function. When a function is small, a substantial percentage of execution time may be spent in such over-heads. One solution to this problem is to use macro definitions, popularly known as ‘macros’. Preprocessor macros are popular in C. The major drawback with macros is that they are not really functions and therefore, the usual error checking does not occur during compilation.
C++ has a different solution to this problem to eliminate the cost of calls to small functions. C++ proposes a new feature called ‘inline functions’. Inline function is a function that is expanded inline when it is invoked. That is, the compiles replaces the function call with the corresponding function code. Inline functions are defines as follows.
We should exercise care before making a function ‘inline’. The speed benefits of ‘inline’ functions diminish as the function grows in size. Usually, the functions are made inline when they are small enough to be defined in one or two lines.
Remember that the ‘inline’ keyword merely sends a ‘request’, not a command, to the compiler. The compiler may ignore this request if the function definition is too long or too complicated and compile the function as a normal function.
# includeinline float mul (float x, float y) /* inline function*/
{
return (x*y);
}
inline double div (double p, double q) /* inline function */
{
return (p/q);
}
void main( )
{
float a = 12.345;
float b = 9.82;
cout <<< “\n”;
cout <
<< “\n”;}
DEFAULT ARGUMENTS:
C++ allows us to call a function without specifying all its arguments. In such cases, the function assigns a default value to the parameter, which does not have a matching argument in the function call. Default values are specified when the function is declared. Here is an example of a prototype (i.e. function declaration with default values).
float amount (float principal, int period, float rate = 0.15)
The default value is specified in a manner syntactically similar to a variable initialization. The above prototype declares a default value of 0.15 to the argument ‘rate’. A subsequent function call like
value = amount (5000, 7) ; /* one argument missing */
passes the value of 5000 to ‘principal’ and 7 to ‘period’ and then lets the functions use default value of 0.15 for ‘rate’. The call
value = amount (5000, 5, 0.12) ; /* no missing argument *
passes an explicit value of 0.12 to ‘rate’.
A default argument is checked for type at the time of declaration and evaluated at the time of call. One important point to note is that only the trailing arguments can have default value. That is, we must add defaults from ‘right to left’. We cannot provide a default value to a particular argument in the middle of a argument list. Default arguments are useful in situations where some arguments always have the same value.
‘CONST’ FUNCTION ARGUMENTS: -
We have seen that passing an argument by reference can be used to allow a function to modify a variable in the calling program. However, there are other reasons to pass by reference One is efficiency. Some variables used for function arguments can be very large; a large structure would be an example. If an argument is large, then passing by reference is more efficient because, behind the scenes, only an address is really passed, not the entire variable.
Suppose, you want to pass an argument by reference for efficiency, but not only do you want the function not to modify it, you want a guarantee that the function cannot modify it.
To obtain such a guarantee, you can apply the constant modifier to the variable in the function declaration. The example given below shows how this looks like.
#includevoid afunc (int &a, const int &b); // declaration
void main ( )
{
int alpha = 7;
int beta = 11;
afunc (alpha, beta);
}
void afunc (int &a, const int &b) ;// definition
{
a = 107; /* OK */
b = 111; /* error : cannot modify constant argument */
}.
Defining Macros:
By defining a macro, one can define a symbol (a token) to be equal to some C++ code and use that symbol wherever the code is required in program. When the source file is preprocessed, every occurrence of a macro’s name is replaced with its definition. A common use of this feature is to define a symbolic name for a numerical constant and use the symbol instead of the numbers in your program. This improves the readability of the source code, because with a descriptive name, you are not left guessing why a particular number is being used in the program. You can define such macros in a straightforward manner using the ‘# define’ directives as follows.
# define PI 3.14159# define BUFSIZE 512
Once these symbols are defined, you can use PI and BUFSIZE instead of the numerical constants through out the source file.
The capability of macros, however go well beyond replacing a symbol for a constant. Macros can accept parameters and replace each occurrence of a parameter with the value provided for it when the macro is used in the program. Thus, the code resulting from the expansion of a macro can change depending on the parameter you provide. When using the macro.
For example, the following macro accepts a parameter and expands to an expression designed to calculate the square of the parameter:
# define square(x) ((x)*(x))
If you use square(z) in your program, it becomes ((z)*(z)) after the source of file is processed. This macro is essentially equivalent to a function that computes the square of its arguments. You do not, however, have the overhead of calling a function, because the expression generated by the macro is placed directly in the source file.
New feature provided by ANSI C preprocessor is the ‘stringizing’ operator, which makes a string out of any parameter with a # prefix by putting that parameter in quotes. Suppose we want to print out the value of certain variables in a program. Instead of calling the ‘cout’ directly, we can define a utility macro that will do the work. The required macro to do above job is given below:
# define Trace (x) cout<<#x<<”=”<<<”/n”;
Then, to print out the value of a variable named current_index.
For instance we can simply write this:
Trace (current_index);
When the preprocessor expands this, it generates the following statement:
cout <<<”=” <<<”/n”;
Conditional directives: -
One can use “conditional directives” such as #if, #ifdef, #ifndef, #else, #elif, and #endif to control which parts of a source file get compiled and under which conditions. With this feature, one maintains a single set of source files that can be selectively compiled with different compilers and in different environments.
Common way of using conditional directives are given below:
# ifdef __PROJECT_H# define __PROJECT_H
/* declarations to be included once */
/* _ _ _ */
# endif
The following example shows how you can include a different header file depending on the version number of the software directives. To include a header file only ones, we can use the following:
# if CPU_TYPE = = 8086# include
# elif CPU_TYPE = = 80386
# include
# else
# error Unknown CPU type.
# endif.
The # error directive is used to display error messages during preprocessing.
Other directives:
Several other preprocessor directives are meant for miscellaneous tasks. For example, we can use the # undefined directive to remove the current definition of a symbol. The #pragma is another special-purpose directive that we can use to convey information to the C++ compiler. Preprocessor directives that begin with # pragma are known as pragmas, and they are used to access special features of a compiler and as such they vary from one compiler to another.
ANSI C++ compilers maintain several predefined macros. Of these, the macros _ _file _ _ and _ _line _ _, respectively, refer to the current source file name and the current line number being processed, One can use the #line directive to change these. For example, to set _ _ file _ _ to “file – io.c” and _ _ LINE _ _to 100, one would say this:
# Line 100 “file_io.c”