数组指针和指针数组

很多人以为“指向数组的指针”就是“指向指针的指针”,于是有人写这样的代码:
int a[3][4];
int **p = a;//错误
  这个代码的错误之处在于a是一个数组,它的成员也是数组,所以a叫做“数组的数组”——C++中严格说来没有二维数组。那么,你要用一个指针来记录a,就要用一个能表示“数组的数组”的指针,以下代码是正确的:
int (*p)[4] = a;//正确
  只有这样才能保证++p使p指向a的下一对像(该对像是一个数组)。
  顺便提一句:不要写成“int *p[4];”
来看一下指针数组的指针怎么用,既然都是指针也应该支持动态内存分配,下面的例子展示了如何对数组的指针动态分配内存:
#include <stdio.h>
int main()
{
    int array[3][5]={{1,2,2,4,4},{2,6,5,3,2},{3,5,7,3,5}};
    int (*pa)[5]=array;
    int *pb[3];
    int i=0;
     int (*pp)[5];
     pp = (int (*)[5]) malloc(sizeof(int) * 3);//注:需要这样对指向数组的指针分配空间
     pp = array;
    *pb=*pa;
    *(pb+1)=*(array+1);
    pb[2]=pa[2];
    for(;i<3;++i){
        int j=0;
        for(;j<5;++j)
        printf("%d",pb[i][j]);
        printf(" ");
     }
     printf("/npp pointer point to an array: /n");
     for(i = 0; i < 5; i++)
          printf("%d ",(*pp)[i]);
     printf("/n");
    return 0;
}

运行结果:

数组指针和指针数组

C++ 使用new 可以这样写:

#include <iostream>
using namespace std;

int main()
{
     int (*p)[3];
     int pp[2][3] = {1,2,3,4,5,6};
//     p = new int ((*)[])[3];
     p = (int (*)[3])new int [3];
     p = pp;
     for(int i =0; i < 3; i++)
          cout<<(*p)[i]<<" ";
     cout<<endl;
     p = pp + 1;
     for(int i =0; i < 3; i++)
          cout<<(*p)[i]<<" ";

     cout<<endl;
     return 0;
}

运行结果:

数组指针和指针数组

下面使用一个函数指针数组来展示一下指针数组的用法:

#include <iostream>
using namespace std;
typedef void (*F)();
void func_1()
{
    cout<< "func_1"<<endl;
}
void func_2()
{
    cout << "func_2"<<endl;
}
int main()
{

    F *p = (F *) new int [4];
    p[0] = func_1;
    p[1] = func_2;
    p[0]();
    p[1]();

    return 0;
}

数组指针和指针数组

注:这里指针数组主要是想说一下,函数指针数组如何动态分配。

 

想成为嵌入式程序员应知道的0x10个基本问题

  C语言测试是招聘嵌入式系统程序员过程中必须而且有效的方法。这些年,我既参加也组织了许多这种测试,在这过程中我意识到这些测试能为带面试者和被面试者提供许多有用信息,此外,撇开面试的压力不谈,这种测试也是相当有趣的。

从被面试者的角度来讲,你能了解许多关于出题者或监考者的情况。这个测试只是出题者为显示其对ANSI标准细节的知识而不是技术技巧而设计吗?这个愚蠢的问题吗?如要你答出某个字符的ASCII值。这些问题着重考察你的系统调用和内存分配策略方面的能力吗?这标志着出题者也许花时间在微机上而不上在嵌入式系统上。如果上述任何问题的答案是“是”的话,那么我知道我得认真考虑我是否应该去做这份工作。

从面试者的角度来讲,一个测试也许能从多方面揭示应试者的素质:最基本的,你能了解应试者C语言的水平。不管怎么样,看一下这人如何回答他不会的问题也是满有趣。应试者是以好的直觉做出明智的选择,还是只是瞎蒙呢?当应试者在某个问题上卡住时是找借口呢,还是表现出对问题的真正的好奇心,把这看成学习的机会呢?我发现这些信息与他们的测试成绩一样有用。

有了这些想法,我决定出一些真正针对嵌入式系统的考题,希望这些令人头痛的考题能给正在找工作的人一点帮住。这些问题都是我这些年实际碰到的。其中有些题很难,但它们应该都能给你一点启迪。

这个测试适于不同水平的应试者,大多数初级水平的应试者的成绩会很差,经验丰富的程序员应该有很好的成绩。为了让你能自己决定某些问题的偏好,每个问题没有分配分数,如果选择这些考题为你所用,请自行按你的意思分配分数。

预处理器(Preprocessor)

1 . 用预处理指令#define 声明一个常数,用以表明1年中有多少秒(忽略闰年问题)

#define SECONDS_PER_YEAR (60 * 60 * 24 * 365)UL

我在这想看到几件事情:

•; #define 语法的基本知识(例如:不能以分号结束,括号的使用,等等)

•; 懂得预处理器将为你计算常数表达式的值,因此,直接写出你是如何计算一年中有多少秒而不是计算出实际的值,是更清晰而没有代价的。

•; 意识到这个表达式将使一个16位机的整型数溢出-因此要用到长整型符号L,告诉编译器这个常数是的长整型数。

•; 如果你在你的表达式中用到UL(表示无符号长整型),那么你有了一个好的起点。记住,第一印象很重要。

 

2 . 写一个“标准”宏MIN ,这个宏输入两个参数并返回较小的一个。

#define MIN(A,B) ((A) <= (B) ? (A) : (B))

这个测试是为下面的目的而设的:

•; 标识#define在宏中应用的基本知识。这是很重要的,因为直到嵌入(inline)操作符变为标准C的一部分,宏是方便产生嵌入代码的唯一方法,对于嵌入式系统来说,为了能达到要求的性能,嵌入代码经常是必须的方法。

•; 三重条件操作符的知识。这个操作符存在C语言中的原因是它使得编译器能产生比if-then-else更优化的代码,了解这个用法是很重要的。

•; 懂得在宏中小心地把参数用括号括起来

•; 我也用这个问题开始讨论宏的副作用,例如:当你写下面的代码时会发生什么事?

least = MIN(*p++, b);

3. 预处理器标识#error的目的是什么?

如果你不知道答案,请看参考文献1。这问题对区分一个正常的伙计和一个书呆子是很有用的。只有书呆子才会读C语言课本的附录去找出象这种问题的答案。当然如果你不是在找一个书呆子,那么应试者最好希望自己不要知道答案。

死循环(Infinite loops)

4. 嵌入式系统中经常要用到无限循环,你怎么样用C编写死循环呢?

这个问题用几个解决方案。我首选的方案是:

while(1)

{

?}

一些程序员更喜欢如下方案:

for(;

{

?}

这个实现方式让我为难,因为这个语法没有确切表达到底怎么回事。如果一个应试者给出这个作为方案,我将用这个作为一个机会去探究他们这样做的基本原理。如果他们的基本答案是:“我被教着这样做,但从没有想到过为什么。”这会给我留下一个坏印象。

第三个方案是用 goto

Loop:

goto Loop;

应试者如给出上面的方案,这说明或者他是一个汇编语言程序员(这也许是好事)或者他是一个想进入新领域的BASIC/FORTRAN程序员。

数据声明(Data declarations)

 

5. 用变量a给出下面的定义

a) 一个整型数(An integer)

b)一个指向整型数的指针( A pointer to an integer)

c)一个指向指针的的指针,它指向的指针是指向一个整型数( A pointer to a pointer to an intege)r

d)一个有10个整型数的数组( An array of 10 integers)

e) 一个有10个指针的数组,该指针是指向一个整型数的。(An array of 10 pointers to integers)

f) 一个指向有10个整型数数组的指针( A pointer to an array of 10 integers)

g) 一个指向函数的指针,该函数有一个整型参数并返回一个整型数(A pointer to a function that takes an integer as an argument and returns an integer)

h)一个有10个指针的数组,该指针指向一个函数,该函数有一个整型参数并返回一个整型数( An array of ten pointers to functions that take an integer argument and return an integer )

答案是:

a) int a; // An integer

b) int *a; // A pointer to an integer

c) int **a; // A pointer to a pointer to an integer

d) int a[10]; // An array of 10 integers

e) int *a[10]; // An array of 10 pointers to integers

f) int (*a)[10]; // A pointer to an array of 10 integers

g) int (*a)(int); // A pointer to a function a that takes an integer argument and returns an integer

h) int (*a[10])(int); // An array of 10 pointers to functions that take an integer argument and return an integer

人们经常声称这里有几个问题是那种要翻一下书才能回答的问题,我同意这种说法。当我写这篇文章时,为了确定语法的正确性,我的确查了一下书。但是当我被面试的时候,我期望被问到这个问题(或者相近的问题)。因为在被面试的这段时间里,我确定我知道这个问题的答案。应试者如果不知道所有的答案(或至少大部分答案),那么也就没有为这次面试做准备,如果该面试者没有为这次面试做准备,那么他又能为什么出准备呢?

 

Static

6. 关键字static的作用是什么?

这个简单的问题很少有人能回答完全。在C语言中,关键字static有三个明显的作用:

•; 在函数体,一个被声明为静态的变量在这一函数被调用过程中维持其值不变。

•; 在模块内(但在函数体外),一个被声明为静态的变量可以被模块内所用函数访问,但不能被模块外其它函数访问。它是一个本地的全局变量。

•; 在模块内,一个被声明为静态的函数只可被这一模块内的其它函数调用。那就是,这个函数被限制在声明它的模块的本地范围内使用。

大多数应试者能正确回答第一部分,一部分能正确回答第二部分,同是很少的人能懂得第三部分。这是一个应试者的严重的缺点,因为他显然不懂得本地化数据和代码范围的好处和重要性。

 

Const

7.关键字const有什么含意?

我只要一听到被面试者说:“const意味着常数”,我就知道我正在和一个业余者打交道。去年Dan Saks已经在他的文章里完全概括了const的所有用法,因此ESP(译者:Embedded Systems Programming)的每一位读者应该非常熟悉const能做什么和不能做什么.如果你从没有读到那篇文章,只要能说出const意味着“只读”就可以了。尽管这个答案不是完全的答案,但我接受它作为一个正确的答案。(如果你想知道更详细的答案,仔细读一下Saks的文章吧。)

如果应试者能正确回答这个问题,我将问他一个附加的问题:

下面的声明都是什么意思?

const int a;

int const a;

const int *a;

int * const a;

int const * a const;

/******/

前两个的作用是一样,a是一个常整型数。第三个意味着a是一个指向常整型数的指针(也就是,整型数是不可修改的,但指针可以)。第四个意思a是一个指向整型数的常指针(也就是说,指针指向的整型数是可以修改的,但指针是不可修改的)。最后一个意味着a是一个指向常整型数的常指针(也就是说,指针指向的整型数是不可修改的,同时指针也是不可修改的)。如果应试者能正确回答这些问题,那么他就给我留下了一个好印象。顺带提一句,也许你可能会问,即使不用关键字 const,也还是能很容易写出功能正确的程序,那么我为什么还要如此看重关键字const呢?我也如下的几下理由:

•; 关键字const的作用是为给读你代码的人传达非常有用的信息,实际上,声明一个参数为常量是为了告诉了用户这个参数的应用目的。如果你曾花很多时间清理其它人留下的垃圾,你就会很快学会感谢这点多余的信息。(当然,懂得用const的程序员很少会留下的垃圾让别人来清理的。)

•; 通过给优化器一些附加的信息,使用关键字const也许能产生更紧凑的代码。

•; 合理地使用关键字const可以使编译器很自然地保护那些不希望被改变的参数,防止其被无意的代码修改。简而言之,这样可以减少bug的出现。

 

Volatile

8. 关键字volatile有什么含意?并给出三个不同的例子。

一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。下面是volatile变量的几个例子:

•; 并行设备的硬件寄存器(如:状态寄存器)

•; 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)

•; 多线程应用中被几个任务共享的变量

回答不出这个问题的人是不会被雇佣的。我认为这是区分C程序员和嵌入式系统程序员的最基本的问题。搞嵌入式的家伙们经常同硬件、中断、RTOS等等打交道,所有这些都要求用到volatile变量。不懂得volatile的内容将会带来灾难。

假设被面试者正确地回答了这是问题(嗯,怀疑是否会是这样),我将稍微深究一下,看一下这家伙是不是直正懂得volatile完全的重要性。

•; 一个参数既可以是const还可以是volatile吗?解释为什么。

•; 一个指针可以是volatile 吗?解释为什么。

•; 下面的函数有什么错误:

int square(volatile int *ptr)

{

return *ptr * *ptr;

}

下面是答案:

•; 是的。一个例子是只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。

•; 是的。尽管这并不很常见。一个例子是当一个中服务子程序修该一个指向一个buffer的指针时。

•; 这段代码有点变态。这段代码的目的是用来返指针*ptr指向值的平方,但是,由于*ptr指向一个volatile型参数,编译器将产生类似下面的代码:

int square(volatile int *ptr)

{

int a,b;

a = *ptr;

b = *ptr;

return a * b;

}

由于*ptr的值可能被意想不到地该变,因此a和b可能是不同的。结果,这段代码可能返不是你所期望的平方值!正确的代码如下:

long square(volatile int *ptr)

{

int a;

a = *ptr;

return a * a;

}

位操作(Bit manipulation)

9. 嵌入式系统总是要用户对变量或寄存器进行位操作。给定一个整型变量a,写两段代码,第一个设置a的bit 3,第二个清除a 的bit 3。在以上两个操作中,要保持其它位不变。

对这个问题有三种基本的反应

•; 不知道如何下手。该被面者从没做过任何嵌入式系统的工作。

•; 用bit fields。Bit fields是被扔到C语言死角的东西,它保证你的代码在不同编译器之间是不可移植的,同时也保证了的你的代码是不可重用的。我最近不幸看到 Infineon为其较复杂的通信芯片写的驱动程序,它用到了bit fields因此完全对我无用,因为我的编译器用其它的方式来实现bit fields的。从道德讲:永远不要让一个非嵌入式的家伙粘实际硬件的边。

•; 用 #defines 和 bit masks 操作。这是一个有极高可移植性的方法,是应该被用到的方法。最佳的解决方案如下:

 

#define BIT3 (0x1 << 3)

static int a;

void set_bit3(void) {

a |= BIT3;

}

void clear_bit3(void) {

a &= ~BIT3;

}

一些人喜欢为设置和清除值而定义一个掩码同时定义一些说明常数,这也是可以接受的。我希望看到几个要点:说明常数、|=和&=~操作。

访问固定的内存位置(Accessing fixed memory locations)

10. 嵌入式系统经常具有要求程序员去访问某特定的内存位置的特点。在某工程中,要求设置一绝对地址为0x67a9的整型变量的值为0xaa66。编译器是一个纯粹的ANSI编译器。写代码去完成这一任务。

这一问题测试你是否知道为了访问一绝对地址把一个整型数强制转换(typecast)为一指针是合法的。这一问题的实现方式随着个人风格不同而不同。典型的类似代码如下:

int *ptr;

ptr = (int *)0x67a9;

*ptr = 0xaa55;

A more obscure approach is:

一个较晦涩的方法是:

*(int * const)(0x67a9) = 0xaa55;

即使你的品味更接近第二种方案,但我建议你在面试时使用第一种方案。

中断(Interrupts)

11. 中断是嵌入式系统中重要的组成部分,这导致了很多编译开发商提供一种扩展—让标准C支持中断。具代表事实是,产生了一个新的关键字 __interrupt。下面的代码就使用了__interrupt关键字去定义了一个中断服务子程序(ISR),请评论一下这段代码的。

__interrupt double compute_area (double radius)

{

double area = PI * radius * radius;

printf(“/nArea = %f”, area);

return area;

}

这个函数有太多的错误了,以至让人不知从何说起了:

•; ISR 不能返回一个值。如果你不懂这个,那么你不会被雇用的。

•; ISR 不能传递参数。如果你没有看到这一点,你被雇用的机会等同第一项。

•; 在许多的处理器/编译器中,浮点一般都是不可重入的。有些处理器/编译器需要让额处的寄存器入栈,有些处理器/编译器就是不允许在ISR中做浮点运算。此外,ISR应该是短而有效率的,在ISR中做浮点运算是不明智的。

•; 与第三点一脉相承,printf()经常有重入和性能上的问题。如果你丢掉了第三和第四点,我不会太为难你的。不用说,如果你能得到后两点,那么你的被雇用前景越来越光明了。

*****

代码例子(Code examples)

12 . 下面的代码输出是什么,为什么?

void foo(void)

{

unsigned int a = 6;

int b = -20;

(a+b > 6) ? puts(“> 6”) : puts(“<= 6”);

}

这个问题测试你是否懂得C语言中的整数自动转换原则,我发现有些开发者懂得极少这些东西。不管如何,这无符号整型问题的答案是输出是 ”>6”。原因是当表达式中存在有符号类型和无符号类型时所有的操作数都自动转换为无符号类型。因此-20变成了一个非常大的正整数,所以该表达式计算出的结果大于6。这一点对于应当频繁用到无符号数据类型的嵌入式系统来说是丰常重要的。如果你答错了这个问题,你也就到了得不到这份工作的边缘。

13. 评价下面的代码片断:

unsigned int zero = 0;

unsigned int compzero = 0xFFFF;

/*1’s complement of zero */

对于一个int型不是16位的处理器为说,上面的代码是不正确的。应编写如下:

unsigned int compzero = ~0;

这一问题真正能揭露出应试者是否懂得处理器字长的重要性。在我的经验里,好的嵌入式程序员非常准确地明白硬件的细节和它的局限,然而PC机程序往往把硬件作为一个无法避免的烦恼。

到了这个阶段,应试者或者完全垂头丧气了或者信心满满志在必得。如果显然应试者不是很好,那么这个测试就在这里结束了。但如果显然应试者做得不错,那么我就扔出下面的追加问题,这些问题是比较难的,我想仅仅非常优秀的应试者能做得不错。提出这些问题,我希望更多看到应试者应付问题的方法,而不是答案。不管如何,你就当是这个娱乐吧…

动态内存分配(Dynamic memory allocation)

14. 尽管不像非嵌入式计算机那么常见,嵌入式系统还是有从堆(heap)中动态分配内存的过程的。那么嵌入式系统中,动态分配内存可能发生的问题是什么?

这里,我期望应试者能提到内存碎片,碎片收集的问题,变量的持行时间等等。这个主题已经在ESP杂志中被广泛地讨论过了(主要是 P.J. Plauger, 他的解释远远超过我这里能提到的任何解释),所有回过头看一下这些杂志吧!让应试者进入一种虚假的安全感觉后,我拿出这么一个小节目:

下面的代码片段的输出是什么,为什么?

char *ptr;

if ((ptr = (char *)malloc(0)) ==

NULL)

else

puts(“Got a null pointer”);

puts(“Got a valid pointer”);

这是一个有趣的问题。最近在我的一个同事不经意把0值传给了函数malloc,得到了一个合法的指针之后,我才想到这个问题。这就是上面的代码,该代码的输出是“Got a valid pointer”。我用这个来开始讨论这样的一问题,看看被面试者是否想到库例程这样做是正确。得到正确的答案固然重要,但解决问题的方法和你做决定的基本原理更重要些。

Typedef

:

15 Typedef 在C语言中频繁用以声明一个已经存在的数据类型的同义字。也可以用预处理器做类似的事。例如,思考一下下面的例子:

#define dPS struct s *

typedef struct s * tPS;

以上两种情况的意图都是要定义dPS 和 tPS 作为一个指向结构s指针。哪种方法更好呢?(如果有的话)为什么?

这是一个非常微妙的问题,任何人答对这个问题(正当的原因)是应当被恭喜的。答案是:typedef更好。思考下面的例子:

dPS p1,p2;

tPS p3,p4;

第一个扩展为

struct s * p1, p2;

.

上面的代码定义p1为一个指向结构的指,p2为一个实际的结构,这也许不是你想要的。第二个例子正确地定义了p3 和p4 两个指针。

晦涩的语法

16 . C语言同意一些令人震惊的结构,下面的结构是合法的吗,如果是它做些什么?

int a = 5, b = 7, c;

c = a+++b;

这个问题将做为这个测验的一个愉快的结尾。不管你相不相信,上面的例子是完全合乎语法的。问题是编译器如何处理它?水平不高的编译作者实际上会争论这个问题,根据最处理原则,编译器应当能处理尽可能所有合法的用法。因此,上面的代码被处理成:

c = a++ + b;

因此, 这段代码持行后a = 6, b = 7, c = 12。

如果你知道答案,或猜出正确答案,做得好。如果你不知道答案,我也不把这个当作问题。我发现这个问题的最大好处是这是一个关于代码编写风格,代码的可读性,代码的可修改性的好的话题。

好了,伙计们,你现在已经做完所有的测试了。这就是我出的C语言测试题,我怀着愉快的心情写完它,希望你以同样的心情读完它。如果是认为这是一个好的测试,那么尽量都用到你的找工作的过程中去吧。天知道也许过个一两年,我就不做现在的工作,也需要找一个。

 

注:转自学校内网论坛,别人也是转的,没找到出处

C++ 沉思录——Chap8:一个面向对象程序范例

面向对象编程的三要素:数据抽象、继承以及动态绑定。

这里讨论一个算术表达式树问题,如(-5)*(3+4)对应的表达式树为:

C++ 沉思录——Chap8:一个面向对象程序范例

我们希望通过调用合适的函数来创建这样的树,然后打印该树完整的括号化形式。例如:

Expr t = Expr("*", Expr("-",5), Expr("+", 3, 4));
cout << t << endl;

输出结果为:((-5)*(3+4))

此外我们不想为这些表达式的表示形式操心,也不想关心它们内存分配和回收的事宜。

 
从上面图我们可以看出,图中有两种对象节点和箭头。每个节点包含一个值——一个操作数或者一个操作符——并且每个结点又具有零个、一个或者两个子节点。
这些节点既是相同的又是不同的,我们该如何抽象节点的数据结构呢?
 
这些节点首先有一个共同的特点:每个类都要存储一个值以及一些子节点。同时也可以很容易看出这些节点的一些不同点,比如它们存储的值的种类,子节点的数目。
 
在设计类的时候继承使得我们可以不中这些类的共同点,而动态绑定又可以帮助各个节点确定身份(从而才去不同的操作)。
 
仔细分析我们可以把节点分为三种类型:一种表示整数表达式,包含一个整数值,无子节点。另外两种分别表示一元表达式和二元表达式(包含一个操作符,分别有一个或者两个子节点)。因为这些节点的不同,我们在打印的时候也要采取不同的操作,这时候就可以使用动态绑定了:我们可以定义一个virtual 函数来指明应当如何打印各种节点。
注意到这三种节点都成为节点,我们可以先定义一个共同的基类:
class Expr_node
{
     friend     ostream & operator << ( ostream &, const Expr_node &);
     
     protected:
               virtual void print(ostream &) const = 0;
               virtual ~Expr_node() { }
}; 
由于Expr_node 类是一个虚基类,不能实例化对象,只有从其中派生类来实例化对象。Expr_node 类的存在只是为了获得公共的接口。
 
接着我们先来定义输出操作符,它要调用“对应”的print函数:
ostream & operator << ( ostream & o, const Expr_node & e)
{
     e.print(o);
     return 0;
} 
按照前面对三种节点的分析,可以很快定义出第一种节点:
class Int_node : public Expr_node
{
     private:
          int n;
     public:
          Int_node( int k) : n(k) {}
          void print(ostream & o) const { o << n; }
};

 

对于一元表达式或者二元表达式,因为其中包含有子节点,这时候我们并不知道子节点的类型会是什么,因此不能按值存储子节点,必须存储指针。
class Unary_node : public Expr_node
{
     private:
          string op;
          Expr_node *opnd;
     public:
          Unary_node( const string &a, Expr_node *b) : op(a), opnd(b) { }
          void print(ostream & o) const { o << "(" << op << *opnd << ")"; }
};
 
class Binary_node : public Expr_node
{
     private:
          string op;
          Expr_node *left;
          Expr_node *right;
     public:
          Binary_node( const string &a, Expr_node *b, Expr_node *c) : op(a),left(b),right(c) { }
          void print( ostream & o) const { o << "(" << *left << op << *right << ")"; } 
};

 

从现有的定义来看,如果我们要创建下面的表达式树:
Expr t = Expr( “*”, Expr(“-“,5), Expr(“+”,3,4) );
 
则需要像下面一样来实现:(创建一元和二元表达式树的构造函数期望获得指针,而不是对象)
Binary_node *t = new Binary_node("*", new Unary_node("-",new Int_node(5), new Binary_node("+", new Int_node(3), new Int_node(4) );
     这个改进离我们理想的表达方式还有差距,并且我们不再拥有指向内层new的对象的指针,因此上述代码的情形会造成内存泄露,如果我们通过定义好析构函数来解决这个问题,则又可能会多次删除对象,因为理想情况下可能有多个Expr_node指向同一个下层表达式对象,这种形式把内存管理的事情都交给了用户。
 
     既然用户关心的只是树,而不是树中的单个节点,就可以用Expr来隐藏Expr_node的继承层次。这里又回到了前面讨论的句柄类的内容,用户可见的只有Expr。既然用户要乘船的是Expr 而不是Expr_node, 我们就希望Expr的构造函数能代表所有3种Expr_node。每个Expr构造函数豆浆创建Expr_node的派生类的一个合适对象,并且将这个对象的地址存储在正在创建的Expr对象中。Expr 类的用户不会直接看到Expr_node 对象。
 
class Expr
{
     private:
          Expr_node *p;
          friend ostream & operator <<( ostream &, const Expr &);
     public:
          Expr(int );
          Expr(const string &, Expr);
          Expr(const string &, Expr, Expr );
          Expr(const Expr &);
          Expr & operator = (const Expr &);
          ~Expr() { delete p; }
};

 

一系列的构造函数创建适当的Expr_node,并将其地址存储在p中:
Expr :: Expr(int n)
{
     p = new Int_node(n);
}
Expr :: Expr(const string& op, Expr t)
{
     p = new Unary_node(op, t);
}
Expr :: Expr(const string &op, Expr left, Expr right)
{
     p = new Binary_node(op, left, right);
}

 

现在使用Expr 为 Expr_node 分配内存,我i类避免不必要的复制,我们依然维护一个引用计数,但这里引用计数包含在Expr_node 里面,Expr 类和 Expr_node 类系统管理引用计数,因此Expr 需要作为Expr_node的友元出现。
 
class Expr_node
{
          friend ostream & operator <<(ostream &, const Expr &);
          friend class Expr;
          int use;
     protected:
          Expr_node() : use(1) { }
          virtual void print( ostream &) const = 0;
          virtual ~Expr_node() { }     
}; 
当Expr 类“复制”一个Expr_node 时,该Expr 将其引用计数增1,当引用为0的时候删除底层的Expr_node:
Expr 类需要增加复制构造函数,析构函数和赋值函数:
class Expr
{
     // 和前面一样
     public:
          Expr (const Expr & t) { p = t.p; ++p->use; }
          ~Expr() { if( --p->use == 0) delete p; }
          Expr & operator = (const Expr & t);
};
Expr & Expr :: operator = (const Expr &rhs)
{
     rhs.p->use++;
     if(--p->use == 0)
          delete p;
     p = rhs.p;
     return *this;
} 
针对Expr 我们还需要定义输出操作符:
ostream & operator << (ostream & o, const Expr & t)
{
     t.p->print(o);
     return o;
}

 

最后需要更改每个派生自Expr_node的类,令其操作为私有,将Expr 类声明为友元,存储Expr而不是Expr_node的指针,例如:
class Binary_node : public Expr_node
{
     friend class Expr;
     string op;
     Expr left;
     Expr right;
     Binary_node
}; 

 

如果我们需要对表达式求值,在现在的构架下也很容易实现,可以在 Expr 类中增加 eval 成员方法,eval 可以将实际的求值工作委托为做出Expr 的结点来完成。
 
class Expr
{
     private:
          // 和前面一样
     public// 和前面一样
          int eval() const { return p->eval() ; } // 新添加的
};
这样 Expr_node 类就需要添加另一个纯虚函数:
class Expr_node
{
     protected:
          virtual int eval() const = 0;
          // 和前面一样
};

 

针对Expr_node 派生的每一个类添加一个函数来实现求值运算。(这里就不单独列出)
 
将全部代码列出:
/*
既然用户关心的只是树,而不是树中的单个节点,就可以用Expr来隐藏Expr_node的继承层次。
这里又回到了前面讨论的句柄类的内容,用户可见的只有Expr了,内存管理的事情就完全由Expr掌控!
改进后代码如下:
*/
#include <iostream>
#include <string>
using namespace std;
 
class Expr_node
{
friend class Expr; //友元类可以被继承,句柄类Expr还要操作这里的use,所以是必须的
int use;            //引用计数
public:
virtual void print(ostream&) const = 0;
virtual int eval() const = 0;
public:
Expr_node():use(1) {}
virtual ~Expr_node() {}
};
 
class Expr                     //句柄类
{
friend ostream& operator<<(ostream &o, const Expr &e);
private:
Expr_node *p;       //指向基类的指针
public:
Expr(int n);
Expr(const string &op, Expr t);
Expr(const string &op, Expr left, Expr right);
Expr(const Expr &t);
Expr& operator=(const Expr&);
int eval() const { return p->eval();};
~Expr()
{ 
if(--p->use == 0)
delete p;
}
};
 
class Int_node: public Expr_node
{
private:
friend class Expr;
int n;
//public:
Int_node(int k):n(k) {}
void print(ostream &o) const
{
o << n;
}
int eval() const { return n;}
};
 
 
class Unary_node: public Expr_node
{
private:
friend class Expr;
string op;
Expr opnd;
//public:
Unary_node(const string &a, Expr b):op(a), opnd(b) {}
void print(ostream &o) const
{
o << "(" << op << opnd << ")";
}
int eval() const 
{ 
if(op == "-")
return -opnd.eval();
throw "error, bad op" + op + "int UnaryNode";
}
};
 
class Binary_node: public Expr_node
{
private:
friend class Expr;
string op;
Expr left;
Expr right;
//public:
Binary_node(const string &a, Expr b, Expr c):op(a), left(b), right(c) {}
void print(ostream &o) const
{
o << "(" << left << op << right << ")";
}
int eval() const
{
int op1 = left.eval();
int op2 = right.eval();
 
if(op == "-") return op1 - op2;
if(op == "+") return op1 + op2;
if(op == "*") return op1 * op2;
if(op == "/" && op2 != 0) return op1 / op2;
}
};
 
Expr::Expr(int n) { p = new Int_node(n); }
Expr::Expr(const string& op, Expr t) { p = new Unary_node(op,t); }
Expr::Expr(const string &op, Expr left, Expr right) { p = new Binary_node(op, left, right); }
Expr::Expr(const Expr& t) { p = t.p; ++p->use; }
 
Expr& Expr::operator=(const Expr& rhs)
{
rhs.p->use++;
if(--p->use == 0)
delete p;
p = rhs.p;
return *this;
}
 
ostream& operator<<(ostream &o, const Expr &e)
{
e.p->print(o);
return o;
}
 
void main()
{
Expr t = Expr("*",
Expr("-", Expr(5)),
Expr("+", Expr(3), Expr(4)));
cout << t << endl;
cout << t.eval()<<endl;
 
}
/*
这个例子很好的展示了面向对象的三个要素,这样设计出的类具有很好的扩展性,比如再增加有多个子节点的节点,
只要添加个新类,然后在Expr中添加个新构造函数就行了。用户完全不必知道底层的代码是怎么实现的。以后面对
问题的时候要好好的借鉴这种思想!
 
*/
运行结果:
C:/Windows/system32/cmd.exe /c  chap8_Expr2.exe
((-5)*(3+4))
-35
Hit any key to close this window…

C++ 沉思录——Chap6:句柄2

上一回讨论的句柄技术有一个明显的缺点:为了将句柄捆绑到类T的对象上,必须要新定义一个具有类型为T的成员对象的新类。
这个毛病相当麻烦,如果想新设计一个类的句柄类,就需要新定义两个类。
 
C++之父提到过一种定义句柄类的技术可以弥补这一个缺点,主要思想就是将引用技术从数据中分离出来,把引用计数放到句柄类自己的对象之中。
class Handle
{
     public:
          // 和前面一样
     private:
          Point *p;
          int *u;
};
这里不再有指向UPoint的指针,我们使用指向Point的指针和指向一个int的指针表示引用计数。使用Point* 使得我们不仅能够将一个Handle绑定到一个Point,还能将其绑定到一个继承自Point 的类的对象。
 
此时我们的Handle类要在构造析构的时候要处理两个指针,比如:
Handle :: Handle() : u(new int(1)), p(new Point ) { }
 
Handle :: Handle(int x, int y) : u( new int(1)), p(new Point(x, y)) { }
// 需要正确的增加或减少引用计数
Handle :: Handle(const Handle &h) : u(h.u), p(h.p) { ++*u; }
 
Handle&  Handle :: operator = (const Handle &h)
{
     ++*h.u;
     if ( --*u == 0)
     {
          delete u;
          delete p;     
     }
     u = h.u;
     p = h.p;
     return *this;
}
 
从这些实现可以看出,引用计数与Point类没有能够很好的协同,每次都需要单独处理它们。
 
如果想让引用计数和Point类更好的协同作用可以对引用计数进行抽象,用一个辅助类来实现引用计数。
 
class UseCount
{
     public:
          UseCount();
          UseCount(const UseCount &);
          UseCount & operator = (const UseCount &);
          ~UseCount();
 
     private:
          int *p;
};
UseCount成员函数的实现就比较简单了:
UseCount :: UseCount() : p(new int(1)) {}
UseCount :: UseCount(const UseCount &u) : p(u.p) { ++*p; }
UseCount :: ~UseCount {  if ( --*p == 0) delete p;  }
 
现在重写Handle类:
class Handle
{
     public:
          // 和之前一样
     private:
          UseCount u;
          Point *p;
};
 
现在我们来看成员函数的实现,就相对比之前简单了:
Handle :: Handle() : p(new Point) { }
Handle :: Handle(int x, int y) : p(new Point(x,y)) { }
Handle :: Handle(const Point & p0) : p(new Point(p0)) { }
Handle :: Handle(const Handle & h) : u(h.u), p(h.p) { }

 

在写析构函数的时候,我们需要判断引用计数是否为0,以便知道要不要删除句柄的数据。
我们可以让UseCount 类来提供这个数据,通过一个类成员方法来藐视UseCount 对象是不是唯一指向它的计数器对象:
class UseCount
{
     public:
          // 和前面一样
     private:
          bool only();
};
 
bool UseCount :: only() {     return *p == 1; }
 
当UseCount 有了 only() 成员方法,Handle类的析构函数就可以这样写了:
 
Handle :: ~Handle()
{
     if( u.only() )
          delete p;
} 
当Handle类进行复制操作的时候,我们需要对引用计数值增1,或者减1,可能还要删除一个计数器。
但是现阶段我们设计的UseCount 类和Hanlde 类都不支持上面的操作。注意到我们引入UseCount类的原因是使引用计数的处理和Point 协同起来。因此我们将这些操作放在UseCount中来实现,可以增加下面的成员函数:
 
bool UseCount :: reattach( const UseCount & u)
{
     ++*u.p;
     if ( --*p == 0)
     {
          delete p;
          p = u.p;
          return true;
     }
     p = u.p;
     return false;
}

 

现在有了reattach() 成员方法之后,Handle类的赋值操作可以这样写:
Handle & Handle :: operator = (const Handle & h)
{
     if(u.reattach(h.u))
          delete p;
     p = h.p;
     return *this;
} 
 
最后,如果我们要改变Point (注意前面的一切Handle、UseCount 类都是为了管理和控制Point类,我们真正需要操作的数据其实是Point 类 , 虽然都是在Handle类层进行操作以达到操作Point对象的目的),也就是对Point对象的单个元素进行读取和写入。当有多个句柄关联同一个Point对象时,我们对某个句柄操作需要改变该Point对象,但是其他句柄关联所持有的信息不需要改变,此时我们就需要对Point进行复制(记得前面引入Handle 的作用之一也就是避免不必要的复制,但是这里就需要进行复制了!)。
因此可以再UseCount类中在增加一个成员函数,用来对引用计数进行适当的控制。
bool UseCount :: makeonly()
{
     if ( *p == 1)
          return false;// 这里的意思就是说,只有一个Handle关联该对象,因此可以直接进行改变,不需要复制对象
     --*p;
     p = new int (1);
     return true;
} 
 
Handle 类的存取函数可以这样写:
int Handle :: x() const
{
     return p->x();
}
 
Handle & Handle :: x(int x0)
{
     if ( u.makeonly() )
          p = new Point( *p);
     p->x(x0);
     return *this;
}

 

现在列出全部代码:
/*
为了将句柄捆绑到类T的对象上,必须要新定义一个具有类型为T的成员对象的新类。
这个毛病相当麻烦,如果想新设计一个类的句柄类,就需要新定义两个类。
C++之父提到过一种定义句柄类的技术可以弥补这一个缺点,主要思想就是将引用技术从数据中分离出来,
把引用计数放到句柄类自己的对象之中。

这个策略的一个重要优势:UseCount类可以在不了解其使用者任何信息的情况下与之合为一体。这样一来,
我们就可以把这个类当成句柄实现的一部分,与各种不同的数据结构协同工作。 
*/

#include <iostream>
using namespace std;
//-----------------------------------------
class Point
{
private:
    int xval,yval;
public:
    Point():xval(0),yval(0){}
    Point(int x,int y):xval(x),yval(y){}
    int x()const{return xval;}
    int y()const{return yval;}
    Point& x(int xv){xval=xv;return *this;}
    Point& y(int yv){yval=yv;return *this;}
};
//------------------------------------------------------

class UseCount
{
private:
    int* p;
public:
    UseCount();
    UseCount(const UseCount&);
    UseCount& operator=(const UseCount&);
    ~UseCount();
    bool only();
    bool reattach(const UseCount&);
    bool make_only();
};
UseCount::UseCount():p(new int(1)){}
UseCount::UseCount(const UseCount&u):p(u.p){++*p;}
UseCount::~UseCount()
{
    if (--*p==0)
    {
        delete p;
    }
}
bool UseCount::only()
{
    return *p==1;
}
bool UseCount::reattach(const UseCount& u)
{
    ++*u.p;
    if (--*p==0)
    {
        delete p;
        p=u.p;
        return true;
    }
    p=u.p;
    return false;
}
bool UseCount::make_only()
{
    if (*p==1)
        return false;
    --*p;
    p=new int(1);
    return true;
}
//-------------------------------------------

class Handle
{
private:
    Point* p;
    UseCount u;
public:
    Handle();
    Handle(int,int);
    Handle(const Point&);
    Handle(const Handle&);
    Handle& operator =(const Handle&);
    ~Handle();
    int x()const;
    Handle&x(int);
    int y()const;
    Handle&y(int);
};
Handle::Handle():p(new Point){}
Handle::Handle(int x,int y):p(new Point(x,y)){}
Handle::Handle(const Point&p0):p(new Point(p0)){}
Handle::Handle(const Handle&h):u(h.u),p(h.p){}
Handle::~Handle()
{
    if (u.only())
    {
        delete p;
    }
}
Handle& Handle::operator=(const Handle &h)
{
    if (u.reattach(h.u))
        delete p;
    p=h.p;
    return *this;
}
int Handle::x()const
{
    return p->x();
}
int Handle::y()const
{
    return p->y();
}
Handle& Handle::x(int x0)
{
    if (u.make_only())
        p=new Point(*p);
    p->x(x0);
    return *this;
}
Handle& Handle::y(int y0)
{
    if (u.make_only())
        p=new Point(*p);
    p->y(y0);
    return *this;
 
}
//---------------------------------------------------

int main()
{
    Handle h(3,4);
    Handle h2 = h;
    h2.x(5); 
    int n = h.x();
    cout<<n<<endl;
    return 0;
}

运行结果为:3

 

 

C++ 沉思录——Chap6:句柄

     第五章介绍了代理类,这个类能让我们在一个容器中存储类型不同但相互关联的对象。这种方法需要为每个对象创建一个代理,并要将代理存储在容器中。创建代理将会复制所代理的对象。

     如果想避免这些复制该怎么做呢?可以使用句柄类。它允许在保持代理的多态行为的同时,还可以避免进行不必要的复制。
     处于多态的环境中,我们可以知道对象的基类类型,但是不知道对象本身的类型或者怎么样复制这种类型的对象。
     如果多个指针指向同一个对象,就必须考虑要在什么时候删除对象。不能太早也不能太晚,太早删除,就会有某个仍然指向它的指针存在,再使用这个指针就会产生未定义行为。删除得太晚又会占用本来早该另作它用的空间。
     需要一种方法,让我们避免某些缺点的同时又能够获取指针的某些优点,尤其是在能够保持多态性的前提下避免复制对象的代价。C++的解决方法就是定义一个适当的类。由于这些类的对象通常被绑定到它们所控制的对象上,所以这些类常被称为句柄类(handle class)。
     
假定有这样一个类:
class Point
{
private:
     int xval, yval;
public:
     Point() : xval(0), yval(0) { }
     Point(int x, int y) : xval(x), yval(y) { }
     int x() const { return xval;}
     int y() const { return yval;}
     Point& x(int xv)
     {
          xval = xv;
          return *this;
     }
     Point& y(int yv)
     {
          yval = yv;
          return *this;
     }
};
     handle 应该“控制”它所绑定的对象,也就是说handle应该创建和销毁对象。有两种方式:可以创建自己的Point对象并把它赋给一个handle去进行复制,或者可以把用于创建Point的参数传给这个handle。我们要允许这两种方法,所以想让handle类的构造函数和Point类的构造函数一样。也就是,我们想用:
 Handle h0(123,456);
来创建绑定到新分配的坐标为123和456的Point的handle,而用:
Handle h(p);
创建副本,并将handle绑定到该副本。这样,handle就可以控制对副本的操作。从效果上说,handle 就是一种包含单个对象的容器。
 
     当handle绑定到Point类之后,可以对 “->”进行重载,使用operator-> 将handle的所有操作转发给相应的Point操作来执行。但是这种操作也会过于暴露Point的操作,如果想绕开operator->(),就必须为handle提供自己的x和y操作,这两个操作要么返回int,要么返回Handle&。
 
     根据上面的分析给出Handle 类的大致轮廓:
    

 class Handle
     {
          public:
               Handle();
               Handle(int, int);
               Handle(const Point &);
               Handle(const Handle &);
               Handle & operator=(const Handle &);
               ~Handle();
 
               int x() const;
               Handle & x(int );
               int y()  const;
               Handle & y(int );
          private:
               // ..
     }; 
使用句柄原因之一就是为了避免不必要的对象复制,也就是说允许多个句柄绑定到单个对象上。我们必须了解有多少个句柄绑定在同一个对象上,只有这样才能确定应当何时删除对象。而通常使用引用计数来达到这个目的。
 
但是,这个引用计数不能是句柄的一部分,否则句柄的设计会相当麻烦。也不能让引用计数成为对象的一部分,因为那样要求我们重写已经存在的对象类。我们必须定义一个新的类来容纳一个引用计数和一个Point对象。我们成为UPoint。这个类纯粹是为了实现而设计的,所以我们把其所有成员都设置为private,并且将我们的句柄类声明为友元。我们希望能以创建Point的全部方式创建UPoint对象,所以:
     class UPoint
     {
          friend class Handle;
          Point P;
          int     u;
          
          UPoint() : u(1) { } // 引用计数初始化为1
          UPoint(int x , int y) : P(x, y), u(1) { }
          UPoint(const Point & p0) : P(p0), u(1) { }
     } 
现在可以完善Handle类了          
class Handle
{
private:
     UPoint *up;    //和间接层UPoint打交道了
public:
     Handle();
     Handle(int, int);
     Handle(const Point&);
     Handle(const Handle&);
     Handle& operator=(const Handle&);
     ~Handle();
     int x() const;
     Handle& x(int);
     int y() const;
     Handle& y(int);
}; 
现在来列出全部代码:
/*
从效果上来说,handle就是一种只包含单个对象的容器,它通过允许多个handle
对象指向同一个对象来避免复制。定义句柄类,我们还需要新定义一个类来容
纳被引用类对象的引用计数和被引用类对象本身。

这里的引用计数为0时删除p的意思应该是由handle创建的p
handle的构造函数中有一种是跟p有关的,在这类构造函数中
创建了p并初始化了p的计数,当这个计数为0时删除的是由handle创建
的p,如果Point自身不实例化对象的话,这样就真的实现了删除p对象了

可见,handle需要提供hanle(const handle &) , hanle(const Point &)和hanle(int x, int y)
三类构造函数的必要性,都是为了创建Point对象,而且这样创建的Point对象可以与handle关联

*/
#include <iostream>
using namespace std;

class Point{
public:
    Point() : xval(0),yval(0){};
    Point(int x, int y): xval(x), yval(y){};
    int x() const {return xval;};
    int y() const {return yval;};
    Point& x(int xv) 
    { 
        xval = xv; y
        return *this;
    };
    Point& y(int yv) 
    { 
        yval = yv; 
        return *this;
    };
private:
    int xval, yval;
};

class UPoint{                            //引用计数类
    friend class Handle;
    Point p;
    int u;                                   //引用计数变量
    UPoint(): u(1){};
    UPoint(int x, int y): p(x,y), u(1){};
    UPoint(const Point& p0): p(p0),u(1){};
};

class Handle{                       //句柄类
public:
    Handle(): up(new UPoint){};
    Handle(int x,int y): up(new UPoint(x,y)){};//按创建Point的方式构造handle,handle->UPoint->Point
    Handle(const Point& p): up(new UPoint(p)){};//创建Point的副本
    Handle(const Handle& h): up(h.up){ ++up->u;};//此处复制的是handle,但是底层的point对象并未复制,只是引用计数加1
    Handle& operator=(const Handle& h)
    {
        ++h.up->u;                //右边的对象引用计数加1,左边的减1
        if(--up->u == 0)
            delete up;
        up = h.up;
        return *this;
    };
    ~Handle()
    {
        if(--up->u == 0)
        delete up;
    };
    int x() const{return up->p.x();};
    Handle& x(int xv)
    {
        up->p.x(xv); 
        return *this;
    };

    int y() const{return up->p.y();};

    Handle& y(int yv)
    {
        up->p.y(yv); 
        return *this;
    };
    int OutputU(){return up->u;};   //输出引用个数
private:
    UPoint* up;
};

int main()
{
    //Point *p = new Point(8,9); 
    Point p(8,9);
    //Point p1 = p.x(88);
    //Point *pp = &p;
    Handle h1(1,2);
    Handle h2 = h1;        //此处调用的是构造函数Handle(const Handle& h)
    h2.x(3).y(4);               //此处的特殊写法是因为写xy函数内返回了对象
    Handle h3(5,6);        //此处调用Handle的赋值运算符重载函数Handle& operator=(const Handle& h)
    h1 = h3;
    Handle h4(p);
    Handle h5(h4);
    h4.x(7).y(8);
    //Handle h5(p1);
    //Handle h5 = h4;
    cout <<"h1(" << h1.x() <<":"<< h1.y() << "):" << h1.OutputU() <<endl;
    cout <<"h2(" << h2.x() <<":"<< h2.y() << "):" << h2.OutputU() <<endl;
    cout <<"h3(" << h3.x() <<":"<< h3.y() << "):" << h3.OutputU() <<endl;
    cout <<"h4(" << h4.x() <<":"<< h4.y() << "):" << h4.OutputU() <<endl;
    cout <<"h5(" << h5.x() <<":"<< h5.y() << "):" << h5.OutputU() <<endl;
    //delete pp; //不能这样,不是用new分配的空间
    //cout <<"h5(" << h5.x() <<":"<< h5.y() << "):" << h5.OutputU() <<endl;
    cout<<p.x()<<" "<<p.y()<<endl;
    //cout<<&p1<<endl;
    return 0;
}

 

运行结果:

C++ 沉思录——Chap6:句柄

C++ 沉思录——Chap5:代理类

前言:
   OOP 的 意思 在我看来就是使用继承和动态绑定的编程方式。继承是一种抽象,它允许程序员在某些时候忽略相似对象之间的差异,又在其他时候利用这些差异。

     C++ 程序员都应该知道,只有在程序通过指向基类对象的指针或基类对象的引用调用虚函数时,才会发生运行时的多态现象。
     对象的创建和复制不是运行时多态的,这一点严重影响了类的设计。所以,容器——无论是类似于数组或者结构体的内建容器还是用户自定义容器类——只能获得编译时类型一致的元素值。如果有一系列类之间存在继承关系,当我们需要创建、复制和存储对象,而这些对象的确切类型只有到运行时才能知道时,则这种编译时的检查会带来一些麻烦。
     通常,解决这个问题的方法是增加一个间接层。传统C模型可能会建议采用指针来实现这个间接层。这样会给需要使用这些类的用户带来负面影响,使得他们必须参与内存管理。C++ 采用了一种更自然的方法,就是定义一个类来提供并隐藏这个间接层。这种类通常叫做句柄类,本片的许多章都将参照或者使用这种技术。句柄类采用简单的形式,把一个单一类型的对象与一个与之有某种特定继承关系的任意类型的对象捆绑起来。所有,句柄可以让我们忽略正在处理的对象的准确类型,同时还能避免指针带来的关于内存管理的麻烦。


 
如何设计一个容器,能使得它能包含类型不同但是彼此相关的对象?
 
假设有一个表示不同种类的交通工具的类派生层次:
class Vehicle{
          public:
               virtual double weight() const = 0;
               virtual void start() = 0;
               // ...
};
class RoadVehicle : public Vehicle{ / * ... */ };
class AutoVehicle : public RoadVehicle{ /* ... */};
class Aircraft : public Vehicle { / * ... */};
class Helicopter : public Aircraft { / * ... */}; 

 

下面假设我们要跟踪处理一系列不同种类的Vehicle,首先使用数组来实现:
 
Vehicle parking_lot[1000];
上面的写法有问题,因为Vehicle作为抽象类是不能被实例化的。
假设可以以实例化,然后进行下面操作:
AutoVehicle  x= /* ... */
Parking_lot[num_vehicles++] = x;
这样也是不行的,x转化为一个Vehicle对象时会产生问题,x 会丢失在Vehicle类中没有的成员,也就是发生了“截断”。
因此,我们只能说parking_lot 是Vehicle的集合,而不是所有继承自Vehicle的对象的集合。
 
经典解决方案:
 
常见做法提供一个间接层,最早的合适间接层形式就是存储指针,而不是对象本身:
Vehicle * Parking_lot[1000];          // 指针数组
然后:
AutoVehicle x = /* ... */
Parking_lot[ num_vehicles++ ] = &x;
这种情况带来了新的问题:

一旦x不存在了,Parking_lot就不知道指向哪里了。

       我们可以变通一下,让放入Parking_lot的值,指向原来对象的副本,而不直接存放原来对象的地址,例如:     

Parking_lot[num_vehicles++]= new AutoVehicle(x);

 

       这个改进方案又带来一个问题,那就是我们必须知道x的静态类型。假如我们想让parking_lot[p]指向的Vehicle的类型和值与parking_lot[q]指向的对象相同,那在我们无法得知parking_lot[q]的静态类型的情况下,我们无法做到这点。

 
可以想一个办法来复制编译时类型未知的对象:增加虚拟复制函数来复制编译时类型未知的对象
 
class Vehicle
{
     public:
          virtual Vehicle * copy() const = 0;
     /**/
};//Vehicle的派生类都自实现copy函数
class Truck : public RoadVehicle
{
public:
            Vehicle* copy() const { return new Truck(*this); }
     /**/
};
 
//同时也该增加一个析构函数:
class Vehicle{
          publicvirtual double weight() const = 0;
               virtual void start() = 0;   
               virtual Vehicle * copy() const = 0;
               virtual ~Vehicle() {}
               // ...
};
假如我们想让parking_lot[p]指向的Vehicle的类型和值与parking_lot[q]指向的对象相同,可以简单是使用如下代码:parking_lot[p] =parking_lot[q].copy();
 
有没有一种方法既能避免显示地处理内存分配,又能保持Vehicle在运行时绑定的属性?
 
用类来表示概念! 在复制对象的过程中运用这个设计原则,就是定义一个行为和Vehicle对象相似、而又潜在地表示了多有继承自Vehicle类的对象的东西。我们把这种累对象叫做代理类。每个Vehicle代理都代表某个继承自Vehicle类的对象。
class VehicleSurrogate
{
public:
VehicleSurrogate() : vp(NULL) {};
VehicleSurrogate(const Vehicle& v) : vp(v.copy()) {};
~VehicleSurrogate() {};
VehicleSurrogate(const VehicleSurrogate& v) : vp(v.vp ? v.vp->copy() : NULL) {}; //v.vp非零检测
VehicleSurrogate& operator=(const VehicleSurrogate& v) 
{
if (this != &v) // 确保没有将代理赋值给它自身
{
  delete vp;
  vp = (v.vp ? v.vp->copy() : NULL);
}
 
return *this;
};
 
//来自类Vehicle的操作
void start()
{
if (vp == 0)
  throw "empty VehicleSurrogate.start()";
 
vp->start();
};
        double weight() const 
        {
               if(vp == 0)
                    throw "empty VehcileSurrogate.weight()";
               return vp->weight();
        }
 
private:
  Vehicle* vp;
}; 
现在我们可以进行下面的操作了:
VehcileSurrogate  parking_lot[ 1000 ]
AutoVehicle x;
parking_lot[ num_vehicles++] = x;
最后一条语句等价于:
parking_lot[ num_vehicles++ ] = VehicleSurrogate(x);
这句话创建了一个关于该对象的副本,并将VehicleSurrogate对象绑定到该副本,然后将这个对象赋值给parking_lot 的一个元素。
 

C++ 沉思录——Chap4:设计类的核查表

一. 你需要一个构造函数吗?

     构造函数 是一种特殊的方法 主要用来在创建对象时初始化对象 即为对象成员变量赋初始值,总与new运算符一起使用在创建对象的语句中 特别的一个类可以有多个构造函数 可根据其参数个数的不同或参数类型的不同来区分它们 即构造函数的重载。构造函数的特点:1.构造函数的命名必须和类名完全相同。 2.构造函数的功能主要用于在类的对象创建时定义初始化的状态。它没有返回值,也不能用void来修饰。3.构造函数不能被直接调用,必须通过new运算符在创建对象时才会自动调用。
     有些内太简单,它们的结构就是它们的接口,所以不需要构造函数。但是一般情况下都需要构造函数来初始化成员变量。
二. 你的数据成员是私有的吗?
     一般都需要将类的数据成员设置为私有,否则设计者将无法控制何时访问这些成员。
三. 你的类需要一个没有参数的构造函数吗?
     如果一个类已经有了构造函数,而你想声明该类的对象可以不必显示的初始化它们,则必须显示地写一个午餐的构造函数,例如:
class Point{
          public :
               Point(int p, int q) : x(p),y(q) {}
               …
          private:
               int x, y;
}
此时,如果使用下面的语句则为非法:
 
如果需要生成某个类对象的数组的时候,也需要一个无参的构造函数。
 
四. 是不是每个构造函数初始化所有的数据成员?
 
 
五. 类需要析构函数吗?
     如果类分配了资源,而这些资源又不会由成员函数自动释放,则需要析构函数
 
六. 类需要一个虚析构函数吗?
     绝不会用作基类的类是不需要虚析构函数的。如果一个类需要被继承一般都需要有一个虚析构函数。虚析构函数通常是空的。
 
七. 你的类需要复制构造函数吗?
     关键要看复制该类的对象是否就相当于复制器数据成员和基类对象。如果不是则需要复制构造函数。
     如果你的类在构造函数中分配了资源,则需要一个显示的复制构造函数来管理资源。
     如:(此时就需要复制构造函数)
     class String {
          public:
               String();
               String(const char *s);
               …
          private:
               char *data;  //data 指向了其他的内存,这时候就需要复制构造函数了
}
     一般情况下,当类中包含有指针类型的 数据成员,则需要三大函数(复制构造函数,复制操作符,析构函数)。
 
八. 你的类需要一个赋值操作符吗?
     如果需要一个复制构造函数,就一般也需要一个赋值操作符。
 
九. 你的赋值操作符能正确地将对象赋值给对象本身吗?
     
     比如有这样一个类:
     class String{
          public:
               String & operator = (const String &s);
          private:
               char *data;
     };
 
下面是一个不正确例子:(一旦向自身赋值则会失败)
     String & String::operator=(const String &s)
     {
          delete [] data;
          data = new char [strlen(s.data) + 1];
          strcpy(data, s.data);
          return *this;
     }
 
一下是正确的实现方式:
     String & String :: operator=(const String &s)
     {
          if( &s != this)
          {
               delete [] data;
               data = new char[strlen(s.data) + 1];
               strcpy(data, s.data);
          }
          return *this;
     }
 
十. 你的类需要定义关系操作符吗?
 
十一. 删除数组时你记得用delete[] 吗?
 
十二. 记得在复制构造函数和复制操作符的参数类型中加上const吗?
 
十三. 如果函数有引用参数,他们应该是const引用吗?
 
 

FCoE的提出

  传统业务结构下,由于多种技术之间的孤立性(LAN 与 SAN),使得数据中心服务器总是提供多个对外 I/O 接口(在此,可理解成服务器的网卡):用于数据计算与交互的 LAN 接口以及数据访问的存储接口,某些特殊环境如特定 HPC(高性能计算)环境下的超低时延接口。服务器的多个 I/O 接口导致了数据中心环境下多个独立运行的网络同时存在,不仅使得数据中心布线复杂,不同的网络、接口形体造成的异构还直接增加了额外人员的运行维护、培训管理等高昂成本投入,特别是存储网络的低兼容性特点,使得数据中心的业务扩展往往存在约束。
  数据中心里会有两个网络,一个是前端 IP 网络,后端可能会是光纤网络,都会在服务器上做集中服务器上会有以太网卡,会有光纤网卡,跟外部数据交互时候通过 IP 网络进行交互。如果说的更极端一点,在大型数据中心里有:
1.  前端的用户通信网络(以太网)
2.  后台存储网络光纤的通道(FC 光纤网络)
3.  后端做数据更新或者做集群计算的通讯网络(高性能计算 Infiniband 网络)
4.  专门用于虚拟机迁移的网络(各个服务器上有一个普通的以太网网卡,连接到独立的交换机组成的网络上,专门做虚拟机迁移。随着 TRILL 等技术的出现,这个专用的网络不再需要)最多情况下会有八个网卡,这些都是现有的设计,大家可能认为是理所当然的。
FCoE的提出

FCoE的提出

  网络成为数据中心资源的交换枢纽。当前数据中心分为 IP 数据网络、存储网络、服务器集群网络。但随着数据中心规模的逐步增大,也带来的以下问题: 
1.  每个服务器要多个专用适配器(网卡),要不同的布线系统;  
5.  机房要支持更多设备:空间、耗电、制冷;
6.  多套网络无法统一管理,不同的维护人员;
7.  部署/配置/管理/运维困难;
8.  ……
  在下一代的数据中心网络中,存储 FC/网络 IP 等业务最终都降承载在以太网上,围绕国际标准 FCoE(FC  over  Ethernet,以太网数据包内部  FC 帧与 IP 数据分离设计)。网络中原有 FC存储网络(FC 交换机)可连接到数据中心以太网交换机上,下一代 FCoE 磁盘阵列可直接连接到数据中心交换机上。同时由于以太网数据包内部  FC 帧与 IP 数据分离设计,可有效保证存储网络安全。

FCoE的提出

  通过 FCoE,每台服务器不再需要多张网卡,只需要一个 FCoE 网卡就可以与前端的客户进行IP 通信、后端的存储通过光纤进行通讯。这个变革对用户资金的投入、网络建设、运维都是革命性改变有效解决了管理人员分工不同、空间/能耗、不同的布线系统等问题。


存储相关基本概念

缩略语          英文全名                                                     中文解释

ETS              Enhanced Transmission Selection              增强传输选择。 
PFC              Priority-based Flow Control                        基于优先级的流控制
DCB               Data Center Bridging                                   数据中心网桥
DCBX             Data Center Bridging eXchange Protocol    数据中心桥接交换协议
SP(PQ)           Strict Priority Queue                                      严格优先级队列。
WRR               Weighted Round Robin                                加权循环调度,用于带宽分配的调度算法。
LAN                Local Area Network                                       局域网
IPC                 Inter-process communication                       进程间通讯
TC                  Traffic Class                                                    传输类,一般指一个队列 。
FCoE              Fibre Channel over Ethernet                         将光纤通道地映射到以太网,从而可以在以太网传输SAN 

SAN               Storage Area Network                              存储域网络,支撑技术为 FC(Fiber Channel)
DCE               Data Center Ethernet                                数据中心以太网
InfiniBand       infinite band                                             是一种开放标准的。
LLDP                Link Layer Discovery Protocol                    链路层发现协议
ETS                  Enhanced Transmission Selection              增强传输选择
PFC                  Priority-based Flow Control                        基于优先级的流量控制
DCB                 Data Center Bridging                                数据中心桥接 
FCF                FCoE Forwarder                                       FCoE 转发器
CLI                 Command-line interface                          命令行界面
NPIV             N_Port ID virtualization                             N 端口ID 虚拟化
FIP                FCoE Initialization protocol                       FCoE 初始化协议
VF_Port         Virtual F_Port                                           FCoE Switch VF 端口
VE_Port         Virtual E_Port                                             FCoE Switch VE 端口                                                                                                     
E_Node                                                   与交换机相连的 N 节点设备包括: N VN E VE端口
TRILL       Transparent Interconnection of Lots of links 多连接半透明互联
                                                             
说明:存储相关系列文章,主要介绍我在学习过程中总结的资料,现在我的工作主要是虚拟化、SR-IOV、openFCoE、CNA卡相关的研究,一个人学习研究感觉难度比较大。

free 与 delete

1. delete 用于释放 new 分配的空间,free 有用释放 malloc 分配的空间

2. delete [] 用于释放 new [] 分配的空间
3. delete 释放空间的时候会调用 相应对象的析构函数
     顺便说一下new在分配空间的时候同时会调用对象的构造函数,对对象进行初始化,使用malloc则只是分配内存
4. 调用free 之前需要检查 需要释放的指针是否为空,使用delete 释放内存则不需要检查指针是否为NULL
5. free 和 delete 不能混用,也就是说new 分配的内存空间最好不要使用使用free 来释放,malloc 分配的空间也不要使用 delete来释放
     举个例子,<string.h>里通常有个strdup函数,它得到一个char*字符串然后返回其拷贝:
     char * strdup(const char *ps); // 返回ps所指的拷贝
     在有些地方,c和c++用的是同一个strdup版本,所以函数内部是用malloc分配内存。这样的话,一些不知情的c++程序员会在调用strdup后忽视了必须对   strdup返回的指针进行free操作。为了防止这一情况,有些地方会专门为c++重写strdup,并在函数内部调用了new,这就要求其调用者记得最后delete。你可以想象,这会导致多么严重的移植性问题,因为代码中strdup以不同的形式在不同的地方之间颠来倒去。
 
补充一个问题,free和delete 是如何知道需要释放的内存块的大小的?
     
     在调用malloc或new 分配内存空间的时候,实际分配的空间会比程序员申请的空间要大。实际分配的内存空间前面有一部分空间用于保存所分配内存的大小,校验和等信息。当分配函数返回时,将会返回实际可操作的地址(也就是实际分配空间加上前面用于记录分配信息的空间之后的地址)。下面举个例子,例子通过破坏 new 返回地址的前面四个字节的数据导致内存空间释放出问题。如果不破坏前面的数据则不会出现内存不能释放的情况。
#include <stdio.h>
#include <new>
#include <iostream>
#include <stdlib.h>
#include <string.h>

int main()
{
    int *p = NULL,*p1=NULL;
    int i;
    //p = (int *) malloc(10 * sizeof(int));
    p = new int[10];

    memset(p,0,sizeof(int) * 10);
    for(i=0;i<10;i++)
        printf("P:%d/t",p[i]);
    printf("addr p: %x/n",p);

    *(p-1) = 2; //如果不注释掉这一行则程序运行不正确
    *(p+11) = 3;
    printf("addr before p: %x/n",p+11);
    printf("%x %x/n",*(p-1),*(p+11));

    //free(p);
    delete [] p;
    printf("free successfully! /n");
    return 0;
}

当注释了*(p-1) = 2之后运行结果为:

free 与 delete

当不注释*(p-1) =2 这一行时,结果为:

free 与 delete