구조체 (Struct)

구조체란 사용자가 새롭게 정의하는 사용자 정의 자료형이다. 구조체를 이용해 기본 자료형만으로는 표현할 수 없는 복잡한 데이터들을 표현할 수 있다.

예를들어 학생을 나타내는 데이터로는 이름, 학년, 평균 학점 등을 생각할 수 있을 것이다.

char Name[10];    // 이름(문자열)
int Grade;        // 학년(정수형)
double GPA;       // 평균학점(실수형)

그러나 이들의 자료형은 모두 달라 배열로는 묶을 수 없으며 학생 한 명 한 명마다 개별적인 변수들로 정의하여 사용하기에는 매우 번거롭다.

이러한 경우 구조체를 사용할 수 있다.

struct Student
{
    char Name[10];    // 이름(문자열)
    int Grade;        // 학년(정수형)
    double GPA;       // 평균학점(실수형)
};

image.png

배열이 여러 개의 같은 자료형을 하나로 묶는 것이라면 구조체는 서로 다른 자료형을 하나로 묶는 것이다. 이때 구조체를 구성하는 변수들을 구조체의 멤버 또는 멤버 변수라고 한다.

C++의 구조체는 함수까지도 멤버로 가질 수 있으며 객체 지향 프로그래밍의 핵심이 되는 클래스의 기초가 된다.


구조체의 사용

구조체의 정의

구조체는 struct 라는 키워드를 사용하여 다음과 같은 형식으로 정의된다.

struct StructName
{
    MemberValType MemberValName;
    ...
};
struct Student
{
    char Name[10];    // 이름(문자열)
    int Grade;        // 학년(정수형)
    double GPA;       // 평균학점(실수형)
};

구조체 변수 생성

구조체 정의는 구조체 안에 어떤 변수들이 들어간다는 것을 말해주어 새로운 자료형을 생성한 것이기 때문에 변수 선언과 다르다.

즉, 구조체의 정의는 구조체의 형태만 정의한 것이다. 이 형태를 가지고 구조체 변수를 생성할 수 있다. 구조체 변수 생성에는 구조체의 이름을 이용한다.

struct StructName StructValName;
StructName StructValName;
struct Student Student1;    // Student 구조체 변수 Student1 생성
Student Student2;           // Student 구조체 변수 Student2 생성

구조체 변수 초기화

{} 를 통해 구조체 변수를 초기화 할 수 있다.

StructValName = { MemberVal1, MemberVal2, ... };

구조체 정의에서 멤버 변수가 정의된 순서에 따라 차례대로 초기값이 설정되며, 초기값이 설정되지 않은 나머지 멤버 변수들을 기본값으로 초기화된다.

#include <iostream>

// 구조체 선언
struct Student
{
	char Name[10];	// 이름(문자열)
	int Grade;		// 학년(정수형)
	double GPA;		// 평균학점(실수형)
};

int main()
{
	// 구조체 변수 생성
	struct Student Student1;
	Student Student2;

	// 구조체 변수 초기화
	Student1 = { "철수", 2, 3.5 };
	Student2 = { "영희", 3 };

	std::cout << "Student1" << std::endl;
	std::cout << "이름 : " << Student1.Name << std::endl;
	std::cout << "학년 : " << Student1.Grade << std::endl;
	std::cout << "학점 : " << Student1.GPA << std::endl;
	std::cout << std::endl;

	std::cout << "Student2" << std::endl;
	std::cout << "이름 : " << Student2.Name << std::endl;
	std::cout << "학년 : " << Student2.Grade << std::endl;
	std::cout << "학점 : " << Student2.GPA << std::endl;

	return 0;
}
Student1
이름 : 철수
학년 : 2
학점 : 3.5

Student2
이름 : 영희
학년 : 3
학점 : 0

구조체 멤버 접근

구조체에서는 멤버 연산자 . 를 이용해 구조체의 멤버에 접근할 수 있다.

#include <iostream>

// 구조체 선언
struct Student
{
	char Name[10];	// 이름(문자열)
	int Grade;		// 학년(정수형)
	double GPA;		// 평균학점(실수형)
};

int main()
{
	// 구조체 변수 생성
	struct Student Student1;
	Student Student2;

	// 구조체 변수 초기화
	Student1 = { "철수", 2, 3.5 };
	Student2 = { "영희", 3 };

	// 구조체 멤버 접근
	Student2.GPA = 4.5;

	std::cout << "Student1" << std::endl;
	std::cout << "이름 : " << Student1.Name << std::endl;
	std::cout << "학년 : " << Student1.Grade << std::endl;
	std::cout << "학점 : " << Student1.GPA << std::endl;
	std::cout << std::endl;

	std::cout << "Student2" << std::endl;
	std::cout << "이름 : " << Student2.Name << std::endl;
	std::cout << "학년 : " << Student2.Grade << std::endl;
	std::cout << "학점 : " << Student2.GPA << std::endl;

	return 0;
}
Student1
이름 : 철수
학년 : 2
학점 : 3.5

Student2
이름 : 영희
학년 : 3
학점 : 4.5

구조체의 크기

일반적인 경우 구조체의 크기는 모든 멤버들의 크기를 합한 것이지만 항상 그렇지는 않다.

아래의 Student 구조체의 경우 name 은 10 byte, grade 는 4 byte, gpa 는 8 byte 이므로 구조체의 크기는 10 + 4 + 8 = 22 byte 로 예상된다.

#include <iostream>

struct Student
{
	char Name[10];	// 10 byte
	int Grade;		// 4 byte
	double GPA;		// 8 byte
};

int main()
{
	Student Student1;

	std::cout << "sizeof(Name)     : " << sizeof(Student1.Name) << std::endl;
	std::cout << "sizeof(Grade)    : " << sizeof(Student1.Grade) << std::endl;
	std::cout << "sizeof(GPA)      : " << sizeof(Student1.GPA) << std::endl;
	std::cout << "sizeof(Student1) : " << sizeof(Student1) << std::endl;

	return 0;
}
sizeof(Name)     : 10
sizeof(Grade)    : 4
sizeof(GPA)      : 8
sizeof(Student1) : 24

그러나 확인 결과 Student 구조체의 크기는 24 byte로 예상 크기보다 더 큰 값으로 측정되었다.

이는 컴파일러에서 접근 속도를 더 빠르게 하기 위해 더 많은 메모리를 할당한 경우로 이를 바이트 패딩(Byte Padding) 이라고 한다.

따라서 구조체의 크기를 알기 위해서는 sizeof 연산자를 이용하는 것이 정확하다.


구조체의 활용

구조체를 멤버로 가지는 구조체

구조체의 멤버로는 어떤 자료형도 사용 가능하다. 따라서 구조체도 다른 구조체의 멤버가 될 수 있다.

#include <iostream>

// 생년월일 구조체
struct Date
{
	int Year;
	int Month;
	int Day;
};

struct Student
{
	char Name[10];
	int Grade;
	double GPA;
	Date Birth; // 생년월일 구조체를 변수로 가짐
};

int main()
{
	Student Student1;

	// 멤버연산자(.)를 이용해 멤버접근
	Student1.Birth.Year = 1998;
	Student1.Birth.Month = 3;
	Student1.Birth.Day = 15;

	std::cout << "Year  : " << Student1.Birth.Year << std::endl;
	std::cout << "Month : " << Student1.Birth.Month << std::endl;
	std::cout << "Day   : " << Student1.Birth.Day << std::endl;

	return 0;
}
Year  : 1998
Month : 3
Day   : 15

구조체 변수의 대입과 비교

구조체 변수의 대입과 비교에서는 다음과 같은 연산들이 허용된다.

구조체를 다른 구조체에 대입하는 연산을 통해 하나의 구조체 변수에 들어 있는 자료들을 다른 구조체 변수로 복사할 수 있다.

#include <iostream>

// 좌표를 나타내는 구조체
struct Point
{
	int X;
	int Y;
};

int main()
{
	Point P1;
	Point P2;

	P1 = { 3, 4 };
	P2 = P1; // 구조체 변수 대입

	std::cout << "P1(X, Y) : " << P1.X << ", " << P1.Y << std::endl;
	std::cout << "P2(X, Y) : " << P2.X << ", " << P2.Y << std::endl;

	return 0;
}
P1(X, Y) : 3, 4
P2(X, Y) : 3, 4

멤버 변수끼리의 대입 연산 또한 가능하다.

#include <iostream>

// 좌표를 나타내는 구조체
struct Point
{
	int X;
	int Y;
};

int main()
{
	Point P1;
	Point P2;

	P1 = { 3, 4 };
	
	// 멤버 변수 대입
	P2.X = P1.X;
	P2.Y = P1.Y;

	std::cout << "P1(X, Y) : " << P1.X << ", " << P1.Y << std::endl;
	std::cout << "P2(X, Y) : " << P2.X << ", " << P2.Y << std::endl;

	return 0;
}
P1(X, Y) : 3, 4
P2(X, Y) : 3, 4

구조체 변수를 비교하려면 멤버 변수마다 별도의 비교 수식을 적어주어야 한다. 구조체 변수와 구조체 변수를 서로 비교하는 것은 허용되지 않는다.

#include <iostream>

// 좌표를 나타내는 구조체
struct Point
{
	int X;
	int Y;
};

int main()
{
	Point P1;
	Point P2;

	P1 = { 3, 4 };

	P2 = P1;

	if ((P1.X == P2.X) && (P1.Y == P2.Y))
	{
		std::cout << "P1과 P2가 같습니다." << std::endl;
		std::cout << "P1(X, Y) : " << P1.X << ", " << P1.Y << std::endl;
		std::cout << "P2(X, Y) : " << P2.X << ", " << P2.Y << std::endl;
	}
	else
	{
		std::cout << "P1과 P2가 다릅니다." << std::endl;
	}

	// 구조체 변수 끼리의 비교는 컴파일 오류
	//if (P1 == P2)
	//{
	//	std::cout << "P1과 P2가 같습니다." << std::endl;
	//	std::cout << "P1(X, Y) : " << P1.X << ", " << P1.Y << std::endl;
	//	std::cout << "P2(X, Y) : " << P2.X << ", " << P2.Y << std::endl;
	//}

	return 0;
}
P1과 P2가 같습니다.
P1(X, Y) : 3, 4
P2(X, Y) : 3, 4

구조체 배열

구조체 또한 다른 자료형들과 마찬가지로 배열로 사용될 수 있다. 구조체 배열의 선언 방법은 일반적인 배열의 선언 방법과 동일하며 선언과 동시에 초기화할 경우 {} 를 이용해 초기화를 진행할 수 있다.

#include <iostream>

struct Point
{
	int X;
	int Y;
};

int main()
{
	// 구조체의 배열 및 초기화
	Point P[3] =
	{
		{ 0, 1 },    // 첫 번째 요소 초기화
		{ 2, 3 }     // 두 번째 요소 초기화
	};

	P[2] = { 3, 4 }; // 세 번째 요소 초기화

	for (int i = 0; i < 3; i++)
	{
		std::cout << "P[" << i << "] : " << P[i].X << ", " << P[i].Y << std::endl;
	}

	return 0;
}
P[0] : 0, 1
P[1] : 2, 3
P[2] : 3, 4

여러 개의 구조체가 필요할 경우 위처럼 구조체의 배열을 사용할 수 있다.


구조체와 포인터

구조체를 가리키는 포인터

변수를 가리키는 포인터처럼 구조체를 가리키는 포인터도 만들 수 있다.

구조체 포인터는 다음과 같이 선언된다.

#include <iostream>

struct Student
{
	char Name[10];
	int Grade;
	double GPA;
};

int main()
{
	Student NewStudent = { "Kim", 4, 3.8 };

	// 구조체(NewStudent)를 가리키는 포인터 P
	Student* P = &NewStudent;

	return 0;
}

참조 연산자 * 와 멤버 연산자 . 또는 간접 멤버 연산자 -> 를 통해 포인터를 이용하여 구조체의 멤버에 접근할 수 있다.