@taqikema
2018-01-04T12:37:40.000000Z
字数 4804
阅读 1098
C++Primer
学习记录
类
定义在类的内部的函数是隐式的 inline函数。
默认情况下,this的类型是指向类类型非常量版本的常量指针(顶层 const),因此不能在常量对象上调用一个普通的成员函数(即将普通指针指向常量对象)。而如果想要声明常量成员函数,就在参数列表后面加上 const,表示 this指针是一个指向常量的指针。常量对象、引用即指针都只能调用常成员函数。
一些函数在概念上属于类但是不定义在类中,则该函数的声明应与类在同一个头文件内。这样,用户使用接口的任何部分都只需要引入一个文件。
在自定义或重载与输出有关的函数时,应尽量减少对格式的控制,这样可以增强该函数的适用性,由用户自行决定是否换行或进行其它格式控制。
如果一个类没有声明任何构造函数时,编译器会自动生成默认构造函数。然而某些类不能依赖于合成的默认构造函数。
class A{
int i; // 错误,默认初始化时 i的值是未定义的
};
class B{
int i = 0; // 正确
};
=default
来要求编译器生成默认构造函数。在类中,除了定义数据和函数成员之外,还可以自定义某种类型在类中的别名,也存在访问权限。用来定义类型的成员必须先定义后使用,这一点与普通成员有所不同。
class Screen {
public:
// 在类中定义一个类型
using pos = std::string::size_type;
};
定义在类内部的成员函数自动是 inline的,但是也可以在类外部定义时说明 inline,以此来显式指定 inline函数。但是在类外部进行函数定义,既可以在头文件中,也可以在源文件中。考虑到 inline函数可以多次定义但每个定义必须相同的特点,在类外显示指定的 inline函数应该与相应的类定义在同一个头文件中。
可变数据成员。有时会遇到需要修改类的某个数据成员,即使该对象是 const对象或是在 const成员函数内。可以在该变量的声明中加入 mutable关键字,来实现这一目的。
类内初始值必须以符号=
或者{}
表示,不能使用()
。
通过区分成员函数是否是 const的,可以对其重载。常量对象只能调用 const函数,而非常量对象会优先调用 普通函数。
不完全类型,一个类在声明之后定义之前的状态。不完全类型只能在非常有限的场景下使用:定义指向这种类型的指针或引用,声明(但不能定义)以不完全类型作为参数或返回值的函数。
因为只有完成类的定义,编译器才能知道存储该类型的对象需要多少空间,所以,一个类型的对象只有当类完全定义过了,才能被声明成这种类型。也就表明了一个类的成员类型不能是该类自己,但可以是自身类型的指针或引用。
友元类的声明和程序设计较为简单。比如,B是 A的友元类。
// A.h
friend class B;
// B.h
\#include "A.h"
但是令类 B的成员函数是 A的友元,程序的设计就没那么简单了。既要满足声明和定义的彼此依赖关系,又要时刻注意防止头文件循环包含。
// 循环包含的例子
// A.h
#include "B.h"
// B.h
#include "A.h"
// B的成员函数是 A的友元的设计范例
// A.h
#include "B.h"
// B.h
class A;
// B.cpp
#include "A.h"
上面的代码中,类 A需要使用类 B的公有接口或数据,所以要在 .h文件中包含 B.h文件。而为了定义是 A的友元的类 B的成员函数,也需要 A的完整定义。所以在 B.cpp文件中需要包含 A.h文件。
类外定义的函数,参数列表和函数体是在类的作用域之内的,而返回类型中使用的名字是位于类的作用域之外的。所以返回类型必须明确指定它是哪个类的成员。
普通作用域的名字查找过程:
注意,上述查找过程只适用于成员函数定义时出现的名字,而函数声明时,返回类型或参数列表中使用的名字(通常是类型别名),都必须在使用前确保可见。如果成员的声明中使用了类中尚未出现的名字,则编译器将会在定义该类的作用域中继续查找。
typedef double Money;
string bal;
class Account {
public:
// 下面的 Money是外层作用域的,而 bal是类内作用域的
Money balance() { return bal; }
private:
Money bal;
在类中,如果成员使用了外层作用域中的某个名字,而该名字代表一种类型,则类不能在之后重新定义该名字。前提使用过该名字,所以如果在类的开始处,重新定义了该名字,则类中使用的将是类内作用域中定义的版本。
typedef double Money;
class Account {
public:
Money balance() {return bal;} // 使用外层作用域中的 Money
private:
typedef double Money; // 错误,不能重新定义 Money
Money bal;
如果没有在构造函数初始值列表中显式地初始化成员,则该成员在构造函数体执行之前执行默认初始化。随着构造函数体一开始执行,初始化就完成了。因此,如果成员是 const、引用,或者属于某种未提供默认构造函数的类类型,就必须通过构造函数初始值列表来为这些成员提供初始值。
构造函数初始值列表只说明用于初始化成员的值,而不限定成员的初始化顺序。成员的初始化顺序与它们在类定义中的出现顺序一致。下面的代码中看似会先初始化 j,再初始化 i。实则不然。因此,最好令构造函数初始值的顺序与成员函数的声明顺序一致,且避免用某些成员的值初始化其他成员。
class X {
int i;
int j;
public:
// 未定义的:i在 j之前被初始化
X(int val) : j(val), i(j) {}
};
如果一个构造函数为所有参数都提供了默认实参,则它实际上也定义了默认构造函数。
委托构造函数,使用它所属类的其他构造函数执行它自己的初始化过程,或者说把自己的一些(或全部)职责委托给了其他构造函数。形式如下,注意成员初始值列表中只能有一个唯一入口,就是类名本身。
class X {
public:
X(int j) : i(j) {}
X() : X(0) {} // 委托构造函数
private:
int i;
};
构造函数存在委托关系时,程序的执行顺序:受委托构造函数的初始值列表和函数体被依次执行,先执行完受委托构造函数的函数体后,控制权才会交还给委托构造函数的函数体。
X obj(); // 定义了一个函数而非对象
X obj; // 定义了一个对象
关键字 explicit,可以抑制构造函数定义的隐式转换。不过,只对一个实参的构造函数有效,并且只能出现在类内声明处。对于 explicit构造函数,只能使用直接初始化,而不能使用拷贝初始化。
class X {
public:
explicit X(int j) : i(j) {}
X() : X(0) {} // 委托构造函数
private:
int i;
};
X item(10); // 直接初始化
X item2 = 10; // 错误
聚合类。需要满足以下条件:
可以提供一个花括号括起来的成员初始化列表来初始化聚合类的数据成员。初始值的顺序必须与声明的顺序一致。
struct Data {
int ival;
string s;
};
Data val_1 = { 0, "Anna" };
// 错误,不能使用 const char*初始化 int
Data val_2 = { "Anna", 0 };
静态数据成员可以是不完全类型。即,静态数据成员的类型可以就是它所属的类类型,而非静态数据成员只能是所属类的引用或指针。
class Bar {
private:
static Bar mem1; // 正确,静态成员可以是不完全类型
Bar *mem2; // 正确,指针成员可以是不完全类型
Bar mem3; // 错误,数据成员必须完全类型
};
静态成员可以作为默认实参,而非静态成员则不可以。
class Screen {
public:
// bkground表示一个在类中稍后定义的静态成员
Screen& clear(cahr = bkground);
private:
static const char bkground;
};