多态(virtual)

泛型(template)

实践

  • C++函数的缺省参数

    既可以在函数声明中,也可以在函数定义中声明缺省参数,但不能既在函数声明中又在函数定义中同时声明缺省参数。因此,将定义或声明中的任一个缺省参数删除即可。

翁凯C++

2 什么是面向对象

  • Objects = Attributes + Services

    data是被operation包在里面的

  • 面向过程与面向对象

  • one example

3 面向对象基本原理

  • OOP Characteristics

    • What to do not how to do
  • An object has an interface

    • The interface is the way it receives messages
    • It is define in the class the objecvt belong to
  • interface:

    • Communication
    • Protection
  • Encapsulation(封装)

    • bundle data and methods dealing with these data together in an object
    • Hide the details of the data and the action
    • Restrict only access to the publicized methods

4 自动售票机例子

5 头文件

  • The header file

  • .cpp经过编译预处理→.ii,经过编译→.s,经过汇编→.o,经过ld链接→.out

  • Declarations vs. Definitions

    如果在.h文件中variables不加extern关键字,则是对variables的定义,在多个.cpp链接该.h文件时会出现ld:duplicate symbol的错误

    加上extern关键字才是声明Declaration

  • #include:

  • Standard header file structure:

    image-20221109112932883
  • 上面这一章讲的内容的一个相关概括性文章

    多文件编程

6 时钟的例子

  • 设计对象的时候划分的粒度

    抽象Abstract

    image-20221110155850999

7 成员变量

Field, parameters, local variables

成员变量,参数,本地变量

  • 成员变量的作用域是类的作用域,在类的函数中使用
  • 本地变量只能在其所在的函数中使用

成员变量在类的每一个对象中

parameters = local variables

8 成员变量的秘密

  • 函数是属于类的,不是属于对象的

  • this:hedden parameter

    this指针指向对象自身

9 构造和析构

  • constructor function构造函数

    • 名字和类的名字相同

    • 没有返回类型

    • 在对象被创建的时候自动被调用

  • destructor function析构函数

    • 在对象生命周期scope结束的时候自动调用

10 对象初始化

  • Storage Allocation

    image-20221114125613969

    空间在进了大括号之后就会分配

    构造函数则是到定义的那一行才会调用

  • 有构造函数初始化的时候就要用构造函数,不能用括号(下图x2和y1初始化时的区别)

    image-20221114130732340

11 new&delete

Dynamic memory allocation

  • new运算符的返回值是地址

    new时会调用构造函数

    delete时会调用析构函数

  • delete带方括号是用来删除一段空间,例如数组

    1
    2
    delete p;
    delete[] p;
  • Tips for new and delete

    image-20221114132625655
  • new了之后记得delete,避免内存泄露

12 访问限制

private-public

  • C++ access control:public<protected<private

    • public【能给所有人操作的数据或方法】

    • private:只有类的成员函数可以访问,但是同一个类的对象之间可以访问private成员【数据一般都设置为private】

      因为C++的object oriented特性只是在编译的时候做的,只在编译器中检查,运行时不检查

    • protected:private在派生类中不可以被访问,而protected可以【里面一般是留给子类来操作数据的接口】

  • friend

  • class vs. struct

    image-20221114135136488

13 初始化列表

对于类A的成员变量p,初始化列表为:

1
A():p(value){cout<<"function body"<<endl;}
image-20221115071556101

initialization vs. assignment

用initializer list进行初始化做的是initialization

在constructor中赋值做的是先initialization(此时没给参数,相当于用的是default constructor;若类A有个类成员变量是一个类B的对象,且类B无default constructor,则会报错),再assignment

【default constructor】指的是没有参数的构造函数

image-20221115071837094

总之,最好用initializer list初始化,而不要在constructor的function body中做赋值

另外,父类的初始化也要放在初始化列表中

14 对象组合

  • Composition

    image-20221115075105219
    • Fully和By reference是两种内存模型

      前者是类成员变量,后者是类成员变量指针

  • 一个对象组合的例子:

    声明:

    image-20221115080007207

    构造函数写法:

    image-20221115080024846
  • initialization list

    image-20221115080150891

15 继承

继承和组合不同,但都是软件重用的方式

继承inheritance:

image-20221115080859631

一个类的interface指的是它对外公开的部分,即被public的member data和member function

  • 一些名词描述:

    image-20221115081538782
  • 一个例子:

    image-20221115094552984
  • C++:继承访问属性(public/protected/private)

  • 子类被创建时与父类构造方法的关系

  • 实例代码:

    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
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    #include <iostream>
    using namespace std;

    class A
    {
    public:
    A(int ii) : i(ii) { cout << "A:A()" << endl; }
    A() { cout << "A:A() with no arg" << endl; }
    //想让编译通过就得给一个无参的default constructor
    //在B b;时会调用A的构造函数,因为B没写构造函数
    //或者给B添加一个构造函数,在其初始化列表中给A中的i赋值
    ~A() { cout << "A:~A()" << endl; }
    void print() { cout << "i:" << i << endl; }

    protected:
    void set(int ii) { i = ii; }

    private:
    int i;
    };

    class B : public A
    {
    public:
    /*
    B() : A(10) { cout << "B:B()" << endl; }
    要在初始化列表中初始化父类A的成员变量i(即调用A的构造函数)
    有上面B的构造函数,就不会调用A的default constructor
    */
    void f()
    {
    set(100);
    print();
    }
    };

    int main()
    {
    B b;
    b.print();
    b.f();
    return 0;
    }

16 父类子类的关系

  • 名字相同,但参数表不同,是对函数的重载(overload)

    上述的写法可以避免code duplication

  • name hiding(OOP语言中只有C++有这种特性)

    对C++来说,父类A有一个overloaded函数(假设为func(int i)func()),子类B有一个同名的函数,假设为func(),则在子类中func()会覆盖掉父类继承来的func()

    想调用父类的那个函数需要b.A::func(777);

17 函数重载和默认参数

返回值不同的话不能构成overload

默认参数:void stash(int size, int intiQuility = 0);

  • default argument一定要从最右侧的参数往左开始default

    image-20221116201905461
  • default argument只能在函数声明里写,不能在函数定义里写

    因为这是再编译的时候检查的,这个函数还是需要俩参数,但是有函数声明中对参数默认值的声明,相当于在编译阶说明了这个函数的其中一个参数的值。并非生来如此,是后来说明的

18 内联函数

  • Overhead for a function call(函数调用时的额外开销)

    堆栈push和pop等开销

  • inline函数可以不需要上述操作,像预处理宏一样

    把函数代码嵌入到调用的位置,但仍然保证函数的独立性,如函数独立的命名空间等等

  • inline function

    Repeat inline keyword at declaration and definition

    image-20221116203514313
  • inline函数的body只用放在header file,即.h文件中

    inline函数的定义相当于只是声明

    image-20221116204248473
  • inline may not inline:compiler may not honor your request to make a function inline

    image-20221116204818887
  • 成员函数在类class声明的时候,就给出函数的body,则这些函数都是inline的

  • inline or not?

    image-20221116205152020

19 const

  • Pointers and const:

    对象是const:不能通过该指针来修改对象(对象本身不一定是const)

    指针是const:指针本身所指的位置不能修改

    image-20221116205922464
    • 区分的理由是:const写在星号*的前面还是后面

      const在星号前面,**const p**,const修饰的是p,i.e.对象是const

      const在星号后面,* const p,const修饰的是p,i.e.指针是const

      image-20221116210307686
  • 一些例子:

    image-20221116210648731
  • 要把一个大的对象作为参数传进函数

    为了避免拷贝等操作耗时耗力,可以用指针

    为了避免因指针导致对象被修改,可以用const

  • 对于C++中的类和对象:

    • 对象定义/声明前面加const意味着这个对象中的所有变量都不能被修改

    • 类的成员函数声明/定义时加const意味着这个函数不能修改类的成员变量

      image-20230506202036374

      因为int get_value() const { return value; }中const意味着*this是const的

20 不可修饰的对象

接19 const

  • 下面的这个函数,加了const后实际上相当于overload

    运行结果是”f() const”

    两个函数相当于:

    • void f(A* this) { cout << "f() << endl; "}
    • void f(const A* this) { cout << "f() << endl; "}

    即参数不同

  • 如果类中有个成员变量是const的,则需要在initializer list中初始化

21 引用

  • 引用需要在定义的时候给定初始值

    除了参数表,和类的成员变量

  • 函数的参数:引用和值传递不能重载,因为调用形式相同

  • 引用不能再指向别的变量

  • Restrictions:

    image-20221118191327439

    离变量最近的符号决定了变量的基本类型

22 向上造型

  • 子类的对象具有父类的对象的所有东西,所以一个子类的对象可以当做一个父类的对象使用

    image-20221118191538472
  • Upcasting:

    image-20221118192248236
  • Upcasting examples:

    image-20221118192601454

    上述例子中,**ep->print(cout);调用的是Employee的print,即父类的print**

    函数调的是父类的函数,而由于父类有的数据子类也一定会有,所以Upcasting不会出问题

23 多态性

例子:A drawing program

image-20221118193043535 image-20221118193104478
  • 只要祖先中某个函数是virtual的,其子类继承的该函数都永远是virtual的

    virtual限制的函数,有动态绑定的特性,在调用该函数时【需要通过对象的指针或引用来调用】,就调用所指对象所属的类型的对应函数

    (需要用指针或引用来调用)才有动态绑定,不然没有动态绑定

    称之为Polymorphism

    image-20221118193936618

24 多态的实现

书接上文,讲如何实现多态

有virtual的类的对象,内存开头都有一个隐藏的vptr指针,指向一张叫做vtable的表

image-20221118195848533image-20221118200024251

25 引用再研究

  • 类的成员变量是引用类型时,必须在初始化列表中初始化

    image-20221121155542728
  • 引用作为返回值时,不能返回函数内的本地变量,只能返回全局变量

    返回函数内的本地变量能过编译(有warning),但是会出现segment fault

    image-20221121155705940

    除此之外,引用作为返回值后函数可以作为左值:

    image-20221121155929906
  • 传一个很大的参数进函数:使用const reference,如const int& arg

    为了节省空间,不能传递值,可以传指针;

    为了避免函数修改外面调用者的变量的值,可以传const *pointer;

    为了更好的可读性,可以用reference

  • 函数返回类型是自己定义的类,则该函数也可以作为左值

26 拷贝构造

  • 一个对象初始化的时候用另外一个对象初始化,调用的是拷贝构造函数

  • 如果没有自己定义拷贝构造,C++会提供default copy constructor

  • 拷贝构造函数的写法应该是:A(const A& a){...},即参数应该是引用

    参数如果是个对象,则会造成对拷贝构造函数的递归调用,编译器会报error

  • 写一个类 必备的三个函数:

    • default constructor

    • virtual destructor

    • copy constructor

  • 如果不希望你写的类被拷贝,就写个private的拷贝构造

    C++将拷贝构造函数和赋值构造函数声明为私有

    • “双杀”:默认情况,编译器以“位拷贝”的方式自动生成缺省的构造函数和赋值函数。若类中有指针变量,那么缺省的构造函数会出现错误。

      因为这个指针变量所指的空间析构多次

28 静态对象

  • static: two basic meanings

    • static storage: allocate once at a fixed address

    • visibility of a name: internal linkage

      image-20221122163142288
  • C++ 中 static 静态对象的使用总结

    • 全局静态对象:

      • 全局对象在程序执行 main() 之前已经分配好存储空间 ,在 main() 执行完成后进行释放。

      • 相当于编译器在 main 函数的显示代码之前和之后分别插入了一段代码用来管理全局对象

    • 局部静态对象:

      • 局部静态对象是从其所在函数第一次被调用时进行创建,直到整个程序结束才进行销毁;局部静态对象只有在第一次调用时进行初始化操作。
      • 利用局部静态对象只会初始化一次、并且不使用不创建的特点,可以用来实现单例模式

29 静态成员

C++ static静态成员变量详解

  • 注意:static 成员变量的内存既不是在声明类时分配,也不是在创建对象时分配,而是在(类外)初始化时分配。反过来说,没有在类外初始化的 static 成员变量不能使用。
  • 静态成员变量在初始化时不能再加 static,但必须要有数据类型。被 private、protected、public 修饰的静态成员变量都可以用这种方式初始化。
  • 不能在初始化列表中初始化static成员变量,只能在其定义的时候初始化(即在类外)
    "message": `"'int A::i' is a static data member; it can only be initialized at its definition"`
    
  • 可以用Student::m_total = 10;来访问,即直接用类的名字来访问

对于成员变量,Hidden的功能已经可以由private/protected来实现

剩下的Persistent功能:Independent of instances

  • 静态成员变量在所有的对象中是保持一致(persistent)的

    有时候我们希望在多个对象之间共享数据,对象 a 改变了某份数据后对象 b 可以检测到。共享数据的典型使用场景是计数,以前面的 Student 类为例,如果我们想知道班级中共有多少名学生,就可以设置一份共享的变量,每次创建对象时让该变量加 1。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
using namespace std;
class A
{
private:
static int i;

public:
A() {}
void write(int arg) { this->i = arg; }
void read() { cout << this->i << endl; }
};

int A::i = 0;//在类的外面定义的时候要初始化
//且要用"className::varName"来定义,不能只用一个i,不然会"ld:undefined reference to 'A::i'"
int main()
{
A a;
a.read();
a.write(7);
a.read();
return 0;
}

C++ static静态成员函数详解

  • 静态成员函数与普通成员函数的根本区别在于:普通成员函数有 this 指针,可以访问类中的任意成员;而静态成员函数没有 this 指针只能访问静态成员(包括静态成员变量和静态成员函数)。
  • 和静态成员变量类似,静态成员函数在声明时要加 static,在定义时不能加 static。静态成员函数可以通过类来调用(一般都是这样做),也可以通过对象来调用,上例仅仅演示了如何通过类来调用。
  • 其与类的对象存在与否无关,在对象不存在的时候就可以被调用

30 运算符重载-基本规则

  • 允许重载的运算符

    image-20221122200908922
  • 不允许重载的运算符

    image-20221122201009451
  • Restriction限制:

    image-20221122201234857
  • operator关键字+重载的运算符

    const String String::operator + (const String& that);

    const String operator + (const String& l, const String& r);

    image-20221122201545217

    作为member function,不能做z=3+y

    作为global function,能做z=3+y

    image-20221123211927920

31 运算符重载-原型

【翁恺】运算符重载-原型

本章就看上面的记录即可

【是否返回引用】操作符对receiver自己做了修改,则返回引用(如++,–,=);若返回了一个新的对象,则不返回引用,返回一个对象(如+,-,*,/)

【是否返回const】返回值能不能做左值,能做左值的返回类型不带const(如=,[]),不能做左值的返回类型要带const(如+,a+b=6错误)

【参数一定引用】重载一定对某个对象进行重载,所以传进去的参数一定是引用类型的。

【参数是否const】参数能不能加const(member function对receiver自身的const加在函数原型尾部),修改算子的不能加const(如++,–,=),不修改算子的参数要加const,this加const(如+,-,*,/)


C++运算符结合性、优先级详解

根据结合性,所有的关系运算符均为左结合

所以对于关系运算符的重载,如bool operator<(const node &a) const,表达的实际上是**this<a**

32 运算符重载-赋值

【翁恺】运算符重载-赋值

本章同样看上面的记录

讲的是运算符重载中,赋值运算符重载这个特殊的例子

  • 默认的赋值运算符:

    • the compiler will automatically create a type::oprator=(type) if you don’t make one
    • 它做的是memberwise assignment,即所有成员变量直接赋值
  • 赋值运算符重载的标准写法:

    1
    2
    3
    4
    5
    6
    7
    T& T::operator=(const T& rhs){
    //check for self assignment
    if(this != &rhs){//如果是自己赋值给自己,可能在自己被赋值之前被释放,比如new到的东西
    //perform assignment
    }
    return *this;
    }//this checks address vs. check value (*this != rhs)

33 运算符重载-类型转换

【翁恺】运算符重载-类型转换

  • compilers perform implicit conversions using:

    • single-argument constructor

      • 即先调用单个参数的构造函数将参数构造为目标参数类型的对象,再调用目标函数,上面记录里有相关例子

      • 要阻止隐式调用构造函数来进行类型转换,可以在构造函数前面加上关键字explicit

    • implicit type conversion operators

      • operator name is any type descriptor

        操作符的名字是某一个类型,如double

      • return type is same as function name

        No return type:没有返回类型

      • No explicit arguments

        没有显式的参数

1
2
3
4
5
6
7
8
9
10
class Rational{
public:
...
operator double() const;//Rational to double this 变成 double
}
Rational::operator double() const{ // 没有返回类型
return numerator_/(double)denominator_;
}
Rational r(1,3);
double d = 1.3*r;//r=> double

用运算符重载的类型转换还是尽量别用, 因为他是implicit的,不容易理解,还是用专门的例如C++中的to_string()这种函数比较好

34/35 模板

【翁恺】模板

Template:use types as parameters in class or function definitions

function template; templated function; class template; templated class

例子如下:template生效的是跟在template<class T>后面的函数or类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
using namespace std;

template <class T>
void mySwap(T &a, T &b)
{
T tmp = a;
a = b;
b = tmp;
return;
}

int main()
{
double a = 1, b = 2;
mySwap(a, b);
return 0;
}
  • 如果function template的参数表中没有用到T,则需要加一个尖括号,在尖括号中指出T的类型

    1
    2
    3
    4
    template <class T>
    void foo(void){...}
    foo<int>();//type T is int
    foo<float>();//tyoe T is float

class template

1
2
3
4
5
6
7
8
9
10
11
12
13
template <class T>
void sort(vector<T>& arr){ // 函数模板 声明
const size_t last = arr.size()-1;
for(int i = 0;i < last; i++){
for(int j = last; i < j; j--){
if(arr[j] < arr[j-1]){
//which swap?
swap(arr[j], arr[j-1]);
//T要重载"<"运算符,还要支持swap函数
}
}
}
}

Pair类模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
#include <string>
using namespace std;
template <class T1,class T2>
class Pair
{
public:
T1 key; //关键字
T2 value; //值
Pair(T1 k,T2 v):key(k),value(v) { };
bool operator < (const Pair<T1,T2> & p) const;
};
template<class T1,class T2>
bool Pair<T1,T2>::operator < (const Pair<T1,T2> & p) const
//Pair的成员函数 operator <
{
return key < p.key;
}
int main()
{
Pair<string,int> student("Tom",19); //实例化出一个类 Pair<string,int>
cout << student.key << " " << student.value;
return 0;
}

36 异常基本概念