C++ 常见编程技巧总结(一)

【声明】这篇博客,是我阅读《Effective C ++》这本巨作,然后根据自己的理解,加上了一些见解,代码有些是参考本书的,希望可以帮助你们理解C++ 的一些机制,关于详细情况,还是请你们观摩这本巨作。

确定对象被使用前已被初始化

有时候,读取未被初始化的值会导致不明确的行为。

对于无任何成员的内置类型,必须手动完成初始化

int x = 0;
const char* text = "A class";

double d;
std::cin>>d;

构造函数的初始化和赋值是有所区别的。赋值是指在构造函数调用之后,通过用户自己输入的值进行运算,这是赋值,而初始化一定是在调用构造函数时候就完成的
要想真正的初始化,可以使用成员列表进行初始化。初始化的效率比赋值要高

初值列中针对各个成员变量而设的实参,被拿去作为各个成员变量的构造函数的实参。比起先调用构造函数再调用拷贝构造,直接调用一次拷贝构造的效率要高很多
如果成员变量是const 或者 reference ,那就一定需要初始化而不能赋值

基类总是比派生类更早的初始化,class的成员变量总是以声明次序进行初始化,尽管它们在成员初始化列表中的顺序不一样,所以尽量保持两者顺序一致。

  • static成员变量的初始化

non-local static

static 成员寿命是从被构造出来到程序结束为止

函数内被定义的static成员称为local-static 其他的称为 non local-static

以函数调用(返回一个引用指向local static 对象)替换直接访问 non-local-static对象,就可以保证获得的那个引用指向一个历经初始化的对象

class FileSystem{};
FileSystem tfs()   //这个函数用来替换tfs对象,它在FileSystem中可能是个static对象
{
    static FileSystem fs; //定义并初始化一个static对象
    return fs;            //返回一个引用指向上述对象
}

从一个角度来看,通过函数返回static对象初始化过后的引用,使得他们成为inlining的候选人,但是另一方面函数内涵“static独享”的事实使得他们在多线程系统中带有不确定性

具体改进方法:在程序的单线程启动阶段手动调用所有的 返回引用函数,这就可以消除与初始化有关的“竞速形式”

构造/析构/赋值运算

构造函数/拷贝构造/析构函数/赋值运算符重载/这些函数都是在需要时才会被编译器创建出来

派生类的防拷贝,把拷贝构造函数声明为private,这样就可以阻止拷贝行为

**2.将拷贝构造和赋值拷贝声明为私有的,这样可以防止别人调用
但是成员函数和 friend 函数还是可以调用他们**

如果你没有声明这些函数,编译器会自动声明,所以最好还是自己声明必须好

为多态基类声明 virtual 析构函数

如果一个派生类对象经由一个基类指针被释放,而这个基类没有 virtual析构函数的话,那么就会导致派生类对象的成分没有被销毁,造成局部销毁

形成资源泄漏,败坏数据结构,在调试器上浪费许多时间

解决办法:给基类的析构函数声明为virtual ,这样就可以避免局部销毁了

虚函数(在运行期决定哪一个 virtual 函数被调用)

vptr指针指出,vptr指向一个由函数指针构成的数组,称为 vtable,每一个带有虚函数的class都有一个相应的 vtable

如果一个不想被声明为基类的类增加了虚函数,那么对象体积将会增大,一个指针在64位系统中占8个bit

一定要注意不要把一个派生类对象给基类指针,这样会引发问题

SpecialString* pss =  new SpecialString("Impending Doom");
std::string* ps;

ps = pss;
delete ps;//未有定义,现实中*ps会出现资源泄漏,因为派生类的析构函数没有调用

- 生成一个抽象基类,应该具有一个 virtual 函数(不能被实例化)

class AWOV{
    publicvirtual ~AWOV() = 0;
};

AWOV::~AWOV{}
在完成声明之后最好定义一下

异常中的析构函数

类中的数据成员应该被正确的销毁,即使是在抛出异常的情况下。

在两个异常同时存在的情况下,程序不是结束执行就是导致不明确行为

为了解决抛出异常导致程序不能正常关闭的问题,我们可以在析构函数中调用 close函数

class DBConnection{
    public:
    static DBConnection create();

    void close(); // 为了避免没有调用关闭函数,应该像下面这样做
}

class DBConn{
    public:

    ~DBConn()
    {
        db.close();
    }
    private:
    DBConnection db;
    };
这是为了防止客户没有正常调用而利用析构函数在对象结束使用时自动调用的特性
1.如果程序遭遇一个“于析构期间发生的错误”后无法继续执行,“强迫结束程序”,阻止异常从析构函数传播。
因此调用abort()可以抢先制“不明确行为”于死地

- 请记住
1.析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉各种异常,然后吞下他们(不传播)或结束程序
2.如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作

}
  • 绝不在构造和析构函数中调用 virtual 函数
    在继承关系中,如果一个派生类的构造函数被调用,那么首先会去调用基类的构造函数,基类对象构造期间虚函数绝不会下降到派生类阶层,取而代之,对象的行为就像隶属基类一样。可以这么说,在基类构造期间,虚函数不是虚函数

如果在派生类对象还没有被初始化之前就调用虚函数,结果将会是未定义的。

如果在构造函数或者析构函数中调用虚函数,那么虚函数就会用自己的一套处理机制来进行运转。虚函数会根据对象的类型进行显式地调用。假使构造函数中有一个虚函数,那么进行对象构造时,如果由一个派生类的指针 new 一个基类的对象,由于构造函数是虚函数,那么就会根据多态性,只会构造基类对象,那么初始化也就没有完成了。放在析构函数中也是类似的,会造成内存泄漏的问题

令operator= 返回一个reference to*this

关于赋值,我们可以把它写成是

x =y =z =15; 或者 x = (y = (z= 15));

为了实现连锁赋值,赋值操作符必须返回一个引用指向操作符的左侧实参。

class widget{
    public:
     Widget & operator = (const Widget& rhs)
     {
         return *this;
     }
}

在operator= 中处理自我赋值

a[i] = a[j]; //潜在的自我赋值 当 i = j 时成立
*px = *py  //当两个指针指向同一个东西时也算作自我赋值
检查是否是同一个对象必须检查它的地址而不是值,因为有可能值是一样的
所以应该这样检验
 **这种方法实现赋值运算有一点缺陷,如果先释放了 pb 指向的内存空间,但是 new新的对象时失败了,那就会造成数据丢失,得不偿失**


   if(this == &rhs)
       return *this;

    delete pb; //清楚pb这块内存空间的内容
    pb = new Bitmap(*rhs.pb); //让 pb重现指向新开辟出来的内存空间
    return *this;

另一种处理自我赋值的办法
  Widget& Widget::operator=(const Widget& rhs)
  {
      Bitmap* pOrig = pb;
      pb = new Bitmap(*rhs.pb);
      delete pOrig;
      return *this;
  }
第三种方案就是使用 swap 交换一下
 这种方法是最安全的方法。先是调用拷贝构造函数,生成了一份 rhs 的拷贝,然后通过改变指针的指向,
撤销原来的空间,而是当函数执行完出了作用域自己调用析构函数,所以很安全。

 class Widget{
     void swap(Widget &rhs);
    }    
     Widget& widget::operator = (const Widget& rhs)
     {
         Widget tmp(rhs); //生成一份临时拷贝
         swap(tmp);
         return *this;  //通过改变指针的指向来进行拷贝构造
     }

 上面使用的是 rhs的引用,下面这种就直接使用 rhs 的一份临时拷贝作为形参

 Widget& Widget::operator = (Widget rhs)
 {
     swap(rhs);
     return *this;
 }
复制对象时勿忘其每一个成分
  • 如果给class 中增加一个成员变量,那就必须同时修改拷贝函数,还需要类的所有构造函数以及任何非标准形式的operator=

  • 如果要写派生类的拷贝函数,那就必须把基类的拷贝函数也相应的写出来

PriorityCustomer::PriorityCustomer(const PriorityCustomer & rhs)
   :Customer(rhs)
   ,priority(rhs.priority)
   {
       logcall("copy");
   }
   PriorityCustomer&
   PriorityCustomer::operator= (const PriorityCustomer & rhs)
   {
       Customer::operator = (rhs);
      priority = rhs.priority;
      return *this;
   }
确保复制所有的local 对象,调用所有的 base classes 内的适当的拷贝函数

不要尝试在某个拷贝函数实现另一个拷贝函数。而是应该将共同机能放进第三个函数中,并由两个拷贝函数共同调用。拷贝函数的实现如果在继承关系中,是有先后顺序的,并且同一个对象的基类数据和派生类数据的拷贝也是需要各自进行的。为了确保拷贝的顺序永远不会错,我们必须调用各自的拷贝函数,不要怕麻烦,代码出错的话,够你调试的。

注 :

    我也是个小白,若有什么地方分析不到的,欢迎各位给我指导啊,感谢感谢,还有呢,我写博客也希望可以交到志同道合的朋友,共同进步,一起加油

猜你喜欢

转载自blog.csdn.net/zb1593496558/article/details/80420762