Effective C++ 笔记
※ 使用C++风格
尽量用const和inline而不用#define,因为#define经常被认为好象不是语言本身的一部分。
尽量用<iostream>而不用<stdio.h>,scanf和printf很轻巧,很高效,事实上他们不是类型安全的,而且没有扩展性。因为类型安全和扩展性是C++的基石
尽量用new和delete而不用malloc和free,malloc和free(及其变体)会产生问题的原因在于它们太简单:他们不知道构造函数和析构函数。
尽量使用c++风格的注释,行注释//。旧的c注释语法在c++里还可以用,c++新发明的行尾注释不用担心嵌套问题
※ 为需要动态分配内存的类声明一个拷贝构造函数和一个赋值操作符
问题实例
解决这类指针混乱问题的方案在于,只要类里有指针时,就要写自己版本的拷贝构造函数和赋值操作符函数。在这些函数里,你可以拷贝那些被指向的数据结构,从而使每个对象都有自己的拷贝;或者你可以采用某种引用计数机制(见条款 m29)去跟踪当前有多少个对象指向某个数据结构。引用计数的方法更复杂,而且它要求构造函数和析构函数内部做更多的工作,但在某些(虽然不是所有)程序里,它会大量节省内存并切实提高速度。
※ 尽量使用初始化而不要在构造函数里赋值
从纯实际应用的角度来看,有些情况下必须用初始化。特别是const和引用数据成员只能用初始化,不能被赋值。
通过成员初始化列表来进行初始化总是合法的,效率也决不低于在构造函数体内赋值,它只会更高效。另外,它简化了对类的维护。
但有一种情况下,对类的数据成员用赋值比用初始化更合理。这就是当有大量的固定类型的数据成员要在每个构造函数里以相同的方式初始化的时候。
class manydatambrs { public: manydatambrs();// 缺省构造函数 manydatambrs(const manydatambrs& x);// 拷贝构造函数 private: int a, b, c, d, e, f, g, h; double i, j, k, l, m; }; 假如想把所有的int初始化为1而所有的double初始化为0,那么用成员初始化列表就要这样写: manydatambrs::manydatambrs() : a(1), b(1), c(1), d(1), e(1), f(1), g(1), h(1), i(0), j(0), k(0), l(0), m(0) { ... } manydatambrs::manydatambrs(const manydatambrs& x) : a(1), b(1), c(1), d(1), e(1), f(1), g(1), h(1), i(0), j(0), k(0), l(0), m(0) { ... } 这不仅仅是一项讨厌而枯燥的工作,而且从短期来说它很容易出错,从长期来说很难维护。 然而你可以利用固定数据类型的(非const, 非引用)对象其初始化和赋值没有操作上的不同的特点,安全地将成员初始化列表用一个对普通的初始化函数的调用来代替。 void manydatambrs::init() { a = b = c = d = e = f = g = h = 1; i = j = k = l = m = 0; }
※ operator= 赋值操作符
(1)在operator=中检查给自己赋值的情况
(2)在operator=中对所有数据成员赋值,当涉及到继承时,情况就会更有趣,因为派生类的赋值运算符也必须处理它的基类成员的赋值
(3)让operator=返回*this的引用
template<class t> class namedptr { public: namedptr(const string& initname, t *initptr); namedptr& operator=(const namedptr& rhs); private: string name; t *ptr; }; template<class t> namedptr<t>& namedptr<t>::operator=(const namedptr<t>& rhs) { if (this == &rhs) return *this; name = rhs.name; // 给name赋值 *ptr = *rhs.ptr; // 对于ptr,赋的值是指针所指的值 不是指针本身 return *this; // 见条款15 } 初写
class base { public: base(int initialvalue = 0): x(initialvalue) {} private: int x; }; class derived: public base { public: derived(int initialvalue) : base(initialvalue), y(initialvalue) {} derived& operator=(const derived& rhs); private: int y; }; // 正确的赋值运算符 derived& derived::operator=(const derived& rhs) { if (this == &rhs) return *this; base::operator=(rhs); // 重要!!!调用this->base::operator= y = rhs.y; return *this; }
即在继承情况下显式地调用了base::operator=,这个调用和一般情况下的在成员函数中调用另外的成员函数一样,以*this作为它的隐式左值。base::operator=将针对*this的base部分执行它所有该做的工作——正如你所想得到的那种效果。
但如果基类赋值运算符是编译器生成的,有些编译器会拒绝这种对于基类赋值运算符的调用(见条款45)。为了适应这种编译器,必须这样实现derived::operator=:
static_cast<base&>(*this) = rhs;
※ 类与函数
(1)成员函数,非成员函数,友元函数:
假设f是想正确声明的函数,c是和它相关的类:
- 虚函数必须是成员函数。
- operator>>和operator<<决不能是成员函数,
- 只有非成员函数对最左边的参数进行类型转换。如果f需要对最左边的参数进行类型转换,让f成为非成员函数。
- 其它情况下都声明为成员函数。
- 如果还需要访问c的非公有成员,让f成为c的友元函数。
(2)尽可能使用 const
过const,你可以通知编译器和其他程序员某个值要保持不变,确保这种约束不被破坏。
【记忆】==============================================
一般来说,你可以在头脑里画一条垂直线穿过指针声明中的星号(*)位置,如果const出现在线的左边,指针指向的数据为常量;如果const出现在线的右边,指针本身为常量;如果const在线的两边都出现,二者都是常量。
====================================================
(3)尽量传引用,而不是传值
为了避免频繁构造和析构昂贵的开销,就不要通过值来传递对象,而要通过引用,这会非常高效:没有构造函数或析构函数被调用,因为没有新的对象被创建
因为explicit构造函数不能用于隐式转换
(4)必须返回一个对象时不要试图返回一个引用
(5)避免对指针和数字类型重载 : 二义性
(6)划分全局名字空间