C++ Advanced Training(一)

类别:编程语言 点击:0 评论:0 推荐:

C++ Advanced Training(一)

-------C++&ADT

 

作为一名刚来公司不久的新员工,有幸参加由侯捷老师做的高级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);

}

 

output:

the person's salary is 10000

the person's age is 24

the bperson's salary is 5000

the person's age is 23

 

值得大家注意的地方我都用红色加粗的字体了,在上面程序中我们先将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 <cstdio> ß#include <stdio.h>

#include <iostream> ß#include <iostream.h>

²        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

non-const object

const member function

ok

ok

non-const member function

err

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();

}

 

 

 

本文地址:http://com.8s8s.com/it/it25532.htm