构造函数(C++学习笔记 17)

一、构造函数

  • 构造函数是属于某一个类的
  • 类是一种抽象的数据类型,它不占存储空间,不能容纳具体的数据,因此在类声明中不能给数据成员赋初值。
    例如,下面的描述是错误的:
class Complex{
	double real=0;  //错误,在类声明中不能给数据成员赋初值
	double imag=0;  //错误,在类声明中不能给数据成员赋初值
};

如果一个类中的所有成员,都是公有的,则可以在定义对象时对数据成员进行初始化。

class Complex{
	public:
	  double real;
	  double imag;
};
Complex c1={1.1,2.2};   //定义类Complex的对象c1,将c1的数据成员real和imag分别初始化为1.1和2.2

如果类中包含私有的或保护的成员时,就不能用这种方法进行初始化。 这时,可以采用类中的公有成员函数来对对象中的数据成员赋初值,例如:

#include<iostream>
#include<cmath>
using namespace std;
struct Complex{
    private:
	double real;  //复数的实部
	double imag;  //复数的虚部
    public:
	void init(double r,double i)  //定义函数init,给real和imag赋初值
	{
        	real=r;
        	imag=i;
        }
        double abscomplex(){  //定义函数abscomplex,求复数的绝对值
 		double t;
 		t=real*real+imag*imag;  
 		return sqrt(t);
 	}
};
int main()
{
	Complex A;
	A.init(1.1,2.2);
	cout<<"复数的绝对值是:"<<A.abscomplex()<<endl;
	return 0;
}

但是,使用成员函数给数据成员赋初值既不方便也容易忘记,甚至可能出错,所以,C++利用构造函数来完成对象的初始化。
构造函数是一种特殊的成员函数,
构造函数作用: 为对象分配空间,对对象进行初始化,即对数据成员赋初值,这些数据成员通常为私有成员。
要求: ①构造函数的名字必须与类名相同,而不能由用户任意命名;
    ②它可以有任意类型的参数,也可以不带参数(不带参数的构造函数对对象的初始化是固定的)。但不能具有返回值类型,甚至也不能说明为void类型;
    ③它不需要由用户来调用,而是在建立对象时自动执行的,因此,“A.Complex(1.1,2.2);”这种用法是错误的;
    ④构造函数必须为公有类型,写在Public之下;
    ⑤与普通的成员函数一样,构造函数的函数体可写在类体内,也可写在类体外;
    ⑥在构造函数的函数体中不仅可以对数据成员赋初值,而且可以包含其它语句,但是,为了保持构造函数的功能清晰,一般不提倡在构造函数中加入与初始化无关的内容
   ⑦通常需要给每个类定义构造函数,如果没有给类定义构造函数,则编译系统自动地生成一个默认构造函数。 例如,编译系统为类Complex生成一个默认构造函数:Complex::Complex(){};
这个默认的构造函数不带任何参数,函数体是空的,它只能为对象开辟数据成员存储空间,而不能给对象中的数据成员赋初值。

例如,为类Complex建立一个构造函数。

class Complex{
	private:
		double real;   //表示复数的实部
		double imag;  //表示复数的虚部
	public:
		Complex(double r,double i){   //定义构造函数,其名与类名相同 
 			real=r;   //在构造函数中,对私有数据real和imag赋值 
			imag=i;
		} 
		double abscomplex(){
			double t;
			t=real*real+imag*imag;
			return sqrt(t);
		}
};

也可把构造函数的函数体写在类体外:

class Complex{
	private:
		double real;   //表示复数的实部
		double imag;  //表示复数的虚部
	public:
		Complex(double r,double i);  //声明构造函数原型
		double abscomplex();   //声明成员函数原型
};
Complex::Complex(double r,double i){   //在类外定义构造函数
	real=r;
	imag=i;
}
double Complex::abscomplex(){  //在类外定义成员函数
	double t;
	t=real*real+imag*imag;
	return sqrt(t);
}

构造函数可以不带参数,不带参数的构造函数对对象的初始化是固定的:

class Complex{
	private:
		double real;   //表示复数的实部
		double imag;  //表示复数的虚部
	public:
		Complex(){   //不带参数的构造函数
 			real=0;   
			imag=0;
		} 
		void init(double r,double i){  //公有成员函数,作为类的外部接口
			real=r;
			imag=i;
		}
		double abscomplex(){
			double t;
			t=real*real+imag*imag;
			return sqrt(t);
		}
};
int main(){
	Complex A;
	A.init(1.1,2.2);  //调用公有成员函数init
	cout<<"复数的绝对值是:"<<A.abscomplex()<<endl;
	return 0;
}

二、对象的初始化(在构造函数中用赋值语句进行)

在建立对象的同时,采用构造函数给数据成员赋初值,通常有以下两种形式:
形式1: 类名 对象名[(实参表)];
这里的“类名”与构造函数名相同,“实参表”是为构造函数提供的实际参数

形式2: 类名 *指针变量名=new 类名[ ( 实参表 ) ]

例如, 建立对象的同时,采用形式1,利用构造函数给数据成员赋初值。

#include<iostream>
#include<cmath>
using namespace std;
class Complex{
	private:
		double real;   //表示复数的实部
		double imag;  //表示复数的虚部
	public:
		Complex(double r,double i){   //定义构造函数,其名与类名相同 
 			real=r;   //在构造函数中,对私有数据real和imag赋值 
			imag=i;
		} 
		double abscomplex(){
			double t;
			t=real*real+imag*imag;
			return sqrt(t);
		}
};
int main(){
	Complex A(1.1,2.2);  //定义类Complex的对象A时调用构造函数Complex
	cout<<"复数的绝对值是:"<<A.abscomplex()<<endl;
	return 0; 
}

从上边的例子中可看出,在main函数中,没有显示调用构造函数Complex的语句。构造函数是在定义对象时被系统自动调用的。

例如, 采用形式2,利用构造函数给数据成员赋初值。

#include<iostream>
#include<cmath>
using namespace std;
class Complex{
	private:
		double real;   //表示复数的实部
		double imag;  //表示复数的虚部
	public:
		Complex(double r,double i){   //定义构造函数,其名与类名相同 
 			real=r;   //在构造函数中,对私有数据real和imag赋值 
			imag=i;
		} 
		double abscomplex(){
			double t;
			t=real*real+imag*imag;
			return sqrt(t);
		}
};
int main(){
	Complex *pa=new Complex(1.1,2.2);
	cout<<"复数的绝对值是:"<<pa->abscomplex()<<endl;
	delete pa;
	return 0; 
}

这时,编译系统开辟了一段内存空间,并在此空间中存放了一个Complex类的对象,同时调用了该类的构造函数给数据成员赋初值。这个对象没有名字,称为无名对象。但是该对象有地址,这个地址存放在指针pa中,访问用new建立的对象时一般是不用对象名的,而是通过指针访问。当new建立的对象使用结束,不再需要它时,可用delete运算符予以释放。

三、用成员初始化列表对数据成员初始化

  • 用成员初始化列表对数据成员初始化,不在函数体内用赋值语句对数据成员初始化,而是在函数首部实现的。
  • 成员初始化列表写法方便、简练,尤其当需要初始化的数据成员较多时更显其优越性。
  • 对于用const修饰的数据成员,或是引用类型的数据成员,是不允许用赋值语句直接赋值的,因此只能用成员初始化列表对其进行初始化。
  • 带有成员初始化列表的构造函数的一般形式为:
    类名::构造函数名([参数表])[:成员初始化列表]
    {
        //构造函数体
    }

成员初始化列表的一般形式为:
数据成员名1(初始值1), 数据成员名1(初始值1), ··· ···

例如, 用成员初始化列表对引用类型的数据成员和const修饰的数据成员初始化。

#include<iostream>
using namespace std;
class A{
	public:
		A(int x1):x(x1),rx(x),pi(3.14)   //用成员初始化列表对引用类型的数据成员rx和const修饰的数据成员pi初始化 
		{ 
		}
		void print(){
			cout<<"x="<<x<<" "<<"rx="<<rx<<" "<<"pi="<<pi<<endl;
		}
	private:
		int x;
		int &rx;	//rx是整形变量的引用 
		const double pi;	//pi是用const修饰的常量 
};
int main(){
	A a(10);
	a.print();
	return 0;
}

数据成员是按照它们在类中声明的顺序进行初始化的。
例如, 用成员初始化列表对数据成员进行初始化。

#include<iostream>
using namespace std;
class D{
	public:
		D(int i):mem2(i),mem1(mem2+1){
			cout<<"mem1:"<<mem1<<endl;
			cout<<"mem2:"<<mem2<<endl;
		}
	private:
		int mem1;
		int mem2;
};
int main(){
	D d(15);
	return 0;
}

结果为:
在这里插入图片描述
在上例中,用“mem2+1”来初始化“mem1”,但是数据成员是按照它们在类中声明的顺序进行初始化的,数据成员mem1应在mem2 之前被初始化,因此,在mem2没有被初始化时,mem1使用“mem2+1”的值来初始化,所得结果是随机值,而不是16。

四、构造函数的重载

例如, 构造函数重载的应用。

#include<iostream>
using namespace std;
class Date{
	public:
		Date();  //无参的构造函数 ,对对象的初始化是固定的
		Date(int y,int m,int d);  //带有3个参数的构造函数
		void showDate();
	private:
		int year;
		int month;
		int day; 
};
Date::Date()  //定义一个无参数的构造函数
{
	year=2018;
	month=11;
	day=10;	
} 
Date::Date(int y,int m,int d){
	year=y;
	month=m;
	day=d;
}
inline void Date::showDate(){
	cout<<year<<"."<<month<<"."<<day<<endl;
}
int main(){
	Date date1;
	cout<<"Date1 output:"<<endl;
	date1.showDate();
	Date date2(2008,8,8);
	cout<<"Date2 output:"<<endl;
	date2.showDate();
	return 0;
}

输出结果:
在这里插入图片描述
注意:
①如果在类中没有定义构造函数,系统会自动提供一个函数体为空的默认构造函数。但是,只要类中定义了一个构造函数(不一定是无参构造函数),系统将不再给它提供默认构造函数。
例如,

#include<iostream>
using namespace std;
class Location{
	public:
		Location(int m,int n){
			X=m;
			Y=n;
		}
		void init(int initx,int inity){
			X=initx;
			Y=inity;
		}
		int getx(){
			return X;
		}
		int gety(){
			return Y;
		}
	private:
		int X,Y;
}; 
int main(){
	Location A3;
	A3.init(785,900);
	cout<<A3.getx()<<" "<<A3.gety()<<endl;
	return 0;
}

此程序,在“Location A3;”处出现错误,如果改为“Location A3(0,0);”即调用了构造函数。
②尽管在一个类中可以包含多个构造函数,但对每一个对象而言,建立对象时只执行其中的一个构造函数。(bad.cpp)

#include<iostream>
#include<stdlib.h>
#include<string>
using namespace std;
class Timer{
	public:
		Timer(){   //无参构造函数 
			seconds=0;
		}
		Timer(const char *t){    //含1个字符串参数的构造函数 
			seconds=atoi(t);  //atoi将字符串转换为int 
		}
		Timer(int t){  //含1个整型参数的构造函数 
			seconds=t;  
		}
		Timer(int min,int sec){  //含2个整型参数的构造函数 
			seconds=min*60+sec;
		}
		int gettime(){
			return seconds;
		}
	private:
		int seconds;
};
int main(){
	Timer a;  //定义类Timer的对象a,调用无参数的构造函数
	Timer b(10);  // 定义类Timer的对象b,调用含1个整型参数的构造函数 
	Timer c("20");  //定义类Timer的对象c,调用含1个字符串参数的构造函数 
	Timer d(1,10);  //定义类Timer的对象d,调用含2个整型参数的构造函数 
	cout<<"seconds1="<<a.gettime()<<endl;  
	cout<<"seconds2="<<b.gettime()<<endl;  
	cout<<"seconds3="<<c.gettime()<<endl;  
	cout<<"seconds4="<<d.gettime()<<endl;  
	return 0;
}

执行结果:
在这里插入图片描述

五、带默认参数的构造函数

对于带参数的构造函数,在定义对象时必须给构造函数的形参传递参数的值,否则构造函数将不被执行。
例如,带默认参数的构造函数。

#include<iostream> 
#include<cmath>
using namespace std;
class Complex{
	public:
		Complex(double r=0.0,double i=0.0);  //在声明构造函数时指定默认参数值
		double abscomplex(); 
	private:
		double real;
		double imag;
};
Complex::Complex(double r,double i){
	real=r;
	imag=i;
}
double Complex::abscomplex(){
	double t;
	t=real*real+imag*imag;
	return sqrt(t);
}
int main(){
	Complex S1;
	cout<<"复数1的绝对值是:"<<S1.abscomplex()<<endl;
	Complex S2(1.1);
	cout<<"复数2的绝对值是:"<<S2.abscomplex()<<endl;
	Complex S3(1.1,2.2);
	cout<<"复数1的绝对值是:"<<S3.abscomplex()<<endl;
}

运行结果:
在这里插入图片描述
说明:
在主函数main中定义了3个对象S1,S2,S3,它们都是合法的对象,由于传递参数的个数不同,使它们的私有数据成员real和imag取得不同的值。

①定义对象S1时,没有传递参数,所以real和imag均取构造函数的默认值。

②定义对象S2时,只传递了一个实参,这个参数传递给构造函数的第一个形参,而第2个形参取默认值。

③定义对象S3时,传递了两个实参,这两个实参分别传给了real和imag。

注意:
①如果构造函数在类的声明外定义,那么默认参数应该在类内声明构造函数原型时指定,而不能再类外构造函数定义时指定。

②如果构造函数的全部参数都指定了默认值,则在定义对象时可以指定1个或几个实参,也可以不给出实参,这时的构造函数也属于默认构造函数,因此不能同时再声明无参数的默认构造函数:“Complex();”。因为如语句:“Complex S1;” 编译系统将无法识别应该调用哪个构造函数,因此产生二义性。

③在一个类中定义了全部是默认参数的构造函数后,不能再定义重载构造函数,因此,一般不要同时使用构造函数的重载和有默认参数的构造函数。如:
Complex(double r=0.0,double i=0.0); //声明全部是默认参数的构造函数
Complex(double r); //声明有1个参数的构造函数
编译系统将无法判断应该调用哪个构造函数。

六、默认构造函数

默认构造函数的作用
当 对象被 默认初始化值初始化 时自动执行默认构造函数。

只有当类没有声明任何构造函数时,编译器才会自动地生成默认构造函数。

在实际中,如果定义了其他构造函数,那么最好也提供一个默认构造函数。

默认初始化 在以下情况下发生:

  • 当我们在块作用域内不使用任何初始值定义一个非静态变量或者数组时。
  • 当一个类本身含有类类型的成员且使用合成的默认构造函数时。
  • 当类类型的成员没有在构造函数初始值列表中显式地初始化时。

值初始化 在以下情况发生:

  • 在数组初始化的过程中如果我们提供的初始值数量少于数组大小时
  • 当我们不使用初始值定义一个局部静态变量时。
  • 当我们通过书写形式如T( )的表达式显式地请求值初始化时,其中T是类型名(vector 的一个构造函数只接受一个实参用于说明vector 大小,它就是使用一个这种形式的实参来对它的元素初始化器进行值初始化)。

类必须包含一个默认构造函数以便在上述情况下使用,其中大多数情况非常容易判断,不那么明显的一种情况是类的某些数据成员缺少默认构造函数。

猜你喜欢

转载自blog.csdn.net/aaqian1/article/details/83894795
今日推荐