[C] (번외) 우리도 깊은 사이야..

장세민·2022년 10월 3일
0

📝 TIL

목록 보기
24/40

📌 문자열과 포인터

문자열은 포인터와 관계가 깊어서 문자열의 구현 방법을 정확히 이해해야
문자열을 자신 있게 다룰 수 있다.

Before Starting

printf("%s", "apple");

위 문장을 출력하면 당연히 apple이 출력될 것이다.

만약, "apple"이 각 문자 'a', 'p' 등을 가진 배열이라면?
당연히 배열명을 사용할 것이고 실제로는 배열의 시작 위치를 가지고 출력하게 된다.

문자열은 배열의 구조를 가지며 첫 번째 문자의 주소로 쓰인다.


📖 문자열 상수 구현 방법

문자열은 크기가 일정하지 않아 컴파일러는 컴파일 과정에서
문자열을 char 배열 형태로 따로 보관하고 문자열 상수가 있던 곳에는 배열의 위치 값을 사용한다.

  1. printf("%s", "apple");# include <stdio.h>
  2.  
  3. int main(void)
  4. {
  5. printf("apple이 저장된 시작 주소 값: %p\n", "apple");
  6. printf("두 번째 문자의 주소 값: %p\n", "apple" + 1);
  7. printf("첫 번째 문자: %c\n", *"apple");
  8. printf("두 번째 문자: %c\n", *("apple" + 1));
  9. printf("두 번째 문자: %c\n", "apple"[2]);
  10.  
  11. return 0;
  12. }

문자열 "apple"은 문자 'a'가 저장된 메모리의 주소 값으로 바뀐다.

5행은 문자열이 저장된 곳의 위치 값을 출력하고,
"apple"은 배열 형태로 따로 저장되고 printf 함수의 인수로 그 첫 번째 문자 'a'의 주소가 사용된다.

결국

문자열은 컴파일 과정에서 char 변수의 주소로 바뀌므로
6행, 7행처럼 직접 포인터 연산, 간접 참조 연산 등을 수행할 수 있다.
9행과 같이 배열명처럼 사용하는 것도 가능하다.

🚨 다만 주소로 접근하여 문자열을 바꾸서는 안된다.

*"apple" = 't';

와 같이 첫 번째 문자가 저장된 공간에 다른 문자를 대입하여 그 값을 바꾸려는 시도는
실행할 때 운영체제에 의해 강제 종료될 가능성이 있다.

운영 체제는 문자열 상수를 읽기 전용 메모리 영역에 저장하기 때문!

char 포인터로 문자열 사용

  1. # include <stdio.h>
  2.  
  3. int main(void)
  4. {
  5. char *dessert = "apple";
  6.  
  7. printf("오늘 후식은 %s입니다.\n", dessert);
  8. dessert = "banana";
  9. printf("내일 후식은 %s입니다.\n", dessert);
  10.  
  11. return 0;
  12. }

5행은 char 포인터를 선언하고 문자열 상수로 초기화한다.
문자열은 컴파일 과정에서 첫 번째 문자의 주소로 바뀌므로
결국 포인터에는 문자열의 시작 위치 값만 저장된다.


7행의 실제 실행 방식

dessert가 가리키는 곳에는 a가 저장되어 있고,
dessert를 1 증가시키면 다음 문자의 주소를 구할 수 있고
간접 참조 연산을 수행하면 그 문자를 사용할 수 있다.

따라서, dessert의 값을 증가시키면서 널 문자가 나올 때까지 문자를 출력하면
문자열 전체를 출력할 수 있다.

%s 변환 문자의 역할

printf 함수의 %s 변환 문자는 함수 안에서 다음 코드와 같은 일을 수행한다.

while (*dessert != '\0')		// dessert가 가리키는 문자가 널 문자가 아닌 동안
{
	putchar(*dessert);		// dessert가 가리키는 문자 출력
	dessert++;			// dessert로 다음 문자를 가리킨다.
}


scanf 함수를 사용한 문자열 입력

배열에 문자열을 입력하는 방법 중 scanf 함수는
%s를 사용하여 공백이 없는 연속된 문자들을 입력받는다.

  1. # include <stdio.h>
  2.  
  3. int main(void)
  4. {
  5. char str[80];
  6.  
  7. printf("문자열 입력: ");
  8. scanf("%s", str);
  9. printf("첫 번째 단어: %s\n", str);
  10. scanf("%s", str);
  11. printf("버퍼에 남아 있는 두 번째 단어: %s\n", str);
  12.  
  13. return 0;
  14. }

scanf 함수는 enter를 누를 때 문자열을 버퍼에 저장한다.
그 후에 버퍼에서 문자열을 가져와 배열에 저장하는데
중간에 공백, , 개행 문자가 있으면 그 이전까지만 저장한다.

'apple jam'을 입력해보자

scanf 함수는 공백 문자 이전에 널 문자를 붙여서 문자열을 완성하기 때문에
버퍼에는 'jam' 문자열이 남아 있다.
다음에 호출되는 함수가 입력에 사용하여 출력했다.

🔔 scanf 함수를 사용할 때 몇 글자를 사용하나?

배열명을 인수로 받으므로 배열의 크기보다 큰 문자열을 입력하면
할당된 메모리 공간을 넘어서 저장한다.

따라서 scanf 함수로 문자열을 입력할 때는

[배열의 크기 -1]
까지 입력이 가능!

gets 함수를 사용한 문자열 입력

scanf 함수는 중간 공백이 포함된 문자를 한 번에 입력할 수 없었지만,
gets 함수는 중간의 공백이나 탭 문자를 포함하여 문자열 한 줄을 입력한다.

char *gets(char *str)
  1. # include <stdio.h>
  2.  
  3. int main(void)
  4. {
  5. char str[80];
  6.  
  7. printf("문자열 입력: ");
  8. scanf("%s", str);
  9. printf("첫 번째 단어: %s\n", str);
  10. scanf("%s", str);
  11. printf("버퍼에 남아 있는 두 번째 단어: %s\n", str);
  12.  
  13. return 0;
  14. }

gets 함수는 키보드로 Enter를 누를 때 까지 입력한 한 줄을 char 배열에 저장한다.

scanf 함수와 차이는 중간에 있는 공백이나 탭 문자를 모두 가져오므로
한 문장을 한 번에 가져올 수 있다.

🚨 이때 버퍼에서 개행 문자를 가져오지만 배열에는 널 문자로 바꿔 저장한다.

결국 gets 함수도 scanf 함수와 마찬가지로 입력한 문자열을 포인터 연산으로 배열에 저장한다.

🚨 gets 함수는 Enter만 눌러도 입력을 끝낸다.

scanf 함수는 스페이스바, 탭, 엔터를 입력해도 계속 입력을 기다린다.
그러나 gets 함수는 문자열의 일부로 입력하므로 문자열 입력 없이 바로 Enter만 눌러도 입력을 끝낸다.


fgets 함수를 사용한 문자열 입력

scanf 함수와 gets 함수는 입력되는 문자열의 크기가 배열 크기를 넘어설 위험성이 있다.

fgets 함수는 최대 배열의 크기까지만 문자열을 입력하는 장점이 있고,
인수를 3개 사용하는데 각각 다음과 같다.

fgets(str, sizeof(str), stdin);

str : 배열명
sizeof(str) : 배열의 크기 확인
stdin : 표준 입력

두 번째 인수로 배열의 크기를 알려주므로
배열의 크기를 넘는 문자열을 입력해도 배열의 크기만큼만 저장하므로
배열의 크기 -1개의 문자만 저장!


  1. # include <stdio.h>
  2.  
  3.  
  4. int main(void)
  5. {
  6. char str[80];
  7.  
  8. printf("공백이 포함된 문자열 입력: ");
  9. fgets(str, sizeof(str), stdin);
  10.  
  11. printf("입력된 문자열은 %s입니다.\n", str);
  12.  
  13. return 0;
  14. }

fgets 함수가 문자열을 입력하는 방식은 gets 함수와 거의 같으나
개행 문자의 처리 방식이 다르다.

버퍼에 있는 개행 문자도 배열에 저장하고 널 문자를 붙여 문자열을 완성한다.

입력된 개행 문자 때문에 apple jam이 출력되고 바로 줄이 바뀜.

개행 문자 제거 과정

입력된 개행 문자가 불필요하면

str[strlen(str) - 1] = '\0'

다음 공식에 따라 제거한다.

strlen 함수는 배열명을 인수로 받아 널 문자 이전까지의 문자 수를 세어 반환한다.
사용할 때 string.h 헤더 파일을 인클루드 해야한다.

  1. # include <stdio.h>
  2. # include <string.h>
  3.  
  4. int main(void)
  5. {
  6. char str[80];
  7.  
  8. printf("공백이 포함된 문자열 입력: ");
  9. fgets(str, sizeof(str), stdin);
  10. str[strlen(str) - 1] = '\0';
  11. printf("입력된 문자열은 %s입니다.\n", str);
  12.  
  13. return 0;
  14. }

ㅎㅎㅋ


📌 표준 입력 함수의 버퍼 공유 문제

scanf 함수나 getchar 함수 같은 표준 입력 함수는 입력 버퍼를 공유하기 때문에
gets나 fgets 함수에서 개행 문자를 입력의 종료 조건으로 사용하면
개행 문자만 가져오고 입력을 끝내는 문제가 생길 수 있다.

  1. # include <stdio.h>
  2. # include <string.h>
  3.  
  4. int main(void)
  5. {
  6. int age;
  7. char name[20];
  8.  
  9. printf("나이 입력: ");
  10. scanf("%d", &age);
  11.  
  12. printf("이름 입력: ");
  13. fgets(name, sizeof(name), stdin);
  14. name[strlen(name) - 1] = '\0';
  15. printf("나이: %d, 이름: %s\n", age, name);
  16.  
  17. return 0;
  18. }


입력한 나이는 문자열로 버퍼에 저장되었다가
scanf 함수가 숫자로 변환하여 변수 age에 저장하는데,
이때 버퍼에 남아 있는 개행 문자가 fgets 함수의 입력으로 쓰인다.

결국 gets 함수는 버퍼에서 개행 문자를 가져와 입력을 끝내므로
이름을 입력하는 과정이 생략되며 name 배열의 첫 번째 요소에는 널 문자가 저장되어
이름으로는 아무것도 출력되지 않는다.


그럼 버퍼에 남아 있는 개행 문자를 어떻게 지우냐?

개행 문자를 읽어 들이는 문자 입력 함수를 호출한다.

getchar();
scanf("%*c");
fgetc(stdin);

와 같은.

좋아

📖 문자열을 출력하는 함수

화면에 문자열만 출력할 때는 전용 출력 함수 puts와 fputs를 사용한다.

int puts(const char *str)                 // 문자열을 출력하고 자동 줄 바꿈
int fputs(const char *str, FILE *stream)  // 문자열을 출력하고 줄 바꾸지 않음
  1. # include <stdio.h>
  2.  
  3. int main(void)
  4. {
  5. char str[80] = "apple juice";
  6. char *ps = "banana";
  7.  
  8. puts(str);
  9. fputs(ps, stdout);
  10. puts("milk");
  11.  
  12. return 0;
  13. }

puts와 fputs 함수는 문자열의 시작 위치부터 널 문자가 나올 때 까지 모든 문자를 출력한다.
따라서 char 배열의 배열명이나 문자열 상수를 연결하고 있는 포인터를 인수로 줄 수 있다.

puts 함수는 fputs 함수와 달리 문자열을 출력한 후에 자동으로 줄을 바꿔줘 편리함!!

profile
분석하는 남자 💻

0개의 댓글