My Profile Photo

DongChanS's blog


수학과 학생의 개발일지


파이썬 유저를 위한 C++ (6) - 함수

9) 함수

9-1. 함수 사용 절차

함수 원형 선언 => 함수 정의 => 함수 호출

int sum(int left, int right); // 원형 선언

int main(){
    std::cout << sum(3, 4); // 함수 호출
}

int sum(int left, int right){
    return left + right;
} // 함수 정의

이 경우에는, 컴파일러는 main함수를 sum함수보다 먼저 컴파일하기 때문에 sum함수가 있다는 사실 (함수 원형 선언)을 알려줘야합니다.

그런데.. 사실 이렇게만 해도 작동은 됩니다.

int sum(int left, int right){
    return left + right;
} // 함수 정의

int main(){
    std::cout << sum(3, 4); // 함수 호출
}

9-2. 참조자 (reference)

특정 변수의 실제 이름 대신 참조자(&)를 통해서 변수를 사용할 수 있음.

  • 여기서 쓰인 &는 포인터에서 배웠던 주소 연산과는 다릅니다 (오버로딩)
  • 특별한 개념은 아니고, 그냥 특정 메모리 공간을 여러개의 이름(별명)으로 부르고 싶을 때 사용하는 것입니다.

ex) 두 변수의 값 공유

int x = 10; // 변수의 선언
int &y = x; // 참조자 선언

int main(){
    y++;
    std::cout << x << std::endl; // 11
    x++;
    std::cout << y << std::endl; // 12
}

y를 증가시켜도 x가 증가하고, x를 증가시켜도 y가 증가되는 모습입니다.

참조자는 일반 변수와 별 다를게 없어 보이지만, 함수를 호출할 때는 조금 다릅니다.

9-2. 참조자를 이용한 함수 호출

위에서 배운 참조자를 이용하여 함수를 호출하면 조금 특별한 결과를 얻을 수 있습니다.

int square_by_value(int a){
    std::cout << &a << std::endl; // 0x7fff2bf1fccc
    return a * a;
}
int square_by_ref(int &a){
    std::cout << &a << std::endl; // 0x7fff2bf1fcec
    return a * a;
}
int square_by_pointer(int* pta){
    std::cout << pta << std::endl; // 0x7fff2bf1fcec
    return (*pta) * (*pta);
}
int main(){
    int a = 3;
    std::cout << &a << std::endl; // 0x7fff2bf1fcec
    square_by_value(a);
    square_by_ref(a);
    square_by_pointer(&a);
}
  • call by value

    a 변수를 복사한 새로운 임시변수를 생성하여 함수에 전달

    => 원본 데이터 변화 불가능.

  • call by reference : & 연산자 이용

    a 변수를 그대로 함수에 전달 => 원본 데이터 변화 가능.

  • call by pointer:

    이전에 배운 포인터를 이용해서 함수를 호출할 수도 있습니다.

    • 사실 call by value의 일종이긴 합니다.
    • 하지만, 주소값은 보존되므로 포인터 역연산(*)을 이용하여 원본의 값에 접근할 수 있음.

9-3. *와 &를 헷갈리지 말기

int main(){
    // 1. 일반적인 포인터변수의 선언
    int num = 12;
    int* ptr = &num;
    int** dptr = &ptr;
    
    // 2. 참조자를 이용한 포인터변수의 별칭 선언
    int &ref = num;
    int* &pref = ptr;
    int** &dpref = dptr;
    
    // 3. * 포인터 연산자를 이용하여 주소 역연산
    std::cout << ref << std::endl; // 12
    std::cout << *pref << std::endl; // 12
    std::cout << **dpref << std::endl; // 12
}

구분해야할 것은 4가지입니다.

  1. *
    • 변수 선언시 사용 : 포인터 변수임을 명시
    • 변수에 사용 : 포인터 변수에 저장된 주소에 저장된 변수값 반환.
  2. &
    • 변수 선언시 사용 : 참조자 변수임을 명시
    • 변수에 사용 : 변수의 주소값을 반환

9-4. 디폴트 인수

파이썬에서는 그냥 맘대로 함수의 디폴트값을 지정할 수 있었는데.. C++에서는 몇개의 제약이 있습니다.

  1. 함수 원형에서만 디폴트인수를 지정가능
  2. 가장 오른쪽에서 순서대로 지정해야함.
void Display(int x, int y, char ch = 'a', int z = 4);

Display(1, 2); // ok
Display(1, 2, 'c'); // ok
Display(5, 6, 'c', 9); // ok
Display(5, , 'c', 9); // 에러!

9-5. 함수 오버로딩(Overloading)

함수 오버로딩은, 같은 이름의 함수여러가지 매개변수 형태로 사용할 수 있는 기능입니다.

void Display(int x, int y);

void Display(int x, int y, char ch);

void Display(int x, int y, char ch = 'a', int z = 4);

파이썬에는 함수 오버로딩을 지원하지 않지만, 자바같은 경우에는 메서드 오버로딩이 있습니다.

이렇게 함수 오버로딩이 가능한 이유는 함수 시그니쳐라는 개념 때문입니다.

  • 함수의 원형에 명시되는 매개변수 리스트
  • 이 매개변수 리스트에 따라서 함수를 구분함.

!!주의사항!!

함수 오버로딩은 모호한 호출을 허용하지 않습니다.

  • 모호한 호출 : 여러가지 원형이 모두 적용가능한 경우
int main(){
    Display(2, 3, 'a');
}

에러메시지

prog.cc: In function 'int main()':
prog.cc:8:22: error: call of overloaded 'Display(int, int, char)' is ambiguous
    8 |     Display(2, 3, 'a');
      |                      ^
prog.cc:3:6: note: candidate: 'void Display(int, int, char)'
    3 | void Display(int x, int y, char ch);
      |      ^~~~~~~
prog.cc:5:6: note: candidate: 'void Display(int, int, char, int)'
    5 | void Display(int x, int y, char ch = 'a', int z = 4);
      |      ^~~~~~~

9-6. 변수의 유효범위

  • 자동 변수 (automatic variable) : 파이썬의 지역변수랑 같은 개념

    • (프로세스의 stack부분에 저장됨)
  • 레지스터 변수 (register variable) : CPU의 레지스터에 저장되어 빠르게 접근할 수 있는 변수

    • 레지스터(processor register)

      컴퓨터의 CPU가 빠르게 접근가능한 미니 저장소

    • register 키워드를 통해서 선언이 가능함.
  • 정적 변수 (static variable) : 프로그램이 실행되는 내내 유지되는 변수,

    • (프로세스의 data부분에 저장됨)

    • 자동으로 0으로 초기화됨.

    1. static 키워드

      int main(){
          for (int i=0; i<)
      }
           
      void printnum(){
          static int count = 1;
          std::cout << count << std::endl;
          count ++;
       }
      
      • 한 번 선언된 static 변수는 선언문을 만나도 재선언되지 않음. * 이 경우에는 외부 블록에서 count에 접근이 불가능하지만, 블록 바깥에서 static변수를 선언하게 되면 외부 블록에서 모두 접근이 가능함.
    2. 외부 파일에서도 사용할 수 있는 변수

      • 외부 파일을 include하고, extern 키워드를 통해서 변수를 선언하면 외부 파일의 변수를 가져올 수 있습니다.
      // 'exFile.cpp'
           
      #include <iostream>
      int var = 3;
      
      #include <iostream>
      #include "eXFile.cpp"
      extern int var;
      

Bonus) C에서의 함수 포인터

함수의 주소를 가리키는 포인터인 함수포인터에 대해서 간략하게 적으려 했는데..

이 개념이 간략하게 정리할 수 있는 개념이 아니라서 따로 섹션을 작성하게 되었습니다.

B-1. 정의

  • 메모리 내에서 실행 가능 한 코드를 가리킴.
  • 함수 포인터가 가리키는 함수(코드)를 역참조(*)를 통해서 작동시킬 수 있음.

B-2. 문법

  • 기본적인 방법
함수의 type (*함수포인터명) (매개변수들);
  • 예시
int sum(int, int); // 원본 함수

int (*funcPtr) (int, int); // 원본 함수의 형태에 맞는 함수 포인터 선언

B-3. 문법 해부하기

int (*funcPtr) (int, int)

  • 두개의 정수를 매개변수로 받고, 정수를 반환하는 어떤 함수의 포인터

    이런 조건만 만족하면 어떤 함수든 가리킬 수(can point) 있음.

  • (*funcPtr)에서 괄호는 반드시 필요함!

    • 괄호를 안씌우고 int *funcPtr(int, int)라고 선언한다면,

      int* 자료형을 반환하는 funcPtr 함수라는 뜻이 되기 때문입니다.

    • 같은 이유로, 상수 포인터를 만드려면

      (*const funcPtr)이라고 해야합니다. const를 바깥에 빼면 const int 자료형을 반환하는것으로 해석하니까요.

B-4. 사용 예시

ex) 함수를 간접적으로 호출가능

int sum(int, int); 
int (*funcPtr) (int, int);

int main(){
    int a = 10, b = 15;
    funcPtr = sum; // sum 함수를 가리킴
    std::cout << funcPtr(a, b); // 대신 계산이 가능함.
}

int sum(int a, int b){
	return a + b;	
}

B-5. 함수 포인터의 장점

  • 여러 함수를 for문으로 사용가능합니다.

참고링크 : https://blankspace-dev.tistory.com/241

int (*funcPtrArray[4]) (int, int) = {sum, sub, mul, divide}; 
// 4개의 함수를 가리키는 배열

int main(){
    int a = 10, b = 15;
    for (int (*func)(int, int) : funcPtrArray){
        std::cout << func(a, b) << std::endl;
    }
}
  • 다른 함수의 인자로 함수를 전달할 수 있습니다.

EX) 선택정렬 - 정렬의 기준(key)를 상황에 맞게 전달하기.

int key1(int num){
    return num;
}
int key2(int num){
    return -num;
}
int key3(int num){
    return num * num;
}

void selectionSort(int *arrRef, int (*key)(int)){
    int length = 10;
    int nextIdx, temp;
    
    for (int i=0; i<length; i++){
        nextIdx = i;
        for(int j=i+1; j<length; j++){
            if ( key(*(arrRef+nextIdx)) - key(*(arrRef+j)) > 0 ){
                nextIdx = j;
            }
        }
        temp = *(arrRef+nextIdx);
        *(arrRef+nextIdx) = *(arrRef+i);
        *(arrRef+i) = temp;
       
        std::cout << temp << " ";
    }
    std::cout << std::endl;
}

int main(){
    int arr[10] = {1,-2,3,-4,5,-6,7,-8,9,-10};
    selectionSort(arr, key1);
    selectionSort(arr, key2);
    selectionSort(arr, key3);
}

출력결과

-10 -8 -6 -4 -2 1 3 5 7 9 
9 7 5 3 1 -2 -4 -6 -8 -10 
1 -2 3 -4 5 -6 7 -8 9 -10 

B-6. C언어 함수 포인터의 모호함

참고링크 : https://blog.naver.com/jdk7613/100020321205

void func(){
    std::cout << "Good!";
};

int main(){
    void (*funcPtr)();
    
    funcPtr = func;  // ok
    funcPtr = &func; // ok
    
    funcPtr(); // ok
    (*funcPtr)(); // ok
}

첫번째 방식만 소개했었는데, 사실 뒤의 방식대로 해도 문제가 없습니다.

생각해보면 뭔가 이상한거죠 ㅡㅡㅋ

그래서 C++에서는 멤버함수포인터라는 새로운 개념이 있다고 하네요..

일단 그건 클래스를 배우고 나서 나중에 소개하겠습니다.

comments powered by Disqus