[C] 구조체, 포인터

해롱그·2023년 9월 2일
1

자료구조

목록 보기
2/6

구조체

복잡한 객체에는 다양한 타입의 데이터들이 한데 묶여져서 있다.
배열(array): 타입이 같은 데이터의 모임
구조체(structure): 타입이 다른 데이터를 묶는 방법
*C언어에서는 struct 키워드를 이용하여 표기

구조체의 형식

struct 구조체이름 {
	항목1;
    항목2;
    ...
};

구조체의 형식이 위와 같이 정의되었다면 구조체 변수는 다음과 같이 생성한다.

struct 구조체이름 구조체변수;

ex) 학생을 나타내는 구조체 형식 정의

  • 문자 배열로 된 이름
  • 나이를 나타내는 정수값
  • 평균평점을 나타내는 실수값
struct studentTag {
	char name[10];	// 문자배열로 된 이름
    int age;		// 나이를 나타내는 정수값
    double gpa;		// 평균평점을 나타내는 실수값
};

위의 문장은 구조체 형식만을 정의한 것이고 실제로 구조체가 만들어진 것은 아니다.
구조체를 만들려면 아래와 같이 해야한다.

struct studentTag s;	// 구조체 변수 선언(s)

구조체 안에 들어있는 멤버를 사용하려면 구조체 변수 뒤에 '.'을 첨가한 후 항목 이름을 적으면 된다. '.'을 멤버연산자(membership operator)라고 한다.

strcpy(s.name, "Kim")
s.age = 20;
s.gpa = 4.3;

C언어에서는 typedef를 사용하여 구조체를 새로운 타입으로 선언하는 것이 가능하다.

typedef studentTag {
	char name[10];	// 문자배열로 된 이름
    int age;		// 나이를 나타내는 정수값
    double gpa;		// 평균평점을 나타내는 실수값
} student;			// 새로운 데이터 타입의 이름(student)

구조체는 중괄호를 사용하여 선언 시에 초기화하는 것이 가능하다.

student s = {"Kim", 20, 4.3}

위의 설명을 토대로 간단한 프로그램을 작성해보자!
💡 example

#include <stdio.h>

typedef struct studentTag {
    char name[10];
    int age;
    double gpa;
} student;

int main(void) {
    student a = {"kim", 20, 4.3};
    student b = {"park", 21, 4.2}; 
    return 0;
}

포인터(pointer)

다른 변수의 주소를 가지고 있는 변수
모든 변수는 메모리 공간에 저장되고 메모리의 각 바이트에는 주소가 매겨져 있다. 바로 이 주소가 포인터에 저장된다.
*모든 변수는 주소를 가지고 있음
컴퓨터 메모리는 바이트로 구성되어 있고 각 바이트마다 순차적으로 주소가 매겨져 있다.

int a = 100;	// int형 변수 a
int *p;			// int형을 가리키는 포인터
p = &a;			// p가 a의 주소를 가리키게 하려면 a의 주소를 p에 대입

변수의 주소는 & 연산자를 변수에 적용시켜서 추출할 수 있다.

포인터와 관련된 연산자

포인터와 관련된 2가지 중요한 연산이 있다.
& 연산자 - 주소 연산자
* 연산자 - 간접참조 연산자 (=역참조 연산자)

  1. & 연산자: 변수의 주소를 추출하는 연산자
    앞에서 선언한 포인터 p가 특정한 변수를 가리키게 하려면 변수의 주소를 & 연산자로 추출하여서 p에 대입한다.
int a;		// 정수형 변수
p = &a;		// 변수의 주소를 포인터에 저장
  1. * 연산자: 포인터가 가리키는 장소에 값을 저장하는 연산자
    예를 들어 p가 가리키는 장소에 200을 저장하려면
*p = 200;

*p와 a가 동일한 메모리 위치를 참조함을 유의해야 한다.
즉, *p와 a는 전적으로 동일하다. 값만 같은 것이 아니고 동일한 실제적인 객체를 가리키기 때문에 *p의 값을 변경하게 되면 a의 값도 바뀌게 된다.

다양한 포인터

int *p;			// p는 int형 변수를 가리키는 포인터
float = *pf;	// pf는 double형 변수를 가리키는 포인터
char = *pc;		// pc는 char형 변수를 가리키는 포인터

NULL 포인터

어떤 객체도 가리키지 않는 포인터
일반적으로 C언어에서 널 포인터는 NULL이라는 매크로로 표시한다. 포인터를 사용하기 전에는 반드시 널 포인터인지를 검사하여야 한다.

if (p == NULL) {
	fprintf(stderr, "오류: 포인터가 아무 것도 가리키지 않습니다.");
    return;
}

포인터가 아무 것도 가리키고 있지 않을 때는 항상 널 포인터 상태로 만들어 두는 것이 좋다.
널 포인터를 가지고 간접참조하려고 하면 컴퓨터 시스템에서 오류가 발생되어서 쉽게 알 수 있기 때문이다.

배열과 포인터

함수로 배열이 전달되면 함수 안에서 배열의 내용을 변경할 수 있다.
배열의 이름이 배열의 시작위치를 가리키는 포인터이기 때문이다.
💡 example

#include <stdio.h>
#define SIZE 6

void get_integers(int list[]) {
    printf("6개의 정수를 입력하시오: ");
    for (int i=0; i<SIZE; ++i) {
        scanf("%d", &list[i]);
    }
}

int cal_sum(int list[]) {
    int sum = 0;
    for (int i=0; i<SIZE; ++i) {
        sum += *(list+i);
    }
    return sum;
}

int main(void) {
    int list[SIZE];
    get_integers(list);
    printf("합 = %d \n", cal_sum(list));
    return 0;
}

동적 메모리 할당

일반적인 배열은 크기가 고정되어 있다.
예를 들어 학생들의 성적을 저장하는 아래의 배열 scores는 크기가 100으로 저장되어 있다.

int scores = [100];

이렇게 고정된 크기 때문에 많은 문제가 발생한다. 흔히 프로그램을 작성할 당시에는 얼마나 많은 입력이 있을지를 알 수 없기 때문이다. 만약 처음에 결정된 크기보다 더 큰 입력이 들어온다면 처리하지 못할 것이고, 더 작은 입력이 들어온다면 남은 메모리 공간은 낭비될 것이다.

따라서 이러한 문제들을 해결하기 위하여 C 언어에서는 필요한 만큼의 메모리를 운영체제로부터 할당받아서 사용하고,사용이 끝나면 시스템에 메모리를 반납하는 기능이 있다.
이를 동적 메모리 할당(dynamic memory allocation)이라고 한다.

동적 메모리가 할당되는 공간을 heap이라고 한다. 힙은 운영체제가 사용되지 않는 메모리 공간을 모아 놓은 곳이다.
필요한 만큼만 할당을 받고 또 필요한 때에 사용하고 반납하기 때문에 메모리를 매우 효율적으로 사용할 수 있다.

int *p;
p = (int*)malloc(sizeof(int));	// 1. 동적 메모리 할당
*p = 1000;						// 2. 동적 메모리 사용
free(p);						// 3. 동적 메모리 반납
  1. malloc() 함수의 반환되는 주소의 타입은 void*이므로 이를 적절한 포인터로 형변환시켜야 한다. 메모리 확보가 불가능하면 NULL을 함수의 반환값으로 반환한다.

  2. 동적 메모리는 포인터로만 사용할 수 있다. *p는 p가 가리키는 장소이다.
    *p=1000; 문장을 실행하면 p가 가리키는 장소에 1000이 저장된다.

  3. free() 함수는 할당된 메모리 블록을 운영체제에게 반환한다.
    🚨 malloc() 함수가 반환했던 포인터 값을 잊어버리면 안된다! 포인터값을 잊어버리면 동적 메모리를 반환할 수 없다.
    malloc()은 시스템의 메모리가 부족해서 요구된 메모리를 할당할 수 없으면 NULL을 반환한다. 따라서 malloc()의 반환값은 항상 NULL인지 검사하여야 한다. 정수 10개를 저장할 수 있는 메모리를 동적으로 할당해보자.

💡 example

#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>		// malloc을 이용하여 정수 10을 저장할 수 있는 동적메모리를 할당하고 free를 이용하여 메모리를 반납한다.

#define SIZE 10

int main(void) {
    int *p;

    p = (int*)malloc(SIZE * sizeof(int));
    if (p == NULL) {
        fprintf(stderr, "메모리가 부족해서 할당할 수 없습니다.\n");
        exit(1);
    }

    for (int i=0; i<SIZE; i++)
        p[i] = i;

    for (int i=0; i<SIZE; i++)
        printf("%d ", p[i]);
    printf("\n");
    free(p);
    return 0;
}
>>> 0 1 2 3 4 5 6 7 8 9

구조체와 포인터

구조체에 대한 포인터를 선언하고 포인터를 통하여 구조체 멤버에 접근할 수 있다.
여기서 하나 주의할 것은 포인터를 통하여 구조체의 멤버에 접근하는 편리한 표기법 ->이다.
ps가 구조체를 가리키는 포인터라고 할 때,
(*ps).data 보다 ps->data 라고 쓰는 것이 더 편리하다.

자료구조에서 구조체에 대한 포인터도 자주 함수의 매개변수로 전달된다. 구조체 자체를 함수로 전달하는 경우, 구조체가 함수로 복사되어 전달되기 때문에, 큰 구조체의 경우에는 구조체 포인터를 전달하는 것이 좋다.

동적 메모리 할당을 이용하여 구조체를 생성하고 여기에 데이터를 저장해보자.
동적으로 생성된 구조체는 포인터를 통해서만 접근할 수 있다.

💡 example 1

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

typedef struct studentTag {
   char name[10];   // 문자배열로 된 이름
   int age;         // 나이를 나타내는 정수값
   double gpa;      // 평균평점을 나타내는 실수값
} student;

int main(void) {
    student *s;     // 포인터 변수 's'는 학생정보를 저장하는 구조체 변수를 가리키기 위한 포인터

    s = (student*)malloc(sizeof(student));
    if (s == NULL) {
        fprintf(stderr, "메모리가 부족해서 할당할 수 없습니다.\n");
        exit(1);
    }

    strcpy(s->name, "Park");
    s->age = 20;
    s->gpa = 4.3;
    printf("%s\n", s->name);    // Park
    printf("%d\n", s->age);     // 20
    printf("%.1f\n", s->gpa);   // 4.3
    free(s);
    return 0;
}

💡 example 2

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

int main(void) {
    double *p1;
    p1 = (double*)malloc(sizeof(double));
    *p1 = 23.92;
    printf("%.2f\n", *p1);      // 23.92
    free(p1);
    return 0;
}
profile
사랑아 컴퓨터해 ~

0개의 댓글