文章目录

类中有六个默认成员函数,即使一个成员都没有,空类中也不是空白的,任何一个类在我们不写的情况下都会默认生成6个成员函数
下面一一介绍
一、构造函数
1.概念
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有一个合适的初始值,并且在对象的声明周期中只调用一次
2.特征
-
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();
}
- 5.如果类中没有显示的定义构造函数, 则C++编译器会自动生成一个无参的默认构造函数,一旦用户显示定义编译器将不再生成
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;//因为这里是无参的
}
- 6.无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个
编译器生成的默认成员函数的意义
在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
调用它的默认成员函数
3.成员变量的命名风格
我们一般建议将类的成员函数定义的名字加一些前缀和后缀
举例
class Data
{
public:
Data(int year)
{
//无法区分这里的year是成员变量,还是函数形参
year = year;
}
private:
int year;
};
所以我们通常建议这样定义
class Data
{
public:
Data(int year)
{
//这样就容易区分了
_year = year;
}
private:
int _year;
}
4.构造函数体赋值
需要注意的是,构造函数名字虽然叫构造,但是构造函数的主要任务不是开空间创建对象,而是初始化对象
举例
class Data
{
public:
//这里不能倍称为初始化,只能叫赋值
Data (int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
5.初始化列表
5.1 格式
初始化列表是以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个”成员变量“后面跟一个放在括号中的初始值或表达式
举例
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;
};
5.2 注意事项
-
1.每个成员变量在初始化列表中只能出现一次,因为初始化只能初始化一次
-
2.当类中包含以下成员时,必须放在初始化列表位置进行初始化
举例
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.成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
举例
大家可以思考一下面这段代码的结果
#include <iostream>
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;
}
自测结果如下
5.3初始化顺序
6.explicit关键字
构造函数不仅可以构造和初始化对象,对于单个参数的构造函数,还具有类型转换的作用,但是通常会造成代码的可读性不好,使用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;
}
二、析构函数
1.概念
析构函数是特殊的成员函数,与构造函数功能相反,在对象被销毁时,由编译器自动调用,完成类的一些资源清理工作。
2.特征
- 1.析构函数名是在类名前加上
~
- 2.析构函数没有参数也没有返回值
- 3.一个类只能有一个析构函数,且不能重载,如果没有显示定义析构函数,系统会自动生成默认的析构函数
- 4.对象生命周期结束时,
C++
编译系统会自动调用析构函数 - 5.编译器生成的默认析构函数,对自定义类型成员调用他的析构函数
举例
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;
}
三、拷贝构造函数
1.概念
拷贝构造函数是用已存在的类类型对象来创建新的对象,只有单个形参,且该形参必须是本类类型对象的引用,一般用const
修饰
2.特征
- 1.拷贝构造函数时特殊的成员函数
- 2.拷贝构造函数是构造函数的一个重载格式
- 3.拷贝构造函数的参数只有一个且必须是引用传参,用传值的方式会引发无穷递归调用
举例
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;
}
3.为什么必须是引用传参
4.浅拷贝
如果没有显示定义拷贝构造函数的话,系统会生成默认的拷贝构造函数,默认的拷贝构造函数按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝
举例
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;
}
5.自己实现的意义
❓我们刚刚都提到了,如果一个类没有显示实现拷贝构造函数,则编译器会生成一份默认构造函数,既然编译器会给我们生成默认拷贝构造函数,那我们还有必要自己写嘛?
我们要知道默认拷贝构造函数的拷贝方式:将一个对象原封不动
的拷贝到新对象中
例子
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
在底层公用的是同一份堆空间
浅拷贝后果
多个对象共同使用同一份资源,在这些对象被销毁时,同一份资源会被释放多次,引起崩溃
💙所以如果类中涉及到资源管理时,该类必须显示提供析构函数,在析构函数中将对象的资源释放掉
如何判断需要自己实现拷贝构造函数,什么时候实不实现无所谓?
四、赋值运算符重载
1.概念
C++
为了增强代码的可读性引入了运算符重载,运算符重载就是具有特殊函数名的函数,也具有其返回值类型,函数名字和参数列表,其返回值类型与参数列表与普通的函数相似
2.特征
- 1.函数名字:关键字
operator
后面接需要重载的运算符符号 - 2.函数原型:返回值类型
operator
操作符(参数列表) - 3.不能通过连接其他符号来创建新的操作符,例如
operator@
- 4.重载操作符必须有一个类类型或者枚举类型的操作数
- 5.用于内置类型的操作符,其含义不能改变
- 6.作为类成员的重载函数时,其形参看起来比操作数数目少1, 成员函数的操作符有一个默认的形参
this
,限定为第一个形参 - 7.
.*
/::
/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;
}
下面时结果的监视窗口
上面的代码存在两个问题
编译器生成的赋值运算符重载是按照浅拷贝方式实现的,类中涉及到资源管理时,会造成以下两个后果
💙类中涉及资源管理时,赋值运算符重载必须显示写出来
3.赋值运算符重载
赋值运算符重载与函数重载没有任何关系
举例赋值运算符重载
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;
};
注意
五、const成员
1.const修饰的类的成员函数
将const
修饰的类成员函数称为const
成员函数,实际上const
修饰的时成员函数隐藏的this
指针,表明该成员函数不能对类中的任何成员进行修改
2.小问题
3.总结
1.如果在成员函数中不需要修改成员变量,最好将该函数修饰成const
类型
2.如果需要修改当前对象中的成员变量时,该函数不能用const
修饰
六、取地址及const取地址操作符重载
这两个默认成员函数一般不用重新定义,编译器会自动生成
举例
class Data
{
public:
data* operator&()
{
return this;
}
const Data* operator&()const
{
return this;
}
private:
int _year;
int _month;
int _day;
};