< 목차>
8.1 Technicalities
8.2 Declarations and definitions
8.2.1 Kinds of declarations
8.2.2 Variable and constant declarations
8.2.3 Default initialization
8.3 Header files
8.4 Scope
8.5 Function call and return
8.5.1 Declaring arguments and return type
8.5.2 Returning a value
8.5.3 Pass-by-value
8.5.4 Pass-by-const-reference
8.5.5 Pass-by-reference
8.5.6 Pass-by-value vs Pass-by-reference
8.5.7 Argument checking and conversion
8.5.8 Function call implementation
8.5.9 constexpr functions
8.6 Order of evaluation
8.6.1 Expression evaluation
8.6.2 Global initialization
8.7 Namespaces
8.7.1 using declarations and using directives
8.1 Technicalities
생략.
8.2 Declarations and definitions (선언과 정의)
declaration 은 이름을 소개하는것이다.
우리는 항상 사용하기전에 declaration 해야한다.
declaration 은 여러 위치에 올수있다.
실제 우리가 만드는 declaration 의 대부분은 header 파일안에 존재할것이다.
declaration 은 3가지가 될수있다.
- interface of function
- variable
- class
예를 보자.
#include "iostream" // declaration 이 여기에 들어가있음
int f(int); // declaration of f
int main() {
int i = 7; // declaration of i;
std::cout << f(i) << std::endl;
}
위의 코드는 compile 되지만, link-error 가 날것이다.
왜냐면 f(int) 의 definition 이 없기때문이다.
- declaration없으면 : compile-time-error
- definition 없으면 : link-time-error
definition 의 예이다.
int a = 7;
vector<double> v;
double myFunction(double d) { }
모든 definition 은 결과적으로 declaration 이다.
하지만 모든 declaration 이 definition 인것은 아니다.
예를 보자.
double myFunction(double);
extern int a; // "extern" 후에 초기화가 없으면 "not definition" 을 의미한다
definition 을 2번 할수없다.
예를 보자.
double myFunction(double) {} // definition
double myFunction(double) {} // error : double definition
int a; // definition
int a; // error : double definition
definition이 아닌 declaration은 memory 에 allocate 되지않는다.
또한, definition 과 달리, 원하는 만큼 여러번 자주 declare 할수있다.
예제.
int x = 7; // definition
extern int x; // declaration
extern int x; // another delcaration
double myFunction(double); // declaration
double myFunction(double d) { } // definition
double myFunction(double); // another delcaration
double myFunction(double); // another delcaration
int myFunction(double); // error : inconsistent declarations.
마지막 줄이 error 인 이유는, 동일한 이름과 argument type 이 같은 함수인데, return-type 이 달라서이다.
extern keyword 는 definition 이 아닌 declaration 을 의미할때 variable 앞에 붙인다.
하지만 사용하지않기를 권장한다. (global variables 과 연관성이 있어서.)
그림으로 마지막으로 개념 정리.
함수에서는, 아래와 같이 부르기도한다.
- declaration : interface
- definition : implementation
항상 declaration 이 사용전에 이루어져야하지만, class member 는 예외이다. (추후 나옴)
언제가 "declare before use!"
마지막 예.
double hero();
double other() {
hero();
}
double another() {
other();
}
double hero() {
another();
}
8.2.1 Kinds of declarations (declaration 의 종류)
- Variable
- Constant
- Function (8.5 에서 다룸)
- Namespace (8.7 에서 다룸)
- Class (9 에서 다룸)
- Enumeration (9에서 다룸)
- Template (19 에서 다룸)
8.2.2 Variable and constant declarations
variable 은 선언후 초기화 해주지않아도 되지만,
constant 는 선언후 반드시 초기화해야한다. 안그러면 error!
int a; // OK : 초기화 안함
const int b; // error : 초기화해야함! (no initializer)
하지만 초기화되지않은 variable 은 bug 가 생기기 쉽다.
따라서 variable 또한 항상 초기화 하는 습관을 들이도록한다.
variable 을 초기화할때는 = assignment 보다 { } uniform 초기화를 하도록한다.
{ } 초기화는, 아래와 같은 code 를 compile 되지않게 막아준다.
int a { 3. 2}; // error : conversion from 'double' to 'int' requires a narrowing conversion
8.2.3 Default initialization
가끔 우리는 초기화하지않은것같은 코드를 보는데 사실은 초기화를 한것이다.
예를 보자.
vector<string> v; // empty vector 도 vector 다.
string s; // " " 로 초기화된다.
위와 같이 code 를 작성하면, vector나 string 의 경우 default 초기화를 한다.
vector 는 empty vector 가 되고, string 은 " " 로 초기화된다.
default 초기화를 보장하는 이 메카니즘은 default constructor 라고 부른다. (추후 나옴)
하지만 모든 type 의 variable 이 default 초기화 되는게 아니다!
우리가 언제나 직접 초기화 해주어야하는 녀석들은 아래와 같다.
- built-in types
- local variables
- class member
위의 녀석들은, 우리가 직접 초기화를 하거나, default constructor 를 제공해줘야한다.
8.3 Header files
header file 은 declaration 의 모음이라고 볼수있다.
그림을 보자.
#include 는 우리가 적힌 파일속에 include 된 파일의 모든것을 복사한다.
#include 가 실행되는 과정을 pre-processing 이라고 하며, compile 전에 시행된다.
동일한 .h 파일이 여러 .cpp 파일에 여러번 include 될수있다.
8.4 Scope (범위)
scope 의 종류는 아래와 같다.
- global scope : 모든 scope 에 가장 바깥.
- namespace scope
- class scope : class 속
- local scope : block 의 { } 속, 또는 함수의 () 속 argument
- statement scope : for-loop 속같은것들
우리는 global scope 를 피하고, 언제나 local scope 를 추구해야한다. (locality)
scope 관련 good practice 와 bad practice 의 예를 보자.
1. 일반적인 class (Good!!) : 가장 일반적이고 좋은 형태이다.
class C {
public :
void f(); // declaration
}
void C::f() {
} // definition
2. 중첩 class (bad)
class C{
public :
struct M { };
class N {};
}
class 안에 class 가 member class 로 있는 경우인데, 복잡한 class 가 아니면 권장하지않는다.
class 는 항상 작고 simple 해야한다.
3. 지역 class (local class) (No!!!) : 절대 하지말자.
void f() {
class L { };
}
4. 지역 함수 (local function) (No!!! Compile 자체가 안됨. illegal 임)
void f() {
void g() { // illegal
}
}
5. 중첩된 block (nested block) : 중첩 block 은 피할수없지만, 너무 깊은 nesting 은 피할려고 노력해야한다. (에러가 숨어산다)
void f(int x, int y) {
if (x > y) {
}
else {
{ }
}
}
scope 를 위해 namespace 란 기능이있는데, 8.7 에서 다룬다.
8.5 Function call and return
C++ 에서 함수는 argument 와 return 에 대해 배우는것이 가장 중요한듯하다.
다른 언어에서는 볼수없는것들이 많이 나옴.
let's go.
8.5.1 Declaring arguments and return type
함수의 declaration 은 ; 로 끝나고, definition 은 { } 로 끝나야한다.
int f(int a); // 선언 - declaration
int f(int a) { } // 정의 - definition
return 하고싶지않을때는 void 함수를 사용한다.
void 는 return nothing 이라는 뜻이다.
void f();
함수의 declaration 의 arguments 에서 type 을 적고 name 은 적지않아도 된다.
void f(int, string, double);
하지만, 함수의 definition 에서는 일반적으로 name 을 적는다. (우리가 함수 내부에서 사용할거라서)
int show(int myNumber) {
std::cout << myNumber << std::endl;
}
선언된 함수를 구현할때, 특정 argument 을 사용하지않을수도있다.
그럴때는 name 을 생략하는것이 performance 측면에서 좋다.
(Juce 에서 virtual 를 override 할때를 생각해보라.)
int f(int myNumber);
int f(int) {};
8.5.2 Returning a value
함수의 type 과 return 된 type 이 같아야한다는 당연한 이야기.
대부분 아는 내용이고... 하나 유용한 이야기.
void 함수에서도 return 을 return 값 없이 사용하는 경우가 있는데,
함수 실행을 멈추고 빠져나오라는 용도이다.
return; 으로 사용한다.
void print(string quit) {
if (quit) return;
}
8.5.3 Pass-by-value
8.5.4 Pass-by-const-reference
8.5.5 Pass-by-reference
8.5.6 Pass-by-value vs Pass-by-reference
이 부분은 매우 매우 중요하고, 언제나 헤깔린다.
위의 4개의 chapter 를 한꺼번에 정리하겠음.
일단 가장 중요한 사용 규칙을 외우자.
- pass-by-value : 작은 객체를 copy할때 사용한다. ex) int, double
- pass-by-const-reference : 객체가 커서 copy 를 원하지않고, modify 하지않을때 사용한다. ex) vector, container
- reference 를 통해 수정하려하지말고, 결과를 return 하는쪽으로 코드를 작성하라.
- pass-by-reference : 큰 객체를 수정하려고할때만 사용한다. (조심해서 써야함) ex) vector, container
pass-by-reference 를 일반적으로 피해야하나, vector 같은 container 수정시에는 사실상 불가피하다.
마지막으로 literal 에 대한 중요한점을 이야기한다.
- literal 은 lvalue 가 없다.
- literal 은 const-reference 로 사용할수있다.
- literal 은 non-const-reference 로 사용할수없다!
- non-const-reference 로는, lvalue 가 있는 객체가 넘겨져야한다.
예를 본다.
void f(int a, int&b, const int& c) {}
int main() {
int x = 0;
int y = 0;
int z = 0;
f(x, y, z); // 가능
f(1, 2,3); // error : int&b 즉, non-const-refernce는 반드시 객체로 넘겨져야한다.
f(1, y, 3); // OK : const int& c 즉, const-reference 는 literal 이 될수있다.
}
const-reference 가 literal 이 될수있는 이유는,
compile 과정에서, literal 를 compiler 가 내부적으로 temporary object (일시적인 객체) 로 conversion (변환) 한다.
이 부분은, 개발하면서 늘 헷갈린 부분이라, 추후에 생각이 잘 안나면 반드시
책 원본을 정독하자.
8.5.7 Argument checking and conversion
int 를 넘겨야할곳에 double 을 넘기는것은 좋은 생각이 아니다.
이럴때 static_cast 를 사용하도록 한다.
함수 내부에서의 static_cast
void f(double d) {
int x = static_cast<int>(d);
}
함수 인자에서 static_cast
void f(double x);
void main() {
int x = 1;
f(static_cast<int>(x));
}
8.5.8 Function call implementation
stack, call stack, activation record 라는 용어가 등장한다.
어떻게 함수가 stack 에 저장되는가에 대한 이야기인데, 몰라도되고 알면 좋다고한다.
추후 궁금하면 다시 읽어볼것.
8.5.9 constexpr functions
함수의 계산을 compile-time 에서 미리 해놓을수있는 constexpr 함수가 있다.
constexpr 함수를 사용하는 이유는, 답이 바뀌지않는 똑같은 계산을 run-time 에서 몇백만번 다시 하고싶지않기때문이다.
constexpr 이 앞에 붙어있는 함수라고 해도, constant 가 필요한곳에서 호출되기전까지는 일반 함수처럼 작동한다.
constexpr 이 적힌 함수를 사용한 곳곳에 있는 계산을 compile-time 에서 미리 완료한다.
예를 보자!
struct Point {
double x;
double y;
}
constexpr double xScale = 10;
constexpr double yScale = 0.8;
constexpr Point scale(Point p) {
return { xScale * p.x, yScale* p.y};
}
void user(Point p1) {
Point p2 {10, 10);
constexpr Point p3 = scale(p1); // error : p1 is not a constant.
constexpr Point p4 = scale(p2); // OK ! p2 의 값을 알고있으므로, compile-time 에서 계산 가능.
}
constexpr 함수는 void 와 사용될수없고, 반드시 return 값만 있는 단순한 함수여야한다.
compiler 가 보기에 constexpr 함수가 충분히 단순하다 (simple enough) 고 보여지지않으면,
함수가 error 가 있는 함수로 간주하고 compile 되지앟는다.
한 마디로 return 값만 있는 함수여야한다.
constexpr int f(int a) {
return a;
}
C++14 에서는 간단한 loop 도 constexpr 함수내에서 실행할수있다고 하는데, 추후 검색해보도록한다.
8.6 Order of evaluation
객체의 생성 순서와 파괴 순서는 반대이다.
1,2,3 으로 생성됐으면, 파괴는 3,2,1 이다.
local 객체는 scope 를 진입하면 생성되고, 벗어나면 파괴된다.
8.6.1 Expression evaluation
표현식의 순서는, compiler 에 의해 정해져서, 우리가 생각한대로 흘러가지않을때가 있다.
이것을 피하기위해 절대 같은 표현식을 여러번 읽거나 쓰면 안된다!
아래와 같은 코드를 피하라.
v[i] = ++i;
v[++i] = i;
int x = ++i;
++i 가 여러번 사용되고 있다. 이와같은 연산은,
컴퓨터마다 다른 결과를 낼 가능성이 있다. (그냥 하지마라. just don't do it)
lvalue 의 연산이 rvalue 의 연산보다 우선적으로 된다는 보장이 절대 없다.
v[++i] = i 가 undefined 가 될수도있다.
8.6.2 Global initialization
global variable 은 무조건 피해야한다.
global variable 의 문제는, 함수의 호출보다 먼저 초기화된다는 보장이 없는것이다.
static 을 사용하여, static 으로 생성된 객체를 return 하는 방법을 사용한다.
static 으로 생성된 객체는 함수가 최초 호출 되었을때 단 한번만 생성된다.
// Date 를 copy 하고싶지않기때문에, refernece 로 return 한다.
const Date& myYear() {
static const Date d(1970, 1, 1); // static 객체는 함수의 첫 호출때 한번만 생성된다
return d;
}
L) static 이 bad practice 라는 이야기들이 있어서, 이부분은 조금 더 찾아보고 사용하도록하자.
8.7 Namespaces
별거없음.
예제.
namespace GraphicLibrary {
struct Color { };
struct shape{ };
struct Line : Shape { };
struct Function : Shape { };
struct Test : Shape { };
int guiMain() { }
}
8.7.1 using declarations and using directives
using 에 대한 이야기. 설명은 다 해주고
결론은 "using 쓰지마라!"
Review
- What is the difference between a declaration and a definition?
- How do we syntactically distinguish between a function declaration and a function definition?
- How do we syntactically distinguish between a variable declaration and a variable definition?
- Why can't you use the functions without declaring them first?
- Is int a; a definition or just a declaration?
- Why is it a good idea to initialize variable as they are declared?
- What can a function declaration consist of?
- What good does indentation do?
- What are header files used for?
- What is the scope of a declaration?
- What kinds of scope are there? Give an example of each.
- What is the difference between a class scope and local scope?
- Why should a programmer minimize the number of global variables?
- What is the difference between pass-by-value and pass-by-reference?
- What is the difference between pass-by-reference and pass-by-const-reference?
- What is a swap()?
- Would you ever define a function with a vector<double>-by-value parameter?
- Give an example of undefined order of evaluation. Why can undefined order of evaluation be a problem?
- What do x&&y and x||y, respectively, mean?
- Which of the following is standard-conforming C++ : functions within functions, functions within classes, classes within classes, classes within functions?
- What goes into an activation record?
- What is a call stack and why do we need one?
- What is the purpose of a namespace?
- How does a namespace differ from a class?
- What is a using declaration?
- Why should you avoid using directives in a header?
- What is namespace std?
Terms
- activation record
- argument
- argument passing
- call stack
- class scope
- const
- constexpr
- declaration
- definition
- extern
- forward declaration
- function
- function definition
- global scope
- header file
- initializer
- local scope
- namespace
- namespace scope
- nested block
- parameter
- pass-by-const-reference
- pass-by-reference
- pass-by-value
- recursion
- return
- return value
- scope
- statement scope
- technicalities
- undeclared identifier
- using declaration
- using directive