본문 바로가기
프로그램 개발/C++ 개발

C++ 문법 살펴보기 3(함수, 배열, 반복문)

by minchel1128 2021. 5. 29.

이전 게시글에서 내용 이어집니다.

1. 함수

규모가 어느 정도 커지게 된 프로그램의 경우에 모든 코드를 main() 함수에 넣게 되면 관리가 힘들어지고 코드가 복잡해지는 문제가 발생하게 됩니다. 따라서 이들을 기능별로 각각 함수로 만들어 나눠서 작성하는 것이 좋습니다. C++에서는 다른 고급 언어들과는 다르게 반드시 사용하려는 위치보다는 앞에서 선언해야 합니다. 함수를 특정 파일 안에서만 사용하는 경우에는 선언과 구현을 모두 소스파일에 작성하고 다른 모듈이나 파일에도 사용한다면 함수의 선언은 헤더 파일에 작성하고 구현은 소스파일에 작성합니다. 함수를 선언하는 문장을 함수 원형 또는 함수 헤더라 부릅니다. 함수의 내용을 보지는 않고 함수에 접근하는 방식을 표현한다는 의미를 가지고 있습니다. 또한 리턴 타입을 제외한 이름과 매개변수 목록을 함수 서명이라고 부릅니다.

함수를 선언하는 방법은 다음과 같은 방식으로 선언합니다.

[리턴 타입] [함수 이름]([매개변수]){[함수 내용]}

예시로는 다음과 같습니다.

void myFunction(int i, int j);

다만 이처럼 동작을 구현하지 않는다면 링크 과정에서 에러가 발생하게 됩니다. 이 부분을 앞에 쓰고 사용할 위치 뒤에 다시 재정의 하는 방식도 있긴 하지만 사용하는 부분이 보통 main에서 사용하므로 main을 맨 마지막에 작성하는 것이 권장됩니다. 그러므로 일반적으로 정의하는 방식은 다음과 같습니다.

int sum(int num1, int num2)
{
    int result = num1 + num2;
    return result;
    // return num1 + num2; 로 작성하여도 됩니다.
}

이 경우에는 호출하는 방법은 다음과 같습니다.

sum1 = sum(3, 5);
sum2 = sum(sum1, 12);
sum3 = sum(number1, number2);
sum4 = sum(4, number1);

여기서 리턴 타입을 void로 지정하면 return을 사용하지 않고 마찬가지로 호출할 때도 함수와 매개 변수만 입력하면 됩니다. 매개변수에서도 위의 예시처럼 int 이외의 다른 자료형을 사용할 수 있습니다.

기존의 C언어에서는 매개변수를 받지 않는 함수의 결루에서는 매개변수 자리에 void를 적어야 되었지만 C++에서는 그냥 비워도도 됩니다. 하지만 리턴 값이 없음을 표시하기 위해서는 반드시 void라고 적어야 합니다.

이런 식으로 작성할 수도 있습니다.

void print(int a, char b, double c)
{
    std::cout << "정수값 numInt의 값은 " << a << "입니다." << std::endl;
    std::cout << "문자 ch는" << b << "입니다." << std::endl;
    std::cout << "실수값 numDouble의 값은 " << c << "입니다." << std::endl;
}

int main(void)
{
    int numInt = 3;
    char ch = 'A';
    double numDouble = 3.14;
    
    print(numInt, ch, numDouble);
    return 0;
}

실행결과는 다음과 같이 나옵니다.

위 부분까지 합쳐서 사용하면 다음과 같이 나옵니다.

1-1. 함수 리턴 타입 추론

C++14부터는 함수의 리턴 타입을 컴파일러가 알아서 지정이 가능합니다. 이를 사용하려면 함수 리턴 타입 자리에 auto를 넣으면 됩니다. 예를 들면 다음과 같습니다.

auto addNumber(int num1, int num2)
{
    return num1 + num2;
}

이 경우 컴파일러는 return문의 표현식에 따라 리턴 타입을 추론하여 적용하게 됩니다. 예시의 상황에서는 int값으로 반환되게 됩니다. 리턴 값으로 재귀 호출을 지정할 수도 있는데 이때는 비 재귀 호출 return문도 반드시 함께 지정해야 합니다.

1-2. 현재 함수 이름

함수마다 내부적으로 __func__라는 로컬 변수가 정의되어 있습니다. 이 변수는 현재 함수의 이름을 값으로 가지고 있으며 주로 로그를 남기는데 활용합니다.

2. 배열

배열은 같은 타입의 값을 나란히 저장하며 각 항목은 배열에 놓인 위치로 접근합니다. C++에서는 배열을 선언할 때 반드시 배열의 크기를 지정해야 하고 크기는 변수로 지정할 수 없으며 상수 또는 상수 표현식으로 지정해야 합니다. 예를 들면 다음과 같이 선언합니다.

int arr[3];
arr[0] = 1;
arr[1] = 2;
arr[2] = 3;

C++ 언어에서의 배열의 첫 번째 원소의 위치는 1이 아닌 0 임을 주의해야 하고 따라서 크기도 선언한 크기의 숫자에서 1을 뺀 값이 마지막 원소의 위치가 됩니다.

각 원소는 위의 예시처럼 직접 초기화하는 방식이 있고 반복문을 사용해 초기화하는 방식이 있고 영초 기화 방식이 있습니다. 영 초기화란 주어진 객체를 디폴트 생성자로 초기화하는 것으로 기본 정수 타입은 0으로 부동소수점(실수 값) 타입은 0.0으로 포인터 타입은 nullptr로 초기화됩니다.

영초 기화 방법은 다음과 같습니다.

int arr[3] = {0};
//int arr[3] = {};와 동일

이니셜 라이저 리스트를 사용해도 되며 이니셜 라이저 리스트를 사용하는 경우 배열의 크기를 컴파일러가 알아서 결정을 해줍니다.

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

배열의 크기를 지정할 때 이니셜 라이저 리스트에 나온 원소 수가 배열의 크기로 지정한 개수보다 적은 경우 나머지 원소는 0으로 초기화됩니다.

int arr[3] = {1};

위와 같이 초기화하는 경우 arr [0]은 1로 초기화되고 나머지 arr [1], arr [2]는 0의 값을 가지게 됩니다.

C++17부터는 <array> 헤더를 인클루드 한다면 std::size()를 이용해 배열의 크기를 구할 수 있습니다. 또한 기존 방식대로 sizeof 연산자로 크기를 구하여도 됩니다.

unsigned int arraySize = std::size(arr);
//unsigned int arraySize = sizeof(arr) / sizeof(arr[0]); 와 동일

C++은 다차원 배열도 지원하며 n차 배열을 사용하지만 일반적으로는 3차를 넘어서는 경우에는 구조의 이해가 잘 되지 않는 점과 무수히 많은 메모리 소모로 인하여 사용하지 않는 경우가 많습니다.

일반적인 2차원 배열은 int arr [3][3];와 같은 방식으로 선언을 하며 바둑판 혹은 표와 같은 모습을 나타냅니다.

arr [3][3]의 구조는 다음과 같습니다.

arr[0][0] arr[0][1] arr[0][2]
arr[1][0] arr[1][1] arr[1][2]
arr[2][0] arr[2][1] arr[2][2]

위와 같은 방식은 기존의 C언어에서 주로 사용하던 것으로 C++에서도 호환성을 위해 그대로 지원을 하지만 std::array라는 고정 크기 컨테이너를 대신하여 사용합니다. 이 타입은 <array> 헤더 파일에 정의가 되어있어 사용하려면 해당 헤더 파일을 인클루드를 해야 합니다.

std::array는 기존 C 스타일 배열에 비해 여러 장점을 가지는데 크기가 고정되어있어 항상 크기를 정확히 알 수 있고 자동으로 포인터를 캐스팅하지 않아 버그를 방지할 수 있으며 반복자로 배열에 원소에 대한 반복문을 쉽게 작성이 가능합니다. std::array는 고정 크기 컨테이너로 C 스타일 배열을 조금 더 업그레이드시킨 거와 유사하다.

array <매개변수, 크기> 변수 이름 = {초기화};로 작성을 합니다

array<int, 3> arr = {9, 8, 7};
cout << "Array Size = " << arr.size() << endl;
cout << "2nd Element = " << arr[1] << endl;

만약에 크기가 고정되어있지 않은 배열을 사용하고자 한다면 std::vector를 사용하면 됩니다. vector는 동적 할당을 하는 컨테이너로서 메모리 관리가 필요 없고 알아서 메모리 관리를 해 줍니다. vector는 범용 컨테이너로서 거의 모든 종류의 객체를 담을 수 있기 때문에 반드시 객체 타입을 명시해야 합니다. 또한 원소를 추가할 때는 push_back() 메서드를 사용하며 배열과 같은 방식으로 원소에 접근이 가능합니다.

vector는 array가 아닌 vector헤더 파일에 선언되어있습니다.

vector <객체 타입> 변수 이름 = {초기화};로 작성하여 사용합니다.

#include<iostream>
#include<vector>
using namespace std;

int main(void)
{
	//정수 타입의 벡터 생성
	vector<int> vec = { 1, 2 };

	//값 추가 입력
	vec.push_back(3);
	vec.push_back(4);

	//원소 접근
	for (int i = 0; i < vec.size(); i++)
	{
		cout << i + 1 << "번 원소 : " << vec[i] << endl;
	}
	cout << endl;
	//추가 입력
	vec.push_back(5);
	vec.push_back(6);
	cout << "값 추가 확인" << endl;
	//원소 접근
	for (int i = 0; i < vec.size(); i++)
	{
		cout << i + 1 << "번 원소 : " << vec[i] << endl;
	}
}

C++17부터는 구조적 바인딩이란 개념이 도입됐는데 이를 이용해 여러 개의 변수를 선언할 때 배열, 구조체, 페어 또는 튜플의 값으로 초기화가 가능해졌습니다.

구조적 바인딩을 적용할 때에는 반드시 auto 키워드를 붙여야 하며 왼쪽에 나온 선언할 변수 개수와 오른쪽에 나온 표현식 값 개수가 반드시 일치해야 합니다. 또한 모든 멤버가 non-static이면서 public으로 선언된 데이터 구조라면 구조체 같은 것도 적용이 가능합니다.

 

3. 반복문

반복문은 같은 내용을 반복하기 위해 사용하는 것으로 C++에서는 while, do-while, for, 범위 기반 for 등 4가지를 사용합니다.

while문은 while안의 조건이 참인 경우 일정한 코드 블록을 계속해서 반복합니다. 해당 반복문을 벗어나기 위해서는 break문을 사용하거나 조건을 거짓으로 만들면 됩니다. while문의 경우 고의적으로 무한루프를 만들기 위해 사용하는 경우도 있습니다.

do-while문은 while문과 같은 방식으로 동작하지만 약간의 차이점이 있습니다. while문은 코드 블록을 실행하기 이전에 조건을 검사해 참인 경우 실행하고 거짓이면 블록 다음으로 넘어가는 방식이지만 do-while문은 일단 해당 코드 블록을 전부 실행한 이후에 조건을 확인합니다.

for문은 한 줄에 초기 표현식, 종료 조건, 증감식(반복 시 실행할 문장)을 적게 되어 문법이 좀 더 간결하게 표현이 가능합니다.

범위 기반 for문은 컨테이너에 담긴 원소에 대해 반복문을 실행하는데 편하며 C 스타일의 루프, 이니셜 라이저 리스트, array, vector, 표준 라이브러리의 반복자를 반환하는 begin(), end() 메서드가 정의된 모든 타입에도 적용이 가능합니다.

#include<iostream>
#include<array>
using namespace std;

int main(void)
{
	int i = 0;
	//while문 반복
	while (i < 3)
	{
		cout << "while문 반복 입니다." << endl;
		i++;
	}
	printf_s("\n");//기존 C언어의 printf에서 타입 안정성이 높아진 함수 입니다.
	//i값 초기화
	i = 100;
	do {
		cout << "do while문 반복 입니다." << endl;
		i++;
	} while (i < 5);//우선 실행 확인
	printf_s("\n");
	for (int i = 0; i < 5; i++)
	{
		cout << "for문 반복 입니다." << endl;
	}
	printf_s("\n");
	array<int, 4> arr = { 1,2,3,4 };
	for (int i : arr)
	{
		cout << "i에 저장된 값은 " << i << " 입니다." << endl;
	}
    return 0;
}

위의 모든 반복문들을 정리하면 다음과 같은 실행결과가 나옵니다.

4. 이니셜 라이저 리스트

이니셜 라이저 리스트는 <initializer_list> 헤더 파일에 정의되어 있으며 여러 인수를 받는 함수를 쉽게 작성할 수 있습니다. initializer_list는 클래스 템플릿으로 원소 타입에 대한 리스트를 꺾쇠괄호로 묶어서 지정해야 합니다. 또한 타입에 안전하여 정의할 때 지정한 타입만 허용합니다. 즉 정수형과 실수형을 같이 쓰면 컴파일 에러가 발생합니다.

예시는 다음과 같습니다.

#include<iostream>
#include<initializer_list>
using namespace std;

int intSum(initializer_list<int> lst)//이니셜라이저 설정
{
	int sum = 0;
	for (int num : lst)
	{
		sum += num;
	}
	return sum;
}

int main(void)
{
	int sum1, sum2, sum3, sum4;
	sum1 = intSum({ 1, 2, 3 });
	sum2 = intSum({ 1,2,3,4,5,6,7 });
	sum3 = intSum({ 123,456,789,1230,4560,7890 });
	sum4 = intSum({ 1 });

	cout << "sum1의 값 : " << sum1 << endl;
	cout << "sum2의 값 : " << sum2 << endl;
	cout << "sum3의 값 : " << sum3 << endl;
	cout << "sum4의 값 : " << sum4 << endl;
	return 0;
}

0123456789

실행결과 및 데이터 저장 방식은 다음과 같이 이루어집니다.

이어서 C++의 고급 기능은 다음 게시글로 살펴보도록 하겠습니다.

728x90
반응형