mirror of
https://github.com/Estom/notes.git
synced 2026-04-05 11:57:37 +08:00
多态和指针问题
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
> - 继承
|
||||
> - 多态
|
||||
|
||||
## 1 定义抽象数据类型
|
||||
## 1 定义抽象数据类型(抽象)
|
||||
|
||||
### 概念
|
||||
|
||||
@@ -72,7 +72,7 @@ hello{
|
||||
}
|
||||
```
|
||||
|
||||
## 2 访问控制与封装
|
||||
## 2 访问控制与封装(封装)
|
||||
|
||||
### 访问说明符
|
||||
|
||||
@@ -147,7 +147,7 @@ class Hello;
|
||||
* 类内查找。所有的类成员都被考虑。
|
||||
* 在成员函数定义之前的作用域内查找。
|
||||
|
||||
## 5 构造函数再探
|
||||
## 5 构造函数与初始化
|
||||
|
||||
* 当成员是常量或引用的时候,初始化是必不可少的。
|
||||
* 成员初始化的书序与他们在类定义中的出现顺序一致。而非初始化列表中传入参数的顺序。
|
||||
@@ -157,7 +157,7 @@ class Hello;
|
||||
|
||||

|
||||
|
||||
初始化执行的新婚徐
|
||||
初始化执行的顺序
|
||||
|
||||
1. 初始化列表
|
||||
2. 委托构造函数
|
||||
|
||||
@@ -29,12 +29,12 @@ class Foo{
|
||||
|
||||
* 编译器自动生成的拷贝构造函数。从给定的对象中依次将每个非static成员拷贝到正在创建的对象当中。
|
||||
|
||||
### 拷贝初始化
|
||||
### 赋值初始化
|
||||
|
||||
```
|
||||
string nies = string("efji");
|
||||
```
|
||||
* 当我门使用 赋值= 运算符时,发生拷贝初始化。
|
||||
* 当我门使用 赋值= 运算符时,发生赋值初始化,执行拷贝构造函数。
|
||||
* 将一个对象作为实参传递给一个非引用类型的形参
|
||||
* 从一个返回类型为费引用类型的函数返回一个对象
|
||||
* 用花括号列表初始化一个数组中的元素或一个聚合类中的成员
|
||||
BIN
C++/类设计者的工具/2021-03-07-14-26-29.png
Normal file
BIN
C++/类设计者的工具/2021-03-07-14-26-29.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 65 KiB |
BIN
C++/类设计者的工具/2021-03-07-15-55-33.png
Normal file
BIN
C++/类设计者的工具/2021-03-07-15-55-33.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.5 KiB |
BIN
C++/类设计者的工具/2021-03-07-15-58-59.png
Normal file
BIN
C++/类设计者的工具/2021-03-07-15-58-59.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.0 KiB |
BIN
C++/类设计者的工具/2021-03-07-16-00-43.png
Normal file
BIN
C++/类设计者的工具/2021-03-07-16-00-43.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
@@ -1,7 +1,12 @@
|
||||
# 面向对象程序设计
|
||||
|
||||
> 参考文献
|
||||
> * [多态的三种方式](https://blog.csdn.net/qq_41306849/article/details/109081625)
|
||||
> * [虚继承和虚基类](http://c.biancheng.net/view/2280.html)
|
||||
|
||||
|
||||
> 面向对象的基本概念
|
||||
> * 数据抽象和封装
|
||||
> * 数据抽象和封装(在语法基础部分讲解过了)
|
||||
> * 继承
|
||||
> * 多态(动态绑定)
|
||||
|
||||
@@ -15,25 +20,463 @@
|
||||
* 继承:定义相似的类型,对相似的关系建模。实现代码重用。
|
||||
* 动态绑定:可以在以一定程度上忽略相似类型的区别。
|
||||
|
||||
### 继承
|
||||
### 继承概念(继承)
|
||||
|
||||
* 继承:联系在一起的类构成以中层次关系
|
||||
* 基类:层次关系的根部
|
||||
* 派生类:其他类则直接或间接地从基类继承而来。
|
||||
|
||||
* 虚函数:
|
||||
* 派生类与基类的函数继承:
|
||||
* 与类型相关的函数。基类与派生类类型不同,需要重写。
|
||||
* 与类型无关的函数。派生类直接继承,不需要修改。
|
||||
|
||||
## 2 定义基类和派生类
|
||||
* 类派生列表:派生类通过类派生列表,明确指出它的基类。
|
||||
|
||||
```
|
||||
class Dog:public Animal{
|
||||
public:
|
||||
double price()const override;
|
||||
}
|
||||
```
|
||||
|
||||
* 派生类可以通过**override关键字**注明改写基类的函数。
|
||||
|
||||
|
||||
|
||||
### 动态绑定(多态)
|
||||
|
||||
* 在运行时选择函数的版本。通过使用动态绑定,我们能用同一段代码分别处理Animal和Dog的对象。
|
||||
|
||||
## 2 继承与多态详解
|
||||
## 2.1 继承
|
||||
|
||||
### 定义基类
|
||||
```
|
||||
class Quote{
|
||||
public:
|
||||
Quote() = default;
|
||||
Quote(string book,double sales_price):book_no(book),price(sales_price){};
|
||||
string isbn()const{
|
||||
return this.book_no;
|
||||
}
|
||||
virtual double net_price(int n)const{
|
||||
return n*price;
|
||||
}
|
||||
virtual ~Quote()=default;
|
||||
private:
|
||||
string book_no;
|
||||
protected:
|
||||
double price;
|
||||
};
|
||||
```
|
||||
### 定义派生类
|
||||
```
|
||||
class Bulk_quote:public Quote{
|
||||
public:
|
||||
Bulk_quote()=default;
|
||||
Bulk_quote(string,double,int,double);
|
||||
double net_price(int n)const override;
|
||||
private:
|
||||
int mn_qty = 0;
|
||||
double discount=0.0;
|
||||
};
|
||||
Bulk_quote::Bulk_quote(string book,double p,int qty,double disc):Quote(book,p),min_qty(qty),discount(disc){}//委托基类构造函数
|
||||
```
|
||||
|
||||
|
||||
### 继承方式
|
||||
|
||||
* 单一继承:继承一个父类,这种继承称为单一继承,一般情况尽量使用单一继承,使用多重继承容易造成混乱易出问题
|
||||
* 多重继承:继承多个父类,类与类之间要用逗号隔开,类名之前要有继承权限,假使两个或两个基类都有某变量或函数,在子类中调用时需要加类名限定符如c.a::i = 1;
|
||||
* 菱形继承:多重继承掺杂隔代继承1-n-1模式,此时需要用到虚继承,例如 B,C虚拟继承于A,D再多重继承B,C,否则会出错
|
||||
|
||||
|
||||
### 继承权限
|
||||
* 继承权限:继承方式规定了如何访问继承的基类的成员。继承方式指定了派生类成员以及类外对象对于从基类继承来的成员的访问权限
|
||||
|
||||

|
||||
|
||||
### 注意事项
|
||||
|
||||
* 基类的静态成员变量,在整个继承体系中只存在该成员的唯一定义。
|
||||
* 派生类的声明中包含类名,但不能包含派生列表。
|
||||
|
||||
### final关键字
|
||||
|
||||
* 使用**关键字final**说明符防止类被继承
|
||||
|
||||
|
||||
* 使用**关键字final**说明符防止函数被重写
|
||||
```
|
||||
class A final{
|
||||
void f1(int) const;
|
||||
};
|
||||
```
|
||||
|
||||
## 2.2 多态
|
||||
|
||||
### 多态分类
|
||||
|
||||
1. 静态多态,是只在编译期间确定的多态。静态多态在编译期间,根据函数参数的个数和类型推断出调用的函数。静态多态有两种实现的方式
|
||||
1. 重载。(函数重载)
|
||||
2. 模板。
|
||||
2. 动态多态,是运行时多态。通过虚函数机制实现(也称为重写override),使用父类的指针或者是引用,调用一个虚函数时,会根据其指向的具体对象确定调用的函数。基类和子类维护一个虚函数表,对象当中包含的虚指针,指向基类或子类的虚函数表。如果子类没有重写父类的虚函数则会直接调用父类的方法,否则调用子类重写的方法。
|
||||
|
||||
|
||||
### 多态原理
|
||||
|
||||
* (对象的多态性)使用基类的引用或指针调用一个函数时。无法确定该函数作用的对象是什么类型。因为它可能是一个基类的对象,也可能是一个派生类的对象。
|
||||
* (函数的多态性)如果该函数是虚函数,则直到运行时才会决定执行哪个版本。判断的依据是引用或指针所绑定的对象真实类型。
|
||||
* 函数绑定。对非虚函数的调用在编译时进行绑定。我们通过对象进行的函数调用也在编译时绑定。对象的类型是确定不变的。
|
||||
|
||||
> 也就是说多态性体现在指针和引用的不确实能够性上。但对象在内存中的状态是确定的。当且晋档通过指针或引用调用虚函数是,才会在运行时解析该调用,也只有在这种情况下对动态类型才有可能与静态类型不同。
|
||||
|
||||
```
|
||||
Bulk_quote a();//定义了对象a。这时候,无法触发多态。
|
||||
Bulk_quote* b = new Bulk_quote();//指针可以指向不同的类型的对象。
|
||||
Bulk_quote &b = a;//引用可以指向不同类型的对象。
|
||||
```
|
||||
|
||||
### 重写与重定义对比
|
||||
* 重定义:基类中没有声明函数是虚函数。派生类中对普通函数进行了重定义。只是作用域上的覆盖,没有触发多态和动态绑定。
|
||||
* 重定义不能触发动态多态。无论指针或引用绑定的是什么对象,都会根据指针或引用的类型,调用该类型的函数。而不是使用虚指针查找虚函数表。只有调用虚函数的时候,才会去根据对象的虚函数指针,查找类中的虚函数表。
|
||||
|
||||
```
|
||||
class A{
|
||||
public:
|
||||
int a;
|
||||
A():a(10){};
|
||||
int real_ex(){
|
||||
return a;
|
||||
}
|
||||
virtual int virtual_ex(){
|
||||
return a;
|
||||
}
|
||||
};
|
||||
|
||||
class B:public A{
|
||||
public:
|
||||
int b;
|
||||
B():b(20){};
|
||||
int real_ex(){//重定义A的函数
|
||||
return b;
|
||||
}
|
||||
virtual int virtual_ex(){//重写A的函数
|
||||
return b;
|
||||
}
|
||||
};
|
||||
int main(){
|
||||
|
||||
Quote p(Bulk_quote());//直接初始化,拷贝构造函数
|
||||
Quote q = Bulk_quote();//赋值初始化,拷贝构造函数
|
||||
|
||||
// B test_b;
|
||||
// A* test = &test_b;
|
||||
A* test=new B();
|
||||
cout<<test->real_ex()<<endl;//B重定义了函数。但是A类型的指针,调用基类的函数。
|
||||
cout<<test->virtual_ex()<<endl;//B重写类函数。B类型的对象,动态绑定,调用了派生类的函数。
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
### 实现条件
|
||||
运行时多态的条件:
|
||||
* 必须是集成关系
|
||||
* 基类中必须包含虚函数,并且派生类中一定要对基类中的虚函数进行重写。
|
||||
* 通过基类对象的指针或者引用调用虚函数。
|
||||
|
||||
### 注意事项
|
||||
|
||||
以下函数不能作为虚函数
|
||||
1. 友元函数,它不是类的成员函数
|
||||
2. 全局函数
|
||||
3. 静态成员函数,它没有this指针
|
||||
3. 构造函数,拷贝构造函数,以及赋值运算符重载(可以但是一般不建议作为虚函数)
|
||||
|
||||
### 实现原理
|
||||
//参考虚函数。
|
||||
|
||||
|
||||
## 2.3 类型转换
|
||||
### 静态类型和动态类型
|
||||
|
||||
* 静态类型在编译时已知。是指针或者引用的类型。
|
||||
* 动态类型表示内存中的对象类型。动态类型直到运行时才可知。
|
||||
|
||||
|
||||
### 派生类到基类的类型转换。
|
||||
|
||||
* 把派生类对象当成基类对象来使用。将基类的指针或引用绑定到派生类对象。
|
||||
|
||||
### 基类到派生类的类型转换。
|
||||
|
||||
* 不存在从基类向派生类的隐式转换。
|
||||
|
||||
|
||||
|
||||
### 对象之间不存在类型转换
|
||||
|
||||
* 所谓的**类型转换只是指针或者引用的类型转换**,对象本身的类型,没有发生改变。
|
||||
* 但是派生类可以赋值给基类的对象。基类的拷贝构造函数和移动构造函数,接受一个引用对象。将派生类对象赋值给引用对象,实现基类的初始化。实际生成的是一个基类对象。
|
||||
* 这里并非多态,而是执行拷贝构造函数。创建了一个基类对象
|
||||
|
||||
```
|
||||
class A{
|
||||
|
||||
}
|
||||
class B:public A{
|
||||
|
||||
}
|
||||
B b();
|
||||
A a = b;//可以赋值给基类对象。执行拷贝构造函数。并非多态。
|
||||
A aa(b);//直接初始化,执行基类的拷贝构造函数。并非多态。
|
||||
```
|
||||
|
||||
### 转换规则总结
|
||||
|
||||
* 从派生类向基类的类型转换只对指针或引用有效。是指针或引用的类型转换,而不是其指向的对象的类型发生改变。
|
||||
* 基类向派生类不存在隐式类型转换
|
||||
* 派生类向基类的类型转换也可能会由于访问受限而变得不可行。
|
||||
|
||||
## 3 虚函数
|
||||
|
||||
## 4 抽象函数
|
||||
### 虚函数的定义
|
||||
* 虚函数:基类希望它的派生类自定义适合自身的版本。为了实现多态
|
||||
|
||||
## 5 访问控制与继承
|
||||
```
|
||||
class Animal{
|
||||
public:
|
||||
virtual double price(int n)const;
|
||||
}
|
||||
```
|
||||
|
||||
### 虚函数的原理
|
||||
* 除了构造函数的非静态函数都可以是虚函数。
|
||||
* 关键字virtual智能出现在类内部的声明语句之前。不能出现在类外部的函数定义。
|
||||
* 如果把一个函数声明成虚函数,则该函数在派生类中也是隐式的虚函数。(即派生类的派生类,也需要重写次函数)
|
||||
* 派生类可以不用重写虚函数。
|
||||
* 派生类可以在它重写的虚函数前使用**virtual关键字**
|
||||
|
||||
### 回避虚函数的机制
|
||||
|
||||
* 类中的数据成员和成员函数是相互独立的。两者没有必然的联系。
|
||||
* 成员函数通过this指针访问对象的数据成员。在继承体系中,this指针的指向是可以改变。即可以用派生类对象的this指针传递给基类的函数,从而实现派生类调用基类函数的方法。
|
||||
* 调用是不进行动态绑定,而是强迫执行虚函数的某个特定版本。通过域作用运算符实现。
|
||||
```
|
||||
//强制调用基类中定义的函数
|
||||
Bulk_quote *baseP = Bulk_quote();
|
||||
double u = baseP->Quote::net_price();
|
||||
```
|
||||
### 纯虚函数和抽象基类
|
||||
|
||||
```
|
||||
virtual void Eat() = 0;
|
||||
```
|
||||
* 一个纯虚函数无须定义,在函数体的位置书写=0,就可以讲一个虚函数说明为纯虚函数。只能出现在类内部的函数声明语句出。在类的内部必须没有定义,在类的外部可以定义纯虚函数。
|
||||
* 含有纯虚函数的类是抽象基类。纯虚函数相当于接口,不能创建抽象基类的对象。
|
||||
* 派生类构造函数只初始化它的直接基类。
|
||||
|
||||
|
||||
## 6 继承中的类作用域
|
||||
### 虚函数表和虚指针原理
|
||||
|
||||
## 7 构造函数与拷贝控制
|
||||
```C++
|
||||
class A
|
||||
{
|
||||
public:
|
||||
virtual void f();
|
||||
virtual void g();
|
||||
private:
|
||||
int a
|
||||
};
|
||||
|
||||
class B : public A
|
||||
{
|
||||
public:
|
||||
void g();
|
||||
private:
|
||||
int b;
|
||||
};//A、B实现省略
|
||||
```
|
||||
* 因为A有virtual void f()和g(),所以编译器为A类准备了一个虚函数表vtableA,内容如下:
|
||||
|
||||
|
||||
```
|
||||
A::f 的地址
|
||||
A::g 的地址
|
||||
```
|
||||
|
||||
* B因为继承了A,所以编译器也为B准备了一个虚函数表vtableB,内容如下:
|
||||
```
|
||||
A::f 的地址
|
||||
B::g 的地址
|
||||
```
|
||||
> 注意:因为B::g是重写了的,所以B的虚表的g放的是B::g的入口地址,但是f是从上面的A继承下来的,所以f的地址是A::f的入口地址。
|
||||
|
||||
* 某处有语句 B bB;的时候,编译器分配空间时,除了A的int a,B的成员int b;以外,还分配了一个虚指针vptr,指向B的虚函数表vtableB,bB的布局如下:
|
||||
|
||||
```
|
||||
vptr : 指向B的虚表vtableB
|
||||
int a: 继承A的成员
|
||||
int b: B成员
|
||||
```
|
||||
|
||||
## 4 访问控制与继承
|
||||
|
||||
### 访问控制
|
||||
* 派生类能够访问公有成员和受保护的成员。
|
||||
* 派生类不能访问私有成员。
|
||||
|
||||
public:
|
||||
private:
|
||||
protected:
|
||||
|
||||
### 继承类型
|
||||
|
||||
* public 公有继承
|
||||
* private私有继承
|
||||
|
||||
### 默认的集成保护级别
|
||||
|
||||
* class关键字定义的派生类,默认是私有继承
|
||||
* struct关键字定义的派生类,默认是公有继承
|
||||
|
||||
## 5 继承中的类作用域
|
||||
|
||||
### 作用域
|
||||
* 当存在继承关系是,派生类的作用域嵌套在基类的作用域内。如果一个名字在派生类的作用域内无法解析,编译器在外层的基类作用域中寻找改名字的定义。
|
||||
|
||||
|
||||
### 编译时名字查找
|
||||
|
||||
* 引用或指针的静态类型决定了该对象有哪些成员是可见的。一个基类的引用和指针只能访问基类的成员。即是动态对象是其派生类。
|
||||
|
||||
### 名字冲突与继承
|
||||
* 派生了重用定义在直接基类或间接基类中的名字,会屏蔽定义在外层作用域基类中的名字
|
||||
|
||||
### 访问隐藏的成员
|
||||
|
||||
* 通过作用域运算符来使用一个被隐藏的基类成员。
|
||||
|
||||
|
||||
## 6 虚继承和虚基类
|
||||
|
||||
### 多继承
|
||||
* 多继承(Multiple Inheritance)是指从多个直接基类中产生派生类的能力,多继承的派生类继承了所有父类的成员。尽管概念上非常简单,但是多个基类的相互交织可能会带来错综复杂的设计问题,命名冲突就是不可回避的一个。
|
||||
|
||||
* 多继承时很容易产生命名冲突,即使我们很小心地将所有类中的成员变量和成员函数都命名为不同的名字,命名冲突依然有可能发生,比如典型的是菱形继承,如下图所示:
|
||||
|
||||
### 菱形继承
|
||||
|
||||

|
||||
|
||||
* 类 A 派生出类 B 和类 C,类 D 继承自类 B 和类 C,这个时候类 A 中的成员变量和成员函数继承到类 D 中变成了两份,一份来自 A-->B-->D 这条路径,另一份来自 A-->C-->D 这条路径。
|
||||
|
||||
* 下面是菱形继承的具体实现:
|
||||
|
||||
```
|
||||
//间接基类A
|
||||
class A{
|
||||
protected:
|
||||
int m_a;
|
||||
};
|
||||
//直接基类B
|
||||
class B: public A{
|
||||
protected:
|
||||
int m_b;
|
||||
};
|
||||
//直接基类C
|
||||
class C: public A{
|
||||
protected:
|
||||
int m_c;
|
||||
};
|
||||
//派生类D
|
||||
class D: public B, public C{
|
||||
public:
|
||||
void seta(int a){ m_a = a; } //命名冲突
|
||||
void setb(int b){ m_b = b; } //正确
|
||||
void setc(int c){ m_c = c; } //正确
|
||||
void setd(int d){ m_d = d; } //正确
|
||||
private:
|
||||
int m_d;
|
||||
};
|
||||
int main(){
|
||||
D d;
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
* 这段代码实现了上图所示的菱形继承,第 25 行代码试图直接访问成员变量 m_a,结果发生了错误,因为类 B 和类 C 中都有成员变量 m_a(从 A 类继承而来),编译器不知道选用哪一个,所以产生了歧义。
|
||||
|
||||
* 为了消除歧义,我们可以在 m_a 的前面指明它具体来自哪个类:
|
||||
```
|
||||
void seta(int a){ B::m_a = a; }
|
||||
```
|
||||
* 这样表示使用 B 类的 m_a。当然也可以使用 C 类的:
|
||||
```
|
||||
void seta(int a){ C::m_a = a; }
|
||||
```
|
||||
|
||||
### 虚继承(Virtual Inheritance)
|
||||
|
||||
* 为了解决多继承时的命名冲突和冗余数据问题,C++ 提出了虚继承,使得在派生类中只保留一份间接基类的成员。
|
||||
|
||||
* 在继承方式前面加上 virtual 关键字就是虚继承,请看下面的例子:
|
||||
|
||||
```
|
||||
//间接基类A
|
||||
class A{
|
||||
protected:
|
||||
int m_a;
|
||||
};
|
||||
//直接基类B
|
||||
class B: virtual public A{ //虚继承
|
||||
protected:
|
||||
int m_b;
|
||||
};
|
||||
//直接基类C
|
||||
class C: virtual public A{ //虚继承
|
||||
protected:
|
||||
int m_c;
|
||||
};
|
||||
//派生类D
|
||||
class D: public B, public C{
|
||||
public:
|
||||
void seta(int a){ m_a = a; } //正确
|
||||
void setb(int b){ m_b = b; } //正确
|
||||
void setc(int c){ m_c = c; } //正确
|
||||
void setd(int d){ m_d = d; } //正确
|
||||
private:
|
||||
int m_d;
|
||||
};
|
||||
int main(){
|
||||
D d;
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
* 这段代码使用虚继承重新实现了上图所示的菱形继承,这样在派生类 D 中就只保留了一份成员变量 m_a,直接访问就不会再有歧义了。
|
||||
|
||||
* 虚继承的目的是让某个类做出声明,承诺愿意共享它的基类。其中,这个被共享的基类就称为虚基类(Virtual Base Class),本例中的 A 就是一个虚基类。在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含一份虚基类的成员。
|
||||
|
||||
* 现在让我们重新梳理一下本例的继承关系,如下图所示:
|
||||
|
||||

|
||||
|
||||
* 观察这个新的继承体系,我们会发现虚继承的一个不太直观的特征:必须在虚派生的真实需求出现前就已经完成虚派生的操作。在上图中,当定义 D 类时才出现了对虚派生的需求,但是如果 B 类和 C 类不是从 A 类虚派生得到的,那么 D 类还是会保留 A 类的两份成员。
|
||||
|
||||
* 换个角度讲,虚派生只影响从指定了虚基类的派生类中进一步派生出来的类,它不会影响派生类本身。
|
||||
|
||||
### 虚继承在C++标准库中的实际应用
|
||||
* 在实际开发中,位于中间层次的基类将其继承声明为虚继承一般不会带来什么问题。通常情况下,使用虚继承的类层次是由一个人或者一个项目组一次性设计完成的。对于一个独立开发的类来说,很少需要基类中的某一个类是虚基类,况且新类的开发者也无法改变已经存在的类体系。
|
||||
|
||||
* C++标准库中的 iostream 类就是一个虚继承的实际应用案例。iostream 从 istream 和 ostream 直接继承而来,而 istream 和 ostream 又都继承自一个共同的名为 base_ios 的类,是典型的菱形继承。此时 istream 和 ostream 必须采用虚继承,否则将导致 iostream 类中保留两份 base_ios 类的成员。
|
||||
|
||||
|
||||

|
||||
|
||||
### 虚基类成员的可见性
|
||||
|
||||
* 因为在虚继承的最终派生类中只保留了一份虚基类的成员,所以该成员可以被直接访问,不会产生二义性。此外,如果虚基类的成员只被一条派生路径覆盖,那么仍然可以直接访问这个被覆盖的成员。但是如果该成员被两条或多条路径覆盖了,那就不能直接访问了,此时必须指明该成员属于哪个类。
|
||||
|
||||
* 以图2中的菱形继承为例,假设 A 定义了一个名为 x 的成员变量,当我们在 D 中直接访问 x 时,会有三种可能性:
|
||||
* 如果 B 和 C 中都没有 x 的定义,那么 x 将被解析为 A 的成员,此时不存在二义性。
|
||||
* 如果 B 或 C 其中的一个类定义了 x,也不会有二义性,派生类的 x 比虚基类的 x 优先级更高。
|
||||
* 如果 B 和 C 中都定义了 x,那么直接访问 x 将产生二义性问题。
|
||||
|
||||
## 8 容器与继承
|
||||
|
||||
73
C++/类设计者的工具/3.cpp
Normal file
73
C++/类设计者的工具/3.cpp
Normal file
@@ -0,0 +1,73 @@
|
||||
#include<iostream>
|
||||
using namespace std;
|
||||
|
||||
|
||||
|
||||
class Quote{
|
||||
public:
|
||||
Quote() = default;
|
||||
Quote(string book,double sales_price):book_no(book),price(sales_price){};
|
||||
string isbn()const{
|
||||
return book_no;
|
||||
}
|
||||
virtual double net_price(int n)const{
|
||||
return n*price;
|
||||
}
|
||||
virtual ~Quote()=default;
|
||||
private:
|
||||
string book_no;
|
||||
protected:
|
||||
double price;
|
||||
};
|
||||
|
||||
class Bulk_quote:public Quote{
|
||||
public:
|
||||
Bulk_quote()=default;
|
||||
Bulk_quote(string,double,int,double);
|
||||
double net_price(int n)const override;
|
||||
private:
|
||||
int mn_qty = 0;
|
||||
double discount=0.0;
|
||||
};
|
||||
|
||||
Bulk_quote::Bulk_quote(string a,double b,int c,double d):Quote(a,b),mn_qty(c),discount(d){};
|
||||
double Bulk_quote::net_price(int n)const{
|
||||
return n*price*discount;
|
||||
}
|
||||
|
||||
|
||||
class A{
|
||||
public:
|
||||
int a;
|
||||
A():a(10){};
|
||||
int real_ex(){
|
||||
return a;
|
||||
}
|
||||
virtual int virtual_ex(){
|
||||
return a;
|
||||
}
|
||||
};
|
||||
|
||||
class B:public A{
|
||||
public:
|
||||
int b;
|
||||
B():b(20){};
|
||||
int real_ex(){//重定义A的函数
|
||||
return b;
|
||||
}
|
||||
virtual int virtual_ex(){//重写A的函数
|
||||
return b;
|
||||
}
|
||||
};
|
||||
int main(){
|
||||
|
||||
Quote p(Bulk_quote());//直接初始化,拷贝构造函数
|
||||
Quote q = Bulk_quote();//赋值初始化,拷贝构造函数
|
||||
|
||||
// B test_b;
|
||||
// A* test = &test_b;
|
||||
A* test=new B();
|
||||
cout<<test->real_ex()<<endl;//B重定义了函数。但是A类型的指针,调用基类的函数。
|
||||
cout<<test->virtual_ex()<<endl;//B重写类函数。B类型的对象,动态绑定,调用了派生类的函数。
|
||||
return 0;
|
||||
}
|
||||
@@ -2,8 +2,107 @@
|
||||
|
||||
## 1 C中动态内存的实现
|
||||
|
||||
### 概念
|
||||
* c 语言主要是使用malloc / calloc / realloc 来进行内存申请的。
|
||||
|
||||
### 共同点
|
||||
|
||||
* 都是从堆上进行动态内存分配
|
||||
* 释放内存都是需要使用free函数来释放
|
||||
* 三者的返回值都是void*
|
||||
* 都需要强制类型转换
|
||||
* 都需要对申请出的空间判空(因为申请内存失败会返回空)
|
||||
|
||||
### malloc
|
||||
|
||||
```
|
||||
void *malloc( size_t size );
|
||||
```
|
||||
* malloc的参数是用户所需内存空间大小的字节数,不会对申请成功的内存初始化。
|
||||
* malloc 申请空间时并不是需要多少就申请多少,而是会多申请一些空间,
|
||||
* 多申请一个32字节的结构体,里面对申请的空间进行描述,
|
||||
* 在申请的空间前后会各多申请 4 个字节的空间,这就是保护机制,当你操作不当越界了,这 8 个字节的内容会改变,操作系统会检查前后 4 个字节是否改变了,以此判断是否越界了。
|
||||
|
||||
|
||||
### calloc
|
||||
```
|
||||
void *calloc( size_t num, size_t size );
|
||||
```
|
||||
* calloc的参数:第一个:元素的个数,第二个:单个元素所占字节;会把申请成功的空间初始化为 0
|
||||
|
||||
### realloc
|
||||
|
||||
```
|
||||
void *realloc( void *ptr, size_t size );
|
||||
```
|
||||
* realloc的参数:第一个:地址,第二个:字节数。对于 realloc 的第一个参数:
|
||||
* 如果指向空,那么此时realloc 就和 malloc 是一样的;
|
||||
* 如果不为空,那么就将即将要申请的空间与原空间进行比较。
|
||||
* 如果要申请的空间比原空间小,就将原空间缩小,并返回原空间首地址
|
||||
* 如果要申请的空间比原空间大,那么分两种情况:
|
||||
* 第一种:新旧空间之差小于原空间大小,直接在原空间进行延伸,并返回原空间的首地址。
|
||||
* 第二种:新旧空间之差大于原空间的大小,那么直接开辟新空间,并将原空间的数据拷贝到新空间,并返回新空间首地址。
|
||||
|
||||
|
||||
## 2 C++中动态内存的实现
|
||||
### 特点
|
||||
new申请的空间:
|
||||
* 不用强制类型转换;
|
||||
* 不用对申请出来的空间进行判空;
|
||||
* 可以申请时初始化这段空间
|
||||
|
||||
### 申请与释放关系
|
||||
```
|
||||
malloc / free
|
||||
new / delete
|
||||
new[] / delete[]
|
||||
```
|
||||
* 对于内置类型:如果没有配合使用,可能不会出现什么问题。
|
||||
* 对于自定义类型:
|
||||
* malloc:只是将空间动态开辟成功,并不会调用构造函数初始化空间。
|
||||
* free:只是将申请的空间进行释放,并不会调用析构函数清理对象中的资源
|
||||
* new:先将对象的空间开辟成功,然后调用构造函数完成对象的初始化。
|
||||
* delete:先调用析构函数将对象中的资源清理,然后释放对象占用的空间
|
||||
|
||||
* 如果对一个内部有资源的自定义类型使用 malloc 开辟内存,此时调用 delete 进行空间的释放,程序就会崩溃。因为 malloc 只负责开辟空间,并不会调用对象的构造函数对其成员变量进行初始化,那么内部的成员变量是没有分配空间的,当我们调用 delete 时,delete会先对对象进行资源清理,但是对象里的资源 malloc 并没有给其分配,所以我们清理的时候是非法的操作。导致程序崩溃。
|
||||
|
||||
* 对于内部有资源的自定义类型,使用 new 开辟的空间使用 free 释放,会造成内存泄漏,因为 free 并不会调用析构函数清理对象的资源,因此会造成资源泄漏。
|
||||
|
||||
|
||||
## 3 两者的区别和优势劣势
|
||||
### new流程
|
||||
|
||||
1. 第一步:调用operator new() 来申请空间
|
||||
1. 申请空间成功:返回空间的首地址。
|
||||
2. 申请空间失败:检测用户是否提供空间不足的应对措施?如果提供应对措施,则执行应对措施,否则直接抛出 bad_alloc 类型的异常。
|
||||
2. 第二步:调用该类的构造函数
|
||||
|
||||
### new[]的流程
|
||||
1. 第一步:调用void* operator new[](count = sizeof(T) * N + 4),如果T类的析构函数显式提供就多申请4个字节。(多申请的四个字节就是用来保存对象的个数,可以知道未来需要调用几次析构函数。)
|
||||
2. 第二步:将空间前四个字节填充对象的个数,然后调用构造函数构造 N 个 T 类型对象。
|
||||
|
||||
### delete的流程:
|
||||
1. 第一步:调用析构函数清理对象中的资源。
|
||||
2. 第二步:调用void operator delete(void* p)释放空间,void operator delete(void* p)中调用的是 free 释放空间。
|
||||
|
||||
### delete[] 的流程:
|
||||
1. 第一步:从第一个对象空间之前的4个字节中取对象的个数N
|
||||
2. 第二步:调用N次析构函数倒着释放(栈的特性)
|
||||
3. 第三步:void operator delete[](void* p)----这个p就是真正使用位置倒退4个字节的位置,也就是申请的空间的首地址。
|
||||
4. 在这里operator delete[](void* p) 调用 void operator delete(void* p) 调用 free()
|
||||
|
||||
> operator new 和 operator delete 实际上是由 malloc 和 free 来实现的,是对malloc 和 free 的封装。
|
||||
## 3 对比
|
||||
|
||||
### malloc/free 和 new/delete 区别:
|
||||
|
||||
* 共同点:都在堆上申请空间,都需要手动申请 / 释放空间。
|
||||
* 不同点:
|
||||
1. malloc/free 是函数,new/delete是标识符
|
||||
2. malloc 不会对对象进行初始化,new 可以初始化
|
||||
3. malloc 申请空间时,需要手动计算需要申请空间的大小,而new只需在后面跟上类型,编译器会自动计算大小。
|
||||
4. malloc 返回值是 void*,使用时必须要强制类型转换,而 new 并不需要强制类型转换,因为new后跟的就是类型。
|
||||
5. malloc 申请空间失败返回 NULL,因此使用时必须判空,new不需要判空,但是需要捕获异常
|
||||
6. 申请自定义类型对象时,malloc/free只会开辟空间,并不调用构造/析构函数,而 new 是先申请空间,
|
||||
然后调用构造函数完成对象的初始化,delete 在释放空间前会先清理对象占用的资源。
|
||||
7. malloc/free 的效率会比 new/delete 的高,因为 new/delete 中封装的是malloc/free。
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
# 指针
|
||||
|
||||
### 野指针
|
||||
野指针就是未进行初始化的指针,你不知道这个指针变量里面的内容是什么,一般是最近一次这块内存上面的内容,也就是说不知道用这个指针访问的内存是哪一块,所以指针初始化的时候要typename * p = nullptr;
|
||||
|
||||
### 悬空指针
|
||||
悬空指针就是将这个指针指向的内存已经释放了,却没有对指针进行赋空,因为C++并没有垃圾回收机制,你delete了一个指针只是释放了指针指向的内存空间,而指针本身依旧还在,正确的操作是delete一个指针后立马将指针赋值为nullptr
|
||||
|
||||
### 空指针
|
||||
就是指向NULL的指针,但是不保证一定是空,因为NULL本质就是0,我依旧可以用*p来访问地址为0的区域啊,虽然这是会被编译器禁止的。
|
||||
@@ -1,10 +1,8 @@
|
||||
# 类的构造函数
|
||||
|
||||
> 与类同名的,没有返回值的函数,用来创建、赋值、移动、销毁该类的对象。
|
||||
|
||||
## 2 类的构造函数
|
||||
|
||||
与类同名的,没有返回值的函数,用来创建、管理该类。
|
||||
|
||||
### 合成构造函数
|
||||
## 合成构造函数
|
||||
|
||||
编译器自动生成的一系列构造函数。包括以下几种
|
||||
* 合成默认构造函数
|
||||
@@ -12,18 +10,19 @@
|
||||
* 合成拷贝构造函数
|
||||
* 即是用户定义了其他类型的构造函数,编译器还会自动生成合成拷贝构造函数。
|
||||
* 合成析构函数
|
||||
* 系统自动生成的析构函数。
|
||||
|
||||
### 默认构造函数
|
||||
## 默认构造函数
|
||||
|
||||
无参构造函数。
|
||||
|
||||
### 拷贝构造函数
|
||||
## 拷贝构造函数
|
||||
|
||||
唯一参数是当前类类型。
|
||||
唯一参数是当前类类型,或者当前类型的const引用。
|
||||
|
||||
### 移动构造函数
|
||||
## 移动构造函数
|
||||
|
||||
|
||||
### 委托构造函数
|
||||
## 委托构造函数
|
||||
|
||||
使用已有的构造函数初始化。
|
||||
80
C++/面试/16.多态的三种实现方式.md
Normal file
80
C++/面试/16.多态的三种实现方式.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# 多态的三种方式
|
||||
|
||||
## 0 概述
|
||||
### 定义
|
||||
|
||||
多态的定义简单来说就是使一条语句有多种状态。
|
||||
|
||||
### 实现方式
|
||||
|
||||
多态的实现方式分为三块:重载、重写、重定义。下面我们来谈一谈他们各自的实现方式和实现原理。
|
||||
|
||||
## 1 重载
|
||||
### 实现方式
|
||||
|
||||
* 重载是在同一作用域内(不管是模块内还是类内,只要是在同一作用域内),具有相同函数名,不同的形参个数或者形参类型。返回值可以相同也可以不同(在函数名、形参个数、形参类型都相同而返回值类型不同的情况下无法构成重载,编译器报错。这个道理很简单,在函数调用的时候是不看返回值类型的)。
|
||||
|
||||
### 实现原理
|
||||
|
||||
* 重载是一种静态多态,即在编译的时候确定的。C++实现重载的方式是跟编译器有关,编译过后C++的函数名会发生改变,会带有形参个数、类型以及返回值类型的信息(虽然带有返回值类型但是返回值类型不能区分这个函数),所以编译器能够区分具有不同形参个数或者类型以及相同函数名的函数。
|
||||
* 插一句,在C语言中编译器编译过后函数名中不会带有形参个数以及类型的信息,因此C语言没有重载的特性。由此带来麻烦的一点是如果想要在C++中调用C语言的库,需要特殊的操作(extern “C”{})。库中的函数经过C编译器编译的话会生成不带有形参信息的函数名,而用C++的编译器编译过后会生成带有形参信息的函数名,因此将会找不到这个函数。extern “C”{}的作用是使在这个作用域中的语句用C编译器编译,这样就不会出错。这也是一种语言兼容性的问题。
|
||||
|
||||
## 2 重写
|
||||
### 实现方式
|
||||
|
||||
* 重写是在不同作用域内(一个在父类一个在子类),函数名、形参个数、形参类型、返回值类型都相同并且父类中带有virtual关键字(换言之子类中带不带virtual都没有关系)。
|
||||
* 有一种特殊的情况:函数返回值类型可以不同但是必须是指针或者引用,并且两个虚函数的返回值之间必须要构成父子类关系。这种情况称之为协变,也是一种重写。引入协变的好处是为了避免危险的类型转换。
|
||||
|
||||
### 实现原理
|
||||
|
||||
* 重写是一种动态多态,即在运行时确定的。C++实现重写的方式也跟编译器有关,编译器在实例化一个具有虚函数的类时会生成一个vptr指针(这就是为什么静态函数、友元函数不能声明为虚函数,因为它们不实例化也可以调用,而虚函数必须要实例化,这也是为什么构造函数不能声明为虚函数,因为你要调用虚函数必须得要有vptr指针,而构造函数此时还没有被调用,内存中还不存在vptr指针,逻辑上矛盾了)。
|
||||
* vptr指针在类的内存空间中占最低地址的四字节。vptr指针指向的空间称为虚函数表,vptr指针指向其表头,在虚函数表里面按声明顺序存放了虚函数的函数指针,如果在子类中重写了,在子类的内存空间中也会产生一个vptr指针,同时会把父类的虚函数表copy一下当做自己的,然后如果在子类中重新声明了虚函数,会按声明顺序接在父类的虚函数函数指针下。而子类中重写的虚函数则会替换掉虚函数表中原先父类的虚函数函数指针。
|
||||
* 重点来了,在调用虚函数时,不管调用他的是父类的指针、引用还是子类的指针、引用,他都不管,只看他所指向或者引用的对象的类型(这也称为**动态联编**),如果是父类的对象,那就调用父类里面的vptr指针然后找到相应的虚函数,如果是子类的对象,那就调用子类里面的vptr指针然后找到相应的虚函数。
|
||||
* 当然这样子的过程相比静态多态而言,时间和空间上的开销都多了(这也是为什么内联函数为什么不能声明为虚函数,因为这和内联函数加快执行速度的初衷相矛盾)。
|
||||
|
||||
## 3 重定义
|
||||
### 实现方式
|
||||
|
||||
* 重定义是在不同作用域内的(一个在父类一个在子类),只要函数名相同,且不构成重写,均称之为重定义
|
||||
|
||||
### 实现原理
|
||||
|
||||
* 重定义的实现原理跟继承树中函数的寻找方式有关,他会从当前对象的类作用域内开始查找同名的函数,如果没有找到就一直向上查找直到基类为止。如果找到一个同名的函数就停止。这也就说明他不管函数的形参类型或者个数是不是一样,只要函数名一样,他就认为是找到了,如果这时候形参类型或者个数不一致,编译器就会报错。
|
||||
* 多重继承的查找,如果在同一层内出现一样的函数声明那么编译器会报错不知道调用哪一个函数,这类问题也叫钻石继承问题。钻石问题的解决方案可以通过虚继承来实现,这样就不会存在多个一样的函数声明。
|
||||
|
||||
```
|
||||
|
||||
class A{
|
||||
public:
|
||||
int a;
|
||||
A():a(10){};
|
||||
int real_ex(){
|
||||
return a;
|
||||
}
|
||||
virtual int virtual_ex(){
|
||||
return a;
|
||||
}
|
||||
};
|
||||
|
||||
class B:public A{
|
||||
public:
|
||||
int b;
|
||||
B():b(20){};
|
||||
int real_ex(){//重定义A的函数
|
||||
return b;
|
||||
}
|
||||
virtual int virtual_ex(){//重写A的函数
|
||||
return b;
|
||||
}
|
||||
};
|
||||
int main(){
|
||||
|
||||
// B test_b;
|
||||
// A* test = &test_b;
|
||||
A* test=new B();
|
||||
cout<<test->real_ex()<<endl;//10 B重定义了函数。但是A类型的指针,调用基类的函数。
|
||||
cout<<test->virtual_ex()<<endl;//10 B重写类函数。B类型的对象,动态绑定,调用了派生类的函数。
|
||||
return 0;
|
||||
}
|
||||
|
||||
```
|
||||
@@ -636,7 +636,7 @@ STL(Standard Template Library,标准模板库)是惠普实验室开发的一
|
||||
|
||||
STL 的代码从广义上讲分为三类:algorithm(算法)、container(容器)和 iterator(迭代器),几乎所有的代码都采 用了模板类和模版函数的方式,这相比于传统的由函数和类组成的库来说提供了更好的代码重用机会。在 C++标准中,STL 被组织为下面的 13 个头文 件:`<algorithm>、<deque>、<functional>、<iterator>、<vector>、<list>、<map>、<memory>、<numeric>、<queue>、<set>、<stack> 和<utility>`。
|
||||
|
||||
1、算法
|
||||
### 1. 算法
|
||||
|
||||
函数库对数据类型的选择对其可重用性起着至关重要的作用。举例来说,一个求方根的函数,在使用浮点数作为其参数类型的情况下的可重用性肯定比使 用整型作为它的参数类性要高。而 C++通过模板的机制允许推迟对某些类型的选择,直到真正想使用模板或者说对模板进行特化的时候,STL 就利用了这一点提 供了相当多的有用算法。它是在一个有效的框架中完成这些算法的——可以将所有的类型划分为少数的几类,然后就可以在模版的参数中使用一种类型替换掉同一种 类中的其他类型。
|
||||
|
||||
@@ -644,7 +644,7 @@ STL 提供了大约 100 个实现算法的模版函数,比如算法 for_each
|
||||
|
||||
算法部分主要由头文件<algorithm>,<numeric>和<functional>组 成。<algorithm>是所有 STL 头文件中最大的一个(尽管它很好理解),它是由一大堆模版函数组成的,可以认为每个函数在很大程度上 都是独立的,其中常用到的功能范围涉及到比较、交换、查找、遍历操作、复制、修改、移除、反转、排序、合并等等。<numeric>体积很 小,只包括几个在序列上面进行简单数学运算的模板函数,包括加法和乘法在序列上的一些操作。<functional>中则定义了一些模板类, 用以声明函数对象。
|
||||
|
||||
2、容器
|
||||
### 2. 容器
|
||||
|
||||
在实际的开发过程中,数据结构本身的重要性不会逊于操作于数据结构的算法的重要性,当程序中存在着对时间要求很高的部分时,数据结构的选择就显得更加重要。
|
||||
|
||||
@@ -783,7 +783,7 @@ STL 提供了大约 100 个实现算法的模版函数,比如算法 for_each
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
3、迭代器
|
||||
3. 迭代器
|
||||
|
||||
迭代器从作用上来说是最基本的部分,可是理解起来比前两者都要费力一些。软件设计有一个基本原则,所有的问题都可以通过引进一个间接层来简化, 这种简化在 STL 中就是用迭代器来完成的。概括来说,迭代器在 STL 中用来将算法和容器联系起来,起着一种黏和剂的作用。几乎 STL 提供的所有算法都是通 过迭代器存取元素序列进行工作的,每一个容器都定义了其本身所专有的迭代器,用以存取容器中的元素。
|
||||
|
||||
Reference in New Issue
Block a user