GIF:
目录
后记:●由于作者水平有限,文章难免存在谬误之处,敬请读者斧正,俚语成篇,恳望指教! ——By 作者:新晓·故知
1.类的6个默认成员函数
如果一个类中什么成员都没有,简称为空类。空类中什么都没有吗?并不是的,任何一个类在我们不写的情况下,都会自动生成下面6个默认成员函数
class Date {};![]()
2. 构造函数
2.1 概念
对于以下的日期类:class Date { public: void SetDate(int year, int month, int day) { _year = year; _month = month; _day = day; } void Display() { cout << _year << "-" << _month << "-" << _day << endl; } private: int _year; int _month; int _day; }; int main() { Date d1; d1.SetDate(2022, 5, 1); d1.Display(); Date d2; d2.SetDate(2022, 7, 1); d2.Display(); return 0; }
对于Date类,可以通过SetDate公有的方法给对象设置内容,但是如果每次创建对象都调用该方法设置信息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢?构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有 一个合适的初始值,并且在对象的生命周期内只调用一次。
2.2 特性
构造函数是特殊的成员函数,需要注意的是,构造函数的虽然名称叫构造,但是需要注意的是构造函数的主要任务并不是开空间创建对象,而是初始化对象。其特征如下:1. 函数名与类名相同。2. 无返回值。3. 对象实例化时编译器自动调用对应的构造函数。4. 构造函数可以重载。![]()
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; // 调用无参构造函数 Date d2(2015, 1, 1); // 调用带参的构造函数 // 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明 // 以下代码的函数:声明了d3函数,该函数无参,返回一个日期类型的对象 Date d3(); }
5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。class Date { public: /* // 如果用户显式定义了构造函数,编译器将不再生成 Date (int year, int month, int day) { _year = year; _month = month; _day = day; } */ private: int _year; int _month; int _day; }; void Test() { // 没有定义构造函数,对象也可以创建成功,因此此处调用的是编译器生成的默认构造函数 Date d; }
class A { public: A() { cout << "A()" << endl; _a = 0; } private: int _a; }; class Date { public: 1.如果用户显式定义了构造函数,编译器将不再生成 //Date (int year, int month, int day) //{ //_year = year; //_month = month; //_day = day; //} //2.如果用户没有定义构造函数,编译器将生成一个默认无参构造函数 //默认生成构造函数对于内置类型成员变量不作处理,对于自定义类型成员变量才做处理 private: int _year; int _month; int _day; A _aa; }; int main() { Date d1; return 0; }
6. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认成员函数。// 默认构造函数 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; }
如果一个类中的成员全是自定义类型,我们就可以用默认生成的函数
如果有内置类型的成员,或者需要显示传参初始化,那么都要自己实现构造函数7. 关于编译器生成的默认成员函数,很多童鞋会有疑惑:在我们不实现构造函数的情况下,编译器会生成 默认的构造函数。但是看起来默认构造函数又没什么用?d对象调用了编译器生成的默认构造函数,但 是d对象year/month/_day,依旧是随机值。也就说在这里编译器生成的默认构造函数并没有什么用。解答:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语法已经定义好的类型:如 int/char...,自定义类型就是我们使用class/struct/union自己定义的类型,看看下面的程序,就会发现 编译器生成默认的构造函数会对自定类型成员_t调用的它的默认成员函数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; return 0; }
内置类型的未被处理,是随机值。
class Stack { public: Stack() { _a = nullptr; _top = _capacity = 0; } private: int* _a; int _top; int _capacity; }; class MyQueue { public: //默认生成的构造函数就可以用 void push(int x){} int pop(){} /*MyQueue() //无法初始化,因为_st1是私有,无法访问 { _st1 = nullptr; }*/ private: Stack _st1; Stack _st2; //总结:如果一个类中的成员全是自定义类型,我们就可以用默认生成的函数 //如果有内置类型的成员,或者需要显示传参初始化,那么都需要自己实现构造函数 }; int main() { MyQueue q; return 0; }
#include<cassert> class Stack { public: //例1.写了但是无参 /*Stack() { _a = nullptr; _top = _capacity = 0; }*/ //例2.写了但是全缺省的 //Stack(int capacity=10) //{ // _a =(int*) malloc(sizeof(int)*capacity); // assert(_a); // _top = 0; // _capacity = capacity; //} //例3.不写,系统默认生成的 private: int* _a; int _top; int _capacity; }; class MyQueue { public: //例3.不写,系统默认生成的 //默认生成的构造函数就可以用,不用传参就可以调用 void push(int x) {} int pop() {} /*MyQueue() //无法初始化,因为_st1是私有,无法访问 { _st1 = nullptr; }*/ private: Stack _st1; Stack _st2; //总结:如果一个类中的成员全是自定义类型,我们就可以用默认生成的函数 //如果有内置类型的成员,或者需要显示传参初始化,那么都需要自己实现构造函数 }; int main() { MyQueue q; return 0; } //MyQueue调用构造函数进行初始化,需通过调用Stack,而Stack也要调用自己的构造函数 //调用自定义类型成员的默认构造函数有3种 //1.写了但是无参 //2.写了但是全缺省的 //3.不写,系统默认生成的
若不完全是自定义类型,现在加上内置类型int _size=0;则:
#include<cassert> class Stack { public: //例1.写了但是无参 /*Stack() { _a = nullptr; _top = _capacity = 0; }*/ //例2.写了但是全缺省的 //Stack(int capacity=10) //{ // _a =(int*) malloc(sizeof(int)*capacity); // assert(_a); // _top = 0; // _capacity = capacity; //} //例3.不写,系统默认生成的 private: int* _a; int _top; int _capacity; }; class MyQueue { public: //例3.不写,系统默认生成的 //默认生成的构造函数就可以用,不用传参就可以调用 void push(int x) {} int pop() {} /*MyQueue() //无法初始化,因为_st1是私有,无法访问 { _st1 = nullptr; }*/ private: //int _size; int _size = 0; //这里给的是缺省值,编译器自己生成默认构造函数使用 //C++11 打补丁,针对编译器自己生成默认成员函数不初始化的问题 Stack _st1; Stack _st2; //总结:如果一个类中的成员全是自定义类型,我们就可以用默认生成的函数 //如果有内置类型的成员,或者需要显示传参初始化,那么都需要自己实现构造函数 }; int main() { MyQueue q; return 0; } //MyQueue调用构造函数进行初始化,需通过调用Stack,而Stack也要调用自己的构造函数 //调用自定义类型成员的默认构造函数有3种 //1.写了但是无参 //2.写了但是全缺省的 //3.不写,系统默认生成的
8. 成员变量的命名风格// 我们看看这个函数,是不是很僵硬? class Date { public: Date(int year) { // 这里的year到底是成员变量,还是函数形参? year = year; } private: int year; }; // 所以我们一般都建议这样 class Date { public: Date(int year) { _year = year; } private: int _year; }; // 或者这样。 class Date { public: Date(int year) { m_year = year; } private: int m_year; }; // 其他方式也可以的,主要看公司要求。一般都是加个前缀或者后缀标识区分就行。
3.析构函数
3.1 概念
前面通过构造函数的学习,我们知道一个对象时怎么来的,那一个对象又是怎么没呢的?析构函数:与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作
3.2 特性
析构函数是特殊的成员函数。其特征如下:1. 析构函数名是在类名前加上字符 ~。2. 无参数无返回值。3. 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。typedef int DataType; class SeqList { public: SeqList(int capacity = 10) { _pData = (DataType*)malloc(capacity * sizeof(DataType)); assert(_pData); _size = 0; _capacity = capacity; } ~SeqList() { if (_pData) { free(_pData); // 释放堆上的空间 _pData = NULL; // 将指针置为空 _capacity = 0; _size = 0; } } private: int* _pData; size_t _size; size_t _capacity; };
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.栈 class Stack { public: Stack(int capacity=10) { _a =(int*) malloc(sizeof(int)*capacity); assert(_a); _top = 0; _capacity = capacity; } //析构函数——清理资源 //Stack需要写析构函数 ~Stack() { free(_a); _a = nullptr; _top = _capacity = 0; cout << "~Stack()" << endl; } private: int* _a; int _top; int _capacity; }; //2.日期类 class Date { public: Date(int year = 1900, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } //析构函数 //日期类不用写析构函数,因为没有资源需要清理 ~Date() { //检测是否调用析构函数 cout << "~Date()" << endl; } private: int _year; int _month; int _day; }; int main() { //Date d1; //Stack st; //验证构造和析构顺序 //构造顺序:1、2 //析构顺序:2、1 Stack st1(1); Stack st2(2); return 0; }
//析构函数 //栈 class Stack { public: Stack(int capacity=10) { _a =(int*) malloc(sizeof(int)*capacity); assert(_a); _top = 0; _capacity = capacity; } //析构函数——清理资源 //Stack需要写析构函数 ~Stack() { cout << "~Stack():" << this << endl; free(_a); _a = nullptr; _top = _capacity = 0; } private: int* _a; int _top; int _capacity; }; //将队列引入举例调用析构函数 class MyQueue { public: //例3.不写,系统默认生成的 //默认生成的构造函数就可以用, //默认生成的析构函数就可以用, void push(int x) {} int pop() {} private: Stack _st1; Stack _st2; }; int main() { Stack st1(1); Stack st2(2); MyQueue q; return 0; }
4. 拷贝构造函数
4.1 概念
那在创建对象时,可否创建一个与一个对象一模一样的新对象呢?构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用
4.2 特征
拷贝构造函数也是特殊的成员函数,其 特征 如下: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; } private: int _year; int _month; int _day; }; int main() { Date d1; Date d2(d1); return 0; }
![]()
![]()
总结:这里采用Func函数进行传值传参,d1先初始化d,同类型的对象初始化需要调用拷贝构造函数,然后又去调用Func函数。
如何修正:
改成传引用传参
对于自定义类型中,同类型的对象传值传参,需要调用拷贝构造函数,但传值引发对象的拷贝,层层调用形成递归调用循环,采用传引用传参,避免这个问题。
内置类型,传值传参直接拷贝。
自定义类型,传值传参需要调用拷贝构造函数拷贝。
class Date { public: Date(int year = 1900, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } //Date(const Date& d) //Date d2(d1); //d1传给d,d2就是this Date(Date& d) { _year = d._year; _month = d._month; _day = d._day; } private: int _year; int _month; int _day; }; void Func(Date& d) { } int main() { Date d1(2022,07,01); Func(d1); //Date d2(d1); return 0; } //在C语言中,传值传参,形参是实参的临时拷贝 //在c++中,对于自定义类型的对象,传值这里是调用,同类型的对象初始化就要调用拷贝构造函数
![]()
class Date { public: Date(int year = 1900, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } //Date d2(d1); //d1传给d,d2就是this //Date(Date& d) Date(const Date& d) //为了保护d不被修改 //Date(Date& d) { _year = d._year; _month = d._month; _day = d._day; /*d._year = _year; d._month = _month; d._day = _day;*/ } private: int _year; int _month; int _day; }; int main() { Date d1(2022, 07, 01); Date d2(d1); return 0; } //在C语言中,传值传参,形参是实参的临时拷贝 //在c++中,对于自定义类型的对象,传值这里是调用,同类型的对象初始化就要调用拷贝构造函数
class Date { public: Date(int year = 1900, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } Date(const Date* d) //为了保护d不被修改 { _year = d->_year; _month = d->_month; _day = d->_day; } private: int _year; int _month; int _day; }; int main() { Date d1(2022, 07, 01); Date d2(d1); //拷贝构造函数 Date d3(&d1); //普通构造函数 return 0; }
3. 若未显示定义,系统生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝。class Date { public: Date(int year = 1900, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; }; int main() { Date d1; // 这里d2调用的默认拷贝构造完成拷贝,d2和d1的值也是一样的。 Date d2(d1); return 0; }
//栈 class Stack { public: Stack(int capacity = 10) { _a = (int*)malloc(sizeof(int) * capacity); assert(_a); _top = 0; _capacity = capacity; } 栈的拷贝构造——不能这样写,需要深拷贝 st2(st1) //浅拷贝 //Stack(const Stack& st) //{ // _a = st._a; // _top = st._top; // _capacity = st._capacity; //} //析构函数——清理资源 //Stack需要写析构函数 ~Stack() { free(_a); _a = nullptr; _top = _capacity = 0; cout << "~Stack()" << endl; } private: int* _a; int _top; int _capacity; }; int main() { Stack st1(10); Stack st2(st1); return 0; }
![]()
![]()
//1.Date类 class Date { public: Date(int year = 1900, int month = 1, int day = 1) { _year = year; _month = month; _day = day; memset(_a, 0, sizeof(int) * 10); } //当不写拷贝构造函数,编译器会默认生成一个拷贝构造 //1.内置类型的成员会完成值拷贝,浅拷贝 //2.自定义类型的成员,会去调用这个成员的拷贝构造 /*Date(const Date& d) { _year = d._year; _month = d._month; _day = d._day; }*/ private: int _year; int _month; int _day; int _a[10]; }; //2.Stack类 class Stack { public: Stack(int capacity = 10) { _a = (int*)malloc(sizeof(int) * capacity); //在堆区 assert(_a); _top = 0; _capacity = capacity; } 栈的拷贝构造——不能这样写,需要深拷贝 st2(st1) //浅拷贝 //Stack(const Stack& st) //{ // _a = st._a; // _top = st._top; // _capacity = st._capacity; //} Stack(const Stack& st) { cout << "Stack(const Stack& st)" << endl; //验证是否调用 //...Stack用深拷贝 } //析构函数——清理资源 //Stack需要写析构函数 ~Stack() { free(_a); _a = nullptr; _top = _capacity = 0; cout << "~Stack()" << endl; } private: int* _a; int _top; int _capacity; }; //3.MyQueue类,自定义类型 class MyQueue { private: int size = 0; Stack _st1; Stack _st2; }; int main() { //Date d1(2022,07,01); //Date d2(d1); Stack st1(10); Stack st2(st1); MyQueue q1; MyQueue q2(q1); return 0; }
4. 那么编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,我们还需要自己实现吗?当然像日期类这样的类是没必要的。那么下面的类呢?验证一下试试?// 这里会发现下面的程序会崩溃掉?这里就需要我们以后讲的深拷贝去解决。 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; }; int main() { String s1("hello"); String s2(s1); }
这里涉及到析构,会造成程序崩溃。例如:对于同一块地址空间,插入数据、析构都会相互影响等。因此会用深拷贝处理。