Structures allow developers to create their own types ("user-defined" types) to aggregate data relevant to their needs.
Member Initialization
In order to ensure that objects of our Date
structure always start in a valid state, we can initialize the members from within the structure definition.
struct Date {
int day{1};
int month{1};
int year{0};
};
Access Specifiers
Members of a structure can be specified as public
or private
.
By default, all members of a structure are public, unless they are specifically marked private
.
Accessors And Mutators
To access private members, we typically define public "accessor" and "mutator" member functions (sometimes called "getter" and "setter" functions).
struct Date {
public:
int Day() { return day; }
void Day(int day) { this.day = day; }
int Month() { return month; }
void Month(int month) { this.month = month; }
int Year() { return year; }
void Year(int year) { this.year = year; }
private:
int day{1};
int month{1};
int year{0};
};
Avoid Trivial Getters And Setters
Sometimes accessors are not necessary, or even advisable. The C++ Core Guidelines recommend, "A trivial getter or setter adds no semantic value; the data item could just as well be public."
- This
class
could be made into astruct
, with no logic or "invariants", just passive data. The member variables could both be public, with no accessor functions:
struct Point { // Good: concise
int x {0}; // public member variable with a default initializer of 0
int y {0}; // public member variable with a default initializer of 0
};
By convention, programmers use structures when member variables are independent of each other, and use classes when member variables are related by an "invariant".
Class
:- Members are private by default.
- Use it when you want to add in an "invariant", a rule that limits the values of member variables.
Struct
: Members are public by default.
An operation that initializes (“constructs”) an object. Typically a constructor establishes an invariant and often acquires resources needed for an object to be used (which are then typically released by a destructor).
The compiler will define a default constructor, which accepts no arguments, for any class or structure that does not contain an explicitly-defined constructor.
::
is the scope resolution operator. We can use this operator to specify which namespace or class to search in order to resolve an identifier.
Person::move(); \\ Call the move the function that is a member of the Person class.
std::map m; \\ Initialize the map container from the C++ Standard Library.
Namespaces
Namespaces allow programmers to group logically related variables and functions together. Namespaces also help to avoid conflicts between to variables that have the same name in different parts of a program.
namespace English {
void Hello() { std::cout << "Hello, World!\n"; }
} // namespace English
namespace Spanish {
void Hello() { std::cout << "Hola, Mundo!\n"; }
} // namespace Spanish
int main() {
English::Hello();
Spanish::Hello();
}
Initializer lists initialize member variables to specific values, just before the class constructor runs.
Date::Date(int day, int month, int year) : year_(y) { // initialize list
Day(day); // allows us to apply the invariants set in the mutator
Month(month); // allows us to apply the invariants set in the mutator
}
Advantages of initializer lists
- Initializer lists exist for a number of reasons. First, the compiler can optimize initialization faster from an initialization list than from within the constructor.
- A second reason is a bit of a technical paradox. If you have a
const
class attribute, you can only initialize it using an initialization list. Otherwise, you would violate theconst
keyword simply by initializing the member in the constructor! - The third reason is that attributes defined as references must use initialization lists.
Initializing Constant Members (Must use initializer lists)
struct Person {
public:
Person(std::string const& name) : name(name) {}
std::string const name;
};
// Test
int main() {
Person alice("Alice");
Person bob("Bob");
assert(alice.name != bob.name);
}
Encapsulation is the grouping together of data and logic into a single unit.
#include <string>
#include <cstring>
#include <iostream>
class Car {
// TODO: Declare private attributes
private:
int horse_power;
int weight;
char *brand;
// TODO: Declare getter and setter for brand
public:
void SetBrand(std::string name);
std::string GetBrand() const;
};
// Define setters
void Car::SetBrand(std::string name) {
// Initialization of char array
Car::brand = new char[name.length() + 1];
// copying every character from string to char array;
strcpy(Car::brand, name.c_str());
}
// Define getters
std::string Car::GetBrand() const {
return Car::brand;
}
// Test in main()
int main()
{
Car car;
car.SetBrand("Peugeot");
std::cout << car.GetBrand() << "\n";
}
Static Members
Class members can be declared static
, which means that the member belongs to the entire class, instead of to a specific instance of the class. More specifically, a static
member is created only once and then shared by all instances (i.e. objects) of the class. That means that if the static
member gets changed, either by a user of the class or within a member function of the class itself, then all members of the class will see that change the next time they access the static
member.
static
members are declared within their class
(often in a header file) but in most cases they must be defined within the global scope. That's because memory is allocated for static
variables immediately when the program begins, at the same time any global variables are initialized.
#include <cassert>
class Foo {
public:
static int count;
Foo() { Foo::count += 1; }
};
int Foo::count{0};
int main() {
Foo f{};
assert(Foo::count == 1);
}
An exception to the global definition of static
members is if such members can be marked as constexpr
. In that case, the static
member variable can be both declared and defined within the class
definition:
struct Kilometer {
static constexpr int meters{1000};
};