关于我:
睡觉待开机:个人主页
PDF版免费提供:倘若有需要,想拿我写的博客进行学习和交流,可以私信我将免费提供PDF版。
留下你的建议:倘若你发现本文中的内容和配图有任何错误或改进建议,请直接评论或者私信。
倡导提问与交流:关于本文任何不明之处,请及时评论和私信,看到即回复。
参考目录
- 1.前言
- 2.回顾封装的概念
- 3.继承的概念
- 4.继承的定义
- 4.1继承的定义格式
- 4.2继承权限
- 5.父类和子类对象赋值转换
- 6.继承种的作用域
- 7.子类的默认成员函数
- 7.1构成函数
- 7.2拷贝构造函数
- 7.3赋值运算符重载函数
- 7.4析构函数
- 8.继承与友元函数
- 9.继承与静态成员
- 10.菱形继承
- 10.1单继承与多继承
- 10.2多继承中的菱形继承问题及虚拟继承
- 10.3菱形继承中虚拟继承的原理
- 10.4在继承公共基类的时候使用虚继承
- 10.5腰部父类也使用虚继承模型
- 10.6菱形继承的实例
- 11.组合与继承
1.前言
我们知道,CPP有三大特性:封装、继承和多态。在本文中,我们将简单回顾一下封装的理解,由浅到深的去了解继承的相关语法和一些高级语法。
好,那话不多说,让我们开始吧。
2.回顾封装的概念
什么是封装呢?
实际上,这是一个CPP概念理解中一个挺重要的概念之一,请你简谈一下对CPP封装语法的理解。
我是这样理解的,以下回答仅供参考:
从语法的角度:就是把数据和函数方法进行合并,并用访问限定符修饰加以限定。
从上下层角度:就是把一个类放到另一个类里面,通过typedef的方式,封装成为一个全新的类型。
这个类型从上层使用来说,可能会与一些其他类型使用保持一致,但是底层差异很大,比如我们之前接触过的deque和vector,deque的底层完全不是一个连续的空间,但是通过封装的方式,使得deque得使用与vector差别不大。再比如说,我们之前使用正向迭代器封装成为反向迭代器,反向迭代器虽然在使用上与与正向迭代器一致,实际上就是给正向迭代器套了一层壳。
好的,以上便是我对封装得一点简单理解。下面我们开始谈继承得相关概念~我先说继承是一种特殊得代码复用得一种形式。
谈到代码复用,我们前面学过一些代码复用简单来回顾一下:
- 函数逻辑的代码复用
- 针对类/函数大体逻辑相似,只有类型不同的模板代码复用
- 继承,是一种类层次设计上的代码复用
那我们现在正式进入介绍继承语法这一阶段。
3.继承的概念
继承:类层次设计的一种代码复用。
如何理解继承的概念呢?举个比较形象的例子,你父亲的东西你可以拿过来用,这就是你继承了你父亲的一些东西。比如说,你长大了,是不是可以继承一些你父亲的财产?你父亲的技术?哈哈(仅仅是举个例子)。代码也是同理,我们可以定义一个父类,然后去让别的类继承,比如说上图中我们下面定义的学生类、老师类、导员类都是可以去继承人类这个类的,继承了有什么好处呢?很多重复的类代码就不用自己去写了呗,本质就是一种代码复用。比如说学生、老师、导员都可以继承人类的名字,这样就不用再在学生类、老师类、导员类中每个中都去定义一个名字变量了对不对。
如此好用的继承,那该如何定义呢?下面来简单进行介绍。
4.继承的定义
4.1继承的定义格式
先来说一下继承的定义格式:
主要是在一个一般的类声明的基础上在后面跟个冒号,然后写继承方式(public/protected/private),然后再去写明继承父类的名字就好了。
总结下来就是:class 继承子类名称 : 继承方式 继承父类名称
是不是语法设置很简单呢?但是在这里我们提到了继承方式,那啥是继承方式呢?
继承方式指的是继承子类打算以什么方式去继承这个父类的一些成员,继承方式有下面三种:public、private、protected三种继承方式。
除此之外,父类中每个成员都会访问限定符进行修饰。
继承方式和每个成员的访问限定符共同决定了子类中到底继承到的成员具有什么权限。
继承方式有三种,每个父类成员的访问限定符又有三种,所以组合起来一共有9种情况。情况比较多,待我一一道来。
4.2继承权限
前面提过,子类成员继承成员权限 = 父类成员修饰限定符 + 继承方式共同决定。
我总结了下面表格:
可能有些同学会对这个表格的一些内容感到不太理解,没关系,下面我挨个说明,挨个去举例。
class F
{
private:
void PriTest()
{
cout << "F:PriTest()" << endl;
}
protected:
void ProTest()
{
cout << "F:ProTest()" << endl;
}
public:
void PubTest()
{
cout << "F:PubTest()" << endl;
}
};
class PriS : private F
{
public:
void PriSTest_Pri()
{
//PriTest();//父类私有成员,私有继承,类内不能访问
}
void PriSTest_Pro()
{
ProTest();//父类保护成员,私有继承,类内可以访问
}
void PriSTest_Pub()
{
PubTest();//父类公共成员,私有继承,类内可以访问
}
};
void PriSTest()
{
PriS pris;//对于私有继承,所有父类成员均不可在类外访问
}
class ProS : protected F
{
public:
void ProSTest_Pri()
{
//PriTest();//父类私有成员,保护继承,类内不能访问
}
void ProSTest_Pro()
{
ProTest();//父类保护成员,保护继承,类内可以访问
}
void ProSTest_Pub()
{
PubTest();//父类公共成员,保护继承,类内可以访问
}
};
void ProSTest()
{
ProS pros;
//对于保护继承,所有父类成员均不可在类外访问
}
class PubS : public F
{
public:
void PubSTest_Pri()
{
//PriTest();//父类私有成员,保护继承,类内不能访问
}
void PubSTest_Pro()
{
ProTest();//父类保护成员,保护继承,类内可以访问
}
void PubSTest_Pub()
{
PubTest();//父类公共成员,保护继承,类内可以访问
}
};
void PubSTest()
{
PubS pubs;
pubs.PubTest();
//对于公共继承,所有父类成员种只有公共成员才可在类外访问
}
void test1()
{
PriSTest();
ProSTest();
PubSTest();
}
可能有些同学还是不太能理解,虽然上面附了一些代码…
那我直接总结了一些规律来供大家快速理解上面表格。
- 对于保护访问限定符的理解
protected是针对于CPP继承语法而诞生的。
protected所修饰的父类成员,允许在子类中使用,但是不允许在子类类外使用。 - 私有继承和私有成员的理解
私有继承:继承方式是private的继承,私有成员:被private修饰符所修饰的类成员。
私有继承对父类的public、protected修饰的成员是可见的。但是任何继承方式对于父类种private修饰的成员是不可见的。 - 继承访问限定的确定
对于不是父类私有的成员,我们可以取其继承方式和权限修饰限定符的权限较小者。比如说,继承方式是protected,对于父类中的public成员,那么继承下来的就是protected权限。 - struct和class默认继承
其实针对于struct和class继承,是可以进行默认继承的,就是写继承定义语法的时候可以省略继承方式。对于struct,默认继承方式是public继承,对于class,默认就是private继承。
这里我们不妨来做个引申:CPP中struct与class的区别是什么?
struct、class做类,默认是public公开成员的,而class是默认private成员的。
struct、class对于继承来说,struct默认继承是公开继承方式,而class默认继承是私有继承方式。
5.父类和子类对象赋值转换
CPP中支持把子类对象赋值给父类对象,有个专属的名词叫做切片
或切割
很新奇吧?为啥其这么个名字呢?
class Father
{
private:
int f_a;
protected:
int f_b;
public:
int f_c;
};
class Son : public Father
{
private:
int s_a = 1;
protected:
int s_b = 2;
public:
int s_c = 3;
};
void test2()
{
Son s;
Father f = s;//代码为 0。
}
在上图种,父类有name、sex、age三个成员变量,子类呢比父类多个_no的变量,
你想,要把一个子类对象强行放到一个父类类型里面,那是不是_no变量会被扔掉?所以十分切合我们所说的这种意思,CPP就形象的称此为“切片”/“切割”啦。
实际上,除了子类对象可以赋给父类对象之外,自然也支持把子类指针给到父类指针,把子类引用给到父类引用啦(请参见下图)。
void test2()
{
Son s;
Father f = s;//代码为 0。
Father* pf = &s;//代码为 0。
Father& qf = s;//代码为 0。
}
除此之外,我还需要介绍:子类给父类对象的时候是没有中间变量产生的。
我们都知道,隐式类型转换、强制类型转换都会在赋值中间产生一个临时对象,而子类和福哦类的复制转换是没有临时对象产生的。
这是为什么呢?编译器做了特殊处理。其中的道理我也不太懂,暂且留到以后有机会再说吧哈哈。
之后,我还要去强调另外一点:父类对象不能给到子类类型变量哈。
6.继承种的作用域
两个类构成继承,那么对于作用域而言两者也是相互独立的。
子类和父类中有同名成员不会报错,此时会构成 隐藏
。
需要主要的是成员函数的隐藏构成条件是函数名一致即可,不需要参数进行比较,两个不同类中的函数不会构成重载哈!只有在同一个作用域的函数才会有重载这一说,我们刚开始就说了两个类有着相互独立的作用域。
我个人建议大家在继承体系定义的时候尽量不要定义重名的成员,因为容易进坑。
class Father2
{
public:
int a = 1;
void func()
{
cout << " father " << endl;
}
};
class Son2 : public Father2
{
public:
int a = 2;
void func()
{
cout << " son " << endl;
}
};
void test3()
{
Son2 son2;
cout << son2.a << endl;//访问的是son中的变量
son2.func();//访问的是son中的函数
cout << son2.Father2::a << endl;//访问的是father中的变量
son2.Father2::func();//访问的是father中的函数
}
7.子类的默认成员函数
对于子类的默认成员函数认识比较复杂,首先需要对子类的默认成员函数有三个方面进行认识:一整个父类 + 子类中的内置类型 + 子类中的自定义类型
7.1构成函数
子类构造的逻辑:
如果你不写子类的构造函数,那么编译器帮你自动生成一个默认构造函数,这个默认构造函数会忽略子类中的内置类型,会去自动调用子类中的自定义类型,会去自动调用父类的默认构造函数,如果此时父类没有默认构造函数就会报错哈!
class Fa
{
public:
int _fa;
};
class So: public Fa
{
public:
int _so;
};
void test4()
{
So s;
//在父类和子类都不写构造的情况下,子类会生成默认构造
//子类默认构造里会去调用父类的默认构造
}
class Fa
{
public:
int _fa;
Fa(int f, char c)//此时Fa没有默认构造函数
{
}
};
class So : public Fa
{
public:
int _so;
};
void test4()
{
So s;//So::So(void)”: 由于 基类“Fa”不具备相应的 默认构造函数 或重载解决不明确,因此已隐式删除函数
//此时So不写默认构造,编译器会自动生成子类默认构造函数,并去调用父类的默认构造函数、
//但是父类没有默认构造,因而报错
}
什么是默认构造函数?
全缺省的,编译器默认生成的,你显示写的无参的构造函数我们都叫做默认构造函数。
如果你显示写了子类的构造函数,并且都正常去对子类中的内置类型做了处理,也调用了子类中自定义类型的构造函数,指明调用了父类中的构造函数,那么编译器就会按照你写的去走。
但是如果你显示写了子类的构造函数,但是里面什么都没写,那么编译器怎么做呢?此时请注意:编译器依然会对子类内置类型忽略,对子类中的自定义类型去调用对应的构造函数,仍然会调用父类的默认构造。为什么?明明我什么都没有写啊!因为编译器会自动走构造函数的初始化列表!
class Fa
{
public:
int _fa;
//此时_fa存在默认构造函数
};
class So : public Fa
{
public:
int _so;
So()
:_so(1)
{}
};
void test4()
{
So s;
//此时so写了子类构造函数,会去调用父类默认构造函数。
}
class Fa
{
public:
int _fa;
Fa(int c)
:_fa(1)
{
//此时Fa没有默认构造函数
}
};
class So : public Fa
{
public:
int _so;
So()
:Fa(1)//明确写要调用父类的非默认构造函数
,_so(1)
{}
};
void test4()
{
So s;
//So明确写了构造函数,虽然父类中没有默认构造,但是子类构造明确调用父类有参构造,所以也可以正常运行
}
7.2拷贝构造函数
拷贝构造的逻辑基本与构造函数是一样的,依然编译器会自动给你生成一个。这里就不再多介绍了。
不过有一点我需要强调哈:就是拷贝构造函数与构造函数是并列关系,显示写有参构造不会影响编译器生成拷贝构造函数。但是我写一个拷贝构造函数编译器不再生成默认构造函数了哈。
这个地方比较奇怪,这都怪CPP的老古董语法了~
class father
{
public:
int _f = 1;
};
class son : public father
{
public:
int _s = 1;
};
void test5()
{
son s;
son s2(s);
//子类有默认拷贝构造,父类也有,所以这时候是没有问题的
}
class father
{
public:
int _f = 1;
father() = default;//强制生成默认构造函数
//拷贝构造
father(father& f)
:_f(f._f)
{}
};
class son : public father
{
public:
int _s = 1;
son() = default;//强制生成默认构造函数
son(son& s)
:_s(s._s)
{
cout << " father " << endl;
}
};
void test5()
{
son s2;
son s(s2);//father
//子类拷贝构造即使不写调用父类拷贝构造,也会去默认调用
}
class son : public father
{
public:
int _s = 1;
son() = default;//强制生成默认构造函数
son(son& s)
:father(s)//明确写调用父类的拷贝构造,注意这个地方会发生切片
,_s(s._s)
{
cout << " father " << endl;
}
};
void test5()
{
son s2;
son s(s2);//father
//子类拷贝构造写调用父类拷贝构造,那么也会去调用父类的拷贝构造函数
}
7.3赋值运算符重载函数
这个跟上面的构造函数还是不太一样的,需要着重说一下。
如果子类和父类的赋值运算符重载函数自己都不写,编译器都会默认进行生成,对于子类的内置类型,直接浅拷贝(值拷贝),对于自定义类型,那么就直接调用对应的拷贝构造函数,同样对于父类的赋值也自然会去调用。
如果子类中明确写了赋值,但是子类赋值没有写要访问父类赋值,此时并不会去调用父类赋值。为什么跟前面两个拷贝构造、构造不一样呢?因为前两个构造都要走初始化列表,但是赋值函数没有初始化列表这一说。
class F1
{
public:
int _f;
F1()
{
_f = 2;
cout << "F1()" << endl;
}
F1& operator=(const F1& f)
{
if (this != &f)//排除自己给自己赋值的情况
{
cout << "F1& operator=(const F1& f)" << endl;
_f = f._f;
}
return *this;
}
};
class S1 : public F1
{
public:
int _s;
S1()
{
_s = 1;
cout << "S1()" << endl;
}
S1& operator=(const S1& s)
{
if (&s != this)
{
//不写,不去默认调用父类的赋值函数。
cout << "S1& operator=(const S1& s)" << endl;
_s = s._s;
}
return *this;
}
};
void test6()
{
S1 s1;//F1() S1()
S1 s2;//F1() S1()
s2 = s1;//S1& operator=(const S1& s)
}
要显示写调用的话怎么写?前面要加类名限定符。不写的后果就是死递归,然后程序挂掉。
class F1
{
public:
int _f;
F1()
{
_f = 2;
cout << "F1()" << endl;
}
F1& operator=(const F1& f)
{
if (this != &f)//排除自己给自己赋值的情况
{
cout << "F1& operator=(const F1& f)" << endl;
_f = f._f;
}
return *this;
}
};
class S1 : public F1
{
public:
int _s;
S1()
{
_s = 1;
cout << "S1()" << endl;
}
S1& operator=(const S1& s)
{
if (&s != this)
{
F1::operator=(s);//这个地方前面一定要写F1,不然就是死递归
cout << "S1& operator=(const S1& s)" << endl;
_s = s._s;
}
return *this;
}
};
void test6()
{
S1 s1;//F1() S1()
S1 s2;//F1() S1()
s2 = s1;//F1& operator=(const F1& f) S1& operator=(const S1& s)
}
7.4析构函数
子类的析构函数调用结束后会自动调用父类的析构函数。->原因在于要保证先析构子类后析构父类,因为子类是可以访问父类的,如果先析构父类,那么再访问父类的成员会出现意想不到的结果。
class Fa
{
public:
int _fa;
~Fa()
{
cout << "~Fa()" << endl;
}
};
class So : public Fa
{
public:
int _so;
~So()
{
cout << "~So()" << endl;
}
};
void test7()
{
So s;
//~So()
//~Fa()
}
子类和父类的析构函数在子类函数中也会发生隐藏/重定义,写的时候也要前面加上类名->这是因为后面多态的缘故,编译器对析构底层做了特殊处理,使得子类和父类的析构函数产生了隐藏/重定义。
class Fa
{
public:
int _fa;
~Fa()
{
cout << "~Fa()" << endl;
}
};
class So : public Fa
{
public:
int _so;
~So()
{
Fa::~Fa();//这个地方前面也得指明类域
cout << "~So()" << endl;
}
};
void test7()
{
So s;
//~Fa()
//~So()
//~Fa()
}
8.继承与友元函数
把继承这个新语法加入与友元函数又有什么火花呢?
继承对于友元函数是没什么关系哈,我们如果把友元函数比作是朋友,那么继承就类似于父子之间的关系,你父亲的朋友跟你没啥关系,你的朋友也跟你父亲没啥关系。
class Student;//类的声明
class Person
{
public:
friend void Display(const Person& p, const Student& s);
protected:
string _name = "1"; // 姓名
};
class Student : public Person
{
protected:
string _s = "2";
};
void Display(const Person& p, const Student& s)
{
cout << p._name << endl;//友元函数仅可以访问父类的东西
//cout << s._stuNum << endl;//报错
}
void test81()
{
Person p;
Student s;
Display(p, s);
}
不过需要注意的是,因为你继承了你父亲的一些成员,所以友元函数是可以访问你继承了你父亲这一部分的成员的,因为这些成员是属于你的(加入说函数在该类友元的话)。
class Student;//类的声明
class Person
{
public:
protected:
string _name = "1"; // 姓名
};
class Student : public Person
{
friend void Display(const Person& p, const Student& s);
protected:
string _s = "2";
};
void Display(const Person& p, const Student& s)
{
cout << s._name << endl; //友元函数可以访问子类继承父类的东西
cout << s._s << endl; //友元函数仅可以访问子类的东西
//cout << p._name << endl;//此时去访问父类的东西会报错
}
void test81()
{
Person p;
Student s;
Display(p, s);
}
9.继承与静态成员
对于一般的变量,父类对象有一份,继承他的子类对象也有一份(前提是父类变量不是私有的哈)。
对于静态变量比较特殊,CPP规定只有一份,既属于父类,也属于子类。请注意,整个父类无论有多少对象,都只有一个static变量!
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例 。
应用:统计下生成了多少个父类+子类对象
class Person
{
public:
Person() { ++_count; }
protected:
string _name; // 姓名
public:
static int _count; // 统计人的个数。
};
int Person::_count = 0;
class Student : public Person
{
protected:
int _stuNum; // 学号
};
class Graduate : public Student
{
protected:
string _seminarCourse; // 研究科目
};
void TestPerson()
{
Student s1;
Student s2;
Student s3;
Graduate s4;
cout << " 人数 :" << Person::_count << endl;
Student::_count = 0;
cout << " 人数 :" << Person::_count << endl;
}
10.菱形继承
10.1单继承与多继承
在介绍什么是菱形继承之前,我先来说一下什么是单继承与多继承的概念。
继承按照可以继承父类的数量可以分为单继承和多继承。
单继承:
多继承:
前面讲的都是单继承,CPP中也有多继承机制,在多继承机制下,CPP为多种场景提供了更好的支持,但是,多继承中的菱形继承存在一定的小问题!
10.2多继承中的菱形继承问题及虚拟继承
在上面菱形继承中,我们发现同一份变量会继承两份。这样会造成数据冗余和二义性问题。
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; // 主修课程
};
void test10()
{
// 这样会有二义性无法明确知道访问的是哪一个
Assistant a;
//a._name = "peter";
// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
a.Student::_name = "xxx";
a.Teacher::_name = "yyy";
}
虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在Student和Teacher的继承Person时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他地方去使用。
class Person
{
public:
string _name; // 姓名
};
class Student : virtual public Person
{
protected:
int _num; //学号
};
class Teacher : virtual public Person
{
protected:
int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:
string _majorCourse; // 主修课程
};
void test10()
{
// 这样会有二义性无法明确知道访问的是哪一个
Assistant a;
a._name = "peter";
// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
a.Student::_name = "xxx";
a.Teacher::_name = "yyy";
cout << a._name << endl;//yyy
cout << a.Student::_name << endl;//yyy
cout << a.Teacher::_name << endl;//yyy
cout << &a._name << endl;//000000860A0FFAA8
cout << &a.Student::_name << endl;//000000860A0FFAA8
cout << &a.Teacher::_name << endl;//000000860A0FFAA8
}
10.3菱形继承中虚拟继承的原理
虚拟继承是如何解决菱形继承二义性、数据冗余的问题的呢?
为了研究虚拟继承原理,我们给出了一个简化的菱形继承继承体系,再借助内存窗口观察对象成员的模型。
//模型代码
class A
{
public:
int _a;
};
class B : public A
//class B : virtual public A
{
public:
int _b;
};
class C : public A
//class C : virtual public A
{
public:
int _c;
};
class D : public B, public C
{
public:
int _d;
};
void test11()
{
//这是在没有使用虚拟继承情况下的菱形继承,看d的内存空间
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
}
class A
{
public:
int _a;
};
//class B : public A
class B : virtual public A
{
public:
int _b;
};
//class C : public A
class C : virtual public A
{
public:
int _c;
};
class D : public B, public C
{
public:
int _d;
};
void test11()
{
//这是在使用虚拟继承情况下的菱形继承,看d的内存空间
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
}
我们发现在d的存储中的确是只有一个_a了,然后多了两个指针,一个是黄色曲线的,一个是蓝色曲线的。
黄色曲线的指针指向了一个00,后面紧跟着一个数字20,这个20代表在d内存中_b到_a之间的偏移量,蓝色同理,代表的是在d内存中_c到_a之间的偏移量。
B区域的开始,0x63C+0x14=0x650,C区域的开始0x644+0x0C=0x650
其中,_a我们称之为虚基类,一般放在最下面,用偏移量进行访问,主要用于切片时候。
菱形继承中的指针:
void test11()
{
//这是在使用虚拟继承情况下的菱形继承,看d的内存空间
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
B* pb = &d;
C* pc = &d;
cout << pb << endl;//00AFF658
cout << pc << endl;//00AFF660
}
需要虚继承的话,那在什么地方加上关键字virtual呢?
10.4在继承公共基类的时候使用虚继承
在这种情况下,也属于菱形继承,在使用虚拟继承的时候,应该把关键字virtual加到Student、Teacher类上。因为他俩有公共的基类。
10.5腰部父类也使用虚继承模型
之所以这样,是因为方便指针进行访问。使用了菱形虚拟继承之后,定义一个中间父类的指针,我们发现既可以是子类做切片进行访问,又可以是访问它本身,为了统一处理,CPP干脆把中间的父类模型结构也换成了与子类大体一致的模型。
10.6菱形继承的实例
菱形继承在库中有一个案例,就是iostream,这个地方用到的就是菱形虚拟继承的方式进行处理的。
11.组合与继承
与继承相似的一种代码复用方式叫做组合。
组合的概念:一个类把另一个类作为他的成员变量。类似于一种包含关系。
class A
{
public:
int _a;
};
class B
{
public:
A _aa;//组合
int _b;
};
void test13()
{
B b;
}
EOF