Effective C++ (条款1到3)

条款一:将C++视为一个语言联邦

C++起源于C,最初的名称为C with Classes,意为带类的C语言,然而,随着C++的不断发展和壮大,在很多功能上已经远远超越了C,甚至一些C++程序员反过来看C代码会觉得不习惯。

C++可以看成由四个部分组成,分别是:
(1) C。C++从语法和代码风格上以C为基础,这也正是C++命名的由来,是在C的基础上++。有意思的是,在《C++ Primer》这本书里有一个问题,问为什么C++叫“C++”而不叫“++C”呢?原因很简单,C++具有深厚的C的底蕴,++C也许得到的是D,象征着另一种语言,而C++返回的还是C,表明它是站在C这个巨人的肩膀之上的。

(2) 面向对象。面向对象编程是C++不同于C的显著部分,C++引入类和对象的概念,对事物提供了很好的抽象途径,在一个class里面不仅仅可以像C中的结构体一样定义成员变量,而且可以提供方便快捷的成员函数,而不用像在C的结构体中用函数指针来实现。

(3) 模板。C++的模板,亦即泛型编程堪称一绝,有了模板,就不用手工提供所有类型的重载版本了,而交由编译器自行生成,大大减少了代码的冗长。

(4) STL。STL是优秀的C++模板库,里面集成了大量实用的库函数,比如string,vector,list,map等(唯一可惜的是没有提供hash相关的库,同时也要留意一些接口的不一致,比如string大量使用index作为接口,而vector等其他容器则是用iterator作为接口的)。

条款二:尽量以const, enum, inline替换#define

用const和enum替换不带参宏

宏定义#define发生在预编译期,而const,enum定义的常量发生在编译期,两者的重要差别在于编译期里的变量是进符号表的,而预编译期的宏是简单的替换,不进符号表。因此,const, enum定义的常量具有以下优势:

(1)支持类型检查

(2)支持访问权限

第(1)条优势,其实在Visual Studio编译器也已经对宏也引入了类型检查了,但不是所有的编译器都这样;第(2)条优势是指可以把这些常量放在类中,从而避免了全局的作用域,而宏定义只能是全局的(全局变量在多线程中容易出问题,一份优秀的代码里是几乎不出现全局变量的),所以这条优势其实是const和enum替换宏定义最好的理由。在书中还提到了,用const和enum定义的常量名还会出现在编译器的提示信息里,而宏定义在编译器中显示的就不是宏定义名了,而直接是一个数字量了,这样就不方便调试了。

那么什么时候用const,什么时候用enum呢?const适合于单个常量,比如double const PI = 3.1415926,而enum适合于一组相关的常量,比如星期:

enum DayOfWeek
{
    Sunday,
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday,
};

这段枚举定义了Sunday = 0, Monday = 1, …, Saturday = 6(去掉DayOfWeek也行,这时就定义了匿名的枚举类型了)。

用inline替换带参的宏

不带参的宏还说得过,带参的宏本身就是一个坑,可谓是bug四伏,一个不小心,就掉坑里了。举个最简单的例子:

#define square(a) a * a

在main函数中调用时,可能会这样square(3),计算的是3的平方,这是OK的,但如果换成是square(1+2),计算的还是3的平方吗?注意这时编译器解释成1 + 2 * 1 + 2,这样2 * 1的优先级高一些,所以先做了,这就出问题了。

好的,这是常见的错误,有些程序员可能认为多加括号就行,比如:

#define square(a) (a) * (a)

这的确可以避免上面所说的优先级的问题,但万一调用时是这样写的呢?

int main()
{
    int v = 3;
    squre(++v);
}

本意是想求4的平方,但编译器会翻译成(++v)*(++v),这样v就被加了两次,这个结果肯定不是你想要的!

一句话,带参的宏很容易出问题,特别是针对复合操作(一句话做了不止一件事情)时,bug频出。

解决这个问题的方法是:用函数替换!因为函数调用时,若实参是表达式,总是会先计算这个表达式的值,再去进行调用的,不会出现宏展开时的bug。

template <class T>
T square(const T& v)
{
    return v * v;
}

就是一种比较好的解决方案(注意这里v不用加括号了,也不用担心参数被求值多次,而且提供了可靠的作用域限制),但函数调用有一个保存现场和恢复现场的过程(本质是栈的压入和弹出),频繁地调用会导致性能低下,解决方法是在函数前面加上inline关键字,像这样:

template <class T>
inline T square(const T& v)
{
    return v * v;
}

这样就告诉编译器了,我想牺牲空间换时间——把这段函数体部分直接替换到原代码中,就不要保存现场和恢复现场了。但这里注意,inline并不是强制的,就算你用了inline,编译器也不一定100%地进行代码替换,比如递归函数,编译器会直接忽略掉你加的inline。所以,inline只是向编译器建议进行代码内联而已,inline适合于函数体本身短小(无递归)且频繁调用的场景。

条款三:尽可能使用const

const是常量的意思,它可以定义一个不可改变的量,主要用于以下几个地方:

  1. 修饰变量,使之不可改变
    举个例子:
    const int var = 3;
    此时var的值就不能改变了。也正是因为const的变量不能轻易修改存储的值,所以在声明的时候就要初始化,这样就是不行的:
    const int var;
    编译器就会报错。

  2. 修饰指针
    指针是特殊的变量,有时我们希望对它所指向的对象操作,而有时我们又希望对指针本身进行操作。同样,const应用于指针也有两个含义:一个是指向常量(指向的内容不可更改),一个是常量指针(指针的指向不可更改)。看下面这两个例子:

const int* p = &a;
/* p为指向常量的指针,即p指向的对象是常量,不可以通过*p = 3 来修改a的值,但这时p = &b换个指向还是可以的 */
int* const p = &a; 
/* p为常量指针,即p的指向不可更改,不可以通过p = &b来修改p的指向,但这时*p = 3改变a的值还是可以的 */
const int* const p = &a;
/* p为指向常量的常量指针,p的指向以及指向的对象都不可以更改,无论是*p = 3,还是p = &b都是错误的 */

还有一种形式是int const p,这种形式是表示常量指针,还是指向常量的指针呢?Effective C++给出的建议是看“”的位置,当const位于星号左侧时,const修饰的是值,即表示指向常量,而当const位于星号右侧时,const修饰的是指针,即表示常量指针。所以int const *p等价于const int *p,你想对了吗?(关键看const修饰的是*p还是p)。

const有时还会修饰函数的形参或者函数的返回值,都是属于1或2这两种情况。修饰函数形参的用法:

void fun(const char a)
{
a = ‘d’; // 错误,因为的值不可以改变
cout << a; // OK
}

还有一个地方要注意一下,若有:

void fun1(const char* a)
{
cout << a << endl;
}
void fun2(char *a)
{
cout << a << endl;
}

当实参为const时,比如const char* msg = “hello”,此时fun1(msg)是可以的,但fun2(msg)会报编译错,说是无法将const char*转成char*;而当实参为普通变量时,比如char* msg = “hello”fun1(msg)和fun2(msg)都是OK的。这是为什么呢?因为当const的变量传递给非const的变量会不安全(非const的变量可以修改原来定义为常量的东西了!),所以C++限制了这种用法(需用强制类型转换来告诉编译器,编译器才会放行);而反过来,当非const的变量传递给const变量时,不存在安全问题,所以C++编译器总是会放行的。因此,如果在函数体内确实不改变形参a的值,那么采用带const的fun1的写法会更好,适用性更强。

3. 修饰迭代器

C++的STL中使用迭代器作为接口,它定义了普通的迭代器,如vector<T>::iterator,也定义了指向常量的迭代器,如vector<T>::const_iterator,初学者可能想当然地认为const vector<T>::iterator等价于vector<T>::const_iterator,其实不是这样的,const vector<T>::iterator表示这个迭代器的指向不可以更改,即表示的是常量迭代器,而vector<T>::const_iterator表示指向常量的迭代器。

4. 在类中修饰成员函数

const放在类中成员函数的后面,表示这个成为函数不会修改类的成员变量,比如:

class A
{
private:
int a;
double b;
public:
void fun1() const;
void fun2();
};

注意这里的fun1()函数后面有一个const,表示这个函数不会修改类的成员变量(在它的函数体里面出现任何改变a或b的值的操作,均不能通过编译);另一方面fun2()函数后面没有const,表示这个函数可能修改类的成员变量,注意这里用的词是“可能”,fun2()可以修改也可以不修改,但为了增强安全性,所以良好的编程风格一般会把不改动成员变量的成员函数修饰为const的。

有一点要切记:有无const是可以构成成员函数的重载的!

在本书中还提到了一个尖锐的问题,如果假定类是这样的:

class B
{
private:
int* p;
public:};

我们看到,类的成员函数是指针,假定它在构造函数时会被初始化,而指向一段内存空间。那么如果不改变p本身(即指向不变),但是改变了p指向的内容(比如*p = 3),这样到底算不算对成员变量进行改动了呢?

读者可以在VS环境中写一下测试用例,可以发现VS编译器对这种情况是放行的,*p = 3完全可以通过,但是p = &b就不可以了。

虽然编译器是放过你了,但这也许并不是你的本意,本书中推荐的是“从逻辑上看”,就要交由写代码的你去好好思量一下。如果在某个函数里确实改动了p所指向的内容,那么最好就不要加上const;反过来,如果加上了const就不要改变成员变量,包括它所指向的值。

在const和非const成员函数中避免重复
我觉得这是一个非常重要的内容,有没有加const是构成函数重载的,但通常这种重载的相似度很高,就用书上的例子:

class TestBlock
{
private:
string text;
public:

const char& operator[](size_t position) const
{

return text[position];
}

char& operator[](size_t position)
{

return text[position];
}
};
可以看到两个重载函数里面的操作都是一样的,别因此认为可以用ctrl+c,ctrl+v而省事了,如果你要改动其中一个函数体里的内容,另一个就要同步更新,而万一你忘记了更新,后果是非常严重的!

一个好的方法来实现同步——在非const的函数中调用const函数!这样来修改:

char& operator[] (size_t position)
{
return const_cast

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值