C++中 类的构造函数理解(一)
C++中 类的构造函数理解(一)
写在前面
这段时间完成三个方面的事情:
1、继续巩固基础知识(主要是C++ 方面的知识)
2、尝试实现一个iOS的app,通过完成app,学习iOS开发中要用到的知识
3、完善实验室的研究项目,为毕业设计做准备
有了这三个安排之后,就可以把一天的时间大致分为三份了。对于C++ 知识点的学习这部分,主要是看《C++ Primer》以及本科使用的英文教材《C++:How to program》来进行,今天主要探索一下C++ 中类的构造函数。
类简介
什么是类
类(Class)是C++ 提供的一种抽象数据类型,用户可以定义自己的“类”来自定义数据类型。类可以说是C++ 中最重要的特征之一。为什么会出现类呢?我的理解是,C++ 中内置的数据类型(int、double、char等)不能满足用户处理复杂事务时的需求,因此C++ 中提供了一种抽象机制,允许用户DIY自己的数据类型,在DIY自己的数据类型也就是类的时候,需要告诉编译器,你的类名字是什么?它在哪里定义?它有哪些属性(数据成员)以及它可以有哪些操作(成员函数),把这套规则都定义好之后,用户就能在程序中操作自己的类以完成某些任务了。
类的数据成员
在上述简介中,提到了类的数据成员,那么类的数据成员到底是什么呢?“数据成员”这个说法是我学习《C++: How to program》时接触到的,很多人也将“数据成员”称为“成员变量”。简单的说,数据成员就是类的某些“属性”,比如学生类,对应的属性可能就有学号、姓名、专业、年级、课程等,这些属性可以用C++ 内置的数据类型来表示,也可以用一些结构体来表示,甚至也可以用其他类类型来表示。换句话说,类的数据成员是可以被操作的,类中的变量。
类的成员函数
成员函数和普通的函数功能是差不多的,它主要是用来操作数据成员的,或者说用来完成类的某些功能的。
更多类的知识本文就不再扩展了,本文主要探索类的构造函数的一些特性。
构造函数
构造函数简介
创建完类之后,我们在使用类之前,需要对类进行初始化。定义如何对类进行初始化的成员函数就称为构造函数。构造函数在创建类类型的对象时被执行。它的工作是保证每个对象的数据成员具有合适的初始值。构造函数的名字必须与类名字相同,它可以接受参数(当然也可以没有参数),同时也可以定义多个构造函数(也就是说构造函数允许重载)。但有一点要注意的是构造函数不能有返回值,就算是void也不行。另外,由于构造函数设计的初衷是为对象初始化,即初始化对象的数据成员,因此构造函数不能声明为const,否则程序报错。另外,一般情况下构造函数都是公有的,因为构造函数声明为私有的话,其他类将不能生成该类的对象。只有在单例模式下,才将构造函数声明为私有的,以防止类有多个实例出现。
默认构造函数
参数列表为空的构造函数称为默认构造函数。(这个定义不全面,下文中有提到,所有参数都指定了默认值的带参数的构造函数也是默认构造函数)
-
编译器合成的默认构造函数
我们已经知道构造函数的作用是定义类的初始化,那么如果我们不显示的定义构造函数,编译器能为我们创建一个类的实例(对象)吗?(这里啰嗦一句,类的对象也称为类的实例,二者是等价的概念,本文两种说法都有使用)。答案是肯定的,这是因为,当用户没有显示定义类的构造函数时,编译器会为该类生成一个默认的构造函数,也称为合成的构造函数。默认构造函数不会初始化对象的数据成员,但如果当前对象的数据成员中包含其他类类型,那么默认构造函数会调用其他类的默认构造函数。其形式如下:
class MyClass
{
public:
MyClass(){} // default constructor
} ;
编译器不合成默认构造函数的情况
(1)一旦用户定了构造函数之后(只要有一个,无论是公有还是私有,无论哪种形式,哪怕用户定义的形式与编译器合成的形式相同),编译器就不再为用户合成默认的构造函数。
(2)如果类包含内置或复合类型的成员,则不能依赖编译器来合成默认构造函数,因为编译器无法初始化复合类型的成员
-
用户自定义的默认构造函数
用户也可以显示的为自己的类定义默认构造函数,其形式如下:
class MyClass
{
public:
MyClass()
{
cout<<"Default constructor called."<<endl;
m1_int = 7;
m2_double = 0.8;
} // default constructor
private:
int m1_int;
double m2_double;
} ;
定义了上述形式的默认构造函数之后,如果声明一个MyClass的myclass对象,则默认构造函数就会被调用,myclass的两个数据成员就能被初始化。
-
类没有默认构造函数时会出现的情况
假定有一个类NoDefault,它没有默认构造函数,但有一个接受一个string实参的构造函数,因此编译器不会为NoDefault类合成默认的构造函数,这意味着:
(1)具有NoDefault成员的每个类的每个构造函数,必须通过传递一个初始的string值给NoDefault构造函数来显示地初始化NoDefault成员。
(2)编译器不会为具有NoDefault成员的类合成默认构造函数。如果这样的类希望提供默认构造函数,必须显示地定义,并且默认构造函数必须显示地初始化其NoDefault成员。
(3)NoDefault类型不能用作动态分配数组的元素类型。
(4)NoDefault类型的静态分配数组必须为每个元素提供一个显示的初始化式。
(5)如果有一个保存NoDefault对象的容器,例如vector,就不能使用接受容器大小而没有同时提供一个元素初始化式的构造函数。
构造函数初始化列表
构造函数化初始化列表形式如下:
class MyClass
{
public:
//第一种形式
MyClass() : m1_int(7), m2_double(0.8)
{
cout<<"Constructor 1 is called"<<endl;
}
//第二种形式
MyClass(int m1, double m2) : m1_int(m1), m2_double(m2)
{
cout<<"Constructor 2 is called"<<endl;
}
private:
int m1_int;
double m2_double;
} ;
构造函数初始化列表在构造函数名后添加一个冒号,冒号后是以逗号分隔的数据成员列表,每个数据成员后跟一个放在圆括号中的初始化形式。圆括号中的初始化形式可以是任意复杂的表达式。
这里提一下,类的数据成员被初始化的顺序是其定义的次序,而与构造函数初始化列表中的顺序无关。
在main函数中生成两个对象,观察构造函数被调用情况
int main()
{
cout<<"Test in main"<<endl;
MyClass myclass1;//调用第一种形式的构造函数
MyClass myclass(2,0.3);//调用第二种形式的构造函数
//或者用下面的方式创建对象
MyClass myclass1 = MyClass();//调用第一种形式的构造函数
MyClass myclass2 = MyClass(2,0.3);//调用第二种形式的构造函数
system("pause");
return 0;
}
输出结果如下:
值得一提的是,下面这句话的声明是正确的,可以通过编译
MyClass myclass1();
但这个时候我们并不是声明了一个对象,而是声明了类的一个函数。
默认实参的构造函数
默认实参的构造函数确保,在调用构造函数的时候,就算没有提供参数值,构造函数仍然可以正确的初始化类的对象。如果构造函数的所有参数都指定了默认值,这时候的构造函数也属于默认构造函数。同一个类只能有一个默认构造函数,例如下面的类Time
class Time
{
public:
Time( int = 0, int = 0, int = 0);
Time(){}
private:
int hour;
int minute;
int second;
} ;
在Time中,提供了一个所有参数都指定了默认值的带默认参数的构造函数,然后又提供了一个默认构造函数,当不创建任何对象的时候,是可以通过编译的,一旦创建对象,将无法通过编译,因为编译器不知道应该使用哪个默认构造函数来初始化对象,因此报错:
接下来继续看下默认参数的构造函数的列子
class Time
{
public:
Time( int h = 0, int m = 0, int s = 0)
{
setTime(h,m,s);
}
void setTime(int h, int m, int s)
{
setHour( h );
setMinute( m );
setSecond( s );
}
void setHour(int h)
{
hour = h;
}
int getHour()
{
return hour;
}
/////////////////////
void setMinute( int m)
{
minute = m;
}
int getMinute()
{
return minute;
}
/////////////////////
void setSecond( int s )
{
second = s;
}
int getSecond()
{
return second;
}
private:
int hour;
int minute;
int second;
} ;
测试代码
int main()
{
cout<<"Test in main"<<endl;
Time t1;//所有参数都使用默认值
Time t2( 2 );//时指定,分和秒为默认值
Time t3( 21, 24 );//时和分指定,秒为默认值
Time t4( 12, 25, 34 );//所有参数都指定
cout<<"Time of t1:"<<t1.getHour()<<" : "<<t1.getMinute()<<" : "<<t1.getSecond()<<endl;
cout<<"Time of t2:"<<t2.getHour()<<" : "<<t2.getMinute()<<" : "<<t2.getSecond()<<endl;
cout<<"Time of t3:"<<t3.getHour()<<" : "<<t3.getMinute()<<" : "<<t3.getSecond()<<endl;
cout<<"Time of t4:"<<t4.getHour()<<" : "<<t4.getMinute()<<" : "<<t4.getSecond()<<endl;
system("pause");
return 0;
}
输出如下:
可以看到,调用带默认参数的构造函数时,对象的数据成员的缺省顺序与构造函数中的顺序相反。
总结
本文对构造函数的探索暂时就到这里了,通过今天的探索,发现构造函数这一个知识点中蕴藏着相当多的内容,本文只是做了一个简单的概括。关于构造函数,还有很多都没有探究,比如构造函数之间的调用、拷贝构造函数、有多种类型的数据成员时(如全局动态、全局静态等)各成员被创建的时机、有类的继承发生时,构造函数的处理情况等等,这些内容就留待下篇文章去探索了。