文章目录
1. 概念
如果一个类中什么成员都没有,简称为空类。空类中什么都没有吗?并不是的,任何一个类在我们不写的情况下,都会自动生成下面6个默认成员函数
2. 构造函数
构造函数:一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有一个合适的初始值,并且在对象的生命周期内只调用一次 。需要注意的是,构造函数的虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
简单来讲就是为了防止自己忘记初始化,所以创造一个构造函数帮我自动初始化。
其特征如下:
1️⃣函数名与类名相同。
2️⃣无返回值。
3️⃣对象实例化时编译器自动调用对应的构造函数。
4️⃣构造函数可以重载。
class Date
{
public:
//1. 无参构造函数
Date()
{
_year = 1;
_month = 1;
_day = 1;
}
//2. 带参构造函数
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 d3();//不能这样写,编译器会把它认成函数声明(声明了d3函数,该函数无参,返回一个日期类型的对象)
Date d1;//调用无参构造函数
d1.Print();
Date d2(2022, 6, 20);//调用带参的构造函数
d2.Print();
return 0;
}
5️⃣无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认成员函数。(不用传参就能调用的才是默认构造函数)
// 默认构造函数
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;
};
// 以下测试函数能通过编译吗?
void Test()
{
Date d1;
}
//不能。因为会产生歧义,究竟是调用全缺省构造函数还是调用无参构造函数呢?
//往往,我们只保留全缺省构造函数(传不传参数都能用)
6️⃣如果类中没有显式定义构造函数,则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;
};
void Test()
{
// 没有定义构造函数,对象也可以创建成功,因此此处调用的是编译器生成的默认构造函数
Date d;
d.Print();
}
int main()
{
Test();
return 0;
}
执行结果(看起来就是随机值而已,构造函数起作用了吗?):
7️⃣关于编译器生成的默认成员函数,很多人会有疑惑:在我们不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?d对象调用了编译器生成的默认构造函数,但是d对象year/month/day,依旧是随机值。也就说在这里编译器生成的默认构造函数没啥用??
解答:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语法已经定义好的类型:如int/char/指针…,自定义类型就是我们使用class/struct/union自己定义的类型,看看下面的程序,就会发现编译器生成的默认构造函数对于内置类型成员变量不做处理,对于自定义类型变量才会处理!(有些离谱了)
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;
int _month;
int _day;
// 自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
可见只有自定义类型的变量才被默认构造函数初始化成零(其实这里是嵌套的,Date调用默认构造函数,初始化Time _t时调用Time的默认构造函数,这里是因为Time里的构造函数将变量都初始化为零,我们看到的效果才是默认构造函数将自定义类型里的变量初始化为零,否则还是随机值)
总结:如果一个类中的成员全是自定义类型,我们就能直接用默认生成的函数。如果有内置类型的成员,或者需要显示传参初始化,那么都要自己实现构造函数。
在C++11针对编译器生成的默认成员函数不初始化的问题,打了个补丁。给内置类型一个初始值,编译器能自己生成默认构造函数
//class Date
//{
//private:
// // 基本类型(内置类型)
// int _year;
// int _month;
// int _day;
// // 自定义类型
// Time _t;
//};
//给内置类型初始值
class Date
{
private:
// 基本类型(内置类型)
int _year = 2022;
int _month = 6;
int _day = 22;
// 自定义类型
Time _t;
};
3. 析构函数
析构函数:与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作
简单来讲就是为了防止自己忘记destroy,所以创造一个析构函数帮我自动destroy。
特性:
析构函数是特殊的成员函数。
其特征如下:
1️⃣析构函数名是在类名前加上字符 ~。
2️⃣无参数无返回值。
3️⃣一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
4️⃣对象生命周期结束时,C++编译系统系统自动调用析构函数。
typedef int DataType;
class SeqList
{
public:
SeqList(int capacity = 10)
{
_pData = (DataType*)malloc(capacity * sizeof(DataType));
assert(_pData);
_size = 0;
_capacity = capacity;
}
~SeqList()
{
if (_pData)
{
free(_pData); // 释放堆上的空间
_pData = NULL; // 将指针置为空
_capacity = 0;
_size = 0;
}
}
private:
int* _pData;
size_t _size;
size_t _capacity;
};
int main()
{
int i = 1;
if (i > 0)
{
SeqList d;
}//大括号里是生命周期,出了大括号自动调用析构函数
return 0;
}
5️⃣栈里面定义对象(后进先出),析构顺序和构造顺序是相反的
typedef int DataType;
class SeqList
{
public:
SeqList(int capacity = 10)
{
_pData = (DataType*)malloc(capacity * sizeof(DataType));
assert(_pData);
_size = 0;
_capacity = capacity;
}
~SeqList()
{
if (_pData)
{
free(_pData); // 释放堆上的空间
_pData = NULL; // 将指针置为空
_capacity = 0;
_size = 0;
}
}
private:
int* _pData;
size_t _size;
size_t _capacity;
};
int main()
{
//栈里面定义对象(后进先出),析构顺序和构造顺序是相反的
SeqList s1(1);
SeqList s2(2);
//调试,构造时看capacity的值,析构的时候看this指针里_capacity的值
return 0;
}
6️⃣关于编译器自动生成的析构函数,内置类型不作处理,对自定类型成员会调用它的析构函数
析构函数和构造函数很像,类比学习就行了。
4. 拷贝构造函数
生活中我们能见到双胞胎,那在创建对象时,可否创建一个与一个对象一某一样的新对象呢?
构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用
拷贝构造函数也是特殊的成员函数,其特征如下:
1️⃣拷贝构造函数是构造函数的一个重载形式。
2️⃣拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用
对于自定义类型的变量,我们传值传参要调用拷贝构造函数(这里涉及深浅拷贝,之后再讲),而我们要调用拷贝构造函数首先要传参,传参要调用拷贝构造……总而言之,形成了一个无限的圈,走不出去。这种时候使用引用传参才能跳出这个闭环
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//Date d2(d1);//const保护d1的值,以防写反了
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️⃣那么编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,我们还需要自己实现吗?像日期类这样的类是没必要的。那么下面的类呢?
class Stack
{
public:
Stack(int capacity = 4)
{
cout << "Stack(int capacity = 4)" << endl;
_a = (int*)malloc(sizeof(int) * capacity);
assert(_a);
_top = 0;
_capacity = capacity;
}
~Stack()
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_capacity = _top = 0;
}
private:
int* _a;
int _top;
int _capacity;
};
int main()
{
Stack st1(10);
Stack st2(st1);
//结束时调用两次析构函数,两次释放同一块空间,报错
//这种情况要用
return 0;
}
我们不写,编译器会默认生成一个拷贝构造。内置类型的成员会完成值拷贝(浅拷贝);自定义类型的成员会去调用该成员的拷贝构造函数
结论:一般的类,默认生成拷贝构造就够了,像Stack这样的类,自己管理资源,才需要我们自己实现深拷贝
5. 赋值运算符重载
5.1 运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名:operator 运算符
参数:运算符的操作数
返回值:运算符运算后的结果
能把运算符重载写到类外面的几种写法
- 我们得把类成员变量改为public才能玩
- 自己写GetYear,GetMonth函数(太麻烦)
- 用友元(这里也不合适)
以下是把类成员变量改为public把运算符重载放到类外面的写法,先来熟悉一下运算符重载的用法。
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
//private:
int _year;
int _month;
int _day;
};
//运算符重载——函数
//函数名:operator 运算符
//参数:运算符的操作数
//返回值:运算符运算后的结果
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, 6, 29);
Date d2(2022, 6, 29);
//内置类型,可以直接用各种运算符
//自定义类型,不能直接用各种运算符
//为了自定义类型可以直接使用各种运算符,运算符重载
//if (d1 > d2)
//{
// cout << ">" << endl;
//}
if (operator==(d1, d2))//就像调用函数一样
{
cout << "==" << endl;
}
//一般会写成以下形式
if (d1 == d2)//编译器会处理成对应重载运算符调用if (operator==(d1, d2))
{
cout << "==" << endl;
}
return 0;
}
我们把运算符重载放到类里,这时就要注意this指针了
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
//bool operator==(Date* const this, const Date& d)
bool operator==(const Date& d)//第一个参数默认是this,引用传参
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022, 6, 29);
Date d2(2022, 6, 29);
if (d1.operator==(d2))//this指针
{
cout << "==" << endl;
}
//一般会写成以下形式
if (d1 == d2)//编译器会处理成对应重载运算符调用if (d1.operator==(&d2))
{
cout << "==" << endl;
}
//碰到运算符重载会优先到类里面找
return 0;
}
注意:
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型或者枚举类型的操作数
- 用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不能改变其含义
- 作为类成员的重载函数时,其形参看起来比操作数数目少一个成员函数,因为操作符有一个默认的形参this,限定为第一个形参
.*
、::
、sizeof
、?:
、.
注意以上5个运算符不能重载。这个经常在笔试选择题中出现
5.2 赋值运算符重载
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
//Date operator=(Date* const this, const Date& d)
//返回值应该是左操作数
//传引用返回方便,不用调用拷贝构造。
//这里不用返回const Date& ,因为连等是可以处理(d1 = d2) = d3; 的
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(2022, 6, 28);
Date d2(2022, 6, 29);
Date d3(d1); // 拷贝构造 -- 一个存在的对象去初始化另一个要创建的对象
d3 = d2 = d1;// 赋值重载/复制拷贝 -- 两个已经存在对象之间赋值
//连续赋值是有返回值的,返回值应该是左操作数
d1 = d1;
return 0;
}
赋值运算符主要有四点:
- 参数类型
- 返回值
- 检测是否自己给自己赋值
- 返回*this
- 一个类如果没有显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝。其实参考拷贝构造,日期类没必要写赋值运算符重载,Stack这样的类,才需要我们自己写赋值运算符重载
6. const成员
将const修饰的类成员函数称为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//void Print()//(Date* const this)
//void Print() const相当于(const Date* const this),用于保护内容
//否则Func函数里无法调用Print,因为从const到非const权限放大了
void Print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
void Func(const Date& d)
{
d.Print();//const Date*
}
int main()
{
Date d1(2022, 7, 2);
d1.Print();//Date*
Func(d1);//const Date*
return 0;
}
如果声明和定义分离,两边都要加const
- const对象可以调用非const成员函数吗?(×)权限放大
- 非const对象可以调用const成员函数吗?(√)权限缩小
- const成员函数内可以调用其它的非const成员函数吗?(×)权限放大
- 非const成员函数内可以调用其它的const成员函数吗? (√)权限缩小
7. 取地址及const取地址操作符重载
class Date
{
public :
Date* operator&()//普通对象调用非const的
{
return this ;
}
const Date* operator&()const//const对象就调用const
{
return this ;
}
private :
int _year ;
int _month ;
int _day ;
};
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可。只有特殊情况,才需要重载,比如想让别人获取到指定的内容! (返回空指针)
8. 实现日期类
//Date.h
#pragma once
#include<iostream>
#include<assert.h>
using std::cin;
using std::cout;
using std::endl;
class Date
{
public:
bool isLeapYear(int year)
{
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}
int GetMonthDay(int year, int month);
Date(int year = 1, int month = 1, int day = 1);
//void Print()//(Date* const this)
void Print() const//(const Date* const this)保护内容
{
cout << _year << "-" << _month << "-" << _day << endl;
}
bool operator<(const Date& d) const;
bool operator==(const Date& d) const;
//在类里面定义的默认是内联函数(内联函数声明和定义不能分离)
bool operator<=(const Date& d) const
{
return *this < d || *this == d;
}
bool operator!=(const Date& d) const
{
return !(*this == d);
}
bool operator>=(const Date& d) const
{
return !(*this < d);
}
bool operator>(const Date& d) const
{
return !(*this <= d);
}
Date& operator=(const Date& d);//一定要加const否则右值权限放大会编译错误
Date& operator+=(int day);
Date operator+(int day) const;
Date& operator-=(int day);
Date operator-(int day) const;
//++d1
Date& operator++() //前置++
{
*this += 1;
return *this;
}
//d1++
Date operator++(int i) //后置++(i用于区分,也能写成(int))
{
Date tmp(*this);
*this += 1;
return tmp;
}
//--d1
Date& operator--()
{
*this -= 1;
return *this;
}
//d1--
Date operator--(int)
{
Date tmp(*this);
*this -= 1;
return tmp;
}
int operator-(const Date& d) const;
private:
int _year;
int _month;
int _day;
};
//Date.cpp
#define _CRT_SECURE_NO_WARNINGS
#include"Date.h"
//缺省参数只在声明里出现
Date::Date(int year, int month, int day)
{
if (year >= 1
&& month <= 12 && month >= 1
&& day <= GetMonthDay(year, month) && day >= 1)
{
_year = year;
_month = month;
_day = day;
}
else
{
cout << "日期非法" << endl;
}
}
int Date::GetMonthDay(int year, int month)
{
assert(year >= 0 && month >= 1 && month <= 12);
//优化:加static
static int monthDayArr[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
if (month == 2 && isLeapYear(year))
{
return 29;
}
return monthDayArr[month];
}
bool Date::operator<(const Date& d) const
{
return ((_year < d._year) || (_year == d._year && _month < d._month) || (_year == d._year && _month == d._month && _day < d._day));
}
bool Date::operator==(const Date& d) const
{
return _year == d._year && _month == d._month && _day == d._day;
}
Date& Date::operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
Date& Date::operator+=(int day)
{
//防止day是负数
if (day < 0)
return *this -= -day;
_day += day;
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
_month++;
if (_month == 13)
{
_year++;
_month = 1;
}
}
return *this;//this会被销毁,*this不会被销毁,可以直接传引用
}
//Date Date::operator+(int day)
//{
// Date ret(*this);
// ret._day += day;
//
// //逐个进位
// while (ret._day > GetMonthDay(ret._year, ret._month))
// {
// ret._day -= GetMonthDay(ret._year, ret._month);
// ret._month++;
// if (ret._month == 13)
// {
// ret._year++;
// ret._month = 1;
// }
// }
// //直接返回值,返回引用可能会有野指针出现
// return ret;
//}
//
//+复用+=函数(如果是反过来复用,+本身就要有两次拷贝【拷贝构造,传值返回】,+=也会变成需要两次拷贝)
Date Date::operator+(int day) const
{
Date ret(*this);
ret += day;
return ret;
}
Date& Date::operator-=(int day)
{
if (day < 0)
return *this += -day;
_day -= day;
while (_day <= 0)
{
_month--;
if (_month == 0)
{
_year--;
_month = 12;
}
_day += GetMonthDay(_year, _month);//顺序要对,+的是上个月的天数
}
return *this;
}
Date Date::operator-(int day) const
{
Date ret(*this);
ret -= day;
return ret;
}
//d1 - d2
int Date::operator-(const Date& d) const
{
Date max = *this;
Date min = d;
int flag = 1;
if (min > max)
{
min = *this;
max = d;
flag = -1;
}
int n = 0;
while (min != max)
{
++min;
++n;
}
return n*flag;
}
//Test.cpp
#define _CRT_SECURE_NO_WARNINGS
#include"Date.h"
void Test1()
{
Date d1(2022, 12, 29);
//if (d1 > d2)
// cout << ">" << endl;
Date d2 = d1 + 6;//这里调用拷贝构造
d2 = d1 + 2;//赋值
d1.Print();
d2.Print();
}
void Test2()
{
Date d1(2022, 3, 10);
Date d2(2022, 5, 19);
//Date d3 = d2 + 14;
//d1 += 31;
Date d3 = d2 - 14;
d1 -= 1100;
d2.Print();
d3.Print();
d1.Print();
}
void Test3()
{
Date d1(2022, 5, 18);
d1 += -100;
d1.Print();
d1 -= -100;
d1.Print();
Date ret1 = d1--;
ret1.Print();
d1.Print();
Date ret2 = --d1;
ret2.Print();
d1.Print();
}
void Test4()
{
Date d1(1, 2, 3);
Date d2(1, 2, 6);
cout << d1 - d2 << endl;
}
void Func(const Date& d)
{
d.Print();//const Date*
}
void Test5()
{
Date d1(2022, 5, 30);
//d1.Print();//Date*
//Func(d1);//const Date*
(d1 + 100).Print();//如果Print不加const就编不过,因为d1+100返回的是临时变量的拷贝,是个const
}
int main()
{
//Test1();
Test5();
return 0;
}
9. <<和>>的重载(引入友元)
我们在实现cin>>(流插入)和cout<<(流提取)时需要用到<<
和>>
的重载
在cplusplus网址上我们能看到下面这张图
cin是istream类型的对象,cout是ostream类型的对象
C++里cout<<
能自动识别类型就是因为有<<的重载
class Date
{
//友元函数(允许类外的函数访问私有的成员变量)
friend std::ostream& operator<<(std::ostream& out, const Date& d);
friend std::istream& operator>>(std::istream& in, Date& d);
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//成员函数第一个参数默认是this导致要写成d1.operator<<(cout);d1 << cout;很奇怪
//void operator<<(std::ostream& out)//out是cout的别名
//{
// out << _year << "-" << _month << "-" << _day << endl;
//}
private:
int _year;
int _month;
int _day;
};
//类外面定义的函数,如果声明和定义分离,在.h里声明,.cpp里定义,当test.cpp和Date.cpp里的.h展开后,会包含两个函数个定义,链接的时候符号表里有多个定义,会冲突,产生链接错误
//类里面定义的函数默认是内联,没有这种问题
std::ostream& operator<<(std::ostream& out, const Date& d)//out是cout对象的别名
{
out << d._year << "-" << d._month << "-" << d._day << endl;//成员变量是私有的,用友元函数
return out;//支持连续提取
}
std::istream& operator>>(std::istream& in, Date& d)//in是cin对象的别名
{
in >> d._year >>d._month >> d._day;//成员变量是私有的,用友元函数
//这里最好再判断一下日期的合法性
return in;//支持连续提取
}
思路:重载函数放到类里(感觉很奇怪)->重载函数放到类外->使用友元才能访问private成员变量->修改返回值让它能支持连续提取->给<<
重载函数的第二个形参加const(>>
不能加,它会修改d成员变量)->判断输入日期的合法性(跟日期类里面一样判断)