从0开撸C++系列
往期地址:
本期主题:
c++中的继承与多态特性
文章目录
1.继承体系下的构造/析构函数关系
我们知道继承指的是子类从父类继承对应的成员变量/函数方法,那么父类中的构造与析构函数,是否会被子类所继承呢?
class 派生类名: 访问控制 基类名1,访问控制 基类名2
看一个实际的例子。
基类:person
派生类:man
//person.h
class person
{
public:
string name;
person(); //添加父类的构造函数
~person(); //添加父类的析构函数
private:
protected:
};
//man.h
class man: public person
{
public:
void coding(void)
{cout << "name :" << this->name << " here is man coding" << endl;};
man(); //添加子类的构造函数
~man(); //添加子类的析构函数
private:
protected:
};
//main.cpp
int main(void)
{
man jason;
return 0;
}
gary@ubuntu:~/workspaces/cpp_study/2.3.constructor$ ./app
here is person::person()
here is man::man()
here is man::~man()
here is person::~person()
gary@ubuntu:~/workspaces/cpp_study/2.3.constructor$
由此可以看出,调用的顺序是 调用父类的构造->调用子类的构造->调用子类的析构->调用父类的析构
2.子类和父类的类型兼容
派生类从基类继承而来,因此派生类和基类其实是有一定的关系的,这个关系可以被称为类型兼容规则
,简单概括一下:
- 派生类的对象可以隐含转换为基类对象
- 派生类的指针可以隐含转换为基类的指针
2.1 派生类与基类的对象转换
int main(void)
{
person person_gen; //定义派生类对象
man jason; //定义基类对象
person_gen = jason; //这里将派生类的对象赋值给基类的对象,按照一般理解此时person_gen已经变成了派生类的内容
person_gen.print_age(); //调用发现,仍然是基类的方法
return 0;
}
gary@ubuntu:~/workspaces/cpp_study/2.3.constructor$ ./app
here is person::person()
here is person::person()
here is man::man()
person::print_age: 10
here is man::~man()
here is person::~person()
here is person::~person()
gary@ubuntu:~/workspaces/cpp_study/2.3.constructor$
2.2 派生类与基类的指针转换
int main(void)
{
man *jason = new man(); //定义派生类的指针
person *p_gen = jason; //定义基类的指针,并且将派生类的指针赋值给这个指针
p_gen->print_age(); //调用发现,仍然是基类的方法
delete jason;
return 0;
}
gary@ubuntu:~/workspaces/cpp_study/2.3.constructor$ ./app
here is person::person()
here is man::man()
person::print_age: 10
here is man::~man()
here is person::~person()
gary@ubuntu:~/workspaces/cpp_study/2.3.constructor$
3.多继承
多继承就是一个子类具有多个父类
class A: xxx B,xxx C, xxx代表访问控制,即可以是public/private/protected
//test.h
class A
{
public:
void helloA(void);
};
class B
{
public:
void helloB(void);
};
class C: public A, public B //c类继承于A类和B类
{
public:
};
//test.cpp
void A::helloA()
{
cout << "hello A" << endl;
}
void B::helloB()
{
cout << "hello B" << endl;
}
int main(void)
{
C testc;
testc.helloA(); //调用继承自A的成员方法
testc.helloB(); //调用继承自B的成员方法
return 0;
}
gary@ubuntu:~/workspaces/cpp_study/2.4.mulextend$ ./app
hello A
hello B
3.1 多继承的二义性问题
当C多继承自A和B时,当C去调用A和B中的同名成员
时,即会有二义性问题,编译器并没有办法确认C想调用A和B中的哪个成员;
上面那个例子中,如果我们将A和B的成员方法同名,看会发生什么问题
gary@ubuntu:~/workspaces/cpp_study/2.4.mulextend$ make
g++ test.cpp -o app
test.cpp: In function ‘int main()’:
test.cpp:20:11: error: request for member ‘hello’ is ambiguous //提示hello是含糊的,定义不清
testc.hello();
^
test.cpp:12:6: note: candidates are: void B::hello()
void B::hello()
^
test.cpp:6:6: note: void A::hello()
void A::hello()
^
3.2 利用namespce指定调用
如果在调用时,指定了具体的父类,则不会有问题
子类对象.父类::方法
int main(void)
{
C testc;
testc.A::hello(); //这样指定父类的方法
return 0;
}
gary@ubuntu:~/workspaces/cpp_study/2.4.mulextend$ ./app
hello A
3.3 用虚继承来解决二义性问题
在代码中,存在这样一种情况,从一个父类A,继承了两个子类B1和B2,类型C又多继承了B1和B2,这样看起来就是一个菱形的结构,这种情况我们称为菱形继承
,在这种情况下,如果子类C去调用A类中原有的方法就会报错
//test.h
class gen //基类
{
public:
void set(void);
};
class A: public gen
{
public:
void hello(void);
};
class B: public gen
{
public:
void hello(void);
};
class C: public A, public B
{
public:
};
//test.cpp
int main(void)
{
C testc;
testc.set(); //调用共同的方法,会报错
return 0;
}
gary@ubuntu:~/workspaces/cpp_study/2.4.mulextend$ make
g++ test.cpp -o app
test.cpp: In function ‘int main()’:
test.cpp:24:11: error: request for member ‘set’ is ambiguous
testc.set();
^
test.cpp:6:6: note: candidates are: void gen::set()
void gen::set()
^
test.cpp:6:6: note: void gen::set()
Makefile:2: recipe for target 'all' failed
make: *** [all] Error 1
可以用虚继承来解决这个问题,注意:这里的虚继承和虚函数并没有任何关系,(就有点像雷锋和雷峰塔的关系,除了名字相近,实际上并没有联系)
//在继承时加上 virtual即可解决这个问题
//test.h
class gen //基类
{
public:
void set(void);
};
class A: virtual public gen //添加上virtual,代表虚继承
{
public:
void hello(void);
};
class B: public gen //添加上virtual,代表虚继承
{
public:
void hello(void);
};
4.多态
面向对象编程的三大特征,封装、继承和多态
4.1 多态的概念
多态按字面的意思就是多种形态。当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。
C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。
- 在基类中声明方法为virtual,代表其为虚函数
- 在派生类中重新实现同名方法,以实现多态,这就是override(重写、覆盖)
//test.h
class gen
{
public:
void set(void);
virtual void hello(void); //这里用virtual声明为虚函数
};
class A: public gen
{
public:
void hello(void);
};
class B: public gen
{
public:
void hello(void);
};
//test.cpp
int main(void)
{
gen *testgen = new gen();
testgen->hello(); //调用基类的方法
A *testa = new A(); //新建派生类指针
testgen = testa; //将派生类指针赋值给基类指针,这句可以理解为多态特性
testgen->hello(); //如果没有virtual,实际上打印还是发现是基类的成员方法,如果有virtual,就会实现多态的特性,实现派生类的方法
//B testb;
//testb.hello();
return 0;
}
//基类的方法中未加virtual的结果
gary@ubuntu:~/workspaces/cpp_study/2.4.mulextend$ ./app
hello gen
hello gen
//加上virtual的结果
gary@ubuntu:~/workspaces/cpp_study/2.4.mulextend$ ./app
hello gen
hello A
4.2 纯虚函数
前面用virtual表达的是虚函数,其实还有一种称为纯虚函数,纯虚函数就是在基类中只有原型而没有实体的一种虚函数
virtual xxx = 0 // =0 定义了是个纯虚函数
纯虚函数的特点在于:
- 纯虚函数所在的类
无法实例化对象
- 纯虚函数并不占用内存
//当想用纯虚函数的类来实例化对象时:
//test.h
class gen
{
public:
void set(void);
virtual void hello(void) = 0; //纯虚函数
};
//test.cpp
int main(void)
{
gen testgen;
return 0;
}
gary@ubuntu:~/workspaces/cpp_study/2.4.mulextend$ make
g++ test.cpp -o app
test.cpp: In function ‘int main()’:
test.cpp:35:9: error: cannot declare variable ‘testgen’ to be of abstract type ‘gen’
gen testgen;
4.3 抽象类
带有纯虚函数的类被称为抽象类,抽象类只能作为基类来派生新类,不可实例化对象。
抽象类的意义:
- 由于派生类必须实现基类的纯虚函数后,才能去实例化对象,因此
基类就规定了派生类的行为
; - 具有接口的概念,定义了一套访问的规则,像java语言中的 interface关键词;
4.4 虚析构函数
- 1.定义:
在析构函数前加virtual,则析构函数变成虚析构函数; - 2.为什么需要虚析构函数:
观察父类/子类的析构函数调用顺序,前面提到父类和子类的构造、析构函数的 调用的顺序是 调用父类的构造->调用子类的构造->调用子类的析构->调用父类的析构
//test.h
class gen
{
public:
void set(void);
virtual void hello(void) = 0;
virtual ~gen();//虚析构函数
};
class A: public gen
{
public:
void hello(void);
~A();
};
//test.cpp
gen::~gen() //父类析构
{
cout << "~gen" << endl;
}
A::~A() //子类析构
{
cout << "~A" << endl;
}
int main(void)
{
//1.写法1,分配在栈上
gen *testgen;
A testa; //分配在栈上,符合预期
testgen = &testa;
//2.写法2,分配在堆上
gen *testgen = new A();
delete testgen;
return 0;
}
//写法1
gary@ubuntu:~/workspaces/cpp_study/2.4.mulextend$ ./app
~A
~gen
//写法2
gary@ubuntu:~/workspaces/cpp_study/2.4.mulextend$ ./app
~gen
能看出上面的结果,当分配在栈上时,调用顺序符合预期,而分配在堆上时,却与预期不一致;
加上在析构函数前加上virtual时,分配在堆上的代码在析构时也符合预期;
virtual虚函数的意义:
让成员函数在运行时动态解析和绑定具体执行的函数
;