지시/주소/인덱스/간접연산자
다루는 내용
- 지시 연산자
- 주소 연산자
- 인덱스 연산자
- 간접 연산자
지시 연산자는 매커니즘 형식의 변수 선언시 해당 변수가 어떠한 형식인지를 지시하는 연산자이다.
포인터 형식을 지시하는 *, 배열 형식을 지시하는 [], 함수를 지시하는 ()가 있다.
정의, 선언, 초기화, 대입
선언은 "이러한 것이 있다."는 것이고 정의는 "이러한 것은 이것이다."라고 규정짓는 것을 말한다.
int Foo(int ,int ); //"이와 같은 함수가 있다." - Foo라는 명칭에 대한 선언문
// "Foo라는 함수명은 이러한 매개변수와 이러한 리턴형식을 갖는다." -Foo라는 명칭의 signature 정의문
int Foo(int a,int b) //"이 함수는 이러한 코드를 수행한다." - (해당 함수에서 하는 연산 행위에 대한) 정의문
{
return a+b;
}
#define MAX_STU_NUM 50 //"MAX_STU_NUM이 있다.", "MAX_STU_NUM을 50이라 정의한다." -선언문이면서 정의문
struct _Foo//"struct _Foo는 다음과 같은 멤버로 구성됨을 정의한다." - 정의문
{
int m; //"이러한 멤버가 있다." - 선언문
};
int i; //?
"이러한 변수가 있다."라는 시각에선 선언문이다. - 변수명에 대한 선언문
"변수 i의 형식은 int이다."라는 시각에선 정의문이다. - 변수명의 형식에 대한 정의문
또한, 변수는 선언으로 인해 메모리를 할당 받게 되며 이는 어떠한 값이든 갖게 됨을 의미한다. 즉, 전역 변수일 경우에는 변수명 선언과 동시에 값이 0으로 정의가 되는 것이고 지역 변수일 경우에는 선언과 동시에 gabage value로 정의가 되는 것이다.
그러므로 변수를 선언한다는 것은 변수명을 선언한다는 것이 좀 더 정확한 표현이며 "변수명의 형식을 정의한다." 혹은 "변수명을 위해 할당된 공간에 값이 ~으로 정의된다."라고 얘기할 수도 있다.
이러한 용어의 혼돈을 피하기 위해서 변수명을 선언시 값을 정의하는 것을 초기화라고 부른다.
그리고 변수의 선언문 이외에 변수의 값을 변경하는 것을 대입(Assign, 변수에 식값 혹은 상수값을 할당)이라 한다.
Look & Feel & Think
|
통합 개발환경에서 나오는 에러메시지는 재정의에 대한 언급만 나와있지만 분명하게 '재정의. 기본형식이 다릅니다.'라는 설명이 있다.
MSDN을 살펴보면 '식별자를 이미 선언했습니다.'라는 사실을 추가적으로 발견할 수 있다.
여기서 중요한 사실은 본인이 바로보는 논리적인 자대가 있어야 될 것이며 이를 일반화하여 상위ontology와 부합될 수 있도록 노력하는 자세가 중요할 것이다. 이러한 노력을 통해 자신과 다른 시각으로 바라보거나 인식하는 것을 포용을 하여 하나의 ontology속에서 생존할 수 있다는 것이 부족한 설명에 대한 저자의 핑계이자 소견이다.
☞오해에 대한 우려
class Foo
{
const int cv = 5; //①
};
const int Foo::cv; //②
①은 선언문인가? 정의문인가? 초기화 구문인가?
"class Foo스코프에 cv라는 것이 있으며 정의되어 질 때 5로 초기화 해 달라." 는 의미이므로 선언문이 맞다.
②는 선언문인가? 정의문인가? 초기화 구문인가?
"실제 Foo스코프에 선언한 ①은 ②이다." - 정의문
이미 ①에 의해 선언된 것을 나타내어 메모리를 할당을 받아 선언문에 명시된 초기값으로 정의가 되어지는 것이다.
Effective C++3판에 이에 대한 해석에 대한 질문을 종종 받을 때가 있다.
☞에피소드
학생 - "강사님, 이 책에는 강사님이 쓰는 정의와 선언이 거꾸로 나온 것 같습니다."
지기 - "강사님은 int i; 를 읽을 때 'int형식의 변수 i를 선언하였다.'라고 하지 않습니까?"
학생 - "이 책에는 const int Foo::cv;구문을 정의문이라고 되어 있는데..."
지기 - "분명히 Effective C++의 번역은 정확한 표현임에 틀림이 없고 내가 읽은 것도 틀림이 없는데"
학생 - "OTL"
여기서는 지시 연산자를 중심으로 살펴보면서 주소, 인덱스, 간접 연산자에 대해 Look & Feel & Think를 하도록 하자.
먼저, 다음의 a에서부터 f까지는 각각 무엇을 의미하는지 해독해 보라.
int (*a)[10];
int *b[10];
int (*c)(int,int);
typedef int (*d)(int,int);
int (*e[4])(int,int);
void (*f(int, void (*fa)(int)))(int);
지시문이 있을 때에는 해당 구문을 분석할 때 명명한 명칭을 중심으로 해석해 나가자.
명칭뒤에 ()오면 함수, []오면 배열이다. 이들이 오지 않은 상태에서 앞에 *이 오면 포인터이다.
물론 지시문 맨 앞에 typedef이 오면 해당 명칭은 새로운 타입명이 된다.
int (*a)[10];
명칭 a가 ()에 묶여 있기 때문에 해석 또한 ( )내부를 먼저해야 한다.
a뒤에 ()나 [] 오지 않았으며 명칭앞에 *이 있으므로 a는 포인터 변수이다.
명칭 a와 지시 연산자를 제외한 int [10];이 원소 타입이다.
Look & Feel & Think
위의 59번 라인 int number_stu[3][10];은 10명으로 구성된 3개반의 학생 번호를 보관하기 위한 배열 선언이다. AllocStuesNum은 학생 번호를 부여하기 위한 함수이고 PrintStuesNum은 부여된 학생 번호를 확인하는 함수이다. 그렇다면 해당 함수에서는 number_stu를 어떠한 타입으로 받아야 할까? 먼저 int nubber_stu[3][10];을 해석해 보자. 배열명number_stu 뒤에 []안에 있는 것이 원소의 개수이고 나머지 int [10]이 원소 타입이다. 즉, number_stu는 10개의 정수형 데이터를 보관할 수 있는 배열을 원소로 하고 원소의 개수가 3이다. 배열명은 원소형식에 대한 포인터 형식을 값으로 지니고 있다고 형식에 대한 설명하는 항목에서 이미 언급한 바가 있다. 그렇다면 number_stu를 매개변수로 받기 위해서는 원소에 대한 포인터 타입으로 받으면 될 것이다. 결론적으로 int (*a)[10]; 이와 같은 형태가 될 것이다.
32번째 항목의 baseclass[i]는 무엇을 의미할까? 지시문 이외에서 나오는 []연산자는 인덱스 연산자로 피 연산자중 하나는 메모리 주소가 다른 하나는 정수값이 오게 된다. 해당 연산의 결과는 해당 메모리 주소에서 원소타입을 거리1로 했을 때의 상대적인 거리에 있는 원소이다. 결론적으로 arr[i]와 *(arr+i)는 동치이다. 즉, baseclass[i]의 원소는 int [10]이므로 baseclass메모리 주소에서 sizeof(int)*10 *i에 해당하는 메모리 주소가 된다. 이는 인덱스 i에 해당하는 학급의 첫번째 학생이 있는 위치가 된다. 주의) 원소의 사이즈가 없거나 계산 되어 질 수 없는 경우에는 인덱스 연산자를 사용할 수가 없다. 예: void *
위 함수는 쉽게 해독이 가능하리라 믿고 설명을 생략한다. 만약 해독이 힘들다면 아직 여기를 볼 때가 아니니 앞으로 돌아가길 바란다.
위 함수는 쉽게 해독이 가능하리라 믿고 설명을 생략한다. 만약 해독이 힘들다면 아직 여기를 볼 때가 아니니 앞으로 돌아가길 바란다.
위는 실행 결과이다.
참고로 void Foo(int basea[][10],int a) 와 void Foo2(int (*baseb)[10],int a)의 차이점은 basea는 배열명으로 포인터 상수로 취급이 되며 baseb는 포인터 변수라는 차이가 있다. |
int *b[10];
명칭 b뒤에 []이 오기 때문에 b는 배열명이다.
배열명 바로 뒤에 오는 []내부가 원소의 개수이며 이를 뺀 나머지 int *가 원소 타입이다.
그렇다면 int *g[10][5];는 어떻게 해석해야 할까?
명칭 g뒤에 []이 오기 때문에 g는 배열명이다.
배열명 바로 뒤에 오는 []내부가 원소의 개수이며 이를 뺀 나머지 int * [5];가 원소 타입이다.
Think & Think & Think 50명의 번호순으로 학생 데이터를 관리하고 성적순으로도 관리를 하고 싶다면 어떻게 할 것인가? Stu orderbynum[50];으로 번호순으로 성적을 보관하고 Stu orderbyscore[50];로 성적순으로 보관을 하면 간단할 수도 있다. 즉, 한 명의 데이터를 원본과 복사본을 두겠다는 것이다.
하지만, 이렇게 되면 특정 기능을 수행하면서 번호순으로 관리되는 학생 자료를 변경을 시키고 성적순으로 관리되는 학생 자료를 갱신을 하지 않는다면 논리적 모순이 생기게 된다. 이는 실제 프로그래밍에서 잘못된 것을 개발시에 확인을 못하고 사용자가 사용을 하면서 알게 될 확률도 높을 수 밖에 없다. 즉, 유지보수 비용이 많이 들 수 있다는 것이다.
Stu orderbynum[50];에 학생 데이터를 관리하고 Stu*orderbyscore[50];와 같이 선언하여 성적순으로 학생 데이터의 위치 정보를 관리를 한다면 어떠한 방향으로든 학생 데이터를 변경하였을 때 앞에서의 문제는 생기지 않게 된다.
논리적으로 분리될 수 없는 것을 나누어 정의하지 말 것이며 논리적으로 분리될 수 있는 것을 한 곳에서 모두 정의하지 말아야 유지보수가 용이하고 가독성 및 재 사용성이 높은 프로그램이 될 수 있을 것이다. |
아래의 사항과 같은 것도 과연 우리가 해석할 줄 알아야 하며 과연 프로그래밍에서 활용을 하는 것일까? 그 대답은 Yes이다. 다만, 이를 문법적으로 이해하고 있는다고 하더라도 프로그래밍에 활용하지 못하면 죽은 지식이 될 것이다. 여기서는 짧게 원리만 살짝 살펴보고 이후에 배열과 포인터 및 일반화, 프로그래밍에 관련된 토픽을 다룰 때 이들의 활용에 대해 다루도록 하겠다.
int (*c)(int,int);
명칭 c가 ()에 묶여 있기 때문에 해석 또한 ()내부를 먼저해야 한다.
명칭 c뒤에 ()나 []이 오지 않았으며 명칭앞에 *이 있으므로 c는 포인터 변수이다.
명칭 c와 지시연산자를 제외한 int (int,int);부분이 원소 타입이다.
typedef int (*d)(int,int);
typedef이 맨 앞에 왔기 때문에 d는 새로운 타입명이 된다.
그 외에 int (*)(int,int);부분이 이전 타입이 된다.
int (*e[4])(int,int);
명칭 e뒤에 []이 왔으므로 e는 배열명이다.
배열명 e뒤에 []에는 원소의 개수이고 그 외 int (*)(int,int);는 원소 타입이다.
void (*f(int, void (*fa)(int)))(int);
명칭 f뒤에 ()이 왔으므로 f는 함수명이다.
함수명 뒤에 ()안에 오는 것은 입력 매개변수리스트이고 나머지는 리턴타입이다.
즉, 입력 매개변수 리스트는 2개로 int, void (*fa)(int) 이다.
리턴 타입은 이들을 뺀 나머지 void (*)(int)이다.
'언어 자료구조 알고리즘 > C 언어 문법' 카테고리의 다른 글
21.제어문 - 선택문 (0) | 2009.08.19 |
---|---|
20. 제어문 - 조건문 (0) | 2009.08.19 |
19. 기본입출력 - 입력 (0) | 2009.08.19 |
18. 기본 입출력 - 출력 (0) | 2009.08.19 |
17.기본 입출력 개요 (0) | 2009.08.19 |
15. 비트/ 쉬프트 연산자 (0) | 2009.08.19 |
14. 비교/논리 연산자 (0) | 2009.08.19 |
13. 대입 연산자 (0) | 2009.08.19 |
12. 증감연산자 (0) | 2009.08.19 |
11. 산술 연산자 (0) | 2009.08.19 |