본 문서는 C 언어의 포인터 및 문자열 처리 기능을 활용한 다양한 프로그래밍 연습 과제를 포함합니다.
Task 1-1: 배열 요소의 최대값 및 최소값 찾기
정수 배열의 모든 요소를 순회하며 최대값과 최소값을 찾는 함수를 구현합니다. 함수는 배열의 첫 번째 요소의 주소를 최소값과 최대값의 초기값으로 설정하고, 나머지 요소를 검사하며 값을 갱신합니다. 이 과정에서 포인터를 사용하여 최소값과 최대값의 주소를 전달받아 값을 수정합니다.
#include <stdio.h>
#include <stdlib.h>
#define ARRAY_SIZE 5
void fill_array(int arr[], int count);
void display_array(int arr[], int count);
void find_extremes(int arr[], int count, int *min_val, int *max_val);
int main() {
int data_array[ARRAY_SIZE];
int minimum, maximum;
printf("%d개의 데이터를 입력하세요:\n", ARRAY_SIZE);
fill_array(data_array, ARRAY_SIZE);
printf("입력된 데이터: \n");
display_array(data_array, ARRAY_SIZE);
printf("데이터 처리 중...\n");
find_extremes(data_array, ARRAY_SIZE, &minimum, &maximum);
printf("결과 출력:\n");
printf("최소값 = %d, 최대값 = %d\n", minimum, maximum);
system("pause");
return 0;
}
void fill_array(int arr[], int count) {
for(int i = 0; i < count; ++i)
scanf("%d", &arr[i]);
}
void display_array(int arr[], int count) {
for(int i = 0; i < count; ++i)
printf("%d ", arr[i]);
printf("\n");
}
void find_extremes(int arr[], int count, int *min_val, int *max_val) {
*min_val = arr[0];
*max_val = arr[0];
for(int i = 1; i < count; ++i) {
if(arr[i] < *min_val)
*min_val = arr[i];
if(arr[i] > *max_val)
*max_val = arr[i];
}
}
- 문제 1: 입력된 데이터에서 최대값과 최소값을 찾습니다.
- 문제 2: 최소값과 최대값을 저장하기 위한 포인터가 배열의 첫 번째 요소의 메모리 주소를 가리키도록 초기화하는 것은 올바르지 않습니다. 각 포인터는 개별 변수 `minimum`과 `maximum`의 주소를 가리켜야 합니다.
Task 1-2: 배열 요소 중 최대값의 주소 반환
정수 배열에서 가장 큰 값을 가진 요소의 메모리 주소를 반환하는 함수를 구현합니다. 함수는 배열의 첫 번째 요소의 주소를 초기 최대값 포인터로 설정하고, 배열을 순회하며 더 큰 값이 발견될 때마다 해당 요소의 주소를 가리키도록 포인터를 업데이트합니다. 최종적으로 최대값을 가리키는 포인터를 반환합니다.
#include <stdio.h>
#include <stdlib.h>
#define ARRAY_SIZE 5
void fill_array(int arr[], int count);
void display_array(int arr[], int count);
int *find_max_element(int arr[], int count);
int main() {
int data_array[ARRAY_SIZE];
int *max_ptr;
printf("%d개의 데이터를 입력하세요:\n", ARRAY_SIZE);
fill_array(data_array, ARRAY_SIZE);
printf("입력된 데이터: \n");
display_array(data_array, ARRAY_SIZE);
printf("데이터 처리 중...\n");
max_ptr = find_max_element(data_array, ARRAY_SIZE);
printf("결과 출력:\n");
printf("최대값 = %d\n", *max_ptr);
system("pause");
return 0;
}
void fill_array(int arr[], int count) {
for(int i = 0; i < count; ++i)
scanf("%d", &arr[i]);
}
void display_array(int arr[], int count) {
for(int i = 0; i < count; ++i)
printf("%d ", arr[i]);
printf("\n");
}
int *find_max_element(int arr[], int count) {
int *current_max_ptr = &arr[0]; // 첫 번째 요소의 주소를 초기 최대값 포인터로 설정
for(int i = 1; i < count; ++i) {
if(arr[i] > *current_max_ptr) {
current_max_ptr = &arr[i]; // 더 큰 값이 발견되면 포인터 업데이트
}
}
return current_max_ptr;
}
- 문제 1: 입력된 데이터에서 최대값을 찾습니다.
- 문제 2: 가능합니다. 포인터 변수를 사용하여 다른 문자열 리터럴의 주소를 할당할 수 있습니다.
Task 2-1: 문자열 배열의 복사 및 크기 비교
두 개의 문자열 배열을 선언하고, `sizeof` 연산자와 `strlen` 함수를 사용하여 각각의 배열 크기와 실제 문자열 길이를 비교합니다. 이후, 임시 배열을 사용하여 두 문자열의 내용을 교환하고, 교환 전후의 결과를 출력합니다. 이 과정에서 배열의 `sizeof`는 할당된 전체 메모리 크기를, `strlen`은 널 종료 문자(`\0`) 이전까지의 길이를 반환함을 확인합니다.
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define BUFFER_SIZE 80
int main() {
char source_str[BUFFER_SIZE] = "C programming is fun";
char destination_str[BUFFER_SIZE] = "C++ programming is challenging";
char temp_buffer[BUFFER_SIZE];
printf("sizeof(source_str) vs. strlen(source_str): \n");
printf("sizeof(source_str) = %zu\n", sizeof(source_str)); // %zu는 size_t 타입에 대한 형식 지정자
printf("strlen(source_str) = %zu\n", strlen(source_str));
printf("\n교환 전: \n");
printf("source_str: %s\n", source_str);
printf("destination_str: %s\n", destination_str);
printf("\n교환 중...\n");
strcpy(temp_buffer, source_str);
strcpy(source_str, destination_str);
strcpy(destination_str, temp_buffer);
printf("\n교환 후: \n");
printf("source_str: %s\n", source_str);
printf("destination_str: %s\n", destination_str);
system("pause");
return 0;
}
- 문제 1: `sizeof(s1)`는 배열 `s1`이 할당받은 전체 메모리 크기(80바이트)를 반환하고, `strlen(s1)`은 문자열의 실제 길이(널 종료 문자 제외)를 반환합니다.
- 문제 2: 불가능합니다. `s1`은 배열의 시작 주소를 나타내는 상수이며, 이 주소 자체를 재할당할 수 없습니다. 문자열 내용을 복사하는 `strcpy` 함수를 사용해야 합니다.
- 문제 3: 문자열 내용이 성공적으로 교환되었습니다.
Task 2-2: 문자열 포인터의 교환
문자열 리터럴을 가리키는 두 개의 문자열 포인터를 선언합니다. `sizeof` 연산자와 `strlen` 함수를 사용하여 포인터 자체의 크기(메모리 주소의 크기)와 포인터가 가리키는 문자열의 길이를 비교합니다. 이후, 임시 포인터를 사용하여 두 문자열 포인터가 가리키는 내용을 교환하고, 교환 전후의 결과를 출력합니다. 이를 통해 포인터 자체의 크기와 문자열의 길이를 구분하고, 포인터 교환 시 실제 문자열 리터럴은 변경되지 않음을 확인합니다.
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define STRING_LEN 80
int main() {
char *str_ptr1;
char *str_ptr2 = "Learning makes me sleepy";
char *temp_ptr;
str_ptr1 = "Learning makes me happy"; // 문자열 리터럴의 주소를 포인터에 할당
printf("sizeof(str_ptr1) vs. strlen(str_ptr1): \n");
printf("sizeof(str_ptr1) = %zu\n", sizeof(str_ptr1)); // 포인터 자체의 크기
printf("strlen(str_ptr1) = %zu\n", strlen(str_ptr1)); // 가리키는 문자열의 길이
printf("\n교환 전: \n");
printf("str_ptr1: %s\n", str_ptr1);
printf("str_ptr2: %s\n", str_ptr2);
printf("\n교환 중...\n");
temp_ptr = str_ptr1;
str_ptr1 = str_ptr2;
str_ptr2 = temp_ptr;
printf("\n교환 후: \n");
printf("str_ptr1: %s\n", str_ptr1);
printf("str_ptr2: %s\n", str_ptr2);
system("pause");
return 0;
}
- 문제 1: `sizeof(s1)`는 포인터 변수 `s1` 자체가 차지하는 메모리 크기(일반적으로 4 또는 8바이트)를 반환하며, `strlen(s1)`은 `s1`이 가리키는 문자열의 길이를 반환합니다.
- 문제 2: Task 2-1과 달리, 여기서는 포인터 변수를 사용하므로 직접 주소 값을 재할당하여 다른 문자열 리터럴을 가리키도록 변경할 수 있습니다.
- 문제 3: `s1`과 `s2` 포인터가 가리키는 주소가 교환되었으며, 실제 문자열 리터럴 데이터는 메모리 상에서 이동하거나 변경되지 않습니다.
Task 3: 2차원 배열과 포인터
2차원 배열 `x`를 선언하고, 다양한 방법으로 배열 요소에 접근하는 방법을 실습합니다. 직접 접근 방식(`x[i][j]`), 1차원 포인터(`ptr1`)를 이용한 접근 방식, 그리고 2차원 배열의 행(1차원 배열)을 가리키는 포인터(`ptr2`)를 이용한 접근 방식을 비교합니다. 각 방식의 포인터 연산과 역참조 방법을 통해 2차원 배열의 메모리 구조를 이해하고, 포인터를 활용한 유연한 데이터 접근 방법을 익힙니다.
#include <stdio.h>
#include <stdlib.h>
int main() {
int matrix[2][4] = {{1, 9, 8, 4}, {2, 0, 4, 9}};
int i, j;
int *element_ptr; // 각 요소를 가리키는 포인터
int (*row_ptr)[4]; // 4개의 int 요소를 가진 1차원 배열(행)을 가리키는 포인터
printf("출력 1: 배열 이름과 인덱스를 이용한 직접 접근\n");
for (i = 0; i < 2; ++i) {
for (j = 0; j < 4; ++j)
printf("%d ", matrix[i][j]);
printf("\n");
}
printf("\n출력 2: 요소 포인터(element_ptr)를 이용한 간접 접근\n");
// &matrix[0][0]은 배열의 첫 번째 요소의 주소입니다.
// 총 요소 수는 2 * 4 = 8개입니다.
for (element_ptr = &matrix[0][0], i = 0; element_ptr < &matrix[0][0] + 8; ++element_ptr, ++i) {
printf("%d ", *element_ptr);
if ((i + 1) % 4 == 0) // 각 행이 끝날 때 줄바꿈
printf("\n");
}
printf("\n출력 3: 행 포인터(row_ptr)를 이용한 간접 접근\n");
// 'x'는 첫 번째 행(int[4] 타입)의 주소와 같습니다.
for (row_ptr = matrix; row_ptr < matrix + 2; ++row_ptr) {
// *row_ptr는 현재 행(int[4] 타입)을 나타냅니다.
// *row_ptr + j 는 해당 행의 j번째 요소의 주소입니다.
for (j = 0; j < 4; ++j)
printf("%d ", *(*row_ptr + j));
printf("\n");
}
system("pause");
return 0;
}
- 문제 1: `int (*ptr)[4]`에서 `ptr`은 4개의 `int` 요소를 가지는 1차원 배열을 가리키는 포인터, 즉 2차원 배열의 한 행(row)을 가리키는 포인터임을 의미합니다.
- 문제 2: `int *ptr[4]`는 4개의 `int` 포인터를 요소로 가지는 배열을 의미합니다.
Task 4: 문자열 내 특정 문자 치환
주어진 문자열에서 특정 문자를 다른 문자로 모두 치환하는 함수 `replace`를 구현합니다. 함수는 문자열의 시작 주소와 치환할 원본 문자, 대상 문자를 인자로 받습니다. 문자열을 순회하면서 원본 문자를 찾으면 해당 위치의 문자를 대상 문자로 변경합니다. 이 과정을 통해 포인터를 사용하여 문자열을 직접 수정하는 방법을 실습합니다.
#include <stdio.h>
#include <stdlib.h>
#define BUFFER_SIZE 80
// 함수 선언
void replace_char(char *str, char old_char, char new_char);
int main() {
char text_buffer[BUFFER_SIZE] = "Programming is difficult or not, it is a question.";
printf("원본 텍스트: \n");
printf("%s\n", text_buffer);
// 'i'를 '*'로 치환
replace_char(text_buffer, 'i', '*');
printf("처리 후 텍스트: \n");
printf("%s\n", text_buffer);
system("pause");
return 0;
}
// 함수 정의
void replace_char(char *str, char old_char, char new_char) {
// 문자열의 끝(널 문자 '\0')에 도달할 때까지 반복
while(*str != '\0') {
if(*str == old_char) {
*str = new_char; // 문자가 일치하면 치환
}
str++; // 다음 문자로 포인터 이동
}
}
- 문제 1: 문자열 내의 특정 문자를 다른 문자로 모두 변경합니다.
- 문제 2: 가능합니다. 포인터를 사용하면 문자열의 내용을 직접 수정할 수 있습니다.
Task 5: 문자열에서 특정 문자 이후 절단
입력받은 문자열에서 특정 문자를 만나면 그 위치부터 문자열 끝까지를 널 문자로 대체하여 문자열을 절단하는 함수 `str_trunc`를 구현합니다. 함수는 문자열 포인터와 절단 기준 문자를 인자로 받습니다. 문자열을 순회하며 기준 문자를 찾으면 해당 위치에 널 문자를 삽입하고 함수를 종료합니다. 이는 문자열을 동적으로 다루는 방법을 연습하는 데 도움이 됩니다. `gets` 함수 사용 시 입력 버퍼 관리에 주의해야 합니다.
#include <stdio.h>
#include <stdlib.h>
#define BUFFER_SIZE 80
char *truncate_string(char *str, char cut_char);
int main() {
char input_str[BUFFER_SIZE];
char delimiter_char;
// gets 함수는 보안상 취약하므로 사용하지 않는 것이 좋습니다.
// fgets 함수 사용을 권장합니다.
while(printf("문자열을 입력하세요 (종료하려면 Enter만 입력): "), fgets(input_str, BUFFER_SIZE, stdin) != NULL) {
// fgets는 개행 문자(\n)도 포함하므로 제거합니다.
input_str[strcspn(input_str, "\n")] = '\0';
// 입력이 빈 문자열이면 종료
if (input_str[0] == '\0') break;
printf("문자열을 절단할 기준 문자를 입력하세요: ");
delimiter_char = getchar();
// getchar() 호출 후 남아있는 개행 문자 제거
while(getchar() != '\n');
printf("절단 처리 중...\n");
truncate_string(input_str, delimiter_char); // 함수 호출
printf("절단 처리된 문자열: %s\n\n", input_str);
}
system("pause");
return 0;
}
char *truncate_string(char *str, char cut_char)
{
while (*str != '\0') {
if (*str == cut_char) {
*str = '\0'; // 기준 문자를 만나면 널 문자로 대체하여 절단
break; // 절단 후 루프 종료
}
str++;
}
return str; // 절단된 위치 또는 문자열 끝의 포인터 반환 (여기서는 사용되지 않음)
}
- 문제: 두 번째 데이터 세트 이후 결과가 올바르게 나오지 않는 문제는 `getchar()` 함수 호출 후 입력 버퍼에 남아있는 개행 문자를 처리하지 않아 발생합니다. `while(getchar() != '\n');`와 같은 코드를 추가하여 버퍼를 비워주어야 다음 `gets` 또는 `getchar` 함수가 정상적으로 동작합니다.
Task 6: 주민등록번호 형식 검사
주어진 문자열이 유효한 한국 주민등록번호 형식(18자리 숫자 또는 X)인지 검사하는 함수 `check_id`를 구현합니다. 함수는 문자열의 길이가 18자리인지 확인하고, 첫 17자리가 숫자인지, 마지막 문자가 숫자 또는 'X'인지 검사합니다. 형식에 맞으면 1을, 그렇지 않으면 0을 반환합니다. 이 과제를 통해 문자열 길이 검사, 문자별 검사, 그리고 조건부 반환을 활용한 데이터 유효성 검증을 실습합니다.
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define NUM_IDS 5
// 함수 선언
int validate_id_format(const char *id_str);
int main() {
// 주민등록번호 형식에 대한 예시 데이터
const char *id_list[NUM_IDS] = {"31010120000721656X",
"330106199602033011", // 마지막 문자를 숫자로 변경
"53010220051126571", // 17자리
"510104199211197977",
"53010220051126133Y"}; // 마지막 문자가 'Y'
for (int i = 0; i < NUM_IDS; ++i) {
if (validate_id_format(id_list[i])) { // 함수 호출
printf("%s\t유효\n", id_list[i]);
} else {
printf("%s\t유효하지 않음\n", id_list[i]);
}
}
system("pause");
return 0;
}
// 함수 정의
// 기능: 입력된 주민등록번호 문자열의 형식이 유효한지 검사
// 형식이 유효하면 1을, 그렇지 않으면 0을 반환
int validate_id_format(const char *id_str) {
int len = strlen(id_str);
// 1. 길이 검사: 정확히 18자리여야 합니다.
if (len != 18) {
return 0;
}
// 2. 앞 17자리 숫자 검사
for (int i = 0; i < 17; ++i) {
if (id_str[i] < '0' || id_str[i] > '9') {
return 0; // 숫자가 아닌 문자가 발견되면 실패
}
}
// 3. 마지막 18번째 문자 검사: 숫자 또는 'X'여야 합니다.
char last_char = id_str[17];
if (!((last_char >= '0' && last_char <= '9') || last_char == 'X')) {
return 0; // 숫자 또는 'X'가 아니면 실패
}
// 모든 검사를 통과하면 유효함
return 1;
}
Task 7: 간단한 문자열 암호화 및 복호화 (Caesar Cipher)
문자열을 입력받아 지정된 `n`만큼 문자를 이동시켜 암호화하고, 다시 복호화하는 기능을 구현합니다. 알파벳 문자는 `n`만큼 뒤의 문자로 치환되며(예: 'a' + n), 26을 넘어가면 순환합니다. 다른 문자는 변경되지 않습니다. 복호화는 암호화의 역과정으로, `n`만큼 이전 문자로 치환합니다. 이 과제는 포인터를 사용하여 문자열을 수정하고, 모듈러 연산(`%`)을 활용하여 알파벳 순환을 처리하는 방법을 다룹니다.
#include <stdio.h>
#include <stdlib.h>
#define BUFFER_SIZE 80
// 함수 선언
void encode_string(char *str, int shift);
void decode_string(char *str, int shift);
int main() {
char text_buffer[BUFFER_SIZE];
int shift_amount;
printf("영문 텍스트를 입력하세요: ");
// gets 함수는 보안상 취약하므로 fgets 사용을 권장합니다.
fgets(text_buffer, BUFFER_SIZE, stdin);
text_buffer[strcspn(text_buffer, "\n")] = '\0'; // 개행 문자 제거
printf("이동할 칸 수(n)를 입력하세요: ");
scanf("%d", &shift_amount);
// 입력 버퍼 비우기
while(getchar() != '\n');
printf("암호화된 텍스트: ");
encode_string(text_buffer, shift_amount); // 함수 호출
printf("%s\n", text_buffer);
printf("복호화된 텍스트: ");
decode_string(text_buffer, shift_amount); // 함수 호출
printf("%s\n", text_buffer);
system("pause");
return 0;
}
/* 함수 정의
기능: 입력된 문자열(str)을 지정된 칸 수(shift)만큼 시저 암호화합니다.
규칙: 알파벳(a-z, A-Z)은 shift만큼 뒤의 문자로 대체됩니다.
다른 문자는 변경되지 않습니다.
*/
void encode_string(char *str, int shift) {
char base;
while(*str != '\0') {
if (*str >= 'a' && *str <= 'z') {
base = 'a';
// (현재 문자 - 기준 문자 + shift) % 26 + 기준 문자
*str = base + (*str - base + shift) % 26;
} else if (*str >= 'A' && *str <= 'Z') {
base = 'A';
*str = base + (*str - base + shift) % 26;
}
str++; // 다음 문자로 이동
}
}
/* 함수 정의
기능: 입력된 문자열(str)을 지정된 칸 수(shift)만큼 시저 복호화합니다.
규칙: 알파벳(a-z, A-Z)은 shift만큼 앞의 문자로 대체됩니다.
다른 문자는 변경되지 않습니다.
*/
void decode_string(char *str, int shift) {
char base;
while(*str != '\0') {
if (*str >= 'a' && *str <= 'z') {
base = 'a';
// (현재 문자 - 기준 문자 - shift + 26) % 26 + 기준 문자
// +26은 음수 결과가 나오는 것을 방지합니다.
*str = base + (*str - base - shift + 26) % 26;
} else if (*str >= 'A' && *str <= 'Z') {
base = 'A';
*str = base + (*str - base - shift + 26) % 26;
}
str++; // 다음 문자로 이동
}
}
Task 8: 명령줄 인수 처리
프로그램 실행 시 전달된 명령줄 인수를 처리하는 예제입니다. `main` 함수의 `argc` (인수 개수)와 `argv` (인수 문자열 배열)를 사용하여, 프로그램 이름 자체를 제외한 나머지 인수들을 순회하며 "hello, [인수]" 형식으로 출력합니다. 이는 C 프로그램이 어떻게 외부로부터 데이터를 받아 처리하는지 보여주는 기본적인 예시입니다.
#include <stdio.h>
int main(int argc, char *argv[]) {
// argc: 명령줄 인수의 총 개수 (프로그램 이름 포함)
// argv: 명령줄 인수를 문자열 배열 형태로 저장
// argv[0]은 프로그램 이름 자체입니다.
// 프로그램 이름(argv[0])을 제외하고 나머지 인수들을 순회
for(int i = 1; i < argc; ++i) {
printf("hello, %s\n", argv[i]); // 각 인수에 대해 인사말 출력
}
return 0; // 정상 종료
}