目录:
1.类的6个默认成员函数
2.构造函数
3.析构函数
4.拷贝构造函数
5.赋值运算符重载
1.类的6个默认成员函数
一个空类中难道真的是什么都没有吗?当然不是,其实编译器会自动生成6个默认成员函数。
默认成员函数:用户没有显示实现,编译器会自动生成的成员函数称为默认成员函数
6个成员函数有:1.构造函数(是初始化一个对象) 2.析构函数(完成对对象的空间的释放) 3.拷贝构造函数(同类对象初始化创建对象,初始化创建至关重要,这是拷贝构造函数的核心定义所在) 4.复制重载是把一个对象的值赋值给另一个对象(这两个对象都是先前已经存在的)。
2.构造函数
2.1构造函数的概念
构造函数是一个特殊的成员函数。 1、名字和类名相同 2、创建类类型对象的时候由编译器自动调用,以保证每一个数据成员都有一个合适的初始值 3、在对象整个生命周期里只调用一次。
2.2构造函数的特性
构造函数是特殊的成员函数,虽然名字叫构造,但是构造函数的主要任务并不是开辟空间,而是初始化对象。
其主要特征如下
1.函数名与类名相同
2.没有返回值
3.支持函数重载
4.对象实例化时自动调用对应的构造函数
5.类中如果没有我们自己手写的构造函数,那么C++编译器就会自动生成一个无参的默认构造函数,一旦用户显示定义,编译器将不再生成
我们先来学习意下如何手写构造函数
这是第一种无参数的构造函数
using namespace std;
class Date
{
public:
Date()//现在我们学习的是如何写构造函数,函数名和类名一样
{
_year = 2005;
_mounth = 11;
_date = 13;
}
/*Date(int year = 2005, int mounth = 11, int date = 13)
{
_year = year;
_mounth = mounth;
_date = date;
}*/
void print()
{
cout << _year << "-" << _mounth << "-" << _date << endl;
}
private:
int _year;
int _date;
int _mounth;
};
int main()
{
Date d1;
d1.print();
return 0;
}
这是第二种全缺省的构造函数
using namespace std;
class Date
{
public:
//Date()//现在我们学习的是如何写构造函数,函数名和类名一样
//{
// _year = 2005;
// _mounth = 11;
// _date = 13;
//}
Date(int year = 2005, int mounth = 11, int date = 13)
{
_year = year;
_mounth = mounth;
_date = date;
}
void print()
{
cout << _year << "-" << _mounth << "-" << _date << endl;
}
private:
int _year;
int _date;
int _mounth;
};
int main()
{
Date d1;
d1.print();
return 0;
}
注意:如果调用无参构造函数或者是全缺省构造函数,对象后面不需要加括号,否则就变成了函数声明,返回值是一个类。
Data d1();//这样的调用方法是不对的
6.关于编译器自动生成的默认构造函数,很多人会有疑问,编译器自动生成的构造函数也不会对内置类型的变量进行初始化,那他这个生成的有什么用呢?(首先补充一点:C++中将类型分为了两类,内置类型和自定义类型。内置类型就是C++里面规定的类型,而自定义类型就是自己定义的,如class,struct)。其实,c++自动生成的默认构造函数会初始化自定义类型(实质上是调用它的默认构造函数)
using namespace std;
class Time
{
public:
Time()//这个是我们写的构造函数
{
cout << "是的,我调用了这个函数" << endl;
_hour = 0;
_minute = 0;
second = 0;
}
private:
int _hour;
int _minute;
int second;
};
class Date
{
public:
//Date()//现在我们学习的是如何写构造函数,函数名和类名一样
//{
// _year = 2005;
// _mounth = 11;
// _date = 13;
//}
/*Date(int year = 2005, int mounth = 11, int date = 13)
{
_year = year;
_mounth = mounth;
_date = date;
}*/
void print()
{
cout << _year << "-" << _mounth << "-" << _date << endl;
}
private:
int _year;
int _date;
int _mounth;
Time today;
};
int main()
{
Date d1;
d1.print();
return 0;
}
#这里的代码可以自己下去运行一下,我在这里就不一一运行了
注意:C++为了弥补系统自己生成的默认构造函数不给内置类型初始化的缺陷,打了一个补丁,即:内置类型成员变量在类中声明时可以给默认值
using namespace std;
class Time
{
public:
Time()//这个是我们写的构造函数
{
cout << "是的,我调用了这个函数" << endl;
_hour = 0;
_minute = 0;
second = 0;
}
private:
int _hour;
int _minute;
int second;
};
class Date
{
public:
//Date()//现在我们学习的是如何写构造函数,函数名和类名一样
//{
// _year = 2005;
// _mounth = 11;
// _date = 13;
//}
/*Date(int year = 2005, int mounth = 11, int date = 13)
{
_year = year;
_mounth = mounth;
_date = date;
}*/
void print()
{
cout << _year << "-" << _mounth << "-" << _date << endl;
}
private:
int _year = 2005;
int _date = 13;
int _mounth = 11;
Time today;
};
int main()
{
Date d1;
d1.print();
return 0;
}
再次注意一下:这里不是初始化,这里只是声明,给的是自定义类型默认的缺省值,就是缺省函数的形参是一样的,给编译器自己生成构造函数时使用。
7.注意:默认构造函数分三种,无参数的构造函数,全缺省的构造函数,以及我们没有写编译器自动生成的构造函数。(但是默认构造函数只能存在一个!!!)
3.析构函数
3.1析构函数的概念
与构造函数的功能相反,但是析构函数并不是完成对对象本身的销毁,局部对象的销毁工作是由编译器自己完成的,而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作
3.2析构函数的特性
1.析构函数的函数名是在类名前面加上~
2.没有参数没有返回值类型
3.一个类只能有一个析构函数。如果用户没有显示定义,系统会自动生成默认的析构函数。注意:析构函数没有参数也没有返回值,所以不能重载!!!
4.在对象的生命周期结束时,c++的编译器会自动调用析构函数
class Date { public: //Date()//现在我们学习的是如何写构造函数,函数名和类名一样 //{ // _year = 2005; // _mounth = 11; // _date = 13; //} Date(int year = 2005, int mounth = 11, int date = 13) { _year = year; _mounth = mounth; _date = date; } ~Date() { cout << "析构函数" << endl; } void print() { cout << _year << "-" << _mounth << "-" << _date << endl; } private: int _year; int _date; int _mounth; /*Time today;*/ }; int main() { Date d1; d1.print(); return 0; }
5.关于编译器自动生成的析构函数,会完成哪些事情呢?答案是:编译器生成的默认狗仔函数,对自定义类型成员调用它的析构函数(因为内置类型成员不需要释放)。
using namespace std; class Time { public: Time()//这个是我们写的构造函数 { cout << "是的,我调用了这个函数" << endl; _hour = 0; _minute = 0; second = 0; } ~Time() { cout << "析构函数" << endl; } private: int _hour; int _minute; int second; }; class Date { public: Date(int year = 2005, int mounth = 11, int date = 13) { _year = year; _mounth = mounth; _date = date; } /*~Date() { cout << "析构函数" << endl; }*/ void print() { cout << _year << "-" << _mounth << "-" << _date << endl; } private: int _year; int _date; int _mounth; Time today; }; int main() { Date d1; d1.print(); return 0; }
#在程序运行下我们可以看到,为什么没有创建TIme类型的对象,但是编译器还是调用了TIme的析构函数呢?其实是因为我们实例化的d1里面包含了四个成员变量,三个内置类型和一个自定义类型成员变量。在创建变量时编译器会自动调用它的构造函数,在销毁的时候,同样会调用它的析构函数。对于内置类型的成员,销毁时不需要资源清理。
注意:1.如果类中没有向内存申请资源,析构函数可以不写,因为本身也没啥需要释放的东西 2.在一个类销毁的时候,他会先保证自己的自定义类型的成员变量先销毁,所以会先调用自定义成员变量的析构函数
4.拷贝构造函数
4.1拷贝构造函数的概念
拷贝构造函数:只有一个形参,这个参数是对本类类型对象的引用(而且一般会用const修饰),再用已存在的类类型对象创建新对象时,由编译器自动调用
4.2拷贝构造函数的特性
1.是构造函数的一个重载形式
2.拷贝构造函数的参数只有一个(是对类类型对象的引用,但使用传值的方式的话编译器会直接报错,因为会引发无穷递归调用)
问题:为什么函数的参数用传值接收的话就会报错呢?是因为C++规定了 1.在调用函数的时候先传参数 2.如果实参是内置类型的话直接拷贝,如果是自定义类型的话调用拷贝构造函数来拷贝
现在我们来写一个拷贝构造函数
Date d1; Date d2(d1); Date(const Date d1) { _year = d1._year; _mounth = d1._mounth; _date = d1._date; }
如果我们像这样接收的话,那就是值传递,而且还是自定义类型的值传递,在运行第二行代码的时候呢,我们本意是想调用这个拷贝构造函数,但是在调用过程中,我们要去传参,传参就又涉及了拷贝构造,现在我们就回到了最初的状态,调用拷贝构造,传参,传承那就是调用拷贝构造......这样就构成了死循环。所以我们只能使用引用传递
Date(const Date& d1) { _year = d1._year; _mounth = d1._mounth; _date = d1._date; }
3.如果我们没有显示定义拷贝构造函数的话,编译器就会默认生成一个拷贝构造函数。但是默认的拷贝构造函数按内存存储按字节序完成拷贝(这种拷贝方式叫做浅拷贝),这个拷贝构造函数对内置类型的成员变量进行浅拷贝,对自定义类型则是调用它的拷贝构造函数。
补充一点:浅拷贝和深拷贝的区别。浅拷贝就像是引用,也就是别名,在对新对象操作的时候原对象也会受影响。深拷贝就是弄出来个独立的个体,新旧对象互不干扰。
class Time { public: Time()//这个是我们写的构造函数 { cout << "是的,我调用了这个函数" << endl; _hour = 0; _minute = 0; _second = 0; } ~Time() { cout << "析构函数" << endl; } Time(const Time& d1) { _hour = d1._hour; _minute = d1._minute; _second = d1._second; cout << "拷贝构造函数" << endl; } private: int _hour; int _minute; int _second; }; class Date { public: Date(int year = 2005, int mounth = 11, int date = 13) { _year = year; _mounth = mounth; _date = date; } /* Date(const Date& d1) { _year = d1._year; _mounth = d1._mounth; _date = d1._date; }*/ ~Date() { cout << "析构函数" << endl; } void print() { cout << _year << "-" << _mounth << "-" << _date << endl; } private: int _year; int _date; int _mounth; Time today; }; int main() { Date d1; Date d2(d1); d1.print(); d2.print(); return 0; }
4.编译器自动生成的拷贝构造函数看似已经可以应对很多场景了,那么我们还需要显示实现吗?先看下面的这一串代码
#pragma once typedef int DataType; #include<iostream> #include<cstring> using namespace std; class Stack { public: Stack(int capacity = 4) { _capacity = capacity; _a = (DataType*)malloc(sizeof(DataType) * _capacity); if (_a == nullptr) { perror("malloc"); return; } _size = 0; } ~Stack() { free(_a); _a = nullptr; _size = 0; _capacity = 0; } /*Stack(const Stack& st1) { _capacity = st1._capacity; _size = st1._size; int sz = sizeof(DataType)* st1._capacity; _a = (DataType*)malloc(sz); memcpy(_a,st1._a,sizeof(DataType)*st1._size); }*/ void Push(DataType x) { if (_size == _capacity) { _a = (DataType*)realloc(_a,sizeof(DataType)*_capacity*2); if (_a == nullptr) { perror("realloc"); return; } _capacity *= 2; } _a[_size] = x; _size++; } //private: int _size; DataType* _a; int _capacity; };
你自己去运行一下就能发现,编译器报错了。为什么会报错呢?这就涉及到了之前的浅拷贝和深拷贝问题,我认为浅拷贝就是傻瓜式的复制,所以st1中的_a和st2中的_a指向了同一块空间,在两个自定义变量的生命周期结束的时候,编译器会自动调用析构函数,这个时候_a指向的空间被连续的释放了两次,所以报错了。
5.拷贝构造函数的使用场景
1.使用已存在的对象创建新的对象
2.只有一个参数,并且参数类型是类类型对象的引用
5.赋值运算符重载
5.1运算符重载
C++为了增强代码的可读性,引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有返回值类型,以及参数列表。
函数的名字为:operator后面接需要重载的运算符符号
注意:
# 不能创建新的操作符,如operator@
# 赋值运算符重载函数的参数必须有一个类类型参数(不能全是内置类型,否则运算符重载就没有意义了)
# 对于内置类型的操作符,不能改变其含义,比如+,你就不能改变含义
# 作为类成员函数重载时(当写在全局的时候,就不会出现这样的情况),其形参看起来会比操作数数目少1,因为成员函数的第一个参数是this
# .* :: sizeof ?: .以上5个运算符不能重载(经常在笔试中出现)
# 运算符有几个操作数就有几个参数
//全局的运算符重载函数 using namespace std; bool operator==(const Date& d1,const Date& d2) { if ((d1._year == d2._year) && (d1._mounth == d2._mounth) && (d1._date == d2._date))\ return true; else return false } class Date { public: Date(int year = 2005, int mounth = 11, int date = 13) { _year = year; _mounth = mounth; _date = date; } /* Date(const Date& d1) { _year = d1._year; _mounth = d1._mounth; _date = d1._date; }*/ ~Date() { cout << "析构函数" << endl; } void print() { cout << _year << "-" << _mounth << "-" << _date << endl; } private: int _year; int _date; int _mounth; //Time today; }; int main() { Date d1; Date d2(d1); d1.print(); d2.print(); return 0; }
我们会发现,全局的运算符重载函数无法访问私有的成员变量。解决方法有两个:1.使用友元,但是这个不推荐使用 2.把运算符重载写在类的里面
using namespace std; class Date { public: Date(int year = 2005, int mounth = 11, int date = 13) { _year = year; _mounth = mounth; _date = date; } bool operator==( const Date& d1) { if ((d1._year == _year) && (d1._mounth == _mounth) && (d1._date == _date)) return true; else return false; } /* Date(const Date& d1) { _year = d1._year; _mounth = d1._mounth; _date = d1._date; }*/ ~Date() { cout << "析构函数" << endl; } void print() { cout << _year << "-" << _mounth << "-" << _date << endl; } private: int _year; int _date; int _mounth; //Time today; }; int main() { Date d1; Date d2(d1); d1.print(); d2.print(); bool a = d1 == d2; printf("%d \n", a); return 0; }
5.2赋值运算符重载
1.赋值运算符重载的格式
# 参数类型是const T&,引用传递可以提高效率(因为不会额外开辟空间)
# 返回值类型:T&,因为要涉到连等问题
# 检查是否能自己给自己赋值(检查的时候建议使用空间地址来进行检查,否则的话又涉及函数重载的问题)
# 返回*this
using namespace std; class Date { public: Date(int year = 2005, int mounth = 11, int date = 13) { _year = year; _mounth = mounth; _date = date; } bool operator==( const Date& d1) { if ((d1._year == _year) && (d1._mounth == _mounth) && (d1._date == _date)) return true; else return false; } ~Date() { cout << "析构函数" << endl; } void print() { cout << _year << "-" << _mounth << "-" << _date << endl; } Date& operator=(const Date& d1) { if (this != &d1) { _year = d1._year; _mounth = d1._mounth; _date = d1._date; } return *this; } private: int _year; int _date; int _mounth; //Time today; }; int main() { Date d1(2025,5,19); Date d2; d1.print(); d2.print(); d2 = d1; d2.print(); return 0; }
2.赋值运算符不能重载成全局函数,因为如果类里面没有赋值运算符(注意,是赋值运算符=)那么会自动生成一个赋值运算符,如果你在全局写了,那么编译器就不知道要调用哪一个了(不只是赋值运算符, 构造函数,拷贝构造函数,析构函数都不能写在全局)
3.编译器自动生成的赋值运算重载,会进行浅拷贝 1.如果是内置类型就逐个字节进行拷贝 2.如果是自定义的类型,就会调用对应的赋值运算符
既然我们已经学过拷贝构造,那就不多说了,直接上结论,如果没有涉及资源管理,用不用赋值运算符都可以;一旦涉及资源管理,就必须自己去实现