Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
CPlusPlusNotesForProfessionals.pdf
Скачиваний:
47
Добавлен:
20.05.2023
Размер:
5.11 Mб
Скачать

Chapter 41: Non-Static Member Functions

Section 41.1: Non-static Member Functions

A class or struct can have member functions as well as member variables. These functions have syntax mostly similar to standalone functions, and can be defined either inside or outside the class definition; if defined outside the class definition, the function's name is prefixed with the class' name and the scope (::) operator.

class CL { public:

void definedInside() {} void definedOutside();

};

void CL::definedOutside() {}

These functions are called on an instance (or reference to an instance) of the class with the dot (.) operator, or a pointer to an instance with the arrow (->) operator, and each call is tied to the instance the function was called on; when a member function is called on an instance, it has access to all of that instance's fields (through the this pointer), but can only access other instances' fields if those instances are supplied as parameters.

struct ST {

ST(const std::string& ss = "Wolf", int ii = 359) : s(ss), i(ii) { }

int get_i() const { return i; }

bool compare_i(const ST& other) const { return (i == other.i); }

private: std::string s; int i;

};

ST st1;

ST st2("Species", 8472);

int i = st1.get_i(); // Can access st1.i, but not st2.i. bool b = st1.compare_i(st2); // Can access st1 & st2.

These functions are allowed to access member variables and/or other member functions, regardless of either the variable or function's access modifiers. They can also be written out-of-order, accessing member variables and/or calling member functions declared before them, as the entire class definition must be parsed before the compiler can begin to compile a class.

class Access { public:

Access(int i_ = 8088, int j_ = 8086, int k_ = 6502) : i(i_), j(j_), k(k_) {}

int i;

int get_k() const { return k; }

bool private_no_more() const { return i_be_private(); } protected:

int j;

int get_i() const { return i; } private:

int k;

int get_j() const { return j; }

bool i_be_private() const { return ((i > j) && (k < j)); }

};

GoalKicker.com – C++ Notes for Professionals

228

Section 41.2: Encapsulation

A common use of member functions is for encapsulation, using an accessor (commonly known as a getter) and a mutator (commonly known as a setter) instead of accessing fields directly.

class Encapsulator { int encapsulated;

public:

int get_encapsulated() const { return encapsulated; } void set_encapsulated(int e) { encapsulated = e; }

void some_func() { do_something_with(encapsulated);

}

};

Inside the class, encapsulated can be freely accessed by any non-static member function; outside the class, access to it is regulated by member functions, using get_encapsulated() to read it and set_encapsulated() to modify it. This prevents unintentional modifications to the variable, as separate functions are used to read and write it. [There are many discussions on whether getters and setters provide or break encapsulation, with good arguments for both claims; such heated debate is outside the scope of this example.]

Section 41.3: Name Hiding & Importing

When a base class provides a set of overloaded functions, and a derived class adds another overload to the set, this hides all of the overloads provided by the base class.

struct HiddenBase {

void f(int) { std::cout << "int" << std::endl; } void f(bool) { std::cout << "bool" << std::endl; }

void f(std::string) { std::cout << "std::string" << std::endl; }

};

struct HidingDerived : HiddenBase {

void f(float) { std::cout << "float" << std::endl; }

};

// ...

HiddenBase hb; HidingDerived hd; std::string s;

hb.f(1);

// Output:

int

hb.f(true); // Output:

bool

hb.f(s);

// Output:

std::string;

hd.f(1.f); // Output: float hd.f(3); // Output: float hd.f(true); // Output: float

hd.f(s); // Error: Can't convert from std::string to float.

This is due to name resolution rules: During name lookup, once the correct name is found, we stop looking, even if we clearly haven't found the correct version of the entity with that name (such as with hd.f(s)); due to this, overloading the function in the derived class prevents name lookup from discovering the overloads in the base class. To avoid this, a using-declaration can be used to "import" names from the base class into the derived class, so

GoalKicker.com – C++ Notes for Professionals

229

that they will be available during name lookup.

struct HidingDerived : HiddenBase {

// All members named HiddenBase::f shall be considered members of HidingDerived for lookup. using HiddenBase::f;

void f(float) { std::cout << "float" << std::endl; }

}; // ...

HidingDerived hd;

hd.f(1.f); // Output: float hd.f(3); // Output: int hd.f(true); // Output: bool hd.f(s); // Output: std::string

If a derived class imports names with a using-declaration, but also declares functions with the same signature as functions in the base class, the base class functions will silently be overridden or hidden.

struct NamesHidden {

 

 

virtual void hide_me()

{}

virtual void hide_me(float) {}

void hide_me(int)

 

{}

void hide_me(bool)

 

{}

};

 

 

struct NameHider : NamesHidden {

 

using NamesHidden::hide_me;

 

void hide_me()

{} // Overrides NamesHidden::hide_me().

void hide_me(int) {} // Hides NamesHidden::hide_me(int).

};

A using-declaration can also be used to change access modifiers, provided the imported entity was public or protected in the base class.

struct ProMem { protected:

void func() {}

};

struct BecomesPub : ProMem { using ProMem::func;

}; // ...

ProMem pm;

BecomesPub bp;

pm.func(); // Error: protected. bp.func(); // Good.

Similarly, if we explicitly want to call a member function from a specific class in the inheritance hierarchy, we can qualify the function name when calling the function, specifying that class by name.

struct One {

GoalKicker.com – C++ Notes for Professionals

230

virtual void f() { std::cout << "One." << std::endl; }

};

struct Two : One { void f() override {

One::f(); // this->One::f(); std::cout << "Two." << std::endl;

}

};

struct Three : Two { void f() override {

Two::f(); // this->Two::f(); std::cout << "Three." << std::endl;

}

}; // ...

Three t;

t.f(); // Normal syntax.

t.Two::f(); // Calls version of f() defined in Two. t.One::f(); // Calls version of f() defined in One.

Section 41.4: Virtual Member Functions

Member functions can also be declared virtual. In this case, if called on a pointer or reference to an instance, they will not be accessed directly; rather, they will look up the function in the virtual function table (a list of pointers-to- member-functions for virtual functions, more commonly known as the vtable or vftable), and use that to call the version appropriate for the instance's dynamic (actual) type. If the function is called directly, from a variable of a class, no lookup is performed.

struct Base {

virtual void func() { std::cout << "In Base." << std::endl; }

};

struct Derived : Base {

void func() override { std::cout << "In Derived." << std::endl; }

};

void slicer(Base x) { x.func(); }

// ...

 

 

 

Base b;

 

 

 

Derived d;

 

 

 

Base *pb = &b,

*pd = &d; // Pointers.

Base &rb = b, &rd = d;

// References.

b.func();

// Output:

In Base.

d.func();

// Output:

In Derived.

pb->func(); // Output:

In Base.

pd->func(); // Output:

In Derived.

rb.func();

// Output:

In Base.

rd.func();

// Output:

In Derived.

GoalKicker.com – C++ Notes for Professionals

231

slicer(b); // Output: In Base. slicer(d); // Output: In Base.

Note that while pd is Base*, and rd is a Base&, calling func() on either of the two calls Derived::func() instead of Base::func(); this is because the vtable for Derived updates the Base::func() entry to instead point to Derived::func(). Conversely, note how passing an instance to slicer() always results in Base::func() being called, even when the passed instance is a Derived; this is because of something known as data slicing, where passing a Derived instance into a Base parameter by value renders the portion of the Derived instance that isn't a Base instance inaccessible.

When a member function is defined as virtual, all derived class member functions with the same signature override it, regardless of whether the overriding function is specified as virtual or not. This can make derived classes harder for programmers to parse, however, as there's no indication as to which function(s) is/are virtual.

struct B {

virtual void f() {}

};

struct D : B {

void f() {} // Implicitly virtual, overrides B::f.

// You'd have to check B to know that, though.

};

Note, however, that a derived function only overrides a base function if their signatures match; even if a derived function is explicitly declared virtual, it will instead create a new virtual function if the signatures are mismatched.

struct BadB {

virtual void f() {}

};

struct BadD : BadB {

virtual void f(int i) {} // Does NOT override BadB::f.

};

Version ≥ C++11

As of C++11, intent to override can be made explicit with the context-sensitive keyword override. This tells the compiler that the programmer expects it to override a base class function, which causes the compiler to omit an error if it doesn't override anything.

struct CPP11B {

virtual void f() {}

};

struct CPP11D : CPP11B { void f() override {}

void f(int i) override {} // Error: Doesn't actually override anything.

};

This also has the benefit of telling programmers that the function is both virtual, and also declared in at least one base class, which can make complex classes easier to parse.

When a function is declared virtual, and defined outside the class definition, the virtual specifier must be included in the function declaration, and not repeated in the definition.

Version ≥ C++11

GoalKicker.com – C++ Notes for Professionals

232