Übungsblatt 05 Angabe WS1718 PDF

Title Übungsblatt 05 Angabe WS1718
Course Informatik 1 für Ingenieurwissenschaften (MSE)
Institution Technische Universität München
Pages 6
File Size 137.1 KB
File Type PDF
Total Downloads 107
Total Views 173

Summary

Übungsblatt 05 Angabe WS1718, die Tutorien orientieren sich an diesen Aufgaben....


Description

Technische Universit¨ at M¨ unchen

Institut f¨ ur Informatik WS 2017/2018 Exercise 5 22nd November 2017

MSE IN8011 Prof. B. H. Menze, Dr. K. Shi

Exercise 5 - Integer Sizes; Using Multiple Files 5.1 Integer Sizes and Overflows You’ve already learned that C has various integer types. In C, integers can be signed (default) or unsigned and they can be int, short int, long int, long long int (or char). However, the quirk is that the C standard doesn’t make a lot of guarantees about how large all of these integers actually are, so their actual sizes may differ from one machine (or compiler) to the next. Common sizes for integers are 1, 2, 4 or 8 bytes. That is 8, 16, 32, or 64 bits. What numbers can these types represent? In any case, a number with N bits can represent 2 N different numbers, but which ones depends on whether the integer is signed or not: Bits

Different Values

Unsigned Min Max 0 28 − 1 = 255 0 216 − 1 = 65535 0 232 − 1 = 4294967295 0 264 − 1 = 18446744073709551615

8 16 32 64

28 = 256 216 = 65536 232 = 4294967296 264 = 18446744073709551616

8 16 32 64

Signed Min Max −27 = −128 27 − 1 = 127 −215 = −32768 215 − 1 = 32767 −231 = −2147483648 231 − 1 = 2147483647 63 −2 = −9223372036854775808 263 − 1 = 9223372036854775807

What happens when we try to store a number that doesn’t fit into our integer type? In that case, we get overflow or underflow (depending on whether the number is too small or too big). In essence, this means that the additional bits are discarded. But in practice, it simply means that numbers wrap around. An example: we have the number 4294967295 (232− 1) stored in an unsigned 32-bit integer. When we try to increment it by 1, the result no longer fits into a 32-bit integer, so instead of the expected 4294967296 we get 0.

1

Another example: we have −128 stored in an 8-bit integer. When we try to decrement it, the expected result −129 would no longer fit, so we actually get +127 instead. These overflows can mess with your computations if you’re not careful, so it’s always important to know how large the numbers in your code can get and to choose an appropriate type for them. Now the interesting question is, how many bits do each of the types in C have? As mentioned above, this depends on the platform. It is guaranteed that the minimum sizes are 8 bits for char, 16 bits for short and int, 32 bits for long and 64 bits for long long. It also guarantees that the sizes remain in this relative order. But that’s about it. In particular, the type int might have 16, 32, or even 64 bits. If you want to find out how many bits each of the types has on your machine, you can run the following simple program: 1 2 3 4 5 6 7 8 9 10

# i ncl ude < stdio .h > int main () { prin tf (" int has %d bits .\ n" , size of ( int ) *8) ; pri nt f ( " ch ar has % d bits .\ n " , si ze of ( char ) *8 ) ; print f (" s hort has % d bits .\ n" , si zeof ( sh ort ) *8) ; pri nt f ( " lo ng has % d bits .\ n " , si ze of ( long ) *8 ) ; pri nt f ( " lo ng l ong ha s %d b its .\ n " , si ze of ( l ong long ) *8) ; }

How do we choose an integer with a specific width if we don’t know what platform the code will be run on? As of the C99 standard, C provides new types in the header , called int8 t, int16 t, int32 t and int64 t for signed types, as well as uint8 t, uint16 t, uint32 t and uint64 t for unsigned types. These are guaranteed to have the same width on any platform. To ensure that gcc compiles your code with the C99 standard, use the option -std=c99. In the homework problems, our Makefiles will do that for you automatically. What values do you get for the following computations if you store the results in a signed 8-bit integer: (a) (b) (c) (d) (e) (f) (g) (h)

125 + 10 -125 - 10 113 + 15 -113 - 15 127 * 2 127 * 3 -128 * 2 -128 * 3

5.2 Multiple files In one of the last exercises we talked about scope and the keyword static. Now we will introduce how you can separate your code into multiple files and use those concepts in practice. There are many reasons you might want to spread your code across multiple files. For example, when you want to:

2

(a) Manage large projects (b) Separate functionality (c) Make code available to other applications in a modular way If you have a large project that has a bunch of math functions and a bunch of input-output functions and a lot of other code, you might want to pull the math functionality out into one set of files, and the I/O functions into another set of files, all of which can be used by your main program, which might be in a completely separate file. This allows you to separate all of the functionality, edit only the parts you need when you need them, and it may allow you to build what is called a library. A Library is a set of functions within a specific context, used by other programs, in order to avoid the need to implement the functionality multiple times (e.g. stdlib, crypto,...). In order to separate your code, you need to separate your functions’, and types’ declarations from their respective definitions. This is where header files come into play.

5.2.1 Header files Header files contain (among other things) declarations — but not implementations — of functions, structs, and other definitions. They are used to make these declarations available to other code, or to make it possible to use functions without considering what order they are in a file. That is, to avoid the problem of use before declaration we already had the declaration of each function at the beginning of the file. The definition of functions in advance of their implementation is called function prototyping. Function prototypes should be stored in separate header files. Therefore, header files are used to separate the definition of a function from its actual implementation. The header file is usually given the same name as the corresponding source file, with the difference, that the file extension is h instead of c. The general structure of a header file is like follows: 1 2 3 4 5 6

# ifnde f F ILE NA ME_ H # defin e F ILE NA ME_ H // your fun cti on de cl ar ati on s here # endif /* F ILE NA ME_ H */

We fill talk about the C preprocessor in future assignments, but this construct (the so-called inculde guard) basically checks that the header file can only be included once. We will understand later why this is important. To make the content of the header header.h available in the source file source.c you have to include it in source.c using the include command: # inc lude " head er .h "

where the header must be in the same directory as the source file. Otherwise, you have to specify the complete path (relative or absolute) to the header file. The include command simply copies the content of the target file into the place where it is called.

3

This takes place before the compilation starts. So, for example, if we have the following function: 1 2 3

void p ri nt_ st uf f ( char * st uff ) { pri nt f ( "% s\ n" , st uff ) ; }

The source file will contain the code above, while the header file will simply contain the function signature consisting of the return type, function name, and function parameters (in addition to the construct shown above). Like this: void p ri nt_ st uf f ( char * st uff ) ;

As you already have seen in the exercise about functions, this only declares the function. Therefore, other source files/programs can also access the symbols referring to the function from the outside. Putting a structure definition1 in a header file also makes it available to other files which include that header file. If you want to define a global variable, you also have to declare it in the header. But if you write int x; into your header, you declare the variable x to be of type int and its state to be undefined, as you do not initialize x. So if you compile the program, x will be located in the code of every file that include your header. Instead, you have to declare that variable without defining it. Thus you have to add the extern keyword, to explicitly state that the variable should not be defined within the header. In this case you must not initialize the variable. For example: exter n int x ;

Nevertheless you still have to define the variable once in your corresponding source file.

5.2.2 Source files The source file, in contrast, contains the actual implementation of things declared in the header file. There can be functions and variables also not declared in the header file, especially those only used internally by the source code. Remember functions that were declared as static for example. We include a header file by adding the following line to the beginning of our source file: # inc lude " head er .h "

Note that in contrast to the angle brackets we used, when we included stdio.h we use double quotes for headers that we defined ourself. For library files, source files do not contain a main function, as that is done by the programs using the library.

1 Presuming

it’s the full definition

4

5.2.3 Including and compiling a simple program with multiple files Let’s say you have three files: libstr.h and libstr.c containing some string processing functions, and a main program file (which also contains your main method) called writetext.c, which uses the libstr functions. First of all, be sure that writetext.c contains #include "libstr.h" (presuming libstr.h is in the same directory as writetext.c) in the first lines. This means the compiler will be able to see the function definitions in libstr.h when compiling and linking the code. Secondly, we need to compile everything to get one program. We will look at two ways to do this. For these examples, we presume everything is all in one directory (for the sake of simplicity). Compiling everything all together at once Determining the order of compilation on the command line can cause no end of trouble, but generally, it seems working in backward order of dependencies — that is, from the most dependent source file to the least — works best. We compile both source files together into one executable (called writer) by first listing the file that depends on the most other files, and then the files it depends on, and then the files they depends on, and so forth. In the above case, writetext.c depends on the code in libstr.c, so we put writetext.c as the first source file for gcc, and then libstr.c. We do not have to add the header file here, as this one is already referenced in the source files. The following will compile our example into a program called writer, and order can matter: gcc -o w riter w rit ete xt . c li bstr . c

Creating a separate library A library is a compiled binary file that does not include its own main function. We can for example compile our libstr functions to a library and use that library with different main functions afterwards. We are not going to get into the complexity of library files here (especially static/shared libraries, installation so that everyone’s programs can read them, etc), but it is also possible to compile libstr as a separate library that can then be included in other programs. We’ll create static libraries in our example here which can then be linked to our main program. In the above example, we only have one source file for our library. To create a static libstr library, we do two things: (a) Compile the library source into object files. We do this with the -c option to gcc: gcc -c l ibstr . c

If there had been multiple source files for your library, you would compile them all using the -c option: gcc -c fi le 1 .c fi le2 . c file3 . c

5

This will produce files called libstr.o or file1.o, file2.o, file3.o respectively (the so-called “object files”). (b) Create the static library using the ar command: ar rcs lib ra ryn am e .a ob jec tf ile 1 ob jec tf il e2 ob je ctf il e3

So for the libstr library: ar rcs li bstr . a libst r .o

This creates a static library file called libstr.a. So the library is created, but how do we now make it accessible to our main file, writetext.c? We have to link the library to our main program using gcc. For local libraries, we just include the library at the end of the gcc command line: gcc -o w riter w rit ete xt . c li bstr . a

For system-wide libraries, we often do that with gcc’s -l option. For the exercise, you’ll have to do a little of both.

5.2.4 Exercise In the lecture you have discussed TicTacToe as an example program. Download this program from the course website, as it will be the basis for this exercise. (a) Write a function declaration for each of the functions within tictactoe.c. (b) Separate the functionality of tictactoe.c into multiple source and header files. For example one source file could contain the output functions, one source file could contain the game logic, one source file could contain the main function. This process is called refactoring. (c) Separate the declarations that you wrote previously into the corresponding header files and include them where required. (d) Compile your new code all at once (without a library) into one program and execute that program. (e) Now compile your new code by compiling source files as a separate library and link them together afterwards.

6...


Similar Free PDFs