最近在阅读<<effective C++>>这本书, 这本书在我刚开始学习编程的时候, 我就听过这本书的大名。 不管在任何论坛和前辈的推荐中, 关于进阶C++, 总会有这本书的影子。
本章浅谈下条款1到条款4。
很好奇为什么这本书要以条款为标题。但是确实新颖, 分层很清晰。也算是这本书一大特色,挺喜欢的。
条款1到条款4重点是在介绍const的重要性, 从各个方面引入,比较, 说const各大优点,甚至在设计的使用的优点等。
1、explicit关键字
我们来看一个例子:
#include<iostream>
using namespace std;
class A {
public:
A(int a = 0, int b = 0)
:_a(a)
,_b(b)
{
}
void Print() {
cout << _a << ' ' << _b << endl;
}
private:
int _a;
int _b;
};
void fun(A object) {
object.Print();
}
int main() {
A a;
fun(10);
return 0;
}
执行结果: 10 0
fun函数的形参是一个A类, 但fun(10)确实可以执行的, 这是因为编译器优化存在隐式类型转化。 这种结果是无法预料的, 我想我们对我们的程序每一步的执行流程都是想了解的。 因此我们可以加关键字explict, 它的作用是来防止隐式类型转化带来一些问题。
explicit A(int a = 0, int b = 0)
:_a(a)
,_b(b)
{
}
当我们在构造函数前面加explicit, 我们发现刚才fun(10)出现问题了。
下面借鉴原话:explicit进制编译器执行非预期(往往不被期望)的类型转化。除非我有一个好理由允许构造函数被用于隐式类型转化,否则我会把它声明为explicit。我鼓励你遵循相同的政策。
2、使用const, enum, inline 替换 #define
在C语言中,我们定义一个常量,我们会用到#define, 对于define定义的常量我们都知道, 在预处理阶段进行替换。它存在很大的问题。
1、没有类型检查。
2、开发中不易修改程序。
书中强烈建议我们使用const, enmu来替换掉define。
1、const定义的常变量是有类型检查的, 而且使用和常量一样。enmu枚举同样!
2、class的专属常量
define定义的常量, 它是全局性的, 不会归于一个类私用的常量的。
但是const 和 enmu就不一样了。
class A {
public:
A() {}
private:
static const int numbers = 5;
int arr[numbers];
};
[]里面只能为常量, 上面的写法是成立的, 而且numbers这个常量是所有A类私有的。
class A {
public:
private:
enum { numbers = 5 };
int arr[numbers];
};
上述的写法也是可以的, 枚举定义的常量numbers, 而且是这个类私有的。
对于enmu来说,相对于const定义的常量, 它更像define, 定义的常量没有地址。
如果你不想让别人知道const定义的常变量的地址, 就可以使用enmu。
3、inline 替换 宏函数
define也是可以定义宏函数, 但是也是替换,它存在很多缺点:
1、没有类型检查, 语法分析。
2、对于参数需要加括号, 不然很容易出现问题。(这点大家有目共睹)
inline内联函数, 他会给我们编译器一个建议, 最终决定权在编译器手里面,
如果展开后,效率高,会在调用出展开, 如果效果不高, 就不会展开。
(这个主要适合代码量小, 减少函数栈的开销从而提升效率!)
由于会在调用出展开原代码,因此它会在编译阶段对于变量进行类型检查 和 语法检查。 具有安全可靠性。
总结一下: 对于定义常量cosnt, enmu 代替 define。
对于定义函数inline 代替 define。
3、const其他用法
1、这也是面试常考的点。
int a = 10;
const int * p = &a;
int * const p1 = &a;
int const * p2 = &a;
const int * const p3 = &a;
指针p的指向可以改变, 但是指向的内容不可以改变。
指针p1的指向不可以改变, 但是指向的内容可以改变。
指针p2的指向可以改变, 但是指向的内容不可以改变。
指针p3的指向和指向的内容都不可以改变。
2、STL容器中的迭代器
std::vector<int> vec;
std::vector<int>::iterator iter = vec.begin();
std::vector<int>::const_iterator citer = vec.cbegin();
iter迭代器对指向的内容发生改变, citer迭代器对指向的内容不能发生改变。
关键在于cbegin()函数返回的const引用。
3、类中const成员函数
class A {
public:
const char& operator[](size_t pos) const {
return text[pos];
}
char& operator[](size_t pos) {
return text[pos];
}
private:
char text[10];
};
上面有两个operaot[]函数, 是为不同类型对象准备。
对于const A的对象, 它的调用是不会修改类里面的test数组内容的。
因此我们在它的返回值返回const char&。
这是一种规范,如果返回得是char&, 那么就乱套了。
4、const和non-const函数代码复用
class A {
public:
const char& operator[](size_t pos) const {
//操作1
//操作2
//操作3
return text[pos];
}
char& operator[](size_t pos) {
//操作1
//操作2
//操作3
return text[pos];
}
private:
char text[10];
};
const和non-const函数的实现是一样的, 只是返回值不同而已, 如果两个都写,就相当于写了两份代码。如果函数的操作非常非常多呢?
我们采用代码复用, non-const复用const函数的代码
class A {
public:
const char& operator[](size_t pos) const {
//操作1
//操作2
//操作3
return text[pos];
}
char& operator[](size_t pos) {
return
const_cast<char&>(
static_cast<const A&>(*this)
[pos]
);
}
private:
char text[10];
};
我们只需要在non-const函数中写一个返回值就行了。
*this是对象本身, 用static_cast将对象本身添加为const属性, 然后调用eperator[], 它会自动调用const的eperator[]函数, 然后返回一个const char&类型, 我们再用const_cast去掉常量性质把它变成变量,然后返回。
从而达到代码复用的情况!
5、初始化问题
1、 对于变量声明一定要初始化,
不管是什么类型,自定义好,内置类型也罢,在c++编程中鼓励变量声明要初始化。(尤其是指针!)(因为你不知道不初始化的变量是指向什么鬼地方。)
2、对于类中成员变量的初始化
强力建议使用初始化列表进行初始化, 不管内置类型还是自定义类型。
对于内置类型:初始化列表没有什么区别。
对于部分自定类类型: 初始化可以提高效率。
举例:
class A {
public:
A(const string& name, const string& addrs, const string& phones,
const int id)
{
_name = name;
_addrs = addrs;
_phones = phones;
_id = id;
}
private:
string _name;
string _addrs;
string _phones;
int _id;
};
对于_name、 _addrs、 _phones string对象上述的赋值调用构造函数 和 赋值运算符重载函数 两步 操作完成。
在构造函数时候,首先会自动调用成员对象类的构造函数。
当构造函数完毕后,进行赋值, 会调用类的赋值运算符重载函数。
class A {
public:
A(const string& name, const string& addrs, const string& phones,
const int id)
:_name(name)
,_addrs(addrs)
,_phones(phones)
,_id(id)
{
}
private:
string _name;
string _addrs;
string _phones;
int _id;
};
当我们使用初始化列表, 对于类成员变量只会调用一个拷贝构造函数就完成初始化了。 效率提升了很多。
但对于_id内置类型, 好像没有什么提升, 但是为了统一规范, 还是建议一起使用初始化解决初始化。