Rust 메모리 안전성 완벽 가이드 | 소유권·Borrow checker·수명·unsafe 실전

Rust 메모리 안전성 완벽 가이드 | 소유권·Borrow checker·수명·unsafe 실전

이 글의 핵심

Rust 메모리 안전성 완벽 가이드에 대한 실전 가이드입니다. 소유권·Borrow checker·수명·unsafe 실전 등을 예제와 함께 상세히 설명합니다.

들어가며: “Rust는 왜 메모리 버그가 없을까?”

왜 Rust 메모리 안전성인가

C/C++에서 use-after-free, 댕글링 포인터, Data Race는 런타임에만 드러나고, 컴파일러는 대부분 통과시킵니다. Rust는 소유권·Borrow checker·수명으로 이런 버그를 컴파일 단계에서 차단합니다. 이 글은 실무에서 겪는 문제 시나리오, 완전한 Rust 메모리 안전 예제(소유권·빌림·수명·unsafe), 자주 하는 실수, 베스트 프랙티스, 프로덕션 패턴까지 한 번에 다룹니다.

이 글에서 다루는 것:

  • 문제 시나리오: 실무에서 겪는 메모리 버그와 Rust가 막는 방식
  • 소유권·이동: 누가 메모리를 해제하는지 타입으로 보장
  • Borrow checker: 불변/가변 빌림 규칙, 이중 빌림 차단
  • 수명(lifetime): 댕글링 참조 컴파일 타임 차단
  • unsafe: FFI·성능 최적화 시 안전한 사용법
  • 자주 하는 실수: borrow checker 에러 해결 패턴
  • 베스트 프랙티스: clone 최소화, 참조 활용, unsafe 격리
  • 프로덕션 패턴: Arc·Mutex·채널, 수명 명시 패턴

관련 글: C++ vs Rust, Rust vs C++ 메모리.

개념을 잡는 비유

이 글의 주제는 여러 부품이 맞물리는 시스템으로 보시면 이해가 빠릅니다. 한 레이어(저장·네트워크·관측)의 선택이 옆 레이어에도 영향을 주므로, 본문에서는 트레이드오프를 숫자와 패턴으로 정리합니다.


목차

  1. 문제 시나리오: 실무에서 겪는 메모리 버그
  2. 소유권과 이동
  3. Borrow checker 완전 가이드
  4. 수명(lifetime) 완전 가이드
  5. unsafe와 메모리 안전 경계
  6. 동시성: Send·Sync·Arc·Mutex
  7. 자주 하는 실수와 해결법
  8. 베스트 프랙티스
  9. 프로덕션 패턴
  10. 정리 및 체크리스트

1. 문제 시나리오: 실무에서 겪는 메모리 버그

시나리오 1: “버퍼를 넘긴 뒤 다시 썼어요”

"네트워크 패킷을 파싱한 Vec<u8>를 다른 스레드로 넘겼는데,
원본을 다시 쓰다가 크래시가 났어요."

C++에서: std::move 후 원본은 “유효하지만 unspecified”. 컴파일 통과, 런타임 UB.

Rust에서: 이동 후 원본은 사용 불가. 같은 코드를 쓰면 컴파일 에러가 납니다.

fn on_packet_received(packet: Vec<u8>) {
    let handle = std::thread::spawn(move || {
        parse_packet(&packet);
    });
    handle.join().unwrap();
    // println!("{}", packet.len());  // 컴파일 에러: use of moved value: `packet`
}

시나리오 2: “로컬 포인터를 반환했어요”

"함수에서 문자열을 만들고 참조를 반환했는데,
호출자가 사용할 때 이미 해제된 메모리를 가리키고 있어요."

Rust에서: 수명 검사로 컴파일 에러가 납니다.

// fn get_name() -> &str {
//     let s = String::from("hello");
//     &s  // 컴파일 에러: borrowed value does not live long enough
// }

fn get_name() -> String {
    String::from("hello")  // 소유권 반환
}

주의사항: &str을 반환하려면 호출자가 소유한 버퍼나 'static 리터럴이 필요합니다.

시나리오 3: “스레드에 Rc를 넘겼어요”

"Rc<T>를 스레드로 넘기려 했는데 Rust에서 컴파일 에러가 나요."

원인: Rc는 non-atomic이라 Send가 아닙니다. 스레드 간 전달 시 컴파일 에러.

// use std::rc::Rc;
// use std::thread;
// let r = Rc::new(42);
// thread::spawn(move || { println!("{}", r); });  // 에러: Rc는 Send가 아님

use std::sync::Arc;
use std::thread;

fn main() {
    let r = Arc::new(42);
    let r_clone = r.clone();
    let handle = thread::spawn(move || {
        println!("{}", r_clone);  // OK: Arc는 Send + Sync
    });
    handle.join().unwrap();
}

시나리오 4: “이터레이터 무효화”

"Vec를 순회하면서 push를 했더니, 이터레이터가 무효화되어 크래시가 났어요."

Rust에서: Borrow checker가 반복 중 가변 빌림을 막습니다.

let mut v = vec![1, 2, 3];
for x in &v {
    if *x == 2 {
        // v.push(4);  // 컴파일 에러: cannot borrow `v` as mutable
    }
}

시나리오 5: “순환 참조로 메모리 누수”

"Rc로 A와 B가 서로를 가리키게 했는데, 참조 카운트가 0이 안 되어
메모리가 해제되지 않아요."

해결: Rc::downgradeWeak를 만들어 순환을 끊습니다. prevWeak로 두면 참조 카운트에 포함되지 않아 순환이 끊깁니다.

use std::rc::{Rc, Weak};
use std::cell::RefCell;

struct Node {
    value: i32,
    next: Option<Rc<RefCell<Node>>>,
    prev: Option<Weak<RefCell<Node>>>,  // Weak로 순환 참조 방지
}

시나리오 6: “Mutex 없이 공유 변수”

"여러 스레드가 같은 카운터를 증가시키는데, mutex 없이 했더니
결과가 매번 달라요."

Rust에서: Send·Sync로 “스레드 안전하지 않은 공유”를 컴파일 단계에서 차단합니다.

문제 시나리오 다이어그램

flowchart TB
    subgraph Problems["실무 메모리 버그"]
        P1[이동 후 사용]
        P2[댕글링 참조]
        P3[이터레이터 무효화]
        P4[Data Race]
        P5[순환 참조]
    end
    subgraph Rust["Rust 해결"]
        R1[소유권·이동]
        R2[수명 검사]
        R3[Borrow checker]
        R4[Send/Sync]
        R5[Weak]
    end
    P1 --> R1
    P2 --> R2
    P3 --> R3
    P4 --> R4
    P5 --> R5

2. 소유권과 이동

소유권 규칙

  1. 각 값은 하나의 소유자만 가집니다.
  2. 소유자가 스코프를 벗어나면 값이 drop됩니다.
  3. 이동이 기본: 대입·함수 인자·반환 시 소유권이 이동합니다.

완전한 소유권 예제

fn main() {
    // 소유권: s가 "hello"를 소유
    let s = String::from("hello");

    // 이동: s의 소유권이 t로 이동, s는 사용 불가
    let t = s;
    // println!("{}", s);  // 컴파일 에러: use of moved value: `s`
    println!("{}", t);  // OK

    // Copy 타입은 복사 (이동 아님)
    let x = 42;
    let y = x;
    println!("{} {}", x, y);  // OK: i32는 Copy
}

코드 설명:

  • String소유 타입: 힙 메모리를 소유하고, drop 시 해제합니다.
  • let t = s;에서 s의 소유권이 t로 이동합니다. s는 더 이상 유효하지 않습니다.
  • i32Copy: 복사가 발생하고, xy 모두 사용 가능합니다.

소유권과 함수

fn take_ownership(s: String) {
    println!("{}", s);
}  // s가 drop됨

fn main() {
    let s = String::from("hello");
    take_ownership(s);
    // println!("{}", s);  // 컴파일 에러: s는 이미 이동됨
}

소유권 반환

fn create_string() -> String {
    let s = String::from("hello");
    s  // 소유권 반환 (이동)
}

fn main() {
    let s = create_string();
    println!("{}", s);  // OK
}

Box: 힙 할당 소유권

fn main() {
    let b = Box::new(42);
    println!("{}", *b);
    // b가 스코프를 벗어나면 Box가 drop되고 힙 메모리 해제
}

소유권 다이어그램

flowchart LR
    subgraph Before["이동 전"]
        S1[String s]
        S2["hello"]
        S1 --> S2
    end

    subgraph After["이동 후"]
        T1[String t]
        T2["hello"]
        T1 --> T2
        S3[s: 사용 불가]
    end

3. Borrow checker 완전 가이드

빌림 규칙

  1. 불변 참조 &T: 여러 개 동시에 가능.
  2. 가변 참조 &mut T: 동시에 하나만. 불변 참조와도 동시에 불가.
  3. 참조의 수명은 원본보다 길 수 없음.

불변 빌림 예제

fn main() {
    let s = String::from("hello");
    let r1 = &s;
    let r2 = &s;
    println!("{} {}", r1, r2);  // OK: 여러 불변 참조
}

가변 빌림: 동시에 하나만

fn main() {
    let mut v = vec![1, 2, 3];
    let r1 = &mut v;
    // let r2 = &mut v;  // 컴파일 에러: cannot borrow `v` as mutable more than once
    r1.push(4);
}

불변과 가변 동시 빌림 불가

fn main() {
    let mut v = vec![1, 2, 3];
    let r = &v;
    // v.push(4);  // 컴파일 에러: cannot borrow `v` as mutable while it is borrowed as immutable
    println!("{}", r[0]);
}

스코프로 빌림 해제

fn main() {
    let mut v = vec![1, 2, 3];
    {
        let r = &v[0];
        println!("{}", r);
    }  // r의 스코프 종료
    v.push(4);  // OK: r이 더 이상 v를 빌리지 않음
}

이터레이터와 빌림

fn main() {
    let mut v = vec![1, 2, 3];

    // 반복 중 가변 수정 시도 → 컴파일 에러
    // for x in &v {
    //     if *x == 2 {
    //         v.push(4);  // 에러
    //     }
    // }

    // 해결 1: 인덱스 루프
    let mut i = 0;
    while i < v.len() {
        if v[i] == 2 {
            v.push(4);
        }
        i += 1;
    }

    // 해결 2: collect로 필터링 후 수정
    let to_add: Vec<_> = v.iter().filter(|&&x| x == 2).cloned().collect();
    for x in to_add {
        v.push(x + 2);
    }
}

NLL (Non-Lexical Lifetimes)

Rust 2018부터 NLL로 “더 이상 사용하지 않는 참조”는 빌림이 해제된 것으로 봅니다.

fn main() {
    let mut v = vec![1, 2, 3];
    let r = &v[0];
    println!("{}", r);  // r 사용 완료
    v.push(4);  // OK: r을 더 이상 쓰지 않으므로 빌림 해제
}

Borrow checker 다이어그램

flowchart TB
    subgraph Rules["빌림 규칙"]
        R1["불변 &T: 여러 개 OK"]
        R2["가변 &mut T: 하나만"]
        R3["&T와 &mut T 동시 불가"]
    end
    subgraph Violation["위반 시"]
        V1[컴파일 에러]
    end
    R1 --> V1
    R2 --> V1
    R3 --> V1

4. 수명(lifetime) 완전 가이드

수명이란

참조가 유효한 범위. “이 참조가 가리키는 값보다 참조가 더 오래 살 수 없다”를 컴파일러가 검사합니다.

댕글링 방지

// fn dangling() -> &str {
//     let s = String::from("hello");
//     &s  // 컴파일 에러: `s` does not live long enough
// }

fn valid() -> String {
    let s = String::from("hello");
    s  // 소유권 반환
}

수명 파라미터

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

fn main() {
    let s1 = String::from("short");
    let s2 = String::from("longer");
    let result = longest(s1.as_str(), s2.as_str());
    println!("{}", result);  // "longer"
}

코드 설명:

  • 'a: 수명 파라미터. “반환 참조는 x, y 중 더 짧은 수명을 가짐”을 의미합니다.
  • 컴파일러가 댕글링 가능성을 검사합니다.

구조체와 수명

struct Excerpt<'a> {
    text: &'a str,
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().expect("Could not find '.'");
    let excerpt = Excerpt { text: first_sentence };
    println!("{}", excerpt.text);
}

수명 생략 규칙

컴파일러가 추론할 수 있으면 생략 가능합니다.

// 수명 생략 전
fn first_word<'a>(s: &'a str) -> &'a str {
    s.split_whitespace().next().unwrap_or("")
}

// 수명 생략 후 (동일)
fn first_word(s: &str) -> &str {
    s.split_whitespace().next().unwrap_or("")
}

‘static 수명

fn get_static() -> &'static str {
    "hello"  // 문자열 리터럴은 'static
}

수명 검사 시퀀스

sequenceDiagram
    participant Caller
    participant fn_longest
    participant x
    participant y

    Caller->>fn_longest: longest(&s1, &s2)
    fn_longest->>x: &'a str
    fn_longest->>y: &'a str
    Note over fn_longest: 반환 참조 수명 ≤ min(x, y)
    fn_longest-->>Caller: &'a str
    Note over Caller: Caller 스코프 내에서만 유효

5. unsafe와 메모리 안전 경계

unsafe가 필요한 경우

  1. Raw 포인터 역참조
  2. FFI (C 라이브러리 호출)
  3. 안전하지 않은 함수 호출
  4. 가변 정적 변수 접근
  5. union 필드 접근

unsafe 블록 최소화

// 안전한 래퍼로 unsafe 격리
fn get_ptr_address<T>(r: &T) -> usize {
    r as *const T as usize
}

// raw 포인터 역참조 (unsafe 필요)
unsafe fn dereference_raw(ptr: *const i32) -> i32 {
    *ptr
}

FFI 예제

// C 라이브러리와 연동
extern "C" {
    fn abs(input: i32) -> i32;
}

fn main() {
    let x = -42;
    let result = unsafe { abs(x) };
    println!("{}", result);  // 42
}

안전한 래퍼 패턴

// unsafe를 최소한의 공간에 격리
fn safe_wrapper(ptr: *const i32) -> Option<i32> {
    if ptr.is_null() {
        return None;
    }
    Some(unsafe { *ptr })
}

unsafe 체크리스트

  • unsafe 블록을 최대한 좁게
  • 불변 조건 문서화
  • 테스트로 경계 검증
  • 안전한 API로 감싸서 노출

6. 동시성: Send·Sync·Arc·Mutex

Send와 Sync

  • Send: 스레드 간 이동이 안전한 타입
  • Sync: 스레드 간 공유 참조 &T가 안전한 타입

Rc vs Arc

타입SendSync용도
Rc<T>XX단일 스레드 공유
Arc<T>OO멀티스레드 공유

Arc + Mutex 패턴

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..10 {
        let counter = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            let mut num = counter.lock().unwrap();
            *num += 1;
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }
    println!("{}", *counter.lock().unwrap());  // 10
}

채널: 메시지 전달

use std::sync::mpsc;
use std::thread;

fn main() {
    let (tx, rx) = mpsc::channel();

    thread::spawn(move || {
        tx.send(42).unwrap();
    });

    let received = rx.recv().unwrap();
    println!("{}", received);  // 42
}

Atomic

use std::sync::atomic::{AtomicU32, Ordering};
use std::thread;

fn main() {
    let counter = AtomicU32::new(0);
    let mut handles = vec![];

    for _ in 0..10 {
        let counter = &counter;
        let handle = thread::spawn(move || {
            counter.fetch_add(1, Ordering::SeqCst);
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }
    println!("{}", counter.load(Ordering::SeqCst));  // 10
}

7. 자주 하는 실수와 해결법

에러 1: “use of moved value”

원인: 이동 후 원본 사용.

// ❌ 잘못된 코드
let s = String::from("hello");
let t = s;
println!("{}", s);  // 에러

// ✅ 해결 1: clone
let s = String::from("hello");
let t = s.clone();
println!("{}", s);

// ✅ 해결 2: 참조로 빌림
let s = String::from("hello");
let t = &s;
println!("{}", s);

에러 2: “borrowed value does not live long enough”

원인: 수명이 맞지 않는 참조 반환.

// ❌ 잘못된 코드
// fn get_name() -> &str {
//     let s = String::from("hello");
//     &s
// }

// ✅ 해결: 소유권 반환
fn get_name() -> String {
    String::from("hello")
}

에러 3: “cannot borrow as mutable”

원인: 이미 불변 빌림이 있는 상태에서 가변 빌림.

// ❌ 잘못된 코드
let mut v = vec![1, 2, 3];
let r = &v[0];
v.push(4);  // 에러

// ✅ 해결: 스코프 분리 또는 값 복사
let mut v = vec![1, 2, 3];
let x = v[0];
v.push(4);

에러 4: “cannot be sent between threads safely”

원인: Rc 등 non-Send 타입을 스레드로 전달.

// ❌ 잘못된 코드
// let r = Rc::new(42);
// thread::spawn(move || { println!("{}", r); });

// ✅ 해결: Arc 사용
let r = Arc::new(42);
let r_clone = r.clone();
thread::spawn(move || {
    println!("{}", r_clone);
});

에러 5: “temporary value dropped while borrowed”

원인: 임시 값에 대한 참조를 오래 유지.

// ❌ 잘못된 코드
// let r: &str = format!("hello").as_str();

// ✅ 해결: 소유권 유지
let s = format!("hello");
let r: &str = &s;

에러 6: “cannot infer an appropriate lifetime”

원인: 수명 추론 실패.

// ❌ 잘못된 코드
// fn first_word(s: &str) -> &str {
//     s.split_whitespace().next().unwrap_or("")
// }
// 에러: 반환 타입에 수명 필요

// ✅ 해결: 수명 명시
fn first_word<'a>(s: &'a str) -> &'a str {
    s.split_whitespace().next().unwrap_or("")
}

에러 7: “already borrowed”

원인: RefCell에서 이미 빌림 중인데 다시 빌림 시도.

use std::cell::RefCell;

// ❌ 런타임 패닉
// let r = RefCell::new(42);
// let a = r.borrow();
// let b = r.borrow_mut();  // 패닉: already borrowed

// ✅ 해결: 스코프 분리
let r = RefCell::new(42);
{
    let a = r.borrow();
    println!("{}", *a);
}
let b = r.borrow_mut();  // OK

에러 해결 플로우

flowchart TB
    E1[use of moved value] --> S1[clone 또는 참조]
    E2[borrowed value does not live long enough] --> S2[소유권 반환 또는 수명 명시]
    E3[cannot borrow as mutable] --> S3[스코프 분리]
    E4[cannot be sent between threads] --> S4[Arc 사용]
    E5[temporary dropped] --> S5[변수에 바인딩]

8. 베스트 프랙티스

1. clone() 최소화

// ❌ 불필요한 clone
fn process(s: String) {
    let s2 = s.clone();
    do_something(&s2);
}

// ✅ 참조로 빌림
fn process(s: &str) {
    do_something(s);
}

2. Result·Option 처리

// ✅ ? 연산자로 에러 전파
fn read_file(path: &str) -> Result<String, std::io::Error> {
    std::fs::read_to_string(path)
}

fn main() -> Result<(), std::io::Error> {
    let content = read_file("file.txt")?;
    println!("{}", content);
    Ok(())
}

3. Rc vs Arc 선택

// 단일 스레드: Rc (가벼움)
use std::rc::Rc;
let r = Rc::new(42);

// 멀티스레드: Arc
use std::sync::Arc;
let a = Arc::new(42);

4. unsafe 격리

// ✅ 안전한 API로 감싸기
pub fn safe_slice_from_raw(ptr: *const u8, len: usize) -> Option<&[u8]> {
    if ptr.is_null() || len == 0 {
        return None;
    }
    Some(unsafe { std::slice::from_raw_parts(ptr, len) })
}

5. Clippy 활용

cargo clippy

6. 문서화

/// # Safety
/// `ptr` must be a valid, non-null pointer to a valid `T`.
unsafe fn from_raw(ptr: *const T) -> &T {
    &*ptr
}

9. 프로덕션 패턴

패턴 1: 수명 명시

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

패턴 2: Arc<Mutex> 공유 상태

use std::sync::{Arc, Mutex};

struct SharedState {
    counter: Arc<Mutex<u32>>,
}

impl SharedState {
    fn increment(&self) {
        *self.counter.lock().unwrap() += 1;
    }
}

패턴 3: 채널 기반 파이프라인

use std::sync::mpsc;
use std::thread;

fn main() {
    let (tx, rx) = mpsc::channel();

    thread::spawn(move || {
        for i in 0..10 {
            tx.send(i).unwrap();
        }
    });

    for received in rx {
        println!("{}", received);
    }
}

패턴 4: Builder에서 소유권 이동

struct Config {
    name: String,
}

impl Config {
    fn new(name: String) -> Self {
        Config { name }
    }

    fn build(self) -> Application {
        Application { config: self }
    }
}

패턴 5: Option·Result 조합

fn find_and_parse(v: &[String]) -> Option<i32> {
    v.iter()
        .find(|s| s.starts_with("num="))
        .and_then(|s| s.strip_prefix("num="))
        .and_then(|s| s.parse().ok())
}

패턴 6: Cow (Clone-on-Write)

use std::borrow::Cow;

fn process(input: Cow<str>) -> String {
    if input.contains("bad") {
        input.replace("bad", "good").into()
    } else {
        input.into_owned()
    }
}

구현 체크리스트

  • clone() 최소화, 참조 활용
  • Result·Option 처리 (? 또는 match)
  • 스레드 사용 시 Arc, Rc 금지
  • unsafe 블록 최소화, 문서화
  • cargo clippy 정기 실행
  • 수명 에러 시 소유권 반환 검토

10. 정리 및 체크리스트

Rust 메모리 안전성 요약

영역Rust 보장
이동 후 사용컴파일 에러
댕글링 참조수명으로 컴파일 에러
이터레이터 무효화Borrow checker로 컴파일 에러
Data RaceSend/Sync로 컴파일 에러
null 역참조Option으로 검사
이중 해제소유권으로 불가능

학습 순서

  1. 소유권·이동 → 2. 빌림 → 3. 수명 → 4. 동시성 → 5. unsafe

참고 자료


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • C++ vs Rust 완전 비교 | 소유권·메모리 안전성·에러 처리·동시성·성능 실전 가이드
  • C++ 스마트 포인터 | 3일 동안 찾지 못한 순환 참조 버그 해결법
  • Rust vs C++ 메모리 안전성 | 컴파일러 오류 차이 [#47-3]

이 글에서 다루는 키워드 (관련 검색어)

Rust 메모리 안전성, 소유권, Borrow checker, 수명, lifetime, unsafe, Send Sync, Arc Mutex 등으로 검색하시면 이 글이 도움이 됩니다.

자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. Rust 프로젝트 메모리 설계, borrow checker 에러 해결, FFI·unsafe 경계 설계, 동시성 안전 코드 작성 시 참고하세요.

Q. borrow checker 에러가 너무 많아요.

A. clone()으로 우회할 수 있지만, 성능이 중요하면 참조와 수명을 익혀야 합니다. Rc·Arc로 소유권 공유를 명시하면 요구사항을 자연스럽게 만족할 수 있습니다.

Q. unsafe는 언제 써야 하나요?

A. FFI, 성능이 극히 중요한 경로, 또는 안전한 Rust로 표현 불가능한 로직에서만 사용합니다. 가능한 한 안전한 API로 감싸서 노출하세요.

Q. 선행으로 읽으면 좋은 글은?

A. C++ vs Rust, Rust vs C++ 메모리를 먼저 읽으면 소유권 개념을 대비해 이해하기 쉽습니다.

Q. 더 깊이 공부하려면?

A. The Rust Book, Rustonomicon(unsafe), Rust by Example을 참고하세요.

한 줄 요약: Rust의 소유권·Borrow checker·수명·unsafe를 이해하면 메모리 안전한 코드를 작성할 수 있습니다.

이전 글: C++ vs Rust 완전 비교

다음 글: Rust vs C++ 메모리 안전성


관련 글

  • C++ vs Rust 완전 비교 | 소유권·메모리 안전성·에러 처리·동시성·성능 실전 가이드
  • Rust vs C++ 메모리 안전성 | 컴파일러 오류 차이 [#47-3]
  • C++ vs Go | 성능·동시성·선택 가이드 완전 비교 [#47-1]
  • C++ 개발자의 뇌 구조로 이해하는 Go 언어 [#47-2]
  • C++ 함수 객체(Functor) 완벽 가이드 | operator·상태 보유