C++基础-类基础

const成员函数

指向非常量的常量指针无法被绑定至常量

在默认情况下,类的this指针的类型是指向类类型非常量版本的常量指针,这一设定意味着我们无法将this指针绑定至类类型的常量上。也就是说我们无法使用常量对象调用普通的成员对象,为了提高程序的兼容性,可以将this声明为指向常量的常量指针,为了达到之一目的可以在成员函数的参数列表之后加入const关键字,表示this指向常量,称这样的成员函数为常量成员函数。

1
std::string isbn() const { return bookNo; }

构造函数

类通过一个或几个特殊的成员函数控制其对象的初始化过程,这些函数称为构造函数。构造函数初始化类对象的数据成员,只要有类的对象被创建,就会指向构造函数。

类的构造函数不能被声明为const,因为直到构造函数完成初始化过程,对象才能真正获得常量属性,在此之前,我们需要向对象中写入数值。

类通过一个特殊的构造函数控制默认初始化过程,这个函数叫做默认构造函数,编译器创建的构造函数又称为合成的默认构造函数。默认构造函数按照如下规则初始化类的数据成员:

  • 如果存在类内初始值,用它初始化成员。
  • 否则,默认初始化。

有些类不能依赖于合成的默认构造函数

原因如下:

  1. 当我们定义了自己的构造函数时,编译器便不会为我们的类合成默认构造函数,需要自己定义默认构造函数。
  2. 合成的默认构造函数可能会执行错误的操作。如果类内包含有内置类型或者符合类型的成员,则只有当这些成员全部被赋予了类内的初始值时,该类才适合于使用合成的默认构造函数。
  3. 编译器无法为一些类合成默认构造函数,如类中包含的其他类没有默认的构造函数时。

当我们定义了其他构造函数时,也必须定义一个默认构造函数

如果没有在构造函数的初始值列表中显示地初始化成员,则该成员将在构造函数体之前执行默认初始化。有时候,在构造函数中不使用初始化列表,而是在函数体中使用赋值操作。这一做法有时是没有问题的,但对于有些必须进行初始化的类型而言,这一做法便不可取。例如,当成员为const或者引用时,就必须对其进行初始化,而不能采用赋值操作为其赋值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class COnstRef
{
public:
4ConstRef(int ii);
private:
4int i;
4const int ci;
4int &ri;
}

ConstRef::COnstRef(int ii)
{
4i = ii;
4ci = ii;//不能向const赋值
4ri = i;//ri为引用,未被初始化
}

初始化const或者引用类型的数据成员的唯一机会就是通过构造函数初始值,因而该构造函数的正确形式为:

1
ConstRef::ConstRef(int ii): i(ii), ci(ii), ri(i) {}

如果成员是const、引用个、或者属于某种未提供默认构造函数的类类型,我们必须通过构造函数初始值列表为这些成员提供初值。

在构造函数,成员的初始化顺序与它们在类定义中的出现顺序一致。不受初始值列表中的顺序的影响。

默认构造函数的作用

当对象被默认初始化或值初始化时自动执行默认构造函数。

  1. 默认初始化发生的情况:

    1. 在块作用域内不适用任何初始值定义一个非静态变量或者数组。
    2. 一个类本身含有其他类类型的成员,同时该类使用合成的默认构造函数。
    3. 类类型的成员未在构造函数初始值列表中显式地初始化。
  2. 值初始化

    1. 数组初始化过程中提供的初始值数量小于数组的大小。
    2. 不使用初始值定义一个局部静态变量。
    3. 使用形如T()的表达式显式地请求值初始化。

 拷贝、赋值和析构

类还需要控制拷贝、赋值和销毁对象时发生的行为。当我们初始化变量、以值的方式传递或返回一个参数时会发生对象的拷贝操作;使用赋值运算符时会发生对象的赋值操作;当对象不在存在时会发生销毁操作。

类的访问控制与封装

封装的作用就是限制用户对于类的接口的控制,我们使用访问说明符加强类的封装性。

  • public:该说明符之后的成员可以在整个程序内被访问,通常将类的接口定义为public;
  • private:该说明符之后的成员可以被类的成员函数访问,但不能被使用该类的外部代码访问。

class和struct关键字

这两个关键字的唯一区别是默认访问权限不一样,类可以在它的第一个访问说明符之前定义成员,对于这些成员的访问权限取决于所使用的关键字,struct说明这些成员是public的,class说明这些成员是private的

友元

有一些函数虽然属于类的接口,但由于其并非类的成员函数,因而无法访问类的私有成员。

为了解决这一问题,类可以允许其他类或者函数访问它的非公有成员,方法是令其他类或者函数成为它的友元,增加一条以friend关键字开头的函数声明语句即可。

友元在类内的具体为知不限,也不受其所在区域的访问控制级别的限制,但一般,最好在类定义的开始或结束前的位置集中声明友元。

我们可以将一个类声明为另一类的友元,这样第一个类将可以访问第二个类的所有成员。

1
2
3
class Screen{
4friend class Window_mgr;
};

需要特别注意:友元关系不存在传递性。

也可以只将类的某一个成员函数声明为友元,需明确指出该成员函数所属的类:

1
2
3
class Screen{
4friend void Window_mgr::clear(ScreenIndex);
};

要想令某个成员函数作为友元,必须仔细组织程序的结构以满足声明和定义的彼此依赖关系。

  • 首先定义Window_mgr类,其中声明clear函数,但不能定义它。因为在clear使用Screen的成员之前,必须首先定义Screen。
  • 定义Screen,包括对clear的友元声明。
  • 定义clear成员函数。

友元声明的作用仅仅是影响访问权限,不具有函数声明的作用,在对一个友元函数进行声明之前是无法对其进行调用的,这一点要特别注意。即使在类的内部定义了该友元函数,也必须在类的外部提供该函数的声明。

不要求类和非成员函数的声明必须在它们的友元之前。

1
2
3
4
5
6
7
8
9
struce X{
4friend void f() {/*可以直接在类的内部对友元函数进行定义*/}
4X() {f();} //错误,f()尚未声明
4void g();
4void h();
};
void X::g() {return f();}//错误,f()尚未声明
void f();
void X::h() {return f();}//正确,f()已声明

类的可变数据成员

我们希望能够修改类的某个数据成员,即使在一个const成员函数内,可以通过在变量的声明中加入mutable关键字完成。

返回*this的成员函数

当我们将成员函数的返回类型定义为引用时,同时该成员函数返回*this时,那么该成员函数的返回值为调用该成员函数的对象本身,如下所示:

1
2
3
4
5
Screen &Screen::set(char c)
{
4contents[cursor] = c;
4return *this; //将this对象作为左值返回
}

如果我们把一系列这样的操作连接成一条表达式:

1
myScreen.move(4, 0).set("#");

这些操作将在同一个对象上进行。

从const成员函数返回*this时,意味着该成员函数返回的是一个常量对象,那么我们将不能随意地将该成员函数嵌入一组动作的序列中去。

基于const关键字进行区分的重载

需要明确一点,常量对象是无法调用非常量的成员函数的,因而我们只能在一个常量对象上调用const成员函数。虽然,也可以使用非常量对象调用常量成员函数,但非常量成员函数更为匹配。

类的作用域

一个类就是一个作用域,当我们在类的外部定义成员函数时,一旦遇到了类名,定义的剩余部分就是在类的作用域之内,剩余部分包括参数列表和函数体。而位于类名之前的函数的返回值部分位与类的作用域之外。