C/C++ 基础语法注意事项 (二)

  • 任何的对象都具有某种特征和行为。OOAD把一个对象的特征称为属性,而把其行为称为服务或方法。

  • 若在逻辑上BA一种is-a-kind-of),则允许B继承A的功能和属性。注意多继承(虚继承可以解决)

  • 组合,用于表示类的整体与部分关系,若在逻辑上AB一部分is-a-part-of),则不允许BA派生,而是要用A和其他部分组合出B

  • 类的组合特性具体表现为两种:聚合(has-a)和关联(holds-a)。更多的类之间的关系。下面引用了《大话设计模式》一个类的关联图。我觉得这张图通俗易懂。

C/C++ 基础语法注意事项 (二)_第1张图片

  • 在绝大多数情况下,程序的功能是在编译的时候就确定下来的,我们成为静态特性。反之,如果程序的功能在运行时刻才确定下来的,则称为动态特性。

  • 一旦类的一个函数被声明为虚函数,那么其派生类的对应函数也自动成为虚函数,这样一级级地传递下去。但是为了提高程序的清晰性,建议你不厌其烦地每一个派生层次中将它显示地声明为虚函数。

  • 如果将基类的虚函数声明为纯虚函数,那么该类就被定义为了抽象基类。抽象基类的主要用途是接口与实现分离”。

  • 每一个具有虚函数的类都叫做多态类,C++编译器必须为每一个多态类至少创建一个虚函数表,它其实就是一个函数指针数组,其中存放着这个类说有的虚函数的地址以及该类的类型信息,其中也包括哪些继承但未改写的虚函数。对虚函数调用语句被编译器改写成:(*p->_vptr[slotNum]))(p, arg-list);

  • 派生类定义中的名字(对象或函数名)将义无反顾地遮蔽掉积累中任何同名的对象或函数。换句话说,派生类中定义了一个(testFunc(int))与基类(testFunc())同名却参数不同的虚函数,此时你调用 派生类指针-> testFunc(); 此时编译器会报错,除非你在派生类里面又显示的定义testFunc(),派生类指针-> testFunc();才能编译通过。

  • C++支持运行时多态特性的手段有两种,这里讲的虚函数机制是其中主要的一种,另一中是RTTI.

  • 删除一个多态对象或者是数组要多加的注意,例如: A * pA = new B(); delete pA; 时,实际上只是调用了 A类的析构函数,而B类的析构函数没有调用到,解决办法就是在数组里放基类的指针或者是基类的智能指针,然后就是为每个类声明虚析构函数。

  • 基本的C++对象模型有下述几个规则:(1)非静态数据成员被放在每一个对象体内作为对象专有的数据成员。(2)静态数据成员被提取出来放在程序的静态数据区内为该类所有对象贡献个,因此仅存一份。(3)静态和非静态成员函数最终都被提取出来放在程序的代码段中并为该类的所有对象共享,因此每个成员函数也只存在一份代码实体。(4)类内嵌套定义的各种类型(typedefclassstructunionenum等)与放在类外面定义的类型除了作于不同外没有本质区别。因此,构成对象本身的只有数据,任何成员函数都不隶属与任何一个对象,非静态成员函数与对象的关系就是绑定,绑定的中介就是this指针。

C/C++ 基础语法注意事项 (二)_第2张图片

  • 增加了继承和虚函数的派生类对象模型如下:(1)派生类继承基类的非静态数据成员,并作为自己对象的专用数据成员。(2)派生类继承基类的非静态成员函数并可以像自己的成员函数一样访问。(3)为每个多态类创建一个虚函数指针数组vtable,该类的所有虚函数(继承基类或者新增的)的地址都保存在这张表里。(4)多态类的每一个对象中安插一个指针成员vptr,其类型为指向函数指针的指针,它总是指向所属类的vtableVptrC++对象的隐含数据成员之一。(5)如果基类已经插入了vptr,则派生类将继承和重用该vptr。(6)如果派生类是从多个基类继承或者有多个继承分支(从所有根类开始算起),而其中若干个继承分支上出现了多态类,则派生类将从这些分之中的每个分支上继承一个vptr,编译器也将为它生成多个vtable,有几个vptr就生成几个vtable,分别与它的多态基类对应。(7vptr在派生类对象中的相对位置不会随着继承层次的逐渐加深而改变,并且现在的编译器一般都将vptr放在所有数据成员的前面。

C/C++ 基础语法注意事项 (二)_第3张图片

  • 为了支持RTTI,为每一个多态类创建一个type_info对象,并把其地址保存在vtable中的固定位置(一般为第一个位置 ——这一条取决与具体的编译器的实现,标准并没有规定)。

  • C++编译器构建vtable的方法的猜想:(1)定义如果下通用的虚函数指针类型和vtable类型 typedef void(_cdel * PVFN)(void); typedef struct{ type-info * _pTypeInfo; PVFN _arrayOfPvfn[];} VTABLE;(2)在每一个继承分支中的第一个多肽类中插入vptr,而在每一个多态类中都插入vtable的声明。(3)vptr的初始化和改写在class的各个构造函数和析构函数中完成。(4)改写调用语句由pShape->Draw();改写成(*(pShape->_vptr[2]))(pShape).但是有个疑问,从vtable中取出来的函数指针类型应该是PVFN,与实际调用的虚函数的类型一般是不匹配的,但是编译器知道我们定义的每一个虚函数的类型,所以还应该有一个反向类型强制转换的过程。(*(P_VFN_Draw)(pShape->_vptr[2]))(pShape)。

  • Vtable中虚函数指针的排列顺序:(1)一个徐含糊如果在当前class中是第一次出现,则将其地址插入到该类class的每一个vtable的尾部。(2)如果派生类改写了基类的虚函数,则这个函数的地址在派生类vtable中的位置与它在其基类vtable中的位置一致。(3)派生类没有改写的基类虚函数被继承下来并插入派生类vtable中(与该虚函数所在基类对应下来的那个vtable),且在派生类vtable中的位置与其在基类vtable的位置相同。派生类的vtable布局应该兼容其基类的vtable.(4)多继承的vtable以及虚继承的对象模式与vptr, vtable会更加的复杂,如果有兴趣可以再去查一些资料看一下。

  • 为了安全起见,建议大家尽量避免在构造函数和析构函数中调用虚函数。

  • 不要在构造函数内做与初始化对象无关的工作,不要在析构函数内做域销毁一个对象无关的工作。在这两个函数中应该做最少的工作量,否则会降低效率,甚至可能会让人误解。例如对于一个用于消息发送和接收的类来说,不应该在构造函数内打开一个socket连接,不要在析构函数内断开一个socket连接。打开和关闭都是应该定义一个专门打开或者是关闭的函数。

  • 一个被创建起来的对象:当给一个对象分配好原始内存空间的时候,这个对象就应该算是创建起来了,只不过它还处于一种“原始状态”,即未初始化的,不良的状态。如果把这样的内存直接拿来使用,除非第一个操作时赋值,否则极有可能出错。

  • 初始化与赋值的区别:初始化是对象创建的同时使用初值直接填充对象的内存单元,因此不会有数据类型转换等中间过程,也就不会产生临时对象;而赋值则是在对对象创建好后任何时候都可以调用的而且可以多次调用的函数,由于它调用的是“=”运算符,因此可能需要进行类型转换,即会产生临时对象。

  • 我们一般习惯在构造函数体内来初始化数据成员,然而这不是真正意义上的初始化,而是赋值。真正的初始化时使用所谓的“初始化表达式”。

  • 构造函数初始化列表的使用规则:(1)如果类存在继承关系,派生类可以直接在其初始化列表里调用基类的特定构造函数以向它传递参数,因为我们不能在初始化对象时访问基类的数据成员。(2)类的非静态const数据成员和引用成员只能在初始化列表里初始化,因为它们只存在初始化语义,而不存在赋值语句。

  • 初始化顺序:(1)调用基类的构造函数,像它们传递参数。(2)初始化本类的数据成员(包括成员对象的初始化)。(3)在函数体内完成其他的初始化工作。

  • 一、C++函数重载便于记忆,提高了函数的易用性。二、类的构造函数需要重载机制,C++规定构造函数必须与类名相同,因此只能有一个名字,所以只能用重载机制来实现。

  • 二进制可执行程序中是不允许有同名函数出现,所有的函数最终都会转换成等效的全局函数,调用语句也会做相应的转换。

  • 只能靠参数列表而不能仅靠返回值类型的不同来区分重载函数。

  • 注意重载、覆盖与隐藏的区别。重载:相同作用域,函数名相同,参数列表不同;覆盖:不同作用域,函数名相同,参数列表完全相同。基类函数必须是虚函数(多态);隐藏:函数名相同,参数列表完全相同(没有virtual)或者是有所差异(不论有无virtual),这时,派生类的函数就是隐藏掉基类的同名的函数。

  • 默认值应该出现在函数的声明中,而不是在函数体定义中。如果函数有多个参数,那么参数只能从后向前依次默认。

  • 运算符重载:C++语言中,可以用关键字operator加上运算符来表示函数。即,运算符本身就是函数名。所以的一元运算符,建议重载为非静态成员函数。=、()、【】、->、*只能重载为非静态成员函数。+=、-=、/=、*=、&=、|=、~=、%=、>>=、<<=建议重载为非静态成员函数。所有其他运算符,建议重载为全局函数。

  • 不能重载“..*::?:sizeof typeid ### static_case<>dynamic_case<>consst_cast<>reinterpret_cast<>”。

  • 正确理解++a a++; int b = ++a ---> a+=1; int b = a; a++ -à int temp =a; a+=1; int b = temp; 在正确理解之后,我们在使用的过程中,要尽量用++a,不要用a++; 因为后者总是要创建一个临时对象,在退出函数时还要销毁它,而且返回临时对象的值时还会调用其拷贝构造函数。

  • C程序中,可以用宏代码提高效率。宏代码本身不是函数,使用起来像函数。编译预处理器复制宏代码的方式取代函数调用,省去了参数压栈、生成会变语言的call调用、返回参数,执行return等过程,从而提高了速度。

  • C++语言支持函数内联,其目的是为了提高函数的执行效率。原理:对任何内联函数,编译器在符号表里放入函数的声明,包括名字,参数类型,返回值类型。(符号表是编译器用来收集和保存字面敞亮和某些符号常量的地方)。如果编译没有发现内联函数时,编译器首先检查调用是否正确,如果正确,内联函数的代码就会直接替换函数调用语句,于是省去了函数调用的开销(增加空间开销,替换时间开销)。C++的内联函数既具备宏代码的效率,又增加了安全性。同时,函数被内联后,编译器就可以通过上下文相关的优化技术对结果代码执行更深入的优化,这种优化在普通函数体内是无法单独进行的,因为一旦进入函数体内它也就脱离了调用环境的上下文。

  • 关键字,inline 与函数定义体放在一起。注意:在类声明之中定义的成员函数自动地成为内联函数。在class A{ public: void Function(void) { …}}.

  • 内联函数,它以代码膨胀为代价(消耗内存空间),仅仅省去了函数调用的开销,从而提高了程序的执行效率(这里的开销,仅指参数压栈,跳转,退栈,和返回操作的开销。)

  • 注意不要轻易的让构造函数和析构函数成为内联函数。构造函数和析构函数代码规模有时候会很大,将它们声明成内联函数会得不偿失。

  • 类型转换函数的本质是创建新的目标对象,并以源对象的值来初始化,所以源对象没有任何的改变,不要把类型转换理解为“将源对象的类型转换为目标类型”。

  • 内联函数,它以代码膨胀为代价(消耗内存空间),仅仅省去了函数调用的开销,从而提高了程序的执行效率(这里的开销,仅指参数压栈,跳转,退栈,和返回操作的开销。)

  • 任何不会修改数据成员的成员函数都应该声明为const类型。如果在编写const成员函数时,不慎写下了试图修改数据成员的代码,或者调用了其他非const成员函数,编译器将指出错误。

  • Static成员函数不能定义为const的。这是因为static成员函数只是全局函数的一个形式上的封装,而全局函数不存在const一说;何况static成员函数不能访问类的非静态成员。

你可能感兴趣的:(面向对象,c/c++,高质量编程)