【构造函数】
全局变量在程序装入内存时就已经分配好了存储空间,程序运行时其地址不变。对于程序员没有初始化的全局变量,程序启动时自动将其全部初始化为0。
对于局部变量,如果不进行初始化,那么它的初始值是随机的。局部变量定义在函数的内部,其存储空间是动态分配在栈中的。函数被调用时,栈会分配一部分空间存放该函数中的局部变量(包括参数),这片新分配的存储空间原来的内容是什么,局部变量的初始内容也就是什么,因此局部变量的初始值是不可预测的。函数调用结束后,局部变量占用的存储空间就被回收,以便分配给下一次函数调用中涉及的局部变量。程序员在定义局部变量时将其初始化了,那么这个初始化的工作也是每次函数被调用时都要做的。
对象和基本类型的变量一样,定义时也可以进行初始化。一个对象,其行为和内部结构可能比较复杂,如果不通过初始化为其某些成员变量赋予一个合理的值,使用时就会产生错误。例如:有些以指针为成员变量的类可能会要求其对象生成时,指针就已经指向一片动态分配的存储空间。对象的初始化往往不只是对成员变量赋值那么简单,也可能还要进行一些动态内存分配、打开文件等复杂的操作,在这种情况下,就不可能用初始化基本类型变量的方法来对其初始化。虽然可以为类设计一个初始化函数,对象定义后就立即调用它,但这样做的话,初始化就不具有强制性,难保程序员在定义对象后不会忘记对其进行初始化。
构造函数:其名字和类一样,不写返回值类型,可以重载。如果类的设计者没有写构造函数,那么编译器会自动生成一个没有参数的构造函数(默认构造函数)。如果编写了构造函数,那么编译器不会自动生成默认构造函数。对象在生成时,一定会自动调用某个构造函数进行初始化,对象一旦生成,就再也不会在其上执行构造函数。
构造函数的作用是初始化对象的内存空间。
【构造函数在数组中的使用】
对象数组中的元素同样需要用构造函数初始化。具体哪些元素用哪些构造函数初始化取决于定义数组时的写法。
在构造函数有多个参数时,数组的初始化列表中要显式包含对构造函数的调用。例如下面的程序:
class CTest{
public:
CTest(int n){}
CTest(int n,int m){}
CTest(){}};
int main(){
CTest array1[3]={1,CTest(1,2)};
CTest array2[3]={CTest(2,3),CTest(1,2),1};
CTest* pArray[3]={new CTest(4),new CTest(1,2)};
return 0;}
pArray数组是一个指针数组, 其元素不是CTest类的对象,而是CTest类的指针。把pArray[0]和pArray[1]初始化为指向动态分配的CTest对象的指针,而这两个动态分配出来的CTest对象又分别是构造函数(1)和构造函数(2)初始化的。pArray[2]没有初始化,其值是随机的,不知道指向哪里,只生成了两个CTest对象,而不是三个,所以也只调用了两次CTest类的构造函数。
【复制构造函数】
复制构造函数是构造函数的一种,也称拷贝构造函数,它只有一个参数,参数类型是本类的引用。复制构造函数的参数可以是const引用,也可以是非const引用。一般使用前者,这样既能以常量对象(初始化后值不能改变的对象)作为参数,也能以非常量对象作为参数去初始化其他对象。一个类中写两个复制构造函数,一个参数是const引用,另一个的参数是非const引用,也是可以的。如果类的设计者不写复制构造函数,编译器就会自动生成复制构造函数。大多数情况下,其作用是实现从源对象到目标对象逐个字节的复制,即使得目标对象的每个成员变量都变得和源对象相等。编译器自动生成的复制构造函数称为“默认复制构造函数”。
注意:默认构造函数(即无参构造函数)不一定存在,但是复制构造函数总是会存在。
如果编写了复制构造函数,则默认复制构造函数就不存在了。
自己编写的复制构造函数并不一定要做复制工作(如果只做复制工作,那么使用编译器自动生成的默认复制构造函数就行了)。但从习惯上来讲,复制构造函数还是应该完成类似于复制的工作为好,在此基础上还可以根据需要做些别的操作。
构造函数不能以本类的对象作为唯一的参数,以免和复制构造函数相混淆。
复制构造函数被调用的三种情况
(1)当用一个对象去初始化同类的另一个对象时,会引发复制构造函数被调用(赋值语句不会引发复制构造函数的调用)。
(2)如果函数F的参数是类A的对象,那么当F被调用时,类A的复制构造函数将被调用。换句话说,作为形参的对象,是用复制构造函数初始化的,而且调用复制构造函数时的参数,就是调用函数时所给的实参。
#include<iostream>
using namespace std;
class A{
A(){}
A(A &a){
cout<<"Copy constructor called"<<endl;}};
void Func(A a){}
int main(){
A a;
Func(a);
return 0;}
函数的形参的值等于被调用时对应的实参,这不一定是正确的。如果形参是一个对象,那么形参的值是否等于实参,取决于该对象所属的类的复制构造函数是如何实现的。例如上面的例子,Fun函数的形参a的值在进入函数时是随机的,未必等于实参,因为复制构造函数没有做复制的工作。
以对象作为函数的形参,在函数被调用时,生成的形参要用复制构造函数初始化,这会带来时间上的开销。如果用对象的引用而不是对象作为形参,就没有这个问题了。但是以引用作为形参有一定的风险,因为这种情况下如果形参的值发生改变,实参的值也会跟着改变。如果要确保实参的值不会改变,又希望避免复制构造函数带来的开销,解决办法就是将形参声明为对象的const引用。
void Function(const Complex &c){
...
}
这样,在Function函数中出现任何有可能导致c的值被修改的语句,都会引发编译错误。
(3)如果函数的返回值是类A的对象,则函数返回时,类A的复制构造函数被调用。换言之,作为函数返回值的对象是用复制构造函数初始化的,而调用复制构造函数时的实参,就是return语句返回的对象。
【类型转换构造函数】
除复制构造函数外,只有一个参数的构造函数一般都可以称作类型转换构造函数,因为这样的构造函数能起到类型自动转换的作用。
#include<iostream>
using namespace std;
class Complex{
pubilc:
double real,imag;
Complex(int i) //类型转换构造函数
{
cout<<"IntConstructor called"<<endl;
imag=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)这个构造函数就是类型转换构造函数。可以看出,该构造函数一共被调用了两次。第一次来自于对c2的初始化,第二次来自于对第20行的赋值语句。这条赋值语句的等号两边的类型是不匹配的,之所以不会报错,就是因为Complex(int)这个类型转换构造函数能够接受一个整型参数。因此,编译器在处理这条赋值语句时,会在等号右边自动生成一个临时的Complex对象,该临时对象以9为实参,用Complex(int)构造函数初始化,然后再将这个临时对象的值赋给c1,也可以说是9被自动转换成一个Complex对象然后再赋值给c1。要注意,第19行是初始化语句而不是赋值语句,编译器经过优化后,往往不会将12转换成一个临时对象,而是直接以12作为参数调用Complex(int)构造函数来初始化c2。
【析构函数】
析构函数时成员函数的一种,它的名字与类名相同,但前面要加“~",没有参数和返回值。一个类有且仅有一个析构函数。如果定义类时没有写析构函数,则编译器生成默认析构函数。如果定义了析构函数,则编译器不生成默认析构函数。
析构函数在对象消亡时即自动调用。可以定义析构函数在对象消亡前做善后工作。例如:对象如果在生存期间用new运算符动态分配了内存,则在各处写delete语句以确保程序的每条执行路劲都能释放这片内存是比较麻烦的事情。有了析构函数,只要在析构函数中调用delete语句,就能确保对象运行中用new运算符分配的空间在对象消亡时被释放。
函数的参数对象以及作为函数返回值的对象,在消亡时也会引发析构函数调用。
【构造函数、析构函数和变量的生存期】
构造函数在对象生成时会被调用,析构函数在对象消亡时会被调用。对象何时生成和消亡是由对象的生存期决定的。下面通过一个例子来加深对构造函数、析构函数和变量的生存期的理解。
#include<iostream>
using namespace std;
class Demo{
int id;
public:
Demo(int i)
{
id=i;
cout<<"id="<<id<<"constructed"<<endl;
}
~Demo()
{
cout<<"id="<<id<<"destructed"<<endl;
}
};
Demo d1(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;
}
程序输出的结果
id=1 constructed
id=4 constructed
id=6 constructed
id=6 destructed
main
id=5 constructed
id=5 destructed
id=2 constructed
id=3 constructed
func
id=3 destructed
main ends
id=6 destructed
id=2 destructed
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)行的输出是由于进入Fun函数后,执行第19行的静态局部对象d2初始化导致的。静态局部对象在函数第一次被调用并执行到定义它的语句时初始化,生存期一直持续到整个程序结束,所以即便Func函数调用结束,d2也不会消亡。Fun函数中的d3初始化导致了第9)行输出。第30行,Func函数调用结束后,d3消亡导致第11)行输出。
main函数结束时,其局部变量d4消亡,导致第13)行输出。整个程序结束时,全局对象d1和静态局部对象d2消亡,导致最后两行输出。
【静态成员变量和静态成员函数】
类的静态成员有两种:静态成员变量和静态成员函数。静态成员变量就是在定义时前面加了static关键字的成员变量;静态成员函数就是在声明时前面加了static关键字的成员函数。
普通成员变量每个对象有各自的一份,而静态成员变量只有一份,被所有同类对象共享。普通成员函数一定是作用在某个对象上的,而静态成员函数并不具体作用在某个对象上。
访问静态成员时,则通过类名::成员名的方式访问,不需要指明被访问的成员属于哪个对象或作用于哪个对象。因此,甚至可以在还没有任何对象生成时就访问一个类的静态成员。(非静态成员的访问方式也适用于静态成员)
使用sizeof运算符计算对象所占用的存储空间时,不会将静态成员变量计算在内。
静态成员变量本质上是全局变量。一个类,哪怕一个对象都不存在,其静态成员变量也存在。静态成员函数并不需要作用在某个具体对象上,因此本质上是全局函数。
设置静态成员目的,是为了将和某些类紧密相关的全局变量和全局函数写到类里面,形式上成为一个整体。
静态成员变量必须在类定义的外面专门声明,声明时变量名前面加类名::,声明的同时可以初始化。如果没有声明,那么程序编译时虽然不会报错,但是在链接(link)阶段会报告"标识符找不到",不能生成.exe文件。
因为静态成员函数不具体作用于某个对象,所以静态成员函数内部不能访问非静态成员变量,也不能调用非静态成员函数。
【常量对象和常量成员函数】
如果希望某个对象的值初始化以后就再也不被改变,则定义该对象时可以在前面加const关键字,使之成为常量对象。
常量对象一旦被初始化后,其值就再也不能更改。因此,不能通过常量对象调用普通成员函数,因为普通成员函数在执行过程中有可能修改对象的值。
但是可以通过常量对象调用常量成员函数,是因为常量成员函数确保不会修改任何非静态成员变量的值,编译器如果发现常量成员函数内出现了有可能修改非静态成员变量的语句,就会出错。因此,常量成员函数内部也不允许调用同类的其他非常量成员函数(静态成员函数除外)。
两个成员函数的名字和参数表相同,但一个是const的,一个不是,则它们算重载。
基本上,如果一个成员函数中没有调用非常量成员函数,也没有修改成员变量的值,那么将其写成常量成员函数是好的习惯。
【成员对象和封闭类】
一个类的成员变量如果是另一个类的对象,就称之为成员对象。包含成员对象的类叫封闭类。
当封闭类的对象生成并初始化时,它包含的成员对象也需要被初始化,这就会引发成员对象构造函数的调用,可以通过在定义封闭类的构造函数时,添加初始化列表的方法解决。
封闭类对象生成时,先执行所有成员对象的构造函数,然后才执行封闭类自己的构造函数。成员对象构造函数的执行次序和成员对象在类的定义中的次序一致,也它们在构造函数初始化列表中出现的次序无关。当封闭类对象消亡时,先执行封闭类的析构函数,然后再执行成员对象的析构函数,成员对象析构函数的执行次序和构造函数的执行次序相反,即先构造的后析构。
封闭类的对象在初始化时,要先执行成员对象的构造函数,是因为封闭类的构造函数中有可能用到成员对象,如果此时成员对象还没有初始化,那么就不合理了。
【封闭类的复制构造函数】
封闭类的对象,如果是用默认复制构造函数初始化的,那么它包含的成员对象也会有复制构造函数初始化。
【const成员和引用成员】
类还可以有常量型成员变量和引用型成员变量。这两种类型的成员变量必须在构造函数的初始化列表中进行初始化。常量型成员变量的值一旦初始化,就不能再改变。
【友元】
在定义一个类的时候,可以把一些函数(包括全局函数和其他类的成员函数)声明为“友元”,这些函数成为该类的友元函数,在友元函数的内部就能访问该类对象的私有成员。
全局函数声明为友元的写法:
friend 返回值类型 函数名(参数表);
其他类的成员函数声明为友元的写法:
friend 返回值类型 其他类的类名::成员函数名(参数表);
但是,不能将其他类的私有成员函数声明为友元。
类的声明:
class 类名;
尽管可以提前声明,但是在一个类的定义出现之前,仍然不能有任何会导致该类对象被生成的语句。但使用该类的指针或引用是没有问题的。
【友元类】
一个类A可以将另一个类B声明为自己的友元,类B的所有成员函数就都可以访问类A对象的私有成员。
friend class 类名;
一般来说,类A将类B声明为友元类,则类B最好从逻辑上和类A有比较接近的关系。
友元关系在类之间不能传递。
【this指针】
C++程序
class CCar
{
public:
int price;
void SetPrice(int p);
};
void CCar::SetPrice(int p)
{
price=p;
}
int main()
{
CCar car;
car.SetPrice(20000);
return 0;
}
翻译后的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++编译器从本质上来说也是按上面的方法来处理成员函数和对成员函数的调用的,即非静态成员函数实际上的形参个数比程序员写的多一个。多出来的参数就是所谓的this指针。这个this指针指向了成员函数作用的对象,在成员函数执行的过程中,正是通过this指针才找到对象所在的地址,因而就能找到对象的所有非静态成员的地址。
C++规定,在非静态成员函数内部可以直接使用this关键字,this就代表指向该函数所作用的对象的指针。
因为静态成员函数并不作用于某个对象,所以在其内部不能使用this指针。
【在多个文件中使用类】
在有多个文件的C++程序中,如果多个.cpp文件都用到同一个类,可以将类的定义写在一个头文件中,然后在各个.cpp文件中包含该头文件。类的非内联成员函数的函数体只能出现在某一个.cpp文件中,不能放在头文件中被多个.cpp文件包含,否则链接时会发生重复定义的错误。类的内联成员函数的函数体最好写在头文件中,这样编译器在处理内联函数的调用语句时,就能在本文件包含的头文件中找到内联函数的代码,并将这些代码插入调用语句处。内联成员函数放在头文件中被多个.cpp文件包含,不会导致重复定义的错误。
小结
定义类时,如果一个构造函数都不写,则编译器自动生成默认(无参)构造函数和复制构造函数。如果编写了构造函数,则编译器不自动生成默认构造函数。一个类不一定会有默认构造函数,但一定会有复制构造函数。
任何生成对象的语句都要说明对象是用哪个构造函数来初始化的。即便定义对象数组,也要对数组中的每个元素如何初始化进行说明。如果不说明,则编译器认为对象是用默认构造函数或参数全部可以省略的构造函数初始化。在这种情况下,如果类没有默认构造函数或者参数可以省略的构造函数,则编译出错。
复制构造函数只有一个参数,类型是同类的引用。
只有一个参数的构造函数(复制构造函数除外)都可以称作为类型转换构造函数。类型转换构造函数可以使一个其他类型的变量或常量转换成一个临时对象。
对象在消亡时会调用析构函数。
每个对象有一份各自的普通成员变量,但静态成员变量只有一份,被所有对象所共享。静态成员函数不具体作用于某个对象。即便对象不存在,也可以访问类的静态成员。静态成员函数内部不能访问非静态成员变量,也不能调用非静态成员函数。
常量对象上面不能执行非常量成员函数,只能执行常量成员函数。
包含对象的类叫做封闭类。任何能够生产封闭类对象的语句,都要说明对象中包含的成员对象是如何初始化的。如果不说明,则编译器认为成员对象用默认构造函数或参数全部可以省略的构造函数初始化。在封闭类的构造函数初始化列表中可以说明成员对象如何初始化。封闭类对象生成时,先执行成员对象的构造函数,再执行自身的构造函数;封闭类对象消亡时,先执行自身的析构函数,再执行成员对象的析构函数。
const成员和引用成员必须在构造函数的初始化列表中初始化,此后值不可修改。
友元分为友元函数和友元类,友元关系不能传递。
成员函数中出现的this指针,就是指向成员函数所作用的对象的指针。因此,静态成员函数内部不能出现this指针。成员函数实际上的参数个数比表面上看到的多一个,多出来的参数就是this指针。