在c++的学习过程中,类和对象是比较难啃的知识点,希望大家坚持学下去,勇于迎难而上,一起加油。
一个类中什么成员都没有,简称空类。空类,并不是类里面什么都没有,编译器会生成6个默认成员函数。
默认成员函数:
没有成员函数时,编译器会自己生成的成员函数称为默认成员函数。
构造函数是一种特殊的成员函数,名字和类名相同,创建类类对象时由编译器自动调用,在对象整个生命周期里只调用一次。(c++规定对象实例化,必须有构造函数)
特征:
#include
using namespace std;
class Date
{
public://公有
Date()//无参构造函数
{}
;
Date(int year, int math, int day)//有参构造函数
{
_year = year;
_math = math;
_day = day;
}
private://私有
int _year;
int _math;
int _day;
};
int main()
{
Date d1;//调用无参构造函数
Date d2(2024,10,13);//调用有参构造函数
/* Date(); 通过无参构造函数初始化对象时,
对象后面不要跟括号,否则就是函数声明*/
return 0;
}
#include
using namespace std;
class Time
{
public://公有
Time()//无参构造函数是默认构造函数
{
_hour = 23;
_minute=44;
_second=59;
}
private://私有
int _hour;
int _minute;
int _second;
};
class Date
{
public://公有
private://私有
//声明不开空间,定义开空间,下面内置类型和自定义类型都是声明
int _year=2008;//给内置类型赋缺省值
int _math=10;
int _day=1;
//自定义类型
Time _t;//调用默认构造函数
};
int main()
{
Date d1;//调用无参构造函数
return 0;
}
#include
using namespace std;
class Date
{
public://公有
//1.编译器自己生成构造函数
/*
空
*/
/*2.无参构造的函数也可以叫默认构造
Date()
{
_year = 2023;
_math = 11;
_day = 21;
}*/
/*3.全缺省参数也可以叫默认构造
Date(int year=2021,int math=12,int day=1)
{
_year = year;
_math = math;
_day = day;
}*/
private://私有
int _year;
int _math;
int _day;
};
int main()
{
Date d1;
Date d2;
Date d3;
return 0;
}
析构函数:
析构函数与构造类型相反,析构函数不是对对象本身进行销毁,局部对象销毁工作是编译器完成的 ,对象在销毁时会自动调用析构函数,完成对象中资源的清理。
特征:
#include
using namespace std;
class Time
{
public://公有
~Time()//
{
cout<<"~Time"<<endl;
}
private://私有
int _hour;
int _minute;
int _second;
};
class Date
{
public://公有
private://私有
//声明不开空间,定义开空间,下面内置类型和自定义类型都是声明
int year=1970;//给内置类型赋缺省值
int math=1;
int day=1;
//自定义类型
Time t;
};
int main()
{
Date d1;
return 0;
}
解释:
程序运行结束后输出: ~Time()
在main中根本没有直接创建Time类的对象,为什么最后会调用Time类的析构函数?
因为: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类生成的默认析构函数。
注意:创建哪个类的对象则调用该类的析构函数,销毁那个类的对象则调用该类的析构函数。
拷贝构造函数主要是解决深拷贝问题。
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般用const修饰),用已经存在的类类型的对象创建一个新的对象,由编译器自动调用。
特征:
#include
using namespace std;
class Time
{
public://公有
Time()//构造函数
{
_hour=1;
_minute=1;
_second=1;
}
Time(const Time& d)//拷贝构造函数
{
_hour = d._hour;
_minute = d._minute;
_second = d._second;
}
private://私有
int _hour;
int _minute;
int _second;
};
class Date
{
public://公有
private://私有
//声明不开空间,定义开空间,下面内置类型和自定义类型都是声明
int _year = 1970;//给内置类型赋缺省值
int _math = 1;
int _day = 1;
//自定义类型
Time _t;
};
int main()
{
Date d1;
//已经存在的d1拷贝构造d2,此处会调用Date类的拷贝构造函数,但是Date类并没有显示定义拷贝构造函数,则编译器会自动给Date类生成一个拷贝构造函数。
Date d2(d1);
return 0;
}
注意: 编译器生成默认拷贝构造函数,内置类型按照字节方式直接拷贝,自定义类型调用拷贝构造函数完成拷贝。
#include
using namespace std;
typedef int DateType;
class Stack
{
public://公有
//构造函数
Stack( int capacity = 10)
{
_a = (DateType*)malloc(sizeof(DateType) * capacity);
if (_a == nullptr)
{
perror(" _a");
exit(-1);
}
_size = 0;
_capacity = capacity;
}
//压栈
void push(const DateType& data)
{
_a[_size] = data;
_size++;
}
//析构函数
~Stack()
{
if (_a)
{
free(_a);
_a = nullptr;
_size = 0;
_capacity = 0;
}
}
private://私有
DateType* _a;//指向的内容
int _size;//下标
int _capacity;//容量
};
int main()
{
Stack d1;
d1.push(1);
d1.push(2);
d1.push(3);
Stack d2(d1);
return 0;
}
解释:
运行上面程序,程序崩溃
d1对象调用了构造函数,申请了10个元素空间,里面存了3个元素1,2,3;
d2对象使用d1拷贝构造,Stack类中没有显示定义拷贝构造函数,编译器就自己生成一份默认拷贝构造函数,默认拷贝构造函数是按照值拷贝得,将d1的内容原封不动拷贝到d2中。因此d1和d2指向同一块空间;
当程序退出时,d1和d2要销毁,先销毁d2,d2销毁时,调用析构函数,将调用的空间释放了,但d1不知道,d1销毁时,会将空间在释放一次,一块空间多次释放,肯定就造成了内存泄漏。
使用已存在的对象创建新对象
函数参数类型为类类型对象
函数返回值类型为类类型对象
深拷贝:
本质是拷贝指向的资源,让我跟你有一样大的空间,一样的值。
浅拷贝也是值拷贝:
默认生成拷贝构造函数。
#include
using namespace std;
class Date
{
public://公有
Date(int year,int month,int day)//构造函数
{
cout << "Date(int year,int month,int day)" << endl;
}
Date(const Date& d)//拷贝构造函数
{
cout << "Date(const Date& d)" << endl;
}
~Date()//析构函数
{
cout << "~Date()" << endl;
}
private://私有
int _year;
int _month;
int day;
};
Date test(Date d2)
{
Date temp(d2);
return temp;
}
int main()
{
Date d1(2024, 11, 1);
test(d1);
return 0;
}
为了提高程序效率,一般对象传参时,尽量使用引用类型。
赋值运算符重载使得自定义类型也能直接使用运算符。(c语言里只有内置类型才能直接使用运算法)
c++为了增强代码的可读性使用了运算符重载,运算符重载是具有特殊函数名的函数,它具有返回值类型、函数名字、参数列表、其中返回值类型和参数列表类似。
函数名为:operator后面加需要重载的运算符符号。
实现一个全局的operator==
方法1:
//定义一个日期类
class Date_1
{
public://公有
//构造函数
Date_1(int year_1 = 1970, int month_1 = 1, int day_1 = 1)
{
_year_1= year_1;
_month_1= month_1;
_day_1= day_1;
}
//private://私有
int _year_1;
int _month_1;
int _day_1;
};
//实现自定义类型的==判断,返回值为布尔类型
bool operator ==(const Date_1& d1, const Date_1& d2)
{
return d1._year_1 == d2._year_1 &&
d1._month_1 == d2._month_1 &&
d1._day_1 == d2._day_1;
}
void test_1()
{
Date_1 d1(2023, 11, 2);
Date_1 d2(2023, 11, 1);
cout << (d1 == d2) << endl;
}
int main()
{
test_1();//运算符重载为全局时——成员变量为公有才能实现
}
方法2:
#include
using namespace std;
//实现一个全局的operator==
//定义一个日期类
class Date_1
{
//使用友元,全局的那个函数可以访问类的私有成员
friend bool operator ==(const Date_1& d1, const Date_1& d2);
public://公有
//构造函数
Date_1(int year_1 = 1970, int month_1 = 1, int day_1 = 1)
{
_year_1 = year_1;
_month_1 = month_1;
_day_1 = day_1;
}
private://私有
int _year_1;
int _month_1;
int _day_1;
};
//实现自定义类型的==判断,返回值为布尔类型
bool operator ==(const Date_1& d1, const Date_1& d2)
{
return d1._year_1 == d2._year_1 &&
d1._month_1 == d2._month_1 &&
d1._day_1 == d2._day_1;
}
void test_1()
{
Date_1 d1(2023, 11, 2);
Date_1 d2(2023, 11, 1);
cout << (d1 == d2) << endl;
}
int main()
{
test_1();//运算符重载为全局时——用友元,可以访问类的私有成员变量
}
方法三:调用成员函数
#include
using namespace std;
//实现一个全局的operator==
//定义一个日期类
class Date_1
{
public://公有
//构造函数
Date_1(int year_1 = 1970, int month_1 = 1, int day_1 = 1)
{
_year_1 = year_1;
_month_1 = month_1;
_day_1 = day_1;
}
//实现自定义类型的==判断,返回值为布尔类型
//成员函数都有一个隐藏的this指针,实际是:bool operator ==( Date_1* this,const Date_1& d2)
bool operator ==( const Date_1& d2)//默认d1.operator ==(d2)——>d1.operator ==(&d1,d2)
{
return _year_1 == d2._year_1 &&
_month_1 == d2._month_1 &&
_day_1 == d2._day_1;
}
private://私有
int _year_1;
int _month_1;
int _day_1;
};
void test_1()
{
Date_1 d1(2023, 11, 2);
Date_1 d2(2023, 11, 1);
cout << (d1 == d2) << endl;
}
int main()
{
test_1();//运算符重载在类里做成员函数的方法
}
注意:
运算符重载和函数重载没有任何关系。
运算符重载:使自定义类型可以直接使用运算符
函数重载:可以允许使用参数不同的同名函数
参数类型:
返回值类型:
检测是否自己给自己赋值
返回* this:成员函数返回 *this,成员函数调用结束后,*this不销毁。(可以连续赋值)
原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载, 就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。
注意:
我们可以重载赋值运算符。不论形参的类型是什么,赋值运算符都必须定义为成员函数。
#include
//定义一个类
class Time
{
public://公有
//构造函数
Time()
{
_hour = 19;
_minute = 1;
_second =30;
}
//赋值运算符重载
Time& operator =(const Time& d)
{
if(this!=&d)
{
_hour = d._hour;
_minute = d._minute;
_second = d._second;
}
return *this;
}
private://私有
int _hour;
int _minute;
int _second;
};
//定义一个类
class Date
{
public://公有
private://私有
//内置类型
int _year=1970;
int _month=1;
int _day=1;
//自定义类型
Time _t;
};
int main()
{
Date d1;
Date d2;
d2 = d1;//两个已经初始化的对象进行赋值运算
return 0;
}
注意:
1.operator =我们不写,编译器会直接生成默认的operator =。
2.跟拷贝构造的行为类似,内置类型值拷贝,自定义类型调用他的赋值。
3.Date和MyQuece也可以不写,默认生成的operator =就可以用。
4.Stack必须自己实现operator =,才能深拷贝。
如果类中未涉及到资源管理,赋值运算是否实现都可以,一旦涉及到资源管理,就必须实现赋值运算。
为了让前置++和后置++能形成正确的重载。需要对后置++进行特殊处理(语法设计有时无法逻辑闭环,那么这时候就要进行特殊处理,后置++重载时多增加一个int类型的参数)
#include
using namespace std;
//创建一个类
class Date
{
public://公有
//构造函数
Date(int year = 1970, int month = 1, int day = 1)
{
_year= year;
_month= month;
_day= day;
}
//获取某月的天数
int GetMonthDay(int year,int month)
{
int arr[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
//判断是否为闰年
if (month==2&&(year % 400 == 0 || year % 4 == 0 && year % 100 != 0))
return 29;
return arr[month];
}
//赋值运算符重载
//前置++:返回+1后的结果
//注意:this指向的对象在函数结束后不会销毁,用引用方式返回提高效率
/*也可以
Date& operator ++()
{
_day+=1;
return *this;
}
*/
Date operator ++()
{
if(GetMonthDay(this->_year,this->_month) <++this->_day)
{
this->_month++;
this->_day = 1;
if (this->_day == 13)
{
this->_year++;//年加一
this->_month = 1;
}
}
return *this;
}
//后置++,特殊处理加一个形参
// 前置++和后置++都是一元运算符,为了让前置++与后置++形成正确重载
// C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传递
//注意:后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将this保存一份, 然后给this+1
//而temp是临时对象,因此只能以值的方式返回,不能返回引用
/*也可以
Date operator ++(int)
{
Date tmp(*this);//拷贝构造函数
_day+=1;
return tmp;
}
*/
Date operator ++(int)
{
Date tmp(*this);//拷贝构造函数
if (GetMonthDay(this->_year, this->_month) < ++this->_day)
{
this->_month++;
this->_day = 1;
if (this->_day == 13)
{
this->_year++;//年加一
this->_month = 1;
}
}
return tmp;
}
//private://私有
//声明内置变量
int _year;
int _month;
int _day;
};
int main()
{
//定义两个对象
Date d;//默认构造函数
Date d1(2023,11,3);//构造函数
d = d1++;//d(2023,11,3)d1(2023,11,4)
cout << d._day << endl;
d = ++d1;//d(2023,11,5) d1(2023,11,5)
cout << d._day << endl;
return 0;
}
将const修饰的“成员函数”称为const成员函数,const修饰成员函数,实际上是修饰该成员函数隐藏的this指针,表明在该成员函数中不能对类的任何成员进行修改。const修饰的是*this
权限可以平移也可以缩小
成员函数定义的规则:
- 能定义成const的成员函数都应该定义成const,这样const对象和非const对象都能调用。
- 要修改变量的成员函数,不能定义成const。
问题:
- const对象可以调用非const成员函数吗?错权限放大
- 非const对象可以调用const成员函数吗?对权限缩小
- const成员函数内可以调用其它的非const成员函数吗?错权限放大
- 非const成员函数内可以调用其它的const成员函数吗?对权限缩小
这两个默认的成员函数一般不用重新定义,编译器默认会生成。
#include
using namespace std;
class Date
{
public://公有
//构造函数
Date(int year = 1, int month = 1, int day = 1)
{
_year=year;
_month=month;
_day=day;
}
//取地址和const取地址操作符重载
//取地址重载
Date* operator&()
{
return this;
}
//const取地址重载
const Date* operator&()const
{
return this;
}
private://私有
int _year;
int _month;
int _day;
};
int main()
{
const Date d1;
Date d2(2023, 11, 6);
//自动匹配,找到更匹配自己的成员函数
cout << &d1 << endl;
cout << &d2 << endl;
return 0;
}
这两个运算符一般不需要重载,编译器会自动生成成员函数,只有特殊情况,才需要重载(想让别人获取到指定的内容)。
补充:
双操作数的运算符,第一个参数是左操作数,第二个参数为右操作数。
写流插入流提取的赋值运算符重载时,定义的函数为全局函数(就可以自己定义函数参数的位置)
流插入——‘<<’
流提取——‘>>’
其他的运算符一般是实现成员函数>> <<流运算符必须实现到全局,这样才能让流对象做第一个参数,才符合可读性。
以日期类为例:
全局函数:
ostream& operator<<(ostream& out,const Date& d2)
{
out<return out;
}
在类里面定义友元函数:
friend void operator<<(ostream& out,const Date& d2);
流本质上是解决自定义类型的输入输出问题。
printf和scanf无法解决自定义类型的输入输出问题。
c++用面向对象+运算符重载来解决问题。