前言
读《effective c++》内容的部分总结
条款02:尽量以const,enum,inline替换 #define
原因如下:
-
宏变量可能没有进入符合表(symbol table)内(映射关系),导致运行此宏常量时获得一个编译错误信息
例如:#define ASN 1.653。当使用ASN时会报错。
解决方法是以一个常量替换上述的宏(#define):
const double ASN=1.653;
-
声明class专属常量(将常量的作用域限制于class内),不能用#define,因为#define并不重视作用域,不提供任何封装特性。
解决方法如下:静态常量
class Solution {
private:
static const double fu; //static class 声明常量位于头文件内
};
const double Solution::fu=1.35; //static class 常量定义位于实现文件内
- 编译器不允许“static整数型class常量”完成“in class”初值设定(即不允许在类内初始化静态变量)
如下面的:
class Solution {
private:
static const int num=5; //错误的,旧的编译器不允许在类内初始化静态变量
int socre[num]; //会报错,无法在编译期间知道数组的大小
};
解决方法如下:用枚举来代替
class Solution {
private:
enum {num=5}; //令num成为5的一个记号名称
int socre[num];
};
- 用#define实现简单函数功能,不推荐
解决方法:用内联函数代替
c++inline关键字详解
条款03:尽可能使用const
- const类型定义
char *p=greeting; //non-const pointer,non-const data
const char *p=greeting; //non-const pointer,const data
char* const p=greeting; //const pointer,non-const data
const char* const p=greeting; //const pointer,const data
如果关键字const出现在星号左边,表示被指物是常量;如果出现在星号右边,表示指针自身是常量
-
函数的返回值和参数尽量用const,避免一些不必要的错误
-
const实施于成员函数
目的:(1)使class接口容易理解,容易看出哪个函数可以改动对象内容而哪个函数不行
(2)便于操作const对象
-
两个成员函数如果只是常量性不同,可以被重载
class TextBlock {
public:
const char& operator[](std::size_t position) const
{ return text[position]; } //const对象
char& operator[](std::size_t position)
{ return text[position]; } //non-const对象
private:
std::string text;
};
TextBlock tb("hello");
std::cout<<tb[0]; //调用non-const
const TextBlock ctb("world");
std::cout<<ctb[0]; //调用const
- bitwise错误
const成员函数内更改成员变量(static除外)报错
如下代码:
class CTextBlock {
public:
std::size_t length() const;
private:
char *pText;
std::size_t textLength;
bool lengthIsValid;
};
std::size_t CTextBlock::length() const
{
if(!lengthIsValid) {
textLength=std::strlen(pText);
lengthIsValid=true; //错误,在const成员函数内不能赋值给textLength和lengthIsValid
}
return textLength;
}
上述代码报错,const成员函数内改变了变量的值。
解决方法: 利用c++的一个与const相关的摆动场:mutable(可变的)。mutable释放掉non-static成员变量的bitwise-constness约束
class CTextBlock {
public:
std::size_t length() const;
private:
char* pText;
mutable std::size_t textLength;
mutable bool lengthIsValid;
};
std::size_t CTextBlock::length() const
{
if(!lengthIsValid)
{
textLength=std::strlen(pText);
lengthIsValid=true;
}
return textLength;
}
关于mutable的学习参考这篇文章:【C++】Mutable关键字
-
“const成员函数调用non-const成员函数”是一种错误的行为,因为对象有可能因此被改动
-
令non-const版本调用const版本可避免代码重复
6-7:非const函数可以调用const函数,const函数不可以调用非const函数
条款04:确定对象被使用前已先被初始化
- 确保每一个构造函数都将对象的每一个成员初始化
c++规定,对象的成员变量的初始化动作发生在进入构造函数本体之前
如下代码:
class Solution {
private:
int ax,bx;
public:
Solution(int a,int b);
};
Solution::Solution(int a,int b) {
ax=a;
bx=b;
}
在Solution构造函数内,ax和bx都不是被初始化,而是被赋值。
初始化的发生时间更早,发生于这些成员的dafault构造函数被自动调用之时(比进入Solution构造函数本体的时间更早)
最佳的写法是用成员初始化列表
Solution::Solution(int a,int b):ax(a),bx(b)
{ }
成员初始化列表避免了调用default设初值,因此效率更高
c++有十分固定的“成员初始化次序”。次序总是相同的:base classes更早于其derived classes被初始化。并且class的成员变量总是以其声明次序被初始化
- “不同编译单元内定义之non-local static对象”的初始化次序
对于static对象,它们的析构函数会在main()结束时被自动调用
条款05:了解c++默默编写并调用哪些函数
- 空类的默认生成哪些
如果写下:
class Empty { };
编辑器就会默认写下如下:
class Empty {
public:
Empty() {...} //default构造函数
Empty(const Empty& rhs) {...} //copy构造函数
~Empty() {...} //析构函数
Empty& operator=(const Empty& rhs) {...} //操作符函数
};
当这些函数被需要(被调用),它们才会被编译器创建出来 (只有被调用时,才会创建)
条款06:若不想使用编译器自动生成的函数,就该明确拒绝
具体做法:所有编译器产出的函数都是public。为阻止这些函数被创建出来,可以自行声明它们为private。这样阻止了编译器暗自创建其专属版本,并且类对象也不能调用它们。(只有声明,不予实现)
条款07:为多态基类声明virtual析构函数
此原因的详细讲解,参考博客:c++多态与虚函数从根上解决中的为什么要把析构函数声明成虚函数;
记住:任何class只要带有virtual函数都几乎确定应该也有一个virtual析构函数
条款08:别让异常逃离析构函数
在析构函数中添加了一些处理函数,可能会出现异常
- 析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常(try),然后吞下它们不传播(catch)或结束程序(abort)
条款09:绝不在构造函数和析构过程中调用virtual函数
基类的构造函数和析构函数中不要有virtual函数。
例如,如下代码:
class Transaction {
public:
Transaction();
virtual void log() const =0;
};
Transaction::Transaction()
{
log();
}
class BuyTransaction:public Transaction {
public:
virtual void log() const;
};
BuyTansaction b;
上述代码,创建对象b时,首先会调用Transaction构造函数,但Transaction构造函数的最后一行调用log函数。这时候被调用的log函数是Transaction内的版本,不是BuyTransaction内的版本。(base class构造期间virtual函数绝不会下降到derived classes阶层)
记住:在derived class 对象的base class构造期间,对象的类型是base class而不是derived class。不只virtual函数会被编译器解析至base class,若使用运行期类型信息,也会把对象视为base class类型。(析构函数也一样)
条款10:令operator=返回一个reference to *this
为了实现“连锁赋值”,赋值操作符必须返回一个reference指向操作符的左侧实参
x=y=z=15;
class Widget {
public:
Widget& operator=(const Widget& rhs) {
...
return* this;
}
};
条款11:在operator=中处理“自我赋值”
w=w;
a[i]=a[j]; //潜在的自我赋值i=j
条款12:复制对象时勿忘其每一个成分
-
为derived class自己撰写copying函数时,要记得也要copy其base class的成员成分,让derived class的copy函数调用相应base class的copying函数 。即编写一个copying函数时,应确保(1)复制所有local成员变量(2)调用所有的base classes内的适当的copying函数
-
拷贝赋值函数(copy assignment 操作符)和拷贝构造函数(copy构造函数)不能互相调用。
两种区别请看这篇博客:copy构造函数与copy assignment操作符区别
如果发现两个函数之间有相近的代码,消除重复代码的做法是,建立一个新的成员函数给两者调用,这样的函数往往是private而且常被命名为init。这样便可以消除两者之间的代码重复了
条款13:以对象管理资源
为防止资源泄露,分配资源时,建议使用智能指针,在构造函数中获得资源并在析构函数中释放资源。
避免自己忘了delete对象
条款14:在资源管理类中小心copy行为
动态分配的资源进行复制时:引用计数,深拷贝
条款15:在资源管理类中提供对原始资源的访问
显示转换和隐式转换
条款16:成对使用new和delete时要采取相同形式
new -------------> delete
new [ ] ----------------> delete [ ]
要彼此搭配。
内存有一个(或更多)构造函数被调用,针对此内存就会有一个(或更多)析构函数被 调用
(注意底层实现)
条款17:以独立语句将newed对象置入智能指针
本条款要学习的知识核心是注意编译器在同一语句中,调用次序具有不确定性,不同语句中,调用次序确定。
上面的话什么意思?
请看下面代码:
int priority();
int processWidget(shared_ptr<Widget> pw, int priority);
processWidget(shared_prt<Widget> pw(new Widget), priority());
以上代码运行三个行为,各自是
1、运行priority()函数
2、运行new Widget
3、运行shared_ptr构造函数
大家知道这三个行为顺序吗?
我想没人敢非常自信的说顺序是什么,由于编译器在运行时,对以上三个行为的运行次序是不确定的。唯一确定的次序就是2行为在3行为之后。
假设,运行次序是2、1、3.那么当函数priority()调用出现异常。new Widget返回的指针还没来得及放入shared_ptr中。这样会造成内存泄露。
所以,我们在编程的时候,最好将紧密行为单独编写为单一语句。
例如以下:
shared_prt<Widget> pw(new Widget);
processWidget(pw,priority());
条款18:让接口容易被正确使用,不容易被误用