文章目录
Effective C++ 55 个具体做法
1 视C++为一个语言联邦
1.1 C
C++还是以C为基础,但是C语言没有模板,没有异常,没有重载
1.2 Object-oriented C++
这部分就是C++面向对象:classes(包含构造函数和析构函数)、封装、继承、多态、虚函数(动态绑定)等等,这一部分是面向对象设计的
1.3 Template C++
template模板编程
1.4 STL
STL是一个template的程序库。它对容器、迭代器、算法以及函数对象的规约有几家的紧密配合与协调
2 尽量以const, enum, inline 替换 #define
这个条款或许可以改为“宁可以编译器替换预处理器” 比较好,因为或许 #define不被视为语言的一部分。证实因为它的问题存在。
前三者是C++关键字。而#define是C++中宏定义的方法
我们先来看一段程序,比较三个关键字和宏定义的区别
#include<iostream>
using namespace std;
#define PI 3.14
const float pi = 3.14;
#define NUM 3+2
const int num = 3 + 2;
int main()
{
float a = 2 * pi;
float b = 2 * pi;
int c = 3 * NUM;
int d = 3 * num;
cout << a << b << c << d << std::endl;
}
程序运行结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Nik39IUw-1654842513371)(C:\Users\E00503\AppData\Roaming\Typora\typora-user-images\image-20220301143048564.png)]
为什么第二个会出现15和11呢?下面说一下这个原因的本质和原理。
首先产生这种现象的原因是,宏定义操作知识会对源代码进行简单的字符替换,所以你在初始化c的时候,实际上执行的是 c=3*3 + 2,这样就算到了11。
而造成这种现象的本质是宏定义操作是预处理器的操作,而const定义常量是编译器的操作。
c++的编译过程首先包括一个预编译过程,这个预编译过程包括一些基本操作就是加载你需要的头文件,还有就是预编译宏定义,截取上述程序关键预编译后的结果。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aElVWTyQ-1654842513377)(C:\Users\E00503\AppData\Roaming\Typora\typora-user-images\image-20220301144214379.png)]
可以看到define只是简单的替换。
同理enum和inline也是这个道理。
enum是C++枚举类的关键字,可以声明枚举类型。
inline是内联的关键字,声明一个函数为内敛就是告诉编译器当运行到这个函数的时候,直接把这个函数编译成机器码,而不是通过控制单元去寻找这个函数的机器码。
3 尽量使用const
const使用案例分析
-
const修饰普通类型的变量
const int a = 7; int b = a; a = 8;
a 被定义为一个常量,并且可以将a赋值给b,但是不能给a再次赋值。对一个常量赋值是违法的事情。因为a被编译器认为是一个常量,其值不允许修改。
接下来看下面的操作:
#include<iostream> int main(void) { const int a = 7; int *p = (int *)&a; // a指针给p指针 p* = 8; cout << a; // 这时候a的值不会改变 return true; }
从结果中我们可以看到,编译器认为a是一开始定义的7,所以对const a的操作就会产生上面的情况。所以千万不要轻易对const变量设法赋值,这会产生意向不到的行为。
如果不想让编译器察觉上面对const的操作,我们可以在const的前面加上volatile关键字。
volatile关键字跟const对应相反,是易变的,容易改变的意思。所以不会被编译器优化,编译器也就不会改变对a变量的操作
#include<iostream>
using namespace std;
int main(void)
{
volatile const int a = 7;
int *p = (int*)&a;
*p = 8;
cout<<a;
system("pause");
return 0;
}
这时就会输出8
- const修饰指针变量
const修饰指针变量有以下三种情况:
- const修饰指针指向的内容,则内容为不可变量
- const修饰指针,则指针为不可变量
- const修饰指针和指针指向的内容,则指针和指针指向的内容都是不可变量
对于A:
const int* p = 8; // const 修饰指针指向的内容
则指针指向的内容8 不可改变,简称左定值,因为const位于*号的左侧
对于B
int a = 8;
int* const p = &a; // const修饰指针,该指针为不可变量。就是说地址不能变
*p = 9; // 正确
int b = 7;
p = &b; // 错误
对于const指针p其指向的内存地址不能够改变,但其内容可以改变。简称右定值,因为const位于*号的左边
对于C(对于A和B的合并)
int a = 8;
const int* const p = &a;
这时,const p的指向的内容和指向的内存地址都已固定,不可改变
对于A、B、C三种情况,根据const位于*号的位置不同,左定值,右定向,const修饰不变量
-
const参数传递和函数返回值
对于const修饰函数参数可以分为三种情况
A:值传递的const修饰传递,一般这种情况不需要const修饰,因为函数会自动产生临时变量复制实参值
#include <iostream> void Cpf(const int a) { cout << a; }
B:
4 确定对象被使用前已先被初始化
关于初始化的事情,C++似乎反复无常。如果你这么写
int x;
在某些语境下面x保证被初始化(为0),但是在其他语境中却不保证。如果你这么写:
class Point{
int x, y;
};
...
Point p;
P的成员变量有时候被初始化(为0),有时候不会。如果你来自其他语言阵营而那儿并不存在 “无初值对象”,那么请小心,这点很重要。
最佳的处理办法:永远在使用对象之前先将初始化。对于无任何成员的内置类型
例如:
int x = 0; // 对int进行手工初始化
const char * test = "A C-style string"; // 对指针进行手工初始化
double d;
std::cin >> d; // 以读取input stream的方式完成初始化
至于内置类型以外的任何其他东西,初始化责任落在构造函数身上。规则比较简单:确保每一个构造函数都将对象的每一个成员初始化。
对于构造函数,重要的是别混淆了 赋值和初始化。
class PhoneNumber {...}
class ABEntry{
public:
ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phone)
private:
std::string theName;
std::string theAddress;
std::list<PhoneNumber> thePhone;
int numTimesConsulted;
};
ABEntry::ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones)
{
theName = name; // 这些都是赋值 并非初始化
theAddress = address;
thePhones = phones;
numTimesConsultes = 0;
}
C++规定,对象的成员变量初始化动作发生在进入构造函数之前。在ABEntry构造函数内,theName,theAddress和thePhones都不是被初始化,而是被赋值。初始化的发生时间更早,发生于这些成员的default构造函数之前。
ABEntry构造函数的一个较佳的写法是,使用所谓的 member initoalization list 成员初值列 替换赋值动作:
ABEntry::ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phone):theName(name),
theAddress(address),
thePhones(Phones),
numTimeConsulted(0){}
// 这些都是初始化,构造函数本体不必有任何动作
这个构造函数和上一个的最终结果相同,但通常效率较高。基于赋值的那个版本首先调用default构造函数为theName,theAddress设初值,然后立刻再对他们赋予新值。
**总是使用成员初值列**
请记住:
- 为内置型对象进行手工初始化,因为C++不保证初始化他们
- 构造函数最好使用成员初值列,而不要在构造函数本体内使用赋值操作。初值列列出的成员变量,其排列次序应该和它们在class中的声明次序一致。
- 为免除 跨编译单元之初始化次序的问题,请以local static对象替换 non-local static 对象?
5 了解C++默默编写并调用哪些函数
几乎写的每一个class都会有一个或者多个构造函数、一个析构函数、一个copy assignment(拷贝赋值运算符)操作符。
请记住:编译器可以暗自为class创建default构造函数、copy构造函数、copy assignment操作符,以及析构函数。
注意:private中有 const的参数
class Empty{};
相当于以下类:
class Empty{
public:
Empty(){} // default 构造函数
Empty(const Empty& rhs){} // 默认拷贝构造函数
~Empty(){} // 默认析构函数
Empty& operator=(const Empty& rhs) // copy assignment 操作符
};
6 若不想使用编译器自动生成的函数,就该明确拒绝
为驳回编译器自动提供的功能,可将相应的成员函数声明为private并且不予实现。使用像uncopyable这样的base class也是一种做法。
class HomeForSale {
public:
...
private:
...
HomeForSale(const HomeForSale&); //只有声明
HomeForSale& operator=(const HomeForSale&);
};
7 为多态基类声明virtual析构函数
比如一个示例:
class TimeKeeper{
public:
TimeKeeper();
~TimeKeeper();
};
class AtomicClock:public TimeKeeper{}; // 原子钟
class WaterClock:public TimeKeeper{}; // 水钟
class WristWatch:public TimeKeeper{}; // 腕表
// TimeKeeper 派生类的动态分配对象
TimeKeeper* ptr = new AtomicClock;
delete ptr;
// 为遵守factory函数的规矩,因此为了避免泄露内存和其他资源,将factory函数返回的每一个对象适当地
// delete很重要
// 多态使用的时候,如果子类中有属性开辟到堆区,那么父类指针在释放地时候无法调用到子类地析构代码
// 解决方法:将父类中的析构函数改为虚析构或者纯虚析构
class TimeKeeper{
public:
TimeKeeper();
virtual ~TimeKeeper();
};
// 任何class 只要带有virtual函数都几乎确定应该也有一个virtual析构函数
// 如果class不含virtual函数,通常表示它并不意图被用到一个base class
虚析构和纯虚析构共性;
可以解决父类指针释放子类对象,都需要具体的函数实现
虚析构和纯虚析构区别
如果是纯虚析构,该类属于抽象类,无法实例化对象
class AWOV{
public:
virtual ~AMOV() = 0; // 声明纯虚析构函数
};
// 必须为纯虚析构函数提供一份定义
AWOV::~AWOV(){} // 纯虚析构函数地定义
请记住:
- polymorphic(带多态性质的) base classes 应该声明一个virtual析构函数。如果class带有任何virtual函数,它就应该拥有一个virtual析构函数
- Classes的设计目的如果不是作为base classes使用,或不是为了具备多态性,就不该声明virtual析构函数。
8 别让异常在析构函数
瞎扯:
C++并不禁止析构函数吐出异常,但它不鼓励你这样做,这是有理由的。
class Widget()
{
public:
...
~Widget() {...} // 假设这个可能吐出一个异常
};
void doSomething()
{
std::vector<Widget> v;
...
}
// vector v 被销毁,它有责任销毁其内含的所有 Widgets。假设 v 内含十个Widgets,而在析构第一个元素期间,有个异常被抛出。其他九个Widgets还是应该被销毁(否则它们保存的任何资源都会发生泄漏),因此v应该调用期间,第二个Widget析构函数又抛出异常。
// 这很容易理解,但如果你的析构函数必须执行一个动作,而该动作可能会在失败时抛出异常,该怎么办?
class DBConnection {
public:
...
static DBConnection create(); // 这个函数返回
// DBConnection 对象
void close(); // 关闭联机;失败则抛出异常
};
// 为确保客户不忘记在DBConnection对象身上调用close(), 一个合理的想法时创建一个用来管理DBConnect资源的class,并在其析构函数中调用close。
class DBConn {
public:
...
~DBConn()
{
db.close();
}
private:
DBConnection db;
};
// 这便允许客户写出这样的代码
{
DBConn dbc (DBConnection::creat());
...
}
// 最好的处理方式
class DBConn {
public:
...
void close()
{
db.close();
closed = true;
}
~DBConn()
{
if(!closed) {
try {
db.close();
}
catch (...) {
// 制作运转记录,记下对close的调用失败
}
}
}
private:
DBConnection db;
bool closed;
};
// 把调用close的责任从DBConn析构函数手上移到DBConn客户手上。(但DBConn析构函数仞内含一个“双保险调用”)
请记住:
- 析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们或结束程序。
- 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作。
9 绝不在构造函数和析构过程中调用virtual函数
你不该在构造函数和析构函数期间调用virtual函数,因为这样的调用不会带来你预想的结果。
假设你有一个class继承体系,用来模拟交易买进买出的订单等等。这样的交易一定要经过审计。所以每当创建一个对象的时候,在审计日志中也需要创建一笔适当记录。下面的做法看起来颇为合理的做法:
class Transcation {
public:
Transcation();
virtual void logTransaction() const = 0;
...
};
Transaction::Transaction()
{
...
logTransaction();
}
class BuyTransaction:public transaction{
public:
virtual void logTransaction() const;
...
};
class SellTransaction:public Transaction {
public:
virtual void logtransaction() const;
...
};
现在,当以下这行被执行,会发生什么事情。
BuyTransaction b;
无疑会有一个BuyTransaction构造函数被调用,但首先transaction构造函数一定会更早被调用;是的子类对象内的base class成分会在子类自身成分被构造之前先构造妥当。但是Transaction的构造函数最后嗲用了virtual函数,这正是引发惊奇的地方。这时候调用的logtransaction是transaction内的版本,不是Bugtransaction内的版本,即使目前即将建立的对象类型是 Buy…。base class构造期间virtual函数绝不会下降到 子类阶层。在base class 构造期间,virtual 函数不是virtual函数。
重点: 在构造和析构期间不要调用virtual函数。因为这类调用从不下降至derived class(比起当前执行构造函数和析构函数的那层)。
复习虚函数:
可以让成员函数操作一般化,用基类的指针指向不同的派生类的对象时,
基类指针调用其虚成员函数,则会调用其真正指向对象的成员函数。
而不是基类中定义的成员函数(只要派生类改写了该成员函数)。
若不是虚函数,则不管基类指针指向的哪个派生类对象,调用时都
会调用基类中定义的那个函数。
// demo1
#include <iostream>
using namecpace std;
class B0 //基类B0声明
{
public:
void display(){cout<<"B0::display()"<<endl;} //公有成员函数
};
class B1: public B0
{
public:
void display(){cout<<"B1::display()"<<endl;}
};
class D1: public B1
{
public:
void display(){cout<<"D1::display()"<<endl;}
};
void fun(B0 *ptr)
{
ptr->display(); //"对象指针->成员名"
}
void main() //主函数
{
B0 b0; //声明B0类对象
B1 b1; //声明B1类对象
D1 d1; //声明D1类对象
B0 *p; //声明B0类指针
p=&b0; //B0类指针指向B0类对象
fun(p);
p=&b1; //B0类指针指向B1类对象
fun(p);
p=&d1; //B0类指针指向D1类对象
fun(p);
}
// 运行结果
B0::display()
B0::display()
B0::display()
// demo2
#include <iostream>
using namespace std;
class B0 //基类B0声明
{
public: //外部接口
virtual void display() //虚成员函数
{
cout<<"B0::display()"<<endl;}
};
class B1: public B0 //公有派生
{
public:
void display() { cout<<"B1::display()"<<endl; }
};
class D1: public B1 //公有派生
{
public:
void display() { cout<<"D1::display()"<<endl; }
};
void fun(B0 *ptr) //普通函数
{
ptr->display();
}
void main() //主函数
{
B0 b0, *p; //声明基类对象和指针
B1 b1; //声明派生类对象
D1 d1; //声明派生类对象
p=&b0;
fun(p); //调用基类B0函数成员
p=&b1;
fun(p); //调用派生类B1函数成员
p=&d1;
fun(p); //调用派生类D1函数成员
}
// 运行结果
B0::display()
B1::display()
D1::display()
虚函数是动态绑定的基础,是非静态的成员函数。调用方式:通过基类指针或引用,执行时会根据指针指向的对象的类,决定调用哪个函数。
10 令 operator= 返回一个 reference to *this
关于赋值,有趣的是你可以把它们写出连锁形式:
int x, y, z;
x=y=z=15;
// 同样有趣的是,赋值采用右结合律,所以上述连锁赋值被解析为:
x=(y=(z=15));
// 这里15先被赋值给z,然后其结果(更新后的z)再被赋值给y,然后其结果(更新后的y)再被赋值给x。
// 为了实现“连锁赋值”,赋值操作符必须返回一个reference指向操作符的左侧实参。这是你为classes实现赋值操作符时应该遵循协议:
class Widget {
public:
...
widget& operator=(const Widget& rhs)
{
...
return* this;
}
...
};
// 这个协议不仅适用于以上的标准赋值形式,也适用于所有赋值相关运算,例如:
class Widget {
public:
...
Widget& operator+=(const Widget& rhs)
{
...
return *this;
}
widget& operator=(int rhs)
{
...
return *this;
}
};
// 注意这份协议,并无强制性。如果不遵循它,代码一样可以通过编译。然而这份协议被所有内置类型和标准程序库提供的类型如,string vector complex,tr1::shared_ptr 或即将提供的类型共同遵守。
**请记住:**令赋值(assignment)操作符返回一个reference to *this.
11 在operator= 中处理 ”自我赋值“
”自我赋值“ 发生在对象被赋值给自己时:
class Widget {...};
Widget w;
...
w = w; // 赋值给自己
这看起来有点愚蠢,但它合法,所以不要认为客户绝不会这么做。另外并不是所有的赋值都能被一眼看出来,例如:
a[i] = a[j] // 潜在的自我赋值,如果i和j相同的话
*px=*py // 如果px和py正好指向同一对象
// 下面是operator= 实现代码,表面上看起来合理,但自我赋值出现时并不安全(它也不具备异常安全性)
class Bitmap {...};
class Widget {
...
private:
Bitmap* pb; // 指针,指向一个从heap分配而得的对象
};
Widget& Widget::operator=(const widget& rhs)
{
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}
请记住:
- 确保当对象自我赋值时 operator= 有良好行为。其中技术包括比较 “来源对象” 和 “目标对象” 的地址、精心周到的语句顺序、以及 copy-and-swap
- 确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。
12 复制对象的时候勿忘其每一个成分
举例:考虑一个class用来表现顾客,其中手工写出(而非由编译器创建)copying函数,使得外界对它们的调用会被log下来:
void logCall (const std::string& funcName);
class Customer {
public:
...
Customer (const Customer& rhs);
Customer& operator=(const Customer& rhs);
...
private:
std::string name;
};
Customer::Customer (const Customer& rhs):name(rhs.name)
{
logCall ("Customer copy constructor");
}
Customer& Custormer::operator=(const Customer& rhs)
{
logCall("Customer copy assigment operator");
name = rhs.name;
return *this;
}
这里的每一件事情看起来都很好,而实际上每件事情也的确都好,直到另一个成员变量加入战局。
class Date {...}
class Customer {
public:
...
private:
std::string name;
Date lastTransaction;
}
这时候既有的copying函数执行的是局部拷贝:它们的确复制了顾客的 name,但是没有复制新添加的lastTransaction。大多数编译器对此不出任何怨言——即使在最高警告中。这是编译器对“你自己写出copying”函数的复仇行为:既然你拒绝它们为你写出的copying函数,如果你的代码不完全,它们也不告诉你。结论很明显:如果你为class添加一个成员变量,你必须同时修改copying函数。(你也需要修改class的所有构造函数以及任何非标准形式的operator=)。如果你忘记,编译器不太可能提醒你)。
一旦发生继承,可能会造成此一主题最暗中肆虐的一个潜藏危机。试考虑:
class PriorityCustomer: public Customer {
public:
...
PriorityCusomer(const PriorityCustomer& rhs);
PriorityCusomer& operator=(const PriorityCustomer& rhs);
...
private:
int priority;
};
PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)
: Priority(rhs.priority)
{
logcall("priorityCustomer copy constructor");
}
PriorityCustomer& PriorityCustomer(const PriorityCustomer& rhs)
{
logCall("PriorityCustomer copy assignment operator");
priority = rhs.priority;
return *this;
}
PriorityCustormer 的 copying函数看起来好像复制了PriorityCustomer内的每一样东西,但是请再看一眼。是的,它们复制了PriorityCustomer声明的成员变量,但每个PriorityCustomer还内含它所继承的Customer成员变量复件(副本)。而那些成员变量却未被复制。priorityCustomer的copy构造函数并没有指定实参传给base class 构造函数(也就是说它在它的成员初值列中没有提到customer构造函数)初始化。default构造函数将针对name和lastTransaction 执行缺省的初始化动作。
以上事态在PriorityCustomer的copy assignment 的操作符身上只有轻微不同。它不曾企图修改其base class的成员变量,所以那些成员变量保持不变。
任何时候只要你承担起,“为derived class 撰写 copying函数” 的重责大任,必须很小心地也复制其base class成分。那些成分往往是private,所以你无法直接访问它们,你应该让derived class 的copying函数调用相应的base 函数:
PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)
: Customer(rhs), priority(rhs.priority)
{
logCall("PriorityCustomer copy constructor");
}
PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer& rhs)
{
logCall("PriorityCustomer copy assignment operator");
Cusomer::operator=(rhs); // 对base class 成分进行赋值动作
priority = rhs.proprity;
return *this;
}
本条款题目所说的 “复制每一个成分” 现在应该很清楚了。当你编写一个copying函数,请确保(1)复制所有local成员变量,(2)调用所有base classes内的适当的copy函数。
请记住:
- Copying函数应该确保复制 “对象内的所有成员变量” 及 “所有base class 成分” 。
- 不要尝试以某个copying函数实现另一个copying函数。应该将共同机能放进第三个函数中,并由两个coping函数共同调用。