const成员,流插入,流提取重载,初始化列表! 流插入,流提取的重载(6千字长文详解!)

时间:2022-12-20 16:18:12

c++详解之const成员,流插入,流提取重载,初始化列表!

<< 流插入 和 >> 流提取的重载

#include <iostream>
using namespace std;
int main()
{
    int a = 0;
    double b = 1.1111;
    char c = w;
    cout << a << b << c <<endl;
    return 0;
}

我们使用流插入的时候可以看出来 << 可以兼容多种的类型,其实这个本质上就是一个函数重载和运算符重载!

const成员,流插入,流提取重载,初始化列表! 流插入,流提取的重载(6千字长文详解!)

我们可以看到官方库里面就内置了内置类型的输入流重载!但是如果我们想要对自定义类型使用<<的我们就得自己手动的对输入流进行重载!

例如日期类

//date.h
#include <iostream>
using namespace std;
class Date
{
public:
	Date(int year = 10, int month = 10, int day = 10)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};
void operator<<(const Date& d, ostream& out)
{
	out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
}
class Date
{
public:
	Date(int year = 10, int month = 10, int day = 10)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void operator<<(ostream& out)
	{
		out << _year << "年" << _month << "月" << _day << "日" << endl;
	}//cout是ostream类的函数!
private:
	int _year;
	int _month;
	int _day;
};
//date.cpp
int main()
{
	Date d1;
	Date d2;
	cout << d1 << endl;
	d1 << cout;
	d1.operator<<(cout);

	return 0;
}

const成员,流插入,流提取重载,初始化列表! 流插入,流提取的重载(6千字长文详解!)

我们会发现如果直接在类里面怎么写会导致正常我们的使用会出错 cout必须在右边才行!

这是因为this指针默认在左边!如果我们要在类里面使用则必须反正使用!

所以 为了符合我们正常的使用习惯我们一般都会将>> 和<< 的重载定义在类外面!

//date.h
#include <iostream>
using namespace std;
class Date
{
public:
	Date(int year = 10, int month = 10, int day = 10)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	int _year;
	int _month;
	int _day;
};
void operator<<(ostream& out,const Date& d)
{
	out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
}

//date.cpp
#include "date.h"
int main()
{
	Date d1;
	Date d2;
	cout << d1 << endl;
	d1 << cout;
	d1.operator<<(cout);

	return 0;
}

// date2.cpp
#include "date.h"

但是定义在类外面我们会遇到一个很尴尬的问题!我们无法访问内置成员变量!为了方便演示我们姑且将内置成员共有化!后面我们会说解决办法!

但是这引入了一个问题当我们有多个源文件引用了这个头文件会出现

const成员,流插入,流提取重载,初始化列表! 流插入,流提取的重载(6千字长文详解!)

这是什么原因?答案是因为头文件会在每一个引用它的源文件中展开!也就是说这个operator<<函数重载出现多次,导致发生了重定义!

这里要区分一下命名空间是解决同名问题!不能用来解决重定义问题!

解决的办法有三种

  1. 使用静态的函数
//date.h
static void  operator<<(ostream& out,const Date& d)
{
	out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
}

因为static可以改变函数的外部链接属性,使得这个函数在当前源文件可见!!这样子这个函数就不会进入符号表!也就不会发生链接问题!

  1. 声明和定义分离!

在头文件声明!定义放在其他源文件那边!

只有定义的函数才会被放入符号表中!无论头文件展开在多次都可以使用!

//date.h
void operator<<(ostream& out,const Date& d);

// date2.cpp
#include "date.h"
void operator<<(ostream& out,const Date& d)
{
	out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
}
  1. 使用inline 修饰为内联函数

加了inline之后就不会加入符号表,而且可以使这种可能频繁调用且短的函数变成内联函数,避免建立栈帧使的性能损耗

//date.h
inline void operator<<(ostream& out,const Date& d)
{
	out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
}

==从上面我们也可以得出一个结论那就是对于全局函数或者全局变量我们不能放在头文件里面否则一旦头文件多次引用就会发生重定义!==

解决共有成员函数问题

使用友元

//date.h
#include <iostream>
using namespace std;
class Date
{
	friend void operator<<(ostream& out,const Date& d);
public:
	Date(int year = 10, int month = 10, int day = 10)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};
inline void operator<<(ostream& out,const Date& d)
{
	out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
}

友元是c++留的一个后门,允许突破封装直接让外部的函数访问成员变量!

只要在类的==任意位置==进行声明即可!

或者可以在类里面定义一个公共成员函数用来专门获取内部成员变量!

例如getyear() ,getmonth(),getday()....

链式访问的问题

从上面来看我们的流插入基本是完成了最后剩下的问题就是如果遇到

 cout  << d1 << d2<<endl;

这种情况的时候,如果我们使用的是void类型作为返回值会报错,为了解决这个问题我们使用cout的类作引用返回!

//date.h
inline ostream& operator<<(ostream& out,const Date& d)
{
	out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
    return out;
}//cout是全局函数出了全局作用域后仍然可以存在,所以可以使用引用返回

>> 流插入的重载

流插入和流提取要注意的点是一样的!

//date.h
#include <iostream>
using namespace std;
class Date
{
	friend istream& operator>>(istream& in, const Date& d);
public:
	Date(int year = 10, int month = 10, int day = 10)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};
inline istream& operator>>(istream& in,Date& d) 
{
	in >> d._year>> d._month >> d._day;
}
//实际调用就会转换成
//operator>>(cin,d);

const 成员

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

//date.h
class Date
{
public:
	Date(int year = 10, int month = 10, int day = 10)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void print()
	{
		cout << _year << " " << _month << " " << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
//date.cpp

int main()
{
	Date d1;
	const Date d2;
	d1.print();
	d2.print();//这个无法运行!
	return 0;
}

const成员,流插入,流提取重载,初始化列表! 流插入,流提取的重载(6千字长文详解!)

会发生这样的问题的原因是因为发生了权限的扩大!

在类里面this指针的全貌为 Date* const this,后面的const是用来修饰this保证this本身不被修改!所以this指针的真实类型为 *Date **

d2.print();//等价于
print(&d2);

此时d2的类型为 const Date ,则d2的指针类型为**const Date* **

将const Date *传给Date * 这就发生了权限的放大!

那我们该如何解决这个问题呢?我们都知道this指针的参数都是由编译器自行添加的!我们不可以手动的进行显示添加!

所以这就引入了const成员!

class Date
{
public:
	Date(int year = 10, int month = 10, int day = 10)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void print() const//const成员!
	{
		cout << _year << " " << _month << " " << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2022,1,1);
	const Date d2(2022,1,2);
	d1.print();
	d2.print();
	return 0;
}

const成员,流插入,流提取重载,初始化列表! 流插入,流提取的重载(6千字长文详解!)

我们发现不仅d2可以使用了,而且d1也仍然可以使用!

这是因为指针虽然不能从**const Date *传给Date ***

但是可以 **Date *传给const Date ***,原理是因为==权限可以缩小但是不能放大!==

	void print() const//只用来修饰this指针!
	{
		cout << _year << " " << _month << " " << _day << endl;
	}

==记住一点 const是用来修饰this指针的!不会修饰其他函数变量!==

修饰后的this指针的全貌为 const Date * const this ,类型为**const Date ***

	void print(int* x) const
	{
		cout << _year << " " << _month << " " << _day << endl;
	}

==x的类型仍然为int* 不会变成 const int*==

const在类函数中的普遍使用

我们经常看到在类成员函数中const漫天飞的普遍情况,原因就是因为只要不涉及对于类本身的修改,使用const成员可以是的类的成员函数有更好的泛用性!

例如日期类涉及比大小的运算符重载例如

class Date
{
public:
    bool operator==(const Date& d)const;
	bool operator>(const Date& d)const;
	bool operator >= (const Date& d)const;
	bool operator < (const Date& d)const;
	bool operator <= (const Date& d)const;
	bool operator != (const Date& d)const
	Date operator-(int day)const;
	Date operator+(int day)const;
private:
	int _year;
	int _month;
	int _day;
};

如果不加const的反例

class Date
{
public:
    bool operator < (const Date& d);
    bool operator>(const Date& d);
    int operator-(const Date& d) const
	{
		if (*this < d)
        {
            ///...
        }//这样写没有问题!
        if (d > *this)
        {
            ///....
        }
        //这样写会报错!
	}
private:
	int _year;
	int _month;
	int _day;
};

因为这样会变成!operator>(d, this) 发生了经典的权限放大!因为第一个类型为const Date 但是operator>的第一个参数类型为Date* 后面加 const 使得this的类型变成cosnt Date* 就让使用范围更广了!**

==总结凡是内部不改变成员变量,也就是*this对象数据的,这些成员函数都应该加const==

取地址重载

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

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


int main()
{
	Date d1(2022,1,1);
	const Date d2(2022,1,2);
	Date* d3 = &d1;
	const Date* d4 = &d2;

	return 0;
}

这两个一般不用写,编译器会自动默认生成!

只有特别特殊的要求才可能写——比如不让某个类的对象取地址!

   Date* operator&()
	{
		return NULL;
	}
	const Date* operator&()const
	{
		return NULL;
	}

可以重载成这样的样子!

再谈构造函数——初始化列表

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

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

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

初始化列表

我们首先来看初始化列表的格式

class Date
{
public:
	Date(int year = 10, int month = 10, int day = 10)//可以全部都是初始化列表
		:_year(year),
		_month(month),
		_day(day)
	{
	}
private:
int _year;
int _month;
int _day;
};

class stack
{
public:
    //..
	stack(int newcapcacity = 4)
		:_capacity(newcapcacity),
		_top(0)
	{
		_a = (int*)malloc(sizeof(int) * newcapcacity);
		if (_a == nullptr)
		{
			perror("malloc fail");
			exit(-1);
		}
		memset(_a, 0, sizeof(int) * _capacity);
	}//可以初始化列表和构造函数混合使用!
    //..
private:
	int* _a;//都是声明!
	int _top;
	int _capacity;
}

那么初始化列表有什么用呢?

什么情况下需要用到初始化列表呢?

1.const 成员变量

class B
{
public:  
	B()
	{
		_n = 10;
	}
private:
	const int _n;
};

上面的代码我们会发现我们无法在析构函数的内部初始化_n!

const成员,流插入,流提取重载,初始化列表! 流插入,流提取的重载(6千字长文详解!)

因为我们只有在定义的时候才能对const类型的变量进行初始化!

class B
{
private:
	const int _n;//这个位置是声明!不是定义!
};

class stack
{
public:
///......
private:
	int* _a;//都是声明!
	int _top;
	int _capacity;
};
int main()
{
	const int N;
const int N1 = 10;//定义时!

stack st1;//这是对象的实例化!是一个类的整体的定义!
stack st2;
	return 0;
}

const成员,流插入,流提取重载,初始化列表! 流插入,流提取的重载(6千字长文详解!)

我们一般定义一个const的变量的时候会被要求进行初始化

而在类域里面的变量都是声明,而不是初始化!

==所以类里面的成员变量究竟是在哪里进行一个个的定义初始化的?==——初始化列表!

c++之所以给初始化列表就是为了解决这些必须在定义时候才能初始化的变量!

  • 所有成员的初始化要经过初始化列表定义!!
  • 即使没有显性的在初始化列表里面写也会在初始化列表进行初始化!
class B
{
public:
	B()
		:_n(10)//不写这个就会报错
	{
	}
private:
	const int _n;
    int _m;
};

const成员,流插入,流提取重载,初始化列表! 流插入,流提取的重载(6千字长文详解!)

如果不在初始化列表写 _n 就会像上面一样报错!因为没有在n定义的时候进行初始化!

对于没有在初始化列表显示初始化的变量

  1. 内置类型变量_m 即使没有在初始化列表写,也会在初始化列表进行定义初始化为一个随机值!

  2. ==没有显示的写在初始化列表自定义类型==也会经过初始化列表会去==调用它的默认构造函数来这里定义初始化==!==如果没有默认构造函数就会报错!==

初始化为一个随机值算是设计时候的一个缺陷,后期c++为了弥补这个缺陷引入了缺省值!

缺省值是如何在初始化列表进行作用的呢?

class B
{
public:
	B()
		:_n(10)
	{
	}
private:
	const int _n;
    int _m = 10;//缺省值
};
  • 如果我们在初始化列表显性的写了具体的值那么就会执行我们填入的值

  • 如果我们不写,缺省值就会在初始化列表起作用,使用这个缺省值进行定义初始化!

    const成员,流插入,流提取重载,初始化列表! 流插入,流提取的重载(6千字长文详解!)

2.没有默认构造函数的自定义类型变量

class A
{
public:
	A(int a)//没有默认构造!
		:_a(a)
	{}
private:
	int _a;
};
class B
{
public:
	B()
		:_n()
		,_m(100)
	{
	}
private:
	const int _n;
	int _m = 10;
    A _aa;
};
int main()
{
	B b;
	return 0;
}

const成员,流插入,流提取重载,初始化列表! 流插入,流提取的重载(6千字长文详解!)

这种情况A就必须走初始化列表!_a就无法初始化,因为没有默认构造让他调用就必须要显示的写出来!

class A
{
public:
	A(int a)
		:_a(a)
	{}
private:
	int _a;
};
class B
{
public:
	B()
		:_n()
		,_m(100)
		,a(1)
	{
	}
private:
	const int _n;
	int _m = 10;
	A _aa;
};
int main()
{
	B b;

	return 0;
}

const成员,流插入,流提取重载,初始化列表! 流插入,流提取的重载(6千字长文详解!)

**如果我们不写 A 类的构造函数仅仅使用A的默认构造函数,也是可以的,B在初始化列表初始化 _aa 的时候会去调用A类的默认构造初始化 _aa。而又在A类的默认构造函数下面的初始化列表下 _a被初始化成了一个随机值 **

class A
{
public:
private:
	int _a;
};
class B
{
public:
	B()
		:_n()
		,_m(100)
	{
	}
private:
	const int _n;
	int _m = 10;
	A _aa;
};
int main()
{
	B b;
	return 0;
}

const成员,流插入,流提取重载,初始化列表! 流插入,流提取的重载(6千字长文详解!)

再我们认识了初始化列表后让我们看一下下面的代码

class stack
{
public:
	stack(int newcapcacity = 4)
		:_capacity(newcapcacity),
		_top(0)
	{
		_a = (int*)malloc(sizeof(int) * newcapcacity);
		if (_a == nullptr)
		{
			perror("malloc fail");
			exit(-1);
		}
		memset(_a, 0, sizeof(int) * _capacity);
	}
	void Push(int x);
private:
	int* _a;
	int _top;
	int _capacity;
};

class MyQuene
{
public:
	MyQuene()
	{}
    //这样写是没有问题的!
    //因为我们没有在初始化列表显性的写所以它会去调用stack类的默认构造去初始化!
    //如果没有了stack的默认构造就会出现报错!
	void Push(int x)
	{
		_PushST.Push(x);
	}
	//......剩下读者可以加自己实现
private:
	stack _PopST;
	stack _PushST;
};

存在stack类默认构造的情况const成员,流插入,流提取重载,初始化列表! 流插入,流提取的重载(6千字长文详解!)

stack没有默认构造!const成员,流插入,流提取重载,初始化列表! 流插入,流提取的重载(6千字长文详解!)

使用初始化列表!

const成员,流插入,流提取重载,初始化列表! 流插入,流提取的重载(6千字长文详解!)

3.引用类型的成员函数

引用和const成员一样,只能在定义的时候初始化!*-

class B
{
public:
	B()
		:_n()
		, _m(100)
	{
	}
private:
	const int _n;
	int _m = 10;
 int& _c;
};

const成员,流插入,流提取重载,初始化列表! 流插入,流提取的重载(6千字长文详解!)

==所以引用类型的初始化也必须在初始化列表里面!==

class B
{
public:
	B(int x = 10)
		:_n()
		, _m(100)
		,_c(x)
	{
	}
private:
	const int _n;
	int _m = 10;
	int& _c;
};

const成员,流插入,流提取重载,初始化列表! 流插入,流提取的重载(6千字长文详解!)

==总结:==

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

初始化列表的初始化顺序

成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关

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

const成员,流插入,流提取重载,初始化列表! 流插入,流提取的重载(6千字长文详解!)

我们可以看出来 因为先初始化的是 _a2 而不是 _a1所以a2变成了随机值

class A
{
public:
    A(int a)
        :_a1(a)
        , _a2(_a1)
    {}

    void Print() 
    {
        cout << _a1 << " " << _a2 << endl;
    }
private:
    int _a1;
    int _a2;
};
int main() 
{
    A aa(1);
    aa.Print();
}

const成员,流插入,流提取重载,初始化列表! 流插入,流提取的重载(6千字长文详解!)