大规模C++程序设计 -- 基础知识

基础知识

我们先回顾C++程序语言和面向对象分析的一些重要的方面,这些知识对于大型系统设计来说是基本的。

我们仔细分析多文件程序、声明与定义,以及在头文件和实现文件上下文中的内部链接和外部链接,然后研究typedef和assert的使用。

多文件C++程序

对于所有的(除了最小的)程序来说,将这个歌程序都放在单个文件中既不明智也不实用。首先,每次修改程序的任何部分,都必须重新编译整个程序。也不能再另一个程序中重用这个程序的一部分,除非把源码拷贝给另一个文件。这种复制很快就会成为难以维护的代码。

把一个程序中紧密关联的各部分源代码分别放在单独的文件中,可以使程序更有效的编译,同时也可以使它局部能够在其他程序中重用

声明与定义

一个声明就是一个定义,除非:

  • 它声明了一个没有详细说明函数体的函数
  • 它包含一个extern定义符并且没有初始化函数数或者函数体
  • 它是一个包含在一个类定义之内的静态数据成员的声明
  • 他是一个类名声明
  • 他是一个typedef声明

一个定义就是一个声明,除非:

  • 它定义个了一个静态类数据成员
  • 它定义了一个非内联成员函数

一个声明将一个名称引入一个程序;一个定义提供了一个实体在一个程序中的唯一描述

以下代码都是声明,可以重复,编译时候不会报错:

int f(int, int);
int f(int, int);
class IntServer;
class IntServer;
typedef int Int;
typedef int Int;
class A{
    friend class IntServer;
    friend class IntServer;
};
extern int g_var;
extern int g_var;

以下代码既是声明,也是定义,在给定的作用域内只能出现一次:

int x;
char *p;
extern int g_var = 1;
static int s_instance;
static int f(int, int) {}
inline int f(int, int) {}
enum Color{red, blue, green};
enum DummyType{};
enum {SIZE=100};
enum {} silly;
const double DD = 1.0e-6;
class Stack{};
struct Util{};
union Rep{};
template<class T> void sort(const T** array, int size){}

类内的函数和静态数据成员生命是例外,虽然不是定义,但是在类定义中也不能重复

class AA
{
public:
	static int i;
    static int i;
    int f();
    int f();
}

内部连接和外部链接

当文件被编译时候,预处理器首先递归的包含头文件,形成一个含有所有有必要信息的单个源文件。然后把这个中间文件(编译单元)被编译生成一个与主文件名相同的.o文件(目标文件)。连接把在不同的编译单元中产生的符号联系起来,构成一个可执行程序。有两种截然不同的连接:内部的和外部的。连接所用的类型会直接影响到我们如何将一个给定的逻辑结构合并进我们的物理设计中。

如果一个名称对于它的编译单元来说是局部的,并且在连接时不可能与其他编译单元中的同样的名称相冲突,那么这个名称有内部连接

内部连接以为这对这个定义的访问被局限于当前的编译单元中。也就是说,一个有内部连接的定义对于任何其他编译单元来说都是不可见的,所以在连接过程中不能用来解析未定义的符号。例如:static int x; 虽然定义在文件作用域内,但关键词static决定了连接时内部的。内部连接的另外一个例子是枚举类型enum Boolean {No, Yes}; 枚举类型是定义(不仅仅是声明),但是它们绝不会将符号引进.o文件。要想让有内部连接的定义影响程序的其他部分,他们必须放置在头文件中,而不是在源文件中。

有内部连接定义的一个重要例子是一个类的定义。类Point的描述是一个定义,不是声明;因此,他不能和同一个作用域的一个编译单元中重复出现。如果类要在单个编译单元之外使用,那么它不需定义在一个头文件中。内联函数定义是内部连接的另外一个例子。

在一个多文件程序中,如果一个名称在连接时可以和其他编译单元交互,那么这个名称就有外部连接

class Point
{
  	int x_;
    int y_;
public:
    Point(int x, int y) : x_(x), y_(y) {}
    int x() const { return x_; }
    int y() const { return y_; }
};

inline int operator==(const Point& left, const Point& right)
{
    return left.x() == right.x() && left.y() == right.y();
}

外部链接意味着该定义不仅仅局限于单个编译单元中。有外部连接的定义可以在.o文件中产生外部符号,这些外部符号可以被所有其他编译单元访问,用来解析他们未定义的符号。这种外部符号必须在整个程序中是唯一的,否则这个程序不能被连接。

非内联成员函数有外部链接,非内联函数、非静态自由函数也一样。有外部链接的函数例子如下:

Point& Point::operator+=(const Point& right) 
{
    x_ += right.x_;
    y_ += right.y_;
    return *this;
}
Point& Point::operator+(const Point& left, const Point& right) 
{
    return Point(left.x() + right.x(), right.y() +left.y());
}

其中,我们提到的自由函数,绝不是友元函数。一个自由函数不必是任何类的友元;无论如何他都应该是一个实现细节

因为声明只对当前编译单元有用,多以声明本身并不会将任何东西引入.o文。考虑以下声明:

int f();
extern int i;
struct S
{
    int g();
}

这些声明不会影响到最终编译文件的内容。每一个都只是命名一个外部符号,使当前的编译单元在需要的时候可以访问相应额全局定义。实际上是符号名称的使用(例如,调用一个函数)而不是声明本身导致了一个未定义的符号被引入到.o文件。正式这个事实允许构建早期的原型:只要所缺的功能不是所需的,那么部分完成的对象可以用在运行程序中。

在前面的例子中,三个声明中的每一个都激活对一个外部定义函数或者对象的访问。我们也许会很随便,说这些声明有外部连接。但是还有其他种类的声明不能用来激活对外部定义的访问。我们常常会称这类声明有内部连接。例如:typedef int Int; 是一个typedef声明。它既不能将任何符号引进.o文件,也不能通过它来访问一个有外部连接的全局对象;它的连接时内部的。一种重要的恰好有内部连接的声明是类的声明。

这些声明在把名称Point作为某种用户自定义类型引入时都有相同的作用;特殊的声明类型不必和实际定义的类型相匹配:

class Point;
struct Point;
union Point;

这些声明潜在引用的定义也有内部连接;这个特性把类声明与前面所举例子中的外部声明区分开来。类声明和类定义都对.o文件没有共享,只是为当前的编译单元所用。

class Rep;
union Rep {};

另一方面,静态类数据成员有外部连接:静态的类数据成员只是一个声明,但是在源文件中他的定义有外部连接

class Point{
	static int s_num_points_;
};

// cpp
int Point::s_num_points_;

注意:这种规范,每一个静态类数据成员都必须在最终程序的某个准确的地方定义一次

最后,C++语言对枚举类型和类的方式是不同的

enum Point;

在C++中不可能未经定义就声明一个枚举类型,gcc不允许但是msvc编译器是允许的,这主要是因为gcc编译器无法确定前向声明的枚举类型应该如何分配空间,C++11之后,如果使用enum Point : unsigned int;也是可以完成前项声明的。就像我们将要看到的,类前置声明常常用来替换预处理 #include指令,以便可以未经定义就声明一个类。

头文件

C++中,将一个带有外部连接的定义放置在头文件中几乎都是编程错误。如果这样做了,还把这个头包含在不止一个编译单元中,那么把他们连接在一起的时候就会出错,且会出现下面这样的错误信息

MULTIPLY DEFINED SUMBOL

在C++中,在一个头文件作用域内放置带有内部连接的定义是合法的,但这种做法并不是人们希望的。不仅因为你这些文件作用域内的定义会污染全局名称空间,而且在有静态数据和函数的情况下,它们会在每一个包含有这个头的编译单元中消耗数据空间。甚至在文件作用域中把数据声明为const也会引起相同的问题,尤其是在这个常量的地址已经获得的情况下。将一个文件作用域常量和一个静态常量类成员进行比较;在整个程序中只能有一个类作用域常量的拷贝

双重的非成员数据定义这种冗余不仅会影响到程序的大小,还会影响运行的性能,因为它破坏了主机的高速缓存机制。但是,有时也会有正当的理由在头文件中放置一个用户自定义对象的静态实例。特别是,这样一个对象的构造函数可以用来确保一个特殊的全局工具在使用前已经被初始化。虽然这种解决方案对于中小型系统来说是很好的,但是对于大型的系统来说就可能有疑问了。

#program once
int z;
extern int LENGTH = 10;
const int WIDTH = 5;
static int y;
static void func() {}
class Radio
{
    static int s_count_;
    static const double s_pi_;
    int size_;
    
public:
    int size() const;
}

inline int Radio::size() const
{
    return size_;
}
int Radio::s_count_;
double Radio::s_pi_ = 3.1415926;
int Radio::size() const
{
    return size_;
}

源文件cpp

有时候我们会选择定义一些函数和数据用于我们自己的视线,不希望这种实现暴露于我们的编译单元之外。有内部连接的定义可以出现在一个.c文件的文件作用域中,不会影响全局符号名称空间。在.c文件的文件作用域中要避免的定义是:没有声明为静态的数据和函数。例如:

int i;
int max(int a, int b) { return a > b ? a : b; }

上面的这些定义有外部连接,可能会与全局名称空间中的其他相似的名称之间存在潜在的冲突。因为内联和静态自由函数有内部连接,这些种类的函数可以在源文件的文件作用域定义,不会污染全局名称空间。例如:

inline int min(int a, int b) { return a < b ? a: b; }
static int func(int n) { return a <=1 ? 1 : n*func(n-1) }

枚举类型的定义、声明为是static的非成员对象以及const数据定义等也有内部连接。在源文件中的文件作用域中定义这些实体都是安全的。例如:

#include <math.h>
enum {START_SIZE = 1, };
const double pi_sq = M_PI * M_PI;
static const char *names[] = { "aaa", "bbb" };
static Link *s_root_p;
Link *const s_first_p = s_root_p;

其他结构,如typedef声明和预处理宏,不会将输出符号引入.o文件。他们也可以出现在源文件作用域中,不会影响全局命名空间。例如:

typedef int (aaa *)();
#define CASE(x) case x : cout << "x: " << endl;
#define CASE(x) case x : cout <<  #x << endl;

typedef 和宏在C++中用处有限,如果滥用的话可能有害。

typedef声明

一个typedef声明为一个已经存在的类型创建了一个别名,而不是一个新的类型。因此,一个typedef只是提供了类型安全的假象。所以接口中的typedef可以轻而易举地做更多的有害的事情而不是好事

class Person
{
public:
    typedef double Inches;
    typedef double Pounds;
    
    Inches getHeight() const;
    void setWidget(Pounds weight);
}
void func(const Person& p)
{
    Person::Inches height = p.getHeight();
    p.setWeight(height);
}

如上所示,两个类型Inches和Pounds在结构上是相同的,因此是完全可以互换的。这些类型别名绝对不提供编译时类型安全,也使人不容易知道实际类型。

但是在定义复杂的函数参数时,类型别名可以起作用。例如:

typedef int (Person::*PCPMFDI)(double) const;

把PCPMFDI声明为一个类型:指针,指向一个const Person 成员函数,其参数为double类型,并且返回一个int。在跨越不同的编译器和计算机硬件时,有些数据成员的大小必须保持不变,在定义这样的数据成员时,类型别名也是很有用的

assert语句

标准C库提供了一个名为assert的宏,用来保证一个给定的表达式求值为一个非零值;否则就会输出一个出错的信息并且结束程序执行。assert语句使用方便,并且是开发者的一个强有力的文档工具。assert语句就像活动的注释–他们不仅使假定清楚而准确,而且如果违反了它的假设,他们实际上会做某些处理。

要在运行时捕捉程序的逻辑错误,使用assert语句可能是一种有效的方法,并且他们很容易从产品代码中过滤出来。一旦开发结束,只需通过在编译过程中定义预处理器符号NDEBUG,就可以消除这为检测代码错误而进行的冗余测试的运行时开销。但是一定要记住,放在assert中的代码在产品版本中会被省略掉

class String 
{
    enum { DEF = 8 };
    char *array_p_;
    int size_;
    int length_;
public:
    String();
}

String::String()
    : size_(DEF)
    , length(0)
{
// 	assert(array_p_ = new char[size_]);    // error    
    array_p_ = new char[size_];
    assert(array_p_);
}

这种处理容错技术的通用做法是抛出像CodingError这样的异常信息了事。甚至较高层次的软件都使用这种方式来捕捉和处理该问题。在没有错误处理程序时,默认的行为就简化了assert行为。

代码风格的问题

这部分不说,请自己看开源的代码风格学习,项目尽量保持一致即可

继承与分层

在面向对象设计的上下文中,当有人提到层次结构这个词时,许多人就会想到继承,继承是逻辑层次结构的一种形式–分层则是另一种形式。到目前为止,面向对象设计更产检的逻辑层级结构形式产生与分层

如果某个类在它的实现中实质地使用了某个类型,则该类分层于该类型之上

分层是把更小、更简单或者原始的类型建成更大、更复杂或更精密的类型的过程。分层经常通过组合来进行,但任何形式的实质使用(即任何导致物理依赖的使用)都具备分层的资格

在一个源文件内完整的定义一个类,虽然在技术上违背了一条规则,但是在实践中是相对无害的,因为名字冲突趋于向于阻止人们尝试直接使用外部符号。唯一真正的危险是外部定义可能会与一些其他的同样的定义相冲突。如果那个类定义在它自己的单独的组件中仍然会是有相应的头文件wi.h。可以通过组织各种类型的部件对象来创建新的屏幕类型。我们将这M个类型成为s1 s2 sm,每一个类si都存在于自己的独立编译单元之中,带有头文件si.h。

#include "w1.h"
#include "w2.h"
#include "wn.h"
class S13
{
    W1 w1a_;
    W2 w2a_;
    Wn wna_;
}

如果我们有很多的screen类,则每个都要包含所有的screen头文件。

避免高度度的包含,是绝缘的一部分,我们之后讨论

  • 继承:连同动态绑定,可以用来区别面向对象和基于对象语言,后者支持用户自定义来兴和分层,但是不支持继承。继承的语义和分层有很大的不同。例如:基类和派生类的公共功能都可以被客户访问。对继承来说,越特殊越具有的类依赖,越一般越抽象的类。对于分层来说,在较高的层次上的类依赖较低抽象层次上的类。

最小化

有些累作者想让他们的类满足所有人的所有需要。这种常见和似乎高贵的愿望令人忧虑。作为开发人员,我们必须记住,只因为一个客户要求增加功能并不意味着对所有的类都是适当的。假设你是一个类的作者,10个客户中的每一个都请求你进行不同的增加。如果你同意,则会发生两件事:

  • 你不得不实现、测试和存档10个新特征,你开始并没有认为这些新特征是你要实现的抽象的一部分
  • 你的10个客户的每一个都会得到9个他们并没有请求和可能不必要或者不想要的新特征

这种只要组件足够但不必备的最低要求方法适合于蒸菜开发的大型项目,在这种项目中,组件的用户是内部的或者组件的用户处在一个一旦需要即可快速请求和接收额外功能的位置,最极端的情况是组件高度专业化并且作者是唯一有意向的用户。在这种情况下,实现任何不必要的功能都可能是没有根据的。当然,若一个功能实现对一个抽象来说是本质的,则圣罗该功能实现是没有意义的。所以我们要保证功能总是容易添加而不容易删除。

总结

  • 大型C++程序分布在不止一个源文件中,把程序分割成单独的编译单元可以使重编译更有效,并且更可能被重用
  • 虽然大多数C++声明可以在一个给定的作用域重复,但是每一个用在C++程序中的对象、函数或者类只能有一个定义
  • 把有内部连接的定义限制在单个的编译单元中,不能影响其他的编译单元,除非它放在一个头文件中。遮掩干的定义可以存在于源文件的文件作用域内,不会影响全局符号名称空间
  • 在有外部连接的定义在连接时可以用来解析其他编译单元中的未定义符号。这样的定义放在头文件几乎肯定是一个错误
  • typedef声明只是类型的别名,并不提供额外的编译时的类型安全检查
  • 在开发时可以用assert语句来发现代码错误,不会影响程序的大小或在一个产品版本中的运行时性能
  • 43
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

turbolove

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值