2015.04.10 22:08


함수포인터


포인터가 무엇인지는 다들 아실텐데요, 특정 변수에 대한 메모리 주소를 담을 수 있는 변수를 포인터 변수라고 합니다. 그렇다면 함수포인터란, 특정 함수에 대한 메모리 주소를 담을 수 있는 것 이라고 정의할 수 있겠습니다.


함수포인터를 쓰는 이유는 무엇일까요?

  1. 프로그램 코드가 간결해집니다.

  2. 함수포인터를 배열에 담아서도 사용할 수 있으므로 중복되는 코드를 줄일 수 있습니다.

  3. 상황에 따라 해당되는 함수를 호출할 수 있으므로 굉장히 유용합니다.

그 외에도 함수 포인터를 이용하여 콜백함수를 구현할 수 있게 되는 등 편리하고 유용한 코드를 작성할 수 있게 됩니다.



우선 함수포인터의 모양에 대해 알아보도록 하겠습니다.

int (*FuncPtr) (intint)


함수포인터는 위와 같은 모양을 띕니다. 함수의 프로토타입 선언과 모양이 비슷하죠?

함수의 프로토타입과 다른점이 있다면 함수 이름앞에 포인터를 가르키는 *이 붙는 다는 것인데요. 이렇게 선언이 되게 되면 FuncPtr 이라는 함수의 주소를 담을수 있는 '변수'가 생기는 것입니다. 

이 FuncPtr 함수포인터가 담을 수 있는 함수는 위와 같은 모양을 띄어야 합니다. 즉, 함수의 리턴형은 int 여야하고, int형 파라미터 2개를 받는 함수여야 하는 것입니다.


예를 들어,

1:  int add (int first, int second)

2: double div (double first, double second)

위의 보이는 두 함수가 있다고 가정할 때, 함수포인터의 선언 모양과 똑같이 생긴 add 라는 함수의 주소만을 담을 수 있는 것입니다. 


아래의 사용 예제를 한 번 더 보시겠습니다.

1
2
3
4
FuncPtr = add    (o)
FuncPtr = &add   (o)
FuncPtr = div    (x)
FuncPtr = add()  (x)


1, 2 :  2가지 방법 모두 괜찮은 사용 방법입니다. 어떤 것을 쓰셔도 무관합니다.
: div는 FuncPtr의 선언 모양과 프로토타입이 달라서 사용할 수 없습니다. 에러가 발생합니다.
4 : add() 처럼 함수를 호출할 때 처럼 쓰는 것은 결과값이 함수의 호출 이후 리턴 값이 되는 것입니다. 따라서 add() 는 int형을 가리키게 되는 것이므로 사용방법 자체가 잘 못 되었습니다. 에러가 발생합니다.



이렇듯 함수포인터는 담고 싶은 함수의 프로토타입을 따라 선언하여 사용하시면 됩니다. 하지만, 이 모양이 복잡하기 때문에 typedef를 이용하여 타입의 모양을 단순화 시키는 작업을 해 줄수도 있습니다.

typedef int (*FuncPtr)(intint)


프로그램 상단에 위와 같이 선언한 후, 실제 사용을 하실 때에는 FuncPtr 이라는 Type으로 새로운 변수를 사용하실 수 있습니다.


1
2
FuncPtr testFP = NULL;
testFP = add;


이렇게 말이죠. 아, 참고로 모든 변수 특히 포인터 변수를 선언해 주실 때, 초기화 해주는 습관은 정말 좋은 습관이십니다 :) 크리티컬 에러를 미리 예방할 수 있는 방법 중 하나입니다.



자 이제 마지막으로, 함수포인터를 이용해서 만든 실제 예제를 한 번 보여드리도록 하겠습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include <stdio.h>
 
// 함수포인터 타입 정의
typedef int (*calcFuncPtr)(int, int);
 
 
// 덧셈 함수
int plus (int first, int second)
{
    return first + second;
}
// 뺄셈 함수
int minus (int first, int second)
{
    return first - second;
}
// 곱셈 함수
int multiple (int first, int second)
{
    return first * second;
}
// 나눗셈 함수
int division (int first, int second)
{
    return first / second;
}
 
// 매개변수로 함수포인터를 갖는 calculator 함수
int calculator (int first, int second, calcFuncPtr func)
{
    return func (first, second);     // 함수포인터의 사용
}
 
int main(int argc, char** argv)
{
    calcFuncPtr calc = NULL;
    int a = 0, b = 0;
    char op = 0;
    int result = 0;
     
    scanf ("%d %c %d", &a, &op, &b);
     
    switch (op)    // 함수포인터 calc에 op에 맞는 함수들의 주소를 담음
    {
        case '+' :
            calc = plus;
            break;
 
        case '-':
            calc = minus;
            break;
 
        case '*':
            calc = multiple;
            break;
 
        case '/':
            calc = division;
            break;
    }
     
    result = calculator (a, b, calc);
 
    printf ("result : %d", result);
     
    return 0;
}


실행결과는 다음과 같습니다.


간단한 소스코드라 어려운 점은 없을 겁니다. 혹시 코드에 이해 안가는 부분이 있다면 댓글 남겨주시면 바로 답변 드립니다.



신고


Posted by injunech
2015.02.24 11:01




1. 연산자 (Operator)


451


우선순위별로 보면 다음과 같다
1. 소괄호 () : 수학에서 괄호 속을 먼저 연산하는 것을 떠올리면 된다.

2. 단항연산자 : 1개의 항에 대하여 연산을 해준다.
 2-1) 논리연산자 ! : 피연산자 a의 값이 거짓(0)이면 참(1)을, 그 외의 수는 거짓(0)을 반환한다. 사용법 [ !a ]
 2-2) 비트연산자 ~ : 피연산자 a를 bitwise(비트 반전) 시킨다. 즉, 1100 -> 0011. 사용법 [ ~a ]
 2-3) 증감연산자 ++, -- : 피연산자의 앞에 오냐(전위증가), 뒤에 오냐(후위증가)에 따라 의미가 약간 달라진다.
    ++a (전위증가) 의 경우 a를 1 증가시키고 나머지 연산을 시행하는 반면
    a++ (후위증가) 의 경우 연산을 모두 마친 후, 최종적으로 a를 증가시킨다.

    ※ 후위증가가 전위증가보다는 우선순위가 높다. 사용법 [ ++a, --a, a++, a-- ]
 2-4) sizeof(변수, 자료형) : 입력받은 변수 혹은 자료형의 크기를 byte 단위로 반환한다. 사용법 [ sizeof(int), sizeof(a) ]
 2-5) casting연산자 (자료형) : 명시적으로 자료형을 변환해준다. 낮은 곳에서 높은 곳으로 형변환(casting)이 될 때는 데이터 손실이 일어나지 않지만, 높은 곳에서 낮은 곳으로 형변환이 될 때에는 데이터 손실이 일어난다.
   (double)3 : 3이라는 숫자는 정수형이지만, double로 형변환 하여 3.0과 동일한 가치를 지닌다. 상위형변환 이므로 데이터 손실이 일어나지 않는다.
   (int)3.5 : 3.5라는 숫자는 실수형이지만, integer로 형변환 하여 3이라는 가치를 지닌다. 하위형변환 이므로 데이터 손실이 일어난다.

3. 산술연산자 : 뺄셈, 곱셈, 나머지연산 - 덧셈, 뺄셈 순으로 수학과 동일하다.
 a+b, a-b, a*b, a/b : 각 연산에 대한 결과 반환. 단, 5/2의 경우 2.5가 아니라 2가 된다. (int / int = int가 되는데 2.5는 double형이므로 하위형변환이 묵시적으로 일어남)
 a%b : a를 b로 나눈 나머지 값을 반환한다. 즉, 123%10 = 3이다.

4. 비트이동 연산자 <<, >> : a의 비트를 <<(좌측), >>(우측)으로 b만큼 비트를 이동한다. 밀려나는 값들은 사라지고, 새로운 자리에는 0이 들어온다.
 a << 2 : a의 비트가 00001110 이면 00111000이 된다.

 ※ 비트를 왼쪽으로 한 번 이동하는 것은 2를 곱하는 효과, 오른쪽으로 한 번 이동하는 것은 2로 나누는 효과를 가져온다.

5. 관계연산자 : 다음 식이 참이면 1을 반환, 거짓이면 0을 반환한다.
 a<b, a>b, a<=b, a>=b, a==b (같다 라는 표현은 =을 두개 사용하여 나타낸다), a!=b (a와 b가 다르다)

6. 비트논리연산자 : and, or, xor의 논리값을 반환한다.
 a&b : a와 b가 둘 다 참(1)일 때만 1을 반환한다. 그 외에는 0
 a|b : a와 b가 둘 다 거짓(0)일 때만 0을 반환한다. 그 외에는 1
 a^b : a와 b가 서로 다를 때 1을 반환한다. 그 외에는 0

 n bit에 대하여 n 비트 전부를 개별로 검사한다. 그리고 그 값을 개별적으로 반환하여 합하여 계산한다. 
즉, 11000000 & 01000001 = 01000000으로 반환한다. 이는 특정 비트를 검사할 때 매우 유용하다. 특정 비트가 검사된다면 왼쪽 예와 같이 0이 아닌 값을 반환하기 때문이다. 가령 예를 들어 하위 두번 째 비트가 1인지 아닌지 여부를 검사한다고 가정하자. 그러면
& 00000010을 주면 하위 두번 째 비트가 1일 경우에만 0이 아닌 수가 반환된다.

11111101 & 00000010 = 00000000 (거짓값 : 하위 두번 째 비트가 1로 세팅되어 있지 않다)
11100011 & 00000010 = 00000010 (참 값 : 하위 두번 째 비트가 1로 세팅되어 있다)
11000000 & 00000010 = 00000000 (거짓값 : 하위 두번 째 비트가 1로 세팅되어 있지 않다)

7. 논리연산자 : and, or의 논리값을 반환한다. 이 때에는 피연산자 자체가 0이 아닌 경우 참, 0인 경우 거짓을 나타낸다.
 a&&b : a와 b가 둘다 참일 때만 1을 반환한다. 11111101 && 00000010의 경우 둘 다 0이 아닌 수라 참의 값을 지니므로 결과는 참이 된다. 주로 관계연산자와 같이 사용된다. (a>=b) && (a>100) 와 같이 사용한다.
 a||b : a와 b가 둘다 거짓일 때만 0을 반환한다.

8. 조건연산자 : a? b: c 의 꼴이며, a가 참이면 b, 거짓이면 c를 반환한다.
 int max = (a>b)? a: b; 의 형태로 사용된다 // a가 b보다 크면 a, 작으면 b가 반환되서 max라는 변수에 할당이 된다.

9. 할당 및 복합 할당연산자 (오른쪽에서 왼쪽으로)
 a = b :  위에서 a와 b가 같다라는 연산자는 ==를 사용했는지 알 수 있을 것이다. b의 값을 a에 할당한다.
 a += b : a = a+b 와 동일한 의미를 지닌다. 즉, 현재 a값에 b값을 더해 다시 a에 할당하라는 의미.
 a -= b : a = a-b 와 동일
 a *= b : a = a*b 와 동일
 a /= b : a = a/b 와 동일
 ...

10. 콤마 연산자 : 성격이 동일한 자료형을 나열할 때 쓰인다.
 int myInt, yourInt, hisInt, herInt, .... ;



이 모든 것을 어떻게 외우느냐? 하다보면 된다. 진심이다(...)




2. 상수(Constant)


리터럴(literal)이라고 한다. 자기 표현 자체가 값이 되는 것을 의미한다. 가령 예를 들면, int myInt = 1; 에서 1과 같은 것이다.
여기에는 여러 가지 종류가 있는데, 대표적으로는 다음과 같다.

접두어 : 특별한 선언이 없으면 10진수이다. 0x로 시작하면 16진수, 0으로 시작하면 8진수로 인식된다.
접미어 : 특별한 선언이 없으면 정수 / double형 실수로 인식한다. 즉, float myFloat = 1.234; 와 같은 경우, 우변의 1.234가 double형으로 인식되기에 하위형변환이 일어나 데이터가 손실 될 수 있다는 경고메세지가 뜰 것이다. (안뜨는 컴파일러도 있다.)
이럴 때, 명시적으로 float myFloat = 1.234f 혹은 1.234F 와 같이 float형을 명시해주면 경고메세지가 사라진다.

접미어로 L, l(이건 대문자 i와 상당히 유사하기에 보통 쓰이지 않는다)을 명시하면 long 타입의 정수가 된다. 
long int myLongInt = 1L;


혹은 const로 선언하는 변수는 심볼릭 상수(Symbolic Constant)가 된다. 이 경우, 이 변수는 더 이상 값을 수정할 수 없고, 초기값으로 지정되는 값을 상수로 가진다. 즉, 초기화 하지 않으면 쓰레기로 초기화가 되고, 값을 변경할 수 없다.

변수를 선언하는 형식 앞에 const를 추가한다. const double PI = 3.141592;



마지막 방법으로는 #define이라는 매크로를 이용하는 방법이 있다.
#define PI 3.1415 와 같이 선언한다. ( #include <stdio.h> 가 있는 위치에 선언한다 ). 컴파일러에 의해 타입은 자동으로 정해지므로 특별히 신경쓸 일은 없다. 보통 #define MAX 100 와 같은 용도로 선언하고, 프로그램을 수정할 일이 있으면 맨 윗줄의 #define문만 수정하면 되므로 매크로를 사용하지 않았을 때 보다 더욱 간결하다.


예를 들어, 다음과 같은 프로그램이 있다고 가정한다. (의미는 생각하지 않아도 된다)

#include <stdio.h> #include <stdio.h>
#define MAX 100

void main(){ void main(){
int i, j, arr[MAX][MAX]; int i, j, arr[100][100];
for(i=0; i<MAX; i++){ for(i=0; i<100; i++){
for(j=0; j<MAX; j++){ for(j=0; j<100; j++){
printf("%d", arr[i][j]); printf("%d", arr[i][j]);
} }
printf("\n"); printf("\n");
} }
} }


여기서 크기를 100에서 200으로 바꾸고 싶다고 할 때, 왼쪽의 코드는 #define MAX 200으로만 바꾸어 주면 되지만, 오른쪽의 코드는 100이 들어간 부분을 전부 다 찾아서 고쳐야 한다. 코드가 간결할 때는 별 문제가 없지만, 수 백, 수천줄의 코드를 모두 바꾸기란 여간 귀찮은 일이 아니며, 행여 하나라도 수정을 못하면 예상치 못한 결과가 나올 수 있다. 

순수하게 치환되는 방식을 취하므로 다음과 같이 임의로 자신만의 상태(statement)들을 만들 수 있다.

#define A_NONE 1000
#define A_KOR 1001
#define A_JPN 1002
#define A_CNA 1003
...


...

int myNation = A_KOR;
if( myNation == A_JPN ) myNation = A_KOR;


물론 이 경우에 한해서는 #define 대신에 int A_NONE = 1000; 으로 선언하고 똑같이 작성해도 상관없지만, 위에도 말했다시피 #define으로 선언된 것의 자료형은 컴파일러가 알아서 결정해주고, 변수선언으로는 불가능한 작업도 #define은 가능하게 해준다.





신고


Posted by injunech
2015.02.12 11:25


비주얼 스튜디오 2013에서는 컴파일러가 강화되었는지, 예전 2010버전에서는 Warning 정도로 처리했던 unsafe 함수들에 대해 에러로 처리하여 컴파일이 되지 않습니다.


예를 들면 scanf, strcpy 같은 함수들을 사용하면 아래와 같은 오류창을 띄우며 컴파일에 실패합니다.


"error C4996: 'scanf': This function or variable may be unsafe. Consider using scanf_s instead. To disable deprecation, use _CRT_SECURE_NO_WARNINGS. See online help for details."


scanf 는 안전하지 않으니, scanf_s를 사용하거나 _CRT_SECURE_NO_WARNINGS을 사용하라는 내용인데, 저 혼자 테스트 하는 프로젝트라서 _CRT_SECURE_NO_WARNINGS을 추가했습니다.



추가하는 방법은 [프로젝트] 메뉴에 제일 밑에 있는 메뉴를 보면 [(프로젝트명) 속성] 이라는 메뉴가 있습니다.



속성을 클릭한 다음에 아래와 같이 C/C++ 전처리기 탭에 들어갑니다.

뜨는 항목중, 전처리기 정의에 오른쪽에 보이는 화살표를 클릭하면 나타나는 <편집...> 을 눌러줍니다.



정의 내용안에 

_CRT_SECURE_NO_WARNINGS

를 입력하고 확인 눌러줍니다.



전처리기 정의가 끝난 이후에는 scanf나 strcpy를 자유롭게 쓰셔도 에러처리 안하고 컴파일이 잘 됩니다.


위와 같은 방법도 있고, 소스코드의 제일 위에 

1
#define _CRT_SECURE_NO_WARNINGS

을 선언해 주어도 됩니다. 제일 윗 줄에 선언하셔야합니다. (적어도 해당 함수를 담고 있는 헤더 윗줄에)


하지만 소스코드 자체에 이런 "임시 에러 우회방법"을 사용하는것이 마음에 안들어 저는 프로젝트에 추가시키는 편입니다. 사실은 버퍼오버플로우 등의 프로그램 취약성때문에 사용하면 안 되는 함수이기 때문이죠. 하지말라는건 하지 않는게 좋은겁니다 :)

신고


Posted by injunech

티스토리 툴바