뭐라도 쓰겠지
25.03.12 / 문자열 본문
이번 글에선 문자열에 대해 알아보겠다.
문자열이란 문자의 연속으로 이루어진 배열을 말한다.
char strHello[] = { 'H', 'e', 'l', 'l', 'o', '\0' };
char *strWorld = "World";
위는 문자열 변수를 선언하는 방법이고, 아래는 문자열 상수를 선언하는 방법이다.
문자열 변수는 마지막 요소에 NULL 문자를 붙여 문자열의 끝을 알려줘야하고, 문자열 상수는 자동으로 마지막에 NULL 문자를 넣어준다.
char strHello[] = { 'H', 'e', 'l', 'l', 'o', '\0' };
strHello[4] = 'O';
printf("%s\n", strHello);
char* strWorld = "World";
strWorld[4] = 'D'; // 실행 실패
printf("%s\n", strWorld);
출력값
HellO
World
문자열 변수는 내부 요소를 바꾸는게 가능하지만, 문자열 상수는 상수이기 때문에 내부 내용을 바꿀 수 없다.
이번엔 문자열과 관련된 함수 4가지와 문자열 출력 함수를 알아보자.
기본적으로 C언어는 strlen, strcpy, strcmp, strcat을 지원한다. 각각 문자열의 길이 구하기, 문자열 복사하기, 문자열 비교하기, 문자열 합치기 함수이다. 문자열의 출력은 printf로 %s 포맷을 받아 출력할 수 있다. 각 함수를 직접 간단하게 구현해보도록 하자.
void PrintString(const char* const _pStr) {
if (_pStr == NULL) return; // 예외 처리
int i = 0 ;
while (_pStr[i] != '\0') {
printf("%c", _pStr[i]);
++i;
}
}
문자열 _pStr을 입력받아 문자열을 출력하는 함수다. _pStr의 길이를 모르기 때문에 for문이 아닌 while문으로 작성했다 (strlen 등을 이용해 문자열의 길이를 안다면 for문으로도 출력할 수 있다). 최종 길이는 모르지만 일단 i를 0부터 시작해 _pStr[0]부터 1씩 늘려 출력한다. 그러다 NULL 문자를 만나게 되면 문자 출력과 i의 증가를 멈춘다. NULL 문자를 제외하고 문자를 연속으로 출력해 문자열이 출력되게 된다.
// 반환값이 0보다 크면 NULL 문자를 제외한 문자열의 크기
// 반환값이 -1이면 문자열이 NULL
int StrLen(const char* const _pStr) {
if (_pSter == NULL) return -1;
int cnt = 0;
while (_pStr[cnt] != '\0') ++cnt;
return cnt;
}
문자열 _pStr을 입력받아 문자열의 길이를 구하는 함수다. 문자열이 NULL이면 길이를 구할 수 없기 때문에 에러 코드인 -1을 반환한다. 위 코드와 마찬가지로 길이를 모르기 때문에 while문으로 작성해보겠다. 최종 길이는 모르지만 일단 cnt를 0부터 시작해 _pStr[cnt]의 값이 NULL 문자가 되기 전까지 ++cnt를 진행한다. 예를 들어 Hello라고 가정하면 _pStr[0] = H, _pStr[1] = e, ... , _pStr[4] = o가 되는데 _pStr[5]는 NULL 문자이기 때문에 cnt의 증가를 멈추고 반환한다. Hello의 길이, cnt의 최종값은 5.
void StrCpy(char* const _pDst, const char* const _pSrc) {
if (_pDst == NULL || _pSrc == NULL) return;
int srcLen = StrLen(_pSrc);
for (int i = 0; i < srcLen; ++i;) {
_pDst[i] = _pSrc[i];
}
_pDst[srcLen] = '\0';
}
복사 될 목적지 _pDst와 원본 _pSrc를 매개변수로 받아 원본의 내용을 복사해 목적지로 붙여넣는 함수다. 원본이 NULL이거나 복사받을 공간이 NULL이면 복사가 되지 않으므로 예외 처리를 했다. 저번 글에 설명했듯, 함수 안에서 바깥의 변수를 바꾸기 위해선 깊은 복사(Deep Copy)가 필요하다. 우선 위에서 선언한 StrLen 함수를 이용해 _pSrc의 길이를 알아낸다. i의 값을 늘려가며 해당하는 i번째의 원본값을 목적지 값에 대입한다. 마지막에 i가 srcLen이 되면, 즉 srcLen + 1번째 문자열을 대입할 때가 되면 NULL 문자를 대입해 문자열의 종료를 알린다.
그런데 이 코드는 _pDst의 크기가 _pSrc의 크기보다 큰 경우 문제없이 복사가 진행되지만, 작은 경우엔 오류가 발생한다. 이를 버퍼 오버플로우라고 하는데, 버퍼 오버플로우가 발생하면 프로그램이 크래시되거나 다른 메모리 영역이 덮어쓰여져 예상치 못한 결과가 실행되거나 보안 취약점이 발생할 수 있다. 이를 방지하기 위해 우리는 _pDst의 크기를 인자로 받아 문자열의 크기를 제한해야 한다.
void StrCpy(char* const _pDst, const char* const _pSrc, int _dstSize) {
if (_pDst == NULL || _pSrc == NULL || _DstSize == 0) return;
int srcLen = StrLen(_pSrc);
int i = 0;
for (i = 0; i < srcLen && i < _dstSize - 1; ++i;) {
_pDst[i] = _pSrc[i];
}
_pDst[i] = '\0';
}
이렇게 _pDst의 사이즈를 입력받아 for문의 조건에 추가했다. _dstSize -1 인 이유는 마지막에 NULL 문자를 넣어야해 실제 문자를 복사할 수 있는 공간은 _dstSize보다 1 작기 때문이다. 이렇게 하면 버퍼 오버플로우를 방지할 수 있다.
#define TRUE 1
#define FALSE 0
int StrCmp(const char* const _pLhs, const char* const _pRhs) {
if (_pLhs == NULL || _pRhs == NULL) return FALSE;
int lhsLen = StrLen(_pLhs);
int rhsLen = StrLen(_pRhs);
if (lhsLen != rhsLen) return FALSE;
for (int i = 0; i < lhsLen; ++i) {
if (_pLhs[i] != _pRhs[i]) return FALSE;
}
return TRUE;
}
_pLhs와 _pRhs를 입력받아 두 문자열이 같은 지 검사하고 같다면 TRUE(1), 틀리다면 FALSE(0)를 반환하는 함수다. 가시성을 위해 define으로 TRUE는 1, FALSE는 0을 치환해서 쓴다. C언어가 제공하는 strcmp는 어디가 더 큰 배열인지 출력해주는 기능이 있지만, 이번엔 비교의 원리만 간단하게 알아보기 위해 같으면 TRUE(1), 다르면 FALSE(0)을 출력한다.
우선 당연히 _pLhs와 _pRhs 둘 중 하나라도 NULL이면 비교가 불가능하니 FALSE를 반환한다.
그 다음으론 우선 두 문자열의 크기를 구해 비교하는데 크기가 다르면 당연히 값도 다를테니 FALSE를 반환한다.
마지막으로 반복문을 사용해 각 문자열의 i번째 문자를 비교해 (여기까지 왔다면 둘의 크기도 같기 때문에 lhsLen, rhsLen 어느걸 넣어도 괜찮다) 다른게 있다면 FALSE, 모두 같다면 반복문을 종료해 TRUE를 반환한다.
void StrCat(char* const _pDst, const char* const _pStr1, const char* const _pStr2) {
if (_pDst == NULL || _pStr1 == NULL || _pStr2 == NULL) return;
int str1Len = StrLen(_pStr1);
int str2Len = StrLen(_pStr2);
for (int i = 0; i < str1Len; ++i) {
_pDst[i] = _pStr1[i];
}
for (int i = 0; i < str2Len; ++i) {
_pDst[str1Len + i] = _pStr2[i];
}
_pDst[str1Len + str2Len] = '\0';
}
_pStr1, _pStr2 두가지 문자열을 입력받아 _pDst로 합치는 함수다. 역시 _pDst, _pStr1, _pStr2 중 하나라도 NULL이면 복사가 이뤄지지 않기 때문에 예외 처리를 했다.
_pDst의 크기를 구하기 위해 먼저 _pStr1과 _pStr2의 크기를 구한다.
처음엔 인덱스가 똑같기 때문에 _pDst[i]에 _pStr1[i]의 문자를 대입한다. 그리고 그 뒤에 이어져야 하는 _pStr2의 값부터는 이미 입력된 요소 이후, 즉 _pStr1의 길이에 i를 더한 번호째에 이어서 넣어야 하기 때문에, _pDst[str1Len + i]에 _pStr2[i]의 문자를 대입한다.
그리고 마지막으로 _pDst[str1Len + str2Len]에 NULL 문자를 넣어 문자열의 끝을 알린다.
이미 구현되어 있는 함수를 쓰는 것도 좋지만, 이런 식으로 직접 함수를 구현해보면 그 원리와 작동 과정을 보다 더 이해할 수 있다. 항상 더 많은 의구심을 가지고 코드를 분석해보는게 좋은 개발자가 되기 위한 기본 소양이라 볼 수 있겠다.
'프로그래밍 > C' 카테고리의 다른 글
| 25.03.12 / 2중 포인터와 함수를 이용한 문자열의 동적 할당과 복사 (0) | 2025.03.12 |
|---|---|
| 25.03.11 / 다중 차원 배열 (0) | 2025.03.11 |
| 25.03.11 / 동적 배열 (0) | 2025.03.11 |
| 25.03.11 / 배열 (0) | 2025.03.11 |
| 25.03.10 / 메모리의 동적 할당 (0) | 2025.03.10 |