笔记4
本章是关于类和运算符重载的。
(之前写了两个多小时,然后接了个电话,回来什么东西都没有了,CAO......)
1. class和struct的区别。
这个是一般面试里面也会有的题目,之前看到的回答是关于访问权限的,struct默认是public的,而class默认是private的。
其它还有:在继承方面struct默认是public继承,而class默认是private继承。
另外在初始化的时候在大括号初始化方面也有些差别,但是因为C++11中已经统一了,具体也就不提了。
2. 编译器会创建默认的成员函数。
这个在c++笔试题总结1中的16题中已经提到了。
另外,在C++11标准中提供了两个关键字default和delete来控制创建或者不创建默认的成员函数:
class CLS {
public:
//使用默认的构造函数和析构函数
CLS() = default;
virtual ~CLS() = default;
//不要默认的赋值运算符和拷贝构造函数
CLS & operator=(const CLS &c) = delete;
CLS(const CLS &c) = delete;
};
3. 类成员的初始化首选初始化列表。
原因有以下几个:
1) 对于const和引用成员,只能使用初始化列表的方式;
2) 某些赋值操作被禁用的成员也只能使用初始化列表;
3) 某些情况下初始化列表更加高效,至少也不会表赋值操作那种方式差;
4) 由于编译器可能创建默认赋值运算符,而这种默认的情况并不一定满足要求,这就可能存在隐患。
另外还提了一下初始化顺序的问题,需要注意,初始化顺序由类内成员的声明顺序决定,而不是初始化列表中的顺序决定。
4. 某些情况下不能使用对象的复制,那么就要果断的屏蔽复制构造函数和赋值操作符。
具体的做法有几种:
1) 声明成private的。这种方法有个问题就是成员函数和友元还是能够访问;
2) 只声明不定义。这个是可以的,但是如果使用了就会在链接时报错,间接地告诉用户不要使用;
3) 定义一个不能复制和赋值的基类(使用上述的方法),然后自定义的类就继承该类,这个方法已经在Boost库中使用。
5. 注意自定义拷贝函数。
赋值运算符和拷贝构造函数说到底都是一个拷贝,因此统称为拷贝函数。
默认的拷贝构造函数都是浅拷贝,意思是将类中的非static数据成员都复制一份。这样就会产生一个问题,如果类中有指向其它资源的成员,那么浅拷贝后,这个资源就会有两个类中的成员指向它,这就可能导致问题。
因此就有了深拷贝的概念,即资源也被复制了一份。
深拷贝需要自定义拷贝构造函数,这个时候就需要特别的注意。
6. 注意构造函数出错的情况。
构造函数也会出错,但是因为构造函数没有返回值,因此C++标准建议出错时抛出异常。
但是这样也还有问题,比如一个类在构造函数出错前就就已经分配了资源,如果直接抛出异常,那么这些资源就不会被释放,导致内存泄漏。
为了解决这个问题,有几个方法:
1) 在抛出异常(try...catch)之前先释放资源;
2) 使用init()和release()函数,在构造函数中调用init()函数,来完成构造对象的代码,这个函数可以有返回值,如果返回错误就调用release()。
7. 多态基类的析构函数需要是virtual的;构造函数不能是virtual的。
这个就不多说了。
8. 不要在构造函数和析构函数中调用虚函数。
因为这个时候的虚函数没有多态性。下面是一个例子:
#include <iostream>
using std::cout;
using std::endl;
class Base {
public:
//使用默认的构造函数和析构函数
Base() {
cout << "Base constructor" << endl;
func();
}
virtual ~Base() = default;
virtual void func(void) {
cout << "Base func" << endl;
}
};
class Drived : public Base {
public:
Drived() :Base() {
cout << "Drived constructor" << endl;
}
void func(void) override {
cout << "Drived func" << endl;
}
};
int main() {
Base *b = new Drived();
return 0;
}
打印的结果如下:
Base constructor
Base func
Drived constructor
请按任意键继续. . .
注意这里打印的是Base func,即调用的是基类的虚函数func()。
9. 构造函数也可以有默认参数,但是需要注意。
下面是一个错误的例子:
#include <iostream>
#include <string>
using std::string;
using std::cout;
using std::endl;
class CLS {
public:
CLS(int a, string s = "") : age(a), name(s) {
}
CLS(int a) {
age = a;
name = "";
}
private:
int age;
string name;
};
int main() {
CLS *c = new CLS(10);//报错
return 0;
}
此时的new CLS(10)使编译器不知道要调用哪个构造函数了。
10. 搞清楚重载,重写和隐藏。
重载主要是用来函数上,即同一作用域内,函数具体有相同的函数名和不同的参数。
重写是继承关系中,子类对父类的虚函数的重新实现。
隐藏也是在继承关系中,它是只子类和父类具有相同名字的非虚函数,此时子类的函数就将父类中的同名函数屏蔽掉。
11. 重载operator=有标准的三步走。
1) 一定要检查自赋值,即operator=的实现中要首先保证=两边不是同一个东西(尤其是C++引入引用之后,更容易出现=两边是一样的东西的情况),所以这里还要注意opeartor==的重载,不然=两边的比较可能没有意义,或者是错误的。
2) 最后返回*this。
3) 重载的operator=并不会被继承。这是因为编译器会创建默认的版本,隐藏了父类的重载版本。
所以重载opeartor=的结构一般是下面的的形式:
CLS & CLS::operator=(const CLS &c) {
//自赋值检查
//释放原有的空间,申请新空间,数据复制
//返回*this
}
12. 重载运算符应该是类的成员函数还是友元函数。
将重载运算符作为成员函数和友元函数的差别:
1) 作为成员函数,则默认的一个运算操作数就是this,因此将重载运算符作为成员函数时,双目运算符只有一个参数,单目运算符没有参数;而作为友元的话,参数和运算操作数是对应的;
2) 由于成员函数一个操作数是固定的,因此不能有隐式转换了,而对于友元的话,双目运算符两边的操作数都可以有隐式转换。
一般的规则是:双目运算符用友元函数;单目运算符用成员函数。
当然例外很多,比如:
1) =,函数调用符(),下标运算符[],指针->都和this关系密切,会是成员函数;
2) <<的第一个操作时必须是ostream类型,所以<<只能是友元函数。
13. 某些运算符要成对重载。
比如==和!=,>和<,>=和<=。
另外,某些双目运算符比如A+=B,可以避免A被多次运算,效率上更高。一般+=和+也会成对重载,且为了避免代码重复,operator+就用operator+=来实现。
14. 自增自减有前后缀的差别,因此重载时也要注意。
class CLS {
public:
CLS & operator++(); //前缀
const CLS operator++(int); //后缀
CLS & operator--(); //前缀
const CLS operator--(int); //后缀
};
注意这里的const,表示了CLS++++这样的形式是不对的,跟内置类型int i; i++++会报错保持了一致。
15. 不要重载|| && 和逗号“,“。
原因是这些运算符的左右两个的执行顺序没有办法控制,就会导致与原来的这些运算符的特性不符。
16. 合理使用inline函数来提升效率。
几点说明:
1) inline函数跟宏一样也是代码替换,但是它又遵守函数的类型和作用域规则。
2) inline一般要与函数的定义放在一起使用,光跟声明放在一起没有用。
3) 类内部定义函数体,无论是否使用inline关键词,该函数都是inline的。
几点注意:
1) 编译器要不要内联还是它自己决定的,对于过于复杂的函数,即使写了inline,也可能不会真正内联。
2) 内联也会导致代码膨胀。
3) 不要内联构造函数和析构函数。
17. 慎用私有继承。
私有继承将父类中的所有成员都变成了private,这样父类中的内容就只能作为子类的实现细节,而不能作为子类的接口。
公有继承是is-a的关系,而私有继承就跟is-a没有关系了,只是子类想要用到父类中的某些算法。
大部分情况下可以用组合来替代私有继承,下面是一个例子:
#include <iostream>
using std::cout;
using std::endl;
class Engine {
public:
void start() {
cout << "Engine start" << endl;
}
};
//私有继承版本
class Car1 : private Engine {
public:
void move() {
start();
}
};
//组合版本
class Car2 {
public:
Car2(Engine *e) {
this->engine = e;
}
void move() {
if (NULL != engine) {
engine->start();
}
}
private:
Engine *engine;
};
int main() {
Car1 *c1 = new Car1();
c1->move();
Engine *e = new Engine();
Car2 *c2 = new Car2(e);
c2->move();
return 0;
}
当然也有一些情况下只能使用私有继承,而没有办法使用组合,比如:
1) 子类要访问父类的受保护的成员时。
2) 需要重定义继承了的虚函数时。
18. 不要使用多重继承。
19. 小心对象切片。
这里切面的原因是没有使用对象的指针或者引用,而是使用了对象本身。下面是一个例子:
#include <iostream>
#include <string>
using std::string;
using std::cout;
using std::endl;
class Bird {
public:
Bird(const string &name):_name(name){}
virtual string Feature() const {
return _name + "can fly";
}
protected:
string _name;
};
class Parrot : public Bird {
public:
Parrot(const string &name, const string &food) : Bird(name), _food(food) {}
virtual string Feature() const {
return _name + "can fly and eat " + _food;
}
private:
string _food;
};
void DescribeBird(Bird b) {
cout << b.Feature() << endl;
}
int main() {
Bird bird("Crow");
DescribeBird(bird);
Parrot parrot("Polly", "millet");
DescribeBird(parrot);
return 0;
}
打印的结果如下:
Crowcan fly
Pollycan fly
请按任意键继续. . .
显然不是我们想要的结果。
这是因为多态必须依靠指向同一类族的指针或者引用来实现。
修改上面的DescribeBird()函数:
void DescribeBird(Bird &b) {
cout << b.Feature() << endl;
}
问题就解决了。
20. 将数据成员声明为private。
21. 如果要在main函数之前执行某些操作,可以把他们放在全局对象的构造函数中。
下面是一个例子:
#include <iostream>
using std::cout;
using std::endl;
class CLS {
public:
CLS() {
cout << "CLS before main()" << endl;
}
};
CLS cls;
int main() {
cout << "main()" << endl;
return 0;
}
执行的结果是:
CLS before main()
main()
请按任意键继续. . .