Classes

Interfaces

Members

Type Members

Type members can hide some implementation details about a class.

class Screen {
public:
  typedef std::string::size_type pos;
private:
  pos cursor = 0;
  pos height = 0, width = 0;
  std::string contents;
}

We can alternatively use a type alias:

using pos = std::string::size_type pos;

mutable Members

If we want to make some exceptions to the const requirements of member functions, we can mark some members as mutable and its value can be changed by functions labeled with const.

class Screen {
  // ...
  mutable size_t access_ctr;
  // ...
}

void Screen::some_member() const {
  ++access_ctr;
}

Initializers for Data Members of Class Type

Two forms of in-class initialization: = or inside braces.

class WindowsManager {
private:
	std::vector<Screen> screens { Screen(24, 80, '') };
}

Note that the syntax { Screen(24, 80, '') } is a form of list initialization, introduced in C++11. List initialization allows you to initialize containers like std::vector, arrays, or objects with a set of values.

Member Functions

:: Scope Operator

Using scope operator means that we are defining the function inside the scope of the class.

Member Functions and Non-member Functions

Member functions are used to fetch members from the class.

const Member Functions

this is of type Type *const, which means that we cannot bind this to a non-const function. We can specify const after the parameter list to indicate that this is a pointer to const.

If the member was declared as a const member function, then the definition must also specify const after the parameter list.

Overloading Member Functions

We define two get() functions in the Screen class:

class Screen {
public:
  typedef std::string::size_type pos;
  Screen() = default;
  Screen(pos ht, pos wd, char c): height(ht), width(wd), contents(ht * wd, c) {}
  char get() const { return contents[cursor]; }
  inline char get(pos ht, pos wd) const;
  Screen &move(pos r, pos c);
private:
  pos cursor = 0;
  pos height = 0, width = 0;
  std::string contents;
}

The compiler will define which function to call based on the arguments:

Screen myscreen;
char ch = myscreen.get();
ch = myscreen.get(0, 0);

Functions That Return this

Functions that return a reference are lvalues, which means that they return the object itself, not a copy of the object.

class Screen {
public:
  Screen &set(char);
  Screen &set(pos, pos, char);
}

inline Screen &Screen::set(char c) {
	contents[cursor] = c;
  return *this;
}

Consider the following expression:

myScreen.move(4,0).set('#')

If the return values of the two methods are not references, the set will take effect on a temporary copy that is returned by move, rather than myScreen.

Screen temp = myScreen.move(4,0);
temp.set('#');

Overloading Based on const

A const member function that returns *this as a reference should have a return type that is a reference to const. We can overload a member function based on whether it is const.

class Screen {
public:
  Screen &display(std::ostream &os) {do_display(os); return *this;}
  const Screen &display(std::ostream &os) const {do_display(os); return *this;}
private:
  void do_display(std::ostream &os) const {os << contents;}
}

Constructors

Default Constructor

The compiler generates the default constructor for us if and only if we do not define any other constructor for the class.

ClassName() = default: explictly instruct the compiler to generate a default constructor even if some other constructors are defined.

When a member is omitted from the constructor initializer list, it is implicitly initialized using the same process as is used by the synthesized default constructor.

A constructor that supplies default arguments for all its parameters also defines the default constructor.

In order to define an object that uses the default constructor for initialization:

Sales_data obj;

Initializer List Constructor

You can initialize a vector using an initializer list in C++ as follows:

vector<string> v1 = {"a", "b", "c"};
vector<string> v2 {"a", "b", "c"};

Both of these examples use the initializer list constructor. This constructor is defined as:

template <typename T> 
Blob<T>::Blob(std::initializer_list<T> il): data(std::make_shared<std::vector<T>>(il)) { }

In this definition, the constructor accepts a parameter of type std::initializer_list<T>.

The Difference between Initialization and Assignment

class ConstRef {
public:
	ConstRef(int ii);
private:
	int i;
	const int ci;
	int &ri;
};

// Initialization
// ok:explicitly initialize reference and const members
ConstRef::ConstRef(int ii) : i(ii), ci(ii), ri(i) {}

ConstRef::ConstRef(int ii) { 
  // Assignments
  i = ii; // ok
  ci = ii; // error: cannot assign to a const 
  ri = i; // error: ri was never initialized
}

If a member is not explicitly initialized in the constructor initializer list, it will be default-initialized before the constructor body begins executing. However, const members, reference members, and members without a default constructor should be explicitly initialized in the initializer list rather than being assigned after initialization.

Delegating Constructors

class SalesData {
public:
  SalesData(std::string s, unsigned cnt, double price) : bookNo(s), units_sold(cnt), revenue(cnt*price) {}
  Sales_data(): Sales_data("", 0, 0) {}
  Sales_data(std::string s): Sales_data(s, 0,0) {}
  Sales_data(std::istream &is): Sales_data() { read(is, *this); }
}

Implicit Class-Type Conversions

In scenarios where a function f has a parameter of type T that can be initialized with a single argument a, you can directly pass a to f. This works because a constructor that takes a single argument enables an implicit conversion from the argument type to the class type.

string null_book = "9-999-99999-9";

// Constructs a temporary Sales_data object
// with units_sold and revenue set to 0, and bookNo set to null_book 
item.combine(null_book);

Only One Class-Type Conversion Is Allowed: The following code will cause an error because it requires two implicit conversions:

// Error: Requires two user-defined conversions:
// (1) Convert "9-999-99999-9" to string
// (2) Convert the temporary string to Sales_data
item.combine("9-999-99999-9");

To prevent a constructor from being used in situations where an implicit conversion would be required, you can declare the constructor as explicit. The explicit keyword is only applicable to constructors that can be called with a single argument.

Direct Initialization: std::string s("Hello.\n")

Copy Initialization: std::string s = "Hello.\n"

When a constructor is marked as explicit, the class cannot be initialized using string literal to perform copy initialization.

Moreover explicit can also be used on conversion operators:

class MyClass {
public:
    explicit operator int() const { return 42; }
};

int main() {
    MyClass obj;
    // int value = obj;  // Error: No implicit conversion allowed
    int value = static_cast<int>(obj);  // Must explicitly convert
    return 0;
}

Order of Member Initialization

Members are initialized in the order in which they appear in the class definition. It is a good idea to write constructor initializers in the same order as the members are declared. Moreover, when possible, avoid using members to initialize other members.

Feature

new/delete

malloc/free

Memory Allocation

Allocates memory and calls constructor

Allocates memory only

Deallocation

Deallocates memory and calls destructor

Deallocates memory only

Type Safety

Type-safe, returns a pointer of the specified type

Not type-safe, returns void*

Initialization

Calls constructor, can initialize objects

No initialization

Overloading

Can be overloaded

Cannot be overloaded

Error Handling

Throws std::bad_alloc on failure

Returns NULL on failure

Placement

Supports placement new

Does not support placement allocation

Alignment

Handles alignment automatically

Requires manual alignment management

If a class needs a destructor, it also needs a copy constructor and an assignment operator. The “Rule of Three” thus helps avoid common pitfalls related to resource management in C++, ensuring that objects manage their resources correctly when they are copied, assigned, or destroyed.

Access Control and Encapsulation

We can define a class type using either class or struct. The only difference between using class and using struct to define a class is the default access level.

  • If we use the struct keyword, the members defined before the first access specifier are public.

  • If we use class, then the members are private.

Friends

Access the nonpublic members by making another class or function a friend.

class SalesData {
  friend SalesData add(const SalesData&, const SalesData&);
  friend std::istream &read(std::istream&, SalesData&);
  friend std::ostream &print(std::ostream&, SalesData&);
}

Now we can define the function outside the scope of SalesData by:

SalesData add(const SalesData &lhs, const SalesData &rhs) {
	SalesData sum = lhs;
	sum.combine(rhs);
	return sum;
}

We can even make a member function a friend:

class Screen {
  friend void WindowManager::clear(ScreenIndex);
}

Friend Declarations and Scope

The friend function is not visible outside the class even if we have defined it inside the class.

struct X {
  friend void f() { /* friend function can be defined in the class body */ }
  X() { f(); }
  void g();
  void h();
}

void X::g() { return f(); } // error: f hasn't been declared.

In order to get this work, you can define g inside the class body or define f outside of the class.

Class Scope

WindowManager::ScreenIndex WindowManager::addScreen(const Screen &s) {
  screens.push_back(s);
	return screens.size() - 1; 
}

Because the return type appears before the name of the class is seen, it appears ouÒtside the scope of class WindowManager. To use ScreenIndex for the return type, we must specify the class in which that type is defined.

Name Lookup and Class Scope

Name Lookup Steps

  1. Check for the name within the same block, considering only declarations before its use.

  2. If not found, search the enclosing scope(s).

  3. If still not found, it results in a program error.

Class Declarations and Member Functions

All declarations are processed before definitions. Therefore, define any types or other entities used in function signatures before declaring the functions themselves.

Normal Block-Scope Name Lookup inside Member Definitions

  1. Inside the member function.

  2. Inside the class.

  3. In scope before the member function definition.

    void Screen::dummy_fcn(pos height) {
    	cursor = width * ::height;// which height? the global one 
    }

Last updated