C++程序设计(二)

侯捷老师,主要讨论模板泛型编程和虚函数的一些机制。

1.10 转换函数 operator typename() const { … }

所谓转换函数就是将一种类型转换成另外一种类型。 将class A转换为class B(转出去)以及class B转成class A(转回来)

  • 转换函数

    就是将某个类型转换成不同的类型, 转换函数可以不写返回类型的 函数的名称就是operator typename,转换不可以有参数,返回类型理应就是typename,所以就不要写出来了。转换不可能改变里面的类型,所以通常需要加上const(如果外面用const实例调用函数的时候,一定要加const才可以调用)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class Fraction
    {
    public:
    Fraction(int num, int den=1):m_numerator(num), m_denominator(den) { }
    operator double() const { return (double) (m_numerator / m_denminator);}
    // Fraction& operator+(const Fraction& rhs){ ... }
    private:
    int m_numerator;
    int m_denminator;
    };
    {
    Fraction f(3,5);
    double d = 4 + f;
    }

    上面这个函数,就会有一个类型转换,将f转换成了double类型的。编译器先找一下有没有operator+ (const double, const Fraction&),找不到,所以找有没有operator double()将f转换成double类型的。operator string()合理就可以写,不一定要是基本类型,只要是一个type就好啦。

    转换函数把这种东西转换成别的东西

  • non-explicit-one-argument ctor

    non-explicit的一个实参的构造函数,就比如上面那个类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class Fraction
    {
    public:
    Fraction(int num, int den=1):m_numerator(num), m_denominator(den) { } // two parameters , one argument 表示一个实参就够了
    //operator double() const { return (double) (m_numerator / m_denminator);}
    Fraction& operator+(const Fraction& rhs){ ... }
    private:
    int m_numerator;
    int m_denminator;
    };
    {
    Fraction f(3,5);
    Fraction d = f + 4;
    }

    编译器看到+,先看有没有operator+,实现是实现了,但是参数的类型是Fraction,这里是double,显然不可以。所以编译器看看能不能将4转成Fraction,因为有这么一个单实参的构造函数,所以可以转换过来。

    non-explict单实参构造函数将别的东西转换成这种东西

    这个函数,将4加到f上,所以将4转换成为了Fraction(4,1),然后调用重载的operator+进行加法操作。

    但是如果是同时存在呢?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class Fraction
    {
    public:
    Fraction(int num, int den=1):m_numerator(num), m_denominator(den) { }
    operator double() const { return (double) (m_numerator / m_denminator);}
    Fraction& operator+(const Fraction& rhs){ ... }
    private:
    int m_numerator;
    int m_denminator;
    };
    {
    Fraction f(3,5);
    Fraction d = f + 4;
    }

    这里有两种思路呀,一种是将f转换成double(double,将这种转换成另外一种),然后加起来转换成Fraction(单实参构造函数),另外一种是将4转换成Fraction(non-explicit单实参构造,将别的转成这种),然后再加起来。这两种思路都是可行的,所以会引起歧义。

    总结一下会发现

    • 实现了这种单参数的构造函数的时候,可能会有自动的隐式转换
    • 当实现了转换函数(operator typename() const {})的时候,可能会有将当前类型转换成另外一种类型的操作。
    • 当有歧义的时候会出现问题的。

标准库中的实例

image-20211221122743684

(模板的偏特化)这个vector里放的是bool值,这里[]操作返回的不是一个bool值,是一个reference类型的值(代理设计模式)。现在没有传回一个bool值,所以就应该有一个转换函数将reference转换成bool值,现在一看,果然有这么一个转换函数呀。

转换函数就是将一个类转换成另外一种type,这就是基本思想了:转换方式再转换函数里定义。上面这个例子就是将reference转换成了bool。

1.11 explict 关键字

所谓explict关键字,就是在有些情况下,单实参的构造函数里,可能会自动调用构造函数进行类型转换(将别的转换成这种),为了避免这种问题,就需要使用explict关键字。

使用这个关键字的时候,可以避免隐式调用单实参构造函数将类型转换了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Fraction
{
public:
explict Fraction(int num, int den=1):m_numerator(num), m_denominator(den) { }
operator double() const { return (double) (m_numerator / m_denminator);}
Fraction& operator+(const Fraction& rhs){ ... }
private:
int m_numerator;
int m_denminator;
};
{
Fraction f(3,5);
Fraction d = f + 4; //这里会报错,因为将f转换成double加完得到3.6之后无法调单实参的构造函数将这个double转换成Fraction
}

使用这种可以很好地避免偷偷调用这个Fraction的构造函数,然后得到一些奇怪的错误。

1.12 pointer like class 像指针的类

所谓像指针的类就是实现了*->重载方法的类,表现出来的行为就像一个指针,所以是像指针的类。但是比指针更聪明一点。

  • 实例:智能指针

    image-20211221231825491

    里面一定有一个真正的指针。指针所允许的操作,这个class应该也允许这种操作。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    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;
    long* pn;
    ...
    }
    {
    struct Foo
    {
    ...
    void method(void) { ... }
    }
    shared_ptr<Foo> sp(new Foo);
    Foo f(*sp);
    sp->method();
    }

    上面这个例子中,智能指针是一个模板类,现在有一个类Foo, 然后定义一个智能指针,指向的是一个Foo类,这里是一个构造函数,构造函数里面new Foo返回的就是一个指针呀,符合构造函数的定义的。

    • *:就是this调用了这个函数,所以调用重载方法,返回*px的引用,就是拿出了实例了。

    • ->: 就是this调用了这个函数,所以调用重载方法,返回的是px指针,实际调用的是px->method(),这个会有问题,因为->已经被消耗掉了,怎么还能有一个->,只能强行解释了,会返回一个指针,同时还会用->来调用函数,不这么解释就解释不通呀。

      image-20211221140042927

      将天然的指针包装在这个更加聪明的指针里面去。

  • 实例:迭代器

    迭代器也是实现了*->方法的重载的,迭代器还要实现++,--这些重载。

    image-20211221140116561

    套一层直接操作的是里面的data

    image-20211221140212939

    有一个类是list,里面的指针指向一个Foo,使用*->可以操作data,所以这个链表可以像一个指针一样,由*->使用当前这个元素,当然也可集成其他的功能,比如这个例子中的下一个元素和上一个元素的链式结构等。

    重载*->方法,可以像操作一个指针一样操作一个类对象,同时由于一个对象里面还可以实现更多的功能,所以虽然像个指针,但是里面还有很多方法

1.13 function like class 仿函数

所谓仿函数就是重载了()方法的类,表现出来的行为就像一个函数,所以是一个仿函数,有点像python里实现了__call__方法的类。

  • 例子

    image-20211221185532899

    简单来说就是实现了()的重载呀,然后来直接调用这个类对象就完事了。

    标准库里面就有一个class叫做pair,里面有第一个属性和第二个属性,两种不一定是一样的。这里写出了两种构造函数(构造函数和拷贝构造函数)

  • 上面这个例子来自于标准模板库,里面有一些奇怪的事,这个其实现在来看不重要

    image-20211221233714046

    image-20211221233759567

    image-20211221190706416

    上面这些类都继承于这两个类 这个不重要 为什么要继承这个需要在STL里面详细说明。

1.14 namespace

为了避免命名冲突可以使用namespace,很方便

1.15 模板

1.15.1 类模板

你认为你可以把哪些类型抽出来由用户随意指定,你就可以把它抽出来,将来用的时候再说。

image-20211221220304220

类模板就是一个类里面的成员的类型可以是一个不定的,就是类模板类型。类模板定义的时候需要指定类型的。

1.15.2 函数模板

设计一个函数如果认为可以将类型抽出来,就把这个函数设计成函数模板。这里的class可以写成typename

image-20211221220427150

就是一个函数的变量可以是不定的,这个时候就可以弄成函数模板,函数模板是不需要指定类型的,编译器会进行实参推导

1.15.3 成员模板

image-20211221225104600

本身是一个成员,但是它自己本身也是一个模板。外面的模板本身确定之后,里面的成员也可以变化。成员模板经常出现在标准库中。

具体的例子

image-20211221234440998

用生活经验来理解这个问题。

我们首先创建一个对p,其中放鲫鱼和麻雀。现在我们可不可以把这个对做成初始值,放到鸟类和鱼类的pair里面呢?(调用拷贝构造函数,将这个实例拷贝一下?)这个当然可以,反之不可以。为了实现这个功能,我们需要用这种方法来实现。首先有一个基类的构造函数,另外有一个相应的拷贝构造函数,让构造函数更有弹性。

image-20211221235035652

指针可以upcast,所以智能指针也必须可以这样搞,就需上面那种写法呀。

我一个鲫鱼指针,可以转换成鱼类的指针。

下面我的智能指针的模板是鱼类的,但是我理应可以接收鲫鱼类作为我的参数,所以我需要成员模板来做这件事情。

1.15.4 特化

对于某些独特的图形一定要做特殊的设计。

image-20211221235502198

有一个泛化的hash,这个时候我需要对特定的类型实现一个泛化。

1
2
3
4
template <class Key>
struct hash{};
template<> //已经不见了
struct hash<int>{ }; //特化,不允许hash随意了

上面叫泛化,下面一些叫做特化。

1.15.5 偏特化

局部特化

  • 个数上的偏

    image-20211221235749043

    可以指定元素类型和分配器,现在认为如果是bool值得话,我应该有单独的设计,所以我告诉编译器,当元素是bool时不用泛化了,用特化来实现。

  • 范围上的偏

    image-20211221235923184

    本来可以接收任意的T,现在告诉编译器,我必须是一个指针。这就是一种范围上的特化。元素类型是指针的时候就会调用下面的版本。本来是任意范围,现在必须是指针,就是偏特化,

1.15.6 模板模板参数 template template parameter

简而言之:一个模板的参数也是一个模板。比如一个模板类里面要用一个容器,但是这个容器本身也是模板类。

image-20211222000107281

就是模板参数本身就是一个模板。只有在模板的尖括号里面typenameclass可以共通。

第一个参数是一个T,第二个参数本身优势一个模板,名字叫做Container,我的第二个参数也可以是不定的呀。

我希望使用者可以用XCLs<string,list>的使用方式,表示list<string> c但是这是不可以的。只不过因为Container需要两到三个参数才可以用,平时没写是因为有默认值,但是我们在这里用不可以省去。中间那段是2.0特性。

现在再看一个例子

image-20211222111729115

XCls本身是一个模板,里面的第二个参数还是一个模板(模板模板参数)。第一个和第四个都只接收一个模板参数,第二三个是因为一些独特的特性不能使用这个。

模板的参数本身也是一个模板就是这种写法

image-20211222113108098

上面这个不是模板模板参数,因为一旦这么写了之后,就不是模板了,因为已经确定了,没有任何模糊地带了。使用模板模板参数就应该是

1
stack<int,list> s2; //传进去list依然是灰色的,不是确定的,而不是list<int>这种完全确定好的

1.16 C++标准库

库很重要,C++标准库特别重要,一定要学会使用C++标准库。

  • 容器:数据结构
  • 算法:就是正常的算法,但是标准库里面的算法效率都是很高的
  • 比较好的学习方式:一定要把所有的都用过一遍,写个小程序测一遍,效果不好。使用这个东西是一件很愉快的事情。

一下的内容C++11之后才可以使用的

1.17 variadic template

参数不定的模板参数 一个加一包

以前的模板参数的个数是确定的,但是现在说可以随意写多少个,用...来表示

1
2
3
4
5
6
7
8
9
10
11
12
void print(){

}
template<typename T, typename... Types>
void print(const& T firstArg, const Types&... args)
{
cout<<firstArg<<endl;
print(args...); // 这是一个递归调用,最后变成0个了,所以需要上面那个啥都没写的版本,先一个再一包
}
{
print(7.5, "hello", bitset<16>(377),42);
}

...为什么要这么用也是没什么道理的,语言的设计如此你就这么用就好了,可以使用sizeof...(args)知道这一包有多少个参数。

  • 模板参数包
  • 函数参数类型包
  • 函数参数包

image-20211222114432560

标准库中很多地方都用到了这个特性。

1.18 auto

image-20211222114652395

编译器自动帮你推,这是一个语法糖。不用这个语法糖就要像上面蓝色那样写一堆。但是最下面那个不太好,因为这初始化的时候推不出来的。

全都用auto?

除非你每个都初始化了,这个不好呀

1.19 ranged-base for

1
2
3
4
5
6
7
for(decl: coll)
{
statement
}
for(int i: {2,3,5,7,9,11}){
cout << i << endl;
}

遍历一个容器的语法糖,及其清楚。

image-20211222115035547

传引用会影响原来的,这里传值和传引用需要看自己的要求,都可以还是传引用吧,非常快的。

1.20 Reference

image-20211222115311716

理解成一个别名,编译器是看待成指针,但是逻辑上可以理解成别名。所以引用是一定要赋初值的,不然给谁取别名呢。引用有多大呢?看它代表什么,代表啥就是多大。而且编译器营造出来了一种假象,rx的地址也是相同的,但是这个是假象,因为小名也要存一下呀。

下面的例子是32位机器上的。

image-20211222120027991

reference是一种漂亮的pointer

reference常见用法

  • 参数传递

    image-20211222120208928

    签名不包括返回类型的,只包含函数名字,参数和const

    1
    2
    double imag(const double& im) const { ... }
    double imag(const double& im) { ... }

    因为const算签名的一部分,所以上面的可以并存

1.20 对象模型

底层的东西。

1.20.1 不同关系下的析构和构造

所谓由内到外和由外到内,都是编译器自动给我们加的代码,不用自己加代码。(编译器调用的是默认的构造函数)

1.20.2 虚指针和虚表 vptr vtbl

image-20211222122522480

不能做静态绑定,原先调用函数call xxx,现在是通过指针找到虚指针,找虚表,找函数,调用,是动态绑定

什么时候动态绑定,符合以下三个条件

  • 指针
  • 指针要做upcast转型
  • 调的是虚函数

静态绑定就直接指向一个地址,没得商量了,动态绑定需要通过上述步骤去看到底调的是哪一个函数。

image-20211222123523069

虚函数的这种用法就是多态

再看这个例子

image-20211222183533594

当我调用一个函数的时候,就相当于做了用this指针调用,然后这里会遇到满足三个条件:

  • 指针调用this->Serialize()
  • 向上转型this指的是子类的对象
  • 虚函数

所以编译器把这一行编译成(*(this->vptr)[n])(this),就是走虚指针和虚表那一条路(动态绑定),所以调用的是子类的Serialize()函数,而不是父类的这个函数。这就是动态绑定的用处。

1.20.3 动态绑定机制

需要看汇编代码了

image-20211222185314894

上面这个例子中,a.vfunc1()aB类型强转过去的,那么这个调用的是B重载的,还是A的呢?请注意,动态绑定三个条件:指针调用,指针是向上转型的,调的是虚函数。这里是对象调用的,所以就是静态绑定,调用的就是A的。

那子类调用父类的方法呢?如果不满足上面的条件,调用的不就是子类自己的嘛?子类是父类继承下来的,所以子类有父类的方法呀,子类确实调用的是自己的,不过也是调用的是父类继承给自己的,所以相当于调用父类的呀。那要是子类重写了呢?那表现的就是调用子类自己的呀。那这样为啥要虚函数呀?感觉没啥用好像(所谓的vfunc好像就是为了保证向上转型?)

如果不用虚函数写会怎样呢? 如果不用虚函数写,你在清楚定义类型的时候,自然没啥影响,都是自己调用自己,但是如果说,比如我有一个容器,定义成父类的,这样才可以放进去子类的类型,我存进去肯定有一个强转吧,这个时候如果不用虚函数实现的话,应该调用的就是父类的咯,因为是静态调用,啥调用我的我就是啥类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class A
{
public:
void method(){ cout << "A" << endl;}

};
class B : public A
{
public:
void method(){ cout << "B" << endl;}
};
{
B* b = new B;
A* a = (A*) b;
a->method(); //静态绑定的,应该是 A 这个显然是不希望的,因为多态不希望这样,希望打印的是B
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class A
{
public:
virtual void method(){ cout << "A" << endl;}

};
class B : public A
{
public:
virtual void method(){ cout << "B" << endl;}
};
{
B* b = new B;
A* a = (A*) b;
a->method(); //就会触发动态绑定的机制,最后调用的B
}

按照老师讲的(*a).method()应该是调用a的呀,但是这里不是,好像有点问题?还是说这样也相当于是指针调用呀,想想看这样应该是相当于B再调用呀,因为*a是一个对象,这个对象是B,然后传this指针进去,所以应该就是返回B呀,因为找到的是B的方法呀。这对吗?这不对的,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class A
{
public:
virtual void method() { cout << "A" << endl; }

};
class B : public A
{
public:
//virtual void method() { cout << "B" << endl; }
void method2() { cout << "B2" << endl; }
};
int main()
{
B* b = new B;
A* a = (A*)b;
a->method2();//这个不能编译通过呀,说明取到的不是B类型的对象
(*a).method2(); //这样也不可以、所以要实现多态还必须用虚函数,不然都调用不了子类的这个函数
return 0;
}
1
2
3
4
5
{
B b;
A a = (A) b;
a.method(); //这种应该是A,因为强转到了A
}

但是上面这个第一个例子也说明了,*a不是一个指向B类型的,这就很令人迷惑了。还是说,在强转时,本身就会损失子类的特性?东西还是子类的性质,但是子类的特有的都没有了(给b加一个属性,打印一下大小就可以发现这种说法是正确的)(但是虚函数还可访问到,很奇怪呀,可以理解成空间还是在那里的,由于是new的又不会被别人覆盖,但是对外表现出来是只看到A那么大,但是底层调用的时候,就是一些偏移量,这些依然可以被直接访问到的),所以父类一定要有这个么一个虚函数来调用以下,不然就会出问题?好像这样也说明了虚函数的重要性了。

总的来说,需要实现多态,就必须有这样的虚函数的机制。

小结一下强转啊:强转仅仅是改变了对外的表现形式,外界不能调用转到的不存在的东西,但是其实东西还在,底层用地址偏移这样是可以取到的。比如一个函数method,里面要访问B的专属的int a,现在转成了A,里面没有A,不能直接调用B里特有的method2,但是通过method还是可以访问到amethod2的,因为这些东西的内存还在,编译器有办法取到。

image-20211222185336098

1.21 const

  • 常量成员函数:告诉编译器,这个成员函数不会改变class的数据。不会改变class的data

    image-20211222184027744

    const属于签名的一部分

    字符串是共享的设计模式,现在如果有人需要改内容呢?就拷贝一份让它改,然后剩下的继续共享就完事了。[]是可能修改的,就是写的时候,所以[]就可能会被使用者改内容,所以要做copy on write的考虑。如果是个常量字符串来调用中括号,不可能改,所以不用考虑这个,所以需要两份函数来区分到底是被常量对象还是被非常量的对象调用。

    当const和non-const函数版本同时存在的时候,const只能调用const,non-const只能调用non-const。

1.22 new和delete

1.22.1 重载 operator new, operator delete, operator new[], operator delete[]

  • 全局的

image-20211222201441220

​ new

  • 分配空间 使用operator new (底层调用malloc) 分配的是这个类的空间(比如double int struct pointer…)
  • 强转指针
  • 调用构造函数:里面可能也有new,但那是类的事
    delete
  • 调用析构函数 可能也有delete 但那是类的事了
  • 释放空间 operator delete (底层调用free) 释放的是这个类的空间
  • 局部的重载 成员的

  • image-20211222202957342

    image-20211222202404304

    接管之后可以做一个内存池的模式。

  • 例子

    image-20211222202639957

    ::new这样是强制使用全局的,否则是看有无成员重载,有就调用局部的,否则就全局的。

1.22.2 重载class member operator new() placement new

可以写出很多版本,前提是每个都有独一无二的参数列表(因为这个可是没有const的),每一个版本第一个参数必须是size_t, 其余参数以new所指定的placement argument位处置。出现于new(...)小括号内的就是placement arguments

image-20211222203921229

image-20211222204119185

这个写起来好像和局部的operator new形式一样呀。使用方式也有差异的

1
2
A* p= new A(); //调的就是局部的那种operator new或者全局的
A* p = new(200,'1') A(); // 调用的就是placement new

image-20211222204824424

  • © 2019-2022 Wendell
  • Powered by Hexo Theme Ayer

请我喝杯咖啡吧~

支付宝
微信