目录
类的6个默认成员函数
如果一个类中什么成员都没有,简称为空类。
空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数
class Date {};
构造函数
概念
class Date
{
public:
void Init(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 d1;
d1.Init(2022, 7, 5);
d1.Print();
Date d2;
d2.Init(2022, 7, 6);
d2.Print();
return 0;
}
对于上面的Date类,可以通过 Init 公有方法给对象设置日期,但如果每次创建对象时都调用该方法设置信息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢?
这时候就引入构造函数的概念。
构造函数:构造函数是一个特殊的成员函数,名字与类名相同,并且不能有返回值类型,创建类类型对象时由编译器自动调用,在对象整个生命周期内只调用一次。
调用这个函数的目的:保证每个数据成员都有 一个合适的初始值。
public: Date(int year, int month, int day) { _year = year; _month = month; _day = day; }
特性
1.构造函数是可以重载的。
class Date
{
public:
// 1.无参构造函数
Date()
{}
// 2.带参构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
void TestDate()
{
Date d1(2015, 1, 1); // 调用带参的构造函数
Date d2; // 调用无参构造函数
}
这时候上面有一个无参的和一个有参的,如果在添加一个
Date d3(); d3.Print();
这时候d3就不是创建一个新对象,而是一个函数声明,即:编译器认为声明一个函数名为d3,没有参数,返回值类型为Date类型的函数。
d3.Print()编译报错
当调用无参构造函数创建对象时,对象之后的括号必须省略。
2.如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
class Date
{
public:
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
return 0;
}
如果用户没有显示实现任何构造方法,则编译器会给该类生成一个无参的默认构造方法调用。
如果用户显式定义了构造函数,编译器将不再生成无参的构造方法。
3. 关于编译器生成的默认成员函数,很多朋友会有疑惑:不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?d对象调用了编译器生成的默认构造函数,但是d对象_year/_month/_day,依旧是随机值。也就说在这里编译器生成的默认构造函数并没有什么用??
具体看生不生成就要看具体的场景,要看这个代码生成有没有意义。
注意:虽然Date类例子中,编译器生成的无参构造方法意义不大,但并不代表无参构造方法就没有作用。
class Time { public: Time(int hour=0,int minute=0,int second=0) { _hour = 0; _minute = 0; _second = 0; cout<<"Time(int,int,int)"<<endl; } void Print() { cout<<hour<<":"<<minute<<":"<<second<<endl; } private: int _hour; int _minute; int _second; }; class Date { public: void Print() { cout << _year << "-" << _month << "-" << _day << endl; t.Print(); } private: // 基本类型(内置类型) int _year; int _month; int _day; // 自定义类型 Time _t; }; int main() { Date d; d.Print(); return 0; }
在上面分析中:我们日期类中必须要用Time类的构造方法来完成初始化,因为Time类已经显示定义的构造方法。所以编译器要给Date生成构造方法。
所以分析可得:上面编译器给日期类生成构造方法就是有意义的。(如果Time类没有构造方法,则编译器就不会给Date类生成构造方法)。
总结可得:
虽然c++语法规定了,在类中,如果用户没有显示定义任何构造函数,则编译器一定会生成一份无参得构造函数。
但是在具体编译器实现过程中,就会跟语法稍微有所出入,因为编译器可能会考虑程序运行效率问题,如果编译器感觉生成得构造方法没有意义,则不在生成。
4. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。
class Date
{
public:
//无参得构造方法
Date()
{
_year = 1900;
_month = 1;
_day = 1;
}
//全缺省得构造函数
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
void Test()
{
Date d1;
}
析构函数
与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
析构函数的特性:
1. 析构函数名是在类名前加上字符 ~。
2. 无参数无返回值类型。
3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。
下面是一个析构函数的示例:
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 3)
{
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (NULL == _array)
{
printf("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);
}
5 .关于编译器自动生成的析构函数,是否会完成一些事情呢?
注意:像下面Date类一样,如果对象中没有涉及到任何资源管理时,该类的析构函数可以不用给出。
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
~Date()
{
cout << "~Date():" << this << endl;
}
private:
int _year;
int _month;
int _day;
};
6. 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。
拷贝构造函数
只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型
对象创建新对象时由编译器自动调用。现在有一个需求:
如下图代码,我们想要 d2创建好了之后,想要和d1中的日期完全相同,这时候就用到了拷贝构造。
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//拷贝构造
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022, 11, 12);
//需求:d2创建好了之后,想要和d1中的日期完全相同
Date d2(d1);
//Date d2(2022, 11, 12);
return 0;
}
特性
1. 拷贝构造函数是构造函数的一个重载形式。
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(const Date d) // 错误写法:编译报错,会引发无穷递归
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(d1);
return 0;
}
拷贝构造函数调用场景:用已经存在的对象构造新对象时。
为什么传值会引发无穷递归?
因为在进行传值传参时,实参传给形参是把实参的值拷贝一份给形参,而我的实参d1是自定义类型的,需要调用拷贝构造,传值传参是要调用拷贝构造的,但是我如果不想调用拷贝构造呢?就需要引用传参。如果不引用的话就会一直调用拷贝构造,如下图:
解释:就是我们要 把d1传给拷贝构造,就需要调用拷贝构造,这时候就需要把整个Date拷贝一份,这时候就需要传参,传参的话就要继续调用Date,就这样一直传递下去。
3. 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
注意:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。
如下图:
拷贝过程就是逐字节拷贝的,就是将d1中的内容,原封不动的拷贝到d2中,就是值拷贝。
对于上面的Date 类编译器并没有生成拷贝构造函数,但是完成了拷贝构造的工作。
问题:编译器生成的拷贝构造||虽然没有生成但是也可以完成拷贝构造的工作:既然编译器已经可以完成了,那拷贝构造还需要用户自己写吗?
答案:像日期类这种没有涉及到资源管理时,可写可不写,因为编译器就可以完成拷贝的工作,如果需要自己再去实现,注意:编译器是按照值的方式拷贝的-----即:将一个对象中的内容原封不动的拷贝到另一个对象中(浅拷贝)
如果类中涉及到资源管理时,则拷贝构造是必须要实现的。
下面的程序就出现了崩溃,因为它发生了浅拷贝,要用深拷贝去解决
typedef int DataType; struct Stack { public: Stack() { _array = (DataType*)malloc(10 * sizeof(DataType)); if (NULL == _array) { assert(false); return; } _capacity = 3; _size = 0; cout << "Stack():" << this << endl; } void Push(DataType data) { _array[_size] = data; ++_size; } void Pop() { if (Empty()) return; _size--; } DataType Top() { assert(!Empty()); return _array[_size - 1]; } bool Empty() { return 0 == _size; } int Size() { return _size; } //析构函数 ~Stack() { if (_array) { free(_array); _array = NULL; _capacity = 0; _size = 0; } cout << "~Stack():" << this << endl; } private: void _checkCapacity(); private: DataType* _array; size_t _capacity; size_t _size; }; void TestStack() { Stack s1; s1.Push(1); s1.Push(2); s1.Push(3); s1.Push(4); Stack s2(s1); } int main() { TestStack(); return 0; }
为什么出现了程序崩溃?
4.拷贝构造函数典型调用场景
class Date { public: Date(int year, int month, int day) { _year=year; _month=month; _day=day; cout << "Date(int,int,int):" << this << endl; } Date(const Date& d) { _year=d._year; _month=d._month; _day=d._day; cout << "Date(const Date& d):" << this << endl; } ~Date() { cout << "~Date():" << this << endl; } private: int _year; int _month; int _day; };
根据相面代码
void TestDate1()
{
//只要创建对象,就必须调用构造函数
//拷贝构造是:用已经存在的对象构造新对象时调用
//其余创建新对象的场景调用的基本都是构造函数
Date d1(2022,11,15);
//拷贝构造函数调用场景1
Date d2(d1);
}
//拷贝构造函数调用场景2:以值得方式传参
void TestDate2(Date d)
{
Date dd;
}
//拷贝构造函数调用场景3:以值的方式返回对象
void TestDate3(Date d)
{
Date d;
return d;
}
Date TestDate4()
{
return Date(2022,11,5);
}
注意:
1.以值得方式返回时,如果返回的是匿名对象,则编译器不会在用匿名对象拷贝构造临时对象,而是直接将匿名对象返回了。
匿名对象:没有名字的对象
2.如果参数是以值得方式传递,实参如果也是匿名对象,也会少一次拷贝构造
int main()
{
Date md;
TestDate1();
TestDate2(md);
TestDate3();
return 0;
}
class Date { public: Date(int year, int month, 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; }
赋值运算符重载
什么时候调用赋值运算符重载?
请看下面的代码
void TestDate()
{
Date d1(2022,11,15); //拷贝构造
Date d2(d1);
Date d3;
d3=d2; //用已经存在的对象 给另一个已经存在的对象赋值
Date d4=d3; //拷贝构造
}
语法:如果说程序员没有显示定义赋值运算符重载,则编译器会自动生成一份。
实际情况:编译器不一定会生成,但是编译器一定会完成赋值的工作。
当类中涉及到资源管理时,赋值运算符重载也是必须要实现的。
赋值运算符重载方法怎么写?
1.什么是运算符重载
2.赋值运算符该怎么写
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
~Date()
{
cout << "~Date():" << this << endl;
}
//需求:检测两个日期类型对象是否相等
bool IsEqual(const Date& d)
{
return _year == d._year &&
_month == d._month &&
_day == d._day;
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022, 11, 15);
Date d2(d1);
if (d1.IsEqual(d2))
{
cout << "d1==d2" << endl;
}
else
{
cout << "d1 != d2" << endl;
}
//注意:自定义类型不支持“==”的运算符
// 因为编译器不知道该怎么比较
// 如果一定要使用“==”来比较自定义类型对象
// 必须告诉编译器比较的规则
if (d1==d2)
{
cout << "d1==d2" << endl;
}
else
{
cout << "d1 != d2" << endl;
}
return 0;
}
所以引入了赋值运算符重载。
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
// bool operator==(Date* this, const Date& d2) // 这里需要注意的是,左操作数是this,指向调用函数的对象 bool operator==(const Date& d) { return _year == d._year && _month == d._month && _day == d._day; }
if (d1==d2) //if(d1.operator==(d2)) //现在这两个代码就是等价的 { cout << "d1==d2" << endl; } else { cout << "d1 != d2" << endl; }
在底层可以看见会调用上面的运算符重载。
其它类型的运算符重载
//注意:1.operator之后跟的一定是c++语言支持的 运算符 //不能凭空自己臆造运算符 bool operator<(const Date& left, const Date& right) { return left._day < right._day; } //2.重载操作符必须有一个类类型参数 Date& operator+=(Date& d, int days) { d._day += days; return d; } //重载的运算符必须要复合其含义 //d2=d1 可以的 //d3=d2=d1 必须支持连续赋值 必须是d1给d2赋值 然后d2给d3赋值 所以要返回的this //d2=d1---> d2.operator(d1) // d2传给this d1传给d Date& operator=(const Date& d) { _year=d._year; _month=d._month; _day=d._day; return *this; } Date& operator=(const Date& d) { //检测是否为自己给自己赋值 if(this!=&d) { _year=d._year; _month=d._month; _day=d._day; return *this; } } //前置++ Date& operator++() { _day+=1; return *this; } //后置++ //语法中:为了区分前置和后置++ //规定:给后置++多添加一个int类型的参数,目的是为了让前置++和后置++形成函数重载 //后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将this保存一份, //然后给this+1 // 而temp是临时对象,因此只能以值的方式返回,不能返回引用 Date& operator++(int) { Date temp(*this); _day+=1; return temp; } int main() { d1<d2 d3=d2=d1; d3=d3; //自己给自己赋值 d2=++d1; d2=d1++; return 0; }
注意:
1.不能通过连接其他符号来创建新的操作符:比如operator@
2.重载操作符必须有一个类类型参数
3.用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
4.作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
5 .* :: sizeof ?: . 注意以上5个运算符不能重载。
总结
1. 赋值运算符重载格式
参数类型:const T&,传递引用可以提高传参效率
返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
检测是否自己给自己赋值
返回*this :要复合连续赋值的含义2. 赋值运算符只能重载成类的成员函数不能重载成全局函数
原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。
3. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。
4.注意:如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现。跟拷贝构造一样,如果只是浅拷贝就会造成内存泄露。
const成员
const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
class Date { public: Date(int year=1900,int month=1,int day=1) { _year=year; _month=month; _day=day; } //this的类型:Date*const-->this的指向不能修改,this指向空间中的内容可以修改 //可写入:可以修改成员变量 void show() { cout<<_year<<"/"<<_month<<"/"<<_day<<endl; } //const成员函数:被const修饰的成员函数 // 特性:不能修改"成员变量" //const修饰成员函数,实际是在修饰this指针 //this的类型:const Date*const //只读:只能读取this中的成员,不嫩修改 void Print()const { cout<<_year<<"/"<<_month<<"/"<<_day<<endl; } private: int _year; int _month; int _day; }; int main() { //普通对象即可以调用普通成员函数,也可以调用const成员函数 //d1对象:是可读可写对象 Date d1(2023.9.13); d1.show(); d1.Print(); //const对象:只能调用const成员函数 //d2对象:只读的对象,成员函数只能读取该对象中的内容,不能修改 const Date d2(d1); d2.Print(); // d2.show(); //编译报错:d2是一个只读的对象,不允许修改该对象的内容, //但是如果允许该对象调用普通的成员函数,在该成员函数中完全可能会修改const对象中的内容,代码不安全。 return 0; }
1. const对象可以调用非const成员函数吗?
不可以
2. 非const对象可以调用const成员函数吗?可以
void func1() {} void func2()const {} //非const成员函数内可以调用其它的const成员函数吗? 可以 //show(): 普通方法,该方法内部可以修改也可以不修改成员变量 //this的类型:Date*const 即:当前对象可以修改也可以不需要 void show() { func1();//可以调用 func2(); _day+=1; cout<<_year<<"/"<<_month<<"/"<<_day<<endl; } //const成员函数内可以调用其它的非const成员函数吗? 不可以 //const成员函数内部只能调用const成员函数 //const成员函数:const本质修改this指针,表明该成员函数内部一定不会修改成员变量 // void Print()const { //func1();编译失败 func2(); cout<<_year<<"/"<<_month<<"/"<<_day<<endl; }
3. const成员函数内可以调用其它的非const成员函数吗?不可以
4. 非const成员函数内可以调用其它的const成员函数吗?
可以
问题:
const成员函数中,是不能对任何成员变量进行修改的,但是如果一定需要对某个成员变量修改呢?
class Date { //如果在const成员函数中,一定要修改某个成员变量时 //在定义该成员变量的时候,使用mutable关键字修改该成员即可 void func3()const { //_month+=1; //这个就会报错。 _day+=1; } private: int _year; int _month; mutable int _day; };
取地址及const取地址操作符重载
int main()
{
Date d1(2022, 11, 15);需求:在对对象取地址的同时需要将对象的地址打印出来
Date*p = &d1;}
这时候就需要
Date* operator&() { cout << this << endl; return this; }
如果是const类型的话
int main() { Date d1(2022, 11, 15); Date*p = &d1; const Date d2(d1); const Date*p2 = &d2; return 0; }
这时候const的对象就不能调用上面普通的方法来进行取地址。
然后引入下面的重载,但是却不能调用,这是为什么?
const Date* operator&(int)const { cout << this << endl; return this; }
解释:
注意:所以在参数列表中不能加任何参数,否则编译器会将&当成按位&来处理
正确的写法:
//this的类型:const Date*const
const Date* operator&()const
{
cout << this << endl;
return this;
}