4 minute read

🏫Chapters 6 : Technicalities; Functions, etc.

📖Language technicalities

  • 프로그래밍 언어는 모국어가 아닌 외국어
  • 문법상 모호성X : artificial lang이므로 예외가 없음


📖Technicalities

  • 모든 디테일한 문법을 다 외우려 하지 않아도 됨
  • 중요한 개념적 이해와 철학을 알고 있어야 함
  • 나는 자랑스러운 태극기 앞에 자유롭고 정의로운


📖Declarations

  • 컴파일러에게 reserved 키워드를 알려주어야함
  • 선언 : 우리의 프로그램은 user-defined symbol(variable, function)이 섞이므로, 이것을 컴파일러에게 사용전 알려줄 필요가 있음
  • Symbol : 변수명, 타입 + 동일 Symbol에 대한 서로 다른 선언은 문제가 생김 -> 모호성 발생, 컴파일 에러 발생

    • Function : 리턴형, 함수명, 파라미터 타입과 개수
      • 동일 선언이 반복되는 것은 문제 없음
    • C에서는 Function에 대한 동일 선언도 컴파일 에러를 발생시켰으나, C++에서의 다형성 반영에 따라 동일한 함수 이름을 지원한다.


📖Definitions

  • 메모리를 잡는 일

  • Symbol은 메모리에 대한 이름이다

    • Instruction 레벨에서 메모리를 부르는 유일한 이름은 주소 address이다. High-Level Language에서 메모리를 주소로 부르는 일은 매우 생산성이 떨어지는 일이다. 이러한 복잡하고 어려운 주소 대신 symbol을 사용하게 되는데, 이 심볼에 대해 매핑되는 것은 프로그래머 모르게 내부적으로 처리한다.
    • 즉, symbol은 메모리는 부르는 방법. 실제로 가리켜야할 메모리를 잡는 작업.


📖Declarations and Definitions

  • variable : int a;는 선언과 정의가 동시에 이루어지게 되는데, extern이라는 키워드는 이것을 막아준다.
  • function : 정의는 { }가 필수이다. 메모리가 할당된다. 선언은 규칙만 알려주고 메모리를 잡진 않는다.
    • 정의는 동일한 선언에 대한 중복된 정의를 허용하지 않는다. (모호성)


📖Kinds of declaration

  • 컴파일 기반의 언어인 경우에 선언과 정의를 요구한다
  • 파이썬의 경우, 선언을 요구하지 않는다
  • Namespaces : 다형성 반영, 모호성 해결, 동일 이름 사용을 허용하지만 서로 구분짓기 위한 방식
    • default namespace를 사용하고 싶으면 using namespace ___;


📖Header Files and the Preprocessor

  • 함수들의 선언을 묶어놓은 파일
  • 쓸 수는 있게 하지만 정의를 알려주고 싶진 않을 때 이것을 해결할 수 있도록 한다
  • declaration은 high-level에서 공유, definition은 low-level에서 공유. (feat.정의를 공유하는 것을 꺼리던 시절)
  • #include<>로 사용. #은 컴파일러에게 가기 전 처리되는 과정(preprocessor)으로, define이 되어 있는 곳에 미리 해당되는 코드를 복붙해놓는다


📖Source files

  • 우리가 원하는 실행파일은 하나이지만, 실질적으로는 여러개의 .cpp로 나뉘어 실행된다
  • 컴파일러
    • 컴파일러는 한번에 하나의 파일만 알 수 있다
    • 컴파일러는 문법검사가 최우선이기 때문에, 한 파일에서의 모든 symbol과 function을 알아야하므로 해당되는 선언이 모두 되어있어야함
    • 하나의 소스파일에 하나의 오브젝트파일 생성 .cpp -> .obj
    • 결론적으로는 모두 합쳐 단일의 실행파일을 만들어야함.
  • 링커 : 오브젝트 파일을 모두 엮어 하나의 .exe실행파일을 만들어주는 것, 정의를 찾아찾아찾아 연결해주는 역할. symbol의 주소를 연결시켜줌


📖Scope

  • 변수의 스코프
    • 기준 : 보이는지 보이지않는지 / 수명
      • C에서는 전역과 지역의 범위만 있음
      • global : 어디서든 보이고, 프로그램의 수명을 따라가게 됨(프로그램이 실행될때 태어나 종료될때 죽는다)
        • 단, 접근성이 쉬워지기 때문에 안정성 떨어짐
        • 수명이 길기 때문에 메모리 부담이 커짐
        • C++ : 전역변수의 단점에 의해 전역변수를 허용은 하지만, 권장하지 않음
        • JAVA : 문법적으로 아예 허용하지 않음. 대신 Class scope이 존재. 클래스 내부에서만 visibility가 있고, 그 밖에서는 접근 할 수 없는 scope
      • local : function 내부에서만 허용
        • 해당 함수 내부에서만 visibility가 있음. 다른 함수에서 접근 불가
        • 함수의 호출과 함께 메모리가 할당되고 리턴과 함께 메모리가 사라짐
        • 진화되면서 정의가 짧아짐. 초기에는 funcition의 시작에만 변수의 선언이 가능했으나, 현재는 함수의 중간에도 선언이 가능함. 즉, 예전에는 함수의 수명과 지역변수의 수명이 함께 했으나, 현재는 변수가 정의된 곳에서부터 가장 가까이에 있는 중괄호가 끝날 때 까지가 수명이 된다.
      • statement : for loop 때문에 등장
        • for(int i = 0; i < 10; i++); 일 때, i는 언제 죽는가
        • 중괄호에 의해 스코프가 정해지는 것이 아니라 누구에 한해서 일시적으로 유지되는가를 보면 루프에 딸린 하나의 statement에 의해 유지되므로, 해당 statement가 끝나면 사라짐. 원래는 for문 뒤에 하나의 statement만 붙는 것이 허용됐는데, 여러 줄 쓰고 싶을 때 중괄호를 사용하여 한 줄로 인정해줌.

    스코프를 잘게 쪼갬으로써, 우리가 원하는 변수의 안정성을 높이고, 메모리의 효율성을 증대시킬 수 있다


📖Scopes nest

    int x, y;

    int f(){
        int x = 7;
        {
            int x = y;

            ++x;
        }
    }
  • 동일한 스코프에서는 동일 이름의 사용을 허용해주지 않지만, 다른 스코프에서는 모호성을 해결할 수 있다고 판단하여 동일 이름의 사용을 허용한다


📖Functions

Call By Value

  • pros: 안정적이다

  • cons: value의 경우 메모리 접근이 어렵기 때문에, scanf()와 같이 입력 받자마자 값을 바꿔 저장하는 형식이 어렵고, 또한 값을 복사해야하므로 value의 개수가 많아지면 시간이 오래 걸린다

Call By Reference

  • pros: 메모리 접근이 가능하기 때문에 값을 직접적으로 바꿀 수 있다. 또한, 주소에 접근이 가능하여 시간을 아낄 수 있다

  • cons: 안정성이 떨어진다

예제

    #include <iostream>
    #include <vector>

    using namespace std;

    // call by value
    void test1(vector<int> a)
    {
        a.push_back(100);
    }

    // & --> call by ref
    void test2(vector<int> &a)
    {
        a.push_back(100);
    }
    int main()
    {

        vector<int> i;
        vector<int> j;
        test1(i);
        test2(j);

        cout << "i.size(): " << i.size() << endl; // 0
        cout << "j.size(): " << j.size() << endl; // 1
    }
  • 첫번째 함수는 value를 전달한 call by value이므로 main의 i 에는 아무런 영향이 없다.
    그러나 두번째 함수의 경우에는 주소를 전달한 call by ref이므로 main의 j에 영향을 미친다.

    위에서 언급했듯이 만일 전달하는 값의 개수가 1000개로 늘어난다고 한다면, test1()을 호출할 경우 1000개의 데이터를 복사해야하므로 함수 호출 시간이 오래 걸리게 된다.

    test2()의 경우에는 1000개의 데이터를 복사할 필요가 없으므로 빠르게 함수를 호출 할 수 있다. 그러나 값에 직접적으로 접근할 수 있는 것이기 때문에 의도치 않은 값의 변경이 발생할 가능성이 있다. 따라서 안정성의 면에서 call by value가 더 낫다.
    물론, const를 선언하여 값을 변경하지 못하도록 하는 방법도 있다.

    함수를 호출할 때 전달하는 인자가 주소인지 값인지는 알 수 없다. 선언 부분을 확인해야 한다.


에러 찾기

    void f(int a, int &r, const int &cr)
    {
        ++a;
        ++r;
        ++cr; // !ERROR: const사용 
              /*
                const int &a : a가 가리키는 데이터 변경 금지
                int const &a : a가 가리키는 주소 변경 금지
                const int const &a : 다 변경 모대
              */
    }

    // 값을 변경하지 않고 읽기만 한다면 상관 없다
    void g(int a, int &r, const int &cr)
    {
        ++a;
        ++r;
        int x = cr;
        ++x; 
    }

    int main()
    {
        int x = 0;
        int y = 0;
        int z = 0;

        g(x, y, z); // 가능
        g(1, 2, 3); // !ERROR
                    // ref는 값을 변경하고자 하여 &를 붙혔으므로 상수를 전달해줄 수는 없다.
        g(1, y, 3); // OK
                    // 하지만 const &는 상수 전달이 가능하다.
    }


📖Guidance for Passing Variables

  • 규모가 작으면 call by value

  • 규모가 크면 call by reference

  • call by reference는 데이터를 변경하는 목적보다는 값을 리턴하는 경우 사용하는 것이 좋다


📖Namespace

  • 다형성 Polymorphism을 보존하기 위함

  • using namespace NAME 이라고 쓰면 일일이 NAME::Func()식으로 안써도 된다.