作为一名刚来公司不久的新员工,有幸参加由侯捷老师做的高级C++培训,真的是很高兴。从接触Programming以来,C++一直是自己的主打语言(虽然最近正在研究Java^_^)。一天的培训下来,收获还是蛮大的,侯老师的细致入微的讲解给我留下了很深的印象。将我所得到的东西与大家分享同时对我来说又是个复习的过程,不失为一箭双雕之举^_^。

这次的高级C++培训分3个部分,分别为C++&ADT(一个独立Class的设计经验),C++&OOP,OOD&Patterns,我将用几篇文章来写我的收获,由于我的水平也有限,特别是对OOD和Patterns的理解不是很透彻,所以难免会出现不正确的“论点”还希望大家多多指点。

一、侯老师的开场白
侯老师有一部很有名的著作叫“无责任书评”(在侯老师的acer笔记本(我的本本也是acer的^_^)中,壁纸就是这本书的封面),在业界很受欢迎。侯老师自己是技术作家,也出版各类的技术书籍,所以对书有着他独到的见解。由于我们是高级C++培训,所以这次侯老师主要向我们介绍了所谓的C++大系的书籍,简单列举如下:(按侯捷的分类方式)

- 百科全书:《Thinking in C++》、《C++ Primer》、《The C++ Programming language 》、《The C++ Standard Library》
- 专家经验:《Effective C++》、《More Effective C++》、《Exceptional C++》、《More Exceptional C++》
- 底层内幕:《多态与虚拟》、《深入探索C++对象模型》
- 设计相关:《Design Pattern》、《Large Scale C++ Design》

二、正文(主题)

1、从C的角度看object-based programming(基于对象编程)
侯老师从C开始我觉得还是很合适的,一是培训的对象大多使用C做开发(这也与我们公司的业务领域息息相关,老的C程序员也许不太喜欢转移到C++),二是这样做可以间接的把C++编译器的实现方法展现在我们面前,便于对C++作深入理解。

用C提供的语法简单模拟C++类的概念。

分析C能给我们提供什么:
- C的struct能对data进行封装;
- C的struct可使用function pointer来模拟member function

C不能给我们提供什么:
- C的struct不能提供对access level的支持。

这里我也举个例子(不是侯老师讲义中的例子)来模拟C++。

#include "stdio.h"

#define MANAGER 0
#define CLERK    1
#define MEWER    2

typedef struct tagPerson
{
    int age;
    int baseSalary;
    int jobType;
    int (*calcSalary)(struct tagPerson*);
    void (*printInfo)(struct tagPerson*);
}Person;

int calcSalary(Person* this)
{
    if(this->jobType == MANAGER)
        return this->baseSalary*10;
    else if(this->jobType == CLERK)
        return this->baseSalary*5;
    else
        return this->baseSalary;
}

void printInfo(Person* this)
{
    //print the person's info include jobType,salary and so on;
    printf("the person's age is %d \n" , this->age);
}

int main()
{
    Person aPerson={24,1000,MANAGER,&calcSalary,&printInfo};
    Person bPerson={23,1000,CLERK,&calcSalary,&printInfo};
    int salary = aPerson.calcSalary(&aPerson);
    printf("the aperson's salary is %d\n" , salary);
    aPerson.printInfo(&aPerson);
    salary = bPerson.calcSalary(&bPerson);
    printf("the bperson's salary is %d\n" , salary);
    aPerson.printInfo(&bPerson);
}

值得大家注意的地方我都用加粗的字体了,在上面程序中我们先将Person的实例化为对象aPerson和bPerson,并手工将他们的地址传给他们自己的函数指针成员来模拟C++的成员函数的调用。现实中C++编译器将我们这一手工过程自动化了并隐藏了起来,也就是C++在每个成员函数的参数类表中偷偷添加了该对象的地址指针this。看起来也挺好理解的,但是这仅仅是C对C++简单的模拟,C++所提供的强大的面向对象的特性是C所不能比拟的,其实归根结底来说还是思维方式的转变带来的巨大变化。

2、建议使用最新的标准C++ style
C++从诞生那天到现在已经有20多年了,这期间C++程序的style也经历几次大的变化,直至今日,我们提倡采用C++标准程式库的code style,无论是初学者还是老手,都应该这么做,与标准靠拢是最好的选择。这里不详细阐述了,C++的creator的主页上就有很详尽的说明该如何写Standard C++代码,更有其观点“treat the standard c++ as a new language”。这里仅举几个简单观点和例子:

- standard header files  #include <= #include #include <= #include
- namespace std
- try to use stardard library as possible as you can!

3、forward declaration

以前一直对forward declaration不是很理解,今天终于有所突破了,所以就写下来,希望对那些和我有同样困惑的朋友们有所启发和帮助。看下面的两段代码:

代码段1:

class A; //forward declaration
class B
{
    //….
    A* a1;
    A* a2;
};

代码段2:

class A; //forward declaration
class B
{
    //….
    A a1;
    A a2;
};

直接告诉大家结论:代码段1顺利通过编译;代码段2则编译失败。

原因分析:

代码段1:由于B中的两个数据成员都是A*指针类型,指针类型在32位平台上大小都是4byte,编译器无需知道A的具体大小。那为什么还要有class A; //forward declaration这行代码呢,是因为编译器要知道代码中是否存在A这个类型,这行代码就是告诉编译器“你放心编译吧,这个A类型存在”。而代码段2的B中的两个数据成员都是A类型,编译器必须知道A的具体大小,仅仅告诉编译器A类型的存在是远远不够的。

4、function signature &function prototype(注意有特例)
Function prototype即是函数在声明时的所有元素的集合,包括函数名字,返回类型,参数列表;Funcition signature则是function prototype去掉返回类型后的剩余部分。

对于这两个概念我们还是举例说明比较直观,看下面的例子:

Function prototype:double calcSalary(Person* person);
Function signature:clacSalary(Person* person);

是否能够很好的区分这两个概念会直接关系到你对成员函数overloading的理解。牢记Overloading关注的是function signature 而不是function prototype!但是这里有个特例,那就是“pass by value和pass by reference是不同的signature么?”答案:不是。我们也可以举例说明这点,看下面的例子。

class A
{
    Public:
        int getArea(Circle& cir);
        int getArea(Circle cir);
};

main()
{
    Circle aCir;
    A a;
    a.getArea(aCir);//Ambiguous
}

某些编译器在class A的编译时并不报错,但是在真正调用时,发生模棱两可。

还有一个问题就是“为什么overloading不关心返回值类型呢”?我们还是举例说明一下:

class A
{
    Public:
        int PrintInfo(Person& a);
        void PrintInfo(Person& a);
};

main()
{
    Person aPerson;
    A a;
    a.PrintInfo(aPerson); //Ambiguous
}

说明:有些时候我们并不关心返回值,就像上例中的代码。一旦返回值类型可以作为overloading的一个评判依据,某些时候会造成模棱两可的错误。

5、尽量以const和inline替换#define(即macro)
这是个老话题,又是一个大家都容易犯的问题,这里就允许我再提一次吧^_^

使用macro无非两个用处:
1)、定义常量
2)、实现简单的函数功能

使用macro定义常量的缺点:
由于宏是由precompiler处理的,所以在真正的compiler处理之前就被precompiler移走了,没能进入符号表(symbol table),所以导致调试时的困难。

例:#define PI 3.1415926
我们可以用const double PI = 3.1415926替代。

还有一种class专有常数的例子:
class GamePlayer
{
    static const int NUM_TURNS = 5;
    int scores[NUM_TURNS];
    …
};
Const int GamePlayer::NUM_TURNS;

注意:in-class initialization只对整数类型(int ,bool,chars等)才成立且对常数才成立。

使用macro模拟函数功能的缺点:
例:
#define max(a , b) ((a) > (b)) ? (a): (b))
int a = 5 ,b =0;
max(++a , b); //我们期望是6>0,可实际结果是7>0,因为a被累加了两次。

我们的替代方法:inline int max(int a , int b){return a>b ? a : b ;}

Inline function可以对参数进行类型检查,而且拥有和macro一样的效率。

6、by reference vs by value
这里有几个原则(当然每个原则都不是强制性的,也都有特例),我们逐条来理解吧:

1)尽量使用by reference,不要使用by value,无论是传入参数还是传回返回值。
- reference通常不用于变量的修饰,多用于参数传递和返回值的修饰。
- by reference既有by pointer的效率,又保持接口与by value时不变,这些都是by reference的优点所在。
- 如果一定要by value,也不要钻牛角尖非得用by reference不可。(在下面一条的例子中就有体现)

2)不要在函式中传回local object的reference。(会造成dangling”空悬”问题)
Complex& func(…)
{
    Complex c;
    //…
    return c;
}

说明:这个函数会造成reference’s dangling problem,因为函数原型定义要传回reference,而代码却传回一个local object’s ref。

解决办法:修改返回值类型为by value

Complex func(…)
{
     Complex c;
     //…
     return c;
}

这样就一切ok了。(从函数代码的return c还看不出返回值是by value还是by reference, 得看函数原型的声明格式,如果是Complex&,则是by reference)。

7、const object与const member function
我们看一个类的成员函数的原型:

class Test
{
    //…
    const A& function(const B& b)const;
};

相信有很多人看完上述的函数原型后都有些“晕”,那么多const,都起什么作用亚。我们来一一讲解吧。

返回值类型:const A& —— 表示返回的值为常量,一般不能够作为左值,不能被修改(可作为右值)。
参数类型:const B& ——- 表示传入的参数b在函数的执行过程中状态应保持不变,不被修改。
函数后的修饰符const —— 表示该成员函数的执行不会改变类的状态,也就是说不会修改类的数据成员。

所谓的const object即在实例化时前面有const关键字修饰,如const A a;
所谓的const member function是指函数后有修饰符const,其通用的格式为:
    return_type fun_name(parameters list)const;

这样就会涉及到non-const object , const object 是否能够调用 non-const member function, const member function的问题,他们之间的调用关系详述如下:
- "const object" call "const member function" ok
- "non-const object" call "const member function" ok
- "const object" call "non-const member function" error
- "non-const object" call "non-const member function" ok
    
8、  friend & operator <<
有这样一段代码:

Complex c(3,5);
cout << c <<endl;

要想上一段代码编译和运行正常,我们应该做些什么呢?

在Complex类中重载<<符号,现在就有两种选择,是选择member func版还是non-member func版呢,根据上一段代码,cout << c,如果选择member func版,则书写方法应该是c<<cout,这明显不符合C++的习惯。所以我们选择non-member func版。版本选完后,我们来定这个重载函数的prototype, 由于cout为ostream类型且考虑到cout <<c<<c1这种级联形式,我们决定基本的原型如下:

osteam& operator<< (ostream& os , const Complex& r);

由于该函数需要访问Complex的private data member,所以我们再在前面加上一个friend关键字,完整的声明如下:

Class Complex
{
       //…
       friend osteam& operator<< (ostream& os , const Complex& r);
}

有了friend关键字表示Complex类告诉编译器operator<<函数是朋友,可以访问private data member。另外注意friend不具备传递性。

9、member func接受同型的obj,有无权利access private data member
我们还是看例子:

Complex& Complex::operator+=  (const Complex& x)
{
       m_real += x.m_real;
       m_imag+= x.m_imag;
       return *this;
}

从例子中看到member func接受同型的obj,是有权利access private data member的。

10、指针使用的好的编码习惯
当动态分配内存时,指针的使用应该格外的小心,一不注意就会造成memory leak。好的指针使用习惯会帮助你减少这种情况的发生。下面举例说明:

A p = new A();

当要free 这块内存时,应如下作法:

delete p;
p = null;//将p赋值为null以防止别人在free内存后,继续使用这个指针。

当使用p时,应如下作法:

if(p)//做一次判断,如果p不等于null,就可以使用了。
{
       p->dosomething();
}

© 2004, bigwhite. 版权所有.

Related posts:

  1. 本不是第一篇的第一篇