c++vol2 读后感(转)

《C++编程思想》阅读笔记(二)

动态对象创建

1C语言中动态内存分配(malloc)只在堆中分配一片内存,返回一个void指针(分配失败则返回0),并没有创建一个对象。使用时需要强制转换成恰当的类型。调用free则只负责释放一片内存,并没有析构一个对象。

2、创建一个C++对象,有两步:a 为对象分配内存;b 调用构造函数来初始化那片内存。

3C++中把创建一个对象的所有动作都结合在一个称为new的运算符里。当用new创建一个对象时,它在堆里为对象分配内存并为这块内存调用构造函数。当分配内存失败时返回0

4、运算符new分配内存是调用malloc实现的,在调用构造函数前,new会检查内存分配是否成功。

5、与new对应的则是deletedelete首先调用析构函数,然后释放内存(一般是调用free)

6、如果delete的对象指针是0,将不发生任何事情,因此在删除一个对象后,应立即将其指针置0。因为重复删除同一对象会产生错误。

7、定义一个友元函数为内联函数不会改变友元状态,它依然是全局函数,而不是一个类的成员函数。

8、如果对一个void指针进行delete操作,唯一执行的是释放内存,而不会调用析构函数,因为void指针没有,类型信息用来确定调用哪个析构函数。

9、不要将在栈上创建的对象的指针和在堆上创建的对象的指针都放在同一个容器中,否则会产生严重的问题。

10、动态创建数组:new会调用默认的构造函数,delete需要“[]”来标识。

11、使指针更像数组:用如下方式:int* const arr = new int[10];,这里arr即为一个指针常量,其值不可修改,更像一个数组名。若为:int const* arr = new int[10]; const int* arr = new int[10]; 这里的arr为一个常量指针,其指向的值不可修改。另外,数组名和指针的取值方式不一样:指针有两次取值才获得真正的对象,数组只需一次。

12、当new分配内存失败时,会调用new-handler函数,该函数默认产生一个bad_alloc的异常。

13、重载newdelete:重载newdelete时,我们只能改变原有的内存分配方法,因为我们无法控制构造函数和析构函数的调用。

继承与组合

1、在public继承中,派生类的对象也是基类的对象,但基类对象不是其派生类的对象;

2、将基类指针强制转换成派生类指针:derivedPtr = static_cast<baseClass*>(basePtr);

3、名字隐藏:a 在派生类中定义和基类中签名一样的函数,这叫普通成员函数重定义;如果基类中该函数是虚函数,则叫重写(overriding)b 如果签名不同,则基类中所有与该函数同名字的的函数在派生类中均自动隐藏了。

4、在覆盖定义基类成员函数时,调用基类中的该函数版本需用作用域运算符,否则实际调用的是该函数自身,会引起无穷递归。例:BaseClass::function();

5、继承类型:

apublic继承:基类的public成员成为派生类的public成员,protected成员成为派生类的protected成员,private成员则隐藏。

bprotected继承:基类的public成员和protected成员成为派生类的protected成员,private成员在派生类中不可见,隐藏。

cprivate继承:基类的public成员和protected成员成为派生类的private成员,private成员在派生类中不可见,隐藏。

privateprotected继承不是“is-a”)的关系。继承默认是private的。

6、类的构造是从类层次的最根处开始,而在每一层,首先会调用基类构造函数,然后调用成员对象构造函数。析构严格按照相反的顺序。注意:基类的构造在成员对象构造之前。构造函数调用的次序完全不受构造函数的初始化列表中次序的影响。

7、把派生类对象赋给基类对象,然后试图在该基类对象中引用只在派生类中才有的成员是个语法错误。

8、多重继承:用virtual继承时,派生类中只出现一个子对象,可解决重复子对象的问题。

9javaC++的区别:java是纯粹的(pure)的面向对象语言,而C++ 是一种混合的(hybrid)语言。java是基于对象的继承层次结构模型,所有类都是在一个单一的继承层次结构中派生的,有一个基类为根(Object)

10、非自动继承的函数:构造函数,析构函数,赋值函数(operator=)因为它完成类似于构造函数的功能。

11、静态成员函数不可以是虚函数。

12、将私有继承成员公有化:在派生类中的public部分声明其名字,例:

       public

using baseClass::memberName;   //给出一个重载函数的名字,将使基类中所有它的重载版本公有化

13、除赋值运算符外,其余运算符的重载都可以自动地继承到派生类中。

14、类中编译器可自动生成的四个函数:构造函数,拷贝构造函数,析构函数和赋值运算符(operator=)

15、确定使用组合(composition)还是继承(inheritance)的方法是:是否需要从新类向上类型转换; 确定是否需必须使用多重继承的方法是:是否需要将一个对象指针向上类型转换成两个不同的基类类型。

16、多重继承分两种:接口继承(interface inheritance)和实现继承(implementation inheritance)。接口继承仅仅是在派生类接口中加入成员函数的声明,C++不支持这样用法,C++仅提供了实现继承,接口继承需用特别方的法实现。

17、多重继承的一个共同用途包括使用混入类(mixin),混入类的存在是为了通过继承来增加其他类的功能。(卷二P359)

18菱形继承:通过使用虚基类(不是抽象基类)来消除重复子对象,使用virtual关键字继承(virtual public)

19、基类必须有虚析构函数,虽然没有编译器也可以编译通过。

20、编译器对多重继承的实现原则:一个带有多重继承的派生对象必须表现出它好像有多个VPTR,它的每个含有虚函数的直接基类都有一个。

21、由于菱形继承中基类构造的二义性,规定:最高层派生类(继承树最叶子端的类)必须初始化一个虚基类(卷二P367)。而中间层次的派生类对基类的初始化将被忽略。

22、多重继承名字查找问题:若基类中含有同名的函数,派生类调用该函数将产生编译错误。消除方法:用基类名来限定函数的调用,例:

       在派生类中:       public

                                          using baseClass::function;//注意声明函数名即可,不需括号

另外,基类成员在派生类中的作用域规则:在继承层次结构中,离派生类越近的上层类,其成员在派生类中的优先级越高。

23、多重继承的优势只与虚基类一同存在。

虚函数和多态:(多态--polymorphism,封装--encapsulation

1、一旦一个函数被声明为虚函数,即使重新定义时没有被声明为虚函数,它在该点之后的继承层次结构中仍都是虚函数。

2、抽象类:有一个或多个纯虚函数的类,也称抽象基类。其唯一的用途是为其他类提供合适的基类,其他类可从它这继承或实现接口。抽象类仍然可以提供虚函数的实现。

3、若抽象基类的派生类没有提供抽象基类中纯虚函数的定义,则该派生类仍然是抽象类,因为继承了基类的纯虚函数。

4、多态性:通过继承相关的不同类,他们的对象能够对同一个函数调用作出不同的响应。

5、多态性是通过虚函数实现的。通过基类型的指针或引用请求调用虚函数时,C++会在与对象关联的派生类中正确地选择重定义的函数。注意只能是基类型的指针(BaseClass*)或基类型引用(BaseClass&),不能是传值调用(BaseClass),防止对象切片。

6、不能实例化抽象类的对象,但可以声明引用抽象类的指针,指向具体类的对象后,这种指针使派生类对象具有多态操作能力。

7、给基类提供一个虚析构函数,这样删除该类的派生类时,自动删除基类部分。

8、将构造函数定义成虚函数是个语法错误,构造函数不能是虚函数。但一般需要将基类的析构函数定义成虚函数。

9C++多态的实现原理:多态是通过复杂的数据结构实现的,涉及三层指针。

第一层:C++编译时,给每个含有虚函数的类建立一个虚函数表(VTABLE),该表中,对每个虚函数都有函数指针指向,而若为纯虚函数,则函数指针设置为0。因此vtable中有0时为抽象类(abstract class),没有0时为具体类(concrete class)

第二层:对每个实例化的带虚函数的类对象,编译器在对象前面添加指向该类vtable的指针(VPTR)。

第三层:接受虚函数调用的对象句柄(指针或引用)

多态实现步骤:

       a 将基类指针或引用指向具体类对象;

       b 通过基类句柄获得具体类对象,该对象以vtable指针开始;

       c 复引用vtable指针获得该具体类的vtable

       d 搜索vtable找到需调用的函数指针;

       e 复引用函数指针,调用该函数。

10、虚函数增强了类型概念,它使类概念不仅仅只是在结构内隐藏地封装代码。它是面向对象程序设计的核心所在。

11、动态绑定(dynamic binding)(也称晚绑定,运行时绑定)只对virtual函数起作用,且只在使用含有virtual函数的基类的指针或引用时发生。不能是基类对象的一个原因是传值会导致对象切片。

12、创造C语言是为了代替汇编以实现操作系统,发明C++的主要原因之一是让C程序员的工作效率更高。

13、对只存在于派生类中的函数做虚函数调用,会产生编译错误。

14、编译器不允许我们在派生类中改变基类中虚函数的返回值,若不是虚函数,则是可以的,但基类中所有同名函数在派生类中都自动隐藏。另外,如果基类中返回值是某基类对象,在派生类中将返回值改成该基类的派生类,则是可以的。

15、虚机制在构造函数和析构函数中均失效。

16、纯虚析构函数:唯一的效果是阻止基类的实例化。定义纯虚析构函数,我们必须提供一个函数体。

17、单根继承(single-rooted hierarchy),也称基于对象的继承(object-based hierarchy):所有类都直接或间接地从一个公共基类中继承出来。java等除C++外的面向对象语言都是用这种体系。

18、向下类型转换(downcasting)C++提供了一个特殊的称为dynamic_cast的显式类型转换(explicit cast)。但使用dynamic_cast时,对象必须含有虚函数,因为它使用到了VTABLE中的信息。

模板介绍

1、有三种代码重用的方法:aC方法(繁琐,已发生错误,摒弃)bSmalltalk方法:通过继承来实现代码重用,每个容器类都包含通用的基类Object的项目,java中的方法。cC++中的模板:模板实现了参数化类型(parameterized type)的概念。

2、模板语法:template关键字告诉编译器,随后的类定义将操作一个或多个未指明的类型。

       template<class T, int size>.......

template<...>之后的任何东西都意味着编译器在当时不为它分配存储空间,而是一直处于等待状态直到被一个模板实例告知。编译器和连接器中有机制能去掉同一模板的多重定义。

3、懒惰初始化(lazy initialization):数据成员不在构造函数中初始化,而是推迟到第一次访问时初始化。若创建大量的对象,但不访问每一个对象,为节省存储,可以使用懒惰初始化。

4、容器所有权问题:当容器中包含指向对象的指针,而这些指针很可能用于程序的其他地方,此时需考虑所有权问题。处理所有权问题的最好方法是由客户程序员来选择,通常通过构造函数的一个参数来实现。constructor(....bool owner)

5、迭代器(iterator):迭代器是一个对象,它在其它对象的容器上遍历,每次选择其中一个,不需要提供对这个容器的实现的直接访问。迭代器提供了一种访问元素的标准方法,不管容器是否提供了直接访问元素的方法。迭代器是一个灵巧指针,其关键是:从一个容器元素移动到下一个元素的复杂过程被抽象为像一个指针一样。

C++的异常处理机制

1、传统的错误处理:asserterrnoperrorsignal信号处理系统,非局部跳转(setjmp()longjmp())

问题:C中的信号处理方法和函数setjmp/longjmp并不调用析构函数,所以对象不会被正确的清理。

2、异常匹配:匹配一个异常并不要求异常与其处理器之间完全相关,一个对象或是指向派生类对象的引用都会与其基类处理器匹配。匹配过程中不会将一种异常类型自动转换成另一种异常类型,转换构造函数在异常匹配中失效。

3、全能捕获:catch(...){}

4、重新抛出异常:在一个异常处理器内部,可以使用不带参数的throw语句重新抛出异常,且这个异常的所有信息都会自动保留,以传给更高层的异常处理器。

5、异常没有被捕获,则库函数terminate()函数会自动调用。terminate默认调用C库函数abort()abort被调用时,程序不会执行正常的终止函数,全局对象和静态对象的析构函数不会执行。当局部对象的析构函数或全局,静态变量的构造或析构函数抛出异常时,也会调用terminateset_terminate()可设置自己的terminate函数。

6C++异常处理确保当程序执行流程离开一个作用域的时候,对于在这个作用域中所有由构造函数建立起来的对象,它们的析构函数一定会被调用。问题:当在构造函数中发生异常时,析构函数将不会调用,因而申请的资源无法释放,并且后面的一系列的对象也将不能继续构造函数。将出现悬挂指针(naked pointer)。解决办法:a、在构造函数中捕获异常;b、使用模板来封装指针(卷二P13)

7、异常规格说明语法:可能抛出的所有可能的异常类型都应该写在throw后的括号中。

例:void f() throw(exp1exp2,,);

传统的函数声明void f()意味着可以跑出任何类型的异常;而void f() thow()则表示不会抛出任何异常。

8、如果函数所抛出的异常没有列在异常规格说明的异常集中,则会调用unexpected()函数。set_unexpected可设置自己的unexpected函数,该函数不能有参数和返回值。

9、异常规格说明和继承:派生类中覆盖函数不能在异常规格说明列表中添加其他异常,可改为父类中异常类型的派生类型。

10、避免在模板类中使用异常规格说明,因为无法预料模板参数类所抛出的异常类型。

11、异常安全:栈容器中出栈(pop)和删除栈顶元素(top)分开实现的原因是为了保证异常安全。若将两个动作放在一个函数中,当为了得到返回值而调用拷贝构造函数时,拷贝构造函数在最后一行抛出异常,此时,栈顶元素丢失了,而返回却失败。因此将改变栈状态和返回值两个动作分开实现,这也遵守了高内聚设计原则--每个函数只做一件事情。异常安全代码能使对象保持状态的一致性而且避免资源泄漏。

12、通过引用而不是值来捕获异常:a、当异常对象传递到异常处理器中的时候,避免进行不必要的对象拷贝;b、当派生类被当做基类对象捕获时,避免对象切割。

13、不能有异常从析构函数中抛出,因此在抛出其他异常的过程中析构函数会被调用。否则会导致调用terminate函数。析构函数内若可能产生异常,则必须析构函数自行处理。

14、当异常被抛出时,将造成相当多的运行时开销。

深入理解字符串

1、字符串的内部实现:C语言中字符串就是字符型数组,且总是以二进制零结束;C++stringC字符串最大的不同是它隐藏了字符串的物理表示。极大的减少了C语言中3种常见错误:数组越界;野指针访问;释放数组空间后产生悬挂指针。

2、创建并初始化C++字符串的方法:

       string sBlank;         string str("hello");         string strcopy = "this is ""a copy";              string copy(str);    

       string s1(str, 0, 4);         string s2 = strcopy + " abc" + str.substr(1,2);              string s3(str.begin(), str.end());     string s4(5, ‘a’); //初始化为a字符重复5次,即“aaaaa”,第二个参数只能是char字符

3、字符串操作函数:追加: append();插入: insert();长度: size();当前分配的存储空间大小: capacity()

替换: replace();预留空间: reserve()

查找: find()find_first_of()find_last_of()find_first_not_of()find_last_not_of()rfind()//逆向查找,将字符串反过来;

string类中没有改变字符串大小写的函数,可借用C语言的库函数toupper()tolower()实现;

删除:erase();返回const char*c_str()//不可作非const参数使用;

字符串的运算符中,没有逻辑与或非(&&,||,!)运算符,也没有逐位与或非(&,|,~,^)运算符;

比较:常见的比较运算符(>,<,==)compare()        交换:swap();

C++字符串类中提供一种S[n]表示法的替代方法:at(n)函数。若下标越界,at函数会抛出out_of_range类型异常,多用。

4string类基于basic_string<class T>模板实现

5、宽字符(wchar_t):两字节(16)

输入输出流

1、输入输出流:istream对象,ostream对象,iostream对象;文件输入输出流:ifstream对象,ofstream对象,fstream对象;string类输入输出流:istringstream对象,ostringstream对象,stringstream对象。以上流类实际上都是模板的特化(template specialization)

2、流插入符(inserter)<<;提取符(extractor)>>

3、按行读取流:成员函数get();成员函数getline();定义在<string>中的全局函数getline()。前两个函数都有三个参数:指向字符缓冲区的指针;缓冲区大小;结束字符(默认为'/n')。遇到结束字符时,均会在缓冲区

末尾存储一个零。两者区别:get不读取结束符,而getline读取结束符但不放入缓冲区。<string>中的getline为全局函数,两个非默认参数:输入流对象和string对象。读取界定符(delimiter默认'/n')并丢弃。数据存入

string对象中,故不必担心缓冲区溢出。

4、处理流错误:获取状态:eofbit置位--eof()返回真,此时failbit也会置位;failbitbadbit置位--fail()返回真;badbit置位--bad()返回真;goodbit置位--good()返回真。clear()清空流状态标志位,且置gootbit位。setstate()设置标志位,clear()也可设置标志位。

5、文件打开模式:ios::inios::outios::appios::ate(打开一个已存在的文件输入/输出,并将指针移至文件尾)ios::truncios::binary(使用read()write()的时候应该以二进制打开,因为这些函数以字节为单位操作;若要使用流指针定位命令也应以二进制打开)

6、每个输入输出流对象都包含一个指向streambuf的指针。每个输入输出流对象都有一个成员函数rdbuf(),用来访问streambuf,它返回一个指向对象streambuf的指针。例:cout<<in.rdbuf();

7、输入输出流中定位:a、绝对定位:tellp()tellg()获取当前位置,seekp()seekg()定位;b、相对定位:重载版本的seekp()seekg()函数,两个参数:第一个为移动的字符数目,第二个为移动方向(ios::begios::curios::end)

8、创建一个既能读文件又能写文件的方法:法一:fstream io

法二:ifstream in("filename"ios::in | ios::out)     ostream out(in.rdbuf);

9、输出字符串流:成员函数str()可将输出流格式化为string对象,每次调用str()都会返回一个新的string对象。

10、输出流的格式化:格式化标志;格式化域;操纵算子(cin>>ws可吃掉空格)

通用容器(看得很粗糙)

1、对容器中元素灵活访问的方法是使用迭代器:迭代器是一个对象,它作为一个类也提供了一个抽象层,因此可以将容器的内部实现细节与用来访问容器的代码分隔开来。通过迭代器,容器可被看作一个序列,迭代器遍历该序列而无需知道其基本结构。

2STL编程指南网站:http://www.sgi.com/tech//stl/http://www.stlport.org

3C++中容器分三类:

a 序列容器:仅将元素线性的组织起来,是最基本的容器类型,如vectorlistdeque等;

       b 容器适配器:在基本线性序列上添加一些特殊的属性,如stackqueuepriority_queue等;

       c 关联式容器:基于关键字来组织元素,如setmapmultisetmultimap等。

4、标准库中所有的容器都持有存入对象的拷贝,所以这些对象必须是可拷贝构造(具有一个可访问的拷贝构造函数)和可赋值(具有一个可访问的赋值操作符)的。

10、当容器中持有的是对象时,容器被销毁时,其持有的对象也会被销毁,而若持有的是指针,指针不会被销毁,需要程序员自行销毁。

6、使用容器一般方式:

typedef std::容器名<对象类型>   Container; //这样方便更换容器类型,仅将容器名该一下即可

typedef Container::iterator Iter;      //简化迭代器的写法

typedef Contianer::const_iterator;    //这种const()容器中的迭代器不允许反向修改所指向的容器中的元素

typedef Container::reverse_iterator;  //这种可逆容器中的迭代器从序列后向前遍历

typedef Container::const_reverse_iterator; //可逆const容器产生的迭代器

7、所有容器都有begin()end()成员函数,用来产生选择序列开始端和超越末尾端的迭代器。可逆容器则拥有成员函数rbegin()rend()。如果容器是const的,则均产生const型的迭代器。

8、判断迭代器是否到达尾部使用不等于end(!=),不要使用<<=,只有!===测试方式有效。通常将循环写成如下形式:       for(Iter i = ContianerObject.begin(); i != ContianerObject.end(); i++)

9、通过迭代器访问容器中元素的方法:使用"*"解析迭代器引用,*i返回容器中持有的任何东西,同时迭代器还可以反向给非常容器中元素赋值,如*i = *i + 1

10、容器适配器(栈,队列,优先队列)不支持迭代器。

11、所有基本容器都是可逆容器;

       clear()清楚容器中全部内容;

       resize()扩展一个序列,此时新元素调用默认构造函数初始化,内置类型则置0

       insert()插入单个元素或具有相同值的一组元素或由起始和终止迭代器标识的一组来自其他容器的元素;

       erase()清除序列中一个或一组元素,用迭代器指示。

       push_back()pop_back()listdeque还支持push_front()pop_front()

12vector是一种类似数组但可动态扩展的容器,vector的特点是允许快速随机访问其中的元素,其实现也是将存储内容作为一个连续的对象数组来维护,因此其索引和迭代操作非常快,同时也导致插入新元素非常慢。

13、使用vector最有效的方式是:a 在开始时用reserve()预分配正确数量的空间,此后不必再重新分配空间;b 仅仅在序列后端添加或删除元素。reserve()只作内存分配操作,不调用构造函数。

14deque(双端队列)("deck")的特点是方便在序列的前后端添加新元素;像vector样也有operator[]操作符,同时,vector       deque还提供了成员函数at()进行元素随机访问,但访问越界时at()会抛出异常,operator[]不会。

15list(双向链表)的特点是在序列的任何地方快速的插入或删除元素。如果对象较大,较复杂则使用list比较合适。如果需要频繁的遍历序列,则不要用list

17、栈(stack):后进先出;队列(deque):先进先出;优先队列(priority_queue):总将具有最高优先级的元素先弹出来。堆(heap)就是一个优先队列。

18、标准C++不支持多线程处理。

19、关联式容器将关键字与值关联起来(mapmultimap是,setmultiset因结构相似性也归为此类)

16set的主要工作是保持独一无二的元素,同时对元素排序,因为set的实现是用一颗平衡树来存储其元素。

20map中含operator[]操作符,但当用operator[]查寻一个不存在的值时,map会创建一个新的关键字-值对(值使用默认构造函数)

21、所有关联式容器都有count()find()成员函数。count()告知那个关键字在容器中重复存在的数目,find()产生一个指向首次出现给定关键字的元素的迭代器,找不到则返回超越尾端的迭代器(end())

设计模式(看的很粗略)

1、设计模式的目标是封装变化(encapsulate change)

2、对象组合优于类继承,因为组合更简单。《极限编程》(extreme programming)的指导原则之一是只要能用,就用最简单的

3、模式分类:

a 创建型(Creational):用于封装对象的创建过程。如单件(Singleton)模式、工厂(Factory)模式、构建器(Builder)模式;

       b 结构型(Structural):封装对象之间的连接方式。如代理(Proxy)模式、适配器(Adapter)模式。

       c 行为型(Behavioral):封装特定操作类型的对象。如命令(Command)模式、模板方法(template Method)模式、状态(State)模式等。

4、单件(Singleton)模式:限制一个类有且只有一个实例的方法。实现方法是声明所有构造函数为私有,并防止编译器隐式生成任何构造函数。另外,拷贝构造函数和赋值操作符都声明为私有,以防止该类的任何复制操作。

5、单件模式中唯一实例的创建可采用静态创建,也可以使用惰性初始化。

6、单件模式可演化成有限个对象的对象池。

7、命令模式直观的看就是一个函数对象,将函数封装成对象进行传递。

8、工厂模式:封装对象的创建。如果想在构造函数中调用虚函数,可用特殊的技术来模拟虚构造函数。实现方法见(卷二P407)

9、虚函数的思想是发送一个消息给对象,让对象决定要做的正确的事情。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值