일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 수광 소자
- list
- stl
- 자료구조
- html
- set
- directx
- 아두이노 소스
- 시스템프로그래밍
- 컴퓨터 그래픽스
- priority_queue
- vector
- arduino compiler
- Visual Micro
- Array
- Deque
- LineTracer
- Algorithm
- 운영체제
- Arduino
- 라인트레이서
- C언어
- queue
- 통계학
- WinAPI
- Stack
- 아두이노
- map
- c++
- 아두이노 컴파일러
- Today
- Total
Kim's Programming
C언어 - 포인터 고급(1/4) 본문
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
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] = { 1, 2, 3, 4, 5 }; 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 |
- 제일 앞 : ppi가 가리키는 포인터가 가리키는 정수가 상수라는 뜻
- 중간 : ppi가 가리키는 포인터가 상수라는 뜻
- 제일 끝 : ppi 자체가 상수 포인터 라는 뜻
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 = π ppi1++; (*ppi1)++; **ppi1 = 0; //상수 지시 포인터의 포인터 const int **ppi2 = &pci; ppi2++; (*ppi2)++; **ppi2 = 0; //에러 : 최종 대상체(정수) 변경 불가 //비상수 지시 상수 포인터의 포인터 int * const * ppi3 = &cpi; ppi3++; (*ppi3)++; //에러 : 중간 대상체(포인터) 변경 불가 **ppi3 = 0; //비상수 지시 비상수 포인터의 상수 포인터 int **const ppi4 = π ppi4++; (*ppi4)++; //에러 : 포인터 자체 변경 불가 **ppi4 = 0; //상수 지시 상수 포인터를 지시하는 상수 포인터 - 전부 에러 const int * const *const ppi5 = &cpci; ppi5++; (*ppi5)++; **ppi5 = 0; } | cs |
1 2 3 4 | const double RATIO=82.34; RATIO=99.99; //에러 const douvle *pd=&RATIO *pd=99.99 //에러 | cs |
1 2 3 4 5 6 7 8 9 10 11 12 13 | #include<cstdio> void main() { int ar[5] = { 1, 2, 3, 4, 5 }; int *pi1 = &ar[0]; const int *pi2; pi2 = pi1; //가능 pi1 = pi2; //불가능 pi1 = (int *)pi2; //가능 그러나 바람직하지 못함 } | cs |
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 |
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 |
앞에서 우리는 이미 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; i = 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 |