Topic 2: 구조체 배열과 포인터 📚➡️
🎯 학습 목표
- 구조체 배열을 생성하고 활용할 수 있다
- 포인터를 사용해서 구조체에 접근하는 방법을 익힐 수 있다
- 함수 매개변수로 구조체를 전달하는 방법을 이해할 수 있다
- 동적 메모리 할당을 통한 구조체 관리를 할 수 있다
🤔 왜 구조체 배열이 필요할까요?
학급의 모든 학생 정보를 관리한다고 생각해보세요. 한 명씩 변수를 만든다면…
// ❌ 비효율적인 방법
Student student1 = {"김철수", 20, "2024001", 3.8};
Student student2 = {"이영희", 19, "2024002", 4.2};
Student student3 = {"박민수", 21, "2024003", 3.5};
// ... 30명이라면 30개의 변수? 😱
이런 문제를 구조체 배열로 해결할 수 있습니다! 🎯
📋 구조체 배열의 기초
구조체 배열 선언과 초기화
#include <iostream>
#include <string>
using namespace std;
struct Student {
string name;
int age;
string id;
double grade;
};
int main() {
// 방법 1: 배열 선언 후 개별 초기화
Student students[3];
students[0] = {"김철수", 20, "2024001", 3.8};
students[1] = {"이영희", 19, "2024002", 4.2};
students[2] = {"박민수", 21, "2024003", 3.5};
// 방법 2: 선언과 동시에 초기화
Student class1[3] = {
{"김철수", 20, "2024001", 3.8},
{"이영희", 19, "2024002", 4.2},
{"박민수", 21, "2024003", 3.5}
};
// 배열 크기 자동 결정
Student class2[] = {
{"최지우", 22, "2024004", 3.9},
{"한민정", 20, "2024005", 4.0}
}; // 크기가 자동으로 2가 됨
return 0;
}
🔄 구조체 배열 순회하기
기본적인 for 반복문
#include <iostream>
#include <string>
using namespace std;
struct Product {
string name;
int price;
int stock;
string category;
};
int main() {
Product inventory[] = {
{"노트북", 1200000, 5, "전자제품"},
{"마우스", 50000, 20, "전자제품"},
{"키보드", 80000, 15, "전자제품"},
{"의자", 300000, 8, "가구"},
{"책상", 500000, 3, "가구"}
};
int arraySize = sizeof(inventory) / sizeof(inventory[0]);
cout << "📦 === 재고 현황 === 📦" << endl;
cout << "--------------------------------------" << endl;
for (int i = 0; i < arraySize; i++) {
cout << "상품명: " << inventory[i].name << endl;
cout << "가격: " << inventory[i].price << "원" << endl;
cout << "재고: " << inventory[i].stock << "개" << endl;
cout << "카테고리: " << inventory[i].category << endl;
cout << "--------------------------------------" << endl;
}
return 0;
}
범위 기반 for 반복문 (C++11)
#include <iostream>
#include <string>
#include <vector>
using namespace std;
struct Book {
string title;
string author;
int pages;
double rating;
};
int main() {
vector<Book> library = {
{"해리포터", "J.K. 롤링", 400, 4.8},
{"반지의 제왕", "톨킨", 500, 4.9},
{"1984", "조지 오웰", 300, 4.7},
{"위대한 개츠비", "피츠제럴드", 250, 4.5}
};
cout << "📚 === 도서관 목록 === 📚" << endl << endl;
// 범위 기반 for문 - 읽기 전용
for (const auto& book : library) {
cout << "📖 " << book.title << endl;
cout << " 저자: " << book.author << endl;
cout << " 페이지: " << book.pages << "페이지" << endl;
cout << " 평점: " << book.rating << "/5.0" << endl;
// 평점에 따른 별표 표시
cout << " 별점: ";
int stars = (int)(book.rating);
for (int i = 0; i < stars; i++) {
cout << "⭐";
}
cout << endl << endl;
}
// 평점이 4.7 이상인 책만 필터링
cout << "🏆 === 추천 도서 (평점 4.7 이상) === 🏆" << endl;
for (const auto& book : library) {
if (book.rating >= 4.7) {
cout << "- " << book.title << " (평점: " << book.rating << ")" << endl;
}
}
return 0;
}
🎯 포인터를 사용한 구조체 접근
구조체 포인터 기본 사용법
#include <iostream>
#include <string>
using namespace std;
struct Car {
string brand;
string model;
int year;
double price;
};
// 구조체를 출력하는 함수 (포인터 매개변수)
void printCarInfo(const Car* carPtr) {
// 포인터를 통한 멤버 접근: -> 연산자 사용
cout << "🚗 === 자동차 정보 === 🚗" << endl;
cout << "브랜드: " << carPtr->brand << endl;
cout << "모델: " << carPtr->model << endl;
cout << "연식: " << carPtr->year << "년" << endl;
cout << "가격: " << carPtr->price << "만원" << endl;
cout << "========================" << endl;
}
// 구조체를 수정하는 함수 (포인터 매개변수)
void updateCarPrice(Car* carPtr, double newPrice) {
carPtr->price = newPrice;
cout << "💰 " << carPtr->brand << " " << carPtr->model
<< "의 가격이 " << newPrice << "만원으로 변경되었습니다!" << endl;
}
int main() {
Car myCar = {"현대", "아반떼", 2023, 2500.0};
// 구조체 포인터 선언
Car* carPtr = &myCar;
// 포인터를 통한 접근 방법 비교
cout << "일반 접근: " << myCar.brand << endl; // 점(.) 연산자
cout << "포인터 접근: " << carPtr->brand << endl; // 화살표(->) 연산자
cout << "포인터 접근 (다른 방식): " << (*carPtr).brand << endl; // 역참조 후 점 연산자
cout << endl;
// 함수 호출
printCarInfo(carPtr);
updateCarPrice(carPtr, 2800.0);
printCarInfo(carPtr);
return 0;
}
🎯 포인터로 구조체 다루기: 핵심 개념
왜 포인터를 사용하는가?
구조체를 함수에 전달할 때 두 가지 방법이 있습니다:
struct Player {
string name;
int level;
int experience;
};
// 방법 1: 값으로 전달 (복사)
void printPlayer1(Player p) { // 전체 구조체를 복사!
cout << p.name << ": 레벨 " << p.level << endl;
}
// 방법 2: 포인터로 전달 (주소)
void printPlayer2(Player* p) { // 주소만 전달!
cout << p->name << ": 레벨 " << p->level << endl;
}
포인터의 장점
⚡ 메모리 효율성: 큰 구조체도 주소(8바이트)만 전달
🔧 수정 가능: 원본 데이터를 직접 수정할 수 있음
📈 성능 향상: 복사 시간이 없어서 빠름
화살표 연산자 (->
)
포인터로 구조체 멤버에 접근할 때 사용하는 특별한 연산자입니다:
Player player = {"김철수", 5, 100};
Player* ptr = &player;
// 방법 1: 화살표 연산자 (권장)
ptr->name = "이영희";
ptr->level = 10;
// 방법 2: 역참조 + 점 연산자 (복잡함)
(*ptr).name = "이영희";
(*ptr).level = 10;
실습: 레벨업 시스템
#include <iostream>
using namespace std;
struct Character {
string name;
int level;
int exp;
};
// 여기에 코드를 작성하세요
// 1. levelUp 함수를 만들어보세요 (포인터 매개변수 사용)
// 2. 레벨을 1 증가시키고 경험치를 0으로 초기화
void levelUp(Character* c) {
// 코드를 작성하세요
}
int main() {
Character hero = {"용사", 1, 100};
cout << "레벨업 전: " << hero.name << " 레벨 " << hero.level << endl;
levelUp(&hero); // 주소를 전달
cout << "레벨업 후: " << hero.name << " 레벨 " << hero.level << endl;
return 0;
}
구조체 배열과 포인터
Player team[3] = {
{"전사", 5, 200},
{"마법사", 3, 150},
{"궁수", 4, 180}
};
// 배열의 각 원소에 접근
for (int i = 0; i < 3; i++) {
cout << team[i].name << endl; // 직접 접근
cout << (team + i)->name << endl; // 포인터 연산
}
// 포인터로 함수에 전달
void processTeam(Player* players, int size) {
for (int i = 0; i < size; i++) {
players[i].level++; // 모든 팀원 레벨업!
}
}
🤔 흔한 실수와 해결방법
❌ null 포인터 접근
Player* p = nullptr;
cout << p->name; // 💥 프로그램 크래시!
✅ 안전한 접근
if (p != nullptr) {
cout << p->name; // 안전함
}
❌ 점 연산자와 화살표 연산자 혼동
Player player;
Player* ptr = &player;
player->name; // ❌ 변수에 화살표 사용
ptr.name; // ❌ 포인터에 점 사용
✅ 올바른 사용
player.name; // ✅ 변수에는 점
ptr->name; // ✅ 포인터에는 화살표
💾 동적 메모리와 구조체
동적 할당의 필요성
정적 배열의 한계:
struct Student {
string name;
int score;
};
Student class1[30]; // 고정 크기: 30명만 가능
Student class2[50]; // 또 다른 고정 크기
동적 할당의 해결책:
int studentCount;
cout << "학생 수를 입력하세요: ";
cin >> studentCount;
// 실행 중에 크기 결정!
Student* students = new Student[studentCount];
new와 delete 사용법
// 1. 단일 구조체 동적 할당
Student* student = new Student;
student->name = "김철수";
student->score = 95;
delete student; // 메모리 해제
// 2. 구조체 배열 동적 할당
Student* students = new Student[count];
students[0].name = "이영희";
students[0].score = 88;
delete[] students; // 배열 메모리 해제
🚨 메모리 관리 원칙
1. 짝을 맞춰라!
new ↔ delete // 단일 객체
new[] ↔ delete[] // 배열
2. 해제 후 사용 금지
Student* s = new Student;
delete s;
s->name = "금지!"; // ❌ 이미 해제된 메모리 접근
s = nullptr; // ✅ 안전을 위해 nullptr 설정
3. 중복 해제 금지
delete s;
delete s; // ❌ 두 번 해제하면 프로그램 크래시!
실습: 동적 점수 관리
#include <iostream>
using namespace std;
struct Score {
string subject;
int points;
};
int main() {
int subjectCount;
cout << "과목 수를 입력하세요: ";
cin >> subjectCount;
// 여기에 코드를 작성하세요
// 1. Score 배열을 동적 할당하세요
// 2. 각 과목 정보를 입력받으세요
// 3. 모든 과목을 출력하세요
// 4. 메모리를 해제하세요
return 0;
}
스마트 포인터 미리보기
C++에서는 자동 메모리 관리를 위한 스마트 포인터도 제공합니다:
#include <memory>
// 자동으로 메모리 해제됨!
unique_ptr<Student> student = make_unique<Student>();
student->name = "자동 관리";
// delete 불필요 - 자동으로 해제됨
하지만 이건 고급 내용이니 나중에 배우게 됩니다! 지금은 new/delete의 원리를 확실히 이해하는 것이 중요해요.
🔧 함수와 구조체 배열
다양한 전달 방법
#include <iostream>
#include <string>
using namespace std;
struct Score {
string subject;
int points;
double weight; // 가중치
};
// 1. 값으로 전달 (비효율적)
void printScoreByValue(Score score) {
cout << "과목: " << score.subject << ", 점수: " << score.points << endl;
}
// 2. 참조로 전달 (효율적, 수정 가능)
void updateScore(Score& score, int newPoints) {
score.points = newPoints;
cout << score.subject << " 점수가 " << newPoints << "점으로 업데이트되었습니다!" << endl;
}
// 3. 상수 참조로 전달 (효율적, 읽기 전용)
void printScoreByConstRef(const Score& score) {
cout << "📚 " << score.subject << ": " << score.points
<< "점 (가중치: " << score.weight << ")" << endl;
}
// 4. 배열 전달 (포인터로 전달됨)
double calculateWeightedAverage(const Score scores[], int count) {
double totalWeightedScore = 0;
double totalWeight = 0;
for (int i = 0; i < count; i++) {
totalWeightedScore += scores[i].points * scores[i].weight;
totalWeight += scores[i].weight;
}
return totalWeightedScore / totalWeight;
}
// 5. vector 참조로 전달
void sortScoresByPoints(vector<Score>& scores) {
// 간단한 버블 정렬
for (int i = 0; i < scores.size() - 1; i++) {
for (int j = 0; j < scores.size() - 1 - i; j++) {
if (scores[j].points < scores[j + 1].points) {
Score temp = scores[j];
scores[j] = scores[j + 1];
scores[j + 1] = temp;
}
}
}
}
int main() {
// 성적 배열 생성
Score myScores[] = {
{"수학", 85, 1.2},
{"영어", 92, 1.0},
{"과학", 78, 1.1},
{"국어", 88, 1.0},
{"사회", 95, 0.8}
};
int scoreCount = sizeof(myScores) / sizeof(myScores[0]);
cout << "📊 === 성적 관리 시스템 === 📊" << endl << endl;
// 모든 성적 출력
cout << "현재 성적:" << endl;
for (int i = 0; i < scoreCount; i++) {
printScoreByConstRef(myScores[i]);
}
// 가중 평균 계산
double average = calculateWeightedAverage(myScores, scoreCount);
cout << "\n📈 가중 평균: " << average << "점" << endl;
// 점수 수정
cout << "\n=== 점수 수정 ===" << endl;
updateScore(myScores[0], 90); // 수학 점수 수정
// vector로 복사해서 정렬
vector<Score> scoreVector(myScores, myScores + scoreCount);
sortScoresByPoints(scoreVector);
cout << "\n🏆 === 점수 순위 === 🏆" << endl;
for (int i = 0; i < scoreVector.size(); i++) {
cout << (i + 1) << "등: ";
printScoreByConstRef(scoreVector[i]);
}
return 0;
}
💡 성능과 메모리 최적화 팁
구조체 크기 최적화
#include <iostream>
using namespace std;
// ❌ 비효율적인 구조체 (패딩 때문에 메모리 낭비)
struct BadStruct {
char a; // 1 byte
int b; // 4 bytes (3 bytes 패딩 발생)
char c; // 1 byte (3 bytes 패딩 발생)
};
// ✅ 효율적인 구조체 (패딩 최소화)
struct GoodStruct {
int b; // 4 bytes
char a; // 1 byte
char c; // 1 byte (2 bytes 패딩)
};
// 더 나은 방법: 관련 데이터끼리 묶기
struct OptimalStruct {
int numbers[10]; // 40 bytes
char flags[4]; // 4 bytes
// 총 44 bytes, 패딩 최소화
};
int main() {
cout << "구조체 크기 비교:" << endl;
cout << "BadStruct: " << sizeof(BadStruct) << " bytes" << endl;
cout << "GoodStruct: " << sizeof(GoodStruct) << " bytes" << endl;
cout << "OptimalStruct: " << sizeof(OptimalStruct) << " bytes" << endl;
return 0;
}
🧪 퀴즈 타임!
🤔 퀴즈 1: 구조체 배열에서 특정 조건의 요소 개수는?
struct Item {
string name;
int price;
bool available;
};
Item items[] = {
{"사과", 1000, true},
{"바나나", 2000, false},
{"오렌지", 1500, true},
{"포도", 3000, true}
};
// 가격이 2000원 미만이고 구매 가능한 아이템은 몇 개?
정답: 2개 (사과, 오렌지)
조건을 만족하는 아이템: 사과(1000원, available), 오렌지(1500원, available)
🤔 퀴즈 2: 포인터를 통한 구조체 접근에서 올바른 문법은?
struct Point {
int x, y;
};
Point p = {10, 20};
Point* ptr = &p;
다음 중 올바른 접근 방법들은?
ptr->x
(*ptr).x
ptr.x
&ptr->x
정답: 1번과 2번이 올바릅니다.
ptr->x
: 화살표 연산자 (가장 일반적)(*ptr).x
: 역참조 후 점 연산자ptr.x
: ❌ 잘못된 문법&ptr->x
: x의 주소를 가져오는 것 (값 접근과는 다름)
🤔 퀴즈 3: 다음 코드의 출력 결과는?
struct Data {
int value;
};
void modify1(Data d) {
d.value = 100;
}
void modify2(Data* d) {
d->value = 200;
}
int main() {
Data data = {10};
modify1(data);
cout << data.value << " ";
modify2(&data);
cout << data.value << endl;
return 0;
}
정답: 10 200
modify1
은 값으로 전달받으므로 원본이 변경되지 않음 (10 유지)modify2
는 포인터로 전달받으므로 원본이 변경됨 (200으로 변경)
🎯 연습 문제
연습 1: 영화 평점 시스템
영화 정보(제목, 감독, 연도, 평점)를 담는 구조체 배열을 만들고, 평점 순으로 정렬하여 상위 3개 영화를 출력하는 프로그램을 작성하세요.
💡 힌트
- Movie 구조체를 정의하세요
- 평점을 기준으로 내림차순 정렬하세요
- 상위 3개만 출력하는 반복문을 작성하세요
연습 2: 학생 성적 분석기
학생들의 성적 정보를 동적으로 입력받고, 각 과목별 평균과 학생별 총점을 계산하는 프로그램을 포인터를 사용하여 작성하세요.
💡 힌트
- 동적 메모리 할당을 사용하세요
- 함수 매개변수로 포인터를 활용하세요
- 과목별/학생별 계산 함수를 분리하세요
연습 3: 온라인 쇼핑몰 장바구니
상품 정보와 장바구니 기능을 구현하여, 상품 추가/삭제, 총 금액 계산, 할인 적용 등의 기능을 포인터와 구조체 배열로 구현하세요.
💡 힌트
- Product와 CartItem 구조체를 정의하세요
- vector를 사용해서 동적 장바구니를 구현하세요
- 포인터를 사용해서 상품 정보에 효율적으로 접근하세요
🎉 마무리
구조체 배열과 포인터는 효율적인 데이터 관리의 핵심입니다!
오늘 배운 내용:
- 📚 구조체 배열의 생성과 초기화
- 🔄 다양한 반복문을 통한 배열 순회
- 🎯 포인터를 통한 구조체 접근 (-> 연산자)
- 💾 동적 메모리 할당과 구조체
- 🔧 함수 매개변수로 구조체 전달하기
- ⚡ 성능 최적화 방법
다음 단계:
- Topic 3에서는 열거형(enum)과 공용체(union)를 학습합니다
- 더 고급 데이터 타입들을 다루게 됩니다
- 복합적인 자료구조 설계를 배우게 됩니다
이제 구조체를 배열로 관리하고 포인터로 효율적으로 접근하는 방법을 익혔습니다! 💪