Object Slice(对象切割),很令人头晕的东西!

C++多态与对象切割
本文探讨了C++中的对象切割现象与多态机制,通过实例解析了对象切割如何影响派生类对象传递给基类的过程,以及如何正确利用多态特性。

 #include <iostream.h>
class A
{
public:virtual void print(){cout<<"A::print()"<<endl;}
};

class B:public A
{
public:virtual void print(){cout<<"B::print()"<<endl;}
};

class C:public A
{
public:virtual void print(){ cout<<"C::print()"<<endl;}
};

void printfun(A a)
{
a.print();
}
void main()
{
A a;
B b;
C c;

printfun(a);
printfun(b);
printfun(c);
}
将派生类operator=给基类的一个引用,对象会被切割!导致派生类的信息会被切掉!

输出结果:
A::print()
A::print()
A::print()

因为printfun的参数是A,而不是A&,所以这里是object slice而不是多态起作用。

如果是void printfun(A& a)
{
a.print();
}
就会输出
A::print()
B::print()
C::print()
那就用到虚函数了,如果父类是虚函数,那么子类有的就调用子类,子类没有的就调用父类.

来看另一道题目:

#include   <iostream>  
  using   namespace   std;  
   
  class   Base  
  {  
  public:  
  Base()  
  {  
  cout   <<   "Base   ctor()   "   <<   endl;  
  }  
  virtual void   print()  
  {  
  cout   <<   "Base::print()"   <<   endl;  
  }  
  };  
   
  class   Derived:public   Base  
  {  
  public:  
  Derived()  
  {  
          cout   <<   "Derived   ctor()"   <<   endl;  
  }  
  void   print()  
  {  
  cout   <<   "Derived::print()"   <<   endl;  
  }  
   
   
  };  
   
  int   main()  
  {  
  Base         _base;  
  Derived   _derived;   
  Base*   pBase   =   new Derived();  
  pBase->Base::print();  
  pBase->print();  

return   0;
  }

输出结果:

Base   ctor()
Base   ctor()
Derived   ctor()
Base   ctor()
Derived   ctor()
Base::print()
Derived::print()
Press any key to continue
若改成如下代码:

Base&   pBase   =   _derived;  
  pBase.Base::print();  
  pBase.print();  
  //pBase.Derived::print(); 报错 

输出结果:

Base   ctor()
Base   ctor()
Derived   ctor()
Base::print()
Derived::print()
Press any key to continue
由此可见,

Base*   pBase   =   new Derived();  等价于Base& pBase = _derived;也等价于 Base* pBase = &_derived;

在看一道例题:

#include   <iostream.h>  
   
  class   A  
  {  
  public:  
   void   Mark()   {cout<<"A::Mark"<<endl;}  
  };  
   
  class   B   :   public   A  
  {  
  public:  
  void   Mark()    {   cout<<"B::Mark"<<endl;   }  
  };  
   
  int   main()  
  {  
  B   b;  
  b.A::Mark();  
  b.B::Mark();  
  B*   pB   =   &b;  
  pB->A::Mark();                   
  pB->B::Mark();                 
  A*   pA   =   pB;                 
  pA->A::Mark();  
  //pA->B::Mark();  
   
  return   0;  
  }  

输出结果:

A::Mark
B::Mark
A::Mark
B::Mark
A::Mark
Press any key to continue
但通過派生類對象的指針或引用來調用虛函數﹐則不會發生object   slice﹐但是若直接將派生類對象傳給基類型參數則會發生此事。  

pA是指向A的指针,他不能操作其他类的成员(包括它的派生类成员)    不管是用指针,还是引用,只要是对成员的访问都要在编译期确定。    pA->B::Mark();   //出错的原因是找不到   A::B::Mark这个成员   即使Mark是虚函数,也不能这样使用  B::Mark始终不是A的成员   虚函数只是用于保留接口名。   
    如果Mark是虚函数   pA->Mark();   在编译期看上去是调用的A::Mark,但是在运行期确是调用的B::Mark

可以看一下,下面的程序:

#include   <iostream>  
  using   namespace   std;  
  class   Base  
  {  
  public:  
  Base()  
  {  
  }  
   
  };  
   
  class   Derived:public   Base  
  {  
  public:  
  Derived()  
  {  
  }  
  virtual   void   print()  
  {  
  cout   <<   "Derived::print()   called"   <<   endl;  
  }  
  };  
   
  int   main()  
  {  
  Base*   pBase   =   new   Derived();  
  pBase->print();  
  return   0;  
   
  }
结果如下:

error C2039: 'print' : is not a member of 'Base'

首先来看看代码  
  1、有Base::print()这个成员吗?  
  很明显,所以不能用Base*指针去访问print()。解决方法是,如果你不想给Base提供print()方法,那么就可以给Base定义一个纯虚函数,用以保留一个接口。但是在这里,你的Base看上去或在用途上不能保证是一个抽象的基类,所以最好定义成虚函数,而不是纯虚函数。  
  #include   <iostream>  
  using   namespace   std;  
  class   Base{  
  public:  
  Base(){}  
                    virtual   void   print(){}   //这里  
  };  
   
  class   Derived:public   Base{  
  public:  
  Derived(){}  
  virtual   void   print(){  
  cout   <<   "Derived::print()   called"   <<   endl;  
  }  
  };  
  //调用  
  Base*   pBase   =   new   Derived();  
  pBase->print();   编译器找到Base::print()这个接口,所以正确。而对于到底是调用哪个print()会在运行期由vptr决定。  
Q2:vptr和vtbl都是在compile期生成的,并且在执行期不能被改变,vptr的初始化一般发生在对象的ctor中;  
  Q3:必须提出一点,一般性的继承和虚拟继承在实现上有着较大的差别,所以必须分开讨论。  
          1.首先我们来看,一般性的继承。此时,我们可以说,每个对象只能拥有一对vtbl和vptr,所以,是不能单纯的使用copy得到的,两者将在对象的ctor中设置,并且对virtual   function在vtbl中的索引位置以及值都会根据子类对父类中重新定义virtual   function的情况加以调整,但有一点必须注意的是destructor,每个类的vtbl中,如果存在这一项,它都将指向被该类定义的destructor,而非父类的;如果没有定义,对不起,可能出错(视不同的编译器而定)

2.再来看虚拟继承和多继承的情况,嗯,这可是个“大”问题,呵呵,在cpp中,有很多的“凡是”规则可以遵循,比如凡是多态的实现必须借助于“指针”或“索引”完成等等,但其中很多一遇到这个“virtual   base   class”,尤其是带有“菱形”的继承结构,就玩完!:(在这样的继承系统之中,对象是可能拥有一个以上的vtbl的,呵呵应该是vtbls和vptrs了,一般是每个父类对应一个vtbl和一个vptr。说了怎么多废话,但关键的在下面:由于无论时多继承也好,虚拟继承也好,它们必须支持“多态”这个关键性质,这就意味着,它们必须能完成下面的任务:凭借指向父类的指针、引用能完成对子类中成员函数的调用。所有,此时,父类的虚表必须根据其它父类和子类的情况进行“调整”,这种调整将在子类的ctor中完成(可能会进一步借助与父类的ctor),也就是说拥有相同父类的子类对象其所含的父类对应的vtbl的内容将可能是不同的,必须在其各自的ctor中加以调整。

对于单一的继承并且是非virtual继承的类而言,其只能拥有一个vtbl,我想这是来自base的,但注意并不是原来的那个,而是经过调整过得,比如Derived   class对base的声明或定义的virtual   function进行了重载等,至少对应于destructor的那一项已经变成了Derived的了。所以,对应特定的类,这个表也只要有一个就可以了,因而不存在对象之间vtbl的copy问题,而vptr可以认为是类的一个“指针”成员,不过其在对象之间的拷贝由编译器插入一定的代码加以完成

“对象切割”是一个基于“内存”操作上的概念,楼主最初的程序之所以有问题,并非由于内存的变化引起的,引起变化的只是对某块内存的“解释”上;因而很难和切割有什么瓜葛。  
  当Base&   pBase   =   _derived;被写下时,我们必须清楚从compiler的角度上是如果看待pBase的,它是类型Base的一个引用,使用它来标记_derived所对应的那块“内存”,我们将只能看到_derived中属于Base的那部分,由于vtpr一般位于Base所在的那块内存中,并且已经经过compiler的调整(根据Derived中对virtual   function的重载情况),所以“多态”可以发生,但对应Base之外的那些内容,在compiler来说,对不起,单使用pBase是看不到的。


在 Python 中,**`slice`** 是一个内置的类(`built-in class`),它用于表示切片操作。我们平时写的 `lst[1:5:2]` 实际上是 Python 解释器将这个语法转换成一个 `slice` 对象,然后传递给对象的 `__getitem__` 或 `__setitem__` 方法来处理。 --- ### 一、什么是 `slice` 对象? `slice(start, stop, step)` 构造一个切片描述符,它不直接操作数据,而是**描述如何切片**。 你可以把它理解为:“我想从哪里开始,到哪里结束,以什么步长取元素”。 #### 创建 slice 对象的语法: ```python sl = slice(start, stop[, step]) ``` - `start`:起始索引 - `stop`:结束索引(不包含) - `step`:步长(可选,默认为 `None`) > 注意:`slice` 对象本身不会作用于任何列表或字符串,它只是一个“指令包”,需要被应用到某个序列上。 --- ### 二、手动创建并使用 `slice` 对象 #### 示例代码: ```python # 定义一个列表 lst = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] # 手动创建一个 slice 对象:等价于 [2:7:2] sl = slice(2, 7, 2) # 使用该 slice 对象进行切片 result = lst[sl] print(result) # 输出: [2, 4, 6] ``` 这与下面的写法完全等价: ```python result = lst[2:7:2] print(result) # 同样输出: [2, 4, 6] ``` --- ### 三、`slice` 对象的属性 每个 `slice` 实例都有三个只读属性: - `.start` - `.stop` - `.step` 可以用来查看其内容: ```python sl = slice(2, 7, 2) print(sl.start) # 2 print(sl.stop) # 7 print(sl.step) # 2 ``` 如果省略参数(如 `[:]`),则对应值为 `None`: ```python sl = slice(None, None, -1) print(sl) # slice(None, None, -1) ``` 这正好对应 `[::-1]` —— 反转整个序列。 --- ### 四、实际用途:何时要手动创建 `slice`? 虽然大多数时候我们直接用 `[::]` 语法就够了,但在以下场景中,手动创建 `slice` 很有用: #### ✅ 1. 封装重复的切片逻辑 ```python # 比如你有很多数组都需要提取中间偶数位 middle_even = slice(2, 8, 2) a = [0,1,2,3,4,5,6,7,8,9] b = ['a','b','c','d','e','f','g','h'] print(a[middle_even]) # [2, 4, 6] print(b[middle_even]) # ['c', 'e', 'g'] ``` 这样代码更清晰且可复用。 --- #### ✅ 2. 在自定义类中支持切片 当你实现自己的序列类时,可以用 `slice` 来处理 `__getitem__`: ```python class MySequence: def __init__(self, data): self.data = data def __getitem__(self, key): if isinstance(key, slice): print(f"切片请求: start={key.start}, stop={key.stop}, step={key.step}") return self.data[key] else: return self.data[key] seq = MySequence([10, 20, 30, 40, 50]) print(seq[1:4]) # 切片请求... 输出: [20, 30, 40] print(seq[slice(1,4)]) # 等价写法 ``` --- #### ✅ 3. 动态生成切片规则(比如配置驱动) ```python def make_slice(config): if config == "first_half": return slice(0, 5) elif config == "second_half": return slice(5, 10) elif config == "reversed": return slice(None, None, -1) elif config == "every_other": return slice(0, None, 2) # 使用 data = list(range(10)) sl = make_slice("every_other") print(data[sl]) # [0, 2, 4, 6, 8] ``` --- #### ✅ 4. NumPy 和 Pandas 中的高级索引基础 虽然这些库有自己的优化机制,但底层仍基于类似的切片对象思想。例如: ```python import numpy as np arr = np.arange(10) sl = slice(1, 8, 3) print(arr[sl]) # [1 4 7] ``` --- ### 五、注意事项 | 要点 | 说明 | |------|------| | `slice` 不检查越界 | 它只是描述范围,真正访问时才报错(但对列表切片也不会报错) | | 支持负数 | `slice(-5, None)` 表示最后五个 | | 不支持负步长下的自动推断方向 | 必须确保 `start > stop` 当 `step < 0` | ```python sl = slice(8, 2, -2) print(lst[sl]) # [8, 6, 4] —— 正确 ``` --- ### 总结 手动创建 `slice` 对象是一种将“切片行为”抽象出来的编程技巧。虽然日常开发中很少显式使用,但在构建通用函数、框架或处理动态索引时非常强大。 > 💡 核心理解: > `lst[1:5:2]` → 是语法糖 → 等价于 `lst[slice(1, 5, 2)]` ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值