首页 > 代码库 > C++学习笔记
C++学习笔记
C++ Primer Plus中文版第5版(632)
1.char 作为数值类型,则unsigned char表示的数据范围为0~255 而signed char范围为-128~127。
2.cin.get(name,size);是不接收换行符,把它留在了输入流中。cin.get()只接收一个字符,包括换行符。(getline()用法类似)
当get()读取空行后将设置失效位,接下来的输入将被阻断,可用cin.clear()来恢复输入。
3.枚举只定义了赋值操作符(在体外只能把enum值赋给enum变量),没有定义算术运算。
4.静态,有链接性:全局变量(分内外链接性,static为内部链接)。当file1.cpp中定义了全局变量 int tom;和static int m=0;而在file2.cpp中想用file1.cpp中的
tom的数据,同时想在file2.cpp中定义自己的m.file2.cpp中可以这样声明:extern int tom; int m;想要在当前文本中使用另一个
文本的同名变量数据,则必须编译两个文本产生后缀为 .o 的文件和在当前文本中用extern声明想用的变量(一般是不可以初始化,但extern cost 声名的变量可以初始化),这样才能成功链接使用。
5.用mutable声明的变量m,即使是const变量也可以修改该变量,如:结构体中定义了nutable变量,再定义一个const的结构体变量node,则node.m是可以被重新修改的。
6.外部定义静态属性的const或static变量的链接性为 内部的(意为在所有链接起的文件都可以使用相同的外部声明,属文件私有)。如果希望内部链接性为外部的,则
可以使用extern关键字来覆盖默认的内部链接性。
7.函数也有链接性,默认性况下函数的链接性为外部的。可以用关键字static将函数的链接性设置为内部的(仅在本文件中可见),使之只能在一个文件中使用。必须同时在原型声明和函数定义中使用该关键字。
8.C++有一个“单定义规则”,即对于每个非内联函数,程序中只能包含一个定义。对于链接性为外部的函数来说,这意味着在多文件程序中,只能有一个文件
包含该函数的定义,但使用该函的每个文件都应包含其函数原型。 内联函数不受这项规则的约束,这允许程序员能够将内联函数的定义放在头文件中。
这样,包含了头文件的每个文件都有内联函数的定义。不过,C++要求同一个函数的所有内联定义都必须相同。
9.在C++程序的某个文件中调用一个函数,如果该文件中的函数原型指出该函数是静态的,则只在该文件中查找函数定义。若非静态的,将在所有的程序文
件中查找,如果找到有两个定义,编译器将发出错误消息;如在程序文件中没找到,则将在库中搜索。
10.布局new 的一种使用:将己分配的内存再分给其他变量。
#include<new>//使的头文件
struct node{int a;char b[10];};
node *Node;
int *p;
char buffer[100];
Node=new (buffer)node[2];//将buffer中分配静态空间给结构node
p=new (buffer)int[20];//将buffer中分配静态空间给一个包含20个元素的int数组;
/*上述两个new操作,(1)分配buffer的内存都是从相同的起始地址开始分配,要想Node与p不操作同一内存,
可以这样:p=new (buffer+2*sizeof(node))int[20]; (2)buffer是静态内存,所以不能用delete释放p和Node,
要想释放,则buffer必须为new一般分配的动态内存。*/
11.名称空间可以是全局的,也可以位于另一个名称空间中,但不能位于代码块中。所以默认情况下,在名称空间中声明的名称的链接性为外部的(除非它引用了常量)。
12.名称空间定义完后,可以再次添加变量名或定义,如想在己存在的名称空间jack中再添加变量age:namespace jack{int age;}
(注:先用编译指令using namespace jack;后向名称空间jack添加age,那么age也可以直接使用age变量,而不需声明。)
当要用名称空间jack中的age时,可以jack::age=19,或using jack::age; age=19(将age变量添加到它所属声明区域中)。
当用using编译指令:using namespace 名称空间名。这一次性声明,但有一缺点:一个程序中使用多个名称空间,这些名称空间中
有相同的变量名,那么会隐藏之前出现相同的名称。可通过作用域分明。 用using声明导入变量时,只能存在一个同名变量。
13.(1)名称空间声明进行嵌套:一个名称空间声明element中可以包含另一个名称空间的声明fire。
namespace elements
{ namespace fire
{ int flame;....}
float water;
}
using编译指令使用内部的名称用:suing namespace elements::fire;
(2)名称空间中使用using 编译指令和suing 声明,如下所示:
namespace myth
{
using jake::age;
using namespace elements;//using 编译指令,myth中就有了elements中的名称
using std::cout; using std::cin;
}
假设要访问jake::age。由于jake::age现在位于名称空间myth中,因此可以这样访问:myth::age=20则jake::age也是等于20
(3)using 编译指令具有传递性,如:using namespace myth;后using namespace elements也编译了。
(4)可以给名称空间创建别名:namespace myth_other_name=myth;
(5)未命名的名称空间只能在所属文件中使用,潜在作用域为:从声明点到该声明区域末尾(空间中的名称相当内部链接性变量)。
因没有名称,所以不能显示地using 编译指令和suing声明。
14.解释为什新头文件iostream要与名称空间std同时使用:因为新头文件iostream中使用了std名称空间定义,#include<iostrea>只是将std名称空间导入到当前文件中。
而旧头文件iostream.h中没有使用名称空间定义。
15.类声明中默认访问控制权限为private. 类的构造函数中的参数名不能与成员变量名相同,为避免这种混乱,通常在成员变名中使用m_前缀。
16.为防头文件被多次包含,头文件中可以使用:#ifndef MYFILE_H_
#define MYFILE_H_
..........
#endif
17.(1)创建类对象另一种方法(初始化):
Student s=Student("name",20);这样调用构造函数可能创建一个临时对象(也可能不会),然后将该临时对象复到s对象中,后该临时对象
调用析构函数,释放临时内存。(可能立刻册除,也可能会等一段时间)。
(2)赋值:s=Student("name",20); 赋值语句中使用构造函数总会导致在赋值前创建一个临时对象。
(3)注意:如果既可通过初始化,也可通过赋值来设置对象的值,应釆用初始化方式。通常这种方式的效率更高。
18.类的const对象:调用函数时只能调用常成员函数,不能调用非 常成员函数。(原因:常对象不能被修改,而非常成员函数可能会修改对象的值,所以不能被调用)
19.接受一个参数的构造函数允许使用赋值句法来将对象初始化一个值:Classname object=value; <==> Classname object(value);
20.用new创建的对象,必须用delete来析构,这样才会调用类中的析构函数(不是用new创建的对像自动调用析构函数)。一个类只能有一个析构函数,不带任何参数。
21.警告:要创建类对象数组,则这个类必须有默认构造函数,如果没有则会出错。
22.在类中定义一个常量的三种方法:(1)在类中声明枚举enum (2)使用static定义常量(变量的值不能变须再加一个const修饰:static const int CONST;),类外初始化:static不必写出。
(3)用非静态的const声明成员变,但这样必须包含构造函数(要调用默认的则必须有默认构造函数,其他要调用的构造函数同理),
且每个构造函数都必须初始化用const声明的成员变量,而且必须是这样的形式:Classname(形参表):CONST(初始化值){....}
23.随机函数:按一种算法用于一个初始种子值业获得随机数。
(1)包含在头文件cstdlib(以前为stdlib.h) (2)函数srand(seed)是设种子用的;函数rand()取随机数。如果自己每次取数不设种子,那么每取出的数将作为下次取数的种子。
seed可以设置成time(0),这样种子seed就可以随时不同,那么取随机数也就不同,time()函数包含在头文件time.h中。
24.关键字explicit,用来关闭对象自动转换特性。当类中有一个参数的构造函数时:explicit Students(string name){.....}//这样在创建对Students对象s后,不能:s=name;
但如果没有explicit关键字时,可行:s=name。强制转换是可以的,如:s=(Students)name;一般而言:只有一个这样的成员变量才定义这样的构造函数。
25.创建转换函数:例如:可将Students类对象转成string类对象的Students类成员函数:operator string(){... }//假如将double类型转成int型通过这类型的方法,是会四舍五入取值
要求:(1)转换函数必须是类方法 (2)转换函数不能指定返回类型 (3)转换函数不能有参数
警告:应小心使用隐式转换函数。通常,最好先择仅在被显式地调用时才会执行的函数。
26.注意:如果一个类中重载了+运算符,第二个是double类型,并在类中又定义了operator double()成员函数,将造成混乱,因为在该类对象加上double类型值时,就有两种
解释,一种是调用重载+运算符执行该类加法,而另一种是将该类对象转换成double类型,再执行double加法。
27.在执行main()之前调用其他函数做法:因为全局变量是在编译时就创建了,所以可以定义一个全局的类对象,并在类的构造函数中调用一些想在main()之前调用的函数。
28.如果类中有引用变量,则必须包含可调用的构造函数,必须初始化引用成员变量(可对自身进行引用),而且初始化时必须在构造函数打冒号后初始化:Students(int ok):yingyou(ok){...}//yingyou为类的引用成员变量。
29.除虚基类外,类在初始化时,只能将值传递给直接基类。
30.记住:如果要在派生类中重新定义基类的方法,通常应将基类方法声明为虚拟的,这样,程序将根据所指的对象类型而不是引用或指针的类型来选择方法版本 为基类声明一个虚拟析构函数也是一种惯例。
31.用类名调用类的成员情况有二:(1)不须要在派生类中就可以用类名直接调用静态成员变量 (2)要用类名调用成员函数,只有在派生类成员函数中,父类可以用自己的类名调用自身的成员函数。
32.为何需要 虚析构函数:当一个基类对象指针指向了子类对象,而这时子类对象新增的成员变量是用new分配的内存,但又想通过析构基类对象同时来析构子类新增的成员变量,那么基类的虚拟类型将发
挥作用,动态联编性,调用析构函数会从该基类指针对象所指的子类析构函数开始调用直至该基类,这与一般的虚函数用法一样。如果不是虚析构函数,那么只调用该基类的析构函数。
33.虚函数的工作原理:通常,编译器处理虚函数的方法是:给每个类对象添加保存了一个隐藏成员表,即指向函数地址的数组指针。这种数组称为虚函数表,它存储了类对象进行声明的虚函数的地址。
每个类对象都有自己的虚函数表,如果在派生类中有新定义的虚函数,则在创建该对象时也将添加到虚函数表中。
调用虚函数时:程序将查看存储在对象中的vtbl地址,然后转向相应的函数地址表。如果使用类声明中定义的第三个虚函数,则程序将使用该数组中的第三个函数地址,并执行该地址的函数。
总之,使用虚函数时,在内存和执行速度方面有一定的成本,包括:
(1)每个对象都将增大,增大量为存储地址的空间。(2)对每个类,编译器都创建一个虚函数地址表(数组)。(3)每次虚函数调用都需要执行一步额外的操作,查地址,有时间开销。
比较:虽然非虚函数的效率比虚函数稍高,但不具备动态联编功能。
34.注意事项:(1)基类方法中使用关键字virtual 可使该方法在基类以及所有派生类中是虚拟的。
(2)如果使用指向对象的引用或指针来调用虚方法,具有动态联编性,而不是对象的引用或指针来调用虚方法,则没有动态联编性。
(3)如果定义的类将被用作基类,则应将那些要在派生类中重新定义的类方法声明为虚函数。
35.更多注意:(4)构造函数不能是虚函数 (5)析构函数应当是虚函数,除非类不用做基类。(6)友元函数不能是虚函数,因为友元函数不是类成员
( 7 )经验规则:第一,如果重新定义继承的方法,应确保与原来的原型完全相同,但如果返回类型是基类引用或指针,则可以修改为指向派生
类的引用或指针,这种特性被称为返加回型协变,因为允许返回类型随类 类型的变化而变化。
第二,如果基类声明的函数被重载了,则应在派生类中重新定义。如不重载,则派生类对象只能调用最近一次被重载的函数。
36.(1)需要实例化的类,则类中的虚函数必须有定义。而如果该类不实例化,仅仅声明虚函数的类且虚函数没有实现,该类编译是可以通过的!
(2)纯虚函数出现在接口类中,并赋值为0,不要为该函数分配函数地址,从而阻止类的实例化!纯虚函数是没有定义的,如果实现了也不是纯虚函数啦!
(3)一般的成员函数可以只有声明,前提是在应用中不能调用该函数,否则会因找不到定义产生连接错误!
37.实例化类的虚函数必须有定义,原因如下:有虚函数作为成员函数的类, 它的实例化---对象, 在运行过程分配到的内存不止是它的成员数据, 还有一个指向该类虚函数表(vtable)的指针, 虚函
数表中的每个数据项都是一个虚函数的入口地址; 如果一个对象的虚函数只有声明而没有实现, 就会导致这个虚函数表找不到本应作为其数据项之一的虚函数的入口地址, 虚函数表在运行前不能装载完成, 所以产生连接错误!
38.派生类使用基类的友元函数方法:
例如:基类和派生类都重载了输出运算符,现在使用派生类的对象调用基类的友元输出运算符:
基类友元函数:friend std::ostream & operator<<(std::ostream &os,const BaseClass bc)
{ os<<bc; return os; }
子类友元函数调用基类友元函数:friend std::ostream &operator<<(std::ostream &os,const ChildClass cc)
{ os<<(const BaseClass &)cc; os<<新增成员变量; return os;}
39.复制构造函数:类中定义了构造函数,这个构造函数中只有一个参数。而只有在创建对象时才能用=来复值指定的成员变量。
如:BaseClass(Class &k){kk=k;} Class k; BaseClass aa=k;//隐式转换
但如果在构造函数前加关键字:explicit,则不能进行隐式转换,可以显示转换。
40.再次强调:(1)在编写函数的参数时,应按引用而不是按值来传递对象的方式,这样做可以提高效率。因为按值传递时需要调用复制构造函数,然后调用析构函数,
这些调用需要时间。如不修改对象,应将参数声明为const引用。
(2)函数返回对象和返回引用对象:相同点:都生成临时拷贝。 不同:返回引用与按引用传递对象相似,在调用和被调用函数对同一个对象进行操作。
注意:函数不能返回在函数中创建的临时对象的引用,因为当函数结束时,临时对象将消失,因此这种引用将是非法的。
41.const函数(将const放在函数形参表后面),只是不能修改调用该函数的对象(*this). 当函数引用(或指针)返回对象时,函数返回类型必须兼容对象类型。如果对象是const类型,则函数反回类型也必须是const.
42.编译器生成的成员函数:默认构造函数,复制构造函数,赋值操作符。
43.公有继承的考虑因素:(1)所属关系 (2)构造函数,析构函数,赋值操作符函数与友元函数是不能被继承。
44.警告:如果类有间接虚基类,则除非只需使用该虚基类的默认构造函数,否则必须显式地调用该虚基类的某个构造函数。
45.调用构造函数的顺序:(1)一个派生类没有直接虚基类:A:先调用基类的构造函数再子类;B:调用基类时,按照派生类声明继承基类的顺序调用基类的构造函数。
(2)一个派生类有直接虚基类:A:先调用基类的构造函数再子类;B:调用基类时:b1.先调用虚基类后非虚基类,b2.再按声明的顺序被调用。
46.模板:部分具体化:即部分限制模板的通用性。例如:template<class T1,class T2> class Pair {......}; template<class T1> class Pair<T1,int>{......};
47.template<typename T>
class beta{
public:
template<typename U>
U blab(U u,T t);
};
template<typename T>
template<typename U>
U beta<T>::blab(U u,T t){ return u+t;}
48.template<template <typename T>class Thing, typename U, typename V>
class Crab
{ private: Thing<U>S1; Thing<V>S2; .......};
Thing是模板类类型,如果实例化时:Crab<Stack,int,double>nebula; //Stack是模板类,Thing将被替换成Stack ,里面的成员变量变成Stack<int>S1, Stack<double>S2 ;
49.模板类的友元函数有三类:A.非模板友元函数(即友元函数是不带参数或带参的类型都是要具体化的)
B.约束(bound)模板友元,即友元的类型取决于类被实例化时的类型。
C.非约束(undound)模板友元,即友元的所有具体化都是类的每一个具体化的友元。
50. A类:非模板友元函数:template<typename T>
class HasFriend
{ friend void report(HasFriend<T> &); };
在定义友元函数时,必须根据模板类实例化对象时传送的模板类参数类型。具体化定义,如:
要定义一个对象HasFriend<double>object;则友元函数这样定义:void report(HasFriend<double> &f){....}
B类:修改前一个上面的范例,使友元函数本身成为模板。使类的每一个具体化都获得与友元匹配的具体化。包含以下三步:
1.首先,在类定义的前面声明每个模板函数: template <typename T> void counts(); template <typename T> void report(T &);
2.然后在函数中再次将模板声明为友元。这些语句根据类模板参数的类型声明具体化:
template<class TT>
class HasFriend
{ friend void counts<TT>();
friend void report<>(HasFriend<TT> &);
... };
上述声明中的<>指出这是模板具体化。对于report(),<>可以为空,这是因为可以从函数参数推断出模板类型参数(HasFriend<TT>)。不过,
也可以使用report<HasFriend<TT> >(HasFriend<TT> &)。但counts()函数没有参数,因此必须使用模板参数句法(<TT>)来指明其具体化。
还需要注意的是,TT是HasFriend类的参数类型。
3.程序必须为友元提供模板定义,例如:
template <typename T>
void counts() { cout << HasFriend<T>::x << endl;}
template <typename T>
void report(T & hf) {cout << hf.x << endl;}
调用时,counts()需要提供具体化参数类型。report()只需直接调用,例如:
counts<int>(); //调用HasFriend<int>的友元函数
HasFriend<double> hfdb;
report(hfdb); //调用HasFriend<double>的友元函数
C类: 模板类的非约束模板友元函数
通过在类内部声明友元函数模板,可以创建非约束友元函数,即每个函数具体化都是每个类具体化的友元:
template <typename T>
class ManyFriend
{...
template <typename C,typename D> friend void show(C &,D &);
};
在类外定义该友元:
template <typename C,typename D>
void show(C & c,D & d){ ... }
假如创建ManyFriend<int>类对象(hfi1)和ManyFriend<double>类对象(hfi2),传递给show(),那么编译器将生成如下具体化定义:
void show<ManyFriend<int> &,ManyFriend<double> &>
(ManyFriend<int> & c,ManyFriend<double> & d){ ... }
51.嵌套类:被包含在一个类中,访问权限与一般语法一样,嵌套类可以访问包含它的类的成员。
52.终止程序的函数:std::abort()[在头文件 cstdlib 中],exit(0)。
53.异常极其重要的一点:程序进行堆栈解退以回到能够捕获异常的地方时,将释放堆栈中的自动存储变量。如果变量是类对象,将为该对象调用析构函数。
54.捕捉异常虽然用引用对象,但还是会创建拷贝,这是件好事,因为抛出异常的函数执行完后,就会释放抛出的异常对象。
那其中的好处就是:基类引用可以执行派生类对象。与派生类型兼容。(所以通过排列catch块的顺序,让处理异常更有针对性。)
55.exception类用作其他异常类的基类:
1>logic_error类(继承自exception类) :通过合理的编程可以避免逻辑错误。在头文件<stdexcept>中定义,逻辑异常表明存在可以通过编程修复的问题。下面都是它的派生类:
domain_error:定义域或值域错误
invalid_error:传递的值不符合要求
length_error:没有足够的空间执行所需操作,例如string类的append()方法
out_of_bounds :指示索引错误,例如数组下标索引
2>runtime_error类(继承自exception类):在运行期间发生但难以预计和防范的错误。在头文件<stdexcept>中定义,运行时异常表明无法避免的问题,下面派生的类:
range_error:计算的结果不在函数允许的范围内,但没有上溢,也没有下溢
overflow_error:上溢错误
underflow_error:下溢错误
3>bad_alloc类(继承自exception类):在头文件<new>中。当new出现内存分配问题时,C++提供了两种方式处理:
i>返回一个空指针 ii>引发bad_alloc异常
56.异常何时会迷失方向:
1>意外异常:异常,如果是在带异常规范的函数中引发的,则必须与规范列表里的某个异常匹配,若没有匹配的,则为意外异常,默认情况下,会导致程序异常终止
2>未捕获异常:异常如果不是在函数中引发的(或者函数没有异常规范),则它必须被捕获。如果没被捕获(没有try块或没有匹配的的catch块),则为未捕获异常。默认情况下,将导致程序异常终止
3>可以修改这种默认设置(存在意外异常或未捕获异常)
3.1>未捕获异常:此异常不会导致程序立刻终止(terminate()、set_terminate()都在exception头文件中声明)
程序首先调用函数terminate()---->默认情况下,terminate()调用abort()函数,
可以通过set_terminate()函数指定terminate()调用的函数,修改这种行为
若set_terminate()函数调用多次,则terminate()函数将调用最后一次set_terminate()设置的函数
例如:
set_terminate(MyQuit);
void MyQuit(){...}
此时若出现未捕获异常,程序将调用terminate(),而terminate()将调用MyQuit()
3.2>意外异常:
发生意外异常时,程序将调用unexcepted()函数--->unexpected()将调用terminate()函数--->terminate()在默认情况下调用abort()函数
可以通过set_unexcepted()函数,修改这种默认行为,但unexpected()函数受限更多
i>通过terminate()、abort()、exit()终止程序
ii>引发异常
#引发的异常与原来的异常规范匹配,则可以用预期的异常取代意外异常
#引发的异常与原来的异常不匹配,且异常规范中没有包括bad_exception类型(继承自exception类),则程序将调用terminate()
#引发的异常与原来的异常不匹配,且原来的异常规范中包含了bad_exception类型,则不匹配的异常将被bad_exception异常所取代
如果要捕获所有异常,步骤如下:
#include<exception>
using namespace std;
void myUnexception()
{
throw bad_exception();
}
set_unexcepted(myUnexception)
...
function()throw(...,bad_exception);
try{}
catch(bad_exception &be){}
57.动态 判断对象是什么类型 的方法,两个重要的 RTTI 运算符的使用方法,它们是 typeid 和 dynamic_cast。
大多都主张在设计和开发中明智地使用虚拟成员函数,而不用 RTTI 机制。但是,在很多情况下,虚拟函数无法克服本身的局限。每每涉及到处理异类容器和根基类层次(如 MFC)时, 不可避免要对对象类型进行动态判断,也就是动态类型的侦测。如何确定对象的动态类型呢?答案是使用内建的 RTTI 中的运算符:typeid 和 dynamic_cast。
1> 利用 运算符 typeid 可以获取与某个对象关联的运行时类型信息。typeid 有一个参数,传递对象或类型名。因此,为了确定 x 的动态类型是不是Y,可以用表达式:
typeid(x) == typeid(Y)实现:#include <typeinfo> // typeid 需要的头文件 例如:typeid(*pfile)==typeid(MediaFile)
存在的问题:如果指针 pfile是MediaFile 的派生类,则判断为false,类型兼容判断那么这类问题就要用到dynamic_cast
2> dynamic_cast用它来确定某个对象是 MediaFile 对象还是它的派生类对象。dynamic_cast 常用于从多态编程基类指针向派生类指针的向下类型转换。它有两个参数:一个是类型名;另一个是多态对象的指针或引用。其功能是在运行时将对象强制转换为目标类型并返回布尔型结果。也就是说,如果该函数成功地并且是动态的将 *pfile 强制转换为 MediaFile,那么 pfile的动态类型是 MediaFile 或者是它的派生类。否则,pfile 则为其它的类型:
void menu::build(const File * pfile)
{
if (dynamic_cast <MediaFile *> (pfile))
{
// pfile 是 MediaFile 或者是MediaFile的派生类 LocalizedMedia
add_option("play");
}
else if (dynamic_cast <TextFile*> (pfile))
{
// pfile 是 TextFile 是TextFile的派生类
add_option("edit");
}
}
存在的问题:与 typeid 相比,dynamic_cast 用时多些,为了确定是否能完成强制类型转换,dynamic_cast必须在运行时进行一些转换细节操作。因此在使用 dynamic_cast 操作时,应该权衡对性能的影响。指针强制转化失败后可以比较指针是否为零,而引用却没办法,所以引用制转化失败后抛出异常
58.C++的auto_ptr所做的事情,就是动态分配对象以及当对象不再需要时自动执行清理。auto_ptr在构造时获取对某个对象的所有权(ownership),在析构时释放该对象。我们可以这样使用 auto_ptr来提高代码安全性:int* p = new int(0); auto_ptr<int> ap(p); 从此我们不必关心应该何时释放p, 也不用担心发生异常会有内存泄漏。
这里我们有几点要注意:
1) 因为auto_ptr析构的时候肯定会删除他所拥有的那个对象,所有我们就要注意了,一个萝卜一个坑,两个auto_ptr不能同时拥有同一个对象。像这样:
int* p = new int(0);
auto_ptr<int> ap1(p);
auto_ptr<int> ap2(p);
因为ap1与ap2都认为指针p是归它管的,在析构时都试图删除p, 两次删除同一个对象的行为在C++标准中是未定义的。所以我们必须防止这样使用auto_ptr.
2) 考虑下面这种用法:
int* pa = new int[10];
auto_ptr<int> ap(pa);
因为auto_ptr的析构函数中删除指针用的是delete,而不是delete [],所以我们不应该用auto_ptr来管理一个数组指针。
3) 构造函数的explicit关键词有效阻止从一个“裸”指针隐式转换成auto_ptr类型。
4) 因为C++保证删除一个空指针是安全的, 所以我们没有必要把析构函数写成:
~auto_ptr() throw()
{
if(ap) delete ap;
}
1.char 作为数值类型,则unsigned char表示的数据范围为0~255 而signed char范围为-128~127。
2.cin.get(name,size);是不接收换行符,把它留在了输入流中。cin.get()只接收一个字符,包括换行符。(getline()用法类似)
当get()读取空行后将设置失效位,接下来的输入将被阻断,可用cin.clear()来恢复输入。
3.枚举只定义了赋值操作符(在体外只能把enum值赋给enum变量),没有定义算术运算。
4.静态,有链接性:全局变量(分内外链接性,static为内部链接)。当file1.cpp中定义了全局变量 int tom;和static int m=0;而在file2.cpp中想用file1.cpp中的
tom的数据,同时想在file2.cpp中定义自己的m.file2.cpp中可以这样声明:extern int tom; int m;想要在当前文本中使用另一个
文本的同名变量数据,则必须编译两个文本产生后缀为 .o 的文件和在当前文本中用extern声明想用的变量(一般是不可以初始化,但extern cost 声名的变量可以初始化),这样才能成功链接使用。
5.用mutable声明的变量m,即使是const变量也可以修改该变量,如:结构体中定义了nutable变量,再定义一个const的结构体变量node,则node.m是可以被重新修改的。
6.外部定义静态属性的const或static变量的链接性为 内部的(意为在所有链接起的文件都可以使用相同的外部声明,属文件私有)。如果希望内部链接性为外部的,则
可以使用extern关键字来覆盖默认的内部链接性。
7.函数也有链接性,默认性况下函数的链接性为外部的。可以用关键字static将函数的链接性设置为内部的(仅在本文件中可见),使之只能在一个文件中使用。必须同时在原型声明和函数定义中使用该关键字。
8.C++有一个“单定义规则”,即对于每个非内联函数,程序中只能包含一个定义。对于链接性为外部的函数来说,这意味着在多文件程序中,只能有一个文件
包含该函数的定义,但使用该函的每个文件都应包含其函数原型。 内联函数不受这项规则的约束,这允许程序员能够将内联函数的定义放在头文件中。
这样,包含了头文件的每个文件都有内联函数的定义。不过,C++要求同一个函数的所有内联定义都必须相同。
9.在C++程序的某个文件中调用一个函数,如果该文件中的函数原型指出该函数是静态的,则只在该文件中查找函数定义。若非静态的,将在所有的程序文
件中查找,如果找到有两个定义,编译器将发出错误消息;如在程序文件中没找到,则将在库中搜索。
10.布局new 的一种使用:将己分配的内存再分给其他变量。
#include<new>//使的头文件
struct node{int a;char b[10];};
node *Node;
int *p;
char buffer[100];
Node=new (buffer)node[2];//将buffer中分配静态空间给结构node
p=new (buffer)int[20];//将buffer中分配静态空间给一个包含20个元素的int数组;
/*上述两个new操作,(1)分配buffer的内存都是从相同的起始地址开始分配,要想Node与p不操作同一内存,
可以这样:p=new (buffer+2*sizeof(node))int[20]; (2)buffer是静态内存,所以不能用delete释放p和Node,
要想释放,则buffer必须为new一般分配的动态内存。*/
11.名称空间可以是全局的,也可以位于另一个名称空间中,但不能位于代码块中。所以默认情况下,在名称空间中声明的名称的链接性为外部的(除非它引用了常量)。
12.名称空间定义完后,可以再次添加变量名或定义,如想在己存在的名称空间jack中再添加变量age:namespace jack{int age;}
(注:先用编译指令using namespace jack;后向名称空间jack添加age,那么age也可以直接使用age变量,而不需声明。)
当要用名称空间jack中的age时,可以jack::age=19,或using jack::age; age=19(将age变量添加到它所属声明区域中)。
当用using编译指令:using namespace 名称空间名。这一次性声明,但有一缺点:一个程序中使用多个名称空间,这些名称空间中
有相同的变量名,那么会隐藏之前出现相同的名称。可通过作用域分明。 用using声明导入变量时,只能存在一个同名变量。
13.(1)名称空间声明进行嵌套:一个名称空间声明element中可以包含另一个名称空间的声明fire。
namespace elements
{ namespace fire
{ int flame;....}
float water;
}
using编译指令使用内部的名称用:suing namespace elements::fire;
(2)名称空间中使用using 编译指令和suing 声明,如下所示:
namespace myth
{
using jake::age;
using namespace elements;//using 编译指令,myth中就有了elements中的名称
using std::cout; using std::cin;
}
假设要访问jake::age。由于jake::age现在位于名称空间myth中,因此可以这样访问:myth::age=20则jake::age也是等于20
(3)using 编译指令具有传递性,如:using namespace myth;后using namespace elements也编译了。
(4)可以给名称空间创建别名:namespace myth_other_name=myth;
(5)未命名的名称空间只能在所属文件中使用,潜在作用域为:从声明点到该声明区域末尾(空间中的名称相当内部链接性变量)。
因没有名称,所以不能显示地using 编译指令和suing声明。
14.解释为什新头文件iostream要与名称空间std同时使用:因为新头文件iostream中使用了std名称空间定义,#include<iostrea>只是将std名称空间导入到当前文件中。
而旧头文件iostream.h中没有使用名称空间定义。
15.类声明中默认访问控制权限为private. 类的构造函数中的参数名不能与成员变量名相同,为避免这种混乱,通常在成员变名中使用m_前缀。
16.为防头文件被多次包含,头文件中可以使用:#ifndef MYFILE_H_
#define MYFILE_H_
..........
#endif
17.(1)创建类对象另一种方法(初始化):
Student s=Student("name",20);这样调用构造函数可能创建一个临时对象(也可能不会),然后将该临时对象复到s对象中,后该临时对象
调用析构函数,释放临时内存。(可能立刻册除,也可能会等一段时间)。
(2)赋值:s=Student("name",20); 赋值语句中使用构造函数总会导致在赋值前创建一个临时对象。
(3)注意:如果既可通过初始化,也可通过赋值来设置对象的值,应釆用初始化方式。通常这种方式的效率更高。
18.类的const对象:调用函数时只能调用常成员函数,不能调用非 常成员函数。(原因:常对象不能被修改,而非常成员函数可能会修改对象的值,所以不能被调用)
19.接受一个参数的构造函数允许使用赋值句法来将对象初始化一个值:Classname object=value; <==> Classname object(value);
20.用new创建的对象,必须用delete来析构,这样才会调用类中的析构函数(不是用new创建的对像自动调用析构函数)。一个类只能有一个析构函数,不带任何参数。
21.警告:要创建类对象数组,则这个类必须有默认构造函数,如果没有则会出错。
22.在类中定义一个常量的三种方法:(1)在类中声明枚举enum (2)使用static定义常量(变量的值不能变须再加一个const修饰:static const int CONST;),类外初始化:static不必写出。
(3)用非静态的const声明成员变,但这样必须包含构造函数(要调用默认的则必须有默认构造函数,其他要调用的构造函数同理),
且每个构造函数都必须初始化用const声明的成员变量,而且必须是这样的形式:Classname(形参表):CONST(初始化值){....}
23.随机函数:按一种算法用于一个初始种子值业获得随机数。
(1)包含在头文件cstdlib(以前为stdlib.h) (2)函数srand(seed)是设种子用的;函数rand()取随机数。如果自己每次取数不设种子,那么每取出的数将作为下次取数的种子。
seed可以设置成time(0),这样种子seed就可以随时不同,那么取随机数也就不同,time()函数包含在头文件time.h中。
24.关键字explicit,用来关闭对象自动转换特性。当类中有一个参数的构造函数时:explicit Students(string name){.....}//这样在创建对Students对象s后,不能:s=name;
但如果没有explicit关键字时,可行:s=name。强制转换是可以的,如:s=(Students)name;一般而言:只有一个这样的成员变量才定义这样的构造函数。
25.创建转换函数:例如:可将Students类对象转成string类对象的Students类成员函数:operator string(){... }//假如将double类型转成int型通过这类型的方法,是会四舍五入取值
要求:(1)转换函数必须是类方法 (2)转换函数不能指定返回类型 (3)转换函数不能有参数
警告:应小心使用隐式转换函数。通常,最好先择仅在被显式地调用时才会执行的函数。
26.注意:如果一个类中重载了+运算符,第二个是double类型,并在类中又定义了operator double()成员函数,将造成混乱,因为在该类对象加上double类型值时,就有两种
解释,一种是调用重载+运算符执行该类加法,而另一种是将该类对象转换成double类型,再执行double加法。
27.在执行main()之前调用其他函数做法:因为全局变量是在编译时就创建了,所以可以定义一个全局的类对象,并在类的构造函数中调用一些想在main()之前调用的函数。
28.如果类中有引用变量,则必须包含可调用的构造函数,必须初始化引用成员变量(可对自身进行引用),而且初始化时必须在构造函数打冒号后初始化:Students(int ok):yingyou(ok){...}//yingyou为类的引用成员变量。
29.除虚基类外,类在初始化时,只能将值传递给直接基类。
30.记住:如果要在派生类中重新定义基类的方法,通常应将基类方法声明为虚拟的,这样,程序将根据所指的对象类型而不是引用或指针的类型来选择方法版本 为基类声明一个虚拟析构函数也是一种惯例。
31.用类名调用类的成员情况有二:(1)不须要在派生类中就可以用类名直接调用静态成员变量 (2)要用类名调用成员函数,只有在派生类成员函数中,父类可以用自己的类名调用自身的成员函数。
32.为何需要 虚析构函数:当一个基类对象指针指向了子类对象,而这时子类对象新增的成员变量是用new分配的内存,但又想通过析构基类对象同时来析构子类新增的成员变量,那么基类的虚拟类型将发
挥作用,动态联编性,调用析构函数会从该基类指针对象所指的子类析构函数开始调用直至该基类,这与一般的虚函数用法一样。如果不是虚析构函数,那么只调用该基类的析构函数。
33.虚函数的工作原理:通常,编译器处理虚函数的方法是:给每个类对象添加保存了一个隐藏成员表,即指向函数地址的数组指针。这种数组称为虚函数表,它存储了类对象进行声明的虚函数的地址。
每个类对象都有自己的虚函数表,如果在派生类中有新定义的虚函数,则在创建该对象时也将添加到虚函数表中。
调用虚函数时:程序将查看存储在对象中的vtbl地址,然后转向相应的函数地址表。如果使用类声明中定义的第三个虚函数,则程序将使用该数组中的第三个函数地址,并执行该地址的函数。
总之,使用虚函数时,在内存和执行速度方面有一定的成本,包括:
(1)每个对象都将增大,增大量为存储地址的空间。(2)对每个类,编译器都创建一个虚函数地址表(数组)。(3)每次虚函数调用都需要执行一步额外的操作,查地址,有时间开销。
比较:虽然非虚函数的效率比虚函数稍高,但不具备动态联编功能。
34.注意事项:(1)基类方法中使用关键字virtual 可使该方法在基类以及所有派生类中是虚拟的。
(2)如果使用指向对象的引用或指针来调用虚方法,具有动态联编性,而不是对象的引用或指针来调用虚方法,则没有动态联编性。
(3)如果定义的类将被用作基类,则应将那些要在派生类中重新定义的类方法声明为虚函数。
35.更多注意:(4)构造函数不能是虚函数 (5)析构函数应当是虚函数,除非类不用做基类。(6)友元函数不能是虚函数,因为友元函数不是类成员
( 7 )经验规则:第一,如果重新定义继承的方法,应确保与原来的原型完全相同,但如果返回类型是基类引用或指针,则可以修改为指向派生
类的引用或指针,这种特性被称为返加回型协变,因为允许返回类型随类 类型的变化而变化。
第二,如果基类声明的函数被重载了,则应在派生类中重新定义。如不重载,则派生类对象只能调用最近一次被重载的函数。
36.(1)需要实例化的类,则类中的虚函数必须有定义。而如果该类不实例化,仅仅声明虚函数的类且虚函数没有实现,该类编译是可以通过的!
(2)纯虚函数出现在接口类中,并赋值为0,不要为该函数分配函数地址,从而阻止类的实例化!纯虚函数是没有定义的,如果实现了也不是纯虚函数啦!
(3)一般的成员函数可以只有声明,前提是在应用中不能调用该函数,否则会因找不到定义产生连接错误!
37.实例化类的虚函数必须有定义,原因如下:有虚函数作为成员函数的类, 它的实例化---对象, 在运行过程分配到的内存不止是它的成员数据, 还有一个指向该类虚函数表(vtable)的指针, 虚函
数表中的每个数据项都是一个虚函数的入口地址; 如果一个对象的虚函数只有声明而没有实现, 就会导致这个虚函数表找不到本应作为其数据项之一的虚函数的入口地址, 虚函数表在运行前不能装载完成, 所以产生连接错误!
38.派生类使用基类的友元函数方法:
例如:基类和派生类都重载了输出运算符,现在使用派生类的对象调用基类的友元输出运算符:
基类友元函数:friend std::ostream & operator<<(std::ostream &os,const BaseClass bc)
{ os<<bc; return os; }
子类友元函数调用基类友元函数:friend std::ostream &operator<<(std::ostream &os,const ChildClass cc)
{ os<<(const BaseClass &)cc; os<<新增成员变量; return os;}
39.复制构造函数:类中定义了构造函数,这个构造函数中只有一个参数。而只有在创建对象时才能用=来复值指定的成员变量。
如:BaseClass(Class &k){kk=k;} Class k; BaseClass aa=k;//隐式转换
但如果在构造函数前加关键字:explicit,则不能进行隐式转换,可以显示转换。
40.再次强调:(1)在编写函数的参数时,应按引用而不是按值来传递对象的方式,这样做可以提高效率。因为按值传递时需要调用复制构造函数,然后调用析构函数,
这些调用需要时间。如不修改对象,应将参数声明为const引用。
(2)函数返回对象和返回引用对象:相同点:都生成临时拷贝。 不同:返回引用与按引用传递对象相似,在调用和被调用函数对同一个对象进行操作。
注意:函数不能返回在函数中创建的临时对象的引用,因为当函数结束时,临时对象将消失,因此这种引用将是非法的。
41.const函数(将const放在函数形参表后面),只是不能修改调用该函数的对象(*this). 当函数引用(或指针)返回对象时,函数返回类型必须兼容对象类型。如果对象是const类型,则函数反回类型也必须是const.
42.编译器生成的成员函数:默认构造函数,复制构造函数,赋值操作符。
43.公有继承的考虑因素:(1)所属关系 (2)构造函数,析构函数,赋值操作符函数与友元函数是不能被继承。
44.警告:如果类有间接虚基类,则除非只需使用该虚基类的默认构造函数,否则必须显式地调用该虚基类的某个构造函数。
45.调用构造函数的顺序:(1)一个派生类没有直接虚基类:A:先调用基类的构造函数再子类;B:调用基类时,按照派生类声明继承基类的顺序调用基类的构造函数。
(2)一个派生类有直接虚基类:A:先调用基类的构造函数再子类;B:调用基类时:b1.先调用虚基类后非虚基类,b2.再按声明的顺序被调用。
46.模板:部分具体化:即部分限制模板的通用性。例如:template<class T1,class T2> class Pair {......}; template<class T1> class Pair<T1,int>{......};
47.template<typename T>
class beta{
public:
template<typename U>
U blab(U u,T t);
};
template<typename T>
template<typename U>
U beta<T>::blab(U u,T t){ return u+t;}
48.template<template <typename T>class Thing, typename U, typename V>
class Crab
{ private: Thing<U>S1; Thing<V>S2; .......};
Thing是模板类类型,如果实例化时:Crab<Stack,int,double>nebula; //Stack是模板类,Thing将被替换成Stack ,里面的成员变量变成Stack<int>S1, Stack<double>S2 ;
49.模板类的友元函数有三类:A.非模板友元函数(即友元函数是不带参数或带参的类型都是要具体化的)
B.约束(bound)模板友元,即友元的类型取决于类被实例化时的类型。
C.非约束(undound)模板友元,即友元的所有具体化都是类的每一个具体化的友元。
50. A类:非模板友元函数:template<typename T>
class HasFriend
{ friend void report(HasFriend<T> &); };
在定义友元函数时,必须根据模板类实例化对象时传送的模板类参数类型。具体化定义,如:
要定义一个对象HasFriend<double>object;则友元函数这样定义:void report(HasFriend<double> &f){....}
B类:修改前一个上面的范例,使友元函数本身成为模板。使类的每一个具体化都获得与友元匹配的具体化。包含以下三步:
1.首先,在类定义的前面声明每个模板函数: template <typename T> void counts(); template <typename T> void report(T &);
2.然后在函数中再次将模板声明为友元。这些语句根据类模板参数的类型声明具体化:
template<class TT>
class HasFriend
{ friend void counts<TT>();
friend void report<>(HasFriend<TT> &);
... };
上述声明中的<>指出这是模板具体化。对于report(),<>可以为空,这是因为可以从函数参数推断出模板类型参数(HasFriend<TT>)。不过,
也可以使用report<HasFriend<TT> >(HasFriend<TT> &)。但counts()函数没有参数,因此必须使用模板参数句法(<TT>)来指明其具体化。
还需要注意的是,TT是HasFriend类的参数类型。
3.程序必须为友元提供模板定义,例如:
template <typename T>
void counts() { cout << HasFriend<T>::x << endl;}
template <typename T>
void report(T & hf) {cout << hf.x << endl;}
调用时,counts()需要提供具体化参数类型。report()只需直接调用,例如:
counts<int>(); //调用HasFriend<int>的友元函数
HasFriend<double> hfdb;
report(hfdb); //调用HasFriend<double>的友元函数
C类: 模板类的非约束模板友元函数
通过在类内部声明友元函数模板,可以创建非约束友元函数,即每个函数具体化都是每个类具体化的友元:
template <typename T>
class ManyFriend
{...
template <typename C,typename D> friend void show(C &,D &);
};
在类外定义该友元:
template <typename C,typename D>
void show(C & c,D & d){ ... }
假如创建ManyFriend<int>类对象(hfi1)和ManyFriend<double>类对象(hfi2),传递给show(),那么编译器将生成如下具体化定义:
void show<ManyFriend<int> &,ManyFriend<double> &>
(ManyFriend<int> & c,ManyFriend<double> & d){ ... }
51.嵌套类:被包含在一个类中,访问权限与一般语法一样,嵌套类可以访问包含它的类的成员。
52.终止程序的函数:std::abort()[在头文件 cstdlib 中],exit(0)。
53.异常极其重要的一点:程序进行堆栈解退以回到能够捕获异常的地方时,将释放堆栈中的自动存储变量。如果变量是类对象,将为该对象调用析构函数。
54.捕捉异常虽然用引用对象,但还是会创建拷贝,这是件好事,因为抛出异常的函数执行完后,就会释放抛出的异常对象。
那其中的好处就是:基类引用可以执行派生类对象。与派生类型兼容。(所以通过排列catch块的顺序,让处理异常更有针对性。)
55.exception类用作其他异常类的基类:
1>logic_error类(继承自exception类) :通过合理的编程可以避免逻辑错误。在头文件<stdexcept>中定义,逻辑异常表明存在可以通过编程修复的问题。下面都是它的派生类:
domain_error:定义域或值域错误
invalid_error:传递的值不符合要求
length_error:没有足够的空间执行所需操作,例如string类的append()方法
out_of_bounds :指示索引错误,例如数组下标索引
2>runtime_error类(继承自exception类):在运行期间发生但难以预计和防范的错误。在头文件<stdexcept>中定义,运行时异常表明无法避免的问题,下面派生的类:
range_error:计算的结果不在函数允许的范围内,但没有上溢,也没有下溢
overflow_error:上溢错误
underflow_error:下溢错误
3>bad_alloc类(继承自exception类):在头文件<new>中。当new出现内存分配问题时,C++提供了两种方式处理:
i>返回一个空指针 ii>引发bad_alloc异常
56.异常何时会迷失方向:
1>意外异常:异常,如果是在带异常规范的函数中引发的,则必须与规范列表里的某个异常匹配,若没有匹配的,则为意外异常,默认情况下,会导致程序异常终止
2>未捕获异常:异常如果不是在函数中引发的(或者函数没有异常规范),则它必须被捕获。如果没被捕获(没有try块或没有匹配的的catch块),则为未捕获异常。默认情况下,将导致程序异常终止
3>可以修改这种默认设置(存在意外异常或未捕获异常)
3.1>未捕获异常:此异常不会导致程序立刻终止(terminate()、set_terminate()都在exception头文件中声明)
程序首先调用函数terminate()---->默认情况下,terminate()调用abort()函数,
可以通过set_terminate()函数指定terminate()调用的函数,修改这种行为
若set_terminate()函数调用多次,则terminate()函数将调用最后一次set_terminate()设置的函数
例如:
set_terminate(MyQuit);
void MyQuit(){...}
此时若出现未捕获异常,程序将调用terminate(),而terminate()将调用MyQuit()
3.2>意外异常:
发生意外异常时,程序将调用unexcepted()函数--->unexpected()将调用terminate()函数--->terminate()在默认情况下调用abort()函数
可以通过set_unexcepted()函数,修改这种默认行为,但unexpected()函数受限更多
i>通过terminate()、abort()、exit()终止程序
ii>引发异常
#引发的异常与原来的异常规范匹配,则可以用预期的异常取代意外异常
#引发的异常与原来的异常不匹配,且异常规范中没有包括bad_exception类型(继承自exception类),则程序将调用terminate()
#引发的异常与原来的异常不匹配,且原来的异常规范中包含了bad_exception类型,则不匹配的异常将被bad_exception异常所取代
如果要捕获所有异常,步骤如下:
#include<exception>
using namespace std;
void myUnexception()
{
throw bad_exception();
}
set_unexcepted(myUnexception)
...
function()throw(...,bad_exception);
try{}
catch(bad_exception &be){}
57.动态 判断对象是什么类型 的方法,两个重要的 RTTI 运算符的使用方法,它们是 typeid 和 dynamic_cast。
大多都主张在设计和开发中明智地使用虚拟成员函数,而不用 RTTI 机制。但是,在很多情况下,虚拟函数无法克服本身的局限。每每涉及到处理异类容器和根基类层次(如 MFC)时, 不可避免要对对象类型进行动态判断,也就是动态类型的侦测。如何确定对象的动态类型呢?答案是使用内建的 RTTI 中的运算符:typeid 和 dynamic_cast。
1> 利用 运算符 typeid 可以获取与某个对象关联的运行时类型信息。typeid 有一个参数,传递对象或类型名。因此,为了确定 x 的动态类型是不是Y,可以用表达式:
typeid(x) == typeid(Y)实现:#include <typeinfo> // typeid 需要的头文件 例如:typeid(*pfile)==typeid(MediaFile)
存在的问题:如果指针 pfile是MediaFile 的派生类,则判断为false,类型兼容判断那么这类问题就要用到dynamic_cast
2> dynamic_cast用它来确定某个对象是 MediaFile 对象还是它的派生类对象。dynamic_cast 常用于从多态编程基类指针向派生类指针的向下类型转换。它有两个参数:一个是类型名;另一个是多态对象的指针或引用。其功能是在运行时将对象强制转换为目标类型并返回布尔型结果。也就是说,如果该函数成功地并且是动态的将 *pfile 强制转换为 MediaFile,那么 pfile的动态类型是 MediaFile 或者是它的派生类。否则,pfile 则为其它的类型:
void menu::build(const File * pfile)
{
if (dynamic_cast <MediaFile *> (pfile))
{
// pfile 是 MediaFile 或者是MediaFile的派生类 LocalizedMedia
add_option("play");
}
else if (dynamic_cast <TextFile*> (pfile))
{
// pfile 是 TextFile 是TextFile的派生类
add_option("edit");
}
}
存在的问题:与 typeid 相比,dynamic_cast 用时多些,为了确定是否能完成强制类型转换,dynamic_cast必须在运行时进行一些转换细节操作。因此在使用 dynamic_cast 操作时,应该权衡对性能的影响。指针强制转化失败后可以比较指针是否为零,而引用却没办法,所以引用制转化失败后抛出异常
58.C++的auto_ptr所做的事情,就是动态分配对象以及当对象不再需要时自动执行清理。auto_ptr在构造时获取对某个对象的所有权(ownership),在析构时释放该对象。我们可以这样使用 auto_ptr来提高代码安全性:int* p = new int(0); auto_ptr<int> ap(p); 从此我们不必关心应该何时释放p, 也不用担心发生异常会有内存泄漏。
这里我们有几点要注意:
1) 因为auto_ptr析构的时候肯定会删除他所拥有的那个对象,所有我们就要注意了,一个萝卜一个坑,两个auto_ptr不能同时拥有同一个对象。像这样:
int* p = new int(0);
auto_ptr<int> ap1(p);
auto_ptr<int> ap2(p);
因为ap1与ap2都认为指针p是归它管的,在析构时都试图删除p, 两次删除同一个对象的行为在C++标准中是未定义的。所以我们必须防止这样使用auto_ptr.
2) 考虑下面这种用法:
int* pa = new int[10];
auto_ptr<int> ap(pa);
因为auto_ptr的析构函数中删除指针用的是delete,而不是delete [],所以我们不应该用auto_ptr来管理一个数组指针。
3) 构造函数的explicit关键词有效阻止从一个“裸”指针隐式转换成auto_ptr类型。
4) 因为C++保证删除一个空指针是安全的, 所以我们没有必要把析构函数写成:
~auto_ptr() throw()
{
if(ap) delete ap;
}
C++学习笔记
声明:以上内容来自用户投稿及互联网公开渠道收集整理发布,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任,若内容有误或涉及侵权可进行投诉: 投诉/举报 工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。