C 언어 변수

실수형 변수

실수를 처리하기 위해 float와 double을 사용한다. 실수를 2진수를 표현할 때는 부동소수점(Float-Point) 방식을 사용한다. 이것은 국제표준 IEEE 754 규격에 따른다.

포인터

포인터 변수 모두는 메모리의 주소를 지정하는 값을 가지고 있으면 값을 변화시킬 수 있기 때문에 CPU을 설계한 설계 기준에 따라 주소값의 길이와 방식이 결정된다. 일반적인 용도의 대부분의 CPU는 메모리를 지정하는 길이(비트수)는 동일하다. RAM이든 ROM/FLASH 이든 모든 주소는 같다. MCU(8051,...)은 오히려 많은 경우 메모리 영역을 나누어 다른 주소체계를 사용한다. 8051은 내부의 256바이트 내에 변수를 할당 한다. 256바이트는 매우 적기 때문에 많은 데이터 저장용으로 16비트의 저장 공간을 갖는 주소체계를 사용하고 기계어 코드를 분리 했다. 이럴 경우는 주소값이 8비트 또는 16비트가 필요하다.
C언어가 UNIX 계열의 OS 작성 할 때 사용하였으므로 커널의 프로그램 소스를 보면 상당히 많은 부분 포인터 변수를 볼 수 있다.

포인터 변수의 선언

포인터 변수는 *을 이용하여 선언 한다. 포인터는 메모리의 주소값을 가지고 데이터의 위치를 지정하기 때문에 다른 변수의 저장 공간의 주소를 알아야 한다. 따라서 정적 변수의 주소는 & 연산자를 사용한다.
  int ival;
  int *pval;

  // ...

  pval = &ival; // ival의 &연산자는 ival가 존재하는 위치 주소값이다.
  *pval = 30;   // pval가 ival의 주소값을 가지고 있으므로,
                // 이 주소값을 먼저 읽어 30을 쓸 위치를 결정하고
                // 그 위치에 정수 값을 쓴다.
  printf("변수 pval가 존재하는 위치 주소값은 0x%08X, 정수형 데이터 저장공간 지정은 0x%08X\n", &pval, pval );
ival는 정수형 데이터가 들어가는 변수이다. 즉, 정수 숫자를 저장 한다. 그러나 변수 pval은 데이터 공간의 위치를 지정하는 것이지 정수형 데이터를 저장하는 것은 아니다. CPU의 주소체계에서 메모리의 주소값을 저장 함으로써 데이터가 저장 될 주소값을 가지고 액세스 하는 것이다. 그러나 메모리 주소값도 하나의 이진수의 정수형의 일종이라고 생각 할 수 있다.

NULL 사용

포인터 변수는 다른 정적 변수나 동적함수(malloc(), new)에 의해 생성된 저장 공간을 지정하는 변수이다. 그러나 경우에 따라 데이터 저장공간을 지정하지 못한 상태에서 액세스 할 수 있다. 보통 저장공간 지정에 성공했는지를 판단하는 방법으로 NULL을 이용할 수 있다.
NULL  사용 예:
  char data8;
  char *pval = NULL;  // 포인터가 데이터 저장공간을 지정하지 않았다.

   if (pval == NULL)
       pval = data8; // 변수 pval가 데이터 저장 공간을 변수 data8로 지정 한다.
   *pval = 10; // 위에서 지정한 data8에 10을 쓴다.
C/C++에서 NULL은 숫자 0으로 정의 되어 있다. 자료구조 등에서도 '없다'는 의미로 null을 사용하는데, 경우에 따라 정해진 비트수 만큼 이진수로 모두 1인 경우가 있다. 그러나 C/C++에서는 0으로 정의 되어 있으므로 메모리의 0번지에는 특수하게 사용하지 않는다.
만약 포인터 변수가 데이터 저장공간을 지정하지 않았다면 NULL을 사용하여 초기화 시키는 방식이 일반적이다.

포인터 변수형과 액세스

포인터 변수의 길이(변수의 비트수)는 마이크로프로세서에 의존 한다. 즉, 마이크로프로세서가 주소 공간과 액세스 체계를 이미 가지고 있기 때문에 C/C++언어의 컴파일러는 이를 따를 뿐이다. 즉, 포인터 변수의 길이는 CPU 의존적으로 결정 되어 있으므로 모든 포인터의 길이는 같다. 그렇다면 왜 포인터 변수 선언 시, 앞에 변수형이 필요한가는 액세스 할 때 데이터 액세스 길이(비트수)를 결정하기 위해서 이다.
32비트 CPU의 대부분은 32비트 주소공간과 32비트 데이터 액세스 단위를 가지므로
다음 예는 32비트라고 가정 하면:
  // 다음 설명에서 나오는 비트 단위는 32비트 CPU의 경우

  char buff[1024];

  int *pdata = (int*) buff; // 형이 다르나 값을 액세스 할 때, 32비트 정수형으로 액세스 하기 위해 지정
  *pdata = 23;          // 32비트 정수형 쓰기, 따라서 23은 32비트 정수형 이다.
  *((char*)pdata) = -1; // 그러나 char 8비트 정수형으로 변환 하였으므로 -1은 8비트 정수형이다.
                        // 8비트 정수 -1 : 11111111b
  void *pval;      // 데이터 액세스 타입(액세스 비트 단위)가 없는 변수가능 하다.
                   // 모든 주소값은 32비트 이기 때문이다.
  pval= (void *) buff;  // 형이 맞지 않으므로 형 변경
  *pval = 10;       // '''error : 액세스 단위를 결정할 수 없다. 따라서 기계어 코드를 확정할 수 없다.
  *(char*)pval = 10; // 액세스 단위가 8비트 이므로, 10은 8비트 정수형이며 이 값이 써진다.
코드 중 *pval = 10;은 pval가 void형이므로 액세스 시, 10을 어떻게 규정할지 결정할 수가 없다. 보통 10은 정수형으로 취급 되지만 같은 정수형도 8,16,32비트로 다르다. 따라서 pval의 변수형에 따라서 비트수가 결정되는데 여기서는 void이므로 결정 불가능 하다.

예

#include <stdio.h>#include <string.h>/// Global Variableschar name[124];char tel[] = "010-2345-6789";// func.char *read_name(char *pstr, int szmax);int main(int argc, char**argv){
   char *pstr;

    pstr = read_name(name, 120);
    printf("이 름 = %s\n", pstr );
    printf("전화 번호 = %s\n", tel);

    return 0;}char *read_name(char *pstr, int szmax){
   size_t leng;
   static char bstr[256];

    gets(bstr);
    leng = strlen(bstr);
    if (leng >= (size_t)szmax)
        leng = szmax -1;
    strncpy (pstr,bstr,leng);
    *(pstr+leng) = 0;

    return pstr;}
이 프로그램 예에서 실제 스트링 메모리 공간을 갖는 것은 name, tel, bstr 변수 들이다. 그러나 pstr변수는 스트링 데이터가 들어갈 변수가 아니고 데이터가 들어가야할 위치를 지정하는 변수이다. CPU의 주소체계에 의한 주소값으로 데이터의 위치를 지정하는 것이다. 이 포인터 변수는 메모리 주소값만 가지면 되므로 정해진 길이의 비트수를 가지고 동작 한다.

포인터 변수 자료형

포인터 변수는 데이터 저장 위치 주소값을 사용하는 변수이다. 보통 정적 변수의 경우 직접 액세스 모드(direct access mode)의  기계어 코드로 컴파일 된다.
포인터 변수의 예 :
  char   *p8bitvar;       // 8비트 액세스
  short int  *p16bitvar;  // 16비트 액세스
  int    *pintvar;  // int형 액세스
  float  *pfvar;    // 32비트 [[부동소수점]] 데이터 액세스
  double *pdvar;    // 64비트 부동소수점 데이터 액세스
  void   *pvdata;   // 몇 비트 액세스 인지 규정 할 수 없다.

  struct SObject {
    int data;
    struct SObject *link;
  } *pstdata;         // 구조체의 위치를 지정 한다.
변수 자체의 길이는 CPU의 메모리 체계에 의해 결정된다. 32비트 CPU는 거의 다 32비트의 메모리 주소값을 갖는다. 따라서 위의 모든 변수의 크기는 4바이트이다.
앞에 변수형이 필요한 이유는 액세스 할 때, 몇 비트의 단위로 액세스 하는냐를 결정한다. void의 경우는 데이터가 몇 비트인지 결정할 수 없으므로 결국 액세스형 변환을 통해 몇비트 액세스 인지 결정 해야한다.
void 포인터 변수의 예 :
 char num;
 void *pvdata = (void *) &num;
 *(char*) pvdata = 10;
 int ival;
 pvdata = (int *) &ival;
 *(int*) pvdata = 10;

 *pvdata = 10; // 오류 : 액세스 단위가 정의 되지 않음.
   //error C2100: illegal indirection
   //error C2440: '=' : cannot convert from 'int' to 'void *'
위 *pvdata = 10;에서 10이라는 숫자는 기본적으로 정수형인데,  pvdata의 액세스 단위가 결정되지 않았다. 즉, 이런경우 액세스 단위는 변수가 결정 한다. 따라서 이런 경우는 오류로 나타난다.

함수 포인터

함수 역시 기계어 코드가 특정 메모리를 점유하고 이것을 읽어 실행 하는 것이다. 따라서 코드의 위치를 주소값으로 표현할 수 있다.
함수의 위치 정보를 가지고 호출하는 방식으로 실행 시킬 수 있다. 이 변수의 선언은 괄호를 이용한다.
함수의 예 :
  int add(int a, int b) { return a +b; }

  int main()
  {
    int (*fun)(int, int);  // 함수 포인터 변수를 선언 한다.
    fun = add;    // fun의 모양이 맞는 함수의 위치 값을 넣을 수 있다.
    printf("0x%08X : %d\n", (int) fun,  fun(10,20) );

    return 0;
  }
함수 포인터를 사용할 때는 위와 같이 인수까지 정의를 해야 한다. 초기와는 달리 현재의 표준화 된 C/C++언어에서는 인수가 달라지면 다른 함수가 되기 때문이다.
그러나 다음과 같이 함수 포인터 변수와 인수가 맞지 않으면 문제가 발생 한다. fun과 cadd함수는 인수가 다르다. 컴파일 오류가 발생 한다.
인수가 달라서 오류가 발생하는 예 :
  int cadd(int a) { return a+10; }

    int (*fun)(int, int);
    fun = cadd; // error C2198: 'int (__cdecl *)(int,int)' : too few arguments for call
결국 함수 포인터 변수는 인수까지 정의가 되어야 한다.
배열 역시 가능하다.
배열 함수 포인터 사용 예:
#include <math.h>#include <stdio.h>#define PI 3.141592653589793double mySin(double r);double (*pMath[])(double) = {
sin, cos, tan,
mySin,
NULL};double mySin(double r){
double rad = r * PI / 180.;

return sin(rad);}int main(int argc, char* argv[], char**env){
int cnt;
double r;

printf("각도 입력 : "); scanf("%lf", &r);
for (cnt = 0;pMath[cnt];cnt++)
printf("0x%08X : %lf\n", pMath[cnt], pMath[cnt](r) );

return 0;}
실행결과 값 예 :
각도 입력 : 45
0x00D510E6 : 0.850904
0x00D51118 : 0.525322
0x00D510E1 : 1.619775
0x00D51159 : 0.707107