一门程序语言往往可以很快地掌握其基础,但是如何能设计并实现一高效程序却是需要日积月累的学习与实践才能达到,而Effective C++一书介绍了如何去设计一个类,函数或是模板,这将使我们程序编写更直观,简洁,高效。
1.视c++为一个语言联邦
c++发展至今已是个多重泛型编程语言,面向过程,面向对象,泛型编程,元编程,这些功能使c++成为一个无可匹敌的工具,但也使人们对其难以理解。而最简单的方式便是将其视为一个语言联邦,其由C,Object-C,Template,STL四个次级组成,每个语言都有自己特定的语言规约,从这四个层次理解C++将会更加容易。
2.尽量以const,enum,inline替换#define
我们都知道#define是预处理器命令,他并不被视为是语言的一部分,故其不会进行安全性检查,而且宏定义的名称可能不会进入符号表,这将会导致你编译时报错信息并不会明确指示到某个宏。
#define SIMPLE_VALUE 1.963
const float SIMPLE_VALUE = 1.963;
例如我们使用了一个SIMPLE_VALUE的宏常量,但是编译时的错误信息会提示 1.963而不是这个具体的宏名称,这将增加我们错误查找量,而当我们使用上面的const常量时就不会发生此情况,他必将指向SIMPLE_VALUE这个变量。当然我们能够使用enum去代替整形常量的的全局定义,但它只能局限于整形数值。当我们需要申明一个类的专属常量时我们只能使用const,因为#define并不会去确定作用域。
class Game{
private:
static const int NUM = 6;
int scores[NUM];
};
这里定义了一个NUM的Game类的常量,并在其成员中使用其作为数组维数.
我们通常还会用带参宏去定义一些简短的函数,以避免函数调用的内存开销,如下:
#define CALL_MAX(a,b) f((a) > (b) ? (a) : (b))
int a = 5, b =0
CALL_MAX(++a,b);//a自增两次
CALL_MAX(++a,b + 10);//a自增一次
这里会有一个问题,调用f函数之前,a的自增次数回合其比较的直有关,这将导致我们无法取得预期的a值,这边是单一的宏替换所带来的潜在问题,因而我们需要使用inline去避免这种不必要的风险。
template<typename T>
inine void callMax(const & T a,comst & T b){
f(a > b ? a : b);
}
3.尽可能的使用const
const允许我们定义一个不可被改动的对象,他会是编译器强制实施这一约束,当我们知晓一个变量不应被改变时我们就该使用const加以约束。const在修饰指针时可表示其指向,其本身,或二者都不可改动。
const int *p;
int const *p;//指向内容不可改变
int* const p;//指针本身不可改变
const int* const p;//指向和自己均不可改变
const最具威力的使用便是在函数声明时的应用,我们可以修饰函数本身(类的常量成员函数),形参,返回值。令函数返回值是一个const值将会避免函数的错误使用:
class Rational{
friend const Rational operator* (const Rotional& lhs, const Rotional& rhs);
};
Rotional a ,b ,c;
(a * b) = c;//对a*b的结果调用赋值函数
对值得乘积进行赋值时恨不符合数学运算的,故而对乘积的返回值加以const限定,将明确的告诉人们不可以这样做。使用const修饰类的成员函数有两个理由,更容易理解某个类,因为我们会知道那个函数可以更改类的成员,哪个函数不行;const函数只能右const对象调用。这对编写高效代码很有帮助。有事我们会使用const成员函数和non-const函数,为了避免重复我们可以这样做。
class TextBlock{
const char& operator[](std::size_t position) const{
....
return text[position];
}
char& operator[](std::size_t position){
return const_cast<char&>(static_cast<const&>(*this)[position]);
}
};
这里使用了类型转换(虽然类型转换也是一种较为糟糕的代码,单较与代码重复还算可以容忍),并在non-const函数里调用了const函数,首先第一步我们将*this转为const,并调用const函数,再将其返回值转为non-const。’
4.确定对象被使用前已被初始化
我们在声明一个变量时如果没有手动初始化,有些变量可能被赋值为0,但有些并不会,例如数组内的元素,故而在使用这些没有初始化值时会出现一些莫名的问题(野指针),因此在定义一个变量时给其初始化是一个很有效的方法。对于自定义类型,我们更趋向于使用成员初始化列表来替代构造函数类队成员进行赋值,这将减少一步赋值操作。
int a = 10;//定义一个变量并直接初始化
int b;
b = 20;//定义一个变量,执行默认初始化,然后进行赋值操作
5.了解一个类中会有哪些默认函数
对于一个空类,即使你没有声明任何变量和函数,编译器也会为其生成默认构造函数,拷贝构造函数,赋值函数,析构函数。但当你手动声明某一函数后,编译器便不再升成默认函数。
class Empty{
public:
Empty(){};//默认构造
~Empty(){};、、析构
Empty(const Empty& other){...}//拷贝构造
Empty& operator=(const Empty& other){...}//赋值
};
生成默认函数的前提是升成的代码合法且有机会证明其有意义,如:
class Name{
private:
Name(std::string& name,int value):name(name),value(value){}
std::string& name;
const int value;
};
std::string a("peter");
std::string b("bob");
Name p(a,10);
Name q(b,20);
p = q;
上述语句定义了一个Name类,其有一个std::string的私有成员name,在定义完p,q后,用q给p进行赋值,假使会调用默认赋值函数,那么p的name成员将会改变引用,这与c++语义引用本身不可改变相悖,故而岂不会生成默认赋值函数。
6.若不想使用默认函数,就该明确拒绝
有时我们的数据是独一无二的,我们不想执行拷贝亦或是赋值操作,这时我们可将拷贝构造函数或是赋值函数声明为private,这样其在调用时将会报错。也可使用delete关键字显示标识。
class Test{
Test(const Test& other) = delete;
private:
Test& operator=(const Test& other){}
};
7.为多态基类声明虚析构函数
在c++多态里表现在于继承与虚函数,一个指针到底调用何函数,首先会去查找虚表(存储该类虚函数的函数指针),当存在该虚函数时便调用。假使我们使用一个基类指针指向子类内存,然而这个父类并没有虚析构函数,那么在使用delete该指针时只会析构父类内存,并不会调用子类析构函数,故而造成内存泄漏。
class Test{
virtual ~Test(){}
};
class A : public Test{};
Test * p = new A();
delete p;
这里会正常释放基类与派生类内存
虚函数的存在将会增加类型大小(一个虚指针大小,取决于运行环境x 64,x 32),如若使用继承就要确保基类存在虚析构函数。
8.绝不在构造和析构过程调用virtual函数
在构造或是析构构成中调用虚函数将会引发一些意想不到的问题:
class Test{
virtual void test() = 0;
Test(){
test();
}
};
class A:public Test{
virtual void test(){}
};
A a;
当执行A类的构造函数时他会先调用Test类的构造函数,而在构造函数最后一句他会去调用虚函数test,首先基类构造期间,虚函数不会下降到派生类阶层,也就是基类构造期间test函数还不是虚函数。此时它将是一个未初始化的部分,而调用一个未初始化得变量往往会出问题。
9.令operator=返回一个reference to *this
因为赋值运算符合链式操作:
int x,y,z;
x=y=z=15;
class Test{
Test& operator=(const Test& other){
return *this;
}
};
10.在operator=中进行自赋值判断
虽然自赋值是很蠢的行为,但难免有人那样做,故而需要进行自赋值处理
class Test{
Test& operator=(const Test& other){
if(this == &other){
return *this;
}
else{
....
return *this;
}
}
};
11.赋值对象时勿忘其每一个成分
设计良好的面向对象系统会将其内部封装起来,只保留两个函数负责拷贝和赋值,如果你声明了自己的拷贝构造函数,那么就不应该缺省某一部分行为,尤其是父类继承来的那一部分。
class Customer{
public:
Customer(cosnst Customer& other)
{
this.name = other.name;
return *this;
}
Customer& operator=(const Customer& other)
{
if(this == &other)
{
return *this;
}else
{
this.name = other.name;
return *this;
}
}
private:
std::string name;
};
class ProCustomer : public Customer{
public:
ProCustomer(const ProCustomer& other)
{
Customer::opeator=(other);
this.value = other.value;
return *this;
}
private:
int value;
};
上述代码中显示调用了基类的赋值函数来赋值从父类继承来的那部分成员
所谓资源就是,一旦用了它,将来就必须还给系统,如果不这样就会出现问题。c++中最常用的资源就是动态分配内存,除此之外还有互斥锁,数据库连接,网络socket,无论何种资源,不再使用时必须换给系统。
13.成对的使用new和delete时要采取相同形式
std::string *p = new std::string[100];
delete p;
delete []p;
上述代码在使用delete p时将会发生内存泄露,他只释放了第一个元素的内存,还有99个元素没有得到释放,故而申请内存时使用何种形式,释放内存时也使用何种形式。
14.让接口容易被正确使用,不易混淆
理想上,如果客户企图使用某个接口但没有获得他预期的行为,那么这个代码不该通过编译,如果代码通过了编译,那么他的作为就该是客户需要的。
许多客户端错误可以通过引入新的类型来预防,如:
struct Day{
explicit Day(int day):day(day){}
int day;
};
struct Month{
explicit Month(int month):month(month){}
int month;
};
struct Year{
explicit Year(int year):year(year){}
int year;
};
class Date{
public:
Date(const Day& day,const Month& month,const Year& year);
};
Date date(Day(12),Month(10),Year(1995));
如上代码它会明确的告诉你每个参数应该是什么样的,但是即使struct足够规范,明智而审慎的导入新类型仍对预防接口误用有奇效。还有要注意的是客户使用接口时需要注意的事情越少,这个接口越容易使用。
15.设计class犹如设计type
c++和其他面向对象语言一样,当你创建一个新class时也是定义了一个新type,设计好的class是一件艰巨的工作,因为好的type必须有自然的语法,直观的语义,以及一到多个高效实现品。
新type的对象应该如而被创建和删除:这会影响到该类型构造函数和析构函数的。
新type的合法值:对class的成员变量而言,通常只有某些数值是有效的,那些数值决定了你class必须要维护的约束条件,也就确定了你成员函数(构造函数,赋值函数,setter函数)必须进行的错误检查工作。
新type的一般化:当你其实需要定义的是一个type的家族时,那么就不应该设计一个class,而是定义一个新的template。
16.以pass-by-reference-const替换pass-by-value
缺省情况下,c++是以值传递的形式传递对象至函数,除非你另外指定,否则函数参数都是实参的复件,而复件的获取是以对象的拷贝构造函数产出,故而会产生相应的内存消耗。
class Student{
private:
std::string name;
int score;
};
bool student(Student tudent);
bool student(const Student& student);
值传递的形式会比引用传递额外产生一次构造和析构的内存消耗,通过const修饰引用则增加了代码的健壮性,防止函数意外修改值。
17.不要返回栈区的内存
在任何函数中返回栈区的内存是一个天大的错误,因为出了作用域,栈区内存便被系统回收,得到的地址指向未初始化,相当于使用野指针。
int* getString(){
int a[10] = "absgh";
return a;
}
18.将成员变量声明为private
将变量声明为private便是为了封装,如果你通过函数访问获取某个成员变量,那么当你以后对这个class做出改变时,用户也不知道class的内部已经发生了改变。如果你对用户隐藏成员变量,你可以确保class的约束条件总是获得维护,因为只有成员函数可以影响到他们。
请记住,将成员变量声明为private,这可赋予客户访问数据的一致性,可细微划分访问控制,并给与class设计者更高的弹性,且protected并不比public更具封装性。
class AccessLevels{
public:
int getReadOnly(){ return readOnly;}
void setReadWrite(int value){readWrite = value;}
int getReadWrite(){return readWrite;}
void setWrite(int value){writeOnly = value;}
private:
int noAccess;
int readOnly;
int readWrite;
int wrieOnly;
};
19.宁以non-member,non-friend替换member函数
面向对象手则要求,数据以及操作数据的函数应该捆绑在一起,这意味着他建议member函数时比较好的选择,但事实上面向对象要求数据尽可能的被封装,这就要降低数据可能被访问的可能性,也就是说要尽可能的减少member函数和friend函数,这样可以降低编译的依赖性。
在c++里比较自然的做法就是使用命名空间,将类和方法置于同一命名空间。
namespace WebBrowserStuff{
class WebBrowser{ ... };
void clearWebBrowser(WebBrowser& web);
}
当一个像WebBrowser这样的class有很多的便利函数时,为了防止便利函数之间长生依赖关系,我们可以将其函数声明置于不同的头文件中。
//webbrowser.h
namespace WebBrowserStuff{
class WebBrowser{ ... }
//webbrowserbookmarks.h
namespace WebBrowserStuff{
//与书签有关的便利函数
}
//webbrowsercookies.h
namespace WebBrowserStuff{
//与cookies有关的便利函数
}
}
20.尽可能延后变量定义式的出现时间
所谓的尽量延后变量定义式的出现事件就是,变量的定义应该紧靠使用其的地方,这样可以避免构造(和析构)非必要对象,还可以避免无意义的default构造行为。
std::string encryptPassWord(const std::sting &password)
{
using namespace std;
string encrypted;
if(password.length() < MiniumPassWordLeength)
{
throw logic_error("Password is too short");
}
....
return encrypted;
}
---延后
std::string ecryptPassWord(const std::string &password)
{
...
std::string encrypted(password);
encrypted(encrypted);
return encrypted;
}
尽可能延后变量定义式的出现,这样可增加程序的清晰度并改善程序效率。
21.尽量少做转型操作
c++规则的设计目标之一是,保证“类型错误”绝不可能发生,理论上如果你的程序很干净的通过编译,就表示它并不企图任何对象身生执行任何不安全地,无意义的操作。不幸的是,类型转换破坏了类型系统,那可能导致任何种类的麻烦,有些非常晦涩。
c++提供了四种转型方式:
const_cast<T>(expression);
用于对象的常量型转除
dynamic_cast<T>(expression);
用来决定某对象是否归属某一继承体系
static_cast<T>(expression);
强制隐式转换
reinterpret_cast<T>(expression);
低级转换,取决于编译器
任何一个类型转换,无论是显示转换还是编译器隐式转换都会影响运行时效率,如果非要使用转型操作,那么尽量使用c++的四种转型方式
22.避免返回handles指向对象内部成分
class Rectangle{
public:
Point upperLeft() const{return pdata->ulhc;}
private:
std::tr1::shared_ptr<RectData> pData;
};
这里虽然可以通过编译,但是逻辑上存在自我矛盾,它const成员函数返回了成员变量的引用到外部,那么外部便可通过其修改内部成员,这是降低对象封装性。还要一点便是万一传出去的handle的生命周期大于对象本身,那么这也会导致问题的发生。
23.透彻了解inline的里里外外
编译器最优化机制常常用来浓缩那些不含函数调用的代码,当你inline某个函数时,编译器可能有能力对其执行语境相关的最优化。大部分编译器绝对不会对一个非内联函数执行最优化处理。内联是以空间换取时间,过度使用inline会造成程序体积过大,代码膨胀会导致额外的换页行为,降低指令高速缓存装置的击中率,以及伴随着一切而来的效率损失。inline只是对编译器的一个申请,不是强制命令,他可以显示inline也可通过定义在class中隐式表示内联。
class Test{
public:
Test(){}//隐式内联
void test();
};
inline void Test::test(){ ... }//显示内联
构造函数和析构函数往往不适合成为内联函数,因为优化策略,编译器可能会向其内插入一段代码。
24.将文件间的编译依赖关系降到最低
在日常编写c++代码中,我们曾遇到只改动一条语句,在重新编译时仍会执行很久,这个问题在于c++并没有将接口从实现中分离做的很好。
namespace std{
class string;//错误的前项声明
}
class Test{
public:
std::string name() const;
};
首先c++不进行这样设计的原因是,string并不是一个类,他是一个typedef,第二前向声明必须在编译器知晓对子那个大小以便于分配内存。
故而以声明的依存性替代定义的依存性,这才是编译依存性最小的关键,尽量让头文件自我满足,做不到时才让其依存其他文件的声明式。
25.确定public继承而来的类符合is-a关系
c++进行面向对象编程的重要规则是,public继承意味着is-a关系,适用于基类身上的每一件事一定适用于子类才行。
26.绝不重新定义继承而来的缺省参数值
虚函数是动态绑定,而缺省参数值是静态绑定。
class Shape{
public:
enum ShapeColor{ red,green,blue}
virtual void draw(ShapeColor color = red)const = 0;
};
class Rectangle:public Shape{
public:
virtual void draw(ShapeColor color = green)const;
};
class Circle:public Shape {
public:
virtual void draw(ShapeColor color) const;
};
SHape * ps;
Shape * pc = new Circle;
Shape * pr = new Rectangle;
ps = pc;
ps = pr;
pc->draw();//调用Circle::draw(Shape::Red)
pr->draw();//调用Rectangle::draw(Shape::Red);
27.通过复合塑模出has-a
通常组合比继承更具有弹性。
28.不要忽视编译器的警告
class B{
public:
virtual void f() const;
};
class D:public B{
public:
virtual void f();
};
waring: D::f() hides virtual B::f();
上述代码的区别在于D类会继承B类的成员函数f,但是D类自己实现了一个f函数,他并不是const成员函数,故而会隐藏继承过来的函数,并不是override。