《C++编程思想》(Thinking in C++)部分精华提取

本文探讨了C++编程中的关键概念,包括声明与定义的区别,如何通过句柄类实现部分隐藏的实现细节,以及内联函数的作用。在声明与定义中,声明向编译器介绍名字,而定义分配存储空间。句柄类通过只暴露公共接口来提高代码安全性,减少重复编译。内联函数则用于消除函数调用的开销,提高效率。此外,文章还提及了C++中extern的使用,以及值拷贝与位拷贝的差异,强调了防止对象切片和理解RTTI的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1、声明与定义:

首先,必须知道“声明”和“定义”之间的区别,因为这两个术语在全书中会被确切地使用。“声明”向计算机介绍名字,它说,“这个名字是什么意思”。而“定义”为这个名字分配

存储空间。无论涉及到变量时还是函数时含义都一样。无论在哪种情况下,编译器都在“定义”处分配存储空间。对于变量,编译器确定这个变量占多少存储单元,并在内存中产生存放它们的空间。对于函数,编译器产生代码,并为之分配存储空间。函数的存储空间中有一个由使用不带参数表或带地址操作符的函数名产生的指针。定义也可以是声明。如果该编译器还没有看到过名字A,程序员定义int A,则编译器马上为这个名字分配存储地址。声明常常使用于e x t e r n关键字。如果我们只是声明变量而不是定义它,则要求使用e x t e r n。对于函数声明, e x t e r n是可选的,不带函数体的函数名连同参数表或返回值,自动地作为一个声明。函数原型包括关于参数类型和返回值的全部信息。int f(float,char);是一个函数原型,因为它不仅介绍f这个函数的名字,而且告诉编译器这个函数有什么样的参数和返回值,使得编译器能对参数和返回值做适当的处理。C + +要求必须写出函数原型,因为它增加了一个重要的安全层。

extern int i;//声明

extern float f(float);//声明

float b;//声明+定义

float f(float a){//定义 

return a+1.0;

}

int i;//定义

int h(int x){//声明+定义

return x+1;

}

main(){

b=1.0;

i=2;

f(b);

h(i);

}

在函数声明时,参数名可给出也可不给出。而在定义时,它们是必需的。这在C语言中确实如此,但在C + +中并不一定。

2、句柄类:

C + +中的存取控制允许将实现与接口部分分开,但实现的隐藏是不完全的。编译器必须知道一个对象的所有部分的声明,以便创建和管理它。我们可以想象一种只需声明一个对象的公共接口部分的编程语言,而将私有的实现部分隐藏起来。但C + +在编译期间要尽可能多地做静态类型检查。这意味着尽早捕获错误,也意味着程序具有更高的效率。然而这对私有的实现部分来说带来两个影响:一是即使程序员不能轻易地访问实现部分,但他可以看到它;二是造成一些不必要的重复编译。
可见的实现部分:
有些项目不可让最终用户看到其实现部分。例如可能在一个库的头文件中显示一些策略信息,但公司不想让这些信息被竞争对手获得。比如从事一个安全性很重要的系统(如加密算法),我们不想在文件中暴露任何线索,以防有人破译我们的代码。或许我们把库放在了一个“有敌意”的环境中,在那里程序员会不顾一切地用指针和类型转换存取我们的私有成员。在所有这些情况下,就有必要把一个编译好的实际结构放在实现文件中,而不是让其暴露在头文件中。
减少重复编译:
在我们的编程环境中,当一个文件被修改,或它所依赖的文件包含的头文件被修改时,项目负责人需要重复编译这些文件。这意味着无论何时程序员修改了一个类,无论是修改公共的接口部分,还是私有的实现部分,他都得再次编译包含头文件的所有文件。对于一个大的项目而言,在开发初期这可能非常难以处理,因为实现部分可能需要经常改动;如果这个项目非常大,用于编译的时间过多就可能妨碍项目的完成。解决这个问题的技术有时叫句柄类( handle classes)或叫“Cheshire Cat”[ 1 ]。有关实现的任何东西都消失了,只剩一个单一的指针“ s m i l e”。该指针指向一个结构,该结构的定义与其所有的成员函数的定义一样出现在实现文件中。这样,只要接口部分不改变,头文件就不需变
动。而实现部分可以按需要任意更动,完成后只要对实现文件进行重新编译,然后再连接到项目中。这里有个说明这一技术的简单例子。头文件中只包含公共的接口和一个简单的没有完全指定的类指针。这是所有客户程序员都能看到的。这行
struct cheshire;
是一个没有完全指定的类型说明或类声明(一个类的定义包含类的主体)。它告诉编译器,cheshire 是一个结构的名字,但没有提供有关该结构的任何东西。这对产生一个指向结构的指针来说已经足够了。但我们在提供一个结构的主体部分之前不能创建一个对象。在这种技术里,包含具体实现的结构主体被隐藏在实现文件中。
#ifndef HANDLE_H

#define HANDLE_H

class handle{

struct cheshire;

cheshire *smile;

public:

void initialize();

void cleanup();

int read();

void change(int);

};

#endif

这是所有客户程序员都能看到的。这行
struct cheshire;
是一个没有完全指定的类型说明或类声明(一个类的定义包含类的主体)。它告诉编译器,cheshire 是一个结构的名字,但没有提供有关该结构的任何东西。这对产生一个指向结构的指针来说已经足够了。但我们在提供一个结构的主体部分之前不能创建一个对象。在这种技术里,包含具体实现的结构主体被隐藏在实现文件中。

#include "handle.h"

struct handle::cheshire{

int i;

};

void handle::initialize(){

smile=(cheshire *)malloc(sizeof(cheshire));

assert(smile);

smile->i=0;

}

void handle::cheanup(){

free(smile);

}

int handle::read(){

return smile->i;

}

void handle::change(int x){

smile->i=x;

}

cheshire 是一个嵌套结构,所以它必须用范围分解符定义
struct handle::cheshire {
在h a n d l e : : i n i t i a l i z e ( )中,为cheshire struct分配存储空间[ 1 ],在h a n d l e : : c l e a n u p ( )中这些空间被释放。这些内存被用来代替类的所有私有部分。当编译H A N D L E . C P P时,这个结构的定义被隐藏在目标文件中,没有人能看到它。如果改变了c h e s h i r e的组成,唯一要重新编译的是H A N D L E . C P P,因为头文件并没有改动。句柄(h a n d l e)的使用就像任何类的使用一样,包含头文件、创建对象、发送信息。

#include "handle.h"

main(){

handle u;

u.initialize();

u.read();

u.change(2);

}

客户程序员唯一能存取的就是公共的接口部分,因此,只是修改了在实现中的部分,这些文件就不须重新编译。虽然这并不是完美的信息隐藏,但毕竟是一大进步

3、重载:

返回值重载:

void f();

int f();

当编译器能从上下文中唯一确定函数的意思时,如int x = f();这当然没有问题。然而,在C中,我们总是可以调用一个函数但忽略它的返回值,在这种情况下,编译器如何知道调用哪个函数呢?更糟的是,读者怎么知道哪个函数会被调用呢?仅仅靠返回值来重载函数实在过于微妙了,所以在C + +中禁止这样做。

4、const :

与使用# d e f i n e一样,使用c o n s t必须把c o n s t定义放进头文件里。这样,通过包含头文件,可把c o n s t定义单独放在一个地方并把它分配给一个编译单元。C + +中的c o n s t默认为内部连接,也就是说,c o n s t仅在c o n s t被定义过的文件里才是可见的,而在连接时不能被其他编译单元看到。当定义一个常量(c o n s t)时,必须赋一个值给它,除非用e x t e r n作了清楚的说明:
extern const bufsize;
虽然上面的e x t e r n强制进行了存储空间分配(另外还有一些情况,如取一个c o n s t的地址,也要进行存储空间分配),但是C + +编译器通常并不为c o n s t分配存储空间,相反它把这个定义保存在它的符号表里。当c o n s t被使用时,它在编译时会进行常量折叠。

5、volatile:

v o l a t i l e的语法与c o n s t是一样的,但是v o l a t i l e的意思是“在编译器认识的范围外,这个数据可以被改变”。不知何故,环境正在改变数据(可能通过多任务处理),所以,v o l a t i l e告诉编译器不要擅自做出有关数据的任何假定—在优化期间这是特别重要的。如果编译器说:“我已经把数据读进寄存器,而且再没有与寄存器接触”。一般情况下,它不需要再读这个数据。但是,如果数据是v o l a t i l e修饰的,编译器不能作出这样的假定,因为可能被其他进程改变了,它必须重读这个数据而不是优化这个代码。就像建立c o n s t对象一样,程序员也可以建立v o l a t i l e对象,甚至还可以建立const volatile对象,这个对象不能被程序员改变,但可通过外面的工具改变。下面是一个例子,它代表一个类,这个类涉及到硬件通信:
就像c o n s t一样,我们可以对数据成员、成员函数和对象本身使用v o l a t i l e,可以并且也只能为v o l a t i l e对象调用v o l a t i l e成员函数。函数i s r ( )不能像中断服务程序那样使用的原因是:在一个成员函数里,当前对象( t h i s)的地址必须被秘密地传递,而中断服务程序I S R一般根本不要参数。为解决这个问题,可以使i s r ( )成为静态成员函数,这是下面章节讨论的主题。v o l a t i l e的语法与c o n s t是一样的,所以经常把它们俩放在一起讨论。为表示可以选择两个中的任何一个,它们俩通称为c - v限定词。

6、内联函数(inline):

在解决C + +中宏存取私有的类成员的问题过程中,所有和预处理器宏有关的问题也随着消失了。这是通过使宏被编译器控制来实现的。在C + +中,宏的概念是作为内联函数来实现的,而内联函数无论在任何意义上都是真正的函数。唯一不同之处是内联函数在适当时像宏一样展开,所以函数调用的开销被取消。因此,应该永远不使用宏,只使用内联函数。
任何在类中定义的函数自动地成为内联函数,但也可以使用i n l i n e关键字放在类外定义的函数前面使之成为内联函数。但为了使之有效,必须使函数体和声明结合在一起,否则,编译器将它作为普通函数对待。因此
inline int PlusOne(int x);
没有任何效果,仅仅只是声明函数(这不一定能够在稍后某个时候得到一个内联定义)。成功的方法如下:
inline int PlusOne(int x) { return ++x ;}
注意,编译器将检查函数参数列表使用是否正确,并

Bruce Eckel 《Thinking in Java》(Java编程思想)作者。Eckel有20年专业编程经验,并自1986年起教育人们如何撰写面向对象程序,足迹遍及全球,成为一位知名的 C++教师和顾问,如今兼涉Java。他是C++标准委员会拥有表决权的成员之一,曾经写过另五本面向对象编程书籍,发表过150篇以上的文章,是多本计算机杂志的专栏作家。Eckel开创Software Development Conference的C++、Java、Python等多项研讨活动。拥有应用物理学学士和计算机工程学硕士学位。 目录 译者序 前言 第1章 对象导言 第2章 对象的创建与使用 第3章 C++中的C 第4章 数据抽象 第5章 隐藏实现 第6章 初始化与清除 第7章 函数重载与默认参数 第8章 常量 第9章 内联函数 第10章 名字控制 第11章 引用和拷贝构造函数 第12章 运算符重载 第13章 动态对象创建 第14章 继承和组合 第15章 多态性和虚函数 第16章 模板介绍 附录A 编码风格 附录B 编程准则 附录C 推荐读物 索引 第2卷:实用编程技术 出版者的话 专家指导委员会 译者序 前言 第一部分 建立稳定的系统 第1章 异常处理 第2章 防御性编程 第二部分 标准C++库 第3章 深入理解字符串 第4章 输入输出流 第5章 深入理解模板 第6章 通用算法 第7章 通用容器 第三部分 专题 第8章 运行时类型识别 第9章 多重继承 第10章 设计模式 第11章 并发 附录 附录A 推荐读物 附录B 其他 索引 
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值