정글사관학교 개발일지/자료구조&알고리즘

정글사관학교 32일차 TIL: C언어 기초(문자 입력, 포인터)

Woonys 2021. 12. 4. 01:15
반응형

Ch.4: 문자 입력 받기

 

scanf: 화면(키보드)으로부터 결과를 받아들이는 입력 함수 (파이썬에서 input에 해당). 출력은 printf. scanf 역시 printf처럼 각 변수의 타입에 따라 입력받는 포맷(%d, %f, %c 등)을 달리 해야 한다.

 

특이사항: scanf로 double 형 변수를 입력받으려면 %lf로 받아야! printf보다 좀 더 까다로운 점은, printf는 double이나 float 모두 %f로 출력하지만 float은 무조건 %f로 입력받아야 함.

 

버퍼 오버플로우: 허용된 메모리 이상에 데이터를 집어넣어 발생하는 오류. 보안 상 매우 취약.

 

ex) 최대 1바이트를 차지하는 char형 변수인 ch에 한글을 치면 오류가 난다.

 

    printf("char 형 변수 입력: ");
    scanf("%c", &ch);

    printf("short형 변수 입력: ");
    scanf("%hd", &sh);
    
    printf("int 형 변수 입력: ");
    scanf("%d", &i);
    printf("long형 변수 입력: ");
    scanf("%ld", &lo);

    printf("float형 변수 입력: ");
    scanf("%f", &fl);
    printf("double형 변수 입력: ");
    scanf("%lf", &du);

short, long 모두 정수형 변수이나 scanf 함수에서 입력받는 포맷은 %hd, %ld로 표현이 약간 다름.

 

 

ch.5 조건문

 

if-else문은 익숙한 형태.

 

if (/* 조건 */) {
/* 명령 */
}

ex)

#include <stdio.h>
int main() {
    int num;
    printf("아무 숫자나 입력해 보세요 : ");
    scanf("%d", &num);
    if (num == 7) {
        printf("행운의 숫자 7 이군요!\n");
    } else {
        printf("그냥 보통 숫자인 %d 를 입력했군요\n", num);
    }
    return 0;
}

파이썬에서 elif: else if를 사용

 

#include <stdio.h>
int main() {
    int num;
    printf("아무 숫자나 입력해 보세요 : ");
    scanf("%d", &num);
    if (num == 7) {
    	printf("행운의 숫자 7 이군요!\n");
	} else if (num == 4) {
		printf("죽음의 숫자 4 인가요 ;;; \n");
	} else {
		printf("그냥 평범한 숫자 %d \n", num);
	}
	return 0;
}

 

 

if문에서 논리 연산자 쓰기

 

AND: && / OR: || / NOT: ! 기호를 이용

 

ch.6 반복문

 

for문 구조: 초기식 / 조건식 / 증감식이 나오고 이후 명령을 순차적으로 실행

 

for (/* 초기식 */; /* 조건식 */; /* 증감식 */) {
// 명령1;
// 명령2;
// ....
}

// ex)

#include <stdio.h>
int main() {
	int i;
	for (i = 0; i < 20; i++) {
		printf("숫자 : %d \n", i);
	}
	return 0;
}

 

위의 예시를 보자.

 

1. 초기식: i = 0;

초기 조건을 설정해준다. i=0이므로 i는 0에서부터 시작한다. 이때 i를 제어변수라고 한다.

2. 조건식: i < 20;

i가 20보다 작을 때까지 계속해서 반복문을 시행한다는 조건을 설정한다. 만약 이런 조건이 없다면 끊이지 않고 반복하므로 CPU 사용률을 100% 끌어올려 전력을 과도하게 낭비한다.

3. 증감식: i++;

2번 조건식을 만족하면 1번에서 출발해 계속 1씩 더해주는 증감식을 설정한다.

 

do-while문

 

do-while문은 while문과 비슷하나 while문이 명령을 시행하기 전에 조건식의 참 거짓을 먼저 판별하는 반면, do-while에서는 먼저 명령을 실행하고 나서 조건식을 검사한다. 따라서 만약 조건식이 거짓이더라도 최소한 한 번은 실행된다는 점이 다르다.

#include <stdio.h>
int main() {
	int i = 1, sum = 0;
	
    do {
		sum += i;
		i++;
	} while (i < 1);
	
    printf(" sum : %d \n", sum);
	return 0;
}

 

ch.8 포인터

드디어 대망의 포인터...! 여기부터는 좀 더 탄탄하게 정리해보도록 하자.

 

포인터는 특정한 데이터가 저장된 주소값을 보관하는 변수다. 즉, 포인터도 변수라는 것! 다시 말해, 메모리 상에 위치한 특정한 데이터의 (시작)주소값을 보관하는 변수이다.

 

포인터 역시 변수이다 보니 다른 변수와 마찬가지로 형(type)을 가진다. 즉, int형 데이터 주소값을 저장하는 포인터와 char형 데이터 주소값을 저장하는 포인터가 서로 다르다는 것이다. 아직까지는 나도 뭔 말인지 모르겠으니 좀 더 딥다이브 해보자.

 

C언어에서 포인터의 정의는 다음과 같다. 두 가지로 표시할 수 있는데, 차이는 앞의 형 바로 뒤에 *을 붙이냐 뒤의 포인터 이름 앞에 *을 붙이냐의 차이.

 

(포인터에 주소값이 저장되는 데이터의 형)* (포인터의 이름);
or
(포인터에 주소값이 저장되는 데이터의 형) *(포인터의 이름);

예를 들어 p라는 포인터가 int 데이터를 가리키고 싶다고 하면

 

int *p; // 라고 하거나 
int* p; // 로 하면 된다

이러면 위의 포인터 p는 int형 데이터의 주소값을 저장하는 변수가 된다.

 

& 연산자

 

해당 데이터의 주소값을 알아내려면? & 연산자를 사용한다. 그런데 &는 앞에서 배웠던 AND 연산자와 기호가 같다. 이러한 혼동을 방지하기 위해, 앞의 AND 연산자에서는 &기호를 두 개 연속으로 붙여서 쓴다. 예컨대 && 이런 식으로. 하지만 포인터에서의 & 연산자는 하나만을 사용한다.

 

또한, A && B 와 같이 AND 연산자에서는 피연산자가 두 개가 있어야 둘을 비교하는 반면, 포인터에서의 & 연산자는 피연산자 하나만을 필요로 한다. 왜냐하면 해당 피연산자의 주소값만 불러오면 되니 다른 피연산자는 존재할 이유가 없는 것.

 

요약하면, 단항 & 연산자는 피연산자의 주소값을 불러온다. 해당 피연산자 앞에 &를 바로 붙이면 된다.

 

/* & 연산자*/ 
#include <stdio.h> 
int main() {
    int a; 
    a = 2;
	
    printf("%p \n", &a);
    return 0; 
}

이때 출력하는 값은 8바이트 크기의 16자리 16진수이나 앞에 0이 잘려서 출력된다.

 

이제 어떤 변수 하나를 생성하고 거기에 포인터 변수를 대응시키는 작업을 해보자.

 

/* 포인터의 시작 */
#include <stdio.h>
int main(){
    int *p;
    int a;

    p = &a;

    printf("포인터 p에 들어있는 값: %p \n", p);
    printf("int 변수 a가 저장된 주소: %p \n", &a);
    return 0;

}

위를 실행하면 내 컴퓨터에서는 다음과 같이 출력된다.

 

포인터 p에 들어있는 값: 0x7fffffffdffc
int 변수 a가 저장된 주소: 0x7fffffffdffc

둘이 똑같이 나오는 것을 알 수 있는데, 이는 당연하게도 우리가 위에서 p = &a라고 선언했기 때문이다.

 

* 연산자

앞에서 배운 개념 정리.

 

  1. 포인터는 특정 데이터의 주소값을 보관.
  2. 포인터는 주소값을 보관하는 데이터의 형 or 포인터 이름에 *를 붙임으로써 정의.
  3. & 연산자로 특정한 데이터의 메모리 상의 주소값을 알아올 수 있다.

& 연산자가 데이터의 주소값을 얻어내는 연산자라면, 반대로 주소값에서 해당 주소값에 대응되는 데이터를 가져오는 연산자 역시 필요할 것이다. 이 역할을 수행하는 게 바로 * 연산자!

 

* 요놈 역시 곱셈 연산자로 사용하는 애인데, 이 역시 피연산자가 2개일 때만 곱셈 연산자로 작동한다. 만약 피연산자가 하나만 있다면 이 연산자는 해당 포인터가 가리키는 데이터 값을 가져온다. 따라서 * 연산자는 포인터 변수에 달라붙어야 한다. 즉, *연산자의 역할을 정리하면, 포인터에 달라붙어서 "포인터를 포인터에 저장된 주소값에 위치한 데이터로 생각해달라!"는 것.

 

/* * 연산자 이용 */

#include <stdio.h>
int main(){
    int *p; // p는 정수형 변수를 가리키는 포인터
    int a; // a는 정수형 변수

    p = &a; // p는 이제 정수형 변수 a의 주소값을 가리킨다.
    a = 2;

    printf("a의 값: %d \n", a);
    printf("*p의 값: %d \n", *p); // 포인터 변수 앞에 *가 붙었으니 해당 주소값에 들어있는 데이터를 출력한다.

    return 0;
}

출력은 다음과 같다.

a의 값: 2
*p의 값: 2

 

정리하면

 

& 연산자: 해당 포인터가 가리키는 주소값을 출력

* 연산자: 포인터에 붙어서 포인터가 가리키는 주소에 있는 데이터 값을 출력

 

상수 포인터

C에서는 변수 앞에 const를 붙이면 그 변수와 변수에 대응하는 값은 상수로 처리된다. 즉, 어떤 짓을 해도 해당 변수의 값은 그 값을 상수로 선언한 순간부터 상수가 된다.

 

상수 const를 쓰는 이유를 생각해보자. 절대 바뀌지 않을 것 같은 값에 const 설정을 해주면 혹여나 실수로 해당 값을 바꾸는 상황이 발생해도 에러가 뜨기 때문에 실수할 일이 없어진다! 이를 포인터에도 마찬가지로 적용할 수 있다.

 

예제 코드를 하나 보자.

 

/* 상수 포인터? */ 
#include <stdio.h> 
int main() {
    int a;
    int b;
    const int* pa = &a;
    *pa = 3; // 올바르지 않은 문장
    pa = &b; // 올바른 문장
    return 0; 

}

 

하나씩 뜯어보자.

 

const int* pa = &a;

이 코드의 의미는 무엇일까? const int형을 가리키는 포인터? 땡. pa라는 애가 int형 포인터 변수인데, 이 포인터가 가리키는 값이 절대 바뀌면 안된다는 것을 선언하는 것이다.

 

따라서, const int*의 의미는 const int형 변수를 가리킨다는 것이 아니라 int형 변수를 가리키는데 그 값을 절대로 바꾸지 말라는 말이다. 여기서 a 자체는 변수이지 않나? 그렇다. a는 변수이니 값을 자유롭게 변경 가능하다. 그런데 위에서는 상수 포인터 pa를 통해서 a를 간접적으로 가리키고 있다. 이는 컴퓨터에게 const인 변수를 가리키고 있다고 말해주는 것이기 때문에 *pa로 a의 값을 호출할 때는 a의 값을 바꿀 수 없다. 하지만, a=3;이라고 선언하면 이는 오류가 아닌데, a 자체를 다이렉트로 바꾸는 건 a가 변수이기에 상관이 없다. 그래서 *pa = 3;이라는 문장은 오류를 출력한다.

 

이번에는 그 아래인 pa = &b를 보자. 이건 옳은 문장인데, pa가 가리키는 주소에 해당하는 데이터를 바꾸지 말라했지, pa 자체를 바꾸지 말라고는 하지 않았다. (아직은 약간 아리송하긴 함...) 그래서 pa 자체를 바꾸는 건 오케이.

 

정리하면 pa를 바꾸는 건 괜찮은데 pa가 가리키는 주소값에 있는 데이터를 바꾸면 안된다는..?

 

자고 다시 봐야겠다..

 

 

 

 

 

반응형