文章目录
- 默认成员构造函数
- 1. 构造函数
- 1.1 概念
- 1.2 特性
- 2. 析构函数
- 2.1 概念
- 2.2 特性
- 3. 拷贝构造函数
- 3.1 概念
- 3.2 特性
- 4. 运算符重载
- 4.1 赋值重载
- 4.2 自增自减重载
- 4.3 取地址操作符重载
- 5. const成员函数
- 6. 取地址重载
默认成员构造函数
上一节我们说过,空类的大小是1字节用来占位,那空类是不是真的什么都没有呢?
并不是,C++中的任何一个类都具有6个默认成员函数
即使它是空类,它也拥有这6个默认成员函数,下面我们依次介绍这些默认成员函数~
1. 构造函数
1.1 概念
构造函数是当对象定义时编译器默认调用的,用来完成对对象属性初始化的工作。
我们一开始写的
class Date
是这样class Date { private: int _year; int _month; int _day; public: void Init(int year, int month, int day) { _year = year; _month = month; _day = day; } void Print() { cout << _year << " " << _month << " " << _day << endl; } }; int main() { Date d1,d2; d1.Init(2023, 7, 21); d2.Init(2023, 7, 22); d1.Print(); d2.Print(); }
每定义一个对象都需要调用初始化函数初始化对象的属性,如果我们哪一次忘记了初始化,那么再访问该属性时便会访问随机值,因此有没有一种办法定义对象时直接初始化呢?这样就我们就不会忘记初始化了
我们可以定义构造函数帮我们完成这件事:
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。
1.2 特性
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
特征如下
- 函数名与类名相同
- 无返回值类型
- 对象实例化时编译器自动调用对应的构造函数
- 构造函数可以重载(本质就是我们可以写多个构造函数,提供多种初始化方式)
class Date的构造函数
class Date { private: int _year; int _month; int _day; public: Date() { _year = 1; _month = 1; _day = 1; } void Init(int year, int month, int day) { _year = year; _month = month; _day = day; } void Print() { cout << _year << " " << _month << " " << _day << endl; } }; int main() { Date d1; d1.Print(); }
运行结果:
构造函数在对象创建的时候自动调用(对象是编译器创建的,不是构造函数创建的,构造函数只负责对创建对象的属性进行初始化操作)
带参数的构造函数
//带参数的构造函数 Date(int year, int month, int day) { _year = year; _month = month; _day = day; } int main() { Date d1;//调用无参构造函数 Date d2(2023, 7, 21);//想要调用带参数的构造函数必须将括号写在对象名后面而不是类名后面 d1.Print(); d2.Print(); // 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明 // 以下代码的函数:声明了d3函数,该函数无参,返回一个日期类型的对象 // warning C4930: “Date d3(void)”: 未调用原型函数(是否是有意用变量定义的?) Date d3(); }
**注意:**语法规定:调用带参数的构造函数必须将括号写在对象名后面
将带参构造函数和不带参构造函数合并为全缺省构造函数
//全缺省构造函数 Date(int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; }
运行结果:
若定义了全缺省构造函数,也定义了无参构造函数,调用对象时没有指定构造函数的参数编译器不知道调用哪一个构造函数(全缺省还是无参),因而会报错(报错仅仅是因为编译器不知道调用哪一个而不是语法错误)
class Stack
class Stack { private: int* _a; int _top; int _capacity; public: //带缺省参数的构造函数 Stack(int capacity = 4) { _capacity = capacity; _top = 0; _a = (int*)malloc(sizeof(int) * _capacity); assert(_a); } void Push(int val) { if (_capacity == _top) { _capacity *= 2; int* tmp = (int*)realloc(_a, sizeof(int) * _capacity); if (tmp) _a = tmp; } _a[_top] = val; _top++; } int& Top() { return _a[_top - 1]; } void Pop() { assert(_top > 0); _top--; } }; int main() { Stack st; st.Push(1); st.Push(2); st.Push(3); st.Push(4); st.Push(5); cout << st.Top() << endl; st.Pop(); cout << st.Top() << endl; }
运行结果
:
`通过汇编观察定义对象时调用了构造函数
使用了构造函数可以让我们省去和C语言一样手动初始化栈,并且使用默认参数可以让我们设定初始容量,例如我们事先知道栈中要插入1000个元素,我们就可以直接这么定义栈
Stack st(1000);
这省去了后续扩容的消耗。
-
如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数(后面皆称为系统默认构造函数),一旦用户显式定义编译器将不再生成。
class Date
class Date { public: /* // 如果用户显式定义了构造函数,编译器将不再生成 Date(int year, int month, int day) { _year = year; _month = month; _day = day; } */ void Print() { cout << _year << "-" << _month << "-" << _day << endl; } private: int _year; int _month; int _day; }; int main() { // 将Date类中构造函数屏蔽后,代码可以通过编译,因为编译器生成了一个无参的默认构造函数 // 将Date类中构造函数放开,代码编译失败,因为一旦显式定义任何构造函数,编译器将不再生成 // 无参构造函数,放开后报错:error C2512: “Date”: 没有合适的默认构造函数可用 Date d1; d1.Print(); return 0; }
我们没有定义构造函数,因此定义d1时编译器会调用编译器生成的
默认构造函数
打印结果
-
系统默认构造函数作用:
-
系统默认构造函数对类的内置类型不做处理(语言自带的类型),有些编译器可能会进行处理,但是C++标准并没有这么规定
-
系统默认构造函数会去调用自定义类型变量的构造函数(union,struct,class等)
class Date
class Time { public: Time() { cout << "Time()" << endl; _hour = 0; _minute = 0; _second = 0; } private: int _hour; int _minute; int _second; }; class Date { private: // 基本类型(内置类型) int _year; int _month; int _day; // 自定义类型 Time _t; }; int main() { Date d;//调用Date类的系统默认构造函数 return 0; }
运行结果:<
解释:
Date类定义对象时我们会自动调用Date类的系统默认构造函数,系统默认构造函数对Date类的内置类型不做处理,调用自定义类型Time
默认构造函数,因此_t的_hour,_minute,_second都会被初始化为0.来看一段问题代码
class Time { public: Time(int hour, int minute, int second) { cout << "Time()" << endl; _hour = hour; _minute = minute; _second = second; } private: int _hour; int _minute; int _second; }; class Date { private: // 基本类型(内置类型) int _year; int _month; int _day; // 自定义类型 Time _t; }; int main() { Date d;//调用Date类的系统默认构造函数 return 0; }
运行结果:
**解释:**Date只有系统默认构造函数,定义对象时调用系统默认构造函数,系统默认构造函数调用
Time
类的默认构造函数,但是我们已经定义了Time
带参数的构造函数,因此Time
类不具有默认构造函数,因而程序出错。注意:C++11针对系统默认构造函数不会对内置类型进行处理做了一个补丁,C++11后允许将内置成员变量在类中声明时给定默认值
//C++11给定缺省值 class A { public: void Print() { cout << _a << " " << _c << endl; } private: int _a = 1; char _c = 'a'; }; int main() { A a;//调用系统默认构造函数给_a初值1,_c初值'a' a.Print(); return 0; }
-
调用时可以不传参的构造函数称为默认构造函数,默认构造函数有3类:系统默认构造函数,自定义的无参构造函数,全缺省的构造函数。
//默认构造函数 class A { public: //自定义无参构造函数 A() { cout << "A()\n"; } //全缺省构造函数 A(int a = 1, int b = 2) { _a = a; _b = b; cout << "A(int a, int b)\n"; } private: int _a; int _b; }; int main() { A a;//无法编译通过,因为不知道调用哪一个默认构造函数 return 0; }
-
-
-
2. 析构函数
2.1 概念
析构函数与构造函数的功能相反。**析构函数的作用是在对象销毁前后执行对象中资源清理工作(**对象销毁不是析构函数完成的就像对象的创建不是构造函数完成的一样)
2.2 特性
- 析构函数名是在类名前加上字符
~
。 - 无参数无返回值类型。
- 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构
函数不能重载
-
对象生命周期结束时,C++编译系统系统自动调用析构函数
class Stack
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 3)
{
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (NULL == _array)
{
perror("malloc申请空间失败!!!");
return;
}
_capacity = capacity;
_size = 0;
}
void Push(DataType data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
// 其他方法...
~Stack()
{
if (_array)
{
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
int _capacity;
int _size;
};
void TestStack()
{
Stack s;
s.Push(1);
s.Push(2);
}
有了析构函数就不需要我们自己手动销毁栈了
-
编译器生成的默认析构函数作用
- 对于内置类型不做处理
- 对于自定义类型成员,调用自定义类型的析构函数
class Time { public: Time() { cout << "Time()" << endl; } ~Time() { cout << "~Time()" << endl; } private: int _hour; int _minute; int _second; }; class Date { public: Date() { cout << "Date()" << endl; } ~Date() { cout << "~Date()" << endl; } private: // 基本类型(内置类型) int _year = 1970; int _month = 1; int _day = 1; // 自定义类型 Time _t; }; int main() { Date d; return 0; }
运行结果:
-
如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如
class Date
;有资源申请时,一定要写,否则会造成内存泄漏,比如class Stack
3. 拷贝构造函数
3.1 概念
创建对象时用当前已存在的对象初始化新对象称为拷贝。
拷贝构造函数:只有单个形参,**该形参是对本类类型对象的引用(**一般常用const引用),在用已在的类类型对象创建新对象时由编译器自动调用。
c++规定任何自定义类型的拷贝都会调用拷贝构造函数
calss Stack 和 class Date
void fun1(Date d1)
{
}
void fun2(Stack st1)
{
}
int main()
{
Stack st;
Date d;
fun1(d);//传参时调用Date类型的默认拷贝构造函数
fun2(st);//传参时调用Stack类型的默认拷贝构造函数
return 0;
}
运行结果:
调用Stack
拷贝构造函数时会出现问题,原因我们后面解释。
3.2 特性
-
拷贝构造函数是构造函数的一个重要重载形式
-
拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
class Date { public: Date(int year = 1900, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } // Date(const Date& d) // 正确写法 Date( Date d)//错误写法---拷贝构造函数的参数不能是Date,否则会发生无穷递归 { _year = d._year; _month = d._month; _day = d._day; } private: int _year; int _month; int _day; }; int main() { Date d1; Date d2(d1);//传参时将d1拷贝给d:Date d(d1),此时会调用拷贝构造函数,拷贝构造函数右会将d1拷贝给d:Date d(d1)引发无穷递归 }
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5SrIgELR-1690679807038)(images/image-20230723133609228.png)]
传值拷贝引发的无穷递归:
-
若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
class Time { public: Time() { _hour = 1; _minute = 1; _second = 1; } Time(const Time& t) { _hour = t._hour; _minute = t._minute; _second = t._second; cout << "Time::Time(const Time&)" << endl; } private: int _hour; int _minute; int _second; }; class Date { private: // 基本类型(内置类型) int _year = 1970; int _month = 1; int _day = 1; // 自定义类型 Time _t; }; int main() { Date d1; Date d2(d1); return 0; }
运行结果:
-
默认拷贝构造函数的作用
- 对内置类型实现字节序拷贝
- 对自定义类型会调用自定义类型的拷贝构造函数
- 对内置类型实现字节序拷贝
-
c++拷贝自定义对象时为什么要使用拷贝构造函数,和c语言一样全部按字节序拷贝可以吗?
不可以,例如上述
class Stack
,若使用默认拷贝构造函数,则对于内置类型就是字节序拷贝,这就会出现内存问题,我们来回顾一下上述代码class Stack { public: Stack(size_t capacity = 3) { _array = (DataType*)malloc(sizeof(DataType) * capacity); if (NULL == _array) { perror("malloc申请空间失败!!!"); return; } _capacity = capacity; _size = 0; } void Push(DataType data) { // CheckCapacity(); _array[_size] = data; _size++; } ~Stack() { if (_array) { free(_array); _array = NULL; _capacity = 0; _size = 0; } } private: DataType* _array; int _capacity; int _size; }; void fun1(Date d1) { } void fun2(Stack st1) { } int main() { Stack st; Date d; fun1(d);//传参时调用Date类型的默认拷贝构造函数 fun2(st);//传参时调用Stack类型的默认拷贝构造函数 return 0; }
如果是值拷贝。那么拷贝后st的成员_a和st1的成员_a都指向同一块空间
当st1对象销毁时,会调用析构函数,析构函数会将st1对象_a成员指向的空间释放,此时st对象的_a成员就是野指针了,当st对象销毁时调用析构函数释放野指针指向的空间就会引发内存问题。
因此像
Stack
的类我们就需要自己重定义拷贝构造函数,实现深拷贝
//自定义拷贝构造函数实现深拷贝 Stack(const Stack& st) { _array = (DataType*)malloc(sizeof(DataType) * st._capacity); if (nullptr == _array) { perror("malloc fail"); exit(-1); } memcpy(_array, st._array, sizeof(DataType) * st._size); _capacity = st._capacity; _size = st._size; } int main() { Stack st1; st1.Push(1); st1.Push(2); st1.Push(3); Stack st2(st1);//调用拷贝构造函数完成深拷贝 return 0; }
运行结果:
-
拷贝构造函数调用场景:
- 使用已存在的对象创建新对象
- 函数参数为类类型对象
- 函数返回值为类类型对象
class Date { public: Date(int year, int minute, int day) { cout << "Date(int,int,int):" << this << endl; } Date(const Date& d) { cout << "Date(const Date& d):" << this << endl; } ~Date() { cout << "~Date():" << this << endl; } private: int _year; int _month; int _day; }; Date Test(Date d) { Date temp(d); return temp; } int main() { Date d1(2022, 1, 13);//拷贝构造函数 Test(d1); return 0; }
-
-
4. 运算符重载
C++为了增强代码的可读性新增了运算符重载,运算符重载是具有特殊函数名的函数,也具有返回值类型,函数名字以及参数列表,返回值的类型与普通函数相同。
函数名为:operator操作符
函数原型为:返回值类型 operator操作符(参数列表)
注意:
- 不能通过连接其它符号来创建新的操作符:比如
operator@
- 重载操作符必须有一个参数为自定义类型参数
- 用于内置类型的运算符,其含义不能改变,例如:内置类型的+,不能改变其含义
- 用于类成员函数重载时,其形参看起来总是比操作数目少1,因为成员函数的第一个参数为隐藏的this指针
.* :: sizeof ?: .
这五个操作符不可以重载
//运算符重载
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
//运算符重载为成员函数可以访问类的私有成员
bool operator==(const Date& d2)//实际上(Date* const this, const Date&d2)
{
return _year == d2._year && _month == d2._month && _day == d2._day;
}
private:
int _year;
int _month;
int _day;
};
//bool operator==(const Date& d1, const Date& d2)
//{
// return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day;//由于类外部不能直接访问private成员
// //可以通过友元函数解决,这里先将运算符重载作为成员函数.
//}
int main()
{
Date d1(2023, 7, 23);
Date d2(2023, 7, 21);
cout << d1.operator==(d2);
运行结果:
这种调用和调用普通成员函数的方法一样看不出可读性,因此调用运算符重载时可以像内置类型一样直接使用运算符。
需要注意的是
d1==d2
会被编译器转换为d1.operator(d2)
d2==d1
会被编译器转换为d2.operator(d1)
,即操作符的左操作数是调用运算符重载的对象,也是his指针指向的对象
4.1 赋值重载
a.赋值运算符重载格式
- 参数类型:
const T&
,引用传递可以提高传参效率 - 返回值类型:
T&
,有返回值是为了支持连续赋值,返回引用是为了提高返回的效率。 - 检查是否给自己赋值
- 返回
*this
:要符合连续赋值的含义
class Date
{
public:
Date(int year = 2023, int month = 7, int day = 28)
{
_year = year;
_month = month;
_day = day;
}
Date& operator=(const Date& date)
{
if (this != &date)
{
_year = date._year;
_month = date._month;
_day = date._day;
}
return *this;//出了函数后*this还存在,可以返回引用
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(2023, 7, 20);
d2 = d1;//调用赋值重载,转化为d2.operator=(d1);
d2.Print();
return 0;
}
运行结果:
b.编译器会默认生成赋值重载成员函数
如果我们没有显示定义赋值重载,则编译器会默认生成一个复制重载函数,并且该函数完成字节序拷贝,类似于默认生成的拷贝构造函数。对于class Stack
这种类型,使用默认的赋值重载函数将会出错,需要使用自定义的赋值重载函数和自定义的拷贝构造函数。
c.赋值运算符只能重载为成员函数,不可以重载为全局函数
上面刚说过,每一个类都会有自己的默认赋值重载函数,如果我们将赋值重载写为全局函数,那么该类就会生成一个默认重载函数,调用时不知道调用类的赋值重载还是调用全局的赋值重载。
d.Date d; Date d2 = d
属于拷贝构造函数,不会调用赋值重载
**注意:**如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现。
4.2 自增自减重载
自增自减分为前缀自增、后缀自增、前缀自减、后缀自减。前后缀自增都是单目运算符,那么重载时如何区分重载的对应运算符是前缀还是后缀呢?
C++规定,后缀重载时多增加一个int类型的参数,但调用该函数时不需要显示传递参数,编译器会自动传递
class A
{
public:
A()
{
_a = 1;
}
A operator++()//重载前缀++
{
_a++;
return *this;
}
A& operator++(int)//重载后缀++
{
A tmp(*this);
_a++;
return tmp;
}
void Print()
{
cout << _a << endl;
}
private:
int _a;
};
int main()
{
A a;
a++;//编译器转换为a.operator++()
a.Print();
++a;//编译器转换为a.operator++(0);
a.Print();
}
可以看见,对于自定义类型,前置++不需要调用拷贝构造函数,后置++需要调用2次拷贝构造函数。因此对于自定义类型来说前置++的效率更高。
总结:
- 前缀重载运算符效率高
- 定义后缀重载时参数列表给出一个int类型参数作为标记
4.3 取地址操作符重载
取地址及const取地址操作符重载
5. const成员函数
调用成员函数时实际上会传递this指针,this指针指向的是当前对象,我们知道,this指针在函数参数中不可以显示传递接受,那么如果我们要求当前对象的相关信息不可以被更改怎么办呢?我们需要用const修饰this指针,但是这里的const应该放在哪里呢?我们规定,const修饰成员函数时const应该放在函数参数列表最后面。
class Date
{
public:
Date(int year = 2023, int month = 7, int day = 28)
{
_year = year;
_month = month;
_day = day;
}
Date& operator=(const Date& date)//只能重载为成员函数
{
if (this != &date)
{
_year = date._year;
_month = date._month;
_day = date._day;
}
return *this;//出了函数后*this还存在,可以返回引用
}
void Print()const//const成员函数表明调用的对象信息不可以更改
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d;
d.Print();//权限缩小
const Date d1;
d1.Print();//权限平移
return 0;
}
运行结果:
注意:const成员函数可以和普通成员函数同时存在,调用时优先匹配最合适的
class Date
{
public:
Date(int year = 2023, int month = 7, int day = 28)
{
_year = year;
_month = month;
_day = day;
}
void Print()const //const成员函数表明调用的对象信息不可以更改--参数为const的成员函数:既可以打印const对象,又可以打印非const对象
{
cout << _year << "-" << _month << "-" << _day << endl;
cout << "void Print()const\n";
}
//const成员函数重载
void Print()//参数为没有被const修饰的成员函数:只能打印非const对象
{
cout << _year << "-" << _month << "-" << _day << endl;
cout << "void Print()\n";
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
const Date d2;
d1.Print();//void Print()
d2.Print();//void Print() const
return 0;
}
理论上
void Print() const
可以打印非const对象,但是这里重载了非const的Print函数,因此d1.Print()
时会优先调用最佳匹配的,也就是void Print()
关于const成员函数需要知道的几点
- const对象不可以调用非const成员函数
- 非const对象可以调用const成员函数
- const成员函数内部不可以调用非const成员函数
- 非const成员函数内部可以调用const成员函数
6. 取地址重载
//取地址重载
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//普通取地址重载
Date* operator&()
{
return this;
}
//const取地址重载
const Date* operator&()const
{
return this;
}
private:
int _year;
int _month;
int _day;
};
取地址运算符一般不会重载,使用默认生成的即可,除非你不想让别人获取对象的地址