01. 프로그램과 C언어
C 프로그램 실행 파일
- 소스파일은 프로그래머가 언어를 사용해서 작성한 파일.
- 목적파일(기계어)은 컴파일러(번역기)가 소스파일을 번역(컴파일)하면 만들어지는 파일. 사용자가 직접 만들 필요는 없음.
-
실행파일은 컴퓨터에서 실행할 수 있는 파일.
| 빌드 | | 컴파일 | 링크 | | 소스파일(*.c) | 목적파일(*.obj) | 실행파일(*.exe) |
소스파일을 목적파일로 번역하는것을 컴파일이라 하고, 목적파일을 실행파일로 변환하는것을 링크라고 한다. 그리고 컴파일과 링크를 순차적으로 수행하는 것을 빌드라고 한다.
목적파일을 두는 이유
- 소스파일을 번역해서 바로 실행파일을 만들면, 소스파일의 일부만 바뀌어도 소스파일 전체를 다시 번역해서 실행파일을 만들어야하기 때문에 비효율적.
- 컴파일러가 실행 파일을 만드는 시간을 줄이기 위해 만드는 파일.
소스파일을 나누는 이유
C언어 컴파일러는 컴파일을 할 때 먼저 소스파일의 내용이 변경되었는지 체크한 후 변경되었으면 다시 컴파일하여 목적파일을 만들고, 변경되지 않았다면 이전의 목적파일을 재사용한다. 따라서 소스파일이 여러개로 나누어져 있을 때 소스파일의 일부가 바뀌었다면, 변경된 소스파일만 컴파일 되기 때문에 컴파일 시간이 줄어드는 효과가 있다.
03. 자료형
컴퓨터의 자료 기억방식
부호비트를 사용하여 양수와 음수를 구별한다.
- 비트 8개 중 1개에 음수 또는 양수인지 상태를 저장하고 나머지 비트 7개에는 숫자를 저장한다. 여기서 양수와 음수를 구별하는 비트를 부호비트라고 한다.
- 7비트는 -128~127 사이의 숫자 중 하나를 저장.
- 음수일때는 1, 양수일때는 0을 부호비트에 저장.
- 부호를 고려하는 1바이트에서 각 비트의 값이 0 1 1 1 1 1 1 1 이라면 양수 최댓값을 의미하기 때문에 숫자 값은 127.
- 1 0 0 0 0 0 0 0 이라면 음수 최솟값을 의미하기때문에 숫자 값은 -128
- 1 1 1 1 1 1 1 1 이라면 숫자 값은 음수 최대값인 -1.
| 저장 공간 크기 | 비트 단위 환산 | 저장할 수 있는 숫자의 개수 |
|---|---|---|
| 1바이트 | 8비트 | 256개 |
| 2바이트 | 16비트 | 65,536(2 16) |
| 4바이트 | 32비트 | 4,294,967,296(2 32) |
자료형
-
signed char(char) : 부호가 있는 1바이트 저장 공간. -128~127의 숫자 중 하나를 저장 할 수 있는 크기.
-
unsigned char(unsigned char) : 부호가 없는(양수) 1바이트 저장 공간. 0~255
-
signed short int(short) : 부호가 있는 2바이트 저장 공간. -32,768~32767
-
unsigned short int(unsigned short) : 부호가 없는 2바이트 저장 공간. 0~65,535
-
signed long int(int) : 부호가 있는 4바이트 저장 공간. -2,147,483,648~2,147,483,647
-
unsigned long int(unsigned int) : 부호가 없는 4바이트 저장 공간. 0~4,294,967,295. ex)1970년 1월 1일부터. 초는 - 없으니 unsigned 사용하며 signed 사용했으면 저장공간이 얼마 안남았음.
실수 자료형(소수점 자리의 값을 저장)
- float : 소수점 5자리까지 저장. 4바이트
- double : 소수점 그 이상 저장. 8바이트
04. 상수와 변수
상수
상수 : 상수는 항상 같은 수! 변하지 않는 숫자를 의미한다. ex)PI
- 8진수 : 숫자 앞에 0을 붙여서 적는다.
- 16진수 : 숫자 앞에 0x 또는 0X를 붙여서 적는다.
| 10진수 | 8진수 | 16진수 | 2진수 |
|---|---|---|---|
| 0 | 0 | 0 | 0000 |
| 1 | 1 | 1 | 0001 |
| 2 | 2 | 2 | 0010 |
| 3 | 3 | 3 | 0011 |
| 4 | 4 | 4 | 0100 |
| 5 | 5 | 5 | 0101 |
| 6 | 6 | 6 | 0110 |
| 7 | 7 | 7 | 0111 |
| 8 | 10 | 8 | 1000 |
| 9 | 11 | 9 | 1001 |
| 10 | 12 | A | 1010 |
| 11 | 13 | B | 1011 |
| 12 | 14 | C | 1100 |
| 13 | 15 | D | 1101 |
| 14 | 16 | E | 1110 |
| 15 | 17 | F | 1111 |
- 2진수와 16진수는 4비트 단위로 나누어서 쉽게 변환 할 수 있다.
- C언어는 2진법 표현을 제공하지 않는다.
문자형 상수 ex) char key = ‘A’;
변수
변수 : 값이 바뀌는 정보, 저장공간
05. 함수
C언어와 함수
물 마시기 작업
컵을 가져온다 -> 컵에 정수기 물을 따른다 -> 컵에 담긴 물을 마신다
=>이 연속적인 행위들을 물 마시기 작업으로 묶어 정의해 놓은걸 함수라고 한다. =>정해진 단위 작업을 수행하도록 여러 개의 명령문들을 하나의 그룹으로 묶은 것을 함수라고 한다.
main 함수
- C언어는 main이라는 이름의 함수를 프로그램 시작 함수 라고 정했다. 즉 main함수는 ‘컴파일러에게 프로그램이 시작한다’고 알려주는 특별한 함수이다.
- 한 개의 프로그램에서 main함수는 반드시 한 개만 있어야 한다.
- 프로그램의 상태를 알려주기 위해 main함수의 반환값으로 int형을 사용한다. 다음 예시처럼 return 1;이라고 적어주면 프로그램이 정상적으로 작업을 끝내고 종료되었다는 것을 뜻한다.(함수의 반환값은 0, 1이 아니라 어떤 값을 사용해도 상관 없음. 다만 예시에서 사용한 1은 1이 참을 의미하므로 프로그램이 성공적으로 실행되었다는 것을 보여주기 위함이다.)
반환이 필요한 경우 : int형 사용
int main(){
return 1; //값 1을 반환함
}
C언어의 소스파일에는 꼭 하나 이상의 함수가 있다.
함수 정의하고 호출하기
함수 호출 과정
int Sum(int value1, int value2){
int result = value1 + alue2;
return result;
}
void main(){
int a = 2, b = 3;
int value = Sum(a, b);
}
- Sum(a, b) -> Sum(int value1, value2) : main함수가 Sum 함수를 호출하면서 a와 b의 값을 Sum함수에 전달합니다.
- Sum(a, b) -> Sum(int value1, value2) : Sum함수의 매개변수에 main 함수에서 전달 받은 값이 복사됩니다.
- Sum(int value1, value2) -> value1 + value2 : 입력된 값으로 더하기 작업을 수행합니다.
- return result -> Sum(a, b) : main함수로 결과 값을 반환합니다.
특별히 호출자로부터 넘겨받을 정보가 없다면 ()안을 비워두거나 ()안에 void라고 적어 인자가 없음을 명시하면 됩니다.
| 매개변수가 있는 함수 |
|---|
| Sum(int value1, int value2) |
| 매개변수가 없는 함수 | |
|---|---|
| Sum() | Sum(void) |
반환값이란?
-
함수에서 return이라는 예약어를 사용하면 함수는 그 위치에서 종결되며, return뒤에 명시단 result 변수 값이 Sum함수의 반환값이 됩니다.
- yield => 값을 반환하고 함수를 계속 진행. for each와 비슷하다 / 리스트에서 값을 뽑아서 하나씩 던짐
-
return => 값을 반환하고 함수를 종료. 리스트 통째로 던짐
- 모든 함수가 반환 값을 가지는 것은 아니며, 반환 할 값이 없으면 아래의 예시처럼 void를 명시하면 됩니다.(void 형식으로 정의한 함수의 return 뒤에 반환값을 적으면 오류)
/* o */
void ZeroData(int *p){
*p = 0;
}
/* x */
void Test(int value1, int value2){
int result = value1 + value2;
return result; //오류발생!!!
}
- return은 함수를 종결하는 역할도 한다.
void Test(int value1, int value2){ int a = 5; return; //Test 함수가 정상적으로 종료됨 a = a + 1; //오류는 아니지만 함수가 이미 종료되었기 때문에 실행되지 않음. }
함수 이름 짓는 방법
C언어의 예약어는 함수 이름으로 사용할 수 없다. 사용하면 오류로 처리된다. ex) void, return, char, int…
함수 원형 선언하기
컴파일러는 C언어 소스 코드를 기계어로 번역 할 때, 코드의 위쪽에서 아래쪽으로 내용을 읽으며 번역한다. 따라서 호출자가 피호출자보다 위에 놓이는 경우에 오류가 발생한다. 이런 경우 함수원형(Function Prototype)을 사용하여 해결한다.
int Sum(int value1, int value2); //함수 원형 선언
void main(){
int s = Sum(2, 3);
}
int Sum(int value1, int value2){
int result = value1 + value 2;
return result;
}
함수 원형을 선언 할 때 매개변수 이름을 생략할 수 있다. 하지만 어떤 의미의 변수가 전달되는지 짐작할 수 없기 때문에 생략하지 않는게 좋다.
int Sum(int, int);
- 함수를 정의한다 : 함수를 구현하는 행위
- 함수를 선언한다 : 함수의 원형을 선언
전처리기
- 같은 작업 경로(같은 폴더)에 헤더 파일이 있다면 -> #include “MyMath.h”
- 경로가 다르다면(같은 폴더 내에 없다면) 전체경로를 써줘야 한다. -> #include “C:\download\MyMath.h”
| #include <헤더 파일="" 이름="">헤더> | #include “헤더파일이름” |
|---|---|
| 비주얼 스튜디오에서 제공하는 헤더파일 포함 | 프로그래머가 정의해 사용하는 헤더 파일 포함 |
define 전처리기
상수나 명령문을 치환하는 문법이다.
ex) #define MAX_COUNT 3
ex) #define POW_VALUE(a) (a*a)
int data = POW_VALUE(3); //이렇게 명령을 치환해서 사용하는 것이 함수를 호출하는 것과 유사하기 때문에 POW_VALUE를 매크로함수라고 한다.
C 표준 라이브러리와 표준 출력 함수
단일 문자 출력 함수 putchar, putc (한글자 출력)
- putchar 함수에 ‘‘(작은따옴표)를 사용해 문자 상수로 지정하면, 컴파일러가 번역할 때 알아서 해당 문자의 아스키 값으로 변환해준다.
그리고 1바이트 기준 출력이기때문에 한글자에 2바이트인 한글은 출력할 수 없다. - putc는 여러가지 형식을 출력할 수 있는 함수인데, 두번째 매개변수에 표준출력을 의미하는 stdout값을 같이 써야 한다. 매번 stdout을 사용하려면 번거롭기 떄문에 좀 더 편하게 사용할 수 있도록 매크로함수인 putchar를 만들어 놓은것.
ex) putc(‘A’, stdout); - puts는 ““(큰따옴표)를 사용하여 문구 출력 가능.
ex) puts(“Hi~”);
printf 함수를 사용하는 방법
| 키워드 | %d | %f | %c | %s | %u | %o | %x | %% |
|---|---|---|---|---|---|---|---|---|
| 출력 형식 | 정수<10진수> | 실수 | 문자 | 문자열 | unsined 정수 <10진수> | 8진수 | 16진수 | % |
void main(){
char data = 65;
printf("%c 의 ASCII 값은 %d 입니다.", data, data);
//출력 -> A의 ASCII 값은 65 입니다.
}
실수와 정수는 숫자를 표현하는 방식이 다르다
void main(){
float value = 2.1f;
printf("%f", value);
//출력 -> 2.100000
}
- C언어는 기본적으로 double 자료형으로 처리한다. 따라서 float자료형으로 변수를 선언하면 경고가 발생하기 때문에 f를 같이 써줘야 한다.
- float < double
출력 문자열 정렬하기
voind main(){
int data = 7;
printf("[%d], [%5d], [%05d], [%-5d]", dat, data, data, data);
//출력 -> [7], [ 7], [00007], [7 ]
}
단, 출력할 때 .(마침표)도 출력 칸 수에 포함되기때문에 전체 자리수를 명시할 때 칸 수를 잘 계산해야 한다.
void main(){
double data = 3.141592;
printf("[%f], [%.4f], [%8.4f], [%08.4f]", data, data, data, data, data);
// 출력 -> [3.141592], [3.1416], [ 3.1416], [003.1416]
}
제어 코드 사용하기
| 제어코드 | 기능 |
|---|---|
| \n | 캐럿을 다음 줄로 이동(Line Feed). 개행 |
| \r | 캐럿을 해당 줄의 처음으로 이동(Carriage Return) |
| \t | 캐럿을 한 탭만큼 이동 |
| \b | 캐럿을 바로 앞 칸으로 이동 |
| \a | 시스템 스피커로 경고음 발생(모든 컴퓨터에서 이루어지는것이 아님) |
| " | 큰 따옴표 출력 |
| ' | 작은 따옴표 출력 |
07. 연산자
기본 연산자
대입 연산자
- x = 3 -> x에 3을 대입한다는 뜻. 같다는 뜻이 아님
산술 연산자
void main(){
int data1 = 5, data2 = 3;
int result1 = data1 + data2; //덧셈
int result2 = data1 * data2; //곱셈
int result3 = data1 / data2; //나눗셈(몫)
int result4 = data1 % data2; //나눗셈(나머지)
}
증감연산자
| 덧셈 연산자(이항 연산자) | 증가 연산자(단항 연산자) | 증감연산자 전위형 | 증감연산자 후위형 |
|---|---|---|---|
| int i = 5; i = i + 1; //i값에 1을 더하고 다시 i에 대입 |
int i = 5; i++; //i값을 1 증가시킴. i에 6이 대입됨 |
int i = 5 int sum = ++i; //자신의 값을 먼저 증가시켜 6을 만든 뒤 sum에 6을 대입함. i, sum 모두 6이 됨. |
int i = 5, sum; sum = i++; //i의 값 5를 sum에 먼저 대입하고 i를 증가시킴. i는 6이고 sum은 5가 됨. |
변수(i, a 등)에 증감되고 숫자는 안된다! ex) ++3 or 3++ 안됌.
관계 연산자
- 산술연산의 결과 값은 숫자로 나오지만, 관계 연산이나 논리연산의 결과 값은 진릿값(참, 거짓)으로 나온다.
- = 은 대입 연산자이고 ==은 관계 연산자이다.
- ex) A > B, A == B, A != B(A와 B가 같지 않으면 참) 등…
void main(){ int data1 = 5, data2 = 3; int result1 = data1 > 7; //거짓 int result2 = data2 != data1; //3은 5와 같지 않으니 참 /* 관계 연산의 결과 값은 0 또는 1이다 */ printf("%d, %d", result1, result2); // 결과 -> 0, 1 }
논리 연산자
| A | B | A && B (AND 논리곱) |
A ㅣㅣ B (OR 논리합) |
!A |
|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 1 |
| 0 | 1 | 0 | 1 | 1 |
| 1 | 0 | 0 | 1 | 0 |
| 1 | 1 | 1 | 1 | 0 |
void main(){
int data1 = 5, data2 = 3;
int result1 = 0 || 1;
int result2 = 3 && -1;
int result3 = data1 == 3 || data2 == 3;
int result4 = data1 == 3 && data2 == 3;
int result5 = !data1;
printf("%d, %d, %d, %d, %d, ", result1, result2, result3, result4, result5);
//결과 -> 1, 1, 1, 0, 0
//0이 아니면 다 참(1)으로 인지!!!!!!!!!
}
08. 조건문
제어문 (=조건문)
제어문은 프로그램의 실행 흐름을 제어하는 문법이다.
if문
- if(data = 3){} -> 대입연산자 잘못 사용
- if(data == 3){} -> 올바름
- 위의 실수를 줄이기 위해 if(3 == data){}로 사용할 수 있음. 실수로 if(3 = data){} 이렇게 사용해도 값(상수)에 값을 넣을 수 없어 오류가 발생하여 거를 수 있다.
void main(){
int score = 92;
char grade;
if(score >= 90){
grade = 'A';
printf("점수는 %d이고 등급은 %c입니다.\n", score, grade);
}else{
grade = 'B';
printf("점수는 %d이고 등급은 %c입니다.\n", score, grade);
}
printf("작업종료");
//결과 -> 점수는 92이고 등급은 A 입니다.
// 작업종료
}
조건 수식 연산자(3항 연산자?) : 간단한 if문을 쓸 때 사용한다.
결과 값 = (조건 수식)?수식1(참일 때 실행해 결과 값에 저장) : 수식2(거짓일 때 실행해 결과 값에 저장);
point = (value >= 10000) ? value * 0.1 : value * 0.5;
if(data > 5){
return 1;
}else{
return 0;
}
//삼항으로 변환
return (data > 5) ? 1 : 0;
if else문
void main(){
int score = 86;
char grade;
if(score >= 90){
grade = 'A';
}else if(score >= 80){
grade = 'B';
}else if(score >= 70){
grade = 'C';
}else{
grade = 'D';
}
printf("점수는 %d, 등급은 %c", score, grade);
//결과 -> 점수는 86, 등급은 B
}
switch 조건문
변수 값이 이미 정해져 있는 상수들과 비교할 때는 switch 조건문이 유리하다.
void main(){
int score = 86;
char grade;
switch(score / 10){
case 10:
case 9:
grade = 'A';
break;
case 8:
grade = 'B';
break;
case 7:
grade = 'C';
break;
default:
grade = 'D';
break;
}
printf("점수는 %d, 등급은 %c", score, grade);
//결과 -> 점수는 86, 등급은 B
}
09. 반복분
for 반복문
for 반복문은 반복의 3요소(시작 조건, 종결 조건, 조건 변화 수식)를 제일 단순하고 정확하게 표현한 반복문이다. 특히 시작과 끝이 명확한 반복 작업에 많이 사용된다.
예제)
void main(){
int sum = 0, num;
for(num = 1; num <= 5; num++){
printf("num(%d) + sum(%d) = ", num, sum);
sum += num;
printf("%d\n", sum);
}
printf("\n result : num = %d, sum : %d", num, sum);
}
//결과 -> num(1) + sum(0) = 1
// num(2) + sum(1) = 3
// num(3) + sum(3) = 6
// num(4) + sum(6) = 10
// num(5) + sum(10) = 15
//
// result : num = 6, sum : 15
for문이 반복의 3요소(시작조건, 종결조건, 조건변화 수식)를 사용할수 있도록 기본적인 형식을 제공하지만 그 형식을 꼭 지켜야하는 것은 아니다.
예제) 시작조건 생략
//시작조건은 이미 변수에서 초기화 했기 때문에 생략 가능
int sum = 0, num = 1;
for( ; num <= 5; num++){...}
//또는 시작조건에 쉼표를 사용하여 여러 개의 변수 초기화 가능
int sum, num;
for(sum = 0, num = 1; num<=5; num++){...}
예제) for 반복문으로 무한 루프 만들기(3요소를 모두 적지 않는 경우)
void main(){
int sum = 0;
int num = 1; //시작조건
for( ; ; ){ //무한루프
printf("num(%d) + sum(%d) = ", num, sum);
sum += num;
printf("%d\n", sum);
num++; //조건변화 수식
if(num > 5){ //반복문 종료
break;
}
}
printf("\n result : num = %d, sum : %d", num, sum);
}
//결과 -> num(1) + sum(0) = 1
// num(2) + sum(1) = 3
// num(3) + sum(3) = 6
// num(4) + sum(6) = 10
// num(5) + sum(10) = 15
//
// result : num = 6, sum : 15
while 반복문
while 반복문은 끝이 정해져 있지 않을 때 사용, 기약없는 반복문을 쓸 때 사용.
while(종결조건) { 명령문; }
예제)
void main(){
int sum = 0;
int num = 1; //시작조건
while(num <=5 ){ //종결조건
printf("num(%d) + sum(%d) = ", num, sum);
sum += num;
printf("%d\n", sum);
num++; //조건 변화 수식
}
printf("resut = num : %d, sum : %d", num, sum);
}
//결과 -> num(1) + sum(0) = 1
// num(2) + sum(1) = 3
// num(3) + sum(3) = 6
// num(4) + sum(6) = 10
// num(5) + sum(10) = 15
// resut = num : 6, sum : 15
결론적으로 3요소가 분명하게 정해져 있다면 for 반복문을, 시작조건이나, 조건 변화 수식이 다양하거나, 수치로 정할 수 없는 상황이라면 while문 을 사용하는게 편할 수 있다.
while 반복문으로 무한 루프 만들기
while 반복문은 ‘종결조건’을 생략할 수 없기 때문에 괄호 안에 상수값이나 수식을 반드시 적어야 한다. while(1){ … }
do~ while 반복문
while은 먼저 비교하고,
do while은 나중에 비교한다. 잘 쓰지는 않으며 명령문을 무조건 한번은 탄다.
void main(){
do{
명령문;
}while(종결조건);
}
예제) for 반복문을 중첩 사용해서 구구단 전체를 출력하기
void main(){
int step, i;
for(step = 2; step <=9; step++){
for(i = 1; i < 9; i++){
printf("%d * %d = %d\n", step, i, step*i);
}
}
}
//결과 - > 2 * 1 = 2
// 2 * 2 = 4
// ...
// 9 * 7 = 63
// 9 * 8 = 72
예제) 별찍기
#define max 5
void main(){
for(int col = 1; col <= max; col++){
for(int i = 1; i < col; i++){
printf("*");
}
printf("\n");
}
}
//결과 - > *
// **
// ***
// ****
예제) 뒤집어진 별찍기
#define max 5
void main(){
for(int col = max; col > 1; col--){
for(int i = 1; i < col; i++){
printf("*");
}
printf("\n");
}
}
//결과 -> ****
// ***
// **
// *
예제) 공백있는 별찍기
#define max 5
void main() {
int sc = 0;
for (int step = 1; step <= max; step += 2) {
sc = (max - step) / 2;
for (int s = 1; s <= sc; s++) {
printf(" ");
}
for (int i = 1; i <= step; i++) {
printf("*");
}
printf("\n");
}
}
// 결과 - > *
// ***
// *****
break 제어문
break는 하나의 반복문에만 미치기 때문에 중첩된 반복문에서도 하나의 반복문에만 적용된다.
따라서 특정 상황에서 반복문을 둘 다 종료시키고 싶다면 바깥쪽 반복문에도 조건문을 사용하여 break문을 적어줘야 한다.
void main(){
int m, n;
for(m = 5; m < 7; m++){
for(n = 0; n < 3; n++){
if(m == 5 && n ==1){ //m값은 5이고 n값은 1일 때 break문 수행
break;
}
printf("m(%d) - n(%d)", m, n);
}
if(m == 5 && n == 1){
break;
}
}
}
//결과 -> m(5) - n(0)
continue 제어문
continue문은 1회성 취소를 사용한다. (이번 반복은 skip이다 할 때 사용)
void main(){
int m;
for(m = 5; m < 8; m++){
if(m == 6){ //m값이 6일 때 continue문 수행
continue;
}
printf("m(%d)\n", m);
}
}
// 결과 -> m(5)
// m(7)
go to 문
go to ~~~;라고 선언 해주면 go to 문 만났을 때 ~~~라고 선언 해 준 부분으로 그냥 바로 이동해 버린다. go to 문은 되도록 사용하지 않는 것이 좋으며 실무에서 쓰면 돌맞는다. 걍 이런게 있다 정도로만~
10. 시프트 연산자와 비트 연산자
비트 단위 연산과 비트 패턴
알면 좋고 모르면 불편한 정도. 가벼운 맘으로 강의 듣기~, but 하드웨어쪽은 필수!
- c언어의 가장 작은 자료형은 1바이트이다.
- 오른쪽에서 왼쪽으로 갈수록 비트 번호가 커진다.
시프트 연산자
- 시프트 연산자(«, »)는 변수의 값을 지정한 비트 수만큼 왼쪽 또는 오른쪽으로 비트를 이동 시키는 기능을 합니다.
- 비트가 이동한 빈자리에는 0이 채워집니다.
- 변수에 부호가 있는 경우 -> 부호비트가 1이면 빈자리는 1로 채워진다
- 비트가 왼족으로 이동해서 사라지는것을 오버플로(Overflow)라 부르고, 오른쪽으로 이동해서 사라지는 것을 언더플로(Underflow)라고 부른다.
- 결론은 특수한 목적에서만 사용.
데이터 암호화하기
XOR(^) 연산자 사용..
11. 지역 변수와 전역 변수
지역변수(Local Variable)
- 함수 안에 선언한 변수나 매개변수는 지역변수이다.
- 자신을 선언한 함수가 호출 될 때 메모리에 저장공간이 만들어졌다가 함수 호출이 끝나면 함께 사라진다. 즉 함수와 같은 수명을 가진다.
- 지역 변수 이름은 서로 다른 함수에서 선언된 변수라면 중복해서 사용할 수 있다.
- 지역변수는 스스로 초기화되지 않기 때문에 필요할 때 프로그래머가 직접 초기화 해야 한다.
int ReturnNum(){
int num = 5;
return num;
}
void main(){
ReturnNum();
printf("%d", num);
}
// 결과 -> 11행의 num이 선언되지 않은 함수라면서 오류 발생
int Sum(int data1, int data2){ //매개변수 data1, data2는 Sum함수의 지역변수
int result = data1 + data2; //해당 result와 아래 result는 서로 다른 메모리에 할당되어 값이 공유되지 않는다.
return result;
}
void main(){
int result;
result = Sum(5, 3); //해당 result와 위의 result는 서로 다른 메모리에 할당되어 값이 공유되지 않는다. 초기화 되지 않아 어떤 값이 들어있는지 모름
printf("5 + 3 = %d", result);
}
전역변수(Global Variable)
- 이 변수는 프로그램이 시작할 때 만들어졌다가 프로그램이 종료될 때 사라진다. 따라서 수명이 프로그램과 같다.
- 전역 변수는 특별한 초기화 값이 없으면 0으로 초기화 된다.(프로그램마다 다르며 무조건 0으로 초기화되지 않는다.)
- 전역변수를 사용하면 의존적으로 바뀌게 된다. -> 예를들어 다른 프로그램에서 재사용하기 위해 코드를 복사하는 경우에는 변수 선언한것도 같이 옮겨야 하는데, 옮겨 갈 프로그램에 이미 같은 이름으로 선언된 변수가 있다면 하나는 이름을 바꿔야 한다.
- 전역 변수는 꼭 필요한 경우에만 사용하세요 -> 같은 이름의 전역변수를 사용하면 프로그램이 링크 될 때 중복 오류가 발생
지역변수와 전역변수의 이름이 같다면?
두 변수는 성격이 달라서 오류로 처리되지 않으며, 지역을 우선해서 처리한다.
extern 키워드
같은 프로젝트 안에 존재하는 전역 변수를 참조하겠다는 의미로 사용
//Sum.c
int result = 0; //전역변수
void Sum(int data1, int data2){
result = data1 + data2;
}
//----------------------------------------------------------------------------------------------------------------------------------------
//Main.c
void Sum(int, int); //함수 원형 선언
exturn int result; //전역변수 참조 선언
void main(){
Sum(5, 3);
printf("5 + 3 = %d",result);
}
사용시 주의할 점
프로젝트 p
| A.c | B.c |
|---|---|
| int g_data1 = 0; int g_data2 = 0; int g_data5 = 0; |
1. int g_data1; 2. exturn int g_data2; 3. exturn int g_data3; 4. exturn int g_data4 = 0; 5. exturn int g_data5 = 0; |
- 오류 - >다른데서 이미 전역변수 선언해서 썼으니 너는 쓰지마
- 정상
- 오류 -> A.c에 선언되지 않은 전역변수를 참조하여 오류
- 정상 -> exturn 참조할 때 초기화를 사용했기 때문에 처리된다.
- 오류 -> A.c에서 이미 전역변수가 선언됐기 때문에 중복되었다고 오류
static 키워드
- 해당 전역 변수의 사용 범위를 변수를 선언한 소스 파일로 제한
프로젝트 p
| A.c | B.c |
|---|---|
| int g_data1 = 0; static int g_data2 = 0; static int g_data3 = 0; |
1. int g_data1; 2. int g_ data; 3. exturn int g_data3; |
- 오류 -> 같은 이름의 전역변수가 있기 때문에 오류
- 정상 -> static으로 구별되어 별개의 전역변수로 처리
- 오류 -> static이라 exturn으로 참조할 수 없기 때문에 오류
const 키워드
- 변수를 상수처럼 쓰고싶을 때 사용.
- 변경이 안되게끔 잠궈준다.
- 변수의 값을 변경하지 않겠다는 의미.
12. 배열과 문자열
배열
- 배열은 자료형이 같은 변수들을 그룹으로 묶어서 관리할 때 사용한다.
- 색인(인덱스)는 0부터 시작한다.
void main(){
short student[20]; //short 타입 변수 20개 만들어라
student[1] = 10; // 배열의 두번째 요소에 10 대입
}
배열을 사용할 때 주의점
[]안에 반드시 상수를 적어야 한다.
short student[20]; //20은 무조건 상수!
int CNT = 30;
int arr[CNT]; //오류!!! 변수 안된다.
배열 초기화
배열의 각 요소에 일정한 값을 대입하여 초기화해서 사용해야 한다.
쉼표를 사용한 배열 초기화
void main(){
short student[20] = {0, };
short data[5] = {3, }; // 결과 -> 3, 0, 0, 0, 0 처럼 된다
}
배열 크기를 생략하여 사용할 수 있음
short data[] = {1, 2, 1, 2, 1};
void main(){
char data[5] = {1, 2, 3, 4, 5};
int result = 0, i;
for(i = 0; i < 5; i++){
result = result + data[i];
}
printf("data 배열의 각 요소의 합은 %d입니다.", result);
}
// 결과 -> data 배열의 각 요소의 합은 15입니다.
문자열
- C언어에서 문자를 저장하는 데 가장 적합한 자료형은 char형
- 문자의 끝에 NULL(널) 문자 0을 추가로 입력해서 ‘이 배열에 저장된 정보는 문자열이다’ 라고 컴파일러에게 알려줘야 한다. (문자의 끝에 0을 넣어주는것은 컴파일러가 왠만하면 알아서 해줌. 굳이 내가 안써도 된다)
- NULL 문자는 0이라고 적어도 되고 ‘\0’이라고 적어도 된다. (왠만하면 역슬래시 써주는게 좋음)
- 배열의 크기와 문자열의 길이는 다르다 ex) HELLO = 배열의 크기:6, 문자열의 길이:5
char data[6] = {'h', 'a', 'p', 'p', 'y', 0}; //문자개수는 5개이고 끝에 0을 덧붙여야 하므로 배열의 크기는 6
char data[6] = "happy"; //이렇게 쓰면 문자열의 끝에 NULL 문자 0이 자동으로 포함된다
- strlen(문자열이 저장된 변수 이름)
- strcpy(복사해서 저장할 변수 이름, 복사할 기존 변수 이름)
- strcat(기존 문자열이 저장된 변수 이름, 새로 덧붙일 문자열)
예제) 문자열의 길이 구하기
#include <string.h> //문자열 표준 함수를 사용하기 위해 추가함
void main(){
int data_length;
char data[10] = {'h','a','p','p','y',0};
data_length = strlen(data);
printf("data length = %d", data_length);
}
//결과 -> data length = 5
예제) 두 개의 문자열 합치기
#include <string.h> //문자열 표준 함수를 사용하기 위해 추가함
void main(){
char data[10] = "abc";
char result[16];
strcpy(result, data);
strcat(result, "def");
printf("%s + \"def\" = %s", data, result);
}
//결과 -> abc + "def" = abcdef
2차원 배열
- 대괄호 []를 두 번 사용해서 선언하는 배열이 2차원 배열이다.
- []연산자는 동일한 우선순위를 가질 때 왼쪽에서 오른쪽으로 연산을 수행한다.
- 즉 왼쪽에 있는 []연산자를 먼저 처리한다
2차원배열 형식 들어가면 오류가 남…………..
예제) 1차원 배열의 바둑판
void main(){
char data[12] = {0,0,2,0,1,1,0,0,2,1,0,2};
int i, x, y;
for(i = 0; i < 12; i++){
x = i % 4 + 1; //열번호 구함
y = i / 4 + 1; //행번호 구함
printf("%d행 %d열에", y, x);
if(data[i] == 1){
printf("검은 돌이 놓여있습니다\n");
}else if(data[i] == 2){
printf("흰 돌이 놓여있습니다\n");
}else{
printf("돌이 없습니다.\n");
}
}
}
//결과 -> 1행 1열에돌이 없습니다.
// 1행 2열에돌이 없습니다.
// ...
// 3행 3열에돌이 없습니다.
// 3행 4열에흰 돌이 놓여있습니다
예제) 2차원 배열의 바둑판
void main(){
char data[3][4] = { {0,0,2,0,}, {1,1,0,0,}, {2,1,0,2} };
int x, y;
for(y = 0; y < 3; y++){
for(x = 0; x < 4; x++){
printf("%d행 %d열에 ", y+1, x+1);
if(data[y][x] == 1){
printf("검은 돌이 놓여있습니다.\n");
}else if(data[y][x] == 2){
printf("흰 돌이 놓여있습니다.\n");
}else{
printf("돌이 없습니다.\n");
}
}
}
}
//결과 -> 1행 1열에돌이 없습니다.
// 1행 2열에돌이 없습니다.
// ...
// 3행 3열에돌이 없습니다.
// 3행 4열에흰 돌이 놓여있습니다
13. 포인터
운영체제의 메모리 관리 방식
운영체제와 프로그래밍
- C언어 소스코드에서 사용한 변수들은 컴파일 작업 후 기계어로 번경되면 모두 메모리 주소로 바뀌어서 적용된다.
- 결국 기계어에서는 변수 이름보다 변수가 위치한 메모리의 주소가 중요하다.
32비트 운영체제와 64비트 운영체제
- 실제 개발 현장에는 32비트로 개발하는 경우가 많다. 32비트 방식으로 개발해도 64비트 운영체제에서 모두 동작하기 때문.
- 32비트 운영체제는 우리가 메모리라고 부르는 RAM을 4GB(기가바이트, 230)밖에 사용하지 못하지만 64비트 운영체제에서는 16EB(엑사바이트, 260)까지 사용할 수 있다.
- 따라서 자신의 시스템이 RAM을 4GB이상 사용한다면 64비트 운영체제를 설치해야 메모리를 100% 다 사용 할 수 있다.
- 64비트의 단점은 기본적으로 메모리 사용량이 많으며 낮은 사양의 컴퓨터에 64비트 운영체제를 설치하는 것은 오히려 손해이다.
메모리 주소 지정 방식
- 메모리를 사용하려면 반드시 사용할 주소를 지정해야하고, 메모리가 1바이트 단위로만 사용되는 것은 아니기 때문에 프로그래머가 메모리를 사용할 때 한 번에 읽거나 저장할 크기를 명시해야 한다.
- 메모리 주소번지까지 컨트롤 하는 경우는 거의 없다.
*컴퓨터는 오직 2진수(0, 1)를 사용한다 *C는 보통 16진수를 활용하며 컴파일러가 알아서 2진수로 변환해준다.
직접 주소 지정 방식
- C언어의 '변수' 문법과 같다.
- 번역기의 도움을 받아서 내부적으로 변수가 주소로 변환되어 결과적으로는 직접 주소 지정 방식을 사용하게 되는 것이다.
- 직접 주소 지정 방식의 한계 : 각 함수의 지역 변수는 해당 함수 안에서만 사용 가능
간접 주소 지정 방식
- 32비트 운영체제에서는 크기를 4바이트로 고정해야 한다.(64비트는 8바이트)
포인터
- c언어에서 직접 주소 지정 방식은 변수 문법이라고 했다.
-
간접 주소 지정 방식은 값을 저장할 '주소'를 메모리에 저장하는 것이다.
- 포인터 문법을 사용해 선언한 포인터 변수는 메모리 주소**만**을 저장하기 위해 탄생한 특별한 변수이다.
- 포인터 변수는 크기가 무조건 4바이트로 정해져있기 때문에 변수의 크기를 적을 필요가 없다.
short birthday;
short *ptr; //포인터 변수 선언
ptr = &birthday; //birthday 변수의 주소를 ptr 변수에 대입
*ptr = 1042 //ptr에 저장된 주소에 가서 값 1042를 대입함. 즉 birthday = 1042
short *ptr = &birthday; //포인터 변수를 선언!
*ptr = 1042; //ptr 포인터가 가리키는 대상에 가서 1042 값을 대입하겠다는 의미. 정의!
//위와 아래는 같지 않다.
예제) call by value. 일반변수는 값 변동x
void Test(short data){
short soft = 0;
soft = data; //soft = 5
tips = 3; //오류 발생
}
void main(){
short tips = 5;
Test(tips);
}
//data = tips;
예제) call by reference. 포인터 사용시 원본 값 변경된다.
void Test(short *ptr){
short soft = 0;
soft = *ptr; //soft = tips;
*ptr = 3; //tips = 3;
}
void main(){
short tips = 5;
Test(&tips);
}
//ptr = &tips;
예제) 간접 주소 지정 방식(포인터)으로 변수 값 교환하기
void Swap(int *pa, int *pb){
int temp = *pa; //참조
*pa = *pb; //복사
*pb = temp; //참조
}
void main(){
int start = 96, end = 5;
if(start > end){
Swap(&start, &end);
}
printf("start : %d, end : %d", start, end);
}
// 결과 -> start : 5, end : 96
인자값이 2개라서 return값은 하나만 되기 때문에 포인터 사용한다. C에서만 return값을 하나만 사용할 수 있다.
포인터와 const 키워드
주소 변경 실수를 방지하기 위해 const 키워드를 사용한다.
- int * const p; : 주소를 변경하면 오류 발생
- const int *p; : 대상의 값 변경시 오류 발생
- const int * const p; : 둘 다 잠금
포인터 변수의 주소 연산
배열에서 사용시 좋다!!
포인터 변수에 +1을 하면 자신이 가리키는 대상의 크기만큼 증가한다.
short data = 0;
short *p = &data;
p = p+1; //포인터 변수에 저장된 주소 값을 1만큼 증가시킴
char *p1 = (char *)100; //p1에 100번지를 저장함
short *p2 = (short *)100;
int *p3 = (int *)100;
double *p3 = (double *)100;
p1++; //1바이트이기 때문에 p1에 저장된 주소값이 101
p2++; //2바이트라서 102
p3++; //4바이트라서 104
p4++; //8바이트라서 108
포인터가 가리키는 대상의 크기
변수 p에 일반 변수 data의 주소 값을 저장하고 포인터 변수 p를 이용하여 data 변수의 값을 변경하는 경우에는 두 변수의 자료형을 같게 지정하는것이 일반적이다.
int data = 0;
short *p = (short *)&data;
//앞에 두칸만 갖다 사용
예제) int형 변수에 저장된 값을 1바이트 단위로 출력
void main(){
int data = 0x12345678, i;
char *p = (char *)&data;
for(i = 0; i < 4; i++){
printf("%x, ", *p);
p++;
}
}
// 결과 -> 78, 56, 34, 12,
위 예제는 p가 가지고 있는 주소 값을 옮기는 방식으로 작업했는데, p의 주소 값을 변경하지 않고 data 변수 값을 1바이트씩 출력하고 싶다면 반복문 코드만 다음과 같이 변경하면 된다.
for(i = 0; i < 4; i++){
printf("%x, ", *(p + i) );
}
void *형 포인터
- 주소 값을 저장할 수는 있지만 해당 주소에서 값을 읽거나 저장할 때 사용하는 크기는 정해져 있지 않다.
- 즉 사용할 메모리의 시작 주소만 알고 끝 주소를 모를 때 사용하는 포인터 형식이다.
- void * 는 주소를 사용할 때 반드시 ‘사용할 크기’를 표기해야 한다.
int data = 0;
void *p = &data; //data의 시작 주소를 저장함
*p = 5 //오류 발생. 대상 메모리의 크기가 지정되지 않음.
int data = 0;
void *p = &data;
*(int *)p = 5; //형 변환 문법을 사용하여 대상의 크기를 4바이트로 지정하므로 data 변수에 5가 저장됨
예제) void *를 사용항 대상 메모리의 크기 조절하기
int GetData(void *p_data, char type){
int result = 0;
if(type == 1){
result = *(char *)p_data;
}else if(type == 2){
result = *(short *)p_data;
}else if(type == 4){
result = *(int *)p_data;
}
return result;
}
void main(){
int data = 0x12345678;
printf("%x", GetData(&data, 2));
}
//결과 -> 5678
14. 표준 입력 함수
표준 입력 함수
표준 입력 함수란?
다양한 입력 장치 중에 사용하는 시스템이 가장 기본으로 생각하는 장치를 ‘표준입력장치’ 라고 부른다. ex) 키보드
입력 버퍼를 초기화하는 rewind 함수
rewind(stdin); //rewind 함수를 사용하면 입력 버퍼를 초기화 할 수 있음
문자 한 개를 입력 받는 getchar, getc 함수
- char 들어간건 무조건 한 글자
- 두개의 차이점을 모르겠다. 그냥 한글자 받을 때 쓰는 함수들인듯 하다.
예제) getchar 함수를 사용할 때 주의할 점
void main(){
int input_data;
input_data = getchar();
printf("first : %c\n", input_data);
input_data = getchar();
printf("second : %c\n", input_data);
}
//결과
//a
//first : a
//second :
엔터도 문자로 인식해서 버퍼를 초기화해주는 rewind(stdin)을 사용해줘야 한다.
문자열을 입력 받는 gets 함수
- gets 함수는 한 번에 여러 개의 문자를 입력받을 수 있으며, 엔터 키를 입력할 때까지 모든 문자를 하나의 문자열로 간주한다.
- 문자열을 저장하기 위해서 선언된 변수의 시작 주소를 넘겨줘야 한다.
void main(){
char input_string[10];
gets(input_string); //gets를 안쓰고 input_string만쓰면 주소값만 출력된다.
printf("input : %s", input_string);
}
ptr = &일반주소 ptr = 배열은 & 안써줘도 된다.
gets를 실행하고 나오는 값이 NULL(문자출력 X)이냐 NULL이 아니냐(문자출력)
void main(){
char input_string[10];
if(NULL != gets(input_string)){
printf("input : %s", input_string);
}else{
printf("input -> cancled");
}
}
위의 예제는 문자를 9개까지만 저장할 수 있다. 끝에 NULL값(엔터값)이기 때문에
문자열을 정수로 변환하기
gets 함수를 사용하면 1234같은 숫자를 입력해도 정수가 아닌 문자열로 인식한다. 그렇기 때문에 atoi라는 함수를 사용하여 문자열을 정수로 변환한다.
#include <stdlib.h> //atoi 함수를 사용하기 위해 포함
void main(){
int first_num, second_num;
char first_string[16], second_string[16];
printf("input first number : ");
gets("first_string");
printf("input second number :");
gets("second_string");
first_num = atoi(first_string);
second_num = atoi(second_string);
printf("%d + %d = %d", first_num, second_num, first_num + second_num);
}
표준 입력함수 scanf
getchar 함수는 하나의 문자를 입력받는 함수이고 gets 함수는 문자열을 입력받는 함수이다. 그러나 scanf 함수는 문자, 문자열, 정수, 실수 모두 입력받을 수 있도록 형식화된 입력을 제공한다.
| 키워드 | %d | %hd | %f | %lf | %c | %s |
|---|---|---|---|---|---|---|
| 입력 형식 | 정수 int | 정수 short int | 실수 float | 실수 double | 문자 | 문자열 |
int data;
scanf("%d", &data);
//scanf 함수의 기본 형태
void main(){
int int_data;
float float_data;
scanf("%d", &int_data);
scanf("%f", &float_data);
printf("input : %d, %f", input_data, float_data);
}
- scanf 함수 입력 값의 구별은 엔터나 공백(space)문자로도 가능하다.
- 그리고 이 함수는 엔터키나 공백이 여러개 입력되면 무시하고 실제 정보를 기준으로 입력을 받아들인다. -> 아무리 엔터를 눌러도 skip한다.
- 그렇기때문에 사용자가 입력하는 문자열에 공백이 포함된다면 gets함수를 사용하는것이 좋다.
scanf 함수는 입력 형식 키워드와 자료형이 일치해야 한다
char data1 = 5;
short data2 = 6;
int data3 = 7;
printf("%d %d %d", data1, data2, data3) //정상적으로 5,6,7 출력
scanf("%d %d %d", &data1, &data2, &data3); //번역할 때 오류는 없지만 실행할 때 문제 발생. (data1, data2가 자료형이 다름)
| 입력 형식 키워드 | 변수 자료형 |
|---|---|
| %c | char, unsigned char |
| %o, %d %x | int, unsigned int |
| %f | float |
| %lf | double |
| %s | char*, char[] |
예제) scanf 함수를 사용하여 나이 입력 받기
void main(){
int num = 0;
while(1){ //무한루프
printf("input age : ");
if( scanf("%d", &num) == 0 ){ //숫자가 아닌 값을 입력하면 0
rewind(stdin);
printf("[Enter] digit number! \n");
}else{
if(num > 0 && num < 130){
break; //정상적으로 입력되었기 때문에 반복문을 빠져나감
}else{
printf("incorrect age!\n"); //나이의 범위가 잘못입력
}
}
}
printf("your age : %d", num); //입력된 나이 출력
}
15. 배열과 포인터
배열과 포인터 표기법
배열 표기법과 포인터 표기법의 관계
포인터는 포인터 변수가 가리키는 메모리의 시작 주소를 기준으로 삼고, 배열도 해당 배열이 사용하는 메모리 그룹의 시작 주소를 기준으로 삼는다.
따라서 배열과 포인터는 표기법을 서로 바꿔 사용할 수 있다.
void main(){
//배열
char data[5];
data[1] = 5; //아래의 *(data + 1) = 5; 는 해당 data[1] = 5;와 같음
*(data + 1) = 5; //위의 data[1] = 5;는 해당 *(data + 1) = 5;와 같음
//포인터
char data;
char *p = &data; //data 변수의 주소를 p에 저장
*p = 3; //p가 가리키는data 변수에 3을 대입. 아래줄과 같음
p[0] = 3; //*p = 3;과 같음. 위줄과 같음
}
책 369페이지 하단 필기한 그림 참조
배열과 포인터로 사용하는 것 중 배열 표기법이 더 간단해 보일 수 있다. 하지만 어떤 문법이든 표기가 간단하다는 뜻은 표현에 제약이 있다는 뜻
- 배열 표기법은 요소를 구성하는 모든 바이트 값을 한 번에 수정하기 때문에 제약있다.
- 포인터 표기법은 형변환(casting)을 하여 제약 없음(포인터 표기법은 배열 항목의 크기와 상관없이 자유롭게 값을 수정 할 수 있다).
ex)*(char *)(data + 1) = 0x22; //일시적으로 char *형으로 변환함
배열을 사용하는 포인터
포인터와 배열은 연산 횟수의 차이. 배열의 색인 작업도 연산이기 때문에 같은 요소를 반복적으로 사용하는 경우에 효율이 떨어진다.(훨씬 느리다)
예제) 배열 예제를 포인터 사용해서 바꾸기 (포인터를 사용하여 배열의 각 요소에 저장된 값 합산하기)
void main(){
char data[5] = {1,2,3,4,5};
int result = 0, i;
char *p = data;
for(i = 0; i < 5; i++){
result = result + *p;
p++;
}
printf("data 배열의 각 요소의 합은 %d입니다.", result);
}
// 결과 -> data 배열의 각 요소의 합은 15입니다.
배열을 기준으로 포인터와 합체하기
char *형 포인터 변수가 3개 필요하다면 char *p1, *p2, *p3; 등과 같이 선언해서 포인터 변수를 만들어야 하는데, 포인터가 100개 이상 필요시 불편함. 따라서 포인터 변수도 배열로 선언해서 사용할 수 있다.
void main(){
char *p[5]; //char *p1, *p2, *p3, *p4, *p5;라고 선언한것과 같음
}
위와 같이 선언하면 포인터가 5개 선언된 것이기 때문에 p 배열의 크기는 20바이트(포인터의 크기는 4바이트 고정)이다.
void main(){
char data[3][5];
char (*p)[5]; //char[5] 크기의 대상을 가리킬 수 있는 포인터 선언. 가독성이 떨어져서 교수님도 잘 안씀
p = data; //포인터 변수 p는 2차원 배열 data 변수의 시작 주소를 저장
(*p)[1] = 3; // p가 가리키는 대상의 2번재 항목에 3을 대입. p[0][1];과 같음
(*(p+1))[2] = 4; //p+1이 가리키는 대상의 3번재 항목에 4를 대입. p[1][2]=4;와 같음
(*(p+2))[4] = 5; //p+2가 가리키는 대상의 5번째 항목에 5를 대입. p[2][4]=5;와 같음
//-------------------------------------------위는 교재 내용임
(*p)[5]; //포인터와 배열이 결합해서 2차원됐음. 줄을 의미(가로)
//p[0] -> p+0 줄로 이동
//p[1] -> p+1 줄로 이동
//-------------------------------------------위아래 교수님 내용
(*p3)[5] // 한줄
p3 + 1 //다음줄로 이동
*(p3+1) + x //x 옆으로 이동. 옆으로 이동하고싶으면 별쳐주기
*(*(p3+1)+x) //해당 값 가져오기. 별 더쳐주면 갑 가져오기
}
16. 메모리 할당
프로그램과 프로세스
- 프로그래머가 만든 프로그램 실행 파일을 프로그램이라고 부른다.
- 운영체제가 실행파일의 명령들을 읽어서 메모리에 재구성하는것을 프로세스라고 한다.
- 프로세스는 단순히 실행할 명령들로만이루어져 있는 것이 아니라 다음 그림처럼 여러 가지 정보나 사용자가 입력한 데이터를 기억하는 메모리 공간도 포함하고 있습니다. 이러한 공간을 세그먼트라고 한다. (세그먼트 = 정보를 기록하는 메모리 공간)
프로세스는 세그먼트의 집합으로 구성되어 있다.
- 코드 세그먼트 : 컴파일러는 C 언어 소스를 기계어로 된 명령문으로 번역해서 실행 파일을 만든다. 실행 파일이 실행되어 프로세스가 만들어지면 이 기계어 명령들은 프로세스의 ‘코드 세그먼트’에 복사되어 프로그램 실행에 사용된다. (기계어 명령들이 저장되는 곳)
- 데이터 세그먼트 : 프로그램이 시작해서 끝날 때까지 계속 사용되는 데이터는 ‘데이터 세그먼트’에 보관된다.(데이터 저장)
- **스택 세그먼트 ** : ‘스택 세그먼트’는 프로그램 실행 중에 필요한 임시 데이터를 저장하는데 사용하는 메모리 영역이다. 스택 세그먼트는 지역 변수가 놓이는 스택과 동적으로 할당되는 메모리 공간인 힙으로 나뉩니다.
정적 메모리 할당은 일회성 상수같은거에 사용
정적으로 할당된 메모리를 관리하는법
skip
지역 변수와 스택
스택에 대하여
스택은 후입선출 정도만 알고 skip. 이런게 있다정도만. 초심자에게 어렵고 원래 많은 양인데 몇페이지로 될 게 아니다.
동적 메모리 할당 및 해제
정적 메모리 할당의 한계
값이 변할 때마다 수정해야하고, 메모리 크기 제한있다(일정부분밖에 못씀)
동적 메모리 할당
malloc 함수로 동적 메모리를 할당하며(포인터로 해줘야 한다), void * 형식으로 반환해준다. void *를 사용하면 형변환(casting)을 해줘야 한다.
malloc 함수는 메모리 할당에 실패하는 경우도 있다! 이런 경우 malloc함수는 할당된 메모리 주소 대신에 NULL을 반환한다. 따라서 malloc 함수가 메모리 할당에 실패하는 경우를 대비하여 다음과 같이 넘겨받은 주소가 NULL인지 체크하는것이 좋다
short *p = (short *)malloc(100);
if(p != NULL){
//메모리 할당에 성공함. 이 시점부터 100바이트 메모리 사용 가능
}else{
//메모리 할당에 실패
}
free함수로 할당된 메모리 해제하기(반드시 해줘야 한다!)
동적메모리에서 언제 어떻게 할당받고 -> malloc 언제 어떻게 해제해야 하는지 -> free(p) (p가 가지고 있는 주소에 할당된 메모리 해제) 두개는 셋트!
할당되지 않은 메모리를 해제하거나(malloc함수 선언 x), 정적으로 할당된 메모리를 해제하거나(malloc함수 선언 x), 할당된 메모리를 두 번 해제하는 경우 오류 발생
동적 메모리 사용하기
정적 메모리 할당을 사용했을 때 발생할 수 있는 문제점
int data_size = 3;
int data[data_size]; //배열의 요소 개수는 상수만 명시! 오류발생
malloc 함수는 메모리 할당 크기를 변수로 지정 할 수 있다
int data_size = 12;
int *p = (int *)malloc(data_size); //12바이트의 메모리가 동적 할당됨
#include <malloc.h>
void main(){
int *p_num_list, count = 0, sum = 0, limit = 0, i;
printf("사용할 최대 개수를 입력하세요 : ");
scanf("%d", &limit);
p_num_list = (int *)malloc(sizeof(int)*limit); //사용자가 입력한 개수만큼 정수를 저장할 수 있는 메모리를 할당
while(count < limit){
printf("숫자를 입력하세요 (9999를 누르면 종료) : ");
scanf("%d", p_num_list + count);
if(*(p_num_list + count) == 9999){
break;
}
count++;
}
for(i = 0; i < count; i++){
if(i > 0){
printf(" + ");
}
printf("%d", *(p_num_list + i));
sum = sum + *(p_num_list + i);
}
printf(" = %d\n", sum);
free(p_num_list);
}
17. 다차원 포인터
다차원 포인터란?
동훈아라고 직접 말하지 않고 친구야 또는 나의 친구야 같은 자신을 기준으로 대상을 가리키는 간접표현으로 여러 번 가리키는 포인터를 ‘다차원 포인터’라고 한다.
다차원 포인터 정의하기
*이 하나면 1차원, *이 두개면 2차원 포인터라고 한다. 이 포인터 변수를 선언할 때 사용하는 * 키워드는 최대 7개(컴파일러마다 다름)까지 사용할 수 있으며, 왠만하면 3개이상 쓰지 마라.. 3차원도 많이 안씀
일반 변수의 한계와 다차원 포인터
short data = 0;
int my_ptr = (int)&data; //&data는 short * 형식의 값을 가지기 때문에 int형 변수인 my_ptr에 저장하기 위해서 형변환한다. 4바이트 크기라서 정상적으로 주소 저장
*my_ptr = 3; //오류 발생. my_ptr은 포인터가 아니라서 * 연산자 사용 할 수 없다.
2차원 포인터
2차원 포인터는 1차원 포인터의 주소 값을 저장한다.
void main(){
short data =3;
short *p = &data;
short **pp = &p; //1차원 포인터 p변수의 주소 값을 2차원 포인터 p에 저장
printf("[before] data : %d\n", data);
*p = 4;
printf("[use *p] data : %d\n", data);
**pp = 5;
printf("[use **pp] data : %d\n", data);
}
★★★★★★★★★★★★★★★★★★★★★★★★★★★★★
O - > 값(자신이 저장하는 값)
** -> 직전 참조 pointer의 값
*** -> 직전 참조 pointer가 가리키는 값
★★★★★★★★★★★★★★★★★★★★★★★★★★★★★
예제) malloc 함수를 사용하여 2차원 포인터 구조 만들기
void main(){
short **pp; //2차 포인터만 있기때문에 1차 포인터 공간 만들어줘야 함
pp = (short **)malloc(sizeof(short *)); //1차 포인터 공간 만들어줌. 4바이트 할당. sizeof short*은 포인터공간이라 무조건 4바이트!. 앞에 short** 별 두개 붙은거는 pp가 2차포인터라서 맞춰줌
*pp = (short *)malloc(sizeof(short)); //1차 포인터가 가리키는 실제 값 공간 만들어줌. 실제값이 저장되는 공간이라 sizeof 뒤에 별 안붙음. 앞에 short *별 하나 붙는거는 pp가 1차 포인터 별 1개붙은거라 맞춰줌.
**pp = 10;
printf("**pp : %d\n", **pp);
free(*pp); //나중에 만든 것부터 지워야 함!
free(pp);
}
2차원 포인터와 함수의 매개변수
매개변수에 포인터 변수를 잘못 사용한 경우
void GetMyData(int *q){ // 매개변수와 main의 포인터는 둘 다 1차포인터
q = (int *)malloc(8);
}
void main(){
int *p; // GetMyData의 매개변수와 이것 둘 다 1차포인터
GetMyData(p); //주소가 아닌 자기의 값을 넘겨준것.
*p = 5; //오류발생. 할당된 메모리의 첫 4바이트에 값 5를 넣음.
free(p);
}
함수의 매개변수로 2차 포인터 사용하기(위 예제 올바른 방법)
예제) 2차원 포인터로 8바이트 동적 메모리를 할당하는 함수 만들기
void GetMyData(int **q){
*q = (int *)malloc(8);
}
void main(){
int *p;
GetMyData(&p);
*p = 5;
free(p);
}
★★★ p439 필기 그림 참조!!! ★★★
2차원 포인터와 2차원 배열
*(p+1) = p[1] 포인터와 배열은 비슷
여러 개의 1차원 포인터를 정적으로 할당하기
short *p[100]; // short * 형식의 1차원 포인터를 100개 선언
위와 같은 표현은 두 가지 비효율성이 있다. 첫번 째는 배열을 사용했기 때문에 컴파일 할 때 변수 p의 메모리 크기가 400바이트로 고정되어 버린다. 만약 포인터 200개를 사용하도록 변경해야한다면 수정해야하고, 소스코드를 변경했기 때문에 소스 파일을 다시 컴파일해야하는 불편함이 발생한다. 두번 째는 메모리 낭비가 될 수 있다. 실제로 5개만 사용하는 경우 95개의 메모리 공간이 낭비이다.
여러 개의 1차원 포인터를 동적 할당하기
short **pp;
pp = (short **)malloc(sizeof(short *)); //pp = (short **)malloc(4); 와 같은 표현. 뒤에 (short *)로 1차 포인터 공간을 만들어줌. (포인터는 무조건 4바이트)
malloc 함수의 매개변수에 동적으로 할당할 메모리의 크기를 적을 때는 상수 뿐만 아니라 변수를 사용할 수 있다. 따라서 다음과 같이 short* 형식의 1차원 포인터를 n개 할당할 수 있다.
int n;
short **pp;
scanf("%d", &n);
pp = (short **)malloc(sizeof(short *) * n);
이렇게 2차원 포인터와 malloc 함수를 사용하면 포인터의 개수가 바뀌어도 다시 컴파일 할 필요가 없다. 그리고 사용자가 메모리를 사용하고 싶은 크기만큼 선택할 수 있기 때문에 메모리 효율성이 좋다.
예제) 2차원 포인터로 연령별 윗몸 일으키기 횟수 관리하기
#include <malloc.h>
void main(){
unsigned char *p_limit_table; //연령별 인원수를 저장할 포인터. 사용자에게 입력받음
unsigned char **p; //연령별 윗몸 일으키기 횟수를 저장할 2차원 포인터
int age, age_step, member, temp, sum;
printf("20대부터 시작해서 연령층이 몇 개 인가요 : ");
scanf("%d", &age_step);
p_limit_table = (unsigned char *)malloc(age_step);
p = (unsigned char **)malloc(sizeof(unsigned char *) * age_step); //!!1차원 포인터 공간 먼저 확보!!
for(age = 0; age < age_step; age++){ //연령별로 윗몸일으키기 횟수 입력받음
printf("\n %d 0대 연령의 윗몸 일으키기 횟수 \n", age+2);
printf("이 연령대는 몇 명입니까? : ");
scanf("%d", &temp);
*(p_limit_table + age) = (unsigned char)temp;
*(p + age) = (unsigned char *)malloc(*(p_limit_table + age)); //입력받은 인원수만큼 메모리 할당. !!실제 값이 저장될 공간 만들기!!
for(member = 0; member < *(p_limit_table + age); member++){
printf("%d th: ", member + 1);
scanf("%d", &temp);
*(*(p + age) + member) = (unsigned char)temp;
}
}
printf("\n\n 연령별 평균 윗몸일으키기 횟수 \n");
for(age = 0; age <age_step; age++){
sum = 0;
printf("%d 0대 : ", age+2); //20대, 30대, 40대라고 출력함
for(member = 0; member < *(p_limit_table + age); member++){
sum = sum + *(*(p + age) + member); //해당 연령 사람들의 횟수 합산함
}
printf("%5.2f \n", (double)sum / *(p_limit_table + age));
free(*(p_age));
}
free(p);
free(p_limit_table);
}
연습문제 3) char data[2][3]; 과 같은 용도로 사용할 수 있도록 2차원 포인터 변수 p와 malloc 함수를 사용하여 코드를 구성해라.
void main(){
char **p;
int i;
p = (char **)malloc(sizeof(char *) * 2);
for(i = 0; i < 2; i++){
*(p + i) = (char *)malloc(sizeof(char) * 3);
}
for(i = 0; i < 2; i++){
free(*(p + i));
}
free(p)
}
18. 구조체와 연결 리스트
typedef 문법
타입을 정의한다는 의미의 ‘type define’의 줄임 표현이며, 기존의 자료형 중에 자료형 이름의 길이가 긴 경우 프로그래머가 짧고 간결하게 자료형을 재정의하는 문법이다. #define은 치환 작업을 수행하는 전처리기이고, typedef는 기존 자료형을 다른 이름으로 새롭게 정의하는 기능이다.
typedef unsifned short int US;
US temp; //unsigned short int temp;라고 선언한 것과 같음
변수 선언과 착각하지 않도록 새 자료형의 이름은 모두 대문자로 적는 경우가 많다. 모두 대문자는 다른사람이 만든거니 건들지마.
typedef의 장점은 복잡해 보이는 문법을 쉽게 표현할 수 있으며 자료형의 크기를 쉽게 바꿀 수 있다.(유지보수성 ↑, 데이터의 크기에 변화가 생겼을 때 쉽게 대처 가능)
데이터를 그룹으로 묶는 구조체
데이터의 그룹화 1 : 배열. 배열은 크기가 같은 데이터만 그룹으로 묶을 수 있다.
데이터의 그룹화 2 : 구조체
struct People{
char name[12]; //이름, 12바이트. 얘네를 멤버라고 부름
unsigned short int age; //나이 2바이트. 얘네를 멤버라고 부름
float height; //키 4바이트. 얘네를 멤버라고 부름
float weight; //몸무게 4바이트. 얘네를 멤버라고 부름
}
struct와 typedef를 조합해서 사용(그냥 이거알면 될듯)
typedef struct People{
char name[12];
unsigned short int age;
float height;
float weight;
}Person; //첫번째줄 typedef와 맨마지막줄 Person 눈여겨 봐라!
예제) 구조체를 사용해서 사람의 신체 정보를 입력 받고 출력하기
typedef struct People{
char name[12];
unsigned short int age;
float height;
float weight;
}Person;
void main(){
Person data; //person 자료형으로 data 변수 선언. 꼭 해줘야 함!
printf("이름 : ");
scanf("%s", data.name);
printf("나이 : ");
scanf("%hu", &data.age);
printf("키 : ");
scanf("%f", &data.height);
printf("%s : %d세, %.1f cm", data.name, data.age, data.height);
}
구조체 자료형인 Person으로 배열 변수를 선언한 경우에도 각 요소에 접근하는 방식은 같다. 그냥 배열은 . 연산자를 사용
Person friends[3]; //person 데이터 3개를 저장할 수 있는 메모리를 할당
friends[1].age = 22; //두번째 요소의 age에 값 22를 대입
구조체로 선언한 변수를 포인터로 사용하기
포인터 적용시 -> 쓴다.
Person data; //person 자료형으로 data 변수 선언
Person *p; //person 형식으로 선언한 메모리에 접근할 수 있는 포인터 선언
p = &data; //포인터 변수 p는 data 변수의 주소 값을 저장
(*p).age = 23; //p에 저장된 주소에 가서 age 요소에 값 23 대입
p->age = 23; //위와 같은 방식. 위에거 불편하니 이거 씀. ->양옆 공백을 띄우지 않음.
배열과 구조체
skip~~~
struct Test *p1 = (struct Test *)malloc(16); //설정에 따라 오류가 발생할 수 있음
struct Test *p2 = (struct Test *)malloc(sizeof(struct Test)); // 이렇게 사용해야 함! 고정적으로 사이즈 주지 말고 동적이게. 변경되도 대응 가능하게.
구조체를 활용한 연결 리스트
구조체는 C의 꽃~~ 파이널~ 포인터와 배열이 다나옴
구조체를 사용하면(연결리스트, 링크드리스트) -> 효율적으로 저장할 수 있는 데이터 저장 방법. 메모리 관리에 효율적! 배열은 뭉쳐서 크게 써야하지만 링크드는 각자 떨어져도 연결되어있기 때문에 사용 가능.
#include <malloc.h>
typedef struct node{
int number;
struct node *p_next; //다음 노드를 기리킬 포인터
}NODE;
void AddNumber(NODE **pp_head, NODE **pp_tail, int data){
if(NULL != *pp_head){
(*pp_tail)->p_next = (NODE *)malloc(sizeof(NODE));
*pp_tail = (*pp_tail)->p_next; //p_tail(*pp_tail)에 새 노드의 주소 값 저장. 꼬리 새로워짐
}else{
//p_head 값이 NULL이라서 첫 노드가 추가됨. p_head 값에 직접 대입함
*pp_head = (NODE *)malloc(sizeof(NODE));
*pp_tail = *pp_head; //새 노드의 주소 값을 p_tail(*pp_tail)에 저장
}
(*pp_tail)->number = data; //새 노드의 number에 data 값을 저장
(*pp_tail)->p_next = NULL; //다음 노드가 없음을 명시
}
void main(){
NODE *p_head = NULL, *p_tail = NULL, *p;
int sum = 0, temp;
while(1){
printf("숫자를 입력하세요(9999를 누르면 종료) : ");
scanf("%d", &temp);
if(9999 == temp){
break;
}
AddNumber(&p_head, &p_tail, temp);
}
p = p_head;
while(NULL != p){
if(p != p_head){
printf(" + ");
}
printf("%d", p->number); //입력한 숫자 출력
sum = sum + p->number; //입력한 숫자 합산
p = p->p_next; //다음 노드로 이동
}
printf(" = %d \n", sum); //합산 값 출력
while(NULL != p_head){
p = p_head; //현재 노드를 삭제하기 위해 p변수에 노드 주소 값을 저장
p_head = p_head->p_next; //시작 위치를 다음 노드로 옮김
free(p);
}
p_tail = p_head; //반복문을 나오면 p_head 값은 NULL. p_tail값도 NULL로 변함
}
p493 그림 참고…
19.파일 입출력
표준 입출력 라이브러리(깊게 x 간단하게)
표준 입출력 라이브러리란?
표준 입출력 라이브러리는 데이터의 형식에 따라 다른 함수를 제공합니다. 프로그램이 사용하는 데이터 형식은 ‘텍스트(문자열)’와 ‘바이너리(이진)’로 나누어지는데, 자신이 다루는 데이터가 텍스트 형식이면 텍스트 관련 함수를 사용해야 하고 바이너리 형식이면 바이너리 관련 함수를 사용해야 합니다.
텍스트 기반 파일 : html 등 바이너리 : 한글파일, 이미지 파일 등
변수에 저장된 데이터의 크기를 구할 때의 차이점
예를들어 temp 변수에 저장된 데이터의 크기를 구하는 경우에 바이너리 속성은 메모리의 크기를 구해야 변수의 크기를 구할 수 있기 때문에 sizeof 연산자 사용
int data_size = sizeof(temp); //변수 크기를 구함. data_size에는 값 8이 저장됨
반면에 문자열 속성은 temp 변수에 저장된 문자열의 길이를 구하면 변수의 크기를 구할 수 있기 때문에 strlen 함수를 사용하여 문자열 길이를 구한다.
int data_size = strlen(temp); //문자열 길이를 구함. data_size에는 값 3이 저장
변수에 저장된 값을 다른 변수에 복사할 때 차이점
바이너리 모드는 변수에 들어있는 값을 그대로 복사하는 개념을 사용하기 때문에 temp 변수의 값을 dest 변수로 복사하려면 memcpy 함수를 사용. (통째로 복사)
char temp[8] = {'a','b','c',0.};
char dest[8];
memcpy(dest, temp, sizeof(temp)); //temp에서 dest로 8바이트 크기만큼 메모리 복사
반면에 문자열 속성은 temp 변수에 저장되어 있는 문자열만 복사하면 되기 때문에 strcpy 함수 사용(쓸 만큼만)
char temp[8] = {'a','b','c',0.};
char dest[8];
strcpy(dest, temp); //temp에서 dest로 4바이트 크기(NULL 문자 0까지 포함)만큼 복사
파일 열기와 닫기
파일 입출력 함수의 도우미! FILE 구조체
표준 입출력 라이브러리는 ‘FILE 구조체’ 로 포인터 변수를 선언하고 파일 입출력 함수를 호출할 때마다 이 변수를 넘겨주도록 만들어져 있습니다. FILE 구조체는 사용하려는 디스크 상의 파일이 어떤 상태로 사용 중인지에 대한 정보를 담고 있으며, 파일을 좀 더 편하게 사용할 수 있도록 도와준다.(읽고 쓸 때 반드시 필요함)
FILE *p_file;
//파일열기 생략함
fseek(p_file, 0, SEEK_SET);
파일열기 : fopen 함수 (기존에 만들어져있는 함수를 읽어올 때)
파일을 성공적으로 열면 FILE * 형식의 메모리 주소 값을 반환한다. 만약 파일이 존재하지 않거나 파일 형식을 잘못 사용해서 파일 열기에 실패한다면 NULL 값을 반환한다.
FILE *p_file = fopen("tipssoft.dat", "r"); //주소값은 #define or const or 상수로 몰아넣어줘야 한다!!! 무조건!!!
if(p_file != NULL){
//파일 열기에 성공한 경우
}else{
//파일 열기에 실패한 경우
}
주소값은 #define or const or 상수로 몰아넣어줘야 한다!!! 무조건!!! 더 좋은건 파일 이름과 경로를 분리해서 정의하는게 좋다
파일이 다루는 속성에 따라 기본적으로 나누어지는 형식이다. 아래의 두가지 형식은 단독으로 쓸 수는 없고 앞으로 배우게 될 다른 형식들과 함께 써야한다.
| 형식 | 설명 |
|---|---|
| t | 텍스트 속성으로 파일을 사용하겠다는 뜻. 만약 이 형식으로 바이너리 파일을 열면 파일 열기는 성공하겠지만 파일 입출력 함수를 사용하면 오류 발생. |
| b | 바이너리 속성의 파일을 사용한다는 뜻. 이 형식이 기본값. |
파일 내용 읽기 모드 “r”
파일의 내용을 읽기 위한 목적으로 파일을 연다. 만약 파일이 없으면 파일 열기에 실패하고 NULL값을 반환.
FILE *p_file = fopen("tips.dat", "rb"); //rb대신 r만 사용해도 됨. 바이너리 파일을 여는 경우
FILE *p_file = fopen("tips.dat", "rt"); //텍스트 파일 열 때.
파일 데이터 쓰기 모드 “w” (파일을 저장. 파일을 생성해준다고 생각. 덮어쓰기라고 생각)
데이터를 쓰기 위한 목적. 만약 명시한 파일이 작업 경로에 없다면 그 이름으로 파일을 만든 후에 파일을 열기대문에 파일열기에 실패하지 않는다. 하지만 같은 이름을 가진 파일이 이미 존재하는 경우에는 파일을 열면서 그 파일이 가지고 있던 내용을 모두 지우고 시작하기 때문에 주의해야한다.
FILE *p_file = fopen("tips.dat", "wb"); //wb대신 w만 사용해도 됨. 바이너리 파일을 여는 경우
FILE *p_file = fopen("tips.dat", "wt"); //텍스트 파일 열 때.
그러나 디스크에 용량이 부족해서 파일을 만들 수 없거나, CD와 같이 읽기 전용 디스크에 쓰기 모드로 사용하면 파일열기에 실패한다.
파일에 데이터 이어쓰기 모드 “a” (이어쓰기. 추가.)
만약 파일이 작업 경로에 없다면 그 이름으로 파일을 만든 후에(w와 같음) 파일을 연다. 하지만 w와 달리 기존에 파일이 존재하더라도 파일 내용을 지우지 않고 기존 파일 내용에 이어쓰기한다.
FILE *p_file = fopen("tips.dat", "ab"); //ab대신 a만 사용해도 됨. 바이너리 파일을 여는 경우
FILE *p_file = fopen("tips.dat", "at"); //텍스트 파일 열 때.
이 형식도 디스크에 용량이 부족하거나 읽기 전용 디스크에 사용하면 파일 읽기에 실패한다.
파일 사용 형식에서 읽기와 쓰기 같이 사용하기
읽기 강조 “r+”
이 형식으로 파일을 여는 경우 기존 파일이 없으면 파일을 새로 만들지 않고 파일 읽기에 실패. 기존 파일이 있는 경우 해당 파일의 내용을 지우지는 않지만 기존 데이터의 위치로 이동해서 해당 위치의 내용을 덮어 쓸 수 있다. 바이너리 파일에 사용하는 경우 “r+, rb+, r+b” 텍스트 파일에 사용하는 경우 “rt+, r+t”
쓰기 강조 “w+”
파일 여는 경우 기존 파일이 없으면 파일을 열 때 새로만들고, 파일이 이미 존재하면 기존파일의 내용을 모두 지우고 시작. 바이너리 “w+, wb+ w+b” 텍스트 “wt+, w+t”
읽기와 이어쓰기를 같이 사용하기 “a+”
읽기와 이어쓰기 모드 같이 사용하는 경우에 사용하며 확장을 더 강조한다.확장을 더 강조한다는 뜻은 이 형식으로 파일을 여는 경우에 기존 파일이 없으면 파일을 새로 만들고 파일이 존재하면 파일의 내용을 지우지 않고 기존 내용에 이어서 시작한다. 쓰기를 사용하면 현재 위치와 상관 없이 파일의 끝에 내용이 추가된다. r+, w+
읽기+쓰기 -> 메모장에서 내용 확인 후 내용 변경(찾아 바꾸기)
파일 닫기 : fclose 함수
fopen 사용이 끝나면 fclose 함수를 사용하여 파일을 닫아야 한다. (fopen 했으면 fclose 무조건!)
텍스트 파일에 데이터 읽고 쓰기
void main(){
FILE *p_file = fopen("abc.txt", "wt") //쓰기 모드로 텍스트 파일 오픈
if(NULL != p_file){ //파일 열기에 성공한 경우
fprintf(p_file, "HELLO");
fclose(p_file); //파일 닫기
}
}
//해당 파일이 없으면 텍스트파일 자동으로 생성 후 문자열 저장한다.
텍스트 파일에 문자열 저장하기 : fprintf
텍스트 파일에 문자열 읽기 : fscanf
fscanf 함수는 기본적으로 공백문자를 만나면 다음 입력이 시작된 것으로 처리.
예제) fscanf 함수로 문자열 형식의 정수 값 모두 읽어오기
void main(){
int num;
FILE *p_file = fopen("abc.txt", "rt");
if(NULL != p_file){
while(EOF != fscanf(p_file, "%d", &num)){ //텍스트 파일의 끝(End Of File). 따라서 EOF를 반환할 때까지 반복하면서 숫자값을 읽어온다.
printf("%d", num);
}
fclose(p_file);
}
}
텍스트 파일에서 한줄 단위로 문자열 읽기 : fgets
한 줄 단위로 문자열을 처리하고 싶은 경우 사용.
void main(){
char temp[64];
FILE *p_file = fopen("abc.txt", "rt");
if(NULL != p_file){
while(NULL != fgets(temp, sizeof(temp), p_file)){
printf("%s", temp);
}
fclose(p_file);
}
}
바이너리 파일에 데이터 읽고 쓰기
바이너리 파일에 데이터 저장하기 : fwrite
함수 원형 : size_t fwrite(const void *buffer, size_t size, size_t count, FILE *stream); 사용 형식 : fwrite(저장할 데이터의 시작주소, 저장할 데이터의 기준 단위 크기, 반복 횟수, 파일 포인터);
void main(){
int data = 0x0000412;
FILE *p_file = fopen("abc.txt", "wb");
if(NULL != p_file){
fwrite(&data, sizeof(int), 1, p_file);
fclose(p_file);
}
}
반복횟수의 용도
int data[5] = {0,1,2,3,4};
fwrite(data, sizeof(int), 5, p_file); //위아래 같음
fwrite(data, sizeof(int) * 5, 1, p_file); //위아래 같음. 이렇게 사용하는 경우가 더 많음
바이너리 파일에서 데이터 읽기 : fread
p520. 필기 별로 없음 안중요한듯.. 손목아픔
파일 내부의 작업 위치 탐색하고 확인하기 : fseek(중요!), ftell
해당 하무를 사용하여 원하는 위치로 건너 뛸 수 있다.
함수 원형 : int fseek(FILE *stram, long offset, int origin); 사용 형식 : fseek(파일 포인터, 이동거리, 기준 위치);
이 함수는 파일의 데이터를 읽을 기준 위치로 SEEK_SET(파일의 시작), SEEK_END(파일의 끝), SEEK_CUR(현재 위치)를 사용할 수 있고 지정한 기준 위치로부터 사용자가 지정한 이동거리만큼 이동한다. 이동 거리는 양수를 명시하면 지정한 기준 위치에서 뒤로 이동하며 음수를 명시하면 앞으로 이동.
이렇게 이동한 위치를 값으로 확인하고 싶으면 ftell 사용.
예제) fseek 함수와 ftell 함수로 바이너리 파일 크기 알아내기 ->이거는 복잡한 방식이니 이런게 있구나하고 넘겨라.
20. 함수 포인터
int (*p)(int, int); //sum 함수를 가리킬 수 있는 함수 포인터를 선언.(함수 원형을 담은 포인터 선언)
p = &Sum //sum 함수의 주소를 p에 저장
이렇게 함수 포인터를 선언하여 저장했다면 다음과 같이 호출
int result = (*p)(2,3); //int result = Sum(2,3)l 과 같음. 5 저장
콜백 함수 (콜백하려면 함수 포인터 사용해야 함. 개념을 이해!)
타이밍을 알 수 없을 때 미리 요청해두고, 해당 시기가 되면 자동으로 처리 해서 값을 나에게 다시 던져준다?
Comments