类型转换运算符和自增、自减运算符的重载
类型转换运算符和自增、自减运算符是C++中常用的运算符重载之一,它们可以让我们更方便地进行类型转换和实现对象的自增、自减操作。
- 类型转换运算符
类型转换运算符用于将一个类的对象转换为另一个类型。在C++中,类型转换运算符可以被重载为成员函数,并且没有返回类型。
以下是一个示例,展示了如何重载类型转换运算符:
#include <iostream> class MyString { private: std::string data; public: MyString(const std::string& str = "") : data(str) {} // 类型转换运算符重载:将MyString转换为std::string operator std::string() const { return data; } }; int main() { MyString myStr("Hello"); std::string str = static_cast<std::string>(myStr); // 使用类型转换运算符将MyString转换为std::string std::cout << str << std::endl; // 输出:Hello return 0; }
在上述示例中,我们定义了一个MyString类来表示字符串对象。通过重载类型转换运算符,我们可以将MyString对象转换为std::string类型。在重载函数中,我们只需返回MyString对象的data成员变量即可。
在主函数中,我们创建了一个MyString对象myStr,并使用类型转换运算符将其转换为std::string类型,将结果存储在str对象中。然后,我们将str对象输出到控制台上。
2. 自增、自减运算符
自增和自减运算符用于对对象进行递增或递减操作。在C++中,自增和自减运算符可以被重载为成员函数,分别对应前缀形式和后缀形式。
以下是一个示例,展示了如何重载自增、自减运算符:
#include <iostream> class Counter { private: int count; public: Counter(int value = 0) : count(value) {} // 前缀形式自增运算符重载 Counter& operator++() { ++count; return *this; } // 后缀形式自增运算符重载 Counter operator++(int) { Counter temp(*this); ++count; return temp; } }; int main() { Counter counter(5); ++counter; // 使用前缀形式自增运算符递增对象 std::cout << "Count: " << counter.count << std::endl; // 输出:Count: 6 counter++; // 使用后缀形式自增运算符递增对象 std::cout << "Count: " << counter.count << std::endl; // 输出:Count: 7 return 0; }
在上述示例中,我们定义了一个Counter类来表示计数器对象。通过重载自增运算符,我们可以实现对象的自增操作。在重载函数中,前缀形式自增运算符返回递增后的对象本身,而后缀形式自增运算符则返回递增前的对象的副本。
在主函数中,我们创建了一个Counter对象counter,并连续使用前缀和后缀形式的自增运算符对其进行递增操作。然后,我们将计数器的值输出到控制台上。
通过重载自增、自减运算符,我们可以方便地实现自定义类型对象的自增、自减操作,提供更直观的操作方式。需要注意的是,在重载自增、自减运算符时,应根据语义约定和常规用法来定义运算符的行为。
运算符重载的注意事项
- C++不允许定义新的运算符 ;
- 重载后运算符的含义应该符合日常习惯;
complex_a + complex_b word_a > word_b date_b = date_a + n
- 运算符重载不改变运算符的优先级;
- 以下运算符不能被重载:“.”、“.*”、“::”、“?:”、sizeof;
- 重载运算符()、[]、->或者赋值运算符=时,运算符重载函数必须声明为类的成员函数。
在进行运算符重载时,有一些重要的注意事项需要考虑:
- 符合语义:运算符重载应与其原始含义相符,遵循常规用法和语义约定。这样可以提高代码的可读性,并减少使用者的困惑。
- 类型匹配:运算符重载应适用于自定义类型,并与内置类型的行为保持一致。确保操作数的类型与预期一致,并考虑各种可能的组合和转换情况。
- 返回类型:运算符重载函数应返回适当的类型,以确保正确的结果。通常将返回值作为引用或常量引用,以避免不必要的副本构造。
- 顺序和优先级:运算符重载的行为和优先级应与其原始含义一致。如果需要改变优先级,请使用括号来明确表达意图。
- 自我赋值检查:对于赋值运算符等涉及到自我赋值的情况,应首先检查对象是否与自身相同。如果是,则直接返回当前对象的引用,以避免意外行为。
- 异常安全性:保证在运算符重载中处理异常,以确保程序的安全性和稳定性。在分配内存、访问资源等可能引发异常的操作中,应采取适当的异常处理措施。
- 常量成员函数:在某些情况下,运算符重载函数可能需要声明为常量成员函数。这样可以确保运算符的重载不会修改对象的状态。
- 一致性和预期行为:运算符重载的行为应与用户的预期一致,并遵循编程语言的规范和约定。提供适当的文档、注释和示例代码,以帮助其他开发者正确使用运算符重载。
在进行运算符重载时,建议查阅相关的文档和规范,并参考标准库中对于运算符的重载实现。此外,进行单元测试和边界条件的测试也是验证运算符重载行为正确性的重要手段。
总结起来,运算符重载是一项强大的特性,可以提高代码的可读性和灵活性。但要注意遵循语义约定、类型匹配、返回类型、自我赋值检查等原则,以确保正确且一致的行为。
继承和派生
在C++中,继承和派生是面向对象编程的两个重要概念,用于实现类与类之间的关系。
继承是指一个类可以从另一个类中继承属性和方法,并且可以在此基础上扩展出自己的属性和方法。被继承的类称为基类(父类),继承的类称为派生类(子类)。在C++中,可以通过以下方式定义一个派生类:
class DerivedClass : public BaseClass { // 派生类的成员变量和成员函数 };
在上面的示例中,DerivedClass是派生类,BaseClass是基类。关键字public表示使用公有继承,表示DerivedClass继承了BaseClass的所有public和protected成员,但不继承BaseClass的private成员。
派生类可以访问基类的public和protected成员,但不能访问基类的private成员。当派生类的成员变量或成员函数与基类的成员变量或成员函数同名时,可以使用作用域解析运算符::来指定调用哪个类的成员。
在派生类中,可以通过以下方式调用基类的构造函数:
class DerivedClass : public BaseClass { public: DerivedClass(int x, int y, int z) : BaseClass(x, y), m_z(z) {} private: int m_z; };
在上面的示例中,调用了BaseClass的构造函数,并将参数x和y传递给它。
派生类中还可以重写(override)基类的成员函数,即在派生类中重新定义一个和基类相同名称、参数列表和返回类型的成员函数。在调用派生类的成员函数时,会优先调用派生类中的函数,如果派生类中没有定义相应的函数,则会调用基类的函数。
继承和派生是面向对象编程的重要概念,可以实现代码的复用和扩展。在使用继承和派生时,需要注意类之间的关系,避免出现循环继承等问题。同时,需要注意访问权限的控制,避免对private成员的直接访问。
需要继承机制的例子
继承机制可以实现代码的复用和扩展,下面是一些需要继承机制的例子:
- 图形类的继承
在图形类中,可以定义一个基类Shape,包括图形的公共属性和方法,如颜色、位置、面积、周长等。然后可以通过继承机制定义各种具体的图形类,如矩形类、圆形类、三角形类等。这样,可以避免在每个具体的图形类中都定义公共的属性和方法,提高了代码的复用性。 - 汽车类的继承
在汽车类中,可以定义一个基类Vehicle,包括汽车的公共属性和方法,如品牌、型号、颜色、速度、加速度等。然后可以通过继承机制定义各种具体的汽车类,如轿车类、越野车类、卡车类等。这样,可以避免在每个具体的汽车类中都定义公共的属性和方法,提高了代码的复用性。 - 员工类的继承
在员工类中,可以定义一个基类Employee,包括员工的公共属性和方法,如姓名、性别、年龄、职位、工资等。然后可以通过继承机制定义各种具体的员工类,如经理类、销售员类、工人类等。这样,可以避免在每个具体的员工类中都定义公共的属性和方法,提高了代码的复用性。
继承机制可以极大地提高代码的复用性和可维护性,同时也可以实现代码的扩展。在使用继承机制时,需要注意类之间的关系,选择适当的继承方式,避免出现循环继承等问题。同时,需要注意访问权限的控制,避免对private成员的直接访问。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AO70jKCl-1688033782517)(2023-06-19-20-36-13.png)]
派生类的写法
在C++中,可以通过继承来创建派生类。派生类继承了基类的所有成员函数和成员变量,并且可以在此基础上扩展出自己的成员函数和成员变量。
派生类的定义方式如下:
// 基类 class BaseClass { public: int m_varBase; void funcBase(); }; // 派生类 class DerivedClass : public BaseClass { public: int m_varDerived; void funcDerived(); };
在上面的代码中,DerivedClass是派生类,BaseClass是基类。关键字public表示使用公有继承,表示DerivedClass继承了BaseClass的所有public和protected成员,但不继承BaseClass的private成员。
当派生类的成员变量或成员函数与基类的成员变量或成员函数同名时,可以使用作用域解析运算符::来指定调用哪个类的成员。例如,可以通过DerivedClass::m_varBase来访问基类中的成员变量m_varBase。
派生类中可以重写(override)基类的成员函数,即在派生类中重新定义一个和基类相同名称、参数列表和返回类型的成员函数。在调用派生类的成员函数时,会优先调用派生类中的函数,如果派生类中没有定义相应的函数,则会调用基类的函数。
在派生类中,可以通过以下方式调用基类的构造函数:
class DerivedClass : public BaseClass { public: DerivedClass(int x, int y, int z) : BaseClass(x, y), m_varDerived(z) {} private: int m_varDerived; };
在上面的代码中,调用了BaseClass的构造函数,并将参数x和y传递给它。
继承和派生是面向对象编程的重要概念,可以实现代码的复用和扩展。在使用继承和派生时,需要注意类之间的关系,避免出现循环继承等问题。同时,需要注意访问权限的控制,避免对private成员的直接访问。
派生类对象的内存空间
派生类对象在内存中的空间由两部分组成:基类部分和派生类部分。
基类部分是从基类中继承而来的,派生类对象中包含了基类对象的完整副本。在内存中,基类对象的成员变量和成员函数的布局与基类定义的布局相同。
派生类部分是派生类自己定义的成员变量和成员函数,它们被添加到了基类对象的末尾。派生类的成员变量和成员函数的布局与派生类定义的布局相同。
由于派生类对象包含了基类对象的完整副本,因此可以通过派生类对象访问基类对象中定义的成员变量和成员函数。同时,由于基类部分和派生类部分在内存中是连续的,因此派生类对象可以强制转换为基类对象的指针或引用,并且可以在基类对象的范围内使用。
例如,假设有如下的基类和派生类:
class Base { public: int m_varBase; void funcBase() {} }; class Derived : public Base { public: int m_varDerived; void funcDerived() {} };
那么,派生类对象在内存中的布局如下图所示:
|<-- 基类部分 -->|<-- 派生类部分 -->| | m_varBase | m_varDerived | | funcBase() | funcDerived() |
可以看到,派生类对象中包含了基类对象的完整副本,基类部分和派生类部分在内存中是连续的。
需要注意的是,在派生类中访问基类成员时,需要使用作用域解析运算符::来指定基类成员的名称和访问权限,否则可能会产生二义性。例如,可以使用Base::m_varBase来访问基类中的成员变m_varBase。
继承和派生是面向对象编程的重要概念,可以实现代码的复用和扩展。在使用继承和派生时,需要注意类之间的关系,避免出现循环继承等问题。同时,需要注意访问权限的控制,避免对private成员的直接访问。
示例程序:学籍管理
下面是一个简单的学籍管理的实例程序,展示了如何使用继承来实现不同类型的学生对象。
#include <iostream> #include <string> using namespace std; // 基类:学生 class Student { public: Student(string name, int age, string gender) : m_name(name), m_age(age), m_gender(gender) {} void display() { cout << "姓名:" << m_name << endl; cout << "年龄:" << m_age << endl; cout << "性别:" << m_gender << endl; } private: string m_name; int m_age; string m_gender; }; // 派生类1:本科生 class Undergraduate : public Student { public: Undergraduate(string name, int age, string gender, string major) : Student(name, age, gender), m_major(major) {} void display() { Student::display(); cout << "专业:" << m_major << endl; } private: string m_major; }; // 派生类2:研究生 class Postgraduate : public Student { public: Postgraduate(string name, int age, string gender, string research) : Student(name, age, gender), m_research(research) {} void display() { Student::display(); cout << "研究方向:" << m_research << endl; } private: string m_research; }; int main() { Student s1("张三", 20, "男"); s1.display(); Undergraduate s2("李四", 22, "女", "计算机科学与技术"); s2.display(); Postgraduate s3("王五", 25, "男", "计算机视觉"); s3.display(); return 0; }
在上面的代码中,Student是基类,包含了学生的姓名、年龄和性别信息。Undergraduate是派生类1,继承了Student的属性,并添加了专业信息。Postgraduate是派生类2,继承了Student的属性,并添加了研究方向信息。在每个派生类中,都重写了基类的display()函数,以便输出自己的属性。
在主函数中,创建了一个基类对象s1,一个Undergraduate对象s2和一个Postgraduate对象s3,分别输出了它们的属性。
继承可以实现代码的复用和扩展,使得程序更加灵活和可维护。在使用继承时,需要注意类之间的关系,选择适当的继承方式,避免出现循环继承等问题。同时,需要注意访问权限的控制,避免对private成员的直接访问。
继承关系&复合关系
继承关系和复合关系是面向对象编程中两种不同的关系。它们分别用于描述不同的对象之间的关系和组合方式。
继承关系是一种"is-a"的关系,用于描述一个类是另一个类的一种特殊形式。在继承关系中,子类继承了父类的属性和方法,并且可以在此基础上添加自己的属性和方法,从而实现代码的复用和扩展。例如,可以定义一个Animal类作为基类,然后定义Dog类和Cat类作为Animal类的子类,从而实现复用和扩展。
复合关系是一种"has-a"的关系,用于描述一个类包含另一个类的对象。在复合关系中,一个类实例化了另一个类的对象,并将其作为自己的成员变量使用。例如,可以定义一个Car类,包含了多个Wheel类对象,从而实现复杂的组合关系。
虽然继承关系和复合关系都可以用于实现代码的复用和扩展,但它们的应用场景不同。继承关系适用于描述"is-a"的关系,即一个类是另一个类的一种特殊形式;而复合关系适用于描述"has-a"的关系,即一个类包含另一个类的对象。
需要注意的是,在使用继承和复合时,需要考虑类之间的耦合性问题。继承关系会使得子类与父类之间产生紧密的耦合关系,一旦父类发生改变,子类也需要相应地进行修改。而复合关系则可以降低类之间的耦合度,使得类之间更加独立和灵活。
类之间的两种关系是继承关系和组合关系。
下面分别给出继承关系和组合关系的代码实例。
继承关系:
#include <iostream> #include <string> using namespace std; // 基类:人 class Person { public: Person(string name, int age) : m_name(name), m_age(age) {} void display() { cout << "姓名:" << m_name << endl; cout << "年龄:" << m_age << endl; } private: string m_name; int m_age; }; // 派生类:学生 class Student : public Person { public: Student(string name, int age, string school) : Person(name, age), m_school(school) {} void display() { Person::display(); cout << "学校:" << m_school << endl; } private: string m_school; }; int main() { Person p1("李四", 20); p1.display(); Student s1("张三", 18, "清华大学"); s1.display(); return 0; }
在上面的代码中,Person是基类,Student是派生类。Student继承了Person的属性和方法,并添加了自己的属性m_school。在Student中,重写了基类的display()函数,以便输出自己的属性。
组合关系:
#include <iostream> #include <string> using namespace std; // 基类:轮胎 class Tyre { public: Tyre(int size) : m_size(size) {} void display() { cout << "轮胎尺寸:" << m_size << endl; } private: int m_size; }; // 派生类:汽车 class Car { public: Car(string brand, int size) : m_brand(brand), m_tyre(size) {} void display() { cout << "品牌:" << m_brand << endl; m_tyre.display(); } private: string m_brand; Tyre m_tyre; }; int main() { Car c1("宝马", 18); c1.display(); return 0; }
在上面的代码中,Tyre是基类,Car是派生类。Car包含了一个Tyre对象m_tyre,从而实现了复杂的组合关系。在Car中,重写了自己的display()函数,以便输出自己的属性和包含的Tyre对象的属性。
需要注意的是,在使用继承和组合时,需要考虑类之间的耦合性问题。继承关系会使得子类与父类之间产生紧密的耦合关系,一旦父类发生改变,子类也需要相应地进行修改。而组合关系则可以降低类之间的耦合度,使得类之间更加独立和灵活。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jjdAoarO-1688033782518)(2023-06-19-20-43-47.png)]
复合关系的使用
复合关系是面向对象编程中的一种关系,指一个类对象包含了另一个类对象,用于描述对象之间的组合关系。
下面以一个简单的图形类作为例子,说明复合关系的使用。
#include <iostream> #include <string> using namespace std; // 点类 class Point { public: Point(int x, int y) : m_x(x), m_y(y) {} private: int m_x; int m_y; }; // 图形类 class Shape { public: Shape(string type, int width, int height, int x, int y) : m_type(type), m_width(width), m_height(height), m_point(x, y) {} void display() { cout << "图形类型:" << m_type << endl; cout << "宽度:" << m_width << endl; cout << "高度:" << m_height << endl; cout << "位置:" << m_point.m_x << ", " << m_point.m_y << endl; } private: string m_type; int m_width; int m_height; Point m_point; // 复合关系 }; int main() { Shape s("矩形", 100, 50, 10, 20); s.display(); return 0; }
在上述代码中,Point类表示坐标点,Shape类表示图形类,包含了图形类型、宽度、高度和位置信息,其中位置信息通过复合关系包含了一个Point对象。在Shape类中,定义了display()函数以便输出各个属性。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D0b6a6RI-1688033782518)(2023-06-19-20-46-31.png)]
复合关系的使用
正确的写法: 为“狗”类设一个“业主”类的对象指针; 为“业主”类设一个“狗”类的对象指针数组。 class CMaster; //CMaster必须提前声明,不能先 //写CMaster类后写Cdog类 class CDog { CMaster * pm; }; class CMaster { CDog * dogs[10]; };
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fIDja8ER-1688033782518)(2023-06-19-20-47-14.png)]
派生类覆盖基类成员
派生类可以定义一个和基类成员同名的成员,这叫覆盖。在派生类中访问这类成员时,缺省的情况是访问派生类中定义的成员。要在派生类中访问由基类定义的同名成员时,要使用作用域符号::
在C++中,派生类可以覆盖(重写)基类的成员函数或成员变量,从而实现自己的功能。这种覆盖的方式也称为重载。
下面以一个简单的例子说明派生类如何覆盖基类的成员。
#include <iostream> #include <string> using namespace std; // 基类:人 class Person { public: Person(string name, int age) : m_name(name), m_age(age) {} virtual void display() { // 基类的display()函数可以被派生类覆盖,使用virtual关键字进行声明 cout << "姓名:" << m_name << endl; cout << "年龄:" << m_age << endl; } private: string m_name; int m_age; }; // 派生类:学生 class Student : public Person { public: Student(string name, int age, string school) : Person(name, age), m_school(school) {} void display() { // 派生类覆盖了基类的display()函数 cout << "姓名:" << Person::m_name << endl; cout << "年龄:" << Person::m_age << endl; cout << "学校:" << m_school << endl; } private: string m_school; }; int main() { Person* p1 = new Person("李四", 20); // 基类指针指向基类对象 p1->display(); Person* p2 = new Student("张三", 18, "清华大学"); // 基类指针指向派生类对象 p2->display(); return 0; }
在上述代码中,Person是基类,Student是派生类。Student覆盖了Person的display()函数,输出了自己的属性和基类的属性。在main()函数中,指针p1指向基类对象,指针p2指向派生类对象,分别调用了它们的display()函数。
需要注意的是,在使用派生类覆盖基类成员时,需要满足以下条件:
- 派生类中的函数名和基类中的函数名相同;
- 派生类和基类的函数参数列表相同;
- 派生类和基类的函数返回类型相同或者是类型兼容的(如派生类可以返回基类的指针);
- 派生类的访问级别不能低于基类的访问级别;
- 基类的成员函数必须使用virtual关键字进行声明,以便派生类可以覆盖它。
通过派生类覆盖基类成员,可以实现多态性,使得程序更加灵活和可扩展。在实际编程中,也经常会用到这种技术。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c58319Gu-1688033782519)(2023-06-19-20-48-26.png)]
类的保护成员
另一种存取权限说明符:protected
• 基类的private成员:可以被下列函数访问
– 基类的成员函数
– 基类的友元函数
• 基类的public成员:可以被下列函数访问
– 基类的成员函数
– 基类的友元函数
– 派生类的成员函数
– 派生类的友元函数
– 其他的函数
• 基类的protected成员:可以被下列函数访问
– 基类的成员函数
– 基类的友元函数
– 派生类的成员函数可以访问当前对象和其它对象的基类的保护成员
在C++中,类的保护成员是指只能在类的成员函数内部和派生类的成员函数中访问,而不能在类外访问的成员。保护成员的访问级别介于公有成员和私有成员之间。
下面以一个简单的例子说明类的保护成员的使用。
#include <iostream> #include <string> using namespace std; // 基类:动物 class Animal { public: Animal(string name, string color) : m_name(name), m_color(color) {} void display() { cout << "名称:" << m_name << endl; cout << "颜色:" << m_color << endl; cout << "年龄:" << m_age << endl; // 可以在基类成员函数中访问保护成员 } protected: int m_age; // 保护成员 private: string m_name; string m_color; }; // 派生类:狗 class Dog : public Animal { public: Dog(string name, string color, int age) : Animal(name, color) { m_age = age; // 可以在派生类的成员函数中访问保护成员 } void bark() { cout << "汪汪叫!" << endl; } }; int main() { Dog d("旺财", "棕色", 3); d.display(); d.bark(); // cout << d.m_age << endl; // 保护成员不能在类外访问 return 0; }
在上述代码中,Animal是基类,Dog是派生类。基类Animal中定义了一个保护成员m_age,派生类Dog中通过继承可以访问到这个保护成员,因此可以在派生类的成员函数中对m_age进行赋值。在main()函数中,通过Dog的对象d调用了它们的成员函数。
需要注意的是,保护成员可以被派生类访问,但是不能被类外访问。如果在类外访问保护成员,编译器会报错。保护成员的作用在于,它可以在保证封装性的同时,让派生类能够访问基类的成员,从而实现更加灵活和可扩展的类设计。
派生类的构造函数
在C++中,派生类的构造函数可以调用基类的构造函数,以初始化基类的成员变量。派生类的构造函数可以有自己的参数列表,但是必须调用基类的构造函数来初始化基类的成员变量。如果派生类没有显式地调用基类的构造函数,则编译器会默认调用基类的默认构造函数。
下面以一个简单的例子说明派生类的构造函数的使用。
#include <iostream> #include <string> using namespace std; // 基类:人 class Person { public: Person(string name, int age) : m_name(name), m_age(age) { cout << "调用了Person的构造函数" << endl; } void display() { cout << "姓名:" << m_name << endl; cout << "年龄:" << m_age << endl; } private: string m_name; int m_age; }; // 派生类:学生 class Student : public Person { public: Student(string name, int age, string school) : Person(name, age), m_school(school) { cout << "调用了Student的构造函数" << endl; } void display() { cout << "姓名:" << Person::m_name << endl; cout << "年龄:" << Person::m_age << endl; cout << "学校:" << m_school << endl; } private: string m_school; }; int main() { Student s("张三", 18, "清华大学"); s.display(); return 0; }
在上述代码中,Person是基类,Student是派生类。Student的构造函数中调用了Person的构造函数,以初始化Person的成员变量。在main()函数中,创建了Student的对象s,并调用了它的成员函数。
需要注意的是,派生类的构造函数必须调用基类的构造函数,以初始化基类的成员变量。在派生类的构造函数内部,可以使用初始化列表来调用基类的构造函数,并对派生类的成员变量进行初始化。如果没有使用初始化列表,则编译器会默认调用基类的默认构造函数和派生类的默认构造函数(如果有的话)。在使用派生类的构造函数时,也需要考虑构造函数的重载、默认参数等问题。
在创建派生类的对象时,需要调用基类的构造函数:初始化派生类对象中从基类继承的成员。在执行一个派生类的构造函数之前,总是先执行基类的构造函数。
• 调用基类构造函数的两种方式
– 显式方式:在派生类的构造函数中,为基类的构造函数提供参数.
derived::derived(arg_derived-list):base(arg_base-list)
– 隐式方式:在派生类的构造函数中,省略基类构造函数时,派生类的构造函数则自动调用基类的默认构造函数.
• 派生类的析构函数被执行时,执行完派生类的析构函数后,自动调用基类的析构函数。
包含成员对象的派生类的构造函数写法
class Skill { public: Skill(int n) { } }; class FlyBug: public Bug { int nWings; Skill sk1, sk2; public: FlyBug( int legs, int color, int wings); }; FlyBug::FlyBug( int legs, int color, int wings): Bug(legs,color),sk1(5),sk2(color) ,nWings(wings) { }
当一个类包含成员对象时,派生类必须在其构造函数中显式地调用成员对象的构造函数来初始化它们。派生类的构造函数必须使用初始化列表来调用基类的构造函数和成员对象的构造函数。
下面以一个简单的例子说明包含成员对象的派生类的构造函数的写法。
#include <iostream> #include <string> using namespace std; // 基类:人 class Person { public: Person(string name, int age) : m_name(name), m_age(age) { cout << "调用了Person的构造函数" << endl; } void display() { cout << "姓名:" << m_name << endl; cout << "年龄:" << m_age << endl; } private: string m_name; int m_age; }; // 成员对象:地址 class Address { public: Address(string province, string city) : m_province(province), m_city(city) { cout << "调用了Address的构造函数" << endl; } void display() { cout << "地址:" << m_province << "省" << m_city << "市" << endl; } private: string m_province; string m_city; }; // 派生类:学生 class Student : public Person { public: Student(string name, int age, string school, string province, string city) : Person(name, age), m_school(school), m_address(province, city) { cout << "调用了Student的构造函数" << endl; } void display() { cout << "姓名:" << Person::m_name << endl; cout << "年龄:" << Person::m_age << endl; cout << "学校:" << m_school << endl; m_address.display(); } private: string m_school; Address m_address; // 包含成员对象 }; int main() { Student s("张三", 18, "清华大学", "北京", "海淀"); s.display(); return 0; }
在上述代码中,Person是基类,Address是成员对象,Student是派生类。在Student的构造函数中,需要使用初始化列表来调用Person的构造函数和Address的构造函数,以初始化它们的成员变量。在main()函数中,创建了Student的对象s,并调用了它的成员函数。
需要注意的是,在派生类的构造函数中调用成员对象的构造函数,需要在初始化列表中使用成员对象的名称来进行调用,而不是使用构造函数的名称来进行调用。此外,也需要考虑构造函数的重载、默认参数等问题。
封闭派生类对象的构造函数的执行顺序
在创建派生类的对象时:
- 先执行基类的构造函数,用以初始化派生类对象中从基类继承的成员;
- 再执行成员对象类的构造函数,用以初始化派生类对象中成员对象。
- 最后执行派生类自己的构造函数
在派生类对象消亡时:
- 先执行派生类自己的析构函数
- 再依次执行各成员对象类的析构函数
- 最后执行基类的析构函数
析构函数的调用顺序与构造函数的调用顺序相反。
public继承的赋值兼容规则
class base { }; class derived : public base { }; base b; derived d;
• 如果派生方式是 private或protected,则上述三条不可行。
在C++中,公有继承(public inheritance)的赋值兼容规则遵循“is-a”关系,即派生类对象可以赋值给基类对象,但是反过来基类对象不能赋值给派生类对象。
具体来说,如果一个基类指针或引用指向一个派生类对象,那么该指针或引用可以调用基类对象中的成员函数,但是不能调用派生类对象中新增的成员函数。如果需要调用派生类对象中新增的成员函数,可以使用动态类型识别(dynamic_cast)来实现。
下面以一个简单的例子说明公有继承的赋值兼容规则。
#include <iostream> #include <string> using namespace std; // 基类:人 class Person { public: Person(string name, int age) : m_name(name), m_age(age) {} void display() { cout << "姓名:" << m_name << endl; cout << "年龄:" << m_age << endl; } private: string m_name; int m_age; }; // 派生类:学生 class Student : public Person { public: Student(string name, int age, string school) : Person(name, age), m_school(school) {} void display() { cout << "姓名:" << Person::m_name << endl; cout << "年龄:" << Person::m_age << endl; cout << "学校:" << m_school << endl; } void study() { cout << "学生正在学习" << endl; } private: string m_school; }; int main() { Person* p1 = new Student("张三", 18, "清华大学"); p1->display(); // 调用基类成员函数 // p1->study(); // 错误,无法调用派生类新增的成员函数 Person& p2 = *(new Student("李四", 19, "北京大学")); p2.display(); // 调用基类成员函数 // p2.study(); // 错误,无法调用派生类新增的成员函数 return 0; }
在上述代码中,Person是基类,Student是派生类。在main()函数中,使用基类指针p1和基类引用p2分别指向了一个派生类对象。通过p1和p2可以调用基类对象中的成员函数,但是不能调用派生类对象中新增的成员函数。
需要注意的是,公有继承的赋值兼容规则只适用于指针和引用,而不适用于对象。如果需要使用基类对象来初始化派生类对象,需要使用转换操作符(static_cast、dynamic_cast等)来进行类型转换。
直接基类和间接基类
在C++中,一个派生类可以从一个或多个基类中继承成员变量和成员函数。基类可以分为直接基类和间接基类两种类型。
直接基类是指在派生类的声明中明确指定的基类。例如:
class B { ... }; class D : public B { ... }; • 1 • 2
在上述代码中,B是D的直接基类。
间接基类是指在派生类的基类中递归地包含其他基类。例如:
class B { ... }; class C : public B { ... }; class D : public C { ... };
在上述代码中,C是D的直接基类,B是D的间接基类。因为D继承了C中的成员变量和成员函数,而C又继承了B中的成员变量和成员函数。
需要注意的是,派生类在继承基类时,只能通过直接基类来访问其成员函数和成员变量,不能通过间接基类来访问。如果需要访问间接基类中的成员函数和成员变量,可以通过直接基类的成员函数来调用。
下面以一个简单的例子说明直接基类和间接基类的概念。
#include <iostream> using namespace std; class A { public: void f() { cout << "调用了A的f函数" << endl; } }; class B : public A { public: void g() { cout << "调用了B的g函数" << endl; } }; class C : public B { public: void h() { cout << "调用了C的h函数" << endl; } }; int main() { C c; c.f(); // 调用直接基类A的成员函数 c.g(); // 调用直接基类B的成员函数 c.h(); // 调用自身的成员函数 return 0; }
在上述代码中,A是C的间接基类,B是C的直接基类。在main()函数中,创建了C的对象c,并调用了它的成员函数。可以看到,只有直接基类的成员函数和自身的成员函数可以被调用,间接基类的成员函数无法被直接调用。需要通过直接基类的成员函数来间接调用。
虚函数和多态
虚函数
在 C++ 中,虚函数(Virtual Function)是一种在基类中使用的特殊函数,它在基类中被声明为虚函数后,在派生类中也可以被重新定义。虚函数实现了多态特性,可以通过基类指针或引用以及动态绑定的方式,来访问派生类中的同名函数。
虚函数的定义格式如下:
class Base{ public: virtual void func() { // function body } };
在上述代码中,func()
函数被声明为虚函数。在派生类中,可以重新定义该函数,实现多态。
当以基类的指针或引用调用虚函数时,程序会在运行时判断当前指针或引用所指向的对象的类型,然后动态地绑定该函数的调用地址。因此,当指针或引用指向派生类对象时,调用的就是派生类中的函数。
下面是一个简单的例子,演示了虚函数的用法:
#include <iostream> using namespace std; class Shape { public: virtual float area() { cout << "Parent class area :" << endl; return 0; } }; class Rectangle : public Shape { public: float area() { cout << "Rectangle class area :" << endl; return (width * height); } private: int width; int height; public: Rectangle(int w, int h) { width = w; height = h; } }; class Triangle : public Shape { public: float area() { cout << "Triangle class area :" << endl; return (0.5 * base * height); } private: int base; int height; public: Triangle(int b, int h) { base = b; height = h; } }; int main() { Shape* shape; Rectangle rec(10, 7); Triangle tri(10, 5); shape = &rec; shape->area(); shape = &tri; shape->area(); }
在上述代码中,Shape
是基类,Rectangle
和 Triangle
是派生类。Shape
中的 area()
函数被声明为虚函数,因此可以在派生类中进行重载。在 main()
函数中,声明了一个 Shape
类型的指针变量 shape
,通过它来访问派生类中的同名函数。当 shape
指向 Rectangle
对象时,调用的是 Rectangle
中的 area()
函数;当 shape
指向 Triangle
对象时,调用的是 Triangle
中的 area()
函数。
在类的定义中,前面有 virtual 关键字的成员函数就是虚函数。
class base { virtual int get() ; }; int base::get() { } virtual 关键字只用在类定义里的函数声明中,
写函数体时不用
多态的表现形式
在面向对象编程中,多态(Polymorphism)是指同一个函数或方法能够接受不同类型的参数或者返回不同类型的结果。多态是面向对象编程的三大特征之一(封装、继承、多态)。
在 C++ 中,多态的表现形式主要有以下两种:
- 重载函数
在 C++ 中,函数重载(Function Overloading)也是一种多态的表现形式。同一个函数名可以被用于多个参数类型或者参数个数不同的函数,编译器会根据函数的参数类型和个数来决定调用哪个函数。例如:
void add(int a, int b) { cout << "调用的是int类型加法函数:" << a + b << endl; } void add(double a, double b) { cout << "调用的是double类型加法函数:" << a + b << endl; } int main() { add(1, 2); // 调用的是int类型加法函数:3 add(1.5, 2.6); // 调用的是double类型加法函数:4.1 return 0; }
在上述代码中,add()
函数被重载了两次,分别针对 int
和 double
类型的参数。在 main()
函数中,根据传入的参数类型,编译器会自动选择调用对应的函数。
2. 虚函数
前面已经介绍了虚函数的概念和用法,虚函数实现了运行时多态性。通过基类指针或引用以及动态绑定的方式,来访问派生类中的同名函数。
例如:
#include <iostream> using namespace std; class Shape { public: virtual float area() { cout << "Parent class area :" << endl; return 0; } }; class Rectangle : public Shape { public: float area() { cout << "Rectangle class area :" << endl; return (width * height); } private: int width; int height; public: Rectangle(int w, int h) { width = w; height = h; } }; class Triangle : public Shape { public: float area() { cout << "Triangle class area :" << endl; return (0.5 * base * height); } private: int base; int height; public: Triangle(int b, int h) { base = b; height = h; } }; int main() { Shape* shape; Rectangle rec(10, 7); Triangle tri(10, 5); shape = &rec; shape->area(); shape = &tri; shape->area(); }
在上述代码中,Shape
是基类,Rectangle
和 Triangle
是派生类。Shape
中的 area()
函数被声明为虚函数,因此可以在派生类中进行重载。在 main()
函数中,声明了一个 Shape
类型的指针变量 shape
,通过它来访问派生类中的同名函数。当 shape
指向 Rectangle
对象时,调用的是 Rectangle
中的 area()
函数;当 shape
指向 Triangle
对象时,调用的是 Triangle
中的 area()
函数。这就是运行时多态性的表现。
派生类的对象可以赋给基类引用
通过基类引用调用基类和派生类中的同名虚函数时:
(1)若该引用引用的是一个基类的对象,那么被调用是基类的虚函数;
(2)若该引用引用的是一个派生类的对象,那么被调用的是派生类的虚函数。这种机制也叫做“多态”。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-x3SvH3WZ-1688033782520)(2023-06-20-20-11-19.png)]