안녕하세요! 오늘은 프로그래밍 언어의 할아버지라 불리는 C 언어에 대해 알아보겠습니다.
C 언어가 뭔가요? 🤔
여러분이 컴퓨터와 대화하기 위한 통역사가 필요하다고 상상해보세요.
- 여러분의 생각을 0과 1로 된 컴퓨터 언어로 바꿔주는 통역사
- 하지만 너무 상세하게 설명하지 않아도 알아듣는 똑똑한 통역사
C 언어는 바로 이런 역할을 합니다!
- 사람이 이해할 수 있는 코드를 작성하면
- 컴퓨터가 직접 실행할 수 있는 기계어로 변환해주는 마법 ✨
간략한 역사
- 1972년 데니스 리치가 벨 연구소에서 개발
- UNIX 운영체제를 만들기 위해 탄생
- B 언어를 개선해서 만들었기 때문에 'C'라는 이름이 붙음
- 지금까지도 운영체제, 임베디드 시스템, 게임 엔진 등 다양한 분야에서 사용 중
C 언어의 핵심 개념 🧩
1. 기본 문법
#include <stdio.h> // 헤더 파일 포함
int main() { // 프로그램의 시작점
printf("Hello, World!\n"); // 화면에 출력
return 0; // 프로그램 종료
} // 중괄호로 코드 블록 구분
마치 한국어에 '문장의 끝에는 마침표를 찍는다'라는 규칙이 있듯이, C 언어에도 문법 규칙이 있어요:
- 모든 명령문은 세미콜론(
;
)으로 끝나야 함 - 코드 블록은 중괄호(
{}
)로 묶음 - 대소문자를 구분함 (
int
와INT
는 다름!) - 주석은
//
또는/* */
로 표시
2. 변수와 데이터 타입
int age = 25; // 정수형 (예: -1, 0, 42)
float height = 175.5; // 부동 소수점 (예: 3.14)
double precise = 3.141592; // 더 정밀한 소수점
char grade = 'A'; // 문자 (작은따옴표로 묶음)
char name[10] = "홍길동"; // 문자열 (큰따옴표로 묶음)
변수는 마치 이름표가 붙은 상자와 같습니다:
- 상자의 크기와 용도가 정해져 있음 (데이터 타입)
- 상자에 이름표가 붙어 있음 (변수명)
- 상자 안에 물건을 넣을 수 있음 (값 할당)
- 필요할 때 상자에서 물건을 꺼낼 수 있음 (값 사용)
3. 제어 구조
// 조건문
if (score >= 90) {
printf("A 등급입니다.\n");
} else if (score >= 80) {
printf("B 등급입니다.\n");
} else {
printf("더 노력하세요.\n");
}
// 반복문
for (int i = 0; i < 5; i++) {
printf("%d번째 반복\n", i + 1);
}
int count = 0;
while (count < 3) {
printf("while 반복: %d\n", count);
count++;
}
제어 구조는 마치 도로의 교차로나 신호등과 같습니다:
- if-else: "만약 ~라면 이쪽으로, 아니면 저쪽으로"
- for: "이 일을 정해진 횟수만큼 반복해"
- while: "특정 조건이 맞는 동안 계속해서 반복해"
4. 함수
// 함수 선언
int add(int a, int b);
// 메인 함수
int main() {
int result = add(5, 3);
printf("5 + 3 = %d\n", result);
return 0;
}
// 함수 정의
int add(int a, int b) {
return a + b;
}
함수는 레고 블록과 같습니다:
- 입력값(매개변수)을 받아서
- 무언가 작업을 수행하고
- 결과값(반환값)을 돌려줌
- 필요할 때마다 재사용 가능
5. 포인터 - C의 슈퍼파워 💫
int number = 42; // 일반 변수
int *ptr = &number; // 포인터 변수 (number의 주소를 저장)
printf("number의 값: %d\n", number); // 42
printf("number의 주소: %p\n", &number); // 메모리 주소 (예: 0x7ffee13be8ac)
printf("ptr의 값: %p\n", ptr); // number의 주소와 동일
printf("ptr이 가리키는 값: %d\n", *ptr); // 42 (역참조)
*ptr = 100; // ptr을 통해 number 값 변경
printf("변경 후 number 값: %d\n", number); // 100
포인터는 마치 집 주소와 같습니다:
- 변수의 실제 값 대신 그 변수가 저장된 메모리 주소를 가리킴
&
연산자: 변수의 주소를 알려줌 (예: "홍길동씨 집 주소 어디예요?")*
연산자: 주소에 저장된 값을 가져옴 (예: "이 주소로 가서 놓인 물건 가져와")- 직접 메모리를 조작할 수 있는 강력한 기능 (그만큼 위험할 수도 있음!)
6. 메모리 관리
// 동적 메모리 할당
int *arr = (int *)malloc(5 * sizeof(int)); // 5개 정수 크기의 메모리 할당
if (arr == NULL) {
printf("메모리 할당 실패\n");
return 1;
}
// 메모리 사용
for (int i = 0; i < 5; i++) {
arr[i] = i * 10;
printf("arr[%d] = %d\n", i, arr[i]);
}
// 메모리 해제 (매우 중요!)
free(arr);
arr = NULL; // 댕글링 포인터 방지
C 언어에서 메모리 관리는 마치 도서관에서 책을 대출하는 것과 같습니다:
- malloc(): 책을 빌림 (메모리 할당)
- 책을 읽고 사용함 (메모리 사용)
- free(): 책을 반납함 (메모리 해제)
- 반납 안 하면? 벌금! (메모리 누수)
7. 배열과 문자열
// 배열
int scores[5] = {85, 92, 78, 90, 88};
printf("세 번째 점수: %d\n", scores[2]); // 78 (인덱스는 0부터 시작)
// 문자열 (문자 배열)
char greeting[6] = "Hello"; // 문자열 끝에 '\0' 문자가 자동으로 추가됨
printf("인사말: %s\n", greeting);
// 문자열 함수 사용
#include <string.h>
char str1[20] = "Hello";
char str2[20] = " World";
strcat(str1, str2); // str1에 str2를 이어붙임
printf("결합 후: %s\n", str1); // "Hello World"
printf("문자열 길이: %lu\n", strlen(str1)); // 11
배열은 마치 기차의 객차와 같습니다:
- 같은 유형의 데이터를 연속적으로 저장
- 인덱스(0부터 시작)로 각 요소에 접근
- 문자열은 문자들의 배열 + 특별한 종료 문자('\0')
8. 구조체
// 구조체 정의
struct Student {
char name[50];
int age;
float gpa;
};
int main() {
// 구조체 변수 선언 및 초기화
struct Student kim = {"김철수", 20, 3.8};
// 구조체 멤버 접근
printf("이름: %s\n", kim.name);
printf("나이: %d\n", kim.age);
printf("학점: %.1f\n", kim.gpa);
// 구조체 포인터
struct Student *ptr = &kim;
ptr->age = 21; // (*ptr).age = 21; 와 동일
printf("변경된 나이: %d\n", kim.age); // 21
return 0;
}
구조체는 마치 여러 칸으로 나뉜 도시락 통과 같습니다:
- 서로 다른 종류의 데이터를 하나로 묶음
- 각 칸(멤버)에 이름을 붙여 쉽게 접근
- 복잡한 실세계 개체를 프로그램에서 표현 가능
9. 파일 입출력
#include <stdio.h>
int main() {
// 파일 쓰기
FILE *file = fopen("data.txt", "w");
if (file == NULL) {
printf("파일을 열 수 없습니다.\n");
return 1;
}
fprintf(file, "C 언어로 파일에 글 쓰기\n");
fprintf(file, "숫자: %d\n", 42);
fclose(file);
// 파일 읽기
char buffer[100];
file = fopen("data.txt", "r");
if (file == NULL) {
printf("파일을 열 수 없습니다.\n");
return 1;
}
while (fgets(buffer, sizeof(buffer), file) != NULL) {
printf("읽은 내용: %s", buffer);
}
fclose(file);
return 0;
}
파일 입출력은 마치 노트에 메모를 하는 것과 같습니다:
- fopen(): 노트를 펼침 (파일 열기)
- fprintf(), fputs(): 노트에 글 씀 (파일 쓰기)
- fscanf(), fgets(): 노트의 내용을 읽음 (파일 읽기)
- fclose(): 노트를 덮음 (파일 닫기)
C 프로그램의 작동 방식 ⚙️
C 프로그램이 실행되기까지의 과정은 마치 요리 레시피가 완성된 요리가 되는 과정과 같습니다:
전처리(Preprocessing) - 재료 준비
#include 지시문을 실제 코드로 대체 #define 매크로 확장 조건부 컴파일 처리 (#ifdef, #ifndef 등)
컴파일(Compilation) - 요리 방법 해석
C 코드를 어셈블리어로 변환 문법 오류 검사 최적화 수행
어셈블(Assembly) - 재료 손질
어셈블리어를 기계어(목적 코드)로 변환
링킹(Linking) - 요리 완성
여러 목적 파일을 하나의 실행 파일로 결합 라이브러리 함수 연결
실행(Execution) - 요리 제공
운영체제가 프로그램을 메모리에 로드 main() 함수부터 실행 시작
C 언어의 장점은? 🌟
속도가 빠릅니다
- 저수준 언어에 가까워 실행 속도가 매우 빠름
- 최적화가 잘 되어 있어 자원 효율적
- Python이 자전거라면, C는 스포츠카!
메모리를 효율적으로 사용해요
- 메모리를 직접 관리할 수 있어 효율적인 프로그램 작성 가능
- 사용하지 않는 기능으로 인한 오버헤드가 없음
이식성이 뛰어나요
- 거의 모든 컴퓨터 아키텍처와 운영체제에서 실행 가능
- "한 번 작성하고, 어디서나 컴파일"
영향력이 큽니다
- C++, Java, JavaScript, Python 등 현대 언어에 큰 영향
- 이 언어들의 많은 문법이 C에서 유래
하드웨어 제어가 가능해요
- 하드웨어와 직접 상호작용 가능
- 임베디드 시스템, 드라이버 개발에 적합
주의할 점 ⚠️
메모리 관리는 수동으로
- 동적 할당한 메모리는 반드시 직접 해제해야 함
- 해제 안 하면 메모리 누수(leak) 발생
- 이미 해제한 메모리에 접근하면 오류 발생(use-after-free)
포인터는 조심히 다루세요
- NULL 포인터 역참조 시 프로그램 충돌
- 잘못된 메모리 주소 접근 시 segmentation fault 발생
- 배열 범위를 벗어난 접근은 버그의 원인
버퍼 오버플로우
- 배열이나 문자열의 크기보다 더 많은 데이터를 쓰면 위험
- 보안 취약점의 주요 원인
char name[10]; strcpy(name, "이름이 너무 길면 버퍼 오버플로우 발생"); // 위험! // 안전한 방법: strncpy(name, "긴 이름", sizeof(name) - 1);
타입 안전성 부족
- 형변환이 너무 자유로워 의도치 않은 데이터 손실 가능
- 컴파일러가 잡아주지 못하는 오류 존재
문자열 처리가 불편해요
- 문자열 조작 함수를 사용할 때 항상 버퍼 크기 고려 필요
- 문자열 끝의 널 문자('\0')를 항상 신경써야 함
실제 사용 예시 📱
1. 간단한 계산기
#include <stdio.h>
int main() {
double num1, num2;
char operator;
double result;
printf("간단한 계산기\n");
printf("수식을 입력하세요 (예: 5 + 3): ");
scanf("%lf %c %lf", &num1, &operator, &num2);
switch(operator) {
case '+':
result = num1 + num2;
break;
case '-':
result = num1 - num2;
break;
case '*':
result = num1 * num2;
break;
case '/':
if(num2 != 0)
result = num1 / num2;
else {
printf("0으로 나눌 수 없습니다!\n");
return 1;
}
break;
default:
printf("지원하지 않는 연산자입니다!\n");
return 1;
}
printf("계산 결과: %.2lf\n", result);
return 0;
}
2. 학생 관리 시스템
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_STUDENTS 100
#define MAX_NAME_LEN 50
// 학생 구조체 정의
struct Student {
int id;
char name[MAX_NAME_LEN];
float gpa;
};
// 학생 데이터베이스
struct Student students[MAX_STUDENTS];
int studentCount = 0;
// 학생 추가 함수
void addStudent() {
if (studentCount >= MAX_STUDENTS) {
printf("최대 학생 수에 도달했습니다.\n");
return;
}
struct Student newStudent;
printf("학번: ");
scanf("%d", &newStudent.id);
printf("이름: ");
scanf(" %[^\n]s", newStudent.name);
printf("학점: ");
scanf("%f", &newStudent.gpa);
students[studentCount] = newStudent;
studentCount++;
printf("학생이 추가되었습니다.\n");
}
// 학생 목록 출력 함수
void displayStudents() {
if (studentCount == 0) {
printf("등록된 학생이 없습니다.\n");
return;
}
printf("\n%-10s %-20s %-10s\n", "학번", "이름", "학점");
printf("------------------------------------------\n");
for (int i = 0; i < studentCount; i++) {
printf("%-10d %-20s %-10.2f\n", students[i].id, students[i].name, students[i].gpa);
}
printf("------------------------------------------\n");
}
// 메인 함수
int main() {
int choice;
while (1) {
printf("\n학생 관리 시스템\n");
printf("1. 학생 추가\n");
printf("2. 학생 목록 보기\n");
printf("3. 종료\n");
printf("선택: ");
scanf("%d", &choice);
switch (choice) {
case 1:
addStudent();
break;
case 2:
displayStudents();
break;
case 3:
printf("프로그램을 종료합니다.\n");
return 0;
default:
printf("잘못된 선택입니다. 다시 시도하세요.\n");
}
}
return 0;
}
마치며 🎁
C 언어는 마치 프로그래밍 세계의 라틴어와 같습니다. 배우기는 쉽지 않지만, 한번 익히면 다른 언어들을 이해하는 데 큰 도움이 됩니다. 무엇보다 컴퓨터가 실제로 어떻게 작동하는지 배울 수 있는 훌륭한 도구입니다.
C 언어는 오래되었지만 여전히 현역으로 활약하고 있습니다. 리눅스 커널, 윈도우, 맥OS 등 주요 운영체제의 핵심 부분이 C로 작성되어 있고, 임베디드 시스템, IoT 장치, 게임 엔진 등 성능이 중요한 분야에서 널리 사용되고 있습니다.
주의할 점 ⚠️
C 언어를 배울 때는 다음 사항들에 특히 주의하세요:
메모리 관리에 주의하세요: 동적으로 할당한 메모리는 반드시 해제해야 합니다. 그렇지 않으면 메모리 누수가 발생합니다.
포인터 사용 시 항상 조심하세요: 포인터는 C의 강력한 기능이지만, 잘못 사용하면 시스템 충돌이나 보안 취약점을 유발할 수 있습니다.
배열의 경계를 항상 확인하세요: C는 배열 범위 검사를 하지 않습니다. 배열 범위를 벗어나면 예상치 못한 오류가 발생합니다.
문자열 처리에 안전한 함수를 사용하세요: strcpy 대신 strncpy, sprintf 대신 snprintf와 같은 안전한 함수를 사용하세요.
초기화되지 않은 변수에 주의하세요: C에서는 변수를 선언만 하고 초기화하지 않으면 쓰레기 값을 가집니다.
컴파일러 경고를 무시하지 마세요: 컴파일러 경고는 잠재적 문제를 알려주는 중요한 신호입니다.
디버깅 도구를 활용하세요: GDB, Valgrind 같은 도구는 메모리 오류나 버그를 찾는 데 매우 유용합니다.
모던 C 표준을 따르세요: C99, C11, C17과 같은 최신 C 표준은 더 안전하고 효율적인 코드를 작성하는 데 도움이 됩니다.
궁금하신 점 있으시다면 댓글로 남겨주세요! 😊
참고 자료:
- "The C Programming Language" by Brian W. Kernighan and Dennis M. Ritchie
- https://en.cppreference.com/w/c
- https://www.learn-c.org/
- https://www.tutorialspoint.com/cprogramming/index.htm
- https://www.geeksforgeeks.org/c-programming-language/
'200===Dev Language > C' 카테고리의 다른 글
C언어 포인터와 배열 - 메모리 탐험 가이드 🗺️ (1) | 2024.06.09 |
---|---|
C 언어 포인터 완벽 가이드: 초보자부터 전문가까지 😎 (0) | 2024.05.29 |