1.1
- 继承是非常重要的,在面向对象编程中有着举足轻重的地位。尤其是在设计模式中。
1.1.1 生活中的继承
龙生龙,凤生凤,老鼠儿子会打洞。
生活中的继承是一种长相和行为的继承。
1.1.2 类之间的关系
- 组合关系(
思想很重要
)
一台电脑有CPU,主板,显示器等组成 - 继承关系(
思想很重要
)
电脑分为惠普,联想等(is a )
1.1.3
1.2 类的组合关系
1.2.1代码实例
电脑类的实现
#include <iostream>
using namespace std;
class Disk
{
public:
Disk()
{
printf("Disk()\n");
}
~Disk()
{
printf("~Disk()\n");
}
};
class CPU
{
public:
CPU()
{
printf("CPU()\n");
}
~CPU()
{
printf("~CPU()\n");
}
};
class MainBoard
{
public:
MainBoard()
{
printf("MainBoard()\n");
}
~MainBoard()
{
printf("~MainBoard()\n");
}
};
class Memory
{
public:
Memory()
{
printf("Memory()\n");
}
~Memory()
{
printf("~Memory()\n");
}
};
class Computer
{
public:
Computer()
{
printf("Computer()\n");
}
~Computer()
{
printf("~Computer()\n");
}
private:
Disk disk;
Memory memory;
MainBoard mainBoard;
CPU cpu;
};
int main()
{
Computer c1;
return 0;
}
先父母 再客人,后自己
运行结果
1.2.2 组合关系的特点
- 将其他类的对象作为当前类的成员来使用
- 当前类的对象于成员对象的生命期相同
- 成员对象在用法上于普通对象完全一致
- 组合关系是同生死,共存亡
注意: 实际中我们如果能用组合尽量不要用继承,因为继承的复杂度远大于组合的复杂度。
1.3 继承关系
1.3.1 继承
- 子类对象拥有父类的所有属性和行为
子类就是一种特殊的父类
子类对象可以当做父类对象使用
,(子类对象当做父类对象使用会退化成父类对象,也就是子类中属性和方法不能使用了)- 子类中可以添加父类中没有的方法和属性
- 继承可以进行代码复用
1.3.2 重要规则
- 子类是一个特殊的父类
- 子类对象可以直接初始化父类对象
- 子类对象可以直接赋值给父类对象
1.3.3 示例代码
class Parent
{
private:
int mv;
public:
Parent()
{
cout<<"Parent()"<<endl;
mv = 10;
}
void method()
{
cout<< "mv= "<<mv <<endl;
}
};
class Child :public Parent //
{
public:
void print()
{
cout<<"i'm child class"<<endl;
}
};
int main()
{
Child c ;
c.method(); //继承父类的方法
Parent p1 = c; // 调用父类copy 构造
Parent p2 ; //
p2 =c; //子类对象给父类对象赋值
return 0;
}
1.4 继承中的情况分析
1.4.1对类的属性在继承中如何初始化
-
类的属性在继承中初始化的时候分工是很明确的。
(1)父类的成员变量的初始化在父类中进行
(2)子类的成员变量在子类中进行
(3)子类中的类对象的初始化在自己的类中进行初始化,仅仅是在子类中调用初始化列表就行 -
实例程序代码包含了:
(1)二阶构造(父类中二阶构造,子类中二阶构造),注意父类和子类中二级构造的方式,
父类中二级构造和子类中二阶构造是分工进行完成的,只是简单的函数调用而已。
(2)子类中包含类对象(组合的关系),
(3)子类中包含类对象的指针(利用二阶构造进行初始化,),
(4)成员函数是类对象的指针,类对象如何利用组合类的成员函数对类对象进行初始化(这里只能利用成员函数,因为这在类的外部 )
(5)父类和子类中包含同名的成员函数 print函数,成员函数如何被调用(这是后面多态的必要条件)
(6)子类对象初始化父类对象以后,父类对象不能使用子类新增加的成员变量和成员函数。
(7)使用了二阶构造以后,copy构造函数还有用吗?
Child * p2 =child; //true ,因为仅仅是将一个指针指向了一个地址
Child c2 =*child;//error //浅copy ,发生多次释放内存情况,导致程序错误。
具体情况看下面的代码。
#include <iostream>
using namespace std;
class Test
{
protected:
int a;
public:
Test(int a =0)
{
this->a =a;
}
int getA()const
{
return a;
}
void setA(int a)
{
this->a =a;
}
void print()
{
cout <<"Test() a =" << a <<endl;
}
~Test()
{
cout<<"~Test()"<<endl;
}
};
class Parent
{
protected:
int mi;
char * name1;
Parent(int i =0)
{
this->mi =i;
}
bool TwoConstructor(const char* name1)
{
bool ret =true;
this->name1 =new char[strlen(name1)+1]; //importance
if(this->name1 != NULL)
{
strcpy(this->name1,name1);
}
else
{
ret =false;
}
return ret;
}
public:
static Parent * NewInstance(int i,const char* name1)
{
Parent * ret = new Parent(i);
if( !(ret && ret->TwoConstructor(name1)))
{
delete ret;
ret = NULL;
cout<< "Parent::NewInstance(int i,const char* name1) failure "<<endl;
return ret;
}
cout<< "Parent::NewInstance(int i,const char* name1) success "<<endl;
return ret;
}
void print()
{
cout << "mi = "<< mi<< ", name1 = "<<name1<<endl;
}
~Parent()
{
delete[] name1;
name1 = NULL;
cout <<"~Parent()"<<endl;
}
};
class Child :public Parent
{
protected:
int mj;
char *name2;
Test t1;
Test *p1;
Child(int j,int a):t1(a)
{
this->mj =j;
}
bool TwoConstructor(const char* name1,const char* name2,const Test& obj)
{
bool ret =true;
ret =Parent::TwoConstructor(name1);
if(!ret) return ret;
this->name2 = new char[strlen(name2)+1];
if(name2 != NULL)
{
strcpy(this->name2,name2);
}
else
{
return false;
}
p1= new Test;
p1->setA(obj.getA());
return ret;
}
public:
static Child * NewInstance(int j,int a,const char* name1,const char* name2,const Test& obj)
{
Child * ret = new Child(j,a);
if(!(ret && ret->TwoConstructor(name1,name2,obj)))
{
delete ret;
ret = NULL;
cout<< "Child::NewInstance(int i,const char* name1) failure "<<endl;
}
cout<< "Child::NewInstance(int i,const char* name1) success "<<endl;
return ret;
}
void print()
{
Parent::print();
cout<< "mj = "<< mj <<" , name2 = "<<name2 <<endl;
t1.print();
p1->print();
}
void test1()
{
printf("test子类对象初始化父类对象以后,子类对象新添加的成员变量的成员函数是否还能用\n ");
}
~Child()
{
delete[] name2;
delete p1;
cout <<"~Child()"<<endl;
}
};
int main()
{
/*
Parent * parent = Parent::NewInstance(4,"zhangsan");
if(parent != NULL)
{
parent->print();
delete parent;
}
*/
Test t1(2);
Child * child = Child::NewInstance(10,20,"zhangsan","lisi",t1);
if(child != NULL)
{
child->print();
delete child;
}
Parent * parent1 =child;
//parent1->test();//error 子类对象初始化父类对象以后,子类对象新添加的成员变量和成员函数不能使用
return 0;
}
1.4.2 继承的意义
- 继承是C++中代码复用的重要手段。通过继承,可以活动父类的所有功能,并且可以再子类中重写已有功能,或者添加新功能。
1.4.3
1.5 继承的中的访问级别
1.5.1 思考
- 子类是否可以直接访问父类的私有成员?
根据面向对象理论
- 子类拥有父类的一切属性和行为 ===========》子类能够
直接访问
父类的私有成员
根据C++语法: - 外界不能直接访问类的private成员=======》子类
不能直接访问
父类的私有成员
实验证明:
class Parent
{
private:
int mv;
public:
Parent()
{
mv = 10;
}
int value()
{
return mv;
}
};
class Child :public Parent //
{
public:
int addValue(int v)
{
mv = mv +v;
}
};
int main()
{
return 0;
}
如何解决呢?父类中使用protected
关键字
1.5.2 protected
- 面向对象中的访问级别不只是
public
和private
- 可以定义
protected
访问级别 - 关键字protected的意义
- 修饰的成员不能被外界直接访问
- 修饰的成员可以被子类直接访问
1.5.3 思考
- 为什么需要protected关键字
1.5.4 组合和继承的综合实例 - object 类(被继承),Point(点类) ,Line(类)
#include <iostream>
#include <string>
#include <sstream>
using namespace std;
class Object
{
protected:
string mName;
string mInfo;
public:
Object()
{
mName = "Object";
mInfo = "";
}
string name()
{
return mName;
}
string info()
{
return mInfo;
}
};
class Point :public Object
{
private:
int mX;
int mY;
public:
Point(int mx = 0,int my =0)
{
ostringstream s;
mName = "Point";
mX = mx;
mY = my;
s <<"p("<<mX<<","<<mY<<")";
mInfo = s.str();
}
};
class Line :public Object
{
private:
Point mP1;
Point mP2;
public:
Line(Point mp1,Point mp2)
{
ostringstream s;
mP1 = mp1;
mP2 = mp2;
mName ="Line";
s <<"Line from "<<mP1.info() <<" to "<<mP2.info();
mInfo = s.str();
}
};
int main()
{
Object o;
cout<<o.name()<<endl;
cout<<o.info()<<endl;
cout<< endl;
Point p1(10,20);
cout<<p1.info()<<endl;;
cout<<p1.name()<<endl;
Point p2(1,2);
cout<<endl;
Line line(p1,p2);
cout<< line.info()<<endl;
cout<< line.name()<<endl;
return 0;
}
- 注意在写代码过程中,上面代码中下面一行报错了
s <<"Line from "<<mP1.info() <<" to "<<mP2.info();
报错的原因不在于这一行,而是在Object类中的info()函数的时候返回值是void ,应该改成 string
1.6 继承中的构造和析构
1.6.1子类构造函数
- 子类构造函数必须对继承来的成员进行初始化
(1)直接通过初始化列表或者赋值的方式进程初始化
(2)调用父类构造函数进行初始化
1.6.2 父类构造函数在子类中的调用方式
- 默认调用
(1)适用于无参数构造函数和使用默认参数的构造函数 - 显示调用
(1)通过初始化列表进行调用
(2)适用于所有父类构造函数
1.6.3 对象创建时构造函数调用顺序
1.调用父类构造函数
2. 调用成员变量的构造函数
3. 调用类自身的构造函数
总结1:先父母,再客人,后自己
总结2:析构与构造顺序相反
1.7 父子间的冲突
1.7.1 思考
- 子类中是否可以定义父类中的同名成员
- 如果可以,如何区分?如果不可以,为什么?
- 写代码说明
class Parent
{
public:
int mi;
};
class Child : public Parent
{
public:
int mi;
};
int main()
{
Child c;
c.mi = 100; // mi 究竟是子类自定义的,还是从父类继承得到的?
return 0;
}
- mi 究竟是子类自定义的,还是从父类继承得到的?
1.7.2 理论
-
子类可以定义父类中的同名成员
-
子类中的成员将隐藏父类中的同名成员
-
父类中的同名成员依然存在于子类中
-
通过
作用域分辨符(::)
访问父类中的同名成员 -
代码
namespace A
{
int g_i = 0;
}
namespace B
{
int g_i = 1;
}
class Parent
{
public:
int mi;
Parent()
{
cout << "Parent() : " << "&mi = " << &mi << endl;
}
};
class Child : public Parent
{
public:
int mi;
Child()
{
cout << "Child() : " << "&mi = " << &mi << endl;
}
};
int main()
{
Child c;
c.mi = 10;
c.Parent::mi = 100;
cout << "&c.mi = " << &c.mi << endl;
cout << "c.mi = " << c.mi << endl;
cout << "&c.Parent::mi = " << &c.Parent::mi << endl;
cout << "c.Parent::mi = " << c.Parent::mi << endl;
return 0;
}
1.7.3 函数重载再论
- 子类中定义的函数是否能重载父类中的同名函数?
- 示例1
class Parent
{
public:
int mi;
void add(int v)
{
mi += v;
}
void add(int i ,int j)
{
mi += (i + j);
}
};
class Child :public Parent
{
public:
int mi;
};
int main()
{
Child c1 ;
c1.mi = 100;
c1.Parent::mi =1000;
c1.add(1); //why 为什么都累加到父类中的mi了? 正常逻辑来看,当父类定义mi,以及add函数的时候,还没有子类中的mi,add作用于父类的mi是合情合理的。
c1.add(2,3);
cout<< c1.mi<<endl; // 100
cout<< c1.Parent::mi <<endl; //1006
return 0;
}
- 示例代码2:
class Parent
{
public:
int mi;
void add(int v)
{
mi += v;
}
void add(int i ,int j)
{
mi += (i + j);
}
};
class Child :public Parent
{
public:
int mi;
void add(int a,int b, int c)
{
mi += (a + b + c);
}
};
int main()
{
Child c1 ;
c1.mi = 100;
c1.Parent::mi =1000;
c1.add(1);
c1.add(2,3);
cout<< c1.mi<<endl;
cout<< c1.Parent::mi <<endl;
return 0;
}
-
为啥会报错
-
原因: 在子类中定义和父类同名的函数也会发生同名覆盖,父类中的add函数被隐藏 了。父类中的add 和子类中的add 不可能是重载,因为父类中的add和子类中的add在不同的作用域中。
-
那么如何解决上面的问题呢? 加作用域符
c1.Parent::add(1)
; -
实验结论: 同样会发生同名覆盖。
-
子类中的函数将隐藏父类的同名函数
-
子类无法重载父类中的成员函数
-
使用作用域分辨符访问父类中的同名函数
-
子类可以定义父类中完全相同的成员函数(多态的必备)
1.8 同名覆盖引发的问题
1.8.1 父子兼容性
-
子类对象可以直接赋值给父类对象
-
子类对象可以直接初始化父类对象
-
父类指针可以直接指向子类对象
-
父类引用可以直接引用子类对象
-
代码示例
class Parent
{
public:
int mi;
Parent(int mi =0)
{
this->mi =mi;
}
void add(int v)
{
mi += v;
}
void add(int i ,int j)
{
mi += (i + j);
}
};
class Child :public Parent
{
public:
int mv;
Child(int mv = 0)
{
this->mv = mv;
}
void add(int a,int b, int c)
{
mv += (a + b + c);
}
};
int main()
{
Parent p;
Child c;
p = c; // 子类对象可以直接赋值给父类对象
Parent p1(c); // 子类对象可以直接初始化父类对象
Parent &p3 =c; //父类引用可以直接引用子类对象
Parent * p4 = &c; // 父类指针可以直接指向子类对象
p3.mi = 100;
p3.add(1); // // 没有发生同名覆盖
p3.add(2,3); // 没有发生同名覆盖
// 为什么编译不过?
// p4->mv = 0;
// p4->add(1,2,3);
return 0;
}
1.8.2 父类指针(引用)指向子类对象时
- 子类对象退化成父类对象
- 只能访问父类中定义的成员
- 可以直接访问被子类覆盖的同名成员
1.8.3函数重写
-
子类中可以重定义父类中已经存在的成员函数
-
这种重定义发生在继承中,叫函数重写
-
函数重写是同名覆盖的一种特殊情况
-
当函数重新遇上赋值兼容会发生什么?
1.8.4 举例
class Parent
{
public:
int mi;
Parent(int mi =0)
{
this->mi =mi;
}
void print()
{
cout<<"this is parent"<<endl;
}
};
class Child :public Parent
{
public:
int mv;
Child(int mv = 0)
{
this->mv = mv;
}
void print()
{
cout<<"this is child"<<endl;
}
};
void how_to_print(Parent * p)
{
p->print();
}
int main()
{
Parent p;
Child c;
p.print();
c.print();
how_to_print(&p); // this is parent ?
how_to_print(&c); // this is parent?
return 0;
}
- 结果
1.8.5 问题分析 - 编译期间 ,编译器只能根据指针的类型判断所指向的对象
- 根据赋值兼容,编译器认为父类指针指向的是父类对象
- 因此,编译结果只可能是调用父类中定义的同名函数
void how_to_print(Parent * p)
{
p->print();
}
在编译这个函数的时候,编译器不可能知道指针p究竟指向了什么。但是编译器没有理由报错,于是,编译器认为最安全的做法是调用父类的print函数,因为父类和子类肯定都有相同的print函数。
- 问题引出,编译器的处理方法是合理的吗? 是期望的吗?下节介绍
参考一 :狄泰软件学院C++进阶剖析
参考二 : C++ primer中文版