目录
第一部分 语法篇
第四章 重中之重的类
建议36:class与struct的区别
C++中的struct
可以包含函数、可以继承。
- 大括号初始化
(1)struct
如果没有定义构造函数,就能用大括号初始化;否则,不能。
(2)class
只能在所有成员函数为public
,且无自定义构造函数时时,才可以用大括号初始化。 - 默认访问权限:
struct
是public
的,class
是private
的 - 继承方式:
struct
默认是public
继承
建议37:编译器对类悄悄做的事
类的3个重要组成部分:一个或多个构造函数、一个析构函数、一个拷贝赋值运算符。
当定义一个类时,编译器会隐式地生成这些方法。可以通过delete
和default
告诉编译器是否自动生成这些函数。
class A
{
A() = default; // 需要生成
A (const A&) = delete; // 禁止生成
virtual ~A() = default; // 需要生成
A & operator = (const A&) = delete; // 禁止生成
}
注意:默认的拷贝构造函数可能会出现浅拷贝的问题。
对于一个空类,为了能够实现它的实例化,编译器会强制使其大小由0变成1.
建议38:首选初始化列表实现类成员的初始化
- 初始化列表操作能避免产生临时变量,效率高
-
const
成员变量的初始化必须使用初始化列表 - 如果类B中含有类A类型的成员变量,而类A又禁止了赋值操作。此时要想完成类B的成员变量的初始化,必须采用初始化列表方式
- 需保证成员变量的定义顺序与初始化列表的顺序一致
class A
{
private:
A operator=(const A& rhs);
};
class B
{
public:
B();
~B();
private:
A mA;
};
B::B()
: mA(A())
{
mA = A(); // Error
}
如何拒绝对象的复制操作
- 防止复制对象,可以将拷贝构造函数和拷贝赋值函数声明为
private
,并且不给出实现。。 - 防止成员函数和友元函数对其访问。可以不定义对应的函数。因为声明而不定义成员函数是合法的(编译通过),但是调用未定义的成员函数,会导致链接失败。
-
private
继承boost::noncopyable
,因为这个类的拷贝构造函数和拷贝赋值函数是private的。
namespace noncopyable_
{
class noncopyable
{
protected:
noncopyable(){}
~noncopyable(){}
private:
noncopyable(const noncopyable& );
const noncopyable& operator=(const noncopyable& );
}
}
class CStudent: private boost::noncopyable
{};
建议40:自定义拷贝构造函数
- 编译器自动生成的拷贝构造函数都是浅拷贝。
- 当类中有动态申请的资源时,必须要深拷贝。
- 有继承发生时,派生类的拷贝函数必须先拷贝基类部分的数据。
class B: public A
{
B& operator=(const B& b){
if(this == &b){
return *this;
}
A::operator=(b); // 拷贝基类部分的数据。
// copy B ...
return *this;
}
}
建议41:谨防因构造函数抛出异常而引发的问题
构造函数抛出异常会引起对象的部分构造,因为不能自动调用析构函数,在异常发生之前分配的资源将得不到及时的清理,进而造成内存泄露问题。所以,如果对象中涉及了资源分配,一定要对构造之中可能抛出的异常做谨慎而细致的处理。
建议42:多态基类的析构函数应该为虚
- 只要类中包含一个虚函数,就要将析构函数声明为虚。
当通过基类的指针delete派生类对象时,如果基类的析构函数不是虚函数,那么C++不会调用整个析构链。只是调用基类的析构函数,出现“部分析构”的问题。
- 如果一个类的析构不被设计为基类,那么就不应该把析构函数设置为虚。因为会多余地产生“虚函数表”。
另外,标准库中的string、complex,以及STL容器,虚构函数非虚,不能被继承。
建议43:构造函数不能为虚
构造前,内存还没分配,无法找到虚函数表。
建议44:避免在构造/析构函数中调用虚函数
class Base {
public: Base() {
cout << "Base constructor\n";
Init();
}
virtual void Init() {
cout << "Base::Init " << endl;
}
};
class Derived: public Base {
public: Derived(): Base() {
cout << "Derived constructor" << endl;
}
virtual void Init() {
cout << "Derived::Init " << endl;
}
};
int main() {
Derived d;
return 0;
}
// output:
Base constructor
Base::Init
Derived constructor
上例中,调用虚函数,是为了实现多态,即用基类实例去调用派生类的虚函数。
但是,由于在派生类被正确地构造出来之前,调用派生类的徐成员函数是没有意义的。因为构造派生类之前,需要构造基类。因此调用基类的虚函数时,派生类的实例还不存在。
所以,基类的虚函数指向的还是自己。成员函数,包括虚成员函数,都可以在构造、析构的过程中被调用。
构造顺序:基类;派生类
析构顺序:派生类;基类
- 如果在构造函数或析构函数中调用了一个类的虚函数,那它们就变成了普通函数,失去了多态的能力。
换句话说,对象不能在生与死的过程中表现出『多态』。
C++标准规范:
当一个虚函数被构造函数或析构函数直接或间接地调用时,调用对象就是正在构造或者析构的那个对象。
其调用的函数是定义于自身类或者其基类的函数,而不是其派生类或者最底派生类的其他基类的重写函数。
建议45:谨慎使用默认参数的构造函数
不合理地使用默认参数,将会导致重载函数的二义性。
CTimer(string name, int hour=0)
{
}
CTimer(string name)
{
int hour = 11;
}
// 当使用如下方式构造时,将会产生二义性
CTimer cTimer("a");
建议46:重载
Overloading、Overriding与Hiding
- 重载,Overloading。同一作用域的不同函数使用相同的函数名,但是函数参数的个数或类型不同。
- 重写,Overriding。派生类中对基类中的虚函数重新实现,即函数名和参数都一样,只是函数的实现不一样。
-
隐藏,Hiding。派生类中的函数屏蔽基类中相同名字的非虚函数。
重载、重写、隐藏
class Printer{
private:
int mPrivData;
public:
int mPubData;
void Print(int data) {cout << data << endl;};
void Print(float data) {cout << data << endl;};
void Print(const char* pStr, size_t sz) {cout << pStr << endl;};
void SetPrivData(int data) {mPrivData = data;};
};
class StringPrinter: public Printer{
public:
//using Printer::Print; // 需要将基类的Print函数声明引入到派生类
void Print(const string& str) {cout << str << endl;};
};
int main(int argc, char* argv[]){
StringPrinter stringPrinter;
stringPrinter.mPivData = 10; // 编译错误
stringPrinter.mPubData = 10; // 没问题
stringPrinter.SetPrivData(2019); // 没问题
stringPrinter.Print(2019); // 编译错误
return 0;
}
- 派生类StringPrinter中的Print函数并不是基类Printer中Print的重载,因为它们分属于不同的作用域。
- 基类Printer中的Print被派生类的掩盖了
建议47:重载赋值操作符
- 需检查自赋值。否则容易删除自己。
- 需返回*this(自身的引用)。支持链式赋值。
str3.operator=(str3.operator=(str1))
- 返回值应该是此类的引用。
(str3=str1) = str2
- 赋值运算符不能被继承。因为继承时,编译器自动为派生类生成一个赋值运算符,进行了『隐藏』,而这个赋值运算符会存在浅拷贝等问题。除非在派生类中自己实现重写。
- 不要忘了赋值基类的部分
重载派生类的赋值运算符
ColorString & ColorString::operator=(const ColorString& rhs)
// 返回自身的引用
{
if(this == &rhs){ // 检查自赋值
return *this;
}
CString::operator=(rhs); // 赋值基类的部分
// 如果成员变量有指针,需申请内存,再复制
m_dColor = rhs.m_dColor;
return *this;
}
建议48:运算符重载
一般来说,
- 对于双目运算符,重载为友元函数。能够接受左参数和右参数的隐式转换。
- 对于单目运算符,重载为成员函数。只能允许右参数的隐式转换。
- 必须重载为友元函数的运算符:输出运算符等。
- 必须重载为成员函数的运算符:赋值运算符,函数调用运算符(),下标运算符[],指针->等。
建议49:有些运算符应该成对实现
-
==
与!=
-
>
与<
-
>=
与<=
-
+
与+=
,-
与-=
,*
与*=
,/
与/=
建议50:重载自增、自减运算符
T& operator++(); // ++前缀
const T& operator++(int); // ++后缀
T& operator--(); // --前缀
const T& operator--(int); // --后缀
建议51:不要重载operator&&、operator||以及operator,
- 重载operator&&、operator||会破坏短路求值特性
- 重载operator,时很难实现逗号运算符的『整个表达式的结果是最右边表达式的值』特性
建议52:使用inline函数提高效率
内联函数既有宏定义的效率,又保留了函数的作用域特性。
内联是一个编译时行为,因此内联函数一般定义在头文件中。
在类内部定义的函数体的函数,默认为内联函数。一般Get、Set方法会这样定义。
使用内联函数,需注意以下几点:
- 不允许出现for、switch语句,函数不能过于复杂。
- 适合1~5行的小函数
- 对内存空间有限的机器而言,慎用内联。
- 不要对构造/析构函数进行内联。因为编译器在编译期间会给你的构造函数和析构函数额外加入很多的代码。
建议53:慎用私有继承
建议54:慎用多重继承
多重继承很难维护,所以不推荐。
建议55:提防对象切片
多态的实现必须依赖指针或引用,否则会出现对象切片(Object Slicing)的问题。
如果不使用指针或引用的话(或类中没有虚拟机制),就会出现对象的向上强制转换。强制换换后,对象仅保留基类那一部分。
对象切片往往发生在派生类对象被赋值给基类对象。
class Bird
{
public:
Bird(const string& name): mName(name){}
virtual void Feature(){
cout << mName << "can fly."<< endl;
}
protected:
string mName;
}
class Parrot: public Bird
{
public:
Parrot(const string& name, const string& food)
: Bird(name), mFood(food){}
virtual void Feature(){
cout << mName << "can fly. and like to eat " << food << endl;
}
private:
string mFood;
}
void Helper(Bird bird)
{
bird.Feature();
}
int main()
{
Bird bird1("Crow");
Helper(bird1); // output: Crow can fly
Bird bird2("Polly", "millet");
Helper(bird2); // output: Polly can fly
}
输出并不想多态那样,bird2
调用Parrot
的Feature()
方法。
当派生类对象被强制转成基类对象时,发生对象切片现象,如下图:
建议56:在正确的场合使用恰当的特性
- 虚函数:空间开销和时间开销都不大
- 多重继承、虚基类:空间开销和时间开销都较大
- 运行时类型检测(RTTI):主要代价是存储type_info
建议57:将数据成员声明为private
- 不推荐用protected
建议58:明晰对象构造、析构的顺序
- 构造:先基类,后子类
- 析构:先子类,后基类
- 首先调用基类的构造函数,然后调用成员对象的构造函数
- 成员对象的构造顺序与声明顺序一致
建议59:main函数调用前程序的操作
最简单的方式:调用一个全局对象的构造函数。因为全局对象在main()
开始之前进行构造。
C语言中,全局变量的初始化处于编译期
C++中,会先调用Startup
,完成函数库初始化、进程信息设立、I/O stream产生,以及对static对象的初始化。
网友评论