前言
只有品尝过奋斗的滋味,才能体会人生的珍贵;只有勤耕不辍,才能不负韶华、不负自己。
上一篇关于C++学习的文章 C++学习——坚持(二)主要学习了面向对象的一些基本概念,这篇文章主要对类和对象进行进一步的探讨。
一:构造函数
1:简单了解
对于编写程序来说,构造函数可谓是很关键的一部分了,在 c++中,为给对象进行初始化,就提供了一种叫做构造函数的机制,实际上就是为成员变量赋初值的。
构造函数是类中特殊的成员函数,属于类的一部分。当给出类的定义时,由程序员来编写构造函数,如果没有编写类的构造函数,就由系统添加一个参数表为空、函数体也为空的构造函数,也就是说任何类都至少有一个构造函数。
声明对象后,可以用运算符 new 为对象进行初始化,这时调用的是所属对象的构造函数。构造函数的作用是完成对象的初始化工作,用来保证对象的初始状态是确定的
。
2:如何定义构造函数
当我们在定义一个类时,需要为类定义一个相应的构造函数,构造函数的函数名与类名相同,并且没有返回值。同时构造函数允许重载(指在程序的同一范围内声明几个功能相似的同名函数)。
构造函数的声明格式:
构造函数名(即类名)(形参1,形参2,......形参n);
我们假设一个类的成员变量是 X1,X2…Xn,那么在类体外定义构造函数时有以下几种方式:
形式一:
类名::类名(形参1,形参2,…形参n);
{
X1=形参1;
X2=形参2;
…
Xn=形参n;
}
形式二:、
类名::类名()
{
X1 = 初始化表达式1;
X2 = 初始化表达式2;
…
Xn = 初始化表达式n;
}
形式三:
类名::类名(形参1,形参2,…形参n):X1(形参1),X2(形参2)…Xn(形参n){}
例如 :使用固定值在初始化列表中为各个变量赋初值
myDate::myDate():year(2022),month(2),day(2){} //三个函数的初始化均在初始化列表中完成
或使用带入的参数值通过初始化列表为各成员变量赋初值
myDate::myDate(int y,int m,intd):year(y),month(m),day(d){}
在编写构造函数时,能够赋初值的地方有两处:一处是在初始化列表中;一处是在构造函数体内部;构造函数中为成员变量初始化时,既不要有重复,也不要有遗漏
3:怎么使用构造函数
当我们创建好一个对象时,系统就会根据创建对象的语句所提供的参数来选择调用哪一个构造函数去初始化该对象,系统在调用构造函数时是不需要程序员控制的。
假如没有提供参数,就调用无参的构造函数,并且类中定义了哪种构造函数,就决定了创建对象时可以使用那种形式。
例如:定义一个myDate类,构造函数如下:
myDate::myDate(int y = 2022,int m = 8,int d = 1)
{
year = y;
month = m;
day = d;
}
//myDate类的三个参数都有默认值
那么我们创建对象时,就可以采用以下形式:
myDate d0; //输出2022/8/1
myDate d1(2021); //输出 2021/8/1
myDate d2(2021,9); //输出 2021/9/1
myDate d3(2021,9,2); //输出 2021/9/2
调用构造函数时,给定的实参从左至右与形参进行匹配,如果实参的个数少于形参的个数,则不足的形参使用默认值进行初始化。
也可以通过构造函数创建对象指针
假设在类体定义一个构造函数:
myDate::myDate(int y,int m,int d)
{
year = y;
month = m;
day = d;
}
则创建对象时,可以采用:
myData *pa = new myDate (2022,8,1);pa ->printDate();
比如我们声明了一个 Clock类,然后定义一个无参构造函数并调用它:
#include <iostream>
#include <iomanip>
using namespace std;
class Clock
{
public:
Clock();//声明无参构造函数
void showTime();//声明显示时间的成员函数
private:
int hour;//声明表示小时的成员变量
int min; //声明表示分钟的成员变量
int sec;//声明表示秒的成员变量
};
Clock::Clock()//类外实现无参构造函数
{
hour=0;//初始化过程中直接给成员变量赋值
min=0;
sec=0;
}
void Clock::showTime()//类外实现成员函数
{
cout<<setw(2)<<setfill('1')<<hour<<":"<<setw(2)<<setfill('2')<<min<<":"<<setw(2)<<setfill('3')<<sec<<endl; //setw(n)函数用于设置字段宽度为 n位;setfill()函数用于填充字符
}
int main(int argc, char** argv)
{
Clock clock;//创建Clock对象clock
cout<<"clock";
clock.showTime();//对象调用成员函数
return 0;
}
输出结果:
同样的,我们定义一个有参构造函数并调用它;
# include <iostream>
# include <iomanip>
using namespace std;
class Clock
{
public:
Clock(int h,int m,int s);//声明有参构造函数
void showTime();//声明显示时间的成员函数
private:
int hour;
int min;
int sec;
} ;
Clock::Clock(int h,int m,int s) //类外实现有参构造函数
{
hour=h;//将初始值赋给成员变量
min=m;
sec=s;
}
void Clock::showTime()//类外声明成员函数
{
cout<<hour<<":"<<min<<":"<<sec<<endl;
}
int main()
{
Clock clock1(11,11,11);//创建Clock对象 clock1,并传入初值
cout<<"clock1"<<" ";
clock1.showTime();//clock1调用成员函数showTime(),用于显示时间
Clock clock2(22,22,22);//创建Clock对象 clock2,并传入初值
cout<<"clock2"<<" ";
clock2.showTime();//clock2调用成员函数showTime()
return 0;
}
输出结果:
那如果是定义一个含有成员对象的类的构造函数并调用呢?
我们举一个常用的学生和日期的例子,先定义一个日期类myDate,然后再定义一个学生类 Student,Student类中存放学生的姓名和出生日期,其中出生日期类的对象作为Student类的成员变量:
# include <iostream>
using namespace std;
class myDate //创建myDate类
{
public:
myDate(int y,int m,int d);
void show();
private:
int year;
int month;
int day;
};
myDate::myDate(int y,int m,int d):year(y),month(m),day(d)
{
cout<<"myDate类构造函数"<<endl;
}
void myDate::show()
{
cout<<"出生日期:"<<year<<"/"<<month<<"/"<<day<<endl;
}
class Student //创建Student类
{
public:
Student(string n, int y, int m, int d);
void show();
private:
string name;
myDate date;
};
//类外实现构造函数
Student::Student(string n, int y, int m, int d)
:date(y,m,d)
{
cout<<"Student类构造函数"<<endl;
name=n;
}
//类外实现show()函数
void Student::show()
{
cout<<"姓名:"<<name<<endl;
date.show();
}
int main()
{
Student stu("lili",2003,1,1); //创建学生对象stu
stu.show(); //显示学生信息
return 0;
}
输出结果:
二:复制构造函数
复制构造函数也是构造函数的一种,它的作用是使用一个已存在的对象去初始化另一个正在创建的对象。
复制构造函数的格式:
类名::类名(const 类名 &)
为了不改变原有的对象,通常用const 来限定。
或者
类名::类名(类名 &)
声明和实现复制构造函数的一般格式:
class 类名
{
public:
类名(形参表); //构造函数
类名(类名 & 对象名); //复制构造函数
…
};
类名::类名(类名 & 对象名) // 实现复制构造函数
{
函数体
}
需要注意的是,如果类中没有自行定义复制构造函数,那么编译器就会自动生成一个复制构造函数,通常是使得目标对象的每个成员变量都和源对象相等。自动生成的复制构造函数也叫做默认复制构造函数。默认构造函数不一定存在,但是复制构造函数一定存在。
1:调用默认的复制构造函数:
# include <iostream>
using namespace std;
class Complex
{
public:
double real,imag;
Complex(double r,double i)
{
real=r;
imag=i;
}
};
int main()
{
Complex c1(10,24);//声明Complex的对象c1并赋值
Complex c2(c1);//c1作为c2的参数,并调用默认复制构造函数对c2进行初始化
cout<<"c2:("<<c2.real<<","<<c2.imag<<")";
}
输出结果:
从输出结果可以看到没有自行定义一个复制构造函数时,编辑器就会调用默认的复制构造函数对c2初始化,此时c2就成为c1的复制品。
当定义了复制构造函数时,编辑器就不会再生成默认的复制构造函数了
2:调用自定义的复制构造函数:
# include <iostream>
using namespace std;
class Complex
{
public:
double real, imag;
Complex(double r,double i){
real = r; imag = i;
}
Complex(const Complex & c);//复制构造函数的声明
};
Complex::Complex(const Complex & c) //复制构造函数的实现
{
real = c.real+1;
imag = c.imag-1;
cout<<"自定义复制构造函数"<<endl ;
}
int main() {
Complex c1(3, 4);//调用默认的复制构造函数
cout<<"c1: ("<<c1.real<<","<<c1.imag<<")"<<endl;
Complex c2(c1);//调用复制构造函数初始化c2
cout<<"c2: ("<<c2.real<<","<<c2.imag<<")"<<endl;
return 0;
}
或者是在类体内完成复制构造函数的声明和实现:
class Complex
{
public:
double real, imag;
Complex(double r,double i)
{
real = r;
imag = i;
}
Complex(const Complex & c)
{
real = c.real+1;
imag = c.imag-1;
cout<<"Copy Constructor called"<<endl ;
}
};
输出结果:
自动调用复制构造函数的情况有:
1:当用一个对象去初始化本类的另一个对象时,就会调用复制构造函数:
类名 对象名2(对象名1);
类名 对象名2=对象名1;
2:如果函数F的参数是类A的对象,那么调用F的时候,就会调用类A的复制构造函数;
3:如果函数的返回值是类A的对象,那么当函数返回时,会调用类A的复制构造函数。
三:析构函数
析构函数也是成员函数的一种,它的名字和类名相同,类名前面要加一个字符“~”,用于区分构造函数。析构函数没有参数也没有返回值,一个类中有且仅有一个析构函数。若程序中没有定义析构函数,编辑器会自动生成一个函数体为空的默认析构函数,
它的作用是在对象销毁之前,做一个清理善后的工作,保证空间的可利用性。
析构函数会在对象销毁时被调用
,举个简单的例子:
# include <iostream>
using namespace std;
class Deom
{
public:
~Deom()
{
cout<<"调用析构函数"<<endl;
}
};
int main()
{
{
Deom deo;
}//局部变量在超出局部作用域后对象销毁,这时调用析构函数
cout<<"调用完毕"<<endl;
}
当使用new运算符生成对象指针时,编辑器会自动调用本类的析构函数;使用delete删除这个对象时,首先为这个动态对象调用本类的析构函数,然后再释放占用的空间。
using namespace std;
class A
{
public:
A()
{
cout<<"A的构造函数" <<endl;
}
~A()
{
cout<<"A的析构函数"<<endl;
}
};
class B
{
A *a;//在B中定义一个指针
public:
B()
{
cout<<"B的构造函数"<<endl;
a=new A;//指针指向堆区空间 ,使用new生成对象指针时,自动调用本类的构造函数
}
~B()
{
cout<<"B的析构函数" <<endl;
delete a;//使用关键字new 创建的对象,用delete来撤销 ;使用delete删除这个对象时,首先调用本类的析构函数,再释放占用的内存。
}
};
int main()
{
B b;//B的对象b定义在栈区,出栈之后自动调用本类的析构函数
return 0;
}
假如将“delete a;”注释掉,那么就会出现内存泄露的情况,而析构函数可以清理类中有指针、并且指向堆区空间的成员,释放指针指向的堆区空间,防止内存泄露。
四:this指针
类的每个成员函数中都包含 this这个特殊的指针,this 实际上是成员函数的一个形参,在调用成员函数时将对象的地址作为实参传递给 this。它指向当前的对象,通过它可以访问当前对象的所有成员。
在类的成员函数中,当形参的名字和与变量的名字相同时,通常要在成员变量名前面加上“this->”,用于区分成员变量名和形参名。
#include <iostream>
using namespace std;
class Complex
{
public:
double real,imag;
Complex():real(0),imag(0){}
Complex(double,double);
Complex AddReal();
Complex AddImag();
void outPut();
};
Complex::Complex(double real,double imag)
{
this->real=real; //在Complex带参构造函数中,根据变量前是否使用 “this->”,区分是类的成员变量还是构造函数的参数
this->imag=imag;
}
void Complex::outPut()
{
cout<<"("<<real<<","<<imag<<")";
}
Complex Complex::AddReal()
{
this->real++; //“this”代表的是函数所作用的对象,这里在进入AddReal() 函数后,“*this ”就是c1,它的类型就是类Complex 。并且修改的是c1的值。
return *this; //返回的是c1对象本身
}
Complex Complex::AddImag()
{
this->imag++;
return *this;
}
int main()
{
Complex c1(1,2),c2,c3;
c1.outPut();
c2.outPut();
c3.outPut();
cout<<endl<<"分界线"<<endl;
c2=c1.AddReal(); //将c1进入AddReal()函数后的返回对象赋给c2
c1.outPut();
c3=c1.AddImag();//将c1进入AddReal()函数后再进入AddImag()的返回对象 赋给c3
c2.outPut();
c3.outPut();
cout<<endl;
return 0;
}
输出结果:
需要注意的是:
- this 是 const 指针,它的值是不能被修改的;
- this 只能在成员函数内部使用,用在其他地方没有意义,也是非法的;
- 只有当对象被创建后 this 才有意义,类的静态成员函数是没有 this 指针的。
总结
山一程,水一程,走过的都是风景。愿你付出有所得,所想皆成真,在这个秋季,收获更好的自己。
初学乍道,如有不足,望前辈们指正,感谢!