类:定义、成员、构造、析构¶
约 1361 个字 74 行代码 2 张图片 预计阅读时间 5 分钟
类的定义¶
在 C++ 中,用类来定义变量时,不必像 C 语言那样带有 struct
关键字。即,如果有 class Foo
或者 struct Bar
的定义,那么 Foo x;
, class Foo x;
, Bar b;
, struct Bar b;
都是合法的声明语句,C++ 希望用户能够像用内置类型一样地使用自定义类型。
具体的,类的定义如下:
- opt 说明某个元素是可选择的,例如,class-specifier: class-head { \(\text{member-specification}_\text{opt}\) } 说明 class-specifier 中可以没有 member-specification,例如
class Foo {}
或者class A : public B {}
之类的。 - 这里的 class-name 是一个 identifier,例如上面的
Foo
和A
。 - 这里的 class-key 决定了类是否是一个 union,以及默认情况下成员是 public 的还是 private 的。union 一次最多保存一个数据成员的值。也就是说,在 C++ 中,struct, class, union 都是类。但是在本节的后续讨论中,我们暂时只讨论 struct 和 class。
- 这里的 base-clause 定义为
base-clause : base-specifier-list
,是用来处理派生类的。例如 1 中的: public B
。 - 这里的 nested-name-specifier 是
::
或者Foo::
之类的东西:
声明和定义¶
声明将名字引入或重新引入到程序中。定义是声明的一种,指的是那些引入的名字对应的实体足以被使用的声明。
有关重新引入的例子:
上面的例子是合法的。它们只是 i
和 f
的声明而非定义。
而下面的语句都是定义:
类的成员¶
member-specification 说明了类的成员:
其中 member-declaration 是成员的声明,而 access-specifier 是 private
, public
, protected
之一。成员可以包括成员变量、成员函数,也可以(嵌套的)类、枚举等,如本文前面代码中的 Outer::Inner
,还可以包括声明类型的别名(如 typedef
和 using
)等。
C++11 引入了 using
来声明类型别名,它的用途和 typedef
类似,如 typedef struct arraylist_* arraylist;
可以写成 using arraylist = struct arraylist_ *;
。
类型别名的声明也可以是类的成员,其作用域是类的作用域,同样受 access-specifier 的影响。例如:
类的成员函数可以在类内直接给出定义,也可以在类内只声明,在类外给出定义;这不影响成员函数的 access-specifier:
另外,和全局函数一样,类的成员函数也可以只有声明没有定义,只要这个函数没有被使用。
this
指针¶
C++ 早期会被编译成 C 语言,然后再编译成汇编,那么我们考虑 C++ 和 C 语言最显著的区别——类中定义函数,这个函数访问了调用这个函数的对象(calling object),我们只定义了一次函数,但是有很多类,我们怎么在这个函数中,知道这次的 calling object 是什么呢?
在 C++ 中,每个成员函数都会被视为有一个 implicit object parameter,在成员函数的函数体中,this
表达式的值即是 implicit object parameter 即 calling object 的地址。
在成员函数的函数体中,访问任何成员时都会被自动添加 this->
,例如 void Foo::bar(int v) { x += v; }
中的 x += v;
实际是 this->x += v;
。
看一下汇编:
C++ 代码:
汇编代码:
inline
函数¶
众所周知,函数调用是有开销的,比如传递参数和获取返回值。
C++ 的设计哲学决定了,不应当因为封装性而带来性能的额外的性能开销。早在 C with Classes 设计之初,函数调用的问题就已经被内联替换(inline substitution) 解决了。
内联替换即在函数调用的地方将函数展开,不用再传参和传返回值了。
那么,什么样的函数会被内联呢?只有那些函数体写在类的定义中的成员函数才会被内联。而在后来的 C++ 中,inline
关键字被引入;它用在函数声明中,例如 inline int foo(int x) { return add5(x); }
。它向编译器表明一个建议:这里应该优先考虑使用内联替换而非通常的函数调用,当然,编译器通常会忽略这种建议。
但是,如果被内联的函数非常大,则会导致生成的目标代码很大,这会带来内存紧张或者局部性问题;这也可能会对性能产生一定影响。
构造函数¶
构造函数 (constructor) 是一种特殊的成员函数,用于初始化该类的对象。构造函数 constructor 也时常被简写为 ctor 或者 c'tor 等。
构造函数的意义之一是:使程序员能够建立起某种保证,其他成员函数都能依赖这个保证。
在上面的程序中,第 5 行的 Container()
是构造函数。它和其他成员函数的区别是,它不写返回值类型,而且它直接使用类的名字。
第 6 行的 val = nullptr;
就是前面提到的「保证」,即 val
的值要么是 nullptr
,要么是其他成员函数赋的值,而不会是个随机的值。
这样,就可以使用 Container c = Container();
构造一个对象了,其中Container();
会返回一个构造出的无名对象。为了代码更加简洁紧凑,C++ 允许更加简洁的写法:Container c;
。
注意,由于定义一个对象时需要用到构造函数,因此如果要用的构造函数是 private
的,对象就无法被构造。