C++正则表达式

......


一个库接口设计实例

一个能够检查文件系统目录的内容的C库例程集

  • 对不直接支持数据抽象的语言通用的约定,使用数据抽象自动进行管理
    • 在类中隐藏这些约定,使用户免于处理
    • 使类更简单,增强类的健壮性
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <dirent.h>
int main() {
DIR *dp = opendir(".");
struct dirent *d;
while (d = readdir(dp))
printf("%s\n", d->d_name);
closedir(dp);
return 0;
}
  • 指向DIR对象的指针被当作神奇的cookies(magic cookies)

  • 调用opendir就会获得一个DIR指针

  • 对readdir的调用返回一个指向struct dirent的指针,表示刚才读取的目录条目

    • struct dirent的成员以null结尾,包含了这个目录条目名字的字符数组(d_name)
  • 工作方式:调用opendir获得表示当前目录的神奇cookies;反复调用readdir从这个目录从这个目录中取出并打印目录条目;最后调用closedir清理内存

  • 没有用到的两个库函数:

    • telldir函数获得表示目录的DIR指针,并返回一个表示目录当前位置的long
    • seekdir函数获得DIR指针和由telldir返回的值,并将指针移动至指定位置

复杂问题

  • 如果目录不存在:通常返回一个空指针

  • 如果传给readdir的参数是一个空指针:

    • 内核转储或者其他灾难性后果
    • 可以实施某种检查并进行相应处理
  • 如果传给readdir的参数既不是一个空指针也不是一个由opendir函数返回的值:

    • 检测这种错误需要构建存放有效DIR对象的表,每次调用readdir时都对该表进行搜索
    • 复杂,开销大,C库例程通常不这么做
  • 对readdir的调用返回指向由库分配的内存块指针,何时释放这块内存:

1
2
3
d1 = readdir(dp1);
d2 = readdir(dp2);
printf("%s\n", d1->d_name);
  • 怎样知道调用readdir(dp2)后指针d1是否指向一个有效的位置

优化接口

  • 重新设计C++接口:用对象取代神奇cookies,取消对指针的使用
1
2
3
4
5
6
class Dir {
public:
Dir(const char*);
~Dir();
// 关于read、seek和tell的声明
};
  • seek和tell成员函数和结果类型
    • C++版本使用一个小型类表示(目录内的)偏移量
1
2
3
4
5
6
7
8
9
10
11
class Dir_offset {
friend class Dir;
private:
long l;
Dir_offset(long n) {
l = n;
}
operator long() {
return l;
}
};
  • read函数,通过使用C++为read提供一个表示可以放入其结果中的对象的参数,并返回一个表示读取是否成功的布尔值
1
2
3
4
5
6
7
8
9
10
#include <dirent.h>
class Dir {
public:
Dir(const char*);
~Dir();
int read(dirent&);
void seek(Dir_offset);
Dir_offset tell() const;
};
  • 重写范例程序:
1
2
3
4
5
6
7
8
9
10
#include <iostream.h>
#include "dirlib.h"
int main() {
Dir dp(".");
dirent d;
while (dp.read(d))
cout << d.d_name << endl;
}
  • C版本在全局名称空间中加入了7个名字:DIR、dirent、opendir、closedir、readdir、seekdir、telldir
  • C++版本只用了DIR、dirent和Dir_offset

  • C++版本不包含指针变量:d是一个表示目录条目的对象

    • 不会导致由于未定义指针而引起的崩溃
  • 如果目录不存在:

1
2
Dir d(some directory);
d.read(somewhere);
    • 即使打开目录失败,d也是一个对象
    • 确保Dir构造函数将它的对象置于一种恒定的状态,即使opendir底层调用失败
    • 另一种作法:抛出异常
    • 再一种可行的办法:允许创建(不存在的)Dir对象,读取时抛出异常
  • 如果传给readdir的参数是一个空指针:

    • C++版本里不是问题,read的对象必须已经是被创建的
  • 如果传给readdir的参数既不是一个空指针也不是一个由opendir函数返回的值

    • 同上
  • 对readdir的调用返回一个指向由库分配的内存指针,什么时候释放这些内存

    • read读取用户提供的对象而不是返回一个指针

C++接口更健壮

编写代码

  • 通过赋值和初始私有化使对Dir对象复制无效
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class Dir {
public:
Dir(const char*);
int read(dirent&);
void seek(Dir_offset);
Dir_offfset tell() const;
private:
DIR* dp;
Dir(const Dir&);
Dir& operator=(const Dir&);
};
Dir::Dir(const char* file): dp(opendir(file)) { }
// 调用closedir除非打开失败(dp为空)
Dir::~Dir() {
if (dp)
closedir(dp);
}
void Dir::seek(Dir_offset pos) {
if (dp)
seekdir(dp, pos);
}
Dir_offset Dir::tell() const {
if (dp)
return telldir(dp);
return -1;
}
int Dir::read(dirent& d) {
if (dp) {
dirent* r = readdir(dp);
if (r) {
d = *r;
return 1;
}
}
return 0;
}

库设计就是语言设计

C++的一个重要思想:用户自定义类型可以很容易当作內建类型使用

用户可以定制语言:语法与常规不同时会引起混淆(类设计者应当避免)

字符串

  • 例子:变长字符串的通用类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class String {
public:
String(char* p) {
sz = strlen(p);
data = new char[sz + 1];
strcpy(data, p);
}
~String() {
delete [] data;
}
operator char*() {
return data;
}
pricate:
int sz;
char* data;
};
  • 使用例子:
    • String s("hello world");
    • cout << s << endl;

内存耗尽

用户请求很大的String,没有足够的内存空间

  • 构造函数中的new表达式失败
    • 库抛出异常
    • 或者new表达式返回0
  • (如果)指针data被置为0

    • 试图写进数据的strcpy内部导崩溃
    • 复制前需检查data是否已经分配
    • 对零指针运用delete是无操作(no-op),析构函数无问题
    • 对分配失败的String运用operator char*()会返回一个零指针
    • 得到垃圾数据,或者内核转储(core dump)
  • 解决方法:

  • 1.声明内存耗尽的结果是不确定的:

    • 将负担转移给用户:检测operator char*的返回值
    • 容易被忽视
  • 2.在构造函数中检查内存分配是否成功

  • 3.在operator char*()内部检查对象是否有效
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class String {
public:
String(char* p) {
sz = strlen(p);
data = new char[sz + 1];
if (data == 0)
error;
else
strcpy(data, p);
}
operator char*() {
if (data == 0)
error();
return data;
}
// ...
};
  • 增加一个函数显示检查内存耗尽
  • 希望显式检查内存耗尽的用户可能会忽略
1
2
3
int String::valid() {
return data != 0;
}
  • 性能问题:每次访问String时都要检查
  • error可以返回也可能创建为0的data
  • 使用异常处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class String {
public:
String(char* p) {
sz = strlen(p);
data = new char[sz + 1];
if (data == 0)
throw std::bad_alloc();
else
strcpy(data, p);
}
~String() {
delete [] data;
}
operator char*() {
return data;
}
private:
int sz;
char* data;
};
  • 带错误检测的代码
1
2
3
4
5
6
try {
String s(p);
// ... : regular code
} catch(std::bad_alloc) {
// ... : memory run-out
}

复制

缺省的复制构造函数和赋值操作符会使data成员指向相同内存(会被析构函数释放两次)

  • 通过私有化复制构造函数和赋值操作符规定不能复制String
    • 声明而不定义相关不是虚函数的函数(复制构造函数不能是虚函数,赋值操作符可以使)
  • 考虑允许复制String

  • 将某个长度的String赋给一个长度不同的String

    • 一种可能:改变目标String的长度
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class String {
public:
String(char* p) {
assign(p, strlen(p));
}
String(const String& s) {
assign(s.data, s.sz);
}
~String() {
delete [] data;
}
String& operator=(const String& s) {
if (this != &s) {
delete [] data;
assign(s.data, s.sz);
}
return *this;
}
operator char*() {
return *this;
}
private:
int sz;
char* data;
void assign(const char* s, unsigned len) {
data = new char[len + 1];
if (data == 0)
throw std::bad_alloc();
sz = len;
strcpy(data, s);
}
};

隐藏实现

  • public的operator char*()暴露了3个漏洞

    1. 用户可以获得一个指针,修改保存在data中的字符
    • String类没有真正控制自己的资源
    1. 释放String时所占用内存也被释放,任何指向String的指针都会失效
    • 任何指针都会有的同样的问题
    1. 通过释放和重新分配目标String使用的内存将一个String的赋值实现为另一个
    • 导致任何指向String内部的指针失效
  • 定义一个向const char*的类型转换而不是向char*的类型转换可以解决第一个问题

1
2
3
4
5
6
7
class String {
public:
operator const char*() const {
return data;
}
// ...
};
  • 仍然会有某String对象被析构或改变之后继续使用原指向该对象的指针的错误

  • 放弃类型转换可以彻底解决这个问题

    • 但与存在的无数个操纵char*的非标准函数不兼容
  • 通过显式函数操作得到C串而不是隐式进行

1
2
3
4
5
6
7
class String {
public:
const char* make_cstring() const {
return data;
}
// ...
};
  • 由用户提供将data复制进去的空间可以消除指针的使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class String {
public:
int length() const {
return sz;
}
void make_cstring(char* p, int len) const {
if (sz <= len)
strcpy(p, data);
else
throw("Not enough memory supplied");
}
// ...
};

缺省构造函数

  • String数组需要缺省构造函数(或带缺省值的有参数的构造函数)

  • 让data为0或指向空字符串

1
2
3
4
5
6
7
8
class String {
public:
String(): data(new char[1]) {
sz = 0;
*data = '\0';
}
// ...
};

其他操作

  • 重载操作符为比较和合并操作提供更自然的接口

  • +=修改左边的String,作为类String的成员

    • 防止把一个String与自身连接:调用assign前保存指向旧值的指针
1
2
3
4
5
6
7
8
String& String::operator+=(const String& s) {
char* odata = data;
assign(data, sz + s.sz + 1);
strcat(data, s.data);
delete [] odata;
return *this;
}
  • operator+:二元操作符定义为非成员函数
1
2
3
4
5
String operator+(const String& op1, const String& op2) {
String ret(op1);
ret += op2;
return ret;
}
  • 6个关系操作符都可以用strcmp进行实现
    • 都是String的友元函数
1
2
3
int operator==(const String& op1, const String op2) {
return (strcmp(op1.data, op2.data) == 0);
}
  • 定义输入和输出操作符
1
2
3
ostream& operator<<(ostream& os, const String& s) {
return os << s.data;
}

子字符串

  • 返回某个子字符串的第一次、第二次…和最后一次出现的位置
  • 将第一次出现的某个字符或者所有该字符替换为某个其他字符
  • 将第一个出现的某个子字符串或者所有该子字符串替换为某个其他子字符串,长度不必相同

Substring类

  • 指向包含它的String的指针和某个长度
  • 或者两个指向String内部的指针:头和尾

  • 提供遍历、返回下一个符合规定的Substring等操作

    • 类似迭代器

相关问题既影响库设计者,也影响语言设计者


语言设计就是库设计

C++中简化库设计的部分

  • 设计一个好程序库的要求之一就是彻底隔离接口和实现

抽象数据类型

构造函数和析构函数

  • C++语言中将接口与实现分隔开的最基本的方法之一:采用构造函数和析构函数

  • 构造函数本身提供生成给定类对象的方法;析构函数提供相反的行为

  • 构造函数提供方法将用户看待对象的方式与对象的实际表示方式解耦

    • 改变内部表示方式,用户不必知道这个变化
  • 析构函数隐藏销毁对象的细节

成员函数与可见度控制

  • 要能够防止用户访问不应被看到的类成员
    • 私有成员

库和抽象数据类型

类型安全的链接(linkage)

  • 例子:C++曾要求程序员在重载全局函数是显式编写类似下面的语句
    • extern double sqrt(double);
    • extern float sqrt(float); // 曾经是错误
  • 使C和C++函数在单个程序中共存更简单:C不允许两个函数共用同一个名字

  • C++希望函数重载的程序员需要一种表示重载的方法:显式的overload声明

1
2
3
overload sqrt;
extern double sqrt(double);
extern float sqrt(float);
  • C++89版本废弃overload声明,解决多个头文件中的声明的先后问题

  • 与C程序通信的问题依旧存在:引进新的声明语法

    • extern "C" double sqrt(double);
    • 链接时sqrt(double)应该视为C函数来处理
    • 所有C++程序都必须重新编译
    • 绝大多数情况下库设计者不再担心函数名字(和其他库)冲突
    • 不同的编译单元之间进行类型检查时可以提高效率

命名空间

  • 防止不同的程序库设计者为各自组件采用相同的名字(而造成冲突)

  • 采用唯一对应于库提供者的字符串作为外部名字的前缀

    • 修改不易
  • 名称空间允许库设计者对会被库放到全局作用域的所有名称指定一个包装器(wrapper)

    • 用户可以通过使用由名称空间标识的名字
    • LittlePurpleSoftwareCompany::String s;
    • 可以从一个名称空间引入所有名字到程序中
    • using namespace LittlePurpleSoftwareCompany;
    • String s;

内存分配

  • 特殊用途的内存的分配
    • 调整分配策略以适应特定应用
    • 或者使用具有特殊性质的内存
  • 类Foo可以将该类的对象由某对特定的函数分配和释放

1
2
3
4
5
6
class Foo {
public:
void* operator new(size_t);
void operator delete(void*);
// ...
};
  • 另一种通用方式:在容器类的构造过程中完成
    • 容器设计者可能希望分配一大块内存并在这块内存的已知位置放入各个对象
  • 在由指针p定址(addressed)的内存中分配一个类型为T的对象

1
2
3
4
5
6
7
void* p = /* 获取一些空间 */
T* tp = new(p) T;
// 标准库中operator new(size_t, void* p)的定义
void* operator new(size_t, void* p) {
return p;
}
  • 如果将Foo对象放入到一个容器中
    • 容器具有优先权
    • new(p) Foo将现在由p定址的内存中分配一个Foo对象,即使类Foo有自己的内存分配器(allocator)

按成员赋值(memberwise assignment)和初始化

  • 赋值和初始化的递归缺省定义
    • AT&T的第一个C++版本:复制类对象的缺省定义是复制底层C结构体
    • 对于具有复杂类成员的简单类较麻烦
    • 递归地依赖于底层类的成员的赋值和复制的定义

异常处理

  • 实际中有很多错误来源
1
2
3
4
5
6
7
8
9
10
11
12
13
template <class T>
class Vector {
public:
Vector(int size): data(new T[size]) { }
~Vector() {
delete [] data;
}
T& operator[](int n) {
return data[n];
}
private:
// ...
};
  • 创建长度为负的向量
  • 没有足够大的内存保存长向量
  • 下标越界
  • 元素构造函数内部出错

出现错误时能够继续执行程序
返回错误标志不起作用:检查所有错误值使程序过于复杂
C++异常处理机制用于说明出现不应该被忽视的错误,用户如果愿意可以进行检查


《C++沉思录(Cplusplus Thinking)》笔记