目录
了解C++默认编写并调用的函数
若不想使用编译器自动生成的函数,就要明确拒绝
为多态基类声明virtual析构函数
不要让异常逃离析构函数
绝不在构造和析构过程中调用virtual函数
在operator=处理自我赋值
复制对象时不要忘记其每一个成分
这是effective C++第二章节内容:构造/析构/赋值运算
了解C++默认编写并调用的函数
没有太多需要介绍的,编译器会默认创建构造,析构,copy构造,赋值运算(当你没有显示写时,而这些函数又要被调用时)
详细介绍:类与对象——中_"派派"的博客-CSDN博客_isleapyear头文件
这里补充几点:编译器产出的析构函数是个non-virtual,除非这个class 的 base class自身声明有virtual析构函数(这种情况下这个函数的虚属性; virtualness;主要来自base class)。
一般而言只有当生出的代码合法且有适当机会证明它有意义,编译器才会生成,万一两个条件有一个不符合,编译器会拒绝为class 生出operator= 。
例如:
class person
{
public:
person(string& text, int val)
:text_(text)
,val_(val)
{
}
private:
string& text_;
int val_;
};
int main()
{
string s1("hello");
string s2("world");
person p1(s1, 20);
person p2(s2, 30);
p1 = p2; //报错
return 0;
}
这里面对象中的成员是引用,引用只能只向一个数据,它也只能在初始化列表中进行初始化。C++并不允许“让reference改指向不同对象”。也就是说对象不被直接牵扯到赋值操作内。C++的响应是拒绝编译那一行赋值动作。除非自己定义定义copy assignment操作符。
例如:
person& operator=(person& p)
{
text_ = p.text_; //这也只是赋值操作,并不是改变引用的指向
val_ = p.val_;
return *this;
}
补充:“内含const成员的classes,编译器的反应也一样。更改const成员是不合法的,所以编译器不生成赋值函数。最后还有一种情况:如果某个base classes 将 copyassignment操作符声明为private,编译器将拒绝为其derived classes生成一个copyassignment操作符,(赋值操作中,基类的成员是要调用基类的相关函数的,当基类显示写出且是不可见,编译器就不会自动生成)。
甚至基类不含成员,也会报错,类似于把基类构造函数设为私有,同样在派生类构造函数内报错,都是一些继承的语法,不再细说。。
例如:
请记住:
为驳回编译器自动(暗自〉提供的机能,可将相应的成员函数声明为private并且不予实现。使用继承base class 也是一种做法。
若不想使用编译器自动生成的函数,就要明确拒绝
常见做法:将相关函数设为私有,只声明,不定义。用c++11的关键字,这里不细说了。
另外一种做法:例如不想对象被拷贝
为多态基类声明virtual析构函数
直接看一段简单的代码:
class A
{
public:
A()
{
p1 = new int[10];
}
~A()
{
delete[] p1;
}
private:
int* p1;
};
class B:public A
{
public:
B()
{
p2 = new int[10];
}
~B()
{
delete[] p2;
}
private:
int* p2;
};
int main()
{
B* b = new B;
A* a = b;
delete a;
//......
return 0;
}
会造成内存泄漏问题,正确的做法是将父类的析构函数声明为virtual。
virtual ~A()
{
delete[] p1;
}
任何 class只要带有 virtual函数都几乎确定应该也有一个virtual析构函数。若class不含virtual 函数,通常表示它并不意图被用做一个base class。当class不企图被当作 base class,令其析构函数为virtual往往不太好。例如本书的例子:
如果int占用32 bits,那么Point对象可塞入一个64-bit缓存器中。这样一个Point对象也可被当做一个“64-bit量”传给以其他语言如C或FORTRAN撰写的函数。然而当Point的析构函数是virtual,便不能了。当有virtual函数时,对象也不再和其他语言(如C)内的相同声明有着一样的结构(因为其他语言的对应物并没有 vptr),因此也就不再可能把它传递至(或接受自)其他语言所写的函数,除非你明确补偿vptr—一-那属于实现细节,也因此不再具有移植性。
当一个没有virtual函数时,有时候也会出现一些问题。
例如:
一个类去继承string,但它的析构函数不是virtual。后面:
这就变成了上面的第一段代码的问题了。但你总不能去修改stl库的string吧。
相同的分析适用于任何不带virtual析构函数的 class,包括所有STL容器如vector, list, set, unordered_map等等。如果你曾经企图继承一个标准容器或任何其他“带有non-virtual析构函数”的class,需要小心。
补充:有时候令class带一个pure virtual析构函数,可能颇为便利,由于抽象class总是企图被当作一个base class来用,而又由于base class应该有个virtua析构函数,并且由于pure virtual函数会导致抽象class,为你希望它成为抽象的那个class声明一个pure virtual析构函数,并进行定义(派生类会进行调用)。
关于多态的一些知识,可看这篇文章:C++多态_"派派"的博客-CSDN博客
请记住:
1.带多态性质的base classes应该声明一个virtual析构函数。如果class 带有任何virtual函数,它就应该拥有一个virtual析构函数。
2.Classes的设计目的如果不是作为base classes使用,就不该声明virtual析构函数。
不要让异常逃离析构函数
C++11并不禁止析构函数吐出异常,但它不鼓励你这样做。如果你的析构函数必须执行一个动作,而该动作可能会在失败时抛出异常,该怎么办? 假设你使用一个类负责数据库连接:例如:
为确保客户不忘记在 DBConnection对象身上调用close(),一个合理的想法是创建一个用来管理DBConnection资源的class,并在其析构函数中调用close。
这便允许客户写下这样的代码:
但如果该close调用导致异常,DBConn析构函数会传播该异常,也就是允许它离开这个析构函数。那会造成问题。
解决办法:
一个较佳策略是重新设计DBConn接口,使其客户有机会对可能出现的问题作出反应。例如 DBConn自己可以提供一个close函数,因而赋予客户一个机会得以处理“因该操作而发生的异常”。
请记住:
1.析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。
2.如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作。 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么类应该提供一个普通函数(而非在析构函数中)执行该操作.
绝不在构造和析构过程中调用virtual函数
记住这个知识点:base class构造期间 virtual函数绝不会下降到derived classes阶层
看下面一段简单代码:
class person
{
public:
person(int age=20)
:age_(age)
{
show1();
}
~person()
{
show2();
}
virtual void show1()
{
cout << "person age="<<age_ << endl;
}
virtual void show2()
{
cout << "delete person age=" << age_ << endl;
}
private:
int age_;
};
class student:public person
{
public:
student(int age,int id)
:age_(age)
,id_(id)
{
}
~student()
{
show2();
}
virtual void show1()
{
cout << "student age=" << age_<<endl;
}
virtual void show2()
{
cout << "delete student age=" <<age_<< endl;
}
private:
int age_;
int id_;
};
int main()
{
student* s = new student(18,2010100);
delete s;
return 0;
}
结果:
当student对象在构造期间,会去调用person的构造函数,该构造函数有个virtual函数,但调用的是person内的版本,析构时也是一样的道理。一旦 derived class 析构函数开始执完后,对象内的derived class成员变量便呈现未定义值,所以C++视它们仿佛不再存在。进入 base class析构函数后对象就成为一个base class对象。
看本书的例子:
BuyTransaction b;这句代码就是上面提到的问题,解决办法:
请记住:
1.在构造和析构期间不要调用虚拟函数,因为这类调用从不下降至派生类(比起当前执行构造函数和析构函数的那层)。
在operator=处理自我赋值
自我赋值:发生在对象赋值值给自己。
简单例子:
上面的例子还不会出现问题。再往下看:若你使用一个class用来保存一个指针指向一块动态分配的位图:
例如:
下面是operator=实现的代码:
如果operator=函数内的*this(赋值的目的端)和rhs有可能是同一个对象。果真如此delete就不只是销毁当前对象的 bitmap,它也销毁rhs 的 bitmap。为防止这种错误,常见做法:
但它不具备异常安全性,若new可能出现错误,pb可能是野指针等问题。第二种方案:
在operator=函数内手工排列语句(确保代码不但“异常安全”而且“自我赋值安全”)的一个替代方案是,使用所谓的copy and swap技术。
或者:
将“copying动作”从函数本体内移至“函数参数构造阶段”却可令编译器有时生成更高效的代码。
请记住:
1.确保当对象自我赋值时operator=有良好行为。其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy-and-swap。
2.确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。
复制对象时不要忘记其每一个成分
直接看代码:
class Person
{
public:
Person()
{
_age = 20;
_name = "李华";
}
void Print()
{
cout << _name <<" "<<_age << endl;
}
int _age; // 年龄
string _name; // 姓名
};
class Student : public Person
{
public:
Student(int num=20123)
{
_stunum =num;
}
Student(Student& s)
{
_stunum = s._stunum;
}
int _stunum; // 学号
};
int main()
{
Student s1;
Student s2(2010100);
s2._name = "张三";
s2._age = 18;
Student s3(s2);
s1 = s2;
cout << s1._name <<" " << s1._age <<" " << s1._stunum << endl;
cout << s3._name << " " << s3._age << " " << s3._stunum << endl;
return 0;
}
结果:
上面的student 类继承了Person类的成员,但在copy构造函数时,并没有传实参给指定的base class构造函数,也就是说s3对象中的Person成员会被不带实参的Person构造函数初始化。(Person中的默认构造函数必须有一个,否则编译不通过),但上面的=是编译器默认生成的,会将s1成员值变为与s2成员值一样,不写copy 构造,让编译器默认生成,s3成员值也与s2成员值一样,可自行验证。
正确做法:
Student(Student& s)
:Person(s)
{
_stunum = s._stunum;
}
Student& operator=(Student& s) //也在Person类中加个赋值=函数。
{
Person::operator=(s);
_stunum = s._stunum;
return *this;
}
copy assignment函数与copy构造函数内有许多重复代码,但它们不应该互相调用。如果你发现你的 copy构造函数和 copy assignment操作符有相近的代码,消除重复代码的做法是,建立一个新的成员函数给两者调用。这样的函数往往是private而且常被命名为init。这个策略可以安全消除copy构造函数和copy assignment操作符之间的代码重复。
请记住:
1.Copying函数应该确保复制“对象内的所有成员变量”及“所有base class成分”。
2.不要尝试以某个copying 函数实现另一个copying函数。应该将共同机能放进第三个函数中,并由两个coping函数共同调用。