类中有六个默认成员函数,即使一个成员都没有,空类中也不是空白的,任何一个类在我们不写的情况下都会默认生成6个成员函数 下面一一介绍
一、构造函数
1.概念
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有一个合适的初始值,并且在对象的声明周期中只调用一次
2.特征
-
1.函数名与类名相同 -
2.无返回值 -
3.对象实例化时编译器自动调用对应的构造函数 -
4.构造函数可以重载
举例
class Data
{
public:
Data()
{}
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;
}
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.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;
};
举例 大家可以思考一下面这段代码的结果
A.输出1 1 B.程序崩溃 C.编译不通过 D.输出1 随机值
#include <iostream>
using namespace std;
class A
{
public:
A(int a)
:_a1(a)
, _a2(_a1)
{}
void Print()
{
cout << _a1 << " " << _a2 << endl;
}
private:
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;
}
下面时结果的监视窗口 上面的代码存在两个问题 编译器生成的赋值运算符重载是按照浅拷贝方式实现的,类中涉及到资源管理时,会造成以下两个后果
1.浅拷贝:一份内存资源释放多次,引起代码崩溃 2.s1 被赋值后,地址和s2 一样,s1 的内存丢失了,造成内存泄露
💙类中涉及资源管理时,赋值运算符重载必须显示写出来
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;
};
注意
- 参数类型
- 返回值
- 检测是否自己给自己赋值
- 返回
*this - 一个类如果没有显示定义赋值运算符重载,编译器也会生成一个,完成对象按字节的值拷贝
五、const成员
1.const修饰的类的成员函数
将const 修饰的类成员函数称为const 成员函数,实际上const 修饰的时成员函数隐藏的this 指针,表明该成员函数不能对类中的任何成员进行修改
2.小问题
const 对象可以调用非const 成员函数吗??- 非
const 对象可以调用const 成员函数吗?? const 成员函数内可以调用其他的非const 成员函数吗??- 非
const 成员函数内可以调用其他的const 成员函数吗??
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;
};
|