目录
一、写在前面:
我们学习了类的基本构建与输出后,我们进一步学习类的初始化操作。
在程序设计中我们常常会用到变量的初始化,我们在面向过程的程序设计中直接在定义变量是就可以实现,但是基于对象的程序中,该如何使每个对象成员得到自己的初始值呢?这里就需要用到构造函数。
二、情境引入:
为了便于理解,我们还是以一道题为例,全面的分析怎么写这三个函数:
声明一个学生类类型:数据成员(char*)、年龄(short);提供:构造函数、析构函数、复制构造函数。功能:实现对一名学生信息的输入输出、实现对20名学生信息的输入输出。
三、构造函数:
1.构造函数概述:
所谓构造函数,就是一种特殊的成员函数,与其他成员函数不同的是,不需要用户调用它,而是在建立对象时自动执行。构造函数的函数名和类名相同。
2.构造函数应用:
(1)在函数体内初始化
基于上面的概述,我们可以编写一个最基本的构造函数,完成对学生年龄的初始化操作:
#include<iostream>
using namespace std;
class student
{
private:
short age;
public:
student() {//构造函数,将age初始化为99
cout << "构造函数被调用了哦~" << endl;
//用于测试构造函数调用的时机
age = 99;
}
void output() {//输出函数
cout << age;
}
};
int main()
{
student s1;//定义类对象时自动调用构造函数
s1.output();
}
程序运行结果如下图:
根据运行的结果我们可以看出,在声明对象的同时,我们没有有意地调用构造函数,但却被主调函数执行了,这样的调用方式我们称之为“隐式调用”。
(2)含参数的构造函数
不含参数的构造函数方便我们在白鞋程序是为每一组对象都赋予相同的初始值,但是,当我们需要对不同的对象赋予不同的初始值时,我们又该怎么做呢?这里就用到了我们含参数的构造函数了,我们可以把初始值作为参数传入到函数中,我们在函数内为对象赋初值,这样就完成了对象的初始化工作,程序实现如下:
#include<iostream>
using namespace std;
class student
{
private:
short age;
public:
student(int n)//18作为形参传入构造函数
{
age = n; //将18赋值给成员age
}
void output() {//输出函数
cout << this->age;
}
};
int main()
{
student s1(18);//18作为参数传入到构造函数中
s1.output();
}
程序运行结果如下:
需要注意的是,这种调用方式存在一个问题就是在主函数中一定要在定义对象时给定初始者。因为我们自定义的构造函数会屏蔽掉系统自带的构造函数,因此我们自定义构造函数后一定要为其给定初始值,但是下面这个方法就可以完全解决这个问题,请看下面~
(3)构造函数的缺省
学到这里,我们不禁突发奇想:我们能否编写一个构造函数,当我们在声明对象时赋初值时就将其初始化为给定的初值,否则就按统一的初值进行初始化呢?答案是可以的,我们就用到构造函数的缺省:构造函数中参数的值既可以通过实参传递,也可以指定为某些默认值,即如果用户不指定实参值,编译系统就使形参取默认值。在构造函数中也可以采用这样的方法来实现初始化。程序实现如下:
#include<iostream>
using namespace std;
class student
{
private:
short age;
public:
student(int n=99)//当参数缺省时,age被初始化为99
{
age = n; //将18赋值给成员age
}
void output() {//输出函数
cout << this->age << endl;
}
};
int main()
{
student s1(18);//18作为参数传入到构造函数中,此时age初始化为18
student s2;//构造函数参数的缺省,此时age为实参给定的默认值99
s1.output();
s2.output();
}
程序运行结果如下图:
(4)构造函数的重载
函数重载,就是具有相同的名字,而参数的个数或类型不同的两个函数。如果我们自定义的构造函数会屏蔽掉系统的函数,我们也可以通过利用构造函数的重载解决(2)的报错问题。
这里定义了两个构造函数的重载,一个构造函数无参,一个函数含参,从而我们在主调函数中定义对象时如果传参,就会调用第二个构造函数,如果无参,就会调用第一个构造函数。
#include<iostream>
using namespace std;
class student
{
private:
short age;
public:
student()//对象s2在定义时没有参数,所以调用的是这个构造函数
{
age = 99;
}
student(int n) //对象s1向函数传了一个参数,因此定义对象时调用的是这个构造函数
{
age = n;
}
void output() {//输出函数
cout << this->age << endl;
}
};
int main()
{
student s1(19);//将19传入构造函数形参,调用含参构造函数
s1.output();
student s2;//只定义定义对象,没有传参,调用无参构造函数
s2.output();
}
程序运行如下:
(5)参数初始化表法
除此之外,C++还提供了另一种初始化数据成员的方法——参数初始化表法,语法格式为:
类名::构造函数名([参数表])[:成员初始化表]
{
[构造函数体;]
}
(中括号内可省略)
这个思路在空间的动态分配中会用到,我们还是先利用这种方法进行年龄的初始化,程序实现如下:
#include<iostream>
using namespace std;
class student
{
private:
short age;
public:
student(int n) :age(n)//初始化表将其初始化为19
{}
void output() {//输出函数
cout << this->age << endl;
}
};
int main()
{
student s1(19);//将19传入构造函数形参
s1.output();
}
程序运行如下:
(6)数组初始化循环法
这个是面向对象的程序设计中最常用到的方法,就是搬到构造函数里面,我们不过多赘述,直接上程序!
i.不缺省版本:
这里我们在主调函数中定义了一个数组,传入构造函数,同时因为在参数中没有赋值,因此在主调函数传参时不能缺省。
#include<iostream>
#include<cstring>
using namespace std;
class student
{
private:
int score[3];
short age;
public:
student(int scorecpy[], short n)
{
for (int i = 0; i < n; i++)//循环赋值
{
score[i] = scorecpy[i];
}
age = n;
}
void output() {//输出函数
for(int i=0;i<3;i++)
cout << this->score[i] << " ";
cout << " age:" << this->age << endl;
}
};
int main()
{
int score[3] = { 100,99,98 };//传入构造函数的数组
student s1(score,18);
s1.output();
}
程序运行如下:
ii.缺省版本:
我们同时也可以将数组拆分成一个一个的单一变量,在主调函数中依次对其进行赋值,从而实现构造函数初始化数组的缺省,程序实现如下:
#include<iostream>
#include<cstring>
using namespace std;
class student
{
private:
int score[3];
short age;
public:
student(int a = 98, int b = 99, int c = 100, short n=19)
{
score[0] = a;
score[1] = b;
score[2] = c;
age = n;
}
void output() {//输出函数
for (int i = 0; i < 3; i++)
cout << this->score[i] << " ";
cout << " age:" << this->age << endl;
}
};
int main()
{
student s1(1,2,3,18);
s1.output();
student s2;
s2.output();
}
代码解释:
这里在构造函数的参数部分给出了数组元素的三个初始值a=98,b=99,c=100。因此当我们在主调函数中定义对象时给定参数时,就会将参数赋值给对象成员;当我们定义对象时没有给定参数时,就会默认为构造函数参数的四个初始值。因此s1给构造函数传递了新参数,所以初始化为实参,s2在定义时没有给定参数,因此初始化为形参的四个值。
程序运行结果如下:
(7)数组初始化strcpy法
如果数据成员是数组,则应当在构造函数的函数体中用语句对其赋值,而不能在参数初始化表中对其赋值。下面我们就在构造函数的函数体中用strcpy对字符型数组name进行赋值。程序实现如下:
#include<iostream>
#include<cstring>
using namespace std;
class student
{
private:
char name[9];
short age;
public:
student(char namecpy[], int n)
{
strcpy_s(name, strlen(namecpy) + 1, namecpy);
age = n;
}
void output() {//输出函数
cout << "name: " << this->name << " age:" << this->age << endl;
}
};
int main()
{
char name[9] = "tjl";
student s1(name, 18);
s1.output();;
}
当我们在声类的时候声明了一个字符数组成员时,我们可以在主调函数中定义一个我们要赋初值的数组,并把这个数组传入到构造函数中,之后在构造函数中用strcpy将这个数组赋值给数据成员。VS2022新标准:strcpy_s接受三个参数,一个被改变的char类型,一个数组长度,一个要被复制的const char类型,因为要预留一个位置放'\0',因此第二个参数strlen(namecpy)+1。程序运行如下图:
(8)数组初始化指针法
下面就是C语言的灵魂了:指针,我们在声明类中数组成员时,可以只为其提供一个指针,我们可以在构造函数中为其开辟空间并初始化赋值,从而实现内存的动态分配,我们先看代码:
#include<iostream>
#include<cstring>
using namespace std;
class student
{
private:
char* name;
short age;
public:
student(char namecpy[], short n=19)
{
loop:
name=new char[89];
if (name != NULL)
strcpy_s(name, strlen(namecpy) + 1, namecpy);
else
goto loop;
age = n;
}
void output() {//输出函数
cout << this->name << " ";
cout << " age:" << this->age << endl;
}
};
int main()
{
char name[99]="tjl";
student s1(name , 18);
s1.output();
}
代码分析:
我们在类的声明中定义了一个指向name的指针,此时这个指针为空,我们也不知道这个name数组的长度为多少,这时我们在构造函数中为name开辟一个长度为89的数组空间,我们用if语句检查空间是否开辟成功,如果成功,用strcpy_s函数为其赋值,这样就实现了数组的初始化,同时也完成了空间的动态分配。
代码运行如下:
四、写在最后:
这些只是构造函数的最基础的部分,我们还可以多种方法进行结合,自定义出更简洁的构造函数,比如我们可以通过用初始化表法部分代替函数体中的赋值语句等等。慢慢挖掘吧嘻嘻嘻~