Laboratory Training 6
Pointers to Functions and Header Files
1 Training Tasks
1.1 Output of the Table of Values of Function
Create a separate translation unit in which you should implement the function of outputting a table of values of a certain function with a certain step. Function parameters are: the beginning, end of the interval, step and pointer to the function, the values of which will be displayed in the table. In the header file, you must declare the type of function pointer and the prototype of the value table output function. In the implementation file, define the value table output function.
In another translation unit, you should place the functions for which you want to output values in a table, as well as the main () function, which outputs tables of values to at least two different functions. One of the functions for testing is a function determined from the task of the first laboratory training and implemented in the third laboratory training.
1.2 Individual Assignment
Write a program that implements exhaustive search of some value according to an individual assignment. Necessary value can be found by testing intermediate values of a function. Use typedefs and pointers to functions.
The source code should be split into two translation units. The first translation unit will be represented by
both header file and implementation file. The typedef
definition, as well as declaration of
function that searches necessary value, should be placed into header file. The definition of this function will
take place in implementation file corresponding to header file. The testing function, as well as
main()
function, should be placed into another translation unit.
Index of variant (from students list) |
Rule of searching: |
---|---|
1, 15
|
Maximum value of the second derivative |
2, 16
|
Minimum value of the first derivative |
3, 17
|
The least root |
4, 18
|
The greatest root |
5, 19
|
The sum of minimum and maximum values |
6, 20
|
The product of minimum and maximum values |
7, 21
|
Roots count |
8, 22
|
The least root of the second derivative |
9, 23
|
Minimum value of the second derivative |
10, 24
|
Maximum value of the first derivative |
11, 25
|
The least root of the first derivative |
12, 26
|
The greatest root of the first derivative |
13, 27
|
The greatest root of the second derivative |
14, 28
|
The sum of minimum and maximum values |
You should check the functionality of the program on at least two arbitrary functions. One of the functions can be standard.
Note: To calculate first derivative of y(x), you can use the following formula:
-
y(x))
/ Δx,
Where Δx is some tiny value, such as 0.0000001.
2 Instructions
2.1 Typedefs
C++ enables you to create an alias for existing type name by using the typedef
keyword,
which stands for type definition.
As a result, you create synonym for exiting type. It is important to distinguish creation of synonym from
creating a new type (definition of structures, enumerations, and classes). Definition of a synonym starts with
typedef
keyword, followed by the existing type, followed by new name
(identifier). For example,
typedef unsigned long int Cardinal; typedef int IntArray[15];
creates the new name Cardinal that you can use anywhere you might have written unsigned long
int
. The IntArray
identifier can be used for definition of an array of 15 integer
values:
Cardinal c; int f(Cardinal k); IntArray a; // int a[15];
A typedef
declaration is interpreted in the same way as a variable or function declaration,
but the identifier becomes a synonym for the type.
You can use typedef
declarations to construct shorter or more meaningful names for types
already defined by the language or for types that you have declared. Those names allow you to encapsulate
implementation details that may change.
2.2 Pointers to Functions
A pointer to a function is an address where that function's executable code is stored; that is, the address to which control is transferred when that function is called. Just as an array name is a constant pointer to the first element of the array, a function name can be treated as a constant pointer to the function. It is possible to declare a pointer variable that points to a function, and to invoke the function by using that pointer.
A pointer to a function must point to a function of the appropriate return type and signature. In the definition
int (*funcPtr) (double);
funcPtr
is declared to be a pointer that points to a function that takes a floating point
parameter and returns integer value. The parentheses around *funcPtr
are necessary. Without the
first pair of parentheses this would declare a function that takes a double
and returns a
pointer to an int
. The declaration of a function pointer will always include the return type
and the parentheses indicating types of the parameters.
You can assign a pointer to function to a specific function by assigning to the function name without the parentheses. Use the pointer to function just as you would the function name. The pointer to function must agree in return value and signature with the function to which you assign it. For example:
int round(double x) { return x + 0.5; } void main() { int (* funcPtr) (double); double y; cin >> y; funcPtr = round; cout << funcPtr(y); }
The pointer to function does not need to be dereferenced, though you are free to do so. Therefore, if pFunc
is a pointer to a function, and you assign pFunc
to a matching function, you can invoke that
function with either
pFunc(x);
or
(*pFunc)(x);
The two forms are identical.
The typedef
declaration can be used to declare types of pointers to functions:
typedef int (*FuncType)(int); FuncType pf;
You can declare arrays of pointers to functions.
Pointers to functions are more particularly used as types of functions' argument. The callback mechanism involves defining a function, which is called not directly in the part of the code where it is defined, but from another part of the code, where you can send a pointer to this function, for example, as a parameter of another function.
For example, there is a certain universal algorithm, which for its work requires the implementation of calls to another function. The function in this case acts as some information along with numerical and other arguments. These can be different tasks, for example:
- implementation of a universal algorithm for solving an equation
- finding maxima and minima
- derivative calculation
- calculation of the definite integral
- finding inflection points
- processing of an event related to user actions, etc.
Pointers to functions are primarily used to implement this mechanism.
For example, some function (algorithm) requires another function as a parameter:
void someAlgorithm(void (*f)(double)) { double z; //... f(z); //... }
In another part of the code we create the necessary function and pass its address as a parameter:
void g(double x) { //... } void main() { //... someAlgorithm(g); }
Example 3.1 illustrates the use of the callback mechanism to solve an equation using Bisection method.
2.3 Header Files
Every nontrivial program can be divided into relative universal parts that can be used in several projects, and project-specific parts that realize features of particular system. It is a good idea to store universal and problem specific parts separate files.
The simplest way to divide source code into several files is using of preprocessor directives such of
preprocessor directive as #include
allows programmer to insert the text of one source file into
another before compiling procedure.
Preprocessor does not implement a physical copying of a file contents into another file. Instead of such
copying, preprocessor creates a new source text in memory. This text is called translation unit and
contains all parts included into source text using #include
directive. It is also possible to
remove some parts of a source text using #define
, #ifdef
and #ifndef
directives. A preprocessor variable with a specific name have been declared anywhere in source file using #define
directive. It is possible to check for this fact using #ifdef
or #ifndef
directives.
For example,
#define New_Name ... #ifdef New_Name // this code is written into translation unit #else // this code is not written into translation unit #endif
A translation unit appears as a result of processing of source file by preprocessor. A single project can contain several translation units. Names defined in other translation units must be redeclared in each unit that uses them. The wrong declaration of a name can produce errors. To avoid such errors, you must use header files. A separate header file with appropriate descriptions can be included in other source files.
Header file can contain
- named namespaces
- type definitions
- function declarations
- inline function definitions
- data declarations (with
extern
keyword) - constant definitions
- preprocessor directives
- comments.
Header file cannot contain
- ordinary function definitions
- data definitions
- unnamed namespaces.
Header files are conventionally suffixed by .h
, and files containing function or data definitions
are suffixed by .cpp
.
There are numerous standard header files that contain declarations of standard classes and functions. The names
of such files in #include
directive must be written in <>
instead of ""
.
That causes preprocessor to look for such files in standard directories. Otherwise, preprocessor searches the
header starting from current directory.
2.4 Include Guards
Because your programs will use various functions from many libraries, many header files will be included in
each file. Also, header files often need to include one another. For example, header file f2.h
needs to include file f1.h
, header file f3.h
needs to include files f1.h
and f2.h
, and we need to include all of them into our source file:
//f1.h ... //f2.h #include "f1.h" ... //f3.h #include "f1.h" #include "f2.h" ... //main.cpp #include "f1.h" #include "f2.h" #include "f3.h" ...
Preprocessor includes contents of f1.h
into translation unit by processing of
main.cpp
. Then it includes contents of f2.h
into translation unit. The text of f2.h
contains inclusion of f1.h
. Therefore, translation unit contains two copies of f1.h
.
After inclusion of f3.h
, translation unit contains four copies of f1.h
and two copies
of f2.h
. Those inclusions are unmeaning and dangerous because some pieces of code can be placed
into translation unit two, three, or more times. That is legal for simple declarations and illegal for
definitions of inline functions and other definitions.
The traditional solution of a problem is to insert include guards (inclusion guards) in headers. The
text of the header file f1.h
can be organized as follows:
#ifndef F1_H #define F1_H ... // the whole file goes here #endif
The first time program includes this file, preprocessor reads the first line and the test evaluates to TRUE
;
that is, variable F1_H
is not yet defined. So, it goes ahead and defines it and then includes the
entire file.
The second time program includes the f1.h
file, preprocessor reads the first line and the test
evaluates to FALSE
; F1_H
has been defined. It therefore skips to the next
#endif
(at the end of the file). Thus, it skips the entire contents of the file.
The actual name of the defined symbol (F1_H
) is not important, although it is customary to use the
filename with the dot (.) changed to an underscore. There are also other conventions, however.
To create header files in Microsoft Visual Studio, follow the following steps:
- choose Add New Item in Project submenu
- choose Header File (.h) in Templates window
- type new file name without extension in Name field
- press Add button
You can add new implementation file in analogous way.
Sometimes the idea arises to create a global variable in one translation unit and use it to exchange data between the functions of different translation units. The first idea that comes to mind is to define the following variable in the header file:
int someValue; // The variable is intended for data exchange
Such a definition leads to an error. The preprocessor includes a header file in each translation unit, and the compiler creates several such variables. When linking a program, a name conflict occurs, or a variable is created for each translation unit.
In fact, the variable should be declared, not defined in the header file. The variable declaration reports that
the variable exists and is defined elsewhere. Declaration is not a definition, it does not lead to memory
allocation. The extern
keyword is used to declare a variable without definition:
extern int someValue; // the variable will be defined later
The variable should be defined in one (and only one) implementation file. Now all translation units have access to one variable.
2.5 Namespaces
Namespaces determine a logical structure of a program.
A namespace is an optionally-named declarative region. Namespaces can avoid name conflicts. Namespaces give a mechanism for expressing logical grouping.
namespace MySpace { int k = 10; void f(int n) { k = n; } }
Namespace members can be defined separately from their declarations. For example,
namespace MySpace { int k = 10; void f(int n); } void MySpace::f(int n) { k = n; }
Namespaces can be nested in other namespaces:
namespace FirstSpace { namespace SecondSpace { ... } }
You can use an alternate name to refer to a namespace identifier:
namespace YourSpace = MySpace;
Namespaces are discontinuous and open for additional development. If you redeclare a namespace, the effect is that you extend the original namespace by adding new declarations:
namespace FirstSpace { // first part } ... // other declarations namespace SecondSpace { // another namespace } namespace FirstSpace { // second part }
Related declarations can span across several files.
There are three ways to access the elements of a namespace:
- the explicit access qualification
- the using-declaration
- the using-directive.
In the first case, you can use the namespace identifier together with the scope resolution operator
(::
) followed by the member name. For example,
int x = MySpace::k;
In the second case, you can access namespace members individually with the using
-declaration
syntax. When you make a using
-declaration, you add the declared identifier to the
local namespace:
using MySpace::k; using MySpace::f; int y = k + f(k);
If you want to use several (or all of) the members of a namespace, C++ provides an easy way to get access to
the complete namespace. The using
-directive specifies that all identifiers in a namespace
are in scope at the point that the using
-directive statement is made. For example,
using namespace MySpace;
You must avoid using this directive because of possible name conflicts.
The using
-directive can be used if you want to join several namespaces:
namespace NewSpace { using namespace FirstSpace; using namespace SecondSpace; }
You can select some names from one or more namespaces in new namespace with using
-declaration:
namespace NewSpace { using OtherSpace::name1; using OtherSpace::name2; }
Such namespace can be used in multiple projects.
Namespaces in C++ do not cover data. Once namespace was connected with the help of using directive, all names declared in the namespace can be used as global names without any restriction.
Most of the components of the C++ Standard Library are grouped under namespace std
. Namespace
std
is subdivided into additional namespaces such as std::rel_ops
.
There is a special kind of namespaces – the so-called anonymous namespaces (nameless namespaces, unnamed namespaces). For example,
namespace { int k; void f() { } }
It is impossible to connect such space in any way. Within a physical file, access to anonymous space names is not restricted and does not require prefixes. It is not possible to work with anonymous names outside the file.
Creating an anonymous namespace and locating the functions and variables needed only in this translation unit speeds up the linker's work, because it does not even try to find names used in other translation units in the anonymous space. Using anonymous spaces also reduces the probability of name conflicts during linking.
Anonymous namespaces cannot be placed in header files.
3 Sample Programs
3.1 Bisection (Dichotomy) Method
The following program finds roots of an equation using dichotomy method. The only restriction on use of dichotomy method is that the equation must have exactly one root on a given interval.
#include <iostream>
#include <cmath> using std::cout; using std::endl; using std::fabs; typedef double (*FuncType)(double); // The fourth argument has default value: double root(FuncType f, double a, double b, double eps = 0.001) { double x; do { x = (a + b) / 2; if (f(a) * f(x) > 0) { a = x; } else { b = x; } } while (fabs(b - a) > eps); return x; } double g(double x) { return x * x - 2; } void main() { cout << root(g, 0, 6) << endl; cout << root(g, 0, 6, 0.00001) << endl; cout << root(sin, 1, 4) << endl; cout << root(sin, 1, 4, 0.00001) << endl; }
As can be seen from the above code, a callback mechanism is used to obtain intermediate values of the function.
3.2 Using Header Files
Consider we want to create unit built from header file SomeFile.h
and implementation file SomeFile.cpp
.
We should add include guards first:
#ifndef SomeFile_h #define SomeFile_h #endif
Header file contains function prototype before #endif:
#ifndef SomeFile_h #define SomeFile_h int sum(int a, int b); #endif
Implementation file contains function definition placed after #include "SomeFile.h"
directive.
#include"SomeFile.h" int sum(int a, int b) { return a + b; }
We should include SomeFile.h
in main unit:
#include <iostream> #include "SomeFile.h" using namespace std; void main() { int x, y; cout << "Enter two integer values:" << endl; cin >> x >> y; int z = sum(x, y); cout << "Sum is " << z << endl; }
4 Exercises
Implement examples and exercises of training #3 placing into separate unit all
functions, apart from main()
.
5 Quiz
- How to create synonym for existing type?
- What is pointer to function?
- What is usage of painters to functions?
- How to define pointer to function?
- What is translation unit?
- What is the usage of #define directive?
- What are rules of distribution of source code between header file and implementation file?
- What is the difference between inclusion of standard header files and user header files?
- What are include guards?
- What is namespace?
- How to join several namespaces into one?
- How to define alias for existing namespace?