728x90


우선 잘못된 방법 중 하나로 malloc()라서 간과하고 일반적인 malloc()로 2차원 배열을 선언하는 방식은 접근 에러가 뜨게 된다.

그 원인이 한번 선언된 순간 해당 메모리 주소 정보는 메인에서 관리되지 않는다고 한다. 즉 Host에서 익숙하게 malloc() 사용하듯이 cudaMalloc()을 사용하는 순간 Host(CPU 이하 Host)에 등록된 주소는 잃게되고 Device(GPU 이하 Device)의 주소로 넘어가게 되는데 이때 Host에서는 접근이 불가하게 된다.


근데 Host에서 명령하는 cudaMalloc()을 사용하게 되면 접근이 불가능한데 d_array의 주소를 Host가 접근을 하지만 이미 접근권한을 잃은 Host는 d_array에 대해 nullptr 혹은 잘못된 값을 받아오게 될 것이다. 잘못된 주소를 Device에게 메모리 할당을 요청하기 때문에 에러가 발생하게 된다.



마찬가지로 할당된 주소를 달아주는 순간 Host는 그  권한을 잃게된다...


해결방안1. (반만 해결됨)

1차원 배열로 선언하되 2차원 배열로 이용한다. 뭔소리인가 하면, x, y인 2d 데이터에 접근할 때 data[y * width + x]의 형태로 접근하는 것이다.

쉽게 말해서 2 X 5 행렬을 사용한다면 10칸을 할당해서 아래와 같이 논리적으로 생각하고 쓴다는 것이다.




해결방안2. (추천 방법)

Example:

// kernel which copies data from d_array to destinationArray
__global__ void CopyData(float* d_array, 
                                   float* destinationArray, 
                                   size_t pitch, 
                                   int columnCount, 
                                   int rowCount)
{
  for (int row = 0; row < rowCount; row++) 
  {
     // update the pointer to point to the beginning of the next row
     float* rowData = (float*)(((char*)d_array) + (row * pitch));
        
    for (int column = 0; column < columnCount; column++) 
    {
      rowData[column] = 123.0; // make every value in the array 123.0
      destinationArray[(row*columnCount) + column] = rowData[column];
    }
  }
}


int main(int argc, char** argv) 
{	
  int columnCount = 15; 
  int rowCount = 10;
  float* d_array; // the device array which memory will be allocated to
  float* d_destinationArray; // the device array
  
  // allocate memory on the host
  float* h_array = new float[columnCount*rowCount];

  // the pitch value assigned by cudaMallocPitch
  // (which ensures correct data structure alignment)
  size_t pitch; 
  
  //allocated the device memory for source array
  cudaMallocPitch(&d_array, &pitch, columnCount * sizeof(float), rowCount);
  
  //allocate the device memory for destination array
  cudaMalloc(&d_destinationArray,columnCount*rowCount*sizeof(float));
  
  //call the kernel which copies values from d_array to d_destinationArray
  CopyData<<<100, 512>>>(d_array, d_destinationArray, pitch, columnCount, rowCount);

  //copy the data back to the host memory
  cudaMemcpy(h_array,
                    d_destinationArray,
                    columnCount*rowCount*sizeof(float),
                    cudaMemcpyDeviceToHost);

  //print out the values (all the values are 123.0)
  for(int i = 0 ; i < rowCount ; i++)
  {
    for(int j = 0 ; j < columnCount ; j++)
    {
      cout << "h_array[" << (i*columnCount) + j << "]=" << h_array[(i*columnCount) + j] << endl;
    }
  }
}


참고 사이트 : http://www.stevenmarkford.com/allocating-2d-arrays-in-cuda/

출처 : http://k1321a.blog.me/220357402521

728x90

'Parallel Programming > CUDA' 카테고리의 다른 글

CUDA - 원자적 연산  (0) 2016.03.03
CUDA - 시간 측정  (0) 2016.02.26
CUDA - 그래픽스 상호운용 - 6  (0) 2016.02.24
CUDA - 그래픽스 상호운용 - 5  (0) 2016.02.24
CUDA - 그래픽스 상호운용 - 4  (0) 2016.02.19
728x90

- explict - 

explicit는 명시적 호출만 허용하고 묵시적 호출은 허용하지 않는다.

쓰임은 아주 간단하기 때문에 한개의 예로 간단히 알 수 있다.


- 사용 예 -

class AA

{

private : 

int v;


public :

explicit AA(int _v)    // explicit이 적용된 생성자는 묵시적 형변환을 허용하지 않는다.

{

v = _v;

cout << "값 : " << v << endl;

}

};


- mutable -

mutable은 const명령어를 사용하여 상수화시킨 변수에 대해 예외를 두고 변수를 바꾸지 못하는 제한을 풀어버리는 키워드이다. c++의 유연성을 위해 제공하고 있는데 많은 프로그래머들이 mutable을 쓰는 것을 꺼려한다고 한다. 유연성을 위한다고 존재하지만 프로그램의 혼란을 초래할 수 있기 때문이다.

 

-- mutable이 적용된 예 --

class MyClass

{

     private :

          mutable int x;     // Mutable 선언

          int y;

 

     public :

          void SetData(int a, int b) const

          {

               x = a;     // x가 mutable이므로 변경 가능.

               y = b;     // 에러

          }

};

 

int main()

{

     MyClass a;

     a.SetData(10, 20);

     return 0;

}

 

출처 : http://blog.naver.com/sorkelf/40146367692

728x90
728x90

# 객체 포인터 변수

객체의 주소값을 저장하는 포인터 변수

Person * ptr; // 객체 포인터 변수 선언
ptr = new Person(); // 포인터 변수의 객체 참조

위 코드에서 ptr은 Person객체를 가리키게 된다. 그런데 Person형 포인터는 Person 객체뿐만 아니라, 
Person을 상속하는 유도클래스의 객체도 가리킬 수 있다.

class Student : public Person { ... }

Person *ptr = new Student(); // Person을 상속하는 Student 객체를 가리킨다.

class PartTimeStudent : public Student { ... }

Student *ptr = new PartTimeStudent; // Student를 상속하는 PartTimeStudent 객체를 가리킨다.

Person *ptr = new PartTimeStudent; // 기초클래스를 간접적으로 상속하는 PartTimeStudent도 가리킨다.

이를 일반화하면 C++에서 객체 포인터 변수는 선언된 객체를 직접 혹은 간접적으로 상속하는 모든 객체를 
가리킬수 있다.


IS-A관계를 통해서도 논리적으로 이해가 가능하다.

- 학생은 사람이다. (사람 → 학생)
- 근로 학생은 학생이다. (학생 → 근로학생)
- 근로 학생은 사람이다. (사람 → 근로학생)

※ 사람은 학생을 가리킬수 있지만 학생은 사람을 가리킬수 없다. 이 또한 현실세계를 역행하는 코드가 된다.

■ 객체 포인터의 권한
- 포인터를 통해서 접근할 수 있는 객체 멤버의 영역
- 포인터의 자료형(객체)에 따라 가리키는 대상에 관계없이 클래스에 정의된 멤버에만 접근가능하다.


# 함수 오버라이딩(function overriding)

함수 오버라이딩은 기초 클래스의 멤버함수와 같은 함수를 유도 클래스에 정의했을때 이를 가리켜 "유도클래스의
함수가 기초클래스의 함수를 오버라이딩 했다"라고 한다. 오버라이딩 된 기초 클래스의 함수는, 오버라이딩을 한
유도클래스의 함수에 가려진다.


※ 기초클래스와 동일한 이름의 함수를 유도클래스에서 정의한다고 해서 무조건 함수오버라이딩 되는것은 아니다.
매개변수의 자료형 및 갯수가 다르면, 이는 함수오버로딩 되어, 전달되는 인자에 따라서 호출되는 함수가 결정된다.
즉, 함수 오버로딩은 상속의 관계에서도 구성이 될 수 있다.


# 가상 함수(Virtual Function)

함수를 오버라이딩하는 이유는 객체를 가리키는 포인터 변수가 현재 자신이 가리키고 있는 객체의 멤버가 호출
되도록 하기 위함이다. 만약 포인터의 자료형을 이유로 자료형의 클래스의 멤버함수가 호출된다면 이건 분명 
잘못된것이다. 따라서 C++에서는 '가상함수'라는 것을 제공하고있다.

#include <iostream> 
usingnamespacestd;

class First
{
public:
virtualvoid MyFunc()
{
cout<<"FirstFunc"<<endl;
}
};

class Second: public First
{
public:
virtualvoid MyFunc()
{
cout<<"SecondFunc"<<endl;
}
};

class Third: public Second
{
public:
virtualvoid MyFunc()
{
cout<<"ThirdFunc"<<endl;
}
};

int main(void)
{
Third * tptr=new Third();
Second * sptr=tptr;
First * fptr=sptr;

fptr->MyFunc();
sptr->MyFunc();
tptr->MyFunc();
delete tptr;
return0;
}


※ 위 코드처럼 함수가 가상함수로 선언되면, 해당 함수호출시, 포인터의 자료형을 기반으로 호출대상을 결정하지
않고 포인터 변수가 실제로 가리키는 객체(Third)를 참조하여 호출의 대상을 결정한다.


■ 가상 함수의 실체

객체 안에는 실제로 멤버함수가 존재하지 않는다. 실제로 C++의 객체와 멤버함수는 함수를 공유하는 구조를 가진다.
객체가 생성되면 멤버변수는 객체 내에 존재하지만, 멤버함수는 메모리의 한 공간에 별도로 위치하고선, 이 함수가
정의된 클래스의 모든 객체가 이를 공유하는 형태를 취한다. 

virtual로 선언된 가상함수를 하나이상 포함하는 클래스에 대해서는 컴파일러가 '가상함수 테이블(Virtual-Table)'을
만든다. 이는 실제로 호출되어야 할 함수의 위치정보를 담고 있는데 이를 기반으로 함수가 호출되며 오버라이딩 된
가상함수는 유도클래스의 가상 함수 테이블에 존재하지 않는다. 때문에 가장 마지막에 오버라이딩 한 유도 클래스의
멤버함수가 호출된다.


■ 순수가상함수(Pure Virtual Function)와 추상클래스(Abstract Class)

class Employee 
{
private :
char name[100];
public:
Employee(char *name) {...}
void ShowName() {...}
virtualint GetPay() const = 0; // 순수 가상함수
virtualvoid ShowInfo() const =0; // 순수 가상함수
};


'순수가상함수'란 함수의 몸체가 정의되지 않은 함수를 의미한다.이를 표현하기 위해 코드처럼 '0의 대입'을 표시
하고 있다. 이것은 0을 대입하라는 것이 아니고 명시적으로 몸체를 정의하지 않았음을 컴파일러에게 알리는것이다.
이처럼 순수가상함수를 하나이상 지니는 클래스는 객체를 생성하려 할때 컴파일 에러가 발생한다.
이는 완전하지 않은, 그래서 객체생성이 불가능한 클래스라는 의미를 지니기 때문이다.

그리고 이러한 클래스를 가리켜 '추상클래스(abstract class)'라 한다



# 다형성(Polymorphism)

다형성은 어떤 문법이 아니라 가상함수의 호출관계에서 보인 특성을 가리켜 '다형성'이라고 한다.
'다형성(Polymorphism)'이란 '동질이상'을 의미한다. 즉, 다음과 같은 의미를 지닌다.

 "모습은 같은데 형태는 다르다"

이를 C++에 적용하면, "문장은 같은데 결과는 다르다"로 표현할 수 있다.

가장 대표적인 예로 자료형이 같은 포인터가 상속관계에 있는 서로 다른 객체를 가리키고 가상으로 선언된 함수를
호출하면 이는 다른 실행결과가 나타난다. 따라서 호출문장을 같지만 결과는 다른 '다형성'의 예로 볼 수 있다.


# 가상소멸자(Virtual Destructor)

virtual선언은 소멸자에도 올 수 있다. 객체의 소멸시 현재 참조하고 있는 포인터로 호출 할 경우 현재 참조되고 있는
객체의 소멸자만 호출된다. 따라서 이러한 경우에 메모리 누수가 발생하게 된다.
따라서 객체의 소멸은 delete 연산자에 사용된 포인터 변수의 자료형에 상관없이 모든 소멸자가 호출되어야 한다.
그리고 이를 위해서는 소멸자에도 virtual 선언을 추가하면 된다.

virtual ~First()
{
   delete[]arr;
}

※ 가상 소멸자가 호출되면, 상속의 계층구조상 맨 아래에 존재하는 유도 클래스의 소멸자가 대신 호출되면서 기초 
클래스의 소멸자가 순차적으로 호출된다.



# 참조자의 참조 가능성

참조자(Reference)를 이용하여 객체를 참조할 경우 포인터의 특성 및 가상함수의 개념도 모두 그대로 적용된다.


출처 : http://lazypaul.tistory.com/9

728x90

'Basic Programming > C, C++' 카테고리의 다른 글

C++ - auto_ptr의 문제점  (0) 2016.02.26
C++ - Explict와 Mutable 키워드  (0) 2016.02.24
C++ - Visual C++에서 동적 메모리 체크하기  (0) 2016.02.24
C++ - 가변 인자  (0) 2016.02.24
C++ - GET_SET_ACCESSOR 매크로  (0) 2016.02.24
728x90


메모리를 동적으로 할당(new) 한 후 해제(delete) 되지 않았을때 Visual C++ 에서 알아내는 방법에 대하여 알아보자.

참고링크 [Bottom] [Top]

매크로 정의 [Bottom] [Top]

먼저 디버깅 모드에서 동적 메모리를 추적할 수 있도록 아래와 같이 매크로를 정의한다.

  • Toggle line numbers
       1 #ifdef _DEBUG
       2 #define new new( _CLIENT_BLOCK, __FILE__, __LINE__ )
       3 #endif
    

동적 메모리의 추적은 디버그 모드에서만 사용가능하다.

동적 메모리의 추적 방법 설정 [Bottom] [Top]

동적 메모리를 어떻게 추적할지를 제어하기 위하여 설정하는 것으로 메모리를 할당하기 전에 실행해야 한다.

  • _CrtSetDbgFlag( _CRTDBG_LEAK_CHECK_DF | _CRTDBG_ALLOC_MEM_DF );
    

동적 메모리의 추적 방법을 제어하는 비트 필드는 다음과 같다.

  • 비트 필드

    기본값

    설명

    _CRTDBG_ALLOC_MEM_DF

    설정

    디버그 할당을 사용한다. 이 비트를 해제하면 할당은 함께 연결되어 있지만 블록 형식은 _IGNORE_BLOCK 이다.

    _CRTDBG_DELAY_FREE_MEM_DF

    해제

    부족한 메모리 조건을 시뮬레이션하는 것과 관련해서 메모리가 실제로 해제되는 것을 방지한다. 이 비트를 설정하면, 해제된 블록이 디버그 힙의 연결 리스트에 보관되지만 _FREE_BLOCK으로 표시되고 특별한 바이트 값으로 채워진다.

    _CRTDBG_CHECK_ALWAYS_DF

    해제

    모든 할당 및 할당 취소에서 _CrtCheckMemory를 호출하게 한다. 실행 속도는 지연되지만 오류를 신속하게 찾아낸다.

    _CRTDBG_CHECK_CRT_DF

    해제

    _CRT_BLOCK 형식으로 표시된 블록을 누수 탐지와 상태 구별 작업에 포함시킨다. 비트를 해제하면 위와 같은 작업을 하는 동안 런타임 라이브러리가 내부적으로 사용하는 메모리를 무시한다.

    _CRTDBG_LEAK_CHECK_DF

    해제

    _CrtDumpMemoryLeaks를 호출하여 프로그램을 종료할 때 누수 검사를 수행한다. 응용 프로그램이 할당한 모든 메모리를 해제하는 데 실패하면 오류 보고서가 생성된다.

동적 메모리의 추적 예제 [Bottom] [Top]

  • Toggle line numbers
       1 #include <stdio.h>
       2 #include <string.h>
       3 #include <crtdbg.h>
       4 
       5 // 매크로 정의
       6 #ifdef _DEBUG
       7 #define new new( _CLIENT_BLOCK, __FILE__, __LINE__ )
       8 #endif
       9 
      10 int main()
      11 {
      12         // 동적 메모리의 추적 방법 설정
      13         _CrtSetDbgFlag( _CRTDBG_LEAK_CHECK_DF | _CRTDBG_ALLOC_MEM_DF );
      14 
      15         char* s1;
      16         char* s2;
      17 
      18         s1 = new char[128];     // 메모리 할당
      19         strcpy( s1, "string1" );
      20 
      21         s2 = new char[256];     // 메모리 할당
      22         strcpy( s2, "string2" );
      23 
      24         delete[] s1;            // 메모리 해제
      25 
      26         return 0;
      27 }
    

예제의 결과 [Bottom] [Top]

디버그 모드로 실행후 프로그램이 종료되면 아래의 Visual Studio 의 출력창에 다음과 같은 결과가 출력된다.

  • ----------------------------------------------------------------------------
    'debug.exe': 'C:\temp\debug\Debug\debug.exe' 로드, 기호가 로드되었습니다.
    'debug.exe': 'D:\WINXP\system32\ntdll.dll' 로드, 기호가 로드되지 않았습니다.
    'debug.exe': 'D:\WINXP\system32\kernel32.dll' 로드, 기호가 로드되지 않았습니다.
    Detected memory leaks!
    Dumping objects ->
    c:\temp\debug\main.cpp(21) : {44} client block at 0x003710B0, subtype 0, 256 bytes long.
    Data: <string2 > 73 74 72 69 6E 67 32 00 CD CD CD CD CD CD CD CD
    Object dump complete.
    '[3340] debug.exe: 기본' 프로그램이 0 (0x0) 코드에서 끝났습니다.
    ----------------------------------------------------------------------------
    

여기에서 아래의 메세지는 main.cpp 의 21 번째 줄에서 할당한 256 바이트가 해제 안된 것을 알려준다.

  • c:\temp\debug\main.cpp(21) : {44} client block at 0x003710B0, subtype 0, 256 bytes long.
    Data: <string2 > 73 74 72 69 6E 67 32 00 CD CD CD CD CD CD CD CD


출처 : http://www.viper.pe.kr/cgi-bin/moin.cgi/Visual_C%2B%2B_%EC%97%90%EC%84%9C_%EB%8F%99%EC%A0%81_%EB%A9%94%EB%AA%A8%EB%A6%AC_%EC%B2%B4%ED%81%AC%ED%95%98%EA%B8%B0#bottom



그럼 이제 간단히 첨부된 파일 C_Memory.h 파일을 보면서 메모리 릭 발생여부를 알아내고 이를 해결하는 방법에 대해서 알아보자. 

 

이는 MS 사에서 CRT(C Runtime) 라이브러리로 제공해는 CRTDBG 를 이용할 것이다.

 

일단 프로그램 정의부에 다음과 같이 추가한다. 

#include <crtdbg.h>

#define CRTDBG_MAP_ALLOC

#ifdef _DEBUG

#define new new( _NORMAL_BLOCK, __FILE__, __LINE__ ) //궁금하시면 제 게시글 중 <매크로 마법> 편을 보세요.

#endif

 

이제 메인 진입 부 처음에 다음과 같이 선언하고. 일반 main() 이라고 가정하자. 

void main() {

_CrtSetDbgFlag( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF );

// .... 메모리를 동적 할당하는 무언가의 작업을 하겠죠

}

 

추가로 설명하자면, _CrtSetDbgFlag 설정 비트 필드 값은 다음과 같다. 

_CRTDBG_ALLOC_MEM_DF : 디버그 힙 메모리 할당을 허용하고, 또한 _CLIENT_BLOCK과 같은 사용자 정의 메모리 블럭을 사용한다.


_CRTDBG_LEAK_CHECK_DF : 프로그램 종료 시 자동으로 메모리 릭을 찾고 이를 출력하고, 이것은 _CrtDumpmemoryLeaks 함수를 호출한 것과 같은 결과이다. 

 

이제 프로그램을 디버그 모드로 실행하고 종료했을 때, 만약 동적 메모리 해제를 제대로 안해줬다면 다음과 같은 결과가 결과 화면에 출력될 것이다. 

 

 

  

우리에게 필요한 것은 바로 빨간색 선을 그은 정보이다. 우리는 이 번호를 이용해 메모리 릭이 일어나는 부분을 찾아갈 수 있다. 그러나 중요한 점은 똑같은 환경에서 다시 실행했을 때도 메모리 릭 현상 결과가 같아야 한다는 것이다. 그렇지 않으면, 매번 실행할 때마다 메모리 릭이 발생한 위치가 달라지고 따라서 위 번호도 달라져 버리기 때문이다. 따라서, 다른 작업을 하지 마시고 디버깅 작업만 하길 바란다.

일단 위 9637 번호를 이용해서 메모리 릭이 일어나는 곳을 찾아가보자. 이제 다시 main 첫부분에 다음과 같이 추가한다.

void main()

_CrtSetDbgFlag( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF );

_CrtSetBreakAlloc( 9637 );

// .... 메모리를 동적 할당하는 무언가의 작업을 한다...

}


그리고 다시 디버그 모드로 실행을 하면 9637에 지시하는 똑같은 메모리 번지를 알아서 브레이크 포인터를 걸어준다.

우리는 그 부분의 소스를 보고 콜 스택을 추적해 가면서 메모리 릭을 감지하고 해결할 수가 있다.

이런식으로 출력창에 출력되었던 다른 메모리 릭도 모두 찾아 해결하면 된다.

배포 전, 메모리 책임은 프로그래머의 미덕이지 않을까?


C_Memory.h


출처 : http://blog.naver.com/hermet?Redirect=Log&logNo=54353236


728x90

'Basic Programming > C, C++' 카테고리의 다른 글

C++ - Explict와 Mutable 키워드  (0) 2016.02.24
C++ - 함수 오버라이딩, 가상함수, 다형성  (0) 2016.02.24
C++ - 가변 인자  (0) 2016.02.24
C++ - GET_SET_ACCESSOR 매크로  (0) 2016.02.24
C++ - 헝가리안 표기법  (0) 2015.12.12
728x90

printf()와 scanf()와 같은 가변 인자를 받는 함수를 만들거나 이 함수를 덮어 쓰는 wrapper를 만들려면 가변 인자를 처리할 줄 알아야 합니다.


가변 인자를 처리하려면 다음과 같은 data type과 함수(또는 매크로 함수)를 써야 합니다.


• va_list : type

• va_start(va_list argptr, last-arg) : 가변 인자 처리 시작

• va_end(va_list argptr) : 가변 인자 처리 끝

• T va_arg(va_list argptr, T) : 가변 인자 얻기


이들은 모두 표준 헤더 파일인 <stdarg.h>에 선언되어 있습니다. 일단 간단히 N개의 정수(int type)를 받아서 더하는 함수를 만들어 봅시다:

#include <stdarg.h>

int
sum(int nargs, ...)
{
  va_list argptr;
  int i, total = 0;

  va_start(argptr, nargs);        /* 가변 인자 처리 시작 */
  for (i = 0; i < nargs; i++)
    total += va_arg(argptr, int); /* 하나씩 가변 인자 얻기 */
  va_end(argptr);                 /* 가변 인자 처리 끝 */

  return total;
}


위와 같이 정의해 놓고 다음과 같이 쓸 수 있습니다 (주의: sum()의 첫번째 인자는 두번째부터 나올 가변 인자의 갯수입니다):

int sum(int nargs, ...);

int
main(void)
{
  int s1, s2;

  /* 1부터 5까지 더한 값을 s1에 저장. */
  s1 = sum(512345);

  /* 100, 200, 300을 더한 값을 s2에 저장. */
  s2 = sum(3100200300);

  /* ... */
  return 0;
}


일단 가변 인자를 처리하기 위해서는 va_list type의 변수 하나를 선언해야 합니다:

va_list argptr;


그리고, 가변 인자를 처리할 부분 앞에 다음과 같이 써 줍니다:

va_start(argptr, nargs);


va_start()의 첫번째 인자는 아까 선언한 va_list type의 변수 이름이며, 두번째 인자는 이 함수에서 가변 인자를 받는 부분의 바로 앞의 (고정된) argument입니다. 즉, 위에서 예로 든 sum()의 경우, 가변 인자를 나타내는 "..." 앞에 오는 argument인 nargs가 두번째 인자가 됩니다.

눈치가 빠른 분은 이미 알고 있겠지만, 가변 인자를 받는 함수는 여러 가지 제한이 붙습니다. 첫째, va_start()에 가변 인자 바로 앞의 고정된 인자를 주어야 하기 때문에, 가변 인자를 받는 함수는 고정된 인자가 하나 이상 나와야 합니다. 즉, void foo(...)와 같은 함수는 만들 수 없습니다. 둘째, 가변 인자를 나타내는 "..."는 반드시 마지막 인자이어야 합니다. "..."가 중간이나 처음에 나올 수는 없습니다. 예를 들어 void bar(..., int a)와 같은 함수는 만들 수 없습니다.

실제 함수에 전달된 가변 인자를 얻기 위해서는 va_arg()를 불러서 처리합니다. va_arg()는 가변 인자로 전달된 값을 호출할 때마다 하나씩 얻어서 줍니다. va_arg()는 첫번째 인자로, va_start()로 초기화한 va_list type을 받으며, 두번째 인자로는 가변 인자로 전달된 값의 type을 써 주어야 합니다. 우리가 만든 sum()의 경우, 함수의 목적이 모든 int를 더한 값을 계산하는 것이므로, va_arg()의 두번째 인자로 int를 주면 됩니다.

va_arg(argptr, int);


눈치가 더욱 빠른 분이면 아시겠지만, 여기서 가변 인자를 받는 함수에 또다른 제한이 붙습니다. 아시겠지만, 가변 인자로 전달되는 값의 타입에는 제한이 없습니다 (printf(), scanf()를 생각해보세요). 아쉽게도 va_arg()는 전달되는 값의 타입을 알아낼 방법이 없습니다. 따라서 미리 그 type을 알아서 va_arg()의 두번째 인자로 전달해 주어야 합니다.

게다가, 가변 인자를 받는 함수의 입장에서, 몇개의 가변 인자가 전달되었는지 알아낼 방법은 없습니다. 따라서 고정된 인자로 가변 인자의 갯수를 전달하는 등 (위의 sum()의 예처럼)의 방법으로 va_arg()를 몇번을 불러야 되는가를 알아낼 방법을 제공해야 합니다.

더 골치 아픈 것은 va_arg()의 두번째 인자로 쓸 수 있는 type에 제한이 있다는 것입니다. 이건 뒤에서 다루겠습니다.

이제 가변 인자를 처리하는 함수를 만들어 보았으니, printf()의 wrapper를 만들어 봅시다.

노파심에서 말씀드리지만, 다음과 같은 함수는 동작하지 않습니다:

int
my_printf(const char *fmt, ...)
{
  return printf(fmt, ...);
}


일단 먼저, printf의 계열에는 다음과 같은 함수들이 있다는 것을 알아둡시다:

int printf(const char *fmt, ...);
int fprintf(FILE fp, const char *fmt, ...);
int sprintf(char *sr, const char *fmt, ...);
int snprintf(char *s, size_t n, const char *fmt, ...);
/* 이상 <stdio.h>에 선언되어 있는 함수들 */

int vprintf(const char *fmt, va_list ap);
int vfprintf(FILE fp, const char *fmt, va_list ap);
int vsprintf(char *sr, const char *fmt, va_list ap);
int vsnprintf(char *s, size_t n, const char *fmt, va_list ap);
/* 이상 <stdarg.h>에 선언되어 있는 함수들 */


잘 보면 알겠지만, <stdarg.h>에 있는 함수들은 <stdio.h>에 있는 printf() 계열 함수 이름 앞에 'v'가 붙고, "..." 대신에 va_list type의 인자가 온다는 것을 알 수 있습니다.

printf() style의 문자열을 받으려면, 이 v*printf() 계열의 함수를 쓰면 편리합니다. 예를 들어 단순히 printf()와 똑같은 기능을 하는 wrapper는 다음과 같이 만들 수 있습니다:

int
my_printf(const char *fmt, ...)
{
  va_list argptr;
  int ret;

  va_start(argptr, fmt);
  ret = vprintf(fmt, argptr);
  va_end(argptr);

  return ret;
}


즉, 가변 인자 처리를 위한 va_list type 변수를 선언, va_start()로 초기화한 다음에 v*printf() 계열의 함수에 이 va_list type의 변수를 넘겨주기만 하면 됩니다. (va_end()를 불러 끝내는 것을 잊지 마세요!!)

좀더 복잡하게 하나 더 만들어 봅시다. 함수는 error()입니다. 이 함수는 주어진 printf() style의 에러 메시지를 받아서 stderr로 출력하고, 필요하면 errno의 내용도 알려주고, 필요하면 exit()를 불러서 프로그램을 종료하는 함수입니다. 선언은 다음과 같습니다:

void error(int status, int ecode,
           const char *fmt, ...);


이 함수는 ecode가 0이 아닌 경우, strerror(ecode) 값을 출력하고, fmt와 "..."으로 전달된 printf() style의 에러 메시지도 출력한 다음, status가 0이 아닌 경우, exit(status)를 호출합니다. 함수 정의는 다음과 같습니다.

void
error(int status, int ecode,
      const char *fmt, ...)
{
  va_list argptr;

  fflush(stdout);
  fprintf(stderr"error: ");
  if (ecode)
    fprintf(stderr"%s: ", strerror(ecode));

  va_start(argptr, fmt);
  vfprintf(stderr, fmt, argptr);
  va_end(argptr);

  fputc('\n'stderr);

  fflush(stderr);  /* redundant */

  if (status)
    exit(status);
}


다음과 같이 쓸 수 있습니다.

#include <stdio.h>
#include <errno.h>

...

void
foo(const char *filename)
{
  FILE *fp;
  fp = fopen(filename, "r");
  if (!fp)
    error(1, errno, "cannot open file %s.", filename);
  ...
}


참고로 여기서 예로 만든 error()는 GNU C library (glibc)에 이미 포함되어 있습니다. 여기에서는 좀더 간략화해서 만들어 본 것이죠. 실제 여러분의 코드에 <error.h>를 포함시키면 위 error()를 정의할 필요없이 바로 쓸 수 있습니다. (출력 형태는 약간 다를 수 있습니다만, 인자 type이나 갯수는 같으니 똑같이 쓸 수 있습니다)

마지막으로 아까 va_arg()의 두번째 인자로 나올 수 있는 type에 제한이 있다고 했는데 그 얘기를 해보겠습니다. 결론부터 말하면, va_arg()의 두번째 인자로 나올 수 있는 type은 다음과 같습니다:

  • int 계열 (int, unsigned int, signed int)
  • long 계열 (long, unsigned long, signed long)
  • double 계열 (double, long double)
  • 포인터 계열 (모든 타입의 포인터, 예를 들어 void *, char *, 등등) 

다음과 같은 타입은 올 수 없습니다:

  • char 계열 (char, unsigned char, signed char)
  • short 계열 (short, unsigned short, signed short)
  • float 계열 (float) 

그럼 가변 인자로 char나 float type은 올 수 없느냐, 그렇지는 않습니다. 이런 타입을 받기 위해서는, va_arg()에 다음과 같이 전달합니다.

  • char -> int (unsigned char는 unsigned int, 이런 식으로)
  • short -> int (unsigned short는 unsigned short, 이런 식으로)
  • float -> double 

그 이유는 다음과 같습니다. C 언어가 처음 만들어질 때에는 function prototype이란게 없었습니다. 인자가 몇개이고, 어떤 타입이냐에 상관없이 단순히 함수가 있고, 리턴 타입이 뭐다. 정도만 알려 줬죠. 예를 들면 다음과 같습니다:

double bar();

double bar(a, f, ch)
  int a;
  float f;
  char ch;
{
  ...
}


이런 식으로 함수를 선언하게 되면, 컴파일러가 함수의 정의를 보기 전에는 이 함수에 전달되는 인자의 타입이 뭔지 알 수 없습니다. 만약 컴파일러가 bar()의 정의를 보기 전에 다음과 같은 코드를 컴파일해야 한다고 가정해 보기 바랍니다:

void
foo(void)
{
  bar(42.59);
}


일단 bar()의 인자가 3개인데,, "void bar();"만 보고서는 인자가 적절하게 전달되었는지 알 방법이 없습니다. 따라서 인자 갯수 체크를 할 수 없습니다.

둘째로, 각각의 인자가 알맞는 타입인지도 알 수 없습니다. 함수 호출을 위해서는 (stack에 인자를 push해야 하는데), 인자의 type을 알 수 없으니, 첫번째 인자인 4를 push할 때, 8 bit로 4를 push할 것인지, 16 bit로 4를 push할 것인지, 32 bit로 할 것인지 알 수가 없습니다.

따라서 C 언어에는 "integral promotion"이란 용어? 개념?이 있고, 여기에 따라서 모든 인자는 다음과 같은 type으로 변환해서 stack에 push합니다.

  • char, short -> int
  • float -> double
  • int -> int (그대로)
  • long -> long (그대로)
  • double -> double (그대로)
  • long double -> long double (그대로)
  • 모든 pointer type -> 모든 pointer type (그대로) 

즉, 인자로 전달된 값이 character 값이라 할 지라도 함수를 호출할 때에는 int type으로 변환해서 전달합니다.

지금까지는 ANSI C에서 function prototype이 나오기 전의 old style (K&R) C에 필요한 내용입니다. 자, ANSI C에서는 function prototype이 있으니, 컴파일러가 함수 호출을 하기 전에 함수의 인자 갯수와 타입을 모두 알 수 있습니다.

double bar(int a, float f, char ch);


따라서 더 이상 이러한 변환이 필요없게 되었지만, 하나 예외가 있는데, 바로 가변 인자를 처리하는 함수입니다. 가변 인자는 실제로 어떠한 타입이 몇개가 들어올 지 알 수 없기 때문에, 이러한 변환이 계속 수행됩니다. 따라서 va_arg()로 이러한 타입을 받을려면 이 integral conversion을 미리 생각해 두어야 합니다.

예를 들어 문자 하나씩 받아서 출력하는 함수를 만들어 보겠습니다:

void
putchars(int nargs, ...)
{
  va_list argptr;
  char ch;
  int i;

  va_starg(argptr, nargs);
  for (i = 0; i < nargs; i++) {
    ch = (char) va_arg(argptr, int);
    putchar(ch);
  }
  va_end(argptr);
}


즉, 들어올 가변 인자가 char type일지라도 va_arg()에서는 integral promotion에 따라 int type으로 받아서 처리해야 합니다.

잘 생각해 보면, 이미 여기에 대해 알고 있었을 것입니다. printf()와 scanf()를 생각해봅시다.

printf에서 float이나 double을 출력하기 위해서는 "%f"를 씁니다. 그런데, scanf에서는 float은 "%f", double은 "%lf"를 씁니다. 아래 예 참고:

double d;
float f;

printf("%f", d);
printf("%f", f);

scanf("%f", &f);
scanf("%lf", &d);


그 이유는, integral promotino에 의해 printf의 경우, float을 double로 받기 때문입니다. 따라서 float과 double에 관한 차이가 없습니다. 그러나 scanf의 경우에는 모든 인자가 pointer type이기 때문에 이러한 변환이 필요없습니다. 즉, 모든 타입을 각각 구별해야 하는 것이죠.

분명 printf 코드안에서 float을 출력하는 부분은 다음과 같은 코드를 포함합니다:

  value = (float) va_arg(argptr, double);


또한 scanf 코드 안에서 float을 출력하는 부분은 다음과 같은 코드를 포함합니다:

  ptr = va_arg(argptr, float *);


이 정도면 가변 인자를 처리하는 방법은 완벽히! 익히셨을 것입니다. 그럼 다음 기회에...

끝 -- cinsk



  • TODO: Function prototype이 ANSI C에서 소개된 것 맞나?
  • TODO: Integral Promotion이라고 했는데, 여기에 float, double이 포함되는 것이 확실한가?
이거 알아보고 글 고칠 것.. -- cinsk.

함수 선언(정의가 아닌)의 경우에는 그 전에도 있었던 것 같습니다. [http]http://www.ifi.uio.no/forskning/grupper/dsb/Programvare/Xite/ProgrammersManual/node11.html#SECTION000111000000000000000 그러나 매개변수의 타입들을 함께 적어주는 function prototype은 ANSI C에서 새로이 나온 것인 듯 합니다.

가변인자 함수에서 적용되는 것은 Integral Promotion을 포함하는 Default argumnt promotion입니다.

C99 6.5.2.2 Function calls 중에서...

If the expression that denotes the called function has a type that does not include a prototype, the integer promotions are performed on each argument, and arguments that have type float are promoted to double. These are called the default argument promotions.

그리고 표준문서에서는 Integer promotion이라는 용어를 씁니다.

Integer promotion은 이름 그대로 정수형의 승급만을 의미합니다. double이나 포인터는 포함되지 않습니다. int형 또는 unsigned int형보다 rank가 낮은 (즉 표현범위가 좁은) 정수형에 한해서 int 형 또는 unsigned 형으로 승급됩니다. 해당되지 않는 데이터 형들은 그대로 남습니다.

6.3.1.1 Boolean, characters, and integers 중에서...

The following may be used in an expression wherever an int or unsigned int may be used:

  • An object or expression with an integer type whose integer conversion rank is less than the rank of int and unsigned int.
  • A bit-field of type _Bool, int, signed int, or unsigned int. 

If an int can represent all values of the original type, the value is converted to an int; otherwise, it is converted to an unsigned int. These are called the integer promotions. All other types are unchanged by the integer promotions.



가변 인자 매크로 


ISO C 표준을 지원하는 컴파일러에서는 가변 인자를 처리하는 매크로도 정의할 수 있습니다. 함수 형태의 매크로를 정의할 때, "..."을 써 주고, 매크로 정의 부분에서 VA_ARGS 써주면 그 자리에 확장됩니다. 예를 들면, 다음과 같습니다:

#define debug(s, ...)    fprintf(stderr, s, __VA_ARGS__)

void
foo(void)
{
  debug("Entered the function, %s\n"__func__);
  /* ... */
}


간단하게 printf 형태의 메시지를 입력받은 표준 함수 assert()와 비슷한 매크로 함수를 만들어 봅시다; 함수 이름은 ASSERT()로 하겠습니다. ASSERT()의 첫 인자는, assert()의 인자와 같습니다. 즉 거짓인 경우, abort()를 부릅니다. ASSERT()의 두번째 인자부터는 printf()와 같습니다. 또한 NDEBUG 매크로가 정의된 경우, ASSERT()는 아무런 영향을 주지 않습니다.

일단, ASSERT()가 쓰인 시점의 파일 이름, 줄 번호, 그리고 함수 이름을 자동으로 출력하게 합시다. 따라서 미리 정의된 매크로인 FILE과 LINE을 쓰며, 미리 정의된 이름, func를 씁니다.

아래에 ASSERT()의 정의가 나갑니다:

#ifndef NDEBUG
# define ASSERT(exp, s, ...)  assert_(__FILE____LINE____func__, \
                                      #exp, s, __VA_ARGS__)
#else
# define ASSERT(exp, s, ...)  ((void)0)
#endif /* NDEBUG */


간단합니다. NDEBUG가 정의된 경우에는 ASSERT()가 assert_()를 호출합니다. assert_()의 처음 세 인자는 ASSERT()를 부른 곳의 파일 이름, 줄 번호, 함수 이름을 자동으로 전달하게 됩니다. 다음으로 ASSERT()의 첫 인자를 문자열로 바꾸어 전달하며(#exp), 그 다음으로, printf() 스타일의 format string인 s가 들어오며, 그 뒤에 가변 인자가 들어옵니다.

이제 assert_()의 정의가 나갑니다:

#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>

void
assert_(const char *filename, int lineno, const char *func,
        const char *exp, const char *fmt, ...)
{
  va_list argptr;

  fflush(stdout);
  fprintf(stderr"%s:%d: assertion, (%s) failed in function, %s\n",
          filename, lineno, exp, func);
  fprintf(stderr"\treason: ");
  va_start(argptr, fmt);
  vfprintf(stderr, fmt, argptr);
  va_end(argptr);
  putc('\n'stderr);
  abort();
}


예를 들어 다음 함수를 생각해 봅시다:
void
set_age(int age)
{

ASSERT(age > MIN_AGE, "age should be larger than %d.", MIN_AGE);
/* ... */

}
MIN_AGE는 10으로 정의된 매크로이고, 사용자가 set_age(3)을 호출했다면, 다음과 같은 ASSERTion이 발생합니다:

tmp.c:36: assertion, (age > minimum) failed in function, set_age
        reason: age should be larger than 10.
Aborted


자, 그럭 저럭 ASSERT() 쓸만하죠? assert()보다는 좀 나을 것 같습니다.

참고: ISO C 표준 6.4.2.2, 6.10.3



출처 :

https://wiki.kldp.org/whttps://wiki.kldp.org/wiki.php/CLanguageVariableArgumentsListiki.php/CLanguageVariableArgumentsList

728x90
728x90

DXUT.cpp에서 처음 발견한 이 매크로는 아직 유용하게 사용하고 있다.



클래스들의 멤버 변수들에 대해 항상 만들기 귀찮던 Get, Set 함수를 만들어주는 매크로이다.


이것은 DXUT의 뮤텍스와 구조체를 사용하므로, 다음과 같이 나에게 맞게 수정하여 사용하고 있다.


#define SET_ACCESSOR(x, y)    inline void Set##y(x t)    { m_##y = t; };

#define GET_ACCESSOR(x, y)    inline void Get##y(x t)    { return m_##y; };

#define GET_SET_ACCESSOR(x, y)    SET_ACCESSOR(x, y)    GET_ACCESSOR(x, y)


728x90
728x90

Private 멤버를 헤더에서 숨기기위한 기법으로 Pimpl Idiom이 있다.

클래스 내부 구조체에 private 멤버들을 넣는 것인데, 헤더에는 구조체를 선언만하고 소스에서 정의를 하는 방법이다.

간단한 코드를 보면 다음과 같다.




구조체의 포인터 이름을 따서 Pimpl Idiom이라고 하는 기법인데, 이런식으로 하면 헤더에서는 private 멤버들을 전혀 알 수 없고, private 멤버들을 수정할 때 헤더는 건드리지 않으므로 이 헤더를 사용하는 다른 파일들까지 다시 컴파일을 하지 않아도 되는 장점이 있다. 


물론 몇가지 단점도 있다. private 멤버들을 사용할 때는 구조체의 포인터를 이용해야 하기 때문에 약간의 오버헤드가 있고, private 멤버들에서 public 멤버를 엑세스하기 위해서는 구조체에 원래 클래스로의 포인터가 필요한 문제점도 있다.


참고 : Exceptional C++ 책

728x90
728x90

Visual Studio를 사용하다 보면 가끔 "디버깅 정보를 찾을 수 없거나 정보가 일치하지 않습니다." 라는 메세지를 띄우며 디버깅이 안될 때가 있다. 


이럴 때에는 아래와 같이 프로젝트 위에서 오른쪽 클릭으로 '속성'에 들어가서 세가지를 변경하면 된다.


  • 프로젝트 속성 - C/C++ - 일반 - 디버깅 정보 형식 - 편집하며 계속하기를 위한 프로그램 데이터베이스(/ZI)

  • 프로젝트 속성  - C/C++ - 최적화 - 최적화 : 사용 안 함(/Od)

  • 프로젝트 속성 - 링커 - 디버깅 - 디버그 정보 생성 : 예(/DEBUG)


ps. Release에서 변경하면 배포시에 속도가 느려질 수 있으니 Debug에서만 하도록 하자....



728x90

+ Recent posts