C++面向对象整理(5)之继承(基类与派生类)
注:整理一些突然学到的C++知识,随时mark一下
例如:忘记的关键字用法,新关键字,新数据结构
C++ 的 类的继承
提示:本文为 C++ 中 类定义、成员函数 的写法和举例
一、继承的定义和三种方式
继承( inheritance )是面向对象编程的一个重要特性,它允许一个类(派生类或子类)继承另一个类(基类或父类)的属性和方法。C++提供了三种主要的继承方式:公有继承(public )、私有继承(private)和保护继承(protected )。
子类可以直接继承拿得父类中除了private权限的其他成员拿来用且都能访问,但其权限根据其继承方式可能会发生变化,子类的成员还可以新添自己特有的其他成员,还可以重定义同名的成员。
继承的语法使用冒号,格式是class 子类B :继承方式 基类A {}
1、公有继承(Public Inheritance)
当使用公有继承: public
时,基类的公有(public)和保护(protected)成员在派生类中保持其访问权限不变,即公有成员在派生类中仍然是公有的,保护成员在派生类中仍然是保护的。基类的私有(private)成员在派生类中是不可访问的。
cpp
class Base {
public:
void pub_func() { /* ... */ }
protected:
void prot_func() { /* ... */ }
private:
void priv_func() { /* ... */ }
};
class Derived : public Base {
public:
void use_base_funcs() {
pub_func(); // 可以调用,因为pub_func是公有的
prot_func(); // 也可以调用,因为prot_func是受保护的
// priv_func(); // 错误:priv_func是私有的,不可在派生类中直接访问
};
2、私有继承(Private)
在私有继承中,基类的公有和保护成员在派生类中都会变为私有成员。派生类的成员函数可以间接访问它们。基类的私有成员在派生类中仍然不可访问(其实有继承,但编译器给予隐藏)。
class Derived : private Base {
public:
void use_base_funcs() {
pub_func(); // 可以调用,但pub_func在子类Derived中变成了私有
prot_func(); // 可以调用,但prot_func在子类Derived中变成了私有
// priv_func(); // 错误:priv_func是私有的,不可访问
};
};
3、保护继承(Protected Inheritance)
保护继承与私有继承类似,但区别在于基类的公有和保护成员在派生类中变为保护成员。这意味着这些成员在派生类内部是可访问的,但在派生类的对象外部是不可访问的。基类的私有成员在派生类中仍然不可访问。
class Derived : protected Base {
public:
void use_base_funcs() {
pub_func(); // 可以调用,但pub_func在Derived中变成了保护
prot_func(); // 可以调用,prot_func在Derived中仍然是保护
// priv_func(); // 错误:priv_func是私有的,不可访问
};
};
在选择继承方式时,需要考虑如何最好地封装和保护基类的数据,以及派生类如何与基类和其他派生类交互。公有继承通常用于“是一个从属于”的关系(例如,猫是动物),而私有和保护继承更多地用于实现细节和内部结构的复用。注意,无论选择哪种继承方式,派生类都可以再额外添加自己的新成员,并且可以重定义同名的成员函数,还能重写基类中的虚函数(如果它们被声明为virtual的话)。
二、继承中的构造函数调用
1、子类的构造在最后,析构在最先
结论:先基类、再成员变量(如果要到第三方类)、再自己子类的构造顺序。下面看一个例子:
假设有类B
继承自类A
,并且类B
有一个第三方类对象成员C c
时,构造函数的调用会稍微复杂一些。下面是一个简单的例子来说明这个过程:
class A {
public:
A() {
std::cout << "A's constructor called" << std::endl;
}
};
class C {
public:
C() {
std::cout << "C's constructor called" << std::endl;
}
};
class B : public A {
private:
C c;
public:
B() : c() {
std::cout << "B's constructor called" << std::endl;
}
};
int main() {
B b;
return 0;
}
在这个例子中,当创建B类的对象时,构造函数的调用顺序如下:
第一,基类A的构造函数:首先调用基类A的构造函数。这是因为B继承自A,所以A的构造函数会在B的构造函数之前被调用。
第二,成员变量C的构造函数:接下来调用类B的成员变量c即类C的构造函数。成员变量的构造函数在B的构造函数体执行之前被调用。
第三,类B的构造函数:最后,调用类B的构造函数。在这个例子中,B的构造函数使用了初始化列表来显式地调用成员变量C的构造函数。
输出将会是:
//输出结果:
A's constructor called
C's constructor called
B's constructor called
这个顺序是C++中对象构造的一个基本规则:首先构造基类,然后构造成员变量,最后构造派生类本身。在析构时,顺序相反:首先析构派生类,然后析构成员变量,最后析构基类。
三、继承中子类重定义同名成员
如果子类定义了一个与父类同名的成员(无论是数据成员还是成员函数),那么在子类中,这个成员会覆盖父类的同名成员。这意味着,如果你试图在子类的对象上访问这个成员,那么默认情况下你会得到子类中的成员,而不是父类中的。
但也可以访问父类的,C++提供了作用域解析运算符::
,它允许你明确指出你想访问的是哪个作用域中的成员。假设有一个父类BASE
和一个子类S1
,并且它们都有一个名为same1_
的成员,你可以使用作用域解析运算符来访问父类中的same1_
成员。
下面是一个简单的例子来演示这个概念:
class BASE {
public:
int same1_; // 父类中的成员
BASE(int val) : same1_(val) {}
};
class S1 : public BASE {
public:
double same1_; // 子类中的同名成员,类型不同
S1(int baseVal, double subVal) : BASE(baseVal), same1_(subVal) {}
void printBoth() {
std::cout << "BASE::same1_: " << BASE::same1_ << std::endl; // 访问父类的same1_
std::cout << "S1::same1_: " << same1_ << std::endl; // 访问子类的same1_ 也可以直接this->same1_ 因为默认是重定义的子类那个
}
};
int main() {
S1 s1(10, 20.5);
s1.same1_;//访问自己的
s1.BASE::same1_;//访问父类的
s1.printBoth(); // 输出两个same1_的值
return 0;
}
在这个例子中,BASE类
有一个整数类型的same1_
成员,而S1类
有一个双精度浮点类型的same1_
成员。在S1类的printBoth
成员函数中,我们使用BASE::same1_
来明确访问父类中的same1_
成员,而直接写same1_
则访问的是子类中的同名成员。
当你运行这个程序时,printBoth
函数会输出两个值:一个是父类中same1_
的值(整数10),另一个是子类中same1_
的值(双精度浮点数20.5)。通过使用作用域解析运算符,我们可以消除子类成员对父类同名成员的隐藏(或覆盖),从而能够访问父类中的成员。(成员函数一样的道理)
注意如果是静态同名成员,一样的方法,而且还可以在子类下直接通过::访问
//假设same1_是静态成员
s1.same1_;//访问自己的
s1.BASE::same1_;//访问父类的
S1::same1_;//访问自己的
S1::BASE::same1_;访问父类的
同时注意上面例子中这个子类的初始化列表构造函数的写法:S1(int baseVal, double subVal) : BASE(baseVal), same1_(subVal) {}
这里的BASE(baseVal)
是基类的构造函数调用,它可以直接列在初始化列表的冒号后完成赋值。
总结
继承的public、private、protected
三种继承都不能访问基类私有成员
继承同名成员,可以在子类重新定义,但想访问父类时可以用::
访问,静态同名成员完全一样道理
子类构造总是最后调用,子类中的第三方类成员的构造在基类后调用