02 - member functions

Classes, just like member variables (fields) can also contain member functions (methods).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <iostream>

// (greatest common divisor)
// if you have C++17, you can remove this function and use std::gcd from <numeric>
int gcd(int a, int b)
{
	if (b == 0)
		return a;
	else
		return gcd(b, a % b);
}

class fraction
{
public:
	int numerator;
	int denominator;

	void simplify()
	{
		const int n = gcd(numerator, denominator);
		numerator /= n;
		denominator /= n;
	}

	void print()
	{
		std::cout << numerator << "/" << denominator << "\n";
	}
};

int main()
{
	fraction fr1{2, 6};
	fraction fr2{5, 10};

	fr1.print();
	fr2.print();
	fr1.simplify();
	fr2.simplify();
	fr1.print();
	fr2.print();
}

Output:

2/6
5/10
1/3
1/2

Notice few things:

  • Member functions, just like member variables are defined inside the class.

  • Member functions have innate ability to access member variables - as if all member variables were always passed as arguments.

  • Because member functions are tied to a specific class, they can not be called like global functions. They need to be called on a specific object, and this is done using . operator.

If you would like to split the code above into files:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// header
class fraction
{
public:
	int numerator;
	int denominator;

	void simplify();
	void print();
};

// source
// notice that member function definitions use class_name::
// this informs the compiler about access to members
void fraction::simplify()
{
	const int n = gcd(numerator, denominator);
	numerator /= n;
	denominator /= n;
}

void fraction::print()
{
	std::cout << numerator << "/" << denominator << "\n";
}

For simplicity, since majority of examples in the tutorial are single-file they will contain function definitions inside the class definition.

Invariants

Thanks to member functions, the code is now more object-oriented but it still contains some flaws. What happens if we do something like this?

1
2
3
4
5
6
fr1.numerator = 2;
fr1.denominator = 0;
fr2.numerator = 0;
fr2.denominator = 0;
fr1.simplify(); // (1)
fr2.simplify(); // (2)
Answer
  • For (1), the fraction will be simplified to 1/0 because gcd(2, 0) == 1.

  • For (2), the code will invoke undefined behavior because gcd(0, 0) == 0 and integer division by 0 is undefined.

In both cases, the intial problem is that fraction invariant was broken - the denominator can not be zero. No matter how well member functions treat the data, we can always use the . operator to access and modify class members, potentially breaking some conditions on which the methods operate.

Can we protect the code from breaking invariants? Yes. For this, we will need to restrict the access to some class members.