引入 const 成员函数

默认情况下 this 指针是指向类类型的非常量版本的常量指针。其数据类型是一个 ClassName *const。这是一个顶层const,它一直指向某个固定的 ClassName 对象(注意这个对象并不一定是const)。但是由于数据类型不一样,不能把 this 指针绑定在一个常量对象上(即 const ClassName)。也就是不能在常量对象上调用普通的成员函数

为了能让 this 指针能够绑定到const对象上,所以 this 指针的数据类型应该是 const ClassName *const。但是 this 指针是个隐式的,所以给他挂上这个底层const的工作就得依靠const成员函数。其声明方式就是在形参列表后面加一个const,如

1
string isbn() const {return this->bookNo;}

这个const表示this是一个指向正儿八经常量的指针

定义类相关的 非成员函数

某些函数从概念上来说属于类的接口的组成部分,但是他们实际上并不属于类本身,比如一些成员函数的辅助函数。不过一般来说,如果非成员函数是类接口的组成部分,则这些函数的声明应该和类在同一个头文件内。

构造函数

构造函数不能被声明成 const 类型。因为即使创建一个 const 对象,直到对象被构造函数初始化之后才能取得其 const 属性。所以构造函数可以在 const 对象构造的过程中向其写入值

注:如果包含有内置类型或复合类型的成员,则只有当这些成员全被赋予了类内初值时,这个类才适合于使用合成的默认构造函数。

定义构造函数有如下几种方式(先用struct关键字)

1
2
3
4
5
6
7
8
9
10
struct Sales_data
{
Sales_data() = default; //C++11新特性,要求编译器必须支持类内初始值
Sales_data(const std::string &s) : bookNo(s) {} //如果编译器不支持类内初值,可采用初始值列表,下同。
Sales_data(const std::string &s, unsigned n, double p) : bookNo(s), units_sold(n), revenue(p * n) {}

std::string bookNo;
unsigned units_sold = 0; //基本数据类型类内初值
double revenue = 0.0; //基本数据类型类内初值
};

= default 的含义:在C++11中,如果需要默认行为,可以在参数列表后面写上 = default 来要求编译器生成构造函数。如果 = default 出现在类内,则默认构造函数是内联的;如果出现在类外,则默认构造函数不是内联的。这个默认构造函数之所以有效,是因为我们为内置类型的数据成员提供了初值

构造函数初始值列表:即构造函数后的冒号以及冒号到花括号之间的代码。里面放的是类内成员名字的列表,后面跟的是成员初始值(可以使用小括号也可以使用花括号)。不同成员的初始化通过逗号分隔。

构造函数不应该轻易覆盖掉类内初始值。

使用构造函数初始化列表和在构造函数中为成员赋值这两种操作在某些时候有很大差别,如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class ConstRef
{
public:
ConstRef(int ii);
private:
int i;
const int ci;
int &ri;
};
ConstRef::ConstRef(int ii) //先初始化再赋值
{
i = ii; //可以
ci = ii; //不行,不能给const赋值
ri = i; //不行,ri还未被初始化
}
//优先用下面这个版本,效率要高一点。
ConstRef::ConstRef(int ii) : i(ii), ci(ii), ri(i) { } //可以,显式初始化引用和const成员

访问控制与封装

当我们希望定义的类的所有成员是 public 时,使用 struct ;如果希望成员是private的,使用class。这也是structclass的唯一区别

友元

类可以通过友元函数或友元类来允许别的函数和类访问自己的非公有成员。友元不是类的成员,也不受访问控制的约束。如果类想把一个函数作为他的友元,只需要增加一条以friend开头的函数声明语句。

1
2
3
4
5
6
7
8
9
10
11
class Sales_data
{
//友元声明
friend Sales_data add(const Sales_data&, const Sales_data&);
public:
Sales_data() = default;
private:
unsigned unis_sold = 0;
};
//非成员函数声明(由于是非成员,所以不需要访问控制符)
Sales_data add(const Sales_data&, const Sales_data&);

友元的声明仅仅制定了访问的权限,而非一个通常意义上的函数声明。如果希望该函数被调用,就必须在友元声明之外再专门对函数进行一次声明。通常把友元函数的声明放到和类本身相同的头文件中。

类之间的友元关系

如果一个类指定了友元类,则友元类的成员函数可以访问此类包括非公有成员在内的所有成员。如

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

这样声明之后Window_mgr中所有的成员都可以访问screen中的成员了。需要注意的是,友元关系不存在传递性

令成员函数作为友元

也可以让某个类中的某几个成员函数作为友元

1
2
3
4
class Screen
{
friend void Window_mgr::clear(ScreenIndex);//将成员函数声明成友元时,必须明确指出其所属的类
};

函数重载和友元

重载的函数需要对这组函数中的每一个部分分别声明。记住:重载函数仍然是几个不同的函数!

类成员

类可以自定义某种类型在类中的别名,也存在访问限制,可以是publicprivate的一种。用来定义类型的成员必须先定义后使用

1
2
3
4
5
6
7
class Screen
{
public:
//把pos定义成public可以隐藏Screen实现的细节
typedef std::string::size_type pos;
using pos = std::string::size_type; //两个等价的
};

可变数据成员

在变量声明中加入一个mutable关键字来获得一个可变数据成员,它在任何情况下都不会是const类型,就算是const对象也可以改。

类内初值

当我们提供一个类内初值时,必须以符号 = 或者花括号表示

返回 *this 的成员函数

1
2
3
4
5
6
7
8
9
10
11
12
class Screen
{
public:
Screen &set(char);
Screen &set(pos, pos, char);
};
//set的返回值是调用set的对象的引用。返回引用的函数都是左值的,意味着他们返回的是对象本身而不是其副本。
inline Screen &Screen::set(char c)
{
contents[cursor] = c;
return *this;
}

类的声明

不完全类型(前向声明得来的类型)只能在非常有限的情况下使用:可以定义指向这种类型的指针或引用,也可以声明(但不能定义)以不完全类型作为参数或者返回类型的函数。一旦一个类的名字出现后,他就被认为是声明过了(但是没有定义)。所以类允许包含指向它自身类型的引用或指针。如

1
2
3
4
5
6
7
class Screen;
class Link_screen
{
Screen window; //只有这一句是解释了上面的话的
Link_screen *next;
Link_screen *prev;
};

友元声明和作用域

即使在类的内部定义了友元函数,也必须在类的外部提供相应的声明从而使得函数可见。例如

1
2
3
4
5
6
7
8
9
struct X
{
friend void f() {/*此处是友元函数实现*/} //只在当前作用域可见
void g();
void h();
}
void X::g() {return f();} //不行,此时f还没被声明
void f(); //声明上面在X中定义了的函数
void X::h() {return f();} //可以,此时作用域中有f()的声明了

类的作用域

以前从未见过的用法:

1
2
3
4
5
6
7
8
9
10
11
12
class Window_mgr
{
public:
ScreenIndex addScreen(const Scren&);
using ScreenIndex = std::vector<Screen>::size_type;
};
//此时addScreen的定义如下,注意返回值的写法
Window_mgr::ScreenIndex Window_mgr::addScreen(const Screen &s)
{
screens.push_back(s);
return screens.size() - 1;
}

名字查找与类的作用域

成员函数体直到整个类可见后才会被处理。所以成员函数可以使用类中定义的任何名字。

成员初始化的顺序

成员初始化的顺序与它们在类的定义中出现顺序一致。而构造函数的初始值列表不能限定成员初始化的具体顺序。如

1
2
3
4
5
6
7
8
class X
{
int i;
int j;
public:
//这个的顺序其实是先用未初始化的变量j来初始化i,再用val初始化j。原因参考上面定义的顺序:i先,j后。
X(int val) : j(val), i(j) { }
};

注:尽可能使用构造函数的参数作为成员的初始值,这样就不用考虑初始化顺序了。

委托构造函数

C++11新增:一个委托构造函数使用他所属类的其他构造函数执行他自己的初始化过程。在委托构造函数内,成员初始值列表只有一个唯一的入口就是类名本身。如

1
2
3
4
5
6
7
8
9
class Sales_data
{
Sales_data(std::string s, unsigned cnt, double price) : bookNo(s), units_sold(cnt), revenue(cnt * price) { }
//下面全都是委托构造函数,他们的参数列表必须与类中另外一个构造函数匹配
Sales_data() : Sales_data("", 0, 0) {}
Sales_data(std::string s) : Sales_data(s, 0, 0) {}
//下面这个函数首先执行默认构造函数,然后默认构造函数又经过委托给了最上面的构造函数,执行完之后在执行函数体内的read方法。
Sales_data(std::istream &is) : Sales_data() { read(is, *this); }
}

隐式的类类型转换

如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制,该构造函数有时又称转换构造函数。能通过一个参数调用的构造函数定义了一条从构造函数的参数类型向类类型隐式转换的规则。在需要使用的时候,可以通过实参的类型来代替,如

1
2
3
string null_book = "9999-9999";
//combine函数本来是要接受一个Sales_data类,但是此时传入的是一个string对象。在这时就创建了一个临时的Sales_data类,其units_sold和revenue都是0,bookNo是null_book.
item.combine(null_book);

因为Sales_data类的combine函数是一个const引用,所以是可以传递临时量的。

但是这种转换只能是一步一步的,例如下面的代码就是错误的

item.combine("9999-9999");

因为他进行了两步隐式转换:首先要从”9999-9999”转换到string类型,其次要把这个string类型转成Sales_data类。如果要想这样用,要按照以下方式

1
2
3
item.combine(string("9999-9999"));

item.combine(Sales_data("9999-9999"));

抑制构造函数的隐式转换

使用关键字explicit来阻止这种转换,如

1
2
3
4
5
6
7
8
9
10
class Sales_data
{
public:
Sales_data() = default;
Sales_data(const std::string &s, unsigned n, double p) : bookNo(s), units_sold(n), revenue(p * n) { }
explicit Sales_data(const std::string &s) : bookNo(s) { }
explicit Sales_data(std::istream&);
};
//此时就要报错
item.combine(string("9999-9999"));

explicit关键字只对 一个实参 的构造函数有效!而且只能在类内声明构造函数时使用explicit关键字,在类外部定义的时候不应该重复。如

1
2
3
4
explicit Sales_data::Sales_data(istream &s) //这样是错误的
{
read(is, *this);
}

当使用explicit关键字声明构造函数时,他将只能以直接初始化的形式使用。而且编译器不会在自动转换过程中使用该构造函数。

可以使用强制类型转换来使用explicit构造函数,如

1
2
item.combine(Sales_data(null_book)); //这是显式调用explicit构造函数
item.combine(static_cast<Sales_data>(cin)); //static_cast可以使用explicit的构造函数

聚合类

使得用户可以直接访问其成员,并且它具有特殊的初始化语法形式。满足如下特点的就是聚合类:所有成员都是public没有定义任何构造函数没有类内初值没有基类,也没有virtual函数,如

1
2
3
4
5
6
7
struct Data
{
int ival;
string s;
};
//可以提供一个花括号括起来的成员初值列表,并用它初始化聚合类的数据成员
Data val1 = {0, "Anna"}; //初始值的顺序必须与声明的顺序一致!

字面值常量类

数据成员都是字面值类型的聚合类是字面值常量类,或者满足下面要求的非聚合类也是字面值常量类:数据成员都是字面值类型、类必须至少含有一个constexpr构造函数、如果数据成员含有类内初值,则类内初值必须是常量表达式;如果成员属于某种类类型,则初始值必须使用类成员自己的constexpr含构造函数、累必须使用析构函数的默认定义。

constexpr构造函数

构造函数不能是const,但是字面值常量类的构造函数是constexpr。constexpr构造函数可以声明成 = default,否则函数体内只能为空。使用前置关键字constexpr来声明constexpr构造函数

1
2
3
4
5
6
class Debug
{
public:
constexpr Debug(bool b = true) : hw(b), io(b), other(b) { }
constexpr Debug(bool h, bool i, bool o) : hw(h), io(i), other(o) { }
};

constexpr构造函数用于生成constexpr对象以及constexpr函数的参数或返回类型,如

constexpr Debug io_sub(false, true, false);

类的静态成员

静态成员和类本身直接相关,而不是和类的各个对象保持关联。静态数据成员这个类的被所有对象共享。静态成员不包含this指针,也不能被声明称const的。如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Account
{
public:
void calc() { amount += amount * interestRate;} //成员函数不通过作用域运算符也能调用静态成员
static double rate() { return interestRate; }
static void rate(double);
private:
std::string owner;
static double interestRate;
static double initRate();
};
//可以使用作用域运算符访问类的静态成员
double r;
r = Account::rate();
//由于静态成员被所有类所共享,虽然静态成员属于类的某个对象,所以可以通过类的对象、引用或指针来访问静态成员
Account ac1;
Account *ac2 = &ac1;
r = ac1.rate(); //点号调用
r = ac2->rate(); //指针的调用

定义静态成员

可以在类内或者类外定义静态成员函数,如果在类外定义静态成员时,不能重复static关键字,它只出现在类内的声明语句,如

1
2
3
4
5
void Account::rate(double newRate) //指向类外部的静态成员时,必须指明成员所属的类名。static只出现在类内声明
{
interestRate = newRate;
};
double Account::interestRate = initRate(); //定义静态数据成员的方式

注:必须在类外定义和初始化每个静态成员

静态成员的类内初始化

通常情况下静态成员不能在类内初始化,但是可以为他提供const整数类型的类内初始值,不过要求静态成员必须是字面常量类型的constexpr。如

1
2
3
4
5
6
7
class Account
{
private:
static constexpr int period = 30;
};
//即使一个常量静态数据成员在类的内部被初始化了,通常情况下也应在类外定义一下该成员
constexpr int Account::period; //这时候的定义没有初值

静态数据成员由于独立于任何对象,所以他可以是不完全类型。如

1
2
3
4
5
6
7
class Bar
{
private:
static Bar mem1; //可以,静态成员可以是不完全类型,其成员类型可以是它所属的类型
Bar *mem2; //可以,指针和引用可以是不完全类型
Bar mem3; //不行,数据成员必须是完全类型!!!因为类要先被定义才能知道自己要给对象分配多少空间。
}

静态成员可以作为默认实参而普通成员不行。因为普通成员属于对象的一部分。如

1
2
3
4
5
6
7
class Screen
{
public:
Screen& clear(char = background);
private:
static const char background;
}//非静态数据成员就不能当默认实参了,因为它属于对象的一部分,必须要在对象创建过后才能用。