常对象,常成员
派生类与继承
多重继承
虚基类
基类与派生类对象之间的赋值兼容关系
Reference:
常对象,常成员
以下面这张表概括性的描述以下可访问性。
数据成员
普通成员函数
常成员函数
普通数据成员
可以访问,也可以改变值
可以访问,但不可以改变值
常数据成员
可以访问,但不可以改变值
可以访问,但不可以改变值
常对象的数据成员
不允许访问和修改值
可以访问,但不可以改变值
常数据成员
以 const
声明的数据成员被称为
常数据成员 ,其只能通过构造函数的成员初始化列表对该数据成员进行初始化,在此之后无法再做修改。
以如下代码为例。
常数据成员 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include <iostream> using namespace std;class A { public : A (int v) : v (v) {}; void print () { cout << v << endl; } private : const int v; }; int main () { A a (1 ) ; a.print (); return 0 ; }
常成员函数
可以参考 类(C++)- Part 1 —— const 成员函数
在普通成员函数的声明时,在左花括号和变量列表的右括号之间加入
const
关键字
以如下代码为例。
常成员函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include <iostream> using namespace std;class A { public : A (int v) : v (v) {}; void print () const ; private : const int v; }; void A::print () const { cout << v << endl; }int main () { A a (1 ) ; a.print (); return 0 ; }
常成员函数实质是使 this
指针变成
常量指针 ,因此无法再对对象的数据成员进行修改。
注意 const
是函数类型的一个组成部分,因此在声明和定义时均要注明。
常成员函数只能调用常成员函数,而不能调用普通成员函数,这是为了保证常成员函数不会进行任何修改。
常对象
在说明对象时使用 const
修饰,则被说明对象为常对象,在初始化后无法修改。 以如下代码为例。
常对象 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include <iostream> using namespace std;class A { public : A (int v) : v (v) {}; void print () const ; void test () { cout << v << endl; } private : const int v; }; void A::print () const { cout << v << endl; }int main () { const A a (1 ) ; a.print (); return 0 ; }
常对象的定义时,既可以将 const
放在类名前面,又可以放在后面,如
A const a(1);
,也是合法的语句。
需要注意的是,常对象只能调用常成员函数,换句话说,常成员函数是常对象的唯一对外接口。
派生类
派生类有三种继承方式,分别为
public
、private
和
protected
,分别表示公有继承、私有继承和保护继承。
以如下代码为例。
派生类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #include <iostream> using namespace std;class A { public : A (int x):v (x){} void printA () { cout << "A: " << v << endl; } protected : int v; }; class B : public A{ public : B (int x,int y):A (x),w (y){} void printB () { cout << "B: " << w << endl; } protected : int w; }; int main () { B b (1 ,2 ) ; b.printA (); b.printB (); return 0 ; }
其中,B 以 A 为基类,以 public
方式继承。
需要注意的是如果不显式给出继承方式,那么默认以 private
继承。
派生类对基类成员的访问规则
私有继承
访问方式
私有成员
公有成员
保护成员
内部访问
不可访问
可访问
可访问
外部访问
不可访问
不可访问
不可访问
公有继承
访问方式
私有成员
公有成员
保护成员
内部访问
不可访问
可访问
可访问
外部访问
不可访问
可访问
不可访问
保护继承
访问方式
私有成员
公有成员
保护成员
内部访问
不可访问
可访问
可访问
外部访问
不可访问
不可访问
不可访问
基类成员在派生类中的访问属性
基类成员
在公有派生类中的访问属性
在私有派生类中的访问属性
在保护派生类中的访问属性
私有成员
不可直接访问
不可直接访问
不可直接访问
公有成员
公有
私有
保护
保护成员
保护
私有
保护
派生类的构造函数与析构函数
通常情况下创建派生类对象时,先执行基类的构造函数,再执行派生类的构造函数;而撤销派生对象时,则先执行派生类的析构函数再执行基类的析构函数。
以下面的程序为例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include <iostream> using namespace std;class a {public : a () { cout << "Constructor a executed" << endl; } ~a () { cout << "Destructor a executed" << endl; } }; class b : public a {public : b () { cout << "Constructor b executed" << endl; } ~b () { cout << "Destructor b executed" << endl; } }; int main () { b bb; return 0 ; }
执行结果如下
1 2 3 4 Constructor a executed Constructor b executed Destructor b executed Destructor a executed
当基类的构造函数没有参数或没有显式定义构造函数时,派生类可不向基类传递参数,甚至不定义构造函数。
调用基类的构造函数需要在构造函数的初始化列表中,当派生类含有对象成员时,也应当在初始化列表中进行初始化(调用其构造函数)。
调整基类成员在派生类中的访问属性的其他方法
同名成员
定义派生类时,允许派生类中声明的成员与基类中的成员名字相同,如果派生类中定义了与基类成员同名的成员,则派生类成员覆盖了基类的同名成员,在派生类再次使用这个标识符则会访问派生类中重新声明的成员。如要在派生类中使用基类的同名成员,必须在改成员名前加上你基类名与作用域标识符::
。
以下面的代码说明
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include <iostream> using namespace std;class a {public : int x = 10 ; }; class b : public a {public : int x = 11 ; }; int main () { b bb; cout << bb.x << " " << bb.a::x << endl; return 0 ; }
执行以上代码将得到如下输出
注意,在派生类内访问基类成员使用 基类名::成员名
而在类外,对象访问则是 对象.基类名::成员名
访问声明
把基类中的公有成员或保护成员写到私有派生类定义中相应的字段中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include <iostream> using namespace std;class a {protected : int x = 10 ; }; class b : public a {public : using a::x; int y = 11 ; }; int main () { b bb; cout << bb.x << " " << bb.y << endl; return 0 ; }
多重继承
声明两个及以上基类的派生类
模板
1 2 3 class 派生类名: 继承方式1 基类1 ,...,继承方式n 基类名n { ... ... }
多重继承的构造与析构函数
构造函数与析构函数,与单一继承类似,当有参数时需要在初始化列表内初始化每一个基类。
需要注意的是,对于基类成员的访问必须是无二义的,具体来说是对象访问时,public
的基类成员以 public
方式继承时,将允许对象访问,如果有不同的基类定义了两个相同名称的
public
成员就会出现二义性,应当消除这种二义性。
虚基类
为什么要引入虚基类
如果一个类有多个直接基类,而这些直接基类又有一个或多个共同的基类,则在最底层的派生类中会保留这个间接的共同基类数据成员的多份同名成员。访问这些成员时,为了消除二义性,需要在派生类对象名后增加直接基类名。
以下面这个例子为例
共同基类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 #include <bits/stdc++.h> using namespace std;class A { public : A () { v = 5 ; cout << "A - v: " << v << endl; } protected : int v; }; class B : public A { public : B () { v += 10 ; cout << "B - v: " << v << endl; } }; class C : public A { public : C () { v += 20 ; cout << "C - v: " << v << endl; } }; class Derived : public B, public C { public : Derived () { cout << "Base B - v: " << B::v << endl; cout << "Base C - v: " << C::v << endl; } }; int main () { Derived o; return 0 ; }
运行该程序,会得到如下的输出
output 1 2 3 4 5 6 A - v: 5 B - v: 15 A - v: 5 C - v: 25 Base B - v: 15 Base C - v: 25
注意,如果在 Derived 内直接使用 v
,则会显示
"Derived::v" is ambiguousC/C++(266)
,表示产生了二义性。
虚基类正是为了解决此类问题而引入的。
虚基类的概念
不难理解,由于上面的例子中,底层基类 A
被继承了两次,因此产生了同名成员的二义问题,若 A
只有一个复制(单例),那么就不会产生这种冲突。
那么,要使 A
只产生一个复制,可以使用 virtual
修饰原有的继承声明。
虚基类继承语法 1 2 3 class 派生类名: virtual 继承方式 基类名{ ... }
下面使用前文中的例子,应用虚基类
虚基类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 #include <bits/stdc++.h> using namespace std;class A { public : A () { v = 5 ; cout << "A - v: " << v << endl; } protected : int v; }; class B : virtual public A { public : B () { v += 10 ; cout << "B - v: " << v << endl; } }; class C : virtual public A { public : C () { v += 20 ; cout << "C - v: " << v << endl; } }; class Derived : public B, public C { public : Derived () { cout << "Base B - v: " << B::v << endl; cout << "Base C - v: " << C::v << endl; cout << "v: " << v << endl; } }; int main () { Derived o; return 0 ; }
运行这段代码可得到如下的输出
output 1 2 3 4 5 6 A - v: 5 B - v: 15 C - v: 35 Base B - v: 35 Base C - v: 35 v: 35
可以看到,这里的 v
都是同一个了。
虚基类的初始化
以这个例子说明
虚基类的初始化 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #include <bits/stdc++.h> using namespace std;class A { public : A (int x) : v (x) { cout << "A - v: " << v << endl; } protected : int v; }; class B : virtual public A { public : B (int x) : A (x) { cout << "B - v: " << v << endl; } }; class C : virtual public A { public : C (int x) : A (x) { cout << "C - v: " << v << endl; } }; class Derived : public B, public C { public : Derived () : A (4 ), B (5 ), C (6 ) { cout << "Base B - v: " << B::v << endl; cout << "Base C - v: " << C::v << endl; cout << "v: " << v << endl; } }; int main () { Derived o; return 0 ; }
输出为
output 1 2 3 4 5 6 A - v: 4 B - v: 4 C - v: 4 Base B - v: 4 Base C - v: 4 v: 4
具体的执行规则:
虚基类由其最远 派生类的构造函数的调用进行初始化,其他派生列的其他基类对虚基类构造函数的调用将被忽略。
若同一层次同时包含虚基类和非虚基类,先调用虚基类的构造函数,再调用非虚基类的构造函数,最后调用派生类的构造函数。
基类与派生类对象之间的赋值兼容关系
在一定条件下,不同类型的数据之间可以进行类型转换,例如从
int
到 double
的类型自动转换和赋值,称为赋值兼容。在基类与派生类之间也有赋值兼容关系。
在基类的对象可以使用的任何地方,都可以用派生类的对象来代替,但只能使用从基类继承来的成员。
以下面的代码为例
赋值兼容 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include <bits/stdc++.h> using namespace std;class A { public : int a; A (int x) : a (x) {} }; class B : public A { public : int b; B (int x, int y) : A (x), b (y) {} }; int main () { A aa (10 ) ; B bb (20 , 30 ) ; cout << aa.a << endl; cout << bb.a << " " << bb.b << endl; aa = bb; cout << aa.a << endl; return 0 ; }
执行以上代码得到如下输出
output