Effective C++ 55 个具体做法(1-12)

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使用案例分析

  1. 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

  1. 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修饰不变量

  1. 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设初值,然后立刻再对他们赋予新值。

	**总是使用成员初值列**

请记住:

  1. 为内置型对象进行手工初始化,因为C++不保证初始化他们
  2. 构造函数最好使用成员初值列,而不要在构造函数本体内使用赋值操作。初值列列出的成员变量,其排列次序应该和它们在class中的声明次序一致。
  3. 为免除 跨编译单元之初始化次序的问题,请以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析构函数仞内含一个“双保险调用”)

请记住:

  1. 析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们或结束程序。
  2. 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么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;
}

请记住:

  1. 确保当对象自我赋值时 operator= 有良好行为。其中技术包括比较 “来源对象” 和 “目标对象” 的地址、精心周到的语句顺序、以及 copy-and-swap
  2. 确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。

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函数共同调用。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值