第一章 致读者
1、给C程序员的建议
(1)在C++里几乎不需要用宏。用const 或enum定义明显的向量,用inline避免函数调用的额外开销,用template去刻画一族函数或者类型,用namespace去避免名字冲突。
(2)不要再你需要变量之间去声明它,以保证你能立即对它初始化。在语句能够出现的所有位置,声明都可以出现,可以出现在for语句的初始化部分,也可以出现在条件中。
(3)不要使用malloc,new运算符能将同样的事情做得更好。对于realloc(),请试一试vector。
(4)试着去避免void*,指针运算、联合和强制转换,除了在某些函数或者类实现的深层之外。在大部分情况下,强制都是设计错误的指示器。如果你必须使用某个显式的类型转换,请设法去用一个新的强制,设法写出一个描述你想做的事情的更精确的语句。
(5)尽量少用数组和C风格的字符串,与传统的C风格相比,使用C++标准库string和vector常常可以简化程序设计。
2、一些在C++编程里的思考
(1)某些概念不容易表示为某个基本类型,也不容易表示成没有关联数据的函数,那么最好的处理方式就是使用一个类去表示这个概念。
一个C++类就是一个类型,它刻画了这个类的对象的对象的行为:它们如何建立、如何操作、以及如何销毁。
写出好的程序,最关键的就是设计这些类,使它们都能很好的表示这些概念。
(2)将相互有关的概念组织到一个树形结构中,使最一般的概念成为树根,在C++里,派生类表示的就是这种结构。
一个程序常常能组织为一组类,或者一组类的有向无环图。这时,程序员刻画一组基类,每个都有它的一组派生类。
虚函数常常能被用于为一个概念的最一般版本(一个基类)定义操作,如果有必要,可以针对特定的类(派生类),对这些操作的解释进行进一步的准确化。
(3)解开依赖图的一种最好的工具就是界面和实现的清晰分离,抽象类是C++处理这些问题的基本工具。
共性的另一种形式可以通过模板表示,一个类模板刻画了一族类。的模板就是这样一种机制,它刻画的就是如何通过给定另一个类作为参数,就可以生成出一个新类。
最常见的模板是容器类,例如表、数组和关联数组,以及使用这些容器的基本算法。
通过使用继承的类型表达一个类及相关函数的参数化通常是个错误,这件事最好是用模板做。
(4)模块程序设计:确定你所需要的哪些模块,将程序分为一些模块,使数据隐藏于模块之中。
(5)用户定义类型:确定你所需要的类型;为每个类型确定一组完整的操作。
(6)构造函数:名字与类名相同的成员函数被称为构造函数。每个构造函数定义了一种初始化这个类的对象的方式。
(7)析构函数:如果某个类的一个对象出了其作用域,需要做某些清理时,就应该去声明构造函数的对应物-----它被成为析构函数
(8)词语virtual在C++里的意思是:可以在今后由这个类所派生的类里重新定义。
(9)为另外一些不同的类提供界面的类也常常被称作多态类型。
Array_stack后的“:public”可以读作:由其派生,实现或者是它的子类型
即类Array_stack是由类Stack派生、Array_stack实现Stack或者Array_stack是Stack的子类型。
(11)让编译器把一个virtual函数的名字转换为指向这些函数的指针表的一个下标。这种表通常被称为“虚函数表”,或简称vtbl。
每个带有虚函数的类都有标识着它的所有虚函数的vtbl
即使调用者并不知道对象的大小以及它的数据布局,位于vtbl的函数也使对象能正确的使用,调用者需要知道的所有东西就是Stack的那个vtbl的位置,以及对各个虚函数应该使用的下标。这种虚函数调用机制的 效率可以做到基本上与“正常函数调用”机制相同。其空间开销是带有虚函数的类的每个对象里包含一个指针,而每个这样的类需要一个vtbl。
(12)继承机制:
首先,描述一个定义了所有形状的共同特征的类:
Shape:所有提供定义的调用界面但其实现却不能定义的函数都是virtual,特别的,函数draw()和rotate只能针对特定形状的语义去定义,所以它们被声明为virtual。
其次,要定义一个特殊的形状,我们必须说明他是一个形状,并描述它的特殊性质,包括虚函数
在C++里,类Circle被称为是由类Shape派生的,而类Shape被称为是类Circle的基类,还可以说派生类继承了它基类的成员。
因此,对基类和派生类的使用通常也被说成是继承。
(13)现在的程序设计范型:
确定你需要的哪些类;为每个类提供完整的一组操作;利用继承去明确的表示共性。
在不存在共性的地方,数据抽象就足够了。在类型之间,能够通过使用继承和虚函数挖掘出的共性的数量,可以看做是面向对象程序设计对于该问题的实用性的检验。
可以特别的将类设计为构造其他类型的基本构件,现存的类也应该检查,看看它们是否表现出某些共性,能够通过一个基类加以利用。
(14)可以将字符堆栈类型推广到一个任意类型的堆栈的类型,方法就是将它做成一个模板template,用一个模板参数取代特定的类型char:
前缀template<class T>说明在此作为前缀的声明中,T将被看做是一个参数。
成员函数可以类似的定义为:
我们也能将表、向量、映射(也就是关联数组)等都定义为模板。一个能够保存某种类型的一集元素的类,一般被称为一个容器类,或简单的称为容器。
模板是一种编译时的机制,因此,与手工编写的代码相比,它们的使用并不引起任何额外的运行时开销。
注:你并不需要在知道了C++所有细节之后才能写出好的C++程序。
第三章 C++标准库概览
没有任何一个重要程序只用某种赤裸裸的程序设计语言就能写出的。
一、引言
1、#include <iostream>指示编译器去包含位于iostream里的标准流I/O功能的声明。
2、标准库名字空间
标准库定义在一个称为std的名字空间里。
使用std::cout,而不是直接写cout的原因,是明确的说明要使用的是标准cout,而不是其他的什么cout。
你必须包含#include适当的头文件,除此之外,你还必须使用std::前缀,或者将出自std的每个名字都做成全局的:
#include<string> //使标准字符串功能可以使用
using namespace std; //使所有std名字都可用,不必加std::前缀
string s = “dksjfl”; //可以:string就是std::string
库iostream为每种内部类型定义了相应的输出方式。进一步说,为用户类型定义一种输出方式也很容易,默认情况下,送到cout的输出值都将被转换为字符的序列。
请注意:字符常量输出为一个字符,而不是一个数值。
void k()
{
cout <<'a';
cout <<'b';
cout <<‘c';
}
输出为abc
void ml()
{
string s1="hello";
string s2="world";
string s3=s1+","+s2+"!\n";
cout<<s3;
}
对于字符串的加法表示的就是拼接。你可以将一个字符串、或者一个字符常量、或者一个字符加到一个字符串上。
最常见的工作就是将某些东西追加到一个字符串的尾部,操作符+=直接支持这种操作。
string s1;
s1+="\n";
运算符>>被用作输入运算符;cin是标准输入流。>>右边运算对象的类型决定了可以接受什么输入,这个运算对象被作为输入操作的目标。
按照默认方式,输入时遇到一个空白字符则结束一次输入。
3、容器:一个以保存一批对象为主要用途的类通常称为一个容器。
为给定的工作提供适当的容器,并提供一些有用的基本操作来支持它们,这些在任何程序的构造过程中都是最重要的步骤。
异常抛出与捕捉:
try{
}
catch(){
}
向量:
4、表-----list
5、映射
对向量的下标操作开销较小且易于操作,而另一方面,在向量的两个元素之间插入一个元素则是代价昂贵的。
list恰好具有与此相反的性质。
map类似于(关键码、值)对偶的表,除此之外他还针对基于关键码去查询值做了特殊的优化。
6、迭代器:
任何类型的迭代器都是某个类型的对象,当然,存在着许多不同迭代器类型,因为每个迭代器都需要保存为在一个特定容器上完成自己工作所需要的信息。
这些迭代器之间 的差别可以像容器一样大,而且都是为某项特定工作专门量身打造的。
忠告:
(1)尽量的使用标准库,但是也不要迷信库
(2)请使用string,而不是char*
(3)如果怀疑,就使用检查区间范围的向量
(4)vector<T>、list<T>、map<key、value>都比T[]好
(5)如果想一个容器中添加一个元素,用push_back()或者back_inserter()
(6)采用对vector的push_back(),而不是对数组的realloc()
(7)在main()中捕捉公共的异常。
2013年6月17日
第一部分
第四章 类型和声明
一、类型
1、整型:布尔量、字符和整数类型放到一起称为整型。
整型和浮点型一起称为算术类型;
枚举类型和类称为用户定义类型,因为他们必须由用户定义出来,而不能事先没有声明就直接使用,而这正是那些基本类型的情况。
与用户定义类型相对应,其他类型都被称为内部类型。
2、布尔量、bool
可以具有两个值true或false之一,布尔量用于表示逻辑运算的结果。
bool型数据最常见的使用是作为检查某些条件是否成立的函数的结果类型。
在算术和逻辑表达式里,bool都将被转为int型,在这种转换之后得到的值上进行各种算术和逻辑运算,如果结果又被转回bool,那么0将转为false,所有非0值都转为true。
指针也可以隐式的转换到bool型,非零指针转为true,具有零值的指针转为false。
3、字符类型
signed char保存的值是-128~127,而unsigned char保存的值是0~255.
注意:字符类型都是整型,可以对它们使用算术和逻辑运算符。
还提供了另一个类型:wchar_t,用于保存更大的字符集里的字符。
4、字符文字量
字符文字量常被称为字符常量,其形式就是用单引号括起的一个字符,字符文字量的类型是char,实际上是一种符号常量,表示在C++程序运行的计算机上的字符集中该字符的整数值。
宽字符文字量的形式是L'ab',这里放在引号间的字符个数及意义由实现根据wchar_t类型确定。
5、浮点文字量
浮点变量之间不能出现空格,65.43 e-14不是一个浮点变量,因为中间出现空格,编译时可能会引起语法错误。
二、声明
1、用于较大的作用域的名字应该是相对比较长的更加明确的名字,然而,如果在比较小的作用域只是用那些短小而熟悉的名字,代码会显得更清晰些。
2、作用域:一个声明将一个名字引进一个作用域,也就是说,这个名字只能在程序正文的一个特定部分内使用。
局部变量作用域从声明处到所在函数的结尾;
全局变量作用域从声明处或定义处到文件末尾;
注:遮蔽某些名字的情况在写大程序时是不可避免的,但是读程序的人很容易没有注意到某个名字已经被遮蔽了,由于这类错误相对不那么常见,它们反而很难发现,因此还是应该尽量避免变量名字遮蔽的情况,对全局变量或者很大的函数里的局部变量,使用像i或者x一类没有任何意义的名字就是自找麻烦。
被遮蔽的全局变量名字可以使用作用域解析运算符::去引用:
int x;
void f2()
{
int x = l;//遮蔽全局变量x
::x = 2;//给全局变量x赋值
x = 2;//给局部变量x赋值
}
没有办法去使用被遮蔽的局部变量。
3、初始化
如果为一个对象提供了初始式,这个初始式将确定对象的初始值,如果没有提供初始式,全局的,名字空间的和局部静态的对象将被自动初始化为0;
但是,局部变量和动态分配的变量将不会用默认值做初始化。
4、对象和左值
对一个名字的某种需要就是它应该表示“存储器里的什么东西”。
一个对象就是存储中一片连续的区域;
左值就是引用某个对象的表达值,术语左值原本是想说“某个可以放在赋值左边的东西”,然而并不是每个左值都能够被用在赋值的左边,左值也可以引用某个常量,例如数组名表示数组的首地址,字符串常量。
没有被声明为常量的左值常常被称为可修改的左值。
5、typedef
如果一个声明以typedef为前缀,它就是为类型声明了一个新名字,而不是声明一个指定类型的对象。
typedef的另一类使用就是对某个类型的直接饮用限制到一个地方。
忠告:
1、保持较小的作用域:我可不可以理解为尽量少的用全局变量,能够传递参数就传递参数,除非能够极大的简化程序。
2、不要在一个作用域和它外围的作用域采用同样的名字,这样做的目的是为了避免变量的遮蔽
3、在一个声明中只声明一个变量
4、让常用和局部的名字比较短,让不常用和全局的名字比较长
5、避免看起来类似的名字。
6、维持某种统一的命名风格;
7、仔细选择名字,反映其意义而不是反映实现方式,这句话不大懂?
8、如果所用的内部类型表示某种可能变化的值,请用typedef为它定义为一个有意义的名字。
9、用typedef卫类型定义同义词,用枚举或类去定义新类型。
10、
11、
12、
13、优先使用普通的int而不是short int或者long int。
14、优先使用double而不是float或者long double
15、优先使用char而不是 signed char 或者unsigned char
16、避免做出有关对象大小的不必要假设
17、避免做出有关对象大小的不必要假设
18、避免无符号算术,还有无符号的比较
19、应该带着疑问去看待signed 到unsigned,或者从unsigned到signed的转换,这是因为转换的结果一般会让你大吃一惊,因为范围不同
20、应该带着疑问去看待浮点到整数的转换;
21、应该带着疑问去看待向较小类型的转换,如将int型转换到char,因为这是截断
第五章 指针、数组和结构
1、一个bool量最少也要占据像char那么大的空间,即一个字节。
2、0可用于各种标准转换,0可以被用于作为任意整型、浮点类型、指针、还有指向成员的指针的常量。0的类型将由上下文确定。典型情况下,0将被表示为一个适当大小的全零二进制位的模式。
3、由于C++收紧的类型检查规则,采用普通的0而不是一些人建议的NULL宏,带来的问题会更少一些,如果你觉得必须定义NULL,请采用
const int NULL=0;
用const限定词是为了防止无意的重新定义NULL,并保证NULL可以用到那些要求常量的地方。
4、数组:当使用需要变化的界,那么请用vector。
5、字符串常量:
sizeof(“bohr”) = 5;
strlen(“bohr”)= 4;
6、引用
一个引用就是某对象的另一个名字,引用的主要用途是为了描述函数的参数和返回值,特别是为了运算符的重载,记法:
X&表示到X的引用。
为了确保一个引用总能是某个东西的名字,我们必须对饮用做初始化。
对一个引用的初始化是与对它赋值完全不同的另一件事情,除了外表形式之外,实际上根本就没有能操作引用的运算符操作。
int& rr=ii;
一个引用的值在初始化之后就不可能改变了,它总是引用它的初始化所指称的那个对象,要取得所引用对象的地址,可以写成&rr
引用的一种最明显的实现方式是作为一个常量指针,在每次使用它时都自动做间接访问,将引用想象成这种样子不会有任何问题,但要记住的是,一个引用并不是一个对象,
不能像指针那样去操作。
需要区分对变量的引用和对常量的引用,是因为在变量引用的情况下引进临时量极易出错,对变量的赋值将会变成对于临时量(即将消失的)的赋值;对常量的引用则不会有这类问题,以常量的引用作为函数参数经常很重要。
可以通过引用来描述一个函数参数,已使该函数能够改变传递来的变量的值;参数传递的语义通过相应的初始化定义。
为了提高程序的可读性,通常应该尽可能避免让函数去修改它们的参数;相反,你应该让函数明确的返回一个值。
7、指向void的指针
一个指向任何对象类型的指针都可以赋值给类型为void*的变量,void*可以赋值给另一个void*,两个void*可以比较相等与否,而且可以显式的将void*转换到另一个类型,要使用void*,我们就必须显式的将它转换到某个指向特定类型的指针。
void*最重要的用途是需要向函数传递一个指针,而又不能对对象的类型做任何假设;
还有就是从函数返回一个无类型的对象,要使用这样的对象,必须通过显式的类型转换。
8、结构
数组时相同类型的元素的一个聚集,一个struct则是任意类型元素的一个聚集;
在完整声明被看见之前,不能去声明这个结构类型的新对象:
struct no{
no member;
};//这是错误的,错误的递归定义,因为编译时无法确定no的大小。
要想允许两个结构类型相互引用,我们可以先将一个名字声明为结构的名字:
struct list;
struct link{
link* pre;
link* suc;
list* mem_of;
};
struct list{
struct link*head;
};
忠告:
1、避免非平凡的指针算术
2、当心,不要超出数组的界限去读写
3、尽量使用0而不是NULL;
4、尽量使用vector和valarray而不是内部的数组,我连什么是vector都不知道
5、尽量使用string而不是以0结尾的char数组
6、尽量少用普通的引用参数
7、避免void*,除了在某些低级代码里,?什么叫低级代码?
8、避免在代码中使用非平凡的文字量,相反,应该定义和使用各种符号常量。
2013年6月18日
第六章 表达式和语句
一、表达式
1、按照默认方式,运算符>>将跳过空白字符(空格、制表符、换行符),如果操作失败还将保持ch不变。
标准库函数isspace()提供对空白字符的标准检测。
isdigit():检测一个字符是否数字
isalpha():检测一个字符是否字母
isalnum():检测一个字符是否数字或字母
2、运算符
class_name表示一个类的名字,member表示成员的名字,object表示一个能产生类对象的表达式,
pointer是一个产生指针的表达式,expr是表达式,而lvalue是一个表示非常量对象的表达式;
其中type如果出现在括号里,它就可以是一个一般性的类型名,否则对type的使用就存在着某些限制。
int *pp = &(x > y ? x : y);
较大的那个int的地址。
在一个表达式里,子表达式的求值顺序是没有定义的,特别是假定表达式从左到右求值。
int x = f(y)+g(y);这种函数调用方式很糟糕,因为如果f()和g()同时调用同一个全局变量,这样就会可能产生错误。
运算符:,(逗号)、&&(逻辑与)、||(逻辑或)保证了它们左边的运算对象一定在右边运算对象之前求值。
4、存储
由new创建的对象将一直存在,知道它被delete显式的销毁为止,在此之后,new原先分配的空间又可以被new重新分配,由于C++实现并不保证提供一个“废料收集器”,由它去找回不再存在引用的对象并使它们占用的存储重新可以被new使用,因此,所有通过new建立的对象都需要手工的用delete释放。
也可以用new创建对象的数组;
char *save_string(const char*p)
{
char *s = new char[strlen(p) + 1];
strcpy(s,p);//由p复制到s
return s;
}
int main(int argc, char *argv[])
{
if(argc < 2 )
exit(1);
char *p = save_string(argv[1]);
......
delete[] p;
}
简单的delete运算符只能用于删除当个的对象,删除数组需要用delete[]。
为了释放由new分配的空间,delete和delete[]必须能够确定对象分配的大小,这就意味着通过标准实现的new分配的对象将占用比静态对象稍微大一点的空间,在典型情况下需要用一个机器字保存对象的大小。
请注意,一个vector是一个普通的对象,因此可以用简单的new和delete分配和释放。
vector<int> *p= new vector<int>(n);单个对象
int *q = new int [n]; //数组
delete p;
delete[] q;
delete[]运算符只能应用于由new返回的指向数组的指针或者0,应用到0不会产生任何影响。
5、存储耗尽
当运算符new需要为某个对象分配空间时,它将调用operator new()去分配适当数量的字节。
与此类似,当运算符new需要为一个数组分配空间时,就去调用operator new[]()。
operator new()和operator new[]()的标准实现没有对返回的存储进行初始化。
当new无法找到需要分配的空间时会发生什么?
按照默认的方式,这个分配函数将抛出一个bad_alloc异常。
void f()
{
try{
for(;;) new char[10000];
}
catch(bad_alloc) {
cerr<< "memory exhausted!\n"
}
}
new_handler有可能做出某些比简单终止程序更聪明的事情。
通过new_handler,就处理了在程序中所有new的常规使用中对存储耗尽情况的检查。
6、switch语句中的每个case必须以某种方式终止,除非你希望让直行直接进入下一个case中,最常见的终止方式是加break语句,或者加return语句。
7、在条件中的声明
为了避免意外的错误使用变量,在最小的作用域里引进变量是一个很好的想法,特别的,最好是把局部变量的定义到一直推迟到可以给他初始值时再去做。采用这种方式,就不会出现因为未使用未初始化的变量而造成的麻烦了。
在条件中声明变量,除了逻辑上的优点之外,还能产生出更紧凑的源代码。
8、注释
为每一个源文件写一个注释,一般性的描述在他里面有哪些声明,以及对有关手册的引用,为维护而提供的一般性提示
对每个类、模板和名字空间写一个注释
对每个非平凡的函数写一个注释,陈述其用途,所用的算法以及可能的有的关于它对环境所做的假设
对每个全局变量和名字空间的变量和常量写一个注释。
在非明显和/或不可移植的代码处的少量注释。
极少其他的东西
忠告:
1、应尽可能使用标准库,而不是其他的库和手工打造的代码
2、避免过于复杂的表达式
3、如果对运算符的优先级有疑问,加括号:这是我现在经常干的事
4、避免显式类型转换:有时候不得不这样做啊,例如,有些函数为了兼容各种参数类型,声明为void类型,这时候返回值就不得不进行强制类型转换了
5、若必须做显式类型转换,提倡使用特殊强制运算符,而不是C风格的强制?什么才叫C++的特殊强制运算符?
6、避免带有无意义求值顺序的表达式
7、避免goto语句。呃呃呃,这个以后尽量避免吧
8、避免都语句
9、在初始化某个变量之前不要声明它
10、保持一致的缩进编排风格。这个为了美观和看代码的方便
11、倾向于定义一个成员函数operator new()去取代全局operator new()
12、在读输入的时候,总应该考虑病态形式的输入。啥叫病态形式的输入?
第七章 函数
1、函数声明
在一个引用参数的声明中没有const,就应该认为,这是说明该参数将被修改:
使用const参数的重要性将随着程序的规模增大而进一步增长;
请注意,参数传递的语义不同于赋值的语义;对于const参数、引用参数和某些用户定义类型的参数,这一点都非常重要。?什么意思
如果将数组作为函数的参数,传递的就是到数组的首元素的指针。
C风格的字符串是以0结束的,它们的大小就很容易计算
2、重载函数名
对同一个名字用于在不同类型上操作的函数的情况称为重载。例如,对于加法只存在一个名字:+,然而它却可以用于做整数、浮点数和指针值的加法。这种思想很容易扩充到函数。
当一个函数f被调用时,编译器就必须弄清究竟应该调用名字为f的函数中的哪一个;为了完成这项工作,它需要将实际参数类型与所有名字为f的函数的形式参数的类型相比较,基本想法是去调用其中在参数上匹配最好的函数,如果不存在匹配最好的函数,就给出一个编译错误。
判断准则:
(1)准确匹配;也就是说,无须任何转换或者只须做平凡转换:例如,数组名到指针,函数名到函数指针
(2)利用提升的匹配;即包括整数提升(bool到int,char到int,short到int以及它们的无符号版本)以及从float到double的提升
(3)利用标准转换的匹配:int到double,double到int,double到long double,derived*到base*,T*到void*,int到unsigned int的匹配
(4)利用用户定义转换的匹配。
(5)利用在函数声明中的省略号的匹配
注:如果在某个最高层次上同时发现两个匹配,这个调用将作为存在歧义而被拒绝。
重载解析和被考虑的函数声明的顺序无关。
相对而言,重载所依赖的这组规则是比较复杂的,但程序员却很少会对哪个函数被调用感到吃惊。
重载将不考虑返回类型,这样规定的理由就是要保持重载的解析只是针对单独的运算符或者函数调用,与调用的环境无关。
在不同的非名字空间作用域声明的函数不算是重载。
3、对于一个函数只能做两件事:调用它,或者取得它的地址。
const 、inline、template、enum和namespace机制都是为了用作预处理器结构的许多传统使用方式的替代品。
忠告:
1、质疑那些非const的引用参数,如果你想要一个函数去修改其参数,请使用指针或者返回值
2、如果你需要尽可能减少参数赋值时,应该使用const引用参数
3、广泛而一致的使用const
4、避免宏
5、避免不确定数目的参数
6、不要返回局部变量的指针或者引用:因为局部变量的作用域就只在该函数中,该函数执行完毕后就会自动销毁
7、当一些函数在不同的类型执行概念上相同工作时,请使用重载
8、在各种整数重载时,通过提供函数去消除常见的歧义性;
9、在考虑使用指向函数的指针时,请考虑虚函数或模板是不是更好的选择。
10、如果你必须使用宏,清使用带有许多大写字母的丑陋的名字
第八章 名字空间和异常
一、名字空间
1、名字空间是一个描述逻辑分组的机制,也就是说,如果有一些声明按照某种准则在逻辑上属于同一个集团,就可以将它们放入同一个名字空间,以表明这个事实。
一个名字空间的成员必须采用如下的记法形式引入:
namespace namespace-name{
//声明和定义
}
我们不能在名字空间定义之外加限定的语法形式为名字空间引进新成员:
void Parser::logical(bool); //错误:Parser里没有logical()
这里的想法就是为了更容易找到一个名字空间里的所有名字,也能捕捉到拼写或者类型不匹配一类的错误。
一个名字空间也是一个作用域,这样,“名字空间”就是一个最基本的相对简单的概念,一个程序越大,通过名字空间去描述其中逻辑上独立的各个部分也就越重要,常规的局部作用域、全局作用域和类也是名字空间。
理想的情况是,程序里的每个实体都属于某个可以识别的逻辑单位,所以,在理想情况下,一个非平凡的程序里的每个声明都应该位于某个名字空间里,以此指明它在程序中所扮演的逻辑角色。
2、因为名字空间是作用域,所以普通的作用域规则也对名字空间成立。
如果一个名字先前在本名字空间里或者其外围作用域里声明过,它就可以直接使用了,不必再进一步为它操心,也可以使用来自另一个名字空间的名字,但需要该名字所属的名字空间作为限定词。
第一行的Parser::限定词是必须的,用以说明term()就是在Parser里所声明的那一个,而不是别的什么不相干的全局函数,由于term是Parser的成员,因此就不需要再为prim()使用限定词了;但是如果没写Lexer限定词,curr_tok将会被认为是没有声明的,因为名字空间Lexer的成员的作用域不包含名字空间Parser。
当某个名字在它自己的名字空间之外频繁使用时,在反复写它的时候都要加上有关名字空间作为限定词,这样做令人厌烦:
这种多余的东西可以通过一个使用声明而清除掉,只需要在某个地方说明,在这个作用域里所用的变量。
全局性的使用指令是一种完成转变的工具,在其他方面最好避免使用;在一个名字空间里的使用指令是一种名字空间的组合工具,在一个函数里可以安全的使用指令作为一个方便的记忆方式。
名字空间就是为了表示逻辑结构,最简单的这类结构就是分清楚由一个人写的代码与另一个人写的代码,这种简单的划分也可能具有极大的实际重要性。
有时,将一组声明包裹在一个名字空间里就是为了避免可能的名字冲突,这一做法经常是很有价值的,这样做的目的只是为了保持代码的局部性,而不是为用户提供界面。
无名字空间有一个隐含的使用指令:
namespace {
......
}
等价于
namespace $$${
......
}
当一个类的成员调用一个命名函数时,函数查找时应当偏向于同一个类及其基类的其他成员,而不是基于其他参数的类型可能发现的函数。
名字空间别名:
//为名字空间提供较短的别名:
namespace ATT = American_Telephone_and_Telegraph;
全局性的使用指令是一种完成转变的工具,在其他方面最好避免使用,在一个名字空间里使用指令一种名字空间的组合工具,在一个函数里,可以安全的将使用指令作为一种方便的记法方式。
3、多重界面
parser的两个名字空间定义可以共存,这就使各种定义可以被用到最合适的地方;名字空间parser被用于提供两种东西:
(1)实现分析器的所有函数的一个公共环境
(2)分析器提供给用户的一个外部界面
名字空间为用户提供最小界面;而名字空间为实现所提供的界面比为用户提供的界面更大一些,如果这个界面针对的是现实程序里的一个 现实规模的模块,那么通常也会比用户能够看到的界面变化更频繁些,将模块里的用户与这些变化隔离开是非常重要的。
我们并不需要两个相互独立的名字空间来描述这两个不同的界面。
请记住:这里介绍的解决方案是各种解中最简单的,也是最好的;它的主要弱点是两个界面没有采用不同的名字,编译器不一定有足够的信息去检查两个名字空间定义的移植性;当然,即使编译器未必总有机会去检查这种一致性,它通常还是会这样做,进一步说,连接系统将能捕捉到编译器遗漏的大部分错误。
界面的作用就是尽可能的减小程序不同部分之间的相互依赖,最小的界面将会使程序易于理解,有更好的数据隐蔽性质,易于修改,也编译的更快。
4、避免名字冲突
名字空间就是为了表示逻辑结构,最简单的这类结构就是分清楚一个人写的代码与另一个人写的代码,这种简单划分也可能具有极大的实际重要性。
按照理想情况:一个名字空间应该:
(1)描述一个具有逻辑统一性的特征集合
(2)不为用户提供无关特征的访问
(3)不给用户强加任何明显的记述负担
将组合(通过使用指令)和选择(通过使用声明)结合起来能产生更多的灵活性,既能提供对许多机制的访问,又能消除由于组合而产生的名字冲突或者歧义性。
重载可以跨名字空间工作,对于我们能以最小的代价将现存的库修改为使用名字空间的东西而言,这种特征是必不可少的:
名字空间是开放的;也就是说,你可以通过多个名字空间声明给它加入名字。
namespace A{
int f();//现在A有成员f()
}
namespace A{
int g();//现在A有成员f()和g()
}
通过这种方式,我们就能支持将一个名字空间中放入几个大的程序片段,其方式就像老的库和应用存在于同一个全局名字空间那样;为做到这些,我们必须允许一个名字空间的定义分不到多个头文件和源代码文件里。
名字空间的开放性使我们可以通过展示名字空间不同部分的方式,为不同种类的用户提供不同的界面。
在定义一个名字空间里已经声明过的成员时,更安全的方式是采用Mine::语法形式,而不是重新打开Mine:
void Mine::ff()//错误,在Mine里没有声明ff()
{
}
编译器可以捕捉到上面的错误,但是,由于可以在一个名字空间里可以定义为新函数,编译器就无法捕捉在重新打开的名字空间里出现的同样错误。
names Mine{//重新打开Mine以定义函数
void ff()//在Mine里没有声明ff(),这个定义将ff()加进了Mine
{
}
}
二、异常
1、当一个程序是由一些分离的模块组成时,特别是当这些模块来自某些独立开发的库时,错误处理的工作就需要分成两个相互独立的部分:
(1)一方报告出那些无法在局部解决的错误
(2)另一方处理那些在其他地方检查出的错误
2、异常机制是C++中用于将错误报告与错误处理分离开的手段。
提供异常概念就是为了帮组处理错误的报告。
基本思想:如果函数发现了一个自己无法处理的问题,它就抛出(throw)一个异常,希望它的调用者能够处理这个问题;如果一个函数想处理某个问题,它就可以说明自己要捕捉(catch)用于报告该种问题的异常。
程序结构:
catch(){
//.................
}
称为一个异常处理器,它的使用只能紧跟着由try关键字作为前缀的块之后,或者紧跟着另一个异常处理之后,catch也是一个关键字。在括号里包含着一个声明,其使用方式类似于函数参数的声明;也就是说,该声明描述的是这个处理器要捕捉的对象的类型,并可以为所捕捉的对象命名。
如果在一个try块里的任何代码---或者由那里调用 的东西-----抛出了一个异常,那就会逐个检查这个try块后面的异常处理器;如果所抛出的异常属于某个异常处理器所描述的类型,这个处理器就会被执行;如果try块未抛出异常,这些异常处理器都将被忽略,使该try块的活动方式就像是普通的块;如果抛出了一个异常,而又没有try块去捕捉它,整个程序就将终止。
简而言之,C++异常处理就是一种从一个函数里将控制传递到有意确定的一些代码的方式。如果需要,也可以同时把关于错误的某些信息传递给调用者。C程序员可以将异常处理想象为一种取代setjmp/longjmp的、具有良好行为方式的机制。
3、异常的辨识
处理器语句看起来像一个开关语句,但是不需要break;处理器列表在语法上与case列表不同,部分原因也就在于此,另一个原因指明每一个异常树立起都是一个作用域。
一个函数不必捕捉所有可能的异常。
(1)一般说,使处理错误的代码与正常代码代码分离是一种很好的策略。
(2)有导致错误的代码的同一个抽象层次上处理错误是非常危险的。完成错误处理的代码有可能又产生了引起错误处理的那个错误。
(3)修改正常的代码,加上错误处理代码,所需要的工作量比增加单独的错误处理例行程序更多。
异常处理的意图是去处理非局部的问题,如果一个错误可以在局部处理,那么总应该这样做。
忠告:
1、用名字空间表示逻辑结构
2、将每个非局部的名字放入某个名字空间,除了main()之外;这样对于代码编写有好处
3、名字空间的设计应该让你能很方便的使用它,又不会意外的访问其他的无关名字空间
4、避免对名字空间使用很短的名字
5、如果需要,通过名字空间别名去缓和长名字空间名的影响。
6、避免给你的名字空间的用户添加太大的记法负担
7、在定义名字空间的成员时使用namespace::member的形式
8、只在转换时,或者在局部作用域里,采用using namespace
9、利用异常去松弛“错误”处理代码和正常处理代码之间的联系
10、采用用户定义类型作为异常,不用内部类型。???????
11、当局部控制结构足以应付问题时,不要用异常。
第九章 源文件和程序
一、源文件
1、文件是传统的存储单位和传统的编译单位。在典型情况下,标准库和操作系统的代码都不是以源程序形式提供的,不能作为用户 程序的一部分。
2、#Include机制是一种正文操作的概念,用于江源程序片段收集到一起,形成一个提供给编译的单位--文件。
注意:在<>和“”中的内部空格,所以注意不要加空格
3、在一个程序中,任意一个类、枚举和模板等必须只定义唯一的一次。
ORD:一个类、模板或者inline函数的两个定义能够被接受为同一个唯一定义的实例。
在互相分离的编译单位里检查和抵御不一致的类定义,这件事情超出了许多实现能力的范围,因此,违背ODR的声明就可能称为难以琢磨的错误的根源。
4、extern “C”中的C表示一种连接约定,而不是一种语言。
extern “C”指令描述的只是一种连接约定,并不影响调用函数的语义;特别的,声明为extern “C”的函数仍然要遵守C++的类型检查和参数转换规则,而不是C的较弱的规则。
任何声明都可以出现在连接块里;应当特别指出,变量的作用域及存储类都不会受到影响。
5、头文件
为避免头文件的重复包含,可以在头文件的开始使用头文件包含保护符
6、对于不同编译单位的全局变量,其初始化的顺序则没有任何保证,因此,对于不同的编译单位里的全局变量,在它们的初始化之前建立任何顺序的依赖都是很不明智的,此外也没有办法捕捉由全局变量的初始化抛出的异常。一般,最好是尽量减少全局变量的使用,特别是限制使用那些要求复杂初始化的全局变量。
特别饿,动态链接库与具有复杂依赖关系的全局变量也无法很好的共存。???????为啥捏????
7、程序终止:
一个程序可能以多种方式终止:
通过main()返回;
通过exit()返回
通过调用abort();
通过抛出一个未被捕捉的异常;
还有各种各样病态的或者依赖于实现的使程序垮台的方式。
如果一个程序利用标准库函数exit()终止,所有已经构造起来的静态对象的析构函数都将被调用;
然而,如果程序使用标准库函数abort()而终止,那么析构函数就不会被调用;
请注意:这意味exit()并不立即终止程序。在析构函数调用exit()有可能导致无穷递归。
exit()的类型是
void exit(int)
和main()的返回值一样,exit()的参数也将被作为程序的值返回给“系统”,用0指明程序成功结束。
调用exit()结束程序,意味着调用它的函数及其调用者里的局部变量的析构函数都不会执行。抛出一个异常并捕捉它则能保证局部变量被正确的销毁。此外,调用exit()将终止程序,不会给调用exit()函数的调用者留下处理问题的机会;因此,最好是通过抛出异常以脱离一个环境,让异常处理器决定下面应该做些什么。
C和C++标准库函数atexit()使我们让程序在终止前执行一些代码:
void my_cleanup()
void somewhere()
{
if(atexit(&my_cleanup) == 0)
{
//在正常终止时将调用my_cleanup()
}
else{
//
}
}
注意:提供给atexit()的参数函数不能有参数也不能由返回值。
这里存在着一个由实现确定的atexit()函数的最大数目,atexit()通过返回非0值表明达到了最大限制;这些约束情况也使atexit()不如它初看起来那么有用。
在atexit(f)调用之前,静态分配的对象的析构函数将在f的调用之后被调用。在一个atexit(f)调用之后,建立的这种对象的析构函数将在f的调用前被调用。
忠告:
1、利用头文件表示界面和强调逻辑结构
2、用#include将头文件包含到实现有关功能的源文件里
3、不要在不同编译单位里定义具有同样名字,意义类似但有不同的全局实体
4、避免在头文件里定义非inline函数
5、只在全局作用域或名字空间里使用#include。。什么意思
6、只用#include包含完整的定义
7、使用包含保护符
8、用#include将C头文件包含到名字空间,以避免全名字
9、将头文件做成自给自足
10、区分用户界面和实现界面;就好比头文件可以看成用户界面,具体的函数可以看成实现界面
11、区分一般用户界面和专家用户界面
12、在有意向用于非C++程序组成部分的代码中,应避免需要运行时初始化的非局部对象
2013年6月20号
第二部分 抽象机制
第10章 类
一、类
1、一个类就是一个用户定义类型,设计一个新类型,是为了给内部类型中没有定义的的概念提供一个定义。
定义新类型的基本思想:将类型的不重要的性质(例如用于存储该类型对象所采用的数据的布局)和类型至关重要的性质(能够访问其中数据的完整的函数列表)区分开来。
表达这种划分的最好方式就是提供一个特定的界面,令对于数据结构以及内部维护例程的所有使用都通过这个界面进行。
2、一个类里声明的函数被称为成员函数,这种函数只能通过适当类型的特定变量,采用标准的结构成员访问语法形式调用。
由于在不同的结构里可能存在具有同样名字的成员函数,所以在定义成员函数时就必须给出有关结构的名字。
在成员函数里面,结构内部的各个成员的名字可以直接饮用,不必显式的去引用某个对象,在这种情况下,这些名字所引用的就是该函数调用时所针对的那个对象的成员。
类的成员函数总知道它是为哪个对象而调用的。
class X{..........}被称为一个类定义,因为它定义了一个新类型,一个类定义也常被说成一个类声明;可以由于#Include在不同的源文件中重复出现。
标号public将这个类的体分为两部分,其中第一部分(私用部分)只能由成员函数使用;而第二部分(公用部分)则构成了该类对象的公用界面;一个struct也是一个class,但是其成员的默认方式是公用的。
3、构造函数
(1) 采用init()一类的函数提供类对象的初始化,这样做既不优美又容易出错,因为没有任何地方说一个对象必须经过初始化,程序员有可能忘记去做这件事情------或者做了两次;一种更好的途径是让程序员有能力去声明一个函数,其明确目的就是去完成对象的初始化;因为这样一个函数将构造起一个给定类型的值,它就被称为构造函数。
构造函数具有与类同样的名字。
如果一个类有一个构造函数,这个类型的所有对象的初始化都将通过对某个构造函数的调用而完成初始化,如果该构造函数要求参数,那么必须提供这些参数。
(2)
允许以多种方式初始化类对象也可以,并且很好,可以通过提供多个构造函数的方式完成。
构造函数也服从与其他函数完全一样的重载规则;只要构造函数在参数类型上存在足够大的差异,编译器就能为每个使用选出一个正确的。
4、静态成员
减少相关函数的一种方式是采用默认参数;
如果一个变量是类的一部分,却不是该类的各个对象的一部分,它就称为是一个static静态成员;
一个static成员只有唯一的一份副本,而不像常规的非static成员那样在每个对象里各有一份副本;
一个需要访问类成员,然而却并不需要针对特定对象去调用的函数,也被称为一个static成员函数。
静态成员也可以像任何其他成员一样引用,此外,对于静态成员的引用不必提到任何对象,相反,在这里应该给静态成员的名字加上了作为限定词的类名字:
class Date{
int d,m,y;
static Date default_date;
public:
Date(int dd=0,int mm=0,int yy=0);
.........
static void set_default(int,int,int);
};
void f()
{
Date::set_default(4,5,1945);
}
静态成员--包括函数和数据成员----都必须在某个地方另行定义。
Date Date::default_date(16,12,1770);
void Date::set_default(int d,int m,int y)
{
Date::default_date = Date(d,m,y);
}
5、类对象的复制
可以用复制同一个类的对象来对该类的其他对象进行初始化,即使是声明了构造函数的地方,也可以这样做。类对象的复制,就是对各个成员的复制。
6、常量成员函数
对于const或者非const对象都可以调用const成员函数,而非const成员函数则只能对非const对象调用。
7、自引用
对于一组相关的更新函数,可以让它们返回一个对被更新对象的引用,以使对于对象的操作可以串联起来。
表达式*this引用的就是这个函数的这次调用所针对的那个对象。
在非静态的成员函数里,关键字this是一个指针,指向该函数本次调用所针对的对象。
this并不是一个常规变量,不能取得this的地址或者给它赋值。在类X的const成员函数里,this的类型是const X*,以防止对于这个对象本身的修改。
大部分对于this的应用都是隐含的,特别是,对一个类中非静态成员的引用都要依靠隐式的使用this,以获取相应对象的成员。
this的一种常见的显式使用时再链接表的操作中。
8、物理和逻辑的常量性
逻辑常量性:偶然有这种情况,一个成员函数在逻辑上是const,但它却仍然需要改变某个成员的值,对于用户而言,这个函数看似没有改变其对象的状态,然而,它却有可能更新了某些用户不能直接访问的细节,这通常称为逻辑常量性;
Date *th = const_cast<Date*>(this);//强制去掉const
这里用const_cast运算符从this获得一个Date*指针
显式类型转换---强制去掉const,以及由它引起的依赖于实现的行为还是可以避免的,只要将缓存管理所涉及的数据声明为mutable。
存储描述符mutable特别说明这个成员需要以一种允许更新的方式存储----即使它是某个const对象的成员;换言之,mutable意味着不可能是const,这种机制可用于简化
string_rep()的定义
string Date::string_rep()const
{
if(!cache_valid){
compute_cache_value();
cache_valid =true;
}
return cache;
}
这就使string_rep()的合理使用都能合法了;例如,
Date d3;
const Date d4;
string s3 = d3.string_rep();
string s4=d4.string_rep();
如果在某个表示中(只有)一部分允许改变,将这些成员声明为mutable是最合适的。如果一个对象在逻辑上保持const的同时,其中大部分需要修改,那么最好将这些需要改变的数据放入另一个独立的对象里,并间接访问它。假设采用这种技术,使用缓存的字符串将变成:
9、结构和类
按照定义,一个struct也是一个类,但其成员默认为公用的;也就说:
struct s{
.................
}
只是下面定义的简写形式
class s{
public:..........................
}
可以用访问描述符private:说明紧随其后的一些成员都是私用的,就像public:说明随他之后的成员是公用一样。除了名字不同之外,下面两个声明完全等价:
class Datel{
int d,m,y;
public:
Detel();
void add_year();
};
struct Date2{
private:
int d,m,y;
public:
Date2();
void add_year();
};
访问描述符可以在一个类声明中多次使用,容易使代码趋向混乱。
10、在类内部的函数定义
在类内部定义的成员函数应该是最小的、频繁使用的函数。
二、高效的用户定义类型
1、简单而世俗的东西,从统计上看,远比复杂而精妙的东西重要的多:
对于用户定义类型,这样的一组操作很典型:
(1)一个构造函数描述这个类型的对象/变量应该如何初始化。
(2)一组函数使用户可以查看Date。这些函数标记为const,表示在对对象/变量调用时,这些函数不会修改对象的状态。
(3)一组函数使用户可以操作Date,又不必知道其细节表示,也不必直接摆弄其中复杂的语义。
(4)一组隐式定义的函数,使Date可以*的复制。
(5)一个类,Bad_date,用于通过异常报告出错的情况。
2、协助函数
一个类有一批与它相关联的函数,而它们又不是必须在类里定义,因为它们并不需要直接访问有关的表示。
传统的关联方式是将声明直接与类的声明放在同一个文件里,那些需要类的用户在包含了定义它的界面文件时,也就是这些函数都可以使用了。
3、重载运算符
?????????
4、具体类型的意义
三、对象
1、构造函数完成对象的初始化,它建立起一种成员函数将在其中进行操作的环境。
析构函数,通常完成一些清理和释放资源的工作。
析构函数最常见的用途是为了释放构造函数请求的存储空间,考虑一个以某种类型Name为元素的简单的表Table;Table的构造函数必须为保存其元素分配存储。而到最后,当这种表被删除的时候,我们必须保证这些存储能收回,以便将来能再用。
class Name{
const char *s;
//...............
}
class Table{
Name *p;
size_t sz;
public:
Table(size_t s=15){ p = new Name[sz = s];}//构造函数
~Table(){delete[] p;} //析构函数
Name *lookup(const char*);
bool insert(Name*);
};
析构函数采用球补的符号作为提示,以表明析构函数与构造函数之间的关系。
一对相互匹配的构造函数/析构函数时在C++里实现大小可以变化的概念的常用机制。
2、默认构造函数
默认构造函数就是调用时不必提供参数的构造函数。如果用户自己声明一个默认构造函数,那么就会去使用它;否则,如果有必要,而且用户没有声明其他构造函数,编译器就会设法去生成一个;编译器生成的默认构造函数将隐式的为类类型的成员和它的基类调用有关的默认构造函数。
由于const和引用必须进行初始化,包含const或引用成员的类就不能进行默认构造,除非程序员提供默认构造函数。
3、构造和析构
(1)一个命名的自动对象,声明时则建立,离开程序块时择校会。
(2)一个*存储对象,通过new运算符建立,通过delete运算符销毁
(3)一个非静态成员对象,作为另一个类对象的成员,在它作为成员的那个对象建立或销毁时,它也随之被建立或者销毁。
(4)一个数组元素,在它作为元素的那个数组被建立或销毁时建立或销毁;?????
(5)一个局部静态对象,在程序执行中第一次遇到它的声明时建立一次,在程序终止时销毁一次。
(6)一个全局对象、名字空间的对象、类的静态对象,它们只在程序开始时建立一次,在程序终止时销毁一次。
(7)一个临时对象,作为表达式求值的一部分被建立,在表达式的最后被销毁
(8)一个再分配操作中所提供的参数控制,在通过用户提供的函数获得的存储里放置的对象。
(9)一个union成员,它不能有构造函数和析构函数。
5、局部变量
对一个局部变量的构造函数,将在控制线程每次通过该变量的声明时执行,每次当控制离开该局部变量所在块时,就会去执行它的析构函数;一组局部变量的 析构函数将按照它们的相反顺序执行。
6、对象的复制
对于包含了由析构函数/构造函数管理的资源的对象而言,按成员复制的语义通常是不正确的;
对于多次复制来说,某些成员可能会经历多次析构函数的析构,所以会导致结果是无定义的;
解决方式就是将Table复制的意义定义清楚;
复制构造函数与复制赋值通常都很不一样,究其根本原因,复制构造函数是去完成对未初始化的存储区的初始化,而复制赋值运算符则必须正确处理一个结构良好的对象。
7、类对象作为成员
成员的构造函数将在容器类本身的构造函数的体执行之前首先被执行。这些构造函数按照成员在类中声明的顺序执行,而不是按这些成员在初始式表中出现的顺序,为了避免混乱,最好还是按照各成员的声明顺序描述浙西初始式。在类本身的析构函数体执行之后,各成员的析构函数将按照与构造相反的顺序被逐个调用。
对于那些没有默认构造函数的类的成员对象,对于那些const成员和引用成员而言,对成员的初始式都是必不可少的。
对于那些不是new分配的数组,销毁将隐式的完成;否则则对该数组中构造起的各个元素调用析构函数;
必须明确说明要删除的是数组还是单个对象。
8、非局部存储
全局变量、名字空间的变量、以及各个类的static变量:在main()被**之前完成初始化,对于已经构造起来的这些变量,其析构函数将在退出main()之后调用。
new(buf)X这种语法形式称为放置语法,每个new总以对象的大小作为其第一个参数,而被分配对象的大小是隐式提供的。
9、联合
联合可以有成员函数,但却不能有静态成员。
四、忠告
1、用类表示概念
2、直降public数据用在仅是数据却并不存在不变式的地方
3、一个具体类型属于最简单的类;如果适用的话,就应该尽可能使用具体类型,而不要采用更复杂的类,也不要用简单的数据结构。
4、只将那些需要直接访问类的表示的函数作为成员函数。
5、采用名字空间,使类与其协助函数之间的关系更明确
6、将那些不修改对象值的成员函数做成const成员函数。
7、将那些需要访问类的表示,但无需针对特定对象调用的成员函数做成static成员函数。
8、通过构造函数建立类的不变式。
9、故国构造函数申请某种资源,析构函数就应该释放这一资源。
10、如果在一个类里由指针成员,它就需要复制操作,包括复制构造函数和复制赋值
11、如果在一个类里有引用成员,它可能需要有复制操作
12、如果一个类需要复制操作或析构函数,它多半还需要有构造函数、析构函数、复制赋值和复制构造函数。
13、在复制赋值里需要检查自我赋值。
14、在写复制构造函数时,请小心复制每个需要复制的元素。
15、在向某个类中添加新成员时,一定要仔细检查,看是否存在需要更新的用户定义构造函数,以使它能够初始化新成员。
16、在类声明中需要定义整型 常量,请用枚举。
17、在构造全局和名字空间对象时,应避免顺序依赖性,所谓顺序依赖性就是后一个对象的创建依赖于前一个对象的创建
18、用第一次开关去缓和和顺序依赖性问题。
19、请记住、临时对象将在建立他们的那个完整表达式结束时销毁。
2013年6月24日
一、定义类的方法
class 类名
{
public:
公用的数据和成员函数;
private:
私用的数据和成员函数;
}
成员函数的定义;
1、先声明类的类型,再定义对象
class 类名
{
}
类名 对象名,对象名;
2、在声明类的类型同时,定义对象;
class 类名
{
} 对象名,对象名;
3、不声明类名,直接定义对象名
class
{
}对象名;
注:
其他对象可以发送消息给当前对象,通知当前对象修改某个属性,即最好将对象的属性设置为private;
二、类函数访问权限的设定:
把需要被外界调用的成员函数指定为public,它们是类的对外接口;
并非要求把所有成员函数都指定为public:由的函数并不是准备为外界调用,而是为本类中的成员函数所调用,应该声明为private,这种成员函数称为工具函数;
成员函数可以访问本类中的成员函数;
三、在类体中只写成员函数的声明,而在类的外面进行函数定义;
域作用符::用于声明该成员函数属于哪一个类;
(1)inline成员函数:使用inline函数可以节省运行时间,但却增加了目标程序的长度;
(2)一般只将规模很小而且使用频率很高的函数声明为内置函数;
(3)即使类内函数前面不加inline,也被隐含的指定为inline函数。
四、对象的存储
1、只用一段空间来存放共同的函数的代码段;
2、在调用各对象的函数时,都去调用这个公用的函数代码;
3、类是抽象的,在声明时,不占用内存;
4、对象是具体的,在定义时,要分配内存。
五、对象中成员的引用:
1、通过对象名和成员运算符访问对象中的成员;
访问对象中成员的一般形式:对象名.成员名
注意:必须指定对象名
2、通过指向对象的指针访问对象中的成员:
类似指向结构体变量的指针,通过指针访问对象中的成员。
3、通过对象的引用变量访问对象中的成员:
引用变量共占同一段存储单元的,实际上它们是同一个对象,只是用不同的名字表示而已。
六、如何访问私有成员
1、通过公共成员访问私有成员
2、利用指针访问私有数据成员
3、利用函数访问访问私有数据成员:利用公共函数返回私有成员
4、利用引用访问私有数据成员
七、类的公用接口
1、C++通过类来实现封装性,把数据和与这些数据有关的操作封装到一个类中
2、在声明一个类以后,用户主要通过调用公用的成员函数来实现类提供的功能-------称为消息传递
3、公用成员函数时用户使用类的公用接口,或者说是类的对外接口
4、在类外不能直接访问私有数据成员,但可以通过调用公用成员函数来引用甚至修改私有数据成员。
八、类的公用接口与私有实现的分离
1、类的公用接口与私有实现的分离,将接口与实现分离是软件工程的一个最基本的原则
2、类的公用接口与私有实现的分离的好处:
如果想修改或者扩充类的功能,只需要修改本类中有关的数据成员和与它有关的函数,程序中类外的部分不必修改;
如果在编译时发现错误,只需要检查本类
九、名词
1、方法:类的成员函数,是指对数据的操作;一个方法对应一种操作,显然,只有被声明为公用的方法才能被对象外界所**。
2、消息:其实就是一个命令,由程序语句实现:
外界是通过发消息来**有关方法的;
调用对象的成员函数,就是想对象发送一个消息。
十、有关构造函数使用的说明
1、在类对象进入其作用域时调用构造函数
2、构造函数不需要用户调用,也不能被用户调用
3、构造函数没有返回值,也不需要在定义时声明返回值类型
4、在构造函数的函数体中不仅可以对数据成员赋初值,而且可以包含其他语句;但一般不提倡在构造函数中加入与初始化无关的内容,以保持程序的清晰。
5、如果用户自己没有定义构造函数,则C++系统会自动生成一个默认构造函数,只是这个构造函数的函数体是空的,也没有参数,不执行初始化操作。
十一、析构函数
第十一章 运算符重载