# C++基础9-对象和类

本文是《C++ Primer Plus》的笔记,本文中的案例均自己实践过,如需转发请在转发开头贴上原文地址,谢谢!

面向对象编程(OOP)是一种编程的重要指导理念。它包含抽象,封装,继承,多态,复用等重要特性。

# 面向过程和面向对象

面向过程(Process oriented programming POP)就是分析出解决问题所需步骤,然后用函数把这些步骤实现,使用的时候依次调用。它是以事件为中心的编程思想。

面向对象(Object Oriented Programming OOP)是描述对象所需的数据以及描述用户与数据交互所需的操作,将问题分解成各个对象以及他们应该表现的行为。它是以对象为中心的编程思想。

以一个现实生活中的例子来说明他们的区别,比如顾客去饭馆吃饭。
假如我们用面向过程的思想:

  1. 顾客向店员点餐
    order(customer,makingQueue)
  2. 厨师做饭
    makeMeal(chef,foodMaterial)
  3. 店员上餐
    offerMeal(waiter,customer,meal)
  4. 顾客吃完结账
    checkOut(customer,money)

在面向过程中,我们关注更多是一个过程完成了什么事,然后依次调用这些过程,完成整个事件。下面看面向对象如何处理:

  1. 分析这个事件中,有哪些实体: 顾客,店员,厨师,饭菜

  2. 定义这些实体,及其应有的行为:

    实体 属性 行为
    顾客 姓名,年龄 点餐,吃饭,结账
    店员 要服务的客户 提供菜单,上饭菜
    厨师 要做饭菜的队列 做饭
    饭菜 所属顾客,包含的食材,口感,外观 被顾客食用
  3. 在每个阶段让实体去执行相应的行为

    1. 顾客向店员点餐
      customer.order(waiter)
      waiter.offerMenu(customer)
    2. 厨师做饭
      chef.makeMeal()
    3. 店员上餐
      waiter.offerMeal(customer,meal)
    4. 顾客吃完结账
      customer.eat(meal)
      customer.checkOut(waiter)

# 抽象和类

从上面可以看出用面向对象解决问题首先是抽象这个完整事件的对象,比如上面第一步便抽象出这些实体。这些实体在面向对象编程中便是定义成类,然后再实例化成一个个对象。类在C++中属于一种自定义类型,跟结构体一样,但是和结构体不一样的是,我们除了要定义相关的属性外,还要定义要表现的行为。

如何定义类?一般类规范由两个部分组成:

类声明:以成员变量的方式描述数据部分,以成员函数原型的方式描述公有接口。这部分放到头文件中。 类定义:方法的定义。这部分放到源码文件中。

person.h文件

#pragma once
class person
{
public:
    string name = "defaultPerson";
    person();
    person(const char * );
    ~person();
    void setName(string personName);
    void getName();
};

person.cpp文件

#include "pch.h"
#include "person.h"

person::person()
{
    std::cout << "hello world" << endl;
}

person::person(const char *)
{
    std::cout << "create your name!" << endl;
}

person::~person()
{
    cout << "object is destroyed!" << endl;
}

void person::setName(string personName)
{
    name = personName;
}

void person::getName()
{
    std::cout << name << endl;
}

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
32
33
34
35
36
37
38
39
40
41
42
43
44

类的访问控制有private,public和protected。其中private是类的默认访问控制(结构体的默认访问类型是public)。一般数据成员设计成私有的,而公有成员函数时程序和对象的私有成员之间的桥梁,提供了对象和程序之间的接口。这样做一方面避免数据被直接修改,有安全防护的作用(数据隐藏),另一方面将实现细节从接口设计中分离出来,如果以后找到了更好的,实现数据表示或成员函数细节的方法,可以对这些细节进行修改,而无需修改程序接口,使维护起来更容易。

成员函数和常规函数定义非常相似,但是他们有两个特殊的特征:

  1. 定义成员函数时,使用作用域解析运算符::来标识函数所属的类。
  2. 成员函数可以访问类的私有成员。

在该成员函数所属的类内,其他成员函数不用作用域解析运算符可直接使用方法名调用该成员函数,在类外面则需要使用类名::成员函数。

在类声明中定义的成员函数自动成为内联函数,在类声明之外定义的成员函数,要使其成为内联函数,需加上inline限定符。根据改写规则(rewrite rule),在类声明中定义方法等同于用原型替换方法定义,然后在类声明的后面将定义改写为内联函数。

由类实例化的每个对象都有自己的存储空间,用于存储其内部变量和类成员,但同一个类的所有对象共享同一组类方法,类方法只有一个副本。

# 类的构造函数和析构函数

# 构造函数

C++希望使用类对象跟标准类型一样,标准类型比如结构体:

struct sT{
    int index;
    int age;
}

sT money = {1,2};
1
2
3
4
5
6

但是类跟结构体是不一样的,类的数据成员默认是私有的,程序是不能直接访问数据成员,因此类需要一个初始化的机制,C++提供了一个特殊成员函数——类构造函数,专门用于构造新对象,将值赋给它们的数据成员。

构造函数有一个特点,不会声明返回值,即使没有返回值,也不会被声明为void类型。构造函数的参数尽量不要与成员变量重名,否则会出现参数名和成员变量重名,成员变量还要使用this修饰,导致麻烦。比较常见的做法是在成员变量前加m_或者后面加_。当没有构造函数时,编译器会提供默认的构造函数,只不过这个构造函数什么也不会做。但是当有非默认构造函数而没有默认构造函数时编译器会报错。

如果想创建对象而不显式的初始化,则必须定义一个不接受任何参数的默认构造函数,一种是给构造函数所有参数提供默认值,另一种是定义一个没有参数的构造函数。在创建了默认构造函数后,便可以声明对象变量,而不对它们进行显式初始化:

person p1;
person p1 = person();
person p1 = new person;

有几种易混的形式:
person p1("test");
//调用person的person(const char * );构造函数
person p2();
//不会调用person的无参构造函数。
1
2
3
4
5
6
7
8
9

# 析构函数

在创建对象时会调用构造函数,在对象销毁时会调用析构函数。它完成清理工作,比如一些在构造函数中new来分配的内存,在析构函数会使用delete来释放这些内存。在没有显式声明析构函数时会默认有一个隐式析构函数。

析构函数是类名前加上~,同构造函数一样,析构函数也没有返回值和声明类型。而且析构函数不能有参数,析构函数的原型必须为:

~类名();

什么时候调用析构函数,有编译器决定,通常不应该在代码中显式的调用析构函数,如果创建的是静态存储类对象,则其析构函数将在程序结束时自动被调用。如果创建的是自动存储类对象,则其析构函数将在程序执行完代码块时自动被调用。如果对象时通过new创建的,则它将驻留栈内存或自由存储区,当使用delete来释放内存时,其析构函数将自动被调用。

在声明对象时有不太一样的形式,可能会有一些不同的结果:

int main(int argc, char *argv[])
{
    {
        person p1("p1");
        person p2 = person("p2");
        //C++标准允许编译器使用两种方式来执行上面这句话,一种和上面那行一样,再有一种就是调用构造函数来创建一个临时对象,再将临时对象复制到p2中并丢弃它,如果使用这种方式,在丢弃临时对象的时候调用析构函数。
        p2 = person();
        //这行和上面那行有本质区别,上面那行是初始化,有可能会创建临时对象,第二行是赋值,将一个对象的成员复制给另一个成员,总是会导致赋值前创建一个临时变量。
        cout << "p2 generate a temporary object" << endl;
        person p3 = { "p3" };
        person p4{ "p4" };
        person p5 {};
        person p6 = "p6";
        //在构造函数只有一个参数时,可以将这个参数赋值给对象,来初始化对象,比如上面。
    }
    //加大括号可以在代码块结束时在程序结束前自动调用析构函数
    cout << "outer end!" << endl;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# const成员函数

如果我们声明一个const类对象,是不能调用它的方法,因为该方法无法确保对象不被修改。可以使用特殊的语法来定义要调用的函数:

const person p1;
p1.getName();

函数声明为:
void getName2() const;

函数定义为:
void person::getName2() const{
    cout << "hi,getName2" <<endl;
}
p1.getName2();

1
2
3
4
5
6
7
8
9
10
11
12

# this指针

一个类的成员函数要返回它的自身对象的引用可以使用this指针。

在person.h中添加:
    person getSelf();
    person * getSelf2();
    person & getSelf3();

在person.cpp中添加:
person person::getSelf()
{
    return *this;
}

person * person::getSelf2()
{
    return this;
}

person & person::getSelf3()
{
    return *this;
}

main函数中的操作:
int main(int argc, char *argv[])
{
    person p1;
    person p2 = p1.getSelf();
    //复制一个对象
    person * p3 = p1.getSelf2();
    //返回该对象的引用
    person * p4 = p1.getSelf2();
    //返回该对象的引用
    p1.setName("testChange");
    p2.getName();
    p3->getName();
    p4->getName();
}

程序输出:
hello world!
defaultPerson
testChange
testChange
object is destroyed!
object is destroyed!
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
32
33
34
35
36
37
38
39
40
41
42
43
44

# 对象数组

如果要创建一个类的多个对象,可以创建对象数组:

int main(int argc, char *argv[])
{
    person personArr[3] = {
        person(),person(),person()
    };
    personArr[2].getName();
}
1
2
3
4
5
6
7

# 类作用域

在C中有全局文件作用域和局部代码块的作用域,在C++中有一种新的作用域:类作用域。

在类中定义的数据成员和成员函数的作用域为整个类,在类外面是不可见的。

# static变量

在类中static修饰的变量,这个变量是所有对象共享的,而不是一个对象独有的,看实例:

egg.h
#pragma once
class egg
{
public:
    const int maxNum = 10;
    static int totals;
//带有类内初始化表达式的静态 数据成员 必须具有不可变的常量整型类型,或必须被指定为“内联”
    egg();
    ~egg();
};

egg.cpp
#include "pch.h"
#include "egg.h"

int egg::totals = 0;

egg::egg()
{
    totals++;
}


egg::~egg()
{
}

main函数中
int main(int argc, char *argv[])
{
    egg eggArr[3];
    cout << eggArr[0].maxNum << endl;
    cout << eggArr[0].totals << endl;
}
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
32
33
34
35

# 类域内枚举

普通的枚举是没有作用域限定的,比如下面的color1和color2内如果同时包含red的话,会被提示重复定义。C++11引进了作用域内的枚举,这样便不会发生名称冲突了,但是常规枚举可自动转换为整形,而作用域内地枚举不能隐式地转换为整形。

enum color1 {
    red, black
};

enum color2 {
    white
};
//如果color2里面定义red的话,那么就会重复定义

enum class color3 {
    red,black
};
enum class color4 {
    red, white
};

int main(int argc, char *argv[])
{
//int color5 = color3::black; //不存在隐式转换
    int colorInt = black;
    cout << red << endl;
    cout << colorInt << endl;
    if (red < black) {
        cout << "black is bigger than red" << endl;
    }
    if (color3::red < color3::black) {
        cout << "also bigger" << endl;
    }
    if (0 < black) {
        cout << "black is bigger" << endl;
    }
    //if(0<color3::black) //不存在隐式转换
}
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
32
33