实现

item26:postpone variable definitions as long as possible.
定义对象将带来两个操作:调用构造函数和析构函数!必须需要使用对象时再定义对象才是高效的!对于一个抛出异常的函数来说(如下图),早早定义的对象可能还没被使用到函数就抛出了异常,白白浪费了构造函数、析构函数的调用!
在这里插入图片描述修改一下吧!
在这里插入图片描述
定义对象时,相较于先是调用默认构造函数再为对象赋值来说,直接调用合适的构造函数进行初始化以省去赋值操作效率更高!
在这里插入图片描述

总结定义对象时的两个注意点:最好是在使用前、能确定初始化初值后再来定义对象!即可免去不必要的构造函数、析构函数调用,也可省去默认函数的调用!

对于循环时使用的对象,该是在循环内定义呢?还是在循环体外定义呢?需要分别合计一下赋值运算和构造+析构运算的成本!通常来说,除非明确得知赋值操作的成本小于构造+析构函数调用成本,否则还是使用在循环内定义对象更好。(作用域更小)
在这里插入图片描述
在这里插入图片描述
item27:Minimize casting
转型语法:
1、C风格的转型
在这里插入图片描述
2、C++风格转型
在这里插入图片描述
const_cast<>(expression):C++转型风格中唯一支持去除对象的常量性的转型操作符。
dynamic_cast<>(),向下转型,决定可否将对象类型转化为派生类对象类型,是唯一一个旧式转型运算符无法实现的。
static_cast<>(expression):执行多种转化,包括将指向基类的指针转化为指向派生类对象的指针(不安全,强制转化),但无法将const对象转化为非cosnt对象!
用C++风格的转换操作符取代C风格转换操作符是明智的!
转型的一些特别之处:
1、转型就不是编译器将某种类型对象视为另一个类型就可以了,编译器会为其增加相应的支撑代码,如下图:派生类对象包含一个偏移量(有些编译器是这样的),以获取其包含的基类对象地址,因此一个对象身上可能多个地址(多重继承),因此pb和 &d的地址是不同的。
在这里插入图片描述
2、对象做转型操作后产生的是副本!如下图所示,对this指针指向的对象做类型转换得到该类型的一个新对象,并在该新对象调用函数。因此,在新对象副本上的修改,与对象本身的修改不是同一件事,将导致基类对象数据成员和派生类对象成员的分离。
在这里插入图片描述
正确的写法:调用父类方法,传递当前对象的指针!而不要做类型转化!
在这里插入图片描述
何时使用dynamic_cast<类型>(expression)?仅有指向基类类型的指针,但我们希望当其指向派生类对象时能调用其派生类中的方法,这个时候一定要将其进行类型转化才能调用派生类对象的方法!dynamic_cast<>()的效率太低。。能避免使用这个运算符一定要避免。有两种方式可以避免dynamic_cast<>()的使用!
在这里插入图片描述
方法一:直接使用指向派生类对象的指针
在这里插入图片描述
方法二:将派生类对象的方法加入到基类中
在这里插入图片描述
上述两种方法能用就用!但是在应用派生类和基类时,一定要避免写出级联的dynamic_cast<>()!后期维护很麻烦。。效率低。。不妨使用在基类中添加虚函数的方法来解决!
在这里插入图片描述item28:avoid returning “handles” to object internal
handle:指针、引用、迭代器
为啥选用handle呢,而不是复制数据成员并返回?出于效率角度!可是返回对象数据成员的handle,会带来一些问题!

一、数据成员的封装性:取决于返回指向数据成员的handle的函数的访问控制权限,把数据成员声明为私有的,可是却在一个公有方法中返回指向数据成员的handle,那么调用者在类外通过调用这个方法就可以得到这个成员,相当于这个成员就是公有的,可以任由客户修改,一旦将成员方法是const的,那么就与设计初衷相矛盾。
可是真是的场景是我们就是需要返回一个数据成员,但我们不希望客户可以修改他们,只读不可写!
在这里插入图片描述
修改:
在这里插入图片描述
二、悬空的handle
引用指向一个函数返回值的内部成员!
函数返回值是一个临时对象,在临时对象上获取内部成员极有可能产生错误。
这里提出这个观念的目的是,客户有可能误用类设计所编写的返回数据成员引用类型的函数,,设计者在编程时也要注意这个问题!作为设计者我们还是尽可能少的编写返回这种指向对象内部数据成员的函数为好!

在这里插入图片描述item29:strive for exception-safe code
标准程序库中很多函数都可能会抛出异常,如果我们忽视可能产生的异常,对我们的程序将产生怎样的影响呢?**我们在编程时需要考虑异常带来的影响,不能让其破坏我们资源和数据。**如下所示,一旦产生了异常,导致资源不被释放,对象指向一个被释放的空间,将会给程序带来未定义的行为。
在这里插入图片描述
C++提出异常安全代码的概念,需要代码做到发生异常后,不会泄露任何资源,不败坏数据。不泄露资源,使用资源管理类,这可使得无论函数正常与否,资源类对象释放的同时都会释放其所包含的资源。
异常安全的函数提供以下三个保证之一:
1、程序只保证在异常后,数据仍处于有效状态,但客户是无法提前得知状态值。
2、程序保证,发生异常后,数据处于异常前的状态,不发生异常情况下,数据将根据调用函数做出改变。
3、绝不抛异常,所有操作均能正常进行。
将上面这个不安全的函数改为异常安全函数,凡是涉及资源都使用资源管理类或是智能指针,不直接进行指针的释放,利用智能指针所实现的机制,在这里,只有当资源分配成功后,调用智能指针的reset方法,释放旧指针所指向空间,否则,指针指向的空间不被删除不被修改,确实修改了背景图像,才更改对象的值:
在这里插入图片描述
常用copy and swap技术来保证函数强异常安全性,即创建需修改对象的副本,在副本上尝试修改,修改时不发生异常,则将修改的副本与原对象进行swap操作,否则,原对象还是原对象。可以将需要更改的数据抽离到一个类内,而在原类中使用一个智能指针指向该类的对象!而在类中进行修改时,只需要拷贝智能指针改变该指向的数据值,最后再将修改后智能指针与原对象智能指针进行交换。(感觉shared-ptr那里写的有点问题,感觉对智能指针的使用底层东西还不是很清楚)
在这里插入图片描述在这里插入图片描述item30:understand ins and outs of the inlining
以函数本体替换函数调用操作,隐式和显式写法:定义在类中,定义在类外使用inline声明。
如何函数较长,可能导致代码膨胀。
声明为inline函数只是一种对编译器进行的一种请求,当代码太长,或者是将虚函数声明为内联函数时,编译器完全有可能拒绝这类请求。
内联函数需要在头文件中定义,因为需要在编译期间使用函数本体替换调用操作。
template function也需要在头文件中定义,因为编译期间需要具体化template function!之前在这里踩过坑!
对于inlined的函数,编译器还是有可能再为其生成一个outlined函数本体。比如希望获取内联函数的地址,内联函数哪有地址可言?因此编译器会为该内联函数再生成一份outlined函数本体并取其地址!因此,调用内联函数有可能调用的是inlined也有可能不是inlined!
在这里插入图片描述
不要试图将构造函数和析构函数声明为inlining函数!有可能编译器会为构造函数添加代码。
inline函数的弊端,一旦修改inline函数将导致客户端的程序重新编译;在调试期间,无法设置断点。
合理使用inline函数:除非必要的或是对调试影响不大的函数可以声明为inline!使用内联的目的是通过代换函数本体来提高程序效率,因此我们真的想提高效率,不妨将频繁使用的代码进行内联化!
在这里插入图片描述
item31:minimize compliation dependencies between files
关于编译:
仅仅对.cpp文件进行编译,不对.h进行编译!
#include预处理命令以头文件内容代替#include命令后再对.cpp文件进行编译!
cpp文件编译obj文件后再进行链接!但是为了cpp可以编译通过,在调用其他源文件的函数前,需要提前声明(加入符号表),告知编译器需要寻找哪些函数(根据符号表),这就是为什么需要提前声明的原因。
想想程序库有那么多的函数,难道在使用前,要一条条的添加到程序中吗?但这些声明又必须要有,有什么简便的方法吗?有!头文件!将函数声明写入到头文件,利用#include命令等同于函数声明,引入头文件意义所在,以一个#include代替省去很多的函数声明语句、类定义。
头文件里面可以写什么?
由于头文件被引入到多个编译器单元后需要被多次编译!因此,不可以在头文件中定义对象,定义函数!
对于对象来说,const对象很特殊,由于其全局性,因此可以在头文件中定义,对多个文件不会造成干扰!
对于函数来说,模板函数很特殊,由于需要在编译时期就需要在调用的处替换为函数本体!因此,编译器允许在头文件定义模板函数。
对于类来说,头文件可包含类的定义,因为创建类对象的时候通过类的头文件(确实能够表达出类对象的大小,若是类的成员时某个类的对象,必须包含该类的头文件,这就有可能导致文件依赖,当成员对象所属类的头文件修改时,这个类的声明也需修改,凡是使用到这个类的对象的文件都需要重编译)中来确定类对象的大小,因此可在类中定义成员变量,但此时并不需要为成员对象分配空间,只有在实际创建类对象的时候才分配大小!
如果头文件仅仅有声明语句,那么一个cpp文件包含多次这个头文件也不会有问题,声明可以在cpp出现多次,但是对于上面三种能够定义的种类来说,若是头文件被被包含多次,同一个编译器将含有多次定义,这是编译器不允许的情况,如何解决这个问题呢?
创建头文件的时候,使用条件编译,这使得编译器编译cpp文件时,决定是否编译头文件内容。

讲完了头文件、文件编译,就该来谈谈文件间的编译依存关系了。
上面说由于需要在类的定义式中定义数据成员时明确指出数据类型,这将使得包含成员的类型的头文件!但凡是成员类型的头文件修改,那么这个类及其被使用的文件都需要重编译。那么思考一下,定义类的时候,能不包含类成员的头文件,而只是声明个类成员的类型嘛?不能!!!声明类型,不能指出类型所占内存空间的大小!类的定义式必须明确指出各个数据成员所占内存大小!
但是我们如果只是需要一个指向某种类型的指针或是引用,就可以使用指针!因为指针的大小是确定的,因此可以只是使用类型的声明而不用类的头文件!而使用类的声明就不会导致编译文件依赖,这个类型的头文件爱怎么改都不会影响当前类!(当前类不需要重编译)
对于C++类来说,类的头文件变化了,所使用到类的地方都需要修改,类的实现文件变了头文件没变,那么使用到类的文件也不需要重编译!

使用传统方法定义类:头文件中进行类的定义,包含数据成员的定义式和方法接口,将导致一旦数据成员修改,那么使用到类的文件都需要重编译(接口实现放入到编译单元中,改变不会影响到类使用的文件进行重编译)!就为解决数据成员修改带来的文件编译依赖文件,可采用两种方式!
handle class(handle通常意味着指针或是引用变量)或是interface class。被客户使用的类的头文件不包含任何可能需要修改的头文件!

handle class处理方式:
相当于“复制”给客户使用的类,接口一模一样,包含数据成员的定义式!给客户使用类定义式,不包含任何实际数据成员而是包含一个指向实现类的指针,其接口实现就交由实现类来实际完成。给实现类再封装一层!有什么改动,使用实现类,而客户使用的类不用修改。

class Person
{
private:
    string name;
    MyDate birthday;
    MyAddress address;

public:
    // fallows functions
    // ...
};

修改为:
// 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->GetName();
}
string Person::GetAddress() const
{
    return MemberImp->GetAddress();
}

// PersonImp.h
#ifndef PersonImp_H
#define PersonImp_H

#include <string>
#include "MyAddress.h"
#include "MyDate.h"
using namespace std;

class PersonImp
{
private:
    string Name;
    MyAddress Address;
    MyDate Birthday;

public:
    string GetName() const
    {
        return Name;
    }

    string GetAddress() const
    {
        return Address.ToString();
    }

    string GetBirthday() const
    {
        return Birthday.ToString();
    }
};

#endif /* PersonImp_H */
// MyDate.h
#ifndef MY_DATE_H
#define MY_DATE_H

#include <string>
using namespace std;

class MyDate
{
private:
    int Year;
    int Month;
    int DayOfMonth;

public:
    string ToString() const;
}
#endif /* MY_DATE_H */
// MyAddress.h
#ifndef MY_ADDRESS_H
#define MY_ADDRESS_H

#include <string>
using namespace std;

class MyAddress
{
private:
    string Country;
    string Province;
    string City;
    string Street;

public:
    string ToString() const;
};

#endif /* MY_ADDRESS_H */

interface class处理方法:和handle class 类的处理方法一样,提供给客户使用的类,不能包含任何可能修改的头文件!而实际实现的类通过继承来实现,并在interface class(接口都是纯虚函数,具体的实现在派生类)中定义一个静态方法用于返回一个指向派生类的基类指针给客户,使得客户可以使用派生类的方法!在这里派生类不就是handle class中实现类(和客户类的接口要一样),这里实现的方法通过派生类保证!客户端使用基类指针调用派生类的方法,派生类方法怎么变,完全不影响基类指针、引用!

// 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
#include “Person.h”
Person* Person::CreatePerson(string name, const MyAddress& addr, const MyDate& date)//就是指定这个派生类,作为类的设计者,我很明确的知道哪个派生类是这个虚基类的具体实现,就是要写死,一开始还在想可以有多个派生类,这里要多个派生类干嘛呢,我只不过是想减少依赖性,把这个类的具体实现放在另一个地方。
{
    return new RealPerson(name, addr, 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);
…
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值