继承是面向对象的特性之一,可减少重复代码。
1、继承的基本语法
C++ 继承基本语法
语法: class 子类: 继承方式 父类
父类也称作基类,子类也称作派生类
派生类(子类)成员包含两大部分:
一类是从基类(父类)继承过来的,一类是自己增加的成员。从基类继承过来的表现其共性,而新增的成员体现其个性。
#include <iostream>
using namespace std;
//基类(父类)
class Base
{
public:
void printBase()
{
cout << "父类成员函数" << endl;
}
};
//派生类(子类)
class Sub : public Base
{
public:
void printSub()
{
cout << "子类成员函数" << endl;
}
};
int main()
{
//类和对象 - 继承 - 基本语法
Sub s1;
s1.printSub();
s1.printBase();
system("pause");
return 0;
}
输出结果
子类成员函数
父类成员函数
Go语言 继承
Go语言中没有class,继承的实现是通过在struct结构体内置匿名成员来实现的。
package main
import "fmt"
//基类(父类)
type Base struct {
}
func (base *Base) printBase() {
fmt.Println("父类成员函数")
}
//派生类(子类)
type Sub struct {
Base //内置匿名父类结构体
}
func (sub *Sub) printSub() {
fmt.Println("子类成员函数")
}
func main() {
sub := Sub{}
sub.printSub()
sub.printBase()
}
输出结果
子类成员函数
父类成员函数
2、继承方式
C++ 继承方式
三种继承方式:
- public 公共继承:子类继承父类成员时,不能访问父类私有成员,父类中public成员继承后还是public,父类中protected成员继承后还是protected
- protected 保护继承:子类继承父类成员时,不能访问父类私有成员,父类中public成员继承后变成protected,父类中protected成员继承后还是protected
- private 私有继承:子类继承父类成员时,不能访问父类私有成员,父类中public成员与protected成员继承后变成private成员
(1)公共继承(public)
#include <iostream>
using namespace std;
class Base
{
public:
int a;
protected:
int b;
private:
int c;
};
//公有继承
class Sub : public Base
{
public:
void setVal()
{
a = 1;
b = 2;
//c = 3; //子类中不能访问父类的私有成员,报错:成员Base::c不可访问
}
};
int main()
{
//类和对象 - 继承方式 - 公有继承
//public 公共继承:子类继承父类成员时,不能访问父类私有成员,父类中public成员继承后还是public,父类中protected成员继承后还是protected
Sub s1;
s1.a;
//s1.b; //子类对象不能访问父类的保护成员,报错:成员Base::b不可访问
system("pause");
return 0;
}
(2)保护继承(protected)
#include <iostream>
using namespace std;
class Base
{
public:
int a;
protected:
int b;
private:
int c;
};
//保护继承
class Sub : protected Base
{
public:
void setVal()
{
a = 1; //在子类中,已变为保护成员,子类能够访问,子类对象不能访问
b = 2;
//c = 3; //子类中不能访问父类的私有成员,报错:成员Base::c不可访问
}
};
//继承子类
class Grand : public Sub
{
public:
void setVal()
{
a = 3; //子类中可以访问父类的保护成员
b = 5; //子类中可以访问父类的保护成员
}
};
int main()
{
//类和对象 - 继承方式 - 保护继承
//protected 保护继承:子类继承父类成员时,不能访问父类私有成员,父类中public成员继承后变成protected,父类中protected成员继承后还是protected
Sub s1;
//s1.a; //保护继承后,子类对象不能访问父类成员中的公有成员,因为在子类继承中已变成protected,报错:成员Base::a不可访问
//s1.b; //对象不能访问保护成员,报错:成员Base::b不可访问
Grand g1;
//g1.a; //对象不能访问保护成员,报错:成员Base::b不可访问
//g1.b; //对象不能访问保护成员,报错:成员Base::b不可访问
system("pause");
return 0;
}
(3)私有继承(private)
#include <iostream>
using namespace std;
class Base
{
public:
int a;
protected:
int b;
private:
int c;
};
//私有继承
class Sub : private Base
{
public:
void setVal()
{
a = 1; //在子类中,已变为私有成员,不能子类访问
b = 2; //在子类中,已变为私有成员,不能子类访问
//c = 3; //子类中不能访问父类的私有成员,报错:成员Base::c不可访问
}
};
//继承子类
class Grand : public Sub
{
public:
void setVal()
{
//a = 1; //子类中不能访问父类的私有成员,报错:成员Base::a不可访问
}
};
int main()
{
//类和对象 - 继承方式 - 私有继承
//private 私有继承:子类继承父类成员时,不能访问父类私有成员,父类中public成员与protected成员继承后变成private成员
Sub s1;
//s1.a; //私有继承后,子类对象不能访问父类成员中的公有成员,因为在子类继承中已变成private,报错:成员Base::a不可访问
//s1.b; //私有继承后,子类对象不能访问父类的保护成员,因为在子类继承中已变成private,报错:成员Base::b不可访问
system("pause");
return 0;
}
Go语言 继承方式
Go语言中,没有像C++有public、protected、private的修饰访问权限,是通过首字母大小写来确定访问权限的,方法名、常量、变量名、结构体名等,同包内首字母大小写都可访问(理解为公有的),不同包时只有首字母大写才能被访问(理解为公有的),不同包内首字母小写不能被访问(理解为私有的)。
在base包中定义一个父类结构体
package base
//基类(父类) - 结构体名首字母大写
type Base2 struct {
Name string //首字母大写
age int //首字母小写
}
//结构体名首字母小写
type base3 struct {
Name string //首字母大写
age int //首字母小写
}
在main包中继承base包中的父类结构体
package main
import (
"fmt"
base "testProject/CPlus/2-类/继承方式"
)
//基类(父类)
type Base struct {
Name string //
age int //
}
//派生类(子类) - 继承同包中的父类
type Sub struct {
Base //内置匿名父类结构体(同包内)
}
//派生类(子类) - 继承不同包中的父类 - 结构体名首字母大写
type Sub2 struct {
base.Base2 //内置匿名父类结构体(不同包)
}
//派生类(子类) - 继承不同包中的父类 - 结构体名首字母小写
type Sub3 struct {
// base.base3 //不能访问不同包下首字母小写的结构体,error: Unexported type "base3" usage
}
func main() {
fmt.Println("---- 子类继承同包中的父类 ----")
sub := Sub{}
sub.Name = "Hello"
sub.age = 2
fmt.Printf("Name = %s, age = %d\n", sub.Name, sub.age)
fmt.Println("---- 子类继承不同包中的父类 ----")
sub2 := Sub2{}
sub2.Name = "World"
//sub2.age = 23 //error: Unexported field "age" usage
fmt.Printf("Name = %s, 不能访问小写字母开头的属性 age\n", sub2.Name)
}
---- 子类继承同包中的父类 ----
Name = Hello, age = 2
---- 子类继承不同包中的父类 ----
Name = World, 不能访问小写字母开头的属性 age
3、对象模型
C++ 内存对象模型
#include <iostream>
using namespace std;
class Base
{
public:
int a;
protected:
int b;
private:
int c;
};
class Sub : public Base
{
public:
int d;
};
int main()
{
//类和对象 - 继承 - 对象模型
cout << "子类占用空间大小:" << sizeof(Sub) << endl;
system("pause");
return 0;
}
输出结果
子类占用空间大小:16
查看内存对象模型
命令: cl /d1 reportSingleClassLayout类名 "文件名"
reportSingleClassLayout后面连接的是要查看的类名
注意:/d1 此处是数字1,不是小写字母l
F:\CPlus\>cl /d1 reportSingleClassLayoutSub "rs.cpp"
用于 x86 的 Microsoft (R) C/C++ 优化编译器 19.32.31332 版
版权所有(C) Microsoft Corporation。保留所有权利。rs.cpp
class Sub size(16):
+---
0 | +--- (base class Base)
0 | | a
4 | | b
8 | | c
| +---
12 | d
+---
E:\Program\VisualStudio\VC\Tools\MSVC\14.32.31326\include\ostream(301): warning C4530: 使用了 C++ 异常处理程序,但未启用展开语义。请指定 /EHsc
E:\Program\VisualStudio\VC\Tools\MSVC\14.32.31326\include\ostream(294): note: 在编译 类 模板 成员函数“std::basic_ostream<char,std::char_traits<char>> &std::basic_ostream<char,std::char_traits<char>>::operator <<(unsigned int)”时
rs.cpp(23): note: 查看对正在编译的函数 模板 实例化“std::basic_ostream<char,std::char_traits<char>> &std::basic_ostream<char,std::char_traits<char>>::operator <<(unsigned int)”的引用
rs.cpp(23): note: 查看对正在编译的 类 模板 实例化“std::basic_ostream<char,std::char_traits<char>>”的引用
Microsoft (R) Incremental Linker Version 14.32.31332.0
Copyright (C) Microsoft Corporation. All rights reserved./out:rs.exe
rs.obj
Go语言 子类占用空间大小
package main
import (
"fmt"
"unsafe"
)
//基类(父类)
type Base struct {
a int32
b int32
c int32
}
//派生类(子类)
type Sub struct {
Base
d int32
}
func (s *Sub) print() {
fmt.Println(s)
}
func main() {
sub := Sub{}
fmt.Printf("子类占用空间大小 : %d\n", unsafe.Sizeof(sub))
}
输出结果
子类占用空间大小 : 16
4、C++ 继承中构造和析构顺序
- 构造函数顺序:父类先构造,子类再构造
- 析构函数顺序:子类先析构,父类再析构;与构造函数顺序相反
#include <iostream>
using namespace std;
class Base
{
public:
Base()
{
cout << "父类构造函数" << endl;
}
~Base()
{
cout << "父类析构函数" << endl;
}
};
class Sub : public Base
{
public:
Sub()
{
cout << "子类构造函数~" << endl;
}
~Sub()
{
cout << "子类析构函数~" << endl;
}
};
void test()
{
Sub s1;
}
int main()
{
//类和对象 - 继承 - 构造函数与析构函数顺序
test();
system("pause");
return 0;
}
父类构造函数
子类构造函数~
子类析构函数~
父类析构函数
5、继承同名成员处理方式
子类与父类出现同名的成员时:
- 访问子类同名成员,直接访问即可
- 访问父类同名成员,需要加作用域
C++ 继承同名成员处理方式
子类中出现和父类同名的成员函数,子类的同名成员会隐藏掉父类中所有的同名成员函数(成员函数重载时会有多个同名函数),如果子类想访问父类中被隐藏掉的同名成员函数,需要加作用域。
#include <iostream>
using namespace std;
class Base
{
public:
int a;
Base() {
a = 3;
}
void f1()
{
cout << "父类中同名成员函数f1()" << endl;
}
void f1(int _a)
{
cout << "父类中同名成员函数f1(int), 参数值 = " << _a << endl;
}
};
class Sub : public Base
{
public:
int a; //成员属性与父类同名
Sub()
{
a = 5;
}
void f1() //成员函数与父类同名
{
cout << "子类中同名成员函数f1()" << endl;
}
};
int main()
{
//类和对象 - 继承 - 继承同名成员处理方式
/*
访问子类同名成员,直接访问即可
访问父类同名成员,需要加作用域
*/
Sub s1;
cout << "访问子类同名成员属性, a = " << s1.a << endl;
cout << "访问父类同名成员属性, a = " << s1.Base::a << endl;
s1.f1();
s1.Base::f1();
s1.Base::f1(8);
system("pause");
return 0;
}
输出结果
访问子类同名成员属性, a = 5
访问父类同名成员属性, a = 3
子类中同名成员函数f1()
父类中同名成员函数f1()
父类中同名成员函数f1(int), 参数值 = 8
Go语言 继承同名成员处理方式
注:Go语言中没有重载函数
package main
import (
"fmt"
)
//基类(父类)
type Base struct {
a int
}
func (base *Base) print() {
fmt.Println("父类同名函数中,父类同名属性 a = ", base.a)
}
//派生类(子类)
type Sub struct {
Base //内置匿名父类结构体
a int //成员属性与父类同名
}
//成员函数与父类同名
func (sub *Sub) print() {
fmt.Println("子类同名函数中,子类同名属性 a = ", sub.a)
}
func main() {
fmt.Println("---- 子类成员与父类同名 ----")
sub := Sub{}
sub.a = 3
sub.print()
sub.Base.a = 5 //访问父类成员属性
sub.Base.print() //访问父类成员函数
}
输出结果
---- 子类成员与父类同名 ----
子类同名函数中,子类同名属性 a = 3
父类同名函数中,父类同名属性 a = 5
6、继承中静态成员处理方式
C++ 继承中静态成员
继承中同名的静态成员在子类对象中的访问方式,与非静态成员访问方式一样:
- 访问子类同名成员,直接访问即可
- 访问父类同名成员,需要加作用域
可通过对象访问,也可通过类名访问
#include <iostream>
using namespace std;
class Base
{
public:
static int a;
static void f1()
{
cout << "父类中同名成员函数f1()" << endl;
}
static void f1(int _a)
{
cout << "父类中同名成员函数f1(int),参数值 = " << _a << endl;
}
};
int Base::a = 5;
class Sub : public Base
{
public:
static int a; //成员属性与父类同名
static void f1() //成员函数与父类同名
{
cout << "子类中同名成员函数f1()" << endl;
}
};
int Sub::a = 3;
int main()
{
//类和对象 - 继承 - 继承同名成员处理方式 - 静态成员
/*
访问子类同名成员,直接访问即可
访问父类同名成员,需要加作用域
*/
Sub s1;
cout << "--- 通过对象访问 ---" << endl;
cout << "访问子类同名成员属性, a = " << s1.a << endl;
cout << "访问父类同名成员属性, a = " << s1.Base::a << endl;
s1.f1();
s1.Base::f1();
s1.Base::f1(8);
cout << endl << "--- 通过类名访问 ---" << endl;
cout << "访问子类同名成员属性, a = " << Sub::a << endl;
cout << "访问父类同名成员属性, a = " << Sub::Base::a << endl;
Sub::f1();
Sub::Base::f1();
Sub::Base::f1(12);
system("pause");
return 0;
}
输出结果
--- 通过对象访问 ---
访问子类同名成员属性, a = 3
访问父类同名成员属性, a = 5
子类中同名成员函数f1()
父类中同名成员函数f1()
父类中同名成员函数f1(int),参数值 = 8--- 通过类名访问 ---
访问子类同名成员属性, a = 3
访问父类同名成员属性, a = 5
子类中同名成员函数f1()
父类中同名成员函数f1()
父类中同名成员函数f1(int),参数值 = 12
Go语言中没有静态成员
7、多继承
C++ 多继承
C++ 允许一个类继承多个类,多继承可能会引发父类中有同名成员出现,需要加作用域区分。
语法: class 子类: 继承方式 父类1, 继承方式 父类2 ......
#include <iostream>
using namespace std;
//父类
class Base1
{
public:
int a;
int b;
Base1()
{
a = 11;
b = 12;
}
};
//父类
class Base2
{
public:
int a; //同名成员属性
int c;
Base2()
{
a = 21;
c = 22;
}
};
//子类 - 多继承
class Sub : public Base1, public Base2
{
public:
int d;
int e;
Sub()
{
d = 31;
e = 32;
}
};
int main()
{
//类和对象 - 继承 - 多继承
Sub s1;
cout << "访问子类自己的成员属性: d = " << s1.d << endl;
cout << "访问父类Base1的成员属性: b = " << s1.Base1::b << endl;
cout << "访问父类Base2的成员属性: c = " << s1.Base2::c << endl;
cout << "访问父类Base1的同名成员属性: a = " << s1.Base1::a << endl;
cout << "访问父类Base2的同名成员属性: a = " << s1.Base2::a << endl;
system("pause");
return 0;
}
输出结果
访问子类自己的成员属性: d = 31
访问父类Base1的成员属性: b = 12
访问父类Base2的成员属性: c = 22
访问父类Base1的同名成员属性: a = 11
访问父类Base2的同名成员属性: a = 21
Go语言多继承
Go语言可以通过匿名组合方式实现多继承(组合继承)。
package main
//基类(父类)
type Base1 struct {
a int
b int
}
//基类(父类)
type Base2 struct {
a int //同名成员属性
c int
}
//派生类(子类)
type Sub struct {
Base1 //内置匿名父类结构体
Base2
d int
}
func main() {
sub := Sub{}
sub.d = 31 //访问子类自己的成员属性
sub.b = 12 //访问父类Base1的成员属性
sub.c = 22 //访问父类Base2的成员属性
sub.Base1.a = 11 //访问父类Base1的同名成员属性
sub.Base2.a = 21 //访问父类Base2的同名成员属性
}
8、菱形继承
两个派生类继承同一个基类,又有某类同时继承了两个派生类,这种继承被称为菱形继承,或钻石继承。如下图所示,像个菱形一样的继承:
举个栗子,如下图所示:
C++ 菱形继承
(1)菱形继承
#include <iostream>
using namespace std;
//基类 - 动物
class Animal
{
public:
int age;
};
//派生类 - 羊 (继承动物类)
class Sheep : public Animal
{
};
//派生类 - 骆驼 (继承动物类)
class Camel : public Animal
{
};
//派生类 - 羊驼 (继承羊类 与 骆驼类)
class Alpaca : public Sheep, public Camel
{
};
int main()
{
//类和对象 - 菱形继承
Alpaca alpaca;
alpaca.Sheep::age = 18;
alpaca.Camel::age = 20;
//在菱形继承中,两个父类拥有相同的数据,需要加以作用域区分
//在实际中,羊驼的年龄记录一个就够了,不需要两份数据,导致数据不一致,也导致资源浪费
cout << "在父类Sheep中,羊驼的年龄 age = " << alpaca.Sheep::age << endl;
cout << "在父类Camel中,羊驼的年龄 age = " << alpaca.Camel::age << endl;
system("pause");
return 0;
}
输出结果
在父类Sheep中,羊驼的年龄 age = 18
在父类Camel中,羊驼的年龄 age = 20
(2)抛出问题
在实际中,羊驼的年龄记录一个就够了,不需要两份数据,导致数据不一致,也导致资源浪费。
查看内存对象模型命令:cl /d1 reportSingleClassLayoutAlpaca "文件名.cpp"
(3)解决问题(virtual)
利用虚继承解决菱形继承问题。
虚继承:在派生类前加上关键字 virtual
由于派生类为虚继承,所以基类变为虚基类
#include <iostream>
using namespace std;
//基类 - 动物 - 由于派生类为虚继承,所以基类变为虚基类
class Animal
{
public:
int age;
};
//派生类 - 羊 (继承动物类) - 虚继承
class Sheep : virtual public Animal
{
};
//派生类 - 骆驼 (继承动物类) - 虚继承
class Camel : virtual public Animal
{
};
//派生类 - 羊驼 (继承羊类 与 骆驼类)
class Alpaca : public Sheep, public Camel
{
};
int main()
{
//类和对象 - 解决菱形继承问题 - 虚继承
//菱形继承问题:在实际中,羊驼的年龄记录一个就够了,不需要两份数据,导致数据不一致,也导致资源浪费
Alpaca alpaca;
alpaca.Sheep::age = 18;
alpaca.Camel::age = 20;
cout << "在父类Sheep中,羊驼的年龄 age = " << alpaca.Sheep::age << endl;
cout << "在父类Camel中,羊驼的年龄 age = " << alpaca.Camel::age << endl;
cout << "在自己类Alpaca中,羊驼的年龄 age = " << alpaca.age << endl;
system("pause");
return 0;
}
输出结果
在父类Sheep中,羊驼的年龄 age = 20
在父类Camel中,羊驼的年龄 age = 20
在自己类Alpaca中,羊驼的年龄 age = 20
查看内存对象模型
命令: cl /d1 reportSingleClassLayoutAlpaca "文件名.cpp"
vbptr 虚基类指针,v - virtual, b - base, ptr - pointer
vbptr指向一个vbtable 虚基类表
Go语言 菱形继承
(1)菱形继承
package main
import "fmt"
//基类 - 动物
type Animal struct {
age int
}
//派生类 - 羊 (继承动物类)
type Sheep struct {
Animal
}
//派生类 - 骆驼 (继承动物类)
type Camel struct {
Animal
}
//派生类 - 羊驼 (继承羊类 与 骆驼类)
type Alpaca struct {
Sheep
Camel
}
func main() {
//类和对象 - 菱形继承
alpaca := Alpaca{}
alpaca.Sheep.age = 18
alpaca.Camel.age = 20
fmt.Println("在父类Sheep中,羊驼的年龄 age = ", alpaca.Sheep.age)
fmt.Println("在父类Camel中,羊驼的年龄 age = ", alpaca.Camel.age)
}
输出结果
在父类Sheep中,羊驼的年龄 age = 18
在父类Camel中,羊驼的年龄 age = 20
(2)抛出问题
同C++中抛出的问题一样:在实际中,羊驼的年龄记录一个就够了,不需要两份数据,导致数据不一致,也导致资源浪费。
(3)解决问题(interface)
Go语言中没有virtual来表示虚继承,可以通过 interface + struct 来实现虚基类的用法,需要实现interface中定义的方法。
package main
import "fmt"
//接囗(虚基类) - 动物
type IAnimal interface
{
setAge(age int)
getAge() int
}
//接囗实现 - 动物
type AnimalImpl struct {
age int
}
func (m *AnimalImpl) setAge(age int) {
m.age = age
}
func (m *AnimalImpl) getAge() int {
return m.age
}
//派生类 - 羊 (继承接囗)
type Sheep struct {
IAnimal
}
//派生类 - 骆驼 (继承接囗)
type Camel struct {
IAnimal
}
//派生类 - 羊驼 (继承羊类 与 骆驼类)
type Alpaca struct {
Sheep
Camel
}
func main() {
//实现IAnimal接囗动物变量
var animalImpl = AnimalImpl{}
//羊驼变量
var alpaca = Alpaca{
Sheep{&animalImpl},
Camel{&animalImpl},
}
alpaca.Sheep.setAge(18)
alpaca.Camel.setAge(20)
fmt.Println("在父类Sheep中,羊驼的年龄 age = ", alpaca.Sheep.getAge())
fmt.Println("在父类Camel中,羊驼的年龄 age = ", alpaca.Camel.getAge())
}
输出结果
在父类Sheep中,羊驼的年龄 age = 20
在父类Camel中,羊驼的年龄 age = 20