您的位置:首页 > 编程语言 > C语言/C++

C++编程思想笔记(2)

2014-05-16 21:47 211 查看
什么是对象

在c++中,对象就是变量,它的最纯正的定义是“一块存储区”(更明确的说法是,“对象必须有唯一的标识”,在C++中是一个唯一的地址),它是一块空间,在这里能存放数据,而且还隐含着有对这些数据进行处理的操作。

object.menber Function(atglist)是对一个对象“调用一个成员函数”。而在面对对象的用法中,也称之为“向一个对象发送消息”。例如,对于Stash s,语句s.add(&i)“发送消息给s”,也就是说,“将它与自己sdd()”。事实上,面向对象编程可以总结为一句话,“向对象发送消息”。实际上,需要做的所有事情就是创建一束对象并且给它们发送消息。

对象细节

一个struct的大小是它的所有成员大小的和,有时,当一个struct被编译器处理时,会增加额外的字节以使得边界整齐,这主要是为了提高执行效率。

头文件形式

用c++建造大项目的最好的方法是采用库,收集相关的函数到同一个对象模型块或库中,并且使用同一个头文件存放所有这些函数的声明。在C++中这是必须的;在C中,可以把所有的函数都放进C库中,但是在C++中,由抽象数据类型确定库中的函数,这些函数通过它们共同访问一个struct中数据而联系起来。任何成员函数必须在struct声明中声明,不能把它放在其他地方。

头文件的重要性

在C++中,如果在一个头文件中声明了一个struct,我们在使用struct的任何地方和定义这个struct成员函数的任何地方必须包含这个头文件。

头文件的第二个问题是:如果把一个struct声明放在一个头文件中,就有可能在一个编译程序中对此包含这个头文件。C和C++都允许重声明函数,只要两个声明匹配即可,但是两者都不允许重声明结构。重声明在C++中出现了问题,因为每个数据类型(如带函数的结构)一般有它自己的头文件,如果想创造另一个数据类型(它使用第一个数据类型),则我们必须将第一个数据类型的头文件包含在这另一个数据类型中。在我们项目的任何cpp文件中,很有可能包含几个已经包含了这个相同的头文件的文件。在一次变异过程中,编译器可能会对此看到这个相同的头文件,除非特别处理,否则编译器将发现结构的重声明,并报告编译时的错误。

头文件的标准

对于包含结构的每个头文件,应当首先检查这个头文件是否已经包含在特定的cpp文件中。这需要通过测试预处理器的标记来检查,如果这个标记没有设置,这个文件没有包含,则应当设置它(所以这个结构不会被重声明),并声明这个结构。如果这个标记已经设置,则表明这个类型已经声明了,所以应当忽略这段声明它的代码。

#ifndef HEADER_FIAG
#define HEADER_FIAG
//类型声明
#endif


正如所看到的,头文件第一次被包含,这个头文件的内容(包括类型声明)将被包含在预处理器中。对于在单个编译单元中的所有后续的包含,该类型声明被忽略。防止多次包含的这些预处理器语句常常称为包含守卫。

头文件中的名字空间

不要在头文件中放置 使用指令(如using namespace std;),这样的使用指令去除了对这个特定名字空间的保护,并且这个结构一直持续到当前编译单元结束。

在项目中使用头文件

当用C++建立项目时,我们通常要汇集大量不同的类型(带有相关函数的数据结构)。一般将每个类型或一组相关类型的声明放在一个单独的头文件中,然后在一个处理单元中定义这个类型的函数。当使用这个类型时必须包含这个头文件,执行正确的声明。

友元

struct X;

struct Y{
void f(X*);
};

struct X{
...;
friend void Y::f(X*);
}


因为c++的编译器要求在引用任一变量之前必须先声明,但要声明Y::f(X*),又必须先声明struct X。解决的办法是:注意到Y::f(X*)

引用了一个X对象的地址“X*”。如果试图传递整个对象,编译器就必定义须知道X的全部定义以确定它的大小以及如何传递,这就使得程序员无法去声明一个类似于Y::g(X)的函数。

句柄类

有些项目不可让最终客户程序员看到其实现部分。在这种情况下,就有必要把一个编译好的实际结构放在实现文件中,而不是让其暴露在头文件中。

当一个文件被修改,或它所依赖的头文件被修改时,项目管理员需要重复编译该文件。这意味着程序员无论何时修改了一个类,无论修改的是公共的接口部分,还是私有成员的声明部分,他都必须再次编译包含头文件的所有文件。解决这个问题的技术称为句柄类或“Cheshire cat”。有关实体的任何东西都消失了,只剩下一个单指针“smile”。该指针指向一个结构,该结构的定义与其所有的成员函数的定义一同出现在实现文件中。这样,只要接口部分不改变,头文件就不需要变动。而实现部分可以按需要任意更改,完成后只需要队实现文件进行重新编译,然后重新连接到项目中。

class Handle{
struct Cheshire;
Cheshire* smile;
public:
void initialize();
void cleanup();
int read();
void change(int);
};


struct Cheshire;是一个不完全的类型说明或类声明。它告诉编译器,Cheshire是一个结构名,但没有提供有关该struct的任何细节。

清除定义块

一般来说,应该在尽可能靠近变量的使用点处定义变量,并在定义时就初始化

默认构造函数

如果没有定义构造函数,编译器会创建一个默认的构造函数,但是编译器合成的构造函数的行为很少是我们期望的。我们应该把这个特征看成是一个安全网,尽量少用默认构造函数。一般来说,应该明确地定义自己的构造函数,而不让编译器来完成。

联合

struct 和class 惟一的不同之处就在于,struct默认为public,而class默认为private。很自然的,也可以让struct有构造函数和析构函数。另外,一个union也可以带有构造函数、析构函数、成员函数甚至访问控制。union与class的惟一不同之处在于存储数据的方式(也就是说再union中int类型的数据和float类型的数据在同一内存区覆盖存放),但是union不能在继承时作为基类使用,从面向对象设计的观点来看,这是一种极大的限制。

默认参数

在使用默认参数时必须记住两条规则。第一,只有参数列表的后部参数才是可默认的,也就是说,不可以在一个默认参数后面又跟一个非默认的参数。第二,一旦在一个函数调用中开始使用默认参数,那么这个参数后面的所有参数与都必须是默认的(这可以从第一条中导出)

值替代

预处理器在C语言中用值替代名字的典型用法是:

#define BUFSIZE 100

BUFSIZE是一个名字,它只是在预处理期间存在,因此它不占用存储空间且能放一个头文件里,目的是为使用它的所有编译单元提供一个值。大多数情况下,BUFSIZE的工作方式与普通变量类似,而且没有类型信息,这就会隐藏一些很难发现的错误。C++用const来消除这些问题,具体方法是把值替代移交给编译器。

const int bufsize = 100;

这样就可以在编译时编译器需要知道这个值的任何地方使用bufsize,同时编译器还可以执行常量折叠,也就是说,编译器在编译时可以通过必要的计算把一个复杂的常量表达式通过缩减简单化。

头文件里的const

当定义一个const时,必须赋一个值给它,除非使用extern作出了清楚的说明:

extern const int bufsize;

通常C++编译器并不为const创建存储空间,相反它把这个定义保存在它的符号表里,但是,上面的extern强制进行了存储空间分配。

指向const的指针

const int* u;

int const* u;

这两种定义都表示u是一个指针,它指向一个const int 。应该坚持用第一种形式。

const指针

使指针本身成为一个const指针,必须把const标明的部分放在*的右边,如

int d =1;

int* const w=&d;

表示w是一个指针,这个指针是指向int的const指针,编辑器要求给它一个初始值,这个值在指针生命期间内不变。要改变它所指的值可以写

*w=2;

也可以以下形式把一个const指针指向一个const对象:

int d=1;

const int* const x=&d;

int const* const x2=&d;

现在指针和对象都不会改变。

赋值和类型检查

C++可以把一个非const对象的地址赋给一个const指针,因为也许有时不想改变某些可以改变的东西。然而,不能把一个const对象的地址赋给一个非const指针,因为这样做可能通过被赋值的指针改变这个对象的值,、。

传递const值

如果函数参数是按值传递,则可用指定参数是const的。

void f1(const int i){

i++;

}

这里是错的,因为变量初值不会被函数f1()改变。在函数里,const参数不能被改变。

返回const值

如果返回值是一个常量:

const int g();

这就约定了函数框架里的原变量不会被修改。另外,因为这是按值返回的,所以这个变量被制成副本,使得初值不会被返回值所修改。

对于内建类型(内建类型是指C++所支持的基本类型,而用户自定义类型是程序员或者开发商定义的类型,可能是类、或者是struct、enum )来说,按值返回的是否是一个const,是无关紧要的,所以按值返回一个内建类型时,应该去掉const,从而不使客户程序员混淆。

当处理用户定义的类型时,按值返回常量是很重要的,如果一个函数按值返回一个类对象为const时,那么这个函数的返回值不能使一个左值(即它不能被赋值,也不能被修改) 。

#include<iostream>
using namespace std;

class X{
int i;
public:
X(int ii=0);
void modify();
};

X::X(int ii){ i=ii; }
void X::modify(){ i++; }

X f5(){
return X();
}

const X f6(){
return X();
}

void f7(X& x){
x.modify();
}

int main(){
f5()=X(1);
f5().modify();
f7(f5());
f6()=X(1);
f6().modify();
f7(f6());
}


有时候在求表达式值期间,编译器必须创建临时对象。向其他任何对象一样,它们需要存储空间,并且必须能够构造和销毁。上面的例子中,f5()返回一个非congst X对象,但在表达式f7(f5())中,编译器必须产生一个临时对象来保存f5()的返回值,使它能传递给f7()。如果f7()的参数是按值传递的话,在f7()中形成那个临时量的副本,不会对临时对象产生任何的影响,但是f7()的参数是按引用传递,这意味着它取临时对象X的地址,因为f7()所带的参数不是按const引用传递的,所以它允许对临时对象X进行修改。但是编译器知道:一旦表达式计算结束,该临时对象也会不复存在,因此,对临时对象X所作的任何修改也将丢失。

f5()=X(1);

f5()返回一个X对象,而且对编译器来说,要满足上面的表达式,它必须创建临时对象来保存返回值。于是在这个表达式中,临时对象也被修改,表达式被编译之后,临时对象也会将被清除。结果丢失了所有的修改。

标准参数传递

当传递一个参数时,首先选择按引用传递,而且是const引用。对于函数的创建者来说,传递地址总比传递整个类对象更有效,如果按const引用来传递,意味着函数将不改变该地址所指的内容,从客户程序员的观点来看,效果就像按值传递一样。

由于引用的语法的原因,把一个临时对象传递给接受const引用的函数是可能的,但不能把一个临时对象传递给接受指针的函数————对于指针,它必须明确地接受地址。

构造函数初始化列表

在一个类里建立一个普通的(非static的)const时,不能给它初值。这个初始化工作必须在构造函数中进行。初始化所有const的地方,就是构造函数初始化列表。

编译期间类里的常量

一个内建类型的static const可以看做一个编译期间的常量。必须在static const定义的地方对它进行初始化。

const对象和成员函数

如果声明一个成员函数为const,则等于告诉编译器该成员函数可以为一个const对象所调用。一个没有被明确声明为const的成员函数被看成是将要修改对象中数据成员的函数。要理解声明const成员函数的语法,首先注意前面的带const的函数声明,它表示函数的返回值是const,但这不会产生想要的结果。相反,必须把修饰符const放在函数参数表的后面:

int f() const;

int X::f() const {return i;}

因为f()是一个const成员函数,所以不管它试图以何种方式改变i或者调用另一个非const成员函数,编译器都把它标记成一个错误。

内联函数

任何在类中定义的函数自动地成为内联函数,但也可以在非类的函数前面加上inline关键字使之成为内联函数。但为了使之有效,必须使函数体和声明结合在一起。否则,编译器将它作为普通函数对待。因此

inline int plusOne(int x);

没有任何效果,仅仅只是声明函数。成功的方法如下:

inline int plusOne(int x){ return ++x; }

注意,编译器将检查函数参数列表使用是否正确,并返回值。

一般应该把内联定义放在头文件中。当编辑器看到这个定义时,它把函数类型(函数名+返回值)和函数体放在符号表中。使用函数时,编译器检查以调用是正确的且返回值被正确使用,然后将函数调用替换为函数体,因而消除了开销。

9.2.1类内部的内联函数

为了定义内联函数,通常必须在函数定义前面加一个inline关键字。但在类内部定义内联函数时并不是必须的。任何在类内部定义的函数自动地成为内联函数。

因为类内部的内联函数节省了在外部定义成员函数的额外步骤,所以我们一定想在类声明内每一处都使用内联函数。但应记住,使用内联函数的目的是减少函数调用的开销。假如函数较大,由于需要在调用函数的每一处重复复制代码,这样将使代码膨胀,在速度方面获得的好处就会减少。

很明显,小函数作为内联函数工作是理想的,如果函数太复杂,编译器将不能执行内联。这取决于特定的编译器,但对于大多数的编译器这时都会放弃内联方式,这是内联将可能不能提高任何效率。

一般地,任何种类的循环都被认为太复杂而不扩展为内联函数。循环在函数里可能比调用要花费更多的时间。

如果所有的函数都是内联函数,那么使用库就会变得相当简单,不需要进行连接。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: