OOP 学习笔记(3)——创建与销毁

OOP 学习笔记(3)——创建与销毁

Contents

创建与销毁

面向对象编程如何保证可靠性

  • 对对象类型进行严格检查。
  • 隐藏实现,防止不必要干扰
  • 自动初始化与销毁对象

但是初始化与销毁又具有特异性,所以,其方法应由设计者决定,但时间应由编译器决定

构造函数

对象的初始化是由编译器在创建对象处自动生成调用构造函数的代码来完成。

而构造函数则是类的一种特殊成员函数,用于确保每个对象都能正确初始化。

构造函数的特点

  • 构造函数无返回值,函数名即为类名。
  • 类构造函数可重载。

一个简单的例子:

class Pos
{
    int x;
    int y;
public:
    Pos()
    {
        x = y = 0;
    }
    Pos(int X, int Y)
    {
        x = X, y = Y;
    }
};

构造函数的初始化列表

构造函数可以使用初始化列表初始化成员数据。

一般形式为:

ClassName(parameters) : member(init_val), ...
{
    //...
}

其中 member 为类的成员变量,init_val 为其初始值,用逗号分离不同的成员变量。

比如上面的就可以简写为:

class Pos
{
    int x;
    int y;
public:
    Pos(int X = 0, int Y = 0) : x(X), y(Y) {}
};

注意,初始化列表中的执行顺序为类中定义顺序,而非初始化列表中的顺序。

在构造函数的初始化列表中,还可以调用其它构造函数,被称为委派构造函数,具体运行时会先执行初始化列表中相关初始化,再执行构造函数主体中内容。

比如:

class Data
{
public:
    Data()
    {
        Init();
    }
    Data(int ID) : Data() // 委派构造函数
    {
        id = ID;
    }
    Data(char g) : Data()
    {
        gender = g;
    }
private:
    void Init()
    {
        //...
    }
    int id;
    char gender;
};

就地初始化

在 C++11 之后(之前不行)支持如下初始化:

class Data
{
private:
    int id = 2020;
    char gender {'F'};
};

默认构造函数

不带任何参数的构造函数,被称为默认构造函数,也称缺省构造函数

对象定义格式为:

ClassName Obj; // 千万不能加括号

显式声明构造函数

对于构造函数来说,一旦显式实现了一个构造函数,编译器不会再自动产生默认版本的构造函数。

当然特殊情况下,我们也能手动指定生成默认版本的构造函数。

class Data
{
public:
    Data() = default; // 手动指定
private:
    int id;
    char gender;
};

显式删除构造函数

对于如下代码:

class Data
{
    int id;
public:
    Data() = default;
    Data(int ID) : id(ID) {}
};

Data data('x');

这段代码会将 'x' 先转换为 int 类型,然后再调用 Data(int ID)

然而实际上,用字符初始化应该是未定义行为。

所以我们可以使用 delete 显式地删除构造函数,避免产生未预期的行为。

class Data
{
    int id;
public:
    Data() = default;
    Data(int ID) : id(ID) {}
    Data(char ch) = delete;
};

Data data('x'); // 程序报错

对象数组的初始化

  • 调用默认构造函数:
    Data data[100];
    
  • 调用构造函数只有一个参数:
    Data data[5] = {2, 3, 5, 7, 11};
    
  • 调用构造函数有多个参数:
    Data data[5] = {Data(2, 3), Data(3, 4), Data(5, 6), Data(7, 8), Data(11, 12)};
    
  • 当然数组中,不同元素可以调用不同的构造函数,没有特别指定的调用默认构造函数,比如:
    Data data[5] = {2, Data(3, 4)};
    

析构函数

对象作用域结束处,编译器自动生成调用析构函数代码。

清除对象无条件,故析构函数无参数且唯一。

一个类唯一的析构函数名称是 ~ClassName(类名),无返回值无函数参数。

实例

class Data
{
    int *data;
public:
    Data()
    {
        data = new int[100];
    }
    ~Data()
    {
        delete [] data;
    }
};

类中静态成员

使用 static 修饰符修饰的数据成员,称为类的静态数据成员,也称类变量

静态数据成员被所属类的所有对象共享,也就是所有对象的这个成员变量是同一个变量。

同样还有用 static 修饰的成员函数被称为类的静态成员函数

类的静态成员既可以通过对象访问,也可通过类名访问。

类静态数据需要在实现文件中赋初值,格式为:

Type ClassName::static_var = Value;

课件练习

#include <iostream>
using namespace std;

class Test
{
    static int count;
public:
    Test()
    {
        count++;
    }
    ~Test()
    {
        count--;
    }
    static int how_many()
    {
        return count;
    }
};

int Test::count = 0; // 静态数据成员的初始化

void print(Test t)
{
    cout << "in print(), Test#: " << t.how_many() << endl;
}

int main()
{
    Test t1;
    cout << "Test#: " << Test::how_many() << endl;

    Test t2 = t1;
    cout << "Test#: " << Test::how_many() << endl;

    print(t2);

    cout << "Test#: " << t1.how_many() << ","
         << t2.how_many() << endl;
    return 0;
}

输出结果为:

Test#: 1
Test#: 1
in print(), Test#: 1
Test#: 0, 0

提示:初始化赋初值不调用构造函数。

静态成员函数只能调用静态成员!!

(考虑到未实例化,就调用静态成员函数的可能)

静态数据成员的多文件编译

静态数据成员应该在 .h 头文件中声明,在 .cpp 源文件中初始化。

否则若在 .h 中同时声明初始化,则此头文件若被包含多次,这些变量就被定义了多次,导致编译失败。

类中的常量成员

使用 const 修饰符修饰的数据成员,称为类的常量数据成员,在对象的整个声明周期里不可更改。

常量数据成员可以:

  • 在构造函数的初始化列表中被初始化;
  • 就地初始化;
  • 不可在构造函数的函数体中通过赋值设置。

成员函数也可用 const 修饰符修饰,称为常量成员函数

常量成员函数的定义中,不能修改类的数据成员(修改对象状态)。

定义形式为:

ReturnType FuncName(parameters) const
{
    //...
}

若对象被定义为常量,则其只能调用以 const 修饰的成员函数。

实例

#include <iostream>
using namespace std;

class Test
{
    const int ID;
public:
    Test(int id) : ID(id) {}
    int MyID() const
    {
        return ID;
    }
    int Who()
    {
        return ID;
    }
};

int main()
{
    Test obj1(20200202);
    cout << "ID_1 = " << obj1.MyID() << endl;
    cout << "ID_2 = " << obj1.Who() << endl;

    const Test obj2(20200204);
    cout << "id_1 = " << obj2.MyID() << endl;

    return 0;
}

常量静态数据成员

可以定义既是常量也是静态的数据成员,称为类的常量静态数据成员

常量静态数据成员需要在类外初始化,但是 intenum 类型例外,可以就地初始化

满足访问权限的任意函数均可访问常量静态数据成员,但不能修改。

:不存在常量静态函数。

class foo
{
    static const char* cs; // 不可就地初始化
    static const int i = 3; // 可以就地初始化
    static const int j; // 也可以在类外定义
};

const char* foo::cs = "foo C string";
const int foo::j = 4;

对象的构造与析构时机

局部对象

在程序执行到该局部对象的代码时被初始化。

在局部对象作用域结束时被析构。

全局对象和作为类变量的对象

如果对象 A 是类 B 的静态变量,其构造与析构表现和全局对象类似。

main() 函数调用之前进行初始化。

在同一编译单元中,按照对象定义顺序进行初始化。

编译单元——通常以源文件为限定。即同一编译单元就是同一源文件。

不同编译单元中,对象初始化顺序不确定。

main() 函数执行完 return 之后,对象被析构。

考虑到全局对象构造顺序的不完全确定性,一般建议尽量少用全局对象。

函数静态对象

即在函数内部定义的用 static 修饰的对象。

在程序执行到该局部静态对象的代码时被初始化。

离开作用域不析构。

之后字再次执行到该对象代码时,不再初始化,直接使用上一次对象。

main() 函数结束后被析构。(不构造便不会析构)

参数对象

  • 若传递的为形参。
    • 函数调用时,被构造,调用拷贝构造函数(之后讲)进行初始化。
    • 函数结束时,调用析构函数。
  • 若参数为对象的引用。
    • 构造函数与析构函数均不调用。

尽量使用对象引用作为参数,不然可能导致同一块地址被多次释放而报错(多次调用析构函数释放同一地址)。

对象的 newdelete

new

生成一个类对象,并返回地址(构造函数会被调用)。

格式一般为:

ClassName *pObj = new ClassName(parameters);

delete

删除该类对象,释放内存资源(析构函数会被调用)。

格式一般为:

delete pObj;

生成一个类对象的数组

格式一般为:

ClassName *pObj = new ClassName[3];

具体过程为:

  1. 调用 operator new [] 标准库函数来分配足够大的原始未类型化内存,注意要多出 $4$ 个字节来存放数组的大小(前 $4$ 个字节)。
  2. 在刚分配的内存上运行构造函数对新建的对象进行初始化构造。
  3. 返回指向新分配并构造好的对象数组的指针。

删除一个类对象的数组

格式一般为:

delete [] pObj;

具体过程为:

  1. 对数组中各对象运行析构函数,数组大小存在前 $4$ 个字节中。

  2. 调用 operator delete[] 标准库函数释放申请的空间。

    不仅释放对象数组所占空间,还有上面的四个字节。

注意:一定要将 newdeletenew[]delete[] 配套使用。

 

点赞 0

No Comments

Add your comment