> 文档中心 > C++类和对象(二)

C++类和对象(二)

目录

1. 类的6个默认成员函数

2. 构造函数

2.1 概念

2.2 特性

3. 析构函数

3.1 概念

3.2 特性

4. 拷贝构造函数

4.1 概念

4.2 特征

5. 赋值操作符重载

5.1 运算符重载

5.2 赋值运算符重载

6. 日期类的实现(详细见后续博客)

7. const成员函数

7.1 const修饰类的成员函数

8. 取地址及const取地址操作符重载

9.练习题


1. 类的6个默认成员函数

如果一个类中什么成员都没有,简称为空类。空类中什么都没有吗?并不是的,任何一个类在我们不写的情况下,都会自动生成下面6 个默认成员函数

class Date {};

2. 构造函数

2.1 概念

对于以下的日期类:

 class Data{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, d2;d1.SetDate(2018, 5, 1);d1.Display();Date d2;d2.SetDate(2018, 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;}

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;}

 默认构造函数,很多人以为是自己不写,编译默认生成的那一个。这个理解是不全面的。 有下面三种情况:

 1.自己不写,编译器默认生成的  2.自己写的无参的  3.自己写的全缺省

总结: 不用参数就可以调用的函数即为默认构造函数 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;}

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;}

析构顺序问题

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;}

解决方式:引用传参。不能用指针,因为用指针就不是拷贝构造了

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;}

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);}

5. 赋值操作符重载

5.1 运算符重载

C++ 为了增强代码的可读性引入了运算符重载 运算符重载是具有特殊函数名的函数 ,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。 函数名字为:关键字operator后面接需要重载的运算符符号 函数原型: 返回值类型  operator 操作符 ( 参数列表 ) 注意

1.不能通过连接其他符号来创建新的操作符:比如 operator@ 2.重载操作符必须有一个类类型或者枚举类型的操作数 3.用于内置类型的操作符,其含义不能改变,例如:内置的整型 + ,不 能改变其含义 4.作为类成员的重载函数时,其形参看起来比操作数数目少 1 成员函数的 操作符有一个默认的形参 this ,限定为第一个形参 5. .* :: sizeof ?: . 注意以上 5 个运算符不能重载。这个经常在笔试选择题中出现。

 运算符重载和函数重载

函数重载是支持定义同名函数,运算符重载是为了让自定义类型可以像内置类型一样去使用运算符

// 全局的operator==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;};// 这里会发现运算符重载成全局的就需要成员变量是共有的,那么问题来了,封装性如何保证?// 这里其实可以用我们后面学习的友元解决,或者干脆重载成成员函数。bool operator==(const Date& d1, const Date& d2) { return d1._year == d2._year; && d1._month == d2._month && d1._day == d2._day; }void Test (){ Date d1(2018, 9, 26); Date d2(2018, 9, 27); cout<<(d1 == d2)<<endl; }
class Date{public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}// bool operator==(Date* this, const Date& d2)// 这里需要注意的是,左操作数是this指向的调用函数的对象bool operator==(const Date& d2){return _year == d2._year;&& _month == d2._month    && _day == d2._day;}private:int _year;int _month;int _day;};void Test(){Date d1(2018, 9, 26);Date d2(2018, 9, 27);cout << (d1 == d2) << endl;}

5.2 赋值运算符重载

赋值运算符重载也是一个默认成员函数,也就是说我们不写编译器会自动生成一个。编译器默认生成赋值运算符和拷贝构造的特性是一致的。针对内置类型会完成浅拷贝,也就是说像Date这样的类不需要我们自己写赋值运算符重载,Stack就需要自己写。针对自定义类型也一样,它会调用其赋值运算符重载完成拷贝   

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& operator=(const Date& d){if (this != &d){_year = d._year;_month = d._month;_day = d._day;}}private:int _year;int _month;int _day;};

赋值运算符主要有四点: 1. 参数类型 2. 返回值 3. 检测是否自己给自己赋值 4. 返回 *this 5. 一个类如果没有显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝。

 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;Date d2(2018,10, 1);// 这里d1调用的编译器生成operator=完成拷贝,d2和d1的值也是一样的。d1 = d2;return 0;}

实现连续拷贝

那么 编译器生成的默认赋值重载函数已经可以完成字节序的值拷贝了 ,我们还需要自己实现吗?当然像日期类这样的类是没必要的。那么下面的类呢?验证一下试试?

// 这里会发现下面的程序会崩溃掉?这里就需要我们以后讲的深拷贝去解决。class String{public:String(const char* str = ""){_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("world");s1 = s2;}

针对我们不写编译器默认生成的总结一下:

构造和析构的特性是类似的,我们不写编译器内置类型不处理,自定义类型调用它的构造和析构处理,拷贝构造和赋值重载特性是类似的,内置类型会完成浅拷贝,自定义类型会调用他们的拷贝构造和赋值重载。(注意总结关于四个函数和运算符重载的特性和一些使用特点)

 

6. 日期类的实现(详细见后续博客)

class Date{public:// 获取某年某月的天数int GetMonthDay(int year, int month){static int days[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };int day = days[month];if (month == 2&& ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))){day += 1;}return day;}// 全缺省的构造函数Date(int year = 1900, int month = 1, int day = 1);// 拷贝构造函数// d2(d1)Date(const Date& d);// 赋值运算符重载// d2 = d3 -> d2.operator=(&d2, d3)Date& operator=(const Date& d);// 析构函数~Date();// 日期+=天数Date& operator+=(int day);// 日期+天数Date operator+(int day);// 日期-天数Date operator-(int day);// 日期-=天数Date& operator-=(int day);// 前置++Date& operator++();// 后置++Date operator++(int);// 后置--Date operator--(int);// 前置--Date& operator--();// >运算符重载bool operator>(const Date& d);// ==运算符重载bool operator==(const Date& d);// >=运算符重载inline bool operator >= (const Date& d);// <运算符重载bool operator < (const Date& d);// <=运算符重载bool operator <= (const Date& d);// !=运算符重载bool operator != (const Date& d);// 日期-日期 返回天数int operator-(const Date& d);private:int _year;int _month;int _day;};

7. const成员函数

7.1 const修饰类的成员函数

const 修饰的类成员函数称之为 const 成员函数 const 修饰类成员函数,实际修饰该成员函数 隐含的 this 指针 ,表明在该成员函数中 不能对类的任何成员进行修改。

 观察下面代码,思考几个问题

class Date{public:void Display(){cout << "Display ()" << endl;cout << "year:" << _year << endl;cout << "month:" << _month << endl;cout << "day:" << _day << endl << endl;}void Display() const{cout << "Display () const" << endl;cout << "year:" << _year << endl;cout << "month:" << _month << endl;cout << "day:" << _day << endl << endl;}private:int _year; // 年int _month; // 月int _day; // 日};void Test(){Date d1;d1.Display();const Date d2;d2.Display();}

1. const 对象可以调用非 const 成员函数吗?不可以->权限的放大 2. const 对象可以调用 const 成员函数吗?可以    ->权限的缩小 3. const 成员函数内可以调用其它的非 const 成员函数吗? 不可以 4. const 成员函数内可以调用其它的 const 成员函数吗? 可以

 

8. 取地址及const取地址操作符重载

这两个默认成员函数一般不用重新定义 ,编译器默认会生成。

 class Date{public:Date* operator&(){return this;}const Date* operator&()const{return this;}private:int _year; // 年int _month; // 月int _day; // 日};

这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容!

9.练习题

 

 

 

 

 

C++类和对象(二) 创作打卡挑战赛 C++类和对象(二) 赢取流量/现金/CSDN周边激励大奖