Thinking in c++ 总结
终于煲完这本书了...这本书是一本写给由c转c++新手的入门书.它的亮点在于非常详细清晰地阐明c++中一些基础语法的来龙去脉.然后第一章对面向对象的简介也让我对面向对象有了清晰的初步认识.
本文主要以从两个方面记录所得.一是,c++为什么要设计成这样.二是一些语法细节.
首先,为什么c++需要面向对象?
首先c++是一门编程语言,应先强调它跟其他语言一样是一种表达工具.编程语言能够抽象计算机的行为,人类使用编程语言向计算机表达想法,从而操纵计算机为自己服务.由于编程语言是抽象机器行为的结果,所以它的结构与计算机行为很相似.变量与存储器,指令,结构化程序设计中的函数与运算电路.尽管从机器语言,汇编语言到结构化程序设计,编程语言的抽象层次不断提高,表达能力不断增增强,但是编程语言的结构还是离不开机器的结构:数据与对数据的操作.
这个时候问题就来了,人对实际问题的抽象结构往往并不是"数据 + 操作"的结构,为了将实际问题的概念向计算机表达,人必须作概念的转换.书中的说法是establish the association between the machine model(in the solution space) and the model of problem(in the problem space).即我们要把问题域的概念与编程语言的概念联系起来.当使用结构化程序设计的时候,我们往往是将结题"过程"层层分解,直到它能细化到能使用编程语言来表达.这种概念的转换的思维成本很大.具体来说就是,当代码作为代码开发者与代码维护者之前沟通的语言时,开发者将问题域的概念转化为编程语言的概念,然后阅读者又需要将编程语言的概念转化为问题的概念,而后者的过程是很难进行的,对于抽象层次整齐,变量名有较强表达能力的代码来说,这会有所改善.当面对抽象层次混乱,变量名语义模糊的代码来说,维护工作就是灾难.
书中指出,为了减少这种思维成本,部分编程语言通过被设计成面对特定问题,来让代码与问题域的概念更接近.而面向对象编程语言通过提供把数据与对数据的操作绑在一起的表达方式,提供面向问题域概念的抽象能力,代码所表达的概念更接近问题域概念,人们期望通过使用这种抽象能力来降低概念转化的思维成本.注意,是降低,编程语言跟人类语言不可能表达一致的概念,因为我们总要思考编程语言的语义,在面向对象的语言中, 我们总要思考类,对象,方法.
然而,在我看来,问题域的概念有时候用函数来表达更加恰当.并不是一切问题域的概念都是对象,很多时候我们仅仅需要思考"操作".
此外,c++还解决了一些c语言的问题.
c++提供更严格的类型检查.c++需要在编译的时候通过声明确定函数原型.函数参数在编译时连同函数名写入函数签名中,这样当两个函数的函数名和函数参数都一致时,编译器才认为他们是同一函数.function()不再代表任意数量的参数,而是像function(void)一样无参数.这些都可以防止函数误调用.c++中还禁止了通过void*指针乱转数据类型的用法.const则跟进一步划分标识符的类型.此外,由于const能提供宏没有的类型检查,const更安全.内联函数解决了带参数宏各种问题,如:1.宏并不是真正的函数,它的外表与函数相似而有时行为不一致会导致难以发现的bug.2.宏没有作用域的概念.3.宏不能作为对象成员.而内联函数在优化程序的同时也提供了类型检查.
c++解决了c函数库的问题.c不少函数库都是struct+function的结构,因为面对实际问题的时候不得不包装数据和操作.c++的面向对象用法比struct+function更符合语义,object.method()比向function显式传结构体指针更简洁.c++还通过将函数写入struct或class解决了名字冲突的问题.c++的access control分离了接口与实现,使库的用户更清晰什么信息是他们需要的(接口).构造函数与析构函数让资源分配自动化.总之,面向对象的封装方式能抽象出一个简单的概念,给变量封装职责,使这个变量(概念)能够自我管理.
c++通过重载函数提供更强的表达能力.编译器通过形式参数判断函数语义符合人类通过上下文判断词语语义的思维模式,实现一次多意.这也让我们不再需要给函数提供冗余name decoration,如swap_int(),swap_float().也让类能够提供多种构造函数(构造函数重载).默认参数作用相似.
c++在解决c的问题的同时,也引入了新的问题.由于对象这种变量的拷贝与初始化没有内置类型那么简单,所以在构造函数,拷贝构造函数,析构函数都要谨慎考虑资源管理,c++还要引入初始化列表保证分配内存和初始化在用户能够碰在数据之前完成.向函数传对象和函数返回对象的过程也变得复杂.
此外,c++还添加了两个新玩意:运算符重载和模板.虽然本书说运算符重载只不过是语法糖,但是在c++后来的发展中看到,运算符重载为c++的扩展提供了强大的能力,例如你可以通过重载 operator()来伪造出一个函数对象.一些语言如java是利用单一继承树来实现可以装载任何对象的container,而c++不支持单一继承数,所以使用了模板来实现装载任何对象的container.模板后来更衍生出强大的STL.
下面摘录一些细节
c的const与c++不同.c++的const具有内部连接性.c则是外部连接性.c++的const变量在编译期间会被常量代替,在实际程序中不分配内存,除非程序某处用到了该const变量的地址,除非其它目标文件extern这个const,除非这个const变量结构很复杂.c则没有这种优化.临时对象是const对象.const成员必须会被分配内存,需要再初始化列表中初始化.const成员变量有个特殊情况:static const,如果static const 是整型变量,它会被编译器优化,不会分配内存.而且它可以在声明的时候就初始化.当需要用到static const 成员变量的内存时,需要在实现的文件中定义这个变量,并且不能带初始化(不可以static const int Class::val = 0;).另外如果编译器不支持static const 成员变量,则需要enum hack . 成员方法可以是const函数(不是返回值为const),它保证不修改成员变量,除了mutable的变量.另外还可以用强制类型转换打破这种限制( void Y::f()const{ ((Y)this)->i++; }或者(const_cast<Y>(this))->i++ ).构造函数和析构函数不能是const函数.const实际上分为logical const和bitwiseconst.以上说的都是logical const,毕竟还有那么多方法可以打破规则.而bitwise const 则是严格的const, 如果它没有基类,且不自定义的构造函数和析构函数,那么它可以分配到ROM中.返回const内置类型无意义.注意
struct S { int i, j; }const S s[] = {{1, 2}}double d[s[0]]; // error s[0] 需要查内存
内联函数具有内部连接性,所以可以放在头文件中.内联构造函数和析构函数不一定可以加快效率.inline是向编译器提出将函数作为内联函数的请求,不一定能成功.当函数太复杂,如有循环,函数地址被调用,函数调用其它未定义函数,有继承,组合的类的构造函数和析构函数.宏一些不像函数的地方:参数本身是复杂的表达式时,新引入的操作符会改变原本希望的运算顺序,传入参数不是只传一次,当一个变量被传了两次的时候,可能在第二次传的时候该变量已经在第一次传的时候被修改,如向x + x,传 a++.仍然需要用到macro的地方有stringizing operator #, string concatenation, token pasting operator ##.在A类中定义静态A类成员可以让A类只实例化一次.有时候需要解决互相extern的问题.
当使用nested friend 的时候,应先声明nested类,然后声明其为友元,架在一起的时候,编译器会将其当成非nested看待.加了public,private,protected后内存不是按顺序布局.可以通过类指针来减少重复编译.
static作用,将局部变量分配到静态内存区,将全局变量设为内部连接性,内置变量初始化为0.因为exit()函数调用析构函数,所以在析构函数内调用exit()会死循环.abort()不会调用析构函数.局部静态对象在函数未调用时不会被构造.用无名namespace代替const.using 用法包括 scope resolution, using directive, using declaration.using 可放在函数内.static成员变量放在实现的文件中定义,即使为private.不管有没有const,对象数组要再实现中定义.static const数组定义要放在声明外,但声明为内部连接性,对象也如此.
函数调用过程为:将参数压栈,将返回地址压栈,将函数局部变量压栈,返回内置类型的时候,为了避免ISR工作完后修改了局部变量的内存区而没有返回正确的局部变量的值,需要通过寄存器返回变量.当参数为对象时,会有一个helper function将对象copy到栈中,当返回值为对象时,为了避免ISR干扰,而寄存器又不够大,所以会把被赋值的对象地址和函数参数在一开始的时候压入栈中.在返回的时候直接拷贝.当调用返回对象的函数而不用它来赋值的时候,会自动生成一个临时对象来拷贝.返回的对象是直接在return 后面用构造函数生成的时候,编译器会作优化,只进行一次拷贝,而不用先构造,拷贝,再析构.
默认参数值放在声明中.
重载operator = 的时候要注意检查是否自己给自己赋值,运算符重载应重载回内置的语义.Class object = Class();调用构造函数.object = Class();调用operator = .类型转换运算符.类型可以通过构造函数和类型转换运算符.
修改了基类函数参数列表或返回值类型会把基类所有函数重载版本都隐藏.operator = 不会被继承.私有继承可以用来隐藏基类部分接口,可以public: using Base::func();来解除接口的私有化.
可以delete void* ,但会有bug.new运算符可以被重载来管内存分配,从而提高效率或者解决堆内存碎片化问题.重载new分为成员函数重载和全局重载.作成员函数重载时会自动变成static.可以为数组重载new,delete.new失败后,返回0并且throw异常.还有placement new的用法.
当子类对象用值传给基类对象时,会发生object slicing,虚函数表指针也会不一样.子类的重载函数会覆盖基类的所有被重载的函数.当子类override基类虚函数时,不能修改返回值,基类虚函数返回值是基类指针,子类虚函数返回值是子类指针.构造函数中会被悄悄插入初始化虚函数表指针的代码.构造函数内的不会动态绑定,只会调用本类的虚函数.纯虚析构函数必须有函数体.子类的默认析构函数会调用基类的纯虚析构函数.析构函数中虚函数只会调用该类的,防止调用已经被子类析构的虚函数表.运算符重载也可以多态.downcast: dynamic_cast, static_cast
链接器会消除模板在不同文件中的重复定义.