目录

C++ 面向对象编程知识体系 系列一: 构造函数与析构函数

这篇文章总结了 C++ 中面向对象编程的知识体系:构造函数和析构函数,包括其中的底层实现等等。

1 面向对象特性

1. 类与对象

问题
什么是类与对象
  • 对象是现实中的对象在程序中的模拟
  • 是同一类对象的抽象,对象是类的某一特定实体
  • 是一种用户自定义的类型,包含函数与数据的特殊结构体
示例

举一个简单的例子,

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Orange{
public:
  int GetOrangeDegree(); // orange 的成员方法
  int GetSourDegree();
  int GetSweetDegree();
  int GetWaterRatio();
private:
  int m_waterRatio;
  int m_orangeDegree;
  int m_sourDegree;
  int m_sweetDegree;
}

这里所举的例子,Orange是类,但橙子有分不同个体,它们都有对应的酸度、甜度、水分和生长高度,所以不同的橙子个体代表着这一类橙子的不同对象。

类成员的访问控制

  • 访问权限分为公有类型public,保护类型protected,私有类型private
  • 成员默认访问权限为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
class Teacher;
class Student{
public:
  void SetId(uint32_t id){m_id = id;}
  uint32_t GetId();
  friend Teacher;
  friend void PrintStudentId(Student);
private:
  uint32_t m_id = 0;
  uint32_t m_age = 18;
}

class Teacher{
public:
  void PrintStudentId(Student stud)
  {
    std::cout << stud.m_id << std:endl;
  }
}

void PrintStudentId(Student stud)
{
  std::cout << stud.m_id << std:endl;
}

我们定义了 class Teacher, TeacherStudent的友元类,PrintStudentIdStudent的友元函数,该函数可用于访问Student类的保护成员或者私有成员。

例如:就像上面所写的,Teacher类里面定义的PrintStudentId函数可以访问Student类的私有成员stud.m_id

警告
  1. 友元函数是单向的: 打个比方,StudentTeacher当成朋友,而Teacher不把Student当朋友。就如上面的例子,TeacherStudent的友元,但是Student不是Teacher的友元,所以Teacher类的成员函数在没有friend的关键字作用下,不能访问Teacher类的保护成员或者私有成员。
  2. 友元函数不是类的成员。

2. 类的构造与析构

构造函数

  • 构造函数是用于构造函数的特殊函数,在对象被创建时被调用以初始化对象
  • 未定义构造函数时,编译器自动生成不带参数的默认版本
  • 执行构造函数时先执行其初始化列表,再执行函数体
  • 构造函数和其他函数一样,允许被重载和被委托
  • 构造函数的名称与类的名称是完全相同的,并且不会返回任何类型,也不会返回void。构造函数可用于为某些成员变量设置初始值。

构造函数主要有以下三个方面的作用:

  • 给创建的对象建立一个标识符;
  • 为对象数据成员开辟内存空间;
  • 完成对象数据成员的初始化。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Student{
public:
  Student();
  Student(uint32_t id);
  Student(uint32_t id, uint32_t age);
  void SetId(uint32_t id){m_id = id;}
  uint32_t GetId(){return m_id;}
private:
  uint32_t m_id = 0;
  uint32_t m_age;
}

关于以上的Student类,我们声明了三个Student的构造函数。第一个构造函数Student()不带参数。三个构造函数的具体定义如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Student::Student(uint32_t id, uint32_t age):m_id(id), m_age(age)
{
  // do initialization
}
Student::Student():Student(0,18)
{
  // do initialization
}
Student::Student(uint32_t id):Student(id,18)
{
  // do initialization
}

按照上面的写法,我们可以使用初始化列表进行初始化字段。假设有一个类C,具有多个字段X、Y、Z等需要进行初始化,同理地,您可以使用上面的语法,只需要在不同的字段使用逗号进行分隔,如下所示:

1
2
3
4
C::C( double a, double b, double c): X(a), Y(b), Z(c)
{
  ....
}
问题
既然构造函数允许被重载,构造函数是如何做到被重载呢?

需要注意的是, 在进行构造函数的重载时要注意重载和参数默认的关系要处理好, 避免产生代码的二义性导致编译出错, 例如以下具有二义性的重载:

示例
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Point(int x = 0, int y = 0)     //默认参数的构造函数
{
  xPos = x;
  yPos = y;
}

Point()     //重载一个无参构造函数
{
  xPos = 0;
  yPos = 0;
}

在上面的重载中, 当尝试用Point类重载一个无参数传入的对象M时, Point M; 这时编译器就报一条error: call of overloaded 'Point()' is ambiguous的错误信息来告诉我们说 Point 函数具有二义性。

这是因为Point(int x = 0, int y = 0)全部使用了默认参数, 即使我们不传入参数也不会出现错误, 但是在重载时又重载了一个不需要传入参数了构造函数Point(), 这样就造成了当创建对象都不传入参数时编译器就不知道到底该使用哪个构造函数了, 就造成了二义性。

析构函数

与构造函数相反, 析构函数是在对象被撤销时被自动调用, 用于对成员撤销时的一些清理工作, 例如在前面提到的手动释放使用newmalloc进行申请的内存空间。析构函数具有以下特点:

  • 析构函数函数名与类名相同, 紧贴在名称前面用波浪号 ~ 与构造函数进行区分, 例如: ~Point();
  • 构造函数没有返回类型, 也不能指定参数, 因此析构函数只能有一个, 不能被重载;
  • 当对象被撤销时析构函数被自动调用, 与构造函数不同的是, 析构函数可以被显式的调用, 以释放对象中动态申请的内存。

某种意义上理解析构函数是回收间接资源的函数。

/析构函数.png
图1:析构函数回收间接资源

 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
#include <iostream>
#include <cstring>
using namespace std;

class Book
{
 public:
     Book( const char *name )      //构造函数
    {
        bookName = new char[strlen(name)+1];
        strcpy(bookName, name);
    }
    ~Book()                 //析构函数
    {
        cout<<"析构函数被调用...\n";
        delete []bookName;  //释放通过new申请的空间
    }
    void showName() { cout<<"Book name: "<< bookName <<endl; }

 private:
    char *bookName;
};

int main()
{
    Book CPP("C++ Primer");
    CPP.showName();

    return 0;

}

代码中创建了一个Book类, 类的数据成员只有一个字符指针型的bookName, 在创建对象时系统会为该指针变量分配它所需内存, 但是此时该指针并没有被初始化所以不会再为其分配其他多余的内存单元。在构造函数中, 我们使用new申请了一块strlen(name)+1大小的空间, 也就是比传入进来的字符串长度多1的空间, 目的是让字符指针bookName指向它, 这样才能正常保存传入的字符串。

main函数中使用Book类创建了一个对象CPP, 初始化bookName属性为"C++ Primer"。从运行结果可以看到, 析构函数被调用了, 这时使用new所申请的空间就会被正常释放。

自然状态下对象何时将被销毁取决于对象的生存周期, 例如全局对象是在程序运行结束时被销毁, 自动对象是在离开其作用域时被销毁。

如果需要显式调用析构函数来释放对象中动态申请的空间只需要使用 对象名.析构函数名(); 即可, 例如上例中要显式调用析构函数来释放bookName所指向的空间只要:

1
CPP.~Book();

4. 类的继承

5. 类的多态

6. RTTI 与抽象类