Skip to content

8.2 类和类成员 (Classes and class member)

当 C++ 提供了一系列基本类型(例如:char,int,long,float,double等等)常常能够解决一些简单问题,很难用它仅仅用这些类型来解决复杂的问题。一个 C++ 的非常有用的特性就是定义你自己的数据类型,能够符合需要解决的问题。你早就学过了如何枚举类型和结构,用同样的方法,你也可以创建自己的类型。

这有一个结构持有日期的例子

struct DateStruct
{
    int year{};
    int month{};
    int day{};
};

枚举类型和纯数据的结构(仅仅包含变量的结构)仅代表着传统的非 OOP 编程的世界,因为他们只能用来从持有数据,在 C++11 中,我们可以像下方这样创建并且初始化对象。

DateStruct today { 2020, 10, 14 }; // use uniform initialization

现在,如果你想打印这个日期到屏幕(还有很多多我们想做的类似的事情),写一个函数来做这件事是可以的:

#include <iostream>

struct DateStruct
{
    int year{};
    int month{};
    int day{};

};
void print(const DateStruct &date)
{

    std::cout << date.year << '/' << date.month << '/' << date.day;

}
int main()
{
    DateStruct today { 2020, 10, 14 }; // use uniform initialization
    today.day = 16; // use member selection operator to select a member of the struct
    print(today);
    return 0;

}

程序打印:

2020/10/16

在面向对象编程的世界中,我们通茶才能够想要我们的类型不仅仅持有数据,并且也提供一些函数来处理数据。在 C++ 中,这通常通过 class 关键词来完成。类关键词定义了一些用户定义类型叫做 class。

在 C++ 中,类型和结构是基本相同的,事实上,下列结构和类是等效的:

struct DateStruct
{
    int year{};
    int month{};
    int day{};
};

class DateClass
{
public:
    int m_year{};
    int m_month{};
    int m_day{};
};

注意唯一不同的就是类中的 public 关键词。我们将会在下节课讨论这个关键词。

就像一个结构的定义那样,一个类的定义不分配任何内存,他仅仅定义了一个类模样而已。

Warning

就像对待结构那样,在 C++ 中最容易犯的错误之一就是忘了写类型定义结尾的分号。这会导致一个变异错误,在下一行。现代的编译器,例如 Visual Studio 2010 会给你一个指示告诉你你有可能已经忘了一个分号,但是更老的或者更功能更简单的编译器不会那么做,这会让你很难发现这个错误

类(和结构)定义就像一个 blueprint —— 他们描述了对象看起来将会像什么,但是他们不会实际创建一个对象。想要创建一个类的对象,一个类的变量一定要被定义出来才行:

DateClass today { 2020, 10, 14 }; // declare a variable of class DateClass

成员函数

除了持有数据,类(和结构)也可以包含函数!在类中定义的函数被叫做成员函数(有时候叫’方法‘)。成员函数可以被定义在类内或者类外,我们将会定义他们在类内(为了简单),之后展示如何在类外定义。

这是我们的带有一个成员函数来打印日期的类:

class DateClass
{
public:

    int m_year{};
    int m_month{};
    int m_day{};

    void print() // defines a member function named print()
    {
        std::cout << m_year << '/' << m_month << '/' << m_day;
    }

};

Just like members of a struct, members (variables and functions) of a class are accessed using the member selector operator (.):

就像结构的成员,类的成员也可以使用成员选择操作符(.)来访问成员。

# include <iostream>

class DateClass
{
public:

    int m_year{};
    int m_month{};
    int m_day{};

    void print()
    {
        std::cout << m_year << '/' << m_month << '/' << m_day;
    }

};

int main()
{

    DateClass today { 2020, 10, 14 };

    today.m_day = 16; // use member selection operator to select a member variable of the class
    today.print(); // use member selection operator to call a member function of the class

    return 0;
}

打印:

2020/10/16

注意这个程序要比我们上方使用 struct 实现的简单多了。

然而,有一些不同点。在上方 DataStruct 版本的例子中的 print() 我们传入了结构本身作为第一个参数进入 print() 函数。否则,print() 不会知道哪我们想打印哪个对象。我们紧接着不得不使用这个参数,在函数内部。

成员函数工作起来有轻微的不同:所有的成员函数调用必须和这个类的对象关联。当我们调用 today.print() 的时候,我们告诉编译器我们要调用和 today 这个对象关联的 print 成员函数。

现在让我们再看一遍成员函数的定义

    void print() // defines a member function named print()
    {
        std::cout << m_year << '/' << m_month << '/' << m_day;
    }

m_year, m_month, m_day 实际上分别指向哪里?他们指向关联的对象(由调用者决定)。

因此当我们调用 today.print() 的时候,编译器解释 m_day 为 today.m_today,m_month 和 m_year也分别解释为 today.m_monthtoday.m_year,如果我们调用 tomorrow.print()m_day 就会被 tomorrow.m_day 指代。

以这种方式,本质上隐式的传送了一个类的实例到成员函数。因为这样,他才能被隐式的调用。

我们将会在后面的章节中,讨论更多的这个隐式对象如何传入并且起作用。

关键单是,当我们调用非成员函数,我们不得不传入一个对象到函数。而当我们调用成员函数的时候,我们可以假设我们总是有一个隐式的类对象可以被使用。

给成员变量使用 ”m_“ 前缀可以帮助将成员变量、成员函数和在成员函数中使用的外部变量区分开。这是非常有用的,还有很多原因。

  • 第一,当我们看到操作一个带 m_ 前缀的变量时,我们就知道我们正在改变类实例的状态。
  • 第二,不像函数成员和局部变量,可以被定义在函数中,成员变量被定义在类声明中。因此,如果我们想要知道一个 m_ 前缀的变量如何被声明,我们知道应该它被定义在类中定义而不是函数中。

为了方便,类名应该以一个大写的字母开始。

规则:类的名字以大写字母开始

这是另一个类的例子:

# include <iostream>
# include <string>

class Employee
{
public:

    std::string m_name{};
    int m_id{};
    double m_wage{};

    // Print employee information to the screen
    void print()
    {
        std::cout << "Name: " << m_name <<
                "  Id: " << m_id << 
                "  Wage: $" << m_wage << '\n'; 
    }

};

int main()
{

    // Declare two employees
    Employee alex { "Alex", 1, 25.00 };
    Employee joe { "Joe", 2, 22.25 };

    // Print out the employee information
    alex.print();
    joe.print();
    return 0;

}

输出了结果:

Name: Alex  Id: 1  Wage: $25
Name: Joe  Id: 2  Wage: $22.25

在普通的非成员函数下,一个函数不可能调用在 ”下方“ 定义的函数(没有前置声明)

void x()
{
// You can't call y() from here unless the compiler has already seen a forward declaration for y()
}

void y()
{
}

在成员函数中,这个限制没有被应用:

class foo
{
public:

     void x() { y(); } // okay to call y() here, even though y() isn't defined until later in this class
     void y() { };

};

成员类型

除了成员变量和成员函数,类可以又成员类型或者嵌套类型(包括类型别名)。在下面的例子中,我们创建了一个计算器,我们可以快速的改变数字的类型如果需要。(we can swiftly change the type of number it’s using if we ever need to.)

# include <iostream>
# include <vector>

class Calculator
{
public:
  using number_t = int; // this is a nested type alias

  std::vector<number_t> m_resultHistory{};

  number_t add(number_t a, number_t b)
  {

    auto result{ a + b };

    m_resultHistory.push_back(result);

    return result;

  }
};

int main()
{
  Calculator calculator{};

  std::cout << calculator.add(3, 4) << '\n'; // 7
  std::cout << calculator.add(99, 24) << '\n'; // 123

  for (Calculator::number_t result : calculator.m_resultHistory)
  {
    std::cout << result << '\n';
  }
  return 0;
}

输出

7
123
7
123

在这样的上下文中,类名高效的给内嵌类型扮演了一个命名空间的角色。在类中,我们只需要参照 number_t。在类外面,我们可以阿访问类型通过 Calculator::number_t.

当我们决定一个整型不再

TODO翻译中

When we decide that an int no longer fulfills our needs and we want to use a double, we only need to update the type alias, rather than having to replace every occurrence of int with double.

Type alias members make code easier to maintain and can reduce typing. Template classes, which we’ll cover later, often make use of type alias members. You’ve already seen this as std::vector::size_type, where size_type is an alias for an unsigned integer.

Nested types cannot be forward declared. Generally, nested types should only be used when the nested type is used exclusively within that class. Note that since classes are types, it’s possible to nest classes inside other classes -- this is uncommon and is typically only done by advanced programmers.

A note about structs in C++

In C, structs can only hold data, and do not have associated member functions. In C++, after designing classes (using the class keyword), Bjarne Stroustrup spent some amount of time considering whether structs (which were inherited from C) should be granted the ability to have member functions. Upon consideration, he determined that they should, in part to have a unified ruleset for both. So although we wrote the above programs using the class keyword, we could have used the struct keyword instead.

Many developers (including myself) feel this was the incorrect decision to be made, as it can lead to dangerous assumptions. For example, it’s fair to assume a class will clean up after itself (e.g. a class that allocates memory will deallocate it before being destroyed), but it’s not safe to assume a struct will. Consequently, we recommend using the struct keyword for data-only structures, and the class keyword for defining objects that require both data and functions to be bundled together.

Rule

Use the struct keyword for data-only structures. Use the class keyword for objects that have both data and functions.

You have already been using classes without knowing it

It turns out that the C++ standard library is full of classes that have been created for your benefit. std::string, std::vector, and std::array are all class types! So when you create an object of any of these types, you’re instantiating a class object. And when you call invoke a function using these objects, you’re calling a member function.

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

include

include

include

include

int main() {

std::string s { "Hello, world!" }; // instantiate a string class object
std::array<int, 3> a { 1, 2, 3 }; // instantiate an array class object
std::vector<double> v { 1.1, 2.2, 3.3 }; // instantiate a vector class object



std::cout << "length: " << s.length() << '\n'; // call a member function



return 0;

}

Conclusion

The class keyword lets us create a custom type in C++ that can contain both member variables and member functions. Classes form the basis for Object-oriented programming, and we’ll spend the rest of this chapter and many of the future chapters exploring all they have to offer!