본문 바로가기

프로그래밍 언어들/C++

CHAPTER02 - C언어 기반의 C++

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 *= &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++ 프로그래밍