C++面向对象编程Part II

C++程序设计兼谈对象模型 Part II

注:此篇文章是根据侯捷老师的课程所做的笔记。
本课程是上一门课程“面向对象程序设计”的续集,将探讨上一门课程未讨论的主题。上一门课程笔记:
 链接: C++面向对象编程 (C++Object-Oriented Programming) Part I.

 Conversion function(转换函数)

在这里插入图片描述
 上图中黄色背景的部分即为转换函数,转换函数通常的格式为:“operator type() const;”,函数没有参数,也没有返回类型,通常转换函数不会改变数据,函数后面加const。
他是如何被调用的呢?
 上图下方有**“double d = 4 + f”,首先编译器会在全局范围找是否重载了加号操作符“4 + f”,若没有此重载,则进一步找是否有转化函数,若转化类型后能进行此操作,那么就利用转换函数进行转换,上图中将调用operator double()**将f转为0.6。
 上图中存在一些错误,函数体内计算double值时会计算错误,并不会计算出0.6而是0.0。忽略其错误,感受其操作。

 Non-explicit one-argument constructor

在这里插入图片描述
 执行Fraction d2 = f + 4;,可以调用Fraction的构造函数将4转为Fraction(4, 1);,之后调用operator+将两个Fraction类加起来。
 与上一节的例子相反,上一节是将对象转为数,这节则是转为对象。
在这里插入图片描述
 在类内部添加一个转换函数Fraction d2 = f + 4;,编译器会报错,原因是编译器不知道是利用转换函数(double())将f转换为数还是利用构造函数将4转换为类对象,产生了二义性。
 我们注意到,在上一节中,类内也是有相同的构造函数和转换函数(double()),为什么上一节的代码就能通过呢?原因在于上一节的类内没有重载operator+,所以类之间不能进行加法操作,也就不能将4转为类对象,所以只有一条路可以走,那就是利用转换函数(double())将f转为数。
在这里插入图片描述
 这种将一个数转换为类对象的操作有时候是我们不需要的,我们需要杜绝这种现象,那就是在构造函数前面加explict关键字,这样就不会把其他类型转为该类的对象,只允许通过构造函数调用。
 上图中**Fraction d2 = f + 4;**编译出错,原因是4不允许被转换,只能f转换为double类型,那么f+4为double类型,d2也为double类型,d2不能转为Fraction类对象,所以编译出错。
explict关键字通常作用于构造函数前,用于禁止隐式的类型转换。
在这里插入图片描述
 上图为在标准库中使用转换函数的例子。

 Pointer-like classes,关于智能指针

在这里插入图片描述
 智能指针就是一个类,类里面包含了指针,同时也包含了一些其他功能。

 Pointer-like classes,关于迭代器

在这里插入图片描述
在这里插入图片描述
 迭代器封装了指针,相比于智能指针又重载了其他一些操作符。

 Function-like classes,所谓仿函数

在这里插入图片描述

 仿函数,实际上是一个类,执行时像函数,类里面通常重载了**()**操作符,这些类的对象又称函数对象,因为它们像函数。

 标准库中的仿函数的奇特模样

在这里插入图片描述
 仿函数继承了一些奇特的类。这些类(下图所示)没有数据,只有一些定义,所以类的大小为1。
在这里插入图片描述

 namespace经验谈

namespace的意义:
 为了把一些东西区分过来,避免类名、函数名和变量名的冲突,取个名字将它们包起来。
 下图就是namespace的定义以及如何使用。
在这里插入图片描述

 class template,类模板

在这里插入图片描述

 function template,函数模板

在这里插入图片描述
 函数模板可以不用显示说明参数类型,函数模板能自动推导,类模板则不行。为什么这样?
 若是类模板没有指定类型,那就会这样** complex a**,定义了一个对象,编译器不知道这个类的类型,不知道是int还是double,所以编译器不知道它的大小就没法创建内存,也就没法创建对象,所以类模板必须显示的说明类型。
 而函数模板不同,它可以自动推导出类型,并且调用函数是开辟一个栈,不用确定分配给它内存大小。

 member template,成员模板

在这里插入图片描述

 成员模板:在类模板中,成员函数是一个函数模板。在上图中,类模板pair,类的构造函数又是一个函数模板。
在这里插入图片描述

 上图中,**pari<Derived1, Derived2> p;执行的是类模板,其中T类型为Derived。接下来pari<Base1, Base2> p2§;**调用拷贝构造函数,这里T类型变为Base,U的类型为Derived。即将一个鲫鱼和麻雀构成的pair§,拷贝到一个由鱼类和鸟类构成的pair(p2)中,就是父类指针指向子类对象,这样是可以的。但反之是不可以的,因为鱼类不属于鲫鱼,鸟类也不属于麻雀。
在这里插入图片描述

 在智能指针类中,该类也封装了成员模板,为的是允许父类指针指向子类对象。
 成员模板通常使用在构造函数中,使用类使用更灵活,更有弹性。

 specialization,模板特化

在这里插入图片描述
 模板特化就是把泛华模板中的类型确定下来。
上图中的第一个框的内容就是泛华的模板,第二个框就是确定了类型之后特化的模板,注意形式:template <>,声明一下是模板,但特化后<>内不需要给泛化类型;函数名称后面加入特化的类型:hash<char>hash<\int>hash<long>,函数调用时如果类型在特化模板中有会直接模板特化后的类。**hans<long> () (1000);**会直接调用hash<long>模板,其中第一个小括号表示匿名类型,第二个小括号调用的是类中重载的()操作符。

  partial specialization,模板偏特化——个数的偏

在这里插入图片描述
 模板偏特化——个数的偏指的是在模板中,里面声明的类型有多个,其中将某些类型确定下来,例如上图中将bool类型确定下来,其余类型依然待确定。其中如果某些类型确定了,必须将确定的类型写在前面,将不确定的类型写在后面。

  partial specialization,模板偏特化——范围的偏

在这里插入图片描述
 模板偏特化——范围的偏指的是原来模板类型是任意的,现在限制类型的范围,不让它再是任意类型了,上图中将任意类型限制在指针范围内,其中**C<string> obj1;**调用的是第一个类模板,**C<string*> obj1;**调用的是第二个类模板。

 template template parameter,模板模板参数

 模板模板参数指的是模板的参数为模板。
在这里插入图片描述
 上图中,模板类XCLs第一个模板参数为类型T,第二个参数为容器类的模板,在模板内部定义的对象Container<T> c,T类型就是模板的第一个参数,Container为第二个参数的容器类型。在定义时XCLs<string, List> mylst1;,表示XCLs为List容器,容器里面的类型为string,但这样定义是错误的,原因是容器需要好几个模板参数,第二种定义是正确的。
在这里插入图片描述
 以上是关于智能指针的定义。
在这里插入图片描述
 在上面图片的例子中,该类模板不属于模板模板参数,原因在于定义**stack<int, list<int>>时,其中内部第二个参数list<int>**已经被写死了,不是模板类型了。

 variadic templates(since C++11)

在这里插入图片描述
 variadic templates为数量不定的参数模板,参数不确定个数用**"…"**表示。
 上图中,模板print含有一个类型T以及不确定个数的类型Types,其中关键字typename后面有三个句号,函数内部打印第一个参数,然后递归调用自己,让剩下参数的第一个成为typename T类型,打印它并继续进行递归,直到没有参数了,print将调用上面重载的函数结束此次运行。

 auto(since C++11)

在这里插入图片描述
 关键字auto能够自动推导出变量类型,当你想少写些代码或者不关心它的类型的时候,可以使用auto。但是不能用auto去定义类型,就像上图中下面为变量ite定义的操作,这样时错误的。

 ranged-base for(since C++11)

在这里插入图片描述
基于范围的for循环:
 如上图,for循环的新语法,可以挨个遍历容器,并且可以通过值或者通过引用来遍历容器内的元素。如果需要改变容器内的元素时,可以通过引用遍历来解决,语法是在类型后面加&,如auto&

 reference

在这里插入图片描述
 建议引用在定义时一定要初始化,它要绑定到一个地址上,并且绑定完之后只能指向这个地址,不能改变,是一个指针常量。
在这里插入图片描述
 对象和其引用的大小地址是相同的,这其实是一种假象,引用就是指针,在函数传递时也不会传递对象对象大小的内存,就是指针的大小,4字节(32bit)或8字节(64bit)。
在这里插入图片描述
 上图是分别以指针、值、引用传递时的定义以及调用时的语法。
 引用通常不用来声明变量,而是用于参数传递和返回类型的描述。
 在上图中下方的部分,定义了两个重载函数,它们传递的参数类型不同,一种是引用一种时值,这种重载是不允许的,因为在调用时,它们的语法是一样的,编译器不能分辨该使用哪个函数。但重载指针传递与值或重载指针传递与引用传递是可以的。
 另外,const关键字(写在函数括号外面,上图中的灰色区域)可以用于重载函数的区分,调用时可以用常量进行区分。

#include<iostream>  
using namespace std;  
   
class Test  
{  
protected:  
    int x;  
public:  
    Test (int i):x(i) { }  
    void fun() const  
    {  
        cout << "fun() const called " << endl;  
    }  
    void fun()  
    {  
        cout << "fun() called " << endl;  
    }  
};  
   
int main()  
{  
    Test t1 (10);  
    const Test t2 (20);  
    t1.fun();  //调用void fun() 函数,打印fun() called
    t2.fun();  //调用void fun() const ,打印fun() const called 
    return 0;  
}

 但,const用于修饰函数参数传递时不可以重载,即void fun(int a)和void fun(const int a);,这两个函数实际上没有区别,因为函数调用的时候,存在形实结合的过程,所以不管有没有const都不会改变实参的值。
详细请看:链接: C++中const用于函数重载.

 Composition(复合)关系下的构造和析构

这一部分在part I中有描述,这里不再详细描述。
 构造:先复合,后自己
 析构:先自己,后复合

 Inheritance(继承)关系下的构造和析构

 构造:先继承,后自己
 析构:先自己,后继承

 Inheritance+Composition关系下的构造和析构

 构造:先继承,再复合,后自己
 析构:先自己,再复合,后继承

 对象模型(Object Model):关于vptr和vtbl

在这里插入图片描述
关于虚指针和虚函数表:
 如上图右侧,观察它们的继承关系,以及它们内部的函数,一共有8个函数,其中四个非虚函数,4个虚函数,它们放在内存中的不同地方,在继承关系中,子类继承了父类函数的调用权而不是函数的大小。
 在含有虚函数的类中,创建的类对象都有一个虚指针,这个虚指针指向虚函数表,虚函数表记录了虚函数的地址。在对象调用函数的过程中,虚指针指向虚函数表,再通过虚函数表内的地址找到虚函数,完成虚函数的调用。调用是编译器内部的代码:(* (p->vptr) [n]) §;(* p->vptr [n]) §;
在这里插入图片描述
 在上图右侧有类的继承关系。子类继承了父类并重写了父类的虚函数。由于子类跟父类大小并不一样,所以在容器中不能同时存放它们(容器要求存放类型大小一样的对象),故在容器中存放指针,该指针为父类的指针,父类指针可以指向子类对象,这样父类和子类都可以存储。
 在通过指针调用虚函数的过程中,整个程序走的路线跟之前一致。

 对象模型(Object Model):关于this

在这里插入图片描述
 函数调用的路线在part I部分有描述。

 对象模型(Object Model):关于Dynamic Binding

动态绑定的三个条件:
1、指针操作
2、向上转型
3、调用虚函数
在这里插入图片描述
 对象a调用虚函数,该绑定是静态绑定,因为a是对象,不是指针。在汇编语言中写成call xxx。
在这里插入图片描述
 pa调用虚函数属于动态绑定,其中pa是指针,它们向父类转型,且调用虚函数。
为什么动态绑定要求指针?
 编译器在编译的时候不清楚pa指针的类型,所以在调用虚函数的时候就不能确定调用的哪个函数,所以只能晚绑定(指针指向函数的绑定,到底走哪条路?),即动态绑定。对象调用虚函数时,已经确定类型了,所以直到要走哪条路。
为什么动态绑定要求向上转型?
 父类指针可以指向子类的对象,在编译时编译器不清楚指针具体类型,也还是不知道绑定函数走哪条路。
为什么动态绑定要求调用虚函数?
 若不是调用虚函数,非虚函数是类里面特有的,直接有地址,直接可以找到它,也就是只有一条路能走。
总之:动态绑定的核心就是:不知道走哪条路。

 谈谈const

在这里插入图片描述
 上图所示,非常量对象可以调用常量成员函数也可以调用非常量成员函数;常量对象只能调用常量成员函数。
 上图右侧对operator[]进行了重载,由于const也属于区分重载的一部分,所以上面两个函数可以同时存在。当成员函数的常量版本和非常量版本同时存在时,常量对象只能调用常量成员函数版本,非常量对象只能调用非常量成员函数版本。

 关于new,delete

在这里插入图片描述

 这一部分在part I中有描述。

  重载 ::operator new/new[],::operator delete/[]

在这里插入图片描述
::operator new指的是重载全局函数。
 operator new传入参数为内存大小,operator delete传入参数为要删除的指针类型和内存大小(可选,可以不传)。

  重载 member operator new/delete

在这里插入图片描述
 重载成员函数,在调用时会调用重载的成员函数。

  重载 member operator new[]/delete[]

在这里插入图片描述
示例

在这里插入图片描述
 类内重载了new、new[]、delete、delete[]。
在这里插入图片描述
 没有虚函数的类对象大小是125+4,(4为编译器记录数组元素个数);
 有虚函数的类对象大小是12
5+4+4,(4为虚函数指针);
注意:
 构造和析构的顺序:如图,创建时,数组中对象从上向下调用构造函数,删除时,数组中对象从下向上调用析构函数。
在这里插入图片描述
 上图所示,调用的是全局默认的new[]和delete[]。

  重载new(),delete()

在这里插入图片描述

示例

在这里插入图片描述
在这里插入图片描述

  basic_string使用new(extra)扩充申请量

在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值