目录
3.1 构造函数
3.1.1 构造函数的概念和作用
面向对象的程序设计语言倾向于对象一定要经过初始化后,使用起来才比较安全。因此,引入了“构造函数”的概念,用于对对象进行自动初始化。在C++语言中,“构造函数”就是一类特殊的成员函数,其名字和类的名字一样,不写返回值类型(void也不写),可以重载,即一个类可以有多个构造函数。如果类的设计者没有写构造函数,那么编译器会自动生成一个没有参数的构造函数,虽然该无参构造函数什么都不作。无参构造函数,不论是编译器自动生成的,还是程序员写的,都称为“默认构造函数”。如果编写了构造函数,那么编译器就不会自动生成默认构造函数。对象在生成时,一定会自动调用某个构造函数进行初始化,对象一旦生成,就再也不会在其上执行构造函数。
构造函数是可以重载的,即可以写多个构造函数,它们的参数表不同。当编译到能生成对象的语句时,编译器会根据这条语句所提供的参数信息决定该调用那个构造函数,如果没有提供参数信息,编译器就认为应该调用无参构造函数。下面是一个有多个构造函数的Complex类的例子程序:
class Complex {
private :
double real,imag;
public:
Complex (double r);
Complex( double r,double i );
Complex ( Complex c1, Complex c2);
};
Complex::Complex( double r) //构造函数1
{
real = r; imag = 0;
}
Complex::Complex( double r,double i) //构造函数2
{
real = r; imag = i;
}
Complex::Complex( Complex c1, Complex c2) //构造函数3
{
real = c1.real+c2.real;
imag = c1.imag+c2.imag;
}
int main()
{
Complex c1(3) , c2 (1,2), c3( c1,c2),c4=7;
return 0;
}
3.1.2 构造函数在数组中的使用
对象数组中的元素同样需要用构造函数初始化。具体哪些元素用哪些构造函数初始化取决于定义数组时的写法。
在构造函数有多个参数时,数组的初始化列表中要显式包含对构造函数的调用。例如下面的程序:
class CTest{
public:
CTest(int n) {} //构造函数(1)
CTest(int n, int m) {} //构造函数(2)
CTest() {} //构造函数(3)
};
int main()
{
CTest array1[3] = {1, CTest(1,2)};
//三个元素分别用构造函数(1)、(2)、(3)初始化
CTest array2[3] = {CTest(2,3), CTest(1,2), 1};
//三个元素分别用构造函数(2)、(2)、(1)初始化
CTest* pArray3[3] = {new CTest(4), new CTest(1,2)};
//两个元素指向的对象分别用构造函数(1)、(2)初始化
return 0;
}
其中第12行,pArray数组是一个指针数组,其元素不是CTest类的对象,而是CTest类的指针。第12行对pArray[0]和pArray[1]进行了初始化,把它们初始化为指向动态分配的CTest对象的指针,而这两个动态分配出来的CTest对象又分别是用构造函数(1)和构造函数(2)初始化的。pArray[2]没有初始化,其值是随机的,不知道指向哪里。故第12行生成了两个CTest对象,而不是三个,所以也只调用了两次CTest类的构造函数。
3.1.3 复制构造函数
1.复制构造函数的概念
复制构造函数是构造函数的一种,也称拷贝构造函数,它只有一个参数,参数类型是本类的引用。复制构造函数的参数可以是const引用,也可以是非const引用。一般使用前者,这样既能以常量对象(初始化后值不能改变的对象)作为参数,也能以非常量对象作为参数去初始化其他对象。一个的参数时const引用,另一个的参数时非const引用,也是可以的。如果类的设计者不写复制构造函数,编译器就会自动生成复制构造函数。大多数情况下,其作用是实现从源对象到目标对象逐个字节的复制,即使得目标对象的每个成员变量都变得和源对象相等。编译器自动生成的复制构造函数称为“默认复制构造函数”。注意,默认构造函数(即无参构造函数)不一定存在,但是复制构造函数总是会存在。如果编写了复制构造函数则默认复制构造函数就不存在了。
自己编译的复制构造函数不一定要做复制的工作(如果只是复制工作,那么使用编译器自动生成的默认复制构造函数就行了)。但从习惯上来讲,复制构造函数还是应该完成类似于复制的工作为好,在此基础上还可以根据需要做些别的工作。
构造函数不能以本类的对象作为唯一参数,以免和复制构造函数相混淆。例如,不能写如下构造函数:
Complex(Complex c) {...}
2.复制构造函数被调用的三种情况
(1)当用一个对象去初始化同类的另一个对象时,会引发复制构造函数被调用。例如,下面的两条语句都会引发复制构造函数的调用,用以初始化c2。
Complex c2(c1);
Complex c2 = c1;
这两条语句时等价的。注意,第二条语句时初始化语句,不是赋值语句。赋值语句的等号左边是一个早已有定义的变量,赋值语句不会引发复制构造函数的调用。例如:
Complex c1, c2;
c1 = c2;
“c1 = c2;”这条语句不会引发复制构造函数的调用,因为c1早已生成,已经初始化过了。
(2)如果函数F的参数是类A的对象,那么当F被调用时,类A的复制构造函数将被调用。换句话说,作为形参的对象,是用复制构造函数初始化的,而且调用复制构造函数时的参数,就是调用函数时所给的实参。
#include <iostream>
using namespace std;
class A
{
public:
A() { };
A( A & a) {
cout << "Copy constructor called" <<endl;
}
};
void Func(A a)
{
}
int main(){
A a;
Func(a);
return 0;
}
程序的输出结果为:
Copy constructor called
这是因为Func函数的形参a在初始化时调用了复制构造函数。前面说过,函数的形参的值等于函数调用时对应的实参,现在可以知道这不一定是正确的。如果形参是一个对象,那么形参的值是否等于实参,取决于该对象所属的类的复制构造函数时如何实现的。例如上面的例子,Func函数的形参a的值在进入函数时是随机的,未必等于实参,因为复制构造函数没有做复制的工作。
以对象作为函数的形参,在函数被调用时,生成的形参要用复制构造函数初始化,这会带来时间上的开销。如果用对象的引用而不是对象作为形参没救没有这个问题了。但是以引用作为形参有一定的风险,因为这种情况下如果形参的值发生改变,实参的值也会跟着改变。如果要确保实参的值不会改变,有希望避免复制构造函数带来的开销,解决办法就是将形参声明为对象的const引用。例如:
void Function(const Complex & c) {...}
这样,Function函数中任何有可能导致c的值被修改的语句,都会引发编译错误。
(3)如果函数的返回值是类A的对象,则函数返回时,类A的复制构造函数被调用。换言之,作为函数返回值的对象是用复制构造函数初始化的,而调用复制构造函数时的实参,就是return语句所返回的对象。例如下面的程序:
#include <iostream>
using namespace std;
class A{
public:
int v;
A(int n) {v = n;}
A(const A & a){
v = a.v;
cout << "Copy constructor called" << endl;
}
};
A Func()
{
A a(4);
return a;
}
int main()
{
cout << Func().v << endl;
return 0;
}
程序的输出结果是:
Copy constructor called
4
第19行调用了Func函数,其返回值是一个对象,该对象就是用复制构造函数初始化的,而且调用复制构造函数时,实参就是第16行return语句所返回的a。复制构造函数在第9行确实完成了复制的工作,所以第19行Func函数的返回值和第15行的a相等。
3.1.4 类型转换构造函数
除复制构造函数外,只有一个参数的构造函数一般都可以称作类型转换构造函数,因为这样的构造函数能起到类型自动转换的作用。例如:
#include <iostream>
using namespace std;
class Complex {
public:
double real, imag;
Complex( int i) //类型转换构造函数
{
cout << "IntConstructor called" << endl;
real = i; imag = 0;
}
Complex(double r,double i)
{
real = r; imag = i;
}
};
int main ()
{
Complex c1(7,8);
Complex c2 = 12;
c1 = 9; // 9被自动转换成一个临时Complex对象
cout << c1.real << "," << c1.imag << endl;
return 0;
}
程序的输出结果是:
IntConstructor called
IntConstructor called
9,0
Complex(int)这个构造函数就是类型转换构造函数,一个int型变量自动转换成一个Complex对象然后再赋值。
3.2 析构函数
析构函数是成员函数的一种,它的名字与类名相同,但前面要加“~”,没有参数和返回值。一个类仅有一个析构函数,如果定义类时没有写析构函数,则编译器生成默认析构函数。如果定义了析构函数每周二编译器不生成默认析构函数。
析构函数在对象消亡时即自动被调用。可以定义析构函数在对象消亡前做善后工作。例如,对象如果在生存期间用new运算符动态分配了内存,则在各处写delete语句以确保程序的每条执行路径都能释放这片内存是比较麻烦的事情。有了析构函数,只要在析构函数中调用delete语句,就能确保对象运行中用new运算符分配的空间在对象消亡时被释放。
只要对象消亡,就会引发析构函数的调用。
函数的参数对象以及作为函数返回值的对象,在消亡时也会引发析构函数。例如:
#include <iostream>
using namespace std;
class CDemo {
public:
~CDemo() { cout << "destructor" << endl; }
};
void Func(CDemo obj )
{
cout << "func" << endl;
}
CDemo d1;
CDemo Test( )
{
cout << "test" << endl;
return d1;
}
int main(){
CDemo d2;
Func(d2);
Test();
cout << "after test" << endl;
return 0;
}
程序的输出结果是:
fun
destructor
test
destructor
after test
destructor
destructor
程序共输出destructor四次。第一次是由于Func函数结束时,参数对象obj消亡导致的。第二次是因为:第20行调用Test函数,Test函数的返回值是一个临时对象,该临时对象在函数调用所在的语句结束时就消亡了,因此引发析构函数调用。第三次是main函数结束时d2消亡导致的。第四次是整个程序结束时全局对象d1消亡导致的。
3.3 构造函数、析构函数和变量的生存期
构造函数在对象生成时会被调用,析构函数在对象消亡时会被调用。对象何时生成和消亡是由对象的生存期决定的。下面通过一个例子来加深对构造函数、析构函数和变量的生存期的理解。
#include <iostream>
using namespace std;
class Demo{
int id;
public:
Demo(int i)
{
id = i;
cout << "id=" << id << "constructed" << endl;
}
~Demo()
{
cout << "id=" << id << "distructed" << endl;
}
};
Demo di(1);
void Func()
{
static Demo d2(2);
Demo d3(3);
cout << "func" << endl;
}
int main()
{
Demo d4(4);
d4 = 6;
cout << "main" << endl;
{ Demo d5(5)
}
Func();
cout << "main ends" << endl;
return 0;
}
程序的输出结果(行号不是输出的一部分)是:
1) id=1 constructed
2) id=4 constructed
3) id=6 constructed
4) id=6 destructed
5) main
6) id=5 constructed
7) id=5 destructed
8) id=2 constructed
9) id=3 constructed
10) func
11) if=3 destructed
12) main ends
13) id=6 destructed
14) id=2 destructed
15) id=1 destructed
要分析程序的输出,首先要看有没有全局对象。因为全局对象是进入main函数以前就形成的,所以全局对象在main函数开始执行前就会被初始化。本程序第16行定义了全局对象d1,因此d1初始化引发的构造函数调用,导致了第1)行的输出结果。
main函数开始执行后,局部对象d4初始化,导致第2)行输出。
第26行,“d4=6;”,6先自动转换成一个临时对象。这个临时对象的初始化导致第3)行输出。临时对象的值被赋给d4后,这条语句执行完毕,临时对象消亡,因此引发析构函数调用,导致第4)行输出。
第28行的d5初始化导致第6)行输出。d5的作用域和生存期都只到离它最近的,且将其包含在内的那一对“{ }”中的“}”为止,即第29行的“}”,因此程序执行到第29行时d5消亡,引发析构函数调用,输出第7)行。
第8)行的输出是由于进入Func函数后,执行第19行的静态局部对象d2初始化导致的。静态局部对象在函数第一次被调用并执行到定义它的语句时初始化,生存期一直持续到整个程序结束,所以即便Func函数调用结束,d2也不会消亡。Func函数中d3初始化导致了第9)行输出。第30行,Func函数调用结束,d3消亡导致第11)行输出。
main函数结束时,其局部变量d4消亡,导致第13)行输出。整个程序结束时,全局对象d1和静态局部对象d2消亡,导致最后两行输出。
3.4 静态成员变量和静态成员函数
类的静态成员有两种:静态成员变量和静态成员函数。静态成员变量就是定义时前面加了static关键字的成员变量;静态成员函数就是在声明时前面加了static关键字的成员函数。
普通成员变量每个对象有各自的一份,而静态成员变量只有一份,被所有同类对象共享。静态成员函数并不作用在某个对象上。访问静态成员时,可以通过“类名::成员名”的方式访问,不需要指明被访问的成员属于哪个对象或作用于哪个对象。
静态成员变量本质上是一个全局变量。一个类,哪怕一个对象都不存在,其静态成员变量也存在。静态成员函数并不需要作用在某个具体的对象上,因此本质是全局函数。
设置静态成员的目的,是为了将和某些类紧密相连的全局变量和全局函数写到类里面,形式上成为一个整体。考虑一个随时知道矩形总数和总面积的图形处理程序,当让可以用全局变量来记录这两个值,但是将这两个变量作为静态成员封装进类中,就更容易理解和维护。例如:
#include <iostream>
using namespace std;
class CRectangle
{
private:
int w, h;
static int totalArea; //矩形总面积
static int totalNumber; //矩形总数
public:
CRectangle(int w_,int h_);
~CRectangle();
static void PrintTotal();
};
CRectangle::CRectangle(int w_,int h_)
{
w = w_; h = h_;
totalNumber ++; //有对象生成则增加总数
totalArea += w * h; //有对象生成则增加总面积
}
CRectangle::~CRectangle()
{
totalNumber --; //有对象消亡则减少总数
totalArea -= w * h; //有对象消亡则减少总面积
}
void CRectangle::PrintTotal()
{
cout << totalNumber << "," << totalArea << endl;
}
int CRectangle::totalNumber = 0;
int CRectangle::totalArea = 0;
// 必须在定义类的文件中对静态成员变量进行一次声明
//或初始化。否则编译能通过,链接不能通过。
int main()
{
CRectangle r1(3,3), r2(2,2);
//cout << CRectangle::totalNumber; // 错误, totalNumber是私有的
CRectangle::PrintTotal();
r1.PrintTotal();
return 0;
}
程序的输出是:
2,13
2,13
这个程序的基本思想是:CRectangle类只提供一个构造函数,所有CRectangle对象生成时都需要用这个构造函数初始化,因此在这个构造函数中增加矩形的总数和总面积的数值即可;而所有CRectangle对象消亡时都会执行析构函数,所以在析构函数中减少矩形的总数和总面积的数值即可。
静态成员变量必须在类定义的外面专门声明,声明时变量名前面加“类名::”。声明的同时可以初始化。如果没有声明,那么程序编译时虽然不会报错,但在链接(link)阶段会报告“标识符找不到”,不能生成.exe文件。
因为静态成员函数不具体作用于某个对象,所以静态成员函数内部不能访问非静态成员变量,也不能调用非静态成员函数。假如上面程序中的PrintTotal函数如下编写:
void CRectangle::PrintTotal()
{
cout << w << "," << totalNumber << "," << totalArea << endl; //错误
}
其中访问了非静态成员变量w,这是不允许的,编译无法通过。因为如果用“CRectangle::PrintTotal();”这种形式调用PrintTotal函数,那就无法解释进入PrintTotal函数后,w到底属于哪个对象的。
思考题:为什么在静态成员函数内不能调用非静态成员函数?
在上面的程序中,CRectangle类的写法表面看起来没有什么问题,实际上是有漏洞的。原因是,并非所有的CRectangle对象生成时都会用程序中的那个构造函数初始化。如果使用该类的程序稍微复杂一些,包含以CRectangle对象为参数的函数,或以CRectangle对象作为返回值的函数,或出现“CRectangle r1(r2);”这样的语句,那么就有一些CRectangle对象是用默认复制构造函数,而不是CRectangle(int w_, int h_)进行初始化的。这些对象生成时没有增加totalNumber和totalArea的值,而消亡时却减少了totalNumber和totalArea的值,这显然是有问题的。解决办法是为CRectangle类编写如下复制构造函数。
CRectangle::CRectangle(CRectangle & r)
{
totalNumber++;
totalArea += r.w*r.h;
w = r.w; h = r.h;
}
3.5 常量对象和常量成员函数
如果希望某个对象的值初始化以后就再也不被改变,则定义对象时可以在前面加const关键字,使之成为常量对象。例如:
class CDemo{
public:
void SetValue() { }
};
const CDemo Obj; //Obj是常量对象
在Obj.被定义为常量对象的情况下,下面这条语句是错误的,编译不能通过:
Obj.SetValue();
错误的原因是,常量对象一旦初始化后,其值就再也不能改变。因此,不能通过常量对象调用普通成员函数,因为普通成员函数在执行过程中有可能修改对象的值。
但是可以通过常量对象调用常量成员函数。所谓常量成员函数,就是定义时加了const关键字的成员函数(声明时也要加)。例如:
#include <iostream>
using namespace std;
class Sample{
public:
void GetValue() const;
};
void Sample::GetValue() const { }
int main()
{
const Sample o;
o.GetValue(); //常量对象上可以执行常量成员函数
return 0;
}
常量对象上可以执行常量成员函数,是因为常量成员函数确保不会修改任何非静态成员变量的值。编译器如果发现常量成员函数内出现了有可能修改非静态成员变量的语句,就会报错。因此,常量成员函数内部也不允许调用同类的其他非常量成员函数(静态成员函数除外)。
思考题:为什么上面一段话要强调“非静态成员变量”和“静态成员函数除外”?
两个成员函数的名字和参数表相同,但一个是const的,一个不是,则它们算重载。
基本上,如果一个成员函数中没有调用非常量成员函数,也没有修改成员变量的值,那么将其写成常量成员函数是好的习惯。
3.6 成员对象和封闭类
一个类的成员变量如果是另一个类的对象,就称之为“成员对象”。包含成员对象的类叫封闭类。
3.6.1 封闭类构造函数的初始化列表
当封闭类的对象生成并初始化时,它包含的成员对象也需要被初始化,这就会引发成员对象构造函数的调用,即通过在定义封闭类的构造函数时,添加初始化列表的方式解决。
在构造函数中刚添加初始化列表的写法如下:
类名::构造函数名(参数表):成员变量1(参数表),成员变量2(参数表),...
{
...
}
“:”和“{”之间的部分就是初始化列表。初始化列表中的成员变量既可以是成员对象,也可以是基本类型的成员变量。对于成员对象,初始化列表的“参数表”中存放的是构造函数的参数(它指明了该成员对象如何初始化)。对于基本类型成员变量,“参数表”中就是一个初始值。“参数表”中的参数可以是任何有定义的表达式,该表达式中够可以包括变量甚至函数调用等,只要表达式中的标识符都是有定义的即可。例如:
#include <iostream>
using namespace std;
class CTyre{ //轮胎类
private:
int radius; //半径
int width;
public:
CTyre(int r, int w):radius(r), width(w) { }
};
class CEngine //引擎类
{
};
class CCar{ //汽车类
private:
int price; //价格
CTyre tyre;
CEngine engine;
public:
CCar(int p, int tr, int tw):price(p), tyre(tr, tw) { }
};
int main()
{
CCar car(20000, 17, 225);
return 0;
}
封闭类对象生成时,先执行所有成员对象的构造函数,然后才执行封闭类自己的构造函数。成员对象构造函数的执行次序和成员对象在类定义中的次序一致,与它们在构造函数初始化列表中出现的次序无关。当封闭类对象消亡时,先执行封闭类的析构函数,然后再执行成员对象的析构函数,成员对象析构函数的执行次序和构造函数的执行次序相反,即先构造的后析构,这是C++处理此类次序问题的一般规律。例如下面的程序:
#include <iostream>
using namespace std;
class CTyre {
public:
CTyre() { cout << "CTyre contructor" << endl; }
~CTyre() { cout << "CTyre destructor" << endl; }
};
class CEngine {
public:
CEngine() { cout << "CEngine contructor" << endl; }
~CEngine() { cout << "CEngine destructor" << endl; }
};
class CCar {
private:
CEngine engine;
CTyre tyre;
public:
CCar( ) { cout << "CCar contructor" << endl; }
~CCar() { cout << "CCar destructor" << endl; }
};
int main(){
CCar car;
return 0;
}
程序的输出结果是:
CEngine constructor
CTyre constructor
CCar constructor
CCar destructor
CTyre destructor
CEngine destructor
封闭类的对象初始化时,要先执行成员对象的构造函数,是因为封闭类的构造函数中有可能用到成员对象。如果此时成员对象还没有初始化,那就不合理了。
3.6.2 封闭类的复制构造函数
封闭类的对象,如果是用默认复制构造函数初始化的,那么它包含的成员对象也会用复制构造函数初始化。例如:
#include <iostream>
using namespace std;
class A{
public:
A() {cout << "default" << endl;}
A(A & a) {cout << "copy" << endl;}
};
class B{
A a;
}
int main()
{
B b1, b2(b1);
return 0;
}
程序的输出结果是:
default
copy
说明b2.a是用类A的复制构造函数初始化的,而且调用复制构造函数时的实参就是b1.a。
3.7 const成员和引用成员
类还可以有常量型成员变量和引用型成员变量。这两种类型的成员变量必须在构造函数的初始化列表中进行初始化。常量型成员变量的值一旦初始化,就不能再改变。例如:
#include <iostream>
using namespace std;
int f;
class CDemo {
private :
const int num; //常量型成员变量
int & ref; //引用型成员变量
int value;
public:
CDemo( int n):num(n),ref(f),value(4)
{
}
};
int main(){
cout << sizeof(CDemo) << endl;
return 0;
}
程序的输出结果是:
12
3.8 友元
私有成员只能在类的成员函数内部访问,如果想在别处访问对象的私有成员,只能通过类提供的接口(成员函数)间接的进行。这固然能够带来数据隐藏的好处,利于将来程序的扩充,但也会增加程序书写的麻烦。C++是从结构化的C语言发展而来的,需要照顾结构化设计程序员的习惯,所以在对私有成员可访问范围的问题上不可限制太死。因此,C++就有了“友元”的概念,使程序员能够在类的成员函数外部直接访问对象的私有成员。
友元分为两种:友元函数和友元类。
3.8.1 友元函数
在定义一个类的时候,可以把一些函数(包括全局函数和其他类的成员函数)声明为“友元”,这样那些函数就成为了该类的友元函数,在友元函数内部就可以访问该类对象的私有成员了。将全局函数声明为友元的写法如下:
friend 返回值类型 函数名(参数表);
将其他类的成员函数声明为友元的写法如下:
friend 返回值类型 其他类的类名::成员函数名(参数表);
但是,不能把其他类的私有成员函数声明为友元。
3.8.2 友元类
一个类A可以将另一个类B声明为自己的友元,类B的所有成员函数都可以访问类A对象的私有成员。在类定义中声明友元类的写法如下:
friend class 类名;
示例如下:
class CCar{
private:
int price;
friend class CDriver; //声明CDriver为友元类
};
class CDriver
{
public:
CCar myCar;
void ModifyCar() //改装汽车
{
myCar.price += 1000;//因CDriver是CCar的友元类,故此处可以访问其私有成员
}
};
int main()
{
return 0;
}
一般来说,类A将类B声明为友元类,则类B最好从逻辑上和类A有比较接近的关系。例如上面的例子,CDriver代表司机,CCar代表车,司机拥有车,所以CDriver类和CCar类从逻辑上来讲关系比较密切,把CDriver类声明为CCar类的友元比较合理。
友元关系在类之间不能传递,即类A是类B的友元,类B是类C的友元,并不能导出类A是类C的友元。
3.9 this指针
3.9.1 C++程序到C程序的翻译
C++是在C语言的基础上发展而来的,第一个C++的编译器实际上是将C++程序翻译成C语言程序,再用C语言编译器进行编译。C语言没有类的概念,只有结构,函数都是全局函数,没有成员函数。翻译时,将class翻译成struc、对象翻译成结构变量时显而易见的,但是对类的成员函数应该如何翻译?对“myCar.Modify();”这样通过一个对象调用成员函数的语句,又该如何翻译呢?
C语言中只有全局函数,因此成员函数只能被翻译成全局函数;“myCar.Modify();”这样的语句也只能被翻译成普通的调用全局函数的语句。那如何让翻译后的Modify全局函数还能作用在myCar这个结构变量上呢?答案就是引入“this指针”。下面来看一段C++程序到C程序的翻译
C++程序:
class CCar{
public:
int price;
void SetPrice(int p):price(p) { }
};
int main()
{
CCar car;
car.SetPrice(20000);
return 0;
}
翻译后的C程序(此程序应保存为扩展名为.c的文件后再编译):
struct CCar{
int price;
};
void SetPrice(CCar* this, int p)
{
this->price = p;
}
int main()
{
struct CCar car;
SetPrice(&car, 20000);
return 0;
}
可以看出,类被翻译成结构体,对象被翻译成结构变量,成员函数被翻译成全局函数。但是C程序的全局函数SetPrice比C++的成员函数SetPrice多了一个参数,就是“CCar* this”。“car.SetPrice(20000);”,被翻译成“SetPrice(&car, 20000);”,后者在执行时,this形参指向的正是car这个变量,因为达到了SetPrice函数作用在car变量上的效果。
3.9.2 this指针的作用
实际上,现在的C++编译器从本质上来说也是按上面的方法来处理成员函数和对成员函数的调用的,即非静态成员函数实际上的形参个数比程序员写的多一个。多出来的参数就是所谓的“this指针”。这个“this指针”指向了成员函数作用的对象,在成员函数执行的过程中,正是通过“this指针”才能找到对象所在的地址,因为也就能找到对象所有的非静态成员变量的地址。下面程序的运行结果能够证明这一点。
#include <iostream>
using namespace std;
class A{
int i;
public:
void Hello() {cout << "hello" << endl;}
};
int main()
{
A* p = NULL;
p->Hello();
}
程序的输出结果是:
hello
在上面的程序中,p明明是一个空指针,为何通过它还能正确调用A的成员函数Hello呢?因为,参考上面C++到C程序的翻译,“p->Hello()”实质上应该是“Hello(p)”,在翻译后的Hello函数中,cout语句没有用到this指针,因此依然可以输出结结果。如果Hello函数中有对成员变量的访问,则程序就会出错。
C++规定,在非静态成员函数内部可以直接使用this关键字,this就代表指向该函数所作用的对象的指针。看下面例子。
#include <iostream>
using namespace std;
class Complex {
public:
double real, imag;
Complex(double r,double i):real(r),imag(i) { }
Complex AddOne()
{
this->real ++;
return * this;
}
};
int main()
{
Complex c1(1,1),c2(0,0);
c2 = c1.AddOne();
cout << c2.real << "," << c2.imag << endl; //输出 2,1
return 0;
}
第9行,this指针的类型是Complex*。因为this指针就指向函数所作用的对象,所以this->real和real是完全等价的。“*this”代表函数所作用的对象,因此执行第16行,进入AddOne函数后,“*this”实际上就是c1,。因此c2的值就会变得和c1相同。
因为静态成员函数并不作用于某个对象,所以在其内部不能使用this指针;否则,这个this指针该指向哪个对象呢?
3.10 在多个文件中使用类
在有多个文件的C++程序中,如果多个.cpp文件都用到同一个类,可以将类的定义写在一个头文件中,然后在各个.cpp文件中包含该头文件。类的非内联成员函数的函数体只能出现在某一个.cpp文件中,不能放在头文件中被多个.cpp文件包含,否则链接时会发生重复定义的错误。类的内敛成员函数的函数体最好写在头文件中,这样编译器在处理内联函数的调用语句时,就能在本文件包含的头文件中找到内联函数的代码,并将这些代码插入调用语句处。内联成员函数放在头文件中被多个.cpp文件包含,不会导致重复定义的错误。