C++ 多继承的二义性问题

article/2025/10/14 19:18:28

多继承中的二义性问题

在一个表达式中,对函数或变量的引用必须是明确的,无二义性的。对于一个独立的类而言,其成员的标识是唯一的,对其访问不会有二义性问题。但是当类之间具有继承关系时,子类成员可能与父类成员重名;在多继承的情况下,多个父类之间也可能重名。一个引用了基类成员的表达式可能无法确认引用的是哪个基类成员,这时我们就说这个表达式具有二义性。

作用域分辨操作符与支配规则

/*作用域分辨操作符示例*/
#include <iostream>
using namespace std;class A{public:void f(){cout<<this<<"A类的f()调用完毕"<<endl;}
};class B{public:void f(){cout<<this<<"B类的f()调用完毕"<<endl;}void g(){cout<<this<<"B类的g()调用完毕"<<endl;}
};
class C:public A,public B{public:void g(){cout<<this<<"C类的g()调用完毕"<<endl;}void h(){cout<<this<<"C类的h()调用完毕"<<endl;}
};int main(void)
{C c;//c.f(); 产生二义性__A,B类中均存在f()c.A::f();c.g();c.B::g();return 0;
}

分析:

    1. c.f() 该表达式具有二义性,因为类C中有两个f();分别是从A和B继承来的,他们的作用域不存在包含关系,因此编译器无法决定调用哪个。表达式c.A::f()则通过成员名限定消除了二义性。
    1. C中有两个g(),一个继承自B,一个是自己新增的,他们的作用域存在包含关系,自己新增的g()处于内层,会遮挡外层的同名成员(也叫支配规则,子类中的名字支配父类中的名字),因此表达式c.g()没有二义性。如果确实需要调用被遮挡的外层成员,依然可以通过成员名限定来进行解决,例如c.B::g()。
    1. 作用域分辨符的一般形式是: 类名::类成员标识符

虚继承与虚基类

子类继承父类便有了父类的特性,如果再继承一遍,显然有些 “荒谬”
在C++中:

class B:public A,public B

这样的定于是无法通过的,也就是说子类不可能直接多次继承同一个父类。但是重复继承还是有可能间接发生的,如下图所示:
虚继承
因此,也就是说,在多继承时,当派生类的多个直接基类又是从另一个共同的基类派生而来时,这些直接基类都拥有上层共同基类的成员,并将导致下层的派生类成员发生重复。
间接二义性举例:

#include <iostream>
#include <string>using namespace std;class Person{private:string name;public:Person(){name = "空白";cout<<this<<"Person类的缺省样式的构造函数调用完毕"<<endl;}Person(string Name):name(Name){cout<<this<<" Person类的带参构造函数调用完毕"<<endl;}~Person(){cout<<this<<" Person类的析构函数调用完毕"<<endl;}string getName(){return name;}
};class Doctor:public Person{
private:
string title;
public:
Doctor(){title = "医师";cout<<this<<" Doctor类的缺省样式构造函数调用完毕"<<endl;
}
Doctor(string Name,string Title):Person(Name),title(Title){cout<<this<<" Doctor类的带参构造函数调用完毕"<<endl;
}
~Doctor(){cout<<this<<" Doctor类的析构函数调用完毕"<<endl;
}
string getTitle(){return title;
}
};class Armyman:public Person{private:string militaryRank;public:Armyman(){militaryRank = "上尉";cout<<this<<"Armyman类的缺省样式构造函数调用完毕"<<endl;}Armyman(string Name,string MilitaryRank):Person(Name),militaryRank(MilitaryRank){cout<<this<<"Armyman类的带参构造函数调用完毕"<<endl;}~Armyman(){cout<<this<<"Armyman类的析构函数调用完毕"<<endl;}string getMilitaryRank(){return militaryRank;}
};
class ArmySurgeon:public Doctor,public Armyman{private:public:ArmySurgeon(){cout<<this<<"ArmySurgeon类的缺省样式构造函数调用完毕"<<endl;}ArmySurgeon(string Name1,string Title,string Name2,string MilitaryRank):Doctor(Name1,Title),Armyman(Name2,MilitaryRank){cout<<this<<"ArmySurgeon类的带参构造函数调用完毕"<<endl;}~ArmySurgeon(){cout<<this<<"ArmySurgeon类的析构函数调用完毕"<<endl;}void show(){cout<<Doctor::getName()<<","<<getTitle()<<","<<Armyman::getName()<<","<<getMilitaryRank()<<endl;}
};int main(void)
{cout<<"------------开始------------"<<endl;ArmySurgeon as("张三","主治医师","李四","上校");cout<<"as:";as.show();cout<<"----------准备结束---------"<<endl;return 0;    
}

运行结果如下:
间接二义性
在ArmySugeon这个类中,共同基类成员的重复导致我们无法直接使用getName()这个名字,则只能通过作用域分辨符来消除二义性。对于数据成员也一样,从不同途径继承来的Doctor::name和Armyman::name不仅浪费空间,还有可能导致数据不一致(同一名军医有张三和李四两个名字)。由于ArmySugeon类中并没有给name赋予新的含义,Doctor::name和Armyman::name的含义应该都是人的名字,如果能只存储一份,其标识和一致性问题都能够得到解决,为此,C++引入了虚继承和虚基类的概念。
虚基类的概念是伴随虚继承的定义过程产生的,虚继承的定义格式如下:

class 派生类名:virtual 继承方式 基类名

其中:

(1) virtue是关键字,声明继承方式为虚继承,其作用范围和继承方式关键字相同,只对紧跟其后的基类起作用。为了便于表述,我们将该基类称为派生类的虚基类。
(2) 声明了虚基类之后,(2a)编译器确保在后继的进一步派生过程中只保存一份虚基类的成员,(2b)但需要虚基类与这些间接派生类共同维护这份虚基类成员。
(3) 同样为了便于表述,当后续的间接派生类要创建对象时,我们称之为最远派生类。注意这是一个相对的概念,哪个子类要创建对象,他就是最远派生类。

将上述代码稍作修改:在继承方式中加virtue关键字

class Doctor:virtual public Person
class Armyman:virtual public Person

去掉getName前的作用域分辨符:

void show(){cout<<getName()<<","<<getTitle()<<","<<getName()<<","<<getMilitaryRank()<<endl;}

运行结果如下:
virtual

从运行结果来看,声明了虚继承之后,在最远派生类(ArmySurgeon)中没有出现二义性和name的不一致,的确实现了(2a),这是virtual的第一层含义。但name并没有按照希望的值初始化,而是通过Person的缺省样式构造函数初始化为“空白”了,这说明带参构造函数调用链在通往虚基类的地方(也就是写virtual处)断裂了。这恰恰说明了virtual的另一层含义。

虚基类的成员的构造和析构

由前面的分析可知,虚基类的所有间接派生类在创建对象时(此时该类就是最远派生类),构造函数调用链在虚基类之前断裂,所以需要在最远派生类的构造函数中"越级"调用虚基类的构造函数。具体分为三种情况:

  • 若虚基类没有构造函数,则系统自动补上虚基类的缺省构造函数。
  • 若虚基类定义了缺省样式的构造函数,系统也自动补上虚基类的缺省样式的构造函数。
  • 若虚基类定义了带参构造函数,虚基类所有间接派生类都要在其构造函数的初始化列表中显式列出虚基类的构造函数。这就是上面(2b)处所说的"共同维护"。
    我们通过下面的例子来体会一下:
/*虚继承时的构造函数*/
#include <iostream>
#include <string>using namespace std;class Person{
private:string name;
public:Person(){name = "空白";cout<<this<<"Person类的缺省样式的构造函数调用完毕!"<<endl; }Person(string Name):name(Name){cout<<this<<"Person类的带参构造函数调用完毕"<<endl;}~Person(){cout<<this<<"Person类的析构函数调用完毕"<<endl;}string getName(){return name;}
};
class Doctor:virtual public Person{
private:string title;
public:Doctor(){title = "医师";cout<<this<<"Doctor类的缺省样式的构造函数调用完毕!"<<endl;}Doctor(string Name,string Title):Person(Name),title(Title){cout<<this<<"带参构造函数调用完毕!"<<endl;}~Doctor(){cout<<this<<"Doctor类的析构函数调用完毕"<<endl;}string getTitle(){return title;}
};
class Armyman:virtual public Person{
private:string militaryRank;
public:
Armyman(){militaryRank = "上尉";cout<<this<<"Armyman类的缺省样式的构造函数调用完毕!"<<endl;
}
Armyman(string Name,string MilitaryRank):Person(Name),militaryRank(MilitaryRank){cout<<this<<"Armyman类的带参样式构造函数调用完毕!"<<endl;
}
~Armyman(){cout<<this<<"Armyman类的析构函数调用完毕!"<<endl;
}
string getMilitaryRank(){return militaryRank;
}
};
class ArmySurgeon:public Doctor,public Armyman{
public:ArmySurgeon(){cout<<this<<"ArmySurgeon类的缺省样式的构造函数调用完毕"<<endl;}ArmySurgeon(string Name,string Title,string MilitaryRank):Doctor(Name,Title),Armyman(Name,MilitaryRank),Person(Name){cout<<"ArmySurgeon类的带参构造函数调用完毕!"<<endl;}~ArmySurgeon(){cout<<this<<"ArmySurgeon类的析构函数调用完毕!"<<endl;}void show(){cout<<getName()<<getTitle()<<getMilitaryRank()<<endl;}
};
int main(void)
{cout<<"--------------开始--------------"<<endl;ArmySurgeon as("张三","主治医生","上校");cout<<"as:"<<endl;as.show();cout<<"--------------结束--------------"<<endl;return 0;
}

运行结果如下:
虚基类的构造
通过在ArmySurgeon构造函数的初始化列表中加上Person(Name),就将断裂的构造函数链接上了。在此例中,虚基类只有一层间接派生类。当存在多层间接派生类时,每一层都进行这样的修补,会不会导致虚基类被多次初始化呢?我们通过下一个例子来进行说明!

/*虚基类的构造函数示例2*/
#include <iostream>using namespace std;class A{private:int a;public:A(int a){this->a = a;cout<<this<<"A类的带参构造函数调用完毕"<<endl;}~A(){cout<<this<<"A类的析构函数调用完毕!"<<endl;}int getA(){return a;}
};
class B:public A{
private:int b;
public:B(int a,int b):A(a){this->b = b;cout<<this<<"B类的带参构造函数调用完毕!"<<endl;}~B(){cout<<this<<"B类的析构函数调用完毕"<<endl;}int getB(){return b;}
};
class C{
private:int c;
public:C(int c){this->c = c;cout<<this<<"C类的构造函数调用完毕"<<endl;}~C(){cout<<this<<"C类的析构函数调用完毕"<<endl;}int getC(){return c;}
};
class D:virtual public B{
private:int d;
public:D(int a,int b,int d):B(a,b){this->d = d;cout<<this<<"D类的带参构造函数调用完毕"<<endl;}~D(){cout<<this<<"D类的析构函数调用完毕"<<endl;}int getD(){return d;}
};
class E:virtual public B{
private:int e;
public:E(int a,int b,int e):B(a,b){this->e = e;cout<<this<<"E类的带参构造函数调用完毕"<<endl;}~E(){cout<<this<<"E类的析构函数调用完毕"<<endl;}int getE(){return e;}
};
class F:public D{
private:int f;
public:F(int a,int b,int d,int f):D(a,b,d),B(a,b){this->f = f;cout<<this<<"F类的带参构造函数调用完毕"<<endl;}~F(){cout<<this<<"F类的析构函数调用完毕"<<endl;}int getF(){return f;}
};
class G:public C,public F,public E{
private:int g;
public:G(int a,int b,int c,int d,int e,int f,int g):C(c),F(a,b,d,f),E(a,b,e),B(a,b){this->g = g;cout<<this<<"G类的带参构造函数调用完毕"<<endl;}~G(){cout<<this<<"G类的析构函数调用完毕"<<endl;}int getG(){return g;}
};
void f1(){cout<<"------------f1准备创建------------"<<endl;F f1(1,2,4,6);cout<<"f1:"<<f1.getA()<<','<<f1.getB()<<','<<f1.getD()<<','<<f1.getF()<<endl;cout<<"------------f1准备销毁------------"<<endl;
}
void g1(){cout<<"------------g1准备创建------------"<<endl;G g1(1,2,3,4,5,6,7);cout<<"g1:"<<g1.getA()<<','<<g1.getB()<<','<<g1.getC()<<','<<g1.getD()<<','<<g1.getE()<<','<<g1.getF()<<','<<g1.getG()<<','<<endl;cout<<"------------g1准备销毁------------"<<endl;
}
int main(void){f1();g1();return 0;
}

运行结果如下:
虚基类的构造2

NOTICE:
C++规定:
*只有最远派生类(也就是当前要创建对象的类)的构造函数才真正调用虚基类的构造函数,而该派生类上层的其他非虚基类的构造函数中所列出的对虚基类构造函数的调用将被忽略,从而保证了对虚基类成员只进行一次初始化。

  • 当初始化列表中同时存在对虚基类和非虚基类构造函数的调用时,虚基类的构造函数优先执行。
  • 虚继承时,最远派生类对象的析构顺序与构造顺序正好相反,先析构最远派生类自身,最后析构虚基类及其上层基类。并且虚基类也只虚构一次。

http://chatgpt.dhexx.cn/article/9e94D5hX.shtml

相关文章

C++多继承中的二义性问题

在C中&#xff0c;派生类继承基类&#xff0c;对基类成员的访问应该是确定的、唯一的&#xff0c;但是常常会有以下情况导致访问不一致&#xff0c;产生二义性。 1.在继承时&#xff0c;基类之间、或基类与派生类之间发生成员同名时&#xff0c;将出现对成员访问的不确定性——…

二义性文法和无二义性文法

二义性文法是指一种产生式规则可以被解释成两种或更多种不同的语法结构的文法。这种文法会导致语言的歧义和不确定性&#xff0c;使得相同的语句可以有不同的解释。 以下是三个例子&#xff1a; S → aSb | ε 这个文法可以生成字符串"aaabbb"&#xff0c;但是它有两…

如何消除文法的二义性

文法举例 显然&#xff0c;对于not p and q有两种推导方式 默认not优先级高于and,即(not p) and q 默认and优先级高于 not,即not(p and q) 先and再not先not再and 两种消除二义性的方法 简单来说&#xff0c;就是人为规定not\and\or的优先级即可 重写的文法相当于默…

C++多继承中二义性的解决方案

出现二义性的原因&#xff1a; 派生类在访问基类成员函数时&#xff0c;由于基类存在同名的成员函数&#xff0c;导致无法确定访问的是哪个基类的成员函数&#xff0c;因此出现了二义性错误。 1. 什么是多重继承的二义性 class A{ public:void f(); }class B{ public:void f(…

编译原理——证明文法的二义性(1)

目录 推导和语法树推导语法树 文法二义性 在证明文法的二义性之前&#xff0c;我们需要熟悉几个基本的概念。 推导和语法树 推导 这里的推导&#xff0c;简单的来说就是指根据给出的句型&#xff08;句子&#xff09;&#xff0c;对文法进行推理变化最终得到句型&#xff08…

二义性

自然语言的二义性什么意思 面这个问题.很清楚的说明了自然语言的二义性.. 用红墨水写一个“蓝”字&#xff0c;请问&#xff0c;这个字是红字还是蓝字&#xff1f; 可能很少有人意识到&#xff0c;像红字和蓝字这样的词语都存在着二义性。可能是红色的字&#xff0c;也可能是“…

2.5.2 文法的二义性

2.5.2 文法的二义性 设有文法 G [ E ]: E → E E | E * E | ( E ) | i句子 i * i i 有两个不同的最左推导,对应两棵不同的语法树,见图 2.6 和图 2.7 。 最左推导 1 E ⇒ E E ⇒ E * E E ⇒ i * E E⇒ i * i E⇒ i * i i 最左推导 2 E ⇒ E * E ⇒ i * E⇒ i * E E⇒…

编译原理(三)语法分析:3.二义性与二义性的消除

文章目录 一、二义性1.定义2.原因 二、二义性的消除1.改写二义文法为非二义文法&#xff08;1&#xff09;步骤&#xff08;2&#xff09;例子&#xff08;3&#xff09;缺点 2.为文法符号规定优先级和结合性3.修改语言的语法&#xff08;表现形式被改变&#xff09; 【编译原理…

二义性和C++消除二义性

1.二义性 二义性的定义是&#xff1a;“如果文法G中的某个句子存在不只一棵语法树&#xff0c;则称该句子是二义性的。如果文法含有二义性的句子&#xff0c;则称该文法是二义性的。”&#xff08;该定义来自于百度百科&#xff09;用通俗的话讲&#xff0c;如果一句话或者一个…

C++ 二义性是什么?怎么解决?

一、什么是二义性 在多继承的场景里&#xff0c;当父类中存在同名变量时&#xff0c;子类访问父类的同名变量&#xff0c;将出现二义性&#xff0c;因为编译器不知道你将要访问的是哪个父类中的变量。 举个例子&#xff1a; class A { public:int a; // B1&#xff0c;B2 都…

ES6一维数组去重,合并去重方法分享

ES6去重的几个小方法 1.使用set方法 2.setArray.from() 3.filtermap 4、Array.fromsetflatsort 排序去重 flat:对数组进行扁平化处理 sort:排序 b-a:倒序 a-b:正序 5.数组合并去重

[记录]es6常用去重方法(数组、字符串)

数组去重 ES6 ES6以下方法除了代码简洁外,对于undefined和NaN也同样可以达到去重的效果 new Set()是ES6新增的数据结构,类似于数组,但它的一大特性就是所有元素都是唯一的,没有重复的值,我们一般称为集合,Set本身是一个构造函数,用来生成 Set 数据结构。 Set搭配扩展运算符 ……

JavaScript数组去重—ES6的两种方式

说明 JavaScript数组去重这个问题&#xff0c;经常出现在面试题中&#xff0c;以前也写过一篇数组去重的文章&#xff0c;&#xff08;JavaScript 数组去重的多种方法原理详解&#xff09;但感觉代码还是有点不够简单&#xff0c;今天和大家再说两种方法&#xff0c;代码可是足…

ES6 Set() 数组去重

ES6 Set()去重 Set。它类似于数组&#xff0c;但是成员的值都是唯一的 通过add()方法向 Set 结构加入成员 let arr [1,2,3,4,1,5,2,3]; var set2 new Set(); arr.forEach(item>{set2.add(item) }) console.log(set2); console.log(Array.from(set2));let str [测试1,测试…

ES6数组去重的三个简单办法

ES6数组去重的三个简单办法 简单说一下利用ES6实现数组去重的三个办法。 第一种: 利用Map对象和数组的filter方法 贴上相关代码 打印后的结果 通过打印我们发现&#xff0c;确实实现了我们想要的效果。那么下面简单来解释一下。 1.Map对象是ES6提供的一个新的数据结构&…

一文弄懂Python中的*args 和 **kwargs

1. 引言 在本文中&#xff0c;我们将讨论 Python 中的 *args 和 **kwargs 及其用法和示例。 闲话少说&#xff0c;我们直接开始吧。 2. 问题引入 在Python中写函数的时候&#xff0c;我们经常需要给函数传值&#xff0c;这些值被称为函数参数。 我们不妨来举个栗子&#xff…

**kwargs python_python中**kwargs怎么用?

1、使用两个星号是收集关键字参数&#xff0c;可以将参数收集到一个字典中&#xff0c;参数的名字是字典的 “键”&#xff0c;对应的参数的值是字典的 “值”。请看下面的例子&#xff1a;>>> def print_kwargs(**kwargs): ... print(kwargs) ... >>> pr…

Python技巧:​args 和 kwargs 原来这么强大

大家好&#xff0c;今天我给大家分享Python技巧&#xff1a;​args 和 kwargs的相关技巧。喜欢记得收藏、关注、点赞。 注&#xff1a;完整代码、资料、技术交流文末获取 现在args和 kwargs参数仍然是 Python 中非常有用的特性&#xff0c;而且理解它们的威力将使您成为更有效…

*args和**kwargs是什么意思

去面试的时候&#xff0c;做了一道笔试题——什么是*args和**kwargs&#xff0c;区别在哪里&#xff1f; 有点蒙&#xff0c;好像见过&#xff0c;但是不知道具体的意思。所以回来查了一下资料&#xff0c;做一下笔记。 总的来说&#xff0c;*args代表任何多个无名参数&#x…

埃及分数c语言设计,埃及分数(四)

最优分解 源程序 下面就是求埃及分数最优分解的 C 语言源程序 EgyptianFraction.c&#xff1a; 1: #include 2: #include 3: #include 4: 5: const int SIZE 64; 6: static int wp -1, bp -1; 7: static mpz_t z, w, work[SIZE], best[SIZE]; 8: static mpq_t q1, q2; 9:…