C++基础(八):类和对象 (下)

          经过前面的学习,我们已经翻过了两座大山,类和对象入门知识就剩下这一讲了,加油吧,少年!

目录

一、再谈构造函数

 1.1 构造函数体赋值

1.2 初始化列表(理解)

1.3 explicit关键字(C++98)

1.3.1 引入(理解)

1.3.2 explicit关键字介绍

1.3.3 总结

二、Static成员

2.1 概念

2.2 static修饰成员变量和成员函数的特性(重点理解)

2.3 思考(理解)

2.3.1 问题一

2.3.2 问题二

2.3.3 如何理解类域?

三、C++11的成员初始化新玩法

四、友元

4.1 友元函数

4.2 友元类

五、内部类

5.1 概念

5.2 特性

六、匿名对象

七、拷贝对象时的一些编译器优化

八、再次理解封装

九、再次理解面向对象


一、再谈构造函数

 1.1 构造函数体赋值

          在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。

class Date
{
    public:
      Date(int year, int month, int day)
      {
         _year = year;
         _month = month;
         _day = day;
      }

    private:
        int _year;
        int _month;
        int _day;
};

虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量 的初始化,构造函数体中的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始 化一次,而构造函数体内可以多次赋值。

1.2 初始化列表(理解)

      初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟 一个放在括号中的初始值或表达式。

class Date
{
 public:

     /*下面是函数体内赋值
      Date(int year, int month, int day)
      {
           _year=year;    
           _month=month;
           _day=day;
      }
     */


     下面是初始化列表:冒号开始,逗号分隔,初始化的值放在圆括号中,分行书写更加清晰。
     Date(int year, int month, int day)
       : _year(year)      //注意:这里采用等号初始化=不正确!
       , _month(month)
       , _day(day)
     { 
 
     
     }

 private:
    int _year;
    int _month;
    int _day;
};

为什么要有初始化列表这个东西???

为什么上述三类成员变量必须在初始化列表中进行初始化??(如果放在函数体内进行赋值,会发生编译错误!)

理解:         

        初始化列表是对象的成员变量定义的地方,有些成员变量在定义的时候必须要初始化,比如:常变量、引用变量(引用定义的时候必须要初始化)、没有默认构造函数的自定义类型成员变量!因此,它们必须要放在初始化列表中进行初始化,其他的一些变量可以不进行初始化,他就是随机值。

class A
{
     public:
       A(int a)     //带参的构造函数
       :_a(a)       初始化列表

       {

       }

     private:
       int _a;
};


class B
{
     public:
可以理解成初始化列表是对象的成员变量定义的地方,这些成员变量比较特殊,定义的时候必须要初始化
       B(int a, int ref)  //构造函数
        :_aobj(a) 自定义数据类型会调用它的构造函数进行初始化,它是带参的构造函数,所以必须要传参
        ,_ref(ref)
        ,_n(10)

        {
           _x=10;    

        }
     private:
        A _aobj;  // 调用它的构造函数初始化,因为没有默认构造函数(默认构造函数:不用传参就可以调用的那些构造函数)
        int& _ref;  // 引用,定义的时候必须要初始化
        const int _n; // const 常变量,定义的时候必须要初始化

        int _x;     //普通变量,并没有要求定义的时候必须要初始化。

       //这里是成员变量的声明,不会为这些成员变量开辟内存空间!
};

int main()
{
  
   B b(1,2);   //这里是由类实例化对象(对象定义),也就是在内存空间为对象开辟内存空间!

   return 0;
}

//初始化列表和函数体内赋值并不是非此即彼的关系,但是对于这三类变量必须要放在初始化列表初始化!

记住:尽可能的使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量, 一定会先使用初始化列表初始化。

但是以下三类变量必须在初始化列表中进行初始化:

1、 const成员变量(常变量,因为不可以进行修改的变量)

2、引用成员变量(因为引用定义的时候,必须要初始化)

3、 没有默认构造函数的自定义成员变量,这个时候你就必须要传参进行初始化了;

class Time
{
     public:
        Time(int hour = 0)   //全缺省的默认构造函数
        :_hour(hour)         //初始化列表

       {
         cout << "Time()" << endl;
       }

    private:
         int _hour;
};


class Date
{
     public:
        Date(int day) //构造函数,没有初始化列表,且函数体内也没有赋值!
       {
                   

       }
     private:
        int _day;
        Time _t;    //自定义类型,它会自动调用它的默认构造函数,进行初始化
};


int main()
{
    Date d(1);
}

     成员变量在初始化列表初始化的顺序是按照声明的顺序,不是初始化列表出现的定义的顺序,因此,我们在实际开发中应该保持初始化列表中的初始化顺序和成员变量的声明顺序保持一致!!!

class A
{
public:
    A(int a)
       :_a1(a)      //这一行后进行初始化
       ,_a2(_a1)   //这里先进行初始化
   {

   }
    
    void Print()
    {
        cout<<_a1<<" "<<_a2<<endl;
    }
private:
    int _a2;
    int _a1;
};

int main() 
{
    A aa(1);
    aa.Print();
}

A. 输出1  1
B.程序崩溃
C.编译不通过
D.输出1  随机值

       上述的结果为D,因为编译器在初始化的时候,是按照成员变量声明的顺序进行初始化的,先声明的是_a2,因此它先对_a2进行初始化(用的是_a1的值),但此时_a1还未初始化,它是随机值,因此_a2是随机值,然后才会对_a1进行初始化(用的是a的值,也就是1),因此,_a1被初始化为1 ,打印的时候,先打印_a1再打印_a2。

1.3 explicit关键字(C++98)

1.3.1 引入(理解)

class Date
{
 public:

      Date(int year)   //构造函数
      :_year(year)

      {
           cout<<"Date(int year)"  <<endl; 
           
      }


      Date(const Date& d)  //拷贝构造函数
      {
           cout<<"Date(const Date& d)"  <<endl; 
           
      }

         

 private:
    int _year;
    int _month;
    int _day;
};


int main()
{
   Date d1(1);    //这里是创建对象,并调用构造函数初始化
   Date d2 = 2;   //这里是隐式的类型转换,底层过程:先构造出临时对象tmp(2),然后再拷贝构造出d2即d2(tmp);   对于编译器它直接优化成创建d2对象并构造

      Date& d2 = 2;  //这里编译就无法通过!2是右值,具有常性,只能用常引用来引用它!引用的是中间产生的临时对象!
const Date& d2 = 2;  //编译可以通过!(底层就是隐式类型转换,会产生中间的临时对象!)

   Date d3 = d1;   //这里是调用拷贝构造函数
   Date d4(d1);   //这里也是调用拷贝构造函数
  

    return 0;
}


如何理解这里的隐式类型转换?
   int i = 1;
   double d = i;   //这里就是隐式类型转换,先生成double类型的临时变量,再将这个double类型的变量赋值给d。
  
如何证明这个问题?看下面
   double& d =i;  //这里编译器不会通过!!因为产生的临时对象具有常性,只能用常引用来引用它!!修改如下:
  
 const double& d =i;  //编译可以通过!(底层就是隐式类型转换,会产生中间的临时变量!)

如何不让中间的隐式类型转换发生呢?那就是这一小节要讲的关键字explicit ,在构造函数前面加上explicit,这样的话:程序走到Date d2 = 2;  就会发生类型转换错误!!!

1.3.2 explicit关键字介绍

      上一小节,构造函数只有一个参数,那如果是多个参数呢?

class Date
{
 public:

      Date(int year,int month,int day)   //构造函数:多个参数
      :_year(year)

      {
           cout<<"Date(int year)"  <<endl; 
           
      }


      Date(const Date& d)  //拷贝构造函数
      {
           cout<<"Date(const Date& d)"  <<endl; 
           
      }

         
 private:
    int _year;
    int _month;
    int _day;
};


int main()
{
   Date d1(1,2,3);
  
   Date d2 = (1,2,3);  //这里编译是无法通过的(C++98)!


   C++11支持下面这种写法!
   Date d2 = {1,2,3};  //编译可以通过!统一的初始化方案,如果构造函数加了关键字explicit,不会进行隐式类型转换,编译无法通过!

    return 0;
}

1.3.3 总结

      构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值 的构造函数,还具有类型转换的作用。

class Date
{
public:
// 1. 单参构造函数,没有使用explicit修饰,具有类型转换作用
 // explicit修饰构造函数,禁止类型转换---explicit去掉之后,代码可以通过编译
 explicit Date(int year)
 :_year(year)
 {}


 /*
 // 2. 虽然有多个参数,但是创建对象时后两个参数可以不传递,没有使用explicit修饰,具
有类型转换作用
 // explicit修饰构造函数,禁止类型转换
 explicit Date(int year, int month = 1, int day = 1)
 : _year(year)
 , _month(month)
 , _day(day)
 {}
 */

 Date& operator=(const Date& d)
 {
   if (this != &d)
   {
     _year = d._year;
     _month = d._month;
     _day = d._day;
   }
      return *this;
 }

private:
      int _year;
      int _month;
      int _day;
};

void Test()
{
   Date d1(2022);
 // 用一个整形变量给日期类型对象赋值
 // 实际编译器背后会用2023构造一个无名对象,最后用无名对象给d1对象进行赋值
 d1 = 2023;
 // 将1屏蔽掉,2放开时则编译失败,因为explicit修饰构造函数,禁止了单参构造函数类型转
换的作用
}

上述代码可读性不是很好,用explicit修饰构造函数,将会禁止构造函数的隐式转换。

二、Static成员

2.1 概念

       声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用 static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化。可以讲static修饰的成员变量理解成加了访问限定符的全局变量。每个对象都共享,因此,它只需要初始化一次,就应该放在类外进行定义然后初始化。

面试题:实现一个类,计算程序中创建出了多少个类对象。

      首先,明白一个问题:对象的来源?我们知道:对象是由构造函数和拷贝构造函数创建出来的,也就是说:每调用一次构造函数或者拷贝构造函数就会产生一个对象(函数的调用次数和创建对象的个数是一样的),那么,我们是不是就可以定义一个全局变量,然后在函数内部进行++操作,这样最后获取这个全局变量,是不是就可以得到创建出了多少个对象?

先看如下代码: 


#include<iostream>
using namespace std;

int n = 0;
class A
{
public:
	A()
	{
		cout << "构造函数被调用" << endl;
		++n;
	}

	A(const A& a)
	{
		cout << "拷贝构造函数被调用" << endl;
		++n;
	}

};


A func(A a)   //a1传给a,传参也会发生拷贝构造创建出一个新的对象
{
	return a;    //传值返回也会发生拷贝构造,创建出一个对象,然后再返回这个对象
}


int main()
{ 
	A a1;         //创建出一个对象,调用构造函数
	A a2;        //创建出一个对象,调用构造函数

	func(a1);   //创建出两个对象,调用两次拷贝构造函数
	func(a2);  //创建出两个对象,调用两次拷贝构造函数

	cout << n << endl;
	return 0;
}

再看如下代码: 

#include<iostream>
using namespace std;

int n = 0;
class A
{
public:
	A()
	{
		cout << "构造函数被调用" << endl;
		++n;
	}

	A(const A& a)
	{
		cout << "拷贝构造函数被调用" << endl;
		++n;
	}

};


A func(A& a)   //传引用不会发生拷贝构造,他就是传过来的对象的别名,同一个实体
{
	return a;    //传值返回会发生拷贝构造,创建出一个对象,然后再返回这个对象
}


int main()
{ 
	A a1;         //创建出一个对象,调用构造函数
	A a2;        //创建出一个对象,调用构造函数

	func(a1);   //创建出一个对象,调用一次拷贝构造函数
	func(a2);  //创建出一个对象,调用一次拷贝构造函数

	cout << n << endl;
	return 0;
}

再继续看如下代码:

#include<iostream>
using namespace std;

int n = 0;
class A
{
public:
	A()
	{
		cout << "构造函数被调用" << endl;
		++n;
	}

	A(const A& a)
	{
		cout << "拷贝构造函数被调用" << endl;
		++n;
	}

};


A& func(A& a)   //传引用不会发生拷贝构造,他就是传过来的对象的别名,同一个实体
{
	return a;    //传引用返回不会发生拷贝构造,直接返回传进来的对象
}


int main()
{ 
	A a1;         //创建出一个对象,调用构造函数
	A a2;        //创建出一个对象,调用构造函数

	func(a1);   //不会创建对象
	func(a2);  //不会创建对象

	cout << n << endl;
	return 0;
}

     上面就是在函数传参和返回值设计为引用时与直接传值(对象)的重要区别!只要出了局部作用域,对象本身还在,就可以以引用方式返回!!!

上述第一个代码存在一个问题:这个全局变量n,谁都可以来修改它!数据没有得到很好的保护,封装性不好!比如下面:这里的n就跳出了类的管理!

#include<iostream>
using namespace std;

int n = 0;
class A
{
public:
	A()
	{
		cout << "构造函数被调用" << endl;
		++n;
	}

	A(const A& a)
	{
		cout << "拷贝构造函数被调用" << endl;
		++n;
	}

};


A func(A a)   
{
	return a;    
}


int main()
{ 
	A a1;         
	A a2;    
    
	func(a1);

    n=1;   
	func(a2);  

	cout << n << endl;
	return 0;
}

如何解决呢?这就要用到static关键字

先看代码,公有情况下直接访问:

#include<iostream>
using namespace std;


class A
{
public:
	A()
	{
		cout << "构造函数被调用" << endl;
		++n;
	}

	A(const A& a)
	{
		cout << "拷贝构造函数被调用" << endl;
		++n;
	}


	static int n;  //注意:这里是声明,并且这个成员变量不是属于某个对象,是属于类的所有对象,属于这个类,所有对象共用一个n !!!
	               //n不是存储在对象中,n存储在数据区(静态的)
};


int A::n = 0;    //注意:这里是定义,不用在加static ,静态的成员变量必须在类外定义进行初始化

A func(A a)   
{
	return a;   
}


int main()
{ 
	A a1;         
	A a2;        

	func(a1);  	
	func(a2);

	//n的访问权限为公有的情况下:
	cout << a1.n << endl;
	cout << a2.n << endl;
	cout << A::n << endl;
	cout << endl;

	//打印地址是一个,说明它只存储一份!在数据区
	cout << &a1.n << endl;
	cout << &a2.n << endl;
	cout << &A::n << endl;
	cout << endl;

	//可以进行修改
	a1.n = 100;
	cout << a1.n << endl;

	return 0;
}

再看私有情况下,可以通过成员函数访问

#define _CRT_SECURE_NO_WARNINGS 1

#include<iostream>
using namespace std;


class A
{
public:
	A()
	{
		cout << "构造函数被调用" << endl;
		++n;
	}

	A(const A& a)
	{
		cout << "拷贝构造函数被调用" << endl;
		++n;
	}

	int GetN()
	{
		return n;
	}


private:
	static int n;  //注意:这里是声明,并且这个成员变量不是属于某个对象,是属于类的所有对象,属于这个类,所有对象共用一个n !!!
	               //n不是存储在对象中,n存储在数据区(静态的)
};


int A::n = 0;    //注意:这里是定义,不用在加static ,静态的成员变量必须在类外定义进行初始化

A func(A a)   
{
	return a;   
}


int main()
{ 
	A a1;         
	A a2;        

	func(a1);  	
	func(a2);

	//n的访问权限为私有的情况下:
     cout << a1.GetN() << endl;
     cout << a2.GetN() << endl;

	return 0;
}




那么静态的成员函数呢?

#define _CRT_SECURE_NO_WARNINGS 1

#include<iostream>
using namespace std;


class A
{
public:
	A()
	{
		cout << "构造函数被调用" << endl;
		++n;
	}

	A(const A& a)
	{
		cout << "拷贝构造函数被调用" << endl;
		++n;
	}

	static int GetN()   //成员函数为静态的!静态的成员函数,它的特性是:没有this指针!!这就导致一个现象:在这个函数体中不能访问非静态的成员,只能访问静态的成员变量
	{
		//_a=10;       //这里不允许!
		return n;
	}


private:
	int _a;
	static int n;  //注意:这里是声明,并且这个成员变量不是属于某个对象,是属于类的所有对象,属于这个类,所有对象共用一个n !!!
	               //n不是存储在对象中,n存储在数据区(静态的)
};


int A::n = 0;    //注意:这里是定义,不用在加static ,静态的成员变量必须在类外定义进行初始化

A func(A a)   
{
	return a;   
}


int main()
{ 
	A a1;         
	A a2;        

	func(a1);  	
	func(a2);

	//n的访问权限为私有的情况下:
     cout << a1.GetN() << endl;
     cout << a2.GetN() << endl;
	 cout << A::GetN() << endl;    //非静态成员函数的话,这种方式访问是错误的!因为它是属于某个对象的,必须通过对象调用
	 cout << A::GetN() << endl;   //静态成员函数的话,就可以通过这种方式访问!因为它是属于这个类的,可以直接通过域解析符,然后调用!


	return 0;
}




2.2 static修饰成员变量和成员函数的特性(重点理解)

1. 静态成员变量不存储在对象中,存储在静态区,因此,它不属于某个具体的对象,它属于这个类的所有对象,也是属于这个类;

2. 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明;

3. 类静态成员即可用类名::静态成员 或者 对象.静态成员 来访问;

4. 静态成员函数没有隐藏的this指针,因为它也是属于这个类的,所以不使用对象就可以调用,方式为:类名::func(),但是它不能访问任何非静态成员(包括非静态成员函数和非静态的成员变量);

5. 静态成员(不论是静态的成员变量还是静态的成员函数)也是类的成员,受public、protected、private 访问限定符的限制;

2.3 思考(理解)

2.3.1 问题一

在静态成员函数中可以调用非静态成员函数吗?

      不可以静态成员函数没有this指针,因此无法直接访问对象的非静态成员(包括非静态成员函数和非静态的成员变量)。静态成员函数是属于类本身,而不是属于类的某个实例,所以它们不能直接操作具体对象的状态。

#include<iostream>
using namespace std;


class Date
{
public:
    Date(int year=0, int month=1, int day=1)
     :_year(year)
     ,_month(month)
     ,_day(day)
    {
        
    }

    void func1()      //它的第一个参数为隐含的this指针
    {

    }

    static void func2()
    {
        func1();  //这是不可以的,因为func2()函数没有this指针,没办法传给func1()函数

    }


private:
    int _year;
    int _month;
    int _day;
};


int main()
{ 
	       
	return 0;
}

2.3.2 问题二

非静态成员函数可以调用类的静态成员函数吗?

       可以非静态成员函数可以直接调用静态成员函数,因为静态成员函数属于类本身,而非静态成员函数是通过对象实例调用的。因此,非静态成员函数在访问静态成员函数时,不需要通过对象实例来访问,而是直接通过类名或者直接调用。

#include<iostream>
using namespace std;


class Date
{
public:
    Date(int year=0, int month=1, int day=1)
     :_year(year)
     ,_month(month)
     ,_day(day)
    {
        
    }

    void func3()
    {
        func4();  //这里是可以的,突破类域+访问限定符就可以访问,类里面是一个整体,都在这个类域中
    }

    static void func4()  //静态成员函数
    {
       
    }


private:
    int _year;
    int _month;
    int _day;
};


int main()
{ 
	Date d1(1,2,3);
    Date d2(3,4,5);
   
突破类域访问的两个条件,编译器可以知道到哪个域找+访问限定符是否可以访问。
以下为类外访问静态成员函数的两种方式(前提是静态的成员函数的访问限定符是公有的public)
    d2.func4();
    Date::func4();
     
	return 0;
}



2.3.3 如何理解类域?

        类的作用域叫做类域,类域是指包裹类里面的成员(成员变量和成员函数),在类里面不受访问限定符的限制,可以任意访问,它是用来限制的是类外的访问的,可以把类域想象成一个围墙,而把访问限定符想象成一把把锁,它是用来保护成员变量和成员函数的。

三、C++11的成员初始化新玩法

      非静态成员变量,可以在成员变量声明时给缺省值,但是对于静态的成员变量必须要在类外定义然后初始化!!

#include<iostream>
using namespace std;


class Date
{
public:

    Date()
        :_year(2021)
        ,_month(7)
        ,_day(6)
    {

    }



    void print()
    {
        cout << _year << "-" << _month << "-" << _day << endl;
     }
         

private:
    //C++11可以这么做,这里可不是初始化(初始化是在定义的时候赋值的),这叫做声明时给缺省值(如果调用构造函数不进行初始化,就会用这个缺省值)
    int _year=2024;
    int _month=7;
    int _day=6;

   static int _t =10;  //注意静态的成员变量不可以这么给!必须要放在类外!
};

int main()
{ 
    Date d1;
    d1.print();
	return 0;
}

四、友元

      通过前面的学习,我们知道类的作用域叫做类域,类域是指包裹类里面的成员(成员变量和成员函数),在类里面不受访问限定符的限制,可以任意访问,它是用来限制的是类外的访问的,对于私有的成员变量,类外是不能直接访问的,一般是通过公有的成员函数间接的来访问,那有没有一种方法可以解决这个问题?类外可以直接访问私有的成员变量。这就是这一小节需要讲的。

#include<iostream>
using namespace std;


class Date
{
public:

    void print()
    {
        cout << _year << "-" << _month << "-" << _day << endl;
     }
         

private:
    
    int _year=2024;
    int _month=7;
    int _day=6;
};


void func(Date& d)
{
    d._year = 2022;          //编译会报错!不可访问类内的私有成员变量
    cout << d.year << endl; //编译会报错!不可访问类内的私有成员变量
}



int main()
{ 
    Date d1;
    func(d1);
	return 0;
}

     友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以 友元不宜多用。 友元分为:友元函数和友元类

4.1 友元函数

      友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在 类的内部声明,声明时需要加friend关键字。

        使用场景:如果想在类外访问这个私有的成员,也就是突破访问限定符,就可以使用这个友元函数。


#include<iostream>
using namespace std;


class Date
{
public:

    //下面这句话表示:这个函数是这个类的友元函数,它就不再受访问限定符的限制!
    friend void func(Date& d);                   //friend关键字+函数声明


    void print()
    {
        cout << _year << "-" << _month << "-" << _day << endl;
     }
         

private:
    
    int _year=2024;
    int _month=7;
    int _day=6;
};


void func(Date& d)
{
    d._year = 2022;            //编译器不会报错!
    cout << d._year << endl;  //编译器不会报错!
}



int main()
{ 
    Date d1;
    func(d1);
	return 0;
}

下面这个场景必须使用友元函数:

         对于自定义类型如:对象,如果想要使用运算符,必须要进行重载。我们先进行输出提取符重载,代码如下:

#include<iostream>
using namespace std;


class Date
{
public:

    void print()
    {
        cout << _year << "-" << _month << "-" << _day << endl;
     }
 

cout是std库里面的一个ostream类型的全局对象      
     //重载输出提取符:<< ,它有两个操作数,只需要设计一个参数,隐含的第一个参数是this指向传入的对象(左操作数),第二个参数为右操作数
    void operator<<(ostream& out)   //out就是cout的别名
    {
        out << _year << "/" << _month << "/" << _day << endl;  //内置类型的输出
            
    }

private:
    
    int _year=2024;
    int _month=7;
    int _day=6;
};

int main()
{ 
    Date d1;
    d1.print();

    //cout输出基本数据是可以的,但是对于我们自定义的对象类型,这是不允许的,如果想要使用,就必须对输出提取符进行运算符重载
    //cout << d1;    //重载以后,直接这么用还是会报错

   
    // 编译器底层:d1.operator<<(cout);
    d1.operator<<(cout); //如果这么显示调用,这里就不会报错!可以正常输出
    d1 << cout;          //这么使用也不会报错!可以正常输出,但是可读性差!
	return 0;
}

d1 << cout; -> d1.operator<<(&d1, cout); 不符合常规调用
因为成员函数第一个参数一定是隐藏的this,所以d1必须放在<<的左侧

很明显,虽然可以使用:d1<<cout输出对象,但是可读性差,导致这样的原因是什么呢?其实还是this指针捣的鬼,我们先思考一个问题:为什么运算符重载函数要放在类中进行实现?很容易回答:因为运算符重载函数内部要对私有的成员变量进行访问,如果放在类外,那不是不能访问了吗?并且运算符重载函数的第一个参数为固定的this指针,它指向调用这个运算符重载函数的对象(左操作数),第二个为右操作数,这两个位置无法交换!!!所以我们在调用运算符重载的时候,这个顺序就要遵循运算符重载函数的顺序,第一个必须是对象(左操作数),由this指针指向,这样,便会出现上面的问题!!!如何解决?那此时就必须要放在类外进行输出提取运算符重载了!

#include<iostream>
using namespace std;


class Date
{
public:

    friend void operator<<(ostream& out, const Date& d);   //对重载的函数进行声明

    void print()
    {
        cout << _year << "-" << _month << "-" << _day << endl;
     }
         

private:
    
    int _year=2024;
    int _month=7;
    int _day=6;
};


//在类外进行重载输出提取符:<< ,这样就没有this指针的干扰,我们可以自己实现参数的设计,让对象放在第二个位置!
void operator<<(ostream& out,const Date& d)   //out就是cout的别名
{
    out << d._year << "/" <<d._month << "/" <<d._day << endl;  //内置类型的输出

}

int main()
{ 
    Date d1;
    d1.print();

    //cout输出基本数据是可以的,但是对于我们自定义的对象类型,这是不允许的,如果想要使用,就必须对输出提取符进行运算符重载
    cout << d1;    //此时,就不会报错了!可以正常输出!   本质:operator<<(cout,d1);

	return 0;
}

总结:

        重载operator<<,然后发现没办法将operator<<重载成成员函数。因为cout的输出流对象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作数了。但是实际使用中cout需要是第一个形参对象,才能正常使用。所以要将operator<<重载成
全局函数。但仅会导致类外没办法访问成员,此时就需要友元来解决。operator>>同理。

此外,为了支持连续输出,即:cout<<d1<<d2; 就要修改代码如下:

#include<iostream>
using namespace std;


class Date
{
public:

    friend ostream& operator<<(ostream& out, const Date& d);   //对重载的函数进行声明
    friend istream& operator>>(istream& _cin, Date& d);

private:
    
    int _year=2024;
    int _month=7;
    int _day=6;
};



//返回值类型修改为引用,避免拷贝构造
ostream& operator<<(ostream& out,const Date& d)   //out就是cout的别名
{
    out << d._year << "/" <<d._month << "/" <<d._day << endl;  //内置类型的输出
    return out;

}


istream& operator>>(istream& _cin, Date& d)
{
     _cin >> d._year;
     _cin >> d._month;
     _cin >> d._day;
    return _cin;
}




int main()
{ 
    Date d1;
    Date d2;
    cout << d1 << d2<<endl;  //连续输出
    cin >> d1 >> d2;


    int x = 1;
    double y = 1.22;

    //为什么C++会自动识别数据的类型,这就是函数重载的作用!
    cout << x;       //函数重载 ostream& operator<<(int val);
    cout << y;       //函数重载 ostream& operator<<(double val);


	return 0;
}

友元函数的特性:

  1. 友元函数可访问类的私有和保护成员,但不是类的成员函数(所以它也没有this指针);
  2. 友元函数不能用const修饰,因为它没有this指针这个东西,const修饰的是this指针指向的内容;
  3. 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
  4. 一个函数可以是多个类的友元函数
  5. 友元函数的调用与普通函数的调用原理相同

4.2 友元类

友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的私有或者保护成员。

class Time
{
   friend class Date;   // 声明日期类为时间类的友元类,则在日期类中就直接访问Time类
中的私有成员变量
public:
 Time(int hour = 0, int minute = 0, int second = 0)
 : _hour(hour)
 , _minute(minute)
 , _second(second)
 {}
   
private:
   int _hour;
   int _minute;
   int _second;
};
class Date
{
public:
   Date(int year = 1900, int month = 1, int day = 1)
       : _year(year)
       , _month(month)
       , _day(day)
   {}
   
   void SetTimeOfDate(int hour, int minute, int second)
   {
       // 直接访问时间类私有的成员变量
       _t._hour = hour;
       _t._minute = minute;
       _t._second = second;
   }
   
private:
   int _year;
   int _month;
   int _day;
  Time _t;
};

五、内部类

5.1 概念

       如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类, 它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越 的访问权限。

      注意:内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。

class A
{
    private:
       static int k;
       int h;
    public:
        class B // B天生就是A的友元
       {
           public:
                void foo(const A& a)
                {
                   cout << k << endl;//OK
                   cout << a.h << endl;//OK
                }
       };

};

int A::k = 1;

int main()
{
    A::B b;
    b.foo(A());
    
    return 0;
}

5.2 特性

  1. 内部类可以定义在外部类的public、protected、private都是可以的。
  2. 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
  3. sizeof(外部类)=外部类,和内部类没有任何关系。

六、匿名对象

class A
{
public:

    A(int a = 0)   //全缺省的默认构造函数
    :_a(a)
   {
      cout << "A(int a)" << endl;
   }

   ~A()          //析构函数
  {
      cout << "~A()" << endl;
  }

private:
    int _a;

};



class Solution
 {
    public:
       int Sum_Solution(int n)
       {
          //...
          return n;
      }
};




int main()
{
 A aa1;
不能这么定义对象A aa1();,因为编译器无法识别下面是一个函数声明,还是对象定义,但是我们可以这么定义匿名对象,匿名对象的特点不用取名字,但是他的生命周期只有这一行,我们可以看到下一行他就会自动调用析构函数。如下:

 A();


 匿名对象在这样场景下就很好用,
 Solution().Sum_Solution(10);  //只有这一行会使用这个创建对象,别人不需要使用。
 return 0;
}

七、拷贝对象时的一些编译器优化

      在传参和传返回值的过程中,一般编译器会做一些优化,减少对象的拷贝,这个在一些场景下还 是非常有用的。

class A
{
    public:
       A(int a = 0)
       :_a(a)
      {
            cout << "A(int a)" << endl;
      }

       A(const A& aa)
       :_a(aa._a)
      {
         cout << "A(const A& aa)" << endl;
      }

      A& operator=(const A& aa)
      {
         cout << "A& operator=(const A& aa)" << endl;
         if (this != &aa)
         {
            _a = aa._a;
         }
          return *this;
      }

     ~A()
     {
         cout << "~A()" << endl;
     }

   private:
         int _a;
};



void f1(A aa)
{

}

A f2()
{
      A aa;
      return aa;
}


int main()
{
     // 传值传参
     A aa1;
     f1(aa1);
     cout << endl;

     // 传值返回
     f2();
     cout << endl;

     // 隐式类型,连续构造+拷贝构造->优化为直接构造
     f1(1);

     // 一个表达式中,连续构造+拷贝构造->优化为一个构造
     f1(A(2));
     cout << endl;
     // 一个表达式中,连续拷贝构造+拷贝构造->优化一个拷贝构造
     A aa2 = f2();
     cout << endl;
     // 一个表达式中,连续拷贝构造+赋值重载->无法优化
     aa1 = f2();
     cout << endl;
     return 0;
}

八、再次理解封装

        C++是基于面向对象的程序,面向对象有三大特性即:封装、继承、多态。
C++通过类,将一个对象的属性与行为结合在一起,使其更符合人们对于一件事物的认知,将属于该对象的所有东西打包在一起;通过访问限定符选择性的将其部分功能开放出来与其他对象进行交互,而对于对象内部的一些实现细节,外部用户不需要知道,知道了有些情况下也没用,反而增加了使用或者维护的难度,让整个事情复杂化。
       下面举个例子来让大家更好的理解封装性带来的好处,比如:乘火车出行

我们来看下火车站:

  1.  售票系统:负责售票---乘客凭票进入,对号入座。
  2.  工作人员: 售票. 咨询,安检、保全、卫生等
  3.  火车:带乘客到目的地

     火车站中所有工作人员配合起来,才能让大家坐车有条不紊的进行,不需要知道火车的构造,票务系统是如何操作的,只要能正常方便的应用即可。想想下,如果是没有任何管理的开放性站台呢?火车站没有围堵。站内火车管理调度也是随意,乘车也没有规矩,比如:

九、再次理解面向对象

       现实生活中的实体计算机并不认识,计算机只认识二进制格式的数据。如果想要让计算机认识现实生活中的实体,用户必须通过某种面向对象的语言,对实体进行描述,然后通过编写程序,创 建对象后计算机才可以认识。比如想要让计算机认识洗衣机,就需要:

  1. 用户先要对现实中洗衣机实体进行抽象---即在人为思想层面对洗衣机进行认识,洗衣机有什么属性,有那些功能,即对洗衣机进行抽象认知的一个过程
  2.  经过1之后,在人的头脑中已经对洗衣机有了一个清醒的认识,只不过此时计算机还不清楚,想要让计算机识别人想象中的洗衣机,就需要人通过某种面向对象的语言(比如:C++、 Java、Python等)将洗衣机用类来进行描述,并输入到计算机中
  3. 经过2之后,在计算机中就有了一个洗衣机类,但是洗衣机类只是站在计算机的角度对洗衣机对象进行描述的,通过洗衣机类,可以实例化出一个个具体的洗衣机对象,此时计算机才能知道洗衣机是什么东西。
  4. 用户就可以借助计算机中洗衣机对象,来模拟现实中的洗衣机实体了。

   

 在类和对象阶段,大家一定要体会到,类是对某一类实体(对象)来进行描述的,描述该对象具有那些属性,那些方法,描述完成后就形成了一种新的自定义类型,才用该自定义类型就可以实例化 具体的对象。面向对象更好的去模拟描述这个世界,而实际工程项目写代码,本质上就是模拟现实世界的运转。面向对象更关注的类和类之间的关系。工程中要求低耦合(类和类之间的关系越少越好),高内聚(相关的都要放到类的里面,封装起来)。工程项目才好维护扩展。

至此,类和对象的全部内容就学习完毕,内容相对较多,一定要及时复习巩固!C++相对来说较为复杂,我们应该时刻理清自己的思路,耐下心来,一点点积累, 星光不问赶路人,加油吧,感谢阅读,如果对此专栏感兴趣,点赞加关注! 

  • 23
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

未来可期,静待花开~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值