目录
constructor(ctor,构造函数)被放在private区
const member functions(常量成员函数)
参数传递:pass by value vs. pass by reference(to const)
返回值传递:return by value vs. return by reference(to const)
相同class的各个objects互为friends(友元)
operator overloading(操作符重载-1,成员函数包含this指针)
operator overloading(操作符重载-2,非成员函数,无this指针)
temp object(临时对象):“typename();”
本篇接着上篇博客继续进行续记。
Header(头文件)的布局
类声明部分
constructor(ctor,构造函数)被放在private区
构造函数是通过对应的类创建对象的时候,自动调用的,如果把其放在了private区域,那么默认其不能被外界调用的,这就会出现矛盾,所以不能通过这种生硬的方式直接将构造函数放在private区域。
那是不是这就意味着不要把构造函数放在private区域了?也不是,这有独特的应用场景,比如,当我不希望为外界创建这个对象的时候,在Singleton(单体)设计模式中,意思是说,通过我所设计的这个A类给外界所设计的对象,外界只能创建一份,即强制要求我这个类所创建的对象只能为一份:
那么这种场景下,就是在private区中创建了一个构造函数,那稍微再引申一下,那如何在不能调用构造函数的情况下去创建独有的对象呢?外界可以通过"A::getInstance().setup();"的方式,去调用A类中被规定为"static"的getInstance()函数去创建唯一的对象。所以,通过这个场景,希望你明白,在C++中,确实有一种需求是要把构造函数放在private区域中的。
const member functions(常量成员函数)
这里有个被很多人忽略的规范:要在函数的后头(小括号"()"的后面,大括号"{}"的前面),加关键字"const"
这是什么意思呢?以上面的real函数和image函数为例,这两个函数要完成取得复数实部跟虚部的任务,所以,这两个函数并不会改变这两个数据的,只是把这俩数据给取出来,那么这就又引出了一个区分类中的函数的标准:按是否会改变类中的数据来进行区分,一种是会改变类中的数据的,另一种是不会改变类中的数据的。不会改变数据内容的函数,一定要加上const关键字!如果一个函数不会改变数据,那这个函数一定要加上const!
这个的应用场景是,比如你今天在设计接口,那么通过会改变数据,和不会改变数据先给区分出来,那么当进行实际函数定义的时候(可能是一个礼拜之后的事情了),就可以清晰的知道,那些函数是可以改变值的,那些是不可以的!
那如果该加const的时候,如果不加const会有什么后果呢?还是从编译器的角度去考虑,如果是如下方式调用的:
这样是没问题的,因为是正常的调用。
再看另一种调用的方式,在外界直接声明const,说明我调用的这个函数的值是不可改变的:
但是,当运行到内部的时候,编译器发现,函数内部的函数没有加const,说明内部函数是运行更改值的,那么,外界说不可更改,内部说允许更改,这不就矛盾了吗?遇到这种情况,编译器就会报错了!
参数传递:pass by value vs. pass by reference(to const)
如图上的标示,一个就是通过值传递,一个是引用传递。
值传递,就是整包传递,把所有的值都传给进去,如果值非常大,那把所有的字节(很多字节)给都传进去就效率非常低,在C中,可以传指针(相当于四个字节),而在C++中,可以传引用。传引用就相当于传指针,但引用的形式是很漂亮的!传引用速度很快!
最好所有的参数传递都传引用!,当然如过变量只有一个字节,或者两个字节,少于传递引用所用的四个字节,传值也是可以的,但这是小众了就。
那么再接着往下想,传引用的确是比传值的速度更快,但是,如果我传的值不想被更改的话,我是在函数中再创建一个变量来存放这个值,虽然慢,但我函数中如果更改这个值,是不会影响到函数外的这个值的,但是,传引用(指针)的话,我如果在函数中更改了值,那么函数外的值也会联动的被进行相应的更改,那其不是传引用在安全性上不让传值吗?
这样考虑是对的,为了解决这个问题,我们需要在引用前加"const"关键字,来让传进去的引用值,为只读属性,只能读,不能改。这样速度和安全性就兼得了!
比如,下面是一个重载的例子,单看函数名,就可以知道,os变量要在函数中被修改,而x变量在函数中仅作调用使用,甚至编程老鸟可以通过这种判断,一眼就可以看出一组函数接口各自变量所充当的角色!
返回值传递:return by value vs. return by reference(to const)
同样,在返回值传递的时候,也是应该用引用的,理由跟上面一样。
需要强调的是返回值也好,值也好,都是尽量鼓励在适合的场景用引用的方式传递,但并非所有的场景都需要,要根据实际情况来判断。
friend(友元)
类的友元函数可以取类中的数据来用:
那么,在函数外,__diap1函数就可以去拿类中的私有成员变量,re和im:
友元的存在,其实是打破了函数的封装属性。
相同class的各个objects互为friends(友元)
当位于一个类中的多个函数,可以互相进行调用彼此的数据,这是为什么呢?假设在这个复数类中有个函数func,这个函数可以获得传进来的一个复数的实部和虚部,通过这种直接拿的方式来使用:
外界调用:
这样的话,不是破坏了封装性吗?而且,这种方式也没有友元的关键字,这是为什么呢?答案就是:相同class的各个objects互为friends(友元)。
类声明部分代码
class complex
{
public:
complex (double r = 0, double i = 0): re (r), im (i) { }
complex& operator += (const complex&);
complex& operator -= (const complex&);
complex& operator *= (const complex&);
complex& operator /= (const complex&);
double real () const { return re; }
double imag () const { return im; }
private:
double re, im;
friend complex& __doapl (complex *, const complex&);
friend complex& __doami (complex *, const complex&);
friend complex& __doaml (complex *, const complex&);
};
类定义部分
class body外的各种定义(definitions)
区分什么时候返回引用,什么时候返回值。举个例子:
看上面的这个函数,中间是进行一个“+=”的操作,那么它的逻辑是将第二个变量的值加到第一个变量的值上,最后和的结果,也是存在第一个变量的内存上,那么这个过程中,没有划分新的内存去存储加和的结果;
那么,如果是执行的“+”操作呢?那么就是第一个变量的值加上第二个变量的值,它们的结果,要存放在一个新开辟的内存空间上面,这个新开辟的内存空间的生命周期,是跟所在的函数的生命周期一致的,当函数消亡的时候,其对应的内存空间也就被析构掉了。
反过头来,再说我们最开始讨论的什么时候应该返回引用,什么时候应该返回值,对于第一种情况,就应该返回引用,因为第一个变量的内存空间并不是在当前这个函数中创建的,而是在进入函数前就创建好的,所以,即便这个当前函数析构了,也不会受到影响,所以,应该返回一个引用。
而对于第二种情况,如果返回引用的话,相当于返回一个在函数中新创建的一个地址,这个地址的内容是所存储的加和的值,那么当函数结束,所存储的值也会被析构,如果返回引用的话,只会得到一个被析构后的内存空间值,所以这种情况不能返回引用,要返回值。
operator overloading(操作符重载-1,成员函数包含this指针)
所有的成员函数都带着一个隐藏的参数,一个是this指针(谁调用这个函数,this就指向谁),不可以在参数里面写出来,但可以直接在函数中写出来用。
用引用的好处:传递者无需知道接收者是以引用形式接收
下面举个例子:
这个例子中出现了非常奇特的现象,返回的是一个指针指向的内容(传递者),或者可以理解为是一个值,而函数名中定义的返回形式是一个引用(接收者),这两个是不对营的,这样可以吗?答案是可以的,这便是个用引用来充当接收者的一个好处:传递者无需知道接收者是以引用形式接收。如果用指针来作为接收者,那传递的时候,必须跟接收者对应,才可以。那么这样对使用的人就非常方便了,就不用考虑传递者和接收者之间的关系,之间用就可以了。
那么,为什么"+="的这个重载操作函数的返回值不可以设定为"void"呢?这是,因为,如果使用者进行链式赋值的时候,就如上面的:“c3+=c2+=c1;”,先是把c1的值加到c2中,再把c2的结果加到了c3上,最后,要返回的结果是c3所存的变量,如果设置返回类型为"void",那不就没法返回值了嘛!因为把c1的结果加到c2上去之后,c2返回的是一个void,那么不就加到c3的时候是一个空值吗?细细体会一下可以发现,再编写函数的时候,要把所有的情况都给考虑到是非常重要且必要的。
operator overloading(操作符重载-2,非成员函数,无this指针)
如果执行下面的语句:
那编译器就开始找对应的函数来进行处理,至于编译器到底是先找成员函数还是先找非成员函数,我们是不知道的, 但是先找谁都无所谓,因为对于编译器而言,只能有一个是合适的,如果两个函数都合适,那编译器就会因为无法区分清楚而报错。那么对应的其实是有三个版本的,那对应的究竟那个是合适的呢?
那么上面是三种重载函数,为了应对使用者不同场景的需要。需要注意的是,这种是非成员函数,是定义在类之外的,通过设定为内联函数的方式来进行的,所以,这种方式定义到函数,不会涉及到this指针的使用。
那么我们接着看,为什么在这种场景下,不返回一个引用类型,而都是返回的值呢(蓝色标亮的部分),答案也很简单,因为在者三个函数中执行的是“+”操作,而不是“+=”操作,这就意味着,在这三个函数中,加和的结果是要新开辟一个内存来进行存储的,在函数快结束时,把这个加和的值给返回出去,然后这个新开辟出来的内存空间就可以随着函数的消亡而消亡了,所以只能返回值,而不能返回引用。
temp object(临时对象):“typename();”
那么接着上面的思路再细究,不是说要“新开辟一个内存来进行存储”的吗?为啥没见到程序里面,有新建什么变量出来呢?这就牵扯到临时对象的概念了(对很多人而言,这个语法是陌生的,但这个在标准库里面是非常容易见到的),仔细对照上面的三个函数的return返回语句可以发现,它们都是“typename();”的形式,这其实就是一个创建临时变量的语法,就类似于"int(i)"这样一种语法,创建一个临时对象(临时创建出来的对象,也不想给他名称)出来。那么这个临时对象具体放的数据,就是括号里面数据计算之后的结果了。
应该强调的是临时对象的生命周期只有一行,用于return的时候,临时对象就为了返回一个值而存在,这种场景和合适。
其实临时对象在语法编程中很常见,就比如上面的“Complex();”这条语句其实也是创建一个临时对象,Complex是一个类名,也是一个typename,括号内容为空,说明会导入默认值进来。只不过,其生命周期很短,当进行到下一行的时候,临时对象就被析构了。同理,“Complex(4,5);”也是如此,而“Complex c1(2,1);”因为有名称c1,不会再第二行就被析构了。
取反操作:negate
“+"不仅可以表示加号,还可以表示正号,在被使用的时候,通过靠参数的个数来表现到底是加号还是正号。这个地方其实在第一个函数的时候,返回引用更好!(标准库也是有错误的)
特殊符号的重载:”<<“操作符的重载
设计的一个共轭复数:"conj()"吗,用"<<"输出出来,但如果连用的话,发现并不是我想要的那种效果,所以需要重载"<<"。
重载的过程,发现ostream前不能加const,因为ostream是标准库,当每次输出的时候,都在改变cout(os)的状态,下面再看返回的类型,因为输出到屏幕就是结果,没在乎还会传回什么结果,所以,按照常理返回值”void“也是可以的,但如果使用者会连续输出,进行链式编程,那么就不形了,可见不能写成“void”。
类定义部分代码
inline complex&
__doapl (complex* ths, const complex& r)
{
ths->re += r.re;
ths->im += r.im;
return *ths;
}
inline complex&
complex::operator += (const complex& r)
{
return __doapl (this, r);
}
inline complex&
__doami (complex* ths, const complex& r)
{
ths->re -= r.re;
ths->im -= r.im;
return *ths;
}
inline complex&
complex::operator -= (const complex& r)
{
return __doami (this, r);
}
inline complex&
__doaml (complex* ths, const complex& r)
{
double f = ths->re * r.re - ths->im * r.im;
ths->im = ths->re * r.im + ths->im * r.re;
ths->re = f;
return *ths;
}
inline complex&
complex::operator *= (const complex& r)
{
return __doaml (this, r);
}
inline double
imag (const complex& x)
{
return x.imag ();
}
inline double
real (const complex& x)
{
return x.real ();
}
inline complex
operator + (const complex& x, const complex& y)
{
return complex (real (x) + real (y), imag (x) + imag (y));
}
inline complex
operator + (const complex& x, double y)
{
return complex (real (x) + y, imag (x));
}
inline complex
operator + (double x, const complex& y)
{
return complex (x + real (y), imag (y));
}
inline complex
operator - (const complex& x, const complex& y)
{
return complex (real (x) - real (y), imag (x) - imag (y));
}
inline complex
operator - (const complex& x, double y)
{
return complex (real (x) - y, imag (x));
}
inline complex
operator - (double x, const complex& y)
{
return complex (x - real (y), - imag (y));
}
inline complex
operator * (const complex& x, const complex& y)
{
return complex (real (x) * real (y) - imag (x) * imag (y),
real (x) * imag (y) + imag (x) * real (y));
}
inline complex
operator * (const complex& x, double y)
{
return complex (real (x) * y, imag (x) * y);
}
inline complex
operator * (double x, const complex& y)
{
return complex (x * real (y), x * imag (y));
}
complex
operator / (const complex& x, double y)
{
return complex (real (x) / y, imag (x) / y);
}
inline complex
operator + (const complex& x)
{
return x;
}
inline complex
operator - (const complex& x)
{
return complex (-real (x), -imag (x));
}
inline bool
operator == (const complex& x, const complex& y)
{
return real (x) == real (y) && imag (x) == imag (y);
}
inline bool
operator == (const complex& x, double y)
{
return real (x) == y && imag (x) == 0;
}
inline bool
operator == (double x, const complex& y)
{
return x == real (y) && imag (y) == 0;
}
inline bool
operator != (const complex& x, const complex& y)
{
return real (x) != real (y) || imag (x) != imag (y);
}
inline bool
operator != (const complex& x, double y)
{
return real (x) != y || imag (x) != 0;
}
inline bool
operator != (double x, const complex& y)
{
return x != real (y) || imag (y) != 0;
}
#include <cmath>
inline complex
polar (double r, double t)
{
return complex (r * cos (t), r * sin (t));
}
inline complex
conj (const complex& x)
{
return complex (real (x), -imag (x));
}
inline double
norm (const complex& x)
{
return real (x) * real (x) + imag (x) * imag (x);
}
前置声明部分代码
class complex;
complex&
__doapl (complex* ths, const complex& r);
complex&
__doami (complex* ths, const complex& r);
complex&
__doaml (complex* ths, const complex& r);
如何区分程序员是不是有良好的编程习惯?
- 数据要放在private里面
- 参数尽可能的用引用来传,看状况加const
- 返回值尽可能用引用来传
- 在类的本体中,应该加const的,就要加,如果不加,使用者使用的时候,编译器会报错
- 构造函数的特殊写法(使用冒号的那种)