개발 이야기/[스터디] c++

[A Tour of C++] Chapter 1. The Basics

경이로운아일라 2024. 8. 23. 20:57
이 글은 <A Tour of C++ Second Edition, Bjarne Stroustrup>를 참고하여 작성하였습니다.

 

Chapter 1. The Basics

이번 장에서는 C++ 기본 구성 요소부터 알아보자.

 

1.2. Programs

C++ 프로그램 동작 과정은 아래와 같습니다.

http://cnl.sogang.ac.kr/

 

소스 파일은 먼저 컴파일러에 의해 object 파일로 변환되며, 이 object 파일들은 라이브러리 파일과 함께 링커에 의해 결합되어 실행 가능한 파일로 만들어집니다. 최종적으로 생성된 실행 파일은 운영 체제에서 실행될 수 있는 상태가 됩니다.

 

* object 파일: 파일러가 C++ 소스 코드를 컴파일한 후 생성하는 중간 파일입니다. 객체 파일은 기계어 코드로 변환된 프로그램의 일부분을 포함하며, 아직 실행 가능한 프로그램은 아닙니다. Object 파일은 프로그램을 여러 모듈로 나누어 독립적으로 컴파일할 수 있게 해, 코드의 수정이나 업데이트 시 전체를 재컴파일할 필요 없이 변경된 부분만 컴파일하고 다시 링킹할 수 있도록 합니다. 이를 통해 대규모 프로젝트의 유지 보수성과 관리가 크게 향상됩니다.

 

1.3. Functions

함수 정의

아래와 같이 리턴 타입, 함수명, 인자를 표시하여 함수를 정의할 수 있습니다.

double get(const vector<double>& vec, int index)

Overloading

같은 이름을 가진 여러 함수를 정의하되, 각 함수의 매개변수 리스트(인자의 타입, 개수, 순서)가 다르도록 하는 것을 의미합니다.

#include <iostream>
using namespace std;

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

void print(double f) {
    cout << "실수: " << f << endl;
}

void print(int i, double f) {
    cout << "정수와 실수: " << i << ", " << f << endl;
}

int main() {
    print(5);           // 정수: 5
    print(3.14);        // 실수: 3.14
    print(7, 2.71);     // 정수와 실수: 7, 2.71
    return 0;
}

 

 

함수는 짧고 간결하게, 하나의 작업만 수행하도록 정의해야 합니다.

 

 

1.4. Types, Variables, and Arithmetic

기본 데이터 타입

C++에는 다양한 기본 데이터 타입이 있으며, 각 타입은 메모리에서 특정 크기를 차지합니다. 일반적으로 많이 사용되는 타입과 그 크기는 다음과 같습니다:

  • bool (1 byte): 불리언 타입으로, true 또는 false 값을 가집니다.
  • char (1 byte): 문자 타입으로, ASCII 문자 하나를 저장합니다.
  • int (4 bytes): 기본 정수형 타입으로, 일반적인 정수 값을 저장합니다.
  • double (8 bytes): 실수형 타입으로, 배정밀도 부동소수점 수를 저장합니다.

또한, unsigned 키워드를 사용하여 음수를 허용하지 않는 정수형 타입을 정의할 수 있습니다. 예를 들어, unsigned int는 0 이상만 표현 가능한 정수 타입입니다. C++에서는 표준 라이브러리에서 제공하는 더 큰 정수형 타입인 std::uint64_t도 사용하여 64비트 크기의 무부호 정수를 다룰 수 있습니다.

Variable Initialization

C++에서는 변수를 여러 가지 방식으로 초기화할 수 있습니다. 초기화 방법에 따라 동작이 달라지므로, 상황에 맞는 초기화 방법을 선택하는 것이 중요합니다.

 

- 기본 초기화

= 연산자를 사용하여 변수를 초기화하는 방식입니다.이 방식은 널리 사용되며, 변수를 선언함과 동시에 값을 할당합니다. 그러나 이 방법은 narrowing conversion과 같은 위험한 변환을 허용할 수 있습니다.

 

double d = 2.3;

 

 

- 중괄호 초기화

C++11에서 도입된 중괄호 {}를 사용하는 초기화 방식입니다. 이 방식은 더 엄격한 초기화를 제공합니다. 예를 들어, narrowing conversion이 발생할 경우, 컴파일러는 오류를 발생시킵니다. 따라서 안전한 초기화를 보장받을 수 있습니다.

int d = 2.3;   // OK: d는 2로 자동 변환
int d {2.3};   // ERROR: 컴파일 에러 발생
 
 
Auto
 
C++에서 auto 키워드는 변수의 타입을 자동으로 추론하도록 하는데 사용됩니다. 초기화 값이 명확한 경우, 타입을 명시하지 않고 auto를 사용하면 가독성이 높아지고 코드가 간결해집니다.
auto i = 42;   // i는 int 타입으로 자동 추론됨

 

Tip

  • Narrowing conversion 피하기: 타입 변환으로 인해 데이터 손실이 발생할 수 있으므로, 이러한 위험을 피하기 위해 중괄호 {} 초기화를 사용하는 것이 좋습니다.
  • auto 적극 사용: 변수 타입을 명시적으로 선언할 필요가 없고, 초기화 값이 명확하다면 auto 키워드를 사용하여 코드의 가독성과 유지 보수성을 높이는 것이 좋습니다.

 

1.5. Scope and Lifetime

Local Scope

 

  • 정의: 함수 내부에서 선언된 변수는 지역 스코프를 가집니다.
  • 특징: 지역 스코프의 변수는 함수가 실행되는 동안에만 존재하며, 함수가 종료되면 소멸합니다. 다른 함수에서는 이 변수를 접근할 수 없습니다.

 

Class Scope

 

  • 정의: 클래스 내부(함수 외부) 또는 열거형(enum) 내부에서 선언된 변수는 클래스 스코프를 가집니다.
  • 특징: 클래스 스코프의 변수는 클래스의 인스턴스가 존재하는 동안에만 유효하며, 클래스의 멤버 함수들을 통해 접근할 수 있습니다.

 

Namespace Scope

 

  • 정의: 네임스페이스 내부에서 선언된 변수는 네임스페이스 스코프를 가집니다. 네임스페이스 밖에서 선언된 변수들도 전역 네임스페이스에 포함됩니다.
  • 특징: 네임스페이스 스코프의 변수는 해당 네임스페이스 내에서 접근할 수 있으며, 전역 네임스페이스에 선언된 변수들은 프로그램 전반에서 접근 가능합니다.

 

변수를 사용할 수 있는 범위를 최소화하여 선언하세요. 즉, 필요한 곳에서만 변수를 선언함으로써 코드의 안전성과 가독성을 높일 수 있습니다.

 

 

1.7. Constants

const vs constexpr

C++에서 const와 constexpr는 모두 불변(immutable) 값을 표현하는 데 사용되지만, 그 사용 시기와 의미에는 중요한 차이가 있습니다.

 

공통점 - 불변성: const와 constexpr 모두 변수나 객체의 값을 한 번 설정한 후에는 변경할 수 없도록 합니다. 이로 인해 프로그램의 안전성과 예측 가능성이 높아집니다.

const int a = 10;       // a는 이후에 변경할 수 없음
constexpr int b = 20;   // b는 이후에 변경할 수 없음

 

차이점 - 값 결정 시기: const 변수는 실행 시(runtime)에 그 값이 결정될 수 있습니다. 즉, 컴파일 시점에는 값이 확정되지 않더라도, 프로그램이 실행되면서 값이 정해지는 경우에도 사용할 수 있습니다. constexpr 변수는 컴파일 시점(compile-time)에 값이 반드시 결정되어야 합니다. 컴파일러는 constexpr 변수의 값을 컴파일 시에 계산하고, 상수로 다룹니다. 이를 통해 최적화가 가능하며, 컴파일 타임 상수(constant expressions)를 정의할 때 유용합니다.

const int x = someFunction();  // someFunction()은 실행 시 호출
constexpr int y = 10 * 20;  // 컴파일 시점에 값이 결정됨

 

이처럼, const는 실행 중 값을 결정해야 할 때 사용하고, constexpr은 컴파일 타임에 값이 확정되어야 하는 상수를 정의할 때 사용하는 것이 좋습니다. 각각의 용도를 잘 이해하고 적절히 사용하면, 코드의 성능과 안전성을 동시에 높일 수 있습니다.

 

1.8. Pointers, Arrays, and References

C++에서 배열, 포인터, 레퍼런스는 메모리와 객체에 효율적으로 접근하고 조작하는 데 중요한 역할을 합니다. 이들은 직접적인 메모리 주소를 다룰 수 있는 강력한 도구를 제공합니다.

 

Array

정의: 배열은 같은 타입의 데이터를 연속된 메모리 공간에 저장하는 자료 구조입니다.

예시

// v는 int 타입의 10개 요소를 가진 배열로, 각각의 요소는 연속된 메모리 위치에 저장됩니다.
int v[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};

 

 

Pointer

정의: 포인터는 특정 타입의 객체가 위치한 메모리 주소를 저장하는 변수입니다.

역참조(dereferencing): 포인터 변수에 *를 붙이면, 그 포인터가 가리키는 주소의 값을 반환합니다.

char a = 'A';
char* p = &a;   // p는 a의 주소를 가리킴
char x = *p;    // x는 a의 값을 참조하여 'A'가 됨

Reference

정의: 레퍼런스는 특정 객체의 주소를 참조하는 또 다른 이름입니다. 변수 선언 시 &를 사용하여 참조자를 정의합니다.

특징: 레퍼런스는 포인터와 달리 항상 유효한 객체를 참조해야 하며, 참조가 초기화된 후에는 다른 객체를 참조할 수 없습니다.

int a = 10;
int& ref = a;  // ref는 a의 참조자

 

Null Pointer

정의: 포인터가 유효한 객체를 가리키지 않을 때 사용하는 특별한 포인터 값입니다. nullptr은 포인터가 아무런 객체도 가리키지 않음을 나타냅니다.

int* p = nullptr;  // p는 아무런 객체도 가리키지 않음

 

객체를 복사하지 않고 직접 값을 수정하거나, 배열의 요소를 순차적으로 접근하고 싶을 때 포인터와 레퍼런스를 사용합니다.

 

1.9. Mapping to Hardware

C++은 하드웨어, 특히 메모리와 매우 밀접하게 연결된 언어입니다. 이 섹션에서는 C++이 메모리를 어떻게 모델링하고, 포인터를 통해 하드웨어와 상호작용하는지에 대해 설명합니다.

 

1. 메모리와 포인터

  • 메모리 모델: C++은 메모리를 일련의 연속된 위치들로 취급합니다. 메모리의 각 위치는 특정 크기(일반적으로 1 바이트)를 가지며, 이 위치들은 주소에 의해 식별됩니다. 포인터는 이러한 메모리 주소를 저장하는 변수로, 특정 데이터가 저장된 위치를 가리킵니다.
  • 예를 들어, int 타입의 배열이 메모리에 연속적으로 저장되면, 포인터를 사용하여 배열의 첫 번째 요소부터 마지막 요소까지 순차적으로 접근할 수 있습니다.

2. 변수의 독립성

  • 변수와 메모리 주소: C++에서 두 변수가 서로 다른 메모리 주소를 가지고 있다면, 하나의 변수 값을 다른 변수에 할당해도 두 변수는 독립적입니다. 이 말은, 값을 할당하면 값 자체는 복사되지만, 각각의 변수는 여전히 서로 다른 메모리 위치를 가리킵니다.
int x = 5;
int y = x;  // y는 x의 값을 복사하지만, x와 y는 서로 다른 메모리 위치에 저장됨
x = 10;    // x의 값을 변경해도 y는 영향을 받지 않음

 

3. 포인터를 사용한 메모리 주소 조작

  • 포인터의 역할: 변수의 값을 독립적으로 관리하는 것과 달리, 포인터를 사용하면 변수의 실제 메모리 주소에 접근하고 조작할 수 있습니다. 이는 메모리 주소를 직접 다루어야 할 때, 또는 특정 데이터에 대한 참조를 공유해야 할 때 유용합니다.
int x = 5;
int* p = &x;  // p는 x의 메모리 주소를 가리킴
*p = 10;      // p가 가리키는 메모리 위치에 있는 값을 변경 (x = 10)

 

 

이 예시에서, p는 x의 메모리 주소를 가리키고, *p를 사용하여 x의 값을 직접 변경할 수 있습니다. 이는 변수의 값을 복사하는 것이 아니라, 변수의 원래 메모리 위치에 저장된 값을 변경하는 것입니다.

 

 

  • 포인터 간의 관계: 만약 두 포인터가 동일한 메모리 주소를 가리키도록 설정하면, 그 이후에는 한 포인터를 통해 값을 변경할 때 다른 포인터를 통해 동일한 값을 확인할 수 있습니다.
int a = 5;
int* p1 = &a;
int* p2 = p1;  // p2는 p1과 동일한 주소를 가리킴
*p2 = 20;      // a의 값이 20으로 변경됨

이 예시에서, p는 x의 메모리 주소를 가리키고, *p를 사용하여 x의 값을 직접 변경할 수 있습니다. 이는 변수의 값을 복사하는 것이 아니라, 변수의 원래 메모리 위치에 저장된 값을 변경하는 것입니다.