스터디/C++ 스터디

C++ 챕터 1~ 9장 기초 정리

DDD Developer 2024. 9. 24. 09:43
728x90
반응형

C++는 C 언어의 강력함과 객체 지향 프로그래밍의 유연함을 결합하여 탄생한 프로그래밍 언어입니다. 1990년대에 등장하여 빠른 실행 속도와 효율성을 바탕으로 시스템 프로그래밍 분야를 석권했고, 오늘날에는 게임 개발, 금융, 인공지능 등 다양한 분야에서 핵심적인 역할을 수행하고 있습니다.

C++의 핵심: 세 가지 프로그래밍 방식의 조화

절차적 프로그래밍: C 언어의 핵심인 절차적 프로그래밍은 데이터 처리 절차를 명확하게 정의하여 프로그램을 구현하는 방식입니다.

객체 지향 프로그래밍: C++의 핵심 기능인 객체 지향 프로그래밍은 데이터와 함수를 하나의 객체로 묶어 코드의 재사용성과 유지보수성을 높입니다.

 

일반화 프로그래밍: C++의 템플릿 기능을 활용하는 일반화 프로그래밍은 다양한 데이터 형식에 대해 동일한 코드를 적용하여 효율성을 극대화합니다.



 

C++ 학습: C 기초부터 객체 지향, 그리고 일반화 프로그래밍까지

이 책은 C++ 학습 여정의 친절한 안내자입니다. C 언어에 대한 지식이 없어도 C++의 기초부터 차근차근 배울 수 있도록 구성되어 있습니다. C++의 핵심 개념인 객체, 클래스, 상속, 다형성 등을 배우고, 템플릿을 활용한 일반화 프로그래밍 기법을 익히게 될 것입니다.

C++의 역사: C 언어의 진화, 그리고 객체 지향 프로그래밍의 등장

C++는 1970년대에 개발된 C 언어의 철학을 계승하면서 객체 지향 프로그래밍 패러다임을 수용하여 탄생했습니다. C 언어는 Unix 운영 체제 개발을 위해 만들어졌으며, 저수준 언어의 효율성과 고수준 언어의 이식성을 결합한 혁신적인 언어였습니다.

C++ 프로그램 작성 및 실행 요령

C++ 프로그램을 작성하고 실행하는 과정은 다음과 같습니다.

  1. 소스 코드 작성: 텍스트 에디터나 IDE를 사용하여 C++ 코드를 작성하고 파일로 저장합니다. 이 파일이 소스 코드입니다. 파일 이름은 .cpp, .cxx, .cc 등의 확장자를 사용해야 합니다.
  2. 컴파일: 컴파일러를 사용하여 소스 코드를 컴퓨터가 이해할 수 있는 기계어로 번역합니다. 이렇게 번역된 파일이 목적 코드입니다.
  3. 링크: 링커를 사용하여 목적 코드와 라이브러리 코드, 시동 코드를 결합하여 실행 가능한 프로그램(실행 코드)을 생성합니다.

컴파일 및 링크 절차

  • Unix/Linux: CC 또는 g++ 명령을 사용하여 컴파일 및 링크를 수행합니다. 여러 개의 소스 파일을 컴파일할 때는 파일 이름을 나열하고, 필요한 라이브러리를 명시적으로 지정할 수 있습니다.
  • Windows 명령행: g++ 명령을 사용하여 컴파일 및 링크를 수행합니다.
  • Windows IDE (예: Visual Studio): 프로젝트를 생성하고 소스 파일을 추가한 후, IDE에서 제공하는 컴파일, 빌드, 링크, 실행 기능을 사용합니다.

IDE 활용

  • 통합 개발 환경: IDE는 코드 작성, 컴파일, 링크, 실행, 디버깅 등 프로그램 개발에 필요한 모든 기능을 제공합니다.
  • 프로젝트 생성: IDE에서 프로젝트를 생성하고 소스 파일을 추가하여 프로그램을 구성합니다.
  • 컴파일 및 실행: IDE에서 제공하는 기능을 사용하여 프로그램을 컴파일하고 실행합니다.
  • 디버깅: IDE의 디버깅 기능을 활용하여 프로그램의 오류를 찾고 수정할 수 있습니다.

주의 사항

  • 표준 준수: 특정 컴파일러에서 프로그램이 잘 동작한다고 해서 모든 컴파일러에서 동작한다는 보장은 없습니다. 최신 C++ 표준을 준수하는 것이 중요합니다.
  • 에러 메시지: 컴파일러는 에러 메시지를 통해 오류 발생 위치와 원인을 알려줍니다. 에러 메시지를 주의 깊게 읽고 문제를 해결해야 합니다.
  • 결과 확인: 프로그램 실행 결과를 확인하기 위해 cin.get();과 같은 코드를 추가하여 프로그램 종료를 잠시 멈출 수 있습니다.

 

C++ 시작하기: 기초부터 차근차근

2.1 C++의 시작: 첫 번째 프로그램

C++의 세계로 향하는 긴 여정의 첫걸음은 간단한 메시지 출력 프로그램입니다.

// myfirst.cpp - 메시지를 출력한다
#include <iostream>

int main() {
    using namespace std;

    cout << "C++의 세계로 오십시오." << endl;
    cout << "후회하지 않으실 겁니다!" << endl;

    return 0; 
}
 
 
  • 주석: //로 시작하는 주석은 컴파일러에 의해 무시되며, 코드 설명이나 개발자 메모 작성에 사용됩니다.
  • 전처리 지시자: #include <iostream>은 iostream 파일의 내용을 프로그램에 포함시키는 지시자입니다. iostream 파일에는 cout, cin, endl과 같은 입출력 기능이 정의되어 있습니다.
  • main() 함수: 모든 C++ 프로그램은 main() 함수에서 시작됩니다. int main()은 main() 함수가 정수 값을 반환한다는 것을 의미하며, 일반적으로 프로그램의 성공적인 실행을 나타내는 0을 반환합니다.
  • using namespace std;: std 이름 공간에 정의된 요소들을 사용할 수 있도록 허용하는 지시자입니다. cout, cin, endl은 std 이름 공간에 속합니다.
  • cout: cout 객체는 문자열, 숫자 등 다양한 정보를 출력하는 데 사용됩니다. << 연산자는 출력할 내용을 cout 객체에 전달합니다.
  • endl: endl 조정자는 새로운 행을 시작하고 출력 버퍼를 비우는 역할을 합니다.
  • return 0;: main() 함수를 종료하고 운영 체제에 0을 반환하여 프로그램의 성공적인 실행을 알립니다.

2.2 C++ 구문: 변수 선언과 대입

C++ 프로그램은 다양한 구문들의 집합으로 이루어져 있습니다. 이번에는 변수 선언과 대입 구문을 사용하여 데이터를 저장하고 처리하는 프로그램을 살펴보겠습니다.

// carrots.cpp - 음식물 처리 프로그램
// 하나의 변수를 사용하고 출력한다
#include <iostream>

int main() {
    using namespace std;

    int carrots;            // 정수 변수 carrots 선언
    carrots = 25;           // 변수 carrots에 값 25 대입
    cout << "나는 당근을 " << carrots << " 개 가지고 있다." << endl;

    carrots = carrots - 1;  // 변수 carrots의 값 변경
    cout << "아삭아삭, 이제 당근은 " << carrots << "개이다." << endl;

    return 0;
}
 
  • 변수 선언: int carrots;는 carrots라는 이름의 정수형 변수를 선언합니다. 변수는 데이터를 저장하는 메모리 공간에 대한 이름입니다.
  • 대입: carrots = 25;는 carrots 변수에 값 25를 대입합니다. =는 대입 연산자입니다.
  • cout의 다양한 활용: cout은 문자열뿐만 아니라 변수의 값도 출력할 수 있습니다. cout << carrots;는 carrots 변수에 저장된 값을 출력합니다.
  • 출력 결합: 여러 개의 cout 출력을 << 연산자를 사용하여 하나의 구문으로 결합할 수 있습니다.

2.3 C++의 기타 구문: 입력과 출력 결합

이번에는 cin 객체를 사용하여 사용자로부터 입력을 받고, cout을 사용하여 출력을 결합하는 프로그램을 살펴보겠습니다.

 
// getinfo.cpp - 입력과 출력
#include <iostream>

int main() {
    using namespace std;

    int carrots;

    cout << "당근을 몇 개나 가지고 있니?" << endl;
    cin >> carrots;        // C++ 입력

    cout << "여기 두 개가 더 있다. ";
    carrots = carrots + 2;

    // 다음 라인은 출력을연결한다.
    cout << "이제 당근은 모두 " << carrots << "개이다." << endl;

    return 0;

}
 
  • cin: cin 객체는 키보드와 같은 입력 장치로부터 데이터를 읽어오는 데 사용됩니다. >> 연산자는 입력 스트림에서 데이터를 추출하여 변수에 저장합니다.
  • 입력과 출력 결합: cout은 여러 개의 출력을 하나의 구문으로 결합할 수 있을 뿐만 아니라, 변수의 값과 문자열을 함께 출력할 수 있습니다.

2.4 함수: 프로그램의 기본 구성 요소

함수는 C++ 프로그램의 핵심 구성 요소입니다. 함수는 특정 작업을 수행하는 코드 블록이며, 프로그램을 모듈화하고 코드 재사용성을 높이는 데 중요한 역할을 합니다.

  • 함수 호출: 함수를 실행하려면 함수 이름과 필요한 매개변수를 사용하여 함수를 호출합니다.
  • 함수 정의: 함수 정의는 함수의 이름, 반환 유형, 매개변수 목록, 함수 몸체로 구성됩니다.
  • 함수 원형: 함수 원형은 함수의 인터페이스를 정의하며, 컴파일러에게 함수의 반환 유형, 이름, 매개변수 목록을 알려줍니다.
  • 라이브러리 함수: C++는 다양한 기능을 제공하는 표준 라이브러리 함수들을 제공합니다. sqrt(), pow(), rand() 등이 그 예입니다.
  • 사용자 정의 함수: 프로그래머는 필요에 따라 직접 함수를 정의하여 사용할 수 있습니다.

예제: 스톤을 파운드로 변환하는 함수

// convert.cpp - 스톤을 파운드로 환산한다
#include <iostream>

int stonetolb(int);  // 함수 원형

int main() {
    using namespace std;

    int stone;
    cout << "체중을 스톤 단위로 입력하시오: ";
    cin >> stone;

    int pounds = stonetolb(stone);
    cout << stone << " 스톤은 " << pounds << " 파운드입니다." << endl;

    return 0;
}

int stonetolb(int sts) {
    return 14 * sts;
}
 
  • tonetolb() 함수: 스톤 값을 입력받아 파운드 값으로 변환하여 반환하는 함수입니다.
  • 함수 호출: main() 함수에서 stonetolb(stone)을 호출하여 스톤 값을 파운드 값으로 변환합니다.
  • 반환 값: stonetolb() 함수는 계산된 파운드 값을 return 문을 사용하여 main() 함수에 반환합니다.

C++ 프로그램의 구조

 

C++ 프로그램은 함수들의 집합으로 구성되며, 각 함수는 독립적인 코드 블록입니다. 함수는 프로그램을 모듈화하고 코드 재사용성을 높여 효율적인 프로그램 개발을 가능하게 합니다.

 

클래스와 객체

  • 클래스: 클래스는 사용자 정의 데이터 형식으로, 데이터와 함수를 하나의 단위로 묶어 객체를 생성하는 틀입니다.
  • 객체: 객체는 클래스의 인스턴스이며, 클래스에 정의된 데이터와 함수를 가지고 있습니다. cin과 cout은 각각 istream과 ostream 클래스의 객체입니다.

입력과 출력

  • cin: cin 객체는 입력 스트림을 나타내며, >> 연산자를 사용하여 입력 스트림에서 데이터를 추출합니다.
  • cout: cout 객체는 출력 스트림을 나타내며, << 연산자를 사용하여 데이터를 출력 스트림에 삽입합니다.
  • 자동 형 변환: cin과 cout은 입력 및 출력 데이터의 형식을 자동으로 변환하여 처리합니다.

 

3장 데이터 처리: C++ 데이터 다루기의 모든 것

이 장에서는 C++에서 데이터를 표현하고 처리하는 다양한 방법을 자세히 살펴봅니다. 변수, 정수형, 부동 소수점형, 연산자, 데이터형 변환 등 C++ 프로그래밍의 핵심 요소들을 예제와 함께 설명합니다.

3.1 간단한 변수: 데이터 저장의 시작

변수는 프로그램에서 데이터를 저장하고 사용하기 위한 이름입니다. C++에서는 변수를 사용하기 전에 반드시 선언해야 하며, 변수 이름은 특정 규칙을 따라야 합니다.

  • 변수 이름 규칙:
    • 영문자, 숫자, 밑줄(_)만 사용 가능
    • 숫자로 시작 불가
    • 대소문자 구분
    • C++ 키워드 사용 불가
    • 특정 예약된 이름 사용 불가
    • 길이 제한 없음 (일부 플랫폼 제외)

예제: 변수 선언 및 사용

int braincount; 
braincount = 5; 
 

3.2 정수형: 다양한 크기와 표현 범위

C++는 다양한 크기와 표현 범위를 가진 여러 정수형을 제공합니다. short, int, long, long long 등의 기본형과 각각의 unsigned 버전을 사용하여 필요에 맞는 정수형을 선택할 수 있습니다.

  • 정수형 크기:
    • short: 최소 16비트
    • int: 최소 short 이상
    • long: 최소 32비트, 최소 int 이상
    • long long: 최소 64비트, 최소 long 이상

예제: 정수형 크기 확인

#include <iostream> 
#include <climits> 

int main() {
    using namespace std; 

    cout << "int는 " << sizeof(int) << " 바이트이다." << endl; 
    cout << "short는 " << sizeof(short) << " 바이트이다." << endl; 
    cout << "long은 " << sizeof(long) << " 바이트이다." << endl; 
    cout << "long long은 " << sizeof(long long) << " 바이트이다." << endl; 

    return 0; 
}
 
  • sizeof 연산자: 데이터형이나 변수의 크기를 바이트 단위로 반환합니다.
  • climits 헤더 파일: 각 정수형의 최댓값과 최솟값을 나타내는 기호 상수를 정의합니다. (INT_MAX, INT_MIN 등)

3.3 const 제한자: 안전하고 의미 있는 상수 정의

const 제한자를 사용하여 변수를 상수로 선언하면 값을 변경할 수 없도록 보호하고, 코드의 가독성을 높일 수 있습니다.

const int MONTHS = 12; // MONTHS는 12를 나타내는 기호 상수
  • 상수 이름 규칙: 일반적으로 상수 이름은 모두 대문자로 작성합니다.
  • 초기화: const 상수는 선언과 동시에 초기화해야 합니다.

3.4 부동 소수점 수: 소수와 큰 숫자 표현

부동 소수점형은 소수부가 있는 수나 매우 큰 수, 매우 작은 수를 표현할 수 있습니다. float, double, long double 세 가지 부동 소수점형이 있으며, 각각 다른 정밀도와 표현 범위를 가집니다.

  • 부동 소수점 수 표기:
    • 소수점 표기법: 3.14159, 0.00023
    • 지수 표기법: 3.45E6 (3.45 x 10^6)

예제: 부동 소수점형 정밀도 비교

#include <iostream> 

int main() {
    using namespace std; 

    cout.setf(ios_base::fixed, ios_base::floatfield); 
    float tub = 10.0 / 3.0; 
    double mint = 10.0 / 3.0; 
    const float million = 1.0e6; 

    cout << "tub = " << tub; 
    cout << ", a million tubs = " << million * tub; 
    cout << ",\nten million tubs = "; 
    cout << 10 * million * tub << endl; 

    cout << "mint = " << mint << " and a million mints = "; 
    cout << million * mint << endl; 

    return 0; 
}
 
  • float vs double: float형은 double형보다 정밀도가 낮습니다. float형은 일반적으로 6~7개의 유효 숫자를 가지며, double형은 약 15개의 유효 숫자를 가집니다.
  • cfloat 헤더 파일: 각 부동 소수점형의 최댓값, 최솟값, 정밀도 등에 대한 정보를 제공합니다.

3.5 C++ 산술 연산자: 컴퓨터에게 계산을 시켜보자

C++는 덧셈, 뺄셈, 곱셈, 나눗셈, 나머지 연산을 위한 산술 연산자를 제공합니다.

  • 산술 연산자:
    • +: 덧셈
    • -: 뺄셈
    • *: 곱셈
    • /: 나눗셈
    • %: 나머지 (정수에 대해서만 사용 가능)

예제: 산술 연산 수행

#include <iostream> 

int main() {
    using namespace std; 

    float hats, heads; 

    cout.setf(ios_base::fixed, ios_base::floatfield); 

    cout << "수를 하나 입력하십시오: ";
    cin >> hats; 

    cout << "다른 수를 입력하십시오: ";
    cin >> heads; 

    cout << "hats = " << hats << "; heads = " << heads << endl; 
    cout << "hats + heads = " << hats + heads << endl; 
    cout << "hats - heads = " << hats - heads << endl; 
    cout << "hats * heads = " << hats * heads << endl; 
    cout << "hats / heads = " << hats / heads << endl; 

    return 0; 
}
  • 연산 순서: C++는 일반적인 수학 연산 순서 규칙(곱셈과 나눗셈을 덧셈과 뺄셈보다 먼저 수행)을 따릅니다. 괄호를 사용하여 연산 순서를 변경할 수 있습니다.
  • 데이터형 변환: 서로 다른 데이터형을 혼합하여 연산할 경우, 작은 크기의 데이터형이 큰 크기의 데이터형으로 자동 변환됩니다. (예: int + double -> double + double)

3.6 데이터형 변환: 데이터형 간의 변신

C++는 다양한 데이터형을 제공하지만, 경우에 따라 데이터형을 변환해야 할 필요가 있습니다. C++는 자동 형 변환과 강제 형 변환 두 가지 방법을 제공합니다.

  • 자동 형 변환: 대입, 수식 계산, 함수 호출 시 컴파일러가 자동으로 데이터형을 변환합니다.
  • 강제 형 변환: (typeName) value 또는 typeName(value) 형식의 데이터형 변환자를 사용하여 특정 데이터형으로 강제 변환합니다.

예제: 데이터형 변환

// typecast.cpp - 강제 데이터형 변환 
#include <iostream> 

int main() {
    using namespace std; 

    int auks, bats, coots; 

    // C++ 구문은 두 값을 double형으로 더한 후에 
    // 그 합을 int형으로 변환하여 대입한다
    auks = 19.99 + 11.99; 

    // 다음 두 구문은 두 값을 int형으로 변환한 후에 더한다
    bats = (int) 19.99 + (int) 11.99;     // 구식 C 스타일

 

3.7 C++11에서의 auto 선언

  • 자동 형 추론: C++11부터 auto를 사용하여 초기화 값을 기반으로 변수형 자동 추론
  • 주의: 자동 형 변환 발생 가능성 고려 필요, 복잡한 형식에 유용

예제:

auto n = 100;     // n은 int
auto x = 1.5;      // x는 double
auto y = 1.3e12L; // y는 long double

std::vector<int> scores;
auto pv = scores.begin(); // pv는 std::vector<int>::iterator

 

4장 복합 데이터형: C++ 데이터 구조 깊이 알아보기

이 장에서는 C++에서 데이터를 효율적으로 구성하고 관리하는 데 사용되는 복합 데이터형에 대해 자세히 살펴봅니다. 배열, 문자열, 구조체, 공용체, 포인터 등 다양한 데이터형과 함께 C++11의 새로운 기능들도 함께 설명합니다.

4.1 배열

  • 배열: 동일한 데이터형의 여러 값을 연속적으로 저장하는 데이터 구조
  • 선언: typeName arrayName[arraySize]; 형식으로 선언, 배열 크기는 상수 또는 컴파일 시간에 결정되는 값이어야 함
  • 접근: 인덱스(0부터 시작)를 사용하여 각 원소에 접근 (예: arrayName[0])
  • 초기화: 선언과 동시에 초기화 가능, 초기화 리스트의 값 개수가 배열 크기보다 작으면 나머지 원소는 0으로 초기화

예제: 배열 선언, 초기화, 사용

 
int yams[3];        // 3개의 int형 원소를 가진 배열 생성
yams[0] = 7;        // 첫 번째 원소에 값 대입
int yamcosts[3] = {200, 300, 50}; // 배열 생성과 동시에 초기화
  • C++11 배열 초기화: = 생략 가능, 빈 중괄호로 0 초기화, 엄격한 데이터형 변환 규칙 적용

4.2 문자열

  • 문자열: 메모리에 연속적으로 저장된 문자들의 배열
  • C 스타일 문자열: char형 배열에 저장, 끝에 널 문자(\0) 추가
  • 문자열 상수: 큰따옴표로 묶어 표현 (예: "Hello, world!")
  • 문자열 입력: cin은 공백이나 탭을 만나면 입력 종료, cin.getline()은 개행 문자까지 읽어 한 행 전체 입력 가능
  • 문자열 길이: strlen() 함수 사용 (널 문자 제외)

예제: 문자열 입력 및 길이 출력

#include <iostream>
#include <cstring> 

int main() {
    using namespace std;

    const int Size = 15;
    char name[Size];

    cout << "이름을 입력하십시오:\n";
    cin >> name;
    cout << "당신의 이름은 " << strlen(name) << "자입니다.\n";

    return 0;
}
  • cin.get(): 한 문자 입력, 개행 문자도 입력 가능
  • cin.getline()과 cin.get() 결합: cin.getline(name, ArSize).get(); 형태로 사용하여 개행 문자 처리

4.3 string 클래스

  • string: C++ 표준 라이브러리의 문자열 클래스, 문자열 처리 기능 제공
  • 선언 및 초기화: string str1;, string str2 = "panther";
  • 입출력: cin, cout과 함께 사용 가능
  • 문자열 길이: str.size() 메서드 사용
  • 대입, 결합, 추가: =, +, += 연산자 사용 가능

예제: string 객체 사용

#include <iostream> 
#include <string> 

int main() {
    using namespace std;

    string str1 = "penguin";
    string str2, str3;

    str2 = str1;             // 대입
    str3 = str1 + str2;      // 결합
    str1 += " paste";        // 추가

    cout << "str1: " << str1 << ", str2: " << str2 << ", str3: " << str3 << endl;

    return 0;
}
 
  • C 스타일 문자열 함수: strcpy(), strcat() 등 C 스타일 문자열 함수도 사용 가능하지만, string 객체가 더 안전하고 편리합니다.

4.4 구조체

  • 구조체: 여러 데이터형의 값을 하나의 단위로 묶어 저장하는 데이터 구조
  • 선언: struct structName { /* 멤버 변수 선언 */ }; 형식으로 선언
  • 접근: . 연산자를 사용하여 멤버 변수에 접근 (예: structVar.memberVar)
  • 초기화: 선언과 동시에 초기화 가능, 중괄호 안에 멤버별 초기값 나열

예제: 구조체 선언, 초기화, 사용

#include <iostream>

struct inflatable {
    char name[20];
    float volume;
    double price;
};

int main() {
    using namespace std;

    inflatable guest = {
        "Glorious Gloria", // name 값
        1.88,               // volume 값
        29.99               // price 값
    };

    cout << "이름: " << guest.name << endl;
    cout << "부피: " << guest.volume << " cubic feet\n";
    cout << "가격: $" << guest.price << endl;

    return 0;
}
  • 구조체 배열: 구조체를 원소로 갖는 배열 생성 가능
  • C++11 구조체 초기화: = 생략 가능, 빈 중괄호로 0 초기화, 엄격한 데이터형 변환 규칙 적용
  • 구조체 대입: = 연산자를 사용하여 한 구조체를 다른 구조체에 대입 가능 (멤버별 복사)

4.5 공용체

  • 공용체: 여러 데이터형 중 하나의 값만 저장 가능, 메모리 공간 절약
  • 선언: union unionName { /* 멤버 변수 선언 */ }; 형식으로 선언
  • 접근: . 연산자를 사용하여 멤버 변수에 접근, 한 번에 하나의 멤버만 사용 가능
  • 크기: 가장 큰 멤버의 크기와 같음

예제: 공용체 선언 및 사용

union one4all {
    int int_val;
    long long_val;
    double double_val;
};

one4all pail;
pail.int_val = 15;       // int형 값 저장
pail.double_val = 1.38;  // double형 값 저장 (int형 값은 사라짐)
  • 익명 공용체: 이름 없는 공용체, 멤버들이 같은 메모리 공간 공유

4.6 열거형

  • 열거형: 관련된 기호 상수들을 정의하는 데 사용
  • 선언: enum enumName { enumerator1, enumerator2, ... }; 형식으로 선언
  • 열거자: 각 상수를 나타내는 이름, 기본적으로 0부터 1씩 증가하는 값 할당
  • 열거형 변수: 열거형 이름을 사용하여 선언, 열거자 값만 대입 가능

예제: 열거형 선언 및 사용

enum spectrum {red, orange, yellow, green, blue, violet, indigo, ultraviolet};
spectrum band;
band = blue;    // 유효한 대입
band = 2000;    // 잘못된 대입 (컴파일 에러 또는 경고 발생)
  • 열거자 값 설정: enum enumName { enumerator1 = value1, enumerator2, ... }; 형식으로 명시적 값 할당 가능
  • 열거형 값 범위: 열거자 값들을 기반으로 자동 결정, 데이터형 변환을 통해 범위 내의 정수값 대입 가능
  •  

4.7 포인터와 메모리 해제

포인터: 주소를 저장하는 특별한 변수

포인터는 값 자체를 저장하는 것이 아니라, 값이 저장된 메모리의 주소를 저장하는 변수입니다. 포인터를 사용하면 프로그램 실행 중에 메모리를 동적으로 할당하고 관리할 수 있습니다.

  • 선언: typeName * pointerName; (예: int * p_updates;)
  • 주소 연산자 (&): 변수 앞에 &를 붙여 변수의 주소를 얻습니다.
  • 간접 참조 연산자 (*): 포인터 앞에 *를 붙여 포인터가 가리키는 메모리 위치에 저장된 값에 접근합니다.

예제: 포인터 선언 및 사용

int updates = 6;        // int형 변수 선언 및 초기화
int * p_updates;        // int형 포인터 선언
p_updates = &updates;  // updates의 주소를 p_updates에 저장

cout << "updates 값: " << updates;
cout << ", *p_updates 값: " << *p_updates << endl; 

cout << "updates 주소: " << &updates;
cout << ", p_updates 값(주소): " << p_updates << endl; 

*p_updates = *p_updates + 1; 
cout << "변경된 updates 값: " << updates << endl; 
 
  • 포인터의 초기화: 포인터는 반드시 유효한 메모리 주소로 초기화해야 합니다. 초기화되지 않은 포인터를 사용하면 프로그램 오류가 발생할 수 있습니다.
  • 포인터와 숫자: 포인터는 메모리 주소를 저장하지만, 정수형 변수와는 다릅니다. 포인터에 직접 정수 값을 대입하려면 명시적인 형 변환이 필요합니다. (예: pt = (int *) 0xB8000000;)

new를 사용한 메모리 할당

new 연산자를 사용하면 프로그램 실행 중에 필요한 만큼의 메모리를 동적으로 할당할 수 있습니다.

  • 할당: typeName * pointerName = new typeName; (예: int * pn = new int;)
  • 배열 할당: typeName * pointerName = new typeName[arraySize]; (예: int * psome = new int[10];)

예제: new를 사용한 메모리 할당 및 사용

#include <iostream> 

int main() {
    using namespace std; 

    int nights = 1001; 
    int * pt = new int;        // int형 데이터 저장 공간 할당
    *pt = 1001;                // 할당된 메모리에 값 저장

    cout << "nights 값 = " << nights << " : 메모리 위치 " << &nights << endl; 
    cout << "int: 값 = " << *pt << " : 메모리 위치 = " << pt << endl; 

    double * pd = new double; // double형 데이터 저장 공간 할당
    *pd = 10000001.0;           // 할당된 메모리에 값 저장

    cout << "double: 값 = " << *pd << " : 메모리 위치 = " << pd << endl; 
    cout << "포인터 pd의 메모리 위치 : " << &pd << endl; 
    cout << "pt 크기 = " << sizeof(pt); 
    cout << " : *pt 크기 = " << sizeof(*pt) << endl; 
    cout << "pd 크기 = " << sizeof pd; 
    cout << " : *pd 크기 = " << sizeof(*pd) << endl; 

    return 0; 
}
 
  • 포인터 변수와 데이터 객체: 포인터 변수는 메모리 주소를 저장하고, * 연산자를 통해 해당 주소에 저장된 데이터 객체에 접근합니다.
  • 데이터형 중요성: 포인터 선언 시 데이터형을 명시해야 컴파일러가 메모리 접근 및 연산을 올바르게 처리할 수 있습니다.
  • 메모리 부족: new 연산자는 메모리 할당에 실패하면 널 포인터(nullptr) 또는 bad_alloc 예외를 반환할 수 있습니다.

delete를 사용한 메모리 해제

new로 할당한 메모리는 사용 후 반드시 delete 연산자를 사용하여 해제해야 합니다. 메모리 해제를 잊으면 메모리 누수가 발생하여 프로그램 성능 저하 또는 오류를 유발할 수 있습니다.

  • 해제: delete pointerName; (예: delete ps;)
  • 배열 해제: delete [] pointerName; (예: delete [] psome;)

주의 사항

  • new로 할당한 메모리만 delete로 해제해야 합니다.
  • 같은 메모리 블록을 두 번 해제하면 안 됩니다.
  • newdelete, new[]delete[]는 반드시 짝을 맞춰 사용해야 합니다.
  • 널 포인터에 delete를 사용하는 것은 안전합니다.

예제: new와 delete 함께 사용

int * ps = new int; 
// ... 메모리 사용 ...
delete ps; 

new를 사용한 동적 배열 생성 및 사용

  • 동적 배열: 프로그램 실행 중에 크기를 결정하여 생성하는 배열
  • 생성: typeName * pointerName = new typeName[arraySize];
  • 접근: 포인터를 배열 이름처럼 사용하여 인덱스로 접근 (예: pointerName[0])
  • 해제: delete [] pointerName;

예제: 동적 배열 생성 및 사용

#include <iostream> 

int main() {
    using namespace std; 

    double * p3 = new double [3]; // 3개의 double형 원소를 가진 동적 배열 생성
    p3[0] = 0.2; 
    p3[1] = 0.5; 
    p3[2] = 0.8; 

    cout << "p3[1]은 " << p3[1] << "입니다.\n"; 
    p3 = p3 + 1; // 포인터를 다음 원소로 이동

    cout << "이제는 p3[0]이 " << p3[0] << "이고, "; 
    cout << "p3[1]이 " << p3[1] << "입니다.\n"; 
    p3 = p3 - 1; // 포인터를 원래 위치로 이동

    delete [] p3; // 배열 메모리 해제

    return 0; 
}
  • 포인터 연산: 포인터에 정수를 더하거나 빼면 포인터가 가리키는 메모리 주소가 이동합니다. 이동하는 크기는 포인터가 가리키는 데이터형의 크기에 따라 결정됩니다.
  • 배열 표기와 포인터 표기: arrayName[i]*(arrayName + i)와 동일하며, 포인터에도 동일하게 적용됩니다.
  • 배열 이름과 포인터의 차이: 배열 이름은 상수이지만, 포인터는 변수입니다. sizeof 연산자를 사용하면 배열 이름은 배열 전체 크기를, 포인터는 포인터 자체의 크기를 반환합니다.

포인터와 문자열

  • 문자열 표현: C++에서 문자열은 char형 배열 또는 string 객체로 표현할 수 있습니다.
  • char형 배열: 널 문자(`\0)로 끝나는 연속된 문자들입니다. 문자열은 큰따옴표로 묶어서 문자열 상수로 나타낼 수 있으며, 이때 끝내기 널 문자는 내부적으로 처리됩니다. 문자열은char형의 배열에 저장할 수 있으며,char`형을 가리키는 포인터를 사용하여 나타낼 수도 있습니다. 이때 포인터는 문자열의 첫 번째 문자의 주소를 가리킵니다.
  • strlen() 함수: 널 문자를 제외한 문자열의 길이를 반환합니다.
  • strcpy() 함수: 한 위치에서 다른 위치로 문자열을 복사합니다.
  • strncpy() 함수: 문자열을 특정 길이만큼 복사하고, 널 문자를 추가하여 안전하게 복사합니다.

예제: 문자열과 포인터

#include <iostream>
#include <cstring> 

int main() {
    using namespace std;

    char animal[20] = "bear";     // char형 배열로 문자열 초기화
    const char * bird = "wren";   // char형 포인터로 문자열 초기화
    char * ps;                    // 초기화되지 않은 포인터

    cout << animal << " and ";  // 배열 이름으로 문자열 출력
    cout << bird << "\n";       // 포인터로 문자열 출력
    // cout << ps << "\n";       // 초기화되지 않은 포인터 사용 -> 오류 발생 가능

    cout << "동물의 종류를 입력하십시오: ";
    cin >> animal;                 // 배열에 새로운 문자열 입력

    // cin >> ps;                 // 초기화되지 않은 포인터에 입력 -> 오류 발생 가능

    ps = animal;                    // 포인터에 배열의 주소 대입
    cout << ps << "s!\n";         // 포인터로 문자열 출력

    cout << "strcpy() 사용 전:\n";
    cout << (int *) animal << ": " << animal << endl; 
    cout << (int *) ps << ": " << ps << endl; 

    ps = new char[strlen(animal) + 1]; // 새로운 메모리 할당
    strcpy(ps, animal);                 // 문자열 복사

    cout << "strcpy() 사용 후:\n";
    cout << (int *) animal << ": " << animal << endl; 
    cout << (int *) ps << ": " << ps << endl; 

    delete [] ps; 
    return 0;
}
  • 문자열 출력: cout은 char형 배열 이름이나 포인터를 전달받으면, 널 문자를 만날 때까지 문자열을 출력합니다.
  • 문자열 입력: cin은 공백이나 탭을 만나면 입력을 멈추고, 입력받은 문자열을 널 문자로 끝나는 char형 배열에 저장합니다.
  • 포인터와 문자열: char형 포인터는 문자열의 시작 주소를 저장하여 문자열을 나타낼 수 있습니다.
  • 문자열 복사: strcpy() 함수를 사용하여 문자열을 복사할 때는 목적지 배열의 크기가 충분한지 확인해야 합니다. 그렇지 않으면 메모리 오버플로우가 발생할 수 있습니다.
  • 동적 메모리 할당: new 연산자를 사용하여 문자열을 저장할 메모리를 동적으로 할당하고, delete[] 연산자를 사용하여 해제해야 합니다.

string 클래스는 C 스타일 문자열보다 사용하기 편리하고 안전합니다.

  • 자동으로 메모리 관리를 처리하므로, 문자열 길이에 대한 걱정 없이 사용할 수 있습니다.
  • +, += 등의 연산자를 사용하여 문자열을 쉽게 결합하고 추가할 수 있습니다.
  • size() 메서드를 사용하여 문자열 길이를 얻을 수 있습니다.

따라서, C++에서는 가능하면 string 객체를 사용하는 것이 좋습니다.

4.8 포인터, 배열, 포인터 연산

  • 포인터 연산: 포인터에 정수를 더하거나 빼면 포인터가 가리키는 메모리 주소가 해당 데이터형의 크기만큼 이동합니다.
  • 배열 표기와 포인터 표기: arrayName[i]는 *(arrayName + i)와 동일하며, 포인터에도 동일하게 적용됩니다.
  • 배열 이름과 포인터의 차이: 배열 이름은 상수이지만, 포인터는 변수입니다. sizeof 연산자를 사용하면 배열 이름은 배열 전체 크기를, 포인터는 포인터 자체의 크기를 반환합니다.
  • 동적 배열: new 연산자를 사용하여 프로그램 실행 중에 배열의 크기를 결정하고 메모리를 할당할 수 있습니다.
  • 정적 배열: 컴파일 시간에 배열의 크기가 결정되는 배열입니다.

예제: 포인터 연산과 배열, 포인터 비교

#include <iostream>

int main() {
    using namespace std;

    double wages[3] = {10000.0, 20000.0, 30000.0};
    short stacks[3] = {3, 2, 1};

    // 배열의 주소를 알아내는 두 가지 방법
    double * pw = wages;         // 배열 이름 사용
    short * ps = &stacks[0];     // 배열 원소에 주소 연산자 사용

    cout << "pw = " << pw << ", *pw = " << *pw << endl;
    pw = pw + 1;
    cout << "pw 포인터에 1을 더함: \n";
    cout << "pw = " << pw << ", *pw = " << *pw << "\n\n";

    cout << "ps = " << ps << ", *ps = " << *ps << endl;
    ps = ps + 1;
    cout << "ps 포인터에 1을더함: \n";
    cout << "ps = " << ps << ", *ps = " << *ps << "\n\n";

    cout << "배열 표기로 두 원소에 접근\n";
    cout << "stacks[0] = " << stacks[0]
         << ", stacks[1] = " << stacks[1] << endl;

    cout << "포인터 표기로 두 원소에 접근\n";
    cout << "*stacks = " << *stacks
         << ", *(stacks + 1) = " << *(stacks + 1) << endl;

    cout << sizeof(wages) << " = wages 배열의 크기\n";
    cout << sizeof(pw) << " = pw 포인터의 크기\n";

    return 0;
}
 
 
  • 포인터 덧셈: 포인터에 1을 더하면 다음 원소의 주소를 가리키게 됩니다. 데이터형의 크기만큼 주소 값이 증가합니다.
  • 배열 표기와 포인터 표기: stacks[1]은 *(stacks + 1)과 동일하게 작동합니다.
  • sizeof 연산자: 배열 이름에 sizeof를 사용하면 배열 전체 크기를, 포인터에 사용하면 포인터 자체의 크기를 반환합니다.

4.9 변수형의 조합

C++는 배열, 구조체, 포인터와 같은 다양한 데이터형을 제공하며, 이들을 조합하여 복잡한 데이터를 효율적으로 표현하고 관리할 수 있습니다.

1. 구조체 변수와 멤버 접근

  • antarctica_years_end 구조체는 year라는 멤버 변수를 가집니다.
  • antarctica_years_end s01, s02, s03;와 같이 여러 개의 구조체 변수를 선언할 수 있습니다.
  • 멤버 연산자(.)를 사용하여 구조체 변수의 멤버에 접근하고 값을 할당할 수 있습니다.
    • 예: s01.year = 1998;

2. 구조체 포인터와 간접 멤버 접근 연산자

  • 구조체 변수의 주소를 저장하는 포인터를 선언할 수 있습니다.
    • 예: antarctica_years_end * pa = &s02; (pas02 구조체 변수의 주소를 저장하는 포인터)
  • 간접 멤버 접근 연산자(->)를 사용하여 포인터를 통해 구조체 멤버에 접근하고 값을 할당할 수 있습니다.
    • 예: pa->year = 1999;

3. 구조체 배열

  • 구조체를 원소로 갖는 배열을 생성할 수 있습니다.
    • 예: antarctica_years_end trio[3]; (trio는 3개의 antarctica_years_end 구조체를 원소로 갖는 배열)
  • 배열 표기법과 멤버 연산자를 함께 사용하여 배열 원소(구조체)의 멤버에 접근할 수 있습니다.
    • 예: trio[0].year = 2003;
  • 배열 이름은 포인터처럼 사용할 수 있으므로, 간접 멤버 접근 연산자를 사용할 수도 있습니다.
    • 예: (trio + 1)->year = 2004; (이는 trio[1].year = 2004;와 동일)

4. 포인터 배열

  • 포인터를 원소로 갖는 배열을 생성할 수 있습니다.
    • 예: const antarctica_years_end * arp[3] = {&s01, &s02, &s03}; (arpantarctica_years_end 구조체를 가리키는 3개의 포인터를 원소로 갖는 배열)
  • 배열 표기법과 간접 멤버 접근 연산자를 함께 사용하여 포인터 배열의 원소(포인터)를 통해 구조체 멤버에 접근할 수 있습니다.
    • 예: arp[1]->year

5. 배열 포인터

  • 배열을 가리키는 포인터를 생성할 수 있습니다.
    • 예: const antarctica_years_end ** ppa = arp; (ppaantarctica_years_end 구조체 포인터의 배열 arp를 가리키는 포인터)
  • C++11의 auto 키워드를 사용하여 컴파일러가 자동으로 포인터의 형식을 추론하도록 할 수 있습니다.
    • 예: auto ppb = arp;
  • 포인터의 포인터를 사용하여 간접적으로 구조체 멤버에 접근할 수 있습니다.
    • 예: (*ppa)->year, (*(ppb + 1))->year

6. 예제 코드

#include <iostream>

struct antarctica_years_end {
    int year;
    /* some really interesting data, etc. */
};

int main() {
    antarctica_years_end s01, s02, s03;
    s01.year = 1998;

    antarctica_years_end * pa = &s02;
    pa->year = 1999;

    antarctica_years_end trio[3]; 
    trio[0].year = 2003;

    std::cout << trio->year << std::endl; 

    const antarctica_years_end * arp[3] = {&s01, &s02, &s03};
    std::cout << arp[1]->year << std::endl;

    const antarctica_years_end ** ppa = arp;
    auto ppb = arp; 

    std::cout << (*ppa)->year << std::endl;
    std::cout << (*(ppb + 1))->year << std::endl;

    return 0;
}
 

실행 결과:

2003
1999
1998
1999

7. 결론

C++는 배열, 구조체, 포인터를 다양하게 조합하여 복잡한 데이터 구조를 효율적으로 표현하고 관리할 수 있는 강력한 기능을 제공합니다. 이러한 조합을 통해 C++는 다양한 프로그래밍 요구 사항을 충족시킬 수 있는 유연성을 제공하며, 개발자는 상황에 맞는 최적의 데이터 구조를 선택하여 사용할 수 있습니다.

  • 주의: 포인터를 사용할 때는 메모리 접근 오류와 메모리 누수에 주의해야 합니다. 항상 포인터가 유효한 메모리 주소를 가리키는지 확인하고, new로 할당한 메모리는 delete를 사용하여 해제해야 합니다.

4.10 배열의 대안: 더욱 유연하고 안전한 데이터 관리

C++에서 배열은 강력한 도구이지만, 크기가 고정되어 있어 유연성이 떨어지고 잘못된 인덱스 접근으로 인한 오류 가능성이 있습니다. 이러한 문제를 해결하기 위해 C++는 vector 템플릿 클래스와 C++11부터는 array 템플릿 클래스를 제공합니다.

 

1. Vector 템플릿 클래스

  • 특징:
    • 동적 크기 조절: 프로그램 실행 중에 크기 변경 가능
    • 삽입/삭제: 데이터 삽입 및 삭제 기능 제공
    • 범위 검사: at() 메서드를 사용하여 범위 밖 접근 방지
    • 편리한 기능: 다양한 멤버 함수 제공 (정렬, 검색 등)
  • 선언 및 초기화:
    • vector<typeName> vec; (빈 벡터 생성)
    • vector<typeName> vec(n_elem); (n_elem개의 원소로 초기화, n_elem은 정수형 상수 또는 변수)
    • vector<typeName> vec {val1, val2, ...}; (초기화 리스트 사용, C++11)
  • 예제:
#include <iostream>
#include <vector>

int main() {
    using namespace std;

    vector<int> vi;  // 빈 int 벡터 생성
    vi.push_back(1); // 값 추가
    vi.push_back(2);

    cout << "vi의 첫 번째 원소: " << vi[0] << endl;
    cout << "vi의 두 번째 원소: " << vi.at(1) << endl; // 범위 검사 기능 사용

    return 0;
}
 

2. Array 템플릿 클래스 (C++11)

  • 특징:
    • 고정 크기: 컴파일 시간에 크기 결정, 효율적인 메모리 사용
    • 범위 검사: at() 메서드를 사용하여 범위 밖 접근 방지
    • 배열과 유사한 사용법: 배열 표기법([]) 사용 가능
    • STL 알고리즘과 호환: begin(), end() 등의 메서드 제공
  • 선언 및 초기화:
    • array<typeName, n_elem> arr; (n_elem개의 원소를 가진 배열 생성, n_elem은 정수형 상수)
    • array<typeName, n_elem> arr {val1, val2, ...}; (초기화 리스트 사용)
  • 예제:
#include <iostream>
#include <array>

int main() {
    using namespace std;

    array<int, 5> arr1;  // 5개의 int 원소를 가진 배열 생성
    array<int, 5> arr2 = {1, 2, 3, 4, 5}; // 초기화 리스트 사용

    cout << "arr2의 세 번째 원소: " << arr2[2] << endl;
    cout << "arr2의 네 번째 원소: " << arr2.at(3) << endl; // 범위 검사 기능 사용

    return 0;
}

3. 배열, Vector, Array 비교

특징배열 (내장)VectorArray (C++11)
크기 고정 동적 고정
메모리 할당 스택 스택
범위 검사 없음 at() 메서드 at() 메서드
초기화 가능 가능 가능
대입 불가능 가능 가능
STL 호환 제한적 완벽 호환 완벽 호환

예제: 배열, Vector, Array 비교

#include <iostream>
#include <vector>
#include <array>

int main() {
    using namespace std;

    // C 스타일 배열
    double a1[4] = {1.2, 2.4, 3.6, 4.8};

    // Vector
    vector<double> a2(4); 
    a2[0] = 1.0/3.0;
    a2[1] = 1.0/5.0;
    a2[2] = 1.0/7.0;
    a2[3] = 1.0/9.0;

    // Array (C++11)
    array<double, 4> a3 = {3.14, 2.72, 1.62, 1.41};
    array<double, 4> a4;
    a4 = a3; 

    // 배열 표기법 사용
    cout << "a1[2]: " << a1[2] << " at " << &a1[2] << endl;
    cout << "a2[2]: " << a2[2] << " at " << &a2[2] << endl;
    cout << "a3[2]: " << a3[2] << " at " << &a3[2] << endl;
    cout << "a4[2]: " << a4[2] << " at " << &a4[2] << endl;

    // 잘못된 접근 (런타임 에러 발생 가능)
    a1[-2] = 20.2;
    // a2.at(-2) = 20.2; // 컴파일 에러 발생
    // a3.at(200) = 1.4; // 컴파일 에러 발생

    cout << "a1[-2]: " << a1[-2] << " at " << &a1[-2] << endl;
    cout << "a3[2]: " << a3[2] << " at " << &a3[2] << endl;
    cout << "a4[2]: " << a4[2] << " at " << &a4[2] << endl;

    return 0;
}
 
 
실행 결과:
a1[2]: 3.6 at 0x7ffeefbff570
a2[2]: 0.142857 at 0x16c0e0320
a3[2]: 1.62 at 0x7ffeefbff560
a4[2]: 1.62 at 0x7ffeefbff550
a1[-2]: 20.2 at 0x7ffeefbff560
a3[2]: 1.62 at 0x7ffeefbff560
a4[2]: 1.62 at 0x7ffeefbff550

 

결론:

  • vector: 동적인 크기 조절이 필요할 때 유용
  • array: 고정 크기 배열이 필요하고 STL 기능을 활용하고 싶을 때 유용
  • 배열: 간단한 경우나 C 스타일 코드와의 호환성이 필요할 때 사용 가능, 범위 밖 접근 오류에 주의해야 함

4. 추가 설명

  • vector의 장점:
    • 실행 중 크기 변경 가능하여 메모리 효율성 높임
    • 다양한 멤버 함수 제공으로 편리한 사용 가능
    • STL 알고리즘과의 완벽한 호환성 제공
  • array의 장점:
    • 고정 크기로 인해 효율적인 메모리 사용
    • at() 메서드로 범위 밖 접근 방지하여 안전성 향상
    • STL 알고리즘과의 완벽한 호환성 제공
  • 내장 배열:
    • 간단한 경우나 C 스타일 코드와의 호환성이 필요할 때 사용
    • 범위 밖 접근 시 런타임 에러 발생 가능성 높음, 주의 필요

 

5장 루프와 관계 표현식: C++ 프로그램 흐름 제어하기

 

이 장에서는 C++ 프로그램의 실행 흐름을 제어하는 데 사용되는 핵심 도구인 루프와 관계 표현식에 대해 자세히 알아봅니다. for, while, do while 루프를 통해 반복 작업을 효율적으로 처리하고, 관계 연산자를 사용하여 조건에 따라 프로그램의 실행 흐름을 분기하는 방법을 익힙니다. 또한, 텍스트 입력 처리, 중첩 루프, 2차원 배열 등 실제 프로그래밍에서 자주 사용되는 기법들을 예제와 함께 설명합니다.

5.1 for 루프: 반복 작업의 정석

for 루프는 초기화, 조건 검사, 갱신 세 가지 요소를 사용하여 특정 횟수만큼 코드 블록을 반복 실행하는 데 사용됩니다.

C++

 

for (초기화; 조건 검사; 갱신) {
    // 루프 몸체 (반복 실행될 코드)
}
 
  • 초기화: 루프 시작 전에 한 번 실행되며, 주로 루프 카운터 변수를 초기화하는 데 사용됩니다.
  • 조건 검사: 매 루프 반복 시작 시 조건식을 평가하여 루프를 계속 실행할지 결정합니다. 조건식이 참이면 루프 몸체를 실행하고, 거짓이면 루프를 종료합니다.
  • 갱신: 루프 몸체 실행 후 매번 실행되며, 주로 루프 카운터 변수를 증가시키거나 감소시키는 데 사용됩니다.
  • 루프 몸체: 조건 검사 결과가 참일 때 반복 실행되는 코드 블록입니다.

예제: for 루프를 사용하여 "C++가 루프를 사용합니다." 5번 출력하기

#include <iostream>

int main() {
    using namespace std;

    for (int i = 0; i < 5; i++) { 
        cout << "C++가 루프를 사용합니다. \n";
    }
    cout << "루프를 언제 끝내야 하는지 C++는 알고 있습니다.\n";

    return 0;
}
 

실행 결과:

C++가 루프를 사용합니다. 
C++가 루프를 사용합니다. 
C++가 루프를 사용합니다. 
C++가 루프를 사용합니다. 
C++가 루프를 사용합니다. 
루프를 언제 끝내야 하는지 C++는 알고 있습니다.
  • int i = 0;: 루프 카운터 변수 i를 0으로 초기화합니다.
  • i < 5;: i가 5보다 작을 때까지 루프를 반복합니다.
  • i++: 매 루프 반복 후 i를 1씩 증가시킵니다.
  • cout << "C++가 루프를 사용합니다. \n";: 루프 몸체이며, "C++가 루프를 사용합니다."를 출력하고 줄을 바꿉니다.

5.2 while 루프: 조건에 따라 유연하게 반복

while 루프는 조건 검사 표현식이 참인 동안 루프 몸체를 반복 실행합니다. for 루프와 달리 초기화 및 갱신 부분이 없으므로, 반복 횟수가 명확하지 않거나 조건에 따라 유연하게 루프를 제어해야 할 때 유용합니다.

while (조건 검사) {
    // 루프 몸체 (반복 실행될 코드)
}
  • 조건 검사: 루프 시작 시 조건식을 평가하여 루프를 계속 실행할지 결정합니다. 조건식이 참이면 루프 몸체를 실행하고, 거짓이면 루프를 종료합니다.
  • 루프 몸체: 조건 검사 결과가 참일 때 반복 실행되는 코드 블록입니다. 루프 몸체 안에서 조건 검사에 영향을 주는 변수를 변경해야 합니다.

예제: while 루프를 사용하여 문자열의 각 문자와 ASCII 코드 출력하기

#include <iostream>

const int ArSize = 20;

int main() {
    using namespace std;

    char name[ArSize];

    cout << "영문 이름을 입력하십시오: ";
    cin >> name;

    cout << "귀하의 영문 이름을 한 줄에 한 자씩\n";
    cout << "ASCII 코드와 함께 표시하면 이렇습니다. \n";

    int i = 0; 
    while (name[i] != '\0') { 
        cout << name[i] << " : " << int(name[i]) << endl;
        i++; 
    }

    return 0;
}
  • while (name[i] != '\0'): 현재 문자가 널 문자('\0')가 아닐 때까지 루프를 반복합니다.
  • 루프 몸체에서는 현재 문자와 해당 ASCII 코드를 출력하고, i를 증가시켜 다음 문자로 이동합니다.

5.3 do while 루프: 최소 한 번은 실행되는 루프

do while 루프는 탈출 조건 루프로, 루프 몸체를 먼저 실행하고 나서 조건을 검사합니다. 조건이 거짓이 될 때까지 루프를 반복하며, 최소한 한 번은 루프 몸체가 실행됩니다.

do {
    // 루프 몸체 (반복 실행될 코드)
} while (조건 검사); 
  • 루프 몸체: 먼저 실행되는 코드 블록입니다.
  • 조건 검사: 루프 몸체 실행 후 조건식을 평가하여 루프를 계속 실행할지 결정합니다. 조건식이 참이면 루프 몸체를 다시 실행하고, 거짓이면 루프를 종료합니다.

예제: do while 루프를 사용하여 숫자 맞추기 게임

#include <iostream>

int main() {
    using namespace std;

    int n;

    cout << "1부터 10까지의 수 중에서 ";
    cout << "내가 좋아하는 수를 한 번 맞추어 보십시오.\n";

    do {
        cin >> n;
    } while (n != 7);

    cout << "맞았습니다. 내가 좋아하는 수는 럭키 세븐 7입니다.\n";

    return 0;
}
  • do 블록 안의 코드는 무조건 한 번 실행됩니다.
  • 사용자가 입력한 숫자가 7이 아니면, while 조건이 참이 되어 루프 몸체가 다시 실행됩니다.
  • 사용자가 7을 입력하면, while 조건이 거짓이 되어 루프를 종료합니다.

do while 루프는 최소한 한 번은 루프 몸체를 실행해야 하는 경우나, 사용자 입력을 받아 조건을 검사해야 하는 경우에 유용합니다.

5.4 쉼표 연산자

쉼표 연산자는 여러 개의 표현식을 하나의 표현식으로 결합하는 데 사용됩니다. 쉼표 연산자는 왼쪽 표현식부터 차례대로 평가하고, 마지막 표현식의 값을 전체 쉼표 표현식의 값으로 반환합니다.

 

예시:

i = 20, j = 2 * i;  // i를 20으로 설정한 후, j를 40으로 설정
cats = 17, 240;    // cats에 17을 대입하고, 240은 아무것도 수행하지 않음
cats = (17, 240);  // cats에 240을 대입
  • for 루프에서의 활용: 쉼표 연산자를 사용하여 for 루프의 초기화 부분이나 갱신 부분에 여러 개의 표현식을 넣을 수 있습니다.

예제: for 루프에서 쉼표 연산자 사용

for (j = 0, i = word.size() - 1; j < i; --i, ++j) {
    // ...
}
 
  • 초기화 부분: j = 0, i = word.size() - 1 (j를 0으로, i를 문자열의 마지막 인덱스로 초기화)
  • 갱신 부분: --i, ++j (i를 감소시키고, j를 증가)

5.6 루프와 텍스트 입력

C++에서는 cin 객체를 사용하여 키보드 입력을 받을 수 있습니다. cin은 빈칸, 탭, 개행 문자를 구분자로 인식하여 입력을 처리합니다.

  • cin >> variable;: 빈칸, 탭, 개행 문자를 만날 때까지 입력을 읽고 변수에 저장합니다.
  • cin.get(char);: 한 문자를 읽고 변수에 저장합니다. 빈칸, 탭, 개행 문자도 읽습니다.
  • cin.get();: 한 문자를 읽지만, 변수에 저장하지 않고 버립니다. 주로 입력 버퍼를 비우거나 개행 문자를 처리하는 데 사용됩니다.
  • cin.getline(charArray, size);: 최대 size-1개의 문자를 읽어 charArray에 저장하고, 개행 문자를 만나면 입력을 멈춥니다.
  • 파일 끝(EOF): 파일 입력의 끝을 나타내는 특수한 값입니다. cin.eof() 또는 cin.fail() 메서드를 사용하여 EOF를 감지할 수 있습니다.

주의:

  • cin >> 연산자는 빈칸, 탭, 개행 문자를 구분자로 인식하므로, 빈칸을 포함한 문자열을 입력받으려면 cin.getline() 함수를 사용해야 합니다.
  • cin.getline() 함수는 입력 버퍼에 남아있는 개행 문자를 처리하기 위해 cin.get() 함수와 함께 사용하는 것이 좋습니다.
  • 파일 입력 시에는 cin.eof() 또는 cin.fail() 메서드를 사용하여 파일의 끝을 감지하고 루프를 종료해야 합니다.

5.7 중첩 루프와 2차원 배열

중첩 루프는 루프 안에 다른 루프가 포함된 형태입니다. 중첩 루프를 사용하면 2차원 배열과 같이 행과 열로 구성된 데이터를 효율적으로 처리할 수 있습니다.

  • 2차원 배열: 행과 열로 구성된 표 형태의 데이터 구조입니다. C++에서는 배열의 배열 형태로 2차원 배열을 구현합니다.
  • 선언: typeName arrayName[rowSize][colSize]; (예: int maxtemps[4][5];)
  • 접근: arrayName[rowIndex][colIndex] 형식으로 접근합니다.
  • 초기화: 중첩된 중괄호를 사용하여 초기화할 수 있습니다.

예제: 중첩 루프와 2차원 배열

#include <iostream>

const int Cities = 5;
const int Years = 4;

int main() {
    using namespace std;

    const char * cities[Cities] = { 
        "Gwangju", "Seoul", "Daejeon", "Busan", "Jeju" 
    };

    int maxtemps[Years][Cities] = {
        {35, 32, 33, 36, 35}, 
        {33, 32, 34, 35, 31}, 
        {33, 34, 32, 35, 34}, 
        {36, 35, 34, 37, 35} 
    };

    cout << "2009년부터 2012년까지의 연중 최고 온도\n\n";

    for (int city = 0; city < Cities; ++city) {
        cout << cities[city] << ":\t"; 
        for (int year = 0; year < Years; ++year) {
            cout << maxtemps[year][city] << "\t"; 
        }
        cout << endl;
    }

    return 0;
}
  • 중첩 루프: 외부 루프는 도시를 순회하고, 내부 루프는 각 도시의 연도별 최고 온도를 출력합니다.
  • 2차원 배열 접근: maxtemps[year][city] 형식으로 배열 원소에 접근합니다.

중첩 루프를 사용하면 2차원 배열과 같이 행과 열로 구성된 데이터를 효율적으로 처리할 수 있습니다.

추가 내용:

  • typedef: 기존 데이터형에 새로운 이름을 부여하여 코드의 가독성을 높일 수 있습니다. (예: typedef char byte;)
  • C++11의 auto 선언: 초기화 값을 기반으로 변수의 데이터형을 자동으로 추론하여 선언할 수 있습니다. 복잡한 데이터형을 다룰 때 유용합니다.

6장. 분기 구문과 논리 연산자: C++ 프로그램의 흐름 제어

이 장에서는 C++ 프로그램의 실행 흐름을 제어하는 데 사용되는 핵심 도구인 분기 구문논리 연산자에 대해 자세히 알아봅니다. if, if else, switch 구문을 통해 조건에 따라 프로그램의 실행 흐름을 분기하고, 논리 연산자를 사용하여 복잡한 조건을 표현하는 방법을 익힙니다. 또한, 문자 처리 함수, 조건 연산자, breakcontinue 구문, 파일 입출력 등 다양한 주제를 다룹니다.

1. if 구문: 조건에 따라 실행 여부 결정

if 구문은 특정 조건이 참일 때만 코드 블록을 실행하는 데 사용됩니다.

if (조건식) {
    // 조건식이 참일 때 실행될 코드
}
  • 조건식: 참 또는 거짓으로 평가되는 표현식입니다.
  • 코드 블록: 조건식이 참일 때 실행되는 하나 이상의 문장입니다. 중괄호 {}로 묶어줍니다.

예제: 빈칸과 문자 개수 세기

#include <iostream>

int main() {
    using namespace std;
    char ch;
    int spaces = 0;
    int total = 0;

    cin.get(ch);
    while (ch != '.') { 
        if (ch == ' ')  // ch가 빈칸이면
            ++spaces;  // spaces 변수 1 증가
        ++total; 
        cin.get(ch);
    }
    cout << "총 문자 수: " << total << ", 빈칸 수: " << spaces << endl;
    return 0;
}
  • if (ch == ' '): ch가 빈칸일 때만 ++spaces;를 실행하여 빈칸 개수를 셉니다.
  • ++total;if 구문과 관계없이 항상 실행되어 총 문자 수를 셉니다.

2. if else 구문: 두 가지 선택지 중 하나 실행

if else 구문은 조건식에 따라 두 가지 코드 블록 중 하나를 선택하여 실행합니다.

if (조건식) {
    // 조건식이 참일 때 실행될 코드
} else {
    // 조건식이 거짓일 때 실행될 코드
}
 

예제: 문자 암호화

#include <iostream>

int main() {
    char ch;

    std::cout << "타이핑하시면, 암호화하여 반복 출력합니다. \n";
    std::cin.get(ch);

    while (ch != '.') {
        if (ch == '\n')    // 개행 문자일 때
            std::cout << ch; 
        else
            std::cout << ++ch; // 그 외의 문자일 때 암호화하여 출력
        std::cin.get(ch);
    }
    std::cout << "\n암호 해독을 해보세요!\n";

    return 0;
}
  • if (ch == '\n'): ch가 개행 문자면 그대로 출력하고, 아니면 ++ch를 통해 암호화하여 출력합니다.

3. if else if else 구문: 여러 선택지 중 하나 실행

if else if else 구문은 여러 개의 조건을 순차적으로 검사하여 해당하는 코드 블록을 실행합니다.

if (조건식1) {
    // 조건식1이 참일 때 실행될 코드
} else if (조건식2) {
    // 조건식1은 거짓이고 조건식2가 참일 때 실행될 코드
} else {
    // 모든 조건식이 거짓일 때 실행될 코드
}
 

예제: 나이에 따른 메시지 출력

#include <iostream>

const int Fave = 27;

int main() {
    using namespace std;
    int n;

    cout << "1부터 100까지 범위에서 하나의 수를 알아맞히는 게임입니다. \n";
    cout << "내가 좋아하는 수는 무엇일까요? ";

    do {
        cin >> n;
        if (n < Fave)
            cout << "그것보다 큽니다. 무엇일까요? ";
        else if (n > Fave)
            cout << "그것보다 작습니다. 무엇일까요? ";
        else
            cout << "맞았습니다. 정답은 " << Fave << "입니다.\n";
    } while (n != Fave);

    return 0;
}
  • if, else if, else를 사용하여 사용자 입력 값에 따라 다른 메시지를 출력합니다.

4. 논리 표현식: 복잡한 조건 표현하기

논리 연산자를 사용하여 여러 조건을 결합하여 복잡한 조건식을 만들 수 있습니다.

  • 논리 AND (&&): 두 조건이 모두 참일 때만 참을 반환합니다.
  • 논리 OR (||): 두 조건 중 하나라도 참이면 참을 반환합니다.
  • 논리 NOT (!): 조건의 결과를 반대로 바꿉니다.

예제: 논리 연산자 사용

#include <iostream>

int main() {
    using namespace std;
    int age;
    double income;

    cout << "나이와 소득을 입력하십시오: ";
    cin >> age >> income;

    if ((age > 18 && age < 65) && income > 35000) {
        cout << "대출 가능합니다.\n";
    } else {
        cout << "대출 불가능합니다.\n";
    }

    return 0;
}
  • (age > 18 && age < 65) && income > 35000: 나이가 19세 이상 64세 이하이고 소득이 35000 초과인 경우에만 true를 반환합니다.

5. cctype 라이브러리: 문자 처리 함수 활용

cctype 라이브러리는 문자 관련 함수들을 제공하여 문자의 종류를 판별하거나 변환하는 데 유용합니다.

  • isalpha(ch): ch가 알파벳 문자이면 true를 반환합니다.
  • isdigit(ch): ch가 숫자 문자이면 true를 반환합니다.
  • isspace(ch): ch가 공백 문자(빈칸, 탭, 개행 문자 등)이면 true를 반환합니다.
  • ispunct(ch): ch가 구두점 문자이면 true를 반환합니다.
  • tolower(ch): ch가 대문자이면 소문자로 변환하여 반환하고, 그렇지 않으면 ch를 그대로 반환합니다.
  • toupper(ch): ch가 소문자이면 대문자로 변환하여 반환하고, 그렇지 않으면 ch를 그대로 반환합니다.

예제: 문자 종류 분석

#include <iostream>
#include <cctype>

int main() {
    using namespace std;
    cout << "분석할 텍스트를 입력하십시오. ";
    cout << "입력의 끝을 @으로 표시하십시오.\n";

    char ch;
    int whitespace = 0;
    int digits = 0;
    int chars = 0;
    int punct = 0;
    int others = 0;

    cin.get(ch);
    while (ch != '@') {
        if (isalpha(ch))     chars++;
        else if (isspace(ch)) whitespace++;
        else if (isdigit(ch)) digits++;
        else if (ispunct(ch)) punct++;
        else others++;
        cin.get(ch);
    }

    cout << "알파벳 문자 " << chars << ", "
         << "화이트스페이스 " << whitespace << ", "
         << "숫자 " << digits << ", "
         << "구두점 " << punct << ", "
         << "기타 " << others << endl;

    return 0;
}
  • cctype 헤더 파일을 포함하여 문자 처리 함수를 사용합니다.
  • isalpha(), isspace(), isdigit(), ispunct() 함수를 사용하여 문자의 종류를 판별하고 해당 카운터 변수를 증가시킵니다.
  • others 변수는 위의 네 가지 범주에 속하지 않는 문자들을 세는 데 사용됩니다.

6. 조건 연산자: 간결한 조건 처리

조건 연산자 ?:는 간단한 조건에 따라 두 개의 표현식 중 하나를 선택하여 값을 반환합니다.

조건식 ? 표현식1 : 표현식2
  • 조건식: 참 또는 거짓으로 평가되는 표현식입니다.
  • 표현식1: 조건식이 참일 때 평가되는 표현식입니다.
  • 표현식2: 조건식이 거짓일 때 평가되는 표현식입니다.

예제: 조건 연산자 사용

#include <iostream>

int main() {
    using namespace std;
    int x, y, z;

    cout << "두 수를 입력하십시오: ";
    cin >> x >> y;

    z = (x > y) ? x : y; // x와 y 중 큰 값을 z에 대입

    cout << "두 수 중 큰 수는 " << z << "입니다.\n";

    return 0;
}
  • (x > y) ? x : y: xy보다 크면 x를, 그렇지 않으면 y를 반환하여 z에 대입합니다.

7. switch 구문: 다중 분기 처리

switch 구문은 하나의 값을 여러 가지 경우와 비교하여 해당하는 코드 블록을 실행합니다.

switch (표현식) {
    case 값1:
        // 표현식의 값이 값1과 같을 때 실행될 코드
        break;
    case 값2:
        // 표현식의 값이 값2과 같을 때 실행될 코드
        break;
    // ...
    default:
        // 어떤 case에도 해당하지 않을 때 실행될 코드
}
  • 표현식: 정수형 값으로 평가되는 표현식입니다.
  • case 레이블: case 뒤에 오는 값은 정수 상수여야 합니다.
  • break 문: case 블록 실행 후 switch 구문을 빠져나가도록 합니다.
  • default 레이블: 어떤 case에도 해당하지 않을 때 실행될 코드를 지정합니다.

예제: switch 구문을 사용하여 메뉴 선택 처리

#include <iostream>

using namespace std;

int main() {
    cout << "다음 중에서 하나를 선택하십시오.\n";
    cout << "1) 당근\t2) 비디오 게임\n";
    cout << "3) C++ 강좌\t4) 기타\n";
    cout << "번호를 입력하십시오: ";

    int choice;
    cin >> choice;

    switch (choice) {
        case 1: 
            cout << "당근은 맛있습니다.\n";
            break;
        case 2:
            cout << "비디오 게임은 재미있습니다.\n";
            break;
        case 3:
            cout << "C++ 강좌는 유익합니다.\n";
            break;
        case 4:
            cout << "기타는 다양합니다.\n";
            break;
        default: 
            cout << "유효하지 않은 선택입니다.\n";
    }

    return 0;
}
  • switch (choice): 사용자 입력 값 choice를 각 case 레이블의 값과 비교합니다.
  • 일치하는 case 레이블을 찾으면 해당 블록의 코드를 실행하고, break 문을 만나 switch 구문을 빠져나갑니다.
  • 일치하는 case 레이블이 없으면 default 블록의 코드를 실행합니다.

8. break와 continue

  • break: 현재 실행 중인 루프 또는 switch 구문을 즉시 종료합니다.
  • continue: 현재 루프 반복을 종료하고 다음 반복으로 넘어갑니다.

예제: break와 continue 사용

for (int i = 0; i < 100; i++) {
    if (i % 2 == 0)  // 짝수일 때
        continue;    // 다음 반복으로 건너뜀
    cout << i << endl; // 홀수만 출력
    if (i > 10)
        break;        // i가 10보다 커지면 루프 종료
}
  • continue: 짝수일 때 continue를 만나 현재 반복을 종료하고 다음 반복으로 넘어갑니다.
  • break: i가 10보다 커지면 break를 만나 루프를 완전히 종료합니다.

9. 파일 입출력: 데이터 영구 저장 및 불러오기

C++는 파일 입출력 기능을 제공하여 프로그램 외부에 데이터를 저장하고 불러올 수 있습니다.

  • fstream 헤더 파일: 파일 입출력 관련 클래스와 함수를 제공합니다.
  • ofstream: 출력 파일 스트림을 나타내는 클래스입니다.
  • ifstream: 입력 파일 스트림을 나타내는 클래스입니다.
  • fstream: 입력과 출력 모두 가능한 파일 스트림을 나타내는 클래스입니다.

예제: 파일에 텍스트 쓰기 및 읽기

#include <iostream>
#include <fstream> 
#include <string> 

int main() {
    using namespace std; 

    ofstream outFile;       // 출력 파일 스트림 객체 생성
    outFile.open("poem.txt"); // 파일 열기

    outFile << "Roses are red,\n";
    outFile << "Violets are blue.\n";
    outFile.close();        // 파일 닫기

    ifstream inFile;        // 입력 파일 스트림 객체 생성
    inFile.open("poem.txt");

    char ch;
    while (inFile.get(ch))  // 파일 끝까지 읽기
        cout << ch;

    inFile.close();

    return 0;
}
  • ofstream outFile;: 출력 파일 스트림 객체 outFile을 생성합니다.
  • outFile.open("poem.txt");: "poem.txt" 파일을 쓰기 모드로 엽니다.
  • outFile << ...;: outFile 객체를 사용하여 파일에 텍스트를 씁니다.
  • outFile.close();: 파일을 닫습니다.
  • ifstream inFile;: 입력 파일 스트림 객체 inFile을 생성합니다.
  • inFile.open("poem.txt");: "poem.txt" 파일을 읽기 모드로 엽니다.
  • while (inFile.get(ch)): 파일 끝까지 문자를 읽어 ch에 저장하고 출력합니다.
  • inFile.close();: 파일을 닫습니다.

파일 입출력을 통해 프로그램은 데이터를 영구적으로 저장하고 필요할 때 다시 불러와 사용할 수 있습니다.

 

7장. 함수 - C++의 프로그래밍 모듈

이 장에서는 C++의 핵심 구성 요소인 함수에 대해 자세히 알아봅니다. 함수는 프로그램을 모듈화하고 코드 재사용성을 높이는 데 중요한 역할을 합니다. 함수의 기본 개념부터 매개변수 전달 방식, 반환 값 처리, 배열, 문자열, 구조체 처리, 함수 포인터, 재귀 호출까지 다양한 함수 활용 방법을 배우고 실제 코드 예제를 통해 익힐 수 있습니다.

1. 함수의 기초

  • 함수: 특정 작업을 수행하는 코드 블록으로, 프로그램을 모듈화하고 코드 재사용성을 높이는 데 사용됩니다.
  • 함수 사용 3단계:
    1. 함수 정의: 함수의 이름, 반환 유형, 매개변수 목록, 함수 몸체를 정의합니다.
    2. 함수 원형: 컴파일러에게 함수의 인터페이스(반환 유형, 이름, 매개변수 목록)를 알려줍니다.
    3. 함수 호출: 함수를 실행하기 위해 함수 이름과 필요한 인자를 사용하여 호출합니다.

예제: 간단한 함수 호출 예제

#include <iostream>

void simple(); // 함수 원형

int main() {
    using namespace std;

    cout << "main()에서 simple() 함수를 호출합니다: \n";
    simple(); // 함수 호출
    cout << "main()이 simple() 함수와 종료됩니다. \n";

    return 0;
}

// 함수 정의
void simple() {
    using namespace std;
    cout << "여기는 simple() 함수입니다.\n";
}
  • simple() 함수: 화면에 "여기는 simple() 함수입니다."라는 메시지를 출력하는 간단한 함수입니다.
  • 함수 원형: void simple();simple() 함수가 반환 값이 없고 매개변수도 없음을 컴파일러에게 알려줍니다.
  • 함수 호출: main() 함수에서 simple()을 호출하여 simple() 함수의 코드를 실행합니다.

2. 함수 매개변수와 값 전달

함수는 매개변수를 통해 외부 데이터를 받아 처리할 수 있습니다. C++에서는 기본적으로 값에 의한 전달(pass-by-value) 방식을 사용하여 매개변수를 전달합니다.

  • 값에 의한 전달: 함수 호출 시 실제 매개변수(argument)의 값을 복사하여 형식 매개변수(parameter)에 전달합니다. 따라서 함수 내부에서 형식 매개변수의 값을 변경해도 원본 데이터는 영향을 받지 않습니다.
  • 형식 매개변수: 함수 정의에서 사용되는 매개변수입니다.
  • 실제 매개변수: 함수 호출 시 전달되는 값입니다.

예제: 매개변수를 사용하는 함수

#include <iostream>

void n_chars(char, int);

int main() {
    int times;
    char ch;

    std::cout << "문자를 하나 입력하십시오: ";
    std::cin >> ch;

    while (ch != 'q') {
        std::cout << "정수를 하나 입력하십시오: ";
        std::cin >> times;

        n_chars(ch, times); 

        std::cout << "\n계속하려면 다른 문자를 입력하고, 끝내려면 q를 누르십시오: ";
        std::cin >> ch;
    }

    return 0;
}

void n_chars(char c, int n) {
    while (n-- > 0) {
        std::cout << c;
    }
}
  • n_chars(ch, times): ch 문자를 times 횟수만큼 출력하는 함수입니다.
  • char c, int n: n_chars() 함수의 형식 매개변수입니다. 함수 호출 시 전달된 값들이 복사되어 저장됩니다.

3. 함수 원형: 컴파일러에게 함수 정보 제공

함수 원형은 컴파일러에게 함수의 인터페이스(반환 유형, 이름, 매개변수 목록)를 알려주는 역할을 합니다. 함수 원형은 함수 호출 전에 선언되어야 하며, 함수 정의와 동일한 반환 유형과 매개변수 목록을 가져야 합니다.

  • 함수 원형의 필요성:
    • 컴파일러가 함수 호출 시 전달되는 인자의 개수와 데이터형을 확인하여 오류를 방지합니다.
    • 컴파일러가 함수의 반환 값을 올바르게 처리할 수 있도록 정보를 제공합니다.
    • 여러 파일로 구성된 프로그램에서 함수 간의 연결을 돕습니다.
  • 함수 원형 작성: 함수 머리에 세미콜론(;)을 붙여 작성합니다. 매개변수 이름은 생략 가능합니다.

예제: 함수 원형

double cube(double x); // double형 값을 반환하고 double형 매개변수를 하나 받는 함수
void n_chars(char, int); // 반환 값이 없고 char형과 int형 매개변수를 받는 함수

4. 배열을 처리하는 함수

배열을 함수에 전달할 때는 배열 전체를 복사하는 대신 배열의 시작 주소(포인터)를 전달하여 메모리 사용량과 처리 시간을 절약합니다.

  • 배열 매개변수: 함수 정의에서 배열 매개변수는 typeName arrayName[] 또는 typeName * arrayName 형식으로 선언할 수 있습니다. 둘 다 배열의 시작 주소를 나타내는 포인터로 처리됩니다.
  • 배열 크기 전달: 함수는 배열의 크기를 알 수 없으므로, 배열 크기를 나타내는 추가 매개변수를 전달해야 합니다.

예제: 배열 원소의 합을 계산하는 함수

int sum_arr(int arr[], int n) {
    int total = 0;
    for (int i = 0; i < n; i++) {
        total += arr[i];
    }
    return total;
}
  • int arr[]: 배열의 시작 주소를 나타내는 포인터 매개변수입니다.
  • int n: 배열의 크기를 나타내는 매개변수입니다.
  • 함수 내부에서는 arr[i]를 사용하여 배열 원소에 접근하고 합계를 계산합니다.

5. 문자열을 처리하는 함수

C 스타일 문자열은 널 문자(\0)로 끝나는 문자 배열입니다. 문자열을 함수에 전달할 때는 문자열의 시작 주소(포인터)를 전달합니다.

  • 문자열 매개변수: 함수 정의에서 문자열 매개변수는 char * str 또는 const char * str 형식으로 선언할 수 있습니다. const를 사용하면 함수 내부에서 문자열을 변경할 수 없도록 보호합니다.
  • 문자열 끝 판별: 널 문자(\0)를 만날 때까지 문자열을 처리합니다.

예제: 문자열에서 특정 문자의 개수를 세는 함수

unsigned int c_in_str(const char * str, char ch) {
    unsigned int count = 0;

    while (*str) {  // *str가 '\0'이 아니면 (즉, 문자열의 끝이 아니면)
        if (*str == ch)
            count++;
        str++; // 다음 문자로 이동
    }
    return count;
}
  • const char * str: 문자열의 시작 주소를 나타내는 포인터 매개변수입니다. const를 사용하여 문자열을 변경할 수 없도록 보호합니다.
  • while (*str): 널 문자를 만날 때까지 루프를 반복합니다.
  • str++: 포인터를 다음 문자로 이동시킵니다.

6. 구조체를 처리하는 함수

구조체는 여러 데이터형의 값을 하나의 단위로 묶어 저장하는 데이터 구조입니다. 구조체를 함수에 전달할 때는 값에 의한 전달 또는 주소에 의한 전달 방식을 사용할 수 있습니다.

6.1 값에 의한 전달 (Pass-by-value)

  • 방식: 함수 호출 시 구조체의 모든 멤버 변수 값을 복사하여 새로운 구조체 객체를 생성하고, 이 복사본을 함수에 전달합니다.
  • 장점: 함수 내부에서 구조체를 변경해도 원본 구조체에는 영향을 미치지 않으므로 안전합니다.
  • 단점: 구조체의 크기가 클 경우 복사 과정에서 메모리 사용량과 시간이 증가하여 성능 저하를 초래할 수 있습니다.
  • 적용: 구조체의 크기가 작거나 함수 내부에서 구조체를 변경하지 않을 때 사용하는 것이 좋습니다.

예제: 값에 의한 전달

#include <iostream>

struct travel_time {
    int hours;
    int mins;
};

travel_time sum(travel_time t1, travel_time t2); // 값 전달

int main() {
    // ... (구조체 변수 선언 및 초기화)

    travel_time trip = sum(day1, day2); // 구조체 값을 복사하여 전달
    // ...
}

travel_time sum(travel_time t1, travel_time t2) {
    // ... (t1과 t2는 복사본이므로 변경해도 원본에 영향 없음)
}

6.2 주소에 의한 전달 (Pass-by-address / Pass-by-reference)

  • 방식: 함수 호출 시 구조체의 메모리 주소를 함수에 전달합니다. 함수 내부에서는 포인터 또는 참조를 사용하여 원본 구조체에 직접 접근합니다.
  • 장점: 구조체 복사가 발생하지 않으므로 메모리 사용량과 실행 시간을 절약할 수 있습니다. 큰 구조체를 다룰 때 효율적입니다.
  • 단점: 함수 내부에서 구조체를 변경하면 원본 구조체에도 영향을 미치므로 주의해야 합니다.
  • 적용: 구조체의 크기가 크거나 함수 내부에서 구조체를 변경해야 할 때 사용하는 것이 좋습니다.

예제: 주소에 의한 전달 (포인터 사용)

void rect_to_polar(const rect * pxy, polar * pda); // 포인터 사용

int main() {
    // ... (구조체 변수 선언 및 초기화)

    rect_to_polar(&rplace, &pplace); // 구조체 주소 전달
    // ...
}

void rect_to_polar(const rect * pxy, polar * pda) {
    // ... (pxy와 pda는 포인터이므로 -> 연산자를 사용하여 멤버 접근)
}
 

예제: 주소에 의한 전달 (참조 사용)

void swapr(int & a, int & b); // 참조 사용

int main() {
    // ... (변수 선언 및 초기화)

    swapr(wallet1, wallet2); // 변수의 참조(별칭) 전달
    // ...
}

void swapr(int & a, int & b) {
    // ... (a와 b는 참조이므로 원본 변수에 직접 접근하여 값 변경)
}

6.3 언제 어떤 방식을 사용해야 할까?

  • 구조체 크기: 구조체 크기가 작으면 값 전달, 크면 주소 전달을 고려합니다.
  • 함수 내부에서의 변경: 구조체를 변경하지 않을 경우 값 전달, 변경할 경우 주소 전달을 고려합니다.
  • const: 주소 전달 시 함수 내부에서 구조체를 변경하지 않을 경우 const 키워드를 사용하여 안전성을 높입니다.

일반적으로는 값 전달 방식이 안전하지만, 성능이 중요하거나 구조체를 변경해야 하는 경우에는 주소 전달 방식을 사용하는 것이 좋습니다.

 

7. 함수와 구조체: 구조체를 활용한 함수 예제

C++ 구조체는 여러 데이터형의 값을 하나의 단위로 묶어서 사용할 수 있게 해주는 편리한 도구입니다. 함수와 결합하여 사용하면 더욱 효과적으로 데이터를 관리하고 처리할 수 있습니다. 이 섹션에서는 구조체를 함수의 매개변수와 반환 값으로 활용하는 예제를 통해 구조체와 함수의 관계를 더 깊이 이해해 보겠습니다.

7.6.1 여행 시간 계산 프로그램

이 예제에서는 여행에 소요되는 시간을 나타내는 travel_time 구조체를 정의하고, 두 여행 시간을 더하여 총 여행 시간을 계산하는 sum() 함수를 작성합니다. 또한, 계산된 여행 시간을 출력하는 show_time() 함수도 함께 작성합니다.

#include <iostream>

struct travel_time {
    int hours;
    int mins;
};

const int Mins_per_hr = 60;

travel_time sum(travel_time t1, travel_time t2);
void show_time(travel_time t);

int main() {
    using namespace std;

    travel_time day1 = {5, 45};     // 5시간 45분
    travel_time day2 = {4, 55};     // 4시간 55분

    travel_time trip = sum(day1, day2);
    cout << "이틀간 소요시간: ";
    show_time(trip);

    travel_time day3 = {4, 32};
    cout << "사흘간 소요시간: ";
    show_time(sum(trip, day3)); 

    return 0;
}

travel_time sum(travel_time t1, travel_time t2) {
    travel_time total;

    total.mins = (t1.mins + t2.mins) % Mins_per_hr; 
    total.hours = t1.hours + t2.hours + (t1.mins + t2.mins) / Mins_per_hr;

    return total; 
}

void show_time(travel_time t) {
    using namespace std;
    cout << t.hours << "시간 " << t.mins << "분\n";
}
  • travel_time 구조체: hoursmins 멤버 변수를 통해 시간과 분을 저장합니다.
  • sum() 함수: 두 개의 travel_time 구조체를 받아, 시간과 분을 각각 더하고 60분 이상인 경우 시간으로 올림 처리하여 총 여행 시간을 계산합니다.
  • show_time() 함수: travel_time 구조체를 받아 시간과 분을 출력합니다.
  • main() 함수: sum() 함수를 호출하여 총 여행 시간을 계산하고, show_time() 함수를 호출하여 결과를 출력합니다.

실행 결과:

이틀간 소요시간: 10시간 40분
사흘간 소요시간: 15시간 12분

7.6.2 좌표 변환 프로그램

이 예제에서는 2차원 평면에서 점의 위치를 나타내는 두 가지 좌표계, 직교 좌표계극좌표계를 사용합니다. rect 구조체는 직교 좌표(x, y)를 저장하고, polar 구조체는 극좌표(거리 distance, 각도 angle)를 저장합니다. rect_to_polar() 함수는 직교 좌표를 극좌표로 변환하고, show_polar() 함수는 극좌표를 출력합니다.

#include <iostream>
#include <cmath>

// 극좌표 구조체
struct polar {
    double distance;    // 원점으로부터의 거리
    double angle;       // 수평축으로부터의 각도
};

// 직교 좌표 구조체
struct rect {
    double x;           // 원점으로부터의 수평 거리
    double y;           // 원점으로부터의 수직 거리
};

// 함수 원형
polar rect_to_polar(rect xypos);
void show_polar(polar dapos);

int main() {
    using namespace std;

    rect rplace;
    polar pplace;

    cout << "x값과 y값을 입력하십시오: ";
    while (cin >> rplace.x >> rplace.y) {
        pplace = rect_to_polar(rplace); 
        show_polar(pplace);
        cout << "x값과 y값을 입력하십시오 (끝내려면 q를 입력): ";
    }
    cout << "프로그램을 종료합니다.\n";

    return 0;
}

// 직교 좌표를 극좌표로 변환
polar rect_to_polar(rect xypos) {
    using namespace std;
    polar answer;

    answer.distance = sqrt(xypos.x * xypos.x + xypos.y * xypos.y);
    answer.angle = atan2(xypos.y, xypos.x);

    return answer; 
}

// 극좌표 출력 (라디안 -> 도 변환)
void show_polar(polar dapos) {
    using namespace std;
    const double Rad_to_deg = 57.29577951; 

    cout << "거리 = " << dapos.distance;
    cout << ", 각도 = " << dapos.angle * Rad_to_deg;
    cout << " 도\n";
}
  • rect_to_polar() 함수: rect 구조체를 받아 피타고라스 정리와 atan2() 함수를 사용하여 극좌표를 계산하고 polar 구조체를 반환합니다.
  • show_polar() 함수: polar 구조체를 받아 거리와 각도(라디안을 도로 변환)를 출력합니다.
  • main() 함수: 사용자로부터 직교 좌표를 입력받아 rect_to_polar() 함수를 호출하여 극좌표로 변환하고, show_polar() 함수를 호출하여 결과를 출력합니다.

실행 결과:

x값과 y값을 입력하십시오: 30 40
거리 = 50, 각도 = 53.1301 도
x값과 y값을 입력하십시오 (끝내려면 q를 입력): -100 100
거리 = 141.421, 각도 = 135 도
x값과 y값을 입력하십시오 (끝내려면 q를 입력): q
프로그램을 종료합니다.

7. 결론

이 섹션에서는 C++에서 구조체를 함수의 매개변수와 반환 값으로 활용하는 방법을 살펴보았습니다. 구조체를 사용하면 관련된 데이터를 하나로 묶어 효율적으로 관리하고 함수를 통해 데이터를 쉽게 처리할 수 있습니다.

다음 섹션에서는 구조체를 포인터와 함께 사용하는 방법과 재귀 호출에 대해 알아보겠습니다.

소스 및 관련 콘텐츠
 
 

7. 함수와 구조체: 구조체 포인터를 이용한 효율적인 데이터 처리

앞서 살펴본 예제에서는 구조체를 값으로 전달하여 함수 내에서 복사본을 사용했습니다. 이 방법은 간단하지만, 구조체의 크기가 클 경우 메모리 사용량과 복사 시간이 증가하여 성능 저하를 초래할 수 있습니다. 이를 개선하기 위해 구조체의 주소를 함수에 전달하여 포인터를 통해 원본 구조체를 직접 조작하는 방법을 사용할 수 있습니다.

7.6.3 구조체 포인터를 사용한 좌표 변환 프로그램 개선

앞서 작성한 좌표 변환 프로그램을 구조체 포인터를 사용하도록 수정하여 메모리 사용량과 실행 시간을 줄여보겠습니다.

 
#include <iostream>
#include <cmath>

// 극좌표 구조체
struct polar {
    double distance;    // 원점으로부터의 거리
    double angle;       // 수평축으로부터의 각도
};

// 직교 좌표 구조체
struct rect {
    double x;           // 원점으로부터의 수평 거리
    double y;           // 원점으로부터의 수직 거리
};

// 함수 원형
void rect_to_polar(const rect * pxy, polar * pda);
void show_polar(const polar * pda);

int main() {
    using namespace std;

    rect rplace;
    polar pplace;

    cout << "x값과 y값을 입력하십시오: ";
    while (cin >> rplace.x >> rplace.y) {
        rect_to_polar(&rplace, &pplace); // 주소 전달
        show_polar(&pplace);             // 주소 전달
        cout << "x값과 y값을 입력하십시오 (끝내려면 q를 입력): ";
    }
    cout << "프로그램을 종료합니다.\n";

    return 0;
}

// 극좌표 출력 (라디안 -> 도 변환)
void show_polar(const polar * pda) {
    using namespace std;
    const double Rad_to_deg = 57.29577951; 

    cout << "거리 = " << pda->distance;
    cout << ", 각도 = " << pda->angle * Rad_to_deg;
    cout << " 도\n";
}

// 직교 좌표를 극좌표로 변환
void rect_to_polar(const rect * pxy, polar * pda) {
    using namespace std;

    pda->distance = sqrt(pxy->x * pxy->x + pxy->y * pxy->y);
    pda->angle = atan2(pxy->y, pxy->x);
}
  • rect_to_polar(), show_polar() 함수: 이제 구조체 포인터를 매개변수로 받습니다.
  • main() 함수: rect_to_polar(), show_polar() 함수에 구조체 변수의 주소를 전달합니다.
  • 함수 내부: 간접 멤버 접근 연산자(->)를 사용하여 포인터를 통해 구조체 멤버에 접근합니다.

개선된 점:

  • 구조체를 값으로 전달하는 대신 주소를 전달하여 메모리 사용량과 복사 시간을 줄였습니다.
  • const 키워드를 사용하여 함수 내에서 구조체 데이터가 변경되지 않도록 보호합니다.

8. 함수와 string 클래스 객체

string 객체는 클래스 기반이므로, 구조체와 마찬가지로 값 전달 또는 주소 전달 방식을 사용하여 함수에 전달할 수 있습니다.

  • 값 전달: string 객체를 함수에 값으로 전달하면 복사본이 생성되어 전달됩니다. 함수 내부에서 객체를 변경해도 원본 객체는 영향을 받지 않습니다.
  • 주소 전달: string 객체의 주소를 함수에 전달하면 함수 내부에서 포인터를 통해 원본 객체를 직접 조작할 수 있습니다.

예제: string 객체 배열 처리

#include <iostream>
#include <string>

using namespace std;

const int SIZE = 5;

void display(const string sa[], int n);

int main() {
    string list[SIZE]; 

    cout << "좋아하는 밤하늘의 광경을 " << SIZE << "개 입력하시오: \n";
    for (int i = 0; i < SIZE; i++) {
        cout << i + 1 << ": ";
        getline(cin, list[i]);
    }

    cout << "입력하신 내용은 다음과 같습니다: \n";
    display(list, SIZE);

    return 0;
}

void display(const string sa[], int n) {
    for (int i = 0; i < n; i++) {
        cout << i + 1 << ": " << sa[i] << endl;
    }
}
  • string list[SIZE]: string 객체 5개를 담는 배열을 선언합니다.
  • display(list, SIZE): list 배열을 display() 함수에 값으로 전달합니다.
  • display() 함수: const string sa[] 형식으로 매개변수를 선언하여 배열 내의 문자열을 변경할 수 없도록 보호합니다.

9. 함수와 array 객체 (C++11)

C++11에서 도입된 array 템플릿 클래스는 크기가 고정된 배열을 나타내며, 구조체와 유사하게 값 전달 또는 주소 전달 방식을 사용하여 함수에 전달할 수 있습니다.

  • 값 전달: array 객체를 함수에 값으로 전달하면 복사본이 생성되어 전달됩니다. 함수 내부에서 객체를 변경해도 원본 객체는 영향을 받지 않습니다.
  • 주소 전달: array 객체의 주소를 함수에 전달하면 함수 내부에서 포인터를 통해 원본 객체를 직접 조작할 수 있습니다.

예제: array 객체 처리

#include <iostream>
#include <array>
#include <string>

// 상수 데이터
const int Seasons = 4;
const std::array<std::string, Seasons> Snames = 
    {"Spring", "Summer", "Fall", "Winter"};

// array 객체를 수정하는 기능
void fill(std::array<double, Seasons> *pa);

// 수정하지 않고 객체를 사용하는 기능
void show(std::array<double, Seasons> da);

int main() {
    std::array<double, Seasons> expenses;
    fill(&expenses); 
    show(expenses); 
    return 0;
}

void fill(std::array<double, Seasons> *pa) {
    using namespace std;
    for (int i = 0; i < Seasons; i++) {
        cout << Snames[i] << "에 소요되는 비용: ";
        cin >> (*pa)[i]; 
    }
}

void show(std::array<double, Seasons> da) {
    using namespace std;
    double total = 0.0;
    cout << "\n계절별 비용\n";
    for (int i = 0; i < Seasons; i++) {
        cout << Snames[i] << ": $" << da[i] << endl;
        total += da[i];
    }
    cout << "총 비용: $ " << total << endl;
}
  • fill() 함수: array 객체의 주소를 받아 각 계절별 비용을 입력받습니다.
  • show() 함수: array 객체를 값으로 받아 각 계절별 비용과 총 비용을 출력합니다.
  • main() 함수: expenses array 객체를 생성하고 fill(), show() 함수를 호출하여 비용 정보를 입력받고 출력합니다.
 
10. 함수 포인터: 함수를 가리키는 포인터

함수 포인터는 함수의 주소를 저장하는 특별한 포인터입니다. 함수 포인터를 사용하면 함수를 변수처럼 다루고, 다른 함수에 매개변수로 전달하거나 배열에 저장할 수 있습니다.

  • 함수 포인터 선언: 반환_값_유형 (*포인터_이름)(매개변수_목록);
  • 함수 포인터 초기화: 함수 이름 또는 함수의 주소를 사용하여 초기화합니다.
  • 함수 포인터 호출: 포인터 이름 뒤에 괄호와 인자를 사용하여 호출합니다.

예제: 함수 포인터 사용

#include <iostream>

double betsy(int);
double pam(int);

// 함수 포인터를 매개변수로 받는 함수
void estimate(int lines, double (*pf)(int));

int main() {
    using namespace std;

    int code;

    cout << "몇 줄의 코드를 작성할 예정입니까?: ";
    cin >> code;
    cout << "함수 betsy()를 사용한 예상 시간: ";
    estimate(code, betsy);
    cout << "함수 pam()을 사용한 예상 시간: ";
    estimate(code, pam);

    return 0;
}

double betsy(int lns) {
    return 0.05 * lns;
}

double pam(int lns) {
    return 0.03 * lns + 0.0004 * lns * lns;
}

void estimate(int lines, double (*pf)(int)) {
    using namespace std;
    cout << lines << " 줄은 ";
    cout << (*pf)(lines) << " 시간이 걸릴 것입니다.\n";
}
  • double (*pf)(int): estimate() 함수의 매개변수로, int형 매개변수를 하나 받고 double형 값을 반환하는 함수를 가리키는 포인터입니다.
  • estimate(code, betsy);: betsy 함수의 주소를 estimate() 함수에 전달합니다.
  • estimate(code, pam);: pam 함수의 주소를 estimate() 함수에 전달합니다.
  • (*pf)(lines): 함수 포인터 pf가 가리키는 함수를 호출하고 lines를 인자로 전달합니다.

11. 함수 포인터 배열: 다양한 함수 선택 실행

함수 포인터 배열을 사용하면 여러 함수를 배열에 저장하고, 인덱스를 통해 원하는 함수를 선택하여 실행할 수 있습니다.

예제: 함수 포인터 배열 사용

// choices.cpp -- 메뉴 선택 프로그램
#include <iostream>
#include <vector>
#include <array>

char get_choice();
char get_first();
int get_int();
void count();
void report();

int main() {
    using namespace std;

    // 함수 포인터 배열 선언 및 초기화
    const char * choices[5] = {
        "계산 종료",
        "계산 시작",
        "결과 보고",
        "데이터 입력",
        "도움말"
    };

    int choice;
    while ((choice = get_choice()) != 'q') {
        switch (choice) {
            case 'c' : count();
                       break;
            case 'p' : report();
                       break;
            case 'f' : get_first();
                       break;
            case 'g' : get_int();
                       break;
            case 'h' : 
                for (int i = 0; i < 5; i++)
                    cout << choices[i] << endl;
                break;
        }
        cout << "다음 선택을 하십시오 (q는 종료): ";
    }
    cout << "프로그램 종료\n";
    return 0;
}

char get_choice() {
    // ... (사용자 선택 입력 처리)
}

char get_first() {
    // ... (첫 번째 입력 처리)
}

int get_int() {
    // ... (정수 입력 처리)
}

void count() {
    // ... (계산 처리)
}

void report() {
    // ... (결과 보고 처리)
}
  • const char * choices[5]: 메뉴 항목을 저장하는 문자열 포인터 배열입니다.
  • switch (choice): 사용자 선택에 따라 해당하는 함수를 호출합니다.
  • case 'h': 메뉴 항목을 출력합니다.

12. 재귀 호출: 함수가 자기 자신을 호출

재귀 호출은 함수가 자기 자신을 호출하는 것을 의미합니다. 재귀 호출은 특정 문제를 해결하는 데 유용하지만, 종료 조건을 명확하게 설정해야 무한 루프에 빠지지 않습니다.

예제: 재귀 호출을 사용하여 팩토리얼 계산

long long factorial(int n) {
    if (n == 0)
        return 1;
    else
        return n * factorial(n - 1);
}
 
  • factorial(n): n의 팩토리얼을 계산하는 함수입니다.
  • if (n == 0): 종료 조건입니다. n이 0이면 1을 반환합니다.
  • else: n이 0이 아니면 n * factorial(n - 1)을 통해 자기 자신을 호출하여 팩토리얼을 계산합니다.
 

13. 함수 오버로딩: 이름은 같지만 다른 함수들

함수 오버로딩은 같은 이름의 함수를 여러 개 정의하는 것을 허용합니다. 각 함수는 매개변수의 개수나 데이터형이 달라야 하며, 컴파일러는 함수 호출 시 전달되는 인자를 기반으로 어떤 함수를 호출할지 결정합니다.

  • 함수 오버로딩 조건:
    • 함수 이름은 같아야 합니다.
    • 매개변수 목록이 달라야 합니다. (개수, 데이터형, 순서 중 하나 이상)
    • 반환 유형은 오버로딩 조건에 영향을 주지 않습니다.

예제: 함수 오버로딩

#include <iostream>

void print(int i);
void print(double d);
void print(const char * c);

int main() {
    using namespace std;

    print(10);         // int 버전 호출
    print(3.14);       // double 버전 호출
    print("C++");      // const char * 버전 호출

    return 0;
}

void print(int i) {
    cout << "정수 출력: " << i << endl;
}

void print(double d) {
    cout << "실수 출력: " << d << endl;
}

void print(const char * c) {
    cout << "문자열 출력: " << c << endl;
}
  • print() 함수: 세 가지 버전의 print() 함수가 정의되어 있습니다. 각 함수는 다른 데이터형의 매개변수를 받습니다.
  • main() 함수: print() 함수를 호출할 때 전달되는 인자의 데이터형에 따라 적절한 버전의 print() 함수가 호출됩니다.

14. 함수 템플릿: 일반화 프로그래밍의 시작

함수 템플릿은 다양한 데이터형에 대해 동일한 작업을 수행하는 함수를 생성하는 데 사용됩니다. 템플릿을 사용하면 코드 중복을 줄이고 유지보수성을 높일 수 있습니다.

  • 함수 템플릿 정의: template <typename T> 반환_값_유형 함수_이름(매개변수_목록) 형식으로 정의합니다.
  • 템플릿 매개변수: typename T는 템플릿 매개변수로, 함수 내에서 사용될 데이터형을 나타냅니다.
  • 함수 템플릿 인스턴스화: 컴파일러는 함수 호출 시 전달되는 인자의 데이터형에 따라 적절한 함수 템플릿 인스턴스를 생성합니다.

예제: 함수 템플릿 사용

#include <iostream>

// 두 값 중 작은 값을 반환하는 함수 템플릿
template <typename T>
T lesser(T a, T b) {
    return (a < b) ? a : b;
}

int main() {
    using namespace std;

    int m = 20;
    int n = -30;
    double x = 15.5;
    double y = 25.9;

    cout << lesser(m, n) << endl;  // int 버전 호출, -30 출력
    cout << lesser(x, y) << endl;  // double 버전 호출, 15.5 출력
    cout << lesser<>(m, n) << endl; // 명시적 템플릿 인자 지정, -30 출력

    return 0;
}
  • template <typename T>: T라는 템플릿 매개변수를 사용하여 함수 템플릿을 정의합니다.
  • lesser(m, n): int형 인자를 전달하여 int 버전의 lesser() 함수가 인스턴스화되고 호출됩니다.
  • lesser(x, y): double형 인자를 전달하여 double 버전의 lesser() 함수가 인스턴스화되고 호출됩니다.
  • lesser<>(m, n): 명시적으로 템플릿 인자를 int로 지정하여 int 버전의 lesser() 함수를 호출합니다.

8장 함수의 활용: C++ 함수의 다채로운 활용법 심층 분석

이 장에서는 C++ 함수의 고급 기능들을 상세히 살펴보고, 각 기능의 활용 예시를 통해 C++ 프로그램의 효율성과 유연성을 극대화하는 방법을 깊이 있게 탐구합니다.

1. 인라인 함수 (Inline Functions)

  • 목적: 함수 호출 오버헤드를 줄여 프로그램 실행 속도 향상
  • 원리: 함수 호출 시 함수 코드를 직접 삽입하여 실행 흐름 변경 없앰
  • 사용: 함수 선언 또는 정의 앞에 inline 키워드 추가

예시:

inline int square(int x) {
    return x * x;
}

int main() {
    int result = square(5);  // 함수 호출 대신 square 함수 코드가 직접 삽입됨
    std::cout << result << std::endl;  // 출력: 25
    return 0;
}
  • 주의:
    • 컴파일러가 inline 요청을 거부할 수 있음 (함수 크기, 재귀 호출 등의 이유로)
    • 컴파일 시간 증가 가능성 (코드 중복 삽입으로 인해)
    • 디버깅 어려움 (실제 함수 호출이 아닌 코드 삽입으로 인해)
  • 적용: 작고 자주 호출되는 함수에 유용 (e.g., getter/setter 함수, 간단한 수학 연산 함수)

2. 참조 변수 (Reference Variables)

  • 목적: 변수에 대한 별칭(alias) 생성, 함수 매개변수로 활용하여 원본 데이터 직접 접근 및 수정
  • 선언: int &ref = var; (ref는 var의 참조)

예시:

void swap(int &a, int &b) {
    int temp = a;
    a = b;
    b = temp;
}

int main() {
    int x = 10, y = 20;
    swap(x, y);  // x와 y의 값이 실제로 교환됨
    std::cout << x << " " << y << std::endl;  // 출력: 20 10
    return 0;
}
 
  • 특징:
    • 반드시 초기화 필요
    • 초기화 후 변경 불가 (다른 변수 참조 불가)
    • 포인터와 유사하지만 더 안전하고 사용하기 편리
    • null 값 가질 수 없음
    • 배열 참조 가능 (e.g., int (&arrRef)[5] = myArray;)
  • 활용:
    • 함수 매개변수: 값 복사 없이 원본 데이터 변경 가능 (큰 객체 전달 시 효율적)
    • 함수 반환값: 객체 복사 없이 객체 자체 반환 가능 (주의: 지역 변수 참조 반환은 안 됨, dangling reference 발생 가능)
    • const 참조: 데이터 변경 방지, 임시 변수 생성 허용 (e.g., void func(const std::string &str);)

3. 디폴트 매개변수 (Default Arguments)

  • 목적: 함수 호출 시 일부 매개변수 생략 가능, 함수 사용의 유연성 증가
  • 지정: 함수 원형에서 매개변수에 기본값 할당 (e.g., void func(int x = 0);)

예시:

void greet(const std::string &name, const std::string &greeting = "Hello") {
    std::cout << greeting << ", " << name << "!" << std::endl;
}

int main() {
    greet("Alice");        // 출력: Hello, Alice!
    greet("Bob", "Hi");    // 출력: Hi, Bob!
    return 0;
}
  • 규칙:
    • 디폴트 매개변수는 매개변수 리스트의 오른쪽에서부터 지정해야 함
    • 함수 오버로딩과 함께 사용 시 주의 필요 (모호성 발생 가능)
  • 활용: 함수 호출 시 선택적 매개변수 사용 가능, 함수 인터페이스 단순화

4. 함수 오버로딩 (Function Overloading)

  • 목적: 같은 이름의 함수를 여러 개 정의하여 다양한 데이터 형 처리
  • 원리: 함수 시그니처(매개변수 타입 및 개수)를 기반으로 함수 구분

예시:

int absolute(int x) {
    return x >= 0 ? x : -x;
}

double absolute(double x) {
    return x >= 0 ? x : -x;
}

int main() {
    int intResult = absolute(-5);      // int 버전 호출
    double doubleResult = absolute(-3.14);  // double 버전 호출
    std::cout << intResult << " " << doubleResult << std::endl;  // 출력: 5 3.14
    return 0;
}
  • 활용: 동일한 작업을 다양한 데이터 형에 대해 수행할 때 유용, 함수 인터페이스 직관적 설계 가능
  • 주의:
    • 반환 타입만 다른 함수는 오버로딩 불가
    • 모호성 발생 가능성 (매개변수 타입 변환 등으로 인해)

5. 함수 템플릿 (Function Templates)

  • 목적: 다양한 데이터 형에 대해 동일한 알고리즘 적용, 코드 재사용성 증가
  • 선언: template <typename T> (T는 임의의 데이터 형)

예시:

template <typename T>
T max(T a, T b) {
    return a > b ? a : b;
}

int main() {
    int intResult = max(3, 5);        // int 버전 생성 및 호출
    double doubleResult = max(2.71, 3.14);  // double 버전 생성 및 호출
    std::cout << intResult << " " << doubleResult << std::endl;  // 출력: 5 3.14
    return 0;
}
  • 원리: 컴파일 시점에 템플릿을 사용하여 필요한 데이터 형에 맞는 함수 생성
  • 활용:
    • 일반화 프로그래밍 (generic programming)
    • 컨테이너 (containers)
    • 알고리즘 (algorithms)
  • 특수화 (Specialization): 특정 데이터 형에 대해 별도의 함수 정의 제공 가능

예시:

template <>
const char* max(const char* a, const char* b) {
    return strcmp(a, b) > 0 ? a : b;
}
  • 오버로딩: 템플릿 함수도 오버로딩 가능

예시:

template <typename T>
void swap(T &a, T &b) { ... }  // 일반적인 swap 함수

template <typename T, size_t N>
void swap(T (&a)[N], T (&b)[N]) { ... }  // 배열 swap 함수 (특수화된 형태)

9장 C++ 메모리 모델과 이름 공간 심층 분석

이 장에서는 C++ 프로그램의 구조와 데이터 관리 방법에 대해 자세히 알아봅니다. 특히, 분할 컴파일, 메모리 모델, 이름 공간 등 C++ 프로그램의 확장성과 안정성을 높이는 데 중요한 개념들을 다룹니다.

1. 분할 컴파일 (Separate Compilation)

  • 목적: 대규모 프로그램을 여러 파일로 나누어 관리하고 컴파일하여 효율성 향상
  • 원리: 각 파일을 독립적으로 컴파일한 후, 링커를 통해 하나의 실행 파일로 연결
  • 헤더 파일: 함수 원형, 구조체 정의, 매크로 등 여러 파일에서 공유할 정보를 저장
  • 소스 파일: 실제 함수 정의 및 변수 선언 등 프로그램 로직을 포함
  • 컴파일 과정:
    1. 전처리기: #include 지시자를 통해 헤더 파일 내용을 소스 코드에 삽입
    2. 컴파일러: 각 소스 파일을 컴파일하여 목적 코드 파일 생성
    3. 링커: 목적 코드 파일들을 라이브러리와 결합하여 실행 파일 생성
  • 장점:
    • 코드 재사용성 증가
    • 컴파일 시간 단축 (수정된 파일만 다시 컴파일)
    • 유지 보수 용이성 향상 (모듈화된 코드 관리)
  • 주의 사항:
    • 헤더 파일 중복 포함 방지 (#ifndef, #define, #endif 활용)
    • 함수 및 변수 정의는 헤더 파일에 넣지 않고 소스 파일에 넣기
    • 컴파일러 간 호환성 문제 (외부 라이브러리 링크 시 주의)

예시:

// coordin.h (헤더 파일)
#ifndef COORDIN_H_
#define COORDIN_H_

struct polar { ... };  // 극좌표 구조체 정의
struct rect { ... };   // 직교좌표 구조체 정의

polar rect_to_polar(rect xypos);  // 함수 원형
void show_polar(polar dapos);     // 함수 원형

#endif
 
// file1.cpp (소스 파일)
#include <iostream>
#include "coordin.h" 

int main() {
    // ... (rect_to_polar, show_polar 함수 호출)
}
 
// file2.cpp (소스 파일)
#include <cmath>
#include "coordin.h"

// rect_to_polar, show_polar 함수 정의

2. 기억 존속 시간, 사용 범위, 링크

C++ 변수와 함수는 기억 존속 시간, 사용 범위, 링크라는 세 가지 속성을 가집니다. 이 속성들은 변수와 함수의 메모리 할당 방식, 접근 가능 범위, 파일 간 공유 여부를 결정합니다.

2.1 기억 존속 시간 (Storage Duration)

  • 자동 기억 존속 시간 (automatic): 함수 내부에서 선언된 변수, 함수 매개변수 등이 해당됩니다. 함수 호출 시 생성되고 함수 종료 시 소멸합니다.
  • 정적 기억 존속 시간 (static): 프로그램 실행 시작 시 생성되고 종료 시 소멸합니다. static 키워드를 사용하거나 함수 외부에서 선언된 변수가 해당됩니다.
  • 쓰레드 기억 존속 시간 (thread_local, C++11): 각 쓰레드마다 독립적인 변수를 생성합니다. thread_local 키워드를 사용하여 선언합니다.
  • 동적 기억 존속 시간 (dynamic): new 연산자로 메모리를 할당하고 delete 연산자로 해제할 때까지 유지됩니다.

2.2 사용 범위 (Scope)

  • 블록 사용 범위 (block scope): 함수 또는 블록 내부에서 선언된 변수가 해당 블록 내에서만 접근 가능합니다.
  • 파일 사용 범위 (file scope): 함수 외부에서 선언된 변수가 해당 파일 내에서 접근 가능합니다.
  • 함수 원형 사용 범위 (function prototype scope): 함수 매개변수 이름이 함수 원형 내에서만 유효합니다.
  • 클래스 사용 범위 (class scope): 클래스 내부에서 선언된 멤버가 해당 클래스 내에서만 접근 가능합니다.
  • 이름 공간 사용 범위 (namespace scope): 이름 공간 내에서 선언된 요소들이 해당 이름 공간 내에서만 접근 가능합니다.

2.3 링크 (Linkage)

  • 외부 링크 (external linkage): 여러 파일에서 공유 가능한 변수 또는 함수입니다. 기본적으로 함수 외부에서 선언된 변수와 함수는 외부 링크를 가집니다.
  • 내부 링크 (internal linkage): 해당 파일 내에서만 공유 가능한 변수 또는 함수입니다. static 키워드를 사용하여 선언합니다.
  • 링크 없음 (no linkage): 블록 내부에서 선언된 변수처럼 공유되지 않는 변수 또는 함수입니다.

3. 이름 공간 (Namespaces)

  • 목적: 이름 충돌 방지, 코드 구성 및 관리 용이성 향상
  • 선언: namespace 이름 { ... }
  • 사용:
    • 정규화된 이름 (qualified name): 이름공간::요소 형태로 접근
    • using 선언: using 이름공간::요소; 특정 요소를 현재 범위로 가져옴
    • using 지시자: using namespace 이름공간; 해당 이름 공간의 모든 요소를 현재 범위로 가져옴 (주의: 이름 충돌 가능성 증가)
  • 중첩된 이름 공간: 이름 공간 안에 다른 이름 공간을 선언 가능
  • 익명 이름 공간: 이름 없는 이름 공간, 파일 내부에서만 접근 가능 (내부 링크 변수 대체 가능
namespace Pers {
    struct Person { ... };
    void getPerson(Person&);
    void showPerson(const Person&);
}

namespace Debt {
    using namespace Pers;  // Pers 이름 공간의 요소들을 사용 가능하게 함

    struct Debt {
        Person name;
        double amount;
    };

    void getDebt(Debt&);
    void showDebt(const Debt&);
    double sumDebts(const Debt ar[], int n);
}

int main() {
    using Debt::Debt;
    using Debt::showDebt;

    Debt golf = { {"Benny", "Goatsniff"}, 120.0 };
    showDebt(golf); 

    // ...
}

 

 
728x90
반응형