Topic 2: 함수 매개변수 전달 방식 📦
🎯 학습 목표
- call-by-value, call-by-address, call-by-reference의 차이를 이해할 수 있다
- 각 전달 방식의 장단점을 파악하고 적절히 선택할 수 있다
- 상황에 맞는 매개변수 전달 방식을 구현할 수 있다
🌟 왜 매개변수 전달 방식이 중요한가?
“함수 매개변수 전달 방식은 프로그램의 성능과 안정성을 결정하는 핵심 요소입니다!”
C++는 세 가지 매개변수 전달 방식을 제공합니다:
- Call by Value (값에 의한 전달)
- Call by Address (주소에 의한 전달 - 포인터)
- Call by Reference (참조에 의한 전달)
📚 세 가지 전달 방식 비교
🎯 핵심 비유
집에 친구를 초대한다고 생각해보세요:
- Call by Value: 집 사진을 보여줌 (복사본)
- Call by Address: 집 주소를 알려줌 (포인터)
- Call by Reference: 집 열쇠를 직접 줌 (참조)
1️⃣ Call by Value (값에 의한 전달)
개념: 함수에 인자의 복사본을 전달합니다.
#include <iostream>
using namespace std;
// Call by Value - 원본은 변경되지 않음
void changeValue(int num) {
num = 100; // 복사본을 변경
cout << "함수 내부: num = " << num << endl;
}
int main() {
int original = 50;
cout << "호출 전: original = " << original << endl;
changeValue(original);
cout << "호출 후: original = " << original << endl; // 여전히 50
return 0;
}
출력:
호출 전: original = 50
함수 내부: num = 100
호출 후: original = 50
장단점
✅ 장점:
- 원본 데이터 보호 (안전)
- 함수 내에서 자유롭게 수정 가능
- 이해하기 쉬움
❌ 단점:
- 큰 객체 복사 시 성능 저하
- 원본 수정 불가능
2️⃣ Call by Address (주소에 의한 전달 - 포인터)
개념: 함수에 변수의 주소를 전달합니다.
#include <iostream>
using namespace std;
// Call by Address - 포인터를 통한 원본 변경
void changeValueByPointer(int* ptr) {
*ptr = 200; // 포인터가 가리키는 원본을 변경
cout << "함수 내부: *ptr = " << *ptr << endl;
}
// 두 값을 교환하는 예제
void swap(int* a, int* b) {
int temp = *a;
*a = *b;
*b = temp;
}
int main() {
int original = 50;
cout << "=== Call by Address 예제 ===" << endl;
cout << "호출 전: original = " << original << endl;
changeValueByPointer(&original); // 주소를 전달
cout << "호출 후: original = " << original << endl; // 200으로 변경됨
cout << "\n=== Swap 예제 ===" << endl;
int x = 10, y = 20;
cout << "교환 전: x = " << x << ", y = " << y << endl;
swap(&x, &y);
cout << "교환 후: x = " << x << ", y = " << y << endl;
return 0;
}
출력:
=== Call by Address 예제 ===
호출 전: original = 50
함수 내부: *ptr = 200
호출 후: original = 200
=== Swap 예제 ===
교환 전: x = 10, y = 20
교환 후: x = 20, y = 10
장단점
✅ 장점:
- 원본 수정 가능
- 큰 객체도 주소만 전달 (효율적)
- NULL 체크 가능
❌ 단점:
- 포인터 문법 복잡 (*와 & 사용)
- NULL 포인터 위험
- 실수로 잘못된 메모리 접근 가능
3️⃣ Call by Reference (참조에 의한 전달)
개념: 함수에 변수의 **별칭(alias)**을 전달합니다.
#include <iostream>
using namespace std;
// Call by Reference - 참조를 통한 원본 변경
void changeValueByReference(int& ref) {
ref = 300; // 참조를 통해 원본을 직접 변경
cout << "함수 내부: ref = " << ref << endl;
}
// 참조를 사용한 swap (더 간단한 문법)
void swapByReference(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}
// const 참조 - 읽기 전용
void printValue(const int& ref) {
cout << "값: " << ref << endl;
// ref = 100; // 에러! const 참조는 수정 불가
}
int main() {
int original = 50;
cout << "=== Call by Reference 예제 ===" << endl;
cout << "호출 전: original = " << original << endl;
changeValueByReference(original); // 그냥 변수를 전달
cout << "호출 후: original = " << original << endl; // 300으로 변경됨
cout << "\n=== Reference Swap 예제 ===" << endl;
int x = 10, y = 20;
cout << "교환 전: x = " << x << ", y = " << y << endl;
swapByReference(x, y);
cout << "교환 후: x = " << x << ", y = " << y << endl;
cout << "\n=== Const Reference 예제 ===" << endl;
printValue(original); // 읽기만 가능
return 0;
}
출력:
=== Call by Reference 예제 ===
호출 전: original = 50
함수 내부: ref = 300
호출 후: original = 300
=== Reference Swap 예제 ===
교환 전: x = 10, y = 20
교환 후: x = 20, y = 10
=== Const Reference 예제 ===
값: 300
장단점
✅ 장점:
- 간단한 문법 (포인터보다 직관적)
- 원본 수정 가능
- NULL이 될 수 없음 (안전)
- const로 읽기 전용 가능
❌ 단점:
- 초기화 후 다른 변수 참조 불가
- 함수 호출 시 원본 변경 여부가 명확하지 않음
🔍 세 가지 방식 종합 비교
#include <iostream>
#include <string>
using namespace std;
struct Student {
string name;
int score;
};
// 1. Call by Value
void updateScoreByValue(Student s, int newScore) {
s.score = newScore; // 복사본만 변경
cout << "By Value - 함수 내부: " << s.name << "의 점수 = " << s.score << endl;
}
// 2. Call by Address (Pointer)
void updateScoreByPointer(Student* s, int newScore) {
if (s != nullptr) { // NULL 체크 필요
s->score = newScore; // 원본 변경
cout << "By Pointer - 함수 내부: " << s->name << "의 점수 = " << s->score << endl;
}
}
// 3. Call by Reference
void updateScoreByReference(Student& s, int newScore) {
s.score = newScore; // 원본 변경
cout << "By Reference - 함수 내부: " << s.name << "의 점수 = " << s.score << endl;
}
// 4. Const Reference (읽기 전용)
void printStudent(const Student& s) {
cout << "학생 정보: " << s.name << ", 점수: " << s.score << endl;
// s.score = 100; // 에러! const 참조는 수정 불가
}
int main() {
cout << "=== 세 가지 전달 방식 비교 ===" << endl;
// 1. Call by Value
Student alice = {"Alice", 85};
cout << "\n[Call by Value]" << endl;
cout << "호출 전: " << alice.name << "의 점수 = " << alice.score << endl;
updateScoreByValue(alice, 95);
cout << "호출 후: " << alice.name << "의 점수 = " << alice.score << endl; // 변경 안됨
// 2. Call by Address
Student bob = {"Bob", 75};
cout << "\n[Call by Address]" << endl;
cout << "호출 전: " << bob.name << "의 점수 = " << bob.score << endl;
updateScoreByPointer(&bob, 90); // & 연산자로 주소 전달
cout << "호출 후: " << bob.name << "의 점수 = " << bob.score << endl; // 변경됨
// 3. Call by Reference
Student charlie = {"Charlie", 80};
cout << "\n[Call by Reference]" << endl;
cout << "호출 전: " << charlie.name << "의 점수 = " << charlie.score << endl;
updateScoreByReference(charlie, 100); // 그냥 변수 전달
cout << "호출 후: " << charlie.name << "의 점수 = " << charlie.score << endl; // 변경됨
// 4. Const Reference (읽기 전용)
cout << "\n[Const Reference - 읽기 전용]" << endl;
printStudent(charlie);
return 0;
}
📊 언제 어떤 방식을 사용할까?
선택 가이드라인
상황 | 추천 방식 | 이유 |
---|---|---|
기본 타입 (int, char, float) | Call by Value | 복사 비용이 적음 |
큰 객체 읽기만 | const Reference | 복사 없이 안전하게 읽기 |
원본 수정 필요 | Reference 또는 Pointer | 직접 수정 가능 |
NULL 가능성 있음 | Pointer | NULL 체크 가능 |
배열 전달 | Pointer | 배열은 포인터로 전달 |
간단한 문법 원함 | Reference | 포인터보다 직관적 |
실무 예제: 효율적인 함수 설계
#include <iostream>
#include <vector>
#include <string>
using namespace std;
class DataProcessor {
public:
// 작은 값: Call by Value
int square(int n) {
return n * n;
}
// 큰 객체 읽기: Const Reference
void printVector(const vector<int>& vec) {
cout << "벡터 내용: ";
for (int val : vec) {
cout << val << " ";
}
cout << endl;
}
// 원본 수정: Reference
void sortVector(vector<int>& vec) {
// 간단한 버블 정렬
for (size_t i = 0; i < vec.size(); i++) {
for (size_t j = 0; j < vec.size() - i - 1; j++) {
if (vec[j] > vec[j + 1]) {
swap(vec[j], vec[j + 1]);
}
}
}
}
// 선택적 매개변수: Pointer (NULL 가능)
void processData(vector<int>* data, string* log = nullptr) {
if (data == nullptr) return;
// 데이터 처리
for (int& val : *data) {
val *= 2;
}
// 로그가 제공되면 기록
if (log != nullptr) {
*log += "Data processed successfully\n";
}
}
};
int main() {
DataProcessor processor;
// 1. 작은 값 전달
cout << "5의 제곱: " << processor.square(5) << endl;
// 2. 큰 객체 읽기
vector<int> numbers = {5, 2, 8, 1, 9};
processor.printVector(numbers);
// 3. 원본 수정
processor.sortVector(numbers);
cout << "정렬 후: ";
processor.printVector(numbers);
// 4. 선택적 매개변수
string log;
processor.processData(&numbers, &log);
cout << "처리 후: ";
processor.printVector(numbers);
cout << "로그: " << log << endl;
return 0;
}
💭 생각해보기
Q1. 왜 C++는 세 가지 방식을 모두 제공할까?
💡 답변
각 방식은 서로 다른 장점이 있습니다:
- Call by Value: 안전성 (원본 보호)
- Call by Pointer: 유연성 (NULL 허용, 동적 할당)
- Call by Reference: 편의성 (간단한 문법)
프로그래머가 상황에 맞는 최적의 방식을 선택할 수 있도록 다양한 옵션을 제공합니다.
Q2. const reference는 언제 사용하나요?
💡 답변
큰 객체를 함수에 전달할 때, 복사 비용을 줄이면서도 원본을 보호하고 싶을 때 사용합니다.
예: void print(const vector<int>& vec)
Last updated on