我们将会在下面介绍到:
0.初识类的6个默认成员函数
1.构造函数
2.析构函数
3.拷贝构造函数
4.复制运算符重载
5.const成员函数
6.取地址及const取地址操作符重载
---------------------------------------------------------------------------------------------------------------------------------
0.初始默认成员函数
0.1
如果一个类是空类,它并不是什么都没有,编译器会默认为我们生成6个默认成员函数。但是我们也可以自己定义默认成员函数,这时编译器就不会生成。
0.2
不同的默认成员函数可以完成不同的功能。
1.构造函数(在对象构造时调用的函数,用来初始化)
1.1构造函数的分类
值得注意的是,类中只允许有一个默认构造函数,而且一旦我们自己写了构造函数,那么编译器就不会自己生成构造函数。
1.2构造函数的性质
a.函数名和类名同 b.无返值 c.类实例化时将自动用 d.构造函数可以重载
class Data
{
public:
Data()
{
}//无参数形构造函数
Data(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
} // 无缺省的构造函数
Data(int year= 0, int month=1, int day=1)
{
_year = year;
_month = month;
_day = day;
} // 全缺省的构造函数
private:
int _year;
int _month;
int _day;
};
1.3观察构造函数
我们首先看编译器默认生成的构造函数
这里看到似乎并没有初始化,但是真的没有初始化吗?
#include<iostream>
using namespace std;
class person
{
public:
person(int age = 18)
{
_age = age;
}
//private:
int _age;
};
class Data
{
public:
void print()
{
cout << _year << "-" << _month << "-" << _day << "-"<<claus._age << endl;
}
private:
int _year;
int _month;
int _day;
person claus;
};
int main()
{
Data dl;
dl.print();
return 0;
}
当我们运行上诉代码时发现,claus.age居然打印出的是18.我们可以得出结论:
编译器生成的默认构造函数对内置类型(如int char ..)并不做处理,对自定义类型会调用它的构造函数进行处理.
我们在日常时一般写全缺省参数的构造函数。这样当我们需要自己设定默认值时Data d2(2024, 10, 21);当我们不需要自己设定时Data dl;
2.析构函数
2.1概念
当对象的生命周期结束后,析构函数就会自动被调用,完成清理工作,但是值得注意的是:析构函数完成的并不是对对象的销毁(这是交给编译器自己完成的),而是销毁后的清理工作。
2.2性质
a.析构函数是在类名上加上~ b.无参数,无返回值 c.一个类只有一个析构函数,自定义的析构函数不能重载 d.对象生命周期结束后,编译器会自动调用析构函数。
class my_Stack
{
public:
my_Stack(int n = 4)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc");
exit(1);
}
_arr = tmp;
_size = 0;
_capacity = n;
}
//......
~my_Stack()
{
free(_arr);
_arr = nullptr;
_size = _capacity = 0;
}
private:
int* _arr;
int _size;
int _capacity;
};
上述代码就是完成了构造函数和析构函数的模拟栈。
值得注意的是,析构函数和构造函数在某些方面有相似性,默认的析构函数对内置类型不会处理,对自定义类型会调用它的析构函数进行处理。
如果类没有申请资源时,析构函数可以不写,直接使用默认的。
#include<iostream>
using namespace std;
//class person
//{
//public:
// person(int age = 18)
// {
// _age = age;
// }
private:
// int _age;
//};
class my_Stack
{
public:
my_Stack(int n = 4)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc");
exit(1);
}
_arr = tmp;
_size = 0;
_capacity = n;
}
//......
~my_Stack()
{
free(_arr);
_arr = nullptr;
_size = _capacity = 0;
cout << "清理my-stack" << endl;
}
private:
int* _arr;
int _size;
int _capacity;
};
class Data
{
public:
Data(int year = 0, 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;
my_Stack sl;
};
int main()
{
Data dl;
return 0;
}
运行上述代码我们发现:
3. 拷贝构造函数
引入:我们是否可以创建一个与已存在对象一摸一样的的对象
3.1特性
a.拷贝构造函数是构造函数的一个重载形式 b.拷贝构造函数的参数只能有一个,且必须是类对象的引用(如果是传值,那么将实参赋值给形参的过程又是个拷贝构造,将会诱发无穷无尽的递归)
#include<iostream>
using namespace std;
class Data
{
public:
Data(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Data(const Data& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
void print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Data dl(2024, 10, 21);
dl.print();
Data d2(dl);//等价于 d2 = d1;
d2.print();
return 0;
}
为什么要用const修饰呢?因为我们为了防止复制时改变源数据。
4.赋值运算符重载
引入:c++中的操作符只能作用于于内置类型,那么我们能不能将操作符也作用于自定义类型呢?
答案是肯定的,我们引入关键字operator。
函数原型为: 返回值类型 operator操作符(参数列表)
4.1性质
a.不能通过连接其他非操作符的符号来创建。如operator@;b.参数列表必须有一个类类型参数;
c.内置类型的运算符不能改变其含义,如+不能定义为-的含义;d.作为类成员函数时,其形参比操作数少1,原因是本身有个this指针; e. (.*),(::),(sizeof),(?:),(.)不能重载。
值得注意的是,当我们将operator引入全局时,以现在的知识储备(后续的友元可以解决),无法访问类中的private成员变量,因此我们直接将重载定义在类中。
class Data
{
public:
Data(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Data(const Data& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
bool operator==(const Data& d)
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
void print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
4.2赋值运算符重载
1.赋值运算符重载的格式
a.参数类型const 类型&,用引用提高效率;b:返回值是类&,提高返回效率,返回的目的是使支持连续赋值;c.检查是不是给自己赋值,可以减少开销;d.返回*this
Data& operator=(const Data& d)
{
if (this != &d)
{
_year = d._day;
_month = d._month;
_day = d._day;
}
return *this;
}//赋值运算符重载
5.上诉4种默认函数的总结和问题抛出
5.1针对构造函数和析构函数:
一般的类我们都要写一个全缺省构造函数完成对内置类型的初始化,因为默认的构造函数对内置类型初始化并不做处理,对自定义类型调用它的构造函数。
对析构函数而言,当我们在堆上手动开了空间时,例如malloc,calloc等,都需要我们手动写析构函数,完成free和置空。因为默认的析构函数对内置类型并不做处理,对自定义类型调用它的析构函数。
5.2针对拷贝构造函数和赋值运算符重载:
其实上述的Data类型,我们不写拷贝构造函数和赋值运算符重载也能使用编译器自动生成的完成处理。因为编译器默认生成的拷贝构造函数和赋值运算符重载,对内置类型是直接按照字节方式完成对对象成员的拷贝复制(浅拷贝)。那么既然编译器能实现,我们为什么还要自己实现呢?
5.3默认浅拷贝的问题
class my_stack
{
public:
my_stack(int n = 10)
{
_arr = (int*)malloc(sizeof(int) * n);
size = 0;
capacity = n;
}
~my_stack()
{
free(_arr);
_arr = nullptr;
size = capacity = 0;
}
private:
int* _arr;
size_t size;
size_t capacity;
};
int main()
{
my_stack sl(20);
my_stack s2(sl);
return 0;
}
上述代码,使用的是默认的拷贝构造函数,但是无法运行。
究其原因就是浅拷贝惹的祸,浅拷贝实现的是对类成员的逐个字节拷贝,_arr指向的是sl的malloc开辟的空间首地址,浅拷贝是直接将sl的_arr直接给s2的_arr,这样就会使不同栈的_arr指向同一块空间,这里还不会发生报错,但是当析构函数处理时,按照栈后进先出的规则,s2会首先调用析构函数free掉_arr,当sl也析构时,就会发生报错,因为不能free掉同一块堆空间。
后续我们会学到深拷贝,就能自己实现拷贝构造和赋值运算符重载解决这种问题。
5.const成员函数
5.1概念:
用cost修饰的成员函数称为const成员函数,实际上const修饰的是隐含的this指针,表明在该成员函数中成员变量不可被修改。
class Date
{
private:
int _year;
int _month;
int _day;
public:
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()//const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
void f1(const Date* d)
{
d->Print();//会报错
}
};
5.2引入
当我们写上上述代码是会报错,原因是指针的权限放大,将一个const Date* 的指针传给了Date*,因此会报错,如何修改呢?在print中this指针是隐藏指针,不能在参数列表中定义,c++中提供了方法void Print()const.
可见const对象不能调用非const成员函数。同理得非const对象能调用const成员函数,因为属于权限的缩小。成员函数的相互调用也同样,const成员函数不能调用非const成员函数,非const成员函数可以调用const成员函数。
附图
当成员函数中不修改成员变量时,理论上都应该const修饰
6.取地址和const取地址操作符
这两个默认成员函数一般不用重新定义,编译器会默认生成
Date* operator&()
{
return this;
}
const Date* operator&()const
{
return this;
}
他们分别构成重载,分别获取的是非const对象的地址,和const对象的地址
如果想要让别人获取到我们想要他获取的信息时,就可以自己定义
Date* operator&()
{
return nullptr;
}
const Date* operator&()const
{
return nullptr;
}