[C++] 02. C언어 기반의 C++ #2

kkado·2023년 10월 12일
0

열혈 C++

목록 보기
2/16
post-thumbnail

💬 윤성우 님의 <열혈 C++ 프로그래밍> 책을 혼자 공부하며 배운 내용을 정리합니다. 글의 모든 내용은 책에서 발췌하였습니다.


간단한 포인터, 메모리, 상수 등에 대한 복습

const

const : 어떤 변수를 상수 취급하겠다 (변경이 불가능하도록)

포인터의 상수화, 상수 포인터 등의 차이에 유념. 아래 사진의 빨간줄 주목

메모리 공간의 종류

  • 데이터 : 전역 변수가 저장되는 영역
  • 스택 : 지역변수 및 매개변수가 저장되는 영역
  • 힙 : 프로그램이 실행되면서 동적 할당이 이루어지는 영역

call by value / call by reference

  • call by value : 함수의 매개변수로 전달한 인자를 복사하여, 원래 값은 변하지 않고 복사본이 함수로 전달됨
  • call by reference : 함수의 매개변수로 특정한 메모리 주소를 전달하여, 주소를 가지고 메모리 주소의 값을 직접 바꾸기 때문에 원본이 변함

참조자

int a = 0;

위와 같이 선언하면서 어떤 메모리 주소 공간에 4바이트만큼을 할당받고, 0이라는 값을 저장할 수 있다.

여기서 b라는 다른 변수로 같은 공간에 접근하고 싶다.

int &num2 = num1;

처음 보는 괴상한 문법을 통해 가능하다.

& 연산자는 변수의 주소 값을 반환하는 연산자이지만, 새로 선언되는 변수 앞에 등장하게 되면 참조자(reference)를 선언하는 데 사용된다.

포인터(*)와 참조자(&)에 대해서 잘 알아놓자.

  • 참조자의 개수는 제한이 없다.
  • 어떤 참조자를 다음 참조자를 선언할 때 사용할 수 있다.
  • 변수에 대해서만 선언할 수 있고 (상수는 참조할 수 없다.) 선언됨과 동시에 누군갈 참조해야 한다.


call by address ??

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++에서는 함수 외부 변수의 참조도구로 주소 값을 이용하는 방식과 참조자를 이용하는 방식이 있다.

참조자를 이용한 call by reference

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는 각각 또다른 이름인 ref1ref2를 갖게 되는 것이다.

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의 참조자로서 refsomefunc 안에서 참조되었고, 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"; // 다음과 같이 호출해야만 한다.
}

new, delete

C에서 동적할당을 배우면서 mallocfree를 사용해 본 적이 있다. 다음과 같이 사용할 것이다.

char *str = (char*)malloc(sizeof(char)*20);

그러나 malloc에는 두 가지 불편한 점이 있다.

  • 할당 대상의 정보를 무조건 바이트 크기단위로 전달해야 한다
  • 반환형이 void형 포인트라서 형변환을 거쳐야 한다

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];

객체의 생성에는 new & delete

객체를 생성함에 있어 malloc을 사용하는 것과 new를 사용하는 것은 그 동작에 차이가 있다. 아직은 객체를 다루지 않아서 모르겠지만 우선 차이가 있다는 것만 짚고 다음에 더 자세히 알아보는 걸로...

포인터 없이 힙에 할당된 변수에 접근

const 참조자가 아닌 경우 참조자는 변수를 대상으로만 가능하다고 했는데, C++에서는 new 연산자를 이용해 할당된 메모리 공간도 변수로 취급이 가능하여 변수명을 사용하지 않고도 참조자로 바로 참조가 가능하다.

int* ptr = new int;
int &ref = *ptr;
ref = 20;

잘 사용하는 문법은 아니지만 포인터 연산 없이 힙에 접근했다는 사실은 알아둘 만 하다.


profile
울면안돼 쫄면안돼 냉면됩니다

0개의 댓글