Effective c++ 读书总结 条款1 - 5
条款一:视C++为一个语言联邦
目前C++已经是个多重泛型编程语言、它支持:面向过程、面向对象、函数形式、泛型形式、元编程形式。
正是因为c++的风格多变、也导致了c++这种语言的复杂程度越来越高。一个便于我们学习的方式就是将c++语言视为多种语言的集合、在使用某一种次语言的使用只需要遵循该次语言的编程风格和规则、这样对于c++的学习和使用就会更加轻松。
c++一共包含四种次语言:
1、C语言部分:C语言是C++语言的基础、区块、语句、内置类型、指针等都是来自于C语言(没有类、没有模板、没有异常,不能重载)
2、Object-Oriented C++:这部分属于面向对象、封装、继承、多态。
3、Template C++:模板C++、这是C++泛型编程的部分、在C++中很多地方都使用template来设计接口和一些工具类、STL就是个基于template的程序库、为程序设计提供便利。
4、STL:STL就是个模板程序库、它包括一些容器、迭代器、算法、使用它可使程序设计更加便捷。
条款二:尽量以const,enum,inline替换#define
1、全局const变量替换#define定义
#define WITH_SIZE 1.6
这段代码属于预处理期需要执行的、可能被预处理器移走了、但是在编译期编译器可能从来没有见过WITH_SIZE、所以可能它没有被加入到编译符号表中、那么就会产生编译错误(但是很少出现这种情况)、由于预处理器盲目地把WITH_SIZE替换成1.6、这可能导致目标码出现多份1.6 增加了码量、而使用全局常量则不会如此、所以尽量使用const double WithSize = 1.6;、这就是以编译器替换掉预处理器。
使用const声明指针时需要注意:
const char* p = &str; 表示p所指之物不可更改、也就是 (*p)[0] = '1' 错误
char* const p = &str; 表示p本身不可再指向其他地址、也就是 p = &str1 错误
const char* const p = &str; 都不可更改、星号左侧所指之物、右侧指针本身。
2、class专属常量
#define WITH_SIZE 1.6 定义的替换在全局有效、即使WITH_SIZE被定义在class中、不存在private 修饰的 #define形式(因为完全没意义)、(可在某处使用#undef WITH_SIZE)这时可使用静态常量成员解决:
//只有静态整型常量才可以在类内直接初始化、当然char是可以的
class Test
{
private:
static const int index = 0;
static const double widthSize;
};
//static必须提供定义式、但是由于在声明时已经初始化了、所以这里不能再初始化
const int Test::index;
const double Test::widthSize = 1.6;
3、枚举类型可以类内设置其值
在有些情况一些static const 不能在类内初始化、例如上面的double widthSize;、如果使用static const作为数组的长度、由于编译器在编译阶段必须知道数组长度、那么static const的类外初始化则会在链接期完成、也就是会发生链接期错误、解决方案就是使用枚举类型:枚举常量是整型所以刚好满足数组的初始化要求:
class Test
{
private:
enum {Length = 5};
int array[Length];
};
其实枚举类型数值和#define 定义的整型数值是一样的、它们都不能被取地址、不能被赋值、不占用内存。
4、使用inline替换形如函数的#define替换
#define M(a,b) (a) > (b) ? (a) : (b)
int num = M(4,2); 则num = 4;
#define 的作用就是简单的替换、在使用函数型的宏定义时、对每一个参数必须使用小括号包围、但是还是难以保证出现错误 使用template加inline就可达到与#define一样的函数替换效果
template<class T>
inline T max(T a,T b)
{
return a > b ? a : b;
}
//内联函数适用于函数体小的、频繁被调用的函数使用、在class中定义的函数都被隐式声明为inline、但是至
//于编译器是否遵从、还是依赖于函数体的大小、如果函数体过大、即使声明为inline也不能像inline一样替换
5、有了const、enum、inline我们对预处理器的需求变低了、但是#ifdef/#ifndef这种控制编译的预处理语句、还扮演着重要的角色。
条款三:尽可能使用const
1、const的多种功能
可以用它在class外修饰global或namespace作用域中的常量、或修饰文件、函数、区块作用域中被声明为static的对象、也可修饰class内的static/non-static对象、前面也提到了修饰指针的方式。
2、使用const修饰函数参数、返回值
//两个函数都在函数体内修改参数
void fun(string str)
{
str[0] = '1';
}
void fun1(char* str)
{
str[0] = '1';
}
char fun2(const char* str)
{
return char();
}
int main()
{
fun("123"); //没毛病、修改的是一个string对象、且string的长度超过0、若是str[4] = '4'则不可
fun1("123"); //相当于传递char* p = "123";字符常量区不能修改、发生错误、
//重点是错误可以通过编译、所以当确认参数不可修改时应当将参数设置为const、这样就算在函数中无意
//执行了修改语句、则在编译期就会发现
fun2("123") = c;
//编译通过、不过没有意义对副本操作、在这行代码执行完、这个对象就被销毁了、在以值作为返回值时
//都要加上const 修饰
}
//更改
void fun(const string& str)
{//使用引用消除参数拷贝的开销、但是对于内置类型pass-by-value比pass-by-reference更好
.....
}
void fun1(const char* str)
{
str[1] = '1'; //编译错误
}
const char fun2(const char* str)
{
return char();
}
3、const成员函数
const对象只能调用const成员函数、并且const函数内不可以对成员变量进行修改、当然const成员函数的返回值应该pass-by-value或者pass-by-const-reference传递、这样可以保证不会改变成员变量。
两个函数的常量性不同也被视为一种重载、常量对象只能调用常量函数、但是对于一些总是需要变化的变量来说、即使const对象也需要调用、可以使用mutable、告诉编译器这个变量总是需要改变即使是在常量函数中、也就是允许常量函数改变mutable修饰的变量:
class Test
{
private:
char* str;
mutable size_t length;
mutable bool flag;
public:
size_t GetLength() const
{//常量对象第一次调用时、也会得到长度值
if(flag == false)
{
length = strlen(str);
flag = true;
}
return length;
}
};
4、避免const和non-const成员函数代码重复
例如使用操作符“[ ]”时:
class Test
{
public:
const char& operator[](size_t pos) const
{
if(pos > max_pos)
throw pos;
.... //其他操作
return str[pos];
}
char& operator[](size_t pos)
{//需要将*this转换为const对象、是为了调用const版的[]运算符、避免代码重复
const char& c = (static_cast<const Test&>(*this))[pos];
return const_cast<char&>(c);
}
};
//在使用这种一个函数调用另一个函数避免重复时、一定要是non-const调用const不能相反
条款四:确定对象在使用前已先被初始化
1、内置类型与复合类型的初始化
对于复合类型的初始化就交由它的构造函数来完成、只需要记得在构造函数中将所有成员都初始化就可以了、内置类型在不同的作用域内会有不同的初始值、全局的或在命名空间中的整型变量默认初始化为0、而在其他位置它的初始值是不确定的。
2、认清初始化和赋值
对于内置类型来说在变量定义时就直接赋值的为初始化:int a = 0;(初始化)、int a;a = 0;(赋值)
在定义自定义类型时、需要将所有成员放入“类初始化列表”中、才可称为初始化、
class Test
{
private:
string str;
int a;
double* p;
public:
Test();
};
Test::Test():str("123"),a(0),p(NULL) //初始化列表
{
//放在函数体中属于赋值、在函数体中赋值则会经历:默认构造->再赋值、而在初始化列表中只有:拷贝构造
//使用初始化列表效率更高、
}
如果变量是const修饰的或reference型、则必须使用初始化列表(因为不能对这些变量赋值、只能初始化)、另外、
c++有着固定的成员初始化次序、次序总是与成员声明的顺序相同、为了避免错误
({int a;char*b;} Test::Test():a(2),b(new char[a]));就是在初始化时不按顺序误认前面的变量会先被初始化而造成错误
3、不同编译单元内定义的non-local static对象的初始化次序
所谓不同编译单元是指产出单一目标文件的那些源码、基本上它是单一源码文件加上所包含的头文件。
non-local static就是指那些在global或namespace中的static变量。
现在考虑两个不在同一单元内的全局static变量(为什么一定要讨论static对象? 因为non-static对象不存在这样的问题、如果是一个non-static对象的初始化需要使用、另一个单元中的static对象、则一定是static对象先被初始化)、static对象在程序启动时就存在了直到程序结束mian函数后static对象才被销毁、:两个static对象(不同单元)的初始化的先后顺序是不能被确定的、所以在其中一个对象初始化时需要使用另一个对象时、可能发生不可预测的情况。
由于c++保证、函数内的local static对象会在函数第一次调用期间被初始化、所以如果以一个“函数调用”返回一个static对象的reference、替换直接访问一个non-local static对象则可保证、对象一定被初始化了。
总之就是以函数内的local static替换全局的non-local static对象、这也是设计模式中的单例模式的常用手法、另外、如果这个函数没有被调用那么就不需要支付构造和析构的开销、但是在面对多线程调用时、还是会发生初始化“竞速趋势”、解决方案:可以在单线程阶段、将各个类似这样的函数都调用一遍就可以了。
条款五:了解C++默默编写并调用哪些函数
1、构造、析构、拷贝构造、拷贝运算符
如果定义一个类、没有手动声明,构造、析构、拷贝构造、拷贝运算符。编译器帮你创建默认的、
编译器创造析构函数时它的virtual属性是根据当前类的base的析构是不是virtual而确定的、
而有时编译器默认的拷贝构造和拷贝运算符不能真正按照我们的意思定义:
class Test
{
private:
char str[100];
int a;
string c;
public:
Test(){}
Test(const Test& t):str(t.str),a(t.a),c(t.c)
{
//默认的拷贝构造使用这种方式、对成员变量进行初始化、而str(t.str);这会使this->str 和
//t.str会指向同一处空间、而其实我们要的是strcpy_s(str,100,t.str);、所以有时候重写拷贝构造和拷贝运算符是必要的
}
};
然而面对class中定义了const或reference类型的对象的类、因为拷贝构造和拷贝运算符传递的都是const-reference类型、如果是reference变量完成初始化、那么要是这个reference变量的值改变了参数的const reference对象也就改变了。
在使用编译器默认拷贝运算符中需要向const对象进行复制。然而这是不合法的。
面对以上两种情况、经过测试:第一种reference指向同一个对象的情况是存在的、默认生成的拷贝构造能够完成、而如果成员中使用了const或reference那么编译器不会声明拷贝运算符的、
class Test
{
public:
char& c;
const int num;
public:
Test(char& cc,int nNum):c(cc),num(nNum){};
~Test(){}
};
int main()
{
char c = '1';
Test t1(c,100);
Test t2(t1); //使用拷贝构造reference指向同一个对象
Test t3 = t1; //一样的拷贝构造
t2 = t1; //拷贝运算符、编译错误
}
下一篇: std::enable_if