码迷,mamicode.com
首页 > 编程语言 > 详细

Effictive C++知识点复习

时间:2017-08-02 11:54:28      阅读:271      评论:0      收藏:0      [点我收藏+]

标签:有关   设计   ret   出错   等于   了解   amp   拷贝构造函数   using   

1、尽量以const、enum、inline替换#define或者宁可以编译器替换预处理器
eg:#define NUM_RATIO 1.653
由于NUM_RATIO在编译器开始处理源码之前都被预处理器移走,因而当常量在编译时出错,只会提示到1.653.
对于程序员并不知道1.653在哪个文件中存放。故追踪会浪费时间。即所使用的名称并未进入记号表中。
解决方法:用一个常量替换上面的宏
const double NumRatio = 1.653;
注意:两个常量定义时的写法
2、若在头文件定义一个常量的字符串(char*),则必须写两次const
const char* const authorName = "uestc";
3、class专属常量,为了将常量的作用域限制于class内,必须让它成为class的一个成员。为了确保此常量只有一份实体,必须让他成为一个static成员
class GamePlayer
{
private:
static const int NumTurns = 5;//只是声明并没有定义
int score[NumTurns];//使用常量
};
有一个问题出现,若是要对NumTurns取地址,必须声明且定义。或者编译器要求必须有定义,则此时就必须对该变量进行定义处理。
const int GamePlayer::NumTurns;由于声明已获得初值,因此定义时不可以再设初值。
另外一个问题,若编译器要求不能在一个类中对static进行设定初值。即"in-class初值设定"也只允许对整数常量进行。则此时只能将初值放在定义式。
class GamePlayer
{
private:
static const int NumTurns;//只是声明并没有定义
int score[NumTurns];//使用常量
};
const int GamePlayer::NumTurns=5;
上述出现了问题,就是NumTurns要求必须在编译时期得到其值。则改用enum方法。
class GamePlayer
{
private:
enum{NumTurns = 5};//只是声明并没有定义
int score[NumTurns];//使用常量
};
此时就不能对NumTurns 取地址。
如果不想让别人获得一个指针或者引用指向你的某个整数常量,就可以使用enum.
4、当用实现宏,看起来像函数,但不会招致函数调用带来的开销。
#define MAX(a,b) f((a)>(b)?(a):(b))
你必须为宏中的所有实参加上小括号。
int a = 5, b = 0;
MAX(++a, b);
调用MAX之前,a的递增次数取决于“它被拿来和谁比较”
template<typename T>
inline void MAX(const T&a, const T&b)
{
f(a>b?a:b);
}
总结:
····对于单纯常量,最好以const对象或enums替换#defines
····对于形似函数的宏,最好改用inline函数替换#define

----------------------------------------------------------

1、尽可能使用const
声明指针为const ,即T* const指针。
声明指针所指向的东西是const,则const T*
例如:vector
const std::vector<int>::iterator iter = vec.begin();//如同T* const
*iter = 10; //改变iter所指物,没问题
++iter; //错误, iter是const
std::vector<int>::const_iterator cIter = vec.begin();//cIter的作用是const T*
*cIter = 10;//*cIter是const
++cIter;//没问题,改变cIter
2、const成员函数
将成员函数作为const修饰,目的就是为了确认该成员函数可作用于const对象身上。
重要的原因:
可以知道哪个函数可以改动对象内容而哪个函数不行;
使操作对象成为可能。
3、如果两个成员函数只是常量型不同,可以被重载
4、const成员函数不可以更改对象任何non-static成员变量
mutable释放掉non-static成员变量的const特性,可能会被const成员函数修改。
5、可以使用non-const版本的成员函数调用const版本,使用const_cast去除const即可。
但是不可以用const版本调用non-const版本。
---------------------------------------------------
1、确定对象被使用前已先被初始化
2、对象的成员变量的初始化发生在构造函数本体之前。
而在构造函数内部,只是对成员变量进行赋值的。
3、 构造函数的成员初始值列替换赋值动作,此时对成员变量而言是初始化操作。
默认构造函数则是先为成员变量进行设初值,然后立刻再对它赋予新值。
成员初始值列的效率大于默认构造函数。(内置类型效率是一样的)
4、成员初始化次序总是以其声明次序被初始化。
5、若成员变量是const或者引用类型,他们就一定需要初值,不能被赋值。
6、static对象包括global对象,定义在namespace作用域内的对象,在classes内,在函数内,以及在file作用域内被声明为static的对象。
non-logical static对象是除了在函数内的对象都是。
函数内的对象叫做logical static对象。
7、编译单元--是指产出单一目标文件的那些源码。
如果某一个编译单元内的某个non-local static对象的初始化动作使用了另一编译单元内的某个non-local static 对象,它所用到的这个对象可能尚未初始化。
定义于不同编译单元内的non-local static 对象的初始化次序并无明确定义。
解决方法:将每个non-local static对象搬到自己的专属函数内(该对象在函数内声明为static)。这样做,C++保证,函数内的local static对象会在该函数被调用期间首次遇上该对象的定义式时被初始化。
8、这些函数内涵static对象在多线程系统中带有不确定性。运用返回引用函数防止初始化次序问题。
9、请为内置型对象进行手工初始化,因为C++不保证初始化他们。
10、为免除跨编译单元之初始化次序问题,请以local static 对象替换non-local static对象。
-----------------了解C++默默编写并调用那些函数------------

1、什么时候空类不再时空类呢?当C++处理之后。
编译器会自动生成
拷贝构造函数
赋值构造函数
析构函数
默认构造函数(没有声明任何构造函数之前)
这些函数都是public且inline
只有这些函数在被需要才会被编译器创建出来。
2、在一个内含引用成员的class以及内含const成员的class,必须定义自己的拷贝赋值操作符。
3、若基类将拷贝赋值操作符声明为private,编译器将拒绝为其派生类生成一个copy赋值操作符。因为编译器为派生类生成的拷贝赋值操作符中需要处理base class 成分。
------------------不想使用编译器自动生成的函数------------
对于拷贝函数和赋值函数,将其声明为私有private且必须不能对其进行定义。
但是类成员函数和类友元函数还是可以调用private函数,此时连接器会发生错误,进而转移到编译期也是可能的。因而若不想使用编译器自动生成的函数,就将函数声明为私有,且不对其进行定义。
通常并不是在该类本身做私有操作,而是在该类的基类做相应的操作,然后做private继承。(这样做的原因,是在该类做拷贝和赋值操作时,编译器会试图生成一个拷贝构造函数和赋值构造函数,此时这些函数会尝试调用基类的对应的构造函数,基类会拒绝)

-----------为多态基类声明virtual析构函数----------------
了解工厂函数,返回一个基类指针,指向新生成的派生类对象。
什么时候会用到工厂函数?
当客户只关心使用的时间,而不想关心时间如何计算的细节,此时就可以使用工厂函数,返回的指针是一个基类指针,指向新生成派生类对象。工厂函数的返回对象必须是动态分配的对象,即存储在heap中。主要还要记得用delete 父类指针;
eg:
父类指针* getTimeKeeper()
{
return new 子类对象;
}
由于父类指针指向子类对象,但是要delete 父类指针,而new 的是子类对象,故此时基类必须有virtual 析构函数。
若是一个non-virtual 析构函数,实际执行通常发生的是派生类对象的成分没被销毁。此时交造成了资源泄露,局部销毁对象行为。

但是,并不是所有的析构函数都是virtual,当一个类不企图做基类时,不要将其析构函数作为一个virtual,因为会占用空间。

当想创建一个抽象类,但手上没有任何纯虚函数,且抽象类又常做基类,基类中常要含有一个虚析构函数。故此时就要有一个纯虚析构函数。注意你必须为纯虚析构函数提供一份定义。
析构函数的运作方式:最深层派生的那个类其析构函数最先被调用,然后是其每一个基类的析构函数。

------------别让异常逃离析构函数----------------------
析构函数突出异常会导致程序过早结束或者发生不明确行为的风险。
第一种情况,通过调用abort完成;抢先制止不明确行为于死地
A::~A()
{
try
{
db.close();//此时若抛出异常;
}
catch(...)
{
std::abort();//强制结束程序;
}
}
第二种情况,吞下异常
A::~A()
{
try
{
db.close();//此时若抛出异常;
}
catch(...)
{
制作运转记录;//吞下异常
}
}
最好的方法,就是将close()责任从析构函数转移到对象上。
class A
{
public:
void close()
{
db.close();
close = true;
}
~A()
{
if (!close)
{
try
{
db.close();//将异常问题转移到对象来处理。
}
catch(...)
{
制作运转记录;
}
}
}
};

---------------绝不在构造和析构过程中调用virtual函数--------
基类构造期间virtual 函数绝不会下降到派生类。即在base class 构造期间,virtual 函数不是vitual 函数。
class A
{
public:
A();
virtual void Copy() const = 0;
};
A::A()
{
...
Copy();//此时会出现无法连接,因为连接器找不到必要的A::copy实现的代码。
}
class B:public A
{
virtual void Copy() const;
}
B b;//首先会调用A的构造函数。其次再调用B 的构造函数。即B对象内的基类成分会在派生类自身成分被构造之前先构造。而A的构造函数中会调用virtual函数copy。这时候调用的copy是A类中的版本,不是B类中的版本。
这就是基类构造期间virtual 函数绝不会下降到派生类。
原因是:在派生类对象的基类构造期间,对象类型时基类,而不是派生类。不只virtual函数会被编译器解析至基类,若使用运行期类型信息(dynamic_cast 和 typeid),也会把对象视为基类类型。
同理:也适用于析构函数。
注意:不仅要确定构造函数和析构函数都没有virtual函数,而他们调用的所有函数也都服从同一约束。
eg:
class A
{
public:
A()
{
init();//错误。不仅要保证构造函数没有虚函数,而且构造函数所调用的函数内也不能有虚函数。
}
virtual void Copy() const = 0;
private:
void init()
{
...
Copy();
}
};
总结;无法使用虚函数从基类向下调用,在构造期间,你可以,让派生类将必要的构造信息向上传递到基类的构造函数。

------------令opertator=返回一个引用*this-----------
为了实现连锁赋值,则赋值操作符必须返回一个引用指向操作符的左侧实参。
实现自我赋值----
class A
{
private:
char* m_data;
};
A& A::operator=(const A& a)
{
if (this == &a)
{
return *this;
}
delete[] m_data;
m_data = NULL;
m_data = new char[strlen(a.m_data)+1];
strcpy(m_data, a.m_data);
return *this;
}
考虑异常安全性问题
A& A::operator=(const A&a)
{
char* tmp = m_data;
tmp = new char[strlen(a.m_data)+1];
delete[] tmp;
return *this;
}

----------复制对象时勿忘其每一个成分-------------
注意为派生类写拷贝函数时,必须要复制其基类成分,那些成分往往时private,所以你无法直接访问他们,你应该让派生类的拷贝函数调用相应的基类函数。
class A
{
A(const A& rhs)
{
a = rhs.a;
}
private:
int rhs;
};
class B:public A
{
B(const B& lhs):A(lhs),b(lhs.b);
{
}
private:
int b;
};
不要尝试以某个拷贝函数实现另一个拷贝函数。而应该将共同机能放进第三个函数中,并由两个拷贝函数共同调用。
--------------------以对象管理资源----------------
将资源放在对象内,保证析构函数会自动调用确保资源被释放。
例如auto_ptr是个类指针对象。其析构函数会自动对其所指对象调用delete.
void f()
{
std::auto_ptr<Investment> pInv(createInvestment());
}
涉及2个知识点:
获得资源后立即放进管理对象内----调用工厂函数之后,返回的资源被当作其管理者auto_ptr的初值。
管理对象运用析构函数确保资源被释放---一旦对象被销毁,其析构函数自然会被自动调用,于是资源被释放。

注意:auto_ptr销毁时会自动删除它所指的对象,因而不要让多个auto_ptr同时指向同一对象。
解决方案:使用引用计数的智慧指针,即shared_ptr。
尽量不要将动态分配用在auto_ptr 和 shared_ptr;
不要 shared_ptr<int> spi(new int[1024]);因为这样必须使用delete spi,而不是delete[] spi;

------------在资源管理类中小心拷贝行为------------
···通常是禁止复制。
···对底层资源祭出引用计数法--将资源的引用计数递增。shared_ptr就是这样的。
(因为shared_ptr允许指定所谓的删除器,那是一个函数或者函数对象,当引用次数为0时便被调用)
···或者复制底部资源
···转移底部资源的拥有权

------------在资源管理类中提供对原始资源的访问------------
std::auto_ptr<Investment> pInv(createInvestment());
假设你希望以某个函数处理Investment对象。
int daysHeld(const Investment* pi);
这样调用
daysHeld(pInv);错误。因为daysHeld需要的是Investment* 指针。
但传给它的却是auto_ptr<Investment>对象
此时需要一个函数将RAII class对象转换为原始资源,即Investment*资源。
即shared_ptr 和 auto_ptr都提供了一个get成员函数。用来执行显示转换,也就是它会返回智能指针内部的原始指针。
即更改为
int days = daysHeld(pInv.get());此时正确。显示转化原始指针。

shared_ptr 和 auto_ptr 都重载了指针取值操作符,因而他们允许隐式转换到底部原始指针。
std::auto_ptr<Investment> pInv(createInvestment());
bool taxable = !(pInv->idTaxFree());隐式转换为原始指针,然后调用operator->访问资源。

----------成对使用new 和delete时要采取相同形式----------
new操作符
··内存被分配出来;
··针对内存会有一个(或更多)构造函数被调用。
delete操作符
··会由一个(或更多)析构函数被调用;
··内存才会被释放。
typedef string AddressLines[4];
string* pa1 == new AddressLines;//和ew string[4]一样。
deletep[] pal;
因而最好尽量不要对数组形式做typedef 动作。

-------以独立语句将newed对象置入智能指针-------------
void processWidget(std::tr1::shared_ptr<Widget> pw, int priority);
processWidget决定对其动态分配得来的Widget运用智能指针。
当调用processWidget
processWidget(new Widget, priority());
不能通过编译。shared_ptr构造函数需要一个原始指针,但该构造函数是个explicit构造函数,无法进行隐式转换。
更改为:
processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());
但是这样会出现内存泄漏;
函数参数需要做以下三件事:
·执行new Widget;
·调用priority();
·调用tr1::shared_ptr构造函数
当调用priority()出现异常,则会发生内存泄露。
---以独立语句将newed对象存储于置入智能指针内。否则会有异常抛出。
避免这类问题,使用分离语句,分别写出
(1)创建Widge,
(2)将它置入一个智能指针内,然后
再把那个智能指针传给processWidget
std::tr1::shared_ptr<Widget> pw(new Widget);
processWidget(pw, priority());

----让接口容易被正确使用,不易被误用---------------
示例1:
struct Month{
Month(int mon):val(mon){}
int val
};
struct Day{
Month(int day):val(day){}
int val
};
struct Year{
Month(int year):val(year){}
int val
};
接口就会设计成这样:
Data(Month , Day, Year);
那么调用的时候可能就需要这样去调用:
Data(Month(month), Day(day), Year(year));
示例2:
Investment * createInvestment();
//使用原始指针可能会使得用户饭错误的概率大大增加,这里尝试使用一个智能指针来返回更好!
shared_ptr<Investment> createInvestment()
{
...
return shared_ptr<Investment>(new Investment(tmpIvst));
}
一般返回智能指针应该给他指定一个更好的删除器。
对于一个指针在一个Dll中被赋值,但是另一个Dll中却被delete掉,而
对于shared_ptr就不会有这种问题,其删除器是在new的时候就指定好了的删除器,所以不会出现像上述那样的跨越Dll的new/delete情况。

---------设计class相当于设计一个内置类型---------
1. 新的type应该如何创建与销毁
2. 对象的初始化与赋值应该有什么样的区别
3. 新type的对象如果被pass-by-value,有什么影响?
4. 什么事新type的合法值
5. 新的type需要什么样的转换
6. 什么样的操作符和函数对于这个type而言是合理的
7. 什么样的标准函数应该驳回
8. 谁应该取用新的type成员
9. 什么事新type的未声明接口
10. 新的type应该如何的进行一般化
11. 这个新的type的建立是一定的吗?

-----------------以按常引用传递替换按值传递----------------
按值传递---通常都以实际实参的副本为初值,而调用端所获得也是函数返回值的一个复件。这些复件也是由对象的拷贝否早函数产生,故会带来额外的开销。
按引用传递,没有任何构造函数或析构函数被调用。因为没有任何新对象被创建。同时也避免了切割问题。当一个派生类对象以按值方式传递一个基类对象,基类的拷贝构造函数会被调用,而此时派生类对象的特化性质都被切割掉,仅仅保留了一个基类对象。而解决切割的问题就是以按引用方式传递。因为引用往往以指针实现出来。
一般而言,按值传递的对象是内置类型和STL迭代器和函数对象。其他的尽可能按照引用方式传递。

-----------必须返回对象时,别忘返回其引用---------------

返回引用,可以减少返回时候拷贝带来的开销操作;
1.在函数中使用new在heap上创建对象而不是在stack上创建对象:
Rational & operator(const Rational & lhs, const Rational & rhs)
{
Retional result = new Rational(lhs.numirator + rhs.numirator, lhs.denumirator * rhs.denumirator);
return result;
}
但是如果这么做的话谁又来保证客户可以安然无恙的将资源合理的释放呢。
2.在函数内部用static对象来取代临时变量,像下面这样:
const Rational & operator*(const Rational & lhs, const Rational & rhs)
{
static Rational result;
Rational = Ratioanl(lhs.numirator * rhs.numirator, lhs.denumirator * rhs.denumirator);
return result;
}
首先,加入static可定会对多线程的情况带来一些麻烦。 
Rational a, b, c, d;
3.if(a * b == c * d);
//相当于if(operator*(a, b) == operator*(c, d))
显然,这两个operator*返回的static变量肯定是指向同一个的,那么这个if必定等于1。其实正常的情况就是返回一个value就可以了,虽然可能带来效率上的损失(只是可能,现代的编译器有的都能消除这种情况带来的损失),这种损失也可以使用std::move来加以弥补。

总结:不能返回指针或引用指向一个栈对象,或返回引用指向一个堆分配对象,或返回指针或引用指向一个局部静态对象而有可能同时需要多个这样的对象。

----------将成员变量声明为private-------------------
1.protected成员变量的封装性并非高于public变量。
成员变量的封装性与成员变量的内容改变时所破坏的代码数量成反比。因为一个public成员变量改变,会导致所有使用过它的代码都会发生破坏。而protected 成员变量改变,会导致所有使用它的派生类都会被破坏。
因此从封装的角度来看,其实只有两种访问权限:private(提供封装)和其他(不提供封装)
2.将成员变量声明为private,这可赋予客户访问数据的一致性,可细微划分访问控制,提供class充分实现弹性。

------------宁以non-number、non-friend替换member函数-------
1.
class webBroswer
{
public:
...
void clearCache();
void clearHistory();
void removeCookies();
...
};
2.
member函数
class WebBrowser
{
public:
...
void clearEverything();
};
3.
non-member函数
void clearBrowser(WebBrowser & wb)
{
wb.clearCache();
wb.clearHistory();
wb.clearCookie();
}
比较,是member函数还是non-member函数好呢?
面向对象的误解问题,数据以及操作数据的那些函数应该被捆绑在一块。意味着member函数是较好的选择。
面向对象的守则要求是数据尽可能被封装,根源member带来的封装性比non-member函数要低。
封装的原因---使我们能够改变事物而只影响有限客户。
但是越多东西被封装,就越少人可以看到它,因而就有越大的弹性去变化它。因为我们的改变仅仅直接影响看到改变那些东西的能力也就越大。
为什么是non-member 和 non-friend具有较大的封装性呢?
因为它并不增加能够访问class 内的private成分的函数数量。但是这个non-menber可以是另一个class 的member。

C++为了保证具有较强的封装性,通常将一个non-member函数且将其位于类所在的同一个命名空间内,
namespace A
{
class A{};
void ClearA(A& a);//non-member函数,这样的便利函数可以在一个namespace里面声明多个。
}
注意namespace 和 classes的区别:
前者可以跨越多个源码文件,而后者不能。

---------若素有参数都需类型转换,请采用non-member函数-----
实例:
class Rational
{
public:
const Rational operator*(const Rational& rhs)const;
....;
};
Rational onehalf(1, 2);
result = onehalf*2;正确;
result = 2*onehalf;错误;
等价于
result = onehalf.operator*(2);正确;
result = 2.operator*(onehalf);错误;
通过结论,只有当参数被位于参数列内,这个参数才是隐式类型转换的合格参与者。
解决方法:支持混合式算术运算。让operator*成为一个non-member函数。
class Rational
{

};
const Rational operator*(const Rational& lhs, const Rational& rhs)
{
return Rational(lhs.numberator()*rhs.numberator(), lhs.denominator()*rhs.denominator());
}
result = onehalf*2;正确;
result = 2*onehalf;正确;

------------考虑写出一个不抛出异常的swap函数-----------
1.标准库的swap算法
namespace std
{
template<typename T>
void swap(T & a, T & b)
{
T tmp = a;
a = b;
b = tmp;
}
}
2.如何完成高效的swap操作,比如拷贝两个对象。
class WidgetImpl
{
public:
...
private:
int a, b, c;
std::vector<double> v;
...
};
class Widget
{
pubcic:
.....
private:
WidgteImpl * pImpl;
};
如何高效交换WidgteImp1类对象
第一种,将函数特例化-----针对的是class而言;
class Widget
{
public:
...
void swap(Widget & other)
{
using std::swap;
swap(pUmpl, other.pUmpl);
}
...
};
namespace std
{
template<>
void swap<Widget>(Widget & a, Widget & b)
{
a.swap(b);
}
}
第二种做法:
当Widget和WidgetImp1都是class templates而非classes

template<typename T>
class WidgetImpl
{
...
};
template<typename T>
class Widget
{
...
};
此时不能在对swap特例化;而是做偏特化.错误。C++编译器只允许对类做偏特化。
namespace std
{
template<typename T>
void swap<Widget<T>>(Widget<T>& a, Widget<T>& b)
{
a.swap(b);----错误错误错误
}
}
对于函数偏特化模板,通常为它添加一个重载版本
namespace std
{
template<typename T>
void swap(Widget<T>& a, Widget<T>& b)
{
a.swap(b);
}
}
std规定,可以全特化std内的templates,但不可以添加新的template到std里。
故第二种方法,只能这样做
namespace WidgetStuff
{
template<typename T>
class Widget
{
...
};
template<typename T>
void swap(Widget<T>& a, Widget<T>& b)
{
a.swap(b);
}
}
C++名称查找法则,先global作用域或者T所在命名空间内任何T专属的swap;然后是T所在的命名空间,然后std::swap。
考虑写出一个不抛出异常的swap函数总结:在你的class 或 template所在的命名空间内提供一个non-menber swap,并令它调用上述的swap。

-----------尽可能延后变量定义式的出现时间-----------
尽量少做转型动作
1. const_cast:常量性的转除。
2. dynamic_cast:安全的向derived --class进行转型,可能会带来很高的开销
3. reinterpret_cast:低级转型,例如可讲pointer转成int,不建议使用
4. static_cast: 强迫隐式转换,例如int to const int,int to double, 但是const int 到 int 只有const_cast能做到。

-----避免返回handles指向对象的内部成分------------
class Point
{
public:
point(int x, int y);
...
void setX(int newVal);
void setY(int newVal);
...
};
struct RectData
{
Point ulhc; //左上角
Point lrhc; //右下角
};
class Rectangle
{
...
private:
shared_ptr<RectData> pData;
};
class Rectangle
{
public:
...
Point & upperLeft() const {return pData->ulhc; }
Point & lowerRight() const{return pData->lrhc; }
...
};
这样只是能通过编译,但是设计确是错误的,在成员函数被声明为const的情况下返回了一个内部成员的引用,这样使得ulhc 以及 lrhc对象都可以被更改。但是二者都是private的,实际上二者都是不应该被改变的。
返回句柄的对象都会造成内部状态暴露在外面。容易改变内部状态或者造成空悬的指针。
解决方法:就是在成员函数返回的handle前加上const。

---------为异常安全努力是值得的-------------
异常安全性包括:
不泄露任何资源;
不允许数据败坏;

-------透彻了解inlining的里里外外------------
inlining的优点:
1.可以消除函数调用带来的开销;编译器对这种非函数代码可以做出优化策略;
2.若inline函数本体很小的话,那么inline函数展开的代码大小可能比调用非inline函数展开的代码更小,这样就可以提高缓存的集中率,从而提高函数的效率。

inlining的缺点:
肯定会导致程序代码的大小更加的庞大,这样会带来代码膨胀,造成损害,首先就是会导致计算机额外的换页行为,降低指令告诉缓存的集中率。这就会带来效率的损失。


标准库中的template::max实际上就是一个inline函数,但是并非所有的template版本的函数都是inline函数。 如果声明某个模板的时候认为其所有的具体化都应该是模板,那么就应该将模板声明成为inline的形式。

第一种实例:
inline void f(){...}
void (*pf)() = f;
...
f();
pf();
上面的f()可能会被正确的inline,但是下面的pf并不一定,因为函数要提供地址给pf,那么一般是应该实现出inline函数的一个outline版本提供给函数指针的。
第二种实例:
class Base
{
public:
private:
std::string;
};
class Derived : public Base
{
public:
Derived(){}//它不能是inline。因为derived构造函数至少一定会陆续调用其成员变量和base class两者的构造函数。而那些调用会影响编译器是否对此空函数inlining.
...
private:
std::string dm1, dm2, dm3;
};

而且inline函数是无法链接的,这样当函数有改动的时候,会导致整个模块的重新链接, 可能造成较为恐怖的开销。
inline 函数若被改变,则必须重新编译;
non-inline一旦有任何修改,只需重新连接。

-----------------将文件间的编译依存关系降至最低-------------
避免大量依赖性编译的解决方案是:在头文件中用class声明外来类,用指针或引用代替变量的声明,在CPP文件中包含外来类的头文件。
1.
假设有三个类ComplexClass, SimpleClass1和SimpleClass2,采用头文件将类的声明与类的实现分开,这样共对应于6个文件,分别是ComplexClass.h,ComplexClass.cpp,SimpleClass1.h,SimpleClass1.cpp,SimpleClass2.h,SimpleClass2.cpp。
ComplexClass复合两个BaseClass,SimpleClass1与SimpleClass2之间是独立的,ComplexClass的.h是这样写的:

#ifndef COMPLESS_CLASS_H
#define COMPLESS_CLASS_H

#include “SimpleClass1.h”
#include “SimpleClass2.h”

class ComplexClass
{
SimpleClass1 xx;
SimpleClass2 xxx;
};

#endif /*COMPLESS _CLASS_H*/

问题1:当SimpleClass1.h发生了变化,比如添加了一个新的成员变量。则此时SimpleClass1.cpp必须重新编译。由于SimpleClass2因为与SimpleClass1是独立的,所以SimpleClass2是不需要重编的。那么现在的问题是,ComplexClass需要重编吗?
引出话题1:
答案是“是”,因为ComplexClass的头文件里面包含了SimpleClass1.h(使用了SimpleClass1作为成员对象的类),而且所有使用ComplexClass类的对象的文件,都需要重新编译!

再次引出问题:
如果把ComplexClass里面的#include “SimpleClass1.h”给去掉,当然就不会重编ComplexClass了,但问题是也不能通过编译了,因为ComplexClass里面声明了SimpleClass1的对象xx。那如果把#include “SimpleClass1.h”换成类的声明class SimpleClass1,会怎么样呢?能通过编译吗?
答案是“否”,因为编译器需要知道ComplexClass成员变量SimpleClass1对象的大小,而这些信息仅由class SimpleClass1是不够的,但如果SimpleClass1作为一个函数的形参,或者是函数返回值,用class SimpleClass1声明就够了。如:

1 // ComplexClass.h
2 class SimpleClass1;
3 …
4 SimpleClass1 GetSimpleClass1() const;
5 …

但如果换成指针呢?像这样:
// ComplexClass.h
#include “SimpleClass2.h”

class SimpleClass1;

class ComplexClass:
{
SimpleClass1* xx;
SimpleClass2 xxx;
};
这样能通过编译吗?

答案是“是”,因为编译器视所有指针为一个字长(在32位机器上是4字节),因此class SimpleClass1的声明是够用了。但如果要想使用SimpleClass1的方法,还是要包含SimpleClass1.h,但那是ComplexClass.cpp做的,因为ComplexClass.h只负责类变量和方法的声明。

问题2:
那么还有一个问题,如果使用SimpleClass1*代替SimpleClass1后,SimpleClass1.h变了,ComplexClass需要重编吗?
先看Case2。
回到最初的假定上(成员变量不是指针),现在SimpleClass1.cpp发生了变化,比如改变了一个成员函数的实现逻辑(换了一种排序算法等),但SimpleClass1.h没有变,那么SimpleClass1一定会重编,SimpleClass2因为独立性不需要重编,那么现在的问题是,ComplexClass需要重编吗?
答案是“否”,因为编译器重编的条件是发现一个变量的类型或者大小跟之前的不一样了,但现在SimpleClass1的接口并没有任务变化,只是改变了实现的细节,所以编译器不会重编。

问题3:
Case 3:
// ComplexClass.h
#include “SimpleClass2.h”

class SimpleClass1;

class ComplexClass
{
SimpleClass1* xx;
SimpleClass2 xxx;
};
// ComplexClass.cpp

void ComplexClass::Fun()
{
SimpleClass1->FunMethod();
}
答案是“否”,因为这里用到了SimpleClass1的具体的方法,所以需要包含SimpleClass1的头文件,但这个包含的行为已经从ComplexClass里面拿掉了(换成了class SimpleClass1),所以不能通过编译。
解决问题:
只要在ComplexClass.cpp里面加上#include “SimpleClass1.h”就可以了。
这样做是为了什么?假设这时候SimpleClass1.h发生了变化,会有怎样的结果呢?
SimpleClass1自身一定会重编,SimpleClass2当然还是不用重编的,ComplexClass.cpp因为包含了SimpleClass1.h,所以需要重编,但换来的好处就是所有用到ComplexClass的其他地方,它们所在的文件不用重编了!因为ComplexClass的头文件没有变化,接口没有改变!
总结:对于C++类而言,如果它的头文件变了,那么所有这个类的对象所在的文件都要重编,但如果它的实现文件(cpp文件)变了,而头文件没有变(对外的接口不变),那么所有这个类的对象所在的文件都不会因之而重编。

---再理解Handles classes
#include <string>
#include "MyDate.h"
#include "MyAddress.h"
class Person
{
private:
string name;
MyDate birthday;
MyAddress address;

public:
// fallows functions
// ...
};
在MyDate.h里面写好日期类相关的成员变量与方法,而在MyAddress.h里面写好地址类相关的成员变量与方法。但如果此后要往MyDate类或者MyAddresss类添加成员变量,那么不仅仅所有用到MyDate或者MyAddress对象的文件需要重新编译,而且所有用到Person对象的文件也需要重编译,一个小改动竟然会牵涉到这么多的地方!
第一种是采用Handler Classes(用指针指向真正实现的方法),就是就是.h里面不包含类的自定义头文件,用“class 类名”的声明方式进行代替(也要把相应的成员变量替换成指针或引用的形式),在.cpp文件里面包含类的自定义头文件去实现具体的方法。
更改如下:
// Person.h
#include <string>
using namespace std;

class PersonImp;

class Person
{
private:
//string Name;
//MyDate Birthday;
//MyAddress Address;
PersonImp* MemberImp;

public:
string GetName() const;
string GetBirthday() const;
string GetAddress() const;
// follows functions
// ...
};
// Person.cpp
#include "PersonImp.h"
#include "Person.h"

string Person::GetName() const
{
return MemberImp->GetName();
}
string Person::GetBirthday() const
{
return MemberImp->GetBirthday();
}
string Person::GetAddress() const
{
return MemberImp->GetAddress();
}
在Person.h里面并没有使用MyDate*和MyAddress*,而是用了PersonImp*,由PersonImp里面包含MyDate与MyAddress,这样做的好处就是方便统一化管理,它要求PersonImp里面的方法与Person的方法是一致的。以后Person添加成员变量,可以直接在PersonImp中进行添加了,从而起到了隔离和隐藏的作用,因为客户端代码大量使用的将是Person,而不必关心PersonImp,用于幕后实现的PersonImp只面向于软件开发者而不是使用者。
第二种是Interface Classes(抽象基类)。
Interface Classes则是利用继承关系和多态的特性,在父类里面只包含成员方法(成员函数),而没有成员变量,
// Person.h
#include <string>
using namespace std;

class MyAddress;
class MyDate;
class RealPerson;

class Person
{
public:
virtual string GetName() const = 0;
virtual string GetBirthday() const = 0;
virtual string GetAddress() const = 0;
virtual ~Person(){}
};
// RealPerson.h
#include "Person.h"
#include "MyAddress.h"
#include "MyDate.h"

class RealPerson: public Person
{
private:
string Name;
MyAddress Address;
MyDate Birthday;
public:
RealPerson(string name, const MyAddress& addr, const MyDate& date):Name(name), Address(addr), Birthday(date){}
virtual string GetName() const;
virtual string GetAddress() const;
virtual string GetBirthday() const;
};
在RealPerson.cpp里面去实现GetName()等方法。
子类的头文件变化了,则子类一定会重编,所有用到子类头文件的文件也要重编。为了防止重编,应该尽量少用子类的对象。利用多态的特性,可以使用父类的指针。
Person* p = new RealPerson(xxx),然后p->GetName()实际上是调用了子类的GetName()方法。

问题引出:
new RealPerson()这句话一写,就需要RealPerson的构造函数,那么RealPerson的头文件就要暴露了,这样可不行。还是只能用Person的方法。
解决问题:
// Person.h
static Person* CreatePerson(string name, const MyAddress& addr, const MyDate& date);
这个方法是静态的(没有虚特性),它被父类和所有子类共有,可以在子类中去实现它:
// RealPerson.cpp
#include “Person.h”
Person* Person::CreatePerson(string name, const MyAddress& addr, const MyDate& date)
{
return new RealPerson(name, addr, date);
}
这样在客户端代码里面,可以这样写:
// Main.h
class MyAddress;
class MyDate;
void ProcessPerson(const string& name, const MyAddress& addr, const MyDate& date);
// Main.cpp
#include "Person.h"
#include “MyAddress.h”;
#include “MyDate.h”;

void ProcessPerson(const string& name, const MyAddress& addr, const MyDate& date)
{
Person* p = Person::CreatePerson(name, addr, date);

}
总结:
Handler classes与Interface classes解除了接口和实现之间的耦合关系,从而降低文件间的编译依存性。减少编译依存性的关键在于保持.h文件不变化,具体地说,是保持被大量使用的类的.h文件不变化,
1. 支持“编译依存性最小化”的一般构想是:相依于声明式,不要相依于定义式,基于此构想的两个手段是Handler classes和Interface classes。

2. 程序库头文件应该以“完全且仅有声明式”的形式存在,这种做法不论是否涉及templates都适用。

注意:重写只能适用于实例方法,不能用于静态方法。多余静态方法,只能隐藏(形式上被重写,但是不符合多态的特性),重写是用来实现多态性的,只有实例方法可以实现多态,而静态方法无法实现多态。

----------确定你的public继承继承塑模出is-a关系-----------------
在所有public继承的背后,一定要保证父类的所有特性子类都可以满足(父类能飞,子类一定可以飞),抽象起来说,就是在可以使用父类的地方,都一定可以使用子类去替换。
class Bird
{
public:
virtual void fly(){cout << "it can fly." << endl;}
};

class Penguin: public Bird
{
// fly()被继承过来了,可以覆写一个企鹅的fly()方法,也可以直接用父类的
};

int main()
{
Penguin p;
p.fly(); // 问题是企鹅并不会飞!
}
企鹅是鸟,但是企鹅不能飞,该怎么解决这个问题呢?
方法1:
在Penguin的fly()方法里面抛出异常,一旦调用了p.fly(),那么就会在运行时捕捉到这个异常。
方法2:
去掉Bird的fly()方法,在中间加上一层FlyingBird类(有fly()方法)与NotFlyingBird类(没有fly()方法),然后让企鹅继承与NotFlyingBird类。
方法3:
保留所有Bird一定会有的共性(比如生蛋和孵化),去掉Bird的fly()方法,只在其他可以飞的鸟的子类里面单独写这个方法
一句话:
is-a关系:----任何父类可以出现的地方,子类一定可以替代这个父类,只有当替换使软件功能不受影响时,父类才可以真正被复用。通俗地说,是“子类可以扩展父类的功能,但不能改变父类原有的功能”。
“public继承”意味着is-a。适用于base classes身上的每一件事情一定也适用于derived classes身上,因为每一个derived class对象也都是一个base class对象。

------------避免遮掩继承而来的名称-----------------
实例1
//例1:普通变量遮掩
int i = 3;

int main()
{
int i = 4;
cout << i << endl; // 输出4
}
这是一个局部变量遮掩全局变量的例子,编译器在查找名字时,优先查找的是局部变量名,找到了就不会再找,所以不会有warning,不会有error,只会是这个结果。

实例2
//例2:成员变量遮掩
class Base
{
public:
int x;
Base(int _x):x(_x){}
};

class Derived: public Base
{
public:
int x;
Derived(int _x):Base(_x),x(_x + 1){}
};

int main()
{
Derived d(3);
cout << d.x << endl; //输出4
}
因为定义的是子类的对象,所以会优先查找子类独有的作用域,这里已经找到了x,所以不会再查找父类的作用域,因此输出的是4,如果子类里没有另行声明x成员变量,那么才会去查找父类的作用域。那么这种情况下如果想要访问父类的x,怎么办呢?
利用Base::x,可以使查找指定为父类的作用域,这样就能返回父类的x的值了。
实例3:
//例3:函数的遮掩
class Base
{
public:
void CommonFunction(){cout << "Base::CommonFunction()" << endl;}
void virtual VirtualFunction(){cout << "Base::VirturalFunction()" << endl;}
void virtual PureVirtualFunction() = 0;
};

class Derived: public Base
{
public:
void CommonFunction(){cout << "Derived::CommonFunction()" << endl;}
void virtual VirtualFunction(){cout << "Derived::VirturalFunction()" << endl;}
void virtual PureVirtualFunction(){cout << "Derived::PureVirtualFunction()" << endl;}
};

int main()
{
Derived d;
d.CommonFunction(); // Derived::CommonFunction()
d.VirtualFunction(); // Derived::VirtualFunction()
d.PureVirtualFunction(); // Derived::PureVirtualFunction()
return 0;
}
与变量遮掩类似,函数名的查找也是先从子类独有的作用域开始查找的,一旦找到,就不再继续找下去了。这里无论是普通函数,虚函数,还是纯虚函数,结果都是输出子类的函数调用。
实例4:
//例4:重载函数的遮掩
class Base
{
public:
void CommonFunction(){cout << "Base::CommonFunction()" << endl;}
void virtual VirtualFunction(){cout << "Base::VirturalFunction()" << endl;}
void virtual VirtualFunction(int x){cout << "Base::VirtualFunction() With Parms" << endl;}
void virtual PureVirtualFunction() = 0;
};

class Derived: public Base
{
public:
void CommonFunction(){cout << "Derived::CommonFunction()" << endl;}
void virtual VirtualFunction(){cout << "Derived::VirturalFunction()" << endl;}
void virtual PureVirtualFunction(){cout << "Derived::PureVirtualFunction()" << endl;}
};

int main()
{
Derived d;
d.VirtualFunction(3); // 代码编译不通过,并不是调用父类的成员函数
return 0;
}

C++的确是支持重载的,编译器在发现函数重载时,会去寻找相同函数名中最为匹配的一个函数(从形参个数,形参类型两个方面考虑,与返回值没有关系),如果大家的匹配程度都差不多,那么编译器会报歧义的错。
因为编译器先查找子类中独有的域,一单发现了完全相同的函数名,就不再往父类查找,在核查函数参数时,发现了没有带整型形参,所以直接报编译错了。
只有去掉子类的virtualFunction(),才会找到父类的VirtualFunction(int).
解决上述问题:
一种采用using 声明;
class Derived: public Base
{
public:
using Base::VirtualFunction; // 第一级查找也要包括Base::VirtualFunction
void CommonFunction(){cout << "Derived::CommonFunction()" << endl;}
void virtual VirtualFunction(){cout << "Derived::VirturalFunction()" << endl;}
void virtual PureVirtualFunction(){cout << "Derived::PureVirtualFunction()" << endl;}
};
用了using,实际上是告诉编译器,把父类的那个函数也纳入第一批查找范围里面,这样就能发现匹配得更好的重载函数了。
--------------------
一种是定义转交函数;采用这种做法,需要在子类中再定义一个带int参的同名函数,在这个函数里面用Base进行作用域的指定,从而调用到父类的同名函数。
class Derived: public Base

1 {
2 public:
3 using Base::VirtualFunction;
4 void CommonFunction(){cout << "Derived::CommonFunction()" << endl;}
5 void virtual VirtualFunction(){cout << "Derived::VirturalFunction()" << endl;}
6 void virtual PureVirtualFunction(int x){cout << "Derived::PureVirtualFunction()" << endl;}
7 void virtual VirtualFunction(int x){Base::VirtualFunction(x)};//转交函
8 };


总结:
1. derived classses内的名称会遮掩base classes内的名称。在public继承下从来没有人希望如此。

2. 为了让被遮掩的名称再见天日,可使用using声明式或转交函数(forwarding functions)。

-----------区分接口继承和实现继承------------------
只要能理解三句话即可,
第一句话是:
纯虚函数只继承接口;
第二句话是:
虚函数既继承接口,也提供了一份默认实现;
第三句话是:
普通函数既继承接口,也强制继承实现。
解析:
纯虚函数有一个“等于0”的声明,具体实现一般放在派生中(但基类也可以有具体实现),所在的类(称之为虚基类)是不能定义对象的,派生类中仍然也可以不实现这个纯虚函数,交由派生类的派生类实现,总之直到有一个派生类将之实现,才可以由这个派生类定义出它的对象。

虚函数则必须有实现,否则会报链接错误。虚函数可以在基类和多个派生类中提供不同的版本,利用多态性质,在程序运行时动态决定执行哪一个版本的虚函数(机制是编译器生成的虚表)。virtual关键字在基类中必须显式指明,在派生类中不必指明,即使不写,也会被编译器认可为virtual函数,virtual函数存在的类可以定义实例对象。

普通函数则是将接口与实现都继承下来了,如果在派生类中重定义普通函数,将会出现名称的遮盖
普通函数所代表的意义是不变性凌驾与特异性,所以它绝不该在派生类中被重新定义。
注意:
书上提倡用纯虚函数去替代虚函数,因为虚函数提供了一个默认的实现,如果派生类的想要的行为与这个虚函数不一致,而又恰好忘记去覆盖虚函数,就会出现问题。但纯虚函数不会,因为它从语法上限定派生类必须要去实现它,否则将无法定义派生类的对象。
同时,因为纯虚函数也是可以有默认实现的(但是它从语法上强调派生类必须重定义之,否则不能定义对象),所以完全可以替换虚函数。
总结:
1. 接口继承和实现继承不同。在public继承之下,derived class总是继承base class的接口;
2. pure virtual函数只具体指定接口继承;
3. impure virtual函数具体指定接口继承和缺省实现继承;(可以重定义实现,则此时实现就会不再继承,但是接口仍然会继承)
4. non-virutal函数具体指定接口继承以及强制性实现继承。(不可以重定义在派生类)

----------考虑virtual函数意外的其他选择--------------

 

Effictive C++知识点复习

标签:有关   设计   ret   出错   等于   了解   amp   拷贝构造函数   using   

原文地址:http://www.cnblogs.com/maleyang/p/7272959.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!