1. 目标

  • 泛型编程(Generic Programming)和面向对象编程(Object-Oriented Programming)虽然分属不同思维,但它们正是C++的技术主线,所以本课程也讨论template(模板)。
  • 深入探索面向对象之继承关系(inheritance)所形成的对象模型(Object Model),包括隐藏于底层的this指针,vptr(虚指针),vtbl(虚表),virtual mechanism(虚机制),以及虚函数(virtual functions)造成的polymorphism(多态)效果。

    2.转换函数conversion function

    2.1 operator double() const {}

    1. class Fraction
    2. {
    3. public:
    4. Fraction(int num, int den = 1) : m_numerator(num), m_denominator(den) { }
    5. operator double() const { // 转换函数:无参数;返回类型:防止写矛盾,所以不用写
    6. return ((double)m_numerator)/m_denominator;
    7. }
    8. private:
    9. int m_numerator; // 分子
    10. int m_denominator; // 分母
    11. }

    转换函数不可以有参数,也不需要返回值。通常都会是const.

    2.2 non-explicit one argument ctor

    class Fraction
    {
    public:
    Fraction(int num, int den=1)//没有explicit,一个参数,的构造函数
      : m_numerator(num), m_denominator(den) {}
    
    Fraction operation + (const Fraction& f){
      return Fraction(......);
    }
    private:
    int m_numrator;
    int m_denominator;
    };
    

    Fraction f(3, 5); Fraction d2 = f + 4;//调用non-explicit cotr将4转为Fraction(4),然后调用operator+

    2.3 conversion function vs. non-explicit one argument ctor

    Fraction f(3, 5); Fraction d2 = f + 4;//[Error] ambiguous

  • /编译器方法1:先找全局函数+,第一个参数是整数或者浮点数,第二个参数是Fraction → ng. 可以将4转换为Fraction,发现有non-explicit-one-argument构造函数,行的通

  • 编译器方法2:找Fraction转换成double的方法 → ok. 将f转换为double,发现有转换函数,也行的通

因此编译器会报歧义的错误,ambiguous

2.4 explicit-one-argument ctor

class Fraction
{
pulbic:
  explict Fraction(int num, int den=1)//explict表示明确的.
  //告诉编译器,既然设计成构造函数,就应该以构造函数的形式来用;告诉编译器不要自动帮我做事情,等我真的调用构造函数的时候你再调用
  //不可以自动将4,变成1/4;
    : m_numerator(num), m_denominator(den) {}
  operator double() const{
    return (double) (m_numerator / m_denominator);
  }
  Fraction operator+(const Fraction& f){
    return Fraction(......);
 }
private:
  int m_numerator;
  int m_denominator;
};

Fraction f(3, 5); Fraction d2 = f + 4;
//[Error]conversion from ‘double’ to ‘Fraction’ requested
explicit这个关键字,90%用在构造函数前面。

2.5 STL中实例

20200411173550995.png

3. 智能指针与仿函数

3.1 pointer-like classes,关于智能指针

  • pointer-like classes中一定有一个指向某个类的对象的指针

    template<class T>
    class shared_ptr
    {
    public:
    T& operator*() const//操作符*重载
    { return *px;}
    T* operator->() const//操作符->重载
    { return px;}
    
    shared_ptr(T* p) 
      : px(p)
    {}
    private:
    T*    px;//px是一个指针
    long* pn;
    ......
    };
    

    20200413233051918.png
    迭代器
    不止有* -> 还有++ —等
    关于迭代器.png
    20200413233051918.png

    3.2 function-like classes,所谓仿函数

    对操作符()的重载

继承一些奇怪的base classes

4. namespace

5. template

5.1 class template,类模板

5.2 function template,函数模板

类模板使用时要指定类的类型,函数模板使用时不用指定,因为函数模板在使用时,一定是在调用它,调用时会放参数,编译器会对函数模板进行实参推导。
模板的编译,编出来也只是一个半成品,不能保证后面一定会成功,即后面在使用时,会在编译一次。

5.3 member template, 成员模板

20200417004915666.png
黄色部分的构造函数pair是一个成员模板。
20200417005013456.png
Derived1—>Base1(鲫鱼—>鱼类)Derived2—>Base2(麻雀—>鸟类)
可以将一个由鲫鱼和麻雀构成的pair,放进(拷贝到)一个由鱼类和鸟类构成的pair中;反之不行。在标准库中大量的类的构造函数写成成员模板的形式,就是为了让构造函数更有弹性。
20200417005519891.png

6. specialization, 模板特化

6.1 模板特化

范化就是模板,是特化的反面 特化是对某种特殊类型的处理 20200418210509906.png
hash()表示一个临时对象。

6.2 模板偏特化 partial specialization

  • 个数上20200418210908697.png

注意:特化的参数,需要从左边到右边连续的,比如有五个模板参数,不能特化1 3 5参数,留下2 4 参数

  • 范围上

范围缩小成指针

  • 20200418211244167.png

    6.3 template template paremeter 模板模板参数

    2020041822074978.png
    XCls mylst2可以看出,Lst还是模板,类型是不确定的。
    20200418220805916.png
    //C++2.0的语法
    template<typename T>
    using Lst = list<T, allocator<T>>;
    
    只有在 template<>中,class和typename可以共通 。

这不是template template parameter
template template parameter 3.png
在使用时,如果要写出第二参数,写法:stack> s2可以看出list已经不是模板了,list已经绑定了。

7. C++标准库20200418223502840.png

8. C++11 三个主题

8.1 variadic templates
数量不定的模板参数

#include <iostream>
#include <bitset> 

using namespace std;

void print() {
    cout << __cplusplus << endl;
}

template<typename T, typename... Types>
void print(const T& firstArg, const Types&... args) {
    cout << "firstArg = " << firstArg << "   sizeof...(args) = " << sizeof...(args)<< endl;
    print(args...); // 有种递归的感觉,但是每次递归的参数类型是变化的
}

int main() {
    print(7.5, "hello", bitset<16>(377), 42); //bitset中应该重载<<
    return 0;
}

20200419183609205.png
sizeof…(args)可以算出pack中有几个参数。
标准库中很多代码都用这个语法重写了。
8.2 auto
不赋值的时候不能用auto,编译器无法推断类型。
初学者最好少用auto,要自己清楚自己每个变量的类型。2020041918390427.png
8.3 for range循环
20200419194046140.png

9. reference

9.1 概念

引用:变量或对象的别名。
1:声明引用时必须显式初始化赋值,且设定初值后不可以再改变引用所代表的变量或对象,但可以修改其值。
2:引用与指针不等价,但大多数编译器底层使用指针实现引用
3:变量或对象和其引用的内存大小相同地址相同。(均为假象
4:Java中,所有变量均为引用(reference)

9.2 常见用途

常用于参数传递(函数参数类型和返回类型的表示)
image.png

  • 同名函数的签名,形参列表的非引用版本和引用版本不可同时存在,不构成函数重载。否则存在歧义,编译器不清楚应该调用哪个函数。
  • const是函数签名的一部分,未使用const的版本和使用const的版本可同时存在,构成函数重载。所以const可以用来区分两个函数。

const 表示该成员函数不可以修改成员数据,所以该函数隐形的this指针会变成底层const指向常量的指针:代表不能改变其指向内容的指针
double imag ( T const * this, const double& im) {};
这样一来,如果实参是const 那就只能调用const成员函数;
而&相当于顶层const(常量指针,不可以改变指向)

10. 继承与组合关系下的构造和析构

10.1 继承关系下

  • 继承关系:从内存/数据的角度而言,子类对象包含父类的成分。
  • 构造函数的调用:由内向外

子类/派生类的构造函数先调用父类/基类的默认构造函数(由编译器完成),再执行本身。
Derived::Devired(…) : Base() { … } // 编译器自动调用父类默认构造

  • 析构函数的调用:由外向内

子类/派生类的的析构函数先执行本身,再调用父类/基类的析构函数(由编译器完成)。
Derived::~Devired(…) { … ~Base() } // 执行完子类析构后,再执行父类析构

  • 父类/基类的构造函数必须为虚函数,由子类进行重写,否则会导致未定义的行为 undefine behavior。

    10.2 组合关系下

  • 组合/复合关系:从内存/数据的角度而言,Container对象包含Component类的成分。

  • 构造函数的调用:由内向外

Container的构造函数先调用Component的默认构造函数(由编译器完成),再执行本身。
Container::Container(…) : Component() { … } // 编译器自动调用Component类默认构造

  • 析构函数的调用:由外向内

Container::~Container(…) { … ~Component() } // 执行完Container类析构后,再执行Component类析构

10.3 继承和组合关系下

  • 从内存/数据的角度而言,Devired对象包含父类和Component类的成分
  • 构造函数的调用:由内向外

Devired的ctor先调用Base的默认构造函数,再调用Component的默认构造函数,最后执行本身。
Devired::Devired(…) : Base(), Component() { … }

  • 析构函数的调用:由外向内

Devired的析构函数先执行本身,再调用Component的析构函数,最后调用Base的析构函数。
Devired::~Devired(…) { … ~Component(), ~Base() }

11. 对象模型(Object Model):虚指针vptr和虚表vtbl

11.1 定义

虚指针(virtual pointer):当类包含虚函数时,其对象包含指向虚函数表的虚指针。
虚函数表/虚表(virtual function table):存储的元素均为函数指针,指向虚函数的内存地址。
注:继承虚函数,是继承函数的调用权,而不是函数所占的内存大小。
image.png
只要类中有虚函数,类的对象里面就多一个指针;
动态绑定的过程:指向对象的指针,依次通过虚指针、虚表来调用虚函数。

11.2 静态绑定(C语言) vs. 动态绑定(C++语言)

  • 静态绑定(C语言):编译器针对调用的动作,编译为CALL_XXX(地址),当调用某个函数时,编译器进行解析并跳转至对应地址,随后return返回。 通过对象。
  • 动态绑定(C++语言):通过指向对象的指针(可向上转型,即多态),依次查找虚指针、虚函数表及指向的虚函数地址。指向当前对象的指针即this指针。

    11.3 虚机制

    满足动态绑定的3个条件时,编译器会以动态绑定的形式进行编译。

  • 通过指向对象的指针调用;

  • 指向对象的指针向上转型(up-cast),即多态(polymorphism);
  • 调用虚函数。

使用C语言的语法,实现的动态绑定:
((p->vptr)[n])(p);或( p->vptr[n])(p);
注:1. 具体调用哪个类的虚函数,由传入对象的具体类型决定。
2. 多态、虚函数、动态绑定(虚指针与虚函数表)为同一概念。

示例:存储不同形状的容器

  • 不同形状对应的类,其对象所占用的内存大小可能不同。
  • C++容器只能存储相同类型的元素。

为保证容器可以存储不同形状的对象,需将容器的元素类型定义为指向形状基类的指针类型
例如:list shapeList;
image.png

11.4. 对象模型(Object Model):this指针

this指针:当前调用成员变量或成员函数的对象的地址。
注1:C++中所有成员函数均包含隐藏的this指针,表示当前调用该成员函数的对象。
注2:this指针即指向当前对象的指针,可通过动态绑定,依次查找虚指针、虚表及指向的虚函数地址。
虚函数应用的场景:

  • 多态(Polymorphism)
  • 模板方法(Template Method):在抽象基类中定义整个算法流程,而将若干具体实现步骤(通过使用虚函数)延迟到子类中。模板方法使得子类可以不改变抽象基类的算法流程,即可重定义该算法的某些特定步骤(重写虚函数)。不同的子类能够以不同的逻辑实现特定步骤,而不影响抽象基类的算法流程。

image.png

12. const

常成员函数:const修饰的成员函数,只访问且不修改类的数据成员。
语法:T func() const {}
image.png
当成员函数的const版本和non-const版本同时存在时(即函数重载):

  • 常对象(const object)只会(只能)调用成员函数的const版本;
  • 非常对象(non-const object)只会(只能)调用成员函数的non-const版本。

注:非常成员函数(non-const)可调用常成员函数(const),反之不行。否则报错:connot convert ‘this’ pointer from ‘const class X’ to ‘class X &’. Conversion loses qualifiers.(转换丢失限定符)


示例:
template class std::basic_string<..> 包括2个同名成员函数。
STL字符串类通过引用计数实现,对象拷贝后共享同一份数据内容。
当涉及共享操作时,需考虑数据修改的问题。

/* const版本 */
// 常量字符串调用const版本,无需考虑COW
charT operator[](size_type pos) const 
{ 
    /* 不必考虑COW(Copy On Write) */
}

/* 非const版本 */
// 非常量字符串调用非const版本,需考虑COW
reference operator[](size_type pos)
{ 
    /* 必须考虑COW(Copy On Write) */
}

注:const属于函数签名的一部分,使用和未使用const的同名函数构成函数重载。

13. new 和delete的补充

有三种new: plain new、array new、placement new。

13.1 new表达式和delete表达式

new表达式:先分配内存,再调用构造函数。

Complex* pc = new Complex(1, 2);
/* 编译器内部实现 */
Complex* pc;

//1.分配内存
//new操作符内部调用malloc()函数
void* mem = operator new(sizeof(Complex));

//2.静态类型转换
pc = static_cast<Complex*>(mem);

//3.调用构造函数
//pc调用构造函数,则pc即隐藏的this指针
pc->Complex::Complex(1, 2);

delete表达式:先调用析构函数,再释放内存。

String* ps = new String("Hello");
...
delete ps;

/* 编译器内部实现 */
//1.调用析构函数
String::~String(ps);
//2.释放内存
//delete操作符内部调用free(ps)函数
operator delete(ps);

其中 operator new()和operator delete()函数可重载。

13.2 重载全局函数::operator new、::operator delete、::operator new[]、::operator delete[]

重载后的全局函数由编译器调用。

void* myAlloc(size_t size)
{
    return malloc(size);
}

void myFree(void* ptr)
{
    free(ptr);
}

/* 重载全局函数::operator new */
inline void* operator new(size_t size)
{
    cout << "self global new()" << endl;
    return myAlloc(size);
}

/* 重载全局函数::operator new[] */
inline void* operator new[](size_t size)
{
    cout << "self global new[]()" << endl;
    return myAlloc(size);
}

/* 重载全局函数::operator delete */
inline void operator delete(void* ptr)
{
    cout << "self global delete()" << endl;
    myFree(ptr);
}

/* 重载全局函数::operator delete[] */
inline void operator delete[](void* ptr)
{
    cout << "self global delete[]()" << endl;
    myFree(ptr);
}

13.3 重载成员函数operator new/delete、operator new[]/delete[]

重载成员函数operator new/delete、operator new[]/delete[],可改变调用者的行为。

class Foo {
public:
    /* per-class allocator */
    void* operator new(size_t);
    void operator delete(void*, size_t /* optional */);
};
/* 调用成员函数operator new */
Foo* p = new Foo;

/* 调用成员函数operator new的底层步骤 */
try{
    // 1.调用重载后的成员函数operator new
    void* mem = operator new(sizeof(Foo));
    // 2.静态类型转换:void* → Foo*
    p = static_cast<Foo*>(mem);
    // 3.调用构造函数
    p->Foo:Foo();
}
/* 调用成员函数operator delete */
delete p;

/* 调用成员函数operator delete的底层步骤 */
// 1.调用析构函数
p->Foo:~Foo();
// 2.调用重载后的成员函数operator delete
operator delete(p);

image.png
重载成员函数operator new[]/delete[]

class Foo {
public:
    /* per-class allocator */
    void* operator new[](size_t);
    void operator delete[](void*, size_t /* optional */);
};
/* 调用成员函数operator new[] */
Foo* p = new Foo[N];

/* 调用成员函数operator new[]的底层步骤 */
try{
    // 1.调用重载后的成员函数operator new[]
    void* mem = operator new(sizeof(Foo) * N + 4);
    // 2.静态类型转换:void* → Foo*
    p = static_cast<Foo*>(mem);
    // 3.调用构造函数
    p->Foo:Foo();    /* 调用N次构造函数 */
}
/* 调用成员函数operator delete[] */
delete[] p;

/* 调用成员函数operator delete[]的底层步骤 */
// 1.调用析构函数
p->Foo:~Foo();    /* 调用N次析构函数 */
// 2.调用重载后的成员函数operator delete
operator delete(p);

image.png
13.4 示例
示例1:调用全局函数及成员函数的operator new/delete
若调用者使用全局函数::operator new/delete,则编译器底层调用全局的void ::operator new(size_t)或void operator delete(void)。
image.png
示例2:调用重载的成员函数operator new[]/delete[]
image.png
示例3 调用重载的全局函数::operator new[]/delete[]
image.png

13.5 重载placement new / placement delete,即new() delete()

类成员函数operator new()可进行函数重载,需满足如下条件:

  • 每个重载版本的函数声明,必须有唯一的参数列表(不同重载版本的参数列表不能相同);
  • 参数列表的第1个参数,必须是size_t类型;
  • 参数列表的其余参数,以new所指定的placement argument为初值,即出现在new(…)小括号中的参数。

类成员函数operator delete()可进行函数重载(非强制),但重载版本一定不会被delete所调用。仅当new所调用的构造函数抛出异常(Exception),才会调用重载版本的operator delete()。
主要作用是释放未完全创建成功的对象所占用的内存。
注:operator delete()即使未与operator new()一一对应,也不会出现任何报错,编译器会视为:放弃处理构造函数抛出的异常。

示例:refernence counting
image.png