💬 윤성우 님의 <열혈 C++ 프로그래밍> 책을 혼자 공부하며 배운 내용을 정리합니다. 글의 모든 내용은 책에서 발췌하였습니다.
const : 어떤 변수를 상수 취급하겠다 (변경이 불가능하도록)
포인터의 상수화, 상수 포인터 등의 차이에 유념. 아래 사진의 빨간줄 주목
int a = 0;
위와 같이 선언하면서 어떤 메모리 주소 공간에 4바이트만큼을 할당받고, 0이라는 값을 저장할 수 있다.
여기서 b
라는 다른 변수로 같은 공간에 접근하고 싶다.
int &num2 = num1;
처음 보는 괴상한 문법을 통해 가능하다.
&
연산자는 변수의 주소 값을 반환하는 연산자이지만, 새로 선언되는 변수 앞에 등장하게 되면 참조자(reference)를 선언하는 데 사용된다.
포인터(*
)와 참조자(&
)에 대해서 잘 알아놓자.
int * func(int* ptr);
이 함수는 call by refer도 될 수 있고 call by value도 될 수 있다.
int * func(int *ptr) {
return ptr + 1;
}
이렇게 구현하면 call by value
이다. 다만 그 연산의 대상이 주소값일 뿐이다.
int * f(int *ptr) {
*ptr = 20;
return ptr;
}
이렇게 구현하면 주소로 전달한 변수의 값이 실제로 바뀌므로 call by refer
이다.
포인트는 주소 값이 전달되었는가
가 아닌, 주소 값이 참조의 도구로 사용되었는가
이다.
본래 C언어에서 말하는 call by reference는 다음과 같은 의미를 지닌다.
주소 값을 전달받아서 함수 외부에 선언된 변수에 접근하는 형태의 함수
포인트는 주소 값이 전달되었는가
가 아닌, 주소 값이 참조의 도구로 사용되었는가
이다. 즉 이것이 call by ref/value를 결정하는 기준이다.
C++에서는 함수 외부 변수의 참조도구로 주소 값
을 이용하는 방식과 참조자
를 이용하는 방식이 있다.
C++에서는 참조자를 기반으로도 call by reference를 할 수 있다.
여러 번 언급했듯 call by reference의 핵심은 함수 외부에 선언된 변수에 접근할 수 있다는 것이다.
void SwapByRef2(int &ref1, int &ref2) {
int temp = ref1;
ref1 = ref2;
ref2 = temp;
}
int main()
{
int a = 10, b = 20;
SwapByRef2(a, b);
std::cout << a << b << "\n";
// 출력결과 : 20 10
}
참조자는 선언과 동시에 변수로 초기화되어야 한다고 했는데, 매개변수로 전달됐다? 그리고 정상적으로 swap이 됐다?
매개변수는 함수가 호출되어야 초기화된다.
즉 위와 같이 매개변수를 선언하면, 초기화가 이루어지지 않은 것이 아니라 함수 호출 시 전달받은 인자로 초기화를 하겠다는 의미의 선언이다.
main 함수에서 a, b에 각각 10, 20을 넣고 SwapByRef2(a, b)
를 호출하면, 호출됨과 동시에 참조자가 선언 및 참조되어 a, b는 각각 또다른 이름인 ref1
과 ref2
를 갖게 되는 것이다.
int &ref1 = a;
int &ref2 = b;
위의 코드가 바로 실행된다고 봐도 괜찮으려나? (잘 모르겠음.)
int num = 24;
someFunc(num);
std::cout << num << "\n";
위의 실행 결과는 자명하게 24일까? num의 주소값을 전달하지 않으니까 call by value일까?
그렇지 않다.
void someFunc(int &ref);
someFunc
이 위와 같이 선언 및 사용된다면, num
의 값을 변경시킬 수 있는 것이다. 이것이 참조자가 지닌 약간의 맹점이다. 함수의 호출 부분만 보고서는 결과값을 예상할 수 없다.
따라서 함수의 원형까지 살펴보아야 알 수 있다. 다만 const
키워드를 통해 단점을 어느정도 극복할 수 있다.
void someFunc(const int &ref);
위와 같이 선언한다면?
const
는 "이 함수 내에서 참조자 ref을 통한 값의 변경은 하지 않겠습니다."
라는 뜻이다.
따라서 다음과 같은 코드는 컴파일 에러가 발생한다.
void someFunc(const int &ref) {
ref = 10;
}
함수 내에서, 참조자를 통한 값의 변경을 진행하지 않을 경우, 이 참조자를
const
로 선언해서 함수의 원형을 보지 않고 선언부만 보더라도'이 함수에 전달된 매개변수는 변경될 일이 없다'
는 사실을 알려줄 수 있다.
int& someFunc(int &ref) {
ref++;
return ref;
}
int main()
{
int a = 10;
int &b = someFunc(a);
std::cout << a << b << "\n";
// 출력 결과 11 11
}
위 코드의 someFunc
처럼 함수의 반환형도 참조자일 수 있다.
이를 main 함수에서 num2
라는 참조자로 받으면, someFunc
에서 참조자 ref
로 받았던 것이 a
이고 이를 그대로 반환해주고 있으니 num2
는 결과적으로 a
와 같은 주소를 가리킨다.
위 코드에서, main 함수를 다음과 같이 조금만 바꾸면 어떻게 될까?
int& someFunc(int &ref) {
ref++;
return ref;
}
int main()
{
int a = 10;
int b = someFunc(a);
a += 1;
b += 100;
std::cout << a << "\n" << b << "\n";
// 출력 결과 ??
}
출력 결과가 이전과 달라진다.
왜냐하면 someFunc
의 반환값을 받고 있는 b
의 자료형이 참조자가 아닌 일반 정수 변수이다. 즉 someFunc
을 통해 ref
를 거쳐 b에게 전달된 a의 값이, 참조자 형태가 아니라 call by value처럼 정수 값만 들어가는 것이다.
따라서 a와 b는 서로 다른 주소에 붙어있는 변수명이다.
그러므로 출력 결과는 12 111
이 된다.
다음과 똑같은 구조라고 보면 되려나 (확실 X)
int a = 10;
int &ref = a; // 여기서 a와 ref는 둘 다 같은 주소를 가리킴
int b = ref; // ref의 주소에 저장된 '값'만을 별도의 변수 b에 저장함
이번에는 함수의 반환형을 정수형으로 바꿔서 다음과 같이 구성하였다.
int someFunc(int &ref) {
ref++;
return ref;
}
int main()
{
int a = 10;
int b = someFunc(a);
a += 1;
b += 100;
std::cout << a << "\n" << b << "\n";
}
a
의 참조자로서 ref
가 somefunc
안에서 참조되었고, ref++;
에 의해 a 역시 11이 되었다. 그리고 ref
의 값을 참조자가 아닌 정수형으로 반환하게 되면 마치 상수 '11' 을 반환하는 효과가 난다.
따라서 b에는 11이 들어가되 a와는 다른 주소의 값이 들어가게 되고, 아래 증감을 거쳐 최종 실행 결과가 12 111
이 된다. (직전 예제와 동일)
하나 더 살펴보자면 다음과 같이 지역변수를 참조자로 참조하는 일은 없어야 한다.
int& someFunc(int n) {
int a = 20;
a += n;
return a;
}
int main()
{
int a = 10;
int &b = someFunc(a);
std::cout << a << "\n" << b << "\n";
}
someFunc
안에서 정의한 a를 반환하여 b로 하여금 참조하게 하였다.
그러나 someFunc
함수가 종료되면 a는 소멸하여 쓰레기 값이 남게 된다.
a는 소멸하였으나 main 함수에서 여전히 b로 그 메모리 주소 위치를 참조하고 있게 되는 셈이다. (실행하면 b의 값은 매번 다르게 나온다.)
const int num = 20;
int &ref = num;
ref += 10;
std::cout << num << "\n";
위 코드를 보면 num
을 상수로 선언해두고 참조자 ref
로 같은 위치를 참조하였다. 이렇게 되면 ref를 통해서는 얼마든지 값을 수정할 수 있게 될 것 같으나 C++에서는 이를 허용하지 않고 컴파일 에러를 발생시킨다.
따라서 변수 num
과 같이 상수에 대해 참조할 때는 다음과 같이 해야 한다.
const int num = 20;
const int &ref = num;
그리고, const
참조자는 상수도 참조가 가능하다. 아까 참조자가 처음 등장했을 때는 상수는 참조할 수 없다더니?
int num = 20 + 30;
여기서 20, 30은 리터럴
또는 리터럴 상수
라고 부른다.
그리고 이들은 임시적으로만 존재하는 값이며 다음 행으로 넘어가면 존재하지 않게 된다
는 특징이 있다.
즉 num을 정의할 때 사용되는 20, 30은 재참조 가능하지 않다.
그래서 C++에서는 const int &ref = 50;
와 같은 문장이 성립할 수 있도록 const
참조자를 이용해 상수를 참조할 때 임시변수
라는 것을 만들고, 이 장소에 50을 저장하고선 참조자가 이 곳을 참조하게끔 한다.
왜 임시변수라는 낯선 개념까지 사용하면서 상수의 참조가 가능하게 했을까.
int adder(const int &num1, const int &num2) {
return num1 + num2;
}
위와 같이 두 상수 참조자를 인자로 받아 덧셈한 결과를 출력하는 함수를 보자.
만약 상수 참조가 불가능했다면, 상수를 인자로 넘겨주며 이 함수를 호출할 수 없다. 따라서 어떤 변수에 상수를 할당하고 변수를 인자로 넘겨주는 불필요한 작업을 진행해야 한다.
int main()
{
std::cout << adder(3, 4) << "\n"; // 상수 참조가 불가능했다면 이와 같이 호출할 수 없고
int a = 3, b = 4;
std::cout << adder(a, b) << "\n"; // 다음과 같이 호출해야만 한다.
}
C에서 동적할당을 배우면서 malloc
과 free
를 사용해 본 적이 있다. 다음과 같이 사용할 것이다.
char *str = (char*)malloc(sizeof(char)*20);
그러나 malloc에는 두 가지 불편한 점이 있다.
C++에는 malloc
대신 new
, free
대신에 delete
를 사용함으로써 이러한 불편을 덜 수 있다.
new
의 사용방법은 다음과 같다.
int* ptr1 = new int;
double* ptr2 = new double;
int* arr1 = new int[3];
double* arr2 = new double[7];
보다시피 바이트 크기로 계산해서 전달할 필요도 없고 형변환을 거칠 필요가 없다.
delete
는 다음과 같이 사용한다.
delete ptr1;
delete ptr2;
delete []arr1;
delete []arr2;
할당된 영역이 배열 구조라면 []
를 앞에 추가로 명시해 주기만 하면 된다.
따라서 위에 작성한
char* str = (char*)malloc(sizeof(char)*20);
이 코드는 new
를 이용해서 다음과 같이 변경할 수 있다.
char* str = new char[20];
객체를 생성함에 있어 malloc을 사용하는 것과 new를 사용하는 것은 그 동작에 차이가 있다. 아직은 객체를 다루지 않아서 모르겠지만 우선 차이가 있다는 것만 짚고 다음에 더 자세히 알아보는 걸로...
const
참조자가 아닌 경우 참조자는 변수를 대상으로만 가능하다고 했는데, C++에서는 new
연산자를 이용해 할당된 메모리 공간도 변수로 취급이 가능하여 변수명을 사용하지 않고도 참조자로 바로 참조가 가능하다.
int* ptr = new int;
int &ref = *ptr;
ref = 20;
잘 사용하는 문법은 아니지만 포인터 연산 없이 힙에 접근했다는 사실은 알아둘 만 하다.