변수 선언과 초기화
변수를 선언하고, 복사 초기화와 직접 초기화, 중괄호 초기화의 차이와 중괄호가 막아 주는 손실 변환을 익히며, 초기화와 대입을 구별하고 초기화하지 않은 변수를 읽으면 안 되는 이유와, 이름을 짓는 규칙과 const로 바꿀 수 없는 변수를 만드는 방법을 다룹니다.
앞 글의 타입으로 실제 값을 담으려면 변수(variable)가 필요합니다. 변수는 특정 타입의 값을 담는 메모리 공간인 객체(object)에 이름을 붙인 것으로, 프로그램은 그 이름으로 값을 읽고 바꿉니다. 게임의 점수처럼 실행하는 동안 바뀌는 값을 변수에 담습니다.
이 글은 변수를 선언하는 방법과 처음 값을 정하는 여러 초기화 방법을 익히고, 초기화와 나중에 값을 바꾸는 대입이 어떻게 다른지 봅니다. 이어서 이름을 짓는 규칙과, 한 번 정하면 바꿀 수 없는 const 변수를 다룹니다.
변수의 선언
변수를 쓰려면 먼저 어떤 타입의 값을 담을지와 어떤 이름으로 부를지를 컴파일러에게 알립니다. 이것을 변수 선언(declaration)이라고 하며, 타입 이름;의 모양으로 적습니다.
int age;
double weight;
char initial;
bool isStudent;함수 안에서 이렇게 선언한 기본 타입의 변수는 객체는 만들어지지만 값이 정해지지 않은 상태입니다. 흔히 쓰레기 값이 들어 있다고 말하지만, 그 값을 읽는 것은 C++20에서 정의되지 않은 동작이어서 아무 값이나 읽히는 정도가 아니라 프로그램 전체의 결과를 믿을 수 없게 됩니다. C++26부터는 이것을 잘못된 동작(erroneous behavior)이라는 새 분류로 바꾸어 컴파일러가 진단하기 쉽게 했지만, 어느 쪽이든 값을 정하기 전에 읽어서는 안 됩니다.
더 깊이: 값이 정해지는 변수와 정해지지 않는 변수
값 없이 선언해도 0으로 시작하는 변수가 있습니다. 함수 밖의 전역 변수나 static 지역 변수처럼 정적 저장 기간을 가진 변수는, 다른 초기화보다 먼저 0으로 채우는 영 초기화(zero initialization)를 거칩니다. 그래서 int n;이 전역에 있으면 0이고, 값이 정해지지 않는 것은 함수 안의 지역 변수일 때입니다. new int로 만든 동적 객체도 값이 정해지지 않습니다.
C++26은 함수 안 지역 변수의 이 상태를 바꿉니다. 초깃값 없이 만든 자동 저장 기간 객체의 바이트는 구현이 프로그램의 상태와 상관없이 정한 잘못된 값(erroneous value)을 가지며, 그 값을 읽는 것은 정의되지 않은 동작이 아니라 잘못된 동작이 됩니다. 성능 때문에 값을 비워 두어야 하는 변수에는 [[indeterminate]] 속성을 붙여 예전 규칙을 따르게 할 수 있고, 동적 저장 기간 객체는 C++26에서도 값이 정해지지 않는 쪽에 남습니다.
컴파일러는 이 변화보다 먼저 비슷한 옵션을 제공했습니다. GCC 12에서 들어온 -ftrivial-auto-var-init은 자동 변수를 0으로 채우는 zero와, 논리 오류가 나중에 충돌로 드러나기 쉽도록 0xFE 바이트로 채우는 pattern을 고를 수 있습니다. GCC 문서에 따르면 이 옵션의 기본값은 아무것도 채우지 않는 uninitialized이지만, C++26에서는 옵션을 주지 않으면 자동 변수를 0으로 초기화하고 패딩 비트는 채우지 않습니다. GCC 16의 변경 사항에는 이 C++26 규칙(P2795R5)을 구현했다고 적혀 있습니다.
초기화와 대입
기본 타입 변수의 초기화와 대입
score=10으로 saved를 초기화한 뒤 score=20을 대입해도 saved=10이라는 실제 값 변화이다.
C++ · OBJECT INITIALIZATION
초기화는 생성 시, 대입은 생성 후 값을 정한다
각 표현은 독립된 예시입니다. 함수 안의 자동 지역 기본 타입 변수를 기준으로 비교합니다.
int x; · 값 없이 선언
함수 안의 자동 지역 int 객체는 생기지만, 사용할 값으로 초기화되지 않았습니다. 먼저 값을 저장한 뒤 읽습니다.
int x{3}; · 생성 시 초기화
객체를 만들면서 값 3으로 초기화합니다. 이후 읽을 수 있습니다.
x = 3; · 기존 객체에 대입
이미 존재하는 변경 가능한 객체에 3을 저장합니다. 객체 생성과 구분합니다.
const int x{3}; · 변경 금지
생성 시 3으로 초기화합니다. 이후 x = 4;처럼 이 객체에 대입할 수 없습니다.
int x{3.5}; · narrowing 오류
리스트 초기화에 허용되지 않는 부동 소수점→정수 축소 변환입니다. 프로그램은 ill-formed이며 진단이 필요합니다.
int x{}; · 0으로 초기화
값 초기화로 0을 얻습니다. 위의 값 없이 선언한 경우와 다릅니다.
저장 기간과 타입에 따라 초기화 규칙은 달라집니다. 먼저 유효한 값을 만든 뒤 읽고, 중괄호 초기화의 narrowing 경계를 확인합니다.
이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
| 범위 | 원문 |
|---|---|
| 관계·조건 설명 | score 10의 값을 saved 10에 복사해 초기화한다. score에 20을 대입해도 saved 10은 그대로다. 세 화살표는 초깃값 복사와 이후 상태 추적을 나타낸다. |
| 관계·조건 설명 | 초기화 때 score 10의 값을 saved 10에 복사한다. score에 20을 대입하면 score만 20이 되고 saved는 10을 유지한다. 실제 주소나 두 이름의 참조 공유를 나타내지 않는다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 초기화 score의 값 10으로 독립 객체 saved를 초기화한다. |
|
|
| 원본 재대입 score에 20을 대입한다. |
|
|
기본 타입 변수의 초기화와 대입
score=10으로 saved를 초기화한 뒤 score=20을 대입해도 saved=10이라는 실제 값 변화이다.
C++ · OBJECT INITIALIZATION
초기화는 생성 시, 대입은 생성 후 값을 정한다
각 표현은 독립된 예시입니다. 함수 안의 자동 지역 기본 타입 변수를 기준으로 비교합니다.
int x; · 값 없이 선언
함수 안의 자동 지역 int 객체는 생기지만, 사용할 값으로 초기화되지 않았습니다. 먼저 값을 저장한 뒤 읽습니다.
int x{3}; · 생성 시 초기화
객체를 만들면서 값 3으로 초기화합니다. 이후 읽을 수 있습니다.
x = 3; · 기존 객체에 대입
이미 존재하는 변경 가능한 객체에 3을 저장합니다. 객체 생성과 구분합니다.
const int x{3}; · 변경 금지
생성 시 3으로 초기화합니다. 이후 x = 4;처럼 이 객체에 대입할 수 없습니다.
int x{3.5}; · narrowing 오류
리스트 초기화에 허용되지 않는 부동 소수점→정수 축소 변환입니다. 프로그램은 ill-formed이며 진단이 필요합니다.
int x{}; · 0으로 초기화
값 초기화로 0을 얻습니다. 위의 값 없이 선언한 경우와 다릅니다.
저장 기간과 타입에 따라 초기화 규칙은 달라집니다. 먼저 유효한 값을 만든 뒤 읽고, 중괄호 초기화의 narrowing 경계를 확인합니다.
이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
| 범위 | 원문 |
|---|---|
| 관계·조건 설명 | score 10의 값을 saved 10에 복사해 초기화한다. score에 20을 대입해도 saved 10은 그대로다. 세 화살표는 초깃값 복사와 이후 상태 추적을 나타낸다. |
| 관계·조건 설명 | 초기화 때 score 10의 값을 saved 10에 복사한다. score에 20을 대입하면 score만 20이 되고 saved는 10을 유지한다. 실제 주소나 두 이름의 참조 공유를 나타내지 않는다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 초기화 score의 값 10으로 독립 객체 saved를 초기화한다. |
|
|
| 원본 재대입 score에 20을 대입한다. |
|
|
그림처럼 초기화(initialization)는 객체를 만들면서 처음 값을 정하는 것이고, 대입(assignment)은 이미 있는 객체에 새 값을 쓰는 것입니다. int x{3};은 x를 만들면서 3으로 초기화하고, int x; 뒤의 x = 3;은 값이 없던 x에 3을 대입합니다. 기본 타입에서는 결과가 비슷해 보여도, const 변수나 뒤에서 배울 참조처럼 대입할 수 없고 초기화만 할 수 있는 것이 있어서 둘을 구별해야 합니다.
#include <iostream>
int main() {
int score{10};
int saved{score};
score = 20;
std::cout << score << ' ' << saved << '\n';
}20 10int saved{score};는 그 순간의 score 값 10으로 새 객체 saved를 초기화합니다. 그 뒤의 score = 20;은 score만 바꾸므로 saved는 10 그대로입니다. 두 이름은 서로 다른 두 int 객체를 가리키며, 한 객체에 이름을 둘 붙이는 뒤의 참조와는 다릅니다.
| 실행한 줄 | score | saved | 한 일 |
|---|---|---|---|
int score{10}; | 10 | 없음 | 만들면서 초기화 |
int saved{score}; | 10 | 10 | 그때의 값을 복사해 초기화 |
score = 20; | 20 | 10 | score에만 대입 |
초기화의 세 가지 모양
C++에는 초기화를 적는 모양이 여러 가지 있습니다. 아래 줄들은 등호, 괄호, 중괄호 가운데 하나로 처음 값을 정하는데, 어느 모양이든 변수를 만드는 순간 값이 들어간다는 점은 같습니다. 모양에 따라 달라지는 것은 어떤 변환을 허용하느냐이며, 이 차이는 바로 뒤에서 봅니다.
int score = 100;
double price(99.99);
int level{10};
char grade{'A'};
int zero{};
int x = 10, y = 20, z = 30;int score = 100;은 복사 초기화(copy initialization)로, 등호가 있지만 대입이 아니라 새 객체의 초기화입니다. double price(99.99);는 괄호를 쓰는 직접 초기화(direct initialization)이고, int level{10};은 C++11에 들어온 중괄호 초기화(brace initialization)입니다. 빈 중괄호 int zero{};는 값 초기화(value initialization)로, 기본 타입이면 0이나 0.0, false가 됩니다. 같은 타입의 변수는 쉼표로 이어 한 줄에 선언할 수도 있습니다.
중괄호 초기화는 값을 잃는 변환, 즉 축소 변환(narrowing conversion)을 오류로 막아 준다는 점에서 다릅니다.
int a = 3.7;
int b(3.7);
int c{3.7};앞의 두 줄은 컴파일되며 소수점 아래를 버린 3으로 초기화되고, 경고 옵션에 따라 경고가 나올 뿐입니다. 셋째 줄은 double의 3.7을 int에 담으면 값을 잃으므로, 축소 변환이라는 오류로 컴파일되지 않습니다. 그래서 값이 바뀌지 않아야 하는 곳에서는 중괄호 초기화가 실수를 일찍 찾아 줍니다. 다만 클래스 타입에서는 중괄호와 괄호가 다른 생성자를 고를 수 있습니다. 예를 들어 뒤에서 배울 std::vector<int>에서 std::vector<int> a(3, 0);은 0이 세 개 든 벡터를 만들지만, std::vector<int> b{3, 0};은 원소 목록을 받는 생성자가 먼저 골라져 3과 0 두 원소를 담은 벡터를 만듭니다.
값을 정하고 바꾸기
다음 프로그램은 여러 방법으로 변수를 초기화하고, 나중에 일부 값을 바꿉니다. 앞의 네 변수는 복사, 직접, 중괄호 초기화로 만들면서 값을 정하고, numberOfAttempts만 값 없이 선언한 뒤 따로 대입합니다. 출력은 구분선을 사이에 두고 바꾸기 전과 바꾼 뒤로 나뉘므로, score와 isPassed의 두 줄을 비교해 보세요.
#include <iostream>
#include <string>
int main() {
int studentId = 20240001;
std::string studentName = "Kim";
double score(85.5);
bool isPassed{true};
int numberOfAttempts;
numberOfAttempts = 3;
std::cout << "student id: " << studentId << "\n";
std::cout << "name: " << studentName << "\n";
std::cout << "score: " << score << "\n";
std::cout << "passed: " << std::boolalpha << isPassed << "\n";
std::cout << "attempts: " << numberOfAttempts << "\n";
score = 92.0;
isPassed = false;
std::cout << "\n--- after change ---\n";
std::cout << "score: " << score << "\n";
std::cout << "passed: " << isPassed << "\n";
}student id: 20240001
name: Kim
score: 85.5
passed: true
attempts: 3
--- after change ---
score: 92
passed: falsestd::string은 <string> 헤더의 문자열 타입으로, 뒤의 장에서 자세히 다룹니다. numberOfAttempts는 선언한 뒤 3을 대입했으므로 읽을 때는 값이 있습니다. 92.0을 대입한 score가 92로 출력되는 것은, std::cout이 기본으로 불필요한 소수점 아래의 0을 보여 주지 않기 때문입니다. std::boolalpha는 한 번 보내면 그 스트림에 남으므로 마지막 줄도 false라는 낱말로 출력됩니다.
변수는 쓰기 직전에, 쓸 수 있는 가장 좁은 범위에서 선언하면서 바로 초기화하는 것이 좋습니다. 선언과 첫 대입 사이가 멀어지면 그 사이에서 값이 없는 변수를 읽는 실수가 생기기 쉽습니다.
이름을 짓는 규칙
변수의 이름인 식별자(identifier)는 다음 규칙을 지켜야 하며, 어기면 컴파일 오류가 납니다.
| 규칙 | 예 |
|---|---|
| 영문자와 숫자, 밑줄로 짓고 숫자로 시작할 수 없습니다. | player1은 되고 1stPlayer는 안 됩니다. |
int, if, for 같은 키워드는 쓸 수 없습니다. | int for;는 오류입니다. |
| 대소문자를 구별합니다. | age와 Age는 다른 변수입니다. |
| 공백을 넣을 수 없습니다. | my age는 안 됩니다. |
표준은 일부 유니코드 글자도 식별자로 허용하지만, 도구와 팀의 규칙과 맞추려고 이 교재는 영문 이름을 씁니다. 밑줄 두 개가 들어간 이름과, 밑줄 하나 다음에 대문자로 시작하는 _Value 같은 이름은 컴파일러와 표준 라이브러리가 쓰도록 예약되어 있으므로 피합니다.
규칙을 지키는 것과 함께, 이름만 보고도 쓰임을 알 수 있게 totalScore, userName, isValid처럼 짓습니다. 단어를 잇는 방식은 myVariableName 같은 카멜 표기(camel case)나 my_variable_name 같은 스네이크 표기(snake case)가 흔하며, 표준 라이브러리는 스네이크 표기를 씁니다. 어느 쪽이든 한 프로젝트 안에서는 하나로 맞춥니다.
const로 바꿀 수 없는 변수
원주율이나 게임의 최대 생명 수처럼 실행하는 동안 바뀌면 안 되는 값은 타입 앞에 const를 붙여 선언합니다. const 변수는 선언할 때 반드시 초기화해야 하고, 그 뒤로는 대입할 수 없습니다.
#include <iostream>
int main() {
const double PI = 3.141592;
const int MAX_LIVES = 3;
std::cout << "PI: " << PI << "\n";
std::cout << "MAX_LIVES: " << MAX_LIVES << "\n";
}PI: 3.14159
MAX_LIVES: 3PI가 3.14159로 출력되는 것은 값이 바뀐 것이 아니라, std::cout이 기본으로 유효 숫자 6자리까지만 보여 주기 때문입니다. PI = 3.14;처럼 const 변수에 대입하는 줄을 더하면, 읽기 전용인 변수에 대입할 수 없다는 컴파일 오류가 납니다. const는 이 값이 바뀌지 않는다는 뜻을 코드를 읽는 사람과 컴파일러에게 함께 알려, 실수로 바꾸는 코드를 컴파일 단계에서 막아 줍니다.
정리
- 변수는 타입이 있는 객체에 이름을 붙인 것이며,
타입 이름;으로 선언합니다. - 함수 안의 기본 타입 변수는 선언만 하면 값이 정해지지 않으며, 그 값을 읽는 것은 C++20에서 정의되지 않은 동작이므로 값을 정한 뒤에 읽습니다.
- 초기화는 객체를 만들면서 처음 값을 정하는 것이고, 대입은 이미 있는 객체에 새 값을 쓰는 것입니다.
- 초기화는 등호, 괄호, 중괄호로 적을 수 있으며, 중괄호 초기화는 값을 잃는 축소 변환을 오류로 막고 빈 중괄호는 0으로 초기화합니다.
- 식별자는 숫자로 시작할 수 없고 키워드를 쓸 수 없으며 대소문자를 구별하고, 밑줄 두 개가 들어간 이름은 피합니다.
const변수는 선언할 때 초기화해야 하고 그 뒤로는 대입할 수 없습니다.
연습 문제
1. 복사하는 시점
본문의 copy_then_change.cpp에서 saved를 만든 뒤 score에 30을 대입하면 무엇이 출력되나요? int saved{score};를 score = 20; 뒤로 옮기면 어떻게 되나요?
해설 보기
saved는 만들어질 때의 값 10을 그대로 가지므로, 30을 대입하면 30 10이 출력됩니다. 복사하는 줄을 score = 20; 뒤로 옮기면 그때의 score가 20이므로 saved도 20으로 초기화되어 20 20이 출력됩니다. 출력은 값을 복사한 시점에 따라 정해집니다.
2. const 변수에 대입하기
const int saved{score};로 바꾼 뒤 그 아래에 saved = 40;을 더하면 어떻게 되나요? 초기화하는 값이 다른 변수에서 왔다는 것이 영향을 주나요?
해설 보기
saved는 const이므로 대입할 수 없어 컴파일 오류가 납니다. 초기화하는 값이 리터럴이든 다른 변수든 상관없이, const 변수는 처음 초기화한 뒤로는 바꿀 수 없습니다.
3. 축소 변환
다음 세 줄 가운데 컴파일되지 않는 것은 무엇이고, 컴파일되는 줄의 n은 얼마인가요? 줄마다 초기화의 모양과 오른쪽 값의 타입이 무엇인지 함께 보세요.
int n1 = 9.99;
int n2{9.99};
int n3{9};해설 보기
int n2{9.99};는 double을 int로 바꾸면서 값을 잃는 축소 변환이어서 컴파일되지 않습니다. n1은 소수점 아래를 버린 9가 되며, 반올림하지 않는다는 점에 주의합니다. n3는 정수 리터럴 9이므로 그대로 9입니다.
다음 글 상수와 리터럴에서는 코드에 직접 적는 값인 리터럴의 종류와 타입, 그리고 const와 constexpr로 이름 붙인 상수를 만드는 방법을 다룹니다.