[ 함수 포인터 ]

 

함수 포인터란?

"함수의 이름은 메모리상에 존재하는 함수의 위치를 가리키는 포인터" 

 

함수 포인터의 포인터 타입

 

 int func( int a )

 {

     a++;

     return a;

 }

 

위의 코드를 풀어서 보면,

리턴타입이 int형이고 int형 인자 하나를 전달 받는 함수다.

함수 포인터의 포인터 타입을 결정하는것은 리턴형과 전달인자이다.

 

int (*fPtr1)(int);  

위의 선언은 리턴타입이 int형이고 매개변수로 int형 변수 하나를 받는 함수포인터인 것이다. 

 

[코드]


 

위의 코드는 함수포인터의 선언부터 호출까지 보여줍니다.

 

함수포인터를 왜 쓸까? 

 

함수포인터는 운영체제에게 우리가 정의한 함수를 알려 줄 때 유용하게 사용되는 개념입니다.

우리가 정의한 함수의 이름을 운영체제에 전달하면 운영체제는 전달된 함수 이름을 이용해서 우리를 대신해 함수를 호출해줍니다. 이때 해당되는 개념이 콜백함수(CallBack Function)라고도 하죠.

 

뿐만아니라, 리턴타입과 매개변수의 갯수만 맞게 함수포인터를 선언하게되면, 

각각 기능을 하는 함수를 여러개를 정의 하고, 선언한 함수포인터로 필요에 맞게 변경만 시켜주는 swich부분에서도 많이 쓰이곤 합니다.

 

순수 저의 생각과 경험으로 함수포인터를 보자면 ... 

 

일단 예를 들어 봅시다. 글보단 코드를 짜서 설명하는게 아무래도 쉽겠죠;;;



 

위의 코드는 아주 간단하게 함수포인터를 이용해서 구현한 버튼 처리입니다.

이 코드는 함수포인터를 활용해서 코드가 얼마나 간결해지고, 가독성이 높은지 설명하는 소스이므로, 많은 기능은 빠져 있습니다.

 

간략하게 소스를 보자면,

우선 GUI_BUTTON 구조체를 보면, 선택인덱스와 함수포인터(CALLBACK함수)가 있습니다.

그리고 GUI_BUTTON 구조체를 이용해 배열을 하나 만들었습니다. 또한 우리가 버튼을 선택했을때 CALLBACK 되는, 즉! 호출되는 함수도 funcBtn1, funcBtn2로 정의 하였습니다.

 

main() 함수에선 등록되어있는 버튼을 하나하나 화면상에 보여주고, 등록되어있는 버튼중에 인덱스를 입력하게 되면, 함수포인터로가 가르키는 함수를 호출하게 되는 구조입니다.

 

일단 알겠는데,

꼭 저렇게 함수포인터를 사용해서 코딩을 해야하나? 아직까지도 확 FEEL이 오지는 않습니다.

 

하지만,

코드를 자세히보면 우리는 main()을 건드릴(?) 필요가 없습니다.

버튼생성을 하고, 버튼이 선택되었을때 처리되는 함수(ex. funcBtn3)만들어서 연결(?)만 시켜주면, main() 함수에서 이루어지는 로직을 그대로 사용할 수 있는것이죠.

 

main()함수에서는 버튼을 등록시켰으니 화면상에 등록되어진 버튼을 보여줄테고, 그 버튼을 선택하면 우리가 새로정의해놓은 함수가 실행되겠죠.

 

위의 코드는 간단하게 구현되어서 그렇지 프로그램이 커지고 몇만줄의 코딩을 하면서, 모듈화 하기에는 여간 힘든게 아닙니다. 그런데 위의 예제 처럼 함수포인터를 활용하게 되면, 우리는 유지보수나 리펙토링 하기가 쉬워집니다. 가독성도 높죠.

 

아직도 왜 사용하는지 해깔리네요~ 확실한게 없네요( 유지보수, 리펙토링, 가독성 높아진다네 )

추상적으로만 보여지네요. 사실 이부분이 함수포인터는 가장 중요한게 아닙니다. ( 아래 CALLBACK, 동적바인딩부분이 중요함 )

 

버튼을 정의하고 처리하는 함수만 만들면되니 유지보수하기에도 쉬워질테고, 코드가 간결하니 가독성도 높아지고, main()함수에서 버튼을 좀더 빠르게, 호출하기위해 리펙토링을 시작했다고 치면 main()함수에서 함수를 호출하는 부분만 손을 조금 바주면 빠르게 작업이 가능하게되죠.

 

무엇보다 확실한 한방은? main() 함수는 메인 로직만 있고, 추가작업이나 건드리지 않고, 함수포인터 없이 이 프로그램을 구현해 보면, 함수포인터의 필요성을 느끼게 되는거죠~ 이게 가장 확실하긴 합니다. 프로그램을 만드는거야 어떻게 해서든 어렵지는 않게 만들수 있습니다. 문제는 몇백개가 되는 버튼을 관리하고 호출하기에는 코드가 간결해지기 힘듭니다. 명시적으로 잘 보이지도 않죠.

 

정리를 해보자면, 

 

위의 예제프로그램에서는 오히려 우리에게 프로그램이 요청하는 꼴이 되는 것입니다.

우리가 할일은 버튼만 등록해주면 끝이니까

 

이 프로그램은 우리에게

"내가 리턴타입이 void형이고 매개변수가 없는 함수포인터를 만들어놨으니, 프로그래머들은 함수포인터 타입에 맞게 버튼을 정의해서 나한테 입력해줘~ 그럼 내가 그 함수 처리해줄께"

 

가장중요한 함수포인터의 활용 중 CALLBACK에 해당되는 사항이죠~ 흔히들 예기하는 운영체제에 등록하고, 자동으로 운영체제에서 호출되는 함수들은 모두 함수 포인터를 지녔습니다.

 

이 모든건 프로그램 실행 동안에 즉! Runtime 동안에 동적으로 처리되는 사항입니다.

이것을 함수의 동적바인딩(프로그램 실행 도중에 어떤함수를 실행할지 선택)이라고 하죠.

 

결과적으로 함수포인터는 동적바인딩을 가능하게 하기위해서 필요한 것이죠~

 

동적바인딩 자세한 사항은 아래 링크에서 참고

http://blog.naver.com/PostView.nhn?blogId=julyflower50&logNo=60018310034 

 

어느정도 이해가 가셨는지 모르겠지만, 

분명한건 쓰고안쓰고는 프로그래머 몫입니다. 상황에 맞게 대처 해야합니다.

 

 

함수 포인터를 가까이 하는 습관을 기르자.

 

typedef VOID (CALLBACK *PCALLBACKGUIEVENT) ( UINT nEvent, int nControlID );

 

─────────────────────────────────────────────

 

[ void형 포인터 ]

 

void형 포인터란?

변수이건, 함수이건, 포인터주소값이던, 어떠한 뭐든 담을 수 있는 포인터이다.

 

void형 포인터는 주소값을 저장하는 것 이외에 아무것도 할 수 없기때문에, 명시적 형변환 과정을 무조건 거쳐야 한다.

 


 

 

void형 포인터는 언제, 왜쓰는가?

 

void형 포인터는 포인터의 타입이 결정되지 않는 상태에서 주소값을 미리 저장해놓고, 포인터 타입은 나중에 결정할때 사용한다. 주로 동적할당시 많이 사용한다. malloc함수 리턴타입이 (void*)

 

malloc 함수처럼 타입에 상관없이, 공통되는 분모가 생길때 void형 포인터를 선택하거나,

다양한 자료형을 저장할때 등등

'0x0001 > C, C++' 카테고리의 다른 글

[C++] const, static 클래스 멤버  (0) 2019.02.08
[C언어] 메모리 관리와 동적할당  (0) 2019.02.08
[C언어] 다차원 배열, 다중 포인터  (0) 2019.02.07
[C언어] const 키워드  (0) 2019.02.07
[C언어] extern "C"  (0) 2019.02.07

[ 다차원 배열 ]

 

다차원 배열이란 ?

2차원 이상의 배열을 의미하는 것이다. 2차원배열, 3차원배열 등등

 

2차원 배열 선언과 동시에 초기화

int main(void)

{

 //요소 전체 초기화

 int buila[3][3] = { 

                            { 1, 2, 3 },                          1  2  3

        { 4, 5, 6 },            =>           4  5  6

        { 7, 8, 9 }                           7  8  9

     };

 

//일부 요소만 초기화, 이런경우 부족한부분은 0으로 채워진다.

int buila2[3][3] = {

{ 1 },                                1  0  0

{ 4, 6 },              =>           4  6  0

{ 7, 8, 9}                           7  8  9

      };

 

//1차원 배열 초기화 형태를 취하는경우, 초기화 리스트의 수가 배열요소의 수보다 적은경우에는 나머지 요소들은 0으로 초기화 된다.                 

1  2  3

int buila3[3][3] = { 1, 2, 3, 4, 5, 6, 7 };   =>          4  5  6

7  0  0

 

//배열의 크기를 알려주지 않고 초기화하기, 최소한 한개의 인덱스는 가지고 있어야한다. 첫번째 인덱스만 생략 가능하다. 2 x 4 or 4 x 2

 

int buila4[][4]   = { 1, 2, 3, 4, 5, 6, 7,  8 }; 

 

....

}

 

 

2차원 배열 이름의 포인터 타입 

 


 

위의 코드에서 보듯이

 

a[0] == a는 같다,  

a[0] == a[0][0]은 같다. a[0]은 a[0][0]을 가르킨다.

a[1] == a[1][0]은 같다. a[1]은 a[1][0]을 가르킨다.

a[2] == a[2][0]은 같다. a[2]은 a[2][0]을 가르킨다.  

 

 

─────────────────────────────────────────────

2차원 배열 이름을 이용한 포인터 연산  

 


 

위의 코드를 보면

 

a는    a[0][0]을 가르킨다.

a+1는 a[1][0]을 가르킨다.

a+2는 a[2][0]을 가르킨다. 

 

즉!

a    == a[0] 같다. a[0]은 a[0][0]을 가르킨다.

a+1 == a[1] 같다. a[1]은 a[1][0]을 가르킨다.

a+2 == a[2] 같다. a[2]은 a[2][0]을 가르킨다. 

 

"2차원 배열 이름은 포인터 연산 시 행 단위로 이동한다." 

"int a[3][2];  // (4바이트 * 2) 행단위로 차이가 난다 "

"int a[2][3];  // (4바이트 * 3) 행단위로 차이가 난다."

 

자 이제 위의 내용을 정리해보면 배열이름의 포인터 타입이 나온다. 

 

int arr[2][4]; => int (*pArr)[4];  

 

읽어보면,

int형 변수를 가르키고 포인터연산시 4칸씩 이동하는 pArr포인터

 

위에 [4]는 포인터 연산에 따른 증가 혹은 감소의 폭을 말한다.

포인터 연산에 의해 값이 1 증가 및 감소 될경우 4칸씩(행단위 이동이니까) 이동하는 포인터라는 것

pArr이 가리키는 대상이 int형 데이터이므로 (4 * 4) 이동하게 될것이다.

 

 

 

 

─────────────────────────────────────────────

함수 인자로 다차원 배열 전달하기 

 

 

[코드] 



 

[결과]


 

위의 코드는?

int (*ptr)[4] 포인터 타입으로 받을수있는 서로다른 2차원배열의 요소값을 다중for문을 이용해서 출력하는 예제 입니다. 

 

여기서 잠깐!!  

int (*pArr)[4];    //int형 변수를 요소로 지니고 포인터 연산시 4칸씩 이동하는 2차원 배열을

                          가리키는 포인터

int* pArr[4];      //int형 변수의 주소값 4개를 저장할 수 있는 배열

 

 

 

 int* arr1[5];               =>     int** p1;          //1차원 배열이니까 포인터에 포인터선언

 int* arr2[3][5];          =>     int* (*p2)[5];   //2차원 배열이니까 (*p2)선언 및 포인터 연산시 [5]칸

 int** arr3[5];             =>     int*** p3;

 int*** arr4[3][5]        =>     int*** (*pArr)[5];

 

해깔린다. 명백히 해놓자.

  

[ 다중 포인터 ]

 

다중포인터란?

포인터의 포인터 혹은 더블포인터라 불리는 포인터이다.

 

ex ) int ** pP;

 

포인터는 기본적으로 그 대상 변수의 양(사이즈)이 많거나, 함수안에서 사용하는 목적으로 만듭니다.

(어디까지나 나의 생각 기준 입니다.)

 

그럼 다중포인터는 왜 필요해서 쓰는걸까?  

 

예를 들어봅시다.

void malloc_Func(int *x)

{

    x = (int*)malloc(sizeof(int)*100);

}

 

void main()

{

    int *p = NULL;

    malloc_Func(p); //문제점!!

}

 

위의 코드는 함수에서, 호출한 쪽의 포인터에 메모리 할당을 하기 위해서 간단하게 코딩한것입니다.

 

언뜻보기엔 문제가 없어 보이지만, 이 코드에는 문제가 있습니다.

 

간단하게 정리하자면, int* p 와  int * x 는 각각의 포인터 변수이고, 매개변수로 p가 전달됨으로써

x는 p가 가르키는 (NULL)을 똑같이 가르킬 뿐이지, 각각의 포인터라는 뜻입니다.

위의 코드대로 x에 메모리 할당을 해봤자, 각각의 포인터 변수이니까 p는 할당이 안되는것이죠

 

즉 같은 곳을 가르키는 전혀다른 포인터 변수 라는것입니다. 이걸 그냥 복사본이라고 하죠

int *x 는 int * p 하고는 상관이 없는 것입니다.

 

그럼 왜 복사본이 되는것일까?

이유는 간단합니다. 포인터 변수에다가 포인터 변수를 담았으니, 주소값이 대입이 이루어집니다.

변수는 다르지만, 같은것을 가르키게 되는것이죠. Call-By-Value

 

그럼 이문제를 어떻게 해결하면 좋을까요?

우리는 int * p 를 x 가 참조하여 x 를 통해 p를 변경하면 가능합니다.

쉽게 말해 x가 p를 가르키게 만들면, x에다가 할당하면 p가 할당이 되는 것입니다. x가 p의 주소값을 가르키기 때문에 x에다가 동적 할당 하면, 결국 p의 주소에 메모리가 할당 되는것이죠

 

자 그럼 더블 포인터에 의한 Call-By-Reference 바꿔보죠

void malloc_Func(int **x) // main()의 p 변수 자체를 가리키는, "p에 대한 포인터".

{

    *x = (int*)malloc(sizeof(int)*100); // *x가 p와 같음. main()의 p 가 새 메모리를 가리키게 함.

}

void main()

{

     int *p = NULL;

     malloc_Func(p);

}

 

위의 코드에서 더블포인터를 사용해서 메모리 할당을 하였습니다.

왜냐? p 변수 자체를 가르키는 포인터를 정의 하기위해서 더블포인터를 사용한것이죠.

이렇게 되면, 매개변수 x는 p가 가르키는 (NULL)을 가르키게 되는게 아니라,

포인터변수 p를 가르키게 되겠죠.

즉 매개변수 x로 p를 바꿀수있게 되는것입니다.(*x == p) 가 되는것이죠. 

 

그럼 main함수에 있는 포인터 p는 함수에서 할당된 새메모리를 가르키게 되는것입니다.

더블포인터를 사용해서 깔끔하게 수정이 됐습니다. 

 

다중,더블포인트는 쓰이는데는 여러가지가 있습니다.

하지만, 쓰이는곳이 조금씩 틀릴뿐이지, 여기서 크게 의미가 바뀌는 부분은 없습니다.

    

'0x0001 > C, C++' 카테고리의 다른 글

[C언어] 메모리 관리와 동적할당  (0) 2019.02.08
[C언어] 함수 포인터, void형 포인터  (0) 2019.02.07
[C언어] const 키워드  (0) 2019.02.07
[C언어] extern "C"  (0) 2019.02.07
[C언어] 재귀적 함수, 가변인자 함수  (0) 2019.02.07

변수 상수화

 

const는 대개 변수 선언부 앞에 붙여서 수정이 불가능 하게 제한할 목적으로 사용한다. 변수가 상수화되는것이다. 한가지 예를 들어보자.


 

위의 코드를 컴파일 하면 에러를 발생한다. const 키워드가 붙은 변수 pi를 변경하려했기 때문이다. 이처럼 const 변수는 초기화 된 후 변경이 불가능 하며, 초기화는 반드시 선언과 함께 해주어야한다.

 

 

─────────────────────────────────────────────

 

포인터와 const

 

포인터 변수의 경우는 const키워드가 올 수 있는 위치가 두가지가 있다. 한가지는 위와같이 선언부의 맨 앞에 위치하는 거이고, 다른 한가지는 변수명 앞에 위치하는것이다.

 


 

위의 코드를 보면 3가지 선언이 있다.

 

상수화된 int포인터는 값을 바꿀수없다.

int*를 상수화하면 가르키는 위치를 바꿀 수없다.

 

첫번째 ( 상수화한다. int* 가 가르키는 변수 a를 즉! 값은 변경할 수 없다. )

두번째 ( int* 를 상수화 한다. 즉! int* p2 가 지니는 주소값을 변경할 수 없다. )

세번째 (첫번째,두번째의 중첩 즉! 값도, 지니는 주소값도 변경할수 없게된다.)

//error 구문을 자세히 보면된다. 

 

 

포인터가 가리키는 변수의 상수화 후 포인터 변수 대입연산이 가능할까?

 

[코드]

 

결과는 불가능

 

[결과]


 

 

중요한건 왜 사용하냐 인데

 

변경되지 않아야 하는 변수가 있다고 하고, 나는 알고있지만, 다른 사람이 코드수정시 그변수를 조작한다고 본다면 이건 분명 큰 실수 이다. 변수를 조작한다고 해서 프로그램 에러가 나지 않는게 큰 문제다. 값이 변하면 프로그램은 어떻게 될지는 모르는 일이다.

 

예를 들어 원의 넓이 구하는 프로그램이 있다.

원의 넓이는 3.14라는 파이값은 변하지 않는다. 이값은 const 키워드를 통해 상수화 시킨다.

이값이 const키워드 없이 변경이 된다면 원의 넓이 구하는 프로그램은 에러가 안난다고

잘 실행되는것인가?

 

const 키워드는 많은 개발자들이 중요성을 인식 못하지만, 습관화 하는것이 좋다.

적절히 잘 사용해 보자.

 

아래코드는 무엇을 의미할까?

 

 

#ifdef __cplusplus

extern "C" {

#endif

 

현재 코드가 C++ 일 경우 이후 구문들은 extern "C" 선언과 함께 범위(scope)로 지정되는 것이겠죠.

C++은 컴파일시 함수 이름을 모두 다른 이름으로 바꿔주기 때문에 C에서 컴파일된 함수를 링크시킬때는 해당함수의 선언부 혹은 include부분에 extern "C"라는 키워드를 사용하여 해당 함수가 C로 컴파일된 함수라는것을 컴파일러에게 명시적으로 알려야한다.

 

즉! C로 컴파일된 함수를 C++에서 부를 수 없기에 이를 가능하게 하는 기법이다

 

 

//CFile.c

 

 

//CppFile.cpp

 

 

[ 컴파일 & 실행 ]


 

[ 재귀적 함수 ]

 

재귀함수란 “자기 자신을 재참조하는 함수”이기 때문에 본인이 본인을 호출하는 구조로 되어있다.

 

한번 보자

 

[코드]

 

 

[결과]


 

 

남들은 보니 팩토리얼을 예제로 많이 사용했네요

팩토리얼은 그냥 공식만

 

n!=n×(n-1)!의 성질에서 n이 1일 때 1=1×0! 이 되므로 0!=1로 약속한다. 

 

 int factorial( int num )
{
if ( num <= 1 )  
return 1; 

else

{        // 재귀 호출-> num이 5라면 5, 4, 3, 2, 1들어가서 곱해지면서 리턴 1* 2* 3* 4* 5

return num * factorial( num - 1 );

}

}

 

 

 재귀함수는 무한루프에 빠지지 않도록 주의를 기울여야한다.

간단 코드니 여기서 무한루프 빠져도 금방 눈에 보이지만, 프로젝트가 커지면 디버깅도 힘들고

스트레스로 인해 머리털이 빠지는 현상이 일어 날지 모른다.

 

생각하며 코딩하자

 

 

[ 가변 인자 함수 ]

 

printf() 함수를 보면 신기한것이 있습니다.

 

함수원형은 stdio.h 파일에 포함되어있습니다. 자 그럼 함수 원형을 보죠.

 _Check_return_opt_ _CRTIMP int __cdecl printf(_In_z_ _Printf_format_string_ const char * _Format, ...);

 

printf( const char * _Format, ...);

문자열을 받는건 알겠는데, ... 이건 멀까요?

 

"..."  

 

이것이 가변인자(Variable argument function)을 나타내는 것이다

 

자 그럼 우리도 함수 하나 만들어 봅시다.

 

[코드]

 

 

[결과]


 

간단하게 구현해봤다.

우선 몇개의 매개변수를 전달할지의 int cnt를 두고 가변인자를 둬서

 

printf( 3, a, b, c ); //4byte 씩 차례대로 할당이 되므로

 

포인터 연산하여 매개변수 갯수에 맞게 출력하게 끔 구현되었다. 

 

 

 

─────────────────────────────────────────────

이제 좀 함수들을 이용해서

va_list, va_start(), va_end(), vsprintf() 매크로 함수를 써서 printf 함수를 만들어 보자.

 

[코드]

 

 

간단하게 함수 설명

 

va_start() 함수를 통해 가변인자 포인터변수 ap를 _vFormat_, 다음에 오는 매개변수 위치에 이동시켜고, vsprintf(); 문자열을 조립하여 pszlog에 저장합니다. va_end(); ap를 해제합니다.

 

 

[결과]


 

가변인자의 첫번째 매개변수는 전달될 매개변수의 갯수와 타입를 정하는데 사용한다. 

ex. "%d. Hello World %s\n " int, char* 총 2개가 전달된다.

 

 

그다지 어려운 부분은 없다. MSDN 에 깊은 설명이 있으니 참고

 

가변인자는 왜 쓰고, 언제 쓸까?

"인자의 갯수가 정해지지않았을때, 인자의 자료형이 정해지지 않았을때"

...

[ 문자열 함수 ]

 

스트림에 대한 이해 

 

- 연속된 데이터의 열(Line)을 의미한다.

- 입력하는 데이터나 출력하는 데이터를 입출력 순서에 의해서 순차적으로 처리되는 데이터 열(Line)

- 데이터를 이동 시킬수 있는 다리 역할을 한다.

 

이름

스트림의 종류

입 출력 장치

stdin

표준 입력 스트림

키보드

stdout

표준 출력 스트림

모니터 

stderr

표준 에러 스트림

모니터

  

 

─────────────────────────────────────────────

문자 단위 입출력 함수 

 

1) 문자 출력함수

 

 - 하나의 문자를 출력할 때 일반적으로 사용하는 함수

int putchar( int c );

int fputc( int c, FiLE* stream); /* 스트림 지정 가능   

 * 모니터 뿐만아니라 파일에도 문자를 출력할 

 * 수 있는 함수다. 

 */ 

2) 문자 입력함수

 

 - 하나의 문자를 입력할 때 일반적으로 사용되는 함수

int getchar();

int fgetc( FiLE* stream );          /* 스트림 지정 가능

                                                    * 키보드 뿐만 아니라 파일로부터도 데이터를

                                          * 입력 받을 수 있다.

                                          */ 

 

 


 

* fgetc 나 getchar() 함수가 파일의 끝에 도달하는 경우에도 EOF(-1)가 반환 된다.

* getchar() 함수는 표준 입력 함수지만, Ctrl + Z 키입력시 파일의 끝이라고 정의

 

 

─────────────────────────────────────────────

문자열 단위 입출력 함수

1) 문자열 출력함수

int puts( const char* s );                          // 자동 줄바꿈처리

int fputs( const char* s, FILE* stream ); 

 

 

2) 문자열 입력 함수

char* gets(char* s);                                // 잠재적인 문제, 쓰지말자

char* fgets(char* s, int n, FiLE* stream ); 

 


 

"fgets함수는 입력 받을 수 있는 최대 문자열의 길이 n을 초과하는 문자열이 입력되는 경우에는 n-1개까지의 문자만 입력을 받고, 마지막에 NULL문자를 삽입 해준다."

 

 

─────────────────────────────────────────────

표준 입출력 버퍼 

 

 - 표준 입 출력 함수를 사용하는 경우에는 버퍼(여분의 임시메모리 공간)을 사용하게 된다

 - 바로바로 처리하는것보다, 어느정도 쌓아 두었다가 처리를 하는것이 효율적

 

 

버퍼를 지우는 작업을 하는 fflush 함수 

 

int fflush( FILE* stream);   

 

"fflush함수를 입력스트림에 사용할경우 입력 버퍼 안의 데이터들은 모두 버려지게됨"

"fflush함수를 출력스트림에 사용할경우 출력 버퍼 안의 데이터들은 모두 즉시 출력"

 

언제 쓸까?

예를 들어 5자리를 입력받아야 하는데, 계속입력해서 10자리가 입력되었다. 이걸 fgets함수에서 읽어드릴 사이즈를 5자리로 정해놓았다면, 버퍼에는 나머지 5자리가 남아있다. 이렇게 된다면, 다시 입력을 받을때 입력버퍼에 남아있는 5자리가 밀려서 들어간다. 이때 잔챙이(?) 남아있는 버퍼를 지우고 다시 입력을 받아야 하므로 fflush로 버퍼를 비우는게 맞다.

 

─────────────────────────────────────────────

문자열 조작 함수

 

 

 

 

 

 

 

 

 

 

 

 

 

 

[ 배열 ]

 

배열의 선언 

 

 int array[10];

 

 배열 요소 자료형 int

 배열 이름 array

 배열 길이 10 

 

 

배열은 선언과 동시에 초기화  

 int arr1[5] = {1, 2, 3, 4, 5};    // 1,2,3,4,5

 int arr2[ ] =  {1, 3, 5, 7, 9};   // 1,3,5,7,9

 int arr3[5] = {1, 2};              // 1,2,0,0,0

 int arr4[5] = {0, };        // 0,0,0,0,0 실무에서 많이들 배열 초기화할때 사용한다.

 

 char str1[5] = "Good";         // G o o d \0  모든 문자열 끝에는 NULL문자가 자동 삽입

 char str2[]  = "morning";     // m o r n i n g \0

 

위의 선언에서 중요한건 문자열이다

 

예를 들어

char str[10] = "Hello";

이렇게 코드를 입력하면, 자동으로 \0 널문자가 삽입되지만,

 

10개의 char 형 변수를 잡아논 상태에서 초기화가 이루어지지 않으면,

 

char str[10];

[0] = 'H';

[1] = 'e';

[2] = 'l';

[3] = 'l';

[4] = 'o';

 

나머지 [5] ~ [9] 까진 쓰레기 값이 들어간다.

결국 printf 함수는 문자열의 끝인 NULL문자가 없으므로 어디가 마지막 문자인지 알수가 없어진다.

그래서 쓰레기문자까지 출력하게 된다.

 

해결할려면?

#include <string.h>

 

int main()

{

      char str[10];

      memset( str, 0, sizeof(char) * 10 );   //초기화

      return 0;

}

 

문자열 배열을 memset() 함수를 통해 초기화를 하던가, 아니면 문자열입력후 \0문자를 직접 넣어주면 된다. 아무래도 초기화가 더 편하겠죠. 문자열을 다룰때는 좀 더 신경쓰자.

 

 

여기서 정적 배열의 길이선언은 무조건 상수로 한다. 

왜 상수로 해야할까? 

 

힙영역의 필요성과도 결합되는데, 예를 한번 들어보자 

 

 void func( void )

{

int i = 10;

int array[i]; // error

}

 

위의 코드를 그냥 봐서는 문제가 없을꺼같다. 하지만, 컴파일러 입장에서는 에러를 내뿝는다.

왜냐면, 컴파일러는 변수의 대입된 값을 런-타임(프로그램이 실행되는 동안)에 결정하자 하고 넘어가 버린다. 그래서 int array[i]; 문장을 만나면, 컴파일러는 i값이 먼지 모르니 에러가 나는것이다.

 

즉 변수 i의 값이 결정되는 시기는 '컴파일-타임'이 아니라 '런-타임' 이다.

 

조금만 생각해보면, 왜 힙영역이 필요한지 바로 나온다.

나는 프로그램 실행도중 즉 런-타임동안 메모리를 할당하고 싶은데, 이때 유용하게 사용되는 메모리공간이 바로 힙(Heap) 영역이다.

 

 

[ 포인터 ]

 

 

 "포인터란 메모리의 주소 값을 저장하기 위한 변수이다."  

 

이 말이 가장 맞는말 같다.

 

포인터의 선언 및 연산자

 


 

 

여기서 한가지더!  

포인터는 반드시 선언과 동시에 초기화 하는것이 좋다. 초기화를 하지 않으면 쓰레기값이 들어가 어디를 포인터변수가 가르킬지 모르니 항상 선언과 동시에 초기화 해놓는 습관을 길러야 한다.

int main(void)

{

int * pA;    //초기화 안됐으니, 대박 쓰레기값

*pA = 10;  //error

return 0;

 

 

 

─────────────────────────────────────────────

포인터에 다양한 타입이 있는 이유

 

포인터는 주소값을 저장하는 4바이트로 구성되어있다. 하지만 위의 코드처럼 다양한 포인터형이 있는건 이유가 있다. 포인터는 주소값을 가르키고 타입에 따라 몇바이트를 읽어 들어야 하는지를 알아낸다

 

즉!

포인터의 타입은 메모리를 참조하는 방법을 알려주는 역할을 한다. 

 

 

 

[ 포인터와 배열 ]

 

"배열 이름도 포인터, 단 그값을 바꿀 수 없는 상수라는 점이 일반적인 포인터와의 유일한 차이점이다." 


 

"배열의 이름은 배열의 첫번째 요소의 주소 값을 나타낸다."  

왜? 위의 코드에서 보면 배열 a의 이름값이 배열 &a[0] 주소값과 같기 때문에

  

포인터 연산 

 


 

포인터 연산은 바이트 단위이고, 바이트수는 포인터 자료형에 따라서 달라진다. 

 

여기서! 중요한 결론 

 

arr[i] == *(arr+i)  => 즉 arr[3] == *(arr+3) 과 같다.

풀어서 읽어보면, 배열 arr의 [3]요소는 arr을 가르키는 포인터변수에 즉 arr[0] 에 +3 포인터연산으로 이동된 주소에서 배열 arr의 [3]요소의 참조값과 같다. 값은 위의 코드에서 보면 4다.

 

 

─────────────────────────────────────────────

문자열 상수를 가르키는 포인터 

 

char *str1 = "abcd"; 

char * str2 = "ABCD";  // 문자열 상수 선언하고 바로 char* str2 가 가르키고 있다.

 

이게 가능한이유는?

"문자열 상수는 메모리 공간에 저장이 되면, 그 순간에 문자열 상수의 주소값이 반환된다."  

 

즉 ABCD의 문자열 첫번째 문자('A')의 주소를 str2가 가르키게 된다.

 

 

 

─────────────────────────────────────────────

배열 요소로 포인터를 지니는 포인터 배열 

 

포인터 배열이란?

"메모리 주소 값을 저장할 수 있는, 즉 포인터를 요소로 지니는 배열"  

 

[코드] 


 

[결과] 


 

하나만 읽어보자

 

printf("%d \n"', *(*(arr+1)));

배열 arr[0] 요소에서 +1 포인터연산 4byte이동되고 arr[1] (*)참조값이니 &b의 주소값이 되고, (&b)의 (*)참조값이니 20의 값을 출력하게 된다.

 

 

[ 함수 인자로 배열 전달하기 ]

 

입력한 두값을 바꿔주는 swap() 함수를 만들어 보면서 Call-By-Value와 Call-By-Reference를 충분히 이해해 보자

 

 

함수 호출방식

 

1. Call-By-Value ( 값의 복사 )

 

void swap( int a, int b )

{

    int temp = a;

    a = b;

    b = temp;

 

    printf("a : %d \n", a);

    printf("b : %d \n", b);

}

 

 

코드는 간단하다. 임시변수 temp를 통해 a를 저장하고 b를 a에 옮기고 a의 값을 임시로 저장했던 temp를 다시 b로 복사해주는것이다. 바로 printf() 호출도 하고있다.

 

이함수에서 매개변수로 넘어오는 int a, int b 값은 복사되어 넘어온다.

즉! 매개변수로 입력했던 값은 이함수안에서 a와 b를 수정한다해서 바뀌지 않는다는 말.

 

 

2. Call-By-Reference ( 참조의 의한 호출 )

 

 

void swap( int* a, int* b )  // void swap( int a[], int b[] )

{

   int temp = *a;

  *a = *b;

  *b = temp;

}

 

 

참조의 의한 호출

 

즉!

매개변수를 포인터로 받아, 매개변수로 전달된 변수도 swap함수에서 변경하면 바뀌게 된다는점이

가장큰 Call-By-Value 랑은 차이점이다.

 

"인자로 전달된 주소가 가리키는 변수의 조작을 함수 내에서 가능하게 하는것!"

+ Recent posts