OOP的特性(继承)
走到这一步,我们可以进一步讨论有关OOP在编程上的具体实现。
上一次,我们提出来类的概念。其实类对应了OOP中的抽象这一概念。我们将事物的共同点提取出来,抽象成了类。
这一次,为了提高代码的重用性,C++提出了继承语法。很好理解,我们将各种种类的羊抽象出来,写出了class 羊。将各种牛特点提取出来,抽象成了 class 牛。如果我们还有抽象出来 马 这一动物。我们发现了牛,羊,马本身也都有共同点,就是它们动物,一般动物会吃喝跑,它们也都会。所以我们抽象出动物这一特性,让牛羊马来继承动物的特性。这样可以有效提高我们代码的效率。
类继承
基本语法
通过以上的例子,我们称动物类为父类(又称基类),牛羊马类为继承动物类的子类(又称派生类)
//父类
class Base
{
};
//子类继承父类
class Son : public Base
{
};
继承的格式:
class 派生类名 : 继承方式 基类名
{
//派生类新增的成员变量或者成员函数
}
- 派生类对象存储了基类的数据成员(派生类继承了基类的实现)
- 派生类对象可以使用基类的方法(派生类继承了基类的接口)
当然,这不意味这子类可以啃老。子类还需要自己实现一些事情:
- 派生类需要自己的构造函数
- 派生类会根据需要添加额外的数据成员和成员函数
有关访问权限(继承方式)
- public
- private
- protected
在这里有一件事需要重点关注:
派生类不能直接访问基类的private函数和变量,但是可以访问protected函数和变量
即,对于外部世界来说,protected和private是一样的;对于派生类来说,protected和public是一样的
派生类不能直接访问基类的私有成员,必须通过基类的方法来进行访问(get和set函数)
这使得我们想到构造函数,不过很遗憾派生类的构造函数不能直接设置继承成员,而必须使用基类的公有方法来访问私有基类成员。即,派生类构造函数必须使用基类构造函数
Son::Son(int a,int b,int c,int d):Base(_b,_c,_d)
{
this->a = a;
}
上述代码如何理解呢?
子类Son的构造函数其实只赋值了a这一成员变量。后面Base(_b,_c,_d)
我们叫成员初始化列表。举一个例子,如果我们给子类Son实例化 Son son(1,2,3,4);
时Son的构造函数把实参 2, 3, 4赋值给形参_a,_b,_c然后将这些参数作为实参赋值给父类Base构造函数,后者将嵌套一个Base对象,并将数据存入这个Base对象,然后进入Son的构造函数,完成对Son对象的创建,并将参数a赋值给this->a。
当然如果使用基类的拷贝构造函数也是可以的:
Son::Son(int a,int b,int c,int d):Base(base)
{
this->a = a;
}
这里使用的是拷贝构造函数,这种方式我们称是隐式的,而上述方法是显式的
如果说我们后面什么都不写,编译器默认调用默认构造函数,即:
Son::Son(int a,int b,int c,int d)
{
this->a = a;
}
就等于
Son::Son(int a,int b,int c,int d):Base()
{
this->a = a;
}
我们总结一下刚刚说过的要点
- 基类对象首先被创建
- 派生类构造函数应通过成员初始化列表将基类信息传递给基类构造函数
- 派生类构造函数应初始化派生类新增的数据成员
我们在这里并没有讨论析构函数,但是需要强调的是:析构函数的顺序与构造函数是相反的,也就是先调用子类的析构,再调用父类的析构。
在使用子类的时候请记住要包含头文件。
[注]:可以和函数的初始化列表做一个联系
有关基类和派生类的一个特殊的用法
这个用法其实需要注意:
基类指针可以在没有进行显示类型转的情况下指向派生类对象;同时基类的引用可以在不进行显示类型转的情况下引用派生类对象。
这样做听起来很美好,不过要注意的是:
基类的指针或者引用只用于调用基类的方法。
这一点是至关重要的,即
Son son1(1,2,3,4);
Base & sn = son1;
Base * psn = &son1;
//这样是允许的。但是使用sn 或者 *psn调用派生类(Son)的方法是不允许的!
其实,我到目前为止一直在避免提及内存的问题,但是时至今日也应该慢慢开始C++的内存管理问题了。没错,这里就是涉及一个内存的问题,请慢慢看下去:
首先,毋庸置疑的是子类的存储空间肯定比父类的存储空间大。(父类有的子类都有,子类有的父类却不一定有)
所以,一个指向父类的指针 的寻址范围是不是比起子类的存储空间要小。这时,你用这个指向父类的指针去寻子类方法的地址,很有可能会超出这个指向父类的指针的寻址能力,这样是会有很大的安全隐患,编译器是不允许的。
当然引用也是这个道理。(我们等等还会继续说这个问题,目前先这样。)
但是,反过来——指向派生类的指针或者引用可以调用父类的方法吗?
答案是可以的,我们把这种手法称之为“多态”。
多继承
C++中的继承并不像Java中只能单继承。C++的子类是可以继承多个父类。
但是,在多继承中很容易引发二义性。这时请使用作用域运算符进行解决。
实际上,多继承容易出现的问题并不仅仅是命名问题,还有一个就是菱形继承。
(这里就不给UML图了,本人还是懒)
- 羊继承了动物,驼也同样继承了动物。当羊驼调用属性或者方法时就会出现二义性。
- 羊驼继承了羊和驼,而羊和驼都继承了动物,所以羊驼这里就会将动物的数据复制两份,这样就造成了空间的浪费。
这时,我们又要引入C++的一个解决办法:虚基类
首先,具体怎么做?在继承方式前加 vitual 关键字
class Animal { public: int Age; };
class Sheep : virtual public Animal { };//虚基类
class Tuo : virtual public Animal { };
class SheepTuo: public Sheep,public Tuo { };
虚基类的工作原理
虚继承可以解决多种继承前面提到的两个问题:
虚继承底层实现原理与编译器相关,一般通过虚基类指针和虚基类表实现,每个虚继承的子类都有一个虚基类指针(占用一个指针的存储空间,4字节)和虚基类表(不占用类对象的存储空间)(需要强调的是,虚基类依旧会在子类里面存在拷贝,只是仅仅最多存在一份而已,并不是不在子类里面了);当虚继承的子类被当做父类继承时,虚基类指针也会被继承。
实际上,vbptr指的是虚基类表指针(virtual base table pointer),该指针指向了一个虚基类表(virtual table),虚表中记录了虚基类与本类的偏移地址;通过偏移地址,这样就找到了虚基类成员,而虚继承也不用像普通多继承那样维持着公共基类(虚基类)的两份同样的拷贝,节省了存储空间。
后面我们会和虚函数(多态)进行比较。
同时这里会有一个使用Visual Studio命令提示功能来查看内存分布的技巧,就不展开说了。
OOP的特性(多态)
其实,刚刚的描述已经解释了什么是多态了。用指向子类对象的指针或者引用去调用父类的函数。为什么要这么做?我们从常识来理解一下。比如:猫和鱼都可以继承动物这个类。如果动物这个类里面有 move() 移动这个方法。但是,我们都知道猫和鱼的移动方式是不同的。所以我们要利用多态,来使得我们的程序更符合现实。
类多态
重载(overload)
函数重载
函数重载,从常识来考虑。比如:我们通过一个函数来计算得某个结果。但是,给这个函数一个参数 函数可以计算,给函数两个参数 函数也可以计算(计算的方法可能不一样),或者给函数三个参数仍然可以计算(可能计算出来的精确度进一步提升了)。这样的话,我们的函数名字一样,但是参数却不一样。这样的就是函数重载。当然没有必要计算的意义也一样。简单的来说,函数重载就名字一样,返回值类型一样,就是参数不一样了。
所以,我们提炼出几点:
- 函数名称相同
- 函数返回值相同
- 函数参数的个数,类型可以不同
- 需要在同一作用域下
[注]:当函数重载遇到默认参数时,要避免二义性。
void func(int a,int b = 10) { }
void func(int a) { }
void test() { func(10);//二义性,编译器不知道使用哪个func()了 }
简单的说一下默认参数,其实很简单。就是给函数参数设置一个默认值,在参数列表直接等于就行了,按照以上例子你可以不给b值,默认是10;但是默认参数必须是最后面。不能插入没有设定默认值的参数void test(int a = 10 ,int b);
这样是不行的。
重载的原理
在面对func()时,编译器会可能默认把名字改成_func;当碰到func(int a)时,可能会默认改成_func_int;当碰到func(int a,char b)编译器可能会默认该成_func_int_char。这个“可能”意思时指,如何修饰函数名,编译器并没有一个统一的标准,所以不同编译器会产生不同的内部名。
算符重载
C++同时也允许给算符赋予新的意义
返回值 opertaor算符 (参数列表)
但是C++中并不是所有算符都可以重载的:
以下是可以重载的算符:
以下是不可以重载的算符:
虽然在规则上是可以重载 && 和 ||,但是在实际应用中最好不好重载这两个运算符。其原因是内置版本的&& ||首先计算左边的表达式,如果可以确定结果,就无需计算右边了,我们已经习惯这种特性了,一旦重载便会失去这种特性。
- =,[] ,->,() 操作符只能通过成员函数进行重载
- <<和>> 只能通过全局函数配合友元函数进行重载
- 不要重载&& 和||,因为无法实现其运算规则
算符重载的重要应用——智能指针
由于C++语言没有自动内存回收机制,程序员每次new出来的内存都要手动delete。程序员忘记delete,流程太复杂,最终导致没有delete,异常导致程序过早退出,没有执行delete的情况并不罕见。
所以,开发者可以通过算符重载,从而达到智能管理内存的效果。
1.对于编译器来说,智能指针实际上是一个栈对象,并非指针类型,在栈对象生命期即将结束时,智能指针通过析构函数释放有它管理的堆内存。所有智能指针都重载了“operator->”操作符,直接返回对象的引用,用以操作对象。访问智能指针原来的方法则使用“.”操作符。
2.所谓智能指针,是根据不同的场景来定制智能指针。以下给出一个最简单的应用:
class Person
{
public:
Person(int age)
{
this->Age = age;
}
~Person()
{
}
void showAge()
{
cout<<"年龄为:"<<this->Age<<endl;
}
private:
int Age;
};
//智能指针,用来托管自定义类型的对象,让对象自动释放。
class smartPointer
{
public:
smartPointer(Person * person)
{
this->person = person;
}
//重载->让智能指针像Person *p一样去使用
Person * operator->()
{
return this->person;
}
//重载*
Person & operator*()
{
return * this->person;
}
~smartPointer()
{
cout<<"智能指针析构了!"<<endl;
if(this->person != NULL)
{
delete this->person;
this->oerson = NULL;
}
}
private:
Person * person;
}
void test()
{
Person p(10);//自动析构
//相当于:
//Person * p = new Person(10);
//delete p;
smartPointer sp(new Person(10));//开辟到栈上,自动析构
sp->showAge();//本身sp不支持这样的调用,所以要重载->
(*sp).showAge();//同样作为智能指针,也要支持这样的写法。所以依旧重载*
}
有关C++11中的智能指针
我们上文中是通过算符重载来实现的智能指针,在C++11标准中引入了智能指针概念。
1.理解智能指针
- 从较浅的层面看,智能指针是利用了一种叫做RAII(资源获取即初始化)的技术对普通的指针进行封装,这使得智能指针实质是一个对象,行为表现的却像一个指针。
- 智能指针的作用是防止忘记调用delete释放内存和程序异常的进入catch块忘记释放内存。另外指针的释放时机也是非常有考究的,多次释放同一个指针会造成程序崩溃,这些都可以通过智能指针来解决。
- 智能指针还有一个作用是把值语义转换成引用语义。
C++和Java有一处最大的区别在于语义不同,在Java里面下列代码:
Animal a = new Animal();
Animal b = a;
//你当然知道,这里其实只生成了一个对象,a和b仅仅是把持对象的引用而已。但在C++中不是这样,
Animal a;
Animal b = a;
//这里却是就是生成了两个对象。
2.智能指针的本质
智能指针是一个类对象,这样在被调函数执行完,程序过期时,对象将会被删除(对象的名字保存在栈变量中),
这样不仅对象会被删除,它指向的内存也会被删除的。
3.智能指针的使用
智能指针在C++11版本之后提供,包含在头文件<memory>中,shared_ptr、unique_ptr、auto_ptr
这里只给出建议(智能指针会涉及到很多知识,属于C++的综合题):
- 每种指针都有不同的使用范围,unique_ptr指针优于其它两种类型,除非对象需要共享时用shared_ptr。
- 如果你没有打算在多个线程之间来共享资源的话,那么就请使用unique_ptr。
- 使用make_shared而不是裸指针来初始化共享指针。
- 在设计类的时候,当不需要资源的所有权,而且你不想指定这个对象的生命周期时,可以考虑使用weak_ptr代替shared_ptr。
重写(override)
简单来说,重写就是返回值,参数,函数名都和圆脸一样,之后函数体里面的方法重写了。
下面将详细介绍。
动态联编和静态联编
程序调用函数时,编译器将源代码中的函数调用解释为特定函数代码块被称为函数名联编(binding)。C语言中没有重载,所以每个函数名字都不同,由于C++中有重载的概念,所以编译器必须查看函数参数以及函数名才能确定使用哪个函数。C/C++编译器可以在编译过程中完成联编。而在编译过程实现的联编称静态联编(static binding)。所谓动态联编(dynamic binding)是指联编在程序运行时动态地进行,根据当时的情况来确定调用哪个同名函数,实际上是在运行时虚函数的实现。国内教材有的称之为束定。
通过动态联编引出了虚函数:
虚函数
语法上来说,虚函数的写法是:在类成员函数声明的时候添加 vitual关键字。
我们继续刚刚有关基类和派生类的特殊用法继续说,
将派生类的引用或指针转换成基类的引用和指针我们称之为:向上强制转换(upcasting)
相反,将基类的引用或指针转换成派生类的引用和指针我们称之为:向下强制转换(downcasting)
我们现在知道,向下转型是不被允许的。
虚函数的工作原理——虚函数表和虚函数指针
虚函数指针
虚函数指针 (virtual function pointer) 从本质上来说就只是一个指向函数的指针,与普通的指针并无区别。它指向用户所定义的虚函数,具体是在子类里的实现,当子类调用虚函数的时候,实际上是通过调用该虚函数指针从而找到接口。
虚函数指针是确实存在的数据类型,在一个被实例化的对象中,它总是被存放在该对象的地址首位,这种做法的目的是为了保证运行的快速性。与对象的成员不同,虚函数指针对外部是完全不可见的,除非通过直接访问地址的做法或者在DEBUG模式中,否则它是不可见的也不能被外界调用。
只有拥有虚函数的类才会拥有虚函数指针,每一个虚函数也都会对应一个虚函数指针。所以拥有虚函数的类的所有对象都会因为虚函数产生额外的开销,并且也会在一定程度上降低程序速度。与JAVA不同,C++将是否使用虚函数这一权利交给了开发者,所以开发者应该谨慎的使用
虚函数表(以下解释,来自于https://blog.csdn.net/haoel/a...。因为做图太麻烦,所以直接选择性的截取一点。)
在这个表中(V-Table),主是要一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其容真实反应实际的函数。这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数。
编译器应该是保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证取到虚函数表的有最高的性能——如果有多层继承或是多重继承的情况下)。 这意味着我们通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。
举个例子:
class Base {
public:
virtual void f() { cout << "Base::f" << endl; }
virtual void g() { cout << "Base::g" << endl; }
virtual void h() { cout << "Base::h" << endl; }
};
按照上面的说法,我们通过把Base实例化,来获得虚函数表。
typedef void(*Fun)(void);
Base b;
Fun pFun = NULL;
cout << "虚函数表地址:" << (int*)(&b) << endl;
cout << "虚函数表 — 第一个函数地址:" << (int*)*(int*)(&b) << endl;
// Invoke the first virtual function
pFun = (Fun)*((int*)*(int*)(&b));
pFun();
实际结果如下:
虚函数表地址:0012FED4
虚函数表—第一个函数地址:0044F148
Base::f
通过这个示例,我们可以看到,我们可以通过强行把&b转成int ,取得虚函数表的地址,然后,再次取址就可以得到第一个虚函数的地址了,也就是Base::f(),这在上面的程序中得到了验证(把int 强制转成了函数指针)。通过这个示例,我们就可以知道如果要调用Base::g()和Base::h(),其代码如下:
(Fun)*((int*)*(int*)(&b)+0); // Base::f()
(Fun)*((int*)*(int*)(&b)+1); // Base::g()
(Fun)*((int*)*(int*)(&b)+2); // Base::h()
在上面这个图中,我在虚函数表的最后多加了一个结点,这是虚函数表的结束结点,就像字符串的结束符“/0”一样,其标志了虚函数表的结束。这个结束标志的值在不同的编译器下是不同的。(有可能是NULL也有可能是0)
同时,派生类是否对父类函数进行了覆盖,虚函数表也是不一样的,所以我们分情况来讨论。
1.无覆盖
定义如下的继承关系:
对于实例而言,其虚函数表:
- 虚函数按照其声明顺序放于表中。
- 父类的虚函数在子类的虚函数前面。
2.有覆盖(这才是一般情况,因为虚函数不覆盖便毫无意义)
我们只重载了f()。所以其虚函数表:
- 覆盖的f()函数被放到了虚表中原来父类虚函数的位置。
- 没有被覆盖的函数依旧。
Base \*b =newDerive();
b->f();
由b所指的内存中的虚函数表的f()的位置已经被Derive::f()函数地址所取代,于是在实际调用发生时,是Derive::f()被调用了。这就实现了多态。
3.有多个继承但是无覆盖
这是其虚函数表:
- 每个父类都有自己的虚表。
- 子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)
4.有多个继承且有覆盖
其虚函数表是:
三个父类虚函数表中的f()的位置被替换成了子类的函数指针。这样,我们就可以任一静态类型的父类来指向子类,并调用子类的f()了。
Derive d;
Base1 *b1 = &d;
Base2 *b2 = &d;
Base3 *b3 = &d;
b1->f(); //Derive::f()
b2->f(); //Derive::f()
b3->f(); //Derive::f()
b1->g(); //Base1::g()
b2->g(); //Base2::g()
b3->g(); //Base3::g()
总结
走到这一步,我们就可以总结一下了。
刚刚一直在说一个新的词汇——覆盖。但其实,可能很多人现在已经知道了,这里的覆盖就是重写。
所谓静态联编就是函数重载,所谓动态联编就是函数重写
向下强制转型不被编译器允许。
同时,我们利用虚函数表的特性仍可以做非法的行为:
访问non-public的函数
如果父类的虚函数是private或是protected的,但这些非public的虚函数同样会存在于虚函数表中,所以,我们同样可以使用访问虚函数表的方式来访问这些non-public的虚函数,这是很容易做到的。
class Base {
private:
virtual void f() { cout << "Base::f" << endl; }
};
class Derive : public Base{
};
typedef void(*Fun)(void);
void main() {
Derive d;
Fun pFun = (Fun)*((int*)*(int*)(&d)+0);
pFun();
}
我们现在应该明白:编译对虚函数使用动态联编的意思了。
Q:为什么编译默认是静态联编?
A:我们除了功能以外始终不能忽视就是效率。因为根据上文的描述,我们不难想到用一些方法来跟踪基类指针或引用指向的类模型这件事本身其实增加我们的开销。所谓C++编译器选择了开销更低的方式。我们应该优先选择效率更高的方式来开发程序。
当我们知道了虚函数的原理的同时也必须知道虚函数到底增加哪些开销:
- 每个对象都会增加存储地址的空间。
- 对于每一个类,编译器都会创建虚函数表(数组)
- 每一个函数调用时都增加了额外操作——查找地址。
注意
- 在基类方法中声明关键字virtual可使该方法在基类以及所有派生类中是虚拟的
- 如果使用指向对象的指针或引用来调用虚方法,程序将使用为对象类型定义的方法,而不使用为指针或引用类型定义的方法。这称为动态联编
- 如果定义的类被用做基类,则应将那些要在派生类中重新定义的类方法声明中虚拟
- 构造函数不能是虚函数,派生类不会继承基类的构造函数
- 析构函数应该是虚函数,除非不是基类。(最好这么做,因为普通析构不会调用子类析构函数,会导致释放不干净)
- 友元不能是虚函数,友元根本就不是类成员。
- 如果你在编程的时候写出了如下代码:
class A
{
public:
vitual void A(int a){...}
};
class B:piblic A
{
public:
vitual void A(){...}
};
派生类中没有参数的A把基类中有参数的A给隐藏了,并没有重写。有可能编译器给你警告,也有可能不会。在《C++ Prime Plus》中将这样的错误称为“返回类型协变(covariance of return type)”
- 如果两个函数构成了重写的关系,必须两个都加vitual关键字。
抽象类与接口
抽象类(abstract base class ,ABC),这里的抽象类其实就是Java中所说的接口。并不难理解。
这里举一个例子:羊类。我们可以写出山羊类来继承羊类,同样也可以写绵羊类来继承羊类,也许我们还能写出更不一样的羊来继承羊类。但别忘了,我们必须给羊类的成员函数做出一个定义,即便羊类的成员函数里根本没有有意义的代码。那我们与其写没有意义的代码,倒不如干脆什么都别写。再具体一点: 羊会跑——void run() 同时 void run()中可能会用到羊类里面的一个属性——奔跑的速度。但是,不同种类的羊跑的速度又不一样快。这是我们会在void run()里面什么都不写。直接一个{}就完事。等待子类重写这个void run()。所以,这里的run()虽然有定义,但是这却是一个接口的思想。所以,我们可以把void run()写出ABC的样子:vitual void run() = 0;
这样这个类就变成了抽象类,而run这个函数就成为了纯虚函数即,这个羊类纯粹是为了让其他类继承重写而出现的。这样如果以后有新的需求可以直接来实现这个羊的接口。
- 只要类里有一个纯虚函数,这个类就是抽象类
- 当继承一个抽象类时,必须实现其所有纯虚函数。如果不这么做的话,派生类仍是抽象类
- 抽象类不能实例化!
有关继承和动态内存分配
派生类不使用 new 的情况
- 析构函数使用默认析构函数即可。默认析构函数也是执行一些操作:执行完自身后调用基类的析构函数。
- 拷贝构造函数使用默认的拷贝构造函数即可。
- 赋值操作符也是使用系统默认即可。
综上所述,如果没有new运算符,析构函数,拷贝构造函数和赋值操作符使用默认即可
派生类使用 new 的情况
- 派生类的析构函数自动调用基类析构函数,故其自身的职责就是对派生类的构造函数申请的堆空间进行清理
- 派生类的拷贝构造函数只能访问派生类的数据,所以派生类的拷贝构造函数必须调用基类的拷贝构造函数来处理共享的基类数据。
- 赋值操作符:由于派生类使用了new动态分配了内存,所以它需要一个显式赋值运算符。因为派生类的方法只能访问派生类的数据,但是派生类的赋值运算符必须负责所有继承的基类对象的赋值,可以显式调用基类赋值操作符来完成这个工作。
综上所述,当基类和派生类都动态分配内存时,派生类的析构函数,拷贝构造函数,复制运算符都必须使用相应基类的方法来处理基类元素。当然这三者完成这项任务的手段都不同:
- 析构函数是自动完成
- 拷贝构造函数通过初始化成员列表中调用基类的拷贝构造函数来完成,如果这么做就会默认调用基类的默认构造函数
- 赋值运算符,是通过作用域运算符来显式调用基类的赋值运算符来完成的。
来自《C++ Prime Plus》的一个范例:
//"dma.h"
#ifndef DMA_H_
#define DMA_H_
#include <iostream>
class baseDMA
{
private:
char * label;
int rating;
public:
baseDMA(const char * l = "null", int r = 0);
baseDMA(const baseDMA & rs);
virtual ~baseDMA();
baseDMA & operator= (const baseDMA & rs);
friend std::ostream & operator<<(std::ostream & os,const baseDMA & rs);
};
class lacksDMA:public baseDMA
{
private:
enum{COL_LEN = 40};
char color[COL_LEN];
public:
lacksDMA(const char * c = "blank", const char * l = "null", int r = 0);
lacksDMA(const char * c, const baseDMA & rs);
friend std::ostream & operator<<(std::ostream & os,const lacksDMA & rs);
};
class hasDMA:public baseDMA
{
private:
char * style;
public:
hasDMA(const char * s = "none", const char * l = "null", int r = 0);
hasDMA(cosnt char * s, const baseDMA & rs);
~hasDMA();
hasDMA & operator= (cosnt hasDMA & rs);
friend std::ostream & operator<< (std::ostream & os,const hasDMA & rs);
};
#ennif
#include "dam.h"
#include <cstring>
baseDMA::baseDMA(const char * l, int r)
{
label = new char[std::strlen(l) + 1];
std::strcpy(label, rs.label);
rating = rs.rating;
}
baseDMA::~baseDMA()
{
delete [] label;
}
baseDMA & baseDMA::operator=(const baseDMA & rs)
{
if(this == &rs)
reurn *this;
delete [] label;
label = new char[std::strlen(rs.label) + 1];
std::strcpy(label, rs.label);
rating = rs.rating;
return *this;
}
std::ostream & operator<<(std::ostream & os, const baseDMA & rs)
{
os<<"Label:"<< rs.label <<std::endl;
os<<"Rating:"<< rs.rating << std::endl;
return os;
}
lacksDMA::lacksDMA(const char * c, const char * l, int r):baseDMA(l,r)
{
std::strcpy(color, c, 39);
color[39] = '\0';
}
lacksDMA::lacksDMA(const char * c, const baseDMA & rs):baseDMA(rs)
{
std::strncpy{color, c, COL_LEN - 1};
color[COL_LEN - 1] = '\0';
}
std::ostream & operator<<(std::ostream & os, const lacksDMA & ls)
{
os<< (const baseDMA &) ls;
os<<"Color: "<< ls.color << std::endl;
}
hasDMA::hasDMA(cosnt char * s, const char * l, int r):baseDMA(l, r)
{
style = new char[std::strlen(s) + l];
std::strcpy(style,s);
}
hasDMA::hasDMA(const char * s, const baseDMA & rs):baseDMA(rs)
{
style = new char[std::strlen(s) + l];
std::strcpy(style,hs.style);
}
hasDMA::~hasDMA()
{
delete [] style;
}
hasDMA & hasDMA::operator=(const hasDMA & hs)
{
if(this == &hs)
return *this;
baseDMA::operator=(hs);
style = new char[std::strlen(hs.style) + 1];
std::strcpy(style, hs.style);
return *this;
}
std::ostream & operator<<(std::ostream & os, const hasDMA & hs)
{
os << (cosnt baseDMA & ) hs;
os << "Style: " << hs.style << std::endl;
return os;
}
//main.cpp
#include <iostream>
#include "dma.h"
int main()
{
using std::cout;
using std::endl;
baseDMA shirt("Porablelly", 8);
lacksDMA balloon("red", "Blimpo", 4);
hasDMA map("Mercator", "Buffalo Keys", 5);
cout << shirt << endl;
cour << balloon << endl;
lacksDMA balloon2(balloon);
hasDMA map2;
map2 = map;
cout << balloon2 << endl;
cout << map2 << endl;
return 0;
}
-----本文仅个人观点,欢迎讨论。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。