C++进阶语法
继承
- 继承是为了解决,多个不同类 但都具有共同属性 而出现重复编写 这些属性 所导致的 冗余而设计的语法
- 基类用于放公共属性,派生类放自己单独的属性
- 类的访问限定符
- public(公有):任何作用域代码 都可以访问 此关键字修饰的
类成员,允许该类和其子孙类域下直接访问即可,非子孙类域需子孙类域::/祖先类域::/祖先对象./子孙对象.的方式访问 - private(私有):只有类域本身下的代码 才能访问 此关键字修饰的
类成员 - protected(保护):只有类域本身和其子孙类域下的代码 才能访问并且可以直接访问 此关键字修饰的
类成员
- public(公有):任何作用域代码 都可以访问 此关键字修饰的
- 子孙类对象在实例化开辟成员变量空间时,会单独给该子孙类对象 开辟出该子孙的 成员变量以及祖先的成员变量 对应的空间
继承中 基类和派生类 关系逻辑图
python
_____________________ ____________________ ____________________
|class B1(A of Child)| |class B2(A of Child)| |class B3(A of Child)|
| | | | | |
|____________________| |____________________| |____________________|
____|_________________________|_______________________|___________
|class A |
| |
|__________________________________________________________________|
____|_______________ _____|______________ _____|______________
|class B4(A of Child)| |class B5(A of Child)| |class B6(A of Child)|
| | | | | |
|____________________| |____________________| |____________________|
继承代码演示
访问祖先类的相对权限,祖先的非子孙是按照自己的访问限定符来修饰自己的代码,但是对于自己子孙则按照
Min(继承方式,祖先类访问限定符)的方式来修饰自己的代码
cpp
class Animal
{
public:
Animal()
{
std::cout<<"Animal"<<std::endl;
}
};
class Animal:public/*继承方式*/ Cat
{
public:
Cat()
{
std::cout<<"Cat"<<std::endl;
}
};
class Animal:public/*继承方式*/ Dog
{
public:
Dog()
{
std::cout<<"Dog"<<std::endl;
}
};
基类和派生类对象赋值转换
- 派生类对象可以给基类对象赋值,但是基类对象不可以给派生类对象赋值
- 这里派生类对象给基类对象赋值 不涉及隐式类型转换所以不会产生临时对象,涉及新的概念 叫
is-a和 切割,因为子类是is-a的所以切割出 基类的部分 赋值给 基类对象- 子类对象里基类的部分 给基类对象 赋值时,是会走 该基类对应的 拷贝构造 和 赋值重载的
- 派生类对象给基类对象赋值时 不涉及 隐式类型转换的动作也叫做 赋值兼容
is-a定义:每个子类对象都是一个特殊的父类对象,派生类只看其继承部分就相当于其是一个特殊的基类- 切割定义:切割是 基类和派生类对象赋值转换 中的定义,根据
is-a去将 派生类对象 的 派生类继承的部分 和 派生类单独的部分 分割开来的使用
- 这里派生类对象给基类对象赋值 不涉及隐式类型转换所以不会产生临时对象,涉及新的概念 叫
- 祖先类指针指向子孙类对象地址,祖先指针只会指向 子孙对象里 祖先部分的开头地址,该指针也只会访问 子孙类对象里祖先部分的数据
基类和派生类对象赋值转换 逻辑图
cpp
Person Student
___________ ___________
| | | |
|char* _name|<-----|char* _name|
|___________| |___________|
| | | |
|int _age |<-----|int _age |
|___________| |___________|
| |
|int _number|
|___________|
隐藏
- 隐藏:派生类域如果和基类域都定义和声明了同名成员,直接手动方式 如
子类对象./子类域::/直接子类域下去调用或者使用时,也不指定哪个类域,那么编译器默认就调用子类的
隐藏现象演示
cpp
class A
{
public:
void Print()
{
std::cout<<_num<<std::endl;
}
public:
int _num=1;
};
class B
{
public:
int _num=2;
};
int main()
{
B b;
b.Print();
return 0;
}
隐藏解决方式:出现父子类域同名类域,指定父类成员 使用和调用 代码演示
cpp
class A
{
public:
int _num=1;
};
class B
{
public:
void Print()
{
std::cout<<A::_num<<std::endl;
}
public:
int _num=2;
};
int main()
{
B b;
b.Print();
return 0;
}
函数名构成隐藏的例子
这里函数名(语言层函数名)相同,构成隐藏的例子
cpp
class A
{
public:
void Print()
{
std::cout<<A::_num<<std::endl;
}
public:
int _num=1;
};
class B
{
public:
void Print(int i)
{
std::cout<<A::_num+i<<std::endl;
}
public:
int _num=2;
};
派生类的默认成员函数
- 构造函数:派生类对象执行的自己的构造函数前,会先执行权限允许的基类的构造函数,对派生类对象的基类部分 进行初始化 再执行 派生类对应的构造函数
- 注意:初始化列表是构造函数的一部分,派生类的初始化列表无法直接初始化 其基类部分的成员变量,需在初始化列表中指定调用基类构造去初始化 派生类里基类部分 的成员变量
- 派生类对象执行基类构造,要么默认执行基类的默认构造,要么就手动去初始化列表指定执行对应的构造
手动让派生类对象的 基类部分 去执行指定的 基类构造
cpp
class A
{
public:
A()
{
std::cout<<"A默认构造"<<std::endl;
}
A(int i)
{
std::cout<<"指定的A构造"<<std::endl;
}
};
class B:public A
{
public:
B():A(1)
{
std::cout<<"B默认构造"<<std::endl;
}
};
class C:public A
{
public:
C()
{
std::cout<<"B默认构造"<<std::endl;
}
};
int main()
{
B b;
C c;
return 0;
}
- 拷贝构造函数:派生类对象执行的自己的拷贝构造函数前,会先执行权限允许的基类的拷贝构造函数,对派生类对象的基类部分 进行初始化 再执行 派生类对应的拷贝构造构造函数
- 注意:拷贝构造也是构造,也具有初始化列表
- 派生类对象执行基类构造,要么默认执行编译器自生成的基类拷贝构造,要么就手动去初始化列表 指定执行对应 手写的基类的拷贝构造
手动让派生类对象的 基类部分 去执行指定的 基类拷贝构造
cpp
class A
{
public:
A(const A& a)
{
std::cout<<"指定的A拷贝构造"<<std::endl;
}
};
class B:public A
{
public:
B(const B& b):A(b)/*指定拷贝构造*/
{
std::cout<<"B拷贝构造"<<std::endl;
}
};
int main()
{
B b;
B bb(b);
return 0;
}
- 赋值重载:派生类对象执行的自己的赋值重载函数前,会先执行权限允许的基类的赋值重载函数,对派生类对象的基类部分 进行赋值 再执行 派生类对应的赋值重载函数
- 派生类对象执行基类赋值重载,要么默认执行编译器自生成的基类赋值重载,要么就手动去赋值重载函数体内 指定执行对应 手写的基类的拷贝构造
- 函数体内手动指定需要
基类域::函数重载(...)来指定调用,因为派生类和基类的赋值重载会构成隐藏,因此调用时不指定基类域 就会默认调用 子类的赋值重载 从而导致死循环崩溃
- 函数体内手动指定需要
手动让派生类对象的 基类部分 去执行指定的 基类赋值重载
cpp
class A
{
public:
A& operator=(const A& a)
{
std::cout<<"A赋值重载"<<std::endl;
}
};
class B:public A
{
public:
B& operator=(const B& a)
{
A::operator=(a); /*指定赋值重载*/
std::cout<<"B赋值重载"<<std::endl;
}
};
int main()
{
B b;
B bb;
bb=b;
return 0;
}
- 析构函数:派生类对象执行的自己的析构函数后,会再自动执行权限允许的基类的析构函数,对派生类对象的基类部分 进行析构
- 派生类对象执行自己派生类析构函数后,会直接自动执行基类默认生成或手写的析构函数,就无需手动去 派生类析构函数体内 指定执行对应 手写的基类的析构函数,如果手动指定调用基类 析构则会多调用一次 基类析构函数
- 函数体内手动指定需要
基类域::析构函数名(...)来指定调用,因为派生类和基类的析构函数会构成隐藏,因此调用时不指定基类域 就会默认调用 子类的析构函数 从而导致死循环崩溃 - 在语言层析构函数名都叫
destructor,虽然语言层上层的用户层析构函数名为~类名()
- 函数体内手动指定需要
手动让派生类对象的 基类部分 去执行指定的 基类析构函数
cpp
class A
{
public:
~A()
{
std::cout<<"A析构函数"<<std::endl;
}
};
class B:public A
{
public:
~B()
{
A::~A();
std::cout<<"B析构函数"<<std::endl;
}
};
int main()
{
B b;
return 0;
}
继承和友元
- 基类的友元不是派生类的友元,友元关系不可被继承
继承域静态成员
- 基类的
static静态成员,这个静态成员在基类和子孙类中 就独一份,所有子孙或者基类的对象 只能用这同一个静态成员,并且只供 基类和其子孙类 的对象等 共用
基类静态成员和子孙类的 逻辑关系
cpp
_______________________________________________
|class Father _____________________ |
| | | |
|_______________| |_________|
|class Child | | |
| |Father static member | |
|_______________| |_________|
|class Child | | |
| |_____________________| |
|_______________________________________________|
多继承与单继承
- 单继承:就是指 一个子类只能有一个直接父类 的关系,那么这段关系就叫做单继承
- 多继承:就是指 一个子类存在多个直接父类 的关系,那么这段关系就叫做 多继承
- 拓展:java 不像 cpp,java 只有单继承
单继承语法 代码演示
cpp
class A
{
public:
A()
{
std::cout<<"A"<<std::endl;
}
protected:
int _a;
};
class B:public A
{
public:
B()
{
std::cout<<"B"<<std::endl;
}
private:
int _b;
};
多继承语法 代码演示
cpp
class A
{
public:
A()
{
std::cout<<"A"<<std::endl;
}
protected:
int _a;
};
class B
{
public:
B()
{
std::cout<<"B"<<std::endl;
}
protected:
int _b;
};
class C:public A,public B //继承A和B
{
public:
C()
{
std::cout<<"C"<<std::endl;
}
private:
int _c;
};
菱形继承
- 菱形继承:将当前类 的祖先类 全部画出,派生类和基类 相连,构成环路的 这些类组成的关系 就叫做菱形继承
菱形继承代码演示
cpp
class A
{
public:
A()
{
std::cout<<"A"<<std::endl;
}
protected:
int _a;
};
class B:public A
{
public:
B()
{
std::cout<<"B"<<std::endl;
}
protected:
int _b;
};
class C:public A
{
public:
C()
{
std::cout<<"C"<<std::endl;
}
private:
int _c;
};
class D:public B,public C
{
public:
D()
{
std::cout<<"D"<<std::endl;
}
private:
int _d;
};
- 菱形继承存在的问题:类在实例化时,会出现重复性质的 类成员对象
- 注意:建议编写时尽量避免菱形继承的现象
菱形继承代码
D 类对象的所有成员变量所包含哪些成员变量:
_b+_c+2_a+_d
cpp
class A
{
public:
A()
{
std::cout<<"A"<<std::endl;
}
public:
int _a;
};
class B:public A
{
public:
B()
{
std::cout<<"B"<<std::endl;
}
public:
int _b;
};
___________ :B对象
| int _a |_part of A
|___________|
| int _b |
|___________|
class C:public A
{
public:
C()
{
std::cout<<"C"<<std::endl;
}
public:
int _c;
};
___________ :C对象
| int _a |_part of A
|___________|
| int _c |
|___________|
class D:public B,public C
{
public:
D()
{
std::cout<<"D"<<std::endl;
}
public:
int _d;
};
int main()
{
D d;
std::cout<<d._a<<std::endl; !!!此时会报错,因为不知道访问哪个_a
std::cout<<d.B::_a<<std::endl; //此时访问的是 根据基类B的基类 所开辟的空间
std::cout<<d.C::_a<<std::endl; //此时访问的是 根据基类C的基类 所开辟的空间
}
___________ :D对象
part of A_| int _a |_
|___________| |_B类部分
| int _b |_|
|___________|
part of A_| int _a |_
|___________| |_C类部分
| int _c |_|
|___________|
| int _d |
|___________|
菱形继承包含公式 逻辑图
cpp
//例1
A
/ \
B C
\ /
D
D = (A通往D路线数)*(A的成员对象)+(B通往D路线数)*(B成员变量)+(C通往D路线数)*(C成员变量)+D成员变量
= 2_a+_b+_c+_d
//例2
A
/ \
B C
/ \ \
D E F
\ | /
\ | /
\ | /
G
G = (A通往G路线数)*(A的成员对象)+(B通往G路线数)*(B成员变量)+(C通往G路线数)*(C成员变量)+(D通往G路线)*(D对象)+(E通往G路线)*(E对象)+(F通往G路线)*(F对象)+G对象
= 3_a+2_b+_c+_d+_e+_f+_g
//例3
A
/ \
B C D
\ | /
\ | /
\ | /
E
E = (A通往E路线数)*(A的成员对象)+(B通往E路线数)*(B成员变量)+(C通往E路线数)*(C成员变量)+(D通往E路线数)*(D成员变量)+E对象
= 2_a+_b+_c+_d+_e
虚继承
- 为了解决菱形继承带来的数据二义性和空间冗余,此时 Cpp 设计中就设计出虚继承这个语法,对出现问题的类对象里 对基类的基类成员 去重,减少数据冗余和避免数据二义性
- 虚继承 用 virtual 关键字来解决,在每个环路的第二大辈分对应 的类中 执行虚继承
- 执行虚继承的类 其类对象 实例化后,对应的 成员变量空间 也会改变
- 被虚拟继承的类就是虚基类
虚继承代码演示
虚继承原理 如原理图可知,之所以虚继承采用这种存储方式解决二义性等问题,主要是为了方便 底层满足的切割操作
cpp
class A //虚基类
{
public:
A()
{
std::cout<<"A"<<std::endl;
}
protected:
int _a;
};
class B:virtual public A
{
public:
B()
{
std::cout<<"B"<<std::endl;
}
protected:
int _b;
};
class C:virtual public A
{
public:
C()
{
std::cout<<"C"<<std::endl;
}
private:
int _c;
};
//C类对象内存逻辑图,C类采用虚继承因此内部的内存逻辑图也会跟着改变,ptr指向的表也叫做虚基表,虚基表专门存放偏移量
//C类内存逻辑图之所以会改变是为了方便底层切割动作统一,这样内存格式一样 那么C和其子孙类 底层既可以采用统一的切割方式
____________ :C对象 ___________
| int* ptr--|---------->| 8(offset) |~~~~~~&ptr1+8
|____________| |___________| |
| int _b | |
|____________| |
| int _a |<__________________________________|
|____________|
class D:public B,public C
{
public:
D()
{
std::cout<<"D"<<std::endl;
}
private:
int _d;
};
int main()
{
D d;
std::cout<<d._a<<std::endl; !!!此时会报错,因为不知道访问哪个_a
std::cout<<d.B::_a<<std::endl<<d.C::_a<<std::endl; //此时只有一个_a,所以这两次访问的都是一个_a
}
//D类对象内存逻辑图,ptr1和ptr2指向的表也叫做虚基表,虚基表专门存放偏移量
____________ :D对象 ___________
| int* ptr1-|---------->| 20(offset)|~~~~~~&ptr1+20
|____________| |___________| |
| int _b | |
|____________| ___________ |
| int* ptr2-|---------->| 12(offset)|~&ptr2+12 |
|____________| |___________| | |
| int _c | | |
|____________| | |
| int _d | | |
|____________| | |
| int _a |<___________________________| |
|____________|<__________________________________|
继承总结
- 避免菱形继承,因为用起来太复杂了,还有 能组合就组合,尽量少的继承
- 组合就是将存有公共属性的类,作为不同类的类成员对象,本质和继承解决的 问题都一样
组合和继承的代码演示
cpp
class A
{
public:
int _a;
};
class B //组合
{
private:
A a;
int _b;
};
class C:public A //继承
{
private:
int _c;
};
//C和B类都有A这种公共属性
- 耦合是用来形容类与类之间的关系,内聚是用来形容类内之间成员的关系