1. 전처리


2. 어휘 분석

 : 전처리 된 파일을 읽어 소스를 문법적으로 의미 있는 가장 최소 단위(토큰)로 나눈다.


3. 구문 분석

 : 파서에 의해 구문 분석이 이루어짐. 구문 분석은 어휘 분석 단계에서 스캐너가 토큰을 인식하고 파서에서 넘겨주면 파서가 문법적 오류가 있는지 검사한 후에 중간 언어 생성을 위한 파서 트리를 만든다.


4. 의미 분석

 : 문법상 오류는 없지만 의미상 오류가 있는 것을 검사한다.
 ex) 선언되지 않은 변수의 사용 / 함수의 인자 개수나 인자형의 불일치 / 자료형의 불일치 등


5. 중간 언어 생성

 : 작성된 언어에 독립적인 중간 언어 생성

 ex) gcc의 경우 RTL(Register Transfer Language)이라는 중간언어를 생성함


6. 최적화

 : 중간 코드 최적화

 : 목적 코드 최적화


7. 목적 코드 생성

 : 중간 언어를 바탕으로 목적 코드를 생성한다. 목적 코드는 어셈블리 언어로 작성되며 이후 어셈블러에 의해서 인스트럭션으로 변환 된다. 목적 코드를 생성할 때는 되도록 재배치가 가능한 코드를 생성하게 한다.


8. 어셈블리

 : 어셈블리 언어로 작성된 목적 코드를 기계어 코드로 변환한다.


8. 링킹



Extern, Static 변수와 함수

Extern, Static 키워드는 변수와 함수에서 쓰일 때 그 의미가 조금 다르다. 형식과 쓰임새가 차이가 있기 때문이다.

 

 

<Extern 전역변수, 지역변수>

extern 변수는 다른 파일에서 변수를 공유해서 쓰기 위해 있는 키워드인데, 전역변수는 키워드를 생략해도 기본으로 extern선언이 되는 성질이 있다.

아래 코드를 보면,

-----------------------

/*main.c*/

#include <stdio.h>

#include "fun1.h"

int a=1; // 변수 선언

void main(){

f();

printf("%d\n",a); // 여기서의 a는 위의 a

}

-----------------------

/*fun1.c*/

#include <stdio.h>

#include "fun1.h"

int a; // 또 다른 선언문

void f(){

a = 7;

printf("%d\n",a); //여기서의 a는 바로 위의 a

}

-----------------------

/*fun1.h*/

void f();

-----------------------

결과는: 7

7

-----------------------

fun1.c에서 바꾼 a값이 main.c의 a에도 적용되어있다.

따라서 main.c와 fun1.c의 전역변수 a는 동일한 변수이거나 항상 싱크가 되어있다는 것을 알 수 있다.

여기에서 main.c와 fun1.c의 선언문에 extern을 붙여보자.

(main.c : int a=1; -> extern int a=1;)

(fun1.c : int a; -> extern int a;)

한쪽파일에만 extern키워드를 쓰든 양쪽에 다 쓰든, 모든 경우에서 결과는 같다.

-----------------------

//간략화한 코드//

/*main.c*/

extern int a=1;

/*fun1.c*/

int a;

-----------------------

/*main.c*/

int a=1;

/*fun1.c*/

extern int a;

-----------------------

/*main.c*/

extern int a=1;

/*fun1.c*/

extern int a;

-----------------------

결과는 항상

7

7

-----------------------

이 된다. 즉, 전역변수로 선언할 경우 extern를 넣든 안 넣든 결과는 똑같아 보인다. extern키워드를 생략해도, 전역변수는 기본으로 extern이기 때문일 것이다. 그런데 초기화구문의 extern과 초기화가 없는 extern선언문은 의미에 차이가 있다. 초기화에 쓰이는 extern은 변수를 새로 정의하는 선언문이라는 의미의 extern이고 실제로 메모리를 할당해서 변수를 정의한다. 그런데 초기화가 없는 선언문은 변수를 새로 정의하는 것이 아닌, “다른 파일(혹은 같은 파일 내의 다른 부분)에서 찾아서 있으면 그것을 가져다 쓰겠다”는 ‘참조선언’의 의미를 갖는 extern이다. 그렇다면 어떤 것이 정의선언이고 어떤 것이 참조선언일까? 아래의 경우를 보자. 아래의 경우는 모두 전역변수로 선언된 경우이다.

-----------------------

전역변수 선언문 종류

1. extern int a=1 -> extern변수 정의 및 초기화

2. int a =1; -> extern 변수 정의 및 초기화

3. extern int a; -> extern 변수(새로 정의하지 않고)참조선언

4. int a; -> extern 변수 정의(0으로초기화) 또는 참조선언

-----------------------

4의 경우는 그 쓰임새가 애매하다. 1이나 2와 쓰일 때는 참조선언이 되고, 3과 쓰일 때는 정의구문이 된다.(컴파일러에서 자동으로 처리함) 애매한만큼 지양해야 할 코드이다.

따라서

-----------------------

/*main.c*/

(extern생략가능)int a=1;

/*fun1.c*/

(extern생략가능)int a=2;

-----------------------

같은 식으로 여러번 초기화를 해주면 둘 다 정의 선언문이므로 ‘여러번 정의했다’는 에러가 뜨게 된다. 같은 a를 두 번 정의했으니 에러가 뜨는 것이다. 아까처럼

-----------------------

/*main.c*/

(extern생략가능)int a=1; //extern변수 정의 및 초기화

/*fun1.c*/

(extern생략가능)int a; //a라는 extern변수를 다른 곳에서 찾아서 쓰겠다는 의미

-----------------------

로 코딩을 했을 경우, 에러가 없다. 이 관계는 다음의 예시에서 더욱 명백해진다.

-----------------------

/*main2.c*/

int a=1;

int a;

void main(){

int b=1; (1)

int b; (2)

}

-----------------------

새로운 main2.c파일에서 a는 전역변수, b는 지역변수로 선언되었다. 처음 int a=1;로 선언과 동시에 초기화되었고, 바로 아래줄에서 int a;는 선언이 아닌, “다른 곳에서 a를 찾아본 뒤에 그것을 쓰겠다”는 표현이 되므로 에러가 나지 않는다.(extern int a;의 생략형이라고 이해하면 쉬울 것이다.) 그러나 지역 변수 b의 경우 “여러번 재정의”에러가 뜬다. 잠깐 지역변수부분만 살펴보면, extern키워드를 통해 초기화하는 것이 아예 불가능하다.

-----------------------

void main(){

extern int b=1;

}

-----------------------

처럼 코딩을 하면, “블록 범위를 사용하여 extern변수를 초기화할 수 없습니다.”라는 에러메세지가 뜬다. 블록 범위, 즉 로컬변수선언시엔 무조건 그 블록 범위 내에서만 사용하게 되어있으므로, 다른 곳에서 이 변수를 사용할 수 있도록 하는 extern초기화 구문을 사용 못하게 막아놓은 것이다. 로컬변수를 다른 곳에서 사용하려면, 함수 결과값으로 끄집어내서 사용하는 수 밖에는 없다. 대신 초기화가 아닌 extern참조선언문은 함수 내에서도 전역에서 쓰듯이 사용이 가능하다.

-----------------------

// 간략화한 코드 //

/*main.c*/

(extern생략가능)int a=1; //extern변수 정의 및 초기화

/*fun1.c*/

void fun1(){

extern int a; // 다른 곳의 extern변수 a를 찾아 쓰겠다는 의미

}

-----------------------

그런데 지역변수에서 extern변수를 참조선언해서 가져다 쓸 때 주의할 점이 있다. 함수 밖에서 참조선언을 할 때는 참조에 실패하면 컴파일 에러가 나지만, 함수 내에서 참조선언을 하면 참조할 것이 없어도 컴파일 상의 에러가 없다.(헉쓰...) 아래의 코드를 보자.

-----------------------

/*main.c*/

int a=1; //extern변수 정의 및 초기화

/*fun1.c*/

extern int b; //extern변수 b참조선언.

-----------------------

결과: “b의 외부기호를 확인할 수 없다”는 에러.

-----------------------

/*main.c*/

int a=1; //extern변수 정의 및 초기화

/*fun1.c*/

void fun1(){

extern int b; //extern변수 b참조선언.

}

-----------------------

결과: no error

-----------------------

아래의 경우처럼 에러가 없으면 나중에 있지도 않은 b변수를 사용할 때 에러가 난다.

-----------------------

/*main.c*/

int a=1; //extern변수 정의 및 초기화

/*fun1.c*/

void fun1(){

extern int b; //extern변수 b참조선언.

printf("%d",b); //변수 b 사용

}

-----------------------

결과: “b의 외부기호를 확인할 수 없다”는 에러.

-----------------------

따라서 결국 b를 안전하게 사용하고 싶으면 전역변수로 참조선언 한 뒤에 함수에서 쓰는 것이 좋은 코딩이라는 이야기다. 아래의 코드가 그렇다.

-----------------------

/*main.c*/

int a=1; //extern변수 정의 및 초기화

int b=2; //extern변수 b를 추가해보았다.

/*fun1.c*/

extern int b; //extern변수 b참조선언은 밖에

void fun1(){

printf("%d",b); //변수 b 사용

}

-----------------------

결과: 2

-----------------------

정리하자면, 전역변수는 초기화인 경우 항상 쓰지 않아도 extern으로 정의가 되며, (즉 필연적으로 모든 전역으로 정의되는 변수는 이름 하나당 변수 하나가 최대다. 단, static변수로 만들지 않을 경우에 한해서) 초기화구문이 아닌 경우엔 extern키워드를 쓰면 확실히 참조선언문이 되지만 extern을 생략하면 경우에 따라 참조선언도 되지만 정의가 되어있지 않았다면 정의구문이 되어버린다. 따라서 extern은 기본적으로 생략 가능하지만 헷갈림을 방지하고 코딩의 가독성을 위해서 아래와 같이 여러 파일에서 사용할 때는 extern키워드를 명시해주는 것이 좋다.

-----------------------

/*main.c*/

extern int a=1; // extern변수를 선언하겠다는 의미. 다른 파일에서도 사용하겠다는 뜻.

생략해도 크게 상관은 없다.(외부에서 쓸 일이 있을 땐 생략 삼가)

/*fun1.c*/

extern int a; //다른 곳에 있는 extern변수를 가져다 쓰겠다는 의미.

생략하면 위의 경우와 구분이 어렵다.(쓰는 것을 필수)

-----------------------

덧붙이자면, 아래의 경우는 어떨까?

-----------------------

/*main.c*/

extern int a;

/*fun1.c*/

extern int a;

-----------------------

둘 모두 참조선언문이라 어디에서도 정의가 되어있지 않기 때문에, 외부 기호를 확인할 수 없다는 에러가 뜨는 것은 당연하다.(전문용어로 찾다가 GG) 그럼 아래의 경우는 또 어떨까?

-----------------------

/*main.c*/

int a;

/*fun1.c*/

int a;

-----------------------

둘 중 하나의 구문의 정의 선언문이 되고 나머지가 참조선언문이 되는 것은 확실하다. 그러나 그게 어느것인지는 나도 모르겠다.

 

 

<Static 전역, 지역변수>

아까 전역변수의 경우에는 extern키워드를 생략해도 자동으로 extern선언이 된다고 했다. 그렇다면 한 파일 내에서 전역변수로 선언해서 쓰고는 싶은데 다른 파일에서 참조하는 것은 막고 싶을 경우(전역변수지만 extern은 아닌)는 어떻게 할까? 그럴 땐 static선언을 하면 된다.

-----------------------

/*main.c*/

static int a=1; //static은 생략하면 안 된다.

/*fun1.c*/

int a; // int a=2, 혹은 extern int a=2도 가능

-----------------------

위의 경우, static변수로 선언한 main.c의 a는 main.c파일 안에서만 사용 가능하고 다른 파일에서는 extern키워드로 참조할 수 없다. fun1.c에서 선언한 a는 main.c의 a와는 다른 메모리에 있는 a이다. 이 경우는 fun1.c의 a가 extern변수이고 main.c의 a는 extern변수가 아닌 mian.c에 속박된 static변수가 되는 것이다. 따라서 아래와 같이 쓰면 에러가 난다.

-----------------------

/*main.c*/

static int a=1; //static은 생략하면 안 된다.

/*fun1.c*/

extern int a;

-----------------------

extern int a;에서 참조선언을 했으므로 다른 곳에서 a를 찾아서 가져다 써야할텐데, 유일한 a는 static으로 묶여있으니 참조를 할 수 없다. 그래서 외부기호 확인 불가능 에러가 뜨는 것이다. 지역변수의 경우엔 extern변수 정의가 불가능하므로 static으로 따로 정의할 필요가 없을 것이다.

<extern, static 함수>

함수의 경우 extern, static개념이 간단하다. 함수는 쓰임새에 따라서 외부함수, 내부함수로 나눠 부르지만 사실 전역, 지역의 개념이 없고 굳이 말하자면 모두 전역이기 때문이다.

기본적으로 함수선언은 생략해도 모두 extern이다.

아래 코드를 보자.

-----------------------

/*main.c*/

#include <stdio.h>

void f(); (1)

void main(){

f();

}

-----------------------

/*fun1.c*/

#include <stdio.h>

#include "fun1.h"

void f(){

printf("fun1.c\n");

}

-----------------------

/*fun1.h*/

void f();

-----------------------

결과: fun1.c

-----------------------

main.c에서 헤더파일을 추가하지도 않았는데, fun1.c의 함수를 가져다 쓴 것을 알 수 있다. fun1.c의 f()함수 선언 자체가 extern으로 되어있고, main.c의 (1)에서도 extern으로 선언해서 다른 파일의 함수를 자동으로 검색해서 call했기 때문이다. 이 점은 위 코드 어느 부분에 extern을 넣어도(심지어 함수정의부분에서도) 똑같은 결과가 나온다는 점에서 알 수 있다. 심지어 (1)을 생략해도 경고가 뜨지만 실행은 된다.(하지만 피해야 할 코딩방법) 함수는 기본적으로 extern으로 선언된다는 것을 알 수 있는 부분이다. 아래와 같이, main.c에서도 같은 이름의 함수 f()를 정의하면 어떻게 될까?

-----------------------

/*main.c*/

#include <stdio.h>

void f();

void main(){

f();

}

void f(){

printf("main.c\n");

}

-----------------------

/*fun1.c*/

#include <stdio.h>

#include "fun1.h"

void f(){

printf("fun1.c\n");

}

-----------------------

/*fun1.h*/

void f();

-----------------------

결과: 함수 재정의 오류

-----------------------

main.c에서 이미 정의한 f()함수가 자동으로 extern이기 때문에, fun1.c에서 또 정의를 해주면 이것도 자동으로 extern이고, extern함수는 고유해야하기 때문에 같은 함수를 두 번 정의한 것으로 인식해서 에러가 나게 된다. 이것을 막고, 두 함수를 모두 사용하고 싶다면

그 때 static 키워드로 선언을 하면 된다.

-----------------------

/*main.c*/

#include <stdio.h>

static void f(); (1)

void main(){

f();

}

void f(){

printf("main.c\n");

}

-----------------------

/*fun1.c*/

#include <stdio.h>

#include "fun1.h"

void f(){

printf("fun1.c\n");

}

-----------------------

/*fun1.h*/

void f();

-----------------------

결과: main.c

-----------------------

잘 보면 mian.c의 f()함수는 static으로 선언되었고, fun1.c의 함수는 extern으로 선언되었으므로, main.c에서 실행한 f()함수는 mian.c에서 정의한 함수가 되며 출력도 그렇게 나온다. 반대로 mian.c의 함수를 extern으로, fun1.c의 함수를 static으로 정의해도 결과는 똑같다. 둘 다 static으로 정의해도 마찬가지다. 이유는 생각해보시길...





링커가 모든 심볼을 해석하고 나면, 심볼 참조는 오직 하나의 심볼 정의만을 가지게 된다. 그 때, 링커는 아래 두 스텝으로 구성된 재배치 작업을 하게 된다.

 - 섹션과 심볼정의들을 재배치 한다. 링커는 같은 타입의 모든 섹션들을 새로운 하나의 섹션으로 통합한다. 예로 들면, 링커는 입력으로 받은 모든 재배치 가능한 오브젝트 파일들의 .data 섹션을 합쳐 하나의 .data 섹션을 만든다. 같은 과정이 .code에 대해서도 행해진다. 그런 후에 링커는 병합된 새로운 섹션과, 병합된 새로운 색션내의 각 색션, 그리고 모든 심볼들에 대해 런-타임 메모리 주소를 할당한다. 이러한 작업 후에는 프로그램의 모든 코드와 전역 변수들은 고유한 로드-타임 주소를 가지게 된다.

 - 섹션들안에 있는 심볼의 참조를 재배치 한다. 이 과정에서,  링커는 코드와 데이터 섹션에 있는 모든 심볼 참조를 수정하여, 그것들이 올바른 로드-타임 주소를 가지게 한다.


 어셈블러가 해석 안 된 심볼들을 만날 때마다, 어셈블러는 오브젝트 파일의 .rel.text / .rel.data 섹션에 해석 안 된 심볼들을 위한 재배치 항목을 생성한다. 이러한 재배치 항목은 해석 안된 심볼들이 어떻게 해석되어야 하는지에 대한 정보들을 담고 있다. 전형적인 ELF 재배치 항목은 다음과 같은 멤버들로 구성된다.

 -  옵셋, 재배치 되어질 필요가 있는 심볼 참조의 섹션내에서의 옵셋을 나타내며, 혹은 디스크의 저장공간이 오브젝트 파일 내에서 재배치 되어질 필요가 있을 시, 이 값은 재배치 될 필요가 있는 디스크의 섹션의 처음부터 바이트 단위로 얼마만큼 떨어져 있는가를 나타낸다.

 -  심볼, 이것은 심볼 테이블에서의 인덱스로서, 아직 해석 안 된 심볼이 심볼 테이블에서 몇 번째 위치에 있는가를 나타낸다.

 -  타입, 재배치 타입, 일반적으로 R_386_PC32는 S+A-P로 계산하며, R_386_32는 S+A로 계산한다. 이 계산에서, S는 재배치 항목의 심볼 항목에 들어있는 값을 가리키며, P는 섹션 옵셋 혹은 재배치 되는 저장장치의 주소를 나타낸다. 그리고 A는 재배치 가능한 필드를 계산하는데 필요한 주소이다.

출 처 :  http://wiki.kldp.org/wiki.php/DocbookSgml/LinkerLoader-TRANS 



모든 재배치 가능한 오브젝트 파일들은 심볼 테이블과 그와 관련된 심볼들을 가지고 있다. 링커의 관점에서 볼 때 심볼들은 다음과 같이 분류할 수 있다.

 - 현재의 파일에서 정의되고, 다른 파일들에서 참조되는 전역 심볼. 모든 non-static 함수들과 전역 변수들이 이 분류에 해당한다.

 - 현재의 파일에서 참조는 되나, 다른 곳에서 정의된 전역 심볼. extern으로 정의된 모든 함수들과 변수들이 이 분류에 해당한다.

 - 현재의 파일에서만 정의되고 참조되는 지역 심볼. 모든 static 함수들과 변수들이 이 분류에 해당한다.

링커는 심볼의 참조를 해석할 때, 입력으로 주어지는 재배치 가능한 오브젝트 파일의 심볼 테이블로부터 꼭 하나만 존재하는 심볼의 정의를 참조하여 심볼 참조를 해석한다. 지역 심볼은 그에 대한 다중 정의를 심볼 테이블이 가질 수 없으므로 쉽게 해석 된다. 그러나 전역 심볼의 해석은 약간의 트릭이 요구된다. 컴파일 타임 때, 컴파일러는 전역 심볼들을 strong 혹은 weak 한 것으로 만드는데, 함수들과 초기화 된 전역 변수들은 strong 하게, 초기화 되지 않은 변수들은 weak하게 만든다. 그러면 링커는 아래의 룰을 적용하여 심볼들을 해석하게 된다. 

1. 다중 strong 심볼들은 허가되지 않는다.

2. 하나의 strong 심볼과 여러 개의 weak 심볼들이 있으면, strong 심볼을 선택한다.

3. 여러 개의 weak 심볼들이 있으면, 그것들 중 아무거나 선택한다.

출 처 : http://wiki.kldp.org/wiki.php/DocbookSgml/LinkerLoader-TRANS 



1. 초기화 된 전역 변수는 strong 하게, 초기화 되지 않은 변수들은 weak 하게 만든다..

- funcA.c -
1
2
3
4
5
6
7
8
#include <stdio.h>

int a = 6;          // Strong Symbol

void funcA()
{
    printf("funcA()\n");
}

- main.c -
1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

int a;                  // Weak Symbol

int main()
{
    printf("a = %d\n", a);
    funcA();

    return 0;
}

- 결과 화면 -




2. 2개의 초기화 된 전역 변수 (Strong Symbol) 일 때

- funcA.c -
1
2
3
4
5
6
7
8
#include <stdio.h>

int a = 6;          // Strong Symbol

void funcA()
{
    printf("funcA()\n");
}

- main.c -
1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

int a = 10;         // Strong Symbol

int main()
{
    printf("a = %d\n", a);
    funcA();

    return 0;
}

- 결과 화면 -


컴파일 시 에러 발생...




3. 초기화 된 전역 변수 하나를 __weak 속성을 부여 했을 때

- funcA.c -
1
2
3
4
5
6
7
8
#include <stdio.h>

int a = 6;          // Strong Symbol

void funcA()
{
    printf("funcA()\n");
}

 - main.c -
1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

int __attribute__((weak)) a = 10;   // Weak Symbol

int main()
{
    printf("a = %d\n", a);
    funcA();

    return 0;
}
 
- 결과 화면 -
 

초기화 된 전역 변수 2개를 선언했더라도, 2번 경우와 다르게 하나의 심볼을 weak로 지정해서 에러없이 컴파일 되며, 실행도 잘 되었다.


 

typeof 는 gcc의 확장된 기능으로, 해당 변수의 타입을 반환하는 역할을 하는 매크로 함수이다.

예제 코드)
#include <stdio.h>

int main()
{
    char s1;
    typeof(s1) s2;

    unsigned char u1;
    typeof(u1) u2;

    s2 = 0xff;
    u2 = 0xff;

    printf("s2 = %d\n", s2);
    printf("u2 = %d\n", u2);
    return 0;
}

결과)
s2 = -1
u2 = 255


strlen 함수를 사용시 인자로 NULL 포인터를 넘기면 segmentation fault 가 발생한다.
이는 0 번지로 가서 문자 개수를 세는 것으로 생각되며, 0번지를 참조 하려고 하기 때문에 segmentation fault가 발생하는 것 같다.




////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

"\0" 문자열로 바꿔주면 segmentation fault가 발생하지 않는다.



 

매크로 함수

#1. 매크로 함수란?

#2. 괄호 속에 있는 이유는?

#3. 매크로와 함수 및 템플릿의 비교

#4. 인라인 함수란?

#5. 문자열 조작

#6. 문자열화 연산자

#7. 결합연산자

#8. 내장매크로

#9. assert()

 

#1. 매크로 함수란? (a)

  - 매크로 함수는 #define을 사용하여 만들어지는 기호이다. 이 기호는 함수처럼 하나의 인수를 가진다.

  - 전처리기는 특정 인수가 무엇이든 상관없이 치환 문자열로 대치한다.

  1.        #define TWICE(x) ( (x) * 2 )

  - 매크로는 하나 이상의 매개 변수를 가질 수 있고, 반복해서 사용할 수 있다.

  1.        #define MAX(x,y) ( (x) > (y) ? (x) : (y) )
  2.        #define MIN(x,y) ( (x) < (y) ? (x) : (y) )

  [주의] 매크로 함수 정의에서 매개 변수 목록에 대한 여는 괄호가 매크로 이름 바로 다음에 빈칸 없이 곧바로 이어져야 한다.

 

#2. 괄호 속에 있는 이유는? (b)

  - 전처리기는 치환 문자열의 인수 주위에 괄호가 있을 것을 요구하지 않지만, 괄호는 복잡한 값을 매크로에 전달할 경우 원하지 않는 결과가 나오는 것을 방지하는데 도움을 준다.

#3. 매크로와 함수 및 템플릿의 비교 (c)

  - 문제점

    ① 매크로가 커질 경우 혼란스러워질 수 있다.

    ② 사용한 곳에서 곧바로 인라인으로 확장된다.

    ③ 매크로가 컴파일러에서 사용하는 중간 소스 코드에 나타나지 않는다. 대부분의 대버거에서 사용할 수 없다.

    ④ 형에 안전하지 않다.

#4. 인라인 함수란? (d)

  - 확장인라인 : 함수의 내용이 함수가 호출된 코드에 삽입되는 것을 말한다.

  1.     inline unsigned long Square(unsigned long a) { return a * a; }

#5. 문자열 조작 (e)

  - 문자열화 연산자(#)는 이 연산자 다음에 나오는 것은 무엇이든 간에 겹따옴표 속에 넣어 문자열로 만든다.

#6. 문자열화 연산자 (f)

  - 문자열화 연산자는 그 연산자 다음에 나오는 어떤 글자들이라도 다음 공백 문자까지 겹따옴표로 둘러싸게 된다.

  1.        #define WRITERSTRING(x) count << #x
  2.        WRITERSTRING(This is a string);    =     count << "This is a string";

#7. 결합연산자 (g)

  - 결합연산자는 하나 이상의 용어를 합쳐서 새로운 단어로 만들어 내는 것이다.

  - 새 단어란 실제로 클래스 이름, 변수 이름, 배열에 대한 오프셋, 또는 어떤 것이든 나타날 수 있는 문자열로 사용할 수 있다.

  1.         #define fPrint(x) f ## x ## print
  2.         #define Listof(Type) class Type##List \
  3.          { \
  4.              public : \
  5.              Type##List(){} \
  6.              private : \
  7.              int itsLength; \
  8.          };

#8. 내장매크로 (h)

  - 많은 컴파일러들은 몇가지 유용한 매크로들을 내장하고 있다.

  - _DATE_ : 현재 날짜

     _TIME_ : 현재 시간

     _LINE_ : 소스코드 행 번호

     _FILE_ : 소스코드 파일 이름


  - 프로그램에서 사용하는 이름들과 겹칠 가능성을 줄이기 위해 두 개의 밑줄로 둘러 싸여 있다.

#9. assert() (i)

  - 매개 변수가 참으로 평가될 경우 참을 반환하고, 거짓으로 평가될 경우 몇 가지 종류의 동작을 한다.

  - 많은 컴파일러들이 assert()가 실패할 경우 프로그램을 강제로 종료한다. 그밖의 컴파일러들은 예외를 발생시킨다.

  - DEBUG가 정의되지 않을 경우 전처리기가 이 매크로를 무시한다. 수행성의 낭비나 실행 버전 프로그램의 크기의 증가가 없다.

  1.         #define DEBUG
  2.         #ifdef DEBUG
  3.              #define ASSERT(x)
  4.         #else
  5.              #define ASSERT(x) \
  6.                      if( !(x) ) \
  7.                      { \
  8.                            cout << "ERROR!! Assert " << #x << " falied\n"; \
  9.                            cout << "On line " << _LINE_ << "\n"; \
  10.                            cout << "in file" << _FILE_ << "\n"; \
  11.                       }
  12.          #endif

출처 : http://lonelysm.springnote.com/pages/4011761?print=1#h

물론 고수님들에겐 쉬운 문제지만 다음 구조체에서 char data[1]의 역할이 무엇인지 생각 해봅시다.

typedef struct tagWHATTHE {
int data1;
int data2;
char data[1];
} WHATTHE;

모르시는 분들은 최소 1분 정도 생각을 해봅시다.



그래도 잘 모르겠으면 약간의 힌트:

typedef struct tagWHATTHE {
int size;
int type;
char data[1];
} WHATTHE;



char data[1]의 역할은

난생 처음 이런 구조체를 보면 data[1] 변수가 도대체 어떤 의미인지 감을 잡기 힘들다. 왜 이런 변수가 쓰이는지는 직접 예제 코드를 보는 것이 가장 좋을 것 같다.

typedef struct tagHEADER {
int size;
int type;
char data[1];
} HEADER;

// int data_length, int* data;
HEADER* hd = (HEADER*)malloc(sizeof(HEADER) + data_length);
hd->size = sizeof(HEADER) + data_length;
hd->type = 0;
memcpy(hd->data, data, data_length);
fwrite(hd, 1, hd->size, file_handle);

한 마디로 char data[1]의 의미는 길이가 정해지지 않은 데이터를 담기 위한 일종의 더미 변수라고 보면 된다. 예에서도 있듯이 이런 코딩 테크닉은 어떤 데이터의 헤더를 표현할 때 많이 등장한다. 메모리 덩어리를 할당 받고 그것을 이 헤더 타입으로 캐스팅을 하면 앞 부분은 헤더가 놓이고 바로 뒤에 갖고 싶은 가변 길이 데이터를 쉽게 이을 수 있다. 윈도우 프로그래밍을 해보신 분이라면 아마 이런 것을 자주 봤을 것이다. 예를 들면:

typedef struct _RGNDATAHEADER {
DWORD dwSize;
DWORD iType;
DWORD nCount;
DWORD nRgnSize;
RECT rcBound;
} RGNDATAHEADER;

typedef struct _RGNDATA {
RGNDATAHEADER rdh;
char Buffer[1];
} RGNDATA;

RGN이라는 그래픽에서 그리는 영역을 지정하는 클리핑 객체를 담는 자료구조다. RGNDATA에 채워질 데이터가 몇 개나 올지 알 수 없으므로 이렇게 하였다. 또, 대표적인 비트맵 파일 관련 구조체도 빼놓을 수 없다.

typedef struct tagBITMAPINFO {
BITMAPINFOHEADER bmiHeader;
RGBQUAD bmiColors[1];
} BITMAPINFO;

배열 크기가 여기는 [1]인데 [0]으로 둬도 사실 문제 없다. 그리고 소켓 프로그래밍할 때 보게 되는 hostent에도 이제는 과거 호환성 때문에만 남았지만 크기가 0인 배열을 볼 수 있다. 그런데 보통 크기 0인 배열을 선언하면 컴파일러가 워닝을 띄운다. 그래서 C99 표준에는 flexible array member라고 해서 맨 마지막 구조체 변수의 선언이 char data[] 처럼 될 수 있도록 하기도 한다.

도대체 왜?

그렇다면 왜 이렇게 했을까? 보다 직관적으로 코드를 만든다면 아래처럼 쓸 수도 있다.

typedef struct tagHEADER {
int size;
int type;
char* data;
} HEADER;

HEADER* hd = (HEADER*)malloc(sizeof(HEADER));
hd->size = 1024;
hd->type = 0;
hd->data = (char*)malloc(hd.size);
...

그러나 이 접근은 단점이 있다. 위에서 예를 들었듯이 이 자료구조를 파일에 쓴다고 하면 헤더 부분 따로 그리고 data 영역을 따로 두 번에 걸쳐 파일 쓰기를 해야 한다. 결국 이 말은 이 자료구조에 접근하려면 헤더 한번, 실제 한번, 즉 두 번의 참조가 필요하다는 이야기다.

여기서 쪼잔하게 따지면 위 코드는 데이터 참조의 지역성(locality)이 줄어들어 캐시 미스를 한번 더 유발할 수 있는 단점도 있다. 요즘 프로세서들 캐시가 수 메가 바이트니 이 정도 괜찮다라는 생각을 함부로 하지 말자. 정말 성능이 중요한 프로그램은 이런 사소한 지역성 차이에서 오는 차이를 결코 무시할 수 없다. 비슷한 예로 “구조체의 배열(Array of Structure)” 이냐 “배열의 구조체(Structure of Array)”라는 문제도 있다 (우리말과 영어의 어순이 반대라 이거 이름이 참 헷갈린다). 간단한 예를 보면..

구조체의 배열(Array of Structure, AoS):

struct {
double x;
double y;
double z;
} vector[4];

배열의 구조체(Structure of Array, SoA):

struct {
double x[4];
double y[4];
double z[4];
} vector_set;

각각 상황에 따라 적절히 선택해서 골라야 한다. 사소해 보이지만 경우에 따라 큰 성능 차이가 나타날 수 있다.

AoS 같은 접근은 루프 한 순회에서 모든 원소들이 접근될 때 유리하다. vector[i]을 읽으면 일단 i번 째 x, y, z는 모두 캐시에 올라오기 때문이다. 반면 원소는 3개인데 루프에서 고작 x원소만 접근 된다면? 이 vector 배열이 매우 크다면 손실은 심각하다. 그럴 때는 SoA 구조가 바람직하다. 그 외에도 사소한 영향을 더 생각할 수 있지만 생각보다 글이 길어져서 이쯤에서 그만..

한줄요약: char data[1]; 같은 것이 보여도 쫄지 맙시다.

출 처 : http://minjang.egloos.com/2254472

sizeof 요넘이 자료형의 크기를 얻기위해 쓰이죠.

 

sizeof(int);

하면 int형이 몇바이트인지도 알 수 있고.

 

int i;

sizeof(i);

하면 변수 i가 몇바이트인지 알 수 있죠.

 

char arr[24];

sizeof(arr);

배열의 크기도 구할 수 있습니다.

 

---------------------------------------------------------

 

이러한 크기들은 어떻게 구하는 걸까요?

 

CPU가 계산해서 크기를 구하는게 아닌

컴파일러가 컴파일할때 크기를 인식해서 상수로 바꿉니다.

 

char arr[24];

printf("%d", sizeof(arr) );

여기서 sizeof(i)를 처리할 때...

컴파일러는 위의 배열을 선언한 부분을 참조합니다.

그리고 arr은 24바이트구나~~ 하면서

printf("%d", sizeof(arr) ); -> printf("%d", 24);

이렇게 바꿉니다.

 

----------------------------------------------------------

sizeof 는 컴파일러가 처리한다는 것을 알았으니

1개를 알려주면 10개를 안다는 분들은 감이 오실겁니다.

 

CPU가 32비트든, 64비트든, 운영체제가 32비트든 64비트든 간에

컴파일러가 32비트 컴파일러면 sizeof(int)는 모두 4로 처리된다는 겁니다.

 

32비트 CPU쓰고 32비트 운영체제를 쓴다 할지라도

16비트 컴파일러를 사용하면 sizeof(int)는 2가 됩니다.

 

-------------------------------------------------------------

 sizeof가 컴파일 될때 처리된다는 것을 알았으니

여기서 또 한가지를 알 수 있지요.

 

c언어로 표현하면

int *p;

p = malloc(16);

printf("%d", sizeof(p) );

일때, sizeof(p)는 어떻게 될까요?

정답은 16이 아닌 4 입니다.(32비트 컴파일러 기준)

 

p가 선언된 부분 int *p; 를 보고. 포인터는 4바이트니까 sizeof(p) -> 4

이렇게 한겁니다.

 

---------------------------------------------------------------------------

malloc(16)을 보면 16이라는 것을 알수 있는데, 컴파일러는 왜 4라고 처리하는가?

char arr[24];

printf("%d", sizeof(arr) );

배열 일때는 할당된 크기를 sizeof 값으로 처리했으면서.

 

 

malloc은 c언어 문법장치가 아닌, c언어 함수 입니다.

무슨말인가 하면, p = malloc(16);을 컴파일러 입장에서 보면.

"malloc 함수에 인자로 16을 주고, 리턴값을 p에 넣는다." 일 뿐이지 컴파일러에게 그 이상의 의미는 없습니다.

 

반면에 sizeof()는 함수가 아닌, c언어 문법장치 입니다.

그러므로 sizeof(p)가 "sizeof 함수에 p를 인자로 넣는다"가 아닌, "p의 바이트크기 상수값"을 의미하게 되는거지요.

 

배열일 때는, c언어 문법장치인 char arr[24]; 을 고려해서 sizof값을 정하는 것이고

포인터일 때는 c언어 문법장치인 int *p; 를 고려해서 sizof 값을 정하는 것입니다.

 

---------------------------------------------------

malloc 함수, 즉. 동적할당된 크기를 sizeof에 반영하지 않는 또다른 이유를 알아봅시다.

 

int *p;

int i;

scanf("%d", &i)

p = malloc(i);

printf("%d", sizeof(p) );

코드가 이러할때 sizeof(p)는 어떤 값을 가져야 할까요?

 

전에 말했듯이 sizeof는 c언어 문법장치이며, 함수도 아닙니다.

즉, 컴파일 될때 그 값이 결정되서 상수값으로 치환되어 컴파일 됩니다.

 

만약, malloc 으로 동적할당한 값을 sizeof 값으로 하려 한다면

컴파일 할때는 i의 값을 알수 없으므로 sizeof를 처리하는게 불가능 합니다.

 

이러한 이유도 있습니다.


출 처 : http://cafe.naver.com/haebop.cafe?iframe_url=/ArticleRead.nhn%3Farticleid=5173

제로 확장... unsigned 일 때
부호 확장... signed 일 때


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 부호 확장
#include 

int main()
{
	char A = 0x7A;
	int B = A;
	
	printf("B = %x\n", B);
	
	A = 0x8A;
	B = A;
	printf("B = %x\n", B);
	
	return 0;
}



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 제로 확장
#include <stdio.h>

int main()
{
	unsigned char A = 0x7A;
	unsigned int B = A;
	
	printf("B = %x\n", B);
	
	A = 0x8A;
	B = A;
	printf("B = %x\n", B);
    return 0;
}



1
2
3
4
5
6
7
8
9
10
11
// 확장
#include <stdio.h>

int main()
{
	char c = 0x8A;
	
	printf("%x\n", c);
	
	return 0;
}

printf 인자로 스택영역에 있는 c의 값을 넘겼다. 여기서 스택은 메모리에 있고 버스(32 bit)를 통과함으로 확장이 일어난다. c 변수 자체가 signed이기 때문에 부호 확장이 일어났음을 알 수 있다.


+ Recent posts