회사에서 C를 다루게 되어 인프런 강의 및 책을 참고하며 간단한 이론을 정리하고자
저장 공간 확보 ⇒ 입력 ⇒ 처리 ⇒ 출력
C 언어는 UNIX OS를 모든 기종에서 사용 가능하게끔 하기 위해 개발된 언어
OS는 컴퓨터의 여러 자원을 관리해주는 역할을 하는데 모든 기종에서 사용 가능하게 끔을 더 한다면 C는 여러 자원을 관리할 수 있는 역할을 목적으로 개발되었다 ⇒ 하드웨어를 직접적으로 다룰 수 있는 언어
때문에 하드웨어 제어, 호환성, 이식성 함수 면에서 뛰어나며 자원을 관리하는데 보다 효율적인 방안으로 비트 연산자, 포인터 등이 있다
K&R C, 초창기의 전통적인 C 언어
ANSI, 미국 표준 기구에서 지정
C99, ISO에서 정한 표준
SCII : 아스키 코드, 미국 표준 정보 교환 부호
1
bit : 1
진수 1개
3
bit : 8
진수 1개
4
bit : 16
진수 1개
소스 코드를 아스키 코드 값으로 작성을 하되 실제로 CPU가 이해할 수 있는 형태로 바꿔 주는 작업이 필요함 ⇒ 컴파일
문자들이 CPU가 실행시킬 수 있는 명령어의 형태로 바뀐다
프로그램은 운영체제에서 실행된다
알파벳 소문자, 대문자, 특수 문자, 제어 문자 포함해 127가지
2^7, 7 bit로 모두 표현 가능 = 1byte 내로 표현 가능
⇒ 1byte면 표현이 충분하지만 정수와 포맷을 맞추기 위해 4byte로 사용한다(앞에 3byte는 0으로 채움)
/*
char type은 1byte만 담을 수 있지만 위에 언급했던 것처럼
아스키 코드는 맨 마지막 1byte만으로도 충분히 표현 가능하기에 아래처럼 정의 가능
*/
char a = 'a';
// 기능문자를 %c를 이용해 출력하는 경우 해당 기능 수행
printf("[%c%c]", 97, 10); // 10은 줄바꿈
/* 출력
[a
]
*/
입력 받을 때 제어 문자를 스킵하고 싶은 경우는 해당 입력에 스페이스 추가
// 1 2를 입력하고 싶은 경우
char ch1, ch2;
int a;
scanf("%c %c", &ch1, &ch2);
scanf("%c%c", &ch1, &ch2); // %c 사이를 띄우지 않는다면 1하고 sb가 입력된다
a = getchar() // 단일 문자 입력 받을 수 있다, int 형태로 저장되기 때문에 a의 type은 int
putchar(a) // 단일 문자 출력, a의 값 출력
컴파일러가 처리 X, 전처리기가 처리
전처리기 : 컴파일러에 소스 코드 넘어가기 전에 소스 편집해주는 역할
항상 #
으로 시작
#include
지정한 파일의 내용을 읽어 지시자 위치에 붙여 넣는다
<>
: 컴파일러가 지정한 폴더에서 헤더 파일 가져옴
""
: 소스 파일이 저장된 디렉토리에서 먼저 찾고 없으면 include 디렉토리에서 다시 한번 찾음
직접 파일 경로 지정할 수도 있다
#include <stdio.h>
#include "right.h"
#include "C:\user\myhdr.h"
#define
복잡한 상수나 문장에 대해 매크로 명을 정의한다
#define PI 3.14159
#define MSG "passed!"
#define ERR_PRN printf("범위를 벗어났습니다\n")
#define INTRO "Perfect C Language \
& Basic Data Structure"
// 줄바꿈은 \ 이용해 표시
#define SUM(a, b) ((a) + (b)) // 괄호는 무조건 필요
#define MUL(a, b) ((a) * (b))
int a = 10, b = 20;
int res;
printf("%d", SUM(a, b));
/*
여기서 만약 괄호 없는 경우 치환이 되면
30 / a * b, 형태로 우리가 원하는 형태가 아닌 다른 형태로 된다
*/
res = 30 / MUL(a, b);
typedef
와 차이점을 유의해야된다
define
은 단순히 글자 대치, typedef
는 컴파일러가 직접 처리(컴퓨터 시스템에 따라 유연하게 설정값 사용 가능)코드 상에 매크로를 집어 넣어 사용하며 컴파일하면서 컴파일러에 이미 정의되어 있는 형태로 치환된다
#
: 문자열로 치환##
: 두 인수를 붙여 치환 말 그대로 조건이 충족하는 경우만 컴파일 진행하며 #
이용해 코드 정의
#if
구문, 이 때 #else
가 아니라 #elif
사용 #if defined x
: 만약 x가 정의되어 있는 경우로 해석할 수 있다#ifdef x
와 똑같은 명령어 #undef
: 매크로명 해제하는 지시자#error
: 메시지 출력하고 컴파일 중단#pragma
: 컴파일러에 컴파일 방법을 제어header file
main 함수 정의 이전에 전처리 지시자들을 이용해 현재 파일에 존재하지 않지만 필요한 함수들을 선언해준다
이 때 전처리 지시자에 헤더 파일을 정의해준다
함수로 이루어져 있다
매개 변수
: 함수가 실행되는데 필요한 변수토큰
: 의미 있는 하나의 단어운영체제는 하드 디스크
에 저장하며 메인 메모리
에 옮겨 프로그램 실행
startup code
: 컴파일러가 소스 코드 컴파일 시 붙여줌, 이 친구가 main 함수를 호출함
0
return, 비정상이면 1
return #include
: 컴파일 전에 전처리를 해주는 전처리 기라는 것이 존재하는데 전처리 이 전에 사용하는 지시자를 의미
C 운영체제는 출력이 8byte? bit? 단위인가?
\n
, \r
, \b
, \t
, \a
제어 문자를 통해 출력 형태를 컨트롤 할 수 있다
커서를 컨트롤 하며 수정, 삭제, 하드웨어 제어 등이 가능
프로그램과 데이터는 별개로 보면 된다
상수
라고 한다014
: 8진수 12
12
: 10진수 12
0xc
: 16진수 12
정수는 (4byte
= 32bit) 크기의 2진수로 변환되어 사용 된다
10ll
or 10LL
형태로 사용하는 경우 4byte로 표현 가능한 10을 의도적으로 8byte 형태로 명시하는 것을 의미한다 4byte
크기의 2의 보수 형태로 저장소수점 형태, 지수 형태 존재
컴파일러는 실수를 기본적으로 8byte
로 핸들링
64bit에 부호
, 지수
+ 1023(bias)
, 그 외 비트에 유효숫자에 첫번째 자리를 제외하고 나머지를 적음
첫번째 섹션 양수이기에 0
,
두번째 섹션 지수(=2) + 1023 = 1025
세번째 섹션 유효숫자(1101)에서 맨 앞 1을 제외한 나머지 수를 앞에 적고 그 외 0으로 채움, 맨 앞에 1을 제외하는 이유는 정규화 한 경우 정수 부는 무조건 1이기 때문
만약 6.5가 아니라 6.4 처럼 소수점 자리가 무한 소수인 경우 64bit에 모든 수가 표현이 되지 않기 때문에 엡실론(epsilon)이라는 오차가 발생한다
문자('')
문자를 사용하면 해당 문자의 아스키 코드 값이 들어감(A인 경우는 65) ⇒ 정수형 상수는 4byte
로 번역
문자열("")
문자열 내 문자를 1byte
로 변환하고 문자열 마지막에 null 문자(00000000)을 붙임
ex) 실제 연산이 가능한 형태로 사용하기 위해 변환
printf("%d", 10);
printf("%lf\n", 3.4); // 3.400000 출력
printf("%.1lf\n", 3.4); // 3.4 출력(소수점 첫째 자리까지 표시, 반올림)
int 4byte
short 2byte
long 4byte
long long 8byte
char 4byte
양수에서 -1을 하는 이유는 0도 양수로 표현하기 때문
signed int a; // default로 양수, 음수 모두 사용한다는 의미
unsigned int b; // 양수값 적용으로 사용한다는 것
printf("%u", b); // unsigned로 출력하는 경우
int, long은 같은 byte인데 굳이?
float 4byte
double 8byte
long double 10, 12, 16(컴파일러마다 다름), 그냥 double로 처리하는 컴파일러도 있음
char str[20]; // char 20개를 저장할 수 있는 메모리를 연속적으로 할당한다는 의미
char str[20] = "apple"; // a, p, p, l, e, null(종료 시점 체크)
str = "banana"; // 이렇게 재할당은 불가, str은 주소값을 지칭하고 있기 때문
strcpy(str, "banana"); // 사용해야된다
const
키워드
const double tax = 0.17; // 변수 재할당 불가하게함
A~Z, a~z. 0~9, (_) : 4가지 표기법 이용해 사용
공백, 특수 문자, 숫자 시작 불가
대입 연산자를 기준으로 왼쪽에 있는 경우와 오른쪽에 있는 경우를 다르게 취급
L=VALUE
)을, 오른쪽에는 변수가 들어갈 값(R-VALUE
)을 취급operand(피연산자), operator(연산자)
비교 연산자도 순차적으로 실행되는 것을 유의해야된다
int x = 10;
if (0 < x < 5) printf("ok") // ok 출력
/*
0 < 10을 비교해 1을 반환하고 1 < 5를 비교해 조건문을 만족시키는 결과 도출
*/
and, or 연산자 숏 서킷 룰
첫 조건의 만족 유무로 이후 조건 검사 유무를 결정
조건 연산자 참, 거짓에 따른 반환 값에 함수나 대입도 넣을 수 있다
이 때 함수는 반환 값이 존재해야된다
정수와 실수를 연산하면 4byte, 8byte로 단위 다르기에 직접적으로 연산 불가
연산을 진행하면 정수 ⇒ 실수로 자동(=암시, 묵시적) 형변환을 통해 연산 진행
int grad = 100;
double res;
res = (double)grad/90; // grad만 실수형으로 바꿔주면 암시적 형변환된다
변수의 크기를 byte 단위로 계산해줌
int a = 10;
sizeof a // 함수 아니고 연산자라 괄호 없이 사용 가능
// 4 출력(4 byte)
int arr[5];
sizeof arr // 20 출력, 배열의 전체 주소를 의미하기에 4byte 5칸
sizeof arr / sizeof arr[0] // 배열 요소의 수, 20/4
우측 이동 /= 2
, 좌측 이동 *=2
자료형에 따라 다름
int일 때 우측 이동한 경우 왼쪽에 남는 bit 부호에 맞게 채움, +면 0으로 -면 1로
for문 내부 마지막으로 가는 것 유의
while문에서는 증감 직접 정의하기 때문에 continue 사용에 유의해야된다, 무한 루프 돔
int i = 0;
while(i < 5){
continue;
i++;
}
함수명
은 함수가 정의되어 있는 메모리의 시작 위치
매개변수(parameter) = 함수 정의할 때 함께 정의되는 변수
인수(argument) = 전달 인자, 함수 호출할 때 전달되는 변수
call by reference
: 주소 값이 전달되는 경우
call by value
: 변수 값이 전달되는 경우
main 함수를 시작으로 프로그램들을 호출하기 때문에 main 함수에서 만약 다른 함수를 사용하는 경우 main 함수 보다 앞 서 정의 해줘야 된다
함수 선언부만 미리 적어주는 것으로 개선 가능
int sum(int a, int b);
int main(void) {
}
int sum(int a, int b) {
return a + b;
}
함수에서 매개변수 없는 경우 정의할 때 void 적어줘야 되며 호출할 땐 괄호만 표기
// 함수 정의
int get_pos(void) {
int b = 10;
return b;
}
// 함수 호출
int a = get_pos();
보통 C 함수들은 포인터를 이용해 값을 변경하고 0, 1을 반환해주는 방식을 사용하는 것 같다
메모리를 안놔주고 계속 물고 있음 ⇒ 메모리 초과
스택 영역에 함수 호출 관련 정보들이 저장되는데 재귀의 경우 함수가 종료되면서 다음 함수를 호출하는 것이 아니라 물고 있는 상태에서 호출되는 것이기에 메모리를 계속해서 사용하고 있다
배열을 정의한 경우 컴파일러는 배열명
을 배열이 할당 된 공간의 시작 주소로 명시한다
배열 정의 시
int arr[5] = {1, 2, 3}; // 1, 2, 3, 0 ,0 으로 초기화 된
int arr[] = {1, 2, 3}; // 1, 2, 3 총 3개를 담을 수 있는 배열 초기화된다
컴파일러는 (주소 값
+ 정수
)는 (주소 값
+ (정수
* 주소가 가리키는 크기
))로 연산을 한다
정수
* (sizeof 주소 데이터 타입
)이라 보면 된다
EX) 주소 값 + 1 진행 시 다음 주소를 가리킴
// arr의 시작 주소가 100이라고 할 때 int = 4byte씩 4개 16byte(100~116)가 할당됨
int arr[4];
// arr + 1은 100 + (1*4) = 104를 의미하며 배열의 두번째 요소에 추가된다
*(arr+1) = 2; // 시작 주소 104가 가리키는 값
printf("%d", *(arr+1)); // 2를 출력
int arr[5]; // arr 변수는 배열의 시작 주소값을 갖고있다
arr[0] = 1; // arr 배열 첫번째에 1이라는 값 할당
*arr = 1; // 위와 같은 의미의 코드
arr[1] = 2;
*(arr + 1) = 2; // arr 시작 주소 기준 다음 메모리에 할당
arr[2] = 3;
*(arr + 2); // arr 시작 주소 기준 다 다음 메모리에 할당
배열의 이름은 배열이 할당된 메모리의 시작 주소로 변경된다 라고 알 수 있었는데 배열의 시작 주소를 포인터 변수로 할당 했을 때 크기 관련해서 이슈가 발생
밑에 포인터 필기 보는 경우 cpu bit 수에 따라 포인터 변수의 고정적인 크기가 존재(16bit - 2byte
, 32bit - 4byte
, 64bit - 8byte
)
int a[4];
double b[4];
int * p = a;
printf("%d\n", sizeof a); // int는 4byte 크기이기에 4byte * 4 = 16 출력
printf("%d\n", sizeof b); // double은 8byte 크기이기에 8byte * 4 = 32 출력
printf("%d\n", sizeof p); // 현재 노트북이 64bit로 8byte 출력
배열의 시작 주소를 할당한 포인터 변수
가 아니라 배열의 이름
을 직접적으로 사용하거나 배열의 크기를 따로 계산해서 인수로 전달해야된다 문자열은 기본적으로 char 배열에 저장됨
C는 메모리를 직접적으로 핸들링 할 수 있는 언어이며 이를 위한 수단을 포인터라 말한다
하나의 포인터로 다양한 자료구조 참조 가능
주소 값의 표기는 모두 동일하지만 가리킬 수 있는 데이터의 타입은 여러 종류일 수 있기 때문
전역 변수의 사용 억제 가능
배열, 구조체 등 복잡한 데이터의 쉽게 접근 및 조작 가능
주소 값이 들어 있는 변수
주소 연산자 : &
간접 참조 연산자 : *
, 역 참조라고도 불린다
int a;
&a // a의 시작 주소 = int 형 저장 공간의 시작 주소
printf("%d", &a); // 주소값 확인 가능
printf("%u", &a); // 주소 값은 양수이기에, 만약 주소 값이 매우 커 sign bit가 1인 경우는 음수로 출력 됨
printf("%p", &a); // 주소값 출력 전용 변환 문자
*(주소값) // 시작 주소가 가리키는 공간을 통해 사용(변수값을 통해 X)
// 주소가 가리키는 공간과 값 모두를 사용할 수 있다
/*
8byte(double) 저장 공간을 4byte(int) 포인터 변수로 참조한다면?
=> 이거 불가, 컴파일에서 에러 발생, 프로그램 멈추지 않지만 정상적으로 참조되지 않음
*/
메모리가 할당되지 않은 포인터로 포인터에 메모리를 할당하기 전에 NULL
인지 확인하고 NULL
이면 메모리를 할당하는 패턴을 주로 사용
타입과 변수명 사이에 *를 붙임
이는 컴파일 시 전달하는 요소일 뿐 실제 변수명에 들어가거나 사용되지는 않는다
간접 참조 연산자와 다른 점은 변수명을 선언할 때 가 들어가는 경우와 를 연산으로 사용하는 경우
특정 변수의 시작 주소 값을 저장한다
int a, b; // 변수 a의 주소는 100부터 시작한다고 가정, 100~104 총 4byte 존재
a = 10; // 변수 a에 10을 할당
int *p; // 포인터 변수, 자료형에 따라 byte 공간 확보
// int 변수의 시작 주소를 넣기 위해서는 int type에 포인터 변수 필요
p = &a; // 포인터 변수 p에 변수 a의 시작 주소값 100이 적재된다
printf("%d", p); // 100이 출력
printf("%d", *p); // 포인터 변수 p가 가리키는 시작 주소값에 들어 있는 변수 10을 출력
포인터 변수의 타입은 주소 값이 여러 형태로 들어갈 수 있는 지를 의미하는지 아니면 단순히 들어가는 시작 주소 관련 변수의 타입을 말하는 것인지?
4byte
or 8byte
로 어떤 포인터 변수 타입으로 선언해도 모두 동일 int a = 10, b = 20;
int *pa = &a;
/*
변수의 값을 받기 위해서 scanf 함수를 사용
scanf는 주소 값을 인수로 사용
a 변수에 값을 입력하기 위해서는 a의 주소값이 필요
*/
scanf("%d", &a); // a의 주소값
scanf("%d", pa); // pa는 위에서처럼 a의 주소값을 의미하는 포인터 변수
scanf("%d", &pa); // 형태는 포인터 변수 pa의 시작 주소를 의미하기에 X
/*
아래형태는 pa가 가리키는 값을 의미, a의 값을 넘겨준다는 의미
만약에 a의 값이 10이고 메모리 시작 주소 10을 사용하는 변수가 존재한다면
크리티컬한 이슈가 발생할 수 있다
*/
scanf("%d", *pa);
포인터 변수를 따로 초기화 하지 않는 경우 쓰레기 값이 들어가기 때문에 null로 초기화 필요
프로그램에 문제 야기할 수 있다
// pa라는 포인터 변수에 시작 주소가 100인 int 타입 변수의 주소값이라는 의미
int *pa = NULL;
pa = (int*)100;
메모리의 경우 참조하고 있는 포인터가 참조하지 않는다고 자동으로 해당 메모리가 재사용 가능하게 초기화 되는 것이 아니다
free
메서드를 통해 해당 포인터가 참조하는 메모리를 재사용 가능하게 초기화하고 참조를 변경하며 메모리 재사용성을 증가시킨다 포인터 변수의 저장되는 시작 주소
수정 가능 여부
포인터 변수의 저장된 시작 주소가 가리키는 값
수정 가능 여부
2개의 조건을 갖고 총 3가지의 경우의 수 존재
얕게 얘기하면 C는 메모리에 접근하는데 있어 일반 변수, 포인터 변수 총 2가지의 입구가 있다고 생각하면 된다
const
키워드는 어느 쪽이든 입구를 막는다는 개념이라 생각하면 될 듯 int a = 10;
/*
아래 2개의 코드는 동일한 코드라고 보면 된다
자료형 기준 앞 뒤로 const가 존재하는 경우는 같은 것을 의미
변수명 앞에 const 유무로 값 수정 여부
*/
const int * pa = &a;
int const * pa = &a;
얼마든지 다른 주소를 넣을 수 있다,
포인터 변수에 저장되는 시작 주소 값을 바꿀 수 있지만 포인터 변수가 가리키는 시작 주소에 있는 값을 바꾸지 못하게 한다
const 변수로 선언하게 되면 쓰레기 값을 계속해서 사용해야 되는 수고가 있는데 const 포인터 변수로 선언된 것을 어느 정도 유연하게 사용가능 하게끔 한다
주소 값을 바뀌며 선택적으로 변수를 상수로 사용할 수 있게끔 함
int a = 10;
int * const pa = &a;
처음 코드와 다르게 const가 포인터 변수명 바로 앞에 온다면 일반 const 변수 선언처럼 처음 선언된 주소 값 외에 수정이 불가하다
int a = 10;
const int * const pa = &a;
포인터들의 배열, 배열 포인터와는 다른 것 유의
char * sp[5]; // 총 20byte의 메모리 확보
sp[0] = "dog"; // 첫번째 요소에 할당, sp[0]에 dog 문자열 저장되어 있는 주소값 저장된다
int a[5] = {1, 2, 3, 4, 5};
int b[5] = {11, 12, 13, 14, 15};
int c[5] = {21, 22, 23, 24, 25};
int *ap[3] = {a, b, c}; // 위에 a, b, c 배열을 포인터 배열에 할당
printf("%d", ap[0][0]); // 1 출력, 2차원 배열처럼 사용 가능
다중 포인터
int *
→ int **
→ int ***
, 다중 포인터 자료형 예
int a = 10;
int *p = &a; // p라는 포인터 변수의 a의 주소값이 들어간다
int **pp = &p; // pp라는 포인터 변수의 p의 주소값(a의 주소값이 들어있는 주소값)이 들어간다
/*
p를 전달해준다고 할 때 포인터 변수를 이용해 값을 핸들링해야되기 때문에 포인터 변수 이용해 받는다
이 때 int * type의 p이기 때문에 (int *)을 가리키는 포인터 변수로 (int *)* => int ** 사용
*/
int a_method(int ** pa) {
*pa = 20; // p가 가리키는 값 a를 20으로 변경하는 코드
}
배열 포인터는 배열 전체의 시작 주소를 의미
배열명 의미
1. 첫번째 배열 요소의 시작 주소
2. 배열 전체의 저장 공간 나타내는 논리적 변수, 해당 배열 전체를 지칭한다, 유의
int a[5]; // a 배열의 시작 주소 값은 100으로 가정, 100~119까지 저장되어 있다
// +1이 의미하는 바가 다르다는 것 유의
a+1; // 시작 주소 100 + 4byte = 104byte, 여기서 a는 배열 전체의 시작 주소
&a + 1; // 시작 주소 100 + 1(배열의 총 크기, 20) = 120byte
int a[5]; // a 시작 주소 100이라 가정
int (*p)[5]; // p는 int [5]형의 시작 주소를 가리키는 포인터 변수
p = &a; // 배열 시작 주소를 가리킴, 이 때 배열의 전체 크기를 단위로 가리킨다
/*
p가 가리키는 배열의 첫번째 요소를 10으로 할당
p는 배열 전체를 의미하기 때문에 *p = *&p = a
+ 괄호 없으면 배열 indexing 우선으로 판단하기 때문에 괄호 필요
*/
(*p)[0] = 10;
// 배열의 시작 주소(100) + 배열 전체 크기 주소값(20) = 120 출력
// 주소값 120 ~ 139에 할당되는 다음 배열 의미
p+1;
(*(p+1))[0] = 33; // 주소 120부터 시작되는 int [5] 배열 첫번째 요소에 33 할당
int a[4][5]; // a의 시작 주소 100이라 가정
/*
2차원 배열은 2중 포인터이기에, a[0]은 5칸짜리 배열의 포인터 변수
때문에 a+1은 5칸짜리 배열만큼의 다음 주소
*/
a+1 -> 120
a+2 -> 140
/*
위의 예에 더해 포인터를 한번 더 써줘야 단일 메모리를 지칭한다
*/
(*(a+1))[2] // a[1][2] 지칭
/*
2차원 배열을 파라미터로 받는 함수
ap는 100을 가리킴
*/
input_ary(int (*ap) [5]){
...
}
함수 포인터
함수의 명령어들이 저장되어 있는 위치
int sum(int a, int b) // 함수가 있다고 가정
int a = 10, b = 20;
// 함수 포인터 선언 및 할당
int (*fp) (int, int);
fp = sum;
// 함수 사용 2가지 방법
res = (*fp)(a, b); // 변수 a, b를 sum 함수로 넘겨 반환 받는 코드
res = fp(a,b);
void 포인터
임의의 주소(포인터)를 다 저장할 수 있음
int a;
void * vp
// vp에 a의 주소값 대입, 포인터 변수 입장에서 아직 데이터 타입 모름
vp = &a;
vp + 1 // 사용 불가, 자료형 타입 모름
(int *)vp + 1 // 가능, 원하는 데이터 타입을 변환하는 작업 필요
// 값 지칭(a에 40 할당), a의 주소값 할당해도 대입할 때 형 명시 필요
*(int *)vp = 40;
->
간접 멤버 연산자로 접근하며 구조체 변수는 .
이용해 접근, 차이점 유의->
연산자로 접근 필요char *
, char 변수[]
두 가지 유의 필요 char *
은 외부 메모리에 데이터 저장하고 포인터 참조char 변수[]
은 변수 내부에 데이터 저장const
키워드 사용구조체 내 일반, 포인터 변수와 구조체
->
키워드를 이용해 값을 할당char *
의 경우 복사하고자 한다면 포인터 변수이기에 메모리를 할당하는 작업 후에 복사 가능
char[]
은 내부 메모리에 데이터 저장 char *
은 외부 메모리를 참조하는 형식
완충제, 완충제 역할을 하는 것을 의미하며 CS에서는 데이터 전송을 할 때 완충 작용을 하기 위한 임시 데이터 저장 공간(char 배열)을 의미
sacnf는 우리가 입력한 내용을 직접적으로 호출하는 것이 아닌 버퍼에 저장되어 있는 내용을 호출
우리가 데이터를 입력할 때 엔터를 이용해 입력의 끝을 알리는데 입력한 데이터는 바로 scanf가 핸들링 하는 것이 아닌 버퍼에 저장
scanf는 버퍼로부터 값을 받아오는데 만약 버퍼에 입력이 차 있는 경우 추가로 입력 받지 않고 버퍼에서 값을 가져온다
/*
입력 : tiger
출력 : tig
1. scanf는 버퍼가 비어있는 경우 운영체제에 제어권 전달
2. 운영체제는 사용자의 입력을 받아 버퍼에 저장
3. scanf는 버퍼를 스캔하며 데이터를 확인
-1. 데이터가 존재하는 경우 ch 공간에 가져옴
-2. 없는 경우 다시 1번 형태로 돌아감
* 이 때 변환 문자마다 제어 문자를 인식하는지 무시하는지 다름
*/
char ch;
for(int i = 0; i < 3; i++){
scanf("%c", &ch);
printf("%c", ch);
}
/*
입력
t
i
g
개행 문자 아스키 코드 값 까지 버퍼에 들어가기 때문에
만약 %c가 아니라 %d로 int type을 받는 경우에는 개행 문자를 무시한다
출력
116 t
10
15 i
*/
char ch;
for(int i = 0; i < 3; i++){
scanf("%c", &ch);
printf("%d %c\n", ch, ch);
}
scanf의 경우 sp 입력 불가
char str[20];
/*
apple jam 입력 시 apple만 저장
*/
scanf("%s", str);
gets(str);
/*
sp 포함 줄 단위로 입력 받는 함수
입력받은 문자열 버퍼에 저장하고 한 글자씩 파싱하면서 배열에 넣어주는 내부 동작
*/
저장 공간의 크기를 체크할 수 있는 수단이 없다는 것
개행 문자 전까지만 출력, 개행 문자가 버퍼 맨 앞에 있는 경우 이후 문자 핸들링 불가
gets와는 다르게 크기를 제어하며 버퍼로부터 개행 문자 가져옴
저장 공간의 크기 -1 만큼만 저장한다, 마지막에 null 문자가 들어가야 되기 때문
char str[20];
fgets(str, sizeof(str), stdin);
문자열 출력할 때 문자열을 특정 메모리에 따로 저장해놓고 해당 메모리의 시작 주소를 이용해 출력
주소값이 들어가 있기에 포인터로 이용 가능
printf("%c", "apple"[2]); // p 출력됨
문자열 출력, 자동 줄바꿈
char str[20];
puts(str);
하지만 문자열을 할당하지 않고 정의만 한 경우에 재할당이 안되는 문제 발생(포인터 변수는 주소값만 저장이 가능하기 때문)
선언 시 할당을 함께하면 알아서 특정 메모리에 저장하고 메모리 시작 주소가 포인터 변수에 저장되지만 선언과 할당을 분리한다면 알아서 작업되지 않기 때문
// apple이라는 문자열은 특정 메모리에 따로 저장되고 cp에 메모리 시작 주소가 저장된다
char * cp = "apple";
printf("%s", cp); // apple 출력
printf("%s", cp+1); // pple 출력
auto int a = 0; // auto는 default
int a = 0; // auto 취급
지역 변수 원리를 이용해 main 함수 내부에 작업을 중괄호를 이용해 사용 가능
int main(void) {
int a = 10;
{
int temp = 3;
a *= temp;
}
}
알다시피 어디서든 사용 가능
여기저기 다 사용하기 때문에 디버깅 힘들고 수정도 힘듬
int a; // 0으로 자동 초기화
int main(void) {
printf("%d", a); // 0출력
}
전역 변수와 거의 같지만 사용 범위가 함수 내부에서만 사용 가능함
static int a; // static 이용해 선언
지역 변수에서만 사용 가능
register int i;
딱 1개를 제외하고 지역 변수와 모두 같음
데이터를 저장하기 위한 메모리 저장 공간을 동적으로 할당
변수명이 없기에 포인터 이용해서 사용할 수 밖에 없다
프로그램이 실행 중에 입력되는 데이터들을 핸들링하기 위해서 필요한 기능
코드 영역
: 실행할 프로그램의 코드가 올라가는 메모리 영역
데이터 영역
: 프로그램의 전역 변수, 정적 변수, 문자열 상수가 저장되는 영역
스택 영역
: 함수 호출과 관련된 지역 변수, 매개변수가 할당되는 메모리 영역
힙 영역
: 사용자가 직접 관리할 수 있고 해야만 하는 영역, 사용자에 의해 동적으로 할당, 해제
코드가 실행 되기 전에 코드는 코드
영역에 올라가고 코드의 전역, 정적 변수, 문자열 상수 등은 데이터
영역에 올라간다. 코드의 함수와 관련된 데이터들은 스택
영역에 들어가고 코드가 실행이 되면서 사용되는 영역은 힙
에 저장된다
스택
- 힙
영역은 서로 붙어 있다(스택이 할당 속도가 훨씬 빠름)
데이터
, 스택
영역에 할당되는 메모리의 크기는 컴파일 타임에 미리 결정됨
힙
영역의 크기는 프로그램이 실행되는 런 타임 시 사용자가 직접 결정 overflow
스택은 위에서 아래로 메모리가 채워지며 힙은 아래에서 위로 채워지는데 이 때 두 공간의 메모리 주소가 겹치는 경우를 의미
stackoverflow
: 스택이 힙 영역을 침범한 경우
heapoverflow
: 힙이 스택 영역을 침범한 경우
컴파일 시점에 소스 코드를 읽고 메모리 공간을 확보하는 것을 정적 할당(staticallocation
), 프로그램이 실행되는 런타임 중에 메모리 공간을 확보하는 것을 동적 할당(dynamic allocation
)
동적 할당 함수
힙 영역에서 메모리 할당
지역 변수와 다르게 특정 함수가 마칠 경우 자동으로 반납되지 않는다
메모리를 연속적으로 할당하기 때문에 빈 공간들이 존재해도 연속적
으로 할당되지 못하는 상황이라면 할당되지 못한다
메모리 주소
를 반환, 실패하면 NULL
을 반환int *p; // 스택 영역에 할당
/*
byte 단위로 사용하고자 하는 메모리를 전달, sizeof 이용
확보한 메모리의 첫번째 주소값 void 포인터로 형변환해서 반환
*/
// 결국 스택 영역에 할당된 포인터 변수 p에 힙 영역에 할당된 malloc의 반환 주소가 들어가 있다
p = (int *)malloc(byte 단위);
free(p); // 동적 할당 받았던 메모리 공간 반납
처음 생성 시 0으로 자동 초기화 시켜줌, 초기화하는데 시간 소모
calloc(5, sizeof(int)); // int 크기 5개만큼 동적 할당
// 이미 할당되어 있는 포인터 변수, 조정하고자 하는 크기
p = (int *)realloc(p, 8); // 포인터 변수 p의 크기를 8byte로 변경
// null pointer인 경우는 두번째 인자 크기로 새로 할당한다
메모리를 효율적으로 사용하고자 동적 할당을 사용
/*
임시 문자열 temp를 이용해 메모리 동적 할당
*/
int main(void)
{
char temp[200];
char *sp[3];
int i;
for(i = 0; i < 3; i++){
gets(temp);
sp[i] = (char *)malloc(strlen(temp) + 1);
strcpy(sp[i], temp);
}
for(i = 0; i < 3; i++){
printf("%ds\n", sp[i]); // 주소값이 정상적으로 출력되는지 확인
}
for(i = 0; i < 3; i++){
free(sp[i]);
}
}
명령행(사용자가 직접 명령을 입력하는 행) 인수, 프로그램이 실행될 때 전달되는 변수
int main(void) {}
int main(int argc, char **argv) {} // argc : 명령형 인자 갯수, argv : 주소값(이중 포인터)
하나 이상의 변수를 묶어 새로운 자료형을 정의하는 도구, 자료형
1. 기본(=내장) 자료형
// 사용 전 구조 선 정의 필요, 전체 멤버 변수 크기 합한만큼 메모리 할당
struct student
{
int num, // 변수 선언 아님, 메모리 할당 X
double grade
};
int main(void){
struct student a; // 멤버 변수의 크기를 모두 합한 16byte 메모리 할당
a.num = 10; // 이용해 student 구조체의 a 인스턴스 num 접근, 이 상황이 int 변수와 동일하다 보면 된다
scanf("%d", &a.num); // 입력을 받을 수도 있다
};
실질적으로는 12byte가 아니라 16byte 정도 잡히는데 이는 패딩 byte라 한다
byte alignment
, CPU가 메모리 접근 시 접근 효율을 높이기 위해 일정 byte 기준(8byte)으로 접근 구조체 변수를 추가적으로 정의할 수 있는 방법
typedef struct student
{
~
~
} Student
구조체 변수는 단순히 사용 가능
struct vision
{
double left;
double right;
};
// 구조체를 매개변수로 변수에 직접 대입 가
struct vision swap(struct vision b){
double temp;
temp = b.left;
b.left = b.right;
b.right = temp;
return b;
};
int main(void){
struct vision a = {1.5, 2.0};
a = swap(a);
printf("%.1f, %.1f\n", a.left, a.right);
};
비트 단위로 구조체 처리
struct bit_field
{
unsigned int son:2; // 2bit
unsigned int daughter:2; // 2bit
unsigned int pet:3; // 3bit
};
struct bit_field
{
unsigned int son:2; // 2bit
unsigned int 2; // 2bit 패딩 비트
unsigned int daughter:2; // 2bit
unsigned int pet:3; // 3bit
};
->
: 간점 멤버 접근 연산자
struct score
{
int kor;
int eng;
int meat;
};
int main(void)
{
struct score a;
struct score * sp = &a; // sp는 a 전체를 가리킴
// 값을 할당하는 3가지 방법
a.kor = 90;
a.eng = 80;
a.mat = 70;
(*sp).kor = 90; // 괄호 안하면 (.)이 우선 순위 높아 오류 발생
(*sp).eng = 80;
(*sp).mat = 70;
sp -> kor = 90; // -> 연산은 위에 (*). 연산을 간략화 시킨 것
sp -> eng = 80; // 왼쪽에 구조체 포인터, 오른쪽에 구조체 멤버 변수
sp -> mat = 70;
};
struct score
{
int kor;
int eng;
int meat;
};
void print_st(struct score *p);
int main(void)
{
struct score a;
struct score * sp = &a;
a.kor = 90;
a.eng = 80;
a.mat = 70;
print_st(&a);
};
void print_st(struct score *p)
{
// 두개 같은 출력
printf("%d", p->kor);
printf("%d", (*p).kor);
};
struct address
{
char name[20];
int age;
char tel[20];
char addr[80];
};
void print_ary(struct address *p);
int main(void)
{
struct address a[5]; // 배열의 각 요소들이 address 구조체로 이루어짐
// 생성자 만드는 것처럼 초기화 가능
printf("%s", a[0].name);
printf("%d", a[0].age);
print_ary(a);
};
// 구조체 포인터 배열 a 순차 출력하는 함수
void print_ary(struct address *p)
{
int i;
for(i = 0; i < 5; i++){
printf("%s", p->name);
printf("%d", p->age);
p++; //
}
};
struct list
{
int num;
struct list *next;
};
int main(void)
{
struct list a = {10}, b = {20}, c = {30};
// a next가 b를 b next가 c를 지칭하며 single linked-list 생성된다
a.next = &b;
b.next = &c;
// single linked-list로 a, b, c 모든 num에 접근 가능
a.num
a.next -> num
a.next -> next -> num
};
위의 코드에서 포인터 변수 이용해 num에 접근하는 경우는 list가 길어질수록 코드의 가독성 반비례
/*
head point, 보통 구조체 배열의 첫번째 주소를 따로 기억해둔다
첫번째 노드를 가리킨다
*/
struct list* tp = &a;
struct list* head = &a;
while(tp != NULL)
{
printf(%d", tp->num);
tp = tp->next;
};
구조체와 많은게 유사
차이점은 멤버 변수 중 크기가 가장 큰 멤버의 크기만큼 메모리 공간을 확보하고 저장 공간을 멤버들끼리 공유
union student
{
int num;
double grade;
};
int main(void)
{
union student a;
a.num = 365;
printf("%d\n", a.num);
a.grade = 3.5;
printf("%d", a.num); // grade가 저장 공간을 사용 중이기에 존재 X
};
초기화, 기호화 해서 처리해준다
열거형은 가독성은 물론 바운더리를 지정한다
enum season{spring, summer, fall, winter};
enum season a;
a = spring;
if (a == spring) {
printf("~~~");
}
인위적으로 타입 관련 키워드를 추가해줄 수 있다
typedef int INT; // INT라는 이름으로 int 타입으로 정의 가능하다
typedef int* IP; // int *를 IP로 정의해서 사용 가능
typedef struct student Student; // struct student를 Student로 정의 가능하게 한다
Student a; // 구조체 정의
컴파일 1 : https://wonit.tistory.com/546#comment12685164
컴파일 2 : https://wonit.tistory.com/547
포인터 1 : https://hongong.hanbit.co.kr/c언어-포인터를-사용하는-이유/
포인터 2 : https://wn42.tistory.com/89
포인터 3 : https://coding-factory.tistory.com/636
buffer : https://blog-of-gon.tistory.com/199
메모리 구조 : https://velog.io/@wonhee010/%EB%A9%94%EB%AA%A8%EB%A6%AC-%EA%B5%AC%EC%A1%B0-feat.-%EC%9E%AC%EA%B7%80-vs-%EB%B0%98%EB%B3%B5%EB%AC%B8
동적 메모리 할당 : https://velog.io/@saint6839/C%EC%96%B8%EC%96%B4-%EB%8F%99%EC%A0%81-%EB%A9%94%EB%AA%A8%EB%A6%AC-%ED%95%A0%EB%8B%B9-%EA%B0%9C%EB%85%90-%EC%9E%A1%EA%B8%B0
동적 할당 함수 malloc : https://tcpschool.com/c/c_memory_malloc
헤더 파일 관련 1 : https://programfrall.tistory.com/20
헤더 파일 관련 2 : https://ko.wikipedia.org/wiki/헤더_파일
헤더 파일 관련 3 : https://losskatsu.github.io/programming/c-header/#헤더-파일을-사용하지-않고-프로그래밍하기
헤더 파일 관련 4 : https://code4human.tistory.com/110