JavaScript 재입문

들어가며

새삼스래 왠 재입문? JavaScript가 the world's most misunderstood programming language (세계에서 가장 잘못 이해되고있는 프로그래밍 언어)에 소개된 것과 같이 의미있는 비판을 받고 있기 때문입니다. 장난감 정도로 비웃음을 사고 있지만, 그 속기쉬운 이면에는 몇가지 강력한 언어 요소를 내재하고 있습니다. 2005년에는 이 기술에 대한 깊은 이해가 웹 개발자 누구에게 있어서도 중요한 능력이 된다는 것을 보여주는 많은 고급 JavaScript 응용 프로그램들이 나타났습니다.

언어의 역사에서 시작하는 것이 이 이야기를 이해하는데 도움이 됩니다. JavaScript는 1995년 Netscape의 엔지니어 Brendan Eich에 의해 만들어졌고, 이른 1996년에 Netscape 2와 함께 처음 릴리즈 된것입니다. 원래 LiveScript로 불리워지기로 되어있었지만, Sun Microsystem의 Java 언어의 성공에 편승해보려고 두 언어 사이의 공통점이 매우 적음에도 불구하고, 이런 불행이 예견된 마케팅 결정에 따라 이름이 바뀌게 됩니다. 이 사실은 사상 유래가 없는 혼란의 근원이 되어버립니다.

Microsoft는 몇달 후 IE3와 함께 JavaScript와 대부분이 호환되는 JScript로 불리워지는 언어를 발표합니다. Netscape는 1997년에 ECMAScript 표준의 첫번째 판이 되는 JavaScript를 유럽 표준화 단체인 Ecma International에 보냅니다. 표준은 1999년에 ECMAScript edition 3에 따라 큰 규모의 개정을 거친 후, 현재 4번째 판이 제정 준비 중에 있지만 유래없이 아주 안정된 상태로 계속 유지되고 있습니다.

이 안정 상태는 다양한 코드 구현을 하는데 충분한 시간이기 때문에 개발자들에게는 더없이 좋은 소식입니다. 저는 대부분 이 3판에 집중하려고 합니다. 친숙함을 위하여 JavaScript 전반에 걸쳐 여기서 사용된 용어를 준수하겠습니다.

대부분의 프로그래밍 언어와는 달리, JavaScript 언어는 입출력 개념이 없습니다. 호스트 환경 아래에서 스크립트 언어로서 동작하도록 디자인 되어있고, 따라서 외부 세계와 통신하기위해 호스트 환경이 제공하는 메커니즘에 의존합니다. 대부분의 경우 일반적인 호스트 환경은 브라우저이지만 JavaScript 인터프리터는 Adobe Acrobat, Photoshop, Yahoo! 위젯 엔진, 등의 제품에서도 발견할 수 있습니다.

개요

어떤 언어에서라도 기초가 되는 부분인 타입을 살펴보는 것부터 시작해봅시다. JavaScript 프로그램은 값을 다루고 해당 값은 모두 타입을 가지고 있습니다. JavaScript의 타입은 다음과 같습니다:

... 오, 그리고 약간 특별한 타입인 정의되지않음(Undefined) 와 널(Null) 이 있습니다. 또한 객체의 특별한 종류인 배열(Array) 객체. 그리고 자유롭게 사용할 수 있는 날짜(Date) 객체 와 정규식(RegExp) 객체가 있습니다. 그리고 기술적으로 정확히 말해 함수(Function)는 단지 객체의 특별한 타입으로 취급됩니다. 따라서 타입 구조도를 정리해보면 다음과 같이 됩니다:

  • 수 (Number)
  • 문자열 (String)
  • 부울 (Boolean)
  • 객체 (Object)
    • 함수 (Function)
    • 배열 (Array)
    • 날짜 (Date)
    • 정규식 (RegExp)
  • 널 (Null)
  • 정의되지않음 (Undefined)

그리고 또 몇 가지 오류 타입이 내장되어 있습니다. 그렇지만 처음 구조도를 기억하고만 있으면 다른 것들도 아주 쉽게 이해할 수 있을 것입니다.

수 (Numbers)

설계 명세서에 의하면 JavaScript에서 수는 "이중정밀도 64비트 형식 IEEE 754 값"으로 정의됩니다. 이것은 몇가지 흥미로운 결과를 가져옵니다. JavaScript에는 정수와 같은 것이 존재하지 않으므로, C 나 Java 에서 수학 계산을 한 경험이 있다면 산술할 때 약간 조심할 필요가 있습니다. 다음과 같은 경우를 주의해야 합니다:

0.1 + 0.2 = 0.30000000000000004

덧셈, 뺄셈, 계수 (또는 나머지) 연산을 포함하는 표준 산술 연산자가 지원됩니다. 또한 앞에서 언급하는 것을 깜박 잊은 고급 수학 함수와 상수를 다루기 위한 수학(Math)으로 불리워지는 내장 객체가 있습니다:

Math.sin(3.5);
d = Math.PI * r * r;

내장 parseInt() 함수를 사용하여 문자열을 정수로 변환할 수 있습니다. 이는 다음과 같이 옵션으로 주어지는 두번째 매개변수를 밑으로 하여 수행할 수 있습니다:

> parseInt("123", 10)
123
> parseInt("010", 10)
10

밑을 주지 않으면, 다음과 같이 예상치 못한 결과를 얻을 수 있습니다:

> parseInt("010")
8

이 같은 결과는 parseInt 함수가 0으로 시작되는 문자열을 8진수로 취급하기 때문에 발생합니다.

만약 이진수를 정수로 변환하고 싶다면, 밑을 바꾸기만하면 됩니다:

> parseInt("11", 2)
3

문자열이 수가 아닌 경우 NaN ("Not a Number" (수가 아님)을 줄인 약자)로 불리워지는 특별한 값을 돌려줍니다:

> parseInt("hello", 10)
NaN

NaN 는 독성을 가지고 있습니다: 어떤 수학 연산의 입력값으로써 주어지면 그 결과는 역시 NaN가 되기 때문입니다:

> NaN + 5
NaN

내장 isNaN() 함수를 사용해서 NaN 인지 여부를 검사할 수 있습니다:

> isNaN(NaN)
true

JavaScript는 또 특별한 값 Infinity와 -Infinity를 가지고 있습니다:

> 1 / 0
Infinity
> -1 / 0
-Infinity

문자열 (Strings)

JavaScript에서 문자열은 문자 하나하나가 연결되어 만들어진 것입니다. 좀 더 정확히 말하자면, 각각이 16비트로 표현된 유니코드 문자들이 길게 이어져있는 것입니다. 이는 국제화(i18n, internationalization) 하려하는 누구에게라도 환영받을만한 소식입니다.

한 개의 문자를 나타내려면 길이가 1인 문자열을 사용하면 됩니다.

문자열의 길이를 알고싶다면, 해당 문자열의 length 속성(해당 객체가 소유하고 있는 성질을 나타내는 값)에 접근하면 됩니다:

> "hello".length
5

우리의 첫 JavaScript 객체입니다! 문자열도 역시 객체로 취급된다고 언급했던적이 있죠? 다음과 같이 메소드까지 있는 확실한 녀석입니다:

> "hello".charAt(0)
h
> "hello, world".replace("hello", "goodbye")
goodbye, world
> "hello".toUpperCase()
HELLO

이외의 타입들

JavaScript는 의도적으로 값이 없음을 가리키는 '객체' 타입의 객체인 null과 초기화되지 않은 값 — 아직 어떤 값도 주어지않은(할당되지않은) 변수임을 가리키는 '정의되지 않음' 타입의 객체인 undefined로 구분됩니다. 값에 대해서 나중에 언급할 것이지만 JavaScript에서 변수에 값을 주지않고 선언하는 것이 가능합니다. 이럴 경우, 변수의 타입은 undefined이 되는 것입니다.

JavaScript는 true 와 false 값 (둘은 모두 키워드로 예약되어있는 값)을 가질 수 있는 부울 타입을 가지고 있습니다. 다음과 같은 규칙에 따라 어떤 임의의 값을 부울값으로 변환할 수 있습니다:

  1. false0, 빈 문자열 (""), 수가 아님을 뜻하는 NaNnull, 와 undefined은 모두 false가 됩니다.
  2. 다른 모든 값은 true가 됩니다.

이 변환은 Boolean() 함수를 써서 명시적으로 이 작업을 수행하실 수 있습니다:

> Boolean("")
false
> Boolean(234)
true

하지만 반드시 이렇게 할 필요는 거의 없습니다. JavaScript는 이러한 변환 작업을 if 문 (아래를 보세요)과 같이 부울값이 필요한 경우를 만나게되면 자동으로 사용자가 모르는 사이에 처리해버리기 때문입니다. 이러한 이유로 인해 우리는 가끔 부울 타입으로 변환되었을 때, true와 false이 됨을 의미하는 값들을 각각 "참 값"과 "거짓 값"으로 부를 것입니다. 또는 각각 "참으로 취급되다"와 "거짓으로 취급되다"라는 식으로 불릴 수도 있습니다.

부울 연산자는 && (논리적 와, 그리고), || (논리적 또는), 그리고 ! (논리적 부정)이 지원됩니다. 아래에서 다시 언급하겠습니다.

변수 (Variables)

JavaScript에서 새로운 변수는 var 키워드로 선언됩니다:

var a;
var name = "simon";

만약 변수에 아무런 값을 주지 않고 선언하면 해당 변수의 타입은 undefined가 됩니다.

JavaScript에는 블록 유효 범위가 따로 없습니다. 여기에 대한 것은 블록 문장에서 참고바랍니다.

연산자 (Operators)

JavaScript의 산술 연산자로는 +-*/%(나머지 연산자)가 있습니다. 값은 = 연산자로 할당할 수 있고, += 와 -=처럼 다른 연산자를 같이사용해서 할당할 수 있습니다. 이렇게 쓰인 연산자는 x = x 연산자 y와 같은 결과를 나타냅니다.

x += 5
x = x + 5

++ 와 -- 를 각각 점진적인 증가와 감소에 사용할 수 있습니다. 이들은 또한 전처리 또는 후처리 연산자로 사용될 수 있습니다.

+ 연산자는 문자열 이어붙이기도 합니다:

> "hello" + " world"
hello world

문자열에 어떤 수 (또는 다른 값)를 더하면 일단 모두 문자열로 바뀌게 됩니다. 다음 예를 보시면 무슨 말씀인지 아실 수 있을겁니다:

> "3" + 4 + 5
345
> 3 + 4 + "5"
75

빈 문자열에 어떤 값을 더하는 것은 해당 값을 문자열로 바꾸는 요령입니다.

JavaScript에서 비교는 <><= 와 >= 를 통해 가능합니다. 이 연산자들은 문자열과 수 양쪽 모두에서 동작합니다. 상동은 약간 직관성이 떨어지는데 이중 등호 (==) 연산자는 서로 다른 타입을 줄 경우 타입 강제 변환을 수행하기 때문에 다음과 같이 때때로 기대하지 않은 결과를 내보내기 때문입니다:

> "dog" == "dog"
true
> 1 == true
true

타입 강제 변환을 하지 않게 하려면, 삼중 등호 연산자 (===)를 사용해야합니다:

> 1 === true
false
> true === true
true

이와 비슷하게 != 와 !== 연산자가 있습니다.

JavaScript는 값을 비트로 취급하는 연산자도 가지고 있습니다. 사용하고 싶을 때 언제라도 사용할 수 있도록 말이죠.

제어 구조

JavaScript는 C 계열의 다른 언어들과 비슷한 제어 구조를 가지고 있습니다. 조건문은 if 와 else를 지원하는데, 원하시는대로 얼마든지 중첩 시켜서 사용할 수 있습니다:

var name = "kittens";
if (name == "puppies") {
  name += "!";
} else if (name == "kittens") {
  name += "!!";
} else {
  name = "!" + name;
}
name == "kittens!!"

JavaScript는 while 반복문과 do-while 반복문도 사용할 수 있습니다. 첫번째 것은 단순 반복에 유용하게 사용할 수 있고, 두번째 것은 반복문이 반드시 적어도 한번이상 실행 되도록 하고 싶을 때 사용할 수 있습니다:

while (true) {
  // an infinite loop!
}

do {
  var input = get_input();
} while (inputIsNotValid(input))

JavaScript의 for 반복문은 C 와 Java의 그것과 같습니다. 말하자면, 반복문에 필요한 제어 정보를 한줄에 표현할 수 있다는 이야기지요.

for (var i = 0; i < 5; i++) {
  // Will execute 5 times
}

&& 와 || 연산자는 첫번째 식을 평가한 결과에 따라서 두번째 식을 평가를 실행하는 단축평가(short-circuit) 논리를 사용합니다. 이는 다음과 같이 객체에 접근하기 전에 null 객체인지, 아닌지를 검사하는데 유용하게 사용될 수 있습니다:

var name = o && o.getName();

또는 기본 값 설정을 위해서 다음과 같이 이 성질을 사용할 수 있습니다:

var name = otherName || "default";

JavaScript는 한줄로 조건문을 쓸 수 있게 해주는 삼중 연산자도 가지고 있습니다:

var allowed = (age > 18) ? "yes" : "no";

스위치 문은 숫자나 문자열을 기반으로 다중 분기되는 문장을 작성하는데 사용될 수 있습니다:

switch(action) {
    case 'draw':
        drawit();
        break;
    case 'eat':
        eatit();
        break;
    default:
        donothing();
}

break 문장을 추가하지 않았다면, 다음 단계로 "넘어가서" 실행합니다. 이렇게 되는 것을 기대하는 것은 매우 드문경우 입니다. 실은 디버깅하는데 용이하도록 하기위해 주석으로서 일부러 붙여놓은 넘어가기 이름표 입니다:

switch(a) {
    case 1: // fallthrough
    case 2:
        eatit();
        break;
    default:
        donothing();
}

default 구문은 선택적으로 적용할 수 있습니다. 스위치와 케이스 부분에서 원할경우 둘다 식을 사용할 수 있습니다. === 연산자를 사용해서 두 문장을 비교해보시기 바랍니다.

switch(1 + 3):
    case 2 + 2:
        yay();
        break;
    default:
        neverhappens();
}

객체 (Objects)

JavaScript 객체는 간단히 이름-값 쌍(name-value pairs)의 모임입니다. 그렇기 때문에, JavaScript의 객체의 모임은 다음과 비슷하다고 할 수 있습니다:

  • Python의 Dictionaries
  • Perl 과 Ruby의 Hashes
  • C 와 C++ 의 Hash tables
  • Java 의 HashMaps
  • PHP의 Associative arrays

이 데이터 구조가 매우 광범위하게 사용된다는 사실은 활용 방도가 다양함을 입증합니다. JavaScript내 모든 것 (코어 타입들은 제외)은 객체로 취급되기 때문에 어떤 JavaScript 프로그램도 기본적으로 해쉬 테이블을 검색하는데 필요한 출중한 성능을 가지고 있습니다. 매우 빠르기 때문에 장점이 됩니다!

값은 객체를 포함하여 아무 JavaScript 값이 될 수 있는 반면, "이름" 부분은 JavaScript 문자열 입니다. 이는 무작위적인 복잡성을 가지는 데이터 구조를 만들 수 있도록 해줍니다.

빈 객체를 생성하는데 두가지 방법이 있습니다:

var obj = new Object();

와:

var obj = {};

이들은 의미적으로 동치입니다. 두번째 방법은 객체의 엄밀한 구문으로 말할 수 있으며 더 편리합니다. 객체 엄밀 구문이 초기 버전에는 없었기 때문에 예전 방법을 사용한 코드를 많이 볼 수 있습니다.

일단 생성되면, 객체의 속성에 다음의 두가지 방법들 중 한가지로 접근할 수 있습니다:

obj.name = "Simon"
var name = obj.name;

그리고...

obj["name"] = "Simon";
var name = obj["name"];

이들은 의미적으로 역시 같습니다. 두번째 방법은 속성의 이름이 실행시간(run-time)에 계산될 수 있는 문자열로 주어집니다. 또한 예약된 단어(키워드)로 되어있는 이름으로 객체의 속성을 설정하거나 얻어낼 수 있습니다:

obj.for = "Simon"; // 구문 오류, for 가 예약된 단어(키워드)이기 때문에
obj["for"] = "Simon"; // 정상 동작

객체 엄밀 구문으로 객체의 전체적인 구조를 초기화 할 수 있습니다:

var obj = {
    name: "Carrot",
    "for": "Max",
    details: {
        color: "orange",
        size: 12
    }
}

속성에 연속적으로 접근할 수 있습니다:

> obj.details.color
orange
> obj["details"]["size"]
12

배열 (Arrays)

JavaScript에서 배열은 실제로는 객체의 특별한 타입입니다. (숫자로 나타낸 속성은 자연스럽게 [] 구문만을 사용해서 접근하게 되므로) 일반 객체와 많이 비슷하게 동작하지만, 이 객체는 'length'라는 한가지 마법 속성을 가집니다. 이는 항상 배열에서 가장 큰 인덱스보다 하나 더 큰 값으로 존재합니다.

배열을 생성하는 예전 방법은 다음과 같습니다:

> var a = new Array();
> a[0] = "dog";
> a[1] = "cat";
> a[2] = "hen";
> a.length
3

한가지 더 편리한 배열 표현 방법은 배열 상수를 사용하는 것입니다:

> var a = ["dog", "cat", "hen"];
> a.length
3

배열 상수 끝에 콤마(",")를 꼬리로 남겨두는 것은 브라우저마다 다르게 처리하므로 그렇게 하지는 마시기 바랍니다.

array.length 는 배열에 들어있는 항목의 수를 반드시 반영하지는 않는다는 점을 주의하시기 바랍니다. 다음과 같은 경우를 고려해보겠습니다:

> var a = ["dog", "cat", "hen"];
> a[100] = "fox";
> a.length
101

기억해두세요 - 배열의 length 속성은 최대 인덱스에 하나를 더한 값일 뿐입니다.

존재하지 않는 배열 인덱스를 참조하려고하면 다음과 같이 undefined 을 얻게됩니다:

> typeof(a[90])
undefined

위의 사항들을 감안하면 배열을 반복문으로 처리할 때 다음과 같은 방법으로 처리하실 수 있을 것입니다:

for (var i = 0; i < a.length; i++) {
    // a[i] 로 뭔가를 수행
}

이 코드는 루프를 반복할 때마다 배열의 length 속성을 찾아보게되므로 약간 비 효율적입니다. 개선책은:

for (var i = 0, len = a.length; i < len; i++) {
    // a[i] 로 뭔가를 수행
}

보다 더 좋은 관용적인 코드는:

for (var i = 0, item; item = a[i]; i++) {
    // item 으로 뭔가를 수행
}

여기서 우리는 두개의 변수를 설정합니다. for 루프 중간 부분의 할당문은 참인지 거짓인지 테스트 하는데, 참으로 밝혀지면, 루프를 계속 돕니다. i가 루프를 돌 때마다 하나씩 증가하기 때문에 배열의 항목들은 순차적으로 item 변수에 할당됩니다. "거짓으로 취급되는" 항목 (undefined와 같은 항목)을 발견하면 루프는 멈춥니다.

이 요령은 "거짓으로 취급되는" 값이 포함되지 않은, 예를 들어 객체의 배열이나 DOM 노드들과 같은 배열에 사용되어야만 합니다. 0을 포함하는 수로 표현된 데이터나 빈 문자열을 포함하는 문자열 데이터에 대하여 반복문을 적용할 경우에는 i, j를 사용하는 관용 코드를 대신 사용해야 합니다.

반복문을 사용하는 또다른 방법은 for...in 루프를 사용하는 것입니다. 누군가가 Array.prototype에 새로운 속성을 추가한 경우, 이 루프에 의해 그 속성도 반복된다는 점을 주의하시기 바랍니다:

for (var i in a) {
  // a[i] 으로 뭔가를 수행
}

배열에 어떤 항목을 덧붙이길 원하면 다음과 같이 안전한 방법으로 수행할 수 있는 방법이 있습니다:

a[a.length] = item;                 // a.push(item); 와 같음

a.length는 가장 큰 인덱스의 하나더 큰 값이기 때문에 배열 끝의 빈 공간에 할당한다는 점을 확신할 수 있습니다.

배열 객체은 다음과 같이 많은 메소드를 사용할 수 있습니다:

a.toString(), a.toLocaleString(), a.concat(item, ..), a.join(sep),
a.pop(), a.push(item, ..), a.reverse(), a.shift(), a.slice(start, end),
a.sort(cmpfn), a.splice(start, delcount, [item]..), a.unshift([item]..)
  • concat 해당 배열에 지정한 항목들을 추가한 새로운 배열을 돌려줍니다
  • pop 마지막 항목을 제거한 다음 돌려둡니다
  • push 마지막에 하나 이상의 항목을 추가합니다 (ar[ar.length]와 같이)
  • slice 배열의 일부분을 돌려줍니다
  • sort 비교에 사용할 함수를 따로 지정할 수 있습니다
  • splice 구역을 삭제하거나 항목을 추가해서 배열을 수정할 수 있게합니다
  • unshift 배열의 시작부분에 항목을 붙일 수 있습니다

함수 (Functions)

객체와 마찬가지로, 함수는 JavaScript를 이해하는데 핵심이되는 컴포넌트입니다. 가장 기본적인 함수의 예는 다음과 같습니다:

function add(x, y) {
    var total = x + y;
    return total;
}

이 예는 기본 함수에 대해 알아야할 모든 것을 보여주고 있습니다. JavaScript 함수는 0 이상의 이름이 있는 매개변수를 가질 수 있습니다. 함수의 본체는 원하는 만큼의 문장을 포함할 수 있고 해당 함수에 지역적으로 변수를 보유하도록 선언할 수 있습니다. return 문은 언제나 값을 돌려주고 함수의 실행을 끝내는데 사용될 수 있습니다. 리턴 문이 없으면 (혹은 값이 없는 리턴이) 사용되면, JavaScript는 undefined을 돌려줍니다.

이름 붙여진 매개변수들은 다른 어떤 것보다도 해당 함수가 어떤 함수인지 설명해주는 좋은 역할을 할 수 있습니다. 해당 함수가 원하는 매개변수를 주지않고 함수를 호출할 수 있지만 그럴 경우 해당 변수들은 undefined로 설정됩니다.

> add()
NaN // undefined에 대해 덧셈을 수행할 수 없습니다

함수가 기대하는 원래의 매개변수보다 많은 배개변수를 넘겨줄 수도 있습니다:

> add(2, 3, 4)
5 // 처음의 두 수가 더해집니다. 4는 무시됨

이 예는 조금 바보같아보이지만, 함수는 추가적으로 주어진 변수는 해당 함수 내에서 매개변수로 넘겨진 모든 값을 가지고 있는 배열과 비슷한 객체인 arguments로 접근할 수 있습니다. 우리가 원하는만큼 값을 취하는 add 함수를 다시 써보겠습니다:

function add() {
    var sum = 0;
    for (var i = 0, j = arguments.length; i < j; i++) {
        sum += arguments[i];
    }
    return sum;
}

> add(2, 3, 4, 5)
14

하지만 2 + 3 + 4 + 5을 직접쓰는 것보다 더 좋지는 않으니, 평균내는 함수를 만들어 보겠습니다:

function avg() {
    var sum = 0;
    for (var i = 0, j = arguments.length; i < j; i++) {
        sum += arguments[i];
    }
    return sum / arguments.length;
}
> avg(2, 3, 4, 5)
3.5

이건 매우 유용합니다만 새로운 문제점도 함께 따라왔습니다. avg() 함수는 콤마로 구분된 매개변수 목록을 취하지만, 배열의 평균을 내고 싶은 경우라면요? 함수를 다음과 같이 다시 쓰기만 하면 됩니다:

function avgArray(arr) {
    var sum = 0;
    for (var i = 0, j = arr.length; i < j; i++) {
        sum += arr[i];
    }
    return sum / arr.length;
}
> avgArray([2, 3, 4, 5])
3.5

하지만 우리가 이미 만든 함수를 다시 사용할 수 있다면 좋을 것입니다. 운이 좋게도 JavaScript는 함수 객체라면 모두 가지게 되는 apply() 메소드를 사용해서 임의의 매개변수 배열을 함수에 넘겨줄 수 있습니다.

> avg.apply(null, [2, 3, 4, 5])
3.5

두번째 매개변수 apply()는 배열을 매개변수로 사용합니다. 첫번째 매개변수는 나중에 설명하도록 하겠습니다. 이는 함수가 역시 객체임을 명확히 해주는 사실입니다.

JavaScript는 익명의 함수를 만들 수 있도록 허용하고 있습니다.

var avg = function() {
    var sum = 0;
    for (var i = 0, j = arguments.length; i < j; i++) {
        sum += arguments[i];
    }
    return sum / arguments.length;
}

이것은 의미적으로 function avg() 형식과 같습니다. 문장의 어느 곳에나 일반적인 방식으로 완전한 함수 정의를 넣을 수 있도록 허용하는 것이기 때문에 매우 강력합니다. 이는 다양한 요령을 부릴 수 있게합니다. 다음 예는 C 의 블록 유효 범위를 적용 시킨 것 처럼 지역 변수를 "숨기는" 요령을 보여줍니다:

> var a = 1;
> var b = 2;
> (function() {
    var b = 3;
    a += b;
})();
> a
4
> b
2

JavaScript는 재귀적으로 함수를 부를 수 있습니다. 이는 브라우저 DOM 등에서 얻을 수 있는 트리 구조를 다루는데 유용합니다.

function countChars(elm) {
    if (elm.nodeType == 3) { // TEXT_NODE
        return elm.nodeValue.length;
    }
    var count = 0;
    for (var i = 0, child; child = elm.childNodes[i]; i++) {
        count += countChars(child);
    }
    return count;
}

다음의 예는 익명 함수를 사용함에 있어 잠재적인 문제점을 보여줍니다: 이름이 없으면 어떻게 재귀적으로 부를 수 있을까요? 답은 매개변수의 목록으로서의 역할을 수행함과 동시에 arguments.callee로 불리우는 속성을 제공하는 arguments 객체에 나와있습니다. 이는 현재의 함수를 반영하기 때문에 익명 함수도 재귀적으로 부를 수 있게 해줍니다:

var charsInBody = (function(elm) {
    if (elm.nodeType == 3) { // TEXT_NODE
        return elm.nodeValue.length;
    }
    var count = 0;
    for (var i = 0, child; child = elm.childNodes[i]; i++) {
        count += arguments.callee(child);
    }
    return count;
})(document.body);

arguments.callee는 현재 함수이고 모든 함수는 객체이므로, 같은 함수를 여러번 부르는 동안의 정보를 저장하는데 arguments.callee를 사용할 수 있습니다. 다음의 예는 함수 자체가 몇 번 불리워졌는지 기억하는 함수 입니다:

function counter() {
    if (!arguments.callee.count) {
        arguments.callee.count = 0;
    }
    return arguments.callee.count++;
}

> counter()
0
> counter()
1
> counter()
2

사용자 정의 객체

고전 객체지향 프로그래밍에서 객체는 데이터와 해당 데이터들을 다루는 메소드의 집합이었습니다. 이름과 성을 필드로 가지고 있는 'person' 객체를 고려해보도록 합시다. 이름을 표시하는 두가지 방법이 있을 수 있습니다. 예를 들어, "이름 성" 또는 "성, 이름" 이런 식으로 말이죠. 이전에 다룬 함수와 객체를 사용해서 이를 표현하면 다음과 같습니다:

function makePerson(first, last) {
    return {
        first: first,
        last: last
    }
}
function personFullName(person) {
    return person.first + ' ' + person.last;
}
function personFullNameReversed(person) {
    return person.last + ', ' + person.first
}
> s = makePerson("Simon", "Willison");
> personFullName(s)
Simon Willison
> personFullNameReversed(s)
Willison, Simon

이렇게하면 작동하긴하지만, 너무 어설픕니다. 전역 이름공간에 관련 함수가 주렁주렁 달려야 하니까요. 정말 우리에게 필요한 것은 객체에 함수를 붙여놓는 것입니다. 함수는 객체이기 때문이 이렇게 하는 것은 쉽습니다:

function makePerson(first, last) {
    return {
        first: first,
        last: last,
        fullName: function() {
            return this.first + ' ' + this.last;
        },
        fullNameReversed: function() {
            return this.last + ', ' + this.first;
        }
    }
}
> s = makePerson("Simon", "Willison")
> s.fullName()
Simon Willison
> s.fullNameReversed()
Willison, Simon

여기서 우리가 기존에 보지 못했던 뭔가를 볼 수 있습니다: 'this' 키워드가 바로 그것입니다. 함수 안쪽에서 사용되어, 'this'는 현재 객체를 참조합니다. 그것이 실재로 의미하는 바는 당신이 부른 바로 그 함수를 지정하는 것입니다. 객체에서 점 표기법이나 꺽쇠 괄호 표기법을 사용해서 부른 경우, 해당 객체는 'this'가 됩니다. 함수를 부르는데 점 기호를 사용하지 않은 경우, 'this'는 전역 객체를 참조합니다. 이 때문에 다음과 같은 실수가 자주 발생합니다:

> s = makePerson("Simon", "Willison")
> var fullName = s.fullName;
> fullName()
undefined undefined

fullName()을 불렀을 때, 'this'는 전역 객체에 속해있습니다. 불러낼 first 또는 last 전역 변수가 없기 때문에 각각에 대해 undefined 결과를 얻게됩니다.

makePerson 함수를 향상시키는데 'this' 코드의 잇점을 취할 수 있도록 해보겠습니다:

function Person(first, last) {
    this.first = first;
    this.last = last;
    this.fullName = function() {
        return this.first + ' ' + this.last;
    }
    this.fullNameReversed = function() {
        return this.last + ', ' + this.first;
    }
}
var s = new Person("Simon", "Willison");

여기서 'new'라는 또다른 키워드를 도입했습니다. new는 'this'와 깊게 연관되어 있습니다. 새로운 빈 객체를 만든 다음 지정된 함수를 불러 새로운 객체를 'this' 에 설정합니다. 'new' 에 의해 불리워지도록 디자인된 함수는 컨스트럭터 함수라고 불리워집니다. 보통 실재에서는 이러한 함수의 첫자를 대문자로 써서 new로 불리워질 컨스트럭터 함수임을 나타냅니다.

우리가 만들었던 person 객체가 점점 개선되고 있지만, 아직 좀 생뚱맞은 면이 있습니다. 매번 person 계열의 객체를 만들 때마다 그 안에 2개의 새로운 함수 객체를 만들어줘야 하는 걸까요? 이 코드를 공유하도록 해보면 어떨까요?

function personFullName() {
    return this.first + ' ' + this.last;
}
function personFullNameReversed() {
    return this.last + ', ' + this.first;
}
function Person(first, last) {
    this.first = first;
    this.last = last;
    this.fullName = personFullName;
    this.fullNameReversed = personFullNameReversed;
}

이렇게 메소드 함수를 한번 만들어서 컨스트럭터 내에 해당 메소드들을 참조하도록 할당하는게 하는게 더 낫습니다. 이보다 더 낫게 할 수 있을까요? 네, 그렇게 할 수 있습니다:

function Person(first, last) {
    this.first = first;
    this.last = last;
}
Person.prototype.fullName = function() {
    return this.first + ' ' + this.last;
}
Person.prototype.fullNameReversed = function() {
    return this.last + ', ' + this.first;
}

Person.prototype는 인스턴스된 모든 Person 객체에서 공유할 수 있는 객체입니다. 이는 찾아보기 체인의 한 부분을 이룹니다. (이건 "prototype chain"이라는 특수한 이름을 따로 가지고 있습니다) 다시 말해, Person 객체의 설정되지 않은 속성에 접근을 시도할 때마다 JavaScript는 Person.prototype에 대신 존재하는 속성이 있는지 없는지 살펴봅니다.

이것은 믿을 수 없을 정도로 강력한 도구입니다. JavaScript는 어떤 객체의 prototype을 프로그램 내에서 언제든지 변형할 수 있게합니다. 다시 말해, 다음과 같이 실행시간 언제든지 이미 존재하는 객체에 추가적인 메소드 추가가 가능하다는 이야기입니다:

> s = new Person("Simon", "Willison");
> s.firstNameCaps();
TypeError on line 1: s.firstNameCaps is not a function
> Person.prototype.firstNameCaps = function() {
    return this.first.toUpperCase()
}
> s.firstNameCaps()
SIMON

흥미롭게도, 기본적으로 들어있는 JavaScript 객체 prototype에 더할 수 있습니다. String 객체에 문자열을 뒤집어서 돌려주는 메소드를 추가해보도록 합시다:

> var s = "Simon";
> s.reversed()
TypeError on line 1: s.reversed is not a function
> String.prototype.reversed = function() {
    var r = "";
    for (var i = this.length - 1; i >= 0; i--) {
        r += this[i];
    }
    return r;
}
> s.reversed()
nomiS

우리가 추가한 새로운 메소드는 심지어 문자열 자체에서도 동작합니다!

> "This can now be reversed".reversed()
desrever eb won nac sihT

기존에 언급한 바와같이, prototype은 체인의 한 부분을 이룹니다. 해당 체인의 루트는 객체를 문자열로 나타내려할 때 부르게되는 toString() 메소드를 포함하는 Object.prototype 입니다. 이 메소드는 우리의 Person 객체의 디버깅에 유용하게 사용할 수 있습니다:

> var s = new Person("Simon", "Willison");
> s
[object Object]
> Person.prototype.toString = function() {
    return '<Person: ' + this.fullName() + '>';
}
> s
<Person: Simon Willison>

avg.apply()의 첫번째 매개변수가 null 이었던걸 기억하십니까? apply()에 적용되는 첫번째 매개변수는 'this'로 취급될 수 있는 객체이어야만 합니다. 예를 들어, 'new'의 간단한 실례를 보도록 합시다:

function trivialNew(constructor) {
    var o = {}; // 빈 객체를 생성
    constructor.apply(o, arguments);
    return o;
}

이것은 prototype 체인을 설정하지 않으므로 new의 완벽한 대체물이 될 수 없습니다. apply() 는 설명하기 어렵습니다. 자주 사용하지는 않아도 알아두면 좋겠지요.

apply() call 이름을 가진 자매 함수를 가지고 있습니다. 이는 역시 'this'를 다시 설정할 수 있게하지만, 동시에 apply()와는 대조적으로 확장된 메소드 리스트를 가지게됩니다.

function lastNameCaps() {
    return this.last.toUpperCase();
}
var s = new Person("Simon", "Willison");
lastNameCaps.call(s);
// 위의 구문은 다음과 같습니다:
s.lastNameCaps = lastNameCaps;
s.lastNameCaps();

내장 함수

JavaScript 함수 선언은 내부에 다른 함수를 허용합니다. 우리는 이것은 예전에 makePerson() 함수 최초 버전에서 한번 본적이 있습니다. JavaScript 내장 함수의 세부사항 중에 중요한 것은 다음과 같이 그들이 속해있는 부모 함수 범위에 있는 변수에 접근할 수 있다는 사실입니다:

function betterExampleNeeded() {
    var a = 1;
    function oneMoreThanA() {
        return a + 1;
    }
    return oneMoreThanA();
}

이 사실은 많이 사용하는 유틸리티를 쓸 때 좀 더 유지관리가 쉬운 코드를 작성할 수 있도록 해줍니다. 어떤 함수가 사용하는 다른 몇개의 함수가 작성한 코드의 다른 부분에는 별로 유용하지 않는 코드라면, 이런 유틸리티 함수를 해당 함수를 불러서 사용하는 함수의 내부에 내장되도록 할 수 있습니다. 전역 범위에 들어 있는 함수의 수를 낮게 유지합니다. (이렇게 하는 것은 항상 좋다고 볼 수 있습니다.)

이것은 또한 전역 변수에 대한 유혹을 뿌리칠 수 있는 좋은 대안이 됩니다. 복잡한 코드를 쓸 때, 보통 다양한 함수들간에 변수를 공유할 수 있도록 전역 변수 사용 유혹을 받기 쉽습니다 - 코드 유지 보수가 힘들죠. 내장 함수는 그 부모 함수의 변수를 공유할 수 있으므로, 이 방법을 사용하면 전역 변수 이름공간을 과도하게 사용하지 않고도 연관된 함수를 묶을 수 있습니다. - '지역 전역'이라고 불러도 괜찮겠네요. 이 기술을 사용할 때는 주의를 요하겠지만, 반드시 알아둬야할 유용한 기술입니다.

폐포 (Closures)

폐포 (글자 그대로 한국어로 해석하면 닫힌 주머니)는 JavaScript가 제공해야만 하는 가장 막강한 추상 계념으로 우리를 이끕니다 - 하지만 동시에 잠재적으로 가장 혼란스럽기도 합니다. 다음 함수는 무엇을 하는 걸까요?

function makeAdder(a) {
    return function(b) {
        return a + b;
    }
}
x = makeAdder(5);
y = makeAdder(20);
x(6)
?
y(7)
?

makeAdder 함수의 이름은 다음과 같은 과정을 거쳐 반드시 없어집니다: 해당 함수가 한 매개변수를 받아 불리워졌을 때, 생성될 때 주어진 매개변수를 더하는 새 'adder' 함수를 생성합니다.

여기서 일어나는 일은 다른 함수의 내에 정의된 어떤 함수가 외부 함수의 변수에 액세스한다는 점에서 앞에 언급한 내장 함수에서 일어나는 일과 매우 비슷합니다. 한가지 다른 점은 외부 함수가 리턴 된다는 점인데, 상식적으로 그것에 들어 있는 변수는 사라진다고 볼 수 있습니다. 하지만 그들은 여전히 존재합니다 - 그렇지 않으면 adder 함수는 동작하지 않겠지요. 게다가, makeAdder 지역 변수의 서로 다른 두 "복사본"이 존재합니다 - 하나의 a는 5이고, 다른 하나의 a는 20이죠. 따라서 해당 함수를 부른 결과는 다음과 같습니다:

x(6) // 11을 돌려줌
y(7) // 27을 돌려줌

이건 실재로 일어나는 일입니다. JavaScript 함수가 실행될 때는 언제나, '범위' 객체가 생성되어 해당 함수내에서 생성된 지역 변수를 여기에 저장하고 있습니다. 함수 매개변수로서 넘겨진 어떤 변수라도 여기에 초기값으로 저장하고 있습니다. 이것은 모든 전역 변수와 함수가 들어있는 전역 객체와 비슷하지만, 두가지 중요한 차이점이 있습니다. 첫번째로, 함수가 실행될 때마다 새로운 범위 객체가 생성된다는 점과, 두번째로, (브라우저에서 window로 접근가능한) 전역 객체와 달리 범위 객체는 JavaScript 코드에서 직접적으로 액세스할 수 없다는 점입니다. 예를 들자면 현재 범위 객체의 속성에 반복 접근할 수 있는 수단이 없습니다.

따라서 makeAdder 가 불리워지면, 범위 객체는 makeAdder 함수에 매개변수로 넘겨진 하나의 속성 a를 가진 상태로 생성됩니다. 일반적으로 JavaScript의 가비지 컬렉터가 이때 makeAdder에 의해 생성된 범위 객체를 청소해야겠지만, 리턴된 함수가 여전히 범위 객체를 참조하고 있습니다. 결과적으로 범위 객체는 makeAdder에 의해 리턴된 함수 객체가 더는 참조되지 않을 때까지 가비지 컬렉터에 의해 정리되지 않게됩니다.

범위 객체는 JavaScript 객체 체계에서 사용되는 prototype 사슬과 비슷한 범위 사슬이라고 불리워지는 사슬을 형성합니다.

폐포는 함수와 함수에 의해 생성되는 범위 객체를 함께 지칭하는 용어입니다.

또한 폐포는 상태를 저장할 수 있도록 허용합니다 - 그렇기 때문에, 객체의 내부에서 자주 사용될 수 있는 것입니다.

메모리 누출

폐포의 부작용은 Internet Explorer에서 심각하지는 않지만 쉽게 메모리 누출이 된다는 것입니다. JavaScript는 가비지 컬렉트를 하는 언어 입니다. 객체가 생성됨에 따라서 메모리가 할당되고, 사용하고난 메모리는 더 참조하는 다른 객체가 없을 때 되돌아가는 방식으로 동작하는 언어란 말이죠. 호스트 환경에서 제공되는 객체들은 해당 환경에 의해 다뤄집니다.

브라우저 호스트는 HTML 페이지에 DOM 객체로서 표현되어있는 많은 수의 객체를 다뤄야 합니다. 이 객체들을 어떻게 할당하고 다시 거둬들일지는 브라우저 책임이죠.

Internet Explorer는 이를 위해 자신만의 고유한, JavaScript의 그것과는 다른 가비지 컬렉션 방식을 사용합니다. 두 언어간에 상호작용이 일어날 수 있고 이 과정에서 메모리 누출이 발생할 수 있습니다.

IE에서 메모리 누출은 JavaScript 객체와 고유 객체간에 참조하는 중 자기 자신을 참조 (circular reference, 순환 참조)하게 되는 일이 발생할 경우라면 언제든지 발생하게 됩니다. 다음을 고려해 보도록 합시다:

function leakMemory() {
    var el = document.getElementById('el');
    var o = { 'el': el };
    el.o = o;
}

위의 코드는 순환 참조로서 메모리 누출을 일으킵니다. IE는 완전히 다시 시작되기 전까지는 el와 o에 의해 사용되는 메모리를 반환하지 못합니다.

위의 경우는 알아채지 못하고 지나갈 확률이 높습니다. 메모리 누출은 사실 오랫동안 실행되거나 큰 데이터 구조나 반복, 순환에 의해 누출된는 메모리 양이 많은 경우에서 실질적으로 고려할만한 가치가 생깁니다.

누출이 이처럼 명확한 경우는 드뭅니다. 누출을 일으키는 데이터 구조는 수차례에 걸친 참조 구조를 가지고 있어서 순환 참조를 하고있는지 명확하지 않은 경우가 더 많습니다.

폐포는 그렇게 되도록 하지않아도 간단하게 메모리 누출을 일으킬 수 있습니다. 다음을 고려해 봅시다:

function addHandler() {
    var el = document.getElementById('el');
    el.onclick = function() {
        this.style.backgroundColor = 'red';
    }
}

위의 코드는 클릭했을때 배경색이 빨강으로 바뀌는 엘레멘트를 설정합니다. 그리고 메모리 누출도 일으킵니다. 어째서냐고요? el을 참조하면 의도와는 달리 익명 내부 함수 때문에 생성된 폐포 내에 붙잡혀 있게 되기 때문입니다. 이는 JavaScript 객체 (내부 함수)와 원시 객체 (el)간에 순환 참조를 만듭니다.

이 문제를 피할 수 있는 많은 방법이 있습니다. 가장 간단한 건 이겁니다:

function addHandler() {
    var el = document.getElementById('el');
    el.onclick = function() {
        this.style.backgroundColor = 'red';
    }
    el = null;
}

이렇게 하면 순환 참조 고리를 끊을 수 있습니다.

놀랍게도, 폐포에 의해 발생된 순환 참조를 고리를 끊기 위한 한 요령은 또다른 폐포를 추가하는 것입니다:

function addHandler() {
    var clickHandler = function() {
        this.style.backgroundColor = 'red';
    }
    (function() {
        var el = document.getElementById('el');
        el.onclick = clickHandler;
    })();
}

내부 함수는 실행되고 바로 사라지므로서, clickHandler와 함께 생성된 폐포로부터 그 내용을 숨깁니다.

폐포를 피할 수 있는 또다른 좋은 요령은 window.onunload 이벤트가 발생하는 동안 순환 참조를 끊는 것입니다. 많은 이벤트 라이브러리가 이렇게 동작합니다. 주의할 것은 그렇게 하도록하면 Firefox 1.5의 bfcache를 비활성화 하게 되므로, 별 다른 이유가 없다면 Firefox에서 unload listener를 등록해서는 안 된다는 것입니다.

원본 문서 정보

  • 저자: Simon Willison
  • 최근 갱신 날짜: March 7, 2006
  • 저작권: © 2006 Simon Willison, contributed under the Creative Commons: Attribute-Sharealike 2.0 license.
  • 추가 정보: For more information about this tutorial (and for links to the original talk's slides), see Simon's Etech weblog post.

 

출처 : https://developer.mozilla.org/ko/A_re-introduction_to_JavaScript



차례
1절. 서문
1.1절. C++ 프로그램? C++ vs. Java/PHP
1.2절. 어떤 것을 선택할 것인가. Ada95, C, C++, Java 아니면 PHP?
1.3절. 현재 C++ 컴파일러의 문제점
2절. 추천할만한 C++ 컴파일러
2.1절. MS Windows 2000/NT/95/98/ME/XP를 위한 컴파일러
2.2절. UNIX와 기타 UNIX-like 시스템들의 컴파일러
3절. 여러가지 문자열관련 class
3.1절. Multiple Inheritance - 자신만의 문자열 class 만들기
4절. String class 다운받기
4.1절. 어떻게 저자의 String class를 믿을 수 있나?
5절. String class 사용하기
5.1절. 연산자
5.2절. 함수
5.3절. String class의 이름 바꾸기
5.3.1절. Case 1: 단순한 이름 바꾸기
5.3.2절. Case 2: 이름이 겹칠 때
6절. String.h 파일
6.1절. StringBuffer.h
6.2절. StringTokenizer.h
7절. 표준 C++ 라이브러리 string class
7.1절. 예제로 살펴보는 string
7.2절. 문자열을 찾기
7.3절. string tokenizer
8절. File class
9절. C++ 에서의 메모리 할당
9.1절. C++ Zap (Delete) 함수
9.2절. my_malloc 과 my_free 의 사용
9.3절. C++ 에서의 가바지 콜렉터
10절. 포인터가 문제이다
11절. 디버깅
11.1절. 디버깅 파일
12절. C++을 위한 IDE(Integrated Development Enviroment)와 에디터
12.1절. IDE 들
12.2절. 에디터
12.3절. 다른 참고할만한 내용
13절. C++ Online 텍스트와 문서들
13.1절. C++ 사이트들
13.2절. C++ Tutorials
13.3절. 유용한 링크들
13.4절. C++ Quick-Reference
13.5절. C++ Usenet 뉴스그룹
13.6절. Java 형태의 API
14절. C++ 코딩 관습
15절. C++ 스크립트 언어
15.1절. PIKE & PHP (C/C++ Scripting Languages)
15.2절. SoftIntegration Ch (C/C++ 스크립트 언어)
15.3절. PHP (C++ Scripting Language)
16절. Templates
17절. STL References
17.1절. STL 개요
17.2절. 헤더 파일
17.3절. 컨테이너 class 인터페이스
17.4절. 벡터 : Vectors
17.4.1절. 벡터 만들기
17.4.2절. 벡터를 체크하기
17.4.3절. 벡터의 원소에 접근하기
17.4.4절. 벡터의 원소를 추가 / 삭제하기
17.4.5절. Vector Iterator
17.4.6절. 벡터의 비교
17.5절. Iterator 와 STL
17.6절. 리스트
17.7절. 집합(Set)
17.7.1절. Set을 만들기
17.7.2절. Function Objects란 무엇인가?
17.7.3절. 출력하기
17.7.4절. 원소의 수 구하기
17.7.5절. 집합이 서로 같은지 검사하기
17.7.6절. 원소를 추가하거나 삭제하기
17.7.7절. 원소를 찾기
17.7.8절. 집합 연산
17.8절. 맵
17.9절. STL 알고리즘
18절. C++에서의 쓰레드
18.1절. 쓰레드 튜토리얼
18.2절. C++에서 쓰레드 class 디자인하기
18.2.1절. 소개
18.2.2절. 쓰레드에 대한 간단한 소개
18.2.3절. 기본적인 접근방법
18.2.4절. 구현
18.2.5절. Thread Class 사용하기
18.2.6절. 결론
19절. C++ 유틸리티들
19.1절. 메모리 툴
20절. 이 문서의 다른 포맷
20.1절. Acrobat PDF 포맷
20.2절. linuxdoc 을 Docbook 포맷으로 바꾸기
20.3절. MS WinHelp 포맷으로 바꾸기
20.4절. 여러가지 포맷의 문서를 읽기
21절. 다른 언어로의 번역
22절. Copyright
23절. 부록 A String 프로그램 파일

1절. 서문

(이 문서의 최신판은 http://www.milkywaygalaxy.freeservers.com 에서 구할 수 있다)

이 문서의 목적은 C++에 관한 URL과 C++ online 책에 대한 링크, C++ 프로그래밍 팁 등을 포괄적으로 제공하는 것이다. 또한, 이 문서는 Java 스타일의 String class, string tokenizer, 메모리 함수등 일반적인 C++ 프로그램에서 널리 쓰일 수 있는 많은 함수들을 제공한다. C++ 과 Java는 오늘날 많은 소프트웨어 프로젝트에서 쓰이고 있다. 프로그래머는 C++과 Java를 번갈아가며 쓰게될 것이고, 이 Java 스타일의 class가 매우 유용함을 알게 될 것이다. 이 library와 C++ 표준 라이브러리의 사용법을 알려줄 다양한 예제가 제시될 것이다.

이 문서는 C++에 대한 교과서가 아니며, 이에 대해서는 이미 몇가지 좋은 on-line 책들이 있다. C++이 꽤 오랜시간동안 사용되어왔기 때문에, 매우 많은 수의 C++ 문서/글/튜토리얼이 인터넷상에 존재한다. 만약 당신이 C++을 처음 접하는 것이고, C++ 프로그램을 짜 본 적이 없다면, 13절장에 링크되어 있는 on-line C++ 책을 먼저 읽어보거나, Amazon 이나 barnes과 같은 곳에서 C++ 책을 사 볼 것을 추천한다.

누군가가 말했듯 - C/C++ 언어는 OS나 디바이스드라이버, 빠른 응답을 필요하는 real-time 프로그램등을 만드는 시스템 엔지니어나 쓰라고 하고, 당신은 2002년보다 컴퓨터가 몇백만배 빨라질 2005년을 생각하면 Java나 PHP-scripting을 써야한다. 하드웨어 는 점점 싸면서도, 빨라진다.


1.1절. C++ 프로그램? C++ vs. Java/PHP

C++은 가장 강력한 언어들 중 하나이고, Java나 PHP-scripting 같은 것이 나타났지만, 앞으로도 오랜 시간동안 쓰이게 될 것이다. 실시간의 매우 빠른 응답을 필요하는 프로그램은 C나 C++을 쓴다. C++은 매우 빠르게동작하고, 실제로 Java보다 10배에서 20배 정도 빠르다 . Java는 C++의 "자손"이다. Java의 단 하나의 문제점은 바로 - "Java 는 느리다!!" . VM위에서 도는 Java 바이트코드는 컴파일된 실행코드보다 느리다. Java는 JIT(Just-In-Time) 컴파일러위에서 더 빠르게 돌지만, 여전히 C++보다는 느리다. 최적화 된 C/C++ 프로그램은 JIT 나 그 이전의 컴파일러로 컴파일 된 Java 코드보다 약 3 에서 4배 정도 빠르다!! 그렇다면, 왜 사람들이 Java를 쓰는가? 이는 Java가 순수한 객체지향을 지원하고, Java의 자동화된 메모리 관리로 인해 프로그래밍하기가 쉬우며, 프로그래머들이 직접 메모리 관리하기를 싫어하기 때문이다. 이 문서는 C++의 메모리 관리를 자동화하여 훨씬 사용하기 쉽게 하고자 했다. 여기서 나오는 library는 C++을 Java 만큼 쉽게 느끼게 해줄 것이고, C++이 Java와 경쟁할 수 있도록 해줄 것이다.

수동적인 메모리 관리를 위해 C++ 프로그램 디버깅의 대부분 시간이 소모된다. 이 문서는 디버깅 시간을 줄이기 위한 몇가지 아이디어와 팁을 줄 것이다.

언제 C++을 써야하고 언제 Java/PHP를 써야하는가?

아래와 같은 경우엔 C++을 써라:

  • 실행속도와 성능이 매우 중요한 프로그램을 만들 때.

  • 만드는 프로그램의 사용자 수가 많을 때. C++은 컴파일-링킹-디버깅 사이클이 필요하기 때문에, 프로그램 개발에 더 많은 시간이 소요된다. 따라서 사용자수가 충분히 많을 때에나 적당하다. 실행파일을 만들기 위해 많은 수의 object파일을 링크하는 것은 꽤 시간이 걸린다. (링크하는데 걸리는 시간을 줄이기 위해 archive나 라이브러리, 공유 라이브러리를 사용할 수도 있다.)

  • C++프로그래밍 경험이 많을 때.

Java/PHP를 써야할 경우:

  • (C/C++로 작성되는 것에 비해서) 실행속도와 성능이 중요하지 않을 때.

  • 생산 비용을 낮추기 위해 - 컴파일-링크 사이클이 없기 때문에, Java/PHP는 C++보다 개발이 빠르다.

  • 빠른 개발이 필요할 때.

  • 코드 유지보수를 쉽게 하기 위해. C++ 을 유지보수하는 것이 Java나 PHP-scripting 보다 훨씬 어렵다.

  • Java와 PHP-scripting은 미래이다, 하드웨어의 속도는 분자와 원자, 원자보다 작은 크기의 컴퓨터 도입과 함께 급등할 것이다. 미래의 컴퓨터는 오늘날 컴퓨터의 수조배의 성능을 가질 것이다. 미래에 하드웨어 성능이 진보함과 함께, Java나 PHP-script의 실행 성능은 중요치 않게 될 것이다. 오늘날 당신이 쓰는 컴퓨터(현재는 2002년이다.)는 엄청나게 느리고, 기어가고 있으며, 충분히 빠르지 못하다.

NOTE: Java 컴파일러 (JIT 나 다른 것들)에 많은 진보가 있었다. Java 프로그램은 GNU GCJ http://gcc.gnu.org/java로 컴파일 될 수 있다. GCJ는 간편하고, 최적화되어있으며, 진보적인 Java 프로그래밍 언어를 위한 컴파일러이다. GCJ는 Java source 코드를 머신 코드로 바로 컴파일 할 수도 있고, Java 바이트코드(class file)로 컴파일 할 수도 있다.

GCJ 정보:

  • GNU GCJ 홈페이지http://gcc.gnu.org/java,

  • GNU GCJ 의 Redhat RPM http://www.redhat.com/apps/download. 여기로 가서 "Find lastest RPMs" section에서 'gcc-java'와 'libgcj'로 search하면 된다.

  • Redhat GCJ 설치 가이드 http://www.redhat.com/devnet/articles/gcj.pdf


1.2절. 어떤 것을 선택할 것인가. Ada95, C, C++, Java 아니면 PHP?

언어의 선택은 어려운 일이다. 여기엔 너무나 많은 고려할 사항이 있다 - 개발자, 사람의 능력, 비용, 툴들, 정책 (국가의 정치 정책까지도), 사업가나 회사들에 대한 영향까지. 기술적인 이유로는 최적의 언어일지라도 단순히 정치적인 결정으로 인해 선택되지 못할 수도 있다.

David Wheeler의 언어 비교를 보자. Ada 비교 차트. Ada가 93%, Java는 72%, C++은 68% C는 53%을 각각 받았다. C++과 Java는 점수면에서는 비슷하다 (4% 차이). Ada의 개발 비용은 Stephen F. Zeigler에 따르면 C++의 반절이다. Ada95는 아래에서 구할 수 있다 -

  • Ada 홈페이지 http://www.gnuada.org.

  • Google Ada index

C++ 컴파일러는 C 컴파일러보다 훨씬 복잡하고, C++은 C보다 약간 느리게 동작할 수 있다. C 컴파일러는 충분히 오랬동안 잘 쓰여져왔다.

몇몇 시스템에서, 당신은 생성된 코드를 최적화 하기 위해 몇가지 옵션을 쓸 수 있다.

오늘날, C는 주로 운영체제나 디바이스 드라이버, 빠르게 작동해야하는 프로그램을 쓰기 위해 로우레벨 시스템 프로그래밍에 쓰인다.

Note: 이 HOWTO에 제공되는 String, StringBuffer, StringTokenizer class를 이용하여 C++ 코드를 Java와 완전히 똑같이 쓸 수 있다. 이 문서의 일부는 C++로 Java class를 흉내냄으로써 C++과 Java의 차이를 줄이고자 했다. C++과 Java를 왔다갔다하는 Java 프로그래머들은 이 String class를 좋아할 것이다.

만약 C++의 작성-컴파일-디버깅-컴파일 싸이클이 싫다면, web 개발이나 일반적이 프로그래밍에 쓰일 수 있는 PHP같은 script 언어를 알아보아라. PHP나 PERL같은 script언어는 빠른 어플리케이션 개발을 가능하게 한다. PHP는 몇가지 객체지향을 위한 특징도 갖고 있다. PHP HOWTO는http://www.linuxdoc.org/HOWTO/PHP-HOWTO.html (한글번역) 에서 볼 수 있다.


1.3절. 현재 C++ 컴파일러의 문제점

C++은 C를 포함하기 때문에, C의 *나쁜* 점들을 모두 갖고 있다. 메모리의 수동 할당과 해제는 지루하고, 에러를 만들어내기 일쑤이다. ( 9.3절 를 보라).

C 프로그래밍에서는 다음과 같은 것들로 인해 메모리 릭이나 오버플로우가 매우 흔하다.

	Datatype  char * and char[]
	String functions like strcpy, strcat, strncpy, strncat, etc..
	Memory functions like malloc, realloc, strdup, etc..

char *와 strcpy의 사용은 "오버플로우""경계침범에러(fence past errors)""메모리 오염(memory corruption)""다른변수 침범(step-on-others-toe)" 이나 "메로리 릭(memory leaks)" 등의 끔찍한 메모리 문제를 일으킨다. 메모리 문제는 매우 디버깅이 힘들고, 따라서 고치기는데 많은 시간이 든다. 메모리 문제는 프로그래머의 생산성을 떨어뜨린다. 이 문서는 C++의 이러한 단점을 해결하기 위해 고안된 여러가지 방법들을 통해 프로그래머의 생산성을 높이는데 도움을 주고자 한다. 메모리 관련 버그는 잡기 힘들고, 경험많은 프로그래머들도 메모리 관련 문제를 고치는 데는 며칠에서 몇주가 걸린다. 메모리 버그는 몇달동안 코드 속에 숨어서 갑작스런 프로그램 정지를 일으킬 수 있다. char * 와 C/C++에서의 포인터 사용으로 인한 메모리 버그는 디버깅과 프로그램 정지로 인해 매년 20억 달러에 해당하는 시간의 소모를 일으킨다. 만약 C++에서 char * 과 포인터 를 사용한다면, 이는 매우 힘든 일이 될 것이다. 특히 프로그램의 크기가 10,000 줄 이상일 때.

따라서, 아래의 것들이 C-style에서의 문제점을 극복하기 위해 제안되었다. 앞에 나오는 것이 더 좋은 것이다.

  1. 포인터 대신 레퍼런스를 사용한다.

  2. (이 HOWTO에 주어진) Java 형식의 class를 사용하거나, C++ 표준라이브러리의 string class를 사용한다.

  3. C++에서의 문자 포인터(char *) 사용은 String class를 사용하지 못할 때로 그 사용을 제한한다.

  4. 만약 C++에서의 문자 포인터(char *)를 사용하고 싶지 않을 때는, extern 연계를 이용하는 (char *)를.

"C의 char *"를 사용하기 위해서는, C 프로그램을 다른 파일에 넣고, 연계명시 문 extern "C" 를 이용하여 C++ 프로그램에 링크한다 -

extern "C" {
#include <some_c_header.h>
}

extern "C" {
    comp();
    some_c_function();
}

extern "C" 는 연계 명시이고, 양 중괄호로 둘러싸인 블록안의 모든 내용이 C++이 아닌 C의 연계 방법을 사용한다는 말이다.

'String 클래스'는 메모리 할당과 해제를 위해 생성자와 파괴자를 이용하고, ltrimsubstring 등등과 같은 함수를 제공한다.

또한 관련된 7절 를 사용하는 C++ 컴파일러에서 찾아보아라. string 클래스는 표준 C++ 라이브러리의 일부이고, 여러가지 문자열 관련 함수를 제공한다.

C++ 'string 클래스' 와 'String 클래스' 라이브러리가 많은 문자열 함수를 제공하기 때문에, 직접 문자열 함수를 쓰기 위해 문자 포인터를 사용할 필요성이 거의 없다. 또한, C++ 프로그래머는 항상 'malloc'이나 'free'대신 'new', 'delete'를 사용해야 한다.

두 문자열 클래스는 char * 나 char []가 할 수 있는 모든 일을 할 수 있다. 그리고 보태진 좋은 점은 메모리 문제나 메모리 할당에 대해 전혀 걱정할 필요가 없다는 것이다.


3절. 여러가지 문자열관련 class

문자열 class는 프로그래밍에서 가장 중요한 것들 중 하나이고, 문자열 조정을 위해 매우 많이 쓰인다. 문자열 class는 여러가지가 있고, 물론 이들을 상속받음으로써 자신만의 문자열 class를 만들 수도 있다.

  • 이 문서에 쓰여진 문자열 클래스는 23절를 보아라.

  • 표준 C++ 라이브러리 string class (ANSI/ISO string class http://www.msoe.edu/eecs/cese/resources/stl/string.htm 와http://www.sgi.com/tech/stl/basic_string.html

  • Qt의 외부 라이브러리에 있는 Qt String class http://doc.trolltech.com/qstring.html , mirror :http://www.cs.berkeley.edu/~dmartin/qt/qstring.html

  • 이들 중 맘에 드는 것이 없다면, 자신만의 문자열 클래스를 만들 수도 있다. 위에 언급된 하나 혹은 여러 class를 상속받아 만들 수도 있다.


3.1절. Multiple Inheritance - 자신만의 문자열 class 만들기

위에 말한 것 같이, 하나 혹은 여러 class를 상속받아 자신만의 문자열 class를 만들 수도 있다. 여기서는 표준 C++ 라이브러리의 string class와 부록 A의 String class 를 상속받음으로써 다중상속을 이용한 문자열 class를 만들어 볼 것이다.

우선 예제 파일 'string_multi.h'를 23절 에서 다운로드 받아라.

이 파일은 다음과 같다 :

// ******************************************************************
// String class와 표준 라이브러리의 "string" class를 상속받음으로써
// 직접 문자열 class를 만들어보는 예시를 위한 프로그램
// ******************************************************************

#ifndef __STRING_MULTI_H_ALDEV_
#define __STRING_MULTI_H_ALDEV_

#include <string>
#include "String.h"
#include "StringBuffer.h"

#ifdef NOT_MSWINDOWS
#else
using namespace std;  // MS Visual C++ compiler Version 6.0 에서 필요함.
#endif

// 중요! : C++에서는 생성자, 파괴자, 복사 연산자가 같이 상속되지 않는다.
//	따라서 만약 =, + 등의 연산자가 base class에 정의되어 있고, base
//	class의 생성자를 이용한다면, 반드시 같은 역할을 하는 생성자를
//	상속받는 class에도 만들어주어야 한다.
//	아래에 주어진 mystring(), mystring(char [])를 보아라.
//
//	또한 atmpstr이 mystring으로 선언되었다고 할 때, atmpstr + mstr 
//	과 같이 연산자를 쓸 때, 실제로 불리는 것은 atmpstr.operator+(mstr)이다. 

class mystring:public String, string
{
	public:
		mystring():String() {}  // =, + 연산자를 위해 필요하다
		mystring(char bb[]):String(bb) {}  // =, + 연산자를 위해 필요하다

		mystring(char bb[], int start, int slength):String(bb, start, slength) {}
		mystring(int bb):String(bb) {}  // + 연산자를 위해 필요하다
		mystring(unsigned long bb):String(bb) {}  // + 연산자를 위해 필요하다
		mystring(long bb):String(bb) {}  // + 연산자를 위해 필요하다
		mystring(float bb):String(bb) {}  // + 연산자를 위해 필요하다
		mystring(double bb):String(bb) {}  // + 연산자를 위해 필요하다
		mystring(const String & rhs):String(rhs) {}  // + 연산자를 위해 필요한 Copy Constructor
		mystring(StringBuffer sb):String(sb) {}  // Java와의 호환을 위해
		mystring(int bb, bool dummy):String(bb, dummy) {}  // StringBuffer class를 위해

		int mystraa; // mystring의 최적화
	private:
		int mystrbb; // mystring의 최적화
};

#endif // __STRING_MULTI_H_ALDEV_


4절. String class 다운받기

모든 프로그램과 예제는 이 문서의 부록에 주어진다. String class와 라이브러리, 예제 프로그램을 하나의 tar zip 압축파일로 묶어놓은 링크가 다음과 같다.

  • http://www.milkywaygalaxy.freeservers.com 로 가서 "Source code C++ Programming howto" (Milkyway Galaxy site) 를 눌러라.

  • 미러 사이트는 다음과 같다 - angelfire, geocities, virtualave, 50megs, theglobe, NBCi, Terrashare, Fortunecity,Freewebsites, Tripod, Spree, Escalix, Httpcity, Freeservers.


5절. String class 사용하기

이 String class는 표준 C++ 라이브러리의 string class와 다르다는데 주의하라. 이 특별한 String class는 직접 만들어진 것이고, Java 프로그래머들이 C++을 쉽게 사용하도록 하기 위해 만들어졌다. 만약 당신이 C++과 더 익숙하다면 표준 C++ 라이브러리에 제공되는 진짜 string class 를 사용 하는 것이 좋다.

String class를 사용하기 위해, 23절의 "example_String.cpp" 예제 프로그램과 23절의 String class를 보아라.

'String class' 는 char와 char * 타입을 완벽하게 대신할 수 있다. 'String class'를 char 처럼 사용할 수도 있고, 여러가지 다양한 기능도 사용할 수 있다. 23절 에 주어진 makefile에서 만들어지는 'libString.a'를 링크해야하고, C++ 라이브러리가 위치한 모든 곳의 라이브러리를 /usr/lib 나 /lib 디렉토리에 라이브러리를 복사해넣어야 한다. 'libString.a'를 사용하기 위해서는, 다음과 같이 컴파일하라.

	g++ example.cpp -lString
다음에 주어진 예제 코드를 보라.
	String aa;

	aa = "Creating an Universe is very easy, similar to creating a baby human.";

	// 프로그램에서 aa.val()을 'char *' 같이 사용할 수 있다.
	for (unsigned long tmpii = 0; tmpii < aa.length(); tmpii++)
	{
		//fprintf(stdout, "aa.val()[%ld]=%c ", tmpii, aa.val()[tmpii]);
		fprintf(stdout, "aa[%ld]=%c ", tmpii, aa[tmpii]);
	}

	// 실제로 'char *'로 사용하면..
	for (char *tmpcc = aa.val(); *tmpcc != 0; tmpcc++)  
	{
		fprintf(stdout, "aa.val()=%c ", *tmpcc);
	}


5.2절. 함수

String class에서 제공되는 함수들은 Java 의 String class와 같은 이름을 갖는다. 함수 이름과 동작은 Java의 String class와 완전히똑같다. StringBuffer class역시 제공된다. 이들은 Java와 C++간의 포팅을 쉽게 할 것이다 (잘라내기 & 붙여넣기와 최소한의 코드 조정 만을 필요로 할 것이다). Java의 함수에 들어있는 코드를 C++의 멤버함수로 복사하기만 하면 될 것이고, 최소한의 변경만으로도 C++에서 잘 컴파일 될 것이다. 또다른 이점은 Java와 C++을 모두 사용하는 개발자들이 둘 간의 문법이나 함수 이름을 따로따로 기억할 필요가 없어진다는 것이다.

예를들어 integer를 문자열로 바꾸는 것을 보면,

	String	aa;

	aa = 34;  // '=' 연산자가 int를 string으로 바꾼다.
	cout << "The value of aa is : " << aa.val() << endl;

	aa = 234.878;  //  '=' 연산자가 float를 string으로 바꾼다.
	cout << "The value of aa is : " << aa.val() << endl;

	aa = 34 + 234.878;
	cout << "The value of aa is : " << aa.val() << endl;
	// 출력은 '268.878'일 것이다.

	// casting이 필요하다.
	aa = (String) 34 + " Can create infinite number of universes!! " + 234.878;
	cout << "The value of aa is : " << aa.val() << endl;
	// 출력은 '34 Can create infinite number of universes!! 234.878'일 것이다.

String class의 함수이름에 대한 자세한 내용은 23절 를 참고해라. 같은 String.h파일이 다음 섹션에도 나올 것이다.


6절. String.h 파일

C++과 Java는 많은 소프트웨어 프로젝트에서 같이 쓰인다. C++과 Java를 왔다갔다하는 프로그래머들에게는 이 문자열 class가 매우 유용할 것이다.

C++ (혹은 다른 객체지향 언어)에서는, "class 데이터구조"(혹은 인터페이스) 만 읽으면 그 class를 사용할 수 있다. 인터페이스만 이해하면 되지, 인터페이스의 구현까지는 알 필요가 없는 것이다. String class의 경우, String.h 파일에 있는 String class만 읽고 이해하면 된다. String class를 쓰기 위해 구현(String.cpp)을 모두 읽을 필요는 없는 것이다. 객체지향 class들은 시간을 절약하게 해주고, 구현의 내용을 교묘하게 숨겨준다.

( 객체지향인 Java에도 이와 같은 역할을 하여 구현 내용을 숨겨주는 'interface' 란 것이 있다. )

아래의 내용은 String.h 파일이고, 23절을 참고해라.

//
// Author : Al Dev  Email: alavoor[AT]yahoo.com
// string class나 String class를 써라.
//
// 메모리 릭을 막기 위해 - 문자 변수를 관리하기 위한 문자 class
// char[]나 char *보다는 String class나 string class를 써라.
//

#ifndef __STRING_H_ALDEV_
#define __STRING_H_ALDEV_

// 프로그램이 커질 수록 iostream을 사용하지 말아라.
#ifdef NOT_MSWINDOWS
#include <iostream>
#else
#include <iostream.h> // 하위호환성을 위해. C++ 표준은 .h가 없다.
#endif // NOT_MSWINDOWS

#include <stdio.h>   // File과 sprintf()를 위해
//#include <list.h> // list

// MS Windows 95 VC++과 Borland C++ 컴파일러인 경우 - 
// d:\program files\CBuilder\include\examples\stdlib\list.cpp 와 include\list.h
// 을 보라.
//#include <list> // for list
//using namespace std;

const short INITIAL_SIZE = 	50;
const short NUMBER_LENGTH = 300;
const int MAX_ISTREAM_SIZE = 2048;

//class StringBuffer;

// 나는 이 문자열 class를 Linux (Redhat 7.1)와 MS Windows Borland C++ v5.2 (win32) 
// 에서 컴파일 / 테스트 해보았다.
// 또한, MS Visual C++ compiler에서도 작동할 것이다.
class String
{
	public:
		String();
		String(const char bb[]);  // + 연산자를 위해 필요
		String(const char bb[], int start, int slength); // 문자들의 부분집합
		String(int bb);  // + 연산자를 위해 필요
		String(unsigned long bb);  // + 연산자를 위해 필요
		String(long bb);  // + 연산자를 위해 필요
		String(float bb);  // + 연산자를 위해 필요
		String(double bb);  // + 연산자를 위해 필요
		String(const String & rhs);  // + 연산자를 위해 필요한 copy constructor
		//String(StringBuffer sb);  // Java와의 호환성을 위해 
					    // - 그러나 MS windows에서는 
					    // 컴파일되지 않고, core dump를 일으킨다.
		String(int bb, bool dummy);  // StringBuffer class를 위해 필요
		virtual ~String();  // virtual로 선언하여 상속받은 class의 소멸자가
				    // 불리도록 한다.

		char *val() {return sval;} // sval을 public으로 하는 것은 위험하므로

		// Java의 String을 흉내낸 함수들
		unsigned long length();
		char charAt(int where);
		void getChars(int sourceStart, int sourceEnd, 
				char target[], int targetStart);
		char* toCharArray();
		char* getBytes();

		bool equals(String str2); // == 연산자를 참조하라
		bool equals(char *str2); // == 연산자를 참조하라
		bool equalsIgnoreCase(String str2);

		bool regionMatches(int startIndex, String str2, 
				int str2StartIndex, int numChars);
		bool regionMatches(bool ignoreCase, int startIndex, 
				String str2, int str2StartIndex, int numChars);

		String toUpperCase();
		String toLowerCase();

		bool startsWith(String str2);
		bool startsWith(char *str2);

		bool endsWith(String str2);
		bool endsWith(char *str2);

		int compareTo(String str2);
		int compareTo(char *str2);
		int compareToIgnoreCase(String str2);
		int compareToIgnoreCase(char *str2);

		int indexOf(char ch, int startIndex = 0);
		int indexOf(char *str2, int startIndex = 0);
		int indexOf(String str2, int startIndex = 0);

		int lastIndexOf(char ch, int startIndex = 0);
		int lastIndexOf(char *str2, int startIndex = 0);
		int lastIndexOf(String str2, int startIndex = 0);

		String substring(int startIndex, int endIndex = 0);
		String replace(char original, char replacement);
		String replace(char *original, char *replacement);

		String trim(); // 오버로딩 된 trim을 참조하라.

		String concat(String str2);  // + 연산자를 참조
		String concat(char *str2); // + 연산자를 참조
		String concat(int bb);
		String concat(unsigned long bb);
		String concat(float bb);
		String concat(double bb);

		String reverse(); // 오버로딩 된 다른 reverse()를 참조
		String deleteCharAt(int loc);
		String deleteStr(int startIndex, int endIndex); // Java의 "delete()"

		String valueOf(char ch)
			{char aa[2]; aa[0]=ch; aa[1]=0; return String(aa);}
		String valueOf(char chars[]){ return String(chars);}
		String valueOf(char chars[], int startIndex, int numChars);
		String valueOf(bool tf)
			{if (tf) return String("true"); else return String("false");}
		String valueOf(int num){ return String(num);}
		String valueOf(long num){ return String(num);}
		String valueOf(float num) {return String(num);}
		String valueOf(double num) {return String(num);}

		// 이 파일의 아래에 주어진 StringBuffer를 참고하라.

		// ---- 여기까지 Java를 흉내낸 함수들  -----

		//////////////////////////////////////////////////////
		// 		Java에는 없는 추가적인 함수들
		//////////////////////////////////////////////////////
		String ltrim();
		void ltrim(bool dummy); // 직접적으로 object를 변화시킨다.
		String rtrim();
		void rtrim(bool dummy); //  직접적으로 object를 변화시킨다.
					// chopall 참고.

		void chopall(char ch='\n'); // 맨 뒤의 ch를 없앤다. rtrim 참고.
		void chop(); // 맨 뒤의 문자를 없앤다.

		void roundf(float input_val, short precision);
		void decompose_float(long *integral, long *fraction); 

		void roundd(double input_val, short precision);
		void decompose_double(long *integral, long *fraction); 

		void explode(char *separator); // token()과 오버로딩 된 explode()참조
		String *explode(int & strcount, char separator = ' '); //  token()참조
		void implode(char *glue);
		void join(char *glue);
		String repeat(char *input, unsigned int multiplier);
		String tr(char *from, char *to); // character들을 바꾼다(translate).
		String center(int padlength, char padchar = ' ');
		String space(int number = 0, char padchar = ' ');
		String xrange(char start, char end);
		String compress(char *list = " ");
		String left(int slength = 0, char padchar = ' ');
		String right(int slength = 0, char padchar = ' ');
		String overlay(char *newstr, int start = 0, int slength = 0, char padchar = ' ');

		String at(char *regx); // regx의 첫번째 match
		String before(char *regx); // regx 앞의 string
		String after(char *regx); // regx 뒤의 string
		String mid(int startIndex = 0, int length = 0);

		bool isNull();  
		bool isInteger();
		bool isInteger(int pos);
		bool isNumeric();
		bool isNumeric(int pos);
		bool isEmpty();  // length() == 0 과 같은 상태
		bool isUpperCase();
		bool isUpperCase(int pos);
		bool isLowerCase();
		bool isLowerCase(int pos);
		bool isWhiteSpace();
		bool isWhiteSpace(int pos);
		bool isBlackSpace();
		bool isBlackSpace(int pos);
		bool isAlpha();
		bool isAlpha(int pos);
		bool isAlphaNumeric();
		bool isAlphaNumeric(int pos);
		bool isPunct();
		bool isPunct(int pos);
		bool isPrintable();
		bool isPrintable(int pos);
		bool isHexDigit();
		bool isHexDigit(int pos);
		bool isCntrl();
		bool isCntrl(int pos);
		bool isGraph();
		bool isGraph(int pos);

		void clear();
		int toInteger();
		long parseLong();

		double toDouble();
		String token(char separator = ' '); // StringTokenizer와 explode()를 참조
		String crypt(char *original, char *salt);
		String getline(FILE *infp = stdin); // putline() 참조
		//String getline(fstream *infp = stdin); // putline() 참조

		void putline(FILE *outfp = stdout); // getline() 참조
		//void putline(fstream *outfp = stdout); // getline() 참조

		void swap(String aa, String bb); // aa를 bb로 바꾼다
		String *sort(String aa[]);  // String의 array를 sort한다
		String sort(int startIndex = 0, int length = 0);  // string 내의 character들을 sort
		int freq(char ch); // ch가 들어있는 횟수를 센다
		void Format(const char *fmt, ...);
		String replace (int startIndex, int endIndex, String str);

		void substring(int startIndex, int endIndex, bool dummy); // object를 직접 바꾼다
		void reverse(bool dummy); // object를 직접 바꾼다
		String deleteCharAt(int loc, bool dummy); // object를 직접 바꾼다
		String deleteStr(int startIndex, int endIndex, bool dummy);
		void trim(bool dummy); // object를 직접 바꾼다
		String insert(int index, String str2);
		String insert(int index, String str2, bool dummy); // object를 직접 바꾼다
		String insert(int index, char ch);
		String insert(int index, char ch, bool dummy); // object를 직접 바꾼다
		String insert(char *newstr, int start = 0, int length = 0, char padchar = ' ');

		String dump(); // od -c 와 같이 string을 dump한다.

		// Java의 StringBuffer를 위해 필요한 것들
		void ensureCapacity(int capacity);
		void setLength(int len);
		void setCharAt(int where, char ch); // charAt(), getCharAt() 참고

		// Java의 Integer class, Long, Double class를 위해 필요
		int parseInt(String ss) {return ss.toInteger();}
		int parseInt(char *ss)
			{String tmpstr(ss); return tmpstr.toInteger();}
		long parseLong(String ss) {return ss.parseLong();}
		long parseLong(char *ss)
			{String tmpstr(ss); return tmpstr.parseLong();}
		float floatValue() {return (float) toDouble(); }
		double doubleValue() {return toDouble(); }
		char * number2string(int bb);  // String(int) 참고
		char * number2string(long bb);  // String(long) 참고
		char * number2string(unsigned long bb);  // String(long) 참고
		char * number2string(double bb);  // String(double) 참고

		///////////////////////////////////////////////
		// 		겹치는 함수 이름들
		///////////////////////////////////////////////
		// char * c_str() // val() 을 대신사용
		// bool find();  // regionMatches() 를 대신사용
		// bool search();  // regionMatches() 를 대신사용
		// bool matches(); // regionMatches() 를 대신사용
		// int rindex(String str2, int startIndex = 0); lastIndexOf() 을 대신사용
		// String blanks(int slength);  // repeat() 를 대신사용
		// String append(String str2); // concat() 이나 + operator 을 대신사용
		// String prepend(String str2);  // + operator을 대신사용 append()참고
		// String split(char separator = ' ');  // token(), explode() 나 StringTokenizer class 를 대신사용
		bool contains(char *str2, int startIndex = 0); // indexOf() 를 대신사용
		// void empty(); is_empty() 를 대신사용
		// void vacuum(); clear() 를 대신사용
		// void erase(); clear() 를 대신사용
		// void zero(); clear() 를 대신사용
		// bool is_float(); is_numeric(); 을 대신사용
		// bool is_decimal(); is_numeric(); 을 대신사용
		// bool is_Digit(); is_numeric(); 을 대신사용
		// float float_value(); toDouble(); 을 대신사용
		// float tofloat(); toDouble(); 을 대신사용
		// double double_value(); toDouble(); 을 대신사용
		// double numeric_value(); toDouble(); 을 대신사용
		// int int_value(); toInteger() 를 대신사용
		// int tonumber(); toInteger() 를 대신사용
		// String get(); substring() 이나 val() 을 대신 사용. 그러나 Java 스타일의 substring이 더 좋다
		// String getFrom(); substring() 이나 val() 을 대신 사용. 그러나 Java 스타일의 substring이 더 좋다
		// String head(int len); substring(0, len) 을 대신사용
		// String tail(int len); substring(length()-len, length()) 를 대신사용
		// String cut(); deleteCharAt() 이나 deleteStr() 을 대신사용
		// String cutFrom(); deleteCharAt() 이나 deleteStr() 을 대신사용
		// String paste(); insert() 를 대신사용
		// String fill(); replace() 를 대신사용
		// char firstChar(); // substring(0, 1); 을 대신사용
		// char lastChar(); // substring(length()-1, length()); 를 대신사용
		// String findNext(); token(), explode() 이나 StringTokenizer class 를 대신사용

		// begin();  iterator. operator [ii]를 대신사용
		// end();  iterator. operator [ii]를 대신사용
		// copy();  assignment =  연산다를 대신 사용, String aa = bb;
		// clone();  assignment =  연산자를 대신 사용, String aa = bb;
		// void putCharAt(int where, char ch); setCharAt() 을 대신사용
		// void replaceCharAt(int where, char ch); setCharAt() 을 대신사용
		// char getCharAt(int where); CharAt() 을 대신사용
		// void parseArgs(int where, char ch); StringTokensizer class, token() 이나 explode() 를 대신사용
		// void truncate(); trim(), rtrim(), chop() 이나 chopall() 을 대신사용
		// 숫자를 string으로 변환 : notostring(), int2str, long2str은 number2string()을 사용

		// 연산자들...
		String operator+ (const String & rhs);
		friend String operator+ (const String & lhs, const String & rhs);

		String& operator+= (const String & rhs); // 레퍼런스를 이용하면 더 빠를 것이다.
		String& operator= (const String & rhs); // 레퍼런스를 이용하면 더 빠를 것이다.
		bool operator== (const String & rhs); // 레퍼런스를 이용하면 더 빠를 것이다.
		bool operator== (const char *rhs);
		bool operator!= (const String & rhs);
		bool operator!= (const char *rhs); 
		char operator [] (unsigned long Index) const;
		char& operator [] (unsigned long Index);
		friend ostream &  operator<< (ostream & Out, const String & str2);
		friend istream &  operator>> (istream & In, String & str2);

		bool String::operator< (const char *rhs) const; // map & vector 를 위한 유용한 method
		bool String::operator< (const String & rhs) const; // map & vector 를 위한 유용한 method

		//do later: static	list<String> 		explodeH;  // list head

	protected:
		char *sval; // sval을 public으로 하는 것은 위험하다.
		void verifyIndex(unsigned long index) const; // Win32에서의  warning때문에 inline이 아니다.
		void verifyIndex(unsigned long index, char *aa) const;// Win32에서의  warning때문에 inline이 아니다.

		void _str_cat(char bb[]);
		void _str_cat(int bb);
		void _str_cat(unsigned long bb);
		void _str_cat(float bb);

		void _str_cpy(char bb[]);
		void _str_cpy(int bb); // itoa
		void _str_cpy(unsigned long bb);
		void _str_cpy(float bb); // itof

	private:
		// Note: 모든 private 변수와 함수는 _ (밑줄)로 시작한다.
		
		//static String *_global_String; // add 연산에서 필요
		//inline void _free_glob(String **aa);

		bool _equalto(const String & rhs, bool type = false);
		bool _equalto(const char *rhs, bool type = false);
		String *_pString;  // 내부에서 사용하는 임시 포인터
		char *_pNumber2String;  // 내부에서 사용하는 임시 포인터
		inline void _allocpString();
		inline void _allocpNumber2String();
		inline void Common2AllCstrs();
		inline void _reverse();
		inline void _deleteCharAt(int loc);
		inline void _deleteStr(int startIndex, int endIndex);
		inline void _trim();
		inline void _ltrim();
		inline void _rtrim();
		inline void _substring(int startIndex, int endIndex);
		void _roundno(double input_dbl, float input_flt, short precision, bool type);
};

// 전역변수는 String.cpp 에서 정의된다

#endif // __STRING_H_ALDEV_


6.1절. StringBuffer.h

C++ 과 Java는 많은 소프트웨어 프로젝트에서 동시에 쓰인다. C++과 Java를 왔다갔다하는 프로그래머에게 이 stringbuffer class는 매우 유용할 것이다.

//
// Author : Al Dev  Email: alavoor[AT]yahoo.com
//

#ifndef __STRINGBUFFER_H_ALDEV_
#define __STRINGBUFFER_H_ALDEV_

// Java의 StringBuffer 를 모방한 것
// 이 class는 Java의 code를 최소한의 수정만으로도
// C++에서 동작하도록 하기 위해 만들어졌다.
// Note: C++로 코딩하는 동안은 이 StringBuffer
// class를 *쓰지 말아라*.
// 이 class는 오직 Java 코드를 cut/paste하는 경우를
// 위해서 쓰여진 것이다.
class StringBuffer: public String
{
	public:
		StringBuffer();
		~StringBuffer();
		StringBuffer(char *aa);
		StringBuffer(int size);
		StringBuffer(String str);

		int capacity();
		StringBuffer append(String str2);
			// operator + 참조
			//{ *this += str2; return *this;} // 이 code는 core dump를 일으킨다

		StringBuffer append(char *str2);
		StringBuffer append(int bb);
		StringBuffer append(unsigned long bb) ;
		StringBuffer append(float bb) ;
		StringBuffer append(double bb) ;

		StringBuffer insert(int index, String str2);
		StringBuffer insert(int index, char ch);

		StringBuffer reverse();

		// Java의 "delete()"에 해당. (delete는 C++의 keyword이므로 사용하지 못한다)
		StringBuffer deleteStr(int startIndex, int endIndex);
		StringBuffer deleteCharAt(int loc);

		StringBuffer substring(int startIndex, int endIndex = 0);
		void assign(char *str);

	private:
		StringBuffer *_pStringBuffer;
		inline void allocpStringBuffer();
		inline void Common2AllCstrs();
};

#endif // __STRINGBUFFER_H_ALDEV_


7절. 표준 C++ 라이브러리 string class

위에 언급된 String class (S가 대문자인 것에 주의!)는 Java를 사용하는 사람들을 위한 것인 반면, 표준 C++ 라이브러리에서 제공되는 "진짜" string class를 주목할 필요가 있다.

string class는 C에서의 가장 큰 문제점 중 하나인 문자배열의 단점을 극복하기 위해 만들어졌다. 문자배열이 무척 빠르긴 하지만, 많은 단점을 갖고 있다. 문자배열은 많은 버그의 원인이고, 이를 parsing하는 일은 굉장히 귀찮은 일이다.

string class는 문자열을 파싱하고, 조정하는데 필요한 좋은 인터페이스를 제공하고, STL과도 호환가능하다. 즉, 모든 STL의 알고리즘을 사용할 수 있다. 실제로 문자열은 vector<char> ( 문자들을 위한 container 혹은 진보된 문자배열 ) 로 취급될 수 있다.

다음의 사이트에서 참고할만한 것들을 얻을 수 있다:

  • SGI STL 기초_string reference: http://www.sgi.com/tech/stl/basic_string.html.


7.2절. 문자열을 찾기

문자열을 찾는 것은 문자배열보다 훨씬 쉽다. string class는 문자열을 찾는데 효율적인 멤버 함수들을 제공한다. 모든 멤버함수는 string::size_type 을 return한다.

가장 흔한 상황은 문자열을 찾는 것이고, 이는 find() 함수를 사용하면 된다.

string str("Hello, can you find Ben?");
string::size_type position = str.find("Ben");
cout << "First occurence of Ben was found at: " << position << endl;

이 코드는 'Ben'에 대해 대소문자를 구별하는 검색을 하고, 시작위치를 'position'에 string::size_type 타입으로 넣는다. 리턴하는 값이 int가 아니라 특별히 고안된 string::size_type 타입이라는데 주의하라.

find_first_of() 함수는 실제적인 예가 필요할 것이다. 아래와 같은 상황을 보자.

string s = "C++ is an impressive language.";
string::size_type pos = s.find_first_of(" .");

while (pos != string::npos) {
    cout << "Found space or dot at: " << pos << endl;
    pos = s.find_first_of(" .", pos + 1);
}

find_first_of()함수를 쓰면, 우리는 첫번째 인자의 모든 문자를 찾게 되고, 따라서 여기서는 스페이스(' ') 혹은 점('.')을 찾게 된다.

프로그램을 컴파일해서 어떻게 출력되는지 보아라.


7.3절. string tokenizer

문자열을 가지고 자주 하게되는 작업 중 하나는, 어떤 구분자를 가지고 토큰들로 나누는 것이다 (tokenize). tokenizer는 문자열을 find()를 계속 부르는 일 없이 쉽게 조그만 조각들로 쪼갤 수 있도록 해준다. C에서는, 아마도 문자 배열에 대해 strtok() 란 함수를 썼을 것이지만, 문자열에 대해서는 이러한 함수가 없다. 따라서 직접 이런 함수를 만들어야 겠지만, 몇가지 해결책이 있다.

The advanced tokenizer:

void Tokenize(const string& str,
                      vector<string>& tokens,
                      const string& delimiters = " ")
{
    // 맨 첫 글자가 구분자인 경우 무시
    string::size_type lastPos = str.find_first_not_of(delimiters, 0);
    // 구분자가 아닌 첫 글자를 찾는다
    string::size_type pos     = str.find_first_of(delimiters, lastPos);

    while (string::npos != pos || string::npos != lastPos)
    {
        // token을 찾았으니 vector에 추가한다
        tokens.push_back(str.substr(lastPos, pos - lastPos));
        // 구분자를 뛰어넘는다.  "not_of"에 주의하라
        lastPos = str.find_first_not_of(delimiters, pos);
        // 다음 구분자가 아닌 글자를 찾는다
        pos = str.find_first_of(delimiters, lastPos);
    }
}

tokenizer는 다음과 같이 쓰일 수 있다.

#include <string>
#include <algorithm>
#include <vector>

using namespace std;

int main()
{
    vector<string> tokens;

    string str("Split me up! Word1 Word2 Word3.");

    Tokenize(str, tokens);

    copy(tokens.begin(), tokens.end(), ostream_iterator<string>(cout, ", "));
}

위의 코드는 Tokenize 함수를 사용하는 예로서, 첫번째 인자인 str를 쪼갠다. 그리고 우리가 세 번째 인자를 주지 않았기 때문에 디폴트로 설정된 ""(spacebar)를 구분자로 사용한다. 그리고 모든 element는 tokens 벡터에 들어가게 될 것이다.

마지막으로 표준 출력에 벡터 전체를 copy()함으로써 벡터의 내용을 화면으로 볼 수 있을 것이다.

또다른 접근 방법은 stringstream을 사용하는 것이다. C++에서 stream은 특수한 기능이 하나 있는데, 이는 공백(whitespace)를 만날 때까지 읽기를 계속한다는 것이다. 따라서 아래의 코드는 공백을 기준으로 문자열을 나누고자 할 때 잘 동작할 것이다.

#include <vector>
#include <string>
#include <sstream>

using namespace std;

int main()
{
    string str("Split me by whitespaces");
    string buf; // 버퍼 string
    stringstream ss(str); // string을 stream에 넣는다

    vector<string> tokens; // word들을 넣을 vector

    while (ss >> buf)
        tokens.push_back(buf);
}

이제 stringstream은 출력 연산자(>>)를 사용 하여 문자열을 buf 에 공백을 만날 때마다 넣는다. buf는 이를 차례대로 벡터에 push_back() 한다. 그리고 이제 tokens 벡터는 str 에 들어있는 모든 단어를 갖게 된다.


9절. C++ 에서의 메모리 할당

C에서는, 메모리의 할당과 해제를 위해 malloc()과 free()를 비롯한 malloc()계열의 함수를 쓰지만, 다들 단점을 갖고 있다. 그래서 C++ 은 메모리를 다루기 위한 연산자들을 도입했고, 이들은 new 와 delete이다. 이 연산자들은 실행시에 힙(heap - 혹은 자유 공간)으로부터 메모리를 할당, 해제한다.

C++에서는 정말로 꼭 malloc()이나 free()만을 써야하는 상황이 아니라면 언제나 new 와 delete를 써야한다. 그러나 주의할 점은, 이 두 가지를 섞어서 쓰면 안된다는 것이다. malloc()으로 얻은 메모리를 delete로 해제할 수는 없고, 반대로 new로 얻은 메모리를 free()시킬 수도 없다.


9.1절. C++ Zap (Delete) 함수

C++에서의 delete 와 new 연산자는 C의 malloc, free보다 낫다. 따라서 malloc과 free 대신 new와 zap(delete)를 쓰도록 하는 것이 좋다.

delete 연산자가 좀 더 깔끔하게 사용되게 하기위해 다음과 같은 Zap() inline 함수를 만들자. 다음과 같이 zap()을 정의하자.

// x가 NULL인지 체크하기 위해 assert를 사용하였다.
// 이는 프로그램의 "논리적" 에러를 미리 잡아내기 위한 것이다.
// delete가 NULL인 경우에도 잘 동작하긴 하지만, assert를
// 사용함으로써 좀 더 일찍 에러를 잡아낼 수 있다.

// Zap을 template을 사용하여 정의하자.
// delete대신 zap을 사용하면 더 깔끔할 것이다.
template <class T>
inline void zap(T & x)
{
	{assert(x != NULL);}
	delete x;
	x = NULL;
}

// C++에 두 가지 delete 연산자의 용법이 있는 이유는 C++ 에게
// 한 객체에 대한 포인터와 객체의 배열에 대한 포인터를 구별하도록
// 말해주는 방법이 필요하기 때문이다.
// delete연산자는 프로그래머에게 "[]"를 쓰게함으로써 이를 구별한다.
// 따라서 우리는 포인터의 배열을 지우기 위한 zaparr 함수를 다음과 같이 정의할 수 있다
template <class T>
inline void zaparr(T & x)
{
	 {assert(x != NULL);}
     delete [] x;
     x = NULL;
}

zap()함수는 포인터를 delete시키고 NULL로 세팅한다. 이는 똑같은 delete 포인터에 대해 여러번의 zap()이 불려서 프로그램이 망가지는 것을 방지한다. 다음의 zap_example()함수를 보아라. example_String.cpp 'Source code of C++'을 클릭해라.

	//  example_String.cpp에서 zap_example()를 보라.
	zap(pFirstname);
	//zap(pFirstname); // pFirstname이 NULL이므로 코어 덤프가 일어나지 않는다.
	//zap(pFirstname); // pFirstname이 NULL이므로 코어 덤프가 일어나지 않는다.

	zap(pLastname);
	zap(pJobDescription);

	int *iiarray = new int[10];
	zaparr(iiarray);

뭐 특별한 것이 있는 것은 아니고, 이것은 단지 반복적인 코드를 줄이고 타이핑하는 시간을 아껴주며 프로그램을 좀 더 읽기 좋게 만들어주는 것 뿐이다. C++ 프로그래머들은 자주 delete한 pointer를 NULL로 세팅하는 것을 잊는다. 그리고 이는 코어덤프와 오작동으로 이어질 수 있다. zap()은 이러한 문제를 자동으로 처리해준다. zap()에 타입 캐스팅을 할 필요는 없다. 만약 위 zap()함수에서 에러가 난다면, 다른 데서 시작된 에러일 것이다.

또한 9.2절 , my_realloc() 과 my_free() 이 malloc(), realloc() 그리고 free() 대신 쓰여야 한다. 이들은 훨씬 깔끔하고, 여러가지 체크도 해준다. 예를들어, 9.2절 과 my_free() 함수를 사용하는 "String.h" 파일을 보라.

주의 : 'new'로 할당된 메모리를 해제하기 위해 free()를 쓰거나, malloc()으로 할당된 메모리를 해제하기 위해 'delete'를 쓰지 말아라. 그렇지 않으면 결과를 예측할 수 없는 에러에 빠질 것이다.

example_String.cpp 에서 'Source code of C++' 를 클릭한다음, zap함수의 예를 보아라.


9.2절. my_malloc 과 my_free 의 사용

malloc과 realloc 을 최대한 사용하지 말고, new 와 9.1절(delete)을 사용해라. 그러나 때로는 C++에서 C 스타일의 메모리 할당을 사용해야 할 필요도 있다. 이 때는 my_malloc() , my_realloc() , my_free() 을 사용해라. 이 함수들은 적절한 할당과 초기화를 해주고, 메모리 문제를 예방해준다. 또한 이 함수들은 DEBUG모드에서 메모리 할당을 추적해주고, 프로그램 실행 전후에 총 메모리 사용량을 표시해준다. 이는 메모리 릭이 있는지를 알려줄 것이다.

my_malloc 과 my_realloc은 다음과 같이 정의되었다. 이는 약간의 메모리를 더 할당해서 (SAFE_MEM = 5) 초기화시키고, 메모리를 할당할 수 없으면 프로그램을 종료한다. 'call_check(), remove_ptr()' 함수는 DEBUG_MEM 가 makefile에서 ((void)0) (이는 NULL을 의미한다)으로 지정되어있을 때에만 작동한다. 이는 총 메모리 사용량을 추적할 수 있게 해준다.

void *local_my_malloc(size_t size, char fname[], int lineno) 
{
	size_t  tmpii = size + SAFE_MEM;
	void *aa = NULL;
	aa = (void *) malloc(tmpii);
	if (aa == NULL)
		raise_error_exit(MALLOC, VOID_TYPE, fname, lineno);
	memset(aa, 0, tmpii);
	call_check(aa, tmpii, fname, lineno);
	return aa;
}

char *local_my_realloc(char *aa, size_t size, char fname[], int lineno)
{
	remove_ptr(aa, fname, lineno);
	unsigned long tmpjj = 0;
	if (aa) // aa !=  NULL
		tmpjj = strlen(aa);
	unsigned long tmpqq = size + SAFE_MEM;
	size_t  tmpii = sizeof (char) * (tmpqq);
	aa = (char *) realloc(aa, tmpii);
	if (aa == NULL)
		raise_error_exit(REALLOC, CHAR_TYPE, fname, lineno);

	// do not memset memset(aa, 0, tmpii);
	aa[tmpqq-1] = 0;
	unsigned long kk = tmpjj;
	if (tmpjj > tmpqq)
		kk = tmpqq;
	for ( ; kk < tmpqq; kk++)
		aa[kk] = 0;
	call_check(aa, tmpii, fname, lineno);
	return aa;
}
my_malloc 의 모든 구현을 보려면 23절 에서 23절 의 헤더파일을 보면 된다.

my_malloc 과 my_free 를 쓰는 예는 다음과 같다.

	char 	*aa;
	int 	*bb;
	float	*cc;
	aa = (char *) my_malloc(sizeof(char)* 214);
	bb = (int *) my_malloc(sizeof(int) * 10);
	cc = (float *) my_malloc(sizeof(int) * 20);

	aa = my_realloc(aa, sizeof(char) * 34);
	bb = my_realloc(bb, sizeof(int) * 14);
	cc = my_realloc(cc, sizeof(float) * 10);
my_realloc 에서 data type을 cast 할 필요가 없는 것에 주의해라. 이는 인자로 받은 변수의 타입에 맞춰서 리턴값을 보내기 때문이다. The my_realloc 함수는 char *, int *, float * 타입으로 오버로딩 되어있다.


10절. 포인터가 문제이다

포인터는 일반적인 프로그램에서 꼭 필요한 것은 아니다. Java와 같은 현대 언어에서는 포인터가 없다 (Java는 내부적으로만 포인터를 사용한다). 포인터는 프로그램을 어지럽고 읽기 힘들게 만든다.

최대한 포인터의 사용을 피하고, 대신 레퍼런스를 사용해라. 포인터는 정말 문제가 많고, 포인터 없이 프로그램을 쓰는 게 가능하다. 포인터는 레퍼런스를 쓸 수 없는 곳에서만 써야한다.

레퍼런스 는 별칭(alias)이다. 레퍼런스를 만들면, 이는 다른 객체(혹은 대상)에 다른 이름을 주는 것이다. 그 순간부터 레퍼런스는 대상의 다른 이름으로서 돌아가고, 레퍼런스에 행하는 모든 연산이 그 대상에 실제로 적용된다.

레퍼런스의 문법 : 타입을 선언할 때, 뒤에 레퍼런스 연산자 (&) 를 붙임으로써 레퍼런스를 선언할 수 있다. 레퍼런스는 반드시 만들어 질 때 초기화 되어야 한다. 다음의 예를 보자 -

	int	weight;
	int	& rweight = weight;

	DOG	aa;
	DOG	& rDogRef = aa;

레퍼런스를 사용할 때 지킬 것 -

  • 객체에 대한 다른 이름을 주고자 할 때 레퍼런스를 사용해라.

  • 모든 레퍼런스는 초기화되어야 한다.

  • 프로그램의 높은 효율과 퍼포먼스를 위해 레퍼런스를 사용해라

  • 레퍼런스와 포인터를 보호하기 위해 가능한경우면 언제나 const를 사용해라.

레퍼런스를 사용할 때 하지 말아야 할 것 -

  • 중요 : NULL인 객체에 대해 레퍼런스를 쓰지 말아라.

  • 포인터의 주소를 나타내는 &와 레퍼런스 연산자를 헷갈리지 마라. 레퍼런스 연산자는 오직 선언부 (위에 나와있는 레퍼런스 사용법 참조) 에서만 쓰인다.

  • 레퍼런스에 새로 값을 지정하려(즉, 변경하려) 하지 마라.

  • 레퍼런스를 쓸 수 있다면 포인터를 쓰지 마라.

  • 지역변수에 대한 레퍼런스를 리턴하지 마라.

  • 레퍼런스가 스코프가 벗어난 변수를 가리키도록 하지 마라.


11절. 디버깅

정확한 버그의 원인을 알아내는 것은 꽤나 성가신 일이지만, 여기에도 몇가지 테크닉이 있다.

  • 표준출력으로 프린트하여 - 프로그램이 간단한 경우, 몇몇 변수들의 값을 프린트해보고, 어떤 값인지 본다 - 무엇이 잘못되었는지 찾기

  • 디버거를 이용하기. 디버거는 breakpoint를 설정하고, 실행도중에 코드를 추적해볼 수 있도록 해준다. 대부분의 IDE는 디버거가 같이 있다. GNU 시스템의 경우, gdb가 있다.

  • 컴파일러에서 지원하는 옵션들을 사용하여 보다 많은 경고(warning)을 볼 수 있도록 해라. 예를들어 g++의 경우, -Wall 옵션을 사용해라.

디버깅에 도움이 되는 사이트 :

  • Unix 환경에서 C, C++ 디버깅하기 http://www.liacs.nl/~jdassen/onderwijs/stuva/debug/debug.html

  • MPatrol - 유용한 메모리 디버깅 툴 : http://www.cbmamiga.demon.co.uk/mpatrol

  • NJAMD - 역시 유용한 메모리 디버깅 툴 : http://sourceforge.net/projects/njamd/

  • LeakTracer - 메모리 릭을 찾는 간단하면서도 유용한 툴 : http://www.andreasen.org/LeakTracer/


12절. C++을 위한 IDE(Integrated Development Enviroment)와 에디터

C++로 프로그래밍을 할 때, 에디터나 IDE를 사용하는 것이 좋다. 대부분의 프로그래머는 자신이 좋아하는 것들을 갖게 마련이고, 어떤 것이 좋은지에 대해 거의 종교적인 믿음을 갖는다.

너는 내장 에디터와 컴파일러, 문서들과 기타 등등으로 모두 포함하는 IDE (Intergrated Development Environment : 통합 개발환경)를 사용할 수도 있다. 또는 몇몇 사람들이 그러는 것 처럼, 단순한 에디터만을 사용할 수도 있다.


13절. C++ Online 텍스트와 문서들

C++에 대한 수백만의 온라인 문서/텍스트/참고자료 가이드 등이 존재한다. 이는 C++이 매우 오랫동안 쓰이고 있기 때문이다. 아마 Google, Yahoo, Lycos, Excite 등의 인터넷 검색엔진을 사용하면 도움이 될 것이다.

  • "C++ Annotations" 온라인 북 사이트:Annotations

  • "Teach Yourself C++ in 21 days" 온라인 사이트 Teach C++

  • C++ Textbook by Bruce Eckel Thinking in C++

  • C++ Open books: Panorama Open Books를 클릭하면 된다.

  • "Who's Afraid of C++?" online textbook: Steveheller

  • "Introduction to Object Oriented Programming" an ebook C++ OOP

  • C++ in Hypertext C++ Hypertext

  • Object Oriented Systems OOP article

  • C++ Language Reference from cplusplus.com http://www.cplusplus.com/ref

  • C++ Documentation from cplusplus.com http://www.cplusplus.com

  • Common C++ Pitfalls to be avoided http://www.horstmann.com/cpp/pitfalls.html

  • Porting C++ to Java PortingC

  • C/C++ Journals UtahJournals

  • Yahoo C++ category site CCyahoo

  • C Library Reference Guidec_guide

  • Online textbooks C++/Java FreeLib

  • "C++ In Action" by Bartosz Milewski at http://www.relisoft.com/book/index.htm

  • 어떻게 코드를 쓰면 안되는가에 대한 재미있는 예제. "How to write unmaintainable code" http://mindprod.com/unmain.html

C++ 프로그래머를 위해 유용할 Java 책들 :

  • Great Web reference site WebRef

  • Many Java books JBooks

  • Intro to Java V3.0 JavaNotes mirror JavaNotes

  • Web Library: http://www.itlibrary.com

  • Thinking in Java: Thinking Java

  • John Hopkins Univ - Java resources Hall

  • online Java tutorial Chortle

  • Practical guide for Java SunBooks

  • Java Soton


14절. C++ 코딩 관습

코딩 관습은 프로그램의 가독성과 유지보수를 위해 매우 중요한 요소이다. 또한 프로그래머의 생산성을 크게 향상시킨다. 이는 좋은 코딩 훈련을 위해 필요하다. 아래의 내용은 class 정의에 있어 제안된 것이다.

  • 모든 public 변수들은 mFooVar과 같이 m 으로 시작해야 한다. m 은 member를 의미한다.

  • 모든 protected 변수들은 mtFooVar 와 같이 mt 로 시작해야하고, 메쏘드는 tFooNum() 와 같이 t로 시작해야 한다. t 는 protected를 의미한다.

  • 모든 private 변수들은 mvFooVar와 같이 mv 로 시작해야하고, 메쏘드들은 vFooLone() 와 같이 v로 시작해야 한다. v 는 private 을 의미한다.

  • 모든 public, protected, private 변수이름들은 m 다음에는 mFooVar 의 F같이 대문자로 시작해야한다.

  • 모든 포인터 변수들은 다음과 같이 p로 시작해야 한다.

    • Public 변수 mpFooVar 과 메쏘드 FooNum()

    • Protected 변수 mtpFooVar 와 메쏘드 tFooNum()

    • Private 변수 mvpFooVar 와 메쏘드 vFooNum()

세계적으로 일관된 C++ 코딩 관습은 보다 프로그래밍을 잘 할 수 있도록 도와줄 것이다.

아래에 주어진 예제 코드에서 t 는 protected를, v 는 private를, m 는 member-variable 를, p 는 pointer를 의미한다.

class SomeFunMuncho
{
	public:
		int	mTempZimboniMacho; // OOP에서는 오직 임시 변수들만 public이어야 한다.
		float	*mpTempArrayNumbers;
		int	HandleError();
		float	getBonyBox();  // 변수에 접근하기 위한 함수
		float	setBonyBox();  // 변수에 접근하기 위한 함수

	protected:
		float	mtBonyBox;
		int	*mtpBonyHands;
		char	*tHandsFull();
		int	tGetNumbers();
	private:
		float	mvJustDoIt;
		char	mvFirstName[30];
		int	*mvpTotalValue;
		char	*vSubmitBars();
		int	vGetNumbers();
};
프로그램이 수백만 라인으로 커지게 되면, 위와 같은 관습을 매우 좋아하게 될 것이다. 단순히 mvFirstName 라는 변수이름을 본 것만으로도, 이것이 클래스의 멤버이고, private 변수라는 것을 알 수 있으므로 코드의 가독성은 올라가게 된다.

다음의 C++ 코딩 표준 URL들을 방문해보아라.

  • C++ FAQ Lite - Coding standards http://www.parashift.com/c++-faq-lite/coding-standards.html

  • Rice university coding standard http://www.cs.rice.edu/~dwallach/CPlusPlusStyle.html

  • Identifiers to avoid in C++ Programs http://oakroadsystems.com/tech/cppredef.htm

  • Coding standards from Possibility http://www.possibility.com/Cpp/CppCodingStandard.html and mirror site

  • Coding standards for Java and C++ from Ambysoft http://www.ambysoft.com/JavaCodingStandards.html

  • Rules and recommendations http://www.cs.umd.edu/users/cml/cstyle/

  • Indent and annotate http://www.cs.umd.edu/users/cml/cstyle/indhill-annot.html

  • Elemental rules http://www.cs.umd.edu/users/cml/cstyle/Ellemtel-rules.html

  • C++ style doc http://www.cs.umd.edu/users/cml/cstyle/Wildfire-C++Style.html

  • C++ Coding Standards by Brett Scolcum http://www.skypoint.com/~slocum/prog/cppstds.html

  • Logikos C++ Coding Standards http://www.logikos.com/standards/cpp_std.html

  • NRad C++ coding standards http://cadswes.colorado.edu/~billo/standards/nrad

  • BEJUG C++ coding standards http://www.meurrens.org/ip-Links/java/joodcs/ToddHoff.html

  • Arctic Labs coding standards http://www.arcticlabs.com/codingstandards

    See also

  • For rapid navigation with ctags Vim color text editor

  • To improve productivity see C++ Beautifier HOWTO


15절. C++ 스크립트 언어

C++의 큰 단점은 조그만 변경을 할 때마다 항상 컴파일과 링크를 다시 해주어야 실행파일을 만들 수 있다는 것이다. 컴파일/링크/디버깅 사이클은 많은 시간이 걸리고 생산적이지 못하다. 현대의 CPU와 RAM은 매우 빠르고 싸지고 있으므로, 어떤 때는 하드웨어에 많은 돈을 투자하고, 개발을 위해서는 script 언어를 쓰는 것이 좋을 수도 있다.


16절. Templates

Template 는 코드 재사용을 쉽게 만들어 generic 프로그래밍을 가능하게 하는 C++의 특징이다.

아래와 같은 간단한 예를 보자 :

#include <string>
#include <iostream>

void printstring(const std::string& str) {
    std::cout << str << std::endl;
}

int main()
{
    std::string str("Hello World");
    printstring(str);
}

printstring() 는 std::string 를 첫번째 인자로 받는다. 따라서 이는 오직 string만 프린트 할 수 있고, 문자배열(char array)을 프린트 하기 위해서는 함수를 오버로딩시키든가 새로운 이름의 함수를 만들어야 한다.

이는 함수의 구현이 중복되므로 좋지 않은 것이고, 유지보수하기가 힘들어지게 된다.

template을 쓰면 우리는 코드를 재사용가능하게 만들 수 있다. 아래와 같은 함수를 보라 :

template<typename T> 
void print(const T& var) {
    std::cout << var << std::endl;
}

컴파일러는 우리가 무슨 타입을 넘겨주든지 알아서 print 함수의 코드를 자동으로 만들어 줄 것이다. 이것이 template의 중요한 장점이다. Java는 template이 없고, 따라서 Java에서의 generic 프로그래밍과 코드 재 사용은 더 힘들다.

레퍼런스 :

  • http://babbage.cs.qc.edu/STL_Docs/templates.htm 미러 : http://www.mike95.com/c_plusplus/tutorial/templates

  • 이것은 #pragma template에 대해 알려준다 : -http://www.dgp.toronto.edu/people/JamesStewart/270/9697f/notes/Nov25-tut.html

  • 매우 좋은 site: http://www.cplusplus.com/doc/tutorial/tut5-1.html http://www.cplusplus.com/doc/tutorial

  • C++의 검증을 위해 : http://examware.com 에 가서 "Tutorials"를 클릭하고 C/C++ 버튼을 누르시오.

  • C++ Open books: http://www.softpanorama.org/Lang/cpp.shtml tutorial을 클릭

  • Templates tutorial : http://www.infosys.tuwien.ac.at/Research/Component/tutorial/prwmain.htm


17절. STL References

STL에 관한 다음 사이트들을 방문해보라 :

  • iterator에 대한 매우 좋은 설명 http://www.cs.trinity.edu/~joldham/1321/lectures/iterators/

  • Intro to STL SGI http://www.sgi.com/tech/stl/stl_introduction.html

  • Mumits STL 초보 가이드 (약간 오래된 것) http://www.xraylith.wisc.edu/~khan/software/stl/STL.newbie.html

  • ObjectSpace 예제 : ObjectSpace는 300개가 넘는 예제를 가지고 있고, 따라서 초보자에게 아주 좋은 출발점을 제시해준다.ftp://butler.hpl.hp.com/stl/examples.zip

  • Joseph Y. Laurino's STL page. http://weber.u.washington.edu/~bytewave/bytewave_stl.html

  • Marian Corcoran's STL FAQ. ftp://butler.hpl.hp.com/stl/stl.faq

STL tutorials:

  • Phil Ottewell's STL Tutorial - http://www.yrl.co.uk/~phil/stl/stl.htmlx

  • 좋지만 오래된 문서 - http://www.decompile.com/html/tut.html 미러 : http://mip.ups-tlse.fr/~grundman/stl-tutorial/tutorial.html

  • The Code Project, C++/STL/MFC 에 대한 소개 http://www.codeproject.com/cpp/stlintroduction.asp

  • C++ Standard Template Library, another great tutorial, by Mark Sebernhttp://www.msoe.edu/eecs/cese/resources/stl/index.htm

  • Technical University Vienna by Johannes Weidl http://dnaugler.cs.semo.edu/tutorials/stl mirrorhttp://www.infosys.tuwien.ac.at/Research/Component/tutorial/prwmain.htm

Main STL sites:

  • C++ STL from SGI http://www.sgi.com/tech/stl

  • C++ STL from RPI univ http://www.cs.rpi.edu/projects/STL/htdocs/stl.html

  • C++ STL site ODP for STL 와 미러

  • STL for C++ Programmers http://userwww.econ.hvu.nl/~ammeraal/stlcpp.html

  • C++ STL from halper http://www.halpernwightsoftware.com/stdlib-scratch/quickref.html


17.1절. STL 개요

STL은 프로그래머에게 몇가지 유용한 데이터구조와 알고리즘을 제공한다. 이는 다음과 같은 것들이 있다.

  • 컨테이너. 두 가지 타입이 있다 :

    • 순차적(Sequential). 여기에는 vector, list, deque 등이 있다.

    • 정렬된 조합(Associative). 여기에는 set, map, multiset, multimap 이 있다.

  • Iterator. 컨테이너의 내용을 살펴볼 수 있게 해주는 포인터 같은 것들이다.

  • 일반적인(generic) 알고리즘들. STL은 컨테이너 타입에 대해 동작하는 여러가지 효과적으로 구현된 표준 알고리즘들 (예를들어 find, sort, merge 등)이 있다. (몇몇 container들은 이 중 일부를 특별한 목적으로 멤버함수로 갖고 있다)

  • Function obejct. function object는 operator()의 정의를 제공하는 class의 instance이다. 이는 이 object들을 함수 같이 사용할 수 있다는 것이다.

  • Adaptors. STL은 다음과 같은 것들을 제공한다.

    • Container adaptor는 vector를 stack을 만들기 위한 기초로 사용할 수 있게 해준다.

    • function adaptor 는 이미 존재하는 function object로부터 새로운 function object를 만들 수 있게 해준다.

  • Allocators. 모든 STL 컨테이너 class는 프로그램이 사용하는 메모리 정보를 갖고 있기 위한 allocator class를 사용한다. 하지만 나는 이 부분은 생략할 것이다.

앞으로 vector, list, set 그리고 map 컨테이너의 사용법을 살펴볼 것이다. 이들을 사용하기 위해서는 내가 STL iterator에 대해 말할 수 있도록 iterator를 쓸 줄 알아야 할 것이다. 또 set과 map 컨테이너를 사용한다는 것은 내가 function object에 대해 뭔가 설명할 수 있도록 간단한 function object가 있어야 한다는 것이다. STL이 지원하는 알고리즘에 대해서는 간단히 설명할 것이고, adoptor는 언급하지 않을 것이다.

몇몇 함수 인자의 타입에 대해서 이름이 바뀔 수 있다. 예를들어 대부분의 int 타입 인자들은 실제로는 size_type이라는 type을 갖고 이것이 적절한 기본 타입으로 tyepdef되는 형태에 의해 쓰인다. 만약 여러 함수들의 실제 인자 타입을 알고싶다면 작업하는 것에 대한 문서나 헤더파일을 참고해라.

STL에서 제공되는 몇가지 유틸리티 class들이 있는데, 이 중 제일 중요한 것은 pair class이다. 이는 다음과 같이 정의되어있다.

template<class T1, class T2>
class pair {
public:
    T1 first;
    T2 second;
    pair(const T1& a, const T2& b) : first(a), second(b) {}


};

그리고 쉽게 pair를 만들도록 다음과 같은 make_pair 함수가 제공된다 :

pair<T1,T2> make_pair(const T1& f, const T2&,s)

또한 ==와 < 연산자도 있다. 이 template class에는 복잡한 것이 없고 그냥 사용하면 된다. 이를 이용하기 위해서는 #include 로 <utility>를 include하면 된다. pair는 여러곳에서 쓰일 수 있는데, 특히 set과 map class에서 많이 나타난다.


17.4절. 벡터 : Vectors

벡터는 C++의 배열과 비슷한, 하지만 이를 발전시킨 컨테이너이다. 특히, 벡터는 선언시에 얼마나 벡터가 커야할지를 알 필요가 없고, push_back함수를 이용하여 언제나 새로운 원소를 추가할 수 있다. ( 사실 insert 함수가 어디에든 새 원소들을 넣을 수 있게 해주지만, 이는 매우 비효율적이다. 만약 이를 자주 해야한다면 list를 대신 사용하는 것을 고려해보아라. )


17.4.4절. 벡터의 원소를 추가 / 삭제하기

위에 언급된 [] 외에도, 벡터의 원소에 접근하거나 바꿀 수 있는 방법이 몇가지 더 있다.

  • push_back은 새로운 원소를 벡터의 끝에 더할 것이다.

  • pop_back은 벡터의 끝에서 원소를 하나 없앨 것이다.

  • insert 는 하나 또는 여러개의 원소를 벡터의 원하는 위치에 삽입할 것이다.

  • erase는 하나 또는 여러개의 원소를 원하는 위치에서 없앨 것이다.

그런데 insert나 erase는 벡터에서 오버헤드가 큰 연산임에 주의하라. 만약 insert나 erase를 써야한다면, 벡터 대신 list 데이터구조를 사용하는 것이 더 효율적일 것이다.

<vector-mod.cpp>=
#include <iostream>
#include <vector>

using namespace std;

int main()
{
    vector<int> v;

    for (int i=0; i<10; i++) v.push_back(i);
    cout << "Vector initialised to:" << endl;
    for (int i=0; i<10; i++) cout << v[i] << ' ' ;
    cout << endl;

    for (int i=0; i<3; i++) v.pop_back();
    cout << "Vector length now: " << v.size() << endl;
    cout << "It contains:" << endl;
    for (int i=0; i<v.size(); i++) cout << v[i] << ' ';
    cout << endl;

    int a1[5];
    for (int i=0; i<5; i++) a1[i] = 100;

    v.insert(& v[3], & a1[0],& a1[3]);
    cout << "Vector now contains:" << endl;
    for (int i=0; i<v.size(); i++) cout << v[i] << ' ';
    cout << endl;

    v.erase(& v[4],& v[7]);
    cout << "Vector now contains:" << endl;
    for (int i=0; i<v.size(); i++) cout << v[i] << ' ';
    cout << endl;
}

위의 예에서는 벡터 v가 선언된 후, push_back을 사용하여 초기화 되었다. 그리고 pop_back으로 뒤의 몇 원소가 없어졌고, 배열이 하나 만들어져서 그 내용이 insert를 사용해서 v에 삽입되었다. 마지막으로 몇몇 원소들을 지우기 위해 erase가 사용되었다. 위에 사용된 함수들은 다음과 같은 인자들을 받는다.

  • push_back : vector에 들어가는 것과 같은 타입의 인자를 하나 받는다.

  • pop_back : 인자를 받지 않는다. 그리고 빈 벡터에 대해 pop_back을 하면 안된다.

  • insert 는 세 가지 형태로 쓰인다.

    • insert(pos, T& x) : 원소 x 하나를 pos가 가리키는 위치에 삽입한다.

    • insert(pos, start, end) : 다른 컨테이너 안의 내용을 pos가 가리키는 위치에 삽입한다.

    • 삽입되는 원소들은 start에서 시작해서, end를 만날 때까지 (end가 가리키는 것은 들어가지 않는다) 이다.

    • insert(pos, int rep, T& x) : rep 개의 x값을 pos 위치에 삽입한다. (같은 값을 여러번 삽입)

위의 코드에 나와있듯이, pos가 가리키는 포지션 값은 원소가 삽입될 곳의 주소여야 한다. 마찬가지로 start와 end도 주소값이어야 한다. (사실 이것은 이들이 iterator이기 때문이다. 이에 대해서는 다음 장에서 더 살펴볼 것이다.)

  • erase는 두 가지 형태로 쓰인다 (pos, start와 end는 insert와 같은 형식을 갖는다)

    • erase(pos) : pos가 가리키는 위치의 원소를 없앤다.

    • erase(start,end) : start에서 end까지(end는 포함하지 않음)에 해당하는 원소들을 없앤다.


17.4.5절. Vector Iterator

벡터 v의 원소들을 차례대로 보는 가장 쉬운 방법은 위에 한 방법같이 하는 것이다.

for (int i=0; i<v.size(); i++) { ... v[i] ... }

또다른 방법은 바로 iterator를 이용하는 것이다. iterator는 컨테이너의 포인터라고 생각하면 된다. 따라서 이를 증가시키면서 원소를 하나씩 접근하는 것이 가능하다. 벡터가 아닌 컨테이너의 경우는 iterator가 원소를 차례대로 접근할 수 있는 유일한 방법이다.

type T의 원소를 갖고 있는 벡터의 경우 :

vector<T> v;

iterator는 다음과 같이 선언된다.

vector<T>::iterator i;

이러한 iterator는 begin()이나 end()같은 함수에 의해 리턴되는 값으로 만들어진다. 같은 타입의 iterator들은 == 나 != 로 비교가능하고, ++을 이용한 증가나 *를 이용한 참조 등이 가능하다. [ 이 외에도 벡터 iterator는 더 많은 연산자를 갖고 있다. 이에 대해서는 다음 장을 참고해라 ].

다음은 iterator를 어떻게 벡터와 사용하는 지에 대한 예제이다.

<vector-iterator.cpp>=
#include <iostream>
#include <vector>

using namespace std;

int main()
{
    vector<int> v(10);

    int j = 1;

    vector<int>::iterator i;

    // v를 1에서 10까지의 정수로 채운다.
    i = v.begin();
    while (i != v.end())
    {
        *i = j;
        j++;
        i++;
    }

    // v의 각 원소를 제곱한다.
    for (i=v.begin(); i!=v.end(); i++) *i = (*i) * (*i);

    // v의 내용을 출력한다.
    cout << "The vector v contains: ";
    for (i=v.begin(); i!=v.end(); i++) cout << *i << ' ';
    cout << endl;

}

*i 가 등호의 왼쪽(LHS)에서는 값을 변경하기 위해, 오른쪽(RHS)에서는 값을 참조하기 위해 쓰인 것에 주목해라.


17.4.6절. 벡터의 비교

두 개의 벡터를 ==와 <를 이용해서 비교할 수 있다. ==는 양 쪽의 벡터가 같은 수의 원소를 갖고 대응되는 각원소들이 모두 같을 때 true를 리턴할 것이다. <은 두 벡터의 원소들을 차례대로 사전순서(lexicographical order)대로 비교한다. 예를들어 v1과 v2를 비교한다고 해보자 (v1 < v2). i=0이라 할 때, v1[i] < v2[i] 이면 true를 리턴하고, v1[i] > v2[i] 이면 false를 리턴한다. 만약 둘이 같으면 i를 증가시킨다 (즉, 다음 원소로 넘어간다). 만약 v1의 끝이 v2가 끝나기 전에 나타났다면 (즉, v1의 원소의 개수가 더 작고, v1이 v2의 앞부분과 내용이 같을 때) true를 리턴하고, 그렇지 않으면 false를 리턴한다. 다음의 예를 보자.

(1,2,3,4) < (5,6,7,8,9,10) 는 false.
(1,2,3) < (1,2,3,4) 는 true
(1,2,3,4) < (1,2,3) 는 false
(0,1,2,3) < (1,2,3) 는 true

아래의 코드는 위에서 세번째 예를 보여준다.

<vector-comp.cpp>=
#include <vector>
#include <iostream>

using namespace std;

int main()
{
    vector<int> v1;
    vector<int> v2;
    for (int i=0; i<4; i++) v1.push_back(i+1);
    for (int i=0; i<3; i++) v2.push_back(i+1);

    cout << "v1: ";
    for (int i=0; i<v1.size(); i++) cout << v1[i] << ' ';
    cout << endl;

    cout << "v2: ";
    for (int i=0; i<v2.size(); i++) cout << v2[i] << ' ';
    cout << endl;

    cout << "v1 < v2 is: " << (v1<v2 ? "true" : "false") << endl;
}
<= 와 >= 역시 예상하는 대로 동작할 것이다.


17.7절. 집합(Set)

set 컨테이너 타입은 벡터같이 인덱스를 통해 원소에 접근하는 것이 아니라, 원소를 직접 저장하고 뺄 수 있도록 해준다. set 컨테이너는 서로 다른 원소들만을 갖는 수학적인 집합과 같이 동작한다. 그러나, 수학적인 집합과는 다르게, 집합 안의 원소들은 (사용자가 지정하는) 어떤 순서 대로 저장되게 된다. 실제로 이것은 set 컨테이너로 수학적인 집합을 구현하는 데 있어 작은 제한일 뿐이고, 이렇게 함으로써 순서가 없는 것보다 많은 연산에서 더 효율적이 될 수 있다.


17.7.1절. Set을 만들기

set 컨테이너를 만들기 위해서는 두 가지 template 인자가 필요하다 - 이는 set이 갖게 될 원소들의 타입과 두 원소를 비교할 수 있는 비교함수 function object의 타입이다.

set<T, Compare> s;

(set < T > s와 같은 선언도 가능해야한다. 이는 두번째 인자로서 디폴트 template 인자인 less < T >를 사용한다. 하지만 많은 C++ 컴파일러 (g++포함)가 기본 template 인자를 지원하지 못하고 있다.)

간단한 타입 T 에 대해서는 less < T > function object를 쓸 수도 있다. ( "function object"가 무엇인가 하는 고민은 할필요 없다.) 예를들어 아래와 같이 선언하면 된다.

set<int, less<int> > s1;
set<double, less<double> > s2;
set<char, less<char> > s3;
set<string, less<string> > s4;

( 선언할 때 뒤쪽의 > 두 개가 space로 띄어져 있음에 주의하라. 이는 compiler가 >를 쉬프트 연산자(>>) 와 구별하기 위해 꼭 필요한 것이다.) 각각의 경우 function object들은 각각의 타입에 맞게 <를 사용할 것이다. (이는 각각 int, double, char, string 타입이다. )

아래의 코드는 정수(int)의 set을 선언하고, insert 메쏘드를 사용하여 정수를 몇개 추가한다. 그리고 set을 차례대로 보면서 원소들을 출력한다. 재미있는 것은 추가하는 순서가 어떤 순서이든지 set의 내용은 정렬된 상태로 출력된다는 것이다.

<set-construct1.cpp>=
#include <iostream>
#include <set>

using namespace std;

int main()
{
    set<int, less<int> > s;
    set<int, less<int> >::iterator i;

    s.insert(4);
    s.insert(0);
    s.insert(-9);
    s.insert(7);
    s.insert(-2);
    s.insert(4);
    s.insert(2);

    cout << "The set contains the elements: ";
    for (i=s.begin(); i!=s.end(); i++) cout << *i << ' ';
    cout << endl;
}

4가 두번 추가되었음에도 불구하고, 한번밖에 나오지 않는 것에 주의해라. 이는 집합이기 때문에 당연한 것이다.


17.7.2절. Function Objects란 무엇인가?

C++의 멋진 특징 중 하나는 연산자의 오버로딩이다. 따라서 새로 만들어진 class에 대해 + 가 어떤 의미든지 갖도록 할 수 있다. 그런데, C++에서 오버로드 할 수 있는 연산자 중 함수 호출 연산자인 ()가 있고, 이는 class의 인스턴스가 함수와 같이 동작할 수 있도록 해줄 수 있다. 이것이 function object이다.

간단한 예제를 보자.

<function-object.cpp>=
#include <iostream>

using namespace std;

template<class T>
class square {
public:
    T operator()(T x) { return x*x; }
};
// 이는 *가 정의되는 어떤 T에 대해서든지 쓰일 수 있다.

int main()
{
    // function object를 만든다.
    square<double> f1;
    square<int> f2;

    // 이를 사용한다.
    cout << "5.1^2 = " << f1(5.1) << endl;
    cout << "100^2 = " << f2(100) << endl;

    // 아래의 내용은 컴파일 에러를 출력할 것이다.
    // cout << "100.1^2 = " << f2(100.1) << endl;
}

function object는 STL의 몇몇 부분, 특히 set과 map에서 많이 쓰인다.

function object가 필요한 경우를 생각해보자. 아래의 내용을 만족하는 comp라는 것을 생각해보자.

  1. 만약 comp(x,y), comp(y,z)가 true이면, comp(x,z)도 역시 true이다.

  2. comp(x,x)는 언제나 false이다.

어떤 x,y에 대해 comp(x,y)와 comp(y,x)가 false이면 x와 y는 같은 객체이다.

이는 숫자에서 미만관계 ( < )를 나타낸다. 위에서 쓰인 less < T > function object 는 type T에 대해 < 연산자로 정의되어 있다. 즉, 다음과 같다.

template<class T>
struct less {
  bool operator()(T x, T y) { return x<y; }
}

(진짜 정의는 레퍼런스를 사용하고, 적절한 const 선언을 사용하며 binary_function template class를 상속받는다.)

이는 만약 T가 < 연산자를 그 타입에 대해 정의해놓았다면, T 타입의 집합을 선언할 때, 비교를 위한 것으로 less < T > 를 사용할 수 있다는 것이다. 만약 < 연산자가 하고자 하는 것과 맞지 않을 수도 있다. 이럴 때는 다른 예가 있다. 이는 < 연산자를 이용하여 간단한 class를 만들고, 다른 방식의 비교를 하는 function object를 만든다. 오버로딩 된 <와 () 연산자가 STL과 잘 돌아가기 위해서는 const 를 적당히 써줘야 한다는 것에 주의하라.

<set-construct2.cpp>=
#include <iostream>
#include <set>

using namespace std;

// 이 class는 두 개의 멤버 변수를 갖는다.
// 오버로딩된 <은 멤버 f1값을 갖고 두 class를 비교한다.
class myClass {
private:
    int f1;
    char f2;
public:
    myClass(int a, char b) : f1(a), f2(b) {}
    int field1() const { return f1; }
    char field2() const { return f2; }
    bool operator<(myClass y) const
    { return (f1<y.field1()); }
};

// 이 function object는 멤버 f2의 값을 기초로
// myClass 타입의 객체들을 비교한다.
class comp_myClass {
public:
    bool operator()(myClass c1, myClass c2) const
    { return (c1.field2() < c2.field2()); }
};

int main()
{
    set<myClass, less<myClass> > s1;
    set<myClass, less<myClass> >::iterator i;
    set<myClass, comp_myClass> s2;
    set<myClass, comp_myClass>::iterator j;

    s1.insert(myClass(1,'a'));
    s2.insert(myClass(1,'a'));
    s1.insert(myClass(1,'b'));
    s2.insert(myClass(1,'b'));
    s1.insert(myClass(2,'a'));
    s2.insert(myClass(2,'a'));

    cout << "Set s1 contains: ";
    for (i=s1.begin(); i!=s1.end(); i++)
    { 
        cout << "(" << (*i).field1() << "," 
                << (*i).field2() << ")" << ' ';
    }
    cout << endl;

    cout << "Set s2 contains: ";
    for (j=s2.begin(); j!=s2.end(); j++)
    {
        cout << "(" << (*j).field1() << "," 
                << (*j).field2() << ")" << ' ';
    }
    cout << endl;
}

(1,a)와 (2,a)를 가진 집합 s1은 f1을 기준으로 비교를 한다. 따라서 (1,a)와 (1,b)는 같은 원소로 취급된다. (1,a)와 (1,b)를 가진 집합 s2는 f2를 기준으로 비교를 하기 때문에 (1,a)와 (2,a)가 같은 원소로 취급된다.


17.7.6절. 원소를 추가하거나 삭제하기

집합에 원소를 추가하는 것은 insert 메쏘드 (위에 사용한 것과 같이)를, 삭제하는 것은 erase 메쏘드를 통해 이루어진다.

타입 T의 원소들을 갖고 있는 집합의 경우, 다음과 같이 이루어진다 :

  • pair < iterator, bool> insert(T& x). 이는 표준 insert 함수이다. 리턴값은 무시할수도 있고, 성공적으로 추가했는지를 알기 위해 사용할 수도 있다 (같은 원소가 이미 집합에 있을 경우 실패한다). 만약 추가가 성공했다면, bool 값은 true이고, iterator는 금방 추가된 원소를 가리키게 될 것이다. 만약 원소가 이미 존재하는 것이라면, bool 값은 false이고, iterator는 이미 있는 값이 동일한 원소를 가리키게 될 것이다.

  • iterator insert(iterator position, T& x). 이 insert 함수는 인자로서 추가하고자 하는 원소 외에 iterator를 받는데, 이는 추가할 위치를 찾기 시작할 iterator이다. 리턴되는 iterator는 위와 마찬가지로 새로 추가된 원소나 이미 존재하는 같은 값의 원소이다.

  • int erase(T& x). 이 erase함수는 지우고자 하는 원소를 인자로 받아 만약 그 원소가 존재하면 지우고서 1을 리턴하고, 없으면 0을 리턴한다.

  • void erase(iterator position). 이 erase함수는 특정 원소를 가리키는 iterator를 인자로 받아 그 원소를 지운다.

  • void erase(iterator first, iterator last). 이 erase함수는 두 iterator를 인자로 받아 [first,last] 범위의 모든 원소를 지운다.

아래의 예는 위 함수들의 사용법을 보여준다.

<set-add-delete.cpp>=
#include <iostream>
#include <set>
#include "printset.h"

using namespace std;

int main()
{
    set<int, less<int> > s1;

    // 표준적인 방식으로 원소를 추가한다.
    s1.insert(1);
    s1.insert(2);
    s1.insert(-2);

    // 특정위치에 원소 삽입
    s1.insert(s1.end(), 3);
    s1.insert(s1.begin(), -3);
    s1.insert((s1.begin()++)++, 0);

    cout << "s1 = " << s1 << endl;

    // 성공적으로 추가되었는지 체크
    pair<set<int, less<int> >::iterator,bool> x = s1.insert(4);
    cout << "Insertion of 4 " << (x.second ? worked. : failed.) 
            << endl;
    x = s1.insert(0);
    cout << "Insertion of 0 " << (x.second ? worked. : failed.) 
            << endl;

    // insert에서 리턴된 iterator를 두번째 형태의 insert의 인자로
    // 사용할 수 있다.
    cout << "Inserting 10, 8 and 7." << endl;
    s1.insert(10);
    x=s1.insert(7);
    s1.insert(x.first, 8);

    cout << "s1 = " << s1 << endl;

    // 몇 원소들을 지운다.
    cout << "Removal of 0 " << (s1.erase(0) ? worked. : failed.)
            << endl;
    cout << "Removal of 5 " << (s1.erase(5) ? worked. : failed.)
            << endl;

    // 원소를 찾아서, 지운다. (find 함수는 다음 장을 참조)
    cout << "Searching for 7." << endl;
    set<int,less<int> >::iterator e = s1.find(7);
    cout << "Removing 7." << endl;
    s1.erase(e);

    cout << "s1 = " << s1 << endl;

    // 마지막으로 모든 원소를 지운다.
    cout << "Removing all elements from s1." << endl;
    s1.erase(s1.begin(), s1.end());
    cout << "s1 = " << s1 << endl;
    cout << "s1 is now " << (s1.empty() ? empty. : non-empty.)
            << endl;
}


17.7.8절. 집합 연산

STL은 부분집합, 합집합, 교집합, 차집합, 대칭차집합(XOR) 등의 집합연산을 generic 알고리즘으로 제공한다. 이 함수들을 이용하기 위해서는 algo.h를 include 해야한다. (아래의 내용중 iter는 적절한 iterator를 의미한다).

  • bool includes(iter f1,iter l1,iter f2,iter l2).

    위 함수는 [f2,l2] 범위에 있는 것들이 [f1,l1] 안의 것들을 포함하는 지를 체크한다. 만약 포함하면 true를, 그렇지 않으면 false를 리턴한다. 따라서 한 집합이 다른 집합을 포함하는 지를 보려면, 다음과 같이 하면 된다.

    includes(s1.begin(), s1.end(), s2.begin(), s2.end())

    The includes function checks the truth of 3#3 ( that is of 4#4). 이 함수는 집합이 < 연산자를 이용해 정렬되었다고 본다. 만약, <이 아닌 다른 연산자 가 사용되었다면, 이(function object)를 마지막 인자로서 추가로 넘겨주면 된다.

  • iter set_union(iter f1,iter l1,iter f2,iter l2,iter result).

    이는 [f1,l1]과 [f2,l2] 범위에 있는 집합들의 합집합을 만든다. 인자로 주는 result 값은 새로만들어진 합집합의 첫 인자를 가리키는 iterator이다. 리턴값은 새로운 집합의 끝(end)를 가리키는 iterator이다.

result 인자가 iterator란 말은, 다음과 같은 식으로 set_union을 사용하면 안된다는 것이다.

      set<int, less<int> > s1, s2, s3;
      // s1 과 s2의 원소를 가지고 합집합을 만든다.
      // (그러나 이런 식으로는 동작하지 않음)
      set_union(s1.begin(), s1.end(), 
                s2.begin(), s2.end(), 
                s3.begin());

그 이유는 begin()과 end()가 집합이나 맵에 사용될 때는 상수 input iterator가 되기 때문이다. 이러한 iterator는 집합의 원소를 읽기 위해서는 사용될 수 있지만, 값을 쓸 수는 없다. (또한 만약 값을 쓸 수 있게 한다면 집합의 순서를 망가뜨릴 수 있는 위험이 있기 때문이기도 하다)

해결책은 set_type의 insert iterator를 사용하는 것이다. 이는 (*i)=value 같은 불가능한 구문을 s.insert(i,value)의 형태로 쓸 수 있게 해준다. (여기서 s는 iterator i가 가리키는 집합이다. 이는 다음과 같이 쓰인다.

      // 편의를 위해 Typedef를 사용
      typedef set<int, less<int> > intSet;  
      intSet s1, s2, s3;
      // s1과 s2에 몇 원소를 추가.
      // 그리고 합집합을 구한다.
      set_union(s1.begin(), s1.end(), 
                s2.begin(), s2.end(), 
                insert_iterator<intSet>(s3,s3.begin()) );

이제 위에 나오는 것들을 종합적으로 사용하는 예제를 보자.

<set-theory.cpp>=
#include <iostream>
#include <set>
#include <algorithm>
#include <iterator>
#include "printset.h"

using namespace std;

int main()
{
    typedef set<int, less<int> > intSet;

    intSet s1, s2, s3, s4;

    for (int i=0; i<10; i++)
    { s1.insert(i);
        s2.insert(i+4);
    }
    for (int i=0; i<5; i++) s3.insert(i);

    cout << "s1 = " << s1 << endl;
    cout << "s2 = " << s2 << endl;
    cout << "s3 = " << s3 << endl;

    // s1이 s2의 부분집합인가?
    bool test = includes(s2.begin(),s2.end(),s1.begin(),s1.end());
    cout << "s1 subset of s2 is " << (test ? true. : false.) << endl;

    // s3가 s1의 부분집합인가?
    test = includes(s1.begin(),s1.end(),s3.begin(),s3.end());
    cout << "s3 subset of s1 is " << (test ? true. : false.) << endl;

    // s1과 s2의 합집합.
    set_union(s1.begin(), s1.end(), s2.begin(), s2.end(),
            insert_iterator<intSet>(s4,s4.begin()) );
    cout << "s1 union s2 = " << s4 << endl;

    // s4를 지우고, s1과 s2의 교집합을 구한다.
    // ( 만약 s4를 지우지 않으면 원래 s4에 들어있는 것들도
    // 같이 들어가게 될 것이다. )
    s4.erase(s4.begin(),s4.end());
    set_intersection(s1.begin(), s1.end(), s2.begin(), s2.end(),
            insert_iterator<intSet>(s4,s4.begin()) );
    cout << "s1 intersection s2 = " << s4 << endl;

    // 차집합
    s4.erase(s4.begin(),s4.end());
    set_difference(s1.begin(), s1.end(), s2.begin(), s2.end(),
            insert_iterator<intSet>(s4,s4.begin()) );
    cout << "s1 minus s2 = " << s4 << endl;

    // 차집합은 대칭적이지 않다. (즉, A-B != B-A)
    s4.erase(s4.begin(),s4.end());
    set_difference(s2.begin(), s2.end(), s1.begin(), s1.end(),
            insert_iterator<intSet>(s4,s4.begin()) );
    cout << "s2 minus s1 = " << s4 << endl;

    // 대칭차집합
    s4.erase(s4.begin(),s4.end());
    set_symmetric_difference(s1.begin(), s1.end(), s2.begin(), s2.end(),
            insert_iterator<intSet>(s4,s4.begin()) );
    cout << "s1 symmetric_difference  s2 = " << s4 << endl;

    // 대칭차집합은 대칭적이다. (즉, commutative)
    s4.erase(s4.begin(),s4.end());
    set_symmetric_difference(s2.begin(), s2.end(), s1.begin(), s1.end(),
            insert_iterator<intSet>(s4,s4.begin()) );
    cout << "s2 symmetric_difference  s1 = " << s4 << endl;
}


18절. C++에서의 쓰레드


18.2절. C++에서 쓰레드 class 디자인하기

이 장은 Ryan Teixeira 에 의해 쓰여졌고, 그 문서는 여기에 있다. .


18.2.2절. 쓰레드에 대한 간단한 소개

쓰레드를 이해하기 위해서는, 한꺼번에 돌아가는 여러 프로그램을 생각해야한다. 또한, 이 프로그램들이 똑같은 전역변수와 함수들에 접근한다고 생각해보아라. 이 프로그램들은 실에 비유될 수 있고, 그래서 쓰레드라고 불린다. 중요한 차이점이 있다면, 각각의 쓰레드는 다른 쓰레드가 진행하는 것을 기다릴 필요가 없다는 것이다. 모든 쓰레드가 동시에 진행된다. 비유를 하자면, 이들은 육상선수와 같이 아무도 다른 선수를 기다리지 않는다. 각자 자신의 속도로 진행되는 것이다.

왜 쓰레드를 사용하냐고 물어본다면, 쓰레드는 종종 어플리케이션의 성능을 향상시킬 수 있고, 구현하는게 까다롭지 않다. 즉, 조그만 투자로 큰 효과를 볼 수 있는 것이다. 이미지를 서비스하는 이미지 서버 프로그램을 생각해보아라. 이 프로그램은 다른 프로그램으로부터 이미지에 대한 요청을 받는다. 그러면 이 이미지를 데이터베이스에서 찾아 요청을 보낸 프로그램에게 다시 보내준다. 만약 서버가 하나의 쓰레드로 만들어졌다면, 한번에 하나의 프로그램만 요청을 보낼 수 있을 것이다. 만약 프로그램이 이미지를 찾거나 보내주는 중이라면 다른 요청을 처리할 수 없을 것이다. 물론 이러한 시스템을 쓰레드를 이용하지 않고도 만들 수 있지만, 쓰레드를 쓰면, 여러개의 요청을 아주 자연스럽게 처리할 수 있게 된다. 간단한 접근 방법은 하나의 요청당 하나의 쓰레드를 만드는 것이다. 메인 쓰레드는 요청에 따라 쓰레드를 만들어주기만 하면 된다. 그러면 새로 만들어진 쓰레드가 요청하는 프로그램과 대화하면서 서비스를 해주면 된다. 이미지를 찾아서 보낸 후에는 쓰레드가 스스로 종료하면 된다. 이렇게 하면 하나의 요청을 서비스 하는 도중에도 다른 요청을 받을 수 있는 유연한 시스템이 될 것이다.


18.2.4절. 구현

우리는 약간 제한된 기능을 갖는 쓰레드 class를 만들 것이다. 실제 쓰레드는 이 class가 하는 것보다 훨씬 많은 일들을 할 수 있다.

class Thread
{
   public:
      Thread();
      int Start(void * arg);
   protected:
      int Run(void * arg);
      static void * EntryPoint(void*);
      virtual void Setup();
      virtual void Execute(void*);
      void * Arg() const {return Arg_;}
      void Arg(void* a){Arg_ = a;}
   private:
      THREADID ThreadId_;
      void * Arg_;

};

Thread::Thread() {}

int Thread::Start(void * arg)
{
   Arg(arg); // user 데이터를 저장함.
   int code = thread_create(Thread::EntryPoint, this, & ThreadId_);
   return code;
}

int Thread::Run(void * arg)
{
   Setup();
   Execute( arg );
}

/*static */
void * Thread::EntryPoint(void * pthis)
{
   Thread * pt = (Thread*)pthis;
   pthis->Run( Arg() );
}

virtual void Thread::Setup()
{
        // Setup에 해당하는 일들
}

virtual void Thread::Execute(void* arg)
{
        // 실행할 내용
}

우리가 쓰레드를 C++ 객체로 사용하고자 한다는 것을 이해하는 것이 중요하다. 각각의 객체는 하나의 쓰레드에 대한 인터페이스를 제공한다. 쓰레드와 객체는 다르다. 객체는 쓰레드 없이 존재할 수 있다. 이 구현에서, 쓰레드 자체는 Start 함수가 불릴 때까지 존재하지 않는다.

여기서 user의 인자를 class에 저장한다는데 주의해라. 이는 쓰레드가 시작될 때까지 임시로 이를 저장할 공간이 필요하기 때문이다. 운영체제 쓰레드는 인자를 하나 넘길 수 있게 해주지만, 우리는 this 때문에 이를 직접 넘겨줄 수 없다. 그래서 우리는 인자를 잠시 class에 저장했다가 함수가 시작될 때 다시 꺼내서 넘겨주게 된다.

Thread(); 생성자이다.

int Start(void * arg); 이 함수는 쓰레드를 만들고, 이를 시작하게 해준다. 이 인자는 쓰레드에 데이터를 넘겨주기 위해 사용되고, Start()는 운영체제의 쓰레드 생성 함수를 부름으로써 쓰레드를 만든다.

int Run(void * arg); 이 함수는 건드리면 안되는 함수이다.

static void * EntryPoint(void * pthis); 이 함수는 쓰레드의 시작 점 역할을 한다. 이 함수는 단순히 pthis를 Thread *로 casting해서 Run 함수를 불러준다.

virtual void Setup(); 이 함수는 쓰레드가 만들어진 후, 실행이 시작되기 전에 불려진다. 이 함수를 override 할 때는, 부모 class의 Setup()를 부르는 것을 기억하라.

virtual void Execute(void *); 하고자 하는 일을 위해 이 함수를 override해라.


19절. C++ 유틸리티들

C++ 유틸리티를 위한 다음 사이트를 방문해보라.

  • Portable C++ utilities from http://www.boost.org. Boost 웹사이트는 이식 가능한 C++ 소스 라이브러리를 제공한다. 중요한 점은 이 라이브러리가 C++ 표준 라이브러리와 함께 잘 동작한다는 것이다. 이 목적은 "쓰일 수 있는 실제적인 것"을 만들자는 것으로, 실제 구현에 참고사항이될 수 있다. 따라서 Boost 라이브러리는 최종 표준화에 적절할 것이다.

  • The smart pointer library ( http://www.boost.org/libs/smart_ptr/index.htm ) 는 다섯가지 smart pointer class template를 제공한다. smart pointer는 C++의 new 표현으로 할당되는 동적 메모리의 관리를 쉽게 해준다. 추가로 scoped_ptr는 다른 방법들로 할당되는 동적 메모리 관리를 쉽게 해준다.

  • C++ Binary File I/O http://www.angelfire.com/country/aldev0/cpphowto/cpp_BinaryFileIO.html

  • Portability Guide http://www.angelfire.com/country/aldev0/cpphowto/cpp_PortabilityGuide.html

  • Snippets collections of C++ routines http://www.angelfire.com/country/aldev0/cpphowto/cpp_Snippets.html 와 snippets site

  • escape ISB for C++ - 프로그램을 어떻게 개발하고, 분산처리, 객체기반 어플리케이션을 Netscape Internet Service Broker를 사용하는 윈도우와 유닉스에서 어떻게 개발하는지에 대한 정보를 제공한다.http://docs.iplanet.com/docs/manuals/enterprise/cpluspg/contents.htm

  • Common C++ http://www.voxilla.org/projects/projape.html

  • Large List of free C++ libs http://www.thefreecountry.com/developercity/freelib.html

  • C++ Tools http://development.freeservers.com

  • C++ Tools CUJ http://www.cuj.com/code

  • C++libs Univ of vaasa http://garbo.uwasa.fi/pc/c-lang.html


20절. 이 문서의 다른 포맷

이 문서는 14가지 포맷으로 배포된다. - DVI, Postscript, Latex, Adobe Acrobat PDF, LyX, GNU-info, HTML, RTF(Rich Text Format), Plain-text, Unix man pages, 하나의 HTML파일, SGML (linuxdoc format), SGML (Docbook format), MS WinHelp 포맷.

이 howto 문서는

  • http://www.linuxdoc.org 에서 HOWTOs를 누르고, 웹브라우저에서 CTRL+f 나 ALT+f를 이용해 howto 이름을 넣어서 찾을 수 있다.

또한, 다음 미러사이트에서도 찾을 수 있다 -

  • http://www.caldera.com/LDP/HOWTO

  • http://www.linux.ucla.edu/LDP

  • http://www.cc.gatech.edu/linux/LDP

  • http://www.redhat.com/mirrors/LDP

  • 또는 다음 페이지에서 가까운 미러사이트를 찾아볼 수도 있다. http://www.linuxdoc.org/mirrors.html site를 하나 골라서 /LDP/HOWTO/xxxx-HOWTO.html을 찾아가면 된다.

  • 이 문서의 HTML, DVI, Postscript 혹은 SGML 파일의 tar-ball을 다음 주소에서 받을 수 있다.ftp://www.linuxdoc.org/pub/linux/docs/HOWTO/other-formats/ 와 http://www.linuxdoc.org/docs.html#howto

  • Plain text 포맷은 : ftp://www.linuxdoc.org/pub/linux/docs/HOWTO 와 http://www.linuxdoc.org/docs.html#howto

  • 하나의 HTML 파일은 : http://www.linuxdoc.org/docs.html#howto 하나의 HTML 파일은 다음의 명령으로 만들어질 수 있다. (sgml2html의 man page를 보라) sgml2html -split 0 xxxxhowto.sgml

  • 프랑스어, 독일어, 스페인어, 중국어, 일본어 등의 번역은 ftp://www.linuxdoc.org/pub/linux/docs/HOWTO 와http://www.linuxdoc.org/docs.html#howto 에서 찾을 수 있다. 또 다른 언어로의 번역또한 환영한다.

이 문서는http://www.sgmltools.org에서 찾을 수 있는 "SGML-Tools"로 쓰여졌다. 소스를 컴파일하려면 다음과 같이 하면 된다.

  • sgml2html xxxxhowto.sgml (html 만들기)

  • sgml2html -split 0 xxxxhowto.sgml (하나의 html file로 만들기)

  • sgml2rtf xxxxhowto.sgml (RTF file만들기)

  • sgml2latex xxxxhowto.sgml (latex file만들기)


20.4절. 여러가지 포맷의 문서를 읽기

dvi 포맷의 문서를 보기 위해서는, xdvi 프로그램을 사용해라. xdvi 프로그램은 Redhat 리눅스의 경우 ControlPanel | Applications | Publishing | Tex menu 버튼에 있고, tetex-xdvi*.rpm 패키지에 들어있다. dvi 문서를 읽으려면 다음과 같은 명령을 쓰면 된다.

	xdvi -geometry 80x90 howto.dvi
	man xdvi
그리고 윈도우 크기를 마우스로 조정 한다. 이리저리 살표보기 위해서는 화살표키나 PageUp, PageDown키, 그리고 'f', 'd', 'u', 'c', 'l', 'r', 'p', 'n' 키 등을 위, 아래 중앙으로 움직이거나 다음페이지, 이전페이지 등으로 넘기기 위해 쓸 수 있다. expert 메뉴를 끄기 위해서는 'x'를 누르면 된다.

Postscript 파일을 읽기 위해서는 'gv'(ghostview)프로그램이나 'ghostscript'를 쓰면 된다. ghostscript 프로그램은 ghostscript*.rpm 패키지에 있고, gv 프로그램은 gv*.rpm 패키지에 들어있다. 이들은 ControlPanel | Applications | Graphics menu 버튼에 있다. gv 프로그램이 ghostscript보다 훨씬 사용하기 편하다. ghostscript와 gv 는 윈도우나 OS/2 등 다른 플랫폼에서도 사용가능하다.

  • 윈도우, OS/2등 모든 OS를 위한 ghostsciprt는 다음 사이트에 있다. http://www.cs.wisc.edu/~ghost

Postscript 문서를 읽기 위해서는 다음과 같이 하면 된다 -

		gv howto.ps
		ghostscript howto.ps

HTML 포멧 문서는 Netscape Navigator, Microsoft Internet explorer, Redhat Baron Web browser 나 다른 웹브라우저로 읽으면 된다.

latex, LyX 는 latex의 X-Window 프론트엔드인 LyX 로 읽으면 된다.


23절. 부록 A String 프로그램 파일

모든 프로그램 파일을 하나의 tar.gz 으로 4절에서 받을 수 있다. 그리고 압축을 다음과 같이 풀면 된다.

bash$ man tar
bash$ tar ztvf C++Programming-HOWTO.tar.gz
이는 압축된 파일들을 보여줄 것이다.

bash$ tar zxvf C++Programming-HOWTO.tar.gz
이는 압축을 실제로 풀 것이다.

  • 헤더파일을 먼저 읽고, 예제 cpp파일을 보아라.

    • String.h http://www.angelfire.com/country/aldev0/cpphowto/String.h

    • StringBuffer.h http://www.angelfire.com/country/aldev0/cpphowto/StringBuffer.h

    • StringTokenizer.h http://www.angelfire.com/country/aldev0/cpphowto/StringTokenizer.h

    • StringRW.h http://www.angelfire.com/country/aldev0/cpphowto/StringRW.h

    • string_multi.h http://www.angelfire.com/country/aldev0/cpphowto/string_multi.h

    • example_String.cpp example_String.cpp 'Source code of C++ howto'을 클릭해라.

  • 파일 관리 class. length() 함수만 구현되었다.

    • File.h http://www.angelfire.com/country/aldev0/cpphowto/File.h

    • File.cpp File.cpp 'Source code of C++ howto'를 클릭하라.

  • zap() 함수의 구현은 아래 링크에 있다.

    • my_malloc.h http://www.angelfire.com/country/aldev0/cpphowto/my_malloc.h

    • my_malloc.cpp my_malloc.cpp 'Source code of C++ howto'를 클릭해라.

  • String class의 구현.

    • String.cpp String.cpp 'Source code of C++ howto'를 클릭해라.

    • StringTokenizer.cpp StringTokenizer.cpp 'Source code of C++ howto'를 클릭해라.

    • StringBuffer.cpp StringBuffer.cpp 'Source code of C++ howto'를 클릭해라.

    • StringRW.cpp StringRW.cpp 'Source code of C++ howto'를 클릭해라.

  • 디버깅 도구들 ..

    • debug.h http://www.angelfire.com/country/aldev0/cpphowto/debug.h

    • debug.cpp debug.cpp 'Source code of C++ howto'를 클릭해라.

    • Makefile.unx http://www.angelfire.com/country/aldev0/cpphowto/Makefile.unx

  • String class의 작동을 테스트하기 위한 예시용 자바 파일.

    • string.java http://www.angelfire.com/country/aldev0/cpphowto/string.java




'프로그래밍 > C, C++' 카테고리의 다른 글

레슨 4: Functions  (0) 2011.06.07
레슨 3: Loops  (0) 2011.06.07
레슨 2 : If statements (if,else문)  (0) 2011.06.07
Setting Up Code::Blocks and the MINGW Compiler on Windows  (0) 2011.06.07
gcc 옵션!  (1) 2011.06.06



원문: http://kelp.or.kr/korweblog/stories.php?story=02/03/31/9629811

 

gcc는 예전에는 GNU C Compiler의 약자였으나 지금은 GNU Compiler Collection의 약자로 다양한(?) 언어의 컴파일러들의 집합체이다. gcc는 한마디로 GNU에서 개발된 ANSI C 표준을 따르는 C 언어 컴파일러라고 말할 수 있다. gcc는 ANSI C 표준에 따르기는 하지만 ANSI C 표준에는 없는 여러 가지 확장 기능이 있다. 또한 gcc는 통합개발환경(IDE)을 가지고 있지 않은 command line 컴파일러이다. 옛날 Turbo-C를 주로 사용해 보셨던 분들은 tcc.exe와 비슷하다고 생각하면 된다.

(*) -v 옵션 
현재 사용되고 있는 gcc의 버전을 나타내는 옵션이다. 특정 소프트웨어 패키지를 컴파일하기 위해 어느 버전 이상의 gcc를 쓰도록 권장하는 경우가 있는데 시스템에 깔려있는 gcc의 버전을 파악하려고 할때 사용한다.

이제 직접 프로그램 하나를 컴파일하면서 설명하도록 하겠다. 아래는 hello.c의 소스이다.

#include〈stdio.h〉 

int main() 

  printf(“hello gccn”); 
  return 0; 
}

$ gcc -o hello hello.c 
로 컴파일하면 실행파일 hello가 만들어진다.

(*) -o 파일이름 옵션 
gcc의 수행 결과 파일의 이름을 지정하는 옵션이다. 위의 예제를 단순히 
$ gcc hello.c 
로 컴파일 하면 hello라고 하는 실행파일이 만들어 지는 것이 아니라 보통의 경우 a.out이라는 이름의 실행파일이 만들어진다.
-o hello 옵션을 줌으로써 결과물을 hello라는 이름의 파일로 만들어지게 하였다.

위의 컴파일 과정을 외부적으로 보기에는 단순히 hello.c파일이 실행파일 hello로 바뀌는 것만 보이지만 내부적으로는 다음과 같은 단계를 거쳐 컴파일이 수행된다. 

 (1) C Preprocessing 
 (2) C 언어 컴파일 
 (3) Assemble 
 (4) Linking 

C Preprocessing은 C 언어 배울 때 배운 #include, #define, #ifdef 등 #으로 시작하는 여러 가지를 처리해 주는 과정이다. 그 다음 C 언어 컴파일은 C Preprocessing이 끝난 C 소스 코드를 assembly 소스코드로 변환하는 과정이다. Assemble은 그것을 다시 object 코드(기계어)로 변환하고 printf()함수가 포함되어 있는 라이브러리와 linking을 하여 실행파일이 되는 것이다. 
위의 네 가지 과정을 모두 gcc라는 실행파일이 해 주는 것일까? 겉으로 보기에는 그렇게 보일지 모르지만 실제로는 gcc는 소위 말해 front-end라고 하여 껍데기에 지나지 않고 각각을 해 주는 다른 실행파일을 gcc가 부르면서 수행된다. 
C Preprocessing을 전담하고 있는 실행파일은 cpp라고 하여 /usr/bin 디렉토리와 /usr/lib/gcc-lib/i386-redhat-linux/egcs-2.95.12 디렉토리(당연히 gcc버전과 시스템에 따라 디렉토리 위치가 다르다. gcc -v로 확인하길 바란다.)에 존재한다. C 언어 컴파일은 cc1이라는 실행파일이 담당하는데 /usr/lib/gcc-lib/i386-redhat-linux/egcs-2.95.12 디렉토리에 존재한다. Assemble과 linking은 각각 as와 ld라는 실행파일이 담당하고 /usr/bin 디렉토리에 존재하는 파일이다. (참고 : 시스템에 따라 /usr/bin이 아니라 /bin또는 /usr/local/bin 디렉토리에 존재할 수도 있다.)

gcc라는 실행파일이 하는 일을 정리해 보면 다음과 같다.
 (1) 사용자에게 옵션과 소스 파일명들의 입력을 받는다. 
 (2) 소스 파일명의 확장자를 보고 어떤 단계를 처리해야 할지 결정한다.
 (3) 사용자의 옵션을 각각의 단계를 맡고 있는 실행파일의 옵션으로 변경한다. 
 (4) 각각의 단계를 맡고 있는 실행파일을 호출(fork, exec)하여 단계를 수행하도록 한다. 

=== C Preprocessing(cpp) 
C preprocessing을 우리말로 하면 "C 언어 전처리"라고 할 수 있을 것이다. 모든 C 언어 문법책에서 정도의 차이는 있지만 C preprocessing에 대한 내용을 다루고 있다. C preprocessing에 대한 문법은 C 언어 문법의 한 부분으로 가장 기본이 되는 부분이다. C preprocessing에 관한 문법은 모두 '#'으로 시작된다. '#' 앞에는 어떠한 문자(공백 문자 포함)도 오면 안된다. 하지만 대부분의 compiler가 '#'앞에 공백 문자가 오는 경우에도 처리를 한다.

== C preprocessing이 하는 일 
 (1) 입력 : C 언어 소스 코드 
 (2) 출력 : 전처리가 완료된 C 언어 소스 코드 
 (3) 하는 일 
  - 파일 포함(file inclusion - 헤더파일 및 기타파일) 
  - 매크로(macro) 치환 
  - 선택적 컴파일(conditional compile) 
  - 기타(#line, #error, #pragma) 

cpp는 C 언어 소스코드를 입력 받아서 C preprocessing에 관련된 문법 사항을 적절히 처리하고 결과로 C 언어 소스코드를 출력하는 프로그램이다. 입력은 작성된 C 언어 소스 코드이고, 출력으로 나온 C 언어 소스 코드에는 C preprocessing 문법에 관련된 어떠한 것도 남아있지 않는다. 즉, #define, #include 등을 찾을 수 없다. 남아 있는 정보가 있다면 file 이름과 줄수(line number)에 관한 정보이다. 그 이유는 추후의 컴파일 과정에서 에러가 날 때 그 정보를 이용해서 error를 리포팅할 수 있도록 하기 위해서이다. 그렇다면 C preprocessing을 직접 해보자.
$ gcc -E -o hello.i hello.c 
결과로 hello.i라는 파일이 생긴다. 그 파일을 에디터로 열어보면 hello.c의 첫번째 줄에 있는 #include를 처리한 결과가 보일것이다.

(*) -E 옵션 
-E 옵션은 gcc의 컴파일 과정 중에서 C preprocessing까지만 처리하고 나머지 단계는 처리하지 말라는 것을 지시하는 것이다. 평소에는 별로 쓸모가 있는 옵션이 아니지만 다음과 같은 경우에 유용하게(?) 사용할 수 있다. 
 (1) C 언어 소스 코드가 복잡한 선택적 컴파일을 하고 있을 때, 그 선택적 컴파일이 어떻게 일어나고 있는지 알고 싶은 경우. 
 (2) preprocessing의 문제가 C 언어 에러로 나타날 경우. 다음과 같은 소스코드를 고려해 보자. 

#define max(x, y) ((x) 〉(y) ? (x) : (y) /* 마지막에 ")"가 없다!!! */ 
int myMax(int a, int b) 

  return max(a, b); 
}

$ gcc -c cpperr.c 
다음과 같은 에러가 난다.
cpperr.c: In function `myMax': 
cpperr.c:4: parse error before `;' 
cpperr.c파일의 4번째 줄에서 ';'가 나오기 전에 parse error가 났다. 하지만 실제 에러는 #define에 있었으므로 그것을 확인하려면 -E 옵션으로 preprocessing을 하여 살펴 보면 쉽게 알 수 있다. 

(*) 참고 : parse error before x(어떤 문자) 에러는 소스코드를 parsing 할 때 발생한 에러를 말한다. parsing이란 syntax analysis(구문해석) 과정인데 쉽게 말하면 C 언어 소스코드를 읽어들여 문법적 구성요소들을 분석하는 과정이라고 할 수 있다. 보통 gcc에서 parse error라고 하면 괄호가 맞지 않았거나 아니면 ';'를 빼먹거나 했을 때 발생한다. 보통의 경우 before x라고하여 x라는 것이 나오기 전에 parse error가 발생하였음을 알려주기 때문에 그 x가 나오기 전에 있는 C 소스 코드를 잘 살펴보면 문제를 해결할 수 있다.

C preprocessing의 문법과 나머지 C 언어의 문법과는 거의 관계가 없다. 관계가 있는 부분이 있다면 정의된 macro가 C 언어의 문법 상의 char string literal에는 치환되지 않는다는 정도이다. (좀 더 쉽게 이야기 하면 큰 따옴표 안에서는 macro 치환이 되지 않는다.) 또한 c preprocessing은 architecture dependent하지 않다. 즉, i386용으로 컴파일된 cpp를 다른 architecture에서 사용해도 무방하다. 조금 문제가 있을 수 있는 부분이 있다면 gcc의 predefined macro(i386의 경우 i386용 자동으로 define된다.)가 다를 수 있다는 점 뿐이다. 따라서 cpp를 C 언어 소스코드가 아닌 다른 부분에서 사용하는 경우도 있다. 대표적으로 assembly 소스 코드에서도 사용한다. assembler가 사용하고 있는 macro 문법이 c preprocessing의 macro문법 보다는 배우기 쉽기 때문이다.

이제 preprocessing이 하는 일에 대해서 좀더 알아 보자. 
== 파일 포함(file inclusion) 
#include 〈stdio.h〉
#include "config.h" 
위와 같이 많은 C 언어 소스코드에서 헤더 파일을 포함한다.〈〉와 ""의 차이는 기본적인 헤더파일과, 사용자 정의 헤더파일을 구분하는 정도이다. include한 헤더 파일은 default로 특정 디렉토리를 찾게 된다. Linux 시스템의 경우 /usr/include가 default 디렉토리이다. (실제로도 그곳에 stdio.h라는 파일이 있다.) 그 다음은 현재 디렉토리를 찾게 된다.(물론〈〉와 ""에 따라서 다르다.) 파일이 없으면 당연히 에러가 발생한다. gcc의 경우 다음과 같은 에러가 발생한다.
>>소스코드파일명:line number: 헤더파일명: No such file or directory 
또는(LANG=ko일때) 
>>소스코드파일명:line number: 헤더파일명: 그런 파일이나 디렉토리가 없음 

그렇다면 include하고 싶은 파일이 default 디렉토리와 현재 디렉토리에 없으면 어떻게 할까? 그런 문제를 해결하기 위해서 다음과 같은 옵션이 존재한다.

(*) -Idir 옵션 
여기서 dir은 디렉토리 이름이고 -I와 디렉토리 이름을 붙여 써야 한다. 그럼 include한 헤더 파일을 그 디렉토리에서도 찾아 주게 된다. 당연히 옵션을 여러 번 다른 디렉토리 이름으로 사용할 수도 있어서 헤더 파일을 찾을 디렉토리를 여러 개로 지정할 수 있다. 꼭 알아 두어야 할 옵션이다. 

(*) -nostdinc 
이 옵션은 default 디렉토리(standard include 디렉토리)를 찾지말라고 지시하는 옵션이다. 어플리케이션 프로그래머는 관심을 둘 필요가 없지만 kernel 프로그래머는 관심 있게 볼 수 있는 옵션이다. 

== macro 치환 
macro 치환에 대해서는 특별히 일어날 만한 에러는 없다. 가끔 문제가 되는 부분이 macro 정의가 한 줄을 넘어선 경우 역슬레쉬('')로 이어져야 하는데 그 소스 파일이 windows용 에디터로 편집 되었으면 parse error가 나는 경우가 있다. 그것은 개행문자(new line character)가 서로 달라서 그런 것인데...음 자세히 이야기하자면 끝이 없으므로 그냥 넘어가도록 해야한다. 또한 macro가 define된 상황에서 macro를 undef하지 않고 다시 define하면 다음과 같은 Warning이 난다. 
'xxxx' redefined 

macro 치환에서 대한 옵션 두개를 알아보도록 하자. 

(*) -Dmacro 또는 -Dmacro=defn 옵션 
gcc의 command line에서 macro를 define할 수 있도록 하는 옵션이다. 예를 들어 -D__KERNEL__이라는 옵션을 주면 컴파일 과정 중에 있는 C 언어 소스코드의 맨 처음에 #define __KERNEL__이라고 해준 것과 같이 동작한다. 또한 -DMAXLEN=255라고하면 C 언어 소스코드의 맨 처음에 #define MAXLEN 255 라고 한 것과 동일한 결과를 준다. 선택적 컴파일을 하는 경우에 많이 이용하는 옵션으로 꼭 알아야 할 옵션이다. 

(*) -Umacro 옵션 
이 옵션은 #undef하고 하는 일이 똑같은데 C 언어 소스코드와는 하등의 관계가 없다. -Dmacro옵션처럼 C 언어 소스코드의 맨처음에 #undef macro를 해주는 것은 아무런 의미가 없기 때문이다.(어짜피 #define은 그 이후에 나올 것이므로...) 이 옵션의 목적은 위의 -Dmacro옵션으로 define된 macro를 다시 undef하고자 할 때 쓰는 옵션이다. 평상시에는 별로 쓸 일이 없는 옵션이지만 그냥 -Dmacro와 같이 짝으로 알아 두길 바란다. 

== 선택적 컴파일 
#if 시리즈와 #else, #elif, #endif 등으로 선택적 컴파일을 수행할 수 있다. 위에서 설명한 -Dmacro 옵션과 같이 쓰는 경우가 많다. 특별히 설명할 옵션은 없고 #if와 #else, #endif의 짝이 잘 맞아야 한다. 그렇지 않으면 당연히 에러가 발생한다. 단순히 parse error라고 나오는 경우는 드물고, #else, #if 에 어쩌고 하는 에러가 난다. 많이 경우의 수가 있으므로 직접 에러가 발생되도록 코딩을 해보고 확인해 보는 것이 좋다. 

== 기타(#line, #error, #pragma) 
#line, #error, #pragma라는 것이 있는지도 모르는 사람들이 꽤 있것이다. 자세한 것은 C 언어 문법 책을 찾아보길 바란다. #line의 경우 C 언어 소스코드 직접 쓰이는 경우는 거의 없으니까 무시하고 #pragma는 compiler에 dependent하고 gcc에서 어떤 #pragma를 사용하는지도 알 수 없으므로 그냥 넘어가도록 한다. #error의 경우 C preprocessing 과정에서 강제로 에러를 만드는 지시어이다. 선택적 컴파일 과정에서 도저히 선택되어서는 안 되는 부분에 간혹 쓰인다. 당연히 #error를 만나면 에러가 생긴다. linux kernel 소스코드에서 include 디렉토리를 뒤져 보시면 사용하는 예를 만날 수 있다. 

== predefined macro 
사용자가 C 언어 소스코드에서 #define을 하지 않아도 이미 #define된 macro가 있다. ANSI C에서는 __LINE__, __FILE__, __TIME__, __DATE__, __STDC__ 다섯 가지는 이미 define되어 있는 macro로 강제적인 사항이다.(문법책 참조) gcc도 당연히 다섯 가지 macro를 predefine 한다. 뿐만 아니라 GCC의 버전에 대한 macro, architecture에 관한 사항 등을 -Dmacro 옵션 없이도 predefine 한다. -v 옵션을 실행하여 출력되는 specs파일을 열어보면 쉽게 알 수 있을 것이다.(specs파일이 어떻게 해석되는지는 나도 잘 모른다.) 

== 꼭 알아두면 좋은 옵션 한가지 
다음과 같이 shell 상에 입력해 보라.(hello.c는 계속되는 그 녀석이다.) 
$ gcc -M hello.c
어떤 것이 출력되나? "hello.o: hello.c /usr/include/stdio.h 어쩌구저쩌구"가 출력될 것이다. 어디서 많이 본 듯한 형식 아닌가? 

(*) -M 옵션 
-M 옵션은 cpp에게 makefile로 만들 수 있는 rule을 만들어 달라고 하는 요청을 보내는 명령이다. file을 include하는 녀석은 cpp이므로 rule은 cpp가 만들 수 있다. 당연히 -Dmacro, -Umacro, -Idir 옵션 등을 같이 사용할 수 있고 그에 따라 결과가 달라질 수도 있다. makefile을 좀 쉽고 정확하게 만들 때 쓰는 옵션이므로 알아두면 좋다. 단점은 default 디렉토리에 있는 보통 사용자는 고칠 수도 없는 파일까지도 만들어 준다는 것이다. 

=== C 언어 컴파일 과정 
C 언어 컴파일 과정은 gcc라고 하는 frontend가 cc1이라는 다른 실행파일을 호출(fork & exec)하여 수행하게 된다. 사용자가 cc1이라는 실행파일을 직접 실행해야 할 하등의 이유도 없고 권장되지도 않는다. gcc의 입력으로 여러 개의 파일(C 소스 파일, object 파일 등)을 준다고 하더라도 컴파일 과정 중 앞 3단계, 즉 cpp, C 컴파일, assemble은 각각의 파일 단위로 수행된다. 서로 다른 파일의 영향을 받지 않고 진행된다. 특정 C소스 코드에서 #define된 macro가 다른 파일에는 당연히 반영되면 안된다. header 파일의 존재 의미를 거기서 찾을 수 있다. 

이제 C 언어 컴파일 과정이 하는 일을 알아보도록 하자. 

== C 언어 컴파일 과정이 하는 일 
 (1) 입력 : C 언어 소스 코드(C preprocessing된) 
 (2) 출력 : Assembly 소스 코드 
 (3) 하는 일 : 컴파일(너무 간단한가?) 

C preprocessing과 마찬가지로 너무 간단하다. 하지만 위의 “컴파일” 과정은 cc1 내부에서는 여러 단계로 나누어져 다음과 같은 순서로 일어난다. Parsing(syntax analysis)이라고 하여 C 언어 소스 코드를 파일로부터 읽어 들여 컴파일러(여기서는 cc1)가 이해하기 쉬운 구조로 바꾸게 된다. 그 다음에 그 구조를 컴파일러가 중간 형태 언어(Intermediate Language)라고 하는 다른 언어로 변환하고 그 중간 형태 언어에 여러가지 최적화를 수행하여 최종 assembly 소스 코드를 만들게 된다. 

직접 수행해 보자. 다음과 같이 shell의 command line에 입력하라. 역시 지긋지긋한 hello.c를 이용하도록 한다.
$ gcc -S hello.c 
결과로 출력된 hello.s를 에디터로 열어서 살펴보라 (혹시 위의 command로 hello.s가 만들어 지지 않는다면 gcc -S -o hello.s hello.c로 하라.). “.”으로 시작하는 assembler directive와 “:”로 끝나는 label명, 그리고 몇 개의 assembly mnemonic이 보이나? Assembly 소스를 읽을 줄 몰라도 그게 assembly 소스 코드구나 생각하면 된다. 

(*) -S 옵션 
-S 옵션은 gcc의 컴파일 과정 중에서 C 언어 컴파일 과정까지만 처리하고 나머지 단계는 처리하지 말라는 것을 지시하는 것이다. 평소에는 거의 사용하지 않는 옵션이지만 다음과 같은 경우에 유용하게 사용할 수 있다. 
 (1) 어셈블리 코드가 어떻게 생겼는지 보고 싶은 호기심이 발동한 경우
 (2) C calling convention을 알아보거나 stack frame이 어떻게 관리되고 있는 지 보고 싶은 경우 

보통의 경우는 아니지만 사용자가 직접 assembly 코딩을 하는 경우가 종종 있다. 아무래도 사람이 기계보다는 훨씬 똑똑하기 때문에 사람이 직접 assembly 코딩을 해서 최적화를 시도하여 소프트웨어의 수행 시간을 단축시키거나, 아니면 linux kernel이나 bootloader 등과 같이 꼭 assembly가 필요한 경우가 있다. 이때도 보통의 경우는 소프트웨어의 전 부분을 assembly 코딩하는 것이 아니라 특정 부분만 assembly 코딩을 하고 나머지는 C 언어나 다른 high-level 프로그래밍 언어를 써서 서로 연동을 하도록 한다. 그럼 C 언어에서 assembly 코딩된 함수를 호출할 때(반대의 경우도 마찬가지), 함수의 argument는 어떻게 전달되는 지, 함수의 return 값은 어떻게 돌려지는지 등을 알아볼 필요가 있다. 이렇게 argument와 return 값의 전달은 CPU architecture마다 다르고 그것을 일정한 약속(convention)에 따라서 처리해 주게 된다. 위의 hello.s를 i386용 gcc로 만들었다면 파일 중간에 xorl %eax,%eax라는 것이 보일 것이다. 자기 자신과 exclusive or를 수행하면 0(zero)이 되는데 이것이 바로 hello.c에서 return 0를 assembly 코드로 바꾼 것이다. 결국 i386 gcc에서는 %eax 레지스터에 return 값을 넣는다는 convention이 있는 것이다.(실제로는 gcc뿐 아니라 i386의 convention으로 convention을 따르는 모든 compiler가 %eax 레지스터를 이용하여 return값을 되돌린다.) argument의 경우도 test용 C 소스를 만들어서 살펴볼 수 있을 것이다. 물론 해당 CPU architecture의 assembly 소스코드를 어느 정도 읽을 수 있는 사람들에게만 해당하는 이야기 이다. stack frame도 비슷한 얘기 쯤으로 알아 두길 바란다. 

== Parsing(Syntax Analysis) 
위에서 cc1이 컴파일을 수행하는 과정 중에 맨 첫 과정으로 나온 Parsing에 대해서는 좀더 언급을 한다. Parsing과정은 그야말로 구문(Syntax)을 분석(Analysis)하는 과정이다. Parsing의 과정은 파일의 선두에서 뒤쪽으로 한번 읽어 가며 수행된다. Parsing 중에 컴파일러는 구문의 에러를 찾는 일과 뒤에 수행될 과정을 위해서 C 언어 소스 코드를 내부적으로 다루기 쉬운 형태(보통은 tree형식을 이용)로 가공하는 일을 수행한다. 이 중에 구문의 에러를 찾는 과정은 (1) 괄호 열고 닫기, 세미콜론(;) 기타 등등의 문법 사항을 체크하는 것 뿐만 아니라, (2) identifier(쉽게 말해 변수나 함수 이름 들)의 type을 체크해야 한다. 
 (1) 괄호 열고 닫기, 세미콜론(;) 기타 등등의 문법 사항에 문제가 생겼을 때 발생할 수 있는 에러가 전에 이야기한 parse error이다. 보통 다음과 같이 발생한다. 
>> 파일명과 line number: parse error before x 
당연히 에러를 없애려면 ‘x’ 앞 부분에서 괄호, 세미콜론(;) 등을 눈 빠지게 보면서 에러를 찾아 없애야 한다. 
 (2) type checking 
구문 에러를 살필 때 type 체크를 왜 해야 할까? 다음과 같은 예를 보자. 
var3 = var1 + var2; 
앞 뒤에서 parse error가 없다면 위의 C 언어 expression은 문법적인 문제가 없는가? 하지만 var1이 파일의 앞에서 다음과 같이 정의(definition)되었다면 어떻게 될까? 
struct point { int x; int y; } var1; 
당연히 ‘+’ 연산을 수행할 수 없다.(C 언어 문법상) 결국은 에러가 난다. 이렇게 identifier(여기서는 var1, var2, var3)들의 type을 체크하지 않고서는 구문의 에러를 모두 찾아낼 수 없다. 
만약 var1과 var3가 파일의 앞에서 int var1, var3;로 정의되어 있고 var2가 파일의 앞에 어떠한 선언(declaration)도 없이 파일의 뒤에서 int var2;로 정의되어 있다면 에러가 발생할까? 정답은 “발생한다”이다. 위에서 언급했듯이 Parsing은 파일의 선두에서 뒤쪽으로 한번만(!!!) 읽으면서 진행하기 때문이다.(모든 C 컴파일러가 그렇게 동작할지는 의심스럽지만 ANSI C 표준에서는 그렇게 되어 있는 것으로 알고 있다. Assembler는 다르다.) 
그렇다면 어떤 identifier를 사용하려면 반드시 파일 중에 사용하는 곳 전에 identifier의 선언(declaration) 또는 정의(definition)가 있어야 한다. 하지만 identifier가 함수 이름일 경우(즉 identifier뒤에 (…)가 올 경우)는 조금 다르다. C 컴파일러는 함수 이름 identifier의 경우는 int를 return한다고 가정하고 Error가 아닌 Warning만 출력한다.(Warning옵션에 따라 Warning조차 출력되지 않을 때도 있다.) 그럼 다음과 같은 소스 코드를 생각해 보자. 
int var3, var2; 
…. 
var3 = var1() + var2; 
…. 
struct point var1(void) { … } 
위와 같은 경우도 문제가 생긴다. 맨 처음 var1이라는 함수 이름 identifier를 만났을 때 var1 함수는 int를 return한다고 가정했는데 실제로는 struct point를 return하므로 에러 또는 경고를 발생한다. 
결국 권장하는 것은 모든 identifier는 사용하기 전(파일 위치상)에 선언이나 정의를 해 주는 것이다. 다음과 같은 에러 메시지들을 짧막하게 설명해 본다.
 파일명 line number: ‘x’ undeclared …. 에러 --> ‘x’라는 이름의 identifier가 선언되어 있지 않았다.
 파일명 line number: warning: implicit declaration of function `x' … 경고 --> ‘x’라는 이름의 함수가 선언되어 있지 않아 int를 return한다고 가정했다는 경고(Warning) 메시지이다. 

변수나 함수의 선언(declaration)과 정의(definition)에 대해서 알지 못한다면 C 언어 문법책을 찾아서 숙지하길 바란다. 그런 내용이 없다면 그 문법책을 휴지통에 버리길 바란다. 

Parsing 과정에는 위의 identifier 에러 및 경고를 비롯한 수많은 종류의 에러와 경고 등이 출력될 수 있다. 에러는 당연히 잡아야 하고 경고도 무시하지 않고 찾아서 없애는 것이 좋은 코딩 습관이라고 할 수 있다. 경고 메시지에 대한 gcc 옵션을 살펴보도록 하자. 

(*) -W로 시작하는 거의 모든 옵션 
이 옵션들은 어떤 상황 속에서 경고 메시지를 내거나 내지 말라고 하는 옵션이다. -W로 시작하는 가장 강력한 옵션은 -Wall 옵션으로 모든 경고 메시지를 출력하도록 한다. 보통은 -Wall 옵션을 주고 컴파일 하는 것이 좋은 코딩 습관이다. 

== Parsing 이후 과정 
특별한 경우가 아닌 이상 Parsing을 정상적으로 error나 warning없이 통과한 C 소스 코드는 문법적으로 완벽하다고 봐야 한다. 물론 논리적인 버그는 있을 수 있지만 이후 linking이 되기 전까지의 과정에서 특별한 error나 warning이 나면 안된다. 그런 경우가 있다면 이제는 사용자의 잘못이 아니라 gcc의 문제로 추정해도 무방하다. Parsing이후에 assembly 소스 코드가 생성되는데, 당연히 이 과정에는 특별히 언급할 만한 error나 warning은 없다. 그냥 중요한 옵션 몇 가지만 집고 넘어가도록 하겠다. 

(*) -O, -O2, -O3 등의 옵션 
이 옵션은 컴파일러 최적화를 수행하라는 옵션이다. -O 뒤의 숫자가 올라갈수록 더욱 많은 종류의 최적화를 수행하게 된다. 최적화를 수행하면 당연히 코드 사이즈도 줄어 들고 속도도 빨라지게 된다. 대신 컴파일 수행 시간은 길어진다. 그리고 linux kernel을 위해 언급하고 싶은 것은 inline 함수들은 이 옵션을 주어야 제대로 inline 된다는 것이다. 

(*) -g 옵션 
이 옵션은 소스 레벨 debugger인 gdb를 사용하기 위해 debugging 정보(파일명, line number, 변수와 함수 이름들과 type 등)를 assembly code와 같이 생성하라는 옵션이다. 당연히 gdb를 이용하고 싶으면 주어야 한다. -g 옵션을 주지 않고 컴파일한 프로그램을 gdb로 디버깅하면 C 소스 레벨이 아닌 assembly 레벨 디버깅이 된다. 즉 C 소스 코드에 존재하는 변수 이름, line number 등이 없는 상황에서 디버깅을 해야 한다. 또한 -g 옵션을 -O 옵션과 같이 사용할 수도 있다. 단 그런 경우 최적화 결과, C 소스 코드에 존재하는 심볼(symbol; 쉽게 말해 함수와 변수 이름)중에 없어지는 것들이 발생한다. 

(*) 여기서 잠깐 
identifier와 symbol이 모두 “쉽게 말해 함수와 변수 이름”이라고 했는데 어떻게 차이가 날까? 엄밀히 말하면 차이가 조금 있다. symbol이 바로 “쉽게 말해 함수와 변수 이름”이며 각 symbol은 특정 type과 연계되어 있다. 하지만 identifier는 그냥 “이름” 또는 “인식어”일 뿐이다. 예를 들어 struct point { int x; int y; };라는 것이 있을 때 point는 symbol은 아니지만 identifier이다. 보통 identifier라는 말은 parsing에서만 쓰인다는 정도만 알아두면 된다.

(*) -p 옵션과 -pg 옵션 
profiling을 아는가? 수행시간이 매우 중요한 프로그램(real time 프로그램이라고 해도 무방할 듯)을 작성할 때는 프로그램의 수행 시간을 함수 단위로 알아야 할 필요가 있는 경우가 많다. 프로그램의 수행 시간을 함수 단위나 더 작은 단위로 알아보는 과정을 profiling이라고 하는데, profiling은 프로그램 최적화에 있어서 중요한 기능을 담당한다. 대부분의 개발 툴이 지원하고 Visual C++에도 존재한다. 옛날 turbo C에는 있었나? 아무튼 gcc도 역시 profiling을 지원한다. -p 옵션 또는 -pg 옵션을 주면 프로그램의 수행 결과를 특정 파일에 저장하는 코드를 생성해 주게 된다. 그 특정 파일을 적당한 툴(prof또는 gprof 등)로 분석하면 profiling 결과를 알 수 있게 해 준다. 당연히 linux kernel 등에서는 사용할 수 없다.(이유는 특정 파일에 저장이 안되므로…) 초보자들은 이런 옵션도 존재하고 profiling을 할 수 있다는 정도만 알아 두면 좋을 듯 싶다. 나중에 필요하면 좀 더 공부해서 사용하면 된다.

(*) 기타 옵션(-m과 -f시리즈) 
중요한 옵션들이기는 하지만 초보자가 알아둘 필요가 없는 옵션 중에 f또는 m으로 시작하는 옵션들이 있다. f로 시작되는 옵션은 여러 가지 최적화와 assembly 코드 생성에 영향을 주는 architecture independent한 옵션이다.(assembly 코드 생성이 architecture dependent 이므로 정확히 말하면 f로 시작되는 옵션이 architecture independent라고 할 수는 없다.) m으로 시작되는 옵션은 보통 architecture dependent 하며 주로 CPU의 종류를 결정하는 옵션으로 assembly 코드 생성에 영향을 주게 된다. 하지만 대부분은 초보자는 그런 것이 있다는 정도만 알아두면 되고 특별히 신경 쓸 필요는 없다고 생각된다. m으로 시작되는 옵션 중에 -msoft-float옵션이 있다.(물론 특정 architecture에만 존재하는 옵션이다.) -msoft-float 옵션은 CPU에 FPU(floating point unit)가 없고, kernel에서 floating-point emulation을 해 주지 않을 때 C 소스 코드 상에 있는 모든 floating-point 연산을 특정 함수 호출로 대신 처리하도록 assembly 코드를 생성하라고 지시하는 옵션이다. 이 옵션을 주고 라이브러리를 linking시키면 FPU가 없는 CPU에서도 floating 연산을 할 수 있다.(대신 엄청 느리다. 어찌보면 kernel floating-point emulation보다는 빠를 것 같은데 확실하지는 않다.) 

=== Assemble 과정 
Assemble 과정은 앞선 과정과 동일하게 gcc라는 frontend가 as라는 실행 파일을 호출하여 수행된다. 그런데 as는 cpp와 cc1과는 달리 gcc 패키지 안에 존재하는 것이 아니라 별도의 binutils라고 하는 패키지에 존재한다. binutils 패키지 안에는 as를 비롯해 linking을 수행하는 ld, library 파일을 만드는 ar, object 파일을 보거나 복사할 수 있는 objdump, objcopy 등 여러 가지 툴이 들어 있다. 

이제 Assemble 과정이 하는 일을 알아보도록 하자. 

== Assemble 과정이 하는 일 
 (1) 입력 : Assembly 소스 코드 
 (2) 출력 : relocatable object 코드 
 (3) 하는 일 : assemble(너무 간단한가?) 

입력은 당연히 C 언어 컴파일 과정을 거치면 나오는 Assembly 소스 코드이다. Assemble 과정을 거치면 소위 기계어(machine language)라는 결과가 relocatable object 형식으로 나온다. “relocatable”이라는 말이 어려우면 그냥 object 코드라고 해 두자. 이제 직접 수행해자. shell의 command line에 다음과 같이 입력하면 된다.
$ gcc -c hello.c 
결과는 hello.o라고 하는 파일이 나온다. hello.o는 binary형식의 파일이니깐 editor로 열어봐야 정보를 얻기 힘들다. 당연히 위의 예는 assemble 과정만 수행한 것이 아니라 C preprocessing 과정, C 언어 컴파일 과정, Assemble 과정을 수행했다. Assemble 과정만 수행하고 싶으면 다음과 같이 입력하면 된다. 
$ gcc -c hello.s 
역시 hello.o가 생긴다. hello.s는 C 언어 컴파일 과정에서 -S 옵션으로 만들었던 그 파일이다. 별로 관심이 안 생기면 as를 직접 수행할 수도 있다. 다음과 같다. 
$ as -o hello.o hello.s 
역시 hello.o가 생긴다.

(*) -c 옵션 
많이 쓰는 옵션이다. Assemble 과정까지의 과정만 수행하고 linking 과정을 수행하지 말라는 옵션이다. 여러 개의 C 소스 파일로 이루어진 프로그램을 컴파일 할 때 모든 소스 파일을 assemble 과정까지 수행하고 맨 마지막에 linking한다. 보통은 Makefile을 많이 이용하는데 그 때 많이 쓰이는 옵션이다. 

Assemble 과정에서는 더 이상 기억해야 하는 옵션도 없고 이게 끝이다. C 언어 컴파일 과정에서 말한 바대로 C 언어 컴파일 과정이 끝난 C 소스 파일은 문법적으로 완전하다고 볼 수 있으므로 assemble 과정에서 Error나 Warning 나는 경우는 없다. 만약 Error나 Warning이 나는 경우가 있다면 gcc의 inline assemble을 이용했을 때, 그 inline assemble 소스 코드에 있는 문제 때문에 생길 수 있다. 안타깝지만 error나 warning 메시지가 나온 다면 C 소스 파일과 line number 정보는 없다. 잘 알아서 처리하는 수 밖에 다른 방법은 없는 것 같다. inline assemble 같은 것을 사용하지 않았는데도 error나 warning이 난다면 gcc의 버그라고 생각해도 무방하다. 

== relocatable object 코드 파일 내용 
어떤 정보가 object 파일 안에 들어있을까? 당연히 code와 data가 들어 있다. C 컴파일 과정에서 C 언어 함수 안에 있는 내용들이 assembly mnemonic 들로 바뀌었고 그것이 assemble되어 기계어(machine language)가 되었을 것이다. 그 부분이 code를 이룬다. C 언어 소스 코드에 있는 나머지는 전역 변수(external variable)와 정적 변수(static variable)들이 data를 이룰 것이다. 또한 문자열 상수를 비롯한 상수도 data에 들어 있다. 또한 프로그램 수행에 쓰이지는 않고 단순한 정보로서 들어 있는 data들도 있다. 예를 들어 -g 옵션을 주고 컴파일 하면 프로그램의 디버깅 정보(변수, 함수 이름, C 소스 파일이름, line number 등)가 data에 속한다고 볼 수 있다. 그런데 code와 data가 무질서하게 섞여 있는 것은 아니고 section이라고 불리우는 단위로 서로 구분되어 저장되어 있다. Code는 text section에 들어 있고, data는 성격에 따라 data section, bss section, rodata section 등에 나누어져 저장되어 있다.(text, data, bss, rodata 등의 section 이름은 그냥 관습적인 것이다.) 아무튼 section 이야기는 이 정도만 우선 알아두면 될 듯 싶다.

== Symbol 이야기
relocatable object code안에 code와 data가 들어 있다고 했는데, 아주 중요한 것을 빠뜨렸다. 이 이야기는 linking 과정을 이해하기 위해 꼭 필요한 부분이므로 반드시 읽어야 할 것이다. 우선 Symbol이 무엇인지 알 것이다. C 언어 컴파일 과정에서 identifier와 함께 설명했는데 잠시 다시 말씀하자면 Symbol은 함수와 변수 이름이다. 변수 중에 특히 관심두어야 할 것 들은 자동 변수(?,auto variable)들이 아닌 전역 변수(external variable)와 정적 변수(static variable)이다. 자동 변수는 함수의 stack frame에 존재하는 변수이기 때문에 현재 stack pointer(sp, 보통의 CPU의 register중에 하나)에 대한 offset으로 표현된다. 즉 현재 함수에서 자동 변수(auto variable)를 access(read/write)하고 싶으면 sp+상수의 어드레스를 access하면 된다. 하지만 전역 변수와 정적 변수는 그냥 32bit(32bit CPU기준) 어드레스를 읽어야 한다. stack pointer랑은 전혀 관계 없다. 아무튼 여기서 관심을 두는 Symbol은 함수, 전역 변수와 정적 변수의 이름이라고 할 수 있다. 
이제 생각해 볼 것은 C 언어 소스 파일을 C preprocessing, C 언어 컴파일, assemble 과정을 거치면 완전한 기계어로 바꿀 수 있느냐 하는 점이다. 완전히 기계어로 바꿀 수 있을까? C 언어 소스 파일 하나로 이루어지는 프로그램이라면 완전히 기계어로 바꾸는 것이 가능하겠지만 일반적으로는 불가능 하다. 다음과 같은 예제를 살펴보자. 
int func3(void); /* func3 선언 */ 
extern int mydata; /* mydata 선언 */ 

int func2(void) /* func2 정의 */ 

…. 


int func1(void) /* func1 정의 */ 

int i; 
….. 
func2(); 
….. 
func3(); 
…. 
i= mydata+3; 
….. 

-- end of test1.c 
-- start of test2.c 
int mydata = 3; /* mydata 정의 */ 
int func3(void) /* func3 정의 */ 

….. 
}

위의 예제를 컴파일 한다고 생각해보자. test1.c에서 func1()의 내용을 기계어로 바꾸고 싶은데 func2()를 호출하는 시점에서는 별로 문제가 안된다. func2()는 같은 소스 코드 내에 존재하고 func2()를 호출하는 instruction과 func2()의 실제 위치(어드레스)의 차이를 계산해 낼 수 있으므로 상대 어드레스를 이용하는 함수 호출 instruction으로 완전히 기계어로 바꿀 수 있다. 그런데 문제는 func3()를 호출할 때는 func3()의 실제 위치(address)를 계산할 수 없다는 문제점이 있다. 당연히 동일한 파일에 존재하는 함수가 아니므로 그 함수가 존재하게 될 어드레스를 계산할 수 없다. 어드레스를 모르는데 함수 호출 instruction을 완전히 만들 수 있을까? 만들 수 없다. 당연히 전역 변수 mydata를 access하는 부분도 마찬가지로 mydata의 어드레스를 모르므로 완전히 instruction으로 바꿀 수 없다. 그럼 어떻게 해야 될까? 
그때 assembler는 그냥 함수 어드레스 없는 함수 호출 instruction을 기계어로 바꾸어 놓는다. 그런 다음에 그 instruction에 “func3()를 호출한다”라는 표지를 붙여 놓는다. 그럼 그 후의 과정(linking)에서 func3()의 address를 계산했을 때 그 빈 공간을 채워 넣게 된다. mydata와 같은 전역 변수도 마찬가지로 동작한다. test1.c을 컴파일할 때는 “func3()”, “mydata” 라는 표지를 사용해야 한다. 그럼 test2.c를 컴파일 할 때는 무엇이 필요할까? 상식적으로 생각하면 “func3()”, “mydata”가 여기 있다라는 정보를 가지고 있어야한다. 
정리하면 object 파일 안에는 그 object 파일에 들어있는 symbol들(test1.o에서는 func1과 func2, test2.o에서는 func3와 mydata)에 대한 정보가 들어있고, 그 object 파일이 reference하고 있는 symbol들(test1.o에서 func3와 mydata 사용)에 대한 정보가 들어 있다.

== Relocatable의 의미 
위에서 object 코드라고 하지 않고 relocatable object 코드라고 지칭했는데 relocatable이 뜻하는 것을 잠시 집고 넘어 가자. Relocatable을 사전에서 찾아보면 “재배치가 가능한” 정도의 뜻이다. “재배치가 가능한” 이라는 의미는 상당히 모호하다. 좀 더 구체적으로 말하자면 위에서 설명된 symbol들의 절대 어드레스가 정해지지 않았다는 뜻이다. 즉 test1.c의 func1()이 절대 어드레스 0x80000000에 존재해야 한다라고 정해지지 않고 어떤 절대 어드레스에 존재해도 관계 없다는 뜻이다. 그런데 이 말과 헷갈리는 말이 한가지 더 있는데 그것은 position independent code이다. C 언어 컴파일 과정에서 설명한 옵션중에 -f 시리즈가 있었다. 그 중에 -fpic라는 position independent code를 만들라고 강제하는 옵션이 있다. position independent code도 역시 절대 어드레스상에 어느 위치에 있어도 무방한 code를 지칭한다. 하지만 두 가지는 분명 차이가 있는데, 그냥 넘어가기로 하자. 쉽게 relocatable은 절대 어드레스가 결정되지 않았다는 뜻, 그러나 position independent code와는 다른 말이다.

=== Linking 과정 
Linking 과정은 ld라고 하는 실행파일이 담당하고 있다. Assemble을 담당하는 as와 마찬가지로 binutils 패키지의 일부분이다. 보통 어플리케이션을 컴파일하는 경우에는 gcc(실행파일)를 이용하여 ld를 호출하나, 특별한 경우에 있어서는 ld를 직접 수행하여 linking을 하는 경우가 종종 있다. 

== Linking 과정이 하는 일 
 (1) 입력 : 하나 이상의 relocatable object 코드 와 library 
 (2) 출력 : 실행파일(executable) 또는 relocatable object 코드 
 (3) 하는 일 : symbol reference resolving & location 
Linking 과정은 하나 또는 그 이상의 object 파일과 그에 따른 library를 입력으로 받는다. 출력은 보통의 경우는 실행파일(executable file)이지만, 경우에 따라서 object 파일을 생성하게 할 수도 있다. 여러 개의 object 파일을 합쳐서 하나의 object 파일로 만드는 과정을 partial linking이라고 부르기도 한다. Linking 과정이 하는 일은 symbol reference resolving하고 location이라고 했는데, 저도 정확한 단어를 적은 것인지 의심스럽다. 정확한 용어를 사용한다면 좋겠지만 그렇지 못하더라도 내용을 정확히 이해하는 것이 중요하니깐 내용에 대해서 살펴보도록 하겠다. 

== symbol reference resolving 
어떤 C 소스 파일에서 다른 파일에 있는 함수와 전역 변수(symbol)에 대한 참조(reference)를 하고 있다면 assemble 과정에서 완전한 기계어로 바꿀 수 없다.(실제로는 같은 소스 파일에 있는 전역 변수를 참조하는 것도 보통의 경우, 완전한 기계어로 바꿀 수 없다.) 그 이유는 당연히 assemble 까지의 과정은 단일 파일에 대해서만 진행되고, 다른 파일에 있는 해당 함수와 전역 변수의 address가 상대적이든 절대적이든 결정될 수 없기 때문이다. 따라서 완전히 기계어로 바꿀 수 없는 부분은 그대로 “공란”으로 남겨두고 표시만 해 두게 된다. 
Linking 과정에서 그 “공란”을 채워 넣게 된다. 그 과정을 보통 “resolve한다”라고 말한다. 어떻게 할까? 당연히 실행 파일을 이루는 모든 object 파일을 입력으로 받기 때문에 object 파일들을 차곡 차곡 쌓아 나가면(아래 location 참조) object 파일 안에 있는 모든 symbol(함수나 전역 변수 이름)의 address를 상대적이든 절대적이든 계산할 수 있다. 이제 각 symbol의 address가 계산되었으므로 표시가 남아 있는 “공란”에 해당하는 symbol의 address를 잘 넣어주면 된다. 
linking 과정에서 나올 수 있는 에러는 대부분 여기에서 발생한다. 표시가 남아 있는 “공란”을 채울 수 없는 경우가 있다. 크게 두 가지로 나누어지는데, 우선 reference하고 있는 symbol을 찾을 수 없는 경우와 reference하고 있는 symbol의 정의가 여러 군데에 있는 경우이다. 

>> object파일명: In function ‘func’: 
>> object파일명: undefined reference to ‘symbolname’ 
위의 에러 메시지는 함수 func 안에서 사용되고 있는 symbolname이란 이름의 symbol이 어디에도 정의되지 않아서 “공란”을 채울 수 없다는 뜻이다. 당연히 symbolname을 잘못 입력하였던지 아니면 그 symbol이 속해있는 object 파일이나 library와 linking되지 않았기 때문이다. 

>> object파일명1: multiple definition of ‘symbolname’ 
>> object파일명2: first defined here 
위의 에러 메시지는 symbolname이란 이름의 symbol이 여러 번 정의되고 있다는 뜻이다. object파일1에서 정의가 있는데 이미 object파일2에서 정의된 symbol이므로 그 symbol을 reference하고 있는 곳에서 정확하게 “공란”을 채울 수 없다는 뜻이다. 당연히 두 symbol중에 하나는 없애거나 static으로 바꾸거나 해야 해결될 것이다. 

== location(용어 정확하지 않을 수 있음) 
이전 까지 object 코드를 모두 relocatable이라고 표현했다. 아직 절대 address가 결정되지 않았다는 의미로 사용된다.(position independent code와는 다른 의미) object 코드의 절대 address를 결정하는 과정이 “location”이다. Symbol reference resolving 과정에서 입력으로 받은 모든 object 파일들을 차곡 차곡 쌓아 나간다고 했다. 그런데 object 파일이 무슨 벽돌도 아닌데 차곡 차곡 쌓는 다는 것이 말이 되나? 여기서 쌓는 다는 말을 이해하기 위해서 다음과 같은 그림(?)을 살펴 보도록 하자.

많은 object code들 
----------------- address(0xAAAAAAAA+0x5000) 
test2.o(size 0x3000) 
----------------- address(0xAAAAAAAA+0x2000) 
test1.o(size 0x2000) 
----------------- address(0xAAAAAAAA) 

절대 address 0xAAAAAAAA에 test1.o의 내용을 가져다 놓는다. test1.o의 크기(파일 크기와는 의미가 조금 다르지만 그냥 무시하고 파일 크기라고 생각하기 바람)가 0x2000이므로 다음에 test2.o를 쌓을 수 있는 address는 0xAAAAAAAA+0x2000가 된다. 그곳에 다시 test2.o를 쌓고 또 test2.o의 크기를 보고 새로운 address 계산하고 또 object 코드 쌓고, 계속 반복이다. 이렇게 쌓을 때 초기 절대 address 0xAAAAAAAA가 무슨 값을 가지게 되면 모든 object 파일에 있는 symbol의 절대 address도 계산해 나갈 수 있을 것이다. 그걸로 symbol reference를 resolve하게 된다. 그 초기 절대 address 0xAAAAAAAA의 값을 정하는 것을 location이라고 한다. 그럼 왜 절대 address를 결정해야 할까? 꼭 그래야 할 필요는 없지만 CPU의 instruction이 대부분의 경우 절대 address를 필요로 하는 경우가 많기 때문이라고 할 수 있다. 
(주의) object 를 쌓는 것은 위의 예처럼 단순하지는 않다. 실제로는 object 전체를 쌓지 않고 object안에 있는 section별로 쌓게 된다.

그럼 이제 직접 수행해 보자.
$ gcc -o hello hello.o 
object 파일이 하나라서 너무 단순하다고 생각하는가? 물론 hello.o 하나만 command line에 나타나지만 실제로는 조금 많은 object 파일이 linking되고 있다. (아래에서 좀더 자세하게 설명한다.) 지겹지만 hello를 실행해 보라. 제대로 동작하는가? 제대로 동작한다면 그 사이 어떤 일이 벌어졌을까? 그 사이에 벌어진 일을 간단히 적어보면 다음과 같다. shell이 fork() 시스템콜을 호출하고 자식 process는 exec() 시스템콜을 통해 hello라는 파일 이름을 kernel에 넘긴다. kernel에서는 hello파일을 보고 linking할 때 location된 address(여기서는 absolute virtual address 이다.)상의 메모리로 hello 파일을 복사하고 PC(program counter)값을 바꾸면 수행되기 시작한다. 
(주의) 실제로 위의 hello가 수행되는 과정은 많은 생략과 누락이 있다. 실제로는 hello 파일을 완전히 메모리로 복사하는 것도 아니고, dynamic linking & loading 등의 개념이 완전히 빠져 있지만 그냥 이해하기 쉽게 하기 위해서 간단하게 적어 본 것이다. 

= library 
hello.o를 linking하여 hello라고 하는 실행파일을 만드는데 command line에서는 아무것도 없지만 library가 같이 linking되고 있다. 그것은 지극히 당연하다. hello.c의 main함수에서 printf함수를 호출(linking이니깐 참조 혹은 reference라고 해야 좋겠다.)하고 있는데 printf함수 자체는 소스 중에 그 어디에도 없다.(물론 stdio.h에 printf함수의 선언은 있습니다만 정의는 어디에도 없다.) 잘 알다시피 printf함수는 C standard library 안에 있는 함수이다. C standard library가 같이 linking되었기 때문에 제대로 동작하는 hello 실행파일이 생긴 것이다. 
library라는 것은 아주 간단한 것이다. relocatable object 파일들을 모아 놓은 파일이다. 소스로 제공할 수도 있으나, 그러면 매번 cpp, c 컴파일, assemble 과정을 거쳐야 하므로 컴파일 시간이 매우 증가하게 된다. 그래서 그냥 relocatable object 파일로 제공하는 것이 컴파일 시간 단축을 위해서 좋다. 그런데 필요한 relocatable object 파일이 너무 많으면 귀찮으니까 그것을 묶어서 저장해 놓은 녀석이 바로 library라고 할 수 있다. 
Linux를 비롯한 unix 계열에서는 대부분의 library 파일의 이름이 lib로 시작된다. 확장자는 두 가지가 있는데, 하나는 .a이고 또 하나는 .so입니다.(뒤에 library 버전 번호가 붙는 경우가 많다.) .a로 끝나는 library를 보통 archive형식의 library라고 말하며 .so로 끝나는 library를 보통 shared object라고 부른다. /lib 디렉토리와 /usr/lib 디렉토리에 가면 많이 볼 수 있다. 
archive library 안에 있는 symbol를 reference하게 되면 library중에 해당 부분(object 파일 단위)을 실행 파일 안에 포함시켜 linking을 수행한다. 즉, 해당 object 파일을 가지고 linking을 수행하는 것과 동일한 결과를 가진다. 보통 이런 linking을 static linking이라고 부른다. 
그런데 시스템 전체에 현재 수행되고 있는 실행파일(실행파일이 수행되고 있는 하나의 단위를 process라고 한다.)들에서 printf함수를 사용하고 있는 녀석들이 매우 많으므로 그것이 모두 실행 파일에 포함되어 있다면 그것은 심각한 메모리 낭비를 가져온다는 문제점을 가지고 있다. 그래서 생각해 낸 것이 dynamic linking이라는 개념이다. 예를 들어 실행파일이 printf함수를 사용한다면 실행파일이 메모리로 loading될 때 printf가 포함되어 있는 library가 메모리 상에 있는 지 검사를 해 보고 있으면 reference resolving만 수행하고, 아니라면 새로 loading과 reference resolving을 하게 된다. 그렇게 되면 printf가 포함되어 있는 library는 메모리 상에 딱 하나만 loading되면 되고 메모리 낭비를 막을 수 있다. 그런 일을 할 수 있도록 도입된 것이 shared object 이다. MS Windows쪽의 프로그래밍을 하시는 사람이라면 DLL과 동일한 개념이라고 보면 된다. 
그런 shared object를 이용하여 dynamic linking을 하면 실행파일의 크기가 줄어든다. 반면에 당연히 실행파일이 메모리에 loading될 때는 reference resolving을 위해서 CPU의 연산력을 사용한다. 하지만 MS Windows의 DLL과는 달리 shared object 파일과 static linking을 할 수도 있다.(반대로 archive library를 이용하여 dynamic linking을 수행할 수는 없다.) 

(*) -static 옵션 
dynamic linking을 지원하고 있는 시스템에서 dynamic linking을 수행하지 않고 static linking을 수행하라는 옵션이다. dynamic linking을 지원하고 있는 시스템에서는 dynamic linking이 default 이다. 

직접 수행해 보자.
$ gcc -o hello_static -static hello.o 
실행파일 hello, hello_static 을 수행하면 결과는 똑같다. 파일의 크기를 비교해 보면 차이가 난다는 것을 알 수 있을 것이다.

/lib, /usr/lib에는 엄청 많은 library 파일들이 존재한다. 그럼 linker가 찾아야 하는 symbol을 모든 library 파일에 대해서 검사를 하는 것일까? CPU하고 HDD가 워낙 빠르면 그래도 무방하겠지만, 그렇게 하지 않는다.(“사용자가 쉽게 할 수 있는 일을 컴퓨터에게 시키지 말라.”라는 컴퓨터 사용 원칙이다.) 우선 gcc는 기본적인 library만 같이 linking을 하게 되어 있다. 나머지 library는 사용자의 요구가 있을 때만 같이 linking을 시도하도록 되어 있다. 그럼 기본적인 library가 무엇인지 알아야 하고 gcc에게 사용자의 요구를 전달할 옵션을 있어야 할 것이다. 기본적인 library는 당연히 C standard library 이다. C standard library의 이름은 libc.a또는 libc.so 이다. 최근의 linux 머신은 /lib/libc.so.6 이라는 파일을 찾아 볼 수 있을 것이다 (symbolic link되어 있는 파일이다). 그리고 libgcc라고 하는 것이 있는데… 생략하고. 이제 옵션을 알아보자.

(*) -nostdlib 옵션 
이름에서 의미하는 바대로 standard library를 사용하지 말고 linking을 수행하라는 뜻이다. 실제로는 standard library 뿐 아니라 startup file이란 녀석도 포함하지 않고 linking이 수행된다. startup file에 대해서는 좀 있다가 알아보도록 하겠다. 

(*) -l라이브러리이름 옵션 
특정 이름의 library를 포함하여 linking을 수행하라는 뜻이다. 예를 들어 -lmyarchive라고 하면 libmyarchive.a(또는 libmyarchive.so)라는 library파일과 같이 linking을 수행하는 것이다. library 파일 이름은 기본적으로 lib로 시작하니깐 그것을 빼고 지정하도록 되어 있다. 

library에 대해서 또 하나의 옵션을 알아야 할 필요가 있다. 다름 아닌 “어느 디렉토리에서 library를 찾는가”이다. 모든 library가 /lib와 /usr/lib에 있으라는 보장이 없다. 그 디렉토리를 정하는 방법은 두 가지 인데 LD_LIBRARY_PATH라고 하는 이름의 환경 변수를 셋팅하는 방법이 있고 또 한 가지는 gcc의 옵션으로 넘겨 주는 방법이 있다. 

(*) -Ldir 옵션 
library 파일을 찾는 디렉토리에 “dir”이란 디렉토리를 추가하라는 옵션이다.(-Idir 옵션처럼 -L과 dir을 붙여서 적습니다.) 예를 들어 -L/usr/local/mylib 라고 하면 /usr/local/mylib라는 디렉토리에서 library 파일을 찾을 수 있게 된다. 

== entry 이야기 
application을 작성하고 compile, linking 과정이 지나면 실행 파일이 만들어진다. 그리고 그 실행 파일이 수행될 때는 메모리로 load되어 수행이 시작된다는 사실을 알고 있다. 여기서 한가지 의문이 생기는데, “과연 코드의 어떤 부분에서 수행이 시작되는가?”이다. 답이 너무 뻔한가? main함수부터 수행된다고 답할 것인가? 다소 충격적이겠지만 “땡”이다. main함수부터 수행되지 않고 그전에 수행되는 코드가 존재한다. 그 먼저 수행되는 코드에서 하는 일은 여러 가지가 있는데 그냥 건너 뛰도록 하겠다. 아무튼 그 코드에서 main함수를 호출해 주고 main함수가 return하면 exit 시스템호출을 불러 준다. 그래서 main이 맨 처음 수행되는 것처럼 보이고 main이 return하면 프로그램 수행이 종료되는 것이다. 그럼 그 코드는 어디 있을까? 시스템에 따라서 다르겠지만 일반적으로 /lib혹은 /usr/lib 디렉토리에 crt1.o라는 이름의 object 파일이 있는데 그 object 파일 안에 있는 _start라는 이름의 함수(?)가 맨 먼저 수행되는 녀석이다. 결국 보통 application의 entry는 _start함수가 된다. 
그럼 crt1.o object 파일 역시 같이 linking 되어야 한다. gcc를 이용해 linking을 수행할 때 command line에 아무 이야기를 해주지 않아도 자동으로 crt1.o 파일이 함께 linking 된다. 실제로는 crt1.o 뿐 아니라 비슷한 crt*.o 파일들도 같이 linking 된다. 그렇게 같이 linking 되고 있는 object파일들을 startup file이라고 부르는 것 같다.(-nostdlib 옵션 설명할 때 잠시 나왔던 startup file이 바로 이 녀석들이다.) 그럼 ld는 _start파일이 entry인지 어떻게 알고, 다른 이름의 함수를 entry로 할 수는 없는것일까? 그것에 대한 해답은 아래 linking script부분에서 해결될 것이다. 

== 실행 파일에 남아 있는 정보 
linking의 결과 실행파일이 생겼는데, 보통 linux에서는 실행파일 형식이 ELF라는 포멧을 가진다.(linux 시스템에 따라 다를 수 있다.) ELF는 Executable and Linkable Format의 약자이다. 보통 linux 시스템에서의 relocatable object 파일의 형식도 ELF이다. 실제로 실행파일과 relocatable object 파일과는 조금 다른 형식을 가진다. 암튼 그건 상식으로 알아두고, 그럼 실행파일에 있는 정보는 무엇일까? 
이제까지의 알아낸 정보들을 모두 종합하면 알 수 있다. 우선 실행 파일이라는 녀석이 결국은 relocatable object 를 여러 개 쌓아놓은 녀석이므로 원래 relocatable object 파일이 가지고 있던 code와 data 정보는 모두 남아있을 것이다. 그리고 entry를 나타내는 address가 있어야 수행을 할 수 있을 것이다. 또, dynamic linking을 했을 경우 관련된 shared object 정보도 남아있어야 한다. 
실행 파일 속에 남아있는 data는 relocatable object에 있는 data처럼 프로그램 수행에 필요한 data가 있고 그냥 실행 파일을 설명하는 정보로서의 data가 있다. 예를 들어 -g 옵션을 주고 컴파일한 실행파일에서 디버깅 정보들은 실행과는 전혀 관계 없다. 따라서 그러한 정보들은 실행 파일 수행시에 메모리에 load될 필요도 없다.(load하면 메모리 낭비니깐) 실행 파일 속에 남아있는 code와 data는 relocatable object 처럼 특별한 단위로 저장되어 있다. ELF 표준에서는 segment라고 부르는데 보통의 경우는 object 파일처럼 section이라는 말이 쓰인다. reloctable object 파일과 마찬가지로 code는 text section에 저장되고 프로그램 수행 중에 필요한 data가 성격에 따라 나누어져 data, rodata, bss section이란 이름으로 저장되어 있다. 그 section단위로 메모리로 load될 필요가 있는지에 대한 flag정보가 있고 각 section이 load될 address(location과정에서 정해진다.)가 적혀 있어야 정확하게 loading을 할 수 있다. 
기타로 symbol reference resolving이 끝났는데도 ELF형식의 실행파일은 보통의 경우 많은 symbol 정보를 그냥 가지고 있는 경우가 있다. symbol 정보 역시 수행에는 하등 관계가 없으므로 없애도 되는데, strip이라고 하는 binutils안에 있는 tool로 없앨 수 있다. 

== linking script 
흠 이제 좀 어려운 이야기를 할 차례이다. Location과정에서 어떤 절대 address를 기준으로 각 section들을 쌓는지, 그리고 entry는 어떤 symbol인지에 대한 정보를 linker에게 알려줄 필요가 있다. 보통 application의 경우는 시스템 마다 표준(?, 예를 들어 entry는 _start 다 하는 식)이 있는지라 별로 문제될 것은 없는데, bootloader나 kernel을 만들 때는 그런 정보를 사용자가 넘겨 주어야 할 필요가 있다. 그런 것들을 ld의 command line argument로 넘길 수도 있지만 보통의 경우는 linking script라고 하는 텍스트 형식의 파일 안에 저장하여 그 script를 참조하라고 알려준다. (아무래도 command line argument로 넘겨 줄 수 있는 정보가 한계가 있기 때문이라고 생각이 든다. location과 entry에 관한 내용 중에 ld의 command line argument로 줄 수 있는 옵션이 몇가지 있으나 한계가 있다.) ld의 옵션 -T으로 linking script 파일 이름을 넘겨 주게 된다.(gcc의 옵션 아님) linux kernel source를 가지고 있는 사람은 arch/*/*.lds 파일을 한번 열어 보길 바란다. 그게 linking script고, 초기 절대 address 하고 section 별로 어떻게 쌓으라는 지시어와 entry, 실행 파일의 형식 등을 적어 놓은 내용이 보일 것이다. 물론 한 줄 한 줄 해석이 된다면 이런 글을 읽을 필요가 없다. 그 script를 한 줄 한 줄 정확히 해석해 내려면 GNU ld manual 등을 읽어야 할 것이다. 

== linux의 insmod 
linux kernel을 구성하고 device driver 등은 linux kernel module(이하 module) 형식으로 run-time에 올릴 수 있다는 것을 알고 있을 것이다. module을 run-time에 kernel에 넣기 위해서 사용하는 명령어가 insmod이다.(modprobe도 가능) 이 module 이라는 것이 만들어 지는 과정을 잘 살펴 보면 gcc의 옵션중에 -c옵션으로 컴파일만 한다는 것을 알 수 있다. 확장자는 .o를 사용한다. relocatable object 파일이고, ELF형식이다. 그럼 이 module이 linux kernel과 어떻게 합쳐질까? 당연히 linking 과정을 거쳐야 된다. 일종의 run-time linking 이다. 당연히 module은 kernel내의 많은 함수와 전역 변수를 참조한다. 그렇지 않다면 그 module은 linux kernel의 동작과는 전혀 관계 없는 의미 없는 module이 될것이다. 그럼 참조되고 있는 symbol을 resolving하기 위해서는 symbol의 절대 address를 알아야한다 . 그 내용은 linux kernel 내부에 table로 존재한다. /proc/ksyms라고 하는 파일을 cat 해보면 절대 address와 symbol 이름을 살펴볼 수 있을 것이다. 살펴보면 알겠지만 생각보다 적은 양이다. 적은 이유는 그 table이 linux kernel source에 있는 전역 symbol의 전부를 포함한 것이 아니라 kernel source 내부나 module 내부에서 EXPORT_SYMBOL()과 같은 특별한 방법으로 선언된(?, 이 선언은 C 언어 문법의 declaration과는 다르다.) symbol들만 포함하기 때문이다. 다른 전역 symbol 들은 module 프로그래밍에 별 필요가 없다고 생각되어 지는 녀석들이기 때문에 빠진 것이다. 따라서 EXPORT_SYMBOL()등으로 선언된 symbol들만 사용하여 module을 작성해야 한다. 당연히 linking 과정을 거치기 때문에 앞서 설명한 linking에서 발생할 수 있는 에러들이 발생할 수 있다. 제일 많이 발생할 수 있는 것은 역시 undefined reference 에러이다. gcc의 에러와는 조금 다른 메시지가 나오겠지만 결국은 같은 내용이다. 

== map 파일 
linking 과정을 끝내면 당연히 모든 symbol에 대한 절대 address가 정해지게 된다. 그 정보를 알면 프로그램 디버깅에 도움이 될 수도 있으니 알면 좋을 것이다. ld의 옵션중에 '-Map 파일이름'이라는 옵션이 있는데 우리가 원하는 정보를 문서 파일 형식으로 만들어 준다. 그 파일을 보통 map 파일이라고 부른다. symbol과 address 정보 말고 section에 대한 정보도 있고 많은 정보가 들어 있다. 
linux kernel을 컴파일을 하고 나면 나오는 결과 중에 System.map이라는 파일이 있는데 이 녀석이 바로 ld가 만들어 준 map 파일의 내용 중에 symbol과 symbol의 절대 address가 적혀 있는 파일이다. linux kernel panic으로 특정 address에서 kernel이 죽었다는 메시지가 console에 나오면 이 System.map 파일을 열어서 어떤 함수에서 죽었는지 알아볼 수도 있다. 

== 옵션 넘기기 
gcc의 이야기 맨 처음에 gcc는 단순히 frontend로 command line으로 받은 옵션을 각 단계를 담당하고 있는 tool로 적절한 처리를 하여 넘겨준다고 말했었다. 위에서 나온 ld의 옵션 -T와 -Map 과 같은 옵션은 gcc에는 대응하는 옵션이 존재하지 않는다. 이런 경우 직접 ld를 실행할 수도 있고 gcc에게 이런 옵션을 ld에게 넘겨 주라고 요청할 수 있다. 하지만 application을 컴파일할 때는 ld를 직접 실행하는 것은 조금 부담이 되므로, gcc에 옵션을 넘기라고 요청하는 방법이 조금 쉽다고 볼 수 있다. 그런 경우 사용되는 것이 -Wl 옵션인데 간단히 이용해 보도록 하자. 
$ gcc -o hello -static -Wl,-Map,hello.map hello.c 
그럼 hello.map이라는 매우 큰 문서 파일이 만들어진다. 한번 살펴 보도록 하자.(-static 옵션을 안 넣으면 살펴볼 내용이 별로 없을까봐 추가했다.) 
실제로는 -Wl 옵션처럼 as에게도 옵션을 넘겨 줄 수 있는 -Wa와 같은 옵션이 있는데 쓰는 사람을 본 적이 없다. 



'프로그래밍 > C, C++' 카테고리의 다른 글

레슨 4: Functions  (0) 2011.06.07
레슨 3: Loops  (0) 2011.06.07
레슨 2 : If statements (if,else문)  (0) 2011.06.07
Setting Up Code::Blocks and the MINGW Compiler on Windows  (0) 2011.06.07
C++ Programming HOW-TO  (0) 2011.06.06

* MySQL 재시작 하기

> service mysqld restart

 

* MySQL 사용자 추가

mysql> grant privileges on *.* to 아이디@도메인 indentified by '패스워드' with grant option;

 => 모든 데이터베이스에 권한을 주면서 추가할때

mysql> gran select , insert, update, delete, create, drop on test.* to 'test@localhost' indentified by '패스워드'


* 비밀번호 설정된 MySQL 접속하기
> mysql -u root -p
> 비밀번호 입력

* DB 보기
mysql> show databases;

* DB 사용하기
mysql> use 데이타베이스명;

 

* DB 삭제
mysql> drop 데이터베이스명;


* 테이블 목록보기
mysql> show tables; 

 

* 테이블 구조보기

mysql> explain 테이블명; 또는 describe 테이블명;

 

* 현재 상태보기
mysql> status; 


* DB의 권한 필드정보 보기
mysql> use mysql;
mysql> select * from db;

* DB 에 권한 주기
mysql> use mysql;
mysql> insert into db values("localhost", "데이타베이스명", "사용자명", 권한 필드 갯수만큼의 "Y"); 
예) insert into db values("localhost", "testdb", "root", "Y", "Y", "Y", "Y", "Y" ...........); 

* DB에서 datetime 현재시간 가져오기
mysql> select now();

* 디렉토리에 있는 스크립트 sql 파일 실행하기
mysql> source /디렉토리경로/파일명.sql;

* datatime을 CHAR 타입으로 바꾸기(형변환하기)
mysql> select cast(now() as char);
mysql> select convert(date_format(now(),'%Y-%m-%d') , char(10)) as time;

* mysql 에서 나가기
mysql> exit

+ Recent posts