以良好的方式编写C class 假设现在我们要实现一个复数类complex,在类的实现过程中探索良好的编程习惯。
① Header(头文件)中的防卫式声明
代码语言:javascript复制complex.h:
# ifndef __COMPLEX__
# define __COMPLEX__
class complex
{
}
# endif
防止头文件的内容被多次包含。
② 把数据放在private声明下,提供接口访问数据
代码语言:javascript复制# ifndef __COMPLEX__
# define __COMPLEX__
class complex
{
public:
double real() const {return re;}
double imag() const {return im;}
private:
doubel re,im;
}
# endif
③ 不会改变类属性(数据成员)的成员函数,全部加上const声明
例如上面的成员函数:
代码语言:javascript复制double real () `const` {return re;}
double imag() `const` {return im;}
``
既然函数不会改变对象,那么就如实说明,编译器能帮你确保函数的const属性,阅读代码的人也明确你的意图。
而且,const对象才可以调用这些函数——const对象不能够调用非const成员函数。
④ 使用构造函数初始值列表
```c
class complex
{
public:
complex(double r = 0, double i =0)
: re(r), im(i) { }
private:
doubel re,im;
}
在初始值列表中,才是初始化。在构造函数体内的,叫做赋值。
⑤如果可以,参数尽量使用reference to const
为complex 类添加一个 =操作符:
代码语言:javascript复制class complex
{
public:
complex& operator = (const complex &)
}
使用引用避免类对象构造与析构的开销,使用const确保参数不会被改变。内置类型的值传递与引用传递效率没有多大差别,甚至值传递效率会更高。
例如,传递char类型时,值传递只需传递一个字节;引用实际上是指针实现,需要四个字节(32位机)的传递开销。但是为了一致,不妨统一使用引用。
⑥ 如果可以,函数返回值也尽量使用引用
以引用方式返回函数局部变量会引发程序未定义行为,离开函数作用域局部变量被销毁,引用该变量没有意义。但是我要说的是,如果可以,函数应该返回引用。
当然,要放回的变量要有一定限制:该变量的在进入函数前,已经被分配了内存。以此条件来考量,很容易决定是否要放回引用。而在函数被调用时才创建出来的对象,一定不能返回引用。
说回operator =,其返回值就是引用,原因在于,执行a =b时,a已经在内存上存在了。
而operator ,其返回值不能是引用,因为a b的值,在调用operator 的时候才产生。
下面是operator = 与’operator ’ 的实现:
代码语言:javascript复制inline complex & complex :: operator = (const complex & r)
{
this -> re = r->re;
this -> im = r->im;
return * this;
}
inline complex operator (const complex & x , const complex & y)
{
return complex ( real (x) real (y), //新创建的对象,不能返回引用
imag(x) imag(y));
}
在operator =中返回引用还是必要的,这样可以使用连续的操作:
代码语言:javascript复制c3 = c2 = c1;
⑦ 如果重载了操作符,就考虑是否需要多个重载
就我们的复数类来说, 可以有多种使用方式:
代码语言:javascript复制complex c1(2,1);
complex c2;
c2 = c1 c2;
c2 = c1 5;
c2 = 7 c1;
为了应付怎么多种加法, 需要有如下三种重载:
代码语言:javascript复制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));
}
⑧ 提供给外界使用的接口,放在类声明的最前面
这是某次面试中,面试官大哥告诉我的。想想确实是有道理,类的用户用起来也舒服,一眼就能看见接口。
Class with pointer member(s):记得写Big Three C 的类可以分为带指针数据成员与不带指针数据成员两类,complex就属于不带指针成员的类。而这里要说的字符串类String,一般的实现会带有一个char *指针。带指针数据成员的类,需要自己实现class三大件:拷贝构造函数、拷贝赋值函数、析构函数。
代码语言:javascript复制class String
{
public:
String (const char * cstr = 0);
String (const String & str);
String & operator = (const String & str);
~String();
char * get_c_str() const {return m_data};
private:
char * m_data;
}
如果没有写拷贝构造函数、赋值构造函数、析构函数,编译器默认会给我们写一套。然而带指针的类不能依赖编译器的默认实现——这涉及到资源的释放、深拷贝与浅拷贝的问题。在实现String类的过程中我们来阐述这些问题。
①析构函数释放动态分配的内存资源
如果class里有指针,多半是需要进行内存动态分配(例如String),析构函数必须负责在对象生命结束时释放掉动态申请来的内存,否则就造成了内存泄露。
局部对象在离开函数作用域时,对象析构函数被自动调用,而使用new动态分配的对象,也需要显式的使用delete来删除对象。而delete实际上会调用对象的析构函数,我们必须在析构函数中完成释放指针m_data所申请的内存。下面是一个构造函数,体现了m_data的动态内存申请:
代码语言:javascript复制/*String的构造函数*/
inline
String ::String (const char *cstr = 0)
{
if(cstr)
{
m_data = new char[strlen(cstr) 1]; // 这里,m_data申请了内存
strcpy(m_data,cstr);
}
else
{
m_data= new char[1];
*m_data = '