一、让自己习惯C++
写在前面
第一遍看《Effective C++》时,在准备暑期实习生的招聘,没有时间好好地捋一下,将一些要点记录下来。现在实习回来,重读此书,并记录一些要点,为今后的复习亦或是学习铺垫。
这篇介绍第一章的4个条款。
条款01:视C++为一个语言联邦
- C++是一个多重范型编程语言:
- 支持过程形式
- 支持面向对象形式
- 支持函数/泛型形式
- 支持元编程形式
- 理解C++,必须首先认识其主要的次语言:
- C语言。 C++ 以C为基础,区块,语句,预处理器,内置数据类型,数组,指针等全部都来自于C。C++是C的高级解法揭露了C语言的局限性:
- 没有模板。
- 没有异常处理。
- 没有重载。
- ...(以及封装,继承等面向对象的特性等其它)
- Object-Oriented C++: 简单总结就是面向对象的特点。
- 类。每个类都有构造函数,析构函数。
- 封装,继承,多态,虚函数(动态绑定).
- Template C++: C++范型编程的部分。有了模板可以带来崭新的编程范型。
- STL: Standard Template Library。对容器,迭代器,算法以及函数对象的规约有极佳的机密配合与协调。
- 对于内置类型,pass-by-value通常比pass-by-reference更加高效,但是对于用户自定义(user-defined)类型,由于构造函数和析构函数的存在,pass-by-reference-to-const往往更好。
作者总结:
C++的高效编程守则视状况而变化,取决于你使用C++的哪一部分。
个人总结:
此条款介绍了C++的组成部分,在开发过程中,要高效利用C++的这几个“次语言”特性,在使用不同的“次语言”的时候,要选择相应的高效的编程方式。
条款02:尽量用const,enum,inline替换#define
首先要明白使用#define的缺点:
如#define NUM 1.2,调试的时候出现的是1.2而不是NUM,如果这个宏定义不是自己写的就更难定位问题了。
宏定义#define MAX(a,b) ((a) > (b) ? (a) : (b))看似可行的一个宏定义函数,但是考虑以下情况:
int a = 5,b = 0;
MAX(a++,b); // a被累加两次
MAX(++a,b + 10); // a被累加一次
a的累加次数取决于a和b的大小,显然不是调用者所期待的情形。
class的专属常量
假定我们在GamePlayer类中有个常量成员,有个数组,数组大小使用该常量表示。
class GamePlayer
{
public:
static const int iNum = 5; //常量声明式
int iScores[iNum];
。。。 // 其它成员
}
要明确一点:上述const是一个声明式,并不是一个定义式。
为什么要声明为static?
如果不是static,在该类还未构造时,iNum是不存在的,编译器也就无法知道数组iScores的大小。编译器会坚持要求知道数组的大小。
旧式的编译器中,不允许static在声明的时候不允许被赋初值。如果不支持声明时候赋初值,就应该改为:
class GamePlayer
{
public:
static const int iNum;
...
}
在函数体外再赋初值:
int GamePlayer::iNum = 5;
但是采取这种写法就无法在类中定义一个常量大小的数组。
采用enum解决
声明enum常量,就可以防止不同编译器对const能否赋初值所带来的不便之处。
class GamePlayer
{
public:
enum { iNum = 5 };
int iScores[iNum];
}
使用enum的更多好处
enum声明的常量是一个右值。如果不想别人用一个pointer或者reference指向一个整型常量,使用enum即可。引用和指针都无法绑定在一个枚举常量上。
enum
{
first = 1,
};
//int &First = first; 无法通过编译
//int *pFirst = &first; 无法通过编译
作者总结
对于单纯常量,最好以const对象或enum替换#define.
对于形似函数的宏,最好改用inline function替换#define.
条款03:尽可能使用const
先写一下老生常谈的const和pointer的不同组合的效果。
char hello[] = "hell0";
char *p1 = hello; // non-const pointer,non-const data
const char *p2 = hello; // non-const pointer,const data
char* const p3 = hello; // const pointer,non-const data
const char* const p = hello; // const pointer,const data
初学者不容易记住,其实只要记住const后面是什么(数据类型不看),什么就不变就对了。
比如说,const char *p,const 后面是 *p, p是指针所指向的数据,所以是data不变。又比如 char const p; const后面是p,p是一个指针,所以是个const pointer,指针指向的地址不能改变。
由此延申出const在STL迭代器中的使用
假设我们用一个迭代器指针去操作一个vector容器:
如果用const显式修饰:
vector<int> vct(10,1);
const vector<int>::iterator it = vct.begin(); // 此迭代器指针是一个non-const data,const pointer类型。
*it = 9; // 正确。
++it; // 错误,const pointer不能改变指向的位置。
上述代码中,const 修饰的迭代器指向的地址不可变,所以只能指向vct.begin()位置。
此外,STL的迭代器中有一个const_iterator,是一个non-const pointer,const data类型的迭代器。
vector<int> vct(10,1);
vector<int>::const_iterator cIt;
*cIt = 10; // 错误,const data,不可改变其值。
++cIt; // 正确。non-const pointer.可以改变其指向。
令函数返回一个常量值可以降低错误发生的概率
const Rational operator *(const Rational &lhs, const Rational &rhs);
如果返回的不是const,那么很可能写成
if(a * b = c)
这个是程序员写错的时候的情形,如果返回const那么就会提示报错,就能立即定位错误。而如果不是const类型,那么这个错误就可能很难被发现。
const修饰成员函数
const修饰的成员函数,在函数体内不能修改任何一个成员变量。如果是可能被修改的成员变量,那么这些成员变量应该是使用mutable关键词来修饰。mutable关键词可以去掉non-static成员变量的bitwise constness约束。
注意:两个成员函数的常量性不同,是可以被重载的。
例如:
class TextBlock
{
public:
const char &operator[](std::size_t position) const
{
... // 记录数据1
... // 记录数据2
... // 记录数据3
return text[position];
}
char & operator[](std::size_t position)
{
... // 记录数据1
... // 记录数据2
... // 记录数据3
return text[position];
}
}
如果这两个版本实现了相同的函数体,只是返回值的常量性不同,那么可以将non-const版本改成以下版本:
char & operator[](std::size_t position)
{
return const_cast<char &>(
static_cast<const TextBlock&>(*this)
[position])
);
}
这语句有两个转型的动作:
(1) static_cast<const TextBlock&>.将当前对象转成const的对象。
(2) const_cast<char &>是去掉const属性,恢复成原来的非const对象。
我们重载了[]运算符,const和非const版本都有。当对象为const 属性的时候调用的是const版本,非const属性对象就调用非const版本。
这样写可以避免代码冗余。
注意:只能用非const去调用const,如果使用const去调用非const,那么就先要将const属性去掉,那么原本const函数体中的数据就不被保证不会被修改,也就失去了我们一开始使用const修饰的初衷。
作者总结
将某些东西声明为const可帮助编译器侦测出错误用法。const可被施加于在任何作用域内的对象,函数参数,函数返回类型,成员函数本体。
编译器强制实施bitwise constness,但你编写程序的时候应该使用“概念上的常量性”。
当const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码重复。
条款04:确定对象使用前已经被初始化。
先搞清楚赋值和初始化是不一样的:
假设有一个类ABEntry:
class ABEntry
{
public:
ABEntry(const string name,const string addr);
private:
string theName;
string theAddr;
}
以下的构造函数中是赋值给私有成员变量,而不是初始化私有成员变量。
ABEntry::ABEntry(const string name,const string addr)
{
theName = name; // 赋值
theAddr = addr; // 赋值
}
真正的初始化:
ABEntry::ABEntry(const string name,const string addr)
:theName(name),theAddr(addr)
{
}
二者的不同:
第一个构造函数中:
(1) 在赋值之前,theName和theAddr先执行了它们各自的默认构造函数,也就是string类中的默认构造函数,有了一个初值(为空)。
(2) 进行赋值的时候,调用了copy assignment操作符。将name和addr复制给theName和theAddr.
所以它充其量只是一个赋值,并不能说是初始化,第一小步就已经初始化成一个空string了。
而在第二个构造函数中,只调用了一个copy构造函数去构造初始值。《C++ Primer》中将这种初始化方式叫做成员列表初始化。
单单使用一个copy构造函数显然是比较高效的。在内置类型中,不需要调用默认构造函数,二者的效率是差不多的。
const和reference初始化
由于const和reference一定需要初值,而不能被赋值改变,所以需要采用成员列表初始化的方式来进行初始化操作。
成员变量的初始化顺序
在C++中,成员变量的初始化顺序严格遵守变量的声明顺序。
class Text
{
public:
...
private:
string strAddr;
string strName;
int iCall;
}
在上述类之中,如果采用成员列表初始化,那么初始化顺序依此为strAddr,strName,iCall.如果需要使用strName的值去初始化strAddr, 那么是错误的做法,因为strAddr先于strName初始化,strName这个时候尚未有值。
作者总结:
为内置型对象进行手工初始化,因为C++不保证初始化它们。
构造函数最好使用成员初始列,而不要在构造函数本体内使用赋值操作,初值列列出的成员变量,其排列次序应该和它们在class中的声明次序相同。
为免除“跨编译单元之初始化次序”问题,请以local static对象替换non-local static对象。
一、让自己习惯C++的更多相关文章
- 防御性编程习惯:求出链表中倒数第 m 个结点的值及其思想的总结
防御性编程习惯 程序员在编写代码的时候,预料有可能出现问题的地方或者点,然后为这些隐患提前制定预防方案或者措施,比如数据库发生异常之后的回滚,打开某些资源之前,判断图片是否存在,网络断开之后的重连次数 ...
- 漫谈C++:良好的编程习惯与编程要点
以良好的方式编写C++ class 假设现在我们要实现一个复数类complex,在类的实现过程中探索良好的编程习惯. ① Header(头文件)中的防卫式声明 complex.h: # ifndef ...
- PHP新手常见的一些不好习惯(抄的 有待理解)
1.不写注释(是个好习惯,不过也没必要每个语句都要写) 2.不使用可以提高生产效率的IDE工具 3.不使用版本控制 4.不按照编程规范写代码 5.不使用统一的方法 6.编码前不去思考和计划 7.在执行 ...
- XCode设置自己windows习惯的快捷键(比如Home、End键)
Xcode的preference(command+,)中可以设置Key Bindings.绑定自己习惯的快捷键.实测系统快捷键设置同样名字也可以生效,但操作比较繁琐这里就不介绍了. 1.打开Xcode ...
- Web开发者需养成的好习惯
Web开发者需养成的8个好习惯 每个行业有着每个行业的标准和一些要求,自己只是一个进入前端领域的小白,但是深刻的知道,习惯很重要,就Web开发分享一下,要养成的一些好的习惯. 优秀的Web开发人员工作 ...
- C#与Java对比学习:类型判断、类与接口继承、代码规范与编码习惯、常量定义
类型判断符号: C#:object a; if(a is int) { } 用 is 符号判断 Java:object a; if(a instanceof Integer) { } 用 inst ...
- SQL SERVER全面优化-------写出好语句是习惯
前几篇文章已经从整体提供了诊断数据库的各个方面问题的基本思路...也许对你很有用,也许你觉得离自己太远.那么今天我们从语句的一些优化写法及一些简单优化方法做一个介绍.这对于很多开发人员来说还是很有用的 ...
- java程序员保持天天快乐的6个习惯
忍不住感叹,我第一次对Buffer(在社交媒体上发布最简单的方式)有所想法已经差不多是两年前的事了.并且,在我有想法的一年半前,我还在前面那家新创公司工作的时...... 忍不住感叹,我第一次对Buf ...
- java良好的编码习惯
1. 尽量在合适的场合使用单例 使用单例可以减轻加载的负担,缩短加载的时间,提高加载的效率,但并不是所有地方都适用于单例,简单来说,单例主要适用于以下三个方面: 第一,控制资源的使用,通过线程同步 ...
- callback res.end 记得return(Javascript需要养成的良好习惯)
错误示例: app.get('do',function(req,res,next){ getUserId(function(err,userId){ if(err){ res.end(err);//错 ...
随机推荐
- Maven引入oracle驱动包
1.下载驱动包 2.加载到本地maven库中 mvn install:install-file -DgroupId=com.oracle -DartifactId=ojdbc6 -Dversion=1 ...
- mybatis-sql执行流程源码分析
1. SqlSessionFactory 与 SqlSession. 通过前面的章节对于mybatis 的介绍及使用,大家都能体会到SqlSession的重要性了吧, 没错,从表面上来看,咱们都是通过 ...
- TP5使用phpoffice phpexcel包操作excel(导出)
安装composer(window版本) 安装composer(MAC版本) 安装composer(Linux版本) 在PhpStorm配置 导出excel 1.使用composer安装phpoffi ...
- Java重要类之LinkedList
一.ArrayList与LinkedList 基本概念:List是一个接口,Arraylist和LinkedList是它的两个实现类,只是实现的方式不一样.我在“单链表java实现”一文中已经对单链表 ...
- response.getWriter().wirte和out.print()的区别
1.首先介绍write()和print()方法的区别: (1).write():仅支持输出字符类型数据,字符.字符数组.字符串等 (2).print():可以将各种类型(包括Object)的数据通 ...
- Git复习(九)之理解git工作区和暂存区
前言 Git和其他版本控制系统如SVN的一个不同之处就是有暂存区的概念. 版本库 在工作区目录中有一个.git文件,这个其实不是工作区而是Git的版本库 版本库中包含两个部分,一个是暂存区index/ ...
- 请写出正则表达式(regex),取得下列黄色部分的字符串 TEL: 02-236-9655/9659 FAX:02-236-9654 (黄色部分即02-236-9655/9659 ) ( 测试面试题)
请写出正则表达式(regex),取得下列黄色部分的字符串 TEL: 02-236-9655/9659 FAX:02-236-9654 答: package test1; import java.uti ...
- microsoft office powerpoibt automation 二次开发
背景 首先office的产品powerpoint是支持二次开发的,这里的二次开发并不是指在powerpoint产品中嵌入一些自己的控件,而是一些简单的automation的控制(进入放映状态,上一页, ...
- 使用C#表达式树为两个对象的相同属性赋值
//缓存表达式树 private static Dictionary<string, object> objCache = new Dictionary<string, object ...
- 个人总结的J2EE目前知道的涵盖面,从大方向入手,多写单元测试,加强基础
JEE Development process: java SE 普通语法,,异常处理,数据结构,循环,面向对象,泛型, 属性,反射,多线程,线程池,锁, lambada,异步编程,并发 框架spri ...