Skip to content

Instantly share code, notes, and snippets.

@kevr
Last active January 31, 2017 06:42
Show Gist options
  • Save kevr/37895aa938c1757ab1a28883e0481d97 to your computer and use it in GitHub Desktop.
Save kevr/37895aa938c1757ab1a28883e0481d97 to your computer and use it in GitHub Desktop.
Learn C++ classes

C++ Classes

Introduction

When writing classes in a programming language, we can think of them as a direct correlation to nouns in the English language. For example, I have a dog named Spirit. We want to represent Spirit in code, so we write a class to represent a dog.

class Dog
{
};

Member Variables

Alright. We have a dog. What kind of information do we want to know about the dog at this point? As the programmer, all I care about is the name of the dog at this point. Anything I care about knowing about the class I'm writing will be member variables inside of that class.

In this case, all I care about is the dog's name. So our class must have a member variable called name.

In C++, we're required to give a type to a variable. In some languages, like python, this doesn't matter.

The type I choose to represent a name for a dog is an std::string.

class Dog
{
private:
    std::string name;
};

Alright, now we have a Dog that has a name member variable. You may notice a private: in the code above. In C++, classes have three types of member variables or member functions: private, public, and protected. These keywords control the access permissions of data or functions in your class.

private members can only be accessed internally by the class functions.

public members can be accessed by anything; internally or externally -- these are members that are exposed to the user of your class (in this case, it's us, but in the real world, you might be writing a class that's used by a variety of different programmers).

protected members can be accessed internally or by classes derived from them.

Since protected members require a bit more knowledge about classes, we won't use them now, but remember that they exist.

Member Functions

Okay, so we've added a private member variable to track our dog's name. That's good, but we can't do anything with that externally. This style of writing classes is used to obscure the implementation (create an abstraction).

The reason for this may not be obvious to you. This style of programming greatly simplifies the object when it's being used by the user of the class. Personally, I usually name public member functions after actions (if they're mutable) or nouns (if they're immutable).

mutable (adj): Modifiable; able to be modified.
immutable (adj): Not mutable.

// Mutable examples
void setName(std::string name);

Immutable functions generally return some constant calculation or member variable so user's of the class can, for example, get the dog's name.

/// Immutable examples
const std::string& name();

When writing a class, it's a good idea to visualize the user using an object of the class you're writing. For example, it's much more natural, as the user, to get the dog's name with dog.name() as opposed to an action like dog.getName(). This is purely stylistic though, but it's a widely used methodology.

class Dog
{
private: /* Private member variables */
	// We give the internal variable a p prefix
	// to denote that it's a private variable
	std::string pName;

public: /* Public member functions */
	// Set a name and return it at the same time
	// (Could just call setName and getName)
	const std::string& name(std::string name)
	{
		setName(name);
		return getName();
	}
	
	// Just set a name
	void setName(std::string name)
	{
		pName = name;
	}
	
	// Just get a constant internal name
	const std::string& getName() const
	{
		return pName;
	}
	// Just get a constant internal name
	// (same as getName)
	const std::string& name() const
	{
		return pName;
	}
};

Cool. Now we have a class designed that can actually set and get the dog's name.

Let's use a Dog object as a user now.

int main(int argc, char *argv[])
{
	Dog dog; // Construct a dog object
	dog.setName("Spirit"); // Set it's name to Spirit
	
	// Print out the dog's name
	std::cout << dog.name() << endl;

    return 0;
} 
// Console output
Spirit

Some brief explanations about the const keyword. const is a promise in C++ that you're not changing the value of the const variable. This provides both practical and mental benefits. Mentally, as a programmer, when you see a const variable, you know that it's something that isn't supposed to be changed.

const can also be used as a function specification keyword. In the code above, you see name() and getName() is defined with a trailing const keyword. This labels a member function constant, and const member functions are the only functions that can be used on a const object of your class.

int main(int argc, char *argv[])
{
	const Dog dog;
	dog.setName("Spirit"); // Error, will not compile
	std::cout << dog.name() << std::endl; // Works, will compile

	Dog mutableDog;
	mutableDog.setName("Not const");
	std::cout << mutableDog.name() << std::endl;
}

The const keyword also promises the compiler that the value won't be changed, so the compiler can choose to optimize certain things about your const variables and functions. As you'll explore later, templates parameters are all const, and provide you a way to calculate constant values and types at compile time, building logic into your executable, as opposed to figuring everything out at run time like many interpreter languages do. You'll learn about templates later, but for now, let's extend the functionality of our Dog class.

Also, it reduces the ability for an external programmer to introduce bugs into your classes by making accessors immutable.

Constructors

One of the strongest features of C++ is Resource Acquisition Is Initialization (RAII). RAII is a paradigm where you control how an object is born and how it dies. Commonly when you're creating an object of some type, there is some sort of initialization step that needs to occur for the object to mean anything. This may be something like setting a dog's name. After all, without a name, it's tougher to get a dog's attention.

RAII in C++ is handled by two components of the language: constructors and destructors. They do just what they sound like; a constructor constructs an object (the initialization stage) and a destructor destroys an object (the tear-down stage).

RAII is also very strong because you can dynamically allocate memory during construction, and ensure that you free that memory when your object is being destroyed.

The naming convention for declaring a constructor of some class is...

class Class
{
public:
	Class(); // Constructor
};

Just like any other function, a constructor can be overloaded (or specialized). Let's say you wanted a default setting if nothing was specified, but you wanted to set the dog's name if it's given to the constructor.

class Dog
{
private:
	/* ... */
	
public:
    Dog()
	{
		pName = "Unnamed";
	}

	Dog(std::string name)
	{
		pName = name;
	}

	/* ... */
};

Now we can create a dog with a given name if we want to, which again, is much more natural when you're creating some representative object that has a name to use in code. It just makes more sense.

int main(int argc, char *argv[])
{
	// Call Dog(std::string name) constructor
	// (constructors only work on initialization)
	Dog dog("Spirit");
	
	// Print out "Spirit"
	std::cout << dog.name() << std::endl;

	// Call Dog() constructor
	Dog otherDog;
	
	// Print out "Unnamed"
	std::cout << otherDog.name() << std::endl;
	
	return 0;
}	

As you can see, this makes a lot more sense than the previous alternative (to explicitly set it after initialization). This is because it's just more natural to humans to already have an object with the qualities that's unique to it, as opposed to changing it to have those qualities down the line.

This type of mutation is still needed however. Sometimes you need to change values; what if your dog is a puppy and you changed it's name after a few days?

Keep constructors in mind, because they make objects more representative of real life objective nouns and are much easier to reason with.

"This object is a dog named Spirit" vs "This object is a dog and his name is Spirit"

Destructors

Just like the opposite of a constructor, a destructor is executed during the destruction phase of your object (when you call delete on it, or it goes out of scope). There can only be one destructor per object, since only one destruction phase occurs.

class Class
{
public:
	~Class(); // Destructor
};

Let's define a dummy destructor for Dog and check out it's effects.

class Dog
{
private:
	/* ... */
	
public:
	/* ... */
	
	~Dog()
	{
		std::cout << pName << " is being destroyed" << std::endl;
	}

	/* ... */

Now let's run some different scoping and initialization tests to see what happens.

int main(int argc, char *argv[])
{
	Dog dog("Spirit");
	std::cout << dog.name() << endl;

	// This is the end of the main function,
	// so dog goes out of scope and is destroyed.
	return 0;
}
// Console output
Spirit
Spirit is being destroyed

Let's call another function inside main.

void foo()
{
	Dog dog("Foo");
	std::cout << dog.name() << std::endl;
}

int main(int argc, char *argv[])
{
	foo(); // foo() call is done here (end of foo scope)
	std::cout << "End of Main!" << std::endl;
	return 0;
}
// Console output
Foo
Foo is being destroyed
End of Main!

Scope is a very important topic in general programming. Scope is generally defined by curly-brace code blocks. This differs for language, but in general, scope extends for the life of it's execution body.

void foo()
{
	std::cout << "A" << std::endl;
	std::cout << "B" << std::endl;
}

Here, the length of foo's scope begins immediately before A is printed, and immediately after B is printed. When objects go out of scope, they are destroyed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment