13.1、一个简单的基类
一个简单的基类通常包括一些数据成员和成员函数,而这些成员函数可以用于被派生类继承和/或重写。下面是一个例子:
class Shape {
public:
// 成员函数声明
virtual double area() const;
virtual double perimeter() const;
};
在上述源代码中,我们定义了一个 Shape
类,作为几何图形的基类。Shape
类有两个虚函数 area()
和 perimeter()
,用于返回图形的面积和周长。这些虚函数将被派生类继承和重写。在这个例子中,我们并没有定义任何数据成员。
注意,我们在虚函数的声明中使用了 virtual
关键字来标识这些函数。这表示这些函数是虚函数,并且需要在派生类中进行重写,以提供特定于派生类的实现。如果不使用 virtual
关键字,则这些函数将成为普通的成员函数,如果不在派生类中进行重写,将使用基类中的默认实现。
另外,我们在这个例子中还使用了 const
限定符,用于指示这些成员函数不修改任何成员变量的值。这一点在一些情况下是有用的,可以提高代码的可读性和效率。
13.1.2、派生一个类
派生类可以从一个或多个基类派生而来,通过继承基类的成员,在其基础上添加或修改成员变量和成员函数,从而实现不同的功能。
派生一个类的语法为:
class 派生类名 : [访问修饰符] 基类名1, [访问修饰符] 基类名2, ... {
// 派生类内容
};
其中:
派生类名
:指定要派生的类的名称。基类名
:指定要继承的基类名称。访问修饰符
:可选项,表示派生类对于基类的访问权限,默认为private
。
下面是一个例子,演示了如何从 Shape
基类派生一个新的 Circle
派生类:
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) {}
double area() const override {
return 3.14159 * radius * radius;
}
double perimeter() const override {
return 2 * 3.14159 * radius;
}
};
在这个例子中,我们从 Shape
基类中派生了一个新的 Circle
派生类。我们添加了一个名为 radius
的私有成员变量,并且实现了 Circle
类的 area()
和 perimeter()
函数,这两个函数分别返回了圆的面积和周长的值。
注意,我们使用了 public
关键字来指明 Circle
派生类继承 Shape
基类时的访问权限。这表示 Circle
类可以访问 Shape
类中的公有成员函数和公有数据成员,但是不能直接访问基类的私有成员。还要注意的是,我们使用了 const
关键字来修饰 area()
和 perimeter()
函数,这保证了函数不会修改类的成员变量的值。
最后,我们在构造函数中初始化了 radius
成员变量,该成员变量用于存储圆的半径。
13.1.3、使用派生类
使用派生类通常与使用其基类类似。派生类通常可以继承基类的公有成员函数和公有数据成员,并且可以添加自己的成员函数和成员变量。此外,我们还可以使用基类的指针或引用来访问派生类的对象,称为“向上转型”。
下面是一个演示如何使用 Circle
派生类的例子:
int main() {
// 创建 Circle 对象
Circle circle(5.0);
// 使用 Circle 对象调用成员函数
std::cout << "Circle area: " << circle.area() << std::endl;
std::cout << "Circle perimeter: " << circle.perimeter() << std::endl;
// 使用 Shape 指针访问 Circle 对象
Shape* shape = &circle;
std::cout << "Shape area: " << shape->area() << std::endl;
std::cout << "Shape perimeter: " << shape->perimeter() << std::endl;
return 0;
}
在上述代码中,我们首先创建了一个 Circle
对象,并调用了其 area()
和 perimeter()
成员函数来计算圆的面积和周长。接着,我们使用一个 Shape*
指针来指向 Circle
对象,并通过该指针访问 Shape
基类的成员函数来计算图形的面积和周长。
注意,在使用指向派生类对象的基类指针或引用时,虽然我们可以访问基类的成员函数,但是无法访问派生类特有的成员函数和成员变量,因为这些成员是派生类特有的,基类指针或引用无法访问到。如果需要访问派生类特有的成员,可以使用强制类型转换来将指针或引用转换为派生类指针或引用。
13.1.4、派生类和基类之间的特殊关系
派生类和基类之间存在一些特殊的关系,如:
-
派生类可以访问基类的公有成员,但不能访问基类的私有成员。
-
派生类可以重写基类的虚函数,以提供特定于派生类的实现。当使用基类指针或引用来访问派生类对象时,将根据对象的实际类型调用相应的成员函数。
-
派生类可以添加自己的成员函数和成员变量,从而扩展其基类的功能。
-
如果一个派生类直接或间接地继承了多个同名的虚函数,那么必须在派生类中显式地指明要使用的虚函数。这通常可以通过使用作用域解析符(
::
)来实现,或者使用using
声明来引入基类中的某个成员函数。 -
基类的构造函数会在派生类的构造函数中自动调用,以初始化基类的数据成员。在派生类的构造函数中,可以使用成员初始化列表来初始化基类的构造函数,或者通过调用基类的构造函数来完成初始化。
-
基类的析构函数也会在派生类的析构函数中自动调用,以释放基类的资源。在派生类的析构函数中,不需要显式地调用基类的析构函数,因为这会自动发生。
总之,派生类和基类之间的关系是一种继承关系,派生类继承了基类的接口和部分实现,并可以通过添加自己的成员来扩展其功能。这使得我们可以更加灵活地设计和组织类的层次结构,从而实现更加清晰和可维护的代码。
13.2、继承:is-a关系
继承是面向对象编程中的一种重要的概念,它建立了类与类之间的“is-a”关系。在继承中,派生类会从基类继承一些属性和方法,从而可以拓展已有的代码。继承在C++中以public
、protected
、private
访问修饰符为基础实现,表示派生类对基类的访问权限。public
表示公共继承,protected
表示保护继承,private
表示私有继承。
在一个“is-a”关系中,派生类是基类的一种特定类型。例如,我们可以说狗是一种动物,这里狗就是派生类,动物是基类。狗继承了动物的一些属性和行为,同时可以拓展自己的方法和属性,比如说描写狗的颜色和体型的方法。又如汽车是一种车辆,这里汽车就是派生类,车辆是基类。派生类继承了基类的通用属性和方法,还可以拓展自己的独特属性和方法,比如说加速度、车费等。
下面是一个示例展示了一个Person
基类和Student
派生类:
class Person {
private:
std::string name;
int age;
public:
Person(const std::string& pname, int page) : name(pname), age(page) {}
void say() const { std::cout << "I am a person." << std::endl; }
};
class Student : public Person {
private:
int grade;
public:
Student(const std::string& pname, int page, int pgrade) : Person(pname, page), grade(pgrade) {}
void study() const { std::cout << "I am studying." << std::endl; }
};
在这里,我们定义了一个Person
基类和一个Student
派生类。Person
类有一个name
和age
数据成员和一个say()
成员函数,Student
类继承了Person
类,同时添加了一个grade
数据成员和一个study()
成员函数。由于Student
是Person
的一种类型,所以我们可以说Student
is-a Person
,即Student
是一种特定类型的Person
。
通过从基类继承,派生类可以复用基类的代码,从而减少了代码的冗余,提高了代码的可重用性。同时,派生类也可以在基类的基础上进行拓展,从而实现更加复杂的功能。总之,继承是一种重要的代码复用机制,可以提高代码的可维护性和可扩展性。
13.3、多态公有继承关系
多态是面向对象编程的一种信息隐藏和代码灵活性的机制,它允许我们使用统一的方式处理不同类型的对象。在C++中,多态是通过虚函数实现的,它可以在运行时确定实际调用的成员函数。
在使用多态时,通常需要使用公有继承和虚函数来实现。公有继承表示派生类对基类的访问权限,而虚函数表示在派生类中重写基类的函数,从而实现多态的效果。
下面是一个示例演示了多态的使用:
class Shape {
public:
virtual double area() const = 0;
virtual double perimeter() const = 0;
};
class Rectangle : public Shape {
private:
double width, height;
public:
Rectangle(double w, double h) : width(w), height(h) {}
double area() const override {
return width * height;
}
double perimeter() const override {
return 2 * (width + height);
}
};
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) {}
double area() const override {
return 3.14159 * radius * radius;
}
double perimeter() const override {
return 2 * 3.14159 * radius;
}
};
int main() {
Rectangle rectangle(5.0, 6.0);
Circle circle(3.0);
Shape* shape;
shape = &rectangle;
std::cout << "Area of rectangle: " << shape->area() << std::endl;
shape = &circle;
std::cout << "Area of circle: " << shape->area() << std::endl;
return 0;
}
在这个例子中,我们定义了一个Shape
基类和两个派生类Rectangle
和Circle
,其中Shape
基类中有两个纯虚函数area()
和perimeter()
,这些函数在派生类中将被重写。Rectangle
和Circle
分别计算矩形和圆的面积和周长。
在main()
函数中,我们创建了一个Rectangle
对象和一个Circle
对象。然后,我们创建了一个Shape
指针指向Rectangle
对象,再计算矩形的面积;接着,我们将该指针指向Circle
对象,再计算圆的面积。由于Shape
指针指向的是不同的对象,所以实际调用的成员函数也不同,这就是多态的效果。
总之,使用公有继承和虚函数可以实现多态的效果,从而提高了代码的灵活性和可重用性。当我们需要对一组不同类型的对象执行相似的操作时,可以使用多态来简化代码,提高代码的可维护性。
13.4、静态联编和动态联编
静态联编和动态联编是两种不同的函数调用方式,它们在编译时和运行时的行为不同。
1、静态联编
静态联编也称为早绑定,是在编译期间完成函数调用的解析。在静态联编中,编译器根据函数的名称、参数数量和类型来确定调用哪个函数。由于解析过程在编译期间完成,因此在运行时不需要额外的开销,具有较高的效率。
例如:
int sum(int a, int b) {
return a + b;
}
double sum(double a, double b) {
return a + b;
}
int main() {
int x = 1, y = 2;
double a = 1.0, b = 2.0;
cout << sum(x, y) << endl; // 调用int sum(int, int)
cout << sum(a, b) << endl; // 调用double sum(double, double)
return 0;
}
在这个例子中,我们定义了两个同名的函数sum
,一个接受两个整数参数,另一个接受两个浮点数参数。在main()
函数中,我们分别调用这两个函数,并且编译器会根据参数类型选择不同的函数。
2、动态联编
动态联编也称为晚绑定,是在运行时根据实际对象类型进行函数的解析。在动态联编中,调用一个虚函数时,编译器会为对象生成一个虚表,虚表中存储了实际对象类型的信息及其对应的虚函数地址,从而实现在运行时根据实际对象类型调用正确的函数,而不是根据指针类型调用函数。由于解析过程在运行时完成,因此具有较高的灵活性。
例如:
class Animal {
public:
virtual void speak() {
cout << "Animal speaks." << endl;
}
};
class Cat : public Animal {
public:
void speak() override {
cout << "Meow." << endl;
}
};
class Dog : public Animal {
public:
void speak() override {
cout << "Woof." << endl;
}
};
int main() {
Animal* animal = new Animal();
animal->speak(); // Animal speaks.
animal = new Cat();
animal->speak(); // Meow.
animal = new Dog();
animal->speak(); // Woof.
return 0;
}
在这个例子中,我们定义了一个Animal
基类和两个派生类Cat
和Dog
,它们都重写了speak()
虚函数。在main()
函数中,我们创建了一个Animal
对象和两个派生类对象,并分别调用它们的speak()
函数。由于speak()
是一个虚函数,编译器会创建一个虚表,根据实际对象类型调用对应的speak()
函数。
总之,静态联编和动态联编都是实现函数调用的方式,它们的区别在于解析过程是在编译时还是运行时执行,从而影响了程序运行的效率和灵活性。在使用面向对象编程时,动态联编常常用于实现多态,提高代码灵活性和可维护性。
13.4.1、指针和引用类型的兼容性
指针和引用类型都可以用来访问变量或对象,它们在语义和用法上有很多相似之处,但也存在一些区别。对于指针和引用类型的兼容性的问题,需要分别考虑以下几个方面:
1、相互赋值
在C++中,允许将引用类型和指针类型相互赋值。指针可以直接赋值给指向相同类型的引用,引用也可以绑定到指向相同类型的指针上。例如:
int a = 1;
int* p = &a;
int& r = *p;
在这个例子中,p
是指向a
的指针,*p
是a
本身,r
是一个指向a
的引用。
2、函数参数类型
在C++中,允许使用引用类型作为函数参数,以便在函数内部修改实参的值。指针也可以作为函数参数,并且也可以通过指针来修改实参的值。例如:
void func(int& x) {
x = 2;
}
void func(int* x) {
*x = 3;
}
int main() {
int a = 1;
func(a);
cout << a << endl; // 输出2
func(&a);
cout << a << endl; // 输出3
return 0;
}
在这个例子中,func(int& x)
和func(int* x)
都是修改实参的函数,一个使用引用类型,一个使用指针。
3、类型推导
在C++11后,可以使用类型推导(type inference)来自动推导指针和引用类型,从而使代码更加简洁。例如:
int a = 1;
auto& r = a; // 推导出 int&
auto* p = &a; // 推导出 int*
在这个例子中,使用auto
关键字可以自动推导出引用类型和指针类型。
总之,在C++中指针类型和引用类型之间存在较高的兼容性,它们可以相互转换、相互赋值,并且都可以作为函数参数。需要根据具体的情况选择使用哪种类型,以满足代码的需求。
13.4.2、虚成员函数和动态联编
在C++中,将一个成员函数声明为virtual
,就可以将其定义为“虚函数”,从而使得该函数可以进行动态联编(dynamic dispatch)。
动态联编是在运行时绑定函数的实际实现,而非在编译时确定。这意味着,通过虚函数可以实现运行时多态性(runtime polymorphism)。
当一个类拥有虚函数时,编译器会为该类创建一个虚表(vtable),该表保存了虚函数指针。每个对象在其内存布局中都有一个指向虚表的指针,通过该指针就可以访问类的虚函数。
当调用虚函数时,会使用对象的虚表指针来查找该函数的实际实现,然后调用该函数。换句话说,实际调用的函数取决于对象的类型而不是所使用的指针或引用的类型。
下面是一个简单的例子,演示了虚函数的使用:
#include <iostream>
using namespace std;
class Shape {
public:
virtual double getArea() {
return 0.0;
}
};
class Rectangle : public Shape {
public:
Rectangle(double l, double w) : length(l), width(w) {}
double getArea() {
return length * width;
}
private:
double length;
double width;
};
class Circle : public Shape {
public:
Circle(double r) : radius(r) {}
double getArea() {
return 3.14 * radius * radius;
}
private:
double radius;
};
int main() {
Shape* shape;
Rectangle rect(4, 5);
Circle circle(3);
shape = ▭
cout << "Rectangle area: " << shape->getArea() << endl;
shape = &circle;
cout << "Circle area: " << shape->getArea() << endl;
return 0;
}
在上面的例子中,Shape和其派生类Shape和其派生类Rectangle和Circle都重写了虚函数getArea。当基类指针(shape)指向派生类对象时,调用虚函数getArea时,会使用派生类对象的虚表指针来查找其实际实现,从而实现多态性。
总之,虚函数和动态联编是C++中非常重要的概念,它们可以方便地实现多态性,满足面向对象编程的需求。
13.5、访问控制:protected
在C++中,类的成员变量和成员函数默认情况下是private
访问控制。这意味着,只有类的本身和其友元类可以访问这些成员。
但是,有时候我们希望某些成员可以被其派生类访问,但却不能被其他类或函数访问。在这种情况下,可以使用protected
关键字对成员进行声明。
将成员声明为protected
,意味着这些成员可以被该类的成员函数访问,也可以被该类的派生类的成员函数访问。但是,其他类和函数仍然不能直接访问这些成员。
以下是一个示例,展示了如何使用protected
关键字:
#include <iostream>
class Animal {
public:
void eat() {
std::cout << "Eating..." << std::endl;
}
void sleep() {
std::cout << "Sleeping..." << std::endl;
}
protected:
int age;
};
class Dog : public Animal {
public:
void setAge(int a) {
age = a;
}
void bark() {
std::cout << "Barking..." << std::endl;
}
};
int main() {
Dog myDog;
myDog.setAge(3);
myDog.eat();
myDog.sleep();
myDog.bark();
return 0;
}
在上面的代码中,Animal类的age
成员被声明为protected
,这意味着它可以被Dog
类及其子类的成员访问。Dog
类继承自Animal
类,并可以使用setAge
函数来设置age
值。Dog
类也定义了自己的成员函数bark
,可以被调用来让狗发出叫声。
总之,protected
关键字允许我们在一个类的派生类中访问基类的成员,同时防止其他类和函数直接访问这些成员。
13.6、抽象基类
在面向对象的程序设计中,有时候我们希望某些类可以被其他类继承,并实现其成员函数,但又不希望这个基类被实例化。这时我们可以使用抽象基类(abstract base class)来实现这个需求。
抽象基类是指它包含至少一个纯虚函数(pure virtual function)的类,纯虚函数是在基类中声明但没有实现的虚函数。抽象基类不能被直接实例化,只能通过其派生类来创建对象。由于至少有一个纯虚函数没有实现,因此抽象基类的派生类必须实现所有的纯虚函数才能被实例化。
下面是一个抽象基类的例子:
class Shape {
public:
virtual double area() const = 0;
virtual double perimeter() const = 0;
};
class Circle : public Shape {
public:
Circle(double r) : radius(r) {}
virtual double area() const {
return 3.14 * radius * radius;
}
virtual double perimeter() const {
return 2 * 3.14 * radius;
}
private:
double radius;
};
class Rectangle : public Shape {
public:
Rectangle(double w, double h) : width(w), height(h) {}
virtual double area() const {
return width * height;
}
virtual double perimeter() const {
return 2 * (width + height);
}
private:
double width, height;
};
在上面的代码中,Shape
类是一个抽象基类,包含两个纯虚函数area
和perimeter
,它不能被直接实例化。Circle
和Rectangle
类继承自Shape
类,必须实现基类的纯虚函数,才能被实例化。
抽象基类在面向对象的程序设计中有很多应用,例如将共同的接口抽象出来,以统一不同对象的使用方式;或者在框架设计中,定义基本的数据结构和算法,以便派生类来定义特定的应用。
13.6.1、应用ABC概念
ABC(abstract base class)概念在面向对象的程序设计中有很多应用。下面简单介绍几个应用:
1、接口和多态性
抽象基类通常用于定义接口,可以将类的公共接口抽象出来,从而使得不同的类可以实现相同的接口,而且代码的可读性和可维护性也会得到提高。同时,由于使用了虚函数,派生类的实际实现可以根据需要来进行动态绑定,实现多态性。
2、插件机制
在大型软件系统中,插件机制使得开发人员可以通过提供插件来扩展现有系统的功能。抽象基类在插件开发中有着重要的作用,可以定义插件接口的公共部分,同时也可以隐藏实现的细节,从而实现灵活的插件机制。
3、设计模式
抽象基类在设计模式中也有很多应用,例如工厂模式、策略模式等。在工厂模式中,抽象基类通常用于定义工厂接口,从而使得不同的工厂可以实现相同的接口,而客户端代码则可以统一地使用这些工厂。在策略模式中,抽象基类可以表示不同的策略,而派生类则实现具体的策略,从而实现动态地选择不同的策略。
在总体上,ABC概念有助于将程序的结构分解为层次结构,提高代码的可重用性和可维护性。
13.6.2、ABC理念
ABC(Abstract Base Class)理念是一种面向对象编程的设计模式,它强调使用抽象基类来定义接口和实现多态性。ABC理念的核心思想是: “为了实现灵活的设计,依靠高层抽象而非低层细节”。
ABC理念将软件系统的设计分解为多个层次,每个层次都是由抽象基类来定义公共接口。它不仅隔离了代码的实现细节,还为软件系统提供了一种强大的扩展和定制功能的方式。
ABC理念对于构建大型的软件系统具有很大的优势,它将系统的设计分解为多个层次,每个层都可以通过抽象基类来进行定义,系统的各个模块间可以通过接口来进行通信,不同的模块之间的相互影响也最小化。
值得注意的是,ABC难以被实例化,无法直接使用,必须通过它的派生类对其进行实例化。这也意味着,ABC可以作为接口来使用,使不同的模块实现相同的功能,从而提高程序的可读性和可维护性。同时,通过使用虚函数机制,ABC可以实现多态性,使得同一个接口在不同的环境下表现出不同的行为。
总之,ABC理念在面向对象的程序设计中具有重要的作用,它可以帮助程序员更好地完成程序设计中的抽象和分层,同时提高程序的可维护性和可重用性。
13.7、继承和动态内存分配
在C++中,派生类对象包含了基类子对象,因此如果使用动态内存分配来创建派生类对象,需要注意基类子对象的构造和析构。
当使用new
操作符来创建派生类对象时,需要先为基类子对象分配内存空间,再为派生类对象分配内存空间。此时,需要调用基类构造函数来初始化基类子对象。
下面以一个简单的例子来说明继承和动态内存分配的关系:
#include <iostream>
using namespace std;
class Base {
public:
Base() {
cout << "Base Constructor" << endl;
}
~Base() {
cout << "Base Destructor" << endl;
}
};
class Derived : public Base {
public:
Derived() {
cout << "Derived Constructor" << endl;
}
~Derived() {
cout << "Derived Destructor" << endl;
}
};
int main() {
Derived *ptr = new Derived;
delete ptr;
return 0;
}
运行上面的程序,可以看到输出结果为:
Base Constructor
Derived Constructor
Derived Destructor
Base Destructor
从输出结果中可以看出,在创建Derived
对象时,先调用了Base
类的构造函数,再调用Derived
类的构造函数。在删除Derived
对象时,先调用了Derived
类的析构函数,再调用Base
类的析构函数。
这是因为当使用new
操作符来创建派生类对象时,先为基类子对象分配内存空间,再为派生类对象分配内存空间。析构时的顺序正好与构造时相反。
在动态内存分配中,如果没有正确地释放基类子对象的内存,会导致内存泄漏。因此,在使用动态内存分配时,必须小心处理基类子对象的构造和析构。
13.7.1、第一种情况:派生类不使用new
在C++中,派生类对象包含了基类子对象,因此如果不使用动态内存分配来创建派生类对象,也需要注意基类子对象的构造和析构。
当使用栈来创建派生类对象时,派生类对象和基类子对象都会分配在栈上,此时,在创建派生类对象时,也需要先调用基类构造函数来初始化基类子对象。
下面以一个简单的例子来说明派生类不使用new
时的情况:
#include <iostream>
using namespace std;
class Base {
public:
Base() {
cout << "Base Constructor" << endl;
}
~Base() {
cout << "Base Destructor" << endl;
}
};
class Derived : public Base {
public:
Derived() {
cout << "Derived Constructor" << endl;
}
~Derived() {
cout << "Derived Destructor" << endl;
}
};
int main() {
Derived obj;
return 0;
}
运行上面的程序,可以看到输出结果为:
Base Constructor
Derived Constructor
Derived Destructor
Base Destructor
从输出结果中可以看出,在创建Derived
对象时,先调用了Base
类的构造函数,再调用Derived
类的构造函数。在删除Derived
对象时,先调用了Derived
类的析构函数,再调用Base
类的析构函数。
与使用动态内存分配时的情况相同,需要注意正确地释放基类子对象的内存,否则会导致内存泄漏。
13.7.2、第二种情况:派生类使用new
在C++中,如果派生类对象使用动态内存分配来创建,需要先为基类子对象分配内存空间,再为派生类对象分配内存空间。此时需要注意在创建和销毁派生类对象时的基类子对象的构造和析构。
下面以一个使用new
操作符来创建派生类对象,并使用delete
操作符来销毁对象的例子来说明:
#include <iostream>
using namespace std;
class Base {
public:
Base() {
cout << "Base Constructor" << endl;
}
~Base() {
cout << "Base Destructor" << endl;
}
};
class Derived : public Base {
public:
Derived() {
cout << "Derived Constructor" << endl;
}
~Derived() {
cout << "Derived Destructor" << endl;
}
};
int main() {
Derived *ptr = new Derived();
delete ptr;
return 0;
}
运行上述程序,可以看到输出结果为:
Base Constructor
Derived Constructor
Derived Destructor
Base Destructor
从输出结果中可以看出,在创建Derived
对象时,先调用了Base
类的构造函数,再调用了Derived
类的构造函数。在销毁Derived
对象时,先调用了Derived
类的析构函数,再调用了Base
类的析构函数。
需要注意的是,如果使用动态内存分配时没有正确地释放基类子对象的内存,会导致内存泄漏。因此,在使用动态内存分配时,必须小心处理基类子对象的构造和析构。
13.7.3、使用动态内存分配和友元的继承示例
下面是一个使用动态内存分配和友元的继承示例。
#include <iostream>
class A {
public:
A() {
std::cout << "A()" << std::endl;
}
~A() {
std::cout << "~A()" << std::endl;
}
};
class B : public A {
private:
int* m_data;
public:
B() {
std::cout << "B()" << std::endl;
m_data = new int[10];
}
~B() {
std::cout << "~B()" << std::endl;
delete[] m_data;
}
friend void bar(B& b);
};
void bar(B& b) {
std::cout << "Accessing private data of B:" << std::endl;
for (int i = 0; i < 10; ++i) {
std::cout << b.m_data[i] << std::endl;
}
}
int main() {
B* b = new B();
bar(*b);
delete b;
return 0;
}
这个示例中,A
是一个基类,B
是一个派生类,B
含有一个动态分配的整数数组m_data
,同时还有一个友元函数bar
,可以用来访问B
的私有成员。
在main
函数中,我们首先通过new
操作符动态分配了一个B
对象,然后通过bar
函数访问了B
对象的私有成员m_data
,最后又通过delete
操作符销毁了这个对象。
运行上述程序,可以得到如下输出:
A()
B()
Accessing private data of B:
0
0
0
0
0
0
0
0
0
0
~B()
~A()
从输出结果可以看出,在创建B
对象时,首先调用了A
的构造函数,再调用了B
的构造函数;在删除B
对象时,先调用了B
的析构函数,再调用了A
的析构函数。
同时,通过bar
函数可以成功访问了B
对象的私有成员m_data
。由于bar
是B
的友元函数,因此可以访问B
对象的私有成员。
13.8、类设计回顾
在C++中,类设计是面向对象编程的核心,一个好的类设计需要考虑以下几个方面:
-
封装:将数据成员和成员函数进行分离,通过访问控制符将数据成员封装起来,防止外部程序直接访问类的私有成员。
-
继承:使用继承关系来建立类之间的关系,继承可以让派生类重用基类的成员和方法,并且可以通过虚函数等方式实现多态性。
-
多态:使用虚函数和抽象类等机制可以实现运行时的多态性,也就是在程序运行时根据对象的实际类型来调用相应的成员函数。多态可以让程序更加灵活和易扩展。
-
重载:使用函数重载可以让函数具有不同的参数列表,使程序更加清晰和易读。
-
模板:使用模板可以让类和函数具有泛化的能力,可以为不同的类型提供相同的功能。
一个好的类设计需要考虑到上述几个方面,以实现数据和方法的封装、复用、多态和可扩展性,这样设计的程序可以更加灵活和易于维护。
13.8.1、编译器生成的成员函数
在C++中,如果没有显式地声明一个成员函数,编译器会自动生成默认版本的成员函数。这些默认生成的成员函数包括:
-
默认构造函数(Default Constructor):当类的对象被默认初始化时调用。
-
拷贝构造函数(Copy Constructor):当类的对象通过值传递的方式传递给函数或者以值的方式返回时调用。
-
析构函数(Destructor):当类的对象生命周期结束时自动调用。
-
拷贝赋值运算符(Copy Assignment Operator):当类的对象被赋值为另一个对象时调用。
-
移动构造函数(Move Constructor)和移动赋值运算符(Move Assignment Operator):在C++11标准引入,用于处理右值引用的操作。当类的对象被移动时(例如,使用
std::move
函数)触发调用。
这些默认生成的成员函数可以满足大多数情况下的需要,但如果需要更精细地控制类的行为,可以自己声明并实现这些成员函数。此外,在某些情况下,如果不希望生成这些函数(例如,禁止对象复制或移动),可以通过显式删除它们来禁用这些函数的生成。
13.8.2、其他的类方法
除了默认生成的成员函数,类还可以定义其他的方法来实现特定的功能。下面介绍几种常见的类方法:
-
构造函数(Constructor):用于初始化类的对象,在创建类的对象时调用。可以根据不同的参数定义不同的构造函数,也可以使用默认构造函数。
-
析构函数(Destructor):用于清理类的对象,在对象生命周期结束时调用。析构函数的作用是释放动态分配的内存、关闭打开的文件句柄等。
-
成员函数(Member Function):在类定义中声明并定义的函数,用于对类的成员进行封装和封装的操作。成员函数可以是类的公有、私有或保护成员,根据需要定义相应的访问控制符。
-
静态成员函数(Static Member Function):在类定义中声明并定义的静态函数,用于对类的静态成员进行封装和操作。静态成员函数不属于任何类对象,可以直接通过类名进行调用。
-
友元函数(Friend Function):在类定义外部声明和定义,并使用
friend
关键字与类进行关联的函数。友元函数可以访问类的私有成员,但不是类的成员函数,因此不能直接调用类的成员函数。 -
运算符重载函数(Operator Overloading Function):定义在类内或类外的特殊函数,用于重载运算符的操作。运算符重载函数的名称需要以关键字
operator
开头。
类方法的定义和实现需要考虑到类的封装性、复用性和可扩展性,合理定义类方法可以使程序更加灵活和易于维护。
13.8.3、共有继承的考虑因素
在C++中,共有继承是最常见的继承方式,它意味着派生类可以访问基类中的公有成员和受保护成员。当在设计一个新类的时候需要使用继承机制时,需要考虑以下因素:
-
类的继承关系是否可行:在使用继承关系之前,需要仔细考虑类之间的关系,确保所建立的继承关系是合理的。
-
是否需要访问基类的成员:如果派生类需要访问基类的成员,那么就需要使用共有继承来实现这个目的。
-
基类的访问控制:如果基类中的成员都是共有的,那么使用共有继承可以直接访问这些成员。如果基类中的成员是受保护或私有的,那么派生类需要提供一个公有的接口来访问这些成员。
-
派生类的行为是否需要改变:如果派生类需要改变基类的行为,那么应该使用虚函数来实现多态性。如果不需要改变基类的行为,那么可以直接使用共有继承。
-
是否需要实现多重继承:如果需要同时继承多个基类,那么就需要使用多重继承。多重继承可能会增加代码的复杂度和难度,需要在使用时进行适当的权衡。
在设计中,需要根据具体情况选择不同的继承方式,或者使用组合等其他方式来实现类的功能。同时,在使用继承关系时,需要遵循封装、复用、多态等面向对象的设计原则,以建立一个有效的、可维护的程序。
13.8.4、类函数小结
类函数是C++中面向对象编程的核心。类函数分为构造函数、析构函数、成员函数和静态成员函数等,针对性不同的任务分别实现,包括对象的初始化和清理、面向对象编程的封装、类的成员数据和对其进行操作、类的静态成员和对其进行操作,以及友元函数和运算符重载函数等。在实现类函数时,需要从封装、复用、多态等面向对象的角度设计类函数,确保类函数与类其他组成部分之间的协同合作。同时,在使用继承关系时,需要根据具体情况选择不同的继承方式,以建立一个有效的、可维护的程序。