跳转至

类与对象

1 结构与类

  1. 结构是 C 语言中的一种自定义的数据类型;
  2. C++ 对结构进行了扩充:在结构中不仅有不同类型的数据,还可以有函 数和访问控制;
  3. 结构中的数据和函数都是结构的成员,分别称为数据成员和成员函数;
  4. 扩充后的结构就是类。
  • 结构中的私有成员只能为该结构的其他成员所访问,其他不行
  • C++ 规定:默认情况下,结构的成员是公有的,谁都可以访问
  • 封装规定:数据成员一般为私有
  • C++ 通过引入类解决这一问题:把 struct 变成 class,结构即成为类。

2 类和对象的概念

类(class)是面向对象系统中最基本的组成元素,是一种自定义数据类型。在C++中,类是一些具有相同属性和行为的对象的抽象。 对象是某个特定类所描述的实例。现实世界中的任何一种事物都可以看成一个对象(Object),即万物皆对象。

3 类的定义

3.1 定义格式

class 类名 {
    private:
    数据成员或成员函数;
    protected:
    数据成员或成员函数;
    public:
    数据成员或成员函数;
};

3.2 类成员访问控制权限

默认情况下为 public

3.2.1 public 公有类型

public 声明成员为公有成员。定义了类的外部接口,对外是完全公 开的,即提供了外部对象与类对象相互之间交互的接口。在类外只 能访问该类的公有成员。公有成员通常都是成员函数。

3.2.2 private 私有类型

private 声明成员为私有成员。具有这个访问控制级别的成员对类外是完全保密的, 只能被它本类中的成员函数或该类的友元函数访问。其他来自类外部任何访问都是非法的。

私有类型的访问方法 sample:

#include<iostream>

using namespace std;

class Human {
private:
    int weight;
    int stature;
public:
    void SetStature(int s) {
        stature = s;
    }
    void SetWeight(int w) {
        weight = w;
    }
    void GetStature() {
        cout<<"Your stature is:"<<stature<<endl;
    }
    void GetWeight() {
        cout<<"Your weight is:"<<weight<<endl;
    }
};

int main() {
    Human Tom;
    // Tom.weight = 80;
    // Tom.stature = 185;
    Tom.SetStature(185);
    Tom.SetWeight(90);
    Tom.GetStature();
    Tom.GetWeight();

    return 0;
}

3.2.3 protected 保护类型

protected 声明成员为保护成员。具有这个访问控制级别的成员,外界是无法直接访问的。它只能被它所在类及从该类派生的子类的成员函数及友元函数访问。 保护成员和私有成员的性质相似,其差别在继承过程中对产生的新类影响不同。

3.3 成员函数实现方式

  1. 在类中进行函数原型说明,而函数体则在类外进行定义。 采用这种方式定义类函数时,必须用作用域符 “::” 表明该函数所属的类。 cpp void Clock::setTime(int newH,int newM,int newS) { hour = newH; minute = newM; second = newS; }
  2. 在类内直接进行定义。这种方式一般用在代码比较少的成员函数。被默认为内联函数。

3.4 对象的创建与使用

在 C++ 中,声明了类,只是定义了一种新的数据类型,只有当定义了类的对 象后,才是生成了这种数据类型的特定实体(实例)。对象是类的实际变量, 创建一个对象称为实例化一个对象或创建一个对象实例。

  1. 必须先定义类,然后再定义类的对象。多个对象之间用逗号分隔。
  2. 声明了一个类就是声明了一种新的数据类型,它本身不能接收和 存储具体的值,只有定义了类的对象后,系统才为其对象分配存储空间。
  3. 在声明类的同时定义的类对象是一种全局对象,它的生存期一直 到整个程序运行结束。

3.4.1 对象成员的访问

  1. 通过对象名和成员运算符访问对象的成员
     object_name.member
     object_name.func(argvs)
    

[!Note] 对象只能访问其 public 的成员

  1. 通过指向对象的指针访问对象中的成员
 PtrToObj->member
 PtrToObj->func(argvs)
  1. 通过对象的引用访问对象中的成员
&ReferenceName = ObjectName

3.5 构造函数

构造函数(Constructor)是一种特殊的成员函数,用来完成在声明对象的同时,对对象中的数据成员进行初始化。

3.5.1 构造函数的定义:

ClassName(argvs) ;

ClassName::ClassName(argvs) {
    // function body
}
  1. 构造函数的名称必须与类名相同
  2. 构造函数没有返回值类型,也不能指定为 void 。
  3. 构造函数可以有任意个任意类型的参数。
  4. 如果没有显式的定义构造函数,系统会自动生成一个默认的构造函数。 这个构造函数不含有参数,也不对数据成员进行初始化,只负责为对象分配存 储空间。
  5. 如果显式的为类定义了构造函数,系统将不再为类提供默认构造函数。
  6. 定义对象时,系统会自动调用构造函数。
  7. 构造函数可以重载
  8. 构造函数一般被定义为公有访问权限

3.5.2 默认构造函数

如果类没有定义构造函数,系统会自动生成一个默认的构造函数。这个构 造函数不含有参数,也不会对数据成员进行初始化。默认构造函数的形式如下:

 BuildFuncName(){}

此时要特别注意,数据成员的值是随机的。程序运行时容易出错。

3.5.3 构造函数的重载

一个类可以定义多个构造函数,这些构造函数具有相同的名字,但参数的个数或参数的类型存在差别,这称为构造函数的重载。

Date::Date() {
    year  = 2012;
    month = 10;
    day   = 11;
}
Date::Date(int y, int m, int d) {
    year  = y;
    month = m;
    day   = d;
}

3.5.4 带默认参数的构造函数

  1. 默认参数只能在原型声明中指定,不能在构造函数的定义中指定
  2. 在构造函数原型声明中,所有给默认值的参数都必须在不给默认值的参数的右面。
  3. 在对象定义时,若省略构造函数的某个参数的值,则其右面所有参数的值都必须 省略,而采用默认值。
    1. 构造函数带有默认参数时,在定义对象时要注意避免二义性。例如: Date(int y=2012,int m=1,int d=1); and Date();

3.6 析构函数

析构函数(Destructor) 与构造函数相反,当对象的生命期结束时,就会自动调用析构函数清除它所占用的内存空间。

系统执行析构函数的四种情况:

  1. 在一个函数中定义了一个对象,当这个函数被调用结束时,该对 象应该释放,在对象释放前会自动执行析构函数。
  2. 具有 static 属性的对象在函数调 用结束时该对象并不释放,因此也不调用析构函数。只在 main 函数结束 或调用 exit 函数结束程序时,其生命期将结束,这时才调用析构函数。
  3. 全局对象,在 main 函数结束时,其生命期将结束,这时才调用其 的析构函数。
  4. 用 new 运算符动态地建立了一个对象,当用 delete 运算符释放该 对象时,调用该对象的析构函数。

析构函数的定义格式:

 ~ ClassName();
  1. 析构函数名是由 “~” 加类名组成,区别于构造函数。
  2. 析构函数没有参数、没有返回值,而且不能重载
  3. 一个类有且仅有一个析构函数,且应为 public
  4. 在对象的生命期结束前,由系统自动调用析构函数。
  5. 如果没有定义析构函数,系统会自动生成一个默认的析构函数,这个析构函数不做任何事情。
#include<string>
#include<iostream>
using namespace std;
class Student{
private:
    string name;
    int number;
public:
    Student(string na, int nu);
    ~Student();
    void Output();
};

Student::Student(string na, int nu) {
    name = na;
    number = nu;
}
Student::~Student() {
    cout << "Destruct" << endl;
}

3.7 构造函数和析构函数的调用顺序

一般情况下,调用析构函数的次序正好与调用构造函数的次序相反,也就是最先被调用的构造函数,其对应的析构函数最后被调用,而最后被调用的构造函数,其对应的析构函数最先被调用。

3.8 对象数组与对象指针

3.8.1 对象数组

对象数组的元素是对象,它不仅具有数据成员,而且也具有成员函数 定义对象数组、使用对象数组的方法与基本数据类型相似。 在执行对象数组说明语句时,系统不仅为对象数组分配内存空间,以存放数组中的每个对象,而且还会自动调用匹配的构造函数完成数组内每个对象的初始化工作。

声明对象数组的格式:

 ClassName ArryName[xx];

访问对象数组的格式:

 ArrayName[i].member;

访问对象数组元素的成员函数的格式:

 ArrayName[i].func(argvs);
class Box {
public:
    Box(int h=10, int w=12, int len=15);
    int volume() {
        return (height * width * length);
    }
private:
    int height;
    int width;
    int length;
};

Box::Box(int h, int w, int len):
    height(h), width(w), length(len)
{}

int main() {
    Box a[3] = {
        Box(),
        Box(15, 18, 20);
        Box(16, 20, 26);
    };
    cout << "volume of a[0] is " << a[0].volume() << endl;
    cout << "volume of a[1] is " << a[1].volume() << endl;
    cout << "volume of a[2] is " << a[2].volume() << endl;
}

[!Note] 初始化列表

Box::Box(int h, int w, int len):
    height(h), width(w), length(len)
{}

成员初始化列表会保留默认参数的属性。这意味着在构造函数的成员初始化列表中,可以使用默认参数值来初始化成员变量,即使在创建对象时没有提供对应的参数值。

对象数组可以像普通数组一样在定义的同时进行初始化

int main() {
    A a[3] = {A(100), A(200), A(300)};
    return 0;
}

3.8.2 对象指针

  1. 声明对象指针的格式为:
ClassName *ObjectPtrName;
  1. 用对象指针访问对象数据成员的格式为:
ObjectPtrName->member;
  1. 用对象指针访问对象成员函数的格式为:
ObjectPtrName->funct(argvs);

同一般变量的指针一样,对象指针在使用之前必须先进行初始化。可以让它指 向一个已定义的对象,也可以用 new 运算符动态建立堆对象。

3.8.2.1 通过对象指针间接访问对象成员的格式:
(*PtrName).member; // 访问数据成员
(*PtrName).func(argvs); // 访问成员函数
PtrName->member // 访问数据成员

e.g.

#include<stdafx.h>
#include<iostream>
using namespace std;
class Square {
private:
    double length;
public:
    Square(double len);
    void Output();
};

Square::Square(double len):
    length(len)
{}

void Square::Output() {
    cout << "Square Area:" << length * length << endl;
}

int main() {
    Square s(2.5), *s1;
    s1 = &s;
    s1->Output();
    Square *s2 = new Square(3.5);
    s2->Output();
    delete s2;
    return 0;
}

也可以通过对象指针来访问对象数组,这时对象指针指向对象数组的首地址。

int main() {
    Box a[3] = ...;
    Box *p = a;
    for (int i = 0; i < 3; i++, p++) {
        cout << "volume of a[" << i << "] is " << p->volume() << endl;
    }
}

3.8.3 this 指针

this 指针是一个隐含于每一个成员函数中的特殊指针。它是一个指向正操作该成员函数的对象。当对一个对象调用成员函数时,编译程序先将对象的地址赋给 this 指针,然后调用成员函数。每次成员函数存取数据成员时, C++ 编译器将根据 this 指针所指向的对象来确定应该引用哪一个对象的数据成员。 通常 this 指针在系统中是隐含存在的,也可以把它显式表示出来。

[!Warning] 静态成员函数没有 this 指针。

3.9 向函数传递对象

C++ 语言中,函数的参数和返回值的传递方式有三种:值传递、 指针传递和引用传递。其方法与传递其它类型的数据一样。

3.9.1 使用对象作为函数参数

把作为实参的对象的值复制给形参创建的局部对象,这种传递是单向的,只从实参到形参。因此,函数对形参值做的改变不会影响到实参

3.9.2 使用对象指针作为函数参数

class Square {
private:
    double length;
public:
    Square(double len);
    void Add(Square *s);
};

Square::Square(double len):
    length(len)
{}
void Square::Add(Square *s) {
    s->length = s->length + 1.0;
}

3.9.3 使用对象引用作为函数参数

采用了引用方式进行参数传递,形参对象就相当于是实参对象的“别名”,对形参的操作其实就是对实参的操作。 使用对象引用作为函数参数不但有指针作为参数的优点,而且比指针作为参数更简单、更直接。

class Square {
private:
    double length;
public:
    Square(double len);
    void Add(Square &s);
};

Square::Square(double len):
    length(len)
{}
void Square::Add(Square &s) {
    s.length = s.length + 1.0;
}

[!Warning] 注意规则: 1. 引用被创建的同时必须被初始化(指针则可以在任何时候被初始 化)。 2. 不能有 NULL 引用,引用必须与合法的存储单元关联(指针则可以 是 NULL )。 3. 一旦引用被初始化,就不能改变引用的关系(指针则可以随时改变 所指的对象)。

3.9.4 三种传递方式比较

  1. 值传递是单向的,形参的改变并不引起实参的改变。指针和引用 传递是双向的,可以将改变由形参“传给”实参。
  2. 引用是 C++ 中的概念。 int m; int &n = m; n 相当 m 的别名或 者绰号,对 n 的任何操作就是对 m 的操作。所以 n 既不是 m 的拷贝,也不 是指向 m 的指针,其实 n 就是 m 它自己。实际上“引用”可以做的任何事情 “指针”也都能够做。
  3. 指针能够毫无约束地操作内存中的任何东西。指针虽然功能强大,但是用起来十分危险,所以如果的确只需要借 用一下某个对象的“别名”,那么就用“引用”,而不要用“指针”,以免发生意外。
  4. 使用引用作为函数参数与使用指针作为函数参数相 比较,前者更容易使用、更清晰,而且当参数传递的数据较大时,引用传递参数的效率高和所占存储空间更小。

3.10 对象的赋值和复制

3.10.1 对象赋值的一般形式为:

 ObjectName1 = ObjectName2;

对象名 l 和对象名 2 必须属于同一个类的两个对象。

  1. 对象的赋值只对其中的数据成员赋值,不对成员函数赋值。数据成员是占存储空间的,不同对象的数据成员占有不同的存储空间, 赋值的过程是将一个对象的数据成员在存储空间的状态复制给另一对象 的数据成员的存储空间。而不同对象的成员函数是同一个函数代码段, 不需要、也无法对它们赋值。
  2. 类的数据成员中不能包括动态分配的数据,否则在赋值时可 能出现意想不到的严重后果

3.10.2 拷贝构造函数

对象初始化:

  1. 创建对象时由构造函数初始化;
  2. 用已有的同类的对象通过赋值方式进行初始化。

通过赋值方式进行初始化的过程,实际上是通过类的拷贝构造函数 来完成的。 拷贝构造函数是一种特殊的构造函数,它具有一般构造函数的所有 特性,但其形参是本类对象的引用。

拷贝构造函数定义格式如下:

构造函数名(类名 &)

拷贝构造函数的参数采用引用方式。 特点:

  1. 函数名与类名相同,并且该函数没有返回值。
  2. 该函数只有一个参数,并且是同类对象的引用
  3. 每个类都必须有一个拷贝构造函数。如果类中没有定义,则系统会自动生成一个默认拷贝构造函数

使用拷贝构造函数时注意:

  1. 并不是所有的类声明都需要拷贝构造函数,仅当准备用传值的方式传递类对象时,才要拷贝构造函数。
  2. 拷贝构造函数的名字必须与类名相同,并且没有返回值。
  3. 拷贝构造函数只有一个参数,必须是本类对象的引用。
  4. 每一个类必须至少有一个拷贝构造函数。如果用户在定义类时没有给出拷贝构造函数,系统会自动产生一个缺省的拷贝构造函数。

3.11 对象的组合

所谓类的组合是指一个类内嵌其它类的对象作为本类的成员。两者之间是包含 与被包含的关系。

··