35. TypeScript 학습 전 배경지식 (2)

JSLEE·2024년 6월 13일

연산자 :

연산자는 변수와 상수 데이터를 조합해서 새로운 데이터를 창출한다.

연산자의 종류 :

  • 산술 연산자 : +, -, * , / , %
  • 대입 연산자 : = , += , -= , *=, /=, %=
  • 부호 연산자 : -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문

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문

for(초기문; 조건문; 증감문)
{
  반복 동작
}

무한 루프

무한 루프란, 반복문에서 조건문이 계속해서 참인 상태이기 때문에, 코드의 반복 수행이 무한히 일어나는 동작이다.

ex)

while(true){
  printf("무한 루프")
}

무한루프는 유용하게 사용할 수 있다.
예를 들어, 윈도우와 같은 운영체제는 계속해서 무한루프를 수행하면서 입력과 같은 이벤트가 들어올 때마다 처리를 해준다. 또한, 시스템이나 애플리케이션을 종료하게 되면 무한루프를 벗어나게 된다.
즉, 무한루프를 특정 시점에 빠져나올 수 있다면 문제가 없이 사용할 수 있다. 이 때 사용하는 것이, breakreturn이다.

break / continue

반복문 내부에서는 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 함수이름(매개변수)
{
  함수의 내용
}

void란?

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가지가 존재한다.

  1. 값에 의한 복사 (CallbyValue)
  2. 참조에 의한 복사 (CallbyReference)
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"라는 문자를 출력했다.

profile
공부한 내용들을 정리하기 위해 사용하는 블로그입니다.

0개의 댓글