Effective C++阅读笔记

cpp 专栏收录该内容
10 篇文章 0 订阅

1,cpp的四个特点

c, object-oriented cpp, template cpp, STL

2, 不要用 #define

使用 const, enum, inline替换define, 因为我们宁可以编译器替换预处理器。

//定义常量char
const char* const autoName = "sm";
const string authorName ("sm");

在类中定义作用域仅在类中的常量:

class GamePlayer{
private:
	static const int Num = 5;
}
//如果编译器要求看到定义式(不要在设初值):
const int GamePlayer::Num;

还有一种方法:

class GamePlayer{
private:
	enum { Num = 5};
}

同样,要用inline 替换掉 define:

template <typename T>
inline void callwithMax(const T& a, const T& b)
{
	f(a > b ? a : b);
}

3,尽可能使用const

注意下下面的几个区别:

char* p = greeting;
const char* p = greeting; // const data;
char* const p = greeting; //const pointer;
const char* const p = greeting; //both const;

4,初始化对象

cpp中的无初值初始化的case还是比较复杂的,有时候会初始化为0,有时候不会。所以建议手工进行初始化。
注意,初始化和赋值是有区别的:

class ABentry{
public: 
	public: ABentry(const std:: string & name);
private:
std::string a;
	int num;
};

ABentry::ABentry(const std:: string & name){
	a = name;
	num = 0;
} //这是赋值

ABentry::ABentry(const std:: string & name)
	: a(name),
	num(0) //这是列表初始化
	{}

通常来讲,对于内置类型,用赋值的方法,会先调用默认构造函数,然后赋值,这样做比直接使用列表初始化复杂,因为列表初始化直接拿来实参进行拷贝构造,效率高很多。当然还有其他的一些情况分析,总之建议用列表初始化,不要用赋值。当然某个特殊情况下可以单独分析,比如有好多构造函数,为了避免重复初始化工作,可以将一些值通过一个私有函数进行赋值操作。

5,cpp默认编写什么东西

编译器会默认创建default构造函数,copy构造函数和copy assignment操作符:

class Empty { };
//其实你已经做了以下操作:
class Empty{
	public:
		Empty() {...}
		Empty(const Empty& rhs) { ...}
		~Empty(){ ...}
		Empty& operator=(const Empty& rhs){... }
		};

6, 如何禁用拷贝构造函数和拷贝运算符

上面条例表明,cpp会自动生成就算你不去声明,我们的惯用伎俩就是将这两个声明为private并不去定义他们。

class Empty{
	public:
		Empty() {...}
		~Empty(){ ...}
	private:
		Empty(const Empty& rhs); //只有声明
		Empty& operator=(const Empty& rhs);
		};

或者还有个微妙的做法就是声明一个专门用来禁用拷贝的类,需要他的时候把他继承过来:

class noncopyable{
	public:
		noncopyable() { }
		~noncopyable(){ }
	private:
		noncopyable(const noncopyable& ); //只有声明
		noncopyable& operator=(const noncopyable& );
		};

class Empty:: private noncopyable{
	...
};

这里面继承关系不一定要用public, 同时noncopyable的析构函数也不一定要virtual,(因为我们并不是为了实现什么多态,就像很多STL容器作为base class使用,但并不是要实现多态),多态基类是一定要声明virtual desctrutor,下面会有条款。最后,我们还可以用boost直接引入noncopyable。

7, 为多态基类声明virtual desctrutor

具有多态性质的base class要声明虚析构,如果class带有任何virtual函数,他也应该拥有一个虚析构。

如果class的目的不是为了实现多态,或者不是为了用作base class,那就不需要虚析构。

8, 别让异常逃离析构函数

虽然不禁止析构函数抛出异常,但是最好不要这样做,比如有这样一个vector, 存放着是个类,当这个vector要被销毁时, 如果在类的析构中抛出异常, 可能当抛出第二个异常的时候,程序就会导致不明确的行为了.

解决方案是:要么强制结束程序,要么将异常吞掉.

不过这里仍然推荐一个新的方案,就是将析构函数所要做的事情重新定义一个新的函数,在析构函数中调用这个函数,并记录下对该函数的调用失败情况. 这样做就把析构所执行的功能交给了用户,当用户没有执行该功能时,析构也能调起该功能,并进行记录.

9, 不要在构造和析构函数中调用virtual函数

10,令operator = 返回一个reference to *this

通常来讲,对于运算符重载,推荐要这么做:

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

11,在operator = 中处理自我赋值

为了不让自我赋值的事情发生,我们需要证同测试。

Widget& Widget::operator=(const Widget &rhs)
{
	if(this == &rhs) return *this;
	delete pb;
	pb = new Bitshop(*rhs.pb);
	return *this;
}

12, 复制对象时要复制每个成分

在自己定义拷贝构造函数的时候,要记得为derived class撰写拷贝构造函数,这些成分往往是private的,所以应该让derived class的拷贝构造函数调用相应的base class函数、

13,以对象管理资源

在释放对象内存的过程中,可以利用auto_ptr和shared_ptr确保对象释放。对于如下的片段的释放内存方案是有风险的:

void f()
{
Investment * pinv = createInvestment();
...
delete pinv;
}

我们无法确保。。。中不会抛出什么异常导致最后的delete部分无法执行,这会带来内存泄露的风险。利用智能指针可以帮助我们解决这个问题:

void f()
{
std::auto<Investment> pinv(createInvestment());
...
}

这样做示范的是以对象管理资源的两个关键想法:
1,获得资源后立刻放进管理对象内。
2,管理对象运用析构函数确保内存释放。
但是有的STL容器是不支持auto_ptr的,他的替代品是reference-counting smart pointer. 比如tr1::shared_ptr. 两者都在其析构函数中做delete, 而不是delete[ ]. 所以注意这对动态分配而得的array身上使用这两个ptr是不行的。

14, 在资源管理类中小心copying行为

???

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

???

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

std::string* a = new std::string;
std::string* b= new std::string[];

delete a;
delete [] b; 

17, 以独立语句将newed对象置入智能指针

???

18,让接口容易被正确使用

防止误用的办法包括建立新类型,限制类型上的操作,束缚对象值,以及消除客户资源管理责任。

19,设计class犹如设计type

20, 使用传引用,不要传值

如果我们现在有这些类的声明和定义在这里:

class Person{
	public:
		Person();
		virtural ~Person();
	private:
		std::string name;
		std::string address;
};

class Student: public Person{
	public::
		Student();
		~Student();
	private::
		std::string schoolname;
		std::string schooladdress;
};

然后做以下操作:

bool validateStudent(Student s);
Student plato;
bool platoisok = validateStudent(plato);

这里做了传值操作,总计发生了六次构造和析构函数的行为。
换成传引用将避免发生这些动作。

bool platoisok = validateStudent(const Student &plato);

另外的好处在于,传引用可以避免对象切割问题:
如果有以下的类:

class Window{
	public:
	...
	std::string name() const;
	virtual void display() const;
	};

class Windowscore: public Window{
	public:
		virtual void display() const;
	};

void printname(Window w){
	std::cout<<w.name();
	w.display();
}

如果有以下的操作:

Windowscore wind;
printname(wind);

就算这里想使用的是windscore的display函数,但是传值方式会发生对象切割,给你提供的display函数属于window, 如果换成传引用,就可以自动识别传进来的类型:

void printname(Window& w){
	std::cout<<w.name();
	w.display();
}

21, 必须返回对象时候,不要返回reference

举个例子:
Ration是个类,能够通过 Ration(a,b)进行初始化。

const Ration & operator* (const Rational& lhs, const Rational & rhs){
	Rational result(lhs.n * lhs.n, lhd.d *lhd.d);
	return result;
}

这样做是危险的,因为result是局部变量,离开函数后就会被消亡,所以是无法返回的。
你可以这么做:

inline const Ration operator* (const Rational& lhs, const Rational & rhs){
	return Rational(lhs.n * lhs.n, lhd.d *lhd.d);
}

可以看一下以下的几个对比方式:

//有这么一个time类
Time::Time(int h, int m)
{
    hours = h;
    minutes = m;
}

//传递引用的overload
// Time &Time::operator+(Time &t)
// {
//     hours += t.hours;
//     minutes += t.minutes;
//     return *this;
// }

// 直接return 的方式
Time Time::operator+(Time &t)
{
    return Time(hours + t.hours, minutes + t.minutes);
}

// 实现过程中定义一个类,在把他返回出来,返回的必须是值不能是引用
// Time Time::operator+(Time &t)
// {
//     Time sum;
//     sum.hours = hours + t.hours;
//     sum.minutes = minutes + t.minutes;

//     return sum;
// }

22,将成员变量声明为private

从封装的角度来说,只有private(封装)和其他(不封装),事实上,protectd并不比public更具封装性。

23,用non-member, non-friend替换member函数

越少的函数能够对数据进行访问,数据的封装性就越好,所以我们希望将一些组合功能(相近的),比如以浏览器为例,一系列清理功能组合成一个函数,调用类中的小功能(比如清理内存,清理浏览记录等等),尽管我们也可以写一个成员函数clear_all将这些子功能包含在一块,但是更好的办法是通过命名空间将一个非成员函数定义出来,去组合这些子功能,因为这样最大程度的保有了数据原有的封装性。

24,若所有类型都需类型转换,就使用non-member函数

我们定义一个有理数类,并且重载 * 运算符。

class Rational {
	public:
		Rational(int numerator = 0, int denominator = 1);
		int numerator() const;
		int denominator() const;
		const Rational operator*  (const Rational & rhs) const;
	private:
		...
	};

可想而知,我们可以

Rational one(1,8);
Rational two(1,2);
Rational result = one * two;  // 行
result =  2 * one; // 不行

为了解决这个问题,将重载运算符定义为non-member 函数

class{
	...
};

const Rational operator *(const Rational & lhs, const Rational & rhs)
{
	return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
}

这样一来:

Rational one(1,8);
Rational two(1,2);
Rational result = one * two;  // 行
result =  2 * one; // 行

25, 考虑写出一个不抛出异常的swap

???

26,尽可能延后变量定义式的出现时间

只要你定义了一个变量而其类型带有一个构造函数或析构函数,那么当程序执行到这个变量的定义式时,你便要承受构造成本。所以,如果这个变量未被使用,就应该尽量避免他被定义,因为你白白花费了构造和析构成本。更明确的,不止应该延后变量定义,更应该尝试延后这份定义直到能够给他赋初值为止。对于循环问题,如下:

// 定义于循环之外
Widget w;
for (int i = 0; i < n; i++){
	w = i;
	...
	}

// 定义于循环之内
for (int i = 0; i < n; i++){
	Widget w (i);
	...
	}

注意上面的Widget w被直接通过拷贝构造了获得了初值,而不是先通过默认构造,然后再进行赋值行为, 如下所示。 这也是一种提高效率的办法。

Widget w;
w(i);

对于上面的循环问题,需要确定:
做法A: 1个构造,1个析构,n个赋值
做法B: n个构造,n个析构
到底那个速度快。 当然做法A让widget的作用域更大了,这也是和程序的可理解性和易维护性相矛盾的。总体上说,除非你知道 (1)你知道赋值成本比构造+析构成本低,或者(2)你正在处理效率敏感度很高的部分,否则你就应该使用做法B。

27, 尽量少用转型

???

28, 避免返回handles指向对象内部

尽量避免返回handles(reference, pointer,iterator)指向对象内部,如果一定要使用,在前面加上const,这样开放了一部分封装性的权限,但是依旧保证了对象内部数据的只读不写的特性。

29,为“异常安全”而努力是值得的

???

30,理解inline

将大多数inlining限制在小型被频繁调用的函数身上。
不要只因为function templates出现在头文件就将它声明为inline.

31, 将文件依赖降到最低

原则上就是相依于声明式而不是定义式

32,确定public继承式is-a关系

很简单,不用解释

33,避免遮掩继承而来的名称

derived class 内的名称会遮掩base class内的名称。如果依然想要使用base class 中的名称,就可以使用using名称空间,如果base class中有重载函数,而你只想要用其中的某个,则可以使用转交函数:

class Base{
public:
	virtual void mf1() = 0;
	virtual void mf1(int);
	};
class derived : private Base {
public:
	virtual void mf1()
	{ Base:: mf1();}
	};

如果要使用base class中的函数、

。。。

class derived : public Bae{
public:
	using Base::mf1;
	using Base::mf3; // 使用using引入名为 mf3的base class 中的所有函数
	};

34,区分接口继承和实现继承

???

35,

36,

37,

38,

39,

40,

  • 1
    点赞
  • 0
    评论
  • 1
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

相关推荐
©️2020 CSDN 皮肤主题: Age of Ai 设计师:meimeiellie 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值