当建立一个对象时,通常最需要立即做的工作是初始化对象,如对数据成员赋初值等
构造函数就是用来在创造对象时初始化对象,为对象数据成员赋初始值
——初始化对象+申请资源
下面以创建Date对象举例说明:
创建Data对象的同时并给对象设置一个值
C++提供了构造函数(constructor)来处理对象的初始化问题。构造函数是类的一种特殊成员函数,不需要人为调用,而是在建立对象时自动被执行。换言之,在建立对象时构造函数就被自动执行了,而且在上述代码中:
1.名字与类名相同,创建类类型对象时由编译器自动调用
2.在对象的声明周期只调用一次
构造函数的特性其主要特征如下:
1.名字与类名相同
2.无返回值
3.对象实例化时编译器会自动调用构造函数
4.构造函数可以重载
在上述代码中,带参构造函数和无参构造函数形成重载
如果我们在类中没有写一个构造函数,那么我们调用对象时,编译器自动生成一个默认构造函数。一旦用户在类中定义了一个构造函数,则这个默认构造函数将不会生成,也就是说首先会调用用户定义的构造函数。
在Data类中没有一个构造函数,当我们创建一个对象时,编译器会自动调用默认构造函数将a进行了初始化,只是a中的成员变量为随机值,使用无参的构造函数不需要()。
类中无构造函数,调用系统默认构造函数,初始化后a的值为随机值,具体代码如下:
综上:我们建议定义出一个全缺省构造函数,因为它可以灵活应用,它既可以传参,也可以不需要传参也能够初始化对象。
用构造函数创建对象后,程序负责跟踪该对象,直到其过期为止。对象过期时,程序将自动调用一个特殊的成员函数——析构函数,用该函数去完成一些资源清理工作——释放资源。
例如构造函数使用new来分配内存,则析构函数将使用delete来释放这些内存。
那么如何去声明和定于析构函数?
析构函数是特殊的成员函数
其特征如下:
1.析构函数是在在类名加 ~
2.一个类只能有一个析构函数,如果类中没有定义一个析构函数,则系统会自动生成一个默认的析构函数
3.无参数无返回值。
4.对象声明周期结束时会编译器会自动调用析构函数。
将SeqList中的变量s进行初始化:
下面程序,我们看到编译器生成的默认析构函数,对会自定类型成员调用它的析构函数。
我们自己没有写析构函数,那么编译器会去调用系统默认的析构函数,而这个析构函数对内置类型不做处理,对自定义类型会去调用它自己的析构函数
注意:析构函数是无参数返回的函数且没有参数,一个类只有一个析构函数,不写的话则会生成默认析构函数
拷贝构造函数是一种特殊的构造函数,函数的名称必须和类名称一致,它的唯一的一个参数是本类型的一个引用变量,该参数是const类型,不可变的。例如:类X的拷贝构造函数的形式为X(X& x)。
在下述代码中,d1调用拷贝构造函数将d的内容拷贝给自己,虽然没有定义拷贝构造函数,然而系统调用默认的拷贝构造函数。
拷贝构造函数也是特殊的成员函数,特征如下:
1.拷贝构造函数是构造函数的一个重载形式
2.拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用
3.若未显示定义,系统生成默认的拷贝构造函数 (默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝)
如果拷贝构造函数的参数为为传值,那么在传值的过程会去调用拷贝构造函数去拷贝一个临时变量,每一次调用都会去在调用拷贝构造函数,以此类推,则会引起无穷无尽的调用拷贝构造函数。所以拷贝构造函数必须传引用
data(data &a)为语法要求,不会为a开辟空间,仅当a的别名使用,不会触发其他函数调动
如果不加&,对象初始化对象就会调动拷贝构造函数,就得引发下一个a,这样下去就没完没了了,不允许产生无限递归的现象。
4.如果说编译器生成默认拷贝构造函数就可以实现拷贝的话,是不是自己就不用写了?对于下面这种类,程序运行是崩溃的,那么我们就需要用深拷贝来解决。
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号
函数原型:返回值类型operator操作符
示例:
说白了运算符重载就是要给
注意:
不能通过连接其他符号来创建新的操作符:比如operator@
重载操作符必须有一个类类型或者枚举类型的操作数
用于内置类型的操作符,其含义不能改变,例如:内置类型+,不能改变其含义
*,::,sizeof,?,、以上5个运算符不能重载
运算符重载实际上就是给这个所谓的运算符赋一个其它的涵义
t4给t3赋值其实可以直接调动成员函数的方法,类似于t4.Assign(t3),t4.Assign(t3)与t4.operator=(t3)的结果是一致的。但是在C++中更想要简单一点(不希望调函数,比如t1+t2,我们不希望写成add(t1,t2),而是直接t1+t2),其实在这里,t4=t3 中的=已经不是一个“=”了,而是 operator= 但是这里的operator可以省略,所以说 这里的 = 就不是等号了,而是一个“函数”。
问题1:
上述代码中——Test * operator = (const Test &t) 这个const可不可以不要?,还有&
当然可以取消,取消不影响代码结果,但是为了程序的运行效率,取消&的话,会多调用一次拷贝构造函数,注意析构函数是从后往前析构。也就是说加了&会提升程序的运行效率(少一次调用拷贝构造,当然也少一次析构,不开辟空间,不花费时间)具体示例如下:
因为引用的对象我们一般是不希望被改变的,因此加上const,if(this !=&t)是防止自己给自己赋值
对于上述代码,return *this返回后 this指针的值是存在的,所以上述Test后面可以加&进行引用,所以说能不能引用返回,就看函数结束后,被引用的对象还活着没有而且建议用引用,因为可以少调动一次拷贝构造函数。
综上:赋值运算符主要有四点:
1.参数类型
2.返回值
3.检测是否给自己赋值
4.返回*this
5.一个类如果没显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝
对于如下这么多对象,他们构造,拷贝构造,析构的顺序及对应的流程如下:
下面这种办法少产生一次拷贝构造的调用,就是说拷贝构造产生的无名临时变量,直接就是t4的值。而不是像上述代码一样多拷贝了一次,然后再将拷贝后的值给t4,这样增加了程序的运行效率
下面为运算符重载的相关代码;
// An highlighted block
class MyInt;
ostream& operator<<(ostream &out, const MyInt &t);
class MyInt
{
friend ostream& operator<<(ostream &out, const MyInt &t);//声明友元函数
public:
MyInt(int i = 0)
{
m_i = i;
}
public:
MyInt operator+(const MyInt &x)
{
return MyInt(m_i + x.m_i);
}
MyInt operator-(const MyInt &x)
{
return MyInt(m_i - x.m_i);
}
MyInt operator*(const MyInt &x)
{
return MyInt(m_i * x.m_i);
}
MyInt operator/(const MyInt &x)
{
return MyInt(m_i / x.m_i);
}
MyInt operator%(const MyInt &x)
{
return MyInt(m_i % x.m_i);
}
public:
MyInt& operator+=(const MyInt &x)
{
m_i += x.m_i;
return *this;
}
MyInt& operator-=(const MyInt &x);
MyInt& operator*=(const MyInt &x);
MyInt& operator/=(const MyInt &x);
MyInt& operator%=(const MyInt &x);
public:
//a > b x.operator>(b)
bool operator>(const MyInt &x)
{
return m_i > x.m_i;
}
bool operator<=(const MyInt &x)
{
return !(*this > x);//当前对象比x大然后取反就是小于等于就是真
}
bool operator<(const MyInt &x)
{
return m_i < x.m_i;
}
bool operator>=(const MyInt &x)
{
return !(*this < x);
}
bool operator==(const MyInt &x)
{
return m_i == x.m_i;
}
bool operator!=(const MyInt &x)
{
return m_i != x.m_i;
}
public:
MyInt& operator++() //ǰ++
{
m_i++;
return *this;
}
MyInt operator++(int) //++
{
MyInt tmp = *this;
m_i++;
return tmp;
}
private:
int m_i;
};
ostream& operator<<(ostream &out, const MyInt &t)//友元函数实现
{
out << t.m_i;
return out;
}
void main()
{
MyInt a = 10;
MyInt b = 20;
MyInt c;
c = a + b;
c = a - b;
c = a * b;
c = a / b;
c = a % b;
cout << "c = " << c << endl;
a += b;
if (a > b)
cout << "a > b" << endl;
else
cout << "a <= b" << endl;
MyInt x = a++;
MyInt y = a--;
}
定义复数类的运算符重载实现:
对于Complex y = c1+10来说可以,但是Complex y = 10+c1就不可以。c1是个对象,+是函数,函数要被对象才能驱动,c1+10 实际上是c1.operator。10不能构造成临时对象,10+的话就不匹配重载的+号了,而是想象成普通的运算符了。
当然如果非要Complex y = 10+c1成立的话,这里需要用到友元函数来操作,如下:
重载为友元函数,且友元函数必须给出成员的参数,成员函数和友元不一样,不需要加类的作用域限定符::
下面运算符输出流的重载,它通常作为某个类的友元函数出现
下面为C++输出的时候,cout不管任何情况就可以直接输出各种类型的结果,会自动匹配类型。
cout其实是ostream的一个对象(endl同样也是一个对象的概念)而且是一个全局对象,所以引入头文件后都可以使用。
ostream其实是这个东西的一个命名方式,是一个基本输出流对象 basic_ostream
表面上看起来它什么都不管,实际上背后可以生成各种类型的类型,相当于调用了不同的运算符重载,这些底层的代码我们平时是看不到的。
下面举相关个别运算符重载:
ostream是输出的一种类型,cout是out的对象,为什么out可以引用cout,因为他们两个是同一种类型,那为什么要重载为友元,是因为希望第一个参数就是cout,第二个参数才是对象,如果不重载友元,那么第一个参数只能用对象去驱动它。最后把cout返回,然后继续往后输出。
将const修饰的类成员函数称为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在函数成员函数中不能对类的任何成员进行修改。
const修饰的方法我们称之为常方法 ,如下代码我们在函数后面加了const实际上就是对内部的this指针变量加了限制不可改变。
普通的对象调动常方法是没有问题的,常对象调用普通方法就不行
那么什么时候写成常方法?
要修改数据成员的话则不能写成常方法,如果只为读而不改,则强烈建议写成常方法。c++采用最佳匹配原则,常方法和普通方法可以共存。
六种类的默认成员函数:
构造函数,析构函数,拷贝构造函数,运算符重载之外的另外两种为: