- 개인적으로 공부하면서 지속적으로 정보를 추가, 수정, 삭제합니다.
- 일반적으로 TIL에서 작성한 내용을 정리해서 추가합니다.
- 정확하지 않은 부분 피드백 주시면 감사합니다.
- 컴퓨터 과학, 자료구조, 알고리즘 with C언어 노트와 일부 내용이 연결됩니다.
- 참조
부스트코스 모두를 위한 컴퓨터 과학 (CS50 2019)
에드위드 해외명강 컴퓨터 과학 교양 강좌: CS50
에드위드 자료구조 및 알고리즘 개론 I
에드위드 자료구조 및 알고리즘 개론 II
에드위드 데이터 구조 및 분석: Linear Structure and Dynamic Programming
에드위드 데이터 구조 및 분석: Non-Linear Structure, Optimization, and Algorithms
에드위드 파이썬으로 배우는 알고리즘 기초
부스트코스 자바로 구현하고 배우는 자료구조
부스트코스 [MIT] 파이썬을 이용한 알고리즘의 이해
한빛미디어 유튜브 | 혼자 공부하는 컴퓨터 구조 + 운영체제
에드위드 | C프로그래밍 기초
에드위드 | C프로그래밍 심화
1. C언어 기초
1. C 기초
- 핵심 단어와 개념
- stdio.h
- clang
- 컴파일러
- C언어 특징 및 기초적인 주의사항
- C언어는 동작 끝에
;
꼭 필요함! - 파이썬과 달리 출력시 줄바꿈을 일일이 해줘야 하는 면이 있음
- 컴파일을 수동으로 해주어야 함
- 변수에 담을 자료형을 먼저 지정해야 함
- C언어의 문법이 자바스크립트 문법이랑 비슷하다! → C언어의 기본 구문에 바탕을 두고 개발되었다고 함
- C에서 자료형을 알려주는 기능은 없다고 함! wow
- C언어는 동작 끝에
- C언어 메인함수
#include <stdio.h> int main(void) { int n; // 변수 선언부 printf("hello, world\n"); // 데이터 처리부 return 0; }
- 파이썬에서
import
하듯이#include <모듈파일.h>
※ 함수 호출 실행 순서 printf()
: print + formatting 의 줄임말로, 출력 함수 (stdio.h
파일에 포함되어 있음)int n;
: 변수 값을 따로 넣지 않고 미리 선언할 수 있음 → 이때 변수 n에는 쓰레기 값garbage value 라고 하는 것이 들어가 있음return 0;
: 메인 함수를 끝내는 역할
- 파이썬에서
2. 컴파일러
- 용어
- 소스코드: 우리가 작성한 코드
- 머신코드: 2진수로 작성된 기계가 이해하는 코드
- 컴파일러: 소스코드를 머신코드로 바꿔주는 프로그램
- C언어의 컴파일러
- clang, gcc 등을 주로 사용
- 프롬프트에서
$ clang(혹은 gcc) 파일명.c
라고 치면 컴파일이 됨 $ clang -o <실행파일제목이될부분> <C소스코드파일> -l<헤더에include한파일이름>
→ 자세히 컴파일하기`$ make 파일명
→ 컴파일 + 해당 파일명을 가진 프로그램 생성 + 링킹 작업까지 해주는 명령어
- 컴파일링
- 핵심 단어
- 컴파일링
- 어셈블링
- 링킹
- 컴파일링의 4단계 ^yawgn3
- 전처리(precomplie, preprocess)
- 실질적인 컴파일을 하기 전에 처리하는 작업
- 추가된 라이브러리 파일 확인 → 실제로 그 파일에 들어가 해당하는 소스코드를 복사해옴(ex:
#include
) - 프로그래밍 편의를 위해 작성된 매크로 변환(ex:
#define
) - 컴파일할 영역 명시 (ex:
#if
,#ifdef
등) test.c
→test.i
로 확장자가 바뀜
- 컴파일링(compile)
- C코드를 어셈블리어라는 저수준 프로그래밍 언어로 변환
- 컴퓨터가 이해할 수 있는 언어와 최대한 가까운 프로그램으로 만듦
- 어셈블리어는 C보다 연산의 종류가 훨씬 적지만, 여러 연산들이 함께 사용되면 C에서 할 수 있는 모든 것들을 수행 가능
- 컴파일이라는 용어는 이 단계를 얘기하기도, 전체 컴파일링 4단계를 통칭하여 부르기도 함
test.i
→test.s
로 확장자가 바뀜
- 어셈블링(assemble)
- 어셈블리어를 기계어로 변환
- 어셈블리 코드를 목적 코드(오브젝트 코드)로 변환 → binary 형태의 목적 파일이 됨
- CPU가 프로그램을 어떻게 수행해야 하는지 알 수 있는 명령어 형태인 연속된 0과 1들로 바꿔주는 작업.
- 이 변환작업은 어셈블러라는 프로그램이 수행.
- 소스 코드에서 오브젝트 코드로 컴파일 되어야 할 파일이 딱 한 개라면, 컴파일 작업은 여기서 끝나지만, 그렇지 않은 경우에는 링크라 불리는 단계가 추가.
test.s
→test.o
로 확장자가 바뀜
- 링킹(link)
- 만약 프로그램이 (math.h나 cs50.h와 같은 라이브러리를 포함해) 여러 개의 파일로 이루어져 있고, 하나의 실행 파일로 합쳐져야 하는 경우 필요한 단계.
- 링커는 여러 개의 다른 목적 파일을 실행 가능한 하나의 목적 파일로 합침.
- 예를 들어, 컴파일을 하는 동안에 CS50 라이브러리를 링크하면 오브젝트 코드는
GetInt()
나GetString()
같은 함수를 어떻게 실행할 지 알 수 있게 됨. - 목적 파일은 링킹을 거친 이후에 실행 파일이 됨
test.o
→test.exe
로 확장자가 바뀜
- 전처리(precomplie, preprocess)
- 핵심 단어
3. 버그와 디버깅
- 버그와 디버깅이란
- 버그: 코드에 들어있는 오류
- 디버깅: 코드에 있는 버그를 식별해 고치는 과정 → 디버거라는 프로그램 사용
- 디버깅의 기본
- 중지점을 설정해 한 단계씩 수행하게 함
printf
함수 사용
4. 코드의 디자인
- 코드의 정확성 체크하기
- 코드의 스타일가이드 참조하기
- 코드의 설계를 생각하기
- 이게 중요함
- 어떻게 더 효율적이고 문제를 적절하게 잘 해결하는 코드를 만들 것인가
5. 변수와 자료형
- 상수, 변수, 대입문
int age; // 변수 선언 age = 15; // 대입문: 변수에 상수 데이터를 저장
- 여행 가기 전에 펜션을 예약하듯이, 데이터를 담을 변수를 예약하는 것 → 메모리 공간이 할당됨
- 상수: 데이터 값 자체
- 변수: 저장한 상수값에 따라 변수 값도 변함
- 대입문: 대입 연산자(
=
)를 사용하여 오른 쪽의 식을 변수에 저장하는 것
- C의 자료형
- 그림에서 괄호는 생략 가능
- 큰 자료형 값을 작은 자료형 변수에 넣으려면 작은 자료형 변수 사이즈에 맞춰지므로 조심(코끼리를 냉장고에 꾸겨 넣는 것)
- sign과 unsign의 차이
- sign은 음수와 양수의 표시를 의미하며, 비트의 맨 왼쪽 칸을 할애하여 사용함
- 따라서 같은 3비트를 사용하여도 unsign 자료형과 sign 자료형이 표현할 수 있는 숫자의 범위는 다름! → 목적에 따라 음수 양수를 둘 다 표현할건지, 음수 버리고 양수만 크게 쓸 것인지 선택
- 음수, 양수 부호가 붙어있다는 뜻으로, unsigned는 양수만 쓰겠다는 것.
sizeof(자료형)
: 자료형의 크기를 확인하는 함수
- 아래의 볼드체는 기본적으로 많이 사용하는 자료형이라는 뜻!
- 정수
- (signed) int: 특정 크기 또는 특정 비트까지의 정수 (ex:
5
,28
,-3
,0
(일부 거대 기업이 아닌 이상 일반 사용자들은 대부분 int 사용)) - long: 더 큰 크기의 정수
- (signed) int: 특정 크기 또는 특정 비트까지의 정수 (ex:
- 실수
- double: 부동소수점을 포함한 더 큰 실수 (float보다 메모리 두 배를 차지함)
- float: 부동소수점을 갖는 실수 (ex:
3.14
,0.0
,.2
,4.
,-28.56
) - long double: 시스템에 따라 16 바이트 일수도 있다고 함.
- 문자
- char: 문자 하나 (ex:
'a'
,'Z'
,'?'
)
- char: 문자 하나 (ex:
- bool: 불리언 표현 (ex: True, False, 1, 0, yes, no). 1 byte.
- 왜 이렇게 많은 자료형?
- 값에 따라 가장 적절한 자료형을 선택하도록 하게끔(ex. 나이를 저장하는데 굳이 double형 변수에 저장할 필요 X)
- 실수형은 2진수 변환 시 오차가 발생함 (ex. 0.1을 10개 더하면 1이 아닌 0.999…가 되지만 출력할 때는 1이라고 출력된다고 함)
- C언어 자료 종류
- 16진수는 로봇이나 제품에 들어가는 프로그램을 작성할 때 많이 쓴다고 함
- string: 문자열, ""로 묶은 1개 이상의 문자 (ex:
"apple"
,"한"
→ 한글 1자는 최소 2바이트가 필요하므로 문자 상수가 못 됨)
- 식별자, 예약어
- 식별자identifier: 프로그램에서 이름으로 사용하는 것의 총칭(ex: 변수명, 배열명, 함수명, 구조체명)
- 예약어reserved word, keyword: 자료형 이름,
for
,while
등 (※printf
같은 C언어에서 제공하는 라이브러리 함수명은 예약어가 아니며, 식별자로 사용할 수 있지만 함수 원래의 기능은 사라진다고 함 )
6. 출력 & 입력
printf
함수의 형식 지정자(변환 명세) 더 자세한 형식 지정자 확인하기%d
,%i
: int형- 출력 시 둘은 차이가 없으나, 입력시
%i
는 10진수 외에 8진수, 16진수를 입력 받을 수 있음 %3d
: 필드 폭 설정. 3칸을 차지하면서 int형 출력%-d
: 왼쪽 맞춤%+d
: 부호 표시 → 음수, 양수 표현이 가능해짐
- 출력 시 둘은 차이가 없으나, 입력시
%lf
,%f
: float, double- 무조건 소수 아래 6자리 출력
%.2f
: 소수점 2번째 자리까지만 표현하기
%li
: long%c
: char (문자 한 개 )%s
: stringprintf("%s\n", 배열명)
: 배열의 인덱스를 따로 지정하지 않고 배열명만 포함(배열명 자체가 문자열 배열의 시작 위치를 가리키는 포인터이므로,&
을 붙여도 되고 안 붙여도 됨)
%p
: 포인터 변수
- 출력값과 형식 지정자가 틀리면?
- 에러는 발생하지 않으면서 이상한 값 출력 → 결과가 이상할 때 먼저 확인하기
putchar
함수putchar('A')
- 문자 1개를 출력
- 형식 지정자 필요 없음
- 입력함수 scanf
scanf("%d", &height)
:- 사용자로부터 형식 지정자에 해당되는 값을 입력받아 변수에 저장
- 형식 지정자는 printf와 동일
&
기호는 주소를 나타내는 연산자로, printf에서는 필요하지 않지만 scanf에서 필요함scanf("%d %d", &age, &height)
: 두 개의 변수에 각각의 값을 저장. 이때 두 개의 변수 입력을 구분할 때 공백 문자(빈칸, 탭, 엔터키)를 이용 → 따라서 아래 주의점 1번과 연결됨
- 주의할 점
scanf("%d\t", %height)
,scanf("%d\n", %height)
쌍따옴표를 닫기 전에 빈칸, 줄바꿈을 넣으면 안 됨! → 데이터를 한개 더 입력해야 입력이 완료되고, 이 입력값은 다음 scanf문에서 사용된 변수 안에 저장 됨%2d
처럼 필드폭을 지정하면 최대 n자리까지 입력을 제한하는 효과scanf("키는?%d", &height)
처럼 처럼 출력할 내용을 scanf에 담지 말고 printf와 scanf를 따로 사용함
- getchar 함수
변수 = getchar();
- 키보드에서 문자 1개를 입력
- 형식 지정자 필요 X
7. 연산자
- 기본 연산자 및 주석
+
: 더하기-
: 빼기*
: 곱하기- 제곱 연산자는 없다…!!! → 라이브러리 함수를 이용
/
: 나누기- 정수끼리 연산하면 값도 정수가 나오므로 주의
- 컴파일링이 자동 형 변환 해줌
%
: 나머지&&
: 그리고||
: 또는<
,>
,<=
,>=
: 비교 연산자.10 <= x < 20
처럼 사용하면 이상한 값이 나올 거니까 조심.//
: 주석/* 긴 주석 */
- 삼항 조건 연산자
조건문 ? 참값 : 거짓값;
int data = num1 > num2 ? num1 : num2;
: 숫자1이 숫자2보다 크다면 숫자1, 아니면 숫자2를 data에 넣는다- if ~ else 문을 일부 대체할 수 있으므로 알아둘 것!
- 특별한 연산자
- 복합 대입 연산자
- 증감 연산자 (위치가 중요하다!)
변수++
,변수--
: 현재 변수의 값 그대로 식에서 사용 → 1씩 증가(감소) (ex:a
가 2일때,++a * 10
은 30)++변수
,--변수
: 일단 1씩 증가(감소) → 그 값을 식에서 사용 (ex:a
가 2일때,a++ * 10
은 21)
- 형 변환 연산자(type cast operator)
(바꾸려는자료형) 변수[값][(수식)]
변수 = (double)sum / (double)n
: 괄호 안에 변환하고자 하는 자료형을 입력해주면, 저 식에서만 자료형이 바뀌어서 계산 됨.(int) (pi+1.9)
: 수식을 사용할 땐 괄호- 내가 할당하려는 왼쪽 변수에는 형 변환을 할 수 없음(==냉장고의 크기를 마음대로 바꿀 수 없듯이 ==)
- 비트 연산자 bitwise operators
- 비트끼리 연산함
&
,|
,~
: 각각 기본 논리연산자의 AND, OR, NOT (1획이면 충분하다는 공통점)<<
,>>
: 특정 수의 비트를 왼쪽, 오른쪽으로 이동하여 수를 계산함. 전자는 곱하기 대신, 후자는 나누기 대신으로 사용할 수 있음. (ex:5 << 1
의 값은 5 * 2 = 10이다 → 5를 이진수로 나타내면 0101인데 이를 왼쪽으로 한 칸 옮기면 1010이고 이것을 십진수로 나타내면 10)
- 복합 대입 연산자
8. 조건문과 루프
- 자바스크립트랑 문법이 똑같음! (※
;
꼭 붙여줘야 함)
- 조건문 - if문
if (조건)
{
수행할 구문;
}
else if (조건)
{
수행할 구문;
}
else (조건)
{
수행할 구문;
}
- 조건문 - switch문
// 변수와 case의 값이 동일할 때, 해당하는 case 실행문이 수행된다
// 만약 동일한 값이 없다면 default의 실행문을 실행함 (default는 생략 가능)
switch(변수)
{
case 값1 :
실행문;
break;
case 값2 :
실행문;
break;
default :
실행문;
}
- 반복문
while (조건)
{
수행할 구문;
}
// 횟수를 지정하고 반복 할 때
for (변수 초기화; 변수 조건; 변수 증가)
{
수행할 구문;
}
// 일반적인 for문 예시
for (int i = 0; i < 반복횟수; i++)
{
수행할 구문;
}
// do-while문
do
{
n = get_int("Positive Integer: ");
}
while (n < 1);
do-while
문은while
단독 사용과 달리,do
의 구문을 무조건 한 번은 수행하게 하는 차이가 있음
9. 사용자 정의 함수
- 사용자 함수 사용하기
#include <stdio.h>
// 사용할 함수의 프로토타입을 미리 선언문
void cough(void);
// 메인 프로그램
int main(void)
{
cough();
}
// 내가 정의한 함수
void cough(void)
{
printf("cough\n");
}
- 메인 함수와 분리하기
- 메인 함수 내에서 정의하지 않음.
- 메인 함수와 내가 정의한 함수는 완전히 별개. (메인 함수는 우리 집, 내가 정의한 함수는 친구 집 )
- 함수 원형prototype 선언
- 프로토타입을 미리 선언하지 않으면 오류 → C는 오래된 언어라 뒤에 뭐가 있을 거라 생각을 안함
- 사용자 함수 정의하기
아웃풋자료형 함수이름(인풋자료형1 인수명1, 인풋자료형2 인수명2)
{
수행할 구문
}
- 입출력
- 입출력이 없으면
void
- 출력이 있다는 건
return
값이 있다는 것
- 입출력이 없으면
- 인풋 자료형 선언
- 메인 함수에서 이미 자료형을 선언한 값이라 할지라도, 인수로 넘겨주면 별개로 저장되는 값이 됨 → 인수명에서 따로 또 선언을 해주어야 함!
10. 하드웨어의 한계
- 핵심 단어
- 메모리
- 오버플로우
- 메모리
- 모든 프로그램은 실행 중에 RAMrandom access memory이라는 물리적 저장장치에 저장 됨
- RAM은 유한하다 → 때때로 부정확한 결과를 냄
- 오버플로우
- 부동 소수점 부정확성
- float 타입이 표현 가능한 범위 이상의 소수점 자리수를 지정한 뒤 출력하면 부정확한 숫자가 나옴
- 정수 오버플로우
- 숫자의 크기가 커져 int 타입(40억까지 표현 가능) 이상의 수를 넘기면 (232 이상의 숫자) 이상한 숫자가 나옴
- 실생활 사례
- 1999년에 연도가 마지막 두 자리수로 저장되어 있어 99에서 00으로 정수 오버플로우 발생 → 새해가 2000년이 아닌 1900년으로 인식되는 문제 → 돈을 때려 부어 메모리를 추가해 해결함
- 보잉 787은 248일이 지나면 모든 전력이 초기화 → 248일은 대략 232임 → 오버플로우 발생 → 주기적으로 변수를 0으로 세팅함
- 부동 소수점 부정확성
2. 배열
1. 배열
- RAM과 바이트
- 노란색 사각형 → 메모리
- 작은 사각형 하나 → 1바이트(8비트) 라고 생각!
- 배열이란?
- 자료형이 같은 많은 자료를 하나의 이름으로 저장하는 연속된 기억 공간 (ex. 5단 옷 서랍장, 12층 아파트, … )
- 배열 인덱스는 왜 0부터 시작할까?: 배열 첫 원소로부터 몇 개 뒤의 원소인가를 나타냄
- 배열 선언 및 초기화
const int N = 3; // 배열에 저장할 값 갯수를 *상수* 전역변수로 설정
int main(void)
{
// 방법 1
int 배열이름[N]; // 배열선언: N개 만큼의 값을 저장할 공간 생김
배열이름[i] = 값1;
...
배열이름[i] = 값n;
// 방법 2
int 배열이름[5] = {1, 2, 3, 4, 5}; // 중괄호를 이용해 배열 선언 및 초기화
// 다차원 배열 선언 및 초기화
int 배열이름[2][4] = {1, 2, 3, 4, 5, 6, 7, 8};
}
자료형 배열명[원소수];
- 다른 자료형을 섞어서 같은 배열에 저장할 수 없음
- 배열을 선언할 때 원소수는 상수여야 함 (
const
) - 원소 개수보다 큰 인덱스에 값을 할당하는 경우 인덱스 에러 뜨지 않음! ⇒ 다음 메모리 공간에 값을 할당하기 때문에 결과를 예측할 수 없게 됨.
자료형 배열명[원소수] = {값1, 값2, 값3 …};
- 중괄호를 이용한 값 저장은 배열을 선언함과 동시에만 가능. 선언문이 아닌 곳에서는 불가능.
2. 문자열과 배열
- 문자열
- 문자열 데이터는 “char 자료형의 배열”
- 노란색 한 칸은 1바이트(char 데이터 메모리)
- 문자열 데이터는 끝에
\0
이라고 하는 널 종단 문자(null terminating character)가 포함 됨! (모든 비트가 0인 1바이트) → 따라서 작성하고자 하는 문자열 개수 +1 을 해서 원소수를 결정! - 우리가 정한 임의의 배열명 자체가 배열 시작 주소이며 즉, 배열 시작 위치를 가리키는 포인터
- 문자열 선언 및 초기화
char 변수명[6] = {'H', 'e', 'l', 'l', 'o', '\0'};
char 변수명[6] = "Hello";
char *변수명 = "Hello";
: 원소수 지정하지 않고 초기화하는 방법(아래 “포인터와 문자열” 항목에서 더 자세히)
- 관련 함수
strlen()
: 스트링 + 렝쓰의 합성어로 문자열의 길이를 알려줌string.h
파일에 포함 됨toupper()
: 첫 글자를 대문자로 바꿔주는 함수ctype.h
파일에 포함strcmp(문자열1, 문자열2)
: 두 문자열이 같으면 결과값이 0인 함수.string.h
라이브러리에 포함strstr()
: 문자열 안에서 특정 문자열을 검색할 때 사용함. 찾으려는 문자열이 여러개 존재하는 경우, 반환 값을 받아서 다시 검색하는 코드를 사용하면 순차적으로substr
에 접근하거나 몇 개의substr
이 있는지 체크할 수 있음str
에서substr
을 찾은 경우:str
기준으로 첫 번째substr
의 위치를 포인터로 반환str
에서substr
을 찾지 못한 경우: NULL 반환
#include <string.h>
// 검색을 진행할 문자열, str 문자열에서 검색하려는 sub string
// - 검색을 진행할 문자열 : 종단 문자가 나올 때까지 `str`의 내용을 검색함
// - `substr`: 반드시 종단 문자로 종료되어야 함
char* strstr(const char* str, const char* substr);
- 매크로 상수
#define 변수명(대문자로사용) 값
#define N 5
와 같이 많이 사용- 코드 내의 변수명을 뒤의 값으로 정의함
- 전처리기가 소스 파일에서 해당 변수명을 만나면 정의한 내용으로 바꿔줌
3. 명령행 인자
- 핵심 단어
make 파일명.c
처럼 파일을 실행할 때, 뒤에 파일명을 써넣는 프로그램을 만드려면?- 명령행 인자
- argv
- argc
- 명령행 인자 설정하기
int main(int argc, string argv[]) { if (argc == 2) // 명령행 인자가 입력되면 다음을 실행하라 { printf("hello, %s\n", argv[1]); } else { printf("hello, world\n"); } }
argc
- main 함수가 받게 될 입력의 개수
argv[]
- 우리가 입력하는 인자가 포함된 배열(즉, 파일명 처럼 우리가 입력하는 내용)
argv[0]
는 기본적으로 프로그램의 이름- 하나의 입력이 더 주어진다면
argv[1]
에 저장
3. 메모리
1. 메모리 주소
- 16진수(Hexadecimal)
- 컴퓨터 과학에서는 10진수, 2진수 대신 16진수를 사용하는 경우가 많음 → 2진수를 간단하게 나타낼 수 있음 (4비트가 하나의 16진수로 표현 → 1바이트는 두 자리의 16진수로 표현)
- 0~9 + a~f 로 나타냄!
0x
는 뒤에 붙은 문자가 16진수임을 나타내는 표기
2. 포인터
- 포인터란?
- 메모리 주소를 담고 있는 변수
- 포인터가 담고 있는 주소가 뭔지가 중요한 게 아니라, 그 포인터가 가리키고 있는 것이 무슨 변수인지가 더 중요. ("접근"의 개념) 보물지도 같은 것. P의 상자 안에는 '화살표’가 있다고 생각해라!
- 포인터는 보통 64비트! (=8바이트)
- 초기에 별표를 붙이는 것은 포인터를 선언하겠다는 뜻이고, 선언된 이후에는 지정된 메모리 주소에 해당하는 메모리 값을 뜻하게 됨
&
와*
&변수명
: 해당 변수의 메모리 시작 주소(즉, 지도)*포인터변수명
: 해당 포인터 변수의 주소가 가리키는 곳이 담고 있는 값
- 사용 예시
int n = 50;
int *p = &n;
printf("%p\n", &n); // 주소 = 포인터
printf("%i\n", *&n); // 주소로 간 곳 = 정수, float, 문자 등의 값
printf("%p\n", p); // p란 변수는 포인터 변수
printf("%i\n", *p); // p에 담긴 주소로 가라는 것
- 다중 포인터
- 포인터를 가리키는 포인터
**포인터변수명
,***포인터변수명
처럼 사용하고, 이때*
의 개수가 화살표의 개수라고 생각해보면 됨- 사용 예시
int n = 50;
int *p1 = &n;
int **p2 = &p1;
printf("%p\n", p1); // p1 변수가 가리키는 화살표 -> n으로 가는 주소 값
printf("%p\n", p2); // p2 변수가 가리키는 화살표 -> p1으로 가는 주소 값
printf("%i\n", *p1); // p1 변수가 가리키는 곳에 담겨 있는 것 -> n 값
printf("%i\n", **p2); // p2 변수가 가리키는 곳에 담겨 있는 것 -> n 값
printf("%p\n", &p1); // p1의 주소 = p2
printf("%p\n", &n); // n의 주소 = p1
3. 포인터와 문자열
- 문자열은 결국 문자의 배열
char *s = "EMMA"
에서 변수 s는 결국 이러한 문자열을 가리키는 포인터- 변수는 가장 첫번째 문자, 즉
변수[0]
을 가리키는 것이나 마찬가지
- 문자열 비교
- 두 문자열이 같은 내용이라는 것은 두 문자열의 주소가 같다는 것!
printf("%c\n", *s); // s의 첫 글자 printf("%c\n", *(s+1)); // s의 두 번째 글자(s의 첫주소에 +1 한 것) printf("%c\n", *(s+2)); printf("%c\n", *(s+3));
- 문자열 복사
malloc()
함수
2. 우리가 문자열을 정의할 때, 주소를 전달하는 것이기에 문자열을 다른 변수에 할당한다고 해서 사본이 만들어지지 않음 → 동일한 주소를 전달하기에 결국 똑같은 데이터를 가리키게 됨
3. malloc을 이용해 또 다른 메모리 주소를 할당하고 여기에 문자열을 복붙해서 넣어야 함
4. 말록을 통해 메모리 주소를 할당할 때, 문자열이 경우 널 종단 문자까지 포함해야 하므로 +1 바이트 추가하여 할당
4. 메모리 할당과 해제
malloc
- 동적 메모리 할당을 위해 사용
- 말록 함수는 정해진 크기 만큼 메모리를 할당하고, free시킬 수 있는 포인터를 반환
- malloc(memory allocation)은 비워진 메모리의 첫 주소를 줌! ⇒ 포인터 변수에 담을 수 있다!
- 초기화 된 포인터 변수에 malloc으로 메모리를 할당하지 않은 채, 데이터를 넣으면 안 됨 → 그 변수에 어떤 쓰레기 값이 담겨져 있을지 모름! 오류 발생!
- 예를 들어, int 형 변수를 선언하기 위해 메모리를 할당하려면 다음과 같이 malloc 함수를 사용
// int 형 변수를 선언하기 위해 메모리를 할당
int *ptr;
ptr = (int*) malloc(sizeof(int));
calloc
- 항상 malloc 함수와 비슷하게 사용
- 이 함수는 메모리를 할당하고 할당한 메모리를 0으로 초기화
(void *) calloc(size_t num, size_t size)
- 예를 들어, int 형 변수를 선언하기 위해 메모리를 할당하려면 다음과 같이 calloc 함수를 사용
// int 형 변수를 선언하기 위해 메모리를 할당
int *ptr;
ptr = (int*) calloc(1, sizeof(int));
realloc
- 이 함수는 메모리의 크기를 변경하고, 기존 메모리를 복사하여 유지(ex: 배열의 크기를 늘려야 하는 경우)
- 식구가 더 늘어날 예정이라 기존 식구들 데리고 더 큰 집으로 이사하는 상황
- 먼저 메모리의 크기를 늘려야 하는 배열의 포인터를 인자로 받음 → 새로운 크기로 재할당한 메모리를 반환
(void *) realloc(void *p, size_t size)
- 메모리 크기를 줄일 때도
realloc
을 사용할까? → Yes- 주어진 크기보다 작은 값을 인자로 줄 경우, 해당 메모리 영역의 크기가 줄어들고 남은 부분은 반환
- 언제 그렇게 사용할까?
- realloc을 사용해 메모리 크기를 줄이는 것은 크기가 큰 메모리 공간을 줄여야 할 때 유용
- 특히 동적 메모리 할당을 사용하는 경우, 가비지 컬렉션을 사용하지 않는 C 프로그램이나 시스템 프로그래밍에서도 자주 사용
- 예를 들어, int 형 변수를 선언하기 위해 메모리를 늘려야한다면 다음과 같이 realloc 함수를 사용
// int 형 변수를 선언하기 위해 메모리 크기 늘리기
int *ptr;
ptr = (int*) realloc(ptr, 2 * sizeof(int));
// A라는 리스트에 원소를 더 추가하기 위해 메모리 크기 늘리기
int *A = (int*) realloc(A, sizeof(int)*(n+1));
A[n] = x; // x를 배열 A의 마지막 인덱스에 추가합니다.
n++; // 원소 개수를 하나 늘립니다.
malloc
과realloc
의 사용 예시
// malloc으로 메모리를 할당 받아서 0부터 2까지 값 삽입
int *ptr;
ptr = (int*) malloc(3 * sizeof(int));
for(int i = 0; i < 3; i++)
ptr[i] = i+1;
// 3, 4를 추가하여 5개의 원소를 가지기 위해 메모리 크기를 늘려야한다면
ptr = (int*) realloc(ptr, 5 * sizeof(int)); // int 타입 5개가 들어갈 메모리 할당 받음
for(int i = 3; i < 5; i++) // 3, 4를 삽입
ptr[i] = i+1;
free
- 말록을 이용해 메모리를 할당한 후
free(포인터변수)
을 통해 메모리 해제해야 함 - 그렇지 않으면 메모리에 저장한 값은 쓰레기 값으로 남아, 메모리 누수가 발생
- 말록을 이용해 메모리를 할당한 후
memcpy()
- 소스 영역에서 목적지 영역으로 특정 바이트만큼 복사한 후, 목적지 영역의 포인터를 반환!
- 말록 랩의 realloc의 메커니즘에 포함 되었음
#include <string.h>
// src 영역에서, dest 영역으로, n 바이트만큼 복사 → dset의 포인터 반환
void *memcpy(void *dest, const void *src, size_t n);
- valgrind
4.$ valgrind ./파일명
→ 메모리 누수가 얼마나 일어나는지 검사해줌 - 버퍼 오버플로우
- 말록으로 n개의 메모리를 할당했는데, n+1 번 인덱스에 접근하려고 할 때 생기는 현상
- 할당하지 않은 메모리에 접근하려고 할 때!
5. 메모리 교환, 스택, 힙
- x의 값을 y에, y의 값을 x로 옮기는 것
- x와 y 교환을 위해
swap(x, y)
와 같은 함수를 이용한다고 가정 → 메모리 주소를 넘겨주지 않으므로 이는 작동하지 않음! (=call by value) - malloc으로 메모리를 할당한 데이터는 힙(heap) 영역에 존재
- main 함수를 포함하여 우리가 할당한 변수 x, y, 함수의 매개변수 a, b 등 함수와 관련된 것들은 스택(stack) 영역에 존재
- 함수에 변수를 넘겨주면 동일한 스택 구역이지만 데이터 값을 "복제"하여 전달 (=call by value)
- 따라서
swap(&x, &y)
처럼 x, y의 주소를 함수에 넘겨주어야 함! (아래 그림 처럼 되게) + swap 함수의 매개변수는 포인터 변수여야 함! (주소를 건네줄거니까) (=call by address)
void swap(int *a, int *b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
*a
: 역참조 연산자. 의미는 'a라는 변수에 화살표(지도)가 있고 그 화살표가 가리키는 곳’을 의미함! (지도 자체가 아니라 지도가 가리키는 곳!&a
가 ‘a로 가는 지도’ 자체임.)- 포인티(pointee) : 포인터가 가리키고 있는 것.
malloc()
함수를 통해 별도로 우리가 설정해줘야 한다. - 힙 오버플로우: malloc을 계속 호출하다 보면 점점 사용하는 메모리 영역이 아래로 늘어나고, 스택 영역을 침범하게 되어 힙 오버플로우 현상 발생.
- 스택 오버플로우: 재귀함수처럼 자기 자신을 계속 호출하는 함수를 쓰다 보면, 점점 메모리 영역이 위로 늘어나며 힙 영역을 침범하는 스택 오버플로우 현상 발생.
4. 구조체
- 구조체
typedef struct { string name; int year; float gpa; } student; student s1 = {'Zamyla', 2014, 4.0}; s1.gpa = 3.5;
- 배열 말고 데이터를 묶어주는 또 다른 방법
- 서로 다른 자료형의 변수를 하나로 묶어 새로운 자료형을 만들 수 있음(int가 자료형이듯, 예시의 student는 우리가 정의한 자료형)
구조체명.멤버명
으로 접근할 수 있음(ex:student.name
)
- 구조체와 배열의 비교
- 배열은 같은 자료형만 묶을 수 있으나, 구조체는 다른 자료형도 묶을 수 있음
- 학생이 몇 명 있는지 미리 선언할 필요 없음
- 배열은 인덱스를 이용해 순환할 수 있지만, 구조체는 멤버를 순환할 수 없음
5. 파일 쓰기
- 핵심 단어
- scanf
- fopen
- fprintf
- fclose
FILE *file = fopen("phonebook.csv", "a");
: 파일 열기, a는 append, r은 read, w은 write 모드- file이 NULL이면 잘못되었으므로 1을 반환하는 함수를 fopen뒤에 넣어 줌
- return시, 뭔가 정상적이면 0을 반환. 뭔가 잘못되면 1을 반환.
fprintf(file, "%s,%s\n", name, number);
: fprintf는 file printf. 사용자에게 name, number라는 문자열을 입력 받고, 파일에 출력 해줌fclose(file);
: 파일 닫기- 파일 읽기
fread(bytes, 3, 1, file);
: fread(배열, 읽을 바이트 수, 읽을 횟수, 읽을 파일)
6. C 라이브러리
#include <math.h>
pow(숫자, 지수)
: 제곱을 구할 때sqrt(숫자)
: 루트값abs(숫자)
: 절대값exp(x)
: 자연상수 exlog10(x)
log(x)
#include <stdlib.h>
rand()
: 정수 0~32767 중의 난수 한 개를 반환srand(time(NULL))
: 현재 시간(time(NULL)
)을 난수 발생기의 씨드로 설정
#include <stdint.h>
; 자세한 용례는 여기서 ☞ C언어 코딩도장- 크기별로 정수 자료형이 정의된 헤더 파일