c언어 문자열 다루기

임정우·2023년 7월 24일
post-thumbnail

write()

저수준 파일 출력 함수

저수준 파일 입출력

  • 바이트 단위로 동작
    -> 빠르지만 바이트를 적당한 형태의 데이터로 변환하는 함수 등 여러 가지 추가적인 기능을 구현해야함

헤더: unistd.h
형태: ssize_t write (int fd, const void buf, size_t n)
반환: ssize_t 쓰기에 성공했다면 쓰기한 바이트 개수를, 실패했다면 -1을 반환
인수:
int fd: 파일 디스크립터 (1을 기입)
void
buf: 파일에 기록할 데이터를 저장하고 있는 메모리 영역
size_t n: 쓰기할 바이트 개수

> 파일디스크립터란?
모든 저수준 파일 입출력 함수는 파일 기술자를 사용
현재 열려있는 파일을 구분할 목적으로 유닉스가 붙여놓은 번호
파일 기술자는 정수
0: 표준 입력
1: 표준 출력
2: 표준 오류 출력

read()

저수준 파일 입력 함수

헤더: unistd.h
형태: ssize_t read(int fd, void *buf, size_t nbytes)
반환: write와 동일
인수: write와 동일 (fd에 0을 기입)

문자열

큰따옴표("")를 사용해 표현되는 문자열 = 문자열 상수(string constant)

*상수?
해당 문자열이 이름을 가지고 있지 않으며, 문자열의 내용 또한 변경할 수 없기 때문

C언어에서 문자열(string)은 메모리에 저장된 일련의 연속된 문자(character)들의 집합
-> 문자형 배열을 선언하면 이 배열이 곧 문자열 변수

이때 C는 문자열의 끝을 알려주는 널문자(\0)를 하나 더 삽입
-> 문자열과 쓰레기값을 구분

strcpy

설명: origin에 있는 문자열을 dest로 복사하는 함수
헤더: <string.h>
형태: char strcpy(char dest, const char origin)
인수:
char
dest: 복사한 값을 넣을 문자열
const char* origin: 복사할 문자열
반환값: 복사된 문자열에 대한 포인터

주의할 점:
널문자(\0)가 있는 곳을 문자열의 끝으로 판단하고, 널문자가 나올 때까지 복사한다.

char dest[10]에 "hello" 복사 가능
char dest[10]에 "hi im fine thank you" (22글자) 복사 불가능 (런타임 에러)

구현:

char	*replica_strcpy(char *dest, char *src)
{
	int	idx;

	idx = 0;
	while (src[idx] != '\0')
	{
		dest[idx] = src[idx];
		idx++;
	}
	dest[idx] = '\0';
	return (dest);
}

dest에 널포인터가 들어오면 프로그램이 죽는다.
0번지 다음에는 heap에 code section이 있는데, 이를 침범하게 되기 때문에 segment fault가 발생한다.
참조할 수 없는 영역이기 때문에 MMU에서 Memory Protection Fault 오류로 차단하는 것이다.

strncpy

설명: origin에 있는 문자열을 dest로 복사를 하는데, n 만큼만 복사하는 함수
헤더: <string.h>
형태: char strncpy(char dest, const char origin, size_t len)
반환값: 복사된 문자열에 대한 포인터
인수:
char
dest: 복사한 값을 넣을 문자열
const char* origin: 복사할 문자열
size_t len: 복사할 길이

주의할 점:
strncpy는 '\0'를 상관하지 않고 n의 길이만큼만 복사

dest2[100];
char origin[] = hi im fine thank you"; (22글자)
strncpy(dest2, origin, 4)
결과: "hi i" (\0 없이 복사 -> 문자열의 끝이 지정돼있지 않으므로 4번 인덱스부터 99번 인덱스에는 쓰레기값 저장)

구현

char	*replica_strncpy(char *dest, char *src, unsigned int n)
{	
	unsigned int		idx;

	idx = 0;
	while (*(src + idx) && idx < n)
	{
		*(dest + idx) = *(src + idx);
		++idx;
	}
	while (idx < n)
	{
		*(dest + idx) = '\0';
		++idx;
	}
	return (dest);
}

strlcpy

설명: strncpy와 유사하게 작동하지만, strncpy와 달리 항상 뒤에 널문자를 삽입한다.
헤더: <string.h>
형태: size_t strlcpy(char dest, const char origin, size_t dstsize)
반환값: 함수가 생성한 문자열의 총 길이
인수:
char dest: 복사한 값을 넣을 문자열
const char
origin: 복사할 문자열
size_t len: 복사할 길이

strncpy VS strlcpy

strncpy()
strncpy(str, "123456", 100);
>123456\0
strncpy(str, "123456", 4);
> 1234

strlcpy()
strlcpy(str, "123456", 100);
123456\0
strlcpy(str, "123456", 4);
123\0

이렇듯 strncpy는 널문자와 관계없이 항상 지정 인덱스까지 무조건 같게 만들고
strlcpy는 마지막 인덱스는 항상 널문자로 지정하여 저장하기 때문에 origin의 길이가 len보다 짧다면 len - 1만큼 저장하고 마지막은 널문자로 저장한다.

구현

unsigned	int	replica_strlcpy(char *dest, char *src, unsigned int size)
{
	unsigned int	idx;
	unsigned int	len_src;

	idx = 0;
	len_src = 0;
	while (*(src + len_src))
		++len_src;
	while (*(src + idx) && idx < size - 1 && size != 0)
	{
		*(dest + idx) = *(src + idx);
		++idx;
	}
	if (size > 0)
		*(dest + idx) = '\0';
	return (len_src);
}

strcmp

설명:
두 문자열이 같은지 판단하는 함수다.
두 문자열이 같으면 0 다르면 양수 혹은 음수를 반환한다.
앞에서부터 하나씩 비교하다가 다른 문자열이 나오면 앞 문자에서 뒤 문자의 차이값을 반환한다.
따라서 앞 문자가 더 크면 양수를 작으면 음수를 반환한다.
Ex)
strcmp(“ABC”, “ABC”) = 0
strcmp(“ABF”,”ABC”) > 0
strcmp(“AB”, “ABC”) < 0

헤더: <string.h>
형태: char strcmp( const char s1, const char * s2 )

구현

int	replica_strcmp(char *s1, char *s2)
{
	int	idx;

	idx = 0;
	while ((s1[idx] != '\0') && (s2[idx] != '\0') && (s1[idx] == s2[idx]))
		idx ++;
	return (s1[idx] - s2[idx]);
}

strncmp

설명:
strcmp와 유사하게 동작하지만, n번째까지만 비교한다
반환값은 strcmp와 동일하게 두 문자열이 같으면 0 다르면 양수 혹은 음수를 반환한다.
Ex)
strcmp(“ABC”, “AB”, 2) = 0
strcmp(“ABF”,”AB”, 3) > 0

헤더: <string.h>
형태: char strncmp( const char s1, const char * s2, size_t n )

구현

int	replica_strncmp(char *s1, char *s2, unsigned int n)
{
	unsigned int	idx;

	if (n == 0)
		return (0);
	idx = 0;
	while (s1[idx] != '\0' && s2[idx] != '\0' && s1[idx] == s2[idx] && idx < n)
		idx++;
	if (idx == n)
		return (s1[n - 1] - s2[n - 1]);
	else
		return (s1[idx] - s2[idx]);
}

strcat

설명:origin에 있는 문자열을 dest 뒤쪽에 이어 붙이는 함수, dest 문자열 끝의 널문자는 삭제되고 그 위치에 origin이 붙는다.
ex)
char origin[] = "aaa";
char dest[20] = "bbb";
strcat(dest, origin);
printf("%s",dest) > aaabbb

형태: char strcat(char dest, const char src);
인수:
char
dest: src를 붙일 대상 문자열
char* src: dest 뒤에 붙여질 문자열

구현

char	*replica_strcat(char *dest, char *src)
{
	int	si;
	int	di;

	di = 0;
	while (dest[di] != '\0')
		di++;
	si = 0;
	while (src[si] != 0)
		dest[di++] = src[si++];
	dest[di] = '\0';
	return (dest);
}

strncat

구현

char	*replica_strncat(char *dest, char *src, unsigned int nb)
{
	unsigned int	di;
	unsigned int	si;

	di = 0;
	si = 0;
	while (dest[di] != '\0')
		di++;
	while (si < nb && src[si] != '\0')
		dest[di++] = src[si++];
	dest[di] = '\0';
	return (dest);
}

strlcat

구현

int	len_arr(char *str)
{
	int	i;

	i = 0;
	while (str[i])
		i++;
	return (i);
}

unsigned int	replica_strlcat(char *dest, char *src, unsigned int size)
{
	unsigned int	idx1;
	unsigned int	idx2;
	unsigned int	len_dest;
	unsigned int	len_src;

	if (src[0] == '\0')
		return (0);
	idx1 = len_arr(dest);
	idx2 = 0;
	len_dest = len_arr(dest);
	len_src = len_arr(src);
	if (len_dest > size)
		return (size + len_src);
	while ((idx1 + 1 < size) && (src[idx2] != '\0'))
		dest[idx1++] = src[idx2++];
	dest[idx1] = '\0';
	return (len_src + len_dest);
}

strstr

설명:
str안에서 to_find가 있는지 판단하고, 만약 있다면 그 위치에 해당하는 포인터를 반환하며, 없다면 널포인터를 반환한다.

구현

char	*ft_strstr(char *str, char *to_find)
{
	int	idx1;
	int	idx2;

	idx1 = 0;
	idx2 = 0;
	if (*to_find == '\0')
		return (str);
	while (str[idx1] != '\0')
	{
		if (str[idx1] == to_find[idx2])
		{
			if (to_find[++idx2] == '\0')
				return (&str[idx1] - (idx2 - 1));
		}
		else
			idx2 = 0;
		idx1++;
	}
	return (0);
}

strdup

설명: strcpy는 단순히 문자열만 복사하지만, strdup은 추가적으로 메모리를 할당해준다.
헤더: <string.h>
형태: char strdup(const char string)
반환값: 복사된 데이터의 주소 (에러 발생시 null반환)
인수:
데이터를 복사할 주소, const *char형

strcpy가 있는데 strdup이 필요한 이유
strcpy는 복사된 배열을 스택에 저장한다. 반면, strdup은 힙에 메모리를 새로 할당하여 저장한다.
main 함수에 직접 사용하거나 함수내에서도 반환할 필요가 없는 경우에는 큰 차이가 없다.
하지만 함수내에서 반환할 필요가 있을 때 strcpy를 사용하여 복사된 배열을 반환하려고 하면 에러가 발생한다.
이 때 복사된 배열은 스택에 저장되었기 때문에 함수가 끝나면 소멸되기 때문이다.
따라서 이런 경우에는 strdup을 이용하여 복사해야한다.
또 사용자는 strdup이 malloc 함수 호출로 메모리를 할당하기 때문에 반환 된 char 포인터를 해제해야한다.

구현

#include <stdlib.h>

char	*ft_strdup(char *src)
{
	char	*str;
	int 	size;
	int		i;

	size = 0;
	i = 0;
	while (!(src + size))
		size++;
	str = (char *)malloc(size);
	while (src[i])
	{
		str[i] = src[i];
		++i;
	}
	return (str);
}

c에서 메모리 형태

메모리는 스택 메모리와 힙 메모리로 나눌 수 있다.

heap:

  • 쌓아 올린 더미를 의미,
  • 범용적인 기본 형태,
  • 메모리를 사용하려면 가져가고 가져간 후에 돌려놔야함

stack:

  • 쓰레드마다 특별한 용도로 사용하려고 별도로 뗴어 둔 메모리 (예시: 함수를 호출할 때 매개변수 전달할 때 사용)
  • 높은 주소부터 낮은 주소에 저장

스택 메모리의 단점:

  • 수명: 함수가 반환되면 그 안에 있던 데이터가 사라짐
  • 크기: 특정 용도로 떼어놨기 때문에 컴파일할 때 크게 잡지 못함

힙 메모리의 장점:

  • 원하는 때에 반납: 컴파일러나 cpu가 메모리 관리를 안하기 때문에 프로그래머가 원하는 떄에 원하는 만큼 할당하여 반납
  • 용량 제한이 없음: 남아있는 메모리만큼 사용가능

힙 메모리의 단점:

  • 메모리 누수 가능
  • 스택에 비해 느림

예시:

#include <stdio.h>

void fct1(int);
void fct2(int);

int a = 10;	// 데이터 영역에 할당
int b = 20;	// 데이터 영역에 할당

int main() {

	int i = 100;	// 지역변수 i가 스택 영역에 할당

	fct1(i);
	fct2(i);

	return 0;
}

void fct1(int c) {
	int d = 30;	// 매개변수 c와 지역변수 d가 스택영역에 할당
}

void fct2(int e) {
	int f = 40;	// 매개변수 e와 지역변수 f가 스택영역에 할당
}

메모리의 스택 영역은 함수의 호출과 관계되는 지역 변수와 매개변수가 저장되는 영역이다.
스택 영역은 함수의 호출과 함께 할당되며, 함수의 호출이 완료되면 소멸한다.
재귀함수에서 무한 재귀가 발생하면 스택 오버 플로우가 발생하는 이유도 이 때문이다.

int main() {
	
	int i = 10;
	int arr[i];

	return 0;
}

다음 코드는 작동하지 않는다.
스택 메모리에 할당될 공간은 컴파일 시간에 결정된다.
컴파일 시간에는 int i가 4바이트라는 사실을 알 수 있으나, i에 무슨 값이 저장되는지는 알 수 없다.
i에 10이 저장되는 것은 런타임에 결정되기 때문이다.
따라서 컴파일 시간에 arr의 크기를 결정해야하지만, i에 무슨 값이 저장되었는지 알 수 없으므로 이 코드는 작동하지 않는다.

메모리의 힙 영역은 사용자가 직접 관리할 수 있는 ‘그리고 해야만 하는’ 메모리 영역이다.
힙 영역은 런타임때 사용자에 의해 메모리 공간이 동적으로 할당되고 해제된다.
힙 관리자에게 원하는 바이트 수만큼 할당 요청 -> 관리자는 그만큼 연속된 메모리를 찾아서 반환 이때 저장하는 자료형은 포인터

런타임:
컴파일 과정을 마친 컴퓨터 프로그램이 실행되고 있는 환경 또는 동작되는 동안의 시간을 말한다.

참고

레지스터:

  • 메모리는 아니며,
  • 휘발성으로 데이터를 저장하는 공간,
  • 메모리에 접근하지 않고 cpu에서 처리하기 때문에 빠르게 처리할 수 있다는 특징
    (메모리에 접근하는 시간을 아낄 수 있고, 메모리는 보통 DRAM이고 이는 정보를 기록하는데에 때문에 느림 SRAM은 비쌈)
  • 레지스터를 사용하려면 어셈블리 언어로 가능
  • c에서는 register <자료형> <변수형>으로 가능

void 자료형

int a=1;
void *b=&a;

void는 컴퓨터가 자료형을 모를 때 빈공간처럼 볼 때도 이용된다.
위의 코드를 해석하면
"b는 a의 주소를 가지고 있다. 그리고 그 안에는 void형 데이터가 있다."가 된다.
따라서 다음은 오류가 발생한다. void형은 딱 포인터만큼의 크기만 갖고 있고 얼마나 참조해야하는지 모르기 때문이다.

printf("%d\n", *b); //오류

따라서 void안의 값을 다루고 싶다면 형변환을 통하여 어떤 크기만큼 참조해야할지 알려줘야 한다.

printf("%d\n", *(int*)b); //정상작동

profile
경희대학교 소프트웨어융합학과

2개의 댓글

comment-user-thumbnail
2023년 7월 24일

유익한 글이었습니다.

1개의 답글