Pointers and const

Pointing to const variables

So far, all of the pointers you’ve seen are non-const pointers to non-const values:

However, what happens if value is const?

The above snippet won’t compile -- we can’t set a non-const pointer to a const variable. This makes sense: a const variable is one whose value can not be changed. Hypothetically, if we could set a non-const pointer to a const value, then we would be able to dereference the non-const pointer and change the value. That would violate the intention of const.

Pointer to const value

A pointer to a const value is a (non-const) pointer that points to a constant value.

To declare a pointer to a const value, use the const keyword before the data type:

In the above example, ptr points to a const int.

So far, so good, right? Now consider the following example:

A pointer to a constant variable can point to a non-constant variable (such as variable value in the example above). Think of it this way: a pointer to a constant variable treats the variable as constant when it is accessed through the pointer, regardless of whether the variable was initially defined as const or not.

Thus, the following is okay:

But the following is not:

Because a pointer to a const value is not const itself (it just points to a const value), the pointer can be redirected to point at other values:

Const pointers

We can also make a pointer itself constant. A const pointer is a pointer whose value can not be changed after initialization

To declare a const pointer, use the const keyword between the asterisk and the pointer name:

Just like a normal const variable, a const pointer must be initialized to a value upon declaration. This means a const pointer will always point to the same address. In the above case, ptr will always point to the address of value (until ptr goes out of scope and is destroyed).

However, because the value being pointed to is still non-const, it is possible to change the value being pointed to via dereferencing the const pointer:

Const pointer to a const value

Finally, it is possible to declare a const pointer to a const value by using the const keyword both before the type and before the variable name:

A const pointer to a const value can not be set to point to another address, nor can the value it is pointing to be changed through the pointer.

Recapping

To summarize, you only need to remember 4 rules, and they are pretty logical:

  • A non-const pointer can be redirected to point to other addresses.
  • A const pointer always points to the same address, and this address can not be changed.
  • A pointer to a non-const value can change the value it is pointing to. These can not point to a const value.
  • A pointer to a const value treats the value as const (even if it is not), and thus can not change the value it is pointing to.

Keeping the declaration syntax straight can be challenging. Just remember that the type of value the pointer points to is always on the far left:

Conclusion

Pointers to const values are primarily used in function parameters (for example, when passing an array to a function) to help ensure the function doesn’t inadvertently change the passed in argument. We will discuss this further in the section on functions.


Processing Data with Java SE 8 Streams, Part 1

by Raoul-Gabriel Urma

Use stream operations to express sophisticated data processing queries.

What would you do without collections? Nearly every Java application makes and processes collections. They are fundamental to many programming tasks: they let you group and process data. For example, you might want to create a collection of banking transactions to represent a customer’s statement. Then, you might want to process the whole collection to find out how much money the customer spent. Despite their importance, processing collections is far from perfect in Java.

First, typical processing patterns on collections are similar to SQL-like operations such as “finding” (for example, find the transaction with highest value) or “grouping” (for example, group all transactions related to grocery shopping). Most databases let you specify such operations declaratively. For example, the following SQL query lets you find the transaction ID with the highest value: "SELECT id, MAX(value) from transactions".

As you can see, we don’t need to implement how to calculate the maximum value (for example, using loops and a variable to track the highest value). We only express what we expect. This basic idea means that you need to worry less about how to explicitly implement such queries—it is handled for you. Why can’t we do something similar with collections? How many times do you find yourself reimplementing these operations using loops over and over again?

Second, how can we process really large collections efficiently? Ideally, to speed up the processing, you want to leverage multicore architectures. However, writing parallel code is hard and error-prone. 

Here’s a mind-blowing idea: these two operations can produce elements “forever.”

Java SE 8 to the rescue! The Java API designers are updating the API with a new abstraction called Stream that lets you process data in a declarative way. Furthermore, streams can leverage multi-core architectures without you having to write a single line of multithread code. Sounds good, doesn’t it? That’s what this series of articles will explore.

Before we explore in detail what you can do with streams, let’s take a look at an example so you have a sense of the new programming style with Java SE 8 streams. Let’s say we need to find all transactions of type grocery and return a list of transaction IDs sorted in decreasing order of transaction value. In Java SE 7, we’d do that as shown in Listing 1. In Java SE 8, we’d do it as shown in Listing 2.

List<Transaction> groceryTransactions = new Arraylist<>();
for(Transaction t: transactions){
  if(t.getType() == Transaction.GROCERY){
    groceryTransactions.add(t);
  }
}
Collections.sort(groceryTransactions, new Comparator(){
  public int compare(Transaction t1, Transaction t2){
    return t2.getValue().compareTo(t1.getValue());
  }
});
List<Integer> transactionIds = new ArrayList<>();
for(Transaction t: groceryTransactions){
  transactionsIds.add(t.getId());
}

Listing 1

List<Integer> transactionsIds = 
    transactions.stream()
                .filter(t -> t.getType() == Transaction.GROCERY)
                .sorted(comparing(Transaction::getValue).reversed())
                .map(Transaction::getId)
                .collect(toList());

Listing 2

Figure 1 illustrates the Java SE 8 code. First, we obtain a stream from the list of transactions (the data) using the stream() method available on List. Next, several operations (filtersortedmapcollect) are chained together to form a pipeline, which can be seen as forming a query on the data.

streams-f1

Figure 1

So how about parallelizing the code? In Java SE 8 it’s easy: just replace stream() with parallel Stream(), as shown in Listing 3, and the Streams API will internally decompose your query to leverage the multiple cores on your computer.

List<Integer> transactionsIds = 
    transactions.parallelStream()
                .filter(t -> t.getType() == Transaction.GROCERY)
                .sorted(comparing(Transaction::getValue).reversed())
                .map(Transaction::getId)
                .collect(toList());

Listing 3

Don’t worry if this code is slightly overwhelming. We will explore how it works in the next sections. However, notice the use of lambda expressions (for example, t-> t.getCategory() == Transaction.GROCERY) and method references (for example, Transaction::getId), which you should be familiar with by now. (To brush up on lambda expressions, refer to previous Java Magazine articles and other resources listed at the end of this article.)

For now, you can see a stream as an abstraction for expressing efficient, SQL-like operations on a collection of data. In addition, these operations can be succinctly parameterized with lambda expressions.

At the end of this series of articles about Java SE 8 streams, you will be able to use the Streams API to write code similar to Listing 3 to express powerful queries.

Getting Started with Streams

Let’s start with a bit of theory. What’s the definition of a stream? A short definition is “a sequence of elements from a source that supports aggregate operations.” Let’s break it down: 

  • Sequence of elements: A stream provides an interface to a sequenced set of values of a specific element type. However, streams don’t actually store elements; they are computed on demand.
  • Source: Streams consume from a data-providing source such as collections, arrays, or I/O resources.
  • Aggregate operations: Streams support SQL-like operations and common operations from functional programing languages, such as filtermapreducefindmatchsorted, and so on. 

Furthermore, stream operations have two fundamental characteristics that make them very different from collection operations:

  • Pipelining: Many stream operations return a stream themselves. This allows operations to be chained to form a larger pipeline. This enables certain optimizations, such as laziness and short-circuiting, which we explore later.
  • Internal iteration: In contrast to collections, which are iterated explicitly (external iteration), stream operations do the iteration behind the scenes for you. 

Let’s revisit our earlier code example to explain these ideas. Figure 2 illustrates Listing 2 in more detail.

streams-f2

Figure 2

We first get a stream from the list of transactions by calling the stream() method. The datasource is the list of transactions and will be providing a sequence of elements to the stream. Next, we apply a series of aggregate operations on the stream: filter (to filter elements given a predicate), sorted (to sort the elements given a comparator), and map (to extract information). All these operations except collect return a Stream so they can be chained to form a pipeline, which can be viewed as a query on the source.

No work is actually done until collect is invoked. The collect operation will start processing the pipeline to return a result (something that is not a Stream; here, a List). Don’t worry about collect for now; we will explore it in detail in a future article. At the moment, you can see collect as an operation that takes as an argument various recipes for accumulating the elements of a stream into a summary result. Here, toList() describes a recipe for converting a Stream into a List.

Before we explore the different methods available on a stream, it is good to pause and reflect on the conceptual difference between a stream and a collection.

Streams Versus Collections

Both the existing Java notion of collections and the new notion of streams provide interfaces to a sequence of elements. So what’s the difference? In a nutshell, collections are about data and streams are about computations.

Consider a movie stored on a DVD. This is a collection (perhaps of bytes or perhaps of frames—we don’t care which here) because it contains the whole data structure. Now consider watching the same video when it is being streamed over the internet. It is now a stream (of bytes or frames). The streaming video player needs to have downloaded only a few frames in advance of where the user is watching, so you can start displaying values from the beginning of the stream before most of the values in the stream have even been computed (consider streaming a live football game).

In the coarsest terms, the difference between collections and streams has to do with when things are computed. A collection is an in-memory data structure, which holds all the values that the data structure currently has—every element in the collection has to be computed before it can be added to the collection. In contrast, a stream is a conceptually fixed data structure in which elements are computed on demand.

Using the Collection interface requires iteration to be done by the user (for example, using the enhanced for loop called foreach); this is called external iteration.

In contrast, the Streams library uses internal iteration—it does the iteration for you and takes care of storing the resulting stream value somewhere; you merely provide a function saying what’s to be done. The code in Listing 4 (external iteration with a collection) and Listing 5 (internal iteration with a stream) illustrates this difference.

List<String> transactionIds = new ArrayList<>(); 
for(Transaction t: transactions){
    transactionIds.add(t.getId()); 
}

Listing 4

List<Integer> transactionIds = 
    transactions.stream()
                .map(Transaction::getId)
                .collect(toList());

Listing 5

In Listing 4, we explicitly iterate the list of transactions sequentially to extract each transaction ID and add it to an accumulator. In contrast, when using a stream, there’s no explicit iteration. The code in Listing 5 builds a query, where the map operation is parameterized to extract the transaction IDs and the collect operation converts the resulting Stream into a List.

You should now have a good idea of what a stream is and what you can do with it. Let’s now look at the different operations supported by streams so you can express your own data processing queries.

Stream Operations: Exploiting Streams to Process Data

The Stream interface in java.util .stream.Stream defines many operations, which can be grouped in two categories. In the example illustrated in Figure 1, you can see the following operations: 

  • filtersorted, and map, which can be connected together to form a pipeline
  • collect, which closed the pipeline and returned a result 

Stream operations that can be connected are called intermediate operations. They can be connected together because their return type is a Stream. Operations that close a stream pipeline are called terminal operations. They produce a result from a pipeline such as a List, an Integer, or even void (any non-Stream type).

You might be wondering why the distinction is important. Well, intermediate operations do not perform any processing until a terminal operation is invoked on the stream pipeline; they are “lazy.” This is because intermediate operations can usually be “merged” and processed into a single pass by the terminal operation.

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8);
List<Integer> twoEvenSquares = 
    numbers.stream()
           .filter(n -> {
                    System.out.println("filtering " + n); 
                    return n % 2 == 0;
                  })
           .map(n -> {
                    System.out.println("mapping " + n);
                    return n * n;
                  })
           .limit(2)
           .collect(toList());

Listing 6

For example, consider the code in Listing 6, which computes two even square numbers from a given list of numbers. You might be surprised that it prints the following:

filtering 1
filtering 2
mapping 2
filtering 3
filtering 4
mapping 4


This is because limit(2) uses short-circuiting; we need to process only part of the stream, not all of it, to return a result. This is similar to evaluating a large Boolean expression chained with the and operator: as soon as one expression returns false, we can deduce that the whole expression is false without evaluating all of it. Here, the operation limit returns a stream of size 2

The Streams API will internally decompose your query to leverage the multiple cores on your computer.

In addition, the operations filter and map have been merged in the same pass.

To summarize what we’ve learned so far, working with streams, in general, involves three things: 

  • A datasource (such as a collection) on which to perform a query
  • A chain of intermediate operations, which form a stream pipeline
  • One terminal operation, which executes the stream pipeline and produces a result 

Let’s now take a tour of some of the operations available on streams. Refer to the java.util .stream.Stream interface for the complete list, as well as to the resources at the end of this article for more examples.

Filtering. There are several operations that can be used to filter elements from a stream: 

  • filter(Predicate): Takes a predicate (java.util.function.Predicate) as an argument and returns a stream including all elements that match the given predicate
  • distinct: Returns a stream with unique elements (according to the implementation of equals for a stream element)
  • limit(n): Returns a stream that is no longer than the given size n
  • skip(n): Returns a stream with the first n number of elements discarded 

Finding and matching. A common data processing pattern is determining whether some elements match a given property. You can use the anyMatchallMatch, and noneMatch operations to help you do this. They all take a predicate as an argument and return a boolean as the result (they are, therefore, terminal operations). For example, you can use allMatch to check that all elements in a stream of transactions have a value higher than 100, as shown in Listing 7.

boolean expensive =
    transactions.stream()
                .allMatch(t -> t.getValue() > 100);

Listing 7

In addition, the Stream interface provides the operations findFirst and findAny for retrieving arbitrary elements from a stream. They can be used in conjunction with other stream operations such as filter. Both findFirst and findAny return an Optional object, as shown in Listing 8.

Optional<Transaction> = 
    transactions.stream()
                .filter(t -> t.getType() == Transaction.GROCERY)
                .findAny();

Listing 8

The Optional<T> class (java.util .Optional) is a container class to represent the existence or absence of a value. In Listing 8, it is possible that findAny doesn’t find any transaction of type grocery. The Optional class contains several methods to test the existence of an element. For example, if a transaction is present, we can choose to apply an operation on the optional object by using the ifPresent method, as shown in Listing 9 (where we just print the transaction).

  transactions.stream()
              .filter(t -> t.getType() == Transaction.GROCERY)
              .findAny()
              .ifPresent(System.out::println);

Listing 9

Mapping. Streams support the method map, which takes a function (java.util.function.Function) as an argument to project the elements of a stream into another form. The function is applied to each element, “mapping” it into a new element.

For example, you might want to use it to extract information from each element of a stream. In the example in Listing 10, we return a list of the length of each word from a list. Reducing. So far, the terminal operations we’ve seen return a boolean (allMatch and so on), void (forEach), or an Optional object (findAny and so on). We have also been using collect to combine all elements in a Stream into a List.

List<String> words = Arrays.asList("Oracle", "Java", "Magazine");
 List<Integer> wordLengths = 
    words.stream()
         .map(String::length)
         .collect(toList());

Listing 10

However, you can also combine all elements in a stream to formulate more-complicated process queries, such as “what is the transaction with the highest ID?” or “calculate the sum of all transactions’ values.” This is possible using the reduce operation on streams, which repeatedly applies an operation (for example, adding two numbers) on each element until a result is produced. It’s often called a fold operation in functional programming because you can view this operation as “folding” repeatedly a long piece of paper (your stream) until it forms one little square, which is the result of the fold operation.

It helps to first look at how we could calculate the sum of a list using a for loop:

int sum = 0;
for (int x : numbers) {
    sum += x; 
}


Each element of the list of numbers is combined iteratively using the addition operator to produce a result. We essentially “reduced” the list of numbers into one number. There are two parameters in this code: the initial value of the sum variable, in this case 0, and the operation for combining all the elements of the list, in this case +.

Using the reduce method on streams, we can sum all the elements of a stream as shown in Listing 11. The reduce method takes two arguments:

int sum = numbers.stream().reduce(0, (a, b) -> a + b);

Listing 11 

  • An initial value, here 0
  • BinaryOperator<T> to combine two elements and produce a new value 

The reduce method essentially abstracts the pattern of repeated application. Other queries such as “calculate the product” or “calculate the maximum” (see Listing 12) become special use cases of the reduce method.

int product = numbers.stream().reduce(1, (a, b) -> a * b);
int product = numbers.stream().reduce(1, Integer::max);

Listing 12

Numeric Streams

You have just seen that you can use the reduce method to calculate the sum of a stream of integers. However, there’s a cost: we perform many boxing operations to repeatedly add Integer objects together. Wouldn’t it be nicer if we could call a sum method, as shown in Listing 13, to be more explicit about the intent of our code?

int statement = 
    transactions.stream()
                .map(Transaction::getValue)
                .sum(); // error since Stream has no sum method

Listing 13

Java SE 8 introduces three primitive specialized stream interfaces to tackle this issue—IntStreamDoubleStream, and LongStream—that respectively specialize the elements of a stream to be intdouble, and long.

The most-common methods you will use to convert a stream to a specialized version are mapToIntmapToDouble, and mapToLong. These methods work exactly like the method map that we saw earlier, but they return a specialized stream instead of a Stream<T>. For example, we could improve the code in Listing 13 as shown in Listing 14. You can also convert from a primitive stream to a stream of objects using the boxed operation.

int statementSum = 
    transactions.stream()
                .mapToInt(Transaction::getValue)
                .sum(); // works!

Listing 14

Finally, another useful form of numeric streams is numeric ranges. For example, you might want to generate all numbers between 1 and 100. Java SE 8 introduces two static methods available on IntStreamDoubleStream, and LongStream to help generate such ranges: range and rangeClosed.

Both methods take the starting value of the range as the first parameter and the end value of the range as the second parameter. However, range is exclusive, whereas rangeClosed is inclusive. Listing 15 is an example that uses rangeClosed to return a stream of all odd numbers between 10 and 30.

IntStream oddNumbers = 
    IntStream.rangeClosed(10, 30)
             .filter(n -> n % 2 == 1);

Listing 15

Building Streams

There are several ways to build streams. You’ve seen how you can get a stream from a collection. Moreover, we played with streams of numbers. You can also create streams from values, an array, or a file. In addition, you can even generate a stream from a function to produce infinite streams! 

In contrast to collections, which are iterated explicitly (external iteration), stream operations do the iteration behind the scenes for you.

Creating a stream from values or from an array is straightforward: just use the static methods Stream .of for values and Arrays.stream for an array, as shown in Listing 16.

Stream<Integer> numbersFromValues = Stream.of(1, 2, 3, 4);
int[] numbers = {1, 2, 3, 4};
IntStream numbersFromArray = Arrays.stream(numbers);

Listing 16

You can also convert a file in a stream of lines using the Files.lines static method. For example, in Listing 17 we count the number of lines in a file.

long numberOfLines = 
    Files.lines(Paths.get(“yourFile.txt”), Charset.defaultCharset())
         .count();

Listing 17

Learn More


 Java 8 Lambdas in Action

 GitHub repository with Java SE 8 code examples

 Java 8: Lambdas, Part 1” by Ted Neward

Infinite streams. Finally, here’s a mind-blowing idea before we conclude this first article about streams. By now you should understand that elements of a stream are produced on demand. There are two static methods—Stream.iterate and Stream .generate—that let you create a stream from a function. However, because elements are calculated on demand, these two operations can produce elements “forever.” This is what we call an infinite stream: a stream that doesn’t have a fixed size, as a stream does when we create it from a fixed collection.

Listing 18 is an example that uses iterate to create a stream of all numbers that are multiples of 10. The iterate method takes an initial value (here, 0) and a lambda (of type UnaryOperator<T>) to apply successively on each new value produced.

Stream<Integer> numbers = Stream.iterate(0, n -> n + 10);

Listing 18

We can turn an infinite stream into a fixed-size stream using the limit operation. For example, we can limit the size of the stream to 5, as shown in Listing 19.

numbers.limit(5).forEach(System.out::println); // 0, 10, 20, 30, 40

Listing 19

Conclusion

Java SE 8 introduces the Streams API, which lets you express sophisticated data processing queries. In this article, you’ve seen that a stream supports many operations such as filtermapreduce, and iterate that can be combined to write concise and expressive data processing queries. This new way of writing code is very different from how you would process collections before Java SE 8. However, it has many benefits. First, the Streams API makes use of several techniques such as laziness and short-circuiting to optimize your data processing queries. Second, streams can be parallelized automatically to leverage multicore architectures. In the next article in this series, we will explore more-advanced operations, such as flatMap and collect. Stay tuned.

  • 개인적으로 cygwin+mingw32+boost+SDL+directx+opengl+ACE기반에서 작업을 합니다. :) 그래서 여기에 요약을 해봅니다.

[�몄쭛]

링크 및 아티클 #

[�몄쭛]

#

  • cygwin 기본 패키지 외의 다른 라이브러리들을 설치할 경우(예 : SDL), autoconf를 지원하는 소스배포본이면 ./configure를 다음과 같이 실행해야합니다.
    ./configure --prefix=/usr/local
    이것은 라이브러리의 모든 결과화일이 /usr/local/include, /usr/local/lib로 들어가도록 해줍니다. (다른 ./configure들도 모두 --prefix=/usr/local를 붙여 실행하는 것이 좋습니다. 이것은 패키지 배포본은 /usr/include, /usr/lib에, 개인적으로 autoconf로 설치한것은 /usr/local 아래에 설치되게 됩니다.)
  • cygwin에서 directx를 사용할 수 있습니다. (SDL 설치시 directdraw wrapper를 사용하시려면 필수가 되겠죠) w32api 패키지를 설치하면 기본적으로 dx관련 라이브러리 화일들이 설치되지만, 아쉽게도 헤더화일은 배포본에 포함되어있지 않은 것을 알수 있습니다. (아마도 저작권 문제지 않을까...)
    1. [http]Directx SDK 8.0a를 다운 받습니다. (100메가가 조금 넘습니다. :( ) -> 여기에서 아래 헤더화일만 복사한 것 입니다. @w32api_dx8a_headers.zip (103.63 KB)
    2. 알집같은 것으로 열어서 include 디렉토리에서 ddraw.h, dinput.h, dsound.h d3dtypes.h를 /usr/include/w32api에 복사합니다. (/lib/w32api에 보면 d3d관련으로 추측되는 라이브러리들이 있습니다. 아마도 8.0까지 지원하는 것 같은데... 확실하지 않아서리...누가좀 알려주세요.. :) )
    3. 끝입니다. SDL과 같은 라이브러리를 컴파일 해봅니다. (./configure만 해봐도 directx header and library = yes 로 되는 것을 알 수 있습니다.)

[�몄쭛]

MinGW를 Cygwin과 사용하려면? #

cygwin으로 컴파일한 실행화일은 항상 cygwin1.dll을 필요로 합니다. 헌데, 이 dll을 포함한 어플리케이션은 상용으로 판매하려면 redhat사로부터 라이센스를 구입해야하므로 회사에서 쓰기에는 참 깝깝해지죠. 그래서 mingw 배포본에 포함된 gcc 및 binutil을 사용하여 컴파일하는 방법도 존재하지만, cygwin의 막강하고 깔끔한 bash 에뮬 개발환경이 부럽게 됩니다. 다음은 cygwin에서 mingw를 사용하여 "라이센스로부터 해방된" 실행화일을 만들기위한 설정들입니다.

[�몄쭛]

첫번째 방법 : 같이 깔기 #

cygwin 개발환경을 사용하려고 하고 cygwin 독립적인 실행화일을 생성하고자 하는데 있어서, "-mno-cygwin"보다 더 간단한 방법이 존재합니다. 단지 Cygwin과 Mingw 배포본을 각각 별도의 디렉토리(예를 들면 "C:\CYGWIN"과"C:\MINGW")에 설치한 후, 여러분의 PATH 환경변수에서 Mingw 설치 디렉토리의 /bin 디렉토리를 cygwin 설치 디렉토리의 /bin보다 앞에 오게 설정합니다. (예를 들면, "PATH=%PATH%;C:\MINGW\BIN;C:\CYGWIN\BIN"). 이것은 원하는 모든 UNIX 툴을 이용할 수 있으면서 gcc는 mingw 버전을 사용하는 것을 보장합니다. (어~ 알고 있는데~ 하시는 분은 그냥 넘어가시길... :) )

cygwin상의 "/etc/profile"화일안을 보면 시스템 수준의 경로가 "/usr/local/bin", "/usr/bin", "/bin" 순으로 지정되어있다는 것을 명심하시기 바랍니다. 따라서, 윈도우즈 경로를 설정한 것으로는 충분하지 않습니다... 즉, cygwin 환경내에서도 mingw /bin 경로가 앞에 설정되도록 수정해야합니다. ("/etc/profile"을 수정하거나 수동으로 설정하는지 선택하시기 바랍니다).

[�몄쭛]

두번째 방법 : gcc-mingw32 패키지 이용하기 #

앞의 방법은 몇몇 라이브러리에서는 충돌이 일어납니다. (순수 ANSI C 기반이라면 괜찮지만, win32용 개발이거나 directx관련 코드 컴파일시에는 문제가 생기더군요.) 그래서, redpixel은 다음과 같은 방법을 추천합니다. (한달전에 약 2일정도에 걸친 엄청난 삽질을 통해 알아냈지만 의외로 방법은 간단하더군요.)

우선 gcc-mingw32 패키지를 cygwin setup을 통하여 설치합니다. 그런다음 /usr/include/mingw와 /usr/lib/mingw 디렉토리가 생겼는지 확인합니다.

끝입니다. :) 이제부터는 모든 컴파일, 링크때마다 항상 gcc, g++ 뒤에 --target=i686-pc-mingw32 -mno-cygwin 옵션을 붙이면 mingw로 컴파일됩니다. 여기에 주의할 점은 반드시 gcc 다음에 이어서 적어야한다는 점이며, 가급적 패키지가 아닌 라이브러리들은 /usr/local 위에 설치해야한다는 점입니다. (autoconf기반의 프로젝트라면 ./configure --target=i686-pc-mingw32 라고 적으면 mingw기반의 makefile을 만들어 냅니다.) 이제 cygwin1.dll을 사용하지 않는다는 것을 알려면 다음과 같이 해봅니다.

objdump -p 컴파일된실행화일명 | grep "DLL Name"

그러면 다음과 비슷한 출력이 나옵니다.

       DLL Name: msvcrt.dll
       DLL Name: msvcrt.dll
       DLL Name: KERNEL32.dll

여기에 cygwin1.dll이 껴있으면 mingw를 사용하지 않고 cygwin기반으로 컴파일되었다는 뜻입니다. 주의할 점은 이렇게 컴파일해서 실행하려면, 템플릿 기반인 boost같은 라이브러리를 제외하고는 모든 /usr/local 아래에 있는 사용자 라이브러리들을 재컴파일 해서 사용해야만 합니다. 즉, mingw32기반의 obj코드와 cygwin기반의 obj는 호환이 되질 않더군요.

[�몄쭛]

세번째 방법 : gcc-mingw 패키지 -> mingw32 전체 복사 #

앞서의 방법에 대한 보충입니다. 두번째 방법과 같이 gcc-mingw 패키지를 설치하고 mingw32 배포본도 적당한 디렉토리에 설치합니다. 그다음에는 다음과 같이 모든 mingw 라이브러리와 헤더화일을 복사합니다.

cp /cygdrive/c/mingw/include /usr/include/mingw
cp /cygdrive/c/mingw/lib /usr/lib/mingw
중간에 gl 디렉토리 관련해서 복사를 못한다고 나오면, /usr/include/mingw 디렉토리에 있는 gl디렉토리 하드링크를 삭제하고 복사하면 됩니다. 조금 귀찮긴 하지만 이렇게 하면 컴파일할때 -mno-cygwin 옵션만 붙이면 완벽히 cygwin 독립적인 코드가 생성됩니다. }}}

아참, 마지막으로 혹시나 자신의 라이브러리를 빌드할 생각이시라면 msys+mingw32를 설치하고 여기서 빌드한 후 /usr/local로 복사해 사용하는 방법도 좋습니다. msys 배포본에도 autotool은 지원하지 않지만 gmake는 지원하고 있고 위와 같이 cygwin을 설치하면 완전히 mingw와 호환되므로 좋더군요. _ SDL같은 경우는 cygwin에서 그냥 빌드해도 cygwin1.dll에 대한 의존성이 없도록 빌드하지만, 대부분의 오픈소스 라이브러리들(ACE조차도!)은 가급적 msys에서 빌드해서 사용하는 것이 좋습니다.

부디 제가 겪었던 삽질이 줄어들길 바랍니다. :)

==> http://www.redwiki.net/wiki/wiki.php/Cygwin 참조

'It's Code' 카테고리의 다른 글

FILE IO  (0) 2008.09.08
리눅스 커널 소스 디렉토리  (0) 2007.08.17
리눅스 커널 소스 (The Linux Kernel Sources)  (0) 2007.08.17
Java classpath 관리하기  (0) 2007.08.16
JAVAScript TIP  (0) 2007.08.10

 

All About: File I/O in C++

By Ilia Yordanov, loobian@cpp-home.com

www.cpp-home.com ; C++ Resources

 

This tutorial may not be republished without a written permission from the author!

 

Introduction


This tutorial will start with the very basis of File I/O (Input/Output) in C++. After that, I will look into aspects that are more advanced, showing you some tricks, and describing useful functions.

You need to have good understanding of C++, otherwise this tutorial will be unfamiliar and not useful to you!

 

 

Your Very First Program

 

I will first write the code, and after that, I will explain it line by line.

The first program, will create a file, and put some text into it.

 

#include <fstream>

using namespace std;

 

int main()

{

ofstream SaveFile("cpp-home.txt");

SaveFile << "Hello World, from www.cpp-home.com and Loobian!";

SaveFile.close();

return 0;

}

 

Only that? Yes! This program will create the file cpp-home.txt in the directory from where you are executing it, and will put “Hello World, from www.cpp-home.com and Loobian!” into it.

Here is what every line means:

#include <fstream> - You need to include this file in order to use C++’s functions for File I/O.

In this file, are declared several classes, including ifstream, ofstream and fstream, which are all derived from istream and ostream.

 

ofstream SaveFile(“cpp-home.txt”);

1)ofstream means “output file stream”. It creates a handle for a stream to write in a file.

2)SaveFile – that’s the name of the handle. You can pick whatever you want!

3)(“cpp-home.txt”); - opens the file cpp-home.txt, which should be placed in the directory from where you execute the program. If such a file does not exists, it will be created for you, so you don’t need to worry about that!

Now, let’s look a bit deeper. First, I’d like to mention that ofstream is a class. So, ofstream SaveFile(“cpp-home.txt”); creates an object from this class. What we pass in the brackets, as parameter, is actually what we pass to the constructor. It is the name of the file. So, to summarize: we create an object from class ofstream, and we pass the name of the file we want to create, as an argument to the class’ constructor. There are other things, too, that we can pass, but I will look into that, later.

 

SaveFile << “Hello World, from www.cpp-home.com and Loobian!”; - “<<” looks familiar? Yes, you’ve seen it in cout <<. This (“<<”) is a predefined operator. Anyway, what this line makes, is to put the text above in the file. As mentioned before, SaveFile is a handle to the opened file stream. So, we write the handle name, << and after it we write the text in inverted commas. If we want to pass variables instead of text in inverted commas, just pass it as a regular use of the cout <<. This way:

SaveFile << variablename;

That’s it!

 

SaveFile.close(); - As we have opened the stream, when we finish using it, we have to close it. SaveFile is an object from class ofstream, and this class (ofstream) has a function that closes the stream. That is the close() function. So, we just write the name of the handle, dot and close(), in order to close the file stream!
Notice: Once you have closed the file, you can’t access it anymore, until you open it again.

 

That’s the simplest program, to write in a file. It’s really easy! But as you will see later in this tutorial, there are more things to learn!

 

Reading A File

You saw how to write into a file. Now, when we have cpp-home.txt, we will read it, and display it on the screen.
First, I’d like to mention, that there are several ways to read a file. I will tell you about all of them (all I know) later. For now, I will show you the best way (in my mind).

As you are used already- I will first write the code, and after that, I will comment it in details.

#include <fstream.h>

 

void main() //the program starts here

{

ifstream OpenFile("cpp-home.txt");

 

char ch;

while(!OpenFile.eof())

{

OpenFile.get(ch);

cout << ch;

}

OpenFile.close();

}

 

You should already know what the first line is. So, let me explain you the rest.

 

ifstream OpenFile(“cpp-home.txt”) – I suppose this seems a bit more familiar to you, already! ifstream means “input file stream”. In the previous program, it was ofstream, which means “output file stream”. The previous program is to write a file, that’s why it was “output”. But this program is to read from a file, that’s why it is “input”. The rest of the code on this line, should be familiar to you. OpenFile is the object from class ifstream, which will handle the input file stream. And in the inverted commas, is the name of the file to open.

Notice that that there is no check whether the file exists! I will show you how to check that, later!

 

char ch;- Declares an array of type char. Just to remind you- such arrays can hold just one sign from the ASCII table.

 

while(!OpenFile.eof()) – The function eof() returns a nonzero value if the end of the file has been reached. So, we make a while loop, that will loop until we reach the end of the file. So, we will get through the whole file, so that we can read it!

 

OpenFile.get(ch); - OpenFile is the object from class ifstream. This class declares a function called get(). So, we can use this function, as long as we have an object. The get()function extracts a single character from the stream and returns it. In this example, the get() function takes just one parameter- the variable name, where to put the read character. So, after calling OpenFile.get(ch) it will read one character from the stream OpenFile, and will put this character into the variable ch.

Notice: If you call this function for a second time, it will read the next character, but not the same one! You will learn why this happens, later.

That’s why, we loop until we reach the end of the file! And every time we loop, we read one character and put it into ch.

 

cout <<ch; - Display ch, which has the read character.

 

File.close(); - As we have opened the file stream, we need to close it. Use the close() function, to close it! Just as in the previous program!

Notice: Once you have closed the file, you can’t access it anymore, until you open it again.


That’s all! I hope you understood my comments! When you compile and run this program, it should output:

“Hello World, from www.cpp-home.com and Loobian!”

 

 

Managing I/O streamsIn this chapter, I will mention about some useful functions. I will also show you how to open file to read and write in the same time. I will show you, also, other ways to open a file; how to check if opening was successful or not. So- read on!

 

So far, I have showed to you, just one way to open a file, either for reading, either for writing. But it can be opened another way, too! So far, you should be aware of this method:

 

ifstream OpenFile(“cpp-home.txt”);

 

Well, this is not the only way! As mentioned before, the above code creates an object from class ifstream, and passes the name of the file to be opened to its constructor. But in fact, there are several overloaded constructors, which can take more than one parameter.Also, there is function open() that can do the same job. Here is an example of the above code, but using the open() function:

 

ifstream OpenFile;

OpenFile.open(“cpp-home.txt”);

 

What is the difference you ask? Well, I made several tests, and found no difference! Just if you want to create a file handle, but don’t want to specify the file name immediately, you can specify it later with the function open(). And by the way, other use of open() is for example if you open a file, then close it, and using the same file handle open another file. This way, you will need the open() function.
Consider the following code example:

 

#include <fstream.h>

 

void read(ifstream &T) //pass the file stream to the function

{

//the method to read a file, that I showed you before

char ch;

 

while(!T.eof())

{

T.get(ch);

cout << ch;

}

 

cout << endl << "--------" << endl;

}

 

void main()

{

ifstream T("file1.txt");

read(T);

T.close();

 

T.open("file2.txt");

read(T);

T.close();

}

 

So, as long as file1.txt and file2.txt exists and has some text into, you will see it!

Now, it’s time to show you that the file name is not the only parameter that you can pass to the
open()function or the constructor (it’s the same). Here is a prototype:

 

ifstream OpenFile(char *filename, int open_mode);

 

You should know that filename is the name of the file (a string). The new here is the open mode. The value of open_mode defines how to be opened the file. Here is a table of the open modes:

 

Name

Description

ios::in

Open file to read

ios::out

Open file to write

ios::app

All the date you write, is put at the end of the file. It calls ios::out

ios::ate

All the date you write, is put at the end of the file. It does not call ios::out

ios::trunc

Deletes all previous content in the file. (empties the file)

ios::nocreate

If the file does not exists, opening it with the open() function gets impossible.

ios::noreplace

If the file exists, trying to open it with the open() function, returns an error.

ios::binary

Opens the file in binary mode.

In fact, all these values are intconstants from an enumerated type. But for making your life easier, you can use them as you see them in the table.
Here is an example on how to use the open modes:
#include <fstream.h>

 

void main()

{

ofstream SaveFile("file1.txt", ios::ate);

 

SaveFile << "That's new!\n";

 

SaveFile.close();

}

 

As you see in the table, using ios::ate will write at the end of the file. If I didn’t use it, the file will be overwritten, but as I use it, I just add text to it. So, if file1.txt has this text:

 

Hi! This is test from www.cpp-home.com!


Running the above code, will add “
That’s new!” to it, so it will look this way:

 

Hi! This is test from www.cpp-home.com!That’s new!

 

If you want to set more than one open mode, just use the OR operator- |. This way:

 

ios::ate | ios::binary

 

I hope you now understand what open modes are!

Now, it’s time to show you something really useful! I bet you didn’t know that you could create a file stream handle, which you can use to read/write file, in the same time! Here is how it works:

 

fstream File(“cpp-home.txt”,ios::in | ios::out);

In fact, that is only the declaration. I will show you a code example, just several lines bellow. But I first want to mention some things you should know.
The code line above, creates a file stream handle, named “
File”. As you know, this is an object from class fstream. When using fstream, you should specify ios::in and ios::out as open modes. This way, you can read from the file, and write in it, in the same time, without creating new file handles. Well, of course, you can only read or write. Then you should use either ios::ineither ios::out, but if you are going to do it this way, why don’t you do it either with ifstream, either with ofstream?
Here is the code example:

 

#include <fstream.h>

 

void main()
{

fstream File("test.txt",ios::in | ios::out);

 

File << "Hi!"; //put “Hi!” in the file

 

static char str[10]; //when using static, the array is automatically

//initialized, and very cell NULLed

File.seekg(ios::beg); //get back to the beginning of the file

//this function is explained a bit later

File >> str;

cout << str << endl;

 

File.close();
}

 

Okay, there are some new things here, so I will explain line by line:

 

fstream File(“test.txt”, ios::in | ios::out); - This line, creates an object from class fstream. At the time of execution, the program opens the file test.txt in read/write mode. This means, that you can read from the file, and put data into it, at the same time.

 

File << “Hi!”; - I beg you know what this is!

static char str[10]; - This makes a char array with 10 cells. I suppose static may be unfamiliar to you. If so- ignore it. It just initializes the array when at the time of creation.

 

File.seekg(ios::beg); - Okay, I want you to understand what this really do, so I will start with something a bit off-topic, but important.
Remember that? :

while(!OpenFile.eof())

{

OpenFile.get(ch);

cout << ch;

}

Did you ever wonder what really happens there? Yes or no, I will explain you. This is a while loop, that will loop until you reach the end of the file. But how do the loop know if the end of the file is reached? Well, when you read the file, there is something like an inside-pointer, that shows where you are up to, with the reading (and writing, too). It is like the cursor in Notepad. And every time you call OpenFile.get(ch) it returns the current character to the ch variable, and moves the inside-pointer one character after that, so that the next time this function is called, it will return the next character. And this repeats, until you reach the end of the file.

So, let’s get back to the code line. The function seekg()will put the inside-pointer to a specific place (specified by you). You can use:

ios::beg– to put it in the beginning of the file

ios::end- to put it at the end of the file

Or you can also set the number of characters to go back or after. For example, if you want to go 5 characters back, you should write:

 

File.seekg(-5);

 

If you want to go 40 character after, just write:

 

File.seekg(40);I also have to mention, that the seekg()function is overloaded, and it can take two parameters, too. The other version is this one:

 

File.seekg(-5,ios::end);

 

In this example, you will be able to read the last 4 characters of the text, because:

1)You go to the end (ios::end)

2)You go 5 characters before the end (-5)

Why you will read 4 but not 5 characters? Well, just assume that one is lost, because the last thing in the file is not a character nor white space.It is just position.

You now may be wondering why did I use this function? Well, after I put “Hi!” in the file, the inside-pointer was set after it… at the end of the file. And as I want to read the file, I have nothing to read after the end, so I have to put the inside-pointer at the beginning. And that is exactly what this function does.

 

File >> str; - That’s new, too! Well, I believe this line reminds you of cin >> . I fact, it has much to do with it. This line reads one word from the file, and puts it into the specified array.
For example, if the file has this text:

 

Hi! Do you know me?

Using File >> str, will put just “Hi!” to the str array. You should have noticed, that it actually reads until it meets a white space.

And as what I put in the file was “Hi!” I don’t need to do a while loop, that takes more time to code. That’s why I used this way. By the way, in the while loop for reading, that I used so far, the program reads the file, char by char. But you can read it word by word, this way:

 

char str[30]; //the word can’t be more than 30 characters long

while(!OpenFile.eof())

{

OpenFile >> str;

cout << str;

}

You can also read it line by line, this way:

 

char line[100]; //a whole line will be stored here
while(!OpenFile.eof())
{

OpenFile.getline(line,100); //where 100 is the size of the array

cout << line << endl;
}

 

You now might be wondering which way to use? Well, I’d recommend you to use the line-by-line one, or the first that I mentioned- the one which reads char-by-char. The one that reads word-by-word is not good idea, because it won’t read the new line. So if you have new line in the file, it will not display it as a new line, but will append the text to the existing one. But using getline() or get()will show you the file, just as it is!

 


Now, I will show you how to check if the file opening was successful or not. In fact, there are few good ways to check for that, and I will mention them. Notice that where there is X, it can be either “o”, either “i” either nothing (it will then be fstream object).


Example 1: The most usual way

Xfstream File(“cpp-home.txt”);

if (!File)
{

cout << “Error opening the file! Aborting…\n”;

exit(1);
}

 

Example 2: If the file is created, return an error

 

ofstream File("unexisting.txt", ios::nocreate);

 

if(!File)

{

cout << “Error opening the file! Aborting…\n”;

exit(1);

}


Example 3: Using the fail() function
ofstream File("filer.txt", ios::nocreate);

 

if(File.fail())

{

cout << “Error opening the file! Aborting…\n”;

exit(1);

}

The new in
Example 3, is the fail() function. It returns a nonzero value if any I/O error (not end of file) has occurred.

 

I would also like to mention about something , that I find to be very useful! For example, if you have created a file stream, but you haven’t opened a file. This way:

 

ifstream File; //it could also be ofstream


This way, we have a handle, but we still have not opened the file. If you want to open it later, it can be done with the
open() function, which I already covered in this tutorial. But if anywhere in your program, you need to know if currently there is an opened file, you can check it with the function is_open(). It retunrs 0 (false) if a file is not opened, and 1(true) if there is an opened file. For example:

 

ofstream File1;

File1.open("file1.txt");

cout << File1.is_open() << endl;

The code above, will return 1, as we open a file (on line 2). But the code bellow will return 0, because we don’t open a file, but just create a file stream handle:

 

ofstream File1;

cout << File1.is_open() << endl;


Okay, enough on this topic.

 

Checking the I/O status- Flags

 

I won’t be explaining what flags are. But even if you don’t know about them, reading this chapter to the end may give you some idea, and I believe you will understand the theory. Even so, if you don’t know about the flags in C++, I recommend you to find some reading on this subject.
Okay, let’s start!


The Input/Outputsystem in C++, holds information about the result of every I/O operation. The current status is kept in an object from type
io_state, which is an enumerated type (just like open_mode)that has the following values:

godbit

No errors.

eofbit

End of file has been reached

failbit

Non-fatal I/O error

badbit

Fatal I/O error

 

There are two ways, to receive information about the I/O status. One of them is by calling the function rdstate(). It returns the current status of the error-flags (the above mentioned). For example, the rdstate() function will return goodbit if there were no errors.

The other way to check the I/O status is by using any of the following function:

 

bool bad();

bool eof(); //remember this one? “Read until the end of the file has been reached!”

bool fail(); //and this one, too… Check if the file opening was successfull

bool good();

 

The function bad()returns true, if the badbitflag is up. The fail()function returns trueif the failbitflag is up. The good()function returns trueif there were no errors (the goodbit flag is up). And the eof() function returns true if the end of the file has been reached (the eofbit flag is up).

If an error occurred, you will have to clear it if you want your program to continue properly. To do so, use the clear() function, which takes one parameter. The parameter should be the flag you want to put to be the current status. If you want your program to continue “on clear”, just put ios::goodbit as parameter. But notice that the clear() function can take any flag as parameter. You will see that in the code examples bellow.

I will now show you some example code that will confirm your knowledge.


Example 1: Simple status check

 

//Replace FileStream with the name of the file stream handle

if(FileStream.rdstate() == ios::eofbit)

cout << "End of file!\n";

if(FileStream.rdstate() == ios::badbit)

cout << "Fatal I/O error!\n";

if(FileStream.rdstate() == ios::failbit)

cout << "Non-fatal I/O error!\n";

if(FileStream.rdstate() == ios::goodbit)

cout << "No errors!\n";


Example 2: The clear() function

 

#include <fstream.h>

 

void main()

{

ofstream File1("file2.txt"); //create file2.txt

File1.close();

 

//this bellow, will return error, because I use the ios::noreplace

//open_mode, which returns error if the file already exists.

ofstream Test("file2.txt",ios::noreplace);

 

//The error that the last line returned is ios::failbit, so let’s show it

if(Test.rdstate() == ios::failbit)

cout << "Error...!\n";

 

Test.clear(ios::goodbit); //set the current status to ios::goodbit

 

if(Test.rdstate() == ios::goodbit) //check if it was set correctly

cout << "Fine!\n";

 

Test.clear(ios::eofbit); //set it to ios::eofbit. Useless.

 

if(Test.rdstate() == ios::eofbit) //and check again if it is this flag indeed

cout << "EOF!\n";

 

Test.close();

 

}

 

Instead using flags, you can use a function that actually does the same- checks if specific flag is up. The functions were mentioned before, so I won’t mention them again. If you are not sure how to use them, just check out the place of the tutorial, where I showed few ways to check if file opening was successful or not. There, I used the fail() function.

 

Dealing with Binary files

 

Although, the files with formatted text (all that I talked about so far) are very useful, sometimes you may need to work with unformatted files- binary files. They have the same look as your program itself, and it is much different from what comes after using the <<and >>operators. The functions that give you the possibility to write/read unformatted files are get()and put(). To read a byte, you can use get() and to write a byte, use put().You should remember of get()… I have already used it before. You wonder why even using it, the file looks formatted? Well, it is because I used the << and >>operators, I suppose.get() and put() both take one parameter- a char variable or character.
If you want to read/write whole blocks of data, then you can use the
read() and write() functions. Their prototypes are:

 

istream &read(char *buf, streamsize num);

ostream &write(const char *buf, streamsize num);


For the
read()function, bufshould be an array of chars, where the read block of data will be put. For the write() function, buf is an array of chars, where is the data you want to save in the file. For the both functions, num is a number, that defines the amount of data (in symbols) to be read/written.

If you reach the end of the file, before you have read “
num” symbols, you can see how many symbols were read by calling the function gcount(). This function returns the number of read symbols for the last unformatted input operation.
And before going to the code examples, I have to add, that if you want to open a file for binary read/write, you should pass
ios::binary as an open mode.

Now, let me give you some code examples, so that you can see how stuff works.


Example 1: Using get() and put()

 

#include <fstream.h>

 

void main()

{

fstream File("test_file.txt",ios::out | ios::in | ios::binary);

 

char ch;

ch='o';

 

File.put(ch); //put the content of ch to the file

 

File.seekg(ios::beg); //go to the beginning of the file

 

File.get(ch); //read one character

 

cout << ch << endl; //display it

 

File.close();

}

 

Example 2: Using read() and write()

 

#include <fstream.h>

#include <string.h>

 

void main()

{

fstream File("test_file.txt",ios::out | ios::in | ios::binary);

 

char arr[13];

strcpy(arr,"Hello World!"); //put Hello World! into the array

 

File.write(arr,5); //put the first 5 symbols into the file- "Hello"

 

File.seekg(ios::beg); //go to the beginning of the file

 

static char read_array[10]; //I will put the read data, here

 

File.read(read_array,3); //read the first 3 symbols- "Hel"

 

cout << read_array << endl; //display them

 

File.close();

}

 

 

Some useful functions

 

tellg() – Retunrs an int type, that shows the current position of the inside-pointer. This one works only when you read a file. Example:

 

#include <fstream.h>

 

void main()

{

//if we have "Hello" in test_file.txt

ifstream File("test_file.txt");

 

char arr[10];

 

File.read(arr,10);

 

//this should return 5, as Hello is 5 characters long

cout << File.tellg() << endl;

 

File.close();

}

 

tellp() – The same as tellg() but used when we write in a file. To summarize: when we read a file, and we want to get the current position of the inside-pointer, we should use tellg(). When we write in a file, and we want to get the current position of the inside-pointer, we should use tellp(). I won’t show a code example for tellp() as it works absolutely the same way as tellg().

 

seekp() – Remember seekg()? I used it, when I was reading a file, and I wanted to go to specified position. seekp() is the same, but it is used when you write in a file. For example, if I read a file, and I want to get 3 characters back from the current position, I should call FileHandle.seekg(-3). But if I write in a file, and for example, I want to overwrite the last 5 characters, I have to go back 5 characters. Then, I should use FileHandle.seekp(-5).

 

ignore() – Used when reading a file. If you want to ignore certain amount of characters, just use this function. In fact, you can use seekg() instead, but the ignore() function has one advantage- you can specify a delimiter rule, where the ignore()function will stop. The prototype is:

 

istream& ignore( int nCount, delimiter );

 

Where nCount is the amount of characters to be ignored and delimiter is what its name says. It can be EOF if you want to stop at the end of the file. This way, this function is the same as seekg(). But it can also be ‘\n’ for example, which will stop on the first new line. Here is example:

 

#include <fstream.h>

 

void main()

{

//if we have "Hello World" in test_file.txt

ifstream File("test_file.txt");

 

static char arr[10];

 

//stop on the 6th symbol, if you don't meet "l"

//in case you meet "l"- stop there

File.ignore(6,'l');

 

File.read(arr,10);

 

cout << arr << endl; //it should display "lo World!"

 

File.close();

 

}

 

getline() – I have already mentioned about this one. But there is something I didn’t tell you about. This function can be used to read line-by-line, but it can be set to stop reading if it met a certain symbol. Here is how you should pass the parameters to it:

 

getline(array,array_size,delim);


And here is a code example:

 

#include <fstream.h>

 

void main()

{

//if we have "Hello World" in test_file.txt

ifstream File("test_file.txt");

 

static char arr[10];

 

/*read, until one of these happens:

1) You have read 10

2) You met the letter "o"

3) There is new line

*/

File.getline(arr,10,'o');

 

cout << arr << endl; //it should display "Hell"

File.close();

}

 

peek() This function will return the next character from an input file stream, but it won’t move the inside-pointer. I hope you remember, that get() for example, returns the next character in the stream, and after that, it moves the inside-pointer, so that the next time you call the get() function, it will return the next character, but not the same one. Well, using peek()will return a character, but it won’t move the cursor. So, if you call the peek() function, two times in succession, it will return a same character. Consider the following code example:

 

#include <fstream.h>

 

void main()

{

//if we have "Hello World" in test_file.txt

ifstream File("test_file.txt");

 

char ch;

 

File.get(ch);

cout << ch << endl; //should display "H"

 

cout <<char(File.peek()) << endl; //should display "e"

cout <<char(File.peek()) << endl; //should display "e" again

 

File.get(ch);

cout << ch << endl; //should display "e" again

File.close();

 

}

 

And by the way, I forgot to mention- the peek() function actually returns the ASCII code of the char, but not the char itself. So, if you want to see the character itself, you have to call it the way I did, in the example.

 


_unlink() – Deletes a file. Include
io.h in your program, if you are going to use this function. Here is a code example:

 

#include <fstream.h>

#include <io.h>

 

void main()

{

ofstream File;

 

File.open("delete_test.txt"); //creates the file

File.close();

 

_unlink("delete_test.txt"); //deletes the file

 

//tries to open the file, but if it does not exists

//the function will rreturn error ios::failbit

File.open("delete_test.txt",ios::nocreate);

 

//see if it returned it

if(File.rdstate() == ios::failbit)

cout << "Error...!\n"; //yup,it did


File.close();

 

}

 

putback() – This function will return the last read character, and will move the inside-pointer, one with –1 char. In other words, if you use get()to read a char, then use putback(), it will show you the same character, but it will set the inside-pointer with –1char, so the next time you call get() again, it will again show you the same character. Here is a code example:

 

#include <fstream.h>

 

void main()

{

//test_file.txt should have this text- "Hello World"

ifstream File("test_file.txt");

 

char ch;

 

File.get(ch);

 

cout << ch << endl; //it will display "H"

 

File.putback(ch);

cout << ch << endl; //it will again display "H"

 

File.get(ch);

cout << ch << endl; //it will display "H" again

 

File.close();

}

 

flush() – When dealing with the output file stream, the date you save in the file, is not actually immediately saved in it. There is a buffer, where it is kept, and when the buffer gets filled, then the data is put in the real file (on your disk). Then the buffer is emptied, and so on.
But if you want to save the data from the buffer, even if the buffer is still not full, use the
flush()function. Just call it this way- FileHandle.flush(). And the data from the buffer will be put in the physical file, and the buffer will be emptied.
And something in addition (advanced)- The
flush() function calls the sync() function of the associated streambuf. (The last sentence is from MSDN).


Conslusion

 

Well, I hope you can now do your File I/O programs! I covered everything I remembered of! I think it is more than enough for most of you! Even so, there are things I didn’t cover… but things I think you will never need. So, if you need even more advanced information on specific topic, search the web. Try google.com for example! But don’t ask me! I won’t answer any e-mails, asking me how to do certain program or else.


If you liked this tutorial, or not, I would be very happy to see your comments, so feel free to drop me a line at loobian@cpp-home.com

For more C++ tutorials, visit www.cpp-home.com

 

This tutorial may not be republished without a written permission from the author!

 

3 April 2002,

Ilia Yordanov

 

이 글은 스프링노트에서 작성되었습니다.

'It's Code' 카테고리의 다른 글

Cygwin+MinGW32 참고  (0) 2009.06.26
리눅스 커널 소스 디렉토리  (0) 2007.08.17
리눅스 커널 소스 (The Linux Kernel Sources)  (0) 2007.08.17
Java classpath 관리하기  (0) 2007.08.16
JAVAScript TIP  (0) 2007.08.10

Contents

1 GCC(12)에 대해서
2 GCC의 기본 옵션들
2.1 GCC 특징과 지원 환경 확인
2.2 Object 파일의 생성
2.3 실행파일 생성
3 PreProcess 에 대해서
3.1 proprocessed 출력
3.2 매크로 치환 (#define)
4 Assembly(12) 코드 생성
5 라이브러리 사용하기
6 profiling 및 디버깅
7 컴파일 시간 모니터링
8 GCC 최적화
8.1 관련문서


1 GCC(12)에 대해서

GNU C compiler는 GNU system을 이루고 있는 중요한 부분중 하나이며 Richard Stallman에 의해서 만들어졌다. 최초에는 단지 C코드만을 컴파일 할 수 있었기 때문에 GNU C Compiler 로 불리웠었다. 그러던 것이 다른 많은 지원자들에 의해서 C++, Fortran, Ada, Java 등의 컴파일도 지원할 수 있게 되면서 GNU Compiler Collection으로 이름을 변경하게 된다. 이 문서는 단지 C언어코드에 대한 컴파이러로써의 GCC의 사용법에 대해서 다루도록 할 것이다.

GCC는 Linux뿐 만 아니라 FreeBSD, NetBSD, OpenBSD, SunOS, HP-UX, AIX등의 *Nix운영체제 그리고 Cygwin, MingW32의 윈도우환경에서도 사용할 수 있다. 플렛폼 역시 Intel x86, AMD x86-64, Alpha, SPARC 등에서 사용가능 하다. 이러한 다양한 환경에서의 운용가능성 때문에 다양한 플렛폼에서 작동을 해야 하는 프로그램을 만들기 위한 목적으로 유용하게 사용할 수 있다.

2 GCC의 기본 옵션들

GCC가 여러분의 시스템에 설치되어 있다고 가정하고 글을 쓰도록 하겠다.

2.1 GCC 특징과 지원 환경 확인

[root@localhost include]# gcc -v
Reading specs from /usr/lib/gcc/i386-redhat-linux/3.4.4/specs
Configured with: ../configure --prefix=/usr --mandir=/usr/share/man
--infodir=/usr/share/info --enable-shared --enable-threads=posix --disable-checking
--with-system-zlib --enable-__cxa_atexit
--disable-libunwind-exceptions --enable-java-awt=gtk --host=i386-redhat-linux
Thread model: posix
gcc version 3.4.4 20050721 (Red Hat 3.4.4-2)

-v옵션을 이용하면 GCC와 관련된 다양한 정보들을 확인할 수 있다. 위의 정보에서 현재 설치된 gcc는 POSIX(12) 쓰레드 모델을 지원하고 있음을 알 수 있다. 이는 다중쓰레드를 지원하는 애플리케이션의 작성이 가능하다는 것을 의미한다.

이제 하나의 헤더파일과 하나의 소스코드파일로 이루어진 간단한 프로그램을 만들고 컴파일을 해보면서 GCC에서 지원하는 다양한 컴파일 옵션들에 대해서 알아보도록 하겠다.

// helloworld.h
#define COUNT 2

static char hello[] = "hello world";
// helloworld.c
#include <stdio.h>
#include "helloworld.h"

int main()
{
int i;
for (i =0; i <= COUNT; i++)
{
printf("%s - %d\n", hello, i);
}
return 0;
}

2.2 Object 파일의 생성

[root@localhost ~]# gcc -v -c helloworld.c 
Reading specs from /usr/lib/gcc/i386-redhat-linux/3.4.4/specs
Configured with: ../configure --prefix=/usr --mandir=/usr/share/man --infodir=/u
sr/share/info --enable-shared --enable-threads=posix --disable-checking --with-s
ystem-zlib --enable-__cxa_atexit --disable-libunwind-exceptions --enable-java-aw
t=gtk --host=i386-redhat-linux
Thread model: posix
gcc version 3.4.4 20050721 (Red Hat 3.4.4-2)
/usr/libexec/gcc/i386-redhat-linux/3.4.4/cc1 -quiet -v helloworld.c -quiet -dum
pbase helloworld.c -auxbase helloworld -version -o /tmp/ccs8066J.s
ignoring nonexistent directory "/usr/lib/gcc/i386-redhat-linux/3.4.4/../../../..
/i386-redhat-linux/include"
#include "..." search starts here:
#include <...> search starts here:
/usr/local/include
/usr/lib/gcc/i386-redhat-linux/3.4.4/include
/usr/include
End of search list.
GNU C version 3.4.4 20050721 (Red Hat 3.4.4-2) (i386-redhat-linux)
compiled by GNU C version 3.4.4 20050721 (Red Hat 3.4.4-2).
GGC heuristics: --param ggc-min-expand=64 --param ggc-min-heapsize=64407
as -V -Qy -o helloworld.o /tmp/ccs8066J.s
GNU assembler version 2.15.92.0.2 (i386-redhat-linux) using BFD version 2.15.92.
0.2 20040927

위 결과에서 오브젝트 파일을 만들기 위해서 include 경로를 찾는 과정과 GNU assembler를 이용해서 컴파일을 하고 있음을 확인할 수 있다. 컴파일러는 원본파일로 부터 /tmp/ccs8066J.s 라는 어셈블리코드를 만들고, 이것을 as를 이용해서 helloworld.o라는 이름의 오브젝트 파일을 만들었다.

2.3 실행파일 생성

[root@localhost ~]# gcc -v -o helloworld helloworld.c 
...[생략]...
GNU C version 3.4.4 20050721 (Red Hat 3.4.4-2) (i386-redhat-linux)
compiled by GNU C version 3.4.4 20050721 (Red Hat 3.4.4-2).
GGC heuristics: --param ggc-min-expand=64 --param ggc-min-heapsize=64407
as -V -Qy -o /tmp/ccj9GxKi.o /tmp/ccGzRoCh.s
GNU assembler version 2.15.92.0.2 (i386-redhat-linux) using BFD version 2.15.92.0.2 20040927
/usr/libexec/gcc/i386-redhat-linux/3.4.4/collect2 --eh-frame-hdr -m elf_i386 -dynamic-linker
/lib/ld-linux.so.2 -o helloworld /usr/lib/gcc/i386-redhat-linux/3.4.4/../../../crt1.o /usr/lib/gcc/i386-redhat-linux/3.4.4/../../../crti.o
/usr/lib/gcc/i386-redhat-linux/3.4.4/crtbegin.o -L/usr/lib/gcc/i386-redhat-linux/3.4.4 -L/usr/lib/gcc/i386-redhat-linux/3.4.4
-L/usr/lib/gcc/i386-redhat-linux/3.4.4/../../.. /tmp/ccj9GxKi.o -lgcc
--as-needed -lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s
--no-as-needed /usr/lib/gcc/i386-redhat-linux/3.4.4/crtend.o /usr/lib/gcc/i386-redhat-linux/3.4.4/../../../crtn.o

-L 옵션을 이용해서 링크라이브러리의 경로를 찾고 있음을 알 수 있다. 여기에서는 기본적으로 지정된 경로에서만 라이브러리를 찾고 있는데, 별도로 -L 옵션을 줘서 다른 경로에서 라이브러리를 찾도록 할 수도 있다. 그리고 -l옵션을 이용해서 링크할 라이브러리를 지정하고 있음을 알 수 있다. -l로 지정된 라이브러리는 -L에 의해 지정된 경로에서 동일한 이름의 라이브러리가 있는지를 찾아서 링크하게 된다.

필요한 라이브러리들을 링크 시키고 나면, 완전한 실행파일이 만들어지게 된다.

3 PreProcess 에 대해서

전처리기로 불리우기도 하는 preprocessor는 C컴파일러에서 매우 중요한 역할을 담당한다. preprocessor은 #include, #ifdef, #pragma등 '#'으로 시작되는 코드를 분리해 낸다. 이들 코드는 컴파일이 이루어지기 전에, 매크로 치환, 조건부 컴파일 확인, 파일 첨가(include)등의 업무를 처리한다. 예를 들어 #define COUNT 2 라는 코드라인이 있다면, 이부분을 해석해서 소스코드에 있는 모든 COUNT를 2로 치환하는 일을 한다. preprocessor은 줄단위로 처리된다.

3.1 proprocessed 출력

# gcc -E helloworld.c > helloworld.preprocess

파일첨가, 매크로치환등 을 포함한 모든 preprocess 과정을 파일로 저장하고 있다. 편집기를 이용해서 파일을 열어보면 preprocess가 담당하는 일이 어떤건지를 대략적으로 이해할 수 있을 것이다. 실질적으로 C컴파일러는 preprocess가 완전히 끝난 상태인 helloworld.preprocess를 소스코드로 해서 컴파일을 수행하게 된다.

3.2 매크로 치환 (#define)

# gcc -E helloworld.c -dM | sort | less

모든 define 문을 처리하고 이를 sort 해서 보여주고 있다. 여기에서 GNUC, GNUC_MINOR, GNUC_PATCHLEVEL 등등 GCC버전과 관련된 정보들도 확인할 수 있다.

1

4 Assembly(12) 코드 생성

GCC는 이진코드를 만들기전에 C코드를 assembly 코드로 변환되고, 변환된 assembly가 해석되어서 이진코드가 만들어진다. -S 옵션을 이용하면, C코드가 어떤 assembly코드로 변환되는지를 확인할 수 있다.

# gcc -S helloworld.c

다음은 만들어진 assembly 코드다.

  .file "helloworld.c"
.data
.type hello, @object
.size hello, 12
hello:
.string "hello world"
.section .rodata
.LC0:
.string "%s - %d\n"
.text
.globl main
.type main, @function
main:
pushl %ebp
movl %esp, %ebp
subl $8, %esp
andl $-16, %esp
movl $0, %eax
addl $15, %eax
addl $15, %eax
shrl $4, %eax
sall $4, %eax
subl %eax, %esp
movl $0, -4(%ebp)
.L2:
cmpl $2, -4(%ebp)
jg .L3
subl $4, %esp
pushl -4(%ebp)
pushl $hello
pushl $.LC0
call printf
addl $16, %esp
leal -4(%ebp), %eax
incl (%eax)
jmp .L2
.L3:
movl $0, %eax
leave
ret
.size main, .-main
.section .note.GNU-stack,"",@progbits
.ident "GCC: (GNU) 3.4.4 20050721 (Red Hat 3.4.4-2)"

위의 어셈블리 코드에서 우리는 "hello world" 문자열이 읽기전용 데이터로 static하게 정의되어 있는걸 확인할 수 있다. .LC0 섹션은 printf의 인자를 보여주고 있다. 이 값들은 스택에 넣어지며 printf() 가 호출될 때, 읽어오게 된다. .L2 섹션은 loop를 위한 상태검사 코드가 들어간다. .L3 섹션은 함수가 끝난후 필요한 값을 리턴하고 종료하기 위한 코드가 들어간다. 이 어셈블리코드는 컴파일되어서 기계가 해석할 수 있는 이진코드로 변경되며, 이러한 컴파일 작업은 as가 담당하게 된다.

5 라이브러리 사용하기

만든 코드를 다른 프로그램에서 사용할 수 있도록 라이브러리 형태로 만들기를 원한다면 -fpic 옵션을 주면된다. fpic 옵션은 코드를 Position Independent Code (PIC) 로 만들어 준다. 라이브러리에 대한 자세한 내용은 library 만들기를 참고하기 바란다.

6 profiling 및 디버깅

profiling 는 프로그램의 실행시간시 코드의 어떤 부분이 가장 많은 자원을 차지하고 있는지 알아내기 위해서 사용하는 방법이다. profiling은 기본적으로 프로그램의 특정한 지점에 모니터링 코드를 입력하는 방식으로 이루어지며 GCC의 -pg옵션을 이용해서 모니터링 코드를 입력할 수 있다. profiling에 대한 자세한 내용은 Gprof를 이용한 프로그램 최적화를 참고하기 바란다.


일반적으로 프로그램을 디버깅 하기 위해서는 당연히 프로그램 실행 코드 외의 다른 코드가 삽입되어야 할 것이다. -g 옵션을 이용하면, gdb를 통해서 사용할 수 있는 여분의 디버깅 정보가 코드에 삽입된다. 여분의 코드가 삽입되기 때문에 실행파일의 크기가 커진다는 단점이 있지만, 어차피 디버깅이 끝난다음에는 디버깅코드를 제거하고 다시 컴파일하면 되므로 크게 문제될건 없다. 일단 디버깅 모드로 컴파일이 되면 모든 최적화(optimization) 플래그가 무효화 된다.

$ gcc -g -o helloworld helloworld.c #for adding debugging information
$ gcc -pg -o helloworld helloworld.c #for profiling

7 컴파일 시간 모니터링

완전한 하나의 실행파일을 만들기 위해서는 몇개의 단계를 거쳐야 함을 앞에서 배웠다. 그렇다면 각각의 단계에 어느정도의 시간이 소모되는지에 대한 정보를 알고 싶을 때가 있을 것이다. -time 옵션을 사용하면 각 단계별 소모시간을 알 수 있다.

[root@localhost ~]# gcc -time helloworld.c 
# cc1 0.07 0.01
# as 0.00 0.00
# collect2 0.06 0.01

-Q 옵션을 이용하면 좀더 자세한 소모시간을 확인할 수 있다. 더불어 컴파일이 어떠한 과정으로 이루어지는지도 확인 가능하다.

[root@localhost ~]# gcc -Q helloworld.c 
main

Execution times (seconds)
preprocessing : 0.02 (25%) usr 0.00 ( 0%) sys 0.05 (26%) wall
lexical analysis : 0.01 (12%) usr 0.01 (50%) sys 0.01 ( 5%) wall
parser : 0.01 (13%) usr 0.00 ( 0%) sys 0.04 (21%) wall
global alloc : 0.00 ( 0%) usr 0.00 ( 0%) sys 0.01 ( 5%) wall
TOTAL : 0.08 0.02 0.19

8 GCC 최적화

Before we move on to optimizations, we need to look at how a compiler is able to generate code for different platforms and different languages. The process of compilation has three components:
최적화에 대해서 설명하기 전에, 어떻게 컴파일러가 서로다른 플렛폼과 다른 언어에서 사용가능한 코드를 생성해내는지에 대해서 알아보도록 하겠다. 컴파일 프로세스는 다음의 3가지 컴포넌트들로 이루어진다.

  • Front End : 최초에 입력된 코드는 인간이 이해하기 쉬운 문자열로 이루어지는데, 이를 컴퓨터가 능률적으로 해석할 수 형식으로 만들어 줘야 한다. 일반적으로 컴파일러는 주어진 코드를 파싱을 해서 Tree 형식의 데이터 구조로 재구성한다. 이 단계는 최적화할 만한 여지가 거의 없으며, GCC가 지원하는 언어에 따라서 다른 방법으로 Tree를 구성하게 될 것이다.
  • Middle End : 코드생성을 위해서 구성된 트리구조에서 적절한 값들을 가지고 와서 재 배치시킨다. 이 단계는 모든 언어와 플렛폼에서 일반적인 과정을 거치게 되므로 역시 최적화의 요소는 거의 없다고 볼 수 있다. 이러한 과정들이 어떻게 일어나는지를 확인하기 원한다면 컴파일러와 관련된 문서를 읽어야 할 것이다.
  • Back End : 실제 이진코드를 만들어 내기 위한 마지막 전 단계로, 플렛폼의 특성을 따르게 된다. 이 단계에서 플랫폼의 특성에 따른 최적화의 대부분이 이루어지게 된다. 컴파일러는 MMX/SSE2/3DNow와 같은 확장되거나 독특한 instruction셋에 대한 정보를 이용해서 거기에 맞는 바이너리 코드를 생성하려고 한다. CPU의 종류에 따라서 register의 갯수, 캐쉬와 파이프라인의 구조가 다르기 때문에, 컴파일러는 이들 구조간의 차이점을 고려해서 가능한 빠른 코드를 만들어 낸다.

최적화에는 속도 우선 최적화와 공간 우선 최적화의 두가지가 있다. 이상적으로 보자면 속도와 공간을 모두 고려해서 최상의 균형조건을 찾는게 좋을 것이다. 최근에는 메모리의 가격이 내려간 관계로 메모리의 소비를 증가하고 속도를 우선적으로 높이는 방법을 많이 사용하게 된다. 인라인 함수가 가장 대표적인 경우가 될것이다. 인라인 함수를 사용하게 되면 실행파일의 크기가 커지지만, 대신 좀더 빠른 함수의 호출이 가능해지므로 전체적으로 실행속도가 향상될 것이다.

GCC는 4단계의 최적화 정도를 제공한다. 단계는 -O 옵션뒤에 단계를 나타내는 숫자를 명시하는 식으로 이루어진다. 기본적으로는 "최적화를 안함"인데 -O0와 동일하다. -O1, -O2, -O3은 아래와 같은 최적화 수준을 가진다.

  • -O1 는 컴파일 시간의 특별한 증가 없이 코드의 크기와 실행 시간을 줄여준다.
  • -O2 는 코드의 크기와 실행 시간을 절충한 최적화 코드를 만든다.
  • -O3 는 가능한 모든 최적화를 한다. 대신 컴파일 시간이 늘어나게 된다.
[root@localhost ~]# gcc -O3 -o hello3 helloworld.c 
[root@localhost ~]# gcc -O0 -o hello0 helloworld.c

[root@localhost ~]# ls -al hello*
-rwxr-xr-x 1 root root 4754 1월 3 19:12 hello3
-rwxr-xr-x 1 root root 4782 1월 3 19:12 hello0

[root@localhost ~]# time ./hello3 > /dev/null

real 0m0.003s
user 0m0.001s
sys 0m0.001s
[root@localhost ~]# time ./hello0 > /dev/null

real 0m0.002s
user 0m0.000s
sys 0m0.002s

위의 결과를 보면 -O3 최적화를 했을 경우 실행파일의 크기가 더 작아졌음을 알 수 있다.

또한 CPU와 architecture에 따른 최적화도 가능하다. 예를 들어 architecture에 따라서 가질 수 있는 레지스터의 수가 달라질 수 있는데, 이를 잘 이용하면 좀더 최적화된 실행파일을 만들어 낼 수 있게 된다. CPU의 종류는 -mcpu=<CPU name>, architecture는 -march=<architecture 타입> 으로 지정할 수 있다. architecture에는 ix86(i386, i486, i586), Pentiumx(pentium, pentium-mmx, pentiumpro, pentium2, pentium3, pentium4), athlon(athlon, athlon-tbird, athlon-xp, opteron)등이 올 수 있다. architecture를 지정해 줄경우 좀더 세밀한 최적화가 가능하겠지만, 비교적 최신의 아키텍처 타입입을 지정할 경우 주의 해야 한다. -march=i386 으로 최적화한 코드의 경우 i686 에서도 문제없이 돌아가겠지만, -march=i686으로 최적화환 코드는 오래된 CPU에서 작동하지 않을 수도 있기 때문이다.


이 글은 스프링노트에서 작성되었습니다.

'It's Code > It's Tools' 카테고리의 다른 글

이클립스 단축키  (0) 2007.08.14
리눅스 커널 소스 디렉토리 리눅스팁

2005/11/28 12:24

http://blog.naver.com/jskim89/140019915656

< arch >
arch 서브디렉토리는 모든 아키텍쳐에 종속적인 커널코드를 포함하고 있다.
여기에는 서브디렉토리가 더 있는데, 각각 지원하는 아키텍쳐별로 있다.
예를 들어, i386, alpha 같은 이름의 서브디렉토리가 존재한다.

< include >
include 서브디렉토리는 커널코드를 빌드하는데 필요한 모든 include 파일의 대부분을 가지고 있다.
여기에는 지원하는 아키텍쳐별로 하나씩의 서브디렉토리가 있다.
/include/asm 서브디렉토리는 현재 아키텍쳐에 필요한 실제 디록토리로 소프트 링크 되어 있다.
(예를 들어, include/asm-i386)
아키텍쳐를 다른 것으로 바꾸려면 커널 makefile을 수정하고
리눅스 커널 환경 설정 프로그램으로 돌아와야 한다.
초기화 코드를 가지고 있으며, 커널이 어떻게 동작하는지 보기 좋은 곳이다.
< init >
이 디렉토리는 커널의 초기화 코드를 가지고 있으며,
커널이 어떻게 동작하는지 보기 시작하는 곳이다.

< mm >
이 디렉토리는 모든 메모리 관리 코드를 가지고 있다.
아키텍쳐 종속적인 메모리 관리 코드는 arch/*/mm/ 아래에 있따.
예를 들어, arch/i386/mmfault.c 같은 곳에 있다.

< driver >
모든 시스템의 디바이스 드라이버는 이 디렉토리에 있다.
이들은 디바이스 드라이버의 유형별로 좀 더 세분화되어 있다.
예를 들어, 블록 디바이스 드라이버는 block에 있다.

< ipc >
이 디렉토리는 커널의 프로세스간 통신 코드를 가지고 있다.

< modules >
이는 단순히 빌드된 모듈을 저장하기 위한 디렉토리이다.

< fs >
모든 파일시스템 코드를 가지고 있다.
파일시스템별로 하나씩 디렉토리가 세분화된다.
예를 들어, vfat, ext2 같은 서브디렉토리가 있다.

< kernel >
메인 커널 코드가 들어 있다.
아키텍쳐 종속적인 커널 코드는 arch/*/kernel에 있다.

< net >
커널의 네트워킹 코드가 들어 있다.

< lib >
이 디렉토리는 커널의 라이브러리 코드를 가지고 있다.
아키텍쳐 종속적인 라이브러리 코드는 arch/*/lib에 있다.

< scripts >
이 디렉토리는 커널을 설정하는데 사용되는 스크립트를 가지고 있다.
(예를 들어, awk나 tlk 스크립트)

'It's Code' 카테고리의 다른 글

Cygwin+MinGW32 참고  (0) 2009.06.26
FILE IO  (0) 2008.09.08
리눅스 커널 소스 (The Linux Kernel Sources)  (0) 2007.08.17
Java classpath 관리하기  (0) 2007.08.16
JAVAScript TIP  (0) 2007.08.10

리눅스 커널 소스 (The Linux Kernel Sources)

이 장은 특정 커널 함수를 찾기 위해서 리눅스 커널 소스 어디서부터 시작해야 하는지 이 야기한다.

이 책은 C 언어에 대한 지식을 요구하지는 않지만 리눅스 커널의 동작을 보다 잘 이해하려 면 리눅스 커널의 소스를 가지고 있는 것이 좋다. 다시 말하면, 커널의 소스 프로그램은 리 눅스 운영체제를 심도깊게 이해하는데 있어 효과적인 교재이다. 이 장은 커널 소스 전반에 대해 개괄한다. 즉 커널 소스가 어떻게 배열되어 있는지, 특정 코드를 찾으려면 어디서 시작 해야 하는지 설명한다.


어디서 리눅스 커널 소스를 얻을 수 있는가

주요 리눅스 배포판들(Craftworks, Debian, Slackware, Red Hat 등)은 모두 리눅스 커널 소스를 포함하고 있다. 일반적으로 사용자의 리눅스 시스템에 설치된 리눅스 커널은 이 소 스 코드를 컴파일하여 생성한 것이다. 리눅스의 성격상, 소스들이 계속 변경되므로 사용자의 시스템에 설치된 것은 조금 옛날 것이 되고 만다. 최신 버전의 소스 프로그램은 부록 B에 서 언급된 웹 싸이트에서 구할 수 있다. 이들은 ftp://ftp.cs.helsinki.fi과 이를 그 림자처럼 복사하는 다른 웹 싸이트에서 들어 있다. 헬싱키의 웹 싸이트가 가장 최신 버전의 소스를 가지고 있으며, MIT나 Sunsite와 같은 싸이트들로 비교적 최신 버전의 소스를 제공 한다.

웹 싸이트에 접근할 수 없다고 하더라도, 많은 벤더들이 주요 웹 싸이트에 있는 내용들을 CD ROM 형태로 매우 저렴한 가격으로 제공하고 있으므로, 이를 이용하면 될 것이다. 1년에 네번 혹은 매달 정기적으로 업그레이드판을 제공해주는 구독 서비스도 있다. 지역별 리눅스 유저 그룹도 소스를 구하는데 유용한 곳이다1.

리눅스 커널의 버전 형태는 매우 단순하다. 짝수 버전 커널(예를 들자면 2.0.30)은 안정적 이고 발표된 버전이고, 홀수 버전 커널(예를 들자면 2.1.42)은 모두 개발용 커널이다. 본책 은 안정적인 2.0.30 소스 트리를 기반으로 하고 있다. 개발용 커널은 최신 기능들을 모두 포 함하고 있으며 또한 최신 드라이버들도 모두 지원한다. 개발 커널은 불안정할 수도 있고, 이 는 사용자가 바라지 않는 것이겠지만, 최신 커널을 사용해보는 것은 리눅스 공동체에 있어 중요한 일이다. 그래야 전체 공동체를 위해 테스트를 할 수 있다. 실제 제품으로 나온 커널 이 아닌 것을 써보려고 할 때 시스템 전체를 백업해두는 것이 좋다는 것을 기억하기 바란다.

커널 소스에서 바뀐 것들은 패치(patch) 파일로 배포된다. patch 프로그램은 소스 파일들에 편집된 것들을 적용하는데 사용된다. 따라서, 예를 들어 2.0.29 커널 소스를 가지고 있고, 이 를 2.0.30 소스로 바꾸고 싶다면, 2.0.30 패치 파일을 구해서 패치를 소스 트리에 적용하면 된 다.

$ cd /usr/src/inux
$ patch -p1 < patch-2.0.30

이는 전체 소스 트리를 복사할 필요가 없어, 느린 직렬 연결을 통하는 경우 더욱 유용하다. 커널 패치를 구하기 좋은 곳은(공식적이던 비공식적이던) http://www.linuxhq.com 웹 사이트이다2.


커널 소스는 어떻게 배열되어 있는가

소스 트리의 시작인 /usr/src/linux에서 보면 여러개의 디렉토리가 있다.

arch arch 서브디렉토리는 모든 아키텍쳐에 종속적인 커널 코드를 포함하고 있다. 여기에는 서브디렉토리가 더 있는데, 각각 지원하는 아키텍쳐별로 있다. 예를 들어 i386, alpha같은 이름의 서브디렉토리가 존재한다.

include include 서브디렉토리는 커널 코드를 빌드하는데 필요한 모든 인클루드(include) 파 일들의 대부분을 가지고 있다. 여기에는 지원하는 아키텍쳐별로 하나씩 서브디렉토리가 있 다. /include/asm 서브디렉토리는 현재 아키텍쳐에 필요한 실제 디렉토리로 (예를 들어, include/asm-i386) 소프트 링크되어 있다. 아키텍쳐를 다른 것으로 바꾸려면 커널 makefile을 수정하고 리눅스 커널 환경설정 프로그램으로 돌아와야 한다.

init 이 디렉토리는 커널의 초기화 코드를 가지고 있으며, 커널이 어떻게 동작하는지 보기 시작하기에 좋은 곳이다.

mm 이 디렉토리는 모든 메모리 관리 코드를 가지고 있다. 아키텍쳐 종속적인 메모리 관리 코드는 arch/*/mm/ 아래에 있다. 예를 들어, arch/i386/mm/fault.c 같은 곳에 있다.

drivers 모든 시스템의 디바이스 드라이버는 이 디렉토리에 있다. 이들은 디바이스 드라이버 의 유형별로 좀더 세분화 되면. 예를 들어 블럭 디바이스 드라이버는 block에 있다.

ipc 이 디렉토리는 커널의 프로세스간 통신 코드를 가지고 있다.

modules 이는 단순히 빌드된 모듈을 저장하기 위한 디렉토리이다.

fs 모든 파일 시스템 코드를 가지고 있다. 파일 시스템별로 하나씩 디렉토리가 세분화된다. 예를 들어 vfat, ext2 같은 서브디렉토리가 있다.

kernel 메인 커널 코드가 들어 있다. 아키텍쳐 종속적인 커널 코드는 arch/*/kernel에 있다.

net 커널의 네트워킹 코드가 들어 있다.

lib 이 디렉토리는 커널의 라이브러리 코드를 가지고 있다. 아키텍쳐 종속적인 라이브러리 코드는 arch/*/lib/에 있다.

scripts 이 디렉토리는 커널을 설정하는데 사용되는 스크립트(예를 들어 awk나 tlk 스크립 트)를 가지고 있다.


어디서부터 보기 시작할 것이가

리눅스 커널처럼 방대하고 복합적인 프로그램은 들여다보기에 위압적일 수 있다. 이는 실로 된 커다란 공처럼 끝이 보이지 않는 것이기도 하다. 커널의 한 부분을 보다 보면 관련된 다 른 여러 파일들을 보게되고, 오래지 않아 무엇을 찾으려고 했는지 잊어버리게 된다. 다음 작 은 장들은 어떤 주제를 보려 할때 소스 트리의 어디를 보는게 좋은지 힌트를 제공할 것이다.

시스템 시작과 초기화

인텔 기반 시스템에서, 커널은 loadlin.exe나 LILO가 리눅스 커널을 메모리로 읽어들인 후 커널에 제어권을 넘겨줌으로써 시작한다. 이 부분에 대해서는 arch/i386/kernel/- head.S를 보기 바란다. head.S는 아키텍쳐 종속적인 셋업을 한 후 init/main.c에 있 는 main() 루틴으로 점프한다.

메모리 관리

이 코드는 대부분 mm에 있지만, 아키텍쳐 종속적인 코드는 arch/*/mm에 있다. 페이지 폴 트 처리 코드는 mm/memory.c에 있고, 메모리 매핑과 페이지 캐시 코드는 mm/filemap.c 에 있다. 버퍼 캐시는 mm/buffer.c에, 스왑 캐시는 mm/swap_state.c와 mm/- swapfile.c에 구현되어 있다.

커널

상대적으로 일반적인 코드는 kernel에 있고, 아키텍쳐 종속적인 코드는 arch/*/kernel 에 있다. 스케쥴러는 kernel/sched.c에 있고, fork 코드는 kernel/fork.c에 있다. 하반 부 핸들러 코드는 include/linux/interrupt.h에 있다. task_struct 자료구조는 include/linux/sched.h에서 찾을 수 있을 것이다.

PCI

PCI 유사 드라이버는 drivers/pci/pci.c에 있고, 시스템 범위의 정의들은 include/- linux/pci.h에 되어 있다. 각 아키텍쳐들은 특정 PCI BIOS 코드를 가지고 있는데, 알파의 PCI BIOS 코드는 arch/alpha/kernel/bios32.c에 있다.

프로세스간 통신

이것은 모두 ipc에 들어 있다. 모든 시스템 V IPC 오브젝트들은 ipc_perm 자료구조에 들 어 있고, include/linux/ipc.h에서 찾을 수 있다. 시스템 V 메시지들은 ipc/msg.c에, 공유 메모리는 ipc/shm.c에, 세마포어는 ipc/sem.c에 구현되어 있다. 파이프는 ipc/pipe.c에 구현되어 있다.

인터럽트 처리

커널의 인터럽트 처리 코드는 대부분 모두 마이크로프로세서 (때때로 플랫폼) 종속적이다. 인텔의 인터럽트 처리 코드는 arch/i386/kernel/irq.c에 있고, 정의는 include/asm- i386/irq.h에 되어 있다.

디바이스 드라이버

리눅스 커널 소스 코드의 대부분은 디바이스 드라이버에 있다. 모든 리눅스 디바이스 드라 이버 소스는 drivers에 있지만, 이들은 장치 유형에 따라 세분화 된다. /block 블럭 디바이스 드라이버. 예를 들어 IDE 디바이스 드라이버는 ide.c에 있다. 모든 장치가 어떻게 파일 시스템을 가질 수 있으며, 어떻게 초기화되는지 보고 싶다면 drivers/block/genhd.c에 있는 device_setup()을 보기 바란다. 이는 하드 디스크만 초기화하는 것이 아니라, 네트웍을 nfs 파일 시스템에 마운트하려고 한다면 네트웍도 초기 화한다. 블럭 장치에는 IDE와 SCSI 기반 장치가 포함된다.

/char ttys, 시리얼 포트나 마우스같은 문자 기반 장치들을 볼 수 있다.

/cdrom 리눅스의 모든 CDROM 코드가 들어 있다. 특별한 CDROM 장치(Soundblaster CDROM 같은)도 여기서 찾을 수 있다. IDE CDROM 드라이버는 drivers/block에 있는 ide-cd.c에 있고, SCSI CDROM 드라이버는 drivers/scsi에 있는 scsi.c에 있다는 점 에 주의하기 바란다.

/pci 여기에는 PCI 유사 드라이버의 소스가 있다. PCI 서브시스템이 어떻게 매핑되고 초기화 되는지 보기 좋은 곳이다. 알파 AXP PCI 확정 코드는 arch/alpha/kernel/bios32.c에 있고, 이는 볼만한 가치가 있다.

/scsi 모든 SCSI 코드와 함께 리눅스가 지원하는 모든 SCSI 장치들의 드라이버가 있는 곳이 다.

/net 네트웍 장치 디바이스 드라이버를 볼 수 있는 곳이다. DECChip 21040 PCI 이더넷 드라 이버는 tulip.c에 있다.

/sound 모든 사운드 카드 드라이버가 있는 곳이다.

파일 시스템

EXT2 파일 시스템 소스는 fs/ext2/ 디렉토리에 있고 자료구조는 include/linux/- ext2_fs.h, ext2_fs_i.h, ext2_fs_sb.h에 정의되어 있다. 가상 파일 시스템 자료구조 는 include/linux/fs.h에 정의되어 있고, 코드는 fs/*에 있다. 버퍼 캐시와 update 커널 데몬은 fs/buffer.c에 구현되어 있다.

네트웍

네트워킹 코드는 net에 있고, 인클루드(include) 파일들의 대부분은 include/net에 있다. BSD 소켓 코드는 net/socket.c에 있고, IP 버전 4 INET 소켓 코드는 inet/ipv4/- af_inet.c에 있다. 일반적인 프로토콜 지원 코드는 (sk_buff 처리 루틴도 포함하여) net/core/에, TCP/IP 네트워킹 코드는 net/ipv4/에 있다. 네트워크 디바이스 드라이버는 drivers/net에 있다.

모듈

커널 모듈 코드는 일부분은 커널에, 일부분은 modules 패키지에 있다. 커널 코드는 모두 kernel/modules.c에 있고, 자료구조와 커널 데몬 kerneld 메시지는 include/- linux/module.h와 include/linux/kerneld.h에 있다. ELF 오브젝트 파일의 구조는 include/linux/elf.h에서 볼 수 있다.

번역 : 이호, 이대현, 김진석, 심마로
정리 : 이호

'It's Code' 카테고리의 다른 글

Cygwin+MinGW32 참고  (0) 2009.06.26
FILE IO  (0) 2008.09.08
리눅스 커널 소스 디렉토리  (0) 2007.08.17
Java classpath 관리하기  (0) 2007.08.16
JAVAScript TIP  (0) 2007.08.10

    Java classpath (Windows) 관리하기 (한글)

    Windows에서 classpath를 관리하는 방법


    난이도 : 초급

    Elliotte Harold, Adjunct Professor, Polytechnic University

    2007 년 1 월 16 일

    classpath 는 자바™ 플랫폼에서 가장 복잡한 부분 하나이지만, 전문 자바 프로그래머가 되려면 반드시 마스터해야 합니다. Elliotte Rusty Harold classpath sourcepath Windows 플랫폼에서 사용하는 방법을 설명합니다. UNIX 또는 Mac OS X 사용자라면, 관련 기술자료를 참조하십시오.

    classpath는 자바 런타임과 파일시스템간 연결이다. 컴파일러와 인터프리터가 로딩할 .class 파일들을 검색할 장소를 정의한다. 기본 개념은, 파일시스템 계층이 자바 패키지 계층을 미러링 하고, classpath가 파일시스템에서 어떤 디렉토리가 자바 패키지 계층에 대한 루트의 역할을 할 것인지를 지정하는 것이다.

    안타깝게도, 파일시스템은 복잡하고, 플랫폼 의존성이 크며, 자바 패키지와 완벽하게 들어맞지 않는다. 따라서, classpath는 신참 사용자와 숙련된 자바 프로그래머 모두에게서 가시와 같은 존재가 되었다. 자바 플랫폼에서도 환영을 받는 존재도 아니다. 지독하게도 솔루션이 나오지 않는 소소한 문제들을 해결하느라 많은 노력을 기울여야 한다.

    Eclipse 같은 훌륭한 IDE는 classpath 관리의 어려움을 덜어주기는 하지만, 어디까지나 모든 것이 잘 작동하고 있을 경우에만 해당된다. (어떤 것이나 늘 잘못되기 마련이다.) 결국, 모든 자바 프로그래머는 classpath를 완전히 이해해야 한다. 완벽한 통달만이 classpath에서 발생하는 가시 같은 문제들을 해결할 수 있다.

    이 글에서는, Windows 플랫폼에서 자바 classpath(관련 sourcepath 포함)에 대해 알아야 할 모든 것을 망라했다. 관련 기술자료에서는 UNIX와 Mac OS X 플랫폼을 기준으로 설명했다. 이 글은 대부분의 classpath 문제들을 해결할 수 있는 좋은 가이드가 될 것이다.

    패키지 구조

    classpath는 소스 코드부터 시작하겠다. 모든 클래스는 패키지에 속해 있고, 이 패키지는 표준 네이밍 규약을 따라야 한다. 간단히 정리하면 다음과 같다. 패키지 이름은 com.example 또는 edu.poly처럼, 두 개의 도치된(reversed) 도메인 이름으로 시작한다. 그 다음에는 패키지의 내용을 설명하는 한 개 이상의 단어가 나온다. 예를 들어, 도메인 네임 elharo.com이 있고, Fraction 클래스를 작성한다면, 다음과 같은 패키지 형태가 될 것이다.

    • com.elharo.math
    • com.elharo.numbers
    • com.elharo.math.algebra.fields

    도 치된 도메인 네임 다음에는, 한 단어로 된 서브패키지(subpackage) 이름만 사용한다. 단어를 생략하지 말고, 모든 단어를 정확하게 작성한다. 필요할 경우에는, 스펠 체커를 사용한다. classpath 관련 문제들 대부분이 소스 코드에 한 단어를 사용하는 데서 기인하고, 파일시스템의 단어와 약간 다른 스펠링 또는 축약 때문에 발생한다. 가장 현명한 방법은 정확한 철자를 사용하는 것이다.

    전체 패키지 이름은 소문자로 하고, 일반적으로 대문자로 표기하는 고유 명사나 두문자어도 소문자로 한다. Windows는 파일 이름에 대/소문자를 구별하지 않지만, 자바와 일부 유닉스 파일시스템은 구분한다. 시스템들 간 파일을 이동할 때, 케이스(case)는 문제를 일으키는 주요 원인이다.

    패키지 이름은 오직 ASCII 문자들로만 구성된다. 컴파일러는 Hebrew, Cyrillic, Greek, 기타 스크립트로 작성된 패키지 이름들을 허용하지만, 많은 파일 시스템들은 그렇지 않다. 이러한 패키지 이름들은 디렉토리 이름으로서의 이중 기능을 할 필요가 없다. 자바 패키지와 클래스 이름들은 Unicode이지만 FAT을 포함하여, 많은 파일시스템들은 아직 Unicode 방식이 아니다. 안타깝게도, 여전히 FAT 파일시스템들이 많이 남아있다. 다른 디폴트 인코딩을 가진 시스템으로 파일을 복사하면 컴파일러와 인터프리터가 올바른 클래스를 찾을 수 없다.

    폐기용(Throwaway) 코드

    단 한번 실행한 후에 바로 폐기할 클래스를 작성할 경우, 예를 들어, API를 테스트 하기 위해 클래스를 작성했다면, 이것을 패키지에 둘 필요가 없다. 하지만, 한 번 이상 사용될 클래스는 패키지에 있어야 한다.

    패 키지 이름에 인색하게 굴지 말라! 장기적으로 볼 때 큰 재앙이 될 수 있다. 도메인 이름이 필요하면, 구매해도 좋다. 이름이 너무 길다면, 짧은 것을 구매하라. (나는 xom.nu를 샀기 때문에, 나의 패키지 접두사는 단 여섯 문자뿐이다.) 클래스를 디폴트 패키지(클래스에 패키지 문을 추가하지 않을 경우 얻게 되는 패키지)에 두지 말라. 패키지 액세스가 객체 통신을 금하면, 더 많은 퍼블릭 메소드를 클래스에 추가하라. 한 번 이상 사용되는 모든 클래스는 패키지에 있어야 한다.

    Windows 설정하기

    파 일 확장과 경로는 자바와 Windows에 매우 중요하다. 따라서, 다음 단계를 시작하기 전에, 이것부터 검토해야 한다. 파일 이름의 숨겨진 부분은 엔드 유저에게는 허용될 수도 있겠지만(나도 이것이 의심스럽긴 하다.), 개발자에게는 그렇지 않다. 이를 해결하려면, Windows Explorer의 디폴트 설정을 수정해야 한다.

    우선, Windows 데스크탑에서 폴더를 연다. 어떤 것이든 상관 없다. Tools 메뉴로 가서 Folder Options를 선택한다. 다이얼로그에서, 다음과 같이 세 가지 옵션들이 설정되었는지를 확인한다. (그림 1)

    • "Display the full path in the address bar" 항목에 체크가 되어 있어야 한다.
    • "Display the full path in title bar" 항목에 체크가 되어 있어야 한다.
    • "Hide file extensions for known file types" 항목은 체크가 되어 있지 않아야 한다.

    그림 1. Windows Explorer 옵션

    "Show hidden files and folders" 옵션도 체크해야 한다. 이것은 자바 작동에는 많은 영향을 끼치지 않지만, 개인적으로 내가 무슨 작업을 하고 있는지를 볼 수 있는 것이 좋다. 이 옵션을 설정하면 어디서, 무엇을 하는지에 대한 상세를 볼 수 있고, 문제 해결도 훨씬 쉬워진다.




    디렉토리 구조

    다음 단계는 패키지 구조에 맞게 소스 파일을 구성하는 단계이다. 깨끗하고, 비어있는 디렉토리를 만든다. 이 글의 목적에 맞게 이름을 project로 지었다. C:\project처럼, 루트 레벨에 두는 것이 가장 쉽다. 데스크탑 또는 Documents and Settings 폴더에 저장할 수도 있다. 더 길고 복잡한 타이핑을 해야 하는 명령어를 만들 것이기 때문에, 필요한 경우에만 그렇게 한다. (Windows XP 또는 이후 버전을 사용하고, 관리 권한이 없다면 선택권이 없다. 싱글 유저 시스템에서, 관리 권한으로 실행한다면 훨씬 쉬워진다.)

    무엇을 하든지 간에, 이 디렉토리(또는 그 어떤 것이든)를 JDK 폴더에는 넣지 말아야 한다. (예를 들어, C:\jdk1.6.0 or C:\Program Files\Java\j2sdk1.5.0_09). JDK와 JRE 디렉토리는 초기 설치 후에는 건드리지 않아야 한다.

    Project 디렉토리 안에, 두 개의 디렉토리를 더 만든다. bin과 src이다. (어떤 사람들은 build와 source라는 이름을 더 선호하기도 한다.)

    그런 다음, src 디렉토리 안에, 패키지 계층을 모방한 계층을 만든다. 예를 들어, com.elharo.math.Fraction이 라고 하는 클래스가 있다면, src 디렉토리에 com 디렉토리를 넣는다. 그런 다음, com 디렉토리 안에 elharo 디렉토리를 만든다. 그리고 나서, elharo 디렉토리 안에 math 디렉토리를 넣는다. 마지막으로, math 디렉토리 안에 Fraction.java를 넣는다. (그림 2)


    그림 2. 디렉토리 구조는 패키지 구조를 따른다.

    중 요: src 디렉토리에 소스 코드 외 다른 것을 절대 넣지 말라. 그곳에 들어갈 유일한 파일은 .java 파일들이다. 가끔 이 디렉토리에 .html(Javadoc) 또는 다른 유형의 소스 코드를 넣는 경우도 있다. 하지만, .class 파일이나 다른 생성물들을 이 계층에 넣어서는 안된다. 큰 문제가 생길 것이다. javac 컴파일러의 경우, 조심하지 않는다면 반드시 문제를 일으킨다. 다음 섹션에서 이러한 문제를 해결하는 방법을 설명하겠다.




    컴파일

    자바 코드 컴파일은, 여러 가지 관련 사항들을 트래킹 해야 하기 때문에 손이 많이 간다.

    백 슬래시(Back slash)와 포워드 슬래시(forward slash)

    자바 언어를 고안한 유닉스 프로그래머는 컴파일러가 Windows 백 슬래시 대신 유닉스 스타일의 포워드 슬래시를 허용하도록 했다. 따라서, 다음 명령어 역시 작동한다.

    C:\project> javac src/com/elharo/math/Fraction.java

    Windows에서는 언제나 백 슬래시를 사용하는 것이 상책이다.

    • 컴파일 대상 파일
    • 컴파일러가 대상 파일이 반입하는 .java 파일을 검색할 디렉토리
    • 컴파일러가 대상 파일이 반입하는 .class 파일들을 검색할 디렉토리
    • 컴파일러가 아웃풋을 둘 디렉토리

    기본적으로, javac 컴파일러는 이 모든 것이 현재 실행 디렉토리라고 간주하는데, 대게는 우리가 원하는 것이 아니다. 결국, 컴파일 할 때 엘리먼트들을 명확하게 지정해야 한다.

    컴파일 대상 파일

    우선, 컴파일 하고자 하는 .java 파일을 지정해야 한다. 이것은 현재 실행 디렉토리에서 그 파일로 가는 경로로 주어진다. 예를 들어, 그림 1처 럼, project 디렉토리를 생각해 보자. 이 디렉토리에는 src 디렉토리가 있다. src 디렉토리에는 com 디렉토리가 있고, com 디렉토리에는 예제 디렉토리가 포함되어 있다. 여기에는 Fraction.java 파일이 있다. 다음 명령행이 이것을 컴파일 한다.

    C:\project> javac src\com\elharo\math\Fraction.java

    경로가 정확하지 않다면, 다음과 같은 에러 메시지를 받게 된다.

    error: cannot read: src\com\example\mtah\Fraction.java

    에러 메시지가 생기면, 경로를 보면서 철자가 올바르게 쓰였는지를 확인한다. 이 경우, “math” 단어에서 두 번째와 세 번째 문자가 바뀌었다.

    스펠링 에러를 찾지 못했다면, 다음과 같은 dir 명령어를 실행하여, 이 파일이 있어야 할 곳에 있는지를 확인한다.

    C:\project\src> dir src\com\example\math
    ls: src/com/example/math: No such file or directory

    이 문제는 잘못 입력된 경로를 나타내지만, 여러분이 생각한 디렉토리에 있지 않다는 것도 의미한다. 이 예제에서, 현재 실행 디렉토리가 project 디렉토리인지를 확인했다. 명령행에서 C:와 > 사이의 텍스트를 체크하여 제 위치에 있는지를 확인한다. 이 예제에서는, C:\project에 있어야 하는데, 실제로는 C:\project\src에 있다는 것을 알게 될 것이다.

    아웃풋의 위치

    신 택스 에러가 없다고 가정하고, javac는 컴파일 된 .class 파일을 상응하는 .java 파일이 있는 같은 디렉토리에 둔다. 이것은 우리가 원한 것이 아니다. .class와 .java 파일을 섞어 놓으면, 컴파일 된 파일들을 지우기가 매우 어렵다. 실수로 .java 파일들을 지울 수도 있다. 이는 깨끗한 구현도 문제 거리로 만들며, 버저닝 문제도 일으킨다. 또한 바이너리를 배포할 때, 컴파일 된 .class 파일들만 압축하기도 힘들다. 따라서, 완전히 다른 디렉토리에 저장하도록 컴파일러에 명령해야 한다. -d 스위치는 아웃풋 디렉토리(대게 bin, build, classes)를 지정한다.

    C:\project> javac -d bin src\com\elharo\math\Fraction.java

    아웃풋은 그림 3과 같다. javac가 완전한 com\elharo\math 디렉토리 계층을 만들었다. 여러분이 직접 할 필요가 없다.


    그림 3. 병렬 소스와 컴파일 된 계층

    sourcepath

    자바가 소스 파일들을 검색할 디렉토리는 sourcepath이다. 여기에서는, src 디렉토리이다. 이 디렉토리에는, 자신들의 디렉토리로 구성된, 소스 파일의 계층이 포함되어 있다. com 디렉토리도, src\com\elharo\math 디렉토리도 아니다.

    대부분의 프로젝트는 한 개 이상의 클래스와 패키지를 사용한다. 이것은 import 문과 완전한 패키지 자격을 갖춘 클래스 이름에 의해 연결된다. 예를 들어, com.elharo.gui에 새로운 MainFrame 클래스를 만든다고 해보자.(Listing 1)


    Listing 1. 한 패키지의 클래스가 또 다른 패키지의 클래스를 반입할 수 있다.


                                   
    package com.elharo.gui;

    import com.elharo.math.*;

    public class MainFrame {

      public static void main(String[] args) {
        Fraction f = new Fraction();
        // ...
      }

    }

    이 클래스는 MainFrame 클래스와는 다른 패키지에 있는 com.elharo.math.Fraction 클래스를 사용한다. 소스 설정은 그림 4과 같다. (이전 단계의 컴파일 된 아웃풋을 삭제했다. 언제든 다시 컴파일 할 수 있다.)


    그림 4. 여러 패키지의 소스 구조

    이제, 전에 했던 것처럼 MainFrame.java를 컴파일 할 때 어떤 일이 발생하는지 보자.


    Listing 2. MainFrame.java 컴파일


                                   
    C:\project> javac -d bin src\com\elharo\gui\MainFrame.java
    src\com\elharo\gui\MainFrame.java:3: package com.elharo.math does not exist
    import com.elharo.math.*;
    ^
    src\com\elharo\gui\MainFrame.java:7: cannot find symbol
    symbol  : class Fraction
    location: class com.elharo.gui.MainFrame
      private Fraction f = new Fraction();
              ^
    src\com\elharo\gui\MainFrame.java:7: cannot find symbol
    symbol  : class Fraction
    location: class com.elharo.gui.MainFrame
      private Fraction f = new Fraction();
                               ^
    3 errors

    javac가 MainFrame.java를 어디에서 찾아야 할 지를 알지만, Fraction.java를 어디에서 찾아야 할지 모르기 때문에, Listing 2에 에러가 생겼다. (매칭 패키지 계층들을 인식하는 것으로도 충분하다고 생각했는데, 그렇지가 않다.) sourcepath를 지정해야 한다. 이것은 컴파일러가 소스 파일의 계층을 검색할 디렉토리를 지정한다. Listing 2에서는, src이다. 따라서, 나도 다음과 같이, -sourcepath 옵션을 사용한다.

    C:\project> javac -d bin -sourcepath src src\com\elharo\gui\MainFrame.java

    이제 프로그램은 에러 없이 컴파일 하고, 그림 5와 같은 아웃풋을 만들어 낸다. javac 역시 내가 컴파일 했던 파일에 의해 참조된 Fraction.java 파일을 컴파일 했다.


    그림 5. Multiclass 결과

    sourcepath에서 여러 디렉토리 컴파일 하기

    sourcepath 에 한 개 이상의 디렉토리가 생겼다. 반드시 그래야 하는 것은 아니지만, 콜론으로 구분되어 있다. 또 다른 프로젝트용 소스 코드를 관리할 로컬 src 디렉토리와 C:\Projects\XOM\src 디렉토리를 추가하고 싶다면, 다음과 같이 컴파일 할 수 있다.

    C:\project> javac -d bin -sourcepath src;C:\Projects\XOM\src
      src/com/elharo/gui/MainFrame.java

    이 명령어는 그 계층에서 발견된 모든 파일들을 컴파일 하지 않는다. 컴파일 되도록 분명히 요청했던 하나의 .java 파일에 의해 직/간접적으로 참조된 파일만 컴파일 한다.

    디렉토리 이름에서의 공간

    자 바 클래스나 패키지 이름에는 공백이 없다. 하지만, 가끔은 자바 패키지 디렉토리나 소스 파일을 포함하고 있는 디렉토리에 공백이 포함되기도 한다. Documents and Settings가 대표적인 예이다. 경로에 이러한 디렉토리들 중 하나를 추가해야 한다면, 관련 명령행 인자에 더블 쿼트를 사용해야 한다. 루트 C: 디렉토리에서, src 폴더가 C:\Documents and Settings\Administrator\project에 있다면, 다음과 같이 컴파일 해야 한다.

    C:\> javac -d bin -sourcepath "C:\Documents and Settings\Administrator\project"
     -classpath C:\lib\classes
     "C:\Documents and Settings\Administrator\project\src\com\elharo\gui\MainFrame.java"

    대부분의 경우, 프로그램의 컴파일이나 실행 전에 project 디렉토리로 변경하여 이러한 문제들을 피한다.

    java 파일용으로 하나의 소스 디렉토리가 있지만, 사전 컴파일 된 서드 파티 라이브러리가 있는 곳에, 클래스 또는 JAR 아카이브를 위한 여러 디렉토리도 가질 수 있다. 이것은 classpath의 역할이다.

    classpath 설정하기

    중대형 프로젝트에서, 매번 모든 파일들을 재 컴파일 한다는 것은 시간 낭비다. 다른 클래스나 bin 디렉토리에 같은 프로젝트의 독립된 부분들을 개별적으로 컴파일 및 저장함으로써 이러한 문제를 완화시킬 수 있다.

    클래스를 classpath에 추가하는 여러 가지 방법이 있다. 하지만, 여러분은 -classpath 명령행 스위치만 사용해야 한다. 이전에 컴파일 했던 또 다른 프로젝트에서 파일을 C:\lib\classes 디렉토리로 반입하고 싶다면? -classpath C:\lib\classes를 다음과 같이 명령행에 추가한다.

    C:\project> javac -d bin -sourcepath src -classpath C:\lib\classes
      src\com\elharo\gui\MainFrame.java

    두 개의 디렉토리, C:\project1\classes와 C:\project2\classes를 추가한다고 해보자. 콜론으로 구분하여 다음과 같이 추가한다.

    C:\project> javac -d bin -sourcepath src
      -classpath C:\project1\classes;C:\project2\classes
      src\com\elharo\gui\MainFrame.java

    물론, 원한다면, 다양한 형태의 상대 경로를 사용할 수 있다. project1과 project2가 현재 실행 디렉토리와 인접해 있다면(같은 부모 디렉토리를 갖고 있다면) 다음과 같이 한다.

    C:\project> javac -d bin -sourcepath src
      -classpath ..\project1\classes;..\project2\classes
      src\com\elharo\gui\MainFrame.java

    지금까지, 프로그램이 완전하고, 개별적으로 컴파일 된 서드 파티 라이브러리를 사용하지 않는 경우를 가정했다. 만약 그렇다면, 이들을 classpath에도 추가해야 한다. 라이브러리는 junit.jar 또는 icu4j.jar 같은 JAR 파일로 배포된다. 이 경우, classpath에 추가하는 것은, JAR 파일 그 자체이다. 이것을 포함하고 있는 디렉토리가 아니다. (본질적으로, JAR 파일은 컴파일 된 .class 파일들을 포함하고 있는 디렉토리의 역할을 한다.) 예를 들어, 다음 명령어는 classpath에 세 가지를 추가한다. C:\classes 디렉토리, 현재 실행 디렉토리에 있는 icu4j.jar 파일, E:\lib 에 있는 junit.jar 파일.

    C:\project> javac -d bin -sourcepath src
      -classpath C:\classes;icu4j.jar;E:\lib\junit.jar
      src\com\elharo\gui\MainFrame.java

    JAR 파일들은 .class 파일과 classpath에만 사용된다. .java 파일과 sourcepath에는 사용되지 않는다.

    상위 디렉토리

    여 기에서 내가 참조한 디렉토리들은 패키지 계층을 포함하고 있는 상위 디렉토리들이다. 이름이 패키지 이름들(com, elharo, math)과 매치하는 디렉토리들은 sourcepath 또는 classpath에 직접 포함되지 않는다.

    프로그램 실행하기

    프로그램을 성공적으로 컴파일 했고, 이제 실행할 준비가 되었다. 컴파일과 비슷하지만 더 간단하다. 프로그램을 실행할 때, 다음 두 가지를 지정하면 된다.

    • The classpath.
    • main() 메소드를 포함하고 있는 클래스의 전체 이름

    sourcepath를 지정할 필요가 없다.

    일반적으로, classpath는 프로그램을 컴파일 하기 위해 사용했던 것과 같은 classpath이다. 컴파일 된 아웃풋이 배치되었던 디렉토리가 추가된다. 예를 들어, 컴파일 명령어가 다음과 같고,

    C:\project> javac -d bin -sourcepath src
      -classpath C:\classes;E:\lib\junit.jar
      src\com\elharo\gui\MainFrame.java

    main() 메소드가 the class com.elharo.gui.MainFrame 클래스에 있다면, 다음과 같이 프로그램을 실행한다.

    C:\project> java -classpath bin;C:\classes;E:\lib\junit.jar
        com.elharo.gui.MainFrame

    명령행의 마지막 아이템은 클래스 이름이다. 파일 이름이 아니다. .java나 .class로 끝나지 않는다. 이 클래스는 classpath 어디에선가는 발견되어야 한다.




    다른 클래스들을 둘 곳

    컴 파일 할 때나, 실행할 때, 언제나 분명하게 classpath를 지정해야 한다. 파일을 둘 수 있는 다른 장소들이 있기 때문에, classpath에 추가되고, javac 컴파일러와 자바 인터프리터에 의해서 발견될 수 있다. 이 옵션들은 적은 양의 타이핑만 저장하고, classpath에 오래된 버전의 클래스를 둘 경우 디버깅에 많은 시간이 걸릴 것이다.

    예기치 않게 classpath로 와서 문제를 일으키는 클래스 하이딩을 찾을 장소를 설명하겠다. 이것은 서버 같이, 우리들이 제어할 수 없는 머신에서 발생한다.

    현재 실행 디렉토리

    요 청을 하든, 하지 않든 간에, 컴파일러는 현재 실행 디렉토리(.)를 classpath에 추가한다. 같은 디렉토리 안에 무엇이 있고, 없는지를 쉽게 잊는다. 따라서, 클래스나 계층들을 프로젝트나 홈 디렉토리에 두지 말라. 대신, .java 파일용 src 디렉토리와 bin 디렉토리에 명확하게 분리해서 저장해야 한다.

    CLASSPATH

    bin 디렉토리와 JAR 아카이브를 classpath에 직접 추가하는 것이 지겨울 것이다. 이때 CLASSPATH 환경 변수를 찾는다. 디렉토리와 JAR 아카이브를 단 한번만 CLASSPATH 환경 변수에 추가할 수 있다. javac나 자바를 실행할 때마다 경로를 입력할 필요가 없다.

    이 와 같은 유혹과 싸워라. 잘못된 클래스나 잘못된 클래스 버전을 로딩할 때 문제를 일으킬 것이다. 저장할 때 마다, 잘못된 클래스를 로딩했기 때문에 생긴 문제들을 디버깅 할 때 수백 번 취소당할 것이다. classpath를 자동화하고 타이핑을 피할 수 있는 더 나은 방법이 있다.

    jre\lib\ext

    jre\lib\ext 디렉토리에 있는 JAR 아카이브는 가상 머신에서 실행되는 모든 애플리케이션의 classpath에 추가된다. 이것이 안전해 보이지만, 디렉토리들을 CLASSPATH 환경 변수에 추가하는 것과 비슷한 실수이다. 조만간, 생각하지도 않았던 장소에서 잘못된 클래스 버전을 로딩하고, 이것을 디버깅 하느라 귀중한 시간만 낭비하게 될 것이다.

    이 문제는, 서버 측 애플리케이션을 전개할 때 특별히 위험하다. 전개할 서버에 jre/lib/ext 디렉토리에 추가 JAR 파일들이 없어야 한다. classpath에서 잘못된 버전의 JAR 아카이브로 생긴 문제들은, 증상을 인지하지 못하거나, 무엇을 찾아야 할지 모를 경우, 디버깅이 매우 어렵다. 이러한 문제들을 피하기 위해, 일부 프레임웍은 자바 코드의 일반 클래스 로딩 메커니즘을 우회하는 클래스 로더를 작성하기도 한다.

    jre\lib\endorsed

    jre\lib\endorsed에 있는 JAR 파일들은 가상 머신으로 실행되는 모든 애플리케이션의 classpath에도 추가된다. 차이점은, 이 파일들이 실제로 일반 classpath가 아닌 bootclasspath에 추가되고, 표준 클래스를 JDK로 대체할 수 있다. 이러한 방식은 XML 파서를 업그레이드 하고, VM에서 버그를 픽스할 때 유용하다.

    이 기술은 간편해 보이지만, 이 역시 장기적으로 볼 때는 위험하다. JDK 클래스로 대체해야 한다면, 런타임 시 -Xbootclasspath/p 옵션을 사용하여 잘못된 버전의 클래스를 로딩하는 것을 피한다.

    C:\project> java -classpath C:\classes
          -Xbootclasspath/p:xercesImpl.jar com.elharo.gui.MainFrame





    classpath 관리 자동화하기

    못 총(nail gun)을 집어 들기 전에 망치를 사용하는 방법부터 배워야 한다. 마찬가지로, 보다 강력한 툴을 찾기 전에 클래스를 직접 관리하는 것에 익숙해져야 한다. 하지만, 일단 명령행 툴셋을 익히면, sourcepath와 classpath를 자동으로 다루는 툴을 사용하고 싶을 것이다. 대게 이 툴들은 이 글에서 전개했던 라인에 따라 파일을 구성하면 작동한다.

    IDE

    Eclipse와 NetBeans 같은 통합 개발 환경은 classpath 관리의 일부를 자동화 한다. 예를 들어, 패키지 이름을 변경할 때, Eclipse는 상응하는 .java 파일을 매치하는 것으로 옮긴다. (그림 6)


    그림 6. Eclipse에서 classpath용 빠른 픽스

    하 지만, 이러한 IDE는, 특히 다른 툴과 다른 IDE와 통합해야 한다면, 파일시스템 상에 존재하며, 올바르게 설정되어야 한다. 이 툴의 주된 특징은 GUI 다이얼로그, 트리 뷰, 탭들이 명령행 스위치를 대체한다는 점이다. 하지만, 기본 파일 구조는 같다.

    Ant

    Ant는 빌드 프로세스를 자동화 하는 표준 툴이다. 디렉토리를 jre\lib\ext 또는 CLASSPATH 환경 변수에 두는 것과는 달리, Ant에서는 원스텝 빌드 프로세스를 만들 수 있다. 여전히, Ant build.xml 파일에 classpath를 설정하고, 올바른 디렉토리에 소스 파일들을 직접 저장해야 한다. 하지만, 컴파일 할 때마다 이것을 다시 지정할 필요는 없다.

    Maven

    Maven 은 빌드 프로세스와 관련 classpath 문제들을 구성 및 자동화 한다는 점에서 Ant 보다 더 향상되었다. Maven은, 단 몇 줄의 코드로도 간단한 프로젝트를 구현할 수 있도록, 합리적인 디폴트 설정을 제공한다. Maven이 찾을 수 있는 곳에 소스 파일을 두면 된다. 여전히, 파일 시스템 계층과 패키지 계층을 조정해야 한다. Maven은 삼자 라이브러리에 대한 의존성 관리에 특별히 뛰어나다. Ant 만큼 커스터마이징은 쉽지 않다.




    맺음말

    classpath는 문제가 많지만, 몇 가지 간단한 규칙을 사용하면 능히 다스릴 수 있다. 특히,

    • 모든 클래스는 패키지에 둔다.
    • 패키지와 클래스 네이밍 규약 및 대/소문자 규약을 엄격하게 지킨다.
    • 패키지 계층과 디렉토리 계층이 맞는지를 확인한다.
    • javac에는 언제나 -d 옵션을 사용한다.
    • \lib\ext에 어떤 것도 두지 않는다.
    • jre\lib\endorsed에 어떤 것도 두지 않는다.
    • .class 파일과 같은 디렉토리에 .java 파일을 두지 않는다.
    • 현재 실행 디렉토리에 .java나 .class 파일들을 두지 않는다.

    공유 사이트


    마 지막 팁: classpath와 관련한 많은 문제들은 디렉토리 이름을 잘못 입력했다거나, 잘못된 디렉토리에서 컴파일 하는 등 간단한 에러에서 진화한다. 무엇이 잘못되었는지 알 수 없다면, 동료나 친구에게 물어보고 문제를 찾아야 한다. 오히려 문제에 너무 집착하여, 다른 사람들 눈에는 명확하게 보이는 버그를 못 볼 수도 있다. 두 눈은 매우 효과적인 디버깅 방법이다.

    classpath는 분명 쉬운 것은 아니다. 하지만, 관리도 가능하다. 네이밍 규약, 명령행 인자, 디렉토리 구조에 신경을 쓴다면, 실수를 최소한으로 줄이고 프로그램을 컴파일 및 실행할 수 있을 것이다.

    기사의 원문보기


    참고자료

    교육



    제품 및 기술 얻기



    토론


    필자소개



    New Orleans 태생이다. 아내 Beth와 고양이 Charm 그리고 Marjorie와 함께 Brooklyn 근처 Prospect Heights에 살고 있다. Polytechnic University의 조교수로서 자바와 객체 지향 프로그래밍을 강의하고 있다. 그의 Cafe au Lait 웹 사이트는 가장 인기 있는 자바 사이트가 되었고, Cafe con Leche는 가장 대중적인 XML사이트가 되었다. Effective XML, Processing XML with Java, Java Network Programming, Java I/O, 2nd edition를 집필했다. 그는 현재 XML 프로세스용 XOM API, Jaxen XPath 엔진 Jester 테스트 툴 작업을 하고 있다.

'It's Code' 카테고리의 다른 글

Cygwin+MinGW32 참고  (0) 2009.06.26
FILE IO  (0) 2008.09.08
리눅스 커널 소스 디렉토리  (0) 2007.08.17
리눅스 커널 소스 (The Linux Kernel Sources)  (0) 2007.08.17
JAVAScript TIP  (0) 2007.08.10

+ Recent posts