跳转至

高级程序设计-课堂重点提要


课程概述

课程目标

  • 进一步的程序 抽象复用 技术.
  • 提升程序设计的熟练程度和规模, 为后续专业课程提供基础.
  • 掌握一下的基本程序设计的思想和做法:
  • 面向对象设计
  • 泛型(类属)程序涉及和基于STL编程
  • 程序异常处理
  • 事件(消息)驱动的程序设计
  • 基于"文档-视"结构的应用框架
  • 函数式和逻辑式程序设计
  • 编程语言: c++

重点

  • 面向对象设计
  • 泛型程序设计

课程网站

  • 网站: cslabcms.nju.edu.cn
  • 课程名称: 高级程序设计1班
  • 发布课件/通知/提交作业/交流,讨论

课程考核

  • 平时作业(10%)
  • 课后练习
  • 课程网站发布, 在规定时间内提交

  • 上机考核(25%)

  • OJ系统当场出题, 当场提交

  • 7+ 选最高的6次

    • 80分钟内满分
    • 80-110分钟线性衰减, 当天提交60%
  • 课程项目(25%)

  • 2个较大的程序(10% + 15%)[8周一个项目]

  • 课程网站发布, 在规定的时间内提交

  • 期末笔试(40%)

  • 手写编程(70%+)

省去一些前期的基础知识

之前记的一些笔记过于琐碎, 就不放进来了.


Lec 3

拷贝构造函数

shallow copy 浅拷贝

//deep copy
class string{
  char *str;
public:
  string (const &string s) {
    this->str = new char*[defaultSize];
    strcpy(str, s.str);
  } 
}
避免默认拷贝时拷贝的是地址, 导致两个指针指向同一个变量, 同时在析构时发生严重错误!!

初始化列表拷贝成员对象类 vs. 在拷贝函数内部对成员对象类进行赋值

自定义的拷贝构造默认调用的是成员对象类的 默认构造函数 来对成员对象初始化.

前者是自定义初始化, 后者是先使用默认初始化, 再进行赋值.

const关键字

常成员函数只约束数据成员值的修改(指针指向的变量时可以访问并修改的)

对于void f(const Date &d)类型的写法, 函数内部只能调用对象的常成员函数.

结构化

典型的结构化语言: C 不带goto的顺序 分支 循环 流程控制

典型的非结构化语言: 汇编 goto范式

Lec 4 继承-派生类

继承的一些要求

定义派生类时一定要见到基类的定义

默认情况下, 基类的友元关系不会继承

继承与封装的矛盾

由于派生类不能直接访问基类的private成员, 因此引入protected关键字来解决派生类访问父类的一些成员, 同时也保证了数据的封装性(外部仍然不能访问).

一般情况下, 把不太可能发生变动的, 有可能被派生类使用的, 不宜对实例用户公开 的成员声明为protected.

注意区分在外部访问还是在内部访问

void f()是外部通过一个B类的实例进行访问;

class C:public B是在C类的内部(定义)进行访问.

派生类成员标识符的作用域

对于基类而言, 派生类成员标识符的作用域是 嵌套在 基类作用域中的.

如果派生类中定义了与基类同名的成员, 则基类的成员名在派生类的作用域内不直接可见(被隐藏)

//class B : public A
void h() {
  f();
  A::f(); //通过显式的制定作用域来区分
}

关于继承的覆盖问题

问题情境:

在基类中已经有方法Robot::move(char direction), 如果在其派生类中写如上图的方法, 那么, 对于派生类对象的实例rr->move('D')会成功吗?

结论是, 在默认的情况下, r并没有int move(char direction)这一方法, 也就是说, 派生类实例无法调用基类的同名函数, 即使函数的构型(参数, 返回值)不同.

联系继承类的作用域概念, 这样的场景可以类比以下代码:

int main() {
  int foo = 1;
  for(int foo = 2; foo < 10; foo++) {
    printf("%d\n", foo);
  }
}
显然, 在for的作用域内, 原本的foo变量被覆盖了.因此在内部是没法访问外部的foo的. 避免的方法就是尽量不要起同名的变量.

但是对于函数, 多了参数列表和返回两种属性, 注意到Robot::move(char direction)SuperRobot::1::move(char direction, int pace)并不相同, 结合函数重载的知识, 这为什么不属于函数重载呢?

答案是: 作用域不同. 函数重载时要求两个函数具有相同的作用于的, 而派生类的函数是基类的扩展, 两个函数的作用域并不相同.

解决这样的同名的矛盾的方法, 比较常见的是显式指出函数的作用域, 例如`rob1->Robot::move('D')这样的写法.

但是, 如果想不加名空间限制就直接调用rob1->move(char direction)这样的写法呢? 这就要求在class SuperRobot1的public的声明中添加一句using Robot::move, 这样, 在SuperRobot的实例中, 就可以同时调用rob1->move(char direction)rob1->move(char direction, int pace)两种方法.

最后, 具体的PPT参考:

在去年的Advanced Programming的OJ中, 就出现过这样的刁钻样例, 要求外部不加名空间限制符直接调用, 因此这时候就体现出了using的重要性了.

override vs. overload

override 覆盖 对函数的重写

overload 重载

Warning

注意: 在A的派生类B中override函数f()不能算作是函数名重载!: 作用域不同!!

继承方式

派生类的属性:

  • 不可直接访问: 在派生类的内部(定义时)也不能访问基类的该成员

  • private: 在派生类的内部(定义时)可以访问, 但是外部(实例化时)不可访问, 该派生类的派生类不能在内部直接访问

  • protected: 在派生类的内部(定义时)可以访问, 但是外部(实例化时)不可访问, 该派生类的派生类内部可以直接访问

  • public: 在派生类的内部可以访问, 外部也可以访问, 派生类也可以访问.

子类型

在C++中, 把类看做类型, 把以public方式继承的派生类看做是基类的子类型

其实就相当于该派生类就是基类的扩展, 具有基类的所有功能, 因此可以有以下操作:

class B : public A

B b;
A a;
a = b;
A *p = &b;

构造函数和析构函数的顺序

如果一个类D既有基类B, 又有成员对象类M, 则

创建D类对象时, B->M->D

析构时相反.

赋值和拷贝构造

默认情况下, 对基类的赋值操作和拷贝构造采用默认拷贝构造;

除非显式指出.

Lec 5 虚函数 动态绑定 抽象类

对虚函数的几点说明

  • 静态成员函数不能是虚函数.

  • 构造函数不能是虚函数, 但是析构函数(往往)可以是虚函数.

  • 只要在基类中说明了虚函数, 在派生类中同型构的函数成员都是虚函数(不必写virtual)

  • 只有通过指针或引用访问对象类的虚函数时才进行动态绑定.

  • 基类的构造函数和析构函数中对虚函数的调用不进行动态绑定

    因为基类在构造阶段并不清楚派生类的情况. 同样, 当析构阶段, 派生类已经析构, 不能在调用其函数.

Tip

注意override的含义:"覆盖"

Lec 6 多继承 聚合/组合

继承vs. 组合

  • is-a-kind-of : 继承

  • is-a-part-of : 组合

Lec 7 操作符重载

=操作符重载

  1. 注意防止自己赋值给自己

  2. 如果自定义了拷贝构造函数, 最好也同时自定义赋值函数.