연산자는 변수와 상수 데이터를 조합해서 새로운 데이터를 창출한다.
연산자의 종류 :
+, -, * , / , %= , += , -= , *=, /=, %=-a , +a++a, a++ , --a , a-- ( 후위, 전위연산자에 따라 결과가 달라진다. )== , != , > , < , >= , <=! , &&, ||&, |, ^, ~대표적인 조건문 if
if문의 기본 원리 :
if(수행 조건) {
참일 때 동작
}
수행 조건에 관계 연산자나 Boolean 값을 이용한다.
수행조건이 참이면, { }블록 내로 이동하여 동작을 수행하고, 거짓이면 if구문을 건너뛴다.
혹은, 수행 조건 이외의 조건에서 특정 동작을 수행하고 싶다면, else를 이용한다.
if(수행 조건) {
참일 때 동작
} else {
거짓일 때 동작
}
마지막으로, 수행 조건을 여러 개 설정하고 싶다면 else if를 사용한다.
if(수행 조건1) {
조건1이 참일 때 동작
} else if (수행 조건2){
조건2가 참일 때 동작
} else {
위 조건들이 모두 거짓일 때 동작
}
while(반복 조건)
{
반복 문장
}
반복 조건에 관계 연산자나 Boolean 값을 이용한다.
반복 조건을 만족하는 동안 반복 문장을 수행하고,
반복 조건을 만족하지 않으면 while문을 빠져나간다.
이중 while문
while(조건1)
{
반복 문장1
while(조건2)
{
반복 문장2
}
}
예제) 이중 while문으로 구구단 만들기
#include <stdio.h>
int main() {
int num1 = 2;
while ( num1 < 10 ) {
int num2 = 1 ;
while (num2 <10){
printf("%d * %d = %d \n",num1,num2, num1*num2);
num2++;
}
printf("-------------\n");
num1++;
}
return 0;
}
for(초기문; 조건문; 증감문)
{
반복 동작
}
무한 루프란, 반복문에서 조건문이 계속해서 참인 상태이기 때문에, 코드의 반복 수행이 무한히 일어나는 동작이다.
ex)
while(true){
printf("무한 루프")
}
무한루프는 유용하게 사용할 수 있다.
예를 들어, 윈도우와 같은 운영체제는 계속해서 무한루프를 수행하면서 입력과 같은 이벤트가 들어올 때마다 처리를 해준다. 또한, 시스템이나 애플리케이션을 종료하게 되면 무한루프를 벗어나게 된다.
즉, 무한루프를 특정 시점에 빠져나올 수 있다면 문제가 없이 사용할 수 있다. 이 때 사용하는 것이, break와 return이다.
반복문 내부에서는 break를 사용할 수 있다.
반복문 내부를 순회하다가, break문을 만나게 되면 반복문을 빠져나오게 된다.
int a = 0 ;
while(1){
printf("%d\n", a);
a++;
if(a == 10){
break;
}
}
마찬가지로, 반복문에서 continue를 사용할 수 있다.
break와 다르게 continue를 만나게 되면 다음 코드들은 수행하지 않고, 그 즉시 반복문으로 다시 돌아가서 다시 반복문을 수행한다.
int a = 0 ;
while(a<10){
a++;
if(a > 5 && a < 8){
continue;
}
printf("%d\n" , a);
}
return 0;
// 1 2 3 4 5 8 9 10
수학에서의 함수는 y=f(x)의 형태를 가진다.
f라는 함수에, x라는 입력값을 넣으면, y라는 결과값을 얻어낸다.
마찬가지로, 프로그래밍에서의 함수 또한 비슷한 형태를 가진다.
함수는 다음의 장점을 가진다.
프로그래밍에서의 함수는 2가지로 나뉘어진다.
1. 표준 함수 : 언어에서 기본적으로 제공해주는 함수. ex) printf()
2. 사용자 정의 함수 : 사용자가 원하는 기능을 직접 함수로 만들 수 있다.
함수의 기본 형태
// C
자료형 함수이름(자료형 매개변수)
{
함수의 내용
}
// JavaScript
function 함수이름(매개변수)
{
함수의 내용
}
C언어를 사용하다 보면, 종종 void라는 타입을 볼 수 있다.
결론부터 말하자면, void는 결과값을 리턴하지 않는 타입이다. 예시를 보자.
void myPrint()
{
printf("출력");
}
void 타입으로 선언된 함수에 return "값"을 입력하면,
warning: 'return' with a value, in function returning void 라는 경고가 발생하고,
이 함수를 이용해서 변수에 할당하려고 하면 error: void value not ignored as it ought to be 라는 에러가 발생 한다.
( 단순히 return 만을 사용하는 것은, 함수를 벗어나는 것이므로 사용할 수 있다. )
void myAdd(int a, int b)
{
return a + b ;
} // 경고
int main() {
int a = myAdd(1,2);
return 0;
} // 오류
변수는 크게 3가지로 분류된다.
1. 지역변수 : 지역변수는 해당 지역에서만 유효한 변수이다.
2. 전역변수 : 함수 바깥쪽에 선언된 변수로, 전역변수는 모든 지역에서 유효한 변수이다.
3. static 변수 : 생성은 함수 안에서 하지만, 함수가 종료되도 소멸하지 않는 변수이다.
1. 지역변수 예시)
void func1(){
int a = 10;
func2();
}
void func2(){
int a = 20 ;
}
이 예시에서, a라는 이름의 변수가 각 함수마다 존재한다.
이 코드는 충돌하지 않는다. 지역변수이기 때문에, 각각의 구역에서 해당 변수는 독립적이다.
또한, 스택 메모리에 데이터가 쌓이는 순서는 func1의 변수a가 추가된 후, func2의 변수a가 추가된다. 소멸은 이와 반대로, func2 - func1의 순서로 소멸된다.
추가로, 매개변수도 지역변수이다.
2. 전역변수 예시)
#include <stdio.h>
int global;
int main() {
global = 0 ;
func1();
func2();
printf("%d" , global); // 2
return 0;
}
void func1(){
global++;
}
void func2(){
global++;
}
global이라는 변수는 전역변수로, 다른 함수에서 global의 값을 사용할 수 있고, 정상적으로 연산되는 것을 확인될 수 있다.
또한, 전역변수는 데이터 영역이라는 메모리 공간에서 존재하며 프로그램이 종료될 때 메모리에서 소멸한다.
전역변수의 단점으로는, 전역변수가 많아질 수록 코드에 대한 가독성이 떨어지고, 이해가 어려워질 수 있으므로 반드시 필요한 경우에만 사용하도록 한다.
3. static변수 예시)
static변수는 지역변수처럼 중괄호 영역에서 선언되지만,
중괄호를 벗어나도 메모리 상에서 고정되어 소멸하지 않는다. static변수도 데이터 영역 메모리 공간에서 존재하며, 프로그램이 종료될 때 소멸된다.
사용법은 간단하다. 함수 내의 변수에 static이라는 키워드를 붙여주면 된다.
void func(){
static int value = 0;
value++;
printf("%d\n" ,value);
}
int main() {
int i = 0;
while(i<5){
func();
i++;
}
return 0;
}
// 1, 2, 3, 4, 5 출력
단, 위 코드에서 value라는 변수를 main()함수에서는 사용할 수없다.
즉, static 변수는 지역변수처럼 함수 내에서만 독립적으로 작동하지만, 전역변수처럼 계속해서 메모리에 값을 고정시켜 가지고 있다.
배열이란, 같은 속성을 가진 것들을 나열해 놓은 것이다.
형태 : 자료형 배열명[길이]
int a[100] ;
int b[5] = {1,2,3,4,5};
위의 코드에서 a배열은 int(4Byte) * 100 으로 총 400Byte의 메모리 공간을 차지한다.
배열에 접근하려면 배열명[인덱스]의 형태로 접근한다.
다른 방식으로도 배열의 초기화를 수행할 수 있다.
int c[] = {1,3,5,7,9};
이 경우에는, 배열에 들어갈 데이터의 개수를 세지 않아도, 컴파일러가 알아서 배열의 길이를 계산한다.
즉, 배열은 주로 위의 형태로 많이 사용한다.
배열의 복사는, 같은 타입끼리, 요소끼리 복사가 가능하다. 즉, 아래 코드에서 arr2[0] = arr1[0], arr2[1] = arr1[1] ... 로 복사를 수행해야한다.
int arr1[5] = {1,2,3,4,5};
int arr2[5] ;
// arr2 = arr1 ; // (x) 오류발생
for (int i = 0 ; i < 5 ; i++){
arr2[i] = arr1[i];
}
문자열("Hello World")은 모두 상수이다.
문자열에 이름을 붙여주면 변수로 사용 가능하다.
char str[12] = "Hello World";
문자열의 끝에는 null문자가 추가되어야 하기 때문에, 글자수가 11개이지만, 12개의 길이로 배열을 생성했다.
null 문자는 왜 필요한가?
다음 코드를 생각해보자.
char str[100] = "Hello World";
문자수는 11개인데에 비해, 배열의 길이는 100개이므로 89개의 공간이 빈다. 이 89개의 공간에는 쓰레기 값으로 채워져있다.
따라서, 이 문자열의 끝이 어디이며, 쓰레기값이 어디서부터인지 파악하기 위해 null을 이용하여 표시한다.
그렇다고, 비어있는 null공간을 다른 변수가 사용할 수 있는 것이 아니므로 위 코드에서는 여전히 100Byte의 메모리 공간을 차지한다.
컴퓨터는, 메모리 주소를 통해 메모리에 접근하여 값을 읽고 사용한다.
포인터는 포인터 변수의 줄임말로, 메모리의 주소값을 저장하고 있는 변수이다.
포인터는 32비트 운영체제에서 int,float,char 등 모든 포인터의 크기가 4Byte로 동일하고,
64비트 운영체제에서는 8Byte로 동일하다.
이 포인터 변수는 각각 변수들이 차지한 메모리의 주소 중 가장 첫번째의 주소를 가리킨다.
ex)
char a = "A" ; // 1Byte (0x01) - 포인터 값
int b = 100 ; // 4Byte (0x02) 0x03 0x04 0x05
double c = 3.14 // 8Byte (0x06) 0x07 0x08 0x09 0x10 0x11 0x12 0x13
포인터 변수 선언 시 변수명 앞에 * 기호를 붙여준다.
char *pA;
int *pB;
double *pC;
포인터 변수의 초기화 시, 변수의 앞에 & 기호를 붙여준다.
int b = 100;
int *pB = &b;
printf("%d", *pB); // 100
printf("%p", &b); // 0x10...
printf("%p", pB) ; // 0x10... // 포인터
printf("%x", pB) ; // 0x10... // 16진수
위 코드에서 pB는 변수b의 주소값 을 가지고 있고,
*pB는 변수b의 주소값에 들어있는 값에 접근할 수 있다.
즉, *는 메모리 주소에 들어있는 값(역참조)을 의미하고,
&는 메모리 주소(참조) 를 의미한다고 생각하면 될듯하다.
혼동하지 말아야 할 것이,
int *pb ; 의 *와
printf("%d", *pB); 의 *는 같은 의미가 아니라는 것이다.**
int *pb는 이 변수가 포인터 변수라는 선언적 의미의 *이다.
따라서 int* pb 형식으로도 사용할 수 있지만, 이 경우는 포인터라는 변수명이 명확하지 않기 때문에 int *pb 방식으로 사용한다.
printf("%d", *pB); 의 *는 해당 주소값의 메모리에 들어있는 값을 의미한다.
배열의 이름은, 해당 배열의 첫 번째 요소의 주소값을 갖는다.
arr : 0x12ff4c
arr[0] : 0x12ff4c
arr[1] : 0x12ff50
arr[2] : 0x12ff54
arr[3] : 0x12ff58
arr[4] : 0x12ff5c
즉, 배열의 이름은 첫 번째 요소의 포인터라는 의미이다.
따라서 arr와 &arr[0] 의 값은 같다.
arr라는 배열의 이름은, 포인터이면서 상수인 포인터 상수이기 때문에, 다음처럼 변경하려고 하면 오류가 발생한다.
int temp = 10 ;
arr = temp ; // 오류
상수를 바꾼다는 것은 다음과 같은 의미와도 같다.
5 = 10 ;
즉, 배열에 다른 변수를 할당하는 것은 불가능하다.
함수의 매개변수의 전달 형태는 2가지가 존재한다.
int main(void){
int a = 10 ;
Temp(a);
}
void Temp(int b){
}
이 때 Temp의 매개변수 b는 main의 a의 값을 공유하는게 아니라, 값을 복사해서 사용한다.
즉, 메모리에서 총 a(4byte) + b(4byte)로 8byte를 사용한다. 위처럼 값을 복사하는 것을 CallbyValue 라고 한다.
반대로, 매개변수로 주소값을 복사해서 사용하는 것을 CallbyReference라고 한다.
배열은 포인터 상수라고 했다.
따라서, 배열을 함수의 매개변수로 전달하는 방법은 * 기호를 이용하여 값을 불러와야 한다.
void Temp(char *arr) { // arr는 포인터 변수
arr[0] = 'A';
}
int main() {
char arr[] = "Hello World";
Temp(arr);
printf("%s", arr);
return 0;
}
// Aello World
Temp의 *arr은 arr 주소로부터 해당 메모리의 값을 가리키고 있다.
즉, Temp 함수 내에서 arr의 요소의 값을 변경한다면, arr의 값이 변경된다.
이 방법은, 전체 배열을 복사해오는 것이 아니라, 배열의 주소값만 복사(CallbyReference)를 수행하기 때문에 메모리 절감에서 상당히 유리하다.
배열의 요소에는 다음과 같이도 접근할 수 있다.
int main() {
char arr[] = "Hello World";
printf("%c",*(arr + 1));
return 0;
}
코드를 살펴보자.
1. 배열 arr을 만들었다.
2. arr은 "포인터"이다. arr + 1을 통해 다음 요소의 주소를 얻는다.
3. 역참조 *(arr + 1)를 통해 해당 주소에 해당하는 값을 얻는다.
즉, arr는 "H"의 주소를 가리키고 있었는데,
arr+1 은 "e"의 주소를 가리키며,
이를 값으로 얻기 위해 역참조(*)를 수행하여 "e"라는 문자를 출력했다.