类中有六个默认成员函数,即使一个成员都没有,空类中也不是空白的,任何一个类在我们不写的情况下都会默认生成6个成员函数
下面一一介绍
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有一个合适的初始值,并且在对象的声明周期中只调用一次
1.函数名与类名相同
2.无返回值
3.对象实例化时编译器自动调用对应的构造函数
4.构造函数可以重载
举例
class Data
{
public:
//1.无参构造函数
Data()
{}
//2.带参的构造寒素
Data (int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
void Test()
{
Data d1;//调用无参构造函数,不用+”()“
Data d2(2022, 3, 28);//调用带参的构造函数
Data d3();
}
class Data
{
public:
//带参的构造函数,如果用户显示定义构造函数,编译器将不再生成
Data (int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
void Test()
{
//没有定义构造函数,对象也可以创建成功,因此此处调用的是编译器生成的默认构造函数
Data d;//因为这里是无参的
}
(无参构造函数,全缺省构造函数,我们没有写编译器默认生成的构造函数,都可以认为是默认成员函数)
在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 Data
{
private:
//基本类型
int _year;
int _month;
int _day;
//自定义类型
Time _t;
};
void Test()
{
Data d;
return 0;
}
例如上面的例子中,编译器就会生成默认的构造函数对自定义类型成员_t
调用它的默认成员函数
我们一般建议将类的成员函数定义的名字加一些前缀和后缀
举例
class Data
{
public:
Data(int year)
{
//无法区分这里的year是成员变量,还是函数形参
year = year;
}
private:
int year;
};
所以我们通常建议这样定义
class Data
{
public:
Data(int year)
{
//这样就容易区分了
_year = year;
}
private:
int _year;
}
需要注意的是,构造函数名字虽然叫构造,但是构造函数的主要任务不是开空间创建对象,而是初始化对象
举例
class Data
{
public:
//这里不能倍称为初始化,只能叫赋值
Data (int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
构造函数体内的语句只能称作为赋初值,不能称作初始化,主要区别为:初始化只能初始化一次,赋值可以多次,所以通常的成员变量初始化,是由初始化列表实现的
初始化列表是以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个”成员变量“后面跟一个放在括号中的初始值或表达式
举例
class Data
{
public:
//带参的构造函数,如果用户显示定义构造函数,编译器将不再生成
Data (int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
1.每个成员变量在初始化列表中只能出现一次,因为初始化只能初始化一次
2.当类中包含以下成员时,必须放在初始化列表位置进行初始化
1.引用成员变量
2.const
成员变量
3.自定义类型成员
举例
class A
{
public:
A(int a)
private:
int _a;
};
class B
{
public:
B(int a, int ref)
{
: _self(a);
, _ref(ref)
, _n(10)
{}
private:
A _self;//自定义类型成员
int& _ref;//引用成员变量
const int _n;//const成员变量
};
3.尽量使用初始化列表初始化,对于自定义类型成员函数,一定会先使用初始化列表初始化,即使我们不写,编译器也会给,只不过时随机值
4.成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
举例
大家可以思考一下面这段代码的结果
A.输出1 1
B.程序崩溃
C.编译不通过
D.输出1 随机值
#include
using namespace std;
class A
{
public:
A(int a)
:_a1(a)
, _a2(_a1)
{}
void Print()
{
cout << _a1 << " " << _a2 << endl;
}
private:
//是按照这里的顺序初始化的,这个例子中,先初始化_a2,再初始化_a1,但是_a2是由_a1的值来初始化的
int _a2;
int _a1;
};
int main()
{
A aa(1);
aa.Print();
return 0;
}
- 数据成员在类中定义的顺序就是参数列表中的初始化顺序;
- 初始化列表仅用于初始化数据成员,并不指定这些数据成员的初始化顺序;
- 每个成员在初始化列表中只能出现一次;
- 尽量避免使用成员初始化成员,成员初始化顺序最好和成员的定义顺序保持一致。
构造函数不仅可以构造和初始化对象,对于单个参数的构造函数,还具有类型转换的作用,但是通常会造成代码的可读性不好,使用explicit
修饰构造函数,将会禁止单参构造函数的隐式转换
class Data
{
public:
Data (int year)
: _year(year)
{}
explicit Data (int year)
: _year(year)
{}
private:
int _year;
int _month;
int _day;
};
void Test
{
Data d1(2018);
d1 = 2019;
}
析构函数是特殊的成员函数,与构造函数功能相反,在对象被销毁时,由编译器自动调用,完成类的一些资源清理工作。
~
C++
编译系统会自动调用析构函数class String
{
public:
String(const char* str = "jack")
{
_str = (char*)malloc(strlen(str) + 1);
strcpy(_str, str);
}
~String()
{
cout << "~String()" << endl;
free(_str);
}
private:
char* _str;
};
class Person
{
private:
String _name;
int _age;
};
int main()
{
Person p;
return 0;
}
拷贝构造函数是用已存在的类类型对象来创建新的对象,只有单个形参,且该形参必须是本类类型对象的引用,一般用const
修饰
class Data
{
public:
Data (int year = 2022, int month = 03, int day = 30)
{
_year = year;
_month = month;
_day = day;
}
//格式要非常注意
Data(const Data& d)
{
_year = d.year;
_month = d.month;
_day = d.day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Data d1;
Data d2(d1);
return 0;
}
如果没有显示定义拷贝构造函数的话,系统会生成默认的拷贝构造函数,默认的拷贝构造函数按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝
举例
class Data
{
public:
Data (int year = 2022, int month = 03, int day = 30)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Data d1;
Data d2(d1);//这里就调用了默认拷贝构造函数
return 0;
}
❓我们刚刚都提到了,如果一个类没有显示实现拷贝构造函数,则编译器会生成一份默认构造函数,既然编译器会给我们生成默认拷贝构造函数,那我们还有必要自己写嘛?
我们要知道默认拷贝构造函数的拷贝方式:将一个对象原封不动
的拷贝到新对象中
例子
class String
{
public:
String(const char* str = "jack")
{
_str = (char *)malloc(strlen(str) + 1);
strcpy(_str, str);
}
//多次调用析构函数
~String()
{
free(_str);
_str = nullptr;
}
private:
char* _str;
};
int main()
{
String s1("hello");
String s2(s1);
system("pause");
return 0;
}
默认拷贝构造函数执行完成之后,s1
,s2
在底层公用的是同一份堆空间
多个对象共同使用同一份资源,在这些对象被销毁时,同一份资源会被释放多次,引起崩溃
编译器生成的默认拷贝构造函数,是按照浅拷贝方式实现的
浅拷贝就是将一个对象中的内容原封不动的拷贝到另一个对象中
后果是多个对象共享同一份资源,最终在对象销毁时该份资源被释放多次而导致程序崩溃
所以如果类中涉及到资源管理时,该类必须显示提供析构函数,在析构函数中将对象的资源释放掉
如何判断需要自己实现拷贝构造函数,什么时候实不实现无所谓?
如果一个类中如果设计到
资源管理
时,拷贝构造函数是必须要实现的
C++
为了增强代码的可读性引入了运算符重载,运算符重载就是具有特殊函数名的函数,也具有其返回值类型,函数名字和参数列表,其返回值类型与参数列表与普通的函数相似
operator
后面接需要重载的运算符符号operator
操作符(参数列表)operator@
this
,限定为第一个形参.*
/::
/sizeof
/?:
/.
这五个运算符不能重载class Data
{
public:
Data(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
Data(const Data& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
void PrintfData()
{
cout << _year << " " << _month << " " << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Data d1(2022, 1, 12);
Data d2(d1);
Data d3(2022, 1, 13);
//调用赋值运算符重载函数
d1 = d3;
}
如果类没有显示实现赋值运算符重载函数,则编译器会生成一份默认运算符重载函数,完成对象之间的赋值操作
❓但是观察下面的代码有没有问题
class String
{
public:
//构造函数
String(const char* str = "jack")
{
_str = (char *)malloc(strlen(str) + 1);
strcpy(_str, str);
}
//拷贝构造函数
String(const string& s)
{
cout << "拷贝构造函数" << endl;
}
~String()
{
if(_str)
{
free(_str);
_str = nullptr;
}
}
private:
char* _str;
};
int main()
{
String s1("hello world");
String s2("Hello World");
s1 = s2;
system("pause");
return 0;
}
下面时结果的监视窗口
上面的代码存在两个问题
编译器生成的赋值运算符重载是按照浅拷贝方式实现的,类中涉及到资源管理时,会造成以下两个后果
1.浅拷贝:一份内存资源释放多次,引起代码崩溃
2.s1
被赋值后,地址和s2
一样,s1
的内存丢失了,造成内存泄露
类中涉及资源管理时,赋值运算符重载必须显示写出来
赋值运算符重载与函数重载没有任何关系
- 函数重载:在相同作用域,函数名字相同,参数列表不同(个数,类型,类型次序),与返回值类型没有关系
- 运算符重载:为了提高代码的可读性
举例赋值运算符重载
class Data
{
public:
Data(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
Data(const Data& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
Data& operator=(const Data& d)
{
//判断有没有自己给自己赋值
if(this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
注意
- 参数类型
- 返回值
- 检测是否自己给自己赋值
- 返回
*this
- 一个类如果没有显示定义赋值运算符重载,编译器也会生成一个,完成对象按字节的值拷贝
将const
修饰的类成员函数称为const
成员函数,实际上const
修饰的时成员函数隐藏的this
指针,表明该成员函数不能对类中的任何成员进行修改
const
对象可以调用非const
成员函数吗?❌- 非
const
对象可以调用const
成员函数吗?⭕const
成员函数内可以调用其他的非const
成员函数吗?❌- 非
const
成员函数内可以调用其他的const
成员函数吗?⭕
1.如果在成员函数中不需要修改成员变量,最好将该函数修饰成const
类型
2.如果需要修改当前对象中的成员变量时,该函数不能用const
修饰
这两个默认成员函数一般不用重新定义,编译器会自动生成
举例
class Data
{
public:
data* operator&()
{
return this;
}
const Data* operator&()const
{
return this;
}
private:
int _year;
int _month;
int _day;
};