Thinking in C++ 读书笔记(B)

本文深入探讨了C++中的关键概念,包括运算符重载、动态对象创建、继承与组合、多态、模板、异常处理及运行时类型识别等。通过具体示例解释了这些概念的应用场景和技术细节。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

第十章 引用和拷贝构造函数

1.引用是支持C++运算符重载语法的基础,也为函数传入和传出的控制提供了便利。引用的思想来自于algol语言。

2.规则:引用被创建时必须被初始化(指针可以在任何时候被初始化);引用被初始化指向一个对象后就不能指向另一个对象(指针可随时更改自己指向的对象);不能有NULL引用;

3C语言中,想改变指针本身而不是指针指向内容时,需要使用指针的指针。而C++中,使用指针的引用取代指针的指针;

4->* 指向成员对象的指针

 

第十一章 运算符重载

1.不能改变优先级;不能改变操作符元数;不能创造操作符;.不能被重载;

2.由于运算符重载可以看成函数调用,所以++aa++区别的方法可以用函数名搞定,如:

Prefix: const myclass& operator++() { i++; return *this;}

Postfix: const myclass  operator++(int) { myclass before(*this); i++; return before;}

对于后者,编译器为int参数传递一个哑元常量值用来为后缀版本产生不同的署名。

3.  通常运算符重载有两个版本,一个是可以将其声明全局的函数,然后作为类的友元函数;另一个就是直接作为类成员函数。

4.运算符‘=’仅允许作为成员函数。为什么呢?

5.返回效率比较:

a)         return integer(left.i+right.i); 仅构造;到返回值外面的空间;

b)        intger tmp(left.i+right.i); return tmp; 构造、拷贝、析构; 局部空间,后拷贝到外部;

6.  拷贝构造函数和赋值运算符:

       foo B;       // 调用B的构造函数

       foo A = B;       // A的对象未创建时,调用的拷贝构造函数;

       A = B;            // A的对象创建后,调用A的赋值操作符;

7.  构造函数转换,这里的例子特别好:

       class one {public: one() {}};

       void f(two);

       class two {public: two(const one&)};         // 此时调用f(one)无错

       class two {public: explicit two(const one&){}}; // 此时调用f(one)出错,必须f(two(one));

8.       X可以使用operator Y() const 将它本身转换到类Y

9.       所谓的自动类型转换就是指在构造函数中写相应的构造函数。

 

第十二章 动态对象创建

1.  C++中把创建一个对象的所有工作都放到new运算符里。空间分配、长度计算、类型转换和安全检查,最后返回this指针。这使得堆上分配空间和栈上一样容易。

2.  重载newdelete

C++new中的内存分配方案是为通用目的而设计的,在特殊情况下不能满足用户需要:

a)         内存可能还有,但都是碎片,找不到足够大的空间;

b)        可能要在内存中制定的位置上放置一个对象。面向硬件的内嵌系统;

c)        希望调用new时,在不同的内存分配器中选择;

3.  虽然编译器在调用new时会分配空间并调用构造函数,但在重载new时仅能改变内存分配方式;

Operator new(size_t sz);   // 需要有大小的参数;

Operator delete(void* ptr); // 需要指向那片内存的指针;

Operator new[](size_t);          // 对数组而言的new[]delete[]

Operator delete [](void* ptr);

 

第十三章 继承和组合

1.  如果基类中有一个函数名被重载几次,在派生类中重定义这个函数名会掩盖所有的基类版本。更有效的方式是用virtual声明它们。

2.  基类的private对外界和派生类都隐藏,而protected只对外界隐藏,对派生类可见。这是我们设计成员可见性的一个标准。

3.  构造函数和析构函数、operator=也不能被继承。

4.  私有继承成员的公有化:只需要在派生类中的public选项声明它们的名字即可。通过这种方式可以隐藏基类的部分功能。

5.  从某种意义上来说私有继承和被保护继承只是为了维护语言的完整性。

6.  向上映射(upcast)和向下映射:任何将子类的对象、引用和指针转变成父类的对象、引用和指针的动作称为向上映射。向上映射是安全的,所以C++总是允许不需要显示的说明或做其它的标记。此种特性与virtual关键字联系起来作用很大;

 

第十四章 多态和虚函数

1.  binding 把函数体和函数调用相联系称为捆绑。早捆绑、晚捆绑;c++中的晚绑定机制是通过virtual关键字是实现的。

2.  每个包括虚函数的类被编译器创建一个VTABLE,而类中保存一个指向这个表的指针。这一点可以通过查看包含虚函数的类和普通类的大小验证,可以看到含虚函数的类比普通类多了一个void指针长度。

3.  如果一个类的对象总是没有意义的,c++中就可以将其声明成纯抽象基类(包含一个纯虚函数),可以防止用户创建其的对象;而此时,VTABLE就是不完全的,既然是不完全的,就不可能创建一个对象。

4.  在声明为纯虚函数后,还可以为其提供定义,因为我们希望此函数中的一块代码对于一些或所有派生类都能使用。在子类中,可以通过类名和域操作符调用它。

5.  如果派生类中定义了自己的虚函数,则通过基类指针的虚函数表的地址偏移无法准确获得我们所需的虚函数;但若我们确切知道了派生类类型时,就可以调用。

6.  包含虚函数的对象被创建时,必须初始化它的Vptr指向相应的Vtable,这个必须在任何有关虚函数调用之前完成。于是,构造函数承担了这部分工作。

7.  所有基类构造函数总是在继承类构造函数中被调用。因为构造函数本身就是为了保证对象被正确的创建,派生类并不能初始化基类的成员,这项工作通过调用基类构造函数完成。如果可能,我们应该尽量在构造函数的初始化列表中初始化所有成员对象。

8.  upcasting 是自动自发的,不需强制,因为它是绝对安全的。downcasting是不安全的,因为没有关于实际类型的编译信息(参见RTTI)。这里的向上和向下都是以基类为继承树的根讨论的。

9.  虚析构函数:使用基类指针删除子类对象时,有了虚析构函数,会调用子类析构函数,彻底删除堆里面的对象;否则只会调用基类的;

 

第十五章:模板和包容器类

1.              继承和组合允许重用对象代码,但不能解决有关重用的所有问题,模版为编译器提供了一种在类和函数体中代换类型名的方法,以此重用代码;

2.              包容器的概念;

 

第十六章      多重继承

1.  ios, istream, ostream, iostream是多重继承的一个应用实例;

2.  最晚派生类(most derived)是当前所在的类,打算使用虚基类时,最晚派生类的构造函数的职责是对虚基类进行初始化;因为既然继承关系走了一个菱形过程,必然有一层编译器无法分清楚调用谁的构造函数;

3.  指针法术(pointer magic)?

4.  可以知道,为了保证虚继承机制的实现,编译器给一个类增加了不少指针;

5.  避免使用多重继承:有必要使用两个类的公共接口?需要向上映射到两个基类吗?

6.  引入多重继承后必须解决子对象重叠问题。MI被称为90年代的goto

 

第十七章      异常处理 Exception Handling

1.  C中的出错处理: setjmp(): 存储当前程序正常状态;

longjmp(): 用上述状态作为参数,恢复;

上述两个函数相当于non-local goto语句,由于它跳出范围时不调用析构函数,所以c++中这种方法不可行;

2.  通常在出错的当前上下文中获取不到异常处理的足够信息,可以将当前错误信息封装成错误对象发送至更大的上下文对象。称为异常抛出。

3.  异常处理两种模式:终止、恢复;如果错误是致命性的,则调用c++终止模式结束异常状态;恢复模式则希望在异常处理后能继续正常执行程序;

4.  异常规格说明:

Void f( void ) throw(); //函数不会抛出异常;

Void f( void ) throw( myexcep1, myexcep2, …); //会抛出这些异常;

Void f( void ); //可能抛出任何类型的异常,也可能不会;

5.  set_terminate( void terminate() ): 未被捕获的异常;

set_unexpected( void unexpected() ): 实际抛出的异常类型和函数定义时的异常规格说明不一样;

6.  调用构造函数时发生异常,相应的析构函数能够被顺利的调用吗?

7.  用引用而不是值去捕获异常:当对象通过值被捕获时,它被转化成一个base对象(通过构造函数);而通过引用被捕获时,仅仅地址被传递所以对象不会被切片,所以行为表现出其在派生类中的真实情况;

8.  使用一个构造函数为调用成功的对象是十分危险的,所以需要在构造函数中抛出异常;而析构函数会在抛出其它异常时被调用,所以不要打算在其中再抛出另一个异常了。一旦后者发生,意味着在已存在的异常到达引起捕获之前抛出了一个新异常,这会导致对teminate()的调用;

9.  为了使用新特性,必然有开销;c++的所有新特性都是如此。异常处理器(catch块)完成任务后,异常对象被相应销毁;

 

第十八章      运行时类型识别 RTTI

1.  通常我们并不需要知道一个类的确切类型,虚函数机制可以实现那种类型的正确行为。RTTI即在只有一个指向基类的指针或引用时确定一个对象的准确类型;

2.  RTTI的两种用法:typeid(返回一个type_info对象)和安全类型向下映射(使用dynamic_cast);

3.  典型的RTTI是通过在vtable中放入一个额外指针实现的,这个指针指向一个描述该特定类型的typeinfo结构(每个新类型只有一个typeinfo实例);

4.  如果自己写RTTI,从本质上说,只需要两个函数就行了,一个用来指定类的准确类型的虚函数;

5.  static_cast: 所有的良定义转换,包括安全转换和次安全转换,前者是指编译器就可以进行的转换,后者包括窄化(丢失)信息、用void*的强制变换、类层次的静态导航;

6.  const_cast: 把一个const转换成non_cast volatile non_volatile

7.  reinterpret_cast: 不安全;其假设一个对象仅仅是一个比特模式,它可以被当作完全不同的对象对待;什么时候用呢?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值