11-4.배열과 문자열

11-4-가.문자열 상수

코드에서 정수 상수가 필요하면 1234, 98 등과 같이 아라비아 숫자를 바로 적고 문자 상수가 필요할 때는 'a', 'b' 등과 같이 홑 따옴표안에 문자 하나를 적으면 된다. 문자열 상수가 필요할 경우는 겹 따옴표를 사용하여 "String"식으로 적는다. 이미 오래 전에 공부했던 내용이다.

컴파일러는 문자열 상수를 다른 상수들과 달리 아주 특별하게 취급한다. 문자열 상수가 어떻게 취급되는지 조사해 보기 위해 간단한 실험을 해 보자. 다음 예제는 두 개의 문자열 상수를 사용하고 있는데 하나는 문자형 배열을 초기화하기 위해, 하나는 printf 함수의 인수로 사용되었다.

 

: StringConst

#include <Turboc.h>

 

void main()

{

     char Name[]="Kim Sang Hyung";

 

     printf("This is a String Constant.\n");

     puts(Name);

}

 

실행해 보면 두 개의 문자열이 화면으로 출력될 것이다. 이 프로그램에서 사용하고 있는 두 개의 문자열이 어디에 저장되어 있는지 살펴보기 위해 컴파일된 실행 파일을 16진수로 덤프해 보자. 16진 덤프 기능이 있는 편집기로 StringConst.exe 파일을 열어 확인해 보면 한참 뒤쪽에서 다음과 같은 내용을 볼 수 있다.

 

7030: 54 68 69 73 20 69 73 20 61 20 53 74 72 69 6E 67  This is a String

7040: 20 43 6F 6E 73 74 61 6E 7A 2E 0A 00 4B 69 6D 20   Constant...Kim

7050: 53 61 6E 67 20 48 79 75 6E 67 00 00 7C 1E 40 00  Sang Hyung..|.@.

 

이 실험에서 알 수 있듯이 프로그램이 사용하는 문자열 상수는 실행 파일에 같이 기록되는데 정확하게 정적 데이터 영역에 기록된다. 정적 데이터 영역이란 전역변수, 정적변수 등이 저장되는 곳인데 이 영역에 문자열 상수도 같이 저장되어 있는 것이다. 설사 함수 내에서 지역적으로 사용하고 있는 문자열 상수더라도 말이다.

컴파일러는 왜 문자열 상수를 실행 파일에 기록해 넣는 것일까? 정수나 문자형같은 상수는 기본 타입이고 크기가 작기 때문에 그 값을 코드에 곧바로 기록할 수 있다. 예를 들어 a=1234;라는 대입문을 사용했다면 a라는 기억 장소로 1234라는 값을 전송하기만 하면 된다. 특정 메모리 위치에 상수를 저장하는 명령은 기계어 코드로 곧바로 표현할 수 있는데 a=1234; 대입문이 컴파일되면 mov [a],1234;가 된다.

그러나 문자열 상수는 그 자체가 배열이며 길이가 굉장히 길 수 있기 때문에 코드로는 곧바로 표현하지 못한다. mov [str],"아주 긴 문자열" 이런 명령을 CPU가 지원하지 않기 때문에 문자열을 어딘가에 기록해 놓고 포인터를 사용해 메모리끼리 복사해야 하는 것이다. 그 장소가 바로 프로그램 뒤쪽의 정적 데이터 영역이다.

지금 당장 아무 실행 파일이나 열어서 뒷 부분을 확인해 보면 이 프로그램이 어떤 문자열 상수들을 사용하는지 확인해 볼 수 있을 것이다. 실행 파일에 기록된 문자열 상수는 실행 파일과 함께 메모리로 로드되며 코드는 실행중에 이 문자열 상수를 사용한다.

컴파일러는 문자열 상수를 정적 데이터 영역에 기록할 때 항상 널 종료 문자도 같이 포함한다. 그래서 모든 문자열 상수는 항상 널 종료 문자열이다. 코드에서 "Hey"라는 문자열 상수를 사용하면 정적 데이터 영역에는 "Hey\0"가 기록된다. 그래야 문자열 상수를 사용하는 곳에서 이 문자열의 길이를 알 수 있기 때문이다.

코드에서 문자열 상수는 이 문자열의 시작 번지를 가리키는 문자형 포인터 상수로 평가된다. 그래서 문자형 포인터를 인수로 요구하는 모든 함수에서 문자열 상수를 바로 사용할 수 있는 것이다. 다음 두 코드를 보자.

 

puts("Test");

printf("Result is %s","True");

 

이 코드에 사용된 "Test" 문자열 상수는 정적 데이터 영역에 기록되며 그 자체는 문자열의 시작 번지를 가리키는 문자형 포인터 상수이다. puts 함수의 첫 번째 인수는 const char *형이므로 문자열 상수를 인수로 사용할 수 있다. 컴파일러는 다른 상수와는 달리 문자열 상수를 특별나게 취급하는데 다음 두 가지 사항도 참고 삼아 알아 두도록 하자.

첫 번째로 같은 문자열 상수를 두 번 이상 사용하면 이 문자열은 한 번만 기록된다. 코드에서 "Copyright Miyoungsoft 2005"라는 문자열 상수를 여러 번 사용하고 있다고 할 때 이 문자열을 정적 데이터 영역에 매번 기록할 필요가 없다. 문자열 상수는 말 그대로 상수이기 때문에 여러 번 참조를 하더라도 한 번만 기록해 놓고 동일한 상수 번지를 돌려 주기만 하면 된다.

물론 매번 기록해도 되지만 불필요하게 실행 파일의 크기만 늘어나므로 좋지 않을 것이다. 제대로 된 컴파일러는 문자열 상수가 나타날 때마다 이미 앞에서 사용한 적이 있는지 비교해 보고 이미 기록되어 있으면 다시 기록하지 않는다. 따라서 사용자는 같은 문자열 상수를 부담없이 반복해서 사용해도 별 상관이 없다.

두 번째로 컴파일러는 공백이나 탭, 개행 코드 등으로만 구분된, 즉 중간에 콤마나 영문자 등이 없는 연속된 문자열 상수들을 하나로 합쳐서 기록한다. 다음의 간단한 코드를 실행해 보자.

 

puts("abcd" "efgh" " ijkl");

 

세 개의 문자열 상수를 사용하고 있지만 이 문자열 상수는 전처리 과정에서 통째로 합쳐져 정적 데이터 영역에 기록된다. 상수들 가운데에 있는 공백은 무시되며 상수 사이에 여분의 공백이 삽입되지 않는다. 그래서 출력되는 문자열은 "abcdefgh ijkl"이다. 아주 긴 문자열 상수를 표현하고자 할 때는 이 원리를 이용하면 된다.

 

puts("동해물과 백두산이 마르고 닳도록\n"

     "하느님이 보우하사 우리 나라 만세\n"

     "무궁화 삼천리 화려 강산\n"

     "대한 사람 대한으로 길이 보전하세");

 

문자열 상수가 길다고 해서 꼭 한 줄에 다 써야 하는 것은 아니다. 물론 한 줄에 다 쓸 수도 있지만 이렇게 되면 오른쪽 끝이 너무 길어져서 스크롤하면서 봐야 하므로 불편할 것이다. 따옴표를 일단 닫고 개행 한 후 다음 줄에 계속 써도 상관없다. 이 방법 외에 행 계속 문자인 \를 사용할 수도 있다.

 

puts("동해물과 백두산이 마르고 닳도록\n\

하느님이 보우하사 우리 나라 만세\n\

무궁화 삼천리 화려 강산\n\

대한 사람 대한으로 길이 보전하세");

 

행 계속 문자 \를 행의 끝에다 붙이면 다음 줄을 하나로 합쳐 주는데 주로 여러 줄의 매크로를 정의할 때 사용한다. 문자열 상수를 정의할 때도 사용할 수 있지만 보다시피 다음 줄이 반드시 행의 처음에 와야 한다는 제약이 있어 들여쓰기를 마음대로 조정할 수 없는 단점이 있다. 참고로 gcc는 문자열 상수내의 개행 코드를 인정하므로 행 계속 문자를 쓰지 않고도 여러 줄에 문자열 상수를 기술할 수 있다. 그러나 표준에 없는 임의 확장이므로 다소 비판의 여지가 있는 기능이다.