【转载】漫谈C++:良好的编程习惯与编程要点
正文
以良好的方式编写C++ class
假设现在我们要实现一个复数类complex,在类的实现过程中探索良好的编程习惯。
① Header(头文件)中的防卫式声明complex.h:
# ifndef __COMPLEX__
# define __COMPLEX__
class complex
{
}
# endif
防止头文件的内容被多次包含。
② 把数据放在private声明下,提供接口访问数据
# ifndef __COMPLEX__
# define __COMPLEX__
class complex
{
public:
double real() const {return re;}
double imag() const {return im;}
private:
doubel re,im;
}
# endif
③ 不会改变类属性(数据成员)的成员函数,全部加上const声明
例如上面的成员函数:
double real () `const` {return re;}
double imag() `const` {return im;}
既然函数不会改变对象,那么就如实说明,编译器能帮你确保函数的const属性,阅读代码的人也明确你的意图。
而且,const对象才可以调用这些函数——const对象不能够调用非const成员函数。
④ 使用构造函数初始值列表
class complex
{
public:
complex(double r = 0, double i =0)
: re(r), im(i) { }
private:
doubel re,im;
}
在初始值列表中,才是初始化。在构造函数体内的,叫做赋值。
⑤如果可以,参数尽量使用reference to const
为complex 类添加一个+=操作符:
class complex
{
public:
complex& operator += (const complex &)
}
使用引用避免类对象构造与析构的开销,使用const确保参数不会被改变。内置类型的值传递与引用传递效率没有多大差别,甚至值传递效率会更高。例如,传递char
类型时,值传递只需传递一个字节;引用实际上是指针实现,需要四个字节(32位机)的传递开销。但是为了一致,不妨统一使用引用。
⑥ 如果可以,函数返回值也尽量使用引用
以引用方式返回函数局部变量会引发程序未定义行为,离开函数作用域局部变量被销毁,引用该变量没有意义。但是我要说的是,如果可以,函数应该返回引用。当然,要放回的变量要有一定限制:该变量的在进入函数前,已经被分配了内存。以此条件来考量,很容易决定是否要放回引用。而在函数被调用时才创建出来的对象,一定不能返回引用。
说回operator +=
,其返回值就是引用,原因在于,执行a+=b
时,a
已经在内存上存在了。
而operator +
,其返回值不能是引用,因为a+b
的值,在调用operator +
的时候才产生。
下面是operator+=
与'operator +' 的实现:
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 +=
中返回引用还是必要的,这样可以使用连续的操作:
c3 += c2 += c1;
⑦ 如果重载了操作符,就考虑是否需要多个重载
就我们的复数类来说,+
可以有多种使用方式:
complex c1(2,1);
complex c2;
c2 = c1+ c2;
c2 = c1 + 5;
c2 = 7 + c1;
为了应付怎么多种加法,+
需要有如下三种重载:
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三大件:拷贝构造函数、拷贝赋值函数、析构函数。
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的动态内存申请:
/*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 = '\0';
}
}
这个构造函数以C风格字符串为参数,当执行
String *p = new String ("hello");
m_data
向系统申请了一块内存存放字符串hello
:
析构函数必须负责把这段动态申请来的内存释放掉:
inline
String ::~String()
{
delete[]m_data;
}
②赋值构造函数与复制构造函数负责进行深拷贝
来看看如果使用编译器为String默认生成的拷贝构造函数与赋值操作符会发生什么事情。默认的复制构造函数或赋值操作符所做的事情是对类的内存进行按位的拷贝,也称为浅拷贝,它们只是把对象内存上的每一个bit复制到另一个对象上去,在String中就只是复制了指针,而不复制指针所指内容。现在有两个String对象:
String a("Hello");
String b("World");
a、b在内存上如图所示:
如果此时执行
b = a;
浅拷贝体现为:
存储World\0
的内存块没有指针所指向,已经成了一块无法利用内存,从而发生了内存泄露。不止如此,如果此时对象a
被删除,使用我们上面所写的析构函数,存储Hello\0
的内存块就被释放调用,此时b.m_data
成了一个野指针。来看看我们自己实现的构造函数是如何解决这个问题的,它复制的是指针所指的内存内容,这称为深拷贝
/*拷贝赋值函数*/
inline String &String ::operator= (const String & str)
{
if(this == &str) //①
return *this;
delete[] m_data; //②
m_data = new char[strlen(str.m_data)+1]; //③
strcpy(m_data,str.m_data); //④
return *this
}
这是拷贝赋值函数的经典实现,要点在于:
① 处理自我赋值,如果不存在自我赋值问题,继续下列步骤:
② 释放自身已经申请的内存
③ 申请一块大小与目标字符串一样大的内存
④ 进行字符串的拷贝
对于a = b
,②③④过程如下:
同样的,复制构造函数也是一个深拷贝的过程:
inline String ::String(const String & str )
{
m_data = new char[ strlen (str) +1];
strcpy(m_data,str.m_data);
}
另外,一定要在operator = 中检查是否self assignment 假设这时候确实执行了对象的自我赋值,左右pointers指向同一个内存块,前面的步骤②delete掉该内存块造成下面的结果。当企图对rhs的内存进行访问是,结果是未定义的。
static与类
① 不和对象直接相关的数据,声明为static
想象有一个银行账户的类,每个人都可以开银行账户。存在银行利率
这个成员变量,它不应该属于对象,而应该属于银行这个类,由所有的用户来共享。static修饰成员变量时,该成员变量放在程序的全局区中,整个程序运行过程中只有该成员变量的一份副本。而普通的成员变量存在每个对象的内存中,若把银行利率
放在每个对象中,是浪费了内存。
② static成员函数没有this指针
static成员函数与普通函数一样,都是只有一份函数的副本,存储在进程的代码段上。不一样的是,static成员函数没有this指针
,所以它不能够调用普通的成员变量,只能调用static成员变量。普通成员函数的调用需要通过对象来调用,编译器会把对象取地址,作为this指针的实参传递给成员函数:
obj.func() ---> Class :: fun(&obj);
而static成员函数即可以通过对象来调用,也可以通过类名称来调用。
③在类的外部定义static成员变量
另一个问题是static成员变量的定义。static成员变量必须在类外部进行定义:
class A
{
private:
static int a; //①
}
int A::a = 10; //②
注意①是声明,②才是定义,定义为变量分配了内存。
④static与类的一些小应用
这些可以用来应付一下面试,在实现单例模式的时候,static成员函数与static成员变量得到了使用,下面是一种称为”饿汉式“的单例模式的实现:
class A
{
public:
static A& getInstance();
setup(){...};
private:
A();
A(const A & rhs);
static A a;
}
这里把class A的构造函数都设置为私有,不允许用户代码创建对象。要获取对象实例需要通过接口getInstance
。”饿汉式“缺点在于无论有没有代码需要a
,a
都被创建出来。下面是改进的单例模式,称为”懒汉式“:
class A
{
public:
static A& getInstance();
setup(){....};
private:
A();
A(const A& rsh);
...
};
A& A::getInstance()
{
static A a;
return a;
}
"懒汉式"只有在真正需要a
时,调用getInstance
才创建出唯一实例。这可以看成一个具有拖延症的单例模式,不到最后关头不干活。很多设计都体现了这种拖延的思想,比如string的写时复制,真正需要的时候才分配内存给string对象管理的字符串。
【转载】漫谈C++:良好的编程习惯与编程要点的更多相关文章
- 漫谈C++:良好的编程习惯与编程要点(转载)
这个博主写的文章真是细腻,全面,严谨,应当多读几回 原文http://www.cnblogs.com/QG-whz/p/5517643.html 阅读目录 以良好的方式编写C++ class Clas ...
- 漫谈C++:良好的编程习惯与编程要点
以良好的方式编写C++ class 假设现在我们要实现一个复数类complex,在类的实现过程中探索良好的编程习惯. ① Header(头文件)中的防卫式声明 complex.h: # ifndef ...
- iOS高效编程秘诀—坚持编程习惯
资料源于网络 习惯会影响一个人做事的方式,也会直接影响效率.我经常在项目完成后自我总结,有哪些做得好的,有哪些做得不好的?然后把一些好的流程记录下来,并且重新运用回编程中.那些能够坚持去做的流程,就变 ...
- 我的iOS高效编程秘诀—坚持编程习惯
http://www.cocoachina.com/programmer/20150819/13103.html 作者:sunljz 授权本站转载. 习惯会影响一个人做事的方式,也会直接影响效率.我经 ...
- 防御性编程习惯:求出链表中倒数第 m 个结点的值及其思想的总结
防御性编程习惯 程序员在编写代码的时候,预料有可能出现问题的地方或者点,然后为这些隐患提前制定预防方案或者措施,比如数据库发生异常之后的回滚,打开某些资源之前,判断图片是否存在,网络断开之后的重连次数 ...
- 10条PHP编程习惯助你找工作
过去的几周对我来说是一段相当复杂的经历.我们公司进行了大裁员,我是其中之一,但却体验到了其中的乐 趣.我从来没有被开除过,所以很难不去想得太多.我开始浏览招聘板块,一个全职PHP程序员的职位很吸引人, ...
- 【转载】学习C++和编程的几个要点
1.把C++当成一门新的语言学习(和C没啥关系!真的.):2.看<ThinkingIn C++>,不要看<C++变成死相>:3.看<The C++ Programming ...
- 10条PHP编程习惯
过去的几周对我来说是一段相当复杂的经历.我们公司进行了大裁员,我是其中之一,但却体验到了其中的乐 趣.我从来没有被开除过,所以很难不去想得太多.我开始浏览招聘板块,一个全职PHP程序员的职位很吸引人, ...
- [置顶] 学习JDK源码:编程习惯和设计模式
编程习惯 1.用工厂方法替代构造函数 Boolean.valueOf() 通过一个boolean简单类型,构造Boolean对象引用. 优点:无需每次被调用时都创建一个新对象.同时使得类可以严格控制在 ...
随机推荐
- 【3】【MOOC】Python游戏开发入门-北京理工大学【第三部分-游戏开发之机制(事件处理机制)】
学习地址链接:http://www.icourse163.org/course/0809BIT021E-1001873001?utm_campaign=share&utm_medium=and ...
- ubuntu命令安装jdk
1.ubuntu使用的是openjdk,所以我们需要先找到合适的jdk版本.在命令行中输入命令: $apt-cache search openjdk 1 返回结果列表(因个人电脑而有所不同): def ...
- iOS动画暂停和继续-本质是速度控制和时间坐标转换
时间永不停止! 写一个CALayer的分类,控制动画的暂停与继续 extension CALayer { ///暂停动画 func pauseAnimation() { //取出当前时间,转成动画暂停 ...
- 【node.js】REPL(交互式解释器)
Node 自带了交互式解释器,可以执行以下任务: 读取 - 读取用户输入,解析输入了Javascript 数据结构并存储在内存中. 执行 - 执行输入的数据结构 打印 - 输出结果 循环 - 循环操作 ...
- Struts2学习总结——文件上传与下载
Struts2文件上传与下载 1.1.1新建一个Maven项目(demo02) 在此添加Web构面以及 struts2 构面 1.2.1配置Maven依赖(pom.xml 文件) <?xml v ...
- ThinkPHP微信扫码支付接口
最近折腾微信扫码支付,看了微信官方文档,找了很多网页,发现和文档/demo不匹配,现在自己算是弄出来了(文件名称有所更改),贴出来分享一下 一.将有用的官方lib文件和使用的相关文件放置到vendor ...
- mysql5.7 安装版 表不能输入汉字解决方案
安装版本 的安装目录没有 my.ini 配置文件 在所在表执行 alter table 数据表名 CONVERT TO CHARACTER SET utf8;
- #leetcode刷题之路16-最接近的三数之和
给定一个包括 n 个整数的数组 nums 和 一个目标值 target.找出 nums 中的三个整数,使得它们的和与 target 最接近.返回这三个数的和.假定每组输入只存在唯一答案. 例如,给定数 ...
- MVC和MVT的区别
首先,MVC和MVT是框架式不是设计模式. 框架与设计模式虽然相似,但却有着根本的不同.设计模式是对在某种环境中反复出现的问题以及解决该问题的方案的描述,它比框架更抽象:框架可以用代码表示,也能直接执 ...
- react中如何获取onclick事件调用元素的dom对象
今天终于有时间写博客了, 前几天项目有个需求,我感觉用dom操作兄弟元素实现比较方便,但是前端用的react框架不能用jquery的$(this)获取当前元素,查了好多资料和尝试后写下总结: 在HTM ...