1학년 때 내내 파이썬만 공부했다. 그래서 파이썬으로 어느정도 있어보이는 프로그램들을 만들 수 있게 됐을때, 난 굉장히 자만했다. 오,, 나 코딩천재아냐?ㅋ
그리고 구조체와 포인터를 만난 순간 내 자만은 산산조각났다. 쉬운언어 파이썬만 하던 내게 구조체, 포인터 이런 친구들은 졸랭 어려웠다.. 하지만 얘네를 완전히 이해하질 못 하니 어느정도 이상으로 나아가는 느낌이 들지 않는다. 그래서 이번 방학 때는 완전 다 뿌숴버리겠다는 일념으로, 내게 어려운 것 중 첫번째. 구조체를 뿌숴보겠다.
구조체를 공부하기에 앞서, 배열에 대해 간략히 알아보자. 배열에 대한 이모저모는 조만간 포스팅을 해볼 예정이다.
배열
배열(array)은 거의 모든 프로그래밍 언어에서 기본적으로 제공되는 자료형이다.
(파이썬에서는 리스트라는 아주아주아주아주 쉬운 형태로 제공된다.)
배열은 <인덱스, 값>의 쌍으로 이루어진 집합으로 정의할 수 있다.
즉, 인덱스가 주어지면 해당하는 값이 대응되는 자료구조이다.
C언어에서 6개의 정수를 저장할 수 있는 배열을 선언해보자.
배열은 변수 이름 끝에 [ ]을 추가해서 선언한다.
[ ] 안의 숫자는 배열의 크기이다.
int list[6]
위의 코드는, 다음과 같은 배열을 만들어낸다.
인덱스 | 0 | 1 | 2 | 3 | 4 | 5 |
값 |
값을 넣어보자.
list[2] = 100
인덱스 | 0 | 1 | 2 | 3 | 4 | 5 |
값 | 100 |
컴파일러는 배열을 어떻게 구현할까?
컴파일러는 배열에 메모리의 연속된 위치에 할당한다.
첫 번째 배열 요소인 list[0]의 주소가 기본주소가 되고, 다른 요소들의 주소는 다음과 같다.
list[i]의 주소= base+i*sizeof(int)
무슨 구조체 공부하는데 서론이 이렇게 긴가 할 수 있다.
지금까지 정리한 부분은 배열의 아주아주아주 기초적인 부분이다.
배열은, '같은 타입 데이터의 모임'이다.
근데 출석부같은 것만 봐도, '학번' - '이름' 이렇게 숫자, 문자가 대응되는데 '다른 타입 데이터의 모임'은 없는걸까?
그래서 필요한 것이 바로 구조체다.
구조체(structure)
앞서 말했듯 구조체는 타입이 다른 데이터를 묶는 방법이다.
구조체의 형식
구조체의 형식은 다음과 같이 정의한다.
struct 구조체이름 {
항목1;
항목2;
...
};
구조체의 형식이 위와 같이 정의되었다면 구조체 변수는 다음과 같이 생성한다.
struct 구조체이름 구조체변수;
간단한 예로 학생을 나타내는 구조체를 만들어보면 다음과 같다.
구조체에 저장되는 항목들은,
이름 : 문자배열
나이 : 정수값
평균평점 : 실수값
이 있다.
struct studentTag {
char name[10]; // 문자 배열로 된 이름
int age; // 나이를 나타내는 상수값
double gpa; // 평균평점을 나타내는 실수값
};
struct 키워드 다음에 오는 studentTag는 구조체와 구조체를 구별할 수 있게 해주는 식별자로서 보통 구조체 태그(Tag)라고 한다. 위의 문장은 구조체 형식만을 정의한 것이고 실제로 구조체가 만들어진 것은 아니다. 구조체를 만들려면 다음과 같이 해야한다.
struct studentTag s;
구조체 안에 들어 있는 멤버를 사용하려면 어떻게 할까?
구조체 변수 뒤에 '.'을 첨가한 후 항목 이름을 적으면 된다.
' . '을 멤버연산자(membership operator)라고 한다.
strcpy(s.name, "kim");
s.age = 20;
s.gpa = 4.3;
C언어에서는 typedef을 사용하여 구조체를 새로운 타입으로 선언하는 것이 가능하다. 아래의 에에서 student은 새로운 데이터 타입의 이름이 된다.
typedef studentTag {
char name[10]; // 문자 배열로 된 이름
int age; // 나이를 나타내는 상수값
double gpa; // 평균평점을 나타내는 실수값
} student;
이 경우에는 새로운 타입인 student만을 사용하여서 변수를 선언하는 것이 가능해진다.
student는 C에서의 기본 데이터 타입인 int나 float과 마찬가지로 새로운 데이터 타입의 이름이 된다.
student s;
구조체는 중괄호를 사용하여 선언 시에 초기화 하는 것이 가능하다.
student s = { "kim", 20, 4.3 };
실습문제
- 2차원 자표 공간에서 하나의 점을 나타내는 구조체 Point를 정의해보자.
- 1에서 정의한 구조체의 변수인 p1과 p2를 정의해보자.
- p1과 p2를 각각 (1,2)와 (9,8)로 초기화해보자.
- 점을 나타내는 두 개의 구조체 변수를 받아서 점 사이의 거리를 계산하는 함수 get_distance(Point p1, Point p2)를 작성해보자.
코드
#include <iostream>
#include <cmath>
using namespace std;
struct Point {
int x;
int y;
};
double get_distance(Point p1, Point p2) {
int distance, dis_x, dis_y;
dis_x = p2.x - p1.x;
dis_y = p2.y - p1.y;
distance = sqrt(pow(dis_x, 2) + pow(dis_y, 2));
return distance;
}
int main() {
Point p1 = { 1,2 };
Point p2 = { 9,8 };
cout << get_distance(p1, p2);
return 0;
}
실행결과
구조체의 캡슐화
객체지향프로그래밍은 캡슐화, 은닉 뭐 어쩌고저쩌고가 많다.
그 중 캡슐화에 대해 알아보자.
다음과 같은 코드를 짰다고 생각해보자.
#include <iostream>
using namespace std;
struct Position {
int x;
int y;
char ch;
};
void printPos(Position Pos) {
cout << "x = " << Pos.x << ", y = " << Pos.y << '\n';
cout << Pos.ch;
}
void main() {
Position Here = { 30, 50, 'A' };
printPos(Here);
}
여기서 Position 구조체와 printPos 함수는 상호 의존적인 관계에 있다.
서로가 없으면 의미가 없기 때문이다.
이 말은 즉, 이 구조체를 다른 프로그램에서 재사용하려 한다면 구조체와 함수를 같이 가지고 가야 하며, 둘 중 하나만 가지고 가면 아무 짝에도 쓸모가 없다는 소리다. 그럼 걍 합치면 안될까? 된다. 잘된다. 관련된 건 다 때려넣어서 합칠 수 있다.
이렇듯 C++은 연관된 코드와 데이터를 하나의 범위에 포함할 수 있는 방법을 제공하는데, 이 개념이 바로 캡슐화이다.
#include <iostream>
using namespace std;
struct Position {
int x;
int y;
char ch;
void printPos() {
cout << "x = " << x << ", y = " << y << '\n';
cout << ch;
}
};
void main() {
Position Here = { 30, 50, 'A' };
Here.printPos();
}
위의 코드는 구조체와 함수를 하나로 합친 것이다.
구조체에 포함된 함수는 멤버 함수라고 하고, 구조체에 포함된 변수는 멤버 변수라고 한다.
즉, C++에서 구조체는 멤버 변수와 멤버 함수로 구성된다.
함수가 구조체에 포함되면서 소스에서 달라진 부분들이 있다.
어떤 변화가 있는지 정리해보자.
1. 함수가 인수를 받아들일 필요가 없다.
void printPos()
일반 함수일 때는 어떤 구조체의 정보를 사용할 것인지를 인수로 전달 받아야 했지만,
구조체에 소속되었기 때문에 소속된 구조체의 정보를 사용하면 된다.
2. 함수 내부에서 멤버 변수를 참조할 때 소속 구조체를 밝힐 필요가 없어졌다.
cout << "x = " << x << " y = " << y
구조체 밖에 있을 때에는 어떤 구조체에 속한 멤버 변수인지를 밝혀야 했지만,
멤버함수는 별도의 지정없이 자신이 속해있는 구조체의 멤버변수를 이름만으로 엑세스 할 수 있다.
3. main에서 함수 호출 시 함수가 소속된 구조체 변수를 앞에 적어주었다.
Here.printPos();
이제 함수는 구조체에 속한 멤버가 되었으므로 어떤 구조체의 정보를 대상으로 동작할 것인지를 밝혀야한다.
멤버함수를 호출하는 방법은 멤버변수를 참조하는 것과 동일하다.
구조체.함수() 식으로 호출하며, 구조체 포인터라면 구조체->함수() 식으로 호출한다.
구조체가 멤버함수를 포함하면 스스로 동작할 수 있는 독립성이 부여된다.
독립성은 곧 재활용성으로 이어진다.
내부에 정보와 함수를 모두 포함하고 있으므로 이 구조체만 다른 프로젝트로 가져가면 쉽게 재사용 할 수 있다.
이것이 바로 OOP 캡슐화의 기본적인 개념이다. 변수든 함수든 논리적으로 관련된 것을 한 곳에 모아 묶어 놓음으로써 구조체가 프로그램의 부품 역할을 할 수 있게 된 것이다.
멤버함수 작성법
사실 위의 함수는 길이가 짧아서 구조체 선언문에 들어가도 괜찮았지만, 함수의 코드가 길고 이런 멤버 함수가 많다고 생각해보면, 다른 사람들이 봤을 때 이건 뭔가 싶을 것이다.
내 코드가 나 혼자만 이해할 수 있는 이기적인 코드가 아니라, 협업자들이 모두 잘 이해할 수 있는 좋은 코드가 되길 바란다면, 좀 더 보기 편하게 만들어주는 것이 어떨까.
이를 위해 C++은 구조체 선언문에 함수의 원형만 선언하고 본체는 구조체 바깥에 따로 작성하는 방법을 지원한다.
#include <iostream>
using namespace std;
struct Position {
int x;
int y;
char ch;
void printPos();
};
void Position::printPos() {
cout << "x = " << x << ", y = " << y << '\n';
cout << ch;
}
void main() {
Position Here = { 30, 50, 'A' };
Here.printPos();
}
구조체 선언문 안에는 함수의 원형만 밝혀 이 함수가 Position 소속임만을 알린다.
함수의 본체는 구조체 선언문 다음에 별도로 작성하되, 함수 정의문에 이 함수가 어떤 구조체의 멤버 함수인지를 밝혀야 한다.
멤버함수를 정의하는 기본 문법은 다음과 같다.
void Position::printpos(){ // 리턴타입 소속구조체 :: 멤버함수(인수)
}
액세스 지정
구조체 내부의 멤버를 외부에서 맘대로 건드릴 수 있게 냅두면 버그가 발생할 수 있어 위험하며 객체의 독립성도 떨어진다. 그래서 C++에서는 구조체(또는 클래스)의 멤버에 대한 외부의 참조를 허가할 것인지 금지할 것인지를 지정할 수 있다. 이것을 바로 액세스 지정이라고 한다.
액세스 지정의 방법에는 세 가지가 있다.
- private : 외부 액세스 불가. 구조체의 멤버함수만 액세스 가능, 외부에서는 private 멤버를 읽을 수 없음은 물론이고 존재 자체도 알려지지 않음. 파생 클래스조차 참조 불가. 오로지 자신만이 멤버 참조할 수 있음.
- public : 외부로 공개되어 누구나 읽고쓰고 호출 가능. 퍼블릭 멤버를 소위 인터페이스라 함.
- protected : private와 마찬가지로 외부에서는 액세스 불가하나 상속된 파생 클래스는 이 멤버를 액세스 할 수 있다.
예시)
#include <iostream>
using namespace std;
struct Student {
private: // 비공개 영역
double grd;
public: // 공개 영역
char name;
protected: // 보호 영역
int number;
};
void main() {
Student A;
A.name = 'A'; // 대입 가능
A.grd = 4.3; // 에러
A.number = 1; // 에러
}