第一部分
1、 C++是静态类型语言,因为其在编译时会执行类型检查。
2、 通常我们将8位的块作为字节,32位或4个字节作为一个字(word),一个char的最小存储空间为一个字节。
3、 只有内置类型存在字面值,没有类类型的字面值,因此,不存在任何标准库类型的字面值。
4、 内置类型变量是否自动初始化取决于变量定义的位置。在函数体外定义的变量都将被初始化成0,在函数体里定义的内置类型变量不进行自动初始化。
5、 当重复定义了一个全局变量和一个局部变量时,局部变量会屏蔽全局变量。
6、 编程技巧:当需要重复性地使用一个字面值常量时,一定要将该字面值常量赋值给一个变量。这很重要,方便修改和可读性。
7、 常量const对象在定义时必须初始化。因为其在定义后就不能被修改,所以必须初始化。
8、 Const对象有点特殊,在全局作用于声明的const变量是作为一个定义在该对象的文件的局部变量存在的,该变量值存在于那个文件中,而不能被其他文件访问。非const变量默认为extern的,但要使const变量能够在其他的文件中能够被访问,必须显式地指定它为extern
//---------------------------------------------------------普通变量------------------------------------------------
//file_1.cc
Int counter;//定义
//file_2.cc
Extern int counter;//使用file_1中的counter
++counter;
//---------------------------------------------------------const变量------------------------------------------------
//file_1.cc
Extern const int counter=fcn();//定义
//file_2.cc
Extern const int counter;//使用file_1中的counter
++counter; //这里的内容应该是错误的,因为const变量不允许变更
9、 引用就是对象的另一个名字(别名),在实际程序中,引用主要用于作为函数的形式参数。作用在引用上的所有操作事实上都是作用在该引用绑定的对象上的。
引用不可绑定到另一个对象,但可以对其进行重新赋值,因为其赋值是对原对象进行赋值,和它没有任何关系。
引用必须用于该引用同类型的对象进行初始化。
Int ival=1024;
Int &refVal= ival;//ok
Int&refVal2;//error,引用必须初始化
Int &refVal3=10;//error,初始化值必须为一个对象object
“Const引用”术语:是指向const对象的引用。
Const引用可以初始化为不同类型的对象或者初始化为右值。如字面值常量:
Int i=42;
//只对于const引用是合法的
Const int &r= 42;
Const int&r2=r+I;
必须说明,同样的初始化对于非const引用却是不合法的,而且会导致编译时错误,要解释上述行为,以下列代码为例:
Double dval =3.14;
Const int&ri = dval;
编译器会把这些代码转换成如下形式的代码:
Int temp = dval;//创建一个临时性的int对象,并用dval进行初始化
Const int&ri = temp;//将ri绑定到这个临时性对象上。
如果ri不是const的话,那么我们可以给ri赋值一个新的值。这样做不会修改dval,而是修改了temp。而我们期望是对ri的修改会修改dval的程序员就会发现dval并没有被修改。故而我们规定仅允许const引用绑定到需要临时使用的值完全隔断了这个问题发生的可能,因为const引用是只读的。
简而言之:
非const引用只能绑定到与该引用同类型的对象。
Cosnt引用则可以绑定到不同但是相关类型的对象或绑定到右值。
10、typedef通常被用于以下三种目的:(typedef可以被嵌套)
l 为了隐藏特定类型的实现,强调使用类型的目的。
l 简化复杂的类型定义,使得其更容易被理解。
l 允许一种类型用于多个目的,同时使得每次使用该类型的目的明确。(嵌套声明的作用)
11、枚举类型enum不可以被遍历,这是很不恰当的做法。
String专栏
1、 size()方法用来获取string的长度,而不是length。
2、 size()成员函数返回string::size_type类型,特别重要的是,不要把size的返回值赋值给一个int变量。故而遍历时,要将for循环中的临时变量定义为string::size_type类型。而且必须明确不可或缺,即是你添加了usingnamespace std;声明。
同样的,下标操作符[]返回的也是size_type类型。
3、 string的关系操作符==操作符比较的两个string对象,是比较它们的长度是否相同,且是否含有相同的字符,即是平时的完全比较。
4、 对于单个字符的处理,字符操作函数都在cctype头文件中进行定义。
如isalpha(c) 是否为字母,isdigit(c)是否为数字,islower(c)是否为小写字母,tolower(c)转小写字母。
cctype是使用的是c标准库中的ctype.h头文件中的库函数,一般C标准库头文件命名形式为name.h,而C++版本则命名为cname,少了后缀.h而在头文件名前加了c。
5、string类型的标准库查找函数如find,rfind,find_firs_of……这些操作都是返回string::size_type类型的值,以下标形式标记查找匹配所发生的位置;或者返回一个名为string::npos的特殊值,说明查找没有匹配。string类将npos定义为保证大于任何有效下标的值。
C风格字符串
1、 必须引入标准库函数#include<cstring>
2、 标准库举例:前提条件1)指针必须具有非零值。2)指向以null结束的字符数组中的第一个元素。
strlen(返回str的长度,但是不包括字符串结束符null),strcmp,strcat,strcpy,strncat,strncpy
并且C风格字符串的算术运算比较的是指针上存放的地址值,而并非它们所指向的字符串。
3、 C语言风格字符串应用举例
const char *pc = “a very ling literal string”.
const size_t len = strlen(pc); // space to allocate
//performance test on string allocation and copy
for(size_t ix = 0; ix != 100000; ++ix){
char *pc2 =new char[len+1]; //allocate the space
strcpy(pc2,pc); // do the copy
if(strcmp(pc2,pc)) //use the new string
;
delete [] pc2; //free the memory
}
4、 将string类型转为C风格字符串函数c_str();注:最后得到的是一个constchar *类型。
//------------------------------------------------String专栏END-------------------------------------------------------
12、标准库vector类型是一个类模板,也是一个容器。在值进行初始化时,内置类型将默认为0值,类类型调用构造函数初始化。
Vector对象的初始化式
Vector<T> v1;
Vector<T> v2(v1); v2为v1的副本
Vector<T> v3(n,i); v3包含n个值为i的元素
Vector<T> v4(n); v4含有值初始化的元素的n个副本
13、empty()、size()、push_back(t)、v[n]分别为vector的四大方法。其中push_back是在v的末尾添加一个t元素;v[n]下标操作是获得其值,但是不会添加任何元素(for循环中容易误解)。Size()方法也是返回vector<元素类型>::size_type类型的数值。
14、TIP:安全的泛型编程会使用for循环的判断条件为!=?.size()来进行终止循环判断,而不是<来编写循环判断条件。
15、迭代器 vector<元素类型>::iteratoriter;(每个容器都定义了一个iterator的迭代器类型)。
vector<元素类型>::iteratoriter = vec.begin(); //返回第一个元素
或者是使用解引用*来访问迭代器所指向的元素。如*iter。
应用举例:
循环方法遍历:
for(vector<int>::size_typei =0;i!=vec.size();i++)
vec[i]…..dosomething
迭代器方法遍历:
for(vector<int>::iteratoriter = vec.begin();iter!=vec.end();++iter)
*iter…….
16、定位于vector的中间元素:
vector<int>::iteratormid = vi.begin() + vi.size()/2;
需要注意的是:任何改变vector长度的操作都会使已存在的迭代器失效,例如,在调用push_bck之后,就不能在信赖指向vector的迭代器的值了。
17、vector<元素类型>::const_iterator,常量迭代器类型不可以改变其所指向的元素的值,但是可以改变其自身的值。
const_iterator和const的iterator是完全不同的(且是相反的,后者不可以改变其自身的值,但是可以改变其所指向的元素的值),两种不可以混淆,声明一个const的迭代器时,必须初始化迭代器。且一旦被初始化后,就不能改变它的值。所以const的iterator一般不做使用。
vector<int> nums(10);// nums是非常量的
const vector<int>::iterator cit =nums.begin();
*cit = 1;
++cit; //不可以改变cit的值。
18、bieset对象上的操作
b.any() b中是否存在为1的二进制位
b.none()
b.size()
b.test(pos) 访问b中在pos处的二进制位是否为1
b.count() b中置为1的二进制位的个数
b.flip() 取反
其中count操作和size操作的返回类型是标准库中命名为size_t的类型,size_t类型定义在cstddef头文件中,该文件是C标准库的头文件stddef.h的C++版本。
数组与指针专栏
1、 与vector类型相比,数组的显著缺陷在于:数组的长度是固定的,而且程序员无法知道一个给定数组的长度。数组没有获取器容量大小的size操作,也不提供push_back操作在其中自动添加元素。如果需要更改数组的长度,程序员只能创建一个更大的新数组,然后把原数组的所有元素复制到新数组空间中去。
2、 数组也是一种复合数据类型,而且不存在引用性数组。
3、 数组的维数必须用值大于等于1的常量表达式定义。非const变量以及要到运行阶段才知道其值的const变量都不能用于定义数组的维数。
const unsigned buff_size =get_size();
int test[buff_size]; //error,buff_size要直到运行阶段才知道其值。但是如果使用
int test[] = new int[buff_size];是可以的。
4、 不允许对数组进行直接复制和赋值,与vector不同,一个数组不能用另一个数组进行直接初始化,也不能将一个数组赋值给另一个数组,这些操作都是非法的。
int ia[]={0,1,2};
int ia2[](ia); //error,一个数组不能用另一个数组进行直接初始化
int main(){
const unsignedarray_size =3;
int ia3[array_size];//ok,但是元素是没有进行初始化的
ia3 = ia;//error, 不能将一个数组赋值给另一个数组
}
5、 在使用下标访问元素时,vector使用vector::size_type作为下标的类型,而数组下标正确的类型则是size_t。包括sizeof的返回值类型也是size_t。
因为sizeof返回整个数组在内存中的存储长度,所以用sizeof数组的结果除以sizeof其元素类型的结果,即可求出数组元素的个数。
int sz = sizeof(ia)/sizeof(*ia);//返回ia以为数组的元素的个数
二位数组是:sizeof(ia)/sizeof(**ia)
6、 注意的是,除了自己注意细节,并彻底测试自己的程序之外,没有别的办法可以防止数组越界。
7、 创建动态数组
l 创建动态数组时,如果数组元素是内置类型,则无初始化。
int *pia = new int[10];//array of 10 empty ints
也可以使用跟在数组元素后面的一对空圆括号,对数组元素做值初始化。
int *pia = new int[10]();//array of 10 all set to 0
l 允许动态分配空数组
之所以要动态分配数组,往往是由于编译时并不知道数组的长度,
size_t n = get_size();//get_size返回需要的元素个数
int *p = new int[n];
for(int *q = p;q!=p+n;++q)
……
当get_size返回值为0时会怎么样?
代码依旧会正确执行。C++中虽然不允许定义长度为0的数组变量,但明确指出,调用new动态创建长度为0的数组是合法的。
char arr[0];//error,不能定义为0的数组
char *cp = new char[0];//ok,但是cp不可以被解引用。
l
8、 一个有效的指针必然是以下三种状态之一:保存一个特定对象的地址;指向某个对象后面的另一对象;或者是0值(表明它不指向任何对象)。
9、 把int型变量赋值给指针是非法的,尽管此int变量的值可能为0。但是允许把数组0或在编译时可获得0值得const量赋值给指针。
10、 预处理器变量NULL是在C++语言从C语言中继承下来的cstdlib头文件中定义的,其值为0,但同时预处理器变量不是在std命名空间中定义的,因此其名字应该为NULL,而非std::NULL。
10、void*指针,它可以保存任何类型对象的地址:void*指针只表明该指针与一地址值相关,但是不清楚存储在此地址上的对象的类型。
void*指针只支持几种有限的操作:
l 与另一个指针进行比较
l 向函数传递void*指针或从函数返回void*指针
l 给另一个void*指针进行赋值
l 不允许使用void*指针操纵它所指向的对象。
l 不能使用void*指针保存const对象的地址,而必须使用constvoid*类型的指针保存const对象的地址。
11、指针和引用的比较
虽然使用引用(reference)和指针都可间接访问另一个值,但两者之间还是存在两个重要区别。第一个区别在于引用总是指向某个对象:定义引用时没有初始化是错误的。
第二个区别在于赋值行为的差异:给引用赋值修改的是该引用所关联的对象的值,而并不是使引用于另一个对象关联。且引用一经初始化,就始终指向同一个特定对象。
12、使用指针访问数组元素
intia[] = {0,1,2};
int*ip = ia;// ip 指向ia[0]
ip= &ia[2];//iPhone指向ia中的最后一个元素
13、指针的算术运算的减运算需要使用到ptrdiff_t
ptrdiff_tn = ip2 -ip;
ptrdiff_t与size_t类型一样,ptrdiff_t也是一种与机器相关的类型,在cstddef头文件中进行定义。ptrdiff_t是signed类型,其能保证足以存放同一数组中两个指针之间的差距,它有可能是负数。
14、下标和指针
int*p = &ia[2];
intj = p[1];//ok,等价于*(p+1)
intk = p[-2];//ok,等价于*(p-2)
15、计算数组的超出末端的指针,有时我们需要类似vector的end()返回的指针作为循环结束哨兵。
constsize_t arr_size = 5;
intarr[arr_size] = {1,2,3,4,5};
int*p = arr;
int*q = p+ arr_size;
注意:C++允许计算数组或对象的超出末端的地址,但是不允许对此地址进行解引用操作。而计算数组超出末端位置之后或数组首地址之前的地址都是不合法的。
16、指针和const限定符
指向const对象的指针: constdouble *p = &PI;
const指针: double *const q = &PI;//q是指向double类型对象的const指针。
指针和typedef
typedefstring *pstring;
constpstring cstr;
cstr变量的类型?很多人都认为真正的类型是const string *cstr,也就是说const pstring是一种指针,指向string类型的const对象,但这是错误的。
错误的原因在于将typedef当做文本扩展了。声明constpstring时,const修饰的是pstring的类型,这是一个指针。因此,该语句应该是把cstr定义为指向string类型对象的const指针,这个定义等价于:string *const cstr;
以后碰到const和指针混合在一起时都应该作如上理解。
17、指针和多维数组
因为多维数组其实就是数组的数组,所以由多维数组转换而成的指针类型应是指向第一个内层数组的指针。
intia[3][4];
int(*ip)[4] = ia; // ip 指向一个大小为4的一维数组
ip= &ia[2]; // ia[2]也是一个大小为4的一维数组。
用typedef 简化指向多维数组的指针。
typedefint int_array[4]; //可以理解为int_array为一个指向int[4]的指针
int_array*ip= ia; // ip 为指向ia第一行数组的指针地址
for(int_array*p = ia;p!=ia+3;++p)
for(int*q = *p;q!=*p+4;++q)
cout<<*q<<endl;
外层的for循环首先初始化p指向ia的第一个内部数组,然后一直循环到ia的三行数据都处理完毕为止。++p使得p加1,等效于移动指针使其指向ia的下一行。
//--------------------------------------------数组与指针专栏END---------------------------------------------------
19、不应该串接使用关系操作符(<、<=、>、>=)
如if(i<j<k)//只要k>1那么这个等式就永远成立。
20、当使用new和delete创建和释放单个对象时,内置类型默认是不进行初始化的。
另外需要注意的是,如果指针指向不是用new分配的内存地址,那么在该指针上使用delete是不合法的。但是对于指针值为0的指针,C++保证删除0值的指针式安全的,故而无论其是否是new的,都可以delete。
21、显示转换的四个转换操作符:
static_cast,dynamic_cast,const_cast和reinterpret_cast
其中static_cast可以找回存放在void*指针中的值。
void*p = &d;
double*dp = static_cast<double*>(p);
22、在对switch中的控制流进行处理时,有时我们需要让两个或多个case值做相同的动作序列。典型的做法是,把case标号依次排列;为了强调这些case表示的是一个要匹配的范围,我们可以将它们全部在一行中列出。
switch求解的表达式可以非常复杂,特别是,该表达式可以定义和初始化一个变量:
switch(intval = get_response())
case标号必须是整形常量表达式。
23、数组形参
数组有两个特殊的性质,会严重影响我们定义和使用作用在数组上的函数:一是不能复制数组;二是使用数组名字时,数组名会自动转化为指向其第一个元素的指针。
因为数组不能复制,所以无法编写使用数组类型形参的函数。因为数组会被自动转化为指针,所以处理数组的函数通常通过操纵指向数组中的元素的指针来处理数组。
数组实参
和其他类型一样,数组形参可定义为引用或非引用类型。大部分情况下,数组以普通的非引用类型传递,此时数组会悄悄地转换为指针。一般来说,非引用类型的形参会初始化为其相应实参的副本。而在传递数组时,实参是指向数组第一个元素的指针,形参复制的是这个指针的值,而不是数组元素本身。函数操纵的是指针的副本,因此不会修改实参指针的值。然而,函数可通过该指针改变它所指向的数组元素的值。通过指针形参做的任何改变都在修改数组元素本身。让不需要修改数组形参的元素时,函数应该将形参定义为指向const对象的指针: void f(const int *){……}
当通过引用传递数组时,如果形参是数组的引用,编译器不会将数组实参转化为指针,而是传递数组的引用本身。
void test(int(&arr)[10]){…….}
这里的&arr 两边的()是必须的,因为下标操作符具有更高的优先级:
test(int(&arr)[10]) //ok,arr是一个指向包含10个元素的引用
test(int &arr[10])//error,arr是一个存储10个引用的数组
多维数组的传递
voidtest(int (*matrix)[10],int rowSize) 或 void test(int (matrix*)[10],int rowSize)
等价于
voidtest(int matrix[][10],int rowSize)
其中*matrix两边的()是必须的,原因同上。
24、主函数main返回的值视为状态指示器,返回0表示程序运行成功,其他大部分返回值则表示失败,为了使得返回值独立于机器,cstdlib头文件定义了两个预处理变量EXIT_FAILURE和EXIT_SUCCESS分别用于便是程序运行失败与成功。
25、编程时,千万要注意如果函数的返回类型为引用类型,那么千万不要返回局部对象的引用(指针也是同样的道理)。因为当函数执行完毕时,将释放分配给局部变量的存储空间。此时,对局部对象的应用就会指向不确定的内存。考虑下面的程序:
//灾难:该函数返回了一个指向局部对象的引用
const string &manip(const string&s){
stringret = s;
returnret; // error,返回了一个指向局部对象的引用。
}
26、类的const成员函数
classtest{
boolsame_isbn(const Sales_item &rhs) const
{returnisbn == rhs.isbn;}
};
const成员函数等价于
boolsame_isbn(const Sales_item *const this, const Sales_item &rhs) const{……}
const改变了隐含的this形参的类型,在调用total.same_isbn(trans)时,隐含的this形参将是一个指向total对象的constSales_item*类型的指针。
27、重载函数
出现在相同作用域中的两个函数,如果具有相同的名字而形参表不同,则称为重载函数。(不考虑返回值的)
如果两个函数的形参表完全相同,但返回类型不同,则第二个声明是错误的。
28、函数指针
函数指针是指指向函数而非指向对象的指针。
bool(*pf)(const string &,const string &) //pf指向一个函数,该函数返回bool类型并且接收两个conststring &类型的形参。
*pf两侧的()是必须的:
//声明了一个函数为pf,返回bool*类型
bool*pf(const string &,const string &)
29、一个打开并检查输入文件的程序
ifstream&open_file(ifstream &in,const string &file){
in.close();
in.clear();
in.open(file.c_str());
returnin;//in要么已经与指定文件绑定起来了,要么处于错误条件状态。
}
30、stringstream对象的使用
我们已经见过以每次一个单词或每次一行的方式处理输入的程序,第一种使用string输入操作符,而第二种则使用getline函数。然而,有些程序需要同时使用这两种方式:有些处理基于每行实现,而其他处理则要操纵每行中的每个单词,这时可用stringstream对象实现。
string line,word;
while(getline(cin,line)){
istringstreamiss(line);
while(iss>>word){
……do something
}
}
stringstream对象的另一个常见用法是,需要在多种数据类型之间实现自动格式化。(即塞进去是什么类型,出来就是什么类型,每种类型都是一个字段),用<<塞进ostringstream对象,用>>取出istringstream对象。
31、cin.getline(c风格字符串,读入数目)语法(属于istream流)
istream &getline( char *buffer,streamsize num );
istream &getline( char *buffer,streamsize num, char delim );
纯粹的getline(cin,string)属于string流,两者是不一样的
第二部分
1、 标准库定义三种顺序容器类型:vector、list和deque(是双端队列double-end queue)
标准库还提供了三种容器适配器(Adaptor),实际上,适配器是根据原始的容器类型所提供的操作,通过定义新的操作接口,来适应基础的容器类型。顺序容器适配器包括stack、queue和priority_queue类型
2、 泛型算法
标准容器定义了很少的操作。大部分容器都支持添加和删除元素;访问第一个和最后一个元素;获取容器的大小,并在某些情况下重设容器的大小;以及获取指向第一个元素和最后一个元素的下一位置的迭代器。
关键概念:算法永不执行容器提供的操作。
泛型算法本身从不执行容器操作,只是单独依赖迭代器和迭代器操作实现。算法基于迭代器以及操作实现,而并非基于容器操作。这个事实也许比较意外,但本质上暗示了:使用“普通”的迭代器时,算法从不修改基础容器的大小。正如我们所看到的,算法也许会改变存储在容器中的元素的值,也许会在容器内移动元素,但是,算法不直接添加或删除元素。
第三部分 类和数据抽象
1、 类的定义为什么以分号结束
分号是必须的,因为在类定义之后可以接一个对象定义列表。如:
class Sales_item{ /*……*/ };
class Sales_item{ /*……*/ } accum,trans;
不过我们通常不会这么用。将对象定义成类定义的一部分是个相当坏的注意。这样做会使所发生的操作难以理解。对读者而言,将两个不同的实体(类和变量)组合在一个语句中,也会令人迷惑不解。
2、 从const成员函数返回*this
在普通的非const成员函数中,this的类型是一个指向类类型的const指针。我们可以改变this所指向的值,但不能改变this所保存的地址。在const成员函数中,this的类型是一个指向const类类型对象的const指针。既不能改变this所指向的对象,也不能改变this所保存的地址。
也就是说,我们不能从一个const成员函数返回指向类对象的普通引用。const成员函数只能返回*this作为一个const引用。
3、 良好的编程习惯:基于const的重载
有时我们希望能像JQuery一样在一个操作序列中使用,如
myScreen.move(4.0).set(‘#’).display(cout),但是display是一个const成员,假如我们需要这么做:myScreen.display(cout).move(4.0)这样是有问题的,用const对象调用非const成员函数。
我们可以定义一个小函数来完成一些函数的“实际”工作。这里每个display函数都调用do_display的private成员来完成打印工作。
class Screen{
public:
Screen&display(ostream &os){
do_display(os);
return*this;
}
constScreen& display(ostream &os) const{
do_display(os);
return*this;
}
private:
voiddo_display(ostream &os) const{
os<<contents;
}
};
现在,当我们将display嵌入到一个长表达式中时,将调用非const版本。当我们display一个const对象时,就调用const版本。
Screen myScreen(5,3);
const Screen blank(5,3);
myScreen.set(‘#’).display(cout); //调用非const版本
blank.display(cout); //调用const版本
这样做有以下几个原因:
1) 一般希望避免在多个地方编写同样的代码
2) display操作预期会随着类的演变而变得更复杂。当所涉及的动作变得更复杂时,我们只在一处而不是两处编写这些动作有更显著的意义。
3) 如要为do_display增加调试信息,但在最终产品中要去掉这些信息,如果只需要改变一个do_display的定义来增加或删除调试代码,这样做将更容易。
4) 这个函数的调用不存在任何额外的开销,我们可以让do_display成为内联函数。
4、 可变数据成员,甚至是在const成员函数类都可以修改,我们可以通过将它们声明为mutable来实现。
5、 构造函数不能声明为const
class Sales_item{
public:
Sales_item()const ; //error
};
const构造函数是不必要的,创建类类型的const对象时,运行一个普通构造函数来初始化该const对象。
6、 构造函数的初始化式
有些成员必须在构造函数初始化列表中进行初始化。对于这样的成员,在构造函数体中对它们赋值不起作用。如:没有默认构造函数的类类型的成员,以及const成员和引用类型的成员,都必须在构造函数初始化列表中进行初始化。
需要注意的是,类的内置类型的成员不进行隐式初始化。只对定义在全局作用域中的对象才进行初始化。
另外,初始化式仅指定用于初始化成员的值,并不能指定这些初始化执行的次序。成员被初始化的次序是由定义成员时的次序决定的。这就表示了,我们应当尽可能地避免使用类成员来初始化类的其他成员。
7、 默认实参的用法
classSales_item{
public:
//包含两个构造函数Sales_item();和Sales_item(conststring &)
Sales_item(const string &="");
Sales_item(istream&);
private:
string isbn;
int units_sold;
double revenue;
};
Sales_item::Sales_item(const string&book=""):isbn(book),units_sold(0),revenue(0){}
8、 初级C++程序员常犯的一个错误:采用以下方式声明一个用默认构造函数初始化的对象:
//这里是声明了一个函数,而非一个对象
Sales_item myobj();
但是,我们可以这样声明:
//ok,创建了一个没有命名的空的Sales_item,并用其来初始化myobj
Sales_item myobj = Sales_item(); //new 省略了
在这里,我们创建并初始化了一个Sales_item对象,然后用它来初始化myobj。编译器通过运行Sales_item的默认构造函数来按值初始化一个Sales_item。
9、 隐式类类型转换(从形参类型到类类型的转换):是通过用单个实参来调用的构造函数定义了从形参类型到该类类型的一个隐式转换。
如7中的代码所示,这里的两个构造函数都定义了一个隐式转换,因此,在期待一个Sales_item类型对象的地方,我们都可以使用一个string或一个istream:
string null_book = “9-999-999”;
item.same_isbn(null_book);
若不想存在隐式类类型转换,则可使用explicit关键字:
explicit Sales_item(const string & ="");
并且explicit关键字只允许声明在头文件中,也就是说在类的外部定义域外不能声明或重复声明。
内联函数应该在头文件中进行定义,其定义因为必须对编译器而言是可见的,一遍编译器能够在调用点内联展开该函数的代码。此时,仅有函数原型是不够的。
10、复制控制:复制构造函数、赋值操作符、析构函数
实现复制控制操作最困难的部分,往往在于识别何时需要覆盖默认版本。有一种特别常见的情况是需要类定义自己的复制控制成员的:类具有指针成员。或者有成员表示在构造函数中分配的其他资源。
C++支持两种形式的初始化形式:直接初始化和复制初始化。复制初始化使用=符号,而直接初始化将初始化式放在圆括号中。
当用于类类型的对象时,初始化的复制形式和直接形式有所不同:直接初始化直接调用与实参匹配的构造函数,复制初始化总是调用复制构造函数。复制初始化首先使用指定构造函数创建一个临时对象,然后用复制构造函数将那个临时对象进行复制,然后将得到的副本对象复制到正在创建的对象上。
stringnull_book=”9-999-999”; //复制初始化
stringdots(10,’.’); //直接初始化
stringempty_copy = string();//复制初始化
stringempty_direct; //直接初始化
11、合成的复制构造函数
合成的复制构造函数的行为是执行逐个成员初始化,将新对象初始化为源对象的副本。合成复制构造函数直接复制内置类型成员的值,类类型成员使用该类的复制构造函数进行复制。只有数组成员是个例外。虽然一般不能复制数组,但如果一个类具有数组成员,则合成复制构造函数将复制数组。复制数组时合成复制构造函数将复制数组的每一个元素。
禁止复制:类必须显示声明其复制构造函数为private。然而此时类的友员和成员仍然可以进行复制。如果想要连友员和成员中的复制也禁止,就可以声明一个private复制构造函数但不对其进行定义。声明而不定义成员函数是合法的,但是,使用未定义成员的任何尝试都将导致链接失败(编译时出错),这样可以禁止任何复制类类型对象的尝试。
//copy control
//复制构造函数
Sales_item(const Sales_item &org):
isbn(org.isbn),units_sold(org.units_sold),revenue(org.revenue){}
12、合成的赋值操作符
Sales_item& operator=(const Sales_item&org){
isbn= org.isbn;
units_sold= org.units_sold;
revenue= org.revenue;
return*this;
}
13、析构函数
析构函数并不仅限于用来释放资源,一般而言,析构函数可以执行任意操作,该操作是类设计者希望在该类对象的使用完毕之后执行的。与复制构造函数不同或赋值操作符不同,编译器总是会为我们合成一个析构函数,即使我们编写了自己的析构函数,合成析构函数仍然运行。
14、智能指针举例
pointPartner类保存指针和指针的使用计数,每个HasPtr对象都将指向一个pointPartner对象,使用计数将跟踪指向每个pointPartner对象的pointPartner对象的数目。pointPartner定义的仅有函数是构造函数和析构函数,构造函数复制指针,而析构函数删除它。构造函数还将使用计数置为1,表示一个HasPtr对象指向这个pointPartner对象。
//private class for use by HasPtr only
class pointPartner{
friendclass HasPtr;
private:
pointPartner(int*p):ptr(p),use(1){}
~pointPartner(){deleteptr; }
private:
int*ptr;
size_tuse;
};
class HasPtr{
public:
HasPtr(int*p,int value):u_pp(new pointPartner(p)),val(value){}
//copycontrol
HasPtr(constHasPtr &org):u_pp(org.u_pp),val(org.val){++u_pp->use;}
HasPtr&operator=(const HasPtr &org){
++org.u_pp->use; //注意:在类的内部是可以访问其私有成员的
//但是继承层次上只能通过派生类访问protected成员,而不能在派生类中使用基类访问protected成员
if(--u_pp->use==0)
deleteu_pp;
u_pp= org.u_pp;
val= org.val;
return*this;
}
~HasPtr(){
if(--u_pp->use==0)
deleteu_pp;
}
intget_val(){return val;}
voidset_val(int value){val = value;}
int*get_u_pp(){return u_pp->ptr;}
voidset_u_pp(int *p){u_pp->ptr=p;}
intget_u_pp_val(){return *u_pp->ptr;}
voidset_u_pp_val(int value){*u_pp->ptr=value;}
private:
pointPartner*u_pp;
intval;
};
15、值型类:给指针成员提供值语义,复制值型对象,得到一个不同的新副本。
class HasPtr{
public:
HasPtr(int*p,int value):ptr(new int(p)),val(value){}
//copycontrol
//复制构造函数不再复制指针,它将分配一个新的int对象,并初始化该对象以保存与被复制对象相同的值
//每个对象都保存属于自己的int值得不同副本,因为每个对象都保存着自己的副本,所以析构函数将无条件删除指针。
HasPtr(constHasPtr &org):ptr(new int(*org.ptr)),val(org.val){}
//赋值操作符不需要分配新对象,它只是必须记得给其指针所指向的对象赋新值,而不是给指针本身赋值
//换句话说,改变的是指针所指向的值,而不是指针
HasPtr&operator=(const HasPtr &org){
//ptr= new int(*org.ptr); 这里不能这么写
*ptr= *org.ptr;
val= org.val;
return*this;
}
~HasPtr(){deleteptr;}
intget_val(){return val;}
voidset_val(int value){val = value;}
int*get_ptr(){return ptr;}
voidset_ptr(int *p){ptr=p;}
intget_ptr_val(){return *ptr;}
voidset_ptr_val(int value){*ptr=value;}
private:
int*ptr;
intval;
};
16、操作符重载与友元关系
操作符定义为非成员函数时,通常必须将它们设置为所操作类的友元。因为在这种情况下,操作符通常需要访问类的私有部分。
在使用重载操作符时,与内置类型操作数上使用操作符的方式一样。假定item1和item2都是Sales_item对象,可以打印它们的和,就像打印两个int的和一样:
cout<<item1+item2<<endl;
或者显式地调用:cout<<operator+(item1, item2)<<endl;
17、选择成员或非成员实现
为类设计重载操作符的时候,必须选择是将操作符设置为类成员还是普通的非成员函数。在某些情况下,程序员没有选择,操作符必须是成员函数;在另一些情况下,有些经验原则可指导我们作出决定。
l 赋值(=)、下标([])、调用(())和成员访问操作符(->)等操作符必须定义为成员函数,若将这些操作符定义为非成员函数将在编译时标记为错误。
l 像赋值一样,复合赋值操作符(+=)通常应定义为类的成员。与赋值不同的是,不一定非得这样做,但最好定义为类成员(经验)。
l 改变对象状态或与给定类型紧密联系的其他一些操作符,如自增、自减和解引用,通常应定义为类成员。
l 对称的操作符,如算术操作符(+,-,*,/)、相等操作符(==)、关系操作符和为操作符,最好定义为普通的非成员函数。
18、输入和输出操作符(读入和输出一个对象)
为了与标准库iostream为内置类型定义的接口一直,操作符应该接受iostream&作为第一个形参,对类类型const对象的引用作为第二个形参,并返回对iostream形参的引用。
IO操作符必须为非成员函数:不这么做的话,左操作数将智能是该类类型的对象:
//如果operator<<是类的成员函数
Sales_itemitem;
item<<cout;
这个用法与为其他类型定义的输出操作符的正常使用方式正好想法。
19、重载*和->操作符注意点
a) 应当各准备两个版本,一个为非const,一个为const成员函数
b) 重载->,当我们编写point->action();时,是调用(point.operator->())->action,注意,这里相当于做了两步,一部是返回point.operator->()的结果指针,然后再用该指针调用该类型的方法。
20、函数对象(调用操作符),一般用于标准库算法
定义一个函数以确定给定string的长度是否大于6字符
boolGT6(const string &s){ return s.size()>=6;}
然后我们使用GT6作为传给count_if算法的实参,以计算是GT6返回true的单词的数目:vector<string>::size_typewc =
count_if(words.begin(),words.end(),GT6);
函数对象GT_cls
//判断一个给定Word的长度是否长于规定长度值
classGT_cls{
public:
GT_cls(size_tval=0):bound(val){}
booloperator()(const string &s){
returns.size() >= bound;
}
};
调用函数对象:count_if(words.begin(),words.end(),GT_cls(6));
注意:这里GT_cls(6)是调用operator()而不是调用构造函数。
很容易和构造函数搞混。
21、转换与类类型:极大地减少所需操作符的数目(此时特别有用)
转换操作符在类定义体内声明,在保留字operator之后跟着转换的目标类型:
operatortype();
转换函数必须是成员函数,且不能指定返回类型,但必须有返回值,且形参表必须为空。
我们只能应用一个类类型转换,如果想使用转换链,则代码出错。
注意:标准转换在类类型转换之前,即构造函数执行的隐式转换在其之前。(一般)
operatorint() const{……..} //const非必须,但转换函数一般不应该改变被转换的对象,因此,转换操作符通常应定义为const成员。
转换和重载操作符的冲突:既为算术类型提供转换函数,又为同一类类型提供重载操作符,可能会导致重载操作符和内置操作符之间的二义性。
不要定义能相互转换的类,即如果类Foo具有接受类Bar的对象的构造函数,不要再为类Bar定义到类型Foo的转换操作符。
避免到内置算术类型的转换。具体而言,如果定义了到算术类型的转换,则:
l 不要定义接受算术类型的操作符的重载版本。
l 不要定义转换到一个以上算术类型的转换。让标准转换提供到其他算术类型的转换。
最简单的规则是,对于那些“明显正确的”,应避免定义转换函数并限制非显式构造函数。
22、Map遍历
intmain(int argc, char* argv[])
{
map<string, string> mapData;
mapData["a"] = "aaa";
mapData["b"] = "bbb";
mapData["c"] = "ccc";
for (map<string, string>::iterator i=mapData.begin();i!=mapData.end(); /*i++*/)
{
if (i->first == "b")
{
mapData.erase(i++);
}
else
{
i++;
}
}
return 0;
}
Static专栏
1、 static局部对象
static局部对象可以确保不迟于程序执行流程第一次经过该对象的定义语句时进行初始化。这种对象一旦被创建,在程序结束前都不会被撤销。当定义静态局部对象的函数结束时,静态局部对象不会撤销。在该函数被多次调用的过程中,静态局部对象会持续存在并保持它的值。考虑下面的小例子,这个函数计算了自己被调用的次数。
size_t count_calls(){
staticsize_t ctr = 0;//value在第一次调用函数count_calls之前就已创建并赋予初值0
return++ctr;
}
int main(){
for(size_ti=0;i!=10;+=i)
cout<<count_calls()<<endl;
}
这个程序会依次输出1到10的整数。
2、 static类成员
我们一般使用static类成员来代替一个全局对象,因为全局对象会破坏封装,且一般的用户代码就可以修改这个值。优点:1)避免名称冲突。2)实施封装,static成员可以是私有成员,而全局对象不可以。3)static成员是与特定类关联的,这种可见性可清晰地显示程序员的意图。
static数据成员独立于该类的任意对象而存在,每个static数据成员是与类关联的对象,并不是与该类的对象相关联。也就是说,static成员函数没有this形参,它可以直接访问所属类的static成员,但不能直接使用非static成员。同样的,因为static成员不是任何对象的组成部分,所以static成员函数不能声明为const。毕竟,将成员函数声明为const就是承诺不会修改该函数所属的对象。最后,static成员函数也不能声明为虚函数。
static成员函数和explicit一样,只出现在类定义体内部的声明处。
static数据成员必须在类定义体的外部定义(且正好一次),不像普通的数据成员,static成员不是通过类构造函数进行初始化,而是应该在定义时进行初始化。而保证对象正好定义一次的最好方法,就是讲static数据成员的定义放在包含类的非内联成员函数定义的文件中(.cpp文件,猜测)。但是static数据成员如果为static const的话,就可以直接在类的定义体中进行初始化,但同时该数据成员也仍然必须在类的定义体之外进行定义,只不过不需要再制定初始值。
static数据成员可以作为默认实参,因为static成员不是类对象的组成部分。
3、 继承与静态成员
如果基类定义了static成员,则整个继承层次中只有一个这样的成员,无论从基类派生出多少个派生类,每个static成员只有一个实例。且static成员遵循常规的访问控制。
4、
//------------------------------------------------Static专栏END-------------------------------------------------------
第四部分 面向对象编程与泛型编程
1、 面向对象编程基于三个基本概念:数据抽象(封装)、继承、动态绑定(多态,polymorphism)。在C++中,用类进行数据抽象,用类派生从一个类继承另一个类:派生类继承基类的成员。动态绑定使编译器能够在运行时决定是使用基类中定义的函数还是派生类中定义的函数。(引用和指针的静态类型与动态类型可以不同,这是C++用以支持多态性的基石)
2、 在C++中,通过基类的引用(或指针)调用虚函数时,发生动态绑定。引用(或指针)既可以指向基类对象也可以指向派生类对象,这一事实是动态绑定的关键。用引用(或指针)调用的虚函数在运行时确定,被调用的函数是引用(或指针)所指向的对象的实际类型所定义的。
3、 保留字virtual的目的是启用动态绑定。成员默认为非虚函数,对非虚函数的调用在编译时确定。为了指明函数为虚函数,在其返回类型前面加上保留字virtual。成了构造函数之外,任意非static成员函数都可以是虚函数。保留字virtual只在类内部的成员函数声明出现,不能用在类定义体外部出现的函数定义上。这点和static和explicit声明是一样。
要出发动态绑定,必须满足两个条件:第一,只有指定为虚函数的成员函数才能进行动态绑定,第二,必须通过基类类型的引用或指针进行函数调用。(这是指在函数编写时),当调用传入实参时,基类类型或派生类类型的引用或指针均可。
4、 protected,受保护的访问标号,protected成员可以被派生类对象访问但不能被该类型的普通访问。
假设Bulk_item类继承Item_base,Item_base有两个数据成员,private的isbn和protected的price,定义一个memfcn函数:
void Bulk_item::memfcn(const Bulk_item &d,constItem_base &b){
double ret= price; //ok,use this->price
ret =d.price;//ok,use price from a Bulk_item object
ret = b.price;//error,从一个Item_base对象没有对price的访问权限。
//也就是说,从用户级别上,本身的对象对自己本身的protected成员是没有访问权限的。但派生类在用户级别上是有访问权限的。
}
5、 覆盖虚函数机制
在某些情况下,我们会希望覆盖虚函数机制并强制函数调用使用虚函数的特定版本,这是可以使用作用域操作符:
Item_base *base = &derived;
double d = base->Itembase::net_price(42);
这段代码强制将net_price调用确定为Item_base中定义的版本,该调用将在编译时确定。
6、 虚函数与默认实参
如果同一虚函数的基类版本和派生类版本中使用不同的默认实参
通过基类的引用或指针调用虚函数时,默认实参为在基类虚函数声明中制定的值。
如果通过派生类的指针或引用调用虚函数,则默认实参是在派生类的本中声明的值。
7、 每个类控制它所定义的成员的访问。派生类可以进一步限制但不能放松对所继承的成员的访问。也就是说,基类本身制定对自身成员的最小访问控制。
8、 接口继承(public)与实现继承(protected或private)
若想在派生类中恢复继承成员的访问级别,可以在继承类的public部分增加一个using声明。public:
usingBase::size;
9、 转换与继承
派生类对象也是基类对象:派生类对象的引用 自动转换为 基类子对象的引用
所以一般存在使用派生类对象类型对基类类型的对象进行初始化或赋值,但
没有从派生类类型对象到基类类型对象的直接转换。
10、基类显式定义了将派生类类型对象复制或赋值给基类对象的含义,这可以通过定义适当的构造函数或赋值操作符实现。(不常见,一般是隐式地实现)
class Derived;
class Base{
public:
Base(constDerived&);//创建了一个从Derived的新的Base对象
Base&operator=(const Derived&);//从一个Derived复制
};
11、当基类指针或引用实际绑定到一个派生类对象时,从基类到派生类的转换也存在限制:
Bulk_itembulk;
Item_base*base= &bulk;
Bulk_item*p = base;//error,不能从base转为一个Bulk_item对象。
在这些情况下,如果知道从基类到派生类的转换是安全的,就可以使用static_cast强制编译器进行转换。或者,可以用dynamic_cast申请在运行时进行检查。
12、构造函数和复制控制成员不能被继承,每个类都需要定义自己的构造函数和复制控制成员。像任何类一样,如果类不定义自己的默认构造函数和复制控制成员,就将使用合成版本。
如果希望派生类构造函数将基类的数据成员也进行初始化,那么就必须在初始化列表中调用基类的构造函数。
如果派生类显式地定义了自己的赋值操作符,则该操作符必须对基类部分进行显式赋值。(要防止自身赋值if(this!=&org))
派生类的析构函数不负责撤销基类对象的成员。编译器总是显式调用派生类对象基类部分的析构函数,每个析构函数只负责清除自己的成员。
最佳实践:即使析构函数没有工作要做,继承层次的根类也应该定义一个虚析构函数。因为在运行构造函数或析构函数的时候,对象都是后不完整的,为了适应这种不完整,编译器将对象的类型视为在构造或析构期间发生了变化。
13、纯虚函数
定义了纯虚函数后,用户不能创建该类的对象,并指出该纯虚函数仅仅只为了继承用,该版本的该函数没有任何意义。(注意,他的基类的该函数可以定义为有意义的,它只是为后代类型提供了可以覆盖的接口,但是这个类中的版本绝不会调用)
14、容器与继承
我们希望使用数组(或内置数组)保存因继承而相关联的对象。但是,对象不是多态的,这一事实将对容器用于继承层次中的类型有影响。
唯一可行的选择是使用容器保存对象的指针。但代价是需要用户面对管理对象和指针的问题,用户必须保证只要容器存在,被指向的对象就存在。如果对象是动态分配的,用户必须保证在容器消失时适当地释放对象。
15、C++中面向对象编程的一个颇具讽刺意味的地方是,不能使用对象支持面向对象编程,相反,必须使用指针或引用。但是指针或引用会加重类用户的负担。C++中一个通用的技术是定义包装类或句柄类。句柄类存储和管理基类指针。指针所指对象的类型可以变化,它既可以指向基类类型对象又可以指向派生类类型对象。用户通过句柄类访问继承层次的操作。因为句柄类使用指针执行操作,虚成员的行为将在运行时根据句柄实际绑定的对象的类型而变化。因此,句柄的用户可以获得动态行为但无须操心指针的管理。
16、复制未知类型
情况举例:
句柄类接收基类对象引用的构造函数。
问题:我们不知道给予构造函数的对象的实际类型。我们只能知道它是一个基类对象或者是一个派生类类型的对象,但是句柄类经常需要在不知道对象的确切类型时分配已知对象的新副本(即传进来的实参的副本)。
解决方案:这个问题的通用解决方案是定义虚操作进行复制,我们称该操作命名为clone,也就是说为了支持句柄类,需要从基类开始,在继承层次的每个类型中增加clone。
17、复制控制中的重载赋值操作符operator=时,在智能指针和句柄类中不需要强行防止自身赋值,但是在派生类中的赋值操作符必须防止自身赋值(if(this!=&org))
18、
Q1:为什么要将计数类型声明为指针类型
Q2:默认构造函数中p(0)为什么是初始化为0,而不是new 一个Item_base。
19、句柄类定义方案
#include <iostream>
#include <string>
#include "Item_base.h"
#include "Bulk_item.h"
using namespace std;
class Item_partner{
public:
Item_partner():p(0),use(newsize_t(1)){}
//我们希望句柄的用户创建自己的对象,在这些对象上关联句柄。构造函数将分配适当类型的新对象
//并将形参复制到新分配的对象中。这样句柄类将拥有对象并能够保证在关联到该对象的最后一个
//句柄类对象消失之前不会删除对象。
Item_partner(constItem_base& ip):p(ip.clone()),use(new size_t(1)){}
Item_partner(constItem_partner& org):p(org.p),use(org.use){++*use;}
Item_partner&operator=(const Item_partner& org){
++*org.use;
descr_Item_partner();
p= org.p;
use= org.use;
return*this;
}
~Item_partner(){descr_Item_partner();}
Item_base&operator*(){
if(p)
return*p;
else
throwlogic_error("unbound Item_partner.");
}
Item_base*operator->(){
if(p)
returnp;
else
throwlogic_error("unbound Item_partner.");
}
private:
Item_base*p;
size_t*use;
voiddescr_Item_partner(){
if(--*use== 0){
deletep;
deleteuse;
}
}
};
20、最佳编程实践:当需要定义<操作符来根据一个类的一部分来判定类对象的大小,但又同时需要用==来判定两个类对象的大小(所有部分属性都一样)。这时,我们可以指定一个函数(或函数对象)用作比较函数,如将附加的实参传给stable_sort以提供比较函数,来代替<操作符的使用。
比较器概念(使用带比较器的关联容器):比较器是一个函数指针,该函数接受两个容器的key_type类型的对象(或可以转为key_type的任意形参类型)并返回bool值,容器可以推断出这个类型。在我们的例子中(p509),比较器类型是接受两个const Item_partner引用并返回bool值得函数。
首先定义一个类型别名,作为该类型的同义词。
typedefbool (*Comp)(const Item_partner&,const Item_partner&);
这个语句将Comp定义为函数类型指针的同义词,该函数类型与我们希望用来比较Item_partner对象的比较函数相匹配。
第五部分 异常与优化处理
1、 异常与指针
抛出指针通常是个坏主意:因为抛出指针要求在对应处理代码存在的任意地方存在指针所指向的对象,也就是说该对象不能是局部对象。
抛出对指针的解引用时,无论对象的实际类型是什么,异常对象的类型都与指针的静态类型相匹配。如果该指针是一个指向派生类对象的基类类型指针,则那个对象将被分隔,只抛出基类部分。
2、 函数测试块与构造函数
为了处理构造函数初始化式的异常,必须将构造函数编写成函数测试块。可以使用函数测试块将一组catch子句与函数联成一个整体。作为例子,我们可以将第16章的Handle构造函数包装在一个用来检测new中失败的测试块当中。
template<class T> Handle<T>::Handle(T *p)
try:ptr(p),use(new size_t(1))
{
//emptyfunction body
}catch(const bad_alloc &e)
{
handle_out_of_memory(e);
}
3、 exception类型所定义的唯一操作是一个名为what的虚成员,该函数返回const char*对象,它一般返回用来在抛出位置构造异常对象的信息。因为what是虚函数,如果捕获了基类类型引用,对what函数的调用将执行适合异常对象的动态类型的版本。
4、 用类管理资源分配
通过定义一个类来封装资源的分配和释放,可以保证正确释放资源,这一技术常称为“资源分配即初始化”,简称RAII。也就是说,我们应该设计资源管理类,一遍构造函数分配资源而析构函数来释放资源。想要分配资源的时候,就定义该类类型的对象。如果不发生异常,就在获得资源的对象超出作用域的时候释放资源。更为重要的是,如果在创建了对象之后但在它超出作用域之前发生异常,那么,编译器保证撤销该对象,作为展开定义对象的作用域的一部分。
5、 标准库auto_ptr类是一个RAII应用的一个例子,其接受一个类型形参的模板,并为动态分配的对象提供异常安全,该类在头文件memory中进行定义。
每个auto_ptr对象绑定到一个对象或者指向一个对象。当auto_ptr对象指向一个对象的时候,可以说它“拥有”该对象。当auto_ptr对象超出作用域或者另外撤销的时候,就会自动回收auto_ptr所指向的动态分配对象。
需要注意的是,auto_ptr只能用于管理从new返回的一个对象,它不能管理动态分配的数组。因为auto_ptr在被复制或赋值时,有不寻常的行为,它不是形成一个右值的副本,而是将右值的指针的所有权转移到自己身上,同时右值编程未绑定的。因此,不能将auto_ptr存储在标准库容器类型中。(这很好理解,因为复制两者的值竟然是不同的)
6、 测试auto_ptr对象是否绑定到了一个对象
if(auto_ptr.get())
*auto_ptr =1024;
这里应该只用get查询auto_ptr对象或者使用返回的指针值,不能用get作为创建其他auto_ptr对象的实参。
7、 auto_ptr的缺陷
a) 不要使用auto_ptr对象保存指向静态分配对象的指针,我们应该保存指向动态分配对象的指针,否则结果将是未定义的。
b) 永远不要使用两个auto_ptr对象指向同一对象。
c) 不要使用auto_ptr对象保存指向动态分配数组的指针,因为它使用普通的delete操作符,而不是使用数组的delete[]操作符,只释放了一个对象。
d) 不要将auto_ptr对象存储在容器中。
8、 虚继承
需继承是一种机制,类通过虚继承指出它希望共享其虚基类的状态。在虚继承下,对给定虚基类,无论该类在派生层次中作为虚基类出现多少次,只继承一个共享的基类子对象。共享的基类子对象称为虚基类。
第六部分 模板与泛型编程
1、 函数模板
a. 模板定义以关键字template开始,后接模板形参表(templateparameter list),模板参数表是用尖括号括住的一个或多个模板形参的列表,形参之间以逗号分隔。
示例:
Template <typename T>
int compare(const T &v1,const T &v2)
{
If(v1<v2) return -1;
If(v2<v1)return 1;
Return 0;
}
b. inline函数模板
函数模板可以用与非模板函数一样的方式声明为inline。说明符放在模板形参表之后、返回类型之前,不能放在关键字template之前。
//ok
template <typename T>
inline T min(const T&,const T&)
//error
inline Template <typename T> T min(const T&,constT&)
2、 类模板
template <class Type> class Queue
{
Public:
Type&front();
}
类模板也是模板,因此必须以关键字template开头,后接模板形参表。Queue模板接受一个名为type的模板类型的形参。(举例)此时,类模板不定义类型,只有特定的实例才定义了类型。(就是class Queue后面不跟int类型之类的)
使用类模板
与调用函数模板形成对比,使用类模板时,必须为模板形参显示指定实参,然后编译器使用实参来实例化这个类的特定类型版本。实质上,编译器也是用用户提供的实际特定类型代替type,重新编写Queue类,这点和函数模板是一致的。
3、 在函数模板内部完成的操作限制了可用于实例化该函数的类型。程序员的责任是,保证用作函数实参的类型实际上支持所用的任意操作,以及保证在模板使用哪些操作的环境中哪些操作运行正常。
这就要求,我们在编写模板代码时,对实参类型的要求尽可能地少是很有益的。
虽然简单,但它说明了编写泛型代码的两个重要原则:
l 模板的形参是const引用
l 函数体中的测试只用<比较
4、
5、