F.R.I.D.A.Y.

[TIPS 19TH] 06 : 2018.07.13. (목) 본문

외부활동/TIPS 19th

[TIPS 19TH] 06 : 2018.07.13. (목)

F.R.I.D.A.Y. 2018. 7. 13. 16:30
반응형

1. 포인터 상수

 일반 변수와 마찬가지로 포인터도 선언시에 상수로 선언할 수 있다. 종류는 다음과 같다.


int *p;                   // 1번
const int *p;             // 2번
int * const p = &a;       // 3번
const int * const p = &a; // 4번


const가 어느 위치에 들어가느냐에 따라 기능이 달라져서 처음 접할 때는 굉장히 혼란스러울 수 있다. 나도 그랬다.

 1번의 경우에는 일반적인 포인터 선언 방법이다.

 2번은 가리키는 대상의 값을 변경할 수 없는 포인터 p를 선언하는 방법이다. p의 값은 변경할 수 있지만, p가 가리키는 대상의 값은 변경할 수 없다.

 3번은 가리키는 대상의 값은 변경할 수 있지만 가리키는 대상은 변경할 수 없는 포인터 p를 선언하는 방법이다.

 4번은 가리키는 대상의 값도, 자신의 값도 변경시킬 수 없는 포인터 p를 선언하는 방법이다.


 간단히 말하면, const는 선언위치 바로 뒤에 있는 대상을 묶어버리는 놈이다.

바로 뒤에 있는 녀석을 묶어버리는 녀석 SM


 포인터에서 const를 사용해서 발생하는 이점은 크게 없으나, const는 일반 주석보다 강한 의지를 표현하기 위해 사용한다. 일반적인 경우에는 잘 사용하지 않는다.


void swap(int *pa, int *pb){
        int temp = *pa;
        *pa = *pb; // check
        *pb = temp;
}

 두 변수의 값을 변경해주는 코드를 작성한다고 하면, 위 코드처럼 작성을 할 수 있다. 이 때, [ check ] 줄에서 [ * ]를 하나만 누락한 경우, 즉


// case 1
*pa = pb;

// case 2
pa = *pb;

 이런 경우에는 양 측의 데이터 자료형이 다르기 때문에 컴파일 오류를 발생시킨다. 하지만 아래처럼 [ * ] 모두 잊었다면 컴파일 오류를 일으키지 않고 정상작동된다. 시맨틱 오류가 날 뿐.


pa = pb; //시맨틱 오류 발생.

 이러한 문제를 해결하기 위해 [ pa ] [ pb ]의 값을 변경하지 못하도록 int *와 변수명에 const를 붙여준다.


void swap(int * const pa, int * const pb){
...

}

 const는 선언시에만 값을 결정할 수 있기 때문에 위처럼 작성하게 되면 설사 [ * ] 작성하는 것을 잊었대도 컴파일 오류를 일으켜 문제가 있음을 알려준다.


참고

>> const를 작성한다고 하더라도 캐스팅을 통해 이를 무시하고 값을 변경시킬 수 있다고 한다. 그러나 이러한 경우, 문제가 생겼을 때 책임 소재가 달라진다고 한다. const로 선언이 되어있으면 웬만하면 강제 캐스팅 하지 말고 사용하자.



2. 포인터 연산

 어떤 변수를 기억하기 위해서는 시작과 끝을 기억해야한다. 따라서 원래는 포인터는 주소 크기의 두 배 만큼의 크기를 가져야 하지만, 이렇게 되어버리면 배보다 배꼽이 더 큰 경우가 발생한다. 그래서 C언어의 포인터는 다른 방법을 생각했다. "시작주소와 사용할 크기를 정하자." 라고. 이렇게 하면 이득이 되는 것이 두가지 있는데,

첫째, 주소가 변경되면 시작 부분만 바꾸면된다.

둘째, 주소 크기만큼만 메모리를 소비하면 된다.


 포인터는 일반적인 산술 연산과 같이 똑같이 연산할 수 있다.

>> 주소 + 상수

>> 주소 - 상수

>> 주소 - 주소

 이런 식으로 가능하지만, 한가지, [ 주소 + 주소 ]는 불가능하다. 이유는 누구를 기준점으로 더해야할지 모르기 때문이다. 한가지 더 주의해야할 것이 있는데, [ 주소 - 주소 ]는 값을 가리킬 수 있는 대상의 개수를 반환해준다.


int *p1 = (int *)100;
int *p2 = (int *)120;
p2 - p1; //5

 주의해야 할 점은, 포인터의 자료형이 다른 경우 오류를 발생시킨다. 어느 것을 기준을 나누어야할지 모르기 때문이다. 즉, 포인터의 [ - ]는 아래와 같은 수식으로 값이 반환된다.

return p2 - p1 / sizeof( [ data type ] );


 포인터는 단항연산이나 값을 더했을 때, 포인터가 가리키는 데이터 자료형에 따라서 더해지는 값이 달라진다.


char *p1 = 100;
short *p2 = 100;
int *p3 = 100;
double *p4 = 100;

p1++; p2++; p3++; p4++; //포인터에 단항연산으로 값을 1씩 더함.

 모든 포인터의 값이 [ 101 ]이 될 것 같지만, char *로 선언한 [ p1 ]만 101이 되고 나머지는 다른 값이 된다. 개발자들의 편의를 위해 자료형에 맞추어 값을 더해주도록 만들어졌기 때문이다. 포인터 변수에 값을 더하면 실제로는 다음 수식처럼 작용된다.


원래 값 + 더하는 수 * sizeof(해당 포인터가 가리킬 수 있는 변수의 자료형)


 참고

>> 포인터는 단항연산을 할 때, 속도가 빠른 INC 명령어를 사용하지 않는다. 데이터 자료형에 따라서 더해지는 값이 달라지는 포인터의 특성 때문이다.

>> 포인터 크기와 일반 변수의 데이터 형식이 다른 경우에는 다음과 같은 방법으로 대입시킬 수 있다.


int data = 5;
short *p = (short *)&data;



3. void * (보이드 포인터)

 void 포인터는 사용할 대상의 변수 크기를 모를 때 사용한다. 즉, 다른 일반적인 포인터처럼 시작 주소는 가지고 있지만, 자료형의 크기를 내포하는 다른 포인터와 달리 이 void 포인터는 변수 크기를 포함하고 있지 않다.

 따라서 void 포인터를 사용할 때는 꼭 캐스팅 과정을 거쳐야한다. 매번 캐스팅 과정을 거쳐야 하는 포인터인데 사용하는 이유는 함수를 이용할 때 작성자는 불편하더라도 이용하는 사람들은 더 편하게 이용할 수 있기 때문이다.

 예를 들어 scanf를 사용할 때, [ void * ]타입이 없다면 이용자가 일일이 캐스팅을 해주어야 하지만, void *을 사용하므로써 일일이 캐스팅을 해줄 필요가 없다.[각주:1]


int data = 0;
void *p = &data;
*(int *)p = 5; //data의 값이 5로 바뀐다.


 위와 같은 형식은 void * 에서만 사용가능하지 않고, 다른 포인터도 사용할 수 있다.



4. 표준 입력 함수

 우리가 일반적으로 공부를 할 때 사용하는 함수들이다. 성능면이나 여러 측면에서 좋은 점이 없기 때문에 상업 프로그램에서는 오히려 콘솔입출력을 사용한다고 한다. (점차 폐쇄 수순을 밟는 중이라고)


 문자 하나를 받는 함수는 다음과 같다.


char ch;
getchar(ch);  // 엔터를 칠 때까지 대기하다가 엔터를 치면 가장 먼저 친 한 글자만 ch에 넣음
ch = getch(); // 입력과 동시에 ch에 값 하나가 넘어감.

 getchar(); 함수는 엔터를 받을 때까지 버퍼에 값을 보관하다가 엔터를 받으면 첫 번째 값만 받아오기 때문에 한 글자보다 더 많이 작성했을 때는 그 내용이 버퍼에 남아 오류를 일으킬 수 있다. 따라서 다음 함수로 표준 입력 버퍼의 내용을 삭제해주어야 버퍼에 값이 남아서 발생하는 오류를 방지할 수 있다.


rewind(stdin);


 문자열을 받는 함수는 다음과 같다.


char ch[100];
gets(ch);        // 엔터가 입력될 때까지 대기, 공백도 입력받을 수 있음
scanf("%s", ch); // 엔터가 입력될 때까지 대기. 공백이 구분자로 쓰이기 때문에 공백은 입력받을 수 없음.

  scanf에서 공백을 포함한 문자열을 받고싶을 때는 서식지정자를 [ %s ] 대신, [ %[^\n]s ] 를 사용하면 된다. 그러면 \n 외에는 전부 받게 된다. [^ ]에 [ \n ] 대신 다른 문자를 넣을 수도 있다. 둘 이상을 사용하고싶다면 연달아 작성하면 된다.[각주:2]


 참고

>> gets는 입력취소가 되었을 때 NULL을 반환한다.

>> scanf는 정상적으로 입력이 되었을 때, 입력받는 변수의 개수만큼을 반환한다. 단, [ 정상-비정상-정상-정상 ]입력이 동시에 이뤄졌다면, 처음 비정상 입력이 나온 곳에서 값이 멈춘다.


++ 추가

 [ scanf ]가 띄어쓰기를 구분자로 사용해서 띄어쓰기를 포함한 문자열을 입력받을 때에는 [ gets ]를 사용했는데 서식지정자로 [ %[^\n]s ]를 사용하면 띄어쓰기도 받을 수 있으니 굳이 [ gets ]와 [ scanf ]를 나누어야 하는 의문이 들 수 있는데, 두 함수 이름을 보면 왜 구분지어 사용해야할지 알 수 있다.


 gets >> GET String

 scanf >> SCAN Format


 즉, [ gets ]는 단순히 문자열을 입력받는 것이라면 [ scanf ]의 경우 서식화된 문자열을 받기 때문에 [ gets ]로 문자열을 받을 때보다 더 많은 기능이 지원되고, 그렇게 지원되는 기능을 통해 문자열 처리가 용이하다. 따라서 단순히 문자열을 받을 때는 [ gets ]를 사용해도 되겠지만, 만일 좀더 체계적으로 문자열을 받고자 한다면 [ scanf ]함수를 이용하는 것이 좋다.



5. 문자열을 숫자로 변환

 ' 0 '과 0은 무슨 차이가 있을까? 전자의 경우에는 문자 0이고, 후자는 숫자 0이다. 문자는 숫자처럼 연산이 불가능하기 때문에 문자로 구성된 숫자는 숫자로 변환을 해주어야한다. stdlib.h에 [ atoi[각주:3] ] 등의 문자열 변환 함수를 제공하긴 하지만, 함수를 직접 구현함으로써 얻을 수 있는 이익이 많이 있다.


int atoi(char *src) {
	char *p = src;
	int num = 0;
	while (*p) {
		num = num * 10 + *p - '0';
		p++;
	}

	return num;
}

 ASCII의 특성을 이용해 문자 '0'만큼을 빼주게 되면 숫자로 변환할 수 있다.



6. scanf();

 앞에서 설명한것처럼 매개변수를 void *로 받기 때문에 서식지정자를 정확하게 작성해야한다.

>> void *로 받은 후 서식지정자로 구분해 캐스팅을 하기 때문이다.


 또, 구분자로 사용되는 띄어쓰기를 몇개를 사용하던 하나로 인식한다. 즉, 숫자와 문자를 차례로 입력받는다고 하면 [ 10 a ]나 [ 10    a ]나 똑같이 [ 10 a ]로 인식한다는 것이다.



7. 배열과 포인터

 배열은 포인터에서 주소를 직접 지정하는 기능을 컴파일러가 대신하는, 즉 특정 기능을 제한해서 제공하는 문법이라서 더 쉽지만, 그만큼 자유도가 떨어진다.

 또, 포인터에서 주로 사용하는 디스플레이스먼트 어드레싱과 배열에서 주로 사용하는 인덱스드 어드레싱을 서로 교차해서 사용할 수 있다.


 배열에서 디스플레이스먼트 어드레싱을 사용하는 이유는 다음과 같다.


int data[2] = {0x12345678, 0x12345678};
*(char *)(data +1) = 0x22; // 0x123456[ 22 ]로 저장됨. >> 78이 22로 변경됨


 인덱스드 어드레싱을 사용하게 되면 각 배열요소로만 접근할 수 있지만, 디스플레이스먼트 어드레싱을 사용하면 각 요소의 어느 바이트까지 직접 접근할 수 있기 때문에 이러한 상황에서 사용하는 것이다.

 역으로 포인터에서 인덱스드 어드레싱을 사용하는 경우는 이차원 포인터 등을 사용할 때이다. 포인터 연산자( * )는 우선순위가 그리 높지 않기때문에 이 연산 순서때문에 조금만 깊어져도 복잡해지기 때문에 인덱스드 어드레싱 방식을 사용한다.


 배열에서 배열 이름은 배열 첫번째 요소의 주소를 가리킨다.


int num[5];
int *p = &num[0];
      // equal & *(data +0);
         // equal data;

 char *p[5] 와 char (*p)[5] 는 서로 다르다. 전자는 char * 를 다섯 개 선언한 것이고, 후자는 길이 5인 배열을 가리키는 포인터 하나를 선언한 것이다. 따라서 char *p[5]는 총 20 Byte, char (*p)[5]는 총 4 Byte이다.


 이차원 배열을 가리키는 포인터를 만드는 방법은 다음과 같다.


int num[3][5];
int (*p)[5] = num;
(*(p+1))[3] = 5;


 이렇게 작성하면 [ num[1][3] ]에 5를 넣는 것과 동일하게 작용한다. 참고로 배열 연산자인 대괄호는 포인터 연산자인 [ * ]보다 선행되어 계산되기 때문에 꼭 포인터 부분을 괄호로 묶어주어야 한다.



Etc.

(int *)&p[각주:4]와 &(int *)p는 다르다.

>> (int *)&p는 [ &p ]가 [ void ** ]로 바 뀐 후, [ int * ]로 캐스팅 되는 것이고,

>> &(int *)p는 [ p ]가 [ int * ]로 바뀐 후, [ & ]에 의해 [ int ** ]이 된다.


디스플레이스먼트 어드레싱

>> 포인터의 기본 방법, 인덱스드 어드레싱보다 빠름. *(p +1) 등으로 사용

인덱스드 어드레싱

>> 배열의 기본 방법.


 프로그래머가 되려고 했다면 새로운 함수가 나왔을 때, 매뉴얼부터 숙지하자.


  1. scanf는 일일이 캐스팅을 해주는 대신, 서식지정자로 변수의 데이터타입을 구분해 함수 내부에서 캐스팅을 해버린다. 따라서 서식지정자가 중요하다. [본문으로]
  2. r도 구분자로 작성하고 싶으면 " %[^\\nr]s "로 작성하면 된다. [본문으로]
  3. Array TO Integer [본문으로]
  4. 여기서 p는 [ void *p]로 선언되어있다. [본문으로]
728x90
반응형

'외부활동 > TIPS 19th' 카테고리의 다른 글

[TIPS 19TH] 08 : 2018.07.19. (목)  (2) 2018.07.21
[TIPS 19TH] 07 : 2018.07.16. (월)  (2) 2018.07.17
[TIPS 19TH] 05 : 2018.07.09. (월)  (2) 2018.07.10
[TIPS 19TH] 04 : 2018.07.05. (목)  (2) 2018.07.06
[TIPS 19TH] 03 : 2018.07.02. (월)  (0) 2018.07.03
Comments