码迷,mamicode.com
首页 > 编程语言 > 详细

《C和C++程序员面试秘笈[精品]》-笔记

时间:2016-05-18 10:50:55      阅读:481      评论:0      收藏:0      [点我收藏+]

标签:

2015-12-16
原文:在C++中可以通过域操作符“::”来直接操作全局变量


2015-12-16
原文:后缀式(i++)必须返回对象的值,所以导致在大对象的时候产生了较大的复制开销,引起效率降低。因此处理使用者自定义类型(注意不是指内建类型)的时候,应该尽可能地使用前缀式递增/递减,因为它天生“体质”较佳。


2015-12-16
原文:内建数据类型的情况,效率没有区别。 自定义数据类型的情况,++i效率较高。


2015-12-16
原文:当表达式中存在有符号类型和无符号类型时,所有的操作数都自动转换成无符号类型


2015-12-18
原文:按位异或运算符“^”的功能是将参与运算的两数各对应的二进制位相异或,如果对应的二进制位相同,则结果为0,否则结果为1


2015-12-22
原文:C是一个结构化语言,它的重点在于算法和数据结构。对语言本身而言,C是C++的子集。C程序的设计首要考虑的是如何通过一个过程,对输入进行运算处理,得到输出。对于C++,首要考虑的是如何构造一个对象模型,让这个模型能够配合对应的问题,这样就可以通过获取对象的状态信息得到输出或实现过程控制。 因此,C与C++的最大区别在于,它们用于解决问题的思想方法不一样。


2015-12-22
原文:C 是面向过程化的,但是 C++不是完全面向对象化的。在 C++中也完全可以写出与 C一样过程化的程序,所以只能说C++拥有面向对象的特性。Java是真正面向对象化的。


2015-12-24
原文:很多时候,我们需要在程序退出的时候做一些诸如释放资源的操作,但程序退出的方式有很多种,例如main()函数运行结束,在程序的某个地方用exit()结束程序,用户通过Ctrl+C等操作发信号来终止程序,等等,因此需要有一种与程序退出方式无关的方法来进行程序退出时的必要处理。方法就是用atexit()函数来注册程序正常终止时要被调用的函数。 atexit()函数的参数是一个函数指针,函数指针指向一个没有参数也没有返回值的函数。


2015-12-24
原文:可以用 atexit()函数来注册程序正常终止时要被调用的函数,并且在 main()函数结束时,调用这些函数的顺序与注册它们的顺序相反。


2015-12-25
原文:宏只是简单的文本替换


2015-12-25
原文:使用#把宏参数变为一个字符串,用##把两个宏参数贴合在一起。


2015-12-25
原文:1 #define WORD_LO(xxx) ((byte) ((word)(xxx) & 255)) 2 #define WORD_HI(xxx) ((byte) ((word)(xxx) >> 8)) 一个字由两个字节组成。因此WORD_LO(xxx)取参数xxx的低8位,WORD_HI(xxx)取参数xxx的高8位。


2015-12-25
原文:#define ARR_SIZE(a) (sizeof((a)) / sizeof((a[0]))) 假设一个数组定义如下。 int array[100]; 它含有100个int型的元素。如果int为4个字节,那么这个数组总共有400个字节。sizeof(array)为总大小,即400个字节;sizeof(array[0])为一个int大小,即4个字节。两个大小相除就是 100,也就是数组的元素个数。这里,为了保证宏定义不会发生“二义性”,在a以及a[0]上都加了括号。


2015-12-31
原文:a1定义为const int*类型,注意这里的const在int*的左侧,它是用修饰指针所指向的变量,即指针指向为常量


2015-12-31
原文:a2定义为int* const类型,注意这里的const在int*的右侧,它是用修饰指针本身,即指针本身为常量


2015-12-31
原文:a3 定义为const int* const类型,这里有两个const,分别出现在int*的左右两侧,因此它表示不仅指针本身不能修改,并且其指向的内容也不能修改


2016-01-04
原文:#define常量则是一个Compile-Time概念,它的生命周期止于编译期,它存在于程序的代码段,在实际程序中它只是一个常数、一个命令中的参数,并没有实际的存在。 const常量存在于程序的数据段,并在堆栈分配了空间。const常量是一个Run-Time的概念,它在程序中确确实实地存在着并可以被调用、传递。const常量有数据类型,而宏常量没有数据类型。编译器可以对const常量进行类型安全检查。


2016-01-04
原文:C++中const有什么作用(至少说出3个)


2016-01-04
原文:(1)const用于定义常量:const定义的常量编译器可以对其进行数据静态类型安全检查。


2016-01-04
原文:(2)const修饰函数形式参数:当输入参数为用户自定义类型和抽象数据类型时,应该将“值传递”改为“const &传递”,可以提高效率。比较下面两段代码: 1 void fun(A a); 2 void fun(A const &a); 第一个函数效率低。函数体内产生A类型的临时对象用于复制参数a,临时对象的构造、复制、析构过程都将消耗时间。而第二个函数提高了效率。用“引用传递”不需要产生临时对象,节省了临时对象的构造、复制、析构过程消耗的时间。但光用引用有可能改变a,所以加const。


2016-01-04
原文:(3)const修饰函数的返回值:如给“指针传递”的函数返回值加const,则返回值不能被直接修改,且该返回值只能被赋值给加const修饰的同类型指针。例如。 1 const char *GetChar(void){}; 2 char *ch = GetChar(); // error 3 const char *ch = GetChar(); // correct


2016-01-04
原文:(4)const修饰类的成员函数(函数定义体):任何不会修改数据成员的函数都应用const修饰,这样,当不小心修改了数据成员或调用了非const成员函数时,编译器都会报错。const修饰类的成员函数形式为: 1 int GetCount(void) const;


2016-01-05
原文:在C语言中,关键字static有3个明显的作用: (1)在函数体,一个被声明为静态的变量在这一函数被调用的过程中维持其值不变。 (2)在模块内(但在函数体外),一个被声明为静态的变量可以被模块内所有函数访问,但不能被模块外其他函数访问。它是一个本地的全局变量。 (3)在模块内,一个被声明为静态的函数只可被这一模块内的其他函数调用。那就是这个函数被限制在声明它的模块的本地范围内使用。


2016-01-05
原文:static 全局变量与普通的全局变量有什么区别?static 局部变量和普通的局部变量有什么区别?static函数与普通函数有什么区别?


2016-01-05
原文:全局变量的说明之前再加上static就构成了静态的全局变量。全局变量本身就是静态存储方式,静态全局变量当然也是静态存储方式。这两者在存储方式上并无不同。这两者的区别在于,非静态全局变量的作用域是整个源程序,当一个源程序由多个源文件组成时,非静态的全局变量在各个源文件中都是有效的;而静态全局变量则限制了其作用域,即只在定义该变量的源文件内有效,在同一源程序的其他源文件中不能使用它。由于静态全局变量的作用域局限于一个源文件内,只能为该源文件内的函数公用,因此可以避免在其他源文件中引起错误。


2016-01-05
原文:从以上分析可以看出,把局部变量改变为静态变量后是改变了它的存储方式,即改变了它的生存期;把全局变量改变为静态变量后是改变了它的作用域,限制了它的使用范围。


2016-01-05
原文:static 函数与普通函数的作用域不同。static函数的作用域仅在本文件,只在当前源文件中使用的函数应该说明为内部函数(static),内部函数应该在当前源文件中说明和定义。对于可在当前源文件以外使用的函数,应该在一个头文件中说明,要使用这些函数的源文件要包含这个头文件。 因此,static全局变量与普通的全局变量的区别是,static全局变量只初始化一次,防止在其他文件单元中被引用;static局部变量和普通局部变量的区别是,static局部变量只被初始化一次,下一次依据上一次结果值;static函数与普通函数的区别是,static函数在内存中只有一份,普通函数在每个被调用中维持一份复制品。


2016-01-05
原文:static全局变量与普通全局变量的区别是,static全局变量只初始化一次,防止在其他文件单元中被引用。 static局部变量和普通局部变量的区别是,static局部变量只被初始化一次,下一次依据上一次结果值。 static函数与普通函数的区别是,static函数在内存中只有一份,普通函数在每个被调用中维持一份复制品。


2016-01-06
原文:类中的静态成员或方法不属于类的实例,而属于类本身并在所有类的实例间共享。在调用它们时应该用类名加上操作符“::”来引用。


2016-01-06
原文:对数组变量做 sizeof 运算得到的是数组占用内存的总大小。在这里,str的总大小为strlen("Hello")+1,注意数组中要有一个元素保存字符串结束符


2016-01-06
原文:这是因为当我们调用函数 Func(str)时,由于数组是“传址”的,程序会在栈上分配一个4字节的指针来指向数组,因此结果也是4。


2016-01-06
原文:总之,数组和指针的 sizeof 运算有细微的区别。如果数组变量被传入函数中做 sizeof运算,则和指针的运算没有区别,否则得到整个数组占用内存的总大小。对于指针,无论是何种类型的指针,其大小都是固定的,在32位WinNT平台下都是4。


2016-01-12
原文:字节对齐引起的。对齐的作用和原因:各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。其他平台可能没有这种情况,但是最常见的是,如果不按照适合其平台的要求对数据存放进行对齐,会给存取效率带来损失。例如,有些平台每次读都是从偶地址开始,如果一个int型(假设为32位系统)存放在偶地址开始的地方,那么一个读周期就可以读出;而如果存放在奇地址开始的地方,可能会需要 2 个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该int型数据。显然,在读取效率上下降很多。这也是空间和时间的博弈。


2016-01-12
原文:字节对齐的细节和编译器实现相关,一般而言,需要满足3个准则: 结构体变量的首地址能够被其最宽基本类型成员的大小所整除; 结构体每个成员相对于结构体首地址的偏移量(offset)都是成员大小的整数倍,如有需要,编译器会在成员之间加上填充字节(internal adding); 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要,编译器会在最末一个成员之后加上填充字节(trailing padding)。


2016-01-12
原文:现在以下面的结构体为例。 1 struct S 2 { 3  char c1; 4  int i; 5  char c2; 6 }; c1的偏移量为0,i的偏移量为4,c1与i之间便需要3个填充字节。c2的偏移量为8,加起来就是1+3+4+1,等于9个字节。由于这里最宽的基本类型为int,大小为4个字节,再补3个字节凑成4的倍数。这样一共是12个字节。


2016-01-14
原文:普通函数不占用内存,只要有虚函数,就会占用一个指针大小的内存,原因是系统多用了一个指针维护这个类的虚函数表,并且注意这个虚函数无论含有多少项(类中含有多少个虚函数)都不会再影响类的大小。


2016-01-14
原文:使用sizeof计算虚拟继承的类对象的空间大小 出现频率:★★★★ 1 #include <iostream> 2 using namespace std; 3 4 class A 5 { 6 }; 7 8 class B 9 { 10 }; 11 12 class C:public A, public B 13 { 14 }; 15


2016-01-14
原文:16 class D:virtual public A 17 { 18 }; 19 20 class E:virtual public A, virtual public B 21 { 22 }; 23


2016-01-14
原文:24 class F 25 { 26 public: 27  int a; 28  static int b; 29 } 30 31 int F::b = 10; 32 33 int main() 34 { 35  cout << "sizeof(A) = " << sizeof(A) << endl; 36  cout << "sizeof(B) = " << sizeof(B) << endl; 37  cout << "sizeof(C) = " << sizeof(C) << endl; 38  cout << "sizeof(D) = " << sizeof(D) << endl; 39  cout << "sizeof(E) = " << sizeof(E) << endl; 40  cout << "sizeof(F) = " << sizeof(F) << endl;


2016-01-14
原文:42  return 0; 43 } 【解析】 程序说明如下。 代码第 35 行,由于 A 是空类,编译器会安插一个 char 给空类,用来标记它的每一个对象。因此其大小为1个字节。 代码第36 行,类B 大小和A 相同,都是1 个字节。 代码第37 行,类C 是多重继承自A 和B,其大小仍然为1 个字节。


2016-01-14
原文:代码第38 行,类D 是虚继承自A,编译器为该类安插一个指向父类的指针,指针大小为4。由于此类有了指针,编译器不会安插一个char了。因此其大小是4个字节。 代码第 39 行,类 E 虚继承自 A 并且也虚继承自 B,因此它有指向父类 A 的指针与父类B的指针,加起来大小为8个字节。 代码第40行,类F含有一个静态成员变量,这个静态成员的空间不在类的实例中,而是像全局变量一样在静态存储区中,被每一个类的实例共享,因此其大小是4个字节。 【答案】 sizeof(A) = 1 sizeof(B) = 1 sizeof(C) = 1 sizeof(D) = 4


2016-01-14
原文:sizeof(E) = 8 sizeof(F) = 4


2016-01-14
原文:理解sizeof与strlen的区别 出现频率:★★★ 【解析】 它们的区别如下。 sizeof 是操作符,strlen 是函数。 sizeof 操作符的结果类型是size_t,它在头文件中typedef 为unsignedint 类型,该类型保证能容纳实现所建立的最大对象的字节大小。 sizeof 可以用类型做参数,strlen 只能用char*做参数,且必须是以‘‘\0‘‘结尾的。 数组做sizeof 的参数不退化,传递给strlen 就退化为指针了。 大部分编译程序在编译的时候 sizeof 就被计算过了,这就是 sizeof(x)可以用来定义数组维数的原因。strlen的结果要在运行的时候才能计算出来,它用来计算字符串的长度,不是类型占内存的大小。 sizeof 后如果是类型,必须加括弧;如果是变量名,可以不加括弧。这是因为sizeof是个操作符,而不是个函数。


2016-01-14
原文:在计算字符串数组的长度上有区别。例如, 1 char str[20]="0123456789"; 2 int a=strlen(str); 3 int b=sizeof(str); a计算的是以0x00结束的字符串的长度(不包括0x00结束符),这里结果是10。 b计算的则是分配的数组str[20]所占的内存空间的大小,不受里面存储内容的改变而改变,这里结果是20。 如果要计算指针指向的字符串的长度,则一定要使用strlen。例如。 1  char* ss = "0123456789"; 2  int a = sizeof(ss); 3  int b = strlen(ss); a计算的是ss指针占用的内存空间大小,这里结果是4。 b计算的是ss指向的字符串的长度,这里结果是10。


2016-01-14
原文:(1)函数UpperCase(char str[])的意图是将str 指向的字符串中小写字母转换为大写字母。于是在代码第9行利用sizeof(str)/sizeof(str[0])获得数组中的元素个数以便做循环操作。然而,sizeof(str)得到的并不是数组占用内存的总大小,而是一个字符指针的大小,为4字节。因此这里只能循环4次,在代码第18行main()函数的调用中只能改变对数组的前四个字符进行转换。转换的结果为“ABCDe”。 (2)在代码第 17 行,这里的意图是使用要打印字符串的长度。然而,sizeof(str)/sizeof(str[0])计算的是数组元素的个数,比字符串的长度大1,原因是数组的长度还包括字符串的结束符‘\0‘。 应该用strlen()函数来代替sizeof计算字符串长度。正确的代码如下。


2016-01-14
原文:1 #include <iostream.h> 2 #include <string.h> 3 4 void UpperCase(char str[]) 5 { 6  int test = sizeof(str); 7  int test2 = sizeof(str[0]); 8 9  for(size_t i=0; i<strlen(str); ++i) //计算字符串的长度 10   if(‘a‘<=str[i] && str[i]<=‘z‘) 11     str[i] -= (‘a‘-‘A‘); 12 } 13 14 int main() 15 { 16  char str[] = "aBcDe"; 17 18  cout << "The length of str is " << strlen(str) << endl; //计算字符串的长度 19  UpperCase( str ); 20  cout << str << endl; 21  return 0;


2016-01-14
原文:联合体的大小取决于它所有的成员中占用空间最大的一个成员的大小。并且对于复合数据类型,如 union、struct、class 的对齐方式为成员中最大的成员对齐方式。 对于u来说,大小就是最大的double类型成员a了,即sizeof(u)=sizeof(double)=8。 对于 u2 来说,最大的空间是 char[13]类型的数组。这里要注意,由于它的另一个成员int b 的存在,u2 的对齐方式变成4,也就是说,u2 的大小必须在4的对齐上,所以占用的空间变为最接近13的对齐,即16。 对于u3 来说,最大的空间是char[13]类型的数组,即sizeof(u3)=13。


2016-01-14
原文:union u 4 { 5  double a; 6  int b; 7 }; 8 9 union u2 10 { 11  char a[13]; 12  int b; 13 }; 14 15 union u3 16 { 17  char a[13]; 18  char b; 19 };


2016-01-14
原文:编译器会尽量把数据放在它的对齐上以提高内存的命中率。对齐是可以更改的,使用#pragma pack(x)可以改变编译器的对齐方式。C++固有类型的对齐取编译器对齐方式与自身大小中较小的一个。例如,指定编译器按 2 对齐,int类型的大小是4,则int的对齐为2和4中较小的2。在默认的对齐方式下,因为几乎所有的数据类型都不大于默认的对齐方式8(除了long double),所以所有的固有类型的对齐方式可以认为就是类型自身的大小


2016-01-14
原文:#pragma pack(2) 4 5 union u 6 { 7  char buf[9]; 8  int a; 9 };


2016-01-14
原文:C++固有类型的对齐取编译器对齐方式与自身大小中较小的一个。 上面的程序中,由于使用手动更改对齐方式为2,所以int的对齐也变成了2(int自身对齐为4),u的对齐取成员中最大的对齐,也是2,所以此时sizeof(u)=10。 如果注释上面的第3行,int的对齐使用4,u的对齐取成员中最大的对齐,也是4,所以此时sizeof(u)=12。


2016-01-15
原文:引入内联函数的主要目的是,用它替代 C 语言中表达式形式的宏定义来解决程序中函数调用的效率问题。在C语言里可以使用如下的宏定义。 1 #define ExpressionName(Var1,Var2) (Var1+Var2)*(Var1-Var2) 这种宏定义在形式及使用上像一个函数,但它使用预处理器实现,没有了参数压栈、代码生成等一系列的操作,因此效率很高。这种宏定义在形式上类似于一个函数,但在使用它时,仅仅只是做预处理器符号表中的简单替换,因此它不能进行参数有效性的检测,也就不能享受 C++编译器严格类型检查的好处。另外,它的返回值也不能被强制转换为可转换的合适类型,这样,它的使用就存在着一系列的隐患和局限性。 另外,在 C++中引入了类及类的访问控制,这样,如果一个操作或者说一个表达式涉及类的保护成员或私有成员,你就不可能使用这种宏定义来实现(因为无法将 this 指针放在合适的位置)。


2016-01-15
原文:inline推出的目的,也正是为了取代这种表达式形式的宏定义。它消除了它的缺点,同时又很好地继承了它的优点。


2016-01-15
原文:理解内联函数相比于宏定义的优越之处 出现频率:★★★ 【解析】 有如下几种原因: inline定义的类的内联函数,函数的代码被放入符号表中,在使用时直接进行替换(像宏一样展开),没有了调用的开销,效率也很高。 类的内联函数也是一个真正的函数。编译器在调用一个内联函数时,首先会检查它的参数的类型,保证调用正确;然后进行一系列的相关检查,就像对待任何一个真正的函数一样。这样就消除了它的隐患和局限性。 inline 可以作为某个类的成员函数,当然就可以在其中使用所在类的保护成员及私有成员。


2016-01-15
原文:理解内联函数的作用场合 出现频率:★★★ 【解析】 首先使用inline函数可以完全取代表达式形式的宏定义。 内联函数在 C++类中应用最广的,应该是用来定义存取函数。我们定义的类中一般会把数据成员定义成私有的或者保护的,这样,外界就不能直接读写我们类成员的数据了。对于私有或者保护成员的读写就必须使用成员接口函数来进行。如果我们把这些读写成员函数定义成内联函数的话,将会获得比较好的效率。例如下面的代码:


2016-01-15
原文:1 Class A 2 { 3 Private: 4   int nTest; 5 Public: 6   int readTest() 7   { 8    return nTest; 9   } 10  void setTest(int i); 11 }; 12 13 inline void A::setTest(int i) 14 { 15  nTest = i; 16 }; 类 A 的成员函数 readTest()和 setTest()都是 inline 函数。readTest()函数的定义体被放在类声明之中,因而readTest()自动转换成inline函数;setTest()函数的定义体在类声明之外,因此要加上inline关键字。


2016-01-19
原文:内联是以代码膨胀(复制)为代价的,仅仅省去了函数调用的开销,从而提高函数的执行效率。如果执行函数体内代码的时间相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。以下情况不宜使用内联。 如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。 如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。 另外,类的构造函数和析构函数容易让人误解成使用内联更有效。要当心构造函数和析构函数可能会隐藏一些行为,如“偷偷地”执行了基类或成员对象的构造函数和析构函数。


2016-01-19
原文:所以不要随便地将构造函数和析构函数的定义体放在类声明中。一个好的编译器将会根据函数的定义体,自动地取消不值得的内联(这说明了 inline 不应该出现在函数的声明中)。


2016-01-19
原文:理解内联函数与宏定义的区别 出现频率:★★★★ 【解析】 二者区别如下: 内联函数在编译时展开,宏在预编译时展开。 在编译的时候,内联函数可以直接被镶嵌到目标代码中,而宏只是一个简单的文本替换。 内联函数可以完成诸如类型检测、语句是否正确等编译功能,宏就不具有这样的功能。 宏不是函数,inline 函数是函数。 宏在定义时要小心处理宏参数(一般情况是把参数用括号括起来),否则容易出现二义性。而内联函数定义时不会出现二义性。


2016-01-19
原文:7  int a = 10; 8  int b = 20; 9  int &rn = a; 10  int equal; 11 12  rn=b; 13  cout << "a = " << a << endl; 14  cout << "b = " << b << endl; 15 16  rn = 100; 17 18  cout << "a = " << a << endl; 19  cout << "b = " << b << endl; 20 21  equal = (&a == &rn)? 1: 0; 22 23  cout << "equal = " << equal << endl;


2016-01-19
原文:代码第7 行和第8 行,整型变量a 和整型变量b 分别被初始化为10 和20。 代码第9 行,声明rn 为变量a 的一个引用。 代码第12 行,将rn 的值赋为b 的值。此时rn 其实就是a 的一个别名,对rn 的赋值其实就是对a的赋值。因此执行完赋值后,a的值就是b的值,即都是20。 代码第16 行,将rn 的值赋为100,于是a 的值变成了100。 代码第21行,将a的地址与rn的地址进行比较,如果相等,变量equal的值为1,否则为0。将rn声明为a的引用,不需要为rn另外开辟内存单元。rn和a占内存中的同一个存储单元,它们具有同一地址,所以equal为1。


2016-02-01
原文:引用只能在声明的时候被赋值,以后都不能再把该引用名作为其他变量名的别名。


2016-02-01
原文:把const放在引用之前表示声明的是一个常量引用。不能使用常量引用修改引用的变量的值。


2016-02-01
原文:对于常量类型的变量,其引用也必须是常量类型的;对于非常量类型的变量,其引用可以是非常量的,也可以是常量的。但是要注意,无论什么情况,都不能使用常量引用修改其引用的变量的值。


2016-02-02
原文:引用和指针的区别


2016-02-02
原文:区别如下: (1)初始化要求不同。引用在创建的同时必须初始化,即引用到一个有效的对象;而指针在定义的时候不必初始化,可以在定义后面的任何地方重新赋值。 (2)可修改性不同。引用一旦被初始化为指向一个对象,它就不能被改变为另一个对象的引用;而指针在任何时候都可以改变为指向另一个对象。给引用赋值并不是改变它和原始对象的绑定关系。 (3)不存在 NULL 引用,引用不能使用指向空值的引用,它必须总是指向某个对象;而指针则可以是NULL,不需要总是指向某些对象,可以把指针指向任意的对象,所以指针更加灵活,也容易出错。 (4)测试需要的区别。由于引用不会指向空值,这意味着使用引用之前不需要测试它的合法性;而指针则需要经常进行测试。因此使用引用的代码效率比使用指针的要高。


2016-02-02
原文:(5)应用的区别。如果是指一旦指向一个对象后就不会改变指向,那么应该使用引用。如果有存在指向 NULL(不指向任何对象)或在不同的时刻指向不同的对象这些可能性,应该使用指针。 实际上,在语言层面,引用的用法和对象一样;在二进制层面,引用一般都是通过指针来实现的,只不过编译器帮我们完成了转换。总体来说,引用既具有指针的效率,又具有变量使用的方便性和直观性。


2016-02-03
原文:由于不存在空引用,并且引用一旦被初始化为指向一个对象,它就不能被改变为另一个对象的引用,因此引用很安全。 对于指针来说,它可以随时指向别的对象,并且可以不被初始化,或为NULL,所以不安全。const 指针仍然存在空指针,并且有可能产生野指针。


2016-02-19
原文:扩展知识:解读复杂指针声明 使用右左法则:首先从最里面的圆括号看起,然后往右看,再往左看。每当遇到圆括号时,就应该掉转阅读方向。一旦解析完圆括号里面所有的东西,就跳出圆括号。重复这个过程,直到整个声明解析完毕。 这里对这个法则进行一个小小的修正,应该是从未定义的标识符开始阅读,而不是从括号读起。这是因为一个声明里面未定义的标识符只会有一个。


2016-02-19
原文:现在通过几个例子来讨论如何运用右左法则解读复杂指针声明。


2016-02-19
原文:1 int (*func)(int *p); 首先找到那个未定义的标识符,就是func,它的外面有一对圆括号,而且左边是一个*号,这说明func是一个指针;然后跳出这个圆括号,先看右边,也是一个圆括号,这说明(*func)是一个函数,而 func 是一个指向这类函数的指针,就是一个函数指针,这类函数具有int*类型的形参,返回值类型是int。


2016-02-19
原文:2 int (*func)(int *p, int (*f)(int*)); func被一对括号包含,且左边有一个*号,说明func是一个指针,跳出括号,右边也有个括号,那么func 是一个指向函数的指针,这类函数具有int *和int (*)(int*)这样的形参,返回值为int类型。再来看一看func 的形参int (*f)(int*),类似前面的解释,f 也是一个函数指针,指向的函数具有int*类型的形参,返回值为int。


2016-02-19
原文:3 int (*func[5])(int *p); func右边是一个[]运算符,说明func是一个具有5个元素的数组;func的左边有一个*,说明 func 的元素是指针。要注意这里的*不是修饰 func 的,而是修饰 func[5]的,原因是[]运算符优先级比*高,func先跟[]结合,因此*修饰的是func[5]。跳出这个括号,看右边,也是一对圆括号,说明 func 数组的元素是函数类型的指针,它所指向的函数具有 int*类型的形参,返回值类型为int。


2016-02-23
原文:4 int (*(*func)[5])(int *p); func被一个圆括号包含,左边又有一个*,那么func是一个指针;跳出括号,右边是一个[]运算符号,说明func是一个指向数组的指针。现在往左看,左边有一个*号,说明这个数组的元素是指针;再跳出括号,右边又有一个括号,说明这个数组的元素是指向函数的指针。总结一下,就是:func 是一个指向数组的指针,这个数组的元素是函数指针,这些指针指向具有int*类型的形参,返回值为int类型的函数。


2016-02-23
原文:5 int (*(*func)(int *p))[5]; func是一个函数指针,这类函数具有int*类型的形参,返回值是指向数组的指针,所指向的数组的元素是具有5个int元素的数组。


2016-02-23
原文: #include <stdio.h> 2 3 int main(void) 4 { 5  int a[5]={1,2,3,4,5}; 6  int *ptr=(int *)(&a+1); 7 8  printf("%d\n", *(a+1)); 9  printf("%d\n", *(ptr-1)); 10 11  return 0; 12 } 【解析】 这里主要是考查关于指针加减操作的理解。 对指针进行加1操作,得到的是下一个元素的地址,而不是原有地址值直接加1。所以,一个类型为t的指针的移动,以sizeof(t)为移动单位。


2016-02-23
原文:代码第5 行,声明一个一维数组a,并且a 有5 个元素。 代码第6行,ptr是一个int型的指针&a + 1,即取a的地址,该地址的值加sizeof(a)的值,即&a + 5*sizeof(int),也就是a[5]的地址,显然,当前指针已经越过了数组的界限。(int *)(&a+1)则是把上一步计算出来的地址,强制转换为int *类型,赋值给ptr。 代码第 8 行,a 与&a 的地址是一样的,但意思不一样。a 是数组首地址,也就是a[0]的地址;&a是对象(数组)首地址,a+1是数组下一元素的地址,即a[1];而&a+1是下一个对象的地址,即a[5]。因此这里输出为2。 代码第9 行,因为ptr 指向a[5],并且ptr 是int*类型,所以*(ptr-1)指向a[4],输出5。


2016-02-23
原文: char str1[]  = "abc"; 7  char str2[]  = "abc"; 8  const char str3[] = "abc"; 9  const char str4[] = "abc"; 10  const char* str5 = "abc"; 11  const char* str6 = "abc"; 12  char* str7 = "abc"; 13  char* str8 = "abc";


2016-02-23
原文:数组str1、str2、str3和str4都是在栈中分配的,内存中的内容都为"abc"加一个‘\0‘,但是它们的位置是不同的。因此代码第15行和第16行的输出都是0。 指针str5、str6、str7和str8也是在栈中分配的,它们都指向"abc"字符串,注意"abc"存放在数据区,所以str5、str6、str7 和str8 其实指向同一块数据区的内存。因此第17、18和19行的输出是1。


2016-02-24
原文:这里有个小规则,像这样连着的两个词,前面的一个通常是修饰部分,中心词是后面一个词。 常量指针,表述为“是常量的指针”,它首先应该是一个指针。 指针常量,表述为“是指针的常量”,它首先应该是一个常量。 接下来进行详细分析。 常量指针,它是一个指向常量的指针。设置常量指针指向一个常量,为的就是防止写程序过程中对指针误操作出现了修改常量这样的错误,编译系统就会提示我们出错信息。因此,常量指针就是指向常量的指针,指针所指向的地址的内容是不可修改的。


2016-02-24
原文:指针常量,它首先是一个常量,然后才是一个指针。指针常量就是不能修改这个指针所指向的地址,一开始初始化指向哪儿,它就只能指向哪儿了,不能指向其他的地方了,就像一个数组的数组名一样,是一个固定的指针,不能对它移动操作。如果使用p++,系统就会提示出错。但是注意,这个指向的地方里的内容是可以替换的,这和上面说的常量指针是完全不同的概念。总之,指针常量就是指针的常量,它是不可改变地址的指针,但是可以对它所指向的内容进行修改。 【答案】 常量指针就是指向常量的指针,它所指向的地址的内容是不可修改的。 指针常量就是指针的常量,它是不可改变地址的指针,但是可以对它所指向的内容进行修改。


2016-02-25
原文:下述4个指针有什么区别? 1 char * const p1; 2 char const * p2; 3 const char *p3; 4 const char *constp4; 【解析】 如果const位于*号的左侧,则const就是用来修饰指针所指向的变量,即指针指向为常量;如果const位于*号的右侧,const就是修饰指针本身,即指针本身是常量。因此,p1指针本身是常量,但它指向的内容可以被修改。p2和p3的情况相同,都是指针所指向的内容为常量。p4则表示指针本身是常量,并且它指向的内容也不可被修改。


2016-02-25
原文:下列关于this指针的叙述中,正确的是(  )。 A.任何与类相关的函数都有this指针 B.类的成员函数都有this指针 C.类的友元函数都有this指针 D.类的非静态成员函数才有this指针 【解析】 A 错误。类的非静态成员函数是属于类的对象,含有this指针。而类的static 函数属于类本身,不含this指针。 B 错误。类的非静态成员函数是属于类的对象,含有this指针。而类的static 函数属于类本身,不含this指针。 C 错误。友元函数是非成员函数,所以它无法通过this 指针获得一份拷贝。 D 正确。


2016-02-25
原文:下面的代码输出结果是什么?如果取消第14行的注释,输出又是什么? 1 #include <iostream> 2 using namespace std; 3 4 class MyClass 5 { 6 public: 7  int data; 8  MyClass(int data) 9  { 10   this->data = data; 11  } 12  void print() 13  { 14   //cout << data << endl; 15   cout << "hello!" << endl; 16  } 17 };


2016-02-25
原文:20 int main() 21 { 22  MyClass * pMyClass; 23  pMyClass = new MyClass(1); 24  pMyClass->print(); 25  pMyClass[0].print(); 26  pMyClass[1].print(); 27  pMyClass[10000000].print();


2016-02-25
原文:这里需要明白类函数是如何被编译以及如何被执行的。 对于类成员函数而言,并不是一个对象对应一个单独的成员函数体,而是此类的所有对象共用这个成员函数体。当程序被编译之后,此成员函数地址即已确定。我们常说,调用类成员函数时,会将当前对象的 this 指针传给成员函数。没错,一个类的成员函数体只有一份,而成员函数之所以能把属于此类的各个对象的数据区别开,就在于每次执行类成员函数时,都会把当前对象的 this 指针(对象首地址)传入成员函数,函数体内所有对类数据成员的访问,都会被转化为this->数据成员的方式。 如果print函数里没有访问对象的任何数据成员,那么此时传进来的对象this指针实际上是没有任何用处的。这样的函数,其特征与全局函数并没有太大区别。但如果取消第 14行的注释,由于print函数要访问类的数据成员data,而类的数据成员是伴随着对象声明而产生的。但是,我们只 new 了一个 MyClass,显然,下标"1"和下标"10000000"的 MyClass对象根本不存在,那么对它们的数据成员访问也显然是非法的。


2016-02-25
原文:【答案】 注释代码第14行时的输出: 1 hello! 2 hello! 3 hello! 4 hello! 取消代码第 14行注释后的输出: 1 1 2 hello! 3 1 4 hello! 5 -33686019 6 hello! 7 段错误


2016-03-13
原文:指针数组表示它是一个数组,并且数组中的每一个元素是指针。 数组指针表示它是一个指针,并且指向了一个数组。


2016-03-14
原文:指针函数是指带指针的函数,即本质是一个函数,并且返回类型是某一类型的指针。其定义如下: 1 返回类型标识符 *返回名称(形式参数表){函数体 } 事实上,每一个函数,即使它不带有返回某种类型的指针,它本身都有一个入口地址,该地址相当于一个指针。比如函数返回一个整型值,实际上也相当于返回一个指针变量的值,不过这时的变量是函数本身而已,而整个函数相当于一个“变量”。 函数指针是指向函数的指针变量,因而它本身首先应是指针变量,只不过该指针变量指向函数。有了指向函数的指针变量后,可用该指针变量调用函数,就如同用指针变量可引用其他类型的变量一样。


2016-03-14
原文:指针函数是返回指针类型的函数。 函数指针是指向函数地址的指针。


2016-03-14
原文:定义下面的几种类型变量: a.含有10个元素的指针数组 b.数组指针 c.函数指针 d.指向函数的指针数组 【答案】 a.int *a[10]; b.int *a = new int[10]; c.void (*fn)(int, int); d.int (*fnArray[10])(int, int);


2016-03-14
原文:void (*f)(int, int),f 是指向void max(int x, int y)类型的函数指针。 int *fn(),fn 是返回int 指针类型的函数。 const int *p,p 是一个指向const 的指针,指向一个常量。 int* const q,q 是一个const 指针。 const int* const ptr,ptr 是指向const 的const 指针。


2016-03-14
原文:1 #include <stdio.h> 2 int add1(int a1,int b1); 3 int add2(int a2,int b2); 4 int main(int argc,char* argv[]) 5 { 6  int numa1=1,numb1=2; 7  int numa2=2,numb2=3; 8  int (*op[2])(int a,int b); 9  op[0]=add1; 10  op[1]=add2; 11  printf("%d %d\n",op[0](numa1,numb1),op[1](numa2,numb2)); 12  getchar(); 13 14  return 0; 15 } 16


2016-03-14
原文:17 int add1(int a1,int b1) 18 { 19  return a1+b1; 20 } 21 22 int add2(int a2,int b2) 23 { 24  return a2+b2; 25 }


2016-03-14
原文:【解析】 在代码第8行,定义了一个函数指针数组op,它含有两个指针元素。在第9行和第10行把这两个元素分别指向了add1和add2两个函数地址。最后在第11行打印出使用函数指针调用add1和add2这两个函数返回的结果。 【答案】 1 3 5


2016-03-14
原文:下面的定义有什么作用? 1 typedef int (*pfun)(int x,int y); 【解析】 这里的pfun是一个使用typedef自定义的数据类型。它表示一个函数指针,其参数有两个,都是int类型,返回值也是int类型。可以按如下步骤使用: 1 typedef int (*pfun)(int x,int y); 2 int fun(int x, int y); 3 pfun p = fun; 4 int ret = p(2, 3); 简单说明: 第1 行定义了pfun 类型,表示一个函数指针类型。 第2 行定义了一个函数。 第3 行定义了一个pfun 类型的函数指针p,并赋给它fun 的地址。 第4 行调用p(2, 3),实现fun(2, 3)的调用功能。


2016-03-14
原文:malloc与free是C++/C的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。 对于非内部数据类型的对象而言,光用 malloc/free 无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于 malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。


2016-03-14
原文:因此,C++需要一个能完成动态内存分配和初始化工作的运算符new,以及一个能完成清理与释放内存工作的运算符delete。注意:new/delete不是库函数。请看下面的例子。 1 #include <iostream> 2 using namespace std; 3 4 class Obj 5 { 6 public: 7  Obj(void) 8  { 9   cout << "Initialization" << endl; 10  } 11 ~Obj(void) 12  { 13   cout << "Destroy" << endl; 14  } 15 }; 16


2016-03-14
原文:17 void UseMallocFree(void) 18 { 19  cout << "in UseMallocFree()..." << endl; 20  Obj *a = (Obj *)malloc(sizeof(Obj)); 21  free(a); 22 } 23


2016-03-14
原文:void UseNewDelete(void) 25 { 26  cout << "in UseNewDelete()..." << endl; 27  Obj *a = new Obj; 28  delete a; 29 } 30 31 int main() 32 { 33  UseMallocFree(); 34  UseNewDelete(); 35 36  return 0; 37 }


2016-03-14
原文:在这个示例中,类 Obj 只有构造函数和析构函数的定义,这两个成员函数分别打印一句话。函数UseMallocFree()中调用malloc/free 申请和释放堆内存;函数UseNewDelete ()中调用new/delete申请和释放堆内存。可以看到函数UseMallocFree()执行时,类Obj的构造函数和析构函数都不会被调用;而函数UseNewDelete ()执行时,类Obj的构造函数和析构函数都会被调用。执行结果如下: in UseMallocFree()... in UseNewDelete()... Initialization Destroy


2016-03-14
原文:对于非内部数据类型的对象而言,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能把执行构造函数和析构函数的任务强加于malloc/free,因此只有使用new/delete运算符。


2016-03-15
原文: #include <stdio.h> 2 #include <malloc.h> 3 4 struct Tag_Node 5 { 6  struct Tag_Node* left; 7  struct Tag_Node* right; 8  int value; 9 }; 10 typedef struct Tag_Node TNode; 11 12 TNode* root = NULL; 13


2016-03-15
原文:14 void append(int N); 15 void print(); 16 17 int main() 18 { 19  append(63); 20  append(45); 21  append(32); 22  append(77); 23  append(96); 24  append(21); 25  append(17); 26  printf("head: %d\n", root->value); 27  print();     //打印链表所有元素 28 } 29 30 void append(int N) 31 { 32  TNode* NewNode = (TNode *)malloc(sizeof(TNode)); 33  NewNode->value = N; 34  NewNode->left = NULL;  //初始化left 35  NewNode->right = NULL;  //初始化right 36


2016-03-15
原文:37  if(root == NULL) 38  { 39   root = NewNode; 40   return; 41  } 42  else 43  { 44   TNode* temp; 45   temp=root; 46 48     (N < temp->value && temp->right != NULL)) 47   while((N >= temp->value && temp->left != NULL) || 49   { 50     while(N >= temp->value && temp->left != NULL) 51      temp = temp->left; 52     while(N < temp->value && temp->right != NULL) 53      temp = temp->right; 54   } 55   if(N >= temp->value)


2016-03-15
原文:56   { 57     temp->left = NewNode; 58     NewNode->right = temp; //形成双向链表 59   } 60   else 61   { 62     temp->right = NewNode; 63     NewNode->left = temp;  //形成双向链表 64   } 65   return; 66  } 67 } 68 69 void print() 70 { 71  TNode* leftside = NULL; 72 73  if (root == NULL) 74  { 75   printf("There is not any element1"); 76   return; 77  } 78


2016-03-15
原文:79  leftside = root->left; 80 81  while(1) 82  { 83   if (leftside->left == NULL) 84   { 85    break; 86   } 87   leftside = leftside->left; 88  } 89


2016-03-15
原文:90  while(leftside != NULL) 91  { 92   printf("%d ", leftside->value); 93   leftside = leftside->right; 94  } 95 }


2016-03-16
原文:C语言的标准内存分配函数:malloc、calloc、realloc、free等。 malloc与calloc的区别为1块与n块的区别。 malloc 的调用形式为(类型*)malloc(size):在内存的动态存储区中分配一块长度为“size”字节的连续区域,返回该区域的首地址,此时内存中的值没有初始化,是个随机数。 calloc 的调用形式为(类型*)calloc(n,size):在内存的动态存储区中分配n 块长度为“size”字节的连续区域,返回首地址,此时内存中的值都被初始化为0。 realloc 的调用形式为(类型*)realloc(*ptr,size):将 ptr 内存大小增大到 size,新增加的内存块没有初始化。 free 的调用形式为free(void*ptr):释放ptr 所指向的一块内存空间。 C++中,new/delete函数可以调用类的构造函数和析构函数。


2016-03-16
原文:1 #include <iostream> 2 using namespace std; 3 4 void GetMemory(char *p, int num) 5 { 6  p = (char *)malloc(sizeof(char) *num); 7 }; 8 9 void GetMemory2(char **p, int num) 10 { 11  *p = (char *)malloc(sizeof(char) *num); 12 }; 13 14 void GetMemory3(char* &p, int num) 15 { 16  p = (char *)malloc(sizeof(char) *num);


2016-03-16
原文:17 }; 18 19 char *GetMemory4(int num) 20 { 21  char *p = (char *)malloc(sizeof(char) *num); 22 23  return p; 24 } 25 26 int main(void) 27 { 28  char *str1 = NULL; 29  char *str2 = NULL; 30  char *str3 = NULL; 31  char *str4 = NULL; 32 33  //GetMemory(str1, 20); 34  GetMemory2(&str2, 20); 35  GetMemory3(str3, 20); 36  str4 = GetMemory4(20); 37


2016-03-16
原文:38  strcpy(str2, "GetMemory 2"); 39  strcpy(str3, "GetMemory 3"); 40  strcpy(str4, "GetMemory 4"); 41 42  cout << "str1 == NULL? " << (str1 == NULL? "yes":"no") << endl; 43  cout << "str2:" << str2 << endl; 44  cout << "str3:" << str3 << endl; 45  cout << "str4:" << str4 << endl; 46 47  free(str2); 48  free(str3); 49  free(str4); 50  str2 = NULL; 51  str3 = NULL; 52  str4 = NULL; 53 54  return 0; 55 } 在上面的代码中,GetMemory2()函数采用二维指针作为参数传递;GetMemory3()函数采用指针的引用作为参数传递;GetMemory4()函数采用返回堆内存指针的方式。可以看到这3个函数


2016-03-16
原文:都能起到相同的作用。 另外注意第47~52行,这里在主函数推出之前把指针str2、str3和str4指向的堆内存释放并把指针赋为NULL。每当决定不再使用堆内存时,应该把堆内存释放,并把指针赋为NULL,这样能避免内存泄漏以及产生野指针,是良好的编程习惯。 程序运行结果如下所示。 1 str1 == NULL? Yes 2 str2:GetMemory 2 3 str3:GetMemory 3 4 str3:GetMemory 4


2016-03-16
原文:(1)从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在,例如全局变量。 (2)在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。处理器的指令集中有关于栈内存的分配运算,因此效率很高,但是分配的内存容量有限。 (3)从堆上分配,亦称动态内存分配。程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用 free 或 delete 释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多。


2016-03-16
原文:句柄在 Windows 编程中是一个很重要的概念,在许多地方都扮演着重要的角色。在Windows环境中,句柄是用来标识项目的,这些项目包括: 模块(module)。 任务(task)。 实例(instance)。 文件(file)。 内存块(block of memory)。 菜单(menu)。 控制(control)。 字体(font)。 资源(resource),包括图标(icon)、光标(cursor)、字符串(string)等。 GDI对象(GDI object),包括位图(bitmap),画刷(brush)、元文件(metafile),调色板(palette)、画笔(pen)、区域(region),以及设备描述表(device context)。


2016-03-16
原文:Windows 是一个以虚拟内存为基础的操作系统。在这种系统环境下,Windows 内存管理器经常在内存中来回移动对象,以此来满足各种应用程序的内存需要。对象被移动意味着它的地址变化了。由于地址总是如此变化,所以Windows操作系统为各应用程序腾出一些内存储地址,用来专门登记各应用对象在内存中的地址变化,而这地址(存储单元的位置)本身是不变的。Windows 内存管理器在移动对象在内存中的位置后,把对象新的地址告知这个句柄地址来保存。这样我们只需记住这个句柄地址就可以间接地知道对象具体在内存中的哪个位置。这个地址是在对象装载(Load)时由系统分配给的,当系统卸载时(Unload)又释放给系统。 因此,Windows 程序中并不是用物理地址来标识一个内存块、文件、任务或动态装入模块的,相反,WINDOWS API 给这些项目分配确定的句柄,并将句柄返回给应用程序,然后通过句柄来进行操作。


2016-03-16
原文:在Windows编程中会用到大量的句柄,比如HINSTANCE(实例句柄)、HBITMAP(位图句柄)、HDC(设备描述表句柄)、HICON(图标句柄)等。这当中还有一个通用的句柄,就是HANDLE,比如下面的语句: 1 HINSTANCE hInstance; 2 HANDLE hInstance; 句柄地址(稳定)→记载着对象在内存中的地址→对象在内存中的地址(不稳定)→实际对象。但是,必须注意的是,程序每次重新启动,系统不能保证分配给这个程序的句柄还是原来的那个句柄,而且绝大多数情况的确是不一样的。


2016-03-16
原文:指针对应着一个数据在内存中的地址,得到了指针就可以自由地修改该数据。Windows 并不希望一般程序修改其内部数据结构,因为这样太不安全。所以 Windows给每个使用GlobalAlloc等函数声明的内存区域指定一个句柄,句柄是一种指向指针的指针。


2016-03-16
原文:句柄和指针都是地址,不同之处在于: (1)句柄所指的可以是一个很复杂的结构,并且很有可能是与系统有关的。比如说线程的句柄,它指向的就是一个类或者结构,它和系统有很密切的关系。当一个线程由于不可预料的原因而终止时,系统就可以返回它所占用的资料,如 CPU、内存等。反过来想可以知道,这个句柄中的某一些项是与系统进行交互的。由于Windows系统是一个多任务的系统,它随时都可能要分配内存、回收内存、重组内存。 (2)指针也可以指向一个复杂的结构,但是通常是由用户定义的,所以必需的工作都要用户完成,特别是在删除的时候。


2016-03-16
原文:在C/C++中没有专门的字符串变量,通常用一个字符数组来存放一个字符串。字符串是以‘\0‘作为串的结束符。C/C++提供了丰富的字符串处理函数,下面列出了几个最常用的函数: 字符串输出函数puts; 字符串输入函数gets; 字符串连接函数strcat; 字符串复制函数strcpy; 测字符串长度函数strlen。 字符串是笔试以及面试的热门考点,通过字符串测试可以考查程序员的编程规范以及编程习惯。其中也包括了许多知识点,例如内存越界、指针与数组操作等等。许多公司在面试时会要求应试者写一段 strcpy 复制字符串或字符串子串操作的程序。本章列举了一些与字符串相关的面试题及其解析,有些题要求较高的编程技巧。


2016-03-16
原文:C语言提供了几个标准库函数,可以将任意类型(整型、长整型、浮点型等)的数字转换为字符串。下面列举了各函数的方法及其说明。 itoa():将整型值转换为字符串。 ltoa():将长整型值转换为字符串。 ultoa():将无符号长整型值转换为字符串。 gcvt():将浮点型数转换为字符串,取四舍五入。 ecvt():将双精度浮点型值转换为字符串,转换结果中不包含十进制小数点。 fcvt():以指定位数为转换精度,其余同ecvt()。 还可以使用sprintf系列函数把数字转换成字符串,这种方式的速度比itoa()系列函数的速度慢。


2016-03-16
原文:如果不使用 atoi 或 sprintf 等库函数,我们可以通过把整数的各位上的数字加‘0‘转换成char 类型并存到字符数组中。但要注意,需要采用字符串逆序的方法。


2016-03-16
原文:atof():将字符串转换为双精度浮点型值。 atoi():将字符串转换为整型值。 atol():将字符串转换为长整型值。 strtod():将字符串转换为双精度浮点型值,并报告不能被转换的所有剩余数字。


2016-03-16
原文:strtol():将字符串转换为长整型值,并报告不能被转换的所有剩余数字。 strtoul():将字符串转换为无符号长整型值,并报告不能被转换的所有剩余数字。


2016-03-16
原文:已知strcpy函数的原型是: char * strcpy(char * strDest,const char * strSrc); (1)不调用库函数,实现strcpy函数。 (2)解释为什么要返回char *。 【解析】 代码如下。 1 #include <stdio.h> 2 3 char * strcpy(char * strDest, const char * strSrc)


2016-03-16
原文://实现strSrc到strDest的复制 4 { 5   if ((strDest == NULL) || (strSrc == NULL)) //判断参数strDest和strSrc的有效性 6   { 7    return NULL; 8   } 9   char *strDestCopy = strDest;    //保存目标字符串的首地址 10  while ((*strDest++ = *strSrc++)!=‘\0‘); //把strSrc字符串的内容复制到strDest下 11 12  return strDestCopy; 13 } 14


2016-03-16
原文:15 int getStrLen(const char *strSrc)    //实现获取strSrc字符串的长度 16 { 17  int len = 0; //保存长度 18  while(*strSrc++ != ‘\0‘)    //循环直到遇见结束符‘\0‘为止 19  { 20    len++; 21  } 22 23  return len; 24 }; 25 26 int main()


2016-03-16
原文:27 { 28  char strSrc[] = "Hello World!";  //要被复制的源字符串 29  char strDest[20];      //要复制到的目的字符数组 30  int len = 0;       //保存目的字符数组中字符串的长度 31 32  len = getStrLen(strcpy(strDest, strSrc)); //链式表达式,先复制后计算长度 33  printf("strDest: %s\n", strDest); 34  printf("Length of strDest: %d\n", len); 35 36  return 0; 37 }


2016-03-16
原文:实现memcpy函数


2016-03-16
原文:程序代码如下所示。 1 #include <stdio.h> 2 #include <assert.h> 3 4 void *memcpy2(void *memTo, const void *memFrom, size_t size) 5 { 6   assert((memTo != NULL) && (memFrom !=


2016-03-16
原文:NULL)); //memTo和memFrom必须有效 7   char *tempFrom = (char *)memFrom;   //保存memFrom首地址 8   char *tempTo = (char *)memTo;    //保存memTo首地址 9 10  while(size -- > 0)   //循环size次,复制memFrom的值到memTo中 11     *tempTo++ = *tempFrom++ ; 12 13  return memTo; 14 } 15 16 int main() 17 {


2016-03-16
原文:18  char strSrc[] = "Hello World!"; //将被复制的字符数组 19  char strDest[20];     //目的字符数组 20 21  memcpy2(strDest, strSrc, 4);  //复制strSrc的前4个字符到strDest中 22  strDest[4] = ‘\0‘;     //把strDest的第5个元素赋为结束符‘\0‘ 23  printf("strDest: %s\n", strDest); 24 25  return 0; 26 }


2016-03-16
原文:strcpy与memcpy的区别


2016-03-16
原文:字符串复制与内存复制之间的区别


2016-03-16
原文:主要有下面几方面的区别。 复制的内容不同。strcpy只能复制字符串,而memcpy可以复制任意内容,例如字符数组、整型、结构体、类等。 复制的方法不同。strcpy 不需要指定长度,它是遇到字符串结束符‘\0‘而结束的。memcpy则是根据其第三个参数决定复制的长度。 用途不同。通常在复制字符串时用strcpy;而若需要复制其他类型数据,则一般用memcpy。


2016-03-18
原文:strcpy库函数的实现细节


2016-03-18
原文:这个题目非常简单。我们知道字符串是以‘\0‘作为结束符的,所以只需要做一次遍历就可以了。但是需要注意的是,要尽量把程序写得简单且效率高。看下面的示例代码: 1 #include <stdio.h> 2 #include <assert.h> 3 4 int strlen1(const char* src) 5 { 6   assert( NULL != src);  //src必须有效 7   int len = 0;     //保存src的长度 8   while(*src++ != ‘\0‘)  //遇到结束符‘\0‘时退出循环 9    len++;     //每循环一次,len加1 10  return len; 11 }


2016-03-18
原文:13 int strlen2(const char* src) 14 { 15  assert( NULL != src);  //src必须有效 16  const char *temp = src;  //保存src首地址 17  while(*src++ != ‘\0‘);  //遇到结束符‘\0‘时退出循环 18  return (src-temp-1);   //返回尾部指针与头部指针之差,即长度 19 } 20 21 int main() 22 { 23  char p[] = "Hello World!"; 24  printf("strlen1 len: %d\n", strlen1(p)); //打印方法1得到的结果 25  printf("strlen2 len: %d\n", strlen2(p)); //打印方法2得到的结果 26 27  return 0; 28 }


2016-03-18
原文:trlen1和strlen2这两个函数都可以用来计算字符串长度。下面来比较它们的区别: strlen1用一个局部变量len在遍历的时候做自增,然后返回len。因此,每当while循环一次,就需要执行两次自增操作。 strlen2用一个局部变量temp记录src遍历前的位置。while循环一次只需要一次自增操作。最后返回指针之间的位置差。 显然,strlen2比strlen1的效率要高,尤其是在字符串较长的时候。下面是程序的输出结果。 1 strlen1 len: 12 2 strlen2 len: 12


2016-03-18
原文:函数strcmp的实现细节


2016-03-18
原文:此题实际上就是做一个C/C++库函数中的strcmp的实现。对于两个字符串str1和str2,若相等,则返回0,若str1大于str2,则返回1,若str1小于str2,则返回?1。 程序代码如下。 1 #include <iostream> 2 using namespace std; 3 4 int mystrcmp(const char *src, const char *dst) 5 { 6  int ret = 0 ; 7  while( !(ret = *(unsigned char *)src - *(unsigned char *)dst) && *dst) 8  {         //循环比较两个字符是否相等 9    ++src;      //如果不等或者到了dst字符串末尾,则 10   ++dst;     


2016-03-18
原文:退出循环 11  } 12  if ( ret < 0 )      //ret保存着字符比较的结果 13    ret = -1 ; 14  else if ( ret > 0 ) 15    ret = 1 ; 16  return( ret ); 17 } 18 19 int main() 20 { 21  char str[10] = "1234567"; 22  char str1[10] = "1234567";   //str1 == str 23  char str2[10] = "12345678";  //str2 > str 24  char str3[10] = "1234566";   //str3 < str 25 26  int test1 = mystrcmp(str, str1); //测试str与str1比较 27  int test2 = mystrcmp(str, str2); //测试str与str2比较 28  int test3 = mystrcmp(str, str3); //测试str与str3比较


2016-03-18
原文:30  cout << "test1 = " << test1 << endl; 31  cout << "test2 = " << test2 << endl; 32  cout << "test3 = " << test3 << endl; 33 34  return 0;


2016-03-19
原文:char *get2String(long num)   //得到二进制形


2016-03-19
原文:式的字符串 6 { 7   int i=0; 8   char* buffer; 9   char* temp; 10 11  buffer = (char*)malloc(33); 12  temp = buffer; //temp 13  for (i=0; i < 32; i++) 14  {        //给数组的32个元素赋‘0‘或‘1‘ 15    temp[i] = num & (1 << (31 - i)); 16    temp[i] = temp[i] >> (31 - i); 17    temp[i] = (temp[i] == 0) ? ‘0‘: ‘1‘;


2016-03-19
原文:18  } 19  buffer[32] = ‘\0‘;   //字符串结束符 20 21  return buffer; 22 } 23 24 char *get16String(long num)   //得到十六进制形式的字符串 25 { 26  int i=0; 27  char* buffer = (char*)malloc(11); 28  char* temp; 29 30  buffer[0] = ‘0‘; //”0x”开头


2016-03-19
原文:1  buffer[1] = ‘x‘; 32  buffer[10] = ‘\0‘;   //字符串结束符 33  temp = buffer + 2; 34 35  for (i=0; i < 8; i++) 36  {        //给数组的8个元素赋值 37    temp[i] = (char)(num<<4 * i>>28); 38    temp[i] = temp[i] >= 0 ? temp[i] : temp[i] + 16; 39    temp[i] = temp[i] < 10 ? temp[i] + 48 : temp[i] + 55; 40  } 41  return buffer; 42 }


2016-03-19
原文:编写字符串反转函数:strrev。要求时间和空间效率都尽量高。测试用例:输入"abcd",输出应为"dcba"。


2016-03-19
原文:,一个典型的优化策略就是两个字符交换的算法优化,我们可以借助异或运算符(^)完成两个字符的交换,对应这里的解法3和解法4。解法3: 1 char* strrev3(const char* str) 2 { 3   char* tmp = new char[strlen(str) + 1]; 4   strcpy(tmp,str); 5   char* ret = tmp; 6   char* p = tmp + strlen(str) - 1; 7


2016-03-19
原文:8   while (p > tmp) 9   { 10     *p ^= *tmp; 11     *tmp ^= *p; 12     *p ^= *tmp; 13 14    --p; 15    ++tmp; 16  } 17 18  return ret; 19 } 解法4: 1 char* strrev4(const char* str)


2016-03-19
原文:2 { 3   char* tmp = new char[strlen(str) + 1]; 4   strcpy(tmp,str); 5   char* ret = tmp; 6 7   char* p = tmp + strlen(str) - 1; 8 9   while (p > tmp) 10  { 11     *p = *p + *tmp; 12     *tmp = *p - *tmp; 13     *p = *p - *tmp; 14 15    --p;


2016-03-19
原文:16    ++tmp; 17  } 18 19  return ret; 20 }


2016-03-19
原文:我们还可以使用递归来解决这个问题。每次交换首尾两个字符,中间部分则又变为和原来字符串同样的问题。解法5: 1 char* reverse5(char* str,int len) 2 { 3   if (len <= 1) 4    return str; 5 6   char t = *str; 7   *str = *(str + len -1);


2016-03-19
原文:8   *(str + len -1) = t; 9 10  return (reverse5(str + 1,len - 2) - 1); 11 }


2016-03-19
原文:我们知道在 C/C++中有 int、float、double 等类型来表示数字,但是它们的长度都是有限的。而本题要求可以是任意长度,这里可以用字符串表示数字,结果也用字符串表示。因此,我们所要做的就是做一个类似整数加法的字符串转换,主要是字符做加法运算并且要考虑进位。示例程序如下所示。 1 #include <stdio.h> 2 #include <string.h> 3 #include <stdlib.h>


2016-03-19
原文:4 #include <math.h> 5 6 char* addBigInt(char* num1, char* num2) 7 { 8   int c = 0;    //进位,开始最低进位为0 9   int i = strlen(num1)-1; //指向第一个加数的最低位 10  int j = strlen(num2)-1; //指向第二个加数的最低位 11  int maxLength = strlen(num1) >= strlen(num2) ? 12    strlen(num1)+1 : strlen(num2)+1; //得到2个数中较大数的位数 13  char* rst = (char*)malloc(maxLength+1); //保存结果 14  int k;


2016-03-19
原文:15  if (rst == NULL) 16  { 17   printf("malloc error!\n"); 18   exit(1); 19  } 20 21  rst[maxLength] = ‘\0‘; //字符串最后一位为‘\0‘ 22  k = strlen(rst) - 1 ; //指向结果数组的最低位 23  while ( (i >= 0) && (j >= 0) ) 24  { 25   rst[k] = ( (num1[i] - ‘0‘) + (num2[j] - ‘0‘) + c )%10 +‘0‘; //计算本位的值 26   c = ( (num1[i] - ‘0‘) + (num2[j] - ‘0‘) + c )/10;   //向高位进位值


2016-03-19
原文:27   --i; 28   --j; 29   --k; 30  } 31  while ( i >= 0 ) 32  { 33   rst[k] = ( (num1[i] - ‘0‘) + c )%10 + ‘0‘; 34   c = ( (num1[i] - ‘0‘) + c)/10; 35   --i; 36   --k; 37  } 38  while ( j >= 0 ) 39  { 40   rst[k] = ( (num2[j] - ‘0‘) + c )%10 + ‘0‘;


2016-03-19
原文:41   c = ( (num2[j] - ‘0‘) + c )/10; 42   --j; 43   --k; 44  } 45  rst[0] = c + ‘0‘; 46 47  if ( rst[0] != ‘0‘ )    //如果结果最高位不等于0,则输出结果 48  { 49   return rst; 50  } 51  else 52  { 53   return rst+1;


2016-03-19
原文:54  } 55 } 56 57 int main() 58 { 59  char num1[] = "123456789323"; 60  char num2[] = "45671254563123"; 61  char *result = NULL; 62 63  result = addBigInt(num1, num2); 64  printf("%s + %s = %s\n", num1, num2, result); 65 66  return 0; 67 }


2016-03-19
原文:4 /* 冒泡排序算法 */


2016-03-19
原文:5 void mysort(char *str, int num) 6 { 7  int i, j; 8  int temp = 0; 9 10  for (i = 0; i < num; i++) 11  { 12    for (j = i+1; j < num; j++) 13    { 14     if (str[i] < str[j]) //如果下一个值比当前值大, 15     {      //则交换两个元素值 16       temp = str[i]; 17       str[i] = str[j];


2016-03-19
原文:18       str[j] = temp; 19     } 20    } 21  } 22 }


2016-03-19
原文:编写gbk_strlen函数,计算含有汉字的字符串的长度,汉字作为一个字符处理;已知:汉字编码为双字节,其中首字节<0,尾字节在0~63以外(如果一个字节是?128~127)。


2016-03-19
原文:4 int gbk_strlen(const char *str) 5 { 6   const char *p = str;  //p用于后面遍历 7 8   while(*p)    //若是结束符0,则结束循环 9   { 10    if (*p < 0 && (*(p+1)<0 || *(p+1)>63)) //中文汉字情况 11    { 12     str++;  //str移动一位,p移动两位,因此长度加1 13     p += 2; 14    } 15    else


2016-03-19
原文:16    { 17     p++;   //str不动,p移动一位,长度加1 18    } 19  } 20 21  return p-str;   //返回地址之差 22 } 23


2016-03-20
原文:左移操作<<相当于乘法操作,<< n相当于乘以2n。右移操作>>相当于除法操作,>> n相当于除以2n


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


2016-03-20
原文:通常情况下,应试者对这个问题有3种基本的反应: (1)不知道如何下手。该被面试者从没做过任何嵌入式系统的工作。 (2)用bit fields。bit fields是被扔到C语言死角的东西,它保证你的代码在不同编译器之间是不可移植的,同时


2016-03-20
原文:也保证了你的代码是不可重用的。


2016-03-20
原文:(3)用#defines 和 bit masks 操作。这是一个有极高可移植性的方法,是应该被用到的方法。 最佳的解决方案为: 1 #define BIT3 (0x1 << 3) 2 static int a; 3 4 void set_bit3(void) 5 { 6   a |= BIT3; 7 } 8 9 void clear_bit3(void) 10 { 11  a &=~BIT3;


2016-03-20
原文:12 } 在这里,BIT3用来计算需要操作的位,|=和&=分别用来指定位置1和位置0。


2016-03-24
原文:运用位运算交换a、b两数


2016-03-24
原文:源程序如下所示。 1 #include <stdio.h> 2 3 int main() 4 { 5  int a = 3; 6  int b = 5; 7


2016-03-24
原文:8  a ^= b;       //进行三次异或操作 9  b ^= a; 10  a ^= b; 11 12  printf("a = %d, b = %d\n", a, b); //打印a、b值 13  return 0; 14 } ^是位异或的运算符,即比较相同两位的异同,如果相同,则赋值为0,否则为1。 此例中a、b的初始值分别为3和5,对应的二进制分别为00000011和00000101。经过下面的3个步骤交换了两个变量的值。 代码第 8 行,a 的二进制变为 00000110,b 仍为 00000101。即 b 不变,取出所有不相等的位存入a。 代码第9 行,a 的二进制为00000110,b 变为


2016-03-24
原文:0000011。即a不变,取出所有不相等的位存入b。此时b的值为a的初始值。 代码第10 行,a 的二进制变为00000101,b 为00000011。即b不变,取出所有不相等的位存入a。此时a的值为b的初始值。到此完成a、b两变量值的变换。 算法最大的优点是省略了中间变量,但只能用于相同类型数的交换。


2016-03-24
原文:4种运算符如下。 (1)const_cast 操作符:用来帮助调用那些应该使用却没有使用 const 关键字的函数。换句话说,就是供程序设计师在特殊情况下将限制为const成员函数的const定义解除,使其能更改特定属性。 (2)dynamic_cast 操作符:如果启动了支持运行时间类型信息(RTTI),dynamic_cast可以有助于判断在运行时所指向对象的确切类型。它与 typeid 运算符有关。可以将一个基类的指针指向许多不同的子类型(派生类),然后将


2016-03-24
原文:被转型为基础类的对象还原成原来的类。不过,限于对象指针的类型转换,而非对象变量。 (3)reinterpret_cast操作符:将一个指针转换成其他类型的指针,新类型的指针与旧指针可以毫不相关。通常用于某些非标准的指针数据类型转换,例如将void *转换为char *。它也可以用在指针和整型数之间的类型转换上。注意:它存在潜在的危险,除非有使用它的充分理由,否则就不要使用它。例如,它能够将一个int *类型的指针转换为float *类型的指针,但是这样就会很容易造成整数数据不能被正确地读取。 (4)static_cast操作符:它能在相关的对象和指针类型之间进行类型转换。有关的类之间必须通过继承、构造函数或者转换函数发生联系。static_cast操作符还能在数字(原始的)类型之间进行类型转换。通常情况下,static_cast操作符大多用于将数域宽度较大的类型转换为较小的类型。当转换的类型是原始数据类型时,这种操作可以有效地禁止编译器发出警告。


2016-03-31
原文: #define SECONDS_PER_YEAR (60 * 60 * 24 * 365)UL


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


2016-03-31
原文:嵌入式系统经常要求程序员去访问某特定的内存位置的特点。在某工程中,要求设置一绝对地址为0x67a9的整型变量的值为0xaa66。编译器是一个纯粹的ANSI编译器。写代码去完成这一任务。 【解析】 源代码如下。 1 int *ptr; 2 ptr = (int *)0x67a9; 3 *ptr = 0xaa55


2016-03-31
原文:在C语言中,关键字static有以下3个明显的作用。 (1)在函数体内,一个被声明为静态的变量在这一函数被调用的过程中维持其值不变。 (2)在模块内(但在函数体外),一个被声明为静态的变量可以被模块内所有函数访问,但不能被模块外其他函数访问。它是一个本地的全局变量。 (3)在模块内,一个被声明为静态的函数只可被这一模块内的其他函数调用。那就是,这个函数被限制在声明它模块的本地范围内使用。


2016-03-31
原文:一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说,就是优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。下面是volatile变量的几个例子。 (1)并行设备的硬件寄存器(如状态寄存器); (2)一个中断服务子程序中会访问到的非自动变量(Non-automatic variables); (3)多线程应用中被几个任务共享的变量。


2016-03-31
原文:,若处理器是Big_endian的,则返回0;若是Little_endian的,则返回1。 源程序如下。 1 int checkCPU() 2 { 3   union w


2016-03-31
原文:4   { 5    int a; 6    char b; 7   } c; 8   c.a = 1; 9   return (c.b == 1); 10 } 嵌入式系统开发者应该对Little-endian和Big-endian模式非常了解。采用Little-endian模式的CPU对操作数的存放方式是从低字节到高字节,而Big-endian模式对操作数的存放方式是从高字节到低字节。例如,16bit的数0x1234在Little-endian模式CPU内存中的存放方式(假设从地址0x4000开始存放)为: 0x4000:  0x34 0x4001:  0x12


2016-03-31
原文:而在Big-endian模式CPU内存中的存放方式则为: 0x4000:  0x12 0x4001:  0x34 32bit 宽的数 0x12345678 在 Little-endian 模式 CPU 内存中的存放方式(假设从地址0x4000开始存放)为: 0x4000:  0x78 0x4001:  0x56 0x4000:  0x34 0x4001:  0x12 而在Big-endian模式CPU内存中的存放方式则为: 0x4000:  0x12 0x4001:  0x34 0x4000:  0x56 0x4001:  0x78


2016-03-31
原文:联合体union的存放顺序是所有成员都从低地址开始存放,利用该特性,轻松地获得了CPU对内存采用Little-endian还是Big-endian模式的读写。


2016-03-31
原文:unsigned int zero = 0; unsigned int compzero = 0xFFFF; /*1‘s complement of zero */ 【解析】 对于一个int型不是16位的处理器来说,上面的代码是不正确的。应编写如下: unsigned int compzero = ~0; 这一问题能真正揭露出应试者是否懂得处理器字长的重要性。好的嵌入式程序员能够非常准确地明白硬件的细节和它的局限性,然而PC机程序员往往把硬件作为一个无法避


2016-03-31
原文:向对象的基本概念 出现频率:★★★★ 【解析】 面向对象的基本概念:按照人们认识客观世界的系统思维方式,采用基于对象(实体)的概念建立模型,模拟客观世界分析、设计、实现软件的办法。通过面向对象的理念使计算机软件系统能与现实世界中的系统一一对应。它包括下面几方面的内容。 类(class):具有相似的内部状态和运动规律的实体集合。类来自于人们认识自然、认识社会的过程。在这一过程中,人们主要使用两种方法:由特殊到一般的归纳法和由一般到特殊的演绎法。在归纳的过程中,从一个个具体的事物中把共同的特征抽取出来,形成一个一般的概念。在演绎的过程中又把同类的事物,根据不同的特征分成不同的小类。


2016-03-31
原文:对象(object):指现实世界中各种各样的实体,也就是类(class)的实例。它既可以指具体的事物,也可以指抽象的事物。每个对象都有自己的内部状态和运动规律。在面向对象概念中,把对象的内部状态称为属性,运动规律称为方法或事件。 消息(message):指对象间相互联系和相互作用的方式。一个消息主要由 5 部分组成:发送消息的对象、接收消息的对象、消息传递办法、消息内容(参数)、反馈。 类的特性:抽象、继承、封装、重载、多态。 【答案】 面向对象是指按人们认识客观世界的系统思维方式,采用基于对象(实体)的概念建立模型,模拟客观世界分析、设计、实现软件的办法,包括类、对象、消息以及类的特性等方面的内容。


2016-03-31
原文::class和struct的区别


2016-03-31
原文:这里有两种情况下的区别: (1)C语言的struct与C++的class的区别。 (2)C++中的struct和class的区别。 在第一种情况下,struct与


2016-03-31
原文:class有着非常明显的区别。C是一种过程化的语言,struct只是作为一种复杂数据类型定义,struct中只能定义成员变量,不能定义成员函数。例如下面的C代码片断。 1 struct Point 2 { 3   int x; //合法 4   int y; //合法 5   void print() 6   { 7    printf("Point print\n"); //编译错误 8   } 9 } 这里第7行会出现编译错误


2016-03-31
原文:提示如下的错误消息:“函数不能作为Point结构体的成员”。因此大家看到在第一种情况下,struct 只是一种数据类型,不能使用面向对象编程。现在来看第二种情况。首先请看下面的代码。 1 #include <iostream> 2 using namespace std; 3 4 class CPoint 5 { 6   int x;     //默认为private 7   int y;     //默认为private 8   void print()


2016-03-31
原文://默认为private 9   { 10   cout << "CPoint: (" << x << ", " << y << ")" << endl; 11  } 12 public: 13  CPoint(int x, int y)  //构造函数,指定为public 14  { 15   this->x = x; 16   this->y = y; 17  } 18  void print1() //public


2016-03-31
原文:19  { 20   cout << "CPoint: (" << x << ", " << y << ")" << endl; 21  } 22 }; 23 24 struct SPoint 25 { 26  int x;    //默认为public 27  int y;    //默认为public 28  void print()   //默认为public 29  {


2016-03-31
原文:30   cout << "SPoint: (" << x << ", " << y << ")" << endl; 31  } 32  SPoint(int x, int y) //构造函数,默认为public 33  { 34   this->x = x; 35   this->y = y; 36  } 37 private: 38  void print1() //private类型的成员函数 39  { 40   cout << "SPoint: (" << x << ", " << y << ")" <<


2016-03-31
原文:endl; 41  } 42 }; 43 44 int main(void) 45 { 46  CPoint cpt(1, 2);  //调用CPoint带参数的构造函数 47  SPoint spt(3, 4);  //调用SPoint带参数的构造函数 48 49  cout << cpt.x << " " << cpt.y << endl; //编译错误 50  cpt.print();    //编译错误


2016-03-31
原文:51  cpt.print1();   //合法 52 53  spt.print();     //合法 54  spt.print1();     //编译错误 55  cout << spt.x << " " << spt.y << endl; //合法 56 57  return 0; 58 } 在上面的程序里,struct还有构造函数和成员函数,其实它还拥有class的其他特性,例如继承、虚函数等。因此,C++中的


2016-03-31
原文:struct扩充了C的struct功能。那么它们有什么不同呢? main函数内的编译错误全部是因为访问private成员而产生的。因此我们可以看到class中默认的成员访问权限是private的,而struct中则是public的。在类的继承方式上,struct和class又有什么区别?请看下面的程序。 1 #include <iostream> 2 using namespace std; 3 4 class CBase 5 { 6 public: 7  void print()    


2016-03-31
原文://public成员函数 8  { 9    cout << "CBase: print()..." << endl; 10  } 11 }; 12 13 class CDerived1 : CBase    //默认private继承 14 { 15 }; 16 17 class CDerived2 : public Cbase  //指定public继承


2016-03-31
原文:18 { 19 }; 20 21 struct SDerived1 : Cbase   //默认public继承 22 { 23 }; 24 25 struct SDerived2 : private Cbase //指定public继承 26 { 27 }; 28 29 int main()


2016-03-31
原文:30 { 31  CDerived1 cd1; 32  CDerived2 cd2; 33  SDerived1 sd1; 34  SDerived2 sd2; 35 36  cd1.print();    //编译错误 37  cd2.print(); 38  sd1.print(); 39  sd2.print();    //编译错误 40 41  return 0; 42 }


2016-03-31
原文:可以看到,以private方式继承父类的子类对象不能访问父类的public成员。class继承默认是private继承,而struct继承默认是public继承。 另外,在C++模板中,类型参数前面可以使用class或typename。如果使用struct,则含义不同,struct 后面跟的是"non-type template parameter",而class 或typename 后面跟的是类型参数。 事实上,C++中保留struct的关键字是为了使C++编译器能够兼容C语言开发的程序。 【答案】 分以下两种情况。


2016-03-31
原文:C 语言的struct 与C++的class 的区别:struct 只是作为一种复杂数据类型定义,不能用于面向对象编程。 C++中的 struct 和 class 的区别:对于成员访问权限以及继承方式,class 中默认的是private的,而struct中则是public的。class还可以用于表示模板类型,struct则不行。


2016-04-05
原文:初始化列表的初始化顺序与变量声明的顺序一致,而不是按照出现在初始化列表中的顺序。


2016-04-05
原文:是静态成员与非静态成员的区别。静态成员被当作该类类型的全局变量。对于非静态成员,每个类对象都有自己的复制品,而静态成员对每个类的类型只有一个复制品。静态成员只有一份,由该类类型的所有对象共享访问。


2016-04-05
原文:静态数据成员没有进入程序的全局名字空间,因此不存在程序中其他全局名字冲突的可能性。使用静态数据成员可以隐藏信息。因为静态成员可以是 private 成员,而全局对象不能。


2016-04-06
原文:为了与非静态成员变量相区别,i不能在类内部被初始化。


2016-04-06
原文:要知道,静态成员函数和静态成员变量一样,不属于类的对象,因此它不含this指针,也就无法调用类的非静态成员。


2016-04-06
原文:全局对象的构造函数会在main函数之前执行。


2016-04-06
原文:对于一个C++的空类,比如Empty:1 class Empty2 {3 };虽然 Empty 类定义中没有任何成员,但为了进行一些默认的操作,编译器会加入以下一些成员函数,这些成员函数使得类的对象拥有一些通用的功能。默认构造函数和复制构造函数。它们被用于类的对象的构造过程。析构函数。它被用于类的对象的析构过程。赋值函数。它被用于同类的对象间的赋值过程。取值运算。当对类的对象进行取地址(&)时,此函数被调用。即虽然程序员没有定义类的任何成员,但是编译器也会插入一些函数,完整的 Empty类定义如下。


2016-04-06
原文:1 class Empty2 {3 public:4 Empty();       //缺省构造函数5 Empty( const Empty& );    //复制构造函数6 ~Empty();       //析构函数7 Empty& operator=( const Empty& ); //赋值运算符8 Empty* operator&(); //取址运算符9 const Empty* operator&() const;  //取址运算符 const10 };【答案】C++的空类中,默认会产生默认构造函数、复制构造函数、析构函数、赋值函数以及取值运算。


2016-04-06
原文:构造函数可以被重载,因为构造函数可以有多个,且可以带参数。析构函数不可以被重载。因为析构函数只能有一个,且不能带参数。


2016-04-06
原文:以下代码中的输出语句输出0吗?为什么?1 #include <iostream>2 using namespace std;34 struct CLS5 {6   int m_i;7   CLS( int i ) : m_i(i) {}8   CLS()9   {10   CLS(0);11  }12 };13 int main()14 {15  CLS obj;16  cout << obj.m_i << endl;17  return 0;18 }


2016-04-06
原文:在代码第10行,不带参数的构造函数直接调用了带参数的构造函数。这种调用往往被很多人误解,以为可以通过构造函数的重载和相互调用实现一些类似默认参数的功能,其实是不行的,而且往往会有副作用。下面加几条打印对象地址的语句到原来的程序中。1 #include <iostream>2 using namespace std;34 struct CLS5 {6   int m_i;7   CLS( int i ) : m_i(i)8   {9    cout << "CLS(): this = " << this << endl;10 }11  CLS()12  {13   CLS(0);14   cout << "CLS(int): this = " << this << endl;15  }1617 };1819 int main()20 {


2016-04-06
原文:21  CLS obj;22  cout << "&obj = " << &obj << endl;23  cout << obj.m_i << endl;24  return 0;25 }程序执行结果如下。1 CLS(): this = 0012FF202 CLS(int): this = 0012FF7C3 &obj = 0012FF7C4 -858993460可以看到,在带参数的构造函数里打印出来的对象地址和对象obj的地址不一致。实际上,代码第13行的调用只是在栈上生成了一个临时对象,对于自己本身毫无影响。还可以发现,构造函数的互相调用引起的后果不是死循环,而是栈溢出。【答案】输出不为0,是个随机数。原因是构造函数内调用构造函数只是在栈上生成了一个临时对象,对于自己本身毫无影响。


2016-04-06
原文:explicit构造函数是用来防止隐式转换的。请看下面的代码。1 class Test12 {3 public:4   Test1(int n) { num = n; }  //普通构造函数5 private:6   int num;7 };89 class Test210 {11 public:12  explicit Test2(int n) { num = n; } //explicit(显式)构造函数13 private:14  int num;15 };16


2016-04-06
原文:17 int main()18 {19  Test1 t1 = 12;   //隐式调用其构造函数,成功20  Test2 t2 = 12;   //编译错误,不能隐式调用其构造函数21  Test2 t3(12);   //显示调用成功22  return 0;23 }Test1的构造函数带一个int型的参数,代码第19行会隐式转换成调用Test1的这个构造函数。而Test2的构造函数被声明为explicit(显式),这表示不能通过隐式转换来调用这个构造函数,因此代码第20行会出现编译错误。【答案】普通构造函数能够被隐式调用,而explicit构造函数只能被显示调用。


2016-04-06
原文:大家知道,析构函数是为了在对象不被使用之后释放它的资源,虚函数是为了实现多态。那么,把析构函数声明为virtual有什么作用呢?请看下面的代码。1 #include <iostream>2 using namespace std;34 class Base5 {6 public:7   Base() {};   //Base的构造函数8   ~Base()    //Base的析构函数9  {10    cout << "Output from the destructor of class Base!" << endl;11  };12  virtual void DoSomething()13  {


2016-04-06
原文:14    cout << "Do something in class Base!" << endl;15  };16 };1718 class Derived : public Base19 {20 public:21  Derived() {};  //Derived的构造函数22  ~Derived()   //Derived的析构函数23  {24   cout << "Output from the destructor of class Derived!" << endl;25  };26  void DoSomething()27  {28   cout << "Do something in class Derived!" << endl;29  };30 };3132 int main()33 {34  Derived *pTest1 = new Derived(); //Derived类


2016-04-06
原文:的指针35  pTest1->DoSomething();36  delete pTest1;3738  cout << endl;3940  Base *pTest2 = new Derived(); //Base类的指针41  pTest2->DoSomething();42  delete pTest2;4344  return 0;45 }先看程序输出结果:1 Do something in class Derived!2 Output from the destructor of class Derived!3 Output from the destructor of class Base!45 Do something in class Derived!6 Output from the destructor of class Base!代码第36行可以正常释放pTest1的资源,而代码第42行没有正常释放pTest2的资源,因


2016-04-06
原文:为从结果看,Derived类的析构函数并没有被调用。通常情况下,类的析构函数里面都是释放内存资源,而析构函数不被调用的话就会造成内存泄漏。原因是指针pTest2是Base类型的指针,释放pTest2时只进行Base类的析构函数。在代码第8行前面加上virtual关键字后的运行结果如下。1 Do something in class Derived!2 Output from the destructor of class Derived!3 Output from the destructor of class Base!45 Do something in class Derived!6 Output from the destructor of class Derived!7 Output from the destructor of class Base!此时释放指针pTest2时,由于Base的析构函数是virtual的,就会先找到并执行Derived类的析构函数,然后执行Base类的析构函数,资源正常释放,避免了内存泄漏。因此,只有当一个类被用来作为基类的时候,才会把析构函数写成虚函数。


2016-04-06
原文:先来说明什么是复制构造函数,以及它被调用的场合。 复制构造函数是一种特殊的构造函数,它由编译器调用来完成一些基于同一类的其他对象的构件及初始化。 如果在类中没有显式地声明一个复制构造函数,那么,编译器会私下里制定一个函数来进行对象之间的位复制(bitwise copy)。这个隐含的复制构造函数简单地关联了所有的类成员。


2016-04-06
原文:在C++中,下面是3种对象需要复制的情况。因此,复制构造函数将会被调用。 (1)一个对象以值传递的方式传入函数体。 (2)一个对象以值传递的方式从函数返回。 (3)一个对象需要通过另外一个对象进行初始化。 下面的程序代码说明了上述三种情况。 1 #include <iostream> 2 using namespace std; 3 4 class Test 5 { 6 public: 7   int a; 8   Test(int x)


2016-04-06
原文:9   { 10   a = x; 11  } 12  Test(Test &test)   //复制构造函数 13  { 14   cout << "copy constructor" << endl; 15   a = test.a; 16  } 17 }; 18 19 void fun1(Test test)   //(1)值传递传入函数体 20 { 21  cout << "fun1()..." << endl; 22 }


2016-04-06
原文:23 24 Test fun2()      //(2)值传递从函数体返回 25 { 26  Test t(2); 27  cout << "fun2()..." << endl; 28  return t; 29 } 30 31 int main() 32 { 33  Test t1(1); 34  Test t2 = t1;    //(3)用t1对t2做初始化 35  cout << "before fun1()..." << endl; 36  fun1(t1);


2016-04-06
原文:37 38  Test t3 = fun2(); 39  cout << "after fun2()..." << endl; 40  return 0; 41 } 程序执行结果如下。 1 copy constructor 2 before fun1()… 3 copy constructor 4 fun1()… 5 fun2()… 6 copy constructor 7 copy constructor 8 after fun2()…


2016-04-06
原文:fun1()、fun2()以及代码第34行分别对应了上面3种调用复制构造函数的情况。 接下来说明深复制与浅复制。 既然系统会自动提供一个默认的复制构造函数来处理复制,那么,为什么要去自定义复制构造函数呢?下面的程序代码说明了这个问题。 1 #include <iostream> 2 using namespace std; 3 4 class Test 5 { 6 public: 7   char *buf; 8   Test(void)        //不带参数的构造函数


2016-04-06
原文:9   { 10   buf = NULL; 11  } 12  Test(const char* str)     //带参数的构造函数 13  { 14   buf = new char[strlen(str) + 1]; //分配堆内存 15   strcpy(buf, str);     //复制字符串 16  } 17  ~Test() 18  { 19   if (buf != NULL) 20   { 21     delete buf;     //释放buf指向的堆内存


2016-04-06
原文:22     buf = NULL; 23   } 24  } 25 }; 26 27 int main() 28 { 29  Test t1("hello"); 30  Test t2 = t1;       //调用默认的复制构造函数 31 32  cout << "(t1.buf == t2.buf) ? " << 33   (t1.buf == t2.buf ? "yes": "no") << endl; 34


2016-04-06
原文:35  return 0; 36 } 这里 Test 类的 buf 成员是一个字符指针,在带参数的构造函数中为之分配了一块堆内存来存放字符串,然后在析构函数中又将堆内存释放。在main()函数(代码第30行)使用了对象复制,因此会调用默认的复制构造函数。程序的执行结果如下。 1 (t1.buf == t2.buf) ? yes 2 程序崩溃 这里程序崩溃发生在main()函数退出对象析构的时候。由前两行的打印结果可以看出,默认复制构造函数只是简单地把两个对象的指针做赋值运算,它们指向的是同一个地址。当产生两次析构,释放同一块堆内存时发生崩溃。可以在 Test 类里通过添加一个自定义的复制构造函数解决两次析构的问题。 1 Test(Test &test) 2 {


2016-04-06
原文:3  buf = new char[strlen(test.buf) + 1]; 4  strcpy(buf, test.buf); 5 } 程序执行结果如下。 1 (t1.buf == t2.buf) ? no 由于此时buf又分配了一块堆内存来保存字符串,t1的buf和t2的buf分别指向不同的堆内存,析构时就不会发生程序崩溃。 总结:如果复制的对象中引用了某个外部的内容(例如分配在堆上的数据),那么在复制这个对象的时候,让新旧两个对象指向同一个外部的内容,就是浅复制;如果在复制这个对象的时候为新对象制作了外部对象的独立复制,那么就是深复制。 【答案】 复制构造函数是一种特殊的构造函数,它由编译器调用来完成一些基于同一类的其他对象的构件及初始化。 浅复制是指让新旧两个对象指向同一个外部的内容,而


2016-04-06
原文:深复制是指为新对象制作了外部对象的独立复制。


2016-04-06
原文:如果用户没有自定义复制构造函数,并且在代码中用到了复制构造函数,那么编译器就会生成默认的复制构造函数;但如果用户定义了复制构造函数,那么编译器就不会再生成复制构造函数。 如果用户定义了一个构造函数,且不是复制构造函数,而此时在代码中用到了复制构造函数,那么编译器也还会生成默认的复制构造函数;如果没有使用,则编译器就不会生


2016-04-06
原文:成默认的复制构造函数。


2016-04-06
原文:当然,如果基类中没有私有成员,即所有成员都能被派生类访问,则派生类的复制构造函数可以很容易写。但如果基类有私有成员,并且这些私有成员必须在调用派生类的复制构造函数时被初始化,在这种情况下又该怎么做呢? 编写继承类的复制函数有一个原则:使用基类的复制构造函数。这个原则其实就是解决上述问题的方案。请看下面的程序代码。


2016-04-07
原文:构造函数与赋值函数的区别 出现频率:★★★ 【解析】 有3个方面的区别: (1)复制构造是一个对象来初始化一块内存区域,这块内存就是新对象的内存区。例如 1 class A; 2 A a; 3 A b=a; //复制构造函数调用


2016-04-07
原文:5 A b(a); //复制构造函数调用 而赋值函数是对于一个已经被初始化的对象来进行 operator=操作。例如 1 class A; 2 A a; 3 A b; 4 b = a; //赋值函数调用 (2)一般来说是在数据成员包含指针对象的时候,应付两种不同的处理需求:一种是复制指针对象,一种是引用指针对象。复制构造函数在大多数情况下是复制,赋值函数则是引用对象。 (3)实现不一样。复制构造函数首先是一个构造函数,它调用的时候是通过参数传进来的那个对象来初始化产生一个对象。赋值函数则是把一个对象赋值给一个原有的对象,所以,如果原来的对象中有内存分配,要先把内存释放掉,而且还要检查一下两个对象是不是同一个对象,如果是的话,就不做任何操作。


2016-04-07
原文:已知类String的原型为: 1 class String 2 { 3 public: 4   String(const char *str = NULL); //普通构造函数 5   String(const String &other);  //复制构造函数 6   ~ String(void);    //析构函数


2016-04-07
原文:7   String & operator =(const String &other); //赋值函数 8 private: 9   char *m_String;    //私有成员,保存字符串 10 }; 【解析】 程序代码如下。 1 #include <iostream> 2 using namespace std; 3 4 class String 5 { 6 public: 7   String(const char *str = NULL);   //普通构


2016-04-07
原文:造函数 8   String(const String &other);    //复制构造函数 9   ~ String(void);       //析构函数 10  String & operator =(const String &other); //赋值函数 11 private: 12  char *m_String;       //私有成员,保存字符串 13 }; 14 15 String::~String(void) 16 { 17  cout << "Destructing"<< endl; 18  if (m_String != NULL)     //如果m_String


2016-04-07
原文:不为NULL,释放堆内存 19  { 20    delete [] m_String; 21    m_String = NULL;     //释放后置为NULL 22  } 23 } 24 25 String::String(const char *str) 26 { 27  cout << "Construcing" << endl; 28  if(str == NULL)      //如果str为NULL,存空字符串"" 29  {


2016-04-07
原文:30   m_String = new char[1];   //分配一个字节 31    *m_String = ‘\0‘;    //将之赋值为字符串结束符 32  } 33  else 34  { 35   m_String = new char[strlen(str) + 1]; //分配空间容纳str内容 36   strcpy(m_String, str);   //复制str到私有成员 37  } 38 } 39 40 String::String(const String &other) 41 {


2016-04-07
原文:42  cout << "Constructing Copy" << endl; 43  m_String = new char[strlen(other.m_String) + 1]; //分配空间容纳str内容 44  strcpy(m_String, other.m_String);     //复制str到私有成员 45 } 46 47 String & String::operator = (const String &other) 48 { 49  cout << "Operate = Function" << endl; 50  if(this == &other)        //如果对象与other是同一个对象 51  {             //直接返回本身 52   return *this; 53  }


2016-04-07
原文:54  delete [] m_String;        //释放堆内存 55  m_String = new char[strlen(other.m_String)+1]; 56  strcpy(m_String, other.m_String); 57 58  return *this; 59 } 60 61 int main() 62 { 63  String a("hello");        //调用普通构造函数 64  String b("world");        //调用普通构造函数 65  String c(a);          //调用复制构


2016-04-07
原文:造函数 66  c = b;           //调用赋值函数 67 68  return 0; 69 } (1)普通构造函数:这里判断了传入的参数是否为NULL。如果是NULL,初始化一个字节的空字符串(包括结束符‘\0‘);如果不是,分配足够大小长度的堆内存来保存字符串。 (2)复制构造函数:只是分配足够小长度的堆内存来保存字符串。 (3)析构函数:如果类私有成员m_String不为NULL,释放m_String指向的堆内存,并且为了避免产生野指针,将m_String赋为NULL。 (4)赋值函数:首先判断当前对象与引用传递对象是否是同一个对象,如果是,不做操作,直接返回;否则,先释放当前对象的堆内存,然后分配足够大小长度的堆内存复制


2016-04-07
原文:字符串。 程序的执行结果如下。 1 Construcing 2 Construcing 3 Construcing Copy 4 Operate = Function 5 Destructing 6 Destructing 7 Destructing 这里代码第 63~66 行会发生构造函数以及赋值函数的调用,而析构函数的调用发生在main()函数退出时。


2016-04-08
原文:对C++静态成员和临时对象的理解 出现频率:★★★★ 1 #include <iostream> 2 using namespace std ; 3 4 class human 5 { 6 public: 7  human() 8  {


2016-04-08
原文:9   human_num++; 10  } 11  static int human_num; 12  ~human() 13  { 14   human_num— 15   print(); 16  } 17  void print() 18  { 19   cout<<"human nun is: "<<human_num<<endl; 20  } 21 }; 22


2016-04-08
原文:23 int human::human_num = 0; 24 25 human f1(human x) 26 { 27  x.print(); 28  return x; 29 } 30 31 int main(int argc, char* argv[]) 32 { 33  human h1; 34  h1.print(); 35  human h2 = f1(h1); 36  h2.print();


2016-04-08
原文:37 38  return 0; 39 } 【解析】 这个程序的 human 类有一个静态成员 human_num ,每执行一次,普通构造函数human_num加1,每执行一次,析构函数human_num减1。注意,在f1()函数中会使用默认的复制构造函数,而默认的复制构造函数没有对human_num处理。 代码第34行,只构造了对象h1(调用普通构造函数),因此打印1。 代码第35行使用值传递参数的方式调用了f1()函数,这里分为3步: (1)在f1()函数内首先会调用复制构造函数生成一个临时对象,因此代码第27行打印1。 (2)f1()函数内调用复制构造函数,给main的对象h2


2016-04-08
原文:初始化(复制临时对象)。 (3)f1()函数返回后,临时对象发生析构,此时human的静态成员human_num为0,打印出0。 代码第36行打印的还是0。 main()函数结束时有h1和h2两个对象要发生析构,所以分别打印出-1和-2。 程序的意图其实很明显,就是静态成员用human_num记录类human的实例数。然而,由于默认的复制构造没有对静态成员操作,导致了执行结果的不正确。这里可以通过添加一个自定义的复制构造函数解决。 1 human(human &h) 2 { 3  human_num++; 4 } 此时 human_num就能起到应有的作用了。


2016-04-08
原文:函数重载是用来描述同名函数具有相同或者相似的功能,但数据类型或者是参数不同的函数管理操作。


2016-04-08
原文:函数重载是用来描述同名函数具有相同或者相似的功能,但数据类型或者是参数不同的函数管理操作。 函数名经过 C++编译器处理后包含了原函数名、函数参数数量及返回类型信息,而 C语言不会对函数名进行处理。


2016-04-08
原文:函数重载的正确声明 出现频率:★★★★ 1 (A) int calc(int,int); 2  int calc(const int,const int); 3 4 (B) int get(); 5  double get(); 6 7 (C) int *reset(int *); 8  double *reset(double *);


2016-04-08
原文:0 (D) extern "C" int compute(int *,int); 11  extern "C" double compute(double *,double); 【解析】 A错误。第二个函数被视为重复声明,第二个声明中的const修饰词会被忽略。 B错误。第二个声明是错误的,因为单就函数的返回值而言,不足以区分两个函数的重载。 C正确。这是合法的声明,reset()函数被重载。 D 错误。第二个函数声明是错误的,因为在一组重载函数中,只能有一个函数被指定为extern "C"。 【答案】 C正确。


2016-04-09
原文:继承和多态是 C++面向对象程序设计的关键。继承机制使得派生类能够获得基类的成员数据和方法,只需要在派生类中增加基类没有的成员。多态是建立在继承的基础上的,它使用了 C++编译器最核心的一个技术,即动态绑定技术。其核心思想是父类对象调用子类对象的方法。


2016-04-09
原文:C++中继承主要有三种关系:public、protected和private。 (1)public继承 public继承是一种接口继承,子类可以代替父类完成父类接口所声明的行为。此时子类可以自动转换成为父类的接口,完成接口转换。从语法角度上来说,public继承会保留父类中成员(包括函数和变量等)的可见性不变,也就是说,如果父类中的某个函数是 public的,那么在被子类继承后仍然是public的。 (2)protected继承


2016-04-09
原文:protected 继承是一种实现继承,子类不能代替父类完成父类接口所声明的行为,此时子类不能自动转换成为父类的接口。从语法角度上来说,protected继承会将父类中的public可见性的成员修改成为protected可见性,相当于在子类中引入了protected成员,这样在子类中同样还是可以调用父类的protected和public成员,子类的子类也就可以调用被protected继承的父类的protected和public成员。 (3)private继承 private 继承是一种实现继承,子类不能代替父类完成父类接口所声明的行为,此时子类不能自动转换成为父类的接口。从语法角度上来说,private 继承会将父类中的 public 和protected 可见性的成员修改成为 private 可见性。这样一来,虽然子类中同样还是可以调用父类的protected和public成员,但是子类的子类就不可以再调用被private继承的父类的成员了。


2016-04-12
原文:在derived类进行构造时,它首先要调用其基类(base类)的构造方法,由于没有指明何种构造方法,因此默认调用base类不带参数的构造方法。然而,基类base中已经定义了带一个参数的构造函数,所以编译器就不会给它定义默认的构造函数了。因此代码第17行会出现“找不到构造方法”的编译错误。解决办法:可以在derived的构造函数中显示调用base的构造函数。


2016-04-13
原文:Base类含有纯虚函数Fun1()以及Fun2(),因此它为抽象类,它通过虚函数调用了 T 中的重写版本。这种情况就不能使用组合了,因为组合的对象关系中不能使用一个抽象类,抽象类不能被实例化。


2016-04-13
原文:相同点:都可以表示“有一个”关系。不同点:私有继承中派生类能访问基类的protected成员,并且可以重写基类的虚函数,甚至当基类是抽象类的情况。组合不具有这些功能。注意:选择它们的原则为尽可能使用组合,万不得已才用私有继承。


2016-04-13
原文:多态(Polymorphism)、封装(Encapsulation)和继承(Inheritance)是面向对象思想的“三大特征”。可以说,不懂得什么是多态就不能说懂得面向对象。多态性的定义:同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。有两种类型的多态性:(1)编译时的多态性。编译时的多态性是通过重载来实现的。对于非虚的成员来说,系统在编译时,根据传递的参数、返回的类型等信息决定实现何种操作。(2)运行时的多态性。运行时的多态性就是指直到系统运行时,才根据实际情况决定实现何种操作。C++中,运行时的多态性通过虚成员实现。例如下面的程序代码。


2016-04-13
原文:1 #include <iostream>2 using namespace std;34 class Person5 {6 public:7  virtual void print() {cout << "I‘m a Person" << endl;}8 };9 class Chinese : public Person10 {11 public:12  virtual void print() {cout << "I‘m from China" << endl;}13 };1415 class American : public Person16 {


2016-04-13
原文:17 public:18  virtual void print() {cout << "I‘m from USA" << endl;}19 };2021 void printPerson(Person &person)22 {23  person.print();  //运行时决定调用哪个类中的print()函数24 }2526 int main()27 {


2016-04-13
原文:28  Person p;29  Chinese c;30  American a;31  printPerson(p);32  printPerson(c);33  printPerson(a);34  return 0;35 }执行结果如下。1 I‘m a Person2 I‘m from China3 I‘m from USA可以看到,在运行时通过基类Person的对象,可以来调用派生类Chinese和American中的实现方法。Chinese和American的方法都是通过覆盖基类中的虚函数方法来实现的。


2016-04-14
原文:简单地说,虚函数是通过虚函数表实现的。那么,什么是虚函数表呢?事实上,如果一个类中含有虚函数,则系统会为这个类分配一个指针成员指向一张虚函数表(vtbl),表中每一项指向一个虚函数的地址,实现上就是一个函数指针


2016-04-14
原文:在类对象的内存布局中,首先是该类的 vtbl 指针,然后才是对象数据。在通过对象指针调用一个虚函数时,编译器生成的代码将先获取对象类的vtbl指针,然后调用vtbl中对应的项。对于通过对象指针调用的情况,在编译期间无法确定指针指向的是基类对象还是派生类对象,或者是哪个派生类的对象。但是在运行期间执行到调用语句时,这一点已经确定,编译后的调用代码能够根据具体对象获取正确的 vtbl,调用正确的虚函数,从而实现多态性。


2016-04-14
原文:分析一下这里的思想所在,问题的实质是这样,对于发出虚函数调用的这个对象指针,在编译期间缺乏更多的信息,而在运行期间具备足够的信息,但那时已不再进行绑定了,怎么在二者之间做一个过渡呢?把绑定所需的信息用一种通用的数据结构记录下来,该数据结构可以同对象指针相联系,在编译时只需要使用这个数据结构进行抽象的绑定,而在运行期间将会得到真正的绑定。这个数据结构就是vtbl。可以看到,实现用户所需的抽象和多态需要进行后绑定,而编译器又是通过抽象和多态实现后绑定的。


2016-04-14
原文:构造函数调用虚函数考点:C++虚拟机制的理解出现频率:★★★★★1 #include <stdio.h>23 class A4 {5 public:6  A() { doSth(); }    //构造函数调用虚函数


2016-04-14
原文:7  virtual void doSth() { printf("I am A"); }8 };910 class B : public A11 {12 public:13  virtual void doSth() { printf("I am B"); }14 } ;1516 int main()17 {18  B b;19  return 0;


2016-04-14
原文:20 }执行结果是什么?为什么?【解析】在构造函数中,虚拟机制不会发生作用,因为基类的构造函数在派生类构造函数之前执行,当基类构造函数运行时,派生类数据成员还没有被初始化。如果基类构造期间调用的虚函数向下匹配到派生类,派生类的函数理所当然会涉及本地数据成员,但是那些数据成员还没有被初始化,而调用涉及一个对象还没有被初始化的部分自然是危险的,所以C++会提示此路不通。因此,虚函数不会向下匹配到派生类,而是直接执行基类的函数。【答案】


2016-04-14
原文:在构造函数中,虚拟机制不会发生作用,执行结果为:1 I am A


2016-04-14
原文:1 #include <iostream> 2 #include <string> 3 using namespace std; 4 5 void println(const std::string& msg) 6 { 7  cout << msg << "\n"; 8 } 9


2016-04-14
原文:10 class Base 11 { 12 public: 13  Base() 14  { 15   println("Base::Base()"); 16   virt(); 17  } 18  void f() 19  { 20   println("Base::f()"); 21   virt(); 22  } 23  virtual void virt()


2016-04-14
原文:24  { 25   println("Base::virt()"); 26  } 27 }; 28 29 class Derived : public Base 30 { 31 public: 32  Derived() 33  { 34   println("Derived::Derived()"); 35   virt(); 36  } 37  virtual void virt()


2016-04-14
原文:38  { 39   println("Derived::virt()"); 40  } 41 }; 42 43 int main(int argc,char* argv[]) 44 { 45  Derived d; 46  Base *pB=&d; 47  pB->f(); 48  return 0; 49 } 【解析】 代码第45行,构造Derived对象d。首先调用Base的


2016-04-14
原文:构造函数,然后调用Derived的构造函数。在Base类的构造函数中,又调用了虚函数virt(),此时虚拟机制还没有开始作用(因为是在构造函数中),所以执行的是Base类的virt()函数。同样,在Derived类的构造函数中,执行的是Derived类的virt()函数。 代码第47行,通过Base类的指针pB访问Base类的公有成员函数f()。f()函数又调用了虚函数virt(),这里出现多态。由于指针pB是指向Derived类对象的,因此实际执行的是Derived类中的virt()成员。 【答案】 1 Base::Base() 2 Base::virt() 3 Derived::Derived() 4 Derived::virt() 5 Base::f() 6 Derived::virt()


2016-04-14
原文:多重继承的缺点是什么呢?如果派生类所继承的多个基类有相同的基类,而派生类对象需要调用这个祖先类的接口方法,就会容易出现二义性。代码第 33、34 行就是因为这个原因而出现编译错误的。因为通过多重继承的Programmer_Author类拥有Author类和 Programmer类的一份拷贝,而 Author类和 Programmer类都分别拥有Person类的一份拷贝,所以Programmer_Author类拥有Person类的两份拷贝,在调用Person类的接口时,编译器会不清楚需要


2016-04-14
原文:调用哪一份拷贝,从而产生错误。对于这个问题,通常有两个解决方案: (1)加上全局符确定调用哪一份拷贝。比如pa.Author::eat()调用属于Author的拷贝。 (2)使用虚拟继承,使得多重继承类Programmer_Author只拥有Person类的一份拷贝。比如在第11行和第17行的继承语句中加入virtual就可以了。 1 class Author : virtual public Person   //Author虚拟继承自Person 2 class Programmer : virtual public Person  //Programmer虚拟继承自Person 【答案】 实际生活中,一些事物往往会拥有两个或两个以上事物的属性,为了解决这个问题, C++引入了多重继承的概念。 多重继承的优点是对象可以调用多个基类中的接口。 多重继承的缺点是容易出现继承向上的二义性。


2016-04-14
原文:多重继承中二义性的消除 出现频率:★★★★ 类A派生B和C,类D从B,C派生,如何将一个类A的指针指向一个类D的实例? 【解析】 这道题实际上考查的是如何消除多重继承引起的向上继承二义性问题。程序代码如下所示。 1 class A {}; 2 class B : public A {}; 3 class C : public A {}; 4 class D : public B, public C {}; 5


2016-04-14
原文:6 int main() 7 { 8  D d; 9  A *pd = &d; //编译错误 10  return 0; 11 } 由于B、C继承自A,B、C都拥有A的一份拷贝,类D多重继承自B、C,因此拥有A的两份拷贝。如果此时一个类A的指针指向一个类D的实例,会出现“模糊的转换”之类的编译错误。解决办法如下。 1 class A {}; 2 class B : virtual public A {};  //B虚拟继承自A 3 class C : virtual public A {};  //C虚拟继承自A 4 class D : public B, public C {}; 5


2016-04-14
原文:6 int main() 7 { 8  D d; 9  A *pd = &d;     //成功转换 10  return 0; 11 } 将B、C都改为虚拟继承自A,则类D多重继承自B、C时,就不会重复拥有A的拷贝了,因此也就不会出现转换错误了。 【答案】 把B、C都改为虚拟继承自A,消除继承的二义性。


2016-04-14
原文:现在来总结一下,多继承中的构造函数顺序如下: (1)任何虚拟基类的构造函数按照它们被继承的顺序构造。 (2)任何非虚拟基类的构造函数按照它们被构造的顺序构造。 (3)任何成员对象的构造按照它们声明的顺序调用。 (4)类自身的构造函数。


2016-04-16
原文:纯虚函数在基类中是没有定义的,必须在子类中加以实现,很像Java中的接口函数。如果基类含有一个或多个纯虚函数,那么它就属于抽象基类,不能被实例化。 为什么要引入抽象基类和纯虚函数呢?原因有以下两点: (1)为了方便使用多态特性。 (2)在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、狮子等子类,但动物本身生成对象明显不合常理。抽象基类不能够被实例


2016-04-16
原文:化,它定义的纯虚函数相当于接口,能把派生类的共同行为提取出来。


2016-04-16
原文:虚函数和纯虚函数有以下方面的区别。 (1)类里如果声明了虚函数,这个函数是实现的,哪怕是空实现,它的作用就是为了能让这个函数在它的子类里面可以被覆盖,这样编译器就可以使用后期绑定来达到多态了。纯虚函数只是一个接口,是个函数的声明而已,它要留到子类里去实现。 (2)虚函数在子类里面也可以不重载;但纯虚函数必须在子类去实现,这就像Java的接口一样。通常把很多函数加上 virtual,是一个好的习惯,虽然牺牲了一些性能,但是增加了面向对象的多态性,因为很难预料到父类里面的这个


2016-04-16
原文:函数不在子类里面不去修改它的实现。 (3)虚函数的类用于“实作继承”,也就是说继承接口的同时也继承了父类的实现。当然,大家也可以完成自己的实现。纯虚函数的类用于“介面继承”,即纯虚函数关注的是接口的统一性,实现由子类完成。 (4)带纯虚函数的类叫虚基类,这种基类不能直接生成对象,而只有被继承,并重写其虚函数后,才能使用。这样的类也叫抽象类。


2016-04-18
原文:COM(Component Object Mode)即组件对象模型,是组件之间相互接口的规范。其作用是使各种软件构件和应用软件能够用一种统一的标准方式进行交互。COM不是一种面向对象的语言,而是一种与源代码无关的二进制标准。ActiveX 是 Microsoft 提出的一套基于 COM 的构件技术标准,实际上是对象嵌入与链接(OLE)的新版本。基于分布式环境下的 COM 被称作 DCOM(Distribute COM,分布式组件对象模型),它实现了 COM 对象与远程计算机上的另一个对象之间直接进行交互。DCOM 规范定义了分散对象创建和对象间通信的机制,DCOM是ActiveX的基础,因为ActiveX主要是针对Internet应用开发(相比OLE)的技术,当然也可以用于普通的桌面应用程序。


2016-04-18
原文:DLL HELL主要是指DLL(动态链接库)版本冲突的问题。一般情况下,DLL 新版本会覆盖旧版本,那么原来使用旧版本的DLL的应用程序就不能继续正常工作了。扩展知识:虚函数表大家知道,虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。在这个表中,主要是一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,其内容真实反映实际的函数。这样,在有虚函数的类的实例中,这个表被分配在了这个实例的内存中,所以,当用父类的指针来操作一个子类的时候,这张虚函数表就显得尤为重要了。它就像一个地图一样,指明了实际所应该调用的函数。C++的标准规格说明书中说到,编译器必须保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证正确取到虚函数的偏移量)。这意味着通过对象实例的地址得到这张虚函数表,然后就可以遍历其中的函数指针,并调用相


2016-04-18
原文:应的函数。请看下面的程序例子。1 #include <iostream>2 using namespace std;34 class Base5 {6 public:7  virtual void fun1() {cout << "Base::fun1" << endl;}8  virtual void fun2() {cout << "Base::fun2" << endl;}9  virtual void fun3() {cout << "Base::fun3" << endl;}10 private:11  int num1;12  int num2;13 };1415 typedef void (*Fun)(void);1617 int main()18 {19  Base b;20  Fun pFun;2122  pFun = (Fun)*((int*)*(int*)(&b)+0); //


2016-04-18
原文:取得Base::fun1()地址23  pFun();      //执行Base::fun1()24  pFun = (Fun)*((int*)*(int*)(&b)+1); //取得Base::fun1()地址25  pFun();      //执行Base::fun2()26  pFun = (Fun)*((int*)*(int*)(&b)+2); //取得Base::fun1()地址27  pFun();      //执行Base::fun3()2829  return 0;30 }上面程序的执行结果如下。1 Base::fun12 Base::fun23 Base::fun3可以看到,通过函数指针pFun的调用,分别执行了对象b的三个虚函数。通过这个示例发现,可以通过强行把&b 转成int *,取得虚函数表的地址,然后再次取址就可以得到第一个虚函数的地址了,也就是Base::fun1()。如果要调用Base::fun2()和Base::fun3(),只需要把&b先加上数组元素的偏移,后面的步骤类似就可以了。程序中的Base对象b内存结构图如图7.3所示。


2016-04-18
原文:一个类会有多少张虚函数表呢?对于一个单继承的类,如果它有虚拟函数,则只有一张虚函数表。对于多重继承的类,它可能有多张虚函数表。考虑下面代码中的各个类的定义。1 #include <iostream>2 using namespace std;34 class Base15 {


2016-04-18
原文:6 public:7  Base1(int num) : num_1(num) {}8  virtual void foo1() {cout << "Base1::foo1 " << num_1 << endl;}9  virtual void foo2() {cout << "Base1::foo2 " << num_1 << endl;}10  virtual void foo3() {cout << "Base1::foo3 " << num_1 << endl;}11 private:12  int num_1;13 };1415 class Base216 {17 public:18  Base2(int num) : num_2(num) {}19  virtual void foo1() {cout << "Base2::foo1 " <<num_2 << endl;}20  virtual void foo2() {cout << "Base2::foo2 " <<num_2 << endl;}21  virtual void foo3() {cout << "Base2::foo3 " <<num_2 << endl;}22 private:23  int num_2;


2016-04-18
原文:24 };2526 class Base327 {28 public:29  Base3(int num) : num_3(num) {}30  virtual void foo1() {cout << "Base3::foo1 " << num_3 << endl;}31  virtual void foo2() {cout << "Base3::foo2 " << num_3 << endl;}32  virtual void foo3() {cout << "Base3::foo3 " << num_3 << endl;}33 private:34  int num_3;35 };3637 class Derived1 : public Base138 {39 public:40  Derived1(int num) : Base1(num) {}41  virtual void faa1() {cout << "Derived1::faa1" << endl;} //无覆盖42  virtual void faa2() {cout << "Derived1::faa2" << endl;}


2016-04-18
原文:43 };4445 class Derived2 : public Base146 {47 public:48  Derived2(int num) : Base1(num) {}49  virtual void foo2() {cout << "Derived2::foo2" << endl;} //只覆盖了Base1::foo250  virtual void fbb2() {cout << "Derived2::fbb2" << endl;}51  virtual void fbb3() {cout << "Derived2::fbb3" << endl;}52 };5354 class Derived3 : public Base1, public Base2, public Base3 //多重继承,无覆盖55 {56 public:57  Derived3(int num_1, int num_2, int num_3) :58    Base1(num_1), Base2(num_2), Base3(num_3) {}


2016-04-18
原文:59  virtual void fcc1() {cout << "Derived3::fcc1" << endl;}60  virtual void fcc2() {cout << "Derived3::fcc2" << endl;}61 };6263 class Derived4 : public Base1, public Base2, public Base3  //多重继承,有覆盖64 {65 public:66  Derived4(int num_1, int num_2, int num_3) :67    Base1(num_1), Base2(num_2), Base3(num_3) {}68  virtual void foo1() {cout << "Derived4::foo1" << endl;} //覆盖了Base1::foo1,69               //Base2::foo1, Base3::foo170  virtual void fdd() {cout << "Derived4::frr" << endl;}71 };这个例子说明了4种继承情况下的虚函数表。1.一般继承(无虚函数覆盖)


2016-04-20
原文:Derived1类继承自Base1类,Derived1的虚函数表如图7.4所示。Derived1类内没有任何覆盖基类Base1的函数,因此两个虚拟函数faa1()和faa2()被依次添加到了虚函数表的末尾。2.一般继承(有虚函数覆盖)Derived2类继承自Base1类,并对Base1类中的虚函数foo2()进行了覆盖。Derived2的虚函数表如图7.5所示。Derived2 覆盖了基类 Base1 的 faa1() ,因此其虚函数表中 Derived2::foo2()替换了Base1::foo2()一项,fbb2()和fbb3()被依次添加到了虚函数表


2016-04-20
原文:末尾。图7.4 Derived1虚函数表图 图7.5 Derived2虚函数表图 3.多重继承(无虚函数覆盖)Derived3类继承自Base1类、Base2类、Base3类,Derived3的虚函数表如图7.6所示。


2016-04-20
原文:每个父类都有自己的虚表,Derived3 也就有了 3 个虚表,这里父类虚表的顺序与声明继承父类的顺序一致。这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。例如1 Base2 *pBase2 = new Derived3();2 pBase2->foo2(); //


2016-04-20
原文:用Base2::foo2()把Base2类型的指针指向Derived3实例,那么调用将是对应Base2虚表里的那些函数。4.多重继承(有虚函数覆盖)Derived4类继承自Base1类、Base2类、Base3类,并对Base1类的foo1()、Base2类的foo1()、Base3类的foo1()都进行了覆盖。Derived4的虚函数表如图7.7所示。可以看见,Base1::foo1()、Base2::foo1()和Base3::foo1()都被替换成了Derived::foo1()。这样,我们就可以把任意一个静态类型的父类指向子类,并调用子类的f()了。如


2016-04-20
原文:1 Base1*pBase1 = new Derived4();2 pBase1->foo1();   //调用从Base1继承的虚表中的Derived4::foo1()下面是我们所讨论的四种继承情况下的测试代码。图7.7 Derived4虚函数表图 1 int main()2 {


2016-04-20
原文:3  Base1 *pBase1 = NULL;4  Base2 *pBase2 = NULL;5  Base3 *pBase3 = NULL;67  cout << "----- 一般继承自Base1,无覆盖 ----------" << endl;8  Derived1 d1(1);  //Derived1一般继承自Base1,无覆盖9  pBase1 = &d1;10  pBase1->foo1();  //执行Base1::foo1();


2016-04-20
原文:12  cout << "----- 一般继承自Base1,覆盖foo2() ---------" << endl;13  Derived2 d2(2);  //Derived2一般继承自Base1,覆盖了Base1::foo2()14  pBase1 = &d2;15  pBase1->foo2();  //执行Derived2::foo2();1617  cout << "----- 多重继承,无覆盖 ----------" << endl;18  Derived3 d3(1, 2, 3); //Derived3多重继承自Base1,Base2,Base3,没有覆盖


2016-04-20
原文:19  pBase1 = &d3;20  pBase2 = &d3;21  pBase3 = &d3;22  pBase1->foo1();  //执行Base1::foo1();23  pBase2->foo1();  //执行Base2::foo1();24  pBase3->foo1();  //执行Base3::foo1();2526  cout << "------ 多重继承,覆盖foo1() ---------" << endl;27  Derived4 d4(1, 2, 3); //Derived4多重继承自Base1,Base2,Base3,覆


2016-04-20
原文:oo1()28  pBase1 = &d4;29  pBase2 = &d4;30  pBase3 = &d4;31  pBase1->foo1();  //执行Derived4::foo1();32  pBase2->foo1();  //执行Derived4::foo1();33  pBase3->foo1();  //执行Derived4::foo1();34  return 0;35 }测试结果如下。1 一般继承自Base1,无覆盖 ----------


2016-04-20
原文:2 Base1::foo1 13 一般继承自Base1,覆盖foo2()4 Derived2::foo25 ----- 多重继承,无覆盖 ----------6 Base1::foo1 17 Base2::foo1 28 Base3::foo3 39 ------ 多重继承,覆盖foo1() ---------10 Derived4::foo111 Derived4::foo112 Derived4::foo1


2016-04-20
原文:单链表的结构是数据结构中最简单的,它的每一个节点只有一个指向后一个节点的指针,其模型如图8.1所示。图8.1 单链表模型 循环链表与单链表一样,是一种链式的存储结构;不同的是,循环链表的最后一个节点的指针指向该循环链表的第一个节点或者表头节点,从而构成一个环形的链。其结构模型如图8.2所示。图8.2 循环链表模型 当对单链表进行操作时,有时你要对某个结点的直接前驱进行操作,又必须从表头开始查找。


2016-04-20
原文:在双向链表中,结点除含有数据域外,还有两个指针,一个存储直接后继结点地址,另一个存储直接前驱结点地址。


2016-04-20
原文:双向循环链表其实就是把双向链表的首尾相连,


2016-04-29
原文:队列与栈是两种不同的数据结构。它们有以下区别。(1)操作的名称不同。队列的插入称为入队,队列的删除称为出队。栈的插入称为进栈,栈的删除称为出栈。(2)可操作的方向不同。队列是在队尾入队,队头出队,即两边都可操作。而栈的进栈和出栈都是在栈顶进行的,无法对栈底直接进行操作。(3)操作的方法不同。队列是先进先出(FIFO),即队列的修改是依先进先出的原则进行的。新来的成员总是加入队尾(不能中间插入),每次离开的成员总是队列头上的(不允许中途离队)。而栈为后进先出(LIFO),即每次删除(出栈)的总是当前栈中“最新的”元素,即最后插入(进栈)的元素,而最先插入的被放在栈的底部,要到最后才能删除。


2016-04-29
原文:如何只使用stack实现queue呢?我们知道stack是先进后出的(FILO),而queue是先进先出的(FIFO)。也就是说,stack 进行了一次反向。如果进行两次反向,就能实现queue的功能,所以我们需要两个stack实现queue。下面是具体的思路。假设有两个栈A和B,且都为空。可以认为栈A为提供入队列的功能,栈B提供出队列的功能。(1)如果栈B不为空,直接弹出栈B的数据。(2)如果栈B为空,则依次弹出栈A的数据,放入栈B中,再弹出栈B的数据。


2016-05-03
原文: //节点类定义2 class Node3 {4 public:5  int data;         //数据6  Node *parent;         //父亲节点7  Node *left;         //左子节点8  Node *right;         //右子节点9 public:10  Node() : data(-1), parent(NULL), left(NULL), right(NULL) { };11  Node(int num) : data(num), parent(NULL), left(NULL), right(NULL) { };


2016-05-03
原文:12 };1314 //二叉排序树类定义15 class Tree16 {17 public:18  Tree(int num[], int len);      //插入num数组的前len个数据19  void insertNode1(int data);     //插入节点,递归方法20  void insertNode(int data);     //插入节点,非递归方法21  Node *searchNode(int data);     //查找节点22  void deleteNode(int data);     //删除节点及其子树23 private:24  void insertNode(Node* current,int data);  //递归插入方法25  Node *searchNode(Node* current,int data);  //递归查找方法26  void deleteNode(Node* current);    //递归删除方法27 private:28  Node* root;    


2016-05-03
原文://二叉排序树的根节点29 };


2016-05-03
原文: //插入num数组的前len个数据2 Tree::Tree(int num[], int len)3 {4  root = new Node(num[0]);  //建立root节点5  //把数组中的其他数组插入到二叉排序树中6  for(int i=1; i < len; i++)7  {8   //insertNode(num[i]);9   insertNode1(num[i]);10  }11 };


2016-05-03
原文:nsertNode1()使用非递归方法插入节点,其代码如下。1 //插入数据为参数data的节点,非递归方法2 void Tree::insertNode1(int data)    //插入节点3 {4  Node *p, *par;5  Node *newNode = new Node(data);   //创建节点67  p = par = root;8  while(p != NULL)       //查找插入在哪个节点下面9  {10   par = p; //保存节点11   if (data > p->data)     //如果data大于当前节点的data12     p = p->right;     //下一步到左子节点,否则进行到右子节点13   else if(data < p->data)14     p = p->left;15   else if (data ==


2016-05-03
原文:p->data)   //不能插入重复数据16   {17    delete newNode;18    return;19   }20  }21  newNode->parent = par;22  if (par->data > newNode->data)   //把新节点插入在目标节点的正确位置23   par->left = newNode;24  else


2016-05-03
原文:25   par->right = newNode;26 }


2016-05-03
原文:insertNode()使用非递归的方法插入节点,它的内部调用了private函数insertNode()。代码如下。1 //插入数据为参数data的节点,调用递归插入方法2 void Tree::insertNode(int data)3 {4  if(root != NULL)5  {6   insertNode(root, data);     //调用递归插入方法


2016-05-03
原文:7  }8 }910 //递归插入方法11 void Tree::insertNode(Node* current,int data)12 {13  //如果data小于当前节点数据,则在当前节点的左子树插入14  if(data < current->data)15  {16   if(current->left == NULL)    //如果左节点不存在,则插入到左节点


2016-05-03
原文:17   {18     current->left = new Node(data);19     current->left->parent = current;20   }21   else22     insertNode(current->left,data); //否则对左节点进行递归调用23  }2425  //如果data大于当前节点数据,则在当前节点的右子树插入26  else if(data > cur


2016-05-03
原文:ent->data)27  {28   if(current->right == NULL)    //如果右节点不存在,则插入到右节点29   {30     current->right = new Node(data);31     current->right->parent = current;32   }33   else34     insertNode(current->right,data); //否则对右节点进行递归调用35  }


2016-05-03
原文:3637  return;          //data等于当前节点数据时,不插入38 };


2016-05-03
原文:比较这两个版本的插入函数可以看出,使用递归方法写出的程序简单,容易理解;而非递归方法使用循环,与递归方法的程序结构相比显得臃肿。递归时需要不断地进行函数入栈、出栈操作,因此效率方面较低;并且如果递归的深度较大,可能会引发栈溢出。


2016-05-03
原文:查找节点也使用了递归,由于与插入节点类似,这里只列出private方法。1 //递归查找方法2 Node* Tree::searchNode(Node* current,int data)3 {4  //如果data小于当前节点数据,则递归搜索其左子树5  if(data < current->data)6  {7   if (current->left == NULL)   //如果不存在左子树,则返回NULL8     return NULL;9   return searchNode(current->left, data);10  }1112  //如果data大于当前节点数据,则递归搜索其左子树


2016-05-03
原文:13  else if(data > current->data)14  {15   if (current->right == NULL)  //如果不存在右子树,则返回NULL16     return NULL;17   return searchNode(current->right, data);18  }1920  //如果相等,则返回current21  return current;22 }


2016-05-03
原文:searchNode的步骤如下。


2016-05-03
原文:(1)如果data小于当前节点(current)的值,且current的左子树存在,则继续搜索current的左子树,否则返回NULL。(2)如果data大于当前节点(current)的值,且current的右子树存在,则继续搜索current的左子树,否则返回NULL。(3)如果data等于当前节点(current)的值,则返回current。3.删除节点删除节点的操作代码如下。1 //删除数据为data的节点及其子树2 void Tree::deleteNode(int data)


2016-05-03
原文:3 {4  Node *current = NULL;56  current = searchNode(data);  //查找节点7  if (current != NULL)8  {9   deleteNode(current);  //删除节点及其子树10  }11 }1213 //删除current节点及其子树的所有节点


2016-05-03
原文:14 void Tree::deleteNode(Node *current)15 {16  if (current->left != NULL)  //删除左子树17   deleteNode(current->left);18  if (current->right != NULL)  //删除右子树19   deleteNode(current->right);2021  if (current->parent == NULL)22  {23   //如果current是根


2016-05-03
原文:节点,把root置空24   delete current;25   root = NULL;26   return;27  }2829  //将current父亲节点的相应指针置空30  if (current->parent->data > current->data) //current为其父节点的左子节点31    current->parent->left = NULL;32  else //current为parNode的右子节点33   current->parent


2016-05-03
原文:>right = NULL;3435  //最后删此节点36  delete current;37 }


2016-05-03
原文:中序遍历的递归算法定义为,若二叉树非空,则依次执行如下操作。(1)遍历左子树;(2)访问根节点;(3)遍历右子树。中序遍历的递归算法程序代码


2016-05-03
原文:如下。1 //中序遍历2 void Tree::InOrderTree()3 {4  if(root == NULL)5    return;6  InOrderTree(root);7 }89 void Tree::InOrderTree(Node* current)10 {11  if(current!= NULL)12  {


2016-05-03
原文:13   InOrderTree(current->left);  //遍历左子树14   cout << current->data << " ";  //打印节点数据15   InOrderTree(current->right);  //遍历右子树16  }17 }


2016-05-03
原文:3)再中序遍历右子树。代码如下。1 void Tree::InOrderTreeUnRec()2 {3  stack<Node *> s;4  Node *p = root;5  while(p != NULL || !s.empty())6  {7   while(p != NULL)    //遍历左子树8   {9     s.push(p);    //把遍历的节点全部压栈10    p = p->left;11   }1213   if (!s.empty())14   {15     p = s.top();   //得到栈顶内容16     s.pop();    //出栈17     cout << p->data << " "; //打印18     p = p->right;   //指向右子节点,下一次循环时就会中序遍历右子树19   }


2016-05-03
原文:20  }21 }main函数测试如下。1 int main(void)2 {3  int num[] = {5, 3 ,7, 2, 4, 6, 8, 1};4  Tree tree(num, 8);5  cout << "InOrder: ";6  tree.InOrderTree();    //中序遍历,递归方法7  cout << "\nInOrder: ";8  tree.InOrderTreeUnRec();  //中序遍历,非递归方法


2016-05-03
原文:先序遍历的递归算法定义为,若二叉树非空,则依次执行如下操作。(1)访问根结点;(2)遍历左子树;(3)遍历右子树。先序遍历的程序代码如下。1 //先序遍历2 void Tree::PreOrderTree()3 {4  if(root == NULL)5    return;6  PreOrderTree(root);7 };89 void Tree::PreOrderTree(Node* current)10 {11  if(current!= NULL)12  {


2016-05-03
原文:13   cout << current->data << " ";  //打印节点数据14   PreOrderTree(current->left);  //遍历左子树15   PreOrderTree(current->right);  //遍历右子树16  }17  }对于先序遍历非递归算法,这里我们使用一个栈(stack)来临时存储节点,方法如下。(1)打印根节点数据。(2)把根节点的right入栈,遍历左子树。


2016-05-03
原文:(3)遍历完左子树返回时,栈顶元素应为right,出栈,遍历以该指针为根的子树。程序如下1 void Tree::PreOrderTreeUnRec()2 {3  stack<Node *> s;4  Node *p = root;5  while(p != NULL || !s.empty())6  {7   while(p != NULL)    //遍历左子树8   {9     cout << p->data << " "; //打印10     s.push(p);    //把遍历的节点全部压栈11     p = p->left;12   }1314   if (!s.empty())15   {16     p = s.top();   //得到栈顶内容17     s.pop();    //出栈18     p = p->right;   //指向右子节点,下一次循环时就会先序遍历左子树19   }


2016-05-03
原文:20  }21 }main函数测试如下。1 int main(void)2 {3  int num[] = {5, 3 ,7, 2, 4, 6, 8, 1};4  Tree tree(num, 8);5  cout << "PreOrder: ";6  tree.PreOrderTree();   //先序遍历,递归方法7  cout << "\nPreOrder: ";8  tree.PreOrderTreeUnRec();  //先序遍历,非递归方法9  return 0;10 }测试结果为:1 5 3 2 1 4 7 6 82 5 3 2 1 4 7 6 8


2016-05-03
原文:后序遍历的递归算法定义为,若二叉树非空,则依次执行如下操作。(1)遍历左子树;(2)遍历右子树;(3)访问根结点。


2016-05-03
原文:2 void Tree::PostOrderTree()3 {4  if(root == NULL)5    return;6  PostOrderTree(root);7 }89 void Tree::PostOrderTree(Node* current)10 {11  if(current!= NULL)12  {


2016-05-03
原文:13   PostOrderTree(current->left);  //遍历左子树14   PostOrderTree(current->right);  //遍历右子树15   cout << current->data << " ";  //打印节点数据16  }17 }


2016-05-03
原文:现在说明对于后序遍历的非递归方法。假设 T 是要遍历树的根指针,后序遍历要求在遍历完左、右子树后再访问根。需要判断根结点的左、右子树是否均遍历过。可采用标记法,结点入栈时,配一个标志tag一同入栈(tag为0表示遍历左子树前的现场保护,tag为1表示遍历右子树前的现场保护)。首先将T和tag(为0)入栈,遍历左子树;返回后,修改栈顶tag为1,遍历右子树;最后访问根结点。代码如下。1 void Tree::PostOrderTreeUnRec()2 {3  stack<Node *> s;4  Node *p = root;56  while(p != NULL || !s.empty())7  {


2016-05-03
原文:   while(p != NULL)9   {10    s.push(p);    //压栈11    p = p->left;    //遍历左子树12   }1314   if (!s.empty())15   {16     p = s.top();   //得到栈顶元素17     if (p->tag)    //tag为1时18     {19       cout << p->data << " "; //打印节点数据20       s.pop();   //出栈21       p = NULL;  //第二次访问标志其右子树已经遍历22    }23    else24    {25       p->tag = 1;  //修改tag为126       p = p->right; //指向右节点,下次遍历其左子树27    }


2016-05-03
原文:28   }29  }30 }根据上面的分析,代码中Node类添加了一个 int型的成员变量 tag。main函数测试如下。1 int main(void)2 {3  int num[] = {5, 3 ,7, 2, 4, 6, 8, 1};4  Tree tree(num, 8);5  cout << "PostOrder: ";6  tree.PostOrderTree();   //后序遍历,递归方法7  cout << "\nPostOrder: ";8  tree.PostOrderTreeUnRec();  //后序遍历,非递归方法9  return 0;10 }测试结果为:1 1 2 4 3 6 8 7 52 1 2 4 3 6 8 7 5


2016-05-04
原文:层次遍历就是一层一层地进行遍历。比如图8.6共有4层,各层分别为第1层:5第2层:3、7第3层:2、4、6、8第4层:1


2016-05-04
原文:本题很难直接用节点的指针(left、right、parent)来实现,但是借助一个队列就可以轻松地实现,例如图8.7所示的二叉树结构。可以按照下面的方式执行。(1)A入队CX。(2)A出队,同时A的子节点B、C入队(此时队列有B、C)。(3)B出队,同时B的子节点D、E入队(此时队列有C、D、E)。(4)C出队,同时C的子节点F、G入队(此时队列有D、E、F、G)。显然,这样就能使出队的顺


2016-05-04
原文:足层次遍历。图8.7 二叉树图 这里为了方便,我们选择使用C++标准库中的queue模板类。代码如下。1 //层次遍历2 void Tree::LevelOrderTree()3 {4  queue<Node *> q;     //定义队列q,它的元素为Node *类型指针


2016-05-04
原文:5  Node *ptr = NULL;67  q.push(root);      //根节点入队8  while(!q.empty())9  {10   ptr = q.front();    //得到队头节点11   q.pop();    //出队12   cout << ptr->data << " ";  //当前节点存在左节点,则左节点入队13   {14     q.push(ptr->left);


2016-05-04
原文:15   }16   if (ptr->right != NULL)  //当前节点存在右节点,则右节点入队17   {18     q.push(ptr->right);19   }20  }21 }


2016-05-04
原文:排序可以分为下面5类。插入排序;选择排序;交换排序;归并排序;分配排序。


2016-05-04
原文:还有一种改进的方法,即查找比较操作和记录移动操作交替地进行。其具体做法如下。将待插入记录R[i]的关键字从右向左依次与有序区中记录R[j](j=i-1,i-2,…,1)的关键字进行比较:(1)如果R[j]的关键字大于R[i]的关键字,则将R[j]后移一个位置;(2)如果R[j]的关键字小于或等于R[i]的关键字,则查找过程结束,j+1即为R[i]的插入位置。关键字比R[i]的关键字大的记录均已后移,所以j+1的位置已经腾空,只要将R[i]直接插入此位置即可完成一趟直接插入排序。


2016-05-04
原文:希尔(Shell)排序算法先将要排序的一组数按某个增量 d 分成若干组,每组中记录的下标相差 d 对每组中全部元素进行排序,然后用一个较小的增量对它进行再次分组,并对每个新组重新进行排序。当增量减到 1 时,整个要排序的数被分成一组,排序完成。因此希尔排序实质上是一种分组插入方法。希尔排序的时间性能优于直接插入排序,其原因如下。当数组初始状态基本有序时,直接插入排序所需的比较和移动次数均较少。当n 值较小时,n 和n2 的差别也较小,即直接插入排序的最好时间复杂度O(n)和最坏时间复杂度0(n2)差别不大。在希尔排序开始时,增量较大,分组较多,每组的记录数目少,故各组内直接插入较快,后来增量 d 逐渐缩小,分组数逐渐减少,而各组的记录数目逐渐增多。但由于已经按d-1作为距离排过序,数组较接近于有序状态,所以新的一趟排序过程也较快。因此,希尔排序在效率上较直接插入排序有较大的改进。


2016-05-04
原文:另外,由于分组的存在,相等的元素可能会分在不同组,导致它们的次序可能发生变化,因此希尔排序是不稳定的。


2016-05-04
原文:2.实现我们可以这样来设置增量:初始时取序列的一半为增量,以后每次减半,直到增量为1。代码如下。1 #include <iostream>2 using namespace std;34 void shell_sort(int a[], int len)5 {6  int h, i, j, temp;78  for (h=len/2; h>0;


2016-05-04
原文:h=h/2)    //控制增量9  {10   for (i=h; i<len; i++)    //这个for循环就是前面的直接插入排序11   {12     temp = a[i];13     for (j=i-h; (j>=0 && temp<a[j]); j-=h)14     {15       a[j+h] = a[j];16     }17     a[j+h] = temp;18   }


2016-05-04
原文:19  }20 }2122 void print_array(int a[], int len)23 {24  for(int i = 0; i < len; i++)    //循环打印数组的每个元素25  {26    cout << a[i] << " ";27  }28  cout << endl;29 }


2016-05-04
原文:2.实现我们使用上面介绍的改进的方法,即查找比较操作和记录移动操作交替地进行。代码如下。1 #include <iostream>2 using namespace std;34 //直接插入排序5 void insert_sort(int a[], int n)6 {7  int i, j, temp;89  for (i=1; i<n; i++) //需要选择n-1次10  {11    //暂存下标为i的数。下标从1开始,因为开始时12    //下标为0的数,前面没有任何数,此时认为它是排好顺序的13    temp = a[i];14    for (j=i-1; j>=0 && temp<a[j]; j--)15    {16     //如果满足条件就往后挪。最坏的情况就是temp比a[0]小,它要放在最前面


2016-05-04
原文:17     a[j+1] = a[j];18   }1920   a[j+1] = temp;    //找到下标为i的数的放置位置21  }22 }2324 void print_array(int a[], int len)25 {26  for(int i = 0; i < len; i++)  //循环打印数组的每个元素27  {


2016-05-04
原文:28   cout << a[i] << " ";29  }30  cout << endl;31 }3233 int main()34 {35  int a[] = {7, 3, 5, 8, 9, 1, 2, 4, 6};36  cout << "before insert sort: ";37  print_array(a, 9);38  insert_sort(a, 9);     //进行直接插入排序


2016-05-04
原文:39  cout << "after insert sort: "40  print_array(a, 9);41  return 0;42 }


2016-05-04
原文:.冒泡排序冒泡排序的方法为:将被排序的记录数组A[1...n]垂直排列,每个记录A[i]看作重量为A[i]气泡。根据轻气泡不能在重气泡之下的原则,从下往上扫描数组A:凡扫描到违反本原则的轻气泡,就使其向上“飘浮”。如此反复进行,直到最后任何两个气泡都是轻者在上、重者在下为止。冒泡排序是稳定的排序。下面是具体的算法。(1)初始状态下,A[1…n]为无序区。(2)第一趟扫描:从无序区底部向上依次比较相邻的两个气泡的重量,若发现轻者在下、重者在上,则交换二者的位置。即依次比较(A[n],A[n-1]),(A[n-1],A[n-2]),…,&nbsp;(A[2],A[1]);对于每对气泡(A[j+1],A[j]),若A[j+1]<A[j],则交换A[j+1]和A[j]的内容。第一趟扫描完毕时,“最轻”的气泡就飘浮到该区间的顶部,即关键字最小的记录被放在最高位置


2016-05-04
原文:[1]上。(3)第二趟扫描:扫描A[2…n]。扫描完毕时,“次轻”的气泡飘浮到A[2]的位置上。(4)第i趟扫描:A[1...i-1]和A[i…n]分别为当前的有序区和无序区。扫描仍是从无序区底部向上,直至该区顶部。扫描完毕时,该区中最轻气泡飘浮到顶部位置A[i]上,结果是A[1...i]变为新的有序区。最后,经过n-1趟扫描可得到有序区A[1...n]。2.实现根据前面冒泡扫描的方法,可以写出下面的排序代码。1 void bubble_sort_1(int a[], int len)2 {3  int i = 0;4  int j = 0;5  int temp = 0;       //用于交换67  for(i=0; i<len-1; i++)     //进行n-1趟扫描8  {9    for(j=len-1; j>=i; j--)   //从后往前交换,这样最小值冒泡到开头部分10   {11     if(a[j+1] < a[j])   //如果a[j]小于a[j-1],


2016-05-04
原文:则交换两元素的值12     {13      temp = a[j];14      a[j] = a[j+1];15      a[j+1] = temp;16     }17    }18  }19 }这个代码有一个小问题,就是假如进行第i次扫描前,数组已经排好序了,但是它还会进行下一次的扫描,显然以后的扫描都是没有必要的。


2016-05-04
原文:我们可以对上面的这个代码进行一点改进,代码如下。1 void bubble_sort_2(int a[], int len)2 {3  int i = 0;4  int j = 0;5  int temp = 0;      //用于交换6  int exchange = 0;     //用于记录每次扫描时是否发生交换78  for(i=0; i<len-1; i++)    //进行n-1趟扫描9  {


2016-05-04
原文:10   exchange = 0;     //每趟扫描之前对exchange置011   for(j=len-1; j>=i; j--)  //从后往前交换,这样最小值冒泡到开头部分12   {13     if(a[j+1] < a[j])  //如果a[j]小于a[j-1],交换两元素的值14     {15      temp = a[j];16      a[j] = a[j+1];17      a[j+1] = temp;18      exchange = 1;  //发生交换,exchange置119     }20   }21   if (exchange != 1)   //此趟扫描没有发生过交换,说明已经是排序的22     return;     //不需要进行下次扫描23  }24 }这里我们使用一个局部变量exchange来记录在本次扫描时有没有进行过数据交换。每次扫描之前,把exchange置0(代码第10行)。如果扫描时发生数据交换,则把exchange置1(代码


2016-05-04
原文:第18行);如果没有,则说明数组已经是排序的了,不需要进行下一趟扫描(代码第22行)。对两种冒泡排序的测试main函数如下。1 int main()2 {3  int a[] = {7, 3, 5, 8, 9, 1, 2, 4, 6};4  cout << "before bubble sort: ";5  print_array(a, 9);6  //bubble_sort_1(a, 9);    //冒泡排序7  bubble_sort_2(a, 9);     //改进的冒泡排序


2016-05-04
原文:8  cout << "after bubble sort: ";9  print_array(a, 9);10  return 0;11 }测试结果如下。1 before bubble sort: 7 3 5 8 9 1 2 4 62 before bubble sort: 1 2 3 4 5 6 7 8 9


2016-05-04
原文:.快速排序快速排序是C.R.A.Hoare于1962年提出的一种划分交换排序。它采用了一种分治的策略,通常称其为分治法(Divide-and-ConquerMethod)。分治法的基本思想是:将原问题分解为若干个规模更小但结构与原问题相似的子问题。递归地解这些子问题,然后将这些子问题的解组合为原问题的解。快速排序的基本思想:设当前待排序的无序区为A[low…high],利用分治法可描述为:(1)分解:在 A[low...high]中任选一个记录作为基准(pivot),以此基准将当前无序区划分为左、右两个较小的子区间A[low…pivotpos-1]和A[pivotpos+1…high],并使左边子区间中所有记录的关键字均小于等于基准记录(pivot),右边的子区间中所有记录的关键字均大于等于pivot,而基准记录pivot则位于正确的位置上,它无须参加后续的排序。注意,划分的关键是要求出基


2016-05-04
原文:准记录所在的位置pivotpos。划分的结果可以简单地表示为(pivot=A[pivotpos]):A[low…pivotpos-1]≤A[pivotpos]≤A[pivotpos+1…high],其中low≤pivotpos≤high。(2)求解:通过递归调用快速排序对左、右子区间 A[low...pivotpos-1]和 A[pivotpos+1...high]快速排序。(3)组合:当“求解”步骤中的两个递归调用结束时,其左、右两个子区间已有序。对快速排序而言,“组合”步骤无须做什么,可看作空操作。2.实现源代码如下。


2016-05-05
原文:1 void quick_sort(int a[], int low, int high)2 {3  int i, j, pivot;4  if (low < high)5  {6    pivot = a[low];7    i = low;8    j = high;9    while(i<j)10   {11     while (i<j && a[j]>=pivot)12      j--;13     if(i<j)14      a[i++]=a[j];   //将比pivot小的元素移到低端1516     while (i<j && a[i]<=pivot)17      i++;18     if(i<j)19      a[j--] = a[i];   //将比pivot大的元素移到高端20   }21   a[i] = pivot;      //pivot移到最终位置22   quick_sort(a, low, i-1);   //对左区间递归排序23   quick_sort(a, i


2016-05-05
原文:+1, high);   //对右区间递归排序24  }25 }


2016-05-05
原文:这里pivot代表基准值,它的初始值为a[low]。局部变量i和j分别代表low和high的位置。接着按照下面的步骤进行一趟交换。(1)把比pivot小的元素移到低端(low)。(2)把比pivot大的元素移到高端(high)。(3)pivot移到最终位置,此时这个位置的左边元素的值都比pivot小,而其右边元素的值都比pivot大。(4)对左、右区间分别进行递归排序。从而把前三步的粗排序逐渐地细化,直至最终low和high交汇。


2016-05-05
原文:1.直接选择排序直接选择排序的基本思想:n个记录的直接选择排序可经过n-1趟直接选择排序得到有序结果。(1)初始状态:无序区为A[1...n],有序区为空。(2)第1 趟排序:在无序区 A[1...n]中选出最小的记录 A[k],将它与无序区的第 1个记录A[1]交换,使A[1…1]和A[2...n]分别变为记录个数增加1的新有序区和记录个数减少1的新无序区。(3)第i趟排序:第i趟排序开始时,当前有序区和无序区分别为A[1…i-1]和A[i..n](1≤i≤n-1)。该趟排序从当前无序区中选出关键字最小的记录 A[k],将它与无序区的第 1个记录A[i]交换,使A[1…i]和A[i+1...n]分别变为记录个数增加1的新有序区和记录个数减少1的新无序区。这样,n个记录的文件的直接选择排序可经过n-1趟直接选择排序得到有序结果。直接选择排序是不稳定的。


2016-05-05
原文:2.实现源代码如下。1 #include <iostream>2 using namespace std;34 void select_sort(int a[], int len)5 {6  int i,j,x,l;78  for(i=0; i<len; i++)    //进行n-1次遍历9  {10   x = a[i];      //每次遍历前x和l的初值设置11   l = i;12   for(j=i; j<len; j++)   //遍历从i位置向数组尾部进行13   {14     if(a[j] < x)15     {16       x = a[j];  //x保存每次遍历搜索到的最小数17       l = j;   //l记录最小数的位置18     }19   }20   a[l] = a[i];     //把最小元素与a[i]进行交换21   a[i] = x;


2016-05-05
原文:22  }23 }2425 void main()26 {27  int data[9] = {54,38,96,23,15,72,60,45,83};28  select_sort(data, 9);    //快速排序29  for(int i = 0; i<9; i++)30    cout << data[i] << " ";  //打印排序后的数组31 }select_sort 函数进行了 n-1 趟排序。局部变量 x 和 l 分别记录每次遍历时所得的最小元素值及所在位置,代码第20~21行利用它们进行与a[i]的交换。以main函数中的data数组为例,说明其具体步骤。(1)第1次排序:数组各元素为54,38,96,23,15,72,60,45,83,此时i为0,遍历整个数组得到最小元素15,然后与a[0]进行交换,结果为15,38,96,23,54,72, 60,45,83。(2)第2次排序:此时i为1,遍历从a[1]开始到数组末尾结束,得到最小元素23,然后与a[1]进行交换,结果为15,23,96,38,54,72,60,45,


2016-05-05
原文:83。(3)第3次排序:此时i为2,遍历从a[2]开始到数组末尾结束,得到最小元素38,然后与a[2]进行交换,结果为15,23,38,96,54,72,60,45,83。显然,每次排序都选出了一个最小的元素,与遍历起始位置的元素进行交换。通过n-1次这样的排序,最终把整个数组进行了排序。执行结果为:1 15 23 38 45 54 60 72 83 96


2016-05-06
原文:.堆排序堆排序定义:n个序列Al,A2,…,An称为堆,有下面两种不同类型的堆。小根堆:所有子结点都大于其父节点,即Ai≤A2i 且Ai≤A2i+1。大根堆:所有子结点都小于其父节点,即Ai≥A2i 且Ai≥A2i+1。若将此序列所存储的向量A[1...n]看为一棵完全二叉树的存储结构,则堆实质上是满足如下性质的完全二叉树:树中任一非叶结点的关键字均不大于(或不小于)其左、右子节点(若存在)的关键字。因此堆排序(HeapSort)是树形选择排序。在排序过程中,将R[l...n]看成一棵完全二叉树的顺序存储结构,利用完全二叉树中双亲结点和孩子结点之间的内在关系,在当前无序区中选择关键字最大(或最小)的记录。用大根堆排序的基本思想:(1)先将初始A[1...n]建成一


2016-05-06
原文:个大根堆,此堆为初始的无序区。(2)再将关键字最大的记录A[1](堆顶)和无序区的最后一个记录A[n]交换,由此得到新的无序区A[1...n-1]和有序区A[n],且满足A[1...n-1]≤A[n]。(3)由于交换后新的根A[1]可能违反堆性质,故应将当前无序区A[1...n-1]调整为堆。然后再次将A[1...n-1]中关键字最大的记录A[1]和该区间的最后一个记录A[n-1]交换,由此得到新的无序区A[1...n-2]和有序区A[n-1...n],且仍满足关系A[1...n-2]≤A[n-1...n],同样要将A[1...n-2]调整为堆。(4)对调整的堆重复进行上面的交换,直到无序区只有一个元素为止。构造初始堆必须用到调整堆的操作,现在说明Heapify函数思想方法。每趟排序开始前,A[l…i]是以A[1]为根的堆,在A[1]与A[i]交换后,新的无序区A[1…i-1]中只有A[1]的值发生了变化,故除A[1]可能违反堆性质外,其余任何结点为根的子树均是堆。因此,当被调整区间是A[low…high]时,只须调整以A[low]为根的树即可。可以使用“筛选法”进行堆的调整。A[low]的左、右子树(若存在)均已是堆,这两棵子树的根A[2low]和 A[2low+1]分别是各自子树中关键字最大的节点。若


2016-05-06
原文:A[low]不小于这两个孩子节点的关键字,则 A[low]未违反堆性质,以 A[low]为根的树已是堆,无须调整;否则必须将A[low]和它的两个孩子节点中关键字较大者进行交换,即A[low]与A[large] (A[large]=max(A[2low],A[2low+1]))交换。交换后又可能使节点 A[large]违反堆性质。同样,由于该节点的两棵子树(若存在)仍然是堆,故可重复上述调整过程,对以A[large]为根的树进行调整。此过程直至当前被调整的节点已满足堆性质,或者该节点已是叶子为止。上述过程就像过筛子一样,把较小的关键字逐层筛下去,而将较大的关键字逐层选上来。2.实现


2016-05-06
原文:源代码如下。1 #include <iostream>2 using namespace std;34 int heapSize = 0;56 //返回左子节点索引7 int Left(int index) { return ((index << 1) + 1);}89 //返回右子节点索引10 int Right(int index) {return ((index << 1) + 2);}1112 //交换a、b的值13 void swap(int *a, int *b) {int temp = *a;*a = *b;*b = temp;}1415 //array[index]与其左、右子树进行递归对比16 //用最大值替换array[index],index表示堆顶索引17 void maxHeapify(int array[], int index)18 {19  int largest = 0;       //最大数20  int left = Left(index);     //左子节点索引21  int right = Right(in


2016-05-06
原文:dex);    //右子节点索引2223  //把largest赋为堆顶与其左子节点的较大者24  if ((left <= heapSize) && (array[left] > array[index]))25    largest = left;26  else27    largest = index;2829  //把largest与堆顶的右子节点比较,取较大者30  if ((right <= heapSize) && (array[right] > array[largest]))31    largest = right;3233  //此时largest为堆顶、左子节点、右子节点中的最大者34  if (largest != index)35  {36    //如果堆顶不是最大者,则交换,并递归调整堆37    swap(&array[index], &array[largest]);38    maxHeapify(array, largest);39  }40 }


2016-05-06
原文:4142 //初始化堆,将数组中的每一个元素置放到适当的位置43 //完成之后,堆顶的元素为数组的最大值44 void buildMaxHeap(int array[], int length)45 {46  int i;47  heapSize = length;      //堆大小赋为数组长度48  for (i = (length >> 1); i >= 0; i--)49    maxHeapify(array, i);50 }5152 void heap_sort (int array[], int length)53 {54  int i;5556  //初始化堆57  buildMaxHeap(array, (length - 1));5859  for (i = (length - 1); i >= 1; i--)60  {61    //堆顶元素array[0](数组的最大值)被置换到数组的尾部array[i]


2016-05-06
原文:62    swap(&array[0], &array[i]);63    heapSize--;      //从堆中移除该元素64    maxHeapify(array, 0);    //重建堆65  }66 }6768 int main(void)69 {70  int a[8] = {45, 68, 20, 39, 88, 97, 46, 59};71  heap_sort (a, 8);72  for(int i=0; i<8; i++)73  {74    cout << a[i] << " ";75  }76  cout << endl;77  return 0;78 }heap_sort函数按下面步骤进行。(1)调用buildMaxHeap对数组进行堆的初始化(代码第57行)。(2)由于堆顶元素(array[0])的值是最大的(大根堆),因此把它与数组尾部进行交换。并把heapSize递减1,即从


2016-05-06
原文:堆中移除数组尾部元素。(3)由于只有剩下的堆顶元素(array[0])不满足堆,因此调用maxHeapify重建堆。(4)对前面两步进行循环调用,直到堆中只含有堆顶,此时heapSize变为1(i为0)。其中buildMaxHeap函数初始化堆时也调用了maxHeapify函数,而maxHeapify使用递归的方法把堆调整为大根堆。测试结果为:1 20 39 45 46 59 68 88 97


2016-05-09
原文:1.归并排序归并排序(Merge Sort)是利用“归并”技术来进行排序。归并是指将若干个已排序的子文件合并成一个有序的文件。两路归并算法的基本思路:设两个有序的子文件(相当于输入堆)放在同一向量中相邻的位置上:A[low…m],a[m+1…high],先将它们合并到一个局部的暂存向量Temp(相当于输出堆)中,待合并完成后将Temp复制回A[low...high]中。归并排序有两种实现方法:自底向上和自顶向下。


2016-05-09
原文:自底向上方法的基本思想:(1)第1趟归并排序时,将待排序的文件A[1...n]看作n个长度为1的有序子文件,将这些子文件两两归并。若n为偶数,则得到n/2个长度为2的有序子文件;若n为奇数,则最后一个子文件不参与归并。故本趟归并完成后,前n/2个有序子文件长度为2,但最后一个子文件长度仍为1。(2)第2趟归并则是将第1趟归并所得到的n/2个有序的子文件两两归并,如此反复,直到最后得到一个长度为n的有序文件为止。(3)上述每次归并操作,均是将两个有序的子文件合并成一个有序的子文件,故称其为“二路归并排序”。类似地,有k(k>2)路归并排序。


2016-05-09
原文:自顶向下算法的设计,形式更为简洁。设归并排序的当前区间是A[low...high],步骤如下。(1)分解:将当前区间一分为二,即求分裂点。(2)求解:递归地对两个子区间A[low...mid]和A[mid+1...high]进行归并排序。(3)组合:将已排序的两个子区间 A[low...mid]和 A[mid+1...high]归并为一个有序的区间R[low…high]。(4)递归的终结条件:子区间长度为1(一个记录自然有序)。


2016-05-09
原文:2.实现归并排序算法可用顺序存储结构,也易于在链表上实现。本题中我们使用数组结构。根据前面介绍过的自顶向下算法步骤,可以实现如下程序。1 #include <iostream>2 using namespace std;34 //将分治的两端按大小次序填入临时数组,最后把临时数组拷贝到原始数组中5 //lPos到rPos-1为一端,rPos到rEnd为另外一端。6 void Merge(int a[], int tmp[], int lPos,int rPos, int rEnd )7 {8  int i, lEnd, NumElements, tmpPos;9  lEnd = rPos - 1;10  tmpPos = lPos;         //从左端开始11  NumElements = rEnd - lPos + 1;     //数组长度12


2016-05-09
原文:13  while( lPos <= lEnd && rPos <= rEnd )14  {15   if( a[lPos] <= a[rPos] )      //比较两端的元素值16     tmp[tmpPos++] = a[lPos++];   //把较小的值先放入tmp临时数组17   else18     tmp[tmpPos++] = a[rPos++];19  }2021  //到这里,左端或右端只能有一端还可能含有剩余元素22  while( lPos <= lEnd )     //把左端剩余的元素放入tmp23   tmp[tmpPos++] = a[lPos++];2425  while( rPos <= rEnd )     //把右端剩余的元素放入tmp26   tmp[tmpPos++] = a[rPos++];2728  for( i = 0; i < NumElements; i++, rEnd--)29    a[rEnd] = tmp[rEnd];    //把临时数组拷贝到原始数组


2016-05-09
原文:30 }3132 void msort(int a[], int tmp[], int low, int high )33 {34  if(low >= high) //结束条件,原子结点return35    return ;3637  int middle = (low + high) / 2;   //计算分裂点38  msort(a, tmp, low, middle);    //对子区间[low,middle]递归做归并排序39  msort(a, tmp, middle+1, high);   //对子区间[middle+1,high]递归做归并排序40  Merge(a, tmp, low, middle+1, high);  //组合,把两个有序区合并为一个有序区41 }4243 void merge_sort( int a[], int len )44 {45  int *tmp = NULL;46  tmp = new int[len];      //分配临时数组空间47  if(tmp != NULL)48  {49    msort(a, tmp, 0, len-1 );   //调用msort归


2016-05-09
原文:并排序50    delete []tmp;      //释放临时数组内存51  }52 }5354 int main()55 {56  int a[8] = {8, 6, 1, 3, 5, 2, 7, 4};57  merge_sort(a, 8);58  return 0;59 }merge_sort 函数是归并的最外层调用,它调用了msort 函数,msort 是归并算法的递归实现。它的步骤与前面介绍的相同,分为三个步骤:(1)代码第37行,计算分裂点,把区间一分为二。(2)代码第38~39行,递归地对两个子区间A[low...middle]和A[middle+1...high]进行归并排序。(3)代码第40行,调用Merge函数合并两个排序后的区间。Merge函数将分治的两端(这两端是已经排好序的)按大小次序填入临时数组,最后把临时数组拷贝到原始数组中。


2016-05-09
原文:.基数排序基数排序是箱排序的改进和推广。箱排序也称桶排序(Bucket Sort),其基本思想是:设置若干个箱子,依次扫描待排序的记录R[0],R[1],…,R[n-1],把关键字等于k的记录全都装入到第k个箱子里(分配),然后按序号依次将各非空的箱子首尾连接起来(收集)。例如,要将一副混洗的52张扑克牌按点数A<2<…<J<Q<K排序,需设置13个“箱子”,排序时依次将每张牌按点数放入相应的箱子里,然后依次将这些箱子首尾相接,就得到了按点数递增顺序排列的一副牌。基数排序是基于多关键字的,什么是多关键字呢?如果文件中任何一个记录 R[i]的关键字都由 d 个分量构成,而且这 d 个分量中每个分量都是一个独立的关键字,则文件是多关键字的(比如扑克牌有两个关键字:点数和花色)。通常实现多关键字排序有两种


2016-05-09
原文:方法:最高位优先(Most Significant Digit first,MSD);最低位优先(Least Significant Digit first,LSD)。基数排序是典型的LSD排序方法,其基本思想是:从低位到高位依次对数据进行箱排序。在d趟箱排序中,所需的箱子数就是基数rd(可能的取值个数),这就是“基数排序”名称的由来。比如,对于值范围为10~99的整数序列:45,13,58,64,29,74,39,18,使用基数排序需要 10 个箱子(从 0~9 标号)进行分配和收集。我们如果把每一个数看成由两个关键字构成(个位数和十位数),那么可以对它们进行两次分配和收集(分别对于个位和十位),具体步骤如下。(1)对序列的各个元素按个位进行顺序装箱,即45 装入5 号箱,13装入3 号箱,58和18装入8号箱,64和74装入4号箱,29和39装入9号箱。(2)从0到9号箱顺序依次收集到原序列,即3号箱的13,4号箱的64和74,5号箱的45,8号箱的58和18,9号箱的29和39被依次收集。序列变为13,64,74,45,58, 18,29,39。(3)对序列的各个元素按十位进行顺序装箱,即13和18装入1号箱,29装入2号箱, 30装入3号箱,45装入4号箱,58


2016-05-09
原文:装入5号箱,64装入6号箱,74装入7号箱。(4)再次从0到9号箱顺序收集到原序列,序列变为13,18,29,30,45,58,64,74。此时完成基数排序。对于一个两位数来说,其十位数当然比个位数关键。因此使用LSD时,先对个位数开始分配和收集。


2016-05-09
原文:2.实现前面我们已经分析过了使用基数排序对整数序列进行排序的具体步骤。因此,如果整数的范围没有指明,则我们需要查找数组最大的元素有多少位数,以便确定需要进行几次分配和收集,还需要知道每一位是什么。比如数据 167,我们不仅需要知道 167 是一个三位数,而且还需要知道它的个位是7,十位是6,百位是1。程序代码如下。1 #include <iostream>2 #include <math.h>3 using namespace std;45 int find_max(int a[], int len)    //查找长度为len的数组的最大元素6 {7  int max = a[0];       //max从a[0]开始8  for(int i=1; i<len; i++)


2016-05-09
原文:9  {10   if( max < a[i] )     //如果发现元素比max大,11     max = a[i];     //就重新给max赋值12  }13  return max;14 }1516 //计算number有多少位17 int digit_number(int number)18 {19  int digit = 0;20  do21  {22    number /= 10;23    digit++;24  } while(number != 0);25  return digit;26 }2728 //返回number上第Kth位的数字29 int kth_digit(int number, int Kth)30 {31  number /=


2016-05-09
原文:ow(10, Kth);32  return number % 10;33 }3435 //对长度为len的数组进行基数排序36 void radix_sort(int a[], int len)37 {38  int *temp[10];       //指针数组,每一个指针表示一个箱子


2016-05-09
原文:39  int count[10] = {0,0,0,0,0,0,0,0,0,0}; //用于存储每个箱子装有多少元素40  int max = find_max(a, len);    //取得序列中的最大整数41  int maxDigit = digit_number(max);  //得到最大整数的位数42  int i, j, k;43  for(i=0; i<10; i++)44  {45   temp[i] = new int[len];    //使每一个箱子能装下len个int元素46   memset(temp[i], 0, sizeof(int) * len); //初始化为047  }


2016-05-09
原文:48  for(i=0; i<maxDigit; i++)49  {50   memset(count, 0, sizeof(int) * 10); //每次装箱前把count清空51   for(j=0; j<len; j++)52   {53     int xx = kth_digit(a[j], i); //将数据安装位数放入到暂存数组中54     temp[xx][count[xx]] = a[j];55     count[xx]++;     //此箱子的计数递增56   }57   int index = 0;58   for(j=0; j<10; j++)    //将数据从暂存数组中取回,放入原始数组中59   {60     for(k=0; k<count[j]; k++) //把箱子里所有的元素都取回到原始数组61     {62      a[index++] = temp[j][k];63     }64    }65  }66 }


2016-05-09
原文:6768 int main(void)69 {70  int a[] = {22, 32, 19, 53, 47, 29};71  radix_sort(a, 6);7273  return 0;74 }下面简单说明一下radix_sort函数的执行步骤。(1)代码第 40~41 行,调用 find_max 取得序列中的最大整数,并调用 digit_number得到其最大位数maxDigit。(2)代码第43~47行,分配10个足够大的箱子来存放序列中的整数。(3)代码第51~56行,针对序列中整数的个位数,进行第一次分配箱子。(4)代码第57~64行,依次收集每个箱子的元素,放回到原始数组中。步骤(3)和步骤(4)一共需要进行maxDigit次,每一次针对序列中整数的不同位数进行分配箱子。代码第53行调用了kth_digit计算元素各个位数的数字,以确定放入哪一个箱子。另外,在 radix_sort 函数里还有一个局部数组 count,它被用来在每次分配箱子后,保存各箱子里所含整数元素的个数。


2016-05-09
原文:选择排序算法的时候,需要考虑以下几点。数据的规模;数据的类型;数据已有的顺序。一般来说,当数据规模较小时,应选择直接插入排序或冒泡排序。任何排序算法在数据量小时基本体现不出差距。考虑数据的类型,比如全部是正整数时,应该考虑使用桶排序。考虑数据已有顺序,快速排序是一种不稳定的排序(当然可以改进)。对于大部分排好的数据,快速排序会浪费大量不必要的步骤。我们说快速排序好,是指大量随机数据下,使用快速排序的效果最理想,而不是指所有情况。


2016-05-10
原文:冒泡排序算法的时间复杂度是O(n^2)。选择排序算法的时间复杂度是O(n^2)。插入排序算法的时间复杂度是O(n^2)。快速排序是不稳定的,最理想情况下的算法时间复杂度是O(nlog2n),最坏是O(n^2)。堆排序算法的时间复杂度是O(nlogn)。归并排序算法的时间复杂度是O(nlog2n)。


2016-05-10
原文:泛型编程指编写完全一般化并可重复使用的算法,其效率与针对某特定数据类型而设计的算法相同。所谓泛型,是指具有在多种数据类型上皆可操作的含意,在 C++中实际上就是使用模板实现。


2016-05-10
原文:C++提供的类模板是一种更高层次的抽象的类定义,用于使用相同代码创建不同的类模板的定义与函数模板的定义类似,只是把函数模板中的函数定义部分换作类说明,并对类的成员函数进行定义即可。在类说明中可以使用出现在TYPE_LIST中的各个类型标识以及出现在ARG_LIST中的各变量。1 template<<棋板参数表>>2 class<类模板名>3 {<类模板定义体>},例如,我们需要定义一个表示平面的点(Point)类,这个类有两个成员变量,分别表示横坐标和纵坐标,并且这两个坐标的类型可以是 int、float、double 等类型。因此可能写出类似Point_int_int、Point_float_int、Point_float_float等这样的类。通过类模板,我们只需要写一个类。1 #include <iostream>2 using namespace std;34 template <class T1, class T2>5 class Point_T6 {7 public:


2016-05-10
原文:8  T1 a;    //成员a为T1类型9  T2 b;    //成员b为T2类型10  Point_T() : a(0), b(0) {} //默认构造函数11  Point_T(T1 ta, T2 tb) : a(ta), b(tb) {}   //带参数的构造函数12  Point_T<T1, T2>& operator=(Point_T<T1, T2> &pt); //赋值函数13  friend Point_T<T1,T2> operator +(Point_T<T1,T2> &pt1, Point_T<T1, T2> &pt2); //重载+14 };1516 template <class T1, class T2>17 Point_T<T1, T2>& Point_T<T1, T2>::operator=(Point_T<T1, T2> &pt)  //赋值函数18 {19  this->a = pt.a;20  this->b = pt.b;21  return *this;22 }2324 template <class T1, class T2>


2016-05-10
原文:25 Point_T<T1, T2> operator +(Point_T<T1, T2> &pt1, Point_T<T1, T2> &pt2)//重载+26 {27  Point_T<T1, T2> temp;28  temp.a = pt1.a + pt2.a; //结果对象中的a和b分别为两个参数对象的各自a和b之和29  temp.b = pt1.b + pt2.b;30  return temp;31 }3233 template <class T1, class T2>34 ostream& operator << (ostream& out, Point_T<T1, T2>& pt) //重载输出流操作符35 {36  out << "(" << pt.a << ", ";       //输出(a, b)37  out << pt.b << ")";38  return out;39 }4041 int main()42 {43  Point_T<int, int>


2016-05-10
原文:ntPt1(1, 2);     //T1和T2都是int44  Point_T<int, int> intPt2(3, 4);     //T1和T2都是int45  Point_T<float, float> floatPt1(1.1f, 2.2f);  //T1和T2都是float46  Point_T<float, float> floatPt2(3.3f, 4.4f);  //T1和T2都是float4748  Point_T<int, int> intTotalPt;49  Point_T<float, float> floatTotalPt;5051  intTotalPt = intPt1 + intPt2;  //类型为Point_T<int, int>的对象相加52  floatTotalPt = floatPt1 + floatPt2; //类型为Point_T<float, float>的对象相加5354  cout << intTotalPt << endl;  //输出Point_T<int, int>的对象55  cout << floatTotalPt << endl;  //输出Point_T<float, float>的对象5657  return 0;58 }


2016-05-10
原文:有一些概念需要区别:函数模板与模板函数、类模板和模板类是不同的意思。


2016-05-10
原文:函数模板的重点是模板,它表示的是一个模板,用来生产函数。例如前面面试题的max是一个函数模板。而模板函数的重点是函数,它表示的是由一个模板生成的函数。例如max<int>,max<float>等都是模板函数。类模板和模板类的区别与上面的类似。类模板用于生产类,例如Point_T就是一个类模板。而模板类是由一个模板生成的类,例如 Point_T<int, int>和 Point_T<float_float>都是模板类。


2016-05-10
原文:函数模板和类模板有什么区别?在面试题1的程序代码中,我们在使用函数模板max时不一定必须指明T的类型,函数模板max 的实例化是由编译程序在处理函数调用时自动完成的。当调用max(1, 2)时自动生成实例max<int>,而调用max(1.1f, 2.2f)时自动生成实例max<float,float>。当然,也可以显示指定T的类型。对于本面试题的类模板 Point_T 来说,其实例化必须被显示地指定,比如 Point_T<int, int>、Point_T<float, float >。


2016-05-10
原文:函数模板是一种抽象的函数定义,它代表一类同构函数。类模板是一种更高层次的抽象的类定义。函数模板的实例化是由编译程序在处理函数调用时自动完成的,而类模板的实例化必须由程序员在程序中显式地指定。


2016-05-10
原文:templates(模板)是节省时间和避免代码重复的极好的方法。我们可以只输入一个类模板,就能让编译器实例化所需要的很多个特定类及函数。类模板的成员函数只有被使用时才会被实例化,所以只有在每一个函数都在实际中被使用时,我们才会得到这些函数。


2016-05-10
原文:虽然这些函数做了相同的事情(打印一个“work()”和 num),但它们却有不同的二进制代码。这就是所说的由于模板导致的代码膨胀。也就是说,虽然源代码看上去紧凑而整洁,但是目标代码却臃肿而松散,会严重影响程序的运行效率。


2016-05-10
原文:如何避免这种代码膨胀呢?有一个原则,就是把 C++模板中与参数无关的代码分离出来。也就是让与参数无关的代码只有一份复制。


2016-05-10
原文:模板的缺点:不当地使用模板会导致代码膨胀,即二进制代码臃肿而松散,会严重影响程序的运行效率。解决方法:把C++模板中与参数无关的代码分离出来。


2016-05-10
原文:模板类(template class)的实例个数是由类型参数的种类决定的。


2016-05-10
原文:模板的特化(template specialization)分为两类:函数模板的特化和类模板的特化。(1)函数模板的特化:当函数模板需要对某些类型进行特别处理时,称为函数模板的特化。例如1 bool IsEqual(T t1, T t2)2 {3  return t1 == t2;4 }56 int main()7 {8  char str1[] = "Hello";9  char str2[] = "Hello";10  cout << IsEqual(1, 1) << endl;11  cout << IsEqual(str1, str2) << endl; //输出012  return 0;


2016-05-10
原文:13 }代码第11 行比较字符串是否相等。由于对于传入的参数是char *类型的,max 函数模板只是简单地比较了传入参数的值,即两个指针是否相等,因此这里打印0。显然,这与我们的初衷不符。因此,max 函数模板需要对char *类型进行特别处理,即特化:1 template <>2 bool IsEqual(char* t1, char* t2)  //函数模板特化3 {4  return strcmp(t1, t2) == 0;5 }这样,当 IsEqual 函数的参数类型为 char* 时,就会调用 IsEqual 特化的版本,而不会再由函数模板实例化。


2016-05-10
原文:(2)类模板的特化:与函数模板类似,当类模板内需要对某些类型进行特别处理时,使用类模板的特化。例如1 template <class T>2 class compare3 {4 public:5  bool IsEqual(T t1, T t2)6  {7    return t1 == t2;8  }9 };1011 int main()12 {13  char str1[] = "Hello";14  char str2[] = "Hello";15  compare<int> c1;16  compare<char *> c2;17  cout << c1.IsEqual(1, 1) << endl;  //比较两个int类型的参数18  cout << c2.IsEqual(str1, str2) << endl; //比


2016-05-10
原文:较两个char *类型的参数19  return 0;20 }这里代码第18行也是调用模板类compare<char*>的IsEqual进行两个字符串比较。显然这里存在的问题和上面函数模板中的一样,我们需要比较两个字符串的内容,而不是仅仅比较两个字符指针。因此,需要使用类模板的特化:1 template<>2 class compare<char *> //特化(char*)3 {4 public:5  bool IsEqual(char* t1, char* t2)6  {7    return strcmp(t1, t2) == 0;  //使用strcmp比较字符串8  }9 };注意:进行类模板的特化时,需要特化所有的成员变量及成员函数。


2016-05-11
原文:模板有两种特例化:部分模板特例化和全部模板特例化。 全部模板特例化就是模板中的模板参数全被指定为确定的类型,也就是定义了一个全新的类型。全部模板特例化的类中的函数可以与模板类不一样,例如 1 template <class A, class B, class C> 2 class X {}; //(a) 3 template <>


2016-05-11
原文:4 class X<int, float, string> {}; //(b) 若编译器遇到X<int, float, string>的模板实例化请求,则使用特例化后的版本,即(b)。其他任何类型的组合都是用基本模板,即(a)。 部分模板特例化就是模板中的模板参数没有被全部确定,需要编译器在编译时进行确定。它通常有两种基本情况: (1)对部分模板参数进行特例化: 1 template < class B, class C> 2 class X <int, B, C> {...}; // (c) 当编译器遇到X <int, float, string> 的模板实例化请求时,使用这个特例化的版本(c)。而当编译器遇到X <int, double, char> 时,也是用这个特例化版本。也就是说,只要X<>实例化时,第一个模板实参是int,就使用特例化版本。 (2)使用具有某一特征的类型,对模板参数进行特例化: 1 template <class T>


2016-05-11
原文:2 class Y {...};  //(d) 3 template <class T> 4 class Y<T*> {...}; //(e) 当编译器遇到Y<int*>时,使用特例化模板(e)。当编译器遇到T<float*>时,也使用特例化模板(e)。而其他类型的实例化,如Y<int>,则使用基本模板(d)。也就是说,当模板实参符合特例化版本所需的特征时(在上面例子中是某个类型的指针),则使用特例化版本。 这两种情况有时会混合使用,比如 1 template <class A, class B> 2 class Z {...}; //(f) 3 template <typename A> 4 class Z <A&, char> {...}; //(g) 当编译器遇到Z<int&, char>或者Z<string&, char>时,使用特例化模板(g)。其他情况使用基本模板(f),比如Z<int&, float>或Z<int, char>等。


2016-05-11
原文:STL(Standard Template Library),即标准模板库,它涵盖了常用的数据结构和算法,并且具有跨平台的特点。将泛型编程思想和STL库用于系统设计中,明显降低了开发强度,提高了程序的可维护性及代码的可重用性。这也是越来越多的笔试和面试中考查STL相关知识的原因。 STL是C++标准函数库的一部分。STL的基本观念就是把数据和操作分离。图11.1所示的是STL组件之间的合作。  图11.1 STL组件之间的合作 由图11.1可知,STL中数据由容器类别来加以管理,操作则由可定制的算法来完成。迭代器在容器和算法之间充当黏合剂,它使得任何算法都可以和任何容器进行交互运作。STL含有容器、算法、迭代器组件。 其中容器又分为下面几种。


2016-05-11
原文:TL 序列容器:vector、string、deque 和list。 STL 关联容器:set、multiset、map 和multimap。 STL 适配容器:stack、queue 和priority_queue。


2016-05-11
原文:STL(Standard Template Library),即标准模板库,是一个具有工业强度的、高效的C++程序库。它被容纳于C++标准程序库(C++ Standard Library)中,是ANSI/ISO C++标准中最新的也是极具革命性的一部分。该库包含了诸多在计算机科学领域里所常用的基本数据结构和基本算法,为广大C++程序员提供了一个可扩展的应用框架,高度体现了软件的可复用性。它类似于 Microsoft Visual C++中的 MFC(Microsoft Foundation ClassLibrary)。 从逻辑层次来看,在STL 中体现了泛型化程序设计的思想(Generic Programming),引入了许多新的名词,比如需求(requirements)、概念(concept)、模型(mod


2016-05-11
原文:l)、容器(container)、算法(algorithmn)、迭代器(iterator)等。 从实现层次看,整个STL 是以一种类型参数化(type parameterized)的方式实现的,这种方式基于一个在早先 C++标准中没有出现的语言特性——模板(template)。如果查阅任何一个版本的STL源代码,你就会发现,模板作为构成整个STL的基石是一件千真万确的事情。除此之外,还有许多C++的新特性为STL的实现提供了方便。 STL是最新的C++标准函数库中的一个子集,它占据了整个库的大约80%的分量。而作为在实现 STL 过程中扮演关键角色的模板,则充斥了几乎整个 C++标准函数库。C++标准函数库中的各个组件的关系图如图11.2所示。 C++标准函数库包含了如下几个组件。 (1)C 标准函数库,尽管有了一些变化,但是基本保持了与原有 C 语言程序库的良好兼容。C++标准库中存在两套C的函数库,一套是带有.h扩展名的(比如<stdio.h>),而另一套则没有(比如<cstdio>)。它们确实没有太大的不同。


2016-05-11
原文:(2)语言支持(language support)部分,包含了一些标准类型的定义以及其他特性的定义。这些内容被用于标准库的其他地方或是具体的应用程序中。 (3)诊断(diagnostics)部分,提供了用于程序诊断和报错的功能,包含了异常处理(exception handling)、断言(assertions)、错误代码(error number codes)三种方式。 (4)通用工具(general utilities)部分,这部分内容为C++标准库的其他部分提供支持。当然,你也可以在自己的程序中调用相应功能,比如动态内存管理工具、日期/时间处理工具。记住,这里的内容也已经被泛化了(采用了模板机


2016-05-11
原文:制)。 (5)字符串(string)部分,用来代表和处理文本。它提供了足够丰富的功能。 (6)国际化(internationalization)部分,作为OOP特性之一的封装机制在这里扮演着消除文化和地域差异的角色,采用locale和facet可以为程序提供众多国际化支持,包括对各种字符集的支持、日期和时间的表示、数值和货币的处理等等。 (7)容器(containers)部分,是STL的一个重要组成部分,涵盖了许多数据结构,如链表、vector(类似于大小可动态增加的数组)、queue(队列)、stack(堆栈)……string也可以看作一个容器,适用于容器的方法同样也适用于string。 (8)算法(algorithms)部分,是STL的一个重要组成部分,包含了大约70个通用算法,用于操控各种容器,同时也可以操控内建数组。 (9)迭代器(iterators)部分,是STL 的一个重要组成部分。它使得容器和算法能够完美地结合。事实上,每个容


2016-05-11
原文:器都有自己的迭代器,只有容器自己才知道如何访问自己的元素。它有点像指针,算法通过迭代器来定位和操控容器中的元素。 (10)数值(numerics)部分,包含了一些数学运算功能,提供了复数运算的支持。 (11)输入/输出(input/output)部分,就是经过模板化了的原有标准库中的iostream部分。它提供了对C++程序输入/输出的基本支持。在功能上保持了与原有iostream的兼容,增加了异常处理的机制,并支持国际化(internationalization)。 总体上,在C++标准函数库中,STL主要包含了容器、算法、迭代器。string也可以算作STL的一部分。 【答案】 STL,即标准模板库,是一个具有工业强度的、高效的 C++程序库。它是最新的 C++标准函数库中的一个子集,包括容器、算法、迭代器组件。


2016-05-11
原文:vector的定义如下。 1 template<class _Ty, class _A = allocator<_Ty> > 2 class vector { 3 …… 4 }; 这里省略了中间的成员。其中_Ty类型用于表示vector中存储的元素类型,_A默认为allocator<_Ty>类型。 这里需要说明的是allocator类,它是一种“内存配置


2016-05-11
原文:器”,负责提供内存管理(可能包含内存分配、释放、自动回收等能力)相关的服务。于是对于程序员来说,就不用关心内存管理方面的问题了。 vector支持随机访问,因此为了效率方面的考虑,它内部使用动态数组的方式实现。当进行insert或push_back等增加元素的操作时,如果此时动态数组的内存不够用,就要动态地重新分配,一般是当前大小的两倍,然后把原数组的内容拷贝过去。所以,在一般情况下,其访问速度同一般数组,只有在重新分配发生时,其性能才会下降。例如下面的程序。


2016-05-11
原文:初始化时v无元素(size为0),且容量(capacity)也为0。


2016-05-11
原文:注意,vector的size()和capacity()是不同的,前者表示数组中元素的多少,后者表示数组有多大的容量。由上面的分析可以看出,使用 vector 的时候需要注意内存的使用。如果频繁地进行内存的重新分配,会导致效率低下。


2016-05-11
原文:它的内部使用allocator类进行内存管理,程序员不需要自己操作内存。


2016-05-11
原文:考点:vector中iterator的使用 出现频率:★★★★ 1 vector <int> array; 2 array.push_back( 1 ); 3 array.push_back( 2 ); 4 array.push_back( 3 ); 5 for(vector<int>::size_type i=array.size()-1; i>=0; --i ) //反向遍历array数组 6 { 7  cout << array[i] << endl;


2016-05-11
原文:8 } 当运行代码时,没有输出 3 2 1,而是输出了一大堆很大的数字,为什么? 于是修改代码: 1 for(vector <int>::size_type j=array.size();j>0;j--) 2 { 3  cout << "element is " <<array[j-1] <<endl; 4 } 这样就输出了3 2 1,到底是为什么呢? 【解析】 由于vector支持随机访问,并且重载了[]运算符,因此可以像数组那样(比如a[i])来访问vector中的第i+1个元素。 现在来简单分析vector中size_type的定义过程。为方便起见,下面省略了某些头文件。


2016-05-11
原文:在 vector定义中可以看到: 1 typedef _A::size_type size_type; 而_A是 allocator<_Ty>,因此查看 allocator的定义,不难发现: 1 typedef _SIZT size_type; 而_SIZT的定义为: 1 #define _SIZT size_t 最后 size_t的定义为: 1 typedef unsigned int size_t; 因此最后的结果为:size_type 是个unsigned int 类型成员。我们知道,无符号的整数是大于等于0的,因此上面第1段代码(代码第5行)中的i>=0永远为true,程序一直循环,输出很多随机数,最后程序崩溃。而第2段代码(代码第1行)使用了i>0作为循环的条件,即i为0时结束循环。


2016-05-11
原文:考点:理解vector容器的使用 出现频率:★★★★ 1 typedef vector IntArray; 2 IntArray array; 3 array.push_back( 1 ); 4 array.push_back( 2 ); 5 array.push_back( 2 ); 6 array.push_back( 3 ); 7 //删除array数组中所有的2 8 for( IntArray::iterator itor=array.begin(); itor!=ar


2016-05-11
原文:ay.end(); ++itor ) 9 { 10  if( 2 == *itor ) 11   array.erase( itor ); 12 } 【解析】 这道题有两个错误。 (1)代码第 1 行中没有使用类型参数,这将会导致编译错误。由于 array 需要添加 int类型的元素,因此代码第1行定义vector时应该加上int类型。 1 typedef vector<int> IntArray; (2)代码第8~12行的for循环,这里只能删除array数组中的第一个2,而不能删除所有的 2。这是因为,每次调用“array.erase( itor );”后,被删除元素之后的内容会自动往前移,导致迭代漏项,应在删除一项后使itor--,使之从已经前移的下一个元素起继续遍历。


2016-05-11
原文:正确的程序如下。 1 #include <iostream> 2 #include <vector> 3 using namespace std; 4 5 int main() 6 { 7  typedef vector<int> IntArray; 8  IntArray array; 9  array.push_back( 1 ); 10  array.push_back( 2 ); 11  array.push_back( 2 ); 12  array.push_back( 3 ); 13  //删除array数组中所有的2


2016-05-11
原文:14  for( IntArray::iterator itor=array.begin(); itor!=array.end(); ++itor ) 15  { 16   if( 2 == *itor ) 17   { 18     array.erase( itor ); 19     --itor; //删除一项后使itor-- 20   } 21  } 22 23  //测试删除后array中的内容 24  for(itor=array.begin(); itor!=array.end(); ++itor) 25  { 26   cout << *itor << endl;


2016-05-11
原文:27  } 28 } 执行结果为: 1 1 2 3 【答案】 (1)没有使用类型参数,会导致编译错误。 (2)只能删除 array 数组中的第一个 2 ,而不能删除所有的 2。因为每次调用“array.erase( itor );”后,被删除元素之后的内容会自动往前移,导致迭代漏项。


2016-05-15
原文:deque 称为双向队列容器,表面上与 vector 非常相似,甚至能在许多实现中直接替换vector。比较deque和vector两者的成员函数,可以发现下面两点。


2016-05-15
原文:(1)deque比vector多了push_front()和pop_front()两个函数,而这两个函数都是对于首部进行操作的。于是得到第一个使用deque的情况,就是当程序需要从首尾两端进行插入或删除元素操作的时候。(2)deque中不存在capacity()和reserve()成员函数。在vector中,这两个函数分别用于获取和设置容器容量,例如下面的代码段。1 int main()2 {3  vector<int> v;4  v.push_back(1);5  cout << v.capacity() << endl;6  v.reserve(10);7  cout << v.capacity() << endl;89  return 0;10 }代码第3行,此时v的容量为0。代码第4行,添加一个元素到末尾,此时v的容量(capacity)为1,元素个数(size)为1。代码第6行,调用reserve设置v的容量,此时v的容量为10,元素个数仍然为1。


2016-05-15
原文:代码第4行,又添加一个元素到末尾,此时v的元素个数为2。由于元素个数小于容量,因此容量没有扩充,仍旧为10。执行结果为:1 12 10因此,对于vector 来说,如果有大量的数据需要push back,则应当使用reserve()函数先设定其容量大小,否则会出现许多次容量扩充操作,导致效率很低。而deque使用一段一段的定量内存,在进行内存扩充时也只是加一段定量内存,因此不存在容量的概念,也就没有capacity()和reserve()成员函数。最后,在插入(insert)操作上,deque和vector有很大的不同。由于vector是一块连续的内存,所以插入的位置决定执行效率,位置越偏向数组首部,效率越低。而deque中的内存是分段连续的,因此在不同段中的插入效率都相同。


2016-05-15
原文:7 int main()8 {9  stack<int, vector<int> > s;10  queue<int, vector<int> > q;1112  for (int i=1; i <10; i++)13  {14   s.push(i);15   q.push(i);16  }1718  while(!s.empty())19  {20   cout << s.top() << endl;21   s.pop();22  }2324  while(!q.empty())25  {26   cout << q.front() << endl;27   q.pop();28  }2930  return 0;31}


2016-05-15
原文:代码第9~10行,使用vector分别定义了stack和queue。代码第12~16行,把1~9放入s和q中。代码第18~22行,循环打印s的栈顶元素,并且不断退栈,最终s变为空栈,打印的顺序为与入栈的顺序相反,即9 8 7 6 5 4 3 2 1。代码第 24~28 行,与前面的操作方法一样,但是这里会出现编译错误。这是因为,queue是先进先出的,入队(调用push)是对队尾进行操作,由于q使用vector作为其序列容器,因此实际调用的是vector的push_back成员函数。而出队(调用pop)是对队首进行操作,此时q实际需要调用vector的pop_front成员函数,而vector没有这个pop_front成员。因此出现编译错误。stack 是后进先出的,入栈和退栈都是对尾部进行操作,而 vector 有相应的 push_back和pop_back成员函数,因此代码第18~22行能够顺利执行。【答案】


2016-05-15
原文:map中存放的每一个元素都是一个键值对,下面的程序演示了常用的map操作。1 #include <iostream>2 #include <map>3 #include <string>4 using namespace std;56 int main()7 {8  map<int, string> mapstring;    //键为int类型,值为string类型9  mapstring.insert(pair<int, string>(1, "one"));  //插入4个元素10  mapstring.insert(pair<int, string>(4, "four"));11  mapstring.insert(pair<int, string>(3, "three"));


2016-05-15
原文:12  mapstring.insert(pair<int, string>(2, "two"));13  mapstring.insert(pair<int, string>(4, "four four")); //4已经存在,插入失败1415  mapstring[1] = "one one";    //1已经存在,修改键为1对应的值16  mapstring[5] = "five";     //5不存在,添加键为5且值为"five"的元素17  cout << mapstring[1] << endl;   //打印键为1对应元素的值1819  mapstring.erase(2);      //删除键为2的元素20  map<int, string>::iterator f = mapstring.find(2);  //查找键为2的元素21  if (f != mapstring.end())    //判断查找是否成功,如果成功,则不相等22  {23   mapstring.erase(f);24  }2526  map<int, string>::iterator it = map


2016-05-15
原文:string.begin();27  while(it != mapstring.end())   //使用迭代器遍历map中所有元素28  {29   cout << (*it).first << " " << (*it).second << endl; //打印元素的键和值30   it++;31  }3233  return 0;34 }上面的程序中,mapstring 是存放pair<int, string>类型的元素。插入操作,insert成员函数或使用[]操作符都可以进行插入。但它们有一点区别:当map中已经存在此键时,insert插入失败,例如代码第13行的插入没有作用;而[]操作符则修改此键所对应的元素,例如代码第15行则修改键为1对应的值。查找操作,代码第20行查找键为2的元素,如果查找成功,则迭代器指向键为2的元素,否则指向末尾,即mapstring.end()。删除操作,这里演示了两种删除,和别的容器一样,可以删除迭代器指向的元素位置(或者两个迭代器的区间删除)。由于map中元素的键是唯一的,因此map也提供了以键删除的操作(如代码第


2016-05-15
原文:19行)。遍历操作,与其他stl容器遍历操作类似,使用迭代器对begin()和end()之间进行迭代。it指向的是都pair<int, string>元素。pair有两个成员:first和second,分别表示键(key)和值(value)。程序输出结果:1 one one2 one one3 three4 four5 five可以看到,map遍历的结果是按照元素的键(key)的升序排列。这是默认情况,如果想让它们降序排列,只需要把map 和iterator 的声明都改为<int, string, greater<int> >就可以了,其中greater<int>表示按从大到小排列,并且键的类型是int。


2016-05-15
原文:标准的 STL 关联容器(包括 set 和 map 以及 set 的衍生体 multiset 和 map 的衍生体multimap)的内部结构是一个平衡二叉树(balanced binary tree)。平衡二叉树有下面几种。AVL-tree;RB-tree;AA-tree。STL的底层机制都是以RB-tree(红黑树)完成的。RB-tree也是一个独立容器,但并不给外界使用。红黑树这个名字的得来就是由于树的每个结点都被着上了红色或者黑色,节点所着的颜色被用来检测树的平衡性。在对节点插入和删除的操作中,可能会被旋转来保持树的平衡性。平均和最坏情况下的插入、删除、查找时间都是O(lgn)。一个红黑树是一棵二叉查找树,除了二叉查找树带有的一般要求外,它还具有下列的属性。结点为红色或者黑色。


2016-05-15
原文:所有叶子结点都是空节点,并且被着为黑色。如果父结点是红色的,那么两个子节点都是黑色的。结点到其子孙节点的每条简单路径上都包含相同数目的黑色节点。根结点是黑色的。


2016-05-15
原文:map底层是以红黑树实现的。


2016-05-15
原文:底层数据结构不同,map 是红黑树,hashmap 是哈希表。map 的优点在于元素可以自动按照键值排序,而 hash map 的优点在于它的各项操作的平均时间复杂度接近常数。map 属于标准的一部分,而hash map 则不是。


2016-05-15
原文:STL包含了大量的算法。它们巧妙地处理储存在容器中的数据。以reverse算法为例,我们只要简单使用reverse算法就能够逆置一个区间中的元素。1 #include <iostream>2 #include <vector>3 #include <string>4 #include <algorithm> //stl算法头文件5 using namespace std;67 int main()8 {9  int a[4] = {1, 2, 3, 4};10  vector<string> v;11  v.push_back("one");   //插入三个字符串12  v.push_back("two");13  v.push_back("three");


2016-05-15
原文:14  reverse<int [4]>(a, a+4);  //把数组a的所有元素逆置15  reverse< vector<string>::iterator >(v.begin(), v.end()); //逆置v中所有元素16  for(vector<string>::iterator it=v.begin(); it!=v.end(); it++)17  {18   cout << *it << " ";  //输出v中元素19  }20  cout << endl;21  for(int i=0; i<4; i++)22  {23   cout << a[i] << " ";  //输出数组a中元素24  }25  return 0;26 }程序执行结果:1 three two one2 4 3 2 1


2016-05-15
原文:可以看到,为了对数组 a 以及容器 v 中的所有元素进行逆置,我们都只调用了一个reverse方法。这里有几点要注意:reverse 是个全局函数,而不是成员函数。reverse 有两个参数,第一个参数是指向要操作的范围的头的指针,第二个参数是指向尾的指针。reverse 操作一定范围的元素而不是仅仅操作容器,本题中对数组也进行了操作。reverse和其他STL算法一样,它们是通用的,也就是说,reverse不仅可以用来颠倒向量的元素,也可以用来颠倒链表中元素的顺序,甚至可以对数组操作。它们实际上都是函数模板。


2016-05-15
原文:1 #include <iostream>2 #include <deque>3 #include <algorithm>4 using namespace std;56 void print(int elem)7 {8  cout << elem << " ";9 }1011 int main()12 {13  deque<int> coll;14  for (int i=1; i<=9; ++i)15  {16   coll.push_back(i);17  }18  deque<int>::iterator pos1;19  pos1 =


2016-05-15
原文:find(coll.begin(), coll.end(), 2);20  deque<int>::iterator pos2;21  pos2 = find(coll.begin(), coll.end(), 7);22  for_each(pos1, pos2, print);23  cout << endl;2425  deque<int>::reverse_iterator rpos1(pos1);26  deque<int>::reverse_iterator rpos2(pos2);27  for_each(rpos2, rpos1, print);28  cout << endl;2930  return 0;31 }【解析】本题涉及以下内容。deque 容器的操作用法;find 和for_each 两种泛型算法;iterator和reverse_iterator的使用。下面是程序的执行步骤。(1)代码第14~17行,把1~9这9个数字放入coll容器中。(2)代码第19、21行,调用find分别获得整数2和7在


2016-05-15
原文:coll中的存放位置,即pos1指向2,pos2指向7。(3)代码第22行,调用foreach对pos1到pos2的区间元素依次执行print,打印各个元素值。由于左闭右开的原则,从2(pos1)开始到6(pos2 前一位)结束,打印结果为:2 3 4 5 6。(4)代码第25、26行,分别根据迭代器pos1和pos2得到反向迭代器rpos1和rpos2,即rpos1指向1,rpos2指向6。(5)代码第27行,此时使用rpos2和rpos1反向打印各个元素值。由于左闭右开的原则,从6(rpos2)开始到2(rpos1 前一位)结束,打印结果为:6 5 4 3 2。【答案】1 2 3 4 5 62 6 5 4 3 2


2016-05-16
原文:vector中的erase方法与algorithm中的remove有什么区别


2016-05-16
原文:


2016-05-16
原文:vector中erase 是真正删除了元素,迭代器不能访问了。而 algorithm 中的 remove 只是简单地把要 remove 的元素移到了容器最后面,迭代器还是可以访问到的。这是因为 algorithm 通过迭代器操作,不知道容器的内部结构,所以无法做到真正删除。


2016-05-17
原文:许多数据重要的结构以及应用,例如链表、STL 容器、串、数据库系统以及交互式应用必须使用动态内存分配,因此仍然冒着万一发生异常导致内存溢出的风险。C++标准化委员会意识到了这个漏洞并在标准库中添加了一个特殊的类模板,它就是 std::auto_ptr,其目的是促使动态内存和异常之前进行平滑的交互。auto_ptr保证当异常掷出时,分配的对象能被自动销毁,内存能被自动释放。下面是auto_ptr的用法。1 #include <iostream>2 #include <string>3 #include <memory>4 using namespace std;56 class Test7 {8 public:9  Test() {name = NULL;}


2016-05-17
原文:10  Test(const char* strname)     //构造函数11  {12   name = new char[strlen(strname)+1]; //分配内存13   strcpy(name, strname);     //拷贝字符串14  }15  Test& operator = (const char *namestr)  //赋值函数16  {17   if (name != NULL)18   {19     delete name;      //释放原来的内存20   }21   name = new char[strlen(namestr)+1];   //分配新内存22   strcpy(name, namestr);      //拷贝字符串23   return *this;24  }25  void ShowName() {cout << name << endl;}   //打印name26 ~Test()       


2016-05-17
原文://析构函数27  {28   if (name != NULL)29   {30     delete name;       //释放name所指内存31   }32   name = NULL;33   cout << "delete name" << endl;34  }35 public:36  char *name;37 };3839 int main()40 {41  auto_ptr<Test> TestPtr(new Test("Terry"));  //TestPtr智能指针42  //auto_ptr<Test> TestPtr = new Test("Terry");  //编译错误43  TestPtr->ShowName();    //使用智能指针调用ShowName()方法44  *TestPtr = "David";     //使用智能指针改变字符串内容45  TestPtr->ShowName();    //使用智能指针调用ShowName()方法


2016-05-17
原文:4647  int y = 1;48  int x = 0;49  y = y / x;       //产生异常,TestPtr指向对象的内存仍然能得到释放50  return 0;51 }在这里,我们定义了一个Test类用于测试。Test类有一个成员name,并实现它的构造函数、赋值函数以及析构函数,另外还有ShowName()打印name字符串。下面是使用auto_ptr操作的步骤。代码第37行,创建并初始化auto_ptr。auto_ptr后面的尖括


2016-05-17
原文:弧里指定auto_ptr指针的类型,在这个例子中是Test。然后auto_ptr句柄的名字,在这个例子中是TestPtr。最后是用动态分配的对象指针初始化这个实例。注意,只能使用auto_ptr构造器的拷贝,也就是说,代码第38行使用赋值的方式是非法的,因此第38行会出现编译错误。代码第39行,使用TestPtr调用Test中的ShowName()成员函数。和一般指针操作相同,这里使用的是->操作符。代码第40行,使用TestPtr对原对象进行赋值。和一般指针相同,这里也是使用*操作符。代码第41行,再次调用ShowName()打印name字符串。代码第44行,这里会发生除0的异常,程序崩溃,但是智能指针指向的内存仍然能得到释放。没有注释代码第44行,即程序不会崩溃,则main函数返回前,TestPt发生析构,这时会调用Test的析构函数。程序执行结果:1 Terry2 David3 程序崩溃4 Delete name由此可以看出,使用auto_ptr可以代替指针进行类似指针的操作,并且不用关心内存释放。


2016-05-17
原文:auto_ptr 是如何解决前面提到的内存溢出问题的呢?auto_ptr 的析构函数自动摧毁它绑定的动态分配对象。也就是说,当 TestPt 的析构函数执行时,它删除构造 TestPt 期间创建的Test对象指针。


2016-05-17
原文:智能指针是用来实现指针指向的对象的共享的。其实现的基本思想:每次创建类的新对象时,初始化指针并将引用计数置为1;当对象作为另一对象的副本而创建时,拷贝构造函数拷贝指针并增加与之相应的引用计数;对一个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数(如果引用计数减至0,则删除对象),并增加右操作数所指对象的引用计数;调用析构函数时,减少引用计数(如果引用计数减至0,则删除基础对象);重载“->”以及“*”操作符,使得智能指针有类似于普通指针的操作。


2016-05-17
原文:根据以上分析,首先可以得出下面的类模板原型。1 template <class T>2 class SmartPtr3 {4 public:5  SmartPtr(T *p = 0);       //构造函数6  SmartPtr(const SmartPtr& src);    //拷贝构造函数7  SmartPtr& operator = (const SmartPtr& rhs); //赋值函数8  T* operator -> ();       //重载->9  T& operator * ();       //重载*10 ~SmartPtr();        //析构函数11 private:


2016-05-17
原文:12  void decrRef()13  {           //被其他成员函数所调用14   if (--*m_pRef == 0)      //自身的引用计数减115   {          //如果计数为0,则释放内存16     delete m_ptr;17     delete m_pRef;18   }19  }20  T *m_ptr;         //保存对象指针21  size_t *m_pRef;        //保存引用计数22 };上面的私有成员函数decrRef将引用计数减1。如果引用计数减至0,则删除m_ptr所指对象。根据前面的分析,decrRef只被赋值函数以及析构函数使用。下面说明各个成员的具体定义。首先是构造函数与析构函数的定义。普通构造函数中,m_ptr与p指向同一块内存,并初始化引用计数为1。拷贝构造函数中与普通构造函数的不同之处为引用计数需要加1。析构函数调用私有成员decrRef对引用计数递减,并且判断是否需要释放对象。代码如下。


2016-05-17
原文:1 template<class T>2 SmartPtr<T>::SmartPtr(T *p)  //普通构造函数3 {4  m_ptr = p;    //m_ptr与p指向同一内存5  m_pRef = new size_t(1); //m_pRef初值为16 }78 template<class T>9 SmartPtr<T>::SmartPtr(const SmartPtr<T>& src)  //拷贝构造函数


2016-05-17
原文:10 {11  m_ptr = src.m_ptr;  //m_ptr与src.m_ptr指向同一内存12  m_pRef++;13  m_pRef = src.m_pRef;  //拷贝引用14 }1516 template<class T>17 SmartPtr<T>::~SmartPtr()  //析构函数18 {19  decrRef();      //引用减1,如果减后的引用为0,则释放内存


2016-05-17
原文:20  std::cout<<"SmartPtr: Destructor"<<std::endl;21 }接下来是“->”和“*”的重载。这两个函数很简单,只需要分别返回m_ptr以及m_ptr所指的内容即可。注意,如果m_ptr此时为空,则应该抛出异常。代码如下。1 template<class T>2 T* SmartPtr<T>::operator -> () //重载 ->3 {4  if (m_ptr)5    return m_ptr;6  //m_ptr为NULL时


2016-05-17
原文:抛出异常7  throw std::runtime_error("access through NULL pointer");8 }9 template<class T>10 T& SmartPtr<T>::operator * () //重载 *11 {12  if (m_ptr)13   return *m_ptr;14  //m_ptr为NULL时,抛出异常15  throw std::runtime_error("dereference of NULL pointer");


2016-05-17
原文:16 }最后是赋值函数的实现:1 template<class T>2 SmartPtr<T>& SmartPtr<T>::operator = (const SmartPtr<T>& rhs) //赋值函数3 {4  ++*rhs.m_pRef;   //rhs的引用加15  decrRef();   //自身指向的原指针的引用减16  m_ptr = rhs.m_ptr;  //m_ptr合rhs.m_ptr指向同一个对象7  m_pRef =


2016-05-17
原文:rhs.m_pRef; //复制引用8  return *this;9 }这样,就可以像 std::auto_ptr那样来使用SmartPtr。测试程序如下。1 int main()2 {3  SmartPtr<Test> t1;  //空指针4  SmartPtr<Test> t2(new Test("Terry"));5  SmartPtr<Test> t3(new Test("John"));6  try7  {8    t1->ShowName() ; //空指针调用抛出异常9  } catch (const exception& err)10  {11   cout << err.what() << endl;12 }13  t2->ShowName();   //使用t2调用showName()14  *t2 = "David";   //使用t2给对象赋值15  t2->ShowName();   //使用t2调用showName()16  t2 = t3;     //


2016-05-17
原文:赋值,原来t2的对象引用为0,发生析构17         //而t3的对象引用加118  cout << "End of main..." << endl;19  return 0;20 }main函数代码第8行,t1指向一个NULL指针,因此调用ShowName时会出现异常(异常在重载的“->”函数中被抛出)。main函数代码第12~15行,使用SmartPtr对象对Test对象进行操作,其方法与使用Test对象指针的操作方法相同。main函数代码第16行,对t2进行赋值操作,操作完成后,t2引用的原对象发生析构(此对象没有SmartPtr对象引用了),t2和t3引用同一个对象,于是这个对象的引用计数加1。注意,这里我们并没有显示地对t2所引用的原对象进行释放操作,这就是智能指针的精髓所在。


2016-05-17
原文:标准C++提供的auto_ptr。而auto_ptr的使用是有很多限制的,我们一条一条来细数。(1)std::auto_ptr 要求一个对象只能有一个拥有者,严禁一物二主,这一点和前面实现的SmartPtr不同。比如以下用法是错误的。1 classA *pA = new classA;2 auto_ptr<classA> ptr1(pA);3 auto_ptr<classA> ptr2(pA);(2)std::auto_ptr是不能以传值方式进行传递的。因为所有权的转移,会导致传入的智能指针失去对指针的所有权。如果要传递,可以采用引用方式,利用const引用方式还可以避免程序内其他方式的所有权的转移。(3)其他注意事项:不支持数组。


2016-05-17
原文:注意其Release 语意。Release 是指释放出指针,即交出指针的所有权。auto_ptr在拷贝构造和=操作符时的特珠含义决定了它不能做为STL标准容器的成员。


2016-05-17
原文:简单地说,函数对象就是一个重载了“()”运算符的类的对象,它可以像一个函数一样使用。例如1 #include <iostream>2 using namespace std;34 class MyAdd5 {6 public:7  int operator () (int a, int b) { return a+b; }8 };910 class MyMinus11 {12 public:13  int operator () (int a, int b) { return a-b; }14 };1516 int main()17 {


2016-05-17
原文:18  int a = 1;19  int b = 2;20  MyAdd addObj;21  MyMinus minusObj;22  cout << "a+b=" << addObj(a, b) << endl;  //输出 a+b=323  cout << "a-b=" << minusObj(a, b) << endl; //输出a-b=-124  return 0;25 }可以看出,由于类MyAdd和类MyMinus都重载了“()”运算符,因此它们的对象addObj和minusObj可以像函数那样使用。STL中提供了一元和二元函数的两种函数对象。下面列出了各个函数对象。一元函数对象:negate,相反数。二元函数对象:plus,加(+)法。minus,减(-)法。multiplies,乘(*)法。divides,除(/)法。modulus,求余(%)。equal_to,等于(==)。not_equal_to,不等于(!=)。


2016-05-17
原文:greater,大于(>)。greater_equal,大于或等于(>=)。less,小于(<)。less_equal,小于或等于(<=)。logical_and,逻辑与(&&)。logical_or,逻辑或(||)。logical_not,逻辑非(!)。以上这些函数对象都是基于模板实现的,可以这样使用它们:1 minus <int> int_ minus;2 cout << int_minus(3, 4) << endl; //输出-1【答案】函数对象就是一个重载了“()”运算符的类的对象,它可以像一个函数一样使用。


2016-05-17
原文:bind1st 和 bind2nd 都是函数适配器,用于将二元函数对象转换为一元函数对象。下面说明bind1st的用法。例子程序如下。1 #include <iostream>2 #include <algorithm>


2016-05-17
原文:3 #include <functional>4 #include <vector>5 using namespace std;67 int main()8 {9  //plus的第一个参数为1010  binder1st<plus<int> > plusObj = bind1st(plus<int>(), 10);11  //minus的第一个参数为1012  binder1st<minus<int> > minusObj = bind1st(minus<int>(), 10);


2016-05-17
原文:14  cout << plusObj(20) << endl; //执行10 + 20,打印3015  cout << minusObj(20) << endl; //执行10 - 20,打印101617  vector<int> v;18  for (int i=1; i<10; i++)19  {20   v.push_back(i); //v: 1,2,3,4,5,6,7,8,921  }


2016-05-17
原文:23  //使用count_if获得v中大于或等于4的个数24  int n = count_if(v.begin(), v.end(), bind1st(less_equal<int>(), 4));25  cout << "大于或等于4的数有" << n << "个。" << endl;2627  return 0;28 }代码第9~12行,使用bind1st对标准库中的plus和minus函数分别进行装配。因为plus 和 minus 都是二元函数对象,也就是说它们都有两个参数


2016-05-17
原文:所以经过装配后,plusObj和minusObj都变成了一元函数对象(只有1个参数),bind1st<plus<int>>和binder1st<minus <int>>分别是plusObj和minusObj的类型。binder1st表示绑定的是第一个参数(bind first),因此plusObj(20)执行的是10+20,而minusObj(20)执行的是10-20,这里的10都是被bind1st绑定的参数。STL中可以利用bind1st、bind2nd使得算法一般化,以count_if为例。代码第24行, bind1st(less_equal<int>(), 4)返回的是一个用于与 4 比较的一元函数对象。注意比较时 4 在“<=”左边,vector的各个元素


2016-05-17
原文:值在右边,因此得出的是vector中大于或等于4的元素个数。程序执行结果:1 302 -103 大于或等于4的数有6个bind2nd与bind1st用法类似,只有一点不同,就是它绑定的是第二个参数。因此,如果要使用bind2nd完成上面代码第24行的运算,只需要把less_equal换成greater_equal就可以了,即:1 int n = count_if(v.begin(), v.end(), bind2nd(greater_equal<int>(), 4));


2016-05-17
原文:bind1st和bind2nd都是函数适配器,用于将二元函数对象转换为一元函数对象。bind1st绑定的是第一个参数,而bind2nd绑定的是第二个参数。


2016-05-17
原文:标准 C++模板库中有一个名为 bind1st 的函数配接器(实际就是一个函数模板)。它接受两个参数,一个是二元函数对象bin_op,一个是二元函数对象的参数value。返回一个新的一元函数对象uni_op。使用uni_op(param)等效于bin_op(value,param),即二元函数对象的第一个value被“固定”了。试编写程序实现一个类似功能的my_bind1st函数配接器,并给出相应的测试代码。【解析】程序代码如下。1 #include <iostream>2 #include <algorithm>3 #include <functional>4 using namespace std;56 template <class Operation, class Param>7 class my_binder1st8 {


2016-05-17
原文:9 public:10  my_binder1st(Operation op, Param first)11  {12   m_op = op;      //二元函数对象赋值13   m_first = first;    //绑定的参数赋值14  }15  Param operator () (Param second) //重载()运算符16  {17   return m_op(m_first, second); //返回二元函数对象执行结果18  }19 private:20  Operation m_op;      //二元函数对象21  Param m_first;      //绑定的第一个参数22 };2324 template <class Operation, class Param>25 my_binder1st<Operation, Param>26 my_bind1st(Operation op, Param first)27 {28  //返回一个新的一元函


2016-05-17
原文:数对象29  return my_binder1st<Operation, Param>(op, first);30 }3132 int main()33 {34  //plus的第一个参数为1035  my_binder1st<plus<int>, int> plusObj = my_bind1st(plus<int>(), 10);36  //minus的第一个参数为1037  my_binder1st<minus<int>, int> minusObj = my_bind1st(minus<int>(), 10);3839  cout << plusObj(20) << endl;  //执行10 + 20,打印3040  cout << minusObj(20) << endl;  //执行10 - 20,打印104142  return 0;43 }程序中类模板 my_binder1st 重载了“()”运算符并且只有一个参数 second(代码第 17行),因此它是一个一元


2016-05-17
原文:函数对象。my_binder1st有两个私有成员,一个是二元函数对象op,另一个是绑定的第一个参数first。这两个私有成员是通过构造函数被赋值的。函数模板my_bind1st用来得到一个my_binder1st模板类对象,通过把二元函数对象以及绑定的first参数传入,得到转换之后的一元函数对象。最后main函数中对my_bind1st进行了测试。从代码第39行和第40行的打印结果可以看出,plusObj和minusObj确实转换成功。


《C和C++程序员面试秘笈[精品]》-笔记

标签:

原文地址:http://blog.csdn.net/chihun_love/article/details/51441323

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!