如果一个类中什么成员都没有,简称为空类。空类中什么都没有吗?并不是的,任何一个类在我们不写的情况下,都会自动生成下面6个默认成员函数
class Data {};
class Data
{
void SetData(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
Data d1;
Data d1.SetData(2023,2,24);
对于Date类,可以通过SetDate公有的方法给对象设置内容,但是如果每次创建对象都调用该方法设置信息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢?
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有 一个合适的初始值,并且在对象的生命周期内只调用一次。
构造函数是特殊的成员函数,需要注意的是,构造函数的虽然名称叫构造,但是需要注意的是构造函数的主要任务并不是开空间创建对象(建立栈帧的时候创建对象的),而是初始化对象。
其特征如下:
3.
class Data
{
Data (int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
Date d1; // 1.调用无参构造函数
Date d2 (2015, 1, 1); // 2.调用带参的构造函数
// 3.注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
// 以下代码的函数:声明了d3函数,该函数无参,返回一个日期类型的对象
Data d3();❌
我们不写,编译器会生成一个默认无参构造函数
内置类型/基本类型:int/char/double/指针…
自定义类型:class/struct去定义的类型对象
默认生成的构造函数对于内置类型不做处理,对于自定义类型成员变量才会处理
总结:如果一个类中的成员全是自定义类型,我们就可以用默认生成的函数,如果有内置类型的成员,或者需要显示传参数初始化,那么都要自己实现构造函数
默认构造函数三大类:
1.
class Stack
{
--------->我们不写默认生成的
private:
int* _a;
int _top;
int _capacity;
};
2.
class Stack
{
Stack()----->我们写的无参的
{
_a=malloc();
_top=0;
_capacity=4;
}
3.
Stack(int capacity = 10)------->我们写的全缺省的
{
_a=malloc();
_top=0;
}
private:
int* _a;
int _top;
int _capacity;
};
a.我们不写默认生成的;
b.我们写的无参的;
c.我们写的全缺省的;
总结:**不强制传参的就是了**!
如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
class Stack
{
Stack(int capacity)---------->用户显示定义
{
_a=malloc();
_top=0;
_capacity=4;
private:
int* _a;
int _top;
int _capacity;
}
Stack st1;❌:默认构造调不了,编译器会报错。
class MyQueue
{
private:
int _size = 0;------➡️c++11打得补丁,针对编译器自己生成默认成员函数不初始化的问题
---->是给的缺省值,给编译器自己生成默认构造函数用,没有开辟空间。
Stack _st1;
Stack _st2;
};
后面会讲解“初始化列表”!其也是和构造函数相关。
析构函数:与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作 。
默认生成的析构函数对内置类型不会处理,自定义类型会调用其定义的析构函数
class Stack
{
public:
Stack(int capacity)
:_capacity(10)
{
}
~Stack()
{
}
private:
int* _a;
int _top;
int _capacity;
};
class MyQueue
{
~MyQueue()
{
}
private:
int _size = 0;
Stack _st1;
Stack _st2;
};
栈里面定义对象,析构顺序和构造顺序是反的
Stack st1(1);
Stack st2(2);
return 0;-------->会先构造st1,但是会先析构st2因为栈是先进后出的规则!
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
特征如下:
a.拷贝构造函数是构造函数的一个重载形式。
b. 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用。
class Data
{
public:
//Data(Data d):❌
//建议将“const”加上!
Data(const Data& d)---------➡️必须用引用!
{
}
};
int main()
{
Data d1;
Data d2(d1);
}
编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了
a.指向同一块空间,修改数据会互相影响;
b.这块空间析构时会释放两次,程序会崩溃;
c.浅拷贝不全是有问题,对日期类的类型完全够了;
解决方案:自己实现拷贝构造—深拷贝
1.
class Stack
{
public:
Stack(const Stack& st)
{
}
private:
int* _a;
int _top;
int _capacity;
};
Stack st1;
Stack st2(st1);❌----➡️浅拷贝多次析构_a的地址!
class Data
{
priviate:
int _a[];--------➡️这里浅拷贝不回拷贝_a的地址,而是会拷贝_a里面的数据域上面栈中拷贝int* _a拷贝地址不一样。所以这里不会程序崩溃✅
int _year;
int _month;
int _day;
};
a.默认生成的拷贝构造,会对内置类型(与构造和析构不同)进行值拷贝(浅拷贝),对日期类的类型没问题。当然对自定义类型还是会调用自定义的拷贝构造;
Weight f(Weight u)
{}
f(Weight());------构造+拷贝构造-----➡️优化合二为一了
a.内置类型,可以直接用各种运算符,但自定义类型,不能直接用各种运算符;
b.C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似
c.函数名字为:关键字operator后面接需要重载的运算符符号。
d.函数原型:返回值类型 operator操作符(参数列表)
e. 运算符重载------函数
函数名-------operator 运算符
参数--------运算符操作数(如:‘’==''就有两个,‘’++‘’就有一个以此类比)
a.写类外面
此为写在类外面的,此编程是有问题的,因为_year,_month,_day皆为私有成员变量
解决方法:在类里面提供函数 int GetYear()
或者使用“友元”---------➡️破坏“封装”
或者写在类里面,但是写类里面又有变化
bool operator ==(Data d1,Data d2)
{
if (d1._year == d2._year &&
d1._month == d2._month &&
d1._day == d2._day)
return true;
}
b.写类里面----------➡️有“this”指针!
bool operator ==(Data d1)
{
if (this->_year == d2._year &&
this->_month == d2._month &&
this->_day == d2._day)
return true;
}
if (operator==(d1, d2))
{
std::cout << "==" << std::endl;
}
if (d1 == d2)-------➡️等价于if(operator==(d1,d2))
{
std::cout << "==" << std::endl;
}
a.不能通过连接其他符号来创建新的操作符:比如operator@
b.重载操作符必须有一个类类型或者枚举类型的操作数
c.用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不 能改变其含义
d.作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的操作符有一个默认的形参this,限定为第一个形参
e..* 、:: 、sizeof 、?: 、. 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
注意:按道理“=”支持“连续赋值”所以返回值应该是本类型,且应该用“**引用**”!
**✳️Data&** operator=(const Data& d1)
{
✳️if(this!=&d1)---------➡️避免自己给自己赋值,标准写法!
{
_year = d._year;
_month = d. _month;
_day = d._day;
}
return ***✳️this**;
区别于:
Data d2(d1)------➡️拷贝构造:一个存在的对象去初始化另一个要创建的对象
Data d3 = d1------➡️虽然是赋值写法,但是是拷贝构造,紧跟上面拷贝构造的判断!
d1 = d2-------➡️赋值重载:两个已经存在对象之间赋值
}
像日期类其实不需要自己写“赋值重载(同拷贝构造)”
但是栈类的话还是需要写“赋值重载(同拷贝构造)”
int a = 0;
double b = 1.1;
cout << a << b;-------➡️会自动识别类型(内置类型系统本身就已经帮你运算符重载了)
1.
void operator << ( std::ostream& out)
{
out<< _year << _month << _day <<std::end;
}
调用:
d.operator<<(cout);✅
cout << d;❌;
d << cout;✅;
所以需要改进;
2.
void opearator<<(std::ostream& out, const Data& d)
{
out << d._year <<d._month <<d._day;-------➡️但是成员变量是类里面的无法调用呀!
解决方法 1.提供GetYear()等函数;
2.友元函数
在类里面声明:friend void operator<<(std::ostream& out, const Data& d);---➡️便可以访问私有成员了
若是友元函数,则该函数不属于类里面的,是类外面的,若函数声明和定义分离则定义那边不需要指定类域否则会发生在类里面找不到声明的报错。
3.注意全局函数不要在.h里面定义否则会在多个.cpp生成.o文件会在符号表里面重定义引发错误,但若是类里面定义的并不会报错因为“内联”属性不会放进符号表,即使代码块很长也会保持内联属性!
}
注意想要连续流插入和提取,则返回值应该是流类型;
void Pritn()const;-------->外面的“const”是给“this指针参数”添加的!
这样当图中 d.Print()调用时不会出现权限扩大问题,因为&d--->const Data* this
例如:(d1+100).Print()是无法调用 Print();因为&(d1+100)是const Data* this