c++ 不要假设连续定义的局部变量在栈中的地址是连续的

突然想测一测电脑数据在内存中存放模式是小端的还是大端的,而且感觉自己对指针的掌握已经独步天下......

于是乎,挥笔写下如下程序:

int main()
{
     char a = 0;
     char b = 0;
     char c = 1;
     char d = 0;
     int * p = (int *)&d;
     printf("%d\n", *p);

/*
     printf("%d\n", &a);
     printf("%d\n", &b);
     printf("%d\n", &c);
     printf("%d\n", &d);
*/
     return 0;
}

咦?输出竟然不是256(小端)或者65536(大端)

在加入了注释的代码后,又神奇地正常显示256了!

什么鬼?难道printf还能改变变量的值不成?

a,b,c,d这四个变量不是在栈中连续排列的吗?地址不是应该连续的吗?为什么转为整型时出bug了?

反汇编看了一遍:

注释时是这样的:

0x0040134E	mov    BYTE PTR [esp+0x1f],0x0    //char a;
0x00401353	mov    BYTE PTR [esp+0x1e],0x0    //char b;
0x00401358	mov    BYTE PTR [esp+0x1d],0x1    //char c;
0x0040135D	mo
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
复习资料 1.1选择题 1.在一个C++程序,main函数的位置( c )。 (a) 必须在程序的开头 (b) 必须在程序的后面 ( c ) 可以在程序的任何地方 (d) 必须在其它函数间 2.用C++语言编制的源程序要变为目标程序必须要经过( d )。 (a) 解释 (b) 汇编 (c) 编辑 (d) 编译 3.C++程序基本单位是( c )。 (a) 数据 (b) 字符 (c) 函数 (d) 语句 4. C++程序的语句必须以( b )结束。 (a) 冒号 (b) 分号 (c) 空格 (d)花括号 5. 执行C++程序时出现的“溢出”错误属于( c )错误。 (a) 编译 (b) 连接 (c) 运行 (d) 逻辑 6.下列选项,全部都是C++关键字的选项为( c )。 (a) while IF static (b) break char go (c) sizeof case extern (d) switch float integer 7. 按C++标识符的语法规定,合法的标识符是( a ,c )。 (a) _abc (b) new (c) int1 (d) “age” 8.下列选项,( a )不是分隔符。 (a) ? (b) ; (c) : (d) () 9.下列正确的八进制整型常量表示是( b )。 (a) 0a0 (b) 015 (c) 080 (d) 0x10 10.下列正确的十六进制整型常量表示是( a,b,d )。 (a) 0x11 (b) 0xaf (c) 0xg (d) 0x1f 11.在下列选项,全部都合法的浮点型数据的选项为( a,b,d ),全部都不合法的浮点型数据选项是( c )。 (a) -1e3 , 15. , 2e-4 (b) 12.34 , -1e+5 , 0.0 (c) 0.2e-2.5 , e-5 (d) 5.0e-4 , 0.1 , 8.e+2 12.下列正确的字符常量为( b,d )。 (a) " a " (b) '\0' (c) a (d) '\101' 13.下列选项,( a,b,c )能交换变量a和b的值。 (a) t=b ;b=a ;a=t; (b) a=a+b ;b=a-b ;a=a-b; (c) t=a ;a=b ;b=t; (d) a=b ; b=a ; 14.执行语句 int i = 10,*p = &i; 后,下面描述错误的是( a )。 (a) p的值为10 (b) p指向整型变量i (c) *p表示变量i的值 (d) p的值是变量i的地址 15.执行语句 int a = 5,b = 10,c;int *p1 = &a, *p2 = &b; 后,下面不正确的赋值语句是( b )。 (a) *p2 = b; (b) p1 = a; (c) p2 = p1; (d) c = *p1 *(*p2); 16.执行语句 int a = 10,b;int &pa = a,&pb = b; 后,下列正确的语句是( b )。 (a) &pb = a; (b) pb = pa; (c) &pb = &pa; (d) *pb = *pa; 17.执行下面语句后,a和b的值分别为( b )。 int a = 5,b = 3,t; int &ra = a; int &rb = b; t = ra;ra = rb;rb = t; (a) 3和3 (b) 3和5 (c) 5和3 (d) 5和5 18. 在下列运算符,( d )优先级最高。 (a) <= (b)*= (c)+ (d)* 19. 在下列运算符,( d )优先级最低。 (a) ! (b)&& (c)!= (d)?: 20.设i=1,j=2,则表达式i+++j的值为( c )。 (a) 1 (b)2 (c)3 (d)4 21.设i=1,j=2,则表达式 ++i+j的值为( d )。 (a)1 (b)2 (c)3 (d)4 22.在下列表达式选项,( c )是正确。 (a)++(a++) (b)a++b (c)a+++b (d)a++++b 23.已知i=0,j=1,k=2,则逻辑表达式 ++i||--j&&++k的值为( b )。 (a) 0 (b)1 (c)2 (d)3 24. 执行下列语句后,x的值是( d ),y的值是( c )。 int x , y ; x = y = 1; ++ x || ++ y ; (a) 不确定 (b) 0 (c) 1 (d) 2 25.设X为整型变量,能正确表达数学关系1< X < 5的C++逻辑表达式是( b, c, d )。 (a) 1<X<5 (b) X==2||X==3||X==4 (c) 1<X&&X<5 (d) !(X<=1)&&!(X>=5) 26. 已知x=5,则执行语句 x += x -= x*x ; 后,x的值为( c )。 (a)25 (b)40 (c)-40 (d)20 27. 设a=1,b=2,c=3,d=4,则条件表达式a<b?a:c<d?c:d的值为( a )。 (a) 1 (b)2 (c)3 (d)4 28. 逗号表达式“(x=4*5,x*5),x+25的值为( d )。 (a) 25 (b)20 (c)100 (d)45 1.已知 int i,x,y;在下列选项错误的是( c )。 (a) if(x == y)i++; (b) if(x = y)i--; (c) if( xy )i--; (d) if( x+y )i++; 2.设有函数关系为y= ,下面选项能正确表示上述关系为( c )。 (a) y = 1; (b) y = -1; if( x>=0 ) if( x!=0) if( x==0 )y=0; if( x>0 )y = 1; else y = -1; else y = 0 (c) if( x<=0 ) (d) y = -1; if( x<0 )y = -1; if( x<=0 ) else y = 0; if( x<0 )y = -1; else y = 1; else y = 1; 3.假设i=2,执行下列语句后i的值为( b )。 switch(i) { case 1:i++; case 2:i--; case 3:++i;break; case 4:--i; default:i++; } (a) 1 (b) 2 (c) 3 (d) 4 4.已知int i=0,x=0;下面while语句执行时循环次数为( d )。 while(!x && i<3 ){ x++;i++;} (a) 4 (b) 3 (c) 2 (d) 1 5.已知int i=3;下面do_while 语句执行时循环次数为( b )。 do{ i--; cout<<i<<endl;}while( i!= 1 ); (a) 1 (b) 2 (c) 3 (d) 无限 6.下面for语句执行时循环次数为( b )。 for ( int i=0,j=5;i=j;) { cout << i << j << endl; i++;j--; } (a) 0 (b) 5 (c) 10 (d) 无限 7.以下死循环的程序段是( b )。 (a) for(int x=0;x<3 ;){ x++;}; (b) int k=0; do { ++k;} while( k>=0 ); (c) int a=5;while(a){ a--;}; (d) int i=3;for(;i;i--); 1.以下正确的函数原型为( d )。 (a) f( int x; int y ); (b) void f( x, y ); (c) void f( int x, y ); (d) void f( int, int ); 2.有函数原型 void fun1( int ); 下面选项,不正确的调用是( c )。 (a) double x = 2.17 ; fun1( x ); (b) int a = 15 ; fun1( a*3.14 ) ; (c) int b = 100 ; fun1( & b ); (d) fun1( 256 ); 3.有函数原型 void fun2( int * ); 下面选项,正确的调用是( c )。 (a) double x = 2.17 ; fun2( &x ); (b) int a = 15 ; fun2( a*3.14 ); (c) int b = 100 ; fun2( &b ); (d) fun2( 256 ); 4.有函数原型 void fun3( int & ); 下面选项,正确的调用是( c )。 (a) int x = 2.17; fun3( &x ); (b) int a = 15; fun3( a*3.14 ); (c) int b = 100; fun3( b ); (d) fun3( 256 ) ; 5.有声明 int fun4( int ); int (*pf)(int) = fun4; 下面选项,正确的调用是( c )。 (a) int a = 15 ;int n = fun4( &a ); (b) int a = 15; cout<<pf(a*3.14); (c) cout<<(*pf)( 256 ); (d) cout << *pf( 256 ); 注意:选项(b)也可以调用函数fun4,但由于实参为浮点型表达式,VC6编译器将出现与形参类型不匹配的警告。 6.在VC,若定义一个函数的返回类型为void,以下叙述正确的是( a,c )。 (a) 用语句调用函数 (b) 用表达式调用函数 (c) 没有返回值 (d) 通过return语句可以返回指定值 7.函数参数的默认值不允许为( c )。 (a) 全局常量 (b) 全局变量 (c) 局部变量 (d) 函数调用 8.使用重载函数编程序的目的是( a )。 (a) 使用相同的函数名调用功能相似的函数 (b) 共享程序代码 (c) 提高程序的运行速度 (d) 节省存贮空间 9.下列的描述( b )是错误的。 (a) 使用全局变量可以从被调用函数获取多个操作结果 (b) 局部变量可以初始化,若不初始化,则系统默认它的值为0 (c) 当函数调用完后,静态局部变量的值不会消失 (d) 全局变量若不初始化,则系统默认它的值为0 10.下列选项,( c ,d )的具有文件作用域。 (a) 语句标号 (b) 局部变量 (c) 全局变量 (d) 静态全局变量 1.以下对一维数组 a 的正确定义是( c )。 (a) int n = 5, a[n]; (b) int a(5); (c) const int n = 5; int a[n]; (d) int n; cin>>n; int a[n]; 2.下列数组定义语句,不合法的是( a )。 (a) int a[3] = { 0, 1, 2, 3 }; (b) int a[] = { 0, 1, 2 }; (c) int a[3] = { 0, 1, 2 }; (d) int a[3] = { 0 }; 3.已知 int a[10] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }, *p = a ;则不能表示数组 a 元素的式子是( c )。 (a) *a (b) *p (c) a (d) a[ p-a ] 4.已知 int a[] = { 0, 2, 4, 6, 8, 10 }, *p = a ; 值不等于0的表达式是( b,d )。 (a) *(p++) (b) *(++p) (c) *(p-- ) (d) *(--p) 5.以下不能对二维数组a进行正确初始化的语句是( c )。 (a) int a[2][3] = { 0 }; (b) int a[][3] = { { 0, 1 }, { 0 } }; (c) int a[2][3] = { { 0, 1 }, { 2, 3 }, { 4, 5 } }; (d) int a[][3] = { 0, 1, 2, 3, 4, 5 }; 6.已知int a[][3] = { { 0, 1 }, { 2, 3, 4 }, { 5, 6 }, { 7 } } ;则 a[2][1]的值是( c )。 (a) 0 (b) 2 (c) 6 (d) 7 7.已知int a[3][3] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 } ; 则不能表示数组元素a[2][1]的地址是( a,b )。 (a) &[2][1] (b) *(a[2]+1) (c) a[2]+1 (d) *(a+2)+1 8.已知char *a[]={ "fortran", " basic", "pascal", "java", "c++" ; 则 cout<<a[3];的显示结果是( c )。 (a) t (b) 一个地址值 (c) java (d) javac++ 9.若用数组名作为调用函数的实参,则传递给形参的是( a )。 (a) 数组存贮首地址 (b) 数组的第一个元素值 (c) 数组全部元素的值 (d) 数组元素的个数 10.在下列选项,( b, d )是错误的。 (a) gets和puts函数可以输入输出包含空格的字符串 (b) cin不能输入包含空格的字符串 (c) cout不能输出包含空格的字符串 (d) 使用赋值运算符可以对字符数组整体赋值 11.下列描述,错误的是( c )。 (a) 输出字符指针就是输出字符串 (b) 输出字符指针的间接引用就是输出单个字符 (c) 具有相同字符的两个字符串常量相等 (d) 两个数组名的比较是地址的比较 12.判断字符串s1和s2是否相等的表达式为( d )。 (a) s1=s2 (b) s1==s2 (c) strcpy(s1,s2)==0 (d) strcmp(s1,s2)==0 13.判断字符串s1是否大于字符串s2的表达式为( c )。 (a) s1>s2 (b) strcmp(s1,s2)==0 (c) strcmp(s1,s2)>0 (d) strcmp(s2,s1)>0 5.若有以下声明和定义,则下列错误的引用是( d )。 struct worker { int no ; char name[ 20 ] ; } w, *p = &w ; (a) w.no (b) p->no (c) (*p).no (d) *p.no 6.若有以下声明和定义,则下列引用非法的是( d )。 struct data { int n; float score; data *q ; }; data a[3] = {1001,87,&a[1],1002,75,&a[2],1003,90,&a[0]}; data *p = a; (a) p->n++ (b) (*p).n++ (c) ++p->n (d) *p->n 7.关于类和对象不正确的说法是( c )。 (a) 类是一种类型,它封装了数据和操作 (b) 对象是类的实例 (c) 一个类的对象只有一个 (d) 一个对象必属于某个类 8.在类定义的外部,可以被访问的成员有( c )。 (a) 所有类成员 (b) private或protected的类成员 (c) public的类成员 (d) public或private的类成员 9.关于this指针的说法错误的是( a,d )。 (a) this指针必须显式说明 (b) 当创建一个对象后,this指针就指向该对象 (c) 成员函数拥有this指针 (d) 静态成员函数拥有this指针 10.声明一个类的对象时,系统自动调用( b,d )函数;撤消对象时,系统自动调用( c )函数。 (a) 成员函数 (b) 构造函数 (c) 析构函数 (d) 复制构造函数 11.下面对构造函数的不正确描述是( b )。 (a) 系统可以提供默认的构造函数 (b) 构造函数可以有参数,所以也可以有返回值 (c) 构造函数可以重载 (d) 构造函数可以设置默认参数 12.下面对析构函数的正确描述是( a,c )。 (a) 系统可以提供默认的析构函数 (b) 析构函数必须由用户定义 (c) 析构函数没有参数 (d) 析构函数可以设置默认参数 13.对静态成员的不正确描述是( c,d )。 (a) 静态成员不属于对象,是类的共享成员 (b) 静态数据成员要在类外定义和初始化 (c) 调用静态成员函数时要通过类或对象激活,所以静态成员函数拥有this指针 (d) 只有静态成员函数可以操作静态数据成员 14.下面选项,不是类的成员函数为( c )。 (a) 构造函数 (b) 析构函数 (c) 友元函数 (d) 复制构造函数 15.下面对友员的错误描述是( d )。 (a) 关键字friend用于声明友员 (b) 一个类的成员函数可以是另一个类的友员 (c) 友员函数访问对象的成员不受访问特性影响 (d) 友员函数通过this指针访问对象成员 1.在下列运算符,能重载的是( a,c,d )。 (a) ! (b) sizeof (c) new (d) delete 2. 在下列运算符,不能重载的是( c )。 (a) <= (b) >> (c) && (d) &= 3.下列关于运算符重载的描述,( d )是正确的。 (a) 可以改变参与运算的操作数个数 (b) 可以改变运算符原来的优先级 (c) 可以改变运算符原来的结合性 (d) 不能改变原运算符的语义 4.下列函数,能重载运算符的函数是( b,c )。 (a) 成员函数 (b) 构造函数 (c) 析构函数 (d) 友员函数 5.不能用友员函数重载的是( a )。 (a) = (b) == (c) += (d) != 6.下面描述,错误的是( b )。 (a) 只有系统预先定义的运算符才可能被重载 (b) 使用类型转换函数不能把一个类转换为另一个类 (c) 使用类型转换函数可以把类转换为基本类型 (d) 类型转换函数只能定义为一个类的成员函数,不能定义为类的友员函数 1.在c++,类与类之间的继承关系具有( c )。 (a) 自反性 (b) 对称性 (c) 传递性 (d) 反对称性 2.下列关于类的继承描述,( a,b )是错误的。 (a) 派生类可以访问基类的所有数据成员,调用基类的所有成员函数 (b) 派生类也是基类,所以基类具有派生类的全部属性和方法 (c) 继承描述类的层次关系,派生类可以具有与基类相同的属性和方法 (d) 一个基类可以有多个派生类,一个派生类可以有多个基类 3.当一个派生类公有继承一个基类时,基类的所有公有成员成为派生类的( a )。 (a) public 成员 (b)private成员 (c) protected成员 (d)友员 4.当一个派生类私有继承一个基类时,基类的所有公有成员和保护成员成为派生类的( b )。 (a) public 成员 (b)private成员 (c) protected成员 (d)友员 5.当一个派生类保护继承一个基类时,基类的所有公有成员和保护成员成为派生类的( c )。 (a) public 成员 (b)private成员 (c) protected成员 (d)友员 6.不论派生类以何种方式继承基类,都不能使用基类的( b )。 (a) public 成员 (b)private成员 (c) protected成员 (d)public 成员和protected成员 7.下面描述,错误的是( b, c )。 (a) 在基类定义的public成员在公有继承的派生类可见,也能在类外被访问 (b) 在基类定义的public和protected成员在私有继承的派生类可见,在类外可以被访问 (c) 在基类定义的public和protected成员在保护继承的派生类不可见 (d) 在派生类不可见的成员要变成可访问的需进行访问声明 8.在c++,不能被派生类继承的函数是( b,c )。 (a) 成员函数 (b)构造函数 (c) 析构函数 (d)静态成员函数 9.在创建派生类对象时,构造函数的执行顺序是( d )。 (a) 对象成员构造函数、基类构造函数、派生类本身的构造函数 (b) 派生类本身的构造函数、基类构造函数、对象成员构造函数 (c) 基类构造函数、派生类本身的构造函数、对象成员构造函数 (d) 基类构造函数、对象成员构造函数、派生类本身的构造函数 10.当不同的类具有相同的间接基类时,有特点( d )。 (a) 各派生类对象将按继承路线产生自己的基类版本 (b) 派生类对象无法产生自己的基类版本 (c) 为了建立惟一的间接基类版本,应该必须改变类格 (d) 为了建立惟一的间接基类版本,应该声明虚继承 1.在C++,要实现动态联编,必须使用( d )调用虚函数。 (a) 类名 (b) 派生类指针 (c) 对象名 (d) 基类指针 2.下列函数,可以作为虚函数的是( c,d )。 (a) 普通函数 (b) 构造函数 (c) 成员函数 (d) 析构函数 3.在派生类,重载一个虚函数时,要求函数名、参数的个数、参数的类型、参数的顺序和函数的返回值( b )。 (a) 不同 (b) 相同 (c) 相容 (d) 部分相同 4.下面函数原型声明,( b )声明了fun为纯虚函数。 (a) void fun()=0; (b)virtual void fun()=0; (c) virtual void fun(); (d)virtual void fun(){ }; 5.若一个类含有纯虚函数,则该类称为( d )。 (a) 基类 (b) 纯基类 (c) 派生类 (d) 抽象类 6.假设 Aclass为抽象类,下列声明( a,c,d )是错误的。 (a) Aclass fun( int ) ; (b)Aclass * p ; (c) int fun( Aclass ) ; (d)Aclass Obj ; 7.下面描述,正确的是( b,d )。 (a) 虚函数是没有实现的函数 (b) 纯虚函数的实现在派生类定义 (c) 抽象类是只有纯虚函数的类 (d) 抽象类指针可以指向不同的派生类 使用虚函数编写程序求球体和圆柱体的体积及表面积。由于球体和圆柱体都可以看作由圆继承而来,所以可以定义圆类circle作为基类。在circle类定义一个数据成员radius和两个虚函数area()和volume()。由circle类派生sphere类和column类。在派生类对虚函数area()和volume()重新定义,分别求球体和圆柱体的体积及表面积。 #include <iostream.h>const double PI=3.14159265;class circle{ public: circle(double r) { radius = r; } virtual double area() { return 0.0; } virtual double volume() { return 0.0; } protected: double radius;};class sphere:public circle{ public: sphere( double r ):circle( r ){ } double area() { return 4.0 * PI * radius * radius; } double volume() { return 4.0 * PI * radius * radius * radius / 3.0; }};class column:public circle{ public: column( double r,double h ):circle( r ) { height = h; } double area() { return 2.0 * PI * radius * ( height + radius ); } double volume() { return PI * radius * radius * height; } private: double height;};void main(){ circle *p; sphere sobj(2); p = &sobj; cout << "球体:" << endl; cout << "体积 = " << p->volume() << endl; cout << "表面积 = " << p->area() << endl; column cobj( 3,5 ); p = &cobj; cout << "圆柱体:" << endl; cout << "体积 = " << p->volume() << endl; cout << "表面积 = " << p->area() << endl;} 2、定义一个Book(图书)类,在该类定义包括 数据成员: bookname(书名)、price(价格)和number(存书数量); 成员函数: display()显示图书的情况;borrow()将存书数量减1,并显示当前存书数量;restore()将存书数量加1,并显示当前存书数量。 在main函数,要求创建某一种图书对象,并对该图书进行简单的显示、借阅和归还管理。 #include <stdlib.h>#include <stdio.h>int main(){class Book{public:long number;float price;char *bookname;void display(){printf("The name of this book is:%s\n",bookname);printf("The price of this book is:%fdolars\n",price);printf("The number of such book is:%d\n",number);}void restore(){number++;}void borrow(){number--;}};Book b;b.bookname="Harry Potter";b.price=18.00;b.number=100;b.display();b.borrow();b.display();b.restore();b.display();system("pause");return 0;} 1. #include <iostream.h> void main() { int a,b,c,d,x; a = c = 0; b = 1; d = 20; if( a ) d = d-10; else if( !b ) if( !c ) x = 15; else x = 25; cout << d << endl; } 2. #include <iostream.h> void main() { int a = 0, b = 1; switch( a ) { case 0: switch( b ) { case 0 : cout << "a=" << a << " b=" << b << endl; break; case 1 : cout << "a=" << a << " b=" << b << endl; break; } case 1: a++; b++; cout << "a=" << a << " b=" << b << endl; } } 3. #include <iostream.h> void main() { int i = 1; while( i<=10 ) if( ++i % 3 != 1 ) continue; else cout << i << endl; } 4、#include < iostream.h > class T { public : T( int x, int y ) { a = x ; b = y ; cout << "调用构造函数1." << endl ; cout << a << '\t' << b << endl ; } T( T &d ) { cout << "调用构造函数2." << endl ; cout << d.a << '\t' << d.b << endl ; } ~T() { cout << "调用析构函数."<<endl; } int add( int x, int y = 10 ) { return x + y ; } private : int a, b ; }; void main() { T d1( 4, 8 ) ; T d2( d1 ) ; cout << d2.add( 10 ) << endl ; } 答案: 调用构造函数1. 4 8 调用构造函数2. 4 8 20 调用析构函数. 调用析构函数. 5. #include < iostream.h > struct data { int n ; float score ; } ; void main() { data a[3] = { 1001,87,1002,72,1003,90 } , *p = a ; cout << (p++)->n << endl ; cout << (p++)->n << endl ; cout << p->n++ << endl ; cout << (*p).n++ << endl ; } 6. #include < iostream.h > struct node { char * s ; node * q ; } ; void main() { node a[ ] = { { "Mary", a+1 }, { "Jack", a+2 }, { "Jim", a } } ; node *p = a ; cout << p->s << endl ; cout << p->q->s << endl ; cout << p->q->q->s << endl ; cout << p->q->q->q->s << endl ; }
标题:PHP基础教程 出处:风流的CG网络日志 时间:Mon, 28 Aug 2006 07:24:34 +0000 作者:yufeng 地址:http:///read.php?38 内容: 提供给新手学习的 PHP新手教程,是一个比较有价值的PHP新手教程! 一、PHP简介 PHP是一种易于学习和使用的服务器端脚本语言。只需要很少的编程知识你就能使用PHP建立一个真正交互的WEB站点。本教程并不想让你完全了解这种语言,只是能使你尽快加入开发动态web站点的行列。我假定你有一些HTML(或者HTML编辑器)的基本知识和一些编程思想。 1.简介 PHP是能让你生成动态网页的工具之一。PHP网页文件被当作一般HTML网页文件来处理并且在编辑时你可以用编辑HTML的常规方法编写PHP。 PHP代表:超文本预处理器(PHP: Hypertext Preprocessor)。PHP是完全免费的,不用花钱,你可以从PHP官方站点(http://www.php.net)自由下载。PHP遵守GNU公共许可(GPL),在这一许可下诞生了许多流行的软件诸如Linux和Emacs。你可以不受限制的获得源码,甚至可以从加进你自己需要的特色。PHP在大多数Unix平台,GUN/Linux和微软Windows平台上均可以运行。怎样在Windows环境的PC机器或Unix机器上安装PHP的资料可以在PHP官方站点上找到。安装过程很简单。 如果你的机器解决了2000问题,那么PHP也一样没有千年虫问题! 1.1 历史 三年前,Rasmus Lerdorf为了创建他的在线简历而创造了"个人主页工具"(Personal Home Page Tools)。这是一种非常简单的语言。其后越来越多的人们注意到了这种语言并对其扩展提出了各种建议。在许多人的无私奉献下以及这种语言本身的源代码自由性质,它演变成为一种特点丰富的语言,而且现在还在成长。 PHP虽然很容易学习,但是速度上比mod_perl(植入web服务器的perl模块)慢。现在有了可以与mod_perl速度想媲美的被称作Zend的新引擎,而PHP4就可以充分利用这个引擎。PHP4还处在BETA测试阶段。Andy Gutmans和Zeev Suraki是Zend的主要作者。可以去Zend站点(http://www.zend.com)了解更多。 PHP的应用在个人性质的web工程增长显著。根据Netcraft在1999年10月的报告,有931122个域和321128个IP地址利用PHP技术。 1.2 PHP的先进之处 应用PHP有许多好处。当然已知的不利之处在于PHP由于是开放源码项目,没有什么商业支持,并且由此而带来的执行速度缓慢(直到PHP4之前)。但是PHP的邮件列表很是有用而且除非你正在运行像Yahoo!或者Amazon.com这样的极受欢迎的站点,你不会感觉出PHP的速度与其他的有什么不同。最起码我就没有感觉出来!好了,让我们来看看PHP有那些优点: - 学习过程 我个人更喜欢PHP的非常简单的学习过程。与Java和Perl不同,你不必把头埋进100多页的文档努力学习才可以写出一个象样的程序。只要了解一些基本的语法和语言特色,你就可以开始你的PHP编码之旅了。之后你在编码过程如果遇到了什么麻烦,还可以再去翻阅相关文档。 PHP的语法与C,Perl,ASP或者JSP。对于那些对上述之一的语言较熟悉的人来说,PHP太简单了。相反的,如果你对PHP了解较多,那么你对于其他几种语言的学习都很简单了。 你只需要30分钟就可以将PHP的核心语言特点全部掌握,你可能已经非常了解HTML,甚至你已经知道怎样用编辑设计软件或者手工来制作好看的WEB站点。由于PHP代码能够无障碍的添加进你的站点,在你设计和维护站点的同时,你可以很轻松的加入PHP使得你的站点更加具有动态特性。 - 数据库连接 PHP可以编译成具有与许多数据库相连接的函数。PHP与MySQL是现在绝佳的组合。你还可以自己编写外围的函数取间接存取数据库。通过这样的途径当你更换使用的数据库时,可以轻松的更改编码以适应这样的变化。PHPLIB就是最常用的可以提供一般事务需要的一系列基库。 - 可扩展性 就像前面说的那样,PHP已经进入了一个高速发展的时期。对于一个非程序员来说为PHP扩展附加功能可能会比较难,但是对于一个PHP程序员来说并不困难。 - 面向对象编程 PHP提供了类和对象。基于web的编程工作非常需要面向对象编程能力。PHP支持构造器、提取类等。 - 可伸缩性 传统上网页的交互作用是通过CGI来实现的。CGI程序的伸缩性不很理想,因为它为每一个正在运行的CGI程序开一个独立进程。解决方法就是将经常用来编写CGI程序的语言的解释器编译进你的web服务器(比如mod_perl,JSP)。PHP就可以以这种方式安装,虽然很少有人愿意这样以CGI方式安装它。内嵌的PHP可以具有更高的可伸缩性。 - 更多特点 PHP的开发者们为了更适合web编程,开发了许多外围的流行基库,这些库包含了更易用的层。你可以利用PHP连接包括Oracle,MS-Access,Mysql在内的大部分数据库。你可以在苍蝇上画图,编写程序下载或者显示e-mail。你甚至可以完成网络相关的功能。最好的是,你可以选择你的PHP安装版本需要哪些功能。引用Nissan的Xterra的话来说就是PHP可以做到你想让它做到的一切而且无所不能! 1.3 竞争对手:ASP,mod_perl,JSP 我当然不清楚ASP/JSP能做些什么。不过明确的是编写那样的代码有多简单,购买它们会有多昂贵以及它们需要多么昂贵和强大的硬件。如果你有什么立的观点(比如说没有被SUN和Microsoft的百万美金所影响),请顺便通知我。 据我所知,JSP基于Java,因此Java程序员可以轻松开始编码。ASP只是一个一般的引擎,具有支持多种语言的能力,不过默认的并且是最常用的还是VBScript。 mod_perl与Perl一样强大,只是更快一些。 二、PHP入门 PHP站点的在线教程已经很棒了。在那里还有一些其他教程的链接。而本文的该部分将让你对PHP熟悉一点。我不可能做到没有任何遗漏,我的目的只在于能让你迅速开始你的PHP编程。 2.1 首要条件 你首先必须要有一个正在工作着的支持PHP的web服务器。我假定在你的服务器上所有PHP文件的扩展名为.php3。 2.2 PHP的安装 生成一个名为test.php3的文件,含有以下内容: 然后在你的浏览器打开此文件。看看这个页面你就知道你的PHP安装使用的选项了。 2.3 语法 就像前面提到的一样,你可以混合编写你的PHP代码和HTML代码。因此你必须有办法将两者区别开来。以下就是你可以采用的几种方法。你可以选用其一种你最适应的并且就这样坚持这种方法! 从HTML分离 以下是可以使用的方法: . . . 语句 与Perl和C一样,在PHP用(;)来分隔语句。那些从HTML分离出来的标志也表示语句的结束。 注释 PHP支持C,C++和Unix风格的注释方式: /* C,C++风格多行注释 */ // C++风格单行注释 # Unix风格单行注释 Hello,World! 通过我们已经学过的知识,你可以编写一个最简单的程序输出一个也许是程序世界最有名的词语: First PHP page 2.4 数据类型 PHP支持整数、浮点数、字符串、数组和对象。变量类型通常不由程序员决定而由PHP运行过程决定(真是好的解脱!)。但是类型也可以被函数cast或者settype()明确的设定。 数值 数值类型可以是整数或是浮点数。你可以用以下的语句来为一个数值赋值: $a = 1234; # 十进制数 $a = -123; # 负数 $a = 0123; # 八进制数 (等于十进制数的83) $a = 0x12; # 十六进制数(等于十进制数的18) $a = 1.234; # 浮点数"双精度数" $a = 1.2e3; # 双精度数的指数形式 字符串 字符串可以由单引号或双引号引出的字段定义。注意不同的是被单引号引出的字符串是以字面定义的,而双引号引出的字符串可以被扩展。反斜杠(\)可以被用来分割某些特殊字符。举例如下: $first = 'Hello'; $second = "World"; $full1 = "$first $second"; # 产生 Hello World $full2 = '$first $second';# 产生 $first $second 可以将字符和数字利用运算符号连接起来。字符被转化成数字,利用其最初位置。在PHP手册有详细的例子。 数组与哈希表 数组与哈希表以同样的方法被支持。怎样运用取决于你怎样定义它们。你可以用list()或者array()来定义它们,也可以直接为数组赋值。数组的索引从0开始。虽然我在这里没有说明,但是你一样可以轻易的使用多维数组。 // 一个包含两个元素的数组 $a[0] = "first"; $a[1] = "second"; $a[] = "third"; // 添加数组元素的简单方法 // 现在$a[2]被赋值为"third" echo count($a); // 打印出3,因为该是数组有3个元素 // 用一个语句定义一个数组并赋值 $myphonebook = array ( "sbabu" => "5348", "keith" => "4829", "carole" => "4533" ); // 噢,忘了教长吧,让我们添加一个元素 $myphonebook["dean"] = "5397"; // 你定义的carale元素错了,让我们更正它 $myphonebook["carole"] => "4522" // 我还没有告诉你怎样使用数组的相似支持方式吗?让我们看一看 echo "$myphonebook[0]"; // sbabu echo "$myphonebook[1]"; // 5348 其他一些对数组或哈希表有用的函数包括sort(),next(),prev()和each()。 对象 使用new语句产生一个对象: class foo { function do_foo () { echo "Doing foo."; } } $bar = new foo; $bar->do_foo(); 改变变量类型 在PHP手册提到:"PHP不支持(也不需要)直接在声明变量时定义变量类型;变量类型将根据其被应用的情况决定。如果你为变量var赋值为一个字符串,那么它变成了一个字符串。如果你又为它赋了整数值,那么它就变成了整数。" $foo = "0"; // $foo是字符串(ASCII 48) $foo++; // $foo是字符串"1" (ASCII 49) $foo += 1; // $foo现在是整数(2) $foo = $foo + 1.3; // $foo是一个双精度数(3.3) $foo = 5 + "10 Little Piggies"; // $foo是一个整数(15) $foo = 5 + "10 Small Pigs"; // $foo是一个整数(15) 如果想要强行转换变量类型,可以使用与C语言相同的函数settype()。 2.5 变量与常量 可能你已经注意到,变量都有一个美元符号($)的前缀。所有变量都是局部变量,为了使得定义的函数可以使用外部变量,使用global语句。而你要将该变量的作用范围限制在该函数之内,使用static语句。 $g_var = 1 ; // 全局范围 function test() { global $g_var; // 这样就可以声明全局变量了 } 更先进一些的是变量的变量表示。请参考PHP手册。这在有时会显得很有用。 PHP内置了许多已定义的变量。你也可以用define函数定义你自己的常量,比如define("CONSTANT","value")。 2.6 运算符 PHP具有C,C++和Java的通常见到的运算符。这些运算符的优先权也是一致的。赋值同样使用"="。 算术和字符 以下只有一种运算符是有关字符的: $a + $b :加 $a - $b :减 $a * $b :乘 $a / $b :除 $a % $b :取模(余数) $a . $b :字符串连接 逻辑和比较 逻辑运算符有: $a || $b :或 $a or $b :或 $a && $b :与 $a and $b :与 $a xor $b :异或 (当$a或$b为true时为true,两者一样时为false) ! $a :非 比较运算符有: $a == $b :相等 $a != $b :不等 $a < $b :小于 $a $b :大于 $a >= $b :大于等于 与C一样PHP也有三重运算符(?:)。位操作符在PHP同样存在。 优先权 就和C以及Java一样! 2.7 控制流程结构 PHP有着与C一样的流程控制。我将在下面大概介绍。 if, else, elseif, if(): endif if (表达式一) { . . . } elseif (表达式二) { . . . } else { . . . } // 或者像Python一样 if (表达式一) : . . . . . . elseif (表达式二) : . . . else : . . . endif ; Loops. while, do..while, for while (表达式) { . . . } do { . . . } while (表达式); for (表达式一; 表达式二; 表达式三) { . . . } //或者像Python一样 while (expr) : . . . endwhile ; switch switch是对多重if-elseif-else结构的最好的替换: switch ($i) { case 0: print "i equals 0"; case 1: print "i equals 1"; case 2: print "i equals 2"; } break, continue break断当前的循环控制结构。 continue被用来跳出剩下的当前循环并继续执行下一次循环。 require, include 就像C的#include预处理一样。你在require指定的那个文件将替代其在主文件的位置。在有条件的引用文件时,可以使用include()。这样就使得你可以将复杂的PHP文件分割成多个文件并且在不同需要时分别引用它们。 2.8 函数 你可以像以下的例子一样定义自己的函数。函数的返回值可以是任何数据类型: function foo (变量名一, 变量名二, . . . , 变量名n) { echo "Example function.\n"; return $retval; } 所有PHP代码都可以出现在函数定义,甚至包括对其他函数和类的定义。函数必须在引用之前定义。 2.9 类 利用类模型建立类。可以参考PHP手册对类的详细解释。 class Employee { var $empno; // 员工人数 var $empnm; // 员工姓名 function add_employee($in_num, $in_name) { $this->empno = $in_num; $this->empnm = $in_name; } function show() { echo "$this->empno, $this->empnm"; return; } function changenm($in_name) { $this->empnm = $in_name; } } $sbabu = new Employee; $sbabu->add_employee(10,"sbabu"); $sbabu->changenm("babu"); $sbabu->show(); 三、从实例入手  PHP的许多特点与其他软件或者工具有关。利用迄今为止我们所学到的PHP知识,我们可以试着建立一个简单交互的网站。利用这一过程我们又可以学到不少东西。好吧,我们现在开始专注于一个典型个人网站的建设。 3.1 计划一个站点 一般一个个人站点包括一个欢迎页面、一个留言本页面、一个书签链接页面、一个计数器、联系信息,甚至还有照片集和一些音乐文件等等。让我们从一个标题页面、一个联系信息页面和一个简历页面开始。我们同样需要标准的通用的页面头部和底部。 标题页面--front.html 这里我们有一个非常简单的html文件: 我的个人主页--欢迎 我的个人主页 欢迎 欢迎来我的寒舍,虽然这里现在暂时还没有什么。 不过我希望马上就可以多起来。 Copyright ? 我自己,1999 联系信息页面--count.html 同样我们又有了一个简单页面: 我的个人主页--联系信息 我的个人主页 联系信息 你可以通过1-800-PHP-INFO联系我 Copyright ? 我自己,1999 3.2 HTML到PHP 从上面你可以看出,每个页面有相同的头部和底部。像上面那样每个页面都写入相同的信息在工作量少的时候还可以,但是想象一下当有100多页面且你需要全部更改其头部或底部时你要花费多大精力?一页一页的手工更改是一件多么冗长无趣的事情啊!所以我们应该为这些页面编写PHP的头部和底部文件,之后我们只要在每个HTML页面引用它们就行了。我们将把这些include文件放在一个叫include的子目录下。下面我们就把这些站点的通用内容写进文件。 全站通用变量设定:common.inc <? // 全站通用变量 $MyEmail = "[email protected]"; $MyEmailLink = "$MyEmail"; $MyName = "PHP Talk"; $MySiteName = $MyName."'s Home Page"; ?> 通用页面头部:header.inc 通用页面底部:footer.inc Copyright ? by , 1999 新的页面front.php3: 欢迎来我的寒舍,虽然这里现在暂时还没有什么。 不过我希望马上就可以多起来。 新的cont.php3: 你可以通过1-800-PHP-INFO联系我 现在你就可以猜出这样安排的好处了。如果你想改动页面的头部或者底部,你只需要改动相应的文件就可以了。如果你要修改你的e-mail地址甚至你的名字,只要修改common.inc文件就行了。另外值得注意的是你可以把具有任何文件名或者文件扩展名的文件包含进你的文件,你甚至可以包含其他站点上的文件。 3.3 计数器 让我们在首页上加上一个计数器。这个例子已经被讲过多次了,但是还是有利于演示怎样读写文件以及创建自己的函数。counter.inc包含以下代码: 然后我们更改front.php3文件以显示这个计数器: <? include("include/counter.inc"); // 我把计数值放在文件counter.txt,读出并输出 printf ("%06d \n", get_hitcount("counter.txt")); include("include/footer.inc"); ?> 看看我们的新front.php3 3.4 反馈表单 让我们再添加一个反馈表单以便你的浏览者填写并e-mail给你。举例来说我们用一种很简单的方法实现它,我们只需要两个页面:一个为浏览者提供输入表单;一个获得表单数据并处理、mail给你。 PHP获取表单数据是很简单的。当一个表单被发送后,表单所包含的各个元素被赋上了相应的值,而这样就可以像引用一般变量一样使用了。 在process_form.php3,变量$mytext就被赋予了输入的值--非常简单!同样的,你可以从列表框、多选框、单选框、按钮等表单元素取得变量值。你唯一要做的就是为表单的每一个元素取名以便将来可以引用。 根据这个方法,我们可以生成一个简单的包含三个元素的表单:姓名、e-mail地址和留言。当浏览者发送表单后,处理该表单的PHP页面(sendfdbk.php3)读取数据,检查姓名是否为空,最后将数据mail给你。 表单:form.php3 Your feedback on my home page. 处理表单:sendfdbk.php3 <? include("include/common.inc"); $title = "Feedback"; include("include/header.inc"); if ( $name == "" ) { // 现在我很讨厌匿名的留言! echo "Duh ? How come you are anonymous?"; } elseif ($name == "Your name") { // 这个浏览者真是不想透露姓名啊! echo "Hello ? Your name is supposed to be replaced with your actual name!"; } else { // 输出一段礼貌的感谢语 echo " Hello, $name. Thank you for your feedback. It is greatly appreciated. Thanking you $MyName $MyEmailLink "; // 最后mail出去 mail($MyEmail, "Feedback."," Name : $name E-mail : $email Comment : $comment "); } include("include/footer.inc"); ?> 3.5 简单的站内搜索引擎 PHP可以调用外部程序。在Unix环境下我们可以利用程序grep实现一个简单的搜索引擎。我们可以做的稍微复杂一些:使用一个页面既输出一个表单供用户输入搜索字串又输出查询结果。 <FORM ACTION="" METHOD="POST"> <INPUT TYPE="text" NAME="searchstr" value="" SIZE="20" MAXLENGTH="30"> <? if ( ! empty($searchstr) ) { // empty()用来检查查询字串是否为空 // 如果不为空,调用grep查询 echo "\n"; // 调用grep对所有文件进行大小写非敏感模式的查询 $cmdstr = "grep -i $searchstr *"; $fp = popen( $cmdstr, "r" ); // 执行命令并输出管道 $myresult = array(); // 存储查询结果 while( $buffer = fgetss ($fp, 4096)) { // grep返回这样格式: 文件名:匹配字串出现行数 // 因此我们利用函数split()分离处理数据 list($fname, $fline) = split(":",$buffer, 2); // 我们只输出第一次匹配的结果 if ( !defined($myresult[$fname])) $myresult[$fname] = $fline; } // 现在我们将结果存储在数组,下面就可以处理并输出了 if ( count($myresult) ) { echo "\n"; while(list($fname,$fline) = each($myresult)) echo " $fname : $fline \n"; echo "\n"; } else { // 如果没有查询结果 echo "Sorry. Search on $searchstr returned no results.\n"; } pclose($fp); } ?> 注释: PHP_SELF是PHP内建的变量。包含当前文件名。 fgets()按行读取文件,最多4096(指定)字符长度。 fgetss()与fgets()相似,只是解析输出的HTML标记。 split()有一个参数是2,因为我们只需要把输出分成两部分。另外需要省略":"。 each()是一个数组操作函数,用来更方便的遍历整个数组。 popen()、pclose()与fopen()、fclose()的功能很相似,只是增加了管道处理。 请注意以上的代码并不是实现一个搜索引擎的好办法。这只是有助于我们更好学习PHP而举出的一个例子而已。理想的情况是你应该建立一个包含关键字的数据库然后进行搜索 四、与数据库链接 通过PHP你可以轻松的连接到数据库,请求数据并将其显示在你的web站点,甚至修改数据库的数据。MySQL是一种很流行的数据库,并且在互联网有许多有关PHP与MySQL的教程。MySQL是免费的,这一点也许就吸引了不少人。由于其广泛应用,我就不想在这里赘述MySQL的使用方法了。Oracle被大量在企业应用采用,因此我们就利用Oracle来介绍PHP与数据库的连接。我们当然不会提及Oracle数据库的设计原理,原因是这已经超出了我们的讨论范围。 PHP提供了两套函数与Oracle连接,分别是ORA_和OCI函数。其ORA_函数略显陈旧。OCI函数更新据说更好一些。两者的使用语法几乎相差无几。如前所述,你的PHP安装选项应该可以支持两者的使用。 想获得更多有关在Microsoft Windows平台上安装支持PHP3的Apache服务器的知识以及更多有关Oracle数据库的知识,请查阅以下URL:www.csoft.net/~vsbabu/articles/oraphp.html。 4.1 连接 <? if ($conn=Ora_Logon("user@TNSNAME","password")) { echo "SUCCESS ! Connected to database\n"; } else { echo "Failed :-( Could not connect to database\n"; } Ora_Logoff($conn); phpinfo(); ?> 以上代码使用TNSNAME(在你的tnsnames.ora文件指明)定义的Oracle数据库名称、用户名称和密码连接数据库。在成功连接的基础上,ora_logon函数返回一个非零的连接ID并储存在变量$conn。 4.2 查询 假设与数据库已经连接就绪,下面我们就来实际的应用对数据库的查询。下面的代码演示了一个连接并查询的典型例子: <? /* * 连接数据库并执行查询 */ function printoraerr($in_cur) { // 检查Oracle是否出错 // 如果存在错误则显示 // 当指针被激活时每次请求Oracle后调用该函数 if(ora_errorcode($in_cur)) echo "Oracle code - ".ora_error($in_cur)."\n"; return; } /** 主程序 */ if (!($conn=ora_logon("user@TNSNAME","password"))) { echo "Connection to database failed\n"; exit; } echo "Connected as connection - $conn\n"; echo "Opening cursor ...\n"; $cursor=ora_open($conn); printoraerr($cursor); echo "Opened cursor - $cursor\n"; $qry="select user,sysdate from dual"; echo "Parsing the query $qry ...\n"; ora_parse($cursor,$qry,0); printoraerr($cursor); echo "Query parsed \n"; echo "Executing cursor ...\n"; ora_exec($cursor); printoraerr($cursor); echo "Executed cursor\n"; echo "Fetching cursor ...\n"; while(ora_fetch($cursor)) { $user=ora_getcolumn($cursor,0); printoraerr($cursor); $sysdate=ora_getcolumn($cursor,1); printoraerr($cursor); echo " row = $user, $sysdate \n"; } echo "Fetched all records\n"; echo "Closing cursor ...\n"; ora_close($cursor); echo "Closed cursor\n"; echo "Logging off from oracle... \n"; ora_logoff($conn); echo "Logged off from oracle \n"; ?> (译者注:以上代码段缺少注释,请读者参考PHP Manual的Oracle数据库函数部分) 4.3 显示结果 以下代码演示了怎样查询数据库并将结果输出: <? function printoraerr($in_cur, $conn) { // 检查Oracle是否出错 // 如果存在错误则显示 // 当指针被激活时每次请求Oracle后调用该函数 // If it encountered an error, we exit immediately if(ora_errorcode($in_cur)) { echo "Oracle code - ".ora_error($in_cur)."n"; ora_logoff($conn); exit; } return; } function exequery($w_qry,$conn) { $cursor=ora_open($conn); printoraerr($cursor,$conn); ora_parse($cursor,$w_qry,0); printoraerr($cursor,$conn); ora_exec($cursor); printoraerr($cursor,$conn); $numrows=0; $w_numcols=ora_numcols($cursor); // 显示头部 echo " \n"; for ($i=0;$i<$w_numcols;$i++) { $align=(ora_columntype($cursor,$i)=="NUMBER")?"RIGHT":"LEFT"; echo "\t".ora_columnname($cursor,$i)."\n"; } echo "\n"; while(ora_fetch($cursor)) { echo "\n"; for ($i=0;$i<$w_numcols;$i++) { $align=(ora_columntype($cursor,$i)=="NUMBER")?"RIGHT":"LEFT"; if(ora_columntype($cursor,$i)=="LONG") echo "". ora_getcolumn($cursor,$i)."\n"; else echo "".ora_getcolumn($cursor,$i)."\n"; printoraerr($cursor,$conn); } $numrows++; echo "\n"; } if ($numrows==0) echo "Query returned no records \n"; else { echo "\n"; echo "Count\n"; echo "$numrows\n"; echo "\n"; } echo "\n"; ora_close($cursor); return; } // 主程序 if(!($conn=ora_logon("user@SID","password"))) { echo "Error: Cannot connect to database\n"; exit; } $qry="SELECT deptno \"Dept\" ,empno \"Emp\" ,empnm \"Name\" ,salary \"Salary\" FROM employee ORDER BY 1,2"; exequery($qry); ora_logoff($conn); ?> (译者注:以上代码段缺少注释,请读者参考PHP Manual的Oracle数据库函数部分) 4.4 基于HTTP的Oracle登录 将以下代码加在PHP页面代码之前以确认Oracle登录。注意你必须正确设定$ SID。 <? if(!isset($PHP_AUTH_USER)) { Header("WWW-authenticate: basic realm=\"$SID\""); Header("HTTP/1.0 401 Unauthorized"); $title="Login Instructions"; echo " You are not authorized to enter the site \n"; exit; } else { if (!($conn=ora_logon("$PHP_AUTH_USER@$SID",$PHP_AUTH_PW))) { Header("WWW-authenticate: basic realm=\"$SID\""); Header("HTTP/1.0 401 Unauthorized"); $title="Login Instructions"; echo " You are not authorised to enter the site \n"; exit; } } ?> 五、其它功能 5.1 生成图像 PHP可以操作处理图像。如果你已经安装了GD库,你甚至可以利用PHP生成图像。 (译者注:以上代码段缺少注释,请读者参考PHP Manual的图像处理函数部分) 这段代码在其他页面通过以下标记调用,然后以上的那段button.php3代码取得text值并在另外取得的图像文件加上该值--在以上的代码该图像文件是images/button1.gif--最后输出到浏览器。假如你想在表单域使用图像按钮,但是又不希望在每次按钮上的文字改变后不得不重新生成新的图像,就可以利用这样简单的方法动态生成图像文件。 5.2 Cookies PHP支持基于HTTP的cookies。在需要时你可以像使用一般变量一样方便的使用cookie。Cookies是浏览器保存于客户端的一些信息片段,由此你可以知道是否一台特定PC上的任何人都访问过你的站点,浏览者者在你的站点上的踪迹等等。使用cookies的典型例子就是对浏览者偏好的甄别。Cookies由函数setcookie()设定。与输出HTTP标头的函数header()一样,setcookie()必须在任何实际内容杯输出到浏览器之前调用。以下是一个简单例子: <? if (empty($VisitedBefore)) { // 如果没有设定cookie,为cookie赋上当前时间值 // 函数的最后一个参数声明了该cookie保存的时间 // 在这个例子是1年 // time()函数返回自1970年1月1日以来的以秒数计的时间 SetCookie("VisitedBefore",time(), time()+(60*60*24*365)); } else { // 欢迎浏览者再次光临 echo "Hello there, welcome back"; // 读取cookie并判断 if ( (time() - $VisitedBefore) >= "(60*60*24*7)" ) echo "Why did you take a week to come back. You should be here more often!? "; } ?> 5.3 基于HTTP验证 基于HTTP验证当PHP以CGI模式运行时不能实现。我们可以使用函数header()发送HTTP标头强制验证,客户端浏览器则弹出供输入用户名和密码的对话框。这两个变量被储存在$PHP_AUTH_USER和$PHP_AUTH_PW,你可以使用这两个变量验证合法并允许进入。以下的例子通过用户名称/密码对为tnc/nature的验证一名用户的登录: 事实上再实际引用不大可能如上面使用代码段明显的用户名称/密码对,而是利用数据库或者加密的密码文件存取它们。 5.4 文件上传 你可以利用PHP实现文件的功能,注意客户端的浏览器应该是Netscape3以上或者IE3以上。以下就是该功能的简单演示: ( upload.html ): Upload Your File (You may notice a slight delay while we upload your file.) 下面是处理上传的文件: ( receiver.php3 ): 2000000 ) { $error_msg = "Sorry, your file is too large."; return; } $the_time = time (); // 你需要对以下目录有写权限 $upload_dir = "/local/uploads"; $local_file = "$upload_dir/$the_time"; if ( file_exists ( '$local_file' ) ) { $seq = 1; while ( file_exists ( "$upload_dir/$the_time$seq" ) ) { $seq++; } $local_file = "$upload_dir/$the_time$seq"; }; rename ( $uploadfile, $local_file ); display_page (); } function display_page () { // 这里是你的页面内容 } php3 Receiving Script <? if ( $error_msg ) { echo "$error_msg"; } if ( $sendit ) { do_upload (); } elseif ( $cancelit ) { header ( "Location: $some_other_script" ); exit; } else { some_other_func (); } ?> 5.5 常用函数 我们简单来看看一些常用的函数。 数组 array - 生成数组 count - 数组元素个数 sort - 数组排序,另有其他几种排序函数可供使用 list - 列出数组元素 each - 返回下一个key/value对 current - 返回当前数组元素 next,prev - 传回当前数组元素前后指针 日期和时间 checkdate - 验证日期/时间格式 date - 生成日期/时间格式 time - 当前时间信息 strftime - 格式化日期/时间 目录、文件系统 chdir - 改变目录 dir - 目录类别 opendir, readdir, closedir - 开启、读取、关闭目录 fopen, fclose - 开启、关闭文件 fgets, fgetss - 逐行读取内容 file - 将整个文件读入一个数组变量 正则表达式 ereg - 匹配正则表达式 eregi - 大小写非敏感匹配正则表达式 ereg_replace -匹配正则表达式并替换 eregi_replace -大小写非敏感匹配正则表达式并替换 split - 依规则切开字符串并以数组形势存储 字符串 AddSlashes - 加上斜杠后使用字符串 echo - 输出一个或多个字符串 join, implode - 将数组元素合并为字符串 htmlentities, htmlspecialchars - 将HTML特殊字符转换为HTML标记形式 split - 依规则切开字符串并以数组形势存储 5.6 扩展我们的范例主页 我们将使用以上提到的一些函数和思想为我们的范例主页添加更多的动态内容。我们可以在每个页面的顶部加上导航栏,同时使得当前页自动的不被链接显示;同时还可以添加一个用户验证表单以便上传音乐、图像等文件并自动更新页面。 导航栏 实际上就是在footer.inc文件加上一段代码。假设你的web站点所有后缀为.php3的文件都会出现在导航栏,以下就是被存为include/navbar.inc的代码: <? /* 输出该导航栏,链接所有除当前页的站内.php3文件 */ # 读取目录 $d = dir("./"); echo " | \n"; while($entry = $d->read()) { // 忽略无文件情况 if ( !is_file($entry) ) continue; /* 将文件名与扩展名分开。由于.是正则表达式特殊字符,应该用\引出 */ list($filenm, $fileext) = split("\.",$entry, 2); // 忽略非.php3文件情况 if( $fileext != "php3" ) continue; /* 现在我们已经把.php3文件都选出,下面搜寻文件的第一行(标题) 类似$title="something"; 并将以上标题内容分开,用作链接文字 */ $linknm = ""; $fp=fopen($entry,"r"); while($buffer=fgets($fp, 4096)) { $buffer = trim($buffer); // 我们已经把每个文件的标题放在文件的第一行以便搜索 // 但是当你改变变量名称时可能会带来大麻烦 if (ereg("title *= *\"", $buffer)) { /* 我们已经取得了标题内容并可以在此基础上 进行去除空格等处理。 必须以PHP代码方式处理,比如$title = "blah blah" */ eval($buffer); // 然后将链接文字显示为标题文字 $linknm = $title; break; } } fclose($fp); if ( $entry == basename($PHP_SELF) ) echo "$linknm"; else echo "$linknm"; echo " | "; } $d->close(); echo " \n"; ?> 照片收藏夹 我们将引用基于HTTP的验证、文件系统函数和文件上传功能维护放置图像文件的目录。 同时我们需要建立一个可以列出在该目录下所有照片的页面。 文件上传 <? include("include/common.inc"); // 我们在这里再做一次用户验证 if(!isset($PHP_AUTH_USER)) { Header("WWW-Authenticate: Basic realm=\"$MySiteName\""); Header("HTTP/1.0 401 Unauthorized"); echo "Sorry, you are not authorized to upload files\n"; exit; } else { if ( !($PHP_AUTH_USER==$MyName && $PHP_AUTH_PW==$MyPassword ) ) { // 如果是错误的用户名称/密码对,强制再次认证 Header("WWW-Authenticate: Basic realm=\"My Realm\""); Header("HTTP/1.0 401 Unauthorized"); echo "ERROR : $PHP_AUTH_USER/$PHP_AUTH_PW is invalid."; exit; } } if ( $cancelit ) { // 当浏览者按下"取消"按钮则转向首页面 header ( "Location: front_2.php3" ); exit; } function do_upload () { global $userfile, $userfile_size, $userfile_name, $userfile_type; global $local_file, $error_msg; global $HTTP_REFERER; if ( $userfile == "none" ) { $error_msg = "You did not specify a file for uploading."; return; } if ( $userfile_size > 2000000 ) { $error_msg = "Sorry, your file is too large."; return; } // Wherever you have write permission below... $upload_dir = "photos"; $local_file = "$upload_dir/$userfile_name"; if ( file_exists ( $local_file ) ) { $error_msg = "Sorry, a file with that name already exists"; return; }; // 你还可以由此检查文件名称/类型对以确定是何种文件:gif,jpg,mp3… rename($userfile, $local_file); echo "The file is uploaded\n"; echo "Go Back\n"; } $title = "Upload File"; include("include/header.inc"); if (empty($userfile) || $userfile=="none") { // 输出以下表单 ?> <FORM ACTION="" ENCTYPE="multipart/form-data" METHOD=POST> (You may notice a slight delay while we upload your file.) <? } else { if ( $error_msg ) { echo "$error_msg"; } if ( $sendit ) { do_upload (); } } include("include/footer.inc"); ?> 照片图库 Here are some of our family photos. This PHP script can really be made better, by splitting into multiple pages. read()) { if (is_file("photos/$entry")) echo "\n"; } $d->close(); ?> 另外,你可以在文件上传的表单加上一个输入元素去描述该上传的文件。这个元素将被存储在文件,然后被以上的照片图库的那段代码所读出并显示出来。 六、网络资源 你可以通过web上的众多资源更多的了解PHP3。许多邮件列表和书籍对你都非常有用。 6.1 站点 PHP的爆炸性流行使得一夜之间出现了很多基于PHP的站点,其不少站点有在线教程、范例代码、技巧和提示等内容。 国内 http://www.phpuser.com - PHP文用户,也就是这里了 http://www.phpx.com - 国PHP联盟 http://www.phpsite.net - PHP专门站 http://www.phpchina.com - PHP CHINA http://www.cpcw.com/netschool/homepage/cgi/ - 电脑报网页陶吧 国外 http://www.php.net/ - PHP官方站点 http://www.devshed.com/ - 极好的教程 http://px.sklar.com - 代码交换 http://www.phpbuilder.com/ - 教程、专栏和邮件列表档案 http://www.weberdev.com/ - 文章和代码 http://www.phpwizard.net/ - 提示与技巧 http://www.iometrics.com/php/phplist.php3/ - IOMetrics scripts的档案 http://www.e-gineer.com/phpkb/ - PHP知识库 6.2 邮件列表 你可以在PHP官方站点的"支持"栏目内登记获得以下的邮件列表。值得注意的是这些都是高流量流表,一般每天会有100份e-mail。 [email protected] - 主要的列表 [email protected] - 主要针对开发者 [email protected] - 本站的邮件列表,与论坛相通 6.3 引人注目的工程 一些基于PHP的工程已经发展得比较完善。其一些更出色更引人注目的是: http:// phplib.netuse.de - PHPLib,一整套PHP函数库 http://www.phorum.org - Phorum是一个很完善的BBS系统 http://www.fishcartsql.org - FishCartSQL是一个电子商务解决方案 http://www.midgard-project.org - Midgard是一个网络应用开发平台 Generated by Bo-blog 2.0.2 sp2
主体:(一) 一、C++概述 (一) 发展历史 1980年,Bjarne Stroustrup博士开始着手创建一种模拟语言,能够具有面向对象的程序设计特色。在当时,面向对象编程还是一个比较新的理念,Stroustrup博士并不是从头开始设计新语言,而是在C语言的基础上进行创建。这就是C++语言。 1985年,C++开始在外面慢慢流行。经过多年的发展,C++已经有了多个版本。为次,ANSI和ISO的联合委员会于1989年着手为C++制定标准。1994年2月,该委员会出版了第一份非正式草案,1998年正式推出了C++的国际标准。 (二) C和C++ C++是C的超集,也可以说C是C++的子集,因为C先出现。按常理说,C++编译器能够编译任何C程序,但是C和C++还是有一些小差别。 例如C++增加了C不具有的关键字。这些关键字能作为函数和变量的标识符在C程序使用,尽管C++包含了所有的C,但显然没有任何C++编译器能编译这样的C程序。 C程序员可以省略函数原型,而C++不可以,一个不带参数的C函数原型必须把void写出来。而C++可以使用空参数列表。 C++new和delete是对内存分配的运算符,取代了C的malloc和free。 标准C++的字符串类取代了C标准C函数库头文件的字符数组处理函数。 C++用来做控制态输入输出的iostream类库替代了标准C的stdio函数库。 C++的try/catch/throw异常处理机制取代了标准C的setjmp()和longjmp()函数。 二、关键字和变量 C++相对与C增加了一些关键字,如下: typename bool dynamic_cast mutable namespace static_cast using catch explicit new virtual operator false private template volatile const protected this wchar_t const_cast public throw friend true reinterpret_cast try bitor xor_e and_eq compl or_eq not_eq bitand 在C++还增加了bool型变量和wchar_t型变量: 布尔型变量是有两种逻辑状态的变量,它包含两个值:真和假。如果在表达式使用了布尔型变量,那么将根据变量值的真假而赋予整型值1或0。要把一个整型变量转换成布尔型变量,如果整型值为0,则其布尔型值为假;反之如果整型值为非0,则其布尔型值为真。布儿型变量在运行时通常用做标志,比如进行逻辑测试以改变程序流程。 #include iostream.h int main() { bool flag; flag=true; if(flag) cout<<true<<endl; return 0; } C++还包括wchar_t数据类型,wchar_t也是字符类型,但是是那些宽度超过8位的数据类型。许多外文字符集所含的数目超过256个,char字符类型无法完全囊括。wchar_t数据类型一般为16位。 标准C++的iostream类库包括了可以支持宽字符的类和对象。用wout替代cout即可。 #include iostream.h int main() { wchar_t wc; wc='b'; wout<<wc; wc='y'; wout<<wc; wc='e'; wout<<wc<<endl; return 0; } 说明一下:某些编译器无法编译该程序(不支持该数据类型)。 三、强制类型转换 有时候,根据表达式的需要,某个数据需要被当成另外的数据类型来处理,这时,就需要强制编译器把变量或常数由声明时的类型转换成需要的类型。为此,就要使用强制类型转换说明,格式如下: int* iptr=(int*) &table; 表达式的前缀(int*)就是传统C风格的强制类型转换说明(typecast),又可称为强制转换说明(cast)。强制转换说明告诉编译器把表达式转换成指定的类型。有些情况下强制转换是禁用的,例如不能把一个结构类型转换成其他任何类型。数字类型和数字类型、指针和指针之间可以相互转换。当然,数字类型和指针类型也可以相互转换,但通常认为这样做是不安全而且也是没必要的。强制类型转换可以避免编译器的警告。 long int el=123; short i=(int) el; float m=34.56; int i=(int) m; 上面两个都是C风格的强制类型转换,C++还增加了一种转换方式,比较一下上面和下面这个书写方式的不同: long int el=123; short i=int (el); float m=34.56; int i=int (m); 使用强制类型转换的最大好处就是:禁止编译器对你故意去做的事发出警告。但是,利用强制类型转换说明使得编译器的类型检查机制失效,这不是明智的选择。通常,是不提倡进行强制类型转换的。除非不可避免,如要调用malloc()函数时要用的void型指针转换成指定类型指针。 四、标准输入输出流 在C语言,输入输出是使用语句scanf()和printf()来实现的,而C++是使用类来实现的。 #include iostream.h main() //C++main()函数默认为int型,而C语言默认为void型。 { int a; cout<>a; /*输入一个数值*/ cout<<a<<endl; //输出并回车换行 return 0; } cin,cout,endl对象,他们本身并不是C++语言的组成部分。虽然他们已经是ANSI标准C++定义,但是他们不是语言的内在组成部分。在C++不提供内在的输入输出运算符,这与其他语言是不同的。输入和输出是通过C++类来实现的,cin和cout是这些类的实例,他们是在C++语言的外部实现。 在C++语言,有了一种新的注释方法,就是‘//’,在该行//后的所有说明都被编译器认为是注释,这种注释不能换行。C++仍然保留了传统C语言的注释风格/*……*/。 C++也可采用格式化输出的方法: #include iostream.h int main() { int a; cout<>a; cout<<dec<<a<<' ' //输出十进制数 <<oct<<a<<' ' //输出八进制数 <<hex<<a<<endl; //输出十六进制数 return 0; } 从上面也可以看出,dec,oct,hex也不可作为变量的标识符在程序出现。 五、函数参数问题 (一) 无名的函数形参 声明函数时可以包含一个或多个用不到的形式参数。这种情况多出现在用一个通用的函数指针调用多个函数的场合,其有些函数不需要函数指针声明的所有参数。看下面的例子: int fun(int x,int y) { return x*2; } 尽管这样的用法是正确的,但大多数C和C++的编译器都会给出一个警告,说参数y在程序没有被用到。为了避免这样的警告,C++允许声明一个无名形参,以告诉编译器存在该参数,且调用者需要为其传递一个实际参数,但是函数不会用到这个参数。下面给出使用了无名参数的C++函数代码: int fun(int x,int) //注意不同点 { return x*2; } (二) 函数的默认参数 C++函数的原型可以声明一个或多个带有默认值的参数。如果调用函数时,省略了相应的实际参数,那么编译器就会把默认值作为实际参数。可以这样来声明具有默认参数的C++函数原型: #include iostream.h void show(int=1,float=2.3,long=6); int main() { show(); show(2); show(4,5.6); show(8,12.34,50L); return 0; } void show(int first,float second,long third) { cout<<first=<<first <<second=<<second <<third=<<third<<endl; } 上面例子,第一次调用show()函数时,让编译器自动提供函数原型指定的所有默认参数,第二次调用提供了第一个参数,而让编译器提供剩下的两个,第三次调用则提供了前面两个参数,编译器只需提供最后一个,最后一个调用则给出了所有三个参数,没有用到默认参数。 六、函数重载 在C++,允许有相同的函数名,不过它们的参数类型不能完全相同,这样这些函数就可以相互区别开来。而这在C语言是不允许的。 1.参数个数不同 #include iostream.h void a(int,int); void a(int); int main() { a(5); a(6,7); return 0; } void a(int i) { cout<<i<<endl; //输出5 } void a(int i,int j) { cout<<i<<j<<endl; //输出67 } 2.参数格式不同 #include iostream.h void a(int,int); void a(int,float); int main() { a(5,6); a(6,7.0); return 0; } void a(int i,int j) { cout<<i<<j<<endl; //输出56 } void a(int i,float j) { cout<<i<<j<>a; for(int i=1;i<=10;i++) //C语言,不允许在这里定义变量 { static int a=0; //C语言,同一函数块,不允许有同名变量 a+=i; cout<<::a<< <<a<<endl; } return 0; } 八、new和delete运算符 在C++语言,仍然支持malloc()和free()来分配和释放内存,同时增加了new和delete来管理内存。 1.为固定大小的数组分配内存 #include iostream.h int main() { int *birthday=new int[3]; birthday[0]=6; birthday[1]=24; birthday[2]=1940; cout<<I was born on <<birthday[0]<<'/'<<birthday[1]<<'/'<<birthday[2]<>size; int *array=new int[size]; for(int i=0;i<size;i++) array[i]=rand(); for(i=0;i<size;i++) cout<<'\n'<<array[i]; delete [] array; return 0; } 九、引用型变量 在C++,引用是一个经常使用的概念。引用型变量是其他变量的一个别名,我们可以认为他们只是名字不相同,其他都是相同的。 1.引用是一个别名 C++的引用是其他变量的别名。声明一个引用型变量,需要给他一个初始化值,在变量的生存周期内,该值不会改变。& 运算符定义了一个引用型变量: int a; int& b=a; 先声明一个名为a的变量,它还有一个别名b。我们可以认为是一个人,有一个真名,一个外号,以后不管是喊他a还是b,都是叫他这个人。同样,作为变量,以后对这两个标识符操作都会产生相同的效果。 #include iostream.h int main() { int a=123; int& b=a; cout<<a<<','<<b<<endl; //输出123,123 a++; cout<<a<<','<<b<<endl; //输出124,124 b++; cout<<a<<','<<b<等运算符 #include iostream.h void func1(s p); void func2(s& p); struct s { int n; char text[10]; }; int main() { static s str={123,China}; func1(str); func2(str); return 0; } void func1(s p) { cout<<p.n<<endl; cout<<p.text<<endl; } void func2(s& p) { cout<<p.n<<endl; cout<<p.text<<endl; } 从表面上看,这两个函数没有明显区别,不过他们所花的时间却有很大差异,func2()函数所用的时间开销会比func2()函数少很多。它们还有一个差别,如果程序递归func1(),随着递归的深入,会因为的耗尽而崩溃,但func2()没有这样的担忧。 4.以引用方式调用 当函数把引用作为参数传递给另一个函数时,被调用函数将直接对参数在调用者的拷贝进行操作,而不是产生一个局部的拷贝(传递变量本身是这样的)。这就称为以引用方式调用。把参数的值传递到被调用函数内部的拷贝则称为以传值方式调用。 #include iostream.h void display(const Date&,const char*); void swapper(Date&,Date&); struct Date { int month,day,year; }; int main() { static Date now={2,23,90}; static Date then={9,10,60}; display(now,Now: ); display(then,Then: ); swapper(now,then); display(now,Now: ); display(then,Then: ); return 0; } void swapper(Date& dt1,Date& dt2) { Date save; save=dt1; dt1=dt2; dt2=save; } void display(const Date& dt,const char *s) { cout<<s; cout<<dt.month<<'/'<<dt.day<<'/'<<dt.year<<endl; } 5.以引用作为返回值 #include iostream.h struct Date { int month,day,year; }; Date birthdays[]= { {12,12,60}; {10,25,85}; {5,20,73}; }; const Date& getdate(int n) { return birthdays[n-1]; } int main() { int dt=1; while(dt!=0) { cout<<Enter date # (1-3,0 to quit)<>dt; if(dt>0 && dt<4) { const Date& bd=getdate(dt); cout<<bd.month<<'/'<<bd.day<<'/'<<bd.year<<endl; } } return 0; } 程序都很简单,就不讲解了。 主体:(二)类的设计,构造函数和析构函数 类是编程人员表达自定义数据类型的C++机制。它和C语言的结构类似,C++类支持数据抽象和面向对象的程序设计,从某种意义上说,也就是数据类型的设计和实现。 一、类的设计 1.类的声明 class 类名 { private: //私有 ... public: //公有 ... }; 2.类的成员 一般在C++,所有定义的变量和函数都是类的成员。如果是变量,我们就叫它数据成员如果是函数,我们就叫它成员函数。 3.类成员的可见性 private和public访问控制符决定了成员的可见性。由一个访问控制符设定的可访问状态将一直持续到下一个访问控制符出现,或者类声明的结束。私有成员仅能被同一个类的成员函数访问,公有成员既可以被同一类的成员函数访问,也可以被其他已经实例化的类函数访问。当然,这也有例外的情况,这是以后要讨论的友元函数。 类默认的数据类型是private,结构的默认类型是public。一般情况下,变量都作为私有成员出现,函数都作为公有成员出现。 类还有一种访问控制符protected,叫保护成员,以后再说明。 4.初始化 在声明一个类的对象时,可以用圆括号()包含一个初始化表。 看下面一个例子: #include iostream.h class Box { private: int height,width,depth; //3个私有数据成员 public: Box(int,int,int); ~Box(); int volume(); //成员函数 }; Box::Box(int ht,int wd,int dp) { height=ht; width=wd; depth=dp; } Box::~Box() { //nothing } int Box::volume() { return height*width*depth; } int main() { Box thisbox(3,4,5); //声明一个类对象并初始化 cout<<thisbox.volume()<<endl; return 0; } 当一个类没有private成员和protected成员时,也没有虚函数,并且不是从其他类派生出来的,可以用{}来初始化。(以后再讲解) 5.内联函数 内联函数和普通函数的区别是:内联函数是在编译过程展开的。通常内联函数必须简短。定义类的内联函数有两种方法:一种和C语言一样,在定义函数时使用关键字inline。如: inline int Box::volume() { return height*width*depth; } 还有一种方法就是直接在类声明的内部定义函数体,而不是仅仅给出一个函数原型。我们把上面的函数简化一下: #include iostream.h class Box { private: int height,width,depth; public: Box(int ht,int wd,int dp) { height=ht; width=wd; depth=dp; } ~Box(); int volume() { return height*width*depth; } }; int main() { Box thisbox(3,4,5); //声明一个类对象并初始化 cout<<thisbox.volume()<<endl; return 0; } 这样,两个函数都默认为内联函数了。 二、构造函数 什么是构造函数?通俗的讲,在类,函数名和类名相同的函数称为构造函数。上面的Box()函数就是构造函数。C++允许同名函数,也就允许在一个类有多个构造函数。如果一个都没有,编译器将为该类产生一个默认的构造函数,这个构造函数可能会完成一些工作,也可能什么都不做。 绝对不能指定构造函数的类型,即使是void型都不可以。实际上构造函数默认为void型。 当一个类的对象进入作用域时,系统会为其数据成员分配足够的内存,但是系统不一定将其初始化。和内部数据类型对象一样,外部对象的数据成员总是初始化为0。局部对象不会被初始化。构造函数就是被用来进行初始化工作的。当自动类型的类对象离开其作用域时,所站用的内存将释放回系统。 看上面的例子,构造函数Box()函数接受三个整型擦黑素,并把他们赋值给立方体对象的数据成员。 如果构造函数没有参数,那么声明对象时也不需要括号。 1.使用默认参数的构造函数 当在声明类对象时,如果没有指定参数,则使用默认参数来初始化对象。 #include iostream.h class Box { private: int height,width,depth; public: Box(int ht=2,int wd=3,int dp=4) { height=ht; width=wd; depth=dp; } ~Box(); int volume() { return height*width*depth; } }; int main() { Box thisbox(3,4,5); //初始化 Box defaulbox; //使用默认参数 cout<<thisbox.volume()<<endl; //输出60 cout<<defaulbox.volume()<<endl; //输出24 return 0; } 2.默认构造函数 没有参数或者参数都是默认值的构造函数称为默认构造函数。如果你不提供构造函数,编译器会自动产生一个公共的默认构造函数,这个构造函数什么都不做。如果至少提供一个构造函数,则编译器就不会产生默认构造函数。 3.重载构造函数 一个类可以有多个构造函数。这些构造函数必须具有不同的参数表。在一个类需要接受不同初始化值时,就需要编写多个构造函数,但有时候只需要一个不带初始值的空的Box对象。 #include iostream.h class Box { private: int height,width,depth; public: Box() { //nothing } Box(int ht=2,int wd=3,int dp=4) { height=ht; width=wd; depth=dp; } ~Box(); int volume() { return height*width*depth; } }; int main() { Box thisbox(3,4,5); //初始化 Box otherbox; otherbox=thisbox; cout<<otherbox.volume();<<endl; return 0; } 这两个构造函数一个没有初始化值,一个有。当没有初始化值时,程序使用默认值,即2,3,4。 但是这样的程序是不好的。它允许使用初始化过的和没有初始化过的Box对象,但它没有考虑当thisbox给otherbox赋值失败后,volume()该返回什么。较好的方法是,没有参数表的构造函数也把默认值赋值给对象。 class Box { int height,width,depth; public: Box() { height=0;width=0;depth=0; } Box(int ht,int wd,int dp) { height=ht;width=wd;depth=dp; } int volume() { return height*width*depth; } }; 这还不是最好的方法,更好的方法是使用默认参数,根本不需要不带参数的构造函数。 class Box { int height,width,depth; public: Box(int ht=0,int wd=0,int dp=0) { height=ht;width=wd;depth=dp; } int volume() { return height*width*depth; } }; 三、析构函数 当一个类的对象离开作用域时,析构函数将被调用(系统自动调用)。析构函数的名字和类名一样,不过要在前面加上 ~ 。对一个类来说,只能允许一个析构函数,析构函数不能有参数,并且也没有返回值。析构函数的作用是完成一个清理工作,如释放从堆分配的内存。 我们也可以只给出析构函数的形式,而不给出起具体函数体,其效果是一样的,如上面的例子。但在有些情况下,析构函数又是必需的。如在类从堆分配了内存,则必须在析构函数释放 主体:(三)类的转换 C++的内部数据类型遵循隐式类型转换规则。假设某个表达市使用了一个短整型变量,而编译器根据上下文认为这儿需要是的长整型,则编译器就会根据类型转换规则自动把它转换成长整型,这种隐式转换出现在赋值、参数传递、返回值、初始化和表达式。我们也可以为类提供相应的转换规则。 对一个类建立隐式转换规则需要构造一个转换函数,该函数作为类的成员,可以把该类的对象和其他数据类型的对象进行相互转换。声明了转换函数,就告诉了编译器,当根据句法判定需要类型转换时,就调用函数。 有两种转换函数。一种是转换构造函数;另一种是成员转换函数。需要采用哪种转换函数取决于转换的方向。 一、转换构造函数 当一个构造函数仅有一个参数,且该参数是不同于该类的一个数据类型,这样的构造函数就叫转换构造函数。转换构造函数把别的数据类型的对象转换为该类的一个对象。和其他构造函数一样,如果声明类的对象的初始化表同转换构造函数的参数表相匹配,该函数就会被调用。当在需要使用该类的地方使用了别的数据类型,便宜器就会调用转换构造函数进行转换。 #include iostream.h #include time.h #include stdio.h class Date { int mo, da, yr; public: Date(time_t); void display(); }; void Date::display() { char year[5]; if(yr<10) sprintf(year,0%d,yr); else sprintf(year,%d,yr); cout<<mo<<'/'<<da<<'/'<tm_mday; mo=tim->tm_mon+1; yr=tim->tm_year; if(yr>=100) yr-=100; } int main() { time_t now=time(0); Date dt(now); dt.display(); return 0; } 本程序先调用time()函数来获取当前时间,并把它赋给time_t对象;然后程序通过调用Date类的转换构造函数来创建一个Date对象,该对象由time_t对象转换而来。time_t对象先传递给localtime()函数,然后返回一个指向tm结构(time.h文件声明)的指针,然后构造函数把结构的日月年的数值拷贝给Date对象的数据成员,这就完成了从time_t对象到Date对象的转换。 二、成员转换函数 成员转换函数把该类的对象转换为其他数据类型的对象。在成员转换函数的声明要用到关键字operator。这样声明一个成员转换函数: operator aaa(); 在这个例子,aaa就是要转换成的数据类型的说明符。这里的类型说明符可以是任何合法的C++类型,包括其他的类。如下来定义成员转换函数; Classname::operator aaa() 类名标识符是声明了该函数的类的类型说明符。上面定义的Date类并不能把该类的对象转换回time_t型变量,但可以把它转换成一个长整型值,计算从2000年1月1日到现在的天数。 #include iostream.h class Date { int mo,da,yr; public: Date(int m,int d,int y) {mo=m; da=d; yr=y;} operator int(); //声明 }; Date::operator int() //定义 { static int dys[]={31,28,31,30,31,30,31,31,30,31,30,31}; int days=yr-2000; days*=365; days+=(yr-2000)/4; for(int i=0;i<mo-1;i++) days+=dys[i]; days+=da; return days; } int main() { Date now(12,24,2003); int since=now; cout<<since<<endl; return 0; } 三、类的转换 上面两个例子都是C++类对象和内部数据对象之间的相互转换。也可以定义转换函数来实现两个类对象之间的相互转换。 #include iostream.h class CustomDate { public: int da, yr; CustomDate(int d=0,int y=0) {da=d; yr=y;} void display() { cout<<yr<<'-'<<da<<endl; } }; class Date { int mo, da, yr; public: Date(int m=0,int d=0,int y=0) {mo=m; da=d; yr=y;} Date(const CustomDate&); //转换构造函数 operator CustomDate(); //成员转换函数 void display() { cout<<mo<<'/'<<da<<'/'<<yr<<endl; } }; static int dys[] = {31,28,31,30,31,30,31,31,30,31,30,31}; Date::Date(const CustomDate& jd) { yr=jd.yr; da=jd.da; for(mo=0;modys[mo]) da-=dys[mo]; else break; mo++; } Date::operator CustomDate() { CustomDate cd(0,yr); for(int i=0;i<mo-1;i++) cd.da+=dys[i]; cd.da+=da; return cd; } int main() { Date dt(12,24,3); CustomDate cd; cd = dt; //调用成员转换函数 cd.display(); dt = cd; //调用转换构造函数 dt.display(); return 0; } 这个例子有两个类CustomDate和Date,CustomDate型日期包含年份和天数。 这个例子没有考虑闰年情况。但是在实际构造一个类时,应该考虑到所有问题的可能性。 在Date里具有两种转换函数,这样,当需要从Date型变为CustomDate型十,可以调用成员转换函数;反之可以调用转换构造函数。 不能既在Date类定义成员转换函数,又在CustomDate类里定义转换构造函数。那样编译器在进行转换时就不知道该调用哪一个函数,从而出错。 四、转换函数的调用 C++里调用转换函数有三种形式:第一种是隐式转换,例如编译器需要一个Date对象,而程序提供的是CustomDate对象,编译器会自动调用合适的转换函数。另外两种都是需要在程序代码明确给出的显式转换。C++强制类型转换是一种,还有一种是显式调用转换构造函数和成员转换函数。下面的程序给出了三转换形式: #include iostream.h class CustomDate { public: int da, yr; CustomDate(int d=0,int y=0) {da=d; yr=y;} void display() { cout<<yr<<'-'<<da<<endl; } }; class Date { int mo, da, yr; public: Date(int m,int d,int y) { mo=m; da=d; yr=y; } operator CustomDate(); }; Date::operator CustomDate() { static int dys[]={31,28,31,30,31,30,31,31,30,31,30,31}; CustomDate cd(0,yr); for(int i=0;i<mo-1;i++) cd.da+=dys[i]; cd.da+=da; return cd; } int main() { Date dt(11,17,89); CustomDate cd; cd = dt; cd.display(); cd = (CustomDate) dt; cd.display(); cd = CustomDate(dt); cd.display(); return 0; } 五、转换发生的情形 上面的几个例子都是通过不能类型对象之间的相互赋值来调用转换函数,还有几种调用的可能: 参数传递 初始化 返回值 表达式语句 这些情况下,都有可能调用转换函数。 下面的程序不难理解,就不分析了。 #include iostream.h class CustomDate { public: int da, yr; CustomDate() {} CustomDate(int d,int y) { da=d; yr=y;} void display() { cout<<yr<<'-'<<da<<endl; } }; class Date { int mo, da, yr; public: Date(int m,int d,int y) { mo=m; da=d; yr=y; } operator CustomDate(); }; Date::operator CustomDate() { static int dys[]={31,28,31,30,31,30,31,31,30,31,30,31}; CustomDate cd(0,yr); for (int i=0;i<mo-1;i++) cd.da += dys[i]; cd.da+=da; return cd; } class Tester { CustomDate cd; public: explicit Tester(CustomDate c) { cd=c; } void display() { cd.display(); } }; void dispdate(CustomDate cd) { cd.display(); } CustomDate rtndate() { Date dt(9,11,1); return dt; } int main() { Date dt(12,24,3); CustomDate cd; cd = dt; cd.display(); dispdate(dt); Tester ts(dt); ts.display(); cd = rtndate(); cd.display(); return 0; } 六、显式构造函数 注意上面Tester类的构造函数前面有一个explicit修饰符。如果不加上这个关键字,那么在需要把CustomDate对象转换成Tester对象时,编译器会把该函数当作转换构造函数来调用。但是有时候,并不想把这种只有一个参数的构造函数用于转换目的,而仅仅希望用它来显式地初始化对象,此时,就需要在构造函数前加explicit。如果在声明了Tester对象以后使用了下面的语句将导致一个错误: ts=jd; //error 这个错误说明,虽然Tester类有一个以Date型变量为参数的构造函数,编译器却不会把它看作是从Date到Tester的转换构造函数,因为它的声明包含了explicit修饰符。 七、表达式内部的转换 在表达式内部,如果发现某个类型和需要的不一致,就会发生错误。数字类型的转换是很简单,这里就不举例了。下面的程序是把Date对象转换成长整型值。 #include iostream.h class Date { int mo, da, yr; public: Date(int m,int d,int y) { mo=m; da=d; yr=y; } operator long(); }; Date::operator long() { static int dys[]={31,28,31,30,31,30,31,31,30,31,30,31}; long days=yr; days*=365; days+=(yr-1900)/4; //从1900年1月1日开始计算 for(int i=0;i<mo-1;i++) days+=dys[i]; days+=da; return days; } int main() { Date today(12,24,2003); const long ott=123; long sum=ott+today; cout<<ott<< + <<(long) today<< = <<sum; return 0; } 在表达式,当需要转换的对象可以转换成某个数字类型,或者表达式调用了作用于某个类的重载运算符时,就会发生隐式转换。运算符重载以后再学习。 主体:(四)私有数据成员和友元 一、私有数据成员的使用 1.取值和赋值成员函数 面向对象的约定就是保证所有数据成员的私有性。一般我们都是通过公有成员函数来作为公共接口来读取私有数据成员的。某些时候,我们称这样的函数为取值和赋值函数。 取值函数的返回值和传递给赋值函数的参数不必一一匹配所有数据成员的类型。 #include iostream.h class Date { int mo, da, yr; public: Date(int m,int d,int y) { mo=m; da=d; yr=y; } int getyear() const { return yr; } void setyear(int y) { yr = y; } }; int main() { Date dt(4,1,89); cout<<The year is: <<dt.getyear()<<endl; dt.setyear(97); cout<<The new year is: <<dt.getyear(); return 0; } 上面的例子很简单,不分析了。要养成这样的习惯,通过成员函数来访问和改变类的数据。这样有利于软件的设计和维护。比如,改变Date类内部数据的形式,但仍然用修改过的getyear()和setyear()来提供访问接口,那么使用该类就不必修改他们的代码,仅需要重新编译程序即可。 2.常量成员函数 注意上面的程序getyear()被声明为常量型,这样可以保证该成员函数不会修改调用他的对象。通过加上const修饰符,可以使访问对象数据的成员函数仅仅完成不会引起数据变动的那些操作。 如果程序声明某个Date对象为常量的话,那么该对象不得调用任何非常量型成员函数,不论这些函数是否真的试图修改对象的数据。只有把那些不会引起数据改变的函数都声明为常量型,才可以让常量对象来调用。 3.改进的成员转换函数 下面的程序改进了从Date对象到CustomDate对象的成员转换函数,用取值和赋值函数取代了使用公有数据成员的做法。(以前的程序代码在上一帖) #include iostream.h class CustomDate { int da,yr; public: CustomDate() {} CustomDate(int d,int y) { da=d; yr=y; } void display() const {cout<<yr<<'-'<<da<<endl;} int getday() const { return da; } void setday(int d) { da=d; } }; class Date { int mo,da,yr; public: Date(int m,int d,int y) { mo=m; da=d; yr=y; } operator CustomDate() const; }; Date::operator CustomDate() const { static int dys[] = {31,28,31,30,31,30,31,31,30,31,30,31}; CustomDate cd(0,yr); int day=da; for(int i=0;i<mo-1;i++) day+=dys[i]; cd.setday(day); return cd; } int main() { Date dt(11,17,89); CustomDate cd; cd=dt; cd.display(); return 0; } 注意上面的程序Date::operator CustomDate()声明为常量型,因为这个函数没有改变调用它对象的数据,尽管它修改了一个临时CustomDate对象并将其作为函数返回值。 二、友元 前面已经说过了,私有数据成员不能被类外的其他函数读取,但是有时候类会允许一些特殊的函数直接读写其私有数据成员。 关键字friend可以让特定的函数或者别的类的所有成员函数对私有数据成员进行读写。这既可以维护数据的私有性,有可以保证让特定的类或函数能够直接访问私有数据。 1.友元类 一个类可以声明另一个类为其友元,这个友元的所有成员函数都可以读写它的私有数据。 #include iostream.h class Date; class CustomDate { int da,yr; public: CustomDate(int d=0,int y=0) { da=d; yr=y; } void display() const {cout<<yr<<'-'<<da<<endl;} friend Date; //这儿 }; class Date { int mo,da,yr; public: Date(int m,int d,int y) { mo=m; da=d; yr=y; } operator CustomDate(); }; Date::operator CustomDate() { static int dys[] = {31,28,31,30,31,30,31,31,30,31,30,31}; CustomDate cd(0, yr); for (int i=0;i<mo-1;i++) cd.da+=dys[i]; cd.da+=da; return cd; } int main() { Date dt(11,17,89); CustomDate cd(dt); cd.display(); return 0; } 在上面的程序,有这样一句 friend Date; 该语句告诉编译器,Date类的所有成员函数有权访问CustomDate类的私有成员。因为Date类的转换函数需要知道CustomDate类的每个数据成员,所以真个Date类都被声明为CustomDate类的友元。 2.隐式构造函数 上面程序对CustomDate的构造函数的调用私有显示该类需要如下的一个转换构造函数: CustomDate(Date& dt); 但是唯一的一个构造函数是:CustomDate(int d=0;int y=0); 这就出现了问题,编译器要从Date对象构造一个CustomDate对象,但是CustomDate类并没有定义这样的转换构造函数。不过Date类定义了一个成员转换函数,它可以把Date对象转换成CustomDate对象。于是编译器开始搜索CustomDate类,看其是否有一个构造函数,能从一个已存在的CustomDate的对象创建新的CustomDate对象。这种构造函数叫拷贝构造函数。拷贝构造函数也只有一个参数,该参数是它所属的类的一个对象,由于CustomDate类没有拷贝构造函数,于是编译器就会产生一个默认的拷贝构造函数,该函数简单地把已存在的对象的每个成员拷贝给新对象。现在我们已经知道,编译器可以把Date对象转换成CustomDate对象,也可以从已存在的CustomDate对象生成一个新的CustomDate对象。那么上面提出的问题,编译器就是这样做的:它首先调用转换函数,从Date对象创建一个隐藏的、临时的、匿名的CustomDate对象,然后用该临时对象作为参数调用默认拷贝构造函数,这就生成了一个新的CustomDate对象。 3.预引用 上面的例子还有这样一句 class Date; 这个语句叫做预引用。它告诉编译器,类Date将在后面定义。编译器必须知道这个信号,因为CustomDate类引用了Date类,而Date里也引用了CustomDate类,必须首先声明其之一。 使用了预引用后,就可以声明未定义的类的友元、指针和引用。但是不可以使用那些需要知道预引用的类的定义细节的语句,如声明该类的一个实例或者任何对该类成员的引用。 4.显式友元预引用 也可以不使用预引用,这只要在声明友元的时候加上关键自class就行了。 #include iostream.h class CustomDate { int da,yr; public: CustomDate(int d=0,int y=0) { da=d; yr=y; } void display() const {cout<<yr<<'-'<<da<<endl;} friend class Date; //这儿,去掉前面的预引用 }; class Date { ... ... }; Date::operator CustomDate() { ... ... } int main() { ... ... } 5.友元函数 通常,除非真的需要,否则并不需要把整个类都设为另一个类的友元,只需挑出需要访问当前类私有数据成员的成员函数,将它们设置为该类的友元即可。这样的函数称为友元函数。 下面的程序限制了CustomDate类数据成员的访问,Date类只有需要这些数据的成员函数才有权读写它们。 #include iostream.h class CustomDate; class Date { int mo,da,yr; public: Date(const CustomDate&); void display() const {cout<<mo<<'/'<<da<<'/'<<yr<<endl;} }; class CustomDate { int da,yr; public: CustomDate(int d=0,int y=0) { da=d; yr=y; } friend Date::Date(const CustomDate&); }; Date::Date(const CustomDate& cd) { static int dys[] = {31,28,31,30,31,30,31,31,30,31,30,31}; yr=cd.yr; da=cd.da; for(mo=0;modys[mo]) da-=dys[mo]; else break; mo++; } int main() { Date dt(CustomDate(123, 89)); dt.display(); return 0; } 6.匿名对象 上面main()函数Date对象调用CustomDate类的构造函数创建了一个匿名CustomDate对象,然后用该对象创建了一个Date对象。这种用法在C++是经常出现的。 7.非类成员的友元函数 有时候友元函数未必是某个类的成员。这样的函数拥有类对象私有数据成员的读写权,但它并不是任何类的成员函数。这个特性在重载运算符时特别有用。 非类成员的友元函数通常被用来做为类之间的纽带。一个函数如果被两个类同时声明为友元,它就可以访问这两个类的私有成员。下面的程序说明了一个可以访问两个类私有数据成员的友元函数是如何将在两个类之间架起桥梁的。 #include iostream.h class Time; class Date { int mo,da,yr; public: Date(int m,int d,int y) { mo=m; da=d; yr=y;} friend void display(const Date&, const Time&); }; class Time { int hr,min,sec; public: Time(int h,int m,int s) { hr=h; min=m; sec=s;} friend void display(const Date&, const Time&); }; void display(const Date& dt, const Time& tm) { cout << dt.mo << '/' << dt.da << '/' << dt.yr; cout << ' '; cout << tm.hr << ':' << tm.min << ':' << tm.sec; } int main() { Date dt(2,16,97); Time tm(10,55,0); display(dt, tm); return 0; } 主体:(五)析构函数和this指针 一、析构函数 前面的一些例子都没有说明析构函数,这是因为所用到的类在结束时不需要做特别的清理工作。下面的程序给出了一新的Date类,其包括一个字符串指针,用来表示月份。 #include iostream.h #include string.h class Date { int mo,da,yr; char *month; public: Date(int m=0, int d=0, int y=0); ~Date(); void display() const; }; Date::Date(int m,int d,int y) { static char *mos[] = { January,February,March,April,May,June, July,August,September,October,November,December }; mo=m; da=d; yr=y; if(m!=0) { month=new char[strlen(mos[m-1])+1]; strcpy(month, mos[m-1]); } else month = 0; } Date::~Date() { delete [] month; } void Date::display() const { if(month!=0) cout<<month<<' '<<da<<','<<yr; } int main() { Date birthday(8,11,1979); birthday.display(); return 0; } 在Date对象的构造函数,首先用new运算符为字符串month动态分配了内存,然后从内部数组把月份的名字拷贝给字符串指针month。 析构函数在删除month指针时,可能会出现一些问题。当然从这个程序本身来看,没什么麻烦;但是从设计一个类的角度来看,当Date类用于赋值时,就会出现问题。假设上面的main()修改为“ int main() { Date birthday(8,11,1979); Date today; today=birthday; birthday.display(); return 0; } 这会生成一个名为today的空的Date型变量,并且把birthday值赋给它。如果不特别通知编译器,它会简单的认为类的赋值就是成员对成员的拷贝。在上面的程序,变量birthday有一个字符型指针month,并且在构造函数里用new运算符初始化过了。当birthday离开其作用域时,析构函数会调用delete运算符来释放内存。但同时,当today离开它的作用域时,析构函数同样会对它进行释放操作,而today里的month指针是birthday里的month指针的一个拷贝。析构函数对同一指针进行了两次删除操作,这会带来不可预知的后果。 如果假设today是一个外部变量,而birthday是一个自变量。当birthday离开其作用域时,就已经把对象today里的month指针删除了。显然这也是不正确的。 再假设有两个初始化的Date变量,把其一个的值赋值给另一个: Date birthday(8,11,1979); Date today(12,29,2003); today=birthday; 问题就更复杂了,当这两个变量离开作用域时,birthday的month的值已经通过赋值传递给了today。而today构造函数用new运算符给month的值却因为赋值被覆盖了。这样,birthday的month被删除了两次,而todaymonth却没有被删除掉。 二、重载赋值运算符 为了解决上面的问题,我们应该写一个特殊的赋值运算符函数来处理这类问题。当需要为同一个类的两个对象相互赋值时,就可以重载运算符函数。这个方法可以解决类的赋值和指针的释放。 下面的程序,类的赋值函数用new运算符从堆分配了一个不同的指针,该指针获取赋值对象相应的值,然后拷贝给接受赋值的对象。 在类重载赋值运算符的格式如下: void operator = (const Date&) 后面我们回加以改进。目前,重载的运算符函数的返回类型为void。它是类总的成员函数,在本程序红,是Date类的成员函数。它的函数名始终是operator =,参数也始终是同一个类的对象的引用。参数表示的是源对象,即赋值数据的提供者。重载函数的运算符作为目标对象的成员函数来使用。 #include iostream.h #include string.h class Date { int mo,da,yr; char *month; public: Date(int m=0, int d=0, int y=0); ~Date(); void operator=(const Date&); void display() const; }; Date::Date(int m, int d, int y) { static char *mos[] = { January,February,March,April,May,June, July,August,September,October,November,December }; mo = m; da = d; yr = y; if (m != 0) { month = new char[strlen(mos[m-1])+1]; strcpy(month, mos[m-1]); } else month = 0; } Date::~Date() { delete [] month; } void Date::display() const { if (month!=0) cout<<month<<' '<<da<<,<<yr<<endl; } void Date::operator=(const Date& dt) { if (this != &dt) { mo = dt.mo; da = dt.da; yr = dt.yr; delete [] month; if (dt.month != 0) { month = new char [std::strlen(dt.month)+1]; std::strcpy(month, dt.month); } else month = 0; } } int main() { Date birthday(8,11,1979); birthday.display(); Date newday(12,29,2003); newday.display(); newday = birthday; newday.display(); return 0; } 除了为Date类加入了一个重载运算符函数,这个程序和上面的一个程序是相同的。赋值运算符函数首先取得所需的数据,然后用delete把原来的month指针所占用的内存返还给堆。接着,如果源对象的month指针已经初始化过,就用new运算符为对象重新分配内存,并把源对象的month字符串拷贝给接受方。 重载的Date类赋值运算符函数的第一个语句比较了源对象的地址和this指针。这个操作取保对象不会自己给自己赋值。 三、this指针 this指针是一个特殊的指针,当类的某个非静态的成员函数在执行时,就会存在this指针。它指向类的一个对象,且这个对象的某个成员函数正在被调用。 this指针的名字始终是this,而且总是作为隐含参数传递给每一个被声明的成员函数,例如: void Date::myFunc(Date* this); 实际编程时函数的声明不需要包含这个参数。 当程序调用某个对象的成员函数时,编译器会把该对象的地址加入到参数列表,感觉上就好象函数采用了上面所示的声明,并且是用如下方式来调用的: dt.myFunc(& dt); 静态成员函数不存在this指针。 当调用某个对象的成员函数时,编译器把对象的地址传递给this指针,然后再调用该函数。因此,成员函数你对任何成员的调用实际上都隐式地使用了this指针。 1.以this指针作为返回值 使用this指针可以允许成员函数返回调用对象给调用者。前面的程序重载赋值运算符没有返回值,因此不能用如下的形式对字符串进行赋值: a=b=c; 为了使重载的类赋值机制也能这样方便,必须让赋值函数返回赋值的结果,在这里就是目标对象。当赋值函数执行时,其返回值也恰好是this指针所指的内容。 下面的程序对前面那个程序进行了修改,让重载赋值运算符返回了一个Date对象的引用。 #include iostream.h #include string.h class Date { int mo,da,yr; char *month; public: Date(int m=0, int d=0, int y=0); ~Date(); void operator=(const Date&); void display() const; }; Date::Date(int m, int d, int y) { static char *mos[] = { January,February,March,April,May,June, July,August,September,October,November,December }; mo = m; da = d; yr = y; if (m != 0) { month = new char[strlen(mos[m-1])+1]; strcpy(month, mos[m-1]); } else month = 0; } Date::~Date() { delete [] month; } void Date::display() const { if (month!=0) cout<<month<<' '<<da<<,<<yr<<endl; } void Date::operator=(const Date& dt) { if (this != &dt) { mo = dt.mo; da = dt.da; yr = dt.yr; delete [] month; if (dt.month != 0) { month = new char [std::strlen(dt.month)+1]; std::strcpy(month, dt.month); } else month = 0; } return *this; } int main() { Date birthday(8,11,1979); Date oldday,newday; oldday=newday=birthday; birthday.display(); oldday.display(); newday.display(); return 0; } 2.在链表使用this指针 在应用程序,如果数据结构里有指向自身类型的成员,那么使用this指针会提供更多的方便。下面的程序建立了一个类ListEntry的链表。 #include iostream.h #include string.h class ListEntry { char* listvalue; ListEntry* preventry; public: ListEntry(char*); ~ListEntry() { delete [] listvalue; } ListEntry* PrevEntry() const { return preventry; }; void display() const { cout<<endl<<listvalue; } void AddEntry(ListEntry& le) { le.preventry = this; } }; ListEntry::ListEntry(char* s) { listvalue = new char[strlen(s)+1]; strcpy(listvalue, s); preventry = 0; } int main() { ListEntry* prev = 0; while (1) { cout <<endl<> name; if (strncmp(name, end, 3) == 0) break; ListEntry* list = new ListEntry(name); if (prev != 0) prev->AddEntry(*list); prev = list; } while (prev != 0) { prev->display(); ListEntry* hold = prev; prev = prev->PrevEntry(); delete hold; } return 0; } 程序运行时,会提示输入一串姓名,当输入完毕后,键入end,然后程序会逆序显示刚才输入的所有姓名。 程序ListEntry类含有一个字符串和一个指向前一个表项的指针。构造函数从对获取内存分配给字符串,并把字符串的内容拷贝到内存,然后置链接指针为NULL。析构函数将释放字符串所占用的内存。 成员函数PrevEntry()返回指向链表前一个表项的指针。另一个成员函数显示当前的表项内容。 成员函数AddEntry(),它把this指针拷贝给参数的preventry指针,即把当前表项的地址赋值给下一个表项的链接指针,从而构造了一个链表。它并没有改变调用它的listEntry对象的内容,只是把该对象的地址赋给函数的参数所引用的那个ListEntry对象的preventry指针,尽管该函数不会修改对象的数据,但它并不是常量型。这是因为,它拷贝对象的地址this指针的内容给一个非长常量对象,而编译器回认为这个非常量对象就有可能通过拷贝得到的地址去修改当前对象的数据,因此AddEntry()函数在声明时不需要用const。 主体:(六)类对象数组和静态成员 一、类对象数组 类的对象和C++其他数据类型一样,也可以为其建立数组,数组的表示方法和结构一样。 #include iostream.h class Date { int mo,da,yr; public: Date(int m=0,int d=0, int y=0) { mo=m; da=d; yr=y;} void display() const { cout<<mo<<'/'<<da<<'/'<<yr<<endl; } }; int main() { Date dates[2]; Date today(12,31,2003); dates[0]=today; dates[0].display(); dates[1].display(); return 0; } 1.类对象数组和默认构造函数 在前面已经说过,不带参数或者所有参数都有默认值的构造函数叫做默认构造函数。如果类没有构造函数,编译器会自动提供一个什么都不做的公共默认构造函数 。如果类当至少有一个构造函数,编译器就不会提供默认构造函数。 如果类当不含默认构造函数,则无法实例化其对象数组。因为实例花类对象数组的格式不允许用初始化值来匹配某个构造函数的参数表。 上面的程序,main()函数声明了一个长度为2的Date对象数组,还有一个包含初始化值的单个Date对象。接着把这个初始化的Date对象赋值给数组第一个对象,然后显示两个数组元素包含的日期。从输出可以看到,第一个日期是有效日期,而第二个显示的都是0。 当声明了某个类的对象数组时,编译器会为每个元素都调用默认构造函数。 下面的程序去掉了构造函数的默认参数值,并且增加了一个默认构造函数。 #include class Date { int mo, da, yr; public: Date(); Date(int m,int d,int y) { mo=m; da=d; yr=y;} void display() const { cout <<mo<<'/'<<da<<'/'<<yr<<endl; } }; Date::Date() { cout <<Date constructor running<<endl; mo=0; da=0; yr=0; } int main() { Date dates[2]; Date today(12,31,2003); dates[0]=today; dates[0].display(); dates[1].display(); return 0; } 运行程序,输出为: Date constructor running Date constructor running 12/31/2003 0/0/0 从输出可以看出,Date()这个默认构造函数被调用了两次。 2.类对象数组和析构函数 当类对象离开作用域时,编译器会为每个对象数组元素调用析构函数。 #include iostream.h class Date { int mo,da,yr; public: Date(int m=0,int d=0,int y=0) { mo=m; da=d; yr=y;} ~Date() {cout<<Date destructor running<<endl;} void display() const {cout<<mo<<'/'<<da<<'/'<<yr<<endl; } }; int main() { Date dates[2]; Date today(12,31,2003); dates[0]=today; dates[0].display(); dates[1].display(); return 0; } 运行程序,输出为: 12/31/2003 0/0/0 Date destructor running Date destructor running Date destructor running 表明析构函数被调用了三次,也就是dates[0],dates[1],today这三个对象离开作用域时调用的。 二、静态成员 可以把类的成员声明为静态的。静态成员只能存在唯一的实例。所有的成员函数都可以访问这个静态成员。即使没有声明类的任何实例,静态成员也已经是存在的。不过类当声明静态成员时并不能自动定义这个变量,必须在类定义之外来定义该成员。 1.静态数据成员 静态数据成员相当于一个全局变量,类的所有实例都可以使用它。成员函数能访问并且修改这个值。如果这个静态成员是公有的,那么类的作用域之内的所有代码(不论是在类的内部还是外部)都可以访问这个成员。下面的程序通过静态数据成员来记录链表首项和末项的地址。 #include iostream.h #include string.h class ListEntry { public: static ListEntry* firstentry; private: static ListEntry* lastentry; char* listvalue; ListEntry* nextentry; public: ListEntry(char*); ~ListEntry() { delete [] listvalue;} ListEntry* NextEntry() const { return nextentry; }; void display() const { cout<<listvalue<nextentry=this; lastentry=this; listvalue=new char[strlen(s)+1]; strcpy(listvalue,s); nextentry=0; } int main() { while (1) { cout<>name
C++面试题 参考:http://blog.csdn.net/Ghost90/archive/2009/04/22/4099672.aspx 整理:松鼠 时间:2009-5-8 1、const 有什么用途?(请至少说明两种) 答: (1)可以定义 const 常量 (2)const可以修饰函数的参数、返回值,甚至函数的定义体。被const修饰的东西都受到强制保护,可以预防意外的变动,能提高程序的健壮性。 2、在C++ 程序调用被 C编译器编译后的函数,为什么要加 extern “C”? 答:C++语言支持函数重载,C语言不支持函数重载。函数被C++编译后在库的名字与C语言的不同。假设某个函数的原型为: void foo(int x, int y); 该函数被C编译器编译后在库的名字为_foo,而C++编译器则会产生像_foo_int_int之类的名字。 C++提供了C连接交换指定符号extern“C”来解决名字匹配问题。 3、请简述以下两个for循环的优缺点(5分) for (i=0; i<N; i++) { if (condition) DoSomething(); else DoOtherthing(); } if (condition) { for (i=0; i<N; i++) DoSomething(); } else { for (i=0; i<N; i++) DoOtherthing(); } 优点:程序简洁 缺点:多执行了N-1次逻辑判断,并且打断了循环“流水线”作业,使得编译器不能对循环进行优化处理,降低了效率。 优点:循环的效率高 缺点:程序不简洁 4、有关内存的思考题 void GetMemory(char *p) { p = (char *)malloc(100); } void Test(void) { char *str = NULL; GetMemory(str); strcpy(str, "hello world"); printf(str); } 请问运行Test函数会有什么样的结果? 答:程序崩溃。 因为GetMemory并不能传递动态内存, Test函数的 str一直都是 NULL。 strcpy(str, "hello world");将使程序崩溃。 char *GetMemory(void) { char p[] = "hello world"; return p; } void Test(void) { char *str = NULL; str = GetMemory(); printf(str); } 请问运行Test函数会有什么样的结果? 答:可能是乱码。 因为GetMemory返回的是指向“内存”的指针,该指针的地址不是 NULL,但其原现的内容已经被清除,新内容不可知。 void GetMemory2(char **p, int num) { *p = (char *)malloc(num); } void Test(void) { char *str = NULL; GetMemory(&str, 100); strcpy(str, "hello"); printf(str); } 请问运行Test函数会有什么样的结果? 答: (1)能够输出hello (2)内存泄漏 void Test(void) { char *str = (char *) malloc(100); strcpy(str, “hello”); free(str); if(str != NULL) { strcpy(str, “world”); printf(str); } } 请问运行Test函数会有什么样的结果? 答:篡改动态内存区的内容,后果难以预料,非常危险。 因为free(str);之后,str成为野指针, if(str != NULL)语句不起作用。 5、编写strcpy函数(10分) 已知strcpy函数的原型是 char *strcpy(char *strDest, const char *strSrc); 其strDest是目的字符串,strSrc是源字符串。 (1)不调用C++/C的字符串库函数,请编写函数 strcpy char *strcpy(char* strDest, const char* strSrc) { assert((strDest!=NULL) && (strSrc !=NULL));//2分 char *address = strDest;//2分 while( (*strDest++ = * strSrc++) != ‘\0’ )//2分 NULL; return address;// 2分 } 5.1 strcpy能把strSrc的内容复制到strDest,为什么还要char * 类型的返回值? 答:为了实现链式表达式。 // 2分 例如int length = strlen( strcpy( strDest, "hello world") ); 6、编写类String的构造函数、析构函数和赋值函数(25分) 已知类String的原型为: class String { public: String(const char *str = NULL);//普通构造函数 String(const String &other);//拷贝构造函数 ~ String(void);// 析构函数 String & operate =(const String &other);// 赋值函数 private: char* m_data;// 用于保存字符串 }; 请编写String的上述4个函数。 标准答案: // String的析构函数 String::~String(void) // 3分 { delete [] m_data; // 由于m_data是内部数据类型,也可以写成delete m_data; } // String的普通构造函数 String::String(const char *str) // 6分 { if(str==NULL) { m_data = new char[1]; // 若能加NULL 判断则更好 *m_data = ‘\0’; } else { int length = strlen(str); m_data = new char[length+1]; // 若能加NULL 判断则更好 strcpy(m_data, str); } } // 拷贝构造函数 String::String(const String &other) // 3分 { int length = strlen(other.m_data); m_data = new char[length+1]; // 若能加NULL 判断则更好 strcpy(m_data, other.m_data); } // 赋值函数 String & String::operate =(const String &other) // 13分 { // (1) 检查自赋值 // 4分 if(this == &other) return *this; // (2) 释放原有的内存资源 // 3分 delete [] m_data; // ()分配新的内存资源,并复制内容// 3分 int length = strlen(other.m_data); m_data = new char[length+1]; // 若能加NULL 判断则更好 strcpy(m_data, other.m_data); // ()返回本对象的引用 // 3分 return *this; } 7、实现双向链表删除一个节点P,在节点P后插入一个节点,写出这两个函数。 void DeleteNode(DuNode *p) { p->prior->next=p->next; p->next->prior=p->prior; } void InsertNode(DuNode *p, DuNode *s)//Node "s" is inserted after "p" { s->next=p->next; p->next->prior=s; p->next=s; s->prior=p; } 8、Windows程序的入口是哪里?写出Windows消息机制的流程。 WINDOWS入口是WinMain函数 消息机制的流程: 系统发生了某个事件 Windows把这个事件翻译为消息,然后把它放到消息队列 1. 应用程序从消息队列接收到这个消息,把它存放在TMsg记录 2. 应用程序把消息传递给一个适当的窗口的窗口过程 3. 窗口过程响应这个消息并进行处理 9.写一个函数,将其的\t都转换成4个空格。 #include<iostream> using namespace std; char* Convert_t(char *des,char *src) { char *temp; des=new char[100]; temp=des; while(*src!='\0') { if(*src=='\t') { src++; *des++=' '; *des++=' '; *des++=' '; *des++=' '; continue; } *des++=*src++; } *des='\0'; des=temp; return des; } int main() { char *t="asdf\tasd\tasasddas\\tdfasdf",*d; cout<<t<<endl; cout<<Convert_t(d,t); getchar(); } 10.如何定义和实现一个类的成员函数为回调函数? 如果类的成员函数是一个callback函数, 必须宣告它为"static",才能把C++ 编译器加诸于函数的一个隐藏参数this去掉。 11.C++里面是不是所有的动作都是main()引起的?如果不是,请举例。 不是的,C++里面有些动作不是引起的,比如,全局对象的实例化、全局变量的动态空间申请,等等 下面是一个例子: #include<iostream> using namespace std; char *des=new char[100]; //全局变量的动态空间申请在程序运行之后,main运行之前完成。所以不是所有的动作都是main引起的。 int main() { char *des="abc"; cout<<des<<endl; getchar(); } 12.C++里面如何声明const void f(void)函数为C程序的库函数? extern "C" const void f(void); 这样声明之后,相当于告诉C, 函数const void f(void)是在C++语言的文件声明或者实现的,c程序可以使用这个C++的函数了,从而实现C++和c的混合编程。 13、编写一个函数,作用是把一个char组成的字符串循环右移n个。比如原来是“abcdefghi”如果n=2,移位后应该是“hiabcdefgh” 正确解答1: void LoopMove(char* pStr, int steps) { int n = strlen( pStr ) - steps; char tmp[MAX_LEN]; strcpy ( tmp, pStr + n ); strcpy ( tmp + steps, pStr); *( tmp + strlen ( pStr ) ) = '\0'; strcpy( pStr, tmp ); } 正确解答2: void LoopMove(char* pStr,int steps ) { int n = strlen( pStr ) - steps; char tmp[MAX_LEN]; memcpy( tmp, pStr + n, steps ); memcpy(pStr + steps, pStr, n ); memcpy(pStr, tmp, steps ); } 14、写出输出结果 void fun(char s[10]) { char a[10]; cout<<"a:"<<sizeof(a)<<endl; cout<<"s:"<<sizeof(s)<<endl; } 输出: a:10 s:4 15、内存的分配方式的分配方式有几种? 答: 1. 从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量。 2. 在上创建。在执行函数时,函数内局部变量的存储单元都可以在上创建,函数执行结束时这些存储单元自动被释放。内存分配运算内置于处理器的指令集,效率很高,但是分配的内存容量有限。 3. 从堆上分配,亦称动态内存分配。程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多。 16.是不是一个父类写了一个virtual 函数,如果子类覆盖它的函数不加virtual ,也能实现多态? 答:virtual修饰符会被隐形继承的。private 也被集成,只事派生类没有访问权限而已。virtual可加可不加。子类的空间里有父类的所有变量(static除外)。同一个函数只存在一个实体(inline除外)。子类覆盖它的函数不加virtual ,也能实现多态。在子类的空间里,有父类的私有变量。私有变量不能直接访问。 17.进程间通信的方式有? 进程间通信的方式有 共享内存, 管道 ,Socket ,消息队列 , DDE等 18.C++什么数据分配在或堆,New分配数据是在近堆还是远堆? 答:: 存放局部变量,函数调用参数,函数返回值,函数返回地址。由系统管理 堆: 程序运行时动态申请,new 和 malloc申请的内存就在堆上 (Google搜):DOS下程序是独占方式,堆分为近堆和远堆,近堆和是在数据段开辟的同一块内存地址从下往上增长,堆从上向下分配,间没有规定分界线,所以程序控制不当,如深层次的递归,大量的动态地址分配很容易造成堆冲突,即堆地址重叠,从而造成死机和程序运行异常。堆和连在一起说的原因就是如此。至于远堆则是指在数据段和代码段以外计算机所有没有使用的剩余基本内存。Windows采用的是虚拟地址,内存分配方式就不一样了 补充:DOS下堆的分配是由程序而不是操作系统自己控制的,具体分配大小、方式随编译系统、程序模式不同而异。C语言的堆分配代码在启动代码. 19.全局变量和局部变量在内存是否有区别?如果有,是什么区别? 全局变量储存在静态数据库,局部变量在堆。 20.TCP/IP 建立连接的过程?(3-way shake) 答:在TCP/IP协议,TCP协议提供可靠的连接服务,采用三次握手建立一个连接。 第一次握手:建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认; 第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态; 第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。 21.winsock建立连接的主要实现步骤? 答:服务器端:socker()建立套接字,绑定(bind)并监听(listen),用accept()等待客户端连接。 客户端:socker()建立套接字,连接(connect)服务器,连接上后使用send()和recv(),在套接字上写读数据,直至数据交换完毕,closesocket()关闭套接字。 服务器端:accept()发现有客户端连接,建立一个新的套接字,自身重新开始等待连接。该新产生的套接字使用send()和recv()写读数据,直至数据交换完毕,closesocket()关闭套接字。 22.static有什么用途?(请至少说明两种) 1) 在函数体,一个被声明为静态的变量在这一函数被调用过程维持其值不变。 2) 在模块内(但在函数体外),一个被声明为静态的变量可以被模块内所用函数访问,但不能被模块外其它函数访问。它是一个本地的全局变量。 3) 在模块内,一个被声明为静态的函数只可被这一模块内的其它函数调用。那就是,这个函数被限制在声明它的模块的本地范围内使用 23.引用与指针有什么区别? 1) 引用必须被初始化,指针不必。 2) 引用初始化以后不能被改变,指针可以改变所指的对象。 3) 不存在指向空值的引用,但是存在指向空值的指针。 24、请写出下列代码的输出内容 #include<stdio.h> main() { int a,b,c,d; a=10; b=a++; c=++a; d=10*a++; printf("b,c,d:%d,%d,%d",b,c,d); return 0; } 答:10,12,120 main() { int a[5]={1,2,3,4,5}; int *ptr=(int *)(&a+1); printf("%d,%d",*(a+1),*(ptr-1)); } 输出:2,5 *(a+1)就是a[1],*(ptr-1)就是a[4],执行结果是2,5 &a+1不是首地址+1,系统会认为加一个a数组的偏移,是偏移了一个数组的大小(本例是5个int) int *ptr=(int *)(&a+1); 则ptr实际是&(a[5]),也就是a+5 原因如下: &a是数组指针,其类型为 int (*)[5]; 而指针加1要根据指针类型加上一定的值, 不同类型的指针+1之后增加的大小不同 a是长度为5的int数组指针,所以要加 5*sizeof(int) 所以ptr实际是a[5] 但是prt与(&a+1)类型是不一样的(这点很重要) 所以prt-1只会减去sizeof(int*) a,&a的地址是一样的,但意思不一样,a是数组首地址,也就是a[0]的地址,&a是对象(数组)首地址,a+1是数组下一元素的地址,即a[1],&a+1是下一个对象的地址,即a[5]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值