目录
继承和派生
基类 & 派生类
访问控制和继承
派生类的构造函数
派生类的析构函数
继承类型
多继承
重载运算符和重载函数
函数重载
运算符重载
可重载运算符/不可重载运算符
运算符重载实例
继承和派生
先来说继承,这与现实生活中的继承意思差不多,比如一个人继承另一个人的财产、以及子承父业等等是一个意思,拥有的这个过程就叫做继承。同样,在C++中,比如有两个类,新类拥有原有类的全部属性叫做继承!原有类产生新类的过程叫做派生!而我们把原有的这个类称之为父类或基类,由基类派生出的类叫做派生类或者叫做子类。大家从名字上就可以看出他们的关系。
// 基类
class Animal {
// eat() 函数
// sleep() 函数
};
//派生类
class Dog : public Animal {
// bark() 函数
};
那么继承和派生有什么好处呢?为什么C++要有这种机制呢?
- 体现面向对象的编程思想,更好的表达各类型之间的关系。
- 派生类除了可以继承基类的全部信息外,还可以添加自己的那些不同的、有差异的信息,就像生物进化的道理一样,派生类在拥有基类的全部基础之上还将更强大。
- 派生类继承到基类的成员是自动、隐藏的拥有,即不需要我们重新定义,这就节省了大量的代码,体现了代码重用的软件工程思想。
-
继承和派生可以实现代码的模块化和分层,使得程序的结构更加清晰,易于理解和维护。通过继承和派生,我们可以将相似的代码抽象出来,形成一个基类,然后派生出不同的子类,分别实现各自的特定行为。
-
继承和派生可以实现多态性,使得程序的功能更加灵活和可扩展。通过基类的指针或引用,我们可以调用派生类的方法,从而实现对不同类型对象的统一操作。
-
C++引入继承和派生机制,是为了继承C语言的优点,同时弥补其不足。C语言没有面向对象的概念,缺乏代码重用和抽象能力。通过继承和派生,C++可以更好地实现抽象、封装和多态等面向对象编程的特性。
基类 & 派生类
基类和派生类是面向对象编程中的两个重要概念。基类是指在继承关系中处于上层的、最通用的类,它定义了一组通用的属性和方法,派生类则是指在继承关系中处于下层、更具体的类,它从基类继承通用的属性和方法,并可以添加自己的特定属性和方法。
基类的概念源于面向对象编程的封装和抽象原则,其目的是将相似或相关的代码抽象出来,形成一个通用的类,避免代码冗余,提高代码的可维护性和可扩展性。基类通常包含一些数据成员和函数成员,而且这些成员的访问权限可以是public、protected或private。
派生类则是基于基类构建的,通过继承和派生机制,它可以从基类继承代码和数据,但也可以添加自己的特有信息。派生类可以覆盖或增加基类成员函数,重载基类成员函数,添加新的成员函数,以及定义新的数据成员等。派生类可以进一步派生出更加具体的子类,形成类似于树状结构的类层次体系。形式如下:
class derived-class: access-specifier base-class
其中,访问修饰符 access-specifier 是 public、protected 或 private 其中的一个,base-class 是之前定义过的某个类的名称。如果未使用访问修饰符 access-specifier,则默认为 private。
在C++中,使用关键字class或struct定义一个类,它可以从一个或多个基类继承,例如:
class Animal {
public:
void eat();
void move();
protected:
int age;
};
class Mammal : public Animal {
public:
void giveBirth();
void nurse();
private:
int numOfPaws;
};
class Dog : public Mammal {
public:
void bark();
private:
string breed;
};
在上面的例子中,Animal是一个基类,它包含两个公共成员函数和一个保护的数据成员age。Mammal是一个派生类,它从Animal继承了eat()、move()和age成员,并添加了giveBirth()、nurse()和numOfPaws成员。Dog是Mammal的派生类,它除了继承Mammal的所有成员之外,还添加了一个私有成员breed和一个公共成员函数bark()。
基类和派生类的概念是面向对象编程中非常重要的基础,它们提供了代码重用、层次化组织、多态性等重要特性。
访问控制和继承
派生类可以访问基类中的所有非私有成员,这意味着基类成员如果不想被派生类的成员函数访问,应该将其声明为私有(private)。
根据访问权限,可以总结出以下不同的访问类型:
-
同一个类:在同一个类中,所有成员(无论是public、protected还是private)都是可以访问的。这意味着类中的任何成员函数都可以访问该类的所有成员。
-
派生类:派生类可以访问基类中所有的非私有(public和protected)成员,包括变量和函数。但是,对于基类中声明为私有(private)的成员,在派生类中是无法直接访问的。
-
外部的类:外部的类只能访问基类中声明为公共(public)的成员,对于受保护(protected)和私有(private)成员是无法访问的。这意味着,如果一个类想要访问另一个类的受保护和私有成员,那么它必须通过该类的公共成员函数来实现。
此外,在派生类继承基类时,派生类会继承基类的所有方法,但有几个例外情况:
- 基类的构造函数、析构函数和拷贝构造函数:派生类需要定义自己的构造函数、析构函数和拷贝构造函数来处理自己的特定需求,而不是直接继承基类的相应函数。
- 基类的重载运算符:派生类可以定义自己的重载运算符,但不会继承基类的重载运算符。
- 基类的友元函数:派生类不能访问基类的友元函数,因为友元函数只对基类可见。
以下是一个示例:
#include<iostream>
class BaseClass {
public:
void publicMethod() { /* 实现代码 */ }
protected:
void protectedMethod() { /* 实现代码 */ }
private:
void privateMethod() { /* 实现代码 */ }
};
class DerivedClass : public BaseClass {
// 可以访问继承的 public 和 protected 成员
public:
void derivedMethod() {
publicMethod(); // 可以访问继承的 public 成员
protectedMethod(); // 可以访问继承的 protected 成员
// privateMethod(); // 不能访问继承的 private 成员
}
};
int main() {
DerivedClass obj;
obj.publicMethod(); // 可以通过对象访问继承的 public 成员
// obj.protectedMethod(); // 不能通过对象访问继承的 protected 成员
// obj.privateMethod(); // 不能通过对象访问继承的 private 成员
return 0;
}
在上面的示例中,DerivedClass 是一个派生类,它继承了 BaseClass 的成员函数 publicMethod() 和 protectedMethod()。在派生类中,它们保持了原有的访问权限。派生类可以在自己的成员函数中使用继承的公共和受保护的成员函数。但是,私有成员函数 privateMethod() 对于派生类来说是不可见的。
总的来说,通过继承,派生类可以访问基类中的成员,但对于私有成员、友元函数以及一些特殊情况(构造函数、析构函数、拷贝构造函数和重载运算符),存在一些限制。理解这些概念对于正确设计和使用继承关系是非常重要的。
派生类的构造函数
派生类的构造函数是在继承了基类的构造函数的基础上,对派生类自身进行初始化的一种特殊方法。派生类的构造函数可以调用基类的构造函数来初始化从基类继承而来的成员变量和方法。同时,派生类的构造函数也可以扩展或修改基类的构造函数。
派生类的构造函数的语法形式如下:
class Derived : public Base {
public:
Derived(参数列表) : Base(参数列表) {
// 派生类自身操作
}
};
其中,Derived 是派生类的名称,Base 是基类的名称,参数列表 是传递给构造函数的参数,: Base(参数列表) 表示调用基类的构造函数进行初始化,// 派生类自身操作 则是派生类自己的初始化代码。
需要注意的是,如果派生类没有显式地调用基类的构造函数,则编译器会自动调用基类的默认构造函数进行初始化。如果基类没有默认构造函数,或者基类的构造函数不可访问(比如是私有构造函数),则派生类必须显式地调用基类的构造函数,否则会编译错误。
此外,在使用派生类的构造函数时,还需要遵循一些规则和约定,例如构造函数的名称与类名相同,构造函数不能有返回值等。
代码示例
#include <iostream>
using namespace std;
class Base {
public:
Base(int a) { // 基类构造函数
this->a = a;
cout << "Base构造函数被调用" << endl;
}
void print() {
cout << "Base::a = " << a << endl;
}
private:
int a;
};
class Derived : public Base {
public:
Derived(int a, int b) : Base(a) { // 派生类构造函数
this->b = b;
cout << "Derived构造函数被调用" << endl;
}
void print() {
Base::print(); // 调用基类的 print 方法
cout << "Derived::b = " << b << endl;
}
private:
int b;
};
int main() {
Derived d(1, 2);
d.print();
return 0;
}
在上面的代码中,Base 是基类,Derived 是派生类。Base 类有一个带有整型参数的构造函数,Derived 类继承了 Base 类,并添加了一个整型成员变量 b,同时还定义了一个带有两个整型参数的构造函数来初始化 b。
在 Derived 构造函数的初始化列表中,先调用了基类 Base 的构造函数来初始化从基类继承而来的成员变量 a,然后再将参数 b 赋值给派生类自身的成员变量 b。在构造函数中,输出一些调试信息,以便观察构造函数的执行情况。
在 Derived 类中,还重写了基类 Base 的 print 方法,在输出基类 Base 的成员变量 a 的值后,再输出派生类 Derived 的成员变量 b 的值。在 main 函数中,创建了一个 Derived 类型的对象 d 并调用了它的 print 方法。
当程序运行时,先输出 "Base构造函数被调用" 和 "Derived构造函数被调用",然后输出基类 Base 的成员变量 a 和派生类 Derived 的成员变量 b 的值。最终输出:
Base构造函数被调用
Derived构造函数被调用
Base::a = 1
Derived::b = 2
总结:调用顺序是先调用基类的构造函数再调用派生类的构造函数。
派生类的析构函数
派生类的析构函数是在派生类对象被销毁时调用的一种特殊成员函数。与构造函数相对应,在析构函数中可以进行清理操作,例如释放动态分配的内存、关闭文件等等。
派生类的析构函数与基类的析构函数的关系比较特殊。如果基类有虚析构函数,那么派生类的析构函数也应该是虚析构函数;如果基类没有虚析构函数,那么派生类的析构函数就不必是虚析构函数。这是因为,只有当我们使用基类的指针或引用来删除一个派生类对象时,才需要调用虚析构函数来确保正确地释放内存。
派生类的析构函数的语法形式如下:
class Derived : public Base {
public:
~Derived() {
// 派生类自身的清理操作
}
};
以下是一个派生类析构函数的代码示例,它与前面的派生类构造函数的示例非常类似:
#include <iostream>
using namespace std;
class Base {
public:
Base(int a) { // 基类构造函数
this->a = a;
cout << "Base构造函数被调用" << endl;
}
virtual ~Base() { // 基类虚析构函数
cout << "Base析构函数被调用" << endl;
}
void print() {
cout << "Base::a = " << a << endl;
}
private:
int a;
};
class Derived : public Base {
public:
Derived(int a, int b) : Base(a) { // 派生类构造函数
this->b = b;
cout << "Derived构造函数被调用" << endl;
}
~Derived() { // 派生类析构函数
cout << "Derived析构函数被调用" << endl;
}
void print() {
Base::print(); // 调用基类的 print 方法
cout << "Derived::b = " << b << endl;
}
private:
int b;
};
int main() {
Base* p = new Derived(1, 2); // 使用基类指针来管理派生类对象
p->print();
delete p; // 删除派生类对象
return 0;
}
在上面的代码中,Base 和 Derived 类的定义与前面的示例相同。不同之处在于,在 Base 类中添加了一个虚析构函数,在 Derived 类中添加了一个析构函数。在 main 函数中,使用基类指针 p 来管理一个 Derived 类型的对象,并调用它的 print 方法。最后,通过 delete 运算符删除 p 指向的对象,观察析构函数的执行情况。
当程序运行时,先输出 "Base构造函数被调用" 和 "Derived构造函数被调用",然后输出基类 Base 的成员变量 a 和派生类 Derived 的成员变量 b 的值。接着,当 delete p 执行时,先调用派生类 Derived 的析构函数,再调用基类 Base 的虚析构函数,输出 "Derived析构函数被调用" 和 "Base析构函数被调用"。最终输出:
Base构造函数被调用
Derived构造函数被调用
Base::a = 1
Derived析构函数被调用
Base析构函数被调用
总结:
在创建派生类对象时,首先会调用基类的构造函数,然后再调用派生类自身的构造函数。这是因为派生类继承了基类的成员变量和成员函数,需要先初始化基类的部分,然后才能执行派生类的构造过程。因此,在构造对象时,构造函数的调用顺序是从基类到派生类。
而在销毁派生类对象时,先调用派生类的析构函数,然后再调用基类的析构函数。这是因为派生类继承了基类的资源,需要先释放派生类自身的资源,然后再释放基类的资源。因此,在销毁对象时,析构函数的调用顺序是从派生类到基类。
继承类型
在C++中,有三种基本的继承类型:公共继承、保护继承和私有继承。它们决定了派生类如何继承基类的成员。
1、公共继承(public inheritance):
公共继承是最常用的继承方式。在公共继承中,基类中的公共成员和受保护成员都会成为派生类的公共成员和受保护成员。而基类中的私有成员仍然是基类自己的私有成员,无法被派生类访问。
公共继承的语法格式为:
class DerivedClass : public BaseClass
{
//...
};
2、保护继承(protected inheritance):
在保护继承中,基类中的所有成员都将成为派生类的受保护成员。这意味着,基类中的公共成员将变为派生类的受保护成员,而基类中的私有成员仍然是基类自己的私有成员,无法被派生类访问。
保护继承的语法格式为:
class DerivedClass : protected BaseClass
{
//...
};
3、私有继承(private inheritance):
在私有继承中,基类中的所有成员都将成为派生类的私有成员。这意味着,基类中的公共成员和受保护成员都将变为派生类的私有成员,而基类中的私有成员仍然是基类自己的私有成员,无法被派生类访问。
私有继承的语法格式为:
class DerivedClass : private BaseClass
{
//...
};
需要注意的是,无论是公共继承、保护继承还是私有继承,都只影响到成员的访问权限,对于成员函数的重载、重写、隐藏等行为都没有影响。此外,派生类可以通过 using 关键字来改变从基类继承的成员的访问权限。
总的来说,继承类型决定了派生类如何继承基类的成员,理解它们的区别很重要,以便正确地设计和使用继承关系。在大多数情况下,公共继承是最常用的继承方式。
多继承
在面向对象编程中,一个类可以派生自多个基类,这被称为多重继承。通过使用类派生列表,我们可以指定派生类的多个基类。
类派生列表的语法如下:
class DerivedClass : access-specifier BaseClass1, access-specifier BaseClass2, ...
{
// 类成员声明和定义
};
其中,access-specifier 是访问修饰符,可以是 public、protected 或 private,用于指定从每个基类继承的成员的访问权限。如果没有显式地指定访问修饰符,则默认为 private 访问权限。
通过多重继承,派生类可以从每个基类继承其成员函数和数据成员。需要注意的是,当多个基类拥有同名的成员函数或数据成员时,派生类必须通过作用域解析运算符明确指定要使用的成员。
下面是一个示例:
class BaseClass1 {
// 基类1的成员和方法
};
class BaseClass2 {
// 基类2的成员和方法
};
class DerivedClass : public BaseClass1, public BaseClass2 {
// 派生类的成员和方法
};
在上述示例中,DerivedClass 是从 BaseClass1 和 BaseClass2 这两个基类中进行多继承的派生类。派生类将同时拥有这两个基类的成员和方法。
需要注意的是,在多继承中可能存在以下问题和注意事项:
1、名称冲突:如果多个基类具有相同的成员或方法名称,派生类在访问该成员或方法时需要进行限定,以避免歧义。
2、菱形继承问题:当多个基类之间存在继承关系时,派生类可能会继承相同的成员和方法多次,这被称为菱形继承问题。为了解决这个问题,可以使用虚继承(virtual inheritance)来避免重复继承。
class BaseClass {
// 基类的成员和方法
};
class IntermediateClass1 : public virtual BaseClass {
// 中间类1的成员和方法
};
class IntermediateClass2 : public virtual BaseClass {
// 中间类2的成员和方法
};
class DerivedClass : public IntermediateClass1, public IntermediateClass2 {
// 派生类的成员和方法
};
通过在基类之间使用 virtual 关键字,可以确保在派生类中只有一个实例共享基类的成员和方法。
多继承是一种强大的工具,但需要谨慎使用,特别是在存在名称冲突或复杂继承关系的情况下。
代码示例
#include<iostream>
using namespace std;
// 定义基类
class BaseClass {
public:
// 基类的公有方法
void baseFunc() {
cout << "调用了基类的方法" << endl;
}
};
// 定义中间类1,使用虚继承 BaseClass
class IntermediateClass1 : public virtual BaseClass {
public:
// 中间类1的公有方法
void intermediate1Func() {
cout << "调用了中间类1的方法" << endl;
}
};
// 定义中间类2,使用虚继承 BaseClass
class IntermediateClass2 : public virtual BaseClass {
public:
// 中间类2的公有方法
void intermediate2Func() {
cout << "调用了中间类2的方法" << endl;
}
};
// 定义派生类,从中间类1和中间类2进行多继承
class DerivedClass : public IntermediateClass1, public IntermediateClass2 {
public:
// 派生类的公有方法
void derivedFunc() {
cout << "调用了派生类的方法" << endl;
}
};
int main() {
// 创建派生类对象
DerivedClass obj;
// 调用派生类的方法
obj.derivedFunc();
// 调用中间类1的方法
obj.intermediate1Func();
// 调用中间类2的方法
obj.intermediate2Func();
// 调用基类的方法
obj.BaseClass::baseFunc();
return 0;
}
在上述示例中,我们定义了一个基类 BaseClass,然后分别定义了两个中间类 IntermediateClass1 和 IntermediateClass2,它们都从基类 BaseClass 进行虚继承。最后,我们定义了派生类 DerivedClass,它从这两个中间类进行多重继承。
在 main 函数中,我们创建了 DerivedClass 的一个对象,并且分别调用了它自己的方法、以及从中间类和基类继承过来的方法。此外,我们还使用作用域限定符 :: 来标识调用基类 BaseClass 的方法。
编译并运行上述代码,可以得到如下输出:
调用了派生类的方法
调用了中间类1的方法
调用了中间类2的方法
调用了基类的方法
这表明虚继承和作用域限定符都起到了作用,确保派生类只继承了一份基类的成员和方法。
重载运算符和重载函数
C++ 允许在同一作用域中的某个函数和运算符指定多个定义,分别称为函数重载和运算符重载。
重载声明是指一个与之前已经在该作用域内声明过的函数或方法具有相同名称的声明,但是它们的参数列表和定义(实现)不相同。
当调用一个重载函数或重载运算符时,编译器通过把您所使用的参数类型与定义中的参数类型进行比较,决定选用最合适的定义。选择最合适的重载函数或重载运算符的过程,称为重载决策。
函数重载
函数重载是指在同一个作用域内,定义多个同名但参数列表不同的函数。通过函数重载,可以根据不同的参数类型和数量来调用相应的函数。
下面是一个使用函数重载的示例代码:
#include <iostream>
using namespace std;
// 函数重载示例
void print(int num) {
cout << "整数: " << num << endl;
}
void print(double num) {
cout << "浮点数: " << num << endl;
}
void print(char ch) {
cout << "字符: " << ch << endl;
}
int main() {
int a = 10;
double b = 3.14;
char c = 'A';
// 调用不同参数类型的重载函数
print(a);
print(b);
print(c);
return 0;
}
在上述示例中,我们定义了三个重载的 print 函数,分别接受整数、浮点数和字符作为参数,并打印出对应的类型和值。
在 main 函数中,我们声明了一个整数变量 a、一个浮点数变量 b、一个字符变量 c,然后分别调用 print 函数来打印它们的值。由于参数类型不同,编译器会自动匹配调用相应类型的函数。
编译并运行上述代码,可以得到如下输出:
整数: 10
浮点数: 3.14
字符: A
这表明函数重载使得根据传入的参数类型和数量自动选择合适的函数成为可能。注意,函数重载的条件是参数列表不同,包括参数类型、参数个数或参数顺序。返回类型不同的函数不能构成重载。
运算符重载
运算符重载是指在类中重新定义和使用已有的运算符,使其适用于自定义类型的对象。通过运算符重载,可以方便地对对象执行各种操作,提高代码的可读性和易用性。
下面是一个使用运算符重载的示例代码:
#include <iostream>
using namespace std;
// 定义一个复数类
class Complex {
private:
double real;
double imaginary;
public:
// 构造函数
Complex(double r = 0.0, double i = 0.0) : real(r), imaginary(i) {}
// 运算符重载:+
Complex operator+(const Complex& other) const {
return Complex(real + other.real, imaginary + other.imaginary);
}
// 运算符重载:-
Complex operator-(const Complex& other) const {
return Complex(real - other.real, imaginary - other.imaginary);
}
// 运算符重载:*
Complex operator*(const Complex& other) const {
double r = real * other.real - imaginary * other.imaginary;
double i = real * other.imaginary + imaginary * other.real;
return Complex(r, i);
}
// 运算符重载:<<
friend ostream& operator<<(ostream& os, const Complex& c) {
os << "(" << c.real << ", " << c.imaginary << "i)";
return os;
}
};
int main() {
Complex c1(2.0, 3.0);
Complex c2(1.0, 4.0);
// 使用重载的运算符进行操作
Complex sum = c1 + c2;
Complex diff = c1 - c2;
Complex product = c1 * c2;
// 输出结果
cout << "c1 + c2 = " << sum << endl;
cout << "c1 - c2 = " << diff << endl;
cout << "c1 * c2 = " << product << endl;
return 0;
}
在上述示例中,我们定义了一个复数类 Complex,其中包含实部和虚部。然后,我们通过运算符重载重新定义了 +、-、* 运算符,使其适用于 Complex 类型的对象。
此外,我们还通过友元函数重载了输出运算符 <<,以便能够通过 cout 打印复数对象。
在 main 函数中,我们创建了两个复数对象 c1 和 c2,并使用重载的运算符对它们进行操作,将结果存储到相应的变量中。最后,我们使用 cout 打印出运算结果。
编译并运行上述代码,可以得到如下输出:
c1 + c2 = (3, 7i)
c1 - c2 = (1, -1i)
c1 * c2 = (-10, 11i)
这表明通过运算符重载,我们可以像使用内置类型一样对自定义类型的对象执行各种操作。
可重载运算符/不可重载运算符
下面是可重载的运算符列表:
双目算术运算符 | + (加),-(减),*(乘),/(除),% (取模) |
关系运算符 | ==(等于),!= (不等于),< (小于),> (大于),<=(小于等于),>=(大于等于) |
逻辑运算符 | ||(逻辑或),&&(逻辑与),!(逻辑非) |
单目运算符 | + (正),-(负),*(指针),&(取地址) |
自增自减运算符 | ++(自增),--(自减) |
位运算符 | | (按位或),& (按位与),~(按位取反),^(按位异或),,<< (左移),>>(右移) |
赋值运算符 | =, +=, -=, *=, /= , % = , &=, |=, ^=, <<=, >>= |
空间申请与释放 | new, delete, new[ ] , delete[] |
其他运算符 | ()(函数调用),->(成员访问),,(逗号),[](下标) |
下面是不可重载的运算符列表:
- 成员访问运算符 . 用于访问类或结构体的成员。
- 成员指针访问运算符 .* 和 ->* 用于访问类成员指针指向的成员。
- 域运算符 :: 用于访问命名空间、类、结构体、枚举等的成员。
- 长度运算符 sizeof 用于获取类型或表达式的字节大小。
- 条件运算符 ?: 是三元运算符,用于根据条件选择两个表达式之一的值。
- 预处理符号 # 用于预处理指令中的字符串化操作。
运算符重载实例
下面提供了各种运算符重载的实例,帮助您更好地理解重载的概念。
序号 | 运算符和实例 |
---|---|
1 | 一元运算符重载 |
2 | 二元运算符重载 |
3 | 关系运算符重载 |
4 | 输入/输出运算符重载 |
5 | ++ 和 -- 运算符重载 |
6 | 赋值运算符重载 |
7 | 函数调用运算符 () 重载 |
8 | 下标运算符 [] 重载 |
9 | 类成员访问运算符 -> 重载 |