C++02_类与对象(中)

Posted joker D888

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++02_类与对象(中)相关的知识,希望对你有一定的参考价值。

【C++】02_类与对象(中)

01.类的默认成员函数

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

class Date {};
  1. 初始化–>构造函数
  2. 清理–>析构函数
  3. 使用同类对象初始化创建对象–>拷贝构造函数
  4. 把一个对象赋值给另一个对象–>赋值重载函数
  5. 普通对象取地址–>取地址重载;
  6. const对象取地址–>取地址重载。

02.构造函数

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,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. 构造函数可以重载

  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;
}
  1. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数全缺省构造函数、以及编译器默认生成的构造函数,都可以认为是默认成员函数。

    // 默认构造函数
    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; 	//❌,报错,“类 "Date" 包含多个默认构造函数”,由此可验证我们对于默认构造函数的结论;
    
    }
    
  2. 关于编译器生成的默认成员函数,很多童鞋会有疑惑:在我们不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?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.成员变量的命名风格

// 我们看看这个函数,是不是很僵硬,难以辨别两个year,这个问题之前我们也有说过,虽然可以加this->以区别,但依然不推荐这种命名。
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;
};
// 其他方式也可以的,主要看公司要求。一般都是加个前缀或者后缀标识区分就行。

2.3构造函数的调用

构造函数有三种调用方法,括号法(常用),显示法,隐式转换法。

//1、构造函数分类
// 按照参数分类分为 有参和无参构造   无参又称为默认构造函数
// 按照类型分类分为 普通构造和拷贝构造

class Person {
public:
	//无参(默认)构造函数
	Person() {
		cout << "无参构造函数!" << endl;
	}
	//有参构造函数
	Person(int a) {
		age = a;
		cout << "有参构造函数!" << endl;
	}
	//拷贝构造函数
	Person(const Person& p) {
		age = p.age;
		cout << "拷贝构造函数!" << endl;
	}
	//析构函数
	~Person() {
		cout << "析构函数!" << endl;
	}
public:
	int age;
};

//2、构造函数的调用
//调用无参构造函数
void test01() {
	Person p; //调用无参构造函数
}

//调用有参的构造函数
void test02() {

	//2.1  括号法,常用
	Person p1(10);
	//注意1:调用无参构造函数不能加括号,如果加了编译器认为这是一个函数声明
	//Person p2();	 ❌

	//2.2 显式法
	Person p2 = Person(10); 
	Person p3 = Person(p2);
	//Person(10)单独写就是匿名对象 ,当前行结束之后,马上析构

	//2.3 隐式转换法
	Person p4 = 10; // Person p4 = Person(10); 
	Person p5 = p4; // Person p5 = Person(p4); 

	//注意2:不能利用 拷贝构造函数 初始化匿名对象 编译器认为是对象声明
}

int main() {

	test01();
	//test02();
	return 0;
}
  • 默认情况下,c++编译器至少给一个类添加3个函数

    • 默认构造函数(无参,函数体为空)
    • 默认析构函数(无参,函数体为空)
    • 默认拷贝构造函数,对属性进行值拷贝
  • 构造函数的调用规则:

    • 如果用户定义有参构造函数,c++不在提供默认无参构造,但是会提供默认拷贝构造,你得自己提供默认构造;

    • 如果用户定义拷贝构造函数,c++不会再提供其他构造函数,也就是说,你得自己提供默认无和有参构造;

      class Person {
      public:
      	//无参(默认)构造函数
      	Person() {
      		cout << "无参构造函数!" << endl;
      	}
      	//有参构造函数
      	Person(int a) {
      		age = a;
      		cout << "有参构造函数!" << endl;
      	}
      	//拷贝构造函数
      	Person(const Person& p) {
      		age = p.age;
      		cout << "拷贝构造函数!" << endl;
      	}
      	//析构函数
      	~Person() {
      		cout << "析构函数!" << endl;
      	}
      public:
      	int age;
      };
      
      void test01()
      {
      	Person p1(18);
      	//如果不写拷贝构造,编译器会自动添加拷贝构造,并且做浅拷贝操作
      	Person p2(p1);
      
      	cout << "p2的年龄为: " << p2.age << endl;
      }
      
      void test02()
      {
          //可自行注释相应构造来进行验证。
      	//如果用户提供有参构造,编译器不会提供默认构造,会提供拷贝构造
      	Person p1; //此时如果用户自己没有提供默认构造,会出错
      	Person p2(10); //用户提供的有参
      	Person p3(p2); //此时如果用户没有提供拷贝构造,编译器会提供
      
      	//如果用户提供拷贝构造,编译器不会提供其他构造函数
      	Person p4; //此时如果用户自己没有提供默认构造,会出错
      	Person p5(10); //此时如果用户自己没有提供有参,会出错
      	Person p6(p5); //用户自己提供拷贝构造
      }
      
      int main() {
      
      	test01();
          test02();
      	return 0;
      }
      

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

3.3 类对象作为类成员

​ 当我们了解了构造和析构后,来看看类类对象作为类成员时,构造和析构的调用顺序。

​ 如:

class A {}
class B
{
    A a;
}
  • B类中有对象A作为成员,A为对象成员,那么当创建B对象时,A与B的构造和析构的顺序是谁先谁后?

    class Phone
    {
    public:
    	Phone(string name)
    	{
    		m_PhoneName = name;
    		cout << "Phone构造" << endl;
    	}
    
    	~Phone()
    	{
    		cout << "Phone析构" << endl;
    	}
    
    	string m_PhoneName;
    
    };
    
    class Person
    {
    public:
    
    	//初始化列表可以告诉编译器调用哪一个构造函数
    	Person(string name, string pName) :m_Name(name), m_Phone(pName)
    	{
    		cout << "Person构造" << endl;
    	}
    
    	~Person()
    	{
    		cout << "Person析构" << endl;
    	}
    
    	void playGame()
    	{
    		cout << m_Name << " 使用" << m_Phone.m_PhoneName << " 牌手机! " << endl;
    	}
    
    	string m_Name;
    	Phone m_Phone;
    
    };
    void test01()
    {
    	//当类中成员是其他类对象时,我们称该成员为 对象成员
    	//构造的顺序是 :先调用对象成员的构造,再调用本类构造
    	//析构顺序与构造相反
    	Person p("张三" , "苹果X");
    	p.playGame();
    
    }
    
    int main() {
    
    	test01();
    
    	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;
         }
    //    Date(const Date d)	//若不是引用传参,使用传值,相当于拷贝了一份临时副本,则当前函数的形参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操作符(参数列表)

注意🗣:

  • 不能通过连接其他符号来创建新的操作符:比如operator@ ;
  • 重载操作符必须有一个类类型或者枚举类型的操作数
  • 用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不能改变其含义;
  • 作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的操作符有一个默认的形参this,限定为第一个形参;
  • . (成员访问运算符). (成员指针访问运算符))∷(域运算符))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)
     {
         以上是关于C++02_类与对象(中)的主要内容,如果未能解决你的问题,请参考以下文章

C++02_类与对象(下)

Java语言-----类与对象的秘密

Java_类与对象13_小练习

python------面向对象介绍之经典类与新式类的继承顺序

C++学习_类与对象

python 面向对象专题:类的空间问题类与对象之间的关系类与类之间的关系