目录
1.类的六个默认成员函数
如果一个类中什么成员都没有,简称为空类。
class Date
{
};
任何类在什么都不写的情况下,编译器会自动生成以下六个默认成员函数(用户没有显式实现,编译器会生成的成员函数)。

2.构造函数
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时编译器自动调用。保证每个成员变量都有一个合适的初始值,并且在对象整个生命周期只调用一次。
构造函数的主要任务不是开空间创建对象,而是初始化对象。
其特征如下:
- 函数名与类名相同
- 无返回值,无需写void
- 对象实例化时编译器自动调用对应函数
- 构造函数可以重载,但无参调用存在歧义
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;
};
void TestDate()
{
Date d1; // 调用无参构造函数
Date d2(2015, 1, 1); // 调用带参的构造函数
// 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
// 以下代码的函数:声明了d3函数,该函数无参,返回一个日期类型的对象
// warning C4930: “Date d3(void)”: 未调用原型函数
Date d3();//错误,这里是函数声明,未实例化对象!
}
5.如果类中没有显式定义构造函数,则C++编译器会默认生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成
class Date
{
public:
/*
// 如果用户显式定义了构造函数,编译器将不再生成
Date(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类中构造函数屏蔽后,代码可以通过编译,因为编译器生成了一个无参的默认构造函
数
// 将Date类中构造函数放开,代码编译失败,因为一旦显式定义任何构造函数,编译器将不再
//生成
// 无参构造函数,放开后报错:error C2512: “Date”: 没有合适的默认构造函数可用
Date d1;
return 0;
}
注:内置类型(例如int,char,float,指针类型等)不做处理
但是注意
1.很少有些编译器会处理
2.C++打了补丁,内置类型成员变量在类中声明时可以给默认值,
给了缺省值会初始化
自定义类型(例如struct,class,union等)会调用它的默认构造
所以一般情况下,有内置类型成员,就需要自己写析构函数,不能用编译器自己生成的。如果全都是自定义类型成员,可以考虑让编译器自己生成
总结:
- 一般构造函数都需自己写
- 编译器自己生成:a)内置类型成员都有缺省值且初始化符合要求 b)全为自定义类型的构造,且这些类型的定义都默认构造
class Time
{
public:
Time()
{
cout << "Time()" << endl;
_hour = 0;
_minute = 0;
_second = 0;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year = 1970;
int _month = 1;
int _day = 1;//缺省值,不是初始化,只是声明
// 自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
6.无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。
注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数(不传参就可调用的)
class Date
{
public:
Date()
{
_year = 1900;
_month = 1;
_day = 1;
}
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
// 以下测试函数编译失败,类中存在多个默认构造函数,导致调用Date d1时出现二义性。
void Test()
{
Date d1;
}
3.析构函数
析构函数:与构造函数功能相反,不是完成对对象本身的销毁,局部对象的销毁工作由编译器完成。而是对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
其特征如下:
- 函数名是在类名前加上字符~
- 无参数无返回类型
- 一个类只能有一个析构函数,若未显式定义,系统会自动生成默认的析构函数。(注意:内置类型成员不做处理,自定义类型会调用它的析构函数)
- 析构函数不能重载
- 对象生命周期结束的时候,C++编译系统自动调用析构函数。
总结:
1.一般情况下有动态申请资源,一定要写析构函数释放资源,否则造成内存泄露(如Stack类)。
2.没有动态申请的资源或者需要释放资源的成员都是自定义类型,不需要写析构函数 (如Date类)。
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 3)
{
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (NULL == _array)
{
perror("malloc申请空间失败!!!");
return;
}
_capacity = capacity;
_size = 0;
}
void Push(DataType data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
// 其他方法...
~Stack()
{
if (_array)
{
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
int _capacity;
int _size;
};
void TestStack()
{
Stack s;
s.Push(1);
s.Push(2);
}
6.关于编译器自动生成的析构函数,是否会完成一些事情呢?
下面的程序我们会看到,编译器生成的默认析构函数,对自定义类型成员调用它的析构函数
class Time
{
public:
~Time()
{
cout << "~Time()" << endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year = 1970;
int _month = 1;
int _day = 1;
// 自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
程序运行结束后输出:~Time()
Q:在main方法中根本没有直接创建Time类的对象,为什么最后会调用Time类的析构函数?
A:因为main方法中创建了Date对象d,而d中包含4个成员变量,其中_year, _month,_day三个是内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可;而_t是Time类对象,所以在d销毁时,要将其内部包含的Time类的_t对象销毁,所以要调用Time类的析构函数。但是main函数中不能直接调用Time类的析构函数,实际要释放的是Date类对象,所以编译器会调用Date类的析构函数,而Date没有显式提供,则编译器会给Date类生成一个默认的析构函数,目的是在其内部调用Time类的析构函数,即当Date对象销毁时,要保证其内部每个自定义对象都可以正确销毁总而言之就是main函数中并没有直接调用Time类析构函数,而是显式调用编译器为Date类生成的默认析构函数
注意:创建哪个类的对象则调用该类的析构函数,销毁那个类的对象则调用该类的析构函数
构造顺序是按照语句的顺序进行构造,析构是按照构造的相反顺序进行析构 看下面这道题来深度理解一下。

1、类的析构函数调用一般按照构造函数调用的相反顺序进行调用,但是要注意static对象的存在, 因为static改变了对象的生存作用域,需要等待程序结束时才会析构释放对象
2、全局对象先于局部对象进行构造
3、局部对象按照出现的顺序进行构造,无论是否为static
4、所以构造的顺序为 c a b d
5、析构的顺序按照构造的相反顺序析构,只需注意static改变对象的生存作用域之后,会放在局部 对象之后进行析构
6、因此析构顺序为B A D C
4.拷贝构造函数
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),再用已存在的类类型对象创建新对象时由编译器自动调用。C++规定,内置类型直接拷贝,自定义类型必须调用拷贝构造函数完成拷贝。
其特征如下:
- 拷贝构造函数是构造函数的一个重载形式
- 拷贝函数的参数只有一个,且必须是类类对象的引用,(使用传值方式编译器直接报错,因为会引发无穷递归调用)。
class Date { public: Date(int year = 1900, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } // Date(const Date& d) // 正确写法 Date(const Date d) // 错误写法:编译报错,会引发无穷递归 { _year = d._year; _month = d._month; _day = d._day; } private: int _year; int _month; int _day; }; int main() { Date d1; Date d2(d1); return 0; }
3. 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按
字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。内置类型成员完成值拷贝/浅拷贝,自定义类型成员会调用它的拷贝构造
4.编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显式实现吗?
像日期类这样的类是没必要的。那么下面的类呢?
// 这里会发现下面的程序会崩溃掉,这里就需要我们以后讲的深拷贝去解决。
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 10)
{
_array = (DataType*)malloc(capacity * sizeof(DataType));
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}
_size = 0;
_capacity = capacity;
}
void Push(const DataType& data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
~Stack()
{
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
private:
DataType *_array;
size_t _size;
size_t _capacity;
};
int main()
{
Stack s1;
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.Push(4);
Stack s2(s1);
return 0;
}

注意:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;
一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。
5.经典调用拷贝构造函数场景
- 使用已存在对象创建新对象
- 函数参数类型为类类型对象
- 函数返回值类型为类类型对象
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;
}
传值返回会产生一个临时对象,调用拷贝构造,传值引用返回,返回的是返回对象的别名(引用),没有产生拷贝,不会调用拷贝构造。
但是如果返回对象是⼀个当前函数局部域的局部对象,函数结束就销毁了,那么使用引用返回是有问题的,这时的引用相当于⼀个野引用,类似⼀个野指针⼀样。传引用返回可以减少拷贝,但是⼀定要确保返回对象,在当前函数结束后还在,才能用引用返回。

5.赋值运算符重载
1)运算符重载
C++为了增强代码的可读性,引入了运算符重载,运算符重载是具有特殊函数名的函数。函数名为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator 操作符(参数列表)
注:
- 不能通过连接其他符号来创建新的操作符
- 重载操作符必须有一个类类型参数
- 用于内置类型的运算符,其含义不能改变
- 作为类成员的重载时,其形参看起来比操作数数目少一,因为成员函数的第一个参数为隐藏的this。
- .* :: sizeof ?: .以上五个运算符不能重载(笔试常考)
- 操作符有几个操作数,重载函数就有几个参数
// 1.
class Date
{
public:
Date(int year = 1900, 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;
}
//2
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// bool operator==(Date* this, const Date& d2)
// 这里需要注意的是,左操作数是this,指向调用函数的对象
bool operator==(const Date& d2)
{
return _year == d2._year;
&& _month == d2._month
&& _day == d2._day;
}
private:
int _year;
int _month;
int _day;
}
void Test ()
{
Date d1(2018, 9, 26);
Date d2(2018, 9, 27);
cout<<(d1 == d2)<<endl;//第一种情况相当于operator(d1,d2),第二种情况相当于d1.operator(d2)
}
2)赋值运算符重载
i)赋值运算符重载格式
- 参数类型:const T&,传递引用可以提高传参效率
- 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
- 检测是否自己给自己赋值
- 返回*this :要复合连续赋值的含义
和拷贝构造函数区别:拷贝构造函数使用一个已经存在的对象初始化另一个对象, 而赋值运算符重载函数是在已经存在的两个对象之间复制拷贝,本质上是运算符重载函数。
class Date
{
public :
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date (const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._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 ;
}
ii)赋值运算符只能重载类的成员函数,不能重载全局函数
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
int _year;
int _month;
int _day;
};
// 赋值运算符重载成全局函数,注意重载成全局函数时没有this指针了,需要给两个参数
Date& operator=(Date& left, const Date& right)
{
if (&left != &right)
{
left._year = right._year;
left._month = right._month;
left._day = right._day;
}
return left;
}
// 编译失败:
// error C2801: “operator =”必须是非静态成员
原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数
iii)用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。
注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值
总结:默认生成赋值重载跟拷贝构造行为一样
1.内置类型成员——值/浅拷贝
2.自定义类型会调用它的赋值重载
例如:Date和MyQueue不需我们实现,stack需要,因为默认生成浅拷贝,同一块空间会析构两次
3.如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现。
6.日期类的实现
//date.h
#pragma once
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<assert.h>
using namespace std;
class Date
{
public:
// 获取某年某月的天数
int GetMonthDay(int year, int month);
// 全缺省的构造函数
Date(int year = 1900, int month = 1, int day = 1);
// 拷贝构造函数
// d2(d1)
Date(const Date& d);
// 赋值运算符重载
// d2 = d3 -> d2.operator=(&d2, d3)
Date& operator=(const Date& d);
// 析构函数
~Date();
// 日期+=天数
Date& operator+=(int day);
// 日期+天数
Date operator+(int day);
// 日期-天数
Date operator-(int day);
// 日期-=天数
Date& operator-=(int day);
// 前置++
Date& operator++();
// 后置++
Date operator++(int);
// 后置--
Date operator--(int);
// 前置--
Date& operator--();
// >运算符重载
bool operator>(const Date& d);
// ==运算符重载
bool operator==(const Date& d);
// >=运算符重载
bool operator >= (const Date& d);
// <运算符重载
bool operator < (const Date& d);
// <=运算符重载
bool operator <= (const Date& d);
// !=运算符重载
bool operator != (const Date& d);
// 日期-日期 返回天数
int operator-(const Date& d);
void Print();
private:
int _year;
int _month;
int _day;
};
//date.cpp
#include"date.h"
int Date::GetMonthDay(int month, int year)
{
static int days[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
//写成静态数组,就不需要每次都创建数组了,作用域为全局
if (month == 2 && (year % 4 == 0 && year % 100 != 0) || year % 400 == 0)
{
return 29;
}
return days[month];
}
Date::Date(int year, int month , int day)
{
if (month > 0 && month <= 12 && day > 0 && day <= GetMonthDay(month,year))
{
_year = year;
_month = month;
_day = day;
}
else
{
cout<< "非法日期" << endl;
assert(false);
}
}
// 拷贝构造函数
// d2(d1)
Date::Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
// 赋值运算符重载
// d2 = d3 -> d2.operator=(&d2, d3)
Date& Date::operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
// 析构函数
Date::~Date()
{
cout << "~Date()" << endl;
}
// 日期+=天数
Date& Date::operator+=(int day)
{
if (day < 0)
return *this -= -day;
_day += day;
while (_day > GetMonthDay(_month, _year))
{
_month++;
_day -= GetMonthDay(_month, _year);
if (_month > 12)
{
_year++;
_month = 1;
}
}
return*this;
}
// 日期+天数
Date Date::operator+(int day)
{
Date tmp=*this;
*this += day;
return tmp;
}
// 日期-天数
Date Date::operator-(int day)
{
Date tmp = *this;
*this -= day;
return tmp;
}
// 日期-=天数
Date& Date::operator-=(int day)
{
if (day < 0)
return *this += -day;
_day -= day;
while (_day < 0)
{
--_month;
if(_month == 0)
{
_month = 12;
_year--;
}
_day += GetMonthDay(_month, _year);
}
return *this;
}
// 前置++
Date& Date::operator++()
{
*this += 1;
return *this;
}
// 后置++
Date Date::operator++(int)
{
Date tmp = *this;
*this += 1;
return tmp;
}
// 后置--
Date Date::operator--(int)
{
Date tmp = *this;
*this -= 1;
return tmp;
}
// 前置--
Date& Date::operator--()
{
*this -= 1;
return *this;
}
// >运算符重载
bool Date::operator>(const Date& d)
{
return !(*this <= d);
}
// ==运算符重载
bool Date::operator==(const Date& d)
{
return (_year == d._year) && (_month == d._month)&&( _day == d._day);
}
// >=运算符重载
bool Date::operator >= (const Date& d)
{
return !(*this < d);
}
// <运算符重载
bool Date::operator < (const Date& d)
{
if (_year < d._year)return true;
else if (_year == d._year)
{
if (_month < d._month) return true;
else if (_month == d._month)
{
if (_day < d._day)return true;
}
}
return false;
}
// <=运算符重载
bool Date::operator <= (const Date& d)
{
return *this < d || *this == d;
}
// !=运算符重载
bool Date::operator != (const Date& d)
{
return !(*this == d);
}
// 日期-日期 返回天数
int Date::operator-(const Date& d)
{
Date max = *this;
Date min = d;
int flag = 1;
if (*this < d)
{
max = d;
min = *this;
flag = -1;
}
int n = 0;
while (min != max)
{
++min;
++n;
}
return n * flag;
}
void Date::Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
7.const成员
被const修饰的成员函数是const成员函数,实际修饰的是该成员函数隐含的this指针,在该成员函数中不能对类的任何成员进行修改。
成员函数后加const后,普通和const对象都可调用。
但不是所有成员函数都可加这个const,要修改的对象成员变量函数不能加,只要成员函数内部不修改成员变量都应该加const。这样普通和const对象都能调用。

8.取地址及const取地址操作符重载
两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容
总结一下如何在测试中调用以上的函数:
- 构造函数:直接通过
类名(参数)创建对象。 - 成员函数:通过
对象.函数名(参数)调用(因为成员函数属于对象)。 - 友元运算符:作为全局函数,直接通过
运算符 对象调用(如cout << 对象、cin >> 对象)。 - 赋值运算符:作为特殊的成员函数,通过
对象1 = 对象2调用。
2007

被折叠的 条评论
为什么被折叠?



