赞
踩
面向对象程序设计中最重要的一个概念是继承。继承允许我们依据另一个类来定义一个类,这使得创建和维护一个应用程序变得更容易。这样做,也达到了重用代码功能和提高执行效率的效果。
当创建一个类时,您不需要重新编写新的数据成员和成员函数,只需指定新建的类继承了一个已有的类的成员即可。这个已有的类称为基类,新建的类称为派生类。
继承格式:
class derived-class: access-specifier base-class
Person是⽗类,也称作基类。Student是⼦类,也称作派⽣类 :
public
、protected
和private
,它们分别控制成员的可访问性。具体的继承后访问权限如下:
类成员/继承方式 | public继承 | protected继承 | private继承 |
---|---|---|---|
父类的public成员 | 子类的public成员 | 子类的protected成员 | 子类的private成员 |
父类的protected成员 | 子类的protected成员 | 子类的protected成员 | 子类的private成员 |
父类的private成员 | 在子类中不可见 | 在子类中不可见 | 在子类中不可见 |
private
成员在子类中的访问限制父类的private
成员在子类中是不可见的。这意味着,虽然子类对象中仍然包含父类的private
成员,但语法上子类无法访问这些成员,无论是在子类的内部还是外部。
protected
成员的使用场景父类的private
成员在子类中不能被访问。如果需要父类成员在类外不能直接访问,但在子类中能够访问,那么应该将这些成员定义为protected
。protected
成员限定符主要是为了解决继承中的访问控制问题而出现的。
通过继承方式和父类成员的访问限定符,可以总结出父类的其他成员在子类中的访问方式:
public
> protected
> private
子类对父类成员的访问权限是取父类成员的访问限定符与继承方式的最小值。
在使用关键字class
时,默认的继承方式是private
。而使用关键字struct
时,默认的继承方式是public
。尽管如此,最好显式地写出继承方式以提高代码的可读性。
class Base {
private:
int privateMember;
protected:
int protectedMember;
public:
int publicMember;
};
class Derived : public Base {
// 继承方式为public,访问权限如下:
// privateMember:不可见
// protectedMember:protected
// publicMember:public
};
在实际应用中,通常使用public
继承,很少使用protected
或private
继承。原因在于protected
或private
继承的成员只能在子类内部使用,限制了代码的扩展性和可维护性。
class Base { private: int privateMember; protected: int protectedMember; public: int publicMember; }; class PublicDerived : public Base { // privateMember:不可见 // protectedMember:protected // publicMember:public }; class ProtectedDerived : protected Base { // privateMember:不可见 // protectedMember:protected // publicMember:protected }; class PrivateDerived : private Base { // privateMember:不可见 // protectedMember:private // publicMember:private };
“is a”关系:通过继承(Inheritance)来表示,表示类之间的层次关系。
“is a”关系通常表示继承(Inheritance)关系,也就是一个类是另一个类的特殊类型。比如,狗(Dog)是动物(Animal)的一种,我们可以通过继承来表示这种关系:
class Animal {
public:
void makeSound() {
std::cout << "Animal sound" << std::endl;
}
};
class Dog : public Animal { // Dog is an Animal
public:
void makeSound() {
std::cout << "Bark" << std::endl;
}
};
在这个例子中,Dog
类继承自Animal
类,这表明“狗是一种动物”(Dog is an Animal)。Dog
类可以访问Animal
类中的公共成员函数和变量。
“has a”关系:通过组合(Composition)或聚合(Aggregation)来表示,表示一个类拥有另一个类的实例。
“has a”关系通常表示组合(Composition)或聚合(Aggregation)关系,即一个类包含另一个类作为其成员。这种关系强调一个类拥有另一个类的实例。比如,汽车(Car)有一个引擎(Engine),可以用组合来表示这种关系:
class Engine { public: void start() { std::cout << "Engine starts" << std::endl; } }; class Car { // Car has an Engine private: Engine engine; public: void startCar() { engine.start(); std::cout << "Car starts" << std::endl; } };
在这个例子中,Car
类包含一个Engine
类的实例,这表明“汽车有一个引擎”(Car has an Engine)。Car
类可以使用Engine
类中的方法来实现其功能。
template<class T>
class Base {
// 基类内容
};
template<class T>
class Derived : public Base<T> {
// 派生类内容
};
public
、protected
和private
。template<class T>
class Derived : public Base<T> {
public:
void foo() {
Base<T>::bar(); // 调用基类的bar函数
}
};
名称查找与依赖名称的问题主要源于模板的按需实例化机制和两阶段名称查找机制
两阶段名称查找
C++编译器对模板代码进行两次名称查找:
- 第一次名称查找:在模板定义时进行。编译器解析所有与模板参数无关的非依赖名称。
- 第二次名称查找:在模板实例化时进行。编译器解析依赖于模板参数的名称,即依赖名称。
依赖名称(Dependent Names)是指那些依赖于模板参数的名称。在第一次名称查找时,编译器无法确定这些名称的具体含义,只有在模板实例化时才能解析。
template <typename T> class Base { public: void foo() { std::cout << "Base foo" << std::endl; } }; template <typename T> class Derived : public Base<T> { public: void bar() { // 问题点:编译器在第一次名称查找时不知道foo()是从Base<T>继承的 // 因为foo()是依赖于模板参数T的名称 // foo(); // 这会导致编译错误 // 解决方法1:使用this指针 this->foo(); // 解决方法2:使用作用域解析符 Base<T>::foo(); } }; int main() { Derived<int> d; d.bar(); // 输出 "Base foo" return 0; }
编译器会在第一次名称查找时尝试解析foo()
。但是由于foo()
是依赖于模板参数T
的成员函数,编译器无法确定foo()
是从基类继承的。这是因为模板是按需实例化的,编译器在第一次查找时并不知道派生类实例化时会包含哪些基类成员。
在使用Derived<int> d;
初始化的时候会对构造函数进行实例化并调用构造函数,但是当使用d.bar();
时,如果在bar()
中为foo();
即会编译错误,原因就如上述,无法确定从基类继承。
所以解决如下:
**this**
指针:void bar() {
this->foo(); // 正确
}
编译器会在第二阶段名称查找时解析foo()
,并正确地找到基类中的foo()
成员函数。这是因为this
指针在类定义中总是已知的,并且它指向当前对象**(包括从基类继承的部分)**。
void bar() {
Base<T>::foo(); // 正确
}
Base<T>::foo()
明确指出了foo()
来自基类Base<T>
,消除了编译器的名称查找歧义。
在公有继承中,子类对象可以赋值给父类对象、父类指针或父类引用(把⼦类中⽗类那部分切来赋值过去)。这种转换称为向上转换(upcasting)。
class Base { public: void baseMethod() { std::cout << "Base method" << std::endl; } }; class Derived : public Base { public: void derivedMethod() { std::cout << "Derived method" << std::endl; } }; int main() { Derived derivedObj; Base baseObj = derivedObj; // 子类对象赋值给父类对象 Base* basePtr = &derivedObj; // 子类对象的地址赋值给父类指针 Base& baseRef = derivedObj; // 子类对象赋值给父类引用 baseObj.baseMethod(); // 可以调用父类的方法 basePtr->baseMethod(); // 可以通过父类指针调用父类的方法 baseRef.baseMethod(); // 可以通过父类引用调用父类的方法 // 以下调用都会导致编译错误,因为父类对象/指针/引用不能访问子类特有的方法 // baseObj.derivedMethod(); // basePtr->derivedMethod(); // baseRef.derivedMethod(); return 0; }
父类对象不能赋值给子类对象,因为父类对象可能不包含子类对象所需的所有信息。这种转换会导致子类特有的数据丢失或变得不确定。
Base baseObj;
Derived derivedObj;
// 以下赋值会导致编译错误
// derivedObj = baseObj;
父类的指针或引用可以通过强制类型转换赋值给子类的指针或引用,但必须确保父类的指针实际上指向一个子类对象。这种转换称为向下转换(downcasting)
如果父类是多态类型,可以使用RTTI(运行时类型信息)中的dynamic_cast
来进行安全转换。
Base* basePtr = new Derived;
Derived* derivedPtr = dynamic_cast<Derived*>(basePtr);
if (derivedPtr) {
derivedPtr->derivedMethod(); // 安全转换后可以调用子类方法
} else {
// 转换失败,basePtr并不指向Derived对象
}
虽然可以使用static_cast
进行强制转换,但这种转换在父类指针不指向子类对象时是危险的。
Base* basePtr = new Derived;
Derived* derivedPtr = static_cast<Derived*>(basePtr);
derivedPtr->derivedMethod(); // 需要确保basePtr实际指向Derived对象
dynamic_cast
进行安全转换。⽗类::⽗类成员
显式访问)#include <iostream> // 不要忘记包含 iostream 头文件以使用 cout class A { public: void fun() { std::cout << "func()" << std::endl; } }; class B : public A { public: void fun(int i) { std::cout << "func(int i)" << i << std::endl; } }; int main() { B b; b.fun(10); // 调用 B 类的 fun(int i) b.fun(); // 尝试调用 A 类的 fun(),但由于重载,实际上调用的是 B 类的 fun(int i) return 0; }
b.fun();
)子类的构造函数必须调用父类的构造函数来初始化父类的那部分成员。如果父类没有默认构造函数,则必须在子类构造函数的初始化列表中显式调用父类的构造函数。
Student(const char* name, int num)
: Person(name), _num(num) {
cout << "Student()" << endl;
}
在初始化列表中可以注意初始化顺序,先声明的先初始化,所以先声明的父类会先定义。
子类的拷贝构造函数必须调用父类的拷贝构造函数来完成父类部分的拷贝初始化。
Student(const Student& s)
: Person(s), _num(s._num) {
cout << "Student(const Student& s)" << endl;
}
子类的赋值运算符必须调用父类的赋值运算符来完成父类部分的复制。需要注意的是,子类的赋值运算符会隐藏父类的赋值运算符,所以需要显式调用父类的赋值运算符。
Student& operator=(const Student& s) {
cout << "Student& operator=(const Student& s)" << endl;
if (this != &s) {
// 构成隐藏,所以需要显式调用
Person::operator=(s);
_num = s._num;
}
return *this;
}
不用再子类析构函数中显式调用父类的析构函数,子类的析构函数在被调用完成后,会自动调用父类的析构函数来清理父类成员。这样可以保证子类对象先清理子类成员再清理父类成员的顺序。
~Student() {
cout << "~Student()" << endl;
}
析构会按照后定义的先析,先调用子类析构,再调用父类析构。
多态中⼀些场景析构函数需要构成重写,重写的条件之⼀是函数名相同。那么编译器会对析构函数名进⾏特殊处理,处理成destructor()
,所以⽗类析构函数不加virtual
的情况下,⼦类析构函数和⽗类析构函数构成隐藏关系。
⼦类的operator=必须要调⽤⽗类的operator=完成⽗类的复制。需要注意的是⼦类的operator=隐
藏了⽗类的operator=,所以显⽰调⽤⽗类的operator=,需要指定⽗类作⽤域
student& operator=(const student& s)
{
if (this != &s)
{
person::operator=(s);
}
}
有两种方法可以使类不可以被继承:
- ⽗类的构造函数私有,⼦类的构成必须调⽤⽗类的构造函数,但是⽗类的构成函数私有化以后,⼦类看不⻅就不能调⽤了,那么⼦类就⽆法实例化出对象。
- C++11新增了⼀个
final
关键字,final
修改⽗类,⼦类就不能继承了。
class Base final
{
public:
void func5() { cout << "Base::func5" << endl; }
protected:
int a = 1;
private:
// C++98的⽅法
/*Base()
{}*/
}
友元关系不继承:
在C++中,友元关系是特定于某个类的。一个函数或类如果是父类的友元,它不会自动成为子类的友元。因此,父类的友元函数不能访问子类的私有成员和保护成员。同样地,如果你希望某个函数既是父类的友元,又是子类的友元,也可以在子类中声明该友元函数。
class Student; class Person { public: friend void Display(const Person& p, const Student& s); // 声明友元函数 protected: string _name; // 姓名 }; class Student : public Person { protected: int _stuNum; // 学号 }; void Display(const Person& p, const Student& s) { cout << p._name << endl; cout << s._stuNum << endl; // 尝试访问子类的保护成员,编译错误 } int main() { Person p; Student s; Display(p, s); // 编译报错:error C2248: “Student::_stuNum”: 无法访问 protected 成员 return 0; }
Display
函数是Person
类的友元,因此它可以访问Person
类的保护成员 _name
。但是,当它尝试访问Student
类的保护成员_stuNum
时,会产生编译错误。原因是友元关系不继承:Display
函数虽然是Person
的友元,但它不是Student
的友元,所以不能访问Student
的保护成员。
将Display
在子类中声明即可解决该问题:
class Student : public Person {
public:
friend void Display(const Person& p, const Student& s); // 友元函数也要声明在子类中
protected:
int _stuNum; // 学号
};
这样,Display
函数就能同时访问Person
和Student
的保护成员了。
在C++中,静态成员是属于类而不是某个特定对象的。⽗类定义了static
静态成员,则整个继承体系⾥⾯只有⼀个这样的成员,这意味着即使类派生出了多个子类,它们都共享同一个静态成员实例。
class Person { public: string _name; static int _count; }; int Person::_count = 0; // 静态成员初始化 class Student : public Person { protected: int _stuNum; }; int main() { Person p; Student s; // 非静态成员_name地址不同,说明子类继承后,父子类对象各有一份 cout << &p._name << endl; cout << &s._name << endl; // 静态成员_count地址相同,说明子类和父类共用同一个静态成员 cout << &p._count << endl; cout << &s._count << endl; // 公有的情况下,父子类都可以访问静态成员 cout << Person::_count << endl; cout << Student::_count << endl; return 0; }
运行结果:
0133FDE4
0133FDBC
0014E478
0014E478
0
0
_name
是一个非静态成员,在Person
和Student
对象中分别有独立的实例,所以它们的地址不同。_count
是一个静态成员,Person
和Student
共享同一个静态成员实例,因此它们的地址相同。- 无论是通过父类还是子类,都可以访问静态成员。
单继承是指一个子类只有一个直接父类。在这种情况下,子类继承父类的所有非私有成员,继承结构简单明了,访问成员变量也不存在歧义问题。
多继承是指一个子类有多个直接父类。C++支持多继承,这意味着一个子类可以从多个父类继承成员。在多继承中,C++规定在内存布局上,先继承的父类放在前面,后继承的父类放在后面,子类自己的成员放在最后。
class Person { public: string _name; // 姓名 }; class Student : public Person { protected: int _num; // 学号 }; class Teacher : public Person { protected: int _id; // 职工编号 }; class Assistant : public Student, public Teacher { protected: string _majorCourse; // 主修课程 };
问题:下⾯说法正确的是()
A:p1p2p3 B:p1<p2<p3
C:p1==p3!=p2 D:p1!=p2!=p3
class Base2 { public: int _b2; };
class Base1 { public: int _b1; };
class Derive : public Base1, public Base2 { public: int _d; };
int main()
{
Derive d;
Base1* p1 = &d;
Base2* p2 = &d;
Derive* p3 = &d;
return 0;
}
继承的时候会按照生命顺序来进行分配空间,也就是继承顺序。上述例子中先继承的是Base1
,后继承的是Base2
,所以按照规则栈会先为继承的Base1
的信息进行开辟空间(栈向下开辟空间),然后再为Base2
开辟空间,所以空间图如上图所示。
Base1* p1 = &d;
Base2* p2 = &d;
Derive* p3 = &d;
以上是用了继承中基类对于派生类的向上转换(会进行类似切片操作,详见上文),所以此时的指向为下图:
此时的p1
和p3
指向的是同一块地址,p2
指向的之后分配的继承了Base2
的空间。
正确答案为:
p1 == p3 != p2
菱形继承是多继承中的一种特殊情况,发生在一个子类通过两个不同的路径继承自同一个基类时,形成菱形结构。
这种继承方式会带来数据冗余和访问二义性的问题。
class Person { public: string _name; // 姓名 }; class Student : public Person { protected: int _num; // 学号 }; class Teacher : public Person { protected: int _id; // 职工编号 }; class Assistant : public Student, public Teacher { protected: string _majorCourse; // 主修课程 }; int main() { Assistant a; a._name = "peter"; // 编译报错:error C2385: 对“_name”的访问不明确 a.Student::_name = "xxx"; // 需要显式指定访问哪个父类的成员 a.Teacher::_name = "yyy"; // 但是数据冗余问题无法解决 return 0; }
Assistant
类中,由于Student
和Teacher
都继承了Person
,所以Assistant
中会有两份Person
的拷贝。换句话说,Assistant
类中有两份_name
成员,这样会导致内存上的浪费。Assistant
类中访问_name
时,编译器无法确定你想访问的是从Student
继承过来的_name
,还是从Teacher
继承过来的_name
,因此会报错。a.Student::_name = "xxx";
a.Teacher::_name = "yyy";
可以通过显式的指定访问的是哪个父类的成员,或者使用虚继承,即可解决当前问题。
不推荐使用菱形继承
虚继承(virtual inheritance)是C++中的一种特殊继承机制,用来解决多继承中的菱形继承问题,特别是避免数据冗余和访问二义性。
在多继承中,如果一个子类通过不同的路径从同一个基类继承,那么就会形成菱形继承。菱形继承会导致子类中存在多个基类实例,从而产生数据冗余和访问二义性的问题。虚继承通过修改基类在继承链中的存储方式,使得即使存在多重继承,所有子类中只会存在一个基类的实例,从而避免数据冗余和访问二义性。
class Person { public: string _name; // 姓名 }; // 使用虚继承Person类 class Student : virtual public Person { protected: int _num; // 学号 }; // 使用虚继承Person类 class Teacher : virtual public Person { protected: int _id; // 职工编号 }; class Assistant : public Student, public Teacher { protected: string _majorCourse; // 主修课程 }; int main() { // 使用虚继承,可以解决数据冗余和二义性 Assistant a; a._name = "peter"; return 0; }
当一个类通过virtual
关键字虚继承一个基类时,编译器确保在多继承链中该基类只会有一个实例。在上述示例中,Student
和Teacher
都虚继承自Person
,因此在Assistant
类中,Person
的实例只会有一个。
在普通继承中,每个子类都会在其对象中包含父类的成员。但在虚继承中,编译器通过在子类中存储一个指向基类的指针来避免冗余。这个指针指向了唯一的基类实例,确保整个继承体系中只存在一个基类实例。
Assistant
)的构造函数中被调用,而不是在虚继承的直接派生类(如Student
或Teacher
)中。派生类的构造函数负责初始化基类的那部分。class Assistant : public Student, public Teacher {
public:
Assistant(const string& name) : Person(name), Student(), Teacher() {}
};
在这个例子中,由于Person
是通过虚继承的,所以必须在Assistant
的构造函数中显式地调用Person
的构造函数来初始化_name
。
继承(Inheritance)和组合(Composition)是面向对象编程中两种重要的代码复用手段。它们在实际开发中各有优势和适用场景。
继承是一种is-a关系,表示子类是父类的一种特殊类型。通过继承,子类可以复用父类的属性和方法。
特点:
class Car {
public:
void Start() {
cout << "Car starts." << endl;
}
};
class BMW : public Car {
public:
void Drive() {
cout << "BMW drives fast." << endl;
}
};
在上面的代码中,BMW
类继承了Car
类,所以BMW
类可以直接使用Car
类中的Start
方法。
组合是一种has-a关系,表示一个类拥有另一个类的实例。这种方式通过将一个对象作为另一个对象的成员变量来实现代码复用。
组合的特点:
class Engine { public: void Start() { cout << "Engine starts." << endl; } }; class Car { private: Engine engine; // Car has an Engine public: void Start() { engine.Start(); cout << "Car starts." << endl; } };
在上面的代码中,Car
类包含了一个Engine
类的实例,Car
类通过组合来复用Engine
类的功能。
class Tire {
protected:
string _brand = "Michelin"; // 品牌
size_t _size = 17; // 尺寸
};
class Car {
protected:
string _colour = "白色"; // 颜色
string _num = "陕ABIT00"; // 车牌号
Tire _t1, _t2, _t3, _t4; // 轮胎组合
};
在这里,Car
类通过组合了四个Tire
类的实例来实现车轮的功能,这就是一个典型的has-a关系。
class Car {
protected:
string _colour = "白色"; // 颜色
string _num = "陕ABIT00"; // 车牌号
};
class BMW : public Car {
public:
void Drive() { cout << "好开-操控" << endl; }
};
class Benz : public Car {
public:
void Drive() { cout << "好坐-舒适" << endl; }
};
在一些场景中,组合和继承可能会混合使用,例如在一个stack
类中,既可以使用组合来包含一个vector
对象,也可以通过继承来扩展vector
类的功能。
**继承方式: **
template<class T>
class stack : public vector<T> {
// stack继承自vector
};
**组合方式: **
template<class T>
class stack {
public:
vector<T> _v; // 通过组合方式来包含一个vector对象
};
在实际设计时,建议优先考虑组合,这样可以保持类的封装性和独立性,从而提高代码的可维护性。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。