7 분 소요

1. C언어의 동작은 컴파일러마다 다르게 동작함.

EX) malloc : 반환값이 void포인터 -> C컴파일러에서는 자동으로 다른 타입의 포인터로 캐스팅함.

BUT C++컴파일러에서는 자동 형변환 해주지 않음.

2. C에 없는 타입 -> boolean (정수로 참/거짓 표현)

예상 문제)

A. C는 자동으로 실수 연산을 수행한다.

B. C에는 boolean 타입이 존재한다.

C. 정수끼리 연산하면 정수 결과가 나온다.

D. 타입 캐스트는 선택 사항이다.

3. 괄호 순서 & 타입 캐스트

핵심 개념 : C 언어에서는 연산이 먼저인지, 캐스팅이 먼저인지가 괄호에 따라 달라진다.

표현 계산 순서 결과
5 / 2 정수 / 정수 2
(float)5 / 2 실수 / 정수 : 캐스트 -> 연산 2.5
(float)(5 / 2) 정수 / 정수 → 캐스트 2.0

예상 문제) Q. 다음 중 결과가 2.5가 되는 것은?

A. 5 / 2 = 2

B. (float)(5 / 2) = 2.0

C. (float)5 / 2 = 2.5

D. 5 / (float)2 = 2.5

4. 증감 연산자

핵심 개념 : ++가 변수 앞에 오느냐, 뒤에 오느냐에 따라 값이 “언제 증가하느냐”가 달라진다.

표현 동작 순서
++a 증가 → 대입
a++ 대입 → 증가

예상 문제)

int a = 5; printf(“%d”, a++);

출력값 : 5, a값 : 6

int a = 5; printf(“%d”, ++a);

출력값 : 6, a값 : 6

5. C 언어의 문자열

핵심 개념 : C 언어에는 string 타입이 없다.

구분 char str[] char *p
메모리 위치 배열 영역 문자열 상수 영역
내용 변경 가능 불가능
문자열 저장 직접 저장 주소만 저장

6. 구조체를 포인터로 전달

핵심 개념 : 함수의 매개변수로 구조체를 포인터 형태로 전달할 수 있다. 왜 포인터로 받나?

  • 구조체 전체 복사를 피할 수 있음 → 성능 좋음
  • 원본 구조체를 직접 수정 가능
struct Point {    
	int x;    
	int y; 
}; 

void print(struct Point *p) {    // Point 구조체의 주소를 받는 포인터 변수 
	// 포인터이므로 -> 사용    
	printf("%d\n", p->x);    
	printf("%d\n", p->y); 
} 

int main() {    
	struct Point p;   // 구조체 변수 선언 - struct 구조체이름 변수 
	p.x = 10;    
	p.y = 20;     

	print(&p); // 주소를 넘겨줌
}

p->x == (*p).x

상황 사용 연산자
구조체 변수 .
구조체 포인터 ->

void print(struct Point p) { … } : 복사 전달 => call by value

void print(struct Point *p) { … } : 주소 전달 => call by reference

7. switch문과 break

핵심 개념 : switch문에서 break가 없으면, 해당 case부터 아래 모든 case가 순서대로 실행된다.

동작 원리

  1. switch의 값과 일치하는 case를 찾는다
  2. 그 지점부터 break를 만날 때까지 실행
  3. break가 없으면 끝까지 실행
switch (3) {    
	case 1:        
		printf("A");    
	case 2:        
		printf("B");    
	case 3:        
		printf("C");   // 여기부터 실행    
	case 4:        
		printf("D");   // 여기까지 실행 
}

// 실행 결과 : CD

8. 재귀 vs 반복문

재귀 함수는 스택을 많이 사용하므로, 가능하면 반복문이 더 안전하다.

왜 재귀가 안 좋은가?

  • 재귀 함수는
  1. 함수 호출마다 스택(stack) 메모리 사용
  2. 호출이 깊어질수록 스택이 계속 쌓임
  3. 너무 깊어지면 -> 스택 오버플로우(Stack Overflow) 발생 가능
  • 반복문은?
  1. 함수 호출 없음
  2. 스택 사용 거의 없음
  3. 메모리 사용이 안정적
구분 재귀 반복문
메모리 스택 많이 사용 적게 사용
깊은 호출 위험 안전
성능 상대적으로 불리 유리

예상 문제)

A. 재귀는 항상 반복문보다 빠르다

B. 재귀는 스택 메모리를 사용한다

C. 반복문은 스택 오버플로우가 발생한다

D. 재귀는 메모리를 사용하지 않는다

정답: B

9. 배열 길이 구하기 (sizeof)

배열의 전체 크기한 요소의 크기로 나누면 배열의 길이(방 개수)를 구할 수 있다

배열 길이 = sizeof(배열 전체) / sizeof(배열[0])

int arr[5] = {1, 2, 3, 4, 5}; 
int count = sizeof(arr) / sizeof(arr[0]); // 결과 : count = 5

10. 이름(식별자) 명명 규칙

사용할 수 있는 문자 : 영문자 (a ~ z, A ~ Z), 숫자 (0 ~ 9), 밑줄 (_)

  • 숫자로 시작할 수 없음
  • 공백 포함 불가
  • C 키워드 사용 불가
  • 대소문자 구분함 (sum != Sum)

C키워드 리스트

  • 자료형 : int, char, float, double, void, short, long, signed, unsigned

  • 제어문 : if, else switch, case, default for, while, do break, continue, return

  • 구조 관련 : struct, union, enum typedef

  • 저장 클래스 / 속성 : auto, register static, extern const, volatile

  • 기타 : sizeof, goto

11. 함수 포인터

C 언어에서 함수를 매개변수로 전달하려면 함수 포인터를 사용해야 한다.

선언 형식 : [리턴타입] (*변수명) (매개변수 목록);

void print() {    
    printf("Hello\n"); 
} 

void test(void (*p)()) {    
    p();   // 전달된 함수 호출 
} 

int main() {    
    test(print);  // 함수 이름 == 함수 주소 
}
  1. 함수 이름은 함수의 주소다. test(print); // print()가 아니라 print
  2. 매개변수와 리턴 타입이 반드시 같아야 한다. void (*p)(); // 리턴값 없음, 매개변수 없음

함수 이름 == 함수 주소?

함수 이름: 코드 영역의 시작 주소를 가리키는 식별자 (변수 X )

=> 배열 & 함수 : 첫 번째 원소 주소/함수 시작 주소로 자동 변환

12. 문자열 리터럴과 상수 영역

문자열 리터럴은 상수 영역에 저장되며, 수정할 수 없다.

기본 선언 char *p = “apple”;

  • “apple” → 상수 영역(Read-only memory)
  • p → 문자열의 주소만 저장하는 포인터
  • 즉, 문자열 자체를 저장한 게 아님.

[안되는 것]

A. 문자열 내용 변경 안됨

p[1] = 'x';      // 안됨 
*(p + 1) = 'x';  // 안됨

이유:

  • "apple"은 상수 영역
  • 상수 영역의 값은 변경 불가
  • 접근은 가능하지만 수정은 불가

B. scanf("%s", p) 안됨

scanf("%s", p);

이유:

  • p는 주소값만 가지고 있음
  • 문자열을 실제로 저장할 메모리 공간이 없음
  • 입력을 쓸 공간이 없어서 에러

C. scanf("%s", &p) 안됨

scanf("%s", &p);

이유:

  • &pchar** 타입
  • %schar*를 요구
  • 타입도 안 맞고 의미도 틀림

[되는 것]

A. 포인터 자체를 바꾸는 것은 가능

p = "adf";

이유:

  • p는 const가 아님
  • 다른 문자열의 주소를 가리키도록 변경 가능

B. 올바른 문자열 입력 방법 (**)

char p[20]; 
scanf("%s", p);

이유:

  • 배열은 실제 문자열을 저장할 공간이 있음
코드 가능 여부 이유
p[1] = 'x' :x: 상수 영역
*(p+1)='x' :x: 상수 영역
p = "adf" :o: 포인터 변경
scanf("%s", p) // p : 포인터 변수 :x: 저장 공간 없음
char p[20]; scanf("%s", p); :o: 배열 공간 있음

요약 : 문자열 리터럴은 상수 영역에 저장되므로 수정할 수 없고, char 포인터는 문자열을 저장할 공간이 없다.

char *p = "문자열"은 수정 불가다.

13. C 언어의 참(True)과 거짓(False)

핵심 개념 : C 언어에는 boolean 타입이 없다. 양수든 음수든 상관없이 0만 거짓.

14. 포인터 연산과 배열 접근

핵심 개념 : *(p + i)는 p가 가리키는 배열의 i번째 요소에 접근하는 표현이다.

char *p = "apple"; 
// p → 'a' 'p' 'p' 'l' 'e' '\0'      
// 		0   1   2   3   4
p[1]    *(p + 1) // 같은 의미

앞 문제랑 연결됨 : 접근은 가능, 수정은 불가능

15. C 문자열 처리 함수 (string.h)

1번. size_t strlen(const char *str);

  • 널 문자(‘\0’)를 제외한 문자열 길이 반환
  • 문자열 길이만 계산, 변경 없음

2번. char* strcpy(char *dest, const char *src);

  • src 문자열을 dest에 복사
  • dest는 충분한 공간이 있어야 함
  • src는 변경되지 않음 (const)
char a[10]; 
strcpy(a, "hi");

3번. int strcmp(const char *str1, const char *str2);

  • 두 문자열 비교
  • 같으면 0 반환

4번. char* strcat(char *str1, const char *str2);

  • str2를 str1 뒤에 붙임
  • str1은 변경 가능해야 함
  • str1에 충분한 공간 필요

함정 :

char *p = "hi"; 
strcat(p, "!!");   // ❌ (상수 영역)

16. Call by Value vs Call by Reference

C 언어는 기본적으로 call by value이며, call by reference 효과를 내려면 포인터를 사용해야 한다.

1번. Call by Value (값에 의한 전달)

void change(int x) {    
    x = 10; 
} 
int main() {    
    int a = 3;    
    change(a);    
    printf("%d", a);   // 3 
}
  • 함수에 값의 복사본이 전달됨
  • 함수 안에서 값을 바꿔도 원본은 변경되지 않음

2번. Call by Reference (포인터 사용)

void change(int *p) {    // 포인터 변수로 받음 
    *p = 10;  // 값을 의미 
} 
int main() {    
    int a = 3;    
    change(&a);    // 주소를 전달 
    printf("%d", a);   // 10 
}
  • 변수의 주소를 전달
  • 포인터로 접근하여 원본 값 직접 변경
  • C에는 문법적인 call by reference는 없고, 포인터로 “효과만” 구현한다

17. 구조체 멤버의 종류

구조체의 멤버로는 기본 타입뿐만 아니라 포인터, 배열, 다른 구조체, 함수 포인터까지 모두 사용할 수 있다.

[구조체 멤버로 가능한 것들]

1번. 기본 자료형

struct A {    
    int x;    
    char c; 
};

2번. 배열

struct B {    
    int arr[10]; 
};

3번. 포인터

struct C {    
    int *p; 
};

4번. 다른 구조체 (또는 구조체 포인터)

struct Point {    
    int x;    
    int y; 
}; 

struct Rect {    
    struct Point p;      // 구조체 멤버 
}; 

struct Node {    
    struct Node *next;   // 자기 자신을 가리키는 구조체 포인터 
};
  • 자기 자신 타입의 포인터는 가능 / 자기 자신 타입 자체를 멤버로 두는 것은 불가능

5번. 함수 포인터

struct D {    
    void (*func)(); 
};

구조체 안에는 거의 다 넣을 수 있다. 단, 자기 자신은 포인터로만.

18. 3의 배수 판별

어떤 수가 3의 배수인지 확인하려면 3으로 나눈 나머지가 0인지 확인하면 된다.

  1. (i % 3) == 0
  2. !(i % 3)

19. 포인터의 의미 (* 연산자)

포인터 변수는 주소값을 저장하는 변수이다.

1번. 포인터 선언문에서의 *

int *p;
  • p → int형 변수의 주소를 저장하는 포인터
  • 선언문에서의 *는 “p는 포인터다”라는 의미

2번. 실행문(사용할 때)에서의 *

*p = 10;
  • *p → p가 가리키는 주소의 값 (그 주소에 담긴 값)
  • 실행문에서의 *는 간접 참조(역참조, dereference)
int a = 5; 
int *p = &a; 
*p = 10;

// 결과 : a = 10

20. 삼항 연산자

조건에 따라 두 값 중 하나를 선택하는 연산자

  • 기본 형식 : (조건식) ? 참일 때 값 : 거짓일 때 값
max = (a > b) ? a : b; 
printf("%s", (x > 0) ? "양수" : "0 또는 음수");

태그:

카테고리:

업데이트:

댓글남기기