42 Starter 스터디 모임에서 진행하는 C 언어 A to Z 스터디에 내용을 정리해서 포스팅을 진행합니다.

해당 스터디에서는 42 Piscine 과정에서 채우지못한 부족한 C언어 역량을 키우는 것을 목적으로 합니다.


구조체

구조체란, 데이터를 하나로 묶어놓은 것으로 클래스로 넘어가기전에 C언어에서 발전단계로 살펴볼 수 있다.

컴파일러에서 찾지 못하는 매개변수에 순서에 대한 실수를 줄이는 방법, 매개변수의 수를 줄이는 방법으로 사용할 수 있다.

  • 구조체 사용 방법
      struct date {
          int day;
          int month;
          int year;
      }; // 세미콜론 필수 !
    
      struct date date; // 구조체 변수 선언
      //현재 구조체 지역변수는 쓰레기 값들을 갖고있다.
    
      // 멤버 변수 접근하는 방법은 .연산자를 사용한다.
      date.year = 2043;
      date.month = 10;
      date.day = 1
    
  • 별명 붙이기 typedef

    typedef 자료형 별명 순으로 자료형을 별명으로 사용할 수 있다.

    3 가지 사용법을 소개하자면 다음과 같다.

      // typedef 사용방법 1
      struct date {
          int day;
          int month;
          int year;
      };
    
      // struct date 자료형을 date_t라고 별명을 붙인다.
      typedef struct date t_date;
    
      date_t date;
    
      // typedef 사용방법 2
      typedef struct date {
          int year;
          int month;
          int day;
      } date_t;
    
      // typedef 사용방법 3
      // 방법 3은 struct date date; 와 같은 방식으로 변수 선언을 못한다.
      typedef struct {
          int year;
          int month;
          int day;
      } date_t;
    
  • 간편한 구조체 초기화

    직접 0을 대입하거나 아래와 같은 방법으로 초기화가 가능하다.

      date_t date = { 0, };
    
      // 아래와 같은 나열 방식으로 초기화하는 방법은 피하자.
      // 원소의 순서가 섞이는 실수를 유발할 수 있다.
      // 단, const 형은 아래와 같이 사용해야 하는데, const형을 멤버 변수로 사용자체를 피하자.
      date_t date = { 2043, 10, 1 };
    
    
  • 참조형 구조체의 멤버 변수 접근

      typedef struct date {
          int year;
          int month;
          int day;
      } date_t;
    
      date_t *date;
    

    위와 같은 참조형 구조체 변수가 있다고 했을때, 멤버변수 접근은 화살표를 이용할 수 있다.

      // .의 우선순위가 1 *의 우선순위 2 이므로 ()를 꼭 사용해야한다.
      // -> 연산자의 우선순위는 1순위다.
      // 아래 3개는 모두 같다.
      (*date).year = (*date).year + 1;
      date->year = date->year + 1;
      date->year++;
    
  • 구조체에서 얕은 복사로 문자열 복사

    얕은 복사를 통해서 구조체에 있는 문자열을 복사하고 싶을때에는 배열을 이용하여 꼼수로 사용할 수 있다.

      // 상수 정의
      enum { NAME_LEN = 32};
      typedef struct {
          char firstname[NAME_LEN];
          char lastname[NAME_LEN];
      } name_t;
    
      size_t size;
      size = sizeof(name_t) // => 64byte 값을 나타냄
    
      // 얕은 복사를 통해서 문자열이 복사된 것을 확인할 수 있다.
      void check_size(name_t name)
      {
          size_t size;
          size = sizeof(name); // => 64byte 값을 나타냄
      }
    
  • 구조체 메모리 크기

    아래의 코드를 보고 struct user_info가 메모리를 몇 바이트 차지할지 생각해보자.

      enum { NAME_LEN = 32};
      typedef struct {
          char firstname[NAME_LEN];
          char lastname[NAME_LEN];
      } name_t;
    
      struct user_info {
          unsigned int id;
          name_t name;
          unsigned short height;
          float weight;
          unsigned short age;
      };
    

    일반적으로 자료형의 크기를 생각하여 예측을 해보면 4 + 64 + 2 + 4 + 2 의 크기로 총 76 byte라고 생각할 수 있다.

      // 결과 확인 코드
      enum { NAME_LEN = 32};
      typedef struct {
          char firstname[NAME_LEN];
          char lastname[NAME_LEN];
      } name_t;
    
      struct user_info {
          unsigned int id;
          name_t name;
          unsigned short height;
          float weight;
          unsigned short age;
      }user_info_t;
    
      user_info_t info;
    
      int off_id = (char*)&info.id - (char*)&info;		/* 0 */
      int off_name = (char*)&info.name - (char*)&info;	/* 4 */
      int off_height = (char*)&info.height - (char*)&info;/* 68 */
      int off_weight = (char*)&info.weight - (char*)&info;/* 72 */
      int off_age = (char*)&info.age - (char*)&info;		/* 76 */
    

    하지만, 각 시스템마다 메모리를 접근할 때 메모리 접근 효율성을 위해 n바이트 배수인 시작주소에서만 메모리를 접근 가능하도록 되어있다. x86 시스템은 4byte(워드크기) 경계에서 읽어오는게 효율적이므로 위에 코드에서는 short 같은 자료형이 있더라도 4바이트 단위로 메모리에 접근하여 저장한다. 이렇기 때문에 동일한 n바이트 단위로 끊어지게끔 변수의 순서를 변경하거나 표준은 아니지만 요즘 컴파일러들이 잘 지원해주는 #pragma pack(push, 1) 를 사용하는 방법도 있다.

  • 비트 필드

    비트 필드는 자료형의 모든 바이트를 사용하지 않고 비트 단위로 사용하는 방법을 말한다.

      typedef struct {
          unsigned char b0 : 1; // : 1은 1 비트만 사용하겠다를 말한다.
          unsigned char b1 : 1;
          unsigned char b2 : 1;
          unsigned char b3 : 1;
          unsigned char b4 : 1;
          unsigned char b5 : 1;
          unsigned char b6 : 1;
          unsigned char b7 : 1;
      } bitflags_t
    
      sizeof(bitflags_t) // => 1byte 이다.
    

공용체

공용체란 똑같은 메모리 위치에 다른 변수로 접근하는 방법이다. 즉, 공용체 안에 있는 여러 변수들이 같은 메모리를 공유한다.

아래 예시코드를 보고 이해하보도록 하자.

typedef union {
	unsigned char val;
	typedef struct {
		unsigned char b0 : 1; // : 1은 1 비트만 사용하겠다를 말한다.
		unsigned char b1 : 1;
		unsigned char b2 : 1;
		unsigned char b3 : 1;
		unsigned char b4 : 1;
		unsigned char b5 : 1;
		unsigned char b6 : 1;
		unsigned char b7 : 1;
	} bits;
} bitsflags_t;

int main(void)
{
	int is_same;
	int is_zero;
	bitflags_t flags = {0, };

	flags.bits.b1 = 1;
	flags.bits.b4 = 1;
	is_name = (flags.bits.b1 == flags.bits.b7);
	is_zero = (flag.val == 0);
}

공용체에 들어있는 valbits 는 같은 포인터 주소라고 생각할 수 있다. 즉 valbits 가 가리키는 곳이 같다는 것이다. val 는 해당 포인터 주소를 unsigned char 로 읽겠다는 것이고 bits 는 비트 필드의 값으로 사용하겠다는 것으로 같은 메모리를 다른 변수로 공유하는 것이다.

그렇기 때문에 비트 필드에서 못했던 flags.bits == 0 이나 flags.bits = 0xFF 과 같은 연산을 flags.val == 0 과 같이 이용하여 할 수 있다.

예시로 RGBA#FFFFFFFF 로 사용하거나 R, G, B, A 를 각각 사용하고자 할 때 공용체로 이용하여 할 수 있다.

또한 한 메모리 공간을 용도에 따라 다른 기본 데이터형으로 읽을 때 사용하기도 한다.

함수 포인터

함수 포인터란 함수를 가리키는 주소로 함수가 시작하는 코드가 있는 주소를 의미한다.

함수의 데이터 형은 어떻게 정의해야하는지 알아보자.

함수는 다양한 매개변수와 다양한 반환형이 존재하기 때문에 하나로 정의할 수가 없고 함수 포인터가 매개변수로 전달이 되거나 해석되기위해서 이 함수의 반환 형과 매개변수의 형태를 갖고 있어야한다.

따라서 다음과 같은 형태로 함수 포인터를 변수로 사용할 수 있고 매개변수로도 넘겨줄 수 있다.

double add(double x, double y)
{
	return x + y;
}
// add에 반환형과 매개변수를 맞추어 아래와 같이 선언
double (*func)(double, double) = add;

// 매개변수를 함수 포인터로 하는 함수 예시

double calculate(double, double, double (*)(double, double));
double calculate(double x, double y, double (*func)(double, double))
{
	return func(x, y);
}
// double op1, op2 생략
double result = calculate(op1, op2, add);

*추가정보: 어떠한 포인터의 자료형이든 다 매개변수로 다 받을 수 있도록 사용하는 범용 포인터라는 것이 있다. void* 자료형으로 사용되며 값을 참조하는 것은 캐스팅을 통해 해야한다. 예를 들어 void* temp 를 매개변수로 받고 *(int *)temp 와 같이 정수형으로 값을 참조해서 사용할 수 있다.

⤧  Next post ft_printf 구현하기 ⤧  Previous post Oh-My-C-Lang - Section 6 - 05