本篇笔记以复数类(不含指针的类)为例进行面向对象的学习
=========================================================
复数类的声明:
1
class complex
2
{
3
public:
4 complex (double r = 0, double i = 0): re (r), im (i) { }
5 complex& operator += (const complex&);
6 complex& operator -= (const complex&);
7 complex& operator *= (const complex&);
8 complex& operator /= (const complex&);
9
double real () const { return re; }
10
double imag () const { return im; }
11
private:
12
double re, im;
13
14 friend complex& __doapl (complex *, const complex&);
15 friend complex& __doami (complex *, const complex&);
16 friend complex& __doaml (complex *, const complex&);
17 };
=========================================================
一、构造函数
1)C++列表初始化
complex(double r = 0,double i = 0)
:re(r),im(i)
{}
- 一般情况下,构造函数写在public区域. 一个例外情况是Singleton设计模式.
- 构造函数没有返回值.
- 只有构造函数可以使用 : () 构成的列表初始化语法.
- C++的初始化工作可以分为创建阶段和操作阶段.创建阶段是在列表初始化结束之后就结束了;在大括号中的所有操作都是对象已经被创建之后再去进行的操作.理论上讲,可以栽{}使用assign操作代替列表初始化.然而,这样做相当于是放弃了创建阶段的操作.此外,如果对象中含有const变量,是不能在创建阶段结束后再去修改的,这种情况下必须使用列表初始化
2) 构造函数的调用方法
构造函数不显式调用,有以下三种方法。
- complex C1(2,3);
- complex C2;//在构造函数定义中,实部和虚部均有默认值0;
- complex* C3 = new complex(4);//这里并不是进行了一次显式调用,而是声明了一个匿名对象;声明匿名对象的方式就是采用之前的两种方法,区别在于其不存在变量名。如果不是通过new创建的堆变量,栈中的匿名对象的生命周期仅为当前行。
3) 构造函数的重载
在C语言中不允许出现同名函数。但是在C++中,允许函数同名,但必须保证根据参数的不同可以找到唯一的一个函数实体与之对应,不能产生二义性。与大部分成员函数一样,构造函数可以被重载。
* 在编译时,对于重载函数,编译器会给予其一个real name,例如complex::complex@doubledouble,其中包含的参数的类型信息。
也就是说,重载后的函数实际上名字是不同的。
* 当构造函数具有默认值是,应特别注意是否会产生二义性。
4) 默认构造函数
当没有定义构造函数时,C++会提供一个默认构造函数。默认构造函数不接受任何参数,不进行任何操作。
只要用户定义了一个构造函数,不论其参数为何,C++将不再提供默认的构造函数。
5) 构造函数在private区时的情况(Singleton设计模式)
1
class A{
2
public:
3
static A& getinstance();
4
setup() {...};
5
private:
6
A();
7 A(const A & rhs);
8
...
9
};
10
11 A& A::getinstance()
12
{
13
static A a;//静态变量
14
return a;
15 }
调用方法:
A::getinstance().setup();
//这种设计模式下,永远只有一个实例。
二、const 关键字
double real () const { return re; }
在参数表后的const,表示当前函数不会改变对象内部成员变量的值。
complex& operator += (const complex&);
在参数前的const,表示当前参数对于成员函数是一个常量变量,且不会被改变。
* 返回值也可以被指定为const类型。
不加const会产生的后果:
e.g.
const complex C1(2,1);
cout<<C1.real();
如果在real函数定义时没有加上const关键字,C1.real()就不能执行,因为只看这一接口无法保证C1的数据成员不会被real函数修改。
三、 传值
三种传值方案:值传递、引用传递、const引用传递
1)参数传递
- 尽量不使用值传递的传递方案,因为这样复制的效率太低了。引用是C++中提供的用来代替指针的一种方案。相比于指针,其好处是使用更简单,用户可以像值传递时一样使用它,而不用考虑其背后的实现方法;而使用指针时,用户必须知道这是一个指针,又涉及了&操作和*操作,不易于使用并且很容易误操作(指针使用不当产生的内存泄漏、溢出等问题)。
- const 引用传递的目的在上一小节中已经解释过,就是为了解决常量对象的调用问题。
- 在传参时,建议不论什么参数都采用引用传递的方式。
2)返回值传递
- 仍然推荐采用引用传递,但不再是不论什么情况都采用引用传递。
- 采用引用传递的另一个好处是返回值可以作为左值继续进行操作。如a+b+c。
- 不能返回引用的情况:
- 变量为栈空间内的局部变量。
- 要返回的对象为一const对象。这种情况下,引用可能会带来对象内容的更改,所以只能采用值传递或const引用传递
Appendix——关于引用:
1)单纯引用的语法
&ref = obj;
ref成为obj的别名。作为参数传递时,就可以按照这种方式理解。(实际上并不是这样的操作,传参相当于构造,是在第一阶段完成的,而赋值是在第二阶段。不过如第一小节所述效果是一样的)
2)作为返回值时。
e.g. int & getnum(&i) {return i;}
若使用int a=getnum(i):则相当于先调用了getnum(i),其返回就是i的实体;然后再用i进行了复制构造操作。也就是说,a和i是两个变量。
若使用int&a = getnum(i):则相当于a也是i的别名。然而这要求用户知道返回方式为引用传递,因为引用并不能指向一个值。
若getnum(i) = 1;则是对i进行了赋值操作。getnum(i)就是i的一个别名。(相当于是一个匿名变量。)
事实上,返回的就是对象i;至于是引用传递还是值传递,则是接收方式的不同。
注:引用不能无初始化存在,也不能被修改,可以视为常指针。
四、封装的后门——友元
如果一个函数是类的友元函数,则该函数可以直接使用类的private成员,就像是类的成员函数一样。
友元的两个存在方式:
- friend关键字在类的声明中声明。
- 同一个class的各个object互为友元。
这就是为什么在运算符重载时,可以直接使用C2.real变量。也就是说,类的成员函数中的参数如果是同类对象,可以直接使用它的private对象。
五、特殊的函数重载——运算符重载
运算符就可以视作一个函数。
1) 作为成员函数重载(一般为单目)
以+=运算符为例:
1 inline complex&
2 __doapl (complex* ths, const complex& r)
3 {
4 ths->re += r.re;
5 ths->im += r.im;
6 return *ths;
7 }
8 inline complex&
9 complex::operator += (const complex& r)
10 {
11 return __doapl (this, r);
12 }
- 编译器如何看待操作数?
C2+=C1;《=》C2.operator +=(this , const complex &r);//其中r为C1的别名;
* 任何的成员函数都有隐藏的this指针,这个指针只在C++的后台中才会体现,我们使用C++时不可见。
- 返回引用
考虑到存在连续操作的可能,应该为原类型的引用。
- 运算符的结合方式:
在连续操作时,涉及到运算符的结合方式。
左结合,如:<<,[],+,-等;a+b+c <=> (a+b)+c
右结合,如:=,+=,-+等;a=b=c <=> a=(b=c)
2)作为非成员函数:
以+为例的双目函数:
1
inline complex
2
operator + (double x, const complex& y)
3
{
4
return complex (x + real (y), imag (y));//返回一个匿名临时变量
5 }
* 返回值一般采取值传递
e.g.
对于a=b+c,若b+c采取了引用传递,局部变量将会消亡。
单目非成员函数(操作数仅为后值),如取负。
1
inline complex
2
operator - (const complex& x, double y)
3
{
4
return complex (real (x) - y, imag (x));
5 }
注:如果接口合适,非成员函数的操作符重载并不需要在类的声明中体现出来。
*输入输出流相关函数的重载:(由于第一操作数为ostream对象,因此并不能作为成员函数。)
ostream & operator << (ostream& os, const complex &x)
{
return os<<'('<<real(x)<<','<<imag(x)<<')';
}