类的6个默认的成员函数包括:
构造函数、析构函数、拷贝构造函数、赋值运算符重载函数、取地址操作符重载、const
修饰的取地址操作符重载。
这篇文章重点解释前四个。
(一)构造函数
构造函数,顾名思义,为对象分配空间,进行初始化。它是一种特殊的成员函数,具有
以下特点:
1.函数名与类名相同。
2.无返回值。
3.构造对象的时候系统会自动调用构造函数。
4.可以重载。
5.可以在类中定义,也可以在类外定义。
6.如果类中没有给出构造函数,编译器会自动产生一个缺省的构造函数,如果类中有构
造函数,编译器就不会产生缺省构造函数。
7.全缺省的构造函数和无参的构造函数只能有一个,否则调用的时候就会产生冲突。
8.没有this指针。因为构造函数才是创建对象的,没有创建对象就不会有对象的首地址。
构造函数,说来就是给成员变量进行初始化。而初始化却有两种方法:
初始化列表、构造函数函数体内赋值。
举例:依然使用日期类来说明:
#define _CRT_SECURE_NO_WARNINGS 1 #include<iostream> using namespace std; class Date { public: Date()//无参构造函数 { m_year = 2016; m_month = 7; m_day = 6; } Date(int year = 1900, int month = 1, int day = 1)//全缺省的构造函数 { m_year = year; m_month = month; m_day = day; } Date(int year, int month, int day) :m_year(year), m_month(month), m_day(day) //初始化列表初始化成员变量 { } void print() { cout << m_year << "-" << m_month << "-" << m_day << endl; } private: int m_year; int m_month; int m_day; }; int main() { Date date(2016,7,4); date.print(); system("pause"); return 0; }
上边这段代码只是为了解释初始化列表初始化成员变量和在构造函数体内初始化,也解
释了无参构造函数和全缺省的构造函数。声明:由于上边的代码同时给出无参和全缺省
的构造函数,产生调用冲突,编译不通过。
既然有两种初始化的方法,我们究竟该怎样选择呢??
尽量使用初始化列表,因为它更高效。下边用代码说明它是怎么个高效法。
<pre name="code" class="cpp">class Time { public: Time(int hour= 1, int minute=1, int second=1) { m_hour = hour; m_minute = minute; m_second = second; cout << "构造时间类对象中..." << endl; } private: int m_hour; int m_minute; int m_second; }; class Date { public: Date(int year, int month, int day,Time t) /*:m_year(year), m_month(month), m_day(day) */ { m_year = year; m_month = month; m_day = day; m_t = t; } void print() { cout << m_year << "-" << m_month << "-" << m_day << endl; } private: int m_year; int m_month; int m_day; Time m_t; }; int main() { Time t(10,36,20); Date d(2016,7,6,t); system("pause"); return 0; }
上边给出不使用初始化列表初始化日期类中的时间类对象的 办法,会导致时间类构造两
次,一次在主函数中定义时间类对象时,一次在参数列表中调用。而如果我们将所有
的成员变量都用初始化列表初始化,时间类构造函数只会被调用一次,这就是提高效率
所在。
有些成员变量必须再初始化列表中初始化,比如:
1. 常量成员变量。(常量创建时必须初始化,因为对于一个常量,我们给它赋值,是不
对的)
2. 引用类型成员变量。(引用创建时必须初始化)
3. 没有缺省构造函数的类成员变量。(如果构造函数的参数列表中有一个类的对象,并
且该对象的类里没有缺省参数的构造函数时,要是不使用初始化列表,参数中会调用无
参或者全缺省的构造函数,而那个类中又没有。)
注意:在上边的main函数中要是有这样一句:Date d2();这不是定义一个对象,而是声
明了一个函数名为d2,无参,返回值为Date的函数。
(二)析构函数
析构函数是一种特殊的成员函数,具有以下特点:
1. 析构函数函数名是在类名加上字符~。
2. 无参数无返回值(但有this指针)。
3. 一个类有且只有一个析构函数,所以肯定不能重载。若未显示定义,系统会自动生成
缺省的析构函数。
4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。
5. 注意析构函数体内并不是删除对象,而是做一些清理工作。(比如我们在构造函数
中动态开辟过一段空间,函数结束后需要释放,而系统自动生成的析构函数才不管内
存释放呢,所以需要人为地写出析构函数)
注意:对象生命周期结束后,后构造的对象先释放。
(三)拷贝构造函数:用已有的对象创建一个新的对象。仍然使用上边的日期类举例:
int main() { Date d1(2016,7,6); Date d2(d1); system("pause"); return 0; }
上边是用d1创建一个d2,系统会给出默认的拷贝构造函数,并且该函数的参数是一个常
引用,我们想象为什么必须是引用呢,如果不是又会发生什么。
如果不是引用,形参是实参的一份临时拷贝,由于两者都是对象,此时就会调用自己的
拷贝构造函数,陷入无限递归中.......
上边的代码,我们用默认的拷贝构造函数可以得到正确的结果,有时就不会。实例:
class Person { public: Person(char *name,int age) { m_name = (char *)malloc(sizeof(char)*10); if (NULL == m_name) { cout << "out of memory" << endl; exit(1); } strcpy(m_name,name); m_age = age; } ~Person() { free(m_name); m_name = 0; } private: char *m_name; int m_age; }; int main() { Person p1("yang",20); Person p2= p1; system("pause"); return 0; }
上边的代码会出错,原因见图片。
在析构时,同一块空间释放两次就会有问题。
这种仅仅只是值的拷贝的拷贝方式就是浅拷贝。
深拷贝就是为对象重新分配空间之后,然后将值拷贝的拷贝方式。
下边自己给出拷贝构造函数。
Person(const Person &p) { m_name = new char[strlen(p.m_name)+1]; if (m_name != 0) { strcpy(m_name,p.m_name); m_age = p.m_age; } }
下边用图给出实现机理。
调用拷贝构造函数的3种情况:
1.当用类的一个对象去初始化该类的另一个对象时。
2.当函数的形参是类的对象,调用函数时进行形参和实参的结合时。
3.当函数的返回值是对象,函数执行完返回调用者时。(函数运行结束后,返回的对象
会复制到一个无名对象中,然后返回的对象会消失,当调用语句执行完之后,无名对
象就消失了)
调用拷贝构造函数的两种方法:
1.代入法:
Person p2(p1);
2.赋值法:
Person p2 = p1;
(四)赋值运算符重载函数
它是两个已有对象一个给另一个赋值的过程。它不同于拷贝构造函数,拷贝构造函数是
用已有对象给新生成的对象赋初值的过程。
默认的赋值运算符重载函数实现的数据成员的逐一赋值的方法是一种浅层拷贝。
浅层拷贝会导致的指针悬挂的问题:
class Person { public: Person(char *name,int age) { m_name = (char *)malloc(sizeof(char)*10); if (NULL == m_name) { cout << "out of memory" << endl; exit(1); } strcpy(m_name,name); m_age = age; } ~Person() { free(m_name); m_name = 0; } private: char *m_name; int m_age; }; int main() { Person p1("yang",20); Person p2("yao",20); p2 = p1; system("pause"); return 0; }
看图:
使用深层拷贝来解决指针悬挂的问题:
<pre name="code" class="cpp">Person & operator=(const Person &p) { if (this == &p) return *this; delete[]m_name; m_name = new char[strlen(p.m_name) + 1]; strcpy(m_name,p.m_name); m_age= p.m_age; return *this; }
这样先将p2对象里指针指向的旧区域,然后再分配新的空间,再拷贝内容。当然,对于
那些成员变量里没有指针变量就不会涉及到指针悬挂问题。
关于剩下的两个默认成员函数,之后再补充。