前言: 当我们踏上C++编程的旅程时,继承无疑是一个无法回避且至关重要的概念。作为面向对象编程的三大特性之一,继承不仅让我们能够创建出层次清晰、结构合理的代码,还极大地提高了代码的可重用性和可维护性。在本文中,我们将一起深入探讨C++继承的奥秘,从基础概念到高级应用,逐步揭开它的神秘面纱
C++继承允许我们定义一个基类(或称为父类),并从这个基类中派生出新的类(称为派生类、子类)。派生类会继承基类的成员和成员函数,同时还可以添加自己的成员和成员函数。这种能力使得我们能够构建出复杂的类层次结构,实现代码的模块化和复用
在本文的学习中我们不仅仅要了解继承的基本概念。在实际编程中,我们还需要掌握如何正确使用继承、如何避免常见的继承陷阱、以及如何利用继承来优化我们的代码结构。因此,本文将带领大家从多个角度全面学习C++继承,包括继承的语法规则、访问控制、构造函数与析构函数的调用、多重继承与菱形继承等问题
让我们一起踏上学习C++继承的旅程吧!
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用
继承代码示例
class A
{
public:
void func()
{
cout << "A::func()" << endl;
}
protected:
int _a = 10;
};
// 继承后父类A的成员_a(成员函数+成员变量)都会变成子类的一部分
class B : public A
{
public:
// ......
protected:
int _b = 100;
};
int main()
{
A a ;
B b;
a.func();
b.func(); // b可以调用A中的成员函数
return 0;
}
我们从刚刚的代码示例可以看到A是基类(父类),B是派生类(子类)
定义格式
注意:在定义继承的时候继承方式可以省略不写,如果不写则是根据基类的定义来决定默认继承方式,但是建议定义时带上继承方式
class定义的类默认private继承,struct定义的类默认public继承
继承关系和访问限定符
继承基类成员访问方式的变化 继承方式和访问限定符都有三种,虽然它们组合一共有9中能使用的方法,但是我们最常用的只有红色框里面的两种用法
这里我们有以下几点需要注意:
关于赋值规则这里我们先提两点:
我们在讲C++入门知识的时候讲过,引用类型不同的变量时,会产生一个临时变量,临时变量具有常性,需要const修饰,但是在继承中就不需要const修饰
代码示例
int main()
{
int c = 1;
double d = 1.1;
const int& r = d; // 中间产生了一个临时变量,临时变量具有常性,需要const修饰
B b;
A a = b; // 子类可以赋值给基类
// b = a; // false, 基类不可以赋值给子类
A& ra = b; // is-a 的关系中间不会产生临时对象,父子类的赋值兼容规则(切割/切片)
return 0;
}
继承中的对象是is-a
的关系,它们中间并不会产生临时对象,这就是父子类的赋值兼容规则(切割/切片)
关于作用域的注意事项:
当继承的基类与子类有同名的成员变量时,不指定的话,会调用子类的成员变量
代码示例
class A
{
protected:
int _a = 10;
};
class B : public A
{
public:
void Print()
{
cout << "_a:" << _a << endl;
// cout << "A: _a:" << A::_a << endl; // 要想成功打印A类的元素必须要指定
cout << "_b:" << _b << endl;
}
protected:
int _a = 99;
int _b = 100;
};
int main()
{
B b;
// 成员变量同名
// A 和 B中的 _a 构成隐藏
b.Print(); // // _a = 99 , _b = 100; 就近原则
return 0;
}
在继承中,同名函数并不会构成函数重载,因为他们在不同的作用域,每个类都是独立的,成员函数满足函数名相同就构成隐藏
代码示例
class A
{
public:
void func()
{
cout << "func()" << endl;
}
protected:
int _a = 10;
};
class B : public A
{
public:
//
void func(int b)
{
cout << "func(int b)" << endl;
}
protected:
int _b = 100;
};
int main()
{
B b;
// 成员函数同名
// A 和 B中的 func() 构成隐藏
b.func(); // 打印“func(int b)”
}
默认成员函数,“默认”的意思就是指我们不写,编译器会变我们自动生成一个
相关文章:默认成员函数
综上所述:关于基类和子类的调用顺序,一般情况都是先父后子,但是析构必须先子后父,来避免析构完父类之后,子类出错
继承默认函数的实现代码示例
class A
{
public:
A()
{}
A(int a)
:_a(a)
{
cout << "A()" << endl;
}
A(const A& a)
:_a(a._a)
{
cout << "A(const A& a)" << endl;
}
A& operator=(const A& a)
{
cout << "A& operator=(const A& a)" << endl;
if (&a != this)
{
_a = a._a;
}
return *this;
}
~A()
{
cout << "~A()" << endl;
}
protected:
int _a = 10;
};
class B : public A
{
public:
B()
{}
B(int a, int b)
// :_a(a) // _a不是基类成员不能这样初始化
:A(a)
,_b(b)
{
cout << "B()" << endl;
}
B(const B& b)
// :_a(a) // _a不是基类成员不能这样初始化
:A(b)
, _b(b._b)
{
cout << "B(const A& a, const B& b)" << endl;
}
B& operator=(const B& b)
{
cout << "B& operator=(const B& b)" << endl;
if (&b != this)
{
// 需要调用A类的 operator=
A::operator=(b);
_b = b._b;
}
return *this;
}
// 析构函数会先析构父类,而有时候先析构父类,子类会出事
// 不需要显式调用父类析构
~B()
{
cout << "~B()" << endl;
}
protected:
int _b = 100;
};
int main()
{
B b1(1, 100);
B b2(b1);
B b3(2, 200);
b1 = b3;
return 0;
}
友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员,因为朋友的朋友不一定也是自己的朋友,如果基类,子类都想使用必须都在各自的域里面声明
代码示例
class A
{
public:
friend void Print(const A& a, const B& b);
protected:
int _a = 10;
};
class B : public A
{
public:
//
protected:
int _b = 100;
};
void Print(const A& a, const B& b)
{
cout << a._a << endl;
cout << b._b << endl;
}
int main()
{
A a;
B b;
Print(a, b);
}
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例
代码示例
class A
{
public:
A()
{
++_count;
}
static int _count;
protected:
int _a = 10;
};
int A::_count = 0;
class B : public A
{
public:
//
protected:
int _b = 100;
};
int main()
{
A a;
B b;
cout << A::_count << endl;
}
单继承:一个子类只有一个直接父类时称这个继承关系为单继承
class B : public A
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
class D : public B , public C
菱形继承:菱形继承是多继承的一种特殊情况。
class B : public A
{......};
class C : public A
{......};
class D : public B , public C
class A
{
protected:
int _a = 1;
};
class B :public A
{
protected:
int _b = 2;
};
class C :public A
{
protected:
int _c = 3;
};
class D :public B, public A
{
protected:
int _d = 4;
};
菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。在D的对象中_a成员会有两份,我们在访问的时候无法明确知道访问的是哪一个,必须要显示指定访问哪个父类的成员,但是数据冗余任然无法解决!
虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在A和B的继承A时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他地方去使用
代码示例
class A
{
protected:
int _a = 1;
};
class B : virtual public A
{
protected:
int _b = 2;
};
class C : virtual public A
{
protected:
int _c = 3;
};
class D : public B, public A
{
protected:
int _d = 4;
};
加上表中偏移量可以找到最底下的A
回顾学习过程,我们学会了如何定义基类与派生类,掌握了访问控制规则,理解了构造函数与析构函数在继承中的作用,还探讨了多重继承及其带来的挑战。这些知识不仅丰富了我们的编程技能,更为我们解决实际问题提供了有力的工具
在结束对C++继承的学习之旅后,我们不禁感叹其强大的功能和灵活性。通过深入探究继承的基本概念、语法规则以及高级应用,我们逐渐揭开了其背后的奥秘,并体验到了它在面向对象编程中的独特价值
学习C++继承并非一蹴而就的过程。它需要我们不断地实践、思考、总结和创新。在未来的编程之路上,我们将继续深化对继承的理解,探索其更多的应用场景和高级特性,如虚继承、接口继承等,我们也要认识到继承并非万能的。在使用继承时,我们需要权衡其带来的好处和潜在的风险,避免过度使用导致代码结构复杂、难以维护。我们应该根据具体的需求和场景,选择最合适的编程范式和工具!!!
希望本文能够为你提供有益的参考和启示,让我们一起在编程的道路上不断前行! 谢谢大家支持本篇到这里就结束了,祝大家天天开心! 我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=1m2qp8pe3h4nz