目录
1、同名覆盖与函数重写
子类可以定义父类中的同名成员 (成员变量、成员函数),子类中的成员将隐藏父类中的同名成员
父类中的同名成员依然存在于子类中 ,通过作用域分辨符访问父类中的同名成员 ,子类无法重载父类中的成员函数(作用域不同)
#include <iostream>
using namespace std;
class Parent
{
public:
int mi;
void add(int v)
{
mi += v;
}
void add(int a, int b)
{
mi += (a + b);
}
};
class Child : public Parent
{
public:
int mi; // 同名覆盖
// 子类任意一个add函数将隐藏父类的同名函数,子类无法直接调用父类同名函数(同名覆盖)
// 也就是说当删除这里任意add()函数,子类无法再直接调用父类add(int),需要加上作用域分辨符
void add(int v)
{
mi += v;
}
void add(int a, int b)
{
mi += (a + b);
}
void add(int x, int y, int z)
{
mi += (x + y + z);
}
};
int main()
{
Child c;
c.mi = 100; // 子类mi
c.Parent::mi = 1000; // 父类mi
c.add(1); // 调用子类的add函数
c.add(2, 3);
c.add(4, 5, 6);
cout << "c.mi = " << c.mi << endl; // 121 = 100 + 1 + 2 + 3 + 4 + 5 + 6
cout << "c.Parent::mi = " << c.Parent::mi << endl; // 1000
c.Parent::add(1); // 调用父类add(int)
cout << "c.Parent::mi = " << c.Parent::mi << endl; // 1001
return 0;
}
子类中可以定义父类中已经存在的成员函数,这种重定义发生在继承中,叫做函数重写 ,函数重写是同名覆盖的一种特殊情况
2、父子间的赋值兼容
子类对象可以当作父类对象使用
子类对象可以直接赋值给父类对象 ,子类对象可以直接初始化父类对象
父类指针可以直接指向子类对象 ,父类引用可以直接引用子类对象
当使用父类指针(引用)指向子类对象时
子类对象退化为父类对象 ,只能访问父类中定义的成员 ,可以直接访问被子类覆盖的同名成员
#include <iostream>
using namespace std;
class Parent
{
public:
int mi;
void add(int i)
{
mi += i;
}
void add(int a, int b)
{
mi += (a + b);
}
};
class Child : public Parent
{
public:
int mv; // 隐藏父类同名成员
void add(int x, int y, int z) // 隐藏父类同名成员
{
mv += (x + y + z);
}
};
int main()
{
Parent p;
Child c;
p = c; // 赋值
Parent p1(c); // 初始化
Parent& rp = c; // 引用
Parent* pp = &c; // 指针
rp.mi = 100;
rp.add(5); // 调用了父类的add(int)
rp.add(10, 10);
// pp->mv = 1000; // error
// pp->add(1, 10, 100); // error
return 0;
}
3、当函数重写遇上赋值兼容
赋值兼容的问题 problem.cpp
#include <iostream>
using namespace std;
class Parent
{
public:
void print()
{
cout << "I'm Parent." << endl;
}
};
class Child : public Parent
{
public:
void print() // 函数重写
{
cout << "I'm Child." << endl;
}
};
void how_to_print(Parent* p) // 赋值兼容
{
p->print();
}
int main()
{
Parent p;
Child c;
how_to_print(&p); // Expected to print: I'm Parent.
how_to_print(&c); // Expected to print: I'm Child.
return 0;
}
编译期间,编译器只能根据指针的类型判断所指向的对象 ,根据赋值兼容,编译器认为父类指针指向的是父类对象 ,因此,编译结果只可能是调用父类中定义的同名函数
在编译这个函数的时候,编译器不可能知道指针p究竟指向了什么, 但是编译器没有理由报错。于是,编译器认为最安全的做法是调用父类的print函数,因为父类和子类肯定都有相同的print函数。
编译器的这种处理显然不是我们所期望的,于是引入了多态的概念
4、多态、静态联编、动态联编
面向对象中期望的行为
根据实际的对象类型判断如何调用重写函数 ,父类指针(引用)指向父类对象则调用父类中定义的函数 ,指向子类对象则调用子类中定义的重写函数
面向对象中的多态的概念
根据实际的对象类型决定函数调用的具体目标 ,同样的调用语句在实际运行时有多种不同的表现形态
C++语言直接支持多态的概念
通过使用virtual关键字对多态进行支持 ,被virtual声明的函数被重写后具有多态特性 ,被virtual声明的函数叫做虚函数
#include <iostream>
using namespace std;
class Parent
{
public:
virtual void print()
{
cout << "I'm Parent." << endl;
}
};
class Child : public Parent
{
public:
void print()
{
cout << "I'm Child." << endl;
}
};
void how_to_print(Parent* p)
{
p->print(); // 展现多态的行为
}
int main()
{
Parent p;
Child c;
how_to_print(&p); // Expected to print: I'm Parent.
how_to_print(&c); // Expected to print: I'm Child.
return 0;
}
多态的意义
在程序运行过程中展现出动态的特性 ,函数重写必须多态实现,否则没有意义 (对比Java),多态是面向对象组件化程序设计的基础特性
静态联编与动态联编
静态联编
在程序的编译期间就能确定具体的函数调用 , 如:函数重载 :编译期间就能通过函数参数类型,个数判断具体函数调用
动态联编
在程序实际运行后才能确定具体的函数调用 ,如:函数重写 ,引入多态后,到运行后才能确定具体调用函数
#include <iostream>
using namespace std;
class Parent
{
public:
virtual void func()
{
cout << "void func()" << endl;
}
virtual void func(int i)
{
cout << "void func(int i) : " << i << endl;
}
virtual void func(int i, int j)
{
cout << "void func(int i, int j) : " << "(" << i << ", " << j << ")" << endl;
}
};
class Child : public Parent
{
public:
void func(int i, int j)
{
cout << "void func(int i, int j) : " << i + j << endl;
}
void func(int i, int j, int k)
{
cout << "void func(int i, int j, int k) : " << i + j + k << endl;
}
};
void run(Parent* p)
{
p->func(1, 2); // 展现多态的特性
// 动态联编
}
int main()
{
Parent p;
p.func(); // 静态联编
p.func(1); // 静态联编
p.func(1, 2); // 静态联编
cout << endl;
Child c;
c.func(1, 2); // 静态联编
cout << endl;
run(&p);
run(&c);
return 0;
}
多态原理:C++ 对象模型分析
江湖恩怨 test.cpp
#include <iostream>
using namespace std;
class Boss // 逍遥王
{
public:
int fight() // 必杀技
{
int ret = 10; // 伤害
cout << "Boss::fight() : " << ret << endl;
return ret;
}
};
class Master // 名剑山庄老庄主
{
public:
virtual int eightSwordKill() // 八剑齐飞---同时召唤8把名剑 (八个方向)
{
int ret = 8; // 8个伤害
cout << "Master::eightSwordKill() : " << ret << endl;
return ret;
}
};
class NewMaster : public Master // 名剑山庄少庄主 ---报仇
{
public:
int eightSwordKill() // 重写八剑齐飞
{
int ret = Master::eightSwordKill() * 2; // 在老庄主的基础上创新,左右互搏,伤害加倍
cout << "NewMaster::eightSwordKill() : " << ret << endl;
return ret;
}
};
void field_pk(Master* master, Boss* boss)
{
int k = master->eightSwordKill(); // 动态联编
int b = boss->fight(); // 静态联编
if( k < b )
{
cout << "Master is killed..." << endl;
}
else
{
cout << "Boss is killed..." << endl;
}
}
int main()
{
Master master;
Boss boss;
cout << "Master vs Boss" << endl;
field_pk(&master, &boss); // 老庄主弱于逍遥王,惨死
cout << "NewMaster vs Boss" << endl;
NewMaster newMaster;
field_pk(&newMaster, &boss); // 少庄主完胜
return 0;
}
5、C++中的动态类型
5.1、静态类型与动态类型
- 在面向对象中可能出现下面的情况
- 基类指针指向子类对象
- 基类引用成为子类对象的别名
- 静态类型 :变量或对象自身的类型 (期望的类型)
- 动态类型 :指针或引用所指向对象的实际类型
- 基类指针是否可以强制类型转换为子类指针取决于动态类型
// 指针b期望指向一个父类对象,但代码运行时b不一定指向父类对象,b指向的实际对象类型是不确定的
void test(Base* b)
{
/* 危险的转换方式 */
Derived* d = static_cast<Derived*>(b); // 指针b指向子类对象无问题,但若是父类对象就危险了
}
5.2、动态类型识别
5.2.1 利用多态获得动态类型
利用多态
- 在基类中定义虚函数返回具体的类型信息 ,所有的派生类都必须实现类型相关的虚函数
- 每个类中的类型虚函数都需要不同的实现
多态解决方案的缺陷
- 必须从基类开始提供类型虚函数,所有的派生类都必须重写类型虚函数
- 每个派生类的类型名必须唯—
#include <iostream>
using namespace std;
class Base
{
public:
virtual string type() { return "Base"; }
};
class Derived : public Base
{
public:
string type() { return "Derived"; }
void printf()
{
cout << "I'm a Derived." << endl;
}
};
class Child : public Base
{
public:
string type() { return "Child"; }
};
void test(Base* b)
{
/* 危险的转换方式 */
// Derived* d = static_cast<Derived*>(b);
if( b->type() == "Derived" )
{
Derived* d = static_cast<Derived*>(b);
d->printf();
}
// cout << dynamic_cast<Derived*>(b) << endl; // 使用dynamic_cast,仅仅得知转换是否成功,不能得到动态类型
}
int main(int argc, char *argv[])
{
Base b;
Derived d;
Child c;
test(&b);
test(&d);
test(&c);
return 0;
}
5.2.2 类型识别关键字
typeid
- C++提供了typeid操作符关键字用于获取类型信息
- typeid关键字返回对应参数的类型信息,为 type_info类对象 ,包含类型信息
- 当typeid的参数为NULL时将抛出异常
- 当参数为类型时:返回静态类型信息
- 当参数为变量时:
- 不存在虚函数表 ,返回静态类型信息
- 存在虚函数表 ,返回动态类型信息
#include <iostream>
#include <typeinfo>
using namespace std;
class Base
{
public:
virtual ~Base() { }
};
class Derived : public Base
{
public:
void printf()
{
cout << "I'm a Derived." << endl;
}
};
void test(Base* b)
{
// 有虚函数返回动态类型信息,若Base没有虚函数,b指向父类或子类结果一致都是输出父类类型信息
const type_info& tb = typeid(*b);
cout << tb.name() << endl; // 返回动态类型信息
cout << typeid (b).name() << endl << endl; // 返回静态类型信息, b就是一个指针变量,类型是指针
}
int main(int argc, char *argv[])
{
int i = 0;
const type_info& tiv = typeid(i);
const type_info& tii = typeid(int);
cout << (tiv == tii) << endl; // 1
Base b;
Derived d;
test(&b);
test(&d);
return 0;
}
- 不同编译器处理类型名字方式是不一样,所以在使用typeid不能做类型假设
- 关于type_info的定义:C++标准没有确切定义type_info,但必须实现
name(),before(),==,!=
,它与编译器相关
//Part of RTTI.
class type_info
{
protected:
const char *__name;
explicit type_info(const char *__n): __name(__n) { }
private:
// 隐藏拷贝构造和赋值操作符,创建type_info对象只能使用typeid操作符
type_info& operator=(const type_info&);
type_info(const type_info&);
public:
virtual ~type_info();
// 返回C风格的字符串,表示类型名
const char* name() const
{
return __name[0] == '*' ? __name + 1 : __name;
}
//bool before(const type_info& __arg) const;
//bool operator==(const type_info& __arg) const; // 比较的是__name
//bool operator!=(const type_info& __arg) const{ return !operator==(__arg); }
//http://gcc.gnu.org/onlinedocs/gcc-4.6.2/libstdc++/api/a01094_source.html
};
typeinfo与虚函数表的联系
#include <iostream>
#include <typeinfo>
using namespace std;
class Base
{
public:
virtual void print(){}
};
class Derived : public Base
{
public:
void print(){}
};
void test(Base* p)
{
p->print();
const type_info& info = typeid(*p);
printf("%p\n", &info);
return;
}
int main()
{
Base* b = new Derived();
test(b);
return 0;
}
typeof
- typeof 是GNU C编译器的特有关键字
- typeof 只在编译期生效,用于得到变量的类型
int main()
{
int i = 100;
typeof(i) j = i; // int j = i;
const typeof(i)* p = &j; // const int* p = &j;
printf("sizeof(j) = %d\n", sizeof(j));
printf("j = %d\n", j);
printf("*p = %d\n", *p);
return 0;
}
dynamic_cast
- dynamic_cast是与继承相关的类型转换关键字
- dynamic_cast要求相关的类中必须有虚函数
- 用于有直接或者间接继承关系的指针(引用)之间
- 指针:转换成功:得到目标类型的指针 ,转换失败:得到一个空指针
- 引用: 转换成功:得到目标类型的引用 ,转换失败:得到一个异常操作信息(std::bad_cast)
#include <iostream>
using namespace std;
class Base
{
public:
Base()
{
cout << "Base::Base()" << endl;
}
virtual ~Base()
{
cout << "Base::~Base()" << endl;
}
};
class Derived : public Base
{
};
int main()
{
Base* p = new Base;
Derived* pd = dynamic_cast<Derived*>(p); // 预计返回NULL
if( pd != NULL )
{
cout << "pd = " << pd << endl;
}
else
{
cout << "Cast error!" << endl;
}
Base* p1 = new Derived();
Derived* pd1 = dynamic_cast<Derived*>(p1); // 预计转换成功
if( pd1 != NULL )
{
cout << "pd1 = " << pd1 << endl;
}
else
{
cout << "Cast error!" << endl;
}
delete p;
delete p1;
return 0;
}
6、C++ 中的抽象类和接口
面向对象中的抽象类
可用于表示现实世界中的抽象概念,是一种只能定义类型,而不能产生对象的类 ,只能被继承并重写相关函数 ,直接特征是相关函数没有完整的实现
抽象类与纯虚函数
C++语言中没有抽象类的概念 ,C++中通过纯虚函数实现抽象类 ,纯虚函数是指只定义原型的成员函数 ,—个C++类中存在纯虚函数就成为了抽象类
抽象类只能用作父类被继承 ,子类必须实现纯虚函数的具体功能 ,纯虚函数被实现后成为虚函数 , 如果子类没有实现纯虚函数,则子类成为抽象类
#include <iostream>
using namespace std;
class Shape
{
public:
virtual double area() = 0; // "= 0"用于告诉编译器当前是声明纯虚函数,因此不需要定义函数体
};
class Rect : public Shape
{
int ma;
int mb;
public:
Rect(int a, int b)
{
ma = a;
mb = b;
}
double area()
{
return ma * mb;
}
};
class Circle : public Shape
{
int mr;
public:
Circle(int r)
{
mr = r;
}
double area()
{
return 3.14 * mr * mr;
}
};
void area(Shape* p) // 不能定义对象,可以定义指针
{
double r = p->area();
cout << "r = " << r << endl;
}
int main()
{
Rect rect(1, 2);
Circle circle(10);
area(&rect);
area(&circle);
return 0;
}
C++中的接口
类中没有定义任何的成员变量 ,所有的成员函数都是公有的 ,所有的成员函数都是纯虚函数 ,接口是—种特殊的抽象类 (Java中严格区分接口和抽象类)
#include <iostream>
using namespace std;
class Channel
{
public:
virtual bool open() = 0;
virtual void close() = 0;
virtual bool send(char* buf, int len) = 0;
virtual int receive(char* buf, int len) = 0;
};
int main()
{
return 0;
}
7、构造函数、析构函数与虚函数
构造函数是否可以成为虚函数? 析构函数是否可以成为虚函数?
构造函数不可能成为虚函数 ,在构造函数执行结束后,虚函数表指针才会被正确的初始化
析构函数可以成为虚函数 ,建议在设计类时将析构函数声明为虚函数
#include <iostream>
using namespace std;
class Base
{
public:
Base()
{
cout << "Base()" << endl;
}
~Base()
{
cout << "~Base()" << endl;
}
};
class Derived : public Base
{
public:
Derived()
{
cout << "Derived()" << endl;
}
~Derived()
{
cout << "~Derived()" << endl;
}
};
int main()
{
Base* p = new Derived(); // 创建子类对象
// ...
delete p; // 只会析构父类部分
return 0;
}
没调用子类的析构函数,编译器在delete p时只是单纯的根据指针的类型析构
我们需要的是根据p所指向实际对象判断如何调用析构函数(多态)
解决方案 : virtual ~Base()
构造函数中是否可以发生多态? 析构函数中是否可以发生多态?
构造函数中不可能发生多态行为 ,在构造函数执行时,虚函数表指针未被正确初始化
析构函数中不可能发生多态行为 ,在析构函数执行时,虚函数表指针已经被销毁
#include <iostream>
using namespace std;
class Base
{
public:
Base()
{
cout << "Base()" << endl;
func();
}
virtual void func()
{
cout << "Base::func()" << endl;
}
virtual ~Base()
{
func();
cout << "~Base()" << endl;
}
};
class Derived : public Base
{
public:
Derived()
{
cout << "Derived()" << endl;
func();
}
virtual void func()
{
cout << "Derived::func()" << endl;
}
~Derived()
{
func();
cout << "~Derived()" << endl;
}
};
int main()
{
Base* p = new Derived();
// ...
delete p;
return 0;
}
构造函数和析构函数中不能发生多态行为, 只调用当前类中定义的函数版本!!
8、小结
子类可以定义父类中的同名成员 ,子类中的成员将隐藏父类中的同名成员 ,子类和父类中的函数不能构成重载关系
子类可以定义父类中完全相同的成员函数 ,使用作用域分辨符访问父类中的同名成员,
子类对象可以当作父类对象使用(赋值兼容) ,父类指针可以正确的指向子类对象 , 父类引用可以正确的代表子类对象
子类中可以重写父类中的成员函数,函数重写只可能发生在父类与子类之间 ,根据实际对象的类型确定调用的具体函数
virtual关键字是C++中支持多态的唯—方式 ,被重写的虚函数可表现出多态的特性
virtual就是告诉编译当调用虚函数时统一按间接调用的方式生成汇编代码
构造函数不能成为虚函数 ,析构函数可以成为虚函数
构造函数和析构函数中都无法产生多态行为
C++中没有抽象类的概念 ,C++中通过纯虚函数实现抽象类 ,类中只存在纯虚函数的时成为接口