C++

[강의] 10월 29일 수업정리

k-codestudy 2024. 10. 29. 17:56

오늘은 래퍼런스에 추가설명과 복사생성자에 대한 수업을 들었다.

 

 

1. 래퍼런스 추가 설명 

1.1 const 래퍼런스

#include <iostream>

void func(const int& nData);

int main()
{
    int nMain{};
    nMain = 99;

    func(10);     // 상수(임시 객체) 전달 가능
    func(nMain);  // 변수 전달 가능
}

void func(const int& nData)
{
    printf("%d\n", nData);
}

const 참조를 사용하면 상수(임시 객체)와 변수 모두를 전달할 수 있다. 이렇게 하면, 값을 복사하는 call by value처럼 사용할 수 있지만 메모리 낭비를 줄일 수 있다. ( 포인터는 call by value를 하기 때문에 메모리 낭비가 생길 수 도 있다.)

그렇기에 결과를 반환할 때는 포인터보다 레퍼런스가 더 유리하다.

레퍼런스에서 배열을 참조로 전달할 때도 같은 개념이 적용되지만, 참조에 일부 제한이 생겨 참조 배열은 권장하지 않는다.

 

1.2 배열 참조

배열에 참조를 사용하는 방법은 가능하지만 사용하지 않는 이유가 존재한다.

우선 선언 방식의 차이를 알아보자.

void func(int& arData[5]);

void func(int(&arData)[5]);
  • int& arData [5]
    이 선언의 경우 참조 배열처럼 보이지만 실제로는 참조가 아닌 포인터로 해석이 되어버린다. 즉, int *arData와 동일하게 취급을 하기에 배열의 크기 (5)를 보장하지 않으며 배열의 일관성을 확인할 수 없다.
  • nt(&arData)[5] 
    이 선언의 경우 정확히 크기가 5인 int 배열에 대한 참조로 선언이 된다. 배열의 크기를 5로 고정하기 때문에 다른 크기의 배열은 전달할 수 없기에 안정성에서는 이점을 가지지 많은 배열 크기가 고정되어 범용성이 떨어진다.

그럼 예제로 알아보자 

 

#include <iostream>

void func(int(&arData)[5]);

int main()
{
	int arMain[5]{ 5,4,3,2,1 };

	func(arMain);
}

void func(int(&arData)[5])
{
	for (int i = 0; i < 5; i++)
	{
		printf("%d ", arData[i]);
	}
	printf("\n");
}

위 예제의 코드의 경우 func에 참조되어 있는 배열의 크기가 5만 받기 때문에 위의 코드는 문제없이 돌아가게 될 것이다.

하지만 arMain의 크기가 다른 값을 변경되면 컴파일 에러가 발생할 것이다. 
즉, 배열 크기 일관성은 유지할 수 있지만 코드의 범용성을 제한하기에 배열을 넘길 때는 참조로 넘기지 않고 배열로  넘기는 이유이다.

 

 

1.3 요약

  • 참조 사용 장점: 포인터보다 참조를 사용하여 const 값을 전달할 때, 메모리 효율이 좋고 코드가 직관적이다.
  • 기준점:
    • 배열은 포인터를 사용하는 것이 더 일반적입니다.
    • 참조는 값 전달(call by value)처럼 간편하게 사용할 수 있으며, const 키워드 덕분에 상수도 전달할 수 있다.
    • 결과를 반환할 때는 포인터보다 참조가 더 좋다.

 

1.4 추가 설명

  • 래퍼런스가 붙어있으면 무조건 그 값은 바꿔야 하는 값이다 ( 외울 것)
  • 래퍼런스의 장점 -> 래퍼런스는 대상을 무조건 넣어야 한다는 장점, 메모리 복사가 없다 ( 대상을 직접 넘긴다)
  • 래퍼런스에서 함수 오버로딩은 사용할 수 없다. 이유는 함수 오버로딩은 애초에 컴파일러에서 판단을 못하기 때문에 에러가 난다. 아래 예제를 보면서 생각해 보자.
void func(int &nData);

void func(int nData);

 

2. 복사 생성자

정의 : 복사 생성자는 기존 객체의 내용을 그대로 복사하여 새로운 객체를 생성할 때 사용하는 생성자이다.

 

2.1 값에 의한 전달 (call by value) Vs 참조에 의한 전달 (call by Reference)

클래스에서 복사 생성자를 정의할 때 두 가지 방식이 존재한다.

 

 [header.h]

#pragma once

class C_DATA
{
private:
	int m_nData{};
public:
	C_DATA(C_DATA c);  // call by value
	C_DATA(const C_DATA& c); // reference
	void setData(int nData);
	int getData();
};

 

[main.cpp]

#include <iostream>
#include "data.h"

int main()
{
	C_DATA c1{};

	c1.setData(100);

	C_DATA c2 = c1; 
// 선언과 동시에 나와 똑같은 데이터를 넣음 
// 생성자의 생성과 동시에 나와 똑같은 자료형이 들어옴
//  "="를 보고 call by value인지 레퍼런스인지 해석하기 힘듬 

	printf("%d\n", c2.getData());
}

C_DATA(C_DATA c);처럼 매개변수를 값으로 전달하게 되면 객체가 복사될 때마다 복사 생성자가 무한 호출되는 문제가 발생한다. 그렇기에 무한 호출을 막기 위해 C_DATA(const C_DATA& c);와 같은 레퍼런스와 const만을 사용하여 복사생성자를 만들어야 한다.

 

2.2 올바른 복사생성자 

우리가 생각해야 하는 부분이 있다.
생성자를 직접 정의하게 되면 기존의 모든 생성자가 사라지는 기능을 생각해야 한다.

그렇기에 우리는 기본 생성자를 따로 만들어야 하고 따로 기본 생성자를 만들게 된다면 모든 멤버변수를 초기화를 직접 해야 하는 골치 아픈 상황이 발생한다.

그렇기에 복사생성자를 생성할 때 기본생성자를 유지합니다라는 것을 명시적으로 표현해 주기 위해 C_DATA() = default; 구문을 추가해야 한다. 
즉, 복사생성자를 만들게 되면 기본생성자를 무조건 같이 만들어줘야 한다.

 

 [header.h]

#pragma once

class C_DATA
{
private:
	int m_nData{};
public:
	C_DATA() = default;
	explicit C_DATA(const C_DATA& c);
	void setData(int nData);
	int getData();
};

 

 

2.3 왜 복사 생성자가 필요한가? ( 중요 )

우리 C언어는 기본적으로 class기반으로 이루어져 있기에 자료형 외의 모든 객체를 함수에서 값으로 전달하려면 call by value로 값을 넘겨야 하기에 디폴트 복사 생성자가 생성된다. 

즉, 함수 call by value를 가능하게 하기 위해서 디폴트 복사 생성자가 존재하는 것이다.

 

2.4 복사생성자를 배우는 이유

복사 생성자를 배우는 class를 call by value를 하게 되면 문제를 야기할 수 있기에 못쓰게 하기 위해 배우는 것이다.

특히 자료형을 제외한 객체는 call by value를 사용하면 안 된다고 생각해 두면 편할 것이다.

이 아래 코드는 복사생성자를 못쓰게 막아둔 코드이다.

 

 [header.h]

#pragma once

class C_DATA
{
private:
	int m_nData{};
public:
	C_DATA() = default;
	C_DATA(const C_DATA& c) = delete;
	void setData(int nData);
	int getData();
};

 

예전에는 class의 private : 부분에 복사생성자를 넣었던 반면 현재는 = delete를 통해 복사 생성자 사용을 막아 객체가 값으로 전달되지 않도록 한다.

'C++' 카테고리의 다른 글

[강의] 10월 31일 수업정리  (2) 2024.11.01
[강의] 10월 30일 수업정리  (2) 2024.10.31
[강의] 10월 25일 수업정리  (0) 2024.10.27
[강의] 10월 24일 수업정리  (0) 2024.10.25
[강의] 10월 23일 수업정리  (2) 2024.10.24