[GeekBand] C++学习笔记(1)——以复数类为例

本篇笔记以复数类(不含指针的类)为例进行面向对象的学习

=========================================================

复数类的声明:

 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。
  • 不能返回引用的情况:
  1.  
    1. 变量为栈空间内的局部变量。
    2. 要返回的对象为一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成员,就像是类的成员函数一样。

  友元的两个存在方式:

  1. friend关键字在类的声明中声明。
  2. 同一个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)<<')';

}

  

上一篇:maven打包日志输出优化-去掉泛型与过时的警告


下一篇:解决客户 IE 浏览器"兼容性视图"设置带来的问题