php做网站的技术难点,免费网站安全软件大全下载安装,淘宝补流量平台,影视剪辑培训班一、引言 C 作为一种强大的编程语言#xff0c;继承机制在面向对象编程中扮演着至关重要的角色。它允许开发者基于已有的类创建新的类#xff0c;从而实现代码的复用和功能的扩展。然而#xff0c;继承的概念和使用方法并非一目了然#xff0c;特别是在处理复杂的继承关系时…一、引言 C 作为一种强大的编程语言继承机制在面向对象编程中扮演着至关重要的角色。它允许开发者基于已有的类创建新的类从而实现代码的复用和功能的扩展。然而继承的概念和使用方法并非一目了然特别是在处理复杂的继承关系时需要对其原理和规则有深入的理解。本文将详细阐述 C 中继承的各个方面包括基本概念、访问权限、作用域以及默认成员函数等通过实际的代码示例帮助读者更好地掌握这一重要特性。 二、继承的基本概念与定义
一概念
在 C 中继承是一种面向对象程序设计的机制它使程序员能够在已有类基类或父类的基础上创建新的类派生类或子类并对其功能进行扩展或修改。这种机制有效地复用了已有代码同时通过构建层次化的类结构展现了面向对象编程从简单到复杂的演变过程。例如假设有一个基类Vehicle它定义了一些基本的车辆属性如车轮数量和颜色。现在要创建一个Car类除了继承这些基本属性外还需要增加一些汽车特有的属性如座位数量。通过继承Car类可以复用Vehicle类的代码无需重新编写已有的属性定义。
cpp
class Vehicle {
public:void printInfo() {cout Wheel number: _wheelNumber endl;cout Color: _color endl;}protected:int _wheelNumber 4;string _color black;
};// Car类继承自Vehicle类
class Car : public Vehicle {
protected:int _seatNumber;
};在上述代码中Car类继承了Vehicle类的成员函数和成员变量这意味着Car类包含了_wheelNumber和_color两个属性以及printInfo()函数。通过继承实现了代码的复用。
二定义
C 中继承的定义格式如下
cpp
class 子类名 : 继承方式 基类名 {// 子类的成员
};其中继承方式可以是public、protected或private它们决定了基类的成员在派生类中的访问权限。
public继承基类的public成员在派生类中保持publicprotected成员保持protected。protected继承基类的public成员在派生类中变为protectedprotected成员保持protected。private继承基类的public和protected成员在派生类中均变为private。
以下是一个示例代码
cpp
class Truck : public Vehicle {
protected:int _loadCapacity;
};int main() {Car c;Truck t;c.printInfo();t.printInfo();return 0;
}在这个示例中Car和Truck都继承了Vehicle类的printInfo()函数通过c.printInfo()和t.printInfo()可以分别输出Car和Truck对象的车辆信息。
三、继承中的访问权限
一基类成员在派生类中的访问权限
基类的public、protected和private成员在派生类中的访问权限取决于继承方式。下面是不同继承方式下的访问权限表
类成员public继承protected继承private继承基类的public成员publicprotectedprivate基类的protected成员protectedprotectedprivate基类的private成员不可见不可见不可见
从表中可以看出基类的private成员在派生类中始终不可见不可访问无论采用何种继承方式。然而基类的protected成员和public成员则根据继承方式在派生类中具有不同的访问级别。 需要注意的是如果需要基类的某个成员在派生类中可访问但不希望类外部访问则可以将其设置为protected这样可以更好地控制访问权限。
二基类与派生类对象的赋值转换
在 C 中基类和派生类对象的赋值转换是一个常见的操作场景。通常情况下派生类对象可以赋值给基类对象或者通过基类的指针或引用来操作派生类对象。这种转换机制使得 C 在继承结构中实现了多态和代码复用。但需要注意的是基类对象不能直接赋值给派生类对象。
1. 派生类对象赋值给基类对象
派生类对象包含了基类的成员因此派生类对象赋值给基类对象时实际上是将派生类中属于基类的那一部分赋值给基类对象。这种操作称为切片Slicing即派生类对象中的基类部分被切割下来赋值给基类对象。
以下是一个示例代码
cpp
class Animal {
public:string _name;
protected:int _age;
} ;class Dog : public Animal {
public:int _breedId;
};int main() {Dog d;d._name Buddy;d._breedId 1;Animal a d; // 切片操作将派生类对象赋值给基类对象cout Name: a._name endl; // 输出 Buddy// cout a._breedId; // 错误基类对象无法访问派生类的成员return 0;
}在上述代码中Dog对象d被赋值给Animal对象a。由于Animal类没有_breedId成员a无法访问Dog类中的_breedId成员。因此这里发生了切片操作a只保留了Dog类中Animal类的那部分内容
2. 基类指针和引用的转换
派生类对象可以赋值给基类的指针或引用这是实现多态的重要前提条件。通过基把基类指针或引用程序可以在运行时动态绑定到派生类的成员函数。这种方式允许我们在不需要修改代码的情况下扩展程序的功能。
以下是一个示例代码
cpp
class Shape {
public:virtual void draw() {cout Drawing a shape endl;}
protected:string _name Shape;
} ;class Circle : public Shape {
public:void draw() override {cout Drawing a circle endl;}
private:int _radius 5;
} ;void drawShape(Shape s) {s.draw(); // 基类引用调用虚函数实现多态
}int main() {Circle c;drawShape(c); // 输出 Drawing a circlereturn 0;
}在这个例子中我们通过基类Shape的引用调用Circle类中的draw()函数实现了运行时多态。派生类对象c被传递给基类引用s并正确调用了Circle类的重写函数draw()。
3. 强制类型转换的使用
在某些特殊情况下基类指针或引用可能需要转换为派生类的指针或引用。C 提供了dynamic_cast、static_cast等多种类型转换方式。在继承关系中使用dynamic_cast进行安全的类型转换尤为重要特别是在处理多态时。
以下是一个示例代码
cpp
Shape* sp new Circle(); // 基类指针指向派生类对象
Circle* cp dynamic_castCircle*(sp); // 安全的向下转换
if (cp) {cp-draw();
} else {cout Type conversion failed! endl;
}dynamic_cast在运行时进行类型检查确保转换是安全的。如果转换失败将返回nullptr从而避免越界访问的风险。
四、继承中的作用域与成员访问
一作用域的独立性与同名成员的隐藏
在继承关系中基类与派生类各自拥有独立的作用域。如果派生类中定义了与基类成员同名的变量或函数基类的同名成员将被隐藏这种现象称为隐藏Hiding。也叫重定义同名成员在派生类中会覆盖基类中的成员导致基类成员无法被直接访问。
以下是一个示例代码
cpp
class Parent {
protected:int _id 111; // 身份证号
} ;class Child : public Parent {
public:Child(int id) : _id(id) { } // 派生类中的_id覆盖了基类中的_idvoid print() {cout 身份证号: Parent::_id endl; // 访问基类中的_idcout 孩子编号: _id endl; // 访问派生类中的_id}protected:int _id; // 孩子编号
} ;int main() {Child c(999);c.print(); // 输出身份证号和孩子编号return 0;
}在这个例子中Child类中定义了一个_id变量它隐藏了基类Parent中的同名变量。为了访问基类的_id我们使用了Parent::_id来显式地指定访问基数类中的成员。这样可以避免由于成员同名而导致的混淆。需要注意的是在实际的继承体系中最好不要定义同名的成员。
二函数的隐藏
同名成员函数也会构成隐藏只要函数名称相同即使参数列表不同也会发生隐藏。这种行为和函数重载不同。在派生类中如果我们希望访问基类中的同名函数必须显式调用基类的函数。
以下是一个示例代码
cpp
class Base {
public:void function() {cout Base::function() endl;}
} ;class Derived : public Base {
public:void function(int i) { // 隐藏了基类的function()cout Derived::function(int i) - i endl;}
} ;int main() {Derived d;d.function(10); // 调用Derived::function(int i)d.Base::function(); // 显式调用基类的function()return 0;
}在此代码中派生类Derived中的function(int i)函数隐藏了基类Base中的function()函数。如果我们希望调用基类的function()函数必须通过d.Base::function()来显式调用。这与函数重载不同函数隐藏仅要求函数名相同而不考虑参数列表。并且函数重载说的是同一作用域而这里基类和派生类是两个作用域。
五、派生类的默认成员函数
在 C 中当我们不显式定义类的构造函数、拷贝构造函数、赋值运算符和析构函数时编译器会自动为我们生成这些函数。这些自动生成的函数在派生类中也会涉及到对基类成员的操作因此在继承体系中了解这些默认成员函数的调用规则非常重要。
一构造函数的调用顺序
在派生类对象的构造过程中基类的构造函数会优先于派生类的构造函数被调用。如果基类没有默认构造函数则派生类的构造函数必须在初始化列表中显式调用基类的构造函数。
以下是一个示例代码
cpp
class BaseClass {
public:BaseClass(const string name) : _name(name) {cout BaseClass constructor called! endl;}protected:string _name;
} ;class DerivedClass : public BaseClass {
public:DerivedClass(const string name, int value) : BaseClass(name), _value(value) {cout DerivedClass constructor called! endl;}private:int _value;
} ;int main() {DerivedClass d(Alice, 12345);return 0;
}输出cpp
BaseClass constructor called!
DerivedClass constructor called!在这个例子中DerivedClass类的构造函数首先调用了BaseClass类的构造函数来初始化基类部分。随后才执行派生类DerivedClass的构造函数。这种调用顺序确保基类的成员在派生类构造之前就已经被正确初始化。
二拷贝构造函数与赋值运算符的调用
当派生类对象被拷贝时基类的拷贝构造函数会先被调用然后才是派生类的拷贝构造函数。同样赋值运算符的召唤顺序也遵循这一规则基类的赋值运算符会先于派生类的赋值运算符被调用。
以下是一个示例代码
cpp
class OriginalClass {
public:OriginalClass(const string name) : _name(name) { }// 拷贝构造函数OriginalClass(const OriginalClass o) {_name o._name;cout OriginalClass copy constructor called! endl;}// 赋值运算符OriginalClass operator(const OriginalClass o) {_name o._name;cout OriginalClass assignment operator called! endl;return *this;}protected:string _name;
} ;class NewClass : public OriginalClass {
public:NewClass(const string name, int value) : OriginalClass(name), _value(value) { }// 拷贝构造函数NewClass(const NewClass n) : OriginalClass(n) {_value n._value;cout NewClass copy constructor called! endl;}// 出版设赋值运算符NewClass operator(const NewClass n) {OriginalClass::operator(n); // 先调用基类的赋值运算符_value n._value;cout NewClass assignment operator called! endl;return *this;}private:int _value;
} ;int main() {NewClass n1(Alice, 12345);NewClass n2 n1; // 拷贝构造函数NewClass n3(Bob, 54321);n3 n1; // 赋值运算符return 0;
}输出cpp
OriginalClass copy constructor called!
NewClass copy constructor called!
OriginalClass assignment operator called!
NewClass assignment operator called!在拷贝构造和赋值操作过程中基类部分总是优先于派生类部分进行初始化或赋值操作。为了保证派生类对象的完整性派生类的拷贝构造函数和赋值运算符必须调用基类的相应函数确保基类成员正确处理。
三析构函数的召唤顺序
与构造函数的召唤顺序相反析构函数的召唤顺序是先召唤派生类的析构函数然后再召唤基类的析构函数。这确保了派生类的资源先被释放然后基类的资源才能安全地释放。
以下是一个示例代码
cpp
class BaseObject {
public:BaseObject(const string name) : _name(name) { }~BaseObject() {cout BaseObject destructor called! endl;}//xprotected:string _name;
} ;class DerivedObject : public BaseObject {
public:DerivedObject(const string name, int value) : BaseObject(name), _value(value) { }~DerivedObject() {cout DerivedObject destructor called! endl;}private:int _value;
} ;int main() {DerivedObject d(Alice, 12345);return 0;
}输出cpp
DerivedObject destructor called!
BaseObject destructor called!可以看到当DerivedObject对象d析构时首先召唤了DerivedObject的析构函数随后召唤了BaseObject的析构函数。这种析构顺序确保派生类资源如成员变量_value被先行清理而基类的资源如_name则在派生类资源清理后再进行释放。
四虚析构函数
在继承体系中若希望基类指针指向派生类对象并通过该指针安全地释放对象基类的析构函数应当定义为虚函数。否则仅会调用基类的析构函数导致派生类资源没有正确释放从而引发内存泄漏。
以下是一个示例代码
class Person {
public:Person(const string name) : _name(name) {}virtual ~Person() {cout Person destructor called! endl;}protected:string _name;
};class Student : public Person {
public:Student(const string name, int stuid) : Person(name), _stuid(stuid) {}~Student() {cout Student destructor called! endl;}private:int _stuid;
};int main() {Person* p new Student(Alice, 12345);delete p; // 安全删除先调用派生类的析构函数return 0;
}通过将基类的析构函数声明为 virtual当通过基类指针删除派生类对象时派生类的析构函数将首先被调用从而确保所有派生类的资源被正确释放。