TIPWelcome to my CS205 lecture notes! Because the lecture is not in English, I will try my best to translate it.
And at the same time, the
PPT
,lab-file
also use the English,I will write the English notes but not all.
NOTEIf you have a passion to konw more about the course, you can click the link below to learn more about the course. Read the repo.
Waiting for api.github.com...
WARNING由于本文篇幅过长,个人会添加适当的中文注解在里面。
Topic Overview
- Class Inheritance:
- Basic syntax and concepts of inheritance
is-a
relationship- Order of constructor and destructor calls
- Access control and inheritance (
public
,protected
,private
)
- Polymorphism:
- Static binding vs. Dynamic binding
- Virtual functions
- Pure virtual functions and abstract base classes
- Destructors in Inheritance:
- Importance of virtual destructors
- Inheritance and Dynamic Memory Allocation:
- Proper resource management when base and/or derived classes use dynamic memory
- Exercises
Foreword
Welcome to the Lab 12 study notes! This lab delves into two core concepts of object-oriented programming: inheritance and polymorphism.
We will learn how to create class hierarchies through inheritance to achieve code reuse and understand access permissions under different inheritance types.
More importantly, we will explore how polymorphism is achieved through virtual functions, allowing us to treat objects of different derived classes in a uniform manner.
Additionally, we will discuss the importance of correctly managing dynamic memory and using virtual destructors in an inheritance hierarchy.
Class Inheritance
Inheritance is a mechanism in object-oriented programming that allows a class (called a derived class or subclass) to acquire the properties and methods of another class (called a base class or parent class).
1.1 Basic Syntax of Inheritance
In C++, the syntax for inheritance is as follows:
class DerivedClassName : access-specifier BaseClassName {
// Derived class members
};
DerivedClassName
is the name of the derived class.BaseClassName
is the name of the base class.access-specifier
can bepublic
,protected
, orprivate
. It determines the access level of the base class members in the derived class.public
inheritance: Public members of the base class remainpublic
in the derived class, andprotected
members remainprotected
. This is the most common type of inheritance and establishes an “is-a” relationship.protected
inheritance: Public andprotected
members of the base class becomeprotected
in the derived class.private
inheritance: Public andprotected
members of the base class becomeprivate
in the derived class. This usually represents an “is-implemented-in-terms-of” relationship.
1.2 The is-a
Relationship
Public inheritance establishes an “is-a” relationship. This means an object of the derived class is also an object of its base class. For example, if a Student
class publicly inherits from a Person
class, then a Student
object is a Person
object. This allows us to treat derived class objects as base class objects, which is fundamental to polymorphism.
1.3 Order of Constructor and Destructor Calls
When a derived class object is created:
- The base class’s constructor is called first to initialize the base class part of the derived class object.
- Then, the derived class’s constructor is called to initialize the members defined in the derived class itself.
When a derived class object is destroyed, the order is reversed:
- The derived class’s destructor is called first.
- Then, the base class’s destructor is called.
NOTEA derived class constructor can explicitly call a specific base class constructor using its member initializer list. If not explicitly called, the base class’s default constructor will be invoked.
Destructor calls are automatic; you cannot explicitly call a base class destructor in a derived class destructor.
1.4 Access Control and Inheritance
The following table summarizes the access permissions of base class members in the derived class under different inheritance modes:
Base Class Member Access | Access in Derived Class (after public inheritance) | Access in Derived Class (after protected inheritance) | Access in Derived Class (after private inheritance) |
---|---|---|---|
public | public | protected | private |
protected | protected | protected | private |
private | Not directly accessible in derived class | Not directly accessible in derived class | Not directly accessible in derived class |
- Private members of the base class are never directly accessible by the derived class, regardless of the inheritance type. Derived classes can only indirectly access private members of the base class through the base class’s public or protected interface (if provided).
Polymorphism
Polymorphism (from Greek, meaning “many forms”) is one of the core features of object-oriented programming. It allows us to treat objects of different types in a uniform way. In C++, polymorphism is primarily achieved through virtual functions and dynamic binding.
2.1 Static Binding vs. Dynamic Binding
- Static Binding (Early Binding): The function call is resolved at compile time. Non-virtual function calls, as well as virtual function calls made through an object (rather than a pointer or reference), use static binding. The compiler determines which function to call based on the static type of the calling expression (the type the variable is declared as).
- Dynamic Binding (Late Binding): The function call is resolved at runtime. When a virtual function is called through a base class pointer or reference, dynamic binding is used. The program determines which version of the virtual function to call based on the dynamic type of the object pointed to or referenced (the actual type of the object).
2.2 Virtual Functions
By declaring a member function as virtual
in a base class, it can be overridden in derived classes, and dynamic binding can be achieved through base class pointers or references.
Declaration: Add the
virtual
keyword before the function declaration in the base class.class Base { public: virtual void show() { /* Base class implementation */ } // ... };
Overriding: A derived class can provide a function with the same signature (name, parameter list, and
const
qualifier) as a virtual function in the base class. In C++11 and later, it is recommended to add theoverride
keyword after the function signature in the derived class to help the compiler check if the signature matches.class Derived : public Base { public: void show() override { /* Derived class specific implementation */ } // ... };
TIP当通过基类指针或引用调用虚函数时,程序会查找该指针/引用实际指向的对象的类型,并调用该类型对应的虚函数版本。这是通过虚函数表(vtable)机制实现的。
2.3 Pure Virtual Functions and Abstract Base Classes
Pure Virtual Function: A virtual function that is declared in a base class but has no definition provided in the base class. It tells the compiler that the function must be implemented in derived classes. A pure virtual function is declared by appending
= 0
to its declaration.class Shape { public: virtual double area() const = 0; // Pure virtual function virtual ~Shape() {} // Abstract classes should also have a virtual destructor };
Abstract Base Class (ABC): A class containing at least one pure virtual function is called an abstract base class. Abstract base classes cannot be instantiated (i.e., you cannot create objects of an ABC). They primarily serve as interfaces, defining functionality that derived classes must implement. If a derived class fails to implement all pure virtual functions from its base class, it too becomes an abstract class.
NOTE抽象基类不能被实例化,它主要用作接口,定义派生类必须实现的功能。派生类如果未能实现基类中的所有纯虚函数,那么它本身也将成为抽象类。
Destructors in Inheritance
3.1 Importance of Virtual Destructors
When deleting a derived class object through a base class pointer, if the base class’s destructor is not virtual, only the base class’s destructor will be called. The derived class’s destructor will not be invoked. This can lead to resources allocated in the derived class (such as dynamic memory) not being properly released, causing resource leaks.
NOTE当通过基类指针删除一个派生类对象时,如果基类的析构函数不是虚函数,则只会调用基类的析构函数,派生类的析构函数不会被调用。这会导致派生类中分配的资源(如动态内存)无法被正确释放,从而引发资源泄漏。
如果一个类可能作为基类,并且其实例可能通过基类指针被删除,那么它的析构函数应该声明为
virtual
。
class Base {
public:
Base() { /* ... */ }
virtual ~Base() { /* Base class cleanup */ } // Virtual destructor
};
class Derived : public Base {
private:
int* data;
public:
Derived() { data = new int[10]; /* ... */ }
~Derived() override { delete[] data; /* Derived class cleanup */ } // Destructor will also be called
};
// Base* ptr = new Derived();
// delete ptr; // Correctly calls Derived::~Derived() then Base::~Base()
Inheritance and Dynamic Memory Allocation
4.1 Dynamic Memory Allocation in Cpp
When base classes, derived classes, or both use dynamic memory allocation, special attention must be paid to the correct implementation of copy control members (copy constructor, copy assignment operator) and destructors.
If the Derived Class Does Not Use Dynamic Memory Allocation:
- Usually, no explicit definition of copy control members or destructor is needed for the derived class; compiler-generated versions will correctly call the base class versions.
If the Derived Class Also Uses Dynamic Memory Allocation:
- The derived class must explicitly define its own destructor, copy constructor, and copy assignment operator.
- Derived Class Destructor: Responsible for cleaning up resources allocated by the derived class itself. It will automatically call the base class destructor.
- Derived Class Copy Constructor:
- Must explicitly call the base class’s copy constructor to handle copying the base class part (via the member initializer list).
- Then responsible for deep copying the dynamically allocated members defined by the derived class itself.
- Derived Class Copy Assignment Operator:
- Must explicitly call the base class’s copy assignment operator to handle assignment of the base class part.
- Needs to handle self-assignment.
- Release dynamic resources currently held by the derived class.
- Then responsible for deep copying the dynamically allocated members defined by the derived class itself.
#include <iostream>
#include <cstring>
using namespace std;
class Parent{
private:
int id;
char* name;
public:
Parent(int i=0, const char* n="null");
Parent(const Parent& p);
virtual ~Parent();
Parent& operator=(const Parent& prhs);
friend ostream& operator<<(ostream& os, const Parent& p){
os<<"Parent:"<<p.id<<", "<<p.name<<endl;
return os;
}
};
class Child: public Parent{
private:
char* style;
int age;
public:
Child(int i=0, const char* n="null", const char* s="null", int a=0);
Child(const Child& c);
~Child();
Child& operator=(const Child& crhs);
friend ostream& operator<<(ostream& os, const Child& c){
os<<(Parent&)c<<"Child:"<<c.style<<", "<<c.age<<endl;
return os;
}
};
Parent::Parent(int i, const char* n){
cout<<"calling Parent defautl constructor Parent()\n";
id = i;
name = new char[strlen(n) + 1];
//strcpy_s(name,strlen(n)+1, n);
strncpy(name, n,strlen(n)+1);
}
Child::Child(int i, const char* n, const char* s, int a): Parent(i,n){
cout<<"call Child default constructor Child()\n";
style = new char[strlen(s) + 1];
//strcpy_s(style,strlen(s)+1, s);
strncpy(style, s,strlen(s)+1);
age=a;
}
Parent:: ~Parent(){
cout<< "call Parent destructor.\n";
delete [] name;
}
Child::~Child(){
cout<<"call Child destructor.\n";
delete[] style;
}
Parent::Parent(const Parent& p){
cout<<"calling Parent copy constructor Parent(const Parent&)\n";
id = p.id;
name = new char[strlen(p.name)+1];
//strcpy_s(name, strlen(p.name)+1, p.name);
strncpy(name, p.name,strlen(p.name)+1);
}
Child::Child(const Child& c):Parent(c){
cout<<"calling Child copy constructor Child(const Child&)\n";
age = c.age;
style = new char[strlen(c.style)+1];
//strcpy_s(style, strlen(c.style)+1, c.style);
strncpy(style, c.style, strlen(c.style)+1);
}
Parent& Parent::operator=(const Parent& prhs){
cout<<"call Parent assignment operator:\n";
if(this == &prhs)
return *this;
delete []name;
this->id = prhs.id;
name = new char[strlen(prhs.name)+1];
//strcpy_s(name,strlen(prhs.name)+1, prhs.name);
strncpy(name,prhs.name,strlen(prhs.name)+1);
return *this;
}
Child& Child::operator=(const Child& crhs){
cout<<"call Child assignment operator:\n";
if(this == &crhs)
return *this;
Parent::operator=(crhs);
delete []style;
style = new char[strlen(crhs.style)+1];
//strcpy_s(style,strlen(crhs.style)+1,crhs.style);
strncpy(style,crhs.style,strlen(crhs.style)+1);
age = crhs.age;
return *this;
}
int main(){
Parent p1;
cout<< "value in p1\n"<<p1<<endl;
Parent p2(101, "Liming");
cout<< "value in p2\n"<<p2<<endl;
Parent p3(p1);
cout<< "value in p3\n"<<p3<<endl;
p1 = p2;
cout<< "value in p1\n"<<p1<<endl;
Child c1;
cout<<"value in c1\n"<<c1<<endl;
Child c2(201, "Wuhong","teenager",15);
cout<<"value in c2\n"<<c2<<endl;
Child c3(c1);
cout<< "value in c3\n"<<c3<<endl;
c1=c2;
cout<<"value in c1\n"<<c1<<endl;
return 0;
}
4.2 class inheritance in Python
Inheritance in Python is public inheritance, providing flexible data access
The inheritance mechanism in Python is explicit and requires calling the constructor of the parent class during initialization, which can be implemented through super()
Assignment in Python is usually a reference, and deep copy requires the deepcopy method of the copy module
Python does not allow overloading operator, using method instead( assign, str here)
Destructors in Python rely on garbage collection mechanisms.
Rewriting del requires caution and attention to potential memory risks.
import copy
class Parent:
def __init__(self, i=0, n="null"):
print("calling Parent default constructor Parent()")
self.id = i
self.name = n # Python 字符串不可变,无需手动内存管理
def __deepcopy__(self, memo):
print("calling Parent copy constructor Parent(const Parent&)")
new_obj = self.__class__(self.id, self.name)
memo[id(self)] = new_obj
return new_obj
def assign(self, other):
print("call Parent assignment operator:")
if self is other:
return self
self.id = other.id
self.name = other.name
return self
def __del__(self):
print("call Parent destructor.") # 实际无需手动释放资源
def __str__(self):
return f"Parent:{self.id}, {self.name}\n"
class Child(Parent):
def __init__(self, i=0, n="null", s="null", a=0):
super().__init__(i, n)
print("call Child default constructor Child()")
self.style = s
self.age = a
def __deepcopy__(self, memo):
print("calling Child copy constructor Child(const Child&)")
new_obj = self.__class__(self.id, self.name, self.style, self.age)
memo[id(self)] = new_obj
return new_obj
def assign(self, other):
print("call Child assignment operator:")
if self is other:
return self
super().assign(other)
self.style = other.style
self.age = other.age
return self
def __del__(self):
super().__del__()
print("call Child destructor.") # 父类 __del__ 会自动调用
def __str__(self):
#parent_str = super().__str__().replace("Parent:", "Child:", 1)
parent_str = super().__str__()
return f"{parent_str}Child:{self.style}, {self.age}\n"
if __name__ == "__main__":
# 模拟 C++ 对象构造
p1 = Parent()
print("value in p1\n", p1)
p2 = Parent(101, "Liming")
print("value in p2\n", p2)
p3 = copy.deepcopy(p1)
print("value in p3\n", p3)
p1.assign(p2)
print("value in p1\n", p1)
# 子类测试
c1 = Child()
print("value in c1\n", c1)
c2 = Child(201, "Wuhong", "teenager", 15)
print("value in c2\n", c2)
c3 = copy.deepcopy(c1)
print("value in c3\n", c3)
c1.assign(c2)
print("value in c1\n", c1)
# 手动触发析构(Python 垃圾回收时机不确定)
del c3, c2, c1, p3, p2, p1
Exercises
Exercise 1
Point out the errors in the following code and explain why to the TA.
#include <iostream>
class Base
{
private:
int x;
protected:
int y;
public:
int z;
void funBase (Base& b){
++x;
++y;
++z;
++b.x;
++b.y;
++b.z;
}
};
class Derived:public Base
{
public:
void funDerived (Base& b, Derived& d){
++x;
++y;
++z;
++b.x;
++b.y;
++b.z;
++d.x;
++d.y;
++d.z;
}
};
void fun(Base& b, Derived& d){
++x;
++y;
++z;
++b.x;
++b.y;
++b.z;
++d.x;
++d.y;
++d.z;
}
Hints for Solution:
- Carefully analyze what member each
++
operation attempts to access and the context of the access (is it in a base class member function, derived class member function, or global function?). - Recall the access rules for
private
,protected
, andpublic
members, and how their access permissions change upon inheritance. - A member function of a class can access the private and protected members of any object of that class, not just the object that called the function (
this
points to).
Answer:
class Base {
private:
int x;
protected:
int y;
public:
int z;
void funBase(Base& b) {
++x; ++y; ++z;
++b.x; ++b.y; ++b.z; // Base object can access private/protected members of another Base object
}
};
class Derived : public Base {
public:
void funDerived(Base& b, Derived& d) {
++y; // OK: y is protected in Base, Derived is a subclass
++z; // OK: z is public in Base
++b.z; // OK: Can access public member z of arbitrary Base object b
// ++d.x; // Error: x is private in Base
++d.y; // OK: d is a Derived object, can access its inherited protected member y
++d.z; // OK: d is a Derived object, can access its inherited public member z
}
};
void fun(Base& b, Derived& d) {
++b.z; // OK
++d.z; // OK
}
Exercise 2
Run the following program and explain the result to the TA.
#include<iostream>
using namespace std;
class Polygon{
protected:
int width,height;
public:
void set_values(int a,int b){
width=a;
height=b;
}
int area(){
return 0;
}
};
class Rectangle: public Polygon {
public:
int area(){
return width * height;
}
};
class Triangle: public Polygon {
public:
int area(){
return width*height/2;
}
};
int main () {
Rectangle rect;
Triangle trgl;
Polygon * ppoly1 = ▭
Polygon * ppoly2 = &trgl;
ppoly1->set_values (4,5);
ppoly2->set_values (2,5);
cout << rect.area() << endl;
cout << trgl.area() << endl;
cout << ppoly1->area() << endl;
cout << ppoly2->area() << endl;
return 0;
}
Hints for Solution:
- Since
Polygon::area()
is not a virtual function, callingarea()
through the base class pointersppoly1
andppoly2
will always invoke the version defined in thePolygon
class (static binding). - Calling
area()
directly through therect
andtrgl
objects will invoke the versions defined in their respective classes. - Consider how to modify the code to achieve polymorphic behavior (i.e., to make
ppoly1->area()
callRectangle::area()
).
Exercise 3
Run the following program and explain the result to the TA. Are there any problems in the program?
// dynamic allocation and polymorphism
#include <iostream>
using namespace std;
class Polygon
{
protected:
int width, height;
public:
Polygon (int a, int b) : width(a), height(b) {}
virtual int area (void) =0;
void printarea(){
cout << this->area() << '\n';
}
};
class Rectangle: public Polygon {
public:
Rectangle(int a,int b) : Polygon(a,b) {}
int area(){
return width*height;
}
};
class Triangle: public Polygon
{
public:
Triangle(int a,int b) : Polygon(a,b) {}
int area()
{ return width*height/2; }
};
int main () {
Polygon * ppoly = new Rectangle (4,5);
ppoly->printarea();
ppoly = new Triangle (2,5);
ppoly->printarea();
return 0;
}
Hints for Solution:
Polygon
is an abstract base class because it contains the pure virtual functionarea()
. Objects of typePolygon
cannot be created.- The
printarea()
function callsthis->area()
. Sincearea()
is virtual, dynamic binding occurs here, correctly calling the derived class’s implementation ofarea()
. - Potential Problem: The
Polygon
class does not declare a virtual destructor. Whendelete ppoly1;
anddelete ppoly2;
are executed, ifPolygon::~Polygon()
is not virtual, only the base class’s destructor will be called. The destructors of the derived classesRectangle
andTriangle
(even if default-generated) might not be called. If the derived classes have important cleanup work in their destructors (e.g., releasing dynamically allocated resources), this will lead to resource leaks. - Correction: A virtual destructor should be added to the
Polygon
class:virtual ~Polygon() {}
.
CC BY NC SA (Content adapted from course materials)