面向对象程序设计 一

侯捷老师

1. C++编程简介

1.1 基础知识

  • 变量及类型

    • int short long long long char
    • float double
    • struct
  • 作用域

  • 循环

    • for
    • while
    • do
  • 流程控制

    • if else
    • switch case
  • 编译、链接等过程

1.2 目标

  • 大气、正规的编程习惯
    • 以良好的方式编写C++ class 基于对象的
      • 带有指针的
      • 不带有指针的
    • 学习Classes之间的联系
      • 继承
      • 复合
      • 委托

1.3 C++历史

  • B语言
  • C语言
  • C++语言
    • C++98
    • C++03
    • C++11
    • C++14

1.3 第一个例子 Complex 不带指针成员的类

1.3.1 代码基本形式

  • .hfile
  • .cppfile
  • #include<标准库>
  • #include"用户文件"
  • 文件拓展名不一定是.h或者cpp,也可能是.hpp甚至于无扩展名
  • #include<iostream.h>的扩展名可以不要,如果引用C语言的标准库,可以使用#include<cstdio>的形式

1.3.2 Header 中的防卫式声明

  • 防止代码多次被定义

  • #ifndef __COMPLEX__
    #define __COMPLEX__
    
    body
    
    #endif
    <!--0-->
    

1.3.4 class 的声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class complex
{
public:
complex (double r = 0, double i = 0)
: re(r), im(i)
{ }
complex& operator += (const complex&);
double real() const {return re};
double imag() const {return im};
private:
double re, im;

friend complex& __doapl (complex*, const complex&);
};

1.3.5 模板的简介

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
template<typename T>
class complex
{
public:
complex (double r = 0, double i = 0)
: re(r), im(i)
{ }
complex& operator += (const complex&);
double real() const {return re};
double imag() const {return im};
private:
T re, im;

friend complex& __doapl (complex*, const complex&);
};

{
complex<double> c1(1.5,1.2);
complex<int> c2(2,6);
}

1.3.6 inline 内联函数

函数若是定义在class body内部,便自动成为inline的候选人

inline的执行更加快,所以应该尽可能将函数写成内联的,所以将函数定义在类声明的内部便推荐编译器将其编译成为内联的,但是编译器不一定有这个能力接受你的建议。上面的realimag以及构造函数就是建议内联的,而且这么简单的函数,编译器应该有能力将其变成内联的。

对于不定义在类声明的内部的函数,可以使用inline关键字,建议编译器优化成为内联的,至于编译器接不接受建议,还要看这个函数是否足够简单,可以优化成为内联函数。

1
2
3
4
5
inline double
imag(const complex& x)
{
return x.imag();
}

1.3.7 访问级别

  • public:就是所有的都可以访问

  • private: 就是仅仅内部可以访问,没有特殊情况时,应该将所有的变量都写成私有的,方法看情况写成私有还是公有的。

  • proteced: 这里没遇到,先不说

  • 例子

    1
    2
    3
    4
    5
    {
    complex c1(2,1);
    cout<<c1.re; //错误的
    cout<<c1.real(); //正确的
    }

1.3.8 构造函数

  • 构造函数有一个特有的初始化方法:初始化列表

    尽量使用初始化列表,因为这个选择的是初始化赋值,而在构造函数内部赋值相当于跳过了初始化这个阶段,浪费了一丢丢性能

  • 构造函数可以重载

    因为编译器会给不同名字和参数列表的函数重整名字为不同的,这样其实编译器在找的时候找到的还是不同的名字的函数,这并没有违背我们的信条,只不过编译器给你干了一些活(就像Python里面的私有变量也会重整名字一样)

    构造函数同样可以重载

    1
    2
    3
    4
    5
    class complex{
    public:
    complex():re(0),im(0){}
    complex(double r=0, double i=0):re(r), im(i) {}
    };

    但是上面这个例子是不可以的,因为下面这个函数有默认值,你不传参数,编译器不知道调用哪一个函数,就会出错。

    普通函数的重载

    1
    2
    3
    4
    5
    class complex{

    double real() const {return re};
    void real(double r) {re = r;}
    };

    在一类编译器中,可能变成?real@Complex@@QBENXZ?real@Complex@@QAENABN@Z,编译器看到的名字是不一样的。

  • 构造函数被放在private区,代表外界不可以创建这个类,这个也是有可能发生的事

    比较典型的例子就是单例设计模式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class A{
    public:
    static A& getInstance();
    setup(){...}
    private:
    A();
    A(const A& rha);
    ...
    };
    A& A::getInstance()
    {
    static A a;
    return a;
    }
    {
    A::getInstance().setup();
    }

1.3.9 常量成员函数

注意const,当一个函数不修改内部的变量时,我们应当给函数const,表示这个函数不改变传入的变量,如果不这么写呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class complex{

double real() {return re};
void real(double r) {re = r;}
};
{
// 这个例子是可以的
complex c1(1,2);
c1.real();
}
{
//这个例子不可以
const complex c1(1,2);
c1.real();
}

因为下面这个例子中,c1是一个不可变的,你现在传递给real,函数却告诉它我这个函数可能会修改里面的变量,这不就乱了套嘛,所以说不修改内部变量的函数一定要加const,否则可能会出现问题的。

1.3.10 参数传递 By value 和 By reference

  • 传值会将整个参数拷贝一份进行传递

  • 引用在本质上就是通过指针传递,所以传的是一个地址值,但是引用有个好处,它不用像指针传递那样要考虑&*

  • 理论上传递一个地址是很快的,所以传递尽可能通过引用传递

    那像int``double这种也是4个字节的,和地址一样长的,传引用一样快呀,甚至于char传引用更加慢了,所以说这种短的,你就看心情传递吧,总的来说传引用对于大对象来说更加方便

  • 如果说,我只希望传递这个值的相关信息进去,但是不希望这个对象被修改该咋办呢?就比如传值的时候,我不会修改原参数,此时你可以用const限制一下,此时就表示这个不会被修改了,如果内部修改了,就会报错

    1
    ostream& operator << (ostream& os, const complex& x){...}

    比如这个例子,<<一定是作用于左边的参数的,所以说,不会修改右边的参数,会修改左边的参数,所以os不加const, complex加上const,二者都是通过引用传递,效率很高

1.3.11 参数放回 By value 和 By reference

  • 也还是尽可能地传递回来一个引用吧,效率高,但是并不是所有情况下都是可以传递会引用的
  • 当返回的对象是在函数内部创建的,函数执行完这个空间就脏了,此时你返回一个引用就那不到东西了,所以应该返回一个值,其余的情况可以返回一个引用
  • 返回也可以限制为const,表示返回回来的东西就不可以被修改了

1.3.12 friend友元

  • 前面提到外部不可以访问内部的私有变量的,但是有时候我又希望可以访问内部私有变量来实现高效操作,这个时候就可以用友元

  • 友元是一个声明,告诉这个类,我这个东西是你的朋友,可以直接访问你的私有变量

    1
    2
    3
    4
    5
    6
    7
    class complex
    {
    private:
    double re, im;

    friend complex& __doapl (complex*, const complex&);
    };

    告诉complex__doapl可以访问你的私有变量,以后的complex对象要对这个函数开后门

    1
    2
    3
    4
    5
    6
    7
    inline complex&
    __doapl(complex* ths, const complex& r)
    {
    ths->re += r.re;
    ths->im += r.im;
    return *ths;
    }
  • 相同class的各个对象互为友元

    比如下面这个例子

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class complex
    {
    public:
    complex(double r=0, double i=0):re(r),im(i) {}
    int func const (const complex& param) {return param.re + param.im;}
    private:
    double re, im;

    friend complex& __doapl (complex*, const complex&);
    };
    {
    complex c1(2,1);
    complex c2;
    c2.func(c1);
    }

    这里,c2直接访问了c1里面的私有属性reim,这怎么解释呢,因为c2c1的友元,所以c2可以直接访问c1的私有属性,这样就解释得通了。

1.3.13 操作符重载1(成员函数的操作符重载) 和 this指针

1
2
3
4
5
inline complex&
complex::operator += (const complex& r)
{
return __doapl(this, r);
}

因为c2 += c1,是将c1加到c2上去,所以改变了c2,且是c2调用的+=函数

在成员函数里,函数的参数列表里默认有一个this指针变量,指向的是调用这个函数的对象本体,这个参数是默认存在的,不用也不能写出来

+=这种可以写成类的成员函数形式的重载,因为左边一定是这种对象,但是+不一定,可能左边是一个整数也说不定,所以+的重载用非成员函数,但是如果说,规定左边一定是成员的话,能做到嘛?好像也不行欸,因为是谁在调用这个函数呢? +=你可以理解成左边的在调用,+也是嘛,不妨做个实验,实验说明是可以的呀。你可以理解成你的这个东西成为一个二元算子,你希望它是啥性质你就声明称啥样子。

所以你需要设计成啥样还是需要看你希望重载的性质的,这个东西学会了原理就很好理解啦。你只用知道,类里的实例方法,是你创建之后,调用的时候自动将二元操作符的左边的值填进去一个this指针的,理解了这个你对这个东西的理解会更加深刻。(一般来说没有操作右值的二元操作符的)

总的来说就是自动传this指针,这个想法很重要,有了这个你可以理解这种写法

1
2
3
4
5
6
7
class complex{
...;
complex operator - () { return complex ( -this->re, -this->im);} // 这种写法就是负号的重载啦,看似没有参数,其实自动传递了this指针过来
complex operator - (const complex &); //这种就是减法的重载啦,看似只有一个参数,其实自动传递了一个this指针过来
};
complex operator -(const complex&); //这种就是负号的重载啦,虽然看起来和上面类里的一样,但是其实它只传递了一个参数
complex operator -(const complex&, const complex&); //这种才是减法重载,两个参数
1
2
3
//相当于
inline complex&
complex::operator += (this, const complex& r){...} //但是这样写是不对的,不能写出来,只用知道它自动传递了this就好了,就像Python,它的self是一定要写的

1.3.14 Return By Reference 传递者无需知道接收者是以什么样的形式接收的

1
2
3
4
5
inline complex&
__doapl(complex* ths, const complex& r){
...
return *ths;
}

返回的是一个对象呀,此时你可以用一个值返回,也可以用一个引用返回,用值返回返回的就是一个新的对象,拷贝过来的,用引用返回返回的是这个对象的引用,外界接收到的是这个对象(的地址?),只不过用起来和接收到一个对象没啥区别,简单点说就是,返回的是一个对象,外面用什么接收(值或者引用)返回语句是不关心的,但是外界接收的是一个指针的时候,就要关心了,你得返回一个指针,这么一说就明白了吧

1.3.15 操作符重载(非成员函数类型)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
inline complex operator + (const complex& x, const complex& y)
{
return complex(real(x)+real(y), imag(x) + imag(y));
}
inline complex operator + (double x, const complex& y)
{
return complex(x+real(y),imag(y));
}
inline complex operator + (const complex& x, double y)
{
return complex(real(x)+y, imag(x));
}
{
complex c1(2,1);
complex c2;
c2 = c1 + c2;
c2 = 5 + c1;
c2 = c1 + 7;
}

1.3.16 临时对象 typename() 形式

使用这种形式会生成 一个临时对象,这个对象存在这一句话,运行完了这个对象就消失了,有时候功能太简单了,一个对象不想给它起名字就可以用这种方法。

1
2
3
4
inline complex operator + (const complex& x, const complex& y)
{
return complex(real(x)+real(y), imag(x) + imag(y));
}

返回的值就是一个临时对象,另外,由于这个对象是local的,所以返回必须是return by reference

1.3.17 <<的重载

1
2
3
4
ostream& operator << (ostream& os, const complex& c)
{
return os << '(' << c.real() << ',' << c.imag() << ')' ;
}

cout << c; 这个<<作用于cout,所以第一个参数是可变的,第二个参数不变,所以用const, 另外,由于我希望能够链式使用,所以这个有返回值,如果没有返回值,这个就不能像这样cout<<c<<endl;链式使用了,这种链式使用是:先执行了cout << c 返回一个ostream对象赋值给cout ,然后这个新的cout << endl,这个过程还是挺清晰的。

1.4 第二个例子 String 带指针成员的类

complex是来自标准库的简化版,string的标准库过于复杂,所以这里用一个自己实现的版本

1.4.1 拷贝构造、拷贝赋值、析构

1
2
3
4
5
6
7
8
9
//需要的功能是:
{
String s1() ; //没有初始值的
String s2("hello");//有初始值的
String s3(s1); //以s1为初始值,拷贝构造 clone copy
cout << s3 << endl; // 操作符重载
s3 = s2; // 赋值操作 拷贝赋值 与上面的区别 第一个是第一次出现s3 是拷贝构造,现在这个是拷贝赋值
cout << s3 << endl;
}

刚才的complex没有写拷贝构造和拷贝赋值,编译器会给你一套实现好的方法,编译器会将属性一个一个的copy,复数没有带有指针,用编译器的一套就好了,但是有指针的时候,一定要自己写,因为你拷贝的是一个地址呀,并没有实现真正的拷贝,而复数里面的那个属性都是一个实打实的变量,不是地址,拷过去就拷过去啦。所以带有指针的类,拷贝构造和拷贝赋值都要自己写。并且一定要实现Big Three: 拷贝构造、 拷贝赋值、 析构

1
2
3
4
5
6
7
8
9
10
class String{
public:
String(const char* cstr = 0); //构造函数
String(const String& str); //拷贝构造 接收的是自己呀
String& operator=(const String & str); //拷贝赋值呀,为了可以连等于所以要返回值嘛?
~String(); //析构函数
char* get_c_str() const { return m_data; } // 在声明里实现的默认是建议inline的
private:
char* m_data; //指针类型的 动态分配的方式
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
inline String::String(const char* cstr = 0)
{
if(cstr){
m_data = new char[strlen(cstr)+1];
strcpy(m_data, cstr);
}
else{
m_data = new char[1];
*m_data = '\0';
}
}

inline String::~String()
{
delete [] m_data;
}
{
String s1();
String s2("hello");

String* p = new String("hello"); //这也是创建对象的一种方法,动态的创建一个对象就这么干
delete p; // 离开作用域的时候要释放掉这个东西呀
}

new 就是分配 一块内存

析构就是当对象被释放时执行的动作,复数的例子中调用的是编译器给的,啥也不干,但是在带指针的中,要将动态分配的内存释放掉,还给系统呀,不然就内存泄漏了。

带有指针成员的类一定要有拷贝构造和拷贝赋值

不实现会怎么样呢?

image-20211217000128394

首先a, b都指向一个字符串,现在将b = a了,如果没实现拷贝赋值,使用编译器给的拷贝赋值函数就是,就相当于把a(一个地址)拷贝给b,所以b也指向a指向的那个字符串,这个时候就内存泄露了。(这种是浅拷贝,只拷贝了地址)

深拷贝:就是将指向的东西也拷贝了。

1
2
3
4
5
6
7
8
9
10
inline String::String(const String& str)
{
m_data = new char[strlen(str.m_data) + 1]; //因为同一个类的不同对象互为友元,所以可以在这里直接访问另外一个对象的私有变量 s2 访问了 s1的私有变量
strcpy(m_data, str.m_data);
}
{
String s1("hello");
String s2(s1); // 以s1为蓝本创建s2
// String s2 = s1; //这两句话完全相同,都是拷贝,且都是构造新的,所以都是拷贝构造函数
}

拷贝赋值函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
inline String& operator=(const String& str)
{
//先检测一下是不是自我赋值
if(this == &str)
return *this;
//先清空自己
delete [] m_data;
//再分配
m_data = new char[strlen(str.m_data) + 1];
//再赋值
strcpy(m_data, str.m_data);

return *this;
}
{
String s1('hello');
String s2(s1); //拷贝构造了一个
s2 = s1; // 拷贝赋值一下
}
  • 检测自我赋值

    image-20211217001915048

    不写的话,传进来的两个对象是同一个,那么第一步杀掉之后就把唯一这个杀掉了,那再提取长度的时候会出问题了。

1.4.2 重载<<

1
2
3
4
ostream& operator<<(ostream os, const String& str)
{
return os << str.get_c_str();
}

1.4.3 new 和 delete

  • 堆和栈

    栈是在离开作用域后程序自动释放的,堆是用户申请的,离开作用域之后不会自动释放空间,要自己手动释放。

    1
    2
    3
    4
    5
    6
    class Complex {...}
    ...
    {
    Complex c1(1,2); //栈里的
    Complex* p = new Complex(3); //堆里的
    }

    c1所占的空间来自栈里,Complex(3)仍然是个临时对象,其所占用的空间是以new申请的,然后由p指向,所以p指向这个空间就完事了。

  • 栈对象的生命期

    栈对象:在离开作用域之后自动消失,所以这种对象又称为auto object,因为会被自动清理。

  • 静态局部对象的生命期

    静态对象在离开作用域之后仍然会存在,直到整个程序结束,但是其作用域依然是那个大括号。

    1
    2
    3
    {
    static Complex c1(1,2); //第一次调用会创建,然后就到程序结束才结束
    }
  • 全局对象的生命期

    全局对象是不存在任何作用域中的对象,其生命在整个程序都结束之后才会结束,也可以视作一种static,然是作用域是整个程序。

  • 堆对象的生命期

    堆对象在被delete之后小时

    1
    2
    3
    4
    5
    6
    7
    8
    {
    Complex* p = new Complex;
    ...
    delete p;
    }
    {
    Complex* p = new Complex;
    }

    第二个会出现内存泄漏,因为离开作用域,p就看不到了,其指向的空间就看不到的,但是还没有释放掉呀,所以就会出现内存泄漏的问题。

  • new: 先分配内存,再调用构造函数

    image-20211217122738844

    image-20211217124730858

    当我们写下了Complex* pc = new Complex(1,2);时,编译器底层相当于实现了下面的这三个过程:

    • 首先会分配一块内存,用operator new函数,其底层调用一个malloc,分配一块空间,里面放两个double(根据sizeof()得到具体情况)
    • 然后将指针强转一下
    • 然后调用Complex::Complex(1,2)方法,谁调用呢?pc指针调用呀,我们直到调用类内部的方法,会自动传递一个this指针,这个this就是调用者的地址,所以传的就是pc,相当于Complex::Complex(pc,1,2),所以就构造了这个一个对象

    先分配一个对象的空间,再调用构造函数给类内部分配一些空间。

  • delete: 先调用析构函数,再释放内存

    image-20211217123658184

    image-20211217124714335

    编译器转换为

    • 首先调用析构函数
    • 然后调用operator delete,内部会调用free函数

    析构函数会先释放这个对象所分配的内存,然后再将这个指针释放掉

  • 看一些内存机制的东西

    下面这些东西都来自VC中的,所以不一定适用于所有的编译器,但是大家应该都大差不差

    内存块

    image-20211217125015530

    长的是debug模式的,短的是release模式的,上下两块是cookie头,表示有多长,并将最后一位设置成1表示是给出去的内存。然后VC要求16的倍数,所以会有一些padding

    动态分配所得的array

    image-20211217125237220

    大差不差,但是多了一个位数,表示这个东西有多长。

    为什么array new要搭配array delete?

    因为会有内存泄漏,但是不是我们所理解的那种内存泄漏。

    image-20211217125348453

    当我们用array delete时,编译器会根据前面那个长度调用n次析构函数,而用delete时,只会调用一次析构函数,这样就不能将所有的new出来的空间都释放掉了(先调用析构函数,再释放外层的指针)。

    这种仅针对于那种带有指针的类呀,因为带有指针的类才需要在类里分配空间,不带指针的类是不需要分配空间的,也就不需要用析构函数释放,所以不带指针的类理论上可以不用array delete释放的,带指针的一定要用array delete释放,否则会造成内存的泄漏。因为不是指针那种就不会有指出去的空间呀。

1.5 static

image-20211217132857472

static的成员函数和非static的成员函数有区别的。

非static的成员函数在调用的时候会自动传入一个this指针,表示这个对象实例,static成员函数是没有传入this指针的,所以static的成员函数拿不到具体的实例,所以也就访问不到非static的属性呀,所以static的成员函数一定是操作static属性的,不能操作实例属性。

调用非static成员函数的时候,就相当于底下那种传了一个this指针(实际上没有这种写法),这样就能理解它到底是干了啥,static函数是没有这种this指针的。

static属性一定要在外面定义一下,因为类里面的只是一个声明。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Account{
public:
static void set_rate(const double& x) { m_rate = x; }
private:
static double m_rate;
};
double Account::m_rate = 8.0;// 一定要定义一下呀
int main()
{
Account::set_rate(5.0);
Account a;
a.set_rate(7.0);
}

单例设计模式的一个例子呀

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class A{
public:
static A& getInstance();
setup(){...}
private:
A();
A(const A& rhs);
...
};
A& A::getInstance()
{
static A a;
return a;
}

你看,我们定义了一个静态变量,这个东西和实例无关的,然后我们使用一个静态成员函数来干这件事呀。

1.6 cout

cout就是重载了很多种类型的方法来做这个东西。

1.7 类模板和函数模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
template<typename T>
class complex
{
public:
complex(T r=0, T i = 0):re(r), im(i) { }
complex& operator += (const complex&);
T real() const { return re; }
T imag() const { return im; }
private:
T re, im;
friend complex& __doapl(complex*, const complex&);
}
{
complex<double> c1(2.5,1.5);
complex<int> c2(2,5);
}

函数模板

image-20211217135308316

函数模板在泛型编程里是非常常用的一个方法。

1.8 namespace

1
2
3
4
5
6
7
8
9
10
11
namespace std
{
...
}
using namespace std; // using directive
using std::cout; // using declaration

{
std::cout << a << std::endl;
return 0;
}

1.9 面向对象程序设计- 复合、委托和继承

1.9.1 复合 has a

表示一个类中有另外的一个类的实体(会直接分配一个那个类的空间)

如下图

image-20211220122048681

image-20211220122100523

上面的例子中,queue实例中有一个Sequence实体。上面是一个模板类,就是<class T, class Sequence = deque<T> >, 第一个是一个类T,然后第二个是一个Sequence,默认参数是deque<T>,相当于下面那个Adapter

todo: 这个只是还不懂呀,为啥是这样子写呢?其实可以理解,上面那个模板是要一个Sequence,里面放了一个deque<T>

image-20211220124104045

上面那个东西,queue里面有一个deque,deque里面有两个模板类Itr

  • 复合类型下的析构和构造关系

    构造应该是由内而外的,析构应该是由外而内的

    image-20211220124807799

    container中有一个component,表现在内存就是右边这种关系,里面包着一个模块。

    如果要构造,应该是把内部的先做好

    Container::Container(...) : Componet() { ... }先实现了内部的构造,再实现外层容器的构造。

    如果要析构,应该先把外层拆掉,再拆里面

    Comtainer::~Container(...){ ... ~Component()}, 先把外层该释放的释放掉,再释放里面那个。

1.9.2 委托 has a pointer

  • 可以理解成复合by reference

  • 比如一种设计模式handle / body (pImpl)模式

    image-20211220200222580

    如图中所示,图中的String是一个字符串类,里面的实例是通过一个rep指针完成的,(带指针的要实现Big Three,析构, 拷贝构造和拷贝赋值函数)

    这样可以有很多个String实例内部的rep指针指向同一个字符串,同时这个字符串对象可以加一个count,来表示外面有多少个引用,用来做引用计数器,这个想法很nice呀。实例由一个统一的对象完成,留给外面的接口是一个单独的类,这样使用起来很方便呀。(Handle/Body的设计模式)

1.9.3 继承 is a

例子

image-20211220203330393

这个例子的意思是,所有拥有下一个和前一个的数据结构可以认为是一个List Node,至于里面具体怎么组织,更加细节的东西要留给后面的类来具体描述。

继承的内存表示

image-20211220203806124

构造时也是从内到外,析构时从外到内。父类的析构一定要是虚函数,让子类可以修改,否则子类的构造函数没法修改,可能出现未定义的行为。

1
2
Derived::Derived():Base(){ ... }
Derived::~Derived(){ ... ~Base()}
  • 虚函数virtual

    • 非虚函数:不希望子类重新定义(override)
    • 虚函数:希望子类可以定义它,并且也实现了一个自己的默认版本
    • 纯虚函数:子类一定要重新定义它,你对它没有默认的定义。
  • 下面举个例子

    模板方式的设计模式

    image-20211220204259966

框架的提供者设计了一种读取文件的方式,CDocument,里面有一个打开文件的方式,里面会有一个Seriaize()函数是对具体的文件采取不同的方法的,此时就需要将这个函数设计成虚函数,让子类可以重载,这样用户就可以在框架的基础上只需要实现一个具体文件的打开操作方式的函数Serialize函数。

如右图,我们在main函数里调用OnFileOpen函数的时候,传入一个this指针,就是调用者本身即myDoc,这里myDocCMyDoc型,是CDocument的子类,但是在这个子类中并没有OnFileOpen函数呀,但是其父类有,所以可以由子类的实例调用这个函数,然后就到父类中执行这个函数,执行到Serialize时,(类里面的函数都用都是默认写了this->的,所以就相当于this->Serialize(),这类的this依然时CMyDoc类型的,所以调用的仍然是子类实现的这个函数。(调用都理解成this指针调用的,里面的每一个属性的方法都加上this就很好理解这个东西为什么是这样的了,更深的东西还得留着后面来。

下面来表示一下:

image-20211220205312833

  • 继承加上复合关系下的构造和析构模式

    image-20211220205803276

    这个是很好理解的,子类来自于父类,所以子类里包着父类,然后父类复合一个组件,所以父类包着一个组件,所以这个模式就是这样的。构造由外到内,析构由内到外。

    image-20211220205911409

    子类里面有一个复合组件,继承自父类,所以父类和组件应该在同一个里面,那么这个时候是什么的顺序来构造和析构呢? 先调用Base的构造再是Component的构造,然后再是自己,析构是相反的,先外层,再Component再Base

    1
    2
    Derived::Derived():Base(), Component(){...}
    Derived::~Derived(){... ~Component(); ~Base()}
  • 委托加上继承的方式

    Observer设计模式为例

    image-20211220214042648

    实现了一个Subject类,主要用来存储各种数据,里面有一个Observer的容器,用来存放各种的观察方式,这些观察方式都继承于同一个父类,所以可以放在同一个容器中。

    image-20211220214231459

    可以看到这里实现了两种观察模式,分别实现update函数来更新显示出来的视图。

  • 委托加继承的另外一个例子:复合设计模式

    image-20211220214414808

    我现在有一个组件,我希望能够加入一个同类的组件(套娃),也希望能够加入一个实体,这咋办呢,这个时候就可以用这个设计模式,将实体和组件都设计成一个类的子类,这样就可以实现这个功能了。实现功能的是右下边那个add,这样它可以加入一个本身自己这种类型的组件进去,也可以加入一个Primitive的实例进去。

  • 委托加继承的另外一个惊为天人的设计模式:Prototype设计模式

    这种模式是说,我设计一个父类,但是我希望它能得知以后子类长成啥样,这个时候就可以用这种设计模式

    image-20211220214925757

    看这个例子呀,Image类是一个父类,它将来会有LandSatImageSpotImage两种子类甚至更多,但是它现在不知道,但是我们希望它能直到这些子类是长啥样,所以需要这种设计模式。

    父类有指向自己类型的容器,这里面会放实例的。子类的构造函数是私有的,所以子类不能被外界直接创建,通过再构造函数种调用父类的addPrototype方法,将自己的拷贝一份交给父类的容器中,这样自己被创建的时候会直接给父类一个模板(直接传指针,所以不是拷贝是吧),那么在哪里创建呢?因为外面无法创建,所以需要一个静态的自己,这样就可以了。那外界想创建一个这种类型的子类咋办呢?通过父类来就可以了,父类存了每种的样本,找到对应的子类之后,调用子类重新定义的clone函数将自己拷贝一份交给外界就可以。但是这会有问题,因为这样每次都给父类加一个样本,这是不希望的,所以需要重载一个不给父类模板的构造函数,怎么区分呢?参数不一样就好了,所以给一个int dummy参数,这个参数没有啥语义,仅仅为了区分两种构造函数。

    这种设计模式确实惊为天人,太优秀了。

    image-20211220220518475

    image-20211220220534731

    image-20211220220545366

    这样可扩展性比较好。

至此,C++面向对象程序设计的第一阶段的课程已经听完了。

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

请我喝杯咖啡吧~

支付宝
微信