目录
*条款23:宁以non-member、non-frined替换member函数
条款24: 若所有参数皆需类型转换, 请为此采用 non-member 函数
条款21 当必须返回对象时,不要尝试返回其引用
● 前一小节已经讨论过,pass by vaule的代价有时候是巨大的,pass by refrence比较方便。那么肯定也有人会立刻想到,函数返回值的时候,能不能也采用这种办法来提高程序的效率呢?
为了能够简单且说明问题,这里选择了对于内置类型返回其reference:
int& func()
{
int i=3;
return i;
}
void doNothing(int i)
{
}
int main()
{
int& k=func();
doNothing(7); //注释此行,看看有何区别
cout <<k <<endl;
return 0;
}
首先,返回局部变量的引用是不对的。因为如果你返回一个引用,引用肯定是一个对象的别名,那么你一定要先反问自己:这个引用所代表的实际值到底是谁呢?想到这里,我们就一目了然了:你返回的引用是局部变量 i 的别名,但是 i 在函数结束以后,就被释放了!所以此时的返回的是一个垃圾数字。这个程序奇怪的地方在于,如果把doNothing函数注释掉,那么程序竟然能得出正确的值,这里的奥秘在于如果调用了函数,那么栈的内容有可能发生改变,改变之后,程序就得出合理的错误值了。而当这个函数调用被注释掉以后,由于栈的内容没有改变,所以即使 i 已经被释放了,但是它并没有被清零,值还是3,我还是可以引用它。类似的例子还有就是返回局部变量的指针:
int* func()
{
int i=3;
int *p = &i;
return p;
}
void doNothing(int i)
{
}
int main()
{
int *p = func();
doNothing(1); // 再次注释看看区别在哪
cout<<*p<<endl;
return 0;
}
这个程序的结果跟上面的程序结果一样, 但我们调用 doNothing 函数, 然后输出 *p 的 结果就是个垃圾值。 如果我们注释掉doNothing 函数, *p 的输出结果还是3.
下面看一个书中的例子:
class Rational
{
private:
int numerator; // 分子
int denominator; // 分母
public:
Rational():numerator(0), denominator(1){}
friend const Rational& operator* (const Rational& r1, const Rational& r2){…}
};
我们可以设想一下实现:
friend const Rational& operator* (const Rational& r1, const Rational& r2)
{
Rational temp;
temp.numerator = r1.numerator * r2.numerator;
temp.denominator = r1.denominator * r2.denominator;
return temp;
}
这是一种最容易想到的实现方式,在栈上创建对象temp,分子、分母分别相乘后,将之返回。
但仔细再想想,调用函数真的能收到这个temp吗?它是在operator*函数调用时的栈上产生的,当这个函数调用结束后,栈上的temp也会被pop掉,换言之,temp的生命周期仅存在于operator*函数内,离开这个函数,返回的引用将指向一个已经不在的对象!
对此,VS编译器已经给出了warning,如下:
“warning C4172: returning address of local variable or temporary”
千万不能忽略它。
那既然栈上创建对象不行,还可以在堆上创建嘛(new出来的都是在堆上创建的),于是我们有:
friend const Rational& operator* (const Rational& r1, const Rational& r2)
{
Rational *temp = new Rational();
temp->numerator = r1.numerator * r2.numerator;
temp->denominator = r1.denominator * r2.denominator;
return *temp;
}
这下VS编译器没有warning了,之前在资源管理那部分说过,new和delete要配对,这里只有new,那delete了?delete肯定不能在这个函数里面做,只能放在外面,这样new和delete实际位于两个不同的模块中了,程序员很容易忘记回收,而且给维护也带来困难,所以这绝对不是一种好的解决方案。书上举了一个例子,比如:
Rational w, x, y, z;
w = x * y * z;
连乘操作会有两次new的过程,我们很难取得operator*返回的reference背后隐藏的那个指针。
当然,如果把new换成auto_ptr或者是shared_ptr,这种资源泄露的问题就可以避免。
栈上创建的临时对象不能传入主调模块,堆上创建的对象就要考虑资源管理的难题,还有其他解决方法吗?
我们还有static对象可以用,static对象位于全局静态区,它的生命周期与这个程序的生命周期是相同的,所以不用担心它会像栈对象那样很快消失掉,也不用担心它会像堆对象那样有资源泄露的危险。可以像这样写:
friend const Rational& operator* (const Rational& r1, const Rational& r2)
{
static Rational temp;
temp.numerator = r1.numerator * r2.numerator;
temp.denominator = r1.denominator * r2.denominator;
return temp;
}
这样写编译器同样不会报错,但考虑一下这样的式子:
Rational r1, r2, r3;
if(r1 * r2 == r1 * r3){…}
if条件恒为真, 不管它们的值是什么。 因为static 对象被所有类对象所共享。
既然一个static对象不行,那弄一个static数组?把r1*r2的值放在static数组的一个元素里,而把r1*r3放在static数组的另一个元素里?仔细想想就知道这个想法是多么的天马行空。
一个必须返回新对象的正确写法是去掉引用,就这么简单!
inline const Rational operator*(const Rational& lhs, const Rational& rhs)
{
return Rational(lhs.n * rhs.n, lhs.d * rhs.d);
}
该让编译器复制的时候就要放手去复制,就像花钱买东西一样,必须花的终究是要花的。
总结:
绝不要返回一个指向局部对象的指针或者引用,也不要返回指向分配在堆上的对象,也不能返回指向局部 static 对象的 引用 或 指针。
条款22. 将变量声明为private
为什么要将成员变量声明为private而不是public呢:
- 能够提供语法一致性,如果变量都是private,那么用户只能通过函数来获得这个变量,而不用考虑在类外访问时要不要加“.”或者“->” 访问时要不要加圆括号。
- 使用函数,可以对变量进行精确地控制:有的变量不许访问,有的只读,有的可以读写,甚至是可以“只写”。而且在函数中,可以处理用户输入的不合理的参数。
- 封装性。封装性意味着,当你有新的想法需要修改时,只需要改变函数内部的实现细节。只要函数的接口没有改变,那么所有使用这个函数的代码就不需要做改变。而假如你使用的数据,那么整个代码的修改量就会大很多。简而言之,public意味着不封装,所有public下的东西都是别人用的,你不能轻易修改它们。
- 而对于protected,前两点依然适用。而对于第三点,可以从另一个角度来说明:如果public变量被取消那么所有使用它的代码都会被取消。对于protected,则破坏的是他的派生类。这都是无法衡量的。而如果private变量被取消,我们只要修改本类中调用这个变量的函数就行了。
为什么把成员设置为 proteted不行呢?
- 如果不存在继承关系,protected的作用与private等同,除了本类之外,其他类或者类外都不能访问,但如果存在继承关系时,protected标识的成员变量,在它的子类中仍可以直接访问,所以封装性就会受到冲击。这时如果更改父类的功能,相应子类涉及到的代码也要修改,这就麻烦了。而如果使用private标识,则根据规则,所有private标识的变量或者函数根本不被继承的,这样就可以保护父类的封装性了,仍可以在子类中通过父类的成员函数进行访问。
最后总结:
- 将数据成员声明为私有。它在语法上为客户端提供了对数据的统一访问,提供了细粒度的访问控制,允许执行不变量,并提供了类作者实现的灵活性。
- protected的封装性并不比public强、
*条款23:宁以non-member、non-frined替换member函数
类中可能存在一系列函数用来处理一系列操作,而这一系列函数可以放在类中某一成员函数中一起执行,但是也可以放在一个non-member non-frined 函数中一起执行。那么从封装性上来讲哪一个好呢?
答案是 non-member、non-frined函数,这是因为成员函数还可以访问类中的私有成员,但是非non-member、non-frined 函数连私有成员都访问不了,所以non-member、non-frined函数的封装性更好。
现在例举书上的例子:
考虑一个class用来清除浏览器的一些记录,这个class中有清除告诉缓存区的函数,有清除访问过URLs的函数,还有清除cookies的函数:
class WebBrower
{
public:
void ClearCach();
void ClearHistory();
void RemoveCookies();
};
现在呢,你想同时执行这些操作,现在需要将这三个函数打包成一个函数,这个函数执行所有的清理工作,那是将这个清理函数放在类内呢,还是把他放在类外呢?
如果放在类内,那就像这样:
class WebBrower
{
…
void ClearEverything()
{
ClearCach();
ClearHistory();
RemoveCookies();
}
…
};
如果放在类外,那就像这样:
void ClearWebBrowser(WebBrower& w)
{
w.ClearCach();
w.ClearHistory();
w.RemoveCookies();
}
上面两种做法,哪种比较好呢?答案是 non-member、non-frined函数比较好。
为了区分开,我们把在类内的总清除函数称之为ClearEverything,而把类外的总清除函数称之为ClearWebBrower。ClearEverything对封装性的冲击更大,因为它位于类内,这意味着它除了访问这三个公有函数外,还可以访问到类内的私有成员,是的,你也许会说现在这个函数里面只有三句话,但随着功能的扩充,随着程序员的更替,这个函数的内容很可能会逐渐“丰富”起来。而越“丰富”说明封装性就会越差,一旦功能发生变更,改动的地方就会很大。
再回过头来看看类外的实现,在ClearWebBrowser()里面,是通过传入WebBrower的形参对象来实现对类内公有函数的访问的,在这个函数里面是绝对不会访问到私有成员变量。因此,ClearWebBrowser的封装性优于类内的ClearEverything。
但这里有个地方需要注意,ClearWebBrower要是类的 non-member、non-frined 函数,上面的叙述才有意义,因为类的友元函数与类内成员函数对封装性的冲击程度是相当的。
看到这里,你也许会争辩,把这个总清除的功能函数放在类外,就会割离与类内的关联,逻辑上看,这个函数就是类内函数的组合,放在类外会降低类的内聚性。
为此,书上提供了命名空间的解决方案,事实上,这个方案也是C++标准程序库的组织方式,好好学习这种用法很有必要!像这样:
namespace WebBrowserStuff
{
class WebBrowser();
void ClearWebBrowser(WebBrowser& w);
}
总结:宁可拿 non-member non-friend 函数替换member函数,这样做可以提高封装性、封装灵活性和功能可扩展性。
条款24: 若所有参数皆需类型转换, 请为此采用 non-member 函数
我们在前面的条款提到过, 应该避免某一个类有隐式构造函数。 但是呢,也有例外的时候 ,比如说我们想建立数值类型的class。 假设我们用一个class 表示有理数类, 该类呢允许 整数 到有理数的隐式转换。 举书上的例子:
class Rational
{
private:
int numerator;
int denominator;
public:
Rational(int n = 2, int d = 1) : numerator(n), denominator(d) {}
int getNumerator() const { return numerator; }
int getDenominator() const { return denominator; }
const Rational operator* (const Rational& r)
{
return Rational(numerator * r.numerator, denominator * r.denominator);
}
};
注意这个构造函数前面没有加explicit关键字,这意味着允许隐式转换。getNumerator() 和 getDenominator()是用来获取私有成员的值的.
当我们将 Rational 对象 跟一个整数相乘时,第二个调用发生错误,这为什么会发生错误? 乘法不是支持交换律吗?
int main()
{
Rational oneEighth(1, 8);
Rational oneHalf(1, 2);
Rational result = oneHalf * 2; // 正确
result = 2 * oneHalf; //错误
system("pause");
return 0;
}
当我们 指向 “ Rational result = oneHalf * 2 ” 该代码的时候,编译器首先在类中查找,不知道你们还记得吗, 一个类中的每一个成员函数都有一个隐含的 this 指针, 该指针指向调用该函数的对象。所以说当执行 “ operator* ()” 函数时, “ this 指针 ” 指向 oneHalf 对象, 实参2 传递 “ operator* ()” 函数(此时会发生隐式转换)。
那么当我们执行到 “ result = 2 * oneHalf ” 代码的时候, “ 2 ” 只是一个整型数,并非一个类, 所以它并没有 “ this ” 指针。 而且 “ this 指针” 指向本类对象是const的,即 “ *this 指针” 指向的那个对象不能被改变。 所以说 “ 2 ” 不能被隐式转换。
那么编译器可能会在类外查找 non_member 函数 ( non_member 函数 指的是 普通函数 和 friend 函数), 但是在本例中并没有实现。 所以说查找失败。
那么如何 执行 “ result = 2 * oneHalf ” 该代码时正确呢? 书上建议的是把 “ operator* ()” 函数 实现为 non_member 函数 ( non_member 函数 指的是 普通函数 和 friend 函数, 书上建议使用 普通的 非成员函数,原因后面讲),从而允许编译器对所有形参执行隐式类型转换:
class Rational
{
private:
int numerator;
int denominator;
public:
Rational(int n = 2, int d = 1) : numerator(n), denominator(d) {}
int getNumerator() const { return numerator; }
int getDenominator() const { return denominator; }
};
// 实现为 普通的函数
const Rational operator* (const Rational& r1, const Rational& r2)
{
return Rational(r1.getNumerator() * r2.getNumerator(), r1.getDenominator() * r2.getDenominator());
}
int main()
{
Rational oneEighth(10, 20);
Rational oneHalf(10, 30);
Rational result = oneHalf * 2; // 正确
result = 2 * oneHalf; //正确
system("pause");
return 0;
}
这样写正确了。
那么为什么 “ operator* ()” 函数不实现为 friend 函数呢?
- 因为 friend 函数破坏了封装。这就引出了一个重要的结论: 成员函数的对立面是非成员函数,而不是friend函数。 太多的c++程序员认为,如果一个函数与一个类相关,不应该是一个成员函数(例如,由于需要对所有参数进行类型转换),那么它应该是一个 friend 函数。这个例子证明了这种推理是有缺陷的。无论何时,能避免使用friend 函数,就应该避免之。但事实仍然是,一个功能如果不应该是成员 函数,并不意味着它应该是一个friend 函数。
总结 : 如果需要对成员函数的所有参数进行类型转换(包括this指针指向的那个对象),则该函数必须是非成员函数。
条款26:尽可能延后变量定义式的出现时间
当你在某一个作用域中定义一个类类型变量时,当程序进入这个程序块时到达这个变量的定义式才会被创建,那么此时你需要付出构造成本; 当这个变量离开其作用域时,还要付出销毁成本。 有时可能这个变量最终并未使用, 可能仍然需要耗费构造、析构成本, 所以说应该尽可能的避免这样的情况。
比如下面这个函数,把密码转换为“ * “:
string encrytPassword(const string password)
{
using namespace std;
string::size_type miniLength = 5;
string encrypted;
if (password.length() < miniLength)
{
throw logic_error(" password is too short");
}
for (string::size_type i = 0; i != password.size(); ++i)
{
encrypted.append("*");
}
return encrypted ;
}
我们规定了密码长度不能小于5,如果小于5则会抛出异常。仔细研究就会发现,假如密码长度真的小于5,就不会执行 if 后面的部分,所以string encrypted 就不会被使用了。那么前面定义的工作也就白做了。况且这个定义还调用了string的默认构造函数,如果说抛出了异常, encrypted 不一定会被销毁。所以,我们应该延后这个变量出现的时间,把它放在for 语句的前面 。
string encrytPassword(const string password)
{
using namespace std;
string::size_type miniLength = 4;
if (password.length() < miniLength)
{
throw logic_error(" password is too short");
}
string encryted; // 定义在这里
for (string::size_type i = 0; i != password.size(); ++i)
{
encryted.append("*");
}
return encryted;
}
其实,这样做也不是最好的,因为 encrypted 是在没有任何初始化参数的情况下定义的。这意味着会调用它的默认构造函数。 在条款4 中说过,“ 通过默认构造函数构造出的对象然后进行赋值 ” 比 “ 直接在构造对象时指定初值 ” 效率差。 那么这个原则也适用于此。 所以说最好的作法是直接用理想的初值初始化这个变量,从而跳过毫无意义的默认构造函数:
string encrytPassword(const string password)
{
using namespace std;
string::size_type miniLength = 4;
if (password.length() < miniLength)
{
throw logic_error(" password is too short");
}
string encryted = “ huangchengtao ”; // 定义并初始化
for (string::size_type i = 0; i != password.size(); ++i)
{
encryted.append("*");
}
return encryted;
}
所以说我们应该将变量的定义推迟到必须使用该变量之前,还应该尝试将定义推迟到该变量有初始化参数之后。 通过这样做,可以避免构造和销毁不需要的对象,并避免不必要的默认构造。此外 , 而且,一个初值明确的变量能大大提高程序的可读性。
本书还提出一种情况:是把变量定义放在for循环中,还是放在for循环之外?
做法A:
for (int i = 0; i < 5; ++i)
{
Test tt;
if (t.getVal() == i)
{
// Statemets
}
}
做法B:
Test tt;
for (int i = 0; i < 5; ++i)
{
if (t1.getVal() == i)
{
// Statemets
}
}
做法A: 一个构造函数 + 1 个析构函数+ n个赋值操作
做法B: n 个构造函数 + n 个析构函数
二者区别在于:首先,一般而言放在循环体外代码效率更高,因为这样你只需要做一次构造与析构。而在循环体内你得做很多次。其次,在循环体内定义的变量的作用域比较小,易于代码的维护。这取决于你代码所关心的重点:是效率还是安全?
总结: 在变量使用前定义 ,并且初始化。它提高了程序清晰度并提高了程序效率。
条款27:尽量少使用转型动作
本条款开始部分就讲了在C++ 语言中早期版本中的两种显式转换,以及 static_cast、reinterpret_cast、static_cast、dynamic_cast 这些新式转换。 如果你想把这几种转换了解清楚, 可以看C++ primer 这本书, 它们都在C++ primer 第144页都有详细的介绍, 只有 dynamic_cast 在 730页。
dynamic_cast 效率很低,能不用就不用吧。通常,只有当你想在一个认定为派生类对象的身上执行派生类操作,而你却只有一个基类指针或者引用时,才需要dynamic_cast:
class Base
{
public:
Base(int i = 0):bVal(i){cout<<"基类构造函数"<<endl;}
/* virtual void say()
{
cout<<"基类函数"<<endl;
}*/
virtual void fun(){}
private:
int bVal;
};
class Drived:public Base
{
public:
Drived(int i = 10, int j = 5):Base(i),dVal(j){cout<<"派生类构造函数"<<endl;}
void say()
{
cout<<"派生函数"<<endl;
}
private:
int dVal;
};
int main()
{
Base *pd = new Drived;
Drived* pd1 = dynamic_cast<Drived*>(pd); // 把一个基类指针转换为派生类指针
pd1->say();
return 0;
}
注意,这个转化这里要求基类必须有虚函数。
感觉没什么可写的,最后总结一下:
- 如果可以,尽量避免转型,特别是在注重效率的代码中避免 使用dynamic_cast。如果某个代码需要转型动作,看看有没有其它方式来替代转型动作。
- 当需要转型时,尝试将其隐藏在函数内部。然后,客户可以调用该函数,而不是将强制转换放入自己的代码中。
- 宁可使用C++风格的新式转型,少用旧风格转型,因为前者很容易辨识出来,而且它们更具体的说明各个转型的作用。
条款28:避免返回handles指向对象内部成分
举书上的例子:
//点类
class Point
{
public:
Point(int xVal, int yVal) :x(xVal), y(yVal) {}
~Point() {}
void setX(int newX) { x = newX; }
//返回X坐标,以后测试用
int getX()const { return x; }
void setY(int newY) { y = newY; }
private:
int x;
int y;
};
//矩形
struct RectData
{
RectData(const Point& p1, const Point& p2) :ulhc(p1), lrhc(p2) {}
Point ulhc;//坐上
Point lrhc;//右下
};
//矩形类
class Rectangle
{
public:
Rectangle(RectData data) :pData(new RectData(data)) {}
Point& upperLeft()const { return pData->ulhc; }
Point& lowerRight()const { return pData->lrhc; }
private:
std::shared_ptr<RectData> pData;
};
这两个函数都返回对私有内部数据的引用——调用者可以使用这些引用来修改该内部数据,虽然我们声明 upperLeft() 和 lowerRight() 都是const函数,,例如:
int main()
{
Point coord1(0, 0);
Point coord2(100, 100);
RectData data(coord1, coord1);
const Rectangle rec(data);
rec.upperLeft().setX(50); // 这里可以修改rec 对象的数据成员的值
system("pause");
return 0;
}
之所以可以 修改rec 对象的数据成员的值是因为 upperLeft() 和 lowerRight() 这两个函数返回普通的引用,如果它们返回 指针或者迭代器结果都是一样的,原因也相同。 引用、指针和迭代器都是handles ( 号码牌,用来获取某个对象)。 如果返回的 handles 刚好指向的是 “ 对象的数据成员的值 ”,那么 该 “ 对象的数据成员的值 ” 的值就有可能被修改。 甚至出现这种情况 —— “虽然调用const成员函数却造成对象状态被更改” , 上述的例子就是。
比如这里有一个学生类:
class Student
{
private:
int ID;
string name;
public:
string& getName()
{
return name;
}
};
这是一个学生的类,类里面有两个成员变量,一个是学生ID,用整数表示,另一个是姓名,用string表示。有一个公有的方 getName(),获得学生的名字. 根据条款20所说的,使用引用可以防止资源不必要地拷贝,那么在返回值这边就用string&。但现在问题来了,这个函数只是想返回学生的姓名,并不想用户对之进行修改,但返回引用却提供了这样的一个接口,如:
int main()
{
Student s;
s.GetName() = "Jerry"; // 这里就把 Student 对象数据成员的值给修改了
cout << s.GetName() << endl;
system("pause");
return 0;
}
这个例子刚好就说明了 “ 如果返回的 handles 刚好指向的是 “ 对象的数据成员的值 ”,那么 该 “ 对象的数据成员的值 ” 的值就有可能被修改。”
本书中还提出,也不要返回 protected 和 private 区域的 成员函数的 handles。
那回到原来的例子, 如何禁止返回的 handles 不被修改呢?只要对它们的返回类型加上const就可以了:
const Point& upperLeft()const{return pData->ulhc;}
const Point& lowerRight()const{return pData->lrhc;}
有了这样的改变, 客户只可以读取 它们返回的引用,而不能修改它们。
即使这样,upperLeft() 和 lowerRight() 函数还是返回了代表对象内部的handles ,它也会产生其他方面的问题,比如:悬空句柄——这个句柄所指的东西并不存在, 最常见的原因就是该函数的返回值。举一个例子:
//一个GUI对象
class GUIObject
{
public:
GUIObject(Rectangle r) {}
//返回一个指定大小的矩形框
const Rectangle getRec()const
{
Point coord1(50, 50);
Point coord2(200, 200);
RectData data(coord1, coord2);
const Rectangle rec(data);
return rec;
}
};
//返回obj的外框
const Rectangle boundingBox(const GUIObject& obj)
{
return obj.getRec();
}
int main()
{
Point coord1(10, 10);
Point coord2(100, 100);
RectData data(coord1, coord2);
const Rectangle rec(data);
GUIObject obj(rec);
//一个GUI对象指针
GUIObject* pgo = &obj;
//获取它的左上角点
const Point* pUpperLeft = &(boundingBox(*pgo).upperLeft());
cout << pUpperLeft->getX();
system("pause");
return 0;
}
boundingBox返回一个新的Rectangle对象。然后调用upperLeft函数返回它的左上角。但是,当这条语句结束以后,boundingBox的返回值就会被销毁了,导致它的Points被析构,所以pUpperLeft 就成了一个悬空句柄(不代表任何实际存在的对象)。由此可见,返回一个指向对象内部成分的句柄,是一项危险的事情,因为对象可能在任何时候被析构,此后,这个句柄就成了悬空句柄了。
这就是为什么函数如果 “返回一个handle代表对象内部成分” 总是危险的原因,不管这个 handles 返回的是 指针或者引用或者迭代器, 也不论这个handle 是不是const的, 也不论 那个返回handles 的成员函数是否为const成员函数。 关键的是: 只要这个handles 如果被传递出去,您将 面临handles比它所引用的对象活得更久的风险。
当然了,这也不意味着绝不可以让成员函数返回handles。
好了,总结一下:
避免返回handles(包括reference、指针、迭代器)指向对象内部,遵守这个条款可增加封装性,帮助 const 成员函数的行为像个const。 这样也可以避免发生dangling handles的可能性降 最低。如果有必要必须要返回handles,在编写代码时就一定要注意对象和传出handle的生命周期。
条款30:透彻了解inlining的里里外外
将函数指定为内联函数,通常程序会在函数调用的地方直接插入函数体中代码,例如:
inline const string &shorter(const string &s1, const string &s2)
{
return s1.size() <= s2.size() ? s1 : s2;
}
int main()
{
std::string s1("huang");
std::string s2("cheng");
cout << shorter(s1,s2) << endl;
system("pause");
return 0;
}
例如上述程序我们把 shorter 函数定义为内联函数,那么将在编译过程中展开成类似于下面的形式:
cout<< ( s1.size() <= s2.size() ? s1 : s2 ) <<endl;
从而消除了 shorter 函数在运行时的开销。
上面这是一种以空间换时间的做法。把每一次调用都用本体替换,即可以使函数的执行效率提高,也可以节省函数调用的成本,因为函数调用需要将之前的参数以栈的形式保存起来,调用结束后又要从栈中弹出那些参数。但是使用过多的内联函数可能会使代码膨胀。
所以一般来说, 内联函数一般用于 优化规模较小、流程直接、频繁调用的函数。很多编译器都不支持内联递归函数, 而且一个75行的函数也不可能在调用点内敛地展开。
但要注意的是: 使用inline 关键字只是向编译器发出的一个请求, 编译器可以选择忽略这个请求。 它不是强制的命令。
如果说想内联一个普通的函数直接在其返回类型前面加上关键字inline 关键字即可,那么任何在类中定义的函数(包括friend 函数)自动隐式地成为内联函数,也可以使用inline 关键字放在类外定义函数前面使之成为内联函数,但是必须使函数体和声明结合在一起, 否则, 编译器只将这个函数视为普通成员函数。 例如:
inline int ff() // 把一个普通的函数定义为内联函数
{
}
class Rect
{
public:
int width();
int height() // 在类中定义的函数都隐式成为内联函数,除了虚函数,因为内联(代码展开)发生在编译期,而虚函数的行为是在运行期决定的
{
}
};
inline int Rect::width() // 在定义成员函数时,在其返回类型上添加 inline 关键字,使之成为内联函数
{
}
还可以这样定义内联函数:
class Rect
{
inline int width();
};
int Rect::width()
{
}
和其他函数不一样, 内联函数可以在程序中多次定义。毕竞, 编译器要想在编译期间展开函数仅有函数声明是不够的, 为了用被调用函数的主体替换函数调用,编译器必须知道函数的定义。不过, 对于某个给定的内联函数来说, 它的多个定义必须完全一致。基于这个原因, 内联函数通常定义在头文件中。—— 这条是引用了C++ primer 中的, 内联函数在C++ primer 第五版 中 第 214 页有简单的介绍。
书上还提出 模板( 包括函数模板 和 类模板成员函数)通常都被定义在头文件中,因为编译器需要知道模板的定义,以便在使用它时实例化它。( 同样,这不是普遍的。一些构建环境在链接期间执行模板实例化。然而,编译时实例化更为常见 )。但是呢,这也不是绝对的。
模板实例化与内联无关。如果你正在编写一个模板,并且你认为从此模板实例化的所有函数都应该内联,那么就内联地声明该模板;树中的 std::max 代码就是这样做的。但是如果你编写的template 不需要要求实例化时的每一个函数都是内联的, 就应该避免将这个template 声明为inline (不管是隐式的还是显式的)的。内联是有成本的,您不希望在没有预先考虑的情况下产生这些成本。我们已经提到了内联如何会导致代码膨胀。 说明: 关于这点内容在C++ primer 第五版 第582 页 有介绍。
刚才我们说了 “ inline 关键字只是向编译器发出的一个请求, 编译器可以选择忽略这个请求。 它不是强制的命令。 ”, 那么它啥时候会忽略呢:
- 一般在太过复杂的函数(例如 循环和递归)。
虚函数(virtual)可以是内联函数(inline)吗?
- 虚函数可以是内联函数,内联是可以修饰虚函数的,但是当虚函数表现多态性的时候不能内联。
- 内联是在编译器建议编译器内联,而虚函数的多态性在运行期,编译器无法知道运行期调用哪个代码,因此虚函数表现为多态性时(运行期)不可以内联。
inline virtual
唯一可以内联的时候是:编译器知道所调用的对象是哪个类(如Base::who()
),这只有在编译器具有实际对象而不是对象的指针或引用时才会发生。虚函数内联的使用程序示例:
#include <iostream> using namespace std; class Base { public: inline virtual void who() { cout << "I am Base\n"; } virtual ~Base() {} }; class Derived : public Base { public: inline void who() // 不写inline时隐式内联 { cout << "I am Derived\n"; } }; int main() { // 此处的虚函数 who(),是通过类(Base)的具体对象(b)来调用的,编译期间就能确定了, //所以它可以是内联的,但最终是否内联取决于编译器。 Base b; b.who(); // 此处的虚函数是通过指针调用的,呈现多态性,需要在运行时期间才能确定,所以不能为内联。 Base *ptr = new Derived(); ptr->who(); // 因为Base有虚析构函数(virtual ~Base() {}),所以 delete 时,会先调用派生类(Derived)析构函数,再调用基类(Base)析构函数,防止内存泄漏。 delete ptr; ptr = nullptr; system("pause"); return 0; }
这个内容是参考了这里的,需要详情,请点击此链接:http://www.cs.technion.ac.il/users/yechiel/c++-faq/inline-virtuals.html
有时编译器会为内联函数生成一个函数体,即使他们非常愿意内联该函数。比如:使用某个内联函数的地址(指向函数的指针, 意思就是这个指针是指向函数的, 即指针存储的是函数的首地址。):
inline string lengthCompare(const string &s1, const string &s2)
{
return s1.size() < s2.size() ? s2 : s1;
}
int main()
{
string(*pf)(const string &s3, const string &s4) = lengthCompare; //定义函数指针pf
string s3 = (*pf)(s1, s2); //调用lengthCompare函数 , //string s4 = pf(s1,s2); 一个等价的调用
string s5=lengthCompare(s1,s2);
system("pause");
return 0;
}
那么当 调用 该 “ string s3 = (*pf)(s1, s2); ” 代码时这个调用代码可能不会内联,因为它是通过一个函数指针调用的。
虽然有时候构造函数和析构函数虽然“看”似简单,但编译器会在背后做很多事情,比如一个空的构造函数里面会由编译器对该类的成员作默认初始化。如果将之inline,那么当创建该类对象时, 构造函数会在创建该类对象时内联地展开; 假如说这个类很多个数据成员需要默认初始化, 那么在 “ 创建该类对象时内联地展开 ”, 无疑会使代码膨胀,执行效率变低。 析构函数也类似。 所以说我们不要将构造函数和析构函数使之为inline 函数, 即使它们什么也不做。
要慎用inline,是因为一旦编译器真的将之inline了,那么这个inline函数一旦被修改,整个程序都需要重新编译,而如果这个函数不是inline的,那么只要重新连接就好。另外,一些调试器对inline函数的支持也是有限的。
作者认为,“一开始先不要将任何函数声明为inline”,经测试,确实发现某个函数的inline要比不对之inline的性能提升很多,才对之inline。在大多数情况下,inline并不是程序的瓶颈,真正的精力应该放在改善一些算法的修改上,以及反复调用的代码研究上,它们往往才是耗时的瓶颈所在。
下面说下编译器对 inline 函数的处理步骤:
- 将 inline 函数体复制到 inline 函数调用点处;
- 为所用 inline 函数中的局部变量分配内存空间;
- 将 inline 函数的的输入参数和返回值映射到调用方法的局部变量空间中;
- 如果 inline 函数有多个返回点,将其转变为 inline 函数代码块末尾的分支(使用 GOTO)。
inline 的优缺点:
优点
- 内联函数同宏函数一样将在被调用处进行代码展开,省去了参数压栈、栈帧开辟与回收,结果返回等,从而提高程序运行速度。
- 内联函数相比宏函数来说,在代码展开时,会做安全检查或自动类型转换(同普通函数),而宏定义则不会。
- 在类中声明同时定义的成员函数,自动转化为内联函数,因此内联函数可以访问类的成员变量,宏定义则不能。
- 内联函数在运行时可调试,而宏定义不可以。
缺点
- 代码膨胀。内联是以代码膨胀(复制)为代价,消除函数调用带来的开销。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。
- inline 函数无法随着函数库升级而升级。inline函数的改变需要重新编译,不像 non-inline 可以直接链接。
- 是否内联,程序员不可控。内联函数只是对编译器的建议,是否对函数内联,决定权在于编译器。