관리 메뉴

Kim's Programming

C언어 - 포인터 고급(1/4) 본문

Programming/C

C언어 - 포인터 고급(1/4)

Programmer. 2015. 8. 31. 21:18

Const


상수의 정의


키워드 const는 값을 변경할 수 없는 상수를 정의합니다. 기본형태는 다음과 같습니다.


const 타입 변수명 = 초기값;


변수를 선언하는 일반적인 문장과 비슷하되 앞에 const를 붙이고 뒤에 반드시 초기갑슬 적어야 한다는 점만 다릅니다. 다음 소스에서 보겠습니다.

1
2
3
4
5
6
7
8
9
10
#include<stdio.h>
 
const int HourPerDay = 24;
const int MinPerHour = 60;
const int SecPerMin = 60;
 
void main()
{
    printf("하루는 %d초 입니다. \n", HourPerDay*MinPerDay*SecPerDay);
}
cs

하루는 24시간이고 한시간은 60분 1분은 60초 라고 정의 했습니다. 24*60*60이라고 상수를 직접 쓰는 대신 HourPerday 등과 같은 이름이 있는 상수를 사용함으로써 소스를 읽기 쉬워지고 수정하기도 편리합니다. 이 값들은 모두 정수 상수로 선언되었기 떄문에 중간에 값을 변경할 수 없습니다. 따라서 HourPerDay=26;과 같은 대입문은 당장 에러로 처리되며 선언할 때 반드시 초기값을 지정해야합니다.

const키워드는 타입 다음에 붙일 수도 있으며 타입이 생략될 경우 int형으로 간주됩니다. 그래서 다음 세 문장은 모두 동일하게됩니다. 하지만 관습적으로 const는 타입 앞에 붙이는게 보기도 읽기도 좋아서 첫번째 형식을 주로 사용합니다.

1
2
3
const int Day=24;
int const Day=24;
const Day=24;
cs

const에 의해 만들어지는 상수는 컴파일시에 값이 결정되기 떄문에 배열의 크기를 지정에도 사용할 수 있습니다. 반면 변수는 실행 중에 값이 바뀔 수 있기 떄문에 배열 크기를 지정하는 용도로는 사용할 수 없습니다. const 예약어의 용도는 매크로 상수를 정의하는 #deinfe전처리문과 유사합니다. 상수에 좀 더 분명한 의미의 이름을 부여한다는 것과 자주 사용되는 상수를 한 곳에서만 정의함으로써 일괄적인 수정을 쉽게 한다는 점에서 기능상 동일하다고 할 수 있으며 사실 상호 대체 가능합니다. 하지만 const는 #define에 비해 다른과 같은 장점들을 가집니다.


    • #deinfe이 정의하는 매크로 상수는 타입을 지정할 수 없지만 const는 타입을 명확하게 지정할 수 있습니다. 위 예시에서 Day는 실수 24.0이나 문자열 24가 아닌 정수형의 24라는 것을 분명하게 지정합니다. C++은 타입을 중요시하기 떄문에 상수의 정확한 타입이 의미를 가지는 경우가 있습니다.

    • 매크로 상수는 일단 정의된 후에는 언제든지 어느 곳에서나 사용할 수 있지만 const는 통용 범위 규칙의 적용을 받기 떄문에 자신이 선언된 범위 내에서만 사용할 수 있습니다. 함수 내부에서 선언한 상수는 함수 내부에서만 사용할 수 있으며 함수 밖으로는 알려지지 않습니다. 즉 지역상수를 만들 수 있으며 명칭간의 충돌을 최소화 할 수 있습니다.

    • #define은 컴파일러가 아닌 전처리기에 의해 치환되기 떄문에 실제 소스에는 매크로가 치환된 상태로 실행됩니다. 그래서 디버깅 중에 매크로 상수의 값을 확인해 볼 수 없으며 아무리 간단한 버그라도 확인이 안 되면 잡기 힘듭니다. 반면 const 상수는 컴파일러가 처리하기 떄문에 디버깅 중에도 값을 확인해 볼 수 있어서 복잡한 단계를 통해 정의된 상수의 값도 쉽게 살펴볼 수 있습니다.

    • 매크로는 기계적으로 치환되기 떄문에 부작용이 발생할 소지가 많습니다. 괄호로 싸지 않으면 연산 순위에 의해 예상하지 못한 값이 될 위험이 있습니다. 하지만 const 상수는 컴파일러가 문맥에 맞게 처리하므로 이런 부작용이 거의 없습니다. #define A 1+2의 A는 3이 될 가능성이 있을 뿐 주변 연산문에 따라 3이 아닐 수도 있지만 const int A=1+2;는 어떤 경우에도 3이 됩니다.


#define이 C에서 사용하던 방법이라면 const는 C++에서 새로 도입된 진보된 방법입니다. 그래서 상수를 정의할 떄는 가급적이면 #define보다는 const를 사용할 것을 더 권장합니다. 그러나 const가 #define 보다는 더 좋은 방법이긴 하지만 #define도 나름 간편 편리하기 떄문에 금기시 할 필요는 없습니다.


포인터와 const


const를 포인터와 함께 사용하면 효과가 조금 달라집니다. 다음 소스는 포인터와 const의 관계를 알아보기위함인데 컴파일을 하면 몇몇에서 에러가 발생할 것입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include<stdio.h>
 
void main()
{
    int ar[5= { 1234};
 
    int *pi1 = &ar[0];                
    pi1++;                            //포인터는 다른 대상체를 가리킬 수 있음
    *pi1 = 0;                        //대상체를 변경할 수 있음
 
    const int *pi2 = &ar[0];
    pi2++;                            //포인터가 다른 대상체를 가리킬 수 있음
    *pi2 = 0;                        //에러 : 대상체가 상수이므로 변경할 수 없음
 
    int * const pi3 = &ar[0];        
    pi3++;                            //에러 : 포인터가 다른 대상체를 가리킬 수 없음
    *pi3 = 0;                        //대상체는 변경할 수 있음
 
    const int *const pi4 = &ar[0];
    pi4++;                            //에러 : 포인터가 다른 대상체를 가리킬 수 없음
    *pi4 = 0;                        //에러 : 대상체가 상수이므로 변경할 수 없음
}
cs
정수형 배열 ar이 선언이 되어있고 이 배열을 가리키는 포인터 4개를 선언하는데 const의 위치에 따라 상수가 되는 대상이 조금씩 달라집니다. 먼저 const를 쓰지 않은 일반적인 포인터 pi1을 보겠습니다. pi1은 ar[0]의 번지 즉, ar 배열의 첫 번째 요소를 가리키고 있습니다. 이 상태에서 pi1을 증가시키면 배열의 다른 요소를 가리키도록 이동하여 *pi1 연산산식으로 pi1이 가리키는 곳의 값을 읽거나 바꿀 수 있습니다. pi1 포인터 자체도 상수가 아니며 pi1이 가리키는 대상체도 상수가 아니므로 양쪽 모두 원하는대로 변경이 가능합니다.

다음으로 const int *타입으로 선언된 pi2를 보겠습니다. 이 포인터는 const int를 가리키는 포인터 변수로 선언되었으므로 이 포인터가 가리키는 대상체는 정수형 상수이며 포인터 자체는 상수가 아닙니다. 그래서 pi2++이나 pi2-- 또는 pi2에 다른 대상체의 번지를 직접 대입하여 pi2가 다른 대상체를 가리키도록 할 수는 있지만 *pi2를 변경하는 것은 허락되지 않습니다. 상수만 가리키는 포인터로 선언되었기 떄문에 이 포인터가 가리키는 곳의 내용을 변경할 수 없습니다. 이런 상수 포인터를 상수 지시 포인터 (Pointer to Constant)라고 합니다. 물론 pi2가 가리키고 있는 ar 배열 자체는 상수가 아니므로 ar[0]=0;과 같이 직접 이 배열 요소에 값을 대입하는 것은 가능합니다. 하지만 pi2 포인터로 간접적으로 배열 요소의 값을 변경할 수는 없습니다. 상수 지시 포인터가 가리키는 곳의 내용은 상수로 취급되므로 이 포인터를 사용하여 값을 바꾸는 것은 에러로 처리됩니다.

다음으로 const 예약어가 포인터 변수 앞에 사용된 pi3를 보겠습니다. int *const pi3로 선언되어 있는데 이 선언문은 pi3 포인터 변수를 상수로 만듭니다. 이런 포인터를 상수 포인터(Constant Pointer)라고 하며 변수 자체가 상수이므로 다른 대상체를 가리킬 수 없지만 이 포인터가 가리키는 대상체는 상수가 아니므로 대상체의 값을 변경하는 것은 가능합니다.

마지막 4번째 pi4는 const가 대상체와 포인터 변수에 모두 사용되었는데 이렇게 선언하면 포인터 변수 자체도 상수이고 대상체도 상수로 취급됩니다. 이름을 붙이면 상수 지시 상수 포인터라고 할 수 있습니다. 그래서 pi4는 최초 선언될 때 주어진 ar[0] 이 외의 다른 대상체를 가리킬 수 없으며 ar[0]의 값만 읽을 수 있습니다.

포인터 선언시 const 키워드로 상수 포인터를 만드는 4가지 경우를 살펴봤는데 헷갈리기도 합니다. const 키워드의 위치에 따라 상수가 되는 대상이 달라지는데 pi2와 pi3의 경우만 잘 구분하면 됩니다. const 키워드 바로 다음 대상이 상수가 되는데 타입 앞에 있으면 대상체가 상수가 되며 변수 앞에 있으면 변수만 상수가 됩니다.

int const *는 const int *와 같은 표현이며 대상체가 상수인데 const 위치가 직관적이지 않아서 잘 사용되지 않습니다. 다음 한 단계 확장해서 이중 포인터와 const 관계를 보겠습니다. 이중 포인터의 경우는 const int * const * const ppi; 처럼 const를 세 군데나 붙일 수 있고 위치에 따라 const의 의미는 달라집니다.

    • 제일 앞 : ppi가 가리키는 포인터가 가리키는 정수가 상수라는 뜻

    • 중간 : ppi가 가리키는 포인터가 상수라는 뜻

    • 제일 끝 : ppi 자체가 상수 포인터 라는 뜻

다음 소스에서는 const 위치에 따라 이중 포인터가 가지는 의미와 금지된 동작을 알아 보겠습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include<stdio.h>
 
void main()
{
    int i = 5;
    int *pi = &i;
    const int *pci;
    int *const cpi = &i;
    const int * const cpci = &i;
 
    // 일반 이중 포인터 - 모두 가능
    int **ppi1 = &pi;
    ppi1++;
    (*ppi1)++;
    **ppi1 = 0;
 
    //상수 지시 포인터의 포인터
    const int **ppi2 = &pci;
    ppi2++;
    (*ppi2)++;
    **ppi2 = 0;    //에러 : 최종 대상체(정수) 변경 불가
 
    //비상수 지시 상수 포인터의 포인터
    int * const * ppi3 = &cpi;
    ppi3++;
    (*ppi3)++;    //에러 : 중간 대상체(포인터) 변경 불가
    **ppi3 = 0;
 
    //비상수 지시 비상수 포인터의 상수 포인터
    int **const ppi4 = &pi;
    ppi4++;
    (*ppi4)++;    //에러 : 포인터 자체 변경 불가
    **ppi4 = 0;
 
    //상수 지시 상수 포인터를 지시하는 상수 포인터 - 전부 에러
    const int * const *const ppi5 = &cpci;
    ppi5++;
    (*ppi5)++;
    **ppi5 = 0;
}
cs
const 바로 다음 대상이 상수가 된다는 원칙은 동일합니다. 이중 포인터에 대해 const 키워드를 쓰는 경우는 드물지만 포인터의 레퍼런스에 대해서는 종종 이런 표현이 사용되기도 합니다. 그렇다면 왜 상수 포인터가 필요한 걸까요? const int *pi2의 대상체가 상수라는 것을 알 고 있다면 *pi2의 값을 바꾸지 않으면 그만일텐데요 컴파일러 차원에서 이런 상수 포인터를 지원하는 이유는 고의든 실수든 바뀌지 말아야 할 중요한 값을 보호하기 위해서입니다. 다음 코드에서 RATIO는 어떤 계산식에 사용되는 중요한 상수입니다
1
2
3
4
const double RATIO=82.34;
RATIO=99.99//에러
const douvle *pd=&RATIO
*pd=99.99 //에러
cs
상수로 선언을 했으므로 두 번째 줄처럼 RATIO에 어떤 값을 직접 대입하는 것은 당연히 에러로 처리됩니다. 눈에 뻔히 보이는 상수에 값을 대입하는 실수는 하지 않지만 포인터를 통해 간접적으로 이 값을 엑세스 할때는 우발적으로 실수를 할 수 있습니다. RATIO는 상수라서 좌변값이 아니지만 메모리를 점유하고 있고 번지가 있으므로 &연산자의 피 연산자로 사용될 수 있습니다.

만약 이 상수를 가리키는 포인터 pd를 사용하여 간접적으로 RATIO의 값을 바꿀 수 있다면 이것은 잠재적으로 논리적인 에러가 될 위험이 아주 큽니다. 상수를 가리키는 포인터를 함수끼리 주거니 받거니 하다 보면 언제 이 값이 바뀌어 버릴지 알 수 없어집니다. 상수를 가리키는 포인터를 함수까리 주거니 받거니 하다보면 언제 이 값이 바뀔지는 알 수 없어집니다.

포인터는 대상체를 옮겨 다니며 값을 읽기도 하고 바꿀 수도 있는 굉장히 편리한 도구이지만 다소 위험한 면이 있습니다. 어떤 대상체에 대한 번지를 알고 있으면 포인터를 통해 이 대상체를 마음대로 바꿀 수 있기 때문입니다. 일부러 그렇게 하지는 않지만 실수로 포인터를 잘못 조작하거나 또는 외부적인 다른 에러에 의해 포인터가 엉뚱한 곳을 가리킬 가능성은 항상 있습니다. 짧은 코드에서 이런 실수를 하더라도 금방 어디서 문제가 발생했는지를 찾겠지만 대형 프로젝트에서는 실수한 곳과 증상이 발생하는 곳이 딴 곳일 수 있기 떄문에 발견하기 대단히 어렵습니다. 게다가 여러사람이 동시에 작업을 한다고 하면 더 곤란해 질 것입니다. 그래서 컴파일러 차원에서 포인터를 좀 더 안전하게 쓸 수 있는 안전장치 역할을 하는 것이 const입니다.

const는 읽기만 가능한 읽기 전용 포인터를 정의할 수 있도록 함으로써 포인터를 잘못 조작하는 실수를 컴파일러가 발견할 수 있도록 합니다. 구조체를 함수의 인수로 넘길 때는 값으로 넘기는 것보다 포인터로 넘기는 것이 훨씬 더 효율적인데 이렇게 하면 참조 호출이므로 함수 내부에서 실인수를 변경할 수 있어 위험해집니다. 이것을 금지하고 싶을 때 읽기 전용의 상수 지시 포인터로 전달합니다. 4바이트만 전달되므로 속도가 빠르고 함수가 실인수를 읽을 수만 있으므로 안전합니다. 컴파일러는 const 포인터의 안전성을 높이기 위해 const 포인터와 일반 포인터 끼리의 대입에 대해 엄격한 규칙을 적용합니다. 다음 소스를 보겠습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
#include<cstdio>
 
void main()
{
    int ar[5= { 1234};
 
    int *pi1 = &ar[0];
    const int *pi2;
 
    pi2 = pi1;                //가능
    pi1 = pi2;                //불가능
    pi1 = (int *)pi2;        //가능 그러나 바람직하지 못함
}
cs
pi1은 일반 포인터이며 ar[0]를 가리키도록 초기화되었고 pi2는 상수 지시 포인터입니다. 이 상태에서 pi2가 pi1의 번지를 대입받는 것은 가능합니다. 상수 지시 포인터로는 대상체를 읽을 수만 있기 떄문에 읽기 쓰기가 모두 가능한 pi1 대상체의 번지를 대입받아도 아무 문제가 없습니다. 하.지.만! 그 반대는 허가되지 않는데 일반 포인터 pi1이 상수 지시 포인터 pi2의 번지를 대입받을 수는 없습니다. 만약 이것이 허가된다면 상수 지시 포인터를 지원하는 의미가 없어집니다. 상수 대상체를 가리키는 포인터와 동일한 타입의 일반 포인터를 선언한 후 이 포인터로 상수 대상체의 값을 바꿀 수 있다면 대상체를 제대로 보호할 수 없으며 그래서 상수 지시 포인터를 일반 포인터에 대입하는 것은 금지되어 있습니다.

만약 일반포인터가 정 상수 포인터의 번지를 대입받도록 하고 싶다면 캐스트 연산자를 사용할 수는 있습니다. 상수지시 포인터타입(const int *)을 강제로 일반 포인터 타입(int *)로 캐스팅하여 대입하면 일단 대입은 가능합니다. 캐스트 연산자까지 동원해서 타입을 바꾸고자 한다면 컴파일러가 이를 굳이 말리진느 않습니다. 물론 이런 코드는 바람직하지 않으며 꼭 필요할 때만 확실하게 안전하다는 검증을 한 후 사용해야합니다.

const 지정은 컴파일 타임에 컴파일러에 의해 지원됩니다. 컴파일러는 변수의 상수성을 보고 변경할 수 없는 값을 변경하는 코드에 대해 사정없이 에러 메세지를 출력합니다. 그래서 보통의 정상적인 방법으로는 상수를 변경할 수 없습니다. 그렇다면 정상적이지 못한 변칙 코드로는 상수를 변경할 수도 있다는 이야기인데 런타입에 상수의 포인터로 간접적으로 값을 변경하는 것은 가능합니다. 다음 소스에서 보겠습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<cstdio>
 
void main()
{
    const int i = 2;
    int *pi = (int *)&i;
    *pi = 3;
    printf("i=%d\n", i);
    printf("*pi=%d\n"*pi);
 
    const double d = 1.0;
    double *pd = (double *)&d;
    *pd = 2.3;
    printf("d=%f\n", d);
    printf("*pd=%f\n"*pd);
}
cs
int, double 형 변수를 선언하고 비상수 포인터에 강제로 이 상수의 번지를 대입한 후 포인터로 값을 변경해보았습니다. 이런 동작은 스펙에 정의되어 있지 않으므로 결과는 컴파일러마다 다릅니다. 비주얼 스튜디오 에서의 결과는 다음과 같이 나옵니다.


double값은 바뀌지만 int 값은 바뀌지 않는 것 처럼 보입니다. 그러나 내부를 들여다보면 int도 실제로는 값이 바뀌지만 상수 참조문이 원래 정의된 상수값을 곧바로 사용하도록 컴파일하는 것입니다. const 지정된 상수를 포인터로 강제로 변경할 경우의 동작에 대해서는 정의되어 있지 않으므로 어떻다 라고 말할 수는 없습니다. 커마일러 개발자들은 스펙 문서대로 컴파일러를 작성할 뿐이므로 정의되지 않은 코드가 어떻게 컴파일될지 알 수 없는 것입니다. 따라서 우리는 이런 이식성이 없는 코드를 작성하면 안됩니다. 다음은 실수로 전달된 상수 지시 포인터의 상수성을 변경하여 실인수를 변경해보겠습니다. 함수는 임의의 실인수에 대해 동작할 수 있어야 하므로 컴파일러가 원래의 상수값을 특정한 값으로 가정할 수 없어 앞의 예제와는 효과가 조금 다릅니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<stdio.h>
void function(const int *ai)
{
    //*ai = 3;
    int *pi;
    pi = (int *)ai;
    *pi = 3;
}
void main()
{
    int i = 2;
    function(&i);
    printf("%d\n", i);
}
cs
func는 const *int를 전달받음으로 *ai=3 식으로 이 포인터가 가리키는 내용을 직접 변경하는 것은 당연히 불법입니다. 그러나 비상수 포인터 pi로 강제 변경하는 것은 가능합니다. 컴파일러는 pi가 가리키는 내용이 선언할 때부터 상수인지 아니면 실인수는 비상수인데 함수로 전달될 때만 상수인지는 전혀 분간 할 수 없으므로 *pi=3호출에 대해 무조건 값을 변경할 수 밖에 없습니다. 이 코드는 거의 대부분의 컴파일러에서 동작합니다.

const 인수

앞에서 우리는 이미 const 포인터를 인수로 받아들이는 함수를 본 적이 있는데 이제 이 함수를 분석해 보겠습니다. 다음 함수들이 상수 포인터를 취하는 대표적인 함수들입니다.

1
2
3
4
char *strcpy(char *dest, const char *src);
int strcmp(const char *s1, const char *s2);
char *strchr(const char *string, int c);
int atoi(const char *string);
cs


strcpy 함수는 두 개의 문자열을 인수로 가지는데 dest는 상수가 아니고 src는 상수로 되어 있습니다. 이 함수는 src의 내용을 dest에 그대로 복사하는데 함수 호출 후에 dest의 내용이 바뀌므로 dest는 당연히 상수가 아닙니다. 하지만 src는 이 함수 내부에서 읽기만 하며 내용을 변경하지 않으므로 상수 지시 포인터로 되어 있습니다. 이 함수의 원형을 보면 함수가 리턴된 후에도 src의 내용이 그대로 유지된다는 것을 알 수 있으며 따라서 다음과 같은 코드를 안심하고 쓸 수 있습니다.

1
2
3
4
5
6
char src[32]="const pointer";
char dest[32];
 
strcpy(dest,src);
// 호출 후에도 src는 여전히 const pointer를 유지함
puts(src);
cs

strcpy 다음에 있는 puts 함수 호출에서 출력되는 문자열은 여전히 "const pointer"이며 이 문자열을 반복적으로 계속 사용해도 됩니다. 그러나 dest는 strcpy호출 후에 값이 변할 수 있어야 하기 떄문에 다음과 같은 호출문은 에러로 처리됩니다.

1
2
const char *dest="상수 포인터";
strcpy(dest,"const pointer");
cs

strcpy 함수의 dest 인수는 일반 포인터인데 상수 지시 포인터를 인수로 넘겼습니다. 인수 전달 과정에서 대입 연산이 일어나는데 일반 포인터는 상수 지시 포인터를 대입받을 수 없으므로 제대로 컴파일 되지 않는 것입니다. strcmp 함수는 문자열을 비교만 하고, 변경은 하지 않으므로 두 인수가 모두 상수 지시 포인터 입니다. strchr 함수도 문자열 검색만 하므로 상수 지시 포인터를 인수로 취하며 atoi 함수도 문자열에 저장된 값을 읽기만 합니다.


그러나 strchr 함수의 두 번쨰 인수 int c 앞에는 const가 붙어 있지 않습니다. 논리상 검색을 위해 전달되는 문자도 읽기 전용이므로 const int c라고 해야 옮겠지만 상수가 아닌 변수로 전달받습니다. 이렇게 되면 strchr함수의 두 번쨰 인수 int c앞에는 const가 붙어있지 않습니다. 논리상 검색을 위해 전달되는 문자도 읽기 전용이므로 const int c 라고 해야 옳겠지만 상수가 아닌 변수로 전달받습니다. 이렇게 되면 strchr함수 내부에서 c의 값을 함부로 바꾸게 될 것입니다. 그러나 이 경우는 값에  의한 전달이므로 strcpy 함수 내부에서 아무리 c를 바꿔봐야 실인수를 바꿀 수는 없으므로 위험하지 않습니다. const인수가 꼭 필요한 경우는 포인터를 통한 참조 호출일 때뿐이며 값 호출일때는 큰 의미 없습니다.


표준 함수 들은 자신이 전달받은 포인터의 대상체를 변경하지 않을 때 항상 const로 인수를 전달받도록 되어 있습니다. 이는 함수 내부에서 인수의 값을 바꾸지 않는다는 것을 명확히 표하며 함수 호출부에서 const로 전달된 인수의 값을 안심하고 재사용할 수 있도록 해줍니다. 사용자가 직접 함수를 만들 때도 변경할 필요가 없는 인수는 가급적이면 const로 전달받는 것이 좋습니다.


volatile


volatile 키워드는 const와 함께 변수의 성질을 바꾸는 역할을 하는데 이 둘을 묶어서 cv 지정자(Qualifier: 제한자)라고 합니다. const에 비해 상대적으로 사용 빈도가 지극히 낮으며 이 키워드가 꼭 필요한 경우는 무척 드뭅니다. volatile이 필요한 경우를 보겠습니다.

1
2
3
4
5
6
7
8
int i;
double j;
 
for (i = 0; i < 100; i++)
{
    j = sqrt(2.8+ log(3.5+ 56;
    //do something
}
cs

이 코드는 루프를 100번 실행하면서 어떤 작업을 하는데 루프 내부에서 j에 복잡한 연산 결과를 대입하고 있습니다. j값을 계산하는 이식이 조금 복잡하지만 제어 변수 i값을 참조하지 않기 떄문에 i루프가 실행되는 동안 j의 값은 상수나 마찬가지며 절대로 변경되지 않습니다. 루프 실행 중에는 상수이므로 이 값을 매 루프마다 다시 계산하는 것은 시간낭비 입니다. 그래서 제대로된 컴파일러는 이 루프를 다음과 같이 수정하여 컴파일합니다.

1
2
3
4
5
6
7
int i;
double j;
= sqrt(2.8+ log(3.5+ 56;
for (i = 0; i < 100; i++)
{
    //do something
}
cs

j의 값을 계산하는 식을 루프 이전으로 옮겨서 미리 계산해 놓고 루프 내부에서는 j값을 사용하기만했습니다. 루프안에서의 j값이 바뀌지 않기 떄문에 동일한 동작을 하게 됩니다. 훌륭한 컴파일러는 프로그래머가 코드를 대충 짜더라도 속도를 높이기 위해 자동으로 최적화를 하는 기능을 가지고 있으며 이런 암묵적인 최적화 기능에 의해 프로그램의 성능이 향상됩니다. 그렇다면 위 두 코드가 완전히 동일할까 라는 의심을 가져보겠습니다. j는 분명 루프 내부에서 상수이므로 미리 계산해 놓아도 아무 문제가 없음이 확실합니다. 그러나 아주 특수한 경우 최적화된 코드가 원래 코드와 다른 동작을 할 경우가 있습니다. 어떤 경우인가하면 프로그램이 아닌 외부에서 j 값을 변경할 떄입니다.


도스 환경에서는 인터럽트라는 것이 있고 유닉스 환경에서는 데몬, 윈도우즈 환경에서는 서비스 등의 백그라운드 프로세스가 항상 실행됩니다. 이런 백그라운드 프로세스가 메모리의 어떤 상황이나 전역변수들을 변경할 수 있으며 같은 프로세스 내에서도 쓰레드가 여러개라면 다른 쓰레드가 j의 값을 언제든지 변경할 가능성이 있습니다. 또한 하드웨어에 의해 전역 환경이 바뀔 수도 있습니다.


예를 들어서 위 코드를 실행하는 프로세스가 두 개의 스레드를 가지고 있고 다른 쓰레드에서 어떠 조건에 의해 전역변수 j값 (또는 j에 영향을 미치는 값)을 갑자기 바꿀 수도 있다고 하겠습니다. 이 경우 루프 내부에서 매번 j값을 다시 계산하는 것과 루프에 들어가기 전에 미리 계산해 놓는 것이 다른 결과를 가져올 수 있습니다. i 루프가 50회쨰 실행 중에 다른 쓰레드가 j를 바꾸어 버릴 수도 있는 것입니다. 이런 경우에 쓰는 것이 바로 volatile입니다. 이 키워드를 변수 선언문 앞에 붙이면 컴파일러는 이 변수에 대해서는 어떠한 최적화 처리도 하지 않습니다. 컴파일러가 보기에 코드가 비효율적이건 어쨋건 개발자가 작성한 코드 그대로 컴파일합니다. 즉 volatile 키워드는 그냥 있는 그대로 하라는 뜻입니다. 어떤 변수를 다른 프로세스나 쓰레드가 바꿀 수도 있다는 것을 컴파일러는 알 수 없기 떄문에 전역 환경을 참조하는 변수에 대해서는 개발자가 volatile 선언을 해야합니다. 위 코드에서 j 선언문 앞에 volatile만 붙이면 문제가 해결됩니다.

1
volatile double j;
cs

이 키워드가 반드시 필요한 상황에 대한 소스를 만드는 것은 굉장히 어렵습니다. 왜냐하면 외부에서 값을 바꿀 가능성이 있는 변수에 대해서만 이 키워드가 필요한데 그런 예제는 보통 크기가 아니기 떄문입니다. 잘 사용되지는 않습니다.

'Programming > C' 카테고리의 다른 글

C언어 - 포인터 고급(3/4)  (0) 2015.09.06
C언어 - 포인터 고급(2/4)  (0) 2015.09.05
C언어 - 구조체(Structure)(2/2)  (23) 2015.08.21
C언어 - 구조체(Structure)(1/2)  (1) 2015.08.20
C언어 - 포인터(Pointer)(3/3)  (0) 2015.08.19