EFFECTIVE-C++读书笔记

阴高刚
2023-12-01

2. 构造/析构/赋值运算

条款05: 了解C++默默编写并调用了哪些函数

  • 默认会为每个类创建以下的几个函数,但是特别情况下,像拷贝赋值操作符重载不一定会构建,例如给const的类成员赋值时就会拒绝使用重载符=。
// 默认生成的构造函数
class test
{

public:
     test(){} 构造函数
  
     test(const test& rhs){}  拷贝构造函数
     test& operator=(const test& rhs){} 操作符=重载

     ~test(){} 析构函数
};

//copy assign操作符有时候是会被拒绝调用的
class test
{
public:
	test(int data)
		:b(data){}
		
	const int b;
}

test A(1);
test B(2);
A = B; //报错,企图修改const成员变量,是不允许的

条款06:若不想使用编译器自动生成的函数。就该明确拒绝

  • 有些场景下我们并不希望类能够被拷贝,此时我们可以手动明确拒绝,可以将不需要生成的函数放在private中即可避免函数被外部使用
class test
{
private:
     /* data */
     test(const test &rhs)
     {
          this->data = rhs.data;
     };  
public:
     test(/* args */){};
     ~test(){};

     int data;
};

int main(){
     
     test A;
     test B(A);	// error: 'test::test(const test&)' is private within this context test B(A);

     return 0;
}

条款07:为多态基类声明virtual析构函数

  • 当设计基类时,应为析构函数加上virtual属性,因为当基类中存在虚函数时(没有虚函数也没必要设计基类了),当存在基类指针指向派生类时,就是形成多态时,如果基类的析构函数不是virtual,当我们delete 基类的指针时只会回收基类的资源并不会去回收掉派生类的资源从而导致内存泄漏。另外当派生类的析构函数不需释放内存和没有形成多态时,也没必要给基类的析构函数加上virtual属性,因为由于vptr的存在会给其生成对象大小增大,所以不需要时没必要给析构函数加上virtual。
class base
{
public:
     base(/* args */){};

     virtual void fun()
     {
          cout << "base fun" << endl;
     }

     virtual ~base()
     {
          cout << "base delete" << endl;
     };
};

class componet : public base
{
public:

     void fun()
     {
          cout << "componet fun" << endl;
     }

     ~componet()
     {
          cout << "componet delete" << endl;
     };
};

int main(){
     componet A;
     base *p = &A;
     delete p;

     return 0;
}

输出

componet delete
base delete

不加virtual,并不会调用派生类的析构,只会打印
base delete

条款08:别让异常逃离析构函数

  • 析构函数绝对不要吐出异常,如果一个被析构的函数可能抛出异常,应该要被捕捉到,然后不要传播或者程序结束
  • 如果实在需要对异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)以供操作

条款09:绝不在构造和析构过程中调用virtual函数

  • 构造和析构期间不要调用virtual函数,因为此类调用不会下降至派生类

条款10:令operator= 返回一个reference to *this

  • 针对连锁赋值的情况,赋值操作符必须返回一个指向操作符左侧的实参。例如x=y=z=15,这里的赋值顺序为x=(y=(z=15)),所以在赋值操作符结束后该返回一个实参指向自身,否则y以及x无法获得值。
class widget
{
public:
     const widget& operator=(const widget& rhs)
     {
          this->data = rhs.data;

          return *this;
     }

     int data;

};


int main(){
     widget A;
     widget B;
     widget C;

     A = B = C;	//opertor=返回*this才支持连续赋值
     (A=B) = C; //如果不加const 这里不会报错
     return 0;
}

条款11:在operator= 中处理“自我赋值”

  • 在某些情况下有些操作会将自身的值赋值给自己,例如定义一个类A, 给A赋值 A=A,这本身看起来没有什么问题,但是假如赋值操作符重载里在赋值之前释放了某些本身的指针可能就会发生意想不到的问题,例如:
class widget
{
public:
    
    widget &operator =(const widget &rhs)
    {
        delete this->a;
        this->a = new int(*rhs.a);	//此时rhs.a已经被释放了,解引用一个已释放的指针会发生意外的错误
      	return *this;  
    }
    
    int *a;   
};

int main()
{
    widget A;
    A = A;
    return 0;
}

所以我们需要在赋值操作符里处理自我赋值,修改赋值操作符重载的特殊情况如下:

class widget
{
public: 
    widget &operator =(const widget &rhs)
    {
        if(this == &rhs) return *this;
        
        delete this->a;
        this->a = new int(*rhs.a);
        return *this;
    }
  
    int *a;
};

int main()
{
    widget A;
    A = A;
    return 0;
}

条款12:复制对象时勿忘其每一个成分

  • 当手动实现拷贝构造函数或者拷贝赋值函数时,要考虑到派生类继承时的问题。如果基类本身的拷贝函数等实现的不够全面,那么就会导致派生类对应功能缺省的问题,例如:
class customer{
public:
    customer(){}

    customer(const customer &rhs)
        :name(rhs.name)
    {

    }

    string name;
    int age;

};

class priorityCustomer:public customer{
public:
    priorityCustomer(){}

    priorityCustomer(const priorityCustomer &rhs)
        :customer(rhs), priority(rhs.priority)
    {
    }


    int priority;
};

int main()
{
    priorityCustomer tmp;
    tmp.name = "bob";
    tmp.age = 10;
    tmp.priority = 1;
    priorityCustomer component(tmp);
    cout << "_name:"<< component.name << " ,_age:" << component.age << endl;

    return 0;
}

输出

_name:bob ,_age:0

这样设计的拷贝函数在派生类实现的时候会造成基类的成员变量未全部复制,造成意外的错误。所以在设计到copy类的函数时应该确保“复制对象内的每一个成员”


3. 资源管理

条款13:以对象管理资源

  • 当我们使用一个指针时,有时需要自己手动去释放。这就是动态的内存分配的管理痛处,假如在未释放之前就已经退出(例如提前调用return或者break),此时就会导致内存泄漏。所以对于获取到的指针我们可以采用智能指针的方式来进行管理。将指针交给智能指针进行管理,智能指针实现的原理就是在构造函数中获得资源并在析构函数中进行资源释放,所以指针的管理其实就是转到智能指针的类中去管理,就不需要我们自己手动进行释放,下面做一段简单的展示
class Investment{};		//定义一个要操作的类
Investment *createInvestment(){};	//假如该函数申请返回一个有效指针
pInv1 = createInvestment();
...		//如果这时候就退出了就会造成内存的泄漏
delete pInv1;

#最好的解决方法就是将该指针交给智能指针管理
std::auto_ptr<Investment>pInv1(creatInvsetment());  //这样我们只需要用并不在意在哪里去释放,因为auto_ptr这个模板类中已经实现对其管理的指针的释放

值得注意的是auto_ptr已经在C++11中被弃用了,本条只讲解资源管理的具体方式,不对几种智能指针做详细的解释 。

条款14:在资源管理类中小心copying行为

  • 主要是针对几种智能指针的特性,在执行copy时需要注意的地方,此时就得说下几种智能指针的区别
    • auto_ptr //管理权转移, C++98
    • unique_ptr //防拷贝, C++11
    • shared_ptr //共享拷贝, C++11

auto_ptr使用了一种"管理权转移"的思想,在使用拷贝或者赋值时会将原对象置为NULL, 所以在完成拷贝或者赋值后就不要再使用该指针

//auto_ptr的实现原型

template<typename T>
class auto_ptr
{
public:
	//不允许隐式构造
	explicit Mauto_ptr(T* ptr = NULL)//explicit可禁止隐式构造
	{
		_ptr = ptr;
	}
	auto_ptr(const auto_ptr& src)//拷贝构造
	{
		if (&src == this)//防止自拷贝
		{
			return;
		}
		_ptr = src._ptr;
		src._ptr = NULL;
		//这是auto_ptr拷贝构造的最大特点,新的智能指针=拷贝构造成功之后,会将原智能指针置空
	}
	auto_ptr& operator=(const auto_ptr& src)
	{
		if (this == &src)//防止自赋值
		{
			return *this;
		}
		_ptr = src._ptr;
		src._ptr = NULL;//和拷贝构造一样,赋值完成之后,会将原智能指针置空
		return *this;
	}
	~auto_ptr()//auto_ptr的析构直接调用delete就行
	{
		delete _ptr;
	}

private:
	T* _ptr;
};

int main()
{
	auto_ptr<int> ap1(new int(1));
	auto_ptr<int> ap2(ap1);	//此时会将ap1中的指针置为nullptr
	*ap1 = 2; //此时就会crash
	return 0;
}


unique_ptr 禁止进行拷贝和赋值的操作。 (一旦进行拷贝或赋值就会报错)

shared_ptr 是通过引用计数的方式来实现多个shared_ptr对象之间的资源共享。通俗来说就是允许多个shared_ptr指针之间的互相赋值,它的基本原理就是记录对象被引用的次数,当引用次数为 0 的时候,也就是最后一个指向某对象的共享指针析构的时候,共享指针的析构函数就把指向的内存区域释放掉。

    std::shared_ptr<int>p1(new int(5));
    cout << "count:"<< p1.use_count() <<endl;	//count:1
    std::shared_ptr<int>p2(p1);
    cout << "count:"<< p1.use_count() <<endl;	//count:2
    p2.reset();
    cout << "count:" << p1.use_count() <<endl;	//count:1

从上面就不难看出,shared_ptr当涉及copy时采用的就是引用计数的方式,它允许多个对象之间的拷贝,只要不是所有的对象都释放,总会保存当初指向的那个内存,就像这里new int(5)是一直会在堆上存在的的,当生成p2时只是对原有的shared_ptr对象计数加1而已,然后p2仍然指向的还是这个new int(5),同理当p2被释放时也只是计数减1,不会去释放new int(5)申请出来的内存,只有当p1也被释放的时候,计数等于0,才会去释放new int(5)的内存

另外要注意的是智能指针的构造函数默认是explict类型的,禁止隐式转换,像 std::shared_ptrp1 = new int(5)这种隐式的转换是会报错的

条款15:在资源管理类中提供对原始资源的访问

  • 这条是对智能指针的补充,当我们定义一个智能指针时,它是没法隐式转换成原始指针的,只有通过显示转换来调用原始指针,或者通过智能指针其重载过的操作符(operator->和operator*)来隐式转换获得对原始指针的内部访问。这就是我们说的在资源管理类中提供一个“取得其所管理之资源”
class component{
public:
  explicit component(int data)
  	:value(data){}
  int value;
};

component *creatComponent()
{
    component *p1 = new component(100);
    return p1;
}

void fun(component *p)
{
}

int main()
{
    std::shared_ptr<component>ptr1(creatComponent());
    fun(ptr1.get());	//通过其内部的get函数获得原始指针
    fun(ptr1);		//报错,其无法直接隐式转换成原始指针
    cout << (*ptr1).value << endl;	//其重载了operator*,可以直接访问原始指针内部
    cout << ptr1->value << endl;	//其重载了operator->,可以直接访问原始指针内部
    

条款16:成对使用new和delete时要采取相同形式

  • 对于new出来的对象要用对应的形式delete,例如在new表达式中使用[],必须在相应的delete表达式中也使用[],反过来也是如此
string *p = new int[100];
delete p;  //这样就会导致只析构了一个int类型,应该是delete []p;

条款17: 以独立语句将newd对象置入智能指针

  • 在我们new完一个新对象后如果是要交给智能指针管理时,一定要注意其调用的地方,以防智能指针无法及时的获取到该new对象,从而失去对new对象的管理而造成内存泄漏。例如:
processWidget(std::tr1::shared_ptr<widget>(new widget),priority())

在这个函数中,我们的参数执行主要有三步:

  1. 调用priority()
  2. 执行" new widget"
  3. 调用shared_ptr的构造函数

可以知道是" new widget"是最先执行的,然而调用priority() 和 调用shared_ptr的构造函数 谁先谁后在不同的编译器下处理的情况可能都有所不同,如果priority()先行调用出现了crash,则会造成的问题就是" new widget"没有交给智能指针去管理,从而导致了内存泄漏,所以当智能指针管理一个指针时一定要以一个独立的语句去管理,这样我们可以修改如下:

std::tr1::shared_ptr<widget>pw(new widget)
processwidget(pw, priority)

4. 设计与声明

条款18: 让接口容易被正确使用,不易被误用


条款19:设计class犹如设计type

  • class的设计就是type的设计。在定义一个新的type之前,请确定你已经考虑过本条款覆盖的所有讨论主题

条款20: 宁以pass-by-reference-to-const 替换 pass-by-value

  • 当一个类做形参时,最好是以const+引用的方式进行传递,而不是简单的用值去传递。这样的做法是因为当用值传递时,形参会调用拷贝构造,而引用则不会,这样就会减小开销,更加高效。同时用引用做形参时也会避免基类的切割问题
    例如:
class person{
public:
    person(){
        cout << "person()" << endl;
    };
    virtual ~person(){};
};

class student: public person{
public:
    student()
    {
        cout << "student()" << endl;
    };
    student(const student& another)
    {
        cout << "student copy" << endl;
    }
    ~student(){};
};

void fun(student s){}

int main() {
    student bob;
    fun(bob);

    return 0;
}
运行后输出: 
person()
student()
person()
student copy

如果将	void fun(student s){} 改为 void fun(student& s){},则只会输出:
person()
student()
避免了形参在获取值得拷贝构造,已经调用基类的构造

另一点的切割问题就是,当用基类做形参,传递一个派生类时也只会调用基类中的函数,并不会调用派生类中的函数,无法行成多态,例如:

class person{
public:
    virtual void test()
    {
        cout << "person.test() "<< endl;
    }
};

class student: public person{
public:
    void test()
    {
        cout << "student.test()" << endl;
    }
};

void test(person s)
{
    s.test();
}
输出:
person.test()
由于形参时基类,不是引用或者指针无法形成多态,调用的还是基类的test函数

条款21:必须返回对象时,别妄想返回其reference

  • 主要是注意在某些函数返回,如果只是返回引用,则需要考虑其指向的正确性和其所需要消耗的资源,无法用引用解决时可以考虑直接返回值
    例如绝不要返回指针和引用指向一个局部变量,或返回的引用指向static对象
1. a是个局部变量返回时会被释放
int& test()
{
    int a = 10;
    return a;
}

2. static造成了函数中就只会存在这一个对象,当它的值被改时,所有调用这个函数的都会全部被改成一样的,因为他们的引用都指向这个对象
int& test(int b)
{
    static int a = b;
    return a;
}
下面的判断会永远相等,不管传什么数
    if(test(1) == test(2))
    {

        cout << "yes" << endl;
    }

3. 会造成资源泄漏,因为没有调用delete
int& test()
{
   int *a = new int(10);
   return *a;
}

正确的写法就是直接传值,这是以int类型来讲解,假如int是个类,就是会多一步构造和析构,但这总比上面几种方法要好的多
int test()
{
    int a = 10;
    return a;
}

条款22:将成员变量声明为private

 类似资料: