零、C++的指针
1 2 3 4 5 6 7 8 9
| int *p; p = new int; *p = 5;
int *q; q = new int[10]; q[4] = 5;
|
1 2 3
| delete p; delete [] q; delete q;
|
1 2 3 4 5 6 7 8 9
| int a, b; const int *p = &a; *p = 1; a = 1; *p = &b;
int *q; p = q; q = p;
|
1 2 3 4 5
| int a; int *const p = &a; int q; q = p; p = q;
|
const*不能通过指针修改变量值,*const不能指向别的地址。总之,const和*,谁在前面,谁就不允许改变。
一、构造函数&析构函数
构造函数是用来初始化对象的参数的,那么析构函数就是对对象的消亡处理,也就是我们所说的“处理后事” 。个人的习惯是,如果是复杂的构造函数、复制构造函数、析构函数和成员函数都写在类外面,如果写在里面会显得类定义十分混乱。
我们写了一个小程序说明什么是构造函数、复制构造函数(也叫做拷贝构造函数)和析构函数,以及详细说明它们在不同作用域的生存周期。搞清楚这些生存周期是至关重要的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| #include<iostream> using namespace std;
class A{ public: int num; A(int _num) { num = _num; cout<<"对象"<<num<<" 构造啦"<<endl; } A(const A &a) { cout<<"对象"<<a.num<<" 复制然后构造啦"<<endl; } ~A() { cout<<"对象"<<num<<" 毁灭啦"<<endl; } };
A a1(1);
A Test(A a) { cout<<endl<<"Test函数开始噜!"<<endl; static A a6(6); A a7(7); cout<<"Test函数结束噜!"<<endl; return a7; }
int main() { cout<<endl<<"main函数开始噜!"<<endl; A a_array[3]= {2, A(3), 4}; A a5(5); a1 = 100; cout<<"Test函数输出结果:"<<Test(a5).num<<endl<<endl;
A a8(8); a8 = a5; cout<<endl; cout<<"main函数结束噜!"<<endl<<endl; return 0; }
|
输出结果如下(附上简要说明):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| 对象1 构造啦
main函数开始噜! 对象2 构造啦 对象3 构造啦 对象4 构造啦 对象5 构造啦 对象100 构造啦 对象100 毁灭啦 对象5 复制然后构造啦
Test函数开始噜! 对象6 构造啦 对象7 构造啦 Test函数结束噜! Test函数输出结果:7
对象7 毁灭啦 对象1 毁灭啦 对象8 构造啦
main函数结束噜!
对象5 毁灭啦 对象5 毁灭啦 对象4 毁灭啦 对象3 毁灭啦 对象2 毁灭啦 对象6 毁灭啦 对象100 毁灭啦
|
注意: 下面这个程序,这是我们经常犯错的地方:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| #include <iostream> using namespace std; class Sample { public: int v; Sample(int x = 0){ v = x; } Sample(const Sample &o){ v = o.v + 2; } };
void Print(Sample o) { cout << o.v<<endl; }
int main() { Sample a(5); Print(a); Sample b(a); Print(b); return 0; }
|
输出结果不是我们预想的两个5,而是7和9。这是因为函数Print调用了复制构造函数,结果a.v的值就变成了7,b.v的值又在原来基础上加了2,就变成了9。解决办法:复制构造函数的函数体改成v = o.v;就行了。不要看这个例子很简单,当程序变得很复杂时极有可能疏忽这个小细节。
写了这么多,我想说的是,一般情况下不要自己写复制构造函数,因为这可能会导致某函数体的return返回值与函数的返回值不相同(Dev C++的编译器没有这个问题,但是其他编译器比如VS2010编译器会有),除此之外还会导致函数的实参和形参不相等(这个问题基本上每个编译器都会有)。但是,在一些情况下必须要自己写,举个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| #include<iostream> using namespace std;
class A{ public: int *p; A(){ p = new int; } };
int main() { A a1; A a2(a1); cout<<a1.p<<" "<<a2.p<<endl; return 0; }
|
一次的输出结果是:0x396338 0x396338 。
这意味着a1.p、a2.p指向了同一个地址,这是十分危险的! 如果我在析构函数来个delete a1.p,那么a2.p就变成所谓的野指针了。事实上,默认复制构造函数是一种浅拷贝,因此当我们自己写复制函数的时候,就要是深拷贝,写的时候应当重新申请一片内存,再把要拷贝的值拷贝进去(相当于手动操作)。
另外构造函数还可以添加初始化列表:
1 2 3 4 5 6 7 8
| class A{ private: int sth1; int sth2; public: A(int s1, int s2):sth1(s1), sth2(s2){ ...} };
|
二、封闭类
一个类的成员变量如果是另一个类的对象,就称之为“成员对象”。包含成员对象的类叫封闭类。例如下面这个程序,对象A为封闭类对象,对象A1、A2为成员对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| #include<iostream> using namespace std; class A1{ public: A1(){cout<<"对象A1建造"<<endl;} ~A1(){cout<<"对象A1消亡"<<endl;} }; class A2{ public: A2(){cout<<"对象A2建造"<<endl;} ~A2(){cout<<"对象A2消亡"<<endl;} }; class A{ private: A1 a1; A2 a2; public: A(){cout<<"对象A建造"<<endl;} ~A(){cout<<"对象A消亡"<<endl;} };
int main(){ A a; return 0; }
|
输出结果如下:
1 2 3 4 5 6
| 对象A1建造 对象A2建造 对象A建造 对象A消亡 对象A2消亡 对象A1消亡
|
封闭类对象生成时,先运行所有成员对象的构造函数,然后才执行封闭类自己的构造函数;封闭类对象消亡时,则刚好相反,先执行封闭类的析构函数,然后再执行成员对象的析构函数。
成员对象就相当于零件,封闭类对象就相当于一个完整的产品。零件造好了,完整的产品才能出来;拆解产品时,先将产品上的零件全部拿下来,然后再把零件拆了。
三、友元
友元=友元函数+友元类。
1.友元函数
一旦某个函数被某个类声明为友元函数(加修饰符friend),那么在友元函数内部可以访问该类对象的私有成员,甚至是改变其值。友元函数既可以是类里面的成员函数,也可以不是(即普通函数)。一个友元函数的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| #include<iostream> using namespace std; class A;
class B{ public: void sth2(A a); };
class A{ private: int v; public: friend void sth1(A a); friend void B::sth2(A a); };
void sth1(A a)
{ a.v = 1; cout<<"friend! "<<a.v<<endl; }
void B::sth2(A a)
{ a.v = 2; cout<<"friend!!! "<<a.v<<endl; }
int main() { A a; sth1(a); B b; b.sth2(a); return 0; }
|
输出结果如下:
注意,不能把其他类的私有成员函数声明为友元。这就相当于,我和你虽然是朋友(friend),但是我没经过你的允许,我就不能知道你的秘密(private),除非你愿意把秘密公开(public)。
2.友元类
一个类A可以将另一个类B声明为自己的友元。这样类B的所有成员函数就可以访问类A的私有成员了。注意:友元类与封闭类是有区别的。封闭类的成员变量有其他类的对象,这些对象属于封闭类所有;而友元类只是被声明为友元,并不属于被声明的类里面。下面这个程序,类B声明了类A是自己的友元。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| #include<iostream> using namespace std;
class B{ private: int v; public: friend class A; };
class A{ public: void Test(B b) { b.v = 100; cout<<b.v<<endl; } };
int main() { A a; B b; a.Test(b); return 0; }
|
输出结果自然就是:100。
注意:在C++中友元不具有传递性。A是B的友元,B是C的友元,就不能推出A是C的友元。友元不是相互的,具有单向性,A声明B是它的友元,B就可以随意访问A,但是反过来A不能随便访问B。
四、静态成员&常量成员
1.静态成员
无论是成员变量还是成员函数,只要前面加了static,它们就变成了静态的了。要注意,静态成员函数不能访问非静态成员变量,也不能调用非静态成员函数。原因在于,非静态成员函数不具体作用于某个对象。下面给出我自己的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| #include<iostream> using namespace std;
class A{ private: int n; static int total; public: A(int _n); A(const A &a); static void Print(); };
A::A(int _n) { n = _n; total += n; }
A::A(const A &a) { total += a.n; }
void A::Print() { cout<<total<<endl; }
int A::total = 0;
int main() { A num1(2), num2(5); A num3(num2); num1.Print(); return 0; }
|
输出结果显而易见,是12。
2.常量对象和常量成员函数
常量对象可以执行常量成员函数和静态成员函数,但是不能调用非常量成员函数。原因在于,非常量成员函数内部可能含有修改对象的语句。下面来看一个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| #include<iostream> using namespace std;
class A{ public: void az() const { cout<<"const"<<endl; } };
int main() { const A a; a.az(); return 0; }
|
输出的是:const。
如果某个成员函数中不需要调用非常量成员函数,也不需要修改成员变量的值,那么最好写成常量成员函数,这样不容易出错。