Effective C++

重载operator new 和 operator delete(写了自己的operator new,就要写自己的operator delete)

首先我们需要注意的是new操作符和operator new是不一样的。(new、delete操作符不能重载)

string *ps = new string("Memory Management");这里的new是new操作符,功能是分配内存然后调用构造函数初始化内存中的对象。

new操作符为了分配内存所调用的函数是operator new,函数operator new 通常这样声明:

void * operator new(size_t size);

void* A :: operator new(size_t size)

{

    //void *temp = :: operator new(size);//调用全局 operator new函数, 开辟空间,一般自定义new 的目的,是用自己定义的内存管理函数,替换系统内存管理函数

                                                              //(一般是调用、malloc函数),这里偷个懒,直接调用,   :: operator new.是没打算自己写的内存管理函数

    void* temp = malloc(size);

    if(temp!=NULL)

    {

        cout << "开辟空间成功!" << endl;

    }

    return temp;

}

void operator delete(void *temp){ 

    free(temp);

    temp = NULL;

 }

(注意条件:不同参数)当重载了不同参数的operator new,如果不定制对应的operator delete(), 则绝不会调用默认的operator delete函数来释放内存,这里就会导致内存泄露。因此在为某个class 定制 operator new函数的时候,应该定制对应版本的operator delete(); 这里对应版本的参数是对应的;(这里也就是 operator delete(void * ptr, int flag);的主要用途,当构造函数异常的时候,它负责清理已申请的内存)。

比如static void * operator new(size_t size, int flag)与static void operator delete(void * ptr, int flag),类中写了自己的operator new,就要写自己的operator delete

详见C++练习文件夹的 重载operatornew文件

当我们在类中增加 带多个参数的operator new函数 的时候,在这个函数中想要调用全局的::operator new来分配空间(都会使用这一步分配空间),对不起,这样不行,创建对象调用带多个参数的operator new函数的时候(创建对象调用的是这个函数,隐藏了全局的operator new函数),在这个函数中会不经意地阻止了对标准new(全局的operator new函数)的访问。我们可以在类中再定义一个只有一个参数(size_t size)的operator new函数

static void * operator new(size_t size)

{ return ::operator new(size); }

在这个函数中调用全局的::operator new。

上述调用的次序为:先调用类中operator new函数,然后调用构造函数。


为需要动态分配内存的类声明一个拷贝构造函数和一个赋值操作符

当类中有指针而且构造函数为这个指针new出内存空间时,我们要自己重载赋值运算符和拷贝构造函数来避免可能发生的两种问题。

比如现在有一个Stringgg类只有char *data这一个成员,我们需要在构造函数中new出空间来进行赋值,而且在析构函数中delete data,接下来:

1.如果使用的是默认的赋值运算符,Stringgg s1("hello");Stringgg s2("world"); 在调用s1 = s2的时候,只会将s1的data指针指向s2的data指针的地址(两个指针指向同一个地址)。这种情况下,原来s1的data指针指向的内容就不能被删除了,而且当对s2调用析构delete data后,s1的data指针指向的内容也没有了。所以我们需要自己定义赋值运算符避免这个问题:

  stringgg &operator =(stringgg &s)

  {

      cout<<"operator ="<<endl;

      if(this != &s)

      {

          if(data != NULL)

              delete data;//先把本类中的指针给删除了

          int len = strlen(s.getdata());

          data = new char[len+1];

          strcpy(data,s.getdata());

      }

      return *this;//注意返回的是指针

  }

2.如果使用的是默认的拷贝构造函数,在调用在调用Stringgg s3 = s2的时候,同样也会遇到上述问题。只会将s3的data指针指向s2的data指针的地址(两个指针指向同一个地址)。所以我们需要自己定义拷贝构造函数来避免这个问题(和上边代码区别不大):

stringg(stringg &s)

  {

      cout<<"operator copy"<<endl;

      if(this != &s)

      {

          int len = strlen(s.getdata());

          this->data = new char[len+1];

          strcpy(this->data,s.getdata());

      }

  }

构造函数初始化成员与在构造函数中进行赋值操作

我们知道,在定义一个引用的时候必须要进行初始化,const修饰的变量不能更改。

当类成员是一个引用或者是一个const修饰的变量得时候,不能在构造函数中进行赋值操作,必须使用构造函数初始化。即:

当类中成员为 int x;int &rt;const int pi;(指针为 int *const ptr)

A::A(int x1):x(x1),rt(5),pi(3.14){}

抛开上述条件限制,就算类成员不是const或者是引用类型,也推荐用成员初始化列表,因为这样效率更高。

比如类成员为 string s;当在构造函数中使用赋值操作的时候,会调用string类的默认无参构造函数和operator=函数,当使用成员初始化列表时,只调用拷贝构造函数。


初始化列表中成员列出的顺序和它们在类中声明的顺序相同

template<class t>

class array {

public:

  array(int lowbound, int highbound);

private:

  vector<t> data;               // 数组数据存储在vector对象中// 关于vector模板参见条款49

  size_t size;                  // 数组中元素的数量

  int lbound, hbound;           // 下限,上限

};
template<class t>

array<t>::array(int lowbound, int highbound): size(highbound - lowbound + 1),lbound(lowbound), hbound(highbound),data(size)

{}

比如这个类,我们是不知道成员size的大小的,类成员是按照它们在类里被声明的顺序进行初始化的,和它们在成员初始化列表中列出的顺序没一点关系。用上面的array模板生成的类里,data总会被首先初始化,然后是size, lbound和hbound,所以正确的书写顺序应该是:

private:

  int lbound, hbound;   

  size_t size;               

  vector<t> data;     

为什么这样呢?

我们知道,对一个对象的所有成员来说,析构函数的调用顺序与其在构造函数中的构造顺序是相反的,如果按照成员初始化列表中的顺序来构造,那么对每一个实例化出来的对象,编译器都得跟踪去初始化顺序,这样显然是不合理的。

除此之外我们需要知道:类中不同访问类型的数据成员的内存分布未必与声明的一致,比如先写了private,然后写protected,但是C++的编译器可能会将protected data members放在private data members前面存储。


当有继承存在的时候,一定要将基类析构函数定义成虚的

当有继承存在时,基类指针指向派生类对象,通过基类指针删除派生类的对象,而基类有没有虚析构函数时,结果将是不确定的,可能格式化硬盘、可能给老板发email。我们遇到的一般情况下都是只调用基类析构函数而不调用派生类的析构函数。

当派生类指针指向派生类对象,我们又定义了一个基类指针,使基类指针指向派生类指针指向的派生类对象(跟上边情况不太一样),再去delete基类指针,就算基类的析构函数是虚的,还是会出现“结果不确定”的情况。

当声明抽象类的时候需要一个纯虚函数,当然这个函数可以是析构函数。但是有一点需要注意:最底层的派生类的析构函数先被调用,然后是各个基类,也就是说,就算是抽象类,编译器也要对其析构函数调用,所以我们要保证为它提供函数体(哪怕是只写一个{}) A::~A() {}

既然虚函数这么好,为什么我们不将析构函数默认为虚的呢?

如果一个类不作为基类使用,其析构函数被声明成了虚函数,指向虚函数表的指针会增加类对象的大小,而且,这个对象看起来再也不具有和其他语言如c中声明的那样相同的结构了,因为这些语言里没有vptr。所以,用其他语言写的函数来传递这个对象也不再可能了,除非专门去为它们设计vptr,而这本身是实现的细节,会导致代码无法移植。


避免对指针和数字类型重载

当我们定义两个同名函数,其参数一个为int、一个为string指针,当传入一个void类型指针的时候,因为这个指针可以转换为各类型的指针,所以会出现二义性。

void f(int x)

{

    cout<<"int"<<endl;

}

void f(string *ps)

{

    cout<<"string"<<endl;

}

int main()

{

    void *aaa;//void*指针可以转换为各类型指针

    f(0);//int

    //f(aaa);//会出现二义性。

    f(static_cast<string *>(aaa));//强制类型转换,没问题

}

所以,我们最好避免对指针和数字类型的重载。

如果不想使用隐式生成的函数就要显式地禁止它

我们知道,C++会帮我们写默认构造函数、析构函数、拷贝构造函数,赋值运算符等,但是有时候我们不想让类有这些功能。比如说:

double values1[10];

double values2[10];

values1 = values2; // 错误!

values1对象与values2对象不能这样赋值,但是作为对象,C++会默认写赋值运算符的。可以用这种方式来禁止:

private:

// 不要定义这个函数!

Array& operator=(const Array& rhs);

把赋值运算符函数定义成private,(单单这一步是不行的),并且不去实现这个函数。 这样一旦写了 对象1 = 对象2 这样的式子,就一定会报错。


避免返回内部数据的句柄

意思就是,在一个类中,我们定义了一个私有属性,那么在public成员函数中,要避免返回这个私有属性,因为这样可能会改变这个私有属性(当返回指针或者引用的时候)。比如:

class stringg {

public:

  stringg(const char *value)

  {

      int len = strlen(value);

      data = new char[len+1];

      strcpy(data,value);

  }

  ~stringg()

  {

      delete data;

  }

  operator  char *() const// 转换stringg -> char*;

  {

      return data;

  }

  void print() const//这里我们需要注意,const对象只能调用const结尾的函数

  {

      cout<<data<<endl;

  }

private:

  char *data;

};

int main()

{

    const stringg b("hello world");      // b是一个const对象(这里我们需要注意,const对象只能调用const结尾的函数)

    char *str = b;               // 调用b.operator char*()

    strcpy(str, "hi mom");       // 修改str指向的值

    cout<<str<<endl;//输出hi mom

    b.print();//输出hi mom

}

但是我们也可以避免这个问题:从 operator char *()函数入手,让其返回值不能被修改。

 operator  const char *() const// 转换string -> char*;

  {

      return data;

  }

int main()

{

    const stringg b("hello world");      // b是一个const对象

    char const *str = b;               // 调用b.operator char*(),必须是const类型的,const *p 只能赋给const *pp.

    //strcpy(str, "hi mom");       //这样的话,这句话就会出错,修改了const类型

    cout<<str<<endl;

    b.print();

}

返回局部对象与返回局部对象的引用、返回函数内部用new初始化的指针的引用

class ReturnAnObject {

public:

    int num = 55;

};

ReturnAnObject returnAnObjectFunc() {

    ReturnAnObject obj;

    obj.num = 88;

    return obj;

}

int main() {

    ReturnAnObject obj;

    obj = returnAnObjectFunc();

    cout<<obj.num<<endl;;//88,也就是说对象obj获得了那个局部对象。为什么呢?

    return 0;

}

局部对象在函数被调用的时候创建,存储在栈区,函数结束后被释放,如果存在返回值,那么当前对象也会被释放。只不过在释放前,做了一次拷贝,拷贝到了对象obj(新的对象被赋值,旧的对象被回收)。

不要返回局部对象的引用和函数内部用new初始化的指针的引用

不能返回局部对象的引用是因为对象在函数结束后立马被销毁了,返回其引用,谁知道会返回什么东西。

不能返回new初始化的指针是因为人们常常会忘了去删除这个指针。(写下面的是为了知道如何去返回这个指针并delete)

inline const rational& operator*(const rational& lhs, const rational& rhs)

{

  rational *result =new rational(lhs.n * rhs.n, lhs.d * rhs.d);

  return *result;

}

const rational& four = two * two;      // 得到废弃的指针;将它存在一个引用中

delete &four;   // 得到指针并删除

如果实在要返回,那就返回一个新的对象:

inline const rational& operator*(const rational& lhs, const rational& rhs)

{

      return rational(lhs.n * rhs.n, lhs.d * rhs.d);

}

char* getMemory()

{

    //char p[] = "hello";//str能被成功赋值,因为函数返回后栈释放。

    char *p = "hello";//str能被成功赋值,因为此时字符串存储在文字常量区,该区的生存周期为整个程序执行期间。

    return p;

}

void test()

{

    char* str = NULL;

    str = getMemory();

    cout<<str<<endl;

}

所以说,返回局部变量的指针,且指针指向常量区,可以,返回new出来的指针也可以。但是返回数组或者返回指向数组的指针不行。


将文件间的依赖性降至最低(句柄类)

一般,我们在一个类的实现(.h文件)中用到其他类(下边这种情况),都会include这个类的.h文件,殊不知,这样大大增加了文件之间的编译依赖性。即修改一个.h文件,凡是include这个.h文件的都要重新编译(项目中遇到的就是这样)。

class Person {

public:

  Person(const string& name, const Date& birthday,

         const Address& addr, const Country& country);

  virtual ~Person();

  ...                      // 简化起见,省略了拷贝构造

                           // 函数和赋值运算符函数

  string name() const;

  string birthDate() const;

  string address() const;

  string nationality() const;

private:

  string name_;            // 实现细节

  Date birthDate_;         // 实现细节

  Address address_;        // 实现细节

  Country citizenship_;    // 实现细节

};

有一种解决方法,就是使用声明,我们仅仅在定义Person类的时候去声明需要用到的类,即下边这种方法.

class string;         // "概念上" 提前声明string 类型

                      // 详见条款49

class Date;           // 提前声明

class Address;        // 提前声明

class Country;        // 提前声明

class Person {

public:

  Person(const string& name, const Date& birthday,

         const Address& addr, const Country& country);

  virtual ~Person();

  ...                      // 拷贝构造函数, operator=

  string name() const;

  string birthDate() const;

  string address() const;

  string nationality() const;

};

但是如果使用这种方法,

int main()

{

  int x;                      // 定义一个int

  Person p(...);              // 定义一个Person (为简化省略参数)

}

看到x的定义时,编译器知道必须为它分配一个int大小的内存,然而,当看到p的定义时,编译器虽然知道必须为它分配一个Person大小的内存,但怎么知道一个Person对象有多大呢?我们没法知道。但是我们可以在main函数中使用指针,即Person *p;

现在,我们用PersonImpl类(主体类)来实现之前我们想让Person类实现的东西,让Person类作为一个句柄类,作用是在Person类中使PersonImpl(主体类)中的方法去运行。

如下方所示,左边是Person.h,右边是Person.cpp。这样不管其他类如何更改,Person类都不管不问。只在主体类PersonImpl中去include那些需要用到的String、Date类。

[图片上传失败...(image-b2b4f1-1560668653157)]

image.png
           [图片上传失败...(image-759f28-1560668653157)]
image.png

定义某个类型的引用和指针只会涉及到这个类型的声明(不用include),定义此类型的对象则需要类型定义(include)参与。

声明一个函数时,如果用到某个类,是绝对不需要这个类的定义(include)的,即使函数是通过传值来传递和返回这个类。


明白公有继承与私有继承(组合关系)

我们知道,公有继承是is-a形式。公有继承声称:对基类对象适用的任何东西 ---- 任何!---- 也适用于派生类对象。如果基类是矩形,子类是正方形,那么在基类中我们写一个改变面积的方法,这个方法只增加宽,显然,这在子类正方形中是不合适的,所以用公有继承来表示它们的关系只会是错误。

私有继承是has-a,从基类私有继承来的成员都成为了派生类的私有成员,即使他们在基类中是保护的或者是共有的。 私有继承意味着“用....来实现”,这样做是想利用基类中的某些代码,并不是因为基类与派生类之间有什么概念上的关系。比如下边:

class GenericStack{

pritected:

    GenericStack();

    void push(void *object);

    void *pop();

    bool empty() const;

private:

    ........

};

template<class T>

class Stack:private GenericStack{

public:

    void push(T *objectptr)

    {

        return GenericStack::push(objectptr);

    }

    T *pop()

    {

        return static_cast<T *>(GenericStack::pop());

    }

    bool empty() const

    {

        return GenericStack::empty();

    }

};

int main()

{

    Stack<int>  s;

}

需要注意的是,这里GenericStack类的成员函数是保护类型的,所以不能在stack类中声明一个GenericStack类对象(分层)来使用GenericStack类中的代码。因为protected方法和属性都不能通过对象访问。

class parent{

    public: int a;//老爹名字

    protect: int b;//老爹银行卡

    private: int c;//老爹情人

}

若是public继承: 子类内可以访问 a,b,不能访问c。 子类外可以访问a,不能访问b、c。

若是protected继承: 子类内可以访问a,b.不能访问c。 子类外abc都不能访问。

若是private继承: 子类内可以访问a,b.不能访问c。 子类外abc都不能访问。


什么是接口继承?什么是实现继承?(纯虚函数、虚函数、普通函数的异同)

接口继承,就是继承父类没有定义的函数(没有定义意思就是只声明了,没有实现),也就是纯虚函数。纯虚函数最显著的特征是:它们必须在继承了它们的任何具体类中重新声明,而且它们在抽象类中往往没有定义。把这两个特征放在一起,就会认识到: 定义纯虚函数的目的在于,使派生类仅仅只是继承函数的接口。当然,我们可以在纯虚函数中写一个实现,但是没有什么意义,因为抽象类不能实例化。不对,这样说不对,因为纯虚函数的实现有一个用处,就是对于它的实现,可以让子类不用写代码去继承,如下边代码部分:

实现继承就是继承父类定义的函数,也就是普通的虚函数。

非虚函数表示一种特殊性上的不变性,所以它决不能在子类中重新定义。

声明一个除纯虚函数外什么也不包含的类很有用。这样的类叫协议类(Protocol class),它为派生类仅提供函数接口,完全没有实现。

class Airplane {

public:

  virtual void fly(const Airport& destination) = 0;

  ...

};

void Airplane::fly(const Airport& destination)//注意实现方式

{

  飞机飞往某一目的地的缺省代码

}

class ModelA: public Airplane {

public:

  virtual void fly(const Airport& destination)

  { Airplane::fly(destination); }//飞机A和B具有相同的飞行方式,那就可以直接调用父类纯虚函数的实现。但是子类中还是必须要重写fly函数的,如果父类不是纯虚函数,那么子类可以不写。

  ...

};

class ModelB: public Airplane {

public:

  virtual void fly(const Airport& destination)

  { Airplane::fly(destination); }

  ...

};

class ModelC: public Airplane {

public:

  virtual void fly(const Airport& destination);

  ...

};

void ModelC::fly(const Airport& destination)//我们不想用父类纯虚函数的实现

{

  ModelC飞往某一目的地的代码

}       

子类中重新定义父类的非虚函数会怎样?子类中重新定义父类虚函数但是参数默认值不一样会怎样?

首先需要知道,这两种行为都是不正确的行为。

class Base{

public:

    void test1()

    {

        cout<<"Base"<<endl;

    }

};

class Derived:public Base{

public:

    void test1()

    {

        cout<<"Der"<<endl;

    }

};

int main() {

    Derived d;

    d.test1();//Der

    d.Base::test1();//Base,如果子类中没有重新定义test1(),那么调用d.test1()将会调用从父类继承而来的test1.

    Derived d;

    Base *p = &d;

    p->test1();//Base

    Derived *dd = &d;

    dd->test1();//Der

//如果写类D时重新定义了从类B继承而来的非虚函数mf,D的对象就可能表现出精神分裂症般的异常行为。也就是说,D的对象在mf被调用时,行为有可能象B,也有可能象D,决定因素和对象本身没有一点关系,而是取决于指向它的指针所声明的类型。

}

class Base{

public:

    virtual void test1(int a = 5)

    {

        cout<<"Base:"<<a<<endl;

    }

};

class Derived:public Base{

public:

    virtual void test1(int a = 10)

    {

        cout<<"Der:"<<a<<endl;

    }

};

int main() {

    Base *b = new Derived;

    b->test1();//Der:5,按常理,我们想调用的是Derived类中的函数(参数是10),但是却调用了Base类中的参数。

}

关于C++标准库

类似#include<。。。.h>类型,是旧的头文件,没有命名空间。

类似#include<iostream>类型,包含的基本功能和旧的一样,但头文件的内容在名字空间std中。它的实现几乎都是模板。

具有C库功能的新C++头文件具有如<cstdio>这样的名字。它们提供的内容和相应的旧C头文件相同,只是内容在std中。


©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容