variables provide named storage
every C++ variable has a type
a variable’s type determines its size and memory layout
bool flag = true;
int x = 3 + 4;
int y = 2 * x;
int z; // bad: the initial value of z is unknown
const double avg_temp = -22.1; // avg_temp can't be modified
double x; // compiler error: can't have two variables
// with the same name in the same scope
double char = 104.5; // compiler error: variables can't
// use reserved C++ words
char 4a = 'x'; // compiler error: variables can't start
// with a number
int a = 5;
int b(5); // notice this looks like a function call
int c{5};
int d = {5};
long double ld = 3.1415926536;
int a{ld}, b = {ld}; // error: narrowing conversion required
int c(ld), d = ld; // ok: but value will be truncated
not just variables have names: so do functions, classes, namespaces
there is an important difference between declaring a name and defining a name
a declaration makes a name known to a program
a definition creates the name’s associated entity
a name can be declared any number of times
but a name can only be defined once
int x = 5; // variable definition
// and also a variable declaration
this declares the name x, allocates memory to hold 1 int, and initializes it to 5
you can declare a variable without defining it using extern and not providing an initializer
extern int x; // x is declared, but not defined
x = 5; // compiler error: x is undefined
extern int y = 5; // y is defined and declared
this declares the name x, but does not allocate memory to hold an int
that will be done somewhere else in the program
extern is usually used in programs with multiple compilation units (i.e. multiple .cpp files)
it lets you share variables between compilation units
we won’t be using it in this course
C++ is lexically scoped
that means you can tell where a variable is usable by looking at { and } tokens in the source code
(it wasn’t until the late 1970s that this became the dominate scoping rule for almost all programming languages)
int main()
{
int x = 2;
cout << x << endl; // 2
// braces indicate a scope
{
int y = 3;
cout << y << endl; // 3
}
cout << x << endl; // 2
cout << y << endl; // compiler error: y is out of scope
}
int main()
{
int x = 2;
cout << x << endl; // 2
// braces indicate a scope
{
int x = 3; // legal but a bad idea
cout << x << endl; // 3
}
}
avoid declaring a variable in a nested scope with the same name as a variable in an outer scope
references let you give a value multiple names
int a = 7; // a refers to an int with the value 7
int b = a; // this makes a copy of a
// so b refers to a different int with the value 7
cout << a << ' ' << b << endl; // 7 7
a++;
cout << a << ' ' << b << endl; // 8 7
b++;
cout << a << ' ' << b << endl; // 8 8
references let you give a value multiple names
int a = 7;
int& b = a; // this makes b refer to the same memory that
// a refers to; so that memory location
// now has two names
cout << a << ' ' << b << endl; // 7 7
a++;
cout << a << ' ' << b << endl; // 8 8
b++;
cout << a << ' ' << b << endl; // 9 9
pointers store addresses
double s = 6.42;
double* p = &s; // & is the address-of operator
cout << "s is stored at address " << p
<< " and has the value " << *p
<< endl;
in general, the address of a variable changes from run to run, so its exact value is rarely useful
if p has type double*, we say it is a double pointer, or a pointer to a double
if p is a double pointer, then *p is the value of the memory it points to
*p is referred to as de-referencing a pointer
generally, for any C++ type T, the type of a pointer to a T is T*
T can be any type, even another pointer
so you can get stuff like int**, which is a pointer to an int pointer
double s = 6.42;
double* p = &s; // & is the address-of operator
double* q = p; // p and q point to the same memory
cout << *p << ' ' << *q << endl; // 6.42 6.42
(*p)++;
(*p)++;
cout << *p << ' ' << *q << endl; // 8.42 8.42
any pointer can be assigned the value nullptr
nullptr means the pointer is not pointing to any address
char* p = nullptr;
int* q = nullptr;
double* r = nullptr;
don’t use 0 or NULL for a null pointer!
de-referencing nullptr causes a run-time error
char* p = nullptr;
cout << *cp; // Segmentation fault (core dumped)
you can increment and decrement pointers
char a = 'a';
char* cp = &a; // cp points to the memory holding 'a'
++cp; // now cp points 1 byte past the memory holding 'a'
int i = 6;
int* ip = &i; // ip points to the memory holding 6
++ip; // now ip points sizeof(int) bytes past the memory holding 6
support for pointers is perhaps the distinguishing feature of C
they are surprisingly tricky to use correctly (even after you completely understand them); tools like valgrind are practically essential in C programming
most other languages put more restrictions on pointers than C/C++
C++ generalizes pointers to iterators in its standard template library (STL)
pointers are like chainsaws: powerful but dangerous
similar ideas
it’s easy to make complicated examples mixing pointers and references (among other things!)
so lets avoid that
we’ll almost always use references only for pass-by-reference parameter passing
a constant expression is an expression whose value cannot change and that can be evaluated at compile time
const double PI = 3.145926;
cout << PI << endl; // 3.145926
PI = 3.14; // compiler error: can't change PI's value
const int max_files = 20; // max_files is a constant expression
const int limit = max_files + 1; // limit is a constant expression
int staff_size = 27; // staff_size is not a constant expression
const int sz = get_size(); // sz is not a constant expression
constexpr int mf = 20; // 20 is a constant expression
constexpr int limit = mf + 1; // mf + 1 is a constant expression
constexpr int sz = size(); // ok only if size is a constexpr function
typedef makes an alias (synonym) for a type
typedef double real;
typedef real* real_ptr;
int main() {
real r = 5.5;
cout << r << endl; // 5.5
}
real and real_ptr are not new types
they are just different names for double and double*
using is another (more recent) way to make a type alias
using real = double;
using real_ptr = real*;
int main() {
real r = 5.5;
cout << r << endl; // 5.5
}
real and real_ptr are not new types
they are just different names for double and double*
auto can infer the type of a variable from the type of its initializer
auto a = 5; // a is an int because 5 is an int literal
auto b = '5'; // b is a char
auto c = 5; // c is a double
cout << a << ' ' << sizeof(a) << endl // 5
<< b << ' ' << sizeof(b) << endl // 5
<< c << ' ' << sizeof(c) << endl; // 5
}
this is most useful when dealing with complex type names
struct (and class) lets you create your own data types
struct point2d {
double x;
double y;
};
int main() {
point2d p = {-2.3, 4.4};
cout << p.x << ' ' << p.y << endl; // -2.3 4.4
p.x = 2;
p.y = 15.001;
cout << p.x << ' ' << p.y << endl; // 2 15.001
}
you can provide initial values for members of a struct
struct point2d {
double x = 1.0;
double y = 2.0;
};
int main() {
point2d origin;
cout << p.x << ' ' << p.y << endl; // 1.0 2.0
}
we’ll talk more about creating types when we discuss object-oriented programming (OOP)