본문으로 바로가기
반응형

IO 스트림

C의 기본적인 입출력은 printf, scanf이다.

C++은 더 객체 지향적인 cin, cout 입출력 객체를 제공한다.

cout << 데이터 << 데이터 ...    // 출력
cin  >> 변수                    // 입력

'<<' 연산자로 cout 객체에 데이터를 보내기만하면 된다. 데이터를 연속적으로 보낼수 있으며, 데이터 타입은 자동으로 판별하기때문에 %d, %s 같은 서식을 지정할 필요가 없으므로 서식이 잘못되서 프로그램이 다운되는 경우도 없어서 안전하다고 볼 수 있다.

 

C++에서 기본 데이터(?)를 출력해보자.

// 파일이름 : cout.cpp

#include <iostream>
using namespace std;

int main()
{
	cout << "First C++ Program" << endl;

	int i = 56;
	char ch = 'S';
	double d = 2.414;

	cout << i << ch << d << endl;
}

cout를 사용하려면 C++ 기본 헤더파일인 iostream을 포함해야한다.

문자열을 cout 객체로 보내면 그대로 출력되고, endl은 개행하라는 뜻이며 C의 \n 과 같다.

cout는 여러개의 데이터를 연쇄적으로 보낼 수 있고 자동으로 타입을 판별한다. 

데이터는 정수든 실수든 문자열이든 어떤 타입이든 '<<' 연산자로 보내기만하면 내부에서 자동으로 처리한다.

 

 

C++에서 데이터를 입력해보자.

// 파일이름 : cin.cpp

#include <iostream>
using namespace std;

int main() 
{
	int age;
	cout << "나이를 입력하세요 : ";
	cin >> age;
	cout << "당신은 " << age << "살 입니다." << endl;
}

C++에서 입력할때는 cin객체를 사용하며 '>>' 연산자로 입력 내용을 변수로 보낸다.

cin >> age 문장은 정수값을 입력받아 age 변수로 보낸다는 뜻이다.

변수의 타입은 자동으로 구분하며, 데이터 보내는 형식도 직관적이라서 scanf에 비하면 편리하게 사용할 수 있다.

 

 

셀프테스트) IO 스트림으로 정수값 두 개를 입력받아 그 합을 출력하는 프로그램

#include <iostream>
using namespace std;

int main() 
{
	int a = 0;
	int b = 0;

	cout << "첫번째 정수 입력 : ";
	cin >> a;

	cout << "두번째 정수 입력 : ";
	cin >> b;

	cout << "두 정수의 합 : " << a + b << endl;
}

 

 

 

 

레퍼런스

레퍼런스는 변수에 대해 별명을 정의하여 이름을 하나 더 붙인다. 포인터와 비슷한데 차이점도 많다.

포인터는 *를 붙이는데, 레퍼런스는 &를 붙인다.

 자료형 &변수 = 대상체;

 

레퍼런스는 처음 선언할때 대상체를 정해야하므로 초기값으로 반드시 대상을 지정해주어야한다.

(별명을 부르면 누군지 딱 아는데 그 사람이 없으면 별명을 불러도 누군지 모르는 거랑 비슷한 것임)

 

// 파일이름 : ref1.cpp

#include <stdio.h>

int main() 
{
	int i = 3;
	int& ri = i;

	printf("i = %d, ri = %d\n", i, ri);
	ri++;
	printf("i = %d, ri = %d\n", i, ri);
	printf("i번지 = %x, ri번지 = %x\n\n", &i, &ri);

	// pi로 데이터를 건드려보기
	int* pi;
	pi = &i;

	printf("pi의 값 : %d, pi의 주소 : %p\n", pi, &pi);
	printf("pi가 가리키는 값 : %d\n", *pi);

	*pi = i + 1;
	printf("pi의 값 : %d, pi의 주소 : %p\n", pi, &pi);
	printf("pi가 가리키는 값 : %d\n", *pi);
}

 해당 코드에서 정수형 변수 i를 선언하며 3으로 초기화했는데, 정수형 레퍼런스 ri를 i로 초기화하면 i에 대해 ri라는 별명이 하나 더 생기는 것이다.

 이후 i와 ri는 완전히 동일한 대상을 가리켜 둘 중 하나를 바꾸면 나머지 하나도 같이 바뀐다.

 ri를 1 증가시키면 ri뿐만아니라 i도 같이 증가하여 둘 다 4가 되며 반대로 i를 변경하면 ri도 같이 바뀐다.

 두 변수가 저장되어있는 번지를 출력해보면 메모리 위치가 같게 나오는것도 이러한 원리 때문이다.

 

포인터를 사용했을때는 *pi가 i를 가리키고있어서 뭔가 연결(?) 되있는 느낌이있다면

레퍼런스는 i의 주소값을 다른 이름으로 부르는 느낌으로 생각하는 것이 쉽다.

 

레퍼런스가 저장되있는 주소가 같으니 두 변수는 이름만 다른 완전히 같은 변수이다.(이래서 별명과 같은 원리이다)

 

 

 

레퍼런스의 일반적인 특징 및 주의 사항

- 레퍼런스는 같은 타입에 대해 붙이는 것이기때문에 레퍼런스와 대상체는 타입이 완전히 일치해야한다.

int i;            // 가능
int &ri = i;      // 에러
double &rd = i;   // 에러
short &rs = i;    // 에러
unsigned &ru = i; // 에러

 

- 레퍼런스 대상체는 실제 메모리를 점유하는 좌변(L-value)값이어야 한다.

상수는 좌변값이 아니므로 레퍼런스로 만들 수 없고 비트 필드도 주소가 없어 레퍼런스의 대상체가 될 수 없다.

int &ri = 123;

만약 이 선언이 허용된다면 ri = 456; 대입문으로 상수값을 바꿀 수 있으므로 허용되지 않는다.

대신, const 지시자를 붙이면 상수를 가리킬 수 있다.

 

- 레퍼런스 생성시 대상체를 분명하게 지정해야한다.

포인터는 선언만 해놔도되는데, 나중에 가리킬 변수의 번지를 대입할 수 있다. 

레퍼런스는 처음만들때부터 누구의 별명인지 명확히 지정해야하며 NULL 레퍼런스는 인정되지 않는다.

int i;

int *pi;  // 가능
pi = &i;

int &ri;  // 에러
ri = i;

초기값이 없는 int &ri; 선언문 자체가 에러가 떠서 컴파일 자체가 되지 않을 것이다.

 

대신 예외적으로 초기값없이 레퍼런스를 선언할 수 있는 경우가 있는데,

1. 함수의 형식인수로 사용되는 레퍼런스, 호출될 때 실인수의 별명으로 초기화

2. 클래스의 멤버로 선언된 레퍼런 => 생성자에서 초기화

3. 변수를 extern 선언할때는 외부에서 이미 대상체가 지정되어 있음.

 

말이 어려우니 차차 알아가도록하고 결론적으로 레퍼런스를 선언할때는 꼭 초기값인 대상체가 있어야한다는 점을 기억하자.

 

 

레퍼런스 인수

포인터가있는데 쓸데없이 레퍼런스라는 개념을 또 배우는게 쓸데없는 짓일수도 있다.

하지만 함수의 인수로 전달될때 레퍼런스의 위력을 실감할 수 있다.

 

레퍼런스를 이용해서 두 값을 바꾸는 Swab 함수 작성

#include <iostream>
using namespace std;

void ref_swab(int &a, int &b);

int main() 
{
	int a = 10;
	int b = 20;

	cout << "a : " << a << endl;
	cout << "b : " << b << endl;
	
	ref_swab(a, b);

	cout << endl;
	cout << "a : " << a << endl;
	cout << "b : " << b << endl;
}

void ref_swab(int &x, int &y)
{
	int temp;
	temp = x;
	x = y;
	y = temp;
}

매개변수를 받을때 &x, &y를 통해서 받은 것에 주목하자.

C에서는 함수의 인수를 포인터로줬는데,

C++에서는 함수의 인수를 실인수로 주고 매개변수로 받을때만 레퍼런스를 사용해서 인수를 전달해줄때 좀 더 편하게 사용할 수 있다.

 

만약 swap 함수의 매개변수를 &로 받지 않는다면 지역 변수 내에서만 값이 바뀌고 결국 main 함수에는 아무일도 일어나지 않는다.

매개변수에 &를 뺀 다음에 출력하면 가진값 그대로 출력된다

 

call by value(값호출) : 실인수를 복사해서 매개변수에 적용

메인에서 swap(a, b);

void swap(int x, int y) { }    // a, b값이 x, y값으로 복사된다.

 

 

포인터에 비해 레퍼런스를 받는 방법은 형식인수를 이름으로 참조할 수 있어서 깔끔하다.

포인터가 가리키는 실인수를 참조하기 위해 일일이 *를 붙이는 것은 무척 귀찮은 일이고 실수로 *를 빼먹으면 런타임 에러가 발생할 수 있기 때문에 이중포인터를 넘길때는 레퍼런스가 더 직관적이다.

// 파일이름 : refptr

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <malloc.h>
#include <string.h>

// 이중포인터로 매개변수 전달
//void InputName(char** Name)
//{
//	*Name = (char*)malloc(32);
//	strcpy(*Name, "Hong Kil Dong");
//}
//
//int main() 
//{
//	char* Name;      // 포인터 선언
//
//	InputName(&Name); 
//	printf("이름은 %s입니다.", Name);
//	free(Name);
//}

// 레퍼런스로 매개변수 전달
void InputName(char*&Name)       
{
	Name = (char*)malloc(32);
	strcpy(Name, "Hong Kil Dong");
}

int main()
{
	char* Name;      // 포인터 선언

	InputName(Name);
	printf("이름은 %s입니다.", Name);
	free(Name);
}

 

char *&Name인수가 포인터의 레퍼런스이다. 포인터 자체를 받았으므로 Name이라는 이름으로 메모리를 할당하고 여기에 문자열을 복사해서 넣게된다.

 

포인터는 잠재적 배열이어서 주변값을 건드릴 수 있는데 레퍼런스는 대상체만 액세스 할 수 있어서 비교적 안전하다.

그러나 호출부의 형식이 값 호출과 같아서 함수의 원형을 봐야 참조 호출인지 값 호출인지 알 수 있는 번거로움 또한 존재한다.

그래서 레퍼런스를 받는 함수는 이름 뒤에 Ref나 ByRef 등의 접미를 붙여주는 것이 좋다.

 

 

레퍼런스에 대한 레퍼런스는 선언할 수 없다. 레퍼런스가 별명인데 별명에 대해 또 다른 별명을 붙이는 것은 가치가 없다. 포인터는 2중 포인터가 있지만 레퍼런스는 중첩을 허락하지 않는다.

 

 

포인터변수 선언한 다음에 레퍼런스 변수의 주소를 저장할수는 있다.

i의 주소를 가리킴.

 

 

레퍼런스 대상체

레퍼런스가 특수하고 신기해보일 수 있지만, 내부를 들여다보면 포인터의 변형이라고 볼 수 있다.

실용성은 없지만 배열이나 함수에 대한 레퍼런스도 가능은하다.

그러나 모든 타입에 대한 레퍼런스를 다 선언할 수 있는것은 아니며 일부 불가능한 형식도 있다.

 

- 레퍼런스의 중첩은 허용되지않는다.

int i;
int &ri;
int &rri = ri;

포인터는 이중포인터가 있지만 레퍼런스는 중첩이 되지않는다.

 

 

- 포인터에 대한 레퍼런스는 가능하지만 역으로 레퍼런스에 대한 포인터는 선언할 수 없다.

int i;
int &ri = i;
int &*pri = &ri;   // 에러

레퍼런스에대한 포인터는 레퍼런스의 대상체에 대한 포인터형이다.

즉, 단순 포인터와 같으며 굳이 레퍼런스의 포인터형을 정의할 필요도 없다.
(굳이 보자면 int *pi = &ri; 선언은 가능은한데 pi가 ri, 즉 i의 주소를 가리킴 => 쓸데없이 복잡함)

 

- 레퍼런스의 배열도 선언할 수 없다.

int i, j;
int &ra[2] = {i, j};  // 에러

배열은 곧 포인터인데 레퍼런스에대한 포인터를 선언할 수 없으므로 배열도 선언할 수 없다.

i, j의 레퍼런스가 필요하면 각각 따로 레퍼런스를 선언해야한다.

 

레퍼런스 리턴

함수에 레퍼런스 타입을 반환할 수도 있다. 이렇게 되면 리턴되는 대상이 좌변값의 별명이다 보니 함수 호출문에 값을 대입하는 것이 가능해진다.

#include <stdio.h>

int ar[] = { 1,2,3,4,5 };

int& GetAr(int i)   // 콜바이value에 의해 i에 3을 복사해서 저장
{
	return ar[i];   // ar[3]을 리턴 

	//return 3;     // 3이 상수이므로 int&와 맞지않으므로 에러뜸
}

int main() 
{
	// int& ra = 10;  // 상수에는 별명을 줄 수 없음.(에러)

	GetAr(3) = 6;     // 실인수 전달후에 그 변수에 6을 대입
	printf("ar[3] = %d\n", ar[3]);
}

C의 함수는 값을 리턴하기 때문에 이런 형식을 쓸 수 없지만 레퍼런스는 좌변값이어서 함수 호출문에 뭔가를 대입할 수 있다.

 

레퍼런스 내부

레퍼런스가 특수하고 신기해보일 수 있지만, 내부를 들여다보면 포인터의 변형에 불과하다.

컴파일러는 레퍼런스를 포인터로 바꿔 컴파일하며 내부 구현도 포인터로 되어있다.

ref1.cpp 예제는 내부적으로 오른쪽처럼 컴파일된다

int &ri = i; 선언문에 대해 컴파일러는 ri를 정수형 포인터로 생성하고 i의 주소값으로 초기화한다.

레퍼런스가 포인터이므로 대상체에 대해 암시적으로 & 연산자를 붙인다. 

코드에서 ri를 참조하는 모든 문장에 대해 암시적으로 * 연산자를 적용하여 ri가 가리키는 대상체를 읽는다.

(*ri)가 곧 i와 같으니 ri는 i의 완전한 별명으로 동작한다.

 

 

이를 통해 레퍼런스는 컴파일러가 내부에서 절묘하게 조작하는 포인터라고 볼 수 있다.

하지만 주소를 읽고 내용을 액세스하는 &, * 연산자를 붙이는 과정이 자동화되어있어서 코드가 좀 더 간결해지는 이점이 있다. 포인터에대해 암시적 연산자를 붙여 일반 변수처럼 쓸 수 있게 해주는 편의적인 문법이다.

 

또, 클래스가 완전한 타입이 되기 위해서는 기능뿐만 아니라 형식도 기본타입과 일치시켜야하는 복사 생성자나 연산자 오버로딩을 쓸때 레퍼런스가 유용하다.

 

따라서 포인터를 사용하는 경우에 레퍼런스를 사용하면 좀 더 편하게 사용할 수 있고, 대상체의 별명이라는 점만 기억하고 넘어가도록 하자..

반응형