CHAPTER 02의 시작에 앞서
- 상수(const)의 의미
=> const int num = 10; // 변수 num을 상수화
=> const int *ptr1 = &val1; // 포인터 ptr을 통해 val1의 값을 변경할 수 없으나, 다른 변수의 주소 저장 가능
=> int const *ptr1 = &val1; // 포인터 변수 ptr을 상수화
=> const int const *ptr1 = &val1; // 위의 두 가지 특성을 다 가진다.
1. 새로운 자료형 bool
C언어에서는 별도로 매크로 상수로 지정해 주지만, C++에서는 'true'와 'false'가 존재한다.
위의 데이터를 저장하기 위한 자료형을 'bool' 이라고 한다. int, double과 같은 기본자료형의 하나이기 때문에
다음과 같이 선언하는 것이 좋다.
int main( void) { bool flag1 = true; bool flag2 = false; } |
2. 참조자(Reference)의 이해
'참조라'라는 것은 포인터와 비유가 되기 쉽지만, 그보다 조금 더 쉬운 개념으로서
변수에 또 다른 이름(별칭)을 지정하는 것이다.
int main( void) { int num1 = 10; int &num2 = num1; num2 = 20; cout<< num1 << endl; cout<< num2 << endl; } |
위의 결과 값은 20으로 동일하다, 즉 num2와 num1가 동일한 공간을 가르키고 있는 것이다.
※ 참조자의 수에는 제한이 없으며, 참조자를 대상으로 참조자를 선언할 수 있다.
※ 변수만 참조가 가능하고, 선언과 동시에 참조되는 대상을 지정해 주어야 한다.
※ 배열의 특정 원소도 변수로 간주하여 참조가 가능하다.
포인터 변수도 &를 이용하여 참조가 가능하다.
int main( void) { int n = 10; int *p = &n; int **dp= &p; int &ref = n; int *(&pref) = p; int **(&dpref) = dp; } |
3. 참조자(reference)와 함수
시작 전에 다음 의미를 되새겨 보자.
- Call-by-value // 값에 의한 호출
- Call-by-reference // 주소 값을 이용한 호출
두 번째의 경우 꼭 주소를 통해 호출한다고 해서 위의 의미가 맞는 것은 아니다.
정확히 말하자면 "주소 값을 전달받아서, 함수 외부에 선언된 변수에 접근하는 형태의 함수 호출" 이다.
따라서, C++에서는 아래와 같은 두 가지 종류의 'Call-by-reference'가 존재한다.
- 주소 값을 이용한 'Call-by-reference'
- 참조자를 이용한 'Call-by-reference'
void swap( int &val1, int &val2) { int temp; temp = val1; val1 = val2; val2 = temp; } int main( void) { int val1 = 10; int val2 = 20; swap( val1, val2); cout<< "val1 = " << val1 << endl; cout<< "val2 = " << val2 << endl; } |
위의 코드를 보면 한 가지 의문이 들 수 있다. "참조자는 선언과 동시에 초기화 해줘야 한다."
하지만 매개변수는 함수가 호출되어야 초기화가 진행되는 변수들이기 때문에, 함수 호출 시 초기화가 진행된다.
※ 참조자의 단점
C언의 경우 함수 호출을 보고 결과를 예측할 수 있지만, C++의 경우 매개변수를 확인해야 한다.
아래와 같은 소스코드의 경우, C에서는 num의 값이 변경될 수 없지만, C++는 변경이 가능하다.
'const' 형으로 선언해서 어느정도 예방이 가능하다.
int main( void) { int num = 10; func(num); } |
4. 반환형이 참조형(Reference Type)인 경우
아래 예제코드를 보면, 함수의 반환타입이 정수형 참조형이다.
혼동하지 말아야할 부분이, 매개변수를 참조형으로 선언했다고 해서 반환을 참조형으로 해주는 것이 아니다.
int& func(int &val) { return val; } int main( void) { int num = 10; int res1 = func(num); int &res2 = func(num); } |
메인 함수에서 위의 함수로부터 값을 반환받는 함수는 참조형(&res2)과 일반 정수형(res1) 변수이다.
둘 다 가능한 예이지만, 정수형 변수는 단순히 값만 받는 것이고 참조형 변수는 val이라는 변수를 참조 한다는 것이다.
val은 num을 참조하고 있으니, 즉 res2는 num을 참조하게 되는 것이다.
이렇듯 반환형이 참조형인 경우, 반환 값을 어떤 타입의 변수로 저장하느냐에 따라서 결과에 차이가 있다.
또한, 다음 예제를 통해서 참조자와 관련된 반환형에 대해서 익숙해지자.
int& funcR(int &val) { return val; } int funcV(int &val) { return val; } int main( void) { int val = 10; int res1 = funcR(val); // 가능 int &res2 = funcR(val); // 가능 int res3 = funcV(val); // 가능 int &res4 = funcV(val); // 불가능 } |
funcR의 경우는 참조자형으로 데이터를 반환하기 때문에, 일반 변수 및 참조자 변수가 모두 값을 받을 수 있다.
그러나 funcV의 경우 일반 형태로 데이터를 반환하기 때문에, 참조자형 변수로 값을 받을 수 없다.
5. 잘못된 참조의 반환
int& func(int n) { int num = 10; num = num + 10; return num; } int main( void) { int &res = func(10); } |
위의 예제를 보면, 문법적으로는 오류가 없다. 그러나 func 함수에서 num을 참조자 형태로 반환하고 있고,
참조자 변수 res는 그 반환 값을 받는다. 그렇게되면 res는 num의 변수에 별칭을 지정하는 것이므로,
호출이 끝나고 사라지는 지역 변수인 num을 지칭하게 되는 것이다.
6. const 참조자의 특징
int main( void) { const int num = 10; int &ref = num; ref += 10; } |
위의 예제는 논리적인 문제점이 있다. num을 상수화 시켰는데, 참조자를 통해서 num의 값을 변경하려 한다.
물론 이는 허용되지 않기 때문에 C++에서 컴파일 에러를 발생시킨다.
따라서 상수화된 변수를 참조하기 위해서는, 참조자도 상수화를 시켜줘야 한다.
int main( void) { const int num = 10; const int &ref = num; } |
그리고 또 한가지, 앞서 참조자는 선언과 동시에 초기화가 되어야 하고, 그 값이 변수'만' 가능하다고 했다.
하지만 아래 코드도 가능하다.
int main( void) { const int &ref = 10; } |
일반적으로 프로그램상에서 표현되는 숫자를 가리켜 '리터럴(literal)' 또는 '리터럴 상수(literal constant)'라 한다.
그리고 이들의 특징은 다음과 같다. "임시적으로 존재하는 값이다. 다음 행으로 넘어가면 존재하지 않는 상수다."
int num = 10 + 30; |
즉, 10과 30이라는 숫자는 덧셈연산을 위해 메모리 저장되어야 한다. 하지만 다음 행으로 넘어가면 소멸하게 된다.
그렇다면 참조자를 통해서 소멸되는 상수를 참조하는 것이 가능한 이유는 무엇일까?
그것은 const로 상수화된 참조자가 상수를 참조할 때 임시변수를 만들어 이를 참조하게끔 하는 구조라서 가능하다.
따라서 아래와 같은 구조도 가능하다.
int adder( const int &num1, const int &num2) { return num1 + num2; } int main( void) { adder( 3, 5); // call by literals } |
7. malloc & free를 대신하는 new & delete
malloc함수의 단점은 다음과 같다.
- 할당할 대상의 정보를 무조건 바이트 크기단위로 전달해야 한다.
- 반환형이 void형 포인터이기 때문에 적절한 형 변환을 거쳐야 한다.
new를 사용하면 위의 불편함이 사라진다.
int main( void) { int *ptr1 = new int; double *ptr2= new double; int *ar1 = new int[3]; double *ar2 = new double[5]; delete ptr1; delete ptr2; delete []ar1; delete []ar2; } |
형 변환도 필요가 없고, 바이트 단위가 아닌 단순 길이(크기)만 지정해 주면 된다.
아래 부분은 delete의 사용 방법으로, 할당된 구조가 배열이라면 추가로 '[]'를 명시해 주어야 한다.
※ new와 malloc의 동작 방식에는 차이가 있다. 추 후에 자세하게 설명하도록 하겠다.
8. 참조자를 통해 힙에 할당된 변수 접근
int main( void) { int *ptr1 = new int; int *(&ref1) = ptr1; int &ref2 = *ptr1; *ref1 = 10; cout << *ptr1 << endl; ref2 = 30; cout << *ptr1 << endl; } |
참조자는 변수를 대상으로, 해당 변수의 메모리 공간을 참조하는 것으로 알고 있다.
그렇다면 힙 영역도 가능할까? 위의 예제를 보면 알겠지만, 가능하다.
두 가지 방식으로 참조를 하고 있다. 첫 번째의 방법은,
-> 포인터 변수를 참조하여, 그 포인터 변수가 가르키는 힙의 영역을 간접적으로 참조
두 번째의 방법은,
-> 포인터 변수가 가르키는 힙의 영역을 직접 참조
이렇게 참조자를 통해, 포인터 연산 없이 힙 영역에 접근할 수 있다.
9. C++에서 C언어 표준함수 호출하기
C++을 사용하다가, 자신이 잘 알고 사용해온 C언어의 표준함수를 사용하고 싶을 때가 있다.
- c를 더하고 .h를 뺀다.
ex) #include <cstdio> , #include <cstring>
위와 같이 선언하면, C언어의 헤더와 대응되는 C++의 헤더파일 이름이 된다.
※ 하위 버전과의 호환성(backwards compatibility)을 제공하기 위해 C 헤더를 제공하지만,
C++에서는 함수 오버로딩이 가능하기 때문에 개선된 형태의 라이브러리가 구성되어 있으므로,
가급적으로 C++의 표준헤더를 이용하는 것이 좋다.
출처 : 윤성우 열혈 C++ 프로그래밍
'프로그래밍 언어들 > C++' 카테고리의 다른 글
CHAPTER05 - 복사 생성자(Copy Constructor) (0) | 2016.11.14 |
---|---|
CHAPTER04 - 클래스의 완성-2 (0) | 2016.11.14 |
CHAPTER04 - 클래스의 완성-1 (0) | 2016.11.14 |
CHAPTER03 - 클래스의 기본 (0) | 2016.11.12 |
CHAPTER01 - C언어 기반의 C++ (0) | 2016.11.10 |