1. 구조체
구조체(Structure)
관련된 여러 데이터를 하나의 사용자 정의 자료형으로 묶는 기능이다. 서로 다른 자료형의 데이터를 맴버로 포함할 수 있다
학생 한 명의 정보를 표현하려면 이름, 학번, 성적처럼 서로 관련된 데이터가 필요하다
struct Student
{
const char* name;
const char* id;
int midterm;
int final;
};Student는 학생 정보를 저장하는 새로운 자료형이다
| 멤버 | 자료형 | 의미 |
|---|---|---|
| name | const char* | 이름 문자열을 가리키는 포인터 |
| id | const char* | 학번 문자열을 가리키는 포인터 |
| midterm | int | 중간고사 점수 |
| final | int | 기말고사 점수 |
배열과 구조체의 차이
| 구분 | 배열 | 구조체 |
|---|---|---|
| 구성 요소 | 같은 자료형의 원소 | 서로 다른 자료형도 가능한 멤버 |
| 접근 방법 | 인덱스 | 멤버 이름 |
| 기본 용도 | 같은 종류의 데이터 여러 개 저장 | 관련된 데이터를 하나의 단위로 표현 |
배열과 구조체를 함께 사용할 수도 있다. 예를 들어 구조체 안에 점수 배열을 넣거나, 학생 구조체 여러 개를 배열로 만들 수 있다
1.1 구조체의 정의와 변수 선언
1.1.1 구조체 정의
struct 뒤에 자료형 이름을 쓰고, 중괄호 안에 멤버를 선언한다
struct Point
{
int xpos;
int ypos;
};구조체 정의의 끝에는 세미콜론을 붙인다
여기까지는 자료형을 정의한 것이며, Point형 변수는 아직 만들지 않았다
1.1.2 구조체 변수 선언
C++에서는 구조체 이름을 자료형 이름으로 바로 사용할 수 있다
Point pos;C에서는 별도의 별칭이 없다면 struct를 함께 적는다
struct Point pos;| 구분 | 변수 선언 |
|---|---|
| C | struct Point pos; |
| C++ | Point pos; |
C++에서도 struct Point pos; 형태를 사용할 수 있지만, 일반적으로 구조체 이름만 사용한다
구조체를 정의하면서 변수를 함께 선언할 수도 있다
struct Point
{
int xpos;
int ypos;
} pos1, pos2;경우 Point라는 자료형과 pos1, pos2라는 변수 두 개가 만들어진다
1.2 typedef를 이용한 자료형 별칭
typedef이미 존재하는 자료형에 다른 이름인 별칭을 부여하는 키워드이다. 별개의 새로운 자료형을 만드는 것은 아니다
C에서는 struct를 반복해서 쓰지 않도록 구조체와 typedef를 함께 사용하는 경우가 많다
1.2.1 구조체 정의 후 별칭 지정
struct point
{
int xpos;
int ypos;
};
typedef struct point Point;이후에는 다음 두 선언을 모두 사용할 수 있다
struct point pos1;
Point pos2;struct point와 Point는 같은 자료형을 나타낸다
1.2.2 정의와 별칭 지정을 한 번에 작성
typedef struct point
{
int xpos;
int ypos;
} Point;구조체 이름을 생략할 수도 있다
typedef struct
{
int xpos;
int ypos;
} Point;이 경우 구조체 자체에는 태그 이름이 없고, Point라는 별칭을 사용한다
C++에서는 다음과 같이 정의하면 별도의 typedef 없이 Point를 사용할 수 있다
struct Point
{
int xpos;
int ypos;
};1.3 구조체 변수의 초기화
단순한 구조체는 중괄호 안에 값을 나열하여 초기화할 수 있다
Point pos = {10, 20};초기값은 멤버가 선언된 순서대로 대응한다
| 멤버 | 초기값 |
|---|---|
| xpos | 10 |
| ypos | 20 |
다른 자료형의 멤버가 있어도 같은 방식으로 초기화할 수 있다
Student student = {"Kim", "13001", 90, 85};이 구조체의 포인터 멤버에는 문자열 리터럴의 주소가 저장된다
1.3.1 일부 멤버만 초기화
앞에서 정의한 Point에서는 초기값을 일부만 지정하면 나머지 멤버가 0으로 초기화된다
Point pos = {10};
// xpos는 10, ypos는 0빈 중괄호를 사용하면 두 정수 멤버를 모두 0으로 초기화할 수 있다
Point pos = {};
// xpos와 ypos 모두 0반면 함수 안에서 다음과 같이 선언하면, 이 구조체의 정수 멤버가 자동으로 0이 된다고 가정하면 안 된다
Point pos;멤버의 값을 읽기 전에 초기화하거나 값을 대입해야 한다
1.4 멤버 접근 연산자 .
구조체 변수의 멤버에는 점 연산자로 접근한다
Point pos = {10, 20};
pos.xpos; // 10
pos.ypos; // 20멤버에 새로운 값을 대입할 수도 있다
pos.xpos = 30;
pos.ypos = 40;| 표현 | 의미 |
|---|---|
pos | 구조체 객체 전체 |
pos.xpos | pos의 xpos 멤버 |
pos.ypos | pos의 ypos 멤버 |
구조체 변수마다 멤버 값을 따로 가진다
Point pos1 = {10, 20};
Point pos2 = {30, 40};
pos1.xpos = 100;이때 pos1.xpos만 변경되고, pos2.xpos는 30으로 유지된다
1.5 포인터 멤버 선언 시 주의점
한 선언문에서 *는 각각의 선언 대상에 적용된다
const char* name, id, grade;이 코드에서는 name만 포인터이고, id와 grade는 const char형이다. 세 멤버를 모두 문자열 포인터로 만들려면 다음과 같이 작성해야 한다
const char* name;
const char* id;
const char* grade;또는 각 이름 앞에 *를 붙인다
const char *name, *id, *grade;여러 멤버 선언에서 포인터 구분
const char* name, id, grade;는 세 멤버 모두를 포인터로 선언하지 않는다. 각 멤버의 자료형을 확인하고, 혼동을 줄이려면 선언을 한 줄씩 나누어 작성한다
2. 구조체 배열, 중첩 구조체와 포인터
2.1 구조체 배열
구조체 배열(Array of Structures)
구조체를 원소로 가지는 배열이다. 같은 구조의 데이터를 여러 개 저장할 수 있다
struct Point
{
int xpos;
int ypos;
};
Point points[3];points는 Point형 객체 3개를 저장한다. 각 원소에는 xpos와 ypos가 따로 존재한다
2.1.1 초기화
각 구조체의 초기값을 중괄호로 묶어 나열한다
Point points[3] = {
{10, 20},
{30, 40},
{50, 60}
};| 배열 원소 | xpos | ypos |
|---|---|---|
points[0] | 10 | 20 |
points[1] | 30 | 40 |
points[2] | 50 | 60 |
2.1.2원소의 멤버 접근
먼저 인덱스로 구조체 원소를 선택한 뒤, .으로 멤버에 접근한다
points[0].xpos; // 10
points[1].ypos; // 40
points[2].xpos = 100;기본 형태는 다음과 같다
배열이름[인덱스].멤버이름배열의 각 원소가 구조체 전체이므로, 한 원소를 통해 관련된 데이터를 함께 다룰 수 있다
2.2 2차원 구조체 배열
구조체도 2차원 배열의 원소로 사용할 수 있다
Point grid[2][3] = {
{{1, 2}, {3, 4}, {5, 6}},
{{7, 8}, {9, 10}, {11, 12}}
};gird는 2행 3열이며, 총 6개의 Point 객체를 가진다
grid[0][1].xpos; // 3
grid[1][2].ypos; // 12
grid[1][0].xpos = 100;기본 형태는 다음과 같다
배열이름[행][열].멤버이름| 표현 | 의미 |
|---|---|
grid[1][2] | 1번 행, 2번 열의 Point 객체 |
grid[1][2].xpos | 해당 객체의 xpos 멤버 |
grid[1][2].ypos | 해당 객체의 ypos 멤버 |
인덱스는 각각 0부터 시작하며, 행과 열 모두 유효한 범위 안에서 접근해야 한다
2.3 중첩 구조체
중첩 구조체(Nested Structure)
구조체의 멤버로 다른 구조체 객체를 포함하는 구성이다. 관련된 데이터를 여러 단계로 묶어 표현할 수 있다
struct Point
{
int xpos;
int ypos;
};
struct Rectangle
{
Point topLeft;
Point bottomRight;
};Rectangle은 두 개의 Point 객체를 멤버로 가진다
2.3.1 초기화
Rectangle rect = {
{0, 10},
{20, 0}
};2.3.2 멤버 접근
구조체 안의 구조체에 접근하므로 .을 연속해서 사용한다
rect.topLeft.xpos; // 0
rect.bottomRight.ypos; // 0
rect.topLeft.xpos = 5;2.4 구조체 포인터
구조체 객체의 주소도 포인터에 저장할 수 있다
Point pos = {10, 20};
Point* ppos = &pos;포인터를 간접 참조한 뒤 .으로 멤버에 접근할 수 있다
(*ppos).xpos = 30;
(*ppos).ypos = 40;2.5 화살표 연산자 ->
화살표 연산자(Arrow Operator)
포인터가 가리키는 구조체나 클래스 객체의 멤버에 접근하는 연산자이다. 내장 포인터에서
p->member는(*p).member와 같은 의미이다
ppos->xpos = 30;
ppos->ypos = 40;다음 두 표현은 같은 멤버에 접근한다
(*ppos).xpos;
ppos->xpos;2.5.1 .과 ->의 구분
| 접근 대상 | 연산자 | 표현 |
|---|---|---|
| 구조체 객체 | . | pos.xpos |
| 구조체를 가리키는 포인터 | -> | ppos->xpos |
(*ppos).xpos에서 괄호를 생략하면 의미가 달라진다. .이 단항 *보다 우선순위가 높으므로 이 표현은 *(ppos.xpos)로 해석된다. ppos는 구조체 객체가 아니라 포인터이므로 이 코드에서는 오류가 발생한다
2.6 구조체 배열과 포인터의 연결
구조체 배열 이름도 많은 식에서 첫 번째 원소를 가리키는 포인터로 변환된다
Point points[3] = {
{10, 20},
{30, 40},
{50, 60}
};
Point* ptr = points;| 표현 | 가리키는 대상 |
|---|---|
ptr | points[0] |
ptr + 1 | points[1] |
ptr + 2 | points[2] |
따라서 다음 표현은 모두 두 번째 원소의 xpos에 접근한다
points[1].xpos;
ptr[1].xpos;
(*(ptr + 1)).xpos;
(ptr + 1)->xpos;포인터를 1 증가시키면 구조체 한 개 크기만큼 이동한다
++ptr; // points[1]을 가리킴구조체 크기와 포인터 이동
구조체는 정렬을 위한 빈 공간인 패딩을 포함할 수 있으므로, 전체 크기가 멤버 크기의 단순한 합과 같다고 단정하면 안 된다.
Point*의+1은 실제sizeof(Point)에 해당하는 크기만큼 이동한다
2.7 중첩 구조체와 포인터의 조합
바깥 구조체를 포인터로 가리키더라도, 내부 멤버가 객체인지 포인터인지에 따라 연산자를 구분한다
Rectangle rect = {
{0, 10},
{20, 0}
};
Rectangle* ptr = ▭ptr->topLeft.xpos = 5;접근 순서는 다음과 같다
ptr->topLeft로 Rectangle 안의Point객체에 접근한다.xpos로 해당Point의 정수 멤버에 접근한다
| 표현 | 사용하는 이유 |
|---|---|
ptr->topLeft | ptr이 포인터이므로 -> 사용 |
ptr->topLeft.xpos | topLeft는 객체이므로 . 사용 |
멤버가 중첩되어 있다는 이유로 모두 ->를 사용하는 것은 아니다. 각 단계에서 왼쪽 대상의 자료형을 확인해야 한다
3. 구조체의 함수 전달과 반환
구조체도 기본 자료형처럼 함수의 인수로 전달하거나 반환값으로 사용할 수 있다. 전달 방식에 따라 원본 변경 여부와 복사 여부가 달라진다
이 단원에서는 다음 구조체를 사용한다
struct Point
{
int xpos;
int ypos;
};3.1 구조체의 값 전달
구조체를 값으로 전달하면 매개변수는 원본을 복사한 별도의 객체가 된다
void change(Point p)
{
p.xpos = 100;
p.ypos = 200;
}Point pos = {10, 20};
change(pos);함수 내부에서 p를 변경해도 원본 pos는 변경되지 않는다
| 시점 | 원본 pos | 매개변수 p |
|---|---|---|
| 함수 진입 시 | {10, 20} | {10, 20} |
| 변경 후 | {10, 20} | {100, 200} |
값 전달은 원본과 분리된 데이터를 처리할 때 사용할 수 있다. 다만 구조체가 크면 복사 비용이 커질 수 있다
3.2 구조체의 반환
함수의 반환형에 구조체 자료형을 지정하면 구조체 객체를 값을 반환할 수 있다
Point getScale2xPoint(Point p)
{
p.xpos *= 2;
p.ypos *= 2;
return p;
}Point p1 = {1, 2};
Point p2 = getScale2xPoint(p1);| 객체 | 호출 후 값 |
|---|---|
| p1 | {1, 2} |
| p2 | {2, 4} |
처리 순서는 다음과 같다
- p1을 복사하여 매개변수 p를 만든다
- p의 두 좌표를 2배로 변경한다
- 변경된 구조체를 값으로 반환한다
- 반환된 결과로 p2를 초기화한다
원본에 결과를 반영하려면 반환값을 다시 대입할 수 있다
p1 = getScale2xPoint(p1);
// p1은 {2, 4}실제 반환 과정에서는 컴파일러가 불필요한 복사를 생략할 수 있다
3.3 포인터를 통한 구조체 전달
원본 구조체를 변경하려면 주소를 전달하고, 포인터를 통해 멤버에 접근할 수 있다
void scale2x(Point* p)
{
p->xpos *= 2;
p->ypos *= 2;
}Point pos = {1, 2};
scale2x(&pos);
// pos는 {2, 4}| 코드 | 역할 |
|---|---|
&pos | 원본 구조체의 주소 |
Point* p | 주소를 전달받는 포인터 매개변수 |
p->xpos | 원본 구조체의 xpos 멤버에 접근 |
포인터 매개변수에는 주소 값이 복사된다. 구조체 전체를 복사하지 않고, 복사된 주소를 통해 원본에 접근한다
따라서 포인터 전달 자체는 값 전달이며, C++의 참조 매개변수와 구분해야 한다
3.4 참조를 통한 구조체 전달
C++에서는 구조체를 참조 매개변수로 받을 수 있다
void scale2x(Point& p)
{
p.xpos *= 2;
p.ypos *= 2;
}Point pos = {1, 2};
scale2x(pos);
// pos는 {2, 4}p는 원본 pos의 별칭이므로, 멤버 변경이 원본에 반영된다
| 구분 | 포인터 매개변수 | 참조 매개변수 |
|---|---|---|
| 선언 | Point* p | Point& p |
| 호출 | scale2x(&pos) | scale2x(pos) |
| 멤버 접근 | p->xpos | p.xpos |
| 원본 변경 | 가능 | 가능 |
참조를 사용할 때는 선언에 &를 붙이고, 호출에서는 객체를 그대로 전달한다
3.5 const를 통한 일기 전용 전달
원본을 변경하지 않으면서 구조체 전체를 복사를 피하려면 const 포인터나 const 참조를 사용할 수 있다
3.5.1 const 포인터 매개변수
int sumCoordinates(const Point* p)
{
return p->xpos + p->ypos;
}Point pos = {10, 20};
int sum = sumCoordinates(&pos); // 30p를 통해 맴버를 읽을 수는 있지만, 변경할 수는 없다
3.5.2 const 참조 매개변수
int sumCoordinates(const Point& p)
{
return p.xpos + p.ypos;
}Point pos = {10, 20};
int sum = sumCoordinates(pos); // 30일반적인 Point 객체를 전달하면 구조체를 복사하지 않고 원본을 참조하며, p를 통한 변경도 제한한다
| 목적 | 매개변수 형태 |
|---|---|
| 복사본을 자유롭게 변경 | Point p |
| 원본을 포인터로 변경 | Point* p |
| 원본을 참조로 변경 | Point& p |
| 포인터를 통해 읽기만 수행 | const Point* p |
| 참조를 통해 읽기만 수행 | const Point& p |
작은 구조체는 값 전달도 적절할 수 있으므로, 모든 구조체를 반드시 참조로 전달해야 하는 것은 아니다
3.6 포인터 멤버가 있는 구조체의 복사
구조체의 값 전달과 복사는 기본적으로 각 멤버를 복사한다. 포인터 멤버가 있다면 포인터가 가리키는 데이터 전체가 아니라 주소 값이 복사된다
struct Data
{
int* value;
};
int num = 10;
Data original = {&num};
Data copied = original;original과 copied는 별도의 구조체 객체지만, 두 포인터 멤버는 같은 num을 가리킨다.
*copied.value = 20;
// num은 20구조체 복사와 포인터 대상의 복사
구조체가 복사되어도 포인터가 가리키는 객체까지 별도로 복사되는 것은 아니다. 복사된 포인터를 통해 같은 객체를 변경할 수 있으므로, 구조체 자체와 포인터의 대상을 구분해야 한다
3.7 구조체 반환 시 수명 주의
지역 구조체를 값으로 반환하는 것은 가능하다
Point makePoint()
{
Point p = {10, 20};
return p;
}반면 함수가 끝나면 사라지는 지역 객체의 주소나 참조를 반환해서는 안 된다
const Point* wrong()
{
Point p = {10, 20};
return &p; // 잘못된 반환
}호출이 끝나면 p의 수명이 끝나므로, 반환된 포인터를 통해 유효한 Point 객체에 접근할 수 없다. 지역 객체의 참조를 반환하는 경우도 같은 문제를 가진다
4. C++ 구조체와 클래스, 접근 제어
4.1 C와 C++의 구조체 차이
C++의 구조체는 데이터를 저장하는 멤버 변수뿐 아니라, 데이터를 처리하는 멤버 함수와 접근 제어 기능도 사용할 수 있다
| 구분 | C의 구조체 | C++의 구조체 |
|---|---|---|
| 멤버 변수 | 가능 | 가능 |
| 멤버 함수 | 직접 정의할 수 없음 | 가능 |
| 접근 지정자 | 없음 | public, private, protected |
| 기본 멤버 접근 | 접근 제어 기능 없음 | public |
| 변수 선언 | struct Point p; 또는 별칭 사용 | Point p; 가능 |
C 구조체에도 함수 포인터를 멤버로 저장할 수 있지만, C++의 멤버 함수와는 다르다
4.2 멤버 변수와 멤버 함수
멤버 변수와 멤버 함수
멤버 변수는 구조체나 클래스에 속하는 데이터를 나타내고, 멤버 함수는 해당 자료형에 속하는 동작을 정의한다
struct Point
{
int x;
int y;
void setXY(int a, int b)
{
x = a;
y = b;
}
};| 구분 | 해당 멤버 |
|---|---|
| 멤버 변수 | x, y |
| 멤버 함수 | setXY() |
객체를 만든 뒤 .으로 멤버 함수를 호출한다.
Point p;
p.setXY(3, 4);
// p.x는 3, p.y는 4멤버 함수 안의 x, y는 함수를 호출한 객체의 멤버를 나타낸다
Point p1;
Point p2;
p1.setXY(1, 2);
p2.setXY(3, 4);p1과 p2는 각각 독립적인 좌표 값을 가진다
4.3 접근 제어와 접근 지정자
접근 제어(Access Control)
구조체나 클래스의 멤버를 어떤 코드에서 사용할 수 있는지 제한하는 기능이다. 접근 지정자를 사용하여 범위를 설정한다
| 접근 지정자 | 기본적인 접근 범위 |
|---|---|
| public | 객체에 접근할 수 있는 외부 코드에서도 사용 가능 |
| private | 해당 클래스의 멤버와 허용된 친구(friend)에서 사용 가능 |
| protected | 해당 클래스와 파생 클래스의 멤버, 허용된 친구 등에서 사용 가능 |
protected의 자세한 접근 규칙은 상속 단원에서 다룬다
접근 지정자는 뒤에 :을 붙이며, 다음 접근 지정자가 나올 때까지 적용된다
struct Point
{
private:
int x;
int y;
public:
void setXY(int a, int b)
{
x = a;
y = b;
}
};4.3.1 public 멤버
외부에서 공개된 멤버 함수를 호출할 수 있다
Point p;
p.setXY(3, 4); // 가능4.3.2 private 멤버
외부에서는 x, y에 직접 접근할 수 없다
p.x = 3; // 오류
p.y = 4; // 오류하지만 setXY()는 해당 구조체의 멤버 함수이므로 내부의 private 멤버를 변경할 수 있다
접근 제어는 이름을 통해 멤버에 접근할 수 있는지를 검사하는 문법 규칙이며, 데이터를 암호화하는 기능은 아니다
4.4 구조체와 클래스의 차이
클래스(Class)
데이터와 관련 기능을 멤버 변수와 멤버 함수로 묶어 정의하는 사용자 정의 자료형이다. 이 자료형을 바탕으로 객체를 만들 수 있다
C++의 struct와 class는 거의 같은 기능을 제공한다. 둘 다 멤버 함수, 접근 제어, 생성자, 상속 등을 사용할 수 있다
주요 차이는 접근 지정자를 생략했을 때의 기본값이다
| 구분 | struct | class |
|---|---|---|
| 기본 멤버 접근 | public | private |
| 기본 상속 접근 | public | private |
4.4.1 구조체의 기본 접근
struct Point
{
int x;
int y;
};
Point p;
p.x = 3; // 가능: 기본 public4.4.2 클래스의 기본 접근
class Point
{
int x;
int y;
};
Point p;
p.x = 3; // 오류: 기본 private클래스에서도 public을 명시하면 외부에서 접근할 수 있다
class Point
{
public:
int x;
int y;
};
Point p;
p.x = 3; // 가능구조체도 접근 제어가 가능하다
C++에서 구조체는 데이터만 저장하고 클래스만 함수를 가진다는 구분은 틀리다. 둘 다 멤버 변수와 멤버 함수를 가질 수 있으며, 기본 접근 수준이 다르다
4.5 공개 함수를 통한 데이터 접근
데이터는 private으로 두고, 필요한 기능만 public으로 제공할 수 있다
class Point
{
private:
int x;
int y;
public:
void setXY(int a, int b)
{
x = a;
y = b;
}
int getX() const
{
return x;
}
int getY() const
{
return y;
}
};Point p;
p.setXY(3, 4);
int xpos = p.getX(); // 3
int ypos = p.getY(); // 4| 작업 | 사용하는 함수 |
|---|---|
| 좌표 변경 | setXY() |
| x좌표 확인 | getX() |
| y좌표 확인 | getY() |
getX(), getY() 뒤의 const는 해당 함수가 객체의 일반적인 멤버 상태를 변경하지 않도록 제한한다
이처럼 외부에서 데이터를 직접 변경하는 대신 함수를 거치도록 하면, 함수 내부에서 값의 범위나 변경 조건을 검사할 수 있다. 다만 private 선언만으로 올바른 데이터 상태가 자동으로 보장되는 것은 아니다
4.6 멤버 함수의 선언과 외부 정의
멤버 함수는 구조체나 클래스 안에서 선언하고, 밖에서 정의할 수도 있다
class Point
{
private:
int x;
int y;
public:
void setXY(int a, int b);
};외부에서 정의할 때는 클래스 이름과 범위 지정 연산자 ::를 붙인다
void Point::setXY(int a, int b)
{
x = a;
y = b;
}클래스 밖에 정의했더라도 Point의 멤버함수이므로 private 멤버에 접근할 수 있다
4.6.1 내부 전용 멤버 함수
멤버 함수 자체도 private으로 둘 수 있다
class Stock
{
private:
int shares;
double shareValue;
double totalValue;
void updateTotal()
{
totalValue = shares * shareValue;
}
public:
void setInfo(int count, double price)
{
shares = count;
shareValue = price;
updateTotal();
}
};Stock stock;
stock.setInfo(20, 12.5); // 가능
stock.updateTotal(); // 오류: private 멤버 함수외부에는 필요한 작업만 공개하고, 내부 계산은 비공개 함수로 관리하는 구성이다
5. 네임스페이스
네임스페이스(Namespace)
변수, 함수, 자료형 등의 이름을 하나의 이름 공간으로 묶는 기능이다. 서로 다른 이름 공간에서 같은 이름을 사용할 수 있어 이름 충돌을 줄인다
여러 라이브러리나 파일을 함께 사용하면 같은 이름의 함수나 자료형이 존재할 수 있다. 네임스페이스를 사용하면 어느 이름 공간에 속하는지 지정하여 구분할 수 있다
namespace first_space
{
int value = 3;
}
namespace second_space
{
int value = 4;
}두 변수의 이름은 모두 value이지만, 서로 다른 네임스페이스에 속하므로 별개의 변수이다
5.1 네임스페이스 정의와 이름 사용
namespace 뒤에 이름을 쓰고, 중괄호 안에 관련 선언을 작성한다
namespace example
{
int value = 3;
int func()
{
return value;
}
}네임스페이스에는 변수, 함수, 구조체, 클래스 등을 선언할 수 있다
외부에서 이름을 사용할 때는 네임스페이스 이름과 ::를 붙인다
int x = example::value; // 3
int y = example::func(); // 3| 표현 | 의미 |
|---|---|
example::value | example에 속한 변수 value |
example::func() | example에 속한 함수 func() 호출 |
함수 func() 내부에서는 같은 네임스페이스에 속한 value를 접두사 없이 사용할 수 있다
5.2 범위 지정 연산자 ::
범위 지정 연산자(Scope Resolution Operator)
이름이 속한 네임스페이스나 클래스 등의 범위를 지정하는 연산자이다
first_space::value;
second_space::value;앞에서 멤버 함수를 클래스 밖에 정의할 때 사용한 ::도 같은 연산자이다
| 표현 | 지정하는 범위 |
|---|---|
example::func() | 네임스페이스 example |
Point::setXY() | 클래스 또는 구조체 Point |
std::cout | 표준 네임스페이스 std |
5.3 같은 이름의 함수 구분
서로 다른 네임스페이스에 같은 이름의 함수를 정의할 수 있다
namespace first_space
{
int func()
{
return 3;
}
}
namespace second_space
{
int func()
{
return 4;
}
}호출할 때 이름 공간을 지정한다
int x = first_space::func(); // 3
int y = second_space::func(); // 4이 상태에서 전역 범위의 함수 안에 단순히 다음과 같이 작성하면 해당 이름을 찾지 못한다
func(); // 오류: 이 위치에서 사용할 func 선언을 찾지 못함다른 네임스페이스에 같은 이름이 존재한다고 해서 자동으로 검색하여 선택하지는 않는다
5.4 using namespace
using namespace를 사용하면 해당 네임스페이스의 이름을 접두사 없이 찾을 수 있도록 한다
using namespace first_space;
int x = func(); // first_space::func() 호출사용 형태는 다음과 같다
using namespace 네임스페이스이름;네임스페이스의 이름을 다른 곳으로 복사하거나 옮기는 기능은 아니다. 이후 이름을 찾는 과정에 영향을 준다
4.4.1 적용 범위
함수 안에서 사용할 수도 있다
int main()
{
using namespace first_space;
int x = func();
return 0;
}이 경우 using namespace의 효과는 해당 블록의 범위 안에서 적용된다
4.4.2 여러 네임스페이스를 사용하는 경우
using namespace first_space;
using namespace second_space;
int x = func(); // 오류: 호출이 모호함두 네임스페이스에 매개변수가 같은 func()가 있으므로 어느 함수를 호출할지 결정할 수 없다
이때는 네임스페이스를 명시하여 구분한다
int x = first_space::func();
int y = second_space::func();using namespace와 이름 충돌
using namespace는 이름을 짧게 쓰도록 돕지만, 여러 이름 공간의 같은 이름이 함께 검색되면 모호성이 생길 수 있다. 이름 충돌이 발생하면
네임스페이스::이름으로 대상을 명확하게 지정한다
5.5 표준 네임스페이스 std
C++ 표준 라이브러리의 주요 클래스, 함수, 객체는 std 네임스페이스에 속한다
| 이름 | 역할 | 필요한 헤더 |
|---|---|---|
std::cout | 표준 출력 | <iostream> |
std::cin | 표준 입력 | <iostream> |
std::endl | 줄 바꿈과 출력 버퍼 비우기 | <ostream> 또는 이를 포함하는 <iostream> |
std::string | 문자열 클래스 | <string> |
#include <iostream>
#include <string>
std::string text = "Hello";함수 안에서는 다음과 같이 출력할 수 있다
std::cout << text << '\n';using namespace std;를 사용하면 cout, string처럼 접두사 없이 쓸 수 있지만, std::를 직접 붙이면 이름의 소속을 명확하게 드러낼 수 있다
헤더를 포함하는 것과 네임스페이스를 지정하는 것은 역할이 다르다
| 코드 | 역할 |
|---|---|
#include <string> | std::string 사용에 필요한 선언 등을 제공 |
std::string | std에 속한 string이라는 이름을 지정 |
using namespace std; | std의 이름을 접두사 없이 검색할 수 있도록 함 |
using namespace std;만 작성한다고 필요한 헤더가 포함되는 것은 아니다
6. 함수 오버로딩
함수 오버로딩(Function Overloading)
매개변수의 개수나 자료형 구성이 다른 함수를 같은 이름으로 선언하는 기능이다. 컴파일러가 전달된 인수에 가장 적합한 함수를 선택한다
int add(int a, int b);
double add(double a, double b);두 함수의 이름은 add로 같지만 매개변수의 자료형이 다르므로 함께 선언할 수 있다
add(1, 2); // int형 매개변수를 받는 함수
add(1.5, 2.5); // double형 매개변수를 받는 함수비슷한 작업을 수행하는 함수들을 같은 이름으로 표현할 수 있다
6.1 오버로딩을 구분하는 기준
일반적인 함수 오버로딩에서는 매개변수의 개수, 자료형, 자료형의 순서를 구분한다
| 구분 기준 | 선언 |
|---|---|
| 개수 | void print(int a); / void print(int a, int b); |
| 자료형 | void print(int a); / void print(double a); |
| 자료형 순서 | void print(int a, double b); / void print(double a, int b); |
6.1.1 매개변수 이름은 구분 기준이 아니다
void print(int value);
void print(int number);위 두 선언은 이름만 다를 뿐 같은 함수를 선언한다
자료형이 모두 같다면 매개변수 이름의 순서를 바꾸어도 다른 함수가 되지 않는다
void process(int x, int y);
void process(int y, int x); // 같은 함수의 선언6.2 반환형만으로는 구분할 수 없다
int calculate(int value);
double calculate(int value); // 오류매개변수가 같고 반환형만 다르므로 함께 선언할 수 없다
반환값을 저장하는 변수의 자료형으로 호출할 함수를 선택하지도 않는다
double result = calculate(10);result가 double형이라는 이유로 double을 반환하는 함수를 선택하는 방식은 아니다
함수 선택은 반환형으로 결정하지 않는다
일반적인 함수 오버로딩에서 반환형은 함수를 구분하는 기준이 아니다. 매개변수 구성이 같은 함수에 반환형만 다르게 지정하면 오류가 발생한다
6.3 인수에 따른 함수 선택
다음 함수들이 선언되어 있다고 하자
void print(const char* str, int width);
void print(double value, int width);
void print(long value, int width);
void print(int value, int width);
void print(const char* str);컴파일러는 인수의 개수와 자료형을 고려하여 가장 적합한 함수를 선택한다
| 호출 | 선택되는 함수 |
|---|---|
print("aaa", 15) | print(const char*, int) |
print("bbb") | print(const char*) |
print(1999.0, 10) | print(double, int) |
print(1999L, 15) | print(long, int) |
print(1999, 12) | print(int, int) |
리터럴의 자료형도 함수 선택에 영향을 준다
| 리터럴 | 자료형 |
|---|---|
| 1999 | int |
| 1999L | long |
| 1999.0 | double |
| 1999.0f | float |
문자열 리터럴은 위 호출에서 첫 문자를 가리키는 const char*로 변환되어 전달된다
6.4 암시적 형 변환과 함수 선택
인수와 정확히 같은 자료형의 매개변수가 없어도, 허용되는 형 변환을 통해 호출할 수 있다
void print(int value, int width);print(1990.3, 10);위 함수만 후보로 존재한다면 첫 번째 인수는 int로 변환되어 전달된다. 따라서 매개변수 value에는 1990이 저장된다
6.4.1정확히 일치하는 함수가 있는 경우
void print(int value, int width);
void print(double value, int width);print(1990.3, 10); // double 버전 선택1990.3은 double형이므로, 이 경우에는 형 변환이 필요 없는 double 버전이 선택된다
| 후보 | 첫 번째 인수 처리 |
|---|---|
print(int, int) | double에서 int로 변환 필요 |
print(double, int) | 정확히 일치 |
함수 선택은 실행 중 인수의 숫자 값을 보고 결정하는 것이 아니라, 컴파일 시 인수의 자료형과 가능한 변환을 바탕으로 결정한다
6.5 모호한 함수 호출
가장 적합한 함수를 하나로 결정할 수 없으면 컴파일 오류가 발생한다
void show(long value);
void show(double value);show(10); // 오류: 호출이 모호함10은 int형이고, 두 후보 모두 변환이 필요하다
| 후보 | 필요한 변환 |
|---|---|
show(long) | int → long |
show(double) | int → double |
이 경우 한쪽이 더 적합하다고 결정할 수 없다
원하는 자료형으로 인수를 지정하면 호출 대상을 명확하게 할 수 있다
show(10L); // long 버전
show(10.0); // double 버전6.5.1 여러 인수에서 적합성이 나뉘는 경우
void process(int a, double b);
void process(double a, int b);process(1, 2); // 오류: 호출이 모호함첫 번째 후보는 첫 인수에 정확히 일치하고, 두 번째 후보는 두 번째 인수에 정확히 일치한다. 어느 한 함수도 전체적으로 더 적합하지 않다
process(1, 2.0); // process(int, double)
process(1.0, 2); // process(double, int)6.7 함수 선언과 정의의 대응
오버로딩된 각 함수는 자신에게 맞는 정의가 필요하다
int add(int a, int b)
{
return a + b;
}
double add(double a, double b)
{
return a + b;
}호출에 사용할 함수는 호출 지점에서 선언이 보여야 한다. 함수 정의가 호출 코드 뒤에 있다면 앞에서 선언할 수 있다
int add(int a, int b);
double add(double a, double b);선언만 있고 선택된 함수의 정의가 프로그램 어디에도 없다면, 일반적으로 링크 단계에서 오류가 발생한다
| 상황 | 일반적인 결과 |
|---|---|
| 인수에 맞는 함수가 없음 | 컴파일 오류 |
| 가장 적합한 함수가 하나로 결정되지 않음 | 컴파일 오류 |
| 선택된 함수의 정의가 없음 | 링크 오류 |
7. const와 기본 인수
7.1 const 변수
const자료형에 붙여 해당 대상을 변경할 수 없도록 제한하는 한정자이다. 초기화 후 변경하지 않아야 하는 값을 표현할 때 사용한다
const int maxCount = 10;maxCount의 값을 읽을 수는 있지만, 새로운 값을 대입할 수는 없다
int count = maxCount; // 가능: 값 읽기
maxCount = 20; // 오류
++maxCount; // 오류7.1.1 선언할 때 초기화
기본 자료형의 const 변수는 정의할 때 초기값이 필요하다
const int x = 10; // 가능
const int y; // 오류나중에 값을 대입하는 것은 초기화가 아니므로 다음과 같이 작성할 수 없다
const int x;
x = 10;7.1.2 초기화와 대입 구분
const int x = 10;
const int y = x; // 가능: y를 초기화
x = y; // 오류: 이미 존재하는 const 변수에 대입오른쪽 값이 const인지보다 대입 대상인 왼쪽 객체를 변경할 수 있는지가 중요하다
7.2 포인터와 참조의 const
const가 붙는 위치에 따라 변경을 제한하는 대상이 달라진다
7.2.2 가리키는 대상의 변경 제한
int num1 = 10;
int num2 = 20;
const int* ptr = &num1;ptr을 통해 정수 값을 변경할 수는 없지만, 다른 객체를 가리키도록 포인터를 변경할 수는 있다
*ptr = 30; // 오류
ptr = &num2; // 가능원래 객체 num1 자체가 const는 아니므로, 다른 경로로 변경할 수 있다
num1 = 30; // 가능7.2.3 포인터 자체의 변경 제한
int* const ptr = &num1;포인터가 저장한 주소를 바꿀 수 없지만, 가리키는 정수는 변경할 수 있다
*ptr = 40; // 가능
ptr = &num2; // 오류| 선언 | 포인터의 주소 변경 | 포인터를 통한 대상 값 변경 |
|---|---|---|
int* ptr | 가능 | 가능 |
const int* ptr | 가능 | 불가능 |
int* const ptr | 불가능 | 가능 |
const int* const ptr | 불가능 | 불가능 |
7.2.4 const 참조 매개변수
구조체나 문자열을 읽기만 하는 함수에서는 const 참조를 사용할 수 있다
void printPoint(const Point& p);기존 Point 객체를 전달하면 복사를 피하면서, 함수가 p를 통해 일반적인 멤버 값을 변경하지 못하게 한다
7.3 기본 인수(Default Argument)
기본 인수(Default Argument)
함수 호출에서 인수를 생략했을 때 사용하도록 매개변수에 미리 지정한 기본값 또는 식이다
void printDate(int year = 2020, int month = 1, int day = 10);인수를 일부 또는 전부 생략하면 지정된 기본값이 사용된다
| 호출 | year | month | day |
|---|---|---|---|
printDate() | 2020 | 1 | 10 |
printDate(2026) | 2026 | 1 | 10 |
printDate(2026, 8) | 2026 | 8 | 10 |
printDate(2026, 8, 16) | 2026 | 8 | 16 |
전달한 인수가 있으면 기본값보다 직접 전달한 값이 사용된다
7.4 기본 인수의 지정과 생략 규칙
일반적인 함수 선언에서는 기본 인수를 오른쪽 매개변수부터 연속해서 지정한다
void printDate(int year, int month = 1, int day = 1);위 선언에서는 year를 반드시 전달해야 한다
printDate(2026); // 가능
printDate(2026, 8); // 가능
printDate(2026, 8, 16); // 가능
printDate(); // 오류: year에 대응하는 인수가 없음기본 인수가 없는 매개변수를 오른쪽에 남겨 두는 다음 선언은 잘못되었다
void printDate(int year = 2020, int month, int day);7.4.1 중간 인수만 생략할 수 없음
호출할 때 전달한 인수는 왼쪽 매개변수부터 대응한다
printDate(2026, 8);여기서 8은 month에 전달된다. day에 전달하고 싶은 값이라고 해서 중간을 비워 둘 수는 없다
printDate(2026, , 16); // 오류month의 기본값을 유지하고 day만 지정하려면, 기본값에 해당하는 월도 직접 전달한다
printDate(2026, 1, 16);
7.5 함수 선언과 정의에서의 기본 인수
선언과 정의를 분리한다면, 일반적으로 호출자에게 보이는 선언에 기본 인수를 지정한다
void printDate(int year, int month = 1, int day = 1);정의에서는 기본 인수를 반복하지 않는다
void printDate(int year, int month, int day)
{
std::cout << year << '/'
<< month << '/'
<< day << '\n';
}std::cout을 사용하려면 <iostream>을 포함해야 한다
동일한 범위에서 이미 보이는 기본 인수를 정의에 다시 지정하면 오류가 발생한다
// 앞의 선언에 기본값이 있는 상태에서:
// void printDate(int year, int month = 1, int day = 1) { ... }
기본 인수를 지정했다고 서로 다른 개수의 함수가 만들어지는 것은 아니다. 하나의 함수를 더 적은 인수로 호출할 수 있게 되는 것이다
7.6 기본 인수와 함수 오버로딩
기본 인수와 오버로딩을 함께 사용하면, 인수를 생략한 호출이 여러 함수에 맞을 수 있다
void printDate(int year = 2020, int month = 1, int day = 10);
void printDate();printDate(); // 오류: 호출이 모호함| 후보 | 인수 없이 호출 가능한 이유 |
|---|---|
printDate(int, int, int) | 세 인수를 모두 기본값으로 처리 |
printDate() | 매개변수가 없음 |
컴파일러가 하나를 선택할 수 없으므로 오류가 발생한다
반면 다음 호출은 세 매개변수를 가진 함수만 사용할 수 있다
printDate(2026);기본 인수까지 고려하여 호출 가능 여부 판단
함수의 매개변수 개수가 달라도 기본 인수 때문에 같은 호출에 모두 대응할 수 있다. 오버로딩된 함수 중 하나만 가장 적합하게 선택될 수 있도록 선언해야 한다
7.6.1 모호성을 없애는 선언
세 매개변수 함수의 첫 번째 인수를 필수로 만들면 구분할 수 있다
void printDate(int year, int month = 1, int day = 1);
void printDate();| 호출 | 선택되는 함수 |
|---|---|
printDate() | 매개변수가 없는 함수 |
printDate(2026) | 세 매개변수를 가진 함수 |
printDate(2026, 8) | 세 매개변수를 가진 함수 |
printDate(2026, 8, 16) | 세 매개변수를 가진 함수 |