태터데스크 관리자

도움말
닫기
적용하기   첫페이지 만들기

태터데스크 메시지

저장하였습니다.

블로그 이미지
어릴 적부터 저의 별명은 별다른 거 없이 성을 따서 "쩐" 이였음다. "야~~ 쩐!!!"... 이젠 저의 세상을 이 곳에서 만들어 볼려고 하고 있습니다.
쩐의시대

글 보관함

calendar

      1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31  

8. 참조

2009/07/01 13:23 | Posted by 쩐의시대
'6장. 6. 생성자와 소멸자를 이용한 향상된 추상화 - 1, 2'에서 보았듯이 디폴트 생성자와 소멸자는 매개변수를 갖지 않고 리턴값도 없다. 다음 장에서 소개할 다른 특별한 멤버 함수들은 매개변수와 리턴값을 필요로 한다. 그러나 이 함수들은 이러한 정보들을 이 장에서 소개할 참조(reference)라는 새로운 방법으로 전달한다. 우선 C에서도 사용했던 값에 의한 전달 (passing by value)을 살펴보고, 그 다음에 참조를 사용하여 정보를 전달하는 새로운 방법에 대해 설명한다. 마지막으로, 정보를 전달하는 이러한 새로운 방법에 있어서 몇 가지 한계점과 문제점을 살펴보도록 한다. 중간에 참조의 사촌격인 포인터와의 차이점도 살펴보도록 한다.

매개변수와 인자
매개변수(parameter) : 함수에 전달되는 값과 그 값을 전달받는 함수 안의 모든 변수
     -> 실매개변수(actual parameter) : 함수에 전달되는 값
     -> 형식 매개변수(formal parameter) : 그 값을 전달받는 함수 안의 모든 변수
Let, 인자(arguement) : 전달되는 값
       매개변수(parameter) : 그 값을 전달받는 함수 안의 변수

C와 C++에서의 값에 의한 전달
C에서는 모든 인자들이 값에 의해 전달된다. 이것은 함수가 호출될 때 인자값이 매개변수에 복사되는 것을 의미한다. 그 후에는 인자와 매개변수는 아무런 관련도 없다. 특히 매개변수의 값을 변화시키는 것은 인자의 값을 변화시키지 못한다.

만약 함수로 인자의 값을 변화시키려면 그 함수에는 인자값을 가리키는 포인터를 전달해야 한다. (포인터에 의한 전달)
void func2(int *pi) {    // func2()는 포인터에 의해 int 형을 받는다.
    // ...
    *pi = 5;                 // pi가 현재 가리키고 있는 것은 5이다.
    // ...
}

main() {
    int n = 3;               // func2()를 호출하기 전에는 n이 3이다.
    func2(&n);            // 포인터에 의해 n이 func2()에 전달된다.
    // ...                    // func2()의 리턴 후에는 n이 5가 된다.

포인터에 의한 전달은 매우 큰 객체를 함수에 전달해야 할 때 그 큰 객체를 복사하지 않아도 전달할 수 있게 해준다.
void func3(BigClass *b) {    // func3()은 BigClass를 포인터에 의해 받는다
    // ...
}

main() {
    BigClass bb;
    func3(&bb);                    // bb는 func3()에 포인터에 의해 전달된다
    // ...
}

여기서 bb는 포인터에 의해 함수 func3()에 전달된다. func3()에서 bb를 변화시킬 의도는 없지만 bb를 포인터로 전달하면 bb의 데이터를 복사하는 것이 아니기 때문에 실행 시간이 줄어든다.

C++에서의 참조에 의한 전달
C++에서도 보통은 값에 의해 매개변수를 전달하지만 때때로 매개변수를 참조에 의해 전달할 필요가 있다. 이것은 C++의 새로운 매개변수 전달 방법이다. 참조에 의한 전달은 인자값이 복사되지 않는다. 대신에 매개변수는 그 인자값의 별명(alias)가 된다. 즉, 인자와 매개변수는 같은 기억장소를 참조하게 된다. 따라서 매개변수의 값을 변화시키면 인자의 값도 변하게 된다. 이것은 마치 포인터에 의한 전달과 아주 유사한 것처럼 보인다. 실제로, 참조에 의한 전달은 포인터에 의한 전달과 아주 유사하다. 그러나 아래의 예를 살펴보면,
void func4(int &ri) {    // func4()는 참조에 의해 int 형을 받는다.(&를 주목하라)
    // ...
    ri = 5;                    // ri에5를 대입한다
    // ...
}

main() {
    int n = 3;                // func4()를 호출하기 전에는 n이 3이다
    func4(n);               // 참조에 의해 n이 func4에 전달된다.
    // ...                     // func4()의 리턴 후에는 n이 5가 된다
}

ri의 선언에서 & 기호는 ri를 값의 매개변수가 아닌 참조 매개변수로 바꿔준다. 함수 func4()가 호출될 때 ri는 n과 함께 엮어진다고 말할 수 있다. ri가 일단 n과 함께 엮어지면 보통의 int와 구별이 되지 않는다. 이 함수가 존속되는 동안은 ri와 n은 같은 객체에 대한 두 개의 이름이 된다. (물론 ri라는 이름은 오직 func4() 내에서만 사용될 수 있는 범위를 갖는다) 따라서 func4() 내에서 ri가 5로 설정되면 실제로는 n도 5로 설정된다. 게다가 매개변수 ri의 주소를 구해 보면 인자 n의 주소와 같은 주소를 얻게 된다. 아래의 프로그램을 살펴보자.
int n;

void func5(int &ri) {
    if (&ri == &n) cout << "you passed n\n";
}

main() {
    func5(n);
}

여기서 func5()는 참조에 의해서 int형을 받는다. 이 함수는 매개변수 ri의 주소를 전역 변수 n의 주소와 비교하여 같으면 메시지를 출력한다. main이 func5()를 n으로 호출하면 이 함수는 메시지를 출력할 것이다. 일단 ri가 n과 함계 엮어지게 되면 그들은 같은 객체를 참조하게 된다.

void inc_each(IntArray &array) {
    size_t i;
    for( i = 0; i < array.getNumElems(); i++)
        array.setElem(i, array.getElem(i)+1);
}

main() {
    IntArray grades;
    // ...
    inc_each(grades);
}

함수 inc_each()는 IntArray를 참조에 의해서 받는다. 이 함수는 배열의 각 요소를 하나씩 증가시킨다. inc_each() 내에서 매개변수 array는 보통의 클래스처럼 취급되고 있음을 주목하라. 단지 array의 선언을 보는 것만으로도 array는 값의 매개변수가 아니라 참조의 매개변수라는 것을 알 수 있다. main()이 inc_each()를 호출할 때, 인자 grades와 매개변수 array는 같은 IntArray 객체에 대한 다른 두 이름이 된다. (하지만 array란 이름은 오직 inc_each() 내에서만 유효하다).

참조 대 포인터
앞에서 언급했듯이 참조는 포인터와 밀접하게 관련이 있다.(컴파일러는 내부적으로 참조에 의한 전달을 거의 포인터에 의한 전달처럼 해석한다) 사실, 참조를 특별한 종류의 포인터로 생각해도 된다. 즉, 명확한 포인터 문법을 필요로 하지 않는 포인터쯤으로 생각할 수 있다. 아래 함수들의 정의와 구동을 비교해보자.
void func_ptr(int *pi) { *pi = 5; }    // i를 바꾸기 위해서는 pi 자체가 아니라 내용을
                                                // 가리키도록 해야 한다.
void func_ref(int &ri) { ri = 5; }      // 내용을 가리키도록 할 필요없이 i를 바꿀 수 있다
main() {
    int i;
    func_ptr(&i);                           // 명확히 i의 주소를 전달한다
    func_ref(i);                             // 단지 i를 전달한다
}

포인터로 조작하기 위해서는 &와 *가 좀 더 많이 필요하지만 참조는 단지 선언할 때를 제외하고는 일반 객체처럼 취급할 수 있다. 하지만 참조가 단순히 몇 번의 키보드 조작 횟수를 줄이기 위해 도입된 것은 아니다. 때때로 추상화는 사용자가 함수가 호출되고 있음을 명확히 알지 못한 상태에서 함수의 구동을 필요로 할 때가 있는데, 참조는 그러한 필요에 부응하여 도입되었다. 이와 관련된 예는 디폴트 생성자에서 볼 수 있었다. (간단한 선언만으로 많은 함수의 구동이 실제로 일어나는 것을 보았다.) C++에서는 이러한 비밀 함수 호출에 사용자가 명확히 주소를 전달할 필요 없이 인자로 큰 객체들을 전달하는 방법을 필요로 한다. 이와 관련된 예는 다음 장에서 보게 될 것이다.
&로 참조를 선언하는 것은 *로 포인터를 선언하는 것과 같은 구문을 가진다. 아래에 몇 가지 선언의 예가 있다.
void func(SomeType &s);          // s는 SomeTYpe에 대한 참조이다.
void func(SomeType *&s);        // s는 SomeType을 가리키는 포인터에 대한 참조이다
void func(SomeType (*&s)());   // s는 매개변수가 없고 SomeType을 리턴하는
                                              // 함수를 가리키는 포인터에 대한 참조이다

포인터와 참조의 또 다른 중요한 유사점은 베이스 클래스로 캐스트하는 것이다. 컴파일러는 유도된 클래스의 포인터를 베이스 클래스의 포인터로 전환함으로써 유도된 클래스 객체의 베이스 클래스 부분을 액세스할 수 있도록 한다는 것을 '5장. 구성과 유도를 이용한 계층성'에서 다루었었다. 따라서 만약 다음과 같은 계층 구조를 갖는다면
class Base { /* ... */ };
class Derived : public Base { /* ... */ };

아래와 같이 할 수도 있다.
void func(Base *pbase);
main() {
    Derived deriv;
    // ...
    func(&deriv);    // 이 함수를 구동하는 동안에는 pbase가 deriv의
                          // 베이스 클래스 부분을 가리키게 된다
}

마찬가지로 참조도 유도된 클래스 객체의 베이스 클래스 부분으로 제한할 수 있다.
void func(Base &rbase);
main() {
    Derived deriv;
    // ...
    func(deriv);    // 이 함수를 구동하는 동안에는 rbase가 deriv의
                        // 베이스 클래스 부분을 참조한다
}

func()이 Base의 참조를 받지만 func()에 deriv를 전달할 수 있다. 컴파일러는 매개변수 rbase를 deriv 인자의 Base 부분으로 자동적으로 제한시켜 준다.

C++에서의 참조에 의한 리턴
참조에 의한 전달은 참조에 의한 리턴을 설정하기 위해 다른 방향에서도 사용될 수 있다. 아래에 전역 객체인 globalTbox에 대한 참조를 리턴하는 getTbox()라는 함수가 있다.
TextBox globalTbox;
TextBox &getTbox() { return globalTbox; }    // 참조에 의해 리턴한다

&기호는 getTbox()의 결과가 TextBox 인스턴스를 참조한다는 것을 나타낸다. 아래와 같이 쓰면 전역 객체 globalTbox의 색은 BLUE로 설정된다.
getTbox().setColor(BLUE);

만약 getTbox()의 리턴값을 전형적인 값에 의한 리턴을 생성하는 & 기호없이 쓰면 globalTbox의 복사본의 색이 위의 표현에 의해 바뀌게 될 것이다.
우리는 전역 변수의 참조를 리턴하는 getTbox()의 예를 보였다. 자동 지역 변수는 참조에 의해 리턴할 수 없는데, 그 이유는 함수가 리턴할 떄 자동 지역 변수는 더 이상 존재하지 않기 때문이다.
int &func() {
    int loc = 5;    // 자동 지역 변수
    return loc;    // 에러 : 참조에 의해 자동 지역 변수를 리턴하고 있다
}

main() {
    cout << func() << '\n';    // 어떤 숫자를 출력할지 정의되어 있지 않다ㅏ
}

func() 함수는 자동 지역 변수인 loc의 참조를 리턴한다. 하지만 loc은 func()을 빠져나오면 더 이상 존재하지 않는다. 따라서 우리는 쓰레기 값의 참조를 얻게 된다. 만약 이 프로그램을 실행시키면 어떤 값을 출력할지 알 수 없다. func()이 값에 의해 리턴하도록 &를 생략해서 이 코드가 동작하게 할 수도 있다.
int func() {
    int loc = 5;    // 자동 지역 변수
    return loc;    // 정상 : 값에 의해 자동 지역 변수를 리턴한다
}

이 경우에 컴파일러가 loc의 값을 임시 위치에 복사하여 함수를 호출한 쪽에서 그 값에 액세스할 수 있도록 한다. 그러나 정말로 func()이 참조에 의해 리턴하기를 원한다면 loc을 static으로 바꾸면 된다.
int &func() {
    static int loc = 5;    // 정적 지역 변수
    return loc;            // 정상 : 참조에 의해 정적 지역 변수를 리턴한다
}

이 경우에 loc은 함수를 빠져나간 후에도 존재하므로 loc의 참조를 리턴하는 데 아무런 문제가 없다.
마지막 예에서 func()은 int형이 허용되는 어디에서나 사용될 수 있다. 예를 들어, func()은 아래와 같이 대입될 수 있다.
main() {
    func() = 1066;    // 지역 정적 변수인 loc에 대입한다
}

이러하나 대입은 loc을 1066으로 바꾼다. 왜냐하면 func()은 loc을 참조에 의해 리턴하기 때문이다. 만약 func()이 loc을 값에 의해 리턴했다면 이러한 할당은 잘못된 것이다. 그러한 경우, 우리는 리턴값을 보관하기 위해 컴파일러가 임시로 만든 값을 바꾸려고 시도해야 할 것이다.

겹지정 : 참조 대 값
값에 의한 전달과 참조에 의한 전달은 호출하는 쪽에서는 동일하게 보이므로 단지 매개변수가 값인지 참조인지에만 의존해서는 함수의 겹지정(overloading)이 불가능하다.
아래의 func()의 두 가지 버전을 만들었다. 하나는 값을 매개변수로 받고, 또다른 하나는 참조를 매개변수로 받는다.
void func(Foo f);      // 이들 두 가지 함수를 함께 가질 수 없다
void func(Foo &f);    // 이들 함수의 시그너처(signature)가 충분하게 다르지 않다

main() {
    Foo ff;
    func(ff);              // 어떤 함수를 구동할 것인가?
}

만약 func()의 두 가지 버전이 허용된다면 컴파일러는 main() 내에서 어떤 것을 구동해야 할지 모를 것이다. 따라서 컴파일러는 이러한 겹지정을 허용하지 않는다.

참조를 변수와 엮을 때의 문제점
우리는 종종 컴파일러가 중간의 임시 변수들을 생성하는 것을 당연하게 받아들인다. 예를 들어, long형을 int형으로 캐스트할 때 컴파일러는 그 결과를 보관하기 위해 눈에 보이지 않는 int형 변수를 생성한다. 이러한 임시 변수들이 참조와 함께 쓰일 때 문제가 될 수 있는데, 그 이유는 컴파일러가 이런 임시 변수들과 참조를 함께 엮을 수가 없기 때문이다. 만약 그것이 가능하다고 가정하면, 참조의 값이 바뀌었을 때 그 변경된 값을 보려고 하면 그것은 임시 변수들과 함께 사라져 버리게 된다.
 이러한 제약이 의미하는 바는 만약 잘못된 형이나 수학식의 결과나 숫자 자체를 전달하려고 하면 컴파일러는 불평을 할 것이라는 것이다. 아래에서 허용되지 않는 몇 가지 예를 보이고 있다.
void func(int &ri);        // int 형의 참조를 받는 함수
main() {
    int int_var;
    long long_var;

    func(int_var);          // 정상 : int형 변수를 전달한다
    func(long_var);       // 에러 : long형 변수를 전달한다
    func(int_var * 2);     // 에러 : 수학식의 결과를 전달한다
    func(5);                 // 숫자 자체를 전달한다
}

첫번째를 제외한 세 가지 구동은 컴파일러에 의해 허용되지 않는데, 그 이유는 세 가지 모두 ri가 컴파일러에 의해 생성된 임시 변수와 엮어지게 되기 때문이다.

아래의 val_func()에서처럼 함수가 값으로 리턴할 때에도 컴파일러는 임시 변수를 만들어낸다.
int val_func() {        // 함수가 값으로 리턴한다(임시로 생성됨)
    int i = 5;    return i; }

int &ref_func() {      // 함수가 참조로 리턴한다(임시로 생성되는 것은 없음)
    static int i = 5;
    return i;
}

void func(int &ri);    // 함수가 int형으로 참조된다
main() {
    func(val_func());    // 에러 : 값으로 리턴을 전달한다
    func(ref_func());    // 정상 : 참조로 리턴을 전달한다
}

여기서 val_func()에 의한 리턴값을 보관하기 위해 컴파일러는 임시값을 생성할 필요가 있다. 여러분은 이 결과를 참조에 의해 전달할 수 없다. ref_func()은 참조를 리턴하므로 임시값이 생성되지 않으며, 따라서 이 결과를 참조에 의해 전달할 수 있다.

위에서 보인 에러들을 수정하기 위해 단순히 값을 정확한 형의 객체에 대입한 후에 그 객체를 전달한다.
main() {
    int n;
    // ...
    n = 5;             // 숫자를 변수에 대입한다
    func(n);         // 정상 : 변수를 참조로 전달한다

    n = val_func();    // 리턴된 값을 변수에 대입한다
    func(n);             // 정상 : 변수를 참조에 의해 전달한다
}


숫자 5와 val_func()에 의해 리턴된 값을 변수 n에 대입하였으므로 이러한 func()의 구동은 적절하다. 그리고 나서 n은 참조에 의해 전달되었다. 위의 두 가지 예에서 보인 다른 에러들을 해결하기 위해서는 long형과 수식을 n에 대입하면 된다.


** 관련 글 **
1. 클래스를 이용한 객체지향 프로그래밍
2. 클래스를 제외한 C++
3. 멤버 함수를 이용한 추상화
4. 액세스 지정자를 이용한 캡슐화
5. 구성과 유도를 이용한 계층성
6. 생성자와 소멸자를 이용한 향상된 추상화 - 1
6. 생성자와 소멸자를 이용한 향상된 추상화 - 2
7. new와 delete를 이용한 향상된 추상화
저작자 표시
이올린에 북마크하기(0) 이올린에 추천하기(0)
이전 1 2 3 4 5 ... 201 다음