第20讲:初窥面向对象程序设计

《计算机程序设计》

苏醒

计算机学院计算机研究所编译系统研究室

课前准备

  • 在WSL Ubuntu中建立一个专门的目录用于课上练习,如~/course
$ mkdir ~/course
  • 打开VSCode并连接到WSL,打开此目录并新建文件,如~/course/main.cpp
  • 编辑main.cpp,随堂编程练习的代码请直接在此文件中编辑
main.cpp
#include <iostream>
#include <iomanip>
using namespace std;

int main() {
  // ============= begin =============

  // =============  end  =============
  return 0;
}

温故

  • 阅读下列程序,判断程序输出
文件读写
#include <fstream>
#include <iostream>
using namespace std;
int main() {
  int arr[5] = {1, 2, 3, 4, 5};
  ofstream fout("data.txt");
  for (int i = 0; i < 5; i++)
    fout << arr[i] << ' ';
  fout.close();
  ifstream fin("data.txt");
  int value;
  while (not fin.eof()) {
    fin >> value;
    cout << value << ' ';
  }
  return 0;
}

学习目标

  • 学习内容
    • 类与对象
    • 构造函数与析构函数

学习目标

  • 掌握C++中类的定义与对象的使用方法
  • 理解访问控制、接口-实现分离的理念
  • 理解构造函数与析构函数的作用

一、类与对象

引入

考考你

使用文件流打开文件,和一般的函数调用有什么不同之处?

使用文件流打开文件
ofstream fout;
fout.open("out.txt");
fout << "Hello world!\n";

明察秋毫

fout.open()是一个对象方法,不是普通函数

1.1 类的定义

  • C++支持面向对象程序设计,允许用户定义
  • 类可以同时封装数据计算,其成员分为
    • 变量成员(3–4行),简称变量
    • 函数成员(6–12、14行),也称方法
  • 类支持访问控制,可以设置成员的访问权限
    • public成员可以被外界访问
    • private成员不可被外界访问
类的定义
class List {
private:
  Node *head;
  Node *tail;
public:
  int length() const;
  Node *getHead();
  Node *getTail();
  Node *get(int index) const;
  Node *insert(int index, int value);
  Node *insert(int index, double value);
  Node *remove(int index);
private:
  void find(int index, Node *&prev, Node *post);
}; // class List

1.2 对象

  • 类定义的本质是向类型系统注册新类型
  • 注册新类型后即可使用新类型定义变量,称为实例化
  • 可以通过对象成员运算符(.)与指针成员运算符(->)访问对象的数据与方法
对象的定义与使用
List l;
l.insert(0, 1);
l.insert(0, 2.0);
List *pl = &l;
pl->insert(0, 3);
cout << l.getHead()->value.i << '\n';

考考你

类对象成员的访问方式和结构体对象有没有区别?

1.3 类与结构

  • 在C++中,结构(struct)与类(class)没有本质区别
    • 结构体也可以定义成员函数,也支持访问控制
结构体成员函数与访问控制
struct Node {
public:
  enum Type { INT, DOUBLE };
private:
  Type type;
  union {
    int i;
    double d;
  } value;
  Node *next;
public:
  Type getType() const;
  int getInt() const;
  double getDouble() const;
}; // struct Node

明察秋毫

struct的成员默认为publicclass的成员默认为private仅此差别

1.4 访问控制

  • 类成员的访问控制用于控制哪些成员可以由外界访问
    • “外界”指不属于本类的代码
accesscontrol.cpp
struct Node {
public:
  enum Type { INT, DOUBLE };
private:
  Type type;
  union {
    int i;
    double d;
  } value;
public:
  Type getType() const;
  int getInt() const;
  double getDouble() const;
}; // struct Node

int processNode(Node &n) {
  int x = n.getInt();
  int y = n.value.i;
  return x;
}
$ g++ -c code/accesscontrol.cpp
code/accesscontrol.cpp: In function ‘int processNode(Node&)’:
code/accesscontrol.cpp:18:13: error: ‘Node::<unnamed union> Node::value’ is private within this context
   18 |   int y = n.value.i;
      |             ^~~~~
code/accesscontrol.cpp:9:5: note: declared private here
    9 |   } value;
      |     ^~~~~

考考你

访问控制的目的是什么?

1.4 访问控制

考考你

  • 开发人员认为联合体成员value的成员命名不佳,希望改为intValdoubleVal,下面两种设计哪种更好维护?
    • 变量成员私有,使用getInt()getDouble()方法访问
    • 变量成员共有,使用value.ivalue.d访问
  • 访问控制的目的
    • 限定类的公共接口,达到接口-实现分离目的
    • 避免外界对对象的意外修改导致软件BUG

教员箴言

接口-实现分离可以显著提升代码的可维护性!

1.5 接口-实现分离

  • 接口定义在头文件中,实现定义在源文件中
    • 修改实现不改变接口
Node.h
#pragma once

struct Node {
public:
  enum Type { INT, DOUBLE };
private:
  Type type;
  union {
    int i;
    double d;
  } value;
  Node *next;
public:
  Type getType() const;
  int getInt() const;
  double getDouble() const;
  Node *getNext() const { return next; }
}; // struct Node
Node.cpp
#include "Node.h"

Node::Type Node::getType() const {
  return type;
}
int Node::getInt() const {
  return value.i;
}
double Node::getDouble() const {
  return value.d;
}

1.6 静态成员

  • 类的成员可以使用static关键字修饰,称为静态成员
    • 静态成员属于类本身,而非类的对象,在内存中只有一份拷贝,所有对象共享
staticmember.cpp
#include <iostream>
class Point {
  static int count;  // 静态成员变量
  double x, y;
public:
  // 静态成员函数
  static int getCount() { return count; }
  int increaseCount() { return ++count; }
}; // class Point
// 静态成员变量必须在类的外部定义,且必须初始化
int Point::count = 0;
int main() {
  Point p1, p2;
  std::cout << p1.increaseCount() << std::endl;
  std::cout << p2.increaseCount() << std::endl;
  std::cout << Point::getCount() << std::endl;
  return 0;
}
$ g++ -o code/staticmember code/staticmember.cpp
$ ./code/staticmember
1
2
2

考考你

  • 静态成员变量的存储类型是?
  • 静态成员函数是否可以访问非静态成员变量?
  • 它和函数内部的静态变量有无类似之处?

考考你

  • 静态成员变量的存储类型是?静态存储
  • 静态成员函数是否可以访问非静态成员变量?
  • 它和函数内部的静态变量有无类似之处?都是静态存储,前者独立于具体对象,后者独立于函数调用

二、构造函数与析构函数

2.1 构造函数与析构函数

  • C++类中有两类特殊方法,分别称为构造函数析构函数,二者均无返回值
    • 构造函数用于初始化对象,在对象建立时被自动调用
    • 析构函数用于释放对象资源,在对象销毁时被自动调用
List.h
#pragma once
#include <iostream>
#include "Node.h"
class List {
private:
  Node *head, *tail;
public:
  List() : head(nullptr), tail(nullptr) { std::cout << "Constructor" << '\n'; }
  ~List() {
    std::cout << "Destructor" << '\n';
    for (Node *p = head; p; p = p->getNext())
      delete p;
  }
  Node *getHead() { return head; }
  Node *getTail() { return tail; }
}; // class List
List.cpp
#include <iostream>
#include "List.h"
int main() {
  std::cout << "Before creating List" << std::endl;
  List list;
  std::cout << "After creating List" << std::endl;
  return 0;
}
$ g++ -o code/List code/List.cpp
$ ./code/List
Before creating List
Constructor
After creating List
Destructor

2.2 重载构造函数

  • C++类可以提供多个构造函数,用于执行不同的初始化操作
Node2.cpp
#include <iostream>
struct Node {
public:
  enum Type { INT, DOUBLE };
  Node(int i, Node *next = nullptr) : type(INT), next(next) { value.i = i; }
  Node(double d, Node *next = nullptr) : type(DOUBLE), next(next) { value.d = d; }
  int getInt() const { return value.i; }
  double getDouble() const { return value.d; }
private:
  Type type;
  union { int i; double d; } value;
  Node *next;
}; // struct Node
int main() {
  Node n1(1);
  Node n2(2.0, &n1);
  std::cout << n2.getInt() << std::endl;
  std::cout << n2.getDouble() << std::endl;
  return 0;
}
$ g++ -o code/Node2 code/Node2.cpp
$ ./code/Node2
0
2

2.3 默认构造函数

  • 每个类都至少有一个构造函数
    • 如果无显式定义构造函数,编译器会自动提供一个默认构造函数
    • 如果有显式定义构造函数,则不再提供默认构造函数
defaultctor.cpp
#include <iostream>
class Point {
private:
  double x, y;
public:
  void print() const {
    std::cout << "(" << x << ", " << y << ")";
  }
}; // class Point
int main() {
  Point p1;
  p1.print();
  return 0;
}
$ g++ -o code/defaultctor code/defaultctor.cpp
$ ./code/defaultctor
(6.92504e-310, 6.92504e-310)

明察秋毫

默认构造函数无参数,不执行任何操作

2.4 拷贝构造函数

  • 拷贝构造函数用于复制对象
    • 复制对象时,编译器会自动调用拷贝构造函数
    • 拷贝构造函数的参数为常量引用类型
copyctor.cpp
#include <iostream>
class Point {
  double x, y;
public:
  Point(double x, double y) : x(x), y(y) {}
  Point(const Point &p) : x(p.x), y(p.y) {
    std::cout << "Copy constructor called\n";
  }
}; // class Point
Point copy(Point p) { return p; }
int main() {
  Point p1(1, 2);
  Point p2(p1);
  Point p3 = p2;
  Point p4 = copy(p3);
  return 0;
}
$ g++ -o code/copyctor code/copyctor.cpp
$ ./code/copyctor
Copy constructor called
Copy constructor called
Copy constructor called
Copy constructor called

考考你

拷贝构造函数的4次调用分别发生在哪里?

总结

本节内容

---
config:
  look: handDrawn
  themeVariables:
    fontSize: 24px
---
mindmap
初窥面向对象
  构造函数与析构函数
    构造函数
    析构函数
    重载构造函数
    默认构造函数
    拷贝构造函数
  类与对象
    类
    对象
    类与结构体
    访问控制
    接口-实现分离
    静态成员

学习目标

  • 掌握C++中类的定义与对象的使用方法
  • 理解访问控制、接口-实现分离的理念
  • 理解构造函数与析构函数的作用

计算机程序设计