
目录
上期我们介绍了一些关于类的基础知识,学会了如何定义一个类,体会到了面向对象中封装的特征。本期我们将继续类和对象的学习,重点讨论C++类中的成员函数,并在下期我们将自己动手实现一个类----日期类。
话不多说,上菜咯!!!
如果一个类中什么成员都没有,我们将其称之为空类![]()
- //空类
- class Date
- {
-
- };
但是空类中真的什么都没有吗?实则不然。任何类在什么都不写时,编译器会自动生成6个默认成员函数。默认成员函数:用户没有显式实现,编译器会自动生成的成员函数。如下所示:

接下来的内容,我们就对这6个默认成员函数进行逐一分析![]()
![]()
![]()
我们来看看下面的日期类:
- class Date
- {
- public:
- void Init(int year, int month, int day)
- {
- _year = year;
- _month = month;
- _day = day;
- }
- void Print()
- {
- cout << _year << "-" << _month << "-" << _day << endl;
- }
- private:
- int _year;
- int _month;
- int _day;
- };
-
- int main()
- {
- Date d1;
- d1.Init(2022, 7, 5);
- d1.Print();
- Date d2;
- d2.Init(2022, 7, 6);
- d2.Print();
- return 0;
- }
对于上面Date类,我们发现我们每次创建一个对象后,都要通过Init 方法给对象设置日期,这未免显得过于麻烦,那能否在对象创建时,就同步将信息设置进去呢?
使用构造函数就能很好的进行解决。构造函数是一个特殊的成员函数,函数名与类名相同,创建类对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次。其形式如下:
- class Date
- {
- public:
- //Date的构造函数
- Date()
- {
- //进行初始化
- //...
- }
- };
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名为构造,但是其主要任务并不是创建对象开辟空间,而是初始化对象。
构造函数有如下特征:
- class Date
- {
- public:
- //Date的构造函数
- Date()
- {
-
- }
- void Date(){} //错误写法,没有返回值
- };
- class Date
- {
- public:
- // 1.无参构造函数
- Date()
- {}
- // 2.带参构造函数
- Date(int year, int month, int day)
- {
- _year = year;
- _month = month;
- _day = day;
- }
- private:
- int _year;
- int _month;
- int _day;
- };
-
- int main()
- {
- //1.创建对象
- //2.调用相应的构造函数
- Date d1; //调用无参构造函数
- Date d2(2023, 8, 22); //调用带参构造函数
- }
需要注意的是,调用无参的构造函数时,对象后面无需带(),否则会变成函数声明:
- Date d1; //调用无参构造函数
-
- Date d3(); //声明一个没有形参的函数d3,它的返回值类型为Date
构造函数是默认成员函数。如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成![]()
- class Date
- {
- public:
-
- private:
- int _year;
- int _month;
- int _day;
- };
-
- int main()
- {
- Date d1; //调用编译器自动生成的默认构造函数,默认构造是无参的,相匹配
-
- Date d2(2023, 8, 22); //该行代码会报错,没有匹配的带参构造函数
- }
而如果我们显式地定义了构造函数,编译器就不会自动生成无参的默认构造函数,如下:
- class Date
- {
- public:
- //显式定义带参的构造函数
- Date(int year, int month, int day)
- {
- _year = year;
- _month = month;
- _day = day;
- }
- private:
- int _year;
- int _month;
- int _day;
- };
-
- int main()
- {
- Date d1; //该行代码会报错,没有匹配的默认构造函数
-
- Date d2(2023, 8, 22); //调用带参的构造函数
- }

编译器自动生成的默认构造函数对内置类型不会进行初始化,如:int,char,double等等;而对于自定义类型,会去调用该自定义类型的默认构造函数。
- class Time
- {
- public:
- Time() //Time类的默认构造函数
- {
- _hours = 0;
- _minute = 0;
- _second = 0;
- }
- private:
- int _hours;
- int _minute;
- int _second;
- };
- class Date
- {
- public:
-
- private:
- //内置类型
- int _year;
- int _month;
- //自定义类型
- Time _day;
- };
-
- int main()
- {
- Date d; //调用编译器自动生成的默认构造函数
- return 0;
- }

我们发现Date的默认构造函数对_year和_month没有进行初始化,依然是随机值,而对_day则去调用了Time类的默认构造函数,将其成员变量初始化为0。我们可以通过调试进一步进行验证:
默认构造函数调试
值得一提的是:在C++11中,针对默认构造函数对内置类型不进行初始化的缺陷进行了改进,支持内置类型的成员变量在类中声明时给默认值。如下:
- class Date
- {
- public:
- void Print()
- {
- cout << _year << '-' << _month << '-' << _day << endl;
- }
- private:
- int _year = 0; //声明时给默认值
- int _month = 0;
- int _day = 0;
- };
-
- int main()
- {
- Date d;
- d.Print();
- return 0;
- }

构造函数也支持给缺省值。无参的构造函数和全缺省的构造函数都称作默认构造函数。而默认构造函数只能有一个,故二者不能同时存在。举例如下![]()
- class Date
- {
- public:
- Date() //无参的构造函数
- {
- _year = 2023;
- _month = 8;
- _day = 22;
- }
- Date(int year = 2023, int month = 8, int day = 22) //全缺省的构造函数
- {
- _year = year;
- _month = month;
- _day = day;
- }
- private:
- int _year;
- int _month;
- int _day;
- };
-
- int main()
- {
- Date d1(2024); //编译通过,调用全缺省的构造函数
- Date d2; //这里编译会报错,d2调用默认构造函数,但存在两个默认构造函数,编译器不知道调用哪个
- return 0;
- }

小贴士:一般我们显式定义构造函数时,习惯将构造函数写成全缺省的,以提高代码的健壮性。
构造函数是在对象创建时对其进行初始化,有初始化便有销毁,析构函数的作用就是在对象生命周期结束时,完成对象中资源的清理和释放。和构造函数一样,析构函数由编译器自动调用。下面是Stack类的构造函数和析构函数的实现![]()
- class Stack
- {
- public:
- Stack(size_t capacity = 4) //构造函数,初始化一个栈,写成全缺省的形式
- {
- _array = (int*)malloc(sizeof(int) * capacity);
- if (nullptr == _array)
- {
- perror("malloc申请空间失败!!!");
- return;
- }
- _capacity = capacity;
- _top = 0;
- }
- ~Stack() //析构函数,在类名前加~号
- {
- free(_array); //堆上动态申请的空间需要由用户自行释放
- //下面的代码也可以不写,栈上的空间操作系统会自动释放
- _array = nullptr;
- _capacity = _top = 0;
- }
- private:
- int* _array;
- int _capacity;
- int _top;
- };
析构函数也是特殊的成员函数,其特征如下:
- class Stack
- {
- public:
- Stack(){
- cout << "Stack()" << endl;
- }
- ~Stack(){
- cout << "~Stack()" << endl;
- }
- private:
- int* _array;
- int _capacity;
- int _top;
- };
-
- int main()
- {
- Stack s;
- return 0;
- }
当s对象创建时编译器自动调用构造函数,当s对象生命周期结束时编译器自动调用析构函数,效果如下:
和构造函数类似,编译器默认生成的析构函数不会对内置类型成员进行清理,最终由操作系统自动进行回收即可;而对于自定义类型成员,默认析构函数会去调用它的析构函数,保证其内部每个自定义类型成员都可以正确销毁。
回到我们之前的日期类:- class Time
- {
- public:
- Time() //Time类的默认构造函数
- {
- cout << "Time()" << endl;
- }
- ~Time() //Time类的析构函数
- {
- cout << "~Time()" << endl;
- }
- private:
- int _hours;
- int _minute;
- int _second;
- };
- class Date
- {
- public:
- //没有显式写出构造函数和析构函数,使用编译器自动生成的
-
- private:
- int _year;
- int _month;
- Time _day;
- };
-
- int main()
- {
- Date d; //调用编译器自动生成的默认构造函数
- return 0;
- }

尽管我们没有直接创建Time类的对象,但依然调用了Time类的构造函数和析构函数。这是因为Date类中的_day成员是Time类的对象,在Date类的默认构造函数和默认析构函数中,会去调用Time类这个自定义类型的构造函数和析构函数,对_day成员进行初始化和清理工作。
如果类中没有动态申请内存时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类;有资源申请时,一定要写,否则会造成内存泄漏,比如Stack类
类的析构函数一般按照构造函数调用的相反顺序进行调用,但是要注意static对象的存在, 因为static改变了对象的生存作用域,需要等待程序结束时才会析构释放static对象。
Q:假设已经有A,B,C,D 4个类的定义,则程序中A,B,C,D析构函数调用顺序为?
- C c;
- int main()
- {
- A a;
-
- B b;
-
- static D d;
-
- return 0;
-
- }
答案是BADC。解析如下:
1、全局变量优先于局部变量进行构造,因此构造的顺序为cabd
2、析构的顺序和构造的顺序相反
3、static和全局对象需在程序结束才进行析构,故会放在局部对象之后进行析构
综上:析构的顺序即为BADC。
在现实生活中,可能存在一个与你长相,我们称其为双胞胎

那我们在创建类对象时,能不能创建一个和已有对象一模一样的对象呢?Ctrl+C和Ctrl+V想必没有人不喜欢吧嘿嘿
这就要谈到我们的拷贝构造函数惹。
拷贝构造函数:只有单个形参,该形参是对本类型对象的引用(一般常用const修饰),在用已存在的类对象创建新对象时编译器会自动调用拷贝构造函数。
- class Date
- {
- public:
- Date() {};
- Date(const Date& d) //Date的拷贝构造函数
- {
- _day = d._day;
- _month = d._month;
- _day = d._day;
- }
- private:
- int _year;
- int _month;
- int _day;
- };
-
- int main()
- {
- Date d1;
- Date d2(d1); //用d1拷贝构造d2
- return 0;
- }
拷贝构造函数也是属于特殊的成员函数,其特征如下:
- //拷贝构造函数的写法
- Date(const Date d) // 错误写法:编译报错,会引发无穷递归
- {
- _year = d._year;
- _month = d._month;
- _day = d._day;
- }
-
- Date(const Date& d) // 正确写法
- {
- _year = d._year;
- _month = d._month;
- _day = d._day;
- }


- class Time
- {
- public:
- Time()
- {
- _hour = 1;
- _minute = 1;
- _second = 1;
- }
- Time(const Time& t)
- {
- _hour = t._hour;
- _minute = t._minute;
- _second = t._second;
- cout << "Time::Time(const Time&)" << endl;
- }
- private:
- int _hour;
- int _minute;
- int _second;
- };
- class Date
- {
- private:
- // 内置类型
- int _year = 2023;
- int _month = 1;
- int _day = 1;
- // 自定义类型
- Time _t;
- };
- int main()
- {
- Date d1; //d1调用默认的构造函数进行初始化
-
- // 用已经存在的d1拷贝构造d2,此时会调用Date类的拷贝构造函数
- // 但Date类并没有显式定义拷贝构造函数,因此编译器会给Date类生成一个默认的拷贝构造函数
- Date d2(d1);
- return 0;
- }
我们可以通过监视窗口来查看d2对象的拷贝情况
可以看出,编译器默认生成的拷贝构造函数不仅会对自定义类型成员进行拷贝(通过调用相应的拷贝构造函数),也会对内置类型成员进行拷贝(按字节序的浅拷贝)。
默认拷贝构造函数调试
- class Stack
- {
- public:
- Stack(size_t capacity = 4)
- {
- _array = (int*)malloc(capacity * sizeof(int));
- if (nullptr == _array)
- {
- perror("malloc申请空间失败");
- return;
- }
- _size = 0;
- _capacity = capacity;
- }
- ~Stack()
- {
- if (_array)
- {
- free(_array);
- _array = nullptr;
- _capacity = 0;
- _size = 0;
- }
- }
- private:
- int* _array;
- size_t _size;
- size_t _capacity;
- };
- int main()
- {
- Stack s1;
- Stack s2(s1);
- return 0;
- }
当我们兴冲冲地运行代码时,诶,程序居然崩溃了![]()

为什么呢?这就涉及到了浅拷贝按字节序拷贝的缺陷,如下图所示![]()

那这种浅拷贝的问题要如何解决呢?
一般有两种解决方案:深拷贝或者引用计数。
所谓深拷贝,就是手动再申请一段空间,然后将原空间的内容依次拷贝到新空间中,最后让s2的_array指针指向这个新空间。这种方法避免了一块空间被多个对象指向的问题。
而引用计数,就是在类中额外增加一个变量count记录堆空间被引用的次数,只有当引用次数变为1时,我们才对这段空间进行释放。这种方法避免了一块空间被多次释放的问题。
现在我们对这两种方式有个初步的印象即可,后续我们会详细讲解。不过无论是深拷贝还是引用计数,都是编译器默认生成的拷贝构造函数无法做到的,需要我们显式地实现拷贝构造函数。
拷贝构造函数有三个典型的调用场景:使用已存在的对象创建新对象、函数形参为类对象、函数返回值为类对象。
- class Date
- {
- public:
- Date(int year, int minute, int day)
- {
- cout << "Date(int,int,int):" << this << endl;
- }
- Date(const Date& d)
- {
- cout << "Date(const Date& d):" << this << endl;
- }
- ~Date()
- {
- cout << "~Date():" << this << endl;
- }
- private:
- int _year;
- int _month;
- int _day;
- };
- Date Test(Date d)
- {
- Date temp(d);
- return temp;
- }
- int main()
- {
- Date d1(2022, 1, 13);
- Test(d1);
- return 0;
- }

结论:为了提高效率,减少拷贝构造的次数,一般对象传参时,我们尽量使用引用传参,函数返回时也是根据实际场景,能用引用返回尽量使用引用返回。
对于内置类型,我们可以使用==、>号运算符判断它们的大小关系,可以使用+,-号运算符对其进行加减......如下所示![]()
- int main()
- {
- int a = 10;
- int b = 20;
- a = a + 10;
- b = b - 10;
- cout << (a == b);
- cout << (a > b);
- //还可以使用许许多多的运算符进行操作,这里就不一一挪列了
- //...
- return 0;
- }
但对于自定义类型来说,也就是我们的类,这些运算符仿佛都失效了
- class Date
- {
- public:
- Date(int year = 2000, int month = 1, int day = 1)
- {
- _year = year;
- _month = month;
- _day = day;
- }
- private:
- int _year;
- int _month;
- int _day;
- };
- int main()
- {
- Date d1(2023, 8, 24);
- Date d2(2023, 8, 25);
- d1 = d1 + 10; //d1类对象使用+号运算符
- d1 == d2; //d2类对象使用==号运算符
- }

很明显编译器报错了,这是因为对于几个固定的内置类型,编译器知道它们的运算规则,而对于我们自定义的类型,编译器并不知道它的运算规则,例如d1+10究竟是年份+10还是月份+10呢?编译器无法进行确定,故报错。
有一种很简单的解决方法就是给类定义成员函数,通过调用成员函数来实现我们想要的运算,如下所示:
- class Date
- {
- public:
- Date(int year = 2000, int month = 1, int day = 1)
- {
- _year = year;
- _month = month;
- _day = day;
- }
- void AddYear(int val)
- {
- _year += val;
- }
- bool isSame(const Date& d)
- {
- return _year == d._year && _month == d._month && _day == d._day;
- }
- private:
- int _year;
- int _month;
- int _day;
- };
- int main()
- {
- Date d1(2022, 8, 24);
- Date d2(2023, 8, 24);
- d1.AddYear(1); //年份+1
- cout << d1.isSame(d2); //比较d1和d2是否相等
- }

上面的方式的确可以解决问题,但还是不够直观,每次进行运算都需要调用函数,代码未免有点挫
有没有什么方法可以让类使用运算符进行运算吗?
这就不得不谈到我们的主角----运算符重载

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型和参数列表与普通的函数类似。
运算符重载的函数名为:关键字operator后面+需要重载的运算符符号
其函数原型为:返回值类型 operator操作符(参数列表)
- //==号运算符重载
- bool operator==(const Date& d)
- {
- //函数内容
- return _year == d._year && _month == d._month && _day == d._day;
- }
通过运算符重载,我们可以对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型,且不改变原有的功能。
进行运算符重载需要注意以下几点:


下面我们来看看日期类==号运算符的重载示例![]()
1、作为全局函数重载
- class Date
- {
- public:
- Date(int year = 2000, int month = 1, int day = 1)
- {
- _year = year;
- _month = month;
- _day = day;
- }
-
- //private: //为了让==运算符重载函数能够访问,将成员变量设置为共有的
- int _year;
- int _month;
- int _day;
- };
-
- //作为全局函数重载
- bool operator==(const Date& d1,const Date& d2)
- {
- //这里需要类外访问成员变量
- return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day;
- }
- int main()
- {
- Date d1(2022, 8, 24);
- Date d2(2023, 8, 24);
- cout << (d1 == d2);
- return 0;
- }
当运算符重载作为全局函数时,由于我们难免需要对成员变量进行访问,我们需要类的成员函数是共有的,可这难免会破坏了类的封装性。
当然,我们还可以使用友元函数来解决,关于友元在下篇会介绍到。不过最推荐的还是将其重载为成员函数。
2、作为成员函数重载
- class Date
- {
- public:
- Date(int year = 2000, int month = 1, int day = 1)
- {
- _year = year;
- _month = month;
- _day = day;
- }
- // bool operator==(Date* const this, const Date& d2)
- // this指向调用的对象
- bool operator==(const Date& d) //重载为成员函数
- {
- //类内访问成员变量不受访问限定符限制
- return _year == d._year && _month == d._month && _day == d._day;
- }
- private:
- int _year;
- int _month;
- int _day;
- };
-
- int main()
- {
- Date d1(2022, 8, 24);
- Date d2(2023, 8, 24);
- //下面两种写法是等价的
- cout << (d1 == d2);
- cout << d1.operator==(d2);
- return 0;
- }
在使用类的过程中,当我们想将一个类赋值给另一个类时,我们便可以对=赋值运算符进行重载。其重载格式如下:
- class Date
- {
- public:
- Date(int year = 2023, int month = 1, int day = 1)
- {
- _year = year;
- _month = month;
- _day = day;
- }
- Date& operator=(const Date& d) //赋值运算符重载
- {
- if (this != &d) //避免自己给自己赋值
- {
- _year = d._year;
- _month = d._month;
- _day = d._day;
- }
- return *this; //返回自身的引用,连续赋值
- }
- private:
- int _year;
- int _month;
- int _day;
- };
-
- int main()
- {
- Date d1;
- Date d2, d3;
- d3 = d2 = d1; //调用赋值运算符重载
- }
下面是赋值运算符重载的几点注意事项![]()
- // 赋值运算符重载成全局函数
- Date& operator=(Date& left, const Date& right)
- {
- if (&left != &right)
- {
- left._year = right._year;
- left._month = right._month;
- left._day = right._day;
- }
- return left;
- }
下面来个小问题试试对拷贝构造的理解:
- int main()
- {
- Date d1, d2;
- Date d3 = d1; //这里调用的是拷贝构造还是赋值重载呢?
- d2 = d1; //这里呢?
- return 0;
- }
答:第一问调用的是拷贝构造函数,第二问调用的是赋值重载。
解析:拷贝构造函数是用已存在的对象去构造新对象,而d3就是我们需要构造的新对象,第一问就是用d1对象去构造d3对象,故调用拷贝构造,这种写法与Date d3(d1)等价。而赋值运算符载函数是两个已存在对象之间进行赋值,d1和d2都是已经存在的对象,故d2=d1调用的是赋值重载。
前置++
下面我们来尝试对Date类实现前置++运算符的重载,用于对天数进行自增。所谓前置++,就是先自增1再返回结果,如下:
- class Date
- {
- public:
- Date(int year = 2023, int month = 1, int day = 1)
- {
- _year = year;
- _month = month;
- _day = day;
- }
- Date& operator++() //前置++运算符重载
- {
- _day += 1; //先自增 (注意:这里为了演示先忽略日期进位,进位处理请看Date类的模拟实现)
- return *this; //返回自身,为了提高效率,用引用返回
- }
- private:
- int _year;
- int _month;
- int _day;
- };
后置++
而后置++,就是先返回结果再进行自增。但有个问题:前置++和后置++都是一元运算符,即只有一个操作数:对象本身。在作为成员函数重载时,重载函数就是无参的,那编译器要如何区分是前置++还是后置++呢?
- int main()
- {
- Date d;
- //编译器要如何区分哪个operator++()函数是前置++,哪个又是后置++ ???
- ++d; //相当于d.operator++()
- d++; //也相当于d.operator++()
- return 0;
- }
为此,C++做了特殊规定:后置++重载时多增加一个int类型的参数用于占位,但调用函数时该参数不用传递,编译器会自动传递。
- class Date
- {
- public:
- Date(int year = 2023, int month = 1, int day = 1)
- {
- _year = year;
- _month = month;
- _day = day;
- }
- Date operator++(int) //后置++运算符重载,int用于占位区分
- {
- Date temp(*this); //由于要返回+1前的结果,所以先对对象进行拷贝
- _day += 1; //然后天数+1
- return temp; //然后将+1前的对象返回。由于temp出了函数就销毁了,故不能用引用返回
- }
- private:
- int _year;
- int _month;
- int _day;
- };
- int main()
- {
- Date d;
- ++d; //相当于d.operator++()
- d++; //相当于d.operator++(int),int类型参数由编译器自动传递
- return 0;
- }
我们来看下面的代码:
- class Date
- {
- public:
- void Print()
- {
- cout << "void Print()" << endl;
- }
- private:
- int _year = 2023;
- int _month = 1;
- int _day = 1;
- };
-
- int main()
- {
- const Date d;
- d.Print();
- return 0;
- }
上面的代码编译时会进行报错,报错原因如下![]()

由于对象d被const所修饰,故其类型为const Date,表示不能对类的任何成员进行修改。当d调用Print函数时,传入的实参就是d的地址,即const Date*类型的指针,而在Print函数中,用于接收的this指针却是Date*类型的,这无疑是一种权限的放大,故编译器会进行报错。
那要怎么解决这个问题呢?很简单,给this指针加上const进行修饰即可。
由于this指针是 "非静态成员函数" 的隐藏形参,我们无法显式地去定义this指针,因此C++规定,在成员函数后面加上const代表它为const成员函数,其this指针的类型为const A* this,编译器会自动进行识别处理![]()

回到上面的代码,当我们在Print函数后加上const后,程序就正常运行啦![]()
- class Date
- {
- public:
- void Print() const //this指针的类型是const Date*
- {
- cout << "void Print() const" << endl;
- }
- private:
- int _year = 2023;
- int _month = 1;
- int _day = 1;
- };
-
- int main()
- {
- const Date d;
- d.Print();
- return 0;
- }

- class Date
- {
- public:
- void Print() //非const成员函数
- {
- cout << "void Print()" << endl;
- }
- void Print() const //const成员函数
- {
- cout << "void Print() const" << endl;
- }
- private:
- int _year = 2023;
- int _month = 1;
- int _day = 1;
- };
-
- int main()
- {
- const Date d1;
- Date d2;
- d1.Print(); //const类型的对象调用cosnt成员函数
- d2.Print(); //非const类型的对象调用非const成员函数
- return 0;
- }

建议给只读成员函数加上const修饰,即内部不涉及修改成员变量的函数
构造函数不能加const修饰。构造函数是对成员变量进行初始化的,显然会涉及到成员变量的修改。
取地址运算符重载有两个版本,一个是const的,一个是非const的。这两个成员函数也是我们一开始讲的默认成员函数,当用户没有显式定义时,编译器会自动生成。
- class Date
- {
- public:
- Date* operator&() //非const版本,this指针类型为Date*
- {
- return this;
- }
- const Date* operator&()const //const版本,this指针类型为const Date*
- {
- return this;
- }
- private:
- int _year; // 年
- int _month; // 月
- int _day; // 日
- };
这两个版本的成员函数和上面的不同,一般使用编译器默认生成的取地址重载即可。只有特殊情况下,才需要显式定义,比如想让别人获取到指定的内容!