문자열 리터럴
정수형 자료형들을 다루었을 때(참고) 문자형 char는 single quote(' ')로 감싸 1바이트 문자 하나를 저장했다. 반면, 문자 하나가 아닌 문자열(sequence of characters)은 double quote(" ")로 감싸고 '문자열 리터럴(string literal)'이라 한다. printf와 scanf를 사용할 때 사용한 서식 문자열 또한 문자열 리터럴이다.
Escape Sequence
문자열 리터럴 내에 \n 와 같은 escape sequences를 사용해서 커서를 다음 줄로 넘어가게 할 수 있다.
\ (backslash)
문자열 리터럴을 여러 줄에 걸쳐서 작성하고싶을 때 역슬래시 문자(\)를 사용할 수 있다. 이때, \ 다음에는 개행문자(\n) 외에 어떠한 문자도 와서는 안된다. 이렇게 \ 를 사용해서 두 개 이상의 프로그램 줄을 하나로 합치는 것을 C 표준에서는 splicing 이라고 한다.
printf("When you come to a fork in the road, take it. \
--Yogi Berra");
다만 위의 코드에서 확인 할 수 있는 것 처럼 \을 사용해서 줄을 나눌 때 다음 문자열은 반드시 다음 줄의 맨 처음부터 이어져야 한다. 이로 인해 프로그램의 들여쓰기 구조가 망가지는 게 싫다면 문자열을 두 개의 문자열로 나누고 , 두 문자열 사이에는 빈 상태를 유지해주는 방법으로서 줄 바꿈을 할 수도 있다. 왜냐하면 C 컴파일러는 이렇게 두 개 이상의 문자열 리터럴이 깨끗한 공백으로만 구분된 상태로 붙어있을 때, 이를 하나의 문자열로 합치기 때문이다. (아래 코드 참조)
printf("When you come to a fork in the road, take it. "
"--Yogi Berra");
문자열 리터럴은 어떻게 저장되는가
C에는 문자열 자료형은 존재하지 않는다. C는 문자열 리터럴을 문자형 배열(character arrays)로 취급 한다. C 컴파일러는 프로그램 내의 n 길이의 문자열 리터럴을 보면, 해당 문자열을 위해 n + 1 바이트를 할당한다. n바이트가 아닌 n+1 바이트를 할당하는 이유는, 메모리에 해당 문자열의 문자들을 저장할 때, 마지막에 널 문자(null character)를 넣어서 문자열의 끝임을 표시하기 때문이다.
💡 널 문자: ASCII 코드 값이 0인 문자로 비트가 전부 0인 바이트. 이스케이프 시퀀스 \0로 표현한다.(숫자 0과 혼동하지 말자)
문자열 리터럴 "abc"를 예로 들어보자. 이 문자열 리터럴은 4개의 문자(a, b, c, \0)를 가진 배열로 저장 된다.
a | b | c | /0 |
빈 문자열 리터럴의 경우, 널 문자 하나로 저장된다.
/0 |
printf 와 scanf 함수를 돌아보자
우리가 지금까지 문자열 리터럴을 전달했던 print 함수와 scanf 함수를 생각해보자.
사실 이 함수들은 첫번째 인자로 '주소'를 받는 함수들이다. 즉 우리가 전달했던 문자열 리터럴들은 사실 문자열 리터럴이 저장된 문자열 배열의 주소인 것이다. 문자열 리터럴은 문자형 배열의 형태로 저장되기 때문에 컴파일러는 이 문자열 리터럴을 char형 변수를 가르키는 포인터(char*)로 취급한다. 그래서 printf나 scanf의 인수로 "문자열 리터럴" 이 아닌 '문자'를 넘겨주면 에러가 발생한다! ('문자'는 포인터가 아닌 정수)
Since a string literal is stored as an array, the compiler treats it as a pointer type char *.
- C programming : a modern approach 2/e .279p
이제 "abc" 라는 문자열리터럴을 printf의 인수로 넘겨주는 코드를 생각해보자. 우리는 printf 함수에 뭘 전달해주고 있을까?
printf("abc");
우리는 메모리 주소를 전달하고 있는 것이다. printf 가 호출될 때 "abc"라는 인수가 전달되는데, 컴파일러는 "abc"를 "abc"가 저장된 메모리 주소(= 문자 a가 저장된 주소)를 가르키는 포인터로 취급하기 때문에, "abc"가 저장된 주소를 인수로 전달해준다.
문자열 리터럴의 사용 및 첨자
일반적으로 C에서 char* 포인터를 사용할 수 있는 곳에는 문자열 리터럴을 사용할 수 있다.
char *p;
p = "abc"; // 포인터 변수 p에 문자열 리터럴 "abc"를 할당
위 코드에서 p = "abc"; 는 "abc"의 문자들을 복사하지 않는다. 단지 p가 문자열의 첫번째 문자인 a를 가리키도록 해준다.
주의) 위와 같은 문자열 리터럴은 수정할 수 없다!
C에서 포인터는 첨자(subscript)가 사용될 수 있기 때문에(=인덱스로 접근이 가능) 문자열 리터럴 또한 첨자가 가능하다.
char ch;
ch = "abc"[1]; // ch의 값은 문자 b가 될 것
하나의 문자를 갖는 문자열 리터럴 vs. 문자형 상수
결론은, 하나의 문자를 갖는 문자열 리터럴은 문자형 상수와 다르다.
문자열 리터럴 "a"는 문자 a의 메모리 주소를 가리키는 포인터로 표현되는 반면, 문자형 상수 'a'는 정수로 표현된다 (문자의 ASCII 코드).
문자열 변수
string 형을 제공하는 다른 프로그래밍 언어들과 다르게 C는 '모든 문자형 일차원 배열이 널 문자로 끝나기만 하면 문자열을 저장할 수 있게 하는 방법' 으로 문자열을 처리한다. 문자열 변수란 문자열을 문자열 길이 + 1의 길이를 가진 일차원 배열로 저장한 것을 말한다.
#define STR_LEN (80)
…
char str[STR_LEN + 1];
많은 C 프로그래머들이 일반적으로 위와 같은 방법으로 문자열 변수를 선언한다. : STR_LEN을 80으로 정의해주어 str이 최대 80 문자를 저장할 수 있음을 나타내고, STR_LEN +1 의 크기로 str을 선언하여 마지막에 널 문자가 들어갈 공간을 만들어 준다.
🚨 C 라이브러리는 모든 문자열들은 널 문자로 종료된다고 인식하기 때문에 널 문자가 들어갈 공간을 잊으면 안 된다!
문자열 변수 초기화
문자열 변수는 선언과 동시에 초기화할 수 있다.
char date1[8] = "June 14"
위의 코드가 더 읽기 쉽지만 아래와 같이 쓸 수도 있다. (C는 위의 코드를 아래 코드와 같은 배열 초기자(initializer)의 축약형으로 본다)
char date1[8] = {'J', 'u', 'n', 'e', ' ', '1', '4', '\0'};
문자열 변수를 초기화 할 때 컴파일러는 "June 14"의 문자들을 date1 배열에 넣고, 끝에 널 문자를 넣어 date1이 문자열처럼 쓰일 수 있게 해줄 것이다. 그렇게 되면 date은 다음과 같이 생길 것이다:
만약 문자열 리터럴(배열의 초기자)이 문자열 변수를 담을 배열보다 짧다면, 컴파일러는 남는 자리에 널 문자를 넣어준다.
char date2[9] = "June 14";
위와 같은 선언 이후에 date2는 다음과 같이 될 것이다:
만약 문자열 리터럴(배열의 초기자)의 길이가 문자열 변수를 담을 배열과 정확히 같은 길이가 된다면, 널 문자가 저장될 수 없기 때문에 문자열 변수의 초기화가 불가능할까? 그렇지 않다. C는 초기자(널 문자를 제외하고)가 변수와 정확히 같은 길이가 되게하는 건 허용한다:
char date3[7] = "June 14";
하지만 널 문자를 넣을 공간이 없으므로 컴파일러는 널 문자를 추가하지 않는다. 그러므로 이 배열은 문자열로서 사용할 수 없다.
따라서 문자열 변수 선언할 땐 크기를 생략해주는 게 더 안전하다. 이 경우 컴파일러가 알아서 계산을 해주기 때문이다! 특히 초기자가 긴 경우 사람이 크기를 직접 확인하는 경우 실수가 생길 수 있다. 차라리 문자열 변수의 크기를 생략해서 컴파일러가 계산하게 두자.
char date4[] = "June 14";
위와 같이 문자열 변수를 선언하고 초기화 하면, 컴파일러는 date4를 위해 "June 14"의 문자들과 널 문자를 저장하기 충분하게 8 개의 문자를 저장할 공간을 할당해준다. 배열의 크기를 명시하지 않았다는 게 나중에 크기를 바꿀 수 있다는 의미는 아니다. 프로그램이 컴파일된 순간 배열의 크기는 고정된다.
문자열 배열 (문자열 변수) vs. 문자열 포인터(문자열 상수)
date을 배열로 선언하는 코드와, date을 포인터로 선언하는 코드를 비교해보자
// 배열 date
char date[] = "June 14";
// 포인터 date
char *date = "June 14";
배열로 선언한 date와 포인터로 선언한 date 둘 다 문자열로 사용할 수 있다. 어떤 함수가 인수로 문자 배열이나 문자 포인터를 받는 다면, 해당 함수의 인수로 두 date를 모두 사용할 수도 있다. 이 둘은 꽤 유사해보지만 분명한, 그리고 상당한 차이점이 있다.
- 수정 가능 여부
- 배열 date의 경우 일반적인 배열에서 원소를 수정할 수 있듯이 date에 저장된 문자들은 수정될 수 있다. (date= 문자열 변수)
- 포인터 date의 경우 읽기 전용 메모리에 저장되어 있는 문자열 리터럴을 가리키고 있다. 이 문자열 리터럴은 수정될 수 없다. 이때의 문자열 리터럴을 '문자열 상수' 라고 한다.
- date의 의미
- 배열 date에서 date는 배열의 이름이다. (= 포인터 상수)
- 포인터 date에서 date은 프로그램 실행 도중 다른 문자열을 가리킬 수 있는 포인터 변수이다.
만약 수정이 불가능한 문자열이 필요하다면 문자열을 저장할 문자 배열이 아닌 포인터 변수를 만들어야 한다.
char *p;
위와 같이 포인터 변수를 선언하면 컴파일러는 포인터 변수를 위한 메모리를 할당한다. 그런데 컴파일러는 문자열의 크기 조차 모르는 상태이기 때문에 문자열을 위한 공간을 할당할 수 없다. 그래서 p를 문자열을 가르키는 포인터 변수로 사용하려면, p를 문자열로 사용하기 이전에 반드시 문자 배열을 가리키게 해주어야 한다.
한 가지 방법은 p가 문자열 변수를 가리키게 만들어주는 것이다. (❓ 근데 이럴 경우, 문자열 리터럴을 가르키게 되는 게.. 맞나? 댓글 좀)
char str[STR_LEN + 1];
char *p;
p = str;
p는 이제 str의 첫번째 문자를 가리키게되므로 p를 문자열처럼 사용해줄 수 있다. (다른 방법은 p를 동적할당된 문자열을 가리키게 만들어주는 것이나 나중에 다룰 것)
아래 코드로 다시 한 번 정리해보자.
#include <stdio.h>
int main(void)
{
char str[] = "asap"; // 문자열 리터럴 "asap"은 배열 str의 원소로 저장됨. 이 문자열은 변경가능한 문자열 변수
char *pStr = "stacy"; // pStr은 문자열 리터럴 "stacy", 즉 이 문자열 리터럴 주소값을 저장 (가리킴)
printf("str: %s \n", str); // str에 저장된 값
printf("pStr: %s \n", pStr); // pStr에 저장된 값
printf("pStr address :%p\n", pStr); // pStr이 저장하고 있는 주소의 값
return 0 ;
}
배열에 저장되는 문자열 변수와 포인터가 문자열 리터럴의 주소를 가리키는 형태에서 메모리에 저장되는 방식은 어떻게 다른지 아래 그림을 참고해보자.
Reference
1. C Programming : A Modern Approach, 2/E K. N. King | W. W. Norton & Company
2. K.N.King 『C 프로그래밍: 현대적 접근 2nd Ed』, 주민하, 2022 (https://wikidocs.net/86254)
3. 나혼자 C언어 이창현 저 | 디지털북스 |
5. https://ehpub.co.kr/tag/%EB%AC%B8%EC%9E%90%EC%97%B4-%EB%A6%AC%ED%84%B0%EB%9F%B4/
'C' 카테고리의 다른 글
[C] 메모리 할당 - 정적 메모리 할당 (김성엽의 기초 C언어 강좌 16장 1) (0) | 2022.10.28 |
---|---|
[C] 연결리스트 | 연결리스트란 / 연결리스트 생성 / 노드 삽입 / 검색 / 노드 삭제 (2) | 2022.10.24 |
[C] 배열과 포인터 | 포인터와 배열의 관계, 배열 이름을 포인터처럼 사용하기, 포인터를 배열의 이름으로 사용하기 (0) | 2022.10.22 |
[C] 포인터 | 포인터란, 포인터 변수의 선언/초기화/호출, &연산자와 *연산자, 포인터의 연산, 포인터에 자료형이 필요한 이유 (0) | 2022.10.22 |
[C] 반복문 | while문, do-while문, for 문, break, continue (0) | 2022.10.21 |
댓글