/

|

C++를 실무에서 계속 사용하고 있어도 기본 개념을 다시 보면 의외로 놓치고 있던 부분이 많다. 이번 글은 A Tour of C++ Chapter 1을 다시 공부하면서 정리한 내용 중 1.1부터 1.4.2까지를 다룬다. 단순히 문법을 나열하기보다, 실제로 공부하면서 질문했던 포인트와 임베디드 개발에서 왜 중요한지를 함께 정리했다.

1.1 Introduction — C++를 볼 때 무엇부터 이해해야 하나

첫 절은 세부 문법을 외우기 전에 C++가 어떤 언어인지 큰 그림을 잡는 부분이다. C++ 프로그램을 이해하려면 코드 표기법, 메모리와 계산 모델, 프로그램을 구성하는 기본 메커니즘을 함께 봐야 한다.

임베디드 개발에서는 “문법상 맞다”보다 “이 값이 실제로 몇 바이트인가”, “어디에 저장되는가”, “이 연산 결과의 타입은 무엇인가”가 더 중요할 때가 많다. CAN frame, register, shared memory, DMA buffer처럼 하드웨어와 가까운 데이터를 다룰 때 타입과 메모리 모델을 모르면 작은 실수가 실제 장비 오동작으로 이어질 수 있다.

1.1 핵심: C++는 문법만 배우는 것이 아니라 타입, 메모리, 연산, 프로그램 구조를 함께 이해해야 한다.

1.2 Programs — 소스 코드가 실행 파일이 되는 과정

C++는 컴파일 언어다. 우리가 작성한 .cpp 파일은 CPU가 바로 실행하는 파일이 아니다. 각 소스 파일은 컴파일러를 거쳐 오브젝트 파일이 되고, 링커가 오브젝트 파일과 라이브러리를 연결해 실행 파일을 만든다.

source1.cpp ── compile ──> source1.o
source2.cpp ── compile ──> source2.o

source1.o + source2.o + libraries
              │
             link
              ↓
          executable

컴파일 에러와 링크 에러는 왜 다른가

컴파일 단계에서는 문법과 타입, 이름 사용 등이 맞는지 확인한다. 링크 단계에서는 다른 파일에 있다고 약속한 함수나 객체의 실제 정의를 연결한다.

// main.cpp
int add(int, int);   // declaration

int main()
{
    return add(1, 2);
}

add()의 선언이 있으므로 main.cpp 자체는 컴파일될 수 있다. 하지만 실제 정의가 프로그램 어디에도 없다면 링크 단계에서 실패할 수 있다.

정적 타입 언어라는 의미

C++에서는 컴파일러가 각 표현식과 객체의 타입을 알아야 한다. 타입은 이름표가 아니라 사용할 수 있는 연산과 메모리 표현을 결정한다.

int count = 10;
double voltage = 12.5;

Hello, World에서 봐야 할 것

#include <iostream>

int main()
{
    std::cout << "Hello\n";
}

main()은 프로그램 실행의 시작점이다. std::coutstd namespace 안의 표준 출력 객체다. 최근 공부할 때도 “std namespace 안의 cout에 값을 전달한다”는 식으로 이해했다.

1.2 핵심: source → compile → object → link → executable의 흐름과 컴파일 오류/링크 오류의 차이를 이해한다.

1.3 Functions — 함수는 값을 어떻게 받는가

함수는 입력을 받고 계산한 뒤 결과를 반환하는 기본 코드 단위다. 중요한 것은 함수 호출 시 인자가 어떤 방식으로 전달되는지 이해하는 것이다.

int add5(int value)
{
    value += 5;
    return value;
}

int main()
{
    int a = 10;
    int b = add5(a);
}

a는 10이고 매개변수 value는 그 값을 복사해서 받는다. 따라서 함수 내부에서 value가 15가 되어도 원본 a는 그대로 10이고, 반환값을 받은 b만 15가 된다.

공부하면서 확인한 포인트:
함수에 값을 넘기면 원본 변수도 바뀌는가? → 값 전달(pass-by-value)이라면 원본은 바뀌지 않는다. 함수는 복사본을 수정한다.

이 개념을 정확히 이해하면 이후 reference와 pointer가 왜 원본 값을 바꿀 수 있는지 비교해서 이해하기 쉬워진다.

1.3 핵심: 기본적인 값 전달에서는 함수가 원본이 아니라 복사본을 받는다.

1.4 Types, Variables, and Arithmetic — 타입과 변수

변수는 값을 저장하는 이름 있는 객체이고, 타입은 어떤 값을 표현할 수 있으며 어떤 연산이 가능한지를 결정한다.

기본 타입

bool enabled = true;
char ch = 'A';
int count = 10;
double ratio = 3.5;

int는 정수, double은 부동소수점 값을 표현한다. 하지만 임베디드 코드에서는 단순히 “정수”라는 정보만으로 부족한 경우가 많다.

std::uint16_t를 왜 사용하는가

#include <cstdint>
std::uint16_t raw_voltage = 1000;

최근 공부하면서 std::uint16_t를 쓰면 “정확히 2바이트 크기의 값이라는 것을 명확하게 알 수 있다”고 정리했다. 더 정확히는 정확히 16비트의 부호 없는 정수 타입이라는 뜻이다. CAN signal, protocol field, register처럼 bit width가 중요한 곳에서 이 명확성이 큰 장점이다.

auto는 타입이 없는 것이 아니다

auto a = 100;   // int
auto b = 3.5;   // double
auto c = 3.5f;  // float

auto는 타입을 없애는 것이 아니라 초기값을 보고 컴파일러가 타입을 추론하는 기능이다. 일반적인 정수 리터럴 100은 기본적으로 int다.

1.4.1 Arithmetic — 산술 연산에서 타입이 결과를 바꾼다

int a = 7 / 2;                         // 3
double b = 7 / 2;                      // 3.0
double c = static_cast<double>(7) / 2; // 3.5

두 피연산자가 모두 정수이면 먼저 정수 나눗셈이 수행된다. 따라서 7 / 2는 3이다. 그 결과를 double 변수에 넣어도 이미 계산 결과는 3이므로 3.0이 된다. 반면 연산 전에 한쪽을 double로 바꾸면 실수 나눗셈이 되어 3.5가 나온다.

공부 문제에서 확인한 답:
정수 연산 결과는 3, 명시적으로 double로 캐스팅한 연산은 3.5.
임베디드 실무 포인트: ADC raw value, 전압, 전력, 퍼센트, scaling 계산에서 정수 나눗셈을 잘못 사용하면 컴파일 에러 없이 값만 틀릴 수 있다.

명시적 캐스팅을 쓰는 이유

static_cast는 개발자가 타입 변환을 의도했다는 사실을 코드에 남긴다. 정수/실수가 섞이는 연산이나 폭이 다른 정수 사이 변환에서 특히 의도가 명확해진다.

1.4.2 Initialization — 변수는 어떻게 첫 값을 갖는가

초기화는 객체가 생성될 때 최초 값을 주는 과정이다. 이미 생성된 객체의 값을 바꾸는 대입과 구분해야 한다.

int a{10};  // initialization
a = 20;    // assignment

중괄호 초기화가 유용한 이유

int a = 3.9; // 3으로 변환될 수 있음
int b{3.9};  // narrowing 때문에 컴파일 오류

중괄호 초기화는 값 손실이 생길 수 있는 narrowing conversion을 더 엄격하게 막아준다. 타입 실수를 빠르게 발견하는 데 도움이 된다.

초기화 시점에 타입을 함께 생각하자

auto x = 100;에서 xint로 추론된다. 변수 선언을 볼 때는 이름만 보는 것이 아니라 “어떤 타입으로 어떤 값으로 초기화되는가”를 함께 읽는 습관이 중요하다.

1.4~1.4.2 핵심: 타입은 메모리와 연산 의미를 정하고, 산술 연산은 피연산자 타입에 따라 결과가 달라지며, 초기화 방식은 잘못된 변환을 조기에 잡는 데 도움을 준다.

마무리

1.1~1.4.2를 다시 공부하면서 C++ 기본 문법이 실제 임베디드 동작과 멀리 떨어져 있지 않다는 점을 다시 느꼈다. 컴파일과 링크, 값 전달, 정수 폭, 산술 변환, 초기화는 드라이버·통신·제어 코드에서 계속 만나는 개념이다.

1.1 Introduction

C++ should be understood as more than syntax. Its type system, memory model, computation rules, and code organization all matter, especially in embedded software where data often maps directly to registers, protocol fields, and buffers.

1.2 Programs

C++ is compiled. Source files become object files, and the linker combines those objects and libraries into an executable. Compiler errors and linker errors therefore represent different stages of failure.

source.cpp -> compiler -> object file -> linker -> executable

C++ is statically typed, so the compiler knows the type of each entity at its point of use.

1.3 Functions

With pass-by-value, a function receives a copy. Changing the parameter does not change the original variable. This becomes the baseline for understanding pointers and references later.

1.4 Types, Variables, and Arithmetic

Types control representation and valid operations. In embedded code, fixed-width types such as std::uint16_t make protocol and register widths explicit.

1.4.1 Arithmetic

int a = 7 / 2;                         // 3
double b = 7 / 2;                      // 3.0
double c = static_cast<double>(7) / 2; // 3.5

Integer division occurs before assignment. Converting an operand before division changes the arithmetic itself.

1.4.2 Initialization

Initialization gives an object its first value. Brace initialization can reject narrowing conversions and catch silent data loss early.

int a = 3.9; // may become 3
int b{3.9};  // compile error

Takeaway

Compilation, value passing, exact integer widths, arithmetic rules, and initialization all directly affect embedded C++ code.

+ Recent posts