read

네번째 열혈강의


computer

이전 포스팅에서 Reference에 대한 얘기를 하는 도중에 끝이 났다. 거의 대부분이 설명 되었지만 이제는 함수에서 반환형이 참조형인 경우에 대해서 설명하겠다.


Reference type의 반환형


int& RefRetFuncOne(int &ref){
	ref++;
    return ref;
}

반환형이 int& 형식으로 되어 있는 함수이다. 쉽게 생각해버리면 파라미터에 참조자가 선언되어있으니, 이 참조자를 그대로 반환하는 함수구나 라고 할 수 있다. 그러나 생각을 좀 달리 해야 한다. 왜 그런지 한 번 생각해보고 다음 예제를 보길 바란다.

#include <iostream>

using namespace std;

int& RefRetFuncOne(int &ref) {
	ref++;
	return ref;
}

int main() {

	int num1 = 1;
	int &num2 = RefRetFuncOne(num1);

	num1++;
	num2++;
	cout << "num1 : " << num1 << endl;
	cout << "num2 : " << num2 << endl;
	return 0;
}

즉, 위 예시를 보면 참조형으로 반환된 값을 참조자에 저장하면 참조자의 관계가 하나더 추가가 된다.

int num1 = 1;
int &ref = num1;// 인자가 전달될 ,
int &num2 = ref;// 함수의 반환과 반환 값의 저장에서 일어난 

여기서 기억해야 하는 게 있다. 함수의 파라미터로 선언된 참조자 ref는 지역변수와 동일한 성격을 가진다. 즉, 함수가 반환을 하면 참조자 ref는 소멸된다. 그러나 참조자는 참조자일 뿐, 그 자체로 변수는 아니기 때문에 참조자가 참조하는 변수는 소멸되지 않는다. 따라서 ref는 소멸되도 num1과 num2는 남아있다.

그럼 다음과 같이 변경해보자.

#include <iostream>

using namespace std;

int& RefRetFuncOne(int &ref) {
	ref++;
	return ref;
}

int main() {

	int num1 = 1;
	int num2 = RefRetFuncOne(num1);

	num1+=1;
	num2+=100;
	cout << "num1 : " << num1 << endl;
	cout << "num2 : " << num2 << endl;
	return 0;
}

아마 조금은 이상하게 보일 수 있다.

int num1 = 1;
int &ref = num1;// 인자가 전달될 ,
int num2 = ref;// 함수의 반환과 반환 값의 저장에서 일어난 

즉, num1과 ref는 같은 공간의 값이고, num2는 다른 곳에 값을 가지게 된다. 그러므로 반환형이 참조형일 때, 반환 값을 무엇으로 저장하는냐에 따라서 결과에 차이가 있을 수 있다.

다음 예제도 보길 바란다.

#include <iostream>

using namespace std;

int RefRetFuncTwo(int &ref) {
	ref++;
	return ref;
}

int main() {

	int num1 = 1;
	int num2 = RefRetFuncOne(num1);

	num1+=1;
	num2+=100;
	cout << "num1 : " << num1 << endl;
	cout << "num2 : " << num2 << endl;
	return 0;
}

위 예제는 참조자를 반환하지만 반환형은 기본자료형인 경우다. 실행결과는 그 위의 예제와 차이가 없다. 심지어 실행과정에서 일어나는 일도 동일하다. 그렇지만 차이가 있다. 반환형이 참조형인 RefRetFuncOne 같은 경우는 두가지 형태로 저장이 가능하다.

int num2 = RefRetFuncOne(num1) --- o

int &num2 = RefRetFuncOne(num1) --- o

그러나 반환형이 기본자료형으로 선언된 RefRetFuncTwo 함수의 경우는 반드시 변수에 저장해야 한다.

int num2 = RefRetFuncTwo(num1) --- o

int &num2 = RefRetFuncTwo(num1) --- x

조금 혼란스러울 수 있지만 반복적으로 보길 바란다.


잘못된 참조의 반환


이제 잘못된 것도 알아야 잘 쓸 수 있다. 한번 알아보도록 하자.

int& RetuRefFunc(int n){
	int num = 20;
    num+=n;
    return num;
}

아마 반환형에 대해 잘 공부했다면 뭐가 잘못 되어있는지 잘 알 수 있다. 위 함수는 지역변수 num에 저장된 값을 반환하는 게 아니라 참조의 형태로 반환한다. 따라서 반환을 헀을 때,

int& ref = RetuRefFunc(10);

의 형태로 호출이 일어나면, 지역변수 num에 ref라는 참조자가 생기게 되고. 정작 지역변수는 소멸이 되므로 큰 문제가 생긴다. 문제는 컴파일러가 에러를 던저주지 못한다. 가끔은 심지어 정상출력이 될 때도 있으니 더욱 조심해야 한다.

그럼 const 참조자의 또 다른 특징에 대해 알아보자. 이전 포스팅에서 const에 대해 설명했었다.

const int num = 20;
int& ref = num;
ref += 10;
cout << num << endl;

위 예제를 보자. 분명 const로 num을 상수화 선언을 했는데, ref로 참조자를 주어 값을 변경하고 있다. 만약 허용이 된다면 const를 쓴 의미가 없어진다. 다행히 C++에서는 이를 허용하지 않고 있다. 참조자 선언에서 컴파일에러를 일으킨다. 그러므로 상수화된 변수에 대한 참조자 선언은 다음과 같다.

const int &ref = num;

즉, 참조자 역시 ref를 통한 값의 변경이 안되므로 논리적인 문제가 생기지 않는다. 또한,

const int &ref = 50;

과 같은 형태도 가능하다.

여기서는 의문이 들어야 한다. 분명 참조자는 변수만 참조 가능한데, 갑자기 상수를 참조한다. 그 이유를 설명해 보겠다.

먼저 이 문장으로 보도록 하자.

int num = 20 + 30;

여기서 20과 30과 같이 프로그램상에서 표현되는 숫자를 리터럴(literal) 또는 리터럴 상수(literal constant)라 한다. 이는 임시적으로 존재하는 값이고 다음 행으로 넘어가면 존재하지 않는 상수라는 특징이 있다.

무슨 뜻이냐면 연산을 위해서는 20도, 30도 모두 메모리 공간에 저장되어야 한다. 하지만 저장됬다고 해서 재 참조가 가능한 값은 아니다. 즉, 다음 행으로 넘어가면 소멸되는 상수라고 해도 틀리지 않다. 그런데 이러한 상수를 참조한다는 것은 맞지 않는 얘기다. 즉, 저런 숫자들은 변수에 할당 되는 것이 아니니 사라진다는 것이다.

그럼 다시 저 문장을 보도록 하자.

const int &ref = 50;

이것은 숫자 30이 메모리 공간에 계속 남아있을 때 가능한 문장이다. 일반적으로 볼 때는 불가능 하다고 보는게 맞다. 그러나 C++에서는 이 문장이 성립할 수 있게, const참조자를 이용해서 상수를 참조할 때는 임시변수를 만든다.

그렇다면 이 임시변수를 참조자가 참조하는 겪이 되므로 문제가 사라지게 된다.

그렇다면 왜 이렇게 해 두었을까?

int Adder(const int& A, const int& B){
	return A+B;
}

여기서 main에서는 여러가지 상황이 나온다. 그럴 떄 만약 파라미터로 상수를 넣어야 하는 상황이 되면 엄청 복잡해진다. 따라서 const 참조자의 상수참조를 허용하게 함으로서 Adder(3,4);와 같은 문장이 가능하도록 한다.


new & delete


computer

C를 공부할 때, malloc과 free 함수의 필요성에 대해 이해하는 게 어쩌면 어려웠을 지도 모른다. 그렇지만 이 책에선 위 내용을 이미 알고 있다고 가정하고 설명하므로 혹시나 모른다면 한 번 보고 오는 게 좋다.

길이 정보를 인자로 받아서, 해당 길이의 문자열 저장이 가능한 배열을 생성하고 배열의 주소값을 반환하는 함수를 정의해보자.

#include <iostream>
#include <string.h>
#include <stdlib.h>

using namespace std;

char* MakeStrAdr(int len){
	char* str = (char*)malloc(sizeof(char)*len);
    return str;
}

int main(void){
	char* str = MakeStrAdr(20);
    strcpy(str,"I am so happy~");
    cout << str << endl;
    free(str);
    return 0;
}

C 언어 동적할당이다. 그럼 불편한 점을 보자.

할당할 대상의 정보를 무조건 바이트 크기단위로 전달해야 한다.

반환형이 void형 포인터이기 때문에 적절한 형변환을 거쳐야 한다.

그러나 C++은 불편함을 많이 감소 시켰다. new는 malloc을 대신하고 delete는 free를 대신하는 키워드다.

다음을 보자.

int 변수의 할당 ---- int* ptr1 = new int;
double 변수의 할당 ---- double
길이가 3 int 배열의 할당 ---- int* arr1 - new int[3];
길이가 7 double 배열의 할당 ---- double* arr2 = new double[7];

그럼 그대로 delete를 보자.

앞서 할당한 int 변수의 소멸 ---- delete ptr1;
앞서 할당한 double 변수의 소멸 ---- delete ptr2;
앞서 할당한 int 배열의 소멸 ---- delete []arr1;
앞서 할당한 double 배열의 소멸 ---- delete []arr2;

아마 바로바로 이해가 될 것이다.

이제 C++에서 malloc과 free함수를 쓰지 말자. 특히나 위 함수의 호출이 문제가 될 수도 있다.

어떤 문제가 생길 수 있는지 간단히 설명해 보겠다. 우리는 아직 class를 언급한 적이 없으므로 간단히 언급만 하고 넘어갈 것이고, 크게 이해하는 것에는 문제가 없을 것이다.

#include <iostream>
#include <stdlib.h>

using namespace std;

class Simple {
public:
	Simple() {
		cout << "I`m simple constructor!" << endl;
	}
};

int main() {

	cout << "case 1 : ";
	Simple* sp1 = new Simple;

	cout << "case 2 : ";
	Simple* sp2 = (Simple*)malloc(sizeof(Simple) * 1);

	cout << endl << "end of main" << endl;
	delete sp1;
	free(sp2);

	return 0;
}

위 예제소스를 보면 (이해는 아직 못해도 된다.) Simple이라는 자료형의 변수로 할당 시키는 것인데, newmalloc 두가지로 구현한 것이다.

그런데 실행에서 차이가 있다. 아마 new로 선언한 것은 “I`m simple constructor!” 라는 것이 찍힐 것이고, malloc은 그렇지 않을 것이다.

그냥 이정도만 알면 된다. new와 malooc 함수의 동작방식에는 차이가 있다. 아니면 둘은 분명 차이가 있다.

후에 class와 생성자 등을 공부하면서 다시 언급할 것이다.


Heap


이제 Heap에 할당된 변수에 대해 얘기를 해볼 것이다. 이전에 언급했지만 Heap에 저장되는 값은 동적할당에 관련이 깊다.

보통 동적할당에 대해서는 포인터로 접근을 한다. 예를 들어

int* ptr = new int;

와 같이 ptr변수에 동적으로 할당을 해주었다. 즉, ptr이라는 변수에 new 연산자로 메모리 공간을 할당한 것이다. 이 메모리 공간의 이름을 ptr이라는 변수로 할당한 것이고 여기에 참조자 역시 가능하다.

int &ref = *ptr; ref = 20; cout << *ptr << endl;

위 코드의 출력은 20이 된다.

흔히 사용하진 않지만 참조자의 선언으로 포인터 연산없이 heap영역에 접근했다는 것에 주목하면 된다.

그리고 예제가 있다.

이 책을 하면서 느끼는 건 예제는 꼭 풀어봐야 한다는 것이다.

예제가 종합적인 설명과 이해하고 있는 지를 확실히 알 수 있다.

이번 예제는 구조체를 이용한 문제인데, 2차원 좌표 구조체를 주고, 합을 구하는 함수를 만드는 것이다.

주어진 코드는

typedef struct __Point{
int xPos;
int yPos;
} Point;



Point& PntAdder(const Point &p1, const Point $p2);

이다.

밑에는 내가 작성한 코드고, 본인이 꼭 작성한 후에 참조해보면 좋을 것이다. 주석도 처리해 두었으니 쉽게 읽을 수 있을 것이다.

#include <iostream>
#include <stdlib.h>

using namespace std;

typedef struct __Point {
	int xPos;
	int yPos;
} Point;

Point& putAdder(const Point& X, const Point& Y);

int main() {
	
	Point* A = new Point;
	Point* B = new Point; //A B 포인터

	A->xPos = 1;
	A->yPos = 2;
	B->xPos = 3;
	B->yPos = 4;

	Point& C = putAdder(*A, *B); //A 가리키는 주소의 값과 B 가리키는 값을 넣음(주소 x)
								 //참조자 C return result 가리키는 값과 연결(주소 x)
	cout << C.xPos << " " << C.yPos << endl;
	return 0;
}

Point& putAdder(const Point& X, const Point& Y) {
	//X,Y Point형의 참조자 선언
	//참조자 X A 가리키는 값과 연결(주소 x), 참조자 Y B 가리키는 값과 연결(주소 x)
	//const 선언으로 X Y 이용한 값은 바뀌지 않음을 선언 (call by value) , 주소값를 참조하지 않음을 말함

	Point* result = new Point;
	result->xPos = (X.xPos) + (Y.xPos); //result 포인터, X Y Point 형의 변수
	result->yPos = (X.yPos) + (Y.yPos);

	return *result; //result 가리키는 값을 return (주소 x)

}

C++에서 C언어의 표준함수 호출


보통 C++을 프로그래밍 하다보면 자신이 알고 써온 C언어 표준함수를 쓰고 싶을 때가 있다. 그 때 어떻게 하면 좋을지 알아 보도록 하자.

보통 C 언어의 라이브러리에는 매우 다양하게 함수들이 정의되어 있다. 이런 라이브러리는 C++에도 표준 라이브러리로 포함되어 있다. 다음은 헤더정보를 일부 가져온 것이다.

#include <stdio.h> ---> #include <cstdio>
#include <stdlib.h> ---> #include <cstdlib>
#include <math.h> ---> #include <cmath>
#include <string.h> ---> #include <cstring>

굉장히 많이 쓰는 라이브러리들을 정리해 두었다. 위 예시만 봐도 알 수 있듯이, h를 제거하고 앞에 C를 넣으면 된다.

사실 namespace std 내에 선언되어 있다는 것만 제외하면 크게 차이가 없다.

#include<cmath>
#include <cstdio>
#include <cstring>

using namespace std;

int main() {
	
	char str1[] = "Result";
	char str2[30];

	strcpy(str2, str1);
	printf("%s : %f \n", str1, sin(0.14));
	printf("%s : %f \n", str2, abs(-1.25));
	return 0;
}

참고로 stdio.h를 선언해도 되긴 되지만, C++의 헤더를 기반으로 하도록 하자. 또한 namespace std를 생략해도 될 때가 있다(최근엔 많은 컴파일러가 그렇게 한다.) 그러나 아닌 컴파일러도 있으므로 꼭 써두도록 하자.

그럼 C++의 헤더를 선언해야 하는 이유를 보자.

C++ 관점에서 여전히 C스타일의 함수호출을 허용하는 것은 하위 버전과의 호완성을 제공하기 위해서 이다.

그리고 C에서의 함수와 C++에서의 함수가 꼭 일치하는 것은 아니다. 예를 들어 abs함수같은 경우, C표준에는 int abs(int num)으로 되어 있다. 그러나 C++에서는 다음으로 오버로딩 되어 있다.

long abs(long num)

float abs(float num)

double abs(double num)

long double abs(long double num)

즉, C++에서는 오버로딩이 가능하므로 자료형에 따라서 함수이름을 달리하지 않고 사용하기 편하게 해두었다. 따라서 C++에서는 C++ 헤더를 사용하도록 하자.

지금까지 Part 1부분으로 C언어 기반의 C++에 대해 알아보았다. 이제 다음 부터는 Part 2부분으로 객체지향의 도입부터 제대로 들어간다.

이후부터는 다음 포스팅에서 하도록 하겠다.

Blog Logo

구찌


Published

Image

구찌의 나도한번 해블로그

구찌의 개발 블로그 입니다.

Back to Overview